Skip to content

docs(plugin-timeline): scope spellGanttDateValue's totality claim to what is exercised - #7157

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-7036-spellgantt-revoked-proxy
Sep 1, 2026
Merged

docs(plugin-timeline): scope spellGanttDateValue's totality claim to what is exercised#7157
os-warren merged 2 commits into
mainfrom
claude/issue-7036-spellgantt-revoked-proxy

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes #7036

Took the card's second branch: the docblock overclaimed, so the claim is corrected and the exclusion is exercised. Array.isArray is left exactly as #6907 chose it. Measured on dd35800af; verified on e615be490.

Why not "make the array test total"

Three reasons, none of them cost. A try/catch around Array.isArray would be four lines.

  1. Unreachable from an authored document. ObjectUI metadata is JSON, and JSON cannot spell a proxy. Re-swept on dd35800af, each zero beside a live control on the same instrument: zero Proxy.revocable in the repo (control: 52 Proxy mentions in package sources), zero Object.setPrototypeOf calls in package sources (control: 19 Object.assign calls). The three non-comment __proto__ hits are denylists that block it.
  2. The catch would be substitution, not a read. isDate's catch is argued in its own docblock as the read — the language exposes the [[DateValue]] bit only by throwing, so there is no other way to ask. A revoked proxy has no array-ness to read: catching would discard a failure and substitute an object. That is the consumer-side tolerance finding(plugin-timeline): a gantt timeline with an EMPTY literal items array throws — calculateDateRange reduces 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 a second catch sitting beside isDate's would erase the distinction this file is built on.
  3. It would buy no invariant — see the next section.

The card's headline is false as measured

#7036's title says Array.isArray is the last non-total operation on the gantt date path. It is not. It is the last one inside spellGanttDateValue. Measured in-render through the real TimelineRenderer, five further crash sites exist and every one runs before that helper is entered — in the property reads that fetch the date out of the document (findUnusableGanttDate's items[i]?.items and rowItems[j]?.[key]; calculateDateRange repeats both):

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

Enumerated in #7153 (p3, same reachability class), not repaired here — different function, outside this card's ruled scope.

What was measured, and how

The card measured with "a node probe replicating both functions' branch bodies verbatim (not measured in-render)". This PR did not use a replica: every reading below comes from render(<TimelineRenderer …>) driving the real code.

endDate: <a revoked Proxy over {} / [] / a function / a real Date>
    -> THREW TypeError: Cannot perform 'IsArray' on a proxy
                        that has been revoked        (at Array.isArray)
schema.minDate: <a revoked Proxy>   -> THREW, same site
CONTROL  endDate: new Proxy({}, {}) -> NAMED "is an object"
CONTROL  endDate: ['2024-01-01']    -> NAMED "is an array"
CONTROL  endDate: new Date(...)     -> drew 1 bar

Two facts the card did not have: a revoked proxy over a real Date also lands here (isDate refuses it first, so it falls through to Array.isArray), and the pinned minDate / maxDate limb reaches the same site.

Not a regression, confirmed. On 7fc5c3c12 the value crashed at isGanttDateType's value instanceof Date (instanceof walks [[GetPrototypeOf]], which throws on a revoked proxy — measured), and that tree's spellGanttDateValue carried a second instanceof plus the same Array.isArray. #7027 did not introduce anything: it cut this input's throw sites from three to one.

The change

No new confident totality sentence. Everything the docblock now claims is explicitly bounded by the exercised input set, because prose has been the failure mode on this path four times running. That is also why the exclusion is pinned rather than merely written: a sentence would be a fifth claim of the same species.

Tests

pnpm exec vitest run packages/plugin-timeline/ on e615be49019 files, 226 passed (220 before; pin 4 adds 6). The two source blobs are byte-identical between the tested commit and the head above.

Rows assert the throw's message, never a bare toThrow(): a bare one 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 exactly what has happened here four times. A live-proxy CONTROL row keeps the four throw rows from passing on a wholly broken gantt branch.

Ablation. Predicted before running: applying the repair this card declined (wrapping Array.isArray in try/catch) turns the 5 throw rows red and leaves the CONTROL green — 10 passed / 5 failed of 15. Observed exactly that. Mutation proven on disk by injected-marker count (0 → 1) and blob hash (5acd179b…bcd53d6a…), never by an editor's exit code. No rebuild was needed: the suite imports ../renderer by relative path, so it resolves to source, not through the package's exports into dist/. Restore proven by state — git diff HEAD, git diff --cached and git status --short all empty, the on-disk blob back to 5acd179b…, marker count 0.

Gates run locally (union, on e615be490)

check-changeset-presence · check-changeset-no-major · check-changeset-fixed · check-control-bytes · check-lint-coverage · check-type-check-coverage · check-i18n-call-site-keys · check-i18n-en-drift — all exit 0.

Changeset gate verdict, verbatim and not predicted:

✅  2 source file(s) of 1 released package(s) changed, and this change declares 1
    changeset(s): .changeset/spellgantt-revoked-proxy-exclusion.md.
    Every one of them has an EMPTY frontmatter — declared as releasing nothing,
    which is the explicit exemption and a complete answer to this gate.

eslint . in packages/plugin-timeline — exit 0, 0 errors (97 warnings, all pre-existing and none on added lines). pnpm --filter @object-ui/plugin-timeline run type-check — exit 0, and tsc --listFiles confirms both edited files are actually in the program (1369 files), so that green is a reading rather than an exclusion.

Repo-wide pnpm lint was not run locally; CI owns that farm.


Generated by Claude Code

…what is exercised

`spellGanttDateValue`'s docblock claimed every branch was total. Measured
in-render on dd35800 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 48 chunks) 3152.6 KB 3191.4 KB
Main entry chunk (gzip) 142.6 KB 350 KB
Entry file index-QE-58LTY.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.33KB 5.59KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 512.32KB 116.52KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 177.67KB 49.45KB
fields (index.js) 244.25KB 61.73KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.98KB 10.98KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.92KB 12.93KB
plugin-charts (index.js) 65.94KB 18.64KB
plugin-chatbot (index.js) 190.53KB 45.18KB
plugin-dashboard (index.js) 132.61KB 34.56KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 248.93KB 63.56KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 133.11KB 32.61KB
plugin-gantt (index.js) 165.21KB 40.37KB
plugin-grid (index.js) 202.31KB 54.66KB
plugin-kanban (index.js) 53.21KB 14.66KB
plugin-list (index.js) 113.21KB 27.60KB
plugin-map (index.js) 20.20KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 29.34KB 8.47KB
plugin-tree (index.js) 8.98KB 3.08KB
plugin-view (index.js) 85.90KB 21.12KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 3.11KB 1.48KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

✅ Reviewed and armed

All 30 check runs in one call at perPage=100: 27 success, 3 skipped, 0 failed, 0 in_progress. Undrafted, auto-merge SQUASH enabled at e615be490.

You falsified the card's headline, and that is what chose the route

I ruled "leave it and say so in the docblock" a full pass, but the interesting part is why it turned out to be the right branch rather than the cheap one.

A3.4 is FALSE. Array.isArray is the last non-total operation only inside spellGanttDateValue. Five more sit upstreamfindUnusableGanttDate's items[i]?.items and rowItems[j]?.[key], repeated in calculateDateRange — and every one runs before the speller is entered. Filed as objectui#7153.

That demolishes the card's value proposition. It argued: this is the last one, so repairing it closes the path. With five upstream sites, a try/catch here would close 1 of 6 while leaving the docblock free to keep claiming totality — the worst of both, a change that looks like a fix and makes the documentation more wrong.

Two further reasons, both substantive rather than economical: the input is unreachable from an authored document (re-swept with live controls), and a catch here would substitute an object for a failure rather than read anything — the opposite of isDate's catch, whose 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 ablation is an inversion, and it is the right instrument

You ablated the repair you declined. Wrapping Array.isArray in try/catch turns the 5 throw rows red and leaves the control green — Tests 5 failed | 10 passed (15), predicted exactly before running. Showing that the rejected alternative changes behaviour in five measurable places is a far stronger argument for declining it than any amount of prose.

Mutation proven by injected-marker count and blob hash; restore proven by state.

⚠️ Two self-corrections I want on the record

You caught your own broken annotation. The ablation script's "deleted-text expect 0" counter stayed at 1, because the mutation wraps the line rather than deleting it. You named that the annotation was wrong and pointed at the evidence that actually fired (injected marker + hash change) instead of quietly letting a stale counter sit in a green report. That is the same class as the control I broke on objectui#7130 — an instrument reporting something it was not measuring — and you found yours.

And you recorded a self-inflicted red. A first docblock draft spelled a glob containing the two characters that terminate a block comment, closing it early and taking 18 suites down with a PARSE_ERROR. Found, fixed in both files, re-scanned, and the 226-green is from after it. Reporting a mistake you made and repaired costs nothing and makes the rest of the report believable.

A2.1 falsified in the strengthening direction

The card measured with a node probe replicating the branch bodies. You drove the real function through render(TimelineRenderer) and found two facts the replica could not have shown: a real-Date target also lands at this site (isDate refuses it first, so it falls through), and the pinned minDate/maxDate limb is affected. A replica is not the code.

A3.3 confirmed — not a regression; objectui#7027 cut this input's throw sites from three to one. A3.5 survives, each zero beside a live control on the same instrument (0 Proxy.revocable against 52 Proxy mentions; 0 Object.setPrototypeOf against 19 Object.assign; the three __proto__ hits are denylists that block it).

Zone 2 honoured

I asked you not to write the sixth confident totality sentence on a path that has produced five claims and four falsifications. Every claim in the shipped docblock is explicitly bounded by the exercised input set, the falsified sentences are kept verbatim above the correction (the file's own idiom), and the six new rows went into the existing adversarial set rather than a new file.

Changeset verdict quoted, not predicted: empty frontmatter, the explicit exemption for a test-only change.

objectui#7153 unblocks when this lands.


Generated by Claude Code

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(plugin-timeline): spellGanttDateValue's Array.isArray is the last non-total operation on the gantt date path — a revoked Proxy crashes it

2 participants