@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.
- package/CHANGELOG.md +115 -0
- package/README.md +8 -1
- package/agents/dgs-executor.md +124 -3
- package/agents/dgs-idea-researcher.md +447 -0
- package/agents/dgs-plan-checker.md +32 -0
- package/agents/dgs-planner.md +41 -8
- package/bin/install.js +44 -0
- package/commands/dgs/audit-milestone.md +2 -1
- package/commands/dgs/diff-report.md +124 -0
- package/commands/dgs/new-project.md +8 -21
- package/commands/dgs/package-scan.md +43 -0
- package/commands/dgs/research-idea.md +1 -0
- package/commands/dgs/switch-project.md +13 -0
- package/deliver-great-systems/bin/dgs-tools.cjs +120 -5
- 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 +311 -16
- package/deliver-great-systems/bin/lib/commands.test.cjs +115 -0
- package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
- package/deliver-great-systems/bin/lib/config.cjs +41 -0
- package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
- package/deliver-great-systems/bin/lib/core.cjs +7 -3
- package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
- 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/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/init.cjs +56 -27
- package/deliver-great-systems/bin/lib/init.test.cjs +212 -5
- package/deliver-great-systems/bin/lib/jobs.cjs +7 -4
- package/deliver-great-systems/bin/lib/milestone.cjs +101 -3
- 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 +18 -1
- package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
- package/deliver-great-systems/bin/lib/projects.cjs +38 -3
- package/deliver-great-systems/bin/lib/projects.test.cjs +112 -2
- package/deliver-great-systems/bin/lib/quick.cjs +178 -23
- package/deliver-great-systems/bin/lib/quick.test.cjs +138 -4
- package/deliver-great-systems/bin/lib/repos.cjs +12 -12
- package/deliver-great-systems/bin/lib/review.cjs +1821 -0
- package/deliver-great-systems/bin/lib/state.cjs +7 -3
- 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/verify.cjs +118 -6
- 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 +27 -1
- package/deliver-great-systems/bin/lib/worktrees.test.cjs +76 -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 +11 -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/add-phase.md +5 -0
- package/deliver-great-systems/workflows/audit-milestone.md +66 -10
- package/deliver-great-systems/workflows/cancel-job.md +1 -1
- package/deliver-great-systems/workflows/codereview.md +103 -9
- package/deliver-great-systems/workflows/complete-milestone.md +26 -7
- package/deliver-great-systems/workflows/complete-quick.md +40 -2
- package/deliver-great-systems/workflows/discuss-phase.md +3 -2
- package/deliver-great-systems/workflows/execute-phase.md +89 -2
- package/deliver-great-systems/workflows/execute-plan.md +10 -1
- package/deliver-great-systems/workflows/help.md +51 -18
- package/deliver-great-systems/workflows/import-spec.md +65 -7
- package/deliver-great-systems/workflows/init-product.md +46 -152
- package/deliver-great-systems/workflows/new-milestone.md +115 -14
- 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/quick-complete.md +40 -2
- package/deliver-great-systems/workflows/quick.md +183 -10
- package/deliver-great-systems/workflows/research-idea.md +80 -142
- package/deliver-great-systems/workflows/run-job.md +21 -35
- package/deliver-great-systems/workflows/settings.md +13 -77
- package/deliver-great-systems/workflows/write-spec.md +9 -11
- package/hooks/dist/dgs-enforce-discipline.js +196 -0
- package/package.json +1 -1
- 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
|
|
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
|
+
}
|