Repository navigation
Commit 45bec43
fix(lint,objectql)!: a formula reading a field its lookup target does not declare is refused at save, and no longer nulls its sibling formulas (#24466)
Fixes #24410
Clause-②: no (narrowing) (the save door newly refuses a formula it
accepted; owes one `CONTRACT_REVIEW_TIER` review, on the narrowing face,
before the queue)
Dispatched by the `domain:engine` seat 1 PM (claim 6258777300).
Developer session: `session_01SE5iTKQC8i5LBRywMuQZmt`.
## What changes
**Save refusal (lint, `judgeFieldFormula`).** The field-formula pass now
judges the field a formula reads one hop through a reference against the
target object's field index: its declared fields, the system columns the
platform injects on it, and `id`. A read of a field the target does not
declare is refused under the existing `EXPRESSION_INVALID` rule
(`expression-invalid`). No new code. The message opens in the shared
validator's `unknown field` words and names the field and the target:
"unknown field `name` on `rhf_book`: this formula reads it through the
reference `record.book` ...". The same pass runs for `os validate`, `os
build` and the object save door.
- At the save door the snapshot carries the live registry's objects in
the written package's closure. So a target from a package the written
one depends on is judged, and a system object is always judged.
- A target outside the snapshot is not judged. That is a missed catch,
never a false refusal.
- A hop into the stored-metadata-body family stays with
`refuseStoredMetadataBodyHop` and is not judged twice.
**Sibling isolation (objectql, `resolveFormulaRelated`).** Measured
cause: no driver read failed. The formula read merged every formula's
hop columns per reference. The shared one-hop loader then answered the
whole reference `undeclared-field` when any column was undeclared. So
the valid sibling read `null` and reported the bad formula's field: "the
formula reads 'name' through `book`". The fix:
- `resolveFormulaRelated` takes the undeclared columns out of the shared
read.
- `formulaHopScope` judges them per formula.
- A bad formula reads `null` with its existing `undeclared-field` fault
report, which now names only its own columns.
- A valid sibling is served from the same single read, exactly as it is
alone.
- The predicate seam's use of the loader is byte-for-byte unchanged. The
declared-field test moved into one helper, `undeclaredRelatedFields`,
that both seams share.
**The WARN text.** "a re-save of the object, refuses it with a located
message" now holds for this fault: a re-save of the object answers 422
with the located refusal (measured below). Left unchanged.
⛔ No change to which records a formula may read: the #24316 / #24358
family guards are untouched.
## Verification (M, booted composition: `bootStack`, real ObjectQL, SQL
driver, REST, auth, metadata)
Pin:
`packages/qa/dogfood/test/formula-hop-undeclared-related-field.dogfood.test.ts`.
The fixture is `rhf_book` (title), `rhf_reader` (reader_name) and
`rhf_loan`. The loan has a valid `summary` formula
(`joinNonEmpty([record.book.title, record.reader.reader_name], ' · ')`)
and a code-declared bad `bad_title` (`record.book.name`).
| Measurement | Before (`main` `db1f1fd`) | After (head) |
|---|---|---|
| `PUT /meta/object/rhf_loan_saved` with `record.book.name` | **200**
`Saved object 'rhf_loan_saved'` | **422** `INVALID_METADATA`, one issue:
rule `expression-invalid`, where `object 'rhf_loan_saved' · field
'summary' expression`, message `unknown field \`name\` on \`rhf_book\`
...` |
| Control: the same save with `record.book.title` | 200 | 200 |
| Control: `GET /data/rhf_loan?fields=summary` (valid alone) |
`RHFBOOK71 · RHFREADER72` | `RHFBOOK71 · RHFREADER72` |
| `GET /data/rhf_loan`: `summary` / `bad_title` | `null` / `null`, and
WARN on `summary`: "reads 'name' through `book`" | `RHFBOOK71 ·
RHFREADER72` / `null`, with WARN on `bad_title` only |
| `GET /data/rhf_loan/:id`: `summary` / `bad_title` | `null` / `null` |
value / `null` |
Unit pins:
- `engine-formula-one-hop.test.ts`: the sibling is served as alone, ONE
book read with fields `id,title`, and only the bad formula reports.
- `validate-expressions.formula-one-hop.test.ts`: the refusal names the
field and the target. `id` and an injected column (`created_at`) are
admitted. A target the stack does not carry is not judged.
- `runtime-gate.object-formula-writes.test.ts`: at the door, the target
comes from another package the written one depends on. The refusal is
LIT and the declared-field read is the CONTROL.
**Ablations** (`scripts/ablation-replace.mjs` wrap form, then
`scripts/ablation-dist-preflight.mjs`). Each restore was checked against
the HEAD blob with an empty `git diff HEAD`.
- **Refusal.** In `refuseUndeclaredHopField`, `if (column === 'id' ||
...) continue;` became an always-continue carrying the marker
`ABLATED_24410`.
- lint build exit 0. Marker present in 4 `dist/` files.
- Red: 2 lint pins (`refuses a read of a field the target does not
declare`, the door `LIT`), and the booted save case.
- The first attempt deleted the call instead. The lint build then exited
1 on the unused closure, so it was redone. The pins were red on that
attempt too.
- **Isolation.** In `formulaHopScope`, `own` became every undeclared
column of the reference, not this formula's own.
- objectql build exit 0. Marker in 4 `dist/` files.
- Red: the objectql sibling pin and the booted sibling case.
- The booted save case was also red on this run, because ablation 1's
restore had not rebuilt `lint/dist` yet. That redness belongs to
ablation 1.
- **Restore legs.** lint and objectql rebuilt (exit 0). `--absent`
checks: marker absent from all 20 lint and all 14 objectql built files.
Booted pin 3/3.
**Corpus.** No formula field the repository ships reads through a
reference: 28 formula fields across examples (app-crm, app-showcase,
app-todo) and platform display titles. So the pass refuses nothing it
accepted in-tree.
## Tests and gates
The full package suites ran on `2293b1c45`, after merging `origin/main`
`9112bcd88`:
- `pnpm --filter @objectstack/lint exec vitest run`: 145 files, 6869
tests passed.
- `pnpm --filter @objectstack/objectql exec vitest run`: 444/445 files
passed. The one red was the federated-reader census, which needed a row
for the new `referenceTargetOf()` use. Row added (`caller-predicate`);
that file plus the one-hop file then passed 31/31.
Then `origin/main` `fa26308bb` was merged in (head `313ee1040`). It
brings #24423, which touches `objectql/hook-binder.ts`: the hook body's
`ctx.title`, which `serveFormulaField` → `resolveFormulaRelated` serves.
It also moves `packages/spec`. On `313ee1040`:
- The closure was rebuilt.
- typecheck: `@objectstack/lint` 0, `@objectstack/objectql` 0,
`@objectstack/dogfood` 0.
- `pnpm --filter @objectstack/spec check:generated`: exit 0.
- objectql one-hop, hook-binder wrap-handler and census files: 34/34
passed.
- Booted pin: 3/3.
Derived gates: `node scripts/pm/dispatch-gates.mjs --commands` (no
paths) on `313ee1040` gives 75 commands. Each was run with its exit code
recorded, and all 75 exited 0.
- `check:dual-build-cjs-loads` exited 3 on the earlier head (8 packages
outside the dogfood closure had no `dist/`). They were built, and it
exits 0.
- `--ran` verdict: "75 derived, 75 run, 0 NOT-MEASURED, 0 UNRUN (a
DERIVED zero — all 75 recorded an exit code and none of them is 3)".
## Diff (+/−)
| File | + | − |
|---|---|---|
| `.changeset/24410-lint-formula-hop-undeclared-field.md` | 19 | 0 |
| `.changeset/24410-objectql-formula-hop-sibling-isolation.md` | 17 | 0
|
| `packages/lint/src/validate-expressions.ts` | 56 | 1 |
| `packages/lint/src/validate-expressions.formula-one-hop.test.ts` | 28
| 0 |
| `packages/lint/src/runtime-gate.object-formula-writes.test.ts` | 40 |
0 |
| `packages/objectql/src/engine.ts` | 70 | 8 |
| `packages/objectql/src/engine-formula-one-hop.test.ts` | 18 | 0 |
| `packages/objectql/src/federated-injected-column-readers.test.ts` | 6
| 0 |
|
`packages/qa/dogfood/test/formula-hop-undeclared-related-field.dogfood.test.ts`
| 139 | 0 |
File surface: within the claim, plus two pin files the measurement
required.
- `runtime-gate.object-formula-writes.test.ts` holds the door pin with a
cross-package target.
- `federated-injected-column-readers.test.ts` holds the census row. It
reds on any new engine `referenceTargetOf()` use.
- The booted pin lives in `packages/qa/dogfood`, to be declared to
`domain:cli` at review.
## Acceptance notes
- **The predicate seam has the same union verdict (read-only inference,
not measured at a door).** `resolvePredicateRelated` hands the shared
loader every rule's hop columns per reference. So one validation rule
naming a field its target does not declare leaves every rule through
that reference without a verdict. The lint added here judges formula
`expression`s only, not hydrated rule `condition`s. The fence leaves
that seam unchanged. Dedupe words: validation rule undeclared related
field, predicate hop sibling refusal, rule traversal related field
existence.
- `content/docs/data-modeling/formulas.mdx`'s "Field must exist" row
does not mention the one-hop read. It does not misstate the new
behaviour, so it was left alone.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01SE5iTKQC8i5LBRywMuQZmt)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent ce2e08e commit 45bec43
10 files changed
Lines changed: 398 additions & 9 deletions
File tree
- .changeset
- packages
- core/src/security
- lint/src
- objectql/src
- qa/dogfood/test
| 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 | + | |
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
201 | 201 | | |
202 | 202 | | |
203 | 203 | | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
204 | 209 | | |
205 | 210 | | |
206 | 211 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
117 | 117 | | |
118 | 118 | | |
119 | 119 | | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
120 | 160 | | |
121 | 161 | | |
122 | 162 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
87 | 87 | | |
88 | 88 | | |
89 | 89 | | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
90 | 118 | | |
91 | 119 | | |
92 | 120 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
115 | 115 | | |
116 | 116 | | |
117 | 117 | | |
118 | | - | |
| 118 | + | |
119 | 119 | | |
120 | 120 | | |
121 | 121 | | |
| |||
1768 | 1768 | | |
1769 | 1769 | | |
1770 | 1770 | | |
| 1771 | + | |
| 1772 | + | |
| 1773 | + | |
| 1774 | + | |
| 1775 | + | |
| 1776 | + | |
| 1777 | + | |
| 1778 | + | |
| 1779 | + | |
| 1780 | + | |
| 1781 | + | |
| 1782 | + | |
| 1783 | + | |
| 1784 | + | |
1771 | 1785 | | |
1772 | 1786 | | |
1773 | 1787 | | |
| |||
1948 | 1962 | | |
1949 | 1963 | | |
1950 | 1964 | | |
| 1965 | + | |
| 1966 | + | |
| 1967 | + | |
| 1968 | + | |
| 1969 | + | |
| 1970 | + | |
| 1971 | + | |
| 1972 | + | |
| 1973 | + | |
| 1974 | + | |
| 1975 | + | |
| 1976 | + | |
| 1977 | + | |
| 1978 | + | |
| 1979 | + | |
| 1980 | + | |
| 1981 | + | |
| 1982 | + | |
| 1983 | + | |
| 1984 | + | |
| 1985 | + | |
| 1986 | + | |
| 1987 | + | |
| 1988 | + | |
| 1989 | + | |
| 1990 | + | |
| 1991 | + | |
| 1992 | + | |
| 1993 | + | |
| 1994 | + | |
| 1995 | + | |
| 1996 | + | |
| 1997 | + | |
| 1998 | + | |
| 1999 | + | |
| 2000 | + | |
| 2001 | + | |
| 2002 | + | |
| 2003 | + | |
1951 | 2004 | | |
1952 | 2005 | | |
1953 | 2006 | | |
| |||
2848 | 2901 | | |
2849 | 2902 | | |
2850 | 2903 | | |
| 2904 | + | |
| 2905 | + | |
2851 | 2906 | | |
2852 | 2907 | | |
2853 | 2908 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
289 | 289 | | |
290 | 290 | | |
291 | 291 | | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
292 | 310 | | |
293 | 311 | | |
294 | 312 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2651 | 2651 | | |
2652 | 2652 | | |
2653 | 2653 | | |
| 2654 | + | |
| 2655 | + | |
| 2656 | + | |
| 2657 | + | |
| 2658 | + | |
| 2659 | + | |
| 2660 | + | |
| 2661 | + | |
| 2662 | + | |
| 2663 | + | |
| 2664 | + | |
| 2665 | + | |
| 2666 | + | |
| 2667 | + | |
| 2668 | + | |
| 2669 | + | |
| 2670 | + | |
| 2671 | + | |
| 2672 | + | |
| 2673 | + | |
| 2674 | + | |
| 2675 | + | |
2654 | 2676 | | |
2655 | | - | |
| 2677 | + | |
| 2678 | + | |
| 2679 | + | |
| 2680 | + | |
| 2681 | + | |
| 2682 | + | |
| 2683 | + | |
2656 | 2684 | | |
2657 | 2685 | | |
2658 | 2686 | | |
| |||
2687 | 2715 | | |
2688 | 2716 | | |
2689 | 2717 | | |
2690 | | - | |
2691 | | - | |
| 2718 | + | |
| 2719 | + | |
| 2720 | + | |
| 2721 | + | |
| 2722 | + | |
| 2723 | + | |
| 2724 | + | |
| 2725 | + | |
| 2726 | + | |
| 2727 | + | |
| 2728 | + | |
| 2729 | + | |
| 2730 | + | |
| 2731 | + | |
| 2732 | + | |
| 2733 | + | |
2692 | 2734 | | |
2693 | 2735 | | |
2694 | 2736 | | |
| |||
6264 | 6306 | | |
6265 | 6307 | | |
6266 | 6308 | | |
6267 | | - | |
6268 | | - | |
| 6309 | + | |
| 6310 | + | |
| 6311 | + | |
| 6312 | + | |
| 6313 | + | |
| 6314 | + | |
| 6315 | + | |
| 6316 | + | |
| 6317 | + | |
| 6318 | + | |
| 6319 | + | |
| 6320 | + | |
| 6321 | + | |
| 6322 | + | |
| 6323 | + | |
| 6324 | + | |
| 6325 | + | |
| 6326 | + | |
| 6327 | + | |
| 6328 | + | |
| 6329 | + | |
| 6330 | + | |
| 6331 | + | |
| 6332 | + | |
6269 | 6333 | | |
6270 | 6334 | | |
6271 | 6335 | | |
| |||
10279 | 10343 | | |
10280 | 10344 | | |
10281 | 10345 | | |
10282 | | - | |
10283 | | - | |
10284 | | - | |
| 10346 | + | |
10285 | 10347 | | |
10286 | 10348 | | |
10287 | 10349 | | |
| |||
0 commit comments