Skip to content

fix(metadata-protocol): a dependent package's same-named row no longer replaces the base package's item on the read naming no package - #28304

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-boxqo3
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-boxqo3

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread
Part of #28238
Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么: 不指定包读取一个视图(以及分层读取的 effective)时,若某个代码包自带这个名字,就从该代码包取内容:它自己的存储行,否则无包覆盖行(Setup 写的那种,仍然优先于代码),否则代码原件。依赖包用同名存的那一行不再顶替基础包。
  • 为什么改: showcase 实测:依赖包 com.default.acme-custom 存了 2 列的 showcase_task.default,不指定包读取就答 2 列,还挂着基础包的 _packageId;Studio 按基础包读看到 7 列,用户看到 2 列。
  • 风险与回滚: 只改读取取内容的地址;不指定包的保存、删除和读取返回的 version 仍落在依赖包那一行(写地址不动,已钉住)。回滚即还原 packagelessContentPackage 的两个调用点。
  • 先看: packages/metadata-protocol/src/protocol-environment-rows.ts 的 packagelessContentPackage;protocol.ts 里 readMetaItem 的 contentPackageId 一段;新测试 packages/objectql/src/protocol-packageless-read-dependent-package.test.ts。
  • 两分钟验证: pnpm --filter @objectstack/metadata-protocol build && pnpm --filter @objectstack/objectql exec vitest run src/protocol-packageless-read-dependent-package.test.ts,应见 5 passed。
  • 需要你做的: 卡片还差一半没修:console 应用仍渲染 2 列,因为它读的是不带包的视图列表(每个包一条、同名取最后一条)。这一半要你在卡片的决策里拍板,本 PR 只修三个按名读取。

What changes

The by-name read (getMetaItem) and the layered read (getMetaItemLayered) that name no package now take their stored content from packagelessContentPackage (new, protocol-environment-rows.ts). It adds the code packages that ship the name as candidates beside the stored rows' bindings:

readMetaItem applies it to the active rows it already read; packagelessReadAddress (the layered read's address) applies it after packagelessAddress, and its host gains shippedArtifactsOf. packagelessAddress itself is untouched, so the package-less save, delete and the read's version resolve exactly as before. Three content spreads now test !== undefined, so null reaches findServedOverlayRow as the package-unbound row.

Files: protocol-environment-rows.ts, protocol-lock-version-gates.ts, protocol.ts (the readMetaItem lines, the packagelessReadAddress host and docblock, two spreads in getMetaItem and one in getMetaItemLayered), the new test, a patch changeset.

Tests

packages/objectql/src/protocol-packageless-read-dependent-package.test.ts, over the real ObjectQL engine, its SchemaRegistry and an in-memory driver. The dependent package is installed through the package door with dependencies: { com.example.showcase: '*' }, and its row is written through the draft save and the per-item publish naming it, as the card's steps wrote it.

  1. The card's three reads answer base / dependent / base (6 / 2 / 6 columns), the package-less answer carries _packageId: com.example.showcase, the layered read's effective is the base's with overlay: null, and the list scoped to the base serves the base's columns.
  2. The write address does not move: packagelessAddress still answers the dependent package, a package-less save writes the dependent package's row and mints no other, and the read naming no package still serves the base.
  3. CONTROL: a writable package's own item (a name no code package ships) is served to the read naming no package, with its own _packageId (metadata-protocol: a save naming a package beside an existing package-less row of the item mints a second row, and the read naming no package keeps serving the unbound one (S6, reachable through today's doors) #23291).
  4. CONTROL: a package-unbound row wins over the code package on the read naming no package and the read naming the base.
  5. Beside the dependent package's row, the package-unbound row is what the read naming no package and the layered read serve (the dependent package's row was, before), and the read naming the dependent package serves its own row.
Red on base 69ce01d: 3 failed, 1 passed (cases 1, 2, 5 red; 3 green; 4 added after)

The three production files written back to their base blobs, @objectstack/metadata-protocol rebuilt, the pin file run (4 cases then), then restored with git checkout HEAD -- (git diff HEAD empty) and rebuilt.

× the card's three steps answer base / dependent / base, and the layered read and the base's list agree
× the write address does not move: a save naming no package still writes the dependent package's row
× CONTROL: a package-unbound row still wins over the code package, beside the dependent package's row too
AssertionError: expected [ 'title', 'status' ] to deeply equal [ Array(6) ]
AssertionError: expected [ 'title' ] to deeply equal [ Array(6) ]
AssertionError: expected [ 'title', 'status' ] to deeply equal [ 'title', 'priority' ]
Tests  3 failed | 1 passed (4)

The third was named CONTROL then; the red showed it is a changed answer, so it was split into the plain control (case 4) and case 5.

Ablation: the old one-shipper answer restored reds 3 of 5; the restore leg is green

node scripts/ablation-replace.mjs on protocol-environment-rows.ts, anchor if (shippers.size === 1) return [...shippers][0]; → return (globalThis as any).ABL28238 ?? (address.packageId ?? undefined); (the write address's package, as before). Anchor 1 → 0, blob 1a530197d936 → 992e059d588a; ablation-dist-preflight found the marker in dist/index.js and dist/index.cjs.

× the card's three steps answer base / dependent / base, and the layered read and the base's list agree
× the write address does not move: a save naming no package still writes the dependent package's row
× beside the dependent package's row, the package-unbound row is what the read naming no package serves
Tests  3 failed | 2 passed (5)
ablation-replace: ok restored: blob == HEAD (1a530197d936) and `git diff HEAD` is empty

Restore leg: rebuild, ablation-dist-preflight --absent (marker absent from all 24 built files), Tests 5 passed (5).

Package suites

Acceptance notes

The card's acceptance is half met. Live on examples/app-showcase (pnpm dev -- --fresh), the card's steps (the showcase's list now has 7 columns, not 6):

base 69ce01d this branch
?package=com.example.showcase 7 7
?package=com.default.acme-custom 2 2
no package 2 7
?layers=true effective 2 7
GET /meta/view?package=com.example.showcase one item, 7 one item, 7
GET /meta/view (no package), items named showcase_task.default 7 and 2, both _packageId: com.example.showcase unchanged
/_console/apps/com.example.showcase/showcase_task headers (Playwright) Title, Status Title, Status

The app still renders 2. The console reads views only from the list naming no package (ui/packages/app-shell/src/providers/MetadataProvider.tsx, EAGER_TYPES and composeObjects(..., readType('view'))), and mergeViewsIntoObjects keys each view by name, so the last item of a name wins: the dependent package's slot. That list keeps one slot per package that holds a name by design (readFlattenedMetaItems, #21934: the env-wide list "holds every package's body of a name"), and the dependent package's slot wears the base's _packageId (the artifact envelope mergeArtifactProtection grafts). Changing either is a choice between a server list rule and a console merge rule, so it goes to the card as a decision; this PR does not touch the list.

  • Write address unchanged, by order. A package-less save of this name still lands in the dependent package's row, and the package-less read's version is null when no row is served (the base has no row). So a package-less save followed by a package-less read does not read back what it wrote, for this one shape. The epic's write gate (N-09: undeclared same-name refused at publish) closes the shape at its source.
  • Declared override: nothing to pin. main has no override declaration yet (N-08), so "no change to declared overrides" holds trivially.
  • Draft reads unchanged. The strict draft read and previewDrafts naming no package still follow the draft address (packagelessDraftAddress), which is the write side.
  • A disabled base. shippedArtifactsOf includes a disabled package's artifact, so a disabled base's name now resolves to the base on the package-less read, which the read then withholds as a disabled package's item, where it used to serve the dependent package's row (read from the code, not measured).

Verification

Derived gates on 21fa3e7 (62 commands, `dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`)

dispatch-gates --ran: 62 derived, 62 run, 0 NOT-MEASURED; every one exit 0. check-plugin-teardown-shape --self-test first answered 3 (its positive control commit was outside this shallow clone); after git fetch --depth=1 origin 621a4876… it answered 0, and that is the code recorded.

node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-engine-split-ratio.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:durability-log-level :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:filter-alias-parity :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

Not run locally, declared to CI: test:repo (Test Core), the artifact-roster and wide-population families, the type-check lanes. pnpm lint scoped to the two touched packages: exit 0.


Generated by Claude Code

…date of the package-less read's content

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5nQHuymasPwjX3mAr2
…'s same-named row over the real engine

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5nQHuymasPwjX3mAr2
…l and beside the dependent package's row

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5nQHuymasPwjX3mAr2
… content address

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5nQHuymasPwjX3mAr2
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 13 documentable anchor(s).

19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 53749673ba7a0005d09e8a79544da10d5faa10bc.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 59 of 211 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 53749673ba7a0005d09e8a79544da10d5faa10bc → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 41e29296679ec91af09b566a69cd502f800fbcb0 — the merge of head 21fa3e7fcbe7f151b62a90cc594310bb58a83bd6 into base 53749673ba7a0005d09e8a79544da10d5faa10bc, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 41e29296679ec91af09b566a69cd502f800fbcb0 && git checkout 41e29296679ec91af09b566a69cd502f800fbcb0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 53749673ba7a0005d09e8a79544da10d5faa10bc 21fa3e7fcbe7f151b62a90cc594310bb58a83bd6 && git checkout -B drift-repro 53749673ba7a0005d09e8a79544da10d5faa10bc && git merge --no-ff 21fa3e7fcbe7f151b62a90cc594310bb58a83bd6

node scripts/docs-audit/affected-docs.mjs --json 53749673ba7a0005d09e8a79544da10d5faa10bc

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 53749673ba7a0005d09e8a79544da10d5faa10bc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-10-31T04:25Z · head 21fa3e7f · #28238

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape:
    • A draft against main with no assignees and a patch changeset for @objectstack/metadata-protocol.
    • It says Part of #28238, because the card's acceptance is half met. The list half ("the app renders 6") is with the maintainer as decision letter 6538867716 (recommending A), so the card stays open after this lands.
    • check-governed-merges --pr 28304 reads NOT governed, 406 changed lines.
    • The claim (6538170135) names this PR's branch, claude/project-thread-boxqo3.
  2. Against the order:
  3. Contract review: not owed. There is no packages/spec/src/** path and no governed path, the PR declares Clause-②: no (no input is newly refused or accepted), and it narrows nothing. Metadata-protocol: none is declared.
  4. Gates:
    • Locally, the dev ran 62 derived families with 0 NOT-MEASURED, every one exit 0. Package suites: metadata-protocol 21860 passed, objectql 9755 passed. Typecheck and scoped lint pass.
    • CI on head 21fa3e7f: 35 success, 13 skipped, 0 red, Test Core 1–8 all green. The state is clean.
  5. Overlap: none of the open PRs touches protocol.ts, protocol-environment-rows.ts or protocol-lock-version-gates.ts. git merge-tree reports no conflict against main 57f8ca2f.
  6. After landing: the landing record, then the card's claim is released and the card goes to needs-user-decision to wait for the list ruling.

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant