fix(types): align TimelineSchema with the keys the timeline renderer reads - #6352
Merged
Merged
Conversation
…reads TimelineSchema declared events (required), orientation and position and nothing else, while TimelineRenderer -- annotated schema: TimelineSchema -- reads variant, items, dateFormat, onItemClick, minDate, maxDate, rowLabel, scale and timeScale off that node and none of the three declared keys. BaseSchema's index signature typed every undeclared key as any, so the annotation constrained nothing and tsc could not see the divergence. Declares the eight authorable keys on the interface and its zod mirror together; scale becomes the canonical axis key (the spec spelling the renderer already reads first) and the designer now offers all six buckets instead of three; timeScale stays as a deprecated alias. events goes required -> optional, which is what the docs page's own TypeScript example needed in order to compile. events, orientation and position stay declared and stay read by nothing: removing them is a breaking narrowing routed through ADR-0049, not done here. Part of #6170
This was referenced Aug 25, 2026
Contributor
✅ Console Performance Budget
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
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #6170.
Gates measured at
e3c074869, which is this branch's head and the tree every number below was read from.My dispatch carried a STOP-AND-REPORT clause: if the fix would widen or narrow the accept set of a published type, stop and report the fork instead of picking a side — naming the deletion of
events/orientation/positionand the absorption of object-bound keys as the two examples.The card already carries a maintainer ruling (2026-08-25, verbatim 「同意」) that decides exactly that fork: "
TimelineSchemaaligns to what authors write andTimelineRendererreads",scalecanonical,events/orientation/positionto the ADR-0049 route, docs re-verified,Clause-②: yes — published type surface changes, contract-review tier at dispatch. The dispatch's assumption table and hypothesis never reference that ruling, which suggests it was composed from the pre-rulingneeds-user-decisionframing.I did not pick a side on the part the ruling did not settle. This PR lands only the non-breaking subset:
events/orientation/position— is NOT here. It is written up below as a fork for the maintainer.That is why this is
Part of #6170and not a closing reference: merging it leaves the ADR-0049 removal and thetimeScalealias retirement live on the card.The census — mine, exact, both directions
Measured on
origin/main@79ebf30d1, overpackages/plugin-timeline/srcexcluding test files.Declared by
TimelineSchemabeyondtype(packages/types/src/data-display.ts:1204):events(required),orientation,position— 3.Read, split by the component that reads it:
TimelineRenderer(renderer.tsx:250) — presentationalvariant,items,dateFormat,onItemClick; dottedminDate,maxDate,rowLabel,scale,timeScaleObjectTimeline(ObjectTimeline.tsx) — object-boundobjectName,timeline,filter,sort,limit,mapping,titleField,dateField,startDateField,endDateField,descriptionField,groupByField,colorField,scale,bind,items,classNameindex.tsx(registration + dataSource mapping)objectName,limitDistinct dotted
schema.*reads across the package: 21. Adding the three destructured-only keys (variant,dateFormat,onItemClick) gives 24 distinct read keys.The difference, both ways:
events,orientation,position. Zero read points anywhere in the repository, not just inplugin-timeline. (TimelineEvent, the element typeeventsnames, is dead with it — itsdatekey is not even thetimekey the renderer reads.)classNamewas declared, and byBaseSchema. AgainstTimelineRendereralone, 9 undeclared reads.M1–M5 verdicts
TimelineSchema(data-display.ts:1204) declares, beyondtype, exactlyevents/orientation/positioneventsrequiredschema.*reads, none of them those threerenderer.tsx:250typesschemaasTimelineSchemaObjectTimeline.tsx:52widens it by handListViewTimelineConfig= specTimelineConfig& { dateField?; [key: string]: any }objectql.ts:116; untouched here (objectui#5155 / objectui#6269)All five hold.
The hypothesis: diagnosis confirmed, proposed remedy refuted
Two timelines: CONFIRMED.
TimelineRenderer.@objectstack/spec'sTimelineConfigviaListViewTimelineConfig, read byObjectTimeline.The composition seam is real and correct:
ObjectTimelineresolves field names against fetched records, buildseffectiveSchema, and calls<TimelineRenderer schema={effectiveSchema} />(ObjectTimeline.tsx:443)."The renderer is typed against the wrong one": REFUTED.
renderer.tsxreads only presentational keys and not one object-bound key.TimelineSchemais meant to be the presentational type — it is the right one of the two. Its problem is that its member list is a third, unimplemented vocabulary (events/orientation/position— an MUI/Ant-style Timeline API that was never built) which is neither presentational-as-implemented nor object-bound.Consequence: the "mechanical re-point" is not available. Re-pointing requires an existing type that declares
variant/items/dateFormat/rowLabel/minDate/maxDate. No such type exists — I searched everyTimelineSchemareference repo-wide and every specuiexport. So the fix necessarily changesTimelineSchema's member list, which is why the STOP clause was reached and why the maintainer ruling is load-bearing.Supporting evidence gathered:
eventsshape? One artifact:packages/types/examples/data-display-examples.jsonauthors{ type: 'timeline', events, orientation, position }. It is imported by nothing, type-checked by nothing (tsconfig.examples.jsonincludes onlyexamples/**/*.ts), and would render an empty rail. Left untouched on purpose — see findings.'timeline'is registered twice:renderer.tsx:498→TimelineRenderer(namespaceplugin-timeline), andindex.tsx:366→ObjectTimelineRenderer(namespaceview).index.tsx:300(export * from './renderer') evaluates first, so the later call wins the bare-name fallback andtype: 'timeline'resolves toObjectTimelineRenderer, which delegates inward toTimelineRenderer. Filed as a finding — the collision guard inRegistry.registerwarns on exactly this.content/docs/plugins/plugin-timeline.mdxdocuments only the presentational vocabulary, and agrees with the renderer and the registryinputson all 8 rows.Producer-boundary check (binding per the ruling): spec does NOT need to move
@objectstack/spec@17.2.0ui/TimelineConfig.jsondeclaresstartDateField,endDateField,titleField,groupByField,colorField,scale, withadditionalProperties: falseandrequired: [startDateField, titleField, scale].Every key is a field name — object-bound. The presentational vocabulary is entirely absent from it and, under
additionalProperties: false, could never live there. The two shapes are disjoint by construction.The only overlap is
scale, and it already agrees: the spec's six values are byte-identical to the renderer'sTIMELINE_SCALES, pinned byplugin-timeline/src/__tests__/timeline-scale-spec-parity.test.ts. DeclaringscaleonTimelineSchemawith those same six adopts the spec's spelling rather than competing with it.⇒ No spec change needed; no fork to the
domain:speclane.packages/specwas not touched.T1 — the mistyped-before / correct-after demonstration
A types-only change proves nothing by compiling, so both directions were measured with the same probe file against the same
tscinvocation.Probe A — the docs page's own TypeScript example (
plugin-timeline.mdx, "TypeScript Support", verbatim):The published page taught an authoring form its own published type refused.
Probe B — values the renderer never implemented (
variant: 'diagonal',dateFormat: 'medieval',scale: 'fortnight'):Both probes are now permanent in
packages/types/src/__tests__/timeline-declared-keys.test.ts, whichtsconfig.test.jsoncompiles — verified bytsc --listFiles | grep -c timeline-declared-keys.test.ts→ 1, so the@ts-expect-errorpins are real enforcement and not decoration.Ablation — the pin has teeth. Predicted direction: removing one declaration reddens
tscwith TS2578 naming that key (the member falls back toanyand the wrong-typed assignment starts succeeding). Renamingvariant?:→variantABLATED?:indata-display.ts, confirmed on disk in both directions (deleted-text count1 → 0, injected-text count1, blob hash ≠HEADblob):Predicted instrument fired; a second independent instrument (the mirror-parity ratchet) fired too. No build leg was needed —
tsconfig.test.jsonresolves../data-display.jsto source, notdist/. Restore leg proven, not assumed:git checkout HEAD -- <abs path>, then blob hash byte-identical to theHEADblob (44a3416d…), residual marker count0,git diff HEADempty.What changed
packages/types/src/data-display.ts—TimelineSchemadeclares the eight authorable keysTimelineRendererreads:variant,items,dateFormat,scale,timeScale,rowLabel,minDate,maxDate. New exportedTimelineScale(the six spec buckets).eventsgoes required → optional.events/orientation/positionkeep their declarations, gain@deprecated+ a zero-read-points warning + the ADR-0049 route note.onItemClick— the ninth read key — is deliberately left undeclared: it is a runtime slotObjectTimelineinstalls, and this package keeps callback-shaped keys off the authored surface (RuntimeOnlyDeclared,zod-mirror-parity.test.ts). Declaring it would have required a new entry in that shared ledger for no authoring gain.itemsis declaredany[]rather than a narrowed union: the renderer accepts two element shapes discriminated byvariantand reads both dynamically. Pinning a precise element union is a separate question (filed below), and narrowing it here would have been the one genuinely risky move in the diff.packages/types/src/zod/data-display.zod.ts— the mirror follows key-for-key. Non-negotiable:zod-mirror-parity.test.tsregisters this pair with noKnownDrift/UnmirroredDeclaredentry, so a declared-but-unmirrored key reddens the ratchet.TimelineScaleSchemais module-local, not exported — every exported const in that directory must be registered inMIRRORSorEXCLUSIONS, and a shared inline enum is not a mirror of any declaration.packages/plugin-timeline/src/renderer.tsx— the designer now offersscalewith all six buckets, sourced from the existingTIMELINE_SCALESconst.timeScalestays for round-tripping stored JSON, markedadvancedwith a DEPRECATEDdescription(ComponentInputhas nodeprecatedslot and no index signature, sodescriptionis this package's stated ceiling). Before this the designer offered onlytimeScalewith three of the six —hour/quarter/yearhave rendered correctly since objectui#2942 but were offered by neither the designer nor the type, so they were authorable and undiscoverable from both surfaces.content/docs/plugins/plugin-timeline.mdx— property table gainsscale, markstimeScaledeprecated; the Schema API block and the "Time Scales" feature line corrected from three buckets to six; a callout thatevents/orientation/positionare read by nothing and render an empty rail.packages/types/src/__tests__/timeline-declared-keys.test.ts— new, modelled ongantt-declared-keys.test.ts(objectui#5903), the house form for this defect class. 16 assertions across runtime + compile-time halves, including the objectui#5155 ceiling stated explicitly ("declaring these did NOT buy rejection of a misspelling") and counter-probes so anever-narrowed declaration cannot satisfy the@ts-expect-errorblock.⛔ The fork I did NOT take — for the maintainer
The ruling routes
events/orientation/positionto ADR-0049 enforce-or-remove. Both directions, with costs:Direction A — remove (tombstone). The house form is already in this same file:
StaticTableColumn(objectui#5474, maintainer ruling 2026-08-22, Option C) uses?: nevermembers with@deprecated, twinned byz.never().optional()in the mirror, giving a tsc error and a loud parse rejection.events: [...]— legal and type-checking today — becomes a tsc error and a Zod rejection.eventsis currently required, so this is the maximal-breakage direction.packages/types/examples/data-display-examples.json. Out-of-repo consumers are unmeasurable from here — the same unmeasurable half objectui#5674's ruling answered with a deprecation window (deprecate one release, then delete). This PR ships that window's first half.check-lucide-icon-record-names.mjs, whose census counts 3 timeline icon names in that file.Direction B — enforce (implement). Build an
events-driven presentational timeline alongside theitems-driven one.My recommendation: A, on the deprecation-window shape — this PR is stage 1 (declared, deprecated, documented, still accepted), a follow-up card is stage 2 (tombstone). It keeps the ruling's direction, gives external consumers a warned break rather than a silent one, and matches the two precedents this repo has already set. But the timing of stage 2 is a maintainer call, not mine, and no code here presumes it.
The
timeScalealias retirement is routed the same way and is likewise not done here.Gates — each gate's own verdict line, at
e3c074869turbo run build --filter='!@object-ui/site'Tasks: 43 successful, 43 totalvitest run packages/types/ packages/plugin-timeline/Test Files 69 passed (69)/Tests 729 passed (729)@object-ui/typestype-checktsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json@object-ui/plugin-timelinetype-checktsc --noEmit && tsc -p tsconfig.test.json--filter '...@object-ui/types'type-check: Donecheck-changeset-presence✅ 4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)check-changeset-no-major✅ No changeset declares a 'major' bump.check-lint-coverage✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).eslint packages/types packages/plugin-timelineerrorCount: 0(311 pre-existing warnings;lint.ymlsets no--max-warnings)check:doc-types✅ Every documented component type is registered.check:doc-snippetsSemantic phase: 267 of 267 block(s) judged, 0 failed.check:doc-fences✅ every TypeScript block in 223 document(s) is fenced ts/tsx/typescriptcheck:control-bytes✅ OK (scanned 5226 tracked text file(s); skipped 85 binary).check:readme-exports✅ OK (… 378 self-imports judged (378 real, 0 wrong-path, 0 fabricated); 3246 export symbol(s) read from 37 of 40 package(s) (0 unbuilt…)check:icon-record-namesOK lucide icon names: 167 authored/declared names … are live 'icons' keyscheck:designer-field-key-paritydesigner-field-key-parity: OKcheck:spec-symbols✅ 1306 files scanned against 4959 spec export namescheck:self-import✅ No package names itself inside its own src/.check:phantom-deps✅ Every in-scope import is declared by the package that publishes it.check:vi-mock-specifiers✅ OK (3749 tracked source file(s)…)check:shell-escape-residue✅ OK (4/4 root(s) resolved…)pnpm check(the CLI checklint.ymlruns)✓ All checks passedobjectui validateTwo runs were NOT MEASURED rather than red, and are recorded as such:
check:readme-exportsandcheck:doc-snippetsfirst returned a precondition failure (the population COLLAPSED — this run proves nothing;PRECONDITION NOT MET (exit 2) — the snippet program was NOT run) because packages were unbuilt. Both are green above, re-run after the build.check-half-states.mjsexits 3 (no reading at all) — it is a scheduled GitHub-board patrol, not a PR gate.Lint scope, declared. Repo-scale
turbo run lintwas narrowed to the two affected packages. The narrowing is a measurement, not a skip, on three pieces of evidence: ① the population came from eslint's own config resolution, not my guess; ② the count is from--format json— 138 files, 0 errors; ③ type-aware linting is not enabled —eslint.config.js'slanguageOptionscarries onlyecmaVersionandglobals, with noparserOptions.project/projectService— so no rule's verdict on an untouched file can depend on this diff.check-lint-coverage.mjsindependently reports 46/46 packages clean.Out-of-scope findings (to be filed unassigned, not fixed here)
'timeline'is registered twice, and the bare-name fallback silently goes to the later registration (index.tsx:366,ObjectTimelineRenderer) overrenderer.tsx:498(TimelineRenderer).Registry.register's own collision guard exists to warn about this. Working-as-intended is plausible; undocumented and order-dependent is not.packages/types/examples/data-display-examples.jsonteaches the dead vocabulary — itstimelinenode authorsevents/orientation/positionand would render an empty rail. It is imported by nothing and type-checked by nothing. Best replaced in the same change as the ADR-0049 removal, since it must move then anyway andcheck-lucide-icon-record-names.mjscounts 3 icon names in it.TimelineEventis dead withevents— nothing reads it, and itsdatekey is not even thetimekey the renderer reads. Belongs to the same ADR-0049 sweep.itemshas no declared element type. Two shapes discriminated byvariant; declaring them precisely is real authoring value but a genuine narrowing, so it wants its own card and its own measurement.schema.bindis read undeclared (ObjectTimeline.tsx:142,useDataScope(schema.bind)) — not onBaseSchema, not on any timeline shape; it rides the index signature. Likely a repo-wide pattern rather than a timeline defect.Generated by Claude Code