Skip to content

Commit f0c522d

Browse files
authored
Merge 991fa79 into ed3d575
2 parents ed3d575 + 991fa79 commit f0c522d

19 files changed

Lines changed: 895 additions & 39 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## 0.4.1 - Unreleased
44

5+
- 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.
6+
- 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.
7+
- 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.
8+
- Added per-feature selection of the C/C++/CUDA validation commands so features tagged `c`, `cpp`, or `cuda` use the declared Makefile or CMake preset commands even when another language wins the project's language-priority defaults, for example a Python repository with CUDA kernels. The detected native command set is persisted as a nullable `nativeCommands` field on the project record and config, and existing `.clawpatch/config.json` files load without migration.
9+
- 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.
10+
511
## 0.4.0 - 2026-05-22
612

713
- Added `clawpatch ci` to initialize, map, review, write a report, and append a GitHub Actions step summary in one CI-friendly command.

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,10 @@ validation commands and records a patch attempt under `.clawpatch/`.
8383
Ecto migrations, project scripts, and ExUnit suites
8484
- Rust `src/main.rs`, `src/bin/*.rs`, `src/lib.rs`, `crates/*`, and
8585
`tests/*.rs`
86-
- C/C++ standalone `main()` files, CMake `add_executable` / `add_library`
87-
targets, and autotools `bin_PROGRAMS` / `lib_LTLIBRARIES` targets
86+
- C/C++/CUDA standalone `main()` files, CMake `add_executable` / `add_library`
87+
targets, autotools `bin_PROGRAMS` / `lib_LTLIBRARIES` targets, and source
88+
groups for files outside any build target, including CUDA `.cu` / `.cuh`
89+
sources
8890
- Python project metadata, console scripts, bounded source groups, pytest suites,
8991
and Flask/FastAPI/Django routes
9092
- SwiftPM `Sources/*` targets and `Tests/*` suites

docs/code-review.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,5 +120,15 @@ Categories requested from the provider:
120120
- `build-release`
121121
- `maintainability`
122122

123+
## CUDA-aware review
124+
125+
When a feature owns CUDA `.cu` / `.cuh` sources, `clawpatch review` (in the
126+
default mode) and `clawpatch fix` add CUDA-specific guidance to the provider
127+
prompt: kernel data races and synchronization barriers, unchecked CUDA runtime
128+
calls and missing post-launch error checks, host versus device pointer
129+
confusion, unsafe global- and shared-memory access, stream and event
130+
synchronization, and device-memory leaks. Findings still use the existing
131+
categories; there is no CUDA-specific category. Deslopify mode is unaffected.
132+
123133
Review does not edit files. Use `clawpatch fix --finding <id>` for the explicit
124134
patch loop.

docs/feature-mapping.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Supported deterministic mappers today:
5454
- Ruby project metadata, executables, source groups, RSpec/Minitest suites,
5555
Rails configs, routes, views, assets, and database files
5656
- Rust Cargo commands, libraries, workspace crates, and integration tests
57-
- C/C++ standalone `main()` files, CMake targets, and autotools targets
57+
- C/C++/CUDA standalone `main()` files, CMake targets, and autotools targets
5858
- C#/.NET projects from `.sln`, `.slnx`, `.csproj`, `.fsproj`, and `.vbproj`,
5959
ASP.NET Core controllers, minimal API endpoints, C#/F#/Visual Basic source
6060
groups, and .NET test projects
@@ -155,7 +155,20 @@ files are skipped.
155155
C/C++ mapping covers generic project shapes only: standalone source files with
156156
`main()`, CMake `add_executable` / `add_library`, and autotools `bin_PROGRAMS` /
157157
`lib_LTLIBRARIES`. It deliberately avoids project-specific C dialects such as
158-
php-src extension metadata.
158+
php-src extension metadata. CUDA `.cu` / `.cuh` files are mapped through the same
159+
C/C++ shapes, including the legacy `FindCUDA` `cuda_add_executable` /
160+
`cuda_add_library` commands; CUDA targets are tagged `cuda`, and a repository with
161+
`.cu` / `.cuh` sources is detected as a `cuda` project. Source files not owned by
162+
any build target are grouped per directory into bounded, low-confidence source
163+
groups. C/C++/CUDA validation commands are emitted only when the project declares
164+
them: a root `Makefile` `check`/`test` target, or a `CMakePresets.json` build
165+
workflow. Otherwise they stay null. When a project mixes C/C++/CUDA sources
166+
with another language whose defaults sit higher in the language-priority
167+
order, such as a Python repository with CUDA kernels, the C/C++/CUDA commands
168+
are also persisted alongside the primary command set as `nativeCommands`, and
169+
`clawpatch fix` selects them for features tagged `c`, `cpp`, or `cuda` so a
170+
CUDA repair is validated with the declared Makefile or CMake preset commands
171+
rather than the project's primary test runner.
159172

160173
Python mapping covers `pyproject.toml`, `setup.cfg`, `setup.py`, and
161174
`requirements.txt` metadata; `[project.scripts]`, `[tool.poetry.scripts]`,

src/app.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,11 @@ export async function initCommand(
9494
const paths = statePaths(stateDir);
9595
await ensureStateDirs(paths);
9696
const project = await detectProject(context.root);
97-
const detectedConfig = { ...config, commands: project.detected.commands };
97+
const detectedConfig = {
98+
...config,
99+
commands: project.detected.commands,
100+
nativeCommands: project.detected.nativeCommands ?? null,
101+
};
98102
const previous = await readProject(paths);
99103
if (previous !== null && flags["force"] !== true) {
100104
throw new ClawpatchError("project already initialized; use --force", 2, "already-initialized");
@@ -550,7 +554,11 @@ export async function showCommand(
550554
const record = assertDefined(finding, `finding not found: ${findingId}`);
551555
const feature = features.find((candidate) => candidate.featureId === record.featureId) ?? null;
552556
const linkedPatches = patches.filter((patch) => patch.findingIds.includes(record.findingId));
553-
const validation = validationCommandsForFeature(feature, loaded.config.commands);
557+
const validation = validationCommandsForFeature(
558+
feature,
559+
loaded.config.commands,
560+
loaded.config.nativeCommands ?? null,
561+
);
554562
if (context.options.json) {
555563
return {
556564
finding: findingSummary(record, feature),
@@ -1032,7 +1040,11 @@ export async function fixCommand(
10321040
};
10331041
const prompt = await buildFixPrompt(loaded.root, finding, feature, config);
10341042
if (flags["dryRun"] === true) {
1035-
const validationCommands = validationCommandsForFeature(feature, config.commands);
1043+
const validationCommands = validationCommandsForFeature(
1044+
feature,
1045+
config.commands,
1046+
config.nativeCommands ?? null,
1047+
);
10361048
return {
10371049
finding: finding.findingId,
10381050
dryRun: true,
@@ -1073,7 +1085,11 @@ export async function fixCommand(
10731085
});
10741086
throw error;
10751087
}
1076-
const validationCommands = validationCommandsForFeature(feature, config.commands);
1088+
const validationCommands = validationCommandsForFeature(
1089+
feature,
1090+
config.commands,
1091+
config.nativeCommands ?? null,
1092+
);
10771093
const commandsRun: CommandResult[] = [];
10781094
for (const command of validationCommands) {
10791095
commandsRun.push(await runCommand(command, loaded.root));

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export function defaultConfig(): ClawpatchConfig {
4949
reasoningEffort: null,
5050
},
5151
commands: defaultCommands,
52+
nativeCommands: null,
5253
review: {
5354
maxContextFiles: 24,
5455
maxOwnedFiles: 12,

src/detect.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export async function detectProject(root: string): Promise<ProjectRecord> {
4646
const frameworks = await detectFrameworks(root, pkg, composer);
4747
const languages = await detectLanguages(root);
4848
const commands = await detectCommands(root, pkg, composer, languages, packageManagers);
49+
const nativeCommands = await detectNativeCommands(root, languages, commands);
4950
const name =
5051
typeof pkg?.name === "string"
5152
? pkg.name
@@ -69,6 +70,7 @@ export async function detectProject(root: string): Promise<ProjectRecord> {
6970
frameworks,
7071
packageManagers,
7172
commands,
73+
nativeCommands,
7274
},
7375
createdAt: now,
7476
updatedAt: now,
@@ -249,6 +251,9 @@ async function languageDefaultCommands(
249251
if (languages.includes("ruby")) {
250252
return rubyDefaultCommands(root);
251253
}
254+
if (languages.includes("c") || languages.includes("cpp") || languages.includes("cuda")) {
255+
return cOrCppDefaultCommands(root);
256+
}
252257

253258
return {
254259
typecheck: null,
@@ -691,6 +696,145 @@ async function rubyDefaultCommands(root: string): Promise<ProjectCommands> {
691696
};
692697
}
693698

699+
async function detectNativeCommands(
700+
root: string,
701+
languages: string[],
702+
primary: ProjectCommands,
703+
): Promise<ProjectCommands | null> {
704+
if (
705+
!languages.some((language) => language === "c" || language === "cpp" || language === "cuda")
706+
) {
707+
return null;
708+
}
709+
const native = await cOrCppDefaultCommands(root);
710+
if (!hasValidationCommand(native)) {
711+
return null;
712+
}
713+
if (projectCommandsEqual(native, primary)) {
714+
return null;
715+
}
716+
return native;
717+
}
718+
719+
function projectCommandsEqual(a: ProjectCommands, b: ProjectCommands): boolean {
720+
return (
721+
a.typecheck === b.typecheck && a.lint === b.lint && a.format === b.format && a.test === b.test
722+
);
723+
}
724+
725+
async function cOrCppDefaultCommands(root: string): Promise<ProjectCommands> {
726+
const makefileCommands = await makefileDefaultCommands(root);
727+
if (makefileCommands !== null) {
728+
return makefileCommands;
729+
}
730+
const presetCommands = await cmakePresetDefaultCommands(root);
731+
if (presetCommands !== null) {
732+
return presetCommands;
733+
}
734+
return { typecheck: null, lint: null, format: null, test: null };
735+
}
736+
737+
async function makefileDefaultCommands(root: string): Promise<ProjectCommands | null> {
738+
if (!(await pathExists(join(root, "Makefile")))) {
739+
return null;
740+
}
741+
const source = await readFile(join(root, "Makefile"), "utf8").catch(() => "");
742+
const test = makefileHasTarget(source, "check")
743+
? "make check"
744+
: makefileHasTarget(source, "test")
745+
? "make test"
746+
: null;
747+
if (test === null) {
748+
return null;
749+
}
750+
return { typecheck: null, lint: null, format: null, test };
751+
}
752+
753+
function makefileHasTarget(source: string, target: string): boolean {
754+
return new RegExp(`^${target}\\s*:(?!=)`, "mu").test(source);
755+
}
756+
757+
type CMakePresetSets = {
758+
workflowPresets: string[];
759+
configurePresets: string[];
760+
buildPresets: string[];
761+
testPresets: string[];
762+
};
763+
764+
async function cmakePresetDefaultCommands(root: string): Promise<ProjectCommands | null> {
765+
if (!(await pathExists(join(root, "CMakePresets.json")))) {
766+
return null;
767+
}
768+
const presets = await readCMakePresets(root);
769+
if (presets === null) {
770+
return null;
771+
}
772+
const testPreset = singlePresetName(presets.testPresets);
773+
return {
774+
typecheck: cmakeBuildCommand(presets),
775+
lint: null,
776+
format: null,
777+
test: testPreset === null ? null : `ctest --preset ${testPreset}`,
778+
};
779+
}
780+
781+
function cmakeBuildCommand(presets: CMakePresetSets): string | null {
782+
const workflow = singlePresetName(presets.workflowPresets);
783+
if (workflow !== null) {
784+
return `cmake --workflow --preset ${workflow}`;
785+
}
786+
const configure = singlePresetName(presets.configurePresets);
787+
const build = singlePresetName(presets.buildPresets);
788+
if (configure !== null && build !== null) {
789+
return `cmake --preset ${configure} && cmake --build --preset ${build}`;
790+
}
791+
return null;
792+
}
793+
794+
function singlePresetName(names: string[]): string | null {
795+
return names.length === 1 ? (names[0] ?? null) : null;
796+
}
797+
798+
async function readCMakePresets(root: string): Promise<CMakePresetSets | null> {
799+
let parsed: unknown;
800+
try {
801+
parsed = JSON.parse(await readFile(join(root, "CMakePresets.json"), "utf8"));
802+
} catch {
803+
return null;
804+
}
805+
if (typeof parsed !== "object" || parsed === null) {
806+
return null;
807+
}
808+
const record = parsed as Record<string, unknown>;
809+
return {
810+
workflowPresets: cmakePresetNames(record["workflowPresets"]),
811+
configurePresets: cmakePresetNames(record["configurePresets"]),
812+
buildPresets: cmakePresetNames(record["buildPresets"]),
813+
testPresets: cmakePresetNames(record["testPresets"]),
814+
};
815+
}
816+
817+
function cmakePresetNames(value: unknown): string[] {
818+
if (!Array.isArray(value)) {
819+
return [];
820+
}
821+
const names: string[] = [];
822+
for (const entry of value) {
823+
if (typeof entry !== "object" || entry === null) {
824+
continue;
825+
}
826+
const preset = entry as { name?: unknown; hidden?: unknown };
827+
if (
828+
typeof preset.name === "string" &&
829+
preset.hidden !== true &&
830+
/^[A-Za-z0-9._-]+$/u.test(preset.name)
831+
) {
832+
names.push(preset.name);
833+
}
834+
}
835+
return names;
836+
}
837+
694838
async function mixProjectInfo(root: string): Promise<MixProjectInfo> {
695839
if (!(await pathExists(join(root, "mix.exs")))) {
696840
return { dependencies: new Set() };
@@ -1285,6 +1429,9 @@ async function detectLanguages(root: string): Promise<string[]> {
12851429
if (!languages.includes("cpp") && (await containsCppFile(root))) {
12861430
languages.push("cpp");
12871431
}
1432+
if (!languages.includes("cuda") && (await containsCudaFile(root))) {
1433+
languages.push("cuda");
1434+
}
12881435
if (!languages.includes("php") && (await containsReviewablePhpFile(root))) {
12891436
languages.push("php");
12901437
}
@@ -1339,6 +1486,13 @@ async function containsCFile(root: string): Promise<boolean> {
13391486
return containsFileWithExtension(root, ".c", 5, shouldSkipCOrCppSearchEntry);
13401487
}
13411488

1489+
async function containsCudaFile(root: string): Promise<boolean> {
1490+
return (
1491+
(await containsFileWithExtensionIgnoringCase(root, ".cu", 5, shouldSkipCOrCppSearchEntry)) ||
1492+
(await containsFileWithExtensionIgnoringCase(root, ".cuh", 5, shouldSkipCOrCppSearchEntry))
1493+
);
1494+
}
1495+
13421496
async function containsCppFile(root: string): Promise<boolean> {
13431497
return (
13441498
(await containsFileWithExtension(root, ".C", 5, shouldSkipCOrCppSearchEntry)) ||

0 commit comments

Comments
 (0)