Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- Fixed `clawpatch ci --since` empty-review output so it reports `reviewed: 0`.
- Fixed formatter configuration so `oxfmt` uses two-space indentation consistently across platforms.
- Added generic package-less monorepo app-root mapping for Node/Next projects under roots such as `apps/*` and `packages/*` when positive source or framework signals are present.
- Added Maven project mapping for root, nested, and multi-module Java/Kotlin projects with Spring role slices, Maven validation defaults, and `pom.xml` detection, thanks @julianshess.
- Added a release-prep checklist for auditing changelog, package metadata, and dry-run package contents without publishing.
- Improved OpenCode malformed JSON diagnostics with output length, event kinds, and a bounded preview, thanks @rohitjavvadi.
- Fixed Express route mapping for aliased Router imports that follow block comment banners, thanks @rohitjavvadi.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ validation commands and records a patch attempt under `.clawpatch/`.
- React Router routes and React components
- Go package slices from `go list ./...`, including command packages
- Go package tests and same-repo imports as review context
- Java/Kotlin Gradle source groups and root Gradle build/test commands
- Java/Kotlin Gradle source groups, Maven source groups, and root Gradle/Maven
build/test commands
- JVM semantic roles from Java and Kotlin code evidence such as annotations,
imports, interfaces, inheritance, supertypes, and method signatures
- Kotlin Android semantic roles for UI entrypoints, ViewModels, data
Expand Down
17 changes: 11 additions & 6 deletions docs/feature-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ Supported deterministic mappers today:
- nested SwiftPM packages
- Apple/Xcode projects from `project.yml`, `.xcodeproj`, or `.xcworkspace`
- Java/Kotlin Gradle modules from `settings.gradle(.kts)` and `build.gradle(.kts)`
- Java/Kotlin Maven modules from root and nested `pom.xml` files, including
multi-module projects
- Laravel/PHP projects from `composer.json` and `artisan`, including controllers
referenced by routes, form requests, Artisan commands, jobs, services, models,
migrations, seeders, Composer scripts, and grouped PHP test suites
Expand Down Expand Up @@ -129,12 +131,15 @@ lazy import, and also maps page/component files under `src/pages` and
`src/components` as UI-flow slices.
Native app mappers use the same bounded grouping model. SwiftPM packages can be
discovered below the repo root, Apple projects are grouped by Swift source area,
and Gradle modules are grouped from `src/main`, `src/test`, and `src/androidTest`.
Root Gradle projects get default `gradle`/`./gradlew` build and test commands.
Java and Kotlin files in Gradle modules also get role-oriented review slices
when code evidence identifies web entrypoints, services, persistence boundaries,
external clients, configuration, framework components, extension boundaries,
Android UI entrypoints, ViewModels, data boundaries, or dependency injection.
Gradle modules are grouped from `src/main`, `src/test`, and `src/androidTest`,
and Maven modules are grouped from `src/main` and `src/test`. Root Gradle
projects get default `gradle`/`./gradlew` build and test commands; root Maven
projects get default `mvn`/`./mvnw` compile and test commands.
Java and Kotlin files in Gradle modules, plus Java files in Maven modules, also
get role-oriented review slices when code evidence identifies web entrypoints,
services, persistence boundaries, external clients, configuration, framework
components, extension boundaries, Android UI entrypoints, ViewModels, data
boundaries, or dependency injection.
Kotlin dependency-injection evidence includes Hilt, Dagger, Koin, and Metro
annotations and imports.

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ stderr so pipes stay parseable.

## What clawpatch does

- **Semantic feature mapping.** Detects npm bins, Next.js routes, React Router routes, Python packages and Flask/FastAPI/Django routes, Ruby/Rails slices, Laravel/PHP slices, Java/Kotlin Gradle modules, C#/.NET projects and ASP.NET endpoints, Go packages, Rust crates, C/C++ build targets, SwiftPM targets, and common config files as reviewable units.
- **Semantic feature mapping.** Detects npm bins, Next.js routes, React Router routes, Python packages and Flask/FastAPI/Django routes, Ruby/Rails slices, Laravel/PHP slices, Java/Kotlin Gradle and Maven modules, C#/.NET projects and ASP.NET endpoints, Go packages, Rust crates, C/C++ build targets, SwiftPM targets, and common config files as reviewable units.
- **Automated code review.** Reviews features with AI providers (Codex CLI today), persists findings with severity, category, and line locations.
- **Explicit fix workflow.** `clawpatch fix` runs validated patches for one finding at a time, never commits or pushes automatically.
- **Stable state model.** All features, findings, patches live in `.clawpatch/` as JSON, resumable across runs.
Expand Down
2 changes: 1 addition & 1 deletion docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ This discovers reviewable features:
- npm package bins and root/workspace scripts
- Next.js routes
- Go packages and commands
- Java/Kotlin Gradle modules
- Java/Kotlin Gradle and Maven modules
- Python packages, console scripts, Flask/FastAPI/Django routes, and pytest suites
- C#/.NET projects, ASP.NET endpoints, source groups, and test projects
- JVM semantic role groups
Expand Down
105 changes: 104 additions & 1 deletion src/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ async function languageDefaultCommands(
) {
return gradleDefaultCommands(root);
}
if (await isRootMavenProject(root)) {
return mavenDefaultCommands(root);
}
if (languages.includes("elixir")) {
return elixirDefaultCommands(root);
}
Expand Down Expand Up @@ -331,6 +334,9 @@ async function detectPackageManagers(root: string): Promise<string[]> {
) {
found.push("gradle");
}
if (!found.includes("maven") && (await containsFileNamed(root, "pom.xml", 5))) {
found.push("maven");
}
if (
!found.includes("cmake") &&
(await containsFileNamed(root, "CMakeLists.txt", 5, shouldSkipCOrCppSearchEntry))
Expand Down Expand Up @@ -408,6 +414,20 @@ async function gradleDefaultCommands(root: string): Promise<ProjectCommands> {
};
}

async function isRootMavenProject(root: string): Promise<boolean> {
return await pathExists(join(root, "pom.xml"));
}

async function mavenDefaultCommands(root: string): Promise<ProjectCommands> {
const runner = (await pathExists(join(root, "mvnw"))) ? "./mvnw" : "mvn";
return {
typecheck: `${runner} -DskipTests compile`,
lint: null,
format: null,
test: `${runner} test`,
};
}

async function dotnetDefaultCommands(root: string): Promise<ProjectCommands> {
const target = await dotnetValidationTarget(root);
const testTarget = await dotnetTestTarget(root, target);
Expand Down Expand Up @@ -1114,6 +1134,11 @@ async function detectFrameworks(
frameworks.push(name);
}
}
for (const name of await detectMavenFrameworks(root)) {
if (!frameworks.includes(name)) {
frameworks.push(name);
}
}
for (const name of await detectDotnetFrameworks(root)) {
if (!frameworks.includes(name)) {
frameworks.push(name);
Expand All @@ -1122,6 +1147,32 @@ async function detectFrameworks(
return uniqueStrings(frameworks);
}

async function detectMavenFrameworks(root: string): Promise<string[]> {
const frameworks: string[] = [];
for (const pom of await collectMavenPomFiles(root, 5)) {
const source = await readFile(join(root, pom), "utf8").catch(() => "");
const activeSource = stripXmlComments(source);
if (mavenPomHasSpring(activeSource)) {
frameworks.push("spring");
}
if (mavenPomHasSpringBoot(activeSource)) {
frameworks.push("spring-boot");
}
}
return uniqueStrings(frameworks);
}

function mavenPomHasSpring(source: string): boolean {
return /<groupId>\s*org\.springframework(?:\.[^<]*)?\s*<\/groupId>/iu.test(source);
}

function mavenPomHasSpringBoot(source: string): boolean {
return (
/<groupId>\s*org\.springframework\.boot\s*<\/groupId>/iu.test(source) ||
/<artifactId>\s*spring-boot-[^<]*\s*<\/artifactId>/iu.test(source)
);
}

async function detectDotnetFrameworks(root: string): Promise<string[]> {
const frameworks: string[] = [];
for (const project of await collectDotnetFiles(root, isDotnetProjectFileName, 5)) {
Expand Down Expand Up @@ -1720,6 +1771,43 @@ async function collectDotnetFiles(
return [...new Set(files)].toSorted();
}

async function collectMavenPomFiles(root: string, maxDepth: number): Promise<string[]> {
const files: string[] = [];
await collectMavenPomFilesAt(root, maxDepth, files);
return [...new Set(files)].toSorted();
}

async function collectMavenPomFilesAt(
dir: string,
remainingDepth: number,
files: string[],
relativeDir = "",
): Promise<void> {
if (remainingDepth < 0 || !(await pathExists(dir))) {
return;
}
const dirInfo = await lstat(dir);
if (!dirInfo.isDirectory() || dirInfo.isSymbolicLink()) {
return;
}
for (const entry of await readdir(dir)) {
const relativePath = relativeDir.length === 0 ? entry : `${relativeDir}/${entry}`;
if (shouldSkipSearchEntry(entry, relativePath)) {
continue;
}
const full = join(dir, entry);
const info = await lstat(full);
if (info.isSymbolicLink()) {
continue;
}
if (info.isFile() && entry === "pom.xml") {
files.push(relativePath);
} else if (info.isDirectory()) {
await collectMavenPomFilesAt(full, remainingDepth - 1, files, relativePath);
}
}
}

async function collectDotnetFilesAt(
dir: string,
remainingDepth: number,
Expand Down Expand Up @@ -1781,7 +1869,22 @@ function hasDotnetTestFrameworkEvidence(source: string): boolean {
}

function stripXmlComments(source: string): string {
return source.replace(/<!--[\s\S]*?-->/gu, "");
let output = "";
let index = 0;
while (index < source.length) {
const start = source.indexOf("<!--", index);
if (start === -1) {
output += source.slice(index);
break;
}
output += source.slice(index, start);
const end = source.indexOf("-->", start + 4);
if (end === -1) {
break;
}
index = end + 3;
}
return output;
}

function isDotnetWebProject(source: string): boolean {
Expand Down
Loading