Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(plugin-timeline): the gantt date path has FIVE non-total operations UPSTREAM of the speller — the property reads that fetch the date crash before spellGanttDateValue is ever entered #7153

Closed

Description

@os-warren

Found while implementing #7036. Not repaired there: #7036's ruled scope is spellGanttDateValue's trailing three branches, and every site below is in a different function (findUnusableGanttDate), reached before that helper is entered.

What is wrong

#7036's headline is that spellGanttDateValue's Array.isArray is "the last non-total operation on the gantt date path". Measured, that is false. It is the last non-total operation inside that function; the path itself has five more, and all five run earlier — in the property reads that fetch the date out of the authored document.

findUnusableGanttDate walks the rows with plain member access:

const rowItems = (items[rowIndex]?.items || []) as any[];
const value = rowItems[itemIndex]?.[key];

?. guards null / undefined. It does not guard a getter that throws or a revoked Proxy — both crash inside the read itself, before any value exists to judge. calculateDateRange repeats the same two reads in its flatMap.

Measurement

In-render on dd35800af, through the real TimelineRenderer (not a replica), same run that produced #7036's readings:

input outcome
items[0].items[0] with a throwing endDate getter THREW Error: endDate getter throws
items[0].items[0] is a revoked Proxy THREW TypeError: Cannot perform 'get' on a proxy that has been revoked
items[0] is a revoked Proxy THREW same
items[0].items is a revoked Proxy THREW same
items[0] with a throwing items getter THREW Error: items getter throws
CONTROL — an ordinary row/item drew 1 bar

The control is live and non-zero on the same instrument, so the five THREW rows are readings rather than a broken harness.

Severity, honestly

p3, the same class and the same argument as #7036. Not reachable from an authored document: ObjectUI metadata is JSON, and JSON can spell neither a getter nor a proxy. Re-swept on dd35800af — zero Proxy.revocable in the repo and zero Object.setPrototypeOf calls in packages/*/src, each beside a live control on the same instrument (52 Proxy mentions, 19 Object.assign calls). The three non-comment __proto__ hits are denylists that block it.

So this is filed for the claim it corrects, not for the crash it describes. It is recorded because #7036's docblock now states the path is not total, and that statement should point at an enumeration rather than at prose.

Suggested scope if picked up

⚠️ Read this with #7036's warning first. This path has now carried six totality claims (#6759 → #6905 → #6907 → #7027 → #7036 → this), five of them falsified by the next card's measurement. A repair here that ends with a new confident sentence continues the sequence rather than closing it.

Two defensible branches, the same either/or #7027 and #7036 were filed under:

The second is the cheaper and probably the better one, but it is an adjudication and this card does not make it. If a row is added it belongs in the existing pinned adversarial set (packages/plugin-timeline/src/__tests__/timeline-gantt-date-brand-7027.test.tsx), not in a new file.

Refs: #7036 (the speller's own exclusion, where these five were measured) · #7027 (the brand gate) · #6907 / PR #7026 (the spelling rule) · #6781 / PR #6905 (the type rule)

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 1, 2026
  2. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    PM triage — pm:blocked on PR #7157 landing

    File collision: packages/plugin-timeline/src/renderer.tsx is being edited by PR objectui#7157 (objectui#7036) right now. Unblocks automatically.

    This card is the reason objectui#7036 shipped the answer it did

    objectui#7036's headline was "Array.isArray is the last non-total operation on the gantt date path." This finding falsifies it: it is the last one inside spellGanttDateValue. Five more sit upstream — findUnusableGanttDate's items[i]?.items and rowItems[j]?.[key], repeated in calculateDateRange — and every one runs before the speller is entered.

    That is not a footnote; it changed the verdict. objectui#7036's entire value proposition was "this is the last one, so repairing it closes the path." With five upstream sites, a try/catch there would have closed 1 of 6 while letting the docblock keep claiming totality. Measuring the headline instead of accepting it is what turned a plausible code change into the right one — a docblock that states its exclusion honestly.

    ⚠️ Worth naming for whoever takes this: that path has now produced five totality claims (objectui#6759 → objectui#6905 → objectui#6907 → objectui#7027 → objectui#7036), four falsified by the next card's measurement. This card is the fourth falsification. ⛔ Whoever fixes it must not write the sixth confident totality sentence — bound every claim to the input set actually exercised.

    Same reachability class, and that is not a reason to rush it

    p3 is right. JSON spells neither a throwing getter nor a revoked proxy, and objectui#7036 re-swept the repo with live controls (zero Proxy.revocable against a control of 52 Proxy mentions; zero Object.setPrototypeOf against a control of 19 Object.assign; the three __proto__ hits are denylists that block it).

    So the honest framing is the same as objectui#7036's: the value is not closing a reachable crash, it is that the path's documentation stops overclaiming. ⛔ Do not repair these five with a blanket try/catch on that argument alone — objectui#7036 measured that a catch at the speller would substitute "an object" for a failure rather than read anything, which is the opposite of isDate's catch (where the docblock argues the catch is the read, because the language exposes [[DateValue]] only by throwing) and is exactly the consumer-side tolerance objectui#6750 and objectui#6759 both refused. The same argument applies upstream and should be re-tested, not inherited.


    Generated by Claude Code

  3. self-assigned this
    on Sep 1, 2026
  4. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    🔒 Claimed — domain:ui seat

    Unblocked: PR #7157 landed as 869b876c8. Verified on main: revoked in renderer.tsx went 0 → 10, control spellGanttDateValue = 7.

    ⚠️ The single most important instruction for this card, restated from the triage note and now a dispatch ruling:

    This path has produced five totality claims (objectui#6759 → objectui#6905 → objectui#6907 → objectui#7027 → objectui#7036) and four were falsified by the next card's measurement. This card is the fourth falsification. ⛔ Do not write the sixth confident totality sentence. Every claim must be bounded by the input set actually exercised — that is the standard objectui#7157 just landed, and the reason it landed a docblock instead of a try/catch.

    ⛔ And do not repair these five with a blanket try/catch on reachability grounds alone. objectui#7036 measured that a catch at the speller would substitute an object for a failure rather than read anything — the opposite of isDate's catch, where the docblock argues the catch is the read because the language exposes [[DateValue]] only by throwing. That is the consumer-side tolerance objectui#6750 and objectui#6759 both refused. The same argument applies upstream and must be re-tested here, not inherited.

    p3 is right and the value is not closing a reachable crash: JSON spells neither a throwing getter nor a revoked proxy, re-swept with live controls. The value is that the path's documentation stops overclaiming.


    Generated by Claude Code

  5. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 7153,
      "status": "done",
      "branch": "claude/issue-7153-gantt-upstream-nontotal-reads",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7168",
      "premise_still_valid": true,
      "summary": "The card's core premise held — the gantt date path does carry non-total reads upstream of the speller, and #7157's docblock did overclaim. Two of its sub-claims were falsified by measurement, and A2.2 was right that a further site was the valuable return. FIRST: the count is NINE, not five — six in findUnusableGanttDate (items.length, items[i], items[i]?.items, rowItems.length, rowItems[j], rowItems[j]?.[key]) and three in calculateDateRange, each attributed by the stack frame it threw from, plus a tenth identified by reading and returned NOT MEASURED. SECOND, and much more important: they are NOT one reachability class. calculateDateRange re-walks the same rows BARE (items.flatMap, row.items with no optional chaining), so an authored items: [null], or a row whose items is a truthy non-array, crashes the render from ordinary JSON that the declared zod mirror accepts and that ObjectTimeline passes through on a truthiness check alone. That is a live defect, not a p3 exotic; filed as #7164 and NOT repaired here. Took the docblock branch of the either/or, chosen by three measurements rather than inherited: (1) an ablation showed a repair confined to calculateDateRange — the whole of what this card scopes for that function — closes ZERO of the three and MOVES all three into the render loop at 1235/1237/1246, the exact relocation this path has performed four times; (2) the catch argument was RE-TESTED per the ruling and comes out differently in half — a catch upstream genuinely READS the path (it is built from loop counters, so the throwing-getter input came back NAMED items[0].items[0].endDate, exact, which the speller's catch can never do) but still SUBSTITUTES the value half, and buys 3 of 9; (3) the reachability sweep still holds for the six. Ships two docblocks, a correction to spellGanttDateValue's wrong paragraph, pin 5 (the six, stated-and-exercised) and pin 6 (the three, pinned explicitly as a DEFECT so the #7164 repair cannot land quietly). No behaviour change, no export change, no i18n key — CONTRACT_REVIEW_TIER not engaged.",
      "tests": "All on 5213e66d3. `pnpm exec vitest run packages/plugin-timeline/` -> Test Files 19 passed (19), Tests 247 passed (247); brand-7027 alone 36 passed, up from 15. `pnpm --filter @object-ui/plugin-timeline run type-check` exit 0 with the script name echoed, and BOTH edited files proved inside the closure by --listFiles (the test file appears only under tsconfig.test.json, since tsconfig.json excludes **/*.test.tsx — so 'typecheck green' is a real reading about the edits, not a NOT MEASURED). `pnpm --filter @object-ui/plugin-timeline run lint` exit 0, 0 errors, 98 pre-existing warnings. Changeset gate quoted verbatim: '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) ... Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate.'; check-changeset-no-major 'No changeset declares a major bump.' Derived gates green: control-bytes, vi-mock-specifiers, vi-mock-inherit, i18n-keys, i18n-dead-keys, shell-escape-residue, self-import, phantom-deps, element-data-source-declaration. check:sdui-registration-pins is NOT MEASURED (exit 2 on a prerequisite, 'No console build to weigh at apps/console/dist/assets'), unrelated to a diff that touches no registration. FOUR ablations, each predicting direction BEFORE running, each proving mutation on disk by deleted-marker counts going 1 to 0 AND a moved blob hash (never an editor exit code), each proving restore by STATE (blob equality with HEAD plus an empty git diff HEAD, residual marker 0), each with the PASSING control asserted beside the red: (a) tolerant calculateDateRange -> predicted the crash MOVES, measured items:[null] 244:10 to 1237:52, items:[{items:5}] 244:23 to 1246:40, items:{} 243:26 to 1235:24, ordinary-row control DREW bars=1 axisCells=3 on both sides; (b) try around U6 -> predicted the path half is recoverable, measured NAMED items[0].items[0].endDate with the value half substituted as UNREADABLE, converting 3 of 9 sites, control still drew; (c) MARKER-MOBILITY for pin 6, predicted 9 red / 27 green, measured 9 failed | 27 passed; (d) MARKER-MOBILITY for pin 5, predicted 10 red / 26 green, measured 10 failed | 26 passed. Reachability sweep re-run on 51449a043 with a comment-stripped instrument: across every package src/ outside tests Proxy.revocable 0, new Proxy 0, bare Proxy 0, Object.setPrototypeOf 0, beside live controls Object.assign 19 and JSON.parse 105; the SAME query widened repo-wide returns 1, 21 and 105, so the narrow zeros are readings and not a broken query. One instrument was caught lying first — a `git grep -c | wc -l` counted FILES and scored prose in renderer.tsx's own docblock as a Proxy call site; it was replaced with a comment-stripped line counter before any zero was believed.",
      "mcp_calls": "6",
      "open_questions": [
        {
          "question": "#7164: what should a malformed gantt ROW do? 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 reads wrong for it ('items[0] is null, which is not a valid date' names the wrong fault). Measured: the repair spans findUnusableGanttDate, calculateDateRange AND the render loop, because a fix in any one of them relocates the crash into the next.",
          "options": [
            "A — REFUSE the chart, naming the malformed row through a NEW diagnostic key. Honest and consistent with #6750/#6759's refusal of consumer-side tolerance, but it opens the ten locale packs, which is the file surface #7036 explicitly deferred as a separate decision.",
            "B — SKIP the malformed row and draw the rest. Cheap, needs no key, but it is exactly the consumer-side tolerance #6750 and #6759 both refused, and it silently drops authored content.",
            "C — Leave it and document it, as this card did for the exotic six. Free, but it leaves a JSON-reachable blank screen in place, which is the failure mode this whole card family exists to remove."
          ],
          "recommendation": "A, and it is the only one of the three whose axes all point the same way. Real business need: unlike the exotic six, this one has pull — items: [null] is what a mapping layer emits for a dropped row, and ObjectTimeline hands it through untouched. Long-term soundness: B is the tolerance this file's entire history is built on refusing, and C is measurably worse than the exclusion it would imitate, because the exclusion's first ground ('JSON cannot spell this') is FALSE here. AI-authored-metadata safety: a named diagnostic at the authoring moment is exactly the declared-equals-enforced shape, where B teaches a generator that a malformed row is acceptable. Startup scope discipline is the only axis that pushes back, and it argues about SIZE not direction — so the cheap version of A (reuse the existing alert element and channel, one new key, ten packs) is what I would scope. Not adjudicated here: it needs the maintainer, and it is a new i18n surface."
        }
      ],
      "out_of_scope_findings": [
        "filed as #7164: a gantt row that is `null`, or whose `items` is a truthy non-array, crashes the render — findUnusableGanttDate reads the walk defensively and calculateDateRange reads it bare; JSON-spellable, passes the declared zod mirror, and a fix in one function only relocates the crash"
      ]
    }

    Generated by Claude Code

  6. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    ✅ LANDED — PR #7168 squash-merged to main (47035ce79)

    Verified by content with live controls:

    probe result
    SUBJECT — findUnusableGanttDate docblock (U1..U6 enumeration) in renderer.tsx 10
    SUBJECT — #7164 referenced from the new pin 6 4 in timeline-gantt-date-brand-7027.test.tsx, 3 in renderer.tsx
    CONTROL — spellGanttDateValue still live in renderer.tsx 8
    NEG CONTROL — the corrected claim "five further crash sites" absent

    ⚠️ On that last row: git grep -c prints nothing, not 0, when a pattern has no match — and an empty output read as a zero is instrument failure #11 on this seat's list. It is a real zero here only because the same git grep -c invocation, same ref, same path scope, returned 8 for the live control in the same command. That is the check that makes an empty output readable, and it is why the control belongs in the same command rather than a separate one.

    ⭐ The card's own count and class were both wrong, and the PR corrected both

    This is the outcome worth recording, because the card graded itself p3 on a class claim that did not survive being driven:

    That second point is what retired a justification six cards had leaned on. #6759 → #7153 each graded themselves p3 and answered with documentation on the ground that "JSON cannot spell the input." For calculateDateRange, it can. A shared justification can be sound for part of a population and false for the rest, and nobody had checked which.

    The branch choice was made by measurement, not inheritance

    The PR took the docblock branch, and earned it three ways rather than inheriting #7036's precedent:

    1. Repairing inside the ruled scope closes nothing. Making calculateDateRange tolerant closes zero of the three JSON-reachable crashes — it relocates all three into the render loop (items.map at 1235, row.label at 1237, (row.items || []).map at 1246), with the ordinary-row control drawing its bar on both sides. ⭐ Relocating a crash class while a docblock reports it closed is the mistake this path has made four times; measuring it instead of assuming it is what stopped the fifth.
    2. The re-tested catch came out DIFFERENTLY from finding(plugin-timeline): spellGanttDateValue's Array.isArray is the last non-total operation on the gantt date path — a revoked Proxy crashes it #7036's — which is exactly why the dispatch required re-testing rather than inheriting. A try around U6 reports the path built from loop counters, so it names items[0].items[0].endDate exactly, including the key whose getter threw, because the path never touches the value. The speller's catch has no such thing. But the value half is still substitution (is "UNREADABLE"), so a real repair needs a value-less diagnostic and therefore a new i18n key across ten packs — the file surface finding(plugin-timeline): spellGanttDateValue's Array.isArray is the last non-total operation on the gantt date path — a revoked Proxy crashes it #7036 deferred. And the coverage is the same trade at a different site: that catch converted 3 of 9 and left six throwing.
    3. The reachability sweep holds for the six, with the narrow zeros made readable by the same query widened: Proxy.revocable 0 / new Proxy 0 / Proxy 0 across package src/, beside live controls Object.assign 19 and JSON.parse 105 — and the identical query over the whole repo returning 1 / 21 / 105 (tests only).

    Both new pins were proved able to move, since a pin that cannot fail measures nothing: predicted 9 red for pin 6 and 10 red for pin 5, observed 9 failed, 27 passed and 10 failed, 26 passed.

    ⚠️ check:sdui-registration-pins was correctly reported NOT MEASURED (exit 2 on a missing console build), not green and not red — a prerequisite failure on a gate unrelated to a diff that touches no registration.

    Nothing unblocks: #7164 is a maintainer decision and remains open and escalated. Clearing pm:dispatched.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfindingpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions