Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ npm run test:coverage # Coverage report
npx vitest run tests/parsers/javascript.test.ts # Single test file
npx vitest run -t "finds cycles" # Single test by name
npm run build:wasm # Rebuild WASM grammars from devDeps (built automatically on npm install)
npm run doctor # Check this worktree for a stale native binary / missing WASM grammars (runs automatically via pretest)
```

**Linter/Formatter:** [Biome](https://biomejs.dev/) — config in `biome.json`, scoped to `src/` and `tests/`.
Expand Down Expand Up @@ -103,6 +104,7 @@ Source is TypeScript in `src/`, compiled via `tsup`. The Rust native engine live
| `shared/paginate.ts` | Pagination helpers for bounded query results |
| **`infrastructure/`** | **Platform and I/O plumbing** |
| `infrastructure/config.ts` | `.codegraphrc.json` loading, env overrides, `apiKeyCommand` secret resolution |
| `infrastructure/doctor.ts` | Environment health checks — stale `better-sqlite3` native ABI, incomplete `grammars/`; see `npm run doctor` |
| `infrastructure/logger.ts` | Structured logging (`warn`, `debug`, `info`, `error`) |
| `infrastructure/native.ts` | Native napi-rs addon loader with WASM fallback |
| `infrastructure/registry.ts` | Global repo registry (`~/.codegraph/registry.json`) for multi-repo MCP |
Expand Down
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ npm test # run the full test suite

**Requirements:** Node.js >= 20

**Working in multiple git worktrees?** Each worktree gets its own untracked
`node_modules/` and `grammars/` — neither is shared via git — so every fresh
`git worktree add` needs its own `npm install`. A worktree set up before a
host Node upgrade, or where `npm install` was interrupted, can be left with a
`better-sqlite3` binary compiled for the wrong Node ABI or an incomplete
`grammars/` directory; both fail in confusing ways deep inside a build or test
run. Run `npm run doctor` to check (or `npm run doctor -- --fix` to repair
in place, scoped to the current worktree) — it also runs automatically before
`npm test` via `pretest`.

## Contributor License Agreement (CLA)

All contributors must sign the [Contributor License Agreement](CLA.md) before
Expand Down Expand Up @@ -88,6 +98,7 @@ npm run test:coverage # Coverage report
npx vitest run tests/parsers/go.test.js # Single test file
npx vitest run -t "finds cycles" # Single test by name
npm run build:wasm # Rebuild WASM grammars
npm run doctor # Check for a stale native binary / missing WASM grammars
```

## Branch Naming Convention
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,12 @@
"build:wasm": "node scripts/node-ts.js scripts/build-wasm.ts",
"typecheck": "tsc --noEmit",
"verify-imports": "node scripts/node-ts.js scripts/verify-imports.ts",
"doctor": "node --experimental-strip-types --import ./scripts/ts-resolve-loader.js scripts/doctor.ts",
"pretest": "npm run doctor",
Comment thread
carlos-alm marked this conversation as resolved.
"test": "vitest run",
"pretest:watch": "npm run doctor",
"test:watch": "vitest",
"pretest:coverage": "npm run doctor",
"test:coverage": "vitest run --coverage",
"test:regression-guard": "vitest run tests/benchmarks/regression-guard.test.ts",
"lint": "biome check src/ tests/",
Expand Down
112 changes: 112 additions & 0 deletions scripts/doctor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env node
/**
* codegraph environment doctor (issue #1733).
*
* Detects two classes of silent per-worktree environment drift: a stale
* better-sqlite3 native binary (ABI mismatch after a Node upgrade) and an
* incomplete grammars/ directory (interrupted or skipped `npm run build:wasm`).
* Both are untracked, worktree-local state — see CLAUDE.md "Parallel
* Sessions" — so every fresh `git worktree add` needs this checked at least
* once, and a long-lived worktree needs it re-checked after a host Node
* upgrade.
*
* Usage:
* npm run doctor # report only; exits 1 only on a blocking ('fail') check
* npm run doctor -- --fix # also run the scoped fix command(s), then re-check
*
* A missing *optional*-language grammar reports as a non-blocking WARN, not
* FAIL — this repo's own parsers are designed to degrade gracefully when a
* non-required grammar is unavailable (see CLAUDE.md), so `npm test` must
* stay runnable in that case, just with narrower language coverage.
*
* Also wired as the `pretest` lifecycle script (report-only, no --fix) so
* `npm test` fails fast with one actionable message instead of a wall of
* unrelated-looking failures scattered across the suite — but only for a
* genuinely blocking problem (stale native binary, missing required grammar).
*
* `--fix` is opt-in rather than automatic: the fixes themselves (`npm rebuild
* better-sqlite3`, `npm run build:wasm`) can take anywhere from seconds to
* over a minute, so running them unattended on every `npm test` would add
* unpredictable latency to a hot dev-loop command. Detect-and-report is the
* safe default; healing is one explicit flag away. Both fix commands always
* run with cwd pinned to this script's own repo root (never process.cwd()),
* so they can never touch a different worktree or a global install.
*/
import { execFileSync } from 'node:child_process';
import os from 'node:os';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { runDoctorChecks } from '../src/infrastructure/doctor.ts';

// doctor.ts's DoctorCheck/DoctorReport interfaces are intentionally not
// exported (nothing outside that module constructs one independently), so
// this derives the shapes structurally rather than importing them by name.
type DoctorReport = ReturnType<typeof runDoctorChecks>;
type DoctorCheck = DoctorReport['checks'][number];

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');

// npm on Windows is npm.cmd; Node refuses to spawn .cmd/.bat without a shell.
const NPM_SHELL = os.platform() === 'win32';

const STATUS_MARK: Record<DoctorCheck['status'], string> = {
ok: 'OK ',
warn: 'WARN',
fail: 'FAIL',
};

function printReport(report: DoctorReport): void {
for (const check of report.checks) {
console.log(`[${STATUS_MARK[check.status]}] ${check.label}: ${check.detail}`);
if (check.status !== 'ok' && check.fixCommand) {
console.log(` fix: ${check.fixCommand}`);
}
}
}

/** Run a non-ok check's fix command, scoped to this repo's own root. */
function runFix(check: DoctorCheck): void {
if (!check.fixCommand) return;
const [cmd, ...args] = check.fixCommand.split(' ');
if (!cmd) return;
console.log(`\n> ${check.fixCommand}`);
try {
execFileSync(cmd, args, { cwd: repoRoot, stdio: 'inherit', shell: NPM_SHELL });
} catch (err) {
console.error(` fix command failed: ${(err as Error).message}`);
}
}

const shouldFix = process.argv.includes('--fix');

let report = runDoctorChecks();
printReport(report);

// --fix repairs anything with something to fix — a non-blocking 'warn' (e.g.
// a missing optional grammar) is still worth fixing when explicitly asked,
// even though it wouldn't block pretest/npm test on its own.
const needsFix = report.checks.filter((c) => c.status !== 'ok');
if (shouldFix && needsFix.length > 0) {
console.log(`\n--fix passed — attempting scoped repairs in ${repoRoot}`);
for (const check of needsFix) runFix(check);

console.log('\nRe-checking...');
report = runDoctorChecks();
printReport(report);
}

if (!report.ok) {
console.error('\ncodegraph doctor: environment is NOT healthy — see fix command(s) above.');
if (!shouldFix) {
console.error('Re-run with --fix to attempt an automatic, worktree-scoped repair:');
console.error(' npm run doctor -- --fix');
}
process.exit(1);
}

const hasWarnings = report.checks.some((c) => c.status === 'warn');
console.log(
hasWarnings
? '\ncodegraph doctor: environment healthy (non-blocking warnings above).'
: '\ncodegraph doctor: environment healthy.',
);
10 changes: 9 additions & 1 deletion src/domain/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,16 @@ import {

const __dirname = path.dirname(fileURLToPath(import.meta.url));

/**
* Absolute path to the `grammars/` directory holding pre-built WASM grammar
* files. Exported so other modules (e.g. `infrastructure/doctor.ts`) can
* verify installation completeness without duplicating this relative-path
* computation.
*/
export const GRAMMARS_DIR = path.join(__dirname, '..', '..', 'grammars');

function grammarPath(name: string): string {
return path.join(__dirname, '..', '..', 'grammars', name);
return path.join(GRAMMARS_DIR, name);
}

let _initialized: boolean = false;
Expand Down
Loading
Loading