Repository navigation
Commit da989bb
Fixes #22474
Clause-②: yes
## What was wrong
`ObjectQL.validate()` (the preview behind `validateData` and the import
dry run) made one `evaluateValidationRules` call and bound no
master-detail header. Every write binds it: the insert passes `parent:
insertParentForRow?.(rows[i])`, the by-id update passes `parent:
roWhenParent` and `previousParent: roWhenPreviousParent`, and the bulk
update passes `parent: parentForRow?.(row)` and `previousParent:
previousParentForRow?.(row)`. So a field `requiredWhen: "parent.status
== 'sent'"` faulted in the preview (`Unknown variable: parent`) and the
preview refused the row as `rule_violation` / `unevaluable`, in both
modes, while the write admitted it under a draft header.
**Measured before the change:** this PR's test file, run with
`engine.ts` put back to its `e148ca9842` blob (`5ca77a148de1`), is 17
red of 21. The 4 green are the no-header-read control, the cannot-read
pin (it holds trivially when the preview refuses every such row), and
the two parity floor cases. The preview answered `description:
rule_violation` for an insert under a draft header and under a sent
header, for an update with a stored line's `id` under either header, and
for the import dry run (through `ObjectStackProtocolImplementation` and
`runImport`) of a created and of a matched line. The real insert, update
and import admitted the draft-header rows and refused the sent-header
rows with `required`. The parity case listed exactly the two missing
inputs: `parent` (3 write calls) and `previousParent` (2 write calls).
## The change (`packages/objectql/src/engine.ts`)
- **Bound as the write binds it.** `validate()` resolves the header per
row from the judged view, the write's own payload-FK-first reading
(`masterIdOf`): the row's FK on an insert, and on an update the patch's
FK, else the FK of the stored row that #22445's change already reads. A
repointing update also binds the stored row's own header as
`previousParent`, for the ADR-0113 pre-check, exactly when the write
resolves it (`repointsMaster`).
- **Under the write's gate.** Only an object that declares a field
`requiredWhen` reading `parent` (`hasParentScopedRequiredWhen`, the
insert's gate and the `requiredWhen` half of the update's gate) reads a
header. Every other object's preview reads none.
- **Read under the caller's access** (new private
`resolvePreviewParents`). The write reads the header elevated
(`resolveMasterDetailParent(s)` under `referenceCheckContext`). The
preview runs none of the write's gates, so it reads as #22445's
stored-row read does:
1. The read door, under the caller's own context, one `find` for the
batch, decides whether the header exists for this caller. A header it
does not return binds as a missing header binds (`null`), so the rule
refuses as unevaluable whatever the header holds. A failed read binds
every header as missing, as the write's own failed header read does, and
logs at `warn`.
2. The write's own header read (`resolveMasterDetailParents`) runs only
for the headers read 1 returned.
3. A header column is kept only where the read door served this caller
that very value (`servedAsStored`). The header is then materialised over
the master's declared fields, so a hidden or masked column reads `null`:
judged as empty.
- The orphaned one-line docblock above `servedAsStored` (named in
#22445's ACCEPT as riding the next PR that touches this spot) is moved
back onto `mergeReadContext`; `servedAsStored`'s docblock now names its
second caller.
`packages/core/src/utils/import-runner.ts` is not touched: the dry run
already sends the row's FK (create) and the matched record's `id`
(update), which is everything the preview needs.
## Pins (`packages/objectql/src/validate-preview-parent.test.ts`, 21
cases)
- (a) Insert, and update with a stored line's `id`: admitted under a
draft header, refused with `required` under a sent header; the real
insert and by-id update give the same verdict.
- (b) A repoint from a draft onto a sent header is refused, as the write
refuses it (the stored row complied under the header it leaves); a
legacy line under a sent header rests through a repoint onto another
sent header, as on the write.
- (c) Control: an object with a master-detail relation and only a
row-scoped `requiredWhen` reads no header in either mode (an engine
middleware counts every header read under any context); positive
control: the parent-scoped line does read one.
- (d) Security. A caller whose read scope excludes the header gets one
verdict whatever the header holds, equal to the verdict for a header id
that names no row (insert and update). A caller served `status` hidden,
and one served it masked, get one verdict whatever the header holds, and
it is the empty-column verdict. Control: a caller who reads the header
in full gets the write's verdict in both modes.
- (e) The import dry run through the protocol, created line and matched
line (an edit, and a repoint onto a sent header), answers what the real
import does.
- (f) Parity, read from `engine.ts` itself (below).
### How the parity pin finds the call sites
It parses `engine.ts` with the TypeScript compiler API, finds every call
whose callee is the identifier `evaluateValidationRules`, and attributes
it to its enclosing class method: the one in `validate` is the preview,
every other is a write (today four: `insert`'s insert call, `update`'s
by-id call and its two bulk calls). A call's inputs are the keys of its
options object literal, including the keys of an object literal spread
into it (`...(c ? { k } : {})`). An options argument it cannot see into
fails the test rather than passing it. It asserts: exactly one preview
call; at least four write calls covering both modes; every key a write
call passes is passed by the preview; the preview passes no key no write
passes.
## Reverse verification and ablations
Each leg through `scripts/ablation-replace.mjs` (anchor must hit once,
blob change and restore proven against `HEAD`), inside a script with its
own EXIT/INT/TERM restore trap. Run on head `57c3ce9f57` and again on
the merged head `0958e192ac` (`engine.ts` blob `df2300664b3c` in both),
with identical red sets. The subject is imported as source
(`./engine.js`), so no `dist/` is in the resolution path.
| leg | mutation | red | green |
|---|---|---|---|
| A1 | preview binds `parent: undefined`, `previousParent: undefined` |
15: all of (a), (b), (e), the hide/mask pins and the full-reader control
in (d) | (c) both, the cannot-read pin, the 3 parity cases |
| A1b | `previousParent: undefined` only | 2: the repoint pin in (b),
the import repoint in (e) | 19 |
| S1 | the visibility read under `referenceCheckContext` (elevated) | 3:
the cannot-read, hide and mask pins | 18, the full-reader control
included |
| S2 | `servedAsStored(...)` replaced by `true` | 1: the mask pin | 20,
the hide pin included |
| S3 | the whole column filter replaced by `true` | 2: the hide and mask
pins | 19 |
| G1 | the `hasParentScopedRequiredWhen` gate replaced by `true` | 1:
the no-header-read control | 20 |
| P1 | a new key added to the insert's write call only | 1: "every key a
write call passes, the preview passes", naming the key and `engine.ts`
line | 20 |
| P2 | `parent` / `previousParent` keys removed from the preview call |
16: A1's 15 plus that parity case | 5 |
| P3 | a key added to the preview call only | 1: "the preview passes no
input that no write passes" | 20 |
A first P3 attempt used an anchor its own replacement contained; the
tool refused it before any run (anchor count did not drop), and it was
re-run with a distinct anchor. An earlier G1 leg ran while a sibling
gate's full build had removed `packages/metadata-protocol/dist`, so its
suite never loaded; it is discarded, and all nine legs above were re-run
together on both heads.
## Verification (head `0958e192ac`, `origin/main` `446c8b2a64` merged
in, no conflict, no file of this PR touched by it)
- `pnpm --filter @objectstack/objectql exec vitest run --project local
--maxWorkers=2`: 392 files, 7737 tests passed (also on `57c3ce9f57`
before the merge, same counts).
- `pnpm --filter @objectstack/objectql typecheck`: exit 0 on
`0958e192ac`; the new test file is in the `tsconfig.test.json` program
(`--listFiles`) and adds no error to the pinned test-typecheck debt.
- Gates from `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` (68 commands): all 68 exit 0,
each exit captured before any pipe; `--ran` reconciliation: 68 derived,
68 run, 0 NOT-MEASURED. On the pre-merge head two gates were red and are
fixed here: `check:objectql-double-limit` (the test double's `find`
carried a read counter its probe could not drive; the control now counts
reads with an engine middleware) and `check:query-options-erasure` (an
`as any` on the header read's options, now `EngineQueryOptions`).
- Lint, narrowed: `eslint --no-inline-config --format json` over the two
changed source files, 2 files, 0 errors, 0 warnings. Population read
from the config (`calculateConfigForFile`: both `.ts` files linted, the
changeset ignored); the config enables no type-aware linting (no
`parserOptions.project`), so the diff cannot move the verdict of an
untouched file.
## Acceptance notes
- **The write reads the header elevated (finding, reported per the
dispatch).** Measured with a throwaway test on this branch: a caller
whose read scope excludes header `hx` inserts a line under it; the write
admits it while `hx` is draft and refuses it with `required` while `hx`
is sent, so the write's verdict depends on a header that caller cannot
read. This is the documented design of `resolveMasterDetailParent` (the
caller's right to write the detail is settled before the header is read;
the tenant wall is kept). The preview keeps the stricter rule, so for
such a caller the preview and the write can answer differently, which
the changeset states.
- **Named limit kept:** the preview still runs no `readonlyWhen` strip,
so the update's judgement of the FK's own `readonlyWhen` lock
(`settleMasterDetailLanding` step 1) is not run; a repoint that lock
would take back out is judged against the header the patch names.
- An option `visibleWhen` that reads `parent` (the card's "likely
sibling") is unchanged: no write binds `parent` for option gates either
(`evaluateOptionVisibility` takes no header), so, read from source and
not measured here, preview and write already agree there (both refuse
the faulting pick).
- The parity pin compares option keys, not values; the behavioural pins
hold the values of `parent` and `previousParent`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Bw3y2DWhT9RPnrmDsNqEVG)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9c93ce6 commit da989bb
3 files changed
Lines changed: 701 additions & 11 deletions
File tree
- .changeset
- packages/objectql/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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3207 | 3207 | | |
3208 | 3208 | | |
3209 | 3209 | | |
3210 | | - | |
3211 | 3210 | | |
3212 | 3211 | | |
3213 | 3212 | | |
3214 | | - | |
| 3213 | + | |
| 3214 | + | |
| 3215 | + | |
3215 | 3216 | | |
3216 | 3217 | | |
3217 | | - | |
3218 | | - | |
3219 | | - | |
3220 | | - | |
3221 | | - | |
| 3218 | + | |
| 3219 | + | |
| 3220 | + | |
| 3221 | + | |
| 3222 | + | |
| 3223 | + | |
3222 | 3224 | | |
3223 | 3225 | | |
3224 | 3226 | | |
| |||
3247 | 3249 | | |
3248 | 3250 | | |
3249 | 3251 | | |
| 3252 | + | |
3250 | 3253 | | |
3251 | 3254 | | |
3252 | 3255 | | |
| |||
8718 | 8721 | | |
8719 | 8722 | | |
8720 | 8723 | | |
| 8724 | + | |
| 8725 | + | |
| 8726 | + | |
| 8727 | + | |
| 8728 | + | |
| 8729 | + | |
| 8730 | + | |
| 8731 | + | |
| 8732 | + | |
| 8733 | + | |
| 8734 | + | |
| 8735 | + | |
| 8736 | + | |
| 8737 | + | |
| 8738 | + | |
| 8739 | + | |
| 8740 | + | |
| 8741 | + | |
| 8742 | + | |
| 8743 | + | |
| 8744 | + | |
| 8745 | + | |
| 8746 | + | |
| 8747 | + | |
| 8748 | + | |
| 8749 | + | |
| 8750 | + | |
| 8751 | + | |
| 8752 | + | |
| 8753 | + | |
| 8754 | + | |
| 8755 | + | |
| 8756 | + | |
| 8757 | + | |
| 8758 | + | |
| 8759 | + | |
| 8760 | + | |
| 8761 | + | |
| 8762 | + | |
| 8763 | + | |
| 8764 | + | |
| 8765 | + | |
| 8766 | + | |
| 8767 | + | |
| 8768 | + | |
| 8769 | + | |
| 8770 | + | |
| 8771 | + | |
| 8772 | + | |
| 8773 | + | |
| 8774 | + | |
| 8775 | + | |
| 8776 | + | |
| 8777 | + | |
| 8778 | + | |
| 8779 | + | |
| 8780 | + | |
| 8781 | + | |
| 8782 | + | |
| 8783 | + | |
| 8784 | + | |
| 8785 | + | |
| 8786 | + | |
| 8787 | + | |
| 8788 | + | |
| 8789 | + | |
| 8790 | + | |
| 8791 | + | |
| 8792 | + | |
| 8793 | + | |
| 8794 | + | |
| 8795 | + | |
| 8796 | + | |
| 8797 | + | |
| 8798 | + | |
| 8799 | + | |
| 8800 | + | |
| 8801 | + | |
| 8802 | + | |
| 8803 | + | |
| 8804 | + | |
| 8805 | + | |
| 8806 | + | |
| 8807 | + | |
| 8808 | + | |
| 8809 | + | |
| 8810 | + | |
| 8811 | + | |
| 8812 | + | |
| 8813 | + | |
| 8814 | + | |
| 8815 | + | |
| 8816 | + | |
| 8817 | + | |
| 8818 | + | |
| 8819 | + | |
| 8820 | + | |
| 8821 | + | |
| 8822 | + | |
| 8823 | + | |
8721 | 8824 | | |
8722 | 8825 | | |
8723 | 8826 | | |
| |||
13046 | 13149 | | |
13047 | 13150 | | |
13048 | 13151 | | |
| 13152 | + | |
| 13153 | + | |
| 13154 | + | |
| 13155 | + | |
| 13156 | + | |
| 13157 | + | |
| 13158 | + | |
| 13159 | + | |
| 13160 | + | |
| 13161 | + | |
| 13162 | + | |
| 13163 | + | |
| 13164 | + | |
13049 | 13165 | | |
13050 | | - | |
13051 | | - | |
13052 | | - | |
| 13166 | + | |
| 13167 | + | |
| 13168 | + | |
| 13169 | + | |
13053 | 13170 | | |
13054 | 13171 | | |
13055 | 13172 | | |
| |||
13090 | 13207 | | |
13091 | 13208 | | |
13092 | 13209 | | |
13093 | | - | |
| 13210 | + | |
| 13211 | + | |
| 13212 | + | |
| 13213 | + | |
13094 | 13214 | | |
13095 | 13215 | | |
13096 | 13216 | | |
| |||
13310 | 13430 | | |
13311 | 13431 | | |
13312 | 13432 | | |
| 13433 | + | |
| 13434 | + | |
| 13435 | + | |
| 13436 | + | |
| 13437 | + | |
| 13438 | + | |
| 13439 | + | |
| 13440 | + | |
| 13441 | + | |
| 13442 | + | |
| 13443 | + | |
| 13444 | + | |
| 13445 | + | |
| 13446 | + | |
| 13447 | + | |
| 13448 | + | |
| 13449 | + | |
| 13450 | + | |
| 13451 | + | |
| 13452 | + | |
| 13453 | + | |
| 13454 | + | |
| 13455 | + | |
| 13456 | + | |
13313 | 13457 | | |
13314 | 13458 | | |
13315 | 13459 | | |
| |||
13391 | 13535 | | |
13392 | 13536 | | |
13393 | 13537 | | |
| 13538 | + | |
| 13539 | + | |
| 13540 | + | |
| 13541 | + | |
| 13542 | + | |
13394 | 13543 | | |
13395 | 13544 | | |
13396 | 13545 | | |
| |||
0 commit comments