@ktpartners/dgs-platform 2.9.0 → 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 (166) hide show
  1. package/CHANGELOG.md +197 -0
  2. package/README.md +34 -2
  3. package/agents/dgs-executor.md +124 -3
  4. package/agents/dgs-idea-researcher.md +447 -0
  5. package/agents/dgs-plan-checker.md +61 -3
  6. package/agents/dgs-planner.md +51 -8
  7. package/bin/install.js +44 -0
  8. package/commands/dgs/abandon-quick.md +28 -0
  9. package/commands/dgs/add-tests.md +2 -2
  10. package/commands/dgs/audit-milestone.md +4 -3
  11. package/commands/dgs/capture-principle.md +11 -11
  12. package/commands/dgs/cleanup.md +2 -2
  13. package/commands/dgs/complete-milestone.md +11 -11
  14. package/commands/dgs/complete-quick.md +28 -0
  15. package/commands/dgs/create-milestone-job.md +2 -2
  16. package/commands/dgs/debug.md +3 -3
  17. package/commands/dgs/develop-idea.md +1 -1
  18. package/commands/dgs/diff-report.md +124 -0
  19. package/commands/dgs/fast.md +3 -1
  20. package/commands/dgs/health.md +1 -1
  21. package/commands/dgs/map-codebase.md +6 -6
  22. package/commands/dgs/new-milestone.md +5 -5
  23. package/commands/dgs/new-project.md +8 -21
  24. package/commands/dgs/package-scan.md +43 -0
  25. package/commands/dgs/plan-milestone-gaps.md +1 -1
  26. package/commands/dgs/progress.md +3 -3
  27. package/commands/dgs/quick-abandon.md +8 -0
  28. package/commands/dgs/quick-complete.md +8 -0
  29. package/commands/dgs/quick.md +10 -3
  30. package/commands/dgs/research-idea.md +3 -2
  31. package/commands/dgs/research-phase.md +3 -3
  32. package/commands/dgs/switch-project.md +14 -1
  33. package/commands/dgs/write-spec.md +3 -3
  34. package/deliver-great-systems/bin/dgs-tools.cjs +401 -32
  35. package/deliver-great-systems/bin/lib/audit-tolerance.cjs +77 -0
  36. package/deliver-great-systems/bin/lib/audit-tolerance.test.cjs +101 -0
  37. package/deliver-great-systems/bin/lib/commands.cjs +626 -46
  38. package/deliver-great-systems/bin/lib/commands.test.cjs +451 -0
  39. package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
  40. package/deliver-great-systems/bin/lib/config.cjs +80 -6
  41. package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
  42. package/deliver-great-systems/bin/lib/context.cjs +120 -0
  43. package/deliver-great-systems/bin/lib/core.cjs +35 -14
  44. package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
  45. package/deliver-great-systems/bin/lib/execution.cjs +49 -17
  46. package/deliver-great-systems/bin/lib/fast-routing.cjs +199 -0
  47. package/deliver-great-systems/bin/lib/fast-routing.test.cjs +108 -0
  48. package/deliver-great-systems/bin/lib/final-commit-precondition.test.cjs +87 -0
  49. package/deliver-great-systems/bin/lib/fixtures/package-scan/bundler-audit-gemfile.json +21 -0
  50. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-expected.md +186 -0
  51. package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-runresult.json +235 -0
  52. package/deliver-great-systems/bin/lib/fixtures/package-scan/govulncheck-import.json +3 -0
  53. package/deliver-great-systems/bin/lib/fixtures/package-scan/npm-audit-v10.json +37 -0
  54. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-clean.json +3 -0
  55. package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-vulns.json +77 -0
  56. package/deliver-great-systems/bin/lib/fixtures/package-scan/pip-audit-requirements.json +28 -0
  57. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-lodash.json +30 -0
  58. package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-workspaces.json +55 -0
  59. package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -0
  60. package/deliver-great-systems/bin/lib/frontmatter.cjs +1 -1
  61. package/deliver-great-systems/bin/lib/governance.cjs +211 -0
  62. package/deliver-great-systems/bin/lib/governance.test.cjs +339 -0
  63. package/deliver-great-systems/bin/lib/health-untracked-phase.test.cjs +269 -0
  64. package/deliver-great-systems/bin/lib/ideas.cjs +206 -91
  65. package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
  66. package/deliver-great-systems/bin/lib/init.cjs +357 -61
  67. package/deliver-great-systems/bin/lib/init.test.cjs +625 -8
  68. package/deliver-great-systems/bin/lib/jobs.cjs +131 -25
  69. package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
  70. package/deliver-great-systems/bin/lib/migration.cjs +409 -1
  71. package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
  72. package/deliver-great-systems/bin/lib/milestone.cjs +154 -31
  73. package/deliver-great-systems/bin/lib/milestone.test.cjs +203 -0
  74. package/deliver-great-systems/bin/lib/package-adapters.cjs +530 -0
  75. package/deliver-great-systems/bin/lib/package-adapters.test.cjs +618 -0
  76. package/deliver-great-systems/bin/lib/package-ecosystems.cjs +350 -0
  77. package/deliver-great-systems/bin/lib/package-ecosystems.test.cjs +348 -0
  78. package/deliver-great-systems/bin/lib/package-runner.cjs +199 -0
  79. package/deliver-great-systems/bin/lib/package-runner.test.cjs +198 -0
  80. package/deliver-great-systems/bin/lib/package-scan-provenance.cjs +56 -0
  81. package/deliver-great-systems/bin/lib/package-scan-provenance.test.cjs +103 -0
  82. package/deliver-great-systems/bin/lib/package-scan-report.cjs +1140 -0
  83. package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +1963 -0
  84. package/deliver-great-systems/bin/lib/package-scan-skill.cjs +96 -0
  85. package/deliver-great-systems/bin/lib/package-scan-skill.test.cjs +136 -0
  86. package/deliver-great-systems/bin/lib/package-scan.cjs +919 -0
  87. package/deliver-great-systems/bin/lib/package-scan.test.cjs +2147 -0
  88. package/deliver-great-systems/bin/lib/phase.cjs +146 -3
  89. package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
  90. package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
  91. package/deliver-great-systems/bin/lib/projects.cjs +65 -10
  92. package/deliver-great-systems/bin/lib/projects.test.cjs +198 -2
  93. package/deliver-great-systems/bin/lib/quick.cjs +739 -0
  94. package/deliver-great-systems/bin/lib/quick.test.cjs +730 -0
  95. package/deliver-great-systems/bin/lib/repos.cjs +37 -13
  96. package/deliver-great-systems/bin/lib/review.cjs +1821 -0
  97. package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
  98. package/deliver-great-systems/bin/lib/specs.cjs +3 -81
  99. package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
  100. package/deliver-great-systems/bin/lib/state.cjs +147 -55
  101. package/deliver-great-systems/bin/lib/summary-frontmatter.cjs +54 -0
  102. package/deliver-great-systems/bin/lib/summary-frontmatter.test.cjs +78 -0
  103. package/deliver-great-systems/bin/lib/sweep-scope.test.cjs +263 -0
  104. package/deliver-great-systems/bin/lib/sync.cjs +75 -0
  105. package/deliver-great-systems/bin/lib/verify.cjs +198 -7
  106. package/deliver-great-systems/bin/lib/verify.test.cjs +82 -0
  107. package/deliver-great-systems/bin/lib/wave-0-template-rename.test.cjs +40 -0
  108. package/deliver-great-systems/bin/lib/worktrees.cjs +790 -0
  109. package/deliver-great-systems/bin/lib/worktrees.test.cjs +963 -0
  110. package/deliver-great-systems/references/agent-step-reliability.md +60 -0
  111. package/deliver-great-systems/references/conflict-resolution.md +4 -0
  112. package/deliver-great-systems/references/context-tiers.md +4 -0
  113. package/deliver-great-systems/references/package-scan-config.md +151 -0
  114. package/deliver-great-systems/references/questioning.md +0 -30
  115. package/deliver-great-systems/references/spec-review-loop.md +1 -2
  116. package/deliver-great-systems/references/workflow-conventions.md +29 -0
  117. package/deliver-great-systems/skills/dgs-tests/package-scan.md +44 -0
  118. package/deliver-great-systems/templates/REVIEW.md +35 -0
  119. package/deliver-great-systems/templates/VALIDATION.md +1 -1
  120. package/deliver-great-systems/templates/claude-md.md +27 -0
  121. package/deliver-great-systems/templates/package-scan-report.md +108 -0
  122. package/deliver-great-systems/templates/project.md +6 -170
  123. package/deliver-great-systems/templates/summary.md +3 -1
  124. package/deliver-great-systems/workflows/abandon-quick.md +89 -0
  125. package/deliver-great-systems/workflows/add-idea.md +3 -3
  126. package/deliver-great-systems/workflows/add-phase.md +5 -0
  127. package/deliver-great-systems/workflows/add-tests.md +14 -0
  128. package/deliver-great-systems/workflows/add-todo.md +1 -0
  129. package/deliver-great-systems/workflows/approve-spec.md +25 -4
  130. package/deliver-great-systems/workflows/audit-milestone.md +66 -10
  131. package/deliver-great-systems/workflows/audit-phase.md +15 -5
  132. package/deliver-great-systems/workflows/cancel-job.md +2 -2
  133. package/deliver-great-systems/workflows/check-todos.md +2 -3
  134. package/deliver-great-systems/workflows/codereview.md +103 -9
  135. package/deliver-great-systems/workflows/complete-milestone.md +218 -24
  136. package/deliver-great-systems/workflows/complete-quick.md +106 -0
  137. package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
  138. package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
  139. package/deliver-great-systems/workflows/develop-idea.md +11 -11
  140. package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
  141. package/deliver-great-systems/workflows/discuss-idea.md +1 -1
  142. package/deliver-great-systems/workflows/discuss-phase.md +3 -2
  143. package/deliver-great-systems/workflows/execute-phase.md +209 -33
  144. package/deliver-great-systems/workflows/execute-plan.md +22 -22
  145. package/deliver-great-systems/workflows/help.md +53 -20
  146. package/deliver-great-systems/workflows/import-spec.md +65 -7
  147. package/deliver-great-systems/workflows/init-product.md +45 -167
  148. package/deliver-great-systems/workflows/new-milestone.md +140 -33
  149. package/deliver-great-systems/workflows/new-project.md +60 -331
  150. package/deliver-great-systems/workflows/package-scan.md +59 -0
  151. package/deliver-great-systems/workflows/plan-phase.md +79 -1
  152. package/deliver-great-systems/workflows/progress-all.md +133 -0
  153. package/deliver-great-systems/workflows/quick-abandon.md +89 -0
  154. package/deliver-great-systems/workflows/quick-complete.md +106 -0
  155. package/deliver-great-systems/workflows/quick.md +328 -26
  156. package/deliver-great-systems/workflows/refine-spec.md +1 -1
  157. package/deliver-great-systems/workflows/research-idea.md +77 -139
  158. package/deliver-great-systems/workflows/resume-project.md +2 -2
  159. package/deliver-great-systems/workflows/run-job.md +29 -43
  160. package/deliver-great-systems/workflows/settings.md +13 -77
  161. package/deliver-great-systems/workflows/validate-phase.md +39 -1
  162. package/deliver-great-systems/workflows/verify-work.md +14 -0
  163. package/deliver-great-systems/workflows/write-spec.md +11 -13
  164. package/hooks/dist/dgs-enforce-discipline.js +196 -0
  165. package/package.json +1 -1
  166. package/scripts/build-hooks.js +1 -0
@@ -0,0 +1,198 @@
1
+ /**
2
+ * package-runner.test.cjs -- Unit tests for exit-code-aware tool invocation
3
+ *
4
+ * Covers PKG-11 exit-code layer (findings vs tool_failure vs missing vs timeout),
5
+ * 50 MB buffer handling (PITFALLS.md execSync 1 MiB truncation), Snyk/OSV/govulncheck
6
+ * exit-code edge cases, env passthrough, and ECOSYSTEM_OVERRIDES export.
7
+ *
8
+ * All fabrications use `node -e` so tests run without any security tool installed.
9
+ */
10
+ 'use strict';
11
+ const { test, describe } = require('node:test');
12
+ const assert = require('node:assert');
13
+ const {
14
+ runTool,
15
+ FINDINGS_EXIT_CODES,
16
+ ECOSYSTEM_OVERRIDES,
17
+ DEFAULT_TIMEOUT_MS,
18
+ DEFAULT_MAX_BUFFER,
19
+ } = require('./package-runner.cjs');
20
+
21
+ const NODE = process.execPath;
22
+ const fakeExitCode = (code, stdout = '', stderr = '') =>
23
+ [NODE, '-e', `process.stdout.write(${JSON.stringify(stdout)}); process.stderr.write(${JSON.stringify(stderr)}); process.exit(${code})`];
24
+ const fakeTimeout = (ms) =>
25
+ [NODE, '-e', `setTimeout(() => {}, ${ms});`];
26
+ const fakeLargeStdout = (bytes) =>
27
+ [NODE, '-e', `const s = 'x'.repeat(${bytes}); process.stdout.write(JSON.stringify({ buf: s }));`];
28
+ const fakeEnvEcho = (varName) =>
29
+ [NODE, '-e', `process.stdout.write(process.env[${JSON.stringify(varName)}] || '');`];
30
+ const fakeMissingBinary = () => ['definitely-not-a-binary-xxx-' + Date.now()];
31
+
32
+ describe('runTool - outcome mapping', () => {
33
+ test('exitCode 0 with stdout -> outcome: "ok"', () => {
34
+ const r = runTool(process.cwd(), 'snyk', fakeExitCode(0, '{}'));
35
+ assert.strictEqual(r.outcome, 'ok');
36
+ assert.strictEqual(r.exitCode, 0);
37
+ assert.strictEqual(r.stdout, '{}');
38
+ });
39
+
40
+ test('Snyk exit 1 (findings) -> outcome: "ok" because 1 in FINDINGS_EXIT_CODES.snyk', () => {
41
+ const r = runTool(process.cwd(), 'snyk', fakeExitCode(1, '{"vulns":[]}'));
42
+ assert.strictEqual(r.outcome, 'ok');
43
+ assert.strictEqual(r.exitCode, 1);
44
+ });
45
+
46
+ test('Snyk exit 2 -> outcome: "tool_failure", stderr preserved', () => {
47
+ const r = runTool(process.cwd(), 'snyk', fakeExitCode(2, '', 'boom'));
48
+ assert.strictEqual(r.outcome, 'tool_failure');
49
+ assert.strictEqual(r.exitCode, 2);
50
+ assert.strictEqual(r.stderr, 'boom');
51
+ });
52
+
53
+ test('npm-audit exit 1 -> outcome: "ok"', () => {
54
+ const r = runTool(process.cwd(), 'npm-audit', fakeExitCode(1, '{}'));
55
+ assert.strictEqual(r.outcome, 'ok');
56
+ });
57
+
58
+ test('pip-audit exit 1 -> outcome: "ok"', () => {
59
+ const r = runTool(process.cwd(), 'pip-audit', fakeExitCode(1, '{}'));
60
+ assert.strictEqual(r.outcome, 'ok');
61
+ });
62
+
63
+ test('bundler-audit exit 1 -> outcome: "ok"', () => {
64
+ const r = runTool(process.cwd(), 'bundler-audit', fakeExitCode(1, '{}'));
65
+ assert.strictEqual(r.outcome, 'ok');
66
+ });
67
+
68
+ test('osv-scanner exit 1 -> outcome: "ok"', () => {
69
+ const r = runTool(process.cwd(), 'osv-scanner', fakeExitCode(1, '{}'));
70
+ assert.strictEqual(r.outcome, 'ok');
71
+ });
72
+
73
+ test('osv-scanner exit 128 -> outcome: "ok" (no-packages-found legitimate)', () => {
74
+ const r = runTool(process.cwd(), 'osv-scanner', fakeExitCode(128, '{}'));
75
+ assert.strictEqual(r.outcome, 'ok');
76
+ });
77
+
78
+ test('govulncheck exit 0 -> outcome: "ok" (always)', () => {
79
+ const r = runTool(process.cwd(), 'govulncheck', fakeExitCode(0, '{}'));
80
+ assert.strictEqual(r.outcome, 'ok');
81
+ });
82
+
83
+ test('govulncheck exit 1 (hypothetical failure) -> outcome: "tool_failure"', () => {
84
+ const r = runTool(process.cwd(), 'govulncheck', fakeExitCode(1, '', 'fail'));
85
+ assert.strictEqual(r.outcome, 'tool_failure');
86
+ });
87
+
88
+ test('exitCode 127 -> outcome: "missing"', () => {
89
+ const r = runTool(process.cwd(), 'snyk', fakeExitCode(127, '', 'not found'));
90
+ assert.strictEqual(r.outcome, 'missing');
91
+ });
92
+
93
+ test('ENOENT spawn error -> outcome: "missing"', () => {
94
+ const r = runTool(process.cwd(), 'snyk', fakeMissingBinary());
95
+ assert.strictEqual(r.outcome, 'missing');
96
+ assert.strictEqual(r.exitCode, 127);
97
+ });
98
+
99
+ test('timeout (SIGTERM after timeoutMs) -> outcome: "timeout", timedOut: true, duration >= timeoutMs', () => {
100
+ const r = runTool(process.cwd(), 'snyk', fakeTimeout(60000), { timeoutMs: 300 });
101
+ assert.strictEqual(r.outcome, 'timeout');
102
+ assert.strictEqual(r.timedOut, true);
103
+ assert.ok(r.duration >= 200, `expected duration >= 200, got ${r.duration}`);
104
+ });
105
+
106
+ test('toolKey null -> outcome: "no_native_tool_for_ecosystem", spawnSync NOT invoked', () => {
107
+ const start = Date.now();
108
+ const r = runTool(process.cwd(), null, [NODE, '-e', 'process.exit(0)']);
109
+ const dt = Date.now() - start;
110
+ assert.strictEqual(r.outcome, 'no_native_tool_for_ecosystem');
111
+ // spawnSync not called: must return essentially instantly (<50ms) and duration: 0
112
+ assert.strictEqual(r.duration, 0);
113
+ assert.ok(dt < 100, `no-spawn branch should be immediate, got ${dt}ms`);
114
+ });
115
+
116
+ test('toolKey undefined -> outcome: "no_native_tool_for_ecosystem"', () => {
117
+ const r = runTool(process.cwd(), undefined, [NODE, '-e', 'process.exit(0)']);
118
+ assert.strictEqual(r.outcome, 'no_native_tool_for_ecosystem');
119
+ });
120
+ });
121
+
122
+ describe('runTool - buffer and env', () => {
123
+ test('10 MB stdout is captured intact (no truncation)', () => {
124
+ const bytes = 10 * 1024 * 1024;
125
+ const r = runTool(process.cwd(), 'snyk', fakeLargeStdout(bytes));
126
+ assert.strictEqual(r.outcome, 'ok');
127
+ assert.ok(r.stdout.length >= bytes, `expected stdout.length >= ${bytes}, got ${r.stdout.length}`);
128
+ });
129
+
130
+ test('opts.env is merged into child process env (child can read injected var)', () => {
131
+ const r = runTool(process.cwd(), 'snyk', fakeEnvEcho('DGS_TEST_INJECT'), {
132
+ env: { DGS_TEST_INJECT: 'hello-world' },
133
+ });
134
+ assert.strictEqual(r.outcome, 'ok');
135
+ assert.strictEqual(r.stdout, 'hello-world');
136
+ });
137
+
138
+ test('process.env is inherited (e.g., child sees PATH)', () => {
139
+ const r = runTool(process.cwd(), 'snyk', fakeEnvEcho('PATH'));
140
+ assert.strictEqual(r.outcome, 'ok');
141
+ assert.ok(r.stdout.length > 0, 'PATH should be inherited and non-empty');
142
+ });
143
+ });
144
+
145
+ describe('runTool - JSON parsing', () => {
146
+ test('on ok with expectJson and well-formed JSON, parsed field is populated', () => {
147
+ const r = runTool(process.cwd(), 'snyk', fakeExitCode(0, '{"ok":true,"count":3}'), { expectJson: true });
148
+ assert.strictEqual(r.outcome, 'ok');
149
+ assert.deepStrictEqual(r.parsed, { ok: true, count: 3 });
150
+ });
151
+
152
+ test('on ok with expectJson and malformed JSON, outcome downgrades to tool_failure', () => {
153
+ const r = runTool(process.cwd(), 'snyk', fakeExitCode(0, 'not json {'), { expectJson: true });
154
+ assert.strictEqual(r.outcome, 'tool_failure');
155
+ assert.ok(/JSON parse error/.test(r.stderr));
156
+ });
157
+
158
+ test('on ok without expectJson, parsed is not set', () => {
159
+ const r = runTool(process.cwd(), 'snyk', fakeExitCode(0, '{"a":1}'));
160
+ assert.strictEqual(r.outcome, 'ok');
161
+ assert.strictEqual(r.parsed, undefined);
162
+ });
163
+
164
+ test('govulncheck NDJSON-style multi-object stream is NOT parsed by runner (runner treats it as raw)', () => {
165
+ // govulncheck emits NDJSON — runner should deliver stdout untouched when expectJson is false.
166
+ const ndjson = '{"message":{"type":"osv"}}\n{"message":{"type":"finding"}}';
167
+ const r = runTool(process.cwd(), 'govulncheck', fakeExitCode(0, ndjson), { expectJson: false });
168
+ assert.strictEqual(r.outcome, 'ok');
169
+ assert.strictEqual(r.stdout, ndjson);
170
+ assert.strictEqual(r.parsed, undefined);
171
+ });
172
+ });
173
+
174
+ describe('runTool - exports', () => {
175
+ test('ECOSYSTEM_OVERRIDES exports yarn: { force_tool: "osv", reason: string }', () => {
176
+ assert.ok(ECOSYSTEM_OVERRIDES.yarn);
177
+ assert.strictEqual(ECOSYSTEM_OVERRIDES.yarn.force_tool, 'osv');
178
+ assert.strictEqual(typeof ECOSYSTEM_OVERRIDES.yarn.reason, 'string');
179
+ assert.ok(ECOSYSTEM_OVERRIDES.yarn.reason.length > 0);
180
+ });
181
+
182
+ test('FINDINGS_EXIT_CODES exports all 6 tool keys with correct code arrays', () => {
183
+ assert.deepStrictEqual(Array.from(FINDINGS_EXIT_CODES.snyk), [1]);
184
+ assert.deepStrictEqual(Array.from(FINDINGS_EXIT_CODES['npm-audit']), [1]);
185
+ assert.deepStrictEqual(Array.from(FINDINGS_EXIT_CODES['osv-scanner']), [1, 128]);
186
+ assert.deepStrictEqual(Array.from(FINDINGS_EXIT_CODES['pip-audit']), [1]);
187
+ assert.deepStrictEqual(Array.from(FINDINGS_EXIT_CODES.govulncheck), []);
188
+ assert.deepStrictEqual(Array.from(FINDINGS_EXIT_CODES['bundler-audit']), [1]);
189
+ });
190
+
191
+ test('DEFAULT_TIMEOUT_MS is 300000', () => {
192
+ assert.strictEqual(DEFAULT_TIMEOUT_MS, 300_000);
193
+ });
194
+
195
+ test('DEFAULT_MAX_BUFFER is 50 * 1024 * 1024', () => {
196
+ assert.strictEqual(DEFAULT_MAX_BUFFER, 50 * 1024 * 1024);
197
+ });
198
+ });
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+ /**
3
+ * package-scan-provenance.cjs — PKG-33
4
+ *
5
+ * Resolves the commit that introduced a dependency manifest entry by running
6
+ * `git log --format=%H%x00%s -S <packageName> -- <manifestPath>` in the repo
7
+ * directory, then extracts a DGS plan-id (NNN-NN) from the commit subject.
8
+ *
9
+ * Pure module modulo the spawnFn seam: caller injects spawnSync for tests.
10
+ *
11
+ * Exports:
12
+ * provenanceLookup({ repoDir, manifestPath, packageName, spawnFn })
13
+ * -> { commit: string|null, plan: string|null }
14
+ * _extractPlanIdFromSubject(subject) -> string|null
15
+ */
16
+ const { spawnSync } = require('child_process');
17
+
18
+ const PLAN_ID_REGEX = /\b(\d{3}-\d{2})\b/;
19
+
20
+ function _extractPlanIdFromSubject(subject) {
21
+ if (subject === null || subject === undefined) return null;
22
+ const m = String(subject).match(PLAN_ID_REGEX);
23
+ return m ? m[1] : null;
24
+ }
25
+
26
+ function provenanceLookup(args) {
27
+ const { repoDir, manifestPath, packageName } = args || {};
28
+ const spawnFn = (args && args.spawnFn) || spawnSync;
29
+ if (!repoDir || !manifestPath || !packageName) {
30
+ return { commit: null, plan: null };
31
+ }
32
+ let result;
33
+ try {
34
+ result = spawnFn('git', ['log', '--format=%H%x00%s', '-S', packageName, '--', manifestPath], {
35
+ cwd: repoDir,
36
+ encoding: 'utf-8',
37
+ timeout: 5000,
38
+ });
39
+ } catch {
40
+ return { commit: null, plan: null };
41
+ }
42
+ if (!result || result.error || typeof result.stdout !== 'string' || result.stdout.length === 0) {
43
+ return { commit: null, plan: null };
44
+ }
45
+ const lines = result.stdout.split('\n').filter(l => l.length > 0);
46
+ if (lines.length === 0) return { commit: null, plan: null };
47
+ // git log emits newest-first; the oldest commit is the LAST non-empty line.
48
+ const oldestLine = lines[lines.length - 1];
49
+ const [fullSha, subject] = oldestLine.split('\u0000');
50
+ if (!fullSha) return { commit: null, plan: null };
51
+ const shortSha = String(fullSha).slice(0, 7);
52
+ const planId = _extractPlanIdFromSubject(subject || '');
53
+ return { commit: shortSha, plan: planId };
54
+ }
55
+
56
+ module.exports = { provenanceLookup, _extractPlanIdFromSubject };
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+ const test = require('node:test');
3
+ const assert = require('node:assert');
4
+ const { provenanceLookup, _extractPlanIdFromSubject } = require('./package-scan-provenance.cjs');
5
+
6
+ test.describe('_extractPlanIdFromSubject (PKG-33)', () => {
7
+ test.test('feat(149-01): ... → 149-01', () => {
8
+ assert.strictEqual(_extractPlanIdFromSubject('feat(149-01): implement scanner'), '149-01');
9
+ });
10
+ test.test('test(152-03): ... → 152-03', () => {
11
+ assert.strictEqual(_extractPlanIdFromSubject('test(152-03): add gate-parity describe block'), '152-03');
12
+ });
13
+ test.test('commit with inline 149-01 → 149-01', () => {
14
+ assert.strictEqual(_extractPlanIdFromSubject('Merge branch feature/149-01-foundation'), '149-01');
15
+ });
16
+ test.test('three-digit phase: feat(999-42) → 999-42', () => {
17
+ assert.strictEqual(_extractPlanIdFromSubject('feat(999-42): something'), '999-42');
18
+ });
19
+ test.test('missing zero pad: 149-1 → null', () => {
20
+ assert.strictEqual(_extractPlanIdFromSubject('feat(149-1): ...'), null);
21
+ });
22
+ test.test('wrong digit count: 12-345 → null', () => {
23
+ assert.strictEqual(_extractPlanIdFromSubject('feat(12-345): ...'), null);
24
+ });
25
+ test.test('random text → null', () => {
26
+ assert.strictEqual(_extractPlanIdFromSubject('random commit message'), null);
27
+ });
28
+ test.test('empty subject → null', () => {
29
+ assert.strictEqual(_extractPlanIdFromSubject(''), null);
30
+ });
31
+ test.test('null subject → null', () => {
32
+ assert.strictEqual(_extractPlanIdFromSubject(null), null);
33
+ });
34
+ test.test('undefined subject → null', () => {
35
+ assert.strictEqual(_extractPlanIdFromSubject(undefined), null);
36
+ });
37
+ });
38
+
39
+ test.describe('provenanceLookup (PKG-33)', () => {
40
+ test.test('empty git log output => {commit:null, plan:null}', () => {
41
+ const spawnFn = () => ({ status: 0, stdout: '', stderr: '' });
42
+ const r = provenanceLookup({ repoDir: '/tmp/x', manifestPath: 'package.json', packageName: 'lodash', spawnFn });
43
+ assert.deepStrictEqual(r, { commit: null, plan: null });
44
+ });
45
+ test.test('git log returns one commit with plan-id => {commit:<7sha>, plan:<plan-id>}', () => {
46
+ const spawnFn = () => ({
47
+ status: 0,
48
+ stdout: 'abc1234567890def1234567890\u0000feat(152-03): add gate-parity describe block\n',
49
+ stderr: '',
50
+ });
51
+ const r = provenanceLookup({ repoDir: '/tmp/x', manifestPath: 'package.json', packageName: 'lodash', spawnFn });
52
+ assert.strictEqual(r.commit, 'abc1234');
53
+ assert.strictEqual(r.plan, '152-03');
54
+ });
55
+ test.test('git log returns multiple commits => picks OLDEST (last line)', () => {
56
+ const spawnFn = () => ({
57
+ status: 0,
58
+ stdout: 'newSHA99999999999999999999\u0000feat(153-01): update\n' +
59
+ 'oldSHA1111111111111111111\u0000feat(149-01): add lodash dep\n',
60
+ stderr: '',
61
+ });
62
+ const r = provenanceLookup({ repoDir: '/tmp/x', manifestPath: 'package.json', packageName: 'lodash', spawnFn });
63
+ assert.strictEqual(r.commit, 'oldSHA1');
64
+ assert.strictEqual(r.plan, '149-01');
65
+ });
66
+ test.test('git log commit without plan-id => {commit:<sha>, plan:null}', () => {
67
+ const spawnFn = () => ({
68
+ status: 0,
69
+ stdout: 'def5678901234567890123456\u0000chore: bump deps\n',
70
+ stderr: '',
71
+ });
72
+ const r = provenanceLookup({ repoDir: '/tmp/x', manifestPath: 'package.json', packageName: 'lodash', spawnFn });
73
+ assert.strictEqual(r.commit, 'def5678');
74
+ assert.strictEqual(r.plan, null);
75
+ });
76
+ test.test('git not found / spawn error => {commit:null, plan:null}', () => {
77
+ const spawnFn = () => ({ error: new Error('ENOENT'), status: null, stdout: '', stderr: '' });
78
+ const r = provenanceLookup({ repoDir: '/tmp/x', manifestPath: 'package.json', packageName: 'lodash', spawnFn });
79
+ assert.deepStrictEqual(r, { commit: null, plan: null });
80
+ });
81
+ test.test('missing manifestPath => {commit:null, plan:null} (no-op)', () => {
82
+ const spawnFn = () => { throw new Error('should not be called'); };
83
+ const r = provenanceLookup({ repoDir: '/tmp/x', manifestPath: null, packageName: 'lodash', spawnFn });
84
+ assert.deepStrictEqual(r, { commit: null, plan: null });
85
+ });
86
+ test.test('missing packageName => {commit:null, plan:null} (no-op)', () => {
87
+ const spawnFn = () => { throw new Error('should not be called'); };
88
+ const r = provenanceLookup({ repoDir: '/tmp/x', manifestPath: 'package.json', packageName: null, spawnFn });
89
+ assert.deepStrictEqual(r, { commit: null, plan: null });
90
+ });
91
+ test.test('spawnFn receives argv: ["git", "log", "--format=%H%x00%s", "-S", packageName, "--", manifestPath]', () => {
92
+ let capturedArgv = null;
93
+ let capturedOpts = null;
94
+ const spawnFn = (cmd, argv, opts) => {
95
+ capturedArgv = [cmd, ...argv];
96
+ capturedOpts = opts;
97
+ return { status: 0, stdout: '', stderr: '' };
98
+ };
99
+ provenanceLookup({ repoDir: '/tmp/r', manifestPath: 'package.json', packageName: 'lodash', spawnFn });
100
+ assert.deepStrictEqual(capturedArgv, ['git', 'log', '--format=%H%x00%s', '-S', 'lodash', '--', 'package.json']);
101
+ assert.strictEqual(capturedOpts.cwd, '/tmp/r');
102
+ });
103
+ });