Skip to content

fix(plugin-timeline): judge gantt dates by TYPE — string, finite number, or Date - #6905

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-6781-gantt-date-type-rule
Aug 30, 2026
Merged

fix(plugin-timeline): judge gantt dates by TYPE — string, finite number, or Date#6905
os-sam merged 2 commits into
mainfrom
claude/issue-6781-gantt-date-type-rule

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6781

Maintainer ruling 2026-08-30 (comment 5469030420, batch #6), option A of three:
a gantt date is judged by type, and the rest is refused through the loud
diagnostic that already exists — no second channel, no new i18n key.

The rule, written down as a rule

packages/plugin-timeline/src/renderer.tsx gains isGanttDateType, a named
module-scope predicate carrying the ruling, the accept set stated as an accept
set, the measurements below, and a citation of this card. findUnusableGanttDate
asks it first and parses second. The ruling asked for exactly this — the rule
recorded as a ruled rule rather than as one more unattributed special case, which
is the defect the previous three cards each added one instance of.

accept: string | FINITE number | Date
reject: everything else

The accept/reject move, exactly

Measured on the base fab4802e3 with a throwaway probe, one row item,
startDate: '2024-01-01' and endDate varied. Every row here is a value that
parses today and stops parsing after this PR:

authored valuebefore (base, measured)after
falseno diagnostic; axis 649 columns Jan 1970 … Jan 2024; bar left: 100%; width: -100%;refused, path named
trueno diagnostic; axis 649 columns; bar left: 100%; width: -99.99999999994131%;refused, path named
['2024-01-01']no diagnostic; axis ["Jan 2024"]; bar left: 0%; width: 100%;refused, path named
[0]no diagnostic; a chart dated to the year 2000refused, path named
{ toString() { return '2024-01-01' } }no diagnostic; axis ["Jan 2024"]refused, path named
a bigint (0n)THREWTypeError: Cannot convert a BigInt value to a number, uncaught, mid-renderrefused, path named
a symbolTHREWTypeError: Cannot convert a Symbol value to a number, uncaughtrefused, path named

What a consumer with such data now sees: the alert #6759 built and #6770
reused — role="alert", naming the authored path (items[0].items[0].endDate)
and the value — and no chart. Before, they saw a chart. The false reading is
byte-identical to the endDate: 0 symptom this card was filed on, which is the
whole argument: the renderer could not tell "the author means the epoch" from
"a mapping layer emitted a boolean into a date column".

The last two rows are not silent renders but crashes, and they move in the
good direction: new Date throws rather than returning an invalid date for these
two types, so on the base the guard died while trying to report the author's
error. Judging the type before parsing makes the predicate total, and it makes
spellGanttDateValue's symbol branch — written by #6759 as "total by
construction" and documented as unreachable — reachable at last.

Not moved, measured: [] and {} were already refused on the base
(new Date([]) and new Date({}) are invalid dates), and stay refused. NaN
and Infinity likewise. The finite clause therefore changes no verdict; it is
there so the accept set reads as the rule it is instead of leaving the exclusion
to a new Date coercion detail a later reader has to rediscover.

0 is kept — the control that had to stay green

Ruled explicitly, and it is the one behaviour this change must not alter. 0 is
a finite number, so it is accepted and still draws the epoch. Two pins hold it,
including this card's own filed reading asserted unchanged down to the bar
geometry
(axis 649 columns, left: 100%; width: -100%;), so a future widening
that catches 0 on its way past turns that row red.

Pre-verification 1 — census of integer 0 fed into a gantt date

Zero in-repo sites. Control that must hit, in the same query family: 222
gantt-date assignment sites repo-wide (startDate / endDate / minDate /
maxDate), so the query is live rather than mis-typed. The only numeric gantt
date anywhere in the repo is endDate: Date.UTC(2024, 2, 1) in #6770's pin file;
the only literal is inside a comment in renderer.tsx.

Honest reading: "keep 0" rests on the encoding's terms — under a
startDate: 1704067200000 millisecond encoding 0 really is a date, and
refusing it would remove a real capability for a hypothetical input — not on a
measured in-repo consumer. That is why the pin for it is a test rather than a
call site.

Pre-verification 2 — can false / [] reach the renderer from the SDUI author surface?

Row dates: yes, entirely unguarded — the type rule is the only gate there.
TimelineSchema declares items as z.array(z.any()) (and items?: any[] in
the TS twin), so measured through TimelineSchema.safeParse:

valueas a row endDateas a minDate pin
falsePARSESrejected
[]PARSESrejected
{}PARSESrejected
0PARSESrejected
'2024-01-01'PARSESPARSES

So the benefit on row dates lands on the full authoring population, JSON
metadata included — not only hand-written TS.

Pins: defence in depth, as the ruling anticipated.minDate / maxDate are
z.string(), so a non-string pin is rejected at the authoring boundary — but
only by the boundary that actually runs it, which is the CLI
(packages/cli/src/commands/check.ts and validate.ts). TimelineRenderer does
not validate at render, so the type rule is the only gate for a document that
never went through objectui check. The pin path barely moves regardless: the
caller judges only a truthy pin, so false / 0 / '' pins stay discarded
by the existing ||, exactly as before.

Clause ② — this is a reject-direction change

Opened as a draft for the CONTRACT_REVIEW_TIER review; not marked ready,
not merged. The reject-direction move is the table above; the changeset states
it in the same terms for people who never read this thread, including what an
author should emit instead.

Verification

Reproduce-red came first, as an ablation of the new arm rather than a claim:
!isGanttDateType(value) || was replaced on disk with the base arm
value === null ||. Mutation proven on disk by anchor counts (new-arm 1 to 0,
base-arm 0 to 1) and a changed blob hash; restore proven both ways
git diff HEAD empty and git hash-object equal to the HEAD blob
5a47a6fef0701cea53fd4b86207492d9020a476e — under a trap ... EXIT INT TERM
with absolute paths. No dist leg exists to skip: the pin file imports
../renderer by relative path, so vitest compiled the mutated source directly.

Predicted 9 red, observed 9 red, same set, row for row (Tests 9 failed | 15 passed (24)):

#testpredictedobserved
1pin 1 refuses falseREDRED
2pin 1 refuses trueREDRED
3pin 1 refuses ['2024-01-01']REDRED
4pin 1 refuses [0]REDRED
5pin 1 refuses { toString }REDRED
6pin 2 bigint is named, not thrownREDRED
7pin 2 symbol is named, not thrownREDRED
8pin 6 truthy non-date pin refusedREDRED
9pin 7 ObjectTimeline reaches the same refusalREDRED
pin 3 endDate: 0 UNCHANGEDGREENGREEN
pin 3 0 as a meaningful epoch startGREENGREEN
pin 1 non-finite number; invalid Date objectGREENGREEN
pin 4 string / number / Date / valid-gantt geometryGREENGREEN
pin 5 null / undefined / one-diagnostic / bad stringGREENGREEN
pin 6 falsy pin discardedGREENGREEN
pin 7 empty gantt; vertical variantGREENGREEN

The two 0 rows are green in both directions, which is the sharpest form of
that control: the change provably does not touch 0.

Union below ran on the pushed head 7b34c944a (tree
e80d2a2e3848dda1a7f1b00fc297b5199e9479ea, byte-identical to the tree the run
was made on), from the repo root as AGENTS.md requires:

gateresult
vitest run packages/plugin-timeline/17 files, 160 tests passed (24 of them new)
turbo run type-check --filter @object-ui/plugin-timeline11 tasks successful
turbo run lint --filter @object-ui/plugin-timeline2 tasks successful
check:control-bytesOK (scanned 5752 tracked text file(s))
check:vi-mock-specifiers, check:vi-mock-inheritexit 0
check:i18n-keys, check:i18n-drift, check:i18n-dead-keysexit 0
check-changeset-presence, changeset:checkexit 0
check:side-effects-arrayexit 0

tsc -p tsconfig.test.json --listFiles confirms both changed files are
inside the typecheck program (1 hit each), so "typecheck clean" is a statement
about the new test file too, not around it.

Lint compared against the base version of the same file, not a bare count:
base renderer.tsx (46711 bytes, extracted from fab4802e3 and byte-checked
before use) reports 18 warnings / 0 errors; head reports 17 / 0 — the same rule
set, one fewerno-explicit-any because the predicate dropped its
value as any cast. The new pin file: 0 errors, 3 no-explicit-any warnings,
against 4 warnings on #6770's sibling pin file it is modelled on.

Declared narrowing. The repo-wide pnpm lint is CI's run. Locally the whole
affected package was linted, and the narrowing is measured rather than assumed:
the population is read from eslint's own config (24 files in
packages/plugin-timeline), the count from --format json (0 errors, 96
warnings, all pre-existing), and eslint.config.js sets no
parserOptions.project or projectService, so type-aware linting is off and
this diff cannot move the verdict on any file it does not touch. Two gates,
check:eager-closure and check:sdui-registration-pins, exited 2 as
PREREQUISITE NOT MET — both print that they need an apps/console build and
that an absent build is "a broken gauge, not a passing budget". They are recorded
as NOT MEASURED locally, not as failures; CI builds the console and runs them.

Scope

packages/plugin-timeline/ only, plus the changeset. Line numbers were
re-derived on this base, not copied from the card (#6782 had already landed in
this file). Nothing under content/docs/releases/, no test skipped, disabled or
quarantined. spellGanttDateValue is deliberately untouched: how a refused value
is spelled is a separate adjudication from which values are refused, and the
ruling named findUnusableGanttDate / isUnusable as the landing site. The
measured consequence is reported separately rather than repaired here — see the
out-of-scope finding filed alongside this PR.

Generated by Claude Code


Generated by Claude Code

…er, or Date
Maintainer ruling 2026-08-30 (option A of three): a gantt date must BE a date
type. Everything else — `false`, `true`, arrays, objects with a plausible
`toString`, bigints, symbols — is refused through the existing loud diagnostic
instead of being coerced by `new Date` into a silent 1970 axis.
`0` is KEPT, deliberately and by ruling: it is a legitimate epoch timestamp
under a millisecond encoding, so refusing it would take away a real capability
to catch a hypothetical input. Its filed `endDate: 0` reading is pinned
UNCHANGED, bar geometry included.
The type gate runs BEFORE `new Date`, which is load-bearing: `new Date` THROWS
on a bigint or a symbol, so on the base the guard crashed while trying to report
the author's error. The predicate is now total.
`null` and `undefined` are subsumed by the rule rather than named separately;
every diagnostic an author sees for them is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.4 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-29MaGKDI.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

The out-of-scope finding the description refers to is #6907spellGanttDateValue names an array date blankly today (endDate is , which is not a valid date, already true on main), and once this PR lands it will name ['2024-01-01'] as 2024-01-01 and a bigint 0n as 0, i.e. as text that reads like a legal value. Left untouched here deliberately: the ruling named findUnusableGanttDate / isUnusable as the landing site, and which spelling is right is its own adjudication (it also moves the undefined / null / quoted-string spellings that #6759 and #6770 pinned).

Session for this implementation: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB

Generated by Claude Code


Generated by Claude Code

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Clause-② review — ACCEPT WITH FOLLOW-UP

CONTRACT_REVIEW_TIER clause-② reviewer, dispatched by the domain:ui PM seat. Reviewed read-only from a detached worktree at head 7b34c944af51d5755f7e29bee11f7005055055c5; merge-base of head and the dispatch-time main is fab4802e3 — the same base every measurement in this PR names. The PR stays draft and is not mine to mark ready, close, or merge.

CI: not terminal at dispatch (5 runs in progress). Terminal at review time, read from the check-runs table on this head: 30 total · 27 success · 3 skipped · 0 failures. The 3 skips (Test (coverage), its shard, dependabot) never ran — no-readings, not greens. The five in flight at dispatch (Type Check + 4 test shards) all completed success. The two gates the PR recorded as locally NOT MEASURED (check:eager-closure, check:sdui-registration-pins) both run in CI in performance-budget.yml, green on this head, and the budget bot posted the eager-closure PASS (3178.4 KB ≤ 3222.7 KB). I confirm the PREREQUISITE reading — exit 2 for a missing console build is a broken gauge, not a red — rather than inheriting it.

Re-measured myself (not inherited)

  • Full suite at head: vitest run packages/plugin-timeline/ → 17 files / 160 tests, all green.
  • The reject-direction table, both arms. I wrote my own probe (measurements, not assertions) and ran it twice: on head, and with isUnusable ablated on disk to the base arm (value === null || Number.isNaN(new Date(value as any).getTime()) — textually identical to base's predicate; the only non-comment code in this diff is the new predicate plus that one line, so the ablated semantics are base's). Every row verified. Base: false → 649-col axis Jan 1970–Jan 2024, bar left: 100%; width: -100%; (byte-identical to the 0 reading); true → 649 cols, width: -99.99999999994131%; ['2024-01-01'] and {toString} → a normal-looking Jan 2024 chart; [0] → a 289-col axis starting Jan 2000 — all with no diagnostic. Head: all refused, role="alert", path items[0].items[0].endDate named.
  • The three corrections to the card's framing, verified: [] and {} were already refused on base (my base-arm probe renders the diagnostic), NaN/Infinity likewise — the finite clause changes no verdict. And 0n / Symbol('s')threw uncaught TypeErrors from inside the guard on base (Cannot convert a BigInt/Symbol value to a number, measured) — on head both are named, and the symbol is spelled Symbol(s), so spellGanttDateValue's documented-unreachable branch is now reachable and correct.
  • The 0 control, both directions: on head, 0 draws the exact filed reading (649 cols, Jan 1970–Jan 2024, left: 100%; width: -100%;); under the base-arm ablation, the identical reading. Under that same ablation the PR's pin file goes 9 failed | 15 passed, the failed set matching the PR's prediction row for row (5×pin 1, 2×pin 2, pin 6 truthy, pin 7 ObjectTimeline), with both pin-3 0 rows among the green. The change provably does not touch 0. Ablation restore verified both ways (empty git diff HEAD, blob back to 5a47a6fef…).
  • Pre-verification 2: re-ran TimelineSchema.safeParsefalse / [] / {} / 0 all parse as a row endDate, all rejected as a minDate pin; string control parses on both surfaces. Declarations confirmed (items: z.array(z.any()).optional(), minDate/maxDate: z.string().optional()). Render path confirmed unguarded — TimelineRenderer runs no zod; one nuance the PR body doesn't mention: ObjectTimeline does call TimelineExtensionSchema.safeParse at render, but on failure it only console.warns and renders anyway, so it is a warning, not a gate, and the claim's substance stands. The CLI is the zod boundary (validate.tssafeValidateSchema from @object-ui/types/zod). The benefit does land on the full authoring population.
  • Pre-verification 1's zero, with my own control: re-ran the census in my own query family at base and head: zero integer-0 gantt-date assignment sites, with a control that must hit and did — 193 assignment-site occurrences at base, 256 at head (53 of them in the new pin file). I could not reproduce the dev's exact control figure of 222 (query-family difference; mine counts key\s*: occurrences over tracked files) — the substance (live control, zero reading) is verified independently, the specific 222 is not. Also verified: the only numeric gantt date at base is Date.UTC(2024, 2, 1) in finding(plugin-timeline): a null gantt date is silently coerced to the epoch — a 649-column axis and a negative-width bar, with no diagnostic #6770's pin file, plus one literal inside a renderer.tsx comment.
  • Lint A/B re-run: base renderer extracted from fab4802e3 and byte-asserted (46711; head blob 5a47a6fef… at 53488 — both match the disclosed VOID-comparison correction), linted under the head config: base 0 errors / 18 warnings (15 no-explicit-any + 3 react-refresh), head 0 / 17 (14 + 3) — exactly the claimed one-fewer-any, same rule set. Also confirmed eslint.config.js sets no parserOptions.project/projectService, so type-aware linting is off and the declared narrowing holds.
  • Scope: exactly 3 files; a single hunk in renderer.tsx; spellGanttDateValue untouched; nothing under content/docs/releases/; no .skip/.only added.

Taken on report (each cross-covered): the dev's local turbo/type-check/gate exit codes (CI re-ran all of them green on this head); tsc --listFiles program membership (CI Type Check green); the dev's own trap/restore ablation hygiene (I reproduced the ablation independently rather than auditing theirs). The duplicated PR-body footer: verified present, cosmetic, and the reason for leaving it (a body PATCH downgrades the session-URL footer) is a fair trade — no action.

#6907 — is shipping the accept set without the spelling repair net-worse for an author?

No — the separation is right, and I would have objected to the opposite. Grounds independent of the dev's stated ones:

  1. The author-experience ledger has no row that gets worse. I measured every newly-refused value on both arms. Each moved from a silent misrender (false, true, [0], {toString}) or an uncaught mid-render crash (0n, Symbol) to a diagnostic whose path component is always exact. The misleading spellings are real — I reproduced ['2024-01-01'] → "is 2024-01-01" and 0n → "is 0" on head — but they mislead only the value half of a sentence whose path half tells the author exactly where to look; at that path their own source shows the wrapper. The base alternative was no sentence, or no render. That is not net-worse.
  2. The blank spelling is not this PR's doing and barely widens.endDate is , for [] is on main today (measured on the base arm) for a value that was already refused. This PR adds non-empty arrays to the same fallback, and those spell as their joined text, not blankly.
  3. The obvious inline repair has a measured totality trap.JSON.stringify(0n) throws TypeError: Do not know how to serialize a BigInt — a drive-by JSON.stringify arm in spellGanttDateValue would reintroduce a crash class inside the very helper whose totality is its design constraint, in the same PR that removes two crash classes. That the leading candidate fails this way is the strongest evidence the spelling needs its own adjudicated design; the other arms trade against the helper's stated purpose (name-the-type stops naming what the author wrote) or a ten-locale key surface (a new sentence).
  4. Ruled scope. The finding(plugin-timeline): non-string gantt dates that coerce to the epoch (0, false, []) still draw a 649-column 1970 axis with no diagnostic #6781 ruling named findUnusableGanttDate/isUnusable as the landing site and directed refusal through the existing loud channel. Respelling inside that channel moves spellings finding(plugin-timeline): two more unusable gantt date ranges — a malformed date still throws, and an inverted author-pinned range silently draws a negative-width bar on no axis #6759/finding(plugin-timeline): a null gantt date is silently coerced to the epoch — a 649-column axis and a negative-width bar, with no diagnostic #6770 pinned — doing it as a rider would repeat the exact unattributed-special-case defect the ruling exists to end.

So: not a gate. #6907 as filed states the arms and the pinned-spelling constraint accurately (its measurements match my probes), and it should be adjudicated on its own.

Changeset

Read as a release-notes reader with no thread context: it says this is a reject-direction change, lists what stops parsing, names the diagnostic and the path it carries, tells the author what to emit instead (date string / ms timestamp / Date), and pins that 0 keeps working. The right content in the right order. Bump level patch on a 17.6.0 package is at least arguable for a reject-direction change, but the repo's Changeset Bump Policy gate is the adjudicator and it is green — no action.

One precision defect, fixable here: the table header reads startDate / endDate / minDate / maxDate, but for the pins the false row is wrong in both columns — a falsy pin was and remains discarded by the caller's truthy-|| (measured: pin 6 green on both arms), never refused and never the source of a 1970 axis. The before/after readings in that table are row-date readings. A one-line header or footnote fix keeps the notes from promising a diagnostic that a minDate: false author will not get.

Verdict

ACCEPT WITH FOLLOW-UP, on terminal-green CI (30 runs: 27 success, 3 never-ran skips, 0 failures):

  1. Changeset pin-row precision — concrete, one line, fixable in this PR (above).
  2. finding(plugin-timeline): the unusable-gantt-date diagnostic spells an array or an object either BLANKLY or as a text that looks like a valid date #6907 adjudication — concrete, not fixable here; the separation this PR made is correct and this review endorses it.

The PR remains draft; marking ready and merging are not this reviewer's to do.


Generated by Claude Code

Clause-② review follow-up. The table header read `startDate / endDate / minDate
/ maxDate`, but its before/after readings are ROW-DATE readings: a falsy
`minDate` / `maxDate` pin is discarded by the caller's existing truthy-only
`||` before anything judges it, so it never drew the 1970 axis and is not
refused now. Promising "refused, named" there would send a `minDate: false`
author looking for a diagnostic that never fires.
Header now scopes the table to a row item's `startDate` / `endDate` or a TRUTHY
pin, and a note states the falsy-pin behaviour explicitly. Release-notes text
only — no source, no test, no scope change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3178.4 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-29MaGKDI.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)174.02KB48.28KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Clause-② follow-up discharge — CONFIRMED at 7d3b920d1

Verified against the diff, not the description: one commit atop the reviewed head 7b34c944a, exactly one file (.changeset/6781-gantt-date-type-rule.md, +7/−1), and the two edits discharge the one in-PR follow-up from my review precisely —

  1. the reject table's header is rescoped to "a row item's startDate / endDate, or a truthyminDate / maxDate pin", which matches the guard's value && isUnusable(value) and my pin-6 measurements on both arms;
  2. the new paragraph states the falsy-pin behaviour outright — discarded by the existing truthy-only ||, never the source of a 1970 axis, not refused now — and labels the table's before/after readings as row-date readings, which is exactly the imprecision I flagged.

Nothing else moved: no source, no test, spellGanttDateValue and #6907 untouched.

CI on this head, read from the check-runs table myself: total_count 30 · 27 success · 3 skipped · 0 in progress · 0 failures — the 3 skips (Test (coverage), its shard, dependabot) each have completed_at at or before started_at, so they never ran: no-readings, not greens. Terminal.

Nothing outstanding on the clause-② review. The remaining follow-up — the #6907 spelling adjudication — is by design not discharged in this PR and does not gate it. Verdict stands: ACCEPT WITH FOLLOW-UP, follow-up now external only. The PR remains draft; marking ready and merging are not this reviewer's to do.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 30, 2026 14:59
@os-sam
os-sam added this pull request to the merge queueAug 30, 2026
Merged via the queue into main with commit 85f6a60Aug 30, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6781-gantt-date-type-rule branch August 30, 2026 15:12
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(plugin-timeline): non-string gantt dates that coerce to the epoch (0, false, []) still draw a 649-column 1970 axis with no diagnostic

2 participants

@os-sam@claude