Repository navigation
feat(spec): the object-form runtime form field declares the grid widget's eight camelCase keys (#21768) - #21825
Conversation
…amelCase keys The snake_case spellings stay refused by name, each prescription naming its camelCase key; totalField is described as the grid's CHILD column. Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…grid field's own key Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0bf211f0acb75b1b4b50edb4d2f94fbf8a5cdf9c && git checkout 0bf211f0acb75b1b4b50edb4d2f94fbf8a5cdf9c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a3ffc4512df5d9ebc1c9e5dee70813d89bf549db bb32836dc9846dc60425922b5254c5d0fd9c0cc4 && git checkout -B drift-repro a3ffc4512df5d9ebc1c9e5dee70813d89bf549db && git merge --no-ff bb32836dc9846dc60425922b5254c5d0fd9c0cc4
node scripts/docs-audit/affected-docs.mjs --json a3ffc4512df5d9ebc1c9e5dee70813d89bf549db
|
… decision in words instead of a tracker number (stage 17) (objectstack-ai#21856) Part of objectstack-ai#20749 Clause-②: no Stage 17 of this card: the next area of class (e), the test strings shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513. This stage takes the third name-ordered file group directly under `packages/spec/src/data/`: the 20 test files from `filter-comparand-type.test.ts` to `filter-view-operator-parity.test.ts`. They carried 95 messages and 99 tracker ids, citing 36 records. Every one of those ids now either states what its record decided, in words (form D), or is dropped where the title already says it. Text only: no assertion, identifier, test count or code comment changes. ## Census at the base (`2799155678`) Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`), `census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`), `census-wide.cjs` (md5 `c98410a19529c439adb0afbfb00026a2`) and `dirtable.cjs` (md5 `dda605c54745b4a60cc14c9a686e4eff`). They are byte-identical to the copies stages 10 to 16 used. A literal counts as a test title when its folded message is argument 0 of a `describe` / `it` / `test` call, `.each` / `.skip` / `.only` chains included. Everything else is an "other" string. Both instruments read **1140 messages / 1207 ids in 245 files**, the seat's reading at `2799155678`. That is stage 16's head reading (1137 / 1204) plus the three ids that PRs objectstack-ai#21819 and objectstack-ai#21825 added in new `ui/` test titles, which ride the `ui/` stages. | directory | files | messages / ids | titles | other | |:--|--:|--:|--:|--:| | `ui/` | 84 | 396 / 419 | 378 / 401 | 18 / 18 | | `data/` (this PR: the third 20 files) | 55 | 280 / 299 | 265 / 282 | 15 / 17 | | `api/` | 40 | 189 / 201 | 181 / 193 | 8 / 8 | | `system/` | 34 | 154 / 165 | 128 / 138 | 26 / 27 | | (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 | | `ai/` | 1 | 2 / 2 | 0 | 2 / 2 | | `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 | | **total** | **245** | **1140 / 1207** | **1069 / 1133** | **71 / 74** | The group reads **95 messages / 99 ids in 20 files**, the seat's figures, file for file: | file (under `data/`) | messages / ids | titles | other | |:--|--:|--:|--:| | `filter-comparand-type.test.ts` | 9 / 10 | 9 / 10 | 0 | | `filter-cross-field-comparison-class.test.ts` | 4 / 4 | 4 / 4 | 0 | | `filter-dotted-head.test.ts` | 2 / 2 | 2 / 2 | 0 | | `filter-empty-operator.test.ts` | 5 / 5 | 5 / 5 | 0 | | `filter-equality-array-schema-door.test.ts` | 5 / 5 | 5 / 5 | 0 | | `filter-field-reference-lowering.test.ts` | 1 / 1 | 1 / 1 | 0 | | `filter-icontains-parse-door.test.ts` | 3 / 3 | 3 / 3 | 0 | | `filter-like-code-point.test.ts` | 3 / 3 | 3 / 3 | 0 | | `filter-like-nul-pattern.test.ts` | 1 / 1 | 1 / 1 | 0 | | `filter-like-wire-lowering.test.ts` | 3 / 3 | 3 / 3 | 0 | | `filter-lowering.test.ts` | 6 / 6 | 1 / 1 | 5 / 5 | | `filter-ne-array-schema-door.test.ts` | 5 / 6 | 5 / 6 | 0 | | `filter-number-comparand-declared-type.test.ts` | 17 / 17 | 17 / 17 | 0 | | `filter-operator-vocabulary.test.ts` | 2 / 4 | 1 / 1 | 1 / 3 | | `filter-preset-comparand.test.ts` | 2 / 2 | 2 / 2 | 0 | | `filter-save-door-face-parity.test.ts` | 14 / 14 | 7 / 7 | 7 / 7 | | `filter-text-comparand.test.ts` | 3 / 3 | 3 / 3 | 0 | | `filter-text-conformance.test.ts` | 2 / 2 | 1 / 1 | 1 / 1 | | `filter-text-operator-declared-type.test.ts` | 6 / 6 | 6 / 6 | 0 | | `filter-view-operator-parity.test.ts` | 2 / 2 | 2 / 2 | 0 | | **20 files** | **95 / 99** | **81 / 83** | **14 / 16** | Two more test files sit in the same name range and carry no id: `filter-subtree-provenance.test.ts` and `filter-verdict.test.ts`. They are not touched. - **Controls.** Lit, a title: `data/filter.test.ts`, outside the group, reads 18 / 19 at the head as at the base. Lit, "other" strings: the twin expect messages in `system/message-queue-retirement.test.ts` (`:87`, `:127`) still read at the head. Dark: `data/filter-operator-vocabulary.test.ts` reads 0 / 0 at the head while 11 of its comment lines still carry a number. Planted in a scratch copy of the head `data/filter-like-nul-pattern.test.ts`: an id put into a title reads 1 / 1, and an id put into a comment reads 0. - **A wider pattern** (any `#` plus digits) reads the same totals as the gate pattern in all 20 files at the base, and 0 in all 20 at the head. - **At the head:** 1045 messages / 1108 ids in 225 files. The 20 files read 0 / 0, and no other file moved. ## How the area was chosen `data/` has no subdirectory to split by, so its stages take name-ordered file groups near the ~100-id bound. Stage 16's re-cut named this group at 99, and this census reads 99, so the rule needed no re-cut. **Named for the next stages** (re-cut from the head census, 1045 / 1108; `data/` 185 / 200 left, the same cut stages 15 and 16 named): - `data/` in two more stages, name-ordered: 1. `filter.test.ts` to `object.test.ts`: 17 files, 103 messages / 114 ids. `object.test.ts` alone carries 42, so no cut lands nearer the bound; 2. `query-transport.test.ts` to `validation.test.ts` (11 files, 34 / 34) with `data/driver/` (7 files, 48 / 52): 86 ids. - `ui/` 419, about four stages. `api/` 201, two. `system/` 165, two. The files directly in `src/`, 120, one. - The three docblock needles (`ai/build-progress.test.ts:236`, `:237`, `contracts/approval-service.test.ts:274`), one stage with their docblocks. ## What each id became 20 literals (23 ids) now state a decision in words. 3 literals (4 ids) get their subject back in words where the number stood in for it. 72 literals (72 ids) drop a number the title already explains. Every cited record was read with its comments through REST: 35 answer 200. objectstack-ai#14426 answers 404, and its decision was read from what landed: commit `40a44b91b` and its CHANGELOG entry (the `undefined` comparand refusal names the null predicate by its full spellings, so following it never lands in another refusal). The title already says that, so the number is dropped. | record(s) | literal (under `data/`) | now reads | |:--|:--|:--| | objectstack-ai#7872 | `filter-comparand-type.test.ts:41` | "the accepted set — one set, defined at the shared door for every driver". The ruling put the comparand-type set at the shared compile face, the measured superset, instead of per-driver policy. | | objectstack-ai#5869 | `filter-comparand-type.test.ts:255` | "does not judge a non-array list-operator comparand — that SHAPE is refused 400 by the engine’s shape gate". The record's 500 became an enveloped 400 at the engine's list-shape gate. | | objectstack-ai#20347 | `filter-cross-field-comparison-class.test.ts:44` | "the class table — one classification, read by every judge". One comparison classification, lifted from the SQL driver, shared by the lint arms and the driver. | | objectstack-ai#8371 | `filter-dotted-head.test.ts:24` | "classifyDottedFilterHead — the FILTER axis dotted verdict, by the type of the head segment". Option 2: a type-directed verdict on the head segment, with the structured head left unjudged. | | objectstack-ai#19757 | `filter-empty-operator.test.ts:148` | "the empty list stays refused in the equality slot — the ruled refusal of an array there is untouched". Ruling 乙: an array in the equality slot is refused at the shared face, the empty array included. | | objectstack-ai#7597 | `filter-field-reference-lowering.test.ts:56` | "equality triples with a `{ $field }` comparand — lowered to `$eq`, never a bare field spec". | | objectstack-ai#20041 | `filter-like-nul-pattern.test.ts:25` | "hasNulInLikePattern — one predicate, so every door refuses a pattern holding U+0000". One spec predicate beside the dangling-escape one, asked at every door. | | objectstack-ai#7536 | `filter-like-wire-lowering.test.ts:51` | "the wire lowering of `like` / `ilike` — its own operator, never folded onto `$contains`". The caller binds the wildcards. | | objectstack-ai#20116, objectstack-ai#19886 | `filter-ne-array-schema-door.test.ts:243` | "§5 — the stored-filter carrier walk refuses $ne: [...] on save too". The save door refuses what the query faces refuse. objectstack-ai#19886 is dropped: the title already says the slot refuses an array. | | objectstack-ai#20336 (2) | `filter-number-comparand-declared-type.test.ts:59`, `:174` | "the numeric grammar — a JSON number literal naming a finite double" and "numberComparandDoorVerdict — a numeric string narrows to its number, a non-numeric one is refused". | | objectstack-ai#20510 | `filter-number-comparand-declared-type.test.ts:308` | "`aggregated` and `boundByDriver` — the refusal says only what is true at its position". The refusal names an aggregated column as such, and names the PostgreSQL server error only where the driver binds. | | objectstack-ai#5701, objectstack-ai#3948, objectstack-ai#6520 | `filter-operator-vocabulary.test.ts:82`, `:89` | One declared expect message: "(measured when $icontains was staged — a dropped predicate WIDENS, which on an RLS read scope is an unauthorized read)" and "Admitting $icontains with an arm on every JS face, in one PR, is the worked example of the clearing direction." | | objectstack-ai#4706 | `filter-operator-vocabulary.test.ts:121` | "covers the operators retired when `$icontains` replaced `$regex`". Option B: `$regex` retired, `$icontains` added. | | objectstack-ai#8793 | `filter-preset-comparand.test.ts:31` | "FilterConditionSchema — a bare preset name in an ordering comparand is refused at publish". | | objectstack-ai#19889 (2) | `filter-save-door-face-parity.test.ts:308`, `:309` | Two declared `it.each` labels: "an implicit list (an equality-slot refusal, unchanged)" and "a $eq list (an equality-slot refusal, unchanged)". Ruling A: the save door refuses an array in the equality slot. | | objectstack-ai#18113 | `filter-text-comparand.test.ts:51` | "isRefusedTextComparand — the one predicate every face reads, driven through every FILTER_TEXT_CASES case". Ruling D: the predicate lives with the contract's owner. | | objectstack-ai#4706 | `filter-text-conformance.test.ts:162` | A declared expect message: "a case-folding $contains returns both — the answer ruled out when $contains was declared case-sensitive". The record's second question, answered A. | | objectstack-ai#15661 | `filter-text-operator-declared-type.test.ts:184` | "textOperatorDoorVerdict — a text operator over a type that never stores a string is refused". C-deny. | **Subject restored (4 ids):** objectstack-ai#5222 and objectstack-ai#7597 at `filter-comparand-type.test.ts:225` ("leaves a FieldReference alone at every position — cross-field push-down, scalar comparison and the `$eq` lowering own its fate"), objectstack-ai#8296 at `filter-dotted-head.test.ts:40` ("a virtual head is refused: the unmaterializable-field verdict finally reaching the dotted spelling") and objectstack-ai#19514 at `filter-save-door-face-parity.test.ts:743` ("… and the text-comparand arm stays silent"). **Dropped only (72 ids):** objectstack-ai#5146 (2), objectstack-ai#5234, objectstack-ai#5240, objectstack-ai#5298 (3), objectstack-ai#5701, objectstack-ai#5869, objectstack-ai#7536 (2), objectstack-ai#7872 (2), objectstack-ai#8220, objectstack-ai#8793, objectstack-ai#8934, objectstack-ai#14079, objectstack-ai#14426, objectstack-ai#15661 (5), objectstack-ai#18113 (2), objectstack-ai#19514 (3), objectstack-ai#19886 (4), objectstack-ai#19889 (5), objectstack-ai#20116 (7), objectstack-ai#20143 (3), objectstack-ai#20311 (3), objectstack-ai#20336 (5), objectstack-ai#20347 (3), objectstack-ai#20446, objectstack-ai#20502 (8), objectstack-ai#20510, objectstack-ai#21448 (4). - Each of these titles already states the decision it pins: for example "$ne a value: a row with no value satisfies it" for objectstack-ai#5298, "§1 — the enumeration: the save door refuses exactly what the query faces refuse" for objectstack-ai#20116, or "matchesLikePattern — `_` is one code point" for objectstack-ai#20143. - **The `§1` … `§7` markers stay.** They number each file's own sections, which its comments (`// §1 …`) and header name; only the card number in front of them goes. - **Files that carried a record as a file-wide tag** (`filter-cross-field-comparison-class`, `filter-number-comparand-declared-type`, `filter-text-operator-declared-type`, `filter-like-wire-lowering`, `filter-text-comparand`): the verdict's describe now states the decision (above). The others name the contract part they pin (the judged fields, the refusal message, the fixture, the case table), and their child titles state what each part does. The files' header comments still name the records; comments are not touched here. - **Declared labels and messages:** the five `filter-lowering.test.ts` `NULL_ROWS` names feed `it(row.name)` and the seven `filter-save-door-face-parity.test.ts` labels feed a `%s` title; vitest prints both whole, as the head run's names show. ## Readers - **Test-name filters:** none. No tracked script, workflow or config passes `-t` / `--testNamePattern`. - **Snapshots:** none. No `__snapshots__` directory is tracked under `packages/spec`. - **Projects:** none of the 20 files is listed in `packages/spec/vitest.repo-tests.json`; all 20 run in the `local` project. - **By substring:** every old literal, plus a window around each id (260 needles), was searched across the tracked tree outside its own file. No gate, doc, filter, snapshot or `scripts/check-*.mjs` self-test reads one. The 7 needle hits fall on 5 lines: a code comment in `filter-comparand-type.ts:392`, and sibling titles in `objectql` (`engine-number-comparand-declared-type-door.test.ts:447`, `:459`, `:484`) and `rest` (`data-number-comparand-door.test.ts:269`). None reads a spec test title. - **The files by name:** `scripts/check-driver-conformance.mjs` names `filter-text-conformance.ts`, the source module, not the test file's titles. The other references to these file names are code comments, plus one design-doc line (`docs/design/predicate-compilation-convergence.md:377`) that points at `filter-lowering.test.ts`'s `INTRODUCED` constant, not at a title. ## Text-only proof Stage 10's scratch tool (`textonly10.cjs`, md5 `d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file on three legs: 1. **Skeleton:** the full AST, with string pieces masked. It must be identical. 2. **Comments:** every comment, byte-equal. 3. **Strings:** each changed string leaf must sit in a test-call title position or on a declared line, must carry a tracker id before, and must carry no `#` plus digits after. The declared lines are `filter-lowering.test.ts:81`, `:83`, `:85`, `:93` and `:95`, `filter-operator-vocabulary.test.ts:82` and `:89`, `filter-save-door-face-parity.test.ts:308`, `:309`, `:311` to `:314` and `:743`, and `filter-text-conformance.test.ts:162`. - **Result:** 20 of 20 files SAME on all three legs, with the per-file counts predicted in writing before the run. - **Totals:** 96 changed string leaves in 95 literals, 81 titles and 15 declared. The diff's `+` and `-` lines are exactly the 96 planned lines, and every file keeps its line count. - **Controls (10 of 10 as predicted, on scratch copies, each anchor hit once):** identifier rename DIFF; numeric literal DIFF; comment edit COMMENT DIFF; a non-title string given an id VIOLATION; a rewritten title given a new id VIOLATION; a title that was id-free at base edited VIOLATION; one title reverted to base SAME; a declared string given a new id VIOLATION; an undeclared `expect` message changed VIOLATION; a title re-split into a `+` chain DIFF. **Test counts:** the 20 files were run at the base, in a separate base worktree, and at the head, with `--project local --project repo`. Both sides read 671 tests, all passed, with the same count and status sequence per file in 20 of 20. 543 full test names change, and each equals the base name with the planned replacements applied (0 mismatches). No full name repeats on either side. ## `main` since the base Re-fetched just before this PR opened, `origin/main` was three commits past the base (`5b2d189e28`). None of them touches `packages/spec` or any of the 20 files, so `main` was not merged. ## Changeset: `skip-changeset` Measured, not assumed: - `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the 20 touched files are in it, and no `*.test.ts` at all. The controls `src/data/filter.zod.ts` and `dist/index.mjs` are in it. - In the built `dist/`, a new phrase and an old literal each read in 0 files. The control `Unrecognized key` reads in 42. So this PR publishes nothing, and no changeset is added. ## Verification (at `2ceeffd7e7`) - `pnpm turbo run build` over all packages: 71 / 71. - `@objectstack/spec`: - `vitest run --project local`: 615 files, 18384 passed, 1 todo. - `typecheck` exit 0, including `check:test-typecheck` (52 files / 246 errors / 135 pinned signatures held). Its program holds all 20 touched files, counted with `tsc --listFilesOnly -p tsconfig.test.json`. - `check:generated`: all 15 generated artifacts up to date. - **Gates:** `dispatch-gates --commands` derived 79 families, the same set as stages 13 to 16, and all 79 exit 0. `--ran` reconciles: 79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN, every family with its exit code recorded. - The five roster families whose rosters sit under a touched directory were also run, and each exits 0: `check:meta-url-spelling`, `check:spec-changes`, `check:authz-resolver`, `check:error-code-casing` and `check:filter-alias-parity`. - **ESLint, a proven narrowing:** `--no-inline-config` over the 20 files reads 0 errors and 0 warnings. The population comes from ESLint's own config: 20 configured, 0 ignored. No file sets `parserOptions.project` or `projectService`, so no untouched file's verdict can move. - `check-governed-merges --test`: NOT governed, 192 changed lines. ## Acceptance notes - **No needle in this group.** Every id was a title or a declared string; no expected value of an assertion over a source docblock was found. The three known needles are untouched. - **Same-id test titles in this card's later stages** go with those stages: 6 lines in `packages/spec/src`. Two are in the next `data/` group (`data/filter.test.ts:576` and `:679`), and four are in `ui/` (`component-object-grid-default-filters.pin.test.ts:56`, `component.test.ts:3518`, `dataset-filter-nested-relation-list.test.ts:220`, `view-filter-rule-value-shape.test.ts:249`). - **Same-id test titles in other packages** are their lanes' test-string shares. A search of `describe` / `it` / `test` lines outside `packages/spec` finds 187 lines in 16 packages and one example app citing ids this PR handled: `driver-sql` 32 (14 files), `service-analytics` 32 (18), `formula` 24 (9), `lint` 21 (8), `objectql` 21 (11), `driver-memory` 13 (10), `driver-turso` 11 (10), `plugin-security` 9 (5), `driver-mongodb` 8 (5), `driver-sqlite-wasm` 6 (5), `rest` 3 (2), `cli` 2, `metadata-protocol` 2, and one each in `core`, `runtime` and `examples/app-todo`. - **Code comments still carry ids** in these files and their sources, for example the `// [objectstack-ai#21448]` line above `filter-save-door-face-parity.test.ts:311` and the header of `filter-operator-vocabulary.test.ts`. Comments are not this card's share, and none is touched here. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21768
Clause-②: yes (widening)
What this does
The spec half of objectstack-ai/objectui#11610, which renamed the
gridwidget's eight field-level keys to camelCase (merge2abec3a96c). The.objectui-shapin onmainis9dfaca654311, and it carries that merge:merge-base --is-ancestor 2abec3a96c 9dfaca654311exits 0, which proves ancestry on its own.This executes ruling B on #21704 fork 2 (record
5978663135). That ruling deferred the eight keys "until objectui camelCases them". This PR also answers objectui's two asks in5981368783and the at-tier review's ③ in5986881331. It works under the claim5987845581.buildObjectFormRuntimeField:object-formcustomFields[], and the inline arm of both forms'sections[].fields[])minRows…sortFieldrefused as unrecognized keystotalFieldrecord:line_itemssum is saved toCensus at the pin (objectui
9dfaca654311, read withgit show)GridFieldMetadata(types/src/field-types.ts:1031-1088) and the zod mirror (types/src/zod/form.zod.ts:1102-1117)fields/src/widgets/GridField.tsx,cfg = fieldat:669)minRowsnumber/z.number():762; Remove stops at it (:882,:1447)min_rows: refusedmaxRowsnumber/z.number():763; Add, Duplicate and the blank row stop at it (:808,:850,:892,:1077,:1487)max_rows: refusedallowAddboolean/z.boolean():749,!== false; off when read-only or disabledallow_add: refusedallowDeleteboolean/z.boolean():750, the sameallow_delete: refusedallowReorderboolean/z.boolean():783, the sameallow_reorder: refusedtotalFieldstring/z.string():771; summed into the footer at:919-923total_field: refusedaddLabelstring/z.string():1322(empty state) and:1491(Add button)add_label: refusedsortFieldstring/z.string():778; stamped on every row byemit(:785-790)sort_field: refusedNo snake_case read remains.
git grepof the eight snake_case spellings over objectui'spackages/*/src(tests and stories excluded) returns only its refusal faces:field-types.ts:1106-1141,form.ts:2108-2143);GRID_FIELD_RETIRED_KEYS(field-types.ts:1159);zod/form.zod.ts:1122-1129);GridField.tsx:617.The widget refuses a field that carries any of them, and draws a refusal instead of the grid (
GridField.tsx:643-650). The control grep for the camelCase spellings over the same scope has 38 hits inGridField.tsxalone.FormFieldtakes each key by reference toGridFieldMetadata(types/src/form.ts:2079-2094).totalFieldis a homonym. The line-items panel and the master-detail form both hand the grid theiramountFieldas itstotalField(plugin-form/src/LineItemsPanel.tsx:710,MasterDetailForm.tsx:876).Writers of either spelling in this repo (base
75ddcd1b41)component-form-custom-fields-sections-typed.pin.test.ts,min_rows).conversions/registry.ts,migrations/registry.tsand two migration entries.examples/,skills/,content/docs/orapps/.addLabelhits there (examples/app-showcase/.../project-workspace.page.ts:59,skills/objectstack-ui/SKILL.md:83) are master-detail detail entries, a different surface that is unchanged.fields-grid/line-items-gridfield, which carries all eight keys.Changes,
packages/spec/src/ui/component.zod.tsobjectFormRuntimeFieldGridMembers(), declares them, and the runtime field spreads it into its shape. The builder's docblock carries the read points above.OBJECT_FORM_GRID_WIDGET_SNAKE_KEYSis now a map from each snake_case key to its camelCase key (satisfiesthe declared key union, so a value naming an undeclared key failstsc).total_fieldbullet also restates the CHILD-column meaning.9dfaca654311:LineItemsPanel.tsx:696-720,:816-827, spelledaddLabel/sortField;customFieldsfork note, which now records the rename;sortFieldretirement note, which keeps its dated read and adds a hop sentence: the derived field is handed to the grid assortFieldatMasterDetailForm.tsx:877, and the entry key is still a tombstone.record:line_items'sortFieldrefusal said "No block takes an authoredsortField". An inlinegridfield now takes one, for the rows of its own value, so the sentence now reads "No block takes an authoredsortFieldfor child records". The accept set is unchanged.content/docs/references/ui/component.mdxwas regenerated bycheck:generated --fix, which named onlygen:docsas stale. The changeset is.changeset/21768-object-form-runtime-field-grid-camelcase-keys.md:@objectstack/specminor, withClause-②: yes (widening)at line start.D3 reading
No accept set narrows. Every value that parsed at the base still parses, so no ADR-0087 D3 entry and no conversion is owed. Before this PR, the eight camelCase keys were refused (
unrecognized_keys) and the eight snake_case keys were refused. After it, the camelCase keys parse and the snake_case keys are still refused. A camelCase key with the wrong value type is still refused, now asinvalid_type. No step-18 file is touched.Tests, at head
bb32836dc9component-form-custom-fields-sections-typed.pin.test.tscustomFieldsmember. Lit controls: the keys on anobject-formsection's inline entry, with the three switchesfalse, and on anobject-master-detail-formsection's inline entry.min_rowsrow keeps its code and path. Its prescription check now asserts that the bullet namesmin_rowsandminRows.unrecognized_keysatcustomFields.0, and exactly one bullet names that key and its camelCase key. The camelCase key then parses with the same value.total_fieldthe same way.invalid_typeat its own path.totalFieldhomonym: the inline grid field's describe starts with the CHILD column and the footer, and namesamountField. Therecord:line_itemsand master-detail detail-entry describes start with "Parent field to receive the rolled-up sum" and never say CHILD or footer.The assertions check named subjects, not copy.
master-detail-detail-sort-field-retirement.test.tssortField: 'position'. The walk's own header covers this case: "A future schema that declares asortFieldof its own would trip this walk: narrow the matcher to detail entries then, never exclude the new file."typeorwidgetgrid/field:gridat its own level, which marks the inline grid field.gridtype, an inline grid field placed earlier than a detail entry in the same text, andtype: 'grids'.Ablation
sortFieldfrom the builder turns six tests red. They are §3's key set, §1's two grid writer rows, §5'ssort_fieldrefusal row, §5'ssortFieldvalue-type row, and §5's describes test. The retirement pin stays green.node scripts/ablation-replace.mjs --delete. The anchor hit once and went 1 → 0, and the blob went298601db7467→48deefce4054.Tests 6 failed | 108 passed (114).298601db7467,git diff HEADis empty, andgit status --porcelainis empty.edbdef7bf8, and again atbb32836dc9. The subject is imported from./component.zod(source, notdist), so no rebuild is involved.Suites
pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec typecheckatbb32836dc9: exit 0.check:test-typechecksays OK, and the debt ledger is unchanged.tsc -p tsconfig.test.json --listFilesOnlylists both edited test files, and neither has a ledger entry.vitest run --project local):Test Files 615 passed (615),Tests 18383 passed | 1 todo. It ran atedbdef7bf8. The only later change is the pin file's assertion wording, re-run atbb32836dc9withalias-integrity.test.tsandstrict-object.test.ts:Tests 166 passed (166).Gates, at head
bb32836dc9node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 110 commands for this change set, and every one exits 0.--ranreconciliation:110 derived, 110 run, 0 NOT-MEASURED, 0 UNRUN.check:type-check-debtran under the verify lock, because its first unlocked run hit the 280s per-command cap.edbdef7bf8), six gates (check:doc-formula-expressions,check:doc-security-posture,check:skill-examples,check:docs-transcript-drift,check:lean-entry-closureandcheck:dual-build-cjs-loads) first answered exit 3, PREREQUISITE NOT MET, because their packages had nodist. After a turbo build oflint,formula,client-reactandobjectql(all cache hits), each answered 0. All six also answered 0 atbb32836dc9.pnpm lintis CI's. Locally, ESLint ran over the three edited TS files atbb32836dc9, with three pieces of evidence:--print-config(packages/**/*.{ts,tsx,mts,cts}), so none is ignored.--format jsonreports 3 files, 0 errors and 0 warnings.parserOptionsholds onlyecmaVersionandsourceType, with noproject, and no typed rule is on. Linting is not type-aware, so this diff cannot change the verdict on any file it did not touch.dispatch-gateslists outside its derived set (Test Core shards, Dogfood, Build Core, Build Docs, Temporal Conformance, and the workspace and consumer type checks). CI runs them.Declared beyond the claim's listed surface
master-detail-detail-sort-field-retirement.test.ts: the claim lists no file for this edit. The newsortFieldmember is what tripped the walk, and the walk's header says what to do, so it was narrowed, not excluded (see Tests).record:line_itemssortFieldrefusal sentence: the sentence became false with this PR, so it was corrected in the same file (see Changes).Acceptance notes
mainis 2 commits past this branch's base (ba57588665). They touchservice-settings, the QA checklist JSON and a comment-only hunk inspec/src/contracts/crypto-provider.ts. None of that overlaps this diff, somainwas not merged in. CI's merge ref covers the combination.replacementtext of the D3 entryui-record-line-items-props-closedsays "sortField, which no block takes".conversions/registry.ts(about:12488) and the retired-key entry18.ui__ObjectMasterDetailFormProps__details.sortFieldsay the derived field is "handed to the grid assort_field(:874)", read at pin89cad75d5570.reasonsays the snake_case keys stay out "until the widget reads a camelCase" spelling..changeset/21464-component-props-form-custom-fields-sections-typed.md) still says the eight keys "come in once the widget reads a camelCase spelling". If it ships in the same release as this changeset, the CHANGELOG reads as a sequence: that entry, then this one stating that they came in. It is not edited here, because it is outside this PR's surface. Carrier: none.Generated by Claude Code