@ktpartners/dgs-platform 2.7.4 → 2.8.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 +25 -0
- package/agents/dgs-executor.md +0 -52
- package/deliver-great-systems/bin/dgs-tools.cjs +66 -10
- package/deliver-great-systems/bin/lib/commands.cjs +1 -8
- package/deliver-great-systems/bin/lib/config.cjs +9 -90
- package/deliver-great-systems/bin/lib/context.cjs +2 -2
- package/deliver-great-systems/bin/lib/context.test.cjs +100 -100
- package/deliver-great-systems/bin/lib/core.cjs +17 -57
- package/deliver-great-systems/bin/lib/core.test.cjs +166 -170
- package/deliver-great-systems/bin/lib/docs.cjs +3 -3
- package/deliver-great-systems/bin/lib/docs.test.cjs +14 -7
- package/deliver-great-systems/bin/lib/execution.cjs +2 -2
- package/deliver-great-systems/bin/lib/execution.test.cjs +65 -67
- package/deliver-great-systems/bin/lib/ideas.cjs +4 -4
- package/deliver-great-systems/bin/lib/ideas.test.cjs +45 -44
- package/deliver-great-systems/bin/lib/init.cjs +9 -4
- package/deliver-great-systems/bin/lib/init.test.cjs +242 -175
- package/deliver-great-systems/bin/lib/jobs.cjs +1 -1
- package/deliver-great-systems/bin/lib/jobs.test.cjs +203 -202
- package/deliver-great-systems/bin/lib/migration.cjs +256 -281
- package/deliver-great-systems/bin/lib/migration.test.cjs +385 -440
- package/deliver-great-systems/bin/lib/milestone.cjs +1 -1
- package/deliver-great-systems/bin/lib/overlap.cjs +4 -4
- package/deliver-great-systems/bin/lib/overlap.test.cjs +45 -44
- package/deliver-great-systems/bin/lib/path-audit.test.cjs +16 -22
- package/deliver-great-systems/bin/lib/paths.cjs +60 -59
- package/deliver-great-systems/bin/lib/paths.test.cjs +192 -225
- package/deliver-great-systems/bin/lib/phase.cjs +5 -4
- package/deliver-great-systems/bin/lib/projects.cjs +8 -8
- package/deliver-great-systems/bin/lib/projects.test.cjs +75 -74
- package/deliver-great-systems/bin/lib/repos.cjs +94 -230
- package/deliver-great-systems/bin/lib/repos.test.cjs +84 -75
- package/deliver-great-systems/bin/lib/search.cjs +4 -4
- package/deliver-great-systems/bin/lib/specs.cjs +2 -2
- package/deliver-great-systems/bin/lib/sync.cjs +1 -1
- package/deliver-great-systems/bin/lib/template.cjs +3 -3
- package/deliver-great-systems/bin/lib/test-helpers.cjs +59 -162
- package/deliver-great-systems/bin/lib/verify.cjs +3 -3
- package/deliver-great-systems/references/planning-config.md +7 -8
- package/deliver-great-systems/workflows/add-tests.md +1 -1
- package/deliver-great-systems/workflows/approve-spec.md +1 -11
- package/deliver-great-systems/workflows/complete-milestone.md +2 -2
- package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
- package/deliver-great-systems/workflows/create-milestone-job.md +2 -2
- package/deliver-great-systems/workflows/discuss-phase.md +2 -2
- package/deliver-great-systems/workflows/execute-phase.md +63 -4
- package/deliver-great-systems/workflows/execute-plan.md +0 -51
- package/deliver-great-systems/workflows/find-related-ideas.md +1 -1
- package/deliver-great-systems/workflows/help.md +25 -58
- package/deliver-great-systems/workflows/init-product.md +14 -451
- package/deliver-great-systems/workflows/map-codebase.md +109 -0
- package/deliver-great-systems/workflows/new-project.md +0 -1
- package/deliver-great-systems/workflows/quick.md +6 -7
- package/deliver-great-systems/workflows/run-job.md +56 -0
- package/deliver-great-systems/workflows/settings.md +30 -0
- package/package.json +5 -1
|
@@ -144,7 +144,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
144
144
|
// Archive REQUIREMENTS.md
|
|
145
145
|
if (fs.existsSync(reqPath)) {
|
|
146
146
|
const reqContent = fs.readFileSync(reqPath, 'utf-8');
|
|
147
|
-
const archiveHeader = `# Requirements Archive: ${version} ${milestoneName}\n\n**Archived:** ${today}\n**Status:** SHIPPED\n\nFor current requirements, see
|
|
147
|
+
const archiveHeader = `# Requirements Archive: ${version} ${milestoneName}\n\n**Archived:** ${today}\n**Status:** SHIPPED\n\nFor current requirements, see \`REQUIREMENTS.md\`.\n\n---\n\n`;
|
|
148
148
|
fs.writeFileSync(path.join(archiveDir, `${version}-REQUIREMENTS.md`), archiveHeader + reqContent, 'utf-8');
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -21,7 +21,7 @@ const SEVERITY_ORDER = { HIGH: 0, MEDIUM: 1, LOW: 2 };
|
|
|
21
21
|
/**
|
|
22
22
|
* Scan a project's plan files and extract repos and files from XML tags.
|
|
23
23
|
*
|
|
24
|
-
* Scans all *-PLAN.md files under
|
|
24
|
+
* Scans all *-PLAN.md files under projects/<slug>/phases/ subdirectories.
|
|
25
25
|
* For each plan file, extracts all <repos> and <files> tags.
|
|
26
26
|
*
|
|
27
27
|
* @param {string} cwd - Working directory (product root)
|
|
@@ -97,7 +97,7 @@ function scanProjectPlanFiles(cwd, slug) {
|
|
|
97
97
|
* Filter out phase directories that are in archived milestone paths.
|
|
98
98
|
*
|
|
99
99
|
* Archived paths match patterns like:
|
|
100
|
-
*
|
|
100
|
+
* milestones/v1.0-phases/01-setup
|
|
101
101
|
*
|
|
102
102
|
* @param {string[]} phaseDirs - Array of full phase directory paths
|
|
103
103
|
* @returns {string[]} Filtered array with archived paths removed
|
|
@@ -181,7 +181,7 @@ function classifyOverlapSeverity(overlapEntry) {
|
|
|
181
181
|
* Get slugs of all active (non-completed) projects.
|
|
182
182
|
*
|
|
183
183
|
* Reads PROJECTS.md to get active project slugs. Falls back to scanning
|
|
184
|
-
*
|
|
184
|
+
* subfolders if PROJECTS.md is missing. Applies ghost project
|
|
185
185
|
* guard (skips folders without STATE.md).
|
|
186
186
|
*
|
|
187
187
|
* @param {string} cwd - Working directory (product root)
|
|
@@ -200,7 +200,7 @@ function getActiveProjectSlugs(cwd) {
|
|
|
200
200
|
});
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
// Fallback: scan
|
|
203
|
+
// Fallback: scan project subfolders
|
|
204
204
|
const folders = getProjectFolders(cwd);
|
|
205
205
|
return folders.filter(slug => {
|
|
206
206
|
const state = readProjectState(cwd, slug);
|
|
@@ -8,16 +8,17 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
10
|
|
|
11
|
-
const { createTempDir, cleanupDir, writeFile } = require('./test-helpers.cjs');
|
|
11
|
+
const { createTempDir, cleanupDir, writeFile , initGitRepo } = require('./test-helpers.cjs');
|
|
12
|
+
const { resetPaths } = require('./paths.cjs');
|
|
12
13
|
|
|
13
|
-
// Helper to create a minimal project with STATE.md under
|
|
14
|
+
// Helper to create a minimal project with STATE.md under projects/<slug>/
|
|
14
15
|
function createProject(cwd, slug, status = 'Ready to execute') {
|
|
15
|
-
writeFile(cwd,
|
|
16
|
-
writeFile(cwd,
|
|
17
|
-
fs.mkdirSync(path.join(cwd, '
|
|
16
|
+
writeFile(cwd, `projects/${slug}/STATE.md`, `# Project State\n\nPhase: 1\nStatus: ${status}\nProgress: [░░░░░░░░░░] 0%\n`);
|
|
17
|
+
writeFile(cwd, `projects/${slug}/PROJECT.md`, `# Project: ${slug}\n`);
|
|
18
|
+
fs.mkdirSync(path.join(cwd, 'projects', slug, 'phases'), { recursive: true });
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
// Helper to create a plan file with repos and files tags under
|
|
21
|
+
// Helper to create a plan file with repos and files tags under projects/<slug>/phases/
|
|
21
22
|
function createPlanFile(cwd, slug, phaseDir, planName, tasks) {
|
|
22
23
|
let content = `---\nphase: ${phaseDir}\nplan: 01\n---\n\n<tasks>\n`;
|
|
23
24
|
for (const task of tasks) {
|
|
@@ -28,7 +29,7 @@ function createPlanFile(cwd, slug, phaseDir, planName, tasks) {
|
|
|
28
29
|
content += ` <action>Do something</action>\n <verify>Check it</verify>\n <done>Done</done>\n</task>\n`;
|
|
29
30
|
}
|
|
30
31
|
content += '\n</tasks>\n';
|
|
31
|
-
writeFile(cwd,
|
|
32
|
+
writeFile(cwd, `projects/${slug}/phases/${phaseDir}/${planName}`, content);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
// Helper to create PROJECTS.md
|
|
@@ -45,7 +46,7 @@ function createProjectsMd(cwd, projects) {
|
|
|
45
46
|
for (const p of projects.filter(p => p.status === 'completed')) {
|
|
46
47
|
content += `| ${p.name} | ${p.completed || ''} | |\n`;
|
|
47
48
|
}
|
|
48
|
-
writeFile(cwd, '
|
|
49
|
+
writeFile(cwd, 'PROJECTS.md', content);
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
const {
|
|
@@ -61,13 +62,13 @@ const {
|
|
|
61
62
|
|
|
62
63
|
describe('scanProjectPlanFiles', () => {
|
|
63
64
|
let tmpDir;
|
|
64
|
-
beforeEach(() => { tmpDir = createTempDir(); });
|
|
65
|
-
afterEach(() => { cleanupDir(tmpDir); });
|
|
65
|
+
beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
|
|
66
|
+
afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
|
|
66
67
|
|
|
67
68
|
it('returns empty array when project has no phases directory', () => {
|
|
68
69
|
createProject(tmpDir, 'project-a');
|
|
69
70
|
// Remove phases dir
|
|
70
|
-
fs.rmSync(path.join(tmpDir, '
|
|
71
|
+
fs.rmSync(path.join(tmpDir, 'projects', 'project-a', 'phases'), { recursive: true, force: true });
|
|
71
72
|
const result = scanProjectPlanFiles(tmpDir, 'project-a');
|
|
72
73
|
assert.ok(Array.isArray(result));
|
|
73
74
|
assert.strictEqual(result.length, 0);
|
|
@@ -105,9 +106,9 @@ describe('scanProjectPlanFiles', () => {
|
|
|
105
106
|
{ name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
|
|
106
107
|
]);
|
|
107
108
|
// Create a SUMMARY.md file (should be skipped)
|
|
108
|
-
writeFile(tmpDir, '
|
|
109
|
+
writeFile(tmpDir, 'projects/project-a/phases/01-setup/01-01-SUMMARY.md', '<repos>server</repos>\n<files>server/x.js</files>');
|
|
109
110
|
// Create a CONTEXT.md file (should be skipped)
|
|
110
|
-
writeFile(tmpDir, '
|
|
111
|
+
writeFile(tmpDir, 'projects/project-a/phases/01-setup/01-CONTEXT.md', '<repos>other</repos>');
|
|
111
112
|
const result = scanProjectPlanFiles(tmpDir, 'project-a');
|
|
112
113
|
assert.strictEqual(result.length, 1);
|
|
113
114
|
// Only web-app from the PLAN.md, not server from SUMMARY.md
|
|
@@ -128,7 +129,7 @@ describe('scanProjectPlanFiles', () => {
|
|
|
128
129
|
|
|
129
130
|
it('handles plan files without repos or files tags', () => {
|
|
130
131
|
createProject(tmpDir, 'project-a');
|
|
131
|
-
writeFile(tmpDir, '
|
|
132
|
+
writeFile(tmpDir, 'projects/project-a/phases/01-setup/01-01-PLAN.md',
|
|
132
133
|
'---\nphase: 01\n---\n\n<tasks>\n<task type="auto">\n <name>Task 1</name>\n <action>Do</action>\n</task>\n</tasks>\n'
|
|
133
134
|
);
|
|
134
135
|
const result = scanProjectPlanFiles(tmpDir, 'project-a');
|
|
@@ -142,13 +143,13 @@ describe('scanProjectPlanFiles', () => {
|
|
|
142
143
|
|
|
143
144
|
describe('buildOverlapMatrix', () => {
|
|
144
145
|
let tmpDir;
|
|
145
|
-
beforeEach(() => { tmpDir = createTempDir(); });
|
|
146
|
-
afterEach(() => { cleanupDir(tmpDir); });
|
|
146
|
+
beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
|
|
147
|
+
afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
|
|
147
148
|
|
|
148
149
|
it('returns empty overlap when only one project exists', () => {
|
|
149
150
|
createProject(tmpDir, 'project-a');
|
|
150
151
|
createProjectsMd(tmpDir, [{ name: 'project-a', status: 'Active' }]);
|
|
151
|
-
writeFile(tmpDir, '
|
|
152
|
+
writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
|
|
152
153
|
createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
|
|
153
154
|
{ name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
|
|
154
155
|
]);
|
|
@@ -164,7 +165,7 @@ describe('buildOverlapMatrix', () => {
|
|
|
164
165
|
{ name: 'project-a', status: 'Active' },
|
|
165
166
|
{ name: 'project-b', status: 'Active' },
|
|
166
167
|
]);
|
|
167
|
-
writeFile(tmpDir, '
|
|
168
|
+
writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
|
|
168
169
|
createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
|
|
169
170
|
{ name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
|
|
170
171
|
]);
|
|
@@ -185,7 +186,7 @@ describe('buildOverlapMatrix', () => {
|
|
|
185
186
|
{ name: 'project-a', status: 'Active' },
|
|
186
187
|
{ name: 'project-b', status: 'Active' },
|
|
187
188
|
]);
|
|
188
|
-
writeFile(tmpDir, '
|
|
189
|
+
writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
|
|
189
190
|
createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
|
|
190
191
|
{ name: 'Task 1', files: ['web-app/src/shared.js', 'web-app/src/a.js'], repos: ['web-app'] },
|
|
191
192
|
]);
|
|
@@ -210,7 +211,7 @@ describe('buildOverlapMatrix', () => {
|
|
|
210
211
|
{ name: 'project-a', status: 'Active' },
|
|
211
212
|
{ name: 'project-b', status: 'completed', completed: '2026-01-01' },
|
|
212
213
|
]);
|
|
213
|
-
writeFile(tmpDir, '
|
|
214
|
+
writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
|
|
214
215
|
createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
|
|
215
216
|
{ name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
|
|
216
217
|
]);
|
|
@@ -227,7 +228,7 @@ describe('buildOverlapMatrix', () => {
|
|
|
227
228
|
{ name: 'project-a', status: 'Active' },
|
|
228
229
|
{ name: 'ghost-project', status: 'Active' },
|
|
229
230
|
]);
|
|
230
|
-
writeFile(tmpDir, '
|
|
231
|
+
writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
|
|
231
232
|
createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
|
|
232
233
|
{ name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
|
|
233
234
|
]);
|
|
@@ -244,7 +245,7 @@ describe('buildOverlapMatrix', () => {
|
|
|
244
245
|
{ name: 'project-a', status: 'Active' },
|
|
245
246
|
{ name: 'project-b', status: 'Active' },
|
|
246
247
|
]);
|
|
247
|
-
writeFile(tmpDir, '
|
|
248
|
+
writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n| server | ./server |\n');
|
|
248
249
|
createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
|
|
249
250
|
{ name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
|
|
250
251
|
]);
|
|
@@ -264,7 +265,7 @@ describe('buildOverlapMatrix', () => {
|
|
|
264
265
|
{ name: 'project-b', status: 'Active' },
|
|
265
266
|
{ name: 'project-c', status: 'Active' },
|
|
266
267
|
]);
|
|
267
|
-
writeFile(tmpDir, '
|
|
268
|
+
writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| shared | ./shared |\n');
|
|
268
269
|
createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
|
|
269
270
|
{ name: 'Task 1', files: ['shared/lib.js'], repos: ['shared'] },
|
|
270
271
|
]);
|
|
@@ -286,7 +287,7 @@ describe('buildOverlapMatrix', () => {
|
|
|
286
287
|
{ name: 'project-a', status: 'Active' },
|
|
287
288
|
{ name: 'project-b', status: 'Active' },
|
|
288
289
|
]);
|
|
289
|
-
writeFile(tmpDir, '
|
|
290
|
+
writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| services-api | ./services/api |\n| services-api-gateway | ./services/api-gateway |\n');
|
|
290
291
|
createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
|
|
291
292
|
{ name: 'Task 1', files: ['services/api/shared.ts', 'services/api-gateway/handler.ts'], repos: ['services-api', 'services-api-gateway'] },
|
|
292
293
|
]);
|
|
@@ -314,7 +315,7 @@ describe('buildOverlapMatrix', () => {
|
|
|
314
315
|
{ name: 'project-a', status: 'Active' },
|
|
315
316
|
{ name: 'project-b', status: 'Active' },
|
|
316
317
|
]);
|
|
317
|
-
writeFile(tmpDir, '
|
|
318
|
+
writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
|
|
318
319
|
// Repo-relative paths: src/index.ts instead of web-app/src/index.ts
|
|
319
320
|
createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
|
|
320
321
|
{ name: 'Task 1', files: ['src/index.ts', 'src/app.ts'], repos: ['web-app'] },
|
|
@@ -341,7 +342,7 @@ describe('buildOverlapMatrix', () => {
|
|
|
341
342
|
{ name: 'project-a', status: 'Active' },
|
|
342
343
|
{ name: 'project-b', status: 'Active' },
|
|
343
344
|
]);
|
|
344
|
-
writeFile(tmpDir, '
|
|
345
|
+
writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
|
|
345
346
|
// Legacy paths without repos tags — files include repo prefix
|
|
346
347
|
createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
|
|
347
348
|
{ name: 'Task 1', files: ['web-app/src/shared.js'] },
|
|
@@ -361,13 +362,13 @@ describe('buildOverlapMatrix', () => {
|
|
|
361
362
|
|
|
362
363
|
describe('excludeArchivedPhases', () => {
|
|
363
364
|
let tmpDir;
|
|
364
|
-
beforeEach(() => { tmpDir = createTempDir(); });
|
|
365
|
-
afterEach(() => { cleanupDir(tmpDir); });
|
|
365
|
+
beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
|
|
366
|
+
afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
|
|
366
367
|
|
|
367
368
|
it('keeps current phase directories', () => {
|
|
368
369
|
const dirs = [
|
|
369
|
-
path.join(tmpDir, '
|
|
370
|
-
path.join(tmpDir, '
|
|
370
|
+
path.join(tmpDir, 'projects/project-a/phases/01-setup'),
|
|
371
|
+
path.join(tmpDir, 'projects/project-a/phases/02-build'),
|
|
371
372
|
];
|
|
372
373
|
const result = excludeArchivedPhases(dirs);
|
|
373
374
|
assert.strictEqual(result.length, 2);
|
|
@@ -375,8 +376,8 @@ describe('excludeArchivedPhases', () => {
|
|
|
375
376
|
|
|
376
377
|
it('excludes directories in milestones archive path', () => {
|
|
377
378
|
const dirs = [
|
|
378
|
-
path.join(tmpDir, '
|
|
379
|
-
path.join(tmpDir, '
|
|
379
|
+
path.join(tmpDir, 'projects/project-a/phases/01-setup'),
|
|
380
|
+
path.join(tmpDir, 'milestones/v1.0-phases/01-setup'),
|
|
380
381
|
];
|
|
381
382
|
const result = excludeArchivedPhases(dirs);
|
|
382
383
|
assert.strictEqual(result.length, 1);
|
|
@@ -517,8 +518,8 @@ describe('classifyOverlapSeverity', () => {
|
|
|
517
518
|
|
|
518
519
|
describe('buildOverlapMatrix severity', () => {
|
|
519
520
|
let tmpDir;
|
|
520
|
-
beforeEach(() => { tmpDir = createTempDir(); });
|
|
521
|
-
afterEach(() => { cleanupDir(tmpDir); });
|
|
521
|
+
beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
|
|
522
|
+
afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
|
|
522
523
|
|
|
523
524
|
it('two projects sharing same file -> severity HIGH', () => {
|
|
524
525
|
createProject(tmpDir, 'project-a');
|
|
@@ -527,7 +528,7 @@ describe('buildOverlapMatrix severity', () => {
|
|
|
527
528
|
{ name: 'project-a', status: 'Active' },
|
|
528
529
|
{ name: 'project-b', status: 'Active' },
|
|
529
530
|
]);
|
|
530
|
-
writeFile(tmpDir, '
|
|
531
|
+
writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
|
|
531
532
|
createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
|
|
532
533
|
{ name: 'Task 1', files: ['web-app/src/shared.js'], repos: ['web-app'] },
|
|
533
534
|
]);
|
|
@@ -547,7 +548,7 @@ describe('buildOverlapMatrix severity', () => {
|
|
|
547
548
|
{ name: 'project-a', status: 'Active' },
|
|
548
549
|
{ name: 'project-b', status: 'Active' },
|
|
549
550
|
]);
|
|
550
|
-
writeFile(tmpDir, '
|
|
551
|
+
writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
|
|
551
552
|
createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
|
|
552
553
|
{ name: 'Task 1', files: ['web-app/src/auth/login.js'], repos: ['web-app'] },
|
|
553
554
|
]);
|
|
@@ -567,7 +568,7 @@ describe('buildOverlapMatrix severity', () => {
|
|
|
567
568
|
{ name: 'project-a', status: 'Active' },
|
|
568
569
|
{ name: 'project-b', status: 'Active' },
|
|
569
570
|
]);
|
|
570
|
-
writeFile(tmpDir, '
|
|
571
|
+
writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
|
|
571
572
|
createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
|
|
572
573
|
{ name: 'Task 1', files: ['web-app/src/auth/login.js'], repos: ['web-app'] },
|
|
573
574
|
]);
|
|
@@ -587,7 +588,7 @@ describe('buildOverlapMatrix severity', () => {
|
|
|
587
588
|
{ name: 'project-a', status: 'Active' },
|
|
588
589
|
{ name: 'project-b', status: 'Active' },
|
|
589
590
|
]);
|
|
590
|
-
writeFile(tmpDir, '
|
|
591
|
+
writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| web-app | ./web-app |\n');
|
|
591
592
|
createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
|
|
592
593
|
{ name: 'Task 1', files: ['web-app/src/a.js'], repos: ['web-app'] },
|
|
593
594
|
]);
|
|
@@ -607,7 +608,7 @@ describe('buildOverlapMatrix severity', () => {
|
|
|
607
608
|
{ name: 'project-a', status: 'Active' },
|
|
608
609
|
{ name: 'project-b', status: 'Active' },
|
|
609
610
|
]);
|
|
610
|
-
writeFile(tmpDir, '
|
|
611
|
+
writeFile(tmpDir, 'REPOS.md', '# Repos\n\n| Name | Path |\n|------|------|\n| server | ./server |\n| web-app | ./web-app |\n');
|
|
611
612
|
// Use separate plan files per repo so each plan has a single <repos> tag
|
|
612
613
|
// This ensures files are mapped precisely to one repo each
|
|
613
614
|
// project-a: server plan (shared.js) + web-app plan (auth)
|
|
@@ -694,8 +695,8 @@ describe('formatOverlapReport severity', () => {
|
|
|
694
695
|
|
|
695
696
|
describe('getActiveProjectSlugs', () => {
|
|
696
697
|
let tmpDir;
|
|
697
|
-
beforeEach(() => { tmpDir = createTempDir(); });
|
|
698
|
-
afterEach(() => { cleanupDir(tmpDir); });
|
|
698
|
+
beforeEach(() => { tmpDir = createTempDir(); tmpDir = fs.realpathSync(tmpDir); initGitRepo(tmpDir); });
|
|
699
|
+
afterEach(() => { resetPaths(); cleanupDir(tmpDir); });
|
|
699
700
|
|
|
700
701
|
it('returns active slugs from PROJECTS.md', () => {
|
|
701
702
|
createProject(tmpDir, 'project-a');
|
|
@@ -724,7 +725,7 @@ describe('getActiveProjectSlugs', () => {
|
|
|
724
725
|
it('falls back to scanning folders when PROJECTS.md missing', () => {
|
|
725
726
|
createProject(tmpDir, 'project-a');
|
|
726
727
|
createProject(tmpDir, 'project-b');
|
|
727
|
-
// No PROJECTS.md — should scan
|
|
728
|
+
// No PROJECTS.md — should scan projects/ subfolders
|
|
728
729
|
const result = getActiveProjectSlugs(tmpDir);
|
|
729
730
|
assert.ok(result.includes('project-a'));
|
|
730
731
|
assert.ok(result.includes('project-b'));
|
|
@@ -733,14 +734,14 @@ describe('getActiveProjectSlugs', () => {
|
|
|
733
734
|
it('ghost project guard: skips folders without STATE.md', () => {
|
|
734
735
|
createProject(tmpDir, 'project-a');
|
|
735
736
|
// Create a ghost folder without STATE.md
|
|
736
|
-
fs.mkdirSync(path.join(tmpDir, '
|
|
737
|
+
fs.mkdirSync(path.join(tmpDir, 'projects', 'ghost-project'), { recursive: true });
|
|
737
738
|
const result = getActiveProjectSlugs(tmpDir);
|
|
738
739
|
assert.ok(result.includes('project-a'));
|
|
739
740
|
assert.ok(!result.includes('ghost-project'));
|
|
740
741
|
});
|
|
741
742
|
|
|
742
743
|
it('returns empty array when no projects exist', () => {
|
|
743
|
-
|
|
744
|
+
// No projects directory exists
|
|
744
745
|
const result = getActiveProjectSlugs(tmpDir);
|
|
745
746
|
assert.strictEqual(result.length, 0);
|
|
746
747
|
});
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
* 2. GATES on library source files (path.join(cwd, '.planning') pattern)
|
|
8
8
|
* 3. Documents baselines for lib module raw string literals
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - workflows/help.md — user documentation explaining directory structure
|
|
10
|
+
* All file categories are gated -- no allowlisted files remain.
|
|
11
|
+
* init-product.md and help.md were cleaned in Phase 122.
|
|
13
12
|
*
|
|
14
13
|
* Allowlisted line patterns:
|
|
15
14
|
* - HTML comments: <!-- .planning/ ... -->
|
|
@@ -45,30 +44,25 @@ const PROJECT_SCOPED_PATTERN = /\.planning\/(?:STATE|ROADMAP|PROJECT(?!S\.md)|RE
|
|
|
45
44
|
|
|
46
45
|
/**
|
|
47
46
|
* Workflow files allowlisted from comprehensive .planning/ scanning.
|
|
48
|
-
*
|
|
47
|
+
* Empty -- all workflow files were cleaned in Phase 122.
|
|
49
48
|
*/
|
|
50
|
-
const ALLOWLISTED_WORKFLOW_FILES = [
|
|
51
|
-
'workflows/init-product.md', // Creates and describes both layouts
|
|
52
|
-
'workflows/help.md', // User documentation explaining directory structure
|
|
53
|
-
];
|
|
49
|
+
const ALLOWLISTED_WORKFLOW_FILES = [];
|
|
54
50
|
|
|
55
51
|
/**
|
|
56
52
|
* Reference files allowlisted from comprehensive .planning/ scanning.
|
|
57
|
-
*
|
|
53
|
+
* Empty -- all reference files were cleaned in Phase 122.
|
|
58
54
|
*/
|
|
59
|
-
const ALLOWLISTED_REFERENCE_FILES = [
|
|
60
|
-
'references/planning-config.md', // Has .planning/ in .gitignore example block
|
|
61
|
-
];
|
|
55
|
+
const ALLOWLISTED_REFERENCE_FILES = [];
|
|
62
56
|
|
|
63
57
|
/**
|
|
64
58
|
* Product-level paths that are intentionally at .planning/ root and should
|
|
65
59
|
* NOT be flagged by the audit.
|
|
66
60
|
*/
|
|
67
61
|
const SAFE_PRODUCT_PATHS = [
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
62
|
+
'config.json',
|
|
63
|
+
'PROJECTS.md',
|
|
64
|
+
'REPOS.md',
|
|
65
|
+
'codebase',
|
|
72
66
|
];
|
|
73
67
|
|
|
74
68
|
/**
|
|
@@ -288,9 +282,9 @@ describe('path audit: comprehensive .planning/ reference scanning', () => {
|
|
|
288
282
|
|
|
289
283
|
// Allowlist must stay minimal
|
|
290
284
|
assert.ok(
|
|
291
|
-
ALLOWLISTED_WORKFLOW_FILES.length
|
|
292
|
-
`Workflow allowlist
|
|
293
|
-
'
|
|
285
|
+
ALLOWLISTED_WORKFLOW_FILES.length === 0,
|
|
286
|
+
`Workflow allowlist should be empty (found ${ALLOWLISTED_WORKFLOW_FILES.length}). ` +
|
|
287
|
+
'All workflow files were cleaned in Phase 122.'
|
|
294
288
|
);
|
|
295
289
|
});
|
|
296
290
|
|
|
@@ -302,9 +296,9 @@ describe('path audit: comprehensive .planning/ reference scanning', () => {
|
|
|
302
296
|
|
|
303
297
|
// Allowlist must stay minimal
|
|
304
298
|
assert.ok(
|
|
305
|
-
ALLOWLISTED_REFERENCE_FILES.length
|
|
306
|
-
`Reference allowlist
|
|
307
|
-
'
|
|
299
|
+
ALLOWLISTED_REFERENCE_FILES.length === 0,
|
|
300
|
+
`Reference allowlist should be empty (found ${ALLOWLISTED_REFERENCE_FILES.length}). ` +
|
|
301
|
+
'All reference files were cleaned in Phase 122.'
|
|
308
302
|
);
|
|
309
303
|
});
|
|
310
304
|
|
|
@@ -1,28 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* paths.cjs -- Planning root detection and path constants
|
|
3
3
|
*
|
|
4
|
-
* LEAF MODULE: Zero DGS imports. Uses only Node.js built-in fs
|
|
5
|
-
* This module sits at the bottom of the dependency
|
|
6
|
-
* circular dependencies with core.cjs and config.cjs.
|
|
4
|
+
* LEAF MODULE: Zero DGS imports. Uses only Node.js built-in fs, path,
|
|
5
|
+
* and child_process. This module sits at the bottom of the dependency
|
|
6
|
+
* graph to prevent circular dependencies with core.cjs and config.cjs.
|
|
7
7
|
*
|
|
8
8
|
* Exports:
|
|
9
9
|
* getPlanningRoot(cwd) -- Returns absolute path to planning root
|
|
10
10
|
* getPaths(cwd) -- Returns frozen PATHS object with all standard paths
|
|
11
11
|
* initPaths(cwd) -- Alias for getPaths (explicit cache population)
|
|
12
|
-
* resetPaths() -- Clears
|
|
12
|
+
* resetPaths() -- Clears caches (for test isolation)
|
|
13
|
+
* isV2Install(cwd) -- Detects V2 multi-project installation
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* (legacy fallback: dgs.config.json at root with planningRoot: '.')
|
|
18
|
-
* 3. dgs.config.json at root without planningRoot: '.' -> .planning/ layout
|
|
19
|
-
* 4. PROJECT.md at root AND no .planning/ directory -> root layout (auto-detect)
|
|
20
|
-
* 5. No signals -> .planning/ layout (default)
|
|
15
|
+
* Root detection (getPlanningRoot):
|
|
16
|
+
* Single `git rev-parse --show-toplevel` call. Errors if not in a git repo.
|
|
17
|
+
* Result cached per-process.
|
|
21
18
|
*/
|
|
22
19
|
|
|
23
20
|
const fs = require('fs');
|
|
24
21
|
const path = require('path');
|
|
22
|
+
const { execSync } = require('child_process');
|
|
25
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Per-process cache for getPlanningRoot result.
|
|
26
|
+
* One git repo = one root per process. Cleared by resetPaths().
|
|
27
|
+
*/
|
|
28
|
+
let _planningRootCache = null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Per-cwd cache for getPaths results.
|
|
32
|
+
* Different cwd values produce different absolute paths.
|
|
33
|
+
*/
|
|
26
34
|
const _cache = new Map();
|
|
27
35
|
|
|
28
36
|
/**
|
|
@@ -34,57 +42,50 @@ const PROJECTS_DIR = 'projects';
|
|
|
34
42
|
|
|
35
43
|
/**
|
|
36
44
|
* Detect the planning root directory for the given cwd.
|
|
45
|
+
* Uses `git rev-parse --show-toplevel` to find the git repo root.
|
|
46
|
+
* Result is cached per-process for performance.
|
|
37
47
|
*
|
|
38
48
|
* @param {string} [cwd] - Working directory (defaults to process.cwd())
|
|
39
|
-
* @returns {string} Absolute path to the planning root
|
|
49
|
+
* @returns {string} Absolute path to the planning root (git repo root)
|
|
40
50
|
*/
|
|
41
51
|
function getPlanningRoot(cwd) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// Step 2: Root layout -- config.local.json at repo root with planningRoot: '.'
|
|
55
|
-
const rootLocalConfigPath = path.join(resolved, 'config.local.json');
|
|
56
|
-
if (fs.existsSync(rootLocalConfigPath)) {
|
|
57
|
-
try {
|
|
58
|
-
const config = JSON.parse(fs.readFileSync(rootLocalConfigPath, 'utf-8'));
|
|
59
|
-
if (config.planningRoot === '.') return resolved;
|
|
60
|
-
} catch {
|
|
61
|
-
// Malformed JSON -- fall through
|
|
62
|
-
}
|
|
52
|
+
if (_planningRootCache !== null) return _planningRootCache;
|
|
53
|
+
try {
|
|
54
|
+
const root = execSync('git rev-parse --show-toplevel', {
|
|
55
|
+
cwd: cwd || process.cwd(),
|
|
56
|
+
encoding: 'utf-8',
|
|
57
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
58
|
+
}).trim();
|
|
59
|
+
_planningRootCache = root;
|
|
60
|
+
return root;
|
|
61
|
+
} catch {
|
|
62
|
+
process.stderr.write('Error: DGS requires a git repository. Run this command from inside a git repo.\n');
|
|
63
|
+
process.exit(1);
|
|
63
64
|
}
|
|
65
|
+
}
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Detect if this is a v2 (multi-project) installation.
|
|
69
|
+
* Requires both file existence AND valid DGS-generated content.
|
|
70
|
+
* PROJECTS.md must start with "# Projects", REPOS.md with "# Repos".
|
|
71
|
+
*
|
|
72
|
+
* @param {string} cwd - Working directory
|
|
73
|
+
* @returns {boolean}
|
|
74
|
+
*/
|
|
75
|
+
function isV2Install(cwd) {
|
|
76
|
+
const checks = [
|
|
77
|
+
{ file: 'PROJECTS.md', header: '# Projects' },
|
|
78
|
+
{ file: 'REPOS.md', header: '# Repos' },
|
|
79
|
+
];
|
|
80
|
+
const planRoot = getPlanningRoot(cwd);
|
|
81
|
+
for (const { file, header } of checks) {
|
|
82
|
+
const filePath = path.join(planRoot, file);
|
|
68
83
|
try {
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
71
|
-
} catch {
|
|
72
|
-
// Malformed JSON -- fall through to next check
|
|
73
|
-
}
|
|
74
|
-
// dgs.config.json exists but no planningRoot: '.' or malformed
|
|
75
|
-
return dotPlanning;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Step 3: Auto-detect root layout -- PROJECT.md at root, no .planning/ dir
|
|
79
|
-
if (
|
|
80
|
-
fs.existsSync(path.join(resolved, 'PROJECT.md')) &&
|
|
81
|
-
!fs.existsSync(dotPlanning)
|
|
82
|
-
) {
|
|
83
|
-
return resolved;
|
|
84
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
85
|
+
if (content.startsWith(header)) return true;
|
|
86
|
+
} catch { /* File doesn't exist or unreadable */ }
|
|
84
87
|
}
|
|
85
|
-
|
|
86
|
-
// Step 4: Default -- .planning/
|
|
87
|
-
return dotPlanning;
|
|
88
|
+
return false;
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
/**
|
|
@@ -99,7 +100,6 @@ function getPaths(cwd) {
|
|
|
99
100
|
if (_cache.has(resolved)) return _cache.get(resolved);
|
|
100
101
|
|
|
101
102
|
const root = getPlanningRoot(resolved);
|
|
102
|
-
const isRoot = (root === resolved);
|
|
103
103
|
|
|
104
104
|
const paths = Object.freeze({
|
|
105
105
|
ROOT: root,
|
|
@@ -112,14 +112,12 @@ function getPaths(cwd) {
|
|
|
112
112
|
MILESTONES: path.join(root, 'milestones'),
|
|
113
113
|
CONFIG: path.join(root, 'config.json'),
|
|
114
114
|
CONFIG_LOCAL: path.join(root, 'config.local.json'),
|
|
115
|
-
CONFIG_LEGACY: path.join(root, 'dgs.config.json'),
|
|
116
115
|
QUICK: path.join(root, 'quick'),
|
|
117
116
|
TODOS: path.join(root, 'todos'),
|
|
118
117
|
RESEARCH: path.join(root, 'research'),
|
|
119
118
|
DEBUG: path.join(root, 'debug'),
|
|
120
119
|
ARCHIVE: path.join(root, 'archive'),
|
|
121
120
|
PROJECTS: path.join(root, PROJECTS_DIR),
|
|
122
|
-
LAYOUT: isRoot ? 'root' : 'dotplanning',
|
|
123
121
|
});
|
|
124
122
|
|
|
125
123
|
_cache.set(resolved, paths);
|
|
@@ -138,10 +136,12 @@ function initPaths(cwd) {
|
|
|
138
136
|
}
|
|
139
137
|
|
|
140
138
|
/**
|
|
141
|
-
* Clear all cached
|
|
142
|
-
*
|
|
139
|
+
* Clear all cached values.
|
|
140
|
+
* Clears both the per-process getPlanningRoot cache and the per-cwd
|
|
141
|
+
* getPaths cache. Call in afterEach for test isolation between test cases.
|
|
143
142
|
*/
|
|
144
143
|
function resetPaths() {
|
|
144
|
+
_planningRootCache = null;
|
|
145
145
|
_cache.clear();
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -150,5 +150,6 @@ module.exports = {
|
|
|
150
150
|
getPaths,
|
|
151
151
|
initPaths,
|
|
152
152
|
resetPaths,
|
|
153
|
+
isV2Install,
|
|
153
154
|
PROJECTS_DIR,
|
|
154
155
|
};
|