From 6260133d638ea5b62333108179c1dc6aa355966d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 05:55:30 +0000 Subject: [PATCH 1/2] fix(spec): title `SelectOptionSchema`'s six row properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/spec/src/data/field.zod.ts | 12 +++++------ .../src/kernel/repeater-item-titles.test.ts | 20 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index 8bf5f106193..7bee63cfd9b 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -335,8 +335,8 @@ export const SelectOptionSchema = lazySchema(() => strictObject({ // no existing pointer is shadowed (`alias-integrity.test.ts`, #7889). guidanceSets: [SELECT_OPTION_EDITABILITY_GUIDANCE], }, { - label: z.string().describe('Display label (human-readable, any case allowed)'), - value: SystemIdentifierSchema.describe('Stored value (lowercase machine identifier)'), + label: z.string().describe('Display label (human-readable, any case allowed)').meta({ title: 'Label' }), + value: SystemIdentifierSchema.describe('Stored value (lowercase machine identifier)').meta({ title: 'Value' }), /** * Optional secondary text for the option (objectui#6153, inheriting the * objectui#6140 ruling frame — maintainer 2026-08-25: a key that is @@ -356,9 +356,9 @@ export const SelectOptionSchema = lazySchema(() => strictObject({ * spelling on its own metadata type (maintainer ruling 2026-09-02 on * objectui#6153). */ - description: z.string().optional().describe('Optional secondary/help text for this option. Lookup option search matches it in addition to the label; renderers may show it as supporting text.'), - color: z.string().optional().describe('Color code for badges/charts'), - default: z.boolean().optional().describe('Is default option'), + description: z.string().optional().describe('Optional secondary/help text for this option. Lookup option search matches it in addition to the label; renderers may show it as supporting text.').meta({ title: 'Description' }), + color: z.string().optional().describe('Color code for badges/charts').meta({ title: 'Color' }), + default: z.boolean().optional().describe('Is default option').meta({ title: 'Default' }), /** * Per-option visibility predicate (CEL) — the option is offered only when this * evaluates TRUE. Omit = always available. Evaluated against the live `record` @@ -384,7 +384,7 @@ export const SelectOptionSchema = lazySchema(() => strictObject({ * rule-validator evaluates the picked value's `visibleWhen`) — hiding it in the * dropdown alone is bypassable. */ - visibleWhen: EvaluatedExpressionInputSchema.optional().describe("Per-option visibility predicate (CEL) — option is offered only when TRUE (else omitted). Env: the live `record` plus the host predicate scope, which binds `current_user`. The one VISIBILITY predicate the SERVER also enforces — the rule validator refuses a write of a value whose predicate is false — so a user-gated CHOICE belongs here. e.g. P`record.country == 'cn'` or P`'admin' in current_user.positions`"), + visibleWhen: EvaluatedExpressionInputSchema.optional().describe("Per-option visibility predicate (CEL) — option is offered only when TRUE (else omitted). Env: the live `record` plus the host predicate scope, which binds `current_user`. The one VISIBILITY predicate the SERVER also enforces — the rule validator refuses a write of a value whose predicate is false — so a user-gated CHOICE belongs here. e.g. P`record.country == 'cn'` or P`'admin' in current_user.positions`").meta({ title: 'Visible When' }), })); /** diff --git a/packages/spec/src/kernel/repeater-item-titles.test.ts b/packages/spec/src/kernel/repeater-item-titles.test.ts index 32aba901caa..c6579a27080 100644 --- a/packages/spec/src/kernel/repeater-item-titles.test.ts +++ b/packages/spec/src/kernel/repeater-item-titles.test.ts @@ -84,19 +84,19 @@ const FORMS: ReadonlyArray = [ * Carriers still owed titles, as measured on `origin/main` at * e758131b3900eb13260f03643e295ca6d625c42b. SHRINK-ONLY — see the header. * - * The remaining `view.*` / `field.*` entries were fenced out of #17232's - * round by in-flight PRs on their carrier files (#17360 `view.zod.ts`, - * #17477 `field.zod.ts` — `field.options` and `object.fields.options` are - * the same `SelectOptionSchema`). This pin OBSERVES them without editing - * them, which is why the set below is the rest of the class and not the - * slice one PR could reach. + * The remaining `view.*` entries were fenced out of #17232's round by an + * in-flight PR on their carrier file (#17360 `view.zod.ts`). This pin + * OBSERVES them without editing them, which is why the set below is the rest + * of the class and not the slice one PR could reach. * - * `dashboard:widgets` and `dashboard:globalFilters` were paid by #17505 and - * DELETED from this set — a paid debt leaves no entry behind. + * `dashboard:widgets` and `dashboard:globalFilters` were paid by #17505, + * `field:options` and `object:fields.options` by #17506 — one edit for both, + * because the two carriers resolve to the SAME `SelectOptionSchema` object + * (`FieldSchema.options` is `z.array(SelectOptionSchema)` and `object.fields` + * is a record of that same `FieldSchema`). All four are DELETED from this set + * — a paid debt leaves no entry behind. */ const LEDGER: ReadonlySet = new Set([ - 'field:options', - 'object:fields.options', 'view:columns', 'view:sort', 'view:tabs', From cff031c8db3ca3cb133df93d4d2704a333951689 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 06:26:18 +0000 Subject: [PATCH 2/2] chore(changeset): patch for the select-option row titles `@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 --- .changeset/17506-select-option-row-titles.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .changeset/17506-select-option-row-titles.md diff --git a/.changeset/17506-select-option-row-titles.md b/.changeset/17506-select-option-row-titles.md new file mode 100644 index 00000000000..861d12bcdc6 --- /dev/null +++ b/.changeset/17506-select-option-row-titles.md @@ -0,0 +1,13 @@ +--- +"@objectstack/spec": patch +--- + +`SelectOptionSchema`'s six row properties carry a JSON Schema `title`, so Studio's property panel stops printing raw machine keys as the column headers of a field's `options` table (#17506). + +Clause-②: no + +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 row property, so the fallback arm ran and the maker saw `label` / `value` / `description` / `color` / `default` / `visibleWhen` inside an otherwise translated panel — **in every locale, English included**. Titles are hard-coded English by design: `system/translation.zod.ts` states that a row property renders from `items.properties[k].title`, and `resolveMetadataFormSchemaTitles` only ever REPLACES a title that is already there, so an untitled property has no layer for a translation to overlay. + +- **One edit clears two carriers.** `field:options` and `object:fields.options` resolve to the *same* `SelectOptionSchema` object — `FieldSchema.options` is `z.array(SelectOptionSchema)` and `object.fields` is a `z.record(..., FieldSchema)` of that same `FieldSchema` — verified by object identity (`===`) against the schemas `getMetadataTypeSchema('field')` and `getMetadataTypeSchema('object')` actually return, with `FormSelectOptionSchema` as the firing control that the probe can tell two schemas apart. Both entries are deleted from the shrink-only `repeater-item-titles` ledger in the same change; `object.zod.ts` needed no edit. +- **Nothing the schema accepts or refuses moved.** `.meta({ title })` is presentation metadata: the generated `authorable-surface/` artifacts are byte-identical, and the pinned accept/refuse suites for this shape (`editability-boundary`, `visible-when-alias-guidance`, `form-select-option`, `evaluated-slot-population`) pass unchanged. +- **The form-view face inherits the titles for free.** `FormSelectOptionSchema` is a shape-level Omit that reuses the same property schema instances, so the five keys it keeps arrive titled too, and its `default`-refusal is untouched.