Repository navigation
Commit 3693a1b
fix(lint): field-no-consumers reads an inline grid column name as a field of the child object (#20950)
Closes #20929
Clause-②: no (a lint verdict changes; no key is added to a published
payload, read against the gate's definition in
`scripts/check-changeset-no-major.mjs`)
## What changes
`field-no-consumers` (`packages/lint/src/validate-field-consumers.ts`)
now reads an inline grid column's `name` as a reference to the **child**
object's field. `name` stays in `LITERAL_KEYS`: it is not dropped
wholesale. One helper, `creditInlineGridColumns`, reads the column
position on its own, against the child object each carrier names:
| carrier | child object | site kind |
|:--|:--|:--|
| a relationship field's `inlineColumns` | the object that **declares**
the field | `display` when the field sets `inlineEdit`, otherwise
`carrier` |
| `form.subforms[].columns`, and the same under each `formViews` entry |
the entry's `childObject` | `display` |
The subform carrier is keyed by `CHILD_COLLECTION_KEYS` (today
`subforms`): its entries are `{ childObject, columns }`. The finding
message now also lists an inline grid column among the consumers, and an
`inlineColumns` entry on a field without `inlineEdit` among the
carriers.
## One correction to the claim: for `inlineColumns` the child is the
declaring object, not the related one
The claim's scope line glossed the `inlineColumns` child as "the related
one"; the seat corrected it in place (ruling `5920513410` on #20929).
The spec, its own peer check, the renderer and the one real producer all
say the opposite, so this PR follows the triage ruling's intent ("a
reference to the **child** object's field"):
- `FieldSchema.inlineEdit` (`packages/spec/src/data/field.zod.ts`): "On
a child's `master_detail`/`lookup` field (whose `reference` is the
parent object)".
- `collectHydratedInlineColumnErrors`
(`packages/spec/src/stack.zod.ts`): "a relationship field's
`inlineColumns` — the field sits on the CHILD object, so a column names
a field of the object that owns the field".
- objectui `attachInlineSubforms`
(`packages/app-shell/src/providers/MetadataProvider.tsx`) turns a
field's `inlineColumns` into a subform with `childObject: child.name`,
the declaring object, on the form of the field's `reference`, the
parent.
- The showcase invoice
(`examples/app-showcase/src/data/objects/invoice.object.ts`) puts
`inlineColumns` on `showcase_invoice_line.invoice` (`reference:
'showcase_invoice'`), and all seven columns are fields of
`showcase_invoice_line`.
The `inlineColumns` pin gives the related (parent) object a same-named
field that nothing reads and holds it reported. Crediting the related
object turns that pin red (ablation A3a below).
## Why `inlineEdit` gates the relationship carrier
The spec's own form help text (`packages/spec/src/data/field.form.ts`)
says `inlineColumns` is "used only when this field sets inlineEdit", and
objectui skips a field whose `inlineEdit` is falsy. Without it, the
columns name the field and draw nothing. They are recorded as a carrier:
the field reads `carrier-only`, with the column path listed as a site a
removal must clean. That is the rule's existing taxonomy ("credits
exactly what a renderer draws"), and it is pinned and ablated (A4).
## Measured at the public door: `os validate --json`, before and after
The probe is one parent (`gc_invoice`) and four children. On each child,
`quantity` and `amount` are named only by grid columns, and `memo` is
named nowhere (the control). The CLI ran from source
(`packages/cli/bin/run-dev.js`), with the validate command's dependency
closure built at each tree.
| child | carrier | BEFORE at `1571aedce5` | AFTER at `f849aa53f6` |
|:--|:--|:--|:--|
| `gc_line_inline` | `invoice.inlineColumns` with `inlineEdit: 'grid'` |
quantity, amount, memo: inert | memo: inert |
| `gc_line_noedit` | `invoice.inlineColumns`, no `inlineEdit` |
quantity, amount, memo: inert | quantity, amount: carrier-only (each
lists its `inlineColumns[i].name` path); memo: inert |
| `gc_line_form` | `form.subforms[0].columns` | quantity, amount, memo:
inert | memo: inert |
| `gc_line_formview` | `formViews.edit.subforms[0].columns` | quantity,
amount, memo: inert | memo: inert |
Both runs exit 0 with `valid: true`. `field-no-consumers` findings go
from 12 to 6. The card had not measured the relationship field's
`inlineColumns` carrier, and it had the same blind spot (first row,
BEFORE). The AFTER reading was first taken at `7ee5c56679` and repeated
at the merged head `f849aa53f6`, with identical findings.
## Pins and ablations
A new `describe` block in `validate-field-consumers.test.ts` uses one
fixture. The parent is `inv` and the child is `line`, related by
`line.invoice`. `line.qty` is named only by the column under test.
`line.memo` is named nowhere (the control). `inv.qty` is a same-named
parent field that nothing reads, so a column credited to the wrong
object shows up as `inv.qty` going quiet.
The pins:
- baseline: with no grid, all three fields are inert;
- `inlineColumns` with `inlineEdit`;
- `form.subforms[].columns`;
- `formViews.edit.subforms[].columns`;
- `inlineColumns` without `inlineEdit`: `carrier-only`, listing the
column path;
- `name` anywhere else stays a literal: a dataset measure named `qty` on
`line` credits nothing.
Every leg below went through `scripts/ablation-replace.mjs`. The anchor
had to hit exactly once, and the mutation was proven on disk by anchor
and replacement counts and a changed blob. The restore was proven by
blob == HEAD and an empty `git diff HEAD`, with the driver's own trap on
EXIT, INT and TERM. The pins import the source relatively, so no `dist`
was involved. The tree was HEAD `7ee5c56679`.
| leg | mutation | pins red |
|:--|:--|:--|
| A1 | the relationship-field credit removed | `inlineColumns`,
no-`inlineEdit` (2) |
| A2 | the child-collection credit removed | `form`, `formViews` (2) |
| A3a | `inlineColumns` credited to the related object
(`referenceTargetOf(field)`) | `inlineColumns`, no-`inlineEdit` (2) |
| A3b | subform columns credited to the view's object (`inner`) |
`form`, `formViews` (2) |
| A4 | the `inlineEdit` gate removed (always `display`) |
no-`inlineEdit` (1) |
| A5 | `name` dropped from `LITERAL_KEYS` wholesale | `form`,
`formViews`, no-`inlineEdit`, dataset-measure literal (4) |
| A6 | the control: every child field credited once a grid exists | all
four carrier pins, through `line.memo` (4) |
A4's first attempt was a no-op and does not count. Its replacement text
(`'display'`) already occurred in the file, so the token count moved 7
to 7, and the tool refused before running the pins. It was redone with a
replacement that did not occur, `(true as boolean) ? 'display' :
'carrier'`, and went red with 1 failed. The final proof after all legs:
blob `4c109d4ef9ee` == HEAD, and `git diff HEAD` is 0 bytes.
## Verification
All on the merged head `f849aa53f6` (clean tree), merge base
`3fbf3ca617`:
- `pnpm turbo run build --filter='@objectstack/lint...'
--concurrency=2`: 4/4 tasks. Then `pnpm --filter @objectstack/lint exec
vitest run --maxWorkers=2`: 117 files, 5444 tests passed. Then `pnpm
--filter @objectstack/lint typecheck` (`tsc --noEmit` plus
`check:test-typecheck`, whose `tsconfig.test.json` is the program that
compiles the changed test file): OK. The three ran joined by `&&` under
`os-verify-lock`: `VERDICT command-exit 0`.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: exit 0, 60 commands. All 60 ran, each with
its exit code captured before any pipe. `--ran` reconciliation: exit 0,
58 exited 0, and 2 are NOT MEASURED. `pnpm check:dual-build-cjs-loads`
and `pnpm check:type-check-debt` exited 3, `PREREQUISITE NOT MET`: each
reads every workspace package's build output, and building the whole
`./packages/*` closure is `lint.yml`'s own step, declared to CI.
- eslint, narrowed to the diff: `eslint --no-inline-config --format
json` on the two changed `.ts` files exits 0 over 2 files, with 0 errors
and 0 warnings. The population is read from eslint's own config (`files:
'**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'`), and neither file is reported
as ignored. Invariance: `eslint.config.mjs` enables no type-aware
linting (no `parserOptions.project`, no `projectService`) and no
cross-file rule (no `import/*` rule), so this diff cannot move a verdict
on an untouched file. The repo-wide `pnpm lint` is CI's.
- A control-byte scan of the three changed files found nothing.
- `origin/main` has since moved to `f80e2a6dad`, which touches
`packages/rest`. It is not re-merged, because CI builds the merge ref.
## Changeset
`.changeset/20929-field-consumers-inline-grid-columns.md` grades
`@objectstack/lint` `patch`. The package publishes `dist` (`files[]`),
so a behaviour change owes a changeset, and `skip-changeset` does not
apply. The level is `patch` because nothing on the public surface moves:
no new export, the same rule id and severity, and the same finding
shape. Only which fields get a warning changes, plus the message
wording. The declaration reads `no`, so the level axis of
`check-changeset-no-major` stands down, and nothing is breaking, so no
ADR-0087 marker is owed.
## Acceptance notes
- **Same family, not addressed here, measured at the door** (`os
validate --json`, CLI at `f849aa53f6`, a second probe):
1. A grid with **no** explicit columns (`inlineEdit: 'grid'`, no
`inlineColumns`) draws columns derived from the child's fields (objectui
`deriveColumns`). Those fields still read `inert`:
`gc_line_derived.quantity` and `.amount`. The derivation lives in
objectui, not in the spec, so crediting it is a design question, like
the synthesized field-group layout, whose derivation the spec owns.
2. A subform's `amountField` names a child field (the spec: "Numeric
child column summed for the running total"), but the walk reads it in
the context of the object the view is bound to, the parent.
`gc_line_amt.amount`, named only there, reads `inert`. `totalField`
names a parent field, so this position cannot simply inherit the
subform's `childObject`.
- The master-detail block's `details[].columns` is not addressed here,
and #20928 remains open. Its entries share the `{ childObject, columns
}` shape `CHILD_COLLECTION_KEYS` reads.
- Column expressions under a subform (`expr`, `readonlyWhen`,
`requiredWhen`) are still scanned in the view object's context. Their
scope is mixed (`record` is the child row, `parent` is the header), so
that is an observation, not a mechanical change.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 3ad65b0 commit 3693a1b
3 files changed
Lines changed: 175 additions & 4 deletions
File tree
- .changeset
- packages/lint/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
589 | 589 | | |
590 | 590 | | |
591 | 591 | | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | | - | |
| 43 | + | |
| 44 | + | |
44 | 45 | | |
45 | 46 | | |
46 | 47 | | |
| |||
271 | 272 | | |
272 | 273 | | |
273 | 274 | | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
274 | 284 | | |
275 | 285 | | |
276 | 286 | | |
277 | 287 | | |
278 | 288 | | |
279 | 289 | | |
280 | 290 | | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
281 | 296 | | |
282 | 297 | | |
283 | 298 | | |
| |||
457 | 472 | | |
458 | 473 | | |
459 | 474 | | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
460 | 518 | | |
461 | 519 | | |
462 | 520 | | |
| |||
513 | 571 | | |
514 | 572 | | |
515 | 573 | | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
516 | 579 | | |
517 | 580 | | |
518 | 581 | | |
| |||
574 | 637 | | |
575 | 638 | | |
576 | 639 | | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
577 | 654 | | |
578 | 655 | | |
579 | 656 | | |
| |||
720 | 797 | | |
721 | 798 | | |
722 | 799 | | |
723 | | - | |
724 | | - | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
725 | 803 | | |
726 | | - | |
| 804 | + | |
| 805 | + | |
727 | 806 | | |
728 | 807 | | |
729 | 808 | | |
| |||
0 commit comments