@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
|
@@ -1157,52 +1157,77 @@ function analyzeMilestonePhases(cwd, version) {
|
|
|
1157
1157
|
);
|
|
1158
1158
|
const sectionMatch = content.match(sectionHeadingPattern);
|
|
1159
1159
|
if (!sectionMatch) {
|
|
1160
|
-
|
|
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
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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
|
-
|
|
1186
|
-
|
|
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
|
-
|
|
1201
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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(
|
|
1522
|
+
files = fs.readdirSync(dirPath).filter(isJobFile);
|
|
1435
1523
|
} catch {
|
|
1436
1524
|
continue;
|
|
1437
1525
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
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
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
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
|
|
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
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
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
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
//
|
|
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,
|