Skip to content

Give a lowered hook body a declared constant surface — every shared constant is currently hand-copied into each handler and pinned by a bespoke drift test (8 rows in the reference app) #13718

Closed as not planned

Description

@os-warren

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)

  1. a constants: key on the hook, whose object is serialised into the lowered body;
  2. an author-time inliner that resolves a whitelisted module-scope const at 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.

⚠️ Sequencing note from triage: #13651's ask 1 is the prerequisite in practice, not just in priority. Today the lowering failure is silent, so an app cannot rely on the build to tell it which constants are unreachable. Ask 1 makes that population visible and countable — which is also the input needed to size this card honestly. ⇒ Decide this one after ask 1 has landed and reported its numbers, unless the maintainer wants to rule the shape earlier.

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.

⚠️ The escape clause in the sequencing note stands and is deliberately not encoded: "unless the maintainer wants to rule the shape earlier". 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

  1. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Aug 31, 2026
  2. huangyiirene commented on Aug 31, 2026

    @huangyiirene
    Collaborator

    裁决:缓裁 —— 带前提停放,#13651 ask-1 出数后滚回决裁批(维护者 2026-08-31)

    项目总监席 · session session_01KGtaLpkW1mycWgkbSb3H6t · 决裁批 #19 ③

    维护者原话(逐字):「批 #19 同意」—— 本卡呈报推荐为「缓裁」。

    裁决内容(第三档带前提裁决)

    状态转移(同笔)

    needs-user-decision → pm:on-hold;domain:spec 不动。解冻触发 = #13651 ask-1 的数字上卡。


    Generated by Claude Code

  3. added
    pm:on-holdDecision made, deliberately deferred — no dispatch, no nag; restart condition in the hold comment
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 31, 2026
  4. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    Contributor

    关 not_planned —— 维护者逐张复核 on-hold 卡时选了「甲」:方向记录在此,不再挂一个永远等不到的前提

    分诊席(session_01Tw7jnJinGHvoGSi8aFkhPJ),2026-09-23T10:06Z。维护者 2026-09-23 在分诊会话里逐张复核 pm:on-hold 卡。本卡在第七组呈报了三个选项(甲 关闭并记录方向 / 乙 不等计数现在定形状 / 丙 维持),推荐甲;维护者对这一组的回复原文:「其他同意」。

    保留的方向(不因关闭而作废)

    2026-08-31T13:43Z 的裁决(5479239204)原样有效:

    • 不是否决:钩子代码在沙箱里够不到共享常量,只能每个处理函数各抄一份、再各配一个测试盯着抄写(参考应用里 8 行),这被承认为结构性缺口,平台方向上该有这个通道;
    • 两个候选形状(钩子上的 constants: 键 / 编写期白名单内联器)都没有定;
    • 分叉禁令照旧:以后重开时,两个形状带成本一起呈报择一,⛔ 任何席位不得先实现其一或借「文档化 workaround」预置其一。

    为什么关

    重开条件

    任一满足即带着证据重开,回决裁批按上面的裁决择一形状:

    关闭理由:not_planned,同时摘掉 pm:on-hold。


    Generated by Claude Code

  5. removed
    pm:on-holdDecision made, deliberately deferred — no dispatch, no nag; restart condition in the hold comment
    on Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:specDomain lane — seat card indexed by label:pm:seat

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions