@ktpartners/dgs-platform 2.7.5 → 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 +16 -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 +2 -2
- package/deliver-great-systems/workflows/run-job.md +56 -0
- package/package.json +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for paths.cjs
|
|
2
|
+
* Tests for paths.cjs -- Planning root detection and PATHS singleton
|
|
3
3
|
*
|
|
4
4
|
* Uses Node.js built-in test runner (node:test) and assert (node:assert/strict).
|
|
5
|
-
* Each test creates an isolated temp
|
|
5
|
+
* Each test creates an isolated temp git repo fixture and cleans up after.
|
|
6
6
|
*
|
|
7
|
-
* Covers PATH-01 through PATH-
|
|
7
|
+
* Covers PATH-01 through PATH-06, OPT-01 requirements.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const { describe, it, afterEach } = require('node:test');
|
|
@@ -12,22 +12,42 @@ const assert = require('node:assert/strict');
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const os = require('os');
|
|
15
|
+
const { execSync } = require('child_process');
|
|
15
16
|
|
|
16
|
-
const { getPlanningRoot, getPaths, initPaths, resetPaths, PROJECTS_DIR } = require('./paths.cjs');
|
|
17
|
+
const { getPlanningRoot, getPaths, initPaths, resetPaths, isV2Install, PROJECTS_DIR } = require('./paths.cjs');
|
|
18
|
+
const { rejectV1Install } = require('./migration.cjs');
|
|
17
19
|
|
|
18
20
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
19
21
|
|
|
22
|
+
const ISOLATED_ENV = Object.assign({}, process.env, {
|
|
23
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
24
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function makeGitTempDir() {
|
|
28
|
+
// Use realpathSync to resolve macOS /var -> /private/var symlink so
|
|
29
|
+
// comparisons with git rev-parse --show-toplevel output match.
|
|
30
|
+
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'dgs-paths-test-')));
|
|
31
|
+
execSync('git init', { cwd: dir, stdio: 'pipe', env: ISOLATED_ENV });
|
|
32
|
+
execSync('git config user.name "Test"', { cwd: dir, stdio: 'pipe', env: ISOLATED_ENV });
|
|
33
|
+
execSync('git config user.email "test@test.com"', { cwd: dir, stdio: 'pipe', env: ISOLATED_ENV });
|
|
34
|
+
// Create initial commit so git rev-parse works
|
|
35
|
+
fs.writeFileSync(path.join(dir, '.gitkeep'), '');
|
|
36
|
+
execSync('git add .gitkeep && git commit -m "init"', { cwd: dir, stdio: 'pipe', env: ISOLATED_ENV });
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
function makeTempDir() {
|
|
21
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), 'dgs-paths-test-'));
|
|
41
|
+
return fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'dgs-paths-test-')));
|
|
22
42
|
}
|
|
23
43
|
|
|
24
44
|
function removeTempDir(dir) {
|
|
25
45
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
26
46
|
}
|
|
27
47
|
|
|
28
|
-
// ─── 1.
|
|
48
|
+
// ─── 1. getPlanningRoot -- git rev-parse (PATH-01) ──────────────────────────
|
|
29
49
|
|
|
30
|
-
describe('
|
|
50
|
+
describe('getPlanningRoot -- git rev-parse', () => {
|
|
31
51
|
let cwd;
|
|
32
52
|
|
|
33
53
|
afterEach(() => {
|
|
@@ -36,38 +56,30 @@ describe('detection cascade — .planning/ layout', () => {
|
|
|
36
56
|
cwd = null;
|
|
37
57
|
});
|
|
38
58
|
|
|
39
|
-
it('
|
|
40
|
-
cwd =
|
|
41
|
-
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
42
|
-
fs.writeFileSync(path.join(cwd, '.planning', 'dgs.config.json'), '{}');
|
|
43
|
-
|
|
59
|
+
it('returns git repo root for a git-initialized directory', () => {
|
|
60
|
+
cwd = makeGitTempDir();
|
|
44
61
|
const root = getPlanningRoot(cwd);
|
|
45
|
-
assert.equal(root,
|
|
62
|
+
assert.equal(root, cwd);
|
|
46
63
|
});
|
|
47
64
|
|
|
48
|
-
it('
|
|
49
|
-
cwd =
|
|
50
|
-
|
|
51
|
-
fs.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
assert.equal(root, path.join(cwd, '.planning'));
|
|
65
|
+
it('returns git repo root from a subdirectory', () => {
|
|
66
|
+
cwd = makeGitTempDir();
|
|
67
|
+
const sub = path.join(cwd, 'a', 'b', 'c');
|
|
68
|
+
fs.mkdirSync(sub, { recursive: true });
|
|
69
|
+
const root = getPlanningRoot(sub);
|
|
70
|
+
assert.equal(root, cwd);
|
|
55
71
|
});
|
|
56
72
|
|
|
57
|
-
it('
|
|
58
|
-
cwd =
|
|
59
|
-
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
60
|
-
fs.writeFileSync(path.join(cwd, '.planning', 'dgs.config.json'), '{}');
|
|
61
|
-
fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
|
|
62
|
-
|
|
73
|
+
it('returns absolute path', () => {
|
|
74
|
+
cwd = makeGitTempDir();
|
|
63
75
|
const root = getPlanningRoot(cwd);
|
|
64
|
-
assert.
|
|
76
|
+
assert.ok(path.isAbsolute(root), 'Expected absolute path');
|
|
65
77
|
});
|
|
66
78
|
});
|
|
67
79
|
|
|
68
|
-
// ─── 2.
|
|
80
|
+
// ─── 2. getPlanningRoot -- per-process caching (OPT-01) ─────────────────────
|
|
69
81
|
|
|
70
|
-
describe('
|
|
82
|
+
describe('getPlanningRoot -- per-process cache', () => {
|
|
71
83
|
let cwd;
|
|
72
84
|
|
|
73
85
|
afterEach(() => {
|
|
@@ -76,59 +88,56 @@ describe('detection cascade — root layout', () => {
|
|
|
76
88
|
cwd = null;
|
|
77
89
|
});
|
|
78
90
|
|
|
79
|
-
it('
|
|
80
|
-
cwd =
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
const root = getPlanningRoot(cwd);
|
|
87
|
-
assert.equal(root, cwd);
|
|
91
|
+
it('returns cached value on subsequent calls', () => {
|
|
92
|
+
cwd = makeGitTempDir();
|
|
93
|
+
const first = getPlanningRoot(cwd);
|
|
94
|
+
// Second call with different cwd should still return cached value
|
|
95
|
+
const second = getPlanningRoot('/some/other/path');
|
|
96
|
+
assert.equal(first, second, 'Expected cached value to be returned');
|
|
88
97
|
});
|
|
89
98
|
|
|
90
|
-
it('
|
|
91
|
-
cwd =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
const root = getPlanningRoot(cwd);
|
|
98
|
-
assert.equal(root, path.join(cwd, '.planning'));
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('falls through when root dgs.config.json has malformed JSON', () => {
|
|
102
|
-
cwd = makeTempDir();
|
|
103
|
-
fs.writeFileSync(path.join(cwd, 'dgs.config.json'), '{not valid json!!!');
|
|
104
|
-
|
|
105
|
-
// Should NOT crash — falls through to default
|
|
106
|
-
const root = getPlanningRoot(cwd);
|
|
107
|
-
// Malformed config falls through; no PROJECT.md => default .planning/
|
|
108
|
-
assert.equal(root, path.join(cwd, '.planning'));
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('auto-detects root layout when PROJECT.md exists at root with no .planning/ dir', () => {
|
|
112
|
-
cwd = makeTempDir();
|
|
113
|
-
fs.writeFileSync(path.join(cwd, 'PROJECT.md'), '# Project: Test');
|
|
114
|
-
|
|
115
|
-
const root = getPlanningRoot(cwd);
|
|
116
|
-
assert.equal(root, cwd);
|
|
99
|
+
it('resetPaths clears the cache so next call re-detects', () => {
|
|
100
|
+
cwd = makeGitTempDir();
|
|
101
|
+
const first = getPlanningRoot(cwd);
|
|
102
|
+
resetPaths();
|
|
103
|
+
// After reset, calling with same cwd should re-detect (same result, but not from cache)
|
|
104
|
+
const second = getPlanningRoot(cwd);
|
|
105
|
+
assert.equal(first, second, 'Expected same root after cache clear + re-detect');
|
|
117
106
|
});
|
|
107
|
+
});
|
|
118
108
|
|
|
119
|
-
|
|
120
|
-
cwd = makeTempDir();
|
|
121
|
-
fs.writeFileSync(path.join(cwd, 'PROJECT.md'), '# Project: Test');
|
|
122
|
-
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
109
|
+
// ─── 3. getPlanningRoot -- non-git directory ────────────────────────────────
|
|
123
110
|
|
|
124
|
-
|
|
125
|
-
|
|
111
|
+
describe('getPlanningRoot -- non-git directory', () => {
|
|
112
|
+
it('exits with error when not in a git repo (subprocess)', () => {
|
|
113
|
+
const dir = makeTempDir();
|
|
114
|
+
try {
|
|
115
|
+
const script = `
|
|
116
|
+
const { getPlanningRoot } = require(${JSON.stringify(path.join(__dirname, 'paths.cjs'))});
|
|
117
|
+
getPlanningRoot(${JSON.stringify(dir)});
|
|
118
|
+
`;
|
|
119
|
+
const tmpScript = path.join(dir, 'test-no-git.cjs');
|
|
120
|
+
fs.writeFileSync(tmpScript, script);
|
|
121
|
+
try {
|
|
122
|
+
execSync(`node ${JSON.stringify(tmpScript)}`, {
|
|
123
|
+
encoding: 'utf-8',
|
|
124
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
125
|
+
env: ISOLATED_ENV,
|
|
126
|
+
});
|
|
127
|
+
assert.fail('Expected process to exit with error');
|
|
128
|
+
} catch (err) {
|
|
129
|
+
assert.ok(err.status === 1, `Expected exit code 1, got ${err.status}`);
|
|
130
|
+
assert.ok(err.stderr.includes('DGS requires a git repository'), 'Expected git repo error message');
|
|
131
|
+
}
|
|
132
|
+
} finally {
|
|
133
|
+
removeTempDir(dir);
|
|
134
|
+
}
|
|
126
135
|
});
|
|
127
136
|
});
|
|
128
137
|
|
|
129
|
-
// ───
|
|
138
|
+
// ─── 4. rejectV1Install ─────────────────────────────────────────────────────
|
|
130
139
|
|
|
131
|
-
describe('
|
|
140
|
+
describe('rejectV1Install', () => {
|
|
132
141
|
let cwd;
|
|
133
142
|
|
|
134
143
|
afterEach(() => {
|
|
@@ -137,17 +146,60 @@ describe('detection cascade — default', () => {
|
|
|
137
146
|
cwd = null;
|
|
138
147
|
});
|
|
139
148
|
|
|
140
|
-
it('
|
|
141
|
-
cwd =
|
|
149
|
+
it('does nothing when no .planning/PROJECT.md exists', () => {
|
|
150
|
+
cwd = makeGitTempDir();
|
|
151
|
+
// No error, no exit
|
|
152
|
+
rejectV1Install(cwd);
|
|
153
|
+
});
|
|
142
154
|
|
|
143
|
-
|
|
144
|
-
|
|
155
|
+
it('does nothing when .planning/PROJECT.md exists with PROJECTS.md at root', () => {
|
|
156
|
+
cwd = makeGitTempDir();
|
|
157
|
+
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
158
|
+
fs.writeFileSync(path.join(cwd, '.planning', 'PROJECT.md'), '# Project');
|
|
159
|
+
fs.writeFileSync(path.join(cwd, 'PROJECTS.md'), '# Projects');
|
|
160
|
+
rejectV1Install(cwd);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('does nothing when .planning/PROJECT.md exists with REPOS.md at root', () => {
|
|
164
|
+
cwd = makeGitTempDir();
|
|
165
|
+
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
166
|
+
fs.writeFileSync(path.join(cwd, '.planning', 'PROJECT.md'), '# Project');
|
|
167
|
+
fs.writeFileSync(path.join(cwd, 'REPOS.md'), '# Repos');
|
|
168
|
+
rejectV1Install(cwd);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('exits with code 1 and writes V1_UNSUPPORTED.md for V1 layout (subprocess)', () => {
|
|
172
|
+
cwd = makeGitTempDir();
|
|
173
|
+
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
174
|
+
fs.writeFileSync(path.join(cwd, '.planning', 'PROJECT.md'), '# Project');
|
|
175
|
+
|
|
176
|
+
const script = `
|
|
177
|
+
const { rejectV1Install } = require(${JSON.stringify(path.join(__dirname, 'migration.cjs'))});
|
|
178
|
+
rejectV1Install(${JSON.stringify(cwd)});
|
|
179
|
+
process.stdout.write('should-not-reach');
|
|
180
|
+
`;
|
|
181
|
+
const tmpScript = path.join(cwd, 'test-v1-reject.cjs');
|
|
182
|
+
fs.writeFileSync(tmpScript, script);
|
|
183
|
+
try {
|
|
184
|
+
execSync(`node ${JSON.stringify(tmpScript)}`, {
|
|
185
|
+
encoding: 'utf-8',
|
|
186
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
187
|
+
env: ISOLATED_ENV,
|
|
188
|
+
});
|
|
189
|
+
assert.fail('Expected process to exit with error');
|
|
190
|
+
} catch (err) {
|
|
191
|
+
assert.equal(err.status, 1, 'Expected exit code 1');
|
|
192
|
+
assert.ok(err.stderr.includes('V1 single-repo layout is no longer supported'), 'Expected V1 rejection error');
|
|
193
|
+
assert.ok(!err.stdout.includes('should-not-reach'), 'Process should have exited before reaching stdout write');
|
|
194
|
+
}
|
|
195
|
+
// V1_UNSUPPORTED.md should have been written
|
|
196
|
+
assert.ok(fs.existsSync(path.join(cwd, 'V1_UNSUPPORTED.md')), 'Expected V1_UNSUPPORTED.md to be written');
|
|
145
197
|
});
|
|
146
198
|
});
|
|
147
199
|
|
|
148
|
-
// ───
|
|
200
|
+
// ─── 5. isV2Install ─────────────────────────────────────────────────────────
|
|
149
201
|
|
|
150
|
-
describe('
|
|
202
|
+
describe('isV2Install', () => {
|
|
151
203
|
let cwd;
|
|
152
204
|
|
|
153
205
|
afterEach(() => {
|
|
@@ -156,23 +208,31 @@ describe('detection cascade — priority', () => {
|
|
|
156
208
|
cwd = null;
|
|
157
209
|
});
|
|
158
210
|
|
|
159
|
-
it('.
|
|
160
|
-
cwd =
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
// Root layout signal
|
|
165
|
-
fs.writeFileSync(
|
|
166
|
-
path.join(cwd, 'dgs.config.json'),
|
|
167
|
-
JSON.stringify({ planningRoot: '.' })
|
|
168
|
-
);
|
|
211
|
+
it('returns true when PROJECTS.md with correct header exists at repo root', () => {
|
|
212
|
+
cwd = makeGitTempDir();
|
|
213
|
+
fs.writeFileSync(path.join(cwd, 'PROJECTS.md'), '# Projects\n\n| Project | Status |');
|
|
214
|
+
assert.equal(isV2Install(cwd), true);
|
|
215
|
+
});
|
|
169
216
|
|
|
170
|
-
|
|
171
|
-
|
|
217
|
+
it('returns true when REPOS.md with correct header exists at repo root', () => {
|
|
218
|
+
cwd = makeGitTempDir();
|
|
219
|
+
fs.writeFileSync(path.join(cwd, 'REPOS.md'), '# Repos\n\n| Name | Path |');
|
|
220
|
+
assert.equal(isV2Install(cwd), true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('returns false when neither marker file exists', () => {
|
|
224
|
+
cwd = makeGitTempDir();
|
|
225
|
+
assert.equal(isV2Install(cwd), false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('returns false when PROJECTS.md has wrong header', () => {
|
|
229
|
+
cwd = makeGitTempDir();
|
|
230
|
+
fs.writeFileSync(path.join(cwd, 'PROJECTS.md'), 'Not a projects file');
|
|
231
|
+
assert.equal(isV2Install(cwd), false);
|
|
172
232
|
});
|
|
173
233
|
});
|
|
174
234
|
|
|
175
|
-
// ───
|
|
235
|
+
// ─── 6. PATHS object shape (PATH-03) ────────────────────────────────────────
|
|
176
236
|
|
|
177
237
|
describe('PATHS object shape', () => {
|
|
178
238
|
let cwd;
|
|
@@ -185,49 +245,26 @@ describe('PATHS object shape', () => {
|
|
|
185
245
|
|
|
186
246
|
const EXPECTED_KEYS = [
|
|
187
247
|
'ROOT', 'PHASES', 'IDEAS', 'SPECS', 'JOBS', 'DOCS',
|
|
188
|
-
'CODEBASE', 'MILESTONES', 'CONFIG', 'CONFIG_LOCAL',
|
|
189
|
-
'QUICK', 'TODOS', 'RESEARCH', 'DEBUG', 'ARCHIVE', 'PROJECTS',
|
|
248
|
+
'CODEBASE', 'MILESTONES', 'CONFIG', 'CONFIG_LOCAL',
|
|
249
|
+
'QUICK', 'TODOS', 'RESEARCH', 'DEBUG', 'ARCHIVE', 'PROJECTS',
|
|
190
250
|
];
|
|
191
251
|
|
|
192
|
-
it('returns object with all
|
|
193
|
-
cwd =
|
|
194
|
-
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
195
|
-
fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
|
|
196
|
-
|
|
252
|
+
it('returns object with all 16 expected keys', () => {
|
|
253
|
+
cwd = makeGitTempDir();
|
|
197
254
|
const paths = getPaths(cwd);
|
|
198
255
|
assert.deepEqual(Object.keys(paths).sort(), EXPECTED_KEYS.slice().sort());
|
|
199
256
|
});
|
|
200
257
|
|
|
201
|
-
it('
|
|
202
|
-
cwd =
|
|
203
|
-
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
204
|
-
fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
|
|
205
|
-
|
|
206
|
-
const paths = getPaths(cwd);
|
|
207
|
-
assert.ok(paths.ROOT.endsWith('.planning'));
|
|
208
|
-
assert.equal(paths.LAYOUT, 'dotplanning');
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('for root layout: ROOT equals cwd and LAYOUT is root', () => {
|
|
212
|
-
cwd = makeTempDir();
|
|
213
|
-
fs.writeFileSync(
|
|
214
|
-
path.join(cwd, 'dgs.config.json'),
|
|
215
|
-
JSON.stringify({ planningRoot: '.' })
|
|
216
|
-
);
|
|
217
|
-
|
|
258
|
+
it('ROOT equals git repo root', () => {
|
|
259
|
+
cwd = makeGitTempDir();
|
|
218
260
|
const paths = getPaths(cwd);
|
|
219
261
|
assert.equal(paths.ROOT, cwd);
|
|
220
|
-
assert.equal(paths.LAYOUT, 'root');
|
|
221
262
|
});
|
|
222
263
|
|
|
223
|
-
it('all path values
|
|
224
|
-
cwd =
|
|
225
|
-
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
226
|
-
fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
|
|
227
|
-
|
|
264
|
+
it('all path values are absolute', () => {
|
|
265
|
+
cwd = makeGitTempDir();
|
|
228
266
|
const paths = getPaths(cwd);
|
|
229
267
|
for (const [key, value] of Object.entries(paths)) {
|
|
230
|
-
if (key === 'LAYOUT') continue;
|
|
231
268
|
assert.ok(
|
|
232
269
|
path.isAbsolute(value),
|
|
233
270
|
`Expected ${key} to be absolute, got: ${value}`
|
|
@@ -236,38 +273,19 @@ describe('PATHS object shape', () => {
|
|
|
236
273
|
});
|
|
237
274
|
|
|
238
275
|
it('CONFIG ends with config.json', () => {
|
|
239
|
-
cwd =
|
|
240
|
-
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
241
|
-
fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
|
|
242
|
-
|
|
276
|
+
cwd = makeGitTempDir();
|
|
243
277
|
const paths = getPaths(cwd);
|
|
244
278
|
assert.ok(paths.CONFIG.endsWith('config.json'));
|
|
245
|
-
assert.ok(!paths.CONFIG.endsWith('dgs.config.json'));
|
|
246
279
|
});
|
|
247
280
|
|
|
248
281
|
it('CONFIG_LOCAL ends with config.local.json', () => {
|
|
249
|
-
cwd =
|
|
250
|
-
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
251
|
-
fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
|
|
252
|
-
|
|
282
|
+
cwd = makeGitTempDir();
|
|
253
283
|
const paths = getPaths(cwd);
|
|
254
284
|
assert.ok(paths.CONFIG_LOCAL.endsWith('config.local.json'));
|
|
255
285
|
});
|
|
256
286
|
|
|
257
|
-
it('CONFIG_LEGACY ends with dgs.config.json', () => {
|
|
258
|
-
cwd = makeTempDir();
|
|
259
|
-
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
260
|
-
fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
|
|
261
|
-
|
|
262
|
-
const paths = getPaths(cwd);
|
|
263
|
-
assert.ok(paths.CONFIG_LEGACY.endsWith('dgs.config.json'));
|
|
264
|
-
});
|
|
265
|
-
|
|
266
287
|
it('subdirectory paths are derived from ROOT', () => {
|
|
267
|
-
cwd =
|
|
268
|
-
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
269
|
-
fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
|
|
270
|
-
|
|
288
|
+
cwd = makeGitTempDir();
|
|
271
289
|
const paths = getPaths(cwd);
|
|
272
290
|
assert.equal(paths.PHASES, path.join(paths.ROOT, 'phases'));
|
|
273
291
|
assert.equal(paths.IDEAS, path.join(paths.ROOT, 'ideas'));
|
|
@@ -283,9 +301,16 @@ describe('PATHS object shape', () => {
|
|
|
283
301
|
assert.equal(paths.ARCHIVE, path.join(paths.ROOT, 'archive'));
|
|
284
302
|
assert.equal(paths.PROJECTS, path.join(paths.ROOT, 'projects'));
|
|
285
303
|
});
|
|
304
|
+
|
|
305
|
+
it('does not contain LAYOUT or CONFIG_LEGACY keys', () => {
|
|
306
|
+
cwd = makeGitTempDir();
|
|
307
|
+
const paths = getPaths(cwd);
|
|
308
|
+
assert.equal('LAYOUT' in paths, false, 'LAYOUT should not exist in paths object');
|
|
309
|
+
assert.equal('CONFIG_LEGACY' in paths, false, 'CONFIG_LEGACY should not exist in paths object');
|
|
310
|
+
});
|
|
286
311
|
});
|
|
287
312
|
|
|
288
|
-
// ───
|
|
313
|
+
// ─── 7. Object.freeze (PATH-03) ─────────────────────────────────────────────
|
|
289
314
|
|
|
290
315
|
describe('Object.freeze', () => {
|
|
291
316
|
let cwd;
|
|
@@ -297,19 +322,13 @@ describe('Object.freeze', () => {
|
|
|
297
322
|
});
|
|
298
323
|
|
|
299
324
|
it('getPaths returns a frozen object', () => {
|
|
300
|
-
cwd =
|
|
301
|
-
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
302
|
-
fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
|
|
303
|
-
|
|
325
|
+
cwd = makeGitTempDir();
|
|
304
326
|
const paths = getPaths(cwd);
|
|
305
327
|
assert.equal(Object.isFrozen(paths), true);
|
|
306
328
|
});
|
|
307
329
|
|
|
308
330
|
it('attempting to add a property does not modify the object', () => {
|
|
309
|
-
cwd =
|
|
310
|
-
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
311
|
-
fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
|
|
312
|
-
|
|
331
|
+
cwd = makeGitTempDir();
|
|
313
332
|
const paths = getPaths(cwd);
|
|
314
333
|
try {
|
|
315
334
|
paths.NEW_PROP = 'value';
|
|
@@ -320,7 +339,7 @@ describe('Object.freeze', () => {
|
|
|
320
339
|
});
|
|
321
340
|
});
|
|
322
341
|
|
|
323
|
-
// ───
|
|
342
|
+
// ─── 8. Caching (PATH-03, OPT-01) ──────────────────────────────────────────
|
|
324
343
|
|
|
325
344
|
describe('caching', () => {
|
|
326
345
|
let cwdA, cwdB;
|
|
@@ -333,35 +352,15 @@ describe('caching', () => {
|
|
|
333
352
|
cwdB = null;
|
|
334
353
|
});
|
|
335
354
|
|
|
336
|
-
it('same cwd returns same object (referential equality)', () => {
|
|
337
|
-
cwdA =
|
|
338
|
-
fs.mkdirSync(path.join(cwdA, '.planning'), { recursive: true });
|
|
339
|
-
fs.writeFileSync(path.join(cwdA, '.planning', 'config.json'), '{}');
|
|
340
|
-
|
|
355
|
+
it('same cwd returns same getPaths object (referential equality)', () => {
|
|
356
|
+
cwdA = makeGitTempDir();
|
|
341
357
|
const first = getPaths(cwdA);
|
|
342
358
|
const second = getPaths(cwdA);
|
|
343
359
|
assert.equal(first === second, true, 'Expected referential equality');
|
|
344
360
|
});
|
|
345
361
|
|
|
346
|
-
it('
|
|
347
|
-
cwdA =
|
|
348
|
-
fs.mkdirSync(path.join(cwdA, '.planning'), { recursive: true });
|
|
349
|
-
fs.writeFileSync(path.join(cwdA, '.planning', 'config.json'), '{}');
|
|
350
|
-
|
|
351
|
-
cwdB = makeTempDir();
|
|
352
|
-
fs.mkdirSync(path.join(cwdB, '.planning'), { recursive: true });
|
|
353
|
-
fs.writeFileSync(path.join(cwdB, '.planning', 'config.json'), '{}');
|
|
354
|
-
|
|
355
|
-
const pathsA = getPaths(cwdA);
|
|
356
|
-
const pathsB = getPaths(cwdB);
|
|
357
|
-
assert.notEqual(pathsA === pathsB, true, 'Expected different objects');
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it('resetPaths clears the cache (new reference after reset)', () => {
|
|
361
|
-
cwdA = makeTempDir();
|
|
362
|
-
fs.mkdirSync(path.join(cwdA, '.planning'), { recursive: true });
|
|
363
|
-
fs.writeFileSync(path.join(cwdA, '.planning', 'config.json'), '{}');
|
|
364
|
-
|
|
362
|
+
it('resetPaths clears the getPaths cache (new reference after reset)', () => {
|
|
363
|
+
cwdA = makeGitTempDir();
|
|
365
364
|
const before = getPaths(cwdA);
|
|
366
365
|
resetPaths();
|
|
367
366
|
const after = getPaths(cwdA);
|
|
@@ -369,7 +368,7 @@ describe('caching', () => {
|
|
|
369
368
|
});
|
|
370
369
|
});
|
|
371
370
|
|
|
372
|
-
// ───
|
|
371
|
+
// ─── 9. initPaths / resetPaths (PATH-04) ─────────────────────────────────────
|
|
373
372
|
|
|
374
373
|
describe('initPaths / resetPaths', () => {
|
|
375
374
|
let cwd;
|
|
@@ -381,20 +380,14 @@ describe('initPaths / resetPaths', () => {
|
|
|
381
380
|
});
|
|
382
381
|
|
|
383
382
|
it('initPaths returns the same PATHS object as getPaths', () => {
|
|
384
|
-
cwd =
|
|
385
|
-
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
386
|
-
fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
|
|
387
|
-
|
|
383
|
+
cwd = makeGitTempDir();
|
|
388
384
|
const fromInit = initPaths(cwd);
|
|
389
385
|
const fromGet = getPaths(cwd);
|
|
390
386
|
assert.equal(fromInit === fromGet, true, 'initPaths and getPaths should return same reference');
|
|
391
387
|
});
|
|
392
388
|
|
|
393
389
|
it('resetPaths clears cache (verified by reference comparison)', () => {
|
|
394
|
-
cwd =
|
|
395
|
-
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
396
|
-
fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
|
|
397
|
-
|
|
390
|
+
cwd = makeGitTempDir();
|
|
398
391
|
const before = initPaths(cwd);
|
|
399
392
|
resetPaths();
|
|
400
393
|
const after = initPaths(cwd);
|
|
@@ -402,7 +395,7 @@ describe('initPaths / resetPaths', () => {
|
|
|
402
395
|
});
|
|
403
396
|
});
|
|
404
397
|
|
|
405
|
-
// ───
|
|
398
|
+
// ─── 10. Leaf module constraint (PATH-05) ────────────────────────────────────
|
|
406
399
|
|
|
407
400
|
describe('leaf module constraint', () => {
|
|
408
401
|
it('paths.cjs has no DGS module imports (no require("./") patterns)', () => {
|
|
@@ -410,12 +403,12 @@ describe('leaf module constraint', () => {
|
|
|
410
403
|
path.join(__dirname, 'paths.cjs'),
|
|
411
404
|
'utf-8'
|
|
412
405
|
);
|
|
413
|
-
// Check for require('./anything')
|
|
406
|
+
// Check for require('./anything') -- would indicate DGS module import
|
|
414
407
|
const dgsImports = source.match(/require\s*\(\s*['"]\.\/[^'"]*['"]\s*\)/g);
|
|
415
408
|
assert.equal(dgsImports, null, 'paths.cjs should have zero DGS module imports');
|
|
416
409
|
});
|
|
417
410
|
|
|
418
|
-
it('paths.cjs uses fs and
|
|
411
|
+
it('paths.cjs uses fs, path, and child_process builtins', () => {
|
|
419
412
|
const source = fs.readFileSync(
|
|
420
413
|
path.join(__dirname, 'paths.cjs'),
|
|
421
414
|
'utf-8'
|
|
@@ -428,10 +421,14 @@ describe('leaf module constraint', () => {
|
|
|
428
421
|
/require\s*\(\s*['"]path['"]\s*\)/.test(source),
|
|
429
422
|
'Expected require("path") in paths.cjs'
|
|
430
423
|
);
|
|
424
|
+
assert.ok(
|
|
425
|
+
/require\s*\(\s*['"]child_process['"]\s*\)/.test(source),
|
|
426
|
+
'Expected require("child_process") in paths.cjs'
|
|
427
|
+
);
|
|
431
428
|
});
|
|
432
429
|
});
|
|
433
430
|
|
|
434
|
-
// ───
|
|
431
|
+
// ─── 11. Edge cases ─────────────────────────────────────────────────────────
|
|
435
432
|
|
|
436
433
|
describe('edge cases', () => {
|
|
437
434
|
let cwd;
|
|
@@ -443,11 +440,9 @@ describe('edge cases', () => {
|
|
|
443
440
|
});
|
|
444
441
|
|
|
445
442
|
it('paths returned even when subdirectories do not exist on disk', () => {
|
|
446
|
-
cwd =
|
|
447
|
-
// No .planning/ directory created, no config files — default case
|
|
448
|
-
|
|
443
|
+
cwd = makeGitTempDir();
|
|
449
444
|
const paths = getPaths(cwd);
|
|
450
|
-
// Should
|
|
445
|
+
// Should return all paths even though none of these directories exist
|
|
451
446
|
assert.ok(paths.PHASES);
|
|
452
447
|
assert.ok(paths.IDEAS);
|
|
453
448
|
assert.ok(paths.SPECS);
|
|
@@ -459,38 +454,10 @@ describe('edge cases', () => {
|
|
|
459
454
|
});
|
|
460
455
|
|
|
461
456
|
it('getPaths().PROJECTS returns absolute path ending in /projects', () => {
|
|
462
|
-
cwd =
|
|
463
|
-
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
464
|
-
fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
|
|
465
|
-
|
|
457
|
+
cwd = makeGitTempDir();
|
|
466
458
|
const paths = getPaths(cwd);
|
|
467
459
|
assert.ok(path.isAbsolute(paths.PROJECTS), 'Expected PROJECTS to be absolute');
|
|
468
460
|
assert.ok(paths.PROJECTS.endsWith('/projects') || paths.PROJECTS.endsWith('\\projects'),
|
|
469
461
|
'Expected PROJECTS path to end with /projects');
|
|
470
462
|
});
|
|
471
|
-
|
|
472
|
-
it('relative cwd is normalized to absolute path', () => {
|
|
473
|
-
// This test verifies that passing a relative cwd gives the same result
|
|
474
|
-
// as passing the equivalent absolute path
|
|
475
|
-
cwd = makeTempDir();
|
|
476
|
-
fs.mkdirSync(path.join(cwd, '.planning'), { recursive: true });
|
|
477
|
-
fs.writeFileSync(path.join(cwd, '.planning', 'config.json'), '{}');
|
|
478
|
-
|
|
479
|
-
// Get paths with absolute cwd
|
|
480
|
-
const absResult = getPaths(cwd);
|
|
481
|
-
resetPaths();
|
|
482
|
-
|
|
483
|
-
// Calling with '.' should resolve to process.cwd(), which is different from
|
|
484
|
-
// our temp dir, but the key behavior is that relative paths get resolved
|
|
485
|
-
// to absolute via path.resolve()
|
|
486
|
-
const dotResult = getPaths('.');
|
|
487
|
-
// The important assertion: all returned paths are absolute regardless of input
|
|
488
|
-
for (const [key, value] of Object.entries(dotResult)) {
|
|
489
|
-
if (key === 'LAYOUT') continue;
|
|
490
|
-
assert.ok(
|
|
491
|
-
path.isAbsolute(value),
|
|
492
|
-
`Expected ${key} to be absolute even with relative cwd input`
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
});
|
|
496
463
|
});
|
|
@@ -10,27 +10,28 @@ const { writeStateMd } = require('./state.cjs');
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Resolve the phases directory for the current project context.
|
|
13
|
-
*
|
|
13
|
+
* Returns path.join(cwd, projectRoot, 'phases') where projectRoot is
|
|
14
|
+
* from getProjectRoot() or '.' as fallback.
|
|
14
15
|
*/
|
|
15
16
|
function resolvePhasesDir(cwd) {
|
|
16
17
|
let projectRoot;
|
|
17
18
|
try {
|
|
18
19
|
projectRoot = getProjectRoot(cwd);
|
|
19
20
|
} catch {
|
|
20
|
-
projectRoot = '.
|
|
21
|
+
projectRoot = '.';
|
|
21
22
|
}
|
|
22
23
|
return path.join(cwd, projectRoot, 'phases');
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Resolve the project root path (relative) for the current context.
|
|
27
|
-
*
|
|
28
|
+
* Returns getProjectRoot() result or '.' as fallback.
|
|
28
29
|
*/
|
|
29
30
|
function resolveProjectRoot(cwd) {
|
|
30
31
|
try {
|
|
31
32
|
return getProjectRoot(cwd);
|
|
32
33
|
} catch {
|
|
33
|
-
return '.
|
|
34
|
+
return '.';
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|