fix(cli): os build refuses a view container whose name disagrees with its object, as boot does (#20393) - #20459
Conversation
… its object, as boot does `os build` / `os compile` now runs the walk `os validate` runs at its step 2c, over the same judge the boot registrar throws the answer of, right after the schema parse and before any artifact write. A stack the server would refuse at boot no longer ships as an artifact. The gate-parity ledger row moves from VALIDATE_ONLY_GATES to SHARED_NON_REGISTRY_GATES, where both doors are held to the call. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
…ner name refusal The pending #20331 entry's closing paragraph said `os build` does not run the check; it does now, and both entries ship in the same release, so that paragraph is corrected to point at this one. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
…ild-view-container-name
📓 Docs Drift CheckThis PR changes 1 package(s): 18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 25 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fda66a194af315e894f2079e7015fca2c76d3037 && git checkout fda66a194af315e894f2079e7015fca2c76d3037
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b285508188ebe9cf14cd1ee621ea857f688a938b 0c4e9c37248551ad36a9261603eea191a225e7f1 && git checkout -B drift-repro b285508188ebe9cf14cd1ee621ea857f688a938b && git merge --no-ff 0c4e9c37248551ad36a9261603eea191a225e7f1
node scripts/docs-audit/affected-docs.mjs --json b285508188ebe9cf14cd1ee621ea857f688a938b
|
Contract reviewServed-tier: PR #20459 ( Check-runs on this head, read at 14:57 UTC: 32 check-runs, newest per name. 23 ① Derived judgments
DELIBERATE CORRECTION —
|
…defineStack Claude-Session: https://claude.ai/code/session_01RTkKf8Dn5F4mepiZZfWoxH Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20393
Clause-②: no
os build/os compilenow runs the checkos validatehas run since #20331: aviews:container whose ownnamedisagrees with the object key it binds to is refused. The build exits 1 with the message the boot registrar prints, and it writes no artifact. Before this change the build exited 0 and wrotedist/objectstack.json, andos servethen refused that artifact at boot. This follows triage's grade on the card (comment 5865077508): the build calls the same function over the same view set, thevalidate-build-gate-parityrow moves fromVALIDATE_ONLY_GATEStoSHARED_NON_REGISTRY_GATES, and there is no second rule.Premise, measured on
origin/main7fa3e3e07before any edit (H0)The setup: the CLI's dependency closure was built (turbo, 59 tasks). Then
os init my-app -t app --no-install,os g object order_lineandos g view order_linewere run, and the view'snamewas hand-edited to'order_line', bound tomy_app_order_line.os validateos buildBuild complete, and wrotedist/objectstack.jsoncarrying[{"name":"order_line","object":"my_app_order_line"}]os servein a directory holding only that artifact (no config)views:container from manifest 'com.example.my-app': the container's ownnameis 'order_line', which disagrees with the object key it binds to, 'my_app_order_line' …"The card's moot condition does not hold: the container body
namestill parses onmain(#20357 retiredlist.tabsonly).What changed
packages/cli/src/commands/compile.ts, new step 3a, right after the schema parse and before the rule table and any artifact write. It makes the same callvalidate.tsstep 2c makes,findViewContainerNameRefusals(result.data). That is the walk over@objectstack/objectql'sviewContainerNameRefusal, the function the boot registrar throws the answer of. There is no second implementation of the check or of the walk. The message is the runtime's own, unchanged.--json:{ success: false, errors, warnings: warningsSoFar(), conversions }. This is build's schema-exit envelope, with the refusal rows inerrorsexactly asos validate --jsoncarries them:{ path, code: 'VALIDATION_ERROR', httpStatus: 400, message }. The exit carrieswarningsandconversionslike every other exit, so thebuild-json-failure-warningscontract holds.os validate's header ("The server would refuse this stack at boot (N view container(s))") and the bullet list. No step line is printed, so a passing build prints what it printed before (the docs transcripts stay true).packages/cli/test/validate-build-gate-parity.test.ts:findViewContainerNameRefusalsmoves toSHARED_NON_REGISTRY_GATES, soboth commands run findViewContainerNameRefusalsnow holds both doors to it.VALIDATE_ONLY_GATESstays, empty, with a note that empty is its steady state. Its two-way pruning test is unchanged.packages/cli/src/commands/validate.ts: one comment sentence in step 2c said "os builddoes not run it", which this change makes false. It now points at build's step 3a. Code is unchanged.packages/cli/test/build-view-container-name.test.ts(integration tier: it spawns the CLI and constructsObjectQL). It has 6 tests:build --jsonexits 1,success: false, anderrors[0]equals whatObjectQL.registerAppthrows for the same payload (message,code,httpStatus,path: 'views[0]'). No artifact is written;Build complete, no artifact;packages[]stack: the divergent container in the second body is refused aspackages[1].manifest.views[0], under that package's id and in the words boot throws for that body. The matching container in the first body is not reported;nameand noname: each exits 0. The matching control's written artifact is registered by boot without a throw..changeset/20393-build-view-container-name.md:@objectstack/clipatch,Clause-②: no..changeset/20331-validate-view-container-name.mdwas also edited. See the gate note below: this is a deliberate correction and needs your confirmation.Which shape build judges, and why the verdict is boot's (H1)
Build judges
result.data, the output ofObjectStackDefinitionSchema.safeParse(lowerCallables(normalized).lowered). That is the same expression, over the same pipeline, as the input tovalidate.tsstep 2c. It is also the object build serializes. Step 4 adds onlydocs,packages[i].manifest.docs(viaattachPackageDocs, docs only) andruntimeModule, never a view. So everyviews:entry the artifact carries is judged here, at the top level or in eachpackages[i].manifestbody. The walker mirrors the load path over that shape:{ ...manifest, ...stack }underartifactPackageId, or each package body under its own id. Measured after the fix on the repro project:os build --json'serrors[0].messageis byte-equal to the lineos serveprinted when booting the pre-fix artifact (cmp: identical, 568 bytes).After the fix, on the same project (CLI from source)
os buildexits 1 with the refusal, and nodist/directory is created.os build --jsonexits 1 withsuccess: false, anderrors[0]isviews[0]/VALIDATION_ERROR/400.name: 'my_app_order_line'gives exit 0 and an artifact. Deletingnamegives exit 0 and an artifact.examples/:os build --jsonexits 0 withsuccess: trueon each of app-crm, app-multi-package, app-showcase and app-todo, with no refusals. The output was written outside the tree.Fixture census (H2): no build-door fixture turned red
A structural scan read 7,909 tracked
.ts/.js/.jsonfiles underpackages/,examples/,apps/andscripts/, including the 260 string and template literals that carry config source (the configs tests write to disk). It found 936 containers. It read each object literal carrying a container arm (list/form/listViews/formViews) and noviewKind, and derived the key as boot does. It found 21 divergent containers, all outside every build door:packages/lintrule unit tests (13),packages/objectql(3, including the refusal's own fixtures),packages/metadata-protocol(2) andpackages/spec(2). None of these runsos build.packages/cli,packages/qaandexamples/have none; #20331's patch round had already fixed that population. There were 9 non-literal-name containers, none in a build-door suite. The behavioural half agrees: all 15 build-door.e2esuites (the nightly tier) and the full unit tier are green at the head.Ablation (H3), with the fix committed first
The mutation went through
scripts/ablation-replace.mjsonpackages/cli/src/commands/compile.ts. It replaced the call withconst containerNameRefusals: ReturnType of typeof findViewContainerNameRefusals = []plus the marker__ABLATION_20393_NO_CALL. The tool reported anchor x1 to x0, replacement x0 to x1, and blob6b4b8871tob7f532cb. On disk, the marker count was 1 and= findViewContainerNameRefusals(counted 0.src/commands/compile.tsas text, and the build-door test runsbin/run-dev.js, which issrc/through tsx. So nodist/leg applies.both commands run findViewContainerNameRefusals(unit), THE PIN, the text face, and thepackages[]case. The premise, both controls and the rest of the parity file stayed green. The direction is red, as expected.git checkout HEAD --on the absolute path. The blob after the restore equals HEAD6b4b8871,git diff HEADis empty,git status --porcelainis empty, and the marker count is 0. The re-run was 30 of 30 green.Tests, at head
0c4e9c3724(after mergingorigin/main3cf644938)@objectstack/clivitest run --project unit --maxWorkers=2, two shards: 117 + 116 files, 1789 + 1547 tests, all passed.--project integration, run locally for the two view-container files only: 11 of 11 passed. The rest of the integration tier is declared to CI..e2ebuild-door suites (OS_TEST_TIERS=nightly), 15 files: 297 of 297 passed.pnpm --filter @objectstack/cli typecheck: exit 0.tsc --listFilesOnlyputs the new test in thetsconfig.test.jsonprogram.TMPDIR, which macOS resolves from/varto/private/var:published-subpath-console.pin,published-subpath-hook-body.pinandconfig-miss-stdout-purity.e2e. Under the defaultTMPDIRthey fail on that prefix alone, for commands this PR does not touch (os diff,os info,os verify, …). WithTMPDIRset to its realpath they pass (29 of 29 and 174 of 174), so the counts above were taken that way. See the Acceptance notes.Gates, at
0c4e9c3724node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsgave 63 commands.--ranreconciled them as 63 run, 0 NOT-MEASURED and 0 UNRUN. Every command exited 0 except the one below.check:dual-build-cjs-loadsandcheck:i18n-coveragefirst exited 3 (PREREQUISITE NOT MET, unbuilt packages). Each exited 0 after the named packages were built.pnpm lint(fulleslint . --no-inline-config): exit 0 in 45 s.node scripts/check-issue-citations.mjs --base origin/main, run after mergingorigin/main: exit 0, 3 citations resolve.check-issue-citations.mjs --censusand the dogfood shard attestation.⚠
check-empty-changeset --base origin/mainexits 1 by design: a pending release note is corrected here.changeset/20331-validate-view-container-name.mdis #20331's pending entry. It closes with "Not changed:os builddoes not run this check, so it still writes an artifact carrying such a container …". This PR makes that sentence false. Both entries ship in the same release while that file is pending, and a publishedCHANGELOG.mdsentence is corrected only in the entry that carries it. So that paragraph now reads "os buildruns the same check as well (#20393, its own entry), so it no longer writes an artifact carrying such a container." The gate classifies this as the DELIBERATE CORRECTION class: it stays red, and its remedy is to confirm the correction on the PR, ⛔ not to restore the file.os validatepasses a view container whose ownnamedisagrees with the object key it binds to, andos servethen refuses that stack at boot #20331 entry.20331-validate-view-container-name.mdbefore this lands, that file's edit becomes a modify/delete conflict. Drop the edit then: the sentence was true in that release.Declared narrowing: verification ran UNLOCKED
Every build and test run above went through
scripts/pm/os-verify-lock.sh, and each run printed this disclosure (quoted verbatim from the first one):Acceptance notes
compile.ts, the parity test,packages/clitests and one new changeset. Two more files were edited, each because this change made a sentence in it false. One is a comment sentence invalidate.tsstep 2c ("os builddoes not run it"). The other is the closing paragraph of the pending.changeset/20331-validate-view-container-name.md(above).packages/cli/src/utils/view-container-names.tsopens "os validate's author-time half of …". Both doors call it now.packages/objectql/src/view-container-name-refusal.tssays "called by the boot registrar and byos validate". It is read-only for this card, andos buildreaches it through the walker.plugins[]entry'sviews, which boot also registers. Its header states why: the stack schema typespluginsasunknown[]. Build inherits that bound. It does not widen it.TMPDIRon a/varvs/private/varprefix:test/published-subpath-console.pin.test.ts,test/published-subpath-hook-body.pin.test.tsandtest/config-miss-stdout-purity.e2e.test.ts. They pass with a realpathTMPDIR, and CI (Linux) is unaffected. There is no public-door reach. Carrier: none.Generated by Claude Code