fix(spec): title SelectOptionSchema's six row properties — clears both repeater ledger carriers - #19257
Conversation
Studio renders a `type: 'repeater'` form field as a table whose column
headers read `items.properties[k].title ?? k` off the JSON Schema derived
from the metadata type schema. `SelectOptionSchema` carried no `title` on
any of its six row properties, so the fallback arm ran and the property
panel printed the raw machine keys — in every locale, English included.
Add `.meta({ title })` to `label`, `value`, `description`, `color`,
`default` and `visibleWhen`, and delete this carrier's two entries from
the shrink-only `repeater-item-titles` ledger. One edit clears both:
`field:options` and `object:fields.options` resolve to the SAME
`SelectOptionSchema` object — `FieldSchema.options` is
`z.array(SelectOptionSchema)` and `object.fields` is a record of that same
`FieldSchema` — verified by object identity, not assumed.
Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-authored-by: Claude <noreply@anthropic.com>
`@objectstack/spec` publishes `dist/` and `src/**/*.zod.ts`, and both now
carry the six `.meta({ title })` calls, so published bytes move and a
changeset is owed.
Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 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 — 136 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 fda6e9dddf70f4aca69d4f7dea84320ab0a73a0b && git checkout fda6e9dddf70f4aca69d4f7dea84320ab0a73a0b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cb005e06aeae13bcedbea9b3827c89de3c6833a4 cff031c8db3ca3cb133df93d4d2704a333951689 && git checkout -B drift-repro cb005e06aeae13bcedbea9b3827c89de3c6833a4 && git merge --no-ff cff031c8db3ca3cb133df93d4d2704a333951689
node scripts/docs-audit/affected-docs.mjs --json cb005e06aeae13bcedbea9b3827c89de3c6833a4 |
Contract reviewServed-tier: Reviewed on a detached worktree at the head above (merge-base with ① Derived judgments1. The ledger still bites — both arms re-fired, predicate unchanged.
2. The two-carrier identity claim — re-derived, holds, and the probe can say no. A probe walking zod internals (
3. Scope and
4. The six titles are right, not merely present. Each read against the property it labels:
② Semver level
③ Boundary flags
The unfiled observation — two api-surface-declarations readingsThe implementer surfaced two readings of
FindingsBLOCKING: none. Nothing in the diff, the ledger, the identity claim, the accept sets or the changeset level fails its check, and I am not manufacturing balance to say otherwise. Noted (prose only; no code change asked, none blocks):
Reviewed at 2026-09-20T06:51Z. CI readings quoted here were read at 2026-09-20T06:45:27Z (the latest job completion in that read). Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #17506
Clause-②: no
Studio renders a
type: 'repeater'form field as a table whose column headers readitems.properties[k].title ?? koff the JSON Schema derived from the metadata type schema.SelectOptionSchema(packages/spec/src/data/field.zod.ts) carried notitleon any of its six row properties, so the fallback arm ran and the maker sawlabel/value/description/color/default/visibleWheninside an otherwise translated panel — in every locale, English included. Titles are hard-coded English by design:system/translation.zod.tsstates that a row property renders fromitems.properties[k].title, andresolveMetadataFormSchemaTitlesonly ever REPLACES a title that is already there, so an untitled property has no layer for a translation to overlay. Triage refused the i18n route by name; this is not routed through translations.Two halves, both here:
.meta({ title })on each of the six row properties —Label,Value,Description,Color,Default,Visible When.packages/spec/src/kernel/repeater-item-titles.test.ts.One schema, two carriers — verified, not assumed
field:optionsandobject:fields.optionsresolve to the sameSelectOptionSchemaobject. Measured by object identity (===) against the schemas the ledger itself derives from, not by structural resemblance:The last line is the firing control: the probe can tell two schemas apart, so the four
truereadings are readings and not a stuck predicate. ⇒packages/spec/src/data/object.zod.tsneeded no edit and was not touched; it reaches the option shape only through itsFieldSchemaimport. The declared file surface held.Re-derived on today's
origin/main, not inherited from the cardThe ledger's own derivation, replayed over all 15
*.form.tsexports (22 carriers):field:optionsrow propertieslabel,value,description,color,default,visibleWhen; all 6 untitledobject:fields.optionsrow propertiesview:columns(14),view:sort(2),view:tabs(9)The 17 already-green carriers are the lit control beside the zeros, and the three
view:*carriers are the dark control: they stay exactly as untitled as they were, which is what an edit scoped toSelectOptionSchemamust look like.The ledger was not weakened — both of its arms were made to fire
Fix committed first, then mutated on disk through
scripts/ablation-replace.mjs(anchor must hit; the write is proven by blob-hash change, never by an exit code), and restored with the restore proven by blob hash againstHEADplus an emptygit diff HEAD.Ablation 1 — delete one title.
.meta({ title: 'Color' })removed fromfield.zod.ts; blob7bee63cfd9b6→5c4d8131a8a9. Predicted direction: both carriers red on exactlycolor, because neither sits in the ledger any more. Observed:Ablation 2 — put a paid entry back.
'view:columns'inLEDGERreplaced with'field:options'; blobc6579a27080a→2c450c99382c. This fires both directions of the exact ratchet at once:A ledger nobody has seen red on this carrier would not be evidence it is holding; it has now been seen red on this carrier, in both directions.
Clause-② —
no, and measured.meta({ title })is JSON-Schema presentation metadata and a ledger row is a test; neither moves what any schema accepts. Two independent readings agree:check:authorable-surfaceis green with the generatedauthorable-surface/artifacts byte-identical — that artifact set IS "what the schema accepts", and it did not move. All 16 generated artifacts report up to date.editability-boundary,visible-when-alias-guidance,form-select-option,evaluated-slot-population(254 tests over 5 files).Changeset — owed, and why
@objectstack/specpublishesdistandsrc/**/*.zod.ts(itsfiles[]), and both carry the six new calls —dist/data/index.mjsreadsColor code for badges/charts").meta({ title: "Color" …, with a nonsense title string as the negative control reading 0. Published bytes move ⇒ apatchchangeset, notskip-changeset.Verification
Run at
cff031c8dbunless stated:pnpm --filter @objectstack/spec test— 500 files / 14642 tests passed.pnpm --filter @objectstack/spec typecheck— green.pnpm --filter @objectstack/spec check:generated— 16 of 16 artifacts up to date.pnpm --filter @objectstack/spec buildthencheck:api-surface/check:api-surface-declarations— green (5364 declarations, text unchanged).@objectstack/metadata-protocolmeta-types derivation tests (3 files / 35 tests) and@objectstack/restmeta-types-schema-titles.test.ts(3 tests) — green.scripts/pm/dispatch-gates.mjs --ranover this diff: 81 derived, 78 run, 3 NOT MEASURED, 0 unrun. The three arecheck:dual-build-cjs-loads,check:lean-entry-closureandcheck:type-check-debt, each exiting 3 (PREREQUISITE NOT MET — they need a whole-workspace build). Those are CI's farm, not a pass and not a finding.pnpm exec eslint --no-inline-configover the two changed source files: 2 files linted, 0 errors, 0 warnings. The narrowing is measured, not assumed: this repo runs oneeslint.config.mjswhich never enables type-aware linting for any file (noparserOptions.project, no typed rules — stated and positively controlled in that file's own header), so this diff cannot move the verdict on a file it does not touch.Acceptance notes
repeater-item-titles.test.tsderives withio: 'input'while the server'stoJsonSchemaSafetakes zod's default'output', and the two part onaction, whose output derivation is{}. The file documents this itself and calls the output-side hole a separate defect; nothing here changes it.view:columns,view:sortandview:tabsstay in the ledger. They areview.zod.ts's debt and that file is held by another PR.check:api-surface-declarationsreported "0 removed, 0 added, 140 reshaped" against apackages/spec/distproduced by a 13-packagepnpm --filter '…^...' buildrun, and reported "declaration text unchanged (5364 declarations)" against a dist produced by a standalonepnpm --filter @objectstack/spec buildof the identical source. The standalone reading is the one quoted above. The variable was not isolated, so this is recorded as an observation rather than filed.Generated by Claude Code