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.
⛔ 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
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).
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/clirepair 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.⛔ 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.sourceand evaluated inside QuickJS with no module scope. A handler body referencing an import, a top-levelconst, or even the factory function's own parameter is aReferenceErrorat runtime.That boundary is deliberate and #13651 explicitly does not ask to cross it:
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:
case.hook.ts,task.hook.ts)test/priority-rank-parity.test.tscase.hook.ts, duplicating_case-sla.ts)test/case-sla-matrix.test.tsaccount.hook.ts, duplicating_territory.ts)test/territory-single-source.test.ts— parses the table out of the LOWERED bodyrefuse()refusal envelope, in every guard (duplicating_refusal.ts)test/refusal-envelope.test.tstest/campaign-member-lifecycle.test.ts— asserts all four are character-identical_case-assignment.ts)test/case-assignment.test.ts_case-assignment.ts)test/unassigned-case-triage-reach.test.tstest/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)
constants:key on the hook, whose object is serialised into the lowered body;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).