feat(#630): extract progress-stream and late-exception primitives (phase 3) - #644
feat(#630): extract progress-stream and late-exception primitives (phase 3)#644BorisTyshkevich wants to merge 5 commits into
Conversation
…o @altinity/clickhouse-http Phase 3 of the #630 extraction: streamLines() (the progress-bearing JSON-lines read loop), parseExceptionText(), and findExceptionFrame() (now byte-oriented, no caller-side latin1 conversion) move into the package alongside their StreamLine/StreamCallbacks wire types. This is a real move+delete, not an additive layer: src/net/clickhouse-http- transport.ts and clickhouse-transport.types.ts are now request/send- only, and src/core/stream.ts no longer declares the moved protocol types/primitives — applyStreamLine narrows an open Record<string, unknown> parsed record instead of re-declaring a second wire type. runQuery calls the package's streamLines directly; export-service.ts's streamToFile calls the package's byte-oriented findExceptionFrame through ch-client.ts's zero-logic re-export gateway, deleting its own latin1 converter. StreamResult, row caps, percentages, raw/result presentation, editor-caret positioning, and auth-expiry/ denial UI policy all stay in SQL Browser, unmoved. build/check-boundaries.mjs gains a narrow legacy-owner rule rejecting the three former owners (the transport adapter, its type contract, and core/stream.ts) from regaining any moved identifier, mirrored in clickhouse-http-package-policy.test.js with sabotage probes and a checker-source drift binding. Tests move (not duplicate) across the package boundary: the stream-mechanics suite into clickhouse-http-progress-stream.test.ts, the exception-parsing suite into clickhouse-http-exceptions.test.ts (byte fixtures, plus new byte-boundary and false-positive coverage), and the shared transport contract suite drops its one stream case to become request/send-only. The real-browser Chromium/WebKit fault harness now streams through the package's streamLines directly instead of a transport method. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
…g real code A two-pass regex strip (block comments globally, then // lines) let a /*-shaped substring inside an unstripped // comment (this file's own `src/core/**` prose) act as a spurious block-comment opener, silently deleting real code up to the next genuine closing */ — which let a reintroduced `export interface StreamLine` sabotage probe slip past both check:arch and its test mirror undetected. A single alternation- based regex pass fixes it: the real // is matched (and its newline- stop keeps the match on one line) before the engine ever reaches a later /*-shaped trap on that same line. Caught during this phase's own required sabotage checks, not by an external review pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
ChatGPT review pass 1Reviewed head: [P2 / acceptance blocker] The new legacy-owner guard still has false negatives around string literals
source.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '')The Phase 3 mirror in I reproduced two false negatives with the exact regex: const marker = "/*";
export interface StreamLine {}
const end = "*/";The stripper reduces that to const u = "https://example"; export function streamLines() {}is reduced to This is not #643's deferred pre-existing scope: that issue covers two other untouched two-pass helpers. This false-negative class is in the new Phase 3 guard itself, whose stated purpose is to mechanically enforce the one-production-owner invariant. The current production code does satisfy the invariant, but the PR's claimed regression proof does not. Action: make this narrow rule token-aware (for example, a TypeScript scanner or a small lexical scanner that recognizes comments only outside strings/templates/regex literals), and add sabotage probes for at least the multiline Other adversarial checks were clean at this head: the transport has no stream forwarding member; VERDICT: REVISE |
The Phase 3 legacy-owner guard's stripComments was a naive
/\*[\s\S]*?\*\/|\/\/.*/ alternation with no notion of "inside a string",
so a /* , */, or // substring that only looked like a comment delimiter
because it sat inside a string literal could swallow a real forbidden
declaration and produce a false-clean scan in both build/check-boundaries.mjs
and its independent test-file mirror (they shared the same regex, so neither
path actually proved the one-owner invariant for inputs shaped this way).
Replace it with a hand-rolled lexical scanner in both copies that tracks
string/template-literal (including nested ${...}) boundaries before treating
/* or // as a comment opener, and add the two reproduced bypasses as
regression tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
ChatGPT review pass 2Previously reviewed head: [P2 / acceptance blocker] The legacy-owner scanner still has a regex-literal false negativeThe pass-1 string-literal bypasses are fixed: the new hand-rolled scanner correctly preserves However, the earlier finding is not fully resolved because the scanner still has no JavaScript/TypeScript regex-literal state. This was part of the pass-1 requested fix: comments must only be recognized outside strings/templates/regex literals. I reproduced a remaining false negative against the exact new const re = /\/\//; export function streamLines() {}The regex literal and the subsequent This is acceptance-relevant for the same reason as pass 1: the current production tree still satisfies the one-owner invariant, but the new Phase 3 architecture gate that claims to mechanically enforce that invariant remains bypassable by syntactically valid source. The fix commit only changed Action: use a token-aware scanner/parser that understands regex literals (the TypeScript scanner is the simplest robust option here), or complete the lexical scanner with correct regex-vs-division recognition. Add the probe above to both the production-checker sabotage coverage and the mirror's regression cases. Reassessment of pass-1 checks
Canonical CI for the new head's merge ref is green: VERDICT: REVISE |
The Phase 3 legacy-owner scanner's stripComments (production build/check-boundaries.mjs and its identical mirror in clickhouse-http-package-policy.test.js) tracked string/template-literal state but had no regex-literal state: a `/` inside a regex literal that happened to sit next to another `/` (e.g. the escaped-slash-then-closing- delimiter tail of `/\/\//`) was read as a `//` line-comment opener, silently deleting everything after it on that line -- including a forbidden declaration the rule exists to catch. Added a regexAllowedAfter heuristic (division-vs-regex disambiguation from the last emitted token, the same class of context Acorn's exprAllowed resolves) and a real regex-literal consumer (escape- and character-class-aware) to both copies, plus a sabotage regression test reproducing the exact bypass and a companion test proving a real division (`read / total`, as it appears in stream.ts) is not misclassified as a regex open. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
ChatGPT review pass 3Previously reviewed head: [P2 / acceptance blocker] The regex-aware legacy-owner scanner is still bypassable after control-flow
|
regexAllowedAfter() in build/check-boundaries.mjs's comment-stripping
scanner (and its lexically-identical test mirror) unconditionally
treated a `/` right after `)` as division, even when that `)` closed
an if/while/for condition and the grammar actually expects the
controlled statement next — which may itself open with a regex
literal. `if (true) /\//.test("/"); export function streamLines() {}`
is valid JS that the old scanner misread: it took the `/` after `)` as
division, then read the escaped-slash-then-closing-slash pair as a
`//` line-comment opener, deleting the rest of the line including the
streamLines declaration the Phase 3 legacy-owner rule exists to catch.
Track, per open `(`, whether the preceding word was if/while/for, and
let regexAllowedAfter consult that flag instead of a blanket `)` ->
false. Added the exact repro as a regression case plus a negative case
proving a plain call/grouping paren (`fn(total) / total`) still reads
as division.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
|
I think we should stop extending the hand-written JS lexer in The last three review passes all found valid bypasses in the same mechanism:
Each individual fix was reasonable, but together they show that the checker is gradually reimplementing JavaScript lexical grammar. That complexity is unrelated to Please replace that mechanism with the following simpler approach. Required implementation
MotivationThe invariant we need to enforce is simple: these moved primitives must have one owner and must not silently reappear in their previous SQL Browser files. A custom lexer is a poor fit for that invariant. Correctly distinguishing comments, strings, templates, regex literals, division, and statement/expression contexts requires essentially reproducing part of the JavaScript grammar. The three review passes have already demonstrated that this produces an open-ended sequence of edge cases. Using the repository's existing TypeScript parser gives us the lexical correctness for free and keeps the policy check small and reviewable. It does not make the network library depend on a parser: TypeScript is used only by development/CI tooling to inspect TypeScript source. The desired outcome is therefore fewer lines of custom parsing code, one implementation of the ownership check, and a narrowly scoped mechanical guard that directly expresses the Phase 3 architectural invariant. |
What & why
Part of #630 — replacing the rejected
@clickhouse/client-webadoption path (#585) witha first-party, Fetch-native
@altinity/clickhouse-httppackage extracted from genericprotocol code SQL Browser already maintains.
This is Phase 3 of 8. It moves
streamLines(),parseExceptionText(),findExceptionFrame(), and theStreamLine/StreamCallbacks/ProgressMetaColumnwiretypes out of
src/net/clickhouse-http-transport.ts/src/core/stream.tsintopackages/clickhouse-http/src/{progress-stream,exceptions}.ts— a real move+delete,not an additive compatibility layer.
StreamResult, row caps, progress percentages,raw/result presentation, editor-caret positioning, and auth-expiry UI policy all remain
in SQL Browser, unmoved.
Claims: A6 (progress-stream decoding has one package implementation while SQL
Browser retains result/view state), A7 (ClickHouse HTTP exception parsing/late-exception
framing are package-owned and byte-safe). A8–A18 remain deferred.
Approved plan: see the ship-log comment on #630. Plan review again needed manual
recovery — the automated ChatGPT-author/Fable loop hit repeated timeouts on a very long
generation (not a Chrome crash this time); the completed response was read directly
from the DOM and Fable invoked directly, same recovery pattern as Phase 2. Fable
approved on the 2nd real review pass after 2 findings (a mis-cited enforcement
mechanism; a missed test-migration site) were incorporated and re-verified.
Contract coverage
streamLines/parseExceptionText/findExceptionFrameownercheck-boundaries.mjslegacy-owner rule + mirrorexport interface StreamLineincore/stream.ts→ caught bycheck:archand the mirror (verified independently by both the coordinator and the pre-PR reviewer)StreamLineexport removedcheck:typesovertests/unit/**+tests/spike/clickhouse-client/**check:typesfails (TS2305)findExceptionFramecomputes offsets via a lossless latin1 byte↔char view, neverTextDecoder, over the clean prefixtests/unit/clickhouse-http-exceptions.test.tsinvalid-UTF-8 + multibyte-message boundary casesTextDecoder→ 2 tests fail (garbled message, off-by-2 byte count)StreamResult/row caps/percentages/parseErrorPos/auth-expiry policy stay incore/stream.ts, logic byte-for-byte unchangedTests
npm run check:types && npm run check:arch && npm run check:schemas && npm run check:examples && npm test && npm run build— green (7232 tests, 100%/97.14%/100%/100% coverage).npm run test:e2e -- tests/e2e/clickhouse-http-transport.spec.js --project=chromium --project=webkit— 18/18 passed; Scenario 6 now exercises the package'sstreamLines()through the real production call path.--project=chromium --project=webkit— 432 passed, 4 skipped (pre-existing, unrelated).Build
npm run buildsucceeds — single self-containeddist/sql.html, no new runtime dependency.Invariant/sabotage verification
Verified three times: once by the implementer (who found and fixed a real bug in its
own sabotage-check tooling — a two-pass comment-stripping regex that let a
/*-shapedsubstring inside an unstripped
//comment silently swallow real code, masking asabotage; fixed with a single alternation-based regex, commit
9ff449e), once by thecoordinator (full gate, direct reproduction of the
StreamLine-reintroduction sabotagepost-fix, e2e), and once by a separate high-effort pre-PR review confirming the
production cutover is real (not forwarding), the result-policy boundary held with
unchanged logic, the byte-accurate boundary is genuinely byte-safe, the comment-stripper
fix is correct and applied identically in both files, and every compile-fallout site was
genuinely retargeted.
Two out-of-scope, pre-existing gaps surfaced during review were filed separately rather
than folded into this PR: #642 (a dynamic-import regex gap in
check-boundaries.mjs,found during Phase 2's review) and #643 (two other test files carrying the same unsound
two-pass comment-strip pattern this phase's own bug demonstrated, found during this
phase's pre-PR review).
Checklist
npm testpasses (the per-file coverage gate is non-negotiable)npm run buildsucceeds (single-filedist/sql.html)CHANGELOG.md([Unreleased]) updatedCLAUDE.md,docs/ARCHITECTURE.md, and.wikipages updated; the issue's own## Phaseschecklist is ticked only after merge is verified onorigin/main, per its explicit stated conventionPart of #630.