docs(spec): the master-detail detail entry's sortField and amountField describes state each renderer path (#21315) - #21355
Conversation
…d describes state each renderer path Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…ce page Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
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 0b1ad68f3038154d74c1b5e987584652ed4d96c0 && git checkout 0b1ad68f3038154d74c1b5e987584652ed4d96c0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6c5bef5f4ee5b3c5ed497e785b4d24f6b2649bf5 e0b23743d508616834cadbd601d62e90035f3495 && git checkout -B drift-repro 6c5bef5f4ee5b3c5ed497e785b4d24f6b2649bf5 && git merge --no-ff e0b23743d508616834cadbd601d62e90035f3495
node scripts/docs-audit/affected-docs.mjs --json 6c5bef5f4ee5b3c5ed497e785b4d24f6b2649bf5 |
Contract reviewServed-tier: PR #21355 (card #21315), the net diff against ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
Fixes #21315
Clause-②: no
What changed
An
object-master-detail-formdetail entry has two describes for keys the renderer can fill in when they are omitted:sortFieldandamountField. 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 bycheck: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 #21307, which did the same forinlineModeandformFields.sortField, before:after:
amountField, 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 a column without atype. At 958, when no entry in the block needs it, every entry is kept as authored.relationshipFieldis set and every column (at least one) has atype. The entry is returned unchanged ({ ...entry, status: 'ready' }). An omittedsortFieldstays undefined, and 862 passessort_field: d.sortField, which is undefined. An omittedamountFieldalso stays undefined.deriveDetail. ThenamountField = d.amountField ?? derived.amountFieldandsortField = d.sortField ?? derived.sortField.relationshipFieldis set and columns are present, but some have notype. The config becomes{ ...d, columns: derived.columns, amountField, sortField }. SoformFieldsandinlineModeare kept as authored here, butsortFieldandamountFieldARE derived. The comment at 1040–1047 gives the reason (objectui#11144).amountFieldandsortFieldat 1065–1066.packages/plugin-form/src/deriveMasterDetail.ts:sortFieldis the first child field whose name is one ofposition,sort_order,sequence,line_no,line_numberorsort.amountFieldispickAmountField(columns). That is a computed number or currency column, else one named inAMOUNT_LIKE_FIELDS, else the last currency column, else the last numeric column.packages/fields/src/widgets/GridField.tsx726–737: the grid stampsrow[sortField] = indexonly whensort_fieldis set. Its comment reads "Without it, reorder is order-of-entry."When no
amountFieldis authored or picked (MasterDetailForm.tsx):e.config.amountField || 'amount'.total_fieldisd.amountField || (d.totalField ? 'amount' : undefined), unless the tax stack below it takes over.totalFieldrollup writessumRows(rows, d.amountField || 'amount').So the path split is not #21284's.
formFieldsandinlineModeare kept as authored on both the fast path and the hydrate path, which is an entry that namesrelationshipFieldand at least one column.sortFieldandamountFieldare kept as authored only on the fast path, where every column also has atype. Each describe states its own split.Why
amountFieldchanges tooThe 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 isline_total, withtotalFieldset andamountFieldomitted. It writes 0 into the parent'stotalField, becausesumRowscounts a missing value as 0. The derived path would have pickedline_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 gapchildObject: 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, andfindRelationshipField(deriveMasterDetail.ts141–154) runs there. That function takes themaster_detailfield that references the parent, else alookup. Holds.columns: "derived from the child object when omitted". The fast and hydrate paths both need columns, so an entry without them always reachesderiveColumns. A column given only itsnamehas notype, so the entry is hydrated. Holds.formFieldsandinlineMode: spec(ui): an object-master-detail-form detail entry's inlineMode describe says the mode is resolved from the relationship's inlineEdit when omitted; on an entry kept as authored the renderer resolves nothing #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.amountFieldandsortField: changed above.totalField: nothing derives it, and its describe says nothing about omission. The sum it receives is the one theamountFielddescribe now explains. Holds.title,minRows,maxRowsandaddLabel: passed through or given a default label (755, 863–865). None of their describes claims an omission behaviour. Holds.sortField"the line-position field the grid stamps on drag-reorder" and claims no derivation. Holds, not edited.The
sortFieldretirement waits on the pin bumpobjectui
mainretired the authoredsortFieldat0a3e5409f(objectui#11376), after this pin.git merge-base --is-ancestor 31971ff1e28f 0a3e5409fexits 0, so the pin is in that commit's history and0a3e5409fis not in the pin's. At the pin,sortFieldis still read (85, 862, 1038). This PR does not retire the key. The.objectui-shabump that crosses0a3e5409fowes the spec half: retiresortFieldfrom this entry, with a tombstone and an ADR-0087 entry, in the same landing. ThesortFielddescribe written here is what the current pin does until then. ForamountField, objectuimain(d8edfe2576) still returns the fast-path entry unchanged at 979, and still sumsd.amountField || 'amount'(740, 1564). So, as of that commit, the bump owes noamountFieldchange.Acceptance notes
subforms[]entry (FormViewSchema.subformsinpackages/spec/src/ui/view.zod.ts) has the sameamountFielddescribe, "Numeric child column summed for the running total". At the pin,ObjectForm.tsx340 and 394 send a form withsubformstoMasterDetailFormasdetails, 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 reported1 of 15 artifact(s) stale: content/docs/references/**, and every other artifact was current, including the authorable surface and the JSON schemas.--fixregenerated 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 at97239c3c8a. All were run, with exit codes recorded.--rananswers102 derived famil(ies) accounted for — 101 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).check:doc-formula-expressions,check:doc-security-posture,check:skill-examples,check:docs-transcript-drift,check:lean-entry-closureandcheck:dual-build-cjs-loads. After building that closure (turbo run buildfor formula, lint, client-react and objectql, 34 tasks), the first five exit 0.pnpm check:dual-build-cjs-loads. It needs every workspace package'sdist(hono, account, setup, studio, client and more), 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 0 errors and 0 warnings for that file. Its 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.mainwas not merged in. Since the base97239c3c8a,origin/mainhas gained three commits, and none of them touchespackages/specor the reference page.Generated by Claude Code