A modular ES-module SPA that builds to one self-contained HTML file served from ClickHouse. No framework; runtime deps are rare and deliberate (currently seven, all bundled — see hard rule 4). Quality is held by tests.
- Coverage gate is non-negotiable.
npm testmust pass, andtsc --noEmitmust pass (ADR-0002 — incremental strict TypeScript, dev-time only; wired into thepreteststep). The suite enforces per-file coverage floors of 100/95/90/100 (statements/functions/branches/lines). Most pure/network/state/DOM and render modules maintain 100/100/100/100;src/ui/app.ts+src/main.tsare browser glue and integration-tested. Add tests in the same change as the code. The whole hand-written tree is strict TypeScript (ADR-0002 complete, #267) — new modules start as.ts. - Keep the layers honest. Pure logic goes in
src/core/(no DOM, no globals). Workspace aggregates go insrc/workspace/; Dashboard model, layout, and application code goes insrc/dashboard/, with dependency directionmodel/layouts <- application <- UI. App-level coordination and sessions go insrc/application/and must not importsrc/ui/orsrc/editor/. SQL Browser's network integration and application policy (OAuth,ChCtx, auth/epoch/retry, product operations) goes insrc/net/, with the fetch seam injected, never imported. Reusable, product-agnostic ClickHouse HTTP/Fetch mechanics (URL serialization, the low-level request, the progress-stream wire shape and its reader/decoder loop, HTTP exception-text/late-exception byte framing, and — since #630 Phase 5 — ClickHouse SQL string-literal/identifier quoting, the generic ClickHouse type-expression AST/parser/canonicalization/wrapper/enum grammar, and the shared lexical scanner that grammar depends on) live in the first-party workspace packagepackages/clickhouse-http(#630 Phase 2; the progress-stream/exception primitives since Phase 3; since Phase 4 also non-consuming HTTP success/error classification, explicit JSON/text/progress consumers, a minimalClickHouseError, and a stateless wire-levelkillQuery) instead. The package itself may depend on nothing under SQL Browsersrc/**and declares zero runtime dependencies, and every deep import into itssrc/**stays forbidden everywhere (mechanically enforced,build/check-boundaries.mjs) — only its public.export is consumable. Bare-specifier package access splits into two categories: TRANSPORT/PROTOCOL APIs (createClickHouseHttpClient,chUrl,streamLines, the response consumers,ClickHouseError) remain importable only undersrc/net/**, exactly as Phase 2 established, alongside OAuth/Basic credential acquisition, refresh, epochs, lifecycle callbacks, retries, and SQL Browser's own product operations/result modes. Since #630 Phase 6, the normal-request auth/epoch/refresh/lifecycle policy this rule already places undersrc/net/is owned bysrc/net/authenticated-clickhouse- request.ts(moved out ofch-client.ts's formerauthedFetch/transportFor(ctx), deleted outright — no forwarding alias): it builds the package client directly (client.request()) and composes it with the package's non-consuming success classifier/response consumers (consumeJsonResponse/consumeTextResponse/consumeProgressResponse), never the package's own conveniencequeryJson/queryText/queryProgressmethods — those need an already-resolved Authorization and give this policy no chance to inspect the settledResponsefirst.ch-client.ts's exportedqueryJson()is the first real production consumer of that response-consumer layer;runQuery/exportQueryreach the new module's raw request entrypoint but keep their own result/error/ body handling. The Phase-4 convenience consuming query APIs (queryJson/queryText/queryProgress) themselves remain additive and not yet consumed by anysrc/**caller (that full cutover, plusrunQuery/exportQuery's own result/export ownership migration, is Phase 7). The name/shape check has no type-only carve-out:import type/export typeand individualimport { type X }specifiers of a transport/protocol name are flagged on exactly the same terms as a value reference — erasure before bundling does not exempt a source-level NAME ownership boundary (build/lib/check-legacy-owners.mjs'sfindPackageImportUsagesdocuments why), matchingdocs/ARCHITECTURE.md. Pure LANGUAGE APIs (sqlString/quoteIdent/qualifyIdent,scanSpans/Span/SpanKind, and the generic type-grammar exports —parseClickHouseType,analyzeTypeModifiers,canonicalType,enumMembers/enumValues, and the rest of the wrapper/structural-query set) may instead be imported directly by their real SQL Browser consumers outsidesrc/net/**too (mechanically allowlisted by name,build/check-boundaries.mjs's revised Rule D) — only as a plain named import, value or type-only; default/namespace/side-effect/dynamic imports and package re-export gateways staysrc/net/**-only regardless of name or type-only-ness.isSupportedOptionScalar(which scalar families are eligible for an option-backed control) is SQL Browser option/control POLICY, not generic grammar, and stays owned bysrc/core/param-type.ts— the package never exports it. DOM rendering goes insrc/ui/as functions that take theappcontroller — except the editor, which lives insrc/editor/behind the injected editor seams (#143/#212): onlymain.jsimports concrete adapters, and everything else addressesapp.sqlEditororapp.specEditorexplicitly. SQL execution, schema insertion, export, and SQL formatting must never target whichever document happens to be visible. Side-effectful environment access (location, crypto, storage, fetch) is injected throughcreateApp(env)so everything is testable. Saved-query Spec static validation comes fromschemas/query-spec-v1.schema.jsonthrough the purecore/spec-schema.jsservice; app-owned feature validators extend that one service for result/context-dependent rules. - No secrets in git.
config.json(rendered) is gitignored; onlydeploy/config.json.exampleis committed. Rememberconfig.jsonis served to browsers: prefer a PKCE public client; if an IdP requires aclient_secretthere, lock the redirect URI and treat the file as public (see README "Configuring OAuth"). - The build is esbuild only; runtime deps are rare and deliberate. Source
files are the tested files; esbuild bundles
src/main.ts→dist/sql.html. Source development requires Node.js 22 or newer;.nvmrcselects Node 22,package.jsondeclares the minimum, and.npmrcmakes unsupported installs fail clearly.package-lock.jsonis committed; usenpm cifor a reproducible dependency graph in local, CI, and release builds, and update the lock only with an intentional dependency change. There are seven bundled runtime dependencies — CodeMirror 6 (the SQL editor, saved-query Spec JSON editor, and read-only source viewer, behind injected seams — #21/#212/#213), Chart.js (the Chart result view) with chartjs-adapter-date-fns and date-fns (registers the date-math backend Chart.js'stimescale needs for line/area charts over a time-role X column — #309; the pure axis/role decision of whether to use it stays incore/chart-data.ts, the adapter is a side-effect-only import next toChartitself inmain.ts), @dagrejs/dagre (the EXPLAIN pipeline-graph layout), @preact/signals-core (the reactivity primitive — seedocs/ADR-0001-reactivity.md), and marked (the Markdown LEXER for #60/#315 reference-doc bodies — used strictly as a pure tokenizer incore/doc-markdown.ts, like the signals precedent it needs no seam;marked.parse()/HTML-string output andinnerHTMLare FORBIDDEN, except forui/dom.ts'shtmlprop: that escape hatch accepts only trusted, code-owned static markup (never user, server, or Markdown content) — the token tree is projected into DOM byui/doc-markdown-view.tsunder the fail-closed policy: images/raw HTML/rejected links render as literal text; measured +44 KB raw / ~3% artifact delta) — all inlined into the artifact, so the page loads no runtime libraries from third-party CDNs.packages/clickhouse-http(#630 Phase 2, the repository's first npm workspace) is project source, not an eighth bundled runtime dependency: it is private, ships nodependencies, and since #630 Phase 8 it has its own independent build/type/test boundary — package- localesbuildcompiles itssrc/**/*.tsto unbundled, browser-first ESM (bundle: false) atdist/**, and package-localtscemits matching.d.tsdeclarations; its manifest'smain/types/exports["."]all resolve to that built output, never source. Rootesbuildbundles that BUILTdist/**into the single served artifact (via the workspacenode_modulessymlink, exactly like any resolved dependency) —build/size-report-lib.mjsstill attributes it to theprojectownership bucket, notexternal, since it is project code either way. Rootnpm run build/build/bundle.sh/deploy/install.shall explicitly build the package first (npm run build:clickhouse-http) so itsdist/**exists before rootesbuildever runs — this environment'signore-scripts=truemeans lifecycle hooks never do this implicitly. Adding another runtime dependency is a deliberate decision (it grows the single served file) — don't do it casually. When a feature needs a library, keep the testable logic pure insrc/core/(chart axis/role/pivot math insrc/core/chart-data.js; DOT→positions insrc/core/dot-layout.js, both 100%-covered) and make the library call an injected seam (app.Chart/app.Dagre/env.Editor/env.SpecEditor/env.CodeViewer, like the fetch/crypto seams) so the DOM wrapper stays fully tested rather than dropping below the coverage gate. (The CM6 adapters are unit-tested against the real libraries under happy-dom.) Ajv andajv-formatsare dev dependencies only: they strictly compile the canonical Library/saved-query/Spec schema graph to deterministic, self-contained generated ESM. The production artifact ships the generated validator, never the general Ajv engine.@fontsource-variable/interand@fontsource-variable/jetbrains-monoare dev dependencies on the same footing: no code from them ships, butbuild/fonts.mjsreads their latin-subset woff2 files and inlines them as base64@font-facesources (~89 KB of woff2 for the pair, +19% gzip on the artifact). DESIGN.md names both faces, and before this they were referenced by name with no@font-faceanywhere — so they rendered only for users who already had them installed. Font weight is a deliberate cost like any other:FONT_BYTE_BUDGETinbuild/fonts.mjsis asserted bytests/unit/typography-contract.test.js, so adding latin-ext or an italic cut has to be a reviewed edit rather than silent growth. That same test is the gate on the whole type system — everyfont-sizeinsrc/styles.cssmust resolve to a--text-*token, no two steps in a ramp may sit closer than 1px, token contrast must clear WCAG AA in both themes, and no class the UI renders may be left with no CSS rule at all (which is how a browser-default Arial confirmation dialog once shipped). - No UI framework; signals for state, imperative adapters for islands. State
reactivity is
@preact/signals-core(signal/effect/computed/batch), migrated slice-by-slice (ADR-0001). No React/Preact/Solid — a Preact spike on the schema panel (spike/preact-schema, ADR-0001 addendum) confirmed a component model removes the in-place-mutation pain but buys a second render paradigm the roadmap doesn't justify. The hard, third-party, or high-frequency-pointer surfaces (the editor, the EXPLAIN/schema graphs, Chart.js, result-grid resize/sort) stay imperative behind an injected seam — signals coordinate state, they don't own every mousemove. The editor is CodeMirror 6 behind explicit injected SQL and Spec editor seams (#21/#212; the SQL completion source swaps to from-scope data in #84). When a second consumer of a complex UI pattern appears, extract a shared primitive (e.g.EditorPort,GraphSurface, a result-view registry,Drawer) rather than copy it — but don't build a primitive speculatively for a single caller.
Touch these in one change:
- the module under
src/core/(pure logic) orsrc/ui/(render) ; - its
tests/unit/<module>.test.jsto 100% ; - if it changes the deployed surface,
deploy/http_handlers.xml+ README.
| Path | What |
|---|---|
src/core/* |
pure logic, 100% covered |
src/net/* |
OAuth + ClickHouse client, injected fetch; authenticated-clickhouse-request.ts (#630 Phase 6) is the sole normal-request auth/epoch/refresh/lifecycle owner, over the package's request() and response consumers |
packages/clickhouse-http/src/* |
first-party npm workspace (repo's first, #630 Phase 2) — chUrl/URL serialization, the low-level injected-fetch() request, the progress-stream read loop and HTTP exception parsing/framing (Phase 3), (Phase 4) non-consuming success/error classification (ensureClickHouseSuccess), JSON/text/progress consumers, a minimal ClickHouseError, convenience queryJson/queryText/queryProgress client methods, and a stateless wire-level killQuery, and (Phase 5) the ONE ClickHouse SQL-quoting implementation (sqlString/quoteIdent/qualifyIdent), the ONE generic type-expression grammar (parseClickHouseType/analyzeTypeModifiers/canonicalType/wrapper/enum helpers), and the shared lexical scanner (scanSpans) — behind a public . export only; transport/protocol APIs stay src/net/**-only, while the pure-language exports above may be imported directly by their real SQL Browser consumers anywhere outside src/net/** too (mechanically allowlisted, build/check-boundaries.mjs Rule D); since Phase 6, src/net/authenticated-clickhouse-request.ts is a real production consumer of request() plus the non-consuming classifier/JSON/text/progress consumers — the convenience queryJson/queryText/queryProgress methods themselves have no src/** consumer as of #630's own closure (a genuinely open item, not reopened or claimed by Phase 8) |
packages/clickhouse-http/{test,build.mjs,tsconfig*.json,vitest.config.ts} |
(#630 Phase 8) the package's own independent build/type/test boundary — package-local esbuild (unbundled browser-first ESM, bundle: false) + tsc (declaration-only emit) produce dist/**; package-local vitest.config.ts (100/95/90/100 per file) exercises the built public barrel via a relative import to src/index.ts; test/isolated-package.mjs (npm run test:pack) proves a real npm pack installs, resolves, and typechecks outside this repository with no source fallback; test/browser/** is this package's own Chromium+WebKit regression suite over the built dist/** (no import map, no vendor client, no Docker/live ClickHouse) |
src/application/* |
app-level coordination, sessions, and pure projections; no UI/editor imports |
src/workspace/* |
pure stored-workspace aggregate, persistence contracts, and mutations |
src/dashboard/* |
Dashboard model, layouts, and application runtime; dependency direction is mechanically checked |
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.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 |
deploy/* |
install/uninstall + http_handlers.xml |
tests/unit/* |
one spec per module (vitest + happy-dom) |
The distilled maintainer/agent knowledge base lives in this repo, at .wiki/
— start at .wiki/Home.md. It is versioned with the code: update the affected
wiki page(s) in the same change that stales them, the same way this file,
docs/*, and CHANGELOG.md get reconciled (see "Reconcile forward work after
a substantive change" below). It maps architecture, workflow, decisions,
deployment, and operational lessons back to their canonical sources (this
file, docs/*, issues). .wiki/Maintaining-This-Wiki.md explains how to use
and update it. The old GitHub project wiki remote
(altinity-sql-browser.wiki.git) is a frozen archive — do not clone or
push to it.
Pure-by-construction modules, injected side-effect seams, per-file coverage thresholds, and a single ClickHouse-served artifact built by esbuild.
- Surface out-of-scope findings, don't bury them. Spot a real bug, data
inconsistency, deprecated API, or future footgun outside the current task →
open an issue labeled
inbox(file:line + why deferred) and tell the user. High signal only, not style nits. - Reconcile forward work after a substantive change. A change to behavior,
schema, or a settled decision can stale tracked work. In the same commit,
reconcile what it reshaped: the roadmap meta-issue (currently #68) — re-check
or re-scope the track it touches; the affected issue's body (Goal/Acceptance);
the relevant ADR addendum and
CHANGELOG.md[Unreleased]; and any issue it obsoletes (close via "Closes #N" in the PR). Flag it if the rework is large. (Trivial typo/comment changes exempt.) - Convert friction into memory. If a task needed retried commits or hit an unexpected failure (test/env/scope surprise), save a memory so the next session doesn't repeat it.
- Contracts specify final-state invariants. Issue contracts state what must be true after an interaction settles — not frame-by-frame behavior during gestures/transitions — unless a user-visible bug forces otherwise. ADR-0004's retrospective: the frame-level focus contract in #487/#488, not the code, was the dominant cost driver.
- Subagent fan-out is read-only unless the prompt says otherwise. A
forked or spawned agent inherits the entire parent conversation —
including this file and any skill script being run — so without an
explicit boundary it can conclude it's the one meant to finish the whole
task: committing, pushing, opening a PR, editing
CHANGELOG.md, or writing to the memory directory. When fanning out review/finder/analysis subagents mid-task, state the boundary in every prompt ("read-only: no Edit/Write, no git/gh mutating commands, no TaskCreate/TaskUpdate, no memory writes — return only <schema>"), and prefer a fresh, self-contained agent overforkwhen the parent context includes an in-progress mutating workflow — a fork inherits that context, a fresh agent doesn't. Diff the working tree,git log, andgh pr listafter every batch regardless: an instruction in a prompt is not an enforced tool restriction.