Uh oh!
There was an error while loading. Please reload this page.
feat(spec,objectql,runtime): declare ctx.referentialFieldClear on HookContextSchema, populate every set_null cleanup write, and carry it across the QuickJS sandbox boundary - #14199
Conversation
…kContextSchema, populate on every set_null cleanup write, carry across the QuickJS sandbox boundary (WIP)
…rface + hook.mdx; changeset
…rtions (census --fix)
… index-based envelope-consistency assertions
…ferential-field-clear
…ng; keeps the test-typecheck ratchet at its ledgered count)
📓 Docs Drift CheckThis PR changes 4 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ea363f033874f43918c13e0f1d6e8714fe3782ee && git checkout ea363f033874f43918c13e0f1d6e8714fe3782ee
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4d672c4ffe15b1fe2365e5efbf7251778f2c3e33 1108016ddda0e77ef797776bd9f8a2d146e4309f && git checkout -B drift-repro 4d672c4ffe15b1fe2365e5efbf7251778f2c3e33 && git merge --no-ff 1108016ddda0e77ef797776bd9f8a2d146e4309f
node scripts/docs-audit/affected-docs.mjs --json 4d672c4ffe15b1fe2365e5efbf7251778f2c3e33
|
Uh oh!
There was an error while loading. Please reload this page.
Same mechanism as the previous merge: main's #14199 re-anchored 11 rows of this machine-maintained table while this branch re-anchors row 21, and a textually clean merge proves nothing about the line numbers. Resolution per the tool: took main's page wholesale, re-derived with check-system-context-census --fix on the merged tree — exactly ONE anchor rewritten (row 21, protocol.ts 1737 to 1741; re-derived, not carried — main has zero commits on protocol.ts, so the insertion shift is unchanged). Delta vs origin/main is that single line; bare gate green (109 sites, 145 anchors). engine-double-contract ledger: zero main-side commits, gate OK on the merged tree — no regeneration owed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5WBDtaUnoz5XuJ6jk8pQ5
Fixes#13644
Executes the 2026-08-31 adoption ruling (decision record: issue comment 5478756633): declare a first-class
ctx.referentialFieldClearonHookContextSchema, populated by the engine on every reference-cleanup write (deleteBehavior: 'set_null'cascade), and carried across the sandbox boundary by contract — with BOTH mandated conditions landed as executable pins in this same PR.Clause-②: yes — published contract-surface widening (one new optional, engine-produced key on
HookContextSchema). Parked for the contract-review tier per the ruling; no enqueue from this seat.Freshness gate honoured
Designed from the filer's correction comment (5474384377), not from the body's "Measured on 17.1.0" table (a rig artifact, per the ruling's own record). The engine builds the cleanup write's context by spreading the caller's envelope and adding the transaction handle and the operation-private marker — it inherits whatever identity the caller supplied — so on the path a real request takes (a REST DELETE carrying a userId),
ctx.user,ctx.sessionandctx.inputare identical between the engine's cascade and a user's hand-clear of the same lookup. Consequences taken in this PR:What landed
packages/spec/src/data/hook.zod.ts:referentialFieldClear(boolean, optional) declared onHookContextSchema, placed with the other server-stamped write metadata (besideprovenance). The doc block carries the one-fact-two-faces relationship to the operation-private key, the sandbox-carry contract, and the trust model (server-derived, never client-supplied). Parse pins added inpackages/spec/src/data/hook.test.ts— load-bearing because this schema STRIPS undeclared keys on parse, so without the declaration a parsed context would silently lose the key.packages/objectql/src/engine.ts: newbuildReferentialFieldClear(sibling ofbuildSession/buildProvenance) projects the operation envelope's private marker onto the declared key atupdate()'s hook-context assembly — a single site that covers both phases and the per-row fan-out, because the after phase reuses the assembled context and the per-row contexts spread it. Conditional spread keeps the key truly absent (never present-but-undefined) on every other write. The 属主守卫与级联 set_null 冲突:非特权删除 sys_user 时 owner_id 级联置空被 #3004 守卫拦截(级联中途失败) #3023 cascade-site comment now names the projection.packages/runtime/src/sandbox/script-runner.ts(ScriptContext.referentialFieldClear, documented),body-runner.ts(buildSandboxContextcopies it, hook face only, only in its declared shapetrue),quickjs-runner.ts(installCtxinstalls it on the VM ctx).packages/core/src/security/operation-private-keys.tsheader now names the declared projection, so the operation-private registration cannot be misread as the app-facing channel. No behavior change, no changeset..changeset/hook-ctx-referential-field-clear.md(spec minor / objectql minor / runtime minor), citing the ruling.content/docs/references/data/hook.mdxregenerated (gen:docs);content/docs/permissions/system-context.mdxre-anchored by the census gate's own--fix(pure line rot from the engine.ts insertions, 14 + 5 anchors, verified OK after).Mandated condition 1 — sandbox reachability, pinned from inside the VM
packages/runtime/src/sandbox/referential-field-clear-signal.integration.test.ts— realObjectQL+ realSqlDriver(better-sqlite3) + realQuickJSScriptRunnerbehindhookBodyRunnerFactory, the same wiringAppPluginperforms. Every assertion lands on what the body OBSERVED (reported out through thelogcapability):ctx.referentialFieldClear === trueinside the VM, in both phases, with the inherited caller identity present alongside;typeofreads'undefined'), so the guard idiom reads false.This is deliberately NOT a kernel-rig read — the ruling names the #11552 declared-vs-observable family as the reason sandbox reachability is a condition of adoption, and the operation-private spelling is exactly what a kernel rig could read and a shipped body could not (
buildSandboxApimay hand a body an object shim with noexecutionContextat all).Mandated condition 2 — populate surface, pinned write site by write site
New describe in
packages/objectql/src/engine-cascade-delete.test.ts:set_nullclear:truein both phases, caller identity inherited, and the hand-clear control has NO key at all (absence asserted with anincheck, not just an undefined read);multiple: truemember removal — the second cleanup write site incascadeDeleteRelations— same assertions against its own hand-edit control;trueexactly where the envelope carries it, write for write (three ops asserted in order: hand-clear, restore, cleanup).Derived judgments — each stated, none silently decided
trueor omits the key, neverfalse.__referentialFieldClearremains the engine/middleware authorization channel — plugin-security's ownership-anchor exemption reads it off the operation context before any hook runs — and theoperation-private-keysstripping rule is untouched. The declared key is a read-only projection, not a replacement; no dual-write transition is needed because the two faces have one producer site each and the projection is derived at assembly. The governance ledger needed no mechanical change (comment updated only).ctx.apiDURING a cleanup dispatch runs on the same inherited envelope and therefore carries the marker too — this is the semantics the operation-private key has always had (and what plugin-security keys on), projected rather than quietly re-scoped. If a strictly per-write-site signal is ever wanted instead, that is a contract decision to take explicitly, not a default this PR assumes.update()'s assembly is the complete populate surface. The find/insert/delete assemblies deliberately do NOT project the envelope — a delete or insert is never itself a reference-cleanup write, and advertising the envelope there would widen the key's meaning without a pull.check:adr-0087-registrationgreen.Tests and gates — real readings, final tree
All exit codes captured redirect-first (never through a pipe); heavy runs serialized through
scripts/pm/os-verify-lock.sh. Final union re-run at HEAD1108016dd(post-merge of origin/main, census deferral discharged):src/data/hook.test.ts: 79 passed (79) — includes the two new parse pinssrc/security/operation-private-keys.test.ts: 8 passed (8)src/engine-cascade-delete.test.ts+src/hook-input-shape-contract.test.ts: 35 passed (35)src/sandbox/referential-field-clear-signal.integration.test.ts: 1 passed (real QuickJS; earlier full sandbox sweep also green:perrow-dispatch-signal.integration+body-runner+quickjs-runner= 101 passed (101))check:test-typecheckratchet held at its ledgered count — one new TS2345 was introduced by the suite and fixed rather than ledgered). Note runtime'stsc --noEmitexcludes test files by its tsconfig (pre-existing), so the new integration test is proven by execution, not by tsc.check:dev-prereqsgreen after the post-merge spec rebuild ("built from the sources on disk");pnpm check:dual-build-cjs-loadsgreen (93 self-test cases + ledger)pnpm --filter @objectstack/spec check:generated: one stale artifact (check:docs) regenerated via--fix; full re-run green.check:authorable-surfacegreen (data shard gained the one declared row)pnpm check:type-check-debton the merged tree: "27 ledger entries re-measured, 1217 raw tsc errors total, none above its recorded number; surplus: none" — nothing raisednode scripts/pm/dispatch-gates.mjs, no hand-fed paths, re-derived after fetching origin/main): all derived local gates green, includingcheck:system-context-census(line-rot re-anchored via its own--fix, verified "109 sites, all anchored"),check:engine-split-ratio(after the gate's own deepen remedy),check:corpus-claim-drift,check:doc-anchors,check:kernel-hook-pairs,check:cross-package-test-inputs,check:test-source-alias,check:durability-log-level, and the spec-scoped family (docs/liveness/strictness-ledger/llms-txt/skill-refs/variant-docs/yaml-examples/empty-state/skill-examples)node scripts/check-test-completeness.mjs: exit 3 PREREQUISITE NOT MET — by its own text the local reading is NOT MEASURED (it grades a saved turbo test log CI produces); left to CIcheck:nul-bytesgreen (7751 files scanned)Out of scope: #13331 remains open and untouched by this PR (its branch carries no commits; its lane escalated to a maintainer decision independently of this card).
Generated by Claude Code
Generated by Claude Code