Uh oh!
There was an error while loading. Please reload this page.
test(objectql): pin the case that defines a script rule as an invariant - #14906
Merged
Conversation
…iant
`checkPredicate` evaluates a `script` / `cross_field` condition against
`ctx.merged` on every write, with no exemption for a violation that was
already stored. A row that already violates is therefore refused on any
edit until a repairing write lands — frozen, not bricked — and that is
exactly what separates a `validations[]` invariant from a
`Field.requiredWhen` transition gate, whose ADR-0113 exemption is pinned
at `rule-validator.test.ts:65` ("legacy rows rest: a pre-existing
violation does not block an unrelated write").
The `script / cross_field predicates` block pinned only the write that
carries the offending value, the write that makes the predicate false,
and the un-evaluable predicate. The distinguishing case — an
unrelated-field write against an already-violating row — had no pin, so
the two mechanisms' boundary had asymmetric coverage.
Two `it` cases, no behaviour change:
- unrelated-field write over a violating prior row is refused, asserted
on the envelope (`VALIDATION_FAILED`, `_record` / `rule_violation`,
authored message) rather than a bare `toThrow`;
- the repairing write passes — frozen, not bricked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Sep 3, 2026
os-musk
marked this pull request as ready for review
September 3, 2026 16:18
os-musk
enabled auto-merge
September 3, 2026 16:18
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#14891
Two
itcases, no behaviour change.packages/objectql/src/validation/rule-validator.tsis untouched — the engine already does the right thing; what was missing was the pin for the case that defines it.Why this case, and not any of the three already there
checkPredicateis handedctx.merged(the prior row overlaid with the incoming patch) on every write, with no exemption for a violation that was already stored. A row that already violates is therefore refused on any edit until a repairing write lands: frozen, not bricked. That is exactly what makes avalidations[]scriptrule an invariant rather than a transition gate.The mirrored case on the gate side is already pinned:
packages/objectql/src/validation/rule-validator.test.ts:65— "legacy rows rest: a pre-existing violation does not block an unrelated write" (the ADR-0113 exemption onField.requiredWhen).The
script / cross_field predicatesblock pinned only the write that carries the offending value (:1813), the write that makes the predicate false (:1824), and the un-evaluable predicate (:1838). The distinguishing case had no pin, so the boundary between the two mechanisms had asymmetric coverage: the gate's exemption was pinned, the invariant's absence of one was not.What landed
The refusal case asserts the envelope —
VALIDATION_FAILED, andfieldsexactly[{ field: '_record', code: 'rule_violation', message: the authored message }]— rather than a baretoThrow, which here would also be satisfied by an unrelated fault.Reverse verification (ablation)
Committed first, then
checkPredicatewas mutated to givescriptrules therequiredWhen-style exemption (skip when the predicate was already TRUE overprevious), i.e. the exact erasure of the boundary these pins exist to notice:Exactly the new frozen pin turns red; the three pre-existing cases in the block stay green, which is the measurement that shows the gap was real. Mutation proved on disk (marker count 1, blob hash moved
0cc67e6f->a6766298); restore proved against the HEAD blob (hash back to0cc67e6f, marker count 0,git diff HEADempty), and the restored tree re-runs 188 passed.Two readings from the card that did not hold
8bcc348beonorigin/main). It touches no predicate evaluation, so there was nothing to land against.requiredWhen(与字段级 min/max)是转换门,validations[]script 是真不变量 —— 边界是刻意的、已被双驱动实测,但平台没有任何一处教它 #13879's contract text has NOT landed. The card and the dispatch both state the invariant wording now ships frompackages/spec/src/data/validation.zod.ts. Onorigin/mainat84b8190aeit does not: thescriptrule'sconditiondescribe string still reads'Predicate (CEL). If TRUE, validation fails. e.g. P\record.amount < 0`', and neither "invariant" nor "transition gate" appears anywhere in the module docblock. #13879 is an **open issue** with an unmerged branch (claude/issue-13879-required-when-transition-gate-docblocks). Nothing here depends on that text — the behaviour was driven directly rather than taken from the spec — but the published-claim-versus-pin framing in the card is a statement about a branch, not aboutmain`, and the spec seat should know that.Gates
Union re-derived from the tree after the final commit and run at
90ed65f08:node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackderives 28 families (24 by path + 6 by change KIND, 2 shared). 25 green, 3 NOT MEASURED locally rather than red:check-test-completeness.mjs— exit 3, prerequisite: it parses a savedturbo run testlog, which only CI produces.check:dual-build-cjs-loads— exit 3, prerequisite: reads builtdist/for 56 packages.check:type-check-debt— needs the full workspace closure built. Declared narrowing: the closure build was started under the shared verify lock, then released to three queued peers rather than held for a 50-line test-only diff. Its substantive reading for this diff was taken directly instead (the--listFilesmeasurement above), and CI runs the ratchet unconditionally onpackages/**.pnpm --filter @objectstack/objectql exec vitest run src/validation/rule-validator.test.ts— 188 passed.pnpm --filter @objectstack/objectql typecheck— green, and the edited file is genuinely in the program:tsc -p tsconfig.test.json --listFileslistssrc/validation/rule-validator.test.ts, which reports 0 errors (so it carries no debt-ledger entry and any error it gains is red on arrival).node scripts/pm/check-governed-merges.mjs --teston the final file list — not a governed surface.node scripts/check-nul-bytes.mjs— clean.skip-changeset: the diff is one test file. Nothing is published from any released package.🤖 Generated with Claude Code
Generated by Claude Code
Generated by Claude Code