Repository navigation
fix(plugin-calendar,i18n): a record with no date leaves the grid for an "unscheduled" area - #7453
Conversation
…an "unscheduled" area `ObjectCalendar` mapped a record whose declared `startDateField` carried no value to `new Date()` — the current moment — so it rendered on today's cell as an ordinary event. The `isNaN` guard six lines below could not catch it by construction: a no-argument `new Date()` is always valid, so the absent-value case was converted into a well-formed lie before the check that would have caught it. `end` two lines away has always been honest about the same absence (`undefined`); this is `start` catching up. Ruled on objectui#7071 by the maintainer (2026-09-01, option 2, minimal form; re-confirmed 2026-09-02 after a second seat recommended option 1). The fabricating arm is deleted and such records are counted in a collapsed "Unscheduled (N)" area below the calendar: visible count, expandable list, no invented date, and — per the ruling — no drag-to-schedule and no scheduling UI. `allDay: !endDate` now applies only to records that HAVE a start, so one absent field no longer silently sets two rendered properties; a record with a start and no end still renders all-day. The `isNaN` filter keeps its original job for values that are present but unparseable, so absent and malformed stay two distinguishable outcomes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
✅ 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
|
ACCEPT — verified on the branch by content, ⛔ not against the report
|
| claim | site on the branch | verdict |
|---|---|---|
start stops fabricating |
:550-553 — if (!startDate) { unscheduled.push({ id, title }); return; } |
✅ the arm is gone, the record leaves the grid |
allDay: !endDate only for records that HAVE a start |
:575, and the early return is at :550 — 25 lines above it |
✅ mechanism verified by ordering, not asserted |
isNaN keeps its original job |
:560 if (isNaN(start.getTime())) return;, live code, after const start = new Date(startDate) at :555 |
✅ present-but-unparseable still dropped, never counted as unscheduled |
⭐ The second row is the one worth stating plainly, because it is the claim a reviewer is most likely to take on trust: allDay: !endDate is unchanged text, and it is correct now only because something 25 lines above it changed. One absent field used to set two rendered properties. The early return is what separates them, and it is genuinely upstream — I read the ordering rather than believing the PR body's account of it.
Machinery present and wired: UnscheduledRecord at :169, the single-pass useMemo at :492, the accumulator at :522, the paired return at :580, and the render at :867-886 reading unscheduledRecords.length. One pass producing both lists means the count under the calendar cannot disagree with the grid — that is a structural guarantee, not a tested coincidence.
i18n: unscheduled present in 10 locale packs, matching the ten the repo ships. Control: CalendarView fires 6 times in the same file on the same ref, so the zeros above are absences rather than a broken pattern.
ARM 4 is the arm I would have missed
The ruling named three pins. The dev added a fourth: a value present but unparseable must be absent from the grid, absent from the area, and not in the count. A fix that routed absent and malformed into one bucket passes 1–3 and fails only here. That distinction is now load-bearing in the code (:550 vs :560) and pinned — the right place for it.
The ablation's informative asymmetry corroborates the ordering I checked: under the mutation, ARM 2 fails on its unscheduled assertion, not on its all-day assertions. The all-day fact stays true. That is the evidence the change did not over-reach — allDay and "has a start" are separate facts and only the second one moved. A blanket red would not have shown that.
The SCOPE FENCE test is correct and I want it kept: the area is ruled inert — no drag-to-schedule, no date picker, no click-through. A later "helpful" addition now has to argue with a red test rather than slip in as an improvement.
Landing
- Clause ② no, confirmed against the diff: no spec key, no published type, no authored-metadata surface moves. This one is this seat's to land.
- All 30 checks read by name: no
failure, noin_progress. The threeskippedare the conditional coverage jobs anddependabot. ⛔ The bar was every check green, not the required subset. mergeable_state: clean.
⇒ Marking ready and enqueueing. ⛔ Not merging by hand and ⛔ not bypassing the queue.
Line drift, recorded
The card and the ruling cite :445 / :451; the live sites are :508 / :514 on the base and moved again with this diff. The dev re-derived rather than trusting them. ObjectCalendar.tsx is also touched by the unlanded #7391 (parked behind #7399) in disjoint regions — line drift, not a conflict.
Generated by Claude Code
Fixes #7071
A record whose declared
startDateFieldcarried no value was mapped tonew Date()— the current moment — and drawn on today's cell as an ordinaryevent, indistinguishable from a real one. The
isNaNguard six lines belowcould not catch it by construction: a no-argument
new Date()is alwaysvalid, so the absent-value case was converted into a well-formed lie before
the check that would have caught it.
endtwo lines away has always been honestabout the same absence (
undefined); this isstartcatching up.The ruling this implements
Maintainer, 2026-09-01 (director decision batch D, verbatim 「同意」): option 2,
minimal form — the fabricating arm is deleted and such records land in a
collapsed "unscheduled (N)" area. Re-confirmed 2026-09-02 (director seat,
summon #8, decision batch #5, verbatim 「同意」) after a second seat, not having
read the first ruling, had recommended option 1 and moved the card back to
needs-user-decision.⛔ Option 1 (drop the row, surface a dropped-rows count) was considered and is
NOT what this PR does. The
ChartFootnoteprecedent it cited is a differentshape; it was put to the maintainer and declined.
What changed
packages/plugin-calendar/src/ObjectCalendar.tsx— the event-mappinguseMemonow makes one pass producing two lists, so the count under the calendar can
never disagree with the grid:
startstops fabricating.startDate ? new Date(startDate) : new Date()loses its second arm. A record with no start value is pushed to
unscheduledRecordsand returns — it never reachesCalendarView.allDay: !endDateapplies only to records that HAVE a start. The earlyreturn above it is the mechanism, and it is the point rather than an accident
of ordering:
!endDateused to fire for a record with no dates at all, soone absent field silently set two rendered properties and a dateless record
rendered as an all-day event on today. A record with a start and no end is
unchanged.
isNaNfilter keeps its original job for a value that is present butunparseable. That record is dropped, never bucketed as unscheduled — absent
and malformed are two different defects and the two paths stay separable in
both the code and the tests.
The area itself: collapsed by default, a visible
Unscheduled (N)count, anexpandable list of titles. Deliberately inert per the ruling — no
drag-to-schedule, no date picker, no way to assign a date from here. A
dedicated
SCOPE FENCEtest pins that, so a later "helpful" addition has toargue with a red test. Click-through to the record was also not added: it is
outside the ruled affordance.
i18n: one new key,
calendar.unscheduled=Unscheduled ({{count}}), added toall ten locale packs. It reads through
useObjectTranslationrather than thefile's existing
useSafeTranslate, because the latter passes no options toi18next and so cannot fill the
{{count}}hole; both spell the provider-lessfallback the same way (objectui#6219), so the label is correct with or without
an
I18nProvider.Changeset:
patchfor@object-ui/plugin-calendar+@object-ui/i18n. Thisis a defect fix — a renderer that invented data — and the area is the honest
surface that fix needs, not a feature in its own right. No contract change
(Clause ② no): no spec key, no public type, no authored metadata moves.
Tests
New
packages/plugin-calendar/src/ObjectCalendar.unscheduled-7071.test.tsx,8 cases. The grid half of each assertion is written against the
eventsprophanded to a mocked
CalendarView(same technique asObjectCalendar.colorFieldLadder-7243.test.tsxnext door), because "appears inthe unscheduled area" alone would pass on an implementation that also still
draws the record on today's cell — which is the defect. The area itself renders
for real;
ObjectCalendarowns that DOM.The ruling's three pins, plus a fourth:
(both halves asserted), with a companion case proving no event that does
reach the grid is stamped with the current moment.
(
allDay === true,end === undefined), with the opposite direction pinnedtoo (a record with both dates is still timed), so "everything is all-day"
cannot pass.
(N)equals the number of startless records (2 of 5 rows),and the list it expands to has exactly that many
lielements.'not a date') goesthrough the
isNaNfilter: absent from the grid, absent from the area, andnot in the count. A fix that routed both cases into one bucket passes 1–3
and fails here.
Plus: collapsed-by-default / expand / collapse, a CONTROL where every record
carries a date and no area is rendered at all, and the SCOPE FENCE above.
Ablation — red-then-green, both legs proved on disk by content
The mutation restores the deleted fabricating arm
(
const start = startDate ? new Date(startDate) : new Date();) — the pre-fixbehaviour exactly. The module is imported by the test through a relative
specifier, so vitest transforms the source file directly: there is no
dist/leg for this ablation to be blind to.
Restore points at
HEADexplicitly (git checkout HEAD --plus an absolutepath), is proved by state rather than by an exit code, and the script carries
trap … EXIT INT TERMwith absolute paths.Directions were predicted before the run and all eight matched. The two
informative ones:
detect this defect, which is exactly why the other arms exist.
assertions. The all-day fact stays true under the mutation, which is the
evidence that change (2) above did not over-reach:
allDayand "has a start"are separate facts, and only the second one moved.
Verification run
All at
a0a21897b, the branch head:The new test file was confirmed to be inside the typecheck program rather
than assumed to be —
tsc -p tsconfig.test.json --listFileslists it (1 hit).Not measured here:
pnpm check:eager-closureneedsapps/console/dist/eager-closure.json, which only a consolevite buildwrites;it exits 2 locally with "broken gauge, not a passing budget". That is a missing
prerequisite, not a red gate on this diff — CI owns that run. The repo-wide
turbo run lintfarm is likewise CI's; the narrowing here is the two packagesthis diff touches, declared.
Drift note
The card and the ruling cite
:445/:451. On currentmainthe site is atObjectCalendar.tsx:508/:514.ObjectCalendar.tsxis also touched by theunlanded PR #7391 (parked behind #7399); its hunks are disjoint from this one and
git merges by content, so this is line drift, not a conflict — this diff was kept
out of those regions.
🤖 Generated with Claude Code
Generated by Claude Code
Generated by Claude Code