Repository navigation
Commit 1289925
Part of #21464
Clause-②: yes (narrowing)
## What this does
The S-forms stage of the `ComponentPropsMap` `z.unknown()` close-out. It
executes the maintainer's rulings on forks 2 and 3 of the decision card
#21704 (ruling record `5978663135`, batch #276, letters B and B), per
the claim `5979114945`. Read points are at the `.objectui-sha` pin
`2e818d0b51ec`, under objectui `packages/` unless named. Every cited
reader file is byte-identical at objectui `main` `fd060f076`, and the
pin is an ancestor of that `main`.
| row · member | was | now |
|:--|:--|:--|
| `object-form` · `customFields` | `z.unknown()` | a list of the closed
runtime form field (module-private, built once): camelCase, keyed by
`name`, only the members the form draws; its `options` entry is the
closed runtime option the form's option controls draw |
| `object-form` · `sections` | `z.array(z.unknown())` | a page-block
section shape of its own (module-private, built once): the form view's
section keys plus the three entry arms the form reads, canonical
spellings only |
| `object-master-detail-form` · `sections` | `z.array(z.unknown())` |
the same section instance (its parent half hands `sections` to the form
verbatim, `plugin-form/src/MasterDetailForm.tsx:1692`) |
The stored form view's `FormSectionSchema` (`view.zod.ts`) is not
edited. Forks 1, 4 and 5 keep their enumeration-pin lines. #21704 is not
addressed beyond forks 2 and 3.
## The fix round (head `716f4c6522`)
The at-tier contract review of `b473439752` (record `5980612890`)
answered **FAIL** on one point, and the seat's order `5980628111` took
its remedy 1. This round, by the session
`session_016tKoy8NJa35Yih1FdzrVmn` (the director seat's dispatch),
changes:
1. **`options` on the runtime form field is re-typed.** It took the form
view's option (`FormSelectOptionSchema`), whose `value` is a stored
field's lowercase identifier, so the shipped `object-manager` dialog's
options (`{ label: 'Box', value: 'Box' }`, objectui
`plugin-designer/src/ObjectManager.tsx:244`-`:245`) were refused. It is
now a closed option of the keys the form's option readers draw, measured
at the pin (below): `label`, `value` (`string | number | boolean`, as
objectui's runtime option declares on purpose,
`types/src/zod/form.zod.ts:142`-`:145`), `description` and
`visibleWhen`. Every other ruled member is unchanged.
2. **The census was re-run and every quote of it corrected** (this body,
the changeset, both D3 entries). The first run read a `.map` over a
constant list as non-static and so never parsed the object manager's
options; the instrument now evaluates it.
3. **The Dogfood Regression Gate's red is fixed.**
`expression-conformance.test.ts` reported four UNCLASSIFIED surfaces
(`buildObjectFormRuntimeField.visibleWhen` / `.readonlyWhen` /
`.requiredWhen`, `buildObjectFormSection.visibleWhen`). They are
classified in `expression-conformance.ledger.ts` as row
`cel-form-block-field-rule`, and the new option's `visibleWhen` as row
`cel-form-block-option-visible`, in the shape of the existing
`ui/component.zod.ts` page-block rows (#20420 is the precedent for a
spec-lane edit of that ledger).
4. **Acceptance notes** carry the review's ③ notes 4 and 5, with carrier
objectstack-ai/objectui#11615.
The widget-only keys question is answered A by the record: the field
stays closed at the measured set.
## Fork 2 B — the runtime form field
**How a member reaches a draw.** `customFieldsMerge.ts:78-108`, called
from `ObjectForm.tsx:1178-1185` and from every other `formType` arm,
merges `customFields` over the generated fields. A member naming a
generated field replaces its whole definition, and any other member is
appended. A section's inline entry is drawn as it stands
(`sectionFields.ts:369-370`). Either way the renderer hands the field to
its widget as the metadata carrier
(`components/src/renderers/form/form.tsx:3171`, `field.field || field`).
**The draw set, measured from the readers, member by member.** These are
not transcribed from objectui's `FormField`.
| members | read at |
|:--|:--|
| `name`, `label`, `description`, `type`, `required`, `disabled`,
`readonly`, `hidden`, `validation`, `visibleWhen`, `readonlyWhen`,
`requiredWhen`, `colSpan` | `form.tsx` `renderFormField` destructure
`:2675-2693`; `hidden` `:2696`; the three rules `:2732`; `validation`
`:2795`; `colSpan` `:2988` |
| `widget`, `multiple` | `:2915-2918` (`widget` ahead of `type`, arity
from `multiple`) |
| `options`, `dependsOn` | `:2934-2944` (the cascading option list) |
| `placeholder`, `inputType` | `:3185`, `:3174` (the built-in input's
`type`) |
| `span`, `colSpan` | `plugin-form/src/autoLayout.ts:162-172` |
| `group` | `plugin-form/src/fieldGroups.ts:52` (the object's
field-group sections are derived over the drawn fields) |
| `rows` · `accept`, `multiple` · `dimensions` · `reference` · `min`,
`max` | `fields/src/widgets/TextAreaField.tsx:102` ·
`FileField.tsx:147-148` · `VectorField.tsx:11` · `LookupField.tsx:326` ·
`NumberField.tsx:88-89` |
| `minLength`, `maxLength`, `pattern` | the built-in input and textarea
branches (`form.tsx:4080`, `:4146`; `pattern` rides onto the native
control) |
| `returnType` · `summaryOperations` · `columns` | `FormulaField.tsx:22`
· `SummaryField.tsx:15` · `GridField.tsx:588-589` |
**The option, measured from the option readers.** Every option control
the form reaches reads the same four keys:
| key | read at |
|:--|:--|
| `label`, `value` | the built-in select (`form.tsx:3975-3977`, the pick
mapped back to the authored value by `matchOptionValue`, `:3955`) and
the four option widgets `SelectField`, `MultiSelectField`, `RadioField`,
`CheckboxesField`, which draw `optionDisplayLabel`
(`core/src/evaluator/optionRules.ts:173`) and stringify `value` only at
the control |
| `visibleWhen` | the cascade: `resolveCascadingOptions` →
`resolveVisibleOptions` (`optionRules.ts:97-110`), from `form.tsx:2940`
and from each widget's `useCascadingOptions` |
| `description` | a lookup field's static options: its typeahead
searches the description beside the label
(`fields/src/widgets/LookupField.tsx:705-706`) |
`color` (drawn only by the list and grid select cell renderer, never by
a form option control), `default`, and objectui's `disabled` (read only
by the standalone `select` node renderer,
`components/src/renderers/form/select.tsx:91`, which a form field never
reaches) and `icon` (read by no option control on the form path) are
refused, each with a prescription. Aliases name `label` (`text`, `name`,
`title`), `value` (`key`, `id`) and `visibleWhen` (`visible`,
`showWhen`).
**Value types.** Where this package already declares a member, its value
schema is taken by reference: the object field's `FieldSchema` members
for `rows`, `accept`, `dimensions`, `reference`, `minLength`,
`maxLength`, `returnType`, `summaryOperations` and `columns`
(`inlineColumns`); `EvaluatedExpressionInputSchema` for the three
`*When` rules; and the object field's `dependsOn` list beside a bare
name. The option's `label` and `description` are the object field's
option's own (`SelectOptionSchema.shape`, pinned def by def). Its
`visibleWhen` is the evaluated predicate declared on the option itself,
because the object field's option is also re-checked by the server on
write and an inline option never is. `group` takes the field-group key
grammar (`SectionGroupKeySchema`). `label`, `description` and
`placeholder` are plain strings, because the renderer draws each as it
stands and an inline locale map would be a React child. `validation` is
`{ required?, minLength?, maxLength?, min?, max? }`. Each bound rule is
`{ value, message }`. `required` is a string, because the renderer
deletes the rule and reads only its message (`form.tsx:2843`, `:2847`).
**Read, and refused anyway, each with a prescription:**
- the `grid` widget's eight snake_case keys (`min_rows`, `max_rows`,
`allow_add`, `allow_delete`, `allow_reorder`, `total_field`,
`add_label`, `sort_field`), by the ruling. Their carrier is
objectstack-ai/objectui#11610.
- `visibleOn` (`form.tsx:2767`) and the legacy `condition` (`:2717`),
two more spellings of the conditional-visibility predicate. ADR-0089 D1
makes `visibleWhen` the single canonical key. The only measured
`visibleOn` writer is a type-level test that never draws (below).
- `id`: the renderer keys the row by `id ?? name` (`:2898`), and `name`
is already unique in the drawn list.
- `fields`: the member claim of the section-divider row the form builds
from a section, not a member of a field.
**The census's two keys outside objectui's 45 members:**
- `group` is read (above), so it is typed.
- `defaultValue` is not read, so it is refused. The form opens on
`initialValues` and on the object's declared defaults
(`schemaDefaults.ts`), and objectui's `initialRecordMerge-9760.test.tsx`
row 7 pins that an inline `defaultValue` seeds nothing. The prescription
moves the value into the block's `initialValues`.
## Fork 3 B — the page-block section shape
- **Section keys, read:**
- `name` and `label`: the heading (`ObjectForm.tsx:1693` and the
per-`formType` maps at `:412`, `:492`, `:520`, `:556`, `:590`).
- `description`.
- `collapsible` / `collapsed` (`resolveSectionCollapse`, `:1702`).
- `visibleWhen` (the divider row's predicate, `:1720`).
- `columns` (`:1731`).
- `pane` (`SplitForm.tsx:445`).
- `group` (`sectionGroups.ts`).
- `fields`.
That is exactly `FormSectionSchema`'s key set, and objectui's
`ObjectFormSection` declares the same (`types/src/objectql.ts:1497`).
The form view's group-reference rule rides with it
(`sectionGroupReferenceRefinement`, the same derived-key lists).
- **Canonical spellings only.** A page block's `properties` is never
parsed on the way to the form, so the form view's two folds do not run
there, and the form reads only `visibleWhen` off a section and only a
numeric `columns` (`clampCol`, `:1599`). A section `visibleOn` and a
string `columns: '2'` were therefore dropped in silence. Both are
refused with the canonical spelling. `label` is a plain string, because
the form draws the heading as it stands. No member carries a schema
default.
- **The three entry arms:**
- **A field name.**
- **The form view's `{ field }` entry.** Its members ARE
`FormFieldSchema`'s object half, pinned def by def, with three
differences that follow from how the page block reaches the form:
- its deprecated `visibleOn` is refused, as above (the form reads
`visibleWhen ?? visibleOn`, `sectionFields.ts:455`);
- `label`, `placeholder` and `helpText` are plain strings (copied onto
the drawn field as they stand, `:386-388`);
- `span` drops the default the form view fills.
Its sub-fields are this same entry, recursively, so the canonical rule
holds at every depth.
- **The inline runtime form field.** This is fork 2's instance, by
identity (pinned).
- **Both rows share one section instance** (pinned by identity).
A bare CEL predicate parses to its `{ dialect, source }` envelope, as on
every evaluated slot. So `object-form`'s input and parsed types now
differ, and the row leaves the type-alias pin's isomorphic family for an
`ObjectFormPropsParsed` alias (ADR-0122), as `object-master-detail-form`
did on #20928. That alias is the one new export.
## The census (re-run in the fix round)
**The instrument** is a TypeScript-AST walk over `.ts` `.tsx` `.js`
`.jsx` `.mjs` `.cjs` `.json` and fenced code in `.md` / `.mdx`. It
finds:
- **Pass 1:** object literals naming either block by `type` (flat or in
`properties`), literals annotated or asserted as `ObjectFormSchema` /
`MasterDetailFormSchema`, the block's React component's `schema` prop,
and direct parses through the row. A member that is a parameter of the
enclosing helper is resolved to the argument in that position at every
same-file call site.
- **Pass 2:** every object literal carrying `customFields` or `sections`
in a file that names a form block.
Values resolve through same-file constants and spreads, and, new in this
round, through a `.map` over a constant list (an arrow with identifier
parameters and an expression body). The first run read such a list as
non-static, so the object manager's option lists were never parsed: that
miss is the review's ① 7. Every static value was parsed through this
head's rows, union arms judged by their best arm. Every value with a
non-static part, and every refusal, was read by hand.
**Positive control.** The same extracted values, parsed through
`b473439752`'s `component.zod.ts`, refuse `ObjectManager.tsx:239`
members 4 and 5 (`icon`, `group`) at every `options.N.value`
(`invalid_format`, the identifier rule). Through this head's rows they
parse.
| corpus | `customFields` | `sections` (`object-form` ·
`object-master-detail-form`) |
|:--|:--|:--|
| objectstack `316be321ef` (the previous merge base; the three commits
`main` gained since add no writer) | 0 | 3 · 0. All parse:
`examples/app-showcase/src/ui/pages/new-project-wizard.page.ts`,
`packages/lint/src/validate-component-props.test.ts`,
`packages/spec/src/ui/component.test.ts`. Field names only. |
| objectui pin `2e818d0b51ec` | 31 parse, 2 refused (probes), 11
non-static | 102 with a static part parse, 2 refused (probes) · 4 parse;
26 fully non-static, read by hand |
| objectui `main` `fd060f076` | as the pin, plus 2 test values that
parse | as the pin, plus 1 test value that parses |
| hotcrm `4054ec2680` | 0 | 0 |
| cloud `2205b53010` | 0 | 0 (one form view, which these rows do not
judge) |
**`customFields` at objectui:**
- **The 31 that parse:**
- the block literals: `guideCrudAppRenders.test.tsx:170`,
`objectFormCustomFieldsMembers-8071.test.tsx:191` and
`submitTargetRefusal.test.tsx:155`, `:317`;
- the designer's `ObjectManager.tsx:239`, the registered
`object-manager` component's modal form. Its labels are `t(...)` calls
(non-static); its `icon` and `group` options are now evaluated from
`ICON_OPTIONS` and `OBJECT_GROUPS` (`'Box'`, `'ShoppingCart'`, `'Custom
Objects'`, …) and parse as runtime option values;
- 26 helper and embeddable-form arguments, which
`EmbeddableForm.tsx:567` hands to the form as `customFields`. Among
them: the merge pins, the sections-and-members pins, the mobile
fullscreen pin with `rows` / `placeholder` / `field:textarea`, the
field-group row with `group`, and `content/docs/guide/public-forms.md`.
- At objectui `main`,
`apps/console/src/__tests__/objectname-neither-hint-11605.test.tsx:78`
and `:84` (a `{ name, label, type }` field each) parse as well.
- **The 2 refused, re-read by hand, both probes and not drawn values:**
- `types/src/__tests__/p1-spec-alignment.test.ts:348`: a type-annotated
literal whose `visibleOn: '${data.industry != null}'` is never rendered
and is not CEL.
- `plugin-form/src/__tests__/initialRecordMerge-9760.test.tsx:236`: the
`memo` member whose `defaultValue` the test's row 7 pins as seeding
nothing.
- **The 11 non-static:** run-time hand-offs (`EmbeddableForm.tsx:567`,
`ObjectView.tsx:2599`, the arm forwards in `DrawerForm`, `ModalForm`,
`ObjectForm`, `SplitForm`, `TabbedForm`, `WizardForm`) and helper
parameters. No other `customFields` value has a non-static `options`.
**`sections` at objectui:**
- **Two block writers are refused, re-read by hand, both probes.**
objectui's own renderer test
(`plugin-form/src/__tests__/formSectionGroupReference-7051.test.tsx:270`,
`:307`) states that this door refuses both shapes at parse: a section
declaring neither `fields` nor `group` ("off-spec, so reachable only
from a programmatic SDUI caller"), and a group-owned `label` /
`collapsible` beside `group`, which the renderer reports and ignores.
These are the form view's own group-reference rule.
- **The parsing values include:**
- the field designer's inline fields
(`plugin-designer/src/FieldDesigner.tsx:344`: `name`, `label`, `type`,
`required`, `placeholder`, `disabled`, `options`, `visibleWhen`). Its
one non-static `options`, `flatTypeOptions`, was read by hand: `{ label:
FIELD_TYPE_META[ft].label, value: ft }` per field type, two keys the
option declares;
- the plugin-form README's data-source-free wizard (inline fields with
`inputType`);
- the one `{ field }` entry
(`cli/src/__tests__/spec-vocabulary-hint.test.ts:54`);
- group, collapse, `columns`, `pane` and `visibleWhen` sections;
- at objectui `main`, `objectname-neither-hint-11605.test.tsx:210` (an
inline field in a section).
- **The 26 fully non-static values** (23 on `object-form`, three on the
master-detail form) are the form's own run-time hand-offs
(`ObjectForm.tsx:386`, `MasterDetailForm.tsx:1692`,
`DrawerForm.tsx:878`, `ModalForm.tsx:1062`, `ViewPreview.tsx:150`,
`ObjectView.tsx:2603`) and test-helper parameters. Read by hand, they
use declared keys only, with one exception:
`sectionStyleKeysRetired-13626.test.tsx`, objectui's probe that a
retired `className` / `gridClassName` reaches nothing (refused here, as
objectui's own type refuses it).
- **Pass 2's refused section values** are `record:details`, detail-view
and object-view form-slot sections, which these rows do not judge.
No measured working writer is refused under this head's shapes.
## Changes
- **`packages/spec/src/ui/component.zod.ts`:**
- the runtime form field, its `validation` block, its option (new in the
fix round), the form view's `{ field }` entry arm and the section shape
— five module-private factories, each built once (a factory and not a
`lazySchema`, for the alias-integrity walk's reason, as
`objectGanttMarker()`);
- the two rows' members;
- `ObjectFormPropsParsed`;
- the imports (`FormFieldSchema`; `FieldSchema` and `SelectOptionSchema`
from `../data/field.zod`).
-
**`packages/spec/src/ui/component-props-unknown-members.pin.test.ts`:**
- the `customFields` and both `sections[]` fork lines leave;
- the predicate ASTs inside the new shapes join the expression-AST lines
(`customFields[]` / `sections[].fields[]` `visibleWhen` / `readonlyWhen`
/ `requiredWhen`, an option's `visibleWhen`, a grid column's two rules,
and `sections[].visibleWhen`);
- the roll-up `summaryOperations.filter{}` gets its own `shared` reason
(`FILTER_CONDITION`: a query `where` over the child object's fields,
judged by the filter schema's own refinement);
- the `fork` stage's text drops the form field.
-
**`packages/spec/src/ui/component-form-custom-fields-sections-typed.pin.test.ts`
(new):**
- §1: 19 byte-identical census and lit-control parses (the fix round
adds runtime option values `'Box'`, `'ShoppingCart'`, `1`, `2`, `true`,
`false`, and an option's `description` and `visibleWhen`), the shipped
`object-manager` dialog's inline fields byte-identical, the predicate
envelope (an option's bare `visibleWhen` included) and the absent case.
- §2: 35 refusals by `code` and `path` (the fix round adds an undeclared
option key, an option `color`, an option `default`, an option with no
`label`, and an object `value`), plus the prescriptions, the option's
included.
- §3: the exact draw set, the option's exact key set (`description`,
`label`, `value`, `visibleWhen`; not the form view's option; `label` and
`description` def-identical to the object field's option; `value`
accepts a string, a number and a boolean and refuses `null`,
`undefined`, an object and an array), no snake_case key, the form view's
section keys minus `visibleOn`, the `{ field }` arm's members def by
def, the inline arm's identity and the shared section instance.
- §4: the two D3 ids.
- **`packages/qa/dogfood/test/expression-conformance.ledger.ts` (fix
round):** rows `cel-form-block-field-rule` (the inline field's three
rules and the section's `visibleWhen`: objectui `resolveFieldRuleState`
→ `evalFieldPredicate`, fail-soft-log) and
`cel-form-block-option-visible` (the option's `visibleWhen`:
`resolveCascadingOptions`, fail-soft-log, UI gating only).
- **`packages/spec/src/type-alias-convention.pin.test.ts`:** the
`ObjectFormPropsSchema` Iso pin leaves (773 → 772), with its receipt.
This file is outside the claim's list. It reds otherwise, and the
convention's route is to declare the alias and delete the pin.
- **`packages/spec/dropped-refinements.baseline.json`:** five new sites.
These are the refinements the new shapes carry by reference, which
`z.toJSONSchema` drops: the section's group-reference rule, the inline
grid column's rules, and the roll-up filter's comparand refinement.
`droppedRefinementSites` goes 665 → 670. The fix round's option adds no
site (the build's own check passes unchanged). This is hand-edited as
the ledger requires, and no rule is weakened.
- **The ADR-0087 kit:**
- `18.ui-object-form-custom-fields-typed.ts` and
`18.ui-object-form-sections-typed.ts` (new D3 entries; the fix round
corrects both census quotes and the option text, and the sections
comment names the `formSectionGroupReference-7051` probes as the refused
values);
- `registry.ts`: the semantic region is regenerated, and the step-18
rationale fragments sit at orders **78 and 79**.
- There is no D2 conversion and no `RETIRED_KEYS_BY_MAJOR` row:
page-component `properties` is not parsed on the save or load path, and
the refused values are nested member values.
- **Generated:** `content/docs/references/ui/component.mdx` (two member
rows and three nested-shape tables; the `options` row now prints the
runtime option),
`docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md` (`ui/`
198 → 204 and `component.zod.ts` 68 → 74, the six new strict sites),
`api-surface/ui.json` and `export-origins/ui.json`
(`ObjectFormPropsParsed`).
-
**`.changeset/21464-component-props-form-custom-fields-sections-typed.md`:**
`@objectstack/spec` `minor`, a BREAKING banner, the `Clause-②` line, the
ADR-0087 `registered` marker naming both ids, FROM → TO (two option rows
added) and the census, corrected.
## Measurements
These are at head `716f4c6522` (merge base `ff29410ed2`: `origin/main`
was merged in through `scripts/pm/os-regen-merge.sh` with no conflict,
and after a rebuild every generated artifact checks up to date) unless
named. `component.zod.ts` is blob `373d03336dd9` from `c04a77716c` to
the head. Heavy runs went through `scripts/pm/os-verify-lock.sh`, each
`VERDICT command-exit` read, and every exit code was captured before any
pipe.
- **Red first, the fix round.** The new pin cases were committed alone
(`1b08dec21d`) and run against the unfixed rows: 9 failed, 61 passed
(70). The runtime option values (`'Box'`, a number, a boolean) and the
object-manager dialog were refused (the old option refused even `value:
'a'`, `too_small` under the identifier rule), and an option `color` was
accepted. On the fix (`c04a77716c`): 70 passed, and an undeclared option
key is still refused (`unrecognized_keys` at
`customFields.0.options.0`).
- **Red first, the stage** (at `b473439752`). On the published
`@objectstack/spec@17.6.0` (the npm tarball,
`ComponentPropsMap[row].safeParse`), all 16 junk values are ACCEPTED:
`customFields: 42`, a member with no `name`, a misspelled member,
`visibleOn`, `defaultValue`, `min_rows` and `validation.required: true`;
on `object-form`, `sections: [42]`, a section `visibleOn`, `columns:
'2'`, `className`, a section with neither `fields` nor `group`, a `{
field }` entry's `visibleOn` and an inline entry's unknown key; on
`object-master-detail-form`, a section `visibleOn` and `[42]`. All 16
are refused with the code and path the pins assert.
- **Tests:**
- `pnpm --filter @objectstack/spec test`: Test Files 614 passed (614);
Tests 18285 passed, 1 todo.
- `pnpm --filter @objectstack/spec typecheck`: exit 0.
`check:test-typecheck` OK at 52 files / 246 errors / 135 signatures
held.
- `pnpm --filter @objectstack/lint test` (its closure from the full
build below): Test Files 119 passed, Tests 5627 passed.
- `pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2
test/expression-conformance.test.ts`: Test Files 1 passed, Tests 7
passed. This is the test the Dogfood Regression Gate (3/3) failed on at
`b473439752`.
- **Public door.** `validateComponentProps` over the built lint and
spec: a stack shaped like the object-manager dialog (`'Box'`, `'Custom
Objects'`, `1` and `true` option values) reports 0 findings; an option
`color` and an option `bogus` report `component-props-unknown-key` at
`customFields.0.options.0.color` (with the prescription) and
`customFields.0.options.1.bogus`.
- **Ablation, the fix round** (at `e948519edd`; `component.zod.ts` blob
`373d03336dd9`, the head's). The driver wraps `node
scripts/ablation-replace.mjs` in its own `EXIT INT TERM` trap on the
absolute path, with the HEAD blob as the restore target and an empty
hash read as failure. The pins import `./component.zod` from source and
the conformance test scans source, so no build or dist preflight is
owed.
| leg | mutation | blob after mutation | result |
|:--|:--|:--|:--|
| option element | `options: z.array(buildObjectFormRuntimeOption())` →
`z.array(z.unknown())`, anchor x1 → x0 | `30c27aa10dd2` | red, 8 failed
/ 62 passed: the five option refusals, the option prescriptions, the
option key set, and an option predicate's envelope |
| discovery control | the ledger row `cel-form-block-option-visible`
with its cover emptied | `1009587f14b8` | red, 1 failed / 6 passed:
`UNCLASSIFIED surface — add a ledger row (ADR-0060):
ui/component.zod.ts:buildObjectFormRuntimeOption.visibleWhen` |
After each leg the blob equals HEAD and `git diff HEAD` is empty. The
stage's three legs (at `3c4c7ce1a8`: `customFields`, `object-form`
`sections` and `object-master-detail-form` `sections` each to
`z.unknown()`, red 24, 21 and 5 failed) cover the members this round
does not change.
- **Derived gates.** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derived 114 commands at
`716f4c6522` (14 paths, +1375 / −75, under the 5000 threshold). All 114
ran at that head. On the first pass 112 exited 0, and
`check:skill-examples` and `check:dual-build-cjs-loads` exited 3
(PREREQUISITE NOT MET: this fresh worktree had no package builds); both
exited 0 after the full `turbo run build` (72 tasks, 71 of them from the
shared turbo cache, exit 0). The `--ran` reconciliation reads: 114
derived, 114 run, 0 NOT-MEASURED, 0 UNRUN.
- **Named extras, with this body as the `pull_request` payload:**
`check-changeset-no-major.mjs --base origin/main --event` exit 0
(declaration line `Clause-②: yes (narrowing)`, arm `narrowing`);
`check-adr-0087-registration.mjs` exit 0 (`registered
ui-object-form-custom-fields-typed, ui-object-form-sections-typed`);
`check-empty-changeset.mjs` exit 0.
- **Narrowed lint.** `eslint --no-inline-config --format json` over the
eight changed TS files: 8 files, 0 errors, 0 warnings.
- The population is read from eslint itself: each of the eight resolves
a config, and the six other changed files (Markdown, MDX, JSON) answer
"File ignored because no matching configuration was supplied"
(`--print-config` prints `undefined`).
- Invariance: `eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`, its own note at about line 328), so this diff
moves no untouched file's verdict.
- The full `pnpm lint` is CI's.
- **Control bytes.** A self-scan of the 14 changed files found no hit,
and `check:nul-bytes` (a derived gate) is green.
- **NOT MEASURED:**
- the Console Pin Gate and the full `pnpm lint`. Reason: CI-owned;
objectui was read at the pin and at `main`, and not built against this
spec.
- CI on this head: not waited on.
## Acceptance notes (not filed)
- **The React tier still publishes the older surfaces.** The React
`ObjectForm` block's overlay types `customFields` as `any[]`, and its
`sections` come from `FormViewSchema`
(`packages/spec/src/ui/react-blocks.ts`, published as
`skills/objectstack-ui/references/react-blocks.md`). The React tier is a
separate, hand-declared contract for programmatic mounts, and a React
prop is not authored metadata. Carrier: none.
- **Two row-level rules are not carried.** The form view refuses `pane`
off a split form and `group` / `visibleWhen` / a `true` collapse pair on
a wizard step, in `FormViewSchema`'s own refinement. These are row-level
rules coupling `formType` to the sections, not section-shape rules, so
the ruled shape does not carry them. On a page block, objectui answers a
wizard `group` with an empty step and a warning. Carrier: none.
- **Three wider field sets are not declared:**
- **Field-widget carrier reads** outside objectui's 45-member
`FormField` vocabulary: `scale` and `step` (`NumberField`,
`SliderField`), `language` (`CodeField`), `maxSize`, `capture` and
`crop` (`FileField`, `ImageField`), and `LookupField`'s `displayField` /
`idField` / `lookupColumns` / … No census writer uses one, and
objectui's own strict face declares none. The record answered A: the
field stays closed at the measured set and grows when a writer appears.
- **The `type` / `widget` namespace rule** objectui's authoring face
enforces (a colon-qualified id must name `field:`) is not carried; both
are strings here. Carrier: none.
- **A union refusal reads as a value verdict at the door.** A section
entry refused by every arm, for example a `{ field }` entry's
`visibleOn`, prints all three arms' refusals rather than the unknown-key
rule id. Each arm's prescription is in the message, but the lint's
single-arm routing needs exactly one arm with key-only issues. Carrier:
none.
- **The default `simple` arm skips an inline section entry its pool
lacks** (the review's ③ note 4). On `formType: 'simple'` (the default),
`ObjectForm` passes its field pool into section resolution
(`plugin-form/src/ObjectForm.tsx:1617`-`:1627`), and
`buildSectionFields` `continue`s on an entry the pool lacks
(`sectionFields.ts:480`-`:484`). So a self-contained inline entry whose
`name` neither the object nor `customFields` declares is skipped there,
while the tabbed, wizard, split, drawer and modal arms resolve with no
pool and draw it; objectui's own inline-sections test runs every arm
except `simple` (`plugin-form/src/submitTargetRefusal.test.tsx:116`,
`:331`-`:358`). The spec admits the inline arm on every `formType`, as
fork 3 B rules; the gap is the renderer's and does not change the ruled
shape. Carrier: objectstack-ai/objectui#11615.
- **objectui's `ObjectFormSection.fields` TypeScript type has no `{
field }` arm** (the review's ③ note 5). It is `(string | FormField)[]`,
and `FormField` requires `name`, so objectui's own TS face has no arm
for the form view's `{ field }` entry that `sectionFields.ts:373-455`
draws (its zod mirror takes `z.any()` there). Carrier: the same card,
objectstack-ai/objectui#11615, which names it.
- **An empty-string option `value` parses, and a select may refuse it**
(the fix round's out-of-scope note, carried here at the at-tier review's
request). The runtime option's `value` is `string | number | boolean`,
as objectui's runtime option declares, so `''` parses. Neither the
built-in select (`form.tsx:3976`) nor `SelectField` (`:212`) guards an
empty item value, and Radix Select refuses one. Refusing `''` on the
shared element would over-refuse radio and checkbox options, and no
writer authors it. Inference, not reproduced. Carrier: none.
## Deviations
- **The commit trailers follow AGENTS.md** (the model-free trailer
pair), not the harness reminder, which names a model. This body was
edited through the relay's `issue_patch` (`PATCH /issues/21742`) and
sent without a footer. The first edit was stored byte-identical (32375
bytes sent and stored, read back over REST), so no footer was appended
and none is carried; the attribution is the session named under the fix
round above.
- **Two files outside the claim's file list are edited,** each because
the change reds it otherwise: `type-alias-convention.pin.test.ts` (the
ADR-0122 route) and `dropped-refinements.baseline.json` (the ledger's
own refusal printed the corrected entries). The fix round adds a third,
`packages/qa/dogfood/test/expression-conformance.ledger.ts`, by the
seat's order.
- **The `{ field }` arm reads `FormFieldSchema`'s object half off its
pipe** (`.in.shape`), because `view.zod.ts` is not on the file surface
and exports no base. One place does this. §3 pins every reused member's
def against the form view's, so the read cannot drift unnoticed.
- **The option's `visibleWhen` is a new declaring position, not the
object field's option's by reference.** That one's describe and its
ledger row (`cel-select-option-visible`) state the server's re-check on
write, which an inline option never gets. So the option declares its own
predicate, and the ledger carries a fifth row beyond the four the order
named.
- **The census instrument is this run's own,** extended in the fix round
with `.map` evaluation. Its counts differ from the S-objectui-held
stage's (27 `customFields` values, 7 inline section entries), because
pass 2 and the designer's code-composed nodes are counted here.
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent ea7ff39 commit 1289925
14 files changed
Lines changed: 1375 additions & 75 deletions
File tree
- .changeset
- content/docs/references/ui
- docs/audits/2026-07-unknown-key-strictness-ledger.counts
- packages
- qa/dogfood/test
- spec
- api-surface
- export-origins
- src
- migrations
- entries/semantic
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
0 commit comments