Repository navigation
fix(rest)!: the /meta item and list reads serve a view stored with no label without an invented label - #24013
Conversation
…o label The pins for the read-side rule: a label-less stored view is served with no `label`, an authored or translated label is served, and the container read is unchanged. Changeset included; the implementation follows. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
…hout an invented label `translateView` answers `view.label ?? view.name` when the catalog carries no string for a view, so a view stored with no `label` was served one equal to its qualified name on `GET /meta/view/NAME` and `GET /meta/view`. The container read already dropped that fallback. The rule is now one module-private helper, `withoutInventedViewLabel`, which the item read, the list read and the container projection all call. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
A default `list` that restates a named list key for key collapses into that item (the composer's documented rule), so the control fixture now keeps the two apart. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
A label-less row and an authored or translated row now fail apart, so a reverse run shows which direction each one takes. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
`check:engine-double-contract --write` adds the one `findOne` double the new route-level pin declares; no other row moves. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 18 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 993d4cc77ea64c17bb385713966f296f8ab747b8 && git checkout 993d4cc77ea64c17bb385713966f296f8ab747b8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 19955fb73fe58d7971a1f0f1fc9c473f2dfdf179 203a8192f6219b165134efabaaae05730c803ab9 && git checkout -B drift-repro 19955fb73fe58d7971a1f0f1fc9c473f2dfdf179 && git merge --no-ff 203a8192f6219b165134efabaaae05730c803ab9
node scripts/docs-audit/affected-docs.mjs --json 19955fb73fe58d7971a1f0f1fc9c473f2dfdf179 |
Contract reviewServed-tier: Net diff read against merge base ① Derived judgments1. Which served documents change — RIGHT. Type
2. When 3. The container path keeps its answer byte-for-byte — RIGHT. Base 4. No other path serves the invented label — RIGHT. Non-test callers of 5. No legitimate consumer reads a served view's
6. 7. The pinned ledger row — RIGHT, gate-prescribed, not a loosening. One row added: 8. The pin file — RIGHT. 39 rows as reported: §1 8 cells (2 locales x 4 views) x 2 reads + 2 = 18, over a real ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT · #23967 ·
Seat answers to the flags the review escalated:
Landing: Generated by Claude Code |
Fixes #23967
Clause-②: no (narrowing)
What changes
GET /api/v1/meta/view/NAMEandGET /api/v1/meta/viewserved a view stored with nolabelas if it had one:labelequal to its qualified name (OBJECT.KEY). This happened on the standalone server and on the runtime dispatcher. The spec declares a view'slabeloptional with no default (packages/spec/src/ui/view.zod.ts,I18nLabelSchema.optional()). The value came from the read-side translator:translateViewanswersview.label ?? view.namewhen the catalog carries no string for the view. The view-container read already dropped that fallback (#22614); the item and list reads did not.The rule is now one module-private helper in
packages/rest/src/meta-item-read-gate.ts,withoutInventedViewLabel(input, translated). All threeviewsites call it:translateMetaDocument's single-view arm: the item read.translateMetaEnvelopewraps it, andRestServer.translateMetaItemdelegates to it.translateMetaList's per-element map: the list read.RestServer.translateMetaItemsdelegates to it, and the runtime dispatcher's/metadomain imports it.projectViewTranslation: container slots and the container's own strings. Its inline check is replaced by the helper, with the same semantics. The reverse run below shows the container answer is unchanged.The rule: a
labelequal to the view'sname, where the stored view carried none, is not served. An authored label is served as authored, including one equal to the name. A catalog translation is still served, for a view with a label and for one without. A catalog string equal to the qualified name cannot be told apart from the fallback, so it is dropped with it, as it already was on the container path. No export is added.packages/spec,rest-server.tsandruntime/src/domains/meta.tsare not touched.The console half is a separate card that the seat files on objectstack-ai/objectui when this lands:
viewTabLabel(view.label || view.name || view.id) needs its own human fallback. It is not addressed here, and the console tab keeps showing a machine name for such a view until then.Evidence
Live boot, BASE
adce19297vs HEAD (standaloneos dev)From a scratch directory outside the repo:
examples/app-todocompiled withos compile -o SCRATCH/objectstack.json, booted withos dev --artifact ./objectstack.json --fresh -p PORTand placeholder admin credentials. Recipe:POST /packages(201),PUT /meta/object/recheck_gate?package=com.qa.recheckwith label, plural label andsharingModel(200),PUT /meta/view/recheck_gate.defaultwith nolabel(200),PUT /meta/view/recheck_gate.namedwithlabel: "All gates"(200).en, andzh-CN: the same in all three)GET /meta/view/recheck_gate.defaultlabel: "recheck_gate.default"labelkeyGET /meta/view, elementrecheck_gate.defaultlabel: "recheck_gate.default"labelkeyGET /meta/view, elements whoselabelequalsname[recheck_gate.default][]GET /meta/view/recheck_gate.namedand its list element"All gates""All gates"GET /meta/view/recheck_gate.default/layers, overlay and effectivelabellabelCloud host shape, BASE vs HEAD
new HttpDispatcher(kernel), which is whatcreateHonoAppbuilds, loaded from the built@objectstack/runtimedist. That dist imports the built@objectstack/rest. The protocol double serves three stored views: label-lessdefault, authorednamed, and label-lesstagged, which thezh-CNcatalog translates.default/tagged, item and listlabel= own namelabelkeyzh-CN:default, item and listlabel: "recheck_gate.default"labelkeyzh-CN:tagged/named已标记/全部闸门已标记/全部闸门labelkey (nothing is translated)labelkeyThe last row is the PM's mechanism assumption 2: with no resolvable locale, no other layer adds the label.
Pins:
packages/rest/src/meta-view-label-not-invented.test.ts, 39 rowsGET /api/v1/meta/:type/:nameandGET /api/v1/meta/:type, over a realSchemaRegistryand the realObjectStackProtocolImplementation, with the views seeded as stored rows. It has one row per locale and view: label-less (no key), authored, catalog-translated label-less, and authored equal to name. It also covers the card's read with no Accept-Language and no catalog.translateMetaEnvelopeandtranslateMetaList, the functions the dispatcher's/metadomain imports, with the same rows, a bare array and the{ type, items }envelope. It also covers no catalog, no mutation, and no locale (the same document is returned).Reverse verification and ablation (one-shot, nothing left in the tree)
The subject is reached by relative import (
./meta-item-read-gate.js, and./rest-server.jsimports it relatively), so it resolves tosrc/; no rebuild sits between legs. Each leg below was proven on disk. The reverse leg's blob equals the BASE blob. Each ablation went throughscripts/ablation-replace.mjs: anchor 1 to 0, blob changed. Each restore was proven: blob equals the HEAD blob andgit diff HEADis empty.return translated)return out)projectViewTranslation)Suites
These ran post-merge at
6ff4da0d8. The one later commit adds only the ledger row below.pnpm --filter @objectstack/rest typecheck: exit 0.tsc -p tsconfig.test.json --listFilesincludes the new file, with 0error TS.pnpm --filter @objectstack/rest test(local): 288 files, 5941 passed, 326 skipped.pnpm --filter @objectstack/rest test:repo(repo): 5 files, 250 passed, 11 skipped.@objectstack/runtime, the 25src/domains/meta-*.test.tsfiles of thelocalproject: 513 passed. Therepoproject'smeta-list-projection-parity.test.ts: 739 passed. Runtime aliases@objectstack/restto its source.pnpm exec eslint --no-inline-configover the two changed TS files: exit 0.Gates, at the final HEAD
203a8192fnode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 72 commands from merge based132140eb.203a8192f, each with its exit code captured before any pipe, and all 72 exit 0.--ranreconciliation over that record reads: 72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero: all 72 recorded an exit code).check:adr-0087-registration(the changeset holdsnot-required (no-migration-prescription)),check:changeset-no-major,check:empty-changeset,check:engine-double-contract(1017 pinned),check:test-source-alias,check:cross-package-test-inputs,check:nul-bytesandcheck:dual-build-cjs-loads(110 require entry points across 68 packages load).Surface note
The claim's file surface was the source file, one test file and one changeset. A fourth file is needed:
scripts/engine-double-contract.pinned.jsongains one row (packages/rest/src/meta-view-label-not-invented.test.ts,findOne, pinned 1).check:engine-double-contractrequires it for the new pin's fake engine and prescribes--write. The regeneration printed 0 rows lost; no other row moves.Acceptance notes
object,app,dashboard,dataset,pageandpicklisttranslators servelabelonly from the catalog or the document; none falls back toname.actiondoes:resolveActionLabelanswersaction.nameandtranslateActionalways writeslabel. ButActionSchemadeclareslabelrequired (packages/spec/src/ui/action.zod.ts:959), so a schema-valid stored action always carries one. No reach was measured, so this is not filed.resolveViewLabel'sview.label ?? view.name(packages/spec/src/system/i18n-resolver.ts:481) is unchanged.translateViewis called by this module's three sites and by the public-form route inrest-server.ts. That route takes only the translatedconfigand keeps its own label reading.packages/cli/src/utils/i18n-extract.tsnames both functions in docblocks only, with no import.63d00da) calls neither.domain:specground; it is noted and not filed.GetMetaItemResponseSchema.item: z.unknown()andGetMetaItemsResponseSchema.items: z.array(z.unknown()).packages/clienttype requires a served view'slabel, and no test asserts that a served view'slabelequals its name.serve-view-container-readintegration test asserts authored labels.view-container-cross-package-defaulttest compares the item and list reads, which now take the same rule.sharingModel. Without it the PUT answers 422security-owd-unset, an authoring rule that postdates the card's run.node_modulesloggedruntime module load FAILED ... Cannot find package '@objectstack/spec', and the todo app's hook was refused at boot. This was seen only in this scratch setup, and it does not touch/meta. Whether an artifact is meant to boot without its dependencies is not established here; noted, not filed.Generated by Claude Code