Skip to content

fix: don't crash on npm 12, where grammar install scripts don't run - #594

Merged
shhdwi merged 1 commit into
mainfrom
fix/npm12-grammars
Oct 14, 2026
Merged

shhdwi merged 1 commit into
mainfrom
fix/npm12-grammars

Conversation

@shhdwi

@shhdwi shhdwi commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

npm 12 stops packages from running install scripts unless you allow them. Our tree-sitter grammars build their native code in an install script, so on npm 12 they never get built. Today that makes trail build (and graft build) crash with "No native build was found ... tree-sitter-kotlin", even in repos with no Kotlin in them.

What changes:

  • Native grammars load lazily, one language at a time, only when a repo has files in that language. A missing Kotlin build no longer breaks a Go repo.
  • When a native grammar is missing, extraction falls back to the WASM grammar for that language and says so once, with the command that fixes it (the package name comes from package.json, so it reads right for both @nanonets/graft and @trailhq/trail-code).
  • build, check and workspace builds warm only the grammars the repo needs.

Node 26 still ships npm 11, so today this only hits people who upgraded npm themselves. That number grows as npm 12 rolls out.

Tests: test/graph-native-fallback.test.ts covers the lazy load and the WASM fallback. Full suite passes locally (1374 tests).

🤖 Generated with Claude Code

…one is missing

npm 12 blocks dependency install scripts by default. tree-sitter-kotlin ships
no prebuilt binary, so after `npm install -g` its addon is absent, and the
static imports in extract.ts threw at module load: every command crashed,
the MCP server and hooks included, even in a repo with no Kotlin.

- native.ts requires tree-sitter and each grammar on first use, caches the
  result, and never throws. A missing core binding disables the native tier
  without crashing.
- A language whose native grammar can't load goes to its WASM stand-in in the
  breadth tier (vendored tags query for kotlin/swift/php/java, the bundle's
  tags.scm for typescript/tsx/python/go/r). .vue scripts follow their language.
- build and check share one dispatch (tierOf/warmGrammarsFor), so check
  stays in sync with a fallback build.
- Cached stand-in parses are marked (`via`) and re-parsed once the native
  grammar loads again.
- The build prints one line naming the languages that fell back and the
  install that fixes it, using the installed package name; nothing when all
  grammars load.
@trailhq-graft

trailhq-graft Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

🌱 graft blast radius

3 areas changed → 6 areas can be affected. 21 dependent symbols, depth 2.
Tests: 1 area updated its tests.
Tag: @anirudhkumar-nanonets — 7 of 9 areas · @Frankie-Xu — Graph Construction · Buseong Kim — Workspace Federation

flowchart TB
  A0(("Pull Request Review<br/>10 symbols"))
  A1(("Workspace Federation<br/>4 symbols"))
  A2(("MCP Tool Invocation<br/>3 symbols"))
  A3(("Graph Engine<br/>2 symbols"))
  A4(("Viewer Build<br/>1 symbol"))
  AX(("1 smaller area<br/>1 symbol"))
  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
Pull Request Review 10 src/app/brain-build.ts:L251-L358 readRepository — calls, depth 1 Review Process, Graph Construction
Workspace Federation 4 src/graph/refresh.ts:L151-L228 ensureFreshGraph — calls, depth 1 Graph Construction
MCP Tool Invocation 3 src/mcp/tools.ts:L216-L244 callTool — calls, depth 2 Graph Construction
Graph Engine 2 src/engine.ts:L92-L102 graph — calls, depth 1 Graph Construction
Viewer Build 1 scripts/build-viewer.mjs:L1-L45 build-viewer.mjs — calls, depth 2 Graph Construction
Synchronous Execution 1 src/claude/sync-run.ts:L19-L33 runSync — calls, depth 2 Graph Construction
Who knows this code — 4 people across 9 areas
Area Who knows it
Graph Construction · changed @anirudhkumar-nanonets — 24 commits, last yesterday · @Frankie-Xu — 8 commits, last 2mo ago
Review Process · changed @anirudhkumar-nanonets — 1 commit, last 1mo ago
CLI · changed @anirudhkumar-nanonets — 54 commits, last today
Pull Request Review · affected @anirudhkumar-nanonets — 12 commits, last 1mo ago
Workspace Federation · affected @anirudhkumar-nanonets — 8 commits, last yesterday · Buseong Kim — 2 commits, last 1mo ago
MCP Tool Invocation · affected @anirudhkumar-nanonets — 7 commits, last 1mo ago
Graph Engine · affected @anirudhkumar-nanonets — 17 commits, last yesterday
Synchronous Execution · affected @bhavesh-gupta-investis — 1 commit, last 2mo ago
…1 further area

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 21 dependent symbols, grouped by area

Pull Request Review — 10 symbols in 6 files

  • src/app/brain-build.ts:L251-L358 — readRepository (calls, depth 1)
    254: const tag = `${job.owner}/${job.repo}`;
  • src/app/review-worker.ts:L1-L110 — review-worker.ts (imports, depth 1)
    9: * - `token` is handed over in the start message. This process never holds the
  • src/app/review.ts:L45-L99 — reviewPullRequest (calls, depth 1)
    51: const checkout = checkoutPullRequest({ owner: job.owner, repo: job.repo, number: job.number, baseRef: job.baseRef, token, log });
  • src/app/server.ts:L1-L292 — server.ts (imports, depth 1)
    92: const { id, token } = pages.put(j.owner, j.repo, j.number, html);
  • src/app/brain-build-worker.ts:L1-L83 — brain-build-worker.ts (calls, depth 2)
    36: message: string;
  • src/app/brain-build-worker.ts:L29-L32 — DoneMessage (references, depth 2)
  • src/app/brain-build.ts:L237-L239 — buildRepoIntoBrain (calls, depth 2)
  • src/app/main.ts:L1-L74 — main.ts (imports, depth 2)
    50: // `POST /brain/build`, and a deployment that only reviews pull requests wants
  • 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/server.ts:L34-L46 — AppSeams (references, depth 2)

Workspace Federation — 4 symbols in 2 files

  • src/graph/refresh.ts:L151-L228 — ensureFreshGraph (calls, depth 1)
    166: // — run graft build" message is the right answer. Auto-building a whole repo
  • src/graph/workspace.ts:L630-L655 — federateCheck (calls, depth 1)
    635: const lines = [`workspace check — ${wg.loaded.length + wg.missing.length} repo(s)`, ""];
  • src/graph/workspace.ts:L709-L722 — splitWorkspace (calls, depth 1)
    712: buildChild: (childDir: string, childName: string) => Promise<void>,
  • src/graph/refresh.ts:L236-L262 — ensureFreshChildren (calls, depth 2)

MCP Tool Invocation — 3 symbols in 1 file

  • src/mcp/tools.ts:L216-L244 — callTool (calls, depth 2)
    242: return { text: err instanceof Error ? err.message : String(err), isError: true };
  • src/mcp/tools.ts:L247-L329 — callSingleTool (calls, depth 2)
    272: const g = await engine.checkGraph(root);
  • src/mcp/tools.ts:L153-L199 — callWorkspaceTool (calls, depth 2)

Graph Engine — 2 symbols in 1 file

  • src/engine.ts:L92-L102 — graph (calls, depth 1)
    92: graph(dir: string, opts: GraphRunOptions = {}): Promise<GraphBuildResult> {
  • src/engine.ts:L83-L85 — checkGraph (calls, depth 1)
    83: checkGraph(dir: string): Promise<GraphCheckResult> {

Viewer Build — 1 symbol in 1 file

  • scripts/build-viewer.mjs:L1-L45 — build-viewer.mjs (calls, depth 2)
    32: // (which ships `dist` only) can load them. generic.ts resolves dist/graph/queries

Synchronous Execution — 1 symbol in 1 file

  • src/claude/sync-run.ts:L19-L33 — runSync (calls, depth 2)
Test signal per changed area — 1 ✓ · 2 –

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 — 11 of 25 reached · 1 test file changed here: test/graph-native-fallback.test.ts
    • not reached: tierOf, warmGrammarsFor, parseSource, fallbackLangOf, rowNamed, loadQuery, sanitizeQuery, coreParser, …6 more
  • – Review Process — 2 test files reach it, none changed here
  • – CLI — no function, method or class changed here
40 test suites also reference this code

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

  • test/app-review-process-probe.ts
  • test/app-review-process.test.ts
  • test/app-server.test.ts
  • test/ask-index.test.ts
  • test/ask.test.ts
  • test/container-extract.test.ts
  • test/context-only-dir.test.ts
  • test/context.test.ts
  • test/covers.test.ts
  • test/generic-extract.test.ts
  • test/generic-node24-probe.ts
  • test/graph-bindings.test.ts
  • test/graph-extract-dedup.test.ts
  • test/graph-go.test.ts
  • test/graph-incremental.test.ts
  • test/graph-invariants.test.ts
  • test/graph-java.test.ts
  • test/graph-languages.test.ts
  • test/graph-php.test.ts
  • test/graph-posix-paths.test.ts
  • …20 more

graft blast · origin/main...HEAD · depth 2 · 11 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 Oct 14, 2026
@shhdwi
shhdwi merged commit c4411a6 into main Oct 14, 2026
7 of 8 checks passed
github-actions Bot added a commit that referenced this pull request Oct 14, 2026
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