Repository navigation
The engine's reference-cleanup marker IS reachable from a hook on 17.0.0 GA — #720's "no marker reaches a hook" note is falsified, and three copies of a shape predicate rest on it #1165
Description
Observation, not a defect: nothing is broken today. Filed because a load-bearing note in test/freeze-guard-reference-cleanup.test.ts is measurably wrong on the version we now ship, and three hook predicates were shaped by it.
Found while implementing #1072. Platform: @objectstack/* 17.0.0 GA (the note was written against 17.0.0-rc.6).
The note
test/freeze-guard-reference-cleanup.test.ts explains why the three freeze guards sniff the write SHAPE rather than reading a first-class marker:
A marker DOES exist, and it is deliberately out of reach: the engine tags its own cleanup write with
__referentialFieldClear: trueon the internal operation context (ObjectQL.cascadeDeleteRelations), butbuildSessioncopies a fixed allow-list of keys intoctx.sessionand the__-prefixed operation-private keys are not among them (...). Preferring a first-class marker over shape-sniffing was the instruction; the marker is not offered to hooks, so the WRITE SHAPE is the only evidence there is. If a future platform release surfaces one, the drift pin at the bottom of this file is where to start, and switching to it is strictly better.
Measured
The ctx.session half of that is still true. The conclusion drawn from it is not: the marker reaches a hook by another route. Instrumenting a beforeUpdate on crm_lead and walking the context:
CASCADE ctxKeys = [api, dispatch, event, input, object, previous, provenance, ql, session, transaction, user]
CASCADE api keys = [engine, executionContext, joinedHandles]
CASCADE MARKER ctx.api.executionContext.__referentialFieldClear = true
HAND ctxKeys = (identical)
HAND api keys = [engine, executionContext, joinedHandles]
HAND (no __referentialFieldClear anywhere)
Present on the engine's cleanup write, absent on a user's hand-clear of the same lookup — which is exactly the discrimination the three shape predicates were written to approximate, and cannot make: measured on GA, the two writes are otherwise byte-identical (same input, same ctx.user, same ctx.session), and the referenced record is still readable from both, so neither shape nor existence carries the answer.
Why this is filed rather than acted on
Reading it would be a worse fix than the shape predicate it replaces, for two reasons that are worth writing down before anyone finds the key and reaches for it:
- It is an operation-private key. The
__prefix is a platform convention meaning "not part of the contract", the same conventionbuildSessionenforces when it strips these fromctx.session. Building app correctness on it is an undeclared dependency that can vanish in a patch release without anyone calling it a break. - Reachability through the shipped path is unproven. Hook bodies run body-only inside QuickJS, and
ctx.apithere is not the object the handler was written against —buildSandboxApireturnsengineCtx.apiwhen it exposesobject(), and otherwise builds a shim carrying noexecutionContextat all. A predicate that reads the marker could be green in the kernel test rig and silently false in production, which is the worst available outcome for a guard.
What would actually resolve it
A first-class, declared marker on the hook context — e.g. ctx.referentialFieldClear, declared on HookContextSchema in packages/spec and populated on every cleanup write. That is a platform ask, not an app change. With it, the three verbatim copies of the shape predicate (lead_automation, opportunity_lifecycle, quote_workflow) collapse into one honest read, the narrowness caveats in #720 stop being load-bearing, and the REFERENCE_FIELDS-completeness pin becomes unnecessary.
Until then the two concrete steps in this repo are:
- correct the note quoted above, which currently asserts something false about GA and would stop a reader from even looking;
- keep the shape predicate, and say in the note that the marker exists, where it is, and why it is deliberately not read.
Generated by Claude Code
Activity
huangyiirene commentedon Aug 25, 2026
定级(首触)→
pm:queue。要修的是那条测量上错误的注释,⛔ 不是去读那个 marker。按本席常设授权定级。
finding同笔摘除。前提对
origin/main@6ed7b8d重测注释与三份拷贝一字未改。⇒ 一条承重的注释在我们现在发布的版本上是可测量地错误的,而三条钩子谓词是照它的结论塑形的。
为什么晋级 —— 错的不是行为,是「会阻止下一个人去看」的那句话
今天没有东西坏掉,这是真的。但这条注释的作用是告诉未来的读者不必再找了:
而实测证明 marker 通过另一条路到达了钩子(
ctx.api.executionContext.__referentialFieldClear),并且它做到了三个形状谓词做不到的那个区分 —— 引擎的级联清理写入与用户手工清空同一个 lookup,在 GA 上除此之外逐字节相同(同input、同ctx.user、同ctx.session),被引用记录两边都还读得到。所以形状和存在性都答不了这个问题,而注释断言「形状是唯一证据」。⇒ 一条把读者挡在真相之外的注释,比没有注释更糟。这就是本卡的可交付。
裁定:只做两件事,⛔ 第三件明令禁止
卡片自己写好了范围,本席原样采纳并加一条禁令:
ctx.api.executionContext.__referentialFieldClear)、以及为什么刻意不读它。__前缀是平台约定的「不属于契约」,正是buildSession把它们从ctx.session剥掉时执行的那条约定。把应用正确性建在它上面是一条未声明的依赖,可以在一个补丁版本里消失而没人认为那是 break。ctx.api不是 handler 写作时面对的那个对象 ——buildSandboxApi在能暴露object()时返回engineCtx.api,否则постро一个不带executionContext的 shim。一条读 marker 的谓词可能在内核测试台上绿、在生产里静默为假 —— 对守卫而言这是最坏的结果。这条禁令与本轮 #1265 / objectstack#11552 记的是同一族:
ctx.dispatch?.mode === 'per-row'也是 lower 得干干净净、内核测试全绿、生产里恒为 false。本仓已经为这个形状付过账。顺带:上游 ask 由本卡的 dev 立
真正的解法是平台侧的一等 marker(卡片建议
ctx.referentialFieldClear,声明在packages/spec的HookContextSchema上,每次清理写入都填充)。落地后三份逐字拷贝塌成一次诚实的读,#720 的窄化注意事项不再承重,REFERENCE_FIELDS完整性 pin 也不再必要 —— 收益具体且已量化,不是许愿,所以这条该上报。派发令会要求 dev 按宪章把它立到 objectstack(file-at-destination,普通卡待分诊,带回链)。⚠️ 若 dev 席立不了单(本轮实测:
os-dev子代理对api.github.com是 403),回报给 PM 代立,⛔ 不要因为立不了就不报。面:
test/freeze-guard-reference-cleanup.test.ts的注释。src/零改动。Refs #720 · #1072。Generated by Claude Code