Repository navigation
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
Conversation
…child reads and derived grids Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
…umns Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
…neGridColumns Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
…t, as the renderer does Claude-Session: https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 44 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 137 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 4606d21259ea882b1bdab260f877b00ff104f1b2 && git checkout 4606d21259ea882b1bdab260f877b00ff104f1b2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 94608a7d72ecef7bf61d10dbab3bd80aea311055 51357074754f69de9685e703590696baf67f3de2 && git checkout -B drift-repro 94608a7d72ecef7bf61d10dbab3bd80aea311055 && git merge --no-ff 51357074754f69de9685e703590696baf67f3de2
node scripts/docs-audit/affected-docs.mjs --json 94608a7d72ecef7bf61d10dbab3bd80aea311055
|
Contract reviewServed-tier: Inputs: card #20951 (body and comments 5920525534, 5921156625, 5924389801, 5925562923), PR #21089 (body, 8-file list, net diff against ① Derived judgments
② Semver level
③ Boundary flags
Triage's ⛔ in 5921156625 (record any difference from objectui's rule; never change the renderer silently). Answer: the rule is reproduced exactly by inspection (①.2) and the renderer is untouched. The one difference the dev records (a null field definition under objectui's Check-runs on the head, read 2026-10-01T05:56Z: 32 runs, 17 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke opt-in), 0 failure, 12 in progress (Dogfood Regression Gate 1/3 and 2/3, Lint & Repo Gates, Temporal Conformance, Test Core 1/6 through 6/6, Type Check consumer gates, Type Check workspace). No red at my reading; the seat re-reads every check before landing. Implemented-by: VERDICT: PASS Generated by Claude Code |
…n key, its per-row expand form, a detail entry's formFields and a record:line_items block against the child (objectstack-ai#21091) (objectstack-ai#21256) Fixes objectstack-ai#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 objectstack-ai#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 objectstack-ai#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 `[objectstack-ai#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 objectstack-ai#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 `[objectstack-ai#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 objectstack-ai#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 objectstack-ai#21089 and PR objectstack-ai#21215. The lint bullets now state the round-2 reads. The rule's message and hint text are unchanged. --- _Generated by [Claude Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20951
Clause-②: yes (widening)
What changes
field-no-consumers(packages/lint/src/validate-field-consumers.ts) called two kinds of in-use child field "inert". Both are corrected here, and the second goes through one new derivation the spec owns.Site 1: a
subformsentry's child-field keys are read per key.amountField("Numeric child column summed for the running total") is now read against the entry'schildObject.totalField("Parent field to receive the rolled-up sum") stays on the parent, which is the context the walk already carries. The read uses the child resolution that PR #20950 added forsubforms[].columns:strName(rec.childObject)in theCHILD_COLLECTION_KEYSbranch ofwalk. There is no second child-object lookup. The generic walk now skips these keys on an entry, so a same-named parent field is no longer credited in the child's place.relationshipField("FK on the child pointing back to the parent") gets the same per-key read. This is a bounded in-place fix: the same defect class asamountField, in the same file, with the same gate family. Evidence is in the probe table below (pr_quote_line.quote). The renderer loads the child rows with$filteron this key and stamps it on save, so the field is read.Site 2: a derived inline grid credits the columns it draws. The new
deriveInlineGridColumnslives inpackages/spec/src/data/inline-grid-columns.ts, besidederiveFieldGroupLayout, and is exported through the@objectstack/spec/databarrel. The lint credits exactly what it returns,defaultHiddenoverflow included, because those columns are collapsed into the column chooser and never dropped. Two carriers trigger it:inlineEdit(true,'grid'or'form'; both modes pass the samecolumnsto the grid),typemaster_detailorlookup, a target that resolves, and no authoredinlineColumns(absent or empty). These are the conditions objectui'sattachInlineSubformschecks;subformsentry with nocolumns(absent or empty). The spec documents this second carrier with the same words, "derived from the child object when omitted", and the renderer uses the same derivation for it.The spec function and objectui's rule (triage's ⛔, PM hypothesis H2)
Signature:
deriveInlineGridColumns(def: unknown, opts?: { relationshipField?: string; exclude?: readonly string[]; maxColumns?: number }): DerivedInlineGridColumn[].DerivedInlineGridColumnis{ name: string; defaultHidden?: true }, which is a valid identity-onlyinlineColumnsentry.DEFAULT_MAX_INLINE_GRID_COLUMNSis6. Import path:@objectstack/spec/data. The input discipline matchesderiveFieldGroupLayout: it takes the child object's definition and tolerates un-parsed input.The rule, as measured in objectui
mainatbe5211522412(packages/plugin-form/src/deriveMasterDetail.ts,deriveColumnspluscurateColumns, read over REST):id,_id,recordId,created_at/updated_at/created_by/updated_byand their camelCase forms,organization_id,tenant_id,space,owner);position,sort_order,sequence,line_no,line_number,sort);exclude;system,readonlyorhidden(a truthy value is enough);formula,summary,rollup,autonumber,auto_number,json,object,grid,table,location,vector,html,markdownorrichtext.defaultHidden.maxColumnsof0or less marks no column hidden.The differential: 80,004 cases and 0 mismatches. I ran the spec function against objectui's
deriveColumns, imported from thatmainfile. The cases were objectui's 4 own fixtures, 50,000 random definitions and 30,000 wide definitions built to exercise the budget (29,075 of them produceddefaultHiddencolumns). The random inputs included null field definitions, array-shapedfields, non-spec type names, truthy and falsy flag values, CEL-envelope expressions, andNaN, negative and absentmaxColumns. Names, order anddefaultHiddenmatched in every case. So the spec function reproduces the rule with no behaviour change. objectui's renderer is not touched here.For objectui's switch (the coordination child; not in this PR):
hydrateColumns(deriveInlineGridColumns(schema, opts), schema)equalsderiveColumns(schema, opts)in every case but one kind. When a derived field's own definition is falsy (null),hydrateColumnsreturns the bare{ name }wherederiveColumnsbuilds a text column labelled with the name. That covered 3,889 of the cases, all of that kind. A served schema never carries a null field definition. Still, keeping objectui's own per-column builder over the returned names makes the switch exact by construction.Evidence
The door:
os validate --json, CLI from this branch's source, before vs after. The probe stack is adefineStackapp with four parent/child pairs. "Before" rebuilt@objectstack/lintfrom the base commit's source;ablation-dist-preflight --absentconfirmed the change was gone fromdist/. Both runs exit 0 withvalid: true.pr_invoice_line.line_totalamountFieldpr_invoice.line_total(unused parent twin)pr_invoice_line.total(unused child twin)totalFieldstays on the parentpr_invoice_line.memopr_quote_line.quote(alookupFK)relationshipField, per keypr_order_item.sku,.quantitypr_ticket_note.bodylookuprelationshippr_order_item.secret(hidden)A real producer:
examples/app-showcase, the same door, before vs after. The finding count went from 57 to 54, and no finding was added. The three removed findings areshowcase_expense_line.category,.incurred_atand.incurred_on, which werecarrier-onlybefore.showcase_expense_line.expense_reportsetsinlineEdit: 'grid'with noinlineColumns.examples/app-crmopportunity_line_item.opportunityhas the same shape (I read it; I did not run it).Tests (final HEAD
513570747):pnpm --filter @objectstack/lint exec vitest run: 118 files, 5,467 tests passed. This includes the new[#20951]block invalidate-field-consumers.test.ts(15 tests) and the unchanged[#20929]block.pnpm --filter @objectstack/spec exec vitest run --project local: 585 files, 17,222 passed and 1 todo. That run was atbf01c7979; the only later commit is a one-line lint change, and the newinline-grid-columns.test.ts(11 tests) was re-run at513570747.pnpm --filter @objectstack/spec --filter @objectstack/lint run typecheck: both exit 0, andcheck:test-typecheckis OK for both.deriveInlineGridColumnscompiles only against the rebuilt.d.ts, because the name does not exist in the base build.pnpm --filter @objectstack/cli exec vitest run --project unit: 238 files and 3,391 tests passed. 2 files (10 tests) are NOT MEASURED; see below.Reverse verification. The fix was committed first. Then
validate-field-consumers.tswas restored to the base blob4c109d4ef. With that source, 11 of the 15 new tests fail, and the 4 baselines and controls pass. The restore went throughgit checkout HEAD --and was checked by blob hash (2ef0118a7, then equal to HEAD);git statuswas clean afterwards.Gates.
node scripts/pm/dispatch-gates.mjs --commandswas derived from this diff (8 paths, 86 commands) and every command was run at513570747.--ranreports "86 derived, 84 run, 2 NOT-MEASURED, 0 UNRUN". 83 exited 0, includingcheck:generated(all 15 artefacts up to date aftergen:api-surfaceandgen:export-origins),check:api-surface,check:export-origins,check:entry-nameability,check:dual-source-exports,check:spec-changes(insidecheck:generated),check:nul-bytesandcheck:engine-double-contract. The rest are NOT MEASURED, listed below.NOT MEASURED (none of these are a verdict on this diff):
check:dts-closureexited 1. It names 55 packages with missing.d.ts. This tree built those packages withOS_SKIP_DTS=1, only so thatos validatecould run from source.spec,lint,formulaandsdui-parserhad full builds and are not named.check:dual-build-cjs-loadsandcheck:type-check-debtexited 3 withPREREQUISITE NOT MET: they need the whole workspace built with declarations.published-subpath-console.pin.test.tsandpublished-subpath-hook-body.pin.test.ts(10 tests) fail withENOENTonpackages/cli/dist/*.d.ts. That is the same JS-only build.integrationtier andpackages/qa/dogfood(an importer offield-group-layout, whose bytes do not change) are left to CI.main. Since the base, 14 commits have landed there, and none of them touches the 8 paths in this diff.Acceptance notes
6. No accept set moves.subformsentries with nocolumns, andrelationshipFieldis read per key. Both are named above.subformsentry that names norelationshipField. The renderer detects the FK itself. The lint keeps no copy of that detection, so the derived list it credits includes the FK. The FK is read anyway, as the join key, so the verdict is unchanged.form.subformsentry for the same child, objectui draws that entry instead of the field-derived grid. The lint still credits the field-derived grid, which matches how PR fix(lint): field-no-consumers reads an inline grid column name as a field of the child object #20950 already treatsinlineColumns. This is an over-credit in that case only.lookuprelationship field that setsinlineEditis the inline grid's join key, yetpr_ticket_note.ticketis still reportedinert.master_detailis exempt;lookupis not.deriveFormFields: rich text, JSON,readonly) are still reported.pr_order_item.spec_sheet, arichtextfield, is reportedinert, while the grid offers the expand form because the child has more form fields than grid columns.Generated by Claude Code