fix(metadata-protocol): a bare-list view container on another package's object expands under its own name - #21430
Conversation
…'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>
…asure it Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
…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>
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>
Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21334
Clause-②: no
What this changes
A runtime view container expands each member to
OBJECT.KEY: a barelist(one that names no key) toOBJECT.default, aformtoOBJECT.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_packageIdand_provenance: 'package'. The container's own default also keptisDefault: true. It either replaced the object's default view or stood beside it as a second list default.expandRuntimeViewContainerinpackages/metadata-protocol/src/protocol.tsnow 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:listexpands toOBJECT.CONTAINER_NAME;OBJECT.CONTAINER_NAME.KEY: alistthat names its key, eachlistViewsandformViewsentry, andform.The spec's own expander produces these names:
expandUnderOwnNameruns it withOBJECT.CONTAINER_NAMEas 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'sobjectis set back to the object it binds. The barelistis lent the container's name as its key, then served asOBJECT.CONTAINER_NAMEwith the lentnametaken back off itsconfig.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 bareliststays 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,
isDefaultincluded: 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 inpackages/spec,packages/restor the save path changes.Patch round 1: the seat's answer (5955628428), OQ1 → A and OQ2 → A
protocol.tsis blob237a0530d7adfrom73da9273f3to HEAD8976a86f94. The pins and ablations below ran againstf233f22fd7's test files.OQ1. A cross-package container never claims the object's default.
expandRuntimeViewContainer:if (crossPackage) delete item.isDefault;.env_localand the unscoped kernel, for a package-scoped, an environment-wide and an organization-scoped container:isDefault;isDefaultitems on the object door are the showcase'sshowcase_task.default(list) andshowcase_task.form(form).GET /api/v1/meta/view?object=showcase_taskserves exactly ONEisDefaultlist view,showcase_task.default.mainMetadataProvider.applyViewItemsetsbucket.primaryto the lastisDefaultlist view it is served. The door now serves one, the packagedshowcase_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.
expandUnderOwnName. Every keyed member spells underOBJECT.CONTAINER_NAME, as listed above.list, namedlist,listViews,formViews,form) × 3 containers × 2 kernels. Each case aims its key at a name the showcase ships. Every case finds:_packageId(none when package-less),_provenancenotpackage,_diagnostics.valid: true, and noisDefault.listViews.in_progress, incom.example.repairassets) leavesshowcase_task.in_progressunchanged on both doors. Its ownshowcase_task.os_qa_keyed_probe.in_progresscarriescom.example.repairassets,_diagnostics.valid: true, and noisDefault.lookupArtifactItem+mergeArtifactProtectionat the end ofreadFlattenedMetaItemsis untouched. It finds no artifact under a name derived from the container's own name, so it grafts nothing. Measured:_packageIdis the container's own and_provenanceis absent on every derived name, on both kernels."Both doors answer the same row" for a derived name: a fork, reported.
CONTAINER_NAME) with its row on every kernel.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 anenv_localkernel, 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.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 declaresname: alistdoes, while aformdoes not, so a namedformis 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.mjsfrom the committed state, and the restore was proved: blob237a0530d7ad= the HEAD blob, andgit diff HEADempty.if (crossPackage) delete item.isDefault;deleted):delete item.isDefaultabsent 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.const expandAt = under;changed toconst expandAt = object;, which is round 0's naming):listkind and the card's probe on every container and kernel, the controls, the enumeration and the nameless case.expandAt = objectpresent in 2 built files): 1 failed / 3 passed. Red: the keyed probe, withshowcase_task.in_progresslabelled 'Probe keyed'. Green: the control, steps 1 to 7, and step 8.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']}}.showcase_task.defaultshowcase_task.defaultenv_local, in-process)_packageId: com.example.showcase,_provenance: packageMeasured over REST at round 0's head
f79124a005, before round 1. A container in another package withlistViews.in_progressstill replacedshowcase_task.in_progresson both doors (keyed members were unchanged from base). After the bare probe was saved,GET /api/v1/meta/viewserved two list defaults forshowcase_task.H2, the contract: silent. The container contract (
view.zod.ts, its header and theViewSchema.namedescribe 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.tsis not edited.H3, the fix site: confirmed, with a second site. The fix site is the inline
byName.setinreadFlattenedMetaItemstogether withexpandRuntimeViewContainer, which grafted the shadowed artifact's_packageIdand_provenance. The second site ishydrateExpandedViewItems, 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: falseby the list door, becauseViewItemNameSchemarequires a dot. The qualified spellings above arevalid: true.Pins
packages/metadata-protocol/src/view-container-runtime-expansion.test.ts, block#21334: 46 cases, plus 16 pre-existing.SchemaRegistry's key shapes.default,in_progress,form,edit) come from the spec's ownexpandViewContainer.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 mergedorigin/mainbdd3654f29first (merge commit3551aede34, fast-forward push). Dists were built through the verify lock, from the closure@objectstack/dogfood^...at the round-1 head. The markerexpandAt = underis inmetadata-protocol/dist.f233f22fd7. The diff from there to8976a86f94is 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.8976a86f94.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderives 68 commands, the same list as atf233f22fd7. All 68 ran at8976a86f94and exited 0.--ran, with the exit codes recorded, reports "68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN".pnpm check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET). It went green after the 8 packages it names were built through the lock.pnpm check:type-check-debtran through the lock: exit 0.check-issue-citations --census, the shard attestations).eslint --no-inline-config --format jsonon the 3 touched source and test files gives 3 files, 0 errors, 0 warnings. eslint'scalculateConfigForFileshows none of the 3 ignored, withparserOptions.projectandprojectServiceboth null. Type-aware linting is off, so no untouched file's verdict can move. The repo-widepnpm lintis CI's.origin/mainmoved 6 commits since round 1's merge, to3a6d92f78b. None of them touchespackages/metadata-protocol, this PR's dogfood file orview.zod.ts, so main was not merged again.Acceptance notes
@objectstack/verifyharness only. Noted, not filed (5955628428).byName. The inline expansion'sbyNamemap 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).stampRenameWarning's_diagnosticsis replaced by the list door's decoration.getViewsByObjectnot covered.MetadataManager.getViewsByObject(packages/metadata) expands containers with its own first-wins rule. It was not touched or measured here.Generated by Claude Code