Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## 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.
- 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.
- 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.
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions docs/code-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id>` for the explicit
patch loop.
17 changes: 15 additions & 2 deletions docs/feature-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -155,7 +155,20 @@ 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. When a project mixes C/C++/CUDA sources
with another language whose defaults sit higher in the language-priority
order, such as a Python repository with CUDA kernels, the C/C++/CUDA commands
are also persisted alongside the primary command set as `nativeCommands`, and
`clawpatch fix` selects them for features tagged `c`, `cpp`, or `cuda` so a
CUDA repair is validated with the declared Makefile or CMake preset commands
rather than the project's primary test runner.

Python mapping covers `pyproject.toml`, `setup.cfg`, `setup.py`, and
`requirements.txt` metadata; `[project.scripts]`, `[tool.poetry.scripts]`,
Expand Down
24 changes: 20 additions & 4 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,11 @@ export async function initCommand(
const paths = statePaths(stateDir);
await ensureStateDirs(paths);
const project = await detectProject(context.root);
const detectedConfig = { ...config, commands: project.detected.commands };
const detectedConfig = {
...config,
commands: project.detected.commands,
nativeCommands: project.detected.nativeCommands ?? null,
};
const previous = await readProject(paths);
if (previous !== null && flags["force"] !== true) {
throw new ClawpatchError("project already initialized; use --force", 2, "already-initialized");
Expand Down Expand Up @@ -550,7 +554,11 @@ export async function showCommand(
const record = assertDefined(finding, `finding not found: ${findingId}`);
const feature = features.find((candidate) => candidate.featureId === record.featureId) ?? null;
const linkedPatches = patches.filter((patch) => patch.findingIds.includes(record.findingId));
const validation = validationCommandsForFeature(feature, loaded.config.commands);
const validation = validationCommandsForFeature(
feature,
loaded.config.commands,
loaded.config.nativeCommands ?? null,
);
if (context.options.json) {
return {
finding: findingSummary(record, feature),
Expand Down Expand Up @@ -1032,7 +1040,11 @@ export async function fixCommand(
};
const prompt = await buildFixPrompt(loaded.root, finding, feature, config);
if (flags["dryRun"] === true) {
const validationCommands = validationCommandsForFeature(feature, config.commands);
const validationCommands = validationCommandsForFeature(
feature,
config.commands,
config.nativeCommands ?? null,
);
return {
finding: finding.findingId,
dryRun: true,
Expand Down Expand Up @@ -1073,7 +1085,11 @@ export async function fixCommand(
});
throw error;
}
const validationCommands = validationCommandsForFeature(feature, config.commands);
const validationCommands = validationCommandsForFeature(
feature,
config.commands,
config.nativeCommands ?? null,
);
const commandsRun: CommandResult[] = [];
for (const command of validationCommands) {
commandsRun.push(await runCommand(command, loaded.root));
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function defaultConfig(): ClawpatchConfig {
reasoningEffort: null,
},
commands: defaultCommands,
nativeCommands: null,
review: {
maxContextFiles: 24,
maxOwnedFiles: 12,
Expand Down
154 changes: 154 additions & 0 deletions src/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export async function detectProject(root: string): Promise<ProjectRecord> {
const frameworks = await detectFrameworks(root, pkg, composer);
const languages = await detectLanguages(root);
const commands = await detectCommands(root, pkg, composer, languages, packageManagers);
const nativeCommands = await detectNativeCommands(root, languages, commands);
const name =
typeof pkg?.name === "string"
? pkg.name
Expand All @@ -69,6 +70,7 @@ export async function detectProject(root: string): Promise<ProjectRecord> {
frameworks,
packageManagers,
commands,
nativeCommands,
},
createdAt: now,
updatedAt: now,
Expand Down Expand Up @@ -249,6 +251,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,
Expand Down Expand Up @@ -691,6 +696,145 @@ async function rubyDefaultCommands(root: string): Promise<ProjectCommands> {
};
}

async function detectNativeCommands(
root: string,
languages: string[],
primary: ProjectCommands,
): Promise<ProjectCommands | null> {
if (
!languages.some((language) => language === "c" || language === "cpp" || language === "cuda")
) {
return null;
}
const native = await cOrCppDefaultCommands(root);
if (!hasValidationCommand(native)) {
return null;
}
if (projectCommandsEqual(native, primary)) {
return null;
}
return native;
}

function projectCommandsEqual(a: ProjectCommands, b: ProjectCommands): boolean {
return (
a.typecheck === b.typecheck && a.lint === b.lint && a.format === b.format && a.test === b.test
);
}

async function cOrCppDefaultCommands(root: string): Promise<ProjectCommands> {
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<ProjectCommands | null> {
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;
if (test === null) {
return null;
}
return { typecheck: null, 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<ProjectCommands | null> {
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<CMakePresetSets | null> {
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<string, unknown>;
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<MixProjectInfo> {
if (!(await pathExists(join(root, "mix.exs")))) {
return { dependencies: new Set() };
Expand Down Expand Up @@ -1285,6 +1429,9 @@ async function detectLanguages(root: string): Promise<string[]> {
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");
}
Expand Down Expand Up @@ -1339,6 +1486,13 @@ async function containsCFile(root: string): Promise<boolean> {
return containsFileWithExtension(root, ".c", 5, shouldSkipCOrCppSearchEntry);
}

async function containsCudaFile(root: string): Promise<boolean> {
return (
(await containsFileWithExtensionIgnoringCase(root, ".cu", 5, shouldSkipCOrCppSearchEntry)) ||
(await containsFileWithExtensionIgnoringCase(root, ".cuh", 5, shouldSkipCOrCppSearchEntry))
);
}

async function containsCppFile(root: string): Promise<boolean> {
return (
(await containsFileWithExtension(root, ".C", 5, shouldSkipCOrCppSearchEntry)) ||
Expand Down
Loading