Files
socraticode/tests
Giancarlo Erra e075d6ffa4 fix(graph): pre-validate ast-grep grammar libraryPath to survive missing prebuilds
Resolves #43. On Linux/Node combinations where one ast-grep grammar
package's prebuilt parser binary is missing for the host architecture
(reporter: Ubuntu 24.04 + WSL2 + Node 24, missing the cpp prebuild),
the existing loader silently failed to register every dynamic grammar,
not just the broken one.

Mechanism: registerDynamicLanguage iterates the modules it receives
and accesses the lazy libraryPath getter on each. If that getter
throws (because the prebuild lookup fails), the entire batch call
aborts atomically — none of the 13 dynamic grammars get registered.
The empty inner `catch {}` in ensureDynamicLanguages discarded the
underlying reason, and the failure surfaced only at WARN as a generic
"Failed to register dynamic ast-grep languages", with no indication
of which grammar was the culprit.

Empirically verified against @ast-grep/napi@0.40.5 in a clean Node
environment:

- Sequential register({A}); register({B}) calls REPLACE rather than
  accumulate; only the last call's grammar survives. So the obvious
  alternative of "register one at a time" is actively broken — it
  would silently leave only the last grammar in langPackages
  registered (php, since it's last in our list). On a polyglot
  codebase that's a regression.

- Batch register({A, B, C}) with one entry whose libraryPath getter
  throws aborts atomically — none of A/B/C end up registered.

The fix is to pre-validate each grammar's libraryPath getter inside
our own per-grammar try/catch, exclude any that throw, then make ONE
batch registerDynamicLanguage call with only the survivors:

- src/services/code-graph.ts: ensureDynamicLanguages now touches
  mod.libraryPath inside the inner try/catch so a missing-prebuild
  failure is contained to that one grammar. Adds module-level
  loadedDynamicLanguages and failedDynamicLanguages state plus a
  public getDynamicLanguageStatus() introspection API. The empty
  `catch {}` becomes `catch (err) { ... }` capturing the actual
  reason. Per-grammar failures and the (now-rare) catch-all log at
  warn level with the underlying message.

- src/tools/graph-tools.ts: codebase_graph_status now appends an
  "AST grammars: Loaded / Failed" block listing both sets, with the
  failure reason for each unloaded grammar. Users see the loader
  state without needing to enable debug logging.

- src/services/graph-symbols.ts: extractSymbolsAndCalls bumps its
  failure log from DEBUG to WARN with one-shot dedupe per language —
  one warn per language for the lifetime of the process, not one
  warn per file (potentially hundreds).

- src/services/graph-imports.ts: extractImports gets the same
  one-shot dedupe per language treatment for consistency.

- tests/unit/dynamic-language-loader.test.ts: nine new tests covering
  the sync contract, idempotence, loaded/failed sort order, disjoint
  sets, and a sanity check that at least one expected grammar
  registers in the test environment.

The loader stays sync. createRequire stays. No call-site signature
changes, no test fixture rewrites. Existing 721 tests pass; with the
new file, 730 tests pass. typecheck and biome clean. CodeRabbit
review of the diff returned no findings.

Co-authored-by: X-Adam <X-Adam@users.noreply.github.com>
2026-05-04 18:50:26 +01:00
..