Repository navigation
Description
Split out by the triage seat from #13651 (ask 2 of 2). That card's two asks are independent — its own words: "They are independent." Ask 1 (make the silent lowering failure loud) is a packages/cli repair and is now queued on #13651 alone. This card carries ask 2, which is a contract-surface expansion and therefore a maintainer decision, not a queue item.
Restart-when: #13651 closes (ask 1 lands and reports the count of constants an app cannot reach)
⛔ Filed unassigned. The measurement and the evidence table below are quoted from #13651; nothing here is a new reading.
The gap
An L2 hook handler is lowered to a metadata-only body.source and evaluated inside QuickJS with no module scope. A handler body referencing an import, a top-level const, or even the factory function's own parameter is a ReferenceError at runtime.
That boundary is deliberate and #13651 explicitly does not ask to cross it:
Reaching module scope at runtime. The body-only sandbox is a deliberate boundary and the app is not trying to cross it — the ask is that the boundary be enforced at author time instead of discovered at review time, and that a legitimate constant not have to be smuggled across it by hand.
What is missing is a declared way to ship a constant with the body.
What the absence costs, measured in the reference app
Every shared value is hand-copied into every handler body, and each copy carries a bespoke test whose only job is to notice drift:
| inlined copy | its pin |
|---|---|
the priority-rank map, twice (case.hook.ts, task.hook.ts) |
test/priority-rank-parity.test.ts |
the 16-cell SLA matrix (case.hook.ts, duplicating _case-sla.ts) |
test/case-sla-matrix.test.ts |
the country-to-territory table (account.hook.ts, duplicating _territory.ts) |
test/territory-single-source.test.ts — parses the table out of the LOWERED body |
the refuse() refusal envelope, in every guard (duplicating _refusal.ts) |
test/refusal-envelope.test.ts |
| the campaign metric recompute, four times | test/campaign-member-lifecycle.test.ts — asserts all four are character-identical |
the position-pool names, twice (_case-assignment.ts) |
test/case-assignment.test.ts |
the claimable-status set (_case-assignment.ts) |
test/unassigned-case-triage-reach.test.ts |
| the reference-cleanup shape predicate, three times | test/freeze-guard-reference-cleanup.test.ts |
⇒ Eight rows of the same shape: a constant declared once, stored N times, and a test that exists only to notice when the copies drift. That is a platform gap being paid for in app-side test infrastructure — and the app must explain itself at each site, because the duplication otherwise reads as gratuitous to the next author.
⭐ The same shape shows up independently in #13644, whose reference-cleanup shape predicate is three verbatim copies for this exact reason ("hook bodies run body-only and cannot reach module scope, so the app also carries a drift pin asserting the three copies stay byte-identical"). Two unrelated cards paying the same tax is the signal that it is structural.
Shapes named in #13651 (neither costed)
- a
constants:key on the hook, whose object is serialised into the lowered body; - an author-time inliner that resolves a whitelisted module-scope
constat lowering time.
⛔ No recommendation between them is carried over — #13651 named them as shapes, not as a design.
Why this is a decision and not a queue item
Both shapes widen a published contract (packages/spec, the hook declaration surface), so the manual floor applies (功能新增 / 协议契约变化). Landing would additionally run at the Clause-② contract-review tier.
Refs: #13651 (parent, ask 1, queued domain:cli) · #13644 (an independent card paying the same tax) · objectstack-ai/hotcrm#1184 (where the evidence table was measured).
⚙️ Bookkeeping, 2026-09-08 — the Restart-when: line above transcribes the sequencing note this card already carried; it invents nothing and it moves nothing. The wake condition was stated in prose ("Decide this one after ask 1 has landed and reported its numbers"), so the unlock sweep could never reach it. A pm:on-hold card with no machine-readable wake condition is illegal under the state model (「无机制可唤醒的卡 ⛔ 不 hold」) — this one was one line short of legal, not wrongly held.
Checked with the repo's own predicate rather than a hand-rolled regex: directiveValues(body, 'Restart-when') from scripts/pm/check-half-states.mjs returns the value above, and .some((v) => !/^manual\b/i.test(v)) is true ⇒ it is not the illegal manual — <reason> escape hatch. The card reference is written first in the value so a card-reference extractor finds #13651 without having to parse the parenthetical.
Restart-when: is the automatic wake; it does not fence the maintainer, who may rule this at any time. Added by the domain:spec execution seat, session session_016N6xmWt5hYm94ffVEwGH8x. ⛔ No pm state changed, no label moved, nothing closed.
Activity
huangyiirene commentedon Aug 31, 2026
裁决:缓裁 —— 带前提停放,#13651 ask-1 出数后滚回决裁批(维护者 2026-08-31)
项目总监席 · session
session_01KGtaLpkW1mycWgkbSb3H6t· 决裁批 #19 ③维护者原话(逐字):「批 #19 同意」—— 本卡呈报推荐为「缓裁」。
裁决内容(第三档带前提裁决)
ctx.referentialFieldClearon HookContextSchema — apps can only tell the engine'sset_nullcleanup write from a user edit by reading the operation-private__referentialFieldClear, or by sniffing write shape #13644 独立付了同一笔,两张不相关的卡撞出同一形状),方向上平台该有此通道;constants:键 / 作者期白名单 inliner)均未估价,而估价输入正是 Hook-body lowering fails OPEN:tryExtractBodyswallowsextractHookBody's refusal into a warnings array and bundles the closure — make it loud (ask 1; the constant-surface half is split out) #13651 ask-1(降低失败响亮化)落地后报出的「不可达常量」人口数 —— 今天该人口不可见不可数;tryExtractBodyswallowsextractHookBody's refusal into a warnings array and bundles the closure — make it loud (ask 1; the constant-surface half is split out) #13651 ask-1 落地并报数 ⇒ 本卡滚回下一决裁批,两形状带成本呈报择一;状态转移(同笔)
needs-user-decision→pm:on-hold;domain:spec不动。解冻触发 = #13651 ask-1 的数字上卡。Generated by Claude Code
fix(pm): H9's issue-ref class requires the reference at the declarati…
objectstack-fleet commentedon Sep 23, 2026
关
not_planned—— 维护者逐张复核 on-hold 卡时选了「甲」:方向记录在此,不再挂一个永远等不到的前提分诊席(
session_01Tw7jnJinGHvoGSi8aFkhPJ),2026-09-23T10:06Z。维护者 2026-09-23 在分诊会话里逐张复核pm:on-hold卡。本卡在第七组呈报了三个选项(甲 关闭并记录方向 / 乙 不等计数现在定形状 / 丙 维持),推荐甲;维护者对这一组的回复原文:「其他同意」。保留的方向(不因关闭而作废)
2026-08-31T13:43Z 的裁决(
5479239204)原样有效:constants:键 / 编写期白名单内联器)都没有定;为什么关
Restart-when:写的是「Hook-body lowering fails OPEN:tryExtractBodyswallowsextractHookBody's refusal into a warnings array and bundles the closure — make it loud (ask 1; the constant-surface half is split out) #13651 关闭(ask-1 落地并报出计数)」。Hook-body lowering fails OPEN:tryExtractBodyswallowsextractHookBody's refusal into a warnings array and bundles the closure — make it loud (ask 1; the constant-surface half is split out) #13651 今天已读不到(404),这个触发永远不会发生。origin/mainc1dfa5241b上packages/cli/src/lint/hook-body-lowering.ts把「悄悄降级」变成了一条 lint 判定,作者现在会被响亮地告知。缺的只是在参考应用上跑出来的那个数 —— 本席够不着那个应用。重开条件
任一满足即带着证据重开,回决裁批按上面的裁决择一形状:
ctx.referentialFieldClearon HookContextSchema — apps can only tell the engine'sset_nullcleanup write from a user edit by reading the operation-private__referentialFieldClear, or by sniffing write shape #13644 之外,又有一个应用独立付出同一笔「抄常量 + 盯抄写测试」的税。关闭理由:
not_planned,同时摘掉pm:on-hold。Generated by Claude Code