Repository navigation
Commit dcc5ef4
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 #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 #21089 and
PR #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>
1 parent 4b09689 commit dcc5ef4
8 files changed
Lines changed: 824 additions & 24 deletions
File tree
- .changeset
- packages
- lint/src
- spec
- api-surface
- export-origins
- src/data
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
803 | 803 | | |
804 | 804 | | |
805 | 805 | | |
| 806 | + | |
806 | 807 | | |
807 | 808 | | |
808 | 809 | | |
| |||
852 | 853 | | |
853 | 854 | | |
854 | 855 | | |
| 856 | + | |
855 | 857 | | |
856 | 858 | | |
857 | 859 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
790 | 790 | | |
791 | 791 | | |
792 | 792 | | |
| 793 | + | |
793 | 794 | | |
794 | 795 | | |
795 | 796 | | |
| |||
839 | 840 | | |
840 | 841 | | |
841 | 842 | | |
| 843 | + | |
842 | 844 | | |
843 | 845 | | |
844 | 846 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
306 | 306 | | |
307 | 307 | | |
308 | 308 | | |
309 | | - | |
310 | | - | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
311 | 313 | | |
312 | 314 | | |
313 | 315 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
5 | 10 | | |
6 | 11 | | |
7 | 12 | | |
| |||
166 | 171 | | |
167 | 172 | | |
168 | 173 | | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| |||
54 | 55 | | |
55 | 56 | | |
56 | 57 | | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
57 | 87 | | |
58 | 88 | | |
59 | 89 | | |
| |||
209 | 239 | | |
210 | 240 | | |
211 | 241 | | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
0 commit comments