Found while implementing #7027 (the gantt date brand gate). Not repaired there: #7027's charter is the type gate, and this is a different reflective operation in a different function, whose current form was a deliberate choice by #6907 rather than an oversight. Fixing it would be second-guessing a landed ruling, so it is recorded instead.
What is wrong
spellGanttDateValue chooses between an array / a function / an object with Array.isArray and typeof. Its docblock argues — correctly — that these "read no author-controlled property", and it explicitly rejected Object.prototype.toString.call because that consults a Symbol.toStringTag getter that can throw.
Array.isArray reads no property, but it is still not total. On a revoked Proxy it throws:
const { proxy, revoke } = Proxy.revocable({}, {}); revoke();
Array.isArray(proxy) -> THREW TypeError:
Cannot perform 'IsArray' on a proxy that has been revoked
IsArray recurses into the proxy's [[ProxyTarget]], and a revoked proxy has none. typeof proxy is still 'object', so nothing upstream separates it out.
What #7027 does and does not change about it
Measured with a node probe replicating both functions' branch bodies verbatim (not measured in-render):
| tree |
where a revoked Proxy crashes |
7fc5c3c12 (before #7027) |
isGanttDateType — instanceof walks [[GetPrototypeOf]], which throws on a revoked proxy |
| after #7027 |
spellGanttDateValue — the new brand test refuses it cleanly, then Array.isArray throws while naming it |
So this is not a regression and #7027 does not introduce it: the value crashed the render before and crashes it after, at a different line. What changed is that it is now the only remaining throw site on this path, which is why it is worth a card of its own rather than staying invisible inside a larger one.
Why this is p3-shaped, and the honest argument against fixing it
Suggested scope if picked up
packages/plugin-timeline/src/renderer.tsx, spellGanttDateValue's trailing three branches. Either make the array test total, or state in the docblock that the totality claim excludes a revoked proxy and why that is acceptable — the same either/or #7027 was filed under. If it is taken, the row belongs in the existing pinned adversarial set (packages/plugin-timeline/src/__tests__/timeline-gantt-date-brand-7027.test.tsx), not in a new file.
⚠️ Worth reading with #7027's header: this is the fifth totality claim on this one code path, and the fourth to be falsified by the next card's measurement (#6759 → #6905 → #6907 → #7027 → this). The lesson that pattern keeps teaching is that totality asserted in prose is a hypothesis; the only thing that has settled it here is an exercised input set.
Refs: #7027 (the brand gate) · #6907 / PR #7026 (the spelling rule, which chose Array.isArray) · #6781 / PR #6905 (the type rule)
Found while implementing #7027 (the gantt date brand gate). Not repaired there: #7027's charter is the type gate, and this is a different reflective operation in a different function, whose current form was a deliberate choice by #6907 rather than an oversight. Fixing it would be second-guessing a landed ruling, so it is recorded instead.
What is wrong
spellGanttDateValuechooses betweenan array/a function/an objectwithArray.isArrayandtypeof. Its docblock argues — correctly — that these "read no author-controlled property", and it explicitly rejectedObject.prototype.toString.callbecause that consults aSymbol.toStringTaggetter that can throw.Array.isArrayreads no property, but it is still not total. On a revokedProxyit throws:IsArrayrecurses into the proxy's[[ProxyTarget]], and a revoked proxy has none.typeof proxyis still'object', so nothing upstream separates it out.What #7027 does and does not change about it
Measured with a node probe replicating both functions' branch bodies verbatim (not measured in-render):
Proxycrashes7fc5c3c12(before #7027)isGanttDateType—instanceofwalks[[GetPrototypeOf]], which throws on a revoked proxyspellGanttDateValue— the new brand test refuses it cleanly, thenArray.isArraythrows while naming itSo this is not a regression and #7027 does not introduce it: the value crashed the render before and crashes it after, at a different line. What changed is that it is now the only remaining throw site on this path, which is why it is worth a card of its own rather than staying invisible inside a larger one.
Why this is p3-shaped, and the honest argument against fixing it
Dateimpostor passes the gantt type gate and then crashesnew Datemid-render #7027 verified: ObjectUI metadata is JSON, JSON cannot spell a proxy, and the repo has noProxy.revocable, nosetPrototypeOf, and no prototype-reconstructing deserializer inpackages/*/src(swept on7fc5c3c12).an object, which says nothing the path did not already say.Array.isArraydeliberately, with a measured argument. The correct replacement shape is not settled by existing evidence — atry/catcharound it, an ordering change, or "leave it and say so" are all defensible, and picking one is an adjudication.Suggested scope if picked up
packages/plugin-timeline/src/renderer.tsx,spellGanttDateValue's trailing three branches. Either make the array test total, or state in the docblock that the totality claim excludes a revoked proxy and why that is acceptable — the same either/or #7027 was filed under. If it is taken, the row 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: #7027 (the brand gate) · #6907 / PR #7026 (the spelling rule, which chose
Array.isArray) · #6781 / PR #6905 (the type rule)