diff --git a/Cargo.lock b/Cargo.lock index 9f06420..6b5f86f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,6 +207,7 @@ dependencies = [ "tree-sitter-go", "tree-sitter-python", "tree-sitter-rust", + "tree-sitter-typescript", "walkdir", "windows-sys 0.61.2", ] @@ -1617,6 +1618,16 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "tree-sitter-typescript" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "typenum" version = "1.20.1" diff --git a/Cargo.toml b/Cargo.toml index 2f084e0..8d9755c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ tree-sitter-fortran = "0.6.0" tree-sitter-go = "0.25.0" tree-sitter-python = "0.25.0" tree-sitter-rust = "0.24.2" +tree-sitter-typescript = "0.23.2" walkdir = "2" [target.'cfg(windows)'.dependencies] diff --git a/README.md b/README.md index 8f7648b..9b0a784 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, TypeScript, +TSX, 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-typescript-tsx.md b/knowledge/architecture/language-typescript-tsx.md new file mode 100644 index 0000000..6cb7470 --- /dev/null +++ b/knowledge/architecture/language-typescript-tsx.md @@ -0,0 +1,28 @@ +--- +description: Built-in TypeScript and TSX discovery, grammar, catalog, and semantic-normalization contract. +resource: repository-architecture +tags: +- architecture +- language-support +- parser +- tree-sitter +- typescript +- tsx +timestamp: 2026-08-25 +title: TypeScript and TSX Parsing Contract +type: architecture +--- +# TypeScript and TSX Parsing Contract + +The Graph Runtime treats TypeScript and TSX as sibling built-in syntax languages so each file selects the correct Tree-sitter grammar and node catalog without changing the one-grammar-per-language profile contract. + +## Public contract + +- `typescript` recognizes `.ts`, `.mts`, and `.cts` using `tree_sitter_typescript@0.23.2`. +- `tsx` recognizes `.tsx` using the TSX grammar and node catalog from the same package version. +- Both keys are advertised by the CLI syntax catalog and MCP `graph_syntax` schema. +- TypeScript declarations, interfaces, enums, type aliases, functions, methods, imports, and calls map to established ontology nodes. JSX/TSX elements remain available through ordered raw syntax captures. + +Framework-specific template semantics 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/knowledge/memory/procedural/cargo-package-ignored-metadata-clean-worktree-2026-08-25.md b/knowledge/memory/procedural/cargo-package-ignored-metadata-clean-worktree-2026-08-25.md new file mode 100644 index 0000000..6e5835e --- /dev/null +++ b/knowledge/memory/procedural/cargo-package-ignored-metadata-clean-worktree-2026-08-25.md @@ -0,0 +1,37 @@ +--- +agent_memory: + version: 1 + kind: procedural + scope: repository + status: candidate + owner: codex + created_at: 2026-08-25T12:39:22+09:30 + last_verified_at: null + verified_by: null + review_after: null + supersedes: [] + superseded_by: null + sources: + - kind: source + reference: Cargo.toml package include = ["src/**", ...] + content_hash: null + - kind: runtime-observation + reference: 2026-08-25 cargo publish dry-run for PR branches 101-104 + content_hash: null + - kind: pull-request + reference: https://github.com/rabii-chaarani/codebaseGraph/pull/101 + content_hash: null + history: [] +description: Ignored Finder metadata under src/** can make cargo publish dirty and contaminate exact package measurements even when git status is clean. +tags: +- cargo +- packaging +- release-verification +- clean-worktree +timestamp: 2026-08-25T12:39:22+09:30 +title: Verify Cargo packages from a clean worktree when include globs are broad +type: agent-memory +--- +When `Cargo.toml` uses a broad package allowlist such as `src/**`, ignored untracked files beneath that tree can still make `cargo publish --dry-run` report a dirty package. During the web-language PRs, local `src/.DS_Store` and `src/adapters/.DS_Store` triggered this condition even though ordinary `git status` was clean. + +For release-representative verification, preserve user metadata and run `cargo publish --dry-run --locked` from a clean detached worktree at the exact commit. Then pass the produced `.crate` to the xtask size gate. When verifying a version already present on crates.io, Cargo may retain the archive under `target/package/tmp-crate/`; use the path Cargo actually produced rather than assuming the top-level package path. \ No newline at end of file diff --git a/src/adapters/cli/format/help.rs b/src/adapters/cli/format/help.rs index a981b65..d4184bd 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, markdown, python, rust, tsx, or typescript\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..c961b1c 100644 --- a/src/adapters/cli/tests/dispatch_materialize.rs +++ b/src/adapters/cli/tests/dispatch_materialize.rs @@ -276,9 +276,38 @@ 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_typescript_and_tsx_source_root_without_profiles() { + let root = unique_temp_dir("codebase-graph-ts-source-root"); + fs::create_dir_all(&root).unwrap(); + fs::write( + root.join("service.ts"), + "export function helper(): number { return 1; }\n", + ) + .unwrap(); + fs::write( + root.join("component.tsx"), + "export function Card() { return
Hello
; }\n", + ) + .unwrap(); + assert_materializes_languages( + &root, + &[("service.ts", "typescript"), ("component.tsx", "tsx")], + ); + let _ = fs::remove_dir_all(root); +} + +fn assert_materializes_language(root: &Path, path: &str, language: &str) { + assert_materializes_languages(root, &[(path, language)]); +} + +fn assert_materializes_languages(root: &Path, expected: &[(&str, &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 +332,9 @@ 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); + for (path, language) in expected { + 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..9034b7e 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 tsx_output = Vec::new(); + run(["syntax", "tsx", "--json"], &mut tsx_output).unwrap(); + let tsx: serde_json::Value = serde_json::from_slice(&tsx_output).unwrap(); + assert_eq!(tsx["language"], "tsx"); + assert!(tsx["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..8793e4e 100644 --- a/src/adapters/mcp/tools.rs +++ b/src/adapters/mcp/tools.rs @@ -228,7 +228,18 @@ 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", + "markdown", + "python", + "rust", + "tsx", + "typescript" + ]) ); } } diff --git a/src/api/catalog.rs b/src/api/catalog.rs index 1468725..f206b73 100644 --- a/src/api/catalog.rs +++ b/src/api/catalog.rs @@ -196,6 +196,28 @@ mod tests { .is_some_and(Vec::is_empty)); } + #[test] + fn syntax_catalog_distinguishes_typescript_and_tsx_grammars() { + let mut typescript = load_catalog("syntax").expect("syntax catalog index should load"); + filter_catalog("syntax", &mut typescript, Some("typescript")) + .expect("TypeScript syntax catalog should load"); + assert_eq!( + typescript["grammar_version"], + "tree_sitter_typescript@0.23.2" + ); + assert!(typescript["node_types"] + .as_array() + .is_some_and(|nodes| nodes + .iter() + .any(|node| node["type"] == "interface_declaration"))); + + let mut tsx = load_catalog("syntax").expect("syntax catalog index should load"); + filter_catalog("syntax", &mut tsx, Some("tsx")).expect("TSX syntax catalog should load"); + assert!(tsx["node_types"] + .as_array() + .is_some_and(|nodes| 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 +227,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, markdown, python, rust, tsx, typescript" + )); } } diff --git a/src/api/lifecycle.rs b/src/api/lifecycle.rs index 9476553..9bee510 100644 --- a/src/api/lifecycle.rs +++ b/src/api/lifecycle.rs @@ -722,6 +722,8 @@ fn language_for_path(path: &Path) -> Option<&'static str> { Some("rs") => Some("rust"), Some("go") => Some("go"), Some("css") => Some("css"), + Some("ts") | Some("mts") | Some("cts") => Some("typescript"), + Some("tsx") => Some("tsx"), 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..a9dba34 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -147,6 +147,41 @@ mod tests { assert!(marked_captures(&output.root).is_empty()); } + #[test] + fn typescript_tree_sitter_parser_marks_profile_captures() { + let output = parse_source( + "import { value } from './dep';\ninterface Service { run(): void }\ntype Id = string;\nclass Worker implements Service { run() { value(); } }\nfunction helper(): Id { return 'ok'; }\n", + &profile("typescript"), + ) + .expect("TypeScript parsing should succeed"); + + let captures = marked_captures(&output.root); + assert!(captures + .iter() + .any(|(capture, _)| capture == "reference.import")); + assert!(captures.contains(&("definition.interface".to_string(), "Service".to_string()))); + assert!(captures.contains(&("definition.type_alias".to_string(), "Id".to_string()))); + 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(), "helper".to_string()))); + assert!(captures.contains(&("reference.call".to_string(), "value".to_string()))); + assert_eq!(output.root.node_type, "program"); + assert!(output.diagnostics.is_empty()); + } + + #[test] + fn tsx_tree_sitter_parser_uses_the_tsx_grammar() { + let output = parse_source( + "export function Card() { return

Hello

; }\n", + &profile("tsx"), + ) + .expect("TSX parsing should succeed"); + + 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..07f4883 100644 --- a/src/parser/tree_sitter.rs +++ b/src/parser/tree_sitter.rs @@ -52,6 +52,10 @@ fn grammar_language(profile: &LanguageProfile) -> Option { ("tree_sitter_go", _) | (_, "go") => Some(tree_sitter_go::LANGUAGE.into()), ("tree_sitter_python", _) | (_, "python") => Some(tree_sitter_python::LANGUAGE.into()), ("tree_sitter_rust", _) | (_, "rust") => Some(tree_sitter_rust::LANGUAGE.into()), + (_, "tsx") => Some(tree_sitter_typescript::LANGUAGE_TSX.into()), + ("tree_sitter_typescript", _) | (_, "typescript") => { + Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()) + } _ => None, } } @@ -65,6 +69,10 @@ pub(super) fn grammar_node_types(profile: &LanguageProfile) -> Option<&'static s ("tree_sitter_go", _) | (_, "go") => Some(tree_sitter_go::NODE_TYPES), ("tree_sitter_python", _) | (_, "python") => Some(tree_sitter_python::NODE_TYPES), ("tree_sitter_rust", _) | (_, "rust") => Some(tree_sitter_rust::NODE_TYPES), + (_, "tsx") => Some(tree_sitter_typescript::TSX_NODE_TYPES), + ("tree_sitter_typescript", _) | (_, "typescript") => { + Some(tree_sitter_typescript::TYPESCRIPT_NODE_TYPES) + } _ => None, } } diff --git a/src/profiles.rs b/src/profiles.rs index 0813694..cbae491 100644 --- a/src/profiles.rs +++ b/src/profiles.rs @@ -93,6 +93,22 @@ pub(crate) fn built_in_profiles() -> Vec { root_node_types: vec!["stylesheet".to_string()], capture_mappings: Vec::new(), }, + LanguageProfile { + language: "typescript".to_string(), + suffixes: vec![".ts".to_string(), ".mts".to_string(), ".cts".to_string()], + grammar_package: "tree_sitter_typescript".to_string(), + grammar_version: "tree_sitter_typescript@0.23.2".to_string(), + root_node_types: vec!["program".to_string()], + capture_mappings: typescript_mappings(), + }, + LanguageProfile { + language: "tsx".to_string(), + suffixes: vec![".tsx".to_string()], + grammar_package: "tree_sitter_typescript".to_string(), + grammar_version: "tree_sitter_typescript@0.23.2".to_string(), + root_node_types: vec!["program".to_string()], + capture_mappings: typescript_mappings(), + }, LanguageProfile { language: "rust".to_string(), suffixes: vec![".rs".to_string()], @@ -177,6 +193,43 @@ pub(crate) fn built_in_profiles() -> Vec { ] } +fn typescript_mappings() -> Vec { + vec![ + mapping( + "definition.class", + &["class_declaration", "abstract_class_declaration"], + "Class", + ), + mapping("definition.interface", &["interface_declaration"], "Class"), + mapping("definition.enum", &["enum_declaration"], "Class"), + mapping( + "definition.type_alias", + &["type_alias_declaration"], + "TypeAlias", + ), + mapping( + "definition.function", + &["function_declaration", "generator_function_declaration"], + "Function", + ), + mapping( + "definition.method", + &[ + "method_definition", + "method_signature", + "abstract_method_signature", + ], + "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 +292,10 @@ mod tests { ("README.md", "markdown"), ("README.mdx", "markdown"), ("styles.css", "css"), + ("service.ts", "typescript"), + ("service.mts", "typescript"), + ("service.cts", "typescript"), + ("component.tsx", "tsx"), ("src/lib.rs", "rust"), ("main.go", "go"), ("service.c", "c"),