fix(metadata): read a view container's own object in the artifact/HMR loader - #14401
Conversation
…MR loader
`MetadataPlugin._parseAndRegisterArtifact` derives which object an aggregated
`defineView` container binds to at two sites, and both walked exactly two
levels — `list.data.object` then `form.data.object` — never consulting
`ViewSchema.object`, the field whose own `.describe()` names its readers
("read by `getViewsByObject()` / `GET /meta/view?object=`").
A container that declares the binding once at the top and omits `data` from
its view arms was therefore dropped by the first site's `if (!viewObject)
continue` BEFORE any registration, so `list('view')` never returned it and
neither the #13913 read backstop nor `GET /meta/view?object=` could see it.
Both sites now call `deriveViewContainerObject` — this package's single
spelling of the derivation, landed with #13913 and documented there as the one
place the drift is repaired rather than a third private copy to fall behind.
It carries the order #13407 settled at the runtime door: the container's own
`object` first, then the unchanged `list.data.object` -> `form.data.object` ->
row-name fallback.
Card relation is declared once in the PR body (this branch squashes).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…tifact-loader-container-object
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 12 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 4857ce2ed70371df3dcddf64a436c045650522de && git checkout 4857ce2ed70371df3dcddf64a436c045650522de
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5c9e40ad91028b57b0748e3ea0347189bac72ce9 6f581cb60bbdbcb8f06ea84319ec2d13b27d131f && git checkout -B drift-repro 5c9e40ad91028b57b0748e3ea0347189bac72ce9 && git merge --no-ff 6f581cb60bbdbcb8f06ea84319ec2d13b27d131f
node scripts/docs-audit/affected-docs.mjs --json 5c9e40ad91028b57b0748e3ea0347189bac72ce9
|
|
Enqueue provenance ( Generated by Claude Code |
Fixes #13912
MetadataPlugin._parseAndRegisterArtifact— the artifact/HMR loader, one of the two SOURCE view registrars — derives which object an aggregateddefineViewcontainer binds to at two sites, and both walked exactly two levels, never consulting the container's own top-levelobject. That is the fieldViewSchema.objectdocuments as the authorial signal, in a.describe()that names its own readers: "Object this container binds to — how a stack-levelviews: [...]entry says which object its views belong to; read bygetViewsByObject()/GET /meta/view?object=".A package-shipped
defineView({ object: 'crm_lead', list: { columns: [...] } })therefore registered nothing at all through this path: the first site'sif (!viewObject) continuedropped the container before any registration, solist('view')never returned it, the #13913 read backstop had nothing to expand, andgetViewsByObject('crm_lead')/GET /meta/view?object=crm_leadanswered empty for that object via this path. Restores declared = enforced; the accept set and the public surface are unchanged.Both sites, before and after
packages/metadata/src/plugin.ts, the aggregated-container branch (was:933-936):packages/metadata/src/plugin.ts, the fall-through registrar (was:973-975):deriveViewContainerObjectisobjectthenlist.data.objectthenform.data.objectthen the row's ownname— the order #13407 settled at the runtime door. On the second site its last term is vacuous by construction: that branch is reached only whenitem.nameis falsy, so what the call adds there is exactly the leadingobjectterm. On the first site thenameterm is new; it is what every sibling derivation already does, and no container in this repo reaches it (measured below).Why a call and not a third local copy — the dependency-direction reading
Both packages' manifests, read from source:
packages/metadata-protocol/package.jsondependenciesincludes"@objectstack/metadata": "workspace:*".packages/metadata/package.jsondependenciesdoes not include@objectstack/metadata-protocol(it iscore,metadata-core,metadata-fs,platform-objects,spec,types, pluschokidar/glob/js-yaml/zod).So importing the protocol's
expandRuntimeViewContainerintoplugin.tswould invert an existing edge and close a cycle, and promoting that private method to a public export would widenmetadata-protocol's surface while still being unreachable from here. No new cross-package export is proposed.It turns out none is needed.
packages/metadata/src/view-container-expansion.ts— landed with #13913, inside this package — already exportsderiveViewContainerObject, and its header states the intent verbatim: the reusable substance is the derivation chain, "which is exactly the part that has silently drifted three ways (the ObjectQL boot loop keys off the registration name,plugin.tswalks two levels, andprotocol.tswalks four since #13407).deriveViewContainerObjectis the one spelling of it for this package, so the drift has a single place to be repaired rather than a third private copy to fall behind." Calling it is the repair that header was written for; restating the chain inline would have produced a fourth distinct order (objectthenlistthenform, noname), which is what the card's fence forbids.Only the derivation is shared. Expansion, per-item
applyProtectionwith the manifest's package coordinates, and the expansion-warning logging stay inplugin.ts— they differ fromexpandRuntimeViewContainer's container-provenance stamping and are this registrar's own contract.protocol.tsreconciliation reading (triage's open question)Triage (comment 5486856691) recorded two opposite orders inside
packages/metadata-protocol/src/protocol.tsand asked the implementer to re-check whether #13929 settled it. Re-measured atorigin/main1dcb995f2:Expansion path,
expandRuntimeViewContainer(protocol.ts:13513):Diagnostic path (
protocol.ts:7514):Both are
object-first, so the opposite-order defect triage flagged is reconciled — #13929 supplied the leading term the expansion path was missing. The tail terms still differ, and that is a difference of INPUT rather than of ordering: the diagnostic path reads a decorated served item (an expanded ViewItem, or an overlay row carryingdata.object/objectName), the expansion path reads an aggregated container. Nothing filed for it, andprotocol.tsis untouched here.One residual divergence was found and is filed unassigned as #14399: the two SOURCE registrars put the row's own
namein opposite positions — first inpackages/objectql/src/engine.ts'sresolveMetadataItemName, last inderiveViewContainerObject— so a container whosenameis not its bound object expands under different keys depending on which registrar loaded it. Out of scope here, un-rehearsed in both directions, nothing shipped moves today.Repro, controls and ablation
New pin:
packages/metadata/src/plugin-artifact-view-container-object.test.ts. Every case drives the real door (_parseAndRegisterArtifact, shared by the boot artifact load and the HMR reload) with a bareObjectStackDefinition, so each fixture also passes the door's strict parse.That last point is load-bearing and was measured, not assumed:
ObjectListViewSchema.datarequiresobjectwhendatais present at all, so a container carryinglist: { data: { provider: 'object' } }with noobjectis refused by the door before this code runs (views.0.list.data.object: Invalid input: expected string, received undefined). The reachable failing shape is the natural one — the binding declared once at the top, with the view arms carrying nodataat all — and that is what the fixture uses. A pin written on the refused shape would have been testing the schema, not this registrar.object: 'crm_lead'and nodata.objectanywhere: the container registers undercrm_lead, the expansioncrm_lead.default/crm_lead.hot/crm_lead.editregisters (4 items total), andgetViewsByObject('crm_lead')answers with those three and never with the container itself ([finding] the nested-plugin seam does not expand an aggregatedviewscontainer — a nested plugin's per-view items never reach the registry, sogetViewsByObject()/GET /meta/view?object=answer with the container alone #7163).object: 'crm_lead'and a disagreeinglist.data.object: 'crm_account'registers undercrm_lead, and nothing lands undercrm_account. No container shipped in this repo sets both (measured: zero top-levelobjectkeys across all 13defineViewcall sites inexamples/,packages/lintandpackages/qa), so nothing existing moves — the pin exists so the derivations cannot drift apart again.list.data.objectstill registers and expands unchanged.object, nodata.object, noname) is still skipped, and nothing is registered. The fix widens where a binding may be declared; it does not invent one.Ablation (both derivation sites reverted to the pre-fix two-deep chain, run on the final HEAD
6f581cb60). Direction predicted before the run and matched exactly: 5 failed | 3 passed (8) — the five cases that depend on the container's ownobjectgo red, while the fixture premise guard and both CONTROLs stay green. The mutation was proved on disk before measuring (2 injected old-chain occurrences, 0 remainingderiveViewContainerObject(item)call sites, on-disk blob hash67c8a807differing from the HEAD blob0b22c5f2), and the restore was proved after (on-disk hash back to0b22c5f2,git diff HEADempty). No rebuild leg is owed on either side: the pin imports./plugin.jsandplugin.tsimports./view-container-expansion.js, both relative, and this package's vitest aliases redirect only@objectstack/coreand@objectstack/specto their ownsrc— nodistis in the resolution path.Verification
All of the following ran on
6f581cb60, the branch head afterorigin/main(1dcb995f2, PR #14340 included) was merged in.pnpm --filter @objectstack/metadata test— 42 files / 663 tests passed.pnpm --filter @objectstack/metadata-protocol --filter @objectstack/runtime run test(the two consumer packages that boot this door) — metadata-protocol 153 passed | 2 skipped, 2107 tests; runtime 207 files / 3061 tests passed.pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'— 70/70 successful.pnpm lint(repo-wideeslint . --no-inline-config, not narrowed) — exit 0.tsc --noEmit -p packages/metadata/tsconfig.json --listFiles: the new test file is in the program, and so issrc/plugin.ts; 89 errors total, exactly the pre-existing count ([finding] @objectstack/metadata has no typecheck script, and its own tsconfig program does not compile: 89 pre-existing errors across 12 test files, checked by nothing #14342), none of them naming either file.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackderives for this diff, re-derived after the final commit with no paths passed: 34 green, and 2 reporting PREREQUISITE NOT MET (exit 3) rather than a finding —check-test-completeness.mjs(needs a savedturbo run testlog CI tees; its own text says "the local reading for this gate is NOT MEASURED... it is not a red") andcheck-half-states.mjs(repo-scoped REST is 403 in this container; "nothing was swept"). Both readings are NOT MEASURED, not passes.🤖 Generated with Claude Code
https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
Generated by Claude Code