Skip to content

fix(lint,spec): field-no-consumers reads an inline relationship's join key, its per-row expand form, a detail entry's formFields and a record:line_items block against the child (#21091) - #21256

Merged
objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/issue-21091-field-consumers-inline-closeout
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/issue-21091-field-consumers-inline-closeout

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21091
Clause-②: yes

What changes

field-no-consumers (packages/lint/src/validate-field-consumers.ts) called several kinds of in-use child field "inert". This PR corrects them. The per-row expand form goes through a new derivation the spec owns, as deriveInlineGridColumns (PR #21089) did for the grid.

  1. Position 1: a lookup's inline-grid join key. A lookup or master_detail field that sets inlineEdit (with a resolvable reference) is now recorded as a behaviour read at its inlineEdit, whether the grid's columns are authored or derived. The renderer loads the child rows filtered on it and stamps it on save (objectui MasterDetailForm.tsx 1321 and 552, at the .objectui-sha pin 31971ff1e28f). master_detail was already exempt; lookup now reads the same.
  2. Position 2: the derived per-row expand form. Two new @objectstack/spec/data exports live in packages/spec/src/data/inline-grid-columns.ts. They sit in the same module as deriveInlineGridColumns because they share its system-name and sort-name sets.
    • deriveInlineRowFormFields(def, { relationshipField?, exclude? }): string[] is objectui's deriveFormFields stated as the spec's rule. It skips the same names as the grid, plus the relationship field, exclude, system / hidden fields and the computed types (formula, summary, rollup, autonumber, auto_number). It keeps readonly fields and every type a cell cannot edit.
    • isInlineRowFormOffered({ inlineMode?, formFields?, columns? }): boolean is the renderer's offer condition at MasterDetailForm.tsx:847: inlineMode === 'form', or more form fields than grid columns.
    • The lint credits the derived row form wherever it credits the derived grid: an inline relationship field with no authored inlineColumns, or a subforms / details entry with no columns. A details entry is excluded when it authors formFields, because an authored list replaces the derived one. No copy of objectui's rule lives in the lint.
  3. Position 3 (pointer 5936875973): a detail entry's authored formFields. These names are read against the entry's childObject; the general walk no longer reads them against the parent. isInlineRowFormOffered decides whether the list is drawn, and a list the form is never offered for is a carrier. The renderer resolves an entry one of two ways, and the lint feeds the predicate what each way feeds the expand control (round 2, F1):
    • Kept as authored: the entry names both relationshipField and at least one column (MasterDetailForm.tsx 967, 1048–1052). Nothing is derived. The form factor is the declared inlineMode, or none at all, so the predicate decides exactly. With an omitted inlineMode, the form is offered only when the list is longer than the grid.
    • Derived: anything else (1055–1066). A declared inlineMode is kept. An omitted one is resolved from the relationship's inlineEdit, else from the child's shape. The lint does not reproduce that resolution, so with an omitted mode the list is credited as drawn. With a declared mode, the predicate decides whenever the grid can be counted.
  4. Position 4 (pointer 5940763140): a record:line_items block. Its raw properties are read as one child entry: authored columns[].name, relationshipField and amountField against childObject, with totalField left on the parent. objectui LineItemsPanel.tsx at the pin reads these keys this way. It derives no grid and offers no row form. RecordLineItemsProps is not imported. Round 2, flag B: the block's sort and filter are now walked in the childObject's context. LineItemsPanel applies them to the child query (366–379, 516–521). Since PR feat(spec)!: record:line_items gets its ComponentPropsMap row, its columns are the inline grid column contract, and the showcase Tasks grid binds by name (#21142) #21244 landed RecordLineItemsProps, the contract declares filter as the ViewFilterRule array. The panel's lowering also takes the field-keyed map, and the lint reads whichever is authored. Both forms are pinned.

Fixture triage (round 1). Six tests in the [#20951] site-2 block pinned that a derived carrier leaves the json and readonly child fields inert. The derived row form now draws them, so their expected sets were re-judged: DERIVED keeps only the hidden field, and NO_ROW_FORM keeps the old set for the three cases that draw no derived row form.

Round 2: the contract review 5942628181 (FAIL) and what this head does about it

  • F1, fixed. The round-1 lint credited an authored formFields list as drawn whenever inlineMode was omitted. On the kept-as-authored path that is false: the renderer leaves the mode undefined, and line 847's count decides. The lint now decides that path with isInlineRowFormOffered({ inlineMode: undefined, formFields, columns }). The docblock and test titles state both paths. The test's own fixture (relationshipField and two columns, one form field) now pins itm.notes as carrier-only.
  • Flag B, measured and closed. See position 4. The probe confirmed it: the three child fields read only by a block's sort / filter were inert, and the same-named parent fields were credited in their place. It is pinned with two enumeration rows (sort[].field, filter[].field) and three unit tests.
  • Flag A, measured; not closed on this surface. Reading below.

Flag A: a row form opened with no field list

This happens when an authored grid is in the form factor and has no formFields. That covers authored inlineColumns with inlineEdit: 'form', or with inlineEdit: true and a child the smart default sends to form, and a detail entry kept as authored with inlineMode: 'form'. The renderer then opens the child's ObjectForm with no fields (MasterDetailForm.tsx 1821). That form draws the child's generated field set (ObjectForm.tsx 961) through filterSystemFields (autoLayout.ts 231): every field except the server-owned names, hidden fields and readonly fields, laid out by fieldGroups when the child declares any.

Probe reading (all three heads below): pg_line.note_g, ph_line.body_h, ph_line.note_h and pi_line.note_i are reported inert, and the renderer draws them. pg_line.ro_g (readonly) and pg_line.hid_g (hidden) are reported inert, and the renderer does not draw them either. The reach is confirmed.

Why it does not close here:

  1. Crediting it needs a spec-owned statement of the default object form's field set: ObjectForm's generated set, the server-owned roster from objectui sanitize.ts, the hidden and readonly filters, and the fieldGroups layout. That is a new cross-repo contract with its own differential and its own objectui consumer.
  2. The inlineEdit: true arm also needs the smart default (resolveInlineMode: the form-only types, the two-rich-field threshold and the eight-field threshold) promoted to the spec.
  3. It meets this rule's documented posture. The default layout is never a site (creditFieldGroupLayout: only a KEYED section counts), because the platform's default form draws every visible field of every object. The probe's own control pa_order.buyer is drawn by pa_order's default form and reported by design. Crediting the same form when a parent opens it as a row editor makes the verdict depend on which door opens it. That is a decision about the rule's contract, not an omission in this diff.

So the module note and a pinned boundary test state the position: an authored grid in the form factor with no formFields keeps those child fields reported. The enumeration pin's sentence now reads "the form the spec derives, and an authored formFields list the form is offered for". The position goes to a point card the seat files. The report carries the options.

The spec functions against objectui's rule (round 1, unchanged)

The differential ran the spec functions against deriveFormFields and line 847's expression, both read from the pinned files (deriveMasterDetail.ts blob 90aa44c9, MasterDetailForm.tsx blob 7a96a130). The offer expression was evaluated from the source text.

  • deriveInlineRowFormFields: 100,004 cases, 0 mismatches. The cases were objectui's 4 fixtures plus 100,000 random definitions: null and string field definitions, array-shaped fields, non-spec type names, truthy and falsy flags, prototype-ish names, and random relationshipField / exclude.
  • isInlineRowFormOffered: 300,012 cases, 0 mismatches.
  • Subset property: 0 violations. The derived grid is always a subset of the derived form.
  • Lit control: 648 of 2,000 mismatches. The same harness was run against a function that is not the rule, so the harness can fail.

Evidence

The door: os validate --json on a defineStack probe stack. Three heads were measured, each built from source:

  • a7d9768e, the card's base, in a separate worktree;
  • 1d1258a5, the round-1 head;
  • a87f03e1, this head.

All three were run with the same probe file (its filter blocks in the rule-array form). All three exit 0 with valid: true. field-no-consumers findings: 32, 17, 17.

field a7d9768 1d1258a a87f03e position
pa_order_note.order / pa_ticket_line.ticket / pb_case_comment.case_ref / ph_line.header (lookup + inlineEdit) inert — — 1
pb_invoice_line.notes / .config / .frozen, pb_case_comment.body, pb_memo_line.long_note inert — — 2
pc_line.memo (detail formFields, inlineMode: 'form') inert — — 3
pc_header.memo (parent twin) — inert inert 3: was credited in the child's place
pd_line.memo2 (declared grid, 1 field vs 2 columns) inert carrier-only carrier-only 3
pf_line.memo_f (kept as authored, no inlineMode, 1 field vs 2 columns) inert — carrier-only 3, F1
pe_line.qty_e / .note_e / .header / .amt (record:line_items columns and keys) inert — — 4
pe_header.amt (parent twin) — inert inert 4
pk_line.srt_k / .flt_k / .flt2_k (block sort, two blocks' filter) inert inert — 4, flag B
pk_header.srt_k / .flt_k (parent twins) — — inert 4, flag B: were credited in the child's place
pg_line.note_g, ph_line.body_h / .note_h, pi_line.note_i (default form) inert inert inert flag A: not credited, see above
pg_line.ro_g / .hid_g (readonly / hidden) inert inert inert flag A: not drawn either
pc_line.position (detail sortField) inert inert inert no lint read; see notes
pa_order.buyer, pb_invoice_line.secret, pe_line.unused_e, pk_line.unused_k inert inert inert controls

(— means not reported.)

A real producer: examples/app-showcase. There are 52 findings at a7d9768e and 52 at a87f03e1, with identical verdict sets. PR #21244 changed its record:line_items page in between, and that block has no sort or filter.

Tests at a87f03e1 (the head of this PR):

  • pnpm --filter @objectstack/lint exec vitest run: 119 files, 5,572 tests passed. The validate-field-consumers.test.ts file has 126 tests, including the [#21091] block: positions 1 to 4, the flag-A boundary, and the enumeration pin's 13 rows, each paired with a control.
  • pnpm --filter @objectstack/spec exec vitest run --project local: 597 files, 17,483 passed and 1 todo.
  • pnpm --filter @objectstack/cli exec vitest run --project unit: 243 files, 3,439 passed, with the CLI closure built with declarations. The integration tier is declared to CI.
  • pnpm --filter @objectstack/spec --filter @objectstack/lint run typecheck: both exit 0, and check:test-typecheck is OK for both.
  • Filter direction: @objectstack/spec, @objectstack/lint, and the downstream lint consumer @objectstack/cli.

Reverse verification and ablations. Each was committed first, made through scripts/ablation-replace.mjs or a blob restore, and restored to the HEAD blob with git diff HEAD empty. All were predicted red, and all were red.

  • Round 1: the lint source restored to the base blob 3efd1236 failed 29 of 115 tests. The spec row form made to drop readonly failed 2 of 20.
  • Round 2, at a87f03e1, flag B: the panel's sort / filter read switched off failed exactly the 5 flag-B tests (3 tests and 2 pin rows).
  • Round 2, at a87f03e1, F1: the kept-as-authored decision switched off failed exactly the F1 carrier test.

Gates. node scripts/pm/dispatch-gates.mjs --commands derived 8 paths and 86 commands at a87f03e1. Every one was run. --ran reports "86 derived, 86 run, 0 NOT-MEASURED, 0 UNRUN", and all 86 exited 0. check:generated: all 15 artefacts are up to date. The two spec shards gain exactly the two names each.

Base. origin/main moved under generated files three times and was merged each time through scripts/pm/os-regen-merge.sh: at 1d1258a5, ee505255 and 6084ce01. The last merge brought PR #21244's RecordLineItemsProps. No merge owed a regeneration, and the delta against origin/main is exactly this PR's 8 paths. Since then, origin/main has moved by 4 commits, none of which touches a generated artefact or one of the 8 paths.

Acceptance notes

  • Exports. There are two new names, both functions: deriveInlineRowFormFields and isInlineRowFormOffered. No schema accepts or refuses anything new.
  • For the objectui ④ child:
    • deriveFormFields(childSchema, opts) equals deriveInlineRowFormFields(childSchema, opts) on every measured input.
    • Line 847's expression equals isInlineRowFormOffered({ inlineMode: d.inlineMode, formFields: d.formFields, columns: d.columns }).
    • The verdicts are above.
  • sortField (pointer position 3), probe reading. pc_line.position is inert at all three heads. At the pin the renderer only stamps it (GridField.tsx:735). It loads rows with $filter and $top and no ordering, so it never reads the field. objectui 0a3e5409f retired the authored key after the pin, and no lint read was added. The general walk still reads details[].sortField against the parent. That reading leaves with the key at the next .objectui-sha bump.
  • Flag A goes to a point card the seat files. The pin sentence and a boundary test state what this PR covers.
  • Kept as stated: an omitted inlineMode on the DERIVED path (the renderer's smart default), and a derived grid with no relationshipField, both credit an authored list as drawn.
  • "Not in this card" stays out: the explicit form.subforms override, and a subforms entry with no relationshipField.
  • Changeset. @objectstack/spec: minor, because Clause-②: yes takes at least minor. @objectstack/lint: patch follows PR fix(lint,spec): field-no-consumers reads a subform entry's child keys against the child, and credits a derived inline grid through deriveInlineGridColumns #21089 and PR feat(spec)!: an object-master-detail-form block's detail entries are a strict shape whose columns are the inline grid column contract (#20928) #21215. The lint bullets now state the round-2 reads. The rule's message and hint text are unchanged.

Generated by Claude Code

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 14 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/export-origins/data.json, packages/spec/src/data/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/troubleshooting.mdx (via auto_number (literal, a string literal in INLINE_ROW_FORM_NON_INPUT_TYPES))
What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/export-origins/data.json, packages/spec/src/data/index.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 137 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 c7396f1a99bf3ec6358186796a9a880a762a2efd → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 44ea63c259adbd29d820abcdf7959833789ce8a1 — the merge of head 3acb46c64755069bf6db5c62398309c67731158c into base c7396f1a99bf3ec6358186796a9a880a762a2efd, 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 44ea63c259adbd29d820abcdf7959833789ce8a1 && git checkout 44ea63c259adbd29d820abcdf7959833789ce8a1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c7396f1a99bf3ec6358186796a9a880a762a2efd 3acb46c64755069bf6db5c62398309c67731158c && git checkout -B drift-repro c7396f1a99bf3ec6358186796a9a880a762a2efd && git merge --no-ff 3acb46c64755069bf6db5c62398309c67731158c

node scripts/docs-audit/affected-docs.mjs --json c7396f1a99bf3ec6358186796a9a880a762a2efd

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c7396f1a99bf3ec6358186796a9a880a762a2efd → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1d1258a5ad17544f3909426ea5967fe333abfa37
Local-runs: none

Inputs: card #21091 (body and all five comments: the triage grade, the two pointers, the claim, the dev report), PR #21256 (body, the eight-file list, the net diff origin/main... the fetched branch tip, confirmed equal to the head above), the check-runs on the head, and objectui at the .objectui-sha pin (31971ff1e28f, read with git show, nothing checked out). Nothing was built, run or re-run; the differential's counts below are the dev's, judged by reading both sides.

① Derived judgments

The four lint positions, each against the pinned renderer.

  1. Position 1 (join key) — RIGHT. attachInlineSubforms (app-shell MetadataProvider.tsx) builds a subform for a master_detail OR a lookup that sets inlineEdit and carries a reference; MasterDetailForm.tsx loads the rows with $filter: { [relationshipField]: recordId } (1321) and stamps the key on every saved row (552). The lint records a behaviour site at .inlineEdit whenever the field type, inlineEdit and the resolved reference all hold, authored or derived columns alike; inlineEdit: false and a missing reference stay unread. The tests pin all four arms.
  2. Position 2 (derived per-row form) — RIGHT, and credited only through the spec. The lint calls deriveInlineRowFormFields({ fields }, { relationshipField }) and isInlineRowFormOffered({ inlineMode, formFields, columns }); the only local logic is formFactorOf, which reads the declared literal and nothing else — no copy of objectui's rule. The call matches the renderer's derived path: deriveDetail computes formFields = deriveFormFields(childSchema, { relationshipField }) with no exclude, and the config takes d.formFields ?? derived.formFields, d.inlineMode ?? derived.mode. Passing the smart default as undefined is harmless for the reason the dev gives: every type the form skips the grid skips too (checked set against set), so the derived columns are a subset of the derived fields and the offer condition can only add fields the grid already credited. An authored list replaces the derived one (renderer ??): RIGHT. Authored columns draw no derived row form: RIGHT (see flag A in ③ for what the renderer does draw there).
  3. Position 3 (detail entry formFields) — RIGHT in the main, one sub-case wrong (F1 below). The names are read against childObject: the renderer opens the row ObjectForm with objectName: childObject and fields: formFields (1813–1821). With a declared inlineMode, isInlineRowFormOffered decides and a never-offered list is a carrier: RIGHT, it is the line-847 expression. sortField deliberately given no new read: RIGHT — at the pin the grid only stamps it on change (GridField.tsx 735) and the loader orders by nothing (1321–1324), so no renderer reads the field.
  4. Position 4 (record:line_items) — RIGHT. The block's raw properties is read as one panel entry: columns[].name, relationshipField, amountField against childObject; totalField stays on the parent. LineItemsPanel.tsx at the pin: find(schema.childObject, { $filter: { [relationshipField]: parentId } … }) (515–521), columns: schema.columns (702), the sum of amountField written to the parent's totalField (667–669). It imports no column derivation, so "derives nothing" holds. The gate is type plus properties, which is the spec's page component shape (page.zod.ts 324), so no dependency on PR feat(spec)!: record:line_items gets its ComponentPropsMap row, its columns are the inline grid column contract, and the showcase Tasks grid binds by name (#21142) #21244's typed row; the record:details control pins the type gate.
  • Control — RIGHT: the hidden child field and the never-named one stay inert in every row of the enumeration pin and in the probe table.
  • The parent twins the dev reports as newly inert (pc_header.memo, pe_header.amt) are correct verdicts, not regressions: before, details[].formFields and the block's amountField were walked in the parent's context and credited a same-named parent field that the renderer never reads there.
  • Fixture triage of the [#20951] block — RIGHT: the json and readonly child fields are drawn by the derived row form, so DERIVED keeps only the hidden one, and NO_ROW_FORM keeps the old set exactly where no derived row form exists.

F1 — one derived judgment is WRONG as stated (position 3, the omitted-inlineMode boundary). The lint credits an authored formFields list as drawn whenever the entry declares no inlineMode, and the creditAuthoredRowForm docblock, the PR body and the test title say the renderer resolves the omitted mode from the relationship's inlineEdit or the child's shape. At the pin that is true only when the entry lacks an authored relationshipField or authored columns (MasterDetailForm.tsx 1055–1066: inlineMode: d.inlineMode ?? derived.mode). With BOTH authored — the test's own fixture: relationshipField: 'ord', columns: [qty, name], formFields: ['notes'] — the renderer takes the fast path (967, 1048–1052), keeps the config as authored, leaves inlineMode undefined, and line 847 then offers the form only when the form has more fields than the grid has columns: one against two, never. itm.notes is a carrier at the pin; the lint reports it consumed, and the test an omitted inlineMode is the renderer's to resolve: the list is credited as drawn pins that. The sentence is FALSE for that path and the verdict is lenient there. The exact decision needs no copy of any objectui rule: isInlineRowFormOffered({ inlineMode: undefined, formFields, columns }) is the renderer's own expression on that path. Not a false inert, so not the card's class (a) — but a derived judgment this diff asserts and pins wrongly.

The two spec exports.

  • deriveInlineRowFormFields(def: unknown, { relationshipField?, exclude? }): string[] — total (null, undefined, a string, a record with no fields, a non-object fields all answer []; prop() on a primitive answers what the renderer's d?.x answers). Its three literal sets equal SYSTEM_FIELDS, SORT_FIELD_NAMES and NON_INPUT_TYPES in the pinned deriveMasterDetail.ts (blob 90aa44c9, as the dev names it) member for member; it skips system and hidden, keeps readonly, and iterates Object.entries in field order exactly as deriveFormFields does. Restates the pinned rule exactly: RIGHT.
  • isInlineRowFormOffered({ inlineMode?, formFields?, columns? }): boolean — equals the line-847 expression token for token: the mode is form, or the form's field count (absent read as zero) exceeds the grid's column count (absent read as zero). RIGHT.
  • The differential's construction is sound for what it claims: the renderer's own fixtures plus random definitions over the inputs that can diverge (null and string field defs, array fields, non-spec type names, truthy and falsy flags, prototype-ish names, random relationshipField / exclude), a lit control that mismatches (648 of 2,000) so the harness can fail, and the subset property that underwrites the undefined smart default. I did not re-run it; the equality is also evident from the two sources side by side.
  • Docblocks: every sentence of the module note and of both function docblocks is TRUE against the two pinned files, with one loose phrase: inlineMode "is the collection's RESOLVED form factor, as the renderer resolved it" — on the authored-FK-and-columns path the renderer passes an unresolved undefined; the parameter is optional, so the contract holds. Loose, not false.
  • api-surface/data.json and export-origins/data.json: exactly the two names each, both to src/data/inline-grid-columns.ts; nothing else moves. ./data is an exports subpath of @objectstack/spec, so this is published surface and Clause-②: yes is the right declaration.

② Semver level

③ Boundary flags

  • open_questions: none declared.
  • Deviations 1–5: benign. Position 4 was added on a pointer the claim holder posted mid-task, on the same file; the origin/main merge went through the regen helper and the delta against main is exactly the eight paths (checked); the base worktree was a measurement aid, removed; the trailer choice follows AGENTS.md; the restarts were re-run at the head.
  • Finding 3 (details[].sortField still walked in the parent's context): ANSWERED, acceptable as an Acceptance note. The renderer stamps the field and never reads it, so the child verdict is right before and after; the parent-context read is a lenient mis-context with a named carrier (the next .objectui-sha bump retires the key).
  • Flag A — finding 1 (authored inlineColumns with a form factor): MUST BE MEASURED, not left as an Acceptance note with carrier none. By reading at the pin it is a class (a) false inert in this card's own family, inside its enumeration pin's own sentence. attachInlineSubforms emits { childObject, relationshipField, inlineMode: resolveInlineMode(…), columns: inlineColumns } and never a formFields; with FK and columns authored, MasterDetailForm keeps that config (1048–1052), so formFields stays undefined; in form mode line 847 always offers the expand and 851 adds through the form; the expanded ObjectForm receives no fields (1821) and draws Object.keys(objectSchema.fields) (ObjectForm.tsx 961) — every child field. The lint credits only the authored columns and the join key, so every other child field is reported inert while the renderer draws it. The reach is wider than the dev's note: inlineEdit: true lands here whenever the smart default resolves to form — a child with a richtext, json or textarea field, exactly the fields position 2 is about — and so does a subforms or details entry with authored columns and relationshipField, inlineMode: 'form' and no formFields. The card's pin reads "After this card, field-no-consumers reports no inert verdict on … a field drawn by the per-row expand form"; with this shape unmeasured, that sentence is not established, and since the card is the family's one carrier, "noted, not filed" leaves the defect with no carrier the moment the card closes. One probe object on the dev's existing stack measures it.
  • Flag B — finding 2 (record:line_items sort[].field and filter): MUST BE MEASURED. LineItemsPanel applies schema.sort as $orderby and merges schema.filter into the child query (366–379, 516–521), so both name child fields; the walk reads them in the page's (parent) context. A child field read only there is a false inert and a same-named parent field is over-credited — the same mis-context position 4 corrects for the block's sibling keys. One probe block measures it.
  • What a PASS on a later head needs (bounded; same file surface, same card): (i) measure flags A and B with the os validate probe; where the reach is confirmed, either add the read and the pin row here — the card's text makes it the family's carrier — or file point cards and narrow the enumeration pin's sentence to the DERIVED per-row expand form so the closeout claim is true as written; "carrier: none" cannot stand on either. (ii) Correct F1: state the renderer's actual behaviour on the authored-FK-and-columns path in the lint docblock and the PR body, and either decide that path with isInlineRowFormOffered on an undefined inlineMode or pin the lenient verdict under its true description.

Check-runs on the head (polled once a minute until every run concluded; final read 2026-10-01T23:24Z): 35 runs, 32 success, 3 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke — path-filtered or opt-in), 0 failed, 0 still running. Their conclusions are the gate verdicts here; nothing was re-run locally. Green gates do not answer flags A, B or F1: none of the three is a position any derived gate measures.

Implemented-by: claude/issue-21091-field-consumers-inline-closeout
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: FAIL

The four positions, the two exports, the artefact moves and both semver levels are right; the FAIL is on the closeout's completeness (flags A and B, a class (a) candidate and a mis-context read, both unmeasured and carrier-less on the family's closeout card) and on one derived judgment the diff states and pins against what the pinned renderer does (F1).

claude added 2 commits October 1, 2026 23:36
…ffer predicate, and read a record:line_items block's sort and filter against its childObject

The default object form a row opens with no field list stays uncredited, and is pinned as the boundary.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <[email protected]>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

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

Second review of this PR. Inputs: card #21091 (body and all seven comments: the triage grade, the two pointers, the claim, both dev reports and the seat's response to the first record), #21281 (the point card for flag A), PR #21256 (body, the eight-file list, both comments including the round-1 record 5942628181, and the net diff origin/main... the fetched branch tip, confirmed equal to the head above; merge-base 2791138c), the check-runs on the head, and objectui at the .objectui-sha pin 31971ff1e28f read with git show only (MasterDetailForm.tsx, deriveMasterDetail.ts, LineItemsPanel.tsx, ObjectForm.tsx, autoLayout.ts, GridField.tsx, app-shell MetadataProvider.tsx). Nothing was built, run or re-run; every count below that is a run result is the dev's or a check-run's, judged by reading both sides.

① Derived judgments

The round-2 delta. Between the round-1 head 1d1258a5 and this head, the eight paths move in exactly three files: the lint source, its test, and the changeset. The two spec exports, the spec test, index.ts, api-surface/data.json and export-origins/data.json are byte-identical to what round 1 judged.

  • F1 — FIXED, RIGHT. creditAuthoredRowForm now reads keptAsAuthored = relationshipField !== undefined && authoredColumns, counts the grid as the authored list when there is one, else the derived grid when relationshipField is authored, else nothing; decidable = columns !== undefined && (keptAsAuthored || inlineMode !== undefined); and credits display only when !decidable || isInlineRowFormOffered({ inlineMode, formFields, columns }), else carrier. On the kept-as-authored path inlineMode is formFactorOf(entry.inlineMode): the declared literal or undefined, which is exactly what line 847 reads at the pin. The fast path (967) returns the entry's config untouched, and the hydrate path (1048–1052) spreads ...d, so formFields and inlineMode stay as authored and columns keeps the authored count (hydrateColumns maps one to one); 847 then offers the form when d.inlineMode === 'form' or when the form's field count exceeds the grid's column count. The predicate decides that path exactly, with no copy of any objectui rule. The test's own fixture (relationshipField: 'ord', two columns, formFields: ['notes'], no mode) now pins itm.notes as carrier-only, and its sibling (two form fields against one column) pins the offered side with frozen still inert. The derived path (1055–1066) sets inlineMode: d.inlineMode ?? derived.mode; with an omitted mode the renderer runs resolveInlineMode, which the lint does not reproduce (the triage direction forbids the copy), so the list is credited as drawn. That is lenient, never a false inert, and now stated truthfully in the docblock ("a resolution this rule does not reproduce, so with an omitted mode the list is credited as drawn"), in the test title, and in the PR body's two-path paragraph: RIGHT as a stated boundary. With a declared mode on the derived path the predicate decides whenever the grid can be counted: authored columns, or the derived grid when relationshipField is authored (deriveInlineGridColumns is the renderer's deriveColumns restated, PR fix(lint,spec): field-no-consumers reads a subform entry's child keys against the child, and credits a derived inline grid through deriveInlineGridColumns #21089, so the counts agree); without relationshipField the lint keeps no copy of findRelationshipField and credits the list as drawn, which the docblock also states. The docblock, the test titles and the PR body state both paths truly. The changeset's clause is judged in ②.
  • Flag B — MEASURED AND CLOSED, RIGHT. PANEL_CHILD_QUERY_KEYS = { sort, filter }. In walk, for a panel entry those two keys are walked with strName(rec.childObject) as the context and then skipped by continue, so they are read nowhere in the parent's context. sort: [{ field, order }]: field is not a LITERAL_KEYS member, so scanText credits the trimmed value against the context; order is literal. filter in the ViewFilterRule array form [{ field, operator, value }]: field credited, operator and value literal. filter as a field-keyed map: PREDICATE_KEYS has filter, so each key is read against inner, which contextOf leaves at the child context (a rule object names no object). Both forms are tested, and the sort / filter path segments land the sites in the behaviour bucket. Against the pin: LineItemsPanel lowers schema.filter through useResolvedFilter and toFilterNodeSafely and merges it into $filter beside the parent condition (366, 516–520), converts schema.sort to $orderby (368, 377, 521), both on dataSource.find(schema.childObject, ...): child fields, read. Since PR feat(spec)!: record:line_items gets its ComponentPropsMap row, its columns are the inline grid column contract, and the showcase Tasks grid binds by name (#21142) #21244, RecordLineItemsProps.filter is z.array(ViewFilterRuleSchema) and sort is z.array(SortItemSchema); the lint reads either filter form, and the docblock's sentence that the contract declares the array while the panel also lowers the map is TRUE (panel prop docs 107–118). One loose phrase: "this rule reads whichever is authored" does not hold for the $and / $or AST wrappers the panel also lowers, which the lint does not walk field-wise; the contract refuses that shape, so no contract-valid stack is affected. Pin rows 12 and 13 (sort[].field, filter[].field) are RIGHT, and the probe's new inert verdicts on the parent twins (pk_header.srt_k, pk_header.flt_k) are correct: those fields were credited in the child's place before.
  • Flag A — CONFIRMED, NARROWED, NOT CLOSED HERE; carrier finding(lint): field-no-consumers reports as inert the child fields that the default record form of an inline grid's row draws, when an authored grid opens that form with no field list #21281. The narrowed sentence and the boundary statement are TRUE. At the pin attachInlineSubforms (MetadataProvider 503–531) emits { childObject, relationshipField, inlineMode: resolveInlineMode(...), columns: inlineColumns when an array } and never formFields; with FK and columns authored, MasterDetailForm keeps that config (967 or 1048–1052), so formFields stays undefined; in form mode 847 always offers the expand; 1821 passes no fields; ObjectForm 961 draws Object.keys(objectSchema.fields) through filterSystemFields (autoLayout 231: server-owned names, hidden and readonly dropped). The module note's boundary paragraph ("a row form opened with NO field list ... the renderer then opens the child's default object form, which draws every visible field the way the child's own create and edit forms do ... No spec derivation states that form's field set ... whether it counts here is not decided by this rule") is TRUE sentence by sentence, and the boundary test pins both arms (authored inlineColumns with inlineEdit: form; a kept-as-authored detail entry with inlineMode: form and no formFields) at GRID_ONLY. The enumeration pin's narrowed sentence, "the form the spec derives (deriveInlineRowFormFields) and an authored formFields list the form is offered for, not a form opened with no field list", is TRUE: every row of the pin is one of those two classes, and the lint reports no inert verdict on either (an authored list is display when offered or undecidable and carrier-only only where the predicate is exact). The reach is confirmed by the dev's three-head probe (four drawn fields inert at a7d9768e, 1d1258a5 and a87f03e1; the readonly and hidden controls inert and not drawn, matching filterSystemFields). This is the second arm of the round-1 PASS condition ("file point cards and narrow the enumeration pin's sentence"). finding(lint): field-no-consumers reports as inert the child fields that the default record form of an inline grid's row draws, when an authored grid opens that form with no field list #21281 is open, names this PR, this card, the measured reach and the A/B choice, and is filed bare for triage. The closeout may stand with finding(lint): field-no-consumers reports as inert the child fields that the default record form of an inline grid's row draws, when an authored grid opens that form with no field list #21281 as the carrier. One hygiene note for the seat, not a condition: the card body's own pin sentence ("a field drawn by the per-row expand form") stays broader than the test's; the closing comment on 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 should quote the narrowed sentence and name finding(lint): field-no-consumers reports as inert the child fields that the default record form of an inline grid's row draws, when an authored grid opens that form with no field list #21281.
  • Base merges — RIGHT. ee505255^2 = 30c530e5, 6084ce01^2 = 2791138c (the PR's base sha); each merge's combined diff is empty, so no conflict resolution carried content; the three-way diff against origin/main is exactly the eight paths (+824 / -24). origin/main has since moved by six commits (to c7396f1a) touching none of the eight. The second merge brought RecordLineItemsProps, and the flag-B tests moved to the rule-array form the contract declares: the right response.

Round-1 RIGHT judgments, re-confirmed on this head. Carried on the evidence that the spec-side files are byte-identical to 1d1258a5, and re-read where the lint source moved:

  1. Position 1 (join key) — RIGHT. walkObject records a behaviour site at .inlineEdit for a lookup or master_detail with a truthy inlineEdit and a resolved reference, authored or derived columns alike, and credits the derived grid plus row form only without authored inlineColumns. Pin: attachInlineSubforms takes both types (503–531); the loader filters on the key (1321) and the save stamps it (552). The five tests pin all arms.
  2. Position 2 (derived row form) — RIGHT, credited only through the spec. creditDerivedRowForm calls deriveInlineRowFormFields({ fields }, { relationshipField }) and isInlineRowFormOffered({ inlineMode, formFields, columns }); formFactorOf reads the declared literal and nothing else. The renderer's derived path computes deriveFormFields(childSchema, { relationshipField }) with no exclude and takes d.formFields ?? derived.formFields; the subset property (every type the form skips the grid skips) makes the undefined smart default harmless. An authored list replaces the derivation (rowForm = undefined when formFields is an array).
  3. Position 3 (authored formFields) — RIGHT, with F1 now closed. Read against childObject (ObjectForm opened with objectName: childObject, fields: formFields, 1813–1821). sortField deliberately given no read: GridField 735 stamps it on reorder, the loader orders by nothing (1321–1324).
  4. Position 4 (record:line_items) — RIGHT. The block's raw properties is one panel entry: columns[].name, relationshipField, amountField against childObject (516, 702, 669), totalField on the parent; it derives nothing and offers no row form (the landed row's own guidance says formFields and inlineMode are not read). The type gate is type plus properties; the record:details control pins it. The keys the lint reads are exactly the keys RecordLineItemsProps declares.
  5. The two spec exports — RIGHT, unchanged. deriveInlineRowFormFields restates deriveFormFields (same skip sets by reference to the grid's, INLINE_ROW_FORM_NON_INPUT_TYPES equals NON_INPUT_TYPES member for member, skips system and hidden, keeps readonly, Object.entries order). isInlineRowFormOffered equals line 847 token for token. Both total on malformed input.
  6. Artefact moves — RIGHT, unchanged: two names each in api-surface/data.json and export-origins/data.json, both to src/data/inline-grid-columns.ts. ./data is an exports subpath, so published surface; Clause-②: yes is the right declaration.
  7. Fixture triage of the [#20951] block (DERIVED keeps only the hidden field; NO_ROW_FORM where no derived row form exists) — RIGHT, unchanged.
  8. Controls — RIGHT: itm.secret (hidden) stays inert beside all 13 pin rows, and the probe's four controls stay inert at all three heads.

② Semver level

  • @objectstack/spec: minor — two additive function exports on a published subpath; Clause-②: yes takes at least minor; the changeset's Clause-②: yes (widening) is one arm, well-formed, and consistent with the PR body's Clause-②: yes. RIGHT, unchanged.
  • @objectstack/lint: patch — a verdict correction at an authoring door with no exported API change, the level PR fix(lint,spec): field-no-consumers reads a subform entry's child keys against the child, and credits a derived inline grid through deriveInlineGridColumns #21089 and PR feat(spec)!: an object-master-detail-form block's detail entries are a strict shape whose columns are the inline grid column contract (#20928) #21215 took. RIGHT.
  • Check Changeset is green on the head. No governed path is among the eight; Governed Surface Queue Guard is green.
  • Changeset prose, the round-2 delta (two bullets edited): bullet 4 (sort or filter read against the block's childObject) is TRUE. Bullet 3's carrier clause, "When the form is never offered for the list — under a declared inlineMode, or, on an entry that names both its relationshipField and its columns, under no inlineMode at all — the list is reported as a carrier", is TRUE for the kept-as-authored arm and over-broad in one corner of the declared-mode arm: a derived entry with inlineMode: 'grid', no relationshipField and no columns is credited as drawn even where the renderer never offers the form, because the lint cannot count the derived grid there. The lint docblock states that corner truthfully; the changeset does not carve it out. The round-1 wording had the same breadth and the round-1 record passed it, so this is not a round-2 regression, and the changeset is a prose face outside this review's three faces (the dispatch seat checks it at ACCEPT). Noted for the seat: adding "where the grid can be counted" to that clause makes it exact. Every other sentence is unchanged since round 1 and TRUE as re-read: the skip sets, the kept readonly and rich types, the subset claim, the offer condition, "no schema accepts anything new or refuses anything new", the parent-twin sentence and the still-reported hidden field.

③ Boundary flags

Check-runs on the head (final read 2026-10-02T01:29Z): 42 runs, all completed: 37 success, 5 skipped (Auto Label and Check PR Size on the PR-edit re-run, Console Pin Gate, Build Docs, Packed-tarball smoke — event-conditioned, path-filtered or opt-in), 0 failed, 0 still running. Their conclusions are the gate verdicts here; nothing was re-run locally. The green gates do not by themselves answer F1, flag A or flag B; those are judged above by reading.

Implemented-by: claude/issue-21091-field-consumers-inline-closeout
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

The three round-1 failure points are resolved on this head: F1 is decided exactly by the renderer's own predicate and stated truthfully on both paths; flag B is measured, read against the child in both filter forms and pinned; flag A is measured, confirmed, bounded in the module note and a pinned test, carved out of the enumeration pin's sentence, and carried by #21281. The four positions, the two exports, the artefact moves and both semver levels stand as judged in round 1. Notes for the seat, none a condition: the changeset's declared-mode carrier clause could be narrowed to "where the grid can be counted"; the card's closing comment should quote the narrowed pin sentence and name #21281; the detail entry's inlineMode describe in the spec carries the same loose sentence F1 corrected in the lint.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3acb46c64755069bf6db5c62398309c67731158c
Local-runs: none

Third review of this PR. Inputs: card #21091 (body and all eight comments: the triage grade, the two pointers, the claim, the three dev reports and the seat's response to the first record), PR #21256 (body, the eight-file list, its three comments: the docs-drift advisory 5942351159 and the two earlier records, 5942628181 FAIL on 1d1258a5 and 5943918881 PASS on a87f03e1; and the net diff origin/main... the fetched branch tip, confirmed equal to the head above; merge-base 2791138c), #21281 (flag A's carrier, read for its state), and the check-runs on the head. Nothing was built, run or re-run: git fetch, git show and git diff on the fetched refs, and REST reads, only.

① Derived judgments

The round-3 delta is the changeset alone: RIGHT, as the dev states. git diff a87f03e1 3acb46c6 names one path, .changeset/21091-inline-row-form-join-key.md (+2 / -2, blob 4b0ac31243 to 2348612b2d), carried by one commit (3acb46c6, single parent a87f03e1, so no base merge). The other seven paths carry byte-identical blobs on both heads: packages/lint/src/validate-field-consumers.ts 0feea678ab, packages/lint/src/validate-field-consumers.test.ts 0bffff8e49, packages/spec/src/data/inline-grid-columns.ts 4c0884e40d, packages/spec/src/data/inline-grid-columns.test.ts 99ba1774aa, packages/spec/src/data/index.ts e51f4fc256, packages/spec/api-surface/data.json 91354442d8, packages/spec/export-origins/data.json 595920d9bf. No source, test or generated file moved. origin/main has moved by six commits since the merge-base 2791138c (to c7396f1a), none touching any of the eight paths, so the three-way diff against origin/main is still exactly the eight paths (+824 / -24).

The two edited sentences, each against the code on this head.

  1. Bullet 3's carrier clause: TRUE on every path. The clause now reads: when the form is never offered for the list, the list is a carrier; that is judged on an entry that names both its relationshipField and its columns under its declared inlineMode or none; on any other entry it is judged under a declared inlineMode where the grid can be counted (authored columns, or the derived grid of a named relationshipField); otherwise the list is credited as drawn. creditAuthoredRowForm (packages/lint/src/validate-field-consumers.ts, blob 0feea678ab) computes inlineMode = formFactorOf(entry.inlineMode) (the declared grid / form literal, else undefined), keptAsAuthored = relationshipField !== undefined && authoredColumns where hasAuthoredColumns requires a non-empty array, columns = the authored array when there is one, else deriveInlineGridColumns on the child's field map when relationshipField is named and the child resolves, else undefined; decidable = columns !== undefined && (keptAsAuthored || inlineMode !== undefined); offered = !decidable || isInlineRowFormOffered({ inlineMode, formFields, columns }); each declared name is recorded display when offered, carrier otherwise. Arm by arm: a kept-as-authored entry always has countable columns, so it is decided under its declared mode or undefined, exactly the clause's first sentence; any other entry is decided only when columns is countable AND a mode is declared, and columns is countable exactly in the two cases the clause names; everything else is offered by the !decidable arm, the clause's "otherwise". The one path the words do not spell (a named relationshipField whose childObject the ledger does not know) records nothing at all, because ledger.declares(childObject, field) is false and the name counts as unresolved, so the clause is not falsified there. The clause now matches the lint docblock's two-path paragraph and the PR body's position-3 text, and the over-broad corner the second record named (a derived entry with inlineMode: 'grid', no relationshipField, no columns, credited as drawn) is now inside "otherwise the list is credited as drawn" rather than inside the carrier arm.
  2. The computed-types parenthetical: TRUE. INLINE_ROW_FORM_NON_INPUT_TYPES (packages/spec/src/data/inline-grid-columns.ts, blob 4c0884e40d) is new Set(['formula', 'summary', 'rollup', 'autonumber', 'auto_number']); the changeset now lists those five names and no other. The PR body's position-2 text already listed auto_number, so the two prose faces now agree with the code.

Carried from the second record (5943918881), on identical blobs. Every judgment below is a reading of a file whose blob id is unchanged between a87f03e1 and this head, so there is nothing to re-judge; each stands as RIGHT. (1) Position 1, the inline relationship's join key, read as a behaviour site at .inlineEdit for lookup as for master_detail. (2) Position 2, the derived per-row expand form credited only through deriveInlineRowFormFields and isInlineRowFormOffered, with the derived grid a subset of the derived form. (3) Position 3, an authored formFields list read against childObject, with F1 decided exactly by the renderer's own predicate on the kept-as-authored path and the derived path's omitted mode stated as a lenient boundary; sortField deliberately given no read. (4) Position 4, a record:line_items block's columns[].name, relationshipField, amountField, sort and filter read against childObject, totalField on the parent, both filter forms read (flag B closed). (5) The two spec exports, total on malformed input and equal to the pinned renderer's deriveFormFields and line 847 member for member and token for token. (6) The artefact moves: two names each in api-surface/data.json and export-origins/data.json, both to src/data/inline-grid-columns.ts; ./data is an exports subpath, so published surface. (7) The [#20951] fixture triage. (8) The controls: the hidden child field and the never-named one stay inert beside all 13 pin rows. Flag A: confirmed, bounded in the module note and a pinned test, carved out of the enumeration pin's sentence, carried by #21281.

Accept-set and public-surface changes this diff implies. No Zod schema moves on any head of this PR, so no accept set changes: the changeset's "no schema accepts anything new or refuses anything new" is TRUE. The public surface gains exactly two function exports on @objectstack/spec/data, unchanged since round 1; @objectstack/lint exports nothing new and changes verdicts only. RIGHT.

② Semver level

  • @objectstack/spec: minor: two additive function exports on a published exports subpath; Clause-②: yes takes at least minor. RIGHT, unchanged.
  • @objectstack/lint: patch: a verdict correction at an authoring door with no exported API change, the level PR fix(lint,spec): field-no-consumers reads a subform entry's child keys against the child, and credits a derived inline grid through deriveInlineGridColumns #21089 and PR feat(spec)!: an object-master-detail-form block's detail entries are a strict shape whose columns are the inline grid column contract (#20928) #21215 took. RIGHT, unchanged.
  • The edited changeset still carries Clause-②: yes (widening): one arm from the closed pair, well-formed, and consistent with the PR body's Clause-②: yes. The (widening) arm is the right one: the published surface only grows (two names added, none removed or renamed, no schema narrowed), so no migration text and no ADR-0087 disposition marker is owed, and the body carries none. RIGHT.
  • Changeset prose on this head, sentence by sentence: the two edited sentences are TRUE as judged in ①; every other sentence is byte-identical to the a87f03e1 text the second record read and is TRUE as read there (the skip sets, the kept readonly and rich types, the subset claim, the offer condition, the four lint bullets' remaining text, the parent-twin sentence and the still-reported hidden field). The corner the second record noted as over-broad is closed by this round's edit; no new corner was opened.
  • Check Changeset is success on this head. No governed path is among the eight; Governed Surface Queue Guard is success.

③ Boundary flags

  • Round-3 dev report 5943952949: open_questions empty, out_of_scope_findings empty, deviations empty. Its two factual claims about this round are TRUE: the delta is the changeset's prose only (checked above), and there was no base merge (one commit, one parent; GitHub reports the PR mergeable).
  • Round-2 open_questions (one, flag A's A/B choice): ESCALATED to finding(lint): field-no-consumers reports as inert the child fields that the default record form of an inline grid's row draws, when an authored grid opens that form with no field list #21281, which is open, names this PR and this card, and awaits triage (no labels yet). Nothing in this PR depends on the answer; the boundary is stated in the module note and pinned either way. Unchanged.
  • Round-1 finding 3 (details[].sortField still walked in the parent's context): unchanged, still acceptable as an Acceptance note; the renderer only stamps the field, and the carrier is the next .objectui-sha bump, where objectui has retired the key.
  • The second record's three notes for the seat: (a) narrow the changeset's carrier clause: DONE this round, judged TRUE in ①. (b) The card's closing comment should quote the narrowed pin sentence and name finding(lint): field-no-consumers reports as inert the child fields that the default record form of an inline grid's row draws, when an authored grid opens that form with no field list #21281: still for the seat at close; not a condition. (c) The detail entry's inlineMode describe in packages/spec/src/ui/component.zod.ts (line 5010 on this head, "Resolved from the relationship field's inlineEdit when omitted") still carries the sentence F1 corrected in the lint; it is outside this diff and unchanged; the seat's call, not a condition.
  • The docs-drift advisory 5942351159 names content/docs/deployment/troubleshooting.mdx through the auto_number literal. That page's only hit is a legacy-spelling table row mapping auto_number to autonumber; it says nothing about the inline row form and is not falsified by this diff. Advisory, no action.
  • Landing posture: the PR is a draft with no auto-merge armed; no needs:contract-review label is on it, so there is none to lift. Head repo equals base repo.

Check-runs on the head (polled from the first read at 2026-10-02T01:38Z until every run concluded; final read 2026-10-02T01:59Z): 35 runs, all completed: 32 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in): path-filtered or opt-in, as on the two earlier heads), 0 failed, 0 still running. Check Changeset, Governed Surface Queue Guard, Lint & Repo Gates, all six Test Core shards and their rollup, and every Type Check job are success. Their conclusions are the gate verdicts here; nothing was re-run locally. The green gates do not by themselves judge the two edited sentences; those are judged above by reading.

Implemented-by: claude/issue-21091-field-consumers-inline-closeout
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

The round-3 delta is two sentences of changeset prose, both now TRUE against the code on this head; the seven other paths are byte-identical to the head the second record passed, so its ①②③ carry unchanged; both semver levels and the Clause-②: yes (widening) arm stand. No flag is open on this head: the one escalation (flag A) has its carrier in #21281.

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:data size/l tests tooling

Projects

None yet

2 participants