Skip to content

feat(plugin-timeline)!: retire the timeScale alias — scale is the only axis spelling - #6656

Merged
os-elon merged 7 commits into
mainfrom
claude/issue-6355-retire-timescale-alias
Aug 28, 2026
Merged

feat(plugin-timeline)!: retire the timeScale alias — scale is the only axis spelling#6656
os-elon merged 7 commits into
mainfrom
claude/issue-6355-retire-timescale-alias

Conversation

@os-sales

@os-salesos-sales commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Fixes#6355

Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49

Maintainer ruling (2026-08-27), verbatim: 「6335 和以后这种类似的,都立即退休。项目在创业阶段,用户也很少,短期不考虑渐进。」 — Option A, immediate retirement, no phased window. The staged window was declined and the silent-reversion cost for externally-stored old documents is accepted by the ruling.

Draft on purpose — governed, human merge. Clause-② per this repo's convention: the diff changes a published type. Not enqueued, no auto-merge.

The failure this change is shaped around

Dropping resolveTimelineScale's ?? schema.timeScale fallback is the whole behavioural change, and on its own it is the wrong shape. A stored document spelling only timeScale would stop being read and fall through to the renderer's historical month default: the chart changes bucket and nothing errors. That is the silent axis breakage #2942 closed, running in the other direction.

So the deliverable is not "scale works". It is that an old-spelling document is refused, loudly, at the authoring boundary.

Why a tombstone rather than deleting the key

BaseSchema is .passthrough() on the Zod side and carries [key: string]: any on the TS side, so an undeclared key is accepted unvalidated by both halves. Deleting timeScale outright would have handed the retired spelling exactly the silent no-op this card exists to prevent — it would parse green, type-check green, and do nothing.

Keeping it declared as ?: never / z.never().optional() is what makes the removal audible. Both halves or neither, per the #5474 lockstep convention: either half alone leaves the other surface silently accepting the retired spelling. absent stays valid on both, so a document that never wrote the alias is untouched.

The pin, shown red before the fix

The pin was committed before its fix (ab88df74), on the pre-fix tree, and run there.

Runtime legpackages/types/src/__tests__/timeline-timescale-retired.test.ts, on the pre-fix tree:

 Test Files 1 failed (1)
Tests 1 failed | 4 passed (5)
AssertionError: a timeScale-only document was ACCEPTED — it will silently revert to the month default: expected true to be false

The four counter-probes passed, so the file failed on the refusal assertion specifically, not because it was broken. After the fix: Tests 5 passed (5).

Compile-time leg — same tree, tsc -p tsconfig.test.json exit 2:

src/__tests__/timeline-timescale-retired.test.ts(116,5): error TS2578: Unused '@ts-expect-error' directive.

The pin asserts the rejection envelope, not merely success: falsecode: invalid_type, expected: 'never', on the timeScale path — so it cannot be satisfied by a document failing for an unrelated reason.

Ablation, with the mutation proven on disk and the restore proven by observation

Reverting the Zod tombstone on the committed tree (data-display.zod.ts):

leginjectedremovedblobresult
mutated10cc5fb86d5f59f688vitest exit 1 — 1 failed | 4 passed
restored5f59f688cc5fb86d (= HEAD blob), git diff HEAD cleanvitest exit 0 — 5 passed

Reverting the TS tombstone (data-display.ts, neverTimelineScale) reddens both compile-time directives (TS2578 ×2, at the member-type leg and the document leg); restore returns blob 5365e337 and git diff HEAD clean, tsc exit 0.

That restore leg earned its keep: it caught a misplaced @ts-expect-error in my own pin — sitting above the const rather than above the property excess-property checking actually reports on, so it suppressed nothing while the real TS2322 stood. Fixed in 82f289e1.

No dist rebuild leg is claimed or needed: the pins import ../zod/data-display.zod.js relatively, and @object-ui/types/zod is aliased to source in vitest.config.mts — there is no exports/dist hop on these pairs, so no stale-dist false green is possible.

Scope, as the ruling enumerates it

  • TombstonetimeScale?: never on TimelineSchemaplus the Zod mirror twin (z.never().optional()).
  • Designer input dropped. The scale control already offers all six buckets.
  • Fallback read dropped.resolveTimelineScale's parameter narrows to { scale?: unknown }.
  • Docs row dropped, plus a retirement callout. packages/components/.../TIMELINE.md's Gantt table now documents scale with the full six-value vocabulary it has accepted since Tier 2 (#2901): spec values that validate and then render nothing #2942 — its row still claimed three.
  • In-repo authors migrated in this PR: the schema-catalog gantt-style-timeline.json fixture and the registration's own examples.gantt block.

A third in-repo author the card did not list

ObjectTimelinecomposes a schema for TimelineRenderer and wrote the resolved axis under the alias. It is invisible to a grep of authored metadata — no author ever sees that object — and it would have sent every object-bound gantt back to the month default the moment the fallback read went, silently, since a composed schema never crosses the authoring boundary the tombstone guards. Migrating it is not a drive-by: dropping the read while leaving this write is simply a broken implementation of the same scope item.

Writing scale after the spread also restores the precedence the surrounding code already intends. resolvedScale is timelineConfig?.scale ?? schema.scale, but the spread carried a flat schema.scale through untouched, and the resolver's scale ?? timeScale ordering then preferred it — so timelineConfig.scale lost to a flat scale, inverted. The ablation shows this directly: with the alias restored, the pin fails expected 'day' to be 'year'.

Verification

Union re-run on the final commit 82f289e1 (git rev-parse --short HEAD from that run), from the repo root:

checkverdict, as printed by the gate
vitest run (touched suites)Test Files 31 passed (31) · Tests 226 passed (226)
turbo run type-check (types, plugin-timeline, components)Tasks: 13 successful, 13 total
type-check:scriptsexit 0
turbo run lintTasks: 4 successful, 4 total
lint:coverage✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
check:control-bytes✅ OK (scanned 5496 tracked text file(s); skipped 85 binary).
check:vi-mock-specifiers✅ OK (… 729 relative specifier(s) resolved …)
check:doc-snippetsEvery covered documentation snippet compiles against the built types. (267/267 judged, 0 failed)
check:readme-exports✅ OK (… 378 real, 0 wrong-path, 0 fabricated; 0 unbuilt)
changeset:check · check-changeset-presence✅ No changeset declares a major bump. · ✅ 8 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)
check:doc-types · check:doc-fences · docs:check-links · check:designer-field-key-parityexit 0

check:doc-snippets and check:readme-exports first reported PRECONDITION NOT MET / population collapsed — both say so in their own words and name the build they need. Those runs were recorded as not measured, never as red; both were re-run after building what they ask for, and the verdicts above are from the real runs.

Version bump

minor, not major, per AGENTS.md §版本号策略: objectui's major tracks the @objectstack major and all 39 publishable packages share one fixed group, so a major would drag the whole group off that cadence. The break is spelled out in the changeset body instead. check-changeset-no-major.mjs enforces this; nothing was re-scored to get it green.

Out of scope — filed separately

Issue #6655 records an object-bound timeline with variant: 'gantt' throwing RangeError: Invalid time value: calculateDateRange reads the gantt row shape while ObjectTimeline composes feed items. Pre-existing — both halves are byte-identical to the merge base, verified with git diff — and out of scope here, so #6655 remains open. This PR's composition pin renders variant: 'vertical' deliberately so it does not couple to that defect.

…n red first
The pin precedes its fix on purpose: on this tree `timeScale` is still
`TimelineScaleSchema.optional()`, so the timeScale-only document parses
green (vitest: 1 failed | 4 passed) and the compile-time half reports
TS2578 on the unused directive. Both legs are the red the retirement
turns green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
… only axis spelling
Maintainer ruling objectui#6355 (2026-08-27): immediate retirement, no
phased window, while the project is at startup stage.
`timeScale` was this renderer's pre-spec spelling of the gantt axis
bucket; `scale` is canonical (@objectstack/spec ui/TimelineConfig.json,
and the key the renderer already preferred).
The alias is tombstoned on BOTH halves in lockstep — `?: never` on
`TimelineSchema` and `z.never().optional()` on the Zod twin — rather
than deleted. BaseSchema is .passthrough() with a `[key: string]: any`
index signature, so deleting the key would let the retired spelling
parse and type-check green while the renderer no longer read it: the
gantt axis would silently fall back to the month default and nothing
would error. The tombstone is what makes the removal audible.
Also: resolveTimelineScale drops the `?? schema.timeScale` fallback;
the designer drops its deprecated timeScale input; ObjectTimeline now
composes `scale` (it wrote the alias, which would have silently
reverted every object-bound gantt); both in-repo authors and the docs
are migrated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
…nonical `scale`
Covers the schema-catalog fixture, the registration's examples.gantt, the
designer inputs, and ObjectTimeline's composed schema — the last of which
no grep of authored metadata would find, and the one that would have
silently reverted every object-bound gantt to the month default.
Each assertion goes through the real resolveTimelineScale rather than
reading the key, so a rename that left the value unreachable cannot pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
…sts__
packages/plugin-timeline's test tsconfig carries no node types, so the
fs-reading assertions belonged where the repo's other catalog test lives.
The registry and designer assertions stay in the package.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
…lidator meet
scripts/__tests__ cannot resolve @object-ui/types/zod (scripts/ is not a
workspace package, so there is no dependency edge); plugin-timeline's test
tsconfig has no node types. packages/types has both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
The existing leg reads the member's type; this one writes a
TimelineSchema document, which is what an author or a metadata-generating
AI actually produces, and is the leg that proves the tombstone survives
BaseSchema's index signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
Excess-property checking reports at the member, so the directive one line
above the const suppressed nothing: it went unused (TS2578) while the real
TS2322 stood. Caught by the ablation's restore leg.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3237.2 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-qfrA3YOZ.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)507.87KB115.07KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.41KB34.47KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.62KB54.56KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.44KB7.59KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)9.30KB3.22KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-elonos-elon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contract review: PASS — director seat, session session_016SG9S6V15MqeAgkehDcTwk, at the contract-review tier (machine-verified this shift). Reviewed on the maintainer's direct summons (live PM chat, 2026-08-28, verbatim: 「#6656 帮我审核 合并」), which is also the landing authorization.

What was verified, against head 82f289e1:

  1. Ruling fidelity — executes ui#6355's full remaining scope under the 2026-08-27 immediate-retirement ruling: fallback read dropped (resolveTimelineScale narrows to { scale?: unknown }), designer input dropped, docs row replaced with a retirement callout, in-repo authors migrated (catalog fixture + examples.gantt), tombstone per the #5474 lockstep convention.
  2. The failure the change is shaped around is actually closed?: never (TS) + z.never().optional() (Zod) on both halves defeats [key: string]: any / .passthrough() silent acceptance; an old-spelling document is refused loudly at the authoring boundary instead of silently re-bucketing to month. The envelope-level pin (invalid_type / expected never / path timeScale) cannot green on an unrelated failure.
  3. The unlisted third author is caughtObjectTimeline's composed-schema write moved to scale, without which every object-bound gantt would have reverted silently; the precedence note (timelineConfig.scale now actually beats a flat schema.scale) is correct by JS spread ordering and pinned.
  4. Independent residue sweep — my own git grep timeScale over the PR tree (FETCH_HEAD = 82f289e1), non-test sources: every remaining occurrence is the tombstone itself or retirement prose; zero live reads, zero live writes.
  5. Evidence discipline — pin committed red-first with both legs shown failing on the pre-fix tree; ablation/restore with blob hashes on both tombstone halves; CI fully green on this head (4 test shards, Type Check, Lint, changeset policy).
  6. Versioningminor with the break spelled out, per AGENTS.md fixed-group policy; Changeset Bump Policy green. #6655 correctly fenced as pre-existing (verified byte-identical halves) and stays open.

Proceeding per 清标即落地: ready → merge, same action.


Generated by Claude Code

@os-elon
os-elon marked this pull request as ready for review August 28, 2026 13:16
@os-elon
os-elon added this pull request to the merge queueAug 28, 2026
Merged via the queue into main with commit 9d86e1dAug 28, 2026
30 checks passed
@os-elon
os-elon deleted the claude/issue-6355-retire-timescale-alias branch August 28, 2026 13:29
Sign up for freeto 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.

Retire the timeScale alias on the timeline node, now that scale is canonical on all three surfaces

3 participants

@os-sales@os-elon@claude