feat(crush): code crusher — Go AST (Tier A) + tree-sitter multi-language (Tier B) - #7
Conversation
Roadmap item 4. detect.SourceCode routed to the extractive text crusher; now a dedicated crusher understands Go structure. New CodeCrusher (crush/code.go): - Go source via the standard library's go/parser + go/ast (no third-party dependency, and AST-accurate brace matching so string/comment braces never fool it). Keeps the file's shape — package clause, imports, type/const/var decls, every function signature — and elides function bodies whose line span exceeds MinBodyLines (3), offloading each body's interior in place. - Reversibility is position-preserving and byte-exact: the marker occupies exactly the bytes between a body's braces, so expanding every marker reconstructs the file byte-for-byte (TestCodeCrusher_RoundTripByteExact). - Non-Go, a package-less snippet, or unparseable input falls back to the extractive text crusher — the same strategy SourceCode used before, so no language regresses. A build-tagged tree-sitter Tier B for other languages is the follow-up. The generic error-vocabulary floor is deliberately NOT applied to code: Go bodies are saturated with "error"/"Errorf", so it would pin nearly every function and defeat compression, and bodies are reversible anyway. MustKeep vocabulary and query terms still protect a body from elision. Fail-open (nil store / unparseable / sub-MinLines / per-body Put failure); deterministic; never empty. 9 crusher tests + router assertion. Measured end-to-end on a real Go file: 4320 -> 3083 tokens (29%), 17 bodies elided and reversible, signatures/imports/types intact. No release tagged.
Brings the code crusher to parity with headroom's multi-language body elision, for the languages Tier A (Go via go/ast) doesn't cover. New module github.com/initializ/ctxzip/codelang (its own go.mod). It's a SEPARATE module on purpose: tree-sitter needs CGO + the C grammars, and ctxzip's core stays dependency-light (bbolt only) — verified, core go.mod is unchanged. Only hosts that want multi-language code compression take the dependency. codelang.Crusher implements crush.Compressor: - detect language from content signals; candidates tried in likelihood order (specific dialects first: TS over JS, C++ over C); - parse with the candidate grammar and VERIFY (reject if ERROR/MISSING nodes cover >15% of bytes → try the next candidate). A wrong-but-clean parse still elides real body nodes, so compression stays valid; - elide each function/method body worth eliding (>MinBodyLines), keeping imports/types/class headers/signatures. Braces kept, interior offloaded in place (Python's braceless block offloaded whole after the ':'). Reversible & byte-exact (expand every marker -> source, tested per language), deterministic, fail-open (unsupported language / parse trouble -> extractive text). The error-vocab floor is not applied to code (same reasoning as Tier A); MustKeep + query terms still protect a body. Core seam: crush.CodeCrusher gains a Fallback Compressor field (nil -> text). Go still uses the stdlib AST path; the injected tree-sitter crusher handles the rest: code := crush.NewCodeCrusher(); code.Fallback = codelang.NewCrusher() Tests (codelang, CGO): per-language elision + correct detection + byte- exact round-trip for all six; MustKeep protection; unsupported-language fallback; determinism; and the integration seam (Python -> tree-sitter, Go -> go/ast). No release tagged.
|
Pushed Tier B — tree-sitter multi-language support, bringing the code crusher to parity with headroom's New:
|
initializ-mk
left a comment
There was a problem hiding this comment.
Review — code crusher (Go AST Tier A + tree-sitter Tier B)
Reviewed empirically from a local checkout: built both modules, ran the suites, and wrote adversarial round-trip probes for both tiers (then removed them). The reversibility invariant holds — proven byte-exact on adversarial input for both tiers. The design is clean: Tier B's CGO/tree-sitter lives in a separate codelang module so the core stays pure-Go (bbolt-only). Findings are about scope/docs and one detection nuance — no correctness or reversibility defects.
🟠 MEDIUM #1 — Undocumented scope: the router change activates three crushers, not one
router.go doesn't just route SourceCode → code; it also flips GitDiff → diff and SearchResults → search (both previously went to text). Those crushers were merged earlier (#4/#6) but never wired — this PR wires all three. It's tested (router_test asserts all three) and the diff/search crushers are unchanged here (wiring only), so it's safe. But the title/body describe only the code crusher, so a changelog reader would miss that diff and search compression just went live. Call it out in the description.
🟡 LOW #2 — Stale "build-tagged / follow-up" wording (body + code comment)
The body says "Build-tagged tree-sitter for the rest (Tier B) is a follow-up," but Tier B ships in this PR (commit f50f296, the codelang module) and is isolated by a separate module, not build tags. crush/code.go repeats the "build-tagged … is a follow-up" phrasing. Update both — the actual (better) isolation story is module-level, and Tier B is not a follow-up.
🟡 LOW #3 — TS content can be claimed by the JavaScript grammar (detection quality)
Empirically, a TypeScript sample (with interface + : string/: number annotations) parsed as code_treesitter:javascript, not typescript — JS signal count outscored TS's higher weight, and the JS parse stayed under maxErrorRatio. Reversibility is unaffected (byte-exact confirmed), but TS-specific bodies (interface method signatures, typed arrow properties) can be missed → less compression. Consider trying TS before JS whenever any TS-only signal fires, or weighting TS-only signals above raw count.
Informational (not a finding): a valid Go file with no elidable bodies returns ok=false from compressGo and then gets text-crushed (extractive line-dropping on source). Reversible and matches the pre-PR SourceCode → text routing, so no regression — just slightly odd to extractively crush parseable Go. Fine to leave.
✅ Verified correct (empirically)
- Tier A (Go AST) byte-exact on braces inside interpreted strings, raw strings, and line/block comments; generics; nested closures; multibyte UTF-8; empty/small/protected bodies — the interior never leaked (AST brace-matching holds). Identical bodies dedup to one store entry yet all positions reconstruct. The error-floor deviation works (error-saturated bodies still elided). Structure (package/imports/types/signatures) preserved.
- Tier B (tree-sitter) byte-exact on TypeScript (braces in strings/template-literals/comments) and Python (indent blocks, braces in strings).
collectBodiesskips descent into an elided body → spans never overlap; the{/}brace guard validates before slicing;maxErrorRatiorejects wrong-grammar parses; fail-open to text. - Module isolation: core
go.modhas no tree-sitter/CGO dependency;codelangdepends on core one-way (replace => ../). Core stays pure-Go; Tier B is opt-in viaCodeCrusher.Fallback. Both modules build; all suites pass.
Verdict: changes requested (all non-blocking) — the reversibility core is solid; the asks are scope/doc clarity (#1/#2) and a detection-quality nit (#3).
| // TODO: dedicated AST crusher. Route to extractive text for now — | ||
| // it only drops near-duplicate lines without a query, so it is safe. | ||
| return r.text | ||
| return r.code |
There was a problem hiding this comment.
🟠 MEDIUM #1 — this change activates three crushers, but the PR describes only code. Alongside SourceCode → code, this switch also flips GitDiff → diff and SearchResults → search (both previously routed to text). Those crushers were merged in #4/#6 but never wired; this PR wires all three. It's tested and the diff/search crushers are unchanged here, so it's safe — but it's a real behavioral change for diff/search content that the title/body don't mention. Please call it out in the PR description/changelog.
| // go/parser + go/ast — no third-party dependency, and AST-accurate brace | ||
| // matching so string/comment braces never fool it. Other languages fall back to | ||
| // the extractive text crusher (unchanged from how SourceCode routed before), so | ||
| // nothing regresses; a build-tagged tree-sitter path for them is a follow-up. |
There was a problem hiding this comment.
🟡 LOW #2 — stale wording. This says a build-tagged tree-sitter path "is a follow-up," but Tier B ships in this same PR as the codelang module (commit f50f296), and it's isolated by a separate module, not build tags (cleaner — the core go.mod has no CGO/tree-sitter dep at all). Update this comment (and the PR body) to describe the module-level isolation and that Tier B is included here, not deferred.
…+>C) Addresses review LOW #3 on PR #7, and the stale-wording LOW #2. Detection reworked so a specific dialect is tried before its base ONLY when the dialect's EXCLUSIVE syntax is present, not on shared tokens. Each language now has a `strong` regex (exclusive markers) separate from `signals` (candidacy); rank = strongHits*1000 + totalHits. So: - a TypeScript file (which also matches JS-shared tokens) ranks TS ahead of JS via its interface/type-annotation strong hits — even when JS-ish signals outnumber TS ones (the review's finding: TS parsed as JS); - but a plain Java or C file is NOT hijacked by C++, whose only cross-signal was a shared `class`/`#include` — those are dropped from C++ candidacy, and Java/Python strong markers outrank C++'s bare `class`. (First weight-first cut regressed Java exactly this way; fixed by the strong/shared split.) TS candidacy no longer fires on plain function/const (JS-shared), so a pure-JS file is never a TS candidate. New TestCrusher_TSNotClaimedByJS locks it: a TS file with more JS signals than TS still detects as typescript and round-trips byte-exact. LOW #2: updated the crush/code.go CodeCrusher doc — the non-Go path is the tree-sitter codelang module (Tier B, shipped, isolated as a separate module), not a build-tagged follow-up.
|
Thanks — thorough review (the adversarial byte-exact probes especially). Addressed below. 🟠 MEDIUM #1 — I think this is a stale-checkout artifact, not a change in this PRThis PR's and 🟡 LOW #2 — fixedUpdated the 🟡 LOW #3 — fixedReworked detection so a dialect is tried before its base only when its exclusive syntax is present. Each language now has a
New All six languages detect correctly; |
🔎 Grype — no fixable vulnerabilities ✅ |
initializ-mk
left a comment
There was a problem hiding this comment.
Re-review — commit c6b7f183 (detection rework)
Verified locally (both modules build; crush, router, codelang suites green; re-ran my own detection + round-trip probes). The code-level findings are resolved.
Resolved
- 🟡 LOW #3 — TS claimed by JS: fixed and verified. The strong/shared split (
score = strongHits*1000 + totalHits, with TS candidacy now requiring TS-exclusive markers) works. I confirmed empirically:typescript,java,cpp,c,javascripteach detect correctly, and a TS file with more JS signals than TS now detects as typescript and round-trips byte-exact via the typescript grammar. I also checked the C++ cross-signal edge you fixed — a Java file with::method references (Object::toString,System.out::println) correctly detects as java, not cpp, because Java's strong markers outrank C++'s lone shared signal. Nice catch on the first-attempt Java regression. - 🟡 LOW #2 (code half) — fixed. The
crush/code.godoc now describes the module-level isolation (core keeps no CGO/tree-sitter dependency; Tier B is the injectablecodelangmodule), not a build-tagged follow-up.
Still open (PR-description edits only — non-blocking)
- 🟠 LOW #1 — the PR body/title still describe only the code crusher. The router change also activates
GitDiff → diffandSearchResults → search(previouslytext); please note that behavioral change in the description. - 🟡 LOW #2 (body half) — the "## How" section still says "Build-tagged tree-sitter for the rest (Tier B) is a follow-up." Tier B ships in this PR as the module-isolated
codelang; update that line to match the code comment you just fixed.
Both are quick PR-body edits (no commit needed). The reversibility core and detection logic are solid and empirically verified — once the description is squared away this is good to merge.
initializ-mk
left a comment
There was a problem hiding this comment.
Final re-review — all items resolved (and a correction on my part)
⚠️ Withdrawing MEDIUM #1 — my error, you were right
I claimed this PR also activated GitDiff → diff and SearchResults → search. That was a false positive: I diffed against a stale local main that predated the #4/#6 router wiring. Verified against a fresh base — git diff origin/main...HEAD -- router/router.go shows only the SourceCode → code flip (plus the code field/constructor); origin/main already routes diff and search. Your scope note in the body is correct. Apologies for the noise — that finding is withdrawn.
Resolved
- 🟡 LOW #2 — fixed (code + body).
crush/code.godescribes the module-level isolation, and the body's stale "build-tagged … follow-up" line is gone, replaced by an accurate "Tier B isolation (module, not build tags)" section with theFallbackseam. - 🟡 LOW #3 — fixed and verified. Detection reworked with a strong/exclusive-marker split; I confirmed empirically that ts/java/cpp/c/js all detect correctly, a JS-heavy TS file now detects as typescript and round-trips byte-exact, and the
::method-reference edge stays java (not cpp).
Verdict: ✅ LGTM — good to merge
The reversibility core is proven byte-exact on adversarial input for both tiers (Go AST and tree-sitter), Tier B's CGO/tree-sitter is fully isolated in the codelang module (core go.mod stays bbolt-only), detection is robust, and every fail-open path is covered. Clean piece of work — nice module boundary and the empirical byte-exactness guarantee is exactly the right invariant to hold the line on.
(No CI on this repo; verified locally — both modules build, all suites green.)
What
Adds a dedicated code crusher for
detect.SourceCode(previously routed to the extractive text crusher). It keeps a file's shape — package clause, imports, type/const/var decls, and every function signature — and elides function bodies, offloading each interior in place. Two tiers:go/parser+go/ast. No third-party dependency; AST-accurate brace matching, so a{/}inside a string or comment never fools it.codelangmodule.Scope note (router)
The
router.gochange in this PR is onlySourceCode → code.GitDiff → diffandSearchResults → searchwere already wired onmainby #4 and #6 — they are not touched here. (git diff origin/main...HEAD -- router/router.goshows the singleSourceCodecase flip.)Tier B isolation (module, not build tags)
tree-sitter needs CGO + the C grammars, so
codelangis its own Go module (github.com/initializ/ctxzip/codelang, owngo.mod). The core module'sgo.modstays bbolt-only — the CGO/tree-sitter dependency is fully isolated and opt-in. It plugs into the core crusher via a smallcrush.CodeCrusher.Fallbackseam (nil → extractive text, so default behavior is unchanged and nothing is forced onto CGO):How Tier B works
Detect language from content signals → try candidate grammars in likelihood order (a dialect is tried before its base only when its exclusive syntax fires — TS's
interface/type-annotations, C++'sstd::/template, not shared tokens likeclass) → parse-and-verify (reject if ERROR/MISSING nodes cover >15% of bytes; try the next) → elide function/method bodies, keeping braces (Python's brace-less block offloaded whole after the:). A wrong-but-clean parse still elides real body nodes, so compression stays valid regardless of the label.Reversibility
Position-preserving and byte-exact for both tiers. Each body's interior is offloaded and replaced in place by a
<<ctxzip:HASH>>marker occupying exactly those bytes, so expanding every marker reconstructs the source byte-for-byte — asserted for Go and for all six Tier-B languages.Deliberate deviation (called out for review)
The generic error-vocabulary floor is intentionally not applied to code. It exists so a log/tool-output crusher never drops the error line a user is about to ask about; source is different — bodies are saturated with
error/Errorf, so the floor would pin nearly every function and defeat compression, and bodies are reversible anyway. MustKeep vocabulary and query terms still protect a body.Safety / tests
Fail-open (nil store / unparseable / sub-
MinLines/ per-bodyPutfailure / unsupported language → text); deterministic; never empty.Fallbackseam) + router assertion;make checkgreen, deps unchanged.codelang(CGO): per-language elision with correct detection, byte-exact round-trip for all six, an adversarial TS-not-claimed-by-JS case, MustKeep protection, unsupported-language fallback, determinism, and the integration seam.go test ./...green.End-to-end on a real Go file: 4320 → 3083 tokens (29%), 17 bodies elided and reversible, structure intact.
Follow-ups (per the plan)
More tree-sitter languages in
codelang(Rust/Ruby/…) · ML prose + embedding scorer (HybridScorer.Embed) · Redisccr.Storebackend. C# was left out per scope.