Commit 4b2d904
fix(lint)!: object-field-ref-unknown judges a field's relatedListColumns, lookupColumns, lookupFilters and dependsOn, and indexes[].fields (#20479)
Part of #20432
Clause-②: no
## What this does
This is step 1 of the triage direction on the card (`5871296642`). The
object-level field-reference rule `object-field-ref-unknown`
(`packages/lint/src/validate-object-field-refs.ts`, `error`, already on
`os validate` / `os build` / `os lint` and the runtime publish door for
object writes) now judges five more field-name lists. Before, it judged
`highlightFields` and `publicSharing.redactFields` only:
- field level: `relatedListColumns`, `lookupColumns` (both arms),
`lookupFilters[].field`, `dependsOn` (both arms);
- object level: `indexes[].fields` (the fold `5870812245`).
A name that is not a field of the object the list addresses is refused
at the exact path. Examples:
`objects[i].fields.FIELD.lookupColumns[j].field` and
`objects[i].indexes[j].fields[k]`. The message has the family's shape:
the string that was written, the object it was judged against, a "Did
you mean" when a name is close, then the consequence. The hint ends with
that object's field list (`Fields on "X": a, b, c.`). The rule id,
severity and suite wiring are unchanged. No new rule file and no new
gate.
Step 2 is not in this PR: `SqlDriver.syncDeclaredIndexes` logging a
skipped declared index at `error` through `logDurabilityFailure`, and
showing it in drift. That is the `domain:engine` seat's second PR on
this card, and #20432 remains open for it.
## Which object a name is judged against (measured on each key's reader)
The PM's mechanism hypothesis 3 asked for this per key. The readers were
read at the objectui pin `.objectui-sha` `dd3f7e1b`:
| Position | Judged against | Reader |
|:---|:---|:---|
| `relatedListColumns[i]` | the object that owns the field (the child) |
`app-shell/src/utils/deriveRelatedLists.ts:291`, where the related list
is `childObject: child.name` |
| `lookupColumns[i]` / `.field` | the referenced object |
`fields/src/widgets/LookupField.tsx:337,484`, picker columns over
`refObjectSchema` of `referenceTo` |
| `lookupFilters[i].field` | the referenced object |
`LookupField.tsx:339,661` `lookupFiltersToRecord`, which feeds the query
on `referenceTo`. `validate-preset-comparands.ts` already binds the same
key this way. |
| `dependsOn[i]` / `.field` | the object that owns the field |
`LookupField.tsx:451-458`, where the gate reads the host record by this
key |
| `dependsOn[i].param`, or the bare name on a picker | the referenced
object | `LookupField.tsx:371-380,647`: `param` defaults to `field`, and
`dependentFilter[param]` |
| `indexes[i].fields[j]` | the object itself, plus injected columns |
`SqlDriver.syncDeclaredIndexes` reads names verbatim against
`physicalColumns` |
The referenced-object positions are judged only on types that render the
picker. `lookup` and `master_detail` render `LookupField`
(`FieldEditWidget.tsx:105-106`). `user` renders `UserField`, which
delegates to `LookupField` with `sys_user` fixed (`UserField.tsx:48`).
The target is the graph's `referenceTargetOf` answer. On any other type
those keys have no reader and are not judged.
`lookupColumns`, `dependsOn` and index columns are read verbatim by
their readers, so a dotted name there is judged as one name.
`relatedListColumns` and `lookupFilters[].field` keep the family's path
resolution. On `dependsOn`, a name that misses on the owner is reported
once, not a second time against the referenced object. The field form
rows that landed with #19332 G1b (`field.form.ts:232-245`) describe the
same addresses, and none of them claims a refusal, so no published text
changes.
## Severity
`error`, the family's existing tier. `os validate` and `os build` turn
author-time `error` findings into exit 1 (`validate.ts` and
`compile.ts`, `splitBySeverity` then `this.exit(1)`). This was measured
on the real built CLI over a two-object fixture (lookup
`invoice.account` to `account`):
- `os validate`: clean variant exit 0; misspelt variant exit 1 with 5
findings, one per position (index, relatedListColumns, lookupColumns
`.field`, lookupFilters `.field`, dependsOn).
- `os build`: clean variant exit 0; misspelt variant exit 1 with the
same 5 findings.
- The runtime publish door on an object write refuses too. This is
pinned in the test file, with `objects.proj_task.indexes[0].fields[1]`
and a lookup resolved against a context object.
## Producers census
Measured before landing, with the rule run from source (`tsx`) over
every exported object:
| Corpus | Objects | Entries judged (idx / rLC / lC / lF / dO) |
Findings |
|:---|---:|:---|---:|
| platform, `packages/**/*.object.ts` at `af444b4cd1` | 84 | 352 / 0 / 0
/ 0 / 3 | 0 |
| `examples/app-showcase` (+ platform: 108) | 24 | 0 / 14 / 4 / 1 / 2 |
0 |
| `examples/app-crm` (+ platform: 90) | 6 | 0 / 0 / 0 / 0 / 0 | 0 |
| `examples/app-todo` (+ platform: 85) | 1 | 5 / 0 / 0 / 0 / 0 | 0 |
| hotcrm `src/**/*.object.ts` at `5bec6eb0` | 18 | 68 / 13 / 0 / 0 / 6 |
0 |
| hotcrm + platform | 102 | 420 / 13 / 0 / 0 / 9 | 0 |
The hotcrm carriers match the card's reading exactly:
`relatedListColumns` in 3 files, `dependsOn` in 4. There is no real
misspelling in any producer, so nothing is fixed at a producer and
nothing is handed to the hotcrm lane.
The controls are lit:
- showcase: 5 planted misspellings (one per field-level position, both
`lookupColumns` arms) gave 5 findings;
- platform: 1 planted index misspelling (`sys_metadata_audit`) gave 1
finding;
- hotcrm: 12 planted misspellings gave 12 new findings.
`examples/app-multi-package` declares 2 inline objects that carry none
of these keys (population 0).
## Tests (all at `af444b4cd1`)
- `@objectstack/lint`: `vitest run`, 115 files, 5355 passed / 5 skipped.
`typecheck`, including `check:test-typecheck` over the test layer, exit
0. The rule file went from 31 to 60 tests: per list, a misspelling is
refused with its code and path, a correct name passes, and a name from
the OTHER object is still refused. Both object arms are covered, plus
`user` resolving to `sys_user`, the three skips, verbatim dotted names,
and junk inertness.
- `@objectstack/cli` `--project unit`: the first run gave 231 files
passed, 3306 tests passed and 29 skipped. The other 2 files refused with
the prerequisite "packages/cli is not built", which is not a red gate.
After `pnpm --filter @objectstack/cli build` those 2 files passed, 29/29
tests. Integration tier is declared to CI.
- `examples/app-showcase`: 29 files / 385 tests passed.
- Runtime-door consumers: `metadata-protocol`
`protocol-publish-drafts-object-field-refs` and
`build-probes-rule-failure` (12 tests), plus `platform-objects`
`sys-email.highlight-fields-resolve` (2 tests), all passed.
- Filter direction: only the package itself (`@objectstack/lint`) and
named consumers were run. No `...@objectstack/lint` sweep was run,
because the public surface (exports, types) is byte-unchanged; the
behaviour narrowing is what the consumer runs above cover.
## Ablation (one-shot, restored, not kept)
The changes were committed first. Each leg was applied through
`scripts/ablation-replace.mjs`, whose anchor must hit, with the landing
proven by anchor count 1 to 0 and a blob change. A shell trap restored
`HEAD` on exit. The test file imports the rule's source, so no build was
involved.
- A, field-level slots emptied: 15 failed.
- B, index walk emptied: 3 failed.
- C, reference address collapsed onto the owner: 13 failed.
Restore was proven by blob equal to HEAD blob `ac79635f` and an empty
`git diff HEAD`.
## Gates
`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` at `af444b4cd1` gave 60 commands, reconciled with `--ran`:
59 exited 0 and 1 is NOT MEASURED. The unmeasured one is `pnpm
check:dual-build-cjs-loads`, which exited 3 with PREREQUISITE NOT MET
because it reads every package's dist and 9 unrelated packages are
unbuilt in this worktree. The four artifact-roster gates flagged under
this diff's directories also exited 0: `check-changeset-fixed`,
`check:authz-resolver`, `check:error-code-casing` and
`check:filter-alias-parity`. ESLint, as a proven narrowing, ran on the 4
changed TS files (`--format json`: 4 files, 0 errors, 0 warnings). The
config enables no type-aware linting (the printed config has
`parserOptions` equal to `ecmaVersion` and `sourceType` only), so this
diff cannot move a verdict on an untouched file. The repo-wide `pnpm
lint` is CI's.
## Acceptance notes
- `carrier: #20432`: step 2, the sync half, belongs to the
`domain:engine` seat's second PR on this card.
`SqlDriver.syncDeclaredIndexes` logs a skipped declared index (`unique`
above all) at `warn`. Its duplicate-row sibling in the same function
logs at `error` through `logDurabilityFailure`. `expectedIndexes` drops
the index from drift. The Studio save door (`ObjectSchema.safeParse`)
still admits a misspelt name, so that half still matters after this PR.
- Boundary: this rule judges existence only. An index column that names
a real but virtual field (a `formula`) passes here and is still skipped
at sync. Materialization stays the driver's question. It is written into
the rule's docblock, which replaces the old "`indexes[].fields[]` is a
storage question" paragraph (reason: that exclusion left a misspelling
with no door at all).
- Boundary: the build-probes receipt plane
(`metadata-protocol/src/build-probes.ts`) runs this rule over a
one-object universe. The referenced-object positions are therefore
unknowable, and silent, there. Its owner-addressed positions are judged.
The door-time gate judges both.
- Boundary, pre-existing in the shared object graph: fields added by an
`objectExtensions` entry are not merged into the graph. A list naming
only such a field would be refused as a false finding. The census gave 0
findings in every producer measured, so no such case arose. Recorded
here, not filed.
- File surface beyond the named file, same package, comments only:
`reference-integrity-suite.ts`'s member note said the rule resolves
names against the object's OWN field map only, which is now false for
the picker keys, so it was rewritten. The `index.ts` export comment
lists the new positions.
- The changeset carries `Clause-②: no (narrowing)` with a BREAKING
banner and `minor`, following the ADR-0087 gate (`not-required
(no-migration-prescription)`) and the precedent of the
`security-role-word` field-groups changeset. This body carries the
claim's `Clause-②: no` verbatim.
- `origin/main` moved during the run (`dc0ab6a2ed`). This branch was not
re-merged. CI validates the merge ref. The gate derivation flagged one
stale family file (`scripts/cross-package-test-inputs.mjs`), whose gate
ran green on this tree.
---
_Generated by [Claude
Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent b810ddb commit 4b2d904
5 files changed
Lines changed: 784 additions & 29 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 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
497 | 497 | | |
498 | 498 | | |
499 | 499 | | |
500 | | - | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
501 | 503 | | |
502 | 504 | | |
503 | 505 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
326 | 326 | | |
327 | 327 | | |
328 | 328 | | |
329 | | - | |
330 | | - | |
331 | | - | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
332 | 335 | | |
333 | 336 | | |
334 | 337 | | |
| |||
0 commit comments