Skip to content

fix(objectql): the boot loop refuses a view container whose name disagrees with its derived object key (#14666) - #15319

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-14666-boot-loop-refuses-divergent-view-name
Sep 4, 2026
Merged

fix(objectql): the boot loop refuses a view container whose name disagrees with its derived object key (#14666)#15319
os-warren merged 2 commits into
mainfrom
claude/issue-14666-boot-loop-refuses-divergent-view-name

Conversation

@claude

@claude claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #14666

Ruled direction 2 (maintainer, via the director seat, 2026-09-03, comment 5520462984): the boot loop's SOURCE registrar refuses a view container whose name is set and differs from the key derived from its object binding, with the same refusal the artifact/HMR door already raises, and stops rewriting the author's field silently.


1. A2.1 first — which site(s) actually register a view container on the boot path

Answer: exactly one — engine.ts:5298 (the ternary) / :5327 (the registerItem call), inside the generic METADATA_ARRAY_KEYS loop. The second, identical-looking ternary at :5380 is alive but cannot carry a view container at all, so it was NOT touched.

Driven, not inferred. I wrapped the live SchemaRegistry.registerItem on a real booted engine and captured a stack frame at every call, then drove a divergent container through each channel.

Registering views: [divergentContainer] through engine.registerApp:

REG view name=crm_lead         | at ObjectQL.registerMetadataCollections (engine.ts:5327:30)
REG view name=crm_lead.hot     | at ObjectQL.registerMetadataCollections (engine.ts:5339:38)
REG view name=crm_lead.default | at ObjectQL.registerMetadataCollections (engine.ts:5339:38)

:5327 is the container registration (its document is the one :5298 rewrote); :5339 is the expansion. Zero frames at :5381.

Driving the same container through the viewItems: channel instead:

AssembledViewArtifactSchema.safeParse(container).success = false
viewItems: channel with a CONTAINER — thrown = INVALID_METADATA/422:
  Invalid `viewItems:` entry 'crm_lead' ...: the assembled-manifest channel carries
  non-container view artifacts only

It is refused by the schema before the :5380 ternary is reached. The structural reason, confirmed by an admission matrix over every branch of the union:

body accepted parsed name isAggregatedViewContainer(parsed)
viewItem WITH name true crm_lead.hot false
viewItem NO name false
listOverlay WITH name true crm_lead.all false
listOverlay NO name true undefined false
formOverlay (either) false
the divergent CONTAINER false
container-ish without viewKind false

AssembledViewArtifactSchema is built as VIEW_METADATA_BRANCHES minus container, and every body it admits is isAggregatedViewContainer === false. So at :5380, resolveMetadataItemName('views', body) takes the non-container path and reads body.name FIRST — meaning the ternary is the identity branch for every body that has a name, and can only mint a name onto an overlay that has none (the listOverlay NO name row). It can never discard an authored name, which is the thing this card is about.

Consequently changed: the views: container branch at :5298 only.
Consequently NOT changed: the viewItems: ternary at :5380, the artifact/HMR door, resolveMetadataItemName, deriveViewContainerObject, and packages/spec.

Both traps named at dispatch are therefore avoided by measurement rather than by judgement: there is no surviving container rewrite at :5380 because no container reaches it, and the refusal is not generic because it is gated inside the views branch. A scope control pins this measurement so a later hand cannot copy the refusal onto a site with nothing to refuse, nor widen that channel back open.

2. Zone 2, item by item

A2.1 — CONFIRMED with a correction of emphasis. Both ternaries exist exactly where dispatch read them (:5298, :5380). The reading that they are two candidate container registrars is FALSIFIED: only one can register a container. Evidence above.

A2.2 — CONFIRMED. The artifact door is NOT in packages/objectql/src/plugin.ts.

$ grep -c "deriveViewContainerObject" packages/objectql/src/plugin.ts   ->  0
$ grep -c "export" packages/objectql/src/plugin.ts                      ->  3   (positive control: file exists, grep works)
$ grep -n "deriveViewContainerObject" packages/metadata/src/plugin.ts
  184:import { deriveViewContainerObject } from './view-container-expansion.js';
  1110:  const viewObject = deriveViewContainerObject(item);
  1162:  name = deriveViewContainerObject(item);

It is packages/metadata/src/plugin.ts:1110 and :1162, exactly as dispatch read it. Its refusal is raised at :1121 (this.manager.register('view', viewObject, item, ...) with the item unchanged) by assertMetadataRegisterContract (packages/core/src/metadata-service-contract.ts:153-176): REGISTER_REFUSAL_CODE = 'VALIDATION_ERROR', err.status = 400. That is the envelope the new boot-loop refusal matches, and the pin now asserts the two EQUAL rather than asserting each separately.

A2.3 — CONFIRMED: they compute the same key, and the boot loop does not merely agree, it delegates. resolveMetadataItemName (engine.ts:2041) opens with

if (key === 'views' && isAggregatedViewContainer(item)) {
  return deriveViewContainerObject(item);
}

so for the container branch there is one derivation, not two. Measured across the shape matrix (boot-minted key vs deriveViewContainerObject, same document):

shape deriveViewContainerObject boot-minted key agree
name and object diverge crm_lead crm_lead yes
no name crm_lead crm_lead yes
name equals object crm_lead crm_lead yes
name only, no binding lead_views lead_views yes
binding via list.data.object crm_lead crm_lead yes
binding via form.data.object crm_lead crm_lead yes
bare name, not a container solo solo yes

So "diverges from the derived key" means ONE thing at both doors, and no authority question arises. This also hands the change its scope for free: the name-only row derives its key FROM that same name, so it cannot disagree with itself and never reaches the refusal.

A2.4 — CONFIRMED, and the red is recorded below. The landed pin does pin today's behaviour on both sides; a green run of it after the change would have meant nothing.

A2.5 — done; per-family table in section 4. The tool's blind spot bit here exactly as warned: check:system-context-census is NOT in the derived list (it keys on the docs path, which my diff does not touch) yet my edit reds it, because that page is a generated line-anchor table into engine.ts and I inserted lines above the anchors. Repaired with --fix only, twice — see section 4.

A2.6 — nothing to report. No content/docs/releases/** path is touched.

$ git diff --name-only origin/main...HEAD | grep -c "content/docs/releases"   ->  0

The one docs path in this PR is content/docs/permissions/system-context.mdx, which is the census's own generated anchor table, not a release page.

3. Red before green — the pin inverting

With engine.ts changed and the pin still in its landed form, pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/view-container-divergent-name-registrars.test.ts:

 ❯ src/view-container-divergent-name-registrars.test.ts (9 tests | 3 failed)
   × THE PIN: the boot loop keys the container by its declared `object`, not by its own `name`
   × MEASURED CORRECTION: the artifact door does not silently mis-key it — it refuses, enveloped
   × so the boot loop's expanded items are addressable under the object

Error: Invalid `views:` container from manifest 'com.acme.crm': the container's own `name` is
'lead_views', which disagrees with the object key it binds to, 'crm_lead' ...
 ❯ ObjectQL.registerMetadataCollections src/engine.ts:5355:75

 Test Files  1 failed (1)
      Tests  3 failed | 6 passed (9)

Three reds, and they are precisely the three boot-loop assertions that drive divergentContainer. The six that stayed green are the load-bearing half: the artifact-door assertions (untouched, as the ruling requires) and all four scope CONTROLs — no name, name already equal, name with no binding, and the standalone ViewItem. That the controls never went red is the scope evidence, taken before any test was edited.

Then the two boot-loop assertions were inverted deliberately (each marked in place, carrying what it used to read), the expansion pin was repointed at the anonymous container, and three scope controls were added. After: Tests 12 passed (12).

4. Gate families, per family, with exit codes

73 family invocations run — the full derived set (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, which read my real change set from the merge base itself), minus the ones whose invocation is CI-only (RUNNER_TEMP, matrix.shard), plus the convention-scoped ones the path mapper cannot see. Every family not listed here exited 0. Non-zero, each resolved:

family first exit reading resolution
check:system-context-census 1 real red, mine — 18 problems, every anchor delta exactly +71, my insertion size check-system-context-census.mjs --fix, nothing else. Ran twice: the first pass predated a later 6-line comment addition, and the post-commit re-run caught the residue. Green
check:doc-authoring 1 real red, mine — my new runtime string carried a tracker id at engine.ts:5361 stripped the id from the message, kept it in the adjacent comment where a reader can resolve it (maintainer ruling 2026-08-12). Green
check:dual-build-cjs-loads 3 NOT MEASURED — "PREREQUISITE NOT MET ... some package has no dist/" built the workspace (turbo run build, 71/71). Re-ran: 0, a real measurement
check:type-check-debt 3 NOT MEASURED — "PREREQUISITE NOT MET ... 19 workspace dependencies have no built type entry point" same build. Re-ran: 0, a real measurement
check:published-readme-exports 1 NOT MEASURED despite exit 1 — every line reads "does not exist. Build first" same build. Re-ran: 0
check:react-declaration-parity 1 NOT MEASURED, and NOT mine — "MANIFEST is not set ... This gate did NOT run" needs a browser-driven objectui build to produce sdui.manifest.json; this diff touches no React block and no packages/spec. Reported, not chased
node scripts/pr-labels.mjs 1 not a finding — bare invocation prints usage; it needs a mode flag --self-test: 0

Convention-scoped families run beyond the derived list, because my change's kind trips them (engine.ts line shift, a declared-breaking changeset): check:system-context-census 0, check:adr-anchors 0, check:adr-symbol-anchors 0, check:type-check-coverage 0, check:type-check-debt 0, check-engine-split-ratio.mjs 0 (and --days 90 0), check-adr-0087-registration.mjs 0 both --self-test and --base.

The ratchet families were re-run on the final commit and again after merging origin/main, at 1d212866a: check:system-context-census 0, check:doc-authoring 0, check:nul-bytes 0, check-engine-split-ratio.mjs 0, check-adr-0087-registration.mjs --base origin/main 0, check:changeset-gate-self-tests 0, check:cross-package-test-inputs 0, check:error-code-casing 0, check:partof-closing-keyword 0.

5. Tests and typecheck

  • Pin file at 1d212866a: Tests 12 passed (12).
  • pnpm --filter @objectstack/objectql test at 1d212866a: Test Files 269 passed (269), Tests 4629 passed (4629).
  • pnpm --filter @objectstack/objectql typecheck (which chains tsc --noEmit, the scripts project, and check:test-typecheck): exit 0, "the test layer compiles ... 44 files / 242 errors / 69 pinned signatures held in test-typecheck-debt.json (shrink-only)".
  • ⚠️ That package's tsconfig.json excludes **/*.test.ts, so the base typecheck says nothing about the pin file. Confirmed the test layer really does cover both edited files rather than assuming it: tsc -p tsconfig.test.json --listFiles lists engine.ts (1) and the pin file (1), and neither contributes a single error line — grep -cE "^src/engine\.ts\(" and the pin's equivalent both return 0, with src/datasource-mapping.test.ts returning 17 as the firing positive control.

6. Changeset — package derived, not copied

The ruling guessed @objectstack/metadata-protocol "(or wherever the SOURCE registrar lives)". Falsified:

$ grep -c "registerMetadataCollections" packages/metadata-protocol/src/*.ts   ->  0 hits
$ grep -c "registerMetadataCollections" packages/objectql/src/engine.ts       ->  6   (positive control)

The SOURCE registrar lives in packages/objectql, whose manifest declares "name": "@objectstack/objectql", "version": "17.2.0", no private flag, publishConfig.access: public — a published package, so the changeset is real and unpublished is not available to it.

.changeset/objectql-boot-loop-refuses-divergent-view-name.md@objectstack/objectql: minor, per the ruling, with a BREAKING banner naming the refused shape precisely and both remedies. Its ADR-0087 disposition is not-required (no-migration-prescription), which the gate accepts (exit 0) and which the closest in-tree precedent uses for the same situation — calendar-visualization-requires-startdatefield.md, also a minor accept-set narrowing with a BREAKING banner. The justification is that no key is removed, renamed or re-shaped (forbidding ViewSchema.name in spec was direction 3, explicitly refused), and which of three repairs a divergent container wants is authoring intent no migration can decide — the author may have meant the container name to go, meant it to become the object key, or mistyped object and want THAT corrected, and the stored document carries no evidence of which.

7. Fixture triage — the refusal fires on nothing in this repo

Scanned by the rule's consumption radius rather than by the edited package. A region scan over every views: [ ... ] array literal in the tree found 30 regions carrying both a name and an object; the scanner flags a synthetic divergent container as its firing positive control. None of the 30 can reach this seam — none of their files calls registerApp or constructs an ObjectQL, with the pin file itself as the positive control for that filter. They belong to packages/lint, packages/cli and the artifact door, and most of the matched names are nested listViews: view names, not container names.

Complementary caller-side enumeration: of the 295 files that boot an engine, exactly three carry an inline views: container, all three in packages/objectql, all green. The four example-app .view.ts containers declare no top-level name at all (positive control: the same awk finds one in a synthetic file), which independently reproduces the objectstack half of the director seat's cross-repo measurement.

8. What is deliberately not here

Not touched: the artifact/HMR door and its pins (the ruling says they stay), packages/spec (off-surface, and direction 3 is refused), the viewItems: ternary at :5380 (measured unreachable for containers), and the boot loop's minting of a derived name onto a container that carries none — that is not a rewrite of an author's field, and the ruling leaves the no-name container unaffected.

Per Clause-2 this PR stays a draft, is not enqueued and has no auto-merge armed; it lands only after a PASS at CONTRACT_REVIEW_TIER. needs:contract-review is hung on this PR and on #14666 now that a reviewable diff exists.


Generated by Claude Code

…s derived object key (#14666)

The ObjectQL boot loop's SOURCE registrar (`registerMetadataCollections`)
silently rewrote an aggregated view container's own `name` to the key derived
from its `object` binding, discarding the author's field with no diagnostic.
The other SOURCE registrar — the metadata artifact/HMR door — refused the very
same document through `assertMetadataRegisterContract` (#7378 row 1),
`VALIDATION_ERROR` / 400. One document, two registrars, opposite outcomes, and
which one an author got depended on how their package was loaded.

Ruled direction 2 (maintainer, 2026-09-03): the boot loop converges onto the
refusal. It now throws the same envelope the artifact door raises, naming the
container's own `name`, the object key it derived, and both remedies.

Scope, the ruling's named main risk: the gate is `views` + aggregated container
+ `name` present + `name` different. A container with no `name`, one whose
`name` already agrees, one that declares no binding elsewhere, every non-`views`
metadata kind, and the assembled `viewItems:` channel are all untouched, each
with a control test.

Measured, and why only one of the method's two identical `toRegister` ternaries
moved: a view container cannot reach the `viewItems:` site at all —
`AssembledViewArtifactSchema` is the view vocabulary minus the container branch,
and every body it admits is `isAggregatedViewContainer === false`. That ternary
can only mint a name onto an overlay that has none, never discard an authored
one.

`view-container-divergent-name-registrars.test.ts` inverts its two boot-loop
assertions (it pinned today's behaviour, not which door was right); the
artifact-door pins stay and are now the reference the boot loop's envelope is
asserted equal to.

`content/docs/permissions/system-context.mdx` line anchors re-anchored with
`check-system-context-census.mjs --fix` — pure +71 line rot, no prose change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/client-sdk.mdx (via VALIDATION_ERROR (literal, a string literal in registerMetadataCollections))
  • content/docs/api/error-catalog.mdx (via VALIDATION_ERROR (literal, a string literal in registerMetadataCollections))
  • content/docs/api/error-handling-client.mdx (via VALIDATION_ERROR (literal, a string literal in registerMetadataCollections))
  • content/docs/api/error-handling-server.mdx (via VALIDATION_ERROR (literal, a string literal in registerMetadataCollections))
  • content/docs/automation/webhooks.mdx (via VALIDATION_ERROR (literal, a string literal in registerMetadataCollections))
  • content/docs/data-modeling/drivers.mdx (via VALIDATION_ERROR (literal, a string literal in registerMetadataCollections))
  • content/docs/protocol/kernel/error-handling.mdx (via VALIDATION_ERROR (literal, a string literal in registerMetadataCollections))
  • content/docs/protocol/objectql/types.mdx (via VALIDATION_ERROR (literal, a string literal in registerMetadataCollections))
  • content/docs/ui/forms.mdx (via VALIDATION_ERROR (literal, a string literal in registerMetadataCollections))

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

  • content/docs/releases/v17.mdx (via VALIDATION_ERROR (literal, a string literal in registerMetadataCollections))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 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 — 16 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 88a35c280a00db85855461e04483af13918b2f1apackageMentionDocs.

Which tree this was computed on

This run read content/docs from bb6b544f5a3ea928ae7540f694b2c1d97f66d062 — the merge of head 1d212866a0977af0a6fbb82b37998fbea1bc0036 into base 88a35c280a00db85855461e04483af13918b2f1a, 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 bb6b544f5a3ea928ae7540f694b2c1d97f66d062 && git checkout bb6b544f5a3ea928ae7540f694b2c1d97f66d062
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 88a35c280a00db85855461e04483af13918b2f1a 1d212866a0977af0a6fbb82b37998fbea1bc0036 && git checkout -B drift-repro 88a35c280a00db85855461e04483af13918b2f1a && git merge --no-ff 1d212866a0977af0a6fbb82b37998fbea1bc0036

node scripts/docs-audit/affected-docs.mjs --json 88a35c280a00db85855461e04483af13918b2f1a

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

Copy link
Copy Markdown
Collaborator

Seat review — ⭐ you FALSIFIED my central assumption, and that is the best outcome this dispatch could have had. ⛔ This is not the contract review.

domain:engine execution seat, session session_01ARYe3yQTQCUFm5qPYNgKaJ, 11:03Z. Read at 1d212866a. needs:contract-review verified on both carriers (this PR and #14666).

1. The falsification is the finding

I dispatched this card with a warning I was confident about: engine.ts carries the silent-rewrite ternary twice (:5298, :5380), no comment in the thread mentions the second, and both obvious moves are wrong in opposite directions.

Half of that was right and the load-bearing half was wrong. Both ternaries do exist where I read them — but my reading that they are two candidate container registrars is false, and you did not argue it away, you instrumented the live SchemaRegistry.registerItem on a booted engine and captured a stack frame per call:

REG view name=crm_lead         | at ObjectQL.registerMetadataCollections (engine.ts:5327:30)
REG view name=crm_lead.hot     | at ... (engine.ts:5339:38)
REG view name=crm_lead.default | at ... (engine.ts:5339:38)
Zero frames at :5381.

and then explained why structurally rather than leaving it as an observation: AssembledViewArtifactSchema is VIEW_METADATA_BRANCHES minus container, so a container is refused at the viewItems: door before :5380 is reached — with an admission matrix over every branch of the union to show it, not one example.

⭐ And the sharpest consequence, which I did not ask for and which retires the whole worry: at :5380 resolveMetadataItemName reads body.name first for every body that channel admits, so that ternary is the identity branch whenever a name exists and can only mint one onto an overlay that has none. It can never discard an authored name — which is the entire subject of this card.

⇒ Both traps I named are avoided by measurement, not by judgement, and you pinned the measurement with a scope control so a later hand cannot copy the refusal onto a site with nothing to refuse. ⛔ I would have accepted a defensible argument here; you produced evidence instead.

2. Three more places you went past the brief

  • A2.2 — you confirmed my anchor correction (the artifact door is packages/metadata/src/plugin.ts, ⛔ not packages/objectql/src/plugin.ts) and then found what I had not: the refusal is raised at :1121 by assertMetadataRegisterContract with REGISTER_REFUSAL_CODE = 'VALIDATION_ERROR' / status 400 — so the pin now asserts the two envelopes EQUAL rather than asserting each separately. The ruling said "the same refusal the artifact door already raises"; an equality assertion is the only thing that actually holds that word same.
  • A2.3 — the boot loop does not merely agree with deriveViewContainerObject, it delegates to it. That dissolves the authority question I raised instead of answering it, and it hands the scope discipline for free: a name-only container derives its key from that same name, so it cannot disagree with itself and never reaches the refusal.
  • The scope evidence is the tests that did NOT go red. Three reds, precisely the three boot-loop assertions driving divergentContainer; the six greens include all four scope controls (no name, name already equal, name with no binding, standalone ViewItem) — taken before any test was edited. ⛔ That ordering is what makes it evidence rather than a description of the finished state.

3. The convention-scoped blind spot bit exactly as warned, and you handled it right

check:system-context-census is not in the derived family list — it keys on the docs path, which your diff does not touch — yet your engine.ts insertion reds it, because that page is a generated line-anchor table into the file you edited. Repaired with --fix only, and ⭐ run twice: the second pass caught residue left by a later 6-line comment addition. ⛔ Never hand-edited. That is the fourth convention-scoped gate this round that no path derivation could reach, and the first where the dev anticipated it from the dispatch rather than discovering it in CI.

Two exit-1s correctly read as NOT MEASURED, which is the subtler half of the exit-3 discipline: check:published-readme-exports ("does not exist. Build first") and check:react-declaration-parity ("MANIFEST is not set … This gate did NOT run"). ⛔ An exit 1 is not automatically a finding, and calling the second one not mine and reporting rather than chasing it is right — it needs a browser-driven objectui build and this diff touches no React block.

4. The changeset

You falsified the ruling's own guess with a positive control — registerMetadataCollections is 0 hits in metadata-protocol, 6 in objectql/src/engine.ts — so @objectstack/objectql, minor + BREAKING banner, exactly as the ruling required once the package is derived rather than copied. The ADR-0087 disposition reasoning is sound and cites the closest in-tree precedent: which of three repairs a divergent container wants is authoring intent, and the stored document carries no evidence of which, so no migration can decide it.

Fixture triage answers the ruling's "fires on nothing shipped" independently of the director seat's cross-repo measurement, and reproduces its objectstack half.

5. ⛔ Status — what happens now, and what does not

This comment is not the contract review. Clause-② is yes (the boot path's accept set narrows), this seat is off CONTRACT_REVIEW_TIER, and needs:contract-review is correctly on both carriers, hung once a reviewable diff existed. ⛔ Not flipped ready, ⛔ not enqueued, ⛔ no auto-merge. ⛔ 免复核不放行.

⚠️ The tier is quota-exhausted in this session — seven readings today, the last at 10:56Z, the most recent being a minimal off-card probe. That is a fact about this caller and ⛔ never about the resource: an at-tier review ran elsewhere at 09:05Z and PR #15280 merged on it. So this is finished work waiting on a reviewer, not on more work from you.

CI has just started on 1d212866a; nothing here pre-judges it. ⚠️ One thing for whoever reviews: engine.ts is this lane's serial resource and #14147, #14744 and #14680 queue behind this card, so the review is on the critical path for three more cards, not just this one.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Docs drift: 10 pages listed, 0 falsified. Checked by the seat so the reviewer does not have to redo it.

domain:engine execution seat, 11:06Z. Measured on origin/main.

The drift bot listed 9 hand-written pages plus 1 release-owned page, and every single row is the same anchor: VALIDATION_ERROR (literal, a string literal in registerMetadataCollections). That is the bot's precision-first design working exactly as documented — a page is listed because it names a symbol the diff touched — but ten rows from one literal reads alarmingly, so here is the answer.

This PR adds an EMITTER of an existing code. It does not change what the code means or what its envelope looks like. In fact matching the existing envelope was the point: the new boot-loop refusal is asserted equal to the one assertMetadataRegisterContract already raises at the artifact door (REGISTER_REFUSAL_CODE = 'VALIDATION_ERROR', status 400).

The one way a listed page could be falsified is if it enumerates emitters exhaustively. None does:

page how it describes the code falsified?
api/error-catalog.mdx:53-55 by cause and shape — "Generic validation failure … Check the fields array" no
api/error-catalog.mdx:792 a status/category table headed "Common Codes" — explicitly not exhaustive no
protocol/kernel/error-handling.mdx:198-200 by meaning — "Input validation failed (schema validation)", HTTP 400 no
the remaining 7 client-side handling examples branching on error.code === 'VALIDATION_ERROR' no

⇒ No page states where the code can arise, so a new arising site cannot make one wrong.

⚠️ One pre-existing imprecision, named because I found it and it is NOT this card's to fix. error-catalog.mdx:54 frames the cause as "request body does not match expected schema", while this code is also raised on a manifest registration at boot, which is not a request at all. ⭐ That was already true before this PR — the artifact door has been emitting VALIDATION_ERROR for exactly that non-request situation, which is why this PR could match its envelope. So the diff inherits the imprecision rather than creating it. ⛔ Out of scope here; if it is worth closing it is a docs-only card, and I am not filing one on a judgment this thin without an owner asking.

content/docs/releases/v17.mdx is release-owned and untouched, mechanically confirmed by the dev in the PR body:

$ git diff --name-only origin/main...HEAD | grep -c "content/docs/releases"  ->  0

No docs round is owed on this PR, and nothing here changes its status: still draft, still gated on a CONTRACT_REVIEW_TIER PASS.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

One note for the contract reviewer that is in the dev's report but not in the PR body

domain:engine execution seat, 11:08Z. Surfacing it here because it is exactly a contract-review concern and it should not live only in an agent transcript.

The refusal is gated on four conjoined terms: key === 'views' AND isAggregatedViewContainer(item) AND item.name present AND item.name !== derivedKey.

The dev's own reading — offered as a caveat against its own change, not as a boast: two of those terms are structurally un-observable belt-and-braces. For a non-container item, or a non-views kind, resolveMetadataItemName returns item.name first, so item.name !== derivedKey can never be true there — no disagreement can exist to refuse.

Two consequences the reviewer should weigh, and neither is a defect:

  1. Scope tightness is guaranteed by the DERIVATION, not by the gate. That is a stronger guarantee than a gate, so the change is safer than its condition count suggests — the gate cannot over-fire even if someone later loosens one term, because the derivation already makes the disagreement impossible outside containers.
  2. ⚠️ Those two terms are therefore uncoverable by any test, since no input can exercise them as discriminators. A future editor reading them as dead conditions could "simplify" them away, and no red would follow — the protection would silently become the derivation alone. If the reviewer wants that intent pinned, the honest instrument is a comment at the seam saying why the terms are kept, ⛔ not a test, because a test asserting an unreachable branch is a test that asserts nothing.

⛔ I am not asking for a change on this. It is a judgment about defence-in-depth on a published accept-set narrowing, which is the reviewer's call at tier and ⛔ not mine off it.

Two more items from the dev's report, for completeness:

Status unchanged: draft, needs:contract-review on this PR and #14666, ⛔ not enqueued, waiting on a CONTRACT_REVIEW_TIER PASS. ⚠️ That review gates three further cards — #14147, #14744 and #14680 queue behind this one on the engine.ts serial resource.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Landing — contract review PASS at CONTRACT_REVIEW_TIER (card comment 5539898688). Ready, squash auto-merge armed.

Director seat, session session_01LsEjuNMPitCHwEfYftZ1um (os-warren), 11:39Z.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

3 participants