Found while implementing #7153. Not repaired there: #7153's ruled scope is the NON-TOTAL READS on the gantt date path, all of which that card and #7036 measured as the same p3 reachability class ("JSON spells neither a throwing getter nor a revoked proxy"). The three crashes below are a DIFFERENT class — they are spelled in ordinary JSON, they pass the declared zod mirror, and a full repair has to touch the render loop, which #7153 does not own.
What is wrong
findUnusableGanttDate reads the row walk DEFENSIVELY and calculateDateRange reads the same walk BARE. The two disagree, and every input in the gap crashes:
findUnusableGanttDate const rowItems = (items[rowIndex]?.items || []) as any[]; // optional chaining
for (... itemIndex < rowItems.length ...) // a non-array is SKIPPED
calculateDateRange items.flatMap((row: any) => // assumes an array
(row.items || []).flatMap(...) // NO optional chaining
So findUnusableGanttDate finds nothing to complain about, returns undefined, and hands three input shapes straight to a TypeError:
authored items | findUnusableGanttDate | then |
|---|
[null] | no diagnostic | THREW TypeError: Cannot read properties of null (reading 'items') |
[{ label: 'R', items: 5 }] | no diagnostic | THREW TypeError: (row.items || []).flatMap is not a function |
{} (a non-array) | no diagnostic | THREW TypeError: items.flatMap is not a function |
The second row generalises: any TRUTHY non-array row.items does it — 5, true, {}, and an array-LIKE { length: 1, 0: {...} }. The array-like is the sharpest reading, because findUnusableGanttDate walks it happily, JUDGES both its dates USABLE, and the very next line dies on .flatMap.
items: [{ items: 'x' }] is the one that does NOT crash — a string has .length and is indexable, so the walk reaches 'x'[0]?.startDate, gets undefined, and refuses through the ordinary diagnostic. That asymmetry is not designed; it is a side effect of which shapes happen to be index-readable.
Measurement
In-render on 51449a043, through the real TimelineRenderer (not a replica), site attributed by the stack frame:
items: [null] THREW renderer.tsx:244:10 (row.items)
items: [{ items: 5 }] THREW renderer.tsx:244:23 ((row.items || []).flatMap)
items: [{ items: {} }] THREW renderer.tsx:244:23
items: [{ items: true }] THREW renderer.tsx:244:23
items: [{ items: {length:1, 0:{dates}} }] THREW renderer.tsx:244:23
items: {} THREW renderer.tsx:243:26 (items.flatMap)
items: 'x' THREW renderer.tsx:243:26
items: 5 THREW renderer.tsx:243:26
items: [{ items: 'x' }] NAMED items[0].items[0].startDate is undefined
CONTROL an ordinary row DREW bars=1 axisCells=3
CONTROL items: [] DREW bars=0 axisCells=1
CONTROL items: [{ label: 'R' }] DREW bars=0 axisCells=1
CONTROL items: [{ items: null }] DREW bars=0 axisCells=1
CONTROL items: [0] / [[]] DREW bars=0 axisCells=1
Five live CONTROL rows on the same instrument, so the THREW rows are readings and not a broken harness.
Why this is NOT #7153's class
- JSON spells all three.
items: [null] and items: [{ items: 5 }] also PASS the declared zod mirror — items: z.array(z.any()) in packages/types/src/zod/data-display.zod.ts accepts a null element and any element value. The items: {} row is the only one the mirror would reject, and no runtime parse gate for TimelineSchema was found on the render path. - The one real composing producer passes them through.
ObjectTimeline returns authored rows on a truthiness check alone (if (schema.items) return schema.items;, ObjectTimeline.tsx:260) — no shape filter, no array check.
Why a repair confined to calculateDateRange is NOT the fix
Measured by ablation on 51449a043 (mutation proved on disk by marker counts 1 to 0 twice and by a moved blob hash; restore proved by blob equality with HEAD and an empty git diff HEAD). Making calculateDateRange tolerant — Array.isArray(items) ? items : [] and row?.items — MOVES all three crashes instead of closing any:
items: [null] 244:10 -> 1237:52 (row.label, in the row-label map)
items: [{ items: 5 }] 244:23 -> 1246:40 ((row.items || []).map, in the bar map)
items: {} 243:26 -> 1235:24 (items.map)
PASSING CONTROL an ordinary row still DREW bars=1 axisCells=3, before and after
That is the failure this code path has repeated four times: the guard did not remove the crash class, it relocated it. A real repair spans findUnusableGanttDate, calculateDateRange AND the render loop at renderer.tsx:1234-1269.
The open adjudication
A ROW that is not a row is not a DATE that does not parse, so #6781's ruled accept set does not answer it and the existing timeline.gantt.unusableRange.malformedDate hole does not read correctly for it ("items[0] is null, which is not a valid date" names the wrong fault). Two branches, and this card does not choose:
Refs: #7153 (the non-total reads, where these were measured) · #7036 / PR #7157 (the speller's exclusion) · #6750 (the EMPTY list, the closest existing card) · #6781 (the ruled type rule)
Found while implementing #7153. Not repaired there: #7153's ruled scope is the NON-TOTAL READS on the gantt date path, all of which that card and #7036 measured as the same p3 reachability class ("JSON spells neither a throwing getter nor a revoked proxy"). The three crashes below are a DIFFERENT class — they are spelled in ordinary JSON, they pass the declared zod mirror, and a full repair has to touch the render loop, which #7153 does not own.
What is wrong
findUnusableGanttDatereads the row walk DEFENSIVELY andcalculateDateRangereads the same walk BARE. The two disagree, and every input in the gap crashes:So
findUnusableGanttDatefinds nothing to complain about, returnsundefined, and hands three input shapes straight to aTypeError:itemsfindUnusableGanttDate[null]TypeError: Cannot read properties of null (reading 'items')[{ label: 'R', items: 5 }]TypeError: (row.items || []).flatMap is not a function{}(a non-array)TypeError: items.flatMap is not a functionThe second row generalises: any TRUTHY non-array
row.itemsdoes it —5,true,{}, and an array-LIKE{ length: 1, 0: {...} }. The array-like is the sharpest reading, becausefindUnusableGanttDatewalks it happily, JUDGES both its dates USABLE, and the very next line dies on.flatMap.items: [{ items: 'x' }]is the one that does NOT crash — a string has.lengthand is indexable, so the walk reaches'x'[0]?.startDate, getsundefined, and refuses through the ordinary diagnostic. That asymmetry is not designed; it is a side effect of which shapes happen to be index-readable.Measurement
In-render on
51449a043, through the realTimelineRenderer(not a replica), site attributed by the stack frame:Five live CONTROL rows on the same instrument, so the THREW rows are readings and not a broken harness.
Why this is NOT #7153's class
items: [null]anditems: [{ items: 5 }]also PASS the declared zod mirror —items: z.array(z.any())inpackages/types/src/zod/data-display.zod.tsaccepts anullelement and any element value. Theitems: {}row is the only one the mirror would reject, and no runtime parse gate forTimelineSchemawas found on the render path.ObjectTimelinereturns authored rows on a truthiness check alone (if (schema.items) return schema.items;,ObjectTimeline.tsx:260) — no shape filter, no array check.Why a repair confined to
calculateDateRangeis NOT the fixMeasured by ablation on
51449a043(mutation proved on disk by marker counts 1 to 0 twice and by a moved blob hash; restore proved by blob equality with HEAD and an emptygit diff HEAD). MakingcalculateDateRangetolerant —Array.isArray(items) ? items : []androw?.items— MOVES all three crashes instead of closing any:That is the failure this code path has repeated four times: the guard did not remove the crash class, it relocated it. A real repair spans
findUnusableGanttDate,calculateDateRangeAND the render loop atrenderer.tsx:1234-1269.The open adjudication
A ROW that is not a row is not a DATE that does not parse, so #6781's ruled accept set does not answer it and the existing
timeline.gantt.unusableRange.malformedDatehole does not read correctly for it ("items[0] is null, which is not a valid date" names the wrong fault). Two branches, and this card does not choose:spellGanttDateValue'sArray.isArrayis the last non-total operation on the gantt date path — a revokedProxycrashes it #7036 explicitly deferred as "a separate decision".itemsarray throws —calculateDateRangereduces an empty list #6750 and 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 both refused, and it would silently drop authored content.Refs: #7153 (the non-total reads, where these were measured) · #7036 / PR #7157 (the speller's exclusion) · #6750 (the EMPTY list, the closest existing card) · #6781 (the ruled type rule)