Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ auto-generated per-PR notes; this file is the curated, human-readable history.
## [Unreleased]

### Changed
- **ADR-0002 TypeScript migration complete** (#267). Every hand-written module
under `src/` (106 runtime modules + 6 type-only seam files) and every unit
test is strict TypeScript, converted
leaf-up in dependency-tier waves with zero behavior change — the tail
covered `chart-data`, `ch-client`, the CM6 adapters, `explain-graph`,
`results` (which now owns the canonical `Result` union), `app.ts` (declared
against the `App` contract, with `app.types.ts` reconciled to reality:
`SchemaFocus.kind`, `specCompletionSources: DynamicSources`, corrected
`loadIdps`/`openNodeDetail` signatures), and `main.ts`.
`tests/helpers/fake-app.ts` satisfies the full `App` contract, deleting
~1,080 lines of per-test stub scaffolding across eight specs. Still `.js`
by design: the two generated artifacts under `src/generated/`, the
Playwright e2e specs (outside the `tsc` gate), and two node-tooling unit
specs pending an `@types/node` decision — the vitest mixed-tree resolver
shim survives only for those two. The build and artifact are unchanged;
runtime deps stay four. The conversion surfaced and fixed two real dropped
`return`s (`exportEntry`, `renderDashboard` promises) and a latent
never-exercised completion-ranking branch.
- **Chart style internals consolidated (post-#258 review).** The per-chart-type
style surface (which fields each type owns, their accepted values, and their
defaults) now lives in one `CHART_STYLE_SPEC` table that both
Expand Down
9 changes: 5 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ all bundled — see hard rule 4). Quality is held by tests.
1. **Coverage gate is non-negotiable.** `npm test` must pass, and `tsc --noEmit`
must pass (ADR-0002 — incremental strict TypeScript, dev-time only; wired
into the `pretest` step). The pure/network/state/DOM and render layers are
gated at **100/100/100/100 per file**. `src/ui/app.js` + `src/main.js` are
gated at **100/100/100/100 per file**. `src/ui/app.ts` + `src/main.ts` are
the browser glue — gated lower and integration-tested. Add tests in the
same change as the code.
same change as the code. The whole hand-written tree is strict TypeScript
(ADR-0002 complete, #267) — new modules start as `.ts`.
2. **Keep the layers honest.** Pure logic goes in `src/core/` (no DOM, no
globals). Network goes in `src/net/` with the fetch seam *injected*, never
imported. DOM rendering goes in `src/ui/` as functions that take the `app`
Expand All @@ -31,7 +32,7 @@ all bundled — see hard rule 4). Quality is held by tests.
`client_secret` there, lock the redirect URI and treat the file as public
(see README "Configuring OAuth").
4. **The build is esbuild only; runtime deps are rare and deliberate.** Source
files are the tested files; esbuild bundles `src/main.js` → `dist/sql.html`.
files are the tested files; esbuild bundles `src/main.ts` → `dist/sql.html`.
`package-lock.json` is committed; use `npm ci` for a reproducible dependency
graph in local, CI, and release builds, and update the lock only with an
intentional dependency change.
Expand Down Expand Up @@ -86,7 +87,7 @@ Touch these in one change:
| `src/ui/*` | hyperscript, icons, render modules, controller |
| `src/editor/*` | injected SQL/Spec editor ports + CodeMirror adapters (#143/#21/#212) |
| `src/state.ts` | state model + pure ops (strict TS — ADR-0002 phase 2) |
| `src/main.js` | bootstrap (OAuth callback, share-links) |
| `src/main.ts` | bootstrap (OAuth callback, share-links) |
| `src/**/*.types.ts` | type-only seam contracts (ADR-0002 phase 0), co-located next to the `.js` file each describes (or, for a shape spanning several consumers like `src/env.types.ts`, at their shared directory); `tsc --noEmit` gate |
| `src/generated/json-schema.types.ts` | **generated** persisted-data types (`QuerySpecV1`/`SavedQueryV2`/`LibraryV2`/`PanelCfg`) emitted by `build/emit-schema-types.mjs` from the schema manifest — never hand-edit, never hand-duplicate these shapes; regenerate via `npm run generate:schemas` |
| `build/build.mjs` | esbuild → `dist/sql.html` |
Expand Down
6 changes: 3 additions & 3 deletions build/build.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Build the single-file SPA: esbuild bundles src/main.js into one IIFE, which
// Build the single-file SPA: esbuild bundles src/main.ts into one IIFE, which
// is inlined (with the stylesheet) into build/template.html → dist/sql.html.
//
// esbuild is the only build-time tool; the bundled runtime dependencies are
Expand Down Expand Up @@ -37,15 +37,15 @@ async function buildStamp() {

async function main() {
const result = await build({
entryPoints: [resolve(root, 'src/main.js')],
entryPoints: [resolve(root, 'src/main.ts')],
bundle: true,
format: 'iife',
target: 'es2020',
minify: true,
write: false,
legalComments: 'none',
});
// Replace the `__ASB_BUILD__` placeholder (a string literal in src/main.js)
// Replace the `__ASB_BUILD__` placeholder (a string literal in src/main.ts)
// with the build stamp before the bundle is inlined — same token-replace seam
// as the styles/script splices below. replaceAll is robust to either quote
// style minify may emit around the literal.
Expand Down
25 changes: 12 additions & 13 deletions docs/ADR-0002-static-typing.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
# ADR-0002: Static typing — incremental strict TypeScript, dev-time only

- **Status:** Accepted — 2026-07-16; **phases 0–5 landed** 2026-07-16 (#262,
one PR): the `tsc --noEmit` gate + typed seam interfaces (phase 0);
generated persisted-data types from the canonical schemas via
`build/emit-schema-types.mjs` (phase 1 — see the emitter addendum below);
`src/state.ts` (phase 2); the saved-query/panel/dashboard contract spine and
the parameter/filter/execution pipeline in `src/core/` (phases 3–4); and the
dashboard runtime slice `src/ui/{dashboard,dashboard-kpi-band,panels}.ts`
(phase 5) — 19 runtime modules strict (the 16-module slice plus the dom.js/
param-scan.js/diagnostics.js leaves whose typed wrappers it would otherwise
have duplicated), their unit tests converted as a
second wave, zero behavior change. Remaining `.js` (app.js glue, net/,
editor adapters, the rest of ui/ and core/) converts leaf-up as feature
work touches it, per decision item 4.
- **Status:** Accepted — 2026-07-16; **migration COMPLETE** 2026-07-17 (#267,
after phases 0–5 landed 2026-07-16 via #262): every hand-written module
under `src/` (106 runtime modules + 6 type-only seam/contract files) and
every unit test is strict TypeScript, converted
leaf-up in dependency-tier waves with zero behavior change. The only
remaining `.js`: the two GENERATED runtime artifacts under `src/generated/`
(Ajv standalone emits JS; `allowJs` stays for them), the Playwright e2e
specs (run by Playwright's own loader, outside the `tsc` gate), and two
node-tooling unit specs (`schema-build`, `spec-examples`) whose typing waits
on a deliberate `@types/node` decision — the vitest mixed-tree resolver shim
survives solely for those two. `tests/helpers/fake-app.ts` satisfies the
full `App` contract; the per-test stub copies are gone.
- **Date:** 2026-07-16
- **Context tracking:** roadmap #68; phase 0 #262
- **Related:** ADR-0001 (the slice-by-slice migration playbook this reuses)
Expand Down
30 changes: 23 additions & 7 deletions src/core/auth-handoff.js → src/core/auth-handoff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,17 @@
// these are the message contract, the key set, and the origin/source predicates,
// kept here so they are trivially 100% testable.

/** A sessionStorage-like object: only the two methods this module uses. */
export interface StorageLike {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
}

/** A snapshot of the present auth keys (only those that were set). */
export type AuthSnapshot = Partial<Record<string, string>>;

/** The sessionStorage keys that carry a live auth session (OAuth or basic). */
export const AUTH_SS_KEYS = [
export const AUTH_SS_KEYS: string[] = [
'oauth_id_token', 'oauth_refresh_token', 'oauth_idp', 'oauth_origin',
'ch_basic_auth', 'ch_basic_user', 'ch_basic_origin',
];
Expand All @@ -20,8 +29,8 @@ export const AUTH_REQUEST = 'asb-auth-request';
export const AUTH_GRANT = 'asb-auth-grant';

/** Read the present auth keys out of a sessionStorage-like object. */
export function snapshotAuth(ss) {
const snap = {};
export function snapshotAuth(ss: StorageLike): AuthSnapshot {
const snap: AuthSnapshot = {};
for (const k of AUTH_SS_KEYS) {
const v = ss.getItem(k);
if (v != null) snap[k] = v;
Expand All @@ -30,25 +39,32 @@ export function snapshotAuth(ss) {
}

/** Write a snapshot's auth keys into a sessionStorage-like object. */
export function restoreAuth(ss, snap) {
export function restoreAuth(ss: StorageLike, snap: AuthSnapshot | null | undefined): void {
for (const k of AUTH_SS_KEYS) {
if (snap && snap[k] != null) ss.setItem(k, snap[k]);
}
}

/** Does a snapshot carry usable credentials (an OAuth token or basic creds)? */
export function hasAuth(snap) {
export function hasAuth(snap: AuthSnapshot | null | undefined): boolean {
return !!(snap && (snap.oauth_id_token || snap.ch_basic_auth));
}

/** The subset of a MessageEvent this module's predicates read. */
export interface AuthMessageEvent {
origin: string;
source: unknown;
data?: { type?: string } | null;
}

/** A well-formed credential *request* from the expected origin + source window. */
export function isAuthRequest(e, origin, source) {
export function isAuthRequest(e: AuthMessageEvent | null | undefined, origin: string, source: unknown): boolean {
return !!e && e.origin === origin && e.source === source
&& !!e.data && e.data.type === AUTH_REQUEST;
}

/** A well-formed credential *grant* from the expected origin + source window. */
export function isAuthGrant(e, origin, source) {
export function isAuthGrant(e: AuthMessageEvent | null | undefined, origin: string, source: unknown): boolean {
return !!e && e.origin === origin && e.source === source
&& !!e.data && e.data.type === AUTH_GRANT;
}
4 changes: 2 additions & 2 deletions src/core/cell.js → src/core/cell.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Pure helpers for the cell-detail drawer. No DOM, no globals.

/** Heuristic: does the string look like an HTML/XML fragment worth rendering? */
export function looksLikeHtml(s) {
export function looksLikeHtml(s: unknown): boolean {
const str = String(s || '');
return /<([a-z!][\s\S]*?)>/i.test(str) && /<\/[a-z]+\s*>|\/>/i.test(str);
}
Expand All @@ -10,7 +10,7 @@ export function looksLikeHtml(s) {
* Pretty-print a cell value for the detail view: valid JSON is reindented;
* anything else is returned as-is (coerced to string, null/undefined → '').
*/
export function prettyValue(s) {
export function prettyValue(s: unknown): string {
if (s == null) return '';
const str = String(s);
const t = str.trim();
Expand Down
Loading
Loading