fix(plugin-timeline): refuse a null gantt date instead of drawing it at the epoch - #6782
Merged
Merged
Conversation
…t at the epoch `new Date(null).getTime()` is `0`, not `NaN` — the epoch, not an invalid date — so a `null` `startDate` / `endDate` passed objectui#6759's parse guard and reached the arithmetic as `1970-01-01`. Measured on c673282, with #6759's guards already in the tree: CASE-NULL-END endDate: null -> no alert, axis 649 columns (Jan 1970 … Jan 2024), bars ["left: 100%; width: -100%;"] CASE-NULL-START startDate: null -> no alert, axis 651 columns, bars ["left: 0%; width: 100%;"] CASE-NULL-BOTH both null -> no alert, axis ["Jan 1970"], bars ["left: 0%; width: 100%;"] The last two are why this refuses rather than repairs: they do not look broken. The arm is #6759's own, not a new one. That card already refuses this input class one spelling over — `new Date(undefined)` is `NaN`, so an omitted `endDate` is refused and named — and which spelling a document carries is decided by the record mapping upstream, not by the author. The scan's predicate is `value === null`, deliberately not "coerces to the epoch": `0` is a legitimate epoch timestamp and `startDate: 1704067200000` renders 2024 correctly today. A PINNED `null` stays unjudged, because the caller's `schema.minDate || dateRange.minDate` discards it before anything reads it. Fixes #6770 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
Contributor
✅ 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
|
os-sales
marked this pull request as ready for review
August 29, 2026 13:58
This was referenced Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6770
What was wrong
new Date(null).getTime()is0, notNaN— the Unix epoch, not an invaliddate — so a
nullgantt date passed objectui#6759's parse guard untouched andreached the arithmetic as
1970-01-01.Re-derived on my own base
c6732825d(which already carries objectui#6759, PR#6772), with a throwaway probe, one row item, before any change:
endDate: nullJan 1970…Jan 2024left: 100%; width: -100%;startDate: nullleft: 0%; width: 100%;null["Jan 1970"]left: 0%; width: 100%;minDate/maxDatepinnednullJan 2024–Mar 2024(correct)endDate: 0left: 100%; width: -100%;The filed case is the first row. The two after it are the reason this is a
refusal rather than a repair: they do not look broken. A full-width bar under a
one-bucket
Jan 1970axis is a chart a reader would believe.The arm taken, and the pin that decided it
Refuse, reusing objectui#6759's
role="alert"diagnostic. The card askedfor a ruling request if the choice was genuinely open; measurement says it is
not, because this input class is already adjudicated one spelling over:
new Date(undefined)isNaN— andpins it: "an ABSENT date is the same input class and is named as
undefined" (timeline-gantt-unusable-date-range.test.tsx, pin 1). Measuredon this base, an omitted
endDatealready renders the diagnostic.nullisthat same absence with a different spelling, and which of the two a document
carries is decided by the record mapping upstream, not by the author. Making
the render differ on it would make the chart depend on how a mapping layer
spells "no value".
spellGanttDateValuealready spellsnullas itself, written by that card for"one they wrote as empty". Until this change nothing could reach that branch:
nullparses, and a pinnednullis discarded by the caller's||."Runs to the end of the axis" is a new rendering behaviour that needs a spec
key of its own rather than a meaning smuggled into
null; "fall back tostartDate" substitutes a plausible value for one the document does not carry— the consumer-side tolerance objectui#6750 and objectui#6759 both rejected,
and AGENTS.md commandment #0.1 with them.
The card's ⭐ note — a
nullfrom a data source differs in kind from an authortyping a wrong date — is real, and it does not separate
nullfromundefined,which is the spelling already refused. If the product wants tasks with no end
date to render open-ended, that is a feature with an authored spelling of its
own, not a coercion of
null; worth a separate card if it is wanted.Where the line is:
null, not "coerces to the epoch"The predicate is
value === null, deliberately nothing wider.0coerces to theepoch as well (measured above, same 649-column axis) — but
0is a legitimateepoch timestamp, indistinguishable from an author who means 1970-01-01, and
startDate: 1704067200000renders 2024 correctly today. Both a numerictimestamp and a
Dateinstance are pinned as still rendering. The otherepoch-coercing values (
0,false,[]) are filed separately asobjectui#6781 rather than swept in here.
Unchanged, and pinned as unchanged
minDate: null/maxDate: nullis falsy, so the caller'sschema.minDate || dateRange.minDatediscards it and the rows' own range stillrenders — the same reasoning objectui#6759 applied to
minDate: ''.items: [].Wording, and why no new i18n key
The diagnostic is objectui#6759's
timeline.gantt.unusableRange.malformedDate,whose
{{value}}hole is filled asnull. Anull-specific sentence would be anew key in all ten locale packs under
packages/i18n, which is outside thiscard's file surface and would drag in nine translations. Flagging it as the one
reviewable choice here: the rendered sentence reads "items[0].items[0].endDate
is null, which is not a valid date. Every gantt date has to parse …", and the
trailing clause is loose for a value that technically does parse.
Verification
Union re-run at
d6fe6d3c4, the final commit, on a clean tree:pnpm exec vitest run packages/plugin-timeline/—Test Files 16 passed (16),Tests 136 passed (136)(14 of them this card's new pins).pnpm --filter @object-ui/plugin-timeline type-check— exit 0, andtsc --listFilesconfirms both the edited renderer and the new test file arein the program (1 hit each), so "typecheck clean" is a statement about this
diff.
pnpm --filter @object-ui/plugin-timeline lint— 0 errors (92 pre-existingno-explicit-anywarnings, none new in kind).check-control-bytes,check-vi-mock-specifiers,check:i18n-keys,check-changeset-presence,check-changeset-no-major,check-changeset-fixed,check-changeset-overwrite,check-lint-coverage,check-type-check-coverage,check-shell-escape-residue— each exit 0 with its own verdict line.Reverse verification (ablation). With the fix committed, the base renderer
was put back on disk pinned by commit sha, the mutation was confirmed on disk
before reading anything (
isUnusableoccurrences 3 → 0,doesNotParse0 → 3,and the on-disk blob hash equal to the base blob), then the new pin file was run:
6 failed | 8 passed. The six are exactly this card's refusal pins; the eight
that stay green are the unchanged-behaviour pins, which must pass on the base
too. Restoration is
git checkout HEAD --on an absolute path, proved bygit diff HEADempty and the on-disk hash equal to the HEAD blob, and the pinswere re-run green afterwards. No rebuild leg: the test imports
../rendererrelatively, so vitest transforms the source and no
dist/sits in the path.Declared narrowing.
check:eager-closureandcheck:sdui-registration-pinsboth require a full
apps/consolebuild; without one they reportNo console build to weigh(exit 2 = measured nothing, not a failure) and areleft to CI. Neither subject is touched here: this diff adds no new runtime
strings — the diagnostic reuses keys already in the eager closure — and no
registration array changes.
Generated by Claude Code
Generated by Claude Code