Skip to content

fix(metadata-protocol): a bare-list view container on another package's object expands under its own name - #21430

Merged
objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/issue-21334-container-default-shadow
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/issue-21334-container-default-shadow

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21334

Clause-②: no

What this changes

A runtime view container expands each member to OBJECT.KEY: a bare list (one that names no key) to OBJECT.default, a form to OBJECT.form, and every member that names a key to that key. Saved under another name, in another package or in none, for an object a code package ships, those expansions replaced that package's views of the same names on the object door (GET /api/v1/meta/view?object=OBJECT), still stamped with the shipping package's _packageId and _provenance: 'package'. The container's own default also kept isDefault: true. It either replaced the object's default view or stood beside it as a second list default.

expandRuntimeViewContainer in packages/metadata-protocol/src/protocol.ts now applies triage's ruling (5946423948), as the seat's answer (5955628428) extends it:

  • On another package's object (a code package owns the object, per getPackagedObjectOwner, and it is not the container's own package), every name the container expands derives from its own name:

    • the bare list expands to OBJECT.CONTAINER_NAME;
    • every keyed member expands to OBJECT.CONTAINER_NAME.KEY: a list that names its key, each listViews and formViews entry, and form.

    The spec's own expander produces these names: expandUnderOwnName runs it with OBJECT.CONTAINER_NAME as its base. So the spec's key rule and in-container de-duplication still apply, and a member kind the spec adds later is placed the same way. Each item's object is set back to the object it binds. The bare list is lent the container's name as its key, then served as OBJECT.CONTAINER_NAME with the lent name taken back off its config.

  • No default claimed. None of these views carries isDefault. The object's defaults stay its owning package's.

  • One exception. When the owning package itself ships OBJECT.CONTAINER_NAME (a container named after one of that package's keys), the bare list stays at the spelling the spec gives it, OBJECT.CONTAINER_NAME.CONTAINER_NAME.

  • A container with no name of its own expands nothing on such an object.

  • The container's own package is the package its row is bound to. For a package-less row that is the name-keyed overlay of a packaged item (ADR-0005), it is the package of the artifact that row overlays.

  • Provenance. Each expanded item carries the container's own package as _packageId. It merges an artifact's protection envelope only when that artifact belongs to the container's own package.

  • Unchanged, isDefault included: a container of the object's own package, a package-less overlay of that package's own container, and a container on an object no code package ships.

Both callers use this one function: the list read's inline per-request expansion, and the registry hydration (hydrateExpandedViewItems) on an unscoped kernel. Nothing in packages/spec, packages/rest or the save path changes.

Patch round 1: the seat's answer (5955628428), OQ1 → A and OQ2 → A

protocol.ts is blob 237a0530d7ad from 73da9273f3 to HEAD 8976a86f94. The pins and ablations below ran against f233f22fd7's test files.

OQ1. A cross-package container never claims the object's default.

  • Change. One line in expandRuntimeViewContainer: if (crossPackage) delete item.isDefault;.
  • Measured in-process, on the env_local and the unscoped kernel, for a package-scoped, an environment-wide and an organization-scoped container:
    • no item from the container carries isDefault;
    • the only isDefault items on the object door are the showcase's showcase_task.default (list) and showcase_task.form (form).
  • Measured over REST (the real showcase, unscoped harness) for the package-scoped probe, the package-less probe and the keyed probe: GET /api/v1/meta/view?object=showcase_task serves exactly ONE isDefault list view, showcase_task.default.
  • objectui's reading, NOT browser-measured. objectui main MetadataProvider.applyViewItem sets bucket.primary to the last isDefault list view it is served. The door now serves one, the packaged showcase_task.default, so that is the view objectui would take as the object's primary tab.

OQ2. Every member derives its name from the container's own name.

  • Change. expandUnderOwnName. Every keyed member spells under OBJECT.CONTAINER_NAME, as listed above.
  • Measured in-process, the enumeration: five member kinds (bare list, named list, listViews, formViews, form) × 3 containers × 2 kernels. Each case aims its key at a name the showcase ships. Every case finds:
    • every shipped name answering the packaged view, once, on the object door and on the by-name read;
    • the container's own name served with its own _packageId (none when package-less), _provenance not package, _diagnostics.valid: true, and no isDefault.
  • Measured over REST: the keyed probe (listViews.in_progress, in com.example.repairassets) leaves showcase_task.in_progress unchanged on both doors. Its own showcase_task.os_qa_keyed_probe.in_progress carries com.example.repairassets, _diagnostics.valid: true, and no isDefault.
  • The final governance pass needed no edit. lookupArtifactItem + mergeArtifactProtection at the end of readFlattenedMetaItems is untouched. It finds no artifact under a name derived from the container's own name, so it grafts nothing. Measured: _packageId is the container's own and _provenance is absent on every derived name, on both kernels.

"Both doors answer the same row" for a derived name: a fork, reported.

  • The by-name read answers the container's own name (CONTAINER_NAME) with its row on every kernel.
  • For a derived name (OBJECT.CONTAINER_NAME.KEY), it answers the same row only where the registry hydrates: an unscoped kernel, for a package-scoped or environment-wide container. On an env_local kernel, and for an organization-scoped container on any kernel, it answers nothing. No stored row carries that name, and the by-name read expands no container.
  • Measured for all three containers on both kernels. This is how every runtime container's expansion reads by name, and it predates this card. It is reported in the dev report, not changed here.

The member-kind enumeration is derived, not listed. Each top-level key of the spec's container schema (ViewSchema.shape) is offered a single view and a record of views. A key that yields an expanded item is a member kind. A single-view kind is enumerated bare, and also named when its own schema declares name: a list does, while a form does not, so a named form is not authorable through the save door. The test fails when the derived set differs from the placed cases.

Reverse verification, one arm at a time. Each arm was mutated with scripts/ablation-replace.mjs from the committed state, and the restore was proved: blob 237a0530d7ad = the HEAD blob, and git diff HEAD empty.

  • OQ1 arm reverted (if (crossPackage) delete item.isDefault; deleted):
    • In-process: 36 failed / 26 passed of 62. Red: every member-kind case and the card's probe, × 3 containers × 2 kernels. Green: 16 pre-existing, the enumeration, the 6 controls, and the de-duplication, shipped-key and nameless cases.
    • Dogfood (rebuilt; the preflight found delete item.isDefault absent from all 24 built files): 3 failed / 1 passed. Red: steps 1 to 7, step 8 and the keyed probe, each on the single-default assertion. Green: the override control.
  • OQ2 arm reverted (const expandAt = under; changed to const expandAt = object;, which is round 0's naming):
    • In-process: 26 failed / 36 passed. Red: the four keyed kinds × 3 containers × 2 kernels, the de-duplication case and the shipped-key case. Green: the bare-list kind and the card's probe on every container and kernel, the controls, the enumeration and the nameless case.
    • Dogfood (rebuilt; the preflight found expandAt = object present in 2 built files): 1 failed / 3 passed. Red: the keyed probe, with showcase_task.in_progress labelled 'Probe keyed'. Green: the control, steps 1 to 7, and step 8.
  • Both restore legs rebuilt metadata-protocol; the preflight confirmed the marker restored, and the tree clean against HEAD.

Zone 2 measurements (round 0, at base ceb4a939b)

H1, the baseline: reproduced, and wider than the card. Probe body: {name, object: 'showcase_task', list: {type: 'grid', columns: ['title','status']}}.

kernel container object door showcase_task.default by-name showcase_task.default
environment-scoped (env_local, in-process) package-scoped / env-wide / org-scoped shadow: 2 columns, _packageId: com.example.showcase, _provenance: package packaged: "All Tasks", 7 columns
unscoped (in-process and the real showcase over REST) package-scoped / env-wide shadow shadow too, through the registry's bare key
unscoped (in-process) org-scoped shadow packaged

Measured over REST at round 0's head f79124a005, before round 1. A container in another package with listViews.in_progress still replaced showcase_task.in_progress on both doors (keyed members were unchanged from base). After the bare probe was saved, GET /api/v1/meta/view served two list defaults for showcase_task.

H2, the contract: silent. The container contract (view.zod.ts, its header and the ViewSchema.name describe text; ADR-0017 §3.2) names an object-scoped container after its object. It states no arm for a name another package owns, so the ruling's arm applies. view.zod.ts is not edited.

H3, the fix site: confirmed, with a second site. The fix site is the inline byName.set in readFlattenedMetaItems together with expandRuntimeViewContainer, which grafted the shadowed artifact's _packageId and _provenance. The second site is hydrateExpandedViewItems, which registered the expansion under the bare key on an unscoped kernel.

H4, the spelling. An expanded ViewItem named with the flat container name is badged _diagnostics.valid: false by the list door, because ViewItemNameSchema requires a dot. The qualified spellings above are valid: true.

Pins

  • packages/metadata-protocol/src/view-container-runtime-expansion.test.ts, block #21334: 46 cases, plus 16 pre-existing.
    • It reuses the file's pinned engine double, with a registry double in the real SchemaRegistry's key shapes.
    • The packaged views (default, in_progress, form, edit) come from the spec's own expandViewContainer.
    • The cases: the member-kind enumeration, 30 member cases, 6 card-probe cases, the controls for each kernel (same-package row; package-less overlay of the package's own container; sanctioned by-name override), the in-container de-duplication, the shipped-key fallback, and the nameless container.
  • packages/qa/dogfood/test/view-container-cross-package-default.dogfood.test.ts: the card's steps over the real showcase through REST. The override control runs first, on the pristine stack. Then steps 1 to 7, step 8, and the keyed probe. Each case asserts both doors and the single list default.

Gates

Readings at HEAD 8976a86f94. Round 1 merged origin/main bdd3654f29 first (merge commit 3551aede34, fast-forward push). Dists were built through the verify lock, from the closure @objectstack/dogfood^... at the round-1 head. The marker expandAt = under is in metadata-protocol/dist.

  • Suites, run at f233f22fd7. The diff from there to 8976a86f94 is one changeset line.
    • pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: exit 0. 202 files passed, 3 skipped; 3034 tests passed, 19 skipped.
    • pnpm --filter @objectstack/metadata-protocol typecheck: exit 0.
    • pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 (the full suite): exit 0. 172 files passed, 1 skipped; 1400 tests passed, 9 skipped.
    • pnpm --filter @objectstack/dogfood typecheck: exit 0.
  • Gates, at 8976a86f94. node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derives 68 commands, the same list as at f233f22fd7. All 68 ran at 8976a86f94 and exited 0. --ran, with the exit codes recorded, reports "68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN".
    • pnpm check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET). It went green after the 8 packages it names were built through the lock.
    • pnpm check:type-check-debt ran through the lock: exit 0.
  • NOT MEASURED: the CI-only families that take a workflow value (check-issue-citations --census, the shard attestations).
  • Lint, a declared narrowing. eslint --no-inline-config --format json on the 3 touched source and test files gives 3 files, 0 errors, 0 warnings. eslint's calculateConfigForFile shows none of the 3 ignored, with parserOptions.project and projectService both null. Type-aware linting is off, so no untouched file's verdict can move. The repo-wide pnpm lint is CI's.
  • Main since round 1's merge. origin/main moved 6 commits since round 1's merge, to 3a6d92f78b. None of them touches packages/metadata-protocol, this PR's dogfood file or view.zod.ts, so main was not merged again.

Acceptance notes

  1. Stale registry expansions on an unscoped kernel. An expansion hydrated into the registry is not unregistered when its container is deleted. Its name is now the container's own, so the stale item no longer covers a packaged name. Reach: the @objectstack/verify harness only. Noted, not filed (5955628428).
  2. Same-name collapse in byName. The inline expansion's byName map keys by bare name, so it collapses two packages' same-name items (ADR-0048) whenever any view row exists for the type. Read, not measured. Noted, not filed (5955628428).
  3. By-name read of a derived name. See the fork above: it answers only through the registry's hydration. This predates the card.
  4. Rename warning. stampRenameWarning's _diagnostics is replaced by the list door's decoration.
  5. getViewsByObject not covered. MetadataManager.getViewsByObject (packages/metadata) expands containers with its own first-wins rule. It was not touched or measured here.

Generated by Claude Code

claude added 6 commits October 2, 2026 14:14
…'s object expands under its own name

On an object a code package owns, a runtime view container that belongs to
another package (or to none) expanded its bare `list` to `<object>.default`
and replaced that package's default view on the object door, wearing the
shadowed artifact's `_packageId` and protection. It now expands under the
container's own name, `<object>.<container name>`, and every expanded item
carries the container's own package and only that package's artifact
envelope. A container of the object's own package still expands to
`<object>.default`.

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
…nst the packaged default

Both read doors answer the packaged `<object>.default` after a package-scoped,
an environment-wide and an organization-scoped container is saved on another
package's object, on an environment-scoped and an unscoped kernel; the
container's own view is served as `<object>.<container name>` with its own
package. Controls: a container of the object's own package, a package-less
overlay of the package's own container, and the sanctioned by-name override.

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
…ntainer, over the real showcase

Through the REST doors: a container saved into a Studio-created package and a
package-less one, both for `showcase_task`, leave `showcase_task.default`
unchanged on the object door and the by-name read; each container's own view
is served as `showcase_task.<container name>`. Control: the by-name override
of `showcase_task.default` reaches both doors.

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
…e stack

The control shared its stack with the shadow cases, so under a reverted fix
the shadow the earlier cases left in the registry decided it. Run first and
undone by its own delete, it reads nothing another case wrote.

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
…st view container

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Oct 2, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/error-catalog.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/api/index.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/automation/flows.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/data-modeling/drivers.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/data-modeling/objects.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/deployment/cli.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/deployment/environment-variables.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/deployment/validating-metadata.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/kernel/cluster.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/kernel/contracts/metadata-service.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/kernel/services-checklist.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/permissions/authorization.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/permissions/permission-sets.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/plugins/packages.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))

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

  • content/docs/releases/implementation-status.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/releases/v17/17-3.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/releases/v17/17-5.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))
  • content/docs/releases/v17/17-6.mdx (via sys_metadata (literal, a string literal in runtimeViewContainerPackage))

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 3a6d92f78bb6a160b762dfee0738fd3b0b7ae6c2 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9c537d29d607a726068b42dbdcae0f6e7a546279 — the merge of head 8976a86f94027c9ffc6e9fcdf9979fdc34aadc73 into base 3a6d92f78bb6a160b762dfee0738fd3b0b7ae6c2, 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 9c537d29d607a726068b42dbdcae0f6e7a546279 && git checkout 9c537d29d607a726068b42dbdcae0f6e7a546279
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3a6d92f78bb6a160b762dfee0738fd3b0b7ae6c2 8976a86f94027c9ffc6e9fcdf9979fdc34aadc73 && git checkout -B drift-repro 3a6d92f78bb6a160b762dfee0738fd3b0b7ae6c2 && git merge --no-ff 8976a86f94027c9ffc6e9fcdf9979fdc34aadc73

node scripts/docs-audit/affected-docs.mjs --json 3a6d92f78bb6a160b762dfee0738fd3b0b7ae6c2

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

claude added 6 commits October 2, 2026 15:28
…ds every member under its own name and claims no default

Patch round 1, the seat's answer to the two open questions. On an object a
code package owns, a container of another package (or of none) now expands
every member under its own name: the bare list as
`<object>.<container name>`, and each keyed member (a named list,
`listViews`, `formViews`, `form`) as `<object>.<container name>.<key>`, by
the spec's own key rule run under that name. None of its views carries
`isDefault`. Where the owning package ships `<object>.<container name>`, the
bare list stays at the spelling the spec gives it under that name.

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
…ss-package container, and the single default

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
…erate a named variant only where the member's schema declares name

The save door refuses `label` and `name` on a form view, so the probes and
the packaged form fixtures carry neither, and the member-kind derivation
offers a named variant only for a slot whose own schema declares `name`.

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 17:01
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 17:02
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 535d1d2 Oct 2, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21334-container-default-shadow branch October 2, 2026 17:29
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/l tests tooling

Projects

None yet

2 participants