@ktpartners/dgs-platform 3.4.2 → 3.5.1
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 +28 -0
- package/README.md +2 -0
- package/agents/dgs-codebase-cross-analyzer.md +1 -1
- package/agents/dgs-codebase-mapper.md +1 -1
- package/agents/dgs-codebase-synthesizer.md +1 -1
- package/agents/dgs-phase-researcher.md +1 -1
- package/bin/install.js +34 -2
- package/deliver-great-systems/bin/dgs-tools.cjs +7 -1
- package/deliver-great-systems/bin/lib/commands.cjs +66 -29
- package/deliver-great-systems/bin/lib/commands.test.cjs +221 -1
- package/deliver-great-systems/bin/lib/context.cjs +6 -6
- package/deliver-great-systems/bin/lib/context.test.cjs +9 -9
- package/deliver-great-systems/bin/lib/core.cjs +199 -9
- package/deliver-great-systems/bin/lib/core.test.cjs +242 -0
- package/deliver-great-systems/bin/lib/execution.cjs +7 -0
- package/deliver-great-systems/bin/lib/governance.cjs +7 -7
- package/deliver-great-systems/bin/lib/init.cjs +25 -17
- package/deliver-great-systems/bin/lib/init.test.cjs +69 -10
- package/deliver-great-systems/bin/lib/jobs.cjs +132 -67
- package/deliver-great-systems/bin/lib/jobs.test.cjs +157 -13
- package/deliver-great-systems/bin/lib/migration.test.cjs +8 -0
- package/deliver-great-systems/bin/lib/milestone-archival.test.cjs +186 -0
- package/deliver-great-systems/bin/lib/milestone.cjs +168 -37
- package/deliver-great-systems/bin/lib/milestone.test.cjs +113 -1
- package/deliver-great-systems/bin/lib/path-audit.test.cjs +128 -0
- package/deliver-great-systems/bin/lib/paths.cjs +1 -2
- package/deliver-great-systems/bin/lib/paths.test.cjs +3 -4
- package/deliver-great-systems/bin/lib/phase-versioned.test.cjs +134 -0
- package/deliver-great-systems/bin/lib/phase.cjs +60 -7
- package/deliver-great-systems/bin/lib/phase.test.cjs +168 -1
- package/deliver-great-systems/bin/lib/projects.test.cjs +38 -0
- package/deliver-great-systems/bin/lib/repos.cjs +8 -4
- package/deliver-great-systems/bin/lib/repos.test.cjs +6 -2
- package/deliver-great-systems/bin/lib/roadmap.cjs +21 -11
- package/deliver-great-systems/bin/lib/state-snapshot.test.cjs +134 -0
- package/deliver-great-systems/bin/lib/state.cjs +173 -26
- package/deliver-great-systems/references/git-integration.md +1 -1
- package/deliver-great-systems/templates/milestone-archive.md +1 -1
- package/deliver-great-systems/templates/roadmap.md +12 -10
- package/deliver-great-systems/workflows/abandon-milestone.md +8 -1
- package/deliver-great-systems/workflows/abandon-quick.md +1 -1
- package/deliver-great-systems/workflows/codereview.md +1 -1
- package/deliver-great-systems/workflows/complete-milestone.md +1 -1
- package/deliver-great-systems/workflows/execute-phase.md +2 -2
- package/deliver-great-systems/workflows/execute-plan.md +2 -2
- package/deliver-great-systems/workflows/new-milestone.md +46 -12
- package/deliver-great-systems/workflows/quick-abandon.md +1 -1
- package/deliver-great-systems/workflows/quick.md +3 -3
- package/package.json +3 -2
|
@@ -16,6 +16,7 @@ const { createFixture, createTempProject } = require('./test-helpers.cjs');
|
|
|
16
16
|
// Import the functions under test
|
|
17
17
|
const {
|
|
18
18
|
resolveProjectPath,
|
|
19
|
+
phasesDir,
|
|
19
20
|
getProjectRoot,
|
|
20
21
|
requireProjectRoot,
|
|
21
22
|
isV2Install,
|
|
@@ -26,6 +27,7 @@ const {
|
|
|
26
27
|
getProjectDir,
|
|
27
28
|
resolveModelInternal,
|
|
28
29
|
MODEL_PROFILES,
|
|
30
|
+
findPhaseInternal,
|
|
29
31
|
} = require('./core.cjs');
|
|
30
32
|
|
|
31
33
|
// ─── Root layout (no v2 markers) Tests ───────────────────────────────────────
|
|
@@ -145,6 +147,246 @@ describe('v2 mode without current_project (guard trigger)', () => {
|
|
|
145
147
|
});
|
|
146
148
|
});
|
|
147
149
|
|
|
150
|
+
// ─── phasesDir canonical resolver contract Tests ──────────────────────────────
|
|
151
|
+
|
|
152
|
+
describe('phasesDir canonical resolver', () => {
|
|
153
|
+
it('v1/flat layout returns phases', () => {
|
|
154
|
+
const fixture = createFixture({
|
|
155
|
+
'config.json': JSON.stringify({}),
|
|
156
|
+
'STATE.md': '# State',
|
|
157
|
+
'ROADMAP.md': '# Roadmap',
|
|
158
|
+
'phases/': null,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const result = phasesDir(fixture.cwd);
|
|
163
|
+
assert.equal(result, path.join('.', 'phases'));
|
|
164
|
+
assert.equal(result, 'phases');
|
|
165
|
+
} finally {
|
|
166
|
+
fixture.cleanup();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('v2 layout returns projects/<slug>/phases', () => {
|
|
171
|
+
const fixture = createFixture({
|
|
172
|
+
'config.json': JSON.stringify({}),
|
|
173
|
+
'config.local.json': JSON.stringify({ current_project: 'auth-overhaul' }),
|
|
174
|
+
'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
|
|
175
|
+
'REPOS.md': '# Repos\n\n| Name | Path |\n',
|
|
176
|
+
'projects/auth-overhaul/STATE.md': '# State',
|
|
177
|
+
'projects/auth-overhaul/ROADMAP.md': '# Roadmap',
|
|
178
|
+
'projects/auth-overhaul/phases/': null,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const result = phasesDir(fixture.cwd);
|
|
183
|
+
assert.equal(result, path.join('projects', 'auth-overhaul', 'phases'));
|
|
184
|
+
} finally {
|
|
185
|
+
fixture.cleanup();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('v2 install with NO current_project throws (fail-loud, no fallback)', () => {
|
|
190
|
+
const fixture = createFixture({
|
|
191
|
+
'config.json': JSON.stringify({}),
|
|
192
|
+
'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
assert.throws(
|
|
197
|
+
() => phasesDir(fixture.cwd),
|
|
198
|
+
(err) => err.message === 'NO_CURRENT_PROJECT_V2'
|
|
199
|
+
);
|
|
200
|
+
} finally {
|
|
201
|
+
fixture.cleanup();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ─── Version-aware (dual-mode) resolver Tests (Phase 165, RSLV-04) ─────────────
|
|
207
|
+
|
|
208
|
+
describe('phasesDir version-aware (dual-mode)', () => {
|
|
209
|
+
// Shared v2 project context builder. STATE.md frontmatter `current_milestone`
|
|
210
|
+
// is what resolveMilestoneVersion reads; `phasesExtra` lets each test add the
|
|
211
|
+
// versioned sub-directory (or not) to exercise the existsSync branch.
|
|
212
|
+
function v2Fixture(stateBody, phasesExtra) {
|
|
213
|
+
const tree = {
|
|
214
|
+
'config.json': JSON.stringify({}),
|
|
215
|
+
'config.local.json': JSON.stringify({ current_project: 'auth-overhaul' }),
|
|
216
|
+
'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
|
|
217
|
+
'REPOS.md': '# Repos\n\n| Name | Path |\n',
|
|
218
|
+
'projects/auth-overhaul/PROJECT.md': '# Project',
|
|
219
|
+
'projects/auth-overhaul/STATE.md': stateBody,
|
|
220
|
+
'projects/auth-overhaul/ROADMAP.md': '# Roadmap',
|
|
221
|
+
'projects/auth-overhaul/phases/': null,
|
|
222
|
+
};
|
|
223
|
+
if (phasesExtra) tree[phasesExtra] = null;
|
|
224
|
+
return createFixture(tree);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
it('Test A: versioned dir present → returns projects/<slug>/phases/<version>', () => {
|
|
228
|
+
const fixture = v2Fixture(
|
|
229
|
+
'---\ncurrent_milestone: v26.0\n---\n# State',
|
|
230
|
+
'projects/auth-overhaul/phases/v26.0/'
|
|
231
|
+
);
|
|
232
|
+
try {
|
|
233
|
+
const result = phasesDir(fixture.cwd);
|
|
234
|
+
assert.equal(
|
|
235
|
+
result,
|
|
236
|
+
path.join('projects', 'auth-overhaul', 'phases', 'v26.0')
|
|
237
|
+
);
|
|
238
|
+
} finally {
|
|
239
|
+
fixture.cleanup();
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('Test B: version present but NO versioned dir → flat fallback (v25.0 invariant)', () => {
|
|
244
|
+
const fixture = v2Fixture(
|
|
245
|
+
'---\ncurrent_milestone: v25.0\n---\n# State',
|
|
246
|
+
null
|
|
247
|
+
);
|
|
248
|
+
try {
|
|
249
|
+
const result = phasesDir(fixture.cwd);
|
|
250
|
+
assert.equal(result, path.join('projects', 'auth-overhaul', 'phases'));
|
|
251
|
+
assert.ok(!result.includes('v25.0'), `must not resolve versioned: ${result}`);
|
|
252
|
+
} finally {
|
|
253
|
+
fixture.cleanup();
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('Test C: version undeterminable → flat fallback, no throw, never v1.0', () => {
|
|
258
|
+
const fixture = v2Fixture('# State (no milestone frontmatter)', null);
|
|
259
|
+
try {
|
|
260
|
+
let result;
|
|
261
|
+
assert.doesNotThrow(() => { result = phasesDir(fixture.cwd); });
|
|
262
|
+
assert.equal(result, path.join('projects', 'auth-overhaul', 'phases'));
|
|
263
|
+
assert.ok(!result.includes('v1.0'), `must never default to v1.0: ${result}`);
|
|
264
|
+
} finally {
|
|
265
|
+
fixture.cleanup();
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('Test D: flat/v1 layout unchanged (regression guard)', () => {
|
|
270
|
+
const fixture = createFixture({
|
|
271
|
+
'config.json': JSON.stringify({}),
|
|
272
|
+
'STATE.md': '# State',
|
|
273
|
+
'ROADMAP.md': '# Roadmap',
|
|
274
|
+
'phases/': null,
|
|
275
|
+
});
|
|
276
|
+
try {
|
|
277
|
+
const result = phasesDir(fixture.cwd);
|
|
278
|
+
assert.equal(result, 'phases');
|
|
279
|
+
} finally {
|
|
280
|
+
fixture.cleanup();
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ─── findPhaseInternal version-aware archive (LOOK-01) Tests ──────────────────
|
|
286
|
+
|
|
287
|
+
describe('findPhaseInternal version-aware archive (LOOK-01)', () => {
|
|
288
|
+
// Build a v2 project with active phases plus milestone archives. Active phases
|
|
289
|
+
// live (flat, v25.0 invariant) at projects/<slug>/phases/<NN>-slug/; archives
|
|
290
|
+
// live product-level at milestones/<v>-phases/<NN>-slug/. Each phase dir needs
|
|
291
|
+
// at least one *-PLAN.md so searchPhaseInDir matches it.
|
|
292
|
+
function look01Fixture(activePhaseDirs, archives) {
|
|
293
|
+
const tree = {
|
|
294
|
+
'config.json': JSON.stringify({}),
|
|
295
|
+
'config.local.json': JSON.stringify({ current_project: 'auth-overhaul' }),
|
|
296
|
+
'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
|
|
297
|
+
'REPOS.md': '# Repos\n\n| Name | Path |\n',
|
|
298
|
+
'projects/auth-overhaul/PROJECT.md': '# Project',
|
|
299
|
+
'projects/auth-overhaul/STATE.md': '---\ncurrent_milestone: v25.0\n---\n# State',
|
|
300
|
+
'projects/auth-overhaul/ROADMAP.md': '# Roadmap',
|
|
301
|
+
'projects/auth-overhaul/phases/': null,
|
|
302
|
+
};
|
|
303
|
+
for (const dir of activePhaseDirs || []) {
|
|
304
|
+
tree[`projects/auth-overhaul/phases/${dir}/01-PLAN.md`] = '# Plan';
|
|
305
|
+
}
|
|
306
|
+
// archives: { 'v3.0': ['07-foo'], 'v4.0': ['03-b'], ... }
|
|
307
|
+
for (const [version, dirs] of Object.entries(archives || {})) {
|
|
308
|
+
for (const dir of dirs) {
|
|
309
|
+
tree[`milestones/${version}-phases/${dir}/01-PLAN.md`] = '# Plan';
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return createFixture(tree);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
it('Test 1: active phase wins (unchanged) — bare number in active phases', () => {
|
|
316
|
+
const fixture = look01Fixture(
|
|
317
|
+
['03-active-feature'],
|
|
318
|
+
{ 'v3.0': ['03-archived-feature'] }
|
|
319
|
+
);
|
|
320
|
+
try {
|
|
321
|
+
const result = findPhaseInternal(fixture.cwd, '03');
|
|
322
|
+
assert.ok(result, 'expected an active match');
|
|
323
|
+
assert.equal(result.found, true);
|
|
324
|
+
assert.equal(result.archived, undefined, 'active match must not carry .archived');
|
|
325
|
+
assert.equal(result.ambiguous, undefined, 'active match must not be ambiguous');
|
|
326
|
+
assert.ok(result.directory.includes('03-active-feature'));
|
|
327
|
+
} finally {
|
|
328
|
+
fixture.cleanup();
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('Test 2: unique archive (flat-layout preserved) — bare number in exactly ONE archive', () => {
|
|
333
|
+
const fixture = look01Fixture(
|
|
334
|
+
[],
|
|
335
|
+
{ 'v3.0': ['07-foo'] }
|
|
336
|
+
);
|
|
337
|
+
try {
|
|
338
|
+
const result = findPhaseInternal(fixture.cwd, '07');
|
|
339
|
+
assert.ok(result, 'expected a single archived match');
|
|
340
|
+
assert.equal(result.found, true, 'single-archive match preserves found:true');
|
|
341
|
+
assert.equal(result.archived, 'v3.0', 'single-archive match tagged with its version');
|
|
342
|
+
assert.equal(result.ambiguous, undefined, 'single archive is not ambiguous');
|
|
343
|
+
assert.ok(result.directory.includes('07-foo'));
|
|
344
|
+
} finally {
|
|
345
|
+
fixture.cleanup();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('Test 3: cross-milestone collision → structured ambiguity, NOT silent newest', () => {
|
|
350
|
+
const fixture = look01Fixture(
|
|
351
|
+
[],
|
|
352
|
+
{ 'v3.0': ['03-a'], 'v4.0': ['03-b'] }
|
|
353
|
+
);
|
|
354
|
+
try {
|
|
355
|
+
const result = findPhaseInternal(fixture.cwd, '03');
|
|
356
|
+
assert.ok(result, 'expected a structured ambiguity object, not null');
|
|
357
|
+
assert.equal(result.found, false, 'ambiguity is found:false');
|
|
358
|
+
assert.equal(result.ambiguous, true, 'ambiguity flag set');
|
|
359
|
+
assert.equal(result.directory, undefined, 'ambiguity must NOT point at either archive dir');
|
|
360
|
+
assert.equal(result.archived, undefined, 'ambiguity carries no single archived tag');
|
|
361
|
+
assert.ok(Array.isArray(result.matches), 'matches is an array');
|
|
362
|
+
assert.equal(result.matches.length, 2, 'both archives listed');
|
|
363
|
+
const versions = result.matches.map(m => m.milestone).sort();
|
|
364
|
+
assert.deepEqual(versions, ['v3.0', 'v4.0'], 'both milestone versions named');
|
|
365
|
+
assert.ok(typeof result.message === 'string' && result.message.length > 0);
|
|
366
|
+
assert.ok(result.message.includes('v3.0'), 'message names v3.0');
|
|
367
|
+
assert.ok(result.message.includes('v4.0'), 'message names v4.0');
|
|
368
|
+
} finally {
|
|
369
|
+
fixture.cleanup();
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('Test 4: collision with an active number is irrelevant — active wins', () => {
|
|
374
|
+
const fixture = look01Fixture(
|
|
375
|
+
['01-active'],
|
|
376
|
+
{ 'v3.0': ['01-a'], 'v4.0': ['01-b'] }
|
|
377
|
+
);
|
|
378
|
+
try {
|
|
379
|
+
const result = findPhaseInternal(fixture.cwd, '01');
|
|
380
|
+
assert.ok(result, 'expected the active match');
|
|
381
|
+
assert.equal(result.found, true);
|
|
382
|
+
assert.equal(result.ambiguous, undefined, 'active match short-circuits ambiguity');
|
|
383
|
+
assert.ok(result.directory.includes('01-active'));
|
|
384
|
+
} finally {
|
|
385
|
+
fixture.cleanup();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
148
390
|
// ─── Strict v2 Marker Validation Tests ────────────────────────────────────────
|
|
149
391
|
|
|
150
392
|
describe('strict v2 marker validation', () => {
|
|
@@ -404,6 +404,13 @@ function detectRepoChanges(cwd, repoNames, useActiveContext) {
|
|
|
404
404
|
*/
|
|
405
405
|
function createRepoBranches(cwd, repoNames, branchName, config, baseBranch) {
|
|
406
406
|
|
|
407
|
+
// Honour the branching strategy: 'none' disables branch creation entirely.
|
|
408
|
+
// The function accepts config.branching_strategy and must respect it — a
|
|
409
|
+
// 'none' strategy is an explicit signal to create no branches.
|
|
410
|
+
if (config && config.branching_strategy === 'none') {
|
|
411
|
+
return { created: false, reason: 'branching_disabled' };
|
|
412
|
+
}
|
|
413
|
+
|
|
407
414
|
const parsed = parseReposMd(cwd);
|
|
408
415
|
const repos = parsed ? parsed.repos : [];
|
|
409
416
|
const branches = [];
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const { extractNameFromAuthor } = require('./identity.cjs');
|
|
13
|
-
const { getMilestonePhaseFilter, getProjectRoot } = require('./core.cjs');
|
|
13
|
+
const { getMilestonePhaseFilter, getProjectRoot, phasesDir } = require('./core.cjs');
|
|
14
14
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
15
15
|
const { getPlanningRoot } = require('./paths.cjs');
|
|
16
16
|
|
|
@@ -58,23 +58,23 @@ function getContributors(cwd) {
|
|
|
58
58
|
const planRoot = getPlanningRoot(cwd);
|
|
59
59
|
const projectRootRel = getProjectRoot(cwd);
|
|
60
60
|
const projectRoot = path.join(planRoot, projectRootRel);
|
|
61
|
-
const
|
|
61
|
+
const phasesAbs = path.join(cwd, phasesDir(cwd));
|
|
62
62
|
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
63
63
|
|
|
64
|
-
if (fs.existsSync(
|
|
65
|
-
const entries = fs.readdirSync(
|
|
64
|
+
if (fs.existsSync(phasesAbs)) {
|
|
65
|
+
const entries = fs.readdirSync(phasesAbs, { withFileTypes: true });
|
|
66
66
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
67
67
|
|
|
68
68
|
for (const dir of dirs) {
|
|
69
69
|
if (!isDirInMilestone(dir)) continue;
|
|
70
70
|
|
|
71
|
-
const phaseFiles = fs.readdirSync(path.join(
|
|
71
|
+
const phaseFiles = fs.readdirSync(path.join(phasesAbs, dir));
|
|
72
72
|
|
|
73
73
|
// Read SUMMARY.md executed_by
|
|
74
74
|
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
75
75
|
for (const s of summaries) {
|
|
76
76
|
try {
|
|
77
|
-
const content = fs.readFileSync(path.join(
|
|
77
|
+
const content = fs.readFileSync(path.join(phasesAbs, dir, s), 'utf-8');
|
|
78
78
|
const fm = extractFrontmatter(content);
|
|
79
79
|
addContributor(fm.executed_by);
|
|
80
80
|
} catch { /* skip unreadable files */ }
|
|
@@ -84,7 +84,7 @@ function getContributors(cwd) {
|
|
|
84
84
|
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
|
85
85
|
for (const p of plans) {
|
|
86
86
|
try {
|
|
87
|
-
const content = fs.readFileSync(path.join(
|
|
87
|
+
const content = fs.readFileSync(path.join(phasesAbs, dir, p), 'utf-8');
|
|
88
88
|
const fm = extractFrontmatter(content);
|
|
89
89
|
addContributor(fm.created_by);
|
|
90
90
|
} catch { /* skip unreadable files */ }
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { execSync } = require('child_process');
|
|
8
|
-
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, toPosixPath, output, error, resolveProjectPath, getProjectRoot, isV2Install, getProjectFolders, getV2Hint, safeReadFile } = require('./core.cjs');
|
|
8
|
+
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, toPosixPath, output, error, resolveProjectPath, getProjectRoot, phasesDir, isV2Install, getProjectFolders, getV2Hint, safeReadFile, isValidMilestoneVersion } = require('./core.cjs');
|
|
9
9
|
const { requireGitIdentity, formatAuthorString } = require('./identity.cjs');
|
|
10
10
|
const { getPlanningRoot, PROJECTS_DIR } = require('./paths.cjs');
|
|
11
11
|
const { parseReposMd, validateReposMdEager } = require('./repos.cjs');
|
|
@@ -242,7 +242,7 @@ function cmdInitExecutePhase(cwd, phase, raw) {
|
|
|
242
242
|
const config = loadConfig(cwd);
|
|
243
243
|
const ctx = resolveProjectContext(cwd);
|
|
244
244
|
let phaseInfo = findPhaseInternal(cwd, phase);
|
|
245
|
-
if (phaseInfo?.archived) phaseInfo = null;
|
|
245
|
+
if (phaseInfo?.archived || phaseInfo?.ambiguous) phaseInfo = null;
|
|
246
246
|
const milestone = getMilestoneInfo(cwd);
|
|
247
247
|
const planRootRel = path.relative(cwd, getPlanningRoot(cwd)) || '.';
|
|
248
248
|
|
|
@@ -348,7 +348,7 @@ function cmdInitPlanPhase(cwd, phase, raw) {
|
|
|
348
348
|
const config = loadConfig(cwd);
|
|
349
349
|
const ctx = resolveProjectContext(cwd);
|
|
350
350
|
let phaseInfo = findPhaseInternal(cwd, phase);
|
|
351
|
-
if (phaseInfo?.archived) phaseInfo = null;
|
|
351
|
+
if (phaseInfo?.archived || phaseInfo?.ambiguous) phaseInfo = null;
|
|
352
352
|
const planRootRel = path.relative(cwd, getPlanningRoot(cwd)) || '.';
|
|
353
353
|
|
|
354
354
|
const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
|
|
@@ -556,8 +556,11 @@ function cmdInitNewMilestone(cwd, raw) {
|
|
|
556
556
|
cadence_pull: getCadence('new-milestone').pull,
|
|
557
557
|
cadence_push: getCadence('new-milestone').push,
|
|
558
558
|
|
|
559
|
-
// Current milestone
|
|
560
|
-
|
|
559
|
+
// Current milestone — current_milestone is the authoritative structured
|
|
560
|
+
// version signal. new-milestone (this flow) and init.cjs are the SOLE
|
|
561
|
+
// setters. Emit only a grammar-valid value; surface a gap as null rather
|
|
562
|
+
// than persisting an out-of-grammar value (and NEVER coerce to v1.0).
|
|
563
|
+
current_milestone: isValidMilestoneVersion(milestone.version) ? milestone.version : null,
|
|
561
564
|
current_milestone_name: milestone.name,
|
|
562
565
|
|
|
563
566
|
// File existence (project-qualified)
|
|
@@ -747,7 +750,7 @@ function cmdInitVerifyWork(cwd, phase, raw) {
|
|
|
747
750
|
const config = loadConfig(cwd);
|
|
748
751
|
const ctx = resolveProjectContext(cwd);
|
|
749
752
|
let phaseInfo = findPhaseInternal(cwd, phase);
|
|
750
|
-
if (phaseInfo?.archived) phaseInfo = null;
|
|
753
|
+
if (phaseInfo?.archived || phaseInfo?.ambiguous) phaseInfo = null;
|
|
751
754
|
|
|
752
755
|
const result = {
|
|
753
756
|
// Models
|
|
@@ -792,7 +795,7 @@ function cmdInitAuditPhase(cwd, phase, raw) {
|
|
|
792
795
|
const config = loadConfig(cwd);
|
|
793
796
|
const ctx = resolveProjectContext(cwd);
|
|
794
797
|
let phaseInfo = findPhaseInternal(cwd, phase);
|
|
795
|
-
if (phaseInfo?.archived) phaseInfo = null;
|
|
798
|
+
if (phaseInfo?.archived || phaseInfo?.ambiguous) phaseInfo = null;
|
|
796
799
|
|
|
797
800
|
const result = {
|
|
798
801
|
// Models
|
|
@@ -834,7 +837,7 @@ function cmdInitPhaseOp(cwd, phase, raw, workflow) {
|
|
|
834
837
|
const ctx = resolveProjectContext(cwd);
|
|
835
838
|
const planRootRel = path.relative(cwd, getPlanningRoot(cwd)) || '.';
|
|
836
839
|
let phaseInfo = findPhaseInternal(cwd, phase);
|
|
837
|
-
if (phaseInfo?.archived) phaseInfo = null;
|
|
840
|
+
if (phaseInfo?.archived || phaseInfo?.ambiguous) phaseInfo = null;
|
|
838
841
|
const cadence = getCadence(workflow || 'plan-phase');
|
|
839
842
|
|
|
840
843
|
// Fallback to ROADMAP.md if no directory exists (e.g., Plans: TBD)
|
|
@@ -858,6 +861,9 @@ function cmdInitPhaseOp(cwd, phase, raw, workflow) {
|
|
|
858
861
|
}
|
|
859
862
|
}
|
|
860
863
|
|
|
864
|
+
let phasesDirRel;
|
|
865
|
+
try { phasesDirRel = phasesDir(cwd); } catch { phasesDirRel = ctx.root ? path.join(ctx.root, 'phases') : path.join(planRootRel, 'phases'); }
|
|
866
|
+
|
|
861
867
|
const result = {
|
|
862
868
|
// Config
|
|
863
869
|
commit_docs: config.commit_docs,
|
|
@@ -891,7 +897,7 @@ function cmdInitPhaseOp(cwd, phase, raw, workflow) {
|
|
|
891
897
|
roadmap_path: ctx.root ? path.join(ctx.root, 'ROADMAP.md') : path.join(planRootRel, 'ROADMAP.md'),
|
|
892
898
|
requirements_path: ctx.root ? path.join(ctx.root, 'REQUIREMENTS.md') : path.join(planRootRel, 'REQUIREMENTS.md'),
|
|
893
899
|
project_path: ctx.root ? path.join(ctx.root, 'PROJECT.md') : path.join(planRootRel, 'PROJECT.md'),
|
|
894
|
-
phases_dir:
|
|
900
|
+
phases_dir: phasesDirRel,
|
|
895
901
|
|
|
896
902
|
// Author
|
|
897
903
|
author: resolveAuthorSafe(cwd),
|
|
@@ -1055,17 +1061,18 @@ function cmdInitMilestoneOp(cwd, raw, workflow) {
|
|
|
1055
1061
|
// Count phases (project-qualified)
|
|
1056
1062
|
let phaseCount = 0;
|
|
1057
1063
|
let completedPhases = 0;
|
|
1058
|
-
|
|
1059
|
-
|
|
1064
|
+
let phasesBase;
|
|
1065
|
+
try { phasesBase = phasesDir(cwd); } catch { phasesBase = ctx.root ? path.join(ctx.root, 'phases') : path.join(planRootRel, 'phases'); }
|
|
1066
|
+
const phasesAbs = path.join(cwd, phasesBase);
|
|
1060
1067
|
try {
|
|
1061
|
-
const entries = fs.readdirSync(
|
|
1068
|
+
const entries = fs.readdirSync(phasesAbs, { withFileTypes: true });
|
|
1062
1069
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
1063
1070
|
phaseCount = dirs.length;
|
|
1064
1071
|
|
|
1065
1072
|
// Count phases with summaries (completed)
|
|
1066
1073
|
for (const dir of dirs) {
|
|
1067
1074
|
try {
|
|
1068
|
-
const phaseFiles = fs.readdirSync(path.join(
|
|
1075
|
+
const phaseFiles = fs.readdirSync(path.join(phasesAbs, dir));
|
|
1069
1076
|
const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
1070
1077
|
if (hasSummary) completedPhases++;
|
|
1071
1078
|
} catch {}
|
|
@@ -1251,14 +1258,15 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1251
1258
|
const planRootRel = path.relative(cwd, getPlanningRoot(cwd)) || '.';
|
|
1252
1259
|
|
|
1253
1260
|
// Analyze phases (project-qualified)
|
|
1254
|
-
|
|
1255
|
-
|
|
1261
|
+
let phasesBase;
|
|
1262
|
+
try { phasesBase = phasesDir(cwd); } catch { phasesBase = ctx.root ? path.join(ctx.root, 'phases') : path.join(planRootRel, 'phases'); }
|
|
1263
|
+
const phasesAbs = path.join(cwd, phasesBase);
|
|
1256
1264
|
const phases = [];
|
|
1257
1265
|
let currentPhase = null;
|
|
1258
1266
|
let nextPhase = null;
|
|
1259
1267
|
|
|
1260
1268
|
try {
|
|
1261
|
-
const entries = fs.readdirSync(
|
|
1269
|
+
const entries = fs.readdirSync(phasesAbs, { withFileTypes: true });
|
|
1262
1270
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
1263
1271
|
|
|
1264
1272
|
for (const dir of dirs) {
|
|
@@ -1266,7 +1274,7 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1266
1274
|
const phaseNumber = match ? match[1] : dir;
|
|
1267
1275
|
const phaseName = match && match[2] ? match[2] : null;
|
|
1268
1276
|
|
|
1269
|
-
const phasePath = path.join(
|
|
1277
|
+
const phasePath = path.join(phasesAbs, dir);
|
|
1270
1278
|
const phaseFiles = fs.readdirSync(phasePath);
|
|
1271
1279
|
|
|
1272
1280
|
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
|
@@ -1657,16 +1657,6 @@ describe('cmdInitQuick quick_dir resolution', () => {
|
|
|
1657
1657
|
beforeEach(() => {
|
|
1658
1658
|
fixture = createFixture({
|
|
1659
1659
|
'config.json': JSON.stringify({ current_project: 'test-project' }),
|
|
1660
|
-
'config.local.json': JSON.stringify({
|
|
1661
|
-
execution: { active_context: 'v19-git-worktrees' },
|
|
1662
|
-
projects: {
|
|
1663
|
-
'test-project': {
|
|
1664
|
-
worktrees: {
|
|
1665
|
-
'v19-git-worktrees': { type: 'milestone' }
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
}),
|
|
1670
1660
|
'PROJECTS.md': '# Projects\n\n| Project | Status |\n|---------|--------|\n| test-project | Active |\n',
|
|
1671
1661
|
'REPOS.md': '# Repos\n\n| Name | Path |\n|------|------|\n',
|
|
1672
1662
|
'projects/test-project/STATE.md': '# State',
|
|
@@ -1676,6 +1666,35 @@ describe('cmdInitQuick quick_dir resolution', () => {
|
|
|
1676
1666
|
'projects/test-project/phases/01-test-phase/01-CONTEXT.md': '# Context',
|
|
1677
1667
|
'projects/test-project/phases/01-test-phase/01-01-PLAN.md': '---\nphase: 01-test-phase\nplan: 01\n---\n# Plan',
|
|
1678
1668
|
});
|
|
1669
|
+
|
|
1670
|
+
// Establish a GENUINE milestone context. detectQuickMode treats a
|
|
1671
|
+
// milestone worktree entry as active only when its `repos` map points at
|
|
1672
|
+
// an on-disk directory; a bare { type: 'milestone' } entry with no live
|
|
1673
|
+
// repos is treated as stale and falls through to PRODUCT mode (the
|
|
1674
|
+
// documented stale-defence from bug 260507-pdp). The previous fixture
|
|
1675
|
+
// omitted `repos`, so it silently exercised product mode and the
|
|
1676
|
+
// project-scoped expectations below could never pass. Create a real
|
|
1677
|
+
// worktree dir inside the fixture (cleaned up with it) and point the
|
|
1678
|
+
// entry at it so milestone-context resolution actually fires.
|
|
1679
|
+
const worktreeDir = path.join(fixture.cwd, 'worktrees', 'v19-git-worktrees');
|
|
1680
|
+
fs.mkdirSync(worktreeDir, { recursive: true });
|
|
1681
|
+
fs.writeFileSync(
|
|
1682
|
+
path.join(fixture.cwd, 'config.local.json'),
|
|
1683
|
+
JSON.stringify({
|
|
1684
|
+
execution: { active_context: 'v19-git-worktrees' },
|
|
1685
|
+
projects: {
|
|
1686
|
+
'test-project': {
|
|
1687
|
+
worktrees: {
|
|
1688
|
+
'v19-git-worktrees': {
|
|
1689
|
+
type: 'milestone',
|
|
1690
|
+
repos: { 'deliver-great-systems': worktreeDir },
|
|
1691
|
+
},
|
|
1692
|
+
},
|
|
1693
|
+
},
|
|
1694
|
+
},
|
|
1695
|
+
}),
|
|
1696
|
+
'utf-8'
|
|
1697
|
+
);
|
|
1679
1698
|
});
|
|
1680
1699
|
|
|
1681
1700
|
afterEach(() => {
|
|
@@ -2196,3 +2215,43 @@ describe('v2 mode multi-project: end-to-end', () => {
|
|
|
2196
2215
|
}
|
|
2197
2216
|
});
|
|
2198
2217
|
});
|
|
2218
|
+
|
|
2219
|
+
// ─── LOOK-01: init coerces a cross-milestone-ambiguous bare number to not-found ─
|
|
2220
|
+
|
|
2221
|
+
describe('init execute-phase coerces ambiguous archive number to not-found (LOOK-01)', () => {
|
|
2222
|
+
// A v2 project where bare "03" is NOT an active phase but exists in TWO
|
|
2223
|
+
// milestone archives. findPhaseInternal returns a {found:false, ambiguous:true}
|
|
2224
|
+
// signal; the init.cjs coercion must treat it as not-found rather than silently
|
|
2225
|
+
// adopting one archive's directory (the pre-fix Group C bug).
|
|
2226
|
+
function ambiguousArchiveFixture() {
|
|
2227
|
+
return createFixture({
|
|
2228
|
+
'config.json': JSON.stringify({}),
|
|
2229
|
+
'config.local.json': JSON.stringify({ current_project: 'auth-overhaul' }),
|
|
2230
|
+
'PROJECTS.md': '# Projects\n\n| Project | Status |\n|---------|--------|\n| auth-overhaul | Active |\n',
|
|
2231
|
+
'REPOS.md': '# Repos\n\n| Name | Path |\n|------|------|\n',
|
|
2232
|
+
'projects/auth-overhaul/STATE.md': '---\ncurrent_milestone: v25.0\n---\n# State',
|
|
2233
|
+
'projects/auth-overhaul/ROADMAP.md': '# Roadmap',
|
|
2234
|
+
'projects/auth-overhaul/REQUIREMENTS.md': '# Requirements',
|
|
2235
|
+
'projects/auth-overhaul/PROJECT.md': '# Project',
|
|
2236
|
+
'projects/auth-overhaul/phases/': null,
|
|
2237
|
+
'milestones/v3.0-phases/03-alpha/01-PLAN.md': '# Plan',
|
|
2238
|
+
'milestones/v4.0-phases/03-beta/01-PLAN.md': '# Plan',
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
it('does NOT silently resolve to either archive directory', () => {
|
|
2243
|
+
const fixture = ambiguousArchiveFixture();
|
|
2244
|
+
try {
|
|
2245
|
+
const result = runInit(fixture.cwd, 'execute-phase 03');
|
|
2246
|
+
// Coerced to not-found: the ambiguity object was nulled out.
|
|
2247
|
+
assert.equal(result.phase_found, false, 'ambiguous number must be not-found');
|
|
2248
|
+
assert.equal(result.phase_dir, null, 'no archive directory adopted');
|
|
2249
|
+
// Belt-and-suspenders: neither archived dir leaked into the result.
|
|
2250
|
+
const blob = JSON.stringify(result);
|
|
2251
|
+
assert.ok(!blob.includes('03-alpha'), 'must not adopt v3.0 archive dir');
|
|
2252
|
+
assert.ok(!blob.includes('03-beta'), 'must not adopt v4.0 archive dir');
|
|
2253
|
+
} finally {
|
|
2254
|
+
fixture.cleanup();
|
|
2255
|
+
}
|
|
2256
|
+
});
|
|
2257
|
+
});
|