Skip to content

feat(spec): curated fieldGroups and indexes repeaters on the object form (#19332, flight G2a) - #20449

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-19332-g2a-fieldgroups-indexes
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-19332-g2a-fieldgroups-indexes

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

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 (spec: every repeater item schema except dashboard header.actions still has no JSON Schema title, so 21 property-panel tables render machine keys in every locale #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 feat(spec)!: retire the list view's own tabs key; named presets are listViews entries #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)


Generated by Claude Code

…orm (#19332, flight G2a)

The object form offers object.fieldGroups and object.indexes as repeaters
with declared sub-rows: the six canonical field-group keys, and the three
keys the SQL driver reads for an index, with unique offering only the
global and organization scopes. The three deprecated collapse aliases of
a field group get nested omit rows in the reconciliation ledger.

Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
…s repeaters

The 22 new metadata-form leaves are regenerated from the object form,
and authored in zh-CN, ja-JP and es-ES instead of left as English
fills.

Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
…e two repeaters; add the changeset

The object form's new fieldGroups (basics) and indexes (advanced)
repeaters move three measured population pins mechanically: the open
section leaves 100 -> 114, the collapsed section leaves 61 -> 69
(advanced 52 -> 60), and the translated-label control 633 -> 644.

Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
… field group's

The fieldGroups repeater's icon sub-row (a group header's Lucide icon)
is a second, different subject from the withdrawn options-repeater icon;
the pin now names both by where they sit.

Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
…rties

The fieldGroups and indexes repeaters' row schemas carry a title on every
authorable property, as the repeater-title class guard requires:
IndexSchema name / fields / unique and ObjectFieldGroupSchema's nine keys,
the three deprecated collapse aliases included. Titles only; no accept
set moves.

Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
Conflict in object-lifecycle-panel-echo-decisions.test.ts resolved by
stacking both intents on the translated-label control: 633 - 10 (the
retired view tabs repeater, #20301) + 11 (this flight's rows) = 634.

Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec, touching 7 documentable anchor(s).

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json b285508188ebe9cf14cd1ee621ea857f688a938b.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b285508188ebe9cf14cd1ee621ea857f688a938b → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 648bbfe62a884636466fcc9945ebee17e84f9a2e — the merge of head c22fc1e2f5e74f32d6bb64befe03dba065027ae5 into base b285508188ebe9cf14cd1ee621ea857f688a938b, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 648bbfe62a884636466fcc9945ebee17e84f9a2e && git checkout 648bbfe62a884636466fcc9945ebee17e84f9a2e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b285508188ebe9cf14cd1ee621ea857f688a938b c22fc1e2f5e74f32d6bb64befe03dba065027ae5 && git checkout -B drift-repro b285508188ebe9cf14cd1ee621ea857f688a938b && git merge --no-ff c22fc1e2f5e74f32d6bb64befe03dba065027ae5

node scripts/docs-audit/affected-docs.mjs --json b285508188ebe9cf14cd1ee621ea857f688a938b

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs b285508188ebe9cf14cd1ee621ea857f688a938b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c22fc1e2f5e74f32d6bb64befe03dba065027ae5
Local-runs: none

Inputs: card #19332 (body + all 19 comments), PR #20449 (body, 11-file list, net diff vs main at the head), the head's check-runs. The API diff and git diff 6e3e5462..c22fc1e2 carry identical changed lines (358 each, same 11 paths, +350/−19). Branch commits: a6f19eb2 → 41b21eb1 → 1a3473f6 → 92fb68da → e8bdb1ad → 215603c8 (merge of 6e3e5462) → c22fc1e2; no rebase. objectui read at the head's .objectui-sha f8a9d0fb in the sibling checkout.

Check-runs on the head (the gate verdicts): 42 completed — 37 success, 5 skipped (Auto Label, Check PR Size, Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)), 0 failures. All five skips are named in scripts/pm/check-expected-skips.mjs at the head (read, not run). Green includes Check Changeset, Lint & Repo Gates (which runs check:authorable-surface, check:api-surface, check:skill-identifier-liveness, check:i18n, check:generated --reconcile-only), Test Core 1–6 and the aggregate, Build Core, Spec property liveness, Type Check ×4, Governed Surface Queue Guard, all three claim guards.

git merge-tree --write-tree origin/main c22fc1e2 (origin/main = 2304b160, three commits past the merge base 6e3e5462): clean, tree 51091dc55a78ddd71cb7b8e51dca4146e4ff13a4, exit 0. object.zod.ts on origin/main vs the head is still exactly the 12 title lines, so main has not moved that file.

① Derived judgments

  1. object.form.ts — two type: 'repeater' rows with hand-written sub-rows. Right.

    • fieldGroups, Basics, directly after searchableFields (beside highlightFields), six sub-rows. Each face matches its ObjectFieldGroupSchema node at the head: key text+required ↔ z.string().regex(snake_case) required; label text+required ↔ z.string() required; icon text ↔ optional string; description textarea ↔ optional string; collapse select over none/expanded/collapsed ↔ z.enum([...]).default('none'), all three members spellable option values (identifiers.zod.ts:105 ^[a-z][a-z0-9_.]*$); visibleWhen type: 'code', language: 'expression' ↔ EvaluatedExpressionInputSchema. Mirror rows exist and match as the PR body names them: the fields.options declared-and-labelled repeater face (object.form.ts:276-286); the Basics name/label/icon text rows (:37-40) and description textarea (:41); the lifecycle.class declared select (:572-581); fields.visibleWhen (:390) over the same node type (field.zod.ts:1765). The three deprecated aliases are not offered — as ruled.
    • indexes, Advanced, directly after datasource, three sub-rows: name text ↔ optional string; fields widget: 'string-tags'+required ↔ z.array(z.string()) required, the highlightFields face (:60); unique a declared select over global / organization only ↔ DeclaredIndexUniqueScopeSchema (boolean | 'global' | 'organization', default false, object.zod.ts:438-441). The boolean arm is unspellable as an option value and is the deprecated positional spelling; the ruling says "offers global / organization only". type / partial are retiredKey() tombstones and correctly have no row.
  2. object.zod.ts — 12 .meta({ title }) calls and nothing else. Right, byte-proved. Stripping the 12 exact .meta({ title: '…' }) calls from the head's file yields a file byte-identical to both 92fb68da and the PR base 6e3e5462 (those two are identical for this file). Titles: IndexSchema Name / Fields / Unique; ObjectFieldGroupSchema Key / Label / Icon / Description / Visible When / Collapse / Default Expanded / Collapsible / Collapsed — each equal to the declared row label. Chain order .describe(…).meta({ title }) is the existing convention (field.zod.ts:392, [finding] Untitled repeater row properties: SelectOptionSchema (6) shared by field.options and object.fields.options — fence dropped, dispatchable now #17506). No other *.zod.ts is in the file list. The spec: every repeater item schema except dashboard header.actions still has no JSON Schema title, so 21 property-panel tables render machine keys in every locale #17232 guard (repeater-item-titles.test.ts) requires a title on every non-tombstoned row property of every repeater's derived row schema and forbids a ledger row, so the aliases needed titles too; the guard is inside the green Test Core. Accept set does not move: .meta() is a JSON Schema annotation with no parse effect; no export changes; no committed artifact carries a title string (the head tree has "Default Expanded" only in object.zod.ts), and check:authorable-surface / check:api-surface are green on the head.

  3. Reconciliation ledger — three nested omit rows under object / fieldGroups. Right, and load-bearing. Shape is the page / interfaceConfig / sourceView precedent row exactly (kind, type, path, key, why with "deliberately not offered to new authors"). reconcileNestedLists computes zodOnly = subKeys − offered − excused − retired over every hand-written repeater path and the test asserts it empty; without the three rows the aliases would be zodOnly under object.fieldGroups. indexes needs no row: type/partial are excused through isRetiredAt. The why texts' parse claim is verified in normalizeSemanticRoleAliases (object.zod.ts:2822-2838): if (grp.collapse != null) return g; then pair → collapse, else defaultExpanded → collapse.

  4. Catalogues — 22 en leaves ×4 locales. Right. 7 rows ×2 under fieldGroups, 4 rows ×2 under indexes; en helpText equals the form text verbatim (spot-checked fieldGroups.key, indexes.fields, indexes.unique); zh-CN / ja-JP / es-ES are authored translations, not echoes. check:i18n is inside the green Lint & Repo Gates.

  5. Pins. All three mechanical and complete.

    • object-collapsed-sections-echo-decisions.test.ts: 61 → 69 and advanced 52 → 60 (= indexes + 3 sub-rows, 8 leaves); open 100 → 114 (= fieldGroups + 6 sub-rows, 14 leaves). The minus lines match the PR base's values.
    • object-lifecycle-panel-echo-decisions.test.ts: the PR base and origin/main read 623 (after spec(ui): retire list.tabs and the view container's body name (2 keys); listViews + ViewTabBar and the row name already deliver both #20301's −10); the head reads 634 = 623 + 11 labels (7 + 4). 215603c8 has parents e8bdb1ad and 6e3e5462; its only hunk in this file is 623 → 634 plus the comment. The "633 − 10 + 11" in the report is the same arithmetic.
    • field-rows-option-description.test.ts (outside the claim's named surface): icon inputs 1 → 2, now pinned by position (the Basics row and the fieldGroups sub-row) with a one-fieldGroups-repeater assertion; the options-repeater no-icon assertions are untouched. A population pin moved by the new sub-row — accepted on the G1b census-pin precedent.
  6. Help text against its readers. Every claim holds; none claims a refusal that does not exist.

    • fieldGroups row: array order = display order; a field joins by its own group; ungrouped fields trail; an empty group is not drawn — spec/src/data/field-group-layout.ts:17-18, 194, 205-208.
    • key: snake_case regex + uniqueness refine (object.zod.ts:1160-1162, 2012-2014); a renamed key leaves its fields in the trailing bucket.
    • icon: detail page only — plugin-detail/src/DetailSection.tsx:697,732 renders section.icon, buildDefaultPageSchema.ts:723 forwards it; plugin-form/src/fieldGroups.ts:71 does not copy it. description and collapse: both surfaces (fieldGroups.ts:73,77-78; buildDefaultPageSchema.ts:724-726; DetailSection.tsx:701,743); "Unset: none" is the schema default. visibleWhen: fieldGroups.ts:74; the text claims only the form.
    • indexes row: syncDeclaredIndexes creates what existing lacks and never drops (sql-driver.ts:11850 "only ever ADDs"). Precision note, not a defect: a legacy unique index is dropped by applyIndexDriftOp replace_unique_index (:13443-13460), which is the off-by-default drift-apply path, not the sync the text names.
    • name: unset → normalizeDeclaredIndex → buildIndexName = idx_ + table + columns, uniq_ when unique (schema-drift.ts:1391-1397, 1835-1838); idx_task_status is a correct example.
    • fields: sql-driver.ts:14187-14194 — this.logger.warn('[sql-driver] skipping declared index … column(s) not materialized') then continue: whole index, warn. No authoring door judges the names: the Zod node is z.array(z.string()); validate-object-field-refs.ts:91-94 excludes indexes[].fields[] by design; data-model-rules.ts R11 reads only bare unique: true (warning); validate-field-consumers.ts answers the inverse question (a declared field with zero consumers; an index is a display consumer) and reports no unknown column. The text claims exactly the skip and no refusal.
    • unique: "Unset: not unique" = default false; only global / organization are offered, both accepted today and after protocol 18; "keeps it until you pick a scope" — RepeaterField.update writes { ...r, ...patch } (SchemaForm.tsx:2364-2366). A stored true/false renders String(value) (SchemaForm.tsx:1975) matching no option — a code reading, flagged as not browser-run in the PR body. Acceptable against the ruling, and honest.
  7. Changeset prose: names the two rows, the three ledger rows, the titles, the no-door claim for indexes[].fields, the 22 leaves, and that the zodOnly direction stays unwired — all true of the diff.

② Semver level

@objectstack/spec: minor, @objectstack/platform-objects: patch — the same pair as G1a (7db1332f) and G1b (ec292cf5), for the same publish: a larger getMetaTypes() form payload and new catalogue keys, no exports change. The 12 titles do not raise the level: the titles-only precedent f5a7250b (#17507) shipped as spec: patch. Clause-②: no holds on the head: the accept set of ObjectSchema / IndexSchema / ObjectFieldGroupSchema is unchanged (byte proof above), title is an annotation on the served JSON Schema — shipped bytes, not the published accept set, per contract-review.md — and METADATA_FORM_REGISTRY is an opaque Readonly record. No (widening)/(narrowing) arm, correctly. Check Changeset is green.

Clause-②: no

③ Boundary flags

Dev flags (patch-round report 5872543115, first-round report 5870753284):

  • object.zod.ts by in-place amendment — answered: claim 5869305892 carries updated_at 2026-09-28T13:26:20Z matching its "Amended 2026-09-28T13:26Z" line; the diff holds exactly the 12 admitted calls; no other *.zod.ts moved.
  • field-rows-option-description.test.ts outside the named surface — answered: mechanical population pin (①5).
  • Changeset paragraph on titles — answered: accurate, level unchanged (②).
  • Lifecycle pin resolved by hand at the merge — answered: 623 + 11 = 634 on the base's value (①5).
  • Suites ran at 215603c8, not c22fc1e2 — answered: git diff --stat 215603c8 c22fc1e2 is the changeset alone (+3/−1); the head's own check-runs are the verdict in any case.
  • Shared checkout / cleanup notes — process only, no diff effect.
  • First round's open_questions[0] (widen the claim) — answered by the seat with option A on the claim comment; the patch round's open_questions is empty.

The brief's four claims:

  1. Residue 6 → 4 — consistent with the diff (two root object keys now offered, none added; the four left are the G2b keys). The probe was the dev's; not re-run here (read-only). The top-level zodOnly direction is unwired, so this is a carried measurement, not a gate verdict.
  2. unique writes only global / organization; stored booleans untouched and unshown — acceptable against ruling 5861442317 G2 and honest (①6).
  3. indexes[].fields unjudged; whole-index skip at warn — the help text states the skip and claims no refusal (①6). The fold into [finding] a misspelt field name in a field's relatedListColumns, lookupColumns, lookupFilters[].field or dependsOn passes every authoring door, and fails only at view or picker time #20432 via comment 5870812245 sits on another card, outside this brief's inputs; carried on the brief's word, not verified here.
  4. Pins — all mechanical and complete (①5).

Carrier-none notes in the PR body, each verified at the pin and none barring the PR (the ruling prescribed this row and this face): ResourceEditPage.tsx:212-214 CANVAS_OWNED_KEYS.object = ['fields', 'fieldGroups'] hides the fieldGroups row on the Studio object edit page — the per-key table (5859943900 row 5) recorded the designer door before recommending the curated repeater, so the ruling chose the row knowing it; ObjectGroupInspector.tsx:12-14 is stale against DetailSection.tsx:697/701 (comment only); the CodeWidget envelope bound is objectui#10963's class; the select cannot be cleared; the stored-boolean display is a code reading.

Escalated for the landing seat, not a defect of this diff: origin/main has moved .objectui-sha to dd3f7e1b (past the head's f8a9d0fb); the sibling checkout does not hold it, so the objectui readings above are at the head's pin only. This diff imports nothing from objectui and the Console Pin Gate skip is rostered, so no break is implied.

Implemented-by: claude/issue-19332-g2a-fieldgroups-indexes
Reviewed-by: session_01ARcDurZ5j34RdqsGgc4jgH

VERDICT: PASS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants