@ktpartners/dgs-platform 3.0.4 → 3.3.0

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 (115) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +8 -1
  3. package/agents/dgs-executor.md +124 -3
  4. package/agents/dgs-idea-researcher.md +447 -0
  5. package/agents/dgs-plan-checker.md +32 -0
  6. package/agents/dgs-planner.md +41 -8
  7. package/bin/install.js +44 -0
  8. package/commands/dgs/audit-milestone.md +2 -1
  9. package/commands/dgs/diff-report.md +124 -0
  10. package/commands/dgs/new-project.md +8 -21
  11. package/commands/dgs/package-scan.md +43 -0
  12. package/commands/dgs/research-idea.md +1 -0
  13. package/commands/dgs/switch-project.md +13 -0
  14. package/deliver-great-systems/bin/dgs-tools.cjs +120 -5
  15. package/deliver-great-systems/bin/lib/audit-tolerance.cjs +77 -0
  16. package/deliver-great-systems/bin/lib/audit-tolerance.test.cjs +101 -0
  17. package/deliver-great-systems/bin/lib/commands.cjs +311 -16
  18. package/deliver-great-systems/bin/lib/commands.test.cjs +115 -0
  19. package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
  20. package/deliver-great-systems/bin/lib/config.cjs +41 -0
  21. package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
  22. package/deliver-great-systems/bin/lib/core.cjs +7 -3
  23. package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
  24. package/deliver-great-systems/bin/lib/fast-routing.cjs +199 -0
  25. package/deliver-great-systems/bin/lib/fast-routing.test.cjs +108 -0
  26. package/deliver-great-systems/bin/lib/final-commit-precondition.test.cjs +87 -0
  27. package/deliver-great-systems/bin/lib/fixtures/package-scan/bundler-audit-gemfile.json +21 -0
  28. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-expected.md +186 -0
  29. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-runresult.json +235 -0
  30. package/deliver-great-systems/bin/lib/fixtures/package-scan/govulncheck-import.json +3 -0
  31. package/deliver-great-systems/bin/lib/fixtures/package-scan/npm-audit-v10.json +37 -0
  32. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-clean.json +3 -0
  33. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-vulns.json +77 -0
  34. package/deliver-great-systems/bin/lib/fixtures/package-scan/pip-audit-requirements.json +28 -0
  35. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-lodash.json +30 -0
  36. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-workspaces.json +55 -0
  37. package/deliver-great-systems/bin/lib/frontmatter.cjs +1 -1
  38. package/deliver-great-systems/bin/lib/governance.cjs +211 -0
  39. package/deliver-great-systems/bin/lib/governance.test.cjs +339 -0
  40. package/deliver-great-systems/bin/lib/health-untracked-phase.test.cjs +269 -0
  41. package/deliver-great-systems/bin/lib/init.cjs +56 -27
  42. package/deliver-great-systems/bin/lib/init.test.cjs +212 -5
  43. package/deliver-great-systems/bin/lib/jobs.cjs +7 -4
  44. package/deliver-great-systems/bin/lib/milestone.cjs +101 -3
  45. package/deliver-great-systems/bin/lib/milestone.test.cjs +203 -0
  46. package/deliver-great-systems/bin/lib/package-adapters.cjs +530 -0
  47. package/deliver-great-systems/bin/lib/package-adapters.test.cjs +618 -0
  48. package/deliver-great-systems/bin/lib/package-ecosystems.cjs +350 -0
  49. package/deliver-great-systems/bin/lib/package-ecosystems.test.cjs +348 -0
  50. package/deliver-great-systems/bin/lib/package-runner.cjs +199 -0
  51. package/deliver-great-systems/bin/lib/package-runner.test.cjs +198 -0
  52. package/deliver-great-systems/bin/lib/package-scan-provenance.cjs +56 -0
  53. package/deliver-great-systems/bin/lib/package-scan-provenance.test.cjs +103 -0
  54. package/deliver-great-systems/bin/lib/package-scan-report.cjs +1140 -0
  55. package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +1963 -0
  56. package/deliver-great-systems/bin/lib/package-scan-skill.cjs +96 -0
  57. package/deliver-great-systems/bin/lib/package-scan-skill.test.cjs +136 -0
  58. package/deliver-great-systems/bin/lib/package-scan.cjs +919 -0
  59. package/deliver-great-systems/bin/lib/package-scan.test.cjs +2147 -0
  60. package/deliver-great-systems/bin/lib/phase.cjs +18 -1
  61. package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
  62. package/deliver-great-systems/bin/lib/projects.cjs +38 -3
  63. package/deliver-great-systems/bin/lib/projects.test.cjs +112 -2
  64. package/deliver-great-systems/bin/lib/quick.cjs +178 -23
  65. package/deliver-great-systems/bin/lib/quick.test.cjs +138 -4
  66. package/deliver-great-systems/bin/lib/repos.cjs +12 -12
  67. package/deliver-great-systems/bin/lib/review.cjs +1821 -0
  68. package/deliver-great-systems/bin/lib/state.cjs +7 -3
  69. package/deliver-great-systems/bin/lib/summary-frontmatter.cjs +54 -0
  70. package/deliver-great-systems/bin/lib/summary-frontmatter.test.cjs +78 -0
  71. package/deliver-great-systems/bin/lib/sweep-scope.test.cjs +263 -0
  72. package/deliver-great-systems/bin/lib/verify.cjs +118 -6
  73. package/deliver-great-systems/bin/lib/verify.test.cjs +82 -0
  74. package/deliver-great-systems/bin/lib/wave-0-template-rename.test.cjs +40 -0
  75. package/deliver-great-systems/bin/lib/worktrees.cjs +27 -1
  76. package/deliver-great-systems/bin/lib/worktrees.test.cjs +76 -0
  77. package/deliver-great-systems/references/agent-step-reliability.md +60 -0
  78. package/deliver-great-systems/references/conflict-resolution.md +4 -0
  79. package/deliver-great-systems/references/context-tiers.md +4 -0
  80. package/deliver-great-systems/references/package-scan-config.md +151 -0
  81. package/deliver-great-systems/references/questioning.md +0 -30
  82. package/deliver-great-systems/references/spec-review-loop.md +1 -2
  83. package/deliver-great-systems/references/workflow-conventions.md +29 -0
  84. package/deliver-great-systems/skills/dgs-tests/package-scan.md +44 -0
  85. package/deliver-great-systems/templates/REVIEW.md +35 -0
  86. package/deliver-great-systems/templates/VALIDATION.md +1 -1
  87. package/deliver-great-systems/templates/claude-md.md +11 -0
  88. package/deliver-great-systems/templates/package-scan-report.md +108 -0
  89. package/deliver-great-systems/templates/project.md +6 -170
  90. package/deliver-great-systems/templates/summary.md +3 -1
  91. package/deliver-great-systems/workflows/add-phase.md +5 -0
  92. package/deliver-great-systems/workflows/audit-milestone.md +66 -10
  93. package/deliver-great-systems/workflows/cancel-job.md +1 -1
  94. package/deliver-great-systems/workflows/codereview.md +103 -9
  95. package/deliver-great-systems/workflows/complete-milestone.md +26 -7
  96. package/deliver-great-systems/workflows/complete-quick.md +40 -2
  97. package/deliver-great-systems/workflows/discuss-phase.md +3 -2
  98. package/deliver-great-systems/workflows/execute-phase.md +89 -2
  99. package/deliver-great-systems/workflows/execute-plan.md +10 -1
  100. package/deliver-great-systems/workflows/help.md +51 -18
  101. package/deliver-great-systems/workflows/import-spec.md +65 -7
  102. package/deliver-great-systems/workflows/init-product.md +46 -152
  103. package/deliver-great-systems/workflows/new-milestone.md +115 -14
  104. package/deliver-great-systems/workflows/new-project.md +60 -331
  105. package/deliver-great-systems/workflows/package-scan.md +59 -0
  106. package/deliver-great-systems/workflows/plan-phase.md +79 -1
  107. package/deliver-great-systems/workflows/quick-complete.md +40 -2
  108. package/deliver-great-systems/workflows/quick.md +183 -10
  109. package/deliver-great-systems/workflows/research-idea.md +80 -142
  110. package/deliver-great-systems/workflows/run-job.md +21 -35
  111. package/deliver-great-systems/workflows/settings.md +13 -77
  112. package/deliver-great-systems/workflows/write-spec.md +9 -11
  113. package/hooks/dist/dgs-enforce-discipline.js +196 -0
  114. package/package.json +1 -1
  115. package/scripts/build-hooks.js +1 -0
@@ -0,0 +1,350 @@
1
+ /**
2
+ * package-ecosystems.cjs -- Manifest detection and monorepo workspace expansion
3
+ *
4
+ * LEAF MODULE: Imports only Node built-ins (fs, path) and `safeReadFile`
5
+ * from core.cjs. MUST NOT import from other DGS modules. Enforced by
6
+ * path-audit.test.cjs.
7
+ *
8
+ * Exports:
9
+ * detectEcosystems(repoDir) -- returns Array<{ ecosystem, manifest_path }>
10
+ * ECOSYSTEM_MANIFESTS -- ordered priority list (for tests/orchestrator)
11
+ *
12
+ * Covers roadmap requirements PKG-08, PKG-09, PKG-10, PKG-31.
13
+ * Does NOT implement PKG-41 (Gradle sub-projects) or PKG-42 (Python src-layout)
14
+ * -- both deferred to a later milestone (see CONTEXT.md deferred section).
15
+ */
16
+ 'use strict';
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { safeReadFile } = require('./core.cjs');
20
+
21
+ const ECOSYSTEM_MANIFESTS = [
22
+ { eco: 'node', manifests: ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'package.json'] },
23
+ { eco: 'python', manifests: ['poetry.lock', 'Pipfile.lock', 'requirements.txt', 'pyproject.toml', 'setup.py'] },
24
+ { eco: 'go', manifests: ['go.sum', 'go.mod'] },
25
+ { eco: 'ruby', manifests: ['Gemfile.lock', 'Gemfile'] },
26
+ { eco: 'java', manifests: ['pom.xml', 'build.gradle', 'build.gradle.kts'] },
27
+ ];
28
+
29
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
30
+
31
+ function _posix(p) {
32
+ if (!p) return p;
33
+ return p.split(path.sep).join('/').replace(/^\.\//, '');
34
+ }
35
+
36
+ function _exists(p) {
37
+ try {
38
+ return fs.existsSync(p);
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ function _hasAnyManifest(repoDir, manifests) {
45
+ for (const m of manifests) {
46
+ if (_exists(path.join(repoDir, m))) return true;
47
+ }
48
+ return false;
49
+ }
50
+
51
+ function _tryParseJson(raw) {
52
+ if (!raw) return null;
53
+ try {
54
+ return JSON.parse(raw);
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ // Minimal pnpm-workspace.yaml parser. Looks for a top-level `packages:` list
61
+ // with one entry per indented `- ...` line. Handles quotes but nothing fancy.
62
+ function _parsePnpmWorkspace(raw) {
63
+ if (!raw) return [];
64
+ const lines = raw.split(/\r?\n/);
65
+ let inPackages = false;
66
+ const result = [];
67
+ for (const line of lines) {
68
+ const trimmed = line.trim();
69
+ if (/^packages\s*:/.test(trimmed)) {
70
+ inPackages = true;
71
+ continue;
72
+ }
73
+ if (inPackages) {
74
+ if (/^[a-zA-Z_][\w-]*\s*:/.test(trimmed)) {
75
+ // new top-level key
76
+ inPackages = false;
77
+ continue;
78
+ }
79
+ const m = trimmed.match(/^-\s*['"]?([^'"\s]+)['"]?\s*$/);
80
+ if (m) result.push(m[1]);
81
+ }
82
+ }
83
+ return result;
84
+ }
85
+
86
+ // Expand a workspace glob pattern relative to repoDir. Supports a single
87
+ // trailing `*` at a depth of at most one (e.g., packages/*, apps/*, *). Does
88
+ // NOT support `**` or nested stars. Returns absolute workspace directories
89
+ // that contain a package.json.
90
+ function _expandWorkspaceGlob(repoDir, pattern) {
91
+ if (!pattern) return [];
92
+ // Strip a trailing slash if present.
93
+ let pat = pattern.replace(/\/$/, '');
94
+ // If no wildcard, treat as a literal path (still cap at one level).
95
+ if (!pat.includes('*')) {
96
+ const abs = path.join(repoDir, pat);
97
+ if (_exists(path.join(abs, 'package.json'))) return [abs];
98
+ return [];
99
+ }
100
+ // Only allow one `*` (either bare `*` or `<dir>/*`).
101
+ const parts = pat.split('/');
102
+ if (parts.length > 2) return []; // cap at one directory level
103
+ let baseDir = repoDir;
104
+ let starSegment = parts[0];
105
+ if (parts.length === 2) {
106
+ baseDir = path.join(repoDir, parts[0]);
107
+ starSegment = parts[1];
108
+ }
109
+ if (starSegment !== '*') return []; // only support trailing bare `*`
110
+ let entries;
111
+ try {
112
+ entries = fs.readdirSync(baseDir, { withFileTypes: true });
113
+ } catch {
114
+ return [];
115
+ }
116
+ const results = [];
117
+ for (const e of entries) {
118
+ if (!e.isDirectory()) continue;
119
+ const full = path.join(baseDir, e.name);
120
+ if (_exists(path.join(full, 'package.json'))) {
121
+ results.push(full);
122
+ }
123
+ }
124
+ return results;
125
+ }
126
+
127
+ // Expand a list of workspace patterns, deduplicating and returning absolute
128
+ // workspace directories (each containing package.json).
129
+ function _collectNodeWorkspaces(repoDir, patterns) {
130
+ const seen = new Set();
131
+ const out = [];
132
+ for (const pattern of patterns) {
133
+ const dirs = _expandWorkspaceGlob(repoDir, pattern);
134
+ for (const d of dirs) {
135
+ if (!seen.has(d)) {
136
+ seen.add(d);
137
+ out.push(d);
138
+ }
139
+ }
140
+ }
141
+ return out;
142
+ }
143
+
144
+ // Parse root package.json for workspace declarations. Handles array form and
145
+ // object form ({ packages: [...] }). Returns patterns[] (possibly empty).
146
+ function _nodeWorkspacePatterns(rootPkg) {
147
+ if (!rootPkg || typeof rootPkg !== 'object') return [];
148
+ const ws = rootPkg.workspaces;
149
+ if (Array.isArray(ws)) return ws.slice();
150
+ if (ws && typeof ws === 'object' && Array.isArray(ws.packages)) return ws.packages.slice();
151
+ return [];
152
+ }
153
+
154
+ // Check whether a package.json carries real root-level dependencies (beyond
155
+ // the `workspaces` field). Returns true if either `dependencies` or
156
+ // `devDependencies` is a non-empty object.
157
+ function _hasRootDeps(rootPkg) {
158
+ if (!rootPkg || typeof rootPkg !== 'object') return false;
159
+ const hasDeps = rootPkg.dependencies && typeof rootPkg.dependencies === 'object'
160
+ && Object.keys(rootPkg.dependencies).length > 0;
161
+ const hasDev = rootPkg.devDependencies && typeof rootPkg.devDependencies === 'object'
162
+ && Object.keys(rootPkg.devDependencies).length > 0;
163
+ return Boolean(hasDeps || hasDev);
164
+ }
165
+
166
+ // Parse <modules> from a root pom.xml (regex walk, no XML dep). Returns
167
+ // module names (strings) as declared.
168
+ function _parseMavenModules(rawXml) {
169
+ if (!rawXml) return [];
170
+ const modulesBlock = rawXml.match(/<modules>([\s\S]*?)<\/modules>/);
171
+ if (!modulesBlock) return [];
172
+ const moduleMatches = modulesBlock[1].match(/<module>\s*([^<\s][^<]*?)\s*<\/module>/g) || [];
173
+ return moduleMatches.map(m => {
174
+ const inner = m.match(/<module>\s*([\s\S]*?)\s*<\/module>/);
175
+ return inner ? inner[1].trim() : null;
176
+ }).filter(Boolean);
177
+ }
178
+
179
+ // Detect whether a pom.xml declares <dependencies> alongside any <modules>.
180
+ // Excludes <dependencyManagement><dependencies> so a pure BOM root doesn't
181
+ // count as a scannable target.
182
+ function _mavenHasDependencies(rawXml) {
183
+ if (!rawXml) return false;
184
+ // Strip dependencyManagement block to avoid false positives.
185
+ const stripped = rawXml.replace(/<dependencyManagement>[\s\S]*?<\/dependencyManagement>/g, '');
186
+ return /<dependencies>[\s\S]*?<dependency>/.test(stripped);
187
+ }
188
+
189
+ // Parse the `use (...)` block (multi-line or single-line) from a go.work file.
190
+ // Returns module directory names as declared (strings starting with ./ or a bare name).
191
+ function _parseGoWorkUse(raw) {
192
+ if (!raw) return [];
193
+ const results = [];
194
+ // Multi-line form: use ( ./a\n./b\n )
195
+ const multiMatch = raw.match(/use\s*\(([\s\S]*?)\)/);
196
+ if (multiMatch) {
197
+ const inner = multiMatch[1];
198
+ for (const line of inner.split(/\r?\n/)) {
199
+ const trimmed = line.trim();
200
+ if (!trimmed || trimmed.startsWith('//')) continue;
201
+ results.push(trimmed);
202
+ }
203
+ return results;
204
+ }
205
+ // Single-line form: use ./a (one per line)
206
+ for (const line of raw.split(/\r?\n/)) {
207
+ const trimmed = line.trim();
208
+ const m = trimmed.match(/^use\s+(\S+)\s*$/);
209
+ if (m) results.push(m[1]);
210
+ }
211
+ return results;
212
+ }
213
+
214
+ // ─── Main export ─────────────────────────────────────────────────────────────
215
+
216
+ /**
217
+ * Detect scannable manifests and monorepo workspaces at a repo root.
218
+ *
219
+ * Returns one entry per scannable target. For simple repos (no workspaces)
220
+ * each entry has manifest_path: null. For monorepos (npm/pnpm/Yarn workspaces,
221
+ * Maven multi-module, Go workspaces), emits one entry per workspace with
222
+ * manifest_path populated as repo-relative POSIX. Gradle sub-projects (PKG-41)
223
+ * and Python src-layout (PKG-42) are explicitly deferred.
224
+ *
225
+ * @param {string} repoDir - Absolute path to the repo root
226
+ * @returns {Array<{ ecosystem: 'node'|'python'|'go'|'ruby'|'java', manifest_path: string|null }>}
227
+ */
228
+ function detectEcosystems(repoDir) {
229
+ if (!repoDir || !_exists(repoDir)) return [];
230
+
231
+ // Per-ecosystem builders — each returns an ordered array of entries.
232
+ const groups = [];
233
+
234
+ // ─── node ──────────────────────────────────────────────────────────────────
235
+ {
236
+ const nodeEntries = [];
237
+ const hasRoot = _hasAnyManifest(repoDir, ECOSYSTEM_MANIFESTS[0].manifests);
238
+ // Workspace expansion
239
+ const rootPkgRaw = safeReadFile(path.join(repoDir, 'package.json'));
240
+ const rootPkg = _tryParseJson(rootPkgRaw);
241
+ let patterns = _nodeWorkspacePatterns(rootPkg);
242
+ // pnpm-workspace.yaml may supply (or supplement) patterns
243
+ const pnpmRaw = safeReadFile(path.join(repoDir, 'pnpm-workspace.yaml'));
244
+ if (pnpmRaw) {
245
+ const pnpmPatterns = _parsePnpmWorkspace(pnpmRaw);
246
+ patterns = patterns.concat(pnpmPatterns);
247
+ }
248
+ const workspaceDirs = patterns.length > 0
249
+ ? _collectNodeWorkspaces(repoDir, patterns)
250
+ : [];
251
+
252
+ // Collect workspace entries
253
+ const wsEntries = workspaceDirs.map(abs => ({
254
+ ecosystem: 'node',
255
+ manifest_path: _posix(path.relative(repoDir, path.join(abs, 'package.json'))),
256
+ }));
257
+ wsEntries.sort((a, b) => a.manifest_path.localeCompare(b.manifest_path));
258
+
259
+ if (wsEntries.length > 0) {
260
+ nodeEntries.push(...wsEntries);
261
+ // Emit root only if root package.json has own deps
262
+ if (_hasRootDeps(rootPkg)) {
263
+ nodeEntries.push({ ecosystem: 'node', manifest_path: 'package.json' });
264
+ }
265
+ } else if (hasRoot) {
266
+ nodeEntries.push({ ecosystem: 'node', manifest_path: null });
267
+ }
268
+ if (nodeEntries.length > 0) groups.push(nodeEntries);
269
+ }
270
+
271
+ // ─── python ────────────────────────────────────────────────────────────────
272
+ {
273
+ const hasRoot = _hasAnyManifest(repoDir, ECOSYSTEM_MANIFESTS[1].manifests);
274
+ if (hasRoot) {
275
+ groups.push([{ ecosystem: 'python', manifest_path: null }]);
276
+ }
277
+ // PKG-42 deferred — do not scan src/ or subdirs.
278
+ }
279
+
280
+ // ─── go ────────────────────────────────────────────────────────────────────
281
+ {
282
+ const goEntries = [];
283
+ const goWorkRaw = safeReadFile(path.join(repoDir, 'go.work'));
284
+ if (goWorkRaw) {
285
+ const declared = _parseGoWorkUse(goWorkRaw);
286
+ const wsEntries = [];
287
+ for (const decl of declared) {
288
+ const modDir = path.resolve(repoDir, decl);
289
+ const modFile = path.join(modDir, 'go.mod');
290
+ if (_exists(modFile)) {
291
+ const rel = _posix(path.relative(repoDir, modFile));
292
+ wsEntries.push({ ecosystem: 'go', manifest_path: rel });
293
+ }
294
+ }
295
+ wsEntries.sort((a, b) => a.manifest_path.localeCompare(b.manifest_path));
296
+ goEntries.push(...wsEntries);
297
+ // Go: never emit root entry when go.work is present
298
+ } else {
299
+ const hasRoot = _hasAnyManifest(repoDir, ECOSYSTEM_MANIFESTS[2].manifests);
300
+ if (hasRoot) goEntries.push({ ecosystem: 'go', manifest_path: null });
301
+ }
302
+ if (goEntries.length > 0) groups.push(goEntries);
303
+ }
304
+
305
+ // ─── ruby ──────────────────────────────────────────────────────────────────
306
+ {
307
+ const hasRoot = _hasAnyManifest(repoDir, ECOSYSTEM_MANIFESTS[3].manifests);
308
+ if (hasRoot) groups.push([{ ecosystem: 'ruby', manifest_path: null }]);
309
+ }
310
+
311
+ // ─── java (Maven + Gradle) ─────────────────────────────────────────────────
312
+ {
313
+ const javaEntries = [];
314
+ const pomPath = path.join(repoDir, 'pom.xml');
315
+ const pomRaw = safeReadFile(pomPath);
316
+ if (pomRaw) {
317
+ const modules = _parseMavenModules(pomRaw);
318
+ if (modules.length > 0) {
319
+ const wsEntries = [];
320
+ for (const mod of modules) {
321
+ const modPom = path.join(repoDir, mod, 'pom.xml');
322
+ if (_exists(modPom)) {
323
+ const rel = _posix(path.relative(repoDir, modPom));
324
+ wsEntries.push({ ecosystem: 'java', manifest_path: rel });
325
+ }
326
+ // Missing modules silently skipped — malformed-tolerance
327
+ }
328
+ wsEntries.sort((a, b) => a.manifest_path.localeCompare(b.manifest_path));
329
+ javaEntries.push(...wsEntries);
330
+ // Root entry only if pom has both <modules> AND <dependencies>
331
+ if (_mavenHasDependencies(pomRaw)) {
332
+ javaEntries.push({ ecosystem: 'java', manifest_path: 'pom.xml' });
333
+ }
334
+ } else {
335
+ javaEntries.push({ ecosystem: 'java', manifest_path: null });
336
+ }
337
+ } else if (_exists(path.join(repoDir, 'build.gradle')) || _exists(path.join(repoDir, 'build.gradle.kts'))) {
338
+ // PKG-41 deferred — single-module Java only
339
+ javaEntries.push({ ecosystem: 'java', manifest_path: null });
340
+ }
341
+ if (javaEntries.length > 0) groups.push(javaEntries);
342
+ }
343
+
344
+ // Flatten groups into the final array — ecosystem order matches ECOSYSTEM_MANIFESTS.
345
+ const out = [];
346
+ for (const g of groups) out.push(...g);
347
+ return out;
348
+ }
349
+
350
+ module.exports = { detectEcosystems, ECOSYSTEM_MANIFESTS };