From 955e4eb45516cf7452affe01ef3d9e9617a746f9 Mon Sep 17 00:00:00 2001 From: Daniel Brodie <11337994+danielbrodie@users.noreply.github.com> Date: Fri, 22 May 2026 13:14:18 -0400 Subject: [PATCH 1/5] feat(mapper): add CUDA support and C/C++ source-group mapping Recognize CUDA .cu/.cuh sources across standalone main() files, CMake add_executable/add_library (including the legacy FindCUDA cuda_add_executable/cuda_add_library commands), and autotools targets. Repositories containing CUDA sources are detected as cuda projects, and CUDA targets carry the concurrency trust boundary. Map source files that no build target owns into bounded, per-directory source groups so loose C/C++/CUDA code is no longer skipped, and map CMakeLists.txt, CMakePresets.json, and configure.ac as config features. Emit conservative C/C++/CUDA validation commands only when the project declares them: a root Makefile check/test target, or a CMakePresets.json build workflow. Otherwise no validation command is set. --- src/detect.ts | 123 ++++++++++++++++++ src/mapper.test.ts | 247 ++++++++++++++++++++++++++++++++++++ src/mappers/c-cpp-groups.ts | 53 ++++++++ src/mappers/c-cpp.ts | 52 +++++--- src/mappers/config.ts | 3 + src/mappers/grouping.ts | 2 +- src/mappers/shared.ts | 25 +++- 7 files changed, 485 insertions(+), 20 deletions(-) create mode 100644 src/mappers/c-cpp-groups.ts diff --git a/src/detect.ts b/src/detect.ts index 926302a..aeb72fe 100644 --- a/src/detect.ts +++ b/src/detect.ts @@ -249,6 +249,9 @@ async function languageDefaultCommands( if (languages.includes("ruby")) { return rubyDefaultCommands(root); } + if (languages.includes("c") || languages.includes("cpp") || languages.includes("cuda")) { + return cOrCppDefaultCommands(root); + } return { typecheck: null, @@ -691,6 +694,116 @@ async function rubyDefaultCommands(root: string): Promise { }; } +async function cOrCppDefaultCommands(root: string): Promise { + const makefileCommands = await makefileDefaultCommands(root); + if (makefileCommands !== null) { + return makefileCommands; + } + const presetCommands = await cmakePresetDefaultCommands(root); + if (presetCommands !== null) { + return presetCommands; + } + return { typecheck: null, lint: null, format: null, test: null }; +} + +async function makefileDefaultCommands(root: string): Promise { + if (!(await pathExists(join(root, "Makefile")))) { + return null; + } + const source = await readFile(join(root, "Makefile"), "utf8").catch(() => ""); + const test = makefileHasTarget(source, "check") + ? "make check" + : makefileHasTarget(source, "test") + ? "make test" + : null; + return { typecheck: "make", lint: null, format: null, test }; +} + +function makefileHasTarget(source: string, target: string): boolean { + return new RegExp(`^${target}\\s*:(?!=)`, "mu").test(source); +} + +type CMakePresetSets = { + workflowPresets: string[]; + configurePresets: string[]; + buildPresets: string[]; + testPresets: string[]; +}; + +async function cmakePresetDefaultCommands(root: string): Promise { + if (!(await pathExists(join(root, "CMakePresets.json")))) { + return null; + } + const presets = await readCMakePresets(root); + if (presets === null) { + return null; + } + const testPreset = singlePresetName(presets.testPresets); + return { + typecheck: cmakeBuildCommand(presets), + lint: null, + format: null, + test: testPreset === null ? null : `ctest --preset ${testPreset}`, + }; +} + +function cmakeBuildCommand(presets: CMakePresetSets): string | null { + const workflow = singlePresetName(presets.workflowPresets); + if (workflow !== null) { + return `cmake --workflow --preset ${workflow}`; + } + const configure = singlePresetName(presets.configurePresets); + const build = singlePresetName(presets.buildPresets); + if (configure !== null && build !== null) { + return `cmake --preset ${configure} && cmake --build --preset ${build}`; + } + return null; +} + +function singlePresetName(names: string[]): string | null { + return names.length === 1 ? (names[0] ?? null) : null; +} + +async function readCMakePresets(root: string): Promise { + let parsed: unknown; + try { + parsed = JSON.parse(await readFile(join(root, "CMakePresets.json"), "utf8")); + } catch { + return null; + } + if (typeof parsed !== "object" || parsed === null) { + return null; + } + const record = parsed as Record; + return { + workflowPresets: cmakePresetNames(record["workflowPresets"]), + configurePresets: cmakePresetNames(record["configurePresets"]), + buildPresets: cmakePresetNames(record["buildPresets"]), + testPresets: cmakePresetNames(record["testPresets"]), + }; +} + +function cmakePresetNames(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + const names: string[] = []; + for (const entry of value) { + if (typeof entry !== "object" || entry === null) { + continue; + } + const preset = entry as { name?: unknown; hidden?: unknown }; + if ( + typeof preset.name === "string" && + preset.hidden !== true && + /^[A-Za-z0-9._-]+$/u.test(preset.name) + ) { + names.push(preset.name); + } + } + return names; +} + async function mixProjectInfo(root: string): Promise { if (!(await pathExists(join(root, "mix.exs")))) { return { dependencies: new Set() }; @@ -1285,6 +1398,9 @@ async function detectLanguages(root: string): Promise { if (!languages.includes("cpp") && (await containsCppFile(root))) { languages.push("cpp"); } + if (!languages.includes("cuda") && (await containsCudaFile(root))) { + languages.push("cuda"); + } if (!languages.includes("php") && (await containsReviewablePhpFile(root))) { languages.push("php"); } @@ -1339,6 +1455,13 @@ async function containsCFile(root: string): Promise { return containsFileWithExtension(root, ".c", 5, shouldSkipCOrCppSearchEntry); } +async function containsCudaFile(root: string): Promise { + return ( + (await containsFileWithExtensionIgnoringCase(root, ".cu", 5, shouldSkipCOrCppSearchEntry)) || + (await containsFileWithExtensionIgnoringCase(root, ".cuh", 5, shouldSkipCOrCppSearchEntry)) + ); +} + async function containsCppFile(root: string): Promise { return ( (await containsFileWithExtension(root, ".C", 5, shouldSkipCOrCppSearchEntry)) || diff --git a/src/mapper.test.ts b/src/mapper.test.ts index 0b0889f..5d25dc7 100644 --- a/src/mapper.test.ts +++ b/src/mapper.test.ts @@ -11786,6 +11786,253 @@ add_executable(headerapp include/headers.hpp) ]); }); + it("maps a standalone CUDA source file with main as a CUDA binary", async () => { + const root = await fixtureRoot("clawpatch-cuda-standalone-"); + await writeFixture( + root, + "saxpy.cu", + "__global__ void saxpy(float *x) { x[threadIdx.x] *= 2.0f; }\nint main(void) { return 0; }\n", + ); + + const project = await detectProject(root); + const result = await mapFeatures(root, project, []); + const saxpy = result.features.find((feature) => feature.title === "CUDA binary saxpy"); + + expect(saxpy?.source).toBe("c-main"); + expect(saxpy?.entrypoints[0]).toMatchObject({ path: "saxpy.cu", symbol: "main" }); + expect(saxpy?.tags).toContain("cuda"); + }); + + it("maps CMake CUDA targets including .cu and .cuh sources", async () => { + const root = await fixtureRoot("clawpatch-cmake-cuda-"); + await writeFixture( + root, + "CMakeLists.txt", + "project(gpuapp CUDA)\nadd_executable(gpuapp src/main.cu src/kernels.cu src/kernels.cuh)\n", + ); + await writeFixture(root, "src/main.cu", "int main(void) { return 0; }\n"); + await writeFixture( + root, + "src/kernels.cu", + "__global__ void scale(float *x) { x[0] = 1.0f; }\n", + ); + await writeFixture(root, "src/kernels.cuh", "__global__ void scale(float *x);\n"); + + const project = await detectProject(root); + const result = await mapFeatures(root, project, []); + const gpuapp = result.features.find((feature) => feature.title === "CMake binary gpuapp"); + + expect(gpuapp?.entrypoints[0]).toMatchObject({ path: "src/main.cu", symbol: "main" }); + expect(gpuapp?.tags).toContain("cuda"); + expect(gpuapp?.ownedFiles).toEqual([ + { path: "src/main.cu", reason: "target source" }, + { path: "src/kernels.cu", reason: "target source" }, + { path: "src/kernels.cuh", reason: "target source" }, + ]); + }); + + it("maps legacy FindCUDA cuda_add_executable and cuda_add_library targets", async () => { + const root = await fixtureRoot("clawpatch-cmake-find-cuda-"); + await writeFixture( + root, + "CMakeLists.txt", + "find_package(CUDA REQUIRED)\ncuda_add_executable(gpuapp src/main.cu)\ncuda_add_library(gpukernels src/kernels.cu)\n", + ); + await writeFixture(root, "src/main.cu", "int main(void) { return 0; }\n"); + await writeFixture( + root, + "src/kernels.cu", + "__global__ void scale(float *x) { x[0] = 1.0f; }\n", + ); + + const project = await detectProject(root); + const result = await mapFeatures(root, project, []); + const titles = result.features.map((feature) => feature.title); + const gpuapp = result.features.find((feature) => feature.title === "CMake binary gpuapp"); + const gpukernels = result.features.find( + (feature) => feature.title === "CMake library gpukernels", + ); + + expect(titles).toContain("CMake binary gpuapp"); + expect(titles).toContain("CMake library gpukernels"); + expect(gpuapp?.entrypoints[0]).toMatchObject({ path: "src/main.cu", symbol: "main" }); + expect(gpuapp?.tags).toContain("cuda"); + expect(gpuapp?.summary).toContain("cuda_add_executable"); + expect(gpukernels?.ownedFiles).toEqual([{ path: "src/kernels.cu", reason: "target source" }]); + }); + + it("detects CUDA projects from .cu sources", async () => { + const root = await fixtureRoot("clawpatch-cuda-detect-"); + await writeFixture(root, "src/kernel.cu", "__global__ void noop(void) {}\n"); + + const project = await detectProject(root); + + expect(project.detected.languages).toContain("cuda"); + }); + + it("tags CUDA build targets with the concurrency trust boundary", async () => { + const root = await fixtureRoot("clawpatch-cuda-concurrency-"); + await writeFixture( + root, + "CMakeLists.txt", + "project(gpuapp CUDA)\nadd_executable(gpuapp src/main.cu)\n", + ); + await writeFixture(root, "src/main.cu", "int main(void) { return 0; }\n"); + + const project = await detectProject(root); + const result = await mapFeatures(root, project, []); + const gpuapp = result.features.find((feature) => feature.title === "CMake binary gpuapp"); + + expect(gpuapp?.trustBoundaries).toContain("concurrency"); + }); + + it("maps CMake and autotools build files as config features", async () => { + const root = await fixtureRoot("clawpatch-build-config-"); + await writeFixture(root, "CMakeLists.txt", "project(app CXX)\nadd_executable(app main.cpp)\n"); + await writeFixture(root, "CMakePresets.json", '{"version":6}\n'); + await writeFixture(root, "configure.ac", "AC_INIT([app],[1.0])\n"); + await writeFixture(root, "main.cpp", "int main(void) { return 0; }\n"); + + const project = await detectProject(root); + const result = await mapFeatures(root, project, []); + const titles = result.features.map((feature) => feature.title); + + expect(titles).toContain("Project config CMakeLists.txt"); + expect(titles).toContain("Project config CMakePresets.json"); + expect(titles).toContain("Project config configure.ac"); + }); + + it("defaults C/C++ validation to make when the Makefile declares a check target", async () => { + const root = await fixtureRoot("clawpatch-cpp-makefile-check-"); + await writeFixture(root, "Makefile", "all:\n\tcc -o app main.c\n\ncheck:\n\t./app\n"); + await writeFixture(root, "main.c", "int main(void) { return 0; }\n"); + + const project = await detectProject(root); + + expect(project.detected.commands.typecheck).toBe("make"); + expect(project.detected.commands.test).toBe("make check"); + }); + + it("defaults C/C++ validation to make with no test command when the Makefile has none", async () => { + const root = await fixtureRoot("clawpatch-cpp-makefile-notest-"); + await writeFixture(root, "Makefile", "all:\n\tcc -o app main.c\n"); + await writeFixture(root, "main.c", "int main(void) { return 0; }\n"); + + const project = await detectProject(root); + + expect(project.detected.commands.typecheck).toBe("make"); + expect(project.detected.commands.test).toBeNull(); + }); + + it("emits a CMake workflow preset validation command when one is declared", async () => { + const root = await fixtureRoot("clawpatch-cmake-preset-"); + await writeFixture(root, "CMakeLists.txt", "project(app CXX)\nadd_executable(app main.cpp)\n"); + await writeFixture(root, "main.cpp", "int main(void) { return 0; }\n"); + await writeFixture( + root, + "CMakePresets.json", + JSON.stringify({ version: 6, workflowPresets: [{ name: "default", steps: [] }] }), + ); + + const project = await detectProject(root); + + expect(project.detected.commands.typecheck).toBe("cmake --workflow --preset default"); + }); + + it("emits no C/C++ validation command for a CMake project without presets", async () => { + const root = await fixtureRoot("clawpatch-cmake-nopreset-"); + await writeFixture(root, "CMakeLists.txt", "project(app CXX)\nadd_executable(app main.cpp)\n"); + await writeFixture(root, "main.cpp", "int main(void) { return 0; }\n"); + + const project = await detectProject(root); + + expect(project.detected.commands.typecheck).toBeNull(); + expect(project.detected.commands.test).toBeNull(); + }); + + it("emits no C/C++ validation command for ambiguous CMake presets", async () => { + const root = await fixtureRoot("clawpatch-cmake-ambiguous-preset-"); + await writeFixture(root, "CMakeLists.txt", "project(app CXX)\nadd_executable(app main.cpp)\n"); + await writeFixture(root, "main.cpp", "int main(void) { return 0; }\n"); + await writeFixture( + root, + "CMakePresets.json", + JSON.stringify({ + version: 6, + workflowPresets: [ + { name: "debug", steps: [] }, + { name: "release", steps: [] }, + ], + }), + ); + + const project = await detectProject(root); + + expect(project.detected.commands.typecheck).toBeNull(); + }); + + it("maps loose C++ sources with no build target as a source-group feature", async () => { + const root = await fixtureRoot("clawpatch-cpp-group-"); + await writeFixture(root, "lib/parser.cpp", "int parse(void) { return 0; }\n"); + await writeFixture(root, "lib/lexer.cpp", "int lex(void) { return 0; }\n"); + + const project = await detectProject(root); + const result = await mapFeatures(root, project, []); + const group = result.features.find((feature) => feature.title === "C/C++ source group lib"); + + expect(group?.kind).toBe("library"); + expect(group?.source).toBe("c-cpp-group"); + expect(group?.ownedFiles).toEqual([ + { path: "lib/lexer.cpp", reason: "source group member" }, + { path: "lib/parser.cpp", reason: "source group member" }, + ]); + }); + + it("excludes files already owned by a CMake target from source groups", async () => { + const root = await fixtureRoot("clawpatch-cpp-group-exclude-"); + await writeFixture(root, "CMakeLists.txt", "add_executable(app src/main.cpp)\n"); + await writeFixture(root, "src/main.cpp", "int main(void) { return 0; }\n"); + await writeFixture(root, "src/helper.cpp", "int help(void) { return 0; }\n"); + + const project = await detectProject(root); + const result = await mapFeatures(root, project, []); + const group = result.features.find((feature) => feature.title === "C/C++ source group src"); + + expect(group?.ownedFiles).toEqual([{ path: "src/helper.cpp", reason: "source group member" }]); + }); + + it("maps a loose CUDA kernel directory as a CUDA source group with concurrency", async () => { + const root = await fixtureRoot("clawpatch-cuda-group-"); + await writeFixture( + root, + "kernels/reduce.cu", + "__global__ void reduce(float *x) { x[0] = 0; }\n", + ); + await writeFixture(root, "kernels/reduce.cuh", "__global__ void reduce(float *x);\n"); + + const project = await detectProject(root); + const result = await mapFeatures(root, project, []); + const group = result.features.find((feature) => feature.title === "CUDA source group kernels"); + + expect(group?.tags).toContain("cuda"); + expect(group?.trustBoundaries).toContain("concurrency"); + expect(group?.ownedFiles).toEqual([ + { path: "kernels/reduce.cu", reason: "source group member" }, + { path: "kernels/reduce.cuh", reason: "source group member" }, + ]); + }); + + it("emits no C/C++ validation command for an autotools-only project", async () => { + const root = await fixtureRoot("clawpatch-autotools-nullcmd-"); + await writeFixture(root, "Makefile.am", "bin_PROGRAMS = app\napp_SOURCES = main.c\n"); + await writeFixture(root, "main.c", "int main(void) { return 0; }\n"); + + const project = await detectProject(root); + + expect(project.detected.commands.typecheck).toBeNull(); + expect(project.detected.commands.test).toBeNull(); + }); + it("maps autotools targets from Makefile.in", async () => { const root = await fixtureRoot("clawpatch-autotools-makefile-in-"); await writeFixture( diff --git a/src/mappers/c-cpp-groups.ts b/src/mappers/c-cpp-groups.ts new file mode 100644 index 0000000..5587f00 --- /dev/null +++ b/src/mappers/c-cpp-groups.ts @@ -0,0 +1,53 @@ +import { chunkFiles, partitionFileGroups } from "./grouping.js"; +import { isCOrCppPath, isCOrCppTestPath, languageTag, withCudaConcurrency } from "./shared.js"; +import { FeatureSeed } from "./types.js"; + +const sourceGroupMaxFiles = 12; + +export function cCppGroupSeeds(sourceFiles: string[], ownedPaths: Set): FeatureSeed[] { + const residual = sourceFiles.filter( + (path) => isCOrCppPath(path) && !ownedPaths.has(path) && !isCOrCppTestPath(path), + ); + if (residual.length === 0) { + return []; + } + const byTopDir = new Map(); + for (const path of residual) { + const slash = path.indexOf("/"); + const topDir = slash === -1 ? "" : path.slice(0, slash); + byTopDir.set(topDir, [...(byTopDir.get(topDir) ?? []), path]); + } + const seeds: FeatureSeed[] = []; + for (const [topDir, files] of [...byTopDir.entries()].toSorted(([left], [right]) => + left.localeCompare(right), + )) { + const groups = + topDir === "" + ? chunkFiles(".", files.toSorted(), sourceGroupMaxFiles) + : partitionFileGroups(topDir, files, sourceGroupMaxFiles); + for (const group of groups) { + seeds.push(groupSeed(group.label, group.files)); + } + } + return seeds; +} + +function groupSeed(label: string, files: string[]): FeatureSeed { + const sorted = files.toSorted(); + const tags = [...new Set(sorted.map(languageTag))]; + const isCuda = tags.includes("cuda"); + return { + title: `${isCuda ? "CUDA" : "C/C++"} source group ${label}`, + summary: `C/C++/CUDA source files under ${label} not owned by a build target.`, + kind: "library", + source: "c-cpp-group", + confidence: "low", + entryPath: sorted[0] ?? label, + symbol: label, + route: null, + command: null, + tags: [...tags, "source-group"], + trustBoundaries: withCudaConcurrency(["filesystem"], isCuda ? "cuda" : "cpp"), + ownedFiles: sorted.map((path) => ({ path, reason: "source group member" })), + }; +} diff --git a/src/mappers/c-cpp.ts b/src/mappers/c-cpp.ts index e3506b7..9c35a79 100644 --- a/src/mappers/c-cpp.ts +++ b/src/mappers/c-cpp.ts @@ -4,12 +4,16 @@ import { isSafeFile, isCOrCppTestPath, isSampleProjectPath, + languageLabel, + languageTag, normalize, packageTrustBoundaries, shouldSkip, stripLineComments, walk, + withCudaConcurrency, } from "./shared.js"; +import { cCppGroupSeeds } from "./c-cpp-groups.js"; import { FeatureSeed, SeedFileRef } from "./types.js"; export async function cCppSeeds(root: string): Promise { @@ -29,15 +33,19 @@ export async function cCppSeeds(root: string): Promise { .flatMap((seed) => [seed.entryPath, ...(seed.ownedFiles?.map((file) => file.path) ?? [])]), ); seeds.push(...(await mainFunctionTargets(root, files, alreadySeeded))); + const ownedPaths = new Set( + seeds.flatMap((seed) => [seed.entryPath, ...(seed.ownedFiles?.map((file) => file.path) ?? [])]), + ); + seeds.push(...cCppGroupSeeds(files.filter(isCOrCppSource), ownedPaths)); return dedupeByEntry(seeds); } function isCOrCppSource(path: string): boolean { - return /\.(?:c|cc|cpp|cxx|h|hh|hpp|hxx)$/iu.test(path); + return /\.(?:c|cc|cpp|cxx|cu|cuh|h|hh|hpp|hxx)$/iu.test(path); } function isCOrCppCompilable(path: string): boolean { - return /\.(?:c|cc|cpp|cxx)$/iu.test(path); + return /\.(?:c|cc|cpp|cxx|cu)$/iu.test(path); } function isMakefile(path: string): boolean { @@ -48,10 +56,6 @@ function isCMake(path: string): boolean { return path.endsWith("CMakeLists.txt") || path.endsWith(".cmake"); } -function languageTag(path: string): "c" | "cpp" { - return /\.(?:C|H)$/u.test(path) || /\.(?:cc|cpp|cxx|hh|hpp|hxx)$/iu.test(path) ? "cpp" : "c"; -} - async function autotoolsTargets(root: string, files: string[]): Promise { const seeds: FeatureSeed[] = []; const makefiles = files.filter(isMakefile); @@ -86,7 +90,7 @@ async function autotoolsTargets(root: string, files: string[]): Promise "")); const effectiveProjectSourceDir = cmakeDeclaresProject(body) ? dir : projectSourceDir; const effectiveProjectName = cmakeProjectName(body) ?? projectName; - for (const args of cmakeCommandArgs(body, "add_executable")) { + for (const { command, args } of cmakeTargetCalls(body, [ + "add_executable", + "cuda_add_executable", + ])) { const [rawTarget = "", ...sources] = splitWords(args); const target = resolveCMakeTargetName(rawTarget, effectiveProjectName); if (!isValidTargetName(target)) { @@ -182,7 +189,7 @@ async function cmakeTargets(root: string, files: string[]): Promise { + return commands.flatMap((command) => + cmakeCommandArgs(body, command).map((args) => ({ command, args })), + ); +} + function cmakeCommandArgs(body: string, command: string): string[] { const args: string[] = []; const needle = command.toLowerCase(); @@ -660,8 +676,8 @@ async function mainFunctionTargets( .at(-1) ?.replace(/\.[^.]+$/u, "") ?? "main"; seeds.push({ - title: `${tag === "cpp" ? "C++" : "C"} binary ${command}`, - summary: `C/C++ source file with a top-level main() at ${file}.`, + title: `${languageLabel(tag)} binary ${command}`, + summary: `${tag === "cuda" ? "CUDA" : "C/C++"} source file with a top-level main() at ${file}.`, kind: "cli-command", source: "c-main", confidence: "medium", @@ -670,7 +686,7 @@ async function mainFunctionTargets( route: null, command, tags: [tag, "cli"], - trustBoundaries: ["user-input", "filesystem", "process-exec"], + trustBoundaries: withCudaConcurrency(["user-input", "filesystem", "process-exec"], tag), }); } return seeds; diff --git a/src/mappers/config.ts b/src/mappers/config.ts index b966128..93f9add 100644 --- a/src/mappers/config.ts +++ b/src/mappers/config.ts @@ -23,6 +23,9 @@ export async function configSeeds(root: string): Promise { "composer.lock", "phpunit.xml", "Makefile", + "CMakeLists.txt", + "CMakePresets.json", + "configure.ac", ]; const seeds: FeatureSeed[] = []; for (const file of candidates) { diff --git a/src/mappers/grouping.ts b/src/mappers/grouping.ts index 330976d..f77d57e 100644 --- a/src/mappers/grouping.ts +++ b/src/mappers/grouping.ts @@ -75,7 +75,7 @@ function partitionAt( return groups; } -function chunkFiles(label: string, files: string[], maxFiles: number): FileGroup[] { +export function chunkFiles(label: string, files: string[], maxFiles: number): FileGroup[] { if (files.length === 0) { return []; } diff --git a/src/mappers/shared.ts b/src/mappers/shared.ts index 07a71e1..75ca189 100644 --- a/src/mappers/shared.ts +++ b/src/mappers/shared.ts @@ -392,7 +392,7 @@ function isJsTestPath(path: string): boolean { } export function isCOrCppPath(path: string): boolean { - return /\.(?:c|cc|cpp|cxx|h|hh|hpp|hxx)$/iu.test(path); + return /\.(?:c|cc|cpp|cxx|cu|cuh|h|hh|hpp|hxx)$/iu.test(path); } export function isCOrCppTestPath(path: string): boolean { @@ -405,6 +405,29 @@ export function isCOrCppTestPath(path: string): boolean { ); } +export type LanguageTag = "c" | "cpp" | "cuda"; + +export function languageTag(path: string): LanguageTag { + if (/\.cuh?$/iu.test(path)) { + return "cuda"; + } + return /\.(?:C|H)$/u.test(path) || /\.(?:cc|cpp|cxx|hh|hpp|hxx)$/iu.test(path) ? "cpp" : "c"; +} + +export function languageLabel(tag: LanguageTag): string { + return tag === "cuda" ? "CUDA" : tag === "cpp" ? "C++" : "C"; +} + +export function withCudaConcurrency( + boundaries: TrustBoundary[], + tag: LanguageTag, +): TrustBoundary[] { + if (tag !== "cuda" || boundaries.includes("concurrency")) { + return boundaries; + } + return [...boundaries, "concurrency"]; +} + function shouldSkipCOrCppNearbyPath(path: string): boolean { return shouldSkip(path) || isCOrCppDependencyPath(path) || isSampleProjectPath(path); } From 448d62b76f2cd6ad2f94ca431abfbdf541cdb75e Mon Sep 17 00:00:00 2001 From: Daniel Brodie <11337994+danielbrodie@users.noreply.github.com> Date: Fri, 22 May 2026 13:14:25 -0400 Subject: [PATCH 2/5] feat(prompt): add CUDA-aware review and fix guidance When a reviewed feature owns CUDA .cu/.cuh sources, add a CUDA hazard checklist to the review prompt (default mode only) and the fix prompt: kernel races and synchronization, unchecked CUDA runtime calls, host/device pointer confusion, memory-access hazards, and device-memory leaks. Detection is by file extension, so mixed host/device targets are covered. Findings map to the existing categories. Deslopify mode and the prompt-file override are unaffected, and non-CUDA prompts are unchanged. --- src/prompt.test.ts | 106 +++++++++++++++++++++++++++++++++++++++++++++ src/prompt.ts | 26 ++++++++++- 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/src/prompt.test.ts b/src/prompt.test.ts index f6ea20a..747b832 100644 --- a/src/prompt.test.ts +++ b/src/prompt.test.ts @@ -195,6 +195,112 @@ describe("review prompt provenance", () => { }); }); +describe("CUDA prompt guidance", () => { + it("includes CUDA review guidance for a feature that owns a .cu file", async () => { + const root = await fixtureRoot("clawpatch-prompt-cuda-review-"); + await writeFixture(root, "src/kernel.cu", "__global__ void k(void) {}\n"); + const cudaFeature: FeatureRecord = { + ...feature(), + entrypoints: [], + ownedFiles: [{ path: "src/kernel.cu", reason: "kernel" }], + contextFiles: [], + }; + const bundle = await buildReviewPromptBundle(root, project(root), cudaFeature, defaultConfig()); + + expect(bundle.prompt).toContain("CUDA hazards"); + }); + + it("omits CUDA review guidance for a non-CUDA feature", async () => { + const root = await fixtureRoot("clawpatch-prompt-noncuda-review-"); + await writeFixture(root, "src/index.ts", "export const value = 1;\n"); + const tsFeature: FeatureRecord = { + ...feature(), + entrypoints: [], + ownedFiles: [{ path: "src/index.ts", reason: "primary" }], + contextFiles: [], + }; + const bundle = await buildReviewPromptBundle(root, project(root), tsFeature, defaultConfig()); + + expect(bundle.prompt).not.toContain("CUDA hazards"); + }); + + it("omits CUDA review guidance in deslopify mode even for a CUDA feature", async () => { + const root = await fixtureRoot("clawpatch-prompt-cuda-deslopify-"); + await writeFixture(root, "src/kernel.cu", "__global__ void k(void) {}\n"); + const cudaFeature: FeatureRecord = { + ...feature(), + entrypoints: [], + ownedFiles: [{ path: "src/kernel.cu", reason: "kernel" }], + contextFiles: [], + }; + const bundle = await buildReviewPromptBundle( + root, + project(root), + cudaFeature, + defaultConfig(), + "deslopify", + ); + + expect(bundle.prompt).not.toContain("CUDA hazards"); + }); + + it("includes CUDA review guidance for a mixed feature whose entrypoint is C++ but owns a .cu file", async () => { + const root = await fixtureRoot("clawpatch-prompt-cuda-mixed-"); + await writeFixture(root, "src/main.cpp", "int main(void) { return 0; }\n"); + await writeFixture(root, "src/kernel.cu", "__global__ void k(void) {}\n"); + const mixedFeature: FeatureRecord = { + ...feature(), + entrypoints: [{ path: "src/main.cpp", symbol: "main", route: null, command: null }], + ownedFiles: [ + { path: "src/main.cpp", reason: "host" }, + { path: "src/kernel.cu", reason: "kernel" }, + ], + contextFiles: [], + }; + const bundle = await buildReviewPromptBundle( + root, + project(root), + mixedFeature, + defaultConfig(), + ); + + expect(bundle.prompt).toContain("CUDA hazards"); + }); + + it("includes CUDA guidance in the fix prompt for a CUDA feature", async () => { + const root = await fixtureRoot("clawpatch-prompt-cuda-fix-"); + await writeFixture(root, "src/kernel.cu", "__global__ void k(void) {}\n"); + const cudaFeature: FeatureRecord = { + ...feature(), + entrypoints: [], + ownedFiles: [{ path: "src/kernel.cu", reason: "kernel" }], + contextFiles: [], + }; + const prompt = await buildFixPrompt( + root, + finding("src/kernel.cu"), + cudaFeature, + defaultConfig(), + ); + + expect(prompt).toContain("CUDA hazards"); + }); + + it("omits CUDA guidance in the fix prompt for a non-CUDA feature", async () => { + const root = await fixtureRoot("clawpatch-prompt-noncuda-fix-"); + await writeFixture(root, "src/index.ts", "export const value = 1;\n"); + const tsFeature: FeatureRecord = { + ...feature(), + entrypoints: [], + ownedFiles: [{ path: "src/index.ts", reason: "primary" }], + contextFiles: [], + }; + const prompt = await buildFixPrompt(root, finding("src/index.ts"), tsFeature, defaultConfig()); + + expect(prompt).not.toContain("CUDA hazards"); + }); +}); + function project(root: string): ProjectRecord { return { schemaVersion: 1, diff --git a/src/prompt.ts b/src/prompt.ts index e0e4283..9d0e354 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -181,6 +181,8 @@ ${customPrompt.trim()} const validEvidencePaths = [ ...new Set(includedFiles.filter((file) => file.readable).map((file) => file.path)), ]; + const cudaBlock = + mode === "default" && featureIncludesCuda(feature) ? `\n${cudaGuidance()}\n` : ""; const prompt = `You are reviewing one semantic feature for clawpatch. Return strict JSON only. No markdown fences. @@ -204,7 +206,7 @@ ${customBlock}Review categories: - release/build hazards - maintainability risks with concrete impact -${reviewModeInstructions(mode)} +${reviewModeInstructions(mode)}${cudaBlock} Inspect owned files, context files, and linked tests. Treat included tests as first-class evidence of intended behavior. If tests contradict a suspected bug, either skip it or @@ -345,6 +347,25 @@ function reviewModeInstructions(mode: ReviewMode): string { throw new Error(`Unsupported review mode: ${mode}`); } +function featureIncludesCuda(feature: FeatureRecord): boolean { + const paths = [ + ...feature.entrypoints.map((entrypoint) => entrypoint.path), + ...feature.ownedFiles.map((file) => file.path), + ]; + return paths.some((path) => /\.cuh?$/iu.test(path)); +} + +function cudaGuidance(): string { + return `This feature includes CUDA .cu/.cuh sources. Inspect for these CUDA hazards: +- Kernel data races; missing, divergent, or conditionally-reached __syncthreads()/__syncwarp() barriers. +- Unchecked CUDA runtime calls (cudaMalloc, cudaMemcpy, cudaFree, async copies) and missing cudaGetLastError()/cudaDeviceSynchronize() after a kernel launch. +- Host vs. device pointer confusion: dereferencing device memory on the host, or passing the wrong memory space or copy direction to cudaMemcpy. +- Out-of-bounds or uncoalesced global-memory access, shared-memory bank conflicts, and blockIdx/threadIdx-derived indices used without bounds checks. +- Stream and event synchronization errors, including use-after-free across asynchronous copies. +- Device-memory leaks: allocations not freed on every return path. +Map findings to the existing categories (concurrency, bug, data-loss, performance). Report only hazards visible in the included code; do not speculate about GPU runtime behavior you cannot see.`; +} + export async function buildRevalidatePrompt(root: string, findingJson: string): Promise { return `Revalidate this clawpatch finding against the current repository at ${root}. @@ -369,6 +390,7 @@ export async function buildFixPrompt( for (const path of fixPromptPaths(finding, feature, config)) { fileBlocks.push(await rawFileBlock(root, path)); } + const cudaBlock = featureIncludesCuda(feature) ? `\n${cudaGuidance()}\n` : ""; return `You are clawpatch applying one small repair in the current repository. Fix only the finding below. Keep the patch minimal. Add or update focused tests when feasible. @@ -382,7 +404,7 @@ After editing, return strict JSON only: "steps": ["string"], "validationCommands": ["string"] } - +${cudaBlock} Finding: ${JSON.stringify(finding, null, 2)} From 92ef586b873934e865c38cb887c1e30158018df0 Mon Sep 17 00:00:00 2001 From: Daniel Brodie <11337994+danielbrodie@users.noreply.github.com> Date: Fri, 22 May 2026 13:14:25 -0400 Subject: [PATCH 3/5] docs: document CUDA support Update the feature-mapping and code-review docs, the README mapping list, and the changelog for CUDA source mapping, validation command defaults, and CUDA-aware review and fix guidance. --- CHANGELOG.md | 5 +++++ README.md | 6 ++++-- docs/code-review.md | 10 ++++++++++ docs/feature-mapping.md | 11 +++++++++-- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a95637..95a2833 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## 0.4.1 - Unreleased +- Added CUDA support to the C/C++ mapper, mapping `.cu` / `.cuh` sources as standalone `main()` files and as CMake and autotools targets, including the legacy `FindCUDA` `cuda_add_executable` / `cuda_add_library` commands. Repositories containing CUDA sources are detected as `cuda` projects; CUDA targets are tagged `cuda` and carry the `concurrency` trust boundary. +- Added C/C++/CUDA source-group mapping, so source files not owned by any CMake, autotools, or `main()` target are grouped per directory into bounded review slices. +- Added conservative C/C++/CUDA validation command defaults from a root `Makefile` `check` / `test` target or a declared `CMakePresets.json` build workflow, and mapped `CMakeLists.txt`, `CMakePresets.json`, and `configure.ac` as config features. +- Made `clawpatch review` and `clawpatch fix` CUDA-aware, injecting CUDA-specific reviewer guidance (kernel races, unchecked CUDA runtime calls, host/device pointer confusion, memory-access hazards, and synchronization mistakes) for features that own `.cu` / `.cuh` sources. + ## 0.4.0 - 2026-05-22 - Added `clawpatch ci` to initialize, map, review, write a report, and append a GitHub Actions step summary in one CI-friendly command. diff --git a/README.md b/README.md index 83cef66..0ce38c8 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,10 @@ validation commands and records a patch attempt under `.clawpatch/`. Ecto migrations, project scripts, and ExUnit suites - Rust `src/main.rs`, `src/bin/*.rs`, `src/lib.rs`, `crates/*`, and `tests/*.rs` -- C/C++ standalone `main()` files, CMake `add_executable` / `add_library` - targets, and autotools `bin_PROGRAMS` / `lib_LTLIBRARIES` targets +- C/C++/CUDA standalone `main()` files, CMake `add_executable` / `add_library` + targets, autotools `bin_PROGRAMS` / `lib_LTLIBRARIES` targets, and source + groups for files outside any build target, including CUDA `.cu` / `.cuh` + sources - Python project metadata, console scripts, bounded source groups, pytest suites, and Flask/FastAPI/Django routes - SwiftPM `Sources/*` targets and `Tests/*` suites diff --git a/docs/code-review.md b/docs/code-review.md index 3ee65a4..1ca3f03 100644 --- a/docs/code-review.md +++ b/docs/code-review.md @@ -120,5 +120,15 @@ Categories requested from the provider: - `build-release` - `maintainability` +## CUDA-aware review + +When a feature owns CUDA `.cu` / `.cuh` sources, `clawpatch review` (in the +default mode) and `clawpatch fix` add CUDA-specific guidance to the provider +prompt: kernel data races and synchronization barriers, unchecked CUDA runtime +calls and missing post-launch error checks, host versus device pointer +confusion, unsafe global- and shared-memory access, stream and event +synchronization, and device-memory leaks. Findings still use the existing +categories; there is no CUDA-specific category. Deslopify mode is unaffected. + Review does not edit files. Use `clawpatch fix --finding ` for the explicit patch loop. diff --git a/docs/feature-mapping.md b/docs/feature-mapping.md index e8c7172..3e4dede 100644 --- a/docs/feature-mapping.md +++ b/docs/feature-mapping.md @@ -54,7 +54,7 @@ Supported deterministic mappers today: - Ruby project metadata, executables, source groups, RSpec/Minitest suites, Rails configs, routes, views, assets, and database files - Rust Cargo commands, libraries, workspace crates, and integration tests -- C/C++ standalone `main()` files, CMake targets, and autotools targets +- C/C++/CUDA standalone `main()` files, CMake targets, and autotools targets - C#/.NET projects from `.sln`, `.slnx`, `.csproj`, `.fsproj`, and `.vbproj`, ASP.NET Core controllers, minimal API endpoints, C#/F#/Visual Basic source groups, and .NET test projects @@ -155,7 +155,14 @@ files are skipped. C/C++ mapping covers generic project shapes only: standalone source files with `main()`, CMake `add_executable` / `add_library`, and autotools `bin_PROGRAMS` / `lib_LTLIBRARIES`. It deliberately avoids project-specific C dialects such as -php-src extension metadata. +php-src extension metadata. CUDA `.cu` / `.cuh` files are mapped through the same +C/C++ shapes, including the legacy `FindCUDA` `cuda_add_executable` / +`cuda_add_library` commands; CUDA targets are tagged `cuda`, and a repository with +`.cu` / `.cuh` sources is detected as a `cuda` project. Source files not owned by +any build target are grouped per directory into bounded, low-confidence source +groups. C/C++/CUDA validation commands are emitted only when the project declares +them: a root `Makefile` `check`/`test` target, or a `CMakePresets.json` build +workflow. Otherwise they stay null. Python mapping covers `pyproject.toml`, `setup.cfg`, `setup.py`, and `requirements.txt` metadata; `[project.scripts]`, `[tool.poetry.scripts]`, From a0ff4670412f7336d108e76010432c65608fac5e Mon Sep 17 00:00:00 2001 From: Daniel Brodie <11337994+danielbrodie@users.noreply.github.com> Date: Fri, 22 May 2026 19:06:09 -0400 Subject: [PATCH 4/5] fix(mapper): derive CUDA tag from all owned target sources CMake and autotools build targets previously took their language tag from the picked entry path alone. For a mixed CMake target like `add_executable(app main.cpp kernel.cu)`, `pickExecutableEntry` selects the host `main.cpp`, so the feature was tagged `cpp` and missed the `concurrency` trust boundary even though the target owns CUDA code. Add a `targetLanguageTag(entryPath, sourcePaths)` helper that returns `cuda` when any owned source is `.cu` or `.cuh`, otherwise the entry's own tag, and apply it at the five multi-source seed sites: `cmake-bin`, `cmake-lib`, `cmake-test`, `autotools-bin`, and `autotools-lib`. The standalone `c-main` site keeps the per-file tag because it has one source by definition. The new regression test covers the mixed `.cpp` + `.cu` CMake target case where the entry is the host source. --- src/mapper.test.ts | 19 +++++++++++++++++++ src/mappers/c-cpp.ts | 14 ++++++++------ src/mappers/shared.ts | 7 +++++++ 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/mapper.test.ts b/src/mapper.test.ts index 5d25dc7..bf499df 100644 --- a/src/mapper.test.ts +++ b/src/mapper.test.ts @@ -11886,6 +11886,25 @@ add_executable(headerapp include/headers.hpp) expect(gpuapp?.trustBoundaries).toContain("concurrency"); }); + it("tags mixed CMake targets as CUDA when any owned source is .cu", async () => { + const root = await fixtureRoot("clawpatch-cmake-mixed-cuda-"); + await writeFixture( + root, + "CMakeLists.txt", + "project(gpuapp CUDA CXX)\nadd_executable(gpuapp src/main.cpp src/kernel.cu)\n", + ); + await writeFixture(root, "src/main.cpp", "int main(void) { return 0; }\n"); + await writeFixture(root, "src/kernel.cu", "__global__ void scale(float *x) { x[0] = 1.0f; }\n"); + + const project = await detectProject(root); + const result = await mapFeatures(root, project, []); + const gpuapp = result.features.find((feature) => feature.title === "CMake binary gpuapp"); + + expect(gpuapp?.entrypoints[0]).toMatchObject({ path: "src/main.cpp", symbol: "main" }); + expect(gpuapp?.tags).toContain("cuda"); + expect(gpuapp?.trustBoundaries).toContain("concurrency"); + }); + it("maps CMake and autotools build files as config features", async () => { const root = await fixtureRoot("clawpatch-build-config-"); await writeFixture(root, "CMakeLists.txt", "project(app CXX)\nadd_executable(app main.cpp)\n"); diff --git a/src/mappers/c-cpp.ts b/src/mappers/c-cpp.ts index 9c35a79..7d9a93c 100644 --- a/src/mappers/c-cpp.ts +++ b/src/mappers/c-cpp.ts @@ -10,6 +10,7 @@ import { packageTrustBoundaries, shouldSkip, stripLineComments, + targetLanguageTag, walk, withCudaConcurrency, } from "./shared.js"; @@ -78,7 +79,7 @@ async function autotoolsTargets(root: string, files: string[]): Promise languageTag(path) === "cuda")) { + return "cuda"; + } + return languageTag(entryPath); +} + export function withCudaConcurrency( boundaries: TrustBoundary[], tag: LanguageTag, From eace83d36573ab0806df5ba2402388120077af59 Mon Sep 17 00:00:00 2001 From: Daniel Brodie <11337994+danielbrodie@users.noreply.github.com> Date: Fri, 22 May 2026 19:06:09 -0400 Subject: [PATCH 5/5] chore(detect): narrow Makefile validation to declared targets only The previous default emitted `typecheck: "make"` for any root `Makefile`, which would run the project's default Makefile target during `clawpatch fix` validation. That broadened local code execution to any Makefile-bearing C/C++ repository, beyond what the project explicitly declared. Narrow the default so the Makefile path only emits `test: "make check"` or `test: "make test"` when the Makefile declares those targets, and otherwise returns no commands. This aligns the implementation with the `docs/feature-mapping.md` description ("only when the project declares them: a root Makefile check/test target, or a CMakePresets.json build workflow") and the matching `CHANGELOG.md` entry, both of which already say "declared" rather than "any". The two existing Makefile validation tests are renamed and updated to match the narrowed shape. --- src/detect.ts | 5 ++++- src/mapper.test.ts | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/detect.ts b/src/detect.ts index aeb72fe..3ffecdc 100644 --- a/src/detect.ts +++ b/src/detect.ts @@ -716,7 +716,10 @@ async function makefileDefaultCommands(root: string): Promise { + it("emits `make check` as the C/C++ test command when the Makefile declares a check target", async () => { const root = await fixtureRoot("clawpatch-cpp-makefile-check-"); await writeFixture(root, "Makefile", "all:\n\tcc -o app main.c\n\ncheck:\n\t./app\n"); await writeFixture(root, "main.c", "int main(void) { return 0; }\n"); const project = await detectProject(root); - expect(project.detected.commands.typecheck).toBe("make"); + expect(project.detected.commands.typecheck).toBeNull(); expect(project.detected.commands.test).toBe("make check"); }); - it("defaults C/C++ validation to make with no test command when the Makefile has none", async () => { + it("emits no C/C++ validation commands when the Makefile has no check or test target", async () => { const root = await fixtureRoot("clawpatch-cpp-makefile-notest-"); await writeFixture(root, "Makefile", "all:\n\tcc -o app main.c\n"); await writeFixture(root, "main.c", "int main(void) { return 0; }\n"); const project = await detectProject(root); - expect(project.detected.commands.typecheck).toBe("make"); + expect(project.detected.commands.typecheck).toBeNull(); expect(project.detected.commands.test).toBeNull(); });