feat(kernel): R3 — TS/JS equivalence gate passed, kernel default-on
Gate evidence (docs/design/rust-kernel-migration-plan.md §4b):
- Graph parity, byte-identical (stronger than the §5 ≤0.5% bar): full
codegraph-init dump-diffs kernel-vs-wasm on express (13,712 rows),
excalidraw (89,898), and vscode (2,378,238 rows) — identical bytes.
Python control repo (flask) identical + timing unchanged. The parity
harness is now ORDER-sensitive (emission order drives rowids, which
drive resolution order) and dumps come from the new
scripts/dump-graph.mjs (natural keys, no rowids/timestamps).
- The one real find, caught by the vscode tier: tree-sitter error
RECOVERY is encoding-dependent — byte-identical grammar sources and
the same core (0.25.10) recover erroring files differently under
UTF-8 (native) vs UTF-16 (web-tree-sitter) parsing; proven by
reproducing the wasm tree with a native UTF-16 parse. Policy: the
kernel defers any file whose tree has_error() to the wasm extractor
(silent 'defer:' signal, per file) — parity by construction on
erroring files (incidence 0-0.42% across the gate repos), and the
harness fails if deferrals exceed 10% so a broken kernel can't hide
behind the fallback.
- Retrieval invariants: canonical excalidraw flow (mutateElement →
renderStaticScene) connects end-to-end on the kernel-indexed graph;
synthesized-edge families present. Agent A/B is vacuous under
byte-identical DBs (same justification as #1320-#1322).
- Perf: vscode init 105.4s → 82.1s (1.28×) on an 11-core Mac;
excalidraw on a 2-CPU/6GB Linux container (the CI-runner envelope)
6.2-7.1s → 4.3-4.8s (~1.5×). Linux arm64 in-container build: all 22
kernel tests green under CODEGRAPH_KERNEL_EXPECT=1. Windows VM leg
deferred (VM stopped; prlctl start needs Parallels Pro) — benign: a
missing .node falls back to wasm, and the release matrix builds and
gates the win32 prebuilds.
- Full suite: 2,465 tests pass WITH default-on routing, so the entire
extraction corpus now exercises the kernel for TS/JS wherever a
.node is staged.
DEFAULT_ROUTED = {typescript, tsx, javascript, jsx}. Override:
CODEGRAPH_KERNEL_LANGS (replaces the set) / CODEGRAPH_KERNEL=0 (kill).
Changelog entry added under [Unreleased].
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
9ad5cd7ba2
commit
c8cca9a601
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Dump a .codegraph/codegraph.db graph by NATURAL KEYS (no rowids, no
|
||||
* timestamps), sorted — two dumps diff clean iff the graphs are semantically
|
||||
* identical. The byte-identical gate used by every perf/kernel PR:
|
||||
*
|
||||
* node scripts/dump-graph.mjs <repo-or-db> > a.dump
|
||||
* node scripts/dump-graph.mjs <repo-or-db> > b.dump
|
||||
* diff a.dump b.dump
|
||||
*
|
||||
* Volatile fields excluded: nodes.updated_at, files.modified_at/indexed_at/
|
||||
* content_hash+size (environment-dependent), edges.id / unresolved_refs.id
|
||||
* (insertion rowids), and unresolved_refs.status (resolution bookkeeping —
|
||||
* kept, actually: status is deterministic given the same input; excluded only
|
||||
* if it proves flaky. We keep status.)
|
||||
*/
|
||||
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
const arg = process.argv[2];
|
||||
if (!arg) {
|
||||
console.error('usage: dump-graph.mjs <repo-root-or-db-path>');
|
||||
process.exit(2);
|
||||
}
|
||||
let dbPath = arg;
|
||||
if (fs.statSync(arg).isDirectory()) {
|
||||
dbPath = path.join(arg, '.codegraph', 'codegraph.db');
|
||||
}
|
||||
const db = new DatabaseSync(dbPath, { readOnly: true });
|
||||
|
||||
function dump(title, sql) {
|
||||
const rows = db.prepare(sql).all();
|
||||
const lines = rows.map((r) => JSON.stringify(r)).sort();
|
||||
process.stdout.write(`== ${title} (${lines.length})\n`);
|
||||
for (const l of lines) process.stdout.write(l + '\n');
|
||||
}
|
||||
|
||||
dump(
|
||||
'nodes',
|
||||
`SELECT id, kind, name, qualified_name, file_path, language, start_line, end_line,
|
||||
start_column, end_column, docstring, signature, visibility, is_exported,
|
||||
is_async, is_static, is_abstract, decorators, type_parameters, return_type
|
||||
FROM nodes`
|
||||
);
|
||||
dump(
|
||||
'edges',
|
||||
`SELECT source, target, kind, metadata, line, col, provenance FROM edges`
|
||||
);
|
||||
dump(
|
||||
'refs',
|
||||
`SELECT from_node_id, reference_name, reference_kind, line, col, candidates,
|
||||
file_path, language, status, name_tail
|
||||
FROM unresolved_refs`
|
||||
);
|
||||
dump('files', `SELECT path, language, node_count FROM files`);
|
||||
@@ -150,7 +150,7 @@ function report(category, sample) {
|
||||
|
||||
let filesWithDiffs = 0;
|
||||
let filesOk = 0;
|
||||
let kernelFailed = 0;
|
||||
let deferred = 0;
|
||||
let totals = { nodes: 0, edges: 0, refs: 0 };
|
||||
|
||||
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
|
||||
@@ -162,8 +162,11 @@ for (const { file, lang } of files) {
|
||||
delete process.env.CODEGRAPH_KERNEL; // kernel path on
|
||||
const kres = kernel.tryKernelExtract(rel, source, lang);
|
||||
if (!kres) {
|
||||
kernelFailed++;
|
||||
report('kernel-extract-failed', rel);
|
||||
// Expected: files with parse errors defer to wasm (parity by
|
||||
// construction — both arms run the same extractor). Counted, and
|
||||
// guarded below so a broken kernel can't silently defer everything.
|
||||
deferred++;
|
||||
report('kernel-deferred', rel);
|
||||
continue;
|
||||
}
|
||||
process.env.CODEGRAPH_KERNEL = '0'; // wasm path
|
||||
@@ -192,6 +195,19 @@ for (const { file, lang } of files) {
|
||||
const o = JSON.parse(x);
|
||||
report(`${table}:extra-in-kernel:${o.kind ?? ''}`, `${rel}: ${x}`);
|
||||
}
|
||||
// ORDER matters too: identical multisets in a different emission order
|
||||
// change DB rowids, and resolution iterates refs in rowid order — the
|
||||
// full-index dump-diff would surface it as a downstream mystery. Catch it
|
||||
// here instead.
|
||||
if (onlyA.length === 0 && onlyB.length === 0) {
|
||||
for (let i = 0; i < wasm.length; i++) {
|
||||
if (wasm[i] !== kern[i]) {
|
||||
fileHasDiff = true;
|
||||
report(`${table}:order-mismatch`, `${rel}: index ${i}: wasm=${wasm[i]} kernel=${kern[i]}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fileHasDiff) {
|
||||
filesWithDiffs++;
|
||||
@@ -202,7 +218,7 @@ for (const { file, lang } of files) {
|
||||
}
|
||||
|
||||
console.log(`\n=== kernel parity: ${filesOk}/${files.length} files byte-parity` +
|
||||
` (${filesWithDiffs} with diffs, ${kernelFailed} kernel-failed)` +
|
||||
` (${filesWithDiffs} with diffs, ${deferred} deferred-to-wasm)` +
|
||||
` | wasm totals: ${totals.nodes} nodes / ${totals.edges} edges / ${totals.refs} refs ===\n`);
|
||||
|
||||
const sorted = [...buckets.entries()].sort((a, b) => b[1].count - a[1].count);
|
||||
@@ -211,4 +227,11 @@ for (const [cat, { count, samples }] of sorted) {
|
||||
for (const s of samples) console.log(` ${s.length > 400 ? s.slice(0, 400) + '…' : s}`);
|
||||
}
|
||||
|
||||
process.exit(filesWithDiffs > 0 || kernelFailed > 0 ? 1 : 0);
|
||||
// Deferrals are per-file parse-error routing (expected, rare). A high rate
|
||||
// means the kernel is broken and hiding behind the fallback — fail loudly.
|
||||
const deferralRate = deferred / files.length;
|
||||
if (deferralRate > 0.1) {
|
||||
console.error(`deferral rate ${(deferralRate * 100).toFixed(1)}% exceeds 10% — kernel likely broken`);
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(filesWithDiffs > 0 ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user