Uh oh!
There was an error while loading. Please reload this page.
docs(objectql): record the deliberate lock-face/static-face asymmetry, and pin both faces - #14690
Conversation
…th faces `isCallerSuppliedValue`'s docblock claimed the readonlyWhen seam runs "the identical two-part test `stripReadonlyFields` applies" and is "textually parallel" with it. Neither has held since #14088 moved the static face onto a hook-write record and #14472 did the same to the insert-side twin. The divergence is deliberate and lock-motivated, so it is now stated where the test lives, with the reason each face guards a different thing. Adds the static face's missing measurement pin (a lone self-assigning hook leaves the CALLER value on the key) beside #14088's own suite, and cross-links it with `LOCK 3b`, which pins the opposite verdict for the identical hook spelling. Comment- and test-only; no behaviour moves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…ck-face-asymmetry-recorded
📓 Docs Drift Check
What this run could not see
Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-musk
commented
Sep 2, 2026
Landing provenance (engine execution seat, session
⭐ The substantive correction this PR carries is to the ruling's own parenthetical. Ruling B said the static face was "already pinned"; measured, the existing
Owed by the seat at MERGED: verify by content on Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
…14693) Three passages in `engine-hook-provenance-sibling-seams.test.ts` still said the `readonlyWhen` sibling seam was "WITHHELD pending a maintainer ruling" and went "to the decision inbox, never resolved here". It was ruled — #14259, maintainer ruling B: that seam keeps value equality and the asymmetry is deliberate — and #14690 landed the corrected `isCallerSuppliedValue` docblock plus a pin on each face. A landed PR now routes readers straight into the sentence that contradicts the ruling. Comment-only: every assertion, test name and fixture is untouched. The prose is replaced with a pointer to where the argument now lives rather than a fourth copy of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
Fixes#14259
Seam 1 residual only. Seam 2 landed as
00ff228fe(PR #14472) with aPart ofline; the card was held open for this half.No behaviour moves. Every changed line in
packages/objectql/src/validation/rule-validator.tsis a comment line, mechanically verified — the diff of that file with comment lines filtered out is empty. The other two files are tests. This is an S card: it writes down a rule that already holds.What the maintainer ruled (decision batch #12 item 4,
#14259comment 5511947941)B. Seam 1 (
isCallerSuppliedValue, behindstripReadonlyWhenFields/stripReadonlyWhenFieldsMulti) stays on value equality. On areadonlyWhenstate lock, a before-phase hook assignment that writes back the value already on the key —data.x = data.x, or a normalisation that is the identity for canonical input — does not count as a hook write: the caller's value is stripped, with the existing warning and the sameonFieldsDropped/strictReadonlyWritesreporting.LOCK 3bstands as the pin of that rule. The divergence from the provenance-based static face is recorded as a deliberate, lock-motivated asymmetry, and each face carries a measurement pin. The residual (a hook deriving a locked field while the caller echoed the identical value loses its write) is accepted; no instance exists in the tree.The three things this PR is
1. The docblock correction
The false sentence, quoted verbatim from
origin/mainbefore this PR:...and, four paragraphs up in the
ReadonlyWhenStripOptionsdocblock the above@links to, the same claim stated more strongly:Both were true when written and neither has held since #14088 moved
stripReadonlyFieldsonto a record of the keys the before-phase hook chain assigned (options.hookWrittenKeys), asked ahead of the value comparison. Both are corrected in place to state the asymmetry and its reason, which is what each face guards:stripReadonlyFields, and its insert-side twinstripRuntimeOwnedFields(fix(objectql): decide the insert-side runtime-owned strip by hook-write provenance #14472) — guards an author-declaredreadonlyor a runtime-owned column. Hook authorship is the exemption on offer, so "the chain assigned this key" is the right evidence, and the record's blindness to the value is precisely what makes it correct: it separates the hook wrote the null the caller also sent from the hook never touched the key.readonlyWhenstate lock, whose entire guarantee is that no caller write survives a TRUE predicate (Parent-scopedreadonlyWhenis unenforced server-side — the field lock fails open, so a paid invoice's frozen lines can be rewritten over the API #4889's frozen paid-invoice lines). That same blindness would let the innocuous line above hand the caller's own value hook ownership and silently unlock the lock. Measured, not feared: threading the record into this predicate turnedLOCK 3bred on fix(objectql): decide the insert-side runtime-owned strip by hook-write provenance #14472's branch.The second correction is inside the same
isCallerSuppliedValueregion and is the same claim; leaving it would have left the docblock still asserting the falsehood this card exists to retire.2. The two face pins, written to be read side by side
LOCK 3binengine-readonly-when-derived-writes.test.ts. Assertions unchanged; its comment now names the asymmetry, the reason, and the sibling pin that records the opposite verdict for the identical hook spelling.MEASURED: a lone self-assigning hook leaves the CALLER value on the key, instripReadonlyFieldsusesObject.isto tell a hook write from a caller write, so a hook cannot clear a readonly field the caller also sent as null #14088's own suiteengine-readonly-strip-caller-values.test.ts.Why a new pin rather than a reference to an existing one. The card asked me to check whether
engine-hook-provenance-sibling-seams.test.tsalready carries this. It carries a case with that exact name — but that suite isseam 2 — the insert-side runtime-owned strip reads PROVENANCE, its subject isengine.inserton anautonumbercolumn, and it pinsstripRuntimeOwnedFields. That is a different face from the update-side staticreadonlyseamstripReadonlyFieldsthe ruling names. Verified: nothing inengine-readonly-strip-caller-values.test.tscovered the self-assignment input, andhook-write-provenance.test.tsunit-tests the recorder rather than a strip face. So the update-side static face genuinely had no pin, and now has one.Both new comment blocks state that a future ruling reversing this inverts the pins rather than deleting them.
3. One
patchchangeset.changeset/lock-face-value-equality-recorded.md.isCallerSuppliedValueandReadonlyWhenStripOptionsare both package-internal and unexported, so neither docblock reachesdist/*.d.ts; the changeset is a plain patch, no ADR-0087 marker owed (not breaking).Verification — all on the final merged head
da2dc1967pnpm --filter @objectstack/objectql test— 263 files / 4553 tests passed.pnpm --filter @objectstack/objectql typecheck— green; verdict linecheck:test-typecheck: OK — @objectstack/objectql's test layer compiles under packages/objectql/tsconfig.test.json. The test layer is compiled by a siblingtsconfig.test.json, so the two edited test files really are type-checked.pnpm lint— full repo,eslint . --no-inline-config, exit 0. No narrowing claimed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(identical before and after theorigin/mainmerge, 38 commands). 34 green. Four exit 3, each printing its ownPREREQUISITE NOT MET/ NOT MEASURED verdict rather than a finding, all needing a full workspace build or a savedturbo run testlog this container cannot produce:check-test-completeness,check:dual-build-cjs-loads,check:type-check-debt,check-half-states. Recorded as NOT MEASURED, not as green and not as red. Exit codes were captured by redirecting to a file first, never through a pipe.Reverse verification of the new pin
Committed first, then mutated, with an absolute-path
traprestore. The pin's hook was retargeted fromcompleted_attotitleso no hook write is recorded for the locked key. Mutation proved on disk before running (target occurrences 1 to 0, injected 1, blob986d3b0atoeb33cb5e). Predicted direction RED; observed RED:That is the stored
STAMPEDtimestamp surviving — the strip taking the caller's forged value away once the self-assignment stops being recorded. The pin measures the provenance seam rather than restating a tautology. Restore proved byte-exact afterwards: blob back to986d3b0a,git diff HEADempty,git status --porcelainempty.Scope
packages/objectql/src/engine.tswas not touched — it is held in flight by another dispatch (#14399).origin/mainwas merged immediately before opening this PR; #14311'scheckStateMachinechange landed around line 2180 of the same file, a different region, and merged cleanly. The full objectql suite and the gate family were re-run afterwards, on the merged head.One stale-prose finding in
engine-hook-provenance-sibling-seams.test.tsis filed separately rather than fixed here; it is named in the report comment on #14259.🤖 Generated with Claude Code
Generated by Claude Code
Generated by Claude Code