From 095e18402871a63c28ccca308ea78bb640b4bc6b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 05:36:20 +0000 Subject: [PATCH 1/6] =?UTF-8?q?wip(spec):=20S-objectui-held=20stage=20?= =?UTF-8?q?=E2=80=94=20gantt=20markers,=20timeline=20mapping=20and=20form?= =?UTF-8?q?=20fields=20typed;=20forks=20recorded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn Co-authored-by: Claude --- .../18.ui-object-form-fields-names-typed.ts | 44 +++ .../18.ui-object-gantt-markers-typed.ts | 40 +++ .../18.ui-object-timeline-mapping-typed.ts | 38 +++ packages/spec/src/migrations/registry.ts | 150 ++++++++++ ...nt-objectui-held-typed-members.pin.test.ts | 244 ++++++++++++++++ ...omponent-props-unknown-members.pin.test.ts | 141 +++++++--- packages/spec/src/ui/component.zod.ts | 260 +++++++++++++++--- 7 files changed, 832 insertions(+), 85 deletions(-) create mode 100644 packages/spec/src/migrations/entries/semantic/18.ui-object-form-fields-names-typed.ts create mode 100644 packages/spec/src/migrations/entries/semantic/18.ui-object-gantt-markers-typed.ts create mode 100644 packages/spec/src/migrations/entries/semantic/18.ui-object-timeline-mapping-typed.ts create mode 100644 packages/spec/src/ui/component-objectui-held-typed-members.pin.test.ts diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-form-fields-names-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-form-fields-names-typed.ts new file mode 100644 index 00000000000..bebf6506984 --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-form-fields-names-typed.ts @@ -0,0 +1,44 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +// #21464 — the top-level `fields` of the `object-form` and +// `object-master-detail-form` page blocks was `z.array(z.unknown())`, held +// while the form drew a `{ name }` field entry its own page-builder guide +// taught. objectstack-ai/objectui#11550 retired that entry from every authoring +// face (the form still draws a STORED one, by its name, as tolerance), so both +// rows now take field names — objectui's own declaration of the member — and +// an object entry is refused with what to write instead. D3 only: page- +// component `properties` is not parsed on the metadata save or load path, so a +// stored page is never refused; and the authored census found no authored +// value to respell — the refused values are fixtures probing the stored read, +// the console warning and objectui's own refusal. +export const entry: SemanticMigration = { + id: 'ui-object-form-fields-names-typed', + surface: 'page `object-form` and `object-master-detail-form` components — `properties.fields` (whose ' + + 'entries used to accept any value)', + replacement: 'a list of bare field names, in the order the form draws them. Write a `{ name: \'email\' }` ' + + 'entry as `\'email\'` — the form only ever drew its name — and move a `label` or `required` override ' + + 'onto a `sections[].fields` entry (`type` is always the object field\'s); write a `{ field: \'email\' }` ' + + 'entry as `\'email\'`, or move it into a section\'s `fields`, the vocabulary it belongs to.', + reason: 'The form reads its top-level `fields` as the names of the fields to draw, in order, selecting ' + + 'from the object\'s fields and from `customFields`; the master-detail form hands its own to the parent ' + + 'form verbatim. objectui declares the member `string[]`, but the page-component rows declared it ' + + '`z.array(z.unknown())` while the form drew a `{ name }` entry by that name — the shape objectui\'s ' + + 'page-builder guide taught, with a `label`, `type` and `required` the form silently dropped. ' + + 'objectui has since retired that entry from every authoring face — the guide and its fixtures name the ' + + 'fields — keeping only a STORED one readable; so both rows now take field names, and refuse an object entry with what to write instead: a ' + + '`{ name }` entry is its bare name, and a `{ field }` entry — the `sections[].fields` vocabulary, which ' + + 'the form skips at the top level with a console warning — is its bare name or belongs in a section. It is ' + + 'read where every page component\'s props are: the component-props gate reports a refused value as an ' + + 'advisory `component-props-invalid` finding on `objectstack validate`, `objectstack build` and ' + + '`objectstack lint`, and a stored page still saves and loads, because a page component\'s `properties` is ' + + 'not parsed on the metadata save or load path. No conversion is registered: nothing on the load path ' + + 'refuses the shape, the form already draws a stored `{ name }` entry by its name, and an override written ' + + 'beside it has no rewrite that keeps it — moving it onto a section is the judgment this entry leaves to ' + + 'the upgrader. Deployed metadata NOT MEASURED.', + acceptanceCriteria: 'Every `object-form` and `object-master-detail-form` node validates: `objectstack ' + + 'validate` reports no `component-props-invalid` finding under `properties.fields`. Each form draws the ' + + 'fields its list names, in that order, with any per-form label or required override taken from its ' + + 'section entry.', +}; diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-gantt-markers-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-gantt-markers-typed.ts new file mode 100644 index 00000000000..9d4658fe0a8 --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-gantt-markers-typed.ts @@ -0,0 +1,40 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +// #21464 — the `object-gantt` page block's `markers` was `z.array(z.unknown())`: +// its element contract lived only in objectui, so a marker with no `date`, a +// numeric `date` or a misspelled member passed the component-props gate, and +// the chart drew no line, or drew it unlabelled and in the default colour. The +// spec now declares objectui's own authoring declaration of a marker, +// `{ date, label?, color? }` with `date` a string, and the row takes it. D3 +// only: page-component `properties` is not parsed on the metadata save or load +// path, so a stored page is never refused; a misspelled member has no rewrite +// that says which of the three the author meant; and the authored census found +// no authored value to respell — the one refused value is objectui's own +// compile-time refusal probe. +export const entry: SemanticMigration = { + id: 'ui-object-gantt-markers-typed', + surface: 'page `object-gantt` components — `properties.markers` (whose entries used to accept any value)', + replacement: 'a list of `{ date, label?, color? }`: `date` an ISO date or date-time string (required), `label` ' + + 'the text drawn against the line, `color` any CSS colour. Write a marker `title`, `text` or `name` as ' + + '`label`, and a `colour` as `color`; give every marker a string `date`.', + reason: 'The gantt reads each marker with one shape — `date` places the line, and a date that does not ' + + 'parse or falls outside the drawn range draws none; `label` is drawn against it; `color` paints it, the ' + + 'theme\'s primary colour when absent — and the page-component row declared the entries `z.unknown()`, ' + + 'because that contract was objectui\'s alone. So a marker with no `date`, a numeric `date` or a ' + + 'misspelled member passed the component-props gate, and the chart drew no line, or drew it with no label ' + + 'and in the default colour. The spec now declares objectui\'s own authoring declaration of a marker, ' + + '`{ date, label?, color? }` with `date` a string (authored metadata is JSON, which cannot carry a ' + + '`Date`), closed as every element shape on that map is. It is read where every page component\'s props ' + + 'are: the component-props gate reports a refused value as an advisory `component-props-invalid` / ' + + '`component-props-unknown-key` finding on `objectstack validate`, `objectstack build` and `objectstack ' + + 'lint`, and a stored page still saves and loads, because a page component\'s `properties` is not parsed ' + + 'on the metadata save or load path. No conversion is registered: nothing on the load path refuses the ' + + 'shape, and a misspelled member has no rewrite that says which member the author meant — which is the ' + + 'judgment this entry leaves to the upgrader. Deployed metadata NOT MEASURED.', + acceptanceCriteria: 'Every `object-gantt` node validates: `objectstack validate` reports no ' + + '`component-props-invalid` / `component-props-unknown-key` finding under `properties.markers`. Each ' + + 'gantt that sets markers draws one line per marker whose date falls in the drawn range, with the label ' + + 'and colour written.', +}; diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-timeline-mapping-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-timeline-mapping-typed.ts new file mode 100644 index 00000000000..9fb9260db9a --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-timeline-mapping-typed.ts @@ -0,0 +1,38 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +// #21464 — the `object-timeline` page block's `mapping` was `z.unknown()`: its +// contract lived only in objectui, so a bare field name, a non-string binding +// or a misspelled member (`titleField` inside `mapping`) passed the +// component-props gate, and the rail bound nothing for it and drew the default +// field. The spec now declares objectui's own declaration of the binding +// record, four optional field names, and the row takes it. D3 only: page- +// component `properties` is not parsed on the metadata save or load path, so a +// stored page is never refused; and the authored census found no refused +// authored value — every one parses. +export const entry: SemanticMigration = { + id: 'ui-object-timeline-mapping-typed', + surface: 'page `object-timeline` components — `properties.mapping` (which used to accept any value)', + replacement: 'the binding record the rail reads: `{ title?, date?, description?, variant? }`, each a field ' + + 'name. Write `titleField`, `dateField` / `startDateField`, `descriptionField` and `variantField` inside ' + + '`mapping` as `title`, `date`, `description` and `variant`; write a bare field name as the member it ' + + 'binds (`mapping: { title: \'subject\' }`).', + reason: 'The timeline rail reads `mapping` as four field names — `title` and `date` between the `timeline` ' + + 'block\'s own member and the flat fallback, `description` ahead of `descriptionField`, and `variant`, ' + + 'the field whose value picks each entry\'s marker colour and the one binding with no other spelling — ' + + 'and the page-component row declared it `z.unknown()`, because that contract was objectui\'s alone. So a ' + + 'bare field name, a non-string binding or a misspelled member passed the component-props gate, and the ' + + 'rail bound nothing for it and drew the default field. The spec now declares objectui\'s own declaration ' + + 'of the binding record, four optional field names, closed as every element shape on that map is. It is ' + + 'read where every page component\'s props are: the component-props gate reports a refused value as an ' + + 'advisory `component-props-invalid` / `component-props-unknown-key` finding on `objectstack validate`, ' + + '`objectstack build` and `objectstack lint`, and a stored page still saves and loads, because a page ' + + 'component\'s `properties` is not parsed on the metadata save or load path. No conversion is ' + + 'registered: nothing on the load path refuses the shape, and the authored census found nothing to ' + + 'respell. Deployed metadata NOT MEASURED.', + acceptanceCriteria: 'Every `object-timeline` node validates: `objectstack validate` reports no ' + + '`component-props-invalid` / `component-props-unknown-key` finding under `properties.mapping`. Each ' + + 'timeline that sets a mapping draws its entries\' title, date, description and marker colour from the ' + + 'fields it names.', +}; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 5d26b213357..dd44262f269 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -6183,6 +6183,20 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + '`columns` untouched) on `object-form` page components, on every form payload a view ' + 'carries, and on the assembled-manifest `viewItems` channel.', }, + { + id: 'ui-object-form-fields-names-typed', + order: 74, + text: + 'It also types the top-level `fields` of the `object-form` and `object-master-detail-form` page ' + + 'blocks (the last stage of the `ComponentPropsMap` `z.unknown()` close-out), the two members the third ' + + 'stage held: the form drew a `{ name }` field entry its own page-builder guide taught, with a `label`, ' + + '`type` and `required` it silently dropped, and objectui has since retired that entry from every ' + + 'authoring face, drawing only a stored one by its name. So both rows take field names, objectui\'s own ' + + 'declaration of the member, and refuse an object entry with what to write instead — a `{ name }` entry ' + + 'is its bare name, and a `{ field }` entry belongs in a section. Read by the component-props gate ' + + '(advisory); a stored page still saves and loads, so no conversion is registered. Its D3 record is the ' + + 'semantic entry `ui-object-form-fields-names-typed`.', + }, { id: 'ui-object-form-members-typed', order: 67, @@ -6199,6 +6213,18 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'still saves and loads, so no conversion is registered. Its D3 record is the semantic entry ' + '`ui-object-form-members-typed`.', }, + { + id: 'ui-object-gantt-markers-typed', + order: 75, + text: + 'It also types the `object-gantt` page block\'s `markers` (the same stage): its entries were ' + + '`z.unknown()` because the marker contract lived only in objectui, so a marker with no `date`, a numeric ' + + '`date` or a misspelled member passed every door and the chart drew no line, or drew it unlabelled. The ' + + 'spec now declares objectui\'s own authoring declaration of a marker, `{ date, label?, color? }` with ' + + '`date` a string, and the row takes it. Read by the component-props gate (advisory); a stored page still ' + + 'saves and loads, so no conversion is registered. Its D3 record is the semantic entry ' + + '`ui-object-gantt-markers-typed`.', + }, { id: 'ui-object-grid-columns-typed', order: 73, @@ -6336,6 +6362,20 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'Read by the component-props gate (advisory); a stored page still saves and loads, so no conversion is ' + 'registered. Its D3 record is the semantic entry `ui-object-metric-drill-down-typed`.', }, + { + id: 'ui-object-timeline-mapping-typed', + order: 76, + text: + 'It also types the `object-timeline` page block\'s `mapping` (the same stage): the binding record — ' + + 'four optional field names for an entry\'s title, date, description and marker colour — was ' + + '`z.unknown()` because its contract lived only in objectui, so a bare field name or a misspelled member ' + + 'passed every door and the rail drew the default field. The spec now declares objectui\'s own ' + + 'declaration of it, and the row takes it. The stage\'s other members — the metric drill-down\'s ' + + '`report`, the form\'s `customFields` and both forms\' `sections`, the timeline\'s `items` and the ' + + 'action containers\' members — stay open: each contract has more than one viable shape that no ruling ' + + 'decides yet. Read by the component-props gate (advisory); a stored page still saves and loads, so no ' + + 'conversion is registered. Its D3 record is the semantic entry `ui-object-timeline-mapping-typed`.', + }, { id: 'ui-record-line-items-props-closed', order: 57, @@ -19717,6 +19757,46 @@ const step18: MigrationStep = { + 'group per value of that field, and a board that showed one swimlane shows one swimlane per value — ' + 'check that this is the grouping you meant.', }, + // #21464 — the top-level `fields` of the `object-form` and + // `object-master-detail-form` page blocks was `z.array(z.unknown())`, held + // while the form drew a `{ name }` field entry its own page-builder guide + // taught. objectstack-ai/objectui#11550 retired that entry from every authoring + // face (the form still draws a STORED one, by its name, as tolerance), so both + // rows now take field names — objectui's own declaration of the member — and + // an object entry is refused with what to write instead. D3 only: page- + // component `properties` is not parsed on the metadata save or load path, so a + // stored page is never refused; and the authored census found no authored + // value to respell — the refused values are fixtures probing the stored read, + // the console warning and objectui's own refusal. + { + id: 'ui-object-form-fields-names-typed', + surface: 'page `object-form` and `object-master-detail-form` components — `properties.fields` (whose ' + + 'entries used to accept any value)', + replacement: 'a list of bare field names, in the order the form draws them. Write a `{ name: \'email\' }` ' + + 'entry as `\'email\'` — the form only ever drew its name — and move a `label` or `required` override ' + + 'onto a `sections[].fields` entry (`type` is always the object field\'s); write a `{ field: \'email\' }` ' + + 'entry as `\'email\'`, or move it into a section\'s `fields`, the vocabulary it belongs to.', + reason: 'The form reads its top-level `fields` as the names of the fields to draw, in order, selecting ' + + 'from the object\'s fields and from `customFields`; the master-detail form hands its own to the parent ' + + 'form verbatim. objectui declares the member `string[]`, but the page-component rows declared it ' + + '`z.array(z.unknown())` while the form drew a `{ name }` entry by that name — the shape objectui\'s ' + + 'page-builder guide taught, with a `label`, `type` and `required` the form silently dropped. ' + + 'objectui has since retired that entry from every authoring face — the guide and its fixtures name the ' + + 'fields — keeping only a STORED one readable; so both rows now take field names, and refuse an object entry with what to write instead: a ' + + '`{ name }` entry is its bare name, and a `{ field }` entry — the `sections[].fields` vocabulary, which ' + + 'the form skips at the top level with a console warning — is its bare name or belongs in a section. It is ' + + 'read where every page component\'s props are: the component-props gate reports a refused value as an ' + + 'advisory `component-props-invalid` finding on `objectstack validate`, `objectstack build` and ' + + '`objectstack lint`, and a stored page still saves and loads, because a page component\'s `properties` is ' + + 'not parsed on the metadata save or load path. No conversion is registered: nothing on the load path ' + + 'refuses the shape, the form already draws a stored `{ name }` entry by its name, and an override written ' + + 'beside it has no rewrite that keeps it — moving it onto a section is the judgment this entry leaves to ' + + 'the upgrader. Deployed metadata NOT MEASURED.', + acceptanceCriteria: 'Every `object-form` and `object-master-detail-form` node validates: `objectstack ' + + 'validate` reports no `component-props-invalid` finding under `properties.fields`. Each form draws the ' + + 'fields its list names, in that order, with any per-form label or required override taken from its ' + + 'section entry.', + }, // #21464 — four members of the `object-form` page block were `z.unknown()` // although the form reads each with a fixed shape, so an off-shape value passed // the component-props gate and the form fell back or ignored it in silence. The @@ -19766,6 +19846,42 @@ const step18: MigrationStep = { + 'Each form that set one of them now shows it: the post-submit behaviour it names, the modal\'s tabbed ' + 'sections, the navigation after a save, and the phone presentation.', }, + // #21464 — the `object-gantt` page block's `markers` was `z.array(z.unknown())`: + // its element contract lived only in objectui, so a marker with no `date`, a + // numeric `date` or a misspelled member passed the component-props gate, and + // the chart drew no line, or drew it unlabelled and in the default colour. The + // spec now declares objectui's own authoring declaration of a marker, + // `{ date, label?, color? }` with `date` a string, and the row takes it. D3 + // only: page-component `properties` is not parsed on the metadata save or load + // path, so a stored page is never refused; a misspelled member has no rewrite + // that says which of the three the author meant; and the authored census found + // no authored value to respell — the one refused value is objectui's own + // compile-time refusal probe. + { + id: 'ui-object-gantt-markers-typed', + surface: 'page `object-gantt` components — `properties.markers` (whose entries used to accept any value)', + replacement: 'a list of `{ date, label?, color? }`: `date` an ISO date or date-time string (required), `label` ' + + 'the text drawn against the line, `color` any CSS colour. Write a marker `title`, `text` or `name` as ' + + '`label`, and a `colour` as `color`; give every marker a string `date`.', + reason: 'The gantt reads each marker with one shape — `date` places the line, and a date that does not ' + + 'parse or falls outside the drawn range draws none; `label` is drawn against it; `color` paints it, the ' + + 'theme\'s primary colour when absent — and the page-component row declared the entries `z.unknown()`, ' + + 'because that contract was objectui\'s alone. So a marker with no `date`, a numeric `date` or a ' + + 'misspelled member passed the component-props gate, and the chart drew no line, or drew it with no label ' + + 'and in the default colour. The spec now declares objectui\'s own authoring declaration of a marker, ' + + '`{ date, label?, color? }` with `date` a string (authored metadata is JSON, which cannot carry a ' + + '`Date`), closed as every element shape on that map is. It is read where every page component\'s props ' + + 'are: the component-props gate reports a refused value as an advisory `component-props-invalid` / ' + + '`component-props-unknown-key` finding on `objectstack validate`, `objectstack build` and `objectstack ' + + 'lint`, and a stored page still saves and loads, because a page component\'s `properties` is not parsed ' + + 'on the metadata save or load path. No conversion is registered: nothing on the load path refuses the ' + + 'shape, and a misspelled member has no rewrite that says which member the author meant — which is the ' + + 'judgment this entry leaves to the upgrader. Deployed metadata NOT MEASURED.', + acceptanceCriteria: 'Every `object-gantt` node validates: `objectstack validate` reports no ' + + '`component-props-invalid` / `component-props-unknown-key` finding under `properties.markers`. Each ' + + 'gantt that sets markers draws one line per marker whose date falls in the drawn range, with the label ' + + 'and colour written.', + }, // #21464 — the `object-grid` page block's `columns` was `z.array(z.unknown())` // although the grid reads it with one shape, so a column keyed `accessorKey` / // `header` / `name`, a column with no `field`, a mixed list or a key the grid @@ -20234,6 +20350,40 @@ const step18: MigrationStep = { + 'and the records behind the number, scoped by the metric\'s own `filter`, in the columns and page size ' + 'written.', }, + // #21464 — the `object-timeline` page block's `mapping` was `z.unknown()`: its + // contract lived only in objectui, so a bare field name, a non-string binding + // or a misspelled member (`titleField` inside `mapping`) passed the + // component-props gate, and the rail bound nothing for it and drew the default + // field. The spec now declares objectui's own declaration of the binding + // record, four optional field names, and the row takes it. D3 only: page- + // component `properties` is not parsed on the metadata save or load path, so a + // stored page is never refused; and the authored census found no refused + // authored value — every one parses. + { + id: 'ui-object-timeline-mapping-typed', + surface: 'page `object-timeline` components — `properties.mapping` (which used to accept any value)', + replacement: 'the binding record the rail reads: `{ title?, date?, description?, variant? }`, each a field ' + + 'name. Write `titleField`, `dateField` / `startDateField`, `descriptionField` and `variantField` inside ' + + '`mapping` as `title`, `date`, `description` and `variant`; write a bare field name as the member it ' + + 'binds (`mapping: { title: \'subject\' }`).', + reason: 'The timeline rail reads `mapping` as four field names — `title` and `date` between the `timeline` ' + + 'block\'s own member and the flat fallback, `description` ahead of `descriptionField`, and `variant`, ' + + 'the field whose value picks each entry\'s marker colour and the one binding with no other spelling — ' + + 'and the page-component row declared it `z.unknown()`, because that contract was objectui\'s alone. So a ' + + 'bare field name, a non-string binding or a misspelled member passed the component-props gate, and the ' + + 'rail bound nothing for it and drew the default field. The spec now declares objectui\'s own declaration ' + + 'of the binding record, four optional field names, closed as every element shape on that map is. It is ' + + 'read where every page component\'s props are: the component-props gate reports a refused value as an ' + + 'advisory `component-props-invalid` / `component-props-unknown-key` finding on `objectstack validate`, ' + + '`objectstack build` and `objectstack lint`, and a stored page still saves and loads, because a page ' + + 'component\'s `properties` is not parsed on the metadata save or load path. No conversion is ' + + 'registered: nothing on the load path refuses the shape, and the authored census found nothing to ' + + 'respell. Deployed metadata NOT MEASURED.', + acceptanceCriteria: 'Every `object-timeline` node validates: `objectstack validate` reports no ' + + '`component-props-invalid` / `component-props-unknown-key` finding under `properties.mapping`. Each ' + + 'timeline that sets a mapping draws its entries\' title, date, description and marker colour from the ' + + 'fields it names.', + }, { id: 'ui-react-list-view-binding-aliases-retired', surface: '`kind:\'react\'` page source — `` and `` ' diff --git a/packages/spec/src/ui/component-objectui-held-typed-members.pin.test.ts b/packages/spec/src/ui/component-objectui-held-typed-members.pin.test.ts new file mode 100644 index 00000000000..cb98344324a --- /dev/null +++ b/packages/spec/src/ui/component-objectui-held-typed-members.pin.test.ts @@ -0,0 +1,244 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#21464, the S-objectui-held stage] The objectui-held contracts this stage + * could write down and type: `object-gantt` `markers`, `object-timeline` + * `mapping`, and the top-level `fields` of `object-form` and + * `object-master-detail-form`. Each element contract was objectui's alone; the + * spec now declares it — objectui's own authoring declaration, taken as it + * stands — and the row takes it. The stage's other members (the drill-down's + * `report`, the form's `customFields` and both forms' `sections`, the + * timeline's `items`, the action containers' members) are forks the stage + * reported, held in the enumeration pin's ledger with the shapes each could + * take. + * + * ## The defect this file closes + * + * Each member is read with one shape (measured at the `.objectui-sha` pin + * `ab1879721595`, unchanged at objectui `main` `94985a92ba`; the read points + * are in the schemas' docblocks), and the rows declared them `z.unknown()`. So + * a marker with no `date`, a numeric `date`, a misspelled marker member, a + * `mapping` written as a bare field name or keyed `titleField`, and a form + * `fields` entry written `{ name }` or `{ field }` all passed the + * component-props gate, and the renderer drew no line, bound the default field + * or dropped the entry's label, type and required — with, at most, a console + * warning. + * + * ## What is pinned, and why each half + * + * - §1 THE DECLARED SHAPES PARSE: every shape a measured writer authors parses + * byte-identical — none of the three carries a default, so the parsed value + * IS the authored one. A refusal pin with no lit control passes just as well + * when the door refuses everything. + * - §2 THE REFUSALS: an off-shape value of each member is refused with the + * code AND the path, so a refusal for the wrong reason reds; the form's two + * object entries carry their prescriptions. + * - §3 ONE DECLARATION: each element shape declares exactly the members + * objectui's declaration names, and the two forms' `fields` are one schema. + * - §4 THE REGISTRATION: the ADR-0087 D3 entries step 18 carries. + * + * The enumeration pin (`component-props-unknown-members.pin.test.ts`) holds the + * other half: these four left its ledger, so a member reverted to + * `z.unknown()` reds there. + */ + +import { describe, it, expect } from 'vitest'; +import type { z } from 'zod'; + +import { + ComponentPropsMap, + ObjectFormPropsSchema, + ObjectGanttPropsSchema, + ObjectMasterDetailFormPropsSchema, + ObjectTimelinePropsSchema, +} from './component.zod'; +import { MIGRATIONS_BY_MAJOR } from '../migrations/registry'; + +type Row = 'object-gantt' | 'object-timeline' | 'object-form' | 'object-master-detail-form'; +const BASE: Record> = { + 'object-gantt': { objectName: 'task' }, + 'object-timeline': { objectName: 'event' }, + 'object-form': { objectName: 'account' }, + 'object-master-detail-form': { objectName: 'invoice' }, +}; +const parse = (row: Row, props: Record) => + ComponentPropsMap[row].safeParse({ ...BASE[row], ...props }); + +/** The issue codes and paths a refusal carries, so a refusal for the WRONG reason reds. */ +function issues(result: z.ZodSafeParseResult): { code: string; path: string }[] { + if (result.success) return []; + return result.error.issues.map((i) => ({ code: i.code, path: i.path.join('.') })); +} +const firstMessage = (result: z.ZodSafeParseResult): string => + (result.success ? '' : result.error.issues[0]!.message); + +// ─────────────────────────────────────────────────────────────────────────── +// §1 the declared shapes parse +// ─────────────────────────────────────────────────────────────────────────── + +describe('§1 each member accepts every shape a measured writer authors', () => { + const BYTE_IDENTICAL: ReadonlyArray]> = [ + // objectui `plugin-gantt/src/__tests__/objectGanttInputs-11168.test.tsx:181`, `:692`, `:699`. + ['a full marker', 'object-gantt', { markers: [{ date: '2026-03-05', label: 'Deadline', color: 'red' }] }], + ['a bare marker', 'object-gantt', { markers: [{ date: '2026-03-05' }] }], + ['a labelled marker past the range', 'object-gantt', { markers: [{ date: '2031-03-05', label: 'Far' }] }], + // objectui `types/src/__tests__/gantt-declared-keys.test.ts:121`. + ['a marker with a hex colour', 'object-gantt', { markers: [{ date: '2024-06-05', label: 'Release', color: '#ef4444' }] }], + ['a date-time marker', 'object-gantt', { markers: [{ date: '2026-07-01T09:00:00Z', label: 'Go-live' }] }], + ['no markers', 'object-gantt', { markers: [] }], + // objectui `plugin-timeline/src/__tests__/objectTimelineInputs-11168.test.tsx:193`, `:418`, `:448`, `:453`. + ['a full mapping', 'object-timeline', { mapping: { title: 'code', date: 'start', description: 'summary', variant: 'kind' } }], + ['a title and date mapping', 'object-timeline', { mapping: { title: 'code', date: 'finish' } }], + ['a description mapping', 'object-timeline', { mapping: { description: 'code' } }], + ['a variant mapping', 'object-timeline', { mapping: { variant: 'kind' } }], + // This package's own `component-element-navigation-17987.test.ts:220`. + ['a title and variant mapping', 'object-timeline', { mapping: { title: 'subject', variant: 'status' } }], + ['an empty mapping', 'object-timeline', { mapping: {} }], + // objectui's page-builder guide at `main` (`skills/objectui/guides/page-builder.md`). + ['form field names', 'object-form', { fields: ['name', 'email'] }], + ['no form fields', 'object-form', { fields: [] }], + // The showcase's project workspace (`examples/app-showcase/src/ui/pages/project-workspace.page.ts:47`). + ['master-detail parent field names', 'object-master-detail-form', { + fields: ['name', 'account', 'status', 'health', 'budget', 'end_date'], + }], + ]; + for (const [label, row, props] of BYTE_IDENTICAL) { + it(`${row}: ${label} parses byte-identical`, () => { + const r = parse(row, props); + expect(issues(r)).toEqual([]); + for (const key of Object.keys(props)) { + expect(r.success && (r.data as Record)[key]).toStrictEqual(props[key]); + } + }); + } + + it('an absent member stays absent on every row', () => { + for (const [row, key] of [ + ['object-gantt', 'markers'], + ['object-timeline', 'mapping'], + ['object-form', 'fields'], + ['object-master-detail-form', 'fields'], + ] as const) { + const r = parse(row, {}); + expect(issues(r), row).toEqual([]); + expect(r.success && r.data, row).not.toHaveProperty(key); + } + }); +}); + +// ─────────────────────────────────────────────────────────────────────────── +// §2 the refusals +// ─────────────────────────────────────────────────────────────────────────── + +describe('§2 an off-shape value is refused with the code and the path', () => { + const REFUSED: ReadonlyArray, code: string, path: string]> = [ + ['a number for markers', 'object-gantt', { markers: 42 }, 'invalid_type', 'markers'], + ['a bare date string as a marker', 'object-gantt', { markers: ['2026-07-01'] }, 'invalid_type', 'markers.0'], + ['a marker with no date', 'object-gantt', { markers: [{ label: 'Deadline' }] }, 'invalid_type', 'markers.0.date'], + // objectui `types/src/__tests__/gantt-declared-keys.test.ts:168`, its own `@ts-expect-error` probe. + ['a numeric marker date', 'object-gantt', { markers: [{ date: 5 }] }, 'invalid_type', 'markers.0.date'], + ['a marker `title`', 'object-gantt', { markers: [{ date: '2026-07-01', title: 'Freeze' }] }, 'unrecognized_keys', 'markers.0'], + ['a marker `colour`', 'object-gantt', { markers: [{ date: '2026-07-01', colour: 'red' }] }, 'unrecognized_keys', 'markers.0'], + ['a bare field name for mapping', 'object-timeline', { mapping: 'subject' }, 'invalid_type', 'mapping'], + ['a non-string mapping member', 'object-timeline', { mapping: { title: 5 } }, 'invalid_type', 'mapping.title'], + ['a `titleField` inside mapping', 'object-timeline', { mapping: { titleField: 'code' } }, 'unrecognized_keys', 'mapping'], + ['a `color` inside mapping', 'object-timeline', { mapping: { color: 'kind' } }, 'unrecognized_keys', 'mapping'], + ['a bare string for fields', 'object-form', { fields: 'name' }, 'invalid_type', 'fields'], + // objectui `plugin-form/src/__tests__/objectFormFieldsMembers-8071.test.tsx:177`, `:182` — the stored-read probe. + ['a `{ name }` field entry', 'object-form', { fields: [{ name: 'note' }] }, 'invalid_type', 'fields.0'], + // objectui `plugin-form/src/__tests__/objectFormFieldsMembers-8071.test.tsx:140` — the warn probe. + ['a `{ field }` field entry', 'object-form', { fields: [{ field: 'note' }] }, 'invalid_type', 'fields.0'], + ['a numeric field entry', 'object-form', { fields: [5] }, 'invalid_type', 'fields.0'], + // objectui's page-builder guide at `.objectui-sha` (respelled on objectui `main`). + ['the guide\'s retired inline field', 'object-form', { + fields: [{ name: 'name', label: 'Name', type: 'text', required: true }, 'email'], + }, 'invalid_type', 'fields.0'], + // objectui `plugin-form/src/__tests__/topLevelFieldsWarnCoverage-8847.test.tsx:258`, `:110`. + ['a master-detail `{ name }` entry beside a name', 'object-master-detail-form', { + fields: [{ name: 'note' }, 'status'], + }, 'invalid_type', 'fields.0'], + ['a master-detail `{ field }` entry', 'object-master-detail-form', { fields: [{ field: 'note' }] }, 'invalid_type', 'fields.0'], + ]; + for (const [label, row, props, code, path] of REFUSED) { + it(`${row}: refuses ${label} — ${code} at ${path}`, () => { + const r = parse(row, props); + expect(r.success).toBe(false); + expect(issues(r)).toEqual([{ code, path }]); + }); + } + + it('a `{ name }` entry is told to write the bare name, and where an override goes', () => { + for (const row of ['object-form', 'object-master-detail-form'] as const) { + const message = firstMessage(parse(row, { fields: [{ name: 'note', label: 'Note' }] })); + expect(message, row).toMatch(/write `'note'`, not an object/); + expect(message, row).toMatch(/a per-form override goes on a `sections\[\]\.fields` entry/); + } + }); + + it('a `{ field }` entry is told it is the section vocabulary', () => { + for (const row of ['object-form', 'object-master-detail-form'] as const) { + const message = firstMessage(parse(row, { fields: [{ field: 'note' }] })); + expect(message, row).toMatch(/`\{ field: 'note' \}` is the `sections\[\]\.fields` vocabulary/); + expect(message, row).toMatch(/Write `'note'`, or move the entry into a section's `fields`/); + } + }); + + it('a numeric entry carries no form prescription — only an object entry is told what to write', () => { + expect(firstMessage(parse('object-form', { fields: [5] }))).not.toMatch(/sections\[\]\.fields/); + }); + + it('a misspelled marker or mapping member is pointed at the declared one', () => { + expect(firstMessage(parse('object-gantt', { markers: [{ date: '2026-07-01', title: 'Freeze' }] }))).toMatch(/`label`/); + expect(firstMessage(parse('object-timeline', { mapping: { titleField: 'code' } }))).toMatch(/`title`/); + }); +}); + +// ─────────────────────────────────────────────────────────────────────────── +// §3 one declaration +// ─────────────────────────────────────────────────────────────────────────── + +describe('§3 each shape is objectui\'s declaration, and the two forms share one', () => { + /** The element schema of an optional array member, or the member itself. */ + const objectKeys = (member: z.ZodType): string[] => { + let s = member as unknown as { unwrap?: () => unknown; element?: unknown; shape?: Record }; + while (typeof s.unwrap === 'function') s = s.unwrap() as typeof s; + if (s.element) s = s.element as typeof s; + while (typeof s.unwrap === 'function') s = s.unwrap() as typeof s; + return Object.keys(s.shape ?? {}).sort(); + }; + + it('a gantt marker declares exactly `date`, `label` and `color` — objectui `ObjectGanttSchema.markers`', () => { + expect(objectKeys(ObjectGanttPropsSchema.shape.markers)).toEqual(['color', 'date', 'label']); + }); + + it('a timeline mapping declares exactly the four bindings — objectui `TimelineMappingSchema`', () => { + expect(objectKeys(ObjectTimelinePropsSchema.shape.mapping)).toEqual(['date', 'description', 'title', 'variant']); + }); + + it('`object-master-detail-form` `fields` answers exactly as `object-form` `fields` does', () => { + const form = ObjectFormPropsSchema.shape.fields; + const md = ObjectMasterDetailFormPropsSchema.shape.fields; + for (const value of [['a', 'b'], [{ name: 'a' }], [{ field: 'a' }], [5], 'a', [], [{ name: 'a', label: 'A' }]]) { + const a = form.safeParse(value); + const b = md.safeParse(value); + expect(b.success, JSON.stringify(value)).toBe(a.success); + expect(b.success ? [] : b.error.issues.map((i) => i.message)) + .toEqual(a.success ? [] : a.error.issues.map((i) => i.message)); + } + }); +}); + +// ─────────────────────────────────────────────────────────────────────────── +// §4 the registration +// ─────────────────────────────────────────────────────────────────────────── + +describe('§4 each narrowing is registered as the ADR-0087 D3 entry step 18 carries', () => { + const ids = MIGRATIONS_BY_MAJOR[18]!.semantic.map((s) => s.id); + it.each([ + 'ui-object-gantt-markers-typed', + 'ui-object-timeline-mapping-typed', + 'ui-object-form-fields-names-typed', + ])('%s', (id) => { + expect(ids).toContain(id); + }); +}); diff --git a/packages/spec/src/ui/component-props-unknown-members.pin.test.ts b/packages/spec/src/ui/component-props-unknown-members.pin.test.ts index 051afbbab96..d448ecb5c74 100644 --- a/packages/spec/src/ui/component-props-unknown-members.pin.test.ts +++ b/packages/spec/src/ui/component-props-unknown-members.pin.test.ts @@ -38,13 +38,16 @@ * since stage 5, which exited its hold once objectui retired the grid's read of * a column `options`. The form family (`object-form` `contentLayout` / * `submitBehavior` / `navigateOnSuccess` / `mobile`) in - * `component-form-family-typed-members.pin.test.ts`; its `fields` and - * `sections`, and the master-detail form's two, are held below, and its - * `customFields` waits with the objectui-held contracts. The metric tile + * `component-form-family-typed-members.pin.test.ts`. The metric tile * (`object-metric` `aggregate` / `trend` / `drillDown` / `compareTo`) in - * `component-metric-family-typed-members.pin.test.ts`; the drill-down's - * `report` waits below with the objectui-held contracts, until the spec - * declares a drill report. + * `component-metric-family-typed-members.pin.test.ts`. The objectui-held + * contracts the last stage could type (`object-gantt` `markers`, + * `object-timeline` `mapping`, and the field-name `fields` of `object-form` + * and `object-master-detail-form`) in + * `component-objectui-held-typed-members.pin.test.ts`; the rest of them are + * held below as forks — the drill-down's `report`, the form's `customFields` + * and both forms' `sections`, the timeline's `items` and the action + * containers' members. * * ## The STAGED reason is debt, not a verdict * @@ -55,6 +58,11 @@ * `staged` lines: a stage that types a member deletes its line here (§1's * second half enforces that), and ⛔ a NEW renderer-read member is typed, never * added as `staged`. + * + * A `fork` line is a member the stage that owned it measured and did NOT type + * under the stop valve: its contract has two or more viable shapes that no + * ruling decides. The line names the shapes (§2 checks there are at least + * two), and the fork is on the card with its census, for a ruling. */ import { describe, it, expect } from 'vitest'; @@ -132,7 +140,7 @@ function unknownMembers(schema: unknown): UnknownMember[] { * would refuse a measured writer is reported, not shipped. */ const STAGES = { - 'objectui-held': 'element contracts the spec does not declare yet, whose declaration is still objectui\'s (`GanttMarker`, `TimelineMappingSchema`, the timeline items, `UIActionSchema` — an objectui interface that borrows some members from the spec `Action` — and the runtime form field `FormField`, identity key `name`), and the metric drill-down\'s `report`, which objectui types as the spec\'s own `ReportSchema` input while no spec drill shape declares a `report` member (the chart\'s drill-down refuses it); the spec declares each first, contract-first, then the row takes it', + 'fork': 'element contracts whose declaration is still objectui\'s (the timeline items, `UIActionSchema` — an objectui interface that borrows some members from the spec `Action` — and the runtime form field `FormField`, identity key `name`, which a form section\'s inline entry is too) and the metric drill-down\'s `report`: the S-objectui-held stage, the last of #21464, measured each and found two or more viable spec shapes that no ruling decides, so under the stop valve each is held and its fork reported on the card with its census; the member is typed once a ruling picks a shape', 'held-for-decision': 'a typed shape exists (by reference, or the renderer\'s own declared type), but measured writers author values it refuses that the renderer draws — the narrowing waits for a ruling', } as const; type Stage = keyof typeof STAGES; @@ -150,8 +158,12 @@ type Reason = | { readonly kind: 'any-value'; readonly why: string } /** A deliberately open bag: the declared members are typed, the rest pass through. */ | { readonly kind: 'open-bag'; readonly why: string } - /** Read with a fixed shape at the pin (`reader`); typing it is a named later stage. */ - | { readonly kind: 'staged'; readonly stage: Stage; readonly reader: string }; + /** + * Read with a fixed shape at the pin (`reader`); typing it is a named later + * stage. A `fork` line also names the viable shapes no ruling has chosen + * between (`shapes`). + */ + | { readonly kind: 'staged'; readonly stage: Stage; readonly reader: string; readonly shapes?: readonly string[] }; const EXPRESSION_AST: Reason = { kind: 'shared', @@ -177,11 +189,12 @@ const RECORDS: Reason = { kind: 'records' }; const SLOT: Reason = { kind: 'slot' }; const RUNNER: Reason = { kind: 'runner' }; const staged = (stage: Stage, reader: string): Reason => ({ kind: 'staged', stage, reader }); +const fork = (reader: string, shapes: readonly string[]): Reason => ({ kind: 'staged', stage: 'fork', reader, shapes }); /** * `ObjectUI` source paths are at the `.objectui-sha` pin `89cad75d55`, except - * the `object-metric` `drillDown.report` line, read at the `.objectui-sha` pin - * `ab1879721595`. + * the `fork` lines, read at the `.objectui-sha` pin `ab1879721595` (each read + * point unchanged at objectui `main` `94985a92ba`). */ const LEDGER = new Map(); const on = (types: readonly string[], paths: readonly string[], reason: Reason): void => { @@ -233,24 +246,73 @@ on(['object-grid'], ['pagination.*'], { why: '`z.looseObject` on purpose: `pageSize` and `pageSizeOptions` are typed and are the only members a read point names; the member\'s own docblock records why the bag stays open', }); -// Read with a fixed shape at the pin — the later stages. +// Read with a fixed shape at the pin — the forks the S-objectui-held stage +// reported, and the one member still held for a ruling. +// // The metric tile's four members are typed (stages 4 and 5); the drill-down's // `report` is not. The tile hands it to the shared drawer, which draws a // dataset-bound report (`isDatasetBoundReport`) and lists the records for any -// other value; objectui types it as the spec's own `ReportSchema` input, but no -// spec drill shape declares a `report` member — the chart's drill-down refuses -// it — so the spec declares that contract first. -on(['object-metric'], ['drillDown.report'], staged('objectui-held', 'plugin-dashboard/src/DrillDownDrawer.tsx:92 (`isDatasetBoundReport`), used at :115; handed over at ObjectMetricWidget.tsx:742')); +// other value. Measured against it, the by-reference candidate admits a joined +// report with no dataset-bound block, which the drawer does not draw, and +// refuses a dataset-bound report with no name, label or values, which it does. +on(['object-metric'], ['drillDown.report'], fork( + 'plugin-dashboard/src/DrillDownDrawer.tsx:92 (`isDatasetBoundReport`), used at :115; handed over at ObjectMetricWidget.tsx:742', + [ + '`ReportSchema` by reference, as it stands: a joined report whose blocks bind no dataset is accepted and lists the records', + '`ReportSchema` once a joined report\'s blocks must each bind a dataset, as its own refinement comment says they do', + 'a drill-report shape of its own, the two arms the drawer draws', + ], +)); // The form's inline members are objectui's runtime form field (`FormField`, // identity key `name`), merged over the generated set and drawn whole; the spec // declares no such field — its own form field is keyed by `field`, and the -// merge never matches it — so the spec declares that contract first. -on(['object-form'], ['customFields'], staged('objectui-held', 'plugin-form/src/customFieldsMerge.ts:78-108 (`FormField`, by `name`), from ObjectForm.tsx:755, :1180')); -on(['object-gantt'], ['markers[]'], staged('objectui-held', 'plugin-gantt/src/ObjectGantt.tsx:2497 (`GanttMarker`)')); -on(['object-timeline'], ['items[]'], staged('objectui-held', 'plugin-timeline/src/ObjectTimeline.tsx:587')); -on(['object-timeline'], ['mapping'], staged('objectui-held', 'plugin-timeline/src/ObjectTimeline.tsx:551, :576-579')); -on(['action:group'], ['actions[]{}'], staged('objectui-held', 'components/src/renderers/action/action-group.tsx:303 (`UIActionSchema[]`)')); -on(['action:menu'], ['actions[]{}'], staged('objectui-held', 'components/src/renderers/action/action-menu.tsx:339 (`UIActionSchema[]`)')); +// merge never matches it. objectui's field is open (an index signature) and +// eight of its forty-five members are the grid widget's snake_case keys. +on(['object-form'], ['customFields'], fork( + 'plugin-form/src/customFieldsMerge.ts:78-108 (`FormField`, by `name`), from ObjectForm.tsx:755, :1180', + [ + 'objectui\'s `FormField` as it stands, open, with its snake_case grid keys', + 'a closed spec runtime field of the members a form draws, the grid keys camelCased or left out', + 'the spec\'s own `FormFieldSchema` re-keyed by `name`', + ], +)); +// A section's `fields` draws, beside a name and the form view's `{ field }` +// entry, an inline runtime form field as it stands ("shape 3") — kept by +// objectui#11550's ruling and declared by objectui +// (`ObjectFormSection.fields: (string | FormField)[]`). So this member takes a +// shape once the spec declares the runtime form field: `customFields`'s fork. +on(['object-form', 'object-master-detail-form'], ['sections[]'], fork( + 'plugin-form/src/sectionFields.ts:369-370 (shape 3), reached from ObjectForm.tsx:364, :1518 and every sectioned arm; the master-detail form hands it on at MasterDetailForm.tsx:1692', + [ + 'the form view\'s `FormSectionSchema`, its field entry widened by the runtime form field `customFields` declares', + 'a page-block section shape of its own, the form view\'s section keys plus the three entry arms the form reads', + ], +)); +// The authored timeline entry is objectui's (`TimelineFeedItem` / +// `TimelineGanttItem`): a feed entry's `content` is child schema nodes, and the +// arm an entry must match is chosen by the parent's `variant`. +on(['object-timeline'], ['items[]'], fork( + 'plugin-timeline/src/ObjectTimeline.tsx:587, into renderer.tsx (`TimelineFeedItem` / `TimelineGanttItem`, types/src/data-display.ts:2973, :3042)', + [ + 'the two arms with `content` a slot position the page walks judge, and the arm chosen by a row refinement on `variant`', + 'the two arms with `content` an opaque member and a plain union of the arms', + ], +)); +// Each member is objectui's `UIActionSchema`, drawn and run by the container. +on(['action:group'], ['actions[]{}'], fork( + 'components/src/renderers/action/action-group.tsx:303 (`UIActionSchema[]`), members at :91-249, run at :329-382', + [ + 'the read set, `action:button`\'s keys by `type`, without the keys the rows leave undecided', + 'the read set with `outcomeMessages`, a member `className` and the member `properties.params` bag declared', + ], +)); +on(['action:menu'], ['actions[]{}'], fork( + 'components/src/renderers/action/action-menu.tsx:342 (`UIActionSchema[]`), members at :80-147, :408, run at :264-328', + [ + 'the read set, `action:button`\'s keys by `type`, without the keys the rows leave undecided', + 'the read set with `outcomeMessages`, a member `className` and the member `properties.params` bag declared', + ], +)); // The list view's own `conditionalFormatting` is the by-reference shape, as on // `object-grid` (#21445) — but objectui's own kanban fixtures author both rule // dialects it refuses (`plugin-kanban/src/__tests__/ObjectKanban. @@ -258,27 +320,6 @@ on(['action:menu'], ['actions[]{}'], staged('objectui-held', 'components/src/ren // `types/src/__tests__/kanban-conditional-formatting.test.ts:29-52`), so the // narrowing is reported for a ruling instead of shipped. on(['object-kanban'], ['conditionalFormatting'], staged('held-for-decision', 'plugin-kanban/src/KanbanBoardCore.tsx:114, evaluated at KanbanImpl.tsx:179 (`resolveConditionalFormatting`)')); -// The renderer's own declared type for the form's `fields` is field-name -// strings (`ObjectFormSchema.fields: string[]`), but its read also draws a -// `{ name }` entry by that name, and measured writers author one: objectui's -// published page-builder guide (`skills/objectui/guides/page-builder.md:263`), -// its field-security and system-managed payload pins, and the `{ name }` row it -// pins as behaviour (`plugin-form/src/__tests__/objectFormFieldsMembers-8071.test.tsx:165-167`). -// Typing the member to strings would refuse a value the form draws. -on(['object-form'], ['fields[]'], staged('held-for-decision', 'plugin-form/src/ObjectForm.tsx:961-981 and flatFields.ts:71-79, a `{ name }` entry drawn by that name')); -// The form view's own `sections` is the by-reference shape, and every section -// key the form reads is declared there — but a section's `fields` also draws an -// inline runtime form field `{ name, type, … }` as it stands ("shape 3"), which -// the form view's field entry (keyed by `field`) refuses; objectui's README -// (`plugin-form/README.md:764`) and its submit-target pins -// (`plugin-form/src/submitTargetRefusal.test.tsx:331-358`) author that shape -// and assert that it renders and submits. -on(['object-form'], ['sections[]'], staged('held-for-decision', 'plugin-form/src/sectionFields.ts:367-369 (shape 3), reached from ObjectForm.tsx:364, :1518 and every sectioned arm')); -// The master-detail form hands both to its parent `object-form` verbatim, so -// each is read exactly as that block's member is, and held with it; its own -// `fields` writers author the `{ name }` entry too -// (`plugin-form/src/__tests__/topLevelFieldsWarnCoverage-8847.test.tsx:254-257`). -on(['object-master-detail-form'], ['sections[]', 'fields[]'], staged('held-for-decision', 'plugin-form/src/MasterDetailForm.tsx:1692-1693, into the parent form, read as `object-form`\'s')); /** Every `z.unknown()` member of every row, keyed as the ledger keys it. */ function census(): Map { @@ -352,6 +393,18 @@ describe('§2 each recorded reason holds', () => { } }); + it('a `fork` line names the two or more shapes no ruling has chosen between, and only a fork does', () => { + for (const [key, reason] of entries) { + if (reason.kind !== 'staged') continue; + if (reason.stage === 'fork') { + expect(reason.shapes?.length ?? 0, key).toBeGreaterThanOrEqual(2); + expect(new Set(reason.shapes).size, `${key}: each shape is a different one`).toBe(reason.shapes!.length); + } else { + expect(reason.shapes, key).toBeUndefined(); + } + } + }); + it('a `shared` member really is the owner\'s: an `ast` beside a `source`, a request beside a `url`', () => { for (const [key, reason] of entries) { if (reason !== EXPRESSION_AST) continue; diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index b3bba43a354..7e4b37622a5 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -3521,6 +3521,22 @@ export type ActionIconPropsParsed = z.infer; * `actionNames`, and a string member would render as an unlabeled button that * runs nothing. */ +/** + * [#21464] The members of `action:group` / `action:menu`: HELD as open + * records, in the enumeration pin's ledger as a fork the S-objectui-held stage + * reported. Each member is objectui's `UIActionSchema` + * (`types/src/ui-action.ts:571` at the `.objectui-sha` pin `ab1879721595`), + * drawn and run by the container itself (`action-group.tsx:91-249`, + * `:303-382`; `action-menu.tsx:80-147`, `:264-328`, `:408`). Measured from + * those reads, the key set this section's method gives is mostly + * `action:button`'s, keyed by `type` rather than `actionType` (a member is not + * a node) — but it also takes keys whose declaration the rows + * above leave undecided: `outcomeMessages` (forwarded, and recorded on + * `action:button` / `action:icon` as "a contract decision, not a pin + * re-measure"), a member `className` (a node key on the rows), the member's own + * `properties.params` bag of static values (`static-params.ts:142-160`) and + * `endpoint` (refused on the rows since #21005). + */ const actionMemberList = () => z.array(z.record(z.string(), z.unknown())); /** @@ -4709,8 +4725,9 @@ const ObjectMetricDrillDownSchema = lazySchema(() => strictObject({ maxRows: ChartDrillDownSchema.shape.maxRows, /** * [#21464] HELD at `z.unknown()`, not typed: the spec declares no drill - * report yet, and the spec declares each such contract first (the - * `objectui-held` stage of the enumeration pin). + * report yet, and the spec declares each such contract first. It waited for + * the `objectui-held` stage, which reported it as a fork (the last paragraph + * below); the enumeration pin's ledger records it under `fork`. * * Read at the `.objectui-sha` pin `ab1879721595`: the tile hands `report` * to the shared drawer (`ObjectMetricWidget.tsx:742`), and @@ -4727,6 +4744,16 @@ const ObjectMetricDrillDownSchema = lazySchema(() => strictObject({ * the conclusion stage 4 recorded stands: the tile draws a value the * by-reference drill shape refuses, and the member waits for the spec to * declare it. + * + * The S-objectui-held stage measured the by-reference candidate + * (`ReportSchema`, `report.zod.ts`) against that predicate and kept the hold, reporting + * a fork: every drawn report the census found parses, but the two do not + * agree. `ReportSchema` admits a `joined` report none of whose blocks binds a + * `dataset` (a block's `dataset` is optional there), which + * `isDatasetBoundReport` does not draw — the drawer lists the records + * instead, the silent fallback this card closes — and it refuses a + * dataset-bound report with no `name`, `label` or `values`, which the drawer + * does draw. */ report: z.unknown().optional() .describe('Drill into a report instead of the record list — not typed on this row yet: the tile draws a dataset-bound report here, but no spec drill shape declares a `report` member yet (the chart\'s drill-down refuses it)'), @@ -5779,6 +5806,46 @@ const ObjectFormMobileSchema = lazySchema(() => strictObject({ .describe('Offer a fullscreen editor on textarea and rich-text fields'), })); +/** + * The refusal an object entry in a form's top-level `fields` meets: the two + * object shapes authors bring, each answered with what to write instead. + */ +function formFieldNameRefusal(input: unknown): string | undefined { + if (input === null || typeof input !== 'object' || Array.isArray(input)) return undefined; + const entry = input as { name?: unknown; field?: unknown }; + if (typeof entry.field === 'string') { + return `\`{ field: '${entry.field}' }\` is the \`sections[].fields\` vocabulary (the form view's field entry), ` + + `not this list's: the top-level \`fields\` takes bare field names, and the form skips an object entry ` + + `here. Write \`'${entry.field}'\`, or move the entry into a section's \`fields\`.`; + } + if (typeof entry.name === 'string') { + return `The top-level \`fields\` takes bare field names: write \`'${entry.name}'\`, not an object. The form ` + + 'draws only the name of an object entry — a `label`, `type` or `required` written beside it is dropped. ' + + 'A field\'s label, type and required come from the object field; a per-form override goes on a ' + + '`sections[].fields` entry.'; + } + return undefined; +} + +/** + * [#21464] A form's top-level `fields` — `object-form`'s, and the + * master-detail form's, which `MasterDetailForm.tsx:1693` hands to the parent + * form verbatim — as the form reads it at the `.objectui-sha` pin + * `ab1879721595` (unchanged at objectui `main` `94985a92ba`): the field NAMES + * to draw, in order, which select from the object's fields and from + * `customFields` (`plugin-form/src/ObjectForm.tsx:961-981`, + * `flatFields.ts:68-79`). objectui declares the member `string[]` + * (`ObjectFormSchema.fields`), and objectstack-ai/objectui#11550's ruling + * retired the `{ name }` entry from every authoring face; the form still draws + * a STORED `{ name }` entry by its name (`:972`, `flatFields.ts:72`) as + * tolerance for metadata already written, and skips any other object entry with + * a console warning (`:978`, `warnUnresolvedTopLevelField`). So this is a list + * of strings, and each object entry is refused with what to write instead. + */ +const formFieldNameList = () => z.array(z.string({ + error: (issue) => formFieldNameRefusal(issue.input), +})); + /** * `object-form` (objectui `plugin-form/src/ObjectForm.tsx` @ `eb7f586b`, plus * the sub-forms it forwards the whole bag into: `TabbedForm`, `WizardForm`, @@ -5815,34 +5882,41 @@ export const ObjectFormPropsSchema = lazySchema(() => strictObject({ .describe("Field layout — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set `columns`"), columns: z.number().optional().describe('Number of field columns (multi-column forms), honoured under either `layout`'), /** - * [#21464] HELD at `z.unknown()` entries, in the enumeration pin's ledger as - * held for a ruling. The renderer's own declared type is field-name strings - * (objectui `ObjectFormSchema.fields: string[]`), but its read also draws a - * `{ name }` entry by that name (`plugin-form/src/ObjectForm.tsx:961-981`, - * `flatFields.ts:71-79` at the pin `89cad75d55`), and measured writers author - * one — objectui's published page-builder guide among them — so typing the - * member to strings would refuse a value the form draws. + * [#21464] Field names, typed in the S-objectui-held stage — see + * {@link formFieldNameList}. Held until then because the form drew a + * `{ name }` entry its own guide taught; objectstack-ai/objectui#11550 retired + * that entry from the authoring faces, and the census at the `.objectui-sha` + * pin `ab1879721595` found it written only by fixtures probing the stored + * read and the refusal. */ - fields: z.array(z.unknown()).optional().describe('Limit/order the fields shown'), + fields: formFieldNameList().optional() + .describe('Field names to draw, in order — bare names selecting from the object\'s fields and from `customFields`. A `{ name }` or `{ field }` object entry is refused: a per-form label or required override goes on a `sections[].fields` entry'), /** - * [#21464] Kept `z.unknown()`, in the enumeration pin's ledger with the - * objectui-held contracts: each member is objectui's runtime form field - * (`FormField`, identity key `name`), drawn whole by the form renderer - * (`plugin-form/src/customFieldsMerge.ts:78-108` at the pin `89cad75d55`). - * The spec declares no such field — its own form field (`FormFieldSchema`, - * `view.zod.ts`) is keyed by `field` and is never matched by the merge — so - * the spec declares that contract first, then this member takes it. + * [#21464] Kept `z.unknown()`, in the enumeration pin's ledger as a fork the + * S-objectui-held stage reported: each member is objectui's runtime form + * field (`FormField`, identity key `name`, `types/src/form.ts:1770` at the + * `.objectui-sha` pin `ab1879721595`), drawn whole by the form renderer + * (`plugin-form/src/customFieldsMerge.ts:78-108`), and the spec declares no + * such field — its own form field (`FormFieldSchema`, `view.zod.ts`) is keyed + * by `field` and is never matched by the merge. Writing that declaration + * here needs decisions no ruling has made: objectui's field is OPEN (an index + * signature beside forty-five members, `:1906`), and eight of them are the + * grid widget's snake_case keys (`min_rows`, `allow_add`, …), which this + * package's camelCase rule for config keys does not admit as written. */ customFields: z.unknown().optional().describe('Custom field definitions merged into the generated set'), /** - * [#21464] HELD at `z.unknown()` entries, in the enumeration pin's ledger as - * held for a ruling. The form view's own `sections` (`FormSectionSchema`) is - * the by-reference shape, and every section key the renderer reads is - * declared there, but a section's `fields` also draws an inline runtime form - * field `{ name, type, … }` as it stands (`plugin-form/src/sectionFields.ts:367-369` - * at the pin `89cad75d55`, its "shape 3"), which the form view's field entry - * (keyed by `field`) refuses; objectui's README and its own pins author that - * shape and assert that it renders and submits. + * [#21464] HELD at `z.unknown()` entries, in the enumeration pin's ledger + * with `customFields`'s fork. The form view's own `sections` + * (`FormSectionSchema`) is the by-reference shape, and every section key the + * renderer reads is declared there, but a section's `fields` also draws an + * inline runtime form field `{ name, type, … }` as it stands + * (`plugin-form/src/sectionFields.ts:369-370` at the `.objectui-sha` pin + * `ab1879721595`, its "shape 3"), which the form view's field entry (keyed by + * `field`) refuses. objectstack-ai/objectui#11550's ruling KEPT that entry — + * objectui declares it (`ObjectFormSection.fields: (string | FormField)[]`) + * and its README's data-source-free wizard relies on it — so this member + * takes a shape only once the spec declares the runtime form field. */ sections: z.array(z.unknown()).optional() .describe('Form sections ({ label, description?, fields } — wizard steps / tab panes)'), @@ -6105,13 +6179,16 @@ export const ObjectMasterDetailFormPropsSchema = lazySchema(() => strictObject({ }).optional().describe("Parent form presentation — the two variants the renderer honours for the parent half"), /** * [#21464] `sections` and `fields` are handed to the parent `object-form` - * verbatim (`plugin-form/src/MasterDetailForm.tsx:1692-1693` at the pin - * `89cad75d55`), so each is read exactly as that block's member is — and is - * HELD with it, in the enumeration pin's ledger, for the same reason (see - * {@link ObjectFormPropsSchema}'s `sections` and `fields`). + * verbatim (`plugin-form/src/MasterDetailForm.tsx:1692-1693` at the + * `.objectui-sha` pin `ab1879721595`), so each is read exactly as that + * block's member is. `fields` takes the same field-name list + * ({@link formFieldNameList}); `sections` is HELD with that block's, in the + * enumeration pin's ledger, for the same reason (see + * {@link ObjectFormPropsSchema}'s `sections`). */ sections: z.array(z.unknown()).optional().describe('Parent form sections'), - fields: z.array(z.unknown()).optional().describe('Parent fields shown'), + fields: formFieldNameList().optional() + .describe('Parent field names to draw, in order — bare names, as on `object-form`; a `{ name }` or `{ field }` object entry is refused'), details: z.array(masterDetailDetailEntry()).optional() .describe('Detail collections — each a strict entry ({ childObject, title?, addLabel?, columns?, relationshipField?, … }) whose `columns` are the inline grid columns a relationship field\'s `inlineColumns` takes; the FK and columns auto-derive from child metadata when omitted'), title: I18nLabelSchema.optional().describe('Form title'), @@ -6510,6 +6587,45 @@ const OBJECT_GANTT_FLAT_CONFIG_GUIDANCE: readonly KeySetGuidance[] = [ }, ]; +/** + * [#21464] One `object-gantt` reference line — the element of the block's + * `markers`, as the chart reads it at the `.objectui-sha` pin `ab1879721595` + * (unchanged at objectui `main` `94985a92ba`). `ObjectGantt.tsx:2505` hands + * `schema.markers` to `GanttView` verbatim, which reads three members per + * marker: `date` places the line (`GanttView.tsx:913-924` re-bases it for the + * chart's time zone, `:2394-2413` maps it through the axis and DROPS a marker + * whose date does not parse or falls outside the drawn range), `label` is the + * text drawn against it, and `color` its CSS colour, the theme's primary colour + * when absent (`:2407`, drawn at `:4083-4100` and in the SVG export at + * `:3320-3332`). Nothing else is read off a marker. + * + * The contract is objectui's own authoring declaration, taken as it stands: + * `ObjectGanttSchema.markers` (`types/src/objectql.ts:3787-3794`) and its zod + * mirror (`types/src/zod/objectql.zod.ts:2738-2746`) declare + * `{ date: string; label?: string; color?: string }`, `date` a STRING because + * authored metadata is JSON and a `Date` cannot survive it (the runtime + * `GanttMarker` this feeds also takes a `Date`, which only a code-composed + * chart can hand it). Closed here, as every element shape on this map is: a + * misspelled member was dropped by the chart in silence. + */ +const ObjectGanttMarkerSchema = lazySchema(() => strictObject({ + surface: 'this `object-gantt` marker', + history: + 'Until this shape was declared, `markers` was `z.array(z.unknown())`: a marker with no `date`, a ' + + 'numeric `date` or a misspelled member passed, and the chart drew no line, or drew it with no label ' + + 'and in the default colour.', + aliases: { + at: 'date', day: 'date', when: 'date', + title: 'label', text: 'label', name: 'label', + colour: 'color', stroke: 'color', + }, +}, { + date: z.string() + .describe('Where the line stands — an ISO date (`2026-07-01`, read as that day on the chart\'s own calendar) or date-time string; a date that does not parse, or falls outside the drawn range, draws no line'), + label: z.string().optional().describe('Text drawn against the line'), + color: z.string().optional().describe('Line colour, any CSS colour (renderer default: the theme\'s primary colour)'), +})); + /** * `object-gantt` (objectui `plugin-gantt/src/ObjectGantt.tsx` plus the registry * shell `plugin-gantt/src/index.tsx`, read at the pin this repo builds against @@ -6641,8 +6757,10 @@ const OBJECT_GANTT_FLAT_CONFIG_GUIDANCE: readonly KeySetGuidance[] = [ * names the schema and this door takes it rather than `z.unknown()`. The * scalars below are read as their coercions say: `!!schema.readOnly`, * `schema.showBaselines !== false`, `schema.persistLayout === false`, - * `new Set(schema.holidays)`. `markers` stays `z.array(z.unknown())` — its - * element contract is `GanttView`'s `GanttMarker`, still objectui's. + * `new Set(schema.holidays)`. `markers` takes {@link ObjectGanttMarkerSchema} + * since #21464's S-objectui-held stage: objectui's own authoring declaration of + * the element, `{ date, label?, color? }` (it was `z.array(z.unknown())` while + * the element contract lived only in `GanttView`'s runtime `GanttMarker`). */ export const ObjectGanttPropsSchema = lazySchema(() => strictObject({ surface: 'this `object-gantt`', @@ -6698,8 +6816,8 @@ export const ObjectGanttPropsSchema = lazySchema(() => strictObject({ .describe('Opt OUT of layout and filter-chip persistence — only an explicit `false` disables it; the storage key is `objectName:viewName`'), viewName: z.string().optional() .describe("Layout-persistence scope, the second half of the `objectName:viewName` storage key (renderer default `'default'`)"), - markers: z.array(z.unknown()).optional() - .describe('Extra vertical reference lines drawn like the Today marker ({ date, label?, color? })'), + markers: z.array(ObjectGanttMarkerSchema).optional() + .describe('Extra vertical reference lines drawn like the Today marker — each `{ date, label?, color? }`: `date` places the line (a date outside the drawn range draws none), `label` is drawn against it, `color` paints it'), criticalPath: z.boolean().optional() .describe('Seed the critical-path highlight ON; the toolbar toggle stays available either way'), showBaselines: z.boolean().optional() @@ -7091,6 +7209,47 @@ const OBJECT_TIMELINE_FLAT_CONFIG_GUIDANCE: readonly KeySetGuidance[] = [ }, ]; +/** + * [#21464] The `object-timeline` block's `mapping` — the record-to-entry + * binding record, as the rail reads it at the `.objectui-sha` pin + * `ab1879721595` (unchanged at objectui `main` `94985a92ba`), in + * `ObjectTimeline.tsx`: `title` (`:551`) and `date` (`:576`) are each read + * BETWEEN the `timeline` block's member and the flat fallback, `description` + * (`:578`) ahead of the flat `descriptionField`, and `variant` (`:579`, default + * `'variant'`) is the field whose value picks each entry's marker colour — the + * one binding with no other spelling. Each is a FIELD NAME: the rail looks the + * record value up by it. + * + * The contract is objectui's own declaration, taken as it stands: the + * component's `mapping` prop (`:254-259`) and the `TimelineMappingSchema` it + * checks the node against (`:144-149`) both declare four optional strings. That + * check only warns, and its object is not strict, so a misspelled member + * (`titleField` inside `mapping`, say) bound nothing and drew the default; it is + * closed here, as every element shape on this map is. + */ +const ObjectTimelineMappingSchema = lazySchema(() => strictObject({ + surface: 'this `object-timeline` `mapping`', + history: + 'Until this shape was declared, `mapping` was `z.unknown()`: a non-string binding or a misspelled ' + + 'member passed, and the rail bound nothing for it and drew the default field.', + // The flat block-level spellings an author carries into the binding record. + aliases: { + titleField: 'title', + dateField: 'date', startDateField: 'date', + descriptionField: 'description', + variantField: 'variant', + }, +}, { + title: z.string().optional() + .describe('Field whose value is each entry\'s title — read after `timeline.titleField`, ahead of the flat `titleField` (renderer default `name`)'), + date: z.string().optional() + .describe('Field whose value is each entry\'s date — read after `timeline.startDateField` / `timeline.dateField`, ahead of the flat spellings'), + description: z.string().optional() + .describe('Field whose value is each entry\'s description — read ahead of `descriptionField` (renderer default `description`)'), + variant: z.string().optional() + .describe('Field whose value picks each entry\'s marker colour (renderer default `variant`) — the only spelling this binding has'), +})); + /** * `object-timeline` (objectui `plugin-timeline/src/ObjectTimeline.tsx`, the * presentational `plugin-timeline/src/renderer.tsx` it composes into, and the @@ -7228,11 +7387,15 @@ const OBJECT_TIMELINE_FLAT_CONFIG_GUIDANCE: readonly KeySetGuidance[] = [ * VALUE posture: `timeline` takes {@link TimelineConfigSchema}, the block * `ListViewSchema.timeline` already declares — one vocabulary, taken by * reference, so this element face cannot fork from the view face. - * `mapping` stays `z.unknown()`: its contract - * (`TimelineMappingSchema`) still lives in objectui, which is the - * `object-calendar.calendar` posture this section's header prescribes for - * exactly that case. `navigation` takes {@link NavigationConfigSchema}, by - * reference, for the reason the ruling gives. + * `mapping` takes {@link ObjectTimelineMappingSchema} since #21464's + * S-objectui-held stage: objectui's own declaration of the binding record (four + * optional field names), written here first and then taken; it was + * `z.unknown()` while that contract lived only in objectui + * (`TimelineMappingSchema`), the `object-calendar.calendar` posture this + * section's header prescribes for exactly that case. `items` stays + * `z.array(z.unknown())`: that stage found two viable spec shapes for the + * authored entry (see the member). `navigation` takes + * {@link NavigationConfigSchema}, by reference, for the reason the ruling gives. * * ⚠️ `variant: 'gantt'` is declared because the registration declares it * (`plugin-timeline/src/index.tsx:446`, in the one `OBJECT_TIMELINE_INPUTS` @@ -7346,6 +7509,21 @@ export const ObjectTimelinePropsSchema = lazySchema(() => strictObject({ */ data: z.array(z.unknown()).optional() .describe("Pre-fetched records — read FIRST as the rail's row source, ahead of the data-scope binding and the fetch, and composed into entries through the same `timeline` field bindings a fetched row takes; authoring it suppresses the object query entirely. Distinct from `items`, which is the already-composed entry shape and wins over this key when both are written"), + /** + * [#21464] HELD at `z.unknown()` elements, in the enumeration pin's ledger as + * a fork the S-objectui-held stage reported. Each element is objectui's + * declared authored timeline entry (`types/src/data-display.ts` at the + * `.objectui-sha` pin `ab1879721595`: `TimelineFeedItem`, `:2973`, or + * `TimelineGanttItem`, `:3042`, ruled on objectui#6356), handed to the rail + * verbatim (`ObjectTimeline.tsx:587`). Writing that contract here needs two + * decisions no ruling has made: a feed entry's `content` is child schema nodes + * (`SchemaNode | SchemaNode[]`), which this map either declares as a slot + * position the page walks judge or keeps as an opaque member; and the arm an + * entry must match is chosen by the PARENT's `variant`, which objectui judges + * in a node-level refinement and a spec row would either repeat or replace + * with a plain union of the two arms. (A gantt bar's dates also take a `Date` + * there, which authored JSON cannot carry.) + */ items: z.array(z.unknown()).optional() .describe("Static inline entries — read ahead of every record source, `data` above included, and bypasses the object query entirely (the renderer becomes a pass-through). Each element is objectui's declared timeline element, `@object-ui/types`'s `TimelineFeedItem` (`variant` absent / `vertical` / `horizontal`) or `TimelineGanttItem` (`variant: 'gantt'`), the arm this node's `variant` selects"), variant: z.enum(['vertical', 'horizontal', 'gantt']).optional() @@ -7360,8 +7538,8 @@ export const ObjectTimelinePropsSchema = lazySchema(() => strictObject({ .describe('Pin the gantt axis end (ISO `yyyy-mm-dd`) instead of deriving it from the rows; only a non-empty value is honoured'), descriptionField: z.string().optional() .describe("Field rendered as each entry's description (renderer default `description`). Declared FLAT because the `timeline` block has no member for it — it is the only spelling this binding has"), - mapping: z.unknown().optional() - .describe("Record-to-entry field mapping ({ title, date, description, variant }) — the objectui-side binding record read BETWEEN the `timeline` block and the flat fallbacks. Its `variant` member (the field whose value picks each marker colour, renderer default `variant`) is the only spelling that binding has"), + mapping: ObjectTimelineMappingSchema.optional() + .describe("Record-to-entry field mapping `{ title?, date?, description?, variant? }`, each a field name — the binding record read BETWEEN the `timeline` block and the flat fallbacks. Its `variant` member (the field whose value picks each marker colour, renderer default `variant`) is the only spelling that binding has"), /** * Entry-click navigation (commit e233db9db — the spec half of the objectui#8652 * maintainer ruling, verbatim `B`), the same carrier and the same def as From 09c3c7d2458d6ad8f0e429bf3470cbc4f0b69156 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 05:38:52 +0000 Subject: [PATCH 2/6] wip(spec): form-family pin header names the S-objectui-held outcome Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn Co-authored-by: Claude --- ...component-form-family-typed-members.pin.test.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/spec/src/ui/component-form-family-typed-members.pin.test.ts b/packages/spec/src/ui/component-form-family-typed-members.pin.test.ts index 8dbaa06bc64..e9335236667 100644 --- a/packages/spec/src/ui/component-form-family-typed-members.pin.test.ts +++ b/packages/spec/src/ui/component-form-family-typed-members.pin.test.ts @@ -3,12 +3,16 @@ /** * [#21464, stage 3] Four of the form family's nine `z.unknown()` members are * typed: `object-form` `contentLayout`, `submitBehavior`, `navigateOnSuccess` - * and `mobile`. The other five stay in the enumeration pin's ledger: the - * form's `fields` and `sections` and the master-detail form's two are held, - * because the form draws a value each typed shape would refuse (a `{ name }` + * and `mobile`. The other five stayed in the enumeration pin's ledger: the + * form's `fields` and `sections` and the master-detail form's two were held, + * because the form drew a value each typed shape would refuse (a `{ name }` * field entry; an inline runtime field inside a section), and `customFields` - * waits with the objectui-held contracts (its entries are objectui's runtime - * `FormField`, which the spec has not declared). + * waited with the objectui-held contracts (its entries are objectui's runtime + * `FormField`, which the spec has not declared). The S-objectui-held stage + * typed both forms' `fields` as field names once objectui retired the + * `{ name }` entry (`component-objectui-held-typed-members.pin.test.ts`), and + * held `customFields` and both `sections` as forks: the runtime form field has + * more than one viable spec shape. * * ## The defect this file closes * From fa4e1582f2f069681439f1d85a180c6a7c5645c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 05:43:32 +0000 Subject: [PATCH 3/6] wip(spec): regenerate the component reference page and the ui/ strictness counts Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn Co-authored-by: Claude --- content/docs/references/ui/component.mdx | 25 ++++++++++++++++--- .../ui.md | 10 ++++---- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 3542156a09c..d06f6e5e985 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -523,7 +523,7 @@ Sort field and direction pair | **formType** | `Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>` | optional | Form presentation | | **layout** | `Enum<'vertical' \| 'horizontal'>` | optional | Field layout — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set `columns` | | **columns** | `number` | optional | Number of field columns (multi-column forms), honoured under either `layout` | -| **fields** | `any[]` | optional | Limit/order the fields shown | +| **fields** | `string[]` | optional | Field names to draw, in order — bare names selecting from the object's fields and from `customFields`. A `{ name }` or `{ field }` object entry is refused: a per-form label or required override goes on a `sections[].fields` entry | | **customFields** | `any` | optional | Custom field definitions merged into the generated set | | **sections** | `any[]` | optional | Form sections (`{ label, description?, fields }` — wizard steps / tab panes) | | **title** | `string \| Record` | optional | Form title | @@ -596,7 +596,7 @@ Sort field and direction pair | **holidays** | `string[]` | optional | Additional non-working dates for the working calendar, ISO `yyyy-mm-dd` strings; folded into a Set for the duration math | | **persistLayout** | `boolean` | optional | Opt OUT of layout and filter-chip persistence — only an explicit `false` disables it; the storage key is `objectName:viewName` | | **viewName** | `string` | optional | Layout-persistence scope, the second half of the `objectName:viewName` storage key (renderer default `'default'`) | -| **markers** | `any[]` | optional | Extra vertical reference lines drawn like the Today marker (`{ date, label?, color? }`) | +| **markers** | `{ date: string; label?: string; color?: string }[]` | optional | Extra vertical reference lines drawn like the Today marker — each `{ date, label?, color? }`: `date` places the line (a date outside the drawn range draws none), `label` is drawn against it, `color` paints it | | **criticalPath** | `boolean` | optional | Seed the critical-path highlight ON; the toolbar toggle stays available either way | | **showBaselines** | `boolean` | optional | Render the planned-vs-actual baseline bars — ON unless an explicit `false` disables it | | **readOnly** | `boolean` | optional | Disable every write path on this gantt and lock the record drawer | @@ -696,6 +696,14 @@ Sort field and direction pair | **size** | `Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>` | optional (default: `"auto"`) | Overlay size bucket for drawer/modal detail: 'auto' (default — renderer derives from field count + viewport; AI writes nothing) or a coarse override sm/md/lg/xl/full. Prefer this over the pixel `width`; page mode ignores it. | | **width** | `string \| number` | optional | [DEPRECATED → size] Pixel/percent width of the drawer/modal (e.g. "600px"). A pixel width cannot be chosen at authoring time without knowing the client viewport — use the `size` bucket. | +### Nested Shape: `ObjectGanttProps.markers[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **date** | `string` | ✅ | Where the line stands — an ISO date (`2026-07-01`, read as that day on the chart's own calendar) or date-time string; a date that does not parse, or falls outside the drawn range, draws no line | +| **label** | `string` | optional | Text drawn against the line | +| **color** | `string` | optional | Line colour, any CSS colour (renderer default: the theme's primary colour) | + --- @@ -1083,7 +1091,7 @@ Sort field and direction pair | **mode** | `Enum<'create' \| 'edit'>` | optional | Form mode | | **formType** | `Enum<'simple' \| 'tabbed'>` | optional | Parent form presentation — the two variants the renderer honours for the parent half | | **sections** | `any[]` | optional | Parent form sections | -| **fields** | `any[]` | optional | Parent fields shown | +| **fields** | `string[]` | optional | Parent field names to draw, in order — bare names, as on `object-form`; a `{ name }` or `{ field }` object entry is refused | | **details** | `{ childObject: string; relationshipField?: string; columns?: object[]; formFields?: string[]; … }[]` | optional | Detail collections — each a strict entry (`{ childObject, title?, addLabel?, columns?, relationshipField?, … }`) whose `columns` are the inline grid columns a relationship field's `inlineColumns` takes; the FK and columns auto-derive from child metadata when omitted | | **title** | `string \| Record` | optional | Form title | | **submitText** | `string \| Record` | optional | Submit button label | @@ -1203,7 +1211,7 @@ View filter rule | **minDate** | `string` | optional | Pin the gantt axis start (ISO `yyyy-mm-dd`) instead of deriving it from the rows; only a non-empty value is honoured | | **maxDate** | `string` | optional | Pin the gantt axis end (ISO `yyyy-mm-dd`) instead of deriving it from the rows; only a non-empty value is honoured | | **descriptionField** | `string` | optional | Field rendered as each entry's description (renderer default `description`). Declared FLAT because the `timeline` block has no member for it — it is the only spelling this binding has | -| **mapping** | `any` | optional | Record-to-entry field mapping (`{ title, date, description, variant }`) — the objectui-side binding record read BETWEEN the `timeline` block and the flat fallbacks. Its `variant` member (the field whose value picks each marker colour, renderer default `variant`) is the only spelling that binding has | +| **mapping** | `{ title?: string; date?: string; description?: string; variant?: string }` | optional | Record-to-entry field mapping `{ title?, date?, description?, variant? }`, each a field name — the binding record read BETWEEN the `timeline` block and the flat fallbacks. Its `variant` member (the field whose value picks each marker colour, renderer default `variant`) is the only spelling that binding has | | **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>; … }` | optional | Entry-click navigation config — the same block `ListViewSchema.navigation` declares (`{ mode, size, openNewTab, preventNavigation }`) | ### Nested Shape: `ObjectTimelineProps.timeline` @@ -1236,6 +1244,15 @@ Sort field and direction pair | **field** | `string` | ✅ | Field name to sort by | | **order** | `Enum<'asc' \| 'desc'>` | ✅ | Sort direction | +### Nested Shape: `ObjectTimelineProps.mapping` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **title** | `string` | optional | Field whose value is each entry's title — read after `timeline.titleField`, ahead of the flat `titleField` (renderer default `name`) | +| **date** | `string` | optional | Field whose value is each entry's date — read after `timeline.startDateField` / `timeline.dateField`, ahead of the flat spellings | +| **description** | `string` | optional | Field whose value is each entry's description — read ahead of `descriptionField` (renderer default `description`) | +| **variant** | `string` | optional | Field whose value picks each entry's marker colour (renderer default `variant`) — the only spelling this binding has | + ### Nested Shape: `ObjectTimelineProps.navigation` | Property | Type | Required | Description | diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md index 2f859bbb156..9942c5b9631 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md @@ -21,7 +21,7 @@ The `strict` column is the one the campaign schedules against; it counts both th | Dir | Sites | strict | passthrough | catchall | strip | |---|---|---|---|---|---| -| `ui/` | 196 | 185 | 4 | 0 | 7 | +| `ui/` | 198 | 187 | 4 | 0 | 7 | ## `ui/` — sites @@ -36,7 +36,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit | `app.zod.ts` | 19 | | `bulk-action.zod.ts` | 4 | | `chart.zod.ts` | 8 | -| `component.zod.ts` | 66 | +| `component.zod.ts` | 68 | | `dashboard.zod.ts` | 11 | | `dataset.zod.ts` | 4 | | `i18n.zod.ts` | 1 | @@ -46,7 +46,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit | `sharing.zod.ts` | 1 | | `view.zod.ts` | 60 | | `widget.zod.ts` | 1 | -| **total** | **196** | +| **total** | **198** | ## `ui/` — open @@ -54,7 +54,7 @@ Per file, how many of its sites still silently discard unknown keys. The `Class` column that decides the bucket split is hand-written in the ledger; the arithmetic over it is here. -**7 strip of 196**, in 4 file(s). +**7 strip of 198**, in 4 file(s). | File | Strip | Sites | |---|---|---| @@ -62,7 +62,7 @@ over it is here. | `app.zod.ts` | 1 | 19 | | `view.zod.ts` | 4 | 60 | | `widget.zod.ts` | 1 | 1 | -| **total** | **7** | **196** | +| **total** | **7** | **198** | | Bucket | Sites | |---|---| From 8e195e42805a1bbc7a8db4965335cfb00fc696e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 05:45:13 +0000 Subject: [PATCH 4/6] wip: changeset for the S-objectui-held stage Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn Co-authored-by: Claude --- ...464-component-props-objectui-held-typed.md | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .changeset/21464-component-props-objectui-held-typed.md diff --git a/.changeset/21464-component-props-objectui-held-typed.md b/.changeset/21464-component-props-objectui-held-typed.md new file mode 100644 index 00000000000..f82477d8b8e --- /dev/null +++ b/.changeset/21464-component-props-objectui-held-typed.md @@ -0,0 +1,46 @@ +--- +'@objectstack/spec': minor +--- + +feat(spec)!: an `object-gantt` page block's `markers`, an `object-timeline` page block's `mapping`, and the top-level `fields` of the `object-form` and `object-master-detail-form` page blocks take the shape each block reads instead of any value (#21464) + +Clause-②: yes (narrowing) + + + +**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. + +**`@objectstack/spec`** + +- **`object-gantt` `markers` takes `{ date, label?, color? }` entries.** Its entries were `z.unknown()`, because the marker contract lived only in objectui: a marker with no `date`, a numeric `date` or a misspelled member passed, and the chart drew no line, or drew it with no label and in the default colour. The spec now declares objectui's own authoring declaration of a marker — `date` an ISO date or date-time string, `label` the text drawn against the line, `color` any CSS colour — closed, and the row takes it. A marker `title`, `text` or `name` is pointed at `label`, and a `colour` at `color`. +- **`object-timeline` `mapping` takes `{ title?, date?, description?, variant? }`**, each a field name. It was `z.unknown()`, for the same reason: a bare field name, a non-string binding or a misspelled member (`titleField` inside `mapping`) passed, and the rail drew the default field. The spec now declares objectui's own declaration of the binding record, closed. `titleField`, `dateField` / `startDateField`, `descriptionField` and `variantField` written inside `mapping` are pointed at the member they meant. +- **`object-form` and `object-master-detail-form` `fields` take field names.** The top-level list was an array of `z.unknown()`, held while the form drew a `{ name }` entry its page-builder guide taught, with a `label`, `type` and `required` it silently dropped. objectui has since retired that entry from every authoring face (the form still draws a stored one by its name), so both rows take field-name strings, objectui's own declaration of the member. A `{ name: 'email' }` entry is refused with `write 'email'` and where a per-form override goes; a `{ field: 'email' }` entry — the `sections[].fields` vocabulary, which the form skips at the top level — is refused with the same name and that pointer. +- **Not narrowed, and still accepting any value:** the `object-metric` drill-down's `report`, `object-form` `customFields`, both forms' `sections`, `object-timeline` `items` and the members of `action:group` / `action:menu`. Each contract still lives in objectui and has more than one viable spec shape that no ruling decides yet; each is typed once one is chosen. +- **`ObjectGanttProps`, `ObjectTimelineProps`, `ObjectFormProps` and `ObjectMasterDetailFormProps`** carry these types on the four members instead of `unknown`. No new member carries a default, so each parsed value is the authored one. + +## FROM → TO + +| you wrote | write instead | +|:--|:--| +| `object-gantt` `markers: [{ date: 5 }]` | `markers: [{ date: '2026-07-01' }]` — an ISO date or date-time string | +| `object-gantt` `markers: [{ label: 'Freeze' }]` | give it a `date`: `[{ date: '2026-07-01', label: 'Freeze' }]` | +| `object-gantt` `markers: [{ date: '2026-07-01', title: 'Freeze', colour: 'red' }]` | `[{ date: '2026-07-01', label: 'Freeze', color: 'red' }]` | +| `object-timeline` `mapping: 'subject'` | `mapping: { title: 'subject' }` — name the member the field binds | +| `object-timeline` `mapping: { titleField: 'subject', variantField: 'status' }` | `mapping: { title: 'subject', variant: 'status' }` | +| `object-form` `fields: [{ name: 'email', label: 'Email', required: true }]` | `fields: ['email']`, with the label and `required` on the object field or on a `sections[].fields` entry | +| `object-form` `fields: [{ field: 'email' }]` | `fields: ['email']`, or move the entry into a section's `fields` | +| `object-master-detail-form` `fields: [{ name: 'note' }, 'status']` | `fields: ['note', 'status']` | + +The one-line fix: write each member as the table above shows. No conversion is registered: a misspelled marker or mapping member has no rewrite that says which member the author meant, and a form already draws a stored `{ name }` entry by its name, while an override written beside it has nowhere to go but a section — the D3 entries `ui-object-gantt-markers-typed`, `ui-object-timeline-mapping-typed` and `ui-object-form-fields-names-typed` carry that judgment. + +## Who is affected, measured + +A writer is a value written on the block: a page-component node (an object literal naming the type, flat or in its `properties` bag, a literal annotated with the block's type, a direct parse through the row), the block's React component with the member as a prop or inside `schema={{…}}`, or the argument of a local test helper that mounts one (positional helper parameters resolved at every call site). Values resolve through same-file constants. Each static value was parsed through the row; a text search for each member key beside the block's name found the writers the walk does not reach, and each was read by hand. + +- **objectstack** at `7d0781482d`, over `examples/`, `packages/`, `content/`, `skills/`, `apps/` and `docs/`: no `markers` and no `object-form` `fields`; one `mapping` (this package's own navigation test, `{ title, variant }`) and four `object-master-detail-form` `fields` (the showcase's project workspace, the objectui layout DSL page, and two test copies), all field names. All parse. +- **objectui** at the `.objectui-sha` pin `ab1879721595` and at `main` `94985a92ba` (every read point identical between the two), every value a test fixture, a document or a run-time hand-off: + - `markers`: 9 values, 8 parse. The refused one is objectui's own compile-time probe that a numeric `date` is refused (`gantt-declared-keys.test.ts`). Five more mount `GanttView`, the runtime chart, directly rather than the block, and are not writers of this member. + - `mapping`: 9 values (the timeline inputs test and the absent-date-axis refusal test), all parse. + - `fields`, both forms: 73 values at `main` — 56 parse, 11 are run-time hand-offs that are not static, and the 6 refused are fixtures probing the read: three `{ field }` entries asserting the form skips them with a warning, a `{ name }` entry asserting objectui's own mirror refuses it, and two `{ name }` entries asserting a stored one still draws. At the pin a seventh is refused: the page-builder guide's `{ name, label, type, required }` example, respelled to names on objectui `main`. (Fourteen more matches are object definitions or permission maps whose own `fields` key the walk read as the block's, and are not writers.) +- **hotcrm** at `4054ec2680` and **cloud** at `b2d7a7f6f8`: no writer of any of the four members. +- **Deployed metadata** was not measured. From a88b9f3e4a0aaae1691f4a017aa4ab588764d8b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 05:54:24 +0000 Subject: [PATCH 5/6] wip(spec): the gantt marker is a factory the row builds, so the alias audit reaches its table Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn Co-authored-by: Claude --- packages/spec/src/ui/component.zod.ts | 46 ++++++++++++++++----------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 7e4b37622a5..1a6b65102a1 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -6607,24 +6607,32 @@ const OBJECT_GANTT_FLAT_CONFIG_GUIDANCE: readonly KeySetGuidance[] = [ * `GanttMarker` this feeds also takes a `Date`, which only a code-composed * chart can hand it). Closed here, as every element shape on this map is: a * misspelled member was dropped by the chart in silence. + * + * A factory the row calls, as {@link masterDetailDetailEntry} is, not a + * {@link lazySchema}: an array element is built with its row, so its alias + * table is registered wherever the row is (a bare lazy proxy as an array + * element is never forced by `alias-integrity.test.ts`'s walk, which reds on + * the unreached table). */ -const ObjectGanttMarkerSchema = lazySchema(() => strictObject({ - surface: 'this `object-gantt` marker', - history: - 'Until this shape was declared, `markers` was `z.array(z.unknown())`: a marker with no `date`, a ' - + 'numeric `date` or a misspelled member passed, and the chart drew no line, or drew it with no label ' - + 'and in the default colour.', - aliases: { - at: 'date', day: 'date', when: 'date', - title: 'label', text: 'label', name: 'label', - colour: 'color', stroke: 'color', - }, -}, { - date: z.string() - .describe('Where the line stands — an ISO date (`2026-07-01`, read as that day on the chart\'s own calendar) or date-time string; a date that does not parse, or falls outside the drawn range, draws no line'), - label: z.string().optional().describe('Text drawn against the line'), - color: z.string().optional().describe('Line colour, any CSS colour (renderer default: the theme\'s primary colour)'), -})); +function objectGanttMarker() { + return strictObject({ + surface: 'this `object-gantt` marker', + history: + 'Until this shape was declared, `markers` was `z.array(z.unknown())`: a marker with no `date`, a ' + + 'numeric `date` or a misspelled member passed, and the chart drew no line, or drew it with no label ' + + 'and in the default colour.', + aliases: { + at: 'date', day: 'date', when: 'date', + title: 'label', text: 'label', name: 'label', + colour: 'color', stroke: 'color', + }, + }, { + date: z.string() + .describe('Where the line stands — an ISO date (`2026-07-01`, read as that day on the chart\'s own calendar) or date-time string; a date that does not parse, or falls outside the drawn range, draws no line'), + label: z.string().optional().describe('Text drawn against the line'), + color: z.string().optional().describe('Line colour, any CSS colour (renderer default: the theme\'s primary colour)'), + }); +} /** * `object-gantt` (objectui `plugin-gantt/src/ObjectGantt.tsx` plus the registry @@ -6757,7 +6765,7 @@ const ObjectGanttMarkerSchema = lazySchema(() => strictObject({ * names the schema and this door takes it rather than `z.unknown()`. The * scalars below are read as their coercions say: `!!schema.readOnly`, * `schema.showBaselines !== false`, `schema.persistLayout === false`, - * `new Set(schema.holidays)`. `markers` takes {@link ObjectGanttMarkerSchema} + * `new Set(schema.holidays)`. `markers` takes {@link objectGanttMarker}'s shape * since #21464's S-objectui-held stage: objectui's own authoring declaration of * the element, `{ date, label?, color? }` (it was `z.array(z.unknown())` while * the element contract lived only in `GanttView`'s runtime `GanttMarker`). @@ -6816,7 +6824,7 @@ export const ObjectGanttPropsSchema = lazySchema(() => strictObject({ .describe('Opt OUT of layout and filter-chip persistence — only an explicit `false` disables it; the storage key is `objectName:viewName`'), viewName: z.string().optional() .describe("Layout-persistence scope, the second half of the `objectName:viewName` storage key (renderer default `'default'`)"), - markers: z.array(ObjectGanttMarkerSchema).optional() + markers: z.array(objectGanttMarker()).optional() .describe('Extra vertical reference lines drawn like the Today marker — each `{ date, label?, color? }`: `date` places the line (a date outside the drawn range draws none), `label` is drawn against it, `color` paints it'), criticalPath: z.boolean().optional() .describe('Seed the critical-path highlight ON; the toolbar toggle stays available either way'), From 723df5474159ed4b2100cd3742a6874f7e22fd69 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 06:20:59 +0000 Subject: [PATCH 6/6] wip(spec): the new pin cites in-repo writers by file, not by line Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn Co-authored-by: Claude --- .../src/ui/component-objectui-held-typed-members.pin.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/spec/src/ui/component-objectui-held-typed-members.pin.test.ts b/packages/spec/src/ui/component-objectui-held-typed-members.pin.test.ts index cb98344324a..0337f2f4f27 100644 --- a/packages/spec/src/ui/component-objectui-held-typed-members.pin.test.ts +++ b/packages/spec/src/ui/component-objectui-held-typed-members.pin.test.ts @@ -91,13 +91,13 @@ describe('§1 each member accepts every shape a measured writer authors', () => ['a title and date mapping', 'object-timeline', { mapping: { title: 'code', date: 'finish' } }], ['a description mapping', 'object-timeline', { mapping: { description: 'code' } }], ['a variant mapping', 'object-timeline', { mapping: { variant: 'kind' } }], - // This package's own `component-element-navigation-17987.test.ts:220`. + // This package's own navigation test (`packages/spec/src/ui/component-element-navigation-17987.test.ts`). ['a title and variant mapping', 'object-timeline', { mapping: { title: 'subject', variant: 'status' } }], ['an empty mapping', 'object-timeline', { mapping: {} }], // objectui's page-builder guide at `main` (`skills/objectui/guides/page-builder.md`). ['form field names', 'object-form', { fields: ['name', 'email'] }], ['no form fields', 'object-form', { fields: [] }], - // The showcase's project workspace (`examples/app-showcase/src/ui/pages/project-workspace.page.ts:47`). + // The showcase's project workspace (`examples/app-showcase/src/ui/pages/project-workspace.page.ts`). ['master-detail parent field names', 'object-master-detail-form', { fields: ['name', 'account', 'status', 'health', 'budget', 'end_date'], }],