Skip to content

feat(spec)!: object-form customFields takes a closed runtime form field, and both forms' sections a page-block section shape (#21464, S-forms) - #21742

Merged
objectstack-fleet[bot] merged 15 commits into
mainfrom
claude/issue-21464-s-forms
Oct 4, 2026
Merged

objectstack-fleet[bot] merged 15 commits into
mainfrom
claude/issue-21464-s-forms

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

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 (feat(spec): ComponentPropsMap rows for action:button/group/menu/icon and element:definition-list/repeater #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 plugin-form: on the default simple form type, an inline section field entry whose name is not in the field pool is skipped, while every other arm draws it, and objectstack now admits that entry on the page block 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 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 continues 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: plugin-form: on the default simple form type, an inline section field entry whose name is not in the field pool is skipped, while every other arm draws it, and objectstack now admits that entry on the page block 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, plugin-form: on the default simple form type, an inline section field entry whose name is not in the field pool is skipped, while every other arm draws it, and objectstack now admits that entry on the page block 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.

claude added 9 commits October 4, 2026 11:14
…he ruled shapes (#21464, S-forms) [wip]

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
…ers; ledger the new dropped-refinement sites [wip]

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
…s, api-surface and export-origins [wip]

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
…p-reference probe values; the changeset counts helper parameters

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:ui tests tooling labels Oct 4, 2026
@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 37 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/dropped-refinements.baseline.json, packages/spec/export-origins/ui.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

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

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

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

Which tree this was computed on

This run read content/docs from b6f02595a7c447b43f044238138e6061dffda98a — the merge of head 716f4c6522652f16c82c086f6c11f77cc06b4c7a into base ff29410ed2133d8f6bcf955960f606e6bfbdbf54, which is what actions/checkout gives a pull_request run. Not the PR head.

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

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

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

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

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

claude added 6 commits October 4, 2026 13:50
…object-manager dialog, and a closed option element [wip, red first]

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
… the form draws — label, value (a string, a number or a boolean), description, visibleWhen — closed [wip]

The option element was the form view's (FormSelectOptionSchema), whose value is
a stored field's lowercase identifier; the shipped object-manager dialog's
{ label: 'Box', value: 'Box' } options were refused.

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
…ction and option predicates in the expression conformance ledger [wip]

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
…unts for the runtime option [wip]

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
…n census — option lists evaluated, the object-manager writer parses, the sections refusals are the group-reference probes [wip]

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 4, 2026 15:00
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 4, 2026 15:00
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit 1289925 Oct 4, 2026
58 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21464-s-forms branch October 4, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants