Add CUDA (.cu/.cuh) support via the C++ extractor

CUDA is a C++ superset, so .cu/.cuh files parse cleanly with
tree-sitter-cpp (already a dependency). Two registrations wire it up:

- detect.py: add .cu/.cuh to CODE_EXTENSIONS so they're detected and
  watched (watch.py's _WATCHED_EXTENSIONS derives from CODE_EXTENSIONS).
- extract.py: route .cu/.cuh through extract_cpp in _DISPATCH, which
  also makes collect_files() pick them up (_EXTENSIONS = _DISPATCH.keys()).

Adds tests/fixtures/sample.cu (kernel + __device__/host functions +
struct + includes) and CUDA cases in test_languages.py covering kernel/
device function extraction, structs, includes, and host call edges.
Documents the new extensions in the README extension table and CHANGELOG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
raylei50653
2026-06-22 22:57:06 +01:00
committed by safishamsi
co-authored by Claude Opus 4.8
parent 2323ce1937
commit dbce4532f4
7 changed files with 67 additions and 4 deletions
+2 -1
View File
@@ -4,6 +4,8 @@ Full release notes with details on each version: [GitHub Releases](https://githu
## Unreleased
- Feat: CUDA (`.cu`/`.cuh`) source files are now extracted. CUDA is a C++ superset, so these files route through the existing C++ (`tree-sitter-cpp`) extractor — no new grammar dependency. `__global__`/`__device__` kernels, host functions, structs and `#include`s are captured, host call edges are inferred, and `<<<grid, block>>>` kernel-launch syntax parses without error. Detection and file-watching follow automatically since both derive their extension sets from the dispatch table / `CODE_EXTENSIONS` (#1411).
## 0.8.45 (2026-06-22)
- Fix: native-backend semantic extraction now produces hyperedges. The `graphify extract --backend <gemini|claude|claude-cli|openai|kimi|…>` prompt (`llm._EXTRACTION_SYSTEM`) only ever showed `"hyperedges":[]` in its output schema and never explained what a hyperedge is, so every native backend silently emitted zero — while the agent/skill path (whose `extraction-spec.md` fully documents hyperedges) produced them. The two prompts had drifted. The native prompt now carries the same "3 or more nodes participate together" instruction and a populated schema example, so both extraction paths yield the same hyperedge behaviour for a given corpus. Verified end-to-end: a doc that previously produced 0 hyperedges now produces one (correctly relativized, per #1418). Guard tests assert the two prompts can't drift apart again.
@@ -12,7 +14,6 @@ Full release notes with details on each version: [GitHub Releases](https://githu
- Fix: the skill runbooks now write a portable `manifest.json`. Step 9 (full build) and the `--update` reference called `save_manifest(...)` without `root=`, so manifest keys were stored as absolute paths; cloning or moving the repo then broke `graphify --update` — every cached file missed and the whole corpus re-extracted. All four runbook call sites (the lean-core `skill.md`, the Aider/Devin monoliths, and the shared `--update` reference) now pass `root='INPUT_PATH'`, relativizing keys to the scan root to match the native `graphify update` path. The monolith change is registered as a new sanctioned change-class in the round-trip guard (#1417).
- Fix: hyperedge `source_file` is now relativized to the scan root like nodes and edges. `build_from_json(root=...)` relativized `source_file` on `nodes[]` and `links[]`, but stored `graph.hyperedges[]` verbatim, so a semantic subagent's absolute path (e.g. `/Users/.../CLAUDE.md`) leaked into `graph.json`. The fix lives in `build_from_json` (not `to_json`, which has no `root` to relativize against) and mirrors the existing node/edge handling (#1418).
- Fix: the `GRAPHIFY_OUT` override is now honoured everywhere instead of a hardcoded `"graphify-out"` literal. The name is consolidated into a single `graphify.paths` module (was duplicated across `__main__`, `cache`, and `watch`); `security.validate_graph_path`'s `base=None` discovery + fallback, `callflow_html`'s project-root resolution, and the post-commit/post-checkout hook bodies (which now read the env var at hook-run time) all use it. Previously a renamed output dir validated against the wrong base or made the hook miss `.graphify_root` (#1423).
- Fix: the Aider and Devin monolith skills now carry the #1392 runbook fixes that the split skill got in 0.8.44. These single-file skills are hand-maintained and frozen against a pinned pristine-v8 blob by a round-trip guard, so they had been excluded. The guard is now a multiset diff that classifies every added/removed line against documented sanctioned change-classes (rather than a positional zip that forbade any line-count change), which lets the multi-line fixes land while still failing on any unsanctioned drift. Both monoliths now propagate `directed=IS_DIRECTED` into every `build_from_json` call (a `--directed` run no longer collapses reciprocal edges), scope semantic extraction to document/paper/image (code is covered by the AST pass), delete `.graphify_cached.json` on a cache miss, and run Step 4's zero-node guard before any write with the report/analysis gated on `to_json` actually persisting the graph (#1392).
## 0.8.44 (2026-06-19)
+1 -1
View File
@@ -238,7 +238,7 @@ To remove graphify from all platforms at once: `graphify uninstall` (add `--purg
| Type | Extensions |
|------|-----------|
| Code (36 tree-sitter grammars) | `.py .ts .js .jsx .tsx .mjs .go .rs .java .c .cpp .h .hpp .rb .cs .kt .scala .php .swift .lua .luau .zig .ps1 .psm1 .ex .exs .m .mm .jl .vue .svelte .astro .groovy .gradle .dart .v .sv .svh .sql .f .f90 .f95 .f03 .f08 .pas .pp .dpr .dpk .lpr .inc .dfm .lfm .lpk .sh .bash .json .dm .dme .dmi .dmm .dmf .sln .slnx .csproj .fsproj .vbproj .razor .cshtml` (`.dm`/`.dme` requires `uv tool install graphifyy[dm]`) |
| Code (36 tree-sitter grammars) | `.py .ts .js .jsx .tsx .mjs .go .rs .java .c .cpp .h .hpp .cu .cuh .rb .cs .kt .scala .php .swift .lua .luau .zig .ps1 .psm1 .ex .exs .m .mm .jl .vue .svelte .astro .groovy .gradle .dart .v .sv .svh .sql .f .f90 .f95 .f03 .f08 .pas .pp .dpr .dpk .lpr .inc .dfm .lfm .lpk .sh .bash .json .dm .dme .dmi .dmm .dmf .sln .slnx .csproj .fsproj .vbproj .razor .cshtml` (`.dm`/`.dme` requires `uv tool install graphifyy[dm]`; CUDA `.cu`/`.cuh` reuse the C++ grammar) |
| Salesforce Apex | `.cls .trigger` (regex-based; classes, interfaces, enums, methods, triggers, SOQL/DML edges) |
| Terraform / HCL | `.tf .tfvars .hcl` (requires `uv tool install graphifyy[terraform]`) |
| MCP configs | `.mcp.json` `mcp.json` `mcp_servers.json` `claude_desktop_config.json` — extracts server nodes, package refs, env var requirements |
+1 -1
View File
@@ -27,7 +27,7 @@ class FileType(str, Enum):
_MANIFEST_PATH = str(out_path("manifest.json"))
CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.ejs', '.ets', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json', '.tf', '.tfvars', '.hcl', '.dm', '.dme', '.dmi', '.dmm', '.dmf', '.sln', '.slnx', '.csproj', '.fsproj', '.vbproj', '.razor', '.cshtml', '.cls', '.trigger'}
CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.ejs', '.ets', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json', '.tf', '.tfvars', '.hcl', '.dm', '.dme', '.dmi', '.dmm', '.dmf', '.sln', '.slnx', '.csproj', '.fsproj', '.vbproj', '.razor', '.cshtml', '.cls', '.trigger'}
DOC_EXTENSIONS = {'.md', '.mdx', '.qmd', '.txt', '.rst', '.html', '.yaml', '.yml'}
PAPER_EXTENSIONS = {'.pdf'}
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
+2
View File
@@ -12299,6 +12299,8 @@ _DISPATCH: dict[str, Any] = {
".cc": extract_cpp,
".cxx": extract_cpp,
".hpp": extract_cpp,
".cu": extract_cpp,
".cuh": extract_cpp,
".rb": extract_ruby,
".cs": extract_csharp,
".kt": extract_kotlin,
+30
View File
@@ -0,0 +1,30 @@
#include <cstdio>
#include <vector>
struct Vec3 {
float x;
float y;
float z;
};
__device__ float dot(const Vec3& a, const Vec3& b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
__global__ void saxpy(int n, float a, const float* x, float* y) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
y[i] = a * x[i] + y[i];
}
}
float host_norm(const Vec3& v) {
return dot(v, v);
}
int main() {
Vec3 v{1.0f, 2.0f, 3.0f};
float n = host_norm(v);
saxpy<<<1, 256>>>(256, 2.0f, nullptr, nullptr);
return 0;
}
+30
View File
@@ -227,6 +227,36 @@ def test_cpp_struct_inherits_edge():
assert found, "RetryingHttpClient (struct) should have inherits edge to HttpClient"
# ── CUDA ──────────────────────────────────────────────────────────────────────
# CUDA is a C++ superset, so .cu/.cuh route through the C++ (tree-sitter-cpp)
# extractor. These tests guard that __global__/__device__ kernels, host
# functions, structs and includes are all extracted.
def test_cuda_no_error():
r = extract_cpp(FIXTURES / "sample.cu")
assert "error" not in r
def test_cuda_finds_kernel_and_device_functions():
r = extract_cpp(FIXTURES / "sample.cu")
labels = _labels(r)
assert any("saxpy" in l for l in labels) # __global__ kernel
assert any("dot" in l for l in labels) # __device__ function
def test_cuda_finds_struct():
r = extract_cpp(FIXTURES / "sample.cu")
assert any("Vec3" in l for l in _labels(r))
def test_cuda_finds_includes():
r = extract_cpp(FIXTURES / "sample.cu")
assert "imports" in _relations(r)
def test_cuda_host_call_edges():
r = extract_cpp(FIXTURES / "sample.cu")
calls = _calls(r)
assert ("host_norm()", "dot()") in calls
assert ("main()", "host_norm()") in calls
# ── Ruby ─────────────────────────────────────────────────────────────────────
def test_ruby_no_error():
Generated
+1 -1
View File
@@ -1150,7 +1150,7 @@ wheels = [
[[package]]
name = "graphifyy"
version = "0.8.42"
version = "0.8.45"
source = { editable = "." }
dependencies = [
{ name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },