@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,618 @@
1
+ /**
2
+ * package-adapters.test.cjs -- Unit tests for per-tool JSON->canonical-finding adapters
3
+ *
4
+ * Covers PKG-11 (canonical shape), PKG-12 (direct/transitive), PKG-13 (chain),
5
+ * PKG-14 (fix recommendation), and the adapter half of PKG-31 (manifest_path).
6
+ *
7
+ * Fixtures live in ./fixtures/package-scan/ -- hand-authored, public-data-only,
8
+ * pinned so assertions are deterministic.
9
+ */
10
+ 'use strict';
11
+ const { test, describe } = require('node:test');
12
+ const assert = require('node:assert');
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+ const {
17
+ adapterSnyk,
18
+ adapterOsv,
19
+ adapterNpmAudit,
20
+ adapterPipAudit,
21
+ adapterGovulncheck,
22
+ adapterBundlerAudit,
23
+ } = require('./package-adapters.cjs');
24
+ const { detectEcosystems } = require('./package-ecosystems.cjs');
25
+ const { runTool } = require('./package-runner.cjs');
26
+
27
+ const SNYK_LODASH = require('./fixtures/package-scan/snyk-lodash.json');
28
+ const SNYK_WORKSPACES = require('./fixtures/package-scan/snyk-workspaces.json');
29
+ const OSV_CLEAN = require('./fixtures/package-scan/osv-clean.json');
30
+ const OSV_VULNS = require('./fixtures/package-scan/osv-vulns.json');
31
+ const NPM_AUDIT = require('./fixtures/package-scan/npm-audit-v10.json');
32
+ const PIP_AUDIT = require('./fixtures/package-scan/pip-audit-requirements.json');
33
+ const GOVULNCHECK = fs.readFileSync(
34
+ path.join(__dirname, 'fixtures/package-scan/govulncheck-import.json'),
35
+ 'utf-8'
36
+ );
37
+ const BUNDLER_AUDIT = require('./fixtures/package-scan/bundler-audit-gemfile.json');
38
+
39
+ const ALLOWED_TOOLS = new Set([
40
+ 'snyk', 'osv', 'npm-audit', 'pip-audit', 'govulncheck', 'bundler-audit',
41
+ ]);
42
+
43
+ describe('adapterSnyk', () => {
44
+ test('parses snyk-lodash.json -- 1 finding with canonical shape', () => {
45
+ const ctx = { repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo' };
46
+ const findings = adapterSnyk(SNYK_LODASH, ctx);
47
+ assert.strictEqual(findings.length, 1);
48
+ const f = findings[0];
49
+ assert.strictEqual(f.tool, 'snyk');
50
+ assert.strictEqual(f.package_name, 'lodash');
51
+ assert.strictEqual(f.installed_version, '4.17.20');
52
+ assert.strictEqual(f.severity, 'critical');
53
+ assert.strictEqual(f.cvss_score, 9.8);
54
+ assert.ok(Array.isArray(f.dependency_chain));
55
+ assert.strictEqual(f.dependency_chain.length, 3);
56
+ assert.strictEqual(f.chain_available, true);
57
+ assert.strictEqual(f.direct_or_transitive, 'transitive');
58
+ assert.strictEqual(f.fix_version, '4.17.21');
59
+ assert.ok(typeof f.remediation === 'string' && f.remediation.length > 0);
60
+ assert.strictEqual(f.id, null);
61
+ assert.strictEqual(f.vulnerability.cve, 'CVE-2020-8203');
62
+ });
63
+
64
+ test('passes through licence field when present', () => {
65
+ const ctx = { repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo' };
66
+ const [f] = adapterSnyk(SNYK_LODASH, ctx);
67
+ assert.strictEqual(f.licence, 'MIT');
68
+ });
69
+
70
+ test('parses snyk-workspaces.json (projects[] form) -- 3 findings across two manifest_paths', () => {
71
+ const ctx = { repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo' };
72
+ const findings = adapterSnyk(SNYK_WORKSPACES, ctx);
73
+ assert.strictEqual(findings.length, 3);
74
+ const paths = new Set(findings.map(f => f.manifest_path));
75
+ assert.ok(paths.has('packages/api/package.json'));
76
+ assert.ok(paths.has('packages/web/package.json'));
77
+ });
78
+
79
+ test('ctx.manifest_path overrides Snyk targetFile on every returned finding', () => {
80
+ const ctx = { repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo', manifest_path: 'packages/api/package.json' };
81
+ const findings = adapterSnyk(SNYK_WORKSPACES, ctx);
82
+ for (const f of findings) {
83
+ assert.strictEqual(f.manifest_path, 'packages/api/package.json');
84
+ }
85
+ });
86
+
87
+ test('normalises absolute Snyk targetFile paths to repo-relative POSIX when ctx.cwd is set', () => {
88
+ const absoluteTargetFile = '/tmp/demo/packages/api/package.json';
89
+ const fake = {
90
+ vulnerabilities: SNYK_LODASH.vulnerabilities,
91
+ targetFile: absoluteTargetFile,
92
+ };
93
+ const ctx = { repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo' };
94
+ const [f] = adapterSnyk(fake, ctx);
95
+ assert.strictEqual(f.manifest_path, 'packages/api/package.json');
96
+ });
97
+ });
98
+
99
+ describe('adapterOsv', () => {
100
+ test('parses osv-clean.json -- returns []', () => {
101
+ const ctx = { repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo' };
102
+ assert.deepStrictEqual(adapterOsv(OSV_CLEAN, ctx), []);
103
+ });
104
+
105
+ test('parses osv-vulns.json -- N findings with canonical shape', () => {
106
+ const ctx = { repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo' };
107
+ const findings = adapterOsv(OSV_VULNS, ctx);
108
+ assert.ok(findings.length >= 2);
109
+ for (const f of findings) {
110
+ assert.strictEqual(f.tool, 'osv');
111
+ assert.ok(typeof f.package_name === 'string' && f.package_name.length > 0);
112
+ assert.ok(f.vulnerability);
113
+ assert.ok(typeof f.vulnerability.reference_url === 'string');
114
+ }
115
+ });
116
+
117
+ test('osv findings without dependency chain get dependency_chain:null + chain_available:false', () => {
118
+ const ctx = { repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo' };
119
+ const findings = adapterOsv(OSV_VULNS, ctx);
120
+ for (const f of findings) {
121
+ assert.strictEqual(f.dependency_chain, null);
122
+ assert.strictEqual(f.chain_available, false);
123
+ }
124
+ });
125
+
126
+ test('osv findings with chain-length <=2 get direct_or_transitive:null (OSV has no chains)', () => {
127
+ const ctx = { repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo' };
128
+ const findings = adapterOsv(OSV_VULNS, ctx);
129
+ for (const f of findings) {
130
+ assert.strictEqual(f.direct_or_transitive, null);
131
+ }
132
+ });
133
+
134
+ test('ctx.manifest_path overrides source.path when provided', () => {
135
+ const ctx = {
136
+ repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo',
137
+ manifest_path: 'packages/web/package.json',
138
+ };
139
+ const findings = adapterOsv(OSV_VULNS, ctx);
140
+ for (const f of findings) {
141
+ assert.strictEqual(f.manifest_path, 'packages/web/package.json');
142
+ }
143
+ });
144
+ });
145
+
146
+ describe('adapterNpmAudit', () => {
147
+ test('parses npm-audit-v10.json -- canonical fields', () => {
148
+ const ctx = { repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo' };
149
+ const [f] = adapterNpmAudit(NPM_AUDIT, ctx);
150
+ assert.strictEqual(f.tool, 'npm-audit');
151
+ assert.strictEqual(f.package_name, 'lodash');
152
+ assert.strictEqual(f.severity, 'high');
153
+ assert.strictEqual(f.cvss_score, 7.4);
154
+ assert.ok(f.cvss_vector && f.cvss_vector.startsWith('CVSS:3.1'));
155
+ assert.strictEqual(f.direct_or_transitive, 'transitive');
156
+ });
157
+
158
+ test('dependency_chain:null + chain_available:false (npm audit does not emit chains)', () => {
159
+ const ctx = { repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo' };
160
+ const [f] = adapterNpmAudit(NPM_AUDIT, ctx);
161
+ assert.strictEqual(f.dependency_chain, null);
162
+ assert.strictEqual(f.chain_available, false);
163
+ });
164
+
165
+ test('fix_version from fixAvailable.version, remediation templated', () => {
166
+ const ctx = { repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo' };
167
+ const [f] = adapterNpmAudit(NPM_AUDIT, ctx);
168
+ assert.strictEqual(f.fix_version, '4.17.21');
169
+ assert.strictEqual(f.remediation, 'npm install lodash@4.17.21 --save');
170
+ });
171
+ });
172
+
173
+ describe('adapterPipAudit', () => {
174
+ test('parses pip-audit-requirements.json -- severity:null', () => {
175
+ const ctx = { repo: 'demo', ecosystem: 'python', cwd: '/tmp/demo' };
176
+ const findings = adapterPipAudit(PIP_AUDIT, ctx);
177
+ assert.ok(findings.length >= 1);
178
+ for (const f of findings) {
179
+ assert.strictEqual(f.tool, 'pip-audit');
180
+ assert.strictEqual(f.severity, null);
181
+ }
182
+ });
183
+
184
+ test('fix_version from vulns[0].fix_versions[0] (lowest), remediation templated', () => {
185
+ const ctx = { repo: 'demo', ecosystem: 'python', cwd: '/tmp/demo' };
186
+ const findings = adapterPipAudit(PIP_AUDIT, ctx);
187
+ const requestsFinding = findings.find(f => f.package_name === 'requests');
188
+ assert.ok(requestsFinding);
189
+ assert.strictEqual(requestsFinding.fix_version, '2.20.1');
190
+ assert.strictEqual(requestsFinding.remediation, 'pip install requests==2.20.1');
191
+ });
192
+
193
+ test('dependency_chain:null + chain_available:false', () => {
194
+ const ctx = { repo: 'demo', ecosystem: 'python', cwd: '/tmp/demo' };
195
+ const findings = adapterPipAudit(PIP_AUDIT, ctx);
196
+ for (const f of findings) {
197
+ assert.strictEqual(f.dependency_chain, null);
198
+ assert.strictEqual(f.chain_available, false);
199
+ }
200
+ });
201
+ });
202
+
203
+ describe('adapterGovulncheck', () => {
204
+ test('parses govulncheck-import.json NDJSON stream', () => {
205
+ const ctx = { repo: 'demo', ecosystem: 'go', cwd: '/tmp/demo' };
206
+ const findings = adapterGovulncheck(GOVULNCHECK, ctx);
207
+ assert.ok(findings.length >= 2);
208
+ for (const f of findings) {
209
+ assert.strictEqual(f.tool, 'govulncheck');
210
+ }
211
+ });
212
+
213
+ test('returns finding with tool:"govulncheck", severity:null, reference_url from OSV entry', () => {
214
+ const ctx = { repo: 'demo', ecosystem: 'go', cwd: '/tmp/demo' };
215
+ const [f] = adapterGovulncheck(GOVULNCHECK, ctx);
216
+ assert.strictEqual(f.tool, 'govulncheck');
217
+ assert.strictEqual(f.severity, null);
218
+ assert.ok(f.vulnerability.reference_url && f.vulnerability.reference_url.includes('pkg.go.dev'));
219
+ assert.strictEqual(f.vulnerability.cve, 'CVE-2023-3978');
220
+ });
221
+
222
+ test('direct_or_transitive inferred from finding.trace[] length', () => {
223
+ const ctx = { repo: 'demo', ecosystem: 'go', cwd: '/tmp/demo' };
224
+ const findings = adapterGovulncheck(GOVULNCHECK, ctx);
225
+ // First finding has trace length 2 -> direct; second has length 1 -> direct
226
+ const traces = findings.map(f => f.dependency_chain.length);
227
+ assert.ok(traces.includes(2));
228
+ assert.ok(traces.includes(1));
229
+ for (const f of findings) {
230
+ assert.ok(f.direct_or_transitive === 'direct' || f.direct_or_transitive === 'transitive');
231
+ }
232
+ });
233
+ });
234
+
235
+ describe('adapterBundlerAudit', () => {
236
+ test('parses bundler-audit-gemfile.json -- canonical fields', () => {
237
+ const ctx = { repo: 'demo', ecosystem: 'ruby', cwd: '/tmp/demo' };
238
+ const [f] = adapterBundlerAudit(BUNDLER_AUDIT, ctx);
239
+ assert.strictEqual(f.tool, 'bundler-audit');
240
+ assert.strictEqual(f.package_name, 'activerecord');
241
+ assert.strictEqual(f.severity, 'high');
242
+ assert.strictEqual(f.cvss_score, 7.5);
243
+ assert.strictEqual(f.vulnerability.cve, 'CVE-2020-8164');
244
+ assert.ok(f.remediation && f.remediation.includes('5.2.4.3'));
245
+ });
246
+
247
+ test('dependency_chain:null + chain_available:false', () => {
248
+ const ctx = { repo: 'demo', ecosystem: 'ruby', cwd: '/tmp/demo' };
249
+ const [f] = adapterBundlerAudit(BUNDLER_AUDIT, ctx);
250
+ assert.strictEqual(f.dependency_chain, null);
251
+ assert.strictEqual(f.chain_available, false);
252
+ });
253
+ });
254
+
255
+ describe('adapter invariants (all six)', () => {
256
+ function allFindings() {
257
+ const ctx = { repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo' };
258
+ return [
259
+ ...adapterSnyk(SNYK_LODASH, ctx).map(f => [f, 'snyk']),
260
+ ...adapterSnyk(SNYK_WORKSPACES, ctx).map(f => [f, 'snyk']),
261
+ ...adapterOsv(OSV_VULNS, ctx).map(f => [f, 'osv']),
262
+ ...adapterNpmAudit(NPM_AUDIT, ctx).map(f => [f, 'npm-audit']),
263
+ ...adapterPipAudit(PIP_AUDIT, { ...ctx, ecosystem: 'python' }).map(f => [f, 'pip-audit']),
264
+ ...adapterGovulncheck(GOVULNCHECK, { ...ctx, ecosystem: 'go' }).map(f => [f, 'govulncheck']),
265
+ ...adapterBundlerAudit(BUNDLER_AUDIT, { ...ctx, ecosystem: 'ruby' }).map(f => [f, 'bundler-audit']),
266
+ ];
267
+ }
268
+
269
+ test('no finding has a populated `id` field', () => {
270
+ for (const [f] of allFindings()) {
271
+ assert.ok(f.id === null || f.id === undefined, `id should be null, got ${f.id}`);
272
+ }
273
+ });
274
+
275
+ test('every finding has a `tool` field matching the adapter name', () => {
276
+ for (const [f, expected] of allFindings()) {
277
+ assert.strictEqual(f.tool, expected);
278
+ assert.ok(ALLOWED_TOOLS.has(f.tool), `unexpected tool: ${f.tool}`);
279
+ }
280
+ });
281
+
282
+ test('every finding has chain_available boolean AND dependency_chain (null or array)', () => {
283
+ for (const [f] of allFindings()) {
284
+ assert.strictEqual(typeof f.chain_available, 'boolean');
285
+ assert.ok(f.dependency_chain === null || Array.isArray(f.dependency_chain));
286
+ // Invariant: chain_available === (dependency_chain !== null)
287
+ assert.strictEqual(f.chain_available, f.dependency_chain !== null);
288
+ }
289
+ });
290
+
291
+ test('malformed entries in JSON are skipped without throwing', () => {
292
+ const malformedSnyk = {
293
+ vulnerabilities: [
294
+ { packageName: 'ok', version: '1.0.0', title: 't' }, // valid
295
+ { version: '1.0.0' }, // missing packageName -> skip
296
+ null, // null -> skip
297
+ ],
298
+ targetFile: 'package.json',
299
+ };
300
+ const ctx = { repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo' };
301
+ const out = adapterSnyk(malformedSnyk, ctx);
302
+ assert.strictEqual(out.length, 1);
303
+ assert.strictEqual(out[0].package_name, 'ok');
304
+
305
+ const malformedBundler = {
306
+ results: [
307
+ { gem: { name: 'rails', version: '1.0.0' }, advisory: { id: 'X', title: 't' } },
308
+ { gem: null, advisory: {} }, // malformed -> skip
309
+ null,
310
+ ],
311
+ };
312
+ const outB = adapterBundlerAudit(malformedBundler, { repo: 'demo', ecosystem: 'ruby', cwd: '/tmp/demo' });
313
+ assert.strictEqual(outB.length, 1);
314
+ });
315
+ });
316
+
317
+ describe('manifest_path handling (PKG-31 adapter half)', () => {
318
+ test('simple repo (ctx.manifest_path absent, no tool-emitted paths) -> manifest_path: null', () => {
319
+ const ctx = { repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo' };
320
+ const [f] = adapterNpmAudit(NPM_AUDIT, ctx);
321
+ assert.strictEqual(f.manifest_path, null);
322
+ });
323
+
324
+ test('workspace invocation (ctx.manifest_path populated) -> adapter stamps that value', () => {
325
+ const ctx = {
326
+ repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo',
327
+ manifest_path: 'packages/api/package.json',
328
+ };
329
+ const [f] = adapterNpmAudit(NPM_AUDIT, ctx);
330
+ assert.strictEqual(f.manifest_path, 'packages/api/package.json');
331
+ });
332
+
333
+ test('whole-repo Snyk scan with ctx.manifest_path absent -> extracts from Snyk targetFile', () => {
334
+ const ctx = { repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo' };
335
+ const findings = adapterSnyk(SNYK_WORKSPACES, ctx);
336
+ const paths = new Set(findings.map(f => f.manifest_path));
337
+ assert.ok(paths.has('packages/api/package.json'));
338
+ assert.ok(paths.has('packages/web/package.json'));
339
+ });
340
+
341
+ test('whole-repo OSV scan with ctx.manifest_path absent -> extracts from OSV source.path', () => {
342
+ const ctx = { repo: 'demo', ecosystem: 'node', cwd: '/tmp/demo' };
343
+ const findings = adapterOsv(OSV_VULNS, ctx);
344
+ for (const f of findings) {
345
+ // OSV source.path was packages/api/package-lock.json -> packages/api/package.json
346
+ assert.strictEqual(f.manifest_path, 'packages/api/package.json');
347
+ }
348
+ });
349
+ });
350
+
351
+ // ─── Cross-module integration smoke ──────────────────────────────────────────
352
+
353
+ const NODE = process.execPath;
354
+ const fakeExitCode = (code, stdout = '', stderr = '') =>
355
+ [NODE, '-e', `process.stdout.write(${JSON.stringify(stdout)}); process.stderr.write(${JSON.stringify(stderr)}); process.exit(${code})`];
356
+ const fakeMissingBinary = () => ['definitely-not-a-binary-xxx-' + Date.now()];
357
+
358
+ function mkTmp() {
359
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'dgs-smoke-'));
360
+ }
361
+
362
+ function writeFile(dir, rel, content) {
363
+ const abs = path.join(dir, rel);
364
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
365
+ fs.writeFileSync(abs, content);
366
+ }
367
+
368
+ describe('integration smoke', () => {
369
+ test('ecosystems -> runner -> adapter (simple node repo)', () => {
370
+ const tmp = mkTmp();
371
+ try {
372
+ writeFile(tmp, 'package.json', JSON.stringify({ name: 'smoke', version: '1.0.0' }));
373
+ writeFile(tmp, 'package-lock.json', '{}');
374
+ const detected = detectEcosystems(tmp);
375
+ assert.deepStrictEqual(detected, [{ ecosystem: 'node', manifest_path: null }]);
376
+
377
+ const fakeStdout = JSON.stringify({ vulnerabilities: [], targetFile: 'package.json' });
378
+ const r = runTool(tmp, 'snyk', fakeExitCode(1, fakeStdout), { expectJson: true });
379
+ assert.strictEqual(r.outcome, 'ok');
380
+ assert.ok(r.parsed);
381
+
382
+ const findings = adapterSnyk(r.parsed, { repo: 'smoke', ecosystem: 'node', cwd: tmp });
383
+ assert.deepStrictEqual(findings, []);
384
+ } finally {
385
+ fs.rmSync(tmp, { recursive: true, force: true });
386
+ }
387
+ });
388
+
389
+ test('workspace manifest_path flows end-to-end', () => {
390
+ const tmp = mkTmp();
391
+ try {
392
+ writeFile(tmp, 'package.json', JSON.stringify({
393
+ name: 'monorepo', private: true, workspaces: ['packages/*'],
394
+ }));
395
+ writeFile(tmp, 'packages/api/package.json', JSON.stringify({ name: 'api' }));
396
+ writeFile(tmp, 'packages/web/package.json', JSON.stringify({ name: 'web' }));
397
+
398
+ const detected = detectEcosystems(tmp);
399
+ const manifestPaths = detected.map(e => e.manifest_path).filter(Boolean).sort();
400
+ assert.deepStrictEqual(manifestPaths, ['packages/api/package.json', 'packages/web/package.json']);
401
+
402
+ const npmAuditPayload = {
403
+ vulnerabilities: {
404
+ lodash: {
405
+ name: 'lodash',
406
+ severity: 'high',
407
+ isDirect: false,
408
+ via: [{
409
+ title: 't', url: '',
410
+ severity: 'high',
411
+ cvss: { score: 7.4, vectorString: 'CVSS:3.1/AV:N/AC:L' },
412
+ range: '<4.17.21',
413
+ }],
414
+ effects: [],
415
+ range: '<4.17.21',
416
+ nodes: ['node_modules/lodash'],
417
+ fixAvailable: { name: 'lodash', version: '4.17.21', isSemVerMajor: false },
418
+ },
419
+ },
420
+ };
421
+
422
+ for (const entry of detected) {
423
+ const wsAbs = path.join(tmp, path.dirname(entry.manifest_path));
424
+ const r = runTool(wsAbs, 'npm-audit', fakeExitCode(1, JSON.stringify(npmAuditPayload)), { expectJson: true });
425
+ assert.strictEqual(r.outcome, 'ok');
426
+ const findings = adapterNpmAudit(r.parsed, {
427
+ repo: 'smoke', ecosystem: 'node', cwd: tmp,
428
+ manifest_path: entry.manifest_path,
429
+ });
430
+ for (const f of findings) {
431
+ assert.strictEqual(f.manifest_path, entry.manifest_path);
432
+ }
433
+ }
434
+ } finally {
435
+ fs.rmSync(tmp, { recursive: true, force: true });
436
+ }
437
+ });
438
+
439
+ test('runner missing-tool -> adapter never invoked', () => {
440
+ const r = runTool(process.cwd(), 'snyk', fakeMissingBinary(), { expectJson: true });
441
+ assert.strictEqual(r.outcome, 'missing');
442
+ // By contract, orchestrator does not invoke the adapter when outcome != 'ok'.
443
+ // We simulate that branch locally and assert the adapter was never called.
444
+ let adapterInvoked = false;
445
+ if (r.outcome === 'ok') {
446
+ adapterInvoked = true;
447
+ adapterSnyk(r.parsed, { repo: 'smoke', ecosystem: 'node', cwd: process.cwd() });
448
+ }
449
+ assert.strictEqual(adapterInvoked, false);
450
+ });
451
+
452
+ test('govulncheck NDJSON flow: runner -> adapter', () => {
453
+ const ndjson = [
454
+ JSON.stringify({
455
+ message: {
456
+ type: 'osv',
457
+ osv: {
458
+ id: 'GO-2023-1',
459
+ summary: 'test vuln',
460
+ aliases: ['CVE-2023-1234'],
461
+ references: [{ type: 'WEB', url: 'https://example.com/go' }],
462
+ },
463
+ },
464
+ }),
465
+ JSON.stringify({
466
+ message: {
467
+ type: 'finding',
468
+ finding: {
469
+ osv: 'GO-2023-1',
470
+ fixed_version: 'v1.2.3',
471
+ trace: [{ module: 'golang.org/x/net', version: 'v1.2.0' }],
472
+ },
473
+ },
474
+ }),
475
+ ].join('\n');
476
+
477
+ const r = runTool(process.cwd(), 'govulncheck', fakeExitCode(0, ndjson), { expectJson: false });
478
+ assert.strictEqual(r.outcome, 'ok');
479
+ assert.strictEqual(r.stdout, ndjson);
480
+
481
+ const findings = adapterGovulncheck(r.stdout, { repo: 'smoke', ecosystem: 'go', cwd: process.cwd() });
482
+ assert.strictEqual(findings.length, 1);
483
+ const [f] = findings;
484
+ assert.strictEqual(f.vulnerability.cve, 'CVE-2023-1234');
485
+ assert.strictEqual(f.fix_version, 'v1.2.3');
486
+ assert.ok(Array.isArray(f.dependency_chain));
487
+ assert.strictEqual(f.dependency_chain.length, 1);
488
+ });
489
+ });
490
+
491
+ // ─── Phase 153 Plan 02: adapterSnyk — .snyk policy suppressions (PKG-32) ─────
492
+
493
+ describe('adapterSnyk — .snyk policy suppressions (PKG-32)', () => {
494
+ function makeCtx() {
495
+ const calls = [];
496
+ const ctx = {
497
+ repo: 'api',
498
+ ecosystem: 'node',
499
+ manifest_path: 'package.json',
500
+ cwd: '/tmp/api',
501
+ recordSuppressions: (n) => calls.push(n),
502
+ };
503
+ return { ctx, calls };
504
+ }
505
+
506
+ test('payload with empty filtered.ignore => recordSuppressions(0)', () => {
507
+ const { ctx, calls } = makeCtx();
508
+ adapterSnyk({ vulnerabilities: [], filtered: { ignore: [] } }, ctx);
509
+ assert.deepStrictEqual(calls, [0]);
510
+ });
511
+
512
+ test('payload with 2 ignored => recordSuppressions(2)', () => {
513
+ const { ctx, calls } = makeCtx();
514
+ adapterSnyk({ vulnerabilities: [], filtered: { ignore: [{ id: 'SNYK-JS-LODASH-123' }, { id: 'SNYK-JS-AXIOS-456' }] } }, ctx);
515
+ assert.deepStrictEqual(calls, [2]);
516
+ });
517
+
518
+ test('payload with 1 vuln + 1 ignored => returns 1 finding + recordSuppressions(1)', () => {
519
+ const { ctx, calls } = makeCtx();
520
+ const findings = adapterSnyk({
521
+ vulnerabilities: [{ packageName: 'lodash', version: '1.0', severity: 'high', title: 't', identifiers: { CVE: [] }, from: ['x@1', 'lodash@1'] }],
522
+ filtered: { ignore: [{ id: 'S-1' }] },
523
+ }, ctx);
524
+ assert.strictEqual(findings.length, 1);
525
+ assert.deepStrictEqual(calls, [1]);
526
+ });
527
+
528
+ test('payload with no filtered key => recordSuppressions(0)', () => {
529
+ const { ctx, calls } = makeCtx();
530
+ adapterSnyk({ vulnerabilities: [] }, ctx);
531
+ assert.deepStrictEqual(calls, [0]);
532
+ });
533
+
534
+ test('filtered.ignore undefined => recordSuppressions(0)', () => {
535
+ const { ctx, calls } = makeCtx();
536
+ adapterSnyk({ vulnerabilities: [], filtered: {} }, ctx);
537
+ assert.deepStrictEqual(calls, [0]);
538
+ });
539
+
540
+ test('filtered.ignore non-array (malformed) => recordSuppressions(0), no throw', () => {
541
+ const { ctx, calls } = makeCtx();
542
+ assert.doesNotThrow(() => adapterSnyk({ vulnerabilities: [], filtered: { ignore: 'oops' } }, ctx));
543
+ assert.deepStrictEqual(calls, [0]);
544
+ });
545
+ });
546
+
547
+ // ─── Phase 153 Plan 03: adapterSnyk — licence roster (PKG-30, PKG-34) ────────
548
+
549
+ describe('adapterSnyk — licence roster (PKG-30, PKG-34)', () => {
550
+ function makeCtx() {
551
+ const calls = [];
552
+ const ctx = {
553
+ repo: 'api',
554
+ ecosystem: 'node',
555
+ manifest_path: 'package.json',
556
+ cwd: '/tmp/api',
557
+ recordLicenceRoster: (entries) => calls.push(entries),
558
+ };
559
+ return { ctx, calls };
560
+ }
561
+
562
+ test('dependencyPackages with multiple deps yields roster entries', () => {
563
+ const { ctx, calls } = makeCtx();
564
+ adapterSnyk({
565
+ vulnerabilities: [],
566
+ dependencyPackages: {
567
+ 'lodash@4.17.21': { name: 'lodash', version: '4.17.21', license: 'MIT' },
568
+ 'gpl-pkg@1.0.0': { name: 'gpl-pkg', version: '1.0.0', license: 'GPL-3.0' },
569
+ },
570
+ }, ctx);
571
+ assert.strictEqual(calls.length, 1);
572
+ const sorted = calls[0].slice().sort((a, b) => a.package_name.localeCompare(b.package_name));
573
+ assert.deepStrictEqual(sorted, [
574
+ { package_name: 'gpl-pkg', installed_version: '1.0.0', licence: 'GPL-3.0' },
575
+ { package_name: 'lodash', installed_version: '4.17.21', licence: 'MIT' },
576
+ ]);
577
+ });
578
+
579
+ test('no dependencyPackages key => recordLicenceRoster called with []', () => {
580
+ const { ctx, calls } = makeCtx();
581
+ adapterSnyk({ vulnerabilities: [] }, ctx);
582
+ assert.deepStrictEqual(calls, [[]]);
583
+ });
584
+
585
+ test('dependencyPackages: null => recordLicenceRoster called with []', () => {
586
+ const { ctx, calls } = makeCtx();
587
+ adapterSnyk({ vulnerabilities: [], dependencyPackages: null }, ctx);
588
+ assert.deepStrictEqual(calls, [[]]);
589
+ });
590
+
591
+ test('entry missing license => roster entry has licence:null', () => {
592
+ const { ctx, calls } = makeCtx();
593
+ adapterSnyk({
594
+ vulnerabilities: [],
595
+ dependencyPackages: { 'pkg@1.0': { name: 'pkg', version: '1.0' } },
596
+ }, ctx);
597
+ assert.strictEqual(calls[0][0].licence, null);
598
+ });
599
+
600
+ test('entry license as array => joined with " OR "', () => {
601
+ const { ctx, calls } = makeCtx();
602
+ adapterSnyk({
603
+ vulnerabilities: [],
604
+ dependencyPackages: { 'pkg@1.0': { name: 'pkg', version: '1.0', license: ['MIT', 'Apache-2.0'] } },
605
+ }, ctx);
606
+ assert.strictEqual(calls[0][0].licence, 'MIT OR Apache-2.0');
607
+ });
608
+
609
+ test('ctx without recordLicenceRoster => adapter does not throw', () => {
610
+ const ctxNoCallback = { repo: 'api', ecosystem: 'node', manifest_path: 'p.json', cwd: '/tmp/api' };
611
+ assert.doesNotThrow(() => adapterSnyk({
612
+ vulnerabilities: [],
613
+ dependencyPackages: { 'lodash@1.0': { name: 'lodash', version: '1.0', license: 'MIT' } },
614
+ }, ctxNoCallback));
615
+ });
616
+ });
617
+
618
+