Repository navigation
[Decision] Is an authored handler EXPRESSION a supported dialect? 56 mirrors say function, 11 say string, and both appear in files that mirror the other #6182
Description
Blocked-by: #7344
Escalated by the domain:devx @ objectui execution seat (#5748), PM session session_019b5UBNMtTzKbVtZZGvFuxe, from #6152's measurement. ⛔ This cannot be read off the code — it is a product question about the authoring contract, and the seat is not guessing it.
RULED 2026-08-25 (maintainer, batch adjudication, verbatim 「同意」): Option A — the handler-expression string dialect is NOT a supported authoring form. The authorable form for actions is the declarative
ActionDefobject; runtime callbacks are the programmatic face. The question below is therefore answered; what remains is the sweep, which is #6124's (the residual left after PR #7339 is #7344's — see the 2026-09-02 comment). This card closes when that sweep lands and a re-verification shows the 11 mirrors no longer carry the barez.string()expression dialect — re-run thergat the bottom of this card on the merged ref.
The measurement
A census of every on* key mirrored in packages/types/src/zod/ found 73 mirrored handler keys in two incompatible dialects:
| dialect | count |
|---|---|
z.function() |
56 |
z.string() — a handler expression |
11 (onClick, onInstall, onPreview, onSave, onCancel, onBack, onViewChange, onChange) |
z.any() |
3 |
| named schema | 3 |
Why it needed a person
The two dialects make incompatible promises to an author:
z.function()cannot be satisfied by any serialized authored document. A JSON or YAML schema can never produce a function, so the key is declared-but-unauthorable — it exists for a runtime caller, not for a metadata author.z.string()says the opposite: an author may write a handler as an expression, and something resolves it.
⭐ objectui#4453 already narrowed one such key to typeof === 'function', so an authored string handler is silently DROPPED (packages/plugin-calendar/src/calendar-view-renderer.tsx:322-331). The ruling confirms that direction: the 11 z.string() mirrors are advertising a capability the runtime discards — the "declared but not enforced" shape this whole family exists to close, pointing the other way.
What it decided
⭐ It changes the remedy for a lane already in flight. #6152 ruled that 23 callback-shaped declared-but-unmirrored keys are runtime-only and non-authorable (reclassified, not mirrored). Under the ruling as issued:
z.function()is the contract ⇒ the 11z.string()mirrors are legacy to retire, and 121 declared-but-unmirrored keys across 16 schema pairs — the lane #6058's new UnmirroredDeclared ledger made visible #6152's 23 keys stay runtime-only, its ruling unchanged.- The alternative — offering an authored expression key beside each runtime callback, a materially larger piece of work — is refused.
- The rule is now stated where tools can read it (this ruling plus the finding(types): 28 zod-mirror keys are declared
z.function(), which NO JSON document can satisfy — 14 in-repo corpus files author them and are silently invalid #6124 ruling comment), so no future card re-derives the dialect from a 56-to-11 vote.
What is NOT being asked
⛔ Not asking anyone to pick a favourite. Not proposing a sweep here — the sweep is #6124's (residual: #7344), where the 11 mirrors get the same per-key treatment (renderer consumes a declarative object form ⇒ union with that form; otherwise runtime-only retirement). ⛔ #6152's option A is not blocked by this and should land on its own.
Reproduce (also the close condition — run it on the merged ref)
rg -n 'on[A-Z][A-Za-z]*:\s*z\.(function|string|any)' packages/types/src/zod/
Refs: #6124 / PR #7339 (the function-site sweep, landed 2026-09-02) · #7344 (the residual sweep this card is blocked on) · #6152 (where it was measured, and the lane it changes) · #6058 / PR #6149 (the ledger that surfaced it) · #4453 (an authored string handler is dropped) · #5155 (rejection lives at the zod/publish boundary) · #6122 / PR #6142 (no renderer reads 16 schema-level event slots).
Activity
os-zhuang commentedon Aug 25, 2026
Triage(决策箱勤务):补标准四棱卡面块 —— 卡内测量完整,此块为收件箱标准件。
① 项目长远合理性:#5155 已裁「拒收活在 zod/publish 边界」,所以镜像层就是作者契约的权威声明 —— 两种方言并存等于契约自相矛盾;裁定其一(或把「分面共存」写成工具可读的规则)都收敛,不裁则这一族每张后续卡都要从 56:11 的票数里重新猜。
② 实际业务拉动:11 个
z.string()镜像今天在广告一个 runtime 丢弃的能力 —— #4453 实测:作者写的 handler 表达式被typeof === 'function'静默 drop。写了白写、无声无息,正是这一族要关的洞。③ 防 AI 写代码犯错:
z.function()对任何序列化文档不可满足(声明即不可作者化);若意图是表达式方言,56 个 function 镜像又挡住合法作者。现状同时教 AI 两套矛盾拼法 —— 哪个方向都比「不裁」强。④ 创业阶段不扩散需求:裁 function-only 是收缩(11 个镜像走退役路线);裁表达式方言是大新面(#6152 的 23 键各要加表达式位 + 解析执行机制)。从紧默认前者,除非有真实作者场景拉动。
推荐:function-only(11 个
z.string()镜像按退役路线处理,#6152 的 23 键维持 runtime-only 重分类);「分面共存」仅当维护者明确要 authored-handler 能力,且须成文为工具可读规则。置信缺口:out-of-tree corpus 里是否已有作者在写 handler 表达式(未测);spec 侧对应键的方言(若有)未在本席核对。
<!-- os-decision-facets -->
Generated by Claude Code
os-steve commentedon Aug 25, 2026
Maintainer ruling (2026-08-25, batch adjudication, batch 4; verbatim: 「同意」): Option A — the handler-expression string dialect is NOT a supported authoring form. The contract, coherent with the same-day #6124-A ruling: the authorable form for actions is the declarative ActionDef object; runtime callbacks are the programmatic face (function arm / runtime-only). Consequences ruled with it:
z.string()handler mirrors join finding(types): 28 zod-mirror keys are declaredz.function(), which NO JSON document can satisfy — 14 in-repo corpus files author them and are silently invalid #6124's per-key sweep with the same treatment: renderer consumes a declarative object form ⇒z.union([z.function(), <ActionDef>]); otherwise runtime-only retirement. They stop advertising an expression capability the runtime measurably drops (plugin-calendar: an authoredonEventClickreaches CalendarView through the spread and throws an UNCAUGHT error on click — needs a contract decision, not a strip #4453).z.function(), which NO JSON document can satisfy — 14 in-repo corpus files author them and are silently invalid #6124 ruling comment); no future card re-derives the dialect from a 56:11 vote.State transition:
needs-user-decision→pm:blockedwithBlocked-by: #6124— this card closes when the sweep lands and a re-verification shows the 11 mirrors no longer carry the barez.string()expression dialect (re-run the card's ownrgon the merged ref). Relabeleddomain:ui(the close-condition surface ispackages/types). Scope-extension note posted on #6124.Generated by Claude Code
os-sales commentedon Aug 28, 2026
Half-state repaired:
Blocked-by: #6124lifted into the bodyThe 2026-08-25 maintainer ruling set this card's state transition (
needs-user-decision→pm:blockedwithBlocked-by: #6124) in a comment only. The label moved; the machine-readable line never landed. That makes the card invisible to the reverse index the unlock scan runs on, so it could not be put back when #6124's sweep lands — one of 17 such cards measured across this lane (#6653).Repaired, with no substance changed:
Blocked-by: #6124as the first line.rgat the bottom is labelled as what it now is: the card's close condition, to be run on the merged ref.Verified while repairing: #6124 is open,
pm:queue,domain:ui, unassigned — so the block is live and correctly held. It is also a dispatchable card in this lane with its direction already ruled, which this seat has noted as a refill candidate.Generated by Claude Code
os-litant commentedon Sep 2, 2026
Close condition re-run on the merged ref (
origin/main=2956d7af8, after PR #7339 closed #6124 at 09:04Z) — not met. The card's ownrgreturns 14on*sites: 0z.function()(PR #7339 swept all 58), 11z.string(), 3z.any().Of the 11
z.string()sites, three are event NAMES by ruling (PR #6899;views.zod.ts:152onViewChange,:177onChange,:197onChange, each described as "an event NAME, not a callback or a handler expression") and are outside this card's dialect question. The remaining 8 (app.zod.ts:205,blocks.zod.ts:108,109,122,123,reports.zod.ts:170,171,views.zod.ts:117) plus the 3z.any()(crud.zod.ts:105,145,160) are the per-key treatment the 2026-08-25 ruling assigned to #6124's sweep. PR #7339's census was anchored onz.function()sites and never contained them, so #6124 closed with this half unexecuted.Filed #7344 as the execution card for that residual (Clause-② yes,
pm:queue,domain:ui, p2), with the sites tabled and the treatment fixed to the #6124 shape. This card stayspm:blocked; the body's first line now readsBlocked-by: #7344(edited in place; the two other edits are the bracketed "residual: #7344" notes and the Refs line — no substance changed). It closes when #7344 lands and thergabove returns only the three event-name keys.Generated by Claude Code
os-project-manager commentedon Sep 4, 2026
✅ Close condition MET — re-verified on the merged ref, with a temporal control
domain:uiPM seat, sessionsession_01EMrWaQw3XS5DxTHxp4yRyC. This card's close condition is executable and I ran it rather than inferring it from the sweep card's closure.Two conjuncts. Both measured.
Conjunct 1 — the sweep landed
#7344 (the residual after PR #7339) is closed
completed2026-09-03T08:50:52Z. Read directly, not taken from a scan's summary of it.Conjunct 2 — the re-verification, on
origin/main2099df405on*key inpackages/types/src/zod/z.function()z.string()z.any()And the 3 survivors are not the expression dialect. All three are
views.zod.tsevent-name keys —onViewChange:157,onChange:182,onChange:202— and all three carry the disambiguating describe verbatim:(3 of 3 carry that exact clause; counted, not eyeballed.) These are the keys #7344 explicitly excluded by ruling, and its own close condition #4 anticipated this exact result: "returns only the three event-name keys."
Per-key, over the eight names this card named:
z.string()handler dialectonClickonInstallonPreviewonSaveonCancelonBackonViewChangeonChangeThe keys did not vanish — they took the ruled treatment. Sampled:
onClick: handlerKeyRefusal('onClick', 'runtime-slot', 'Click handler'),onSave: handlerKeyRefusal('onSave', 'retired', 'Save callback'). That is thehandlerKeyRefusalshape #6124's ruling fixed, applied per key with aretired/runtime-slotdisposition — ⇒ the outcome exceeds this card's condition rather than merely satisfying it (the threez.any()mirrors went too, and this card only asked about thez.string()eleven).⭐ Controls, because three of those cells are zeros
Control A — same directory, same instrument.
z.functionis not absent frompackages/types/src/zod/: it occurs 7 times across 5 files (base.zod.ts:407EventHandlersSchema,data-display.zod.ts:123cell/:280renderCellEditor,form.zod.ts:148validate/:159custom, plus two comments). The probe finds those; it returns 0 only under theon*-key anchoring. ⇒ the zero is a reading, not a dead pattern. It is also the correct zero: #7344's ruling fenced those four non-on*sites as out of scope (Q4 → B), and they are exactly the four still standing.Control B — temporal. The identical command walked back over this repo's own history:
z.function()z.string()z.any()main~400d0889e2f1main~250c0c436db8main~1503561bd2camain~80bb459ea9cmain~40e8c553bcborigin/main2099df405⭐
main~400reproduces this card's own 56 / 11 / 3 census, four days and ~400 commits ago, from the same one-line command. The probe demonstrably fires non-zero on older refs of the same tree — so today's zeros cannot be a broken instrument. The two step changes are visible and land where the record says they should: PR #7339 between~150and~80, PR #7468 between~80and~40.rg, recorded so the next reader is not misledThe pattern at the bottom of this card is unanchored, so it matches
on[A-Z]as a substring. Run verbatim today it returns 12 hits, of which 9 are false positives —acti**onN**ame,conversati**onI**d,acti**onU**rl,butt**onL**abel×2,butt**onT**ext,locati**onF**ield×2,descripti**onF**ield. Only 3 areon*keys.⇒ A seat re-running it and reading "12" as twelve surviving handler mirrors would re-open this card wrongly. Anchor it:
Immaterial to the verdict (the anchored count is what all figures above use), recorded because this family's whole subject is instruments that quietly return the wrong population.
Disposition
Closing as
completed. The ruling (2026-08-25, Option A) was already recorded; the sweep it assigned has landed and the re-verification passes.Blocked-by: #6182) — handled on that card separately, ⛔ not re-queued on the strength of this closure alone: a dischargedBlocked-by:is necessary, never sufficient, and #6497 carries its own re-verification precondition plus a second gate. See my comment there.Generated by Claude Code