@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,530 @@
1
+ /**
2
+ * package-adapters.cjs -- Per-tool JSON->canonical-finding translation
3
+ *
4
+ * Six pure adapter functions -- one per scanner in the cascade -- translate
5
+ * pre-parsed tool JSON into the canonical finding shape consumed by the
6
+ * Phase 150 orchestrator (which assigns ids at merge time) and the Phase 152
7
+ * severity normaliser (which translates raw strings to canonical tiers).
8
+ *
9
+ * Adapters DO NOT:
10
+ * - assign finding ids (orchestrator's job -- avoids collisions across tools)
11
+ * - normalise severity (Phase 152 PKG-23)
12
+ * - map licences to severity (Phase 153 PKG-34)
13
+ * - throw (malformed entries are skipped silently)
14
+ *
15
+ * Adapters DO:
16
+ * - stamp tool provenance (every finding carries `tool`)
17
+ * - preserve raw severity strings
18
+ * - extract CVSS score/vector when the tool emits them
19
+ * - set dependency_chain + chain_available on every finding
20
+ * - pick the lowest fix version when tool emits multiple
21
+ * - honour ctx.manifest_path when populated, else normalise tool-emitted paths
22
+ *
23
+ * Exports (named): adapterSnyk, adapterOsv, adapterNpmAudit, adapterPipAudit,
24
+ * adapterGovulncheck, adapterBundlerAudit
25
+ *
26
+ * Covers roadmap requirements PKG-11, PKG-12, PKG-13, PKG-14, adapter half of PKG-31.
27
+ */
28
+ 'use strict';
29
+ const path = require('path');
30
+
31
+ // ─── Shared helpers ──────────────────────────────────────────────────────────
32
+
33
+ /**
34
+ * Normalise a tool-emitted path into repo-relative POSIX form. Strips absolute
35
+ * prefixes matching ctx.cwd, converts backslashes, and trims leading "./".
36
+ * Returns null for empty/null input.
37
+ */
38
+ function _normaliseManifestPath(raw, ctxCwd) {
39
+ if (!raw) return null;
40
+ let p = String(raw);
41
+ if (ctxCwd && path.isAbsolute(p) && p.startsWith(ctxCwd)) {
42
+ p = p.slice(ctxCwd.length);
43
+ if (p.startsWith(path.sep) || p.startsWith('/') || p.startsWith('\\')) {
44
+ p = p.slice(1);
45
+ }
46
+ }
47
+ p = p.split(path.sep).join('/').replace(/\\/g, '/');
48
+ if (p.startsWith('./')) p = p.slice(2);
49
+ return p;
50
+ }
51
+
52
+ /** Apply ctx.manifest_path when populated, else normalise tool-emitted path. */
53
+ function _pickManifestPath(ctx, fromToolJson) {
54
+ if (ctx && ctx.manifest_path) return ctx.manifest_path;
55
+ return _normaliseManifestPath(fromToolJson, ctx && ctx.cwd);
56
+ }
57
+
58
+ /** Infer direct vs transitive from chain length. Null chain -> null. */
59
+ function _inferDirectOrTransitive(chain) {
60
+ if (!chain || !Array.isArray(chain)) return null;
61
+ return chain.length <= 2 ? 'direct' : 'transitive';
62
+ }
63
+
64
+ /** Semver-naive compare: split on dots, compare numeric segments. */
65
+ function _compareVersions(a, b) {
66
+ const aParts = String(a).split('.').map(s => {
67
+ const n = parseInt(s, 10);
68
+ return Number.isNaN(n) ? s : n;
69
+ });
70
+ const bParts = String(b).split('.').map(s => {
71
+ const n = parseInt(s, 10);
72
+ return Number.isNaN(n) ? s : n;
73
+ });
74
+ const len = Math.max(aParts.length, bParts.length);
75
+ for (let i = 0; i < len; i++) {
76
+ const ai = aParts[i];
77
+ const bi = bParts[i];
78
+ if (ai === undefined) return -1;
79
+ if (bi === undefined) return 1;
80
+ if (typeof ai === 'number' && typeof bi === 'number') {
81
+ if (ai !== bi) return ai - bi;
82
+ } else {
83
+ const as = String(ai);
84
+ const bs = String(bi);
85
+ if (as !== bs) return as < bs ? -1 : 1;
86
+ }
87
+ }
88
+ return 0;
89
+ }
90
+
91
+ /** Pick the lowest fix version from an array. Null/empty -> null. */
92
+ function _pickLowestFix(versionArray) {
93
+ if (!Array.isArray(versionArray) || versionArray.length === 0) return null;
94
+ const sorted = versionArray.slice().sort(_compareVersions);
95
+ return sorted[0];
96
+ }
97
+
98
+ /**
99
+ * Parse a Snyk from-entry like "pkg@1.2.3" or "@babel/core@7.0.0" into
100
+ * { name, version }. Falls back to { name: raw, version: '' } on failure.
101
+ */
102
+ function _parseSnykFromEntry(raw) {
103
+ if (typeof raw !== 'string') return { name: String(raw || ''), version: '' };
104
+ const atIdx = raw.lastIndexOf('@');
105
+ if (atIdx <= 0) return { name: raw, version: '' };
106
+ return { name: raw.slice(0, atIdx), version: raw.slice(atIdx + 1) };
107
+ }
108
+
109
+ /**
110
+ * Map a lockfile path to its adjacent manifest (e.g., package-lock.json ->
111
+ * package.json). Returns the original path if lockfile not recognised.
112
+ */
113
+ function _manifestPathFromLockfile(lockPath) {
114
+ if (!lockPath) return lockPath;
115
+ const lockToManifest = {
116
+ 'package-lock.json': 'package.json',
117
+ 'yarn.lock': 'package.json',
118
+ 'pnpm-lock.yaml': 'package.json',
119
+ 'poetry.lock': 'pyproject.toml',
120
+ 'Pipfile.lock': 'Pipfile',
121
+ 'Gemfile.lock': 'Gemfile',
122
+ 'go.sum': 'go.mod',
123
+ };
124
+ const base = lockPath.split('/').pop();
125
+ if (lockToManifest[base]) {
126
+ const dir = lockPath.slice(0, Math.max(0, lockPath.length - base.length));
127
+ return dir + lockToManifest[base];
128
+ }
129
+ return lockPath;
130
+ }
131
+
132
+ /** Extract fix_versions from OSV affected[].ranges[].events[].fixed. */
133
+ function _extractOsvFixVersions(affected) {
134
+ const out = [];
135
+ if (!Array.isArray(affected)) return out;
136
+ for (const a of affected) {
137
+ if (!a || !Array.isArray(a.ranges)) continue;
138
+ for (const r of a.ranges) {
139
+ if (!r || !Array.isArray(r.events)) continue;
140
+ for (const e of r.events) {
141
+ if (e && e.fixed) out.push(e.fixed);
142
+ }
143
+ }
144
+ }
145
+ return out;
146
+ }
147
+
148
+ /** Find a CVE id in an aliases[] array (pattern CVE-YYYY-NNN). */
149
+ function _findCveInAliases(aliases) {
150
+ if (!Array.isArray(aliases)) return null;
151
+ for (const a of aliases) {
152
+ if (typeof a === 'string' && /^CVE-\d{4}-\d+$/.test(a)) return a;
153
+ }
154
+ return null;
155
+ }
156
+
157
+ /** Extract CVE id from a URL containing a CVE-YYYY-NNN pattern. */
158
+ function _findCveInUrl(url) {
159
+ if (typeof url !== 'string') return null;
160
+ const m = url.match(/CVE-\d{4}-\d+/);
161
+ return m ? m[0] : null;
162
+ }
163
+
164
+ // ─── adapterSnyk ─────────────────────────────────────────────────────────────
165
+
166
+ /**
167
+ * Translate Snyk JSON output into canonical findings. Handles both single-
168
+ * project output (parsed.vulnerabilities + parsed.targetFile) and all-projects
169
+ * output (parsed.projects[] with per-project vulnerabilities/targetFile).
170
+ *
171
+ * @param {object} parsed
172
+ * @param {{ repo: string, ecosystem: string, cwd: string, manifest_path?: string|null }} ctx
173
+ * @returns {Array<object>}
174
+ */
175
+ function adapterSnyk(parsed, ctx) {
176
+ if (!parsed || typeof parsed !== 'object') return [];
177
+ const out = [];
178
+
179
+ function emitOne(v, targetFile) {
180
+ if (!v || !v.packageName) return;
181
+ const from = Array.isArray(v.from) ? v.from : null;
182
+ const chain = from && from.length > 0 ? from.map(_parseSnykFromEntry) : null;
183
+ const fixVersion = Array.isArray(v.fixedIn) ? _pickLowestFix(v.fixedIn) : (v.fixedIn || null);
184
+ let remediation = null;
185
+ if (Array.isArray(v.upgradePath) && v.upgradePath.length > 0) {
186
+ const last = v.upgradePath[v.upgradePath.length - 1];
187
+ if (last && last !== false) remediation = `upgrade ${last}`;
188
+ }
189
+ if (!remediation && fixVersion) {
190
+ remediation = `upgrade to ${v.packageName}@${fixVersion}`;
191
+ }
192
+ out.push({
193
+ tool: 'snyk',
194
+ ecosystem: ctx.ecosystem,
195
+ repo: ctx.repo,
196
+ manifest_path: _pickManifestPath(ctx, targetFile),
197
+ package_name: v.packageName,
198
+ installed_version: v.version || '',
199
+ vulnerability: {
200
+ cve: (v.identifiers && Array.isArray(v.identifiers.CVE) && v.identifiers.CVE[0]) || null,
201
+ title: v.title || '',
202
+ description: v.description || null,
203
+ reference_url: v.url || null,
204
+ },
205
+ severity: v.severity == null ? null : v.severity,
206
+ cvss_score: typeof v.cvssScore === 'number' ? v.cvssScore : null,
207
+ cvss_vector: v.CVSSv3 || null,
208
+ direct_or_transitive: from && from.length ? (from.length <= 2 ? 'direct' : 'transitive') : null,
209
+ dependency_chain: chain,
210
+ chain_available: chain !== null,
211
+ fix_version: fixVersion,
212
+ remediation,
213
+ licence: v.license != null ? v.license : null,
214
+ id: null,
215
+ });
216
+ }
217
+
218
+ if (Array.isArray(parsed.projects)) {
219
+ for (const proj of parsed.projects) {
220
+ const tf = proj && proj.targetFile;
221
+ const vulns = Array.isArray(proj && proj.vulnerabilities) ? proj.vulnerabilities : [];
222
+ for (const v of vulns) emitOne(v, tf);
223
+ }
224
+ } else if (Array.isArray(parsed.vulnerabilities)) {
225
+ for (const v of parsed.vulnerabilities) emitOne(v, parsed.targetFile);
226
+ }
227
+
228
+ // Phase 153 PKG-32: surface .snyk-applied suppression count via ctx callback.
229
+ if (ctx && typeof ctx.recordSuppressions === 'function') {
230
+ let count = 0;
231
+ try {
232
+ const filtered = parsed && parsed.filtered;
233
+ if (filtered && Array.isArray(filtered.ignore)) count = filtered.ignore.length;
234
+ } catch { count = 0; }
235
+ ctx.recordSuppressions(count);
236
+ }
237
+
238
+ // Phase 153 PKG-30/34: surface licence roster via ctx callback.
239
+ if (ctx && typeof ctx.recordLicenceRoster === 'function') {
240
+ const roster = [];
241
+ try {
242
+ const deps = parsed && parsed.dependencyPackages;
243
+ if (deps && typeof deps === 'object' && !Array.isArray(deps)) {
244
+ for (const key of Object.keys(deps)) {
245
+ const dep = deps[key];
246
+ if (!dep || typeof dep !== 'object') continue;
247
+ let licenceValue = dep.license === undefined ? null : dep.license;
248
+ if (Array.isArray(licenceValue)) licenceValue = licenceValue.join(' OR ');
249
+ roster.push({
250
+ package_name: dep.name || key.split('@')[0] || 'unknown',
251
+ installed_version: dep.version || (key.split('@')[1] || 'unknown'),
252
+ licence: licenceValue,
253
+ });
254
+ }
255
+ }
256
+ } catch { /* roster stays empty */ }
257
+ ctx.recordLicenceRoster(roster);
258
+ }
259
+
260
+ return out;
261
+ }
262
+
263
+ // ─── adapterOsv ──────────────────────────────────────────────────────────────
264
+
265
+ /**
266
+ * Translate OSV-Scanner JSON output into canonical findings.
267
+ *
268
+ * @param {object} parsed
269
+ * @param {{ repo, ecosystem, cwd, manifest_path? }} ctx
270
+ */
271
+ function adapterOsv(parsed, ctx) {
272
+ if (!parsed || !Array.isArray(parsed.results) || parsed.results.length === 0) return [];
273
+ const out = [];
274
+ for (const result of parsed.results) {
275
+ if (!result) continue;
276
+ const sourcePath = result.source && result.source.path;
277
+ const manifestCandidate = _manifestPathFromLockfile(sourcePath);
278
+ const pkgs = Array.isArray(result.packages) ? result.packages : [];
279
+ for (const pkgEntry of pkgs) {
280
+ if (!pkgEntry || !pkgEntry.package) continue;
281
+ const pkgName = pkgEntry.package.name;
282
+ const pkgVersion = pkgEntry.package.version || '';
283
+ if (!pkgName) continue;
284
+ const vulns = Array.isArray(pkgEntry.vulnerabilities) ? pkgEntry.vulnerabilities : [];
285
+ for (const vuln of vulns) {
286
+ if (!vuln || !vuln.id) continue;
287
+ const cvssVector = (Array.isArray(vuln.severity) && vuln.severity[0] && vuln.severity[0].score) || null;
288
+ const fixVersion = _pickLowestFix(_extractOsvFixVersions(vuln.affected));
289
+ out.push({
290
+ tool: 'osv',
291
+ ecosystem: ctx.ecosystem,
292
+ repo: ctx.repo,
293
+ manifest_path: _pickManifestPath(ctx, manifestCandidate),
294
+ package_name: pkgName,
295
+ installed_version: pkgVersion,
296
+ vulnerability: {
297
+ cve: _findCveInAliases(vuln.aliases),
298
+ title: vuln.summary || '',
299
+ description: vuln.details || null,
300
+ reference_url: (Array.isArray(vuln.references) && vuln.references[0] && vuln.references[0].url) || null,
301
+ },
302
+ severity: (vuln.database_specific && vuln.database_specific.severity) || null,
303
+ cvss_score: null,
304
+ cvss_vector: cvssVector,
305
+ direct_or_transitive: null,
306
+ dependency_chain: null,
307
+ chain_available: false,
308
+ fix_version: fixVersion,
309
+ remediation: fixVersion ? `upgrade to ${pkgName}@${fixVersion}` : null,
310
+ id: null,
311
+ });
312
+ }
313
+ }
314
+ }
315
+ return out;
316
+ }
317
+
318
+ // ─── adapterNpmAudit ─────────────────────────────────────────────────────────
319
+
320
+ /**
321
+ * Translate npm audit v10+ JSON output into canonical findings.
322
+ *
323
+ * @param {object} parsed
324
+ * @param {{ repo, ecosystem, cwd, manifest_path? }} ctx
325
+ */
326
+ function adapterNpmAudit(parsed, ctx) {
327
+ if (!parsed || !parsed.vulnerabilities || typeof parsed.vulnerabilities !== 'object') return [];
328
+ const out = [];
329
+ for (const entry of Object.values(parsed.vulnerabilities)) {
330
+ if (!entry || !entry.name) continue;
331
+ const viaArr = Array.isArray(entry.via) ? entry.via : [];
332
+ const via0 = viaArr.length > 0 && typeof viaArr[0] === 'object' ? viaArr[0] : null;
333
+ const fixVersion = (entry.fixAvailable && entry.fixAvailable.version) || null;
334
+ const direct = entry.isDirect === true ? 'direct'
335
+ : entry.isDirect === false ? 'transitive' : null;
336
+ out.push({
337
+ tool: 'npm-audit',
338
+ ecosystem: ctx.ecosystem,
339
+ repo: ctx.repo,
340
+ manifest_path: _pickManifestPath(ctx, null),
341
+ package_name: entry.name,
342
+ installed_version: '',
343
+ vulnerability: {
344
+ cve: via0 ? _findCveInUrl(via0.url) : null,
345
+ title: (via0 && via0.title) || entry.name,
346
+ description: null,
347
+ reference_url: (via0 && via0.url) || null,
348
+ },
349
+ severity: entry.severity == null ? null : entry.severity,
350
+ cvss_score: via0 && via0.cvss && typeof via0.cvss.score === 'number' ? via0.cvss.score : null,
351
+ cvss_vector: via0 && via0.cvss && via0.cvss.vectorString ? via0.cvss.vectorString : null,
352
+ direct_or_transitive: direct,
353
+ dependency_chain: null,
354
+ chain_available: false,
355
+ fix_version: fixVersion,
356
+ remediation: fixVersion ? `npm install ${entry.name}@${fixVersion} --save` : null,
357
+ id: null,
358
+ });
359
+ }
360
+ return out;
361
+ }
362
+
363
+ // ─── adapterPipAudit ─────────────────────────────────────────────────────────
364
+
365
+ /**
366
+ * Translate pip-audit v2.10+ JSON output into canonical findings.
367
+ *
368
+ * @param {object} parsed
369
+ * @param {{ repo, ecosystem, cwd, manifest_path? }} ctx
370
+ */
371
+ function adapterPipAudit(parsed, ctx) {
372
+ if (!parsed || !Array.isArray(parsed.dependencies)) return [];
373
+ const out = [];
374
+ for (const dep of parsed.dependencies) {
375
+ if (!dep || !dep.name) continue;
376
+ const vulns = Array.isArray(dep.vulns) ? dep.vulns : [];
377
+ for (const vuln of vulns) {
378
+ if (!vuln || !vuln.id) continue;
379
+ const fixVersion = _pickLowestFix(vuln.fix_versions);
380
+ const descFirstLine = typeof vuln.description === 'string'
381
+ ? vuln.description.split('\n')[0]
382
+ : null;
383
+ out.push({
384
+ tool: 'pip-audit',
385
+ ecosystem: ctx.ecosystem,
386
+ repo: ctx.repo,
387
+ manifest_path: _pickManifestPath(ctx, null),
388
+ package_name: dep.name,
389
+ installed_version: dep.version || '',
390
+ vulnerability: {
391
+ cve: _findCveInAliases(vuln.aliases),
392
+ title: descFirstLine || vuln.id,
393
+ description: vuln.description || null,
394
+ reference_url: null,
395
+ },
396
+ severity: null,
397
+ cvss_score: null,
398
+ cvss_vector: null,
399
+ direct_or_transitive: null,
400
+ dependency_chain: null,
401
+ chain_available: false,
402
+ fix_version: fixVersion,
403
+ remediation: fixVersion ? `pip install ${dep.name}==${fixVersion}` : null,
404
+ id: null,
405
+ });
406
+ }
407
+ }
408
+ return out;
409
+ }
410
+
411
+ // ─── adapterGovulncheck ──────────────────────────────────────────────────────
412
+
413
+ /**
414
+ * Translate govulncheck NDJSON output into canonical findings.
415
+ *
416
+ * Input is the raw NDJSON string (one JSON object per line). The adapter
417
+ * parses each line, indexes OSV entries by id, and emits one finding per
418
+ * `finding` message.
419
+ *
420
+ * @param {string} rawString
421
+ * @param {{ repo, ecosystem, cwd, manifest_path? }} ctx
422
+ */
423
+ function adapterGovulncheck(rawString, ctx) {
424
+ if (typeof rawString !== 'string' || rawString.length === 0) return [];
425
+ const lines = rawString.split('\n');
426
+ const osvMap = Object.create(null);
427
+ const findings = [];
428
+ for (const line of lines) {
429
+ const trimmed = line.trim();
430
+ if (!trimmed) continue;
431
+ let parsedLine;
432
+ try {
433
+ parsedLine = JSON.parse(trimmed);
434
+ } catch {
435
+ continue;
436
+ }
437
+ const msg = parsedLine && parsedLine.message;
438
+ if (!msg || !msg.type) continue;
439
+ if (msg.type === 'osv' && msg.osv && msg.osv.id) {
440
+ osvMap[msg.osv.id] = msg.osv;
441
+ } else if (msg.type === 'finding' && msg.finding && msg.finding.osv) {
442
+ findings.push(msg.finding);
443
+ }
444
+ }
445
+ const ecosystem = (ctx && ctx.ecosystem) || 'go';
446
+ const out = [];
447
+ for (const finding of findings) {
448
+ const osv = osvMap[finding.osv] || null;
449
+ const trace = Array.isArray(finding.trace) ? finding.trace : null;
450
+ const chain = trace ? trace.map(t => ({ name: (t && t.module) || '', version: (t && t.version) || '' })) : null;
451
+ const pkgName = trace && trace.length > 0 ? (trace[0].module || null) : null;
452
+ const pkgVersion = trace && trace.length > 0 ? (trace[0].version || '') : '';
453
+ const fixVersion = finding.fixed_version || null;
454
+ out.push({
455
+ tool: 'govulncheck',
456
+ ecosystem,
457
+ repo: ctx && ctx.repo,
458
+ manifest_path: _pickManifestPath(ctx || {}, null),
459
+ package_name: pkgName,
460
+ installed_version: pkgVersion,
461
+ vulnerability: {
462
+ cve: osv ? _findCveInAliases(osv.aliases) : null,
463
+ title: (osv && osv.summary) || finding.osv,
464
+ description: (osv && osv.details) || null,
465
+ reference_url: (osv && Array.isArray(osv.references) && osv.references[0] && osv.references[0].url) || null,
466
+ },
467
+ severity: null,
468
+ cvss_score: null,
469
+ cvss_vector: null,
470
+ direct_or_transitive: _inferDirectOrTransitive(chain),
471
+ dependency_chain: chain,
472
+ chain_available: chain !== null,
473
+ fix_version: fixVersion,
474
+ remediation: fixVersion && pkgName ? `go get ${pkgName}@${fixVersion}` : null,
475
+ id: null,
476
+ });
477
+ }
478
+ return out;
479
+ }
480
+
481
+ // ─── adapterBundlerAudit ─────────────────────────────────────────────────────
482
+
483
+ /**
484
+ * Translate bundler-audit --format json output into canonical findings.
485
+ *
486
+ * @param {object} parsed
487
+ * @param {{ repo, ecosystem, cwd, manifest_path? }} ctx
488
+ */
489
+ function adapterBundlerAudit(parsed, ctx) {
490
+ if (!parsed || !Array.isArray(parsed.results)) return [];
491
+ const out = [];
492
+ const ecosystem = (ctx && ctx.ecosystem) || 'ruby';
493
+ for (const r of parsed.results) {
494
+ if (!r || !r.gem || !r.gem.name) continue;
495
+ const advisory = r.advisory || {};
496
+ out.push({
497
+ tool: 'bundler-audit',
498
+ ecosystem,
499
+ repo: ctx && ctx.repo,
500
+ manifest_path: _pickManifestPath(ctx || {}, null),
501
+ package_name: r.gem.name,
502
+ installed_version: r.gem.version || '',
503
+ vulnerability: {
504
+ cve: advisory.id || null,
505
+ title: advisory.title || '',
506
+ description: advisory.description || null,
507
+ reference_url: advisory.url || null,
508
+ },
509
+ severity: advisory.criticality == null ? null : advisory.criticality,
510
+ cvss_score: typeof advisory.cvss_v3 === 'number' ? advisory.cvss_v3 : null,
511
+ cvss_vector: null,
512
+ direct_or_transitive: null,
513
+ dependency_chain: null,
514
+ chain_available: false,
515
+ fix_version: null,
516
+ remediation: advisory.solution || null,
517
+ id: null,
518
+ });
519
+ }
520
+ return out;
521
+ }
522
+
523
+ module.exports = {
524
+ adapterSnyk,
525
+ adapterOsv,
526
+ adapterNpmAudit,
527
+ adapterPipAudit,
528
+ adapterGovulncheck,
529
+ adapterBundlerAudit,
530
+ };