fix(metadata-protocol,metadata): the runtime save door refuses a view container whose name disagrees with its save name, through the one judge every door calls (#21412) - #21483
Conversation
…es a container The judge moves into @objectstack/metadata as the ./view-container-name subpath: a container's own name, when set, must equal the key the door files it under. The source registrars derive that key from the binding; the runtime save door files under the save name. The artifact door's container branch now refuses through the judge before the loader write. Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
…name disagrees with its save name saveMetaItem now calls the one judge before normalizeViewMetadata can keep an authored container name, so a container is never stored under its row name and registered under its body's. objectql re-exports the judge from @objectstack/metadata; the ViewSchema comment states who writes a container's name and the rule every door applies to it. Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
…fact door Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
…er-name entry Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
…l, the re-export and the ViewSchema comment Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 24 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 142 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 0445fa50f4896ad2abfbfecb22c5224388f1b935 && git checkout 0445fa50f4896ad2abfbfecb22c5224388f1b935
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin aa4632235ba571ef800b95e6bc18d00a30aa1d57 0e7d0bdb71fd3b4a2c5249414fff6e2b6866556b && git checkout -B drift-repro aa4632235ba571ef800b95e6bc18d00a30aa1d57 && git merge --no-ff 0e7d0bdb71fd3b4a2c5249414fff6e2b6866556b
node scripts/docs-audit/affected-docs.mjs --json aa4632235ba571ef800b95e6bc18d00a30aa1d57
|
…use-2 arm Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #21412 (body and all 7 comments: triage 5954055712, rider 5954896314, claim 5961640434 with revisions 2 and 3, needs_decision 5961864645, seat answer 5961930912, reports 5962619241 and 5962685002), PR #21483 (body, 15-file list, net diff against ① Derived judgmentsAccept-set changes the diff implies:
Public-surface changes the diff implies:
Nothing judged wrong. ② Semver levelFour changesets, each judged against what its package publishes:
The PR body's declaration reads Gate verdicts on the head: ③ Boundary flagsDev flags from the PR body and the reports 5962619241 and 5962685002:
Open questions on the final report (5962685002): none. The earlier Q1 to Q4 (5961864645) were answered by the seat (5961930912) and the diff implements A, A, A, A as judged in ①. CI on the head at read time: 14 check-runs completed success (filter, Check Changeset, Type Check source gates, Spec property liveness, Governed Surface Queue Guard, Validate Package Dependencies, Check PR Size, Check Documentation Links, Flag docs affected by code changes, Auto Label, and the four claim and single-writer guards); 3 skipped (Packed-tarball smoke, Console Pin Gate, Build Docs); 16 in progress (Build Core; Test Core 1 to 6; Type Check workspace, debt ledger and consumer gates; Lint & Repo Gates; Temporal Conformance; Dogfood Verify CLI; Dogfood Regression Gate 1 to 3). This record's PASS is on the diff, the card and the completed checks; landing still waits for every check green. Implemented-by: VERDICT: PASS |
Fixes #21412
Clause-②: yes (narrowing)
The runtime save door (
saveMetaItem, which RESTPUT /api/v1/meta/view/:nameand the dispatcher's metadata save both call) now refuses an aggregated view container whose bodynamedisagrees with the name it is saved under. It answersVALIDATION_ERROR/ 400 before anything is stored or registered, and it refuses through the same judge the source registrars call. Before this change, the card's probe (rowcrm_lead, bodynamelead_views) was accepted, stored undercrm_lead, and registered underlead_viewspluscrm_lead.default, so one document answered under two names.This round follows the seat's answer on the card (comment 5961930912) to the dev's
needs_decisionreport (5961864645): Q1 A, Q2 A, Q3 A and Q4 A.What changed
@objectstack/metadata. New subpath@objectstack/metadata/view-container-name(packages/metadata/src/view-container-name.ts). The judgement: a container's ownname, when set, equals the key the door files the container under. It has two entries, which share one gate, one envelope and one message template:viewContainerNameRefusal(container, sourceLabel, ownerId)is the source registrars' entry. Its key is DERIVED from the binding (deriveViewContainerObject). It is the function that used to live inpackages/objectql, moved, and its words are byte for byte the same (proof below).savedViewContainerNameRefusal(container, saveName)is the save door's entry. Its key is the save name.namedisagrees with the row (P3, P4).@objectstack/metadata.@objectstack/corecannot host the judge: the judge needsderiveViewContainerObject, which lives in@objectstack/metadata, and@objectstack/metadatalists core.@objectstack/metadatais the one layer all three doors already depend on. The judge is a subpath of its own, not the./view-containerleaf, because that leaf imports nothing and the judge needsisAggregatedViewContainerfrom@objectstack/spec. It is not on the root entry either, because the root loads the manager and the filesystem machinery that objectql's ADR-0076 lean entry must not reach.check:lean-entry-closureholds:@objectstack/objectql/coreis 15 packages, the admitted set held exactly.packages/metadata-protocol/src/protocol.ts,saveMetaItem) callssavedViewContainerNameRefusal(request.item, request.name)forviewfirst, beforenormalizeViewMetadatacan keep an authoredname. Containers only.normalizeViewMetadata's docblock says so.packages/metadata/src/plugin.ts) callsviewContainerNameRefusal(item, 'artifact', packageId)after it derives the key and beforememLoader.save/manager.register. The probe document is now refused through the judge, in the judge's words. Row 1 (assertMetadataRegisterContract) is unchanged for every type; on this shape it is simply no longer reached.@objectstack/objectqlkeepsviewContainerNameRefusaland theViewContainerNameRefusaltype as a re-export (src/view-container-name-refusal.ts). Its module header is rewritten: the judge's home, why it moved, why the source registrars' entry derives the key and the save door's takes it.engine.tsandpackages/cliare untouched.ViewSchema'sguidance:inpackages/spec/src/ui/view.zod.tsno longer sayssaveMetaItemsends the name, artifact-shipped containers do, and the sweep injects it. It says the door's own stamp (normalizeViewMetadata) is the only platform writer of the key and states the one rule, worded to Q1 A. No schema change.The message: one template, two renderings
The per-door words are one value, the door's key origin. It fills four slots: the subject, the key clause, the text after the key, and the cross-reference after the shared reason. Everything else is shared.
code,statusandhttpStatuswere all equal,Tests 1 passed. The test was deleted afterwards.view containerand not`views:` container from SOURCE 'OWNER': the save door has noviews:collection and no owning manifest.the name it is saved under.the artifact/HMR loader refuses this same document, which would be false at this door for P3 and P4: the artifact loader accepts a body whosenameequals its binding.Pins (triage pins, restated to Q1 A)
packages/metadata-protocol/src/view-container-runtime-expansion.test.ts, the filing's own stub engine:VALIDATION_ERROR/ 400. No row is stored and nothing is registered.savedViewContainerNameRefusal's, so the refusal goes through the judge.code/statusequal the artifact/HMR registrar's for the same document.crm_lead.default.packages/metadata/src/view-container-name.test.tscovers:namediffers from its row name, and registers it under the body name (the every-type half of #21412) #21470 remains open for the every-type half);namestill registers.os validate's refusal words are unchanged, and their pins are unedited:packages/objectql/src/view-container-name-refusal.test.ts(green) andpackages/cli/test/validate-view-container-name.test.ts(CI; see Tests).packages/objectql/src/view-container-divergent-name-registrars.test.ts, the artifact door's two message assertions now read the judge's words (binds to, 'crm_lead',`name` is 'lead_views') instead of row 1's. The envelope-equality pin is unchanged.Reverse verification (both from committed HEAD, through
scripts/ablation-replace.mjs)if (nameRefusal) throw nameRefusal;inprotocol.tswas disabled (anchor 1 to 0, blob478daa416f90to6f9ae10ffb7b), andview-container-runtime-expansion.test.tswent5 failed | 65 passed (70): P1 three times, P3 and P4. P2, P2b and the control stayed green. The tool then restored the file to HEAD (blob == HEAD (478daa416f90),git diff HEADempty). Both test and subject resolve the protocol fromsrc, so no build was involved.plugin.tswas disabled (blob6924156b5fdatocef5ec76af37), andview-container-name.test.tswent1 failed | 10 passed (11). The refusal came from row 1 instead (nohttpStatus), so the assertion failed. The file was restored to HEAD. The test imports./plugin.jsfromsrc.Tests
Code at
a70d0d61a4is identical to7d4ee0ac46outside.changeset/. All runs went throughscripts/pm/os-verify-lock.sh, and each gaveVERDICT command-exit 0:@objectstack/metadata:pnpm testgaveTest Files 57 passed (57),Tests 847 passed (847), andpnpm typecheckexited 0. Its tsconfig includessrc/**/*, so the tests are type-checked.@objectstack/metadata-protocol:pnpm testgaveTest Files 205 passed | 3 skipped (208),Tests 3092 passed | 19 skipped (3111), andpnpm typecheckexited 0 (tests included).@objectstack/objectql: thelocalproject gaveTest Files 366 passed (366),Tests 7383 passed (7383),test:repogave1 passed, andtypecheckexited 0. That coverstsc, the scripts project, andcheck:test-typecheckOK, held in the debt ledger.turbo run build --filter=@objectstack/objectql^... --filter=@objectstack/objectqlgave14 successful. The new subpath loads under both conditions (requireandimporteach return both entries,VALIDATION_ERROR400), anddist/view-container-name.d.ts/.d.ctsare emitted.packages/cli, theos validatepins.@objectstack/objectql's export keeps its name, signature and bytes of output, and the cli imports it unchanged. Building the cli closure is 60 tasks, 11 of them cached. CI runs them. The byte-identity proof above is the local evidence.packages/metadata/src/serializers/typescript-serializer-annotation.test.tspins how manyexportsentries it visits (5 to 6), so the new entry is checked too.Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandswas derived ata70d0d61a4and gave 99 commands. All 99 were run ata70d0d61a4:pnpm check:dual-build-cjs-loadsexited 3 withPREREQUISITE NOT MET, because it needs every package'sdist/(the direct load check above stands in, but is not the gate).check-adr-0087-registration --base origin/main:1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.check-changeset-no-major: no major; the level axis needs a PR payload.check:lean-entry-closure: admitted set held.check:published-files,check:dts-closure,check:issue-citations(22 resolve),check:doc-authoring,check:spec-docblock-symbol-anchors,check:nul-bytes,check:test-source-alias,check:cross-package-test-inputs: all green.Lint, a proven narrowing at
a70d0d61a4:eslint --no-inline-config --format jsonover the 10 touched.tsfiles reports 10 files, 0 errors, 0 warnings, and none ignored. Every one is in eslint's population (--print-configresolves each).eslint.config.mjsnever enables type-aware linting (its own note near line 327: noparserOptions.project, no typed rules), so this diff cannot move the verdict on any untouched file. The repo-widepnpm lintis CI's.Changesets
@objectstack/metadataminor: the new subpath, and the artifact door's refusal speaking through the judge.@objectstack/metadata-protocolminorwith a BREAKING banner: an accept-set narrowing at the save door, graded like the boot loop's refusal of the same divergence. Its ADR-0087 disposition isnot-required (no-migration-prescription).@objectstack/objectqlpatch: the re-export.@objectstack/specpatch: comment only.src/**/*.zod.tsships as source, and the comment ships in theuiJavaScript output (measured: the new sentence is in 20distfiles; the old one is in none).The
Clause-②line above is the claim's, copied as dispatched. Byscripts/pm/clause2-line.mjs's own definitions, this diff both widens a public surface (the new@objectstack/metadatasubpath, with two functions and a type) and narrows an accept set (the save door). That reads asyes (narrowing). This is raised to the seat in the dev report; the line here is not changed by the dev.Acceptance notes
carrier: #21470(finding(metadata-protocol): the runtime save door accepts any metadata body whosenamediffers from its row name, and registers it under the body name (the every-type half of #21412) #21470 remains open). Container bodies stored before this change that already carry a divergentnamekeep registering under thatnameat boot (loadMetaFromDb, thenhydrateOverlayIntoRegistry).revertCommitandrollbackMetaItemre-persist stored versions throughrepo.restoreVersionwithout passing the save seam. The count of such rows is not measured.namethat differs from the row) is finding(metadata-protocol): the runtime save door accepts any metadata body whosenamediffers from its row name, and registers it under the body name (the every-type half of #21412) #21470's, and is not judged here.Generated by Claude Code