feat(#630): make @altinity/clickhouse-http standalone-buildable, retire client-web vendor spike (phase 8) - #653
Conversation
…etire the client-web vendor spike Phase 8 (final phase of #630) claims A17/A18: A17 — the package now builds/packs/installs/imports/typechecks in isolation with no SQL Browser root-source fallback: - packages/clickhouse-http gets a package-local esbuild build (unbundled browser-first ESM to dist/**), a declaration-only tsc build, package-local tsconfig.json/tsconfig.build.json, vitest.config.ts (100/95/90/100 per file), .gitignore, README.md, and a real (copied, not symlinked) LICENSE. Its manifest's main/types/exports["."] all point at built dist/**, never source. - Root package.json composes build:clickhouse-http as an explicit prerequisite of build/size-report/dev/local/test/test:watch/check:types (this environment's ignore-scripts=true means lifecycle hooks never run this). build/bundle.sh and deploy/install.sh — both of which call node build/build.mjs directly, bypassing root npm scripts — get the same explicit prerequisite line, verified from a clean package dist/ state. - Root tsconfig.json/tests/vitest.config.ts drop package source from their own type-check/coverage trees; package-local tooling owns it instead. - A new packages/clickhouse-http/test/isolated-package.mjs (npm run test:pack) builds the package, runs a real `npm pack`, installs the tarball into a fixture OUTSIDE this repository, imports it as ESM, and compiles a TypeScript consumer against its declarations against --traceResolution output — proving neither runtime nor type resolution ever falls back into this repository's source. - A new packages/clickhouse-http/test/browser/** Chromium+WebKit regression suite serves the package's own generated dist/** directly (harness.html imports /dist/index.js, no import map, no vendor client, no Docker/live ClickHouse) — ported from the former root tests/e2e/clickhouse-http-transport.{html,spec.js}, which splits into that package suite plus a narrower root tests/e2e/authenticated-clickhouse-request.{html,spec.js} for SQL Browser's own authentication-policy variants. fault-server.mjs moves from the retired spike to packages/clickhouse-http/test/browser/ as this package's own first-party deterministic fixture (still importable from the two root e2e specs that need it). A18 — final ownership/architecture guards and documentation are reconciled, the obsolete executable @clickhouse/client-web wiring is removed, and #639's extraction handoff is written: - Removes the migration-only ch-client.ts forwarding aliases (chUrl/parseExceptionText/findExceptionFrame); export-service.ts imports findExceptionFrame directly from the package under one narrow, named Rule-D exception (PHASE8_NARROW_RULE_D_EXCEPTIONS) rather than through that retired gateway. - Five architecture guards, all real-parser-backed (build/lib/check-legacy-owners.mjs's new findModuleSpecifiers/ findTransportSurfaceOwnershipViolations, never a hand-rolled regex scanner): package containment broadens to the package's own test/**/build.mjs/vitest.config.ts (Guard 1); the package relative-deep- import ban widens from src/** to the whole package directory, catching a dist/** escape a source-only ban would miss (Guard 2); root-wide declaration/re-export ownership for the historical chUrl/ createHttpTransport/ClickHouseTransport/TransportDeps/TransportRequest transport surface, exempting the sanctioned package import itself (Guard 3); the same root-wide ownership rule for the moved progress- stream/exception-parsing primitives (Guard 4); and the @clickhouse/client-web ban's former "future official transport file" allowlist is deleted, its scan widened to src/**+packages/clickhouse-http/**(excl. dist/**)+tests/**+build/**, plus structural manifest/lock/script/directory checks (Guard 5). - Removes the @clickhouse/client-web devDependency, its four npm scripts, the whole executable tests/spike/clickhouse-client/ directory (per the plan's exact file-by-file disposition table), and the candidate-build-only additionalNotices/--notices plumbing in build/build.mjs/ build/size-report.mjs. Regenerates package-lock.json. tests/unit/client-web-spike-policy.test.js (which enforced the opposite, spike-executable state) is rewritten as tests/unit/client-web-retirement-policy.test.js. - docs/evidence/585/** and ADR-0005's Rejected decision/historical content are untouched; a narrow Phase 8 current-state addendum documents the executable retirement. .wiki/Decisions-and-Roadmap.md gets the Phase 8 paragraph completing the #630 roadmap entry. Every checkpoint was verified against a clean package dist/ state (build/bundle.sh, deploy/install.sh) with a real sabotage-and-restore round trip on both wrappers and on the package export shape, plus the package's own Chromium+WebKit suite and the two retargeted root e2e specs, all green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
… the #639 extraction handoff Completes issue #630's own documentation/CI reconciliation for Phase 8: - .github/workflows/ci.yml: the test job runs the new isolated-package proof (npm run test:clickhouse-http:pack) between npm test and the build step, CI-gating A17's real tarball/isolation proof; the e2e path filter's dedicated tests/spike/clickhouse-client/fault-server.mjs entry is removed (the fixture moved under packages/**, already covered); PR browser installation widens from Chromium-only to Chromium+WebKit specifically for the new package regression suite (root SQL Browser e2e stays Chromium-only on PRs, unchanged cost policy); the package's own Chromium+WebKit regression suite runs on every applicable CI event; and the bundle job's shellcheck command now covers both modified build wrappers (build/bundle.sh, deploy/install.sh) alongside the existing root installer. - CLAUDE.md hard rule 4 and the repo map: describes the package's final independent build/type/test boundary instead of "source currently bundled directly into the application" (Phase 8's own target state), and adds the package's test/build.mjs/tsconfig/vitest.config.ts row. - docs/ARCHITECTURE.md: new "Standalone package build and final retirement (#630 Phase 8)" section (A17/A18 definitions of done in this repository's own words) plus a corrected Build section (root esbuild resolves the package's BUILT dist/**, never source, since Phase 8). - .wiki/Architecture.md and .wiki/Source-Map.md: Phase 8 build/retirement notes, and a fix to a pre-existing stale claim (the convenience queryJson/queryText/queryProgress client methods were previously documented as "Phase 8's concern" for consumption — Phase 8's own plan does not add that consumer; corrected to a genuinely open item instead of reopening or misattributing it). - docs/clickhouse-http-repository-extraction.md (new): the tested mechanical extraction handoff for issue #639 — package tree that moves unchanged, the mechanical cp -a operation, every command actually run during Phase 8 acceptance (not aspirational prose), what test:pack's isolated-package proof specifically checks, every SQL Browser consumer reference #639 must retarget, #639's own release-work scope, and the rollback story. - CHANGELOG.md [Unreleased]: Phase 8 entry (closes #630, claims A17/A18). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
…tructural predicate Two pre-PR review findings on Phase 8's new architecture guards: - Guards 1/3/4 (tests/unit/clickhouse-http-package-policy.test.js) called the real shared parser helpers but had no checkerSource-text sentinel proving build/check-boundaries.mjs still wires those helpers into its own Guard 1/3/4 rule blocks, unlike Rules A-D and Guard 5. Added a new describe block asserting the exact production markers (guard1Targets/guard34Names, the two real-parser calls, and each violation message) still exist in check-boundaries.mjs's source text. - client-web-retirement-policy.test.js's structural manifest/lock/script sabotage cases re-derived the same boolean predicates build/check-boundaries.mjs's Guard 5 structural check uses, without ever calling that production code. Extracted the three predicates (manifestDependencyFields, lockHasPackage, retiredClientSpikeScriptNames) into build/lib/check-legacy-owners.mjs so both check-boundaries.mjs and the test call the one real implementation; zero behavior change to the emitted violation messages. Verified each new/changed test fails for the right reason when the corresponding production code is sabotaged, then restored to original bytes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
…llcheck SC2155) This PR added deploy/install.sh to the CI shellcheck lint scope (it previously covered only install.sh and build/bundle.sh) — that surfaced a pre-existing SC2155 warning on line 137 that was never linted before. Split the declare+assign so the command substitution's own exit status isn't masked by `local`'s always-successful return, per shellcheck's own suggested fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
ChatGPT review pass 2Reviewed head: The head-to-head delta is only the Finding — P2: A18's new/extended guard set is not actually parser-authoritative in two places
This is acceptance-relevant rather than stylistic: the PR explicitly claims all five new/extended Phase-8 architecture guards use the existing real-parser mechanism, and A18 is the final architecture-hardening criterion. Earlier findings / regression re-check
No other actionable regression found in the updated PR. VERDICT: REVISE |
Guard 2 (Rule C's relative deep-import ban into packages/clickhouse-http, including generated dist/**) moves off the generic RULES loop's hand-rolled extractSpecifiers regex onto its own dedicated findModuleSpecifiers-backed block: a comment sitting between import/export and the specifier, or an escaped string-literal segment, defeated the regex despite this file's own stated Phase 8 design goal. The in-suite mirror gets the same treatment (relativeViolationsParserBacked) plus comment-trivia and escaped-specifier sabotage cases, and the Guard 1/3/4 drift-bind block is extended to also pin Guard 2's production wiring. Guard 5's (@clickhouse/client-web reintroduction ban) prefilter now calls the shared mightReferenceRetiredTopLevelApi helper instead of an inline source.includes(...) check, closing the same independently-hand-copied-prefilter drift risk Rule D already closed; its test's sabotage probes now exercise the full prefilter-to-parser path instead of calling findModuleSpecifiers directly, and a new checkerSource drift-bind block pins its production wiring the same way Guards 1-4 already are. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
…oreAll timeout Rule C/Guard 2's parser-backed relative-import mirror (added by the previous review-pass-1 commit) shipped with no pre-filter at all, unlike its three sibling real-parser checks in the same file's beforeAll — it had to parse every file under src/** unconditionally, making it the most expensive of the four cache-warming calls and, stacked with the other three, pushing CI's already-generous 30000ms setup timeout past its limit under GitHub Actions' more constrained scheduling (did not reproduce locally). Add mightReferenceForbiddenRelativeDir (build/lib/check-legacy-owners.mjs), the same accepted-risk textual pre-filter shape as mightReferencePackage/ mightReferenceRetiredTopLevelApi already use: a relative import can only resolve into a forbidden directory by spelling its leaf segment somewhere in the source, once any backslash escape is decoded, so "no leaf substring AND no backslash anywhere" is the only combination that can safely skip the real parser. Wire it into both the production check-boundaries.mjs block and the test's own relativeViolationsParserBacked mirror, plus a checkerSource drift-bind sentinel proving the production wiring calls it. Measured locally: cuts the files reaching the real parser under src/** by ~68% (225 -> ~70), beforeAll wall time for this one call ~7.1s -> ~2.3s. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
ChatGPT review pass 3Reviewed head: The new head is two commits ahead of the previous review. The delta is confined to four architecture-policy/test files: Finding 1 — P2: pass-2 Guard 5 soundness finding is only partially fixedThe production-wiring/drift part is fixed: Guard 5 and its test now share But the actual false-negative from pass 2 remains. Concrete bypass: import { createClient } from '@clickhouse/client-w\x65b';The module specifier decodes to exactly Fix: use the existing escape-aware Finding 2 — P2: Guard 2 can still deep-import the workspace package through its
|
Guard 5's real-parser prefilter gated on mightReferenceRetiredTopLevelApi, a bare source.includes(name) substring test with no escape awareness — an import spelled through a hex/Unicode escape (e.g. '@clickhouse/client-w\x65b') never contains the raw 'client-web' substring, so the parser (which would have decoded and caught it) was silently skipped. Switched to the shared, escape-aware mightReferencePackage, matching Rule D's own prefilter. Guard 2's resolveRelative returned the lexical path only, with no symlink canonicalization, so a relative import reaching packages/clickhouse-http through the real node_modules/@altinity/clickhouse-http workspace-link symlink (package-lock.json's "link": true entry) resolved to a node_modules/... path that never matched the packages/clickhouse-http prefix Guard 2 compares against, even though it is the same file on disk. resolveRelative now canonicalizes any existing candidate via fs.realpathSync before the comparison. Added a sabotage test for each: an escaped-specifier probe for Guard 5, and a real (already-installed) workspace-link-symlink probe for Guard 2, plus drift-bind assertions pinning both fixes in build/check-boundaries.mjs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
CI failed on this PR's review-pass-2 fix commit with "Hook timed out in 30000ms" in tests/unit/clickhouse-http-package-policy.test.js — the same beforeAll already hardened twice for this exact failure class (Phase 5's original 5000ms->30000ms bump; this PR's own 68% real-parser prefilter reduction). This run measured 31783ms, and the immediately prior CI run of the identical beforeAll body passed comfortably. The four cache-warming calls are already prefiltered and memoized per real tree, so there is no further unbounded work left to cut — this is CI runner scheduling variance around a genuinely bounded, CPU-bound child-process-spawn cost, not a regression. Widened the explicit ceiling to 60000ms, the same class of fix as the original occurrence. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
ChatGPT review pass 3 (manual continuation — CLI pass cap hit)The CLI's own Review scope: diff from Finding 1 (Guard 5 escaped-specifier bypass) — genuinely resolved. Production now Finding 2 (Guard 2 2df6beb (unrelated hook-timeout widening) — confirmed timeout-only, no weakened No remaining actionable defect found in this delta. VERDICT: SHIP Conversation: https://chatgpt.com/c/6a775c43-16a8-83eb-98a2-105a5570df66 |
Summary
Phase 8 of #630 — the final phase of this 8-phase epic ("Epic: extract the
Fetch-native ClickHouse client"). Makes
packages/clickhouse-httpindependentlybuildable, packable, and importable outside this repository with no fallback into
SQL Browser source, then retires the
@clickhouse/client-webvendor dependency andits executable spike-tooling wiring (the migration scaffolding this whole epic has
been working toward removing).
packages/clickhouse-httpnow has its ownbuild/test/check:typesscripts,produces built browser-first ESM + TypeScript declarations into
dist/, and itspackage.jsoncarries publication-shapedexports/files/license/versionmetadata —
exports["."]now points atdist/index.js, never TypeScript source.build/build.mjs(esbuild bundlingsrc/main.ts) now consumes the package viaits built
dist/**, not source — verified via the real esbuild metafile (onlydist/*.jsinputs, zerosrc/**.ts/@clickhouse/client-webinputs) andbuild/size-report-lib.mjs's bucket attribution (stillproject, unchanged).build/bundle.shanddeploy/install.sh— both of which callnode build/build.mjsdirectly, bypassing root npm scripts — now build the package first, verified from a
clean
dist/state.npm pack→ install into a temp fixture outsidethe workspace tree → import as ESM → compile a TypeScript consumer against the
declarations →
tsc --traceResolutionproves no source fallback) is wired intonpm run test:clickhouse-http:packand CI.tests/spike/clickhouse-client/**(33 files, the@clickhouse/client-webvendor-comparison spike from ADR-0005: adopt @clickhouse/client-web behind the SQL Browser transport adapter #585) is retired: historical evidence
(
docs/evidence/585/**, ADR-0005's Rejected decision) is byte-untouched; reusablegeneric fault scenarios became the package's own first-party Chromium+WebKit
regression suite (
packages/clickhouse-http/test/browser/**); the rest is deletedwith zero dangling references.
@clickhouse/client-web(1.23.1) is removed fromthe root manifest and lockfile.
package-deep-imports including
dist/**; regrowth of a second genericrequest/URL implementation; regrowth of duplicate progress/late-exception parsers;
@clickhouse/client-webreintroduction) — all built on the existing real-parsermechanism in
build/lib/check-legacy-owners.mjs, no new hand-rolled scanner..github/workflows/ci.yml: pack-test added to the test job; PR browser-installwidened to Chromium+WebKit for the package's own new suite only (root e2e's
Chromium-only-on-PR policy is unchanged); the retired
fault-server.mjspath-filterentry removed.
docs/clickhouse-http-repository-extraction.mddocuments the tested mechanicalhandoff for follow-up: move @altinity/clickhouse-http into a dedicated Altinity repository #639 — every command it lists is a command this PR's own test suite
actually runs.
docs/ARCHITECTURE.md,CLAUDE.md,.wiki/*, package README, and a narrowADR-0005 current-state addendum (historical content/dates preserved) all reconciled.
Claims A17 (package builds/packs/installs/imports/typechecks in isolation with no
root-source fallback) and A18 (final architecture guards/docs reconciled, obsolete
@clickhouse/client-webwiring removed, #639 has a tested extraction handoff). A1-A16already shipped across Phases 1-7.
At Phase 8 completion, issue #630 itself is complete. The package still lives in
this repository — #639 (creating a separate repository, publishing, and the SQL
Browser consumer cutover to the released package) remains explicitly out of scope,
exactly as the issue separates the two.
Pre-PR review
One high-effort, read-only pass over the complete 86-file diff — every load-bearing
claim in the plan's A17/A18 definition of done was checked against actual execution
(clean-state builds, the real esbuild metafile, a genuine external-install proof, the
browser suite, byte-diffing historical evidence), not taken on faith. Found 2 minor
test-design gaps (both fixed): Guards 1/3/4 lacked the same production-drift sentinel
this repo's existing Rules A-D/Guard 5 already use, and Guard 5's structural
manifest/lock/script check had 3 sabotage tests re-deriving its own logic rather than
calling the real predicate — fixed by extracting that predicate into an exported,
shared function (
build/lib/check-legacy-owners.mjs) thatbuild/check-boundaries.mjsnow calls directly, eliminating the duplicate-logic risk entirely rather than just
detecting drift after the fact. Both fixes verified via real sabotage-of-production-code
probes, confirmed to fail for the right reason, then restored.
Gate
Closes #630.
Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz