Repository navigation
retire(types): remove the EventHandlersSchema published export (ADR-0049 enforce-or-remove) - #9552
Conversation
…erately untouched ⛔ NOT a finished change. The public export `EventHandlersSchema` (`z.record(z.string(), z.function())`) and its barrel line are removed and the knowledge is preserved as a NOTE at the declaration site, per the maintainer ruling on objectui#6124 (decision batch #8, reconfirmed batch #25) recorded on objectui#6910. The third half of that ruling — deleting the parity-ledger row `'base.zod.ts#EventHandlersSchema'` in `packages/types/src/__tests__/zod-mirror-parity.test.ts` — is NOT in this commit. That file is held by the open PR #8763, and this seat's dispatch forbids editing it. So this tree is RED BY CONSTRUCTION: `zod mirror parity — the population is closed > no map entry names a const that no longer exists` fails with `['base.zod.ts#EventHandlersSchema']`. Measured, not predicted. No PR is opened from this commit. It exists so the NOTE and the measurements survive the hand-off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012W3vMLTFY9SPr2LyxhSeYi
…ire-event-handlers-schema
…e halves Completes the retirement the WIP commit on this branch left deliberately incomplete. The public export and its barrel line were already gone; this commit removes the parity-ledger row the removal reddens, repairs the one docblock the removal falsifies, and declares the change. - zod-mirror-parity.test.ts: delete the self-contained two-line EXCLUSIONS entry for the removed const. Its stale-entry census reads exported consts off disk, so the row went red the moment the export left; the sibling HTMLAttributesSchema entry shares the same reason string and is untouched. - handler-keys-json-refusal-6124.test.ts: the docblock over MIRROR_FILES said base.zod.ts "holds only EventHandlersSchema ... and no named key", which the removal makes false. Rewritten to state why base.zod.ts is not one of the nine mirror files; the block is kept, since it is what explains the census population. - base.zod.ts: correct the NOTE. It claimed handlers are authored as BaseSchema.events, an event-name string keying ActionSchema[]. Checked against the tree rather than inherited: BaseSchemaCore declares no events member, nothing reads schema.events, and every authored events in the corpus is TimelineSchema.events, an unrelated array. The NOTE now points at the real JSON face -- handlerKeyRefusal() and the node-type spelling -- and records the stale sketch as a trap rather than repeating it. - One changeset, scored minor by the presence gate's own verdict, with no BREAKING banner: the four-repo consumer census is zero. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…ire-event-handlers-schema
Contract review F1 (MAJOR). The NOTE's warning paragraph asserted that "every authored events in the corpus is TimelineSchema.events, an unrelated array". That was wrong in both directions on the day it was written, and the whole comment block is emitted into the published .d.ts, so it shipped. The instrument behind the claim was the defect: it matched only the JSON-quoted "events": spelling, which cannot see a TypeScript object literal. Re-measured here over the tracked files under packages/, apps/ and examples/ with a control that fires, the object-literal form finds the node-level record shape the paragraph warns about, plus hook and validation string arrays; and of the files declaring a timeline node, the only ones carrying an events key are the declaration itself and a refusal probe. TimelineSchema.events is authored zero times. No replacement count is written. The paragraph now states the mechanism, which does not rot -- nothing declares the key and nothing reads it, so whatever a document writes there, no renderer runs it -- and points at the card that carries the census. A census answer frozen into a comment is what AGENTS.md commandment #9 forbids, and this note had reproduced that defect inside the very fix performed to record it; the paragraph now says so. The operative instruction is unchanged: BaseSchemaCore is .passthrough(), such a node is kept, judged by nothing and run by nothing, do not send an author there. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
|
| field | value |
|---|---|
| verdict | PASS — comment 5674344635, 2026-09-15T03:35:56Z |
| declared head | 53df540f0847ca38db6b27586bda9238644840bc |
pulls/9552.head.sha now |
53df540f0847ca38db6b27586bda9238644840bc |
| comparison | full 40-character string equality, both operands length 40 — ⛔ not a prefix and not a short sha |
| authorship pair | Implemented-by: claude/issue-6910-retire-event-handlers-schema · Reviewed-by: session_01L5xpA5q533BgTTNADibEFt, each token immediately after its colon |
The round-1 verdict returned FAIL on F1 (MAJOR) — a born-false census sentence in the rewritten NOTE, produced by a JSON-quoted matcher that structurally cannot see a TypeScript object literal. The repair removed the count rather than replacing it, because the defect was the shape of the claim, not its value. The targeted re-check confirmed the repaired paragraph carries no census, that the reviewed head ee48fcee7b is still an ancestor of 53df540f08 (nothing rebased, amended or force-pushed), and that F1's text is gone from source and from the rebuilt dist.
The red that was not this PR's, and its resolution
Test (shard 1/4) failed on a repo-root scratch-directory race, which is the already-filed objectui#9468 and was recorded on that card as its third instance — falsifying that card's own "did not recur". I spent the one re-run this lane allows, said so before spending it, and committed to reporting a second failure as a second reading of the same race rather than re-running further. It came back completed: success at 2026-09-15T04:02:09Z, so that commitment did not have to be exercised.
Gates at this head
- ① declared head ==
pulls/9552.head.sha, compared as full strings. MET. - ③
total_count36 == 36 returned;{success: 33, skipped: 3}; 0 not-green, 0 still running; legacy combined statussuccess(1 context);mergeable_state: clean. MET.⚠️ This is the draft check set — a check that only starts on undraft would be a 37th, so ③ is re-taken after the flip and before the queue, not carried forward from here. - ② is taken after this removal, deliberately. See below.
⭐ Why gate ② is read after the carrier comes off, and not before
On PR #9548 earlier this shift I removed this same carrier while the verdict still carried a half-written authorship pair, opening a five-minute carrier-free window over a verdict the gate would have rejected. The root cause is not carelessness about ordering — it is that the ordering the obvious reading suggests cannot work: while needs:contract-review hangs, check-clause2-carriers cannot read the PR thread, so every row that reads the verdict's text is silent, and a blind run and a genuine pass emit the same bytes.
One line, so it survives out of context: a checker whose inputs are gated by the carrier it is deciding to remove cannot produce the reading that authorises the removal.
So the sequence is: this record → the targeted DELETE on PR #9552 and on card objectui#6910, each read back as a JSON set difference → then --pair 9552 as the landing reading of ②. If ② comes back non-zero after the removal, the carrier goes back on in the same shape and this PR does not move.
Generated by Claude Code
Enqueued — three of three at
|
| # | gate | reading | at |
|---|---|---|---|
| ① | declared head == pulls/9552.head.sha |
53df540f0847ca38db6b27586bda9238644840bc both sides, full 40-character string equality |
04:02:35Z |
| ② | PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9552 |
exit 0 — declaration readable in the fixed spelling, both carriers agree, and C6-RECORD names comment 5674344635 as the review of record on this head |
04:03:33Z, after the carrier came off |
| ③ | check census, pre-flip | 36/36, {success: 33, skipped: 3}, 0 not-green, 0 running, legacy combined success, mergeable_state: clean |
04:02:35Z |
| ③ | check census, re-taken post-flip | 37/37, {success: 34, skipped: 3}, 0 not-green, 0 running, legacy combined success, mergeable_state: clean, head unmoved |
04:04:27Z |
⭐ The 37th check is the reason ③ is taken twice. Governed Surface Queue Guard ran once on the draft (03:22:57 → 03:23:11Z) and again on the undraft (04:03:54 → 04:04:05Z), and only the second reading is about the PR that is being enqueued.
Sequence, with its timestamps, since the ordering is the point: discharge record 5674547457 → targeted DELETE of needs:contract-review on this PR and on card objectui#6910, each read back as a JSON set difference ({needs:contract-review, package: types, tests} → {package: types, tests}; {domain:spec, needs:contract-review, pm:dispatched, priority:p3} → {domain:spec, pm:dispatched, priority:p3}; nothing unexpectedly added or removed on either) → gate ② → ready_for_review 04:03:49Z → guard → gate ③ again → auto_merge {"merge_method":"SQUASH"} 04:04:39Z.
added_to_merge_queue is on the timeline at 04:04:40Z. auto_merge reads back null once a PR is queued, so the timeline event is the evidence and the field is not. Queue transit has been 18.5–20.5 min over ten readings this shift, the most recent being PR #9551 at 20 min 09 s — so a merge is expected around 04:23–04:25Z, and that is a range this seat keeps re-measuring rather than a promise.
Generated by Claude Code
Fixes #6910
Retires the published export
EventHandlersSchema(z.record(z.string(), z.function())) from@object-ui/typesunder ADR-0049 enforce-or-remove, per the maintainer ruling recorded on the card (decision batch #25, reconfirming batch #8 on objectui#6124).This is a resume. The branch already carried a WIP commit that deleted the export and the barrel line and left a NOTE in place; that commit was deliberately incomplete and red by construction. This PR merges
origin/maininto the branch (merge, never rebase), then finishes the remaining halves.What landed
zod-mirror-parity.test.tsEXCLUSIONSentry for the removed const, deletedhandler-keys-json-refusal-6124.test.tsMIRROR_FILESdocblock sentence the removal falsifies, rewritten.changeset/6910-retire-event-handlers-schema.mdThe expected red, and its resolution
Removing the export turns the stale-entry census in
zod-mirror-parity.test.tsred with exactly['base.zod.ts#EventHandlersSchema']— that census reads exported consts off disk, so it fires for any route that removes the export. Edit 3 is what clears it. The whole package runs 190 files / 4515 tests, all passing, withtype-checkgreen across all threetscpasses.The deleted row's reason string is shared verbatim with the sibling
HTMLAttributesSchemaentry, so the deletion was anchored on the key line, not on the reason text; the sibling entry is untouched and still present.Round 2 — contract review F1 (MAJOR), repaired
The review reproduced 8 of 8 claims and added two checks of its own that strengthen this PR: a set difference over all 489
EXCLUSIONSkeys (exactly one key removed, the sibling surviving at the identical address on both refs), and a red-first proof that re-adding the retired row turns the census red naming only that key — so the delete was mandatory, not a test weakened to go green.It also found that my rewritten NOTE had shipped a born-false census sentence: "every authored
eventsin the corpus isTimelineSchema.events, an unrelated array."The instrument behind that claim was itself the defect. I had matched only the JSON-quoted
"events":spelling, which structurally cannot see a TypeScript object literal, and then generalised the result to "the corpus". Re-measured on this branch over the tracked files underpackages/,apps/andexamples/, with a live control that fires and a clean absent-token control at zero, the object-literal form finds the node-level record shape the paragraph warns about — on nodes that are not timelines — plus a larger set of hook and validation string arrays. Of the files declaring atype: 'timeline'node, the only ones carrying aneventskey are the declaration itself and a refusal probe.TimelineSchema.eventsis authored zero times. The sentence was wrong in both directions.This was the defect the rewrite existed to remove, recurring inside its own fix, and it reached the emitted
.d.ts. No replacement count is written. The paragraph now states the mechanism, which does not rot — nothing declares the key, nothing reads it, so whatever a document writes there no renderer runs it — points at objectui#9553 for the census, and records that it shipped a frozen census answer once already. The operative instruction is unchanged:.passthrough()⇒ kept, judged by nothing, run by nothing, do not send an author there.Deviation from the WIP commit, stated rather than folded in
The WIP NOTE told the reader that handlers are authored as
BaseSchema.events. That is false on this tree, and it is the one thing in the inherited work that was not adopted as-is:BaseSchemaCorehas 21 members and declares noevents; the TypeScriptBaseSchemainterface declares none either;schema.eventsandprops.eventshave 0 read sites acrosspackages,appsandexamples, against live controlsschema.classNameandschema.visiblefiring in the same pass; and@objectstack/spec17.4.0 declares no UI-nodeeventskeyed to actions.Shipping that sentence would have replaced one unauthorable surface with a pointer to a non-existent one, which is the exact hazard this card exists to remove. The NOTE now points at the real JSON face —
handlerKeyRefusal()and the node-type spelling PR #6498 established.Consumer verification
origin/main, zero composition consumersBaseSchema→ 429 files, fires; absent-token → 0BaseSchemafires in the same corpus@object-ui/*, 16 files@object-ui/*, 3 filesThe cloud and hotcrm legs are carried forward from the published reading on objectstack#15886 and are not presented as readings this seat took; this container holds no access to either repo.
Reverse verification against the rebuilt
distrather than a cache: the built zod barrel exposes 211 named exports, the removed symbol is absent from it, and three sibling controls (HTMLAttributesSchema,BaseSchema,ClassNameStylePropsSchema) all resolve.Acceptance notes
The changeset's level came from the gate's own verdict, not from a sibling card.
check-changeset-presencescored this change@object-ui/types, "Scoredminorat most, nevermajor", so the declaration isminor.On the BREAKING banner (review F2), answered explicitly. The reviewer is right that the distinction I originally drew from objectui#9040 — "that one had a live consumer face, this has none" — is not supported: 9040 also recorded a zero in-repo census and took the banner anyway, and both removals produce an external compile error. I withdraw that reasoning. The banner is nonetheless omitted, for a different and narrower reason: this card's ruling names the banner as a conditional and the condition was not met. The ruling states that a live import anywhere means "the ruling stays retire but the changeset becomes BREAKING and the removal needs ADR-0087's conversion path rather than a plain delete". The four-repo census came back zero, so the plain-delete branch is the one the ruling itself selects. That is a ruled branch, not a house-style judgement, and it is not mine to override; 9040 is a different card under its own ruling. The body still tells a consumer plainly that an external import breaks and gives the FROM/TO, which is what a CHANGELOG reader needs either way.
The fifth occurrence is left alone, and three instruments agree.
.changeset/6124-handler-keys-json-refusal.mdsays "EventHandlersSchemais objectui#6910's card". That is a scope statement about what objectui#6124 did not cover, it stays true after the removal,check-changeset-overwriteexists precisely to report editing a changeset you do not own, and AGENTS.md #11 forbids re-addressing a published record. Not touched.Dist residue (review F3), corrected count. The removed symbol survives in the built output 3 times: 1 in
dist/zod/base.zod.d.tsand 2 in.jsfiles (zod/base.zod.js,zod/index.zod.js) — all of them the NOTE's own prose carried through as a comment. No declaration and no export: a grep for either indistreturns 0.Out-of-scope finding, filed rather than fixed here — AGENTS.md's abridged JSON-protocol sketch publishes an optional
eventsmember typed as a record of event-name string to ActionSchema array as aBaseSchemamember, and its action-system commandment authors one, while no schema declares the key and nothing reads it. BecauseBaseSchemaCoreis.passthrough(), a node written from that sketch is kept by the parser, judged by nothing and run by nothing — a silent discard, and the carrier is the file agents read first. Filed as objectui#9553, whose own census has been corrected for the same instrument error described above. AGENTS.md is a governed surface and is not touched by this PR.Gate readings. Green here: the changeset family (presence, no-major, overwrite, claims), control-bytes, new-line-citations, handler-key-reads, spec-symbols, test-path-roots, unreferenced-sources, the package's
type-check, and the package suite.check:readme-exportsis NOT MEASURED locally — it exits 1 on an unbuilt tree and prints "the population COLLAPSED -- this run proves nothing", a prerequisite failure and not a finding; it needs a full repo build and belongs to CI. It cannot be moved by this diff in any case: 0 READMEs name the removed symbol, against controlBaseSchemafiring in 13 of 54 tracked READMEs.check-governed-queue-guardis CI-only; its surface list was read directly instead, and none of these paths is governed.Clause-②: yes — a published export is removed. The
needs:contract-reviewcarrier is the PM's to hang; no label was touched by this seat, on the card or here.Implemented-by: claude/issue-6910-retire-event-handlers-schema
Generated by Claude Code