Skip to content
Closed
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
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
28 changes: 28 additions & 0 deletions knowledge/architecture/language-typescript-tsx.md
Original file line number Diff line number Diff line change
@@ -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).
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion src/adapters/cli/format/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <language> [--format json|block] [--json] [--pretty]\n\nOPTIONS:\n <language> One of c, cpp, css, fortran, go, markdown, python, or rust\n --format <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 <language> [--format json|block] [--json] [--pretty]\n\nOPTIONS:\n <language> One of c, cpp, css, fortran, go, markdown, python, rust, tsx, or typescript\n --format <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 {
Expand Down
36 changes: 33 additions & 3 deletions src/adapters/cli/tests/dispatch_materialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <section>Hello</section>; }\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(
[
Expand All @@ -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]
Expand Down
8 changes: 8 additions & 0 deletions src/adapters/cli/tests/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
13 changes: 12 additions & 1 deletion src/adapters/mcp/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
])
);
}
}
26 changes: 25 additions & 1 deletion src/api/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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"
));
}
}
2 changes: 2 additions & 0 deletions src/api/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
35 changes: 35 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <section><h1>Hello</h1></section>; }\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(
Expand Down
8 changes: 8 additions & 0 deletions src/parser/tree_sitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ fn grammar_language(profile: &LanguageProfile) -> Option<Language> {
("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,
}
}
Expand All @@ -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,
}
}
Expand Down
Loading