Skip to content

docs(spec): the master-detail detail entry's inlineMode and formFields describes state both renderer paths (#21284) - #21307

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21284-detail-entry-describes
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21284-detail-entry-describes

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21284
Clause-②: no

What changed

An object-master-detail-form detail entry has two describes that say what an omitted key means: inlineMode and formFields. Both now state the renderer's two 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 edit to objectui or packages/lint.

inlineMode, before:

Inline-edit form factor: 'grid' = editable cells; 'form' = read-only list + per-row full form. Resolved from the relationship field's inlineEdit when omitted

after:

Inline-edit form factor: 'grid' = editable cells; 'form' = read-only list + per-row full form. When omitted it is resolved from the relationship field's inlineEdit, else from the child object's shape — except on an entry that names both relationshipField and at least one column, which is kept as authored: nothing is resolved, the collection renders as a grid, and the per-row form is offered only when formFields lists more fields than columns

formFields, before:

Child field names for the per-row expand form (derived from the child object's editable fields when omitted)

after:

Child field names for the per-row expand form. When omitted they are derived from the child object's fields — except on an entry that names both relationshipField and at least one column, which is kept as authored: nothing is derived, and the per-row form is offered only when inlineMode is 'form', where it draws the child object's full field list

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 an untyped column.
  • 967, the fast path: relationshipField set and every column typed. The entry is returned unchanged ({ ...entry, status: 'ready' }).
  • 1048–1055, the hydrate path: relationshipField set and columns non-empty. The config becomes { ...d, columns: derived.columns, amountField, sortField }, so formFields and inlineMode stay as authored.
  • 1056–1069, the derived path: formFields: d.formFields ?? derived.formFields (1063) and inlineMode: d.inlineMode ?? derived.mode (1064). derived.mode comes from resolveInlineMode (deriveMasterDetail.ts 536–539 and 456–470). It is the relationship field's inlineEdit when that is grid or form, and otherwise a default chosen from the child object's shape.
  • 847: the per-row expand control is offered when d.inlineMode === 'form', or when formFields is longer than columns. At 850, displayMode is list only for form; otherwise it is grid.
  • 1821: the row form receives fields only when formFields is non-empty. Without them, ObjectForm (ObjectForm.tsx 961) draws Object.keys(objectSchema.fields), which is every field the child object declares.

So, for the question the card left open about formFields on 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 when inlineMode is form, 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 keeps readonly fields (deriveFormFields at the pin, 404–420, and deriveInlineRowFormFields in packages/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 relationshipField and at least one column, as in the creditAuthoredRowForm docblock in packages/lint/src/validate-field-consumers.ts. The offer condition is isInlineRowFormOffered in @objectstack/spec/data.

A bounded in-place fix, declared

The masterDetailDetailEntry factory's TSDoc (component.zod.ts, the paragraph above the factory) said inlineMode's "absence takes the relationship's own resolution". That is the same claim, true on only one path, and it ships in the tarball under src/**/*.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.0 is published as latest, and its tarball carries the sentence once each in src/ui/component.zod.ts and dist/ui/index.js. So 17.6.0 already ships it. This patch corrects it in the next release.

Acceptance notes

  • sortField's describe, "(derived from a position / sort_order / … field when omitted)", is false on the fast path. At 967, an entry with relationshipField and every column typed is returned as authored, so an omitted sortField is 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 reported 1 of 15 artifact(s) stale: content/docs/references/**, and every other artifact was current, the authorable surface and JSON schemas included. --fix regenerated 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 --ran answers 102 derived famil(ies) accounted for — 101 run, 1 NOT-MEASURED.
    • Five 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 and check:lean-entry-closure. After building that closure (turbo run build for formula, lint, client-react and objectql: 34 tasks), all five exit 0.
    • NOT MEASURED: pnpm check:dual-build-cjs-loads. It needs every workspace package's dist, 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 1 file, 0 errors and 0 warnings. That file's 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.

Generated by Claude Code

claude added 2 commits October 2, 2026 03:09
…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>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation protocol:ui tooling labels Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 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
  • 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 393ae878d3d52fe843c56b4621c004b934dcf853 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6ddc13d60642d2c79528b6df5f77530f4e9bb1a7 — the merge of head e21f622b6512432103df79a6df6eb53a721722d6 into base 393ae878d3d52fe843c56b4621c004b934dcf853, 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 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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e21f622b6512432103df79a6df6eb53a721722d6
Local-runs: none

Isolated contract review of PR #21307 (card #21284), written 2026-10-02T04:11Z. Inputs: the card and its three comments (triage 5944138284, claim 5944837900, dev report 5945264989); the PR body and file list; the net diff of the branch against origin/main at this head (merge base 4e530568a2; the two modified files are byte-identical between that base and origin/main); the head's check-runs; objectui at the .objectui-sha pin 31971ff1e28f, read with git show (MasterDetailForm.tsx, deriveMasterDetail.ts, ObjectForm.tsx under packages/plugin-form/src/); inline-grid-columns.ts and the creditAuthoredRowForm docblock on origin/main; and record 5937457620 on #20928. No dispatch order and no dispatching-seat conclusion was read. Nothing was checked out, built, run or re-run.

① Derived judgments

Accept set and public surface: nothing moves — right. Three files, +18/−5. In component.zod.ts the strictObject member list, the alias table, every type (z.enum(['grid', 'form']), z.array(z.string())), every .optional() and the absence of defaults are unchanged; only two describe() strings and one TSDoc clause change. content/docs/references/ui/component.mdx changes exactly the two table cells that lift those describes, and each new cell is byte-equal to its describe. The gitignored JSON schemas and dist will carry the new description strings: release text, not a contract move. Spec property liveness, Check Changeset and Governed Surface Queue Guard are green on this head, and no governed path is touched.

inlineMode describe — true on both paths at the pin, right.

  • Derived path (an entry that does not name both relationshipField and at least one column): MasterDetailForm.tsx 1056–1069 builds the config with inlineMode: d.inlineMode ?? derived.mode (1064). derived.mode is deriveMasterDetail.ts 538–539: inlineEdit = override.inlineEdit ?? childSchema.fields[relationshipField]?.inlineEdit (the renderer passes no inlineEdit override, 1032–1036), then resolveInlineMode 456–473 returns inlineEdit when it is 'grid' or 'form' (461) and otherwise picks from the child's shape (462–472: a form-only field, a cluster of rich fields, more than eight business fields, else grid). "Resolved from the relationship field's inlineEdit, else from the child object's shape" holds; an inlineEdit: true falls under "else", which the sentence covers and the PR body spells out.
  • Kept-as-authored path: the fast path 965–967 (relationshipField set and every column typed; columnsTyped is false on an empty or absent array) returns { ...entry, status: 'ready' } with the config untouched; the hydrate path 1048–1055 (relationshipField set and columns.length) returns { ...d, columns: derived.columns, amountField, sortField }, so inlineMode and formFields stay as authored; and when no entry needs resolution at all (927–929), 958 publishes the authored entries as they are. "Nothing is resolved" holds on all three. 850 renders displayMode = d.inlineMode === 'form' ? 'list' : 'grid', so an omitted mode is a grid; 847 offers the per-row expand control when d.inlineMode === 'form' or when d.formFields?.length ?? 0 exceeds d.columns?.length ?? 0, so with the mode omitted the offer is exactly "formFields lists more fields than columns". Both sentences hold.

formFields describe — true on both paths at the pin, right.

  • Derived path: 1063 formFields: d.formFields ?? derived.formFields; derived.formFields is deriveFormFields(childSchema, { relationshipField }) (535), which walks childSchema.fields (404–420) skipping the system and sort-name sets, the back-reference FK, fields flagged system or hidden, and the computed types in NON_INPUT_TYPES (396) — and keeps readonly fields. "Derived from the child object's fields" holds, and dropping "editable" is right: the old word named a filter the renderer does not apply.
  • Kept-as-authored path: the same three returns leave formFields as authored, so an omitted list stays omitted — "nothing is derived" holds. With it omitted, 847's count test asks whether zero exceeds the column count, false on an entry with at least one column, so the form is offered only under inlineMode === 'form' — holds. When that form opens, 1821 passes fields only when formFields is non-empty, and ObjectForm.tsx 961 then takes Object.keys(objectSchema.fields || {}), every field the child declares — "draws the child object's full field list" holds at the field-selection step (field-level security still gates per caller downstream, 954–958, which the sentence does not contradict).

Consistency with spec code on main — right. The offer condition is isInlineRowFormOffered verbatim (inline-grid-columns.ts 297). The module note (66–75) states the row form keeps readonly fields ("NOT readonly"), and deriveInlineRowFormFields 274–278 applies the pin's filter, so the dropped "editable" agrees with the spec's own rule. The creditAuthoredRowForm docblock (validate-field-consumers.ts 732–745) defines "kept as authored" as naming BOTH relationshipField and at least one column, with nothing derived and an omitted mode offering the form only when the list is longer than the grid, and "derived" as everything else, resolved from the relationship's inlineEdit else the child's shape. The describes say the same thing in the same terms.

The TSDoc clause — same defect, same ruling, true, no other claim. The masterDetailDetailEntry() paragraph (5006–5012 on origin/main) said inlineMode's "absence takes the relationship's own resolution", the one-path reading this card rules on; it now says that holds "only on an entry the renderer derives, and its describe states both paths", which is what 1064 against 967 and 1051 shows. It is one added comment line in the same region, outside the claim's declared surface but inside the card's defect. git grep on origin/main finds no other copy of the one-path claim (the two describes and this clause are the only three carriers). The No other open PR may claim the same single-writer path check is green on this head, and the claim itself records no in-flight claim on this region (#20274 declared its region disjoint; #21279 is queued, not in flight). Accepted as a bounded in-place fix; the seat owes the claim's file surface the amendment the dev could not post.

The changeset's prose — right. It restates the two paths exactly as the diff and the pin have them (derived: inlineEdit else shape, fields derived; kept: nothing resolved or derived, a grid, the offer by count or by 'form', the full field list), names both old sentences and why each was false, and says "No schema accepts or refuses anything new". Every sentence is one checked above.

② Semver level

@objectstack/spec patch with a bare no declaration — right. The diff adds no exported symbol, accepted key or accepted value and removes none; under the Check Changeset step's WHICH LEVEL rule a change that moves no public surface stays patch, and clause2-line.mjs reads a bare no as "no widening, and no direction declared", which is the truthful reading of a describe-text change: a JSON-schema description is not an accept set. skip-changeset would be wrong — the package publishes the text in src/, dist/ and its JSON schemas, and a fix to a released package takes a changeset (Post-Task Checklist 3). The declaration is spelled on the PR body's second line and in the changeset body, where check-adr-0087-registration reads it; Check Changeset concluded success on this head, and its level axis stands down on a no. Not breaking, so no BREAKING banner and no ADR-0087 disposition is owed. The changeset's bullets state truthfully what ships: two describes, one regenerated page, one source comment.

③ Boundary flags

  1. Falsified sub-premise — @objectstack/spec@17.6.0 already ships the old sentence. Confirmed: npm view answers 17.6.0 as latest (modified 2026-10-02T03:03:18Z), and the Version Packages PR chore: version packages #20639 merged at 2026-10-02T02:29:44Z, after the card and its triage and before the claim. The card's "not yet in a release" was true when written. It changes nothing in this PR: a wrong describe in a released package is a patch fix, the level the changeset already carries and the one that would have been right either way; the changeset body names the old wording, so the CHANGELOG a consumer greps carries the correction; and the carriers are unchanged (the describe, the regenerated reference page, and the dist and JSON-schema description strings the release regenerates). Triage anticipated it ("a patch release also repairs it"). The card's p1 rationale shifts from "freeze before 17.6.0" to "repair in the next patch", which is triage's to re-grade, not this review's.

  2. out_of_scope_findings — the sortField describe is false on the fast path, and that is outside this PR. Verified at the pin: 967 returns an entry with relationshipField and every column typed unchanged, so an omitted sortField stays undefined and 862 passes sort_field: d.sortField as undefined; the hydrate path (1038, 1051) and the derived path (1066) do derive it. The describe's "(derived from a position / sort_order / … field when omitted)" therefore teaches a derivation the fast path does not perform — the same class (c) as this card. Not in this PR's scope, and right to leave it: the claim's file surface names formFields and inlineMode; the card's "sibling describe to measure" named formFields only; and fixing it needs a contract-first decision (derive on the fast path, or narrow the text) that this card did not rule. The acceptance note is the right place for the PR, not the end of the matter.

  3. Is record 5937457620's retirement obligation a sufficient carrier? No — a card is owed, and the dispatching seat files it; this review files nothing. The record notes that objectui main retired the authored sortField at 0a3e5409f (an ancestor of objectui origin/main, after the pin; its subject reads "a detail's sort field is derived only"), and that the pin bump crossing it owes the spec half: retire sortField from the detail entry with a tombstone and an ADR-0087 entry. If that lands first, the describe goes with the key and the defect closes with it. But the obligation (a) is triggered by a pin bump nobody has scheduled, (b) lives in a Landed: record on a closed card (finding(spec): ObjectMasterDetailFormPropsSchema.details is z.unknown(), a third unjudged carrier of the inline grid column: a bogus key or a typed currency column with scale publishes green #20928), which no backlog filter or sweep reads as work, and (c) meanwhile leaves a released sentence (17.6.0, and every patch release until the bump) teaching a behaviour the fast path does not have — the filing gate's class (c), the very class the card above was filed for. The dev's proposed destination, "the family close-out card", does not exist: finding(lint): field-no-consumers still calls two in-use child-context fields "inert" — a lookup's inline-grid join key, and the fields an inline grid's per-row expand form draws (the family's closeout after #20951) #21091 and finding(spec): ObjectMasterDetailFormPropsSchema.details is z.unknown(), a third unjudged carrier of the inline grid column: a bogus key or a typed currency column with scale publishes green #20928 are both closed, and among the 44 open domain:spec issues only 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 names this detail entry. The card should carry both facts — the fast-path falsity at the pin (967, 862) and the retirement obligation with objectui 0a3e5409f — so triage can choose between a point fix (narrow the describe to the two paths that derive) and folding it into the retirement landing; objectui main has already retired the authored key, so "derive on the fast path" is not a live renderer option there. amountField has the same fast-path gap, and its describe claims no derivation, so it owes nothing.

  4. Deviation: labels. The dev wrote none, and skip-changeset was rightly not applied (a changeset is present). needs:contract-review is not on the PR, so this record removes nothing. Right.

  5. Deviation: no test added. A describe is prose no consumer parses, and check:generated already reds the reference page when it drifts from the describe; a test would pin wording. Right.

  6. Line corrections to the card (hydrate return 1048–1055 and derived return 1056–1069, against the card's 1048–1052 and 1055–1066): verified at the pin; the dev's lines are the right ones. The dev's other deviations (attribution footer, worktree cleanup) are outside the contract surface and raise nothing here; the PR body and the changeset carry no model identifier.

  7. Check-runs on this head at the final read (2026-10-02T04:10:22Z): 35 check-runs, all concluded — 33 success, 2 skipped (Console Pin Gate, which this diff's paths do not summon, and Packed-tarball smoke (opt-in)), none failure, none still running. The six Test Core shards, Lint & Repo Gates, the four Type Check lanes, Build Core, Build Docs, the three Dogfood Regression Gate shards, Temporal Conformance, Check Changeset, Spec property liveness and Governed Surface Queue Guard are among the successes. Their conclusions are the gate verdicts this record relies on; nothing was re-run locally.

Implemented-by: claude/issue-21284-detail-entry-describes
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

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/s tooling

Projects

None yet

2 participants