Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2ae322b
Add Maven multi-module repository adapter
adityankale190895 Sep 21, 2026
d56138c
Register Maven repository adapter
adityankale190895 Sep 21, 2026
6520bf1
Track Maven test module ownership
adityankale190895 Sep 21, 2026
2121c41
Integrate Maven adapter into graph capability
adityankale190895 Sep 21, 2026
8a00de4
Classify Java and Maven manifest changes
adityankale190895 Sep 21, 2026
92f4033
Plan selective Maven reactor test commands
adityankale190895 Sep 21, 2026
4898258
Discover conventional Maven Surefire tests
adityankale190895 Sep 21, 2026
6ae2664
Test multi-module Maven impact propagation
adityankale190895 Sep 21, 2026
a1c0727
Assign Java files to nearest Maven module
adityankale190895 Sep 21, 2026
d3a9cb3
Review Maven adapter into release boundary
adityankale190895 Sep 21, 2026
289f7b6
Validate Spring multi-module Maven repository shape
adityankale190895 Sep 21, 2026
e55bec9
Extend Maven module analysis to Kotlin sources
adityankale190895 Sep 21, 2026
a978114
Classify Kotlin as Maven source
adityankale190895 Sep 21, 2026
7399af5
Route Kotlin Maven tests through reactor planner
adityankale190895 Sep 21, 2026
7271694
Discover Maven Kotlin test conventions
adityankale190895 Sep 21, 2026
cfe704a
Recognize Kotlin Maven repositories as supported
adityankale190895 Sep 21, 2026
b078139
Detect Kotlin-only Maven reactors
adityankale190895 Sep 21, 2026
dd5c5b1
Test Kotlin Maven reactor impact propagation
adityankale190895 Sep 21, 2026
1d220d5
Resolve Maven project groupId properties for reactor edges
adityankale190895 Sep 21, 2026
aa8759a
Cover Jicofo-style Maven module dependencies
adityankale190895 Sep 21, 2026
7cac46c
Validate Jicofo three-module reactor propagation
adityankale190895 Sep 21, 2026
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
2 changes: 1 addition & 1 deletion release-files.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"src/repo/analyzer.ts",
"src/repo/adapters/go-test.ts",
"src/repo/adapters/go.ts",
"src/repo/adapters/maven.ts",
"src/repo/adapters/index.ts",
"src/repo/adapters/types.ts",
"src/repo/adapters/vue.ts",
Expand Down Expand Up @@ -92,4 +93,3 @@
"tsconfig.json"
]
}

15 changes: 15 additions & 0 deletions src/planner/test-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,21 @@ export function planSelectiveTestCommands(
const group: SelectiveTestCommandGroup = { runnerId: "go:test", label: "Go package tests", paths: [...goPaths].sort(), commandSpec };
return { commands: [...jsPlan.commands, commandSpec], groups: [...jsPlan.groups, group], unroutedPaths: [] };
}
const javaPaths = selectedPaths.filter((path) => path.endsWith(".java") || path.endsWith(".kt"));
if (javaPaths.length) {
const modules = profile.mavenTestModules ?? {};
const unclaimed = javaPaths.filter((path) => !Object.hasOwn(modules, path));
if (unclaimed.length) return { commands: [], groups: [], unroutedPaths: unclaimed, refusalReason: "Maven test files require verified reactor module metadata" };
if (selectedPaths.some((path) => !path.endsWith(".java") && !path.endsWith(".kt"))) return { commands: [], groups: [], unroutedPaths: [...selectedPaths], refusalReason: "Mixed Maven/JavaScript selective execution requires separate CI jobs" };
const targets = [...new Set(javaPaths.map((path) => modules[path]!))].sort();
if (targets.some((target) => target !== "." && (target.startsWith("/") || target.split("/").includes("..") || /[\\\r\n]/.test(target)))) {
return { commands: [], groups: [], unroutedPaths: javaPaths, refusalReason: "Invalid Maven reactor module target" };
}
const args = targets.length === 1 && targets[0] === "." ? ["test"] : ["-pl", targets.join(","), "-am", "test"];
const commandSpec: CommandSpec = { executable: "mvn", args };
const group: SelectiveTestCommandGroup = { runnerId: "maven:surefire", label: "Maven reactor tests", paths: [...javaPaths].sort(), commandSpec };
return { commands: [commandSpec], groups: [group], unroutedPaths: [] };
}
const paths = [...selectedPaths].sort();
if (paths.length === 0) return { commands: [], groups: [], unroutedPaths: [] };

Expand Down
3 changes: 2 additions & 1 deletion src/repo/adapters/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { readdirSync } from "node:fs";
import { join } from "node:path";
import { goAdapter } from "./go.js";
import { mavenAdapter } from "./maven.js";
import { vueAdapter } from "./vue.js";
import type { RepositoryAdapter } from "./types.js";

export const REPOSITORY_ADAPTERS: readonly RepositoryAdapter[] = [vueAdapter, goAdapter];
export const REPOSITORY_ADAPTERS: readonly RepositoryAdapter[] = [vueAdapter, goAdapter, mavenAdapter];

/** Never follows symlinks or scans dependency/build output directories. */
export function adapterFiles(root: string, exclusions: readonly string[] = []): string[] {
Expand Down
38 changes: 38 additions & 0 deletions src/repo/adapters/maven.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { readFileSync } from "node:fs";
import { dirname, posix } from "node:path";
import { contribution, type AdapterContext, type AdapterContribution, type RepositoryAdapter } from "./types.js";

interface Pom { path:string; dir:string; groupId?:string; artifactId?:string; modules:string[]; dependencies:Array<{groupId:string;artifactId:string}>; }

function tags(xml:string, name:string): string[] {
const re=new RegExp(`<${name}\\b[^>]*>([\\s\\S]*?)<\\/${name}>`,"g"); const out:string[]=[]; let m:RegExpExecArray|null;
while((m=re.exec(xml))) out.push((m[1]??"").trim()); return out;
}
function first(xml:string,name:string):string|undefined { return tags(xml,name)[0]; }
function parsePom(path:string, xml:string):Pom {
const dir=posix.dirname(path)==="."?"":posix.dirname(path);
const parent=first(xml,"parent")??"";
const own=xml.replace(/<parent\b[^>]*>[\s\S]*?<\/parent>/,"");
const deps=tags(xml,"dependency").map(x=>({groupId:first(x,"groupId")??"",artifactId:first(x,"artifactId")??""})).filter(x=>x.artifactId);
return {path,dir,groupId:first(own,"groupId")??first(parent,"groupId"),artifactId:first(own,"artifactId"),modules:tags(first(xml,"modules")??"","module"),dependencies:deps};
}
function under(dir:string,path:string){return !dir||path===dir||path.startsWith(dir+"/");}
function owningPom(poms:Pom[], file:string):Pom|undefined { return poms.filter(p=>under(p.dir,file)).sort((a,b)=>b.dir.length-a.dir.length)[0]; }
function javaFiles(context:AdapterContext, poms:Pom[], pom:Pom){return context.files.filter(f=>owningPom(poms,f)===pom&&/\/src\/(?:main|test)\/(?:java|kotlin)\/.*\.(?:java|kt)$/.test("/"+f));}
function isTest(path:string){return /\/src\/test\/(?:java|kotlin)\//.test("/"+path)&&/(?:Test|Tests|TestCase|IT)\.(?:java|kt)$/.test(path);}

export function analyzeMaven(context:AdapterContext):AdapterContribution {
const r=contribution(mavenAdapter);
const poms=context.files.filter(f=>f==="pom.xml"||f.endsWith("/pom.xml")).map(path=>parsePom(path,readFileSync(context.repoPath+"/"+path,"utf8")));
if(!poms.length){r.blockers.push("Maven analysis found no pom.xml");return r;}
const byGA=new Map(poms.filter(p=>p.artifactId).map(p=>[`${p.groupId??""}:${p.artifactId}`,p]));
const byArtifact=new Map(poms.filter(p=>p.artifactId).map(p=>[p.artifactId!,p]));
const members=new Map<Pom,string[]>(), anchors=new Map<Pom,string>();
for(const p of poms){const files=javaFiles(context,poms,p); if(!files.length) continue; members.set(p,files); anchors.set(p,files[0]!); r.sourcePaths.push(...files); for(const f of files) if(isTest(f)){r.testFiles.push(f);r.testPackages[f]=p.dir||".";}}
for(const [p,files] of members){const a=anchors.get(p)!; for(const f of files) if(f!==a) r.edges.push({from:a,to:f,kind:"import"},{from:f,to:a,kind:"import"});
for(const d of p.dependencies){const normalizedGroup=d.groupId.replace(/\$\{project\.groupId\}/g,p.groupId??"").replace(/\$\{pom\.groupId\}/g,p.groupId??""); const target=byGA.get(`${normalizedGroup}:${d.artifactId}`)??byArtifact.get(d.artifactId); const ta=target&&anchors.get(target); if(ta&&ta!==a) r.edges.push({from:a,to:ta,kind:"import"});}
}
if(!r.sourcePaths.length) r.blockers.push("Maven reactor contains no Java sources");
return r;
}
export const mavenAdapter:RepositoryAdapter={id:"maven",version:"1",kind:"language",detect:({files})=>files.includes("pom.xml")&&files.some(f=>f.endsWith(".java")||f.endsWith(".kt")),analyze:analyzeMaven};
8 changes: 5 additions & 3 deletions src/repo/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,17 +616,18 @@ export async function buildDependencyGraph(
if (contributions.some((item) => item.id === "go") && files.some((file) => /\.(?:[cm]?[jt]sx?|vue)$/.test(file))) {
adapterBlockers.push("Mixed Go/JavaScript repositories require explicit cross-language dependencies; full validation required");
}
if (contributions.length && files.some((file) => /\.(?:py|rs|java|kt|cs|svelte|astro)$/.test(file))) {
if (contributions.length && files.some((file) => /\.(?:py|rs|cs|svelte|astro)$/.test(file))) {
adapterBlockers.push("Unmodeled languages alongside an adapter require full validation");
}
profile.adapters = contributions.map(({ id, version, blockers }) => ({ id, version, blockers }));
profile.goTestPackages = Object.assign({}, ...contributions.map((item) => item.testPackages));
profile.goTestEnvironment = contributions.find((item) => item.id === "go")?.executionEnv;
profile.mavenTestModules = Object.assign({}, ...contributions.filter((item) => item.id === "maven").map((item) => item.testPackages));
const adapterTests = contributions.flatMap((item) => item.testFiles);
profile.testFilePaths = [...new Set([...profile.testFilePaths, ...adapterTests])].sort();
if (profile.testUniverse) {
profile.testUniverse.discoveredTestFiles = profile.testFilePaths.length;
profile.testUniverse.blindSpot = (profile.testUniverse.declaredFrameworks.length > 0 || contributions.some((item) => item.id === "go")) && profile.testFilePaths.length === 0;
profile.testUniverse.blindSpot = (profile.testUniverse.declaredFrameworks.length > 0 || contributions.some((item) => item.id === "go" || item.id === "maven")) && profile.testFilePaths.length === 0;
}
const entryPointPaths = new Set(profile.entryPoints.map((e) => e.path));

Expand Down Expand Up @@ -901,8 +902,9 @@ export function classifyRepositoryProject(repoPath: string): { capable: boolean;
if (typescript.capable) return typescript;
const files = adapterFiles(repoPath);
if (files.includes("go.mod")) return { capable: true, reason: "Go module (package-level analysis)" };
if (files.includes("pom.xml") && files.some((file) => file.endsWith(".java") || file.endsWith(".kt"))) return { capable: true, reason: "Maven reactor (module-level Java analysis)" };
if (files.some((file) => file.endsWith(".vue"))) return { capable: true, reason: "Vue single-file components" };
return { capable: false, reason: "No TypeScript project, Vue components, or root Go module found" };
return { capable: false, reason: "No TypeScript project, Vue components, root Go module, or Maven reactor found" };
}

export interface TypeScriptProjectCapability {
Expand Down
6 changes: 3 additions & 3 deletions src/repo/impact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { repositoryLayout, UNKNOWN_REPOSITORY_LAYOUT, type RepositoryLayout } fr
import { DEFAULT_TEST_FILE_MATCHER, matchesGlob as matchesTestGlob, testFileMatcherForProfile } from "./test-discovery.js";
import { resolveTestFixtureOwners } from "./test-fixture-ownership.js";

const SOURCE_EXTENSIONS = new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".mts",".cts",".vue",".go"]);
const SOURCE_EXTENSIONS = new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".mts",".cts",".vue",".go",".java",".kt"]);
const ASSET_EXTENSIONS = new Set([".css",".scss",".sass",".less",".json",".jsonc",".svg",".png",".jpg",".jpeg",".gif",".webp",".ico",".bmp",".woff",".woff2",".ttf",".otf",".eot",".wasm",".md",".txt"]);
const NEXT_ENTRY_NAMES = new Set(["page","layout","route","api","loading","error","template","not-found","middleware","generatemetadata","generatestaticparams"]);

Expand All @@ -26,7 +26,7 @@ function isDocumentationFile(filePath: string, layout: RepositoryLayout): boolea
function isConfigFile(filePath: string): boolean {
const CONFIG_FILE_NAMES = new Set(["package.json","package-lock.json","yarn.lock","pnpm-lock.yaml","bun.lockb","bun.lock","tsconfig.json","tsconfig.base.json","tsconfig.build.json","jsconfig.json"]);
const base = posix.basename(filePath);
if (["go.mod", "go.sum", "go.work", "go.work.sum"].includes(base)) return true;
if (["go.mod", "go.sum", "go.work", "go.work.sum", "pom.xml"].includes(base)) return true;
if (/^(?:vite|vue|nuxt)\.config\./.test(base)) return true;
if (CONFIG_FILE_NAMES.has(base)) return true;
if (base.startsWith("next.config")) return true;
Expand Down Expand Up @@ -243,7 +243,7 @@ export class ImpactAnalyzer {
const riskSignals: ImpactRiskSignal[] = [];
const fallbackReasons: string[] = [...(graphResult.adapterBlockers ?? [])];
for (const file of delta.files) {
if (allChangePaths(file).some((path) => /(?:^|\/)(?:go\.(?:mod|sum|work)|go\.work\.sum|(?:vite|vue|nuxt)\.config\.[^/]+)$/.test(path))) {
if (allChangePaths(file).some((path) => /(?:^|\/)(?:go\.(?:mod|sum|work)|go\.work\.sum|pom\.xml|(?:vite|vue|nuxt)\.config\.[^/]+)$/.test(path))) {
fallbackReasons.push(`Language/framework configuration changed: ${file.path}`);
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/repo/test-discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export interface TestDiscovery {
export const DEFAULT_TEST_PATTERNS: readonly string[] = [
"**/*.test.{ts,tsx,js,jsx,mjs,cjs,mts,cts}",
"**/*.spec.{ts,tsx,js,jsx,mjs,cjs,mts,cts}",
"**/src/test/java/**/*{Test,Tests,TestCase,IT}.java",
"**/src/test/kotlin/**/*{Test,Tests,TestCase,IT}.kt",
];

const FAMILY_TOKENS: Record<string, TestFamily> = {
Expand Down
2 changes: 2 additions & 0 deletions src/repo/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export interface RepositoryProfile {
adapterBlockers?: string[];
goTestPackages?: Record<string, string>;
goTestEnvironment?: Record<string, string>;
/** Maven/JUnit test file to owning reactor module (repo-relative, "." for root). */
mavenTestModules?: Record<string, string>;
packageManager: PackageManager;
packageJson: {
name?: string;
Expand Down
Loading
Loading