You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
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
Filed unassigned by the domain:ui execution seat while measuring objectui#5392 (session session_01CSoz9uGhaaSgiq3hshtN7L). Out of scope for that card, which is a measurement of zod→JSON Schema fidelity; this is a separate defect its instrument surfaced.
Dedupe search run before filing: #5250 (BaseSchema.passthrough() accepts undeclared keys) is the opposite direction and does not cover this; #5927 / #5853 / #6058 are the mirror-vs-TypeScript-declaration drift family, which is a different axis — this is mirror-vs-JSON-authorability. No existing card names z.function().
What was measured
@object-ui/types is the protocol layer for a JSON-authored UI vocabulary. Its zod mirrors declare 28 distinct keys as z.function():
No JSON value satisfies z.function() — measured, with a positive control in the same run:
null -> REJECT {} -> REJECT
1 -> REJECT [] -> REJECT
"x" -> REJECT true -> REJECT
a real function -> accept <- control: the instrument can say "accept"
Why it matters: 14 in-repo corpus files author these keys and fail
Validating every root-type JSON file in the repo against AnyComponentSchema.safeParse finds 14 files that are rejected today, all for this reason. Example, examples/schema-catalog/src/schemas/components-feedback-toast/destructive.json:
{"type":"button","label":"Destructive Toast","variant":"destructive",
"onClick":{"action":"toast","variant":"destructive","title":"Error","description":"Something went wrong."}}
The file is authoring onClick as a declarative action object, which is the only thing JSON can express. The mirror asks for a JS function. The affected files are the components-feedback-toast/ and components-feedback-sonner/ catalogs.
And they are invisible
All 14 sit in objectui#5127's structure-key-less set — they carry no body/children/className/… at the root, so objectui check never judges them. They are invalid and unreported, which is why this has stood.
The question this needs ruled
Two directions, and this is a contract question rather than a mechanical fix:
Narrow the declaration — if a JSON author can never supply a function, z.function() is declared != enforceable on a JSON surface and the keys should be removed or demoted to a runtime-only (non-authorable) face under ADR-0049 enforce-or-remove.
Widen the mirror to the declarative form — if onClick: {action: …} is the intended authored spelling (which the corpus and the renderer's own action plumbing suggest), the mirror should declare that shape, and the 14 corpus files become valid.
⛔ Not fixed here — picking one changes a published contract. Filed for triage.
Maintainer ruling (2026-08-25, batch adjudication close-out; verbatim: 「6124 同意 A」): Option A — the server-pattern alignment as refined in the previous comment.
Ruled execution shape:
Per-key measurement across all 28 z.function() keys: does the renderer consume a declarative object form for this key?
Consumed ⇒ mirror becomes z.union([z.function(), <closed ActionDef shape>]) — the function arm serves the programmatic/React face, the object arm serves JSON authors (the FlowFunctionEntrySchema precedent, objectstack flow-function.zod.ts); the exact ActionDef union boundary (including whether a string-named handler ref arm belongs, mirroring flow's ref arm) is fixed by the measurements.
Not consumed ⇒ the key leaves the JSON mirror (runtime-only face, ADR-0049 — the driver.zod.ts precedent).
The 14 silently-invalid corpus files go green (or get corrected) in the same PR — that flip is the proof the contract now tells authors the truth.
Rider measurement: if any schema→JSON serialization door exists in objectui (designer save etc.), function values must be loudly refused or lowered at that door (JSON.stringify drops functions silently — the server's lowerCallables is the precedent); a hit files its own follow-up card, not a rider fix.
State transition: needs-user-decision → pm:queue (this comment is the authoritative record if the label lags). Lane domain:ui. Clause-②: yes (published-validator accept-set change) — contract-review tier at dispatch. Size M (28-key measurement + union edits + corpus verification).
Scope extension (triage, same-day ruling #6182-A, maintainer verbatim 「同意」): the 11 z.string() handler-expression mirrors measured in #6182 (rg -n 'on[A-Z][A-Za-z]*:\s*z\.(function|string|any)' packages/types/src/zod/ — the string rows, e.g. onClick/onInstall/onPreview/onSave/onCancel/onBack/onViewChange/onChange) join this card's per-key sweep with the SAME ruled treatment as the 28 z.function() keys: renderer consumes a declarative object form ⇒ union arm; otherwise runtime-only retirement. The expression-string dialect is not a supported authoring form (it is measurably dropped at runtime, #4453). The 3 z.any() rows from the same census get the same per-key walk while you are in the files. #6182 is pm:blocked on this card and closes on its landing.
Claimed after re-reading the whole thread: four comments, no prior Claim: with a different session ID, unassigned. The maintainer's 2026-08-25 ruling (verbatim 「6124 同意 A」) is the authoritative record and the card is execution, not decision.
⚠️ The ruling comment is SANITIZER-TRUNCATED at its most load-bearing line — read the comment before it
The ruling's item 2 reads:
mirror becomes z.union([z.function(), ])
That is not what was ruled. GitHub's sanitizer eats generic-looking type arguments, and it ate the object arm — leaving a shorter string that still parses as a complete sentence, which is exactly why this class of damage survives unnoticed. The arm is spelled out intact in the immediately preceding comment (2026-08-25T06:01Z):
⛔ Do not implement z.union([z.function(), ]) literally. The ruled shape is a function arm plus a declarative-object arm, with the exact ActionDef union boundary — including whether a string-named handler ref arm belongs, mirroring flow's ref arm — fixed by the per-key measurements, not assumed.
Scope, as ruled and as extended
The sweep is not 28 keys. Per the same-day scope extension (#6182-A, maintainer 「同意」):
the 3 z.any() rows get the same per-key walk while the dev is in those files.
Per key: renderer consumes a declarative object form ⇒ union arm; not consumed ⇒ the key leaves the JSON mirror as a runtime-only face under ADR-0049 (the driver.zod.ts precedent).
Item 4 is the proof obligation and is not optional: the 14 silently-invalid corpus files go green (or get corrected) in the same PR — that flip is what demonstrates the contract now tells authors the truth.
Item 5 is a rider measurement, not a rider fix: if any schema→JSON serialization door exists in objectui (designer save, etc.), a function value at that door must be loudly refused or lowered — JSON.stringify drops functions silently, the shape this repo least tolerates. A hit files its own card; ⛔ it is not fixed inside this PR.
Item 6: read #6182's landed ruling first. The later ruling must not silently overturn the earlier one; a conflict forks back to triage rather than being resolved by the dev.
Unlock value
#6182 is pm:blocked on this card and closes on its landing, with an executable close condition (re-run its rg on the merged ref and confirm the 11 mirrors no longer carry the bare z.string() expression dialect).
Tier
Clause-②: yes (published-validator accept-set change) — contract-review tier at dispatch: draft PR, this seat reviews before it goes ready. ⛔ needs:contract-review is not attached: it is a phantom label in this repo with zero readers, and the standing maintainer order for this tier is 「直接入队」 after seat review.
{
"issue": 6124,
"status": "needs_decision",
"branch": "claude/issue-6124-zfunction-mirror-sweep",
"pr": null,
"premise_still_valid": false,
"summary": "No code written. The ruled sweep is not executable as ruled, for three measured reasons, and the card's headline premise is dead on today's main. (1) CENSUS re-run on merge-base 813bf8323 reproduces the card exactly: 28 distinct z.function() keys / 60 call sites / 9 files, 11 on* z.string() rows, 3 on* z.any() rows. (2) PER-KEY: zero of the 42 distinct keys is consumed as a declarative object, so the ruled union arm z.union([z.function(), ActionDefSchema]) has NO qualifying key and its boundary lands EMPTY; every measured consumption is a function CALL, a React-prop forward, or an authored string used as a CustomEvent NAME. (3) ITEM 4 IS ALREADY DISCHARGED ELSEWHERE: PR #6498 (merged 2026-08-26, one day after the ruling) rewrote all 14 toast/sonner fixtures; the corpus reds attributable to this card's defect are 0, so this PR cannot produce the flip the ruling made its proof. (4) THE BLOCKER: executing the retirement branch under BaseSchema.passthrough() converts a LOUD parse-time rejection into a SILENT accept that preserves the bad value — measured, both directions — and onClick is in SDUI_DOM_PASS_THROUGH_KEYS, so the preserved object reaches the DOM listener slot and throws at click. That is a regression on the maintainer's own decisive axis, whose triage wording was '响亮移除'; under passthrough the removal is silent, not loud. Forking back rather than landing it.",
"tests": "No repo files changed (git status clean, zero commits on the branch), so no gates were owed or run — reporting that plainly rather than manufacturing a gate list. All measurement ran through the shared verify lock (bash /home/user/objectstack/scripts/pm/os-verify-lock.sh -c ..., OS_VERIFY_LOCK_SLOT=issue-6124): three runs, verdict lines 'os-verify-lock: VERDICT command-exit 0' (waits 154s / 0s / 0s). Harness was a temporary probe at packages/types/src/__tests__/zz-6124-probe.test.ts, run as 'npx vitest run <that file> --maxWorkers=2' — 'Test Files 1 passed (1) / Tests 3 passed (3)', file count confirmed as the expected 1 (no --project flag used). Probe DELETED afterwards; worktree verified clean. RED-BEFORE-GREEN on the blocker, both legs in one run: [A] today ButtonSchema.safeParse of the card's own historical payload (recovered from git at bca58068) => success=false, issue {'code':'invalid_type','expected':'function','path':['onClick'],'message':'Invalid input: expected function, received object'}; [B] the same payload against ButtonSchema.omit({onClick:true}) — the retirement, simulated — => success=TRUE, and the parsed output still carries onClick={'action':'toast','variant':'error','title':'Error','description':'Something went wrong.'}, i.e. passthrough preserves the value it no longer judges. Independently corroborated: onClick is a member of SDUI_DOM_PASS_THROUGH_KEYS (packages/core/src/utils/dom-props.ts:88-99, read directly, not inherited from #6498). CORPUS SWEEP (AnyComponentSchema.safeParse over every root-`type` JSON in examples/schema-catalog/src/schemas): 428 docs, 355 green, 73 red, toast/sonner reds = 0, and reds whose failure is `expected: function` = 0 — the 73 are a different, pre-existing defect family (text / sidebar / carousel / button-group), untouched and not this card's. No ablation was run: nothing was mutated, so there is no restore leg to prove.",
"mcp_calls": "2 — one targeted search_issues (dedupe for the item-5 card) and one issue_write create, plus one issue_write update to restore a stripped footer and this comment; every read of #6124 / #6182 / #4453 / #5127 / PR #6498 went through the zero-config unauthenticated REST channel, which answers 200 for repo-scoped reads on this public repo. Channel switch declared: repo-scoped REST works, but /search/issues answers 403 for this seat, so dedupe went through MCP as the sanctioned fallback (3 results, all closed, none on the door — a non-empty result set, so no separate control word was needed).",
"open_questions": [
{
"question": "Q1 (blocking, applies to ALL 28 keys). Retiring a handler key from the mirror under BaseSchema.passthrough() turns today's loud rejection into a silent accept that KEEPS the bad value and forwards it to the DOM. Measured: reject -> accept, value preserved. The ruling's ADR-0049 precedent is driver.zod.ts, which is safe there precisely because those schemas are never JSON-authored and nothing passes unknown keys through; neither condition holds here. How should the retirement be made LOUD, as the triage wording ('响亮移除') requires?",
"options": [
"A. Retire the keys AND make the affected mirrors refuse a retired handler key explicitly (a named refusal arm rather than deletion), so an authored onClick is rejected with a message naming the supported spelling. Cost: touches the same 9 mirrors twice and needs a refusal vocabulary that does not exist yet; strongest on the AI-error axis — the only option under which the validator still speaks.",
"B. Retire the keys and pair it with resolving #5250 (BaseSchema.passthrough accepts undeclared keys) so removal is loud by default. Cost: #5250 is a much larger blast radius and is its own open decision; couples this card to it.",
"C. Retire the keys as ruled and accept the silent-accept window as a known regression, tracked by a follow-up. Cost: measurably worsens the exact authoring hazard this card was filed about — an author writing onClick as an object goes from a clear parse error to no error plus an uncaught runtime throw.",
"D. Do nothing to the mirrors. Cost: keeps declared != authorable, which the four-axis analysis called the one unacceptable option."
],
"recommendation": "A. It is the only option that satisfies the axis the maintainer made decisive — making AI-authored metadata hard to get wrong — because it is the only one where the author still gets told. C is refused on that axis by measurement, not by taste: it is strictly worse than today for the shape the card names. B is correct in direction but should not be smuggled in under this card; if the maintainer prefers it, this card should be marked blocked-by #5250 instead. Note A is a NEW capability (a refusal arm), i.e. beyond the ruled shape, which is why this is escalated rather than implemented."
},
{
"question": "Q2 (scope-extension premise is false for 3 of the 11 z.string() rows). The extension retires the string rows on the stated basis that the expression-string dialect 'is measurably dropped at runtime (#4453)'. For three rows that is factually wrong: the authored string is READ and DISPATCHED as a CustomEvent name — ViewSwitcherSchema.onViewChange (packages/plugin-view/src/ViewSwitcher.tsx:249-255), FilterUISchema.onChange (FilterUI.tsx:99-105), SortUISchema.onChange (SortUI.tsx:93-99), each 'new CustomEvent(schema.<key>, {detail: ...})' on window. These are neither the expression dialect #6182 refused nor a declarative object, and they are live JSON-authorable capabilities. Retiring them deletes working behaviour.",
"options": [
"A. Exclude these 3 from the sweep and keep them mirrored as z.string(), re-describing them as event-NAME strings rather than 'callback'/'expression' so the next census does not re-bucket them wrongly. Cost: ~3 describe() edits; keeps a real capability and removes the mislabel that caused the misclassification.",
"B. Retire them with the other 8, deleting a working authored capability. Cost: a silent feature removal justified by a premise that measurement contradicts.",
"C. Treat the window-CustomEvent channel as itself undesirable and retire it deliberately as a capability decision. Cost: a genuine product call about an escape hatch, well beyond this card."
],
"recommendation": "A, and note that the mislabel is the root cause: #6182's census bucketed by zod type, which cannot tell 'handler expression' from 'event name'. The other 8 string rows (app onClick, blocks onInstall/onPreview/onSave/onCancel, reports onSave/onCancel, views onBack) have NO read anywhere and are unaffected by this objection."
},
{
"question": "Q3 (the ruled union arm cannot be instantiated at all). The ruling fixes the ActionDef union boundary 'by the measurements'. The measurements fix it EMPTY: no handler key anywhere consumes a declarative object; schema.events — the ActionDef channel AGENTS.md section 4 documents — is consumed nowhere in the repo; and ActionRunner's runnable vocabulary (script|url|modal|flow|api|form|navigation) has no dispatcher a handler-key object could reach. #6182's landed ruling states 'the authorable form for actions is the declarative ActionDef object', which is true as an intent but is implemented by NO key. Declaring the arm anyway would declare a capability the runtime does not honour — the exact thing the triage comment forbade.",
"options": [
"A. Accept that the sweep is 100% retirement and no union arm is added; the 'widen' half of the ruling is recorded as inapplicable-on-measurement. Cost: none beyond Q1.",
"B. Build the missing capability — make some handler key actually consume an ActionDef object and dispatch it — then widen the mirror to match. Cost: a new renderer capability plus an ActionRunner dispatcher; a materially larger card and explicitly outside this dispatch.",
"C. Fork the widen half to a separate card so the retirement half can proceed alone once Q1 is answered."
],
"recommendation": "A or C, never B under this card. B is real work with real value — it is what would make #6182's 'authorable form' statement true — but it is a capability build, and both this dispatch and the startup-scope axis say a dev does not start one on a sweep card."
},
{
"question": "Q4 (three keys cannot be filed after retirement). cell, custom and validate are not on[A-Z]-shaped, so RuntimeOnlyDeclared refuses them (assertionRuntimeOnlyIsCallbackShapedOnly, which zod-mirror-parity.test.ts calls 'the load-bearing half'), while UnmirroredDeclared refuses callback-shaped keys and records 'ordinary mirroring debt' — a remedy #6152's ruling rejects for function-only keys. cell is genuinely consumed as a called function (plugin-grid/src/VirtualGrid.tsx:153 'column.cell(value, row)', ObjectGrid.tsx:3948), so it is a real runtime-only face that the ledger vocabulary cannot express.",
"options": [
"A. Relax the shape pin deliberately to admit named non-on* runtime-only keys, as the file itself says must happen by RULING and never by quiet refiling. Cost: a governed change to a heavily-pinned test; needs the maintainer.",
"B. Leave cell/custom/validate mirrored and retire only the 25 on* keys. Cost: leaves 3 keys in the state the card calls the defect; internally inconsistent but small.",
"C. File the three in UnmirroredDeclared anyway. Cost: records a remedy the ruling rejects — refused."
],
"recommendation": "A if the sweep proceeds at all, B as the honest partial. C is not acceptable: it would make the ledger lie about what it holds, which is the one thing its own pins exist to prevent."
}
],
"out_of_scope_findings": [
"filed as #6658: the designer save door (packages/react/src/hooks/useSchemaPersistence.ts:58-61) JSON.stringify's the schema with no callable guard, so a function-valued handler key is silently dropped and reload yields a schema that lost it with no error — this is the ruling's item-5 rider measurement, which the ruling directs to file its own card rather than fix here. Dedupe: 3 results, all closed, none on this door.",
"not filed, reported for PM dedup rather than filed blind: 73 of 428 schema-catalog root-`type` documents fail AnyComponentSchema today for reasons unrelated to this card (0 of them for `expected: function`) — concentrated in components-basic-text, components-basic-sidebar, components-complex-carousel and components-basic-button-group. This looks like an existing family (#5127's structure-key-less blind spot is the likely reason they are unreported) and I did not want to spend a second dedupe search to guess at it.",
"observation, not filed: the card's own census command `grep -hoE \"^\\s*[A-Za-z]+: z\\.function\\(\"` is sound, but #6182's `rg 'on[A-Z][A-Za-z]*:\\s*z\\.(function|string|any)'` is UNANCHORED and matches mid-identifier — buttonLabel, actionUrl, conversationId, locationField, actionName and descriptionField all contain an on[A-Z] substring. It over-reports. The anchored recount still reproduces 28/11/3, so no published number moves; worth fixing in place if #6182's close condition is re-run on the merged ref, since that command is the stated close condition."
]
}
⛔ ESCALATE — the ruled sweep is NOT executable as ruled. No code was the right answer.
domain:ui execution seat, PM session session_01CRJge11jso9TpXRWFt1Z49. The dev stopped and reported, wrote zero files, and left the branch with no commits. I am not overriding that, and I am not asking it to try again. pm:dispatched → needs-user-decision.
⛔ This is not a re-opening of the direction. Option A stands. What measurement refutes is the execution shape, on four independent counts — and the first one would make the product worse on the axis the maintainer called decisive.
⭐ 1. The blocker: under passthrough(), "retire the key" means SILENTLY ACCEPT it
Measured, both directions, on the card's own historical payload recovered from git:
the retirement, simulated — same payload against ButtonSchema.omit({onClick:true})
success: TRUE, and the parsed output still carriesonClick: {action:'toast',…}
BaseSchema is .passthrough(), so removing a key does not refuse it — it stops judging it and keeps it. And independently corroborated by reading the source rather than inheriting it: onClick is in SDUI_DOM_PASS_THROUGH_KEYS (packages/core/src/utils/dom-props.ts:88-99), so the preserved object reaches the DOM listener slot and throws at click.
⇒ Executing the retirement branch converts a clear parse error into no error plus an uncaught runtime throw. The triage wording was 「响亮移除」 — loud removal. Under passthrough the removal is silent. That is strictly worse than today for exactly the authoring hazard this card was filed about.
⚠️And this is the same rule that bit #6355 an hour ago, in its third face this week. There, deleting a key would have let the retired spelling parse green while the renderer stopped reading it; the fix was a ?: never tombstone rather than a deletion. Here it is the same mechanism with a worse tail, because the preserved value is then forwarded to the DOM. Under passthrough() / an index signature, absence is not refusal — the refusal has to be declared.
The ruling's ADR-0049 precedent is driver.zod.ts. That precedent is safe there precisely because those schemas are never JSON-authored and nothing passes unknown keys through. Neither condition holds here.
2. The ruled union arm cannot be instantiated — its boundary lands EMPTY
The ruling fixes the ActionDef union boundary "by the measurements". The measurements fix it at nothing: zero of the 42 distinct keys is consumed as a declarative object. Every measured consumption is a function call, a React-prop forward, or an authored string used as a CustomEvent name. schema.events — the ActionDef channel AGENTS.md §4 documents — is consumed nowhere in the repo, and ActionRunner's runnable vocabulary (script|url|modal|flow|api|form|navigation) has no dispatcher a handler-key object could reach.
⇒ #6182's ruled statement "the authorable form for actions is the declarative ActionDef object" is true as intent and implemented by no key. Declaring the arm anyway would declare a capability the runtime does not honour — the exact thing this card's own triage forbade.
3. The ruling's proof obligation is already discharged, elsewhere, and cannot be re-earned here
Item 4 made the 14 silently-invalid corpus files going green the proof the contract now tells authors the truth. PR #6498 merged 2026-08-26 — one day after the ruling — and rewrote all 14 toast/sonner fixtures. Corpus reds attributable to this card's defect are now 0. So no PR under this card can produce that flip; the evidence the ruling asked for no longer exists to be produced.
(Corpus sweep for context: 428 root-type docs, 355 green, 73 red, toast/sonner reds = 0, reds with expected: function = 0. The 73 are a different pre-existing family — text / sidebar / carousel / button-group — untouched and not this card's.)
⭐ 4. Three of the 11 scope-extension rows are FALSE POSITIVES — retiring them deletes working behaviour
The extension retires the z.string() rows on the basis that the expression-string dialect "is measurably dropped at runtime (#4453)". For three rows that is factually wrong — the authored string is read and dispatched as a CustomEvent name:
each new CustomEvent(schema.<key>, { detail: … }) on window. These are neither the expression dialect #6182 refused nor a declarative object — they are live, JSON-authorable capabilities.
⚠️Root cause, and it is worth fixing at the source: #6182's census bucketed by ZOD TYPE, which cannot distinguish "handler expression" from "event name". The other 8 string rows have no read anywhere and are unaffected.
⚠️ Bonus defect: #6182's close condition is an unanchored pattern
rg 'on[A-Z][A-Za-z]*:\s*z\.(function|string|any)' matches mid-identifier — buttonLabel, actionUrl, conversationId, locationField, actionName, descriptionField all contain an on[A-Z] substring. It over-reports. The anchored recount still reproduces 28 / 11 / 3, so no published number moves — but that command is#6182's stated close condition, so it should be fixed in place before anyone runs it on a merged ref.
The census itself reproduced exactly
On merge-base 813bf8323: 28 distinct z.function() keys / 60 call sites / 9 files; 11on*z.string(); 3on*z.any(). The card's numbers hold.
What needs a ruling (four questions, in dependency order)
The three CustomEvent-name rows — exclude and re-describe them as event-name strings so the next census does not re-bucket them wrongly (recommended), or retire them anyway and delete a working capability.
The widen half — record it inapplicable-on-measurement (100% retirement, no union arm), or fork it to its own card. ⛔ Not "build the missing dispatcher" under this card.
cell / custom / validate are not on[A-Z]-shaped, so RuntimeOnlyDeclared refuses them while UnmirroredDeclared refuses callback-shaped keys — the ledger vocabulary cannot express a genuine non-on* runtime-only face. (cellis consumed as a called function: plugin-grid/src/VirtualGrid.tsx:153, ObjectGrid.tsx:3948.) Relaxing that pin is a governed change the file itself says must happen by ruling and never by quiet refiling.
Filed, per the ruling's item 5
#6658 — the designer save door (packages/react/src/hooks/useSchemaPersistence.ts:58-61) JSON.stringifys the schema with no callable guard, so a function-valued handler key is silently dropped and reload yields a schema that lost it with no error. That is the rider measurement the ruling directed to file its own card rather than fix here. Deduped first.
⇒ #6182 stays blocked on this card, and its close condition should be re-stated with the anchored pattern.
Correction to the claim above (same seat, same session): the serial-constraints line was meant to read "the packages/types/src/** serial queue opened when PR #7240 (#7087) landed as c93b4d5f3 (07:32Z)". The dev is dispatched against origin/main at or after that sha.
{
"issue": 6124,
"status": "done",
"branch": "claude/issue-6124-on-handler-keys-refusal-arm",
"pr": "https://github.com/objectstack-ai/objectui/pull/7339",
"premise_still_valid": true,
"summary": "Executed the 2026-08-30 ruling (Q1 A, Q2 A+C, Q3 A, Q4 B) on origin/main c93b4d5f3. All 58 on* z.function() sites (26 distinct keys, 8 mirror files; 62 = 58 + the four non-on* sites, which stay by Q4 B) are now named refusal arms in the #5099 z.custom + guidance shape via a new handlerKeyRefusal(key, disposition, label) helper in zod/tombstone.zod.ts: the message names the key, says why JSON cannot author it, and points at the node-type spelling PR #6498 established; the same string is the describe() metadata. The arm refuses live functions too. The ruling's stop condition was measured and did NOT fire: SchemaRenderer validates through @object-ui/core's structural validateSchema, and the only zod safeParse doors are the CLI validators and the exported validateSchema/safeValidateSchema, none fed a function-bearing object. TS face measured per key: SchemaRenderer spreads every non-metadata schema key as a React prop (SchemaRenderer.tsx:1576), so 36 keys whose value reaches a renderer (read off schema.*, called as props.onX, or spread onto a Radix root / DOM listener slot) keep their function type; 22 keys nothing reads get the ?: never tombstone. Parity ledger: 28 pairs re-stated in KnownDrift (35 keys; MenuItemSchema.onClick is a lazy union in EXCLUSIONS), header counts 12 to 36 entries, 17 to 52 keys, 146 to 122. Premise nuance: the card's 28/60 and the ruling's 29/61 are both stale (renderCellEditor landed with #7241; the population is 62/30 with 58/26 on*); the '14 corpus files' half is dead since PR #6498, as the ruling records. Assignee field was empty at pickup (PM-side state), worked as dispatched.",
"census": "58 sites = 36 runtime slots + 22 retired. RUNTIME SLOT (TS keeps function; mirror refuses by name): KanbanSchema.onCardMove/.onCardClick (plugin-kanban forwards schema.*), CalendarViewSchema.onViewChange (pickHostCallbacks off spread props), FilterBuilderSchema.onChange (props.onChange called), ChatbotSchema.onError/.onSend (forwarded into useObjectChat), DataTableSchema.onRowEdit/.onRowDelete/.onSelectionChange/.onColumnsReorder (data-table.tsx calls schema.*), AccordionSchema.onValueChange, CollapsibleSchema.onOpenChange, ToggleGroupSchema.onValueChange (spread onto Radix roots), ButtonSchema.onClick (toFormControlDomProps forwards it to the DOM; onClick is on SDUI_DOM_PASS_THROUGH_KEYS), InputSchema/TextareaSchema/SelectSchema/CheckboxSchema/FileUploadSchema/DatePickerSchema/InputOTPSchema.onChange (renderers call props.onChange), FormSchema.onSubmit/.onChange/.onCancel (form.tsx destructures off schema and calls them), CodeEditorSchema.onChange (onChange ?? schema.onChange), CardSchema.onClick (props.onClick, spread onto Card), TabsSchema.onValueChange (tabsProps spread after own handler), PaginationSchema.onPageChange (props.onPageChange called), DialogSchema/AlertDialogSchema/SheetSchema/DrawerSchema/PopoverSchema/HoverCardSchema/DropdownMenuSchema.onOpenChange (spread onto Radix/vaul roots), MenuItemSchema.onClick (item.onClick?.() in dropdown/context/menubar). RETIRED (TS ?: never; mirror refuses by name): KanbanSchema.onColumnAdd/.onCardAdd, CarouselSchema.onSlideChange, ChatbotSchema.onSendMessage, AlertSchema.onDismiss, ListItem.onClick, TreeViewSchema.onSelectChange/.onExpandChange, ToastSchema.onDismiss, RadioGroupSchema/SwitchSchema/ToggleSchema/SliderSchema/ComboboxSchema.onChange (whitelist drops), CalendarSchema.onChange (DayPicker uses onSelect), CommandSchema.onChange (lands on cmdk root div as a DOM change listener, not the declared contract), InputOTPSchema.onComplete, BreadcrumbItem.onClick, SidebarSchema.onCollapsedChange, ButtonGroupButton.onClick, AlertDialogSchema.onConfirm/.onCancel. Out of scope by ruling: cell, custom, validate, renderCellEditor (stay z.function, pinned by name); EventHandlersSchema (#6910); the three z.string() CustomEvent-name keys (PR #6899). Full table with file:line per row is in the PR body.",
"files_changed": "packages/types/src/zod/{complex,data-display,disclosure,feedback,form,layout,navigation,overlay}.zod.ts (58 sites), packages/types/src/zod/tombstone.zod.ts (handlerKeyRefusal + HandlerKeyDisposition), packages/types/src/{complex,data-display,disclosure,feedback,form,layout,navigation,overlay}.ts (58 twins: 36 JSDoc runtime-slot notes, 22 ?: never tombstones), packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts (new), packages/types/src/__tests__/zod-mirror-parity.test.ts (KnownDrift: 4 entries extended, 24 added, header counts), packages/types/src/__tests__/menu-item-union.test.ts and chatbot-authoring-face-keys.test.ts (function-value cases restated), examples/schema-catalog/test/component-fixture-declared-keys.test.ts (counter-probe message restated, verdict unchanged), .changeset/6124-handler-keys-json-refusal.md (@object-ui/types minor).",
"tests": "All heavy runs via bash /home/user/objectstack/scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=dev-6124; verdicts quoted from each tool's own lines. RED-FIRST on the unmodified tree c93b4d5f3 (pin file only): vitest 'Tests 178 failed | 62 passed (240)' (census found 58 sites; authored object refused with invalid_type not custom; live functions parsed GREEN), tsc -p tsconfig.test.json: 22 x TS2344 on the RetiredIsNever lines exactly as predicted (plus one TS2740 from my own typing of the lazy MenuItemSchema mirror, fixed). UNION ON THE FINAL COMMIT 11fd53500 (post-commit run): pnpm --filter @object-ui/types run type-check, echoed 'tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json', type-check-exit=0; pnpm exec vitest run packages/types/ examples/schema-catalog/test/component-fixture-declared-keys.test.ts --maxWorkers=2 -> 'Test Files 88 passed (88)' / 'Tests 1455 passed (1455)'; pnpm --filter @object-ui/types build -> 'dist completeness: 1 package(s) complete (118 emitted files verified)'; downstream pnpm --filter @object-ui/core run type-check against the rebuilt dist (core's only workspace dep is @object-ui/types) exit 0; tsc -p tsconfig.test.json --listFiles lists handler-keys-json-refusal-6124.test.ts (544 files). ABLATION (after commit, trap EXIT INT TERM, absolute paths): ButtonSchema.onClick reverted to z.function().optional().describe('Click handler'); on-disk proof 'arm-before=1 mut-before=0 -> arm-after=0 mut-after=1', git diff --stat 1 insertion 1 deletion; no build needed and none run, the pin imports ../zod/form.zod from SOURCE; result 'Tests 6 failed | 234 passed (240)' = census (1 site), Button declared/describe, Button object (invalid_type), Button function (GREEN), wording, arm-vs-tombstone - exactly the Button rows; restore by git checkout HEAD -- ABS_PATH, git diff HEAD empty (exit 0), git hash-object == git rev-parse HEAD:path == 23a63abe30d17838c6dd4e8d445e573e28a424b6, git status clean. GATES (unlocked check:*): check-changeset-presence '21 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'; check-changeset-no-major 'No changeset declares a major bump'; check:control-bytes 'OK (scanned 6023 tracked text file(s); skipped 85 binary)'; check:spec-symbols 'spec symbol derivation: 1333 files scanned against 4959 spec export names'; check:doc-types 'Every documented component type is registered'; check:vi-mock-inherit / check:vi-mock-specifiers OK. LINT narrowed and measured: eslint --no-inline-config --format json over the 22 staged .ts files: population from eslint's json = 22 files, 0 errors, 54 warnings, all @typescript-eslint/no-explicit-any and each warned file's merge-base blob lints to the identical E/W count (complex.ts 0/8, data-display.ts 0/30, form.ts 0/10, complex.zod.ts 0/2, data-display.zod.ts 0/1, navigation.zod.ts 0/2, overlay.zod.ts 0/1) so pre-existing; eslint.config.js has no projectService/parserOptions.project, so the diff cannot move an untouched file's verdict; repo-wide pnpm lint left to CI. DOWNSTREAM TYPE-CHECK narrowed and declared: the only breaking TS change is the 22 ?: never members; a read census over packages/*/src, apps/*/src, examples, scripts for files naming both an affected interface and its retired key found 3 (AppHeader.tsx builds BreadcrumbItem[] without onClick; ObjectCalendar.tsx declares its own local CalendarSchema; chatbot renderer.tsx intersects ChatbotSchema with onSend/onClear and its onSendMessage is the Chatbot component prop), none assigns a retired key on the affected type; @object-ui/core type-checked for real; the full turbo type-check over every consumer needs the whole repo built and exceeds the foreground cap on this shared box - CI runs it. NOT MEASURED, stated as such: no browser run; the DOM-tail behaviour is inherited from the 2026-08-30 report's measurement and not re-run.",
"deviations": "1) Dispatch order said the ledger models #5099 like its own rows; measured, #5099's pattern.value has no ledger row because both faces agree (RegExp), so the runtime-slot keys are recorded as deliberate KnownDrift entries (z.input undefined vs TS function type) with a class note, following the file's PageNodeSchema.pageType precedent; retired keys do not drift. 2) Three existing tests were restated (verdicts unchanged, messages moved): the corpus counter-probe at component-fixture-declared-keys.test.ts:139 asserted zod's bare 'expected function, received object'; menu-item-union and chatbot-authoring-face-keys parsed a live function green. 3) Full downstream turbo type-check replaced by a read census + core type-check (declared above). 4) REST channel gated for this session (403 GitHub access is not enabled for this session, both authed and unauthenticated), so writes went through MCP; the card body came from the public-page payload but it carried only 15 of 65 timeline items, so the remaining comments were read with one MCP issue_read. 5) PR body footer carries both the standard attribution line and the session-URL footer; commit trailers as dispatched.",
"mcp_calls": "8 - issue_read get_comments on #6124 (the public-page payload carried only 15 of 65 timeline items); create_pull_request (draft #7339); pull_request_read get (body + label read-back: body intact after the sanitizer, labels package: types / examples / tests); issue_write update (label union adding needs:contract-review); issue_read get_labels on 7339 (FAILED: that route resolves issues only, counted anyway); pull_request_read get (label read-back); add_issue_comment (this report); issue_read get_comments page 4 (report read-back to the tail). Channel declared: repo-scoped REST answered 403 'GitHub access is not enabled for this session' with and without the token (rate_limit 15000/h = credentials alive, session-gated), so every GitHub write went through MCP; the card body came from the public-page payload; branch/commit facts through git; no search call was needed (nothing filed).",
"open_questions": [],
"out_of_scope_findings": [
"NOT filed, handed to the PM (docs are outside the dispatched file surface; the ruling's scope names no docs work): three doc pages teach retired keys as authorable props and will drift from the tombstones once this lands - content/docs/api/schema-reference.md:926-927 (onColumnAdd, onCardAdd listed as function), content/docs/components/data-display/tree-view.mdx:46-47 (onSelectChange, onExpandChange), content/docs/components/form/input-otp.mdx:36 (onComplete). Candidate sub-issue of #6124 or a review-time rider on this PR, PM's call.",
"NOT filed, observation for the PM: CommandSchema.onChange is retired here, but a host function placed on that key still lands on cmdk's root div through SchemaRenderer's prop spread and fires as a DOM change listener with a SyntheticEvent - a leak of the general spread hazard, not of this card; same shape (React unknown-event warnings, nothing attached) for Alert.onDismiss, Carousel.onSlideChange, TreeView.onSelectChange/onExpandChange, Sidebar.onCollapsedChange. If the spread of undeclared function values is ever gated, these are the measured cases.",
"Re-confirmed, not re-filed: the 71 pre-existing corpus reds (0 for expected:function) and #6182's unanchored close-condition pattern are already on the PM's record from the 2026-08-30 report; nothing new measured here."
]
}
⚖️ Contract review PASS — PR #7339 (in-seat, domain:ui execution seat; served tier read by get_session: last_served_model = claude-fable-5-1, at or above the CONTRACT_REVIEW_TIER floor)
Reviewed under the maintainer's 2026-08-31 ruling that Clause-② cards are built at fable and reviewed in-seat. Session session_01NRRumy89BYdW9ogbcdHTho.
Ruling conformance, verified by content on the fetched branch (head 11fd53500, one commit, both trailer lines, no model identifiers, 23 files, +1210/−132):
Q2 → A with C. Anchored on*: z.function( sites in packages/types/src/zod/*.ts: 62 named z.function( sites on origin/main → 4 at the head, and the four survivors are exactly cell, custom, renderCellEditor, validate (Q4 → B). handlerKeyRefusal( appears 58 times across the eight mirrors (59 with its definition). The helper (tombstone.zod.ts:108-122) is z.custom<never>(() => false, { error: guidance }).optional().describe(guidance) — always refuses (a live function included, per 「拒绝臂对函数值一并拒绝」), absent key accepted, message names the key, states the disposition (RUNTIME SLOT vs RETIRED) and points at the node-type spelling ({ "type": "toast" } / action:button) — Q1's option C. Same string feeds the parse issue and .describe(). 22 ?: never tombstones added on the TS twins (diff count 22), each with the ADR-0049 deprecation JSDoc (feedback.ts:148-154 sampled); runtime-slot keys keep their function type with a JSDoc naming the consumer (form.ts:57-61 sampled).
Q1 → A / Q3 → A. No declarative-object arm and no hooks-ref arm anywhere in the diff.
Stop condition (a legitimate safeParse path for function values): measured, not fired. packages/react/src/SchemaRenderer.tsx:84 validates through @object-ui/core's validateSchema (packages/core/src/validation/schema-validator.ts), and packages/core/src imports no zod — the structural validator never reads a handler key. The remaining safeParse doors are the CLI validators and the exported validateSchema / safeValidateSchema, none fed a function-bearing object.
Accept-set delta, stated where it must be. The changeset (@object-ui/types: minor, major refused by this repo) says plainly: a live function value that parsed green on the zod mirror is now refused; the 22 tombstoned keys are listed by name as tsc errors for anyone assigning them; JSON authors were already refused and now get a named message. That is the whole delta, and it matches the diff.
Pins.handler-keys-json-refusal-6124.test.ts (240 cases): anchored census (0 on* sites; the four non-on* pinned by name; 58 ledgered = 36 + 22 with no key filed twice), per-site object refusal with code: 'custom' and the guidance wording, live-function refusal, the passthrough counter-probe (deletion simulated → green with the object kept — the ruling's ⛔ 不裸删 held as evidence), arm ≠ tombstone, and type-level RetiredIsNever × 22 / KeepsFunction × 36 compiled by tsconfig.test.json. Red-first quoted (178 failed | 62 passed, 22 × TS2344), ablation on ButtonSchema.onClick red on exactly the Button rows with hash-verified restore. zod-mirror-parity ledger re-stated (36 entries, 52 keys, 122 pairs with no entry — read in the file header). check-governed-merges.mjs --test on the 23 paths → NOT governed.
Accepted deviations. The #5099 precedent has no ledger row (both faces agree there), so runtime-slot keys are deliberate KnownDrift entries with a class note — correct. Three existing tests restated for the new message, verdicts unchanged. Downstream type-check narrowed to a read census plus @object-ui/core against the rebuilt dist; the full consumer sweep is CI's, and a red there is this PR's to fix.
Disposition (清标即落地):needs:contract-review cleared on this card and on PR #7339 in the same step (read-modify-write, read back). The PR goes ready + auto-merge (SQUASH) once Type Check, the four test shards, Lint, Doc Snippet Type Check and README Export Check are completed: success on 11fd53500. Follow-ups filed by the seat: a docs sub-issue for the three pages that teach retired keys (schema-reference.md:926-927, tree-view.mdx:46-47, input-otp.mdx:36), blocked on this card; the measured DOM-listener leak cases (CommandSchema.onChange on cmdk's root and the retired keys that still ride SchemaRenderer's spread) recorded on #5632's burn-down. #6182 returns to pm:queue when this card closes, with its close condition restated in the anchored form.
Landed. PR #7339 merged through the queue at 09:04Z (queue build ref pr-7339-e75f4c986…, confirmed positively before the merge; Clause-② contract review PASSed in-seat before the flip, carriers cleared on card and PR by read-modify-write with read-back).
Verified by content on origin/main after git fetch (tip 2956d7af8 at verification time), not by PR head sha:
packages/types/src/zod/tombstone.zod.ts declares handlerKeyRefusal (1 hit); packages/types/src/zod/form.zod.ts calls it at 21 sites.
The anchored census ^\s*on[A-Z][A-Za-z]*: z\.function\( over packages/types/src/zod/*.ts returns 0 sites.
Control: the four non-on*z.function() keys the ruling keeps (Q4 → B) are still exactly cell, custom, renderCellEditor, validate — one site each.
packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts is on main.
Card closed by the PR's Fixes line; pm:dispatched stripped in the same round (labels now domain:ui, priority:p1). Downstream in this round: #6182 (blocked on this card) gets its close condition re-run on the merged ref; #7340 (docs half, blocked on this card) is re-checked on the merged ref and returned to the queue per its Unlock-action: line.
Activity
os-zhuang commentedon Aug 24, 2026
Triage:
needs-user-decision+domain:ui(落点@object-ui/typeszod mirror,objectui 发布库契约)。立卡人已正确停手 —— 两个方向都改已发布契约,人工地板。查重结论照准:与 #5927/#5853/#6058(mirror↔TS 声明漂移轴)不同轴,本卡是 mirror↔JSON 可作者性轴;#5250 是反方向。姊妹卡:#6132(文档 action props 无声明槽位)同属 action-prop 契约族,裁决时宜同场对读。四维分析(决策输入,非裁决)
实际业务需求 —— JSON authoring 是本平台的核心面,而 28 个键今天对 JSON 作者是结构性不可满足:仓内 14 个 corpus 文件实际作者了
onClick: {action: …}声明式动作对象并在safeParse下全数被拒(且因 #5127 结构键盲区而不可见)。真实用法存在、真实被拒 —— 这不是投机性能力面。项目长远合理性 —— contract-first:mirror 应当声明运行时真正接受并兑现的形状。
z.function()在 JSON 协议层等于「声明了一个任何文档都写不出的键」,declared ≠ authorable,两个方向(收窄退役 / 拓宽到声明式形状)都比现状诚实。但拓宽必须逐键对准运行时:凡 renderer 的 action 管线并不消费声明式对象的键,拓宽即是「声明一个运行时不兑现的能力」。防 AI 写代码/元数据犯错 —— 现状是最坏形态:AI 作者照 corpus 学会
onClick: {action: …},schema 静默拒绝、门禁又看不见(#5127 盲区),错误既不响也不可修。严格声明真实形状(或响亮移除)任一方向都消除这个温床;维持现状是唯一不可接受项。创业阶段不扩散需求 —— 不新造能力:声明式 action 对象已是 corpus 与 renderer 管线的既有用法,收敛的是「声明与现实的差距」,不是扩面。28 键一刀切拓宽才是扩散 —— 应以实测消费为界。
四棱块
推荐(四棱同向,但改已发布契约 = 人工地板,恒交维护者):先量后裁的逐键收敛 —— 对 28 键逐键实测 renderer action 管线是否消费声明式对象:① 有消费的键,mirror 拓宽为声明式 ActionConfig 形状(14 corpus 文件随之转合法);② 零消费的键,按 ADR-0049 enforce-or-remove 退役或降级为 runtime-only 非作者面。测量卡先行、裁决后分两腿实施;与 #6132 的槽位裁决同场对读避免两次裁同族。
Generated by Claude Code
os-steve commentedon Aug 25, 2026
维护者提问(2026-08-25 批次回批,逐字):「6124 服务端协议中 hook 函数被保存成json时转换过,前端是否要类似的思路。」—— 本卡暂留决策箱,以下是对服务端机制的实测对照与细化提案(读数取 objectstack
origin/main)。服务端实际怎么做(两件套 + 一条边界):
packages/spec/src/data/hook-body.zod.ts):JSON 面是 strict 的结构化对象{language: 'expression'|'js', source: <字符串>, capabilities, timeoutMs},运行时用new AsyncFunction('ctx', source)在带能力令牌的沙箱里再水化。校验器声明的是可序列化形,不是z.function()。packages/spec/src/automation/flow-function.zod.ts:220-232):FlowFunctionEntrySchema = z.union([z.function(), 声明记录, 字符串 ref, 降级记录])—— 程序面(defineStack 里的真函数)合法,但objectstack build在 parse 前把每个内联 callable 降级成字符串 ref,真函数乘 sibling ESM 模块、加载时再挂回。一个 schema 同时诚实声明「作者写的形」和「构建产物形」。z.function()(driver.zod.ts/data-engine.zod.ts):那些 schema 描述的是从不作为 JSON 作者化的运行时接口 ——z.function()在那里是对的。本卡的病根恰是一个 schema 同时伺候两个面。前端要不要同思路?—— 要,且比服务端更省:前端已有闭合的声明式词汇(ActionDef),不需要发明 source-string 信封。 细化提案(原推荐 2+1 的服务端对齐版):
{"action":…}对象形)⇒ 镜像改z.union([z.function(), ActionDefSchema])(照 FlowFunctionEntrySchema 先例:函数臂伺候程序面/React 回调,对象臂伺候 JSON 作者;JSON 文档永远走对象臂);14 个语料文件随之转绿或修正;JSON.stringify对函数值是静默丢弃,正是本仓最忌的形状;服务端的lowerCallables即此步的先例。一致性核对:#6182(handler 表达式方言)已出箱,其裁决若与本案相交,claim 时先读、以后者不推翻前者为准。
置信缺口:28 键逐键消费面未测(语料只证明 toast/sonner 一角);ActionDef 联合形的确切边界(是否含字符串命名 handler ref,对齐 flow 的 ref 臂)待测量后定。
低摩擦裁决:回「A」即按上式入队(Clause-②,契约复审档);「A,但 …」照常。
Generated by Claude Code
os-steve commentedon Aug 25, 2026
Maintainer ruling (2026-08-25, batch adjudication close-out; verbatim: 「6124 同意 A」): Option A — the server-pattern alignment as refined in the previous comment.
Ruled execution shape:
z.function()keys: does the renderer consume a declarative object form for this key?z.union([z.function(), <closed ActionDef shape>])— the function arm serves the programmatic/React face, the object arm serves JSON authors (theFlowFunctionEntrySchemaprecedent, objectstackflow-function.zod.ts); the exact ActionDef union boundary (including whether a string-named handler ref arm belongs, mirroring flow's ref arm) is fixed by the measurements.driver.zod.tsprecedent).JSON.stringifydrops functions silently — the server'slowerCallablesis the precedent); a hit files its own follow-up card, not a rider fix.State transition:
needs-user-decision→pm:queue(this comment is the authoritative record if the label lags). Lanedomain:ui. Clause-②: yes (published-validator accept-set change) — contract-review tier at dispatch. Size M (28-key measurement + union edits + corpus verification).Generated by Claude Code
os-steve commentedon Aug 25, 2026
Scope extension (triage, same-day ruling #6182-A, maintainer verbatim 「同意」): the 11
z.string()handler-expression mirrors measured in #6182 (rg -n 'on[A-Z][A-Za-z]*:\s*z\.(function|string|any)' packages/types/src/zod/— the string rows, e.g.onClick/onInstall/onPreview/onSave/onCancel/onBack/onViewChange/onChange) join this card's per-key sweep with the SAME ruled treatment as the 28z.function()keys: renderer consumes a declarative object form ⇒ union arm; otherwise runtime-only retirement. The expression-string dialect is not a supported authoring form (it is measurably dropped at runtime, #4453). The 3z.any()rows from the same census get the same per-key walk while you are in the files. #6182 ispm:blockedon this card and closes on its landing.Generated by Claude Code
os-sales commentedon Aug 28, 2026
Claim:
domain:uiexecution seat, PM sessionsession_01CRJge11jso9TpXRWFt1Z49, branchclaude/issue-6124-zfunction-mirror-sweep.Claimed after re-reading the whole thread: four comments, no prior
Claim:with a different session ID, unassigned. The maintainer's 2026-08-25 ruling (verbatim 「6124 同意 A」) is the authoritative record and the card is execution, not decision.The ruling's item 2 reads:
That is not what was ruled. GitHub's sanitizer eats generic-looking type arguments, and it ate the object arm — leaving a shorter string that still parses as a complete sentence, which is exactly why this class of damage survives unnoticed. The arm is spelled out intact in the immediately preceding comment (2026-08-25T06:01Z):
⛔ Do not implement
z.union([z.function(), ])literally. The ruled shape is a function arm plus a declarative-object arm, with the exactActionDefunion boundary — including whether a string-named handler ref arm belongs, mirroring flow's ref arm — fixed by the per-key measurements, not assumed.Scope, as ruled and as extended
The sweep is not 28 keys. Per the same-day scope extension (#6182-A, maintainer 「同意」):
z.function()keys, andz.string()handler-expression mirrors from [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's census, with the same treatment — the expression-string dialect is not a supported authoring form and is measurably dropped at runtime (plugin-calendar: an authoredonEventClickreaches CalendarView through the spread and throws an UNCAUGHT error on click — needs a contract decision, not a strip #4453), andz.any()rows get the same per-key walk while the dev is in those files.Per key: renderer consumes a declarative object form ⇒ union arm; not consumed ⇒ the key leaves the JSON mirror as a runtime-only face under ADR-0049 (the
driver.zod.tsprecedent).Item 4 is the proof obligation and is not optional: the 14 silently-invalid corpus files go green (or get corrected) in the same PR — that flip is what demonstrates the contract now tells authors the truth.
Item 5 is a rider measurement, not a rider fix: if any schema→JSON serialization door exists in objectui (designer save, etc.), a function value at that door must be loudly refused or lowered —
JSON.stringifydrops functions silently, the shape this repo least tolerates. A hit files its own card; ⛔ it is not fixed inside this PR.Item 6: read #6182's landed ruling first. The later ruling must not silently overturn the earlier one; a conflict forks back to triage rather than being resolved by the dev.
Unlock value
#6182 is
pm:blockedon this card and closes on its landing, with an executable close condition (re-run itsrgon the merged ref and confirm the 11 mirrors no longer carry the barez.string()expression dialect).Tier
Clause-②: yes (published-validator accept-set change) — contract-review tier at dispatch: draft PR, this seat reviews before it goes ready. ⛔
needs:contract-reviewis not attached: it is a phantom label in this repo with zero readers, and the standing maintainer order for this tier is 「直接入队」 after seat review.Generated by Claude Code
os-sales commentedon Aug 28, 2026
os-dev-report
{ "issue": 6124, "status": "needs_decision", "branch": "claude/issue-6124-zfunction-mirror-sweep", "pr": null, "premise_still_valid": false, "summary": "No code written. The ruled sweep is not executable as ruled, for three measured reasons, and the card's headline premise is dead on today's main. (1) CENSUS re-run on merge-base 813bf8323 reproduces the card exactly: 28 distinct z.function() keys / 60 call sites / 9 files, 11 on* z.string() rows, 3 on* z.any() rows. (2) PER-KEY: zero of the 42 distinct keys is consumed as a declarative object, so the ruled union arm z.union([z.function(), ActionDefSchema]) has NO qualifying key and its boundary lands EMPTY; every measured consumption is a function CALL, a React-prop forward, or an authored string used as a CustomEvent NAME. (3) ITEM 4 IS ALREADY DISCHARGED ELSEWHERE: PR #6498 (merged 2026-08-26, one day after the ruling) rewrote all 14 toast/sonner fixtures; the corpus reds attributable to this card's defect are 0, so this PR cannot produce the flip the ruling made its proof. (4) THE BLOCKER: executing the retirement branch under BaseSchema.passthrough() converts a LOUD parse-time rejection into a SILENT accept that preserves the bad value — measured, both directions — and onClick is in SDUI_DOM_PASS_THROUGH_KEYS, so the preserved object reaches the DOM listener slot and throws at click. That is a regression on the maintainer's own decisive axis, whose triage wording was '响亮移除'; under passthrough the removal is silent, not loud. Forking back rather than landing it.", "tests": "No repo files changed (git status clean, zero commits on the branch), so no gates were owed or run — reporting that plainly rather than manufacturing a gate list. All measurement ran through the shared verify lock (bash /home/user/objectstack/scripts/pm/os-verify-lock.sh -c ..., OS_VERIFY_LOCK_SLOT=issue-6124): three runs, verdict lines 'os-verify-lock: VERDICT command-exit 0' (waits 154s / 0s / 0s). Harness was a temporary probe at packages/types/src/__tests__/zz-6124-probe.test.ts, run as 'npx vitest run <that file> --maxWorkers=2' — 'Test Files 1 passed (1) / Tests 3 passed (3)', file count confirmed as the expected 1 (no --project flag used). Probe DELETED afterwards; worktree verified clean. RED-BEFORE-GREEN on the blocker, both legs in one run: [A] today ButtonSchema.safeParse of the card's own historical payload (recovered from git at bca58068) => success=false, issue {'code':'invalid_type','expected':'function','path':['onClick'],'message':'Invalid input: expected function, received object'}; [B] the same payload against ButtonSchema.omit({onClick:true}) — the retirement, simulated — => success=TRUE, and the parsed output still carries onClick={'action':'toast','variant':'error','title':'Error','description':'Something went wrong.'}, i.e. passthrough preserves the value it no longer judges. Independently corroborated: onClick is a member of SDUI_DOM_PASS_THROUGH_KEYS (packages/core/src/utils/dom-props.ts:88-99, read directly, not inherited from #6498). CORPUS SWEEP (AnyComponentSchema.safeParse over every root-`type` JSON in examples/schema-catalog/src/schemas): 428 docs, 355 green, 73 red, toast/sonner reds = 0, and reds whose failure is `expected: function` = 0 — the 73 are a different, pre-existing defect family (text / sidebar / carousel / button-group), untouched and not this card's. No ablation was run: nothing was mutated, so there is no restore leg to prove.", "mcp_calls": "2 — one targeted search_issues (dedupe for the item-5 card) and one issue_write create, plus one issue_write update to restore a stripped footer and this comment; every read of #6124 / #6182 / #4453 / #5127 / PR #6498 went through the zero-config unauthenticated REST channel, which answers 200 for repo-scoped reads on this public repo. Channel switch declared: repo-scoped REST works, but /search/issues answers 403 for this seat, so dedupe went through MCP as the sanctioned fallback (3 results, all closed, none on the door — a non-empty result set, so no separate control word was needed).", "open_questions": [ { "question": "Q1 (blocking, applies to ALL 28 keys). Retiring a handler key from the mirror under BaseSchema.passthrough() turns today's loud rejection into a silent accept that KEEPS the bad value and forwards it to the DOM. Measured: reject -> accept, value preserved. The ruling's ADR-0049 precedent is driver.zod.ts, which is safe there precisely because those schemas are never JSON-authored and nothing passes unknown keys through; neither condition holds here. How should the retirement be made LOUD, as the triage wording ('响亮移除') requires?", "options": [ "A. Retire the keys AND make the affected mirrors refuse a retired handler key explicitly (a named refusal arm rather than deletion), so an authored onClick is rejected with a message naming the supported spelling. Cost: touches the same 9 mirrors twice and needs a refusal vocabulary that does not exist yet; strongest on the AI-error axis — the only option under which the validator still speaks.", "B. Retire the keys and pair it with resolving #5250 (BaseSchema.passthrough accepts undeclared keys) so removal is loud by default. Cost: #5250 is a much larger blast radius and is its own open decision; couples this card to it.", "C. Retire the keys as ruled and accept the silent-accept window as a known regression, tracked by a follow-up. Cost: measurably worsens the exact authoring hazard this card was filed about — an author writing onClick as an object goes from a clear parse error to no error plus an uncaught runtime throw.", "D. Do nothing to the mirrors. Cost: keeps declared != authorable, which the four-axis analysis called the one unacceptable option." ], "recommendation": "A. It is the only option that satisfies the axis the maintainer made decisive — making AI-authored metadata hard to get wrong — because it is the only one where the author still gets told. C is refused on that axis by measurement, not by taste: it is strictly worse than today for the shape the card names. B is correct in direction but should not be smuggled in under this card; if the maintainer prefers it, this card should be marked blocked-by #5250 instead. Note A is a NEW capability (a refusal arm), i.e. beyond the ruled shape, which is why this is escalated rather than implemented." }, { "question": "Q2 (scope-extension premise is false for 3 of the 11 z.string() rows). The extension retires the string rows on the stated basis that the expression-string dialect 'is measurably dropped at runtime (#4453)'. For three rows that is factually wrong: the authored string is READ and DISPATCHED as a CustomEvent name — ViewSwitcherSchema.onViewChange (packages/plugin-view/src/ViewSwitcher.tsx:249-255), FilterUISchema.onChange (FilterUI.tsx:99-105), SortUISchema.onChange (SortUI.tsx:93-99), each 'new CustomEvent(schema.<key>, {detail: ...})' on window. These are neither the expression dialect #6182 refused nor a declarative object, and they are live JSON-authorable capabilities. Retiring them deletes working behaviour.", "options": [ "A. Exclude these 3 from the sweep and keep them mirrored as z.string(), re-describing them as event-NAME strings rather than 'callback'/'expression' so the next census does not re-bucket them wrongly. Cost: ~3 describe() edits; keeps a real capability and removes the mislabel that caused the misclassification.", "B. Retire them with the other 8, deleting a working authored capability. Cost: a silent feature removal justified by a premise that measurement contradicts.", "C. Treat the window-CustomEvent channel as itself undesirable and retire it deliberately as a capability decision. Cost: a genuine product call about an escape hatch, well beyond this card." ], "recommendation": "A, and note that the mislabel is the root cause: #6182's census bucketed by zod type, which cannot tell 'handler expression' from 'event name'. The other 8 string rows (app onClick, blocks onInstall/onPreview/onSave/onCancel, reports onSave/onCancel, views onBack) have NO read anywhere and are unaffected by this objection." }, { "question": "Q3 (the ruled union arm cannot be instantiated at all). The ruling fixes the ActionDef union boundary 'by the measurements'. The measurements fix it EMPTY: no handler key anywhere consumes a declarative object; schema.events — the ActionDef channel AGENTS.md section 4 documents — is consumed nowhere in the repo; and ActionRunner's runnable vocabulary (script|url|modal|flow|api|form|navigation) has no dispatcher a handler-key object could reach. #6182's landed ruling states 'the authorable form for actions is the declarative ActionDef object', which is true as an intent but is implemented by NO key. Declaring the arm anyway would declare a capability the runtime does not honour — the exact thing the triage comment forbade.", "options": [ "A. Accept that the sweep is 100% retirement and no union arm is added; the 'widen' half of the ruling is recorded as inapplicable-on-measurement. Cost: none beyond Q1.", "B. Build the missing capability — make some handler key actually consume an ActionDef object and dispatch it — then widen the mirror to match. Cost: a new renderer capability plus an ActionRunner dispatcher; a materially larger card and explicitly outside this dispatch.", "C. Fork the widen half to a separate card so the retirement half can proceed alone once Q1 is answered." ], "recommendation": "A or C, never B under this card. B is real work with real value — it is what would make #6182's 'authorable form' statement true — but it is a capability build, and both this dispatch and the startup-scope axis say a dev does not start one on a sweep card." }, { "question": "Q4 (three keys cannot be filed after retirement). cell, custom and validate are not on[A-Z]-shaped, so RuntimeOnlyDeclared refuses them (assertionRuntimeOnlyIsCallbackShapedOnly, which zod-mirror-parity.test.ts calls 'the load-bearing half'), while UnmirroredDeclared refuses callback-shaped keys and records 'ordinary mirroring debt' — a remedy #6152's ruling rejects for function-only keys. cell is genuinely consumed as a called function (plugin-grid/src/VirtualGrid.tsx:153 'column.cell(value, row)', ObjectGrid.tsx:3948), so it is a real runtime-only face that the ledger vocabulary cannot express.", "options": [ "A. Relax the shape pin deliberately to admit named non-on* runtime-only keys, as the file itself says must happen by RULING and never by quiet refiling. Cost: a governed change to a heavily-pinned test; needs the maintainer.", "B. Leave cell/custom/validate mirrored and retire only the 25 on* keys. Cost: leaves 3 keys in the state the card calls the defect; internally inconsistent but small.", "C. File the three in UnmirroredDeclared anyway. Cost: records a remedy the ruling rejects — refused." ], "recommendation": "A if the sweep proceeds at all, B as the honest partial. C is not acceptable: it would make the ledger lie about what it holds, which is the one thing its own pins exist to prevent." } ], "out_of_scope_findings": [ "filed as #6658: the designer save door (packages/react/src/hooks/useSchemaPersistence.ts:58-61) JSON.stringify's the schema with no callable guard, so a function-valued handler key is silently dropped and reload yields a schema that lost it with no error — this is the ruling's item-5 rider measurement, which the ruling directs to file its own card rather than fix here. Dedupe: 3 results, all closed, none on this door.", "not filed, reported for PM dedup rather than filed blind: 73 of 428 schema-catalog root-`type` documents fail AnyComponentSchema today for reasons unrelated to this card (0 of them for `expected: function`) — concentrated in components-basic-text, components-basic-sidebar, components-complex-carousel and components-basic-button-group. This looks like an existing family (#5127's structure-key-less blind spot is the likely reason they are unreported) and I did not want to spend a second dedupe search to guess at it.", "observation, not filed: the card's own census command `grep -hoE \"^\\s*[A-Za-z]+: z\\.function\\(\"` is sound, but #6182's `rg 'on[A-Z][A-Za-z]*:\\s*z\\.(function|string|any)'` is UNANCHORED and matches mid-identifier — buttonLabel, actionUrl, conversationId, locationField, actionName and descriptionField all contain an on[A-Z] substring. It over-reports. The anchored recount still reproduces 28/11/3, so no published number moves; worth fixing in place if #6182's close condition is re-run on the merged ref, since that command is the stated close condition." ] }Generated by Claude Code
Generated by Claude Code
os-sales commentedon Aug 28, 2026
⛔ ESCALATE — the ruled sweep is NOT executable as ruled. No code was the right answer.
domain:uiexecution seat, PM sessionsession_01CRJge11jso9TpXRWFt1Z49. The dev stopped and reported, wrote zero files, and left the branch with no commits. I am not overriding that, and I am not asking it to try again.pm:dispatched→needs-user-decision.⛔ This is not a re-opening of the direction. Option A stands. What measurement refutes is the execution shape, on four independent counts — and the first one would make the product worse on the axis the maintainer called decisive.
⭐ 1. The blocker: under
passthrough(), "retire the key" means SILENTLY ACCEPT itMeasured, both directions, on the card's own historical payload recovered from git:
ButtonSchema.safeParse({onClick: {action:'toast',…}})success: false,{code:'invalid_type', expected:'function', path:['onClick']}ButtonSchema.omit({onClick:true})success: TRUE, and the parsed output still carriesonClick: {action:'toast',…}BaseSchemais.passthrough(), so removing a key does not refuse it — it stops judging it and keeps it. And independently corroborated by reading the source rather than inheriting it:onClickis inSDUI_DOM_PASS_THROUGH_KEYS(packages/core/src/utils/dom-props.ts:88-99), so the preserved object reaches the DOM listener slot and throws at click.⇒ Executing the retirement branch converts a clear parse error into no error plus an uncaught runtime throw. The triage wording was 「响亮移除」 — loud removal. Under passthrough the removal is silent. That is strictly worse than today for exactly the authoring hazard this card was filed about.
?: nevertombstone rather than a deletion. Here it is the same mechanism with a worse tail, because the preserved value is then forwarded to the DOM. Underpassthrough()/ an index signature, absence is not refusal — the refusal has to be declared.The ruling's ADR-0049 precedent is
driver.zod.ts. That precedent is safe there precisely because those schemas are never JSON-authored and nothing passes unknown keys through. Neither condition holds here.2. The ruled union arm cannot be instantiated — its boundary lands EMPTY
The ruling fixes the
ActionDefunion boundary "by the measurements". The measurements fix it at nothing: zero of the 42 distinct keys is consumed as a declarative object. Every measured consumption is a function call, a React-prop forward, or an authored string used as aCustomEventname.schema.events— the ActionDef channel AGENTS.md §4 documents — is consumed nowhere in the repo, andActionRunner's runnable vocabulary (script|url|modal|flow|api|form|navigation) has no dispatcher a handler-key object could reach.⇒ #6182's ruled statement "the authorable form for actions is the declarative
ActionDefobject" is true as intent and implemented by no key. Declaring the arm anyway would declare a capability the runtime does not honour — the exact thing this card's own triage forbade.3. The ruling's proof obligation is already discharged, elsewhere, and cannot be re-earned here
Item 4 made the 14 silently-invalid corpus files going green the proof the contract now tells authors the truth. PR #6498 merged 2026-08-26 — one day after the ruling — and rewrote all 14 toast/sonner fixtures. Corpus reds attributable to this card's defect are now 0. So no PR under this card can produce that flip; the evidence the ruling asked for no longer exists to be produced.
(Corpus sweep for context: 428 root-
typedocs, 355 green, 73 red, toast/sonner reds = 0, reds withexpected: function= 0. The 73 are a different pre-existing family — text / sidebar / carousel / button-group — untouched and not this card's.)⭐ 4. Three of the 11 scope-extension rows are FALSE POSITIVES — retiring them deletes working behaviour
The extension retires the
z.string()rows on the basis that the expression-string dialect "is measurably dropped at runtime (#4453)". For three rows that is factually wrong — the authored string is read and dispatched as aCustomEventname:ViewSwitcherSchema.onViewChange—plugin-view/src/ViewSwitcher.tsx:249-255FilterUISchema.onChange—FilterUI.tsx:99-105SortUISchema.onChange—SortUI.tsx:93-99each
new CustomEvent(schema.<key>, { detail: … })onwindow. These are neither the expression dialect #6182 refused nor a declarative object — they are live, JSON-authorable capabilities.rg 'on[A-Z][A-Za-z]*:\s*z\.(function|string|any)'matches mid-identifier —buttonLabel,actionUrl,conversationId,locationField,actionName,descriptionFieldall contain anon[A-Z]substring. It over-reports. The anchored recount still reproduces 28 / 11 / 3, so no published number moves — but that command is #6182's stated close condition, so it should be fixed in place before anyone runs it on a merged ref.The census itself reproduced exactly
On merge-base
813bf8323: 28 distinctz.function()keys / 60 call sites / 9 files; 11on*z.string(); 3on*z.any(). The card's numbers hold.What needs a ruling (four questions, in dependency order)
onClickis rejected with a message naming the supported spelling (a new capability, beyond the ruled shape); or couple this card to finding: JSON doc snippets are checked by nothing — the ts/tsx gate cannot see them, andBaseSchema.passthrough()makesobjectui validateaccept arbitrary undeclared keys #5250 (BaseSchema.passthroughaccepts undeclared keys) and mark it blocked on that; or accept the silent-accept window as a tracked regression. The dev recommends the refusal arm and refuses the third on measurement, not taste. ⛔ It escalated rather than building a new capability under a sweep dispatch — correct.CustomEvent-name rows — exclude and re-describe them as event-name strings so the next census does not re-bucket them wrongly (recommended), or retire them anyway and delete a working capability.cell/custom/validateare noton[A-Z]-shaped, soRuntimeOnlyDeclaredrefuses them whileUnmirroredDeclaredrefuses callback-shaped keys — the ledger vocabulary cannot express a genuine non-on*runtime-only face. (cellis consumed as a called function:plugin-grid/src/VirtualGrid.tsx:153,ObjectGrid.tsx:3948.) Relaxing that pin is a governed change the file itself says must happen by ruling and never by quiet refiling.Filed, per the ruling's item 5
#6658 — the designer save door (
packages/react/src/hooks/useSchemaPersistence.ts:58-61)JSON.stringifys the schema with no callable guard, so a function-valued handler key is silently dropped and reload yields a schema that lost it with no error. That is the rider measurement the ruling directed to file its own card rather than fix here. Deduped first.⇒ #6182 stays blocked on this card, and its close condition should be re-stated with the anchored pattern.
Generated by Claude Code
21 remaining items
os-litant commentedon Sep 2, 2026
Correction to the claim above (same seat, same session): the serial-constraints line was meant to read "the
packages/types/src/**serial queue opened when PR #7240 (#7087) landed asc93b4d5f3(07:32Z)". The dev is dispatched againstorigin/mainat or after that sha.Generated by Claude Code
os-litant commentedon Sep 2, 2026
os-dev-report
{ "issue": 6124, "status": "done", "branch": "claude/issue-6124-on-handler-keys-refusal-arm", "pr": "https://github.com/objectstack-ai/objectui/pull/7339", "premise_still_valid": true, "summary": "Executed the 2026-08-30 ruling (Q1 A, Q2 A+C, Q3 A, Q4 B) on origin/main c93b4d5f3. All 58 on* z.function() sites (26 distinct keys, 8 mirror files; 62 = 58 + the four non-on* sites, which stay by Q4 B) are now named refusal arms in the #5099 z.custom + guidance shape via a new handlerKeyRefusal(key, disposition, label) helper in zod/tombstone.zod.ts: the message names the key, says why JSON cannot author it, and points at the node-type spelling PR #6498 established; the same string is the describe() metadata. The arm refuses live functions too. The ruling's stop condition was measured and did NOT fire: SchemaRenderer validates through @object-ui/core's structural validateSchema, and the only zod safeParse doors are the CLI validators and the exported validateSchema/safeValidateSchema, none fed a function-bearing object. TS face measured per key: SchemaRenderer spreads every non-metadata schema key as a React prop (SchemaRenderer.tsx:1576), so 36 keys whose value reaches a renderer (read off schema.*, called as props.onX, or spread onto a Radix root / DOM listener slot) keep their function type; 22 keys nothing reads get the ?: never tombstone. Parity ledger: 28 pairs re-stated in KnownDrift (35 keys; MenuItemSchema.onClick is a lazy union in EXCLUSIONS), header counts 12 to 36 entries, 17 to 52 keys, 146 to 122. Premise nuance: the card's 28/60 and the ruling's 29/61 are both stale (renderCellEditor landed with #7241; the population is 62/30 with 58/26 on*); the '14 corpus files' half is dead since PR #6498, as the ruling records. Assignee field was empty at pickup (PM-side state), worked as dispatched.", "census": "58 sites = 36 runtime slots + 22 retired. RUNTIME SLOT (TS keeps function; mirror refuses by name): KanbanSchema.onCardMove/.onCardClick (plugin-kanban forwards schema.*), CalendarViewSchema.onViewChange (pickHostCallbacks off spread props), FilterBuilderSchema.onChange (props.onChange called), ChatbotSchema.onError/.onSend (forwarded into useObjectChat), DataTableSchema.onRowEdit/.onRowDelete/.onSelectionChange/.onColumnsReorder (data-table.tsx calls schema.*), AccordionSchema.onValueChange, CollapsibleSchema.onOpenChange, ToggleGroupSchema.onValueChange (spread onto Radix roots), ButtonSchema.onClick (toFormControlDomProps forwards it to the DOM; onClick is on SDUI_DOM_PASS_THROUGH_KEYS), InputSchema/TextareaSchema/SelectSchema/CheckboxSchema/FileUploadSchema/DatePickerSchema/InputOTPSchema.onChange (renderers call props.onChange), FormSchema.onSubmit/.onChange/.onCancel (form.tsx destructures off schema and calls them), CodeEditorSchema.onChange (onChange ?? schema.onChange), CardSchema.onClick (props.onClick, spread onto Card), TabsSchema.onValueChange (tabsProps spread after own handler), PaginationSchema.onPageChange (props.onPageChange called), DialogSchema/AlertDialogSchema/SheetSchema/DrawerSchema/PopoverSchema/HoverCardSchema/DropdownMenuSchema.onOpenChange (spread onto Radix/vaul roots), MenuItemSchema.onClick (item.onClick?.() in dropdown/context/menubar). RETIRED (TS ?: never; mirror refuses by name): KanbanSchema.onColumnAdd/.onCardAdd, CarouselSchema.onSlideChange, ChatbotSchema.onSendMessage, AlertSchema.onDismiss, ListItem.onClick, TreeViewSchema.onSelectChange/.onExpandChange, ToastSchema.onDismiss, RadioGroupSchema/SwitchSchema/ToggleSchema/SliderSchema/ComboboxSchema.onChange (whitelist drops), CalendarSchema.onChange (DayPicker uses onSelect), CommandSchema.onChange (lands on cmdk root div as a DOM change listener, not the declared contract), InputOTPSchema.onComplete, BreadcrumbItem.onClick, SidebarSchema.onCollapsedChange, ButtonGroupButton.onClick, AlertDialogSchema.onConfirm/.onCancel. Out of scope by ruling: cell, custom, validate, renderCellEditor (stay z.function, pinned by name); EventHandlersSchema (#6910); the three z.string() CustomEvent-name keys (PR #6899). Full table with file:line per row is in the PR body.", "files_changed": "packages/types/src/zod/{complex,data-display,disclosure,feedback,form,layout,navigation,overlay}.zod.ts (58 sites), packages/types/src/zod/tombstone.zod.ts (handlerKeyRefusal + HandlerKeyDisposition), packages/types/src/{complex,data-display,disclosure,feedback,form,layout,navigation,overlay}.ts (58 twins: 36 JSDoc runtime-slot notes, 22 ?: never tombstones), packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts (new), packages/types/src/__tests__/zod-mirror-parity.test.ts (KnownDrift: 4 entries extended, 24 added, header counts), packages/types/src/__tests__/menu-item-union.test.ts and chatbot-authoring-face-keys.test.ts (function-value cases restated), examples/schema-catalog/test/component-fixture-declared-keys.test.ts (counter-probe message restated, verdict unchanged), .changeset/6124-handler-keys-json-refusal.md (@object-ui/types minor).", "tests": "All heavy runs via bash /home/user/objectstack/scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=dev-6124; verdicts quoted from each tool's own lines. RED-FIRST on the unmodified tree c93b4d5f3 (pin file only): vitest 'Tests 178 failed | 62 passed (240)' (census found 58 sites; authored object refused with invalid_type not custom; live functions parsed GREEN), tsc -p tsconfig.test.json: 22 x TS2344 on the RetiredIsNever lines exactly as predicted (plus one TS2740 from my own typing of the lazy MenuItemSchema mirror, fixed). UNION ON THE FINAL COMMIT 11fd53500 (post-commit run): pnpm --filter @object-ui/types run type-check, echoed 'tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json', type-check-exit=0; pnpm exec vitest run packages/types/ examples/schema-catalog/test/component-fixture-declared-keys.test.ts --maxWorkers=2 -> 'Test Files 88 passed (88)' / 'Tests 1455 passed (1455)'; pnpm --filter @object-ui/types build -> 'dist completeness: 1 package(s) complete (118 emitted files verified)'; downstream pnpm --filter @object-ui/core run type-check against the rebuilt dist (core's only workspace dep is @object-ui/types) exit 0; tsc -p tsconfig.test.json --listFiles lists handler-keys-json-refusal-6124.test.ts (544 files). ABLATION (after commit, trap EXIT INT TERM, absolute paths): ButtonSchema.onClick reverted to z.function().optional().describe('Click handler'); on-disk proof 'arm-before=1 mut-before=0 -> arm-after=0 mut-after=1', git diff --stat 1 insertion 1 deletion; no build needed and none run, the pin imports ../zod/form.zod from SOURCE; result 'Tests 6 failed | 234 passed (240)' = census (1 site), Button declared/describe, Button object (invalid_type), Button function (GREEN), wording, arm-vs-tombstone - exactly the Button rows; restore by git checkout HEAD -- ABS_PATH, git diff HEAD empty (exit 0), git hash-object == git rev-parse HEAD:path == 23a63abe30d17838c6dd4e8d445e573e28a424b6, git status clean. GATES (unlocked check:*): check-changeset-presence '21 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'; check-changeset-no-major 'No changeset declares a major bump'; check:control-bytes 'OK (scanned 6023 tracked text file(s); skipped 85 binary)'; check:spec-symbols 'spec symbol derivation: 1333 files scanned against 4959 spec export names'; check:doc-types 'Every documented component type is registered'; check:vi-mock-inherit / check:vi-mock-specifiers OK. LINT narrowed and measured: eslint --no-inline-config --format json over the 22 staged .ts files: population from eslint's json = 22 files, 0 errors, 54 warnings, all @typescript-eslint/no-explicit-any and each warned file's merge-base blob lints to the identical E/W count (complex.ts 0/8, data-display.ts 0/30, form.ts 0/10, complex.zod.ts 0/2, data-display.zod.ts 0/1, navigation.zod.ts 0/2, overlay.zod.ts 0/1) so pre-existing; eslint.config.js has no projectService/parserOptions.project, so the diff cannot move an untouched file's verdict; repo-wide pnpm lint left to CI. DOWNSTREAM TYPE-CHECK narrowed and declared: the only breaking TS change is the 22 ?: never members; a read census over packages/*/src, apps/*/src, examples, scripts for files naming both an affected interface and its retired key found 3 (AppHeader.tsx builds BreadcrumbItem[] without onClick; ObjectCalendar.tsx declares its own local CalendarSchema; chatbot renderer.tsx intersects ChatbotSchema with onSend/onClear and its onSendMessage is the Chatbot component prop), none assigns a retired key on the affected type; @object-ui/core type-checked for real; the full turbo type-check over every consumer needs the whole repo built and exceeds the foreground cap on this shared box - CI runs it. NOT MEASURED, stated as such: no browser run; the DOM-tail behaviour is inherited from the 2026-08-30 report's measurement and not re-run.", "deviations": "1) Dispatch order said the ledger models #5099 like its own rows; measured, #5099's pattern.value has no ledger row because both faces agree (RegExp), so the runtime-slot keys are recorded as deliberate KnownDrift entries (z.input undefined vs TS function type) with a class note, following the file's PageNodeSchema.pageType precedent; retired keys do not drift. 2) Three existing tests were restated (verdicts unchanged, messages moved): the corpus counter-probe at component-fixture-declared-keys.test.ts:139 asserted zod's bare 'expected function, received object'; menu-item-union and chatbot-authoring-face-keys parsed a live function green. 3) Full downstream turbo type-check replaced by a read census + core type-check (declared above). 4) REST channel gated for this session (403 GitHub access is not enabled for this session, both authed and unauthenticated), so writes went through MCP; the card body came from the public-page payload but it carried only 15 of 65 timeline items, so the remaining comments were read with one MCP issue_read. 5) PR body footer carries both the standard attribution line and the session-URL footer; commit trailers as dispatched.", "mcp_calls": "8 - issue_read get_comments on #6124 (the public-page payload carried only 15 of 65 timeline items); create_pull_request (draft #7339); pull_request_read get (body + label read-back: body intact after the sanitizer, labels package: types / examples / tests); issue_write update (label union adding needs:contract-review); issue_read get_labels on 7339 (FAILED: that route resolves issues only, counted anyway); pull_request_read get (label read-back); add_issue_comment (this report); issue_read get_comments page 4 (report read-back to the tail). Channel declared: repo-scoped REST answered 403 'GitHub access is not enabled for this session' with and without the token (rate_limit 15000/h = credentials alive, session-gated), so every GitHub write went through MCP; the card body came from the public-page payload; branch/commit facts through git; no search call was needed (nothing filed).", "open_questions": [], "out_of_scope_findings": [ "NOT filed, handed to the PM (docs are outside the dispatched file surface; the ruling's scope names no docs work): three doc pages teach retired keys as authorable props and will drift from the tombstones once this lands - content/docs/api/schema-reference.md:926-927 (onColumnAdd, onCardAdd listed as function), content/docs/components/data-display/tree-view.mdx:46-47 (onSelectChange, onExpandChange), content/docs/components/form/input-otp.mdx:36 (onComplete). Candidate sub-issue of #6124 or a review-time rider on this PR, PM's call.", "NOT filed, observation for the PM: CommandSchema.onChange is retired here, but a host function placed on that key still lands on cmdk's root div through SchemaRenderer's prop spread and fires as a DOM change listener with a SyntheticEvent - a leak of the general spread hazard, not of this card; same shape (React unknown-event warnings, nothing attached) for Alert.onDismiss, Carousel.onSlideChange, TreeView.onSelectChange/onExpandChange, Sidebar.onCollapsedChange. If the spread of undeclared function values is ever gated, these are the measured cases.", "Re-confirmed, not re-filed: the 71 pre-existing corpus reds (0 for expected:function) and #6182's unanchored close-condition pattern are already on the PM's record from the 2026-08-30 report; nothing new measured here." ] }Generated by Claude Code
Generated by Claude Code
os-litant commentedon Sep 2, 2026
⚖️ Contract review PASS — PR #7339 (in-seat,
domain:uiexecution seat; served tier read byget_session:last_served_model = claude-fable-5-1, at or above theCONTRACT_REVIEW_TIERfloor)Reviewed under the maintainer's 2026-08-31 ruling that Clause-② cards are built at
fableand reviewed in-seat. Sessionsession_01NRRumy89BYdW9ogbcdHTho.Ruling conformance, verified by content on the fetched branch (head
11fd53500, one commit, both trailer lines, no model identifiers, 23 files, +1210/−132):on*: z.function(sites inpackages/types/src/zod/*.ts: 62 namedz.function(sites onorigin/main→ 4 at the head, and the four survivors are exactlycell,custom,renderCellEditor,validate(Q4 → B).handlerKeyRefusal(appears 58 times across the eight mirrors (59 with its definition). The helper (tombstone.zod.ts:108-122) isz.custom<never>(() => false, { error: guidance }).optional().describe(guidance)— always refuses (a live function included, per 「拒绝臂对函数值一并拒绝」), absent key accepted, message names the key, states the disposition (RUNTIME SLOT vs RETIRED) and points at the node-type spelling ({ "type": "toast" }/action:button) — Q1's option C. Same string feeds the parse issue and.describe(). 22?: nevertombstones added on the TS twins (diff count 22), each with the ADR-0049 deprecation JSDoc (feedback.ts:148-154sampled); runtime-slot keys keep their function type with a JSDoc naming the consumer (form.ts:57-61sampled).base.zod.ts:394unchanged; retire(types):EventHandlersSchema— 公开导出的z.record(z.string(), z.function()),每个值 JSON 不可作者、无任何组合消费、census 与 parity 台账双双结构性不可见(ADR-0049 enforce-or-remove) #6910's).safeParsepath for function values): measured, not fired.packages/react/src/SchemaRenderer.tsx:84validates through@object-ui/core'svalidateSchema(packages/core/src/validation/schema-validator.ts), andpackages/core/srcimports no zod — the structural validator never reads a handler key. The remainingsafeParsedoors are the CLI validators and the exportedvalidateSchema/safeValidateSchema, none fed a function-bearing object.Accept-set delta, stated where it must be. The changeset (
@object-ui/types: minor,majorrefused by this repo) says plainly: a live function value that parsed green on the zod mirror is now refused; the 22 tombstoned keys are listed by name astscerrors for anyone assigning them; JSON authors were already refused and now get a named message. That is the whole delta, and it matches the diff.Pins.
handler-keys-json-refusal-6124.test.ts(240 cases): anchored census (0on*sites; the four non-on*pinned by name; 58 ledgered = 36 + 22 with no key filed twice), per-site object refusal withcode: 'custom'and the guidance wording, live-function refusal, the passthrough counter-probe (deletion simulated → green with the object kept — the ruling's ⛔ 不裸删 held as evidence), arm ≠ tombstone, and type-levelRetiredIsNever× 22 /KeepsFunction× 36 compiled bytsconfig.test.json. Red-first quoted (178 failed | 62 passed, 22 × TS2344), ablation onButtonSchema.onClickred on exactly the Button rows with hash-verified restore.zod-mirror-parityledger re-stated (36 entries,52 keys,122pairs with no entry — read in the file header).check-governed-merges.mjs --teston the 23 paths → NOT governed.Accepted deviations. The
#5099precedent has no ledger row (both faces agree there), so runtime-slot keys are deliberateKnownDriftentries with a class note — correct. Three existing tests restated for the new message, verdicts unchanged. Downstream type-check narrowed to a read census plus@object-ui/coreagainst the rebuilt dist; the full consumer sweep is CI's, and a red there is this PR's to fix.Disposition (清标即落地):
needs:contract-reviewcleared on this card and on PR #7339 in the same step (read-modify-write, read back). The PR goes ready + auto-merge (SQUASH) onceType Check, the four test shards,Lint,Doc Snippet Type CheckandREADME Export Checkarecompleted: successon11fd53500. Follow-ups filed by the seat: a docs sub-issue for the three pages that teach retired keys (schema-reference.md:926-927,tree-view.mdx:46-47,input-otp.mdx:36), blocked on this card; the measured DOM-listener leak cases (CommandSchema.onChangeon cmdk's root and the retired keys that still rideSchemaRenderer's spread) recorded on #5632's burn-down. #6182 returns topm:queuewhen this card closes, with its close condition restated in the anchored form.Generated by Claude Code
os-litant commentedon Sep 2, 2026
Landed. PR #7339 merged through the queue at 09:04Z (queue build ref
pr-7339-e75f4c986…, confirmed positively before the merge; Clause-② contract review PASSed in-seat before the flip, carriers cleared on card and PR by read-modify-write with read-back).Verified by content on
origin/mainaftergit fetch(tip2956d7af8at verification time), not by PR head sha:packages/types/src/zod/tombstone.zod.tsdeclareshandlerKeyRefusal(1 hit);packages/types/src/zod/form.zod.tscalls it at 21 sites.^\s*on[A-Z][A-Za-z]*: z\.function\(overpackages/types/src/zod/*.tsreturns 0 sites.on*z.function()keys the ruling keeps (Q4 → B) are still exactlycell,custom,renderCellEditor,validate— one site each.packages/types/src/__tests__/handler-keys-json-refusal-6124.test.tsis onmain.Card closed by the PR's
Fixesline;pm:dispatchedstripped in the same round (labels nowdomain:ui,priority:p1). Downstream in this round: #6182 (blocked on this card) gets its close condition re-run on the merged ref; #7340 (docs half, blocked on this card) is re-checked on the merged ref and returned to the queue per itsUnlock-action:line.Generated by Claude Code
docs(components): stop teaching #6124's retired handler keys as autho…
docs(components): stop teaching objectstack-ai#6124's retired handler…
feat(types): the `kanban` arm declares the plugin dialect; the Declar…
fix(types,cli): arm the three plugin-ai node types in AnyComponentSch…
feat(types): refuse the function-slot mirror keys by name (objectui#7…
fix(types)!: the AI node declarations stop offering members no runtim…
docs: cite landing commits where pending changesets and source commen…
docs(citations): re-point the 30 dead objectstack#N citations and the…