Repository navigation
docs(spec): the master-detail detail entry's inlineMode and formFields describes state both renderer paths (#21284) - #21307
Conversation
…s describes state both renderer paths An entry that names both relationshipField and at least one column is kept as authored by the renderer: an omitted inlineMode is not resolved from the relationship's inlineEdit and an omitted formFields is not derived. The two describes, and the factory's docblock clause that repeated the inlineMode claim, now say what happens on that path and on the derived one. Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…l entry describes Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. 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 6ddc13d60642d2c79528b6df5f77530f4e9bb1a7 && git checkout 6ddc13d60642d2c79528b6df5f77530f4e9bb1a7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 393ae878d3d52fe843c56b4621c004b934dcf853 e21f622b6512432103df79a6df6eb53a721722d6 && git checkout -B drift-repro 393ae878d3d52fe843c56b4621c004b934dcf853 && git merge --no-ff e21f622b6512432103df79a6df6eb53a721722d6
node scripts/docs-audit/affected-docs.mjs --json 393ae878d3d52fe843c56b4621c004b934dcf853 |
Contract reviewServed-tier: Isolated contract review of PR #21307 (card #21284), written 2026-10-02T04:11Z. Inputs: the card and its three comments (triage ① Derived judgmentsAccept set and public surface: nothing moves — right. Three files, +18/−5. In
Consistency with spec code on The TSDoc clause — same defect, same ruling, true, no other claim. The The changeset's prose — right. It restates the two paths exactly as the diff and the pin have them (derived: ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…d describes state each renderer path (objectstack-ai#21315) (objectstack-ai#21355) Fixes objectstack-ai#21315 Clause-②: no ## What changed An `object-master-detail-form` detail entry has two describes for keys the renderer can fill in when they are omitted: `sortField` and `amountField`. Both now state what happens on each of the renderer's paths. The reference page that lifts them (`content/docs/references/ui/component.mdx`) is regenerated by `check:generated --fix`. No schema accepts or refuses anything new. There is no shape, default or nullability change, and no objectui edit. The model is PR objectstack-ai#21307, which did the same for `inlineMode` and `formFields`. `sortField`, before: > Child field holding the line sort position, stamped on drag-reorder (derived from a `position` / `sort_order` / … field when omitted) after: > Child field holding the line sort position, stamped on drag-reorder. When omitted it is the child object's first field named `position`, `sort_order`, `sequence`, `line_no`, `line_number` or `sort`, if it has one — except on an entry that names `relationshipField` and at least one column and gives every column a `type`. The renderer keeps that entry exactly as authored: nothing is derived, the grid stamps no line position, and a drag-reorder is not saved `amountField`, before: > Numeric child column summed for the running total after: > Numeric child column summed for the running total and the `totalField` rollup. When omitted it is picked from the grid's number and currency columns: a computed one, else one named `amount`, `total`, `subtotal`, `line_total`, `line_amount` or `net_amount`, else the last currency column, else the last numeric one — except on an entry that names `relationshipField` and at least one column and gives every column a `type`. The renderer keeps that entry exactly as authored, so nothing is picked. With no `amountField` authored or picked, the sums read a child column named `amount`, and the grid shows a running total only when `totalField` is set ## What the renderer does (objectui at the `.objectui-sha` pin `31971ff1e28f`, read with `git show`) `packages/plugin-form/src/MasterDetailForm.tsx`: - 927–929, `needsDerive`: an entry needs resolution when it has no `relationshipField`, no columns, or a column without a `type`. At 958, when no entry in the block needs it, every entry is kept as authored. - 965–967, the fast path: `relationshipField` is set and every column (at least one) has a `type`. The entry is returned unchanged (`{ ...entry, status: 'ready' }`). An omitted `sortField` stays undefined, and 862 passes `sort_field: d.sortField`, which is undefined. An omitted `amountField` also stays undefined. - 1032–1038: every other entry that loads its child schema goes through `deriveDetail`. Then `amountField = d.amountField ?? derived.amountField` and `sortField = d.sortField ?? derived.sortField`. - 1048–1055, the hydrate path: `relationshipField` is set and columns are present, but some have no `type`. The config becomes `{ ...d, columns: derived.columns, amountField, sortField }`. So `formFields` and `inlineMode` are kept as authored here, but `sortField` and `amountField` ARE derived. The comment at 1040–1047 gives the reason (objectui#11144). - 1056–1069, the derived path: everything is derived, with `amountField` and `sortField` at 1065–1066. `packages/plugin-form/src/deriveMasterDetail.ts`: - 540 with 55: the derived `sortField` is the first child field whose name is one of `position`, `sort_order`, `sequence`, `line_no`, `line_number` or `sort`. - 534 with 476–495: the derived `amountField` is `pickAmountField(columns)`. That is a computed number or currency column, else one named in `AMOUNT_LIKE_FIELDS`, else the last currency column, else the last numeric column. `packages/fields/src/widgets/GridField.tsx` 726–737: the grid stamps `row[sortField] = index` only when `sort_field` is set. Its comment reads "Without it, reorder is order-of-entry." When no `amountField` is authored or picked (`MasterDetailForm.tsx`): - 728: the document subtotal sums `e.config.amountField || 'amount'`. - 861: the grid's `total_field` is `d.amountField || (d.totalField ? 'amount' : undefined)`, unless the tax stack below it takes over. - 1549–1550: the `totalField` rollup writes `sumRows(rows, d.amountField || 'amount')`. So the path split is not objectstack-ai#21284's. `formFields` and `inlineMode` are kept as authored on both the fast path and the hydrate path, which is an entry that names `relationshipField` and at least one column. `sortField` and `amountField` are kept as authored only on the fast path, where every column also has a `type`. Each describe states its own split. ## Why `amountField` changes too The triage direction put it in this edit. Its old text made no false claim, but it said nothing about omission, and omission changes the result by path. On the fast path nothing is picked, and the sums read a column named `amount`. From 1549–1550 (a reading, not a run): take a fully typed entry whose line-total column is `line_total`, with `totalField` set and `amountField` omitted. It writes 0 into the parent's `totalField`, because `sumRows` counts a missing value as 0. The derived path would have picked `line_total`. An author cannot know to write the key on the fast path unless the describe says so. ## Every member of `masterDetailDetailEntry()`, read for the same gap - `childObject`: required, and nothing derives it. Holds. - `relationshipField`: "auto-detected … when omitted". The fast and hydrate paths both need the key, so an entry without it always takes the derived path, and `findRelationshipField` (`deriveMasterDetail.ts` 141–154) runs there. That function takes the `master_detail` field that references the parent, else a `lookup`. Holds. - `columns`: "derived from the child object when omitted". The fast and hydrate paths both need columns, so an entry without them always reaches `deriveColumns`. A column given only its `name` has no `type`, so the entry is hydrated. Holds. - `formFields` and `inlineMode`: objectstack-ai#21284's text, re-read at the pin. They are kept as authored at 967 and 1048–1055, and derived at 1063–1064. The offer conditions are at 847 and 850. Holds. - `amountField` and `sortField`: changed above. - `totalField`: nothing derives it, and its describe says nothing about omission. The sum it receives is the one the `amountField` describe now explains. Holds. - `title`, `minRows`, `maxRows` and `addLabel`: passed through or given a default label (755, 863–865). None of their describes claims an omission behaviour. Holds. - The factory's TSDoc calls `sortField` "the line-position field the grid stamps on drag-reorder" and claims no derivation. Holds, not edited. ## The `sortField` retirement waits on the pin bump objectui `main` retired the authored `sortField` at `0a3e5409f` (objectui#11376), after this pin. `git merge-base --is-ancestor 31971ff1e28f 0a3e5409f` exits 0, so the pin is in that commit's history and `0a3e5409f` is not in the pin's. At the pin, `sortField` is still read (85, 862, 1038). This PR does not retire the key. The `.objectui-sha` bump that crosses `0a3e5409f` owes the spec half: retire `sortField` from this entry, with a tombstone and an ADR-0087 entry, in the same landing. The `sortField` describe written here is what the current pin does until then. For `amountField`, objectui `main` (`d8edfe2576`) still returns the fast-path entry unchanged at 979, and still sums `d.amountField || 'amount'` (740, 1564). So, as of that commit, the bump owes no `amountField` change. ## Acceptance notes - A form view's `subforms[]` entry (`FormViewSchema.subforms` in `packages/spec/src/ui/view.zod.ts`) has the same `amountField` describe, "Numeric child column summed for the running total". At the pin, `ObjectForm.tsx` 340 and 394 send a form with `subforms` to `MasterDetailForm` as `details`, so the same omission behaviour applies there. That describe makes no false claim, so this is an observation, not a filed defect. This PR keeps to its card's surface. Carrier: none. ## Verification (every reading below is on HEAD `e0b23743d5`) - `pnpm --filter @objectstack/spec build`: `VERDICT command-exit 0`. - `pnpm --filter @objectstack/spec check:generated`: the first run reported `1 of 15 artifact(s) stale: content/docs/references/**`, and every other artifact was current, including the authorable surface and the JSON schemas. `--fix` regenerated that one and its re-check gave `✓ check:docs`. In the gate union it exits 0. - `pnpm --filter @objectstack/spec typecheck`: exit 0. - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2`: `Test Files 597 passed (597)`, `Tests 17487 passed | 1 todo (17488)`. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, run with no paths: 102 commands. The sorted list is identical to the dispatch's derivation at `97239c3c8a`. All were run, with exit codes recorded. `--ran` answers `102 derived famil(ies) accounted for — 101 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)`. - Six gates first exited 3 because their prerequisite packages were not built: `check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples`, `check:docs-transcript-drift`, `check:lean-entry-closure` and `check:dual-build-cjs-loads`. After building that closure (`turbo run build` for formula, lint, client-react and objectql, 34 tasks), the first five exit 0. - NOT MEASURED: `pnpm check:dual-build-cjs-loads`. It needs every workspace package's `dist` (hono, account, setup, studio, client and more), which needs a full-repo build. As a narrower check, all 19 `require` entry points of `@objectstack/spec` load. CI runs the full gate. - eslint, narrowed. Of the diff's three files, only `component.zod.ts` is in eslint's population: for the `.md` and `.mdx` files, eslint answers "File ignored because no matching configuration was supplied". `--format json` reports 0 errors and 0 warnings for that file. Its resolved config has `parserOptions` without `project`, so linting is not type-aware and this diff cannot change the verdict for any untouched file. The full `pnpm lint` runs in CI. - `main` was not merged in. Since the base `97239c3c8a`, `origin/main` has gained three commits, and none of them touches `packages/spec` or the reference page. --- _Generated by [Claude Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21284
Clause-②: no
What changed
An
object-master-detail-formdetail entry has two describes that say what an omitted key means:inlineModeandformFields. Both now state the renderer's two paths. The reference page that lifts them (content/docs/references/ui/component.mdx) is regenerated bycheck:generated --fix. No schema accepts or refuses anything new. There is no shape, default or nullability change, and no edit to objectui orpackages/lint.inlineMode, before:after:
formFields, before:after:
What the renderer does (objectui at the
.objectui-shapin31971ff1e28f, read withgit show)packages/plugin-form/src/MasterDetailForm.tsx:needsDerive: an entry needs resolution when it has norelationshipField, no columns, or an untyped column.relationshipFieldset and every column typed. The entry is returned unchanged ({ ...entry, status: 'ready' }).relationshipFieldset andcolumnsnon-empty. The config becomes{ ...d, columns: derived.columns, amountField, sortField }, soformFieldsandinlineModestay as authored.formFields: d.formFields ?? derived.formFields(1063) andinlineMode: d.inlineMode ?? derived.mode(1064).derived.modecomes fromresolveInlineMode(deriveMasterDetail.ts536–539 and 456–470). It is the relationship field'sinlineEditwhen that isgridorform, and otherwise a default chosen from the child object's shape.d.inlineMode === 'form', or whenformFieldsis longer thancolumns. At 850,displayModeislistonly forform; otherwise it isgrid.fieldsonly whenformFieldsis non-empty. Without them,ObjectForm(ObjectForm.tsx961) drawsObject.keys(objectSchema.fields), which is every field the child object declares.So, for the question the card left open about
formFieldson the kept-as-authored path: when it is omitted, nothing is derived. A grid never offers the per-row form, because zero fields is never more than the column count. The form is offered only wheninlineModeisform, and it then draws the child object's full field list. The old text was false there, so this PR rewords it. It also drops "editable": the derived list keepsreadonlyfields (deriveFormFieldsat the pin, 404–420, andderiveInlineRowFormFieldsinpackages/spec/src/data/inline-grid-columns.ts).The wording matches what the spec already says about both paths. "Kept as authored" means an entry that names both
relationshipFieldand at least one column, as in thecreditAuthoredRowFormdocblock inpackages/lint/src/validate-field-consumers.ts. The offer condition isisInlineRowFormOfferedin@objectstack/spec/data.A bounded in-place fix, declared
The
masterDetailDetailEntryfactory's TSDoc (component.zod.ts, the paragraph above the factory) saidinlineMode's "absence takes the relationship's own resolution". That is the same claim, true on only one path, and it ships in the tarball undersrc/**/*.zod.ts. It now says this holds only on an entry the renderer derives, and points to the describe. This is outside the claim's declared file surface, which names the two describes. It is made under the bounded in-place rule: it is the same defect, this card's ruling fixes its form, no other claim holds that region of the file, and it adds no new gate. It is a comment-only change of one added line.Released surface
The card says the sentence "is not yet in a release". Measured today,
@objectstack/spec@17.6.0is published aslatest, and its tarball carries the sentence once each insrc/ui/component.zod.tsanddist/ui/index.js. So 17.6.0 already ships it. Thispatchcorrects it in the next release.Acceptance notes
sortField's describe, "(derived from aposition/sort_order/ … field when omitted)", is false on the fast path. At 967, an entry withrelationshipFieldand every column typed is returned as authored, so an omittedsortFieldis not derived. The hydrate path at 1037–1051 does derive it, since objectui#11144. This PR does not touch it: it is outside this card's surface, and someone must decide whether the renderer's fast path or the describe changes. It is reported to the seat.Verification (every reading below is on HEAD
e21f622b65)pnpm --filter @objectstack/spec build:VERDICT command-exit 0.pnpm --filter @objectstack/spec check:generated: the first run reported1 of 15 artifact(s) stale: content/docs/references/**, and every other artifact was current, the authorable surface and JSON schemas included.--fixregenerated that one, and its re-check gave✓ check:docs. Run again in the gate union: exit 0.pnpm --filter @objectstack/spec typecheck: exit 0.pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2:Test Files 597 passed (597),Tests 17484 passed | 1 todo.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, run with no paths: 102 commands, the same set as the dispatch's derivation. All were run, and--rananswers102 derived famil(ies) accounted for — 101 run, 1 NOT-MEASURED.check:doc-formula-expressions,check:doc-security-posture,check:skill-examples,check:docs-transcript-driftandcheck:lean-entry-closure. After building that closure (turbo run buildfor formula, lint, client-react and objectql: 34 tasks), all five exit 0.pnpm check:dual-build-cjs-loads. It needs every workspace package'sdist, which needs a full-repo build. As a narrower check, all 19requireentry points of@objectstack/specload. CI runs the full gate.component.zod.tsis in eslint's population. For the.mdand.mdxfiles, eslint answers "File ignored because no matching configuration was supplied".--format jsonreports 1 file, 0 errors and 0 warnings. That file's resolved config hasparserOptionswithoutproject, so linting is not type-aware and this diff cannot change the verdict for any untouched file. The fullpnpm lintruns in CI.Generated by Claude Code