From a9afb44cc0a35a4727880f0982b1fd62751e2e75 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 06:41:36 +0000 Subject: [PATCH 1/2] docs(plugin-timeline): scope spellGanttDateValue's totality claim to what is exercised `spellGanttDateValue`'s docblock claimed every branch was total. Measured in-render on dd35800af through the real `TimelineRenderer`, one branch is not: `Array.isArray` throws on a revoked `Proxy`, because `IsArray` recurses into `[[ProxyTarget]]` and a revoked proxy has none. The exclusion is stated and exercised rather than repaired. A `catch` here would substitute `an object` for a failure rather than read anything, unlike `isDate`'s catch, whose docblock argues the catch IS the read; the input is unreachable from an authored document; and it would buy no invariant, because `Array.isArray` is NOT the last non-total operation on the gantt date path. Five reads that FETCH the date throw first, all upstream of this function. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM --- .../timeline-gantt-date-brand-7027.test.tsx | 93 +++++++++++++++++++ packages/plugin-timeline/src/renderer.tsx | 70 +++++++++++++- 2 files changed, 161 insertions(+), 2 deletions(-) diff --git a/packages/plugin-timeline/src/__tests__/timeline-gantt-date-brand-7027.test.tsx b/packages/plugin-timeline/src/__tests__/timeline-gantt-date-brand-7027.test.tsx index 3cb6e00dc3..b7c34183b9 100644 --- a/packages/plugin-timeline/src/__tests__/timeline-gantt-date-brand-7027.test.tsx +++ b/packages/plugin-timeline/src/__tests__/timeline-gantt-date-brand-7027.test.tsx @@ -30,6 +30,17 @@ * about being a Date, each one asserted to reach a DEFINED outcome. That is * what #7026 gave the speller and what the gate did not have. * + * 5. objectui#7036: the gate's repair held, and the SPELLER still was not + * total — `Array.isArray` throws on a revoked `Proxy`. That card did not + * repair it; it ruled the exclusion STATED AND EXERCISED, which is what + * `pin 4` at the foot of this file is. It also measured that the card's + * own headline was wrong: `Array.isArray` is not the last non-total + * operation on the PATH, only inside that function (objectui#7153). + * + * ⚠️ So this file's input set is not a claim of totality either. It is the + * boundary that has actually been exercised, and the sixth card will be the + * one that says where it ends. + * * ## What "a defined outcome" means here — two kinds, and never a third * * ⚠️ Not every row below is a refusal, and forcing them all to be one would @@ -181,6 +192,17 @@ class HijackedGetTime extends Date { } } +/** + * A REVOKED proxy — the one input `spellGanttDateValue` is documented NOT to + * survive (objectui#7036). `Proxy.revocable` is the only way to spell it, and + * nothing in any package `src/` calls it: this is a test-only value. + */ +const revokedProxy = (target: object = {}) => { + const { proxy, revoke } = Proxy.revocable(target, {}); + revoke(); + return proxy as unknown; +}; + /** An object whose `Symbol.toStringTag` is a throwing getter. */ const throwingToStringTag = () => ({ @@ -347,3 +369,74 @@ describe('pin 3 — the ACCEPT SET is unchanged: #6781’s ruling does not move } }); }); + +describe('pin 4 — the ONE documented EXCLUSION, exercised not asserted (objectui#7036)', () => { + /** + * `spellGanttDateValue`'s `Array.isArray` is not total: `IsArray` recurses + * into `[[ProxyTarget]]` and a REVOKED proxy has none, so it throws while + * naming the value. objectui#7036 adjudicated this as STATED-AND-EXERCISED + * rather than repaired, on three grounds recorded in that function's + * docblock: it is unreachable from an authored document (JSON cannot spell + * a proxy), a `catch` here would SUBSTITUTE `an object` for a failure + * rather than read anything the way `isDate`'s catch does, and it would not + * make the PATH total anyway — five reads that FETCH the date throw first + * (objectui#7153). + * + * ## Why a THROW is pinned here, and why not with a bare `toThrow()` + * + * This file exists because four prose totality claims on this path were + * each falsified by the next card's measurement. A docblock sentence saying + * "except a revoked proxy" would be a fifth claim of the same species. The + * rows below make the exclusion a MEASUREMENT instead, and they assert the + * throw's own MESSAGE: a bare `toThrow()` passes for any error from any + * line, so it would stay green if the crash moved to `instanceof`, to + * `Object.prototype.toString`, or upstream into the row walk — which is + * precisely what has happened on this path four times. The message names + * both the operation (`IsArray`) and the cause (`revoked`), so the pin + * fails if the site moves and fails if the exclusion is ever repaired, + * either of which must come with a docblock edit. + */ + + const REVOKED_ISARRAY = /Cannot perform 'IsArray' on a proxy that has been revoked/; + + // Every target shape: the throw is about revocation, not about the target. + const targets: [string, () => object][] = [ + ['{}', () => ({})], + ['[]', () => []], + ['a function', () => function noop() {}], + ['a real Date', () => new Date('2024-03-01')], + ]; + + for (const [label, makeTarget] of targets) { + it(`a revoked Proxy over ${label} throws at \`Array.isArray\`, by design`, () => { + expect(() => + gantt({ items: rowWith({ startDate: '2024-01-01', endDate: revokedProxy(makeTarget()) }) }), + ).toThrow(REVOKED_ISARRAY); + }); + } + + it('a revoked Proxy pinned as `minDate` reaches the same site', () => { + // The pinned limb takes the same speller, so the exclusion is not + // confined to a row item. `value &&` is ToBoolean and does not throw. + expect(() => + gantt({ + items: rowWith({ startDate: '2024-01-01', endDate: '2024-03-01' }), + minDate: revokedProxy(), + }), + ).toThrow(REVOKED_ISARRAY); + }); + + it('CONTROL — a LIVE Proxy is still NAMED `an object`, so the rows above are about REVOCATION', () => { + // Without this the four rows above would also pass if the whole gantt + // branch had broken. `typeof` does not separate a revoked proxy out + // either: it answers `'object'` for one, exactly as it does here. + const { container } = gantt({ + items: rowWith({ startDate: '2024-01-01', endDate: new Proxy({}, {}) }), + }); + + const text = diagnosticOf(container) ?? ''; + expect(text).toContain(PATH); + expect(text).toContain('is an object'); + expect(barCountOf(container)).toBe(0); + }); +}); diff --git a/packages/plugin-timeline/src/renderer.tsx b/packages/plugin-timeline/src/renderer.tsx index 648f778427..5acd179b30 100644 --- a/packages/plugin-timeline/src/renderer.tsx +++ b/packages/plugin-timeline/src/renderer.tsx @@ -427,7 +427,7 @@ const isDate = (value: unknown): value is Date => { * worse than naming a category: it costs the author a search for a fault that * is not there. * - * ## The branches, and why each is total + * ## The branches, and how far each one's totality reaches * * - `string` -> quoted (#6759's pin; empty and space-padded stay visible). * - `undefined` / `null` -> themselves (#6759 / #6770's pins; how an author @@ -455,7 +455,10 @@ const isDate = (value: unknown): value is Date => { * `Array.isArray` and `typeof`, which read no author-controlled property. * Deliberately NOT `Object.prototype.toString.call`: that consults * `Symbol.toStringTag`, which can be a throwing getter (measured), so the - * more informative spelling is the non-total one. + * more informative spelling is the non-total one. `Array.isArray` reads no + * property and is STILL not total — on a revoked `Proxy` it throws. That is + * the one exclusion this docblock claims, and it is stated and exercised + * rather than repaired: see the objectui#7036 note below. * * All eight `typeof` results are covered and no branch falls through to author * code. The single reflective operation it performs is the Date test, which is @@ -471,6 +474,69 @@ const isDate = (value: unknown): value is Date => { * operation adds no throw site because it HAS none — not because the gate * absorbed it first. * + * ⚠️ objectui#7036 — READ THE TWO PARAGRAPHS ABOVE AS A SEQUENCE, NOT AS A + * CONCLUSION. Each was written as the settled answer and the next card's + * measurement moved it (#6759 -> #6905 -> #6907 -> #7027). This is the fifth + * entry and it is deliberately NOT a fifth claim of totality. The sentence it + * falsifies is the one directly above the #7027 note: this function DOES add + * a throw site the accept gate does not have, and it is `Array.isArray`. + * + * THE EXCLUSION, measured in-render on dd35800af through `TimelineRenderer` + * itself — not a replica of these branch bodies: + * + * endDate: + * -> THREW TypeError: Cannot perform 'IsArray' on a proxy that has + * been revoked (here, at `Array.isArray`) + * + * `IsArray` recurses into `[[ProxyTarget]]`, which a revoked proxy does not + * have, so this throws on an INTERNAL condition while still reading no + * author-controlled property. All four target shapes throw (`{}`, `[]`, a + * function, a real `Date`), and so does a revoked proxy pinned as + * `schema.minDate` / `maxDate`. `typeof` does not separate it out — it + * answers `'object'` (or `'function'`) without throwing. + * + * WHY IT IS LEFT, and none of the three reasons is cost: + * + * 1. It is not reachable from an authored document — ObjectUI metadata is + * JSON and JSON cannot spell a proxy. Re-swept on dd35800af: zero + * `Proxy.revocable` in the repo, zero `Object.setPrototypeOf` calls in + * any package `src/`, each read beside a live control on the same + * instrument + * (52 `Proxy` mentions, 19 `Object.assign` calls) so the zeros are + * readings and not a broken query. The `__proto__` hits are denylists. + * 2. A `catch` here would be SUBSTITUTION, not a read — the opposite of + * `isDate`'s. `isDate` catches because the language exposes the + * `[[DateValue]]` bit ONLY by throwing, so its catch IS the read. A + * revoked proxy has no array-ness to read, so catching would discard a + * failure and substitute `an object`: consumer-side tolerance, which is + * what #6750 and #6759 both refused. A second `catch` beside `isDate`'s + * would erase the distinction this file is built on. + * 3. It would buy no invariant, because of the paragraph below. + * + * ⛔ `Array.isArray` IS NOT THE LAST NON-TOTAL OPERATION ON THE GANTT DATE + * PATH, and this function cannot make the path total. Measured in the same + * run: five further crash sites, every one of them reached BEFORE this + * function is entered, in the property reads that FETCH the date out of the + * authored document (`findUnusableGanttDate`'s `items[i]?.items` and + * `rowItems[j]?.[key]`) — + * + * items[0].items[0] with a throwing `endDate` getter -> THREW + * items[0].items[0] is a revoked Proxy -> THREW + * items[0] is a revoked Proxy -> THREW + * items[0].items is a revoked Proxy -> THREW + * items[0] with a throwing `items` getter -> THREW + * + * They are the same reachability class (JSON spells neither a getter nor a + * proxy) and they are enumerated in objectui#7153 rather than repaired here — + * they are a different function's surface. The true and much narrower + * sentence is that `Array.isArray` is the last non-total operation INSIDE + * THIS FUNCTION. + * + * ⚠️ Whatever totality this docblock claims is bounded by an EXERCISED input + * set — the rows in `__tests__/timeline-gantt-date-brand-7027.test.tsx`, + * which now include the revoked proxy — and by nothing else. On this path + * prose has been a hypothesis four times running. + * * ## What this deliberately does NOT do * * It does not change WHICH values are refused — that is #6781's ruling and it From e615be4904a4ff8a0a4a3b2c83b64f8986827f1b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 06:46:44 +0000 Subject: [PATCH 2/2] chore: declare the plugin-timeline docblock change as releasing nothing Empty frontmatter: the change is a docblock correction plus exercised rows in an existing pinned adversarial set. No published behaviour moves. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM --- .changeset/spellgantt-revoked-proxy-exclusion.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/spellgantt-revoked-proxy-exclusion.md diff --git a/.changeset/spellgantt-revoked-proxy-exclusion.md b/.changeset/spellgantt-revoked-proxy-exclusion.md new file mode 100644 index 0000000000..164a41f979 --- /dev/null +++ b/.changeset/spellgantt-revoked-proxy-exclusion.md @@ -0,0 +1,11 @@ +--- +--- + +Docs and tests only for `@object-ui/plugin-timeline`. `spellGanttDateValue`'s +docblock claimed every branch was total; measured in-render, `Array.isArray` +throws on a revoked `Proxy`, so the claim is now scoped to the input set that +is actually exercised and the exclusion is pinned as rows in the existing +adversarial set. The same measurement falsified the wider claim that +`Array.isArray` is the last non-total operation on the gantt date path — five +reads upstream of the helper throw first, recorded in objectui#7153. No +published behaviour changes.