Skip to content

feat(#630): make @altinity/clickhouse-http standalone-buildable, retire client-web vendor spike (phase 8) - #653

Merged
BorisTyshkevich merged 8 commits into
mainfrom
feat/630-p8-package-standalone-stabilization
Aug 8, 2026
Merged

feat(#630): make @altinity/clickhouse-http standalone-buildable, retire client-web vendor spike (phase 8)#653
BorisTyshkevich merged 8 commits into
mainfrom
feat/630-p8-package-standalone-stabilization

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

Summary

Phase 8 of #630 — the final phase of this 8-phase epic ("Epic: extract the
Fetch-native ClickHouse client"). Makes packages/clickhouse-http independently
buildable, packable, and importable outside this repository with no fallback into
SQL Browser source, then retires the @clickhouse/client-web vendor dependency and
its executable spike-tooling wiring (the migration scaffolding this whole epic has
been working toward removing).

  • packages/clickhouse-http now has its own build/test/check:types scripts,
    produces built browser-first ESM + TypeScript declarations into dist/, and its
    package.json carries publication-shaped exports/files/license/version
    metadata — exports["."] now points at dist/index.js, never TypeScript source.
  • Root build/build.mjs (esbuild bundling src/main.ts) now consumes the package via
    its built dist/**, not source — verified via the real esbuild metafile (only
    dist/*.js inputs, zero src/**.ts/@clickhouse/client-web inputs) and
    build/size-report-lib.mjs's bucket attribution (still project, unchanged).
    build/bundle.sh and deploy/install.sh — both of which call node build/build.mjs
    directly, bypassing root npm scripts — now build the package first, verified from a
    clean dist/ state.
  • A real isolated-package proof (npm pack → install into a temp fixture outside
    the workspace tree → import as ESM → compile a TypeScript consumer against the
    declarations → tsc --traceResolution proves no source fallback) is wired into
    npm run test:clickhouse-http:pack and CI.
  • tests/spike/clickhouse-client/** (33 files, the @clickhouse/client-web
    vendor-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; reusable
    generic fault scenarios became the package's own first-party Chromium+WebKit
    regression suite (packages/clickhouse-http/test/browser/**); the rest is deleted
    with zero dangling references. @clickhouse/client-web (1.23.1) is removed from
    the root manifest and lockfile.
  • 5 new/extended architecture guards (package→SQL-Browser imports; SQL-Browser
    package-deep-imports including dist/**; regrowth of a second generic
    request/URL implementation; regrowth of duplicate progress/late-exception parsers;
    @clickhouse/client-web reintroduction) — all built on the existing real-parser
    mechanism 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-install
    widened 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.mjs path-filter
    entry removed.
  • New docs/clickhouse-http-repository-extraction.md documents the tested mechanical
    handoff 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 narrow
    ADR-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-web wiring removed, #639 has a tested extraction handoff). A1-A16
already 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) that build/check-boundaries.mjs
now 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

npm run check:types                     ✅
npm run check:arch                       ✅ (1670 files, 11 rules, 0 violations; sabotage-verified live)
npm run check:schemas                    ✅
npm run check:examples                   ✅
npm test                                  ✅ 100/97.05/100/100 root coverage; 100/100/100/100 package coverage
npm run build                              ✅ dist/sql.html built
npm run test:clickhouse-http:pack          ✅ isolated-package: OK
npm run test:clickhouse-http:browser       ✅ 20/20, Chromium + WebKit
npm run test:e2e (full suite)              ✅ 215/215 Chromium; 211/211 WebKit (4 pre-existing skips)

Closes #630.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz

BorisTyshkevich and others added 3 commits August 8, 2026 20:28
…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

Copy link
Copy Markdown
Collaborator Author

ChatGPT review pass 2

Reviewed head: f73f59c7d19e80f1e5f61423bb6ab14b0268f20f
Previously reviewed head: d81a672d01e6dbea909ec8c5e6cfdbc5491e0d9f

The head-to-head delta is only the deploy/install.sh SC2155 cleanup (local tbl + separate assignment). I re-audited the complete Phase-8 surface rather than limiting the review to that one-line semantic delta.

Finding — P2: A18's new/extended guard set is not actually parser-authoritative in two places

  1. Guard 2 (src/** must not relatively deep-import any packages/clickhouse-http/**, including generated dist/**) still runs through the legacy regex extractSpecifiers. build/check-boundaries.mjs broadens Rule C's forbidden target to the whole package, but the generic RULES loop obtains specifiers from extractSpecifiers, whose four regexes are explicitly the older lexical scanner. tests/unit/clickhouse-http-package-policy.test.js's Phase-8 Rule-C sabotage also goes through its own relativeViolations/extractSpecifiers regex mirror. So the new dist/** guard inherits the exact lexical-bypass class this issue repeatedly moved away from: e.g. a valid import such as import { chUrl } from/*trivia*/'../../packages/clickhouse-http/dist/index.js' (or an escaped string-literal segment) is parsed/resolved by TS/esbuild but is not extracted by that regex path. Both production and the mirror test miss the same case.

    Fix: make Phase-8 Guard 2 a parser-backed check using the existing findModuleSpecifiers(source, relFile) (preferably a dedicated shared helper/predicate rather than widening all legacy RULES at once), resolve the decoded spec, and remove the Phase-8 Rule-C test's dependence on the local regex mirror. Add sabotage for comment trivia and an escaped specifier into dist/**.

  2. Guard 5 calls the real parser only after an unsound raw-substring prefilter, and its tests do not bind that production prefilter. Production currently does if (!source.includes(CLIENT_WEB_SPECIFIER)) continue; before findModuleSpecifiers. The same repository already documents in mightReferencePackage why an exact raw-substring gate is insufficient: string-literal escapes decode to the target specifier even when the raw source does not contain it. client-web-retirement-policy.test.js repeats the raw source.includes(...) gate for its real-tree scan, while its sabotage cases call findModuleSpecifiers directly and therefore skip the production prefilter entirely. Also, despite the Guard-1/3/4 drift-bind comment referring to a Guard-5 equivalent, the current retirement-policy file has no checkerSource binding for this production source-scan block.

    Fix: gate Guard 5 with the already-shared escape-aware mightReferencePackage(source, CLIENT_WEB_SPECIFIER) (or export one complete Guard-5 source predicate), and add a sabotage that exercises the real prefilter+parser composition plus a production-wiring sentinel if the checker remains a top-level script.

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

  • Guards 1/3/4 now have the production-text drift bind added in d81a672...; confirmed present.
  • Guard 5's manifest/lock/script predicates are shared exports in build/lib/check-legacy-owners.mjs and production calls those shared predicates; the earlier duplicate-logic finding is fixed.
  • The current f73f59c... SC2155 change is behavior-neutral and preserves deploy/install.sh's package-build-before-app-build ordering.
  • Both bypass production paths (build/bundle.sh, deploy/install.sh) explicitly build @altinity/clickhouse-http before build/build.mjs.
  • Package exports point to dist/index.js/dist/index.d.ts; root build/type/test scripts establish the package build prerequisite.
  • test/isolated-package.mjs performs a real npm pack, installs under OS temp outside the workspace with NODE_PATH cleared, runs an ESM consumer with exact import.meta.resolve, compiles a NodeNext TypeScript consumer, checks resolution reaches installed dist/index.d.ts, and cleans up.
  • The package browser harness serves/imports /dist/index.js directly and the regression suite owns scenarios 1–9 plus invalid-UTF8 fidelity on Chromium/WebKit; root auth/export suites consume the moved fault server from the package test tree.
  • @clickhouse/client-web is absent from the current root manifest and removed from the lockfile; executable spike scripts/tree are retired. The ADR change is a narrow Phase-8 addendum rather than a rewrite of the historical decision block.
  • CI keeps root PR e2e Chromium-only while installing WebKit for the package's C/W suite, adds the isolated-pack proof, and removes the old spike fixture path filter. The current-head CI run is green.
  • build/size-report-lib.mjs continues to classify all packages/** inputs as first-party project, so package dist/** does not become an external bucket.

No other actionable regression found in the updated PR.

VERDICT: REVISE

BorisTyshkevich and others added 2 commits August 8, 2026 22:10
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

Copy link
Copy Markdown
Collaborator Author

ChatGPT review pass 3

Reviewed head: 7f89ff879340e9b41f39de43f6db981accc574ff
Previously reviewed head: f73f59c7d19e80f1e5f61423bb6ab14b0268f20f

The new head is two commits ahead of the previous review. The delta is confined to four architecture-policy/test files: build/check-boundaries.mjs, build/lib/check-legacy-owners.mjs, tests/unit/clickhouse-http-package-policy.test.js, and tests/unit/client-web-retirement-policy.test.js. I re-used the prior full-PR audit for byte-unchanged surfaces and re-audited the complete current Phase-8 contract around the new guard changes. Current-head CI is green, including test/pack/build, package Chromium+WebKit, root e2e, bundle/shellcheck, size, docker-smoke, and the final gate.

Finding 1 — P2: pass-2 Guard 5 soundness finding is only partially fixed

The production-wiring/drift part is fixed: Guard 5 and its test now share mightReferenceRetiredTopLevelApi, the sabotage helper exercises prefilter → findModuleSpecifiers, and a checkerSource sentinel pins production wiring.

But the actual false-negative from pass 2 remains. mightReferenceRetiredTopLevelApi is a raw source.includes(name) prefilter and its own doc explicitly says it does not widen for backslash-escaped spellings. That convention is defensible for deliberately obfuscated identifiers, which is what the helper was designed for, but Guard 5 is feeding it a module-specifier string (@clickhouse/client-web). This repository already has mightReferencePackage precisely because raw-substring prefilters are unsound for module specifiers: valid string literals can encode a character with \xNN/\uNNNN, the TypeScript parser decodes node.text, but a raw source.includes(CLIENT_WEB_SPECIFIER)-equivalent gate never invokes the parser.

Concrete bypass:

import { createClient } from '@clickhouse/client-w\x65b';

The module specifier decodes to exactly @clickhouse/client-web; findModuleSpecifiers would report it if called, but the current Guard-5 prefilter returns false first. The new tests share the same prefilter and do not include an escaped-specifier sabotage, so they now eliminate drift without eliminating this correctness hole.

Fix: use the existing escape-aware mightReferencePackage(source, CLIENT_WEB_SPECIFIER) (or an equivalently sound shared package-specifier prefilter), keep the current production-wiring sentinel, and add a full-pipeline sabotage using an escaped vendor specifier.

Finding 2 — P2: Guard 2 can still deep-import the workspace package through its node_modules link

The regex/parser issue from pass 2 is fixed: Rule C/Guard 2 is now a dedicated findModuleSpecifiers path, with comment-trivia and escaped-path sabotage, and the follow-up prefilter preserves those cases while fixing the CI timeout.

However, the guard compares the lexical result of resolveRelative() only against packages/clickhouse-http/**. This repository's lockfile explicitly declares node_modules/@altinity/clickhouse-http as link: true, resolved to packages/clickhouse-http. A normal relative import can therefore bypass the public-export-only invariant through that workspace link:

import { chUrl } from '../../node_modules/@altinity/clickhouse-http/src/index.js';

From src/net/**, the new prefilter runs (the source contains clickhouse-http) and the real parser sees the relative specifier, but resolveRelative() returns a lexical node_modules/@altinity/clickhouse-http/... path. The subsequent check only accepts relResolved === 'packages/clickhouse-http' or that prefix, so it does not flag the import. Rule D does not catch it either because the specifier is relative rather than the bare @altinity/clickhouse-http/... form. esbuild/TypeScript can follow the workspace link, so this is a real deep import of package internals while bypassing the package's "." export.

Fix: make Guard 2 compare canonical package identity, not only lexical repo paths. For existing targets, resolve symlinks before the forbidden-prefix comparison; also explicitly treat relative resolution under node_modules/@altinity/clickhouse-http/** as the workspace package so the guard remains fail-closed even when generated dist/** has not been built yet. Add a sabotage import through the workspace link.

Earlier findings / full-PR regression re-check

  • Pass-2 Guard 2 lexical-scanner finding: fixed; the dedicated Guard-2 path is parser-backed and has comment/escape sabotage plus production drift binding.
  • Guard-2 follow-up CI timeout: fixed by the shared escape-aware forbidden-directory prefilter; current-head CI is green.
  • Pass-2 Guard 5 production-drift finding: fixed; only the raw-substring soundness half above remains.
  • Pre-PR Guards 1/3/4 drift sentinels and shared Guard-5 manifest/lock/script predicates remain present.
  • The two new commits do not touch the package build/export surface, isolated-package proof, browser suite, root build wrappers, CI policy, vendor deletion, docs/evidence, or migration aliases. Those surfaces are byte-identical to the previously reviewed head and retain the prior audit result.
  • docs/evidence/585/** is still absent from the PR changed-file set; historical evidence remains untouched.
  • packages/clickhouse-http/package.json still targets built dist/index.js/dist/index.d.ts, retains private: true/0.0.0, and has no runtime dependency map.

No additional runtime/package regression found beyond the two architecture-boundary gaps above.

VERDICT: REVISE

BorisTyshkevich and others added 2 commits August 8, 2026 22:54
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
@BorisTyshkevich

Copy link
Copy Markdown
Collaborator Author

ChatGPT review pass 3 (manual continuation — CLI pass cap hit)

The CLI's own chatgpt-review pr call for pass 3 was hard-rejected before reaching
ChatGPT ("PR review sessions permit at most three total passes" — an earlier pass
needed an internal retry that consumed an extra session slot). Continued the same
conversation manually instead of starting a fresh session, per this session's own
"one unit, one ChatGPT conversation" convention.

Review scope: diff from 7f89ff8 (pass 2's reviewed head) to 2df6beb (current
head) — 3 changed files: build/check-boundaries.mjs,
tests/unit/clickhouse-http-package-policy.test.js,
tests/unit/client-web-retirement-policy.test.js.

Finding 1 (Guard 5 escaped-specifier bypass) — genuinely resolved. Production now
gates on the escape-aware mightReferencePackage; the new sabotage test proves the old
raw-substring false negative is gone; a drift sentinel pins the production wiring.

Finding 2 (Guard 2 node_modules workspace-symlink bypass) — genuinely resolved.
resolveRelative() now canonicalizes via fs.realpathSync() before the prefix
comparison; the sabotage test uses the real, already-installed symlink (not a virtual
fixture) and would fail against the old lexical-only implementation; a sentinel pins
fs.realpathSync in the production function.

2df6beb (unrelated hook-timeout widening) — confirmed timeout-only, no weakened
assertions or skipped work; current-head CI green. One non-blocking precision note:
fs.realpathSync() adds bounded filesystem work during cache warming, so "unchanged
work" wasn't perfectly literal — doesn't affect the conclusion.

No remaining actionable defect found in this delta.

VERDICT: SHIP

Conversation: https://chatgpt.com/c/6a775c43-16a8-83eb-98a2-105a5570df66

@BorisTyshkevich
BorisTyshkevich merged commit ec18234 into main Aug 8, 2026
8 checks passed
@BorisTyshkevich
BorisTyshkevich deleted the feat/630-p8-package-standalone-stabilization branch August 8, 2026 21:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Epic: extract the Fetch-native ClickHouse client into a reusable package

1 participant