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/skills/codegraph-build-validation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Under Bun the suite runs via `__tests__/bun-homedir.setup.ts` (an `os.homedir` s

The native kernel (`codegraph-kernel/`, Rust) is the **only parser**: every grammar is compiled into it (crates.io pins plus the vendored C under `codegraph-kernel/grammars/`, provenance in `grammars/PROVENANCE.md`). There is no wasm fallback. A source checkout needs a prebuild at `codegraph-kernel/prebuilds/<platform>-<arch>/codegraph-kernel.node`; `npm test` builds it through `scripts/ensure-kernel.mjs` when it is missing or was built from other kernel sources (needs Rust 1.89 or newer), release bundles ship it, and a platform without a prebuild is unsupported (the CLI says so at startup). A host release build of committed kernel sources is saved to a per-user cache (`scripts/kernel-cache.mjs`; `CODEGRAPH_KERNEL_CACHE` moves it, `=off` disables it), so a new worktree whose kernel sources match a recent build restores it instead of compiling; `build:kernel --no-cache` compiles anyway. `CARGO_TARGET_DIR` moves the build output. Languages with a bespoke walker are extracted in Rust; the rest are parsed by the kernel and walked by the generic TypeScript extractor over the serialized tree (`src/extraction/parse-tree.ts`). The whole-graph golden dumps (`__tests__/kernel-golden-dumps.test.ts`) are the regression gate for any extraction change; see `docs/design/kernel-only-extraction-plan.md`.

The kernel also builds as a plain Rust library with no Node-API dependency: `cargo build --manifest-path codegraph-kernel/Cargo.toml --no-default-features --release --example extract-native` produces an internal probe (`target/release/examples/extract-native <source file> <language> <output dir> <path>`) that writes the six extraction buffer tables through the same code the addon uses. It is a development check, not a distributed command. The default `addon` feature keeps the Node-API exports; Rust unit tests that build a `KernelResolver` outside Node need `--features addon-tests` (dynamic Node-API symbols). `lib.rs` allows dead code in builds without the addon because the exports are reached only through it.

One other build step writes into `dist/` and is subject to the same rule: `build:ui` builds the
browser viewer into `dist/viewer/` (never `dist/ui/` — that's the terminal ui).
`scripts/check-ui-build.mjs` asserts `dist/viewer/` after every build and inside every release
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/contained-source.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,5 @@ jobs:
- run: node -e "if (process.versions.node.split('.')[0] !== '24') process.exit(1)"
- run: npm ci --ignore-scripts
- run: npm run build:kernel
- run: cargo test --manifest-path codegraph-kernel/Cargo.toml --release contained_source::tests -- --test-threads=1
- run: cargo test --manifest-path codegraph-kernel/Cargo.toml --release --features addon-tests contained_source::tests -- --test-threads=1
- run: npx vitest run __tests__/source-scan.test.ts __tests__/security.test.ts __tests__/explore-unmatched-names.test.ts -t "scanIndexedSource|native source reader|unmatched names" --maxWorkers=2 --minWorkers=1
4 changes: 2 additions & 2 deletions __tests__/ensure-kernel-stamp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,12 @@ describe('kernel cache', () => {

describe.runIf(process.platform !== 'win32')('build-kernel with the kernel cache', () => {
const lib = process.platform === 'darwin' ? 'libcodegraph_kernel.dylib' : 'libcodegraph_kernel.so';
// A fake cargo: `build` writes a library naming its profile and logs the call;
// A fake cargo: `rustc` (how the script builds the cdylib alone) writes a library naming its profile and logs the call;
// FAKE_EDIT makes it change a kernel source while "compiling".
const fakeCargo = () => {
write('build-bin/cargo', `#!/bin/sh
case "$1" in
build)
rustc)
echo build >> "$CALLS"
profile=release; [ "$2" = "--profile" ] && profile=$3
dir=\${CARGO_TARGET_DIR:-target}/$profile
Expand Down
19 changes: 10 additions & 9 deletions codegraph-kernel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ publish = false
description = "Native extraction kernel for CodeGraph — tree-sitter parse+extract with one JS boundary crossing per file"

[lib]
crate-type = ["cdylib"]
crate-type = ["cdylib", "rlib"]

[features]
default = ["addon"]
addon = ["dep:napi", "dep:napi-derive", "dep:napi-build"]
addon-tests = ["addon", "napi/dyn-symbols"]

[dependencies]
napi = { version = "3", default-features = false, features = ["napi8"] }
napi-derive = "3"
napi = { version = "3", default-features = false, features = ["napi8"], optional = true }
napi-derive = { version = "3", optional = true }
tree-sitter = "0.25"
sha2 = "0.10"
memchr = "2"
Expand Down Expand Up @@ -83,14 +88,10 @@ tree-sitter-language = "0.1"
libc = "0.2"

[build-dependencies]
napi-build = "2"
napi-build = { version = "2", optional = true }
cc = "1"

# Unit tests run outside Node: load the N-API symbols at run time instead of
# linking them, so a test that builds a KernelResolver links. Release builds
# keep the features above.
[dev-dependencies]
napi = { version = "3", default-features = false, features = ["napi8", "dyn-symbols"] }
# Addon unit tests outside Node use --features addon-tests for dynamic symbols.

[profile.release]
# Never `panic = "abort"`: lib.rs walk_file turns a walker panic into a
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down
1 change: 1 addition & 0 deletions codegraph-kernel/build.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
fn main() {
#[cfg(feature = "addon")]
napi_build::setup();

// Kotlin grammar — vendored C, compiled here instead of a crate dep: the
Expand Down
89 changes: 89 additions & 0 deletions codegraph-kernel/examples/extract-native.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//! Internal extraction/parity probe, not a distributed CodeGraph CLI.
use codegraph_kernel::{contract_info, extract_native, NativeExtractBuffers};
use sha2::{Digest, Sha256};
use std::{error::Error, fs, path::Path, time::Instant};

fn tables(output: &NativeExtractBuffers) -> [(&str, &[u8]); 6] {
[
("meta", &output.meta),
("nodes", &output.nodes),
("edges", &output.edges),
("refs", &output.refs),
("bindings", &output.bindings),
("arena", &output.arena),
]
}

fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = std::env::args().skip(1).collect();
if args.first().map(String::as_str) == Some("--bench") && args.len() == 2 {
// TSV rows are source path, language, and logical graph path. Load
// once before timing, just as the production-addon benchmark does.
let manifest = fs::read_to_string(&args[1])?;
let inputs = manifest
.lines()
.map(|line| {
let columns: Vec<_> = line.split('\t').collect();
if columns.len() != 3 {
return Err("expected source, language, path".into());
}
Ok((
columns[2].to_owned(),
fs::read_to_string(columns[0])?,
columns[1].to_owned(),
))
})
.collect::<Result<Vec<_>, Box<dyn Error>>>()?;
if inputs.is_empty() {
return Err("empty input manifest".into());
}
let mut last: Vec<Option<NativeExtractBuffers>> = (0..inputs.len()).map(|_| None).collect();
let iterations = 100;
let mut bytes: usize = 0;
let start = Instant::now();
for _ in 0..iterations {
for (index, (path, content, language)) in inputs.iter().enumerate() {
let output = extract_native(path, content, language)?;
bytes += tables(&output)
.iter()
.map(|(_, table)| table.len())
.sum::<usize>();
last[index] = Some(output);
}
}
let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
let mut hash = Sha256::new();
for output in last.into_iter().flatten() {
for (name, table) in tables(&output) {
if name == "meta" {
if table.len() != 40 {
return Err("unexpected metadata layout".into());
}
let duration = f64::from_le_bytes(table[28..36].try_into()?);
if !duration.is_finite() || duration < 0.0 {
return Err("invalid duration".into());
}
hash.update(&table[..28]);
hash.update(&table[36..]);
} else {
hash.update(table);
}
}
}
let contract = contract_info();
println!("{{\"files\":{},\"iterations\":{},\"bytes\":{},\"elapsedMs\":{},\"outputHash\":\"{:x}\",\"contract\":{{\"abiVersion\":{},\"kernelVersion\":{:?},\"nodeKinds\":{:?},\"edgeKinds\":{:?},\"languages\":{:?}}}}}",
inputs.len() * iterations, iterations, bytes, elapsed_ms, hash.finalize(),
contract.abi_version, contract.kernel_version, contract.node_kinds, contract.edge_kinds, contract.languages);
return Ok(());
}
if !(args.len() == 3 || args.len() == 4) {
return Err("usage: extract-native <source> <language> <output-dir> [logical-path]; or --bench <manifest.tsv>".into());
}
let content = fs::read_to_string(&args[0])?;
let output = extract_native(args.get(3).unwrap_or(&args[0]), &content, &args[1])?;
fs::create_dir_all(&args[2])?;
for (name, table) in tables(&output) {
fs::write(Path::new(&args[2]).join(format!("{name}.bin")), table)?;
}
Ok(())
}
61 changes: 31 additions & 30 deletions codegraph-kernel/src/cfnptr/napi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,26 @@
//! passes and receives, and the entry points for the sweep (stages A/B),
//! the macro environment (stage C) and the link (stages D/E).

#[cfg(feature = "addon")]
use napi_derive::napi;

/// One struct node's extent for the cFnPtr sweep (mirror of the TS caller's
/// `{ id, startLine, endLine }`, with `endLine ?? startLine` applied TS-side).
#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrStructIn {
pub id: String,
pub start_line: u32,
pub end_line: u32,
}

#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrFileIn {
/// RAW file text, exactly as the resolver's readFile returned it.
pub text: String,
pub structs: Vec<CfnptrStructIn>,
}

#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrPathIn {
/// Absolute file path — the kernel reads the text itself (utf-8-lossy, the
/// same bytes JS `readFileSync(path, 'utf-8')` produces) instead of taking
Expand All @@ -29,16 +30,16 @@ pub struct CfnptrPathIn {
pub structs: Vec<CfnptrStructIn>,
}

#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrField {
pub name: String,
pub index: u32,
pub ptr: bool,
#[napi(js_name = "type")]
#[cfg_attr(feature = "addon", napi(js_name = "type"))]
pub ty: String,
}

#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrStructOut {
pub id: String,
pub parsed: bool,
Expand All @@ -47,7 +48,7 @@ pub struct CfnptrStructOut {

/// The cFnPtr extraction-sweep facts for one file — see cfnptr.rs (and the
/// TS synthesizer's `FileFacts`) for field semantics.
#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrFacts {
pub fn_ptr_typedefs: Vec<String>,
pub fn_type_typedefs: Vec<String>,
Expand Down Expand Up @@ -124,7 +125,7 @@ fn empty_cfnptr_facts() -> CfnptrFacts {
/// of files and returns their collected facts, amortizing the NAPI boundary.
/// Feature-detected by the TS loader — absent on older binaries, where the
/// synthesizer keeps its JS sweep.
#[napi]
#[cfg_attr(feature = "addon", napi)]
pub fn cfnptr_scan_files(files: Vec<CfnptrFileIn>) -> Vec<CfnptrFacts> {
files
.into_iter()
Expand All @@ -150,7 +151,7 @@ fn read_lossy(path: &str) -> Option<String> {
/// itself and fans the batch across scoped threads. Output stays 1:1 with
/// input order: an unreadable file or a per-file panic yields empty facts,
/// and a chunk thread's own failure pads its outputs the same way.
#[napi]
#[cfg_attr(feature = "addon", napi)]
pub fn cfnptr_scan_paths(files: Vec<CfnptrPathIn>) -> Vec<CfnptrFacts> {
fn scan_one(f: &CfnptrPathIn) -> CfnptrFacts {
let Some(text) = read_lossy(&f.path) else {
Expand All @@ -172,28 +173,28 @@ pub fn cfnptr_scan_paths(files: Vec<CfnptrPathIn>) -> Vec<CfnptrFacts> {
/// Debug/differential hook: the native `stripCommentsForRegex(text, 'c')`.
/// Exists so the strip differential oracle can pin the Rust stripper against
/// the TS reference directly.
#[napi]
#[cfg_attr(feature = "addon", napi)]
pub fn cfnptr_strip_c(text: String) -> String {
String::from_utf8(super::strip_c(text.as_bytes())).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned())
}

// ---- cFnPtr stage C env extraction + stages D/E link ----

#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrFnMacro {
pub name: String,
pub params: Vec<String>,
pub expansion: String,
}

#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrObjMacro {
pub name: String,
pub value: String,
}

/// One file's `buildEnv` inputs — see `super::file_env`.
#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrFileEnv {
pub fn_macros: Vec<CfnptrFnMacro>,
pub obj_macros: Vec<CfnptrObjMacro>,
Expand All @@ -213,7 +214,7 @@ pub struct CfnptrFileEnv {
/// so a virtual FS still resolves) and per slot of a chunk thread that
/// panicked. OPTIONAL: absent on older binaries, where the synthesizer keeps
/// its lazy LRU-cached extractor path.
#[napi]
#[cfg_attr(feature = "addon", napi)]
pub fn cfnptr_file_envs(paths: Vec<String>) -> Vec<Option<CfnptrFileEnv>> {
fn one(path: &str) -> Option<CfnptrFileEnv> {
let text = read_lossy(path)?;
Expand All @@ -239,22 +240,22 @@ pub fn cfnptr_file_envs(paths: Vec<String>) -> Vec<Option<CfnptrFileEnv>> {

/// `{name, type, isFnPtr}` — the FieldInfo members stages D/E consult; `type`
/// arrives null for fn-pointer fields (the TS side stores `''` there).
#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrLinkField {
pub name: String,
#[napi(js_name = "type")]
#[cfg_attr(feature = "addon", napi(js_name = "type"))]
pub ty: Option<String>,
pub is_fn_ptr: bool,
}

#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrLinkFn {
pub id: String,
pub start_line: u32,
pub end_line: u32,
}

#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrLinkFile {
/// Project-relative path (the node filePath — `registeredAt` uses it).
pub rel: String,
Expand All @@ -268,51 +269,51 @@ pub struct CfnptrLinkFile {
pub fns: Vec<CfnptrLinkFn>,
}

#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrFieldStructs {
pub field: String,
pub structs: Vec<String>,
}

#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrLayout {
pub name: String,
pub fields: Vec<CfnptrLinkField>,
}

#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrLayoutVariants {
pub name: String,
pub variants: Vec<Vec<CfnptrLinkField>>,
}

#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrVarType {
pub var: String,
#[napi(js_name = "type")]
#[cfg_attr(feature = "addon", napi(js_name = "type"))]
pub ty: String,
}

#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrRegEntry {
pub key: String,
pub ids: Vec<String>,
}

#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrArrEntry {
pub file: String,
pub ids: Vec<String>,
}

#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrArrReg {
pub name: String,
pub entries: Vec<CfnptrArrEntry>,
}

/// The registration tables verbatim — see `super::LinkTables`.
#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrLinkTables {
pub field_to_structs: Vec<CfnptrFieldStructs>,
pub struct_layout: Vec<CfnptrLayout>,
Expand All @@ -322,7 +323,7 @@ pub struct CfnptrLinkTables {
pub array_reg: Vec<CfnptrArrReg>,
}

#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrLinkEdge {
pub source: String,
pub target: String,
Expand All @@ -331,15 +332,15 @@ pub struct CfnptrLinkEdge {
pub registered_at: String,
}

#[napi(object)]
#[cfg_attr(feature = "addon", napi(object))]
pub struct CfnptrLinkOut {
pub edges: Vec<CfnptrLinkEdge>,
}

/// Stages D+E of the fn-pointer synthesis — field←field propagation to a
/// fixpoint, then dispatch-site edges — internally threaded, file-order
/// deterministic. OPTIONAL: absent on older binaries.
#[napi]
#[cfg_attr(feature = "addon", napi)]
pub fn cfnptr_link(files: Vec<CfnptrLinkFile>, tables: CfnptrLinkTables) -> CfnptrLinkOut {
let files: Vec<super::LinkFile> = files
.into_iter()
Expand Down
Loading
Loading