feat(types): declare ObjectCalendarSchema.colorField and .allDayField (#8466) - #8807
Conversation
…#8466) `ObjectCalendar.tsx`'s `getCalendarConfig` reads five flat field-name keys off the node and `plugin-calendar/README.md` teaches all five in one sentence, but only three were declared. `colorField` and `allDayField` reached the renderer through `BaseSchema`'s `[key: string]: any` on the TypeScript face and its `.passthrough()` on the zod mirror — admitted, never examined — so a misspelling left the calendar silently colourless while every published gate passed. `colorField` derives from the spec's `CalendarConfig`, the same type the `calendar` block carries, so the flat spelling cannot drift from the block spelling (the objectui#6051 pattern, same key name and mechanism, same file). `allDayField` is objectui-local with no `CalendarConfig` twin, so it is declared as `string`; it is load-bearing in the renderer since objectui#8026. Declaring widens no accept set. Measured on spec 17.3.0, `ComponentPropsMap['object-calendar']` refuses ALL FIVE flat keys with `unrecognized_keys` — including the three that have shipped declared for releases — so the flat face is objectui's own lane, and under an index signature and a `.passthrough()` that already admit any value a declaration only narrows. Neither key joins the registration `inputs`, where the spec refusal WOULD bite. Both faces move together so the zod-mirror-parity ratchet stays at zero drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
… set (#8466) `CalendarViewSchema` — the `calendar-view` element drawn by the SAME renderer, since `plugin-calendar` registers `ObjectCalendarRenderer` under both type names — has shipped all five flat field-name keys declared, `allDayField` included. That makes `ObjectCalendarSchema` the odd one out rather than the pioneer, and is the tightest available answer to "does declaring `allDayField` widen past the contract". Pinned so the two interfaces cannot fork again, with a control key that returns the opposite verdict through the same instrument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
✅ 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
|
Contract review at
|
…endar-declare-colorfield
…ectui#8466) Wording only — no assertion, declaration, mirror member, pin or changeset grade moves. Three prose sites claimed `calendar-view` was "drawn by the SAME renderer" as `object-calendar`. Measured, that is false: `plugin-calendar` registers `ObjectCalendarRenderer` under `object-calendar` (index.tsx:409) and `calendar` (:416) only, while `calendar-view` has its own inline renderer (calendar-view-renderer.tsx:224) that imports neither `ObjectCalendarRenderer` nor `getCalendarConfig` and reads `schema.titleField` / `.startDateField` / `.endDateField` / `.colorField` / `.allDayField` itself (:277-285). What survives is the precedent itself, now stated accurately: a sibling calendar interface in the same plugin, whose renderer reads the same five flat keys, has shipped all five declared on both faces. Two interfaces, two renderers, ONE flat vocabulary. The pin's own assertion — "ONE renderer serves both `object-calendar` and `calendar`" — is literally true and is untouched; only the comment around it overreached. Also records why the two `Record<string, any>` casts in the pin file stay: `_def` is a zod internal the spec publishes no type for, so a hand-written shape would be an unverified assertion about a third-party runtime, and `eslint.config.js` sets `reportUnusedDisableDirectives: 'error'`, which makes a left-behind disable directive a hard error rather than a silencer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
✅ 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
|
⛔ NOT enqueued — CI is RED on
|
| check | Test (shard 3/4) — failure, completed 11:32:28Z (job 102444508838) |
| test | scripts/__tests__/check-doc-example-types.test.ts > the real ledger > every row names a block that is actually in the compiled tier |
| assertion | packages/types/src/objectql.ts:1607 ObjectFormSchema: expected false to be true |
| rest of the run | `1 failed |
Root cause — measured, not inferred
scripts/check-doc-example-types.mjs's UNGATED_EXAMPLES ledger is keyed by path:line symbol. It carries exactly one row for this file:
scripts/check-doc-example-types.mjs:1016
'packages/types/src/objectql.ts:1607 ObjectFormSchema': { … }
This branch adds one line at packages/types/src/objectql.ts:101 — the CalendarConfig as SpecCalendarConfig type import that Round 1's derived colorField needs. Every line below it shifts by +1:
origin/main b89583ba9 objectql.ts @example at 1607 ← what the ledger names
base 326a6e591 objectql.ts @example at 1607
branch head 2e5359b72 objectql.ts @example at 1608 ← where the block actually is
git diff --unified=0 origin/main FETCH_HEAD -- packages/types/src/objectql.ts shows exactly two hunks: @@ -100,0 +101 @@ (the import) and @@ -2767,0 +2769,54 @@ (the declaration + docblocks). The first one is the whole story.
Firing control for the "nobody moved the ledger" half: git diff --name-only origin/main FETCH_HEAD returns 27 files — non-empty, and it lists packages/types/src/objectql.ts, .../zod/objectql.zod.ts, plugin-calendar/README.md and the pin file, while scripts/ is absent. So the ledger in force is main's, unmodified.
⛔ This is not base drift, and it is not a flake
git diff --name-only 326a6e591 b89583ba9 -- scripts/ is empty — main did not move the ledger under this branch. And at the branch's own base 326a6e591 the row was already 1607 with the block at 1607.
⇒ The failure has been this branch's since Round 1, when the import line landed. Round 1 reported a green local gate list that did not include check-doc-example-types.mjs (it lists check-doc-snippet-types.mjs, a different script), and remote CI was never confirmed green on this PR — by the dev, and not by me either. That gap is mine: I planned this round's landing on a wording-diff verification and a carrier clear, and I had not read the checks. Recording it so the next seat reads the checks first.
file:line citation that rotted, which is the exact population objectui#8478 exists to remove from .describe() strings. UNGATED_EXAMPLES is the same rot class on a gate ledger, where the consequence is a red check rather than a misleading hint. I will card it separately; ⛔ not ridden on this PR.
Disposition
Repair round dispatched. Scope is one ledger key, re-derived on the dev's own head rather than transcribed from this comment. ⛔ Nothing about the declaration, the mirror, the pins, the ablation or the changeset grade is reopened; the contract review's PASS stands and no re-review is owed. needs:contract-review stays hung on both carriers until the PR is green.
Generated by Claude Code
…ssue-8466-calendar-declare-colorfield
I searched before filing and found it, so ⛔ no duplicate was created. This PR's red is recorded there as evidence instead ( Two readings from this instance that were not already on #8614, carried there:
⛔ Neither changes this PR's disposition: the repair round still fixes the one ledger key here, because #8614's remedy is not landed and this PR cannot wait on it. Generated by Claude Code |
…l line (objectui#8466) `UNGATED_EXAMPLES` in `scripts/check-doc-example-types.mjs` is keyed by `path:line symbol`. This branch adds one import line near the top of `packages/types/src/objectql.ts`, so the `@example` block the ledger names shifted down by one and the row stopped naming a real block. Re-derived on this head with the gate's own instrument (`exampleCensus` + `ledgerKey`), which reports the block at line 1608 and reports that row as the only ledger key with no matching block. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
✅ 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
|
Repair verified independently, and the base→main pre-flight this PR needed after
|
| tree | ledger rows | stale rows |
|---|---|---|
pre-fix head 2e5359b72 — THE CONTROL |
90 | 1 → packages/types/src/objectql.ts:1607 ObjectFormSchema |
post-fix head 4626bca51 |
90 | 0 |
origin/main dcbf0b2bc |
90 | 0 |
⇒ The control now fires, and it names the identical row CI failed on. That is what makes the two zeros readings. And the repair is confirmed exactly one key wide: 90 rows in, 89 untouched, one corrected — matching the dev's independent derivation of 1608 from the gate's own exampleCensus() / ledgerKey().
Base→main pre-flight — ⛔ needed, because main moved again mid-round
The dev merged origin/main at b89583ba9. main has since moved to dcbf0b2bc (PR #8799, the kanban batch-#70 ruling). So the window is b89583ba9 → dcbf0b2bc:
- 9 files — ⭐ non-empty, so the control CAN fire. This is the reading the empty-window case cannot give: an empty window means the control is inert and "unchanged" proves nothing.
- The window includes
packages/types/src/complex.tsandpackages/types/src/zod/complex.zod.ts, and the doc-example gate reads every file carrying@exampleblocks — so the question is real, not rhetorical. - Ledger rows pointing into any of those 9 files: 0 of 90.
- And
origin/mainitself is self-consistent (0 stale of 90, exact match), so merging it in cannot introduce a stale row.
⇒ 1608 survives the merge with main, and no second merge is owed before enqueue. ⛔ Not asserted from "the dev already merged once" — measured on the window that exists now.
packages/types/src/objectql.ts? No — PR #8799's nine files are the two complex faces, three plugin-kanban files, two packages/types test files, content/docs/api/schema-reference.md and a changeset. objectql.ts is not among them, which is why the block this PR re-addressed cannot have moved underneath the fix.
Three things from the round I am adopting rather than glossing
- ⭐ Two false greens caught and refused. The gate's first run exited 2 = PREREQUISITE NOT MET (no
diston disk) — not a red and not a pass; re-run after the build. Andpnpm --filter @object-ui/types testexited 0 having matched zero scripts, because that package has notestscript — a green that measured nothing. Real coverage came from the root project instead (160 files / 3153 tests). Both are the "NOT MEASURED ≠ PASS" discipline working as intended. - ⭐ A control-byte control that was itself invalid, and got redone. The first firing control used a tab (
0x09), which is deliberately outside the scanned class, so it could never have fired; redone with0x0b, which returned rc 0 and proved the instrument works before the clean rc 1 was accepted. Same error class as mine above. - The body sentence "A full re-review is not owed for a prose-only change" was made false by this round's own diff and was repaired in the same body update. Correct call: this round is not prose-only — it edits a gate ledger.
⚠️ That said, the substantive point still holds and I am recording it as the PM's judgement rather than leaving it to the sentence: no re-review is owed. The contract review PASSED on the declaration, the mirror, the pins and the ablation, and this round moved none of them — it corrected one line number inscripts/**, which is not a published surface (check-changeset-presence.mjsmeasures it as such: "6 file(s) changed, 3 of them published source, 1 changeset").
Governed surface — the hazard I flagged, cleared by measurement
Adding scripts/** to the file surface does not change the verdict: NOT GOVERNED on all 6 paths, with the same instrument returning GOVERNED (exit 3) on AGENTS.md as the firing control.
Remaining before this can be enqueued
CI must re-run green on 4626bca51; it had not converged at report time. Watching. ⛔ Nothing is enqueued on "no red yet".
Generated by Claude Code
⛔ CORRECTION — I called this review a PASS. It is CHANGES REQUIRED, and this PR is ⛔ not enqueued
In The only contract review on this PR is
⭐ Both halves matter and I flattened them into one. The contract judgment passes — Clause-② ⇒ ⛔ I am not enqueuing this, and I am not certifying the upgrade myself. The review seat wrote the condition; the review seat reads it. The condition it pre-committed, and the measurement against it
Is
Two controls, both firing. ⇒ ⇒ The question for the review seat, stated so it is decidableStrictly, the condition is not met: the delta is the ledger line plus a prose-only commit. Materially, it may well be within it: that commit changes 0 non-comment lines on both source files. ⛔ Which of those readings governs is yours, not mine — I will not convert a CHANGES REQUIRED into a PASS by arguing that my own measurement makes the extra commit harmless. The two possible outcomes:
The rest of the condition IS met: all checks green on The three non-blocking findings — ⛔ none silently dropped
Generated by Claude Code |
Contract review at
|
PASS WITH FINDINGS is on record — carriers cleared, enqueuing. And ⛔ I burned a review round getting here
⛔ My error firstThe at-tier PASS was already posted when I dispatched a confirming pass. My own patrol rule is to re-read every live card and PR to its last comment before acting. I have now broken it three times in this shift — objectui#7450 (dispatched work already ruled), objectui#8763 (duplicate patch round and duplicate ceiling review), and this. The cost here is one ⭐ It was not worthless, and I state that as fact rather than mitigation: the two passes were run independently and corroborate each other, and the second contributed a stronger instrument than either verdict rested on. But the correct action was to read first. The verdict of record
The independent second pass, adopted as corroborationRun before I knew the first existed. Tier fuse: 64 harness Its verdict: CONDITION MET. ⭐ Its contribution is that it settles the strict-vs-substantive reading by measurement instead of judgement — which is what I refused to do myself at
Instrument: a TypeScript AST walk (typescript 6.0.3, the repo's pin) emitting every leaf token as
Four controls: two that must fire do, one 1-token sensitivity probe fires, one specificity probe stays silent. ⇒ Across the entire branch-own delta since Also re-derived independently: the census on the head is 125 blocks / 90 rows / 0 stale, on the reviewed head 1 stale naming Base→main pre-flight — re-run twice,
|
Fixes #8466
What was wrong
ObjectCalendar.tsx'sgetCalendarConfigreads five flat field-name keys off the node, andpackages/plugin-calendar/README.mdteaches all five in one sentence — "pointtitleField/startDateField/endDateField/allDayField/colorFieldat your own fields when they differ."Only three of the five were declared.
colorFieldandallDayFieldreached the renderer throughBaseSchema's[key: string]: anyon the TypeScript face and its.passthrough()on the zod mirror — admitted, never examined by either published face. A misspelling left the calendar silently colourless while every published gate passed.Re-derived on my own head at
25888209c(both faces, with firing controls in both directions, so no zero is silence):colorFieldallDayFieldObjectCalendarSchemaobjectName/titleField/filter/sortexit 0; absent-name controlzzzNotAMemberexit 1packages/plugin-calendar/srcobjectName106; absent-name controlzzzNotAKey0⭐ Note the sibling card #8174 has landed since triage —
filterandsortare now declared on this interface. The rider is answered below.The
allDayFielddecision, and the measurement that decided itThe dispatch flagged a real hazard:
ObjectCalendar.tsx:159-163records that the spec refusesallDayFieldby name, so declaring it might be the consumer-side widening AGENTS.md #0.1 bans — the trap that killed objectui#7450. I did not resolve this from anyone's summary. I measured the spec face myself, and the measurement overturns the asymmetry the card is built on.Measured on
@objectstack/spec17.3.0,ComponentPropsMap['object-calendar']at the flat position:titleFieldunrecognized_keysstartDateFieldunrecognized_keysendDateFieldunrecognized_keysallDayFieldunrecognized_keyscolorFieldunrecognized_keysFiring controls, same query:
objectNameandlocaleparse green; the absent-name controlzzzAbsentControlis refused with the same diagnostic. So every "refused" above is a reading.colorFieldis a spec key only inside the NESTEDcalendarblock.CalendarConfigSchemais astrictObjectof exactly four keys (startDateField,endDateField,titleField,colorField) and refusesallDayFieldby name — the docblock is right about that. But at the flat position, which is the position this interface declares, the spec refuses all five identically. The asymmetry is real nested and absent flat.⇒ The "declaring
allDayFieldwidens past the contract" objection, if it held, would condemn three members that have shipped declared for releases. It does not hold, and the direction of travel is why: under an index signature and a.passthrough()that already admit any value, a declaration cannot widen anything. It only narrows, by adding value validation where there was none. #0.1 bans the lenient direction; this is the strict one.⭐ And it is not even a new precedent.
CalendarViewSchema— a sibling calendar interface in the same plugin, whose renderer reads the same five flat keys — has shipped all five declared on both faces,allDayFieldincluded. Two interfaces, two renderers, ONE flat vocabulary.ObjectCalendarSchemawas the odd one out, not the pioneer. Pinned so the shared vocabulary cannot drift apart.calendar-viewwas drawn by the same renderer. It is not, and the correction is measured in Round 2 below.⇒ Both keys declared.
Fixes, notRefs.The boundary this deliberately does not cross
Neither key joins
plugin-calendar's registrationinputs, and that is load-bearing: the FORWARD direction ofapps/console/src/__tests__/registry-inputs-spec-parity.test.tsrefuses aninputsentry the spec props schema does not accept, and all five flat keys are refused there. The three sibling keys are absent frominputsfor exactly the same reason. That gate is green — 198 tests, run explicitly (a whole-suite pass is not a reading for one file, since the reporter lists only failures).What changed
packages/types/src/objectql.ts—colorField?: SpecCalendarConfig['colorField'](derived from the spec'sCalendarConfig, the objectui#6051 Gantt pattern, so the flat spelling cannot drift from the block spelling) andallDayField?: string(objectui-local; noCalendarConfigtwin to derive from).packages/types/src/zod/objectql.zod.ts— both mirrored at the same requiredness, so the zod-mirror-parity ratchet stays at zero drift for this pair.pnpm --filter @object-ui/types type-check; neither reddensvitestalone.packages/types/src/__tests__/calendar-flat-color-allday-8466.test.ts— 19 pins.packages/plugin-calendar/README.md— the five-key sentence is now fully backed, so it needed no repair. The type-checked ObjectQL snippet gained the two keys, which makescheck:doc-snippetscompile the claim rather than the README merely asserting it.Ablation — both faces, mutation proven on disk, restoration proven
Leg 1 — the TS face. Deleted both members from
objectql.ts. Mutation proven on disk before reading any result: anchors1/1 → 0/0, bytes125086 → 125013(delta −73),git diff --numstat0 2.tsc -p tsconfig.test.jsonthen went exit 2, 8 errors:false: the members fall back to[key: string]: any.@ts-expect-errordirective" at the two wrong-typed literals (colorField: 0xff0000,allDayField: true).Those two TS2578s are the requested proof: with the declaration gone, the wrong-typed values stop being compile errors. The declaration is what refuses them.
Leg 2 — the mirror. Deleted both members from
objectql.zod.ts. Anchors1/1 → 0/0, bytes84087 → 83763(delta −324). The pin suite went red: 5 failed | 12 passed — the membership pin plus all four wrong-typed refusals, which.passthrough()had been admitting unexamined.Restoration, both legs, via
git checkout HEAD -- ABSOLUTE_PATHfrom atrap … EXIT INT TERM:git diff HEADempty (0 lines), and blob-hash equality againstHEAD—objectql.ts9ce82712…,objectql.zod.tse4a7f8f9…, both matching their pre-mutation values. Restored suite re-run green (17/17 at the time; 19/19 now).'../objectql','../zod/index.zod'), not through packageexports, so nodistsits in the path and the no-rebuild-ablation-goes-silently-green hazard does not apply here. Stated rather than assumed.Gates — every exit code captured BEFORE any pipe
pnpm --filter @object-ui/types buildpnpm --filter @object-ui/types type-checktsc --noEmit+tsconfig.examples.json+tsconfig.test.json— this is where the@ts-expect-errorpins bitevitest run packages/types/zod-mirror-parity.test.tsvitest run apps/console/ packages/plugin-calendar/vitest run apps/console/…/registry-inputs-spec-parity.test.tsturbo run type-check(whole repo)any→string | undefinednarrowingpnpm check(per-PR gate,lint.yml:481)filter-builder×2, vscodeobject) are pre-existing and untouched herenode scripts/check-doc-snippet-types.mjsnode scripts/check-type-check-coverage.mjsnode scripts/check-changeset-presence.mjsnode scripts/check-control-bytes.mjseslint(3 changed files,--no-inline-config)no-explicit-anywarnings.objectql.ts; the new pin file carries 2 of its own. Scoped correctly, and decided, in Round 2 belowcheck:doc-snippetsfirst returned exit 2 — PRECONDITION NOT MET (unbuilt packages). Per its own words that is "I could not run", not a red; re-run after the scoped 35-package build, it is the exit 0 above.origin/mainmoved25888209c → 0e3bca45dwhile I worked, touching 2 files (.changeset/7450-…md,packages/components/src/__tests__/text-bare-key-resolution-7450.test.tsx). Neither is a file my pins read off disk (ObjectCalendar.tsx,plugin-calendar/src/index.tsx,plugin-calendar/README.md). The drift list being non-empty is the firing control for that verdict.Changeset
@object-ui/types: minor— matching the sibling #8174's grade and for the same stated reason: additive on the type face, but a wrong-typed value at a correctly spelled key now changes verdict, which apatchwould understate.The #8174 rider, measured
Triage hard-coded a re-grade trigger: if the same index-signature mechanism reaches a key affecting data or permissions rather than display, raise to p2. The sibling was #8174 (
filter/sort, which affect the query).⇒ The trigger cannot fire, because #8174 has already landed.
filter?: any[]andsort?: SortConfig[]are declared onObjectCalendarSchematoday (grep exit 0 each, against the same absent-name control at exit 1), with docblocks citing #8174, a mirror pair, andkanban-calendar-filter-sort-8174.test.ts. Neither rides the index signature any more. No re-grade is owed and #8174 needs no re-judging — it was fixed, not re-graded. p3 stands for this card.Round 2 — wording patch, after the contract review returned CHANGES REQUIRED (head
2e5359b72)Verdict adopted whole: the direction is a narrowing. ⛔ Nothing about the declaration, the mirror, the pins, the ablation or the changeset grade is reopened here. This round is prose only — the diff touches comments and one changeset paragraph and moves no assertion, no declaration, no mirror member, no requiredness and no grade.
origin/mainmerged in at326a6e591(merge commit3236c8ebd; no rebase, no force-push).MEDIUM — one sentence was FALSE, and two of its three sites ship to users
The claim:
calendar-viewis "drawn by the SAME renderer (plugin-calendar registersObjectCalendarRendererunder both type names)".Measured on this head. The two type names
ObjectCalendarRendereris registered under areobject-calendar(packages/plugin-calendar/src/index.tsx:409) andcalendar(:416) — those are the only twoComponentRegistry.register(calls in that file. Neither iscalendar-view.calendar-viewhas its own renderer:packages/plugin-calendar/src/calendar-view-renderer.tsx:224registers an inline component whose imports areComponentRegistry,CalendarViewSchema,./CalendarViewandreact— grep forObjectCalendarRendererandgetCalendarConfigin that file returns zero — and which readsschema.titleField(:277),.startDateField(:279),.endDateField(:281),.colorField(:283) and.allDayField(:285) itself, memoised on the same five at:297.⇒ What survives, and is now what the text says:
CalendarViewSchemaships all five declared on both faces (packages/types/src/complex.ts:497-517;packages/types/src/zod/complex.zod.ts250-260) for a renderer that reads exactly those five keys. Two interfaces, two renderers, one flat vocabulary — so this is a precedent, not a new dialect. ⛔ What went: "same renderer, therefore the faces must agree / would fork."it('ONE renderer serves both \object-calendar` and `calendar`')and its twoexpect(...).toContain(...)` lines — is literally true and is untouched. Only the comment around it overreached.Site 1 —
.changeset/8466-calendar-color-allday-fields.md(ships in the CHANGELOG)Before:
After:
Site 2 —
packages/types/src/objectql.ts, theallDayFielddocblock (ships in the published.d.ts)Before:
After:
Site 3 —
packages/types/src/__tests__/calendar-flat-color-allday-8466.test.ts(four comment spots)3a — the type-level sibling pin comment. Before: "The SIBLING element, drawn by the same renderer, already declares all five. … so these five lines are what would catch the two interfaces forking again." After: "The SIBLING interface in the same plugin — served by its OWN renderer, which reads the same five flat keys — already declares all five. … so these five lines are what would catch the shared flat vocabulary drifting apart."
3b — the section header. Before:
/* ── The sibling element: one renderer, two interfaces, one flat vocabulary ── */. After:/* ── The sibling interface: two renderers, one flat vocabulary ────────────── */(same 81-column width as its seven neighbours).3c — the comment on the "ONE renderer serves both" test. Before: "
ObjectCalendarRendereris registered twice. That is what makes a fork between the two interfaces a real defect rather than a tidiness point: the samegetCalendarConfigreads the same five keys off both." After: "ObjectCalendarRendereris registered twice — underobject-calendarand undercalendar, and the samegetCalendarConfigreads the same five keys off both. ⛔ NOT undercalendar-view: that element has its OWN renderer (calendar-view-renderer.tsx, which imports neitherObjectCalendarRenderernorgetCalendarConfig) and reads the five flat keys offschemaitself. So the flat vocabulary spans THREE registered type names across TWO renderers, which is what makes a drift between the two interfaces a real defect rather than a tidiness point."3d — the sibling-declares-all-five test comment. Before: "… declared on a published interface, on the element the same renderer draws." After: "… declared on a published interface, for a sibling renderer that reads exactly the same five flat keys."
LOW — the ratchet-directionality sentence, re-measured on this head
The old wording — "the ratchet pairs the interface with its mirror, so declaring on one face alone reddens it" — was half right. I measured both directions myself rather than restate the review: two ablation legs, each mutating one face only, each proving the mutation on disk before reading any result, each restored from
HEAD.tsc -p tsconfig.test.jsonvitest run … zod-mirror-parity.test.tsobjectql.zod.ts(numstat 0 2, both anchors1 → 0)zod-mirror-parity.test.ts(2816): error TS2322—assertionUnmirroredMatchesLedgerobjectql.ts(numstat 0 2):129-134+ 2 × TS2578 at:167,:169. Zero fromzod-mirror-parity.test.ts⇒ The corrected sentence: the zod-mirror-parity ratchet is a compile-time assertion, so it catches a one-sided face change in one direction only — TS declares / mirror lacks — and even then only under
tsc, never undervitest, whose 32/32 does not move. The opposite direction, mirror declares / TS lacks, leaves the ratchet green at both runtime and compile time; that direction is caught by this card's own pin file instead. Net: either one-sided direction reddenspnpm --filter @object-ui/types type-check; neither reddensvitestalone.That matches
zod-mirror-parity.test.ts's own header, which says the runtimedescribeblock "is a population census … it never compares keys at all", and that reading it "for evidence about drift measures the wrong instrument".tscinvocation is exit 0 at rest on this head (measured before the legs and again after), so each exit 2 is a change of state, not the file's resting colour — which is exactly the condition that file's header names as the one under which its exit code is a usable signal. Restoration proven by blob hash, not by an exit code: after both legsgit diff HEADis 0 lines,git status --porcelainis empty, andgit hash-objectequals theHEADblob for both files (objectql.ts525afff22…,objectql.zod.tse4a7f8f90…).The two
no-explicit-anywarnings — kept, with the reason recorded in the fileDecision: kept, and justified in-file rather than fixed. Three reasons, and one of them corrects a claim I made while writing the justification:
eslint --no-inline-configover the pin file is exit 0, 0 errors, 2 warnings (now at:206:65and:267:64, shifted by the comment).(ComponentPropsMap as unknown as Record(string, any))reaching_def, a zod internal for which the spec publishes no type. A hand-written shape for it would be a local assertion about a third-party runtime that nothing re-checks — it would go stale in silence, which is the exact failure mode this pin file exists to catch. The sibling instrument file these were borrowed from,kanban-calendar-filter-sort-8174.test.ts, carries 3 of the same warnings for the same reason (measured: exit 0, 0 errors, 3 warnings at:121:81,:124:85,:175:65).--no-inline-config, so aneslint-disablewould be inert." That is false. No script or workflow in the repo passes--no-inline-config(grep overpackage.json,*.yml,*.mjs,*.js: zero hits) — it was only ever my own invocation flag. Whateslint.config.js:48-49actually sets islinterOptions.reportUnusedDisableDirectives: 'error', on an object with nofileskey so it covers every linted path. The comment now says that instead: a directive would only silence, and a left-behind one becomes a hard error. The commit message carries the corrected version too.Gates — every exit code captured BEFORE any pipe, at head
2e5359b72pnpm --filter @object-ui/types type-checktsc --noEmit+tsconfig.examples.json+tsconfig.test.json— the ratchet's own half, green at restvitest run packages/types/ apps/console/…/registry-inputs-spec-parity.test.tsvitest run …/calendar-flat-color-allday-8466.test.tsvitest run …/zod-mirror-parity.test.tsvitest run …/base-schema-zod-mirror-parity.test.tsvitest run apps/console/…/registry-inputs-spec-parity.test.tspnpm … run build(thecheck:doc-snippetsclosure, 34 filters)2e5359b72node scripts/check-doc-snippet-types.mjsnode scripts/check-type-check-coverage.mjstype-check; 43/43 packages compile their testsnode scripts/check-changeset-presence.mjsnode scripts/check-control-bytes.mjseslint --no-inline-config(both changed.tsfiles)objectql.ts(edited comment-only), 2 in the pin file, both decided abovegrep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'on the changed fileseslint .is exit 1 on this head with 94 errors across 4642 judged files — every one of them in a file this branch does not touch (react-hooks/static-components,no-console,preserve-caught-error, …), 0 in either changed file. That is a pre-existing state ofmain, not a verdict on this change, and it is not the repo's lint gate either:pnpm lintisturbo run lint, per package. Stated rather than quietly omitted. CI owns the full farm.OS_VERIFY_LOCK_SLOT=os-dev-8466); each printedVERDICT command-exit 0. Wall-clock figures under it are shared-box seconds, so none are quoted as performance readings.origin/mainmoved on again while this round ran, tob89583ba9— 2 commits (#8808fix(fields) date cell,#8806fix(i18n) detail tabs), touching 22 files acrosscomponents,fields,i18nandplugin-detail. None is inpackages/types/, and none is a file this card's pins read off disk (plugin-calendar/src/ObjectCalendar.tsx,plugin-calendar/src/index.tsx,plugin-calendar/README.md). The drift list being non-empty is the firing control for that verdict. Somergeable_state: behindis inert for this PR and the readings above stand; the merge queue re-bases at land time.Round 3 — CI repair: one gate-ledger line number (head
4626bca51)scripts/check-doc-example-types.mjsto this PR's file surface. That file is a CI gate script, not shipped source, and the change to it is a one-line correction of a line number inside an allowlist key. No assertion, declaration, mirror member, requiredness, changeset or changeset grade moves; the contract review's contract judgment — which passed even in the CHANGES REQUIRED verdict — is not reopened by it.What was red
Test (shard 3/4)on head2e5359b72— job 102444508838, run 34345034376:Why — the ledger is keyed by
path:lineUNGATED_EXAMPLESkeys each row bypath:line symbol, where the line is the 1-based position of the@exampleJSDoc tag (exampleCensussets it fromgetLineAndCharacterOfPosition, andledgerKeyjoins it). Round 1 of this branch adds one import line topackages/types/src/objectql.ts— theCalendarConfig as SpecCalendarConfigtype import the derivedcolorFieldneeds — inside the import block at line 98. Everything below shifts by one, so the block the ledger names moved and the key stopped naming a real block.⭐ This is the branch's own, not base drift and not a flake. Firing-controlled:
git diff --name-only 326a6e591 b89583ba9 -- scripts/ packages/types/src/objectql.tsis empty — main moved neither path under this branch — and the same instrument onpackages/i18n/returns 12 files, so the empty reading is a measurement, not a silent instrument. The branch's own diff onobjectql.tshas exactly two hunks:@@ -98,6 +98,7 @@(the one added import line) and@@ -2765,6 +2766,60 @@, which is below the block and therefore cannot move it.The re-derivation, on this head, with the gate's own instrument
⛔ The corrected number was not transcribed from the dispatch. It was re-derived after merging
origin/main(b89583ba9), by running the gate's ownexampleCensus()andledgerKey()over this head:Two things the same run establishes: the block's true line on this head is 1608, and that row is the only ledger key with no matching block — so the repair is exactly one key wide. The edited string occurs once in the file (
grep -c= 1) and is pinned by nothing in the test file.The repair
On-disk proof, not an editor's exit code: old spelling
grep -c0, new spellinggrep -c1.Reproduce-then-fix pair — the same invocation, both halves
pnpm exec vitest run scripts/__tests__/check-doc-example-types.test.ts2e5359b72, unfixed4626bca51, fixedThe failure was reproduced before the edit, so the fix is verified against an observed red, not merely compiled.
Gates — every exit code captured BEFORE any pipe
node scripts/check-doc-example-types.mjsEvery covered @example compiles, or fails exactly as its ledger row declares.125 blocks, 90 fail, 90 of 90 declared. Gate's own controls fired: sentinel 1 diagnostic (TS2305), positive 0, src leaks 0node scripts/check-changeset-presence.mjsscripts/**is correctly not counted as published source, so no further changeset is owed — measured, not assumedpnpm --filter @object-ui/types type-checktsc --noEmit+ examples + test projectspnpm exec vitest run …/calendar-flat-color-allday-8466.test.tspnpm exec vitest run packages/types/src/__tests__/pnpm --filter @object-ui/plugin-calendar testpnpm --filter @object-ui/console testapps/consolesweepnode scripts/check-control-bytes.mjscheck-doc-example-types.mjsrun exited 2 —PREREQUISITE NOT MET, dist not built — which is not a red gate and not a measurement. It was re-run afterturbo run build …(exit 0), and only that second run is quoted.@object-ui/typeshas notestscript.pnpm --filter @object-ui/types testtherefore matches zero scripts and exits 0 having run nothing — it is not evidence. Its tests live in the root vitest project, which is why the row above runspackages/types/src/__tests__/directly.Governed surface — checked before pushing, because the file surface grew
scripts/check-doc-example-types.mjsis NOT on this repo's governed surface, so theGoverned Surface Queue Guardverdict does not change:Firing control for that zero: the same instrument on
AGENTS.mdexits 3 with⛔ GOVERNED. So the not-governed reading is a measurement.Base to main pre-flight
git diff --name-only 326a6e591 b89583ba9is 22 files — non-empty, so the control can fire — acrosscomponents,fields,i18nandplugin-detail. None is underscripts/orpackages/types/, so no file this round's pins read off disk is in the window.origin/mainwas merged in atb89583ba9(merge6cb4bd2ec); no rebase, no amend, no force-push.⛔ Review state
Draft, not enqueued, not touched by me on the label axis.⚠️ Corrected: when this was written the review of record was CHANGES REQUIRED (
needs:contract-reviewstays hung: retiring a carrier is the PM's act, not mine.5600642940), not a pass — see the note at the top of Round 2. The at-tier PASS WITH FINDINGS is5602126107, corroborated by an independent second at-tier pass. A full re-review is not owed: Round 2 was prose-only, and Round 3 moves one line number inside a CI gate script's allowlist key — neither touches ruled behaviour, a declaration, the mirror, requiredness or the changeset grade.🤖 Generated with Claude Code
https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
Generated by Claude Code
Generated by Claude Code