Skip to content

fix(graph): a grammar that fails to load costs its language, not the CLI - #804

Open
Ercaner1988 wants to merge 2 commits into
trailhq:mainfrom
Ercaner1988:grammar-load-isolation
Open

Ercaner1988 wants to merge 2 commits into
trailhq:mainfrom
Ercaner1988:grammar-load-isolation

Conversation

@Ercaner1988

Copy link
Copy Markdown

extract.ts imported all nine depth-tier grammars at the top of the module. They are native (node-gyp) modules, so any one of them failing to load took the whole CLI down at import time, before argv was read — --version and --help died exactly like a build, with a node-gyp-build stack trace that never says "graft". On the machine in #323 that grammar is tree-sitter-kotlin, which ships no prebuilds at all and so cannot load without a C toolchain.

Load them through createRequire instead and keep what loads. The extension table is filtered by that, which is all the rest of the pipeline needs: an unavailable language stops claiming its files, and they take the paths a language graft has no grammar for takes today — the breadth tier where a generic row claims the extension, unindexed where none does.

Warned once per language, the first time a file it would have claimed comes past, so a repo with no Kotlin in it stays quiet and a Kotlin repo never indexes short in silence.

That alone keeps the CLI alive but leaves a language with no breadth-tier row indexing nothing — and Kotlin had none, so the machine in #323 would start and index no Kotlin at all. tree-sitter-wasm ships a kotlin grammar and queries/kotlin.scm has been in the tree unused since Kotlin was promoted to the depth tier, so the fallback is one registry row. A row whose extension the depth tier also claims is unreachable while the native grammar loads — .java has been exactly that all along, which is why the "must not collide" note needed rewriting rather than an exception.

Both warmGenericGrammars calls now apply the tier precedence their own parse loops apply, so a fallback row cannot warm a WASM grammar the run will never call. That was already true of .java; a second row would have doubled it.

Measured on a three-file repo (.ts/.kt/.rs) with tree-sitter-kotlin broken: the CLI does not start before this change; with the isolation alone, 6 nodes [rust, typescript]; with the fallback row too, 11 nodes [kotlin, rust, typescript]. With the grammar healthy the graph is identical either way.

The test breaks a grammar both ways a module can be reached, import and require, so it still fails if the static imports ever come back.

Refs #323

Ercaner1988 and others added 2 commits September 11, 2026 04:35
extract.ts imported all nine depth-tier grammars at the top of the module.
They are native (node-gyp) modules, so any one of them failing to load took
the whole CLI down at import time, before argv was read — `--version` and
`--help` died exactly like a build, with a node-gyp-build stack trace that
never says "graft". On the machine in trailhq#323 that grammar is tree-sitter-kotlin,
which ships no prebuilds at all and so cannot load without a C toolchain.

Load them through createRequire instead and keep what loads. The extension
table is filtered by that, which is all the rest of the pipeline needs: an
unavailable language stops claiming its files, and they take the paths a
language graft has no grammar for takes today — the breadth tier where a
generic row claims the extension, unindexed where none does.

Warned once per language, the first time a file it would have claimed comes
past, so a repo with no Kotlin in it stays quiet and a Kotlin repo never
indexes short in silence.

That alone keeps the CLI alive but leaves a language with no breadth-tier row
indexing nothing — and Kotlin had none, so the machine in trailhq#323 would start and
index no Kotlin at all. tree-sitter-wasm ships a kotlin grammar and
queries/kotlin.scm has been in the tree unused since Kotlin was promoted to
the depth tier, so the fallback is one registry row. A row whose extension the
depth tier also claims is unreachable while the native grammar loads — `.java`
has been exactly that all along, which is why the "must not collide" note
needed rewriting rather than an exception.

Both warmGenericGrammars calls now apply the tier precedence their own parse
loops apply, so a fallback row cannot warm a WASM grammar the run will never
call. That was already true of `.java`; a second row would have doubled it.

Measured on a three-file repo (.ts/.kt/.rs) with tree-sitter-kotlin broken:
the CLI does not start before this change; with the isolation alone, 6 nodes
[rust, typescript]; with the fallback row too, 11 nodes [kotlin, rust,
typescript]. With the grammar healthy the graph is identical either way.

The test breaks a grammar both ways a module can be reached, `import` and
`require`, so it still fails if the static imports ever come back.

Refs trailhq#323

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Ercaner1988
Ercaner1988 marked this pull request as ready for review November 9, 2026 23:25
@trailhq-graft

trailhq-graft Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

🌱 graft blast radius

1 area changed → 9 areas can be affected. 39 dependent symbols, depth 2.
Tests: Graph Construction has tests the diff did not touch.
Tag: @anirudhkumar-nanonets — 7 of 10 areas · @shhdwi — 8 of 10 areas · @qoole — Context Building

flowchart TB
  A0(("Dependency Graph<br/>18 symbols"))
  A1(("CLI Entry Point<br/>6 symbols"))
  A2(("Pull Request Review<br/>5 symbols"))
  A3(("MCP Tools<br/>3 symbols"))
  A4(("Language Detection<br/>2 symbols"))
  AX(("4 smaller areas<br/>5 symbols"))
  classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
  class A0,A1,A2,A3,A4 reached;
  classDef tail fill:#EEF2F3,stroke:#9AA4A9,stroke-width:1px,color:#3A4247;
  class AX tail;
Loading
Can be affected Symbols Nearest hop Reached from
Dependency Graph 18 src/graph/container.ts:L1-L270 container.ts — imports, depth 1 Graph Construction
CLI Entry Point 6 src/engine.ts:L104-L115 graph — calls, depth 1 Graph Construction
Pull Request Review 5 src/app/review.ts:L45-L99 reviewPullRequest — calls, depth 1 Graph Construction
MCP Tools 3 src/mcp/tools.ts:L265-L351 callSingleTool — calls, depth 2 Graph Construction
Language Detection 2 src/graph/lsp/enrich.ts:L1-L148 enrich.ts — imports, depth 1 Graph Construction
Ignored Code Detection 2 src/search/ignored-code.ts:L43-L61 ignoredCode — calls, depth 2 Graph Construction
3 smaller areas 3 Context Building, Edit Overlap Detection, Cloud Onboarding see below
Who knows this code — 6 people across 10 areas
Area Who knows it
Graph Construction · changed @anirudhkumar-nanonets — 22 commits, last 2d ago · @shhdwi — 17 commits, last 13d ago
Dependency Graph · affected @anirudhkumar-nanonets — 23 commits, last 7d ago · @shhdwi — 22 commits, last 13d ago
CLI Entry Point · affected @anirudhkumar-nanonets — 73 commits, last yesterday · @shhdwi — 52 commits, last 13d ago
Pull Request Review · affected @anirudhkumar-nanonets — 11 commits, last 26d ago · @shhdwi — 2 commits, last 13d ago
MCP Tools · affected @shhdwi — 17 commits, last 19d ago · @anirudhkumar-nanonets — 8 commits, last 20d ago
Language Detection · affected @shhdwi — 2 commits, last 3mo ago · cookerpapa — 1 commit, last today
Ignored Code Detection · affected @Faeeth — 1 commit, last 2d ago · @anirudhkumar-nanonets — 1 commit, last 20d ago
Context Building · affected @qoole — 4 commits, last today · @anirudhkumar-nanonets — 4 commits, last 3mo ago
…2 further areas

Ownership is git history over each area's own files, weighted towards recent work (120-day half-life). Merge commits and bots are dropped, and you are dropped from your own PR. A name with no @ has no GitHub handle in its commit email — tag them by hand, or add a .mailmap entry. A suggestion from history, not a CODEOWNERS rule.

All 39 dependent symbols, grouped by area

Dependency Graph — 18 symbols in 9 files

  • src/graph/container.ts:L1-L270 — container.ts (imports, depth 1)
    21: * worse than not indexing the file at all: graft's promise is that its
  • src/graph/refresh-run.ts:L18-L21 — main (calls, depth 1)
    20: await buildGraph(dir, { contextDir, graphOnly: true, onlyDirs, extensions });
  • src/graph/refresh.ts:L202-L225 — rebuild (calls, depth 1)
    205: await buildGraph(args.dir, {
  • src/graph/resolve.ts:L1-L832 — resolve.ts (imports, depth 1)
    107: * and refusing edges for every extension graft cannot name would lose real ones.
  • src/graph/source-files.ts:L20-L22 — supportedExtensions (calls, depth 1)
    21: return [...new Set([...depthExtensions(), ...genericExtensions(), ...containerExtensions()])].sort();
  • src/graph/source-files.ts:L1-L130 — source-files.ts (imports, depth 1)
    14: import { languageOf, depthExtensions } from "./extract.js";
  • src/graph/workspace.ts:L631-L656 — federateCheck (calls, depth 1)
    639: const g = await checkGraph(join(root, child));
  • src/graph/fingerprint.ts:L1-L201 — fingerprint.ts (imports, depth 2)
    4: * Every graft retrieval call runs this, so it has to be ~free on the common
  • src/graph/map.ts:L132-L139 — sortedLanguages (calls, depth 2)
  • src/graph/refresh-run.ts:L1-L33 — refresh-run.ts (calls, depth 2)
    9: import { buildGraph } from "./build.js";
  • src/graph/refresh.ts:L271-L347 — ensureFreshGraph (calls, depth 2)
    280: // worktree, whose parent checkout's `graft/` git could not check out. Copy it
  • src/graph/refresh.ts:L1-L394 — refresh.ts (imports, depth 2)
    5: * background `graft build` *after* the turn ended, so every query an agent made
  • src/graph/resolve.ts:L97-L101 — familyOf (calls, depth 2)
  • src/graph/source-files.ts:L69-L93 — listSourceFiles (calls, depth 2)
    82: // All three must agree here or `build` and `check` would enumerate different sets.
  • src/graph/source-files.ts:L40-L43 — unsupportedExtensions (calls, depth 2)
  • src/graph/workspace-cli.ts:L65-L108 — buildChild (calls, depth 2)
  • src/graph/workspace-cli.ts:L182-L186 — runWorkspaceCheck (calls, depth 2)
  • src/graph/workspace.ts:L1-L726 — workspace.ts (imports, depth 2)
    5: * `graft/` (byte-identical to building that child standalone), and the parent

CLI Entry Point — 6 symbols in 3 files

  • src/engine.ts:L104-L115 — graph (calls, depth 1)
    105: return buildGraph(dir, {
  • src/engine.ts:L95-L97 — checkGraph (calls, depth 1)
    95: checkGraph(dir: string): Promise<GraphCheckResult> {
  • src/cli.ts:L244-L253 — warnUnsupportedExtensions (calls, depth 2)
  • src/cli.ts:L1-L3449 — cli.ts (imports, depth 2)
    3: * The `trail` CLI, also run as `graft`, its old name. Commands: build, ask,
  • src/engine.ts:L1-L194 — engine.ts (imports, depth 2)
    21: import { buildGraph, type GraphBuildOptions, type GraphBuildResult } from "./graph/build.js";
  • src/index.ts:L1-L47 — index.ts (imports, depth 2)
    9: * import { Graft } from "@nanonets/graft";

Pull Request Review — 5 symbols in 4 files

  • src/app/review.ts:L45-L99 — reviewPullRequest (calls, depth 1)
    55: await buildGraph(checkout.dir);
  • src/app/review-process.ts:L178-L182 — childReviewer (references, depth 2)
  • src/app/review-worker.ts:L67-L87 — run (calls, depth 2)
    84: // message is safe to put in the parent's log — which is where it is going.
  • src/app/review.ts:L1-L174 — review.ts (imports, depth 2)
    6: * functions the CLI calls, which is what keeps the App's comment and `graft
  • src/app/server.ts:L31-L37 — AppSeams (references, depth 2)

MCP Tools — 3 symbols in 1 file

  • src/mcp/tools.ts:L265-L351 — callSingleTool (calls, depth 2)
    294: const g = await engine.checkGraph(root);
  • src/mcp/tools.ts:L157-L203 — callWorkspaceTool (calls, depth 2)
  • src/mcp/tools.ts:L1-L352 — tools.ts (imports, depth 2)
    40: const noGraph = (): string => `no graph found — run \`${cmd('graft build')}\` first`;

Language Detection — 2 symbols in 1 file

  • src/graph/lsp/enrich.ts:L1-L148 — enrich.ts (imports, depth 1)
    2: * Opt-in LSP enrichment (`graft build --lsp`): add compiler-grade call edges the
  • src/graph/lsp/enrich.ts:L25-L25 — langOf (calls, depth 2)

Ignored Code Detection — 2 symbols in 1 file

  • src/search/ignored-code.ts:L43-L61 — ignoredCode (calls, depth 2)
  • src/search/ignored-code.ts:L1-L68 — ignored-code.ts (imports, depth 2)

Context Building — 1 symbol in 1 file

  • src/context/build.ts:L1-L613 — build.ts (imports, depth 2)
    75: * whitelist recorded in the graph fingerprint (mirrors `checkGraph`). */

Edit Overlap Detection — 1 symbol in 1 file

  • src/notes/edit-overlap.ts:L1-L244 — edit-overlap.ts (imports, depth 1)
    49: /** The branch's newest commit message on this file, cleaned, or "" when there's none. */

Cloud Onboarding — 1 symbol in 1 file

  • src/cloud/onboarding.ts:L1-L722 — onboarding.ts (imports, depth 2)
    316: const m = text.match(/\[(?:graft|trail)\] tokens saved ≈ ([\d,]+) \((\d+)%\) — this pack ≈ ([\d,]+) tok vs reading the (\d+) source file\(s\) whole ≈ ([\d,]+) tok/);
Test signal per changed area — 1 ⚠

Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.

  • ⚠ Graph Construction — 2 of 5 reached · 49 test files reach it, none changed here
    • not reached: warnUnavailable, entryFor, depthExtensions
61 test suites also reference this code

76 symbols, kept out of the diagram and the table so they cannot crowd out the areas a reviewer has to look at.

  • test/ask-index.test.ts
  • test/ask.test.ts
  • test/claude-hooks.test.ts
  • test/claude-prompt-private.test.ts
  • test/container-extract.test.ts
  • test/context-only-dir.test.ts
  • test/context.test.ts
  • test/covers.test.ts
  • test/depth-parity.ts
  • test/generic-extract.test.ts
  • test/generic-node24-probe.ts
  • test/graph-bash.test.ts
  • test/graph-cross-language.test.ts
  • test/graph-elixir.test.ts
  • test/graph-enrich-pending.test.ts
  • test/graph-extensions.test.ts
  • test/graph-go.test.ts
  • test/graph-incremental.test.ts
  • test/graph-invariants.test.ts
  • test/graph-java.test.ts
  • …41 more

graft blast · origin/main...HEAD · depth 2 · 6 changed files

Open the interactive graph → — click an area to see its dependent symbols at file:line.

github-actions Bot added a commit that referenced this pull request Nov 9, 2026

This branch has not been deployed

No deployments
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.

1 participant