@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
@@ -24,6 +24,8 @@ const {
24
24
  loadConfig,
25
25
  isProjectCompleted,
26
26
  getProjectDir,
27
+ resolveModelInternal,
28
+ MODEL_PROFILES,
27
29
  } = require('./core.cjs');
28
30
 
29
31
  // ─── Root layout (no v2 markers) Tests ───────────────────────────────────────
@@ -388,7 +390,9 @@ describe('getProjectFolders', () => {
388
390
  const fixture = createFixture({
389
391
  'config.json': JSON.stringify({}),
390
392
  'PROJECTS.md': '# Projects\n',
393
+ 'projects/auth-overhaul/PROJECT.md': '# Project\n',
391
394
  'projects/auth-overhaul/STATE.md': '# State',
395
+ 'projects/dashboard-v2/PROJECT.md': '# Project\n',
392
396
  'projects/dashboard-v2/STATE.md': '# State',
393
397
  'phases/': null,
394
398
  'codebase/': null,
@@ -404,10 +408,11 @@ describe('getProjectFolders', () => {
404
408
  }
405
409
  });
406
410
 
407
- it('only includes directories containing STATE.md', () => {
411
+ it('only includes directories containing PROJECT.md', () => {
408
412
  const fixture = createFixture({
409
413
  'config.json': JSON.stringify({}),
410
414
  'PROJECTS.md': '# Projects\n',
415
+ 'projects/valid-project/PROJECT.md': '# Project\n',
411
416
  'projects/valid-project/STATE.md': '# State',
412
417
  'projects/empty-folder/': null,
413
418
  });
@@ -420,10 +425,43 @@ describe('getProjectFolders', () => {
420
425
  }
421
426
  });
422
427
 
428
+ it('returns thin-skeleton project folder containing PROJECT.md but no STATE.md', () => {
429
+ const fixture = createFixture({
430
+ 'config.json': JSON.stringify({}),
431
+ 'PROJECTS.md': '# Projects\n',
432
+ 'projects/word-gen/PROJECT.md': '# Project: Word Gen\n',
433
+ });
434
+
435
+ try {
436
+ const result = getProjectFolders(fixture.cwd);
437
+ assert.deepEqual(result, ['word-gen']);
438
+ } finally {
439
+ fixture.cleanup();
440
+ }
441
+ });
442
+
443
+ it('excludes directory with neither PROJECT.md nor STATE.md', () => {
444
+ const fixture = createFixture({
445
+ 'config.json': JSON.stringify({}),
446
+ 'PROJECTS.md': '# Projects\n',
447
+ 'projects/empty-folder/': null,
448
+ 'projects/real-project/PROJECT.md': '# Project\n',
449
+ });
450
+
451
+ try {
452
+ const result = getProjectFolders(fixture.cwd);
453
+ assert.deepEqual(result, ['real-project']);
454
+ } finally {
455
+ fixture.cleanup();
456
+ }
457
+ });
458
+
423
459
  it('excludes dot-directories', () => {
424
460
  const fixture = createFixture({
425
461
  'config.json': JSON.stringify({}),
462
+ 'projects/.hidden/PROJECT.md': '# Project\n',
426
463
  'projects/.hidden/STATE.md': '# State',
464
+ 'projects/real-project/PROJECT.md': '# Project\n',
427
465
  'projects/real-project/STATE.md': '# State',
428
466
  });
429
467
 
@@ -745,6 +783,7 @@ describe('root layout', () => {
745
783
  'config.json': JSON.stringify({}),
746
784
  'PROJECTS.md': '# Projects\n',
747
785
  'REPOS.md': '# Repos\n',
786
+ 'projects/proj-a/PROJECT.md': '# Project\n',
748
787
  'projects/proj-a/STATE.md': '# State\n',
749
788
  'projects/proj-a/phases/': null,
750
789
  });
@@ -837,3 +876,42 @@ describe('config two-file merge', () => {
837
876
  });
838
877
 
839
878
  });
879
+
880
+ // ─── MODEL_PROFILES dgs-idea-researcher tiering ──────────────────────────────
881
+
882
+ describe('MODEL_PROFILES dgs-idea-researcher tiering', () => {
883
+ let fixture;
884
+
885
+ afterEach(() => {
886
+ if (fixture) fixture.cleanup();
887
+ fixture = null;
888
+ });
889
+
890
+ it('MODEL_PROFILES table contains dgs-idea-researcher with quality/balanced/budget tiers', () => {
891
+ const profile = MODEL_PROFILES['dgs-idea-researcher'];
892
+ assert.ok(profile, 'MODEL_PROFILES[\'dgs-idea-researcher\'] should be defined');
893
+ assert.equal(typeof profile.quality, 'string', 'quality tier present');
894
+ assert.equal(typeof profile.balanced, 'string', 'balanced tier present');
895
+ assert.equal(typeof profile.budget, 'string', 'budget tier present');
896
+ });
897
+
898
+ it('resolveModelInternal returns inherit for dgs-idea-researcher on quality profile', () => {
899
+ fixture = createTempProject({ withConfig: { model_profile: 'quality' } });
900
+ const resolved = resolveModelInternal(fixture.cwd, 'dgs-idea-researcher');
901
+ // Per core.cjs: when the resolved tier is 'opus', the function returns 'inherit'.
902
+ assert.equal(resolved, 'inherit');
903
+ });
904
+
905
+ it('resolveModelInternal returns sonnet for dgs-idea-researcher on balanced profile', () => {
906
+ fixture = createTempProject({ withConfig: { model_profile: 'balanced' } });
907
+ const resolved = resolveModelInternal(fixture.cwd, 'dgs-idea-researcher');
908
+ assert.equal(resolved, 'sonnet');
909
+ });
910
+
911
+ it('resolveModelInternal returns haiku for dgs-idea-researcher on budget profile', () => {
912
+ fixture = createTempProject({ withConfig: { model_profile: 'budget' } });
913
+ const resolved = resolveModelInternal(fixture.cwd, 'dgs-idea-researcher');
914
+ assert.equal(resolved, 'haiku');
915
+ });
916
+
917
+ });
@@ -0,0 +1,199 @@
1
+ // deliver-great-systems/bin/lib/fast-routing.cjs
2
+ //
3
+ // REL-05/06: Fast-mode commit routing helpers for /dgs:fast.
4
+ //
5
+ // Exports:
6
+ // surveyDirty(planningRoot) -> { planningRoot: { dirty, paths }, subRepos: [{ name, path, dirty, paths }], submodules: [{ path, dirty }] }
7
+ // decideRouting(survey) -> { action: 'route-to-planning-root'|'route-to-sub-repo'|'fail', repoCwd?, exitCode?, warnings?: [string], message?: string }
8
+ // enumerateSubmodules(repoPath) -> [{ path, dirty }]
9
+ //
10
+ // REL-05: decideRouting routes commits to the correct repo on multi-repo products.
11
+ // REL-06: surveyDirty + a thin pre-edit caller (in dgs-tools dispatcher) implements pre-edit dirt check.
12
+ //
13
+ // Both helpers honour the fail-loudly contract: surveyDirty returns structured data;
14
+ // decideRouting names the exit-code label (`multi-repo-dirt`) and includes a 1-3 line
15
+ // remediation message suitable for direct display.
16
+
17
+ const fs = require('node:fs');
18
+ const path = require('node:path');
19
+ const { execGit } = require('./core.cjs');
20
+
21
+ // Inline REPOS.md parser — does NOT use repos.cjs's parseReposMd because that
22
+ // helper goes through getPlanningRoot() which has a per-process cache that
23
+ // breaks tests using multiple temp planning roots in the same process.
24
+ function parseReposMdLocal(planningRoot) {
25
+ const filePath = path.join(planningRoot, 'REPOS.md');
26
+ let content;
27
+ try {
28
+ content = fs.readFileSync(filePath, 'utf-8');
29
+ } catch {
30
+ return { repos: [] };
31
+ }
32
+ if (!content || !content.startsWith('# Repos')) return { repos: [] };
33
+ const lines = content.split('\n');
34
+ let tableStart = -1;
35
+ for (let i = 0; i < lines.length; i++) {
36
+ if (lines[i].startsWith('|') && lines[i].includes('Name')) { tableStart = i; break; }
37
+ }
38
+ if (tableStart === -1) return { repos: [] };
39
+ const repos = [];
40
+ for (let i = tableStart + 2; i < lines.length; i++) {
41
+ const line = lines[i].trim();
42
+ if (!line.startsWith('|')) break;
43
+ const cells = line.split('|').map(c => c.trim()).filter((_, idx, arr) => idx > 0 && idx < arr.length - 1);
44
+ if (cells.length < 2) continue;
45
+ const [name, p] = cells;
46
+ if (!name || !p) continue;
47
+ repos.push({ name, path: p });
48
+ }
49
+ return { repos };
50
+ }
51
+
52
+ function readConfigLocalJson(planningRoot) {
53
+ const cfgPath = path.join(planningRoot, 'config.local.json');
54
+ if (!fs.existsSync(cfgPath)) return {};
55
+ try {
56
+ return JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
57
+ } catch {
58
+ return {};
59
+ }
60
+ }
61
+
62
+ function enumerateRegisteredRepos(planningRoot) {
63
+ const seen = new Map(); // absolute path -> { name, path }
64
+
65
+ // Source 1: REPOS.md (inline parser, bypassing repos.cjs cache)
66
+ const reposMd = parseReposMdLocal(planningRoot);
67
+ for (const r of (reposMd.repos || [])) {
68
+ const abs = path.resolve(planningRoot, r.path);
69
+ if (!seen.has(abs)) seen.set(abs, { name: r.name, path: abs });
70
+ }
71
+
72
+ // Source 2: config.local.json projects.*.worktrees.*.repos (map of name -> absolute path)
73
+ const cfg = readConfigLocalJson(planningRoot);
74
+ const projects = (cfg.projects || {});
75
+ for (const proj of Object.values(projects)) {
76
+ const wts = (proj.worktrees || {});
77
+ for (const wt of Object.values(wts)) {
78
+ const repos = (wt.repos || {});
79
+ for (const [name, abs] of Object.entries(repos)) {
80
+ if (typeof abs !== 'string') continue;
81
+ const resolved = path.resolve(abs);
82
+ if (!seen.has(resolved)) seen.set(resolved, { name, path: resolved });
83
+ }
84
+ }
85
+ }
86
+
87
+ return Array.from(seen.values());
88
+ }
89
+
90
+ function isGitRepo(p) {
91
+ try {
92
+ const res = execGit(p, ['rev-parse', '--is-inside-work-tree']);
93
+ if (!res || res.exitCode !== 0) return false;
94
+ return typeof res.stdout === 'string' && res.stdout.trim() === 'true';
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ function porcelainPaths(repoPath) {
101
+ if (!isGitRepo(repoPath)) return null;
102
+ let out = '';
103
+ try {
104
+ const res = execGit(repoPath, ['status', '--porcelain']);
105
+ out = (res && res.stdout) || '';
106
+ } catch {
107
+ out = '';
108
+ }
109
+ // git status --porcelain format: "XY <path>" where XY is a 2-char status code
110
+ // Strip the 2-char status + 1 space; trim and filter empties.
111
+ return out
112
+ .split('\n')
113
+ .map(l => l.replace(/^.{2,3}/, '').trim())
114
+ .filter(Boolean);
115
+ }
116
+
117
+ function enumerateSubmodules(repoPath) {
118
+ if (!isGitRepo(repoPath)) return [];
119
+ let out = '';
120
+ try {
121
+ const res = execGit(repoPath, ['submodule', 'status']);
122
+ out = (res && res.stdout) || '';
123
+ } catch {
124
+ out = '';
125
+ }
126
+ return out
127
+ .split('\n')
128
+ .filter(Boolean)
129
+ .map(line => {
130
+ // Format: ` <sha> <path> (<ref>)` — leading char `+` (mismatch),
131
+ // `-` (uninitialised), ` ` (clean), `U` (conflict)
132
+ const lead = line[0];
133
+ const match = line.trim().match(/^[+\- U]?\w+\s+(\S+)/);
134
+ return {
135
+ path: match ? match[1] : line.trim(),
136
+ dirty: Boolean(lead && lead !== ' '),
137
+ };
138
+ });
139
+ }
140
+
141
+ function surveyDirty(planningRoot) {
142
+ const planningPaths = porcelainPaths(planningRoot) || [];
143
+ const planningRootEntry = { dirty: planningPaths.length > 0, paths: planningPaths };
144
+
145
+ const subRepos = [];
146
+ for (const repo of enumerateRegisteredRepos(planningRoot)) {
147
+ if (path.resolve(repo.path) === path.resolve(planningRoot)) continue; // skip if same dir
148
+ const paths = porcelainPaths(repo.path) || [];
149
+ subRepos.push({ name: repo.name, path: repo.path, dirty: paths.length > 0, paths });
150
+ }
151
+
152
+ const submodules = enumerateSubmodules(planningRoot);
153
+
154
+ return { planningRoot: planningRootEntry, subRepos, submodules };
155
+ }
156
+
157
+ function decideRouting(survey) {
158
+ const warnings = [];
159
+ const dirtySubmodules = (survey.submodules || []).filter(s => s.dirty);
160
+ for (const sm of dirtySubmodules) {
161
+ warnings.push(
162
+ `submodule changes detected at ${sm.path}; auto-routing not supported for submodules — commit manually or use /dgs:quick`
163
+ );
164
+ }
165
+
166
+ const planningRoot = survey.planningRoot || { dirty: false };
167
+ const subRepos = survey.subRepos || [];
168
+ const dirtySubRepos = subRepos.filter(r => r.dirty);
169
+
170
+ // Case (c): multiple sub-repos dirty OR planning + at least one sub-repo dirty
171
+ if (dirtySubRepos.length > 1 || (dirtySubRepos.length >= 1 && planningRoot.dirty)) {
172
+ return {
173
+ action: 'fail',
174
+ exitCode: 'multi-repo-dirt',
175
+ message:
176
+ 'Multiple repos have dirty changes — fast-mode cannot auto-route. Review state with `git status` in each repo, run `/dgs:fast --repo <name>` per repo, or use `/dgs:quick` for multi-repo work.',
177
+ warnings,
178
+ };
179
+ }
180
+
181
+ // Case (a): exactly one sub-repo dirty + planning clean
182
+ if (dirtySubRepos.length === 1 && !planningRoot.dirty) {
183
+ return {
184
+ action: 'route-to-sub-repo',
185
+ repoCwd: dirtySubRepos[0].path,
186
+ warnings,
187
+ };
188
+ }
189
+
190
+ // Case (b): only planning root dirty (or nothing dirty — same code path)
191
+ return { action: 'route-to-planning-root', warnings };
192
+ }
193
+
194
+ module.exports = {
195
+ surveyDirty,
196
+ decideRouting,
197
+ enumerateSubmodules,
198
+ enumerateRegisteredRepos,
199
+ };
@@ -0,0 +1,108 @@
1
+ // deliver-great-systems/bin/lib/fast-routing.test.cjs
2
+ // REL-05/06 regression test scaffold — initially RED. Turns GREEN after plan 03
3
+ // implements bin/lib/fast-routing.cjs with surveyDirty + decideRouting helpers.
4
+
5
+ const test = require('node:test');
6
+ const assert = require('node:assert');
7
+ const fs = require('node:fs');
8
+ const path = require('node:path');
9
+ const os = require('node:os');
10
+ const { execSync } = require('node:child_process');
11
+
12
+ function getModule() {
13
+ try {
14
+ return require('./fast-routing.cjs');
15
+ } catch (err) {
16
+ assert.fail('REL-05/06 module not yet implemented: bin/lib/fast-routing.cjs (plan 03 task)');
17
+ }
18
+ }
19
+
20
+ function setupMultiRepoFixture() {
21
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fast-routing-'));
22
+ execSync('git init -q', { cwd: root });
23
+ execSync('git config user.email test@test', { cwd: root });
24
+ execSync('git config user.name test', { cwd: root });
25
+ const subRepo = path.join(root, '..', `fast-routing-sub-${path.basename(root)}`);
26
+ fs.mkdirSync(subRepo);
27
+ execSync('git init -q', { cwd: subRepo });
28
+ execSync('git config user.email test@test', { cwd: subRepo });
29
+ execSync('git config user.name test', { cwd: subRepo });
30
+ fs.writeFileSync(
31
+ path.join(root, 'REPOS.md'),
32
+ `# Repos\n\n| Name | Path | GitHub URL | Description |\n|------|------|------------|-------------|\n| sub | ${subRepo} | git@x:y.git | test sub-repo |\n`
33
+ );
34
+ // Commit something so the planning-root tree isn't entirely empty
35
+ execSync('git add REPOS.md', { cwd: root });
36
+ execSync('git commit -q -m init', { cwd: root });
37
+ return { root, subRepo };
38
+ }
39
+
40
+ test('REL-06: surveyDirty detects pre-existing dirt in planning root (pre-existing-dirt)', () => {
41
+ const m = getModule();
42
+ const { root } = setupMultiRepoFixture();
43
+ fs.writeFileSync(path.join(root, 'dirty.txt'), 'pre-existing');
44
+ const survey = m.surveyDirty(root);
45
+ assert.ok(survey.planningRoot.dirty, 'planning root should be flagged dirty');
46
+ assert.ok((survey.planningRoot.paths || []).some(p => /dirty\.txt/.test(p)), 'dirty.txt must appear in paths');
47
+ });
48
+
49
+ test('REL-06: surveyDirty detects pre-existing dirt in registered sub-repo', () => {
50
+ const m = getModule();
51
+ const { root, subRepo } = setupMultiRepoFixture();
52
+ fs.writeFileSync(path.join(subRepo, 'dirty.txt'), 'pre-existing');
53
+ const survey = m.surveyDirty(root);
54
+ const subEntry = (survey.subRepos || []).find(r => r.name === 'sub');
55
+ assert.ok(subEntry && subEntry.dirty, 'sub-repo should be flagged dirty');
56
+ });
57
+
58
+ test('REL-05: decideRouting returns route-to-sub-repo for single-sub-repo dirty case', () => {
59
+ const m = getModule();
60
+ const survey = {
61
+ planningRoot: { dirty: false, paths: [] },
62
+ subRepos: [{ name: 'sub', path: '/tmp/sub', dirty: true, paths: ['file.txt'] }],
63
+ submodules: [],
64
+ };
65
+ const decision = m.decideRouting(survey);
66
+ assert.strictEqual(decision.action, 'route-to-sub-repo');
67
+ assert.strictEqual(decision.repoCwd, '/tmp/sub');
68
+ });
69
+
70
+ test('REL-05: decideRouting returns route-to-planning-root for planning-only dirty case', () => {
71
+ const m = getModule();
72
+ const survey = {
73
+ planningRoot: { dirty: true, paths: ['a.md'] },
74
+ subRepos: [{ name: 'sub', path: '/tmp/sub', dirty: false, paths: [] }],
75
+ submodules: [],
76
+ };
77
+ const decision = m.decideRouting(survey);
78
+ assert.strictEqual(decision.action, 'route-to-planning-root');
79
+ });
80
+
81
+ test('REL-05: decideRouting fails with multi-repo-dirt for multiple-dirty case', () => {
82
+ const m = getModule();
83
+ const survey = {
84
+ planningRoot: { dirty: true, paths: ['a.md'] },
85
+ subRepos: [{ name: 'sub', path: '/tmp/sub', dirty: true, paths: ['b.txt'] }],
86
+ submodules: [],
87
+ };
88
+ const decision = m.decideRouting(survey);
89
+ assert.strictEqual(decision.action, 'fail');
90
+ assert.strictEqual(decision.exitCode, 'multi-repo-dirt');
91
+ });
92
+
93
+ test('REL-05: decideRouting warns on submodules but does not auto-route them', () => {
94
+ const m = getModule();
95
+ const survey = {
96
+ planningRoot: { dirty: false, paths: [] },
97
+ subRepos: [{ name: 'sub', path: '/tmp/sub', dirty: true, paths: ['file.txt'] }],
98
+ submodules: [{ path: 'vendor/lib', dirty: true }],
99
+ };
100
+ const decision = m.decideRouting(survey);
101
+ assert.strictEqual(decision.action, 'route-to-sub-repo');
102
+ assert.ok(
103
+ Array.isArray(decision.warnings) && decision.warnings.some(w => /submodule/i.test(w)),
104
+ 'submodule warning must be present in decision.warnings'
105
+ );
106
+ });
107
+
108
+ // REL-05/06 sentinel — flag this file as a Wave-0 RED scaffold for plan 03.
@@ -0,0 +1,87 @@
1
+ // deliver-great-systems/bin/lib/final-commit-precondition.test.cjs
2
+ // REL-08 regression test scaffold — initially RED. Turns GREEN after plan 02
3
+ // adds the pre-commit precondition gate to executor's <final_commit> step.
4
+
5
+ const test = require('node:test');
6
+ const assert = require('node:assert');
7
+ const fs = require('node:fs');
8
+ const path = require('node:path');
9
+ const os = require('node:os');
10
+ const { execSync } = require('node:child_process');
11
+
12
+ // Resolve dgs-tools.cjs CLI: __dirname = .../deliver-great-systems/bin/lib
13
+ // CLI lives at .../deliver-great-systems/bin/dgs-tools.cjs
14
+ const CLI = path.resolve(__dirname, '..', 'dgs-tools.cjs');
15
+
16
+ function setupFixture({ planRequirements, summaryRequirementsCompleted }) {
17
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rel08-'));
18
+ execSync('git init -q', { cwd: tmpDir });
19
+ execSync('git config user.email test@test', { cwd: tmpDir });
20
+ execSync('git config user.name test', { cwd: tmpDir });
21
+ fs.writeFileSync(
22
+ path.join(tmpDir, 'PLAN.md'),
23
+ `---\nphase: test\nplan: 01\nrequirements:\n${planRequirements.map(r => ` - ${r}`).join('\n')}\n---\n`
24
+ );
25
+ fs.writeFileSync(
26
+ path.join(tmpDir, 'SUMMARY.md'),
27
+ `---\nphase: test\nplan: 01\nrequirements_completed: [${summaryRequirementsCompleted.join(', ')}]\n---\n`
28
+ );
29
+ return tmpDir;
30
+ }
31
+
32
+ test('REL-08: precondition aborts with summary-frontmatter-mismatch when PLAN non-empty AND SUMMARY empty', () => {
33
+ const fixture = setupFixture({ planRequirements: ['TEST-01'], summaryRequirementsCompleted: [] });
34
+
35
+ let exitCode = 0;
36
+ let combined = '';
37
+ try {
38
+ execSync(`node ${CLI} final-commit-precondition --plan ${fixture}/PLAN.md --summary ${fixture}/SUMMARY.md`, { stdio: 'pipe' });
39
+ } catch (err) {
40
+ exitCode = err.status;
41
+ combined = (err.stderr ? err.stderr.toString() : '') + (err.stdout ? err.stdout.toString() : '');
42
+ }
43
+
44
+ // RED: the CLI subcommand doesn't exist yet, so execSync throws with a different error
45
+ // (CLI says "Unknown command: final-commit-precondition" or similar — does NOT include the literal label).
46
+ assert.notStrictEqual(exitCode, 0, 'should exit non-zero on mismatch');
47
+ assert.match(combined, /summary-frontmatter-mismatch/, 'stderr must include the exit-code label');
48
+ });
49
+
50
+ test('REL-08: precondition leaves working tree unchanged on abort (REL-08 fail-loudly contract)', () => {
51
+ const fixture = setupFixture({ planRequirements: ['TEST-01'], summaryRequirementsCompleted: [] });
52
+ // Snapshot working-tree state BEFORE precondition runs
53
+ const before = execSync('git status --porcelain', { cwd: fixture }).toString();
54
+
55
+ try {
56
+ execSync(`node ${CLI} final-commit-precondition --plan ${fixture}/PLAN.md --summary ${fixture}/SUMMARY.md`, { stdio: 'pipe' });
57
+ } catch (_) { /* expected non-zero */ }
58
+
59
+ const after = execSync('git status --porcelain', { cwd: fixture }).toString();
60
+ assert.strictEqual(after, before, 'working tree must be unchanged after precondition abort');
61
+ });
62
+
63
+ test('REL-08: precondition is a no-op (exit 0) when PLAN requirements is empty', () => {
64
+ const fixture = setupFixture({ planRequirements: [], summaryRequirementsCompleted: [] });
65
+
66
+ let exitCode = 0;
67
+ try {
68
+ execSync(`node ${CLI} final-commit-precondition --plan ${fixture}/PLAN.md --summary ${fixture}/SUMMARY.md`, { stdio: 'pipe' });
69
+ } catch (err) {
70
+ exitCode = err.status;
71
+ }
72
+ assert.strictEqual(exitCode, 0, 'precondition is no-op when PLAN requirements is empty');
73
+ });
74
+
75
+ test('REL-08: precondition succeeds (exit 0) when SUMMARY requirements_completed matches PLAN', () => {
76
+ const fixture = setupFixture({ planRequirements: ['TEST-01'], summaryRequirementsCompleted: ['TEST-01'] });
77
+
78
+ let exitCode = 0;
79
+ try {
80
+ execSync(`node ${CLI} final-commit-precondition --plan ${fixture}/PLAN.md --summary ${fixture}/SUMMARY.md`, { stdio: 'pipe' });
81
+ } catch (err) {
82
+ exitCode = err.status;
83
+ }
84
+ assert.strictEqual(exitCode, 0, 'precondition exits 0 when SUMMARY matches PLAN');
85
+ });
86
+
87
+ // REL-08 sentinel — flag this file as a Wave-0 RED scaffold for plan 02.
@@ -0,0 +1,21 @@
1
+ {
2
+ "results": [
3
+ {
4
+ "type": "unpatched_gem",
5
+ "advisory": {
6
+ "id": "CVE-2020-8164",
7
+ "url": "https://groups.google.com/g/rubyonrails-security/c/f6ioe4sdpbY",
8
+ "title": "Possible Strong Parameters Bypass in ActionPack",
9
+ "description": "There is a strong parameters bypass vector in ActionPack versions 5.2.4.3 and below.",
10
+ "cvss_v3": 7.5,
11
+ "cvss_v2": 5.0,
12
+ "criticality": "high",
13
+ "solution": "Upgrade to activerecord >= 5.2.4.3"
14
+ },
15
+ "gem": {
16
+ "name": "activerecord",
17
+ "version": "5.2.3"
18
+ }
19
+ }
20
+ ]
21
+ }