@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.
- package/CHANGELOG.md +197 -0
- package/README.md +34 -2
- package/agents/dgs-executor.md +124 -3
- package/agents/dgs-idea-researcher.md +447 -0
- package/agents/dgs-plan-checker.md +61 -3
- package/agents/dgs-planner.md +51 -8
- package/bin/install.js +44 -0
- package/commands/dgs/abandon-quick.md +28 -0
- package/commands/dgs/add-tests.md +2 -2
- package/commands/dgs/audit-milestone.md +4 -3
- package/commands/dgs/capture-principle.md +11 -11
- package/commands/dgs/cleanup.md +2 -2
- package/commands/dgs/complete-milestone.md +11 -11
- package/commands/dgs/complete-quick.md +28 -0
- package/commands/dgs/create-milestone-job.md +2 -2
- package/commands/dgs/debug.md +3 -3
- package/commands/dgs/develop-idea.md +1 -1
- package/commands/dgs/diff-report.md +124 -0
- package/commands/dgs/fast.md +3 -1
- package/commands/dgs/health.md +1 -1
- package/commands/dgs/map-codebase.md +6 -6
- package/commands/dgs/new-milestone.md +5 -5
- package/commands/dgs/new-project.md +8 -21
- package/commands/dgs/package-scan.md +43 -0
- package/commands/dgs/plan-milestone-gaps.md +1 -1
- package/commands/dgs/progress.md +3 -3
- package/commands/dgs/quick-abandon.md +8 -0
- package/commands/dgs/quick-complete.md +8 -0
- package/commands/dgs/quick.md +10 -3
- package/commands/dgs/research-idea.md +3 -2
- package/commands/dgs/research-phase.md +3 -3
- package/commands/dgs/switch-project.md +14 -1
- package/commands/dgs/write-spec.md +3 -3
- package/deliver-great-systems/bin/dgs-tools.cjs +401 -32
- package/deliver-great-systems/bin/lib/audit-tolerance.cjs +77 -0
- package/deliver-great-systems/bin/lib/audit-tolerance.test.cjs +101 -0
- package/deliver-great-systems/bin/lib/commands.cjs +626 -46
- package/deliver-great-systems/bin/lib/commands.test.cjs +451 -0
- package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
- package/deliver-great-systems/bin/lib/config.cjs +80 -6
- package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
- package/deliver-great-systems/bin/lib/context.cjs +120 -0
- package/deliver-great-systems/bin/lib/core.cjs +35 -14
- package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
- package/deliver-great-systems/bin/lib/execution.cjs +49 -17
- package/deliver-great-systems/bin/lib/fast-routing.cjs +199 -0
- package/deliver-great-systems/bin/lib/fast-routing.test.cjs +108 -0
- package/deliver-great-systems/bin/lib/final-commit-precondition.test.cjs +87 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/bundler-audit-gemfile.json +21 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-expected.md +186 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-runresult.json +235 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/govulncheck-import.json +3 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/npm-audit-v10.json +37 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-clean.json +3 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-vulns.json +77 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/pip-audit-requirements.json +28 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-lodash.json +30 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-workspaces.json +55 -0
- package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -0
- package/deliver-great-systems/bin/lib/frontmatter.cjs +1 -1
- package/deliver-great-systems/bin/lib/governance.cjs +211 -0
- package/deliver-great-systems/bin/lib/governance.test.cjs +339 -0
- package/deliver-great-systems/bin/lib/health-untracked-phase.test.cjs +269 -0
- package/deliver-great-systems/bin/lib/ideas.cjs +206 -91
- package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
- package/deliver-great-systems/bin/lib/init.cjs +357 -61
- package/deliver-great-systems/bin/lib/init.test.cjs +625 -8
- package/deliver-great-systems/bin/lib/jobs.cjs +131 -25
- package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
- package/deliver-great-systems/bin/lib/migration.cjs +409 -1
- package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
- package/deliver-great-systems/bin/lib/milestone.cjs +154 -31
- package/deliver-great-systems/bin/lib/milestone.test.cjs +203 -0
- package/deliver-great-systems/bin/lib/package-adapters.cjs +530 -0
- package/deliver-great-systems/bin/lib/package-adapters.test.cjs +618 -0
- package/deliver-great-systems/bin/lib/package-ecosystems.cjs +350 -0
- package/deliver-great-systems/bin/lib/package-ecosystems.test.cjs +348 -0
- package/deliver-great-systems/bin/lib/package-runner.cjs +199 -0
- package/deliver-great-systems/bin/lib/package-runner.test.cjs +198 -0
- package/deliver-great-systems/bin/lib/package-scan-provenance.cjs +56 -0
- package/deliver-great-systems/bin/lib/package-scan-provenance.test.cjs +103 -0
- package/deliver-great-systems/bin/lib/package-scan-report.cjs +1140 -0
- package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +1963 -0
- package/deliver-great-systems/bin/lib/package-scan-skill.cjs +96 -0
- package/deliver-great-systems/bin/lib/package-scan-skill.test.cjs +136 -0
- package/deliver-great-systems/bin/lib/package-scan.cjs +919 -0
- package/deliver-great-systems/bin/lib/package-scan.test.cjs +2147 -0
- package/deliver-great-systems/bin/lib/phase.cjs +146 -3
- package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
- package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
- package/deliver-great-systems/bin/lib/projects.cjs +65 -10
- package/deliver-great-systems/bin/lib/projects.test.cjs +198 -2
- package/deliver-great-systems/bin/lib/quick.cjs +739 -0
- package/deliver-great-systems/bin/lib/quick.test.cjs +730 -0
- package/deliver-great-systems/bin/lib/repos.cjs +37 -13
- package/deliver-great-systems/bin/lib/review.cjs +1821 -0
- package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
- package/deliver-great-systems/bin/lib/specs.cjs +3 -81
- package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
- package/deliver-great-systems/bin/lib/state.cjs +147 -55
- package/deliver-great-systems/bin/lib/summary-frontmatter.cjs +54 -0
- package/deliver-great-systems/bin/lib/summary-frontmatter.test.cjs +78 -0
- package/deliver-great-systems/bin/lib/sweep-scope.test.cjs +263 -0
- package/deliver-great-systems/bin/lib/sync.cjs +75 -0
- package/deliver-great-systems/bin/lib/verify.cjs +198 -7
- package/deliver-great-systems/bin/lib/verify.test.cjs +82 -0
- package/deliver-great-systems/bin/lib/wave-0-template-rename.test.cjs +40 -0
- package/deliver-great-systems/bin/lib/worktrees.cjs +790 -0
- package/deliver-great-systems/bin/lib/worktrees.test.cjs +963 -0
- package/deliver-great-systems/references/agent-step-reliability.md +60 -0
- package/deliver-great-systems/references/conflict-resolution.md +4 -0
- package/deliver-great-systems/references/context-tiers.md +4 -0
- package/deliver-great-systems/references/package-scan-config.md +151 -0
- package/deliver-great-systems/references/questioning.md +0 -30
- package/deliver-great-systems/references/spec-review-loop.md +1 -2
- package/deliver-great-systems/references/workflow-conventions.md +29 -0
- package/deliver-great-systems/skills/dgs-tests/package-scan.md +44 -0
- package/deliver-great-systems/templates/REVIEW.md +35 -0
- package/deliver-great-systems/templates/VALIDATION.md +1 -1
- package/deliver-great-systems/templates/claude-md.md +27 -0
- package/deliver-great-systems/templates/package-scan-report.md +108 -0
- package/deliver-great-systems/templates/project.md +6 -170
- package/deliver-great-systems/templates/summary.md +3 -1
- package/deliver-great-systems/workflows/abandon-quick.md +89 -0
- package/deliver-great-systems/workflows/add-idea.md +3 -3
- package/deliver-great-systems/workflows/add-phase.md +5 -0
- package/deliver-great-systems/workflows/add-tests.md +14 -0
- package/deliver-great-systems/workflows/add-todo.md +1 -0
- package/deliver-great-systems/workflows/approve-spec.md +25 -4
- package/deliver-great-systems/workflows/audit-milestone.md +66 -10
- package/deliver-great-systems/workflows/audit-phase.md +15 -5
- package/deliver-great-systems/workflows/cancel-job.md +2 -2
- package/deliver-great-systems/workflows/check-todos.md +2 -3
- package/deliver-great-systems/workflows/codereview.md +103 -9
- package/deliver-great-systems/workflows/complete-milestone.md +218 -24
- package/deliver-great-systems/workflows/complete-quick.md +106 -0
- package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
- package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
- package/deliver-great-systems/workflows/develop-idea.md +11 -11
- package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
- package/deliver-great-systems/workflows/discuss-idea.md +1 -1
- package/deliver-great-systems/workflows/discuss-phase.md +3 -2
- package/deliver-great-systems/workflows/execute-phase.md +209 -33
- package/deliver-great-systems/workflows/execute-plan.md +22 -22
- package/deliver-great-systems/workflows/help.md +53 -20
- package/deliver-great-systems/workflows/import-spec.md +65 -7
- package/deliver-great-systems/workflows/init-product.md +45 -167
- package/deliver-great-systems/workflows/new-milestone.md +140 -33
- package/deliver-great-systems/workflows/new-project.md +60 -331
- package/deliver-great-systems/workflows/package-scan.md +59 -0
- package/deliver-great-systems/workflows/plan-phase.md +79 -1
- package/deliver-great-systems/workflows/progress-all.md +133 -0
- package/deliver-great-systems/workflows/quick-abandon.md +89 -0
- package/deliver-great-systems/workflows/quick-complete.md +106 -0
- package/deliver-great-systems/workflows/quick.md +328 -26
- package/deliver-great-systems/workflows/refine-spec.md +1 -1
- package/deliver-great-systems/workflows/research-idea.md +77 -139
- package/deliver-great-systems/workflows/resume-project.md +2 -2
- package/deliver-great-systems/workflows/run-job.md +29 -43
- package/deliver-great-systems/workflows/settings.md +13 -77
- package/deliver-great-systems/workflows/validate-phase.md +39 -1
- package/deliver-great-systems/workflows/verify-work.md +14 -0
- package/deliver-great-systems/workflows/write-spec.md +11 -13
- package/hooks/dist/dgs-enforce-discipline.js +196 -0
- package/package.json +1 -1
- package/scripts/build-hooks.js +1 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
const { describe, it } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const { normalizeAuthor, getContributors, checkFourEyes } = require('./governance.cjs');
|
|
4
|
+
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
5
|
+
|
|
6
|
+
describe('normalizeAuthor', () => {
|
|
7
|
+
it('extracts and lowercases name from "Name <email>" format', () => {
|
|
8
|
+
assert.equal(normalizeAuthor('Adrian King <adrian@example.com>'), 'adrian king');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('lowercases name-only strings', () => {
|
|
12
|
+
assert.equal(normalizeAuthor('Adrian King'), 'adrian king');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('handles single-word names', () => {
|
|
16
|
+
assert.equal(normalizeAuthor('Adrian'), 'adrian');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('preserves already-lowercase names', () => {
|
|
20
|
+
assert.equal(normalizeAuthor('adrian king'), 'adrian king');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns empty string for empty input', () => {
|
|
24
|
+
assert.equal(normalizeAuthor(''), '');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns empty string for null', () => {
|
|
28
|
+
assert.equal(normalizeAuthor(null), '');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns empty string for undefined', () => {
|
|
32
|
+
assert.equal(normalizeAuthor(undefined), '');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('handles email-only strings without crashing', () => {
|
|
36
|
+
const result = normalizeAuthor('<adrian@example.com>');
|
|
37
|
+
assert.equal(typeof result, 'string');
|
|
38
|
+
assert.ok(result.length > 0); // passes through as-is, lowercased
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('workflow.four_eyes config key', () => {
|
|
43
|
+
it('is present in VALID_CONFIG_KEYS', () => {
|
|
44
|
+
const { VALID_CONFIG_KEYS } = require('./config.cjs');
|
|
45
|
+
assert.ok(
|
|
46
|
+
VALID_CONFIG_KEYS || true,
|
|
47
|
+
'config.cjs should be importable'
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('getContributors', () => {
|
|
53
|
+
it('is exported as a function', () => {
|
|
54
|
+
assert.equal(typeof getContributors, 'function');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns object with contributors array and normalized Map', () => {
|
|
58
|
+
const result = getContributors(process.cwd());
|
|
59
|
+
assert.ok(Array.isArray(result.contributors));
|
|
60
|
+
assert.ok(result.normalized instanceof Map);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns empty contributors when no SUMMARY.md files exist in milestone', () => {
|
|
64
|
+
const result = getContributors(process.cwd());
|
|
65
|
+
assert.ok(Array.isArray(result.contributors));
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('getContributors - extractFrontmatter integration', () => {
|
|
70
|
+
it('extracts executed_by from SUMMARY.md frontmatter', () => {
|
|
71
|
+
const summaryContent = [
|
|
72
|
+
'---',
|
|
73
|
+
'phase: 130-verification',
|
|
74
|
+
'plan: 01',
|
|
75
|
+
'executed_by: "Adrian King <Adrian@tracetasks.com>"',
|
|
76
|
+
'---',
|
|
77
|
+
'',
|
|
78
|
+
'# Summary',
|
|
79
|
+
].join('\n');
|
|
80
|
+
const fm = extractFrontmatter(summaryContent);
|
|
81
|
+
assert.equal(fm.executed_by, 'Adrian King <Adrian@tracetasks.com>');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('extracts created_by from PLAN.md frontmatter', () => {
|
|
85
|
+
const planContent = [
|
|
86
|
+
'---',
|
|
87
|
+
'phase: 130',
|
|
88
|
+
'plan: 01',
|
|
89
|
+
'created_by: "Adrian King <Adrian@tracetasks.com>"',
|
|
90
|
+
'---',
|
|
91
|
+
'',
|
|
92
|
+
'# Plan',
|
|
93
|
+
].join('\n');
|
|
94
|
+
const fm = extractFrontmatter(planContent);
|
|
95
|
+
assert.equal(fm.created_by, 'Adrian King <Adrian@tracetasks.com>');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('handles SUMMARY.md without executed_by field', () => {
|
|
99
|
+
const summaryContent = [
|
|
100
|
+
'---',
|
|
101
|
+
'phase: 130-verification',
|
|
102
|
+
'plan: 01',
|
|
103
|
+
'---',
|
|
104
|
+
'',
|
|
105
|
+
'# Summary',
|
|
106
|
+
].join('\n');
|
|
107
|
+
const fm = extractFrontmatter(summaryContent);
|
|
108
|
+
assert.equal(fm.executed_by, undefined);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('handles empty executed_by field', () => {
|
|
112
|
+
const summaryContent = [
|
|
113
|
+
'---',
|
|
114
|
+
'phase: 130-verification',
|
|
115
|
+
'plan: 01',
|
|
116
|
+
'executed_by: ""',
|
|
117
|
+
'---',
|
|
118
|
+
'',
|
|
119
|
+
'# Summary',
|
|
120
|
+
].join('\n');
|
|
121
|
+
const fm = extractFrontmatter(summaryContent);
|
|
122
|
+
assert.equal(fm.executed_by, '');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('STATE.md milestone-context quick detection', () => {
|
|
127
|
+
it('regex matches milestone-context quick entries', () => {
|
|
128
|
+
const stateTable = '| 260407-shw | Move archives | 2026-04-07 | abc123 | done | milestone (v21.0) |';
|
|
129
|
+
const pattern = /\|\s*(\S+)\s*\|[^|]*\|[^|]*\|[^|]*\|[^|]*\|\s*milestone\s*\(([^)]+)\)\s*\|/gi;
|
|
130
|
+
const match = pattern.exec(stateTable);
|
|
131
|
+
assert.ok(match, 'regex should match milestone-context entry');
|
|
132
|
+
assert.equal(match[1], '260407-shw');
|
|
133
|
+
assert.equal(match[2], 'v21.0');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('regex does not match non-milestone quick entries', () => {
|
|
137
|
+
const stateTable = '| 260407-shw | Move archives | 2026-04-07 | abc123 | done | quick/260407-shw-move-archives |';
|
|
138
|
+
const pattern = /\|\s*(\S+)\s*\|[^|]*\|[^|]*\|[^|]*\|[^|]*\|\s*milestone\s*\(([^)]+)\)\s*\|/gi;
|
|
139
|
+
const match = pattern.exec(stateTable);
|
|
140
|
+
assert.equal(match, null, 'regex should not match non-milestone entry');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('checkFourEyes', () => {
|
|
145
|
+
it('is exported as a function', () => {
|
|
146
|
+
assert.equal(typeof checkFourEyes, 'function');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('return shape', () => {
|
|
150
|
+
it('always returns object with required fields', () => {
|
|
151
|
+
const result = checkFourEyes([], 'User', 'off');
|
|
152
|
+
assert.ok('passed' in result);
|
|
153
|
+
assert.ok('mode' in result);
|
|
154
|
+
assert.ok('currentUser' in result);
|
|
155
|
+
assert.ok('contributors' in result);
|
|
156
|
+
assert.ok('matchedUser' in result);
|
|
157
|
+
assert.ok('message' in result);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('off mode (REQ-06)', () => {
|
|
162
|
+
it('always passes regardless of match', () => {
|
|
163
|
+
const result = checkFourEyes(
|
|
164
|
+
['Adrian King <a@x.com>'],
|
|
165
|
+
'Adrian King <a@x.com>',
|
|
166
|
+
'off'
|
|
167
|
+
);
|
|
168
|
+
assert.equal(result.passed, true);
|
|
169
|
+
assert.equal(result.mode, 'off');
|
|
170
|
+
assert.equal(result.matchedUser, null);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('passes with empty contributors', () => {
|
|
174
|
+
const result = checkFourEyes([], 'User', 'off');
|
|
175
|
+
assert.equal(result.passed, true);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('warn mode (REQ-07)', () => {
|
|
180
|
+
it('fails when currentUser matches a contributor', () => {
|
|
181
|
+
const result = checkFourEyes(
|
|
182
|
+
['Adrian King <a@x.com>'],
|
|
183
|
+
'Adrian King <a@x.com>',
|
|
184
|
+
'warn'
|
|
185
|
+
);
|
|
186
|
+
assert.equal(result.passed, false);
|
|
187
|
+
assert.equal(result.mode, 'warn');
|
|
188
|
+
assert.equal(result.matchedUser, 'Adrian King <a@x.com>');
|
|
189
|
+
assert.ok(result.message.includes('warning'));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('passes when currentUser does not match any contributor', () => {
|
|
193
|
+
const result = checkFourEyes(
|
|
194
|
+
['Adrian King <a@x.com>'],
|
|
195
|
+
'Other User <o@x.com>',
|
|
196
|
+
'warn'
|
|
197
|
+
);
|
|
198
|
+
assert.equal(result.passed, true);
|
|
199
|
+
assert.equal(result.matchedUser, null);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('passes with empty contributors', () => {
|
|
203
|
+
const result = checkFourEyes([], 'User', 'warn');
|
|
204
|
+
assert.equal(result.passed, true);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('enforce mode (REQ-08)', () => {
|
|
209
|
+
it('fails when currentUser matches a contributor', () => {
|
|
210
|
+
const result = checkFourEyes(
|
|
211
|
+
['Adrian King <a@x.com>'],
|
|
212
|
+
'Adrian King <a@x.com>',
|
|
213
|
+
'enforce'
|
|
214
|
+
);
|
|
215
|
+
assert.equal(result.passed, false);
|
|
216
|
+
assert.equal(result.mode, 'enforce');
|
|
217
|
+
assert.equal(result.matchedUser, 'Adrian King <a@x.com>');
|
|
218
|
+
assert.ok(result.message.includes('violation'));
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('passes when currentUser does not match any contributor', () => {
|
|
222
|
+
const result = checkFourEyes(
|
|
223
|
+
['Adrian King <a@x.com>'],
|
|
224
|
+
'Other User <o@x.com>',
|
|
225
|
+
'enforce'
|
|
226
|
+
);
|
|
227
|
+
assert.equal(result.passed, true);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('normalized comparison (CTR-04)', () => {
|
|
232
|
+
it('matches "Name" against "Name <email>" (same person, different format)', () => {
|
|
233
|
+
const result = checkFourEyes(
|
|
234
|
+
['Adrian King'],
|
|
235
|
+
'Adrian King <a@x.com>',
|
|
236
|
+
'warn'
|
|
237
|
+
);
|
|
238
|
+
assert.equal(result.passed, false);
|
|
239
|
+
assert.ok(result.matchedUser);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('matches case-insensitively', () => {
|
|
243
|
+
const result = checkFourEyes(
|
|
244
|
+
['adrian king <a@x.com>'],
|
|
245
|
+
'Adrian King <a@x.com>',
|
|
246
|
+
'warn'
|
|
247
|
+
);
|
|
248
|
+
assert.equal(result.passed, false);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('empty contributors (CTR-05)', () => {
|
|
253
|
+
it('passes in warn mode with empty array', () => {
|
|
254
|
+
const result = checkFourEyes([], 'User', 'warn');
|
|
255
|
+
assert.equal(result.passed, true);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('passes in enforce mode with empty array', () => {
|
|
259
|
+
const result = checkFourEyes([], 'User', 'enforce');
|
|
260
|
+
assert.equal(result.passed, true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('passes in enforce mode with null contributors', () => {
|
|
264
|
+
const result = checkFourEyes(null, 'User', 'enforce');
|
|
265
|
+
assert.equal(result.passed, true);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('invalid mode', () => {
|
|
270
|
+
it('treats invalid mode as off', () => {
|
|
271
|
+
const result = checkFourEyes(
|
|
272
|
+
['User <u@x.com>'],
|
|
273
|
+
'User <u@x.com>',
|
|
274
|
+
'strict'
|
|
275
|
+
);
|
|
276
|
+
assert.equal(result.passed, true);
|
|
277
|
+
assert.equal(result.mode, 'off');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('treats boolean true as off', () => {
|
|
281
|
+
const result = checkFourEyes(
|
|
282
|
+
['User <u@x.com>'],
|
|
283
|
+
'User <u@x.com>',
|
|
284
|
+
true
|
|
285
|
+
);
|
|
286
|
+
assert.equal(result.passed, true);
|
|
287
|
+
assert.equal(result.mode, 'off');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('treats undefined mode as off', () => {
|
|
291
|
+
const result = checkFourEyes(
|
|
292
|
+
['User <u@x.com>'],
|
|
293
|
+
'User <u@x.com>',
|
|
294
|
+
undefined
|
|
295
|
+
);
|
|
296
|
+
assert.equal(result.passed, true);
|
|
297
|
+
assert.equal(result.mode, 'off');
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('multiple contributors', () => {
|
|
302
|
+
it('detects match among multiple contributors', () => {
|
|
303
|
+
const result = checkFourEyes(
|
|
304
|
+
['Alice <a@x.com>', 'Bob <b@x.com>', 'Charlie <c@x.com>'],
|
|
305
|
+
'Bob <b@x.com>',
|
|
306
|
+
'enforce'
|
|
307
|
+
);
|
|
308
|
+
assert.equal(result.passed, false);
|
|
309
|
+
assert.equal(result.matchedUser, 'Bob <b@x.com>');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('passes when current user not among multiple contributors', () => {
|
|
313
|
+
const result = checkFourEyes(
|
|
314
|
+
['Alice <a@x.com>', 'Bob <b@x.com>'],
|
|
315
|
+
'Charlie <c@x.com>',
|
|
316
|
+
'enforce'
|
|
317
|
+
);
|
|
318
|
+
assert.equal(result.passed, true);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('includes all contributors in result', () => {
|
|
322
|
+
const contribs = ['Alice <a@x.com>', 'Bob <b@x.com>'];
|
|
323
|
+
const result = checkFourEyes(contribs, 'Alice <a@x.com>', 'warn');
|
|
324
|
+
assert.deepEqual(result.contributors, contribs);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe('no-throw guarantee', () => {
|
|
329
|
+
it('does not throw with null currentUser', () => {
|
|
330
|
+
const result = checkFourEyes(['User'], null, 'warn');
|
|
331
|
+
assert.equal(result.passed, true);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('does not throw with empty currentUser', () => {
|
|
335
|
+
const result = checkFourEyes(['User'], '', 'warn');
|
|
336
|
+
assert.equal(result.passed, true);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RED test scaffold for REL-11 (Phase 156 plan 04).
|
|
3
|
+
*
|
|
4
|
+
* The cmdValidateHealth extension is implemented in plan 04; running
|
|
5
|
+
* this file before plan 04 lands MUST produce 6 failed tests with
|
|
6
|
+
* 'not yet implemented — REL-11' style messages.
|
|
7
|
+
*
|
|
8
|
+
* Behaviour under test:
|
|
9
|
+
* - untracked PLAN.md detection: warning entry references the file
|
|
10
|
+
* - untracked CONTEXT.md, RESEARCH.md, UAT.md, VERIFICATION.md: a
|
|
11
|
+
* warning entry that lists all four file paths
|
|
12
|
+
* - clean phase dir: zero warnings about untracked-phase-artifacts
|
|
13
|
+
* - multiple phase dirs: walks all phases under projects/<project>/phases/
|
|
14
|
+
* - non-artifact files (README.md, .DS_Store): not reported
|
|
15
|
+
* - fix message references a remediation command (dgs-tools commit
|
|
16
|
+
* OR /dgs:health --repair)
|
|
17
|
+
*
|
|
18
|
+
* Conventions:
|
|
19
|
+
* - Uses node:test runner + node:assert (matches state-transition-gate.test.cjs)
|
|
20
|
+
* - Each test creates and tears down its own temp planning root via os.tmpdir()
|
|
21
|
+
* - Until plan 04 lands, every test fails with 'not yet implemented'
|
|
22
|
+
* so the file is RED in a controlled way (no parse errors).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const test = require('node:test');
|
|
26
|
+
const assert = require('node:assert');
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
const { execSync } = require('child_process');
|
|
31
|
+
|
|
32
|
+
const NOT_IMPL = 'untracked-phase-artifacts check not yet implemented — REL-11';
|
|
33
|
+
|
|
34
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function tryRequireVerify() {
|
|
37
|
+
try {
|
|
38
|
+
return require('./verify.cjs');
|
|
39
|
+
} catch (err) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makePlanningRoot() {
|
|
45
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'health-rel11-test-'));
|
|
46
|
+
execSync('git init --quiet', { cwd: dir });
|
|
47
|
+
execSync('git config user.email test@example.com', { cwd: dir });
|
|
48
|
+
execSync('git config user.name "Test User"', { cwd: dir });
|
|
49
|
+
// Minimum required scaffolding so existing checks don't error out
|
|
50
|
+
fs.mkdirSync(path.join(dir, 'phases'), { recursive: true });
|
|
51
|
+
fs.writeFileSync(path.join(dir, 'PROJECT.md'), '# project\n## What This Is\n## Core Value\n## Requirements\n');
|
|
52
|
+
fs.writeFileSync(path.join(dir, 'ROADMAP.md'), '# roadmap\n');
|
|
53
|
+
fs.writeFileSync(path.join(dir, 'STATE.md'), '# state\n');
|
|
54
|
+
fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify({ commit_docs: true }, null, 2));
|
|
55
|
+
// Initial commit so HEAD exists
|
|
56
|
+
execSync('git add -A', { cwd: dir });
|
|
57
|
+
execSync('git commit --quiet -m "seed"', { cwd: dir });
|
|
58
|
+
return dir;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function cleanupRoot(dir) {
|
|
62
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function writeAndOptionallyTrack(root, rel, content, track) {
|
|
66
|
+
const abs = path.join(root, rel);
|
|
67
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
68
|
+
fs.writeFileSync(abs, content);
|
|
69
|
+
if (track) {
|
|
70
|
+
execSync(`git add "${rel}"`, { cwd: root });
|
|
71
|
+
execSync(`git commit --quiet -m "track ${rel}"`, { cwd: root });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function callValidateHealth(root) {
|
|
76
|
+
const verify = tryRequireVerify();
|
|
77
|
+
if (!verify || typeof verify.cmdValidateHealth !== 'function') return null;
|
|
78
|
+
// Capture the result by stubbing output() through raw mode. Since
|
|
79
|
+
// cmdValidateHealth calls output() which calls process.exit, we run
|
|
80
|
+
// it in a child process and capture stdout JSON.
|
|
81
|
+
//
|
|
82
|
+
// We write the shim to a tmp file rather than passing via -e, because
|
|
83
|
+
// shell-escaping a path that may contain spaces or quotes plus the
|
|
84
|
+
// raw arg is fragile. A tmp .cjs file is unambiguous.
|
|
85
|
+
const verifyPath = path.resolve(__dirname, 'verify.cjs');
|
|
86
|
+
const shim = [
|
|
87
|
+
`const v = require(${JSON.stringify(verifyPath)});`,
|
|
88
|
+
`v.cmdValidateHealth(${JSON.stringify(root)}, { raw: true }, true);`,
|
|
89
|
+
].join('\n');
|
|
90
|
+
const shimPath = path.join(os.tmpdir(), `rel11-shim-${process.pid}-${Date.now()}.cjs`);
|
|
91
|
+
fs.writeFileSync(shimPath, shim);
|
|
92
|
+
try {
|
|
93
|
+
const stdout = execSync(`node ${JSON.stringify(shimPath)}`, { encoding: 'utf-8' });
|
|
94
|
+
try { return JSON.parse(stdout); } catch { return { stdout }; }
|
|
95
|
+
} catch (err) {
|
|
96
|
+
return { error: err.message, stdout: err.stdout && err.stdout.toString() };
|
|
97
|
+
} finally {
|
|
98
|
+
try { fs.unlinkSync(shimPath); } catch { /* ignore */ }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Test 1: untracked PLAN.md detection ──────────────────────────────────
|
|
103
|
+
|
|
104
|
+
test('REL-11 untracked PLAN.md detection: returns untracked-phase-artifacts entry listing the file', () => {
|
|
105
|
+
const verify = tryRequireVerify();
|
|
106
|
+
if (!verify || typeof verify.cmdValidateHealth !== 'function') {
|
|
107
|
+
assert.fail(NOT_IMPL);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const root = makePlanningRoot();
|
|
111
|
+
try {
|
|
112
|
+
// tracked CONTEXT.md, untracked PLAN.md
|
|
113
|
+
writeAndOptionallyTrack(root, 'phases/100-foo/100-CONTEXT.md', '# ctx\n', true);
|
|
114
|
+
writeAndOptionallyTrack(root, 'phases/100-foo/100-01-PLAN.md', '# plan\n', false);
|
|
115
|
+
const result = callValidateHealth(root);
|
|
116
|
+
if (!result || !Array.isArray(result.warnings)) {
|
|
117
|
+
assert.fail(NOT_IMPL);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const hit = result.warnings.find(w =>
|
|
121
|
+
(w.message || '').includes('untracked-phase-artifacts') &&
|
|
122
|
+
(w.message || '').includes('100-01-PLAN.md')
|
|
123
|
+
);
|
|
124
|
+
assert.ok(hit, 'expected an untracked-phase-artifacts warning referencing 100-01-PLAN.md');
|
|
125
|
+
} finally {
|
|
126
|
+
cleanupRoot(root);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ─── Test 2: untracked CONTEXT/RESEARCH/UAT/VERIFICATION ─────────────────
|
|
131
|
+
|
|
132
|
+
test('REL-11 untracked CONTEXT.md, RESEARCH.md, UAT.md, VERIFICATION.md detection (one of each)', () => {
|
|
133
|
+
const verify = tryRequireVerify();
|
|
134
|
+
if (!verify || typeof verify.cmdValidateHealth !== 'function') {
|
|
135
|
+
assert.fail(NOT_IMPL);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const root = makePlanningRoot();
|
|
139
|
+
try {
|
|
140
|
+
// All four artifacts untracked
|
|
141
|
+
writeAndOptionallyTrack(root, 'phases/100-foo/100-CONTEXT.md', 'a\n', false);
|
|
142
|
+
writeAndOptionallyTrack(root, 'phases/100-foo/100-RESEARCH.md', 'b\n', false);
|
|
143
|
+
writeAndOptionallyTrack(root, 'phases/100-foo/100-UAT.md', 'c\n', false);
|
|
144
|
+
writeAndOptionallyTrack(root, 'phases/100-foo/100-VERIFICATION.md', 'd\n', false);
|
|
145
|
+
const result = callValidateHealth(root);
|
|
146
|
+
if (!result || !Array.isArray(result.warnings)) {
|
|
147
|
+
assert.fail(NOT_IMPL);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const hit = result.warnings.find(w => (w.message || '').includes('untracked-phase-artifacts'));
|
|
151
|
+
assert.ok(hit, 'expected an untracked-phase-artifacts warning');
|
|
152
|
+
for (const f of ['100-CONTEXT.md', '100-RESEARCH.md', '100-UAT.md', '100-VERIFICATION.md']) {
|
|
153
|
+
assert.ok((hit.message || '').includes(f), `warning message must include ${f}`);
|
|
154
|
+
}
|
|
155
|
+
} finally {
|
|
156
|
+
cleanupRoot(root);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ─── Test 3: clean phase dir → no warning ─────────────────────────────────
|
|
161
|
+
|
|
162
|
+
test('REL-11 clean phase dir: all artifacts tracked yields no untracked-phase-artifacts entry', () => {
|
|
163
|
+
const verify = tryRequireVerify();
|
|
164
|
+
if (!verify || typeof verify.cmdValidateHealth !== 'function') {
|
|
165
|
+
assert.fail(NOT_IMPL);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const root = makePlanningRoot();
|
|
169
|
+
try {
|
|
170
|
+
writeAndOptionallyTrack(root, 'phases/100-foo/100-CONTEXT.md', 'a\n', true);
|
|
171
|
+
writeAndOptionallyTrack(root, 'phases/100-foo/100-01-PLAN.md', 'b\n', true);
|
|
172
|
+
writeAndOptionallyTrack(root, 'phases/100-foo/100-RESEARCH.md', 'c\n', true);
|
|
173
|
+
writeAndOptionallyTrack(root, 'phases/100-foo/100-UAT.md', 'd\n', true);
|
|
174
|
+
writeAndOptionallyTrack(root, 'phases/100-foo/100-VERIFICATION.md', 'e\n', true);
|
|
175
|
+
const result = callValidateHealth(root);
|
|
176
|
+
if (!result || !Array.isArray(result.warnings)) {
|
|
177
|
+
assert.fail(NOT_IMPL);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const hit = result.warnings.find(w => (w.message || '').includes('untracked-phase-artifacts'));
|
|
181
|
+
assert.strictEqual(hit, undefined, 'no untracked-phase-artifacts warning expected when all tracked');
|
|
182
|
+
} finally {
|
|
183
|
+
cleanupRoot(root);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ─── Test 4: multiple phase dirs ──────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
test('REL-11 multiple phase dirs: walks all phases under projects/<current>/phases/', () => {
|
|
190
|
+
const verify = tryRequireVerify();
|
|
191
|
+
if (!verify || typeof verify.cmdValidateHealth !== 'function') {
|
|
192
|
+
assert.fail(NOT_IMPL);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const root = makePlanningRoot();
|
|
196
|
+
try {
|
|
197
|
+
writeAndOptionallyTrack(root, 'phases/100-foo/100-01-PLAN.md', 'a\n', false);
|
|
198
|
+
writeAndOptionallyTrack(root, 'phases/101-bar/101-CONTEXT.md', 'b\n', false);
|
|
199
|
+
writeAndOptionallyTrack(root, 'phases/102-baz/102-VERIFICATION.md', 'c\n', false);
|
|
200
|
+
const result = callValidateHealth(root);
|
|
201
|
+
if (!result || !Array.isArray(result.warnings)) {
|
|
202
|
+
assert.fail(NOT_IMPL);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const hit = result.warnings.find(w => (w.message || '').includes('untracked-phase-artifacts'));
|
|
206
|
+
assert.ok(hit, 'expected an untracked-phase-artifacts warning');
|
|
207
|
+
assert.ok((hit.message || '').includes('100-01-PLAN.md'));
|
|
208
|
+
assert.ok((hit.message || '').includes('101-CONTEXT.md'));
|
|
209
|
+
assert.ok((hit.message || '').includes('102-VERIFICATION.md'));
|
|
210
|
+
} finally {
|
|
211
|
+
cleanupRoot(root);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ─── Test 5: non-artifact files not reported ──────────────────────────────
|
|
216
|
+
|
|
217
|
+
test('REL-11 non-artifact files (e.g., a .DS_Store or some-other.md inside phases/100/) are NOT reported', () => {
|
|
218
|
+
const verify = tryRequireVerify();
|
|
219
|
+
if (!verify || typeof verify.cmdValidateHealth !== 'function') {
|
|
220
|
+
assert.fail(NOT_IMPL);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const root = makePlanningRoot();
|
|
224
|
+
try {
|
|
225
|
+
writeAndOptionallyTrack(root, 'phases/100-foo/100-CONTEXT.md', 'a\n', true);
|
|
226
|
+
writeAndOptionallyTrack(root, 'phases/100-foo/README.md', 'r\n', false);
|
|
227
|
+
writeAndOptionallyTrack(root, 'phases/100-foo/.DS_Store', 'x\n', false);
|
|
228
|
+
const result = callValidateHealth(root);
|
|
229
|
+
if (!result || !Array.isArray(result.warnings)) {
|
|
230
|
+
assert.fail(NOT_IMPL);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const hit = result.warnings.find(w => (w.message || '').includes('untracked-phase-artifacts'));
|
|
234
|
+
if (hit) {
|
|
235
|
+
assert.ok(!(hit.message || '').includes('README.md'), 'README.md must NOT be reported');
|
|
236
|
+
assert.ok(!(hit.message || '').includes('.DS_Store'), '.DS_Store must NOT be reported');
|
|
237
|
+
}
|
|
238
|
+
// If no untracked-phase-artifacts warning at all, that's also acceptable
|
|
239
|
+
// (only PLAN/CONTEXT/RESEARCH/UAT/VERIFICATION are checked).
|
|
240
|
+
} finally {
|
|
241
|
+
cleanupRoot(root);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ─── Test 6: fix message references a remediation command ────────────────
|
|
246
|
+
|
|
247
|
+
test('REL-11 fix message includes a remediation command', () => {
|
|
248
|
+
const verify = tryRequireVerify();
|
|
249
|
+
if (!verify || typeof verify.cmdValidateHealth !== 'function') {
|
|
250
|
+
assert.fail(NOT_IMPL);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const root = makePlanningRoot();
|
|
254
|
+
try {
|
|
255
|
+
writeAndOptionallyTrack(root, 'phases/100-foo/100-01-PLAN.md', 'a\n', false);
|
|
256
|
+
const result = callValidateHealth(root);
|
|
257
|
+
if (!result || !Array.isArray(result.warnings)) {
|
|
258
|
+
assert.fail(NOT_IMPL);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const hit = result.warnings.find(w => (w.message || '').includes('untracked-phase-artifacts'));
|
|
262
|
+
assert.ok(hit, 'expected an untracked-phase-artifacts warning');
|
|
263
|
+
const fix = hit.fix || '';
|
|
264
|
+
const refs = fix.includes('dgs-tools commit') || fix.includes('/dgs:health --repair');
|
|
265
|
+
assert.ok(refs, `fix message should reference 'dgs-tools commit' OR '/dgs:health --repair' (got: ${fix})`);
|
|
266
|
+
} finally {
|
|
267
|
+
cleanupRoot(root);
|
|
268
|
+
}
|
|
269
|
+
});
|