feat(#585 phase 1): establish the transport seam without behavior change - #621
Conversation
…TP implementation Defines the narrow SQL Browser transport contract (src/net/clickhouse-transport.types.ts: ClickHouseTransport, TransportDeps, TransportRequest, StreamCallbacks) and puts the current custom HTTP transport behind it (src/net/clickhouse-http-transport.ts: createHttpTransport, plus chUrl/ChUrlOpts moved verbatim from ch-client.ts). A reusable contract-test-suite factory (tests/unit/clickhouse-transport-contract.ts) registers against createHttpTransport only, since no official implementation exists (ADR-0005 is Rejected) and Phases 2-4 do not proceed without a new decision. tests/unit/clickhouse-http-transport.test.ts covers the moved progress-line stream loop, including a split multi-byte UTF-8 character across byte chunks and per-chunk onLine-before-onChunk ordering. ch-client.ts is not yet wired to this seam in this commit — that lands next. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
authedFetch's internals now delegate through the transport (transportFor(ctx)
:= createHttpTransport({ fetch: () => ctx.fetch, origin: () => ctx.origin }))
instead of calling chUrl + ctx.fetch directly. This is authedFetch's one
exported-signature change (Claimed item 3): it now takes a structured
Omit<TransportRequest, 'authorization'> instead of a prebuilt URL string, and
snapshots the incoming request's settings/params synchronously at entry,
before its first await (ctx.getToken()) -- one centralized aliasing defense
for every caller present and future, replacing chUrl's old pre-await URL
serialization as the capture point.
queryJson/runQuery/exportQuery/killQueryWithLease all delegate through the
transport; runQuery's inline stream-parsing loop is deleted in favor of
transport.streamLines. killQueryWithLease builds a one-shot transport
directly from the frozen lease (never transportFor/authedFetch), preserving
hard invariant 8 (no mutable-auth reads during cleanup). ChCtx is unchanged
-- no transport field, no runtime switch; ch-client.ts re-exports chUrl,
ChUrlOpts, and the new contract types so every existing importer (including
tests/spike/clickhouse-client/current-adapter.ts) keeps resolving.
tests/unit/ch-client.test.ts: ~25 direct authedFetch call sites updated
mechanically to the new signature (no assertions changed); added
invocation-time settings/params capture tests (mutation during a pending
getToken and during a pending one-refresh retry), a live-origin-authority
test through the production transportFor wiring, and a killQueryWithLease
sabotage-guard test proving getToken/refresh are never read even if the
lease object happens to carry them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
…t seam build/check-boundaries.mjs: bans any src/** file from importing the bare @clickhouse/client-web specifier, with a single allowlist entry for the future official transport file (src/net/clickhouse-web-transport.ts, which does not exist yet -- ADR-0005 is Rejected and Phases 2-4 do not proceed without a new decision). Adds twin RULES entries forbidding both clickhouse-http-transport.ts and the type-only clickhouse-transport.types.ts from relatively importing ch-client.ts/oauth.ts/oauth-config.ts/ src/application//src/ui, closing the gap where a per-dir rule naming only the implementation file would leave the sibling contract file unguarded (check-boundaries.mjs matches import type too). tests/unit/client-web-spike-policy.test.js: mirrors the bare-specifier ban as a coverage-gated unit test, since this environment's ignore-scripts=true means a bare `npm test` never runs check:arch (CLAUDE.md hard rule 4). An independently implemented scanner, not a shared one -- the accepted two-scanner drift risk tests/unit/dashboard-boundaries.test.js already documents for its own mirror of the same script. Every forbidden category (ch-client.ts, oauth.ts, oauth-config.ts, src/application, src/ui) was individually sabotaged against both the implementation and contract files during implementation and confirmed to fail check:arch; a client-web import in ch-client.ts was confirmed to fail both check:arch and the mirrored unit test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
…wiki Records Phase 1 (transport seam, no behavior change) as landed: CHANGELOG.md [Unreleased], an ADR-0005 addendum (Status stays Rejected -- this only adds a "Phase 1 addendum (landed)" subsection, matching the ADR's own prescription for a Rejected outcome), docs/ARCHITECTURE.md's query- execution section, and .wiki/Architecture.md + .wiki/Source-Map.md + .wiki/Decisions-and-Roadmap.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
Picks up PR #620 (chatgpt-review plan-mode fix, unrelated) so the integration branch tracks main's tip before opening the Phase 1 PR. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
ChatGPT review pass 1Reviewed head: P1 — Pin the request origin at
|
ChatGPT review pass 2Previously reviewed head: Current reviewed head: The canonical branch is still identical to the pass-1 SHA ( Pass-1 finding reassessment
P1 — URL-construction failures now perform auth work and falsely mark the connection offlineThe entry snapshot preserves values, but it does not preserve the old request-preparation boundary. Before this PR, every affected caller evaluated At the current head, This is reachable with the declared In production, Action: preserve request serialization before the first auth await and outside the fetch-rejection classification boundary, or give the transport a preparation/error distinction that lets The complete 14-file PR was rechecked; no dependency, application/UI/core/main, persisted-state, or deployment-surface changes were introduced. URL ordering and the moved stream parser remain covered for their normal inputs, and canonical CI for this unchanged head is green, but it does not cover this preparation-error boundary. VERDICT: REVISE |
authedFetch awaited ctx.getToken() before transport.send() built the request URL, so a chUrl/encodeURIComponent throw (e.g. a lone UTF-16 surrogate in settings/params) was misclassified as a transport failure: onTransportOffline fired and a token had already been read, even though fetch was never invoked. Pre-refactor, every caller built the URL before calling authedFetch, so the same throw never reached token acquisition or the offline-classifying catch. Restore that ordering: authedFetch now calls chUrl once, eagerly and discarded, before its first await, reproducing the exact original failure shape (no token read, no fetch, no onTransportOffline call). transport.send still builds the real URL again at actual send time against the live ctx.origin, unaffected since origin is concatenated, never encodeURIComponent-encoded. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
ChatGPT review pass 3Previously reviewed head: Current reviewed head: The current head is one commit ahead of the previous review. The delta is limited to Earlier findings reassessed
Complete updated PR reviewNo new actionable findings. The fix does not weaken request snapshots, retry reuse, live-origin semantics, epoch-fence adjacency, cancellation behavior, response-body ownership, stream parsing, URL ordering/encoding for valid inputs, or the architecture boundaries. The full PR still changes only the same 14 files; package/lock files and Direct cloning remained unavailable in the review environment because GitHub DNS resolution failed, so canonical metadata, commit comparisons, source files, patches, comments, and CI were inspected through the GitHub connector; the focused failure-path behavior was also reproduced independently. VERDICT: SHIP |
Summary
src/net/clickhouse-transport.types.ts:ClickHouseTransport,TransportDeps,TransportRequest,StreamCallbacks) and puts the current custom HTTP implementation behind it (src/net/clickhouse-http-transport.ts:createHttpTransport, pluschUrl/ChUrlOptsmoved verbatim and re-exported unchanged).ch-client.ts's auth/epoch/retry policy, product operations, andChCtxare otherwise untouched — no newChCtxfield, no runtime transport switch.authedFetchis the one exported-signature change (its only importers arech-client.tsinternals and its own unit test); it now takes a structured request and centralizes a synchronoussettings/paramssnapshot at entry (before its first await) as a single aliasing defense, replacing what would otherwise be a fragile per-caller responsibility.tests/unit/clickhouse-transport-contract.ts), registered against the current implementation only.build/check-boundaries.mjsgains a bare-specifier ban on@clickhouse/client-webanywhere undersrc/**(mirrored as a coverage-gated unit test, sinceignore-scripts=truemeanspretest/check:archdon't run under a barenpm test).Contract coverage
Per the approved plan (
skills/shipplan-review-loop, 5 ChatGPT passes + 1 human-approved fix-in pass — see ship-log): all 8 Phase 1 implement-list bullets claimed and delivered; the "Architecture" acceptance-criteria subset claimed in its enforceable-now form (transport contract exists, only-transport-imports-the-official-client rule active even before that file exists, OAuth/epoch/lifecycle/retry/session/cancellation/caps/exports/domain-queries remain SQL-Browser-owned, product SQL not moved into the transport). Issue "Integration"/"Browser E2E" test lists and adapter-level error translation are explicitly deferred to Phase 2+ (no second implementation exists yet to compare against).Invariant verification
All 20 issue Hard Invariants apply as "stays exactly as before" (pure refactor); the plan's own 12+-row invariant map was implemented with named enforcement + a compile-time or test proof for each (epoch-fence adjacency, frozen-lease auth isolation, live
origin/fetchaccessors vs. snapshot, entry-point settings/params snapshot, no@clickhouse/client-webundersrc/**, no dual transport, no product SQL in the transport, authored FORMAT never duplicated, raw bytes never forced through text). Independently re-verified by a targeted internal review pass (read-only, Medium-risk budget) — no findings.Sabotage cases
All plan-mandated mutations were performed and independently confirmed to fail a named test/check: epoch fence moved after
send;createHttpTransportsnapshottingoriginat construction; pre-consuming the response body insend; appending a FORMAT clause insend; adding an@clickhouse/client-webimport toch-client.ts/the transport files (five per-category checks against the new arch rule); removing/moving the entry-point settings/params snapshot.Tests
npm run check:types && npm run check:arch && npm run check:schemas && npm run check:examples && npm test && npm run build— all green (re-run twice by the coordinator: once on the worker's output, once after syncing withorigin/main). 219 test files / 7151 tests passed; coverage 100% statements / 97.14% branches / 100% functions / 100% lines, no per-file floor violations (the two new transport files don't appear in the coverage table — fully covered at 100/100/100/100).npm run test:client-spike: 91 passed, 19 skipped.npm run test:e2e(Chromium + WebKit): all green except one WebKit flake (dashboard-tree.spec.js, apage.gototimeout under full-suite resource contention) that passed in isolation on retry and doesn't touch any transport/ch-client code.npm run size-report: bundle delta ≈ +167 B gzip (+0.0%) — effectively zero, pure code movement.Plan review
--plannerdefault (Fable/high authors, ChatGPT reviews). 5 passes run (the loop's cap) without a cleanVERDICT: APPROVED— genuine, evidence-backed findings kept surfacing each round (7+2+4+3 accepted across passes 1–4, folded in each time), then 2 more accepted findings on pass 5 (a coverage-claim contradiction re: a nonexistent null-body guard, and a design improvement centralizing a request-snapshot insideauthedFetch) went unresolved when the loop hit its cap. Per the ship skill's FULL STOP protocol, the owner reviewed the outstanding findings directly and approved folding both into the plan without a 6th ChatGPT pass (both were narrow, well-evidenced, non-controversial). Conversation: https://chatgpt.com/c/6a7449f6-893c-83ed-b5f0-e534681aa009Process fix landed separately, in parallel: while reviewing pass 5's rejected findings, we found the plan-mode
chatgpt-reviewprompt had no pass-aware branching (unlikeprmode), so ChatGPT was re-litigating findings already rejected with cited evidence in the plan's own## Review responsessection. Fixed and merged as PR #620 (skills/chatgpt-reviewonly — no product code).Non-goals confirmed
No
@clickhouse/client-webdependency change (package.json/lockfile untouched — the package remains a dev-only Phase-0-spike dependency); no persisted-state/schema/deploy-surface change; no auth/OAuth semantics change;src/application/*,src/ui/*,src/core/*,src/main.tsuntouched entirely.Part of #585 (Phase 1 of 4 remaining; Phase 0 already shipped via #619). Phases 2–4 do not proceed without a new decision per ADR-0005 (Rejected).
🤖 Generated with Claude Code
https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz