@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/bin/install.js +22 -0
  3. package/deliver-great-systems/bin/lib/core.cjs +21 -0
  4. package/deliver-great-systems/bin/lib/core.test.cjs +66 -0
  5. package/deliver-great-systems/bin/lib/ideas.cjs +39 -11
  6. package/deliver-great-systems/bin/lib/ideas.test.cjs +32 -3
  7. package/deliver-great-systems/bin/lib/init.cjs +23 -0
  8. package/deliver-great-systems/bin/lib/init.test.cjs +78 -0
  9. package/deliver-great-systems/bin/lib/jobs.cjs +194 -83
  10. package/deliver-great-systems/bin/lib/jobs.test.cjs +272 -15
  11. package/deliver-great-systems/bin/lib/overlap.cjs +14 -4
  12. package/deliver-great-systems/bin/lib/overlap.test.cjs +13 -0
  13. package/deliver-great-systems/bin/lib/package-scan-report.cjs +19 -0
  14. package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +18 -0
  15. package/deliver-great-systems/bin/lib/phase.cjs +12 -4
  16. package/deliver-great-systems/bin/lib/phase.test.cjs +69 -0
  17. package/deliver-great-systems/bin/lib/projects.cjs +13 -3
  18. package/deliver-great-systems/bin/lib/projects.test.cjs +8 -0
  19. package/deliver-great-systems/bin/lib/roadmap.cjs +26 -5
  20. package/deliver-great-systems/bin/lib/roadmap.test.cjs +86 -0
  21. package/deliver-great-systems/bin/lib/search.cjs +62 -15
  22. package/deliver-great-systems/bin/lib/search.test.cjs +94 -0
  23. package/deliver-great-systems/bin/lib/verify.cjs +37 -20
  24. package/deliver-great-systems/bin/lib/verify.test.cjs +58 -0
  25. package/deliver-great-systems/references/git-integration.md +1 -1
  26. package/deliver-great-systems/workflows/audit-milestone.md +1 -1
  27. package/deliver-great-systems/workflows/cleanup.md +1 -1
  28. package/deliver-great-systems/workflows/codereview.md +1 -1
  29. package/deliver-great-systems/workflows/complete-milestone.md +3 -3
  30. package/deliver-great-systems/workflows/discuss-phase.md +5 -1
  31. package/deliver-great-systems/workflows/execute-phase.md +2 -2
  32. package/deliver-great-systems/workflows/execute-plan.md +2 -2
  33. package/deliver-great-systems/workflows/pause-work.md +1 -1
  34. package/deliver-great-systems/workflows/plan-phase.md +6 -2
  35. package/deliver-great-systems/workflows/resume-project.md +2 -2
  36. package/package.json +1 -1
@@ -1157,52 +1157,77 @@ function analyzeMilestonePhases(cwd, version) {
1157
1157
  );
1158
1158
  const sectionMatch = content.match(sectionHeadingPattern);
1159
1159
  if (!sectionMatch) {
1160
- throw new Error(`Milestone ${version} not found in ROADMAP.md`);
1161
- }
1160
+ // No ##-#### heading carries the version. Support the newer
1161
+ // single-milestone ROADMAP format where the version appears ONLY in the
1162
+ // `# Roadmap: ... (vX.Y)` title and a `**Milestone:** vX.Y` bold line.
1163
+ // Gate the whole-roadmap phase scan behind a single-milestone check so a
1164
+ // stray bold line on a multi-milestone roadmap does not mis-derive a range.
1165
+ const boldMilestonePattern = new RegExp(
1166
+ '^\\*\\*Milestone:\\*\\*\\s*' + escapedVersion + '\\b',
1167
+ 'im'
1168
+ );
1169
+ const versionHeadings = content.match(/^#{2,4}\s+.*v\d+\.\d+/gim) || [];
1170
+ const isSingleMilestone = versionHeadings.length <= 1;
1171
+ if (boldMilestonePattern.test(content) && isSingleMilestone) {
1172
+ const fullPhasePattern = /Phase\s+(\d+)/gi;
1173
+ const phaseNumbers = [];
1174
+ let fpMatch;
1175
+ while ((fpMatch = fullPhasePattern.exec(content)) !== null) {
1176
+ phaseNumbers.push(parseInt(fpMatch[1], 10));
1177
+ }
1178
+ if (phaseNumbers.length === 0) {
1179
+ throw new Error(`Milestone ${version} not found in ROADMAP.md`);
1180
+ }
1181
+ phaseStart = Math.min(...phaseNumbers);
1182
+ phaseEnd = Math.max(...phaseNumbers);
1183
+ } else {
1184
+ throw new Error(`Milestone ${version} not found in ROADMAP.md`);
1185
+ }
1186
+ } else {
1187
+ const headingLevel = sectionMatch[1].length;
1188
+ const sectionStart = sectionMatch.index + sectionMatch[0].length;
1162
1189
 
1163
- const headingLevel = sectionMatch[1].length;
1164
- const sectionStart = sectionMatch.index + sectionMatch[0].length;
1190
+ // Find the end of this section (next heading at same or higher level)
1191
+ const endPattern = new RegExp(
1192
+ `^#{2,${headingLevel}}\\s+`,
1193
+ 'im'
1194
+ );
1195
+ const restContent = content.slice(sectionStart);
1196
+ const endMatch = restContent.match(endPattern);
1197
+ const sectionContent = endMatch
1198
+ ? restContent.slice(0, endMatch.index)
1199
+ : restContent;
1200
+
1201
+ // Collect Phase N: headers within this section
1202
+ const phaseHeaderPattern = /Phase\s+(\d+)\s*:/gi;
1203
+ const phaseNumbers = [];
1204
+ let phMatch;
1205
+ while ((phMatch = phaseHeaderPattern.exec(sectionContent)) !== null) {
1206
+ phaseNumbers.push(parseInt(phMatch[1], 10));
1207
+ }
1165
1208
 
1166
- // Find the end of this section (next heading at same or higher level)
1167
- const endPattern = new RegExp(
1168
- `^#{2,${headingLevel}}\\s+`,
1169
- 'im'
1170
- );
1171
- const restContent = content.slice(sectionStart);
1172
- const endMatch = restContent.match(endPattern);
1173
- const sectionContent = endMatch
1174
- ? restContent.slice(0, endMatch.index)
1175
- : restContent;
1176
-
1177
- // Collect Phase N: headers within this section
1178
- const phaseHeaderPattern = /Phase\s+(\d+)\s*:/gi;
1179
- const phaseNumbers = [];
1180
- let phMatch;
1181
- while ((phMatch = phaseHeaderPattern.exec(sectionContent)) !== null) {
1182
- phaseNumbers.push(parseInt(phMatch[1], 10));
1183
- }
1209
+ // If section-based search found nothing (common when ROADMAP has peer-level
1210
+ // ## headings like ## v1.0, ## Overview, ## Phase Details), search the entire
1211
+ // ROADMAP for Phase N: headings. For single-milestone ROADMAPs all phases
1212
+ // belong to the milestone.
1213
+ if (phaseNumbers.length === 0) {
1214
+ const fullPhasePattern = new RegExp(
1215
+ `Phase\\s+(\\d+)`,
1216
+ 'gi'
1217
+ );
1218
+ let fpMatch;
1219
+ while ((fpMatch = fullPhasePattern.exec(content)) !== null) {
1220
+ phaseNumbers.push(parseInt(fpMatch[1], 10));
1221
+ }
1222
+ }
1184
1223
 
1185
- // If section-based search found nothing (common when ROADMAP has peer-level
1186
- // ## headings like ## v1.0, ## Overview, ## Phase Details), search the entire
1187
- // ROADMAP for Phase N: headings. For single-milestone ROADMAPs all phases
1188
- // belong to the milestone.
1189
- if (phaseNumbers.length === 0) {
1190
- const fullPhasePattern = new RegExp(
1191
- `Phase\\s+(\\d+)`,
1192
- 'gi'
1193
- );
1194
- let fpMatch;
1195
- while ((fpMatch = fullPhasePattern.exec(content)) !== null) {
1196
- phaseNumbers.push(parseInt(fpMatch[1], 10));
1224
+ if (phaseNumbers.length === 0) {
1225
+ throw new Error(`Milestone ${version} not found in ROADMAP.md`);
1197
1226
  }
1198
- }
1199
1227
 
1200
- if (phaseNumbers.length === 0) {
1201
- throw new Error(`Milestone ${version} not found in ROADMAP.md`);
1228
+ phaseStart = Math.min(...phaseNumbers);
1229
+ phaseEnd = Math.max(...phaseNumbers);
1202
1230
  }
1203
-
1204
- phaseStart = Math.min(...phaseNumbers);
1205
- phaseEnd = Math.max(...phaseNumbers);
1206
1231
  }
1207
1232
 
1208
1233
  // Extract all phase detail sections: ### Phase N: Name
@@ -1272,7 +1297,19 @@ function detectActiveMilestone(content) {
1272
1297
  if (match) return match[1];
1273
1298
  // Fallback: milestone list item with (in progress)
1274
1299
  const listMatch = content.match(/-\s+.*?(v\d+\.\d+).*?\((?:in\s+progress)\)/i);
1275
- return listMatch ? listMatch[1] : null;
1300
+ if (listMatch) return listMatch[1];
1301
+ // Ad-hoc container format: version on the heading, in-progress on a separate
1302
+ // **Status:** line (NO parenthetical on the heading). Runs after the legacy
1303
+ // In-Progress heading/list signals so a real multi-milestone In-Progress
1304
+ // heading still wins on multi-milestone roadmaps.
1305
+ const adhocMatch = content.match(/^##\s+Active Milestone:\s*(v\d+\.\d+)/im);
1306
+ if (adhocMatch) return adhocMatch[1];
1307
+ // Final fallback: newer single-milestone format with a `**Milestone:** vX.Y`
1308
+ // bold line. Runs LAST so legacy In-Progress heading/list signals win when
1309
+ // present on multi-milestone roadmaps.
1310
+ const boldMatch = content.match(/^\*\*Milestone:\*\*\s*(v\d+\.\d+)/im);
1311
+ if (boldMatch) return boldMatch[1];
1312
+ return null;
1276
1313
  }
1277
1314
 
1278
1315
  // ─── Milestone Job CLI Wrappers ─────────────────────────────────────────────
@@ -1302,7 +1339,14 @@ function cmdJobsCreateMilestone(cwd, version, check, raw) {
1302
1339
  throw new Error('No active milestone found in ROADMAP.md');
1303
1340
  }
1304
1341
  } else {
1305
- // Validate the version exists in ROADMAP
1342
+ // Validate the version exists in ROADMAP.
1343
+ // Confirmed against every real generator: each emits whitespace immediately
1344
+ // after the FIRST occurrence of the version — a space in the ad-hoc /
1345
+ // bullet headings (`v25.1 add-prs`, `Active Milestone: v25.1 add-prs`) and a
1346
+ // newline after the bold line (`**Milestone:** v25.1\n`). The namespaced
1347
+ // `Phases v25.1/1-9` form has a non-whitespace `/` delimiter, but it is
1348
+ // never the FIRST occurrence (the bold bullet `v25.1 add-prs` precedes it),
1349
+ // so `\s+` always matches the first occurrence. No change needed here.
1306
1350
  const escapedV = resolvedVersion.replace(/\./g, '\\.');
1307
1351
  const versionPattern = new RegExp(escapedV + '\\s+', 'i');
1308
1352
  if (!versionPattern.test(roadmapContent)) {
@@ -1421,47 +1465,86 @@ function cmdJobsMilestonePreview(cwd, version, check, raw) {
1421
1465
  function listJobs(cwd) {
1422
1466
  const jobsDir = path.join(getPlanningRoot(cwd), 'jobs');
1423
1467
  const groups = { in_progress: [], pending: [], completed: [] };
1424
- const dirMap = { 'in-progress': 'in_progress', 'pending': 'pending', 'completed': 'completed' };
1468
+ const statusToGroup = { 'in-progress': 'in_progress', 'pending': 'pending', 'completed': 'completed' };
1469
+
1470
+ // Shared flat-job predicate (identical to findJobFile/healthCheck — reused,
1471
+ // not reinvented). Excludes summary files (job-*-SUMMARY.md).
1472
+ const isJobFile = f => f.startsWith('milestone-') && f.endsWith('.md');
1473
+ const seenVersions = new Set();
1474
+
1475
+ // Stage entries with their mtime so each group sorts most-recent-first.
1476
+ const staged = { in_progress: [], pending: [], completed: [] };
1477
+
1478
+ function stageEntry(groupKey, fullPath, parsed) {
1479
+ let mtime = 0;
1480
+ try { mtime = fs.statSync(fullPath).mtimeMs; } catch {}
1481
+ staged[groupKey].push({
1482
+ mtime,
1483
+ entry: {
1484
+ version: parsed.version,
1485
+ status: parsed.status,
1486
+ check: parsed.check,
1487
+ created_by: parsed.created_by,
1488
+ progress: `${parsed.completedCount}/${parsed.stepCount}`,
1489
+ file: fullPath,
1490
+ },
1491
+ });
1492
+ }
1425
1493
 
1426
- for (const [dirName, groupKey] of Object.entries(dirMap)) {
1427
- const dirPath = path.join(jobsDir, dirName);
1428
- if (!fs.existsSync(dirPath)) {
1429
- fs.mkdirSync(dirPath, { recursive: true });
1494
+ // Flat-first scan: jobs/milestone-*.md grouped by parsed frontmatter status.
1495
+ let flatFiles = [];
1496
+ try {
1497
+ flatFiles = fs.readdirSync(jobsDir).filter(isJobFile);
1498
+ } catch {
1499
+ flatFiles = [];
1500
+ }
1501
+ for (const file of flatFiles) {
1502
+ const fullPath = path.join(jobsDir, file);
1503
+ try {
1504
+ const parsed = parseJobFile(fullPath);
1505
+ const groupKey = statusToGroup[parsed.status];
1506
+ if (!groupKey) continue; // guard unknown/missing status
1507
+ stageEntry(groupKey, fullPath, parsed);
1508
+ if (parsed.version) seenVersions.add(parsed.version);
1509
+ } catch {
1510
+ // Skip unparseable files
1430
1511
  }
1512
+ }
1513
+
1514
+ // Legacy subdir fallback — existence-gated, NEVER created (read-only).
1515
+ const legacyDirs = ['in-progress', 'pending', 'completed'];
1516
+ for (const dirName of legacyDirs) {
1517
+ const dirPath = path.join(jobsDir, dirName);
1518
+ if (!fs.existsSync(dirPath)) continue;
1431
1519
 
1432
1520
  let files;
1433
1521
  try {
1434
- files = fs.readdirSync(dirPath).filter(f => f.startsWith('milestone-') && f.endsWith('.md'));
1522
+ files = fs.readdirSync(dirPath).filter(isJobFile);
1435
1523
  } catch {
1436
1524
  continue;
1437
1525
  }
1438
-
1439
- // Sort by modification time, most recent first
1440
- const fileStats = files.map(f => {
1441
- const fullPath = path.join(dirPath, f);
1442
- let mtime = 0;
1443
- try { mtime = fs.statSync(fullPath).mtimeMs; } catch {}
1444
- return { file: f, fullPath, mtime };
1445
- });
1446
- fileStats.sort((a, b) => b.mtime - a.mtime);
1447
-
1448
- for (const { file, fullPath } of fileStats) {
1526
+ for (const file of files) {
1527
+ const fullPath = path.join(dirPath, file);
1449
1528
  try {
1450
1529
  const parsed = parseJobFile(fullPath);
1451
- groups[groupKey].push({
1452
- version: parsed.version,
1453
- status: parsed.status,
1454
- check: parsed.check,
1455
- created_by: parsed.created_by,
1456
- progress: `${parsed.completedCount}/${parsed.stepCount}`,
1457
- file: fullPath,
1458
- });
1530
+ if (parsed.version && seenVersions.has(parsed.version)) continue;
1531
+ // Group by parsed status, falling back to the dir name if status missing.
1532
+ const groupKey = statusToGroup[parsed.status] || statusToGroup[dirName];
1533
+ if (!groupKey) continue;
1534
+ stageEntry(groupKey, fullPath, parsed);
1535
+ if (parsed.version) seenVersions.add(parsed.version);
1459
1536
  } catch {
1460
1537
  // Skip unparseable files
1461
1538
  }
1462
1539
  }
1463
1540
  }
1464
1541
 
1542
+ // Sort each group most-recent-first and unwrap to entry objects.
1543
+ for (const key of Object.keys(groups)) {
1544
+ staged[key].sort((a, b) => b.mtime - a.mtime);
1545
+ groups[key] = staged[key].map(s => s.entry);
1546
+ }
1547
+
1465
1548
  return groups;
1466
1549
  }
1467
1550
 
@@ -1519,37 +1602,63 @@ function cancelJob(cwd, version) {
1519
1602
  }
1520
1603
 
1521
1604
  /**
1522
- * Validate jobs directory structure. Auto-create missing directories.
1605
+ * Validate jobs directory structure (flat-first).
1606
+ *
1607
+ * Scans the flat `jobs/milestone-*.md` layout first. Legacy
1608
+ * `pending/in-progress/completed` subdirs are an existence-gated optional
1609
+ * fallback: they are counted + parse-validated when present, but are never
1610
+ * created and their absence is not an issue. Only genuine parse failures
1611
+ * flip `healthy` to false.
1523
1612
  *
1524
1613
  * @param {string} cwd - Working directory
1525
1614
  * @returns {{ healthy: boolean, directories: Array, job_count: number, issues?: Array }}
1526
1615
  */
1527
1616
  function healthCheck(cwd) {
1528
1617
  const jobsDir = path.join(getPlanningRoot(cwd), 'jobs');
1529
- const requiredDirs = ['pending', 'in-progress', 'completed'];
1530
1618
  const directories = [];
1531
1619
  const issues = [];
1532
1620
  let jobCount = 0;
1533
1621
 
1534
- for (const dirName of requiredDirs) {
1535
- const dirPath = path.join(jobsDir, dirName);
1536
- const exists = fs.existsSync(dirPath);
1537
- let created = false;
1622
+ // An absent/empty jobs dir is healthy. Ensure it exists, but do not flag it.
1623
+ let jobsCreated = false;
1624
+ if (!fs.existsSync(jobsDir)) {
1625
+ fs.mkdirSync(jobsDir, { recursive: true });
1626
+ jobsCreated = true;
1627
+ }
1628
+
1629
+ // Shared flat-job predicate (identical to findJobFile/listJobs). Excludes
1630
+ // summary files (job-*-SUMMARY.md) since they do not start with 'milestone-'.
1631
+ const isJobFile = f => f.startsWith('milestone-') && f.endsWith('.md');
1538
1632
 
1539
- if (!exists) {
1540
- fs.mkdirSync(dirPath, { recursive: true });
1541
- created = true;
1542
- issues.push(`Missing directory auto-created: ${dirName}/`);
1633
+ // Flat-first scan: jobs/milestone-*.md
1634
+ try {
1635
+ const flatFiles = fs.readdirSync(jobsDir).filter(isJobFile);
1636
+ jobCount += flatFiles.length;
1637
+ for (const file of flatFiles) {
1638
+ const filePath = path.join(jobsDir, file);
1639
+ try {
1640
+ parseJobFile(filePath);
1641
+ } catch (err) {
1642
+ issues.push(`Parse failure in ${file}: ${err.message}`);
1643
+ }
1543
1644
  }
1645
+ } catch {
1646
+ // jobs dir unreadable; treat as empty.
1647
+ }
1648
+
1649
+ directories.push({ name: 'jobs', exists: true, created: jobsCreated });
1544
1650
 
1545
- directories.push({ name: dirName, exists: exists || created, created });
1651
+ // Legacy subdirs: existence-gated optional fallback. Never created.
1652
+ const legacyDirs = ['pending', 'in-progress', 'completed'];
1653
+ for (const dirName of legacyDirs) {
1654
+ const dirPath = path.join(jobsDir, dirName);
1655
+ if (!fs.existsSync(dirPath)) continue;
1656
+
1657
+ directories.push({ name: dirName, exists: true, created: false });
1546
1658
 
1547
- // Scan for job files
1548
1659
  try {
1549
- const files = fs.readdirSync(dirPath).filter(f => f.startsWith('milestone-') && f.endsWith('.md'));
1660
+ const files = fs.readdirSync(dirPath).filter(isJobFile);
1550
1661
  jobCount += files.length;
1551
-
1552
- // Validate each file
1553
1662
  for (const file of files) {
1554
1663
  const filePath = path.join(dirPath, file);
1555
1664
  try {
@@ -1559,7 +1668,7 @@ function healthCheck(cwd) {
1559
1668
  }
1560
1669
  }
1561
1670
  } catch {
1562
- // Directory just created, no files
1671
+ // Subdir unreadable; skip.
1563
1672
  }
1564
1673
  }
1565
1674
 
@@ -2117,6 +2226,8 @@ module.exports = {
2117
2226
  cmdJobsInsertPhaseGapFix,
2118
2227
  cmdJobsCreateMilestone,
2119
2228
  cmdJobsMilestonePreview,
2229
+ analyzeMilestonePhases,
2230
+ detectActiveMilestone,
2120
2231
  cmdJobsListJobs,
2121
2232
  cmdJobsCancelJob,
2122
2233
  cmdJobsRecordStartShas,