Repository navigation
Commit 913fd6d
fix(lint,objectql)!: a validation rule reading a field its lookup target does not declare is refused at save, and no longer leaves its sibling rules without a verdict (#24508)
Fixes #24468
Clause-②: no (narrowing) (the save door newly refuses a validation rule
it accepted; owes one `CONTRACT_REVIEW_TIER` review, on the narrowing
face, before the queue)
Dispatched by the `domain:engine` seat 1 PM (claim 6262728643,
`session_01JtSQQ3EWsbNPzyYMug1tXy`). Developer session:
`session_01XPyxHgkHDoFuEza6RvCJtd`. This is the predicate-seam twin of
#24410 (PR #24466, `45bec43cd9`), and it follows that PR's pattern.
## What changes
**Engine: per-rule isolation (`packages/objectql`).**
`resolvePredicateRelated` reads, per reference, the union of the related
columns every validation rule reads. When one column was undeclared on
the target, `resolveOneHopRelated` answered the whole reference
`undeclared-field`. Every rule through that lookup then had no verdict
and refused the write, naming the other rule's field.
- `resolvePredicateRelated` now takes the undeclared columns out of the
shared read, using the shared `undeclaredRelatedFields` helper (no
second definition of "declared"). It lists them on the binding as
`undeclaredFields`. The loader reads the declared rest under `authority:
'predicate'`, with the same bound as before.
- `resolveTraversalScope` (`validation/rule-validator.ts`, the binding's
consumer) first refuses a rule only for its OWN undeclared columns,
naming only those. A rule naming none of them is judged on the row
exactly as it is alone.
- A reference whose every read column is undeclared still gets a binding
(`unavailable: 'undeclared-field'`), so no rule meets a bare id.
- ⛔ Not moved: the formula seam (`resolveFormulaRelated`,
`formulaHopScope`), `resolveOneHopRelated`, and the predicate read's
authority and bound. No undeclared column enters the system-authority
read.
**Lint: save-door refusal (`packages/lint`, the `domain:spec` rider
declared in 6262742638).** `refuseUndeclaredHopField` (#24410) now also
runs in `check()` wherever `traversalHydration` is set. Those are a
`script` / `cross_field` rule's `condition`, at the top level and inside
a `conditional`'s `then` / `otherwise`. It judges the hop column against
the target's field index (declared fields, injected columns and `id`)
under the existing `expression-invalid` rule, with no new code.
- The message opens with the shared validator's "unknown field" words,
naming the column and the target, then names the reference read. Its
consequence clause is per site: a rule "has no verdict and every write
it judges is refused". The formula message is byte-identical to before.
- One body serves `os validate`, `os build` and the object save door.
- A target the stack does not carry is not judged: a missed catch, never
a false refusal.
- A reference read more than one hop deep is left to the traversal
checks' own refusal (one finding, not two). This applies to the formula
site too, where it only removes a duplicate finding on an expression
already refused.
**Census rows.** objectql `federated-injected-column-readers.test.ts`
gets one row for the new engine use, `engine.ts#resolvePredicateRelated
:: referenceTargetOf()` → `caller-predicate`. `packages/core`'s
`[#22661]` census is per file, and both caller files (`engine.ts`,
`validate-expressions.ts`) already have rows. `pnpm --filter
@objectstack/core test:repo` passed 82/82.
## Measurement (M, booted composition:
`packages/qa/dogfood/test/rule-hop-undeclared-related-field.dogfood.test.ts`)
Fixture: `rrh_order` with a lookup `account` to `rrh_account` (declares
`status`). It carries two `script` rules: `open_account` reads
`record.account.status`, which is declared, and `north_only` reads
`record.account.region`, which is not.
| Reading | Before (`45bec43cd9`) | After (`69f76c6ef`) |
|---|---|---|
| `PUT /meta/object/rrh_order_saved` with both rules | `200` (saved) |
`422 INVALID_METADATA`, one `expression-invalid` issue at `validation
'north_only'`, message opens "unknown field `region` on `rrh_account`" |
| Control: same PUT with only `open_account` | `200` | `200` |
| Control: `open_account` alone, open / closed account | `201` / `400`
with its own message | `201` / `400` with its own message |
| Stored copy, open account | `400`: BOTH rules "could not be evaluated
('rrh_account' declares no 'region')" | `400`: only `north_only`, naming
`region`; `open_account` absent |
| Stored copy, closed account | not reached (the pin stopped at the
open-account row) | `400`: `open_account`'s own message, plus
`north_only`'s refusal |
The before run on `45bec43cd9` had 3 of 4 pins red. The save door
answered 200, and the stored copy's open-account write was refused with
`open_account` "could not be evaluated ... declares no 'region'". The
control's red was the pin's own `200` vs `201` create status, corrected
before the after run. The after run is 4/4, with the #24410 formula pin
also green (7/7).
**Ablations** (each through `scripts/ablation-replace.mjs`,
`ablation-dist-preflight.mjs` marker present in dist, restored to the
HEAD blob with `git diff HEAD` empty, then rebuilt with the marker
absent from dist):
- **A, per-rule isolation** (the `named.delete` of the undeclared
columns removed in `resolvePredicateRelated`). 3 engine pins red
(`engine-predicate-relationship.test.ts`, the `[#24468]` block), and the
booted sibling pin red.
- **B, lint refusal** (the `refuseUndeclaredHopField(..., 'rule')` call
removed). 3 lint pins red (`validate-expressions.rule-one-hop.test.ts`,
and both LIT pins in `runtime-gate.object-validation-writes.test.ts`),
and the booted save-door pin red.
- Restore: `git status --porcelain` clean, both markers absent from
dist, booted pin 4/4.
## Pins
- `packages/objectql/src/engine-predicate-relationship.test.ts`,
`[#24468]` block, on the real engine:
- the valid sibling admits when it admits alone, and refuses with its
own message;
- the bad rule names only `not_a_column`;
- one system read, projecting `['id', 'type']`.
-
`packages/objectql/src/validation/rule-relationship-traversal.test.ts`,
`[#24468]` block, on the binding contract: verdicts per rule; the bad
rule names only its own field when the binding lists two; a fully
undeclared reference still refuses.
- `packages/lint/src/validate-expressions.rule-one-hop.test.ts`, on the
build:
- refusal naming field and target;
- `id` and `created_at` admitted;
- a target outside the stack not judged.
- `packages/lint/src/runtime-gate.object-validation-writes.test.ts`,
`#24468` block, at the save door:
- a target from another package in the closure, beside its valid
sibling;
- a nested `conditional` → `then`;
- the control.
- `packages/lint/src/validate-expressions.test.ts`: the `#5017` receiver
census registers the refusal's `analysis` local as plumbing.
-
`packages/qa/dogfood/test/rule-hop-undeclared-related-field.dogfood.test.ts`:
the booted pin above. Test-only; declared to `domain:cli` by the seat at
review.
**Pin sweep.** The whole-reference `undeclared-field` verdict for a rule
is pinned in two places, and both still hold:
- `rule-relationship-traversal.test.ts` hands a single-rule binding by
hand.
- `engine-predicate-relationship.test.ts` "never smuggles an UNDECLARED
field" has one rule, whose own field is undeclared.
No other test asserts the sibling refusal.
## Other predicate surfaces that read through a lookup
Probed with `validateStackExpressions`:
- **Not reached, and not needed:** a field-rule slot (`requiredWhen`) or
an option `visibleWhen` through a lookup is already refused whole at
save. Nothing hydrates there; the message says "a field-level predicate
never reads the related record". This pass does not run there.
- **Not reached:** a `conditional` rule's `when` is opted out of
hydration by the existing walk (`checkConditional` evaluates it
unhydrated), so this pass does not judge it. See the acceptance notes.
## Verification (final head `69f76c6ef`; merge of `origin/main`
`959ee317b` at `0453ca518`)
- Build: `turbo run build --filter=@objectstack/dogfood^...
--filter=@objectstack/dogfood` exited 0 (66 tasks) on the merged tree.
The 8 extra dists that `check:dual-build-cjs-loads` needed also built,
exit 0.
- `pnpm --filter @objectstack/objectql test`: 446 files, 8387 tests
passed.
- `pnpm --filter @objectstack/lint test`: 146 files, 6885 tests passed
on `69f76c6ef`. The first run, on `0453ca518`, failed one test: the
receiver census's `analysis`, fixed in `69f76c6ef`.
- Dogfood: the rule and formula hop pins, 7/7.
- `typecheck`: objectql exit 0, lint exit 0 (both include their
`check:test-typecheck` leg; their `tsc` excludes tests), dogfood exit 0
(its `tsc --listFiles` includes the new pin).
- `pnpm --filter @objectstack/core test:repo`: 6 files, 82 tests passed.
- `node scripts/pm/dispatch-gates.mjs --commands` (no paths) derived 76
commands. All 76 ran on `0453ca518`; 75 exited 0.
`check:dual-build-cjs-loads` exited 3 (PREREQUISITE NOT MET: 8 dists
missing), was built for, and re-ran with exit 0.
- After `69f76c6ef` (one lint test file), the derivation is identical.
Re-ran: `check:cross-package-test-inputs`, `check:test-source-alias`,
`check:type-check-coverage`, `check:type-check-debt`, `check:nul-bytes`,
`check:issue-citations`, `check:tier-file-adoption`, and the three
changeset gates. All exited 0.
- `--ran` reconciliation: 76 derived, 76 run, 0 NOT-MEASURED, 0 UNRUN.
- `origin/main` moved to `7e58e0a27` after the merge: one `packages/cli`
test budget, no overlap, not merged.
## Changesets
- `@objectstack/lint` `minor`, `fix(lint)!:`, BREAKING (an accept-set
narrowing), with `Clause-②: no (narrowing)`. Its ADR-0087 marker is
`not-required (no-migration-prescription)`. It carries a FROM → TO and a
"What to do".
- `@objectstack/objectql` `patch`, `Clause-②: no` with no arm.
## Acceptance notes
- A `conditional` rule's `when` that reads through a lookup is not
hydrated (`checkConditional`), and the lint opts it out of the traversal
checks. So `when: record.account.status == 'x'` saves clean, and it
meets the bare id at evaluation. This is read from the code (the walk's
own comment says it), not run as a failing probe. It is outside this
card's two seams.
- The lint pass does not see a target field that an `objectExtensions`
entry contributes at `os build` / `os validate`, as for the formula site
(#24410's changeset says the same). The save door judges the merged
object.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01XPyxHgkHDoFuEza6RvCJtd)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 4ad3ce5 commit 913fd6d
12 files changed
Lines changed: 514 additions & 12 deletions
File tree
- .changeset
- packages
- lint/src
- objectql/src
- validation
- 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 | + | |
| 20 | + | |
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
331 | 331 | | |
332 | 332 | | |
333 | 333 | | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 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 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3549 | 3549 | | |
3550 | 3550 | | |
3551 | 3551 | | |
| 3552 | + | |
| 3553 | + | |
| 3554 | + | |
| 3555 | + | |
3552 | 3556 | | |
3553 | 3557 | | |
3554 | 3558 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1774 | 1774 | | |
1775 | 1775 | | |
1776 | 1776 | | |
| 1777 | + | |
| 1778 | + | |
| 1779 | + | |
| 1780 | + | |
1777 | 1781 | | |
1778 | | - | |
1779 | | - | |
1780 | | - | |
1781 | | - | |
1782 | | - | |
| 1782 | + | |
| 1783 | + | |
| 1784 | + | |
| 1785 | + | |
| 1786 | + | |
| 1787 | + | |
| 1788 | + | |
| 1789 | + | |
| 1790 | + | |
| 1791 | + | |
| 1792 | + | |
| 1793 | + | |
| 1794 | + | |
| 1795 | + | |
1783 | 1796 | | |
1784 | 1797 | | |
1785 | 1798 | | |
| |||
1969 | 1982 | | |
1970 | 1983 | | |
1971 | 1984 | | |
| 1985 | + | |
| 1986 | + | |
| 1987 | + | |
| 1988 | + | |
| 1989 | + | |
1972 | 1990 | | |
1973 | 1991 | | |
1974 | 1992 | | |
| |||
1977 | 1995 | | |
1978 | 1996 | | |
1979 | 1997 | | |
1980 | | - | |
| 1998 | + | |
| 1999 | + | |
| 2000 | + | |
| 2001 | + | |
| 2002 | + | |
| 2003 | + | |
1981 | 2004 | | |
1982 | 2005 | | |
1983 | | - | |
| 2006 | + | |
| 2007 | + | |
1984 | 2008 | | |
1985 | 2009 | | |
1986 | 2010 | | |
1987 | 2011 | | |
1988 | | - | |
| 2012 | + | |
| 2013 | + | |
| 2014 | + | |
1989 | 2015 | | |
1990 | 2016 | | |
1991 | 2017 | | |
1992 | 2018 | | |
1993 | 2019 | | |
1994 | 2020 | | |
1995 | | - | |
| 2021 | + | |
1996 | 2022 | | |
1997 | 2023 | | |
1998 | 2024 | | |
| |||
2180 | 2206 | | |
2181 | 2207 | | |
2182 | 2208 | | |
| 2209 | + | |
2183 | 2210 | | |
2184 | 2211 | | |
2185 | 2212 | | |
| |||
2902 | 2929 | | |
2903 | 2930 | | |
2904 | 2931 | | |
2905 | | - | |
| 2932 | + | |
2906 | 2933 | | |
2907 | 2934 | | |
2908 | 2935 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
231 | 231 | | |
232 | 232 | | |
233 | 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 | + | |
234 | 287 | | |
235 | 288 | | |
236 | 289 | | |
| |||
0 commit comments