fix(objectql,cli): os validate refuses a view container whose name disagrees with its object, as boot does (#20331) - #20391
Conversation
…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>
…timeout Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP Co-authored-by: Claude <noreply@anthropic.com>
…lidate-view-container-name
…comments Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 16 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 — 37 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 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
|
Contract reviewServed-tier: ① Derived judgments
② Semver levelWrong as declared. ③ Boundary flags
Implemented-by: Independence: INDEPENDENT AGENT (fed the card, the triage direction and the PR only; not the dispatch order or the seat's conclusions) VERDICT: FAIL
Generated by Claude Code |
…lidate-view-container-name
…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>
Contract reviewServed-tier: Delta review of patch round 1 (PR #20391, card #20331). Round-0 record ① Derived judgmentsRound-0 findings, each resolved:
The whole net diff, accept-set and public-surface changes named:
② Semver levelCorrect as declared. ③ Boundary flags
Implemented-by: 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 |
…efuse a default export defineStack did not build (objectstack-ai#20460) Fixes objectstack-ai#20367 Clause-②: yes Implements ruling B on objectstack-ai#20367 (`Ruling-ref: 5869334748`): **one authoring shape**. `os validate` and `os build` now refuse a default export that no stack producer built, and `composeStacks` refuses an input no producer built. The six `STACK_*` cross-field refusals (capability, cross-reference, namespace prefix, single app, hierarchy scope, trigger capability) therefore reach both doors by construction, not only when the author happened to call `defineStack()`. ## What changed **spec (`@objectstack/spec`)** - `src/stack-provenance.ts` (new): a non-enumerable, non-writable `Symbol.for('objectstack.stack.provenance')` mark. The precedent is `data/filter-subtree-provenance.ts`. Stamping is internal to the two producers. The one published predicate is `hasStackProvenance(value)`, re-exported from `stack.zod.ts`. - `stack.zod.ts`: `defineStack` stamps its return value in both strict and `strict: false` mode. `composeStacks` stamps its artifact at every arity. Its step 0 refuses unbuilt inputs with `STACK_PROVENANCE_MISSING` (422), naming each refused input. This runs first, ahead of the single-input early return. The cross-field validators are unchanged and stay inside the producer. - `api/error-code-ledger.zod.ts`: registers `STACK_PROVENANCE_MISSING` under `@objectstack/spec` (emitted by `composeStacks`) and under `@objectstack/cli` (emitted by the doors). One condition, two emitters, following the `ENVIRONMENT_NOT_FOUND` precedent. The `:1323-1328` comment is left as written. The family is now raised by both doors through provenance. - ADR-0087 semantic migration entry `stack-config-default-export-unbuilt-refused`, plus the regenerated `registry.ts`, `api-surface/root.json`, `export-origins/root.json` and two reference docs pages. **cli (`@objectstack/cli`)** - `utils/config.ts`: `loadConfig` reads the mark off the default export before the named-export merge, which is a spread and drops the mark. It exposes the result as `LoadedConfig.stackProvenance`. - `utils/stack-provenance-refusal.ts` (new): `refuseUnbuiltStack` throws a `STACK_PROVENANCE_MISSING` / 422 error with the prescription to wrap the export in `defineStack(...)`. - `commands/validate.ts` and `commands/compile.ts`: step 1a, directly after load and ahead of every other judgement. The throw lands in each command's existing catch-all, so the `--json` envelope keeps its shape: `valid`/`success`, `error`, `code`, `warnings`, `conversions`. This is the same envelope a `defineStack` refusal raised at load already reaches. PR objectstack-ai#20391's step 2c is on `main`, and this step sits above it in the same `try`, feeding the same catch-all envelope. - `test/validate-build-gate-parity.test.ts`: the roster gains a row for `refuseUnbuiltStack` in `SHARED_NON_REGISTRY_GATES`. - Retired the plain-object door in three places: the plugin README template in `create.ts`, the `generate.ts` field-type comment, and the `environment-routing.mdx` sentence about spread configs. ## Premise test (ruled to be run first) The premise was that host-shaped exports (a `plugins` list of instantiated plugin objects, i.e. the `os serve` / `os migrate` host configs) do not go through these two doors. Measured on this tree, it holds for those host configs: - The only plain-object host configs in the repo are CLI test fixtures for `os serve`, `os migrate` and `os doctor`. None of them runs `os validate`, `os build` or `os dev`, and every one of them passes unchanged in the unit, integration and nightly tiers. - The one host-shaped config that does reach both doors is `examples/app-showcase`, which is authored through `defineStack`. It carries the mark and passes both doors. - A plain host-shaped export can mechanically reach the doors. The ablation leg below shows it passed `os build` at exit 0 before this change. It is now refused like any other unbuilt export, and the prescribed fix works: `defineStack({ manifest, plugins: [instance] })` is accepted by both doors. That row is pinned. ## Pins (`test/stack-provenance-door.test.ts`, integration tier, both doors, `code` + exit) | default export | answer at `os validate --json` and `os build --json` | |:--|:--| | defective stack (`requires: ['no-such-capability']`) as `defineStack({ … })` | `STACK_CAPABILITY_UNKNOWN`, exit 1 | | the SAME stack as a plain object | `STACK_PROVENANCE_MISSING`, exit 1; the prescription's first sentence is asserted; `os build` writes no artifact | | host-shaped plain object | `STACK_PROVENANCE_MISSING`, exit 1 | | host-shaped `defineStack({ … })` | accepted, exit 0 | | spread copy `{ ...defineStack(…), api: {} }` | `STACK_PROVENANCE_MISSING`, exit 1 | | `defineStack` + named export `onEnable` | accepted, exit 0 (the mark is read before the merge) | `packages/spec/src/stack-provenance.test.ts` pins the rest: - both producers stamp, in every mode and at every arity; - the mark is not visible through `Object.keys`, `JSON.stringify` or the strict schema; - `false` for a literal, a spread copy, an assign copy, a JSON copy or a structured clone; - `composeStacks` refuses first, names every input and refuses a lone input; - both ledger rows exist. **Examples: the echo from route D does not reproduce.** Measured with the built CLI at the merged head: | example | `os validate` | `os build` | |:--|:--|:--| | `examples/app-crm` | exit 0, `valid: true` | exit 0, `success: true` | | `examples/app-todo` | exit 0, `valid: true` | exit 0, `success: true` | | `examples/app-multi-package` | exit 0, `valid: true` | exit 0, `success: true` | | `examples/app-showcase` | exit 0, `valid: true` | exit 0, `success: true` | The build door is also held in CI: each example's `build` script is `objectstack build`, and the root `turbo run build` ran all four green (`Tasks: 73 successful, 73 total`). ## Verification record - **Ablation.** Committed first, then mutated through `scripts/ablation-replace.mjs`: `if (loaded.stackProvenance) return;` became `return;`, landing confirmed by anchor 1 → 0 and blob `5ce32a82` → `d78f0948`. The door test's plain-object rows went red (4 failed; under the mutation the plain defective stack answered `code: undefined` at exit 0 and the plain host shape built at exit 0, which is the original defect). The file was restored byte-identical: its blob equals the `HEAD` blob and `git diff HEAD` is empty. The green leg is the full integration run at `HEAD`. - **`@objectstack/spec`:** `vitest run`, 602 files, 17349 passed. - **`@objectstack/cli` unit:** 233 files, 3336 passed. - **`@objectstack/cli` integration:** 62 files, 533 passed, 1 skipped. - **`@objectstack/cli` nightly e2e tier** (`OS_TEST_TIERS=nightly`): the 18 files this change reddened or touched are green, 96 + 57 + 6 tests across three runs. The first full nightly run (17 red files) is what named them. - **Other consumers:** the `composeStacks` test inputs in `@objectstack/metadata`, `@objectstack/runtime`, `@objectstack/plugin-dev` and `@objectstack/plugin-security` are green (1 + 3 + 1 + 1 files). Filter direction: `composeStacks` / `os validate` / `os build` callers found by `git grep` over `packages/**`. No non-test caller of `composeStacks` exists outside `stack.zod.ts`; every other hit is a comment. - **Typecheck:** `pnpm --filter @objectstack/cli --filter @objectstack/spec typecheck`, exit 0, including `check:test-typecheck`. - **Generated artifacts:** `pnpm --filter @objectstack/spec check:generated --fix` rewrote only the three it proved stale (api-surface, export-origins, docs). All 15 were green on re-check. - **Gates:** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 123 commands at head `0360658`. 122 exited 0. One is NOT MEASURED: `node scripts/check-plugin-teardown-shape.mjs --self-test` exited 3 on the shallow clone, because its pinned fixture commit is unreachable; it is checker-health only. `--ran` reconciliation: 123 accounted, 122 run, 1 NOT-MEASURED. Four gates first exited 3 for lack of a full build (`check:skill-examples`, `check:dual-build-cjs-loads`, `check:i18n-coverage`, `check:type-check-debt`). They were re-run after the full build and exited 0. ## Fixture census The dispatch estimated "63 CLI test files". The instrument used here is the suite's own reds after the change, across all three tiers, plus `git grep` for `composeStacks(` inputs: - CLI: 3 unit, 11 integration and 17 nightly e2e files red, plus the re-judged `requires-retired-capability.e2e.test.ts`. Each fixture was rewritten to `defineStack(…)` with spec linked into the temp dir through the new `test/helpers/define-stack-fixture.ts`. - spec: 4 test files red. Their hand-built inputs are now a built stack mutated after `defineStack` returned: the mark survives in-place mutation, so this is the reachable route to the composer guards. - Other packages: 3 test files with plain `composeStacks` inputs. Fixtures for `os serve`, `os migrate`, `os doctor` and `os lint` were not rewritten. Those doors are outside the ruled surface and do not refuse; this is the host-config premise above. ## Acceptance notes - **Pin re-judged: `requires-retired-capability.e2e.test.ts`.** An unknown `requires` token is now the producer's `STACK_CAPABILITY_UNKNOWN`, exit 1, at both doors. The retired token's spec-owned prescription is asserted verbatim in the refusal message, and the typo hint is asserted to appear exactly once, for the misspelled token. - **Pins re-judged: the conversions and `--strict` pins.** These are `validate-json-failure-conversions.e2e`, `build-json-failure-conversions.e2e` and the objectstack-ai#11301 cell of `validate-json-strict-exit.e2e`. `defineStack` applies every ADR-0087 D2 conversion itself at load, in either mode, and reports it on stderr. That leaves the doors' own step-2 sink nothing to convert on any accepted config, so `conversions` is `[]` on every exit and `--strict` does not fail on a retiring conversion. This was already true for every `defineStack` config before this PR. The pins now assert `[]` plus the live notice on the producer's stderr line, matched by conversion id and path. The loss is raised as an open question in the report, not fixed here. - **`os dev`** compiles through `os build`, so it refuses an unbuilt export when it compiles. `os serve`, `os migrate`, `os lint` and `os generate` still load unmarked configs, because the ruling scopes the refusal to `os validate` and `os build`. The `generate.ts` comment now says so rather than claiming the door is gone everywhere. - **Fixture spelling.** Fixtures whose content is the door's to judge use `defineStack(…, { strict: false })`: schema errors the door must report, rule findings, the conversion fixtures. Valid fixtures use strict `defineStack`. - **Landing site vs the declared surface.** Three additions go beyond the claim's file surface: the ADR-0087 semantic entry and its generated `registry.ts` region (the `registered` disposition the breaking changeset needs), test inputs in three other packages, and the stale comment in `capability-preflight.test.ts`. Each is required by the change itself. - **Serial constraint.** PR objectstack-ai#20391 is on `main`. `main` was merged into this branch at `6e3e546` before this PR opened. --- _Generated by [Claude Code](https://claude.ai/code/session_01RTkKf8Dn5F4mepiZZfWoxH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20331
Clause-②: yes
os validatenow runs the same view-containernamecheck 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, andos servethen refused that same stack at boot. The triage direction was "one judge, not a second rule". The check moved out ofObjectQL.registerMetadataCollectionsinto one function, and boot andos validateboth call it. There is no lint twin and no new error code, and boot's message and envelope are unchanged.Premise, measured on
origin/main862b6ce86before any editThe setup: the CLI's dependency closure was built, then
os init my-app -t app --no-install,os g object order_lineandos g view order_line. The view'snamewas then hand-edited to'order_line', bound to objectmy_app_order_line.os validate✓ Validation passed,UI: 1 Viewsos serve --devviews: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' …"os compile{ name: 'order_line', object: 'my_app_order_line' }os serve, booting that artifact (dist/objectstack.json, no config)What changed
@objectstack/objectql:viewContainerNameRefusal(container, sourceLabel, ownerId)(newsrc/view-container-name-refusal.ts) returns the refusal the boot registrar throws, orundefined. It carries the same gate: the container branch (isAggregatedViewContainer), and anamethat 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 answersundefinedfor it. It uses the same message and the sameVALIDATION_ERROR/ 400 envelope. It derives the key itself withderiveViewContainerObject, which for a container is exactly whatresolveMetadataItemNamereturns. So no caller re-spells "which derivation boot uses".registerMetadataCollectionskeeps itskey === 'views'narrowing and throws what the function returns. Nothing else inengine.tsmoved.@objectstack/objectqlroot entry:viewContainerNameRefusaland its typeViewContainerNameRefusal. This is the widening behindClause-②: yes:@objectstack/objectqlis gradedminor, and@objectstack/clistayspatch. The changeset states it.@objectstack/cli:findViewContainerNameRefusals(parsed)(newsrc/utils/view-container-names.ts) owns the WALK only, and the verdict is objectql's. It walks what the load path registers. With nopackages[], that is the top-levelviews, under the manifest id:AppPluginhands{ ...manifest, ...stack }to the manifest service, and the id is read throughartifactPackageId. Withpackages[], it is eachpackages[i].manifest.views, under that package's id, and not the top level, whichresolveArtifactPackageOrdernever registers.os validatestep 2c runs right after the schema parse. It exits 1. The text face prints the refusals as bullets.--jsonemitserrors: [{ path, code, httpStatus, message }]and carrieswarnings/conversionslike every other exit.test/validate-build-gate-parity.test.tsgains aVALIDATE_ONLY_GATESledger. The closed roster had no honest place for a gate wired intovalidate.tsalone:SHARED_NON_REGISTRY_GATESasserts that both commands call it, and aNOT_A_GATErow would be a false statement. The new row is pruned both ways: it goes stale ifvalidate.tsstops calling the gate, and it must move toSHARED_NON_REGISTRY_GATESifcompile.tsstarts calling it.After the fix, on the same project (CLI from source,
@objectstack/objectqldist rebuilt)os validateexits 1 with✗ The server would refuse this stack at boot (1 view container)and the boot message.os validate --jsonexits 1 witherrors[0] = { path: 'views[0]', code: 'VALIDATION_ERROR', httpStatus: 400, message }. Themessageis byte-equal to the lineos serve --devprinted before the fix (diffis empty).name: 'my_app_order_line'gives exit 0, and deletingnamegives exit 0.os serve --devstill exits 1. Its message is byte-identical before and after the refactor (diffof 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 (noname, a matchingname, a name-only container, a non-container, an empty-stringname), 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. Thepackages[]bodies are judged under their own ids, and the top level is not judged whenpackages[]is present.packages/cli/test/validate-view-container-name.test.ts(new, integration tier: it spawns the CLI and constructsObjectQL, 5 tests):--jsonexits 1 anderrors[0].messageequals whatObjectQL.registerAppthrows for the same stack. The text face exits 1 with the same words. The matching control and the no-namecontrol both exit 0. A premise case asserts that boot refuses the divergent stack and accepts both controls.view-container-divergent-name-registrars.test.tsare unchanged and green.4e15d3cea:@objectstack/objectql:vitest run --project local, four shards: 323 files, 5865 tests passed. Typecheck (tsc --noEmit×2 pluscheck:test-typecheck) exit 0.@objectstack/cli:vitest run --project unit, two shards: 232 files, 3315 tests passed. Typecheck exit 0.--project integrationwas run locally for the new file only (5/5); the rest of the integration tier is declared to CI.tsc --listFilesOnlyconfirms every new file is in a typecheck program (tsconfig.json/tsconfig.test.jsonof each package).pnpm lint(fulleslint . --no-inline-config) exit 0.Ablation (fix committed first; mutation through
scripts/ablation-replace.mjs)return undefinedat the top ofviewContainerNameRefusal, marker__ABLATION_20331_NEVER_REFUSE. The tool reported anchor x1 to x0 and blob07da29b9toab52f626. The objectql build andablation-dist-preflight.mjsfound the marker in 4 built files.data.namediffers from the derived key — one silently rewrites the author's field, the other hard-fails the whole artifact load #14666): the boot loop REFUSES the divergent container …" and "MEASURED CORRECTION … refuses, enveloped (MetadataFacadeanswers threeregister→getround-trip cases differently from every other shippedIMetadataService#7378 row 1)".--json) and the text face. The controls stayed green.git checkout HEAD --on the absolute path, by the tool's trap. The blob after the restore equals HEAD07da29b9, andgit diff HEADis empty. After the rebuild,ablation-dist-preflight --absentfound the marker absent from all 14 built files and a working tree clean against HEAD. objectql was then 20/20 green and cli 10/10 green.Gates at
4e15d3ceaThese were derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, which gave 69 commands.--ranreconciled them as 68 run, 1 NOT-MEASURED and 0 UNRUN. Every command exited 0 except the one below:check:type-check-debt --re-measureruns a whole-workspaceturbo run buildinside itself. On this shared box that build did not fit the foreground cap: the first attempt was SIGTERMed mid-build, and I rebuiltpackages/specafterwards. Neither touched package carries aDEBT/TEST_DEBTrow.lint.ymlruns 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-coverageandcheck:i18n-walk-parityexited 3 until the CLI, the examples and the unbuilt packages were built, then 0.check-engine-split-ratio.mjs --days 90exited 2 until the history was deepened with--shallow-since, then 0.check-issue-citations.mjsexited 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 of4e15d3cea)test/union-fold-command-parity.test.ts(PEDIGREE CONTROL) andtest/validate-per-package-authoring-parity.test.tsasserted exit 0 on stacks the boot registrar refuses: each carried containers named*_listbound to*_account/*_order. Each container now names its bound object, and each pin's assertion is unchanged.packages/cli/test/,packages/cli/src/,packages/qa/andexamples/(all 963 tracked.ts/.js/.jsonfiles, the.e2etier included). A structural scan read every object literal carryingnameplus a container arm (list/form/listViews/formViews) and noviewKind, 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 validateexits 0 on each of app-crm, app-multi-package, app-showcase and app-todo, with 0 container refusals.packages/qa/: no hit.viewsis not a map-form collection (MAP_SUPPORTED_FIELDSexcludes it), so no key-injected name can hide there.@objectstack/objectqlis nowminorwithClause-②: yes, for the two new root exports (viewContainerNameRefusal,ViewContainerNameRefusal).@objectstack/clistayspatch. The changeset states the widening.registerMetadataCollectionswarns about and skips an entry whose derived key is falsy before the name check runs.viewContainerNameRefusalnow returnsundefinedfor that entry too. The derivation keeps''forlist: { data: { object: '' } }, so without thisos validatewould refuse a container that boot only skips. A new control pins it: boot throws nothing and registers nothing, and the function returnsundefined. The docblock and theengine.tscomment now say what moved: the message and the envelope moved byte for byte, and the gate moved whole, precondition included.VALIDATE_ONLY_GATEStoo.Runs at
6b5895b97:@objectstack/objectql:--project localin 4 shards, 325 files / 6017 tests passed. Typecheck exit 0. The new test file is 9/9.@objectstack/cli:--project unitin 2 shards, 233 files / 3335 tests passed.--project integrationIN FULL in 3 shards, 61 files / 512 tests passed and 1 skipped. Typecheck exit 0.OS_TEST_TIERS=nightly: the census touched no.e2efile, so the 13 nightly files that runos validatewere run instead. 13 files / 129 tests passed.pnpm lintexit 0.check-issue-citations --base origin/mainexit 0.check-changeset-no-majorlevel axis: driven with--eventon this body withClause-②: yes, it answers "✓ LEVEL AXIS …@objectstack/objectql: minor" (exit 0).--ranreconciles 69 run, 0 NOT-MEASURED, 0 UNRUN, every one exit 0. That includescheck:type-check-debt: 4 entries re-measured, none above its number.scripts/ablation-replace.mjs --deleteremoved theif (!itemName) return undefined;line (anchor x1 to x0, blob21af64e5to56d25d71). Exactly the new control went red: the function returned "Invalidviews:container … binds to, ''" whereundefinedwas expected. The other 20 tests stayed green, the existing boot pins among them. The restore gave a blob equal to HEAD21af64e5and an emptygit diff HEAD, then 21/21 green.Acceptance notes
os compile/os builddoes 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 thatos serverefuses when it boots it, as measured above. TheVALIDATE_ONLY_GATESrow records that gap. I reported it to the seat as an out-of-scope finding.MetadataPlugin._parseAndRegisterArtifact(packages/metadata/src/plugin.ts, the container branch around :1103-:1121) registers the container underderiveViewContainerObject(item)throughthis.manager.register('view', viewObject, item). The refusal there comes from the GENERIC register contractassertMetadataRegisterContract(packages/core/src/metadata-service-contract.ts,MetadataFacadeanswers threeregister→getround-trip cases differently from every other shippedIMetadataService#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.ViewSchema.nameisz.string().optional(), so an empty-string containernameis spec-valid. The boot loop (andos validate, which mirrors it) treats''as absent and registers the container. The generic register contract at the artifact door refuses it, because'' !== undefinedand'' !== key. No producer ofname: ''was found.plugins[]entry'sviewsis registered by boot under the labelnested plugin, butos validatedoes not walk it. The stack schema typespluginsasunknown[], and in an authored config they are runtime plugin instances.origin/mainwas merged atb1cbd9277, and again at0d7ed5a37in 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, touchesengine.tsin theengine.aggregatehunks only, and agit merge-treeprobe merges cleanly, so I did not merge again.packages/spec/**andpackages/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