From 2ae322bfd4adc5c3c796be4296faf32e5d2d3b40 Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:54:42 +0530 Subject: [PATCH 01/21] Add Maven multi-module repository adapter --- src/repo/adapters/maven.ts | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/repo/adapters/maven.ts diff --git a/src/repo/adapters/maven.ts b/src/repo/adapters/maven.ts new file mode 100644 index 0000000..36bd7b8 --- /dev/null +++ b/src/repo/adapters/maven.ts @@ -0,0 +1,36 @@ +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(/]*>[\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 javaFiles(context:AdapterContext, dir:string){return context.files.filter(f=>under(dir,f)&&/\/src\/(?:main|test)\/java\/.*\.java$/.test("/"+f));} +function isTest(path:string){return /\/src\/test\/java\//.test("/"+path)&&/(?:Test|Tests|TestCase|IT)\.java$/.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 members=new Map(), anchors=new Map(); + for(const p of poms){const files=javaFiles(context,p.dir); 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 target=byGA.get(`${d.groupId}:${d.artifactId}`)??[...poms].find(x=>x.artifactId===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")),analyze:analyzeMaven}; From d56138cc20250a9ff5b52da22b55fe5f767ea80f Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:54:50 +0530 Subject: [PATCH 02/21] Register Maven repository adapter --- src/repo/adapters/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/repo/adapters/index.ts b/src/repo/adapters/index.ts index 06c623c..db7856a 100644 --- a/src/repo/adapters/index.ts +++ b/src/repo/adapters/index.ts @@ -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[] { From 6520bf1f256eee61dc888eff7499a9596116131b Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:54:55 +0530 Subject: [PATCH 03/21] Track Maven test module ownership --- src/repo/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/repo/types.ts b/src/repo/types.ts index f876344..33b9cac 100644 --- a/src/repo/types.ts +++ b/src/repo/types.ts @@ -44,6 +44,8 @@ export interface RepositoryProfile { adapterBlockers?: string[]; goTestPackages?: Record; goTestEnvironment?: Record; + /** Maven/JUnit test file to owning reactor module (repo-relative, "." for root). */ + mavenTestModules?: Record; packageManager: PackageManager; packageJson: { name?: string; From 2121c41092edc8bc6adee8d4dd21e335d9f18aa5 Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:55:03 +0530 Subject: [PATCH 04/21] Integrate Maven adapter into graph capability --- src/repo/graph.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/repo/graph.ts b/src/repo/graph.ts index 5e9d618..8556885 100644 --- a/src/repo/graph.ts +++ b/src/repo/graph.ts @@ -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|kt|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)); @@ -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"))) 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 { From 8a00de415c7dbf82e7edcf46c61ab80a4cf08612 Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:55:08 +0530 Subject: [PATCH 05/21] Classify Java and Maven manifest changes --- src/repo/impact.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/repo/impact.ts b/src/repo/impact.ts index 38c5966..e294c21 100644 --- a/src/repo/impact.ts +++ b/src/repo/impact.ts @@ -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"]); 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"]); @@ -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; @@ -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}`); } } From 92f4033a997fb139bc713171192a87579c1885c5 Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:55:17 +0530 Subject: [PATCH 06/21] Plan selective Maven reactor test commands --- src/planner/test-command.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/planner/test-command.ts b/src/planner/test-command.ts index 0e6a559..030601c 100644 --- a/src/planner/test-command.ts +++ b/src/planner/test-command.ts @@ -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")); + 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"))) 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: [] }; From 48982582c6e864bbe380393724ac8186017b819f Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:55:22 +0530 Subject: [PATCH 07/21] Discover conventional Maven Surefire tests --- src/repo/test-discovery.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/repo/test-discovery.ts b/src/repo/test-discovery.ts index 3beb139..01bbcb8 100644 --- a/src/repo/test-discovery.ts +++ b/src/repo/test-discovery.ts @@ -80,6 +80,7 @@ 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", ]; const FAMILY_TOKENS: Record = { From 6ae26640c5207d54ca2f8a4989bb08a2ccfe6f70 Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:55:40 +0530 Subject: [PATCH 08/21] Test multi-module Maven impact propagation --- tests/repo/language-adapters.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/repo/language-adapters.test.ts b/tests/repo/language-adapters.test.ts index 375026e..47a88ec 100644 --- a/tests/repo/language-adapters.test.ts +++ b/tests/repo/language-adapters.test.ts @@ -44,3 +44,26 @@ test("Vue source change reaches the importing test through a component", async ( rmSync(root, { recursive: true, force: true }); } }); + + +test("Maven multi-module source change reaches downstream module tests", async () => { + const root = fixture({ + "pom.xml": `4.0.0exampleparent1pomlibraryapplication`, + "library/pom.xml": `exampleparent1library`, + "library/src/main/java/example/Library.java": "package example; public class Library {}", + "application/pom.xml": `exampleparent1applicationexamplelibrary1`, + "application/src/main/java/example/App.java": "package example; public class App {}", + "application/src/test/java/example/AppTest.java": "package example; public class AppTest {}", + }); + try { + const graph = await buildDependencyGraph({ repoPath: root }); + assert.equal(classifyRepositoryProject(root).capable, true); + assert.deepEqual(graph.adapterBlockers, []); + const impact = new ImpactAnalyzer().analyze(delta("library/src/main/java/example/Library.java"), graph, graph.profile); + assert.equal(impact.fallbackRequired, false); + assert.deepEqual(impact.affectedTests.map((item) => item.path), ["application/src/test/java/example/AppTest.java"]); + const plan = planSelectiveTestCommands(graph.profile, impact.affectedTests.map((item) => item.path)); + assert.equal(plan.groups[0]?.runnerId, "maven:surefire"); + assert.deepEqual(plan.commands[0]?.args, ["-pl", "application", "-am", "test"]); + } finally { rmSync(root, { recursive: true, force: true }); } +}); From a1c0727113adebccad953727cb56941be76cb174 Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:56:40 +0530 Subject: [PATCH 09/21] Assign Java files to nearest Maven module --- src/repo/adapters/maven.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/repo/adapters/maven.ts b/src/repo/adapters/maven.ts index 36bd7b8..c5d5795 100644 --- a/src/repo/adapters/maven.ts +++ b/src/repo/adapters/maven.ts @@ -17,7 +17,8 @@ function parsePom(path:string, xml:string):Pom { 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 javaFiles(context:AdapterContext, dir:string){return context.files.filter(f=>under(dir,f)&&/\/src\/(?:main|test)\/java\/.*\.java$/.test("/"+f));} +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\/.*\.java$/.test("/"+f));} function isTest(path:string){return /\/src\/test\/java\//.test("/"+path)&&/(?:Test|Tests|TestCase|IT)\.java$/.test(path);} export function analyzeMaven(context:AdapterContext):AdapterContribution { @@ -26,7 +27,7 @@ export function analyzeMaven(context:AdapterContext):AdapterContribution { 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 members=new Map(), anchors=new Map(); - for(const p of poms){const files=javaFiles(context,p.dir); 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 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 target=byGA.get(`${d.groupId}:${d.artifactId}`)??[...poms].find(x=>x.artifactId===d.artifactId); const ta=target&&anchors.get(target); if(ta&&ta!==a) r.edges.push({from:a,to:ta,kind:"import"});} } From d3a9cb3e15de05228d408ffec182d9ce7d19b6df Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:57:28 +0530 Subject: [PATCH 10/21] Review Maven adapter into release boundary --- release-files.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-files.json b/release-files.json index ef7495c..4c5b8bf 100644 --- a/release-files.json +++ b/release-files.json @@ -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", @@ -92,4 +93,3 @@ "tsconfig.json" ] } - From 289f7b67381c7ae6c5be8db14cd7b45e2020a53e Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:58:45 +0530 Subject: [PATCH 11/21] Validate Spring multi-module Maven repository shape --- tests/repo/language-adapters.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/repo/language-adapters.test.ts b/tests/repo/language-adapters.test.ts index 47a88ec..25e575d 100644 --- a/tests/repo/language-adapters.test.ts +++ b/tests/repo/language-adapters.test.ts @@ -67,3 +67,27 @@ test("Maven multi-module source change reaches downstream module tests", async ( assert.deepEqual(plan.commands[0]?.args, ["-pl", "application", "-am", "test"]); } finally { rmSync(root, { recursive: true, force: true }); } }); + + +test("Spring gs-multi-module shape selects application when library changes", async () => { + const root = fixture({ + "pom.xml": `org.springframeworkgs-multi-module0.0.1-SNAPSHOTpomlibraryapplication`, + "library/pom.xml": `org.springframework.bootspring-boot-starter-parent3.5.11com.examplelibrary0.0.1-SNAPSHOT`, + "library/src/main/java/com/example/service/MyService.java": "package com.example.service; public class MyService {}", + "library/src/test/java/com/example/service/MyServiceTest.java": "package com.example.service; public class MyServiceTest {}", + "application/pom.xml": `org.springframework.bootspring-boot-starter-parent3.5.11com.exampleapplication0.0.1-SNAPSHOTcom.examplelibrary\${project.version}`, + "application/src/main/java/com/example/application/DemoApplication.java": "package com.example.application; public class DemoApplication {}", + "application/src/test/java/com/example/application/DemoApplicationTest.java": "package com.example.application; public class DemoApplicationTest {}", + }); + try { + const graph = await buildDependencyGraph({ repoPath: root }); + const impact = new ImpactAnalyzer().analyze(delta("library/src/main/java/com/example/service/MyService.java"), graph, graph.profile); + assert.equal(impact.fallbackRequired, false); + assert.deepEqual(impact.affectedTests.map((item) => item.path), [ + "application/src/test/java/com/example/application/DemoApplicationTest.java", + "library/src/test/java/com/example/service/MyServiceTest.java", + ]); + const plan = planSelectiveTestCommands(graph.profile, impact.affectedTests.map((item) => item.path)); + assert.deepEqual(plan.commands[0]?.args, ["-pl", "application,library", "-am", "test"]); + } finally { rmSync(root, { recursive: true, force: true }); } +}); From e55bec9d8ed4289dd51b00733ad4dfc97c4f893c Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:59:19 +0530 Subject: [PATCH 12/21] Extend Maven module analysis to Kotlin sources --- src/repo/adapters/maven.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/repo/adapters/maven.ts b/src/repo/adapters/maven.ts index c5d5795..74040ff 100644 --- a/src/repo/adapters/maven.ts +++ b/src/repo/adapters/maven.ts @@ -18,8 +18,8 @@ function parsePom(path:string, xml:string):Pom { } 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\/.*\.java$/.test("/"+f));} -function isTest(path:string){return /\/src\/test\/java\//.test("/"+path)&&/(?:Test|Tests|TestCase|IT)\.java$/.test(path);} +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); From a978114b2f4e65ea7c669c27712d9f24fa0968de Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:59:26 +0530 Subject: [PATCH 13/21] Classify Kotlin as Maven source --- src/repo/impact.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repo/impact.ts b/src/repo/impact.ts index e294c21..5628fc4 100644 --- a/src/repo/impact.ts +++ b/src/repo/impact.ts @@ -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",".java"]); +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"]); From 7399af5b902bf101dd5aa330ab8ff4ae6d2d9a5b Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:59:32 +0530 Subject: [PATCH 14/21] Route Kotlin Maven tests through reactor planner --- src/planner/test-command.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/planner/test-command.ts b/src/planner/test-command.ts index 030601c..7f93a4d 100644 --- a/src/planner/test-command.ts +++ b/src/planner/test-command.ts @@ -151,12 +151,12 @@ 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")); + 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"))) return { commands: [], groups: [], unroutedPaths: [...selectedPaths], refusalReason: "Mixed Maven/JavaScript selective execution requires separate CI jobs" }; + 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" }; From 7271694454af6772c483c4c968ce51d4574aa2cc Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:59:36 +0530 Subject: [PATCH 15/21] Discover Maven Kotlin test conventions --- src/repo/test-discovery.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/repo/test-discovery.ts b/src/repo/test-discovery.ts index 01bbcb8..2e6e2db 100644 --- a/src/repo/test-discovery.ts +++ b/src/repo/test-discovery.ts @@ -81,6 +81,7 @@ 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 = { From cfe704a8775de3753e1b3c05402d9f84b0ec20d5 Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:59:44 +0530 Subject: [PATCH 16/21] Recognize Kotlin Maven repositories as supported --- src/repo/graph.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/repo/graph.ts b/src/repo/graph.ts index 8556885..21e2990 100644 --- a/src/repo/graph.ts +++ b/src/repo/graph.ts @@ -616,7 +616,7 @@ 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|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 })); @@ -902,7 +902,7 @@ 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"))) return { capable: true, reason: "Maven reactor (module-level Java 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, root Go module, or Maven reactor found" }; } From b0781397e3a6cfae197eed45ffa5c0b54e83f4e6 Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:59:49 +0530 Subject: [PATCH 17/21] Detect Kotlin-only Maven reactors --- src/repo/adapters/maven.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repo/adapters/maven.ts b/src/repo/adapters/maven.ts index 74040ff..1391e09 100644 --- a/src/repo/adapters/maven.ts +++ b/src/repo/adapters/maven.ts @@ -34,4 +34,4 @@ export function analyzeMaven(context:AdapterContext):AdapterContribution { 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")),analyze:analyzeMaven}; +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}; From dd5c5b17bde47d7bb51006159396e3b707326288 Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 21:59:57 +0530 Subject: [PATCH 18/21] Test Kotlin Maven reactor impact propagation --- tests/repo/language-adapters.test.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/repo/language-adapters.test.ts b/tests/repo/language-adapters.test.ts index 25e575d..1fbd493 100644 --- a/tests/repo/language-adapters.test.ts +++ b/tests/repo/language-adapters.test.ts @@ -91,3 +91,29 @@ test("Spring gs-multi-module shape selects application when library changes", as assert.deepEqual(plan.commands[0]?.args, ["-pl", "application,library", "-am", "test"]); } finally { rmSync(root, { recursive: true, force: true }); } }); + + +test("Maven Kotlin multi-module source change reaches downstream tests", async () => { + const root = fixture({ + "pom.xml": `org.exampleparent1pomcommonselector`, + "common/pom.xml": `org.exampleparent1common`, + "common/src/main/kotlin/org/example/Config.kt": "package org.example; class Config", + "common/src/test/kotlin/org/example/ConfigTest.kt": "package org.example; class ConfigTest", + "selector/pom.xml": `org.exampleparent1selectororg.examplecommon1`, + "selector/src/main/kotlin/org/example/Selector.kt": "package org.example; class Selector", + "selector/src/test/kotlin/org/example/SelectorTest.kt": "package org.example; class SelectorTest", + }); + try { + const graph = await buildDependencyGraph({ repoPath: root }); + assert.equal(classifyRepositoryProject(root).capable, true); + assert.deepEqual(graph.adapterBlockers, []); + const impact = new ImpactAnalyzer().analyze(delta("common/src/main/kotlin/org/example/Config.kt"), graph, graph.profile); + assert.equal(impact.fallbackRequired, false); + assert.deepEqual(impact.affectedTests.map((item) => item.path), [ + "common/src/test/kotlin/org/example/ConfigTest.kt", + "selector/src/test/kotlin/org/example/SelectorTest.kt", + ]); + const plan = planSelectiveTestCommands(graph.profile, impact.affectedTests.map((item) => item.path)); + assert.deepEqual(plan.commands[0]?.args, ["-pl", "common,selector", "-am", "test"]); + } finally { rmSync(root, { recursive: true, force: true }); } +}); From 1d220d51f143a8e6cd9e44047a4260e13006a2a5 Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 22:01:21 +0530 Subject: [PATCH 19/21] Resolve Maven project groupId properties for reactor edges --- src/repo/adapters/maven.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/repo/adapters/maven.ts b/src/repo/adapters/maven.ts index 1391e09..cb8e285 100644 --- a/src/repo/adapters/maven.ts +++ b/src/repo/adapters/maven.ts @@ -26,10 +26,11 @@ export function analyzeMaven(context:AdapterContext):AdapterContribution { 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(), anchors=new Map(); 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 target=byGA.get(`${d.groupId}:${d.artifactId}`)??[...poms].find(x=>x.artifactId===d.artifactId); const ta=target&&anchors.get(target); if(ta&&ta!==a) r.edges.push({from:a,to:ta,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; From aa8759abdcddd3546dd0c9917643e4304b280963 Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 22:01:31 +0530 Subject: [PATCH 20/21] Cover Jicofo-style Maven module dependencies --- tests/repo/language-adapters.test.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/repo/language-adapters.test.ts b/tests/repo/language-adapters.test.ts index 1fbd493..d4368a9 100644 --- a/tests/repo/language-adapters.test.ts +++ b/tests/repo/language-adapters.test.ts @@ -117,3 +117,25 @@ test("Maven Kotlin multi-module source change reaches downstream tests", async ( assert.deepEqual(plan.commands[0]?.args, ["-pl", "common,selector", "-am", "test"]); } finally { rmSync(root, { recursive: true, force: true }); } }); + + +test("Jicofo-style Maven property dependency connects Kotlin modules", async () => { + const root = fixture({ + "pom.xml": `org.jitsijicofo-parent1.1-SNAPSHOTpomjicofo-commonjicofo-selector`, + "jicofo-common/pom.xml": `org.jitsijicofo-parent1.1-SNAPSHOTjicofo-common`, + "jicofo-common/src/main/kotlin/org/jitsi/jicofo/JicofoConfig.kt": "package org.jitsi.jicofo; class JicofoConfig", + "jicofo-common/src/test/kotlin/org/jitsi/jicofo/JicofoConfigTest.kt": "package org.jitsi.jicofo; class JicofoConfigTest", + "jicofo-selector/pom.xml": `org.jitsijicofo-parent1.1-SNAPSHOTjicofo-selector\${project.groupId}jicofo-common\${project.version}`, + "jicofo-selector/src/main/kotlin/org/jitsi/jicofo/bridge/BridgeSelector.kt": "package org.jitsi.jicofo.bridge; class BridgeSelector", + "jicofo-selector/src/test/kotlin/org/jitsi/jicofo/bridge/BridgeSelectorTest.kt": "package org.jitsi.jicofo.bridge; class BridgeSelectorTest", + }); + try { + const graph = await buildDependencyGraph({ repoPath: root }); + const impact = new ImpactAnalyzer().analyze(delta("jicofo-common/src/main/kotlin/org/jitsi/jicofo/JicofoConfig.kt"), graph, graph.profile); + assert.equal(impact.fallbackRequired, false); + assert.deepEqual(impact.affectedTests.map((item) => item.path), [ + "jicofo-common/src/test/kotlin/org/jitsi/jicofo/JicofoConfigTest.kt", + "jicofo-selector/src/test/kotlin/org/jitsi/jicofo/bridge/BridgeSelectorTest.kt", + ]); + } finally { rmSync(root, { recursive: true, force: true }); } +}); From 7cac46c62e9507bdb70817ec0a3a5a92f2528245 Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Mon, 21 Sep 2026 22:02:35 +0530 Subject: [PATCH 21/21] Validate Jicofo three-module reactor propagation --- tests/repo/language-adapters.test.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/repo/language-adapters.test.ts b/tests/repo/language-adapters.test.ts index d4368a9..4baaf15 100644 --- a/tests/repo/language-adapters.test.ts +++ b/tests/repo/language-adapters.test.ts @@ -139,3 +139,31 @@ test("Jicofo-style Maven property dependency connects Kotlin modules", async () ]); } finally { rmSync(root, { recursive: true, force: true }); } }); + + +test("Jicofo-style three-module reactor propagates common changes through selector to jicofo", async () => { + const root = fixture({ + "pom.xml": `org.jitsijicofo-parent1.1-SNAPSHOTpomjicofo-commonjicofo-selectorjicofo`, + "jicofo-common/pom.xml": `org.jitsijicofo-parent1.1-SNAPSHOTjicofo-common`, + "jicofo-common/src/main/kotlin/org/jitsi/jicofo/JicofoConfig.kt": "package org.jitsi.jicofo; class JicofoConfig", + "jicofo-common/src/test/kotlin/org/jitsi/jicofo/JicofoConfigTest.kt": "package org.jitsi.jicofo; class JicofoConfigTest", + "jicofo-selector/pom.xml": `org.jitsijicofo-parent1.1-SNAPSHOTjicofo-selector\${project.groupId}jicofo-common`, + "jicofo-selector/src/main/kotlin/org/jitsi/jicofo/bridge/BridgeSelector.kt": "package org.jitsi.jicofo.bridge; class BridgeSelector", + "jicofo-selector/src/test/kotlin/org/jitsi/jicofo/bridge/BridgeSelectorTest.kt": "package org.jitsi.jicofo.bridge; class BridgeSelectorTest", + "jicofo/pom.xml": `org.jitsijicofo-parent1.1-SNAPSHOTjicofo\${project.groupId}jicofo-common\${project.groupId}jicofo-selector`, + "jicofo/src/main/kotlin/org/jitsi/jicofo/JicofoServices.kt": "package org.jitsi.jicofo; class JicofoServices", + "jicofo/src/test/kotlin/org/jitsi/jicofo/JicofoServicesTest.kt": "package org.jitsi.jicofo; class JicofoServicesTest", + }); + try { + const graph = await buildDependencyGraph({ repoPath: root }); + const impact = new ImpactAnalyzer().analyze(delta("jicofo-common/src/main/kotlin/org/jitsi/jicofo/JicofoConfig.kt"), graph, graph.profile); + assert.equal(impact.fallbackRequired, false); + assert.deepEqual(impact.affectedTests.map((item) => item.path), [ + "jicofo-common/src/test/kotlin/org/jitsi/jicofo/JicofoConfigTest.kt", + "jicofo-selector/src/test/kotlin/org/jitsi/jicofo/bridge/BridgeSelectorTest.kt", + "jicofo/src/test/kotlin/org/jitsi/jicofo/JicofoServicesTest.kt", + ]); + const plan = planSelectiveTestCommands(graph.profile, impact.affectedTests.map((item) => item.path)); + assert.deepEqual(plan.commands[0]?.args, ["-pl", "jicofo,jicofo-common,jicofo-selector", "-am", "test"]); + } finally { rmSync(root, { recursive: true, force: true }); } +});