Commit dc0ab6a
Part of #19332
Flight G2a of ruling 5861442317.
Clause-②: no
## Status: draft, no open gap
The first round stopped at one red class guard,
`packages/spec/src/kernel/repeater-item-titles.test.ts` (#17232),
because its fix sat in a file the claim forbade. The seat amended claim
`5869305892` in place (its "Amended 2026-09-28T13:26Z" line).
`packages/spec/src/data/object.zod.ts` joined the surface for `.meta({
title })` on the item properties of `IndexSchema` and
`ObjectFieldGroupSchema` only. The 12 titles landed in `e8bdb1ad` (**Row
titles** below), and the guard is green.
## What
Two live keys that `ObjectSchema` declares had no form row, so an author
could reach them only through the Source tab. Each is now a `type:
'repeater'` row on the object form with hand-written sub-rows, as the
ruling says: 「**G2 (…) — hand-written curated sub-rows**, plus the three
nested `omit` rows under `fieldGroups` (the `[DEPRECATED → collapse]`
aliases, `object.zod.ts:1206-1210`) … `indexes.unique` offers `global` /
`organization` only.」 The four-locale catalogue rows are in this PR.
| key | form, section | sub-rows (face) | the row each sub-row copies |
|:--|:--|:--|:--|
| `object.fieldGroups` | `object.form.ts`, Basics, beside
`highlightFields` | `key`, `label` (required text); `icon` (text);
`description` (textarea); `collapse` (select: `none` / `expanded` /
`collapsed`); `visibleWhen` (`type: 'code'`, `language: 'expression'`) |
the repeater face is `object.form.ts` `fields.options` (declared,
labelled sub-rows). `key` / `label` / `icon` copy the plain text rows
`name` / `label` / `icon` in Basics; `description` copies Basics
`description`; `collapse` copies the `lifecycle.class` select (every
member is a spellable option value); `visibleWhen` copies
`fields.visibleWhen` (`object.form.ts`, same node type,
`EvaluatedExpressionInputSchema`) |
| `object.indexes` | `object.form.ts`, Advanced, beside `datasource` |
`name` (text); `fields` (`widget: 'string-tags'`, required); `unique`
(select: `global` / `organization` only) | `fields.options` repeater
face; `fields` copies the `highlightFields` row (a chip input over
`string[]`); `unique` is a declared select for the reason below |
Ledger: three nested `omit` rows at `object` / `fieldGroups` in
`metadata-form-zod-reconciliation.test.ts`, one per alias:
`defaultExpanded` (line 1206 on `main`), `collapsible` (1208),
`collapsed` (1210). Row shape: the `page` / `interfaceConfig` /
`sourceView` row at the top of the ledger, which is the existing nested
`omit` for a deprecated alias. `indexes` needs no ledger row: its other
two keys, `type` and `partial`, are `retiredKey()` tombstones and are
excused automatically.
Row titles: `object.zod.ts` gives both row schemas a JSON Schema `title`
on every property, 12 in all (**Row titles** below).
The help text states what the runtime does, and each claim was checked
against its reader:
- Group `icon` renders on the record detail page only. At the
`.objectui-sha` pin `f8a9d0fb`, plugin-detail `DetailSection` draws it;
plugin-form `fieldGroups.ts` does not copy it, because
`ObjectFormSection` declares no `icon`.
- Group `description` and `collapse` render on both the entry form and
the detail page (`fieldGroups.ts` and `deriveFieldGroupDetailSections`).
- Group `visibleWhen` gates the entry form's whole group
(`projectSectionDivider`). The help text claims only the form.
- A group `key` that is not snake_case, or is duplicated, is refused by
the parse (probe below). A field whose `group` names no declared key is
ungrouped (`deriveFieldGroupLayout`).
- `indexes`: `SqlDriver.syncTableIndexes` is additive only, so a sync
never drops an index. An unset `name` becomes `buildIndexName`
(`idx_TABLE_COLUMNS`, `uniq_…` for a unique index).
## Where a misspelt field name is refused, read from the code
The ruling's 「a misspelling is refused loudly at parse」 does not hold
here, the same as for G1b's lists.
- `indexes[].fields`: **no authoring door judges it.**
- The Zod parse accepts `{ fields: ['statsu'] }` on an object that
declares only `status` (probe below).
- `validate-object-field-refs.ts` excludes the list by design, as a
storage question for the registration path. No other lint rule, and so
neither the publish door nor `os validate`, reads it for existence.
- At sync, `syncDeclaredIndexes` skips the whole index and logs
`skipping declared index … column(s) not materialized` at `warn`.
- The help text says exactly that: "Nothing checks them when you save or
publish: a name that is not a stored column makes the SQL driver skip
the whole index, with a warning in the server log."
- `fieldGroups` has no field-name list. A field joins a group through
its own `group` key, so there is no name here for a misspelling to hide
in.
## `unique`: how the row treats a stored `true` or `false`
- The node is `boolean | 'global' | 'organization'`, default `false`.
- A derived face takes the union's first arm, the boolean, and renders a
switch that writes the deprecated bare `true`. No option can spell a
boolean either: `FormSelectOptionSchema` refuses `value: true` (probe).
So the row is a select that declares exactly the two scopes.
- **What it can write:** only `global` and `organization`. Both parse
today, and neither is the spelling protocol 18 refuses.
- **A stored `true` or `false`** is left untouched on save. objectui's
`RepeaterField.update` at the pin writes `{ ...row, ...patch }`, so
`unique` changes only when the author picks a scope.
- **Display:** the select renders `String(value)`, and no option matches
`'true'` or `'false'`. Reading the Radix select, the trigger is then
blank and does not show the placeholder. This is a code reading only,
with no browser run. The help text therefore claims only the merge: "The
deprecated bare true (it means global) is not offered; an index that
carries it keeps it until you pick a scope."
- **The same choice already exists in objectui's own embedded index
editor** at the pin (`EmbeddedItemEditor.tsx` `FALLBACK_SCHEMAS.index`):
it offers `global` / `organization` for a new index and leaves a legacy
boolean alone.
## Row titles (landed by claim amendment)
- **The guard.** `repeater-item-titles.test.ts` (#17232) is the class
guard for "a repeater's property-panel table shows raw keys". It derives
each repeater's row schema from `z.toJSONSchema(…, { io: 'input' })` and
requires a `title` on every authorable row property. That includes the
three deprecated aliases, because the guard reads the schema, not the
form. Its ledger says 「⛔ Never add an entry to LEDGER to make this file
green」.
- **The change (`e8bdb1ad`).** 12 `.meta({ title })` calls in
`packages/spec/src/data/object.zod.ts`, the ones the first round
measured:
- `IndexSchema`: `name` → 'Name', `fields` → 'Fields', `unique` →
'Unique';
- `ObjectFieldGroupSchema`: `key` → 'Key', `label` → 'Label', `icon` →
'Icon', `description` → 'Description', `visibleWhen` → 'Visible When',
`collapse` → 'Collapse', `defaultExpanded` → 'Default Expanded',
`collapsible` → 'Collapsible', `collapsed` → 'Collapsed'.
- The titles equal the labels this PR declares, which is the
`view.form.ts` row-property convention.
- **Nothing else moved.** Stripping those 12 exact calls from the new
`object.zod.ts` yields the previous commit's file byte for byte. The
guard and the reconciliation test are green together (2 files, 85
tests). `check:generated` reads "All 15 generated artifacts are up to
date" on the merged head, so nothing regenerated and no accept set
moved. The changeset now names the titles as part of the served JSON
Schema and stays `Clause-②: no`.
## Residue of the reconciliation gate (dispatch assumption 1)
The gate's own helper block was copied verbatim into a scratch probe
that was never committed. At base, that block is lines 1-808, prefix
sha256 `91478ba8d05c70b7…`, the same prefix G1b read. The probe ran in a
scratch worktree detached at the base `e4d3f2ca`. Residue = offerable
root keys − offered − root `omit` rows, per type, with `view` apart.
Controls, asserted inside the probe:
- lit: `name` is offered by 17 of 17 forms;
- dark: `object.zzFabricated19332G2a` and `object.name` are in no
residue;
- named lit key: `object.activityMilestones` (G2b) is in the residue on
both trees.
| tree | residue | per type | view |
|:--|:--|:--|:--|
| base `e4d3f2ca` | **6** | object 5, field 1 | 42 |
| this branch's form + ledger (from `a6f19eb2`, `git diff` against it
empty) | **4** | object 3, field 1 | 42 |
Removed: `object.fieldGroups`, `object.indexes`. Added: none. The four
left are the G2b keys: `object.activityMilestones`,
`object.publicSharing`, `object.userActions`, `field.inlineColumns`.
Nested reading on the same tree, through the gate's own
`reconcileNestedLists`:
- With the three new rows removed from the ledger, `object.fieldGroups`
reads `zodOnly = [collapsed, collapsible, defaultExpanded]`. With them,
it reads `[]`.
- `object.indexes` reads `zodOnly = []` and `unanchored = false`, with
keys `fields, name, partial, type, unique` and retired `partial, type`.
So the three rows are exactly what the gate needs.
Parse probe on the same tree:
- `IndexSchema`: `'global'` and `'organization'` pass; `true` and
`false` pass (17.x); `'tenant'` fails with `invalid_union`.
- `ObjectSchema`:
- a misspelt index column parses (`true`);
- a group carrying `collapse: 'collapsed'` beside `collapsible: true,
collapsed: false` keeps `'collapsed'`;
- an alias-only group (`defaultExpanded: false`) derives `'collapsed'`;
- a key `'Bad Key'` and a duplicate key are both refused.
## Pins moved (measured, mechanical)
| file | pin | from → to | why |
|:--|:--|:--|:--|
| `object-collapsed-sections-echo-decisions.test.ts` | collapsed-section
leaves / `advanced` | 61 → 69 / 52 → 60 | `indexes` + 3 sub-rows, 8
leaves |
| same | open-section leaves | 100 → 114 | `fieldGroups` + 6 sub-rows,
14 leaves |
| `object-lifecycle-panel-echo-decisions.test.ts` | translated `.label`
control | 633 → 644 on the old base; **634** after the merge | 11 new
row labels, per locale. The `origin/main` merge brought #20357's move of
the same pin to 623 (its `view` `tabs` repeater left, 10 labels), and
the conflict was resolved by stacking both intents: 633 − 10 + 11 = 634
|
| `packages/spec/src/data/field-rows-option-description.test.ts` |
`icon` inputs on the object form | 1 → 2 | the `fieldGroups.icon`
sub-row. Outside the claim's named surface: a population pin the new row
moves mechanically. The pin now names both inputs by where they sit, and
the options repeater's no-`icon` assertions are untouched |
No lint census pin moved:
`packages/lint/src/validate-predicate-path-refs.test.ts` passes
unchanged (1 file, 54 tests).
## Verification
Test runs went through `scripts/pm/os-verify-lock.sh`. Real lines. The
suites ran on the merged head `215603c8`, whose tree equals the final
head `c22fc1e2` except the changeset prose:
| run | head | result |
|:--|:--|:--|
| `pnpm --filter @objectstack/spec test` | `215603c8` | `Test Files 564
passed (564)` · `Tests 16641 passed \| 1 todo (16642)` |
| `pnpm --filter @objectstack/spec test:repo` | `215603c8` | `Test Files
37 passed (37)` · `Tests 684 passed (684)` |
| `pnpm --filter @objectstack/platform-objects test` | `215603c8` |
`Test Files 55 passed (55)` · `Tests 911 passed (911)`. The echo pins
hold after the merge: 634, 69 / 60 and 114 |
| `repeater-item-titles.test.ts` +
`metadata-form-zod-reconciliation.test.ts` | `e8bdb1ad` | 2 files, 85
tests passed |
| `pnpm --filter @objectstack/spec typecheck` | `215603c8` | exit 0;
`check:test-typecheck: OK — … 53 file(s) / 251 error(s) / 138 pinned
signature(s) held` |
| `pnpm check:i18n` (after the closure build it names, 59 tasks, at
`215603c8`) | `215603c8` | `check-i18n-bundles: OK (9 package(s) — all
bundles in sync, no undeclared authoring keys)`. The textually merged
catalogues equal a fresh extract |
| `pnpm --filter @objectstack/spec check:generated` | `215603c8` | `All
15 generated artifacts are up to date` |
| `pnpm --filter @objectstack/platform-objects typecheck` | `92fb68da` |
exit 0; `check:test-typecheck: OK — … 1 file(s) / 3 error(s) / 2 pinned
signature(s) held` |
| lint `src/validate-predicate-path-refs.test.ts` | `92fb68da` | 1 file,
54 tests passed |
| cli unit `test/i18n-coverage.test.ts`,
`test/i18n-duplicate-demand.test.ts` | `92fb68da` | 2 files, 27 tests
passed |
| metadata-protocol `src/protocol.meta-types-*.test.ts` | `92fb68da` | 4
files, 58 tests passed |
Catalogues: `node scripts/check-i18n-bundles.mjs --write` regenerated
the 22 `en` leaves. The 66 translated leaves were then authored in
zh-CN, ja-JP and es-ES, with no en echo. A second `--write` kept every
translated value and left no source-hash row, net zero.
Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 86 commands at the final head
`c22fc1e2`. That is the first round's 85 plus
`check:skill-identifier-liveness`, which `object.zod.ts` brings in. All
86 ran on that head, each exit code went to disk before it was read, and
every first run exited 0. `--ran` reports: `86 derived famil(ies)
accounted for — 86 run, 0 NOT-MEASURED`.
No ablation. This PR adds rows, three ledger rows and pin moves, and no
guard. The nested with/without reading above is the measurement that the
ledger rows are load-bearing.
## Acceptance notes
- **objectui hides this `fieldGroups` row on the Studio object edit
page.** At the pin, `ResourceEditPage.tsx` `CANVAS_OWNED_KEYS.object =
['fields', 'fieldGroups']`, because the form designer owns both. The row
still reaches every other consumer of the served form: `getMetaTypes()`,
the catalogues, and the reconciliation direction. The ruling chose the
row knowing the designer edits groups. Carrier: none.
- **objectui's `ObjectGroupInspector` comment is stale.** It says a
group's `icon` and `description` have no consumer, but `DetailSection`
renders both at the pin. It is only a comment. Carrier: none.
- **`fieldGroups.visibleWhen` shares `CodeWidget`'s envelope bound.** A
stored ADR-0089 envelope shows as `[object Object]`, and the first edit
overwrites it. Carried by objectui#10963, the class fix for every `type:
'code'` expression row.
- **The `unique` select cannot be cleared.** Once a scope is picked,
making the index non-unique again means removing and re-adding the
entry, or editing the source. This is a generic select bound. Carrier:
none.
- **Stored `true` / `false` display is not browser-run.** That it shows
blank is a reading of the Radix select, not a measurement.
- **`origin/main` was merged once, with `scripts/pm/os-regen-merge.sh`,
at `6e3e5462`**, because #20357 had moved the catalogues and the
lifecycle pin. The only conflict was that pin (resolved above). No
os-regen path needed main's side, and the rebuild regenerated nothing.
`origin/main` has moved since, but not onto any file in this diff, so it
was not merged again.
## Out-of-scope finding (the seat folds it into #20432's family)
- **class c · reach: the save door, measured.** `ObjectSchema` parses an
index column that names no field.
- The publish door and `os validate` have no rule for it.
- The SQL driver skips the whole index at sync and logs it at `warn`.
For a `unique` index, that leaves a declared constraint unenforced while
the system looks normal. The sibling duplicate-row skip in the same
function logs at `error` through `logDurabilityFailure`.
- Likely family: #20432 (field-name reference integrity).
- Dedupe words: `indexes fields unknown column`, `declared index skipped
column not materialized`, `index field reference integrity`, `unique
index not enforced warn`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent acd0095 commit dc0ab6a
11 files changed
Lines changed: 350 additions & 19 deletions
File tree
- .changeset
- packages
- platform-objects/src/apps/translations
- spec/src
- data
- system
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
Lines changed: 44 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
75 | 103 | | |
76 | 104 | | |
77 | 105 | | |
| |||
269 | 297 | | |
270 | 298 | | |
271 | 299 | | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
272 | 316 | | |
273 | 317 | | |
274 | 318 | | |
| |||
Lines changed: 44 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
75 | 103 | | |
76 | 104 | | |
77 | 105 | | |
| |||
269 | 297 | | |
270 | 298 | | |
271 | 299 | | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
272 | 316 | | |
273 | 317 | | |
274 | 318 | | |
| |||
Lines changed: 44 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
75 | 103 | | |
76 | 104 | | |
77 | 105 | | |
| |||
269 | 297 | | |
270 | 298 | | |
271 | 299 | | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
272 | 316 | | |
273 | 317 | | |
274 | 318 | | |
| |||
Lines changed: 11 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
900 | 900 | | |
901 | 901 | | |
902 | 902 | | |
903 | | - | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
904 | 909 | | |
905 | 910 | | |
906 | | - | |
| 911 | + | |
907 | 912 | | |
908 | 913 | | |
909 | 914 | | |
| |||
917 | 922 | | |
918 | 923 | | |
919 | 924 | | |
920 | | - | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
921 | 929 | | |
922 | 930 | | |
923 | 931 | | |
| |||
Lines changed: 6 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1132 | 1132 | | |
1133 | 1133 | | |
1134 | 1134 | | |
1135 | | - | |
| 1135 | + | |
| 1136 | + | |
| 1137 | + | |
| 1138 | + | |
| 1139 | + | |
| 1140 | + | |
1136 | 1141 | | |
1137 | 1142 | | |
1138 | 1143 | | |
| |||
0 commit comments