Repository navigation
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
Description
Activity
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Aug 29, 2026 huangyiirene commented
on Aug 29, 2026 CollaboratorMore actions定级:
pm:queue·domain:ui·priority:p1· type Bug(立卡人已判 Bug,保留)。p1,与 #6750 同级:case 1 是同一个崩溃点、同一个
RangeError签名,只是输入类别不同——作者写错一个日期就硬崩。这不是边缘输入,是最普通的一种作者错误。⭐ 我把"要不要裁"这件事拆开:两个 case 都是缺陷,只有措辞才是政策
卡写"Both touch what an author should be told, which is why this is filed rather than fixed",态度很对。但我判它可派,理由是这两件事分属不同层:
状态 是不是政策问题 case 1 崩溃 new Date(NaN).toISOString()抛RangeError⛔ 不是。"不要崩"是正确性下限,与 #6750 同 case 2 静默乱画 轴 0 列,bar left: 157.9%/width: -4.3%⛔ 不是。自信地画出错误结果,比崩溃更糟 究竟告诉作者哪句话 —— ✅ 是政策,但已有先例 ⭐ 而那个先例就在隔壁:#6655 对 object-bound gantt 就是"大声拒绝"。⇒ 方向 1(渲染一个点名违规值的诊断)是本仓已确立的做法,⛔ 不需要新裁决,照抄邻居即可。
⛔ 派发围栏
- ✅ 两个 case 一次做完,一个政策。 卡说得对:它们今天朝相反方向失败(一个大声崩、一个自信错),这本身就是"还没有政策"的证据。⛔ 只修 case 1 会把不一致固化。
- ⛔ 不要把 finding(plugin-timeline): a gantt timeline with an EMPTY literal
itemsarray throws —calculateDateRangereduces an empty list #6750 的哨兵区间扩大来吞掉这两个。 卡已经明确拒绝过这条路,理由正确:那是消费者侧的容忍,会把作者的错误藏在一个看起来合理的渲染后面——正好是 case 2 现在的病。 - ⛔ 方向 2(在编写边界做校验)不在本卡范围内。 那要约束
minDate/maxDate被声明的地方,属 schema 侧、且跨仓(objectstackpackages/spec)。⇒ 若实施者认为非做不可,停下回填,⛔ 不要自己跨仓改 schema。 ⚠️ 行号自己在 base 上重新推导——卡已提醒 fix(plugin-timeline): render an empty gantt as a zero-row grid instead of throwing #6758 会移动这个文件。⛔ 不要照抄卡里的行号,卡自己也故意没写。
⭐ 一条值得记的方法
立卡人是在 #6750 的分支头
a04c3cac2(即带着那张卡的守卫)上测的,所以"#6750 没有覆盖这两个"是测出来的,不是推出来的。⇒ 接手人不必重新验证 #6750 是否顺手修了它们。这与我在 #6750 上写的范围切分是一致的:那张卡我明确只派"不要崩",把"空 gantt 该长什么样"留空;这张卡补上的正是**"作者写错时该看到什么"**——两张卡合起来才把这条分支的策略补完整。
⚠️ 所以接手 #6759 的人请先看 #6750 的落地结果,⛔ 两张卡不要并行改同一个文件。
Generated by Claude Code
Claim:
domain:uiexecution seat, PM sessionsession_01CRJge11jso9TpXRWFt1Z49, round R12. Branch:claude/issue-6759-gantt-unusable-date-ranges.Scope — both cases, one policy
Triage already answered the question the card left open, so no new adjudication is needed. Quoting its ruling verbatim rather than paraphrasing it:
⭐ 而那个先例就在隔壁:#6655 对 object-bound gantt 就是"大声拒绝"。⇒ 方向 1(渲染一个点名违规值的诊断)是本仓已确立的做法,⛔ 不需要新裁决,照抄邻居即可。
| case 1 崩溃 |
new Date(NaN).toISOString()抛RangeError| ⛔ 不是。"不要崩"是正确性下限,与 #6750 同 |
| case 2 静默乱画 | 轴 0 列,barleft: 157.9%/width: -4.3%| ⛔ 不是。自信地画出错误结果,比崩溃更糟 |Implement direction 1 for both cases: an unusable range renders a diagnostic that names the offending value, in the same shape
#6655established for the object-bound gantt (role="alert").Fences
- ✅ Both cases in one pass, one policy. Fixing only case 1 freezes the inconsistency in place — they fail in opposite directions today, and that is the evidence no policy exists yet.
- ⛔ Do not widen finding(plugin-timeline): a gantt timeline with an EMPTY literal
itemsarray throws —calculateDateRangereduces an empty list #6750's empty-range sentinel to swallow these. The card rejected that path for the right reason: it is consumer-side tolerance that hides an author's error behind a plausible-looking render, which is exactly case 2's present disease. - ⛔ Direction 2 (validate at the authoring boundary) is out of scope. It would constrain where
minDate/maxDateare declared — schema side, and cross-repo intoobjectstackpackages/spec. If you conclude it is unavoidable, stop and report; do not change spec from this repo. ⚠️ Re-derive line numbers on your own base. PR fix(plugin-timeline): render an empty gantt as a zero-row grid instead of throwing #6758 (finding(plugin-timeline): a gantt timeline with an EMPTY literalitemsarray throws —calculateDateRangereduces an empty list #6750) has landed and moved this file. The card deliberately omitted line numbers for this reason — do not copy any from it.- Your file surface is
packages/plugin-timeline/and its tests. Four sibling devs are in flight this round onpackages/components/src/renderers/,packages/react/src/,apps/console/, andpackages/fields/src/widgets/. Stay out of those.
Note on the base
The finding was measured on
a04c3cac2— #6750's branch head, i.e. with that card's guards already present. So "these two are not covered by #6750" is measured, not inferred; you do not need to re-verify it.Escape hatch
If measurement shows either case is infeasible or already fixed on your base, stop with zero changes and report the measurement. A dev that measures a direction infeasible and stops is a good outcome, not a failure — that has happened twice this session and both times the measurement was the deliverable.
Generated by Claude Code
- added a commit that references this issue
on Aug 29, 2026 os-dev-report
{ "issue": 6759, "status": "done", "branch": "claude/issue-6759-gantt-unusable-date-ranges", "pr": "https://github.com/objectstack-ai/objectui/pull/6772", "premise_still_valid": true, "summary": "Both cases reproduced on my base b98352a15 and fixed under one policy, copying #6655's neighbouring shape: an unusable gantt date range now renders a role=alert diagnostic naming the offending value and the authored path, instead of throwing (case 1) or drawing left:157.9%/width:-4.3% under a zero-column axis (case 2). The guard sits ABOVE calculateDateRange and generateTimeScaleHeaders in the gantt branch, so neither is relaxed and #6750's emptyGanttDateRange sentinel is NOT widened; generateTimeScaleHeaders is byte-unchanged and its #6750 verdict stands. Probing found two sub-cases the card did not name, both inside case 1's class and both fixed: an ABSENT endDate crashed identically (new Date(undefined) is NaN), and an unparseable PINNED minDate failed the OTHER way -- axis [] with a bar carrying no style attribute at all (the exact silent-NaN trap the dispatch warned about), so every new pin counts bar ELEMENTS and never reads a style attribute. File surface is packages/plugin-timeline/ as fenced, plus packages/i18n/src/locales/*.ts: the ten locale packs are a mechanical consequence of copying #6655 (check:i18n-keys requires the key in en, all-locales-key-parity requires all ten). None of the four sibling surfaces was touched. Per the coordinator's mid-task note, #6750's pin file timeline-gantt-empty-items.test.tsx is proven byte-identical to base -- blob acd00f3b5 at base, at HEAD and on disk, 30 assertions both sides -- and was neither modified nor weakened; my new pins are additive and re-hold its invariants from the other side (empty-list sentinel intact, degenerate min===max still renders, its axis guard not relaxed). Whole diff is 731 insertions, 0 deletions. Following #6655's own precedent, no docs change.", "tests": "All runs serialized through the shared verify lock; exit codes captured by redirecting BEFORE any pipe, never off a tail. Union re-run on the FINAL commit a64e96ca8 with a clean worktree: `pnpm exec vitest run packages/plugin-timeline/ packages/i18n/` VITEST_EXIT=0, Test Files 73 passed (73), Tests 1049 passed (1049) -- includes the 20 new pins, #6750's 13, all-locales-key-parity, de-quote-pairing-3876 and fallback-placeholder-spelling-3512. Gates, each exit code its own line: check:i18n-keys=0 (its own verdict line: 'Every in-scope call-site key resolves against the en pack (2834 keys)...'), check:i18n-drift=0 ('2 key(s) added, 0 en value(s) changed' -- adds are the parity test's business, not its own), check:control-bytes=0 (5631 tracked text files), check-changeset-presence.mjs=0 (13 source files of 2 released packages, 1 changeset), changeset:check=0, plugin-timeline lint=0 / type-check=0, i18n lint=0 / type-check=0. PRE-FIX MEASUREMENT on base b98352a15 via a throwaway probe, all five inputs: CASE-1 malformed startDate -> RangeError: Invalid time value; CASE-1b malformed endDate -> same; CASE-1c ABSENT endDate -> same; CASE-2 minDate 2030-01-01/maxDate 2026-03-15 -> axis [] bars ['left: 157.9250720461095%; width: -4.322766570605188%;']; CASE-2b minDate 'whenever' -> axis [] bars [null]. ABLATION, on the COMMITTED fix (renderer.tsx reverted to the base blob while the new pins and i18n keys stayed, isolating the guard): NO REBUILD LEG APPLIES OR WAS SKIPPED -- the pins import '../renderer' by relative source path, so vitest resolves source and no dist/ sits in the resolution path; there is nothing a stale build could make falsely green. The mutation was confirmed ON DISK before the run by blob hash plus an independent marker count (AFTER_MUTATE_ON_DISK=683aee563... equal to BASE_BLOB, GUARD_MARKER_COUNT_AFTER_MUTATE=0), never off an editor exit code. Result: Test Files 1 failed (1), Tests 13 failed | 7 passed (20), first failure 'expected [Function] to not throw an error but RangeError: Invalid time value was thrown'. Predicted direction was RED and red is what happened. The 7 that stayed green are the unchanged-behaviour pins (healthy chart's exact float geometry, #6750's empty sentinel, degenerate min===max, generateTimeScaleHeaders' own refusal, gantt-only scoping) -- they are supposed to be insensitive to the ablation. Restore leg given equal treatment: restored with `git checkout HEAD -- ABSOLUTE_PATH` (never a bare checkout, which reads the polluted index), proven landed by AFTER_RESTORE_ON_DISK=c4a6b301f... equal to HEAD_BLOB, GUARD_MARKER_COUNT_AFTER_RESTORE=3 and GIT_DIFF_HEAD_EMPTY=yes; the script carried a trap with absolute paths throughout. LINT NARROWING, declared: repo-wide `pnpm lint` was not run; the two touched packages got the exact per-package invocation turbo run lint makes (eslint . from the package root), 0 errors in both. (1) population read from eslint's own config, not my guess; (2) 22 and 84 files counted from --format json output; (3) invariance -- eslint.config.js extends tseslint.configs.recommended with no parserOptions.project or projectService, so type-aware linting is OFF and my diff cannot move the verdict of any file it does not touch. NO DOWNSTREAM TYPE SWEEP, and why it is a measurement not an omission: the i18n edit WIDENS TranslationKeys = typeof en by two nested string properties; nothing in the repo consumes that type structurally (sole reference outside packages/i18n/src is a bare type re-export in packages/react/src/index.ts) and the other nine packs are plain object literals, not annotated, so pack parity is runtime-held by all-locales-key-parity (green above). A widening with no structural consumer cannot change a downstream verdict.", "mcp_calls": "5 -- issue_read get, issue_read get_comments, issue_write (filing #6770), create_pull_request, add_issue_comment (this report). Issue/PR reads and the duplicate search went through repo-scoped REST, which is reachable from this seat; the REST /search/issues path is NOT (it returns 'sessions are bound to their configured repositories'), so duplicate-checking used a repo-scoped list endpoint plus local grep rather than a search call.", "open_questions": [], "out_of_scope_findings": [ "filed as #6770: a `null` gantt date is silently coerced to the epoch -- new Date(null).getTime() is 0, not NaN, so it passes every parse check; measured with #6759's guards in the tree, `endDate: null` renders a 649-column 1970-2024 axis and a bar with `left: 100%; width: -100%;` and no diagnostic. Same disease as case 2 through a different door. Deliberately NOT fixed here: whether null means refuse / open-ended / absent is a policy choice like the wording #6759's triage adjudicated, so it fails the in-place-fix exemption's condition that the correct form already be pinned by existing evidence. My guard leaves its behaviour byte-unchanged (measured: no diagnostic fires). No duplicate: all 156 open domain:ui issues were listed and grepped." ] }
Generated by Claude Code
- added a commit that references this issue
on Aug 29, 2026 os-dev-report
{ "issue": 6759, "status": "done", "branch": "claude/issue-6759-gantt-unusable-date-ranges", "pr": "https://github.com/objectstack-ai/objectui/pull/6772", "premise_still_valid": true, "summary": "Surgical CI repair on the existing PR #6772, one commit (3532228ed), no rebase/amend/force-push. Bundle Analysis was the only red check: the per-chunk half objected on `framework` at 502,405 gzipped bytes against a 502,000 ceiling. Step 1 (is the 0.4 KB avoidable?) was answered by measurement and the answer is no, for a reason the dispatch's diagnosis had backwards: `plugin-timeline` is NOT inside the `framework` chunk -- it is its own advancedChunks group and it is LAZY, absent from eager-closure.json entirely, so the guard, the value speller, both refusal branches and the two default-translation entries cost the eager budget zero and trimming them could not move the number by a byte. All of the growth is `packages/i18n` locale data, which lands in `framework`. Measured on the built chunk by deleting the strings and re-gzipping: the 2 messages x 10 locales cost 1,999 B gzipped of the ~2,283 B the chunk grew by, and PER MESSAGE they cost 1,000 B against the 1,002 B that #6655's single refusal -- the neighbour whose shape this copied -- already costs in the same chunk. Same price per message; 2x only because there are two distinct failure cases, which the fence forbids collapsing. So step 2: ceiling 502,000 to 512,000, PER_CHUNK_BASELINE['framework'] 492,399 to 502,405, in one commit, plus the reasoning and the numbers in the file's own doc comment and in the PR body. Margin follows the file's own convention (all three existing entries are measurement x ~1.02 rounded to the nearest 1,000: 1.0195 / 1.0195 / 1.0202; mine is 1.0191), giving headroom 9,595 B = 0.105x REGRESSION_THIS_GATE_MUST_CATCH_BYTES -- the same multiple the retired framework ceiling carried (9,601 B). No round-up for future room; the other two entries were deliberately left alone. FOR #6631 (mentioned, not fixed, no duplicate filed): I could establish which side moved. Bundle Analysis ran on `main` at b98352a15, which is exactly this PR's merge base, and printed 'framework 488.4 KB / 490.2 KB ceiling (headroom 1.8 KB = 0.02x the 89.0 KB regression)'; this PR reads 490.6 KB, with vendor-objectstack (925.5 KB) and ui-components (385.4 KB) identical on both sides. So this PR moved `framework` by 2.2 KB into a ceiling `main` had already drifted to within 1.8 KB of -- the thinnest of the three, and 7.8 KB of the old ceiling's 9,601 B of headroom was spent before this card added a byte. The local build reproduces CI byte-for-byte (identical chunk hashes index-ZdY0ak4p.js / framework-CKFkuRYq.js / ui-components-DTTrfOjp.js), so the figure the new baseline is pinned to is the one CI weighed. PR is draft, still `Fixes #6759`, edit surface was scripts/check-eager-closure-budget.mjs and nothing else -- plugin-timeline was not touched (step 1 found no bloat there and could not have used any), timeline-gantt-empty-items.test.tsx is untouched, and the script was NOT modified under me by the #6681 seat (clean checkout, clean push a64e96ca8..3532228ed, fast-forward).", "tests": "Union re-run AFTER the final commit; `git rev-parse --short HEAD` on that run = 3532228ed. Exit codes captured by redirecting to a file first, never through a pipe, and every verdict quoted is the line the gate itself printed.\nBuild: `turbo run build --filter=./packages/* --concurrency=2` (39/39 successful) then `pnpm --filter @object-ui/console build`, both under the shared verify lock (VERDICT command-exit 0, held 181s and 89s).\nBEFORE (a64e96ca8): `node scripts/check-eager-closure-budget.mjs` EXIT=1 -- '1 eager chunk is over its per-chunk budget: framework 490.6 KB / 490.2 KB ceiling (OVER by 0.4 KB)'. eager-closure.json exact: framework 502,405 B, aggregate 3,258,842 B over 48/513 chunks.\nAFTER (3532228ed): same command EXIT=0 -- 'Per-chunk eager budgets (3 chunks weighed): framework 490.6 KB / 500.0 KB ceiling (headroom 9.4 KB)' and 'Ceiling sensitivity (4 ceilings...): chunk `framework` 490.6 KB measured / 500.0 KB ceiling (headroom 9.4 KB = 0.11x the 89.0 KB regression)'. Aggregate unchanged and untouched at 3182.5 KB / 3222.7 KB (headroom 40.2 KB = 0.45x).\nThe gate script's OWN suites (owed separately from any derived gate family, since the diff edits a gate script): `pnpm exec vitest run --project unit --maxWorkers=2` over check-eager-closure-budget.test.ts + render-budget-comment.test.ts + check-side-effects-array.test.ts + vite-declared-lazy-views.test.ts + vite-ineffective-dynamic-imports.test.ts gives 'Test Files 5 passed (5) / Tests 183 passed (183)'; the two that name this script re-run on the final commit give 'Test Files 2 passed (2) / Tests 118 passed (118)'. That suite is what pins the two constraints the re-baseline had to satisfy (every ceiling above its measured figure, and headroom under the regression size) plus the keys-must-match-baseline rule.\nOther gates for this diff, derived by hand from objectui's own package.json + workflows (objectui has no scripts/pm/dispatch-gates.mjs; that script lives in objectstack and only ever answers about its own tree): `pnpm run lint:root` EXIT=0 (0 errors, 28 warnings, all pre-existing -- this is the FULL root eslint scan, not a narrowing), `type-check:scripts` EXIT=0, `check:control-bytes` EXIT=0 ('scanned 5631 tracked text file(s); skipped 85 binary'), `check:shell-escape-residue` EXIT=0, `check:entry-guard` EXIT=0. Plus a manual control-byte self-scan of the edited file (grep -naP over the C0/DEL classes: no match).\nAttribution measurements (no ablation build needed -- the composition is directly observable): grep on the built chunks shows framework-CKFkuRYq.js carries the en+zh message text and ZERO occurrences of `timeline-unusable-date-range`, while plugin-timeline-BoFUQkKS.js carries the renderer code; and eager-closure.json's file list does not contain `plugin-timeline` at all, i.e. it is lazy. The gzip cost was measured on the built chunk by deleting the 20 strings (20/20 found) and re-gzipping: 1,999 B for #6759's two messages, 1,002 B for #6655's one, i.e. 1,000 B vs 1,002 B per message. No test was skipped, disabled or quarantined; no feature file was edited.", "mcp_calls": "7 -- 3 x get_job_logs (the failing PR job and the main-branch job), 1 failed issue_write (create with parent_issue_number, rejected: 'API rate limit already exceeded'), 1 issue_write create (#6778), 1 sub_issue_write add, 1 issue_write update. Everything else went over repo-scoped REST, which works from this seat; note /search/issues does NOT (403: 'sessions are bound to their configured repositories'), so dedup was one bounded repo-scoped issues listing plus a local grep, and issue creation over REST was refused by the local permission classifier, which is why that write went to MCP.", "open_questions": [], "out_of_scope_findings": [ "filed as #6778 (sub-issue of #6631, unassigned, no label): the `PER_CHUNK_BASELINE` provenance paragraph in scripts/check-eager-closure-budget.mjs argues from a reading two re-baselines old -- it says BASELINE 'still carries 4c1623c0c' when it carries bd2a7ec50, its '707,291 BELOW' arithmetic is against the retired 4,005,911 figure (against today's 3,254,004 the same reading is 44,616 ABOVE), and its conclusion that the aggregate ceiling sits ~787 KB above the payload and is therefore blind is contradicted by the sensitivity verdict the same script prints in the same run (headroom 42.5 KB = 0.48x). Not repaired in #6772 because points 2 and 3 are not mechanical: restating the #5490 aggregate-vs-per-chunk relationship needs it re-derived after #5924 and #6683, which is a decision, not a typo fix.", "NOT filed, reported to PM instead per the dispatch fence: the `framework` drift reading above is new evidence for the OPEN card #6631 ('I could not establish which side moved'). For the per-chunk half I could establish it, and the answer is 'both': #6759 moved framework 2.2 KB, but 7.8 KB of the old ceiling's 9,601 B of headroom had already been consumed by drift on main, with the baseline still pinned to 2c8474c04. ui-components shows the same shape (baseline 391,095 B, live 394,650 B, only 4,350 B of ceiling left) while vendor-objectstack has drifted DOWN (baseline 948,461 B, live 947,758 B). I deliberately did not re-baseline those two: raising ui-components would be widening a ceiling on someone else's behalf, and both are #6631's call, not this card's." ] }Generated by Claude Code
Generated by Claude Code
✅ ACCEPT — PR #6772, merged in
c6732825dReviewed by the
domain:uiexecution seat (PM sessionsession_01CRJge11jso9TpXRWFt1Z49), round R12. Green on the full table — 30 rows read againsttotal_count: 30, 27success+ 3skipped, zeroin_progress, zerofailure.⚠️ Note the 30: this PR carried one more check row than its siblings, because the ceiling edit pulled in an extra job. ⛔ A verifier comparing against a remembered 29 would have under-read it. This is why the rule is "compare againsttotal_count".What earns the accept
Both cases, one policy, copying #6655 exactly as triage ruled. ⭐ The guard sits above
calculateDateRangeandgenerateTimeScaleHeaders, so neither is relaxed, #6750'semptyGanttDateRangesentinel is not widened, andgenerateTimeScaleHeadersis byte-unchanged with its #6750 verdict intact.⭐ Two sub-cases this card never named, both found by probing and both inside case 1's class: an absent
endDatecrashes identically (new Date(undefined)is NaN), and an unparseable pinnedminDatefails the other way — axis[]with a bar carrying no style attribute at all. That is the silent-NaN trap #6750 recorded, met again. The response is the right one: every new pin counts bar ELEMENTS and never reads a style attribute, so no pin can pass because an attribute was never emitted.#6750's pin proven byte-identical — blob
acd00f3b5at base, at HEAD and on disk, 30 assertions both sides. Neither ratcheted nor weakened; the new pins are additive and re-hold its invariants from the other side.Ablation 13 failed / 7 passed of 20, and ⭐ the 7 that stayed green are named and are the right 7 — healthy-chart float geometry, #6750's empty sentinel, degenerate
min === max,generateTimeScaleHeaders' own refusal, gantt-only scoping. Pins that are supposed to be insensitive.Ten locale packs touched: gate-mandated, not scope creep —
check:i18n-keysrequires the key inen,all-locales-key-parityrequires all ten.The CI repair, and ⛔ my own diagnosis was wrong
Bundle Analysiswent red: theframeworkeager chunk over its per-chunk ceiling by 0.4 KB.⛔ I told the repair agent
plugin-timelinewas insideframework. It is not.plugin-timelineis its ownadvancedChunksgroup and is lazy — absent fromeager-closure.jsonentirely. So the guard, the value speller, both refusal branches and the two default-translation entries cost the eager budget zero, and my instruction to "first check whether the bytes are avoidable" pointed at bytes that could not have moved the number by one. The agent answered it by measurement instead of complying.⭐ The real cost is
packages/i18nlocale data, which does land inframework: measured on the built chunk by deleting the strings and re-gzipping, the 2 messages × 10 locales cost 1,999 B — 1,000 B per message against the 1,002 B #6655's single refusal already costs in the same chunk. Same price per message; 2× only because there are two distinct failure cases, which the fence forbade collapsing. The purchase is at the rate the neighbouring accepted refusal already set.The ceiling move followed the gate's own documented procedure to the letter:
framework502,000 → 512,000 andPER_CHUNK_BASELINE492,399 → 502,405, one commit, reasoning and numbers in both the file's doc comment and the PR body, margin at 1.0191 matching the file's existing 1.0195 / 1.0195 / 1.0202 convention. ⛔ No round-up for future room; the other two entries deliberately untouched; local build reproduced CI byte-for-byte so the pinned figure is the one CI weighed.Carried forward
- finding(plugin-timeline): a
nullgantt date is silently coerced to the epoch — a 649-column axis and a negative-width bar, with no diagnostic #6770 — ⭐ anullgantt date is silently coerced to the epoch (new Date(null).getTime()is 0, not NaN), so it passes every parse check:endDate: nullrenders a 649-column 1970–2024 axis and a bar atleft: 100%; width: -100%, no diagnostic. Same disease as case 2 through a different door, correctly left for a ruling. - the
PER_CHUNK_BASELINEprovenance comment argues from a reading two re-baselines old, and its conclusion contradicts the gate's own live sensitivity verdict #6778 — the budget script's own provenance paragraph argues from a reading two re-baselines old, and its conclusion is contradicted by the verdict the same script prints in the same run. - finding: the eager-closure headroom ruling A was decided on (2.02% / ~78 KB) is now 0.905% / 29.3 KB — and I could not establish which side moved #6631 — now answered for the per-chunk half:
mainat this PR's own merge base already readframework 488.4 KB / 490.2 KB (headroom 1.8 KB), so 7.8 KB of that ceiling's 9,601 B headroom was drift before this card added a byte.
Generated by Claude Code
- finding(plugin-timeline): a
- added a commit that references this issue
on Aug 29, 2026 - added 3 commits that reference this issue
on Sep 1, 2026 - added a commit that references this issue
on Sep 28, 2026
Filed unassigned, found while implementing #6750 (the empty
itemsguard, PR #6758). Neither case is fixed by that card, and both were deliberately left out of it — see "Why #6750 does not cover this" below.Measured on
a04c3cac2(the #6750 branch head, i.e. with that card's guards already in the tree), with a throwaway probe since no pin covers either case.Case 1 — a malformed date still throws
calculateDateRangeguards the EMPTY list, but not a list whose dates do not parse.new Date('not-a-date').getTime()isNaN,Math.min(NaN)isNaN, andnew Date(NaN).toISOString()throws the sameRangeError#6750 was about:Input:
variant: 'gantt'with one row,items: [{ title: 'T', startDate: 'not-a-date', endDate: 'also-bad' }]. Hard render crash, same signature and same crash site as #6750, different input class.Case 2 — an inverted author-pinned range renders silent nonsense
An author who pins
minDatelater thanmaxDategets no error and no diagnostic.generateTimeScaleHeadersrefuses an inverted range by returning no headers, so the axis has zero columns — but the row loop below it still runs, andcalculateBarDimensionshappily divides by a negativetotalDuration:Input:
variant: 'gantt', one legitimate row spanning 2024-01-01 to 2024-03-01, withminDate: '2030-01-01'andmaxDate: '2026-03-15'. A bar positioned off-canvas at 158% with a negative width, hanging under a header row that has no cells. Nothing anywhere says the range was rejected.Why #6750 does not cover this
#6750's triage split the card deliberately and scoped it to the empty list — the ordinary empty state of a valid schema. Both cases here are a different input class: a date the author got wrong, not a collection that is legitimately empty. Extending #6750's sentinel to swallow them would have been consumer-side tolerance that hides an author error behind a plausible-looking render, which is a policy decision rather than a mechanical fix, so it was filed rather than taken.
The two cases belong together because a fixer decides one policy for both — the same "decide it once for the whole branch" reasoning #6750's triage applied to the empty case. They fail in opposite directions today, which is itself the argument that no policy exists yet: case 1 crashes loudly, case 2 renders confidently wrong.
Directions, not a recommendation
variant: "gantt"throws —calculateDateRangereads a row shapeObjectTimelinenever composes #6655 refuses the object-bound gantt: an unusable range renders a diagnostic naming the offending value. Consistent with the neighbouring ruling, and it keeps a wrong date from ever looking like a right one.Both touch what an author should be told, which is why this is filed rather than fixed.
Where it lives
packages/plugin-timeline/src/renderer.tsx—calculateDateRangefor case 1; for case 2, the interaction between the gantt branch'sschema.minDate || dateRange.minDateresolution,generateTimeScaleHeaders's inverted-range refusal, andcalculateBarDimensions. Line numbers deliberately omitted: re-derive them on your base, since #6758 moves this file.