@ktpartners/dgs-platform 3.5.0 → 3.5.3
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 +27 -0
- package/bin/install.js +22 -0
- package/deliver-great-systems/bin/lib/core.cjs +21 -0
- package/deliver-great-systems/bin/lib/core.test.cjs +66 -0
- package/deliver-great-systems/bin/lib/ideas.cjs +39 -11
- package/deliver-great-systems/bin/lib/ideas.test.cjs +32 -3
- package/deliver-great-systems/bin/lib/init.cjs +23 -0
- package/deliver-great-systems/bin/lib/init.test.cjs +78 -0
- package/deliver-great-systems/bin/lib/jobs.cjs +194 -83
- package/deliver-great-systems/bin/lib/jobs.test.cjs +272 -15
- package/deliver-great-systems/bin/lib/overlap.cjs +14 -4
- package/deliver-great-systems/bin/lib/overlap.test.cjs +13 -0
- package/deliver-great-systems/bin/lib/package-scan-report.cjs +19 -0
- package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +18 -0
- package/deliver-great-systems/bin/lib/phase.cjs +12 -4
- package/deliver-great-systems/bin/lib/phase.test.cjs +69 -0
- package/deliver-great-systems/bin/lib/projects.cjs +13 -3
- package/deliver-great-systems/bin/lib/projects.test.cjs +8 -0
- package/deliver-great-systems/bin/lib/roadmap.cjs +26 -5
- package/deliver-great-systems/bin/lib/roadmap.test.cjs +86 -0
- package/deliver-great-systems/bin/lib/search.cjs +62 -15
- package/deliver-great-systems/bin/lib/search.test.cjs +94 -0
- package/deliver-great-systems/bin/lib/verify.cjs +37 -20
- package/deliver-great-systems/bin/lib/verify.test.cjs +58 -0
- package/deliver-great-systems/references/git-integration.md +1 -1
- package/deliver-great-systems/workflows/audit-milestone.md +1 -1
- package/deliver-great-systems/workflows/cleanup.md +1 -1
- package/deliver-great-systems/workflows/codereview.md +1 -1
- package/deliver-great-systems/workflows/complete-milestone.md +3 -3
- package/deliver-great-systems/workflows/discuss-phase.md +5 -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/pause-work.md +1 -1
- package/deliver-great-systems/workflows/plan-phase.md +6 -2
- package/deliver-great-systems/workflows/resume-project.md +2 -2
- package/package.json +1 -1
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
parseJobFile, updateJobStep, moveJobFile,
|
|
19
19
|
generateMilestoneSteps, buildJobFileContent,
|
|
20
20
|
cmdJobsCreateMilestone, cmdJobsMilestonePreview,
|
|
21
|
+
analyzeMilestonePhases, detectActiveMilestone,
|
|
21
22
|
findJobFile, updateJobHeader, insertJobSteps,
|
|
22
23
|
buildGapFixSteps, insertGapFixSection,
|
|
23
24
|
listJobs, cancelJob, recordStartShas, rollbackJob,
|
|
@@ -1379,6 +1380,199 @@ Plans:
|
|
|
1379
1380
|
});
|
|
1380
1381
|
});
|
|
1381
1382
|
|
|
1383
|
+
// ─── Bold-line single-milestone format Tests ────────────────────────────
|
|
1384
|
+
|
|
1385
|
+
describe('bold-line single-milestone format', () => {
|
|
1386
|
+
let fixture;
|
|
1387
|
+
|
|
1388
|
+
afterEach(() => {
|
|
1389
|
+
if (fixture) fixture.cleanup();
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
const BOLD_LINE_ROADMAP = `# Roadmap: Tenant data-quality metrics in Admin UI (v30.1)
|
|
1393
|
+
|
|
1394
|
+
**Milestone:** v30.1 — Tenant data-quality metrics in Admin UI
|
|
1395
|
+
|
|
1396
|
+
## Overview
|
|
1397
|
+
|
|
1398
|
+
Single-milestone roadmap. Version is only in the title and the bold line.
|
|
1399
|
+
|
|
1400
|
+
### Phase 1: Metrics schema
|
|
1401
|
+
**Goal**: Define metrics tables
|
|
1402
|
+
Plans:
|
|
1403
|
+
- [ ] 01-01-PLAN.md
|
|
1404
|
+
|
|
1405
|
+
### Phase 2: Admin UI surface
|
|
1406
|
+
**Goal**: Surface metrics in admin
|
|
1407
|
+
Plans:
|
|
1408
|
+
- [ ] 02-01-PLAN.md
|
|
1409
|
+
`;
|
|
1410
|
+
|
|
1411
|
+
const MULTI_MILESTONE_WITH_BOLD = `# Roadmap (v9.9)
|
|
1412
|
+
|
|
1413
|
+
**Milestone:** v9.9 — stray bold line
|
|
1414
|
+
|
|
1415
|
+
## v1.0 Foundation (SHIPPED)
|
|
1416
|
+
### Phase 1: Base
|
|
1417
|
+
### Phase 2: More base
|
|
1418
|
+
|
|
1419
|
+
## v2.0 Next (In Progress)
|
|
1420
|
+
### Phase 3: New
|
|
1421
|
+
### Phase 4: Newer
|
|
1422
|
+
`;
|
|
1423
|
+
|
|
1424
|
+
it('detectActiveMilestone returns v30.1 from the bold line', () => {
|
|
1425
|
+
assert.equal(detectActiveMilestone(BOLD_LINE_ROADMAP), 'v30.1');
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
it('analyzeMilestonePhases derives phases 1 and 2 from the bold-line roadmap', () => {
|
|
1429
|
+
fixture = createFixture({
|
|
1430
|
+
'ROADMAP.md': BOLD_LINE_ROADMAP,
|
|
1431
|
+
'phases/01-metrics-schema/01-01-PLAN.md': '',
|
|
1432
|
+
'phases/02-admin-ui/02-01-PLAN.md': '',
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
const phases = analyzeMilestonePhases(fixture.cwd, 'v30.1');
|
|
1436
|
+
assert.equal(phases.length, 2);
|
|
1437
|
+
assert.deepEqual(phases.map(p => p.number), ['1', '2']);
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
it('cmdJobsMilestonePreview succeeds with an explicit version', () => {
|
|
1441
|
+
fixture = createFixture({
|
|
1442
|
+
'ROADMAP.md': BOLD_LINE_ROADMAP,
|
|
1443
|
+
'phases/01-metrics-schema/01-01-PLAN.md': '',
|
|
1444
|
+
'phases/02-admin-ui/02-01-PLAN.md': '',
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
const result = cmdJobsMilestonePreview(fixture.cwd, 'v30.1', true, false);
|
|
1448
|
+
assert.equal(result.preview, true);
|
|
1449
|
+
assert.equal(result.version, 'v30.1');
|
|
1450
|
+
assert.ok(result.step_count > 0, 'expected at least one step');
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
it('cmdJobsMilestonePreview auto-detects the bold-line version', () => {
|
|
1454
|
+
fixture = createFixture({
|
|
1455
|
+
'ROADMAP.md': BOLD_LINE_ROADMAP,
|
|
1456
|
+
'phases/01-metrics-schema/01-01-PLAN.md': '',
|
|
1457
|
+
'phases/02-admin-ui/02-01-PLAN.md': '',
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
const result = cmdJobsMilestonePreview(fixture.cwd, null, true, false);
|
|
1461
|
+
assert.equal(result.version, 'v30.1');
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
it('analyzeMilestonePhases throws on a multi-milestone roadmap with a stray bold line', () => {
|
|
1465
|
+
fixture = createFixture({
|
|
1466
|
+
'ROADMAP.md': MULTI_MILESTONE_WITH_BOLD,
|
|
1467
|
+
'phases/01-base/01-01-PLAN.md': '',
|
|
1468
|
+
'phases/03-new/03-01-PLAN.md': '',
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
assert.throws(
|
|
1472
|
+
() => analyzeMilestonePhases(fixture.cwd, 'v9.9'),
|
|
1473
|
+
(err) => /v9\.9|not found/i.test(err.message)
|
|
1474
|
+
);
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
it('detectActiveMilestone prefers the In-Progress heading over the bold line', () => {
|
|
1478
|
+
assert.equal(detectActiveMilestone(MULTI_MILESTONE_WITH_BOLD), 'v2.0');
|
|
1479
|
+
});
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
// ─── Ad-hoc container format ────────────────────────────────────────────
|
|
1483
|
+
describe('ad-hoc container format', () => {
|
|
1484
|
+
let fixture;
|
|
1485
|
+
|
|
1486
|
+
afterEach(() => {
|
|
1487
|
+
if (fixture) fixture.cleanup();
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
// Bare seedAdhocRoadmapSection output: version on the heading, in-progress
|
|
1491
|
+
// on a SEPARATE **Status:** line, NO parenthetical on the heading, and NO
|
|
1492
|
+
// phase headings. Used only for the pure detectActiveMilestone assertions.
|
|
1493
|
+
const ADHOC_ROADMAP = `# Roadmap
|
|
1494
|
+
|
|
1495
|
+
## Active Milestone: v25.1 add-prs
|
|
1496
|
+
|
|
1497
|
+
**Goal:** add-prs
|
|
1498
|
+
**Status:** In progress (ad-hoc container — phases added on demand via /dgs:add-phase)
|
|
1499
|
+
|
|
1500
|
+
Phases:
|
|
1501
|
+
|
|
1502
|
+
---
|
|
1503
|
+
`;
|
|
1504
|
+
|
|
1505
|
+
// Ad-hoc container that has accrued a phase (heading + matching PLAN dir),
|
|
1506
|
+
// so analyzeMilestonePhases (called by cmdJobsMilestonePreview) resolves
|
|
1507
|
+
// rather than throwing "Milestone vX.Y not found".
|
|
1508
|
+
const ADHOC_ROADMAP_WITH_PHASE = `# Roadmap
|
|
1509
|
+
|
|
1510
|
+
## Active Milestone: v25.1 add-prs
|
|
1511
|
+
|
|
1512
|
+
**Goal:** add-prs
|
|
1513
|
+
**Status:** In progress (ad-hoc container — phases added on demand via /dgs:add-phase)
|
|
1514
|
+
|
|
1515
|
+
Phases:
|
|
1516
|
+
|
|
1517
|
+
### Phase 1: First ad-hoc phase
|
|
1518
|
+
**Goal**: Do the first thing
|
|
1519
|
+
Plans:
|
|
1520
|
+
- [ ] 01-01-PLAN.md
|
|
1521
|
+
|
|
1522
|
+
---
|
|
1523
|
+
`;
|
|
1524
|
+
|
|
1525
|
+
// Local copies of the multi-milestone / bold-line fixtures (the originals
|
|
1526
|
+
// are scoped to the sibling describe block) for the over-match guard and
|
|
1527
|
+
// bold-line regression assertions.
|
|
1528
|
+
const MULTI_MILESTONE_WITH_BOLD = `# Roadmap (v9.9)
|
|
1529
|
+
|
|
1530
|
+
**Milestone:** v9.9 — stray bold line
|
|
1531
|
+
|
|
1532
|
+
## v1.0 Foundation (SHIPPED)
|
|
1533
|
+
### Phase 1: Base
|
|
1534
|
+
### Phase 2: More base
|
|
1535
|
+
|
|
1536
|
+
## v2.0 Next (In Progress)
|
|
1537
|
+
### Phase 3: New
|
|
1538
|
+
### Phase 4: Newer
|
|
1539
|
+
`;
|
|
1540
|
+
|
|
1541
|
+
const BOLD_LINE_ROADMAP = `# Roadmap: Tenant data-quality metrics in Admin UI (v30.1)
|
|
1542
|
+
|
|
1543
|
+
**Milestone:** v30.1 — Tenant data-quality metrics in Admin UI
|
|
1544
|
+
|
|
1545
|
+
## Overview
|
|
1546
|
+
|
|
1547
|
+
Single-milestone roadmap. Version is only in the title and the bold line.
|
|
1548
|
+
|
|
1549
|
+
### Phase 1: Metrics schema
|
|
1550
|
+
### Phase 2: Admin UI surface
|
|
1551
|
+
`;
|
|
1552
|
+
|
|
1553
|
+
it('detectActiveMilestone resolves v25.1 from the ad-hoc heading', () => {
|
|
1554
|
+
assert.equal(detectActiveMilestone(ADHOC_ROADMAP), 'v25.1');
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
it('detectActiveMilestone prefers the In-Progress heading over an ad-hoc branch (no hijack)', () => {
|
|
1558
|
+
assert.equal(detectActiveMilestone(MULTI_MILESTONE_WITH_BOLD), 'v2.0');
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
it('detectActiveMilestone leaves the bold-line branch unchanged', () => {
|
|
1562
|
+
assert.equal(detectActiveMilestone(BOLD_LINE_ROADMAP), 'v30.1');
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
it('cmdJobsMilestonePreview auto-detects v25.1 from an ad-hoc roadmap', () => {
|
|
1566
|
+
fixture = createFixture({
|
|
1567
|
+
'ROADMAP.md': ADHOC_ROADMAP_WITH_PHASE,
|
|
1568
|
+
'phases/01-first-ad-hoc-phase/01-01-PLAN.md': '',
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
const result = cmdJobsMilestonePreview(fixture.cwd, null, true, false);
|
|
1572
|
+
assert.equal(result.version, 'v25.1');
|
|
1573
|
+
});
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1382
1576
|
// ─── findJobFile Tests ──────────────────────────────────────────────────
|
|
1383
1577
|
|
|
1384
1578
|
describe('findJobFile', () => {
|
|
@@ -1986,10 +2180,11 @@ Plans:
|
|
|
1986
2180
|
assert.deepEqual(result.completed, []);
|
|
1987
2181
|
});
|
|
1988
2182
|
|
|
1989
|
-
it('
|
|
2183
|
+
it('groups FLAT jobs by frontmatter status with correct fields', () => {
|
|
2184
|
+
// Flat layout: jobs/milestone-*.md grouped by parsed status header.
|
|
1990
2185
|
fixture = createFixture({
|
|
1991
|
-
'jobs/
|
|
1992
|
-
'jobs/
|
|
2186
|
+
'jobs/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2187
|
+
'jobs/milestone-v5.0.md': ALL_COMPLETED_JOB,
|
|
1993
2188
|
});
|
|
1994
2189
|
const result = listJobs(fixture.cwd);
|
|
1995
2190
|
|
|
@@ -2011,6 +2206,40 @@ Plans:
|
|
|
2011
2206
|
assert.equal(comp.progress, '2/2');
|
|
2012
2207
|
});
|
|
2013
2208
|
|
|
2209
|
+
it('still lists legacy-subdir jobs (back-compat)', () => {
|
|
2210
|
+
fixture = createFixture({
|
|
2211
|
+
'jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2212
|
+
'jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
|
|
2213
|
+
});
|
|
2214
|
+
const result = listJobs(fixture.cwd);
|
|
2215
|
+
|
|
2216
|
+
assert.equal(result.in_progress.length, 1);
|
|
2217
|
+
assert.equal(result.completed.length, 1);
|
|
2218
|
+
assert.equal(result.in_progress[0].version, 'v6.0');
|
|
2219
|
+
assert.equal(result.completed[0].version, 'v5.0');
|
|
2220
|
+
});
|
|
2221
|
+
|
|
2222
|
+
it('dedupes a job present both flat and in a legacy subdir (by version)', () => {
|
|
2223
|
+
fixture = createFixture({
|
|
2224
|
+
'jobs/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2225
|
+
'jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2226
|
+
});
|
|
2227
|
+
const result = listJobs(fixture.cwd);
|
|
2228
|
+
assert.equal(result.in_progress.length, 1, 'flat + legacy same version should dedupe to 1');
|
|
2229
|
+
assert.equal(result.in_progress[0].version, 'v6.0');
|
|
2230
|
+
});
|
|
2231
|
+
|
|
2232
|
+
it('creates no legacy subdirs (read-only on the filesystem)', () => {
|
|
2233
|
+
fixture = createFixture({
|
|
2234
|
+
'jobs/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2235
|
+
});
|
|
2236
|
+
listJobs(fixture.cwd);
|
|
2237
|
+
|
|
2238
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'pending')), false);
|
|
2239
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'in-progress')), false);
|
|
2240
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'completed')), false);
|
|
2241
|
+
});
|
|
2242
|
+
|
|
2014
2243
|
it('shows check flag false when --no-check was used', () => {
|
|
2015
2244
|
fixture = createFixture({
|
|
2016
2245
|
'jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
|
|
@@ -2182,18 +2411,46 @@ Plans:
|
|
|
2182
2411
|
assert.equal(result.job_count, 0);
|
|
2183
2412
|
});
|
|
2184
2413
|
|
|
2185
|
-
it('
|
|
2414
|
+
it('does not create legacy subdirs and stays healthy when they are absent', () => {
|
|
2186
2415
|
fixture = createFixture({
|
|
2187
2416
|
'': null,
|
|
2188
2417
|
});
|
|
2189
2418
|
const result = healthCheck(fixture.cwd);
|
|
2190
2419
|
|
|
2191
|
-
//
|
|
2420
|
+
// An empty project (no legacy subdirs) is healthy with no jobs.
|
|
2421
|
+
assert.equal(result.healthy, true);
|
|
2422
|
+
assert.equal(result.job_count, 0);
|
|
2192
2423
|
assert.ok(Array.isArray(result.directories));
|
|
2193
|
-
//
|
|
2194
|
-
assert.
|
|
2195
|
-
assert.
|
|
2196
|
-
assert.
|
|
2424
|
+
// Legacy subdirs must NOT be auto-created.
|
|
2425
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'pending')), false);
|
|
2426
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'in-progress')), false);
|
|
2427
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'completed')), false);
|
|
2428
|
+
});
|
|
2429
|
+
|
|
2430
|
+
it('reports healthy: true for a flat jobs layout and does not create legacy subdirs', () => {
|
|
2431
|
+
fixture = createFixture({
|
|
2432
|
+
'jobs/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2433
|
+
});
|
|
2434
|
+
const result = healthCheck(fixture.cwd);
|
|
2435
|
+
|
|
2436
|
+
assert.equal(result.healthy, true);
|
|
2437
|
+
assert.equal(result.job_count, 1);
|
|
2438
|
+
assert.equal(result.issues.length, 0);
|
|
2439
|
+
// No legacy subdirs created for a flat layout.
|
|
2440
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'pending')), false);
|
|
2441
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'in-progress')), false);
|
|
2442
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'completed')), false);
|
|
2443
|
+
});
|
|
2444
|
+
|
|
2445
|
+
it('excludes job-*-SUMMARY.md files from job_count in a flat layout', () => {
|
|
2446
|
+
fixture = createFixture({
|
|
2447
|
+
'jobs/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2448
|
+
'jobs/job-v6.0-SUMMARY.md': '# Summary',
|
|
2449
|
+
});
|
|
2450
|
+
const result = healthCheck(fixture.cwd);
|
|
2451
|
+
|
|
2452
|
+
assert.equal(result.job_count, 1);
|
|
2453
|
+
assert.equal(result.healthy, true);
|
|
2197
2454
|
});
|
|
2198
2455
|
|
|
2199
2456
|
it('validates each job file parses successfully; reports parse failures as issues', () => {
|
|
@@ -2546,14 +2803,14 @@ describe('jobs root-layout', () => {
|
|
|
2546
2803
|
|
|
2547
2804
|
});
|
|
2548
2805
|
|
|
2549
|
-
it('healthCheck
|
|
2806
|
+
it('healthCheck does not create legacy subdirs at root layout', () => {
|
|
2550
2807
|
fixture = createTempProject({ layout: 'root' });
|
|
2551
2808
|
const result = healthCheck(fixture.cwd);
|
|
2552
|
-
assert.
|
|
2553
|
-
//
|
|
2554
|
-
assert.
|
|
2555
|
-
assert.
|
|
2556
|
-
assert.
|
|
2809
|
+
assert.equal(result.healthy, true);
|
|
2810
|
+
// Legacy subdirs must NOT be auto-created.
|
|
2811
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'pending')), false);
|
|
2812
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'in-progress')), false);
|
|
2813
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'completed')), false);
|
|
2557
2814
|
});
|
|
2558
2815
|
|
|
2559
2816
|
it('listJobs works in root layout', () => {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const { safeReadFile, getProjectFolders, output, error, getProjectDir } = require('./core.cjs');
|
|
11
|
+
const { safeReadFile, getProjectFolders, output, error, getProjectDir, isValidMilestoneVersion } = require('./core.cjs');
|
|
12
12
|
const { parseProjectsMd, readProjectState } = require('./projects.cjs');
|
|
13
13
|
const { parseReposMd, resolveFileToRepo } = require('./repos.cjs');
|
|
14
14
|
|
|
@@ -32,18 +32,28 @@ function scanProjectPlanFiles(cwd, slug) {
|
|
|
32
32
|
const phasesDir = path.join(getProjectDir(cwd, slug), 'phases');
|
|
33
33
|
const results = [];
|
|
34
34
|
|
|
35
|
+
// Per-milestone versioned layout: descend a single vN.N child dir if present
|
|
36
|
+
// (phases/<version>/NN-slug). Detect via core.isValidMilestoneVersion — no
|
|
37
|
+
// parallel version regex. No version child → stay flat (back-compat).
|
|
38
|
+
let scanRoot = phasesDir;
|
|
35
39
|
try {
|
|
36
|
-
const
|
|
40
|
+
const kids = fs.readdirSync(scanRoot, { withFileTypes: true });
|
|
41
|
+
const vd = kids.find(e => e.isDirectory() && isValidMilestoneVersion(e.name));
|
|
42
|
+
if (vd) scanRoot = path.join(scanRoot, vd.name);
|
|
43
|
+
} catch {}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const phaseDirs = fs.readdirSync(scanRoot, { withFileTypes: true })
|
|
37
47
|
.filter(e => e.isDirectory())
|
|
38
48
|
.map(e => e.name);
|
|
39
49
|
|
|
40
50
|
// Exclude archived phase directories
|
|
41
51
|
const activePhaseDirs = excludeArchivedPhases(
|
|
42
|
-
phaseDirs.map(d => path.join(
|
|
52
|
+
phaseDirs.map(d => path.join(scanRoot, d))
|
|
43
53
|
).map(d => path.basename(d));
|
|
44
54
|
|
|
45
55
|
for (const phaseDir of activePhaseDirs) {
|
|
46
|
-
const fullPhaseDir = path.join(
|
|
56
|
+
const fullPhaseDir = path.join(scanRoot, phaseDir);
|
|
47
57
|
let files;
|
|
48
58
|
try {
|
|
49
59
|
files = fs.readdirSync(fullPhaseDir)
|
|
@@ -85,6 +85,19 @@ describe('scanProjectPlanFiles', () => {
|
|
|
85
85
|
assert.deepStrictEqual(result[0].files, ['web-app/src/app.js', 'web-app/src/index.js']);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
it('descends versioned phases/<version>/ layout (260628-p59)', () => {
|
|
89
|
+
createProject(tmpDir, 'project-a');
|
|
90
|
+
// Plan lives one level deeper, under phases/v25.0/01-setup/
|
|
91
|
+
createPlanFile(tmpDir, 'project-a', 'v25.0/01-setup', '01-01-PLAN.md', [
|
|
92
|
+
{ name: 'Task 1', files: ['web-app/src/index.js'], repos: ['web-app'] },
|
|
93
|
+
]);
|
|
94
|
+
const result = scanProjectPlanFiles(tmpDir, 'project-a');
|
|
95
|
+
assert.strictEqual(result.length, 1, 'plan under phases/v25.0/ must be found');
|
|
96
|
+
assert.deepStrictEqual(result[0].repos, ['web-app']);
|
|
97
|
+
assert.deepStrictEqual(result[0].files, ['web-app/src/index.js']);
|
|
98
|
+
assert.strictEqual(result[0].plan, '01-setup/01-01-PLAN.md');
|
|
99
|
+
});
|
|
100
|
+
|
|
88
101
|
it('handles plans with multiple tasks', () => {
|
|
89
102
|
createProject(tmpDir, 'project-a');
|
|
90
103
|
createPlanFile(tmpDir, 'project-a', '01-setup', '01-01-PLAN.md', [
|
|
@@ -25,6 +25,7 @@ const fs = require('fs');
|
|
|
25
25
|
const path = require('path');
|
|
26
26
|
const { getPlanningRoot } = require('./paths.cjs');
|
|
27
27
|
const { getLocalConfigPath } = require('./config.cjs');
|
|
28
|
+
const { isValidMilestoneVersion } = require('./core.cjs');
|
|
28
29
|
|
|
29
30
|
// ─── Canonical frontmatter constants ─────────────────────────────────────────
|
|
30
31
|
// Every findings[] entry emitted by this module carries these two constant
|
|
@@ -886,6 +887,24 @@ function _resolveReportPath(cwd, opts) {
|
|
|
886
887
|
const phaseEntries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
887
888
|
for (const phEnt of phaseEntries) {
|
|
888
889
|
if (!phEnt.isDirectory()) continue;
|
|
890
|
+
// Per-milestone versioned layout: the immediate child is a vN.N dir, so
|
|
891
|
+
// the active phase lives one level deeper (phases/<version>/NN-slug).
|
|
892
|
+
// Detect via core.isValidMilestoneVersion (no parallel version regex)
|
|
893
|
+
// and match the active phase among the version dir's children.
|
|
894
|
+
if (isValidMilestoneVersion(phEnt.name)) {
|
|
895
|
+
const versionDir = path.join(phasesDir, phEnt.name);
|
|
896
|
+
let candidates;
|
|
897
|
+
try { candidates = fs.readdirSync(versionDir, { withFileTypes: true }); } catch { continue; }
|
|
898
|
+
for (const cand of candidates) {
|
|
899
|
+
if (!cand.isDirectory()) continue;
|
|
900
|
+
if (cand.name === activeContext) {
|
|
901
|
+
const cm = cand.name.match(/^(\d+)/);
|
|
902
|
+
const cprefix = cm ? cm[1] : cand.name;
|
|
903
|
+
return path.join(versionDir, cand.name, cprefix + '-PACKAGE-SCAN.md');
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
889
908
|
if (phEnt.name === activeContext) {
|
|
890
909
|
const m = phEnt.name.match(/^(\d+)/);
|
|
891
910
|
const prefix = m ? m[1] : phEnt.name;
|
|
@@ -494,6 +494,24 @@ describe('_resolveReportPath — tier 1 (active phase)', () => {
|
|
|
494
494
|
}
|
|
495
495
|
});
|
|
496
496
|
|
|
497
|
+
test('active_context resolves under versioned phases/<version>/ layout (260628-p59)', () => {
|
|
498
|
+
tmpDir = setupPlanningRoot({
|
|
499
|
+
local: {
|
|
500
|
+
current_project: 'gsd',
|
|
501
|
+
execution: { active_context: '03-foo' },
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
// Phase dir lives one level deeper, under phases/v25.0/03-foo
|
|
505
|
+
const versionedPhaseDir = path.join(tmpDir, 'projects', 'gsd', 'phases', 'v25.0', '03-foo');
|
|
506
|
+
fs.mkdirSync(versionedPhaseDir, { recursive: true });
|
|
507
|
+
try {
|
|
508
|
+
const resolved = _resolveReportPath(tmpDir, {});
|
|
509
|
+
assert.equal(resolved, path.join(versionedPhaseDir, '03-PACKAGE-SCAN.md'));
|
|
510
|
+
} finally {
|
|
511
|
+
cleanup(tmpDir);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
|
|
497
515
|
test('active_context points at a phase that does NOT exist → falls through', () => {
|
|
498
516
|
tmpDir = setupPlanningRoot({
|
|
499
517
|
local: {
|
|
@@ -381,14 +381,22 @@ function cmdPhaseAdd(cwd, description, raw) {
|
|
|
381
381
|
updatedContent = content + phaseEntry;
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
-
// Update milestone summary range
|
|
385
|
-
|
|
384
|
+
// Update milestone summary range. Handles both the legacy non-namespaced form
|
|
385
|
+
// ("Phases 124-128" -> "Phases 124-129") and the per-milestone-namespaced form
|
|
386
|
+
// ("Phases v25.1/1-9" -> "Phases v25.1/1-10"). In the live namespaced layout the
|
|
387
|
+
// START endpoint carries the `vX.Y/` prefix while the END endpoint is BARE
|
|
388
|
+
// (e.g. start "v25.1/1", end "9"), so the prefix is derived from whichever
|
|
389
|
+
// endpoint carries it; the START (rangeMatch[1]) is preserved verbatim and the
|
|
390
|
+
// rebuilt END reuses the END endpoint's own prefix (empty in the live form).
|
|
391
|
+
const rangePattern = /Phases\s+((?:v\d+\.\d+\/)?\d+)\s*-\s*((?:v\d+\.\d+\/)?\d+)\s*\(in progress\)/i;
|
|
386
392
|
const rangeMatch = updatedContent.match(rangePattern);
|
|
387
393
|
if (rangeMatch) {
|
|
388
|
-
const
|
|
394
|
+
const endRaw = rangeMatch[2]; // e.g. "9" (bare) or "v25.1/9"
|
|
395
|
+
const prefix = (endRaw.match(/^v\d+\.\d+\//) || [''])[0];
|
|
396
|
+
const rangeEnd = parseInt(endRaw.replace(/^v\d+\.\d+\//, ''), 10);
|
|
389
397
|
if (newPhaseNum > rangeEnd) {
|
|
390
398
|
updatedContent = updatedContent.replace(rangePattern,
|
|
391
|
-
`Phases ${rangeMatch[1]}-${newPhaseNum} (in progress)`);
|
|
399
|
+
`Phases ${rangeMatch[1]}-${prefix}${newPhaseNum} (in progress)`);
|
|
392
400
|
}
|
|
393
401
|
}
|
|
394
402
|
|
|
@@ -585,3 +585,72 @@ describe('cmdPhaseInitVersionedDir (NUM-02 versioned write path)', () => {
|
|
|
585
585
|
}
|
|
586
586
|
});
|
|
587
587
|
});
|
|
588
|
+
|
|
589
|
+
describe('cmdPhaseAdd milestone-summary range bump', () => {
|
|
590
|
+
let phase;
|
|
591
|
+
|
|
592
|
+
beforeEach(() => {
|
|
593
|
+
resetPaths();
|
|
594
|
+
delete require.cache[require.resolve('./phase.cjs')];
|
|
595
|
+
phase = require('./phase.cjs');
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
afterEach(() => {
|
|
599
|
+
resetPaths();
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('bumps a namespaced "Phases v25.1/1-9 (in progress)" range to v25.1/1-10, preserving the prefix', () => {
|
|
603
|
+
// maxPhase resolves to 9 from the `### Phase 9:` heading → newPhaseNum=10.
|
|
604
|
+
const roadmap = `# Roadmap
|
|
605
|
+
|
|
606
|
+
- 🚧 **v25.1 add-prs** -- Phases v25.1/1-9 (in progress)
|
|
607
|
+
|
|
608
|
+
## Phases
|
|
609
|
+
|
|
610
|
+
### Phase 9: Ninth
|
|
611
|
+
**Goal:** done
|
|
612
|
+
|
|
613
|
+
---
|
|
614
|
+
`;
|
|
615
|
+
const fixture = createFixture({ 'config.json': JSON.stringify({}), 'ROADMAP.md': roadmap });
|
|
616
|
+
try {
|
|
617
|
+
captureStdout(() => phase.cmdPhaseAdd(fixture.cwd, 'Tenth phase', false));
|
|
618
|
+
const updated = fs.readFileSync(path.join(fixture.cwd, 'ROADMAP.md'), 'utf-8');
|
|
619
|
+
assert.ok(
|
|
620
|
+
updated.includes('Phases v25.1/1-10 (in progress)'),
|
|
621
|
+
`expected namespaced bump to v25.1/1-10, got:\n${updated}`
|
|
622
|
+
);
|
|
623
|
+
// Prefix preserved on the START endpoint; END stays bare (no v25.1/ on the 10).
|
|
624
|
+
assert.ok(!/Phases v25\.1\/1-v25\.1\/10/.test(updated), 'END endpoint must stay bare');
|
|
625
|
+
assert.ok(!updated.includes('Phases v25.1/1-9 (in progress)'), 'old range must be gone');
|
|
626
|
+
} finally {
|
|
627
|
+
fixture.cleanup();
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('bumps a legacy non-namespaced "Phases 124-129 (in progress)" range to 124-130 (regression)', () => {
|
|
632
|
+
const roadmap = `# Roadmap
|
|
633
|
+
|
|
634
|
+
- 🚧 **v19.0 Git Worktrees** -- Phases 124-129 (in progress)
|
|
635
|
+
|
|
636
|
+
## Phases
|
|
637
|
+
|
|
638
|
+
### Phase 129: One-twenty-nine
|
|
639
|
+
**Goal:** done
|
|
640
|
+
|
|
641
|
+
---
|
|
642
|
+
`;
|
|
643
|
+
const fixture = createFixture({ 'config.json': JSON.stringify({}), 'ROADMAP.md': roadmap });
|
|
644
|
+
try {
|
|
645
|
+
captureStdout(() => phase.cmdPhaseAdd(fixture.cwd, 'One thirty', false));
|
|
646
|
+
const updated = fs.readFileSync(path.join(fixture.cwd, 'ROADMAP.md'), 'utf-8');
|
|
647
|
+
assert.ok(
|
|
648
|
+
updated.includes('Phases 124-130 (in progress)'),
|
|
649
|
+
`expected legacy bump to 124-130, got:\n${updated}`
|
|
650
|
+
);
|
|
651
|
+
assert.ok(!updated.includes('Phases 124-129 (in progress)'), 'old range must be gone');
|
|
652
|
+
} finally {
|
|
653
|
+
fixture.cleanup();
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const { safeReadFile, getProjectFolders, generateSlugInternal, isV2Install, output, error, loadConfig, getProjectDir } = require('./core.cjs');
|
|
11
|
+
const { safeReadFile, getProjectFolders, generateSlugInternal, isV2Install, output, error, loadConfig, getProjectDir, isValidMilestoneVersion } = require('./core.cjs');
|
|
12
12
|
const { writeConfigField } = require('./config.cjs');
|
|
13
13
|
const { getPlanningRoot } = require('./paths.cjs');
|
|
14
14
|
|
|
@@ -126,13 +126,23 @@ function scanProjectReposTags(cwd, slug) {
|
|
|
126
126
|
const phasesDir = path.join(getProjectDir(cwd, slug), 'phases');
|
|
127
127
|
const allRepos = new Set();
|
|
128
128
|
|
|
129
|
+
// Per-milestone versioned layout: descend a single vN.N child dir if present
|
|
130
|
+
// (phases/<version>/NN-slug). Detect via core.isValidMilestoneVersion — no
|
|
131
|
+
// parallel version regex. No version child → stay flat (back-compat).
|
|
132
|
+
let scanRoot = phasesDir;
|
|
129
133
|
try {
|
|
130
|
-
const
|
|
134
|
+
const kids = fs.readdirSync(scanRoot, { withFileTypes: true });
|
|
135
|
+
const vd = kids.find(e => e.isDirectory() && isValidMilestoneVersion(e.name));
|
|
136
|
+
if (vd) scanRoot = path.join(scanRoot, vd.name);
|
|
137
|
+
} catch {}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const phaseDirs = fs.readdirSync(scanRoot, { withFileTypes: true })
|
|
131
141
|
.filter(e => e.isDirectory())
|
|
132
142
|
.map(e => e.name);
|
|
133
143
|
|
|
134
144
|
for (const phaseDir of phaseDirs) {
|
|
135
|
-
const fullPhaseDir = path.join(
|
|
145
|
+
const fullPhaseDir = path.join(scanRoot, phaseDir);
|
|
136
146
|
let planFiles;
|
|
137
147
|
try {
|
|
138
148
|
planFiles = fs.readdirSync(fullPhaseDir)
|
|
@@ -320,6 +320,14 @@ describe('scanProjectReposTags', () => {
|
|
|
320
320
|
const repos = scanProjectReposTags(tmpDir, 'proj');
|
|
321
321
|
assert.deepStrictEqual(repos, []);
|
|
322
322
|
});
|
|
323
|
+
|
|
324
|
+
it('descends versioned phases/<version>/ layout (260628-p59)', () => {
|
|
325
|
+
createProjectManually(tmpDir, 'proj', '# State\n');
|
|
326
|
+
// Plan lives one level deeper, under phases/v25.0/01-setup/
|
|
327
|
+
createPlanFile(tmpDir, 'proj', 'v25.0/01-setup', '01-01-PLAN.md', '<repos>server</repos>\n');
|
|
328
|
+
const repos = scanProjectReposTags(tmpDir, 'proj');
|
|
329
|
+
assert.deepStrictEqual(repos, ['server']);
|
|
330
|
+
});
|
|
323
331
|
});
|
|
324
332
|
|
|
325
333
|
// ─── regenerateProjectsMd ───────────────────────────────────────────────────
|
|
@@ -188,6 +188,20 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
188
188
|
version: 'v' + mMatch[2],
|
|
189
189
|
});
|
|
190
190
|
}
|
|
191
|
+
// Bold-line single-milestone fallback: a `**Milestone:** vX.Y` roadmap has no
|
|
192
|
+
// `## vX.Y` heading, so the scan above yields []. Fire ONLY when the heading
|
|
193
|
+
// scan produced zero milestones — the `milestones.length === 0` guard is the
|
|
194
|
+
// over-match gate, so any roadmap that already has `##` version headings (incl.
|
|
195
|
+
// the `## Active Milestone:` ad-hoc heading, already matched above) ignores it.
|
|
196
|
+
if (milestones.length === 0) {
|
|
197
|
+
const bold = content.match(/^\*\*Milestone:\*\*\s*(v\d+\.\d+)([^\n]*)/im);
|
|
198
|
+
if (bold) {
|
|
199
|
+
milestones.push({
|
|
200
|
+
heading: ('Milestone: ' + bold[1] + bold[2]).trim(),
|
|
201
|
+
version: bold[1],
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
191
205
|
|
|
192
206
|
// Find current and next phase
|
|
193
207
|
const currentPhase = phases.find(p => p.disk_status === 'planned' || p.disk_status === 'partial') || null;
|
|
@@ -268,17 +282,24 @@ function roadmapUpdatePlanProgressInternal(cwd, phaseNum) {
|
|
|
268
282
|
}
|
|
269
283
|
|
|
270
284
|
let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
//
|
|
285
|
+
// Derive a BARE numeric phase key by stripping any leading `vX.Y/` prefix.
|
|
286
|
+
// Checklist lines and detail headings are always bare, so the bare key drives
|
|
287
|
+
// the heading/checkbox regexes; the progress-row matcher allows an optional
|
|
288
|
+
// version prefix and Milestone column on top of the same bare key.
|
|
289
|
+
const bareKey = String(phaseNum).replace(/^v\d+\.\d+\//, '');
|
|
290
|
+
const phaseEscaped = escapeRegex(bareKey);
|
|
291
|
+
|
|
292
|
+
// Progress table row: update Plans column (summaries/plans) and Status column.
|
|
293
|
+
// Format-agnostic: optional `vX.Y/` prefix on the Phase cell and an optional
|
|
294
|
+
// Milestone column (a pure-version cell) between Phase and Plans, both preserved.
|
|
274
295
|
const tablePattern = new RegExp(
|
|
275
|
-
`(\\|\\s
|
|
296
|
+
`(\\|\\s*(?:v\\d+\\.\\d+/)?${phaseEscaped}\\.?\\s[^|]*\\|)(\\s*v\\d+\\.\\d+\\s*\\|)?[^|]*(\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`,
|
|
276
297
|
'i'
|
|
277
298
|
);
|
|
278
299
|
const dateField = isComplete ? ` ${today} ` : ' ';
|
|
279
300
|
roadmapContent = roadmapContent.replace(
|
|
280
301
|
tablePattern,
|
|
281
|
-
`$1 ${summaryCount}/${planCount} $
|
|
302
|
+
`$1$2 ${summaryCount}/${planCount} $3 ${status.padEnd(11)}$4${dateField}$5`
|
|
282
303
|
);
|
|
283
304
|
|
|
284
305
|
// Update plan count in phase detail section
|