Repository navigation
Commit f5a7250
fix(spec): title the view.columns, view.tabs and view.sort repeater row properties (#19917)
Fixes #17507
Clause-②: no
## What this does
Studio renders a `type: 'repeater'` form field as a table. Its column
headers read `items.properties[k].title ?? k` off the JSON Schema
derived from the metadata type schema. None of the 25 row properties of
the three view repeaters carried a `title`, so the maker saw raw keys
(`field`, `width`, `isDefault`, `order`) in every locale, English
included. Each row property now carries a short English `.meta({ title
})`, in the convention the already-paid carriers use (`.describe(...)`
then `.meta({ title: 'Title Case' })`).
Three regions of `packages/spec/src/ui/view.zod.ts` change, and nothing
else in that file:
| carrier | schema | row properties | titles |
|---|---|---|---|
| `view.columns` | `ListColumnSchema` | 14 | Field, Label, Width (px),
Alignment, Hidden, Sortable, Resizable, Wrap Text, Renderer Type,
Pinned, Summary, Prefix, Primary Link, Click Action |
| `view.tabs` | `ViewTabSchema` | 9 | Name, Label, Icon, List View,
Filter, Display Order, Pinned, Default Tab, Visible |
| `view.sort` | the list arm's INLINE `{ field, order }` entry | 2 |
Field, Direction (the same titles as the shared `SortItemSchema`) |
The repeater-title ledger
(`packages/spec/src/kernel/repeater-item-titles.test.ts`) loses its last
three entries (`view:columns`, `view:sort`, `view:tabs`) and is now
EMPTY. Every repeater a form declares is fully titled, so a new untitled
one fails its own PR. The ledger's header comment now says so.
`ViewTabSchema` is also reused outside `view.tabs` (for example
`json-schema/ui/InterfacePageConfig.json` now carries `"Default Tab"`),
so those uses get the same titles. That is expected: it is one schema.
## Measured first, on the base
Base: `44ce049a8c`, which was `origin/main` when the worktree was cut. I
re-derived the counts through the ledger's own predicate:
`z.toJSONSchema(getMetadataTypeSchema('view'), { unrepresentable: 'any',
io: 'input' })`, with union arms kept as separate candidates and the
property node read before its `$ref` target. The derivation helpers were
copied from the ledger file into a scratch script. No source regex was
used.
| carrier | authorable | untitled before | untitled after |
|---|---|---|---|
| view:columns | 14 | 14 | 0 |
| view:tabs | 9 | 9 | 0 |
| view:sort | 2 | 2 | 0 |
These are the same 14 / 9 / 2 as the card's 2026-09-10 reading. I re-ran
the derivation with `io: 'output'`, which is what `/meta/types` serves.
It also reads 0 / 0 / 0 untitled after the change.
- **What "title" means here.** The ledger reads `raw?.title ??
prop?.title`: the property node first, then its `$ref` target. That is
the JSON Schema `title`, which zod emits from `.meta({ title })`.
`.describe()` feeds `description` only.
- **What the ledger records.** It records membership only (a `Set` of
carrier ids), not a count. A carrier in the set must have a non-empty
`untitled` list. A carrier outside the set must have an empty `untitled`
list. So "shrink" here means deleting the three ids.
## Both traps, handled
1. **The union arm.** `columns` is an object array on the list arm and
an INTEGER on the form arm. The ledger keeps every union arm as a
separate candidate and prefers the object-item array arm. The list arm's
`z.union([z.array(z.string()), z.array(ListColumnSchema)])` resolves to
the `ListColumnSchema` rows. Nothing here merges arms.
2. **The inline sort entry.** `view.sort` is titled on the INLINE entry
itself. The shared `SortItemSchema` was titled earlier and never reached
this entry.
## Firing control
The fix was committed first. I then mutated it and restored it through
`scripts/ablation-replace.mjs`:
- **Mutation:** `node scripts/ablation-replace.mjs --file
packages/spec/src/ui/view.zod.ts --anchor ".meta({ title: 'Display
Order' })" --delete -- pnpm --filter @objectstack/spec exec vitest run
--project local --maxWorkers=2 src/kernel/repeater-item-titles.test.ts`
- on disk: anchor count 1 to 0, blob `b4451833e711` to `a014831e9ce2`.
- **RED:** `view:tabs: these row properties have no .meta({ title }) ...
expected [ 'order' ] to deeply equal []`. Tests: 1 failed, 25 passed
(26).
- restored: blob equals HEAD (`b4451833e711`), and `git diff HEAD` is
empty.
- **Green leg:** the same test file on the restored tree at
`7a30204ebf`. Tests: 26 passed (26).
- **An earlier attempt was a no-op.** It used `--replacement` with text
that is a substring of the anchor. The tool refused it before the
command ran (the replacement count rose by 0) and restored the file. It
produced no reading.
## Downstream readers: the generators
| generator | result |
|---|---|
| `pnpm --filter @objectstack/spec build` (runs `gen:schema` and
`gen:openapi`) | no tracked file moved. The gitignored but published
`json-schema/ui/ListColumn.json` now carries the titles (`"title":
"Field"`, `"title": "Width (px)"` and the rest). |
| `pnpm --filter @objectstack/spec check:generated` | all 15 artifacts
up to date: `authorable-surface/` (keys, not titles), `api-surface/` and
`content/docs/references/**` are byte-identical |
| `pnpm i18n:extract` | 11 files written, zero diff |
| `pnpm check:i18n` (all nine bundle packages) | in sync |
No generated file moved. The titles do not reach the metadata-form
bundles, because `viewForm` enumerates no row children for these three
repeaters and `os i18n extract` keys off declared children. See
Acceptance notes.
## Changeset
`.changeset/17507-view-repeater-row-titles.md` is `@objectstack/spec:
patch`. My reading:
- **Published bytes move, so `skip-changeset` is wrong.**
`@objectstack/spec` publishes `dist`, `json-schema` and
`src/**/*.zod.ts` (its `files[]`). After the build, `dist/ui/index.mjs`
carries `Renderer Type` (1 hit; 0 hits in `src` at the base; a nonsense
string reads 0).
- **Nothing an author can write moves.** No key or value is added,
removed or narrowed, and `authorable-surface/` is byte-identical. So
`Clause-②: no`, and a released-package fix takes `patch` (AGENTS.md
Post-Task Checklist step 3).
- **The precedent is split.** `#17506`'s PR read the same shape as
`patch` with `Clause-②: no`. `#17505`'s PR took `yes` / `minor` for the
same shape. This PR follows the claim and the newer reading.
## Verification
All runs are at `7a30204ebf` unless stated. Heavy runs went through
`scripts/pm/os-verify-lock.sh`, on a shared box.
- `pnpm --filter @objectstack/spec build`: green, at `69da9fed7e` (the
fix commit, where `check:generated` also ran; the only later commit adds
the changeset file) and again at `7a30204ebf`.
- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2`: 526 files, 15501 passed, 1 todo.
- `pnpm --filter @objectstack/spec exec vitest run --project repo
--maxWorkers=2`: 35 files, 602 passed.
- `pnpm --filter @objectstack/spec typecheck`: green (tsc, the scripts
program and the test layer).
- `pnpm --filter @objectstack/platform-objects exec vitest run
--passWithNoTests --maxWorkers=2`: 54 files, 883 passed. This includes
`repeater-row-properties.test.ts`, whose dark control ("view enumerates
no repeater row properties yet") still holds.
- **Consumers of the served titles** (the wire shape gains `title`
nodes):
- `@objectstack/metadata-protocol`
`protocol.meta-types-degenerate-derivation.test.ts` and
`protocol.meta-types-unauthorable-columns.test.ts`: 26 passed.
- `@objectstack/rest` `meta-types-schema-titles.test.ts`: 3 passed.
- Declared narrowing: the rest of those two packages' suites are left to
CI.
- **Gates.** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` was re-derived after the
changeset landed: 82 families, all run. The `--ran` reconciliation reads
"82 derived famil(ies) accounted for — 82 run, 0 NOT-MEASURED".
- Two gates first exited 3 and were re-run green after a rebuild.
`check:dual-build-cjs-loads` was missing some workspace dists.
`check:type-check-debt` saw stale spec and platform-objects entry
points, caused by this run's own ablation and extract.
- **ESLint, narrowed and proven.** `eslint --no-inline-config --format
json` over the two changed `.ts` files: 2 files, 0 errors, 0 warnings.
- Population: both files resolve a config under `--print-config`
(neither is ignored), and `pnpm lint` is `eslint .`.
- Invariance: the resolved `parserOptions` are `{ ecmaVersion: 'latest',
sourceType: 'module' }`, with no `project` or `projectService`. Linting
is therefore not type-aware, so this diff cannot change a verdict on any
untouched file.
## Acceptance notes
- **The branch is behind `origin/main`, without overlap.** `origin/main`
has moved 4 commits past the base. None of them touches these three
paths or any `packages/spec` generated artifact; the one `packages/spec`
change upstream is in `src/contracts/`. I did not merge main in; CI's
merge ref validates the joint tree.
- **The titles are English in every locale for now.** `viewForm`
declares no row children for `columns`, `sort` or `tabs`, so `os i18n
extract` emits no catalog key for them and
`resolveMetadataFormSchemaTitles` has nothing to overlay. The
row-property localisation pin
(`packages/platform-objects/src/apps/translations/repeater-row-properties.test.ts`)
scopes `view` out explicitly. The follow-on is to enumerate these
children in `view.form.ts` with translated leaves. That is outside this
card's file surface.
- `#17232` (the parent family) remains open.
---
_Generated by [Claude
Code](https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9282578 commit f5a7250
3 files changed
Lines changed: 59 additions & 43 deletions
File tree
- .changeset
- packages/spec/src
- kernel
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | | - | |
34 | | - | |
35 | | - | |
36 | | - | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
84 | | - | |
85 | | - | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | | - | |
90 | | - | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
91 | 87 | | |
92 | 88 | | |
93 | 89 | | |
94 | 90 | | |
95 | 91 | | |
96 | | - | |
97 | | - | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
98 | 96 | | |
99 | | - | |
100 | | - | |
101 | | - | |
102 | | - | |
103 | | - | |
| 97 | + | |
104 | 98 | | |
105 | 99 | | |
106 | 100 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1015 | 1015 | | |
1016 | 1016 | | |
1017 | 1017 | | |
1018 | | - | |
1019 | | - | |
1020 | | - | |
1021 | | - | |
1022 | | - | |
1023 | | - | |
1024 | | - | |
1025 | | - | |
1026 | | - | |
| 1018 | + | |
| 1019 | + | |
| 1020 | + | |
| 1021 | + | |
| 1022 | + | |
| 1023 | + | |
| 1024 | + | |
| 1025 | + | |
| 1026 | + | |
| 1027 | + | |
1027 | 1028 | | |
1028 | 1029 | | |
1029 | | - | |
| 1030 | + | |
| 1031 | + | |
1030 | 1032 | | |
1031 | 1033 | | |
1032 | 1034 | | |
1033 | | - | |
| 1035 | + | |
| 1036 | + | |
1034 | 1037 | | |
1035 | 1038 | | |
1036 | | - | |
| 1039 | + | |
| 1040 | + | |
1037 | 1041 | | |
1038 | 1042 | | |
1039 | | - | |
1040 | | - | |
| 1043 | + | |
| 1044 | + | |
| 1045 | + | |
| 1046 | + | |
1041 | 1047 | | |
1042 | 1048 | | |
1043 | 1049 | | |
| |||
1664 | 1670 | | |
1665 | 1671 | | |
1666 | 1672 | | |
1667 | | - | |
1668 | | - | |
1669 | | - | |
1670 | | - | |
1671 | | - | |
1672 | | - | |
1673 | | - | |
1674 | | - | |
1675 | | - | |
| 1673 | + | |
| 1674 | + | |
| 1675 | + | |
| 1676 | + | |
| 1677 | + | |
| 1678 | + | |
| 1679 | + | |
| 1680 | + | |
| 1681 | + | |
1676 | 1682 | | |
1677 | 1683 | | |
1678 | 1684 | | |
| |||
2706 | 2712 | | |
2707 | 2713 | | |
2708 | 2714 | | |
2709 | | - | |
2710 | | - | |
| 2715 | + | |
| 2716 | + | |
| 2717 | + | |
| 2718 | + | |
2711 | 2719 | | |
2712 | 2720 | | |
2713 | 2721 | | |
| |||
0 commit comments