Skip to content

fix(objectql,cli): os validate refuses a view container whose name disagrees with its object, as boot does (#20331) - #20391

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20331-validate-view-container-name
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20331-validate-view-container-name

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20331
Clause-②: yes

os validate now runs the same view-container name check the boot registrar runs, and reports the refusal in the boot registrar's own words. Before this change it passed a stack at exit 0, and os serve then refused that same stack at boot. The triage direction was "one judge, not a second rule". The check moved out of ObjectQL.registerMetadataCollections into one function, and boot and os validate both call it. There is no lint twin and no new error code, and boot's message and envelope are unchanged.

Premise, measured on origin/main 862b6ce86 before any edit

The setup: the CLI's dependency closure was built, then os init my-app -t app --no-install, os g object order_line and os g view order_line. The view's name was then hand-edited to 'order_line', bound to object my_app_order_line.

step exit what it printed
os validate 0 ✓ Validation passed, UI: 1 Views
os serve --dev 1 "Invalid views: container from manifest 'com.example.my-app': the container's own name is 'order_line', which disagrees with the object key it binds to, 'my_app_order_line' …"
os compile 0 writes an artifact carrying { name: 'order_line', object: 'my_app_order_line' }
os serve, booting that artifact (dist/objectstack.json, no config) 1 the same message

What changed

  1. @objectstack/objectql: viewContainerNameRefusal(container, sourceLabel, ownerId) (new src/view-container-name-refusal.ts) returns the refusal the boot registrar throws, or undefined. It carries the same gate: the container branch (isAggregatedViewContainer), and a name that is present and differs from the derived key. It also carries boot's precondition for that gate: a falsy derived key is boot's warn-and-skip, and the function answers undefined for it. It uses the same message and the same VALIDATION_ERROR / 400 envelope. It derives the key itself with deriveViewContainerObject, which for a container is exactly what resolveMetadataItemName returns. So no caller re-spells "which derivation boot uses".
  2. registerMetadataCollections keeps its key === 'views' narrowing and throws what the function returns. Nothing else in engine.ts moved.
  3. A new public export from the @objectstack/objectql root entry: viewContainerNameRefusal and its type ViewContainerNameRefusal. This is the widening behind Clause-②: yes: @objectstack/objectql is graded minor, and @objectstack/cli stays patch. The changeset states it.
  4. @objectstack/cli: findViewContainerNameRefusals(parsed) (new src/utils/view-container-names.ts) owns the WALK only, and the verdict is objectql's. It walks what the load path registers. With no packages[], that is the top-level views, under the manifest id: AppPlugin hands { ...manifest, ...stack } to the manifest service, and the id is read through artifactPackageId. With packages[], it is each packages[i].manifest.views, under that package's id, and not the top level, which resolveArtifactPackageOrder never registers.
  5. os validate step 2c runs right after the schema parse. It exits 1. The text face prints the refusals as bullets. --json emits errors: [{ path, code, httpStatus, message }] and carries warnings / conversions like every other exit.
  6. test/validate-build-gate-parity.test.ts gains a VALIDATE_ONLY_GATES ledger. The closed roster had no honest place for a gate wired into validate.ts alone: SHARED_NON_REGISTRY_GATES asserts that both commands call it, and a NOT_A_GATE row would be a false statement. The new row is pruned both ways: it goes stale if validate.ts stops calling the gate, and it must move to SHARED_NON_REGISTRY_GATES if compile.ts starts calling it.

After the fix, on the same project (CLI from source, @objectstack/objectql dist rebuilt)

  • os validate exits 1 with ✗ The server would refuse this stack at boot (1 view container) and the boot message. os validate --json exits 1 with errors[0] = { path: 'views[0]', code: 'VALIDATION_ERROR', httpStatus: 400, message }. The message is byte-equal to the line os serve --dev printed before the fix (diff is empty).
  • Controls: name: 'my_app_order_line' gives exit 0, and deleting name gives exit 0.
  • os serve --dev still exits 1. Its message is byte-identical before and after the refactor (diff of the two boot logs is empty).

Tests

  • packages/objectql/src/view-container-name-refusal.test.ts (new, 9 tests, with the falsy-derived-key control added in patch round 1): the refusal and its envelope. The manifest seam AND the nested-plugin seam throw exactly what the function returns. There are five controls (no name, a matching name, a name-only container, a non-container, an empty-string name), each also registered by boot.
  • packages/cli/src/utils/view-container-names.test.ts (new, unit tier, 5 tests): the walk. Every row's message equals the judge's answer and is never re-spelled. The packages[] bodies are judged under their own ids, and the top level is not judged when packages[] is present.
  • packages/cli/test/validate-view-container-name.test.ts (new, integration tier: it spawns the CLI and constructs ObjectQL, 5 tests): --json exits 1 and errors[0].message equals what ObjectQL.registerApp throws for the same stack. The text face exits 1 with the same words. The matching control and the no-name control both exit 0. A premise case asserts that boot refuses the divergent stack and accepts both controls.
  • The existing boot pins in view-container-divergent-name-registrars.test.ts are unchanged and green.
  • Runs at 4e15d3cea:
    • @objectstack/objectql: vitest run --project local, four shards: 323 files, 5865 tests passed. Typecheck (tsc --noEmit ×2 plus check:test-typecheck) exit 0.
    • @objectstack/cli: vitest run --project unit, two shards: 232 files, 3315 tests passed. Typecheck exit 0. --project integration was run locally for the new file only (5/5); the rest of the integration tier is declared to CI.
    • tsc --listFilesOnly confirms every new file is in a typecheck program (tsconfig.json / tsconfig.test.json of each package).
    • pnpm lint (full eslint . --no-inline-config) exit 0.

Ablation (fix committed first; mutation through scripts/ablation-replace.mjs)

Gates at 4e15d3cea

These were derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, which gave 69 commands. --ran reconciled them as 68 run, 1 NOT-MEASURED and 0 UNRUN. Every command exited 0 except the one below:

pnpm check:type-check-debt -> 3   NOT MEASURED (PREREQUISITE NOT MET)

check:type-check-debt --re-measure runs a whole-workspace turbo run build inside itself. On this shared box that build did not fit the foreground cap: the first attempt was SIGTERMed mid-build, and I rebuilt packages/spec afterwards. Neither touched package carries a DEBT / TEST_DEBT row. lint.yml runs it in CI.

Six gates first answered "prerequisite not met" or asked for deeper history, and each was re-run after its prerequisite was met:

  • check:dual-build-cjs-loads, check:i18n, check:i18n-coverage and check:i18n-walk-parity exited 3 until the CLI, the examples and the unbuilt packages were built, then 0.
  • check-engine-split-ratio.mjs --days 90 exited 2 until the history was deepened with --shallow-since, then 0.
  • check-issue-citations.mjs exited 2 on four added citations that no longer resolve. Those were rephrased, and it then exited 0 with and without --base origin/main.

Patch round 1 (head 6b5895b97, after the FAIL review of 4e15d3cea)

  1. Two red CI pins, and every fixture of the same shape. test/union-fold-command-parity.test.ts (PEDIGREE CONTROL) and test/validate-per-package-authoring-parity.test.ts asserted exit 0 on stacks the boot registrar refuses: each carried containers named *_list bound to *_account / *_order. Each container now names its bound object, and each pin's assertion is unchanged.
    • Census of packages/cli/test/, packages/cli/src/, packages/qa/ and examples/ (all 963 tracked .ts/.js/.json files, the .e2e tier included). A structural scan read every object literal carrying name plus a container arm (list/form/listViews/formViews) and no viewKind, and derived the key as boot does. It found 15 divergent containers in 9 files, all fixed the same way: the 4 above, build-text-face-advisory-count.test.ts (2), format-zod-union.test.ts (1), info-detail-package-fold.test.ts (1), lint-handwritten-checks-package-fold.test.ts (1), lint-label-case-localized.test.ts (2), lint-per-package-authoring-parity.test.ts (2), lint-per-package-authoring-seam.test.ts (2). A rescan finds none, apart from the docblock examples in this PR's own files. examples/: os validate exits 0 on each of app-crm, app-multi-package, app-showcase and app-todo, with 0 container refusals. packages/qa/: no hit. views is not a map-form collection (MAP_SUPPORTED_FIELDS excludes it), so no key-injected name can hide there.
  2. Semver: @objectstack/objectql is now minor with Clause-②: yes, for the two new root exports (viewContainerNameRefusal, ViewContainerNameRefusal). @objectstack/cli stays patch. The changeset states the widening.
  3. Boot's precondition moved with the gate. registerMetadataCollections warns about and skips an entry whose derived key is falsy before the name check runs. viewContainerNameRefusal now returns undefined for that entry too. The derivation keeps '' for list: { data: { object: '' } }, so without this os validate would refuse a container that boot only skips. A new control pins it: boot throws nothing and registers nothing, and the function returns undefined. The docblock and the engine.ts comment now say what moved: the message and the envelope moved byte for byte, and the gate moved whole, precondition included.
  4. The gate-parity remedy text names VALIDATE_ONLY_GATES too.

Runs at 6b5895b97:

  • @objectstack/objectql: --project local in 4 shards, 325 files / 6017 tests passed. Typecheck exit 0. The new test file is 9/9.
  • @objectstack/cli: --project unit in 2 shards, 233 files / 3335 tests passed. --project integration IN FULL in 3 shards, 61 files / 512 tests passed and 1 skipped. Typecheck exit 0.
  • OS_TEST_TIERS=nightly: the census touched no .e2e file, so the 13 nightly files that run os validate were run instead. 13 files / 129 tests passed.
  • pnpm lint exit 0. check-issue-citations --base origin/main exit 0.
  • check-changeset-no-major level axis: driven with --event on this body with Clause-②: yes, it answers "✓ LEVEL AXIS … @objectstack/objectql: minor" (exit 0).
  • Gates: 69 derived, --ran reconciles 69 run, 0 NOT-MEASURED, 0 UNRUN, every one exit 0. That includes check:type-check-debt: 4 entries re-measured, none above its number.
  • Guard ablation: scripts/ablation-replace.mjs --delete removed the if (!itemName) return undefined; line (anchor x1 to x0, blob 21af64e5 to 56d25d71). Exactly the new control went red: the function returned "Invalid views: container … binds to, ''" where undefined was expected. The other 20 tests stayed green, the existing boot pins among them. The restore gave a blob equal to HEAD 21af64e5 and an empty git diff HEAD, then 21/21 green.

Acceptance notes

  • os compile / os build does NOT reach the shared function. It is not the same code path as validate, and under this card's scope I did not widen to it. It still exits 0 on this stack and writes an artifact that os serve refuses when it boots it, as measured above. The VALIDATE_ONLY_GATES row records that gap. I reported it to the seat as an out-of-scope finding.
  • The artifact/HMR loader is not a second spelling of this function. MetadataPlugin._parseAndRegisterArtifact (packages/metadata/src/plugin.ts, the container branch around :1103-:1121) registers the container under deriveViewContainerObject(item) through this.manager.register('view', viewObject, item). The refusal there comes from the GENERIC register contract assertMetadataRegisterContract (packages/core/src/metadata-service-contract.ts, MetadataFacade answers three register → get round-trip cases differently from every other shipped IMetadataService #7378 row 1), in its own words ("IMetadataService.register('view', …): data.name is …"). So both registrars enforce the same rule through different mechanisms, and boot's prose is its own on purpose. packages/metadata/** is untouched.
  • Edge, not measured at a public door (read-only inference): ViewSchema.name is z.string().optional(), so an empty-string container name is spec-valid. The boot loop (and os validate, which mirrors it) treats '' as absent and registers the container. The generic register contract at the artifact door refuses it, because '' !== undefined and '' !== key. No producer of name: '' was found.
  • Bound: a nested plugins[] entry's views is registered by boot under the label nested plugin, but os validate does not walk it. The stack schema types plugins as unknown[], and in an authored config they are runtime plugin instances.
  • Upstream: origin/main was merged at b1cbd9277, and again at 0d7ed5a37 in patch round 1. It has since moved 6 commits. One of them, fix(objectql)!: having resolves placeholders through the where resolver, and the per-aggregation filter refusals name aggregations[i].filter (#20334) #20368, touches engine.ts in the engine.aggregate hunks only, and a git merge-tree probe merges cleanly, so I did not merge again.
  • packages/spec/** and packages/metadata/** are unchanged. The diff adds no lint rule and no error code, and it does not change boot's message or envelope.

Generated by Claude Code

…er name check (#20331)

The divergent view-container `name` refusal moves out of
`ObjectQL.registerMetadataCollections` into `viewContainerNameRefusal`
(`@objectstack/objectql`), unchanged: the boot loop throws what it
returns. `os validate` calls the same function over the views the load
path registers and reports the refusal in the boot registrar's words,
exiting 1 on a stack it used to pass while `os serve` refused it.

Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 28, 2026
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/objectql, touching 11 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/objectql/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.

16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json ab6fb02763e1d73e7045741717a07d1294e4e83c.

⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/objectql/src/index.ts) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: os validate (command, 51 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 37 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 ab6fb02763e1d73e7045741717a07d1294e4e83c → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json ab6fb02763e1d73e7045741717a07d1294e4e83c

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4e15d3cea36793e6b2aeb3b9f1164fc6fd409d24

① Derived judgments

  • Moved check is the same judgment as boot's — mostly correct, one gap. packages/objectql/src/view-container-name-refusal.ts viewContainerNameRefusal carries the isAggregatedViewContainer gate, the typeof name === 'string' && name gate, the message string and the VALIDATION_ERROR/status 400/httpStatus 400 envelope byte-identical to the block removed from engine.ts (diff origin/main... head, registerMetadataCollections :6559–:6580). Key derivation is identical: resolveMetadataItemName (engine.ts :2731) returns deriveViewContainerObject(item) for exactly the isAggregatedViewContainer branch, and with a non-empty name that helper never returns undefined (packages/metadata/src/view-container.ts :77–:83). Gap: boot's loop skips an item whose itemName is falsy BEFORE the check (if (!itemName) { warn; continue }, engine.ts :6532–:6535); the function has no such guard. With views: [{ name: 'x', list: { data: { provider: 'object', object: '' }, … } }] and no top-level object, deriveViewContainerObject yields '' (the ?? chain does not skip an empty string), boot warns and registers nothing, but the function returns a refusal ("binds to, ''"). ListView.data.object is z.string() (packages/spec/src/ui/view.zod.ts :177), so the parse at validate step 2 accepts it and step 2c exits 1 claiming "The server would refuse this stack at boot" — validate refusing what boot accepts. Boot's own behaviour is unchanged (its guard still runs first). Wrong on the "cannot disagree" claim; one-line fix (if (!itemName) return undefined).
  • Boot throws exactly what the function returns — correct. engine.ts :6577–:6580 if (key === 'views') { const refusal = viewContainerNameRefusal(item, sourceLabel, ownerId); if (refusal) throw refusal; }. Pinned at both seams in view-container-name-refusal.test.ts ("the manifest seam throws exactly…", "…nested-plugin seam") against a real new ObjectQL().registerApp.
  • validate derives the same key, label and owner as boot — correct for the reachable set. packages/cli/src/utils/view-container-names.ts findViewContainerNameRefusals: no packages → payload { ...manifest, ...parsed } (same as AppPlugin :390–:392 { ...this.bundle.manifest, ...this.bundle }), owner artifactPackageId(payload) = id || name (same rule registerApp :6230 uses; manifest.id is regex-required non-empty, kernel/manifest.zod.ts :363, so the ''-vs-undefined edge is unreachable); packages[] → each packages[i].manifest.views under artifactPackages(parsed).id (body.id non-empty else body.name; both required strings on AssembledPackageBodySchema = ManifestSchema.extend, so it equals boot's body.id || body.name); label 'manifest' for both, matching registerApp :6347 for every body resolveArtifactPackageOrder returns. Top level skipped when packages present, matching the resolver (core/artifact-packages.ts :203–:217). packages: [] → nothing judged on both sides.
  • Same set of views as boot — not fully. Boot also runs registerMetadataCollections(plugin, id, 'nested plugin') for every object entry of manifest.plugins (engine.ts :6380–:6387, :6480); bundle.plugins rides into the payload, so a plain-object plugin carrying views is judged at boot and not by validate. The PR states this bound explicitly ("Bound"); the stack schema types plugins as z.array(z.unknown()) (stack.zod.ts :751). Named, not hidden; acceptable under the card.
  • New public export — necessary, and in principle passes the rules. The CLI already imports values from the @objectstack/objectql root (utils/authoring-filter-judge.ts :47 ObjectQL, utils/secret-reference-union.ts :95, commands/lint.ts :8), so the . entry is the established seam; ./core is the lean entry and packages/metadata/** is read-only under the claim. tsup.config.ts builds src/index.ts with dts, files: ['dist', …], and the symbol is reachable from dist/index.d.ts, so check:dts-closure/check:published-files hold by construction (dev-reported 0 for both; CI Type Check · workspace green).
  • Pins — correct. Integration test/validate-view-container-name.test.ts "THE PIN" compares errors[0].message with bootRefusal() = a real new ObjectQL().registerApp({ ...manifest, ...stack }) throw, not a literal. Controls: stack(OBJECT) (matching) and stack(undefined) (absent) both pinned exit 0; unit and objectql controls likewise. Ablation description holds by reading: an early return undefined reds the 3 refusal cases + the 2 boot pins (boot throws only what the function returns) and the 6 CLI positives, and leaves every control green. Existing boot pin file blob unchanged (ecda23a0 on both refs).
  • VALIDATE_ONLY_GATES row — correct classification. The file's contract is "validate is the read-only SUPERSET of build" (header :8–:10); a validate-only gate is inside that direction, and SHARED_NON_REGISTRY_GATES would assert a call compile.ts does not make while NOT_A_GATE would be false. compile.ts has no reference to either symbol (grep on head). The both-ways prune test (:1029–:1040) keeps the row honest. Minor prose drift: the unclassified-name remedy text (:836–:841) still lists only three ledgers. Gap reach as the PR measured it: os compile exits 0 and writes dist/objectstack.json carrying { name: 'order_line', object: 'my_app_order_line' }; os serve booting that artifact exits 1 with the same refusal (class a, public door) — reported in the PR's out-of-scope findings; not re-measured here.
  • Comments and changeset prose — one imprecision. The changeset's os build sentence is true by reading (no compile.ts call). The engine comment "Moved there unchanged" and the module docblock "moved here, byte for byte" are true of the string and envelope but not of the gate: the truthy-itemName precondition stayed at the call site only (see bullet 1).

② Semver level

Wrong as declared. @objectstack/objectql must be minor with Clause-②: yes, not patch / no. The rules: lanes/spec.md :17 names 「公开导出面增删」 as a contract-surface shape and :21 says a widening 「不论多小,即条款②」; execution-duties.md :96 scopes Clause-② to the published contract surface, which contract-review.md :10 measures by the package exports map — and viewContainerNameRefusal + ViewContainerNameRefusal land on the . entry of @objectstack/objectql. AGENTS.md Post-Task step 3: "yes takes at least minor". Landed precedent PR #20236 (#20193) is the same shape (a gate moved unchanged into its own module and exported from the . entry so a second door calls the one judge): '@objectstack/rest': minor, Clause-②: yes, consumer '@objectstack/runtime': patch. The dev's own open_questions recommends this (option B). @objectstack/cli patch is right: os validate exiting 1 on a stack the runtime already refuses is 「拉回已声明契约不触它」 (execution-duties.md :98), the same reading as #20236's runtime patch and #20329's Clause-②: no. Both the PR body line and the changeset line must flip to yes; check-changeset-no-major's level axis then requires the objectql minor.

③ Boundary flags

  • Files outside claim 5863262426: none. All 10 files fall under its bullets: engine.ts (only the block + one import), view-container-name-refusal.ts (the function's home under packages/objectql/src/), index.ts (two export lines — value and type — against the claim's "one export line"; trivial), validate.ts and utils/view-container-names.ts (the bullet names either), tests in both packages including utils/view-container-names.test.ts and test/validate-build-gate-parity.test.ts, one .changeset/20331-*.md. packages/spec/** and packages/metadata/** untouched (diff stat). No lint rule, no new error code.
  • PR body vs diff: "Nothing else in engine.ts moved" — true. "Existing boot pins unchanged and green" — blob-identical; green in CI. Test counts 8/5/5 — match. "--project integration was run locally for the new file only; the rest of the integration tier is declared to CI" — true, and that tier is red (next bullet). "Upstream moved 3 commits, none touching this diff" — merge-base b1cbd9277, base 15bf186f; not independently verified.
  • CI on head (34 check-runs, read last; all now completed): RED: Test Core (4/6) and the aggregate Test Core. The job is packages/cli pnpm run test --shard=1/2: 2 failed / 2015 passed — test/union-fold-command-parity.test.ts › PEDIGREE CONTROL and test/validate-per-package-authoring-parity.test.ts › os validate reports every per-package advisory os build does. Both fixtures carry packages[0].manifest.views = { name: 'pp_account_list', object: 'pp_account', list } / { name: 'ob_account_list', object: 'ob_account', list } (and _order_list), which the boot registrar refuses by the same reading (isAggregatedViewContainer true, derived key ≠ name); the two pins were asserting exit 0 on a stack boot refuses — the card's own defect shape — and the PR did not update them. Test Core (4/6) is green on base 15bf186f, so the red is PR-caused. In progress at first read, green on re-read: Test Core (1/6), Type Check · workspace. examples/app-multi-package itself declares no views, so no example is affected.

Implemented-by: claude/issue-20331-validate-view-container-name
Reviewed-by: session_01UYBdGBzWSrAMzpW8ah3GbP

Independence: INDEPENDENT AGENT (fed the card, the triage direction and the PR only; not the dispatch order or the seat's conclusions)

VERDICT: FAIL

  • Fix the two red CLI integration pins so their fixtures are stacks boot accepts: in packages/cli/test/union-fold-command-parity.test.ts (:208, :212) and packages/cli/test/validate-per-package-authoring-parity.test.ts (:52, :56) drop the container name or set it to the bound object (ob_account/ob_order, pp_account/pp_order); run the CLI integration tier; CI Test Core (4/6) must be green on the new head.
  • Re-grade the changeset to '@objectstack/objectql': minor and flip Clause-②: no to Clause-②: yes in both the changeset body and the PR body, stating the widening (the two new .-entry exports); keep '@objectstack/cli': patch.
  • Carry boot's precondition into the shared judge: in viewContainerNameRefusal return undefined when the derived key is falsy (if (!itemName) return undefined), so validate cannot refuse a list.data.object: '' / form.data.object: '' container that boot warns on and skips; add the control to view-container-name-refusal.test.ts (boot registers nothing and throws nothing; the function returns undefined).

Generated by Claude Code

…ontainer judge; fixtures boot accepts

viewContainerNameRefusal now answers undefined for a container whose
derived object key is falsy, the entry registerMetadataCollections warns
about and skips before the name check, so os validate cannot refuse what
boot skips. Pinned by a control that boot registers nothing and throws
nothing.

Every CLI test fixture whose views: container carried a name other than
its bound object (two red integration pins among them) now names the
object, so each is a stack the server loads. The changeset grades
@objectstack/objectql minor with Clause-② yes for its two new root
exports, and the gate-parity remedy text names VALIDATE_ONLY_GATES.

Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6b5895b978310cf4e11f437cd6947e49d38b4760
Local-runs: none

Delta review of patch round 1 (PR #20391, card #20331). Round-0 record 5864600436 (FAIL at 4e15d3ce) and the seat's REWORK 5864616314 named three required changes and one nit. Judged here: the whole net diff main...6b5895b9 (19 files, +844/-69; the compare file list equals the PR file list), card #20331's body and all six comments, the PR body as edited by the seat at 11:4x UTC today, and the 41 check-runs on the head (newest run per name read). Read-only: no build, no test, no gate re-run; the only local activity was text reads (git grep / git show of origin/main fixture files in the checkout, and the head's job logs through the REST API).

① Derived judgments

Round-0 findings, each resolved:

  • Finding 1 (two red CLI integration pins) — RESOLVED. test/union-fold-command-parity.test.ts (:212, :216) and test/validate-per-package-authoring-parity.test.ts (:170, :174) now carry name: 'ob_account' / 'ob_order' and 'pp_account' / 'pp_order', each equal to the bound object; the diff touches the name lines plus a four-line comment only, so each pin's own assertion is unchanged. On the head, Test Core (4/6) (job 108813696682, @objectstack/cli 1/2) is green, and its log shows union-fold-command-parity.test.ts (9 tests) and validate-per-package-authoring-parity.test.ts (4 tests) passing, next to the new validate-view-container-name.test.ts (5 tests). The aggregate Test Core is green. The REWORK's census instruction was executed: 15 containers in 9 files (the 4 above, build-text-face-advisory-count 2, format-zod-union 1, info-detail-package-fold 1, lint-handwritten-checks-package-fold 1, lint-label-case-localized 2, lint-per-package-authoring-parity 2, lint-per-package-authoring-seam 2), all set to the bound object, none dropped, no assertion edited — the diff counts match the PR body's list exactly. My own read-only probe of origin/main (name and object on nearby lines beside a list/form/listViews/formViews arm, over packages/cli/test, packages/cli/src, packages/qa, examples) finds no divergent container outside the nine files; the only near hits are non-containers (doctor-refs.test.ts :98 has no arm; the i18n-* hits are pages, not views). Bound of that probe, same as the dev's: a container assembled by a function or variable is not seen by a text scan; the unit and integration tiers cover those behaviourally, and both are green on the head.
  • Finding 2 (semver grade) — RESOLVED. .changeset/20331-validate-view-container-name.md now grades '@objectstack/objectql': minor, '@objectstack/cli': patch, carries Clause-②: yes, and states the widening in its own paragraph (the two root-entry exports, what the function returns, and that the boot registrar now calls it). The PR body's line 2 reads Clause-②: yes. The newest Check Changeset run (108911405077, 11:43Z, after the body edit) is green and its log prints ✓ LEVEL AXIS: this PR declares clause-② yes ... @objectstack/objectql: minor ... @objectstack/cli: patch. Judged in ② below.
  • Finding 3 (boot's falsy-key precondition) — RESOLVED. view-container-name-refusal.ts :99 if (!itemName) return undefined; sits after the derivation and before the equality test, so the function answers undefined for exactly the entry boot's loop skips (engine.ts :6532 if (!itemName) { warn; continue }). The two doors are now equivalent on the refusal set by reading: boot reaches the function only with a truthy itemName from resolveMetadataItemName, which for the container branch IS deriveViewContainerObject(item) (engine.ts :2731), the same call the function makes; a non-container returns undefined on both sides; a truthy key with a non-empty differing name refuses on both sides. The new control (view-container-name-refusal.test.ts :122-:152) pins the premise (deriveViewContainerObject(unbound) === '', because the ?? chain keeps list.data.object: ''), then boot throwing nothing and registry.listItems('view') empty, then the function returning undefined. The docblock now says "the message and the envelope moved byte for byte. The GATE moved whole, precondition included" and the engine.ts comment says the !itemName skip is the loop's precondition and the function carries it — both true of the head.
  • Nit (remedy text) — carried. validate-build-gate-parity.test.ts :838-:842 names VALIDATE_ONLY_GATES beside the other three ledgers and says "all four".

The whole net diff, accept-set and public-surface changes named:

  • @objectstack/objectql public surface — WIDENED, correctly declared. Two new exports on the . entry (src/index.ts :112-:113; exports['.'] maps to dist/index.*, and ./core is untouched): viewContainerNameRefusal(container, sourceLabel, ownerId) and the type ViewContainerNameRefusal. This is the sole export-map-reachable addition in the diff; @objectstack/cli's new src/utils/view-container-names.ts is not re-exported from any of the CLI's published entries (src/index.ts, src/console.ts, src/hook-body.ts carry no reference), so the level-axis gate's named residual (a second widening graded patch) does not occur here.
  • Boot's accept set — UNCHANGED, correct. registerMetadataCollections keeps its key === 'views' narrowing and its !itemName skip, and throws what the function returns (engine.ts :6581-:6583). The message string and the VALIDATION_ERROR / status 400 / httpStatus 400 envelope in the new module are byte-identical to the block the diff removes from engine.ts. Nothing else in engine.ts moved (one import, one comment rewrite, the block). The two existing boot pins in view-container-divergent-name-registrars.test.ts are not in the file list, so they are unchanged by construction, and the objectql suite is green on the head. Both seams are pinned against a real new ObjectQL().registerApp (manifest and nested-plugin), so "boot throws exactly what the function returns" is measured, not asserted from prose.
  • os validate accept set — NARROWED to the runtime's, correct. Step 2c (validate.ts :335-:379) runs after the schema parse over result.data, walks what the load path registers, and exits 1 on any refusal. The walk (utils/view-container-names.ts): no packages key → the top-level views under artifactPackageId({ ...manifest, ...parsed }), the spread AppPlugin hands the manifest service; packages[] present → each packages[i].manifest.views under that body's id via the CLI's existing artifactPackages reader (body.id, else body.name; both required on an assembled body), and the top level not at all, which matches resolveArtifactPackageOrder (core/artifact-packages.ts :215 returns the caller's object only when packages === undefined). The label is 'manifest' for every body, matching registerApp :6347. That the walk reaches parsed packages[] bodies in the real CLI was demonstrated by round 0 itself: validate-per-package-authoring-parity went red on a packages[] fixture. The stated bound stands and is acceptable under the card: a nested plugins[] entry's views is registered at boot under nested plugin and not walked here (plugins is unknown[] on the stack schema).
  • os validate --json failure payload — one new exit, one new row shape, documented. The new exit emits errors: [{ path, code, httpStatus, message }] with warnings and conversions carried as every other exit does. errors is already exit-specific on this command (Zod issues at :304, rule errors at :452, per-package errors at :533, { token, message } at :616, doc errors at :653), so a new row shape at a new exit is inside the command's existing convention; the changeset states the row (path, code: 'VALIDATION_ERROR', httpStatus: 400, message) and the packages[1].manifest.views[0] path form, which matches ${at}[${index}] in the code. The text face prints the boot message verbatim as a bullet list. Integration pin "THE PIN" compares errors[0].message with a real registerApp throw, not a literal; the text-face pin does the same; the matching and no-name controls exit 0.
  • VALIDATE_ONLY_GATES — correct classification, kept as ruled. compile.ts on the head has zero references to findViewContainerNameRefusals or viewContainerNameRefusal, so the row's second claim holds; the both-ways prune test (:1030-:1041) keeps it honest. The changeset's "Not changed: os build does not run this check" sentence is retained per the REWORK.
  • No lint twin, no new error code, packages/spec/** and packages/metadata/** untouched — all true by the file list and the diff (VALIDATION_ERROR is the existing code).

② Semver level

Correct as declared. '@objectstack/objectql': minor with Clause-②: yes in both the changeset and the PR body (line 2, Clause-②: yes), for the two new .-entry exports — the widening lanes/spec.md names (公开导出面增删, 不论多小即条款②) and contract-review.md measures by the exports map, on the lane's precedent PR #20236 (#20193). '@objectstack/cli': patch is right: os validate refusing a stack the runtime already refuses at boot is 拉回已声明契约 (execution-duties.md, 条款②只指已发布契约面), and the CLI publishes no new export. The Clause-②: line therefore reads yes, and the newest Check Changeset run on the head confirms the level axis on that declaration. Round 0's ② finding is closed.

③ Boundary flags

  • Files outside claim 5863262426 (as amended, taken over by 5869087302 on the same surface): none. All 19 files fall under its bullets: engine.ts (the block, one import, one comment), the function's home and its test under packages/objectql/src/, two export lines in packages/objectql/src/index.ts, validate.ts plus the utils/ walker and its unit test, tests under packages/cli/test/ (the new integration pin, the parity ledger, and the nine census fixtures — "tests in packages/cli", fixed under the REWORK's explicit census instruction), one .changeset/20331-*.md. Read-only areas untouched.
  • Dev deviations (report 5866334331), each answered: (a) worktree re-added and origin/main merged at 0d7ed5a3 with git's default merge message and no trailer pair — a merge commit, squashed on landing; not a blocker. (b) nightly leg ran the 13 .e2e files that spawn os validate because the census touched no nightly file — reasonable; my read of origin/main finds 12 such files by a narrower pattern, so the dev's count is not under. (c) seven more fixtures than the two named were fixed — inside the claim's test surface and inside the REWORK's instruction; each is a stack boot refuses, so the fix is the same shape. (d) classifier outage mid-round — process note; nothing unrecorded. Round-0 deviations already accepted by the seat (key derived inside the function; VALIDATE_ONLY_GATES ledger; root-entry export only) are unchanged. check:type-check-debt, NOT-MEASURED in round 0, is measured now (69/69 gates exit 0); Type Check · debt ledger is green on the head.
  • open_questions: empty. Round 0's one question (option B) was answered by the REWORK and landed as ② above.
  • out_of_scope_findings, each with a carrier or a stated reason: (1) the os build / os compile gap (class a, public door) — carried by the seat, which committed in the REWORK to filing it as its own card in the same family; the VALIDATE_ONLY_GATES row is the in-repo record until then. (2) the empty-string container name accepted by the boot loop and by os validate but refused at the artifact/HMR door by assertMetadataRegisterContract — pre-existing on both sides (boot treated '' as absent before this PR; packages/core and packages/metadata are untouched), no producer found, read-only inference; a note for the seat's family close-out, not a blocker on this card.
  • PR body vs diff: test counts 9 / 5 / 5 match the files; "15 divergent containers in 9 files" matches the diff; "os compile does NOT reach the shared function" verified on the head; "nothing else in engine.ts moved" true; "packages/spec/** and packages/metadata/** unchanged" true. "os validate exits 0 on each example with 0 refusals" is the dev's measurement; consistent with Dogfood Verify CLI green on the head and with my probe finding no divergent container under examples/ or packages/qa/. Upstream: merge-base 0d7ed5a3, main now 20 commits ahead of it; GitHub reports mergeable: true, mergeable_state: clean.
  • CI on the head (41 check-runs; newest per name): 0 failures. 36 success, 5 skipped. The 11:43Z re-runs after the seat's body edit: Check Changeset, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch all success; Auto Label and Check PR Size skipped on the re-run (their 06:30Z runs were success). Also green: Test Core and all six shards, Lint & Repo Gates, all four Type Check jobs, Build Core, Dogfood Verify CLI, Dogfood Regression Gate and its three shards, Temporal Conformance, Governed Surface Queue Guard, Check Documentation Links, Flag docs affected by code changes. Skipped by design: Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in). Round 0's red (Test Core (4/6) and the aggregate) is green on this head.

Implemented-by: claude/issue-20331-validate-view-container-name
Reviewed-by: local_1d2a197c-c20e-4e90-9be8-413d4d432289

Independence: INDEPENDENT AGENT (fed the card with every comment, the round-0 record, the REWORK, the PR body, file list and net diff, and the head's check-runs; not the dispatch order and not the dispatching seat's conclusions)

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 12:03
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit c5d6b2b Sep 28, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20331-validate-view-container-name branch September 28, 2026 12:22
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] os validate passes a view container whose own name disagrees with the object key it binds to, and os serve then refuses that stack at boot

3 participants