diff --git a/Cargo.lock b/Cargo.lock index 9f06420..6ba94c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,7 @@ dependencies = [ "tree-sitter-css", "tree-sitter-fortran", "tree-sitter-go", + "tree-sitter-javascript", "tree-sitter-python", "tree-sitter-rust", "walkdir", @@ -1591,6 +1592,16 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "tree-sitter-javascript" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68204f2abc0627a90bdf06e605f5c470aa26fdcb2081ea553a04bdad756693f5" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-language" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index 2f084e0..d6c6de2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ tree-sitter-cpp = "0.23.4" tree-sitter-css = "0.25.0" tree-sitter-fortran = "0.6.0" tree-sitter-go = "0.25.0" +tree-sitter-javascript = "0.25.0" tree-sitter-python = "0.25.0" tree-sitter-rust = "0.24.2" walkdir = "2" diff --git a/README.md b/README.md index 8f7648b..2c369d5 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # codebaseGraph `codebaseGraph` turns a local source repository into a searchable code graph for -AI coding agents. It indexes Python, Rust, Go, C, C++, Fortran, CSS, Markdown, -and MDX, then exposes compact context, schema information, query helpers, and -bounded read-only graph queries through a native CLI and MCP server. +AI coding agents. It indexes Python, Rust, Go, C, C++, Fortran, CSS, JavaScript, +JSX, Markdown, and MDX, then exposes compact context, schema information, query +helpers, and bounded read-only graph queries through a native CLI and MCP server. This workspace also ships `k-wiki`, an optional subsystem for curated repository knowledge. The graph and wiki have separate source and generated state. diff --git a/knowledge/architecture/language-javascript.md b/knowledge/architecture/language-javascript.md new file mode 100644 index 0000000..40fc06a --- /dev/null +++ b/knowledge/architecture/language-javascript.md @@ -0,0 +1,28 @@ +--- +description: Built-in JavaScript and JSX discovery, grammar, catalog, and semantic-normalization contract. +resource: repository-architecture +tags: +- architecture +- javascript +- jsx +- language-support +- parser +- tree-sitter +timestamp: 2026-08-25 +title: JavaScript and JSX Parsing Contract +type: architecture +--- +# JavaScript and JSX Parsing Contract + +The Graph Runtime exposes JavaScript as one built-in syntax language backed by the Tree-sitter JavaScript grammar, which includes JSX syntax. + +## Public contract + +- `javascript` recognizes `.js`, `.jsx`, `.mjs`, and `.cjs` using `tree_sitter_javascript@0.25.0`. +- The key is advertised by the CLI syntax catalog and MCP `graph_syntax` schema. +- Classes, functions, generator functions, methods, imports, and calls map to established ontology nodes. +- JSX elements remain available through ordered raw syntax captures; they are not promoted to framework-specific component nodes. + +Framework inference and embedded-language extraction are outside this contract. + +Related: [Graph Runtime](./graph-runtime.md) and [Repository Ownership Map](./repository-map.md). \ No newline at end of file diff --git a/src/adapters/cli/format/help.rs b/src/adapters/cli/format/help.rs index a981b65..12a40b3 100644 --- a/src/adapters/cli/format/help.rs +++ b/src/adapters/cli/format/help.rs @@ -43,7 +43,7 @@ pub(in crate::adapters::cli) fn graph_schema_help() -> &'static str { } pub(in crate::adapters::cli) fn graph_syntax_help() -> &'static str { - "codebase-graph syntax\n\nUSAGE:\n codebase-graph syntax [--format json|block] [--json] [--pretty]\n\nOPTIONS:\n One of c, cpp, css, fortran, go, markdown, python, or rust\n --format block or json; defaults to block\n --json Emit compact JSON output\n --pretty Pretty-print JSON output" + "codebase-graph syntax\n\nUSAGE:\n codebase-graph syntax [--format json|block] [--json] [--pretty]\n\nOPTIONS:\n One of c, cpp, css, fortran, go, javascript, markdown, python, or rust\n --format block or json; defaults to block\n --json Emit compact JSON output\n --pretty Pretty-print JSON output" } pub(in crate::adapters::cli) fn graph_query_helpers_help() -> &'static str { diff --git a/src/adapters/cli/tests/dispatch_materialize.rs b/src/adapters/cli/tests/dispatch_materialize.rs index 3e499ee..7ed49f5 100644 --- a/src/adapters/cli/tests/dispatch_materialize.rs +++ b/src/adapters/cli/tests/dispatch_materialize.rs @@ -276,9 +276,26 @@ fn materialize_css_source_root_without_profiles() { ".card { color: red; width: calc(100% - 1rem); }\n", ) .unwrap(); + assert_materializes_language(&root, "styles.css", "css"); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn materialize_javascript_source_root_without_profiles() { + let root = unique_temp_dir("codebase-graph-js-source-root"); + fs::create_dir_all(&root).unwrap(); + fs::write( + root.join("component.jsx"), + "export function Card() { return
Hello
; }\n", + ) + .unwrap(); + assert_materializes_language(&root, "component.jsx", "javascript"); + let _ = fs::remove_dir_all(root); +} + +fn assert_materializes_language(root: &Path, path: &str, language: &str) { let db_path = root.join(".codebaseGraph").join("graph.ldb"); let manifest_path = root.join(".codebaseGraph").join("manifest.json"); - let mut output = Vec::new(); run( [ @@ -303,8 +320,7 @@ fn materialize_css_source_root_without_profiles() { assert_eq!(value["database_written"], true); let manifest: serde_json::Value = serde_json::from_str(&fs::read_to_string(&manifest_path).unwrap()).unwrap(); - assert_eq!(manifest["files"]["styles.css"]["language"], "css"); - let _ = fs::remove_dir_all(root); + assert_eq!(manifest["files"][path]["language"], language); } #[test] diff --git a/src/adapters/cli/tests/graph.rs b/src/adapters/cli/tests/graph.rs index 2368fe9..616c44f 100644 --- a/src/adapters/cli/tests/graph.rs +++ b/src/adapters/cli/tests/graph.rs @@ -40,6 +40,14 @@ fn graph_syntax_outputs_language_catalog_and_validates_language() { .as_array() .is_some_and(|nodes| nodes.iter().any(|node| node["type"] == "rule_set"))); + let mut javascript_output = Vec::new(); + run(["syntax", "javascript", "--json"], &mut javascript_output).unwrap(); + let javascript: serde_json::Value = serde_json::from_slice(&javascript_output).unwrap(); + assert_eq!(javascript["language"], "javascript"); + assert!(javascript["node_types"] + .as_array() + .is_some_and(|nodes| nodes.iter().any(|node| node["type"] == "jsx_element"))); + let error = run(["syntax", "custom"], &mut Vec::new()).unwrap_err(); assert!(error.contains("Unknown syntax language")); assert!(run(["syntax"], &mut Vec::new()) diff --git a/src/adapters/mcp/tools.rs b/src/adapters/mcp/tools.rs index 7962797..b119ef1 100644 --- a/src/adapters/mcp/tools.rs +++ b/src/adapters/mcp/tools.rs @@ -228,7 +228,17 @@ mod tests { assert_eq!(syntax["inputSchema"]["required"], json!(["language"])); assert_eq!( syntax["inputSchema"]["properties"]["language"]["enum"], - json!(["c", "cpp", "css", "fortran", "go", "markdown", "python", "rust"]) + json!([ + "c", + "cpp", + "css", + "fortran", + "go", + "javascript", + "markdown", + "python", + "rust" + ]) ); } } diff --git a/src/api/catalog.rs b/src/api/catalog.rs index 1468725..420a303 100644 --- a/src/api/catalog.rs +++ b/src/api/catalog.rs @@ -196,6 +196,22 @@ mod tests { .is_some_and(Vec::is_empty)); } + #[test] + fn syntax_catalog_describes_javascript_and_jsx_nodes() { + let mut payload = load_catalog("syntax").expect("syntax catalog index should load"); + filter_catalog("syntax", &mut payload, Some("javascript")) + .expect("JavaScript syntax catalog should load"); + + assert_eq!(payload["grammar_version"], "tree_sitter_javascript@0.25.0"); + let nodes = payload["node_types"] + .as_array() + .expect("node types should be an array"); + assert!(nodes + .iter() + .any(|node| node["type"] == "function_declaration")); + assert!(nodes.iter().any(|node| node["type"] == "jsx_element")); + } + #[test] fn syntax_catalog_rejects_missing_and_unsupported_languages() { let mut payload = load_catalog("syntax").expect("syntax catalog index should load"); @@ -205,6 +221,8 @@ mod tests { ); assert!(filter_catalog("syntax", &mut payload, Some("custom")) .unwrap_err() - .contains("Valid languages: c, cpp, css, fortran, go, markdown, python, rust")); + .contains( + "Valid languages: c, cpp, css, fortran, go, javascript, markdown, python, rust" + )); } } diff --git a/src/api/lifecycle.rs b/src/api/lifecycle.rs index 9476553..2bd91ba 100644 --- a/src/api/lifecycle.rs +++ b/src/api/lifecycle.rs @@ -722,6 +722,7 @@ fn language_for_path(path: &Path) -> Option<&'static str> { Some("rs") => Some("rust"), Some("go") => Some("go"), Some("css") => Some("css"), + Some("js") | Some("jsx") | Some("mjs") | Some("cjs") => Some("javascript"), Some("c") | Some("h") => Some("c"), Some("cc") | Some("cpp") | Some("cxx") | Some("hpp") | Some("hh") => Some("cpp"), Some("f") | Some("f90") | Some("f95") | Some("for") => Some("fortran"), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b4d852d..cc920ba 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -147,6 +147,27 @@ mod tests { assert!(marked_captures(&output.root).is_empty()); } + #[test] + fn javascript_tree_sitter_parser_marks_profile_captures_and_jsx() { + let output = parse_source( + "import { value } from './dep.js';\nclass Worker { run() { value(); } }\nfunction Card() { return
Hello
; }\n", + &profile("javascript"), + ) + .expect("JavaScript parsing should succeed"); + + let captures = marked_captures(&output.root); + assert!(captures + .iter() + .any(|(capture, _)| capture == "reference.import")); + assert!(captures.contains(&("definition.class".to_string(), "Worker".to_string()))); + assert!(captures.contains(&("definition.method".to_string(), "run".to_string()))); + assert!(captures.contains(&("definition.function".to_string(), "Card".to_string()))); + assert!(captures.contains(&("reference.call".to_string(), "value".to_string()))); + assert_eq!(output.root.node_type, "program"); + assert!(output.diagnostics.is_empty()); + assert!(contains_node_type(&output.root, "jsx_element")); + } + #[test] fn c_tree_sitter_parser_marks_profile_captures() { let output = parse_source( diff --git a/src/parser/tree_sitter.rs b/src/parser/tree_sitter.rs index 77ef3ba..c505bd4 100644 --- a/src/parser/tree_sitter.rs +++ b/src/parser/tree_sitter.rs @@ -50,6 +50,9 @@ fn grammar_language(profile: &LanguageProfile) -> Option { ("tree_sitter_css", _) | (_, "css") => Some(tree_sitter_css::LANGUAGE.into()), ("tree_sitter_fortran", _) | (_, "fortran") => Some(tree_sitter_fortran::LANGUAGE.into()), ("tree_sitter_go", _) | (_, "go") => Some(tree_sitter_go::LANGUAGE.into()), + ("tree_sitter_javascript", _) | (_, "javascript") => { + Some(tree_sitter_javascript::LANGUAGE.into()) + } ("tree_sitter_python", _) | (_, "python") => Some(tree_sitter_python::LANGUAGE.into()), ("tree_sitter_rust", _) | (_, "rust") => Some(tree_sitter_rust::LANGUAGE.into()), _ => None, @@ -63,6 +66,9 @@ pub(super) fn grammar_node_types(profile: &LanguageProfile) -> Option<&'static s ("tree_sitter_css", _) | (_, "css") => Some(tree_sitter_css::NODE_TYPES), ("tree_sitter_fortran", _) | (_, "fortran") => Some(tree_sitter_fortran::NODE_TYPES), ("tree_sitter_go", _) | (_, "go") => Some(tree_sitter_go::NODE_TYPES), + ("tree_sitter_javascript", _) | (_, "javascript") => { + Some(tree_sitter_javascript::NODE_TYPES) + } ("tree_sitter_python", _) | (_, "python") => Some(tree_sitter_python::NODE_TYPES), ("tree_sitter_rust", _) | (_, "rust") => Some(tree_sitter_rust::NODE_TYPES), _ => None, diff --git a/src/profiles.rs b/src/profiles.rs index 0813694..8047523 100644 --- a/src/profiles.rs +++ b/src/profiles.rs @@ -93,6 +93,19 @@ pub(crate) fn built_in_profiles() -> Vec { root_node_types: vec!["stylesheet".to_string()], capture_mappings: Vec::new(), }, + LanguageProfile { + language: "javascript".to_string(), + suffixes: vec![ + ".js".to_string(), + ".jsx".to_string(), + ".mjs".to_string(), + ".cjs".to_string(), + ], + grammar_package: "tree_sitter_javascript".to_string(), + grammar_version: "tree_sitter_javascript@0.25.0".to_string(), + root_node_types: vec!["program".to_string()], + capture_mappings: javascript_mappings(), + }, LanguageProfile { language: "rust".to_string(), suffixes: vec![".rs".to_string()], @@ -177,6 +190,24 @@ pub(crate) fn built_in_profiles() -> Vec { ] } +fn javascript_mappings() -> Vec { + vec![ + mapping("definition.class", &["class_declaration"], "Class"), + mapping( + "definition.function", + &["function_declaration", "generator_function_declaration"], + "Function", + ), + mapping("definition.method", &["method_definition"], "Method"), + mapping( + "reference.import", + &["import_statement"], + "ImportDeclaration", + ), + mapping("reference.call", &["call_expression"], "CallExpression"), + ] +} + fn c_family_mappings() -> Vec { vec![ mapping("definition.function", &["function_definition"], "Function"), @@ -239,6 +270,10 @@ mod tests { ("README.md", "markdown"), ("README.mdx", "markdown"), ("styles.css", "css"), + ("service.js", "javascript"), + ("component.jsx", "javascript"), + ("module.mjs", "javascript"), + ("config.cjs", "javascript"), ("src/lib.rs", "rust"), ("main.go", "go"), ("service.c", "c"),