ADR-0002: strict TypeScript migration — gate + phases 1–5 (generated schema types, state, contract spine, param pipeline, dashboard runtime, tests) - #265
Merged
Conversation
tsc --noEmit joins the gate (strict, allowJs, checkJs:false, noEmit, erasableSyntaxOnly), wired into pretest alongside check:schemas. Five type-only .types.ts files, each co-located next to the runtime module it describes, declare EditorPort, SpecEditorPort, CodeViewerHandle, CreateAppEnv/BootstrapEnv, and the App controller surface as consumed by render modules — no existing .js module converted, build/artifact unchanged. editor-port.js's JSDoc EditorPort typedef now points at the .ts file instead of duplicating it (comment-only). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…the canonical JSON Schemas
Hand-rolled deterministic emitter (build/emit-schema-types.mjs) instead of
json-schema-to-typescript: refs resolve via the Ajv $id registry (not the
filesystem), 2020-12 unevaluatedProperties/not are honored, names are pinned
in the manifest (QuerySpecV1/SavedQueryV2/LibraryV2) and derived from $defs
keys — never from prose titles. Unhandled shape keywords throw, so schema
evolution fails loudly. Output joins generatedSources() (4→5 artifacts) and
inherits generate:schemas/check:schemas staleness for free.
PanelCfg is a usable discriminated union; the forward-compat catch-all is
FuturePanelCfg { type: string & {} } with a guard asserting not.enum equals
the sibling consts. src/schema-contract.types.ts pins the semantic invariants
(spec open, library/saved-query closed, Extract<> narrowing) at check:types
time. ADR-0002 addendum records the emitter decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XP1KN94N969SVMNFPdVtGg
… strict TS; vitest mixed-tree resolver First leaf-up conversions (ADR-0002 phase 4 order): zero behavior change, strict-clean, unconverted .js imports typed via local non-exported wrapper consts (no sibling .d.ts shims). Exported contracts gain named types (ParsedParamType, MaterializedBlock/MaterializeResult, ParamDeclaration). tests/vitest.config.ts gains a small resolveId plugin: Vite only retries .js→.ts when the importer is itself TS, so a still-.js module or test importing a converted module failed to resolve under vitest (tsc, esbuild, and Node all resolve it natively). The shim maps a relative .js specifier to the sibling .ts only when the .js file no longer exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XP1KN94N969SVMNFPdVtGg
…rict TS saved-query.ts is the first consumer of the generated persisted-data types (Panel/Dashboard/QuerySpecV1); builder results are typed as QueryRoot rather than SavedQueryV2 because id/spec completeness is caller-supplied, unvalidated data. dashboard.ts and param-validate.ts convert mechanically; param-validate exports the ParamValidationResult seam shape its pipeline/filter consumers destructure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XP1KN94N969SVMNFPdVtGg
…pipeline to strict TS Panel spine (panel-cfg, panel-execution, result-choice): panel-type ids and chart families derive from the generated PanelCfg union — no hand-duplicated literals; validation boundaries (panelCfgValid/panelCfgStaticValid) honestly keep cfg: unknown; auto vs resolved vs explicit panel states are distinct exported types. Param pipeline (param-pipeline, dashboard-filters, filter-execution): the analysis/prepare/field-control contracts the pipeline already passes around get exported names (AnalyzedSource, BoundParamSnapshot readonly snapshots, PreparedBatch, FieldControl, ValidationMode 'input'|'execute', ...); still-.js imports typed via local wrapper consts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XP1KN94N969SVMNFPdVtGg
…Script src/state.ts types every signal by its actual value, separates persisted SavedQueryV2 data from runtime QueryTab drafts, and narrows localStorage ingress honestly: savedQueries flows through decodeStoredSavedQueries's discriminated result; the six undecoded prefs carry visible commented assertions instead of a lying generic loadJSON<T>. app.types.ts's Tab/State placeholders become re-exports of the real QueryTab/AppState (one placeholder falsehood fixed: ResultSort.col is a number|null column index, not a string). tests/types/state.test-d.ts pins compile-time invariants via the widened tsconfig include. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XP1KN94N969SVMNFPdVtGg
The panel registry gets typed arms (PanelArm/PanelRenderArgs) and an exported
PanelRenderResult {node, destroy?} contract; KPI band slots get exported
KpiBand/KpiSourceSlot lifecycle types. Still-.js DOM helpers (dom.js h(),
chart/grid/logs/kpi renderers) stay behind local typed wrapper consts; h()
gains a keyof HTMLElementTagNameMap overload so element props type without
per-call casts. Coverage parity with the pre-conversion files verified
branch-by-branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XP1KN94N969SVMNFPdVtGg
…t TypeScript src/ui/dashboard.ts types the slot lifecycle the runtime already enforces: DashSlot = TileSlot | KpiSourceSlot discriminated on kind; generation counters, owned AbortControllers, and PanelRenderResult.destroy have explicit owners; FavoriteSourceHooks<S extends DashSlot> makes tile/KPI handler mixups uncompilable; progress callbacks are typed as label-only so streaming updates cannot deliver results. Execution waves consume the exported PreparedBatch/ PreparedSource contracts. Coverage parity with the .js original verified branch-by-branch (zero new uncovered branches). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XP1KN94N969SVMNFPdVtGg
…wave)
Fourteen tests/unit specs rename to .ts and type their fixtures against the
modules' exported contracts; assertions and it() counts are unchanged.
vitest test.include widens to *.test.{js,ts}; tsconfig include gains
tests/unit and tests/helpers. Deliberately partial/invalid fixtures keep
their values behind targeted single-level casts. Two fixture adjustments
verified behavior-neutral: placeholder message fields on spec-diagnostic
stubs, and KPI keyed-object row fixtures rewritten positionally (readKpiFields
output byte-identical; the keyed-row variant is a known type-vs-runtime gap).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XP1KN94N969SVMNFPdVtGg
…aced applyResultChoice gets its explicit QueryRoot return type (was inferring unknown, forcing caller-side wrapper casts); ResultLike/KpiSourceResult accept serverVersion/error null like the AppState/stream shapes that feed them; FilterDiagnostic names sourceId (the Retry affordance dispatches on it); App.specValidators is the real SpecValidationService instead of unknown. The panels.ts and dashboard.ts wrapper casts those gaps forced are removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XP1KN94N969SVMNFPdVtGg
The 1985-line spec covering ui/dashboard.ts and core/dashboard.ts converts with all 142 it() cases unchanged. fake-app.js bridges to the App contract via a typed withApp() merge; the createApp() glue tests keep the one real object reference (closures mutate their captured app, so a spread copy reads stale state — asApp is a pure type reinterpretation instead). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XP1KN94N969SVMNFPdVtGg
…for phases 1-5 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XP1KN94N969SVMNFPdVtGg
The Playwright fixtures import raw /src/**/*.js as native ESM with no bundler, so the python http.server 404'd every module renamed to .ts (all four editor fixture pages — 36 e2e tests were failing). build/e2e-serve.mjs replaces it: same static serving plus the ADR-0002 mixed-tree rule every other tool already applies — a missing .js falls back to its .ts sibling, type-stripped in-process by esbuild (browsers don't execute TS). Full suite green again (80/80 on chromium+webkit, ~12s). Also from the review pass: ADR-0002's Decision §3/Consequences reconciled with the emitter addendum (they still named json-schema-to-typescript as the tool); the emitter now fails loud on `required` in a properties-less object instead of silently dropping the constraint (+ guard test); saved-query.ts's andGet gains the missing garbage-ingress branch test (restores pre-conversion branch coverage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XP1KN94N969SVMNFPdVtGg
…uplicated wrappers The three tiny leaves whose typed wrapper consts were copy-pasted across the converted slice become strict .ts themselves: dom.ts gives h() its honest overloads (TagNameMap + function components) once, param-scan.ts owns ParamDeclaration/ParamOccurrence, diagnostics.ts owns the Diagnostic factory shape. Eight wrapper blocks across panels/dashboard/dashboard-kpi-band/ dashboard-filters/filter-execution/param-pipeline/optional-blocks/query-params are deleted (query-params re-exports ParamDeclaration so its API is unchanged), plus dashboard.ts's now-dead FilterSqlDiagnosticView bridge. Their tests convert too. Remaining conversion inventory tracked in #267. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XP1KN94N969SVMNFPdVtGg
This was referenced Jul 17, 2026
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.
Implements all of ADR-0002 (
docs/ADR-0002-static-typing.md) in one PR — phase 0 (the gate) plus the five follow-up phases from the migration proposal pack, per maintainer decision to avoid per-phase issue/PR churn. Closes #262.Phase 0 — TypeScript gate (original scope)
tsconfig.json(strict,allowJs,checkJs: false,noEmit,erasableSyntaxOnly, bundler resolution);typescriptdevDependency;check:typeswired intopretest→npm test→ CI.EditorPort,SpecEditorPort,CodeViewer,CreateAppEnv/BootstrapEnv, and theAppcontroller surface.Phase 1 — generated persisted-data types
build/emit-schema-types.mjs: a hand-rolled deterministic emitter (json-schema-to-typescript was evaluated and rejected — Ajv-$id-registry ref resolution, 2020-12unevaluatedProperties/notgaps, unstable title-derived names; recorded as an ADR-0002 addendum). Emitssrc/generated/json-schema.types.ts(QuerySpecV1,SavedQueryV2,LibraryV2, and a usablePanelCfgdiscriminated union with aFuturePanelCfg { type: string & {} }forward-compat member). JoinsgeneratedSources()(4→5 artifacts) sogenerate:schemas/check:schemasstaleness covers it; unhandled shape keywords throw.src/schema-contract.types.tspins semantic invariants (spec open / library+saved-query closed /Extract<>narrowing) atcheck:typestime.Phases 2–5 — 19 modules converted to strict TS (zero behavior change)
src/state.ts— signals typed by value, persistedSavedQueryV2vs runtimeQueryTabseparated, localStorage ingress narrowed honestly (decodeStoredSavedQueriesdiscriminated result; visible commented assertions on the six undecoded prefs).app.types.tsplaceholders became re-exports of the real types.saved-query,panel-cfg,result-choice,panel-execution,core/dashboard— panel-type ids derive from the generated union; validation boundaries keepcfg: unknown.param-type,param-validate,optional-blocks,query-params,param-pipeline,dashboard-filters,filter-execution— exported contracts for analysis/prepared-batch/field-control/validation-mode shapes.ui/dashboard,ui/dashboard-kpi-band,ui/panels— discriminatedDashSlot = TileSlot | KpiSourceSlotlifecycle (generations, ownedAbortControllers,PanelRenderResult.destroy), genericFavoriteSourceHooks<S>so tile/KPI handler mixups don't compile, progress callbacks typed label-only..jsimports are typed via local non-exported wrapper consts (no sibling.d.tsshims) that self-delete as those modules convert later. Coverage parity was verified branch-by-branch on the risky files; no test assertions changed.Test second wave + tooling
tests/unit/*.test.jsspecs →.ts(it() counts unchanged); tsconfig/vitest include widened.tests/vitest.config.tsgains a mixed-treeresolveIdshim (Vite only retries.js→.tsfrom TS importers — tsc/esbuild/Node all resolve it natively).build/e2e-serve.mjsreplacespython3 -m http.serverfor the Playwright harness: the fixtures import raw/src/**/*.jsas native ESM, so renamed modules 404'd — the new server falls back to the.tssibling and type-strips via esbuild. Full e2e suite: 80/80 on chromium+webkit (~12s).Verification
npm test: 101 files / 2850 tests, per-file coverage thresholds green,check:schemas+check:typesinpretest.npm run build:dist/sql.htmlunchanged in kind — four runtime deps, zero third-party requests, no new runtime dependency.7eb9eeb(e2e harness break, ADR self-contradiction, emitterrequiredgap, one new uncovered branch). Semantic .js-vs-.ts deep-dives on state/dashboard/panels found zero behavior deltas.Known follow-ups (deliberately not in this PR)
Duplicated typed wrappersmostly resolved in-PR: dom.js, param-scan.js, and diagnostics.js converted and their 8 wrapper copies deleted (theschemaKey/CHART_TYPEScasts remain until chart-data.js converts). The triplicated testAppstub (fixtests/helpers/fake-app.js) and the remaining conversion inventory are tracked in Finish the ADR-0002 TypeScript migration: remaining .js modules, fake-app App stub, mixed-tree scaffolding retirement #267.ResultLike.rowsdoesn't type the keyed-object row variantreadKpiFieldsalso accepts (type-vs-runtime gap, noted in kpi-band test comments).spec.viewinto the now-union-typedresultViewsignal (found during state typing).🤖 Generated with Claude Code
https://claude.ai/code/session_01XP1KN94N969SVMNFPdVtGg