fix(metadata): read a view container's own object in the artifact/HMR loader - #14401

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-13912-artifact-loader-container-object
Sep 2, 2026
Merged

fix(metadata): read a view container's own object in the artifact/HMR loader#14401
os-musk merged 2 commits into
mainfrom
claude/issue-13912-artifact-loader-container-object

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#13912

MetadataPlugin._parseAndRegisterArtifact — the artifact/HMR loader, one of the two SOURCE view registrars — derives which object an aggregated defineView container binds to at two sites, and both walked exactly two levels, never consulting the container's own top-level object. That is the field ViewSchema.object documents as the authorial signal, in a .describe() that names its own readers: "Object this container binds to — how a stack-level views: [...] entry says which object its views belong to; read by getViewsByObject() / GET /meta/view?object=".

A package-shipped defineView({ object: 'crm_lead', list: { columns: [...] } }) therefore registered nothing at all through this path: the first site's if (!viewObject) continue dropped the container before any registration, so list('view') never returned it, the #13913 read backstop had nothing to expand, and getViewsByObject('crm_lead') / GET /meta/view?object=crm_lead answered 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):

// beforeconstviewObject=(itemasany)?.list?.data?.object??(itemasany)?.form?.data?.object;if(!viewObject)continue;// afterconstviewObject=deriveViewContainerObject(item);if(!viewObject)continue;

packages/metadata/src/plugin.ts, the fall-through registrar (was :973-975):

// beforeletname=(itemasany)?.name;if(!name){if(metaType==='view'){name=(itemasany)?.list?.data?.object??(itemasany)?.form?.data?.object;}}// afterletname=(itemasany)?.name;if(!name){if(metaType==='view'){name=deriveViewContainerObject(item);}}

deriveViewContainerObject is object then list.data.object then form.data.object then the row's own name — the order #13407 settled at the runtime door. On the second site its last term is vacuous by construction: that branch is reached only when item.name is falsy, so what the call adds there is exactly the leading object term. On the first site the name term 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.jsondependencies includes "@objectstack/metadata": "workspace:*".
  • packages/metadata/package.jsondependencies does not include @objectstack/metadata-protocol (it is core, metadata-core, metadata-fs, platform-objects, spec, types, plus chokidar/glob/js-yaml/zod).

So importing the protocol's expandRuntimeViewContainer into plugin.ts would invert an existing edge and close a cycle, and promoting that private method to a public export would widen metadata-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 exports deriveViewContainerObject, 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.ts walks two levels, and protocol.ts walks four since #13407). deriveViewContainerObject is 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 (object then list then form, no name), which is what the card's fence forbids.

Only the derivation is shared. Expansion, per-item applyProtection with the manifest's package coordinates, and the expansion-warning logging stay in plugin.ts — they differ from expandRuntimeViewContainer's container-provenance stamping and are this registrar's own contract.

protocol.ts reconciliation reading (triage's open question)

Triage (comment 5486856691) recorded two opposite orders inside packages/metadata-protocol/src/protocol.ts and asked the implementer to re-check whether #13929 settled it. Re-measured at origin/main1dcb995f2:

Expansion path, expandRuntimeViewContainer (protocol.ts:13513):

constviewObject=(typeofcontainer.object==='string'&&container.object ? container.object : undefined)??container?.list?.data?.object??container?.form?.data?.object??(typeofcontainer.name==='string' ? container.name : undefined);

Diagnostic path (protocol.ts:7514):

constsourceObject=viewDoc?.object??viewDoc?.data?.object??viewDoc?.objectName??viewDoc?.list?.data?.object;

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 carrying data.object / objectName), the expansion path reads an aggregated container. Nothing filed for it, and protocol.ts is untouched here.

One residual divergence was found and is filed unassigned as #14399: the two SOURCE registrars put the row's own name in opposite positions — first in packages/objectql/src/engine.ts's resolveMetadataItemName, last in deriveViewContainerObject — so a container whose name is 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 bare ObjectStackDefinition, so each fixture also passes the door's strict parse.

That last point is load-bearing and was measured, not assumed: ObjectListViewSchema.datarequiresobject when data is present at all, so a container carrying list: { data: { provider: 'object' } } with no object is 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 no data at all — and that is what the fixture uses. A pin written on the refused shape would have been testing the schema, not this registrar.

  • Repro — container with object: 'crm_lead' and no data.object anywhere: the container registers under crm_lead, the expansion crm_lead.default / crm_lead.hot / crm_lead.edit registers (4 items total), and getViewsByObject('crm_lead') answers with those three and never with the container itself ([finding] the nested-plugin seam does not expand an aggregated views container — a nested plugin's per-view items never reach the registry, so getViewsByObject() / GET /meta/view?object= answer with the container alone #7163).
  • Ordering pin — a container setting BOTH object: 'crm_lead' and a disagreeing list.data.object: 'crm_account' registers under crm_lead, and nothing lands under crm_account. No container shipped in this repo sets both (measured: zero top-level object keys across all 13 defineView call sites in examples/, packages/lint and packages/qa), so nothing existing moves — the pin exists so the derivations cannot drift apart again.
  • CONTROL — a container bound only through list.data.object still registers and expands unchanged.
  • CONTROL — a container with no derivable binding at all (no object, no data.object, no name) 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 own object go 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 remaining deriveViewContainerObject(item) call sites, on-disk blob hash 67c8a807 differing from the HEAD blob 0b22c5f2), and the restore was proved after (on-disk hash back to 0b22c5f2, git diff HEAD empty). No rebuild leg is owed on either side: the pin imports ./plugin.js and plugin.ts imports ./view-container-expansion.js, both relative, and this package's vitest aliases redirect only @objectstack/core and @objectstack/spec to their own src — no dist is in the resolution path.

Verification

All of the following ran on 6f581cb60, the branch head after origin/main (1dcb995f2, PR #14340 included) was merged in.

  • pnpm --filter @objectstack/metadata test42 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-wide eslint . --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 is src/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.
  • The 36 gate families node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derives 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 saved turbo run test log CI tees; its own text says "the local reading for this gate is NOT MEASURED... it is not a red") and check-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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/ai/agents.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/api/client-sdk.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/api/error-catalog.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/data-modeling/index.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/deployment/validating-metadata.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/permissions/authorization.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/ui/index.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))

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

  • content/docs/releases/v13.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))

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 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 12 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 5c9e40ad91028b57b0748e3ea0347189bac72ce9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4857ce2ed70371df3dcddf64a436c045650522de — the merge of head 6f581cb60bbdbcb8f06ea84319ec2d13b27d131f into base 5c9e40ad91028b57b0748e3ea0347189bac72ce9, 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 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

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Enqueue provenance (domain:engine execution seat, session_0112hMx9hjJ9BgB28X97DS68): ACCEPT on #13912 (comment 5504343166) → at 04:43Z every check run on head 6f581cb60 was completed with success or skipped (33 runs; Lint & Repo Gates finished 04:41Z), governed-surface test on the three changed paths: NOT governed, mergeable_state not dirty → marked ready and auto-merge (squash) enabled. Landing is by the merge queue; the engine seat follows it to MERGED.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 41aa979Sep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-13912-artifact-loader-container-object branch September 2, 2026 05:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

metadata/plugin.ts: artifact/HMR view-container loader never reads the container's own top-level object field

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(metadata): read a view container's own object in the artifact/HMR loader - #14401

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-13912-artifact-loader-container-object
Sep 2, 2026
Merged

fix(metadata): read a view container's own object in the artifact/HMR loader#14401
os-musk merged 2 commits into
mainfrom
claude/issue-13912-artifact-loader-container-object

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#13912

MetadataPlugin._parseAndRegisterArtifact — the artifact/HMR loader, one of the two SOURCE view registrars — derives which object an aggregated defineView container binds to at two sites, and both walked exactly two levels, never consulting the container's own top-level object. That is the field ViewSchema.object documents as the authorial signal, in a .describe() that names its own readers: "Object this container binds to — how a stack-level views: [...] entry says which object its views belong to; read by getViewsByObject() / GET /meta/view?object=".

A package-shipped defineView({ object: 'crm_lead', list: { columns: [...] } }) therefore registered nothing at all through this path: the first site's if (!viewObject) continue dropped the container before any registration, so list('view') never returned it, the #13913 read backstop had nothing to expand, and getViewsByObject('crm_lead') / GET /meta/view?object=crm_lead answered 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):

// beforeconstviewObject=(itemasany)?.list?.data?.object??(itemasany)?.form?.data?.object;if(!viewObject)continue;// afterconstviewObject=deriveViewContainerObject(item);if(!viewObject)continue;

packages/metadata/src/plugin.ts, the fall-through registrar (was :973-975):

// beforeletname=(itemasany)?.name;if(!name){if(metaType==='view'){name=(itemasany)?.list?.data?.object??(itemasany)?.form?.data?.object;}}// afterletname=(itemasany)?.name;if(!name){if(metaType==='view'){name=deriveViewContainerObject(item);}}

deriveViewContainerObject is object then list.data.object then form.data.object then the row's own name — the order #13407 settled at the runtime door. On the second site its last term is vacuous by construction: that branch is reached only when item.name is falsy, so what the call adds there is exactly the leading object term. On the first site the name term 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.jsondependencies includes "@objectstack/metadata": "workspace:*".
  • packages/metadata/package.jsondependencies does not include @objectstack/metadata-protocol (it is core, metadata-core, metadata-fs, platform-objects, spec, types, plus chokidar/glob/js-yaml/zod).

So importing the protocol's expandRuntimeViewContainer into plugin.ts would invert an existing edge and close a cycle, and promoting that private method to a public export would widen metadata-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 exports deriveViewContainerObject, 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.ts walks two levels, and protocol.ts walks four since #13407). deriveViewContainerObject is 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 (object then list then form, no name), which is what the card's fence forbids.

Only the derivation is shared. Expansion, per-item applyProtection with the manifest's package coordinates, and the expansion-warning logging stay in plugin.ts — they differ from expandRuntimeViewContainer's container-provenance stamping and are this registrar's own contract.

protocol.ts reconciliation reading (triage's open question)

Triage (comment 5486856691) recorded two opposite orders inside packages/metadata-protocol/src/protocol.ts and asked the implementer to re-check whether #13929 settled it. Re-measured at origin/main1dcb995f2:

Expansion path, expandRuntimeViewContainer (protocol.ts:13513):

constviewObject=(typeofcontainer.object==='string'&&container.object ? container.object : undefined)??container?.list?.data?.object??container?.form?.data?.object??(typeofcontainer.name==='string' ? container.name : undefined);

Diagnostic path (protocol.ts:7514):

constsourceObject=viewDoc?.object??viewDoc?.data?.object??viewDoc?.objectName??viewDoc?.list?.data?.object;

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 carrying data.object / objectName), the expansion path reads an aggregated container. Nothing filed for it, and protocol.ts is untouched here.

One residual divergence was found and is filed unassigned as #14399: the two SOURCE registrars put the row's own name in opposite positions — first in packages/objectql/src/engine.ts's resolveMetadataItemName, last in deriveViewContainerObject — so a container whose name is 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 bare ObjectStackDefinition, so each fixture also passes the door's strict parse.

That last point is load-bearing and was measured, not assumed: ObjectListViewSchema.datarequiresobject when data is present at all, so a container carrying list: { data: { provider: 'object' } } with no object is 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 no data at all — and that is what the fixture uses. A pin written on the refused shape would have been testing the schema, not this registrar.

  • Repro — container with object: 'crm_lead' and no data.object anywhere: the container registers under crm_lead, the expansion crm_lead.default / crm_lead.hot / crm_lead.edit registers (4 items total), and getViewsByObject('crm_lead') answers with those three and never with the container itself ([finding] the nested-plugin seam does not expand an aggregated views container — a nested plugin's per-view items never reach the registry, so getViewsByObject() / GET /meta/view?object= answer with the container alone #7163).
  • Ordering pin — a container setting BOTH object: 'crm_lead' and a disagreeing list.data.object: 'crm_account' registers under crm_lead, and nothing lands under crm_account. No container shipped in this repo sets both (measured: zero top-level object keys across all 13 defineView call sites in examples/, packages/lint and packages/qa), so nothing existing moves — the pin exists so the derivations cannot drift apart again.
  • CONTROL — a container bound only through list.data.object still registers and expands unchanged.
  • CONTROL — a container with no derivable binding at all (no object, no data.object, no name) 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 own object go 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 remaining deriveViewContainerObject(item) call sites, on-disk blob hash 67c8a807 differing from the HEAD blob 0b22c5f2), and the restore was proved after (on-disk hash back to 0b22c5f2, git diff HEAD empty). No rebuild leg is owed on either side: the pin imports ./plugin.js and plugin.ts imports ./view-container-expansion.js, both relative, and this package's vitest aliases redirect only @objectstack/core and @objectstack/spec to their own src — no dist is in the resolution path.

Verification

All of the following ran on 6f581cb60, the branch head after origin/main (1dcb995f2, PR #14340 included) was merged in.

  • pnpm --filter @objectstack/metadata test42 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-wide eslint . --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 is src/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.
  • The 36 gate families node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derives 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 saved turbo run test log CI tees; its own text says "the local reading for this gate is NOT MEASURED... it is not a red") and check-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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/ai/agents.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/api/client-sdk.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/api/error-catalog.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/data-modeling/index.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/deployment/validating-metadata.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/permissions/authorization.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/ui/index.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))

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

  • content/docs/releases/v13.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))

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 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 12 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 5c9e40ad91028b57b0748e3ea0347189bac72ce9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4857ce2ed70371df3dcddf64a436c045650522de — the merge of head 6f581cb60bbdbcb8f06ea84319ec2d13b27d131f into base 5c9e40ad91028b57b0748e3ea0347189bac72ce9, 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 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

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Enqueue provenance (domain:engine execution seat, session_0112hMx9hjJ9BgB28X97DS68): ACCEPT on #13912 (comment 5504343166) → at 04:43Z every check run on head 6f581cb60 was completed with success or skipped (33 runs; Lint & Repo Gates finished 04:41Z), governed-surface test on the three changed paths: NOT governed, mergeable_state not dirty → marked ready and auto-merge (squash) enabled. Landing is by the merge queue; the engine seat follows it to MERGED.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 41aa979Sep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-13912-artifact-loader-container-object branch September 2, 2026 05:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

metadata/plugin.ts: artifact/HMR view-container loader never reads the container's own top-level object field

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(metadata): read a view container's own object in the artifact/HMR loader - #14401

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-13912-artifact-loader-container-object
Sep 2, 2026
Merged

fix(metadata): read a view container's own object in the artifact/HMR loader#14401
os-musk merged 2 commits into
mainfrom
claude/issue-13912-artifact-loader-container-object

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#13912

MetadataPlugin._parseAndRegisterArtifact — the artifact/HMR loader, one of the two SOURCE view registrars — derives which object an aggregated defineView container binds to at two sites, and both walked exactly two levels, never consulting the container's own top-level object. That is the field ViewSchema.object documents as the authorial signal, in a .describe() that names its own readers: "Object this container binds to — how a stack-level views: [...] entry says which object its views belong to; read by getViewsByObject() / GET /meta/view?object=".

A package-shipped defineView({ object: 'crm_lead', list: { columns: [...] } }) therefore registered nothing at all through this path: the first site's if (!viewObject) continue dropped the container before any registration, so list('view') never returned it, the #13913 read backstop had nothing to expand, and getViewsByObject('crm_lead') / GET /meta/view?object=crm_lead answered 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):

// beforeconstviewObject=(itemasany)?.list?.data?.object??(itemasany)?.form?.data?.object;if(!viewObject)continue;// afterconstviewObject=deriveViewContainerObject(item);if(!viewObject)continue;

packages/metadata/src/plugin.ts, the fall-through registrar (was :973-975):

// beforeletname=(itemasany)?.name;if(!name){if(metaType==='view'){name=(itemasany)?.list?.data?.object??(itemasany)?.form?.data?.object;}}// afterletname=(itemasany)?.name;if(!name){if(metaType==='view'){name=deriveViewContainerObject(item);}}

deriveViewContainerObject is object then list.data.object then form.data.object then the row's own name — the order #13407 settled at the runtime door. On the second site its last term is vacuous by construction: that branch is reached only when item.name is falsy, so what the call adds there is exactly the leading object term. On the first site the name term 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.jsondependencies includes "@objectstack/metadata": "workspace:*".
  • packages/metadata/package.jsondependencies does not include @objectstack/metadata-protocol (it is core, metadata-core, metadata-fs, platform-objects, spec, types, plus chokidar/glob/js-yaml/zod).

So importing the protocol's expandRuntimeViewContainer into plugin.ts would invert an existing edge and close a cycle, and promoting that private method to a public export would widen metadata-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 exports deriveViewContainerObject, 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.ts walks two levels, and protocol.ts walks four since #13407). deriveViewContainerObject is 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 (object then list then form, no name), which is what the card's fence forbids.

Only the derivation is shared. Expansion, per-item applyProtection with the manifest's package coordinates, and the expansion-warning logging stay in plugin.ts — they differ from expandRuntimeViewContainer's container-provenance stamping and are this registrar's own contract.

protocol.ts reconciliation reading (triage's open question)

Triage (comment 5486856691) recorded two opposite orders inside packages/metadata-protocol/src/protocol.ts and asked the implementer to re-check whether #13929 settled it. Re-measured at origin/main1dcb995f2:

Expansion path, expandRuntimeViewContainer (protocol.ts:13513):

constviewObject=(typeofcontainer.object==='string'&&container.object ? container.object : undefined)??container?.list?.data?.object??container?.form?.data?.object??(typeofcontainer.name==='string' ? container.name : undefined);

Diagnostic path (protocol.ts:7514):

constsourceObject=viewDoc?.object??viewDoc?.data?.object??viewDoc?.objectName??viewDoc?.list?.data?.object;

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 carrying data.object / objectName), the expansion path reads an aggregated container. Nothing filed for it, and protocol.ts is untouched here.

One residual divergence was found and is filed unassigned as #14399: the two SOURCE registrars put the row's own name in opposite positions — first in packages/objectql/src/engine.ts's resolveMetadataItemName, last in deriveViewContainerObject — so a container whose name is 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 bare ObjectStackDefinition, so each fixture also passes the door's strict parse.

That last point is load-bearing and was measured, not assumed: ObjectListViewSchema.datarequiresobject when data is present at all, so a container carrying list: { data: { provider: 'object' } } with no object is 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 no data at all — and that is what the fixture uses. A pin written on the refused shape would have been testing the schema, not this registrar.

  • Repro — container with object: 'crm_lead' and no data.object anywhere: the container registers under crm_lead, the expansion crm_lead.default / crm_lead.hot / crm_lead.edit registers (4 items total), and getViewsByObject('crm_lead') answers with those three and never with the container itself ([finding] the nested-plugin seam does not expand an aggregated views container — a nested plugin's per-view items never reach the registry, so getViewsByObject() / GET /meta/view?object= answer with the container alone #7163).
  • Ordering pin — a container setting BOTH object: 'crm_lead' and a disagreeing list.data.object: 'crm_account' registers under crm_lead, and nothing lands under crm_account. No container shipped in this repo sets both (measured: zero top-level object keys across all 13 defineView call sites in examples/, packages/lint and packages/qa), so nothing existing moves — the pin exists so the derivations cannot drift apart again.
  • CONTROL — a container bound only through list.data.object still registers and expands unchanged.
  • CONTROL — a container with no derivable binding at all (no object, no data.object, no name) 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 own object go 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 remaining deriveViewContainerObject(item) call sites, on-disk blob hash 67c8a807 differing from the HEAD blob 0b22c5f2), and the restore was proved after (on-disk hash back to 0b22c5f2, git diff HEAD empty). No rebuild leg is owed on either side: the pin imports ./plugin.js and plugin.ts imports ./view-container-expansion.js, both relative, and this package's vitest aliases redirect only @objectstack/core and @objectstack/spec to their own src — no dist is in the resolution path.

Verification

All of the following ran on 6f581cb60, the branch head after origin/main (1dcb995f2, PR #14340 included) was merged in.

  • pnpm --filter @objectstack/metadata test42 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-wide eslint . --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 is src/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.
  • The 36 gate families node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derives 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 saved turbo run test log CI tees; its own text says "the local reading for this gate is NOT MEASURED... it is not a red") and check-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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/ai/agents.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/api/client-sdk.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/api/error-catalog.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/data-modeling/index.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/deployment/validating-metadata.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/permissions/authorization.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/ui/index.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))

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

  • content/docs/releases/v13.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))

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 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 12 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 5c9e40ad91028b57b0748e3ea0347189bac72ce9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4857ce2ed70371df3dcddf64a436c045650522de — the merge of head 6f581cb60bbdbcb8f06ea84319ec2d13b27d131f into base 5c9e40ad91028b57b0748e3ea0347189bac72ce9, 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 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

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Enqueue provenance (domain:engine execution seat, session_0112hMx9hjJ9BgB28X97DS68): ACCEPT on #13912 (comment 5504343166) → at 04:43Z every check run on head 6f581cb60 was completed with success or skipped (33 runs; Lint & Repo Gates finished 04:41Z), governed-surface test on the three changed paths: NOT governed, mergeable_state not dirty → marked ready and auto-merge (squash) enabled. Landing is by the merge queue; the engine seat follows it to MERGED.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 41aa979Sep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-13912-artifact-loader-container-object branch September 2, 2026 05:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

metadata/plugin.ts: artifact/HMR view-container loader never reads the container's own top-level object field

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(metadata): read a view container's own object in the artifact/HMR loader - #14401

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-13912-artifact-loader-container-object
Sep 2, 2026
Merged

fix(metadata): read a view container's own object in the artifact/HMR loader#14401
os-musk merged 2 commits into
mainfrom
claude/issue-13912-artifact-loader-container-object

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#13912

MetadataPlugin._parseAndRegisterArtifact — the artifact/HMR loader, one of the two SOURCE view registrars — derives which object an aggregated defineView container binds to at two sites, and both walked exactly two levels, never consulting the container's own top-level object. That is the field ViewSchema.object documents as the authorial signal, in a .describe() that names its own readers: "Object this container binds to — how a stack-level views: [...] entry says which object its views belong to; read by getViewsByObject() / GET /meta/view?object=".

A package-shipped defineView({ object: 'crm_lead', list: { columns: [...] } }) therefore registered nothing at all through this path: the first site's if (!viewObject) continue dropped the container before any registration, so list('view') never returned it, the #13913 read backstop had nothing to expand, and getViewsByObject('crm_lead') / GET /meta/view?object=crm_lead answered 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):

// beforeconstviewObject=(itemasany)?.list?.data?.object??(itemasany)?.form?.data?.object;if(!viewObject)continue;// afterconstviewObject=deriveViewContainerObject(item);if(!viewObject)continue;

packages/metadata/src/plugin.ts, the fall-through registrar (was :973-975):

// beforeletname=(itemasany)?.name;if(!name){if(metaType==='view'){name=(itemasany)?.list?.data?.object??(itemasany)?.form?.data?.object;}}// afterletname=(itemasany)?.name;if(!name){if(metaType==='view'){name=deriveViewContainerObject(item);}}

deriveViewContainerObject is object then list.data.object then form.data.object then the row's own name — the order #13407 settled at the runtime door. On the second site its last term is vacuous by construction: that branch is reached only when item.name is falsy, so what the call adds there is exactly the leading object term. On the first site the name term 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.jsondependencies includes "@objectstack/metadata": "workspace:*".
  • packages/metadata/package.jsondependencies does not include @objectstack/metadata-protocol (it is core, metadata-core, metadata-fs, platform-objects, spec, types, plus chokidar/glob/js-yaml/zod).

So importing the protocol's expandRuntimeViewContainer into plugin.ts would invert an existing edge and close a cycle, and promoting that private method to a public export would widen metadata-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 exports deriveViewContainerObject, 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.ts walks two levels, and protocol.ts walks four since #13407). deriveViewContainerObject is 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 (object then list then form, no name), which is what the card's fence forbids.

Only the derivation is shared. Expansion, per-item applyProtection with the manifest's package coordinates, and the expansion-warning logging stay in plugin.ts — they differ from expandRuntimeViewContainer's container-provenance stamping and are this registrar's own contract.

protocol.ts reconciliation reading (triage's open question)

Triage (comment 5486856691) recorded two opposite orders inside packages/metadata-protocol/src/protocol.ts and asked the implementer to re-check whether #13929 settled it. Re-measured at origin/main1dcb995f2:

Expansion path, expandRuntimeViewContainer (protocol.ts:13513):

constviewObject=(typeofcontainer.object==='string'&&container.object ? container.object : undefined)??container?.list?.data?.object??container?.form?.data?.object??(typeofcontainer.name==='string' ? container.name : undefined);

Diagnostic path (protocol.ts:7514):

constsourceObject=viewDoc?.object??viewDoc?.data?.object??viewDoc?.objectName??viewDoc?.list?.data?.object;

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 carrying data.object / objectName), the expansion path reads an aggregated container. Nothing filed for it, and protocol.ts is untouched here.

One residual divergence was found and is filed unassigned as #14399: the two SOURCE registrars put the row's own name in opposite positions — first in packages/objectql/src/engine.ts's resolveMetadataItemName, last in deriveViewContainerObject — so a container whose name is 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 bare ObjectStackDefinition, so each fixture also passes the door's strict parse.

That last point is load-bearing and was measured, not assumed: ObjectListViewSchema.datarequiresobject when data is present at all, so a container carrying list: { data: { provider: 'object' } } with no object is 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 no data at all — and that is what the fixture uses. A pin written on the refused shape would have been testing the schema, not this registrar.

  • Repro — container with object: 'crm_lead' and no data.object anywhere: the container registers under crm_lead, the expansion crm_lead.default / crm_lead.hot / crm_lead.edit registers (4 items total), and getViewsByObject('crm_lead') answers with those three and never with the container itself ([finding] the nested-plugin seam does not expand an aggregated views container — a nested plugin's per-view items never reach the registry, so getViewsByObject() / GET /meta/view?object= answer with the container alone #7163).
  • Ordering pin — a container setting BOTH object: 'crm_lead' and a disagreeing list.data.object: 'crm_account' registers under crm_lead, and nothing lands under crm_account. No container shipped in this repo sets both (measured: zero top-level object keys across all 13 defineView call sites in examples/, packages/lint and packages/qa), so nothing existing moves — the pin exists so the derivations cannot drift apart again.
  • CONTROL — a container bound only through list.data.object still registers and expands unchanged.
  • CONTROL — a container with no derivable binding at all (no object, no data.object, no name) 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 own object go 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 remaining deriveViewContainerObject(item) call sites, on-disk blob hash 67c8a807 differing from the HEAD blob 0b22c5f2), and the restore was proved after (on-disk hash back to 0b22c5f2, git diff HEAD empty). No rebuild leg is owed on either side: the pin imports ./plugin.js and plugin.ts imports ./view-container-expansion.js, both relative, and this package's vitest aliases redirect only @objectstack/core and @objectstack/spec to their own src — no dist is in the resolution path.

Verification

All of the following ran on 6f581cb60, the branch head after origin/main (1dcb995f2, PR #14340 included) was merged in.

  • pnpm --filter @objectstack/metadata test42 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-wide eslint . --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 is src/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.
  • The 36 gate families node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derives 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 saved turbo run test log CI tees; its own text says "the local reading for this gate is NOT MEASURED... it is not a red") and check-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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/ai/agents.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/api/client-sdk.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/api/error-catalog.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/data-modeling/index.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/deployment/validating-metadata.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/permissions/authorization.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/ui/index.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))

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

  • content/docs/releases/v13.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))

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 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 12 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 5c9e40ad91028b57b0748e3ea0347189bac72ce9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4857ce2ed70371df3dcddf64a436c045650522de — the merge of head 6f581cb60bbdbcb8f06ea84319ec2d13b27d131f into base 5c9e40ad91028b57b0748e3ea0347189bac72ce9, 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 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

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Enqueue provenance (domain:engine execution seat, session_0112hMx9hjJ9BgB28X97DS68): ACCEPT on #13912 (comment 5504343166) → at 04:43Z every check run on head 6f581cb60 was completed with success or skipped (33 runs; Lint & Repo Gates finished 04:41Z), governed-surface test on the three changed paths: NOT governed, mergeable_state not dirty → marked ready and auto-merge (squash) enabled. Landing is by the merge queue; the engine seat follows it to MERGED.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 41aa979Sep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-13912-artifact-loader-container-object branch September 2, 2026 05:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

metadata/plugin.ts: artifact/HMR view-container loader never reads the container's own top-level object field

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(metadata): read a view container's own object in the artifact/HMR loader - #14401

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-13912-artifact-loader-container-object
Sep 2, 2026
Merged

fix(metadata): read a view container's own object in the artifact/HMR loader#14401
os-musk merged 2 commits into
mainfrom
claude/issue-13912-artifact-loader-container-object

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#13912

MetadataPlugin._parseAndRegisterArtifact — the artifact/HMR loader, one of the two SOURCE view registrars — derives which object an aggregated defineView container binds to at two sites, and both walked exactly two levels, never consulting the container's own top-level object. That is the field ViewSchema.object documents as the authorial signal, in a .describe() that names its own readers: "Object this container binds to — how a stack-level views: [...] entry says which object its views belong to; read by getViewsByObject() / GET /meta/view?object=".

A package-shipped defineView({ object: 'crm_lead', list: { columns: [...] } }) therefore registered nothing at all through this path: the first site's if (!viewObject) continue dropped the container before any registration, so list('view') never returned it, the #13913 read backstop had nothing to expand, and getViewsByObject('crm_lead') / GET /meta/view?object=crm_lead answered 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):

// beforeconstviewObject=(itemasany)?.list?.data?.object??(itemasany)?.form?.data?.object;if(!viewObject)continue;// afterconstviewObject=deriveViewContainerObject(item);if(!viewObject)continue;

packages/metadata/src/plugin.ts, the fall-through registrar (was :973-975):

// beforeletname=(itemasany)?.name;if(!name){if(metaType==='view'){name=(itemasany)?.list?.data?.object??(itemasany)?.form?.data?.object;}}// afterletname=(itemasany)?.name;if(!name){if(metaType==='view'){name=deriveViewContainerObject(item);}}

deriveViewContainerObject is object then list.data.object then form.data.object then the row's own name — the order #13407 settled at the runtime door. On the second site its last term is vacuous by construction: that branch is reached only when item.name is falsy, so what the call adds there is exactly the leading object term. On the first site the name term 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.jsondependencies includes "@objectstack/metadata": "workspace:*".
  • packages/metadata/package.jsondependencies does not include @objectstack/metadata-protocol (it is core, metadata-core, metadata-fs, platform-objects, spec, types, plus chokidar/glob/js-yaml/zod).

So importing the protocol's expandRuntimeViewContainer into plugin.ts would invert an existing edge and close a cycle, and promoting that private method to a public export would widen metadata-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 exports deriveViewContainerObject, 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.ts walks two levels, and protocol.ts walks four since #13407). deriveViewContainerObject is 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 (object then list then form, no name), which is what the card's fence forbids.

Only the derivation is shared. Expansion, per-item applyProtection with the manifest's package coordinates, and the expansion-warning logging stay in plugin.ts — they differ from expandRuntimeViewContainer's container-provenance stamping and are this registrar's own contract.

protocol.ts reconciliation reading (triage's open question)

Triage (comment 5486856691) recorded two opposite orders inside packages/metadata-protocol/src/protocol.ts and asked the implementer to re-check whether #13929 settled it. Re-measured at origin/main1dcb995f2:

Expansion path, expandRuntimeViewContainer (protocol.ts:13513):

constviewObject=(typeofcontainer.object==='string'&&container.object ? container.object : undefined)??container?.list?.data?.object??container?.form?.data?.object??(typeofcontainer.name==='string' ? container.name : undefined);

Diagnostic path (protocol.ts:7514):

constsourceObject=viewDoc?.object??viewDoc?.data?.object??viewDoc?.objectName??viewDoc?.list?.data?.object;

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 carrying data.object / objectName), the expansion path reads an aggregated container. Nothing filed for it, and protocol.ts is untouched here.

One residual divergence was found and is filed unassigned as #14399: the two SOURCE registrars put the row's own name in opposite positions — first in packages/objectql/src/engine.ts's resolveMetadataItemName, last in deriveViewContainerObject — so a container whose name is 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 bare ObjectStackDefinition, so each fixture also passes the door's strict parse.

That last point is load-bearing and was measured, not assumed: ObjectListViewSchema.datarequiresobject when data is present at all, so a container carrying list: { data: { provider: 'object' } } with no object is 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 no data at all — and that is what the fixture uses. A pin written on the refused shape would have been testing the schema, not this registrar.

  • Repro — container with object: 'crm_lead' and no data.object anywhere: the container registers under crm_lead, the expansion crm_lead.default / crm_lead.hot / crm_lead.edit registers (4 items total), and getViewsByObject('crm_lead') answers with those three and never with the container itself ([finding] the nested-plugin seam does not expand an aggregated views container — a nested plugin's per-view items never reach the registry, so getViewsByObject() / GET /meta/view?object= answer with the container alone #7163).
  • Ordering pin — a container setting BOTH object: 'crm_lead' and a disagreeing list.data.object: 'crm_account' registers under crm_lead, and nothing lands under crm_account. No container shipped in this repo sets both (measured: zero top-level object keys across all 13 defineView call sites in examples/, packages/lint and packages/qa), so nothing existing moves — the pin exists so the derivations cannot drift apart again.
  • CONTROL — a container bound only through list.data.object still registers and expands unchanged.
  • CONTROL — a container with no derivable binding at all (no object, no data.object, no name) 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 own object go 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 remaining deriveViewContainerObject(item) call sites, on-disk blob hash 67c8a807 differing from the HEAD blob 0b22c5f2), and the restore was proved after (on-disk hash back to 0b22c5f2, git diff HEAD empty). No rebuild leg is owed on either side: the pin imports ./plugin.js and plugin.ts imports ./view-container-expansion.js, both relative, and this package's vitest aliases redirect only @objectstack/core and @objectstack/spec to their own src — no dist is in the resolution path.

Verification

All of the following ran on 6f581cb60, the branch head after origin/main (1dcb995f2, PR #14340 included) was merged in.

  • pnpm --filter @objectstack/metadata test42 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-wide eslint . --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 is src/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.
  • The 36 gate families node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derives 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 saved turbo run test log CI tees; its own text says "the local reading for this gate is NOT MEASURED... it is not a red") and check-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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/ai/agents.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/api/client-sdk.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/api/error-catalog.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/data-modeling/index.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/deployment/validating-metadata.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/permissions/authorization.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/ui/index.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))

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

  • content/docs/releases/v13.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))

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 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 12 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 5c9e40ad91028b57b0748e3ea0347189bac72ce9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4857ce2ed70371df3dcddf64a436c045650522de — the merge of head 6f581cb60bbdbcb8f06ea84319ec2d13b27d131f into base 5c9e40ad91028b57b0748e3ea0347189bac72ce9, 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 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

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Enqueue provenance (domain:engine execution seat, session_0112hMx9hjJ9BgB28X97DS68): ACCEPT on #13912 (comment 5504343166) → at 04:43Z every check run on head 6f581cb60 was completed with success or skipped (33 runs; Lint & Repo Gates finished 04:41Z), governed-surface test on the three changed paths: NOT governed, mergeable_state not dirty → marked ready and auto-merge (squash) enabled. Landing is by the merge queue; the engine seat follows it to MERGED.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 41aa979Sep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-13912-artifact-loader-container-object branch September 2, 2026 05:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

metadata/plugin.ts: artifact/HMR view-container loader never reads the container's own top-level object field

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(metadata): read a view container's own object in the artifact/HMR loader - #14401

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-13912-artifact-loader-container-object
Sep 2, 2026
Merged

fix(metadata): read a view container's own object in the artifact/HMR loader#14401
os-musk merged 2 commits into
mainfrom
claude/issue-13912-artifact-loader-container-object

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#13912

MetadataPlugin._parseAndRegisterArtifact — the artifact/HMR loader, one of the two SOURCE view registrars — derives which object an aggregated defineView container binds to at two sites, and both walked exactly two levels, never consulting the container's own top-level object. That is the field ViewSchema.object documents as the authorial signal, in a .describe() that names its own readers: "Object this container binds to — how a stack-level views: [...] entry says which object its views belong to; read by getViewsByObject() / GET /meta/view?object=".

A package-shipped defineView({ object: 'crm_lead', list: { columns: [...] } }) therefore registered nothing at all through this path: the first site's if (!viewObject) continue dropped the container before any registration, so list('view') never returned it, the #13913 read backstop had nothing to expand, and getViewsByObject('crm_lead') / GET /meta/view?object=crm_lead answered 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):

// beforeconstviewObject=(itemasany)?.list?.data?.object??(itemasany)?.form?.data?.object;if(!viewObject)continue;// afterconstviewObject=deriveViewContainerObject(item);if(!viewObject)continue;

packages/metadata/src/plugin.ts, the fall-through registrar (was :973-975):

// beforeletname=(itemasany)?.name;if(!name){if(metaType==='view'){name=(itemasany)?.list?.data?.object??(itemasany)?.form?.data?.object;}}// afterletname=(itemasany)?.name;if(!name){if(metaType==='view'){name=deriveViewContainerObject(item);}}

deriveViewContainerObject is object then list.data.object then form.data.object then the row's own name — the order #13407 settled at the runtime door. On the second site its last term is vacuous by construction: that branch is reached only when item.name is falsy, so what the call adds there is exactly the leading object term. On the first site the name term 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.jsondependencies includes "@objectstack/metadata": "workspace:*".
  • packages/metadata/package.jsondependencies does not include @objectstack/metadata-protocol (it is core, metadata-core, metadata-fs, platform-objects, spec, types, plus chokidar/glob/js-yaml/zod).

So importing the protocol's expandRuntimeViewContainer into plugin.ts would invert an existing edge and close a cycle, and promoting that private method to a public export would widen metadata-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 exports deriveViewContainerObject, 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.ts walks two levels, and protocol.ts walks four since #13407). deriveViewContainerObject is 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 (object then list then form, no name), which is what the card's fence forbids.

Only the derivation is shared. Expansion, per-item applyProtection with the manifest's package coordinates, and the expansion-warning logging stay in plugin.ts — they differ from expandRuntimeViewContainer's container-provenance stamping and are this registrar's own contract.

protocol.ts reconciliation reading (triage's open question)

Triage (comment 5486856691) recorded two opposite orders inside packages/metadata-protocol/src/protocol.ts and asked the implementer to re-check whether #13929 settled it. Re-measured at origin/main1dcb995f2:

Expansion path, expandRuntimeViewContainer (protocol.ts:13513):

constviewObject=(typeofcontainer.object==='string'&&container.object ? container.object : undefined)??container?.list?.data?.object??container?.form?.data?.object??(typeofcontainer.name==='string' ? container.name : undefined);

Diagnostic path (protocol.ts:7514):

constsourceObject=viewDoc?.object??viewDoc?.data?.object??viewDoc?.objectName??viewDoc?.list?.data?.object;

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 carrying data.object / objectName), the expansion path reads an aggregated container. Nothing filed for it, and protocol.ts is untouched here.

One residual divergence was found and is filed unassigned as #14399: the two SOURCE registrars put the row's own name in opposite positions — first in packages/objectql/src/engine.ts's resolveMetadataItemName, last in deriveViewContainerObject — so a container whose name is 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 bare ObjectStackDefinition, so each fixture also passes the door's strict parse.

That last point is load-bearing and was measured, not assumed: ObjectListViewSchema.datarequiresobject when data is present at all, so a container carrying list: { data: { provider: 'object' } } with no object is 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 no data at all — and that is what the fixture uses. A pin written on the refused shape would have been testing the schema, not this registrar.

  • Repro — container with object: 'crm_lead' and no data.object anywhere: the container registers under crm_lead, the expansion crm_lead.default / crm_lead.hot / crm_lead.edit registers (4 items total), and getViewsByObject('crm_lead') answers with those three and never with the container itself ([finding] the nested-plugin seam does not expand an aggregated views container — a nested plugin's per-view items never reach the registry, so getViewsByObject() / GET /meta/view?object= answer with the container alone #7163).
  • Ordering pin — a container setting BOTH object: 'crm_lead' and a disagreeing list.data.object: 'crm_account' registers under crm_lead, and nothing lands under crm_account. No container shipped in this repo sets both (measured: zero top-level object keys across all 13 defineView call sites in examples/, packages/lint and packages/qa), so nothing existing moves — the pin exists so the derivations cannot drift apart again.
  • CONTROL — a container bound only through list.data.object still registers and expands unchanged.
  • CONTROL — a container with no derivable binding at all (no object, no data.object, no name) 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 own object go 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 remaining deriveViewContainerObject(item) call sites, on-disk blob hash 67c8a807 differing from the HEAD blob 0b22c5f2), and the restore was proved after (on-disk hash back to 0b22c5f2, git diff HEAD empty). No rebuild leg is owed on either side: the pin imports ./plugin.js and plugin.ts imports ./view-container-expansion.js, both relative, and this package's vitest aliases redirect only @objectstack/core and @objectstack/spec to their own src — no dist is in the resolution path.

Verification

All of the following ran on 6f581cb60, the branch head after origin/main (1dcb995f2, PR #14340 included) was merged in.

  • pnpm --filter @objectstack/metadata test42 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-wide eslint . --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 is src/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.
  • The 36 gate families node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derives 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 saved turbo run test log CI tees; its own text says "the local reading for this gate is NOT MEASURED... it is not a red") and check-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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/ai/agents.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/api/client-sdk.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/api/error-catalog.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/data-modeling/index.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/deployment/validating-metadata.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/permissions/authorization.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/ui/index.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))

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

  • content/docs/releases/v13.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))

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 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 12 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 5c9e40ad91028b57b0748e3ea0347189bac72ce9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4857ce2ed70371df3dcddf64a436c045650522de — the merge of head 6f581cb60bbdbcb8f06ea84319ec2d13b27d131f into base 5c9e40ad91028b57b0748e3ea0347189bac72ce9, 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 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

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Enqueue provenance (domain:engine execution seat, session_0112hMx9hjJ9BgB28X97DS68): ACCEPT on #13912 (comment 5504343166) → at 04:43Z every check run on head 6f581cb60 was completed with success or skipped (33 runs; Lint & Repo Gates finished 04:41Z), governed-surface test on the three changed paths: NOT governed, mergeable_state not dirty → marked ready and auto-merge (squash) enabled. Landing is by the merge queue; the engine seat follows it to MERGED.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 41aa979Sep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-13912-artifact-loader-container-object branch September 2, 2026 05:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

metadata/plugin.ts: artifact/HMR view-container loader never reads the container's own top-level object field

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(metadata): read a view container's own object in the artifact/HMR loader - #14401

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-13912-artifact-loader-container-object
Sep 2, 2026
Merged

fix(metadata): read a view container's own object in the artifact/HMR loader#14401
os-musk merged 2 commits into
mainfrom
claude/issue-13912-artifact-loader-container-object

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#13912

MetadataPlugin._parseAndRegisterArtifact — the artifact/HMR loader, one of the two SOURCE view registrars — derives which object an aggregated defineView container binds to at two sites, and both walked exactly two levels, never consulting the container's own top-level object. That is the field ViewSchema.object documents as the authorial signal, in a .describe() that names its own readers: "Object this container binds to — how a stack-level views: [...] entry says which object its views belong to; read by getViewsByObject() / GET /meta/view?object=".

A package-shipped defineView({ object: 'crm_lead', list: { columns: [...] } }) therefore registered nothing at all through this path: the first site's if (!viewObject) continue dropped the container before any registration, so list('view') never returned it, the #13913 read backstop had nothing to expand, and getViewsByObject('crm_lead') / GET /meta/view?object=crm_lead answered 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):

// beforeconstviewObject=(itemasany)?.list?.data?.object??(itemasany)?.form?.data?.object;if(!viewObject)continue;// afterconstviewObject=deriveViewContainerObject(item);if(!viewObject)continue;

packages/metadata/src/plugin.ts, the fall-through registrar (was :973-975):

// beforeletname=(itemasany)?.name;if(!name){if(metaType==='view'){name=(itemasany)?.list?.data?.object??(itemasany)?.form?.data?.object;}}// afterletname=(itemasany)?.name;if(!name){if(metaType==='view'){name=deriveViewContainerObject(item);}}

deriveViewContainerObject is object then list.data.object then form.data.object then the row's own name — the order #13407 settled at the runtime door. On the second site its last term is vacuous by construction: that branch is reached only when item.name is falsy, so what the call adds there is exactly the leading object term. On the first site the name term 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.jsondependencies includes "@objectstack/metadata": "workspace:*".
  • packages/metadata/package.jsondependencies does not include @objectstack/metadata-protocol (it is core, metadata-core, metadata-fs, platform-objects, spec, types, plus chokidar/glob/js-yaml/zod).

So importing the protocol's expandRuntimeViewContainer into plugin.ts would invert an existing edge and close a cycle, and promoting that private method to a public export would widen metadata-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 exports deriveViewContainerObject, 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.ts walks two levels, and protocol.ts walks four since #13407). deriveViewContainerObject is 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 (object then list then form, no name), which is what the card's fence forbids.

Only the derivation is shared. Expansion, per-item applyProtection with the manifest's package coordinates, and the expansion-warning logging stay in plugin.ts — they differ from expandRuntimeViewContainer's container-provenance stamping and are this registrar's own contract.

protocol.ts reconciliation reading (triage's open question)

Triage (comment 5486856691) recorded two opposite orders inside packages/metadata-protocol/src/protocol.ts and asked the implementer to re-check whether #13929 settled it. Re-measured at origin/main1dcb995f2:

Expansion path, expandRuntimeViewContainer (protocol.ts:13513):

constviewObject=(typeofcontainer.object==='string'&&container.object ? container.object : undefined)??container?.list?.data?.object??container?.form?.data?.object??(typeofcontainer.name==='string' ? container.name : undefined);

Diagnostic path (protocol.ts:7514):

constsourceObject=viewDoc?.object??viewDoc?.data?.object??viewDoc?.objectName??viewDoc?.list?.data?.object;

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 carrying data.object / objectName), the expansion path reads an aggregated container. Nothing filed for it, and protocol.ts is untouched here.

One residual divergence was found and is filed unassigned as #14399: the two SOURCE registrars put the row's own name in opposite positions — first in packages/objectql/src/engine.ts's resolveMetadataItemName, last in deriveViewContainerObject — so a container whose name is 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 bare ObjectStackDefinition, so each fixture also passes the door's strict parse.

That last point is load-bearing and was measured, not assumed: ObjectListViewSchema.datarequiresobject when data is present at all, so a container carrying list: { data: { provider: 'object' } } with no object is 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 no data at all — and that is what the fixture uses. A pin written on the refused shape would have been testing the schema, not this registrar.

  • Repro — container with object: 'crm_lead' and no data.object anywhere: the container registers under crm_lead, the expansion crm_lead.default / crm_lead.hot / crm_lead.edit registers (4 items total), and getViewsByObject('crm_lead') answers with those three and never with the container itself ([finding] the nested-plugin seam does not expand an aggregated views container — a nested plugin's per-view items never reach the registry, so getViewsByObject() / GET /meta/view?object= answer with the container alone #7163).
  • Ordering pin — a container setting BOTH object: 'crm_lead' and a disagreeing list.data.object: 'crm_account' registers under crm_lead, and nothing lands under crm_account. No container shipped in this repo sets both (measured: zero top-level object keys across all 13 defineView call sites in examples/, packages/lint and packages/qa), so nothing existing moves — the pin exists so the derivations cannot drift apart again.
  • CONTROL — a container bound only through list.data.object still registers and expands unchanged.
  • CONTROL — a container with no derivable binding at all (no object, no data.object, no name) 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 own object go 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 remaining deriveViewContainerObject(item) call sites, on-disk blob hash 67c8a807 differing from the HEAD blob 0b22c5f2), and the restore was proved after (on-disk hash back to 0b22c5f2, git diff HEAD empty). No rebuild leg is owed on either side: the pin imports ./plugin.js and plugin.ts imports ./view-container-expansion.js, both relative, and this package's vitest aliases redirect only @objectstack/core and @objectstack/spec to their own src — no dist is in the resolution path.

Verification

All of the following ran on 6f581cb60, the branch head after origin/main (1dcb995f2, PR #14340 included) was merged in.

  • pnpm --filter @objectstack/metadata test42 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-wide eslint . --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 is src/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.
  • The 36 gate families node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derives 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 saved turbo run test log CI tees; its own text says "the local reading for this gate is NOT MEASURED... it is not a red") and check-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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/ai/agents.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/api/client-sdk.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/api/error-catalog.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/data-modeling/index.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/deployment/validating-metadata.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/permissions/authorization.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/ui/index.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))

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

  • content/docs/releases/v13.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))

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 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 12 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 5c9e40ad91028b57b0748e3ea0347189bac72ce9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4857ce2ed70371df3dcddf64a436c045650522de — the merge of head 6f581cb60bbdbcb8f06ea84319ec2d13b27d131f into base 5c9e40ad91028b57b0748e3ea0347189bac72ce9, 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 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

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Enqueue provenance (domain:engine execution seat, session_0112hMx9hjJ9BgB28X97DS68): ACCEPT on #13912 (comment 5504343166) → at 04:43Z every check run on head 6f581cb60 was completed with success or skipped (33 runs; Lint & Repo Gates finished 04:41Z), governed-surface test on the three changed paths: NOT governed, mergeable_state not dirty → marked ready and auto-merge (squash) enabled. Landing is by the merge queue; the engine seat follows it to MERGED.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 41aa979Sep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-13912-artifact-loader-container-object branch September 2, 2026 05:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

metadata/plugin.ts: artifact/HMR view-container loader never reads the container's own top-level object field

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(metadata): read a view container's own object in the artifact/HMR loader - #14401

Merged
os-musk merged 2 commits into
mainfrom
claude/issue-13912-artifact-loader-container-object
Sep 2, 2026
Merged

fix(metadata): read a view container's own object in the artifact/HMR loader#14401
os-musk merged 2 commits into
mainfrom
claude/issue-13912-artifact-loader-container-object

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#13912

MetadataPlugin._parseAndRegisterArtifact — the artifact/HMR loader, one of the two SOURCE view registrars — derives which object an aggregated defineView container binds to at two sites, and both walked exactly two levels, never consulting the container's own top-level object. That is the field ViewSchema.object documents as the authorial signal, in a .describe() that names its own readers: "Object this container binds to — how a stack-level views: [...] entry says which object its views belong to; read by getViewsByObject() / GET /meta/view?object=".

A package-shipped defineView({ object: 'crm_lead', list: { columns: [...] } }) therefore registered nothing at all through this path: the first site's if (!viewObject) continue dropped the container before any registration, so list('view') never returned it, the #13913 read backstop had nothing to expand, and getViewsByObject('crm_lead') / GET /meta/view?object=crm_lead answered 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):

// beforeconstviewObject=(itemasany)?.list?.data?.object??(itemasany)?.form?.data?.object;if(!viewObject)continue;// afterconstviewObject=deriveViewContainerObject(item);if(!viewObject)continue;

packages/metadata/src/plugin.ts, the fall-through registrar (was :973-975):

// beforeletname=(itemasany)?.name;if(!name){if(metaType==='view'){name=(itemasany)?.list?.data?.object??(itemasany)?.form?.data?.object;}}// afterletname=(itemasany)?.name;if(!name){if(metaType==='view'){name=deriveViewContainerObject(item);}}

deriveViewContainerObject is object then list.data.object then form.data.object then the row's own name — the order #13407 settled at the runtime door. On the second site its last term is vacuous by construction: that branch is reached only when item.name is falsy, so what the call adds there is exactly the leading object term. On the first site the name term 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.jsondependencies includes "@objectstack/metadata": "workspace:*".
  • packages/metadata/package.jsondependencies does not include @objectstack/metadata-protocol (it is core, metadata-core, metadata-fs, platform-objects, spec, types, plus chokidar/glob/js-yaml/zod).

So importing the protocol's expandRuntimeViewContainer into plugin.ts would invert an existing edge and close a cycle, and promoting that private method to a public export would widen metadata-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 exports deriveViewContainerObject, 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.ts walks two levels, and protocol.ts walks four since #13407). deriveViewContainerObject is 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 (object then list then form, no name), which is what the card's fence forbids.

Only the derivation is shared. Expansion, per-item applyProtection with the manifest's package coordinates, and the expansion-warning logging stay in plugin.ts — they differ from expandRuntimeViewContainer's container-provenance stamping and are this registrar's own contract.

protocol.ts reconciliation reading (triage's open question)

Triage (comment 5486856691) recorded two opposite orders inside packages/metadata-protocol/src/protocol.ts and asked the implementer to re-check whether #13929 settled it. Re-measured at origin/main1dcb995f2:

Expansion path, expandRuntimeViewContainer (protocol.ts:13513):

constviewObject=(typeofcontainer.object==='string'&&container.object ? container.object : undefined)??container?.list?.data?.object??container?.form?.data?.object??(typeofcontainer.name==='string' ? container.name : undefined);

Diagnostic path (protocol.ts:7514):

constsourceObject=viewDoc?.object??viewDoc?.data?.object??viewDoc?.objectName??viewDoc?.list?.data?.object;

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 carrying data.object / objectName), the expansion path reads an aggregated container. Nothing filed for it, and protocol.ts is untouched here.

One residual divergence was found and is filed unassigned as #14399: the two SOURCE registrars put the row's own name in opposite positions — first in packages/objectql/src/engine.ts's resolveMetadataItemName, last in deriveViewContainerObject — so a container whose name is 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 bare ObjectStackDefinition, so each fixture also passes the door's strict parse.

That last point is load-bearing and was measured, not assumed: ObjectListViewSchema.datarequiresobject when data is present at all, so a container carrying list: { data: { provider: 'object' } } with no object is 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 no data at all — and that is what the fixture uses. A pin written on the refused shape would have been testing the schema, not this registrar.

  • Repro — container with object: 'crm_lead' and no data.object anywhere: the container registers under crm_lead, the expansion crm_lead.default / crm_lead.hot / crm_lead.edit registers (4 items total), and getViewsByObject('crm_lead') answers with those three and never with the container itself ([finding] the nested-plugin seam does not expand an aggregated views container — a nested plugin's per-view items never reach the registry, so getViewsByObject() / GET /meta/view?object= answer with the container alone #7163).
  • Ordering pin — a container setting BOTH object: 'crm_lead' and a disagreeing list.data.object: 'crm_account' registers under crm_lead, and nothing lands under crm_account. No container shipped in this repo sets both (measured: zero top-level object keys across all 13 defineView call sites in examples/, packages/lint and packages/qa), so nothing existing moves — the pin exists so the derivations cannot drift apart again.
  • CONTROL — a container bound only through list.data.object still registers and expands unchanged.
  • CONTROL — a container with no derivable binding at all (no object, no data.object, no name) 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 own object go 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 remaining deriveViewContainerObject(item) call sites, on-disk blob hash 67c8a807 differing from the HEAD blob 0b22c5f2), and the restore was proved after (on-disk hash back to 0b22c5f2, git diff HEAD empty). No rebuild leg is owed on either side: the pin imports ./plugin.js and plugin.ts imports ./view-container-expansion.js, both relative, and this package's vitest aliases redirect only @objectstack/core and @objectstack/spec to their own src — no dist is in the resolution path.

Verification

All of the following ran on 6f581cb60, the branch head after origin/main (1dcb995f2, PR #14340 included) was merged in.

  • pnpm --filter @objectstack/metadata test42 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-wide eslint . --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 is src/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.
  • The 36 gate families node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derives 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 saved turbo run test log CI tees; its own text says "the local reading for this gate is NOT MEASURED... it is not a red") and check-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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/ai/agents.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/api/client-sdk.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/api/error-catalog.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/data-modeling/index.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/deployment/validating-metadata.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/permissions/authorization.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))
  • content/docs/ui/index.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))

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

  • content/docs/releases/v13.mdx _(via crm_lead (literal, a string literal in parseAndRegisterArtifact))

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 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 12 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 5c9e40ad91028b57b0748e3ea0347189bac72ce9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4857ce2ed70371df3dcddf64a436c045650522de — the merge of head 6f581cb60bbdbcb8f06ea84319ec2d13b27d131f into base 5c9e40ad91028b57b0748e3ea0347189bac72ce9, 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 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

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Enqueue provenance (domain:engine execution seat, session_0112hMx9hjJ9BgB28X97DS68): ACCEPT on #13912 (comment 5504343166) → at 04:43Z every check run on head 6f581cb60 was completed with success or skipped (33 runs; Lint & Repo Gates finished 04:41Z), governed-surface test on the three changed paths: NOT governed, mergeable_state not dirty → marked ready and auto-merge (squash) enabled. Landing is by the merge queue; the engine seat follows it to MERGED.


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 41aa979Sep 2, 2026
35 checks passed
@os-musk
os-musk deleted the claude/issue-13912-artifact-loader-container-object branch September 2, 2026 05:08
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

metadata/plugin.ts: artifact/HMR view-container loader never reads the container's own top-level object field

2 participants

@os-musk@claude