mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 21:13:11 +08:00
scripts/add-lang/verify-extraction.mjs ran execFileSync('codegraph', ...).
On Windows, npm installs codegraph as a .cmd shim (so do npm link and
local-install.sh), and Node neither finds a .cmd by bare name nor runs
one without a shell. The check always died with "spawnSync codegraph
ENOENT" (exit 2), and /add-lang's bench.sh read that as broken
extraction and skipped its A/B.
The script now asks, in order:
- CODEGRAPH_BIN, when set. A .js, .mjs or .cjs path runs with the
current node; anything else runs directly (a .cmd or .bat through
cmd.exe).
- On Windows, the codegraph Windows itself would pick: the first PATH
folder holding codegraph under one of PATHEXT's runnable extensions.
A .cmd or .bat runs through cmd.exe (shell: true) as one command
string. Each token is quoted for the C runtime and caret-escaped for
cmd, so spaces, &, parentheses, a %NAME% and a trailing backslash in
the repo path arrive intact. An .exe or .com needs no shell.
- Otherwise codegraph by bare name, exactly as before. On macOS and
Linux without CODEGRAPH_BIN, the call and the output are unchanged.
bench.sh takes CODEGRAPH_BIN too. It makes a path absolute (indexing
runs from inside the repo), exports it so verify-extraction.mjs reads
the index with the same build, and runs its own --version and init calls
with it. Without it, bench.sh behaves exactly as before. The A/B's MCP
server stays run-all.sh's own CG_BIN.
Validation (Windows 11, Node 22.22.0):
- New __tests__/add-lang-verify-extraction.test.ts covers CODEGRAPH_BIN
with a .js stand-in (all platforms), a .cmd shim on PATH with an
awkward repo path (Windows only), and the bare-name call (macOS and
Linux only). With main's script both tests that run here fail ("could
not read codegraph status"); with the fix, 2 pass and 1 is skipped.
- The fixture from PR 1246's review (a codegraph.cmd on PATH): main
exits 2 with ENOENT; the fix reads the status.
- 13 scenarios with npm's own generated shims, and a real index in a
folder named "lua repo & co (x86) 100%": main fails every one that
needs the shim or CODEGRAPH_BIN, and the fix passes them with the
exact argv reaching codegraph. With no codegraph anywhere, both exit 2
the same way.
- bench.sh with the A/B stubbed: with npm link's shims on PATH, main's
copy skips the A/B (verify exit 2) and the fix reaches it (PASS).
CODEGRAPH_BIN as an absolute or a relative path reaches it too, and a
missing one stops with a clear message.
- With process.platform reported as linux or darwin, main's script and
the new one make the same execFileSync call and print the same output.
Raised by PR 1246 (@bslatner).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
118 lines
5.6 KiB
JavaScript
Executable File
118 lines
5.6 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// Sanity-check that codegraph extracted REAL symbols (not just file/import nodes)
|
|
// from a repo for a given language. Exits non-zero on a critical failure so it
|
|
// can drive a write-extractor -> build -> re-check loop.
|
|
//
|
|
// Usage: node scripts/add-lang/verify-extraction.mjs <repo-path> <lang>
|
|
// Reads `codegraph status <repo> --json` using whatever codegraph is on PATH,
|
|
// so it reflects the binary that built the index. Set CODEGRAPH_BIN to read it
|
|
// with another one: a binary, or a build's dist/bin/codegraph.js, which runs
|
|
// with this node. bench.sh exports CODEGRAPH_BIN when it is given one.
|
|
//
|
|
// Exit codes: 0 = pass or soft-warn, 1 = critical fail, 2 = could not run.
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import { existsSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const [repo, lang] = process.argv.slice(2);
|
|
if (!repo || !lang) {
|
|
console.error('usage: verify-extraction.mjs <repo-path> <lang>');
|
|
process.exit(2);
|
|
}
|
|
|
|
// On Windows, npm installs codegraph as a .cmd shim (`npm link` too), and
|
|
// execFileSync can neither find a .cmd by bare name nor run one without a
|
|
// shell. So look where Windows would: the first PATH folder holding codegraph
|
|
// under one of PATHEXT's runnable extensions. Only a .cmd or .bat needs the
|
|
// shell; an .exe or .com is found by the plain call below on its own.
|
|
function findWindowsShim() {
|
|
const runnable = ['.com', '.exe', '.bat', '.cmd'];
|
|
const exts = (process.env.PATHEXT || runnable.join(';')).toLowerCase().split(';').filter((e) => runnable.includes(e));
|
|
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
|
|
if (!dir) continue;
|
|
for (const ext of exts) {
|
|
const file = path.join(dir, `codegraph${ext}`);
|
|
if (existsSync(file)) return ext === '.bat' || ext === '.cmd' ? file : null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// One token of a command line for cmd.exe, which hands it on to node.exe
|
|
// through the shim's %*. Double quotes keep it one argument for the C runtime
|
|
// that splits node.exe's command line, so a backslash run before the closing
|
|
// quote is doubled (or it would escape that quote). Then every cmd
|
|
// metacharacter, the quotes included, gets a caret, so cmd neither splits the
|
|
// token nor expands a %NAME% inside it. A Windows path can't contain a double
|
|
// quote, so a token with one is refused rather than mangled.
|
|
function quoteForCmd(token) {
|
|
if (/["\r\n]/.test(token)) throw new Error(`cannot pass ${JSON.stringify(token)} through cmd.exe`);
|
|
return `"${token.replace(/(\\+)$/, '$1$1')}"`.replace(/[()[\]%!^"`<>&|;, *?]/g, '^$&');
|
|
}
|
|
|
|
function runCodegraph(args) {
|
|
const options = { encoding: 'utf8' };
|
|
const bin = process.env.CODEGRAPH_BIN || (process.platform === 'win32' ? findWindowsShim() : null);
|
|
if (!bin) return execFileSync('codegraph', args, options);
|
|
if (/\.[cm]?js$/i.test(bin)) return execFileSync(process.execPath, [bin, ...args], options);
|
|
if (process.platform === 'win32' && /\.(bat|cmd)$/i.test(bin)) {
|
|
// A single command string with no args array: Node hands it to cmd.exe
|
|
// unchanged, so the quoting above is all there is.
|
|
return execFileSync([bin, ...args].map(quoteForCmd).join(' '), { ...options, shell: true });
|
|
}
|
|
return execFileSync(bin, args, options);
|
|
}
|
|
|
|
let status;
|
|
try {
|
|
const out = runCodegraph(['status', repo, '--json']);
|
|
status = JSON.parse(out);
|
|
} catch (e) {
|
|
console.error(`[verify] could not read codegraph status for ${repo}: ${e.message}`);
|
|
process.exit(2);
|
|
}
|
|
|
|
// Kinds that prove the extractor mapped AST node types (everything except
|
|
// 'file' and 'import', which codegraph creates structurally for any language).
|
|
const SYMBOL_KINDS = new Set([
|
|
'module', 'class', 'struct', 'interface', 'trait', 'protocol', 'function',
|
|
'method', 'property', 'field', 'variable', 'constant', 'enum', 'enum_member',
|
|
'type_alias', 'namespace', 'route', 'component',
|
|
]);
|
|
|
|
const byKind = status.nodesByKind || {};
|
|
const langs = status.languages || [];
|
|
const files = status.fileCount || 0;
|
|
const edges = status.edgeCount || 0;
|
|
const symbolKinds = Object.keys(byKind).filter((k) => SYMBOL_KINDS.has(k));
|
|
const symbolCount = symbolKinds.reduce((s, k) => s + byKind[k], 0);
|
|
|
|
const checks = [];
|
|
const add = (severity, ok, label, detail) => checks.push({ severity, ok, label, detail });
|
|
|
|
add('critical', status.initialized === true, 'index initialized', `initialized=${status.initialized}`);
|
|
add('critical', langs.includes(lang), `language "${lang}" detected`, `languages=[${langs.join(', ')}]`);
|
|
add('critical', symbolCount > 0, 'structural symbols extracted', `${symbolCount} symbols (${symbolKinds.join(', ') || 'NONE — only file/import nodes!'})`);
|
|
add('soft', symbolCount >= files, 'symbol density >= 1/file', `${symbolCount} symbols across ${files} files`);
|
|
add('soft', edges > files, 'edges resolved', `${edges} edges across ${files} files`);
|
|
|
|
console.log(`\n# Extraction check — ${repo} (lang=${lang}, backend=${status.backend})`);
|
|
console.log(` files=${files} nodes=${status.nodeCount} edges=${edges}`);
|
|
console.log(` nodesByKind: ${JSON.stringify(byKind)}\n`);
|
|
for (const c of checks) console.log(` ${c.ok ? '✓' : '✗'} ${c.label} — ${c.detail}`);
|
|
|
|
const critical = checks.filter((c) => !c.ok && c.severity === 'critical');
|
|
const soft = checks.filter((c) => !c.ok && c.severity === 'soft');
|
|
console.log();
|
|
if (critical.length) {
|
|
console.log(`RESULT: FAIL (${critical.length} critical) — extractor or grammar wiring is broken. Re-run dump-ast.mjs and fix the node-type mappings.`);
|
|
process.exit(1);
|
|
}
|
|
if (soft.length) {
|
|
console.log(`RESULT: WARN (${soft.length} soft) — extraction works but looks thin; inspect the counts above.`);
|
|
process.exit(0);
|
|
}
|
|
console.log('RESULT: PASS — extraction looks healthy.');
|
|
process.exit(0);
|