Skip to content

Commit dc0ab6a

Browse files
feat(spec): curated fieldGroups and indexes repeaters on the object form (#19332, flight G2a) (#20449)
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
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/platform-objects": patch
4+
---
5+
6+
Clause-②: no
7+
8+
Two live structured object keys are authorable in the metadata form: `fieldGroups` and `indexes`. Each was **declared** by `ObjectSchema`, graded `live` by the liveness ledger, and offered by **no** form in `METADATA_FORM_REGISTRY`, so an author's only door was the Source tab. Each is now a `type: 'repeater'` row on the object form whose sub-rows are declared by hand rather than derived from the schema:
9+
10+
- `fieldGroups` (Basics, beside `highlightFields`) — six sub-rows, one per canonical group key: `key` and `label` (required text), `icon` (text), `description` (textarea), `collapse` (a `none` / `expanded` / `collapsed` select) and `visibleWhen` (`type: 'code'`, `language: 'expression'`, the `fields` grid's predicate rows). The three `[DEPRECATED → collapse]` aliases (`defaultExpanded`, `collapsible`, `collapsed`) are **not** offered; the metadata-form reconciliation ledger records a nested `omit` row for each. The parse still accepts them and derives `collapse` from one only when `collapse` is absent, so a stored entry keeps its meaning, and a `collapse` set in the form outranks any alias it carries.
11+
- `indexes` (Advanced, beside `datasource`) — three sub-rows over the keys the SQL driver reads: `name` (text), `fields` (`widget: 'string-tags'`, required) and `unique`, a select offering **only** `global` and `organization`. The deprecated bare `unique: true` is never offered: a schema-derived control would take the union's first arm and render a switch that writes it. An edit merges into the stored entry, so an index that already carries `true` or `false` keeps it until the author picks a scope, and the select can write only the two values the parse accepts. `type` and `partial` are tombstones and have no row.
12+
13+
The help text states what the runtime does with each value. `indexes[].fields` is free text, and no authoring door judges its names: not the schema parse, not the publish door, not `os validate`. A name that is not a stored column makes the SQL driver skip the whole index at sync with a warning in the server log, and the help text says exactly that. A field group has no field-name list: a field joins a group through its own `group` key.
14+
15+
The two row schemas also carry a JSON Schema `title` on every property, as every repeater row schema must: `IndexSchema` on `name`, `fields` and `unique`, and `ObjectFieldGroupSchema` on its nine keys, the three deprecated aliases included. A property panel that reads the served schema's titles therefore shows a named column instead of a raw key. Each title is a `.meta({ title })` call and nothing more.
16+
17+
⛔ **No schema accept set moves and no export changes.** `METADATA_FORM_REGISTRY` is declared as an opaque `Readonly<Record<string, FormView>>`, so row contents were never part of the declared surface. What changes is the **form payload** `getMetaTypes()` serves (its rows, and the titles above in its JSON Schema) and the translation keys `os i18n extract` walks, hence the regenerated `platform-objects` metadata-form bundles. Their 22 new leaves are authored in `zh-CN`, `ja-JP` and `es-ES` rather than left as extractor fills.
18+
19+
⛔ **The gate that would notice a missing row is NOT landed here.** The reconciliation gate's top-level `zodOnly` direction stays unwired; this change lands offers and three nested ledger rows only.

‎packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,34 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
7272
label: "Searchable Fields",
7373
helpText: "Field names the $search query matches (ADR-0061): the default for the record picker, list quick-search and global search; a view may narrow it. Unset, search uses the name/title field plus short-text fields. Each entry must name a stored field of this object — an unknown name or a virtual formula field is refused at publish."
7474
},
75+
fieldGroups: {
76+
label: "Field Groups",
77+
helpText: "Ordered sections that group this object's fields on the entry form and the record detail page (ADR-0085); array order is display order. A field joins a group by naming its key in the field's own group setting. Fields in no group follow the groups, and a group no field joins is not drawn."
78+
},
79+
"fieldGroups.key": {
80+
label: "Key",
81+
helpText: "Machine key in snake_case, unique within this object — the schema refuses anything else. Fields join the group by naming this key, so renaming it leaves them ungrouped."
82+
},
83+
"fieldGroups.label": {
84+
label: "Label",
85+
helpText: "Header text of the group's section."
86+
},
87+
"fieldGroups.icon": {
88+
label: "Icon",
89+
helpText: "Lucide icon name shown beside the header on the record detail page (e.g. \"banknote\"). The entry form does not show it."
90+
},
91+
"fieldGroups.description": {
92+
label: "Description",
93+
helpText: "Text shown under the header, on the entry form and the record detail page."
94+
},
95+
"fieldGroups.collapse": {
96+
label: "Collapse",
97+
helpText: "Whether the section can be collapsed, on the entry form and the record detail page. Unset: none."
98+
},
99+
"fieldGroups.visibleWhen": {
100+
label: "Visible When",
101+
helpText: "CEL predicate over the record (e.g. record.type == 'invoice') — the entry form shows the whole group, header included, only while it is TRUE."
102+
},
75103
fields: {
76104
label: "Fields",
77105
helpText: "Add the columns this object will store"
@@ -269,6 +297,22 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
269297
label: "Datasource",
270298
helpText: "Target datasource ID (default: \"default\")"
271299
},
300+
indexes: {
301+
label: "Indexes",
302+
helpText: "Database indexes on this object's table. The SQL driver creates each one the table lacks when it syncs the table; a sync never drops an index."
303+
},
304+
"indexes.name": {
305+
label: "Name",
306+
helpText: "Physical index name. Unset: generated from the table and the columns (e.g. idx_task_status)."
307+
},
308+
"indexes.fields": {
309+
label: "Fields",
310+
helpText: "Column names of this object, in key order (e.g. status, owner). 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."
311+
},
312+
"indexes.unique": {
313+
label: "Unique",
314+
helpText: "Uniqueness scope (ADR-0120). Unset: not unique. The deprecated bare true (it means global) is not offered; an index that carries it keeps it until you pick a scope."
315+
},
272316
ownership: {
273317
label: "Ownership",
274318
helpText: "Record-ownership model. Absent resolves to user."

‎packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,34 @@ export const esESMetadataForms: NonNullable<TranslationData['metadataForms']> =
7272
label: "Campos buscables",
7373
helpText: "Nombres de campo con los que coincide la consulta $search (ADR-0061): el valor predeterminado del selector de registros, la búsqueda rápida de listas y la búsqueda global; una vista puede acotarlo. Sin definir, la búsqueda usa el campo de nombre/título más los campos de texto corto. Cada entrada debe nombrar un campo almacenado de este objeto: un nombre desconocido o un campo formula virtual se rechaza al publicar."
7474
},
75+
fieldGroups: {
76+
label: "Grupos de campos",
77+
helpText: "Secciones ordenadas que agrupan los campos de este objeto en el formulario de entrada y en la página de detalle del registro (ADR-0085); el orden del array es el orden de visualización. Un campo se une a un grupo indicando su clave en el ajuste group del propio campo. Los campos sin grupo van después de los grupos, y un grupo al que no se une ningún campo no se dibuja."
78+
},
79+
"fieldGroups.key": {
80+
label: "Clave",
81+
helpText: "Clave de máquina en snake_case, única dentro de este objeto: el esquema rechaza cualquier otra cosa. Los campos se unen al grupo indicando esta clave, así que renombrarla los deja sin grupo."
82+
},
83+
"fieldGroups.label": {
84+
label: "Etiqueta",
85+
helpText: "Texto del encabezado de la sección del grupo."
86+
},
87+
"fieldGroups.icon": {
88+
label: "Icono",
89+
helpText: "Nombre de icono Lucide que se muestra junto al encabezado en la página de detalle del registro (p. ej., \"banknote\"). El formulario de entrada no lo muestra."
90+
},
91+
"fieldGroups.description": {
92+
label: "Descripción",
93+
helpText: "Texto que se muestra bajo el encabezado, en el formulario de entrada y en la página de detalle del registro."
94+
},
95+
"fieldGroups.collapse": {
96+
label: "Contraer",
97+
helpText: "Si la sección se puede contraer, en el formulario de entrada y en la página de detalle del registro. Sin definir: none."
98+
},
99+
"fieldGroups.visibleWhen": {
100+
label: "Condición de visibilidad",
101+
helpText: "Predicado CEL sobre el registro (p. ej., record.type == 'invoice'): el formulario de entrada muestra el grupo entero, encabezado incluido, solo mientras sea TRUE."
102+
},
75103
fields: {
76104
label: "Campos",
77105
helpText: "Añade las columnas que almacenará este objeto"
@@ -269,6 +297,22 @@ export const esESMetadataForms: NonNullable<TranslationData['metadataForms']> =
269297
label: "Fuente de datos",
270298
helpText: "ID de fuente de datos de destino (valor predeterminado: \"default\")"
271299
},
300+
indexes: {
301+
label: "Índices",
302+
helpText: "Índices de base de datos de la tabla de este objeto. El driver SQL crea cada uno que le falte a la tabla cuando la sincroniza; una sincronización nunca elimina un índice."
303+
},
304+
"indexes.name": {
305+
label: "Nombre",
306+
helpText: "Nombre físico del índice. Sin definir: se genera a partir de la tabla y las columnas (p. ej., idx_task_status)."
307+
},
308+
"indexes.fields": {
309+
label: "Campos",
310+
helpText: "Nombres de columna de este objeto, en el orden de la clave (p. ej., status, owner). Nada los comprueba al guardar ni al publicar: un nombre que no sea una columna almacenada hace que el driver SQL omita el índice entero, con una advertencia en el registro del servidor."
311+
},
312+
"indexes.unique": {
313+
label: "Único",
314+
helpText: "Ámbito de unicidad (ADR-0120). Sin definir: no es único. El true desnudo obsoleto (significa global) no se ofrece; un índice que lo lleve lo conserva hasta que elijas un ámbito."
315+
},
272316
ownership: {
273317
label: "Propiedad",
274318
helpText: "Modelo de propiedad del registro. Si se omite, se resuelve como user."

‎packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,34 @@ export const jaJPMetadataForms: NonNullable<TranslationData['metadataForms']> =
7272
label: "検索対象項目",
7373
helpText: "$search クエリが照合する項目名(ADR-0061)。レコードピッカー、一覧のクイック検索、グローバル検索の既定値で、ビューで絞り込めます。未設定の場合は名前/タイトル項目と短いテキスト項目で検索します。各項目はこのオブジェクトの保存済みの項目を指す必要があり、不明な名前や仮想の formula 項目は公開時に拒否されます。"
7474
},
75+
fieldGroups: {
76+
label: "フィールドグループ",
77+
helpText: "このオブジェクトのフィールドを入力フォームとレコード詳細ページでまとめる、順序付きのセクション(ADR-0085)。配列の順序が表示順です。フィールドは自身の group 設定にグループのキーを指定するとそのグループに加わります。どのグループにも属さないフィールドはグループの後に続き、フィールドが 1 つも属さないグループは表示されません。"
78+
},
79+
"fieldGroups.key": {
80+
label: "キー",
81+
helpText: "snake_case のマシンキーで、このオブジェクト内で一意です。それ以外はスキーマが拒否します。フィールドはこのキーを指定してグループに加わるため、キーを変更するとそれらのフィールドはどのグループにも属さなくなります。"
82+
},
83+
"fieldGroups.label": {
84+
label: "表示名",
85+
helpText: "グループのセクションの見出しテキスト。"
86+
},
87+
"fieldGroups.icon": {
88+
label: "アイコン",
89+
helpText: "レコード詳細ページで見出しの横に表示される Lucide アイコン名(例:\"banknote\")。入力フォームには表示されません。"
90+
},
91+
"fieldGroups.description": {
92+
label: "説明",
93+
helpText: "見出しの下に表示されるテキスト。入力フォームとレコード詳細ページに表示されます。"
94+
},
95+
"fieldGroups.collapse": {
96+
label: "折りたたみ",
97+
helpText: "セクションを折りたためるかどうか。入力フォームとレコード詳細ページに適用されます。未設定の場合は none です。"
98+
},
99+
"fieldGroups.visibleWhen": {
100+
label: "表示条件",
101+
helpText: "レコードに対する CEL 述語(例:record.type == 'invoice')。TRUE の間だけ、入力フォームに見出しを含むグループ全体が表示されます。"
102+
},
75103
fields: {
76104
label: "フィールド",
77105
helpText: "このオブジェクトが保存する列を追加"
@@ -269,6 +297,22 @@ export const jaJPMetadataForms: NonNullable<TranslationData['metadataForms']> =
269297
label: "データソース",
270298
helpText: "対象データソース ID(既定: \"default\")"
271299
},
300+
indexes: {
301+
label: "インデックス",
302+
helpText: "このオブジェクトのテーブルのデータベースインデックス。SQL ドライバーはテーブルを同期するとき、テーブルにまだないインデックスを作成します。同期でインデックスが削除されることはありません。"
303+
},
304+
"indexes.name": {
305+
label: "名前",
306+
helpText: "物理インデックス名。未設定の場合は、テーブル名と列名から生成されます(例:idx_task_status)。"
307+
},
308+
"indexes.fields": {
309+
label: "フィールド",
310+
helpText: "このオブジェクトの列名を、キーの順に指定します(例:status、owner)。保存時にも公開時にも検査されません。保存される列ではない名前があると、SQL ドライバーはそのインデックス全体をスキップし、サーバーログに警告を出します。"
311+
},
312+
"indexes.unique": {
313+
label: "一意",
314+
helpText: "一意性のスコープ(ADR-0120)。未設定の場合は一意ではありません。非推奨の裸の true(global を意味します)は選択肢にありません。true を持つインデックスは、スコープを選ぶまでその値を保持します。"
315+
},
272316
ownership: {
273317
label: "所有権モデル",
274318
helpText: "レコードの所有権モデル。未設定の場合は user として解決されます。"

‎packages/platform-objects/src/apps/translations/object-collapsed-sections-echo-decisions.test.ts‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -900,10 +900,15 @@ describe('#19403 round 8 — the population, DERIVED from the form and a shape',
900900
// `requiredPermissions` — a label and a help text each, three rows and six
901901
// leaves, all eighteen translated leaves authored by the same card. `advanced`
902902
// reads 46 → 52; `capabilities` is untouched.
903-
expect(PANEL_LEAVES.length).toBe(61);
903+
// 69 since #19332 flight G2a: that flight gave `advanced` the `indexes`
904+
// repeater beside `datasource` — the row and its three declared sub-rows
905+
// (`name`, `fields`, `unique`), a label and a help text each, four rows and
906+
// eight leaves, all twenty-four translated leaves authored by the same
907+
// flight. `advanced` reads 52 → 60; `capabilities` is untouched.
908+
expect(PANEL_LEAVES.length).toBe(69);
904909
expect(PANEL_LEAVES.every((l) => l.prop === 'label' || l.prop === 'helpText')).toBe(true);
905910
expect(PANEL_LEAVES.filter((l) => l.section === 'capabilities').length).toBe(9);
906-
expect(PANEL_LEAVES.filter((l) => l.section === 'advanced').length).toBe(52);
911+
expect(PANEL_LEAVES.filter((l) => l.section === 'advanced').length).toBe(60);
907912
});
908913

909914
it('⭐ DARK, OUTWARD — the open sections are excluded, and `fields.placeholder` is the one that proves it', () => {
@@ -917,7 +922,10 @@ describe('#19403 round 8 — the population, DERIVED from the form and a shape',
917922
// gave `basics` the object's two own field-name lists, `highlightFields` and
918923
// `searchableFields`, beside `nameField` — four leaves, their twelve
919924
// translated leaves authored by that card, so `openEchoes` stays at zero.
920-
expect(OPEN_LEAVES.length).toBe(100);
925+
// 114 since #19332 flight G2a gave `basics` the `fieldGroups` repeater — the
926+
// row and its six declared sub-rows, fourteen leaves, their forty-two
927+
// translated leaves authored by that flight, so `openEchoes` stays at zero.
928+
expect(OPEN_LEAVES.length).toBe(114);
921929
expect(PANEL_LEAVES.some((l) => l.path === 'fields.placeholder')).toBe(false);
922930
expect(OPEN_LEAVES.some((l) => l.path === 'fields.placeholder')).toBe(true);
923931
for (const path of ['name', 'label', 'fields', 'fields.valueDomain', 'fields.deleteBehavior', 'fields.expression']) {

‎packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1132,7 +1132,12 @@ describe('#19403 round 10 — the verdicts, on the live bundles', () => {
11321132
// (`name`, `label`, `icon`, `view`, `filter`, `order`, `pinned`,
11331133
// `isDefault`, `visible`), ten leaves authored in all three locales, out of
11341134
// the catalog.
1135-
expect(translated.length, `${locale} positive control`).toBe(623);
1135+
// 634 since #19332 flight G2a: eleven new row labels — the object form's
1136+
// `fieldGroups` repeater (and its `key`, `label`, `icon`, `description`,
1137+
// `collapse` and `visibleWhen` sub-rows) and its `indexes` repeater (and
1138+
// its `name`, `fields` and `unique` sub-rows) — authored in all three
1139+
// locales.
1140+
expect(translated.length, `${locale} positive control`).toBe(634);
11361141
}
11371142
// ⭐ DARK — the blindness, executable. On a synthetic two-locale catalog the
11381143
// all-three predicate returns 0 while the per-locale one returns 1, so the

0 commit comments

Comments
 (0)