…ld-level keys (objectui#11070 round 10)
FormField declares min_rows, max_rows, allow_add, allow_delete,
allow_reorder, total_field, add_label and sort_field, each as
GridFieldMetadata's own member by reference, and FormFieldSchema mirrors
them with the same value types. A form fields[] entry of type grid is the
authored path to the grid widget, which reads these keys off the entry,
while the strict authoring face refused each one by name there.
- coverage row and parity note for the eight keys; the refusal-flip pin
(strict-face-read-keys-11070) accepts each on a grid entry, judges each
by its declared type on both faces, and holds the form-field face to
every member GridFieldMetadata declares beyond BaseFieldMetadata;
- a fields-grid catalog fixture writes the full declared set;
- grid.mdx states the keys are accepted on a form field, and its
"Integration with Advanced Grid" section now says what the grid field
and object-grid each read, feature by feature.
Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh
Co-authored-by: Claude <noreply@anthropic.com>
Refs #11070
Clause-②: yes (widening).
FormFieldSchema, a published accept set, accepts eight more keys on a form field entry, each one a key thegridwidget already reads andgrid.mdxteaches. The tolerant face now also judges their values, so a wrong-typed value it used to strip is refused. Both halves are priced in the changeset.Round 10 of objectui#11070, under claim
5930921428(domain:devxseat 2, maintainer direct dispatch R5). It has three items:5928627070;examples/app-crm, for spec(ui): DashboardWidgetOptionsSchema declares none of the renderer extras real producers write through its passthrough — app-crm's metricoptions { format, currency }is spec-valid only by catchall and red on objectui's strict face objectstack#20960's acceptance item 3 (measure only, nothing changed for it);grid.mdx's "Integration with Advanced Grid" section (finding 2 of5929940616).(i) The form-field face mirror
Measured first, at base
0a3e5409GridFieldMetadatadeclares, beyondBaseFieldMetadata,columns(round 7) and exactly the eight keys in the table below.FormFieldresolved each one through its[key: string]: anyindex signature.FormFieldSchema, a plainz.object, stripped each one.type.FormFieldis one interface withtype?: string, andFormFieldSchemais onez.object(...).superRefine(...). Round 7 addedcolumnsto it the same way.FormFieldmember has any of the eight names;@objectstack/spec17.5.0'sFieldSchemadeclares none of them, so no spec change is involved;@object-ui/fields,GridFieldis the only reader.MasterDetailFormandLineItemsPanelwrite the same keys into the grid's config with the same meaning, and theformrenderer reads none of them.SchemaRenderer, the realformrenderer andGridField:allow_add: false, there is no Add button and no Duplicate action.Per key, both faces, before and after
GridFieldMetadataFormField: base → headFormFieldSchema: base → headfields[]grid entry: base → headmin_rowsnumberany) →GridFieldMetadata['min_rows']z.number().optional()fields.0.min_rows→ accepted"1"refusedmax_rowsnumberany) →GridFieldMetadata['max_rows']z.number().optional()fields.0.max_rows→ accepted"20"refusedallow_addbooleanany) →GridFieldMetadata['allow_add']z.boolean().optional()fields.0.allow_add→ accepted"false"refusedallow_deletebooleanany) →GridFieldMetadata['allow_delete']z.boolean().optional()fields.0.allow_delete→ accepted"false"refusedallow_reorderbooleanany) →GridFieldMetadata['allow_reorder']z.boolean().optional()fields.0.allow_reorder→ accepted"false"refusedtotal_fieldstringany) →GridFieldMetadata['total_field']z.string().optional()fields.0.total_field→ accepted3refusedadd_labelstringany) →GridFieldMetadata['add_label']z.string().optional()fields.0.add_label→ accepted1refusedsort_fieldstringany) →GridFieldMetadata['sort_field']z.string().optional()fields.0.sort_field→ accepted0refusedThe base column was read by a scratch probe on the built
@object-ui/typesdist at0a3e5409, with a lit control: the same entry without the keys parses on both faces. Each.describe()on the zod side says that only thegridwidget reads the key.Pins
strict-face-read-keys-11070.test.ts:fields[]grid-entry row per key, each with a misspelled sibling still refused by name, plus an all-eight row whose tolerant output keeps every value;assertionGridFieldKeysOnTheFormFieldFace. Every memberGridFieldMetadatadeclares beyondBaseFieldMetadatamust be a DECLAREDFormFieldmember (not the index signature) with the same type, so a key added to the grid's type and left unmirrored fails to compile. A lit control shows the key set it ranges over is non-empty.form-field-zod-coverage.test.ts: the eight keys joinDECLARED_KEYS.zod-mirror-parity.test.ts:FormFieldSchemawas already a registered pair. The eight keys are LOCAL on both sides, so no ledger total moves. ItsSPEC_DERIVED_PAIRSnote now says so.The refusal flip and the ablations
Predictions were written before each run. Every mutation was committed first and restored to HEAD, with blob == HEAD and
git diff HEADempty. A1 and A2 went through objectstack'sscripts/ablation-replace.mjs(anchor 1 → 0, blob changed). The subjects resolve from source, since the tests import../zodrelatively, so no dist is on the path.form.zod.tsandform.tswere set to base0a3e5409, and this head's pins were run against them.Tests 18 failed | 68 passed (86), andtsc -p tsconfig.test.jsongave 10 errors, all inassertionGridFieldKeysOnTheFormFieldFace(completeness, the mapped equality, the 8 per-key rows).zod-mirror-paritycompiled.allow_addmember was deleted fromFormFieldSchema.allow_addrow, the all-eight row, the wrong-typeallow_addrow, the coverage key set) and the parity file red at tsc.Tests 4 failed | 119 passed (123)over the pin, coverage and parity files. tsc gave 1 error, inzod-mirror-parity.test.tsatassertionUnmirroredMatchesLedger("form.zod.ts#FormFieldSchema"is not assignable tonever).allow_addmember was deleted fromFormField.Tests 123 passed (123). tsc gave 5 errors: the pin file's completeness, mapped-equality and per-keyallow_addrows, andzod-mirror-parity.test.ts'sMirroredUndeclaredreconciliation, by pair and by key ("allow_add"is not assignable tonever).Fixture and docs
examples/schema-catalog/src/schemas/fields-grid/line-items-grid.jsonwrites all eight keys, with columns and seeded rows, so the corpus exercises the declared set. The catalog index is regenerated.gridgoes from 26 to 27 in all three, and in the layout pintextgoes from 698 to 699 rendered and from 154 to 155 with no element.grid.mdxembeds the fixture and says that the keys are accepted on a form field entry oftype: 'grid', and that only thegridwidget reads them.(ii) The app-crm strict-face run (measure only)
The instrument, and why it is the real one. The run uses
StrictAnyComponentSchema, exported by@object-ui/types/zod(the builtpackages/types/dist/zod/index.zod.js), called with.safeParse. That is the call PR #11069'svalidateAuthoredDocumentmakes forobjectui validateandobjectui check.scripts/measure-strict-authoring-face.mjsis not the real face. Its own header says it keeps its own twin walker, PR feat(cli):objectui validateandobjectui checkjudge through the strict authoring face (objectui#5250, slice A) #11069's M1 measured that walker behind the shipped face on registered inputs, and it walks only objectui's own corpora.mainthe CLI still validates through the tolerant face (PR feat(cli):objectui validateandobjectui checkjudge through the strict authoring face (objectui#5250, slice A) #11069 is parked), and neither the CLI nor that script reads TypeScript modules. So nothing in this repository walks an objectstack app.tsx, resolving@objectstack/specto the installed 17.5.0, the version objectstackmaindeclares. It hands each document to the face the way objectui's console hands it to a renderer.Command and shas.
0a3e5409, the base, which ismain. The types dist was built withpnpm --filter @object-ui/types build. The run was repeated on headbe58a328with byte-identical output.f0cc16e8d,origin/main.399e3aa97bis its ancestor (merge-base --is-ancestorexits 0), andgit diff 399e3aa97b f0cc16e8d -- examples/app-crmis empty.git archive f0cc16e8d examples/app-crm, and the run waspnpm exec tsx appcrm-strict-run.mts OBJECTUI_WORKTREE APP_CRM_EXTRACT BEFORE_DASHBOARD.dashboards/pipeline.dashboard.ts·PipelineDashboard{ type: 'dashboard', ...doc }:DashboardViewhands the stored document toDashboardRenderer, and objectui's node face for it is thedashboardarmpages/welcome.page.ts·CrmWelcomePage{ ...page, type: 'page', pageType: page.type }: thepagearm, with the kind onpageTypePageViewwrites it{ ...page, type: page.type, pageType: page.type }, sotype: 'home'invalid_unionattype(no matching discriminator) on both faces. This is the page-kind / node-type collision documented inlayout.zod.ts'sPAGE_SPEC_EXCLUDEDnote (objectui#9642), not an unknown key.apps/crm.app.ts·CrmApp{ type: 'app', ...doc }pipeline.dashboard.tsat399e3aa97b^(39ab2940e){ type: 'dashboard', ...doc }unrecognized_keys[format, currency]atwidgets.0.options,widgets.1.optionsandwidgets.2.options(plus the non-viablemetric-cardslot arm's own issues)options.format/options.currency) does not appear atf0cc16e8d, and the control shows that the face refuses exactly that pair before the removal.(iii)
grid.mdx— "Integration with Advanced Grid"The section offered seven features as
@object-ui/plugin-grid's. The basic grid field has five of them too: inline editing, add and remove, row drag-reorder, computed columns, and a one-column footer total.object-gridhas no computed columns, and it reorders columns, not rows. It is now a table of what each widget reads, feature by feature, each cell checked against the reader:GridField's cell controls versusobject-grid'seditable, ANDed with the principal's write verdict;allow_add/allow_deleteversusoperations.createandoperations.deletewith the host'sonDelete;sortable: false,filter/sortandsearchableFields;allow_reorderandsort_field, versus columns only throughreorderableColumns;exportOptions;computedwithexprin the grid field, versus none inobject-grid(a formula field is shown as served);total_field, versuscolumns[].summaryand groupaggregations.M3 (
scripts/measure-strict-authoring-face.mjs --json, the types dist built at each ref)0a3e5409be58a328form: documents / refused / strict-only / red todayformdocument in the catalog corpus.rendererMentionsOfUndeclaredKeysare identical.unloadableModulesmoved for an environmental reason: app-shell was unbuilt at base.corpusMatchesMainis false at head because the corpus gains that fixture.Local verification
Every heavy command went through
os-verify-lock(slotissue-11070), withNODE_OPTIONS=--max-old-space-size=4096, vitest--maxWorkers=2, turbo--concurrency=2, and a memory watchdog that kills its own process group below 2500 MBMemAvailable. It never fired.turbo run buildovercheck-doc-snippet-types --build-filterran 35 of 35 tasks atbe58a328.turbo run type-checkran 48 tasks, with 13 of 13 type-checks green. The set is every package whose sources or tests referenceFormField/FormFieldSchema(agit grepcensus), plus@object-ui/typesand@object-ui/example-schema-catalog:types,fields,components,core,plugin-form,app-shell,cli,plugin-calendar,plugin-detail,plugin-grid,plugin-kanban,consoleandexample-schema-catalog.be58a328:packages/types/,examples/schema-catalog/and the two GridField pins ran 351 files, with 348 passed and 3 failed. The three failures were the catalog DOM-leak census pins, and261d9c9erecords them.261d9c9e:examples/schema-catalog/plus the eight catalog- and docs-readingscripts/__tests__ran 45 of 45 files and 2601 tests passed. The types sources did not change between the two heads.check:doc-snippets(769 of 769 blocks, 0 failed),check:doc-fences,docs:check-links,check:doc-example-ids,check:doc-types,check:doc-examples,check:spec-symbols,check:test-path-roots,check-type-check-coverage,check:control-bytes,check:new-line-citations(0 new), the catalogregenerate:check,check:changeset-claims,check:pending-changeset-literals,check-changeset-presence,changeset:check,check-changeset-overwrite, andcheck-governed-queue-guard --test(NOT GOVERNED).check:changeset-claimsnames 31 pending changesets that describe a touched file. Each paragraph was read, and none is made false by this round, so no dated note is owed.eslint --no-inline-config --format jsonover the 10 changed.ts/.tsxfiles gave 0 errors, with warnings per file equal to the base blobs.**/*.{ts,tsx}blocks. The JSON, MDX and MD files match none.parserOptions.projectand noprojectService, and no rule ineslint-rules/reads another file, so this diff cannot move an untouched file's verdict.pnpm lintandpnpm test, which CI runs.maindid not move after the base (0a3e5409), so there was nothing to merge before opening.Changeset:
@object-ui/typesminor. A published accept set widens by eight keys, and the tolerant face narrows on their values. The breaking half is stated in the changeset and priced minor under the fixed group's policy. Nothing else ships: the@object-ui/fieldsedit is a test docblock, and the catalog is private.Acceptance notes
fields[]entry, and only thegridwidget reads them, exactly as withcolumns. Moving them to atype-discriminated arm would be a ruling, not this round's work.min_rowsandmax_rowsare typednumberon both faces, asGridFieldMetadatatypes them. No integer or range rule was added: that would tightenGridFieldMetadatatoo, which is a contract decision.packages/fields/src/widgets/GridField.declaredSpelling.test.tsxsaid "the threefields-gridcatalog examples", which the new fixture made stale; it now says "every";type: 'home'has no arm on either face, which is the collision objectui#9642 documents. It is an observation and is not filed.Generated by Claude Code