Skip to content

fix(metadata-protocol)!: a package's stored copy of a container it ships overlays its shipped views, so a withdrawal saved in the copy holds at the anonymous form doors - #22023

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21980-copy-overlays-shipped-names
Oct 7, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21980-copy-overlays-shipped-names

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21980
Clause-②: no (narrowing)

A package's stored copy of a view container it ships now overlays that package's shipped views, so a withdrawal saved in the copy holds at the anonymous form doors.

This is triage's direction for the card (6014736043, unlocked by 6016790773). It lands under the maintainer's ruling on the two questions the first round returned: batch #282 item 1, decision card #22004 (record 6020103367, pointer 6020226416 on the card), 「同意」 at 2026-10-06T15:59Z. OQ1 is answered A: the unscoped hydration line, under one measurement condition (below). OQ2 is answered A: the narrowing arm, minor.

All source edits are in @objectstack/metadata-protocol (packages/metadata-protocol/src/protocol.ts). There is no packages/spec edit, no loader edit, no rest-server.ts edit and no governed path.

What changes

  • The copy's names. expandRuntimeViewContainer gives the loaders' arm to a stored copy of a container its own package ships, bound to the same object (copiesOwnShippedViewContainer). Each member is served as OBJECT.KEY, in the copying package's own slot, which the list and the by-name read already select per package (servedViewExpansion).
    • Every other container on another package's object keeps the own-name arm.
    • A copy on another package's object still declares no default view (the seat's answer on the own-name arm, unchanged).
    • The loaders' names do not change.
    • "The copying package ships this container" is read through the one existing lookup, now a helper shippedViewContainerOf that overlaidShippedContainerViewNames shares (no behaviour change there). The object binding is read by runtimeViewContainerObject, which is the base derivation chain extracted unchanged.
  • The unscoped registry hydration (OQ1 A). hydrateExpandedViewItems no longer registers an expansion under the bare name when another package ships that name (shippedArtifactsOf).
    • SchemaRegistry.getItem answers the bare slot ahead of any package's own entry, so on an unscoped kernel the by-name read naming the other package used to serve this container's view.
    • This predates the copy's new names. It happens when two packages ship one container and one of them stores a copy (measured on base protocol.ts).
    • Every kernel's by-name read already answers such an expansion from its stored row, ahead of the registry (resolveRowlessExpandedView).
  • Changeset: .changeset/21980-copy-overlays-shipped-names.md, @objectstack/metadata-protocol minor. It carries Clause-②: no (narrowing), a BREAKING line naming the three save-door shapes with their remedy, and the ADR-0087 not-required (no-migration-prescription) marker in the shape of the earlier save-door narrowing. The marker states that no census of the writers of such copies was taken.

The save door narrows in three shapes

The collision predicate is unchanged; it judges the copy at its new names. Each shape is a package's copy of a container it ships on another package's object. Each was accepted on base and is refused on head with VALIDATION_ERROR / 400, measured on both kernels:

  • the copy adds a bare list whose loader name, OBJECT.default, only the other package ships;
  • the copy adds a keyed member whose loader name only the other package ships (measured with a formViews key);
  • another stored container, under a different row name, already expands a name the copy now expands. This is reachable only by install order.

Unchanged:

  • a copy with only its shipped members;
  • the package's own view item row of a name the copy expands, which keeps its slot.

A package-less copy whose package is resolved by registry order now takes the loaders' names in both orders. At base it took them in one order only.

The ruling's condition, measured before opening this

The condition: on an unscoped kernel, for a name two packages share, the by-name read naming NO package must answer, never an absence.

It was measured through getMetaItem with no packageId:

  • on both kernels and in both registry orders;
  • for each member, with the object owned by the other package or by none;
  • at three points: base aa09db58c9, the direction b7a2a8f547, and the hydration line 2322b5bb04. Each point was a trap-guarded swap of protocol.ts, and each restore was proven by blob equality and an empty git diff HEAD.

Results:

  • No read answers nothing, anywhere.
  • The hydration line leaves this read unchanged on both kernels: the direction's rows and head's rows are identical.
  • Which body the read answers is not owner-stable, and it was not at base either.
    • At base it answered whichever package registered first. That was the owner's shipped item in one order and the copying package's in the other.
    • From the direction on, it answers the copy's body, which is the last expansion of the name (servedViewExpansion naming no package).
    • When the other package registered first, that answer carries the other package's _packageId. The same mislabel happens on base in the no-owner case. See the Acceptance notes.

This is pinned as block (h): an answer on both kernels, the same body on both, and a body the env-wide list serves under the name.

Census of the readers of the hydrated bare entry outside metadata-protocol:

  • No non-test file calls getItem, listItems or getArtifactItem on view directly.
  • MetadataManager.getViewsByObject reads its own loader store.
  • The objectql facade's generic get and list are getMetaItem's step 2, which runs after step 1b has answered the expansion from its row.

Pins

All pins are in packages/metadata-protocol/src/protocol.org-scoped-write-refused.test.ts, inside #21967's block, reusing its registry double and its composition of the doors.

  • (f) The family's enumeration, derived as a cross product: who owns the object (the copying package, another package, none), whether the copying package ships the container, and the member (bare list, keyed member, default form). Both kernels. Each placement asserts:
    • reach on both read doors;
    • the default the copy declares;
    • no name that only another package ships;
    • the owner's names intact on both read doors;
    • for a form, the withdrawal shuts the anonymous doors, with a control: saved open, the form is served.
  • (g) The earlier no-owner case. Two packages ship container task, and either one stores a copy. The by-name read naming each package answers that package's own item, on both kernels.
  • (h) The condition above.

Measured

  • Base reading of (f) at 5de8db7939: 10 red / 51 green. The reds are exactly the placements where the copying package ships the container on another package's object.
  • Full @objectstack/metadata-protocol suite at ca60b61d7b (merged origin/main at 803764a36f): 218 files and 28127 tests passed, 19 skipped (os-verify-lock VERDICT command-exit 0). Typecheck is green, and the pin file is in the program.
  • Reverse verification on committed head ca60b61d7b, through scripts/ablation-replace.mjs. Each anchor hit once and the blob changed; each restore was proven (blob equals HEAD 400cd431ef84, git diff HEAD empty). The subject is imported from source, so there is no dist leg. Predictions were written first.
    • The hydration line taken out: predicted 9 red / 162 green, measured 9 / 162. The reds are (f)'s 3 unscoped owner reads and (g)'s 6 unscoped cases; (h) stays green, as the condition measurement predicted.
    • The own-name arm restored for the copy: predicted 10 red / 161 green, measured 10 / 161.
  • Clause-②, measured against the built entry declarations: dist/index.d.ts and dist/index.d.cts were built from origin/main's protocol.ts and from head, then diffed. Apart from comments, the only difference is three untyped private member lines on ObjectStackProtocolImplementation; no exported signature moves. The narrowing arm is for the save door's accept set (above).
  • Gates:
    • dispatch-gates --commands (no paths) derives 64; all 64 exit 0, and --ran reports 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN.
    • The artifact-roster block: 51 of 54 exit 0 before this PR existed. The other three need a PR's context (check-closing-target-claim, check-partof-closing-keyword, check-single-claim-paths).
    • The four symbol-anchor sweeps are green.
    • Lint, narrowed: the population is read from eslint.config.mjs (its TS glob covers both .ts files; no glob matches .md). The JSON count is 2 files, 0 errors, 0 warnings. There is no typed linting and the config's only disk reads are two untouched baselines, so the diff cannot move an untouched file's verdict.

Acceptance notes

  • The by-name read naming no package, for a name two packages ship, answers the copy's body wearing the envelope of whichever package registered first. getMetaItem (:10431) grafts lookupArtifactItem(type, name) with no package onto the body it serves. On base this happens when two packages ship one container on an object nobody owns and one stores a copy. From this PR on, it also happens when the object's owner is the other package. Not changed here: it is a by-name read change outside the ruling. Reported to the seat with evidence; the pin (h) does not pin which package's body or stamp that read answers.
  • The loaders keep isDefault on the default list of a container a package ships on another package's object, while that package's stored copy of it declares no default (the seat's earlier answer, kept). Read only, not measured on a door.

Generated by Claude Code

claude added 6 commits October 6, 2026 13:23
… view container can take

The withdrawal-reach family's closing enumeration, committed red against
the base: who owns the object (the copying package, another package,
none) x whether the copying package ships the container x the member the
copy changes (the bare list, a keyed member, the default form), on both
kernels. Base reading: 10 red / 51 green, the reds exactly the placements
where the copying package ships the container on another package's
object.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
…ps expands to the loaders' names

Triage's direction for a stored copy of a view container its own package
ships: on another package's object it expands as the source loaders
expand the shipped container (`<object>.<key>`, in the copying package's
own slot), not under the own-name arm. Any other container on another
package's object keeps the own-name arm, and no copy declares the
object's default.

- `copiesOwnShippedViewContainer`: the copying package ships a view
  container under the copy's name, bound to the same object.
- `shippedViewContainerOf`: the one artifact lookup for "the container a
  package ships under a name", now shared with
  `overlaidShippedContainerViewNames` (no behaviour change there).
- `runtimeViewContainerObject`: the object derivation chain, extracted
  unchanged so the shipped container's binding is read the same way.
- The enumeration pin asserts the default each placement declares.

Measured: the pin file is 144 green / 3 red. The three reds are the
unscoped kernel's by-name read naming the object's owning package, for a
name the copy now shares with it: the registry hydration of the copy's
expansion under the bare name answers it (the same mechanism already
answers that read on main when two packages ship one name and only one
has a stored copy). Closing it needs a change beyond the per-package
keying, so the card returns to triage as needs_decision.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
…rs a view expansion under a name another package ships

The maintainer's ruling on OQ1 (A). On an unscoped kernel,
hydrateExpandedViewItems registered every expansion of a stored view
container under the bare name, and SchemaRegistry.getItem answers the
bare slot ahead of any package's own entry. So the by-name read naming
another package that ships the same name served this container's view,
while the list served that package's own item in its slot. It predates
the copy's new names (two packages ship one container, one stores a
copy), and the copy's new names reached it on another package's object.

An expansion whose name another package ships (shippedArtifactsOf) is no
longer registered there. Every kernel's by-name read answers it from its
stored row ahead of the registry (resolveRowlessExpandedView), and the
list expands the row itself.

Pins: block (f)'s owner reads on the unscoped kernel are green, and
block (g) pins the earlier case: two packages ship the container, either
one stores a copy, and the by-name read naming each package answers that
package's own item on both kernels.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
… name two packages ship, on both kernels

The ruling's condition on the hydration line: for a name two packages
ship, once one of them stores a copy, the by-name read that names no
package answers on both kernels (never an absence), both kernels answer
the same body, and that body is one the env-wide list serves under the
name. Block (h) runs it for each member, with the object owned by the
other package or by none, in both registry orders.

Measured before writing it, over base aa09db5, the direction
b7a2a8f and 2322b5b: no read answered nothing anywhere, and the
hydration line leaves this read unchanged on both kernels.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
…ips overlays its shipped views; the save door refuses three copies it accepted

The changeset for this card, `@objectstack/metadata-protocol` minor,
carrying `Clause-②: no (narrowing)` per the maintainer's ruling on OQ2.

A package's stored copy of a view container it ships overlays that
package's shipped views, so a withdrawal saved in the copy holds at the
anonymous form endpoints. Judged at its new names, the save door now
refuses three copies it accepted before: an added bare list or keyed
member whose name only another package ships, and a copy expanding a
name another stored container already expands. The BREAKING line names
the three shapes and their remedy, and the ADR-0087 marker is
not-required (no-migration-prescription), stating that no census of the
writers of such copies was taken.

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

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 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 — 11 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 b220e0943e87d26256316387e3c484f3f50b8416 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2770d9991c93b923511cabca7d08a547899840f0 — the merge of head 03f727651ed15fc6c1795fe80c18b139ade6b91b into base b220e0943e87d26256316387e3c484f3f50b8416, 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 2770d9991c93b923511cabca7d08a547899840f0 && git checkout 2770d9991c93b923511cabca7d08a547899840f0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b220e0943e87d26256316387e3c484f3f50b8416 03f727651ed15fc6c1795fe80c18b139ade6b91b && git checkout -B drift-repro b220e0943e87d26256316387e3c484f3f50b8416 && git merge --no-ff 03f727651ed15fc6c1795fe80c18b139ade6b91b

node scripts/docs-audit/affected-docs.mjs --json b220e0943e87d26256316387e3c484f3f50b8416

⚠️ 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 b220e0943e87d26256316387e3c484f3f50b8416 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

objectstack-fleet Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

CI: TypeScript Type Check is red because one lane timed out restoring the Turbo cache, not on this PR's code. domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi · 2026-10-06T17:35Z.

Update, 2026-10-07T01:06Z: green. The maintainer flagged the red, so the seat did not wait on the ruling for the base merge.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT (seat review) — PR #22023 at head 03f727651e

domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi · read at 2026-10-07T01:31Z. The dev's report is os-dev-report on #21980 (6021602255). This lands under two rulings: #22004's (6020226416: OQ1 → A, OQ2 → A) and #22027's (6028897060: A, the merge condition is met).

  • The change follows triage's direction (6014736043) and the claim's file surface (6016913296). The seat read the diff in protocol.ts:
    • The copy arm. expandRuntimeViewContainer takes the loaders' names (expandViewContainer under the object) when copiesOwnShippedViewContainer holds. That is, the container's own package ships a view container under the copy's name, bound to the same object. Every other container on another package's object keeps expandUnderOwnName, unchanged.
    • The default. isDefault is still dropped on crossPackage, not on the own-name arm. So the copy declares no default for an object another package owns, as the changeset says.
    • The shared helper. shippedViewContainerOf is the one lookup, now shared with overlaidShippedContainerViewNames, which was refactored to it. runtimeViewContainerObject is the base derivation, extracted unchanged.
    • The hydration line (OQ1 → A). hydrateExpandedViewItems skips the bare-name registration of an expansion whose name another package ships (shippedArtifactsOf). A name only the container's own package ships, or none, registers as before.
  • The ruling's condition (decision: #21980 merge condition — a no-package by-name read of a shared view name now answers the stored copy, not the owner's shipped item (before the change it depended on registry order): land PR #22023, or hold it? #22027 → A). The no-package by-name read never turned into an absence in 72 reads across both kernels and both registry orders. Pin (h) asserts what the ruling guards: an answer, the same body on both kernels, and a body the list serves. The _packageId envelope graft is finding(metadata-protocol): getMetaItem naming no package grafts the first-registered package's artifact envelope onto another package's served view expansion, so a no-package by-name read can carry the wrong _packageId #22024's (pm:blocked p3), not this merge's.
  • Pins: blocks (f)/(g)/(h) in protocol.org-scoped-write-refused.test.ts, 171 cases.
    • Reverse verification on the committed head took the hydration line out and turned exactly the 9 predicted cases red.
    • Restoring the own-name arm for the copy turned exactly the 10 predicted cases red.
    • Both were restored by blob equality.
  • Changeset, sentence by sentence against the diff:
    • "expands as the loaders expand the shipped container", "keeps expanding under its own name, unchanged" and "still declares no default": each matches the code above.
    • "The by-name read on an unscoped kernel": matches the hydration line.
    • The three refused shapes: the collision predicate (viewContainerNameCollisionRefusal) is not edited. It now judges the copy at the names it expands to, as measured in round 1 (H3 a/b/c, 400 VALIDATION_ERROR on both kernels).
    • "Rows stored before this change": no stored row is rewritten; old OBJECT.CONTAINER.KEY references stop resolving, and the remedy is named.
  • Clause-②: no (narrowing), minor, BREAKING. The subject is fix(metadata-protocol)!:, with the ADR-0087 marker not-required (no-migration-prescription). The built declarations gain only three private members. No contract surface is touched (not packages/spec, no governed rule text, no Clause-②: yes), so no contract review is owed.
  • CI on 03f727651e: 31 success, and the 3 skips are in the roster. This head merges main at b220e0943e; the PR's own diff is unchanged line for line from ca60b61d7b (3 files, +370/-10).
  • Readings against main at 9a0401fdd3: NOT governed (0 of 3 paths), 380 changed lines, and git merge-tree is clean. main's 3 newer files touch neither metadata-protocol nor the registry.
  • Release: a fix, so it is eligible for the last 17.x (decision: before the v18 line opens, does main publish one last 17.x release to npm? #22009's ruling).
  • Noted, not filed: the loaders keep isDefault on a shipped container's default list on another package's object, while the package's stored copy declares none (the seat's earlier answer, kept; PR Acceptance notes).

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 7, 2026 01:32
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 7, 2026 01:32
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit 8caa131 Oct 7, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21980-copy-overlays-shipped-names branch October 7, 2026 01:57
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

2 participants