fix(objectql,metadata): derive a view container's object through the shared helper, so the row's own name is LAST at every SOURCE registrar (#14399) - #14665

Merged
os-musk merged 5 commits into
mainfrom
claude/issue-14399-view-container-name-order
Sep 2, 2026
Merged

fix(objectql,metadata): derive a view container's object through the shared helper, so the row's own name is LAST at every SOURCE registrar (#14399)#14665
os-musk merged 5 commits into
mainfrom
claude/issue-14399-view-container-name-order

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14399

What was wrong

Three sites derive "which object does an aggregated defineView container bind to". After #13407 / #13913 / #13912 all three read the container's own top-level object before the list.data.object chain, but they still disagreed about the row's own name:

SiteChainname
packages/objectql/src/engine.tsresolveMetadataItemName('views', item) — the boot-loop SOURCE registrarname then id then object then list.data.object then form.data.objectFIRST
deriveViewContainerObject (@objectstack/metadata) — the artifact/HMR SOURCE registrar and getViewsByObject()object then list.data.object then form.data.object then nameLAST
expandRuntimeViewContainer (@objectstack/metadata-protocol) — the runtime doorsame as aboveLAST

A container written as { name: 'lead_views', object: 'crm_lead', list: { … } } was therefore keyed lead_views by the boot loop and crm_lead everywhere else, expansion (OBJECT.KEY items) included. getViewsByObject() and GET /meta/view?object= filter the expanded items by their object, so which registrar loaded the document decided whether the views were addressable under the object at all. No error, no diagnostic.

The change

The boot loop's CONTAINER branch now calls deriveViewContainerObjectby import, not by re-spelling. A fourth hand-copy of a chain that already existed three times was the defect, not the repair; the tail of the same function now calls the helper too, so engine.ts holds zero copies of the chain where it used to hold one.

The direction is the 2026-08-07 meta-rule rather than taste: one operation with two inconsistent implementations, the side bound by a DECLARATION wins. ViewSchema.object's own .describe() names its readers; the boot loop's order argued from item identity, which declares nothing about the binding. The two sites that already held the winning order are untouched.

deriveViewContainerObject was module-local, so it is added to @objectstack/metadata's root entry (minor for that package). Measured on this tree: packages/objectql/package.json declares @objectstack/metadata as workspace:*, and @objectstack/metadata's full transitive dependency closure is 6 packages with @objectstack/objectqlnot among them — no cycle, and no deep relative import into another package's src/.

Confinement. Only the container branch moves, gated on isAggregatedViewContainer. The assembled viewItems: channel still keys by its own name first — every member of AssembledViewArtifactSchema requires viewKind, so isAggregatedViewContainer is false for all of them, and there is a control asserting exactly that. item.id is untouched and cannot fire for a container: ViewSchema is a strictObject declaring name and object and no id.

Bounded in-place fix, named per the standing clause: the docblock above resolveMetadataItemName asserted "per spec, ViewSchema does NOT have a top-level name field", which was the stated premise for reading name first. It is measurably false — ViewSchema declares an optional name at view.zod.ts — so the docblock now records what the field actually is and what the old sentence got wrong. Same defect class, same file, mechanically settled by the spec source, no new verification surface.

MEASURED CORRECTION to the card

The card predicted the artifact/HMR registrar would "derive crm_lead and mint crm_lead.default, registering the container under crm_lead" — two SILENT keys for one document. The second half is false, and the divergence is sharper than filed:

The boot loop reconciles that field (toRegister = { ...item, name: itemName }) and the artifact door does not. That residual asymmetry is a separate defect at a separate site; #14399 owns the derivation, and the asymmetry is filed on its own card and pinned as an assertion here so it cannot drift unnoticed.

The fixture is the deliverable

No fixture anywhere in the repo set a container name that differed from its bound object, so the divergence was un-rehearsed in BOTH directions — each registrar was individually green on every shape it had ever been shown, which is also why the correction above had gone unnoticed. packages/objectql/src/view-container-divergent-name-registrars.test.ts drives both SOURCE registrars side by side on one document: the boot loop through ObjectQL.registerApp, the artifact door through MetadataPlugin._parseAndRegisterArtifact, exactly as its own #13912 pin drives it. Four controls (name equals object; no object anywhere; the anonymous container both registrars accept; a standalone ViewItem) are green in both directions.

Verification

Head at the time of the runs below: a95087993.

Reverse verification (ablation), direction predicted before running — the boot-loop legs go red, the artifact-door derivation leg stays green because it does not read engine.ts:

  • mutation proved on disk, not by the editor's exit code: anchor hits 1, injected marker count 1, blob 37ab4cb6 to 775ef0b1;
  • no rebuild leg is owed on this mutation and that is stated rather than assumed — the test imports ./engine, a relative SOURCE specifier, so vitest reads the mutated file directly; the helper it calls comes from @objectstack/metadata's dist, which the mutation does not touch;
  • result: 3 failed / 6 passedexpected [ 'lead_views', …(2) ] to deeply equal [ Array(3) ] and expected [ 'lead_views' ] to deeply equal [ 'crm_lead' ]. The "artifact/HMR registrar derives the SAME binding" case stayed green, as predicted;
  • restore under an EXIT INT TERM trap with absolute paths, proved by blob equality (37ab4cb6 equals the HEAD blob), git diff HEAD empty, marker count 0.

Suites and gates, all on a95087993:

  • pnpm --filter @objectstack/objectql exec vitest run — 264 files / 4556 tests passed
  • pnpm --filter @objectstack/metadata exec vitest run — 43 files / 677 tests passed
  • pnpm --filter @objectstack/objectql typecheckcheck:test-typecheck: OK ... 44 file(s) / 242 error(s) / 69 pinned signature(s) (unmoved; the new test file compiles clean and adds no debt)
  • pnpm lint (whole repo, eslint . --no-inline-config) — exit 0, no narrowing claimed
  • pnpm check:nul-bytesOK (scanned 7997 text file(s) ... no raw ASCII control bytes); pnpm check:error-status-conformance — exit 0
  • the full union derived on this tree by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (62 commands): 57 exit 0, including check-system-context-census, check-cross-package-test-inputs, check:test-source-alias, check:published-files, check:type-check-coverage, check:type-source-resolution, check:undeclared-dep-imports, check:engine-split-ratio
  • 5 NOT MEASURED locally, in each gate's own words, never read as green:check-test-completeness and check:type-check-debt and check:dual-build-cjs-loads (exit 3 — each needs a saved turbo test log or a built package closure), check:pm/half-states (exit 3 — repo-scoped GitHub REST answers 403 in this container), and check:skill-examples (exit 1 on its prerequisite branch: packages/client-react/dist holds no .d.ts declarations — the package is not built). Building the closure to measure them was attempted twice and killed by this container's 10-minute foreground cap both times, so the narrowing is declared rather than hidden; CI builds the closure and runs all five.

content/docs/permissions/system-context.mdx was re-anchored by node scripts/check-system-context-census.mjs --fix on the merged tree (15 anchors rewritten), never hand-edited; the gate is green on the final head.

Migration

None. Measured on this tree: of the 54 non-test sources that author or carry view containers, zero declare a name that differs from the object they bind to, so every in-tree container derived identically at all three sites before this change and does after it. What moves is the latent shape only.

Clause-2

yes — self-read from the final diff, against the seat's no. The content limb is hit: packages/metadata/src/index.ts adds a new export to the package's public entry, which widens the published surface (the minor in the changeset is the same fact). No path-limb file is touched — the diff does not enter packages/spec/src/**. needs:contract-review is hung on this PR accordingly.

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…shared helper, so the row's own `name` is LAST at every SOURCE registrar (#14399)
The ObjectQL boot-loop registrar read a container's own `name` FIRST, while
`deriveViewContainerObject` (@objectstack/metadata) and
`expandRuntimeViewContainer` (@objectstack/metadata-protocol) read it LAST. A
container written as `{ name: 'lead_views', object: 'crm_lead', list: { … } }`
was therefore keyed `lead_views` by one SOURCE registrar and `crm_lead` by the
others, expansion included — and `getViewsByObject()` / `GET /meta/view?object=`
filter on the expanded items' `object`, so which registrar loaded the document
decided whether the views were addressable under the object at all.
The boot loop's container branch now calls `deriveViewContainerObject` by
import; a fourth hand-copy of the chain was the defect, not the repair.
`deriveViewContainerObject` moves onto `@objectstack/metadata`'s root entry to
make that import legal without reaching into another package's `src/`.
Only the CONTAINER branch moves, gated on `isAggregatedViewContainer`; the
assembled `viewItems:` channel still keys by its own `name` first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…st view-container sources are divergent) (#14399)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… shift (#14399)
Generated by `node scripts/check-system-context-census.mjs --fix` on the merged
tree; 15 anchors rewritten, never hand-edited.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata, @objectstack/objectql, touching 4 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/metadata/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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 resolveMetadataItemName))
  • content/docs/api/client-sdk.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/api/error-catalog.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/data-modeling/index.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/deployment/validating-metadata.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/permissions/authorization.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/ui/index.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))

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 resolveMetadataItemName))

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
  • 1 changed file(s) yielded no anchor (packages/metadata/src/index.ts) — pages documenting those are invisible to this run
  • 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 — 24 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 2aa8456cf2d66ec3825d262686fe4218e57cfd27packageMentionDocs.

Which tree this was computed on

This run read content/docs from dc744ff995c04d7c29856663afb483fcf15c20f7 — the merge of head a95087993bc3ec03604b95e0596b29250457170c into base 2aa8456cf2d66ec3825d262686fe4218e57cfd27, 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 dc744ff995c04d7c29856663afb483fcf15c20f7 && git checkout dc744ff995c04d7c29856663afb483fcf15c20f7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2aa8456cf2d66ec3825d262686fe4218e57cfd27 a95087993bc3ec03604b95e0596b29250457170c && git checkout -B drift-repro 2aa8456cf2d66ec3825d262686fe4218e57cfd27 && git merge --no-ff a95087993bc3ec03604b95e0596b29250457170c
node scripts/docs-audit/affected-docs.mjs --json 2aa8456cf2d66ec3825d262686fe4218e57cfd27

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session session_0112hMx9hjJ9BgB28X97DS68): flipped ready at 18:13Z and armed auto-merge (squash) at 18:13:38Z on head a95087993. The echo read method: MERGE; the queue build ref is the arming reading, not the echo.

  • Contract review: PASS, adopted verbatim from an isolated reviewer explicitly passed model: fable — not reviewed in-seat. The tier fuse was read with get_session and blew: CONTRACT_REVIEW_TIER is claude-fable-5-1 while this session's session_context.model and last_served_model both read claude-opus-5. Reviewer transcript verified per assistant message before adoption: 79 of 79 report claude-fable-5-1. Full verdict on the card, comment 5514186759.
  • Clause-② yes, derived independently by the reviewer from the final diff — content limb, widening sub-limb (packages/metadata/src/index.ts:56). This seat dispatched no and was wrong; the dev's Zone 2 measurement caught it. needs:contract-review cleared from both carriers (card The two SOURCE view-container registrars disagree on where the row's own name sits in the object-derivation chain #14399 and this PR) in one stroke at PASS.
  • Governed-surface test re-run on the final five-path file list: 0 of 5 path(s) hit the register — ordinary queue landing. Re-derived rather than recalled, per the script's own warning that the register has grown several times in two days.
  • Enqueue bar: all 44 check runs on a95087993 completed success or skipped, read with perPage: 50. The last one standing was Check Changeset, which was re-triggered at 18:11:57Z by this seat's own label edit and came back green at 18:12:50Z — the flip waited for it rather than being armed on a partial reading, even though the same check had already passed twice on this identical head.

⚠️ Known exposure, stated in advance: the queue build runs the full suite, so this PR meets packages/cli/test/run-dev-unbuilt-workspace.e2e.test.ts — the flake tracked on #14648, which ejected #14629 at 17:45Z. If this PR is ejected the same way the disposition is fixed: ⛔ no re-queue while #14648 is open and unfixed, ⛔ no weakening of that test, and the card goes to pm:blocked with an Unlock-action: line rather than being re-dispatched.

Owed by the seat at MERGED: verify by content on origin/main that engine.ts holds zero copies of the derivation chain and that deriveViewContainerObject is on @objectstack/metadata's root entry, strip pm:dispatched from #14399, landing record on the card, inventory reconciliation. The reviewer's advisory finding is already filed as #14680, and the dev's residual-asymmetry finding as #14666; neither blocks this landing.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33669892536 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 12 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33671791799 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 14 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 3c1bbd2Sep 2, 2026
46 checks passed
@os-musk
os-musk deleted the claude/issue-14399-view-container-name-order branch September 2, 2026 19:59
This was referenced Sep 2, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The two SOURCE view-container registrars disagree on where the row's own name sits in the object-derivation chain

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(objectql,metadata): derive a view container's object through the shared helper, so the row's own name is LAST at every SOURCE registrar (#14399) - #14665

Merged
os-musk merged 5 commits into
mainfrom
claude/issue-14399-view-container-name-order
Sep 2, 2026
Merged

fix(objectql,metadata): derive a view container's object through the shared helper, so the row's own name is LAST at every SOURCE registrar (#14399)#14665
os-musk merged 5 commits into
mainfrom
claude/issue-14399-view-container-name-order

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14399

What was wrong

Three sites derive "which object does an aggregated defineView container bind to". After #13407 / #13913 / #13912 all three read the container's own top-level object before the list.data.object chain, but they still disagreed about the row's own name:

SiteChainname
packages/objectql/src/engine.tsresolveMetadataItemName('views', item) — the boot-loop SOURCE registrarname then id then object then list.data.object then form.data.objectFIRST
deriveViewContainerObject (@objectstack/metadata) — the artifact/HMR SOURCE registrar and getViewsByObject()object then list.data.object then form.data.object then nameLAST
expandRuntimeViewContainer (@objectstack/metadata-protocol) — the runtime doorsame as aboveLAST

A container written as { name: 'lead_views', object: 'crm_lead', list: { … } } was therefore keyed lead_views by the boot loop and crm_lead everywhere else, expansion (OBJECT.KEY items) included. getViewsByObject() and GET /meta/view?object= filter the expanded items by their object, so which registrar loaded the document decided whether the views were addressable under the object at all. No error, no diagnostic.

The change

The boot loop's CONTAINER branch now calls deriveViewContainerObjectby import, not by re-spelling. A fourth hand-copy of a chain that already existed three times was the defect, not the repair; the tail of the same function now calls the helper too, so engine.ts holds zero copies of the chain where it used to hold one.

The direction is the 2026-08-07 meta-rule rather than taste: one operation with two inconsistent implementations, the side bound by a DECLARATION wins. ViewSchema.object's own .describe() names its readers; the boot loop's order argued from item identity, which declares nothing about the binding. The two sites that already held the winning order are untouched.

deriveViewContainerObject was module-local, so it is added to @objectstack/metadata's root entry (minor for that package). Measured on this tree: packages/objectql/package.json declares @objectstack/metadata as workspace:*, and @objectstack/metadata's full transitive dependency closure is 6 packages with @objectstack/objectqlnot among them — no cycle, and no deep relative import into another package's src/.

Confinement. Only the container branch moves, gated on isAggregatedViewContainer. The assembled viewItems: channel still keys by its own name first — every member of AssembledViewArtifactSchema requires viewKind, so isAggregatedViewContainer is false for all of them, and there is a control asserting exactly that. item.id is untouched and cannot fire for a container: ViewSchema is a strictObject declaring name and object and no id.

Bounded in-place fix, named per the standing clause: the docblock above resolveMetadataItemName asserted "per spec, ViewSchema does NOT have a top-level name field", which was the stated premise for reading name first. It is measurably false — ViewSchema declares an optional name at view.zod.ts — so the docblock now records what the field actually is and what the old sentence got wrong. Same defect class, same file, mechanically settled by the spec source, no new verification surface.

MEASURED CORRECTION to the card

The card predicted the artifact/HMR registrar would "derive crm_lead and mint crm_lead.default, registering the container under crm_lead" — two SILENT keys for one document. The second half is false, and the divergence is sharper than filed:

The boot loop reconciles that field (toRegister = { ...item, name: itemName }) and the artifact door does not. That residual asymmetry is a separate defect at a separate site; #14399 owns the derivation, and the asymmetry is filed on its own card and pinned as an assertion here so it cannot drift unnoticed.

The fixture is the deliverable

No fixture anywhere in the repo set a container name that differed from its bound object, so the divergence was un-rehearsed in BOTH directions — each registrar was individually green on every shape it had ever been shown, which is also why the correction above had gone unnoticed. packages/objectql/src/view-container-divergent-name-registrars.test.ts drives both SOURCE registrars side by side on one document: the boot loop through ObjectQL.registerApp, the artifact door through MetadataPlugin._parseAndRegisterArtifact, exactly as its own #13912 pin drives it. Four controls (name equals object; no object anywhere; the anonymous container both registrars accept; a standalone ViewItem) are green in both directions.

Verification

Head at the time of the runs below: a95087993.

Reverse verification (ablation), direction predicted before running — the boot-loop legs go red, the artifact-door derivation leg stays green because it does not read engine.ts:

  • mutation proved on disk, not by the editor's exit code: anchor hits 1, injected marker count 1, blob 37ab4cb6 to 775ef0b1;
  • no rebuild leg is owed on this mutation and that is stated rather than assumed — the test imports ./engine, a relative SOURCE specifier, so vitest reads the mutated file directly; the helper it calls comes from @objectstack/metadata's dist, which the mutation does not touch;
  • result: 3 failed / 6 passedexpected [ 'lead_views', …(2) ] to deeply equal [ Array(3) ] and expected [ 'lead_views' ] to deeply equal [ 'crm_lead' ]. The "artifact/HMR registrar derives the SAME binding" case stayed green, as predicted;
  • restore under an EXIT INT TERM trap with absolute paths, proved by blob equality (37ab4cb6 equals the HEAD blob), git diff HEAD empty, marker count 0.

Suites and gates, all on a95087993:

  • pnpm --filter @objectstack/objectql exec vitest run — 264 files / 4556 tests passed
  • pnpm --filter @objectstack/metadata exec vitest run — 43 files / 677 tests passed
  • pnpm --filter @objectstack/objectql typecheckcheck:test-typecheck: OK ... 44 file(s) / 242 error(s) / 69 pinned signature(s) (unmoved; the new test file compiles clean and adds no debt)
  • pnpm lint (whole repo, eslint . --no-inline-config) — exit 0, no narrowing claimed
  • pnpm check:nul-bytesOK (scanned 7997 text file(s) ... no raw ASCII control bytes); pnpm check:error-status-conformance — exit 0
  • the full union derived on this tree by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (62 commands): 57 exit 0, including check-system-context-census, check-cross-package-test-inputs, check:test-source-alias, check:published-files, check:type-check-coverage, check:type-source-resolution, check:undeclared-dep-imports, check:engine-split-ratio
  • 5 NOT MEASURED locally, in each gate's own words, never read as green:check-test-completeness and check:type-check-debt and check:dual-build-cjs-loads (exit 3 — each needs a saved turbo test log or a built package closure), check:pm/half-states (exit 3 — repo-scoped GitHub REST answers 403 in this container), and check:skill-examples (exit 1 on its prerequisite branch: packages/client-react/dist holds no .d.ts declarations — the package is not built). Building the closure to measure them was attempted twice and killed by this container's 10-minute foreground cap both times, so the narrowing is declared rather than hidden; CI builds the closure and runs all five.

content/docs/permissions/system-context.mdx was re-anchored by node scripts/check-system-context-census.mjs --fix on the merged tree (15 anchors rewritten), never hand-edited; the gate is green on the final head.

Migration

None. Measured on this tree: of the 54 non-test sources that author or carry view containers, zero declare a name that differs from the object they bind to, so every in-tree container derived identically at all three sites before this change and does after it. What moves is the latent shape only.

Clause-2

yes — self-read from the final diff, against the seat's no. The content limb is hit: packages/metadata/src/index.ts adds a new export to the package's public entry, which widens the published surface (the minor in the changeset is the same fact). No path-limb file is touched — the diff does not enter packages/spec/src/**. needs:contract-review is hung on this PR accordingly.

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…shared helper, so the row's own `name` is LAST at every SOURCE registrar (#14399)
The ObjectQL boot-loop registrar read a container's own `name` FIRST, while
`deriveViewContainerObject` (@objectstack/metadata) and
`expandRuntimeViewContainer` (@objectstack/metadata-protocol) read it LAST. A
container written as `{ name: 'lead_views', object: 'crm_lead', list: { … } }`
was therefore keyed `lead_views` by one SOURCE registrar and `crm_lead` by the
others, expansion included — and `getViewsByObject()` / `GET /meta/view?object=`
filter on the expanded items' `object`, so which registrar loaded the document
decided whether the views were addressable under the object at all.
The boot loop's container branch now calls `deriveViewContainerObject` by
import; a fourth hand-copy of the chain was the defect, not the repair.
`deriveViewContainerObject` moves onto `@objectstack/metadata`'s root entry to
make that import legal without reaching into another package's `src/`.
Only the CONTAINER branch moves, gated on `isAggregatedViewContainer`; the
assembled `viewItems:` channel still keys by its own `name` first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…st view-container sources are divergent) (#14399)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… shift (#14399)
Generated by `node scripts/check-system-context-census.mjs --fix` on the merged
tree; 15 anchors rewritten, never hand-edited.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata, @objectstack/objectql, touching 4 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/metadata/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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 resolveMetadataItemName))
  • content/docs/api/client-sdk.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/api/error-catalog.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/data-modeling/index.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/deployment/validating-metadata.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/permissions/authorization.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/ui/index.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))

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 resolveMetadataItemName))

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
  • 1 changed file(s) yielded no anchor (packages/metadata/src/index.ts) — pages documenting those are invisible to this run
  • 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 — 24 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 2aa8456cf2d66ec3825d262686fe4218e57cfd27packageMentionDocs.

Which tree this was computed on

This run read content/docs from dc744ff995c04d7c29856663afb483fcf15c20f7 — the merge of head a95087993bc3ec03604b95e0596b29250457170c into base 2aa8456cf2d66ec3825d262686fe4218e57cfd27, 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 dc744ff995c04d7c29856663afb483fcf15c20f7 && git checkout dc744ff995c04d7c29856663afb483fcf15c20f7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2aa8456cf2d66ec3825d262686fe4218e57cfd27 a95087993bc3ec03604b95e0596b29250457170c && git checkout -B drift-repro 2aa8456cf2d66ec3825d262686fe4218e57cfd27 && git merge --no-ff a95087993bc3ec03604b95e0596b29250457170c
node scripts/docs-audit/affected-docs.mjs --json 2aa8456cf2d66ec3825d262686fe4218e57cfd27

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session session_0112hMx9hjJ9BgB28X97DS68): flipped ready at 18:13Z and armed auto-merge (squash) at 18:13:38Z on head a95087993. The echo read method: MERGE; the queue build ref is the arming reading, not the echo.

  • Contract review: PASS, adopted verbatim from an isolated reviewer explicitly passed model: fable — not reviewed in-seat. The tier fuse was read with get_session and blew: CONTRACT_REVIEW_TIER is claude-fable-5-1 while this session's session_context.model and last_served_model both read claude-opus-5. Reviewer transcript verified per assistant message before adoption: 79 of 79 report claude-fable-5-1. Full verdict on the card, comment 5514186759.
  • Clause-② yes, derived independently by the reviewer from the final diff — content limb, widening sub-limb (packages/metadata/src/index.ts:56). This seat dispatched no and was wrong; the dev's Zone 2 measurement caught it. needs:contract-review cleared from both carriers (card The two SOURCE view-container registrars disagree on where the row's own name sits in the object-derivation chain #14399 and this PR) in one stroke at PASS.
  • Governed-surface test re-run on the final five-path file list: 0 of 5 path(s) hit the register — ordinary queue landing. Re-derived rather than recalled, per the script's own warning that the register has grown several times in two days.
  • Enqueue bar: all 44 check runs on a95087993 completed success or skipped, read with perPage: 50. The last one standing was Check Changeset, which was re-triggered at 18:11:57Z by this seat's own label edit and came back green at 18:12:50Z — the flip waited for it rather than being armed on a partial reading, even though the same check had already passed twice on this identical head.

⚠️ Known exposure, stated in advance: the queue build runs the full suite, so this PR meets packages/cli/test/run-dev-unbuilt-workspace.e2e.test.ts — the flake tracked on #14648, which ejected #14629 at 17:45Z. If this PR is ejected the same way the disposition is fixed: ⛔ no re-queue while #14648 is open and unfixed, ⛔ no weakening of that test, and the card goes to pm:blocked with an Unlock-action: line rather than being re-dispatched.

Owed by the seat at MERGED: verify by content on origin/main that engine.ts holds zero copies of the derivation chain and that deriveViewContainerObject is on @objectstack/metadata's root entry, strip pm:dispatched from #14399, landing record on the card, inventory reconciliation. The reviewer's advisory finding is already filed as #14680, and the dev's residual-asymmetry finding as #14666; neither blocks this landing.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33669892536 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 12 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33671791799 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 14 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 3c1bbd2Sep 2, 2026
46 checks passed
@os-musk
os-musk deleted the claude/issue-14399-view-container-name-order branch September 2, 2026 19:59
This was referenced Sep 2, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The two SOURCE view-container registrars disagree on where the row's own name sits in the object-derivation chain

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(objectql,metadata): derive a view container's object through the shared helper, so the row's own name is LAST at every SOURCE registrar (#14399) - #14665

Merged
os-musk merged 5 commits into
mainfrom
claude/issue-14399-view-container-name-order
Sep 2, 2026
Merged

fix(objectql,metadata): derive a view container's object through the shared helper, so the row's own name is LAST at every SOURCE registrar (#14399)#14665
os-musk merged 5 commits into
mainfrom
claude/issue-14399-view-container-name-order

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14399

What was wrong

Three sites derive "which object does an aggregated defineView container bind to". After #13407 / #13913 / #13912 all three read the container's own top-level object before the list.data.object chain, but they still disagreed about the row's own name:

SiteChainname
packages/objectql/src/engine.tsresolveMetadataItemName('views', item) — the boot-loop SOURCE registrarname then id then object then list.data.object then form.data.objectFIRST
deriveViewContainerObject (@objectstack/metadata) — the artifact/HMR SOURCE registrar and getViewsByObject()object then list.data.object then form.data.object then nameLAST
expandRuntimeViewContainer (@objectstack/metadata-protocol) — the runtime doorsame as aboveLAST

A container written as { name: 'lead_views', object: 'crm_lead', list: { … } } was therefore keyed lead_views by the boot loop and crm_lead everywhere else, expansion (OBJECT.KEY items) included. getViewsByObject() and GET /meta/view?object= filter the expanded items by their object, so which registrar loaded the document decided whether the views were addressable under the object at all. No error, no diagnostic.

The change

The boot loop's CONTAINER branch now calls deriveViewContainerObjectby import, not by re-spelling. A fourth hand-copy of a chain that already existed three times was the defect, not the repair; the tail of the same function now calls the helper too, so engine.ts holds zero copies of the chain where it used to hold one.

The direction is the 2026-08-07 meta-rule rather than taste: one operation with two inconsistent implementations, the side bound by a DECLARATION wins. ViewSchema.object's own .describe() names its readers; the boot loop's order argued from item identity, which declares nothing about the binding. The two sites that already held the winning order are untouched.

deriveViewContainerObject was module-local, so it is added to @objectstack/metadata's root entry (minor for that package). Measured on this tree: packages/objectql/package.json declares @objectstack/metadata as workspace:*, and @objectstack/metadata's full transitive dependency closure is 6 packages with @objectstack/objectqlnot among them — no cycle, and no deep relative import into another package's src/.

Confinement. Only the container branch moves, gated on isAggregatedViewContainer. The assembled viewItems: channel still keys by its own name first — every member of AssembledViewArtifactSchema requires viewKind, so isAggregatedViewContainer is false for all of them, and there is a control asserting exactly that. item.id is untouched and cannot fire for a container: ViewSchema is a strictObject declaring name and object and no id.

Bounded in-place fix, named per the standing clause: the docblock above resolveMetadataItemName asserted "per spec, ViewSchema does NOT have a top-level name field", which was the stated premise for reading name first. It is measurably false — ViewSchema declares an optional name at view.zod.ts — so the docblock now records what the field actually is and what the old sentence got wrong. Same defect class, same file, mechanically settled by the spec source, no new verification surface.

MEASURED CORRECTION to the card

The card predicted the artifact/HMR registrar would "derive crm_lead and mint crm_lead.default, registering the container under crm_lead" — two SILENT keys for one document. The second half is false, and the divergence is sharper than filed:

The boot loop reconciles that field (toRegister = { ...item, name: itemName }) and the artifact door does not. That residual asymmetry is a separate defect at a separate site; #14399 owns the derivation, and the asymmetry is filed on its own card and pinned as an assertion here so it cannot drift unnoticed.

The fixture is the deliverable

No fixture anywhere in the repo set a container name that differed from its bound object, so the divergence was un-rehearsed in BOTH directions — each registrar was individually green on every shape it had ever been shown, which is also why the correction above had gone unnoticed. packages/objectql/src/view-container-divergent-name-registrars.test.ts drives both SOURCE registrars side by side on one document: the boot loop through ObjectQL.registerApp, the artifact door through MetadataPlugin._parseAndRegisterArtifact, exactly as its own #13912 pin drives it. Four controls (name equals object; no object anywhere; the anonymous container both registrars accept; a standalone ViewItem) are green in both directions.

Verification

Head at the time of the runs below: a95087993.

Reverse verification (ablation), direction predicted before running — the boot-loop legs go red, the artifact-door derivation leg stays green because it does not read engine.ts:

  • mutation proved on disk, not by the editor's exit code: anchor hits 1, injected marker count 1, blob 37ab4cb6 to 775ef0b1;
  • no rebuild leg is owed on this mutation and that is stated rather than assumed — the test imports ./engine, a relative SOURCE specifier, so vitest reads the mutated file directly; the helper it calls comes from @objectstack/metadata's dist, which the mutation does not touch;
  • result: 3 failed / 6 passedexpected [ 'lead_views', …(2) ] to deeply equal [ Array(3) ] and expected [ 'lead_views' ] to deeply equal [ 'crm_lead' ]. The "artifact/HMR registrar derives the SAME binding" case stayed green, as predicted;
  • restore under an EXIT INT TERM trap with absolute paths, proved by blob equality (37ab4cb6 equals the HEAD blob), git diff HEAD empty, marker count 0.

Suites and gates, all on a95087993:

  • pnpm --filter @objectstack/objectql exec vitest run — 264 files / 4556 tests passed
  • pnpm --filter @objectstack/metadata exec vitest run — 43 files / 677 tests passed
  • pnpm --filter @objectstack/objectql typecheckcheck:test-typecheck: OK ... 44 file(s) / 242 error(s) / 69 pinned signature(s) (unmoved; the new test file compiles clean and adds no debt)
  • pnpm lint (whole repo, eslint . --no-inline-config) — exit 0, no narrowing claimed
  • pnpm check:nul-bytesOK (scanned 7997 text file(s) ... no raw ASCII control bytes); pnpm check:error-status-conformance — exit 0
  • the full union derived on this tree by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (62 commands): 57 exit 0, including check-system-context-census, check-cross-package-test-inputs, check:test-source-alias, check:published-files, check:type-check-coverage, check:type-source-resolution, check:undeclared-dep-imports, check:engine-split-ratio
  • 5 NOT MEASURED locally, in each gate's own words, never read as green:check-test-completeness and check:type-check-debt and check:dual-build-cjs-loads (exit 3 — each needs a saved turbo test log or a built package closure), check:pm/half-states (exit 3 — repo-scoped GitHub REST answers 403 in this container), and check:skill-examples (exit 1 on its prerequisite branch: packages/client-react/dist holds no .d.ts declarations — the package is not built). Building the closure to measure them was attempted twice and killed by this container's 10-minute foreground cap both times, so the narrowing is declared rather than hidden; CI builds the closure and runs all five.

content/docs/permissions/system-context.mdx was re-anchored by node scripts/check-system-context-census.mjs --fix on the merged tree (15 anchors rewritten), never hand-edited; the gate is green on the final head.

Migration

None. Measured on this tree: of the 54 non-test sources that author or carry view containers, zero declare a name that differs from the object they bind to, so every in-tree container derived identically at all three sites before this change and does after it. What moves is the latent shape only.

Clause-2

yes — self-read from the final diff, against the seat's no. The content limb is hit: packages/metadata/src/index.ts adds a new export to the package's public entry, which widens the published surface (the minor in the changeset is the same fact). No path-limb file is touched — the diff does not enter packages/spec/src/**. needs:contract-review is hung on this PR accordingly.

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…shared helper, so the row's own `name` is LAST at every SOURCE registrar (#14399)
The ObjectQL boot-loop registrar read a container's own `name` FIRST, while
`deriveViewContainerObject` (@objectstack/metadata) and
`expandRuntimeViewContainer` (@objectstack/metadata-protocol) read it LAST. A
container written as `{ name: 'lead_views', object: 'crm_lead', list: { … } }`
was therefore keyed `lead_views` by one SOURCE registrar and `crm_lead` by the
others, expansion included — and `getViewsByObject()` / `GET /meta/view?object=`
filter on the expanded items' `object`, so which registrar loaded the document
decided whether the views were addressable under the object at all.
The boot loop's container branch now calls `deriveViewContainerObject` by
import; a fourth hand-copy of the chain was the defect, not the repair.
`deriveViewContainerObject` moves onto `@objectstack/metadata`'s root entry to
make that import legal without reaching into another package's `src/`.
Only the CONTAINER branch moves, gated on `isAggregatedViewContainer`; the
assembled `viewItems:` channel still keys by its own `name` first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…st view-container sources are divergent) (#14399)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… shift (#14399)
Generated by `node scripts/check-system-context-census.mjs --fix` on the merged
tree; 15 anchors rewritten, never hand-edited.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata, @objectstack/objectql, touching 4 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/metadata/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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 resolveMetadataItemName))
  • content/docs/api/client-sdk.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/api/error-catalog.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/data-modeling/index.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/deployment/validating-metadata.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/permissions/authorization.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/ui/index.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))

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 resolveMetadataItemName))

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
  • 1 changed file(s) yielded no anchor (packages/metadata/src/index.ts) — pages documenting those are invisible to this run
  • 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 — 24 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 2aa8456cf2d66ec3825d262686fe4218e57cfd27packageMentionDocs.

Which tree this was computed on

This run read content/docs from dc744ff995c04d7c29856663afb483fcf15c20f7 — the merge of head a95087993bc3ec03604b95e0596b29250457170c into base 2aa8456cf2d66ec3825d262686fe4218e57cfd27, 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 dc744ff995c04d7c29856663afb483fcf15c20f7 && git checkout dc744ff995c04d7c29856663afb483fcf15c20f7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2aa8456cf2d66ec3825d262686fe4218e57cfd27 a95087993bc3ec03604b95e0596b29250457170c && git checkout -B drift-repro 2aa8456cf2d66ec3825d262686fe4218e57cfd27 && git merge --no-ff a95087993bc3ec03604b95e0596b29250457170c
node scripts/docs-audit/affected-docs.mjs --json 2aa8456cf2d66ec3825d262686fe4218e57cfd27

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session session_0112hMx9hjJ9BgB28X97DS68): flipped ready at 18:13Z and armed auto-merge (squash) at 18:13:38Z on head a95087993. The echo read method: MERGE; the queue build ref is the arming reading, not the echo.

  • Contract review: PASS, adopted verbatim from an isolated reviewer explicitly passed model: fable — not reviewed in-seat. The tier fuse was read with get_session and blew: CONTRACT_REVIEW_TIER is claude-fable-5-1 while this session's session_context.model and last_served_model both read claude-opus-5. Reviewer transcript verified per assistant message before adoption: 79 of 79 report claude-fable-5-1. Full verdict on the card, comment 5514186759.
  • Clause-② yes, derived independently by the reviewer from the final diff — content limb, widening sub-limb (packages/metadata/src/index.ts:56). This seat dispatched no and was wrong; the dev's Zone 2 measurement caught it. needs:contract-review cleared from both carriers (card The two SOURCE view-container registrars disagree on where the row's own name sits in the object-derivation chain #14399 and this PR) in one stroke at PASS.
  • Governed-surface test re-run on the final five-path file list: 0 of 5 path(s) hit the register — ordinary queue landing. Re-derived rather than recalled, per the script's own warning that the register has grown several times in two days.
  • Enqueue bar: all 44 check runs on a95087993 completed success or skipped, read with perPage: 50. The last one standing was Check Changeset, which was re-triggered at 18:11:57Z by this seat's own label edit and came back green at 18:12:50Z — the flip waited for it rather than being armed on a partial reading, even though the same check had already passed twice on this identical head.

⚠️ Known exposure, stated in advance: the queue build runs the full suite, so this PR meets packages/cli/test/run-dev-unbuilt-workspace.e2e.test.ts — the flake tracked on #14648, which ejected #14629 at 17:45Z. If this PR is ejected the same way the disposition is fixed: ⛔ no re-queue while #14648 is open and unfixed, ⛔ no weakening of that test, and the card goes to pm:blocked with an Unlock-action: line rather than being re-dispatched.

Owed by the seat at MERGED: verify by content on origin/main that engine.ts holds zero copies of the derivation chain and that deriveViewContainerObject is on @objectstack/metadata's root entry, strip pm:dispatched from #14399, landing record on the card, inventory reconciliation. The reviewer's advisory finding is already filed as #14680, and the dev's residual-asymmetry finding as #14666; neither blocks this landing.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33669892536 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 12 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33671791799 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 14 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 3c1bbd2Sep 2, 2026
46 checks passed
@os-musk
os-musk deleted the claude/issue-14399-view-container-name-order branch September 2, 2026 19:59
This was referenced Sep 2, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The two SOURCE view-container registrars disagree on where the row's own name sits in the object-derivation chain

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(objectql,metadata): derive a view container's object through the shared helper, so the row's own name is LAST at every SOURCE registrar (#14399) - #14665

Merged
os-musk merged 5 commits into
mainfrom
claude/issue-14399-view-container-name-order
Sep 2, 2026
Merged

fix(objectql,metadata): derive a view container's object through the shared helper, so the row's own name is LAST at every SOURCE registrar (#14399)#14665
os-musk merged 5 commits into
mainfrom
claude/issue-14399-view-container-name-order

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14399

What was wrong

Three sites derive "which object does an aggregated defineView container bind to". After #13407 / #13913 / #13912 all three read the container's own top-level object before the list.data.object chain, but they still disagreed about the row's own name:

SiteChainname
packages/objectql/src/engine.tsresolveMetadataItemName('views', item) — the boot-loop SOURCE registrarname then id then object then list.data.object then form.data.objectFIRST
deriveViewContainerObject (@objectstack/metadata) — the artifact/HMR SOURCE registrar and getViewsByObject()object then list.data.object then form.data.object then nameLAST
expandRuntimeViewContainer (@objectstack/metadata-protocol) — the runtime doorsame as aboveLAST

A container written as { name: 'lead_views', object: 'crm_lead', list: { … } } was therefore keyed lead_views by the boot loop and crm_lead everywhere else, expansion (OBJECT.KEY items) included. getViewsByObject() and GET /meta/view?object= filter the expanded items by their object, so which registrar loaded the document decided whether the views were addressable under the object at all. No error, no diagnostic.

The change

The boot loop's CONTAINER branch now calls deriveViewContainerObjectby import, not by re-spelling. A fourth hand-copy of a chain that already existed three times was the defect, not the repair; the tail of the same function now calls the helper too, so engine.ts holds zero copies of the chain where it used to hold one.

The direction is the 2026-08-07 meta-rule rather than taste: one operation with two inconsistent implementations, the side bound by a DECLARATION wins. ViewSchema.object's own .describe() names its readers; the boot loop's order argued from item identity, which declares nothing about the binding. The two sites that already held the winning order are untouched.

deriveViewContainerObject was module-local, so it is added to @objectstack/metadata's root entry (minor for that package). Measured on this tree: packages/objectql/package.json declares @objectstack/metadata as workspace:*, and @objectstack/metadata's full transitive dependency closure is 6 packages with @objectstack/objectqlnot among them — no cycle, and no deep relative import into another package's src/.

Confinement. Only the container branch moves, gated on isAggregatedViewContainer. The assembled viewItems: channel still keys by its own name first — every member of AssembledViewArtifactSchema requires viewKind, so isAggregatedViewContainer is false for all of them, and there is a control asserting exactly that. item.id is untouched and cannot fire for a container: ViewSchema is a strictObject declaring name and object and no id.

Bounded in-place fix, named per the standing clause: the docblock above resolveMetadataItemName asserted "per spec, ViewSchema does NOT have a top-level name field", which was the stated premise for reading name first. It is measurably false — ViewSchema declares an optional name at view.zod.ts — so the docblock now records what the field actually is and what the old sentence got wrong. Same defect class, same file, mechanically settled by the spec source, no new verification surface.

MEASURED CORRECTION to the card

The card predicted the artifact/HMR registrar would "derive crm_lead and mint crm_lead.default, registering the container under crm_lead" — two SILENT keys for one document. The second half is false, and the divergence is sharper than filed:

The boot loop reconciles that field (toRegister = { ...item, name: itemName }) and the artifact door does not. That residual asymmetry is a separate defect at a separate site; #14399 owns the derivation, and the asymmetry is filed on its own card and pinned as an assertion here so it cannot drift unnoticed.

The fixture is the deliverable

No fixture anywhere in the repo set a container name that differed from its bound object, so the divergence was un-rehearsed in BOTH directions — each registrar was individually green on every shape it had ever been shown, which is also why the correction above had gone unnoticed. packages/objectql/src/view-container-divergent-name-registrars.test.ts drives both SOURCE registrars side by side on one document: the boot loop through ObjectQL.registerApp, the artifact door through MetadataPlugin._parseAndRegisterArtifact, exactly as its own #13912 pin drives it. Four controls (name equals object; no object anywhere; the anonymous container both registrars accept; a standalone ViewItem) are green in both directions.

Verification

Head at the time of the runs below: a95087993.

Reverse verification (ablation), direction predicted before running — the boot-loop legs go red, the artifact-door derivation leg stays green because it does not read engine.ts:

  • mutation proved on disk, not by the editor's exit code: anchor hits 1, injected marker count 1, blob 37ab4cb6 to 775ef0b1;
  • no rebuild leg is owed on this mutation and that is stated rather than assumed — the test imports ./engine, a relative SOURCE specifier, so vitest reads the mutated file directly; the helper it calls comes from @objectstack/metadata's dist, which the mutation does not touch;
  • result: 3 failed / 6 passedexpected [ 'lead_views', …(2) ] to deeply equal [ Array(3) ] and expected [ 'lead_views' ] to deeply equal [ 'crm_lead' ]. The "artifact/HMR registrar derives the SAME binding" case stayed green, as predicted;
  • restore under an EXIT INT TERM trap with absolute paths, proved by blob equality (37ab4cb6 equals the HEAD blob), git diff HEAD empty, marker count 0.

Suites and gates, all on a95087993:

  • pnpm --filter @objectstack/objectql exec vitest run — 264 files / 4556 tests passed
  • pnpm --filter @objectstack/metadata exec vitest run — 43 files / 677 tests passed
  • pnpm --filter @objectstack/objectql typecheckcheck:test-typecheck: OK ... 44 file(s) / 242 error(s) / 69 pinned signature(s) (unmoved; the new test file compiles clean and adds no debt)
  • pnpm lint (whole repo, eslint . --no-inline-config) — exit 0, no narrowing claimed
  • pnpm check:nul-bytesOK (scanned 7997 text file(s) ... no raw ASCII control bytes); pnpm check:error-status-conformance — exit 0
  • the full union derived on this tree by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (62 commands): 57 exit 0, including check-system-context-census, check-cross-package-test-inputs, check:test-source-alias, check:published-files, check:type-check-coverage, check:type-source-resolution, check:undeclared-dep-imports, check:engine-split-ratio
  • 5 NOT MEASURED locally, in each gate's own words, never read as green:check-test-completeness and check:type-check-debt and check:dual-build-cjs-loads (exit 3 — each needs a saved turbo test log or a built package closure), check:pm/half-states (exit 3 — repo-scoped GitHub REST answers 403 in this container), and check:skill-examples (exit 1 on its prerequisite branch: packages/client-react/dist holds no .d.ts declarations — the package is not built). Building the closure to measure them was attempted twice and killed by this container's 10-minute foreground cap both times, so the narrowing is declared rather than hidden; CI builds the closure and runs all five.

content/docs/permissions/system-context.mdx was re-anchored by node scripts/check-system-context-census.mjs --fix on the merged tree (15 anchors rewritten), never hand-edited; the gate is green on the final head.

Migration

None. Measured on this tree: of the 54 non-test sources that author or carry view containers, zero declare a name that differs from the object they bind to, so every in-tree container derived identically at all three sites before this change and does after it. What moves is the latent shape only.

Clause-2

yes — self-read from the final diff, against the seat's no. The content limb is hit: packages/metadata/src/index.ts adds a new export to the package's public entry, which widens the published surface (the minor in the changeset is the same fact). No path-limb file is touched — the diff does not enter packages/spec/src/**. needs:contract-review is hung on this PR accordingly.

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…shared helper, so the row's own `name` is LAST at every SOURCE registrar (#14399)
The ObjectQL boot-loop registrar read a container's own `name` FIRST, while
`deriveViewContainerObject` (@objectstack/metadata) and
`expandRuntimeViewContainer` (@objectstack/metadata-protocol) read it LAST. A
container written as `{ name: 'lead_views', object: 'crm_lead', list: { … } }`
was therefore keyed `lead_views` by one SOURCE registrar and `crm_lead` by the
others, expansion included — and `getViewsByObject()` / `GET /meta/view?object=`
filter on the expanded items' `object`, so which registrar loaded the document
decided whether the views were addressable under the object at all.
The boot loop's container branch now calls `deriveViewContainerObject` by
import; a fourth hand-copy of the chain was the defect, not the repair.
`deriveViewContainerObject` moves onto `@objectstack/metadata`'s root entry to
make that import legal without reaching into another package's `src/`.
Only the CONTAINER branch moves, gated on `isAggregatedViewContainer`; the
assembled `viewItems:` channel still keys by its own `name` first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…st view-container sources are divergent) (#14399)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… shift (#14399)
Generated by `node scripts/check-system-context-census.mjs --fix` on the merged
tree; 15 anchors rewritten, never hand-edited.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata, @objectstack/objectql, touching 4 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/metadata/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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 resolveMetadataItemName))
  • content/docs/api/client-sdk.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/api/error-catalog.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/data-modeling/index.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/deployment/validating-metadata.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/permissions/authorization.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/ui/index.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))

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 resolveMetadataItemName))

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
  • 1 changed file(s) yielded no anchor (packages/metadata/src/index.ts) — pages documenting those are invisible to this run
  • 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 — 24 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 2aa8456cf2d66ec3825d262686fe4218e57cfd27packageMentionDocs.

Which tree this was computed on

This run read content/docs from dc744ff995c04d7c29856663afb483fcf15c20f7 — the merge of head a95087993bc3ec03604b95e0596b29250457170c into base 2aa8456cf2d66ec3825d262686fe4218e57cfd27, 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 dc744ff995c04d7c29856663afb483fcf15c20f7 && git checkout dc744ff995c04d7c29856663afb483fcf15c20f7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2aa8456cf2d66ec3825d262686fe4218e57cfd27 a95087993bc3ec03604b95e0596b29250457170c && git checkout -B drift-repro 2aa8456cf2d66ec3825d262686fe4218e57cfd27 && git merge --no-ff a95087993bc3ec03604b95e0596b29250457170c
node scripts/docs-audit/affected-docs.mjs --json 2aa8456cf2d66ec3825d262686fe4218e57cfd27

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session session_0112hMx9hjJ9BgB28X97DS68): flipped ready at 18:13Z and armed auto-merge (squash) at 18:13:38Z on head a95087993. The echo read method: MERGE; the queue build ref is the arming reading, not the echo.

  • Contract review: PASS, adopted verbatim from an isolated reviewer explicitly passed model: fable — not reviewed in-seat. The tier fuse was read with get_session and blew: CONTRACT_REVIEW_TIER is claude-fable-5-1 while this session's session_context.model and last_served_model both read claude-opus-5. Reviewer transcript verified per assistant message before adoption: 79 of 79 report claude-fable-5-1. Full verdict on the card, comment 5514186759.
  • Clause-② yes, derived independently by the reviewer from the final diff — content limb, widening sub-limb (packages/metadata/src/index.ts:56). This seat dispatched no and was wrong; the dev's Zone 2 measurement caught it. needs:contract-review cleared from both carriers (card The two SOURCE view-container registrars disagree on where the row's own name sits in the object-derivation chain #14399 and this PR) in one stroke at PASS.
  • Governed-surface test re-run on the final five-path file list: 0 of 5 path(s) hit the register — ordinary queue landing. Re-derived rather than recalled, per the script's own warning that the register has grown several times in two days.
  • Enqueue bar: all 44 check runs on a95087993 completed success or skipped, read with perPage: 50. The last one standing was Check Changeset, which was re-triggered at 18:11:57Z by this seat's own label edit and came back green at 18:12:50Z — the flip waited for it rather than being armed on a partial reading, even though the same check had already passed twice on this identical head.

⚠️ Known exposure, stated in advance: the queue build runs the full suite, so this PR meets packages/cli/test/run-dev-unbuilt-workspace.e2e.test.ts — the flake tracked on #14648, which ejected #14629 at 17:45Z. If this PR is ejected the same way the disposition is fixed: ⛔ no re-queue while #14648 is open and unfixed, ⛔ no weakening of that test, and the card goes to pm:blocked with an Unlock-action: line rather than being re-dispatched.

Owed by the seat at MERGED: verify by content on origin/main that engine.ts holds zero copies of the derivation chain and that deriveViewContainerObject is on @objectstack/metadata's root entry, strip pm:dispatched from #14399, landing record on the card, inventory reconciliation. The reviewer's advisory finding is already filed as #14680, and the dev's residual-asymmetry finding as #14666; neither blocks this landing.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33669892536 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 12 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33671791799 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 14 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 3c1bbd2Sep 2, 2026
46 checks passed
@os-musk
os-musk deleted the claude/issue-14399-view-container-name-order branch September 2, 2026 19:59
This was referenced Sep 2, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The two SOURCE view-container registrars disagree on where the row's own name sits in the object-derivation chain

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(objectql,metadata): derive a view container's object through the shared helper, so the row's own name is LAST at every SOURCE registrar (#14399) - #14665

Merged
os-musk merged 5 commits into
mainfrom
claude/issue-14399-view-container-name-order
Sep 2, 2026
Merged

fix(objectql,metadata): derive a view container's object through the shared helper, so the row's own name is LAST at every SOURCE registrar (#14399)#14665
os-musk merged 5 commits into
mainfrom
claude/issue-14399-view-container-name-order

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14399

What was wrong

Three sites derive "which object does an aggregated defineView container bind to". After #13407 / #13913 / #13912 all three read the container's own top-level object before the list.data.object chain, but they still disagreed about the row's own name:

SiteChainname
packages/objectql/src/engine.tsresolveMetadataItemName('views', item) — the boot-loop SOURCE registrarname then id then object then list.data.object then form.data.objectFIRST
deriveViewContainerObject (@objectstack/metadata) — the artifact/HMR SOURCE registrar and getViewsByObject()object then list.data.object then form.data.object then nameLAST
expandRuntimeViewContainer (@objectstack/metadata-protocol) — the runtime doorsame as aboveLAST

A container written as { name: 'lead_views', object: 'crm_lead', list: { … } } was therefore keyed lead_views by the boot loop and crm_lead everywhere else, expansion (OBJECT.KEY items) included. getViewsByObject() and GET /meta/view?object= filter the expanded items by their object, so which registrar loaded the document decided whether the views were addressable under the object at all. No error, no diagnostic.

The change

The boot loop's CONTAINER branch now calls deriveViewContainerObjectby import, not by re-spelling. A fourth hand-copy of a chain that already existed three times was the defect, not the repair; the tail of the same function now calls the helper too, so engine.ts holds zero copies of the chain where it used to hold one.

The direction is the 2026-08-07 meta-rule rather than taste: one operation with two inconsistent implementations, the side bound by a DECLARATION wins. ViewSchema.object's own .describe() names its readers; the boot loop's order argued from item identity, which declares nothing about the binding. The two sites that already held the winning order are untouched.

deriveViewContainerObject was module-local, so it is added to @objectstack/metadata's root entry (minor for that package). Measured on this tree: packages/objectql/package.json declares @objectstack/metadata as workspace:*, and @objectstack/metadata's full transitive dependency closure is 6 packages with @objectstack/objectqlnot among them — no cycle, and no deep relative import into another package's src/.

Confinement. Only the container branch moves, gated on isAggregatedViewContainer. The assembled viewItems: channel still keys by its own name first — every member of AssembledViewArtifactSchema requires viewKind, so isAggregatedViewContainer is false for all of them, and there is a control asserting exactly that. item.id is untouched and cannot fire for a container: ViewSchema is a strictObject declaring name and object and no id.

Bounded in-place fix, named per the standing clause: the docblock above resolveMetadataItemName asserted "per spec, ViewSchema does NOT have a top-level name field", which was the stated premise for reading name first. It is measurably false — ViewSchema declares an optional name at view.zod.ts — so the docblock now records what the field actually is and what the old sentence got wrong. Same defect class, same file, mechanically settled by the spec source, no new verification surface.

MEASURED CORRECTION to the card

The card predicted the artifact/HMR registrar would "derive crm_lead and mint crm_lead.default, registering the container under crm_lead" — two SILENT keys for one document. The second half is false, and the divergence is sharper than filed:

The boot loop reconciles that field (toRegister = { ...item, name: itemName }) and the artifact door does not. That residual asymmetry is a separate defect at a separate site; #14399 owns the derivation, and the asymmetry is filed on its own card and pinned as an assertion here so it cannot drift unnoticed.

The fixture is the deliverable

No fixture anywhere in the repo set a container name that differed from its bound object, so the divergence was un-rehearsed in BOTH directions — each registrar was individually green on every shape it had ever been shown, which is also why the correction above had gone unnoticed. packages/objectql/src/view-container-divergent-name-registrars.test.ts drives both SOURCE registrars side by side on one document: the boot loop through ObjectQL.registerApp, the artifact door through MetadataPlugin._parseAndRegisterArtifact, exactly as its own #13912 pin drives it. Four controls (name equals object; no object anywhere; the anonymous container both registrars accept; a standalone ViewItem) are green in both directions.

Verification

Head at the time of the runs below: a95087993.

Reverse verification (ablation), direction predicted before running — the boot-loop legs go red, the artifact-door derivation leg stays green because it does not read engine.ts:

  • mutation proved on disk, not by the editor's exit code: anchor hits 1, injected marker count 1, blob 37ab4cb6 to 775ef0b1;
  • no rebuild leg is owed on this mutation and that is stated rather than assumed — the test imports ./engine, a relative SOURCE specifier, so vitest reads the mutated file directly; the helper it calls comes from @objectstack/metadata's dist, which the mutation does not touch;
  • result: 3 failed / 6 passedexpected [ 'lead_views', …(2) ] to deeply equal [ Array(3) ] and expected [ 'lead_views' ] to deeply equal [ 'crm_lead' ]. The "artifact/HMR registrar derives the SAME binding" case stayed green, as predicted;
  • restore under an EXIT INT TERM trap with absolute paths, proved by blob equality (37ab4cb6 equals the HEAD blob), git diff HEAD empty, marker count 0.

Suites and gates, all on a95087993:

  • pnpm --filter @objectstack/objectql exec vitest run — 264 files / 4556 tests passed
  • pnpm --filter @objectstack/metadata exec vitest run — 43 files / 677 tests passed
  • pnpm --filter @objectstack/objectql typecheckcheck:test-typecheck: OK ... 44 file(s) / 242 error(s) / 69 pinned signature(s) (unmoved; the new test file compiles clean and adds no debt)
  • pnpm lint (whole repo, eslint . --no-inline-config) — exit 0, no narrowing claimed
  • pnpm check:nul-bytesOK (scanned 7997 text file(s) ... no raw ASCII control bytes); pnpm check:error-status-conformance — exit 0
  • the full union derived on this tree by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (62 commands): 57 exit 0, including check-system-context-census, check-cross-package-test-inputs, check:test-source-alias, check:published-files, check:type-check-coverage, check:type-source-resolution, check:undeclared-dep-imports, check:engine-split-ratio
  • 5 NOT MEASURED locally, in each gate's own words, never read as green:check-test-completeness and check:type-check-debt and check:dual-build-cjs-loads (exit 3 — each needs a saved turbo test log or a built package closure), check:pm/half-states (exit 3 — repo-scoped GitHub REST answers 403 in this container), and check:skill-examples (exit 1 on its prerequisite branch: packages/client-react/dist holds no .d.ts declarations — the package is not built). Building the closure to measure them was attempted twice and killed by this container's 10-minute foreground cap both times, so the narrowing is declared rather than hidden; CI builds the closure and runs all five.

content/docs/permissions/system-context.mdx was re-anchored by node scripts/check-system-context-census.mjs --fix on the merged tree (15 anchors rewritten), never hand-edited; the gate is green on the final head.

Migration

None. Measured on this tree: of the 54 non-test sources that author or carry view containers, zero declare a name that differs from the object they bind to, so every in-tree container derived identically at all three sites before this change and does after it. What moves is the latent shape only.

Clause-2

yes — self-read from the final diff, against the seat's no. The content limb is hit: packages/metadata/src/index.ts adds a new export to the package's public entry, which widens the published surface (the minor in the changeset is the same fact). No path-limb file is touched — the diff does not enter packages/spec/src/**. needs:contract-review is hung on this PR accordingly.

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…shared helper, so the row's own `name` is LAST at every SOURCE registrar (#14399)
The ObjectQL boot-loop registrar read a container's own `name` FIRST, while
`deriveViewContainerObject` (@objectstack/metadata) and
`expandRuntimeViewContainer` (@objectstack/metadata-protocol) read it LAST. A
container written as `{ name: 'lead_views', object: 'crm_lead', list: { … } }`
was therefore keyed `lead_views` by one SOURCE registrar and `crm_lead` by the
others, expansion included — and `getViewsByObject()` / `GET /meta/view?object=`
filter on the expanded items' `object`, so which registrar loaded the document
decided whether the views were addressable under the object at all.
The boot loop's container branch now calls `deriveViewContainerObject` by
import; a fourth hand-copy of the chain was the defect, not the repair.
`deriveViewContainerObject` moves onto `@objectstack/metadata`'s root entry to
make that import legal without reaching into another package's `src/`.
Only the CONTAINER branch moves, gated on `isAggregatedViewContainer`; the
assembled `viewItems:` channel still keys by its own `name` first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…st view-container sources are divergent) (#14399)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… shift (#14399)
Generated by `node scripts/check-system-context-census.mjs --fix` on the merged
tree; 15 anchors rewritten, never hand-edited.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata, @objectstack/objectql, touching 4 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/metadata/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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 resolveMetadataItemName))
  • content/docs/api/client-sdk.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/api/error-catalog.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/data-modeling/index.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/deployment/validating-metadata.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/permissions/authorization.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/ui/index.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))

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 resolveMetadataItemName))

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
  • 1 changed file(s) yielded no anchor (packages/metadata/src/index.ts) — pages documenting those are invisible to this run
  • 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 — 24 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 2aa8456cf2d66ec3825d262686fe4218e57cfd27packageMentionDocs.

Which tree this was computed on

This run read content/docs from dc744ff995c04d7c29856663afb483fcf15c20f7 — the merge of head a95087993bc3ec03604b95e0596b29250457170c into base 2aa8456cf2d66ec3825d262686fe4218e57cfd27, 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 dc744ff995c04d7c29856663afb483fcf15c20f7 && git checkout dc744ff995c04d7c29856663afb483fcf15c20f7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2aa8456cf2d66ec3825d262686fe4218e57cfd27 a95087993bc3ec03604b95e0596b29250457170c && git checkout -B drift-repro 2aa8456cf2d66ec3825d262686fe4218e57cfd27 && git merge --no-ff a95087993bc3ec03604b95e0596b29250457170c
node scripts/docs-audit/affected-docs.mjs --json 2aa8456cf2d66ec3825d262686fe4218e57cfd27

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session session_0112hMx9hjJ9BgB28X97DS68): flipped ready at 18:13Z and armed auto-merge (squash) at 18:13:38Z on head a95087993. The echo read method: MERGE; the queue build ref is the arming reading, not the echo.

  • Contract review: PASS, adopted verbatim from an isolated reviewer explicitly passed model: fable — not reviewed in-seat. The tier fuse was read with get_session and blew: CONTRACT_REVIEW_TIER is claude-fable-5-1 while this session's session_context.model and last_served_model both read claude-opus-5. Reviewer transcript verified per assistant message before adoption: 79 of 79 report claude-fable-5-1. Full verdict on the card, comment 5514186759.
  • Clause-② yes, derived independently by the reviewer from the final diff — content limb, widening sub-limb (packages/metadata/src/index.ts:56). This seat dispatched no and was wrong; the dev's Zone 2 measurement caught it. needs:contract-review cleared from both carriers (card The two SOURCE view-container registrars disagree on where the row's own name sits in the object-derivation chain #14399 and this PR) in one stroke at PASS.
  • Governed-surface test re-run on the final five-path file list: 0 of 5 path(s) hit the register — ordinary queue landing. Re-derived rather than recalled, per the script's own warning that the register has grown several times in two days.
  • Enqueue bar: all 44 check runs on a95087993 completed success or skipped, read with perPage: 50. The last one standing was Check Changeset, which was re-triggered at 18:11:57Z by this seat's own label edit and came back green at 18:12:50Z — the flip waited for it rather than being armed on a partial reading, even though the same check had already passed twice on this identical head.

⚠️ Known exposure, stated in advance: the queue build runs the full suite, so this PR meets packages/cli/test/run-dev-unbuilt-workspace.e2e.test.ts — the flake tracked on #14648, which ejected #14629 at 17:45Z. If this PR is ejected the same way the disposition is fixed: ⛔ no re-queue while #14648 is open and unfixed, ⛔ no weakening of that test, and the card goes to pm:blocked with an Unlock-action: line rather than being re-dispatched.

Owed by the seat at MERGED: verify by content on origin/main that engine.ts holds zero copies of the derivation chain and that deriveViewContainerObject is on @objectstack/metadata's root entry, strip pm:dispatched from #14399, landing record on the card, inventory reconciliation. The reviewer's advisory finding is already filed as #14680, and the dev's residual-asymmetry finding as #14666; neither blocks this landing.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33669892536 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 12 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33671791799 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 14 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 3c1bbd2Sep 2, 2026
46 checks passed
@os-musk
os-musk deleted the claude/issue-14399-view-container-name-order branch September 2, 2026 19:59
This was referenced Sep 2, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The two SOURCE view-container registrars disagree on where the row's own name sits in the object-derivation chain

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(objectql,metadata): derive a view container's object through the shared helper, so the row's own name is LAST at every SOURCE registrar (#14399) - #14665

Merged
os-musk merged 5 commits into
mainfrom
claude/issue-14399-view-container-name-order
Sep 2, 2026
Merged

fix(objectql,metadata): derive a view container's object through the shared helper, so the row's own name is LAST at every SOURCE registrar (#14399)#14665
os-musk merged 5 commits into
mainfrom
claude/issue-14399-view-container-name-order

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14399

What was wrong

Three sites derive "which object does an aggregated defineView container bind to". After #13407 / #13913 / #13912 all three read the container's own top-level object before the list.data.object chain, but they still disagreed about the row's own name:

SiteChainname
packages/objectql/src/engine.tsresolveMetadataItemName('views', item) — the boot-loop SOURCE registrarname then id then object then list.data.object then form.data.objectFIRST
deriveViewContainerObject (@objectstack/metadata) — the artifact/HMR SOURCE registrar and getViewsByObject()object then list.data.object then form.data.object then nameLAST
expandRuntimeViewContainer (@objectstack/metadata-protocol) — the runtime doorsame as aboveLAST

A container written as { name: 'lead_views', object: 'crm_lead', list: { … } } was therefore keyed lead_views by the boot loop and crm_lead everywhere else, expansion (OBJECT.KEY items) included. getViewsByObject() and GET /meta/view?object= filter the expanded items by their object, so which registrar loaded the document decided whether the views were addressable under the object at all. No error, no diagnostic.

The change

The boot loop's CONTAINER branch now calls deriveViewContainerObjectby import, not by re-spelling. A fourth hand-copy of a chain that already existed three times was the defect, not the repair; the tail of the same function now calls the helper too, so engine.ts holds zero copies of the chain where it used to hold one.

The direction is the 2026-08-07 meta-rule rather than taste: one operation with two inconsistent implementations, the side bound by a DECLARATION wins. ViewSchema.object's own .describe() names its readers; the boot loop's order argued from item identity, which declares nothing about the binding. The two sites that already held the winning order are untouched.

deriveViewContainerObject was module-local, so it is added to @objectstack/metadata's root entry (minor for that package). Measured on this tree: packages/objectql/package.json declares @objectstack/metadata as workspace:*, and @objectstack/metadata's full transitive dependency closure is 6 packages with @objectstack/objectqlnot among them — no cycle, and no deep relative import into another package's src/.

Confinement. Only the container branch moves, gated on isAggregatedViewContainer. The assembled viewItems: channel still keys by its own name first — every member of AssembledViewArtifactSchema requires viewKind, so isAggregatedViewContainer is false for all of them, and there is a control asserting exactly that. item.id is untouched and cannot fire for a container: ViewSchema is a strictObject declaring name and object and no id.

Bounded in-place fix, named per the standing clause: the docblock above resolveMetadataItemName asserted "per spec, ViewSchema does NOT have a top-level name field", which was the stated premise for reading name first. It is measurably false — ViewSchema declares an optional name at view.zod.ts — so the docblock now records what the field actually is and what the old sentence got wrong. Same defect class, same file, mechanically settled by the spec source, no new verification surface.

MEASURED CORRECTION to the card

The card predicted the artifact/HMR registrar would "derive crm_lead and mint crm_lead.default, registering the container under crm_lead" — two SILENT keys for one document. The second half is false, and the divergence is sharper than filed:

The boot loop reconciles that field (toRegister = { ...item, name: itemName }) and the artifact door does not. That residual asymmetry is a separate defect at a separate site; #14399 owns the derivation, and the asymmetry is filed on its own card and pinned as an assertion here so it cannot drift unnoticed.

The fixture is the deliverable

No fixture anywhere in the repo set a container name that differed from its bound object, so the divergence was un-rehearsed in BOTH directions — each registrar was individually green on every shape it had ever been shown, which is also why the correction above had gone unnoticed. packages/objectql/src/view-container-divergent-name-registrars.test.ts drives both SOURCE registrars side by side on one document: the boot loop through ObjectQL.registerApp, the artifact door through MetadataPlugin._parseAndRegisterArtifact, exactly as its own #13912 pin drives it. Four controls (name equals object; no object anywhere; the anonymous container both registrars accept; a standalone ViewItem) are green in both directions.

Verification

Head at the time of the runs below: a95087993.

Reverse verification (ablation), direction predicted before running — the boot-loop legs go red, the artifact-door derivation leg stays green because it does not read engine.ts:

  • mutation proved on disk, not by the editor's exit code: anchor hits 1, injected marker count 1, blob 37ab4cb6 to 775ef0b1;
  • no rebuild leg is owed on this mutation and that is stated rather than assumed — the test imports ./engine, a relative SOURCE specifier, so vitest reads the mutated file directly; the helper it calls comes from @objectstack/metadata's dist, which the mutation does not touch;
  • result: 3 failed / 6 passedexpected [ 'lead_views', …(2) ] to deeply equal [ Array(3) ] and expected [ 'lead_views' ] to deeply equal [ 'crm_lead' ]. The "artifact/HMR registrar derives the SAME binding" case stayed green, as predicted;
  • restore under an EXIT INT TERM trap with absolute paths, proved by blob equality (37ab4cb6 equals the HEAD blob), git diff HEAD empty, marker count 0.

Suites and gates, all on a95087993:

  • pnpm --filter @objectstack/objectql exec vitest run — 264 files / 4556 tests passed
  • pnpm --filter @objectstack/metadata exec vitest run — 43 files / 677 tests passed
  • pnpm --filter @objectstack/objectql typecheckcheck:test-typecheck: OK ... 44 file(s) / 242 error(s) / 69 pinned signature(s) (unmoved; the new test file compiles clean and adds no debt)
  • pnpm lint (whole repo, eslint . --no-inline-config) — exit 0, no narrowing claimed
  • pnpm check:nul-bytesOK (scanned 7997 text file(s) ... no raw ASCII control bytes); pnpm check:error-status-conformance — exit 0
  • the full union derived on this tree by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (62 commands): 57 exit 0, including check-system-context-census, check-cross-package-test-inputs, check:test-source-alias, check:published-files, check:type-check-coverage, check:type-source-resolution, check:undeclared-dep-imports, check:engine-split-ratio
  • 5 NOT MEASURED locally, in each gate's own words, never read as green:check-test-completeness and check:type-check-debt and check:dual-build-cjs-loads (exit 3 — each needs a saved turbo test log or a built package closure), check:pm/half-states (exit 3 — repo-scoped GitHub REST answers 403 in this container), and check:skill-examples (exit 1 on its prerequisite branch: packages/client-react/dist holds no .d.ts declarations — the package is not built). Building the closure to measure them was attempted twice and killed by this container's 10-minute foreground cap both times, so the narrowing is declared rather than hidden; CI builds the closure and runs all five.

content/docs/permissions/system-context.mdx was re-anchored by node scripts/check-system-context-census.mjs --fix on the merged tree (15 anchors rewritten), never hand-edited; the gate is green on the final head.

Migration

None. Measured on this tree: of the 54 non-test sources that author or carry view containers, zero declare a name that differs from the object they bind to, so every in-tree container derived identically at all three sites before this change and does after it. What moves is the latent shape only.

Clause-2

yes — self-read from the final diff, against the seat's no. The content limb is hit: packages/metadata/src/index.ts adds a new export to the package's public entry, which widens the published surface (the minor in the changeset is the same fact). No path-limb file is touched — the diff does not enter packages/spec/src/**. needs:contract-review is hung on this PR accordingly.

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…shared helper, so the row's own `name` is LAST at every SOURCE registrar (#14399)
The ObjectQL boot-loop registrar read a container's own `name` FIRST, while
`deriveViewContainerObject` (@objectstack/metadata) and
`expandRuntimeViewContainer` (@objectstack/metadata-protocol) read it LAST. A
container written as `{ name: 'lead_views', object: 'crm_lead', list: { … } }`
was therefore keyed `lead_views` by one SOURCE registrar and `crm_lead` by the
others, expansion included — and `getViewsByObject()` / `GET /meta/view?object=`
filter on the expanded items' `object`, so which registrar loaded the document
decided whether the views were addressable under the object at all.
The boot loop's container branch now calls `deriveViewContainerObject` by
import; a fourth hand-copy of the chain was the defect, not the repair.
`deriveViewContainerObject` moves onto `@objectstack/metadata`'s root entry to
make that import legal without reaching into another package's `src/`.
Only the CONTAINER branch moves, gated on `isAggregatedViewContainer`; the
assembled `viewItems:` channel still keys by its own `name` first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…st view-container sources are divergent) (#14399)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… shift (#14399)
Generated by `node scripts/check-system-context-census.mjs --fix` on the merged
tree; 15 anchors rewritten, never hand-edited.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata, @objectstack/objectql, touching 4 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/metadata/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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 resolveMetadataItemName))
  • content/docs/api/client-sdk.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/api/error-catalog.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/data-modeling/index.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/deployment/validating-metadata.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/permissions/authorization.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/ui/index.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))

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 resolveMetadataItemName))

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
  • 1 changed file(s) yielded no anchor (packages/metadata/src/index.ts) — pages documenting those are invisible to this run
  • 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 — 24 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 2aa8456cf2d66ec3825d262686fe4218e57cfd27packageMentionDocs.

Which tree this was computed on

This run read content/docs from dc744ff995c04d7c29856663afb483fcf15c20f7 — the merge of head a95087993bc3ec03604b95e0596b29250457170c into base 2aa8456cf2d66ec3825d262686fe4218e57cfd27, 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 dc744ff995c04d7c29856663afb483fcf15c20f7 && git checkout dc744ff995c04d7c29856663afb483fcf15c20f7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2aa8456cf2d66ec3825d262686fe4218e57cfd27 a95087993bc3ec03604b95e0596b29250457170c && git checkout -B drift-repro 2aa8456cf2d66ec3825d262686fe4218e57cfd27 && git merge --no-ff a95087993bc3ec03604b95e0596b29250457170c
node scripts/docs-audit/affected-docs.mjs --json 2aa8456cf2d66ec3825d262686fe4218e57cfd27

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session session_0112hMx9hjJ9BgB28X97DS68): flipped ready at 18:13Z and armed auto-merge (squash) at 18:13:38Z on head a95087993. The echo read method: MERGE; the queue build ref is the arming reading, not the echo.

  • Contract review: PASS, adopted verbatim from an isolated reviewer explicitly passed model: fable — not reviewed in-seat. The tier fuse was read with get_session and blew: CONTRACT_REVIEW_TIER is claude-fable-5-1 while this session's session_context.model and last_served_model both read claude-opus-5. Reviewer transcript verified per assistant message before adoption: 79 of 79 report claude-fable-5-1. Full verdict on the card, comment 5514186759.
  • Clause-② yes, derived independently by the reviewer from the final diff — content limb, widening sub-limb (packages/metadata/src/index.ts:56). This seat dispatched no and was wrong; the dev's Zone 2 measurement caught it. needs:contract-review cleared from both carriers (card The two SOURCE view-container registrars disagree on where the row's own name sits in the object-derivation chain #14399 and this PR) in one stroke at PASS.
  • Governed-surface test re-run on the final five-path file list: 0 of 5 path(s) hit the register — ordinary queue landing. Re-derived rather than recalled, per the script's own warning that the register has grown several times in two days.
  • Enqueue bar: all 44 check runs on a95087993 completed success or skipped, read with perPage: 50. The last one standing was Check Changeset, which was re-triggered at 18:11:57Z by this seat's own label edit and came back green at 18:12:50Z — the flip waited for it rather than being armed on a partial reading, even though the same check had already passed twice on this identical head.

⚠️ Known exposure, stated in advance: the queue build runs the full suite, so this PR meets packages/cli/test/run-dev-unbuilt-workspace.e2e.test.ts — the flake tracked on #14648, which ejected #14629 at 17:45Z. If this PR is ejected the same way the disposition is fixed: ⛔ no re-queue while #14648 is open and unfixed, ⛔ no weakening of that test, and the card goes to pm:blocked with an Unlock-action: line rather than being re-dispatched.

Owed by the seat at MERGED: verify by content on origin/main that engine.ts holds zero copies of the derivation chain and that deriveViewContainerObject is on @objectstack/metadata's root entry, strip pm:dispatched from #14399, landing record on the card, inventory reconciliation. The reviewer's advisory finding is already filed as #14680, and the dev's residual-asymmetry finding as #14666; neither blocks this landing.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33669892536 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 12 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33671791799 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 14 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 3c1bbd2Sep 2, 2026
46 checks passed
@os-musk
os-musk deleted the claude/issue-14399-view-container-name-order branch September 2, 2026 19:59
This was referenced Sep 2, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The two SOURCE view-container registrars disagree on where the row's own name sits in the object-derivation chain

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(objectql,metadata): derive a view container's object through the shared helper, so the row's own name is LAST at every SOURCE registrar (#14399) - #14665

Merged
os-musk merged 5 commits into
mainfrom
claude/issue-14399-view-container-name-order
Sep 2, 2026
Merged

fix(objectql,metadata): derive a view container's object through the shared helper, so the row's own name is LAST at every SOURCE registrar (#14399)#14665
os-musk merged 5 commits into
mainfrom
claude/issue-14399-view-container-name-order

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14399

What was wrong

Three sites derive "which object does an aggregated defineView container bind to". After #13407 / #13913 / #13912 all three read the container's own top-level object before the list.data.object chain, but they still disagreed about the row's own name:

SiteChainname
packages/objectql/src/engine.tsresolveMetadataItemName('views', item) — the boot-loop SOURCE registrarname then id then object then list.data.object then form.data.objectFIRST
deriveViewContainerObject (@objectstack/metadata) — the artifact/HMR SOURCE registrar and getViewsByObject()object then list.data.object then form.data.object then nameLAST
expandRuntimeViewContainer (@objectstack/metadata-protocol) — the runtime doorsame as aboveLAST

A container written as { name: 'lead_views', object: 'crm_lead', list: { … } } was therefore keyed lead_views by the boot loop and crm_lead everywhere else, expansion (OBJECT.KEY items) included. getViewsByObject() and GET /meta/view?object= filter the expanded items by their object, so which registrar loaded the document decided whether the views were addressable under the object at all. No error, no diagnostic.

The change

The boot loop's CONTAINER branch now calls deriveViewContainerObjectby import, not by re-spelling. A fourth hand-copy of a chain that already existed three times was the defect, not the repair; the tail of the same function now calls the helper too, so engine.ts holds zero copies of the chain where it used to hold one.

The direction is the 2026-08-07 meta-rule rather than taste: one operation with two inconsistent implementations, the side bound by a DECLARATION wins. ViewSchema.object's own .describe() names its readers; the boot loop's order argued from item identity, which declares nothing about the binding. The two sites that already held the winning order are untouched.

deriveViewContainerObject was module-local, so it is added to @objectstack/metadata's root entry (minor for that package). Measured on this tree: packages/objectql/package.json declares @objectstack/metadata as workspace:*, and @objectstack/metadata's full transitive dependency closure is 6 packages with @objectstack/objectqlnot among them — no cycle, and no deep relative import into another package's src/.

Confinement. Only the container branch moves, gated on isAggregatedViewContainer. The assembled viewItems: channel still keys by its own name first — every member of AssembledViewArtifactSchema requires viewKind, so isAggregatedViewContainer is false for all of them, and there is a control asserting exactly that. item.id is untouched and cannot fire for a container: ViewSchema is a strictObject declaring name and object and no id.

Bounded in-place fix, named per the standing clause: the docblock above resolveMetadataItemName asserted "per spec, ViewSchema does NOT have a top-level name field", which was the stated premise for reading name first. It is measurably false — ViewSchema declares an optional name at view.zod.ts — so the docblock now records what the field actually is and what the old sentence got wrong. Same defect class, same file, mechanically settled by the spec source, no new verification surface.

MEASURED CORRECTION to the card

The card predicted the artifact/HMR registrar would "derive crm_lead and mint crm_lead.default, registering the container under crm_lead" — two SILENT keys for one document. The second half is false, and the divergence is sharper than filed:

The boot loop reconciles that field (toRegister = { ...item, name: itemName }) and the artifact door does not. That residual asymmetry is a separate defect at a separate site; #14399 owns the derivation, and the asymmetry is filed on its own card and pinned as an assertion here so it cannot drift unnoticed.

The fixture is the deliverable

No fixture anywhere in the repo set a container name that differed from its bound object, so the divergence was un-rehearsed in BOTH directions — each registrar was individually green on every shape it had ever been shown, which is also why the correction above had gone unnoticed. packages/objectql/src/view-container-divergent-name-registrars.test.ts drives both SOURCE registrars side by side on one document: the boot loop through ObjectQL.registerApp, the artifact door through MetadataPlugin._parseAndRegisterArtifact, exactly as its own #13912 pin drives it. Four controls (name equals object; no object anywhere; the anonymous container both registrars accept; a standalone ViewItem) are green in both directions.

Verification

Head at the time of the runs below: a95087993.

Reverse verification (ablation), direction predicted before running — the boot-loop legs go red, the artifact-door derivation leg stays green because it does not read engine.ts:

  • mutation proved on disk, not by the editor's exit code: anchor hits 1, injected marker count 1, blob 37ab4cb6 to 775ef0b1;
  • no rebuild leg is owed on this mutation and that is stated rather than assumed — the test imports ./engine, a relative SOURCE specifier, so vitest reads the mutated file directly; the helper it calls comes from @objectstack/metadata's dist, which the mutation does not touch;
  • result: 3 failed / 6 passedexpected [ 'lead_views', …(2) ] to deeply equal [ Array(3) ] and expected [ 'lead_views' ] to deeply equal [ 'crm_lead' ]. The "artifact/HMR registrar derives the SAME binding" case stayed green, as predicted;
  • restore under an EXIT INT TERM trap with absolute paths, proved by blob equality (37ab4cb6 equals the HEAD blob), git diff HEAD empty, marker count 0.

Suites and gates, all on a95087993:

  • pnpm --filter @objectstack/objectql exec vitest run — 264 files / 4556 tests passed
  • pnpm --filter @objectstack/metadata exec vitest run — 43 files / 677 tests passed
  • pnpm --filter @objectstack/objectql typecheckcheck:test-typecheck: OK ... 44 file(s) / 242 error(s) / 69 pinned signature(s) (unmoved; the new test file compiles clean and adds no debt)
  • pnpm lint (whole repo, eslint . --no-inline-config) — exit 0, no narrowing claimed
  • pnpm check:nul-bytesOK (scanned 7997 text file(s) ... no raw ASCII control bytes); pnpm check:error-status-conformance — exit 0
  • the full union derived on this tree by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (62 commands): 57 exit 0, including check-system-context-census, check-cross-package-test-inputs, check:test-source-alias, check:published-files, check:type-check-coverage, check:type-source-resolution, check:undeclared-dep-imports, check:engine-split-ratio
  • 5 NOT MEASURED locally, in each gate's own words, never read as green:check-test-completeness and check:type-check-debt and check:dual-build-cjs-loads (exit 3 — each needs a saved turbo test log or a built package closure), check:pm/half-states (exit 3 — repo-scoped GitHub REST answers 403 in this container), and check:skill-examples (exit 1 on its prerequisite branch: packages/client-react/dist holds no .d.ts declarations — the package is not built). Building the closure to measure them was attempted twice and killed by this container's 10-minute foreground cap both times, so the narrowing is declared rather than hidden; CI builds the closure and runs all five.

content/docs/permissions/system-context.mdx was re-anchored by node scripts/check-system-context-census.mjs --fix on the merged tree (15 anchors rewritten), never hand-edited; the gate is green on the final head.

Migration

None. Measured on this tree: of the 54 non-test sources that author or carry view containers, zero declare a name that differs from the object they bind to, so every in-tree container derived identically at all three sites before this change and does after it. What moves is the latent shape only.

Clause-2

yes — self-read from the final diff, against the seat's no. The content limb is hit: packages/metadata/src/index.ts adds a new export to the package's public entry, which widens the published surface (the minor in the changeset is the same fact). No path-limb file is touched — the diff does not enter packages/spec/src/**. needs:contract-review is hung on this PR accordingly.

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…shared helper, so the row's own `name` is LAST at every SOURCE registrar (#14399)
The ObjectQL boot-loop registrar read a container's own `name` FIRST, while
`deriveViewContainerObject` (@objectstack/metadata) and
`expandRuntimeViewContainer` (@objectstack/metadata-protocol) read it LAST. A
container written as `{ name: 'lead_views', object: 'crm_lead', list: { … } }`
was therefore keyed `lead_views` by one SOURCE registrar and `crm_lead` by the
others, expansion included — and `getViewsByObject()` / `GET /meta/view?object=`
filter on the expanded items' `object`, so which registrar loaded the document
decided whether the views were addressable under the object at all.
The boot loop's container branch now calls `deriveViewContainerObject` by
import; a fourth hand-copy of the chain was the defect, not the repair.
`deriveViewContainerObject` moves onto `@objectstack/metadata`'s root entry to
make that import legal without reaching into another package's `src/`.
Only the CONTAINER branch moves, gated on `isAggregatedViewContainer`; the
assembled `viewItems:` channel still keys by its own `name` first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…st view-container sources are divergent) (#14399)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… shift (#14399)
Generated by `node scripts/check-system-context-census.mjs --fix` on the merged
tree; 15 anchors rewritten, never hand-edited.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata, @objectstack/objectql, touching 4 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/metadata/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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 resolveMetadataItemName))
  • content/docs/api/client-sdk.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/api/error-catalog.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/data-modeling/index.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/deployment/validating-metadata.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/permissions/authorization.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/ui/index.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))

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 resolveMetadataItemName))

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
  • 1 changed file(s) yielded no anchor (packages/metadata/src/index.ts) — pages documenting those are invisible to this run
  • 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 — 24 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 2aa8456cf2d66ec3825d262686fe4218e57cfd27packageMentionDocs.

Which tree this was computed on

This run read content/docs from dc744ff995c04d7c29856663afb483fcf15c20f7 — the merge of head a95087993bc3ec03604b95e0596b29250457170c into base 2aa8456cf2d66ec3825d262686fe4218e57cfd27, 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 dc744ff995c04d7c29856663afb483fcf15c20f7 && git checkout dc744ff995c04d7c29856663afb483fcf15c20f7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2aa8456cf2d66ec3825d262686fe4218e57cfd27 a95087993bc3ec03604b95e0596b29250457170c && git checkout -B drift-repro 2aa8456cf2d66ec3825d262686fe4218e57cfd27 && git merge --no-ff a95087993bc3ec03604b95e0596b29250457170c
node scripts/docs-audit/affected-docs.mjs --json 2aa8456cf2d66ec3825d262686fe4218e57cfd27

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session session_0112hMx9hjJ9BgB28X97DS68): flipped ready at 18:13Z and armed auto-merge (squash) at 18:13:38Z on head a95087993. The echo read method: MERGE; the queue build ref is the arming reading, not the echo.

  • Contract review: PASS, adopted verbatim from an isolated reviewer explicitly passed model: fable — not reviewed in-seat. The tier fuse was read with get_session and blew: CONTRACT_REVIEW_TIER is claude-fable-5-1 while this session's session_context.model and last_served_model both read claude-opus-5. Reviewer transcript verified per assistant message before adoption: 79 of 79 report claude-fable-5-1. Full verdict on the card, comment 5514186759.
  • Clause-② yes, derived independently by the reviewer from the final diff — content limb, widening sub-limb (packages/metadata/src/index.ts:56). This seat dispatched no and was wrong; the dev's Zone 2 measurement caught it. needs:contract-review cleared from both carriers (card The two SOURCE view-container registrars disagree on where the row's own name sits in the object-derivation chain #14399 and this PR) in one stroke at PASS.
  • Governed-surface test re-run on the final five-path file list: 0 of 5 path(s) hit the register — ordinary queue landing. Re-derived rather than recalled, per the script's own warning that the register has grown several times in two days.
  • Enqueue bar: all 44 check runs on a95087993 completed success or skipped, read with perPage: 50. The last one standing was Check Changeset, which was re-triggered at 18:11:57Z by this seat's own label edit and came back green at 18:12:50Z — the flip waited for it rather than being armed on a partial reading, even though the same check had already passed twice on this identical head.

⚠️ Known exposure, stated in advance: the queue build runs the full suite, so this PR meets packages/cli/test/run-dev-unbuilt-workspace.e2e.test.ts — the flake tracked on #14648, which ejected #14629 at 17:45Z. If this PR is ejected the same way the disposition is fixed: ⛔ no re-queue while #14648 is open and unfixed, ⛔ no weakening of that test, and the card goes to pm:blocked with an Unlock-action: line rather than being re-dispatched.

Owed by the seat at MERGED: verify by content on origin/main that engine.ts holds zero copies of the derivation chain and that deriveViewContainerObject is on @objectstack/metadata's root entry, strip pm:dispatched from #14399, landing record on the card, inventory reconciliation. The reviewer's advisory finding is already filed as #14680, and the dev's residual-asymmetry finding as #14666; neither blocks this landing.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33669892536 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 12 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33671791799 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 14 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 3c1bbd2Sep 2, 2026
46 checks passed
@os-musk
os-musk deleted the claude/issue-14399-view-container-name-order branch September 2, 2026 19:59
This was referenced Sep 2, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The two SOURCE view-container registrars disagree on where the row's own name sits in the object-derivation chain

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(objectql,metadata): derive a view container's object through the shared helper, so the row's own name is LAST at every SOURCE registrar (#14399) - #14665

Merged
os-musk merged 5 commits into
mainfrom
claude/issue-14399-view-container-name-order
Sep 2, 2026
Merged

fix(objectql,metadata): derive a view container's object through the shared helper, so the row's own name is LAST at every SOURCE registrar (#14399)#14665
os-musk merged 5 commits into
mainfrom
claude/issue-14399-view-container-name-order

Conversation

@os-musk

Copy link
Copy Markdown
Collaborator

Fixes#14399

What was wrong

Three sites derive "which object does an aggregated defineView container bind to". After #13407 / #13913 / #13912 all three read the container's own top-level object before the list.data.object chain, but they still disagreed about the row's own name:

SiteChainname
packages/objectql/src/engine.tsresolveMetadataItemName('views', item) — the boot-loop SOURCE registrarname then id then object then list.data.object then form.data.objectFIRST
deriveViewContainerObject (@objectstack/metadata) — the artifact/HMR SOURCE registrar and getViewsByObject()object then list.data.object then form.data.object then nameLAST
expandRuntimeViewContainer (@objectstack/metadata-protocol) — the runtime doorsame as aboveLAST

A container written as { name: 'lead_views', object: 'crm_lead', list: { … } } was therefore keyed lead_views by the boot loop and crm_lead everywhere else, expansion (OBJECT.KEY items) included. getViewsByObject() and GET /meta/view?object= filter the expanded items by their object, so which registrar loaded the document decided whether the views were addressable under the object at all. No error, no diagnostic.

The change

The boot loop's CONTAINER branch now calls deriveViewContainerObjectby import, not by re-spelling. A fourth hand-copy of a chain that already existed three times was the defect, not the repair; the tail of the same function now calls the helper too, so engine.ts holds zero copies of the chain where it used to hold one.

The direction is the 2026-08-07 meta-rule rather than taste: one operation with two inconsistent implementations, the side bound by a DECLARATION wins. ViewSchema.object's own .describe() names its readers; the boot loop's order argued from item identity, which declares nothing about the binding. The two sites that already held the winning order are untouched.

deriveViewContainerObject was module-local, so it is added to @objectstack/metadata's root entry (minor for that package). Measured on this tree: packages/objectql/package.json declares @objectstack/metadata as workspace:*, and @objectstack/metadata's full transitive dependency closure is 6 packages with @objectstack/objectqlnot among them — no cycle, and no deep relative import into another package's src/.

Confinement. Only the container branch moves, gated on isAggregatedViewContainer. The assembled viewItems: channel still keys by its own name first — every member of AssembledViewArtifactSchema requires viewKind, so isAggregatedViewContainer is false for all of them, and there is a control asserting exactly that. item.id is untouched and cannot fire for a container: ViewSchema is a strictObject declaring name and object and no id.

Bounded in-place fix, named per the standing clause: the docblock above resolveMetadataItemName asserted "per spec, ViewSchema does NOT have a top-level name field", which was the stated premise for reading name first. It is measurably false — ViewSchema declares an optional name at view.zod.ts — so the docblock now records what the field actually is and what the old sentence got wrong. Same defect class, same file, mechanically settled by the spec source, no new verification surface.

MEASURED CORRECTION to the card

The card predicted the artifact/HMR registrar would "derive crm_lead and mint crm_lead.default, registering the container under crm_lead" — two SILENT keys for one document. The second half is false, and the divergence is sharper than filed:

The boot loop reconciles that field (toRegister = { ...item, name: itemName }) and the artifact door does not. That residual asymmetry is a separate defect at a separate site; #14399 owns the derivation, and the asymmetry is filed on its own card and pinned as an assertion here so it cannot drift unnoticed.

The fixture is the deliverable

No fixture anywhere in the repo set a container name that differed from its bound object, so the divergence was un-rehearsed in BOTH directions — each registrar was individually green on every shape it had ever been shown, which is also why the correction above had gone unnoticed. packages/objectql/src/view-container-divergent-name-registrars.test.ts drives both SOURCE registrars side by side on one document: the boot loop through ObjectQL.registerApp, the artifact door through MetadataPlugin._parseAndRegisterArtifact, exactly as its own #13912 pin drives it. Four controls (name equals object; no object anywhere; the anonymous container both registrars accept; a standalone ViewItem) are green in both directions.

Verification

Head at the time of the runs below: a95087993.

Reverse verification (ablation), direction predicted before running — the boot-loop legs go red, the artifact-door derivation leg stays green because it does not read engine.ts:

  • mutation proved on disk, not by the editor's exit code: anchor hits 1, injected marker count 1, blob 37ab4cb6 to 775ef0b1;
  • no rebuild leg is owed on this mutation and that is stated rather than assumed — the test imports ./engine, a relative SOURCE specifier, so vitest reads the mutated file directly; the helper it calls comes from @objectstack/metadata's dist, which the mutation does not touch;
  • result: 3 failed / 6 passedexpected [ 'lead_views', …(2) ] to deeply equal [ Array(3) ] and expected [ 'lead_views' ] to deeply equal [ 'crm_lead' ]. The "artifact/HMR registrar derives the SAME binding" case stayed green, as predicted;
  • restore under an EXIT INT TERM trap with absolute paths, proved by blob equality (37ab4cb6 equals the HEAD blob), git diff HEAD empty, marker count 0.

Suites and gates, all on a95087993:

  • pnpm --filter @objectstack/objectql exec vitest run — 264 files / 4556 tests passed
  • pnpm --filter @objectstack/metadata exec vitest run — 43 files / 677 tests passed
  • pnpm --filter @objectstack/objectql typecheckcheck:test-typecheck: OK ... 44 file(s) / 242 error(s) / 69 pinned signature(s) (unmoved; the new test file compiles clean and adds no debt)
  • pnpm lint (whole repo, eslint . --no-inline-config) — exit 0, no narrowing claimed
  • pnpm check:nul-bytesOK (scanned 7997 text file(s) ... no raw ASCII control bytes); pnpm check:error-status-conformance — exit 0
  • the full union derived on this tree by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (62 commands): 57 exit 0, including check-system-context-census, check-cross-package-test-inputs, check:test-source-alias, check:published-files, check:type-check-coverage, check:type-source-resolution, check:undeclared-dep-imports, check:engine-split-ratio
  • 5 NOT MEASURED locally, in each gate's own words, never read as green:check-test-completeness and check:type-check-debt and check:dual-build-cjs-loads (exit 3 — each needs a saved turbo test log or a built package closure), check:pm/half-states (exit 3 — repo-scoped GitHub REST answers 403 in this container), and check:skill-examples (exit 1 on its prerequisite branch: packages/client-react/dist holds no .d.ts declarations — the package is not built). Building the closure to measure them was attempted twice and killed by this container's 10-minute foreground cap both times, so the narrowing is declared rather than hidden; CI builds the closure and runs all five.

content/docs/permissions/system-context.mdx was re-anchored by node scripts/check-system-context-census.mjs --fix on the merged tree (15 anchors rewritten), never hand-edited; the gate is green on the final head.

Migration

None. Measured on this tree: of the 54 non-test sources that author or carry view containers, zero declare a name that differs from the object they bind to, so every in-tree container derived identically at all three sites before this change and does after it. What moves is the latent shape only.

Clause-2

yes — self-read from the final diff, against the seat's no. The content limb is hit: packages/metadata/src/index.ts adds a new export to the package's public entry, which widens the published surface (the minor in the changeset is the same fact). No path-limb file is touched — the diff does not enter packages/spec/src/**. needs:contract-review is hung on this PR accordingly.

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68


Generated by Claude Code

…shared helper, so the row's own `name` is LAST at every SOURCE registrar (#14399)
The ObjectQL boot-loop registrar read a container's own `name` FIRST, while
`deriveViewContainerObject` (@objectstack/metadata) and
`expandRuntimeViewContainer` (@objectstack/metadata-protocol) read it LAST. A
container written as `{ name: 'lead_views', object: 'crm_lead', list: { … } }`
was therefore keyed `lead_views` by one SOURCE registrar and `crm_lead` by the
others, expansion included — and `getViewsByObject()` / `GET /meta/view?object=`
filter on the expanded items' `object`, so which registrar loaded the document
decided whether the views were addressable under the object at all.
The boot loop's container branch now calls `deriveViewContainerObject` by
import; a fourth hand-copy of the chain was the defect, not the repair.
`deriveViewContainerObject` moves onto `@objectstack/metadata`'s root entry to
make that import legal without reaching into another package's `src/`.
Only the CONTAINER branch moves, gated on `isAggregatedViewContainer`; the
assembled `viewItems:` channel still keys by its own `name` first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…st view-container sources are divergent) (#14399)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… shift (#14399)
Generated by `node scripts/check-system-context-census.mjs --fix` on the merged
tree; 15 anchors rewritten, never hand-edited.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata, @objectstack/objectql, touching 4 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/metadata/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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 resolveMetadataItemName))
  • content/docs/api/client-sdk.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/api/error-catalog.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/data-modeling/index.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/deployment/validating-metadata.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/permissions/authorization.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))
  • content/docs/ui/index.mdx(via crm_lead (literal, a string literal in resolveMetadataItemName))

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 resolveMetadataItemName))

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
  • 1 changed file(s) yielded no anchor (packages/metadata/src/index.ts) — pages documenting those are invisible to this run
  • 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 — 24 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 2aa8456cf2d66ec3825d262686fe4218e57cfd27packageMentionDocs.

Which tree this was computed on

This run read content/docs from dc744ff995c04d7c29856663afb483fcf15c20f7 — the merge of head a95087993bc3ec03604b95e0596b29250457170c into base 2aa8456cf2d66ec3825d262686fe4218e57cfd27, 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 dc744ff995c04d7c29856663afb483fcf15c20f7 && git checkout dc744ff995c04d7c29856663afb483fcf15c20f7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2aa8456cf2d66ec3825d262686fe4218e57cfd27 a95087993bc3ec03604b95e0596b29250457170c && git checkout -B drift-repro 2aa8456cf2d66ec3825d262686fe4218e57cfd27 && git merge --no-ff a95087993bc3ec03604b95e0596b29250457170c
node scripts/docs-audit/affected-docs.mjs --json 2aa8456cf2d66ec3825d262686fe4218e57cfd27

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

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance (engine execution seat, session session_0112hMx9hjJ9BgB28X97DS68): flipped ready at 18:13Z and armed auto-merge (squash) at 18:13:38Z on head a95087993. The echo read method: MERGE; the queue build ref is the arming reading, not the echo.

  • Contract review: PASS, adopted verbatim from an isolated reviewer explicitly passed model: fable — not reviewed in-seat. The tier fuse was read with get_session and blew: CONTRACT_REVIEW_TIER is claude-fable-5-1 while this session's session_context.model and last_served_model both read claude-opus-5. Reviewer transcript verified per assistant message before adoption: 79 of 79 report claude-fable-5-1. Full verdict on the card, comment 5514186759.
  • Clause-② yes, derived independently by the reviewer from the final diff — content limb, widening sub-limb (packages/metadata/src/index.ts:56). This seat dispatched no and was wrong; the dev's Zone 2 measurement caught it. needs:contract-review cleared from both carriers (card The two SOURCE view-container registrars disagree on where the row's own name sits in the object-derivation chain #14399 and this PR) in one stroke at PASS.
  • Governed-surface test re-run on the final five-path file list: 0 of 5 path(s) hit the register — ordinary queue landing. Re-derived rather than recalled, per the script's own warning that the register has grown several times in two days.
  • Enqueue bar: all 44 check runs on a95087993 completed success or skipped, read with perPage: 50. The last one standing was Check Changeset, which was re-triggered at 18:11:57Z by this seat's own label edit and came back green at 18:12:50Z — the flip waited for it rather than being armed on a partial reading, even though the same check had already passed twice on this identical head.

⚠️ Known exposure, stated in advance: the queue build runs the full suite, so this PR meets packages/cli/test/run-dev-unbuilt-workspace.e2e.test.ts — the flake tracked on #14648, which ejected #14629 at 17:45Z. If this PR is ejected the same way the disposition is fixed: ⛔ no re-queue while #14648 is open and unfixed, ⛔ no weakening of that test, and the card goes to pm:blocked with an Unlock-action: line rather than being re-dispatched.

Owed by the seat at MERGED: verify by content on origin/main that engine.ts holds zero copies of the derivation chain and that deriveViewContainerObject is on @objectstack/metadata's root entry, strip pm:dispatched from #14399, landing record on the card, inventory reconciliation. The reviewer's advisory finding is already filed as #14680, and the dev's residual-asymmetry finding as #14666; neither blocks this landing.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33669892536 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 12 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33671791799 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 14 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 3c1bbd2Sep 2, 2026
46 checks passed
@os-musk
os-musk deleted the claude/issue-14399-view-container-name-order branch September 2, 2026 19:59
This was referenced Sep 2, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The two SOURCE view-container registrars disagree on where the row's own name sits in the object-derivation chain

2 participants

@os-musk@claude