@tea-agent/loop-agent 0.24.11-beta.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +53 -4
  2. package/README.md +4 -4
  3. package/dist/application/dag/generate-task-dag.js +25 -8
  4. package/dist/commands/dag-init-hybrid.js +3 -2
  5. package/dist/commands/delegate.js +11 -11
  6. package/dist/commands/init.js +29 -14
  7. package/dist/commands/instructions.js +1 -2
  8. package/dist/executors/dag-pi-executor.js +74 -13
  9. package/dist/executors/pi-executor.js +25 -7
  10. package/dist/executors/pi-prompt-transport.js +198 -0
  11. package/dist/executors/pi-sdk-executor.js +1 -1
  12. package/dist/executors/process-tree.js +33 -0
  13. package/dist/executors/shell-executor.js +362 -24
  14. package/dist/executors/shell-verification.js +4 -2
  15. package/dist/infrastructure/harness/task-store.js +31 -0
  16. package/dist/shared/operator/capabilities.js +6 -0
  17. package/dist/task/read-model.js +3 -7
  18. package/dist/task/runtime.js +12 -3
  19. package/dist/verification/maven/cache.js +142 -0
  20. package/dist/verification/maven/index.js +120 -0
  21. package/dist/verification/maven/plan-commands.js +421 -0
  22. package/dist/verification/maven/pom-static.js +136 -0
  23. package/dist/verification/maven/scope-resolve.js +153 -0
  24. package/dist/verification/maven/stale.js +130 -0
  25. package/dist/verification/maven/types.js +23 -0
  26. package/dist/verification/maven/workspace-graph.js +322 -0
  27. package/dist/worker/cli.js +236 -21
  28. package/dist/worker/delivery/final-verification.js +12 -0
  29. package/dist/worker/delivery/git-transaction.js +32 -7
  30. package/dist/worker/delivery/package.js +9 -5
  31. package/dist/worker/delivery/verification-bundle.js +26 -5
  32. package/dist/worker/feature/advance.js +301 -0
  33. package/dist/worker/feature/doctor.js +223 -0
  34. package/dist/worker/feature/next-action.js +11 -3
  35. package/dist/worker/feature/scaffold.js +798 -0
  36. package/dist/worker/observe/dag-run-artifacts.js +90 -0
  37. package/dist/worker/observe/node-input.js +444 -0
  38. package/dist/worker/observe/routes.js +17 -0
  39. package/dist/worker/observe/static/api.js +9 -0
  40. package/dist/worker/observe/static/constants.js +9 -0
  41. package/dist/worker/observe/static/state.js +14 -0
  42. package/dist/worker/observe/static/styles.css +74 -0
  43. package/dist/worker/observe/static/views/dag-inspector.js +371 -15
  44. package/dist/worker/outcomes/projector.js +5 -1
  45. package/dist/worker/run-task/run-task.js +18 -2
  46. package/dist/worker/runner/run-ready.js +41 -1
  47. package/dist/workflows/dag/backend-test-markdown-workflow.js +300 -9
  48. package/dist/workflows/dag/backend-test-result-contract.js +103 -0
  49. package/dist/workflows/dag/failure-category.js +5 -1
  50. package/dist/workflows/dag/frontend-implementation-contract.js +4 -0
  51. package/dist/workflows/dag/frontend-prewrite-gate.js +0 -17
  52. package/dist/workflows/dag/frontend-test-result-contract.js +106 -41
  53. package/dist/workflows/dag/init-hybrid.js +224 -39
  54. package/dist/workflows/dag/node-execution.js +3 -2
  55. package/dist/workflows/dag/reconcile-run.js +24 -0
  56. package/dist/workflows/dag/types.js +57 -0
  57. package/dist/workflows/dag/validate.js +19 -2
  58. package/dist/workflows/loop/actions/dag-action.js +2 -1
  59. package/docs/architecture/dag-execution.md +5 -1
  60. package/docs/architecture/runtime-boundaries.md +11 -1
  61. package/docs/architecture/worker-and-feature.md +2 -0
  62. package/docs/init-surface.manifest.json +3 -0
  63. package/docs/templates/backend-test-dag.json +3 -3
  64. package/docs/templates/product-line/README.md +17 -1
  65. package/docs/templates/product-line/feature-scaffold-batch.example.yaml +31 -0
  66. package/docs/templates/product-line/scaffold-samples/README.md +36 -0
  67. package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +13 -0
  68. package/docs/templates/product-line/scaffold-samples/backend-only/design.md +14 -0
  69. package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +3 -0
  70. package/docs/templates/product-line/scaffold-samples/backend-only/requirement.md +6 -0
  71. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/BE-IMPL-001.yaml +83 -0
  72. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/task-graph.yaml +14 -0
  73. package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +15 -0
  74. package/docs/templates/product-line/scaffold-samples/fe-with-api/design.md +18 -0
  75. package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +3 -0
  76. package/docs/templates/product-line/scaffold-samples/fe-with-api/requirement.md +6 -0
  77. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/BE-IMPL-001.yaml +84 -0
  78. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/CONTRACT-001.yaml +84 -0
  79. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/FE-IMPL-001.yaml +86 -0
  80. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/task-graph.yaml +40 -0
  81. package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +13 -0
  82. package/docs/templates/product-line/scaffold-samples/frontend-only/design.md +14 -0
  83. package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +3 -0
  84. package/docs/templates/product-line/scaffold-samples/frontend-only/requirement.md +6 -0
  85. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/FE-IMPL-001.yaml +85 -0
  86. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/task-graph.yaml +14 -0
  87. package/examples/l5-report-coms-process-definition.html +322 -0
  88. package/package.json +1 -1
  89. package/skills/agent-worker/references/agent-worker-operator.md +84 -0
  90. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  91. package/skills/loop-agent/SKILL.md +14 -16
  92. package/skills/loop-agent/references/command-reference.md +33 -12
  93. package/skills/loop-agent/references/harness-policy.md +28 -6
  94. package/skills/loop-agent/references/hybrid-dag.md +8 -7
  95. package/skills/loop-agent/references/post-implementation-and-patterns.md +19 -9
  96. package/skills/loop-agent/references/source-and-plan-practice.md +161 -0
  97. package/skills/loop-agent/references/task-workflow.md +13 -5
@@ -0,0 +1,421 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { MAVEN_CACHE_SCHEMA_VERSION, MAVEN_PLANNER_VERSION, MavenPlanError, } from "./types.js";
5
+ const MAVEN_LAUNCHERS = new Set([
6
+ "mvn",
7
+ "mvnw",
8
+ "./mvnw",
9
+ ".\\mvnw",
10
+ ".\\mvnw.cmd",
11
+ "mvnw.cmd",
12
+ "mvnw.bat",
13
+ "./mvnw.cmd",
14
+ "./mvnw.bat",
15
+ ]);
16
+ function toPosix(value) {
17
+ return value.replace(/\\/g, "/");
18
+ }
19
+ export function isMavenLauncherToken(token) {
20
+ const normalized = token.replace(/\\/g, "/");
21
+ if (MAVEN_LAUNCHERS.has(token) || MAVEN_LAUNCHERS.has(normalized)) {
22
+ return true;
23
+ }
24
+ const base = path.posix.basename(normalized).toLowerCase();
25
+ return base === "mvn" || base === "mvnw" || base === "mvnw.cmd" || base === "mvnw.bat";
26
+ }
27
+ const ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=.*$/;
28
+ /** Detect shell metacharacters that make silent rewrite unsafe. */
29
+ export function assertSafeMavenShellPayload(script) {
30
+ // Strip quoted segments so quoted pipes/redirects do not false-positive,
31
+ // but unquoted operators remain visible.
32
+ const withoutQuotes = script
33
+ .replace(/'[^']*'/g, " ")
34
+ .replace(/"[^"]*"/g, " ");
35
+ if (/[|;&><`]/.test(withoutQuotes) || /\n/.test(script)) {
36
+ throw new MavenPlanError(`Unsafe compound shell payload around Maven (pipeline/redirect/control): ${script}`, { code: "maven-unsafe-shell-payload" });
37
+ }
38
+ if (/\$\(|\$\{/.test(withoutQuotes)) {
39
+ throw new MavenPlanError(`Unsupported shell expansion around Maven: ${script}`, { code: "maven-unsafe-shell-payload" });
40
+ }
41
+ }
42
+ function splitEnvPrefix(tokens) {
43
+ const envPrefix = [];
44
+ let i = 0;
45
+ while (i < tokens.length && ENV_ASSIGNMENT_RE.test(tokens[i])) {
46
+ envPrefix.push(tokens[i]);
47
+ i += 1;
48
+ }
49
+ return { envPrefix, remaining: tokens.slice(i) };
50
+ }
51
+ function tokenizeShellLike(command) {
52
+ const tokens = [];
53
+ let current = "";
54
+ let quote = null;
55
+ for (let i = 0; i < command.length; i += 1) {
56
+ const ch = command[i];
57
+ if (quote) {
58
+ if (ch === quote) {
59
+ quote = null;
60
+ }
61
+ else {
62
+ current += ch;
63
+ }
64
+ continue;
65
+ }
66
+ if (ch === "'" || ch === '"') {
67
+ quote = ch;
68
+ continue;
69
+ }
70
+ if (/\s/.test(ch)) {
71
+ if (current) {
72
+ tokens.push(current);
73
+ current = "";
74
+ }
75
+ continue;
76
+ }
77
+ current += ch;
78
+ }
79
+ if (current)
80
+ tokens.push(current);
81
+ return tokens;
82
+ }
83
+ function joinShellTokens(tokens) {
84
+ return tokens
85
+ .map((token) => {
86
+ if (token.length === 0)
87
+ return "''";
88
+ if (/^[A-Za-z0-9_./:=,@%+-]+$/.test(token))
89
+ return token;
90
+ return `'${token.replace(/'/g, `'\\''`)}'`;
91
+ })
92
+ .join(" ");
93
+ }
94
+ /**
95
+ * Detect Maven invocations in VerifyCommand args.
96
+ * Supports direct `mvn ...` and `bash -lc 'mvn ...'`.
97
+ */
98
+ export function parseMavenInvocation(command) {
99
+ const args = command.args;
100
+ if (args.length === 0)
101
+ return null;
102
+ // bash -lc "mvn ..."
103
+ if (args.length >= 3 &&
104
+ /^(bash|sh|zsh)$/.test(args[0] ?? "") &&
105
+ (args[1] === "-lc" || args[1] === "-c")) {
106
+ const script = args[2] ?? "";
107
+ assertSafeMavenShellPayload(script);
108
+ const tokens = tokenizeShellLike(script);
109
+ const launcherIndex = tokens.findIndex(isMavenLauncherToken);
110
+ if (launcherIndex < 0)
111
+ return null;
112
+ const beforeLauncher = tokens.slice(0, launcherIndex);
113
+ const { envPrefix, remaining } = splitEnvPrefix(beforeLauncher);
114
+ if (remaining.length > 0) {
115
+ throw new MavenPlanError(`Unsupported shell prefix before Maven launcher: ${remaining.join(" ")}`, { code: "maven-unsafe-shell-payload", candidates: remaining });
116
+ }
117
+ return {
118
+ prefix: args.slice(0, 2),
119
+ envPrefix,
120
+ embeddedShell: true,
121
+ embedArgIndex: 2,
122
+ launcher: tokens[launcherIndex],
123
+ mavenArgs: tokens.slice(launcherIndex + 1),
124
+ kind: "bash-lc",
125
+ };
126
+ }
127
+ const launcherIndex = args.findIndex(isMavenLauncherToken);
128
+ if (launcherIndex < 0)
129
+ return null;
130
+ const beforeLauncher = args.slice(0, launcherIndex);
131
+ const { envPrefix, remaining } = splitEnvPrefix(beforeLauncher);
132
+ if (remaining.length > 0) {
133
+ // Non-env prefix tokens on direct argv are preserved as outer prefix
134
+ // (e.g. env wrappers). Only pure env assignments become envPrefix.
135
+ return {
136
+ prefix: beforeLauncher,
137
+ envPrefix: [],
138
+ embeddedShell: false,
139
+ launcher: args[launcherIndex],
140
+ mavenArgs: args.slice(launcherIndex + 1),
141
+ kind: "direct",
142
+ };
143
+ }
144
+ return {
145
+ prefix: [],
146
+ envPrefix,
147
+ embeddedShell: false,
148
+ launcher: args[launcherIndex],
149
+ mavenArgs: args.slice(launcherIndex + 1),
150
+ kind: "direct",
151
+ };
152
+ }
153
+ export function commandContainsMaven(command) {
154
+ return parseMavenInvocation(command) !== null;
155
+ }
156
+ function extractMavenFlags(mavenArgs) {
157
+ const rest = [];
158
+ let explicitFile;
159
+ let explicitPl;
160
+ let explicitAm = false;
161
+ for (let i = 0; i < mavenArgs.length; i += 1) {
162
+ const arg = mavenArgs[i];
163
+ if (arg === "-f" || arg === "--file") {
164
+ const value = mavenArgs[i + 1];
165
+ if (!value) {
166
+ throw new MavenPlanError(`Maven ${arg} is missing a path argument`, {
167
+ code: "maven-flag-missing-value",
168
+ });
169
+ }
170
+ explicitFile = toPosix(value);
171
+ i += 1;
172
+ continue;
173
+ }
174
+ if (arg.startsWith("-f=") || arg.startsWith("--file=")) {
175
+ explicitFile = toPosix(arg.slice(arg.indexOf("=") + 1));
176
+ continue;
177
+ }
178
+ if (arg === "-pl" || arg === "--projects") {
179
+ const value = mavenArgs[i + 1];
180
+ if (!value) {
181
+ throw new MavenPlanError(`Maven ${arg} is missing a value`, {
182
+ code: "maven-flag-missing-value",
183
+ });
184
+ }
185
+ explicitPl = value;
186
+ i += 1;
187
+ continue;
188
+ }
189
+ if (arg.startsWith("-pl=") || arg.startsWith("--projects=")) {
190
+ explicitPl = arg.slice(arg.indexOf("=") + 1);
191
+ continue;
192
+ }
193
+ if (arg === "-am" || arg === "--also-make") {
194
+ explicitAm = true;
195
+ continue;
196
+ }
197
+ rest.push(arg);
198
+ }
199
+ return { explicitFile, explicitPl, explicitAm, rest };
200
+ }
201
+ /** Return explicit -f/--file value from maven args, if present. */
202
+ export function extractExplicitMavenFile(mavenArgs) {
203
+ return extractMavenFlags(mavenArgs).explicitFile;
204
+ }
205
+ function assertInRepoFile(repoRoot, relPath, baseCwd) {
206
+ const raw = toPosix(relPath);
207
+ const absolute = path.isAbsolute(raw)
208
+ ? path.normalize(raw)
209
+ : path.resolve(baseCwd ?? repoRoot, raw);
210
+ const rel = path.relative(repoRoot, absolute);
211
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
212
+ throw new MavenPlanError(`Maven -f path escapes repository root: ${relPath}`, { code: "maven-file-outside-repo", candidates: [relPath] });
213
+ }
214
+ if (!existsSync(absolute)) {
215
+ throw new MavenPlanError(`Maven -f path does not exist: ${toPosix(rel)}`, {
216
+ code: "maven-file-missing",
217
+ candidates: [toPosix(rel)],
218
+ });
219
+ }
220
+ return toPosix(rel);
221
+ }
222
+ function groupTargetsByBuild(targets) {
223
+ // reactorRoot -> modules; independent -> each own entry
224
+ const reactorGroups = new Map();
225
+ const independents = [];
226
+ for (const target of targets) {
227
+ if (target.reactorRootPomPath && target.moduleCoordinate) {
228
+ const list = reactorGroups.get(target.reactorRootPomPath) ?? [];
229
+ list.push(target.moduleCoordinate);
230
+ reactorGroups.set(target.reactorRootPomPath, list);
231
+ }
232
+ else if (target.reactorRootPomPath && !target.moduleCoordinate) {
233
+ // Selecting the reactor root itself
234
+ if (!reactorGroups.has(target.reactorRootPomPath)) {
235
+ reactorGroups.set(target.reactorRootPomPath, []);
236
+ }
237
+ }
238
+ else {
239
+ independents.push(target.pomPath);
240
+ }
241
+ }
242
+ const builds = [];
243
+ for (const [root, modules] of reactorGroups.entries()) {
244
+ const uniqueModules = [...new Set(modules)];
245
+ if (uniqueModules.length === 0) {
246
+ builds.push({ filePom: root, plModules: [], useAm: false });
247
+ }
248
+ else {
249
+ builds.push({
250
+ filePom: root,
251
+ plModules: uniqueModules,
252
+ useAm: true,
253
+ });
254
+ }
255
+ }
256
+ for (const pom of independents) {
257
+ builds.push({ filePom: pom, plModules: [], useAm: false });
258
+ }
259
+ // Stable output ordered by filePom path for determinism (not used as selection).
260
+ builds.sort((a, b) => a.filePom.localeCompare(b.filePom));
261
+ return builds;
262
+ }
263
+ function buildMavenArgList(input) {
264
+ const args = ["-f", input.filePom];
265
+ if (input.plModules.length > 0) {
266
+ args.push("-pl", input.plModules.join(","));
267
+ }
268
+ if (input.useAm) {
269
+ args.push("-am");
270
+ }
271
+ args.push(...input.rest);
272
+ return args;
273
+ }
274
+ function rebuildCommand(input) {
275
+ if (input.parsed.kind === "bash-lc" && input.parsed.embeddedShell) {
276
+ const script = joinShellTokens([
277
+ ...input.parsed.envPrefix,
278
+ input.parsed.launcher,
279
+ ...input.mavenArgs,
280
+ ]);
281
+ const args = [...input.original.args];
282
+ args[input.parsed.embedArgIndex ?? 2] = script;
283
+ return {
284
+ ...input.original,
285
+ args,
286
+ };
287
+ }
288
+ return {
289
+ ...input.original,
290
+ args: [
291
+ ...input.parsed.prefix,
292
+ ...input.parsed.envPrefix,
293
+ input.parsed.launcher,
294
+ ...input.mavenArgs,
295
+ ],
296
+ };
297
+ }
298
+ function digestCommand(command) {
299
+ return createHash("sha256")
300
+ .update(JSON.stringify({ label: command.label, args: command.args }))
301
+ .digest("hex");
302
+ }
303
+ function digestInputs(input) {
304
+ return createHash("sha256")
305
+ .update(JSON.stringify({
306
+ commands: input.commands.map((c) => ({
307
+ label: c.label,
308
+ args: c.args,
309
+ })),
310
+ targets: input.targets,
311
+ scope: {
312
+ allowedPaths: input.scope.allowedPaths,
313
+ writeSet: input.scope.writeSet ?? [],
314
+ sourcePaths: input.scope.sourcePaths ?? [],
315
+ },
316
+ plannerVersion: MAVEN_PLANNER_VERSION,
317
+ }))
318
+ .digest("hex");
319
+ }
320
+ /**
321
+ * Rewrite Maven VerifyCommands for the resolved targets.
322
+ * Non-Maven commands pass through unchanged.
323
+ */
324
+ export function planMavenCommands(input) {
325
+ const builds = groupTargetsByBuild(input.targets);
326
+ const planned = [];
327
+ for (const command of input.commands) {
328
+ const parsed = parseMavenInvocation(command);
329
+ if (!parsed) {
330
+ planned.push(command);
331
+ continue;
332
+ }
333
+ const flags = extractMavenFlags(parsed.mavenArgs);
334
+ if (flags.explicitFile) {
335
+ const filePom = assertInRepoFile(input.repoRoot, flags.explicitFile, command.cwd ?? input.repoRoot);
336
+ // Explicit -f: respect user target; still forbid conflicting auto -pl when
337
+ // the file is an independent module pom without those modules.
338
+ if (flags.explicitPl || flags.explicitAm) {
339
+ const node = input.graph.nodes.find((n) => n.pomPath === filePom);
340
+ const isReactorRoot = input.graph.reactors.some((r) => r.rootPomPath === filePom);
341
+ if (!isReactorRoot) {
342
+ throw new MavenPlanError(`Explicit -pl/-am conflicts with automatic planning for non-reactor POM -f ${filePom}`, {
343
+ code: "maven-explicit-pl-conflict",
344
+ candidates: [filePom, flags.explicitPl ?? "-am"],
345
+ });
346
+ }
347
+ // When user already provided full -f/-pl/-am, keep rest + their flags.
348
+ const mavenArgs = buildMavenArgList({
349
+ filePom,
350
+ plModules: flags.explicitPl
351
+ ? flags.explicitPl.split(",").map((s) => s.trim()).filter(Boolean)
352
+ : [],
353
+ useAm: flags.explicitAm,
354
+ rest: flags.rest,
355
+ });
356
+ // Avoid double -f from buildMavenArgList — already correct.
357
+ planned.push(rebuildCommand({
358
+ original: command,
359
+ parsed,
360
+ mavenArgs,
361
+ }));
362
+ continue;
363
+ }
364
+ // Explicit -f without -pl: keep that single target.
365
+ const mavenArgs = buildMavenArgList({
366
+ filePom,
367
+ plModules: [],
368
+ useAm: false,
369
+ rest: flags.rest,
370
+ });
371
+ planned.push(rebuildCommand({ original: command, parsed, mavenArgs }));
372
+ continue;
373
+ }
374
+ if (flags.explicitPl || flags.explicitAm) {
375
+ throw new MavenPlanError("Existing -pl/-am without a validated -f conflicts with automatic Maven planning; provide an explicit in-repo -f or remove -pl/-am.", {
376
+ code: "maven-explicit-pl-conflict",
377
+ candidates: [
378
+ flags.explicitPl ?? "",
379
+ flags.explicitAm ? "-am" : "",
380
+ ].filter(Boolean),
381
+ });
382
+ }
383
+ // Auto plan: expand one original command into one command per build group.
384
+ for (const build of builds) {
385
+ const mavenArgs = buildMavenArgList({
386
+ filePom: build.filePom,
387
+ plModules: build.plModules,
388
+ useAm: build.useAm,
389
+ rest: flags.rest,
390
+ });
391
+ planned.push(rebuildCommand({
392
+ original: {
393
+ ...command,
394
+ label: builds.length > 1
395
+ ? `${command.label}:${build.filePom}`
396
+ : command.label,
397
+ },
398
+ parsed,
399
+ mavenArgs,
400
+ }));
401
+ }
402
+ }
403
+ const plan = {
404
+ schemaVersion: MAVEN_CACHE_SCHEMA_VERSION,
405
+ plannerVersion: MAVEN_PLANNER_VERSION,
406
+ fingerprint: input.graph.fingerprint,
407
+ inputDigest: digestInputs({
408
+ commands: input.commands,
409
+ targets: input.targets,
410
+ scope: input.scope,
411
+ }),
412
+ targets: input.targets,
413
+ commandDigests: planned.map(digestCommand),
414
+ plannedCommands: planned.map((command) => ({
415
+ label: command.label,
416
+ args: command.args,
417
+ })),
418
+ workspaceManifest: input.graph.manifest,
419
+ };
420
+ return { commands: planned, plan };
421
+ }
@@ -0,0 +1,136 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { MavenPlanError } from "./types.js";
4
+ function toPosix(value) {
5
+ return value.replace(/\\/g, "/");
6
+ }
7
+ function decodeXmlEntities(value) {
8
+ return value
9
+ .replace(/&lt;/g, "<")
10
+ .replace(/&gt;/g, ">")
11
+ .replace(/&amp;/g, "&")
12
+ .replace(/&quot;/g, '"')
13
+ .replace(/&apos;/g, "'");
14
+ }
15
+ function stripCommentsAndCdataNoise(xml) {
16
+ return xml
17
+ .replace(/<!--[\s\S]*?-->/g, "")
18
+ .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1");
19
+ }
20
+ function firstTagText(xml, tag) {
21
+ const re = new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)</${tag}>`, "i");
22
+ const match = xml.match(re);
23
+ if (!match)
24
+ return undefined;
25
+ const text = decodeXmlEntities(match[1] ?? "").trim();
26
+ return text.length > 0 ? text : undefined;
27
+ }
28
+ /**
29
+ * Extract text of a direct child tag under the root <project> element.
30
+ * Nested blocks such as <parent> are ignored so parent artifactId/groupId
31
+ * cannot shadow the project-level fields.
32
+ */
33
+ function projectDirectChildText(xml, tag) {
34
+ const projectMatch = xml.match(/<project(?:\s[^>]*)?>([\s\S]*)<\/project>/i);
35
+ const body = projectMatch?.[1] ?? xml;
36
+ // Strip nested element blocks so only project-level direct children remain.
37
+ const withoutNested = body.replace(/<(parent|dependency|plugin|profile|modules|dependencies|build|properties|reporting|repositories|pluginRepositories|developers|contributors|licenses|scm|organization|issueManagement|ciManagement|distributionManagement|dependencyManagement|profiles)(?:\s[^>]*)?>[\s\S]*?<\/\1>/gi, "");
38
+ return firstTagText(withoutNested, tag);
39
+ }
40
+ function collectModuleTexts(xml) {
41
+ const modulesBlock = xml.match(/<modules(?:\s[^>]*)?>([\s\S]*?)<\/modules>/i);
42
+ if (!modulesBlock)
43
+ return [];
44
+ const body = modulesBlock[1] ?? "";
45
+ const modules = [];
46
+ const re = /<module(?:\s[^>]*)?>([\s\S]*?)<\/module>/gi;
47
+ let match;
48
+ while ((match = re.exec(body)) !== null) {
49
+ const text = decodeXmlEntities(match[1] ?? "").trim();
50
+ if (!text)
51
+ continue;
52
+ // Reject property-style module paths that cannot be proven statically.
53
+ if (text.includes("${")) {
54
+ throw new MavenPlanError(`POM module path is not statically provable: ${text}`, { code: "maven-pom-unresolvable-module" });
55
+ }
56
+ modules.push(toPosix(text));
57
+ }
58
+ return modules;
59
+ }
60
+ function parentBlockBody(xml) {
61
+ const parentBlock = xml.match(/<parent(?:\s[^>]*)?>([\s\S]*?)<\/parent>/i);
62
+ return parentBlock?.[1];
63
+ }
64
+ function parentRelativePath(xml) {
65
+ const body = parentBlockBody(xml);
66
+ if (body === undefined)
67
+ return undefined;
68
+ const relative = firstTagText(body, "relativePath");
69
+ if (relative === undefined)
70
+ return undefined;
71
+ if (relative.includes("${")) {
72
+ throw new MavenPlanError(`POM parent relativePath is not statically provable: ${relative}`, { code: "maven-pom-unresolvable-parent" });
73
+ }
74
+ return toPosix(relative);
75
+ }
76
+ /**
77
+ * Parse only the static POM fields required to build reactor relationships.
78
+ * Does not execute Maven and does not resolve the effective POM.
79
+ */
80
+ export function parseStaticPomXml(xml, sourceLabel) {
81
+ const cleaned = stripCommentsAndCdataNoise(xml);
82
+ const artifactId = projectDirectChildText(cleaned, "artifactId");
83
+ if (!artifactId) {
84
+ throw new MavenPlanError(`POM is missing <artifactId>: ${sourceLabel}`, { code: "maven-pom-missing-artifactId" });
85
+ }
86
+ if (artifactId.includes("${")) {
87
+ throw new MavenPlanError(`POM artifactId is not statically provable: ${sourceLabel}`, { code: "maven-pom-unresolvable-artifactId" });
88
+ }
89
+ // Prefer project-level groupId; fall back to parent groupId when present.
90
+ let groupId = projectDirectChildText(cleaned, "groupId");
91
+ if (!groupId || groupId.includes("${")) {
92
+ const parentBody = parentBlockBody(cleaned);
93
+ if (parentBody) {
94
+ const parentGroup = firstTagText(parentBody, "groupId");
95
+ if (parentGroup && !parentGroup.includes("${")) {
96
+ groupId = parentGroup;
97
+ }
98
+ else {
99
+ groupId = undefined;
100
+ }
101
+ }
102
+ else {
103
+ groupId = undefined;
104
+ }
105
+ }
106
+ const packaging = projectDirectChildText(cleaned, "packaging");
107
+ const modules = collectModuleTexts(cleaned);
108
+ const parentRel = parentRelativePath(cleaned);
109
+ return {
110
+ artifactId,
111
+ groupId,
112
+ packaging: packaging && !packaging.includes("${") ? packaging : undefined,
113
+ modules,
114
+ parentRelativePath: parentRel,
115
+ };
116
+ }
117
+ export function readStaticPomFile(absolutePomPath) {
118
+ let xml;
119
+ try {
120
+ xml = readFileSync(absolutePomPath, "utf8");
121
+ }
122
+ catch (error) {
123
+ throw new MavenPlanError(`Unable to read POM: ${absolutePomPath}`, {
124
+ code: "maven-pom-read-error",
125
+ cause: error,
126
+ });
127
+ }
128
+ return parseStaticPomXml(xml, path.basename(absolutePomPath));
129
+ }
130
+ export function repoRelativePosix(repoRoot, absolutePath) {
131
+ const rel = path.relative(repoRoot, absolutePath);
132
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
133
+ throw new MavenPlanError(`Path escapes repository root: ${absolutePath}`, { code: "maven-path-outside-repo" });
134
+ }
135
+ return toPosix(rel);
136
+ }