@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
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
const fs = require('fs');
|
|
31
31
|
const path = require('path');
|
|
32
32
|
const { execSync } = require('child_process');
|
|
33
|
-
const { output, error, getProjectRoot } = require('./core.cjs');
|
|
33
|
+
const { output, error, getProjectRoot, phasesDir } = require('./core.cjs');
|
|
34
34
|
const { getPlanningRoot } = require('./paths.cjs');
|
|
35
35
|
const { requireGitIdentity, formatAuthorString } = require('./identity.cjs');
|
|
36
36
|
const { extractFrontmatter, spliceFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
|
|
@@ -1128,7 +1128,9 @@ function analyzeMilestonePhases(cwd, version) {
|
|
|
1128
1128
|
}
|
|
1129
1129
|
|
|
1130
1130
|
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
1131
|
-
|
|
1131
|
+
let phasesAbs;
|
|
1132
|
+
try { phasesAbs = path.join(cwd, phasesDir(cwd)); }
|
|
1133
|
+
catch { phasesAbs = path.join(projectRoot, 'phases'); }
|
|
1132
1134
|
|
|
1133
1135
|
// Find the milestone section: look for heading containing the version + "In Progress" or "SHIPPED"
|
|
1134
1136
|
// Also match the milestone in the summary list to find its phase range
|
|
@@ -1155,52 +1157,77 @@ function analyzeMilestonePhases(cwd, version) {
|
|
|
1155
1157
|
);
|
|
1156
1158
|
const sectionMatch = content.match(sectionHeadingPattern);
|
|
1157
1159
|
if (!sectionMatch) {
|
|
1158
|
-
|
|
1159
|
-
|
|
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;
|
|
1160
1189
|
|
|
1161
|
-
|
|
1162
|
-
|
|
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
|
+
}
|
|
1163
1208
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
let phMatch;
|
|
1179
|
-
while ((phMatch = phaseHeaderPattern.exec(sectionContent)) !== null) {
|
|
1180
|
-
phaseNumbers.push(parseInt(phMatch[1], 10));
|
|
1181
|
-
}
|
|
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
|
+
}
|
|
1182
1223
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
// ROADMAP for Phase N: headings. For single-milestone ROADMAPs all phases
|
|
1186
|
-
// belong to the milestone.
|
|
1187
|
-
if (phaseNumbers.length === 0) {
|
|
1188
|
-
const fullPhasePattern = new RegExp(
|
|
1189
|
-
`Phase\\s+(\\d+)`,
|
|
1190
|
-
'gi'
|
|
1191
|
-
);
|
|
1192
|
-
let fpMatch;
|
|
1193
|
-
while ((fpMatch = fullPhasePattern.exec(content)) !== null) {
|
|
1194
|
-
phaseNumbers.push(parseInt(fpMatch[1], 10));
|
|
1224
|
+
if (phaseNumbers.length === 0) {
|
|
1225
|
+
throw new Error(`Milestone ${version} not found in ROADMAP.md`);
|
|
1195
1226
|
}
|
|
1196
|
-
}
|
|
1197
1227
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1228
|
+
phaseStart = Math.min(...phaseNumbers);
|
|
1229
|
+
phaseEnd = Math.max(...phaseNumbers);
|
|
1200
1230
|
}
|
|
1201
|
-
|
|
1202
|
-
phaseStart = Math.min(...phaseNumbers);
|
|
1203
|
-
phaseEnd = Math.max(...phaseNumbers);
|
|
1204
1231
|
}
|
|
1205
1232
|
|
|
1206
1233
|
// Extract all phase detail sections: ### Phase N: Name
|
|
@@ -1222,12 +1249,12 @@ function analyzeMilestonePhases(cwd, version) {
|
|
|
1222
1249
|
let diskStatus = 'no_directory';
|
|
1223
1250
|
|
|
1224
1251
|
try {
|
|
1225
|
-
const entries = fs.readdirSync(
|
|
1252
|
+
const entries = fs.readdirSync(phasesAbs, { withFileTypes: true });
|
|
1226
1253
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
1227
1254
|
const dirMatch = dirs.find(d => d.startsWith(padded + '-') || d === padded);
|
|
1228
1255
|
|
|
1229
1256
|
if (dirMatch) {
|
|
1230
|
-
const phaseFiles = fs.readdirSync(path.join(
|
|
1257
|
+
const phaseFiles = fs.readdirSync(path.join(phasesAbs, dirMatch));
|
|
1231
1258
|
const planCount = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
|
|
1232
1259
|
const summaryCount = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
|
|
1233
1260
|
const hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
|
|
@@ -1270,7 +1297,13 @@ function detectActiveMilestone(content) {
|
|
|
1270
1297
|
if (match) return match[1];
|
|
1271
1298
|
// Fallback: milestone list item with (in progress)
|
|
1272
1299
|
const listMatch = content.match(/-\s+.*?(v\d+\.\d+).*?\((?:in\s+progress)\)/i);
|
|
1273
|
-
|
|
1300
|
+
if (listMatch) return listMatch[1];
|
|
1301
|
+
// Final fallback: newer single-milestone format with a `**Milestone:** vX.Y`
|
|
1302
|
+
// bold line. Runs LAST so legacy In-Progress heading/list signals win when
|
|
1303
|
+
// present on multi-milestone roadmaps.
|
|
1304
|
+
const boldMatch = content.match(/^\*\*Milestone:\*\*\s*(v\d+\.\d+)/im);
|
|
1305
|
+
if (boldMatch) return boldMatch[1];
|
|
1306
|
+
return null;
|
|
1274
1307
|
}
|
|
1275
1308
|
|
|
1276
1309
|
// ─── Milestone Job CLI Wrappers ─────────────────────────────────────────────
|
|
@@ -1517,37 +1550,63 @@ function cancelJob(cwd, version) {
|
|
|
1517
1550
|
}
|
|
1518
1551
|
|
|
1519
1552
|
/**
|
|
1520
|
-
* Validate jobs directory structure
|
|
1553
|
+
* Validate jobs directory structure (flat-first).
|
|
1554
|
+
*
|
|
1555
|
+
* Scans the flat `jobs/milestone-*.md` layout first. Legacy
|
|
1556
|
+
* `pending/in-progress/completed` subdirs are an existence-gated optional
|
|
1557
|
+
* fallback: they are counted + parse-validated when present, but are never
|
|
1558
|
+
* created and their absence is not an issue. Only genuine parse failures
|
|
1559
|
+
* flip `healthy` to false.
|
|
1521
1560
|
*
|
|
1522
1561
|
* @param {string} cwd - Working directory
|
|
1523
1562
|
* @returns {{ healthy: boolean, directories: Array, job_count: number, issues?: Array }}
|
|
1524
1563
|
*/
|
|
1525
1564
|
function healthCheck(cwd) {
|
|
1526
1565
|
const jobsDir = path.join(getPlanningRoot(cwd), 'jobs');
|
|
1527
|
-
const requiredDirs = ['pending', 'in-progress', 'completed'];
|
|
1528
1566
|
const directories = [];
|
|
1529
1567
|
const issues = [];
|
|
1530
1568
|
let jobCount = 0;
|
|
1531
1569
|
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1570
|
+
// An absent/empty jobs dir is healthy. Ensure it exists, but do not flag it.
|
|
1571
|
+
let jobsCreated = false;
|
|
1572
|
+
if (!fs.existsSync(jobsDir)) {
|
|
1573
|
+
fs.mkdirSync(jobsDir, { recursive: true });
|
|
1574
|
+
jobsCreated = true;
|
|
1575
|
+
}
|
|
1536
1576
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1577
|
+
// Shared flat-job predicate (identical to findJobFile/listJobs). Excludes
|
|
1578
|
+
// summary files (job-*-SUMMARY.md) since they do not start with 'milestone-'.
|
|
1579
|
+
const isJobFile = f => f.startsWith('milestone-') && f.endsWith('.md');
|
|
1580
|
+
|
|
1581
|
+
// Flat-first scan: jobs/milestone-*.md
|
|
1582
|
+
try {
|
|
1583
|
+
const flatFiles = fs.readdirSync(jobsDir).filter(isJobFile);
|
|
1584
|
+
jobCount += flatFiles.length;
|
|
1585
|
+
for (const file of flatFiles) {
|
|
1586
|
+
const filePath = path.join(jobsDir, file);
|
|
1587
|
+
try {
|
|
1588
|
+
parseJobFile(filePath);
|
|
1589
|
+
} catch (err) {
|
|
1590
|
+
issues.push(`Parse failure in ${file}: ${err.message}`);
|
|
1591
|
+
}
|
|
1541
1592
|
}
|
|
1593
|
+
} catch {
|
|
1594
|
+
// jobs dir unreadable; treat as empty.
|
|
1595
|
+
}
|
|
1542
1596
|
|
|
1543
|
-
|
|
1597
|
+
directories.push({ name: 'jobs', exists: true, created: jobsCreated });
|
|
1598
|
+
|
|
1599
|
+
// Legacy subdirs: existence-gated optional fallback. Never created.
|
|
1600
|
+
const legacyDirs = ['pending', 'in-progress', 'completed'];
|
|
1601
|
+
for (const dirName of legacyDirs) {
|
|
1602
|
+
const dirPath = path.join(jobsDir, dirName);
|
|
1603
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
1604
|
+
|
|
1605
|
+
directories.push({ name: dirName, exists: true, created: false });
|
|
1544
1606
|
|
|
1545
|
-
// Scan for job files
|
|
1546
1607
|
try {
|
|
1547
|
-
const files = fs.readdirSync(dirPath).filter(
|
|
1608
|
+
const files = fs.readdirSync(dirPath).filter(isJobFile);
|
|
1548
1609
|
jobCount += files.length;
|
|
1549
|
-
|
|
1550
|
-
// Validate each file
|
|
1551
1610
|
for (const file of files) {
|
|
1552
1611
|
const filePath = path.join(dirPath, file);
|
|
1553
1612
|
try {
|
|
@@ -1557,7 +1616,7 @@ function healthCheck(cwd) {
|
|
|
1557
1616
|
}
|
|
1558
1617
|
}
|
|
1559
1618
|
} catch {
|
|
1560
|
-
//
|
|
1619
|
+
// Subdir unreadable; skip.
|
|
1561
1620
|
}
|
|
1562
1621
|
}
|
|
1563
1622
|
|
|
@@ -1708,7 +1767,9 @@ function generateJobSummary(cwd, version) {
|
|
|
1708
1767
|
try {
|
|
1709
1768
|
const milestonePhases = analyzeMilestonePhases(cwd, normalizedVersion);
|
|
1710
1769
|
const projectRoot = resolveProjectRoot(cwd);
|
|
1711
|
-
|
|
1770
|
+
let phasesAbs;
|
|
1771
|
+
try { phasesAbs = path.join(cwd, phasesDir(cwd)); }
|
|
1772
|
+
catch { phasesAbs = path.join(projectRoot, 'phases'); }
|
|
1712
1773
|
|
|
1713
1774
|
for (const phase of milestonePhases) {
|
|
1714
1775
|
const phaseNum = phase.number;
|
|
@@ -1717,7 +1778,7 @@ function generateJobSummary(cwd, version) {
|
|
|
1717
1778
|
// Find the phase directory on disk
|
|
1718
1779
|
let phaseDir = null;
|
|
1719
1780
|
try {
|
|
1720
|
-
const entries = fs.readdirSync(
|
|
1781
|
+
const entries = fs.readdirSync(phasesAbs, { withFileTypes: true });
|
|
1721
1782
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
1722
1783
|
phaseDir = dirs.find(d => d.startsWith(padded + '-') || d === padded);
|
|
1723
1784
|
} catch {}
|
|
@@ -1725,7 +1786,7 @@ function generateJobSummary(cwd, version) {
|
|
|
1725
1786
|
if (!phaseDir) continue;
|
|
1726
1787
|
|
|
1727
1788
|
// Look for UAT file
|
|
1728
|
-
const uatPath = path.join(
|
|
1789
|
+
const uatPath = path.join(phasesAbs, phaseDir, `${padded}-UAT.md`);
|
|
1729
1790
|
let uatContent = null;
|
|
1730
1791
|
try {
|
|
1731
1792
|
uatContent = fs.readFileSync(uatPath, 'utf-8');
|
|
@@ -1921,7 +1982,9 @@ function rollbackJob(cwd, version) {
|
|
|
1921
1982
|
|
|
1922
1983
|
// Identify executed phases from completed steps
|
|
1923
1984
|
const projectRoot = resolveProjectRoot(cwd);
|
|
1924
|
-
|
|
1985
|
+
let phasesAbs;
|
|
1986
|
+
try { phasesAbs = path.join(cwd, phasesDir(cwd)); }
|
|
1987
|
+
catch { phasesAbs = path.join(projectRoot, 'phases'); }
|
|
1925
1988
|
|
|
1926
1989
|
for (const step of parsed.steps) {
|
|
1927
1990
|
if (step.status === 'completed' && step.command === 'execute-phase') {
|
|
@@ -1932,12 +1995,12 @@ function rollbackJob(cwd, version) {
|
|
|
1932
1995
|
const padded = phaseNum.replace(/^(\d+)/, (m) => m.padStart(2, '0'));
|
|
1933
1996
|
|
|
1934
1997
|
try {
|
|
1935
|
-
const entries = fs.readdirSync(
|
|
1998
|
+
const entries = fs.readdirSync(phasesAbs, { withFileTypes: true });
|
|
1936
1999
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
1937
2000
|
const dirMatch = dirs.find(d => d.startsWith(padded + '-') || d === padded);
|
|
1938
2001
|
|
|
1939
2002
|
if (dirMatch) {
|
|
1940
|
-
const phaseDir = path.join(
|
|
2003
|
+
const phaseDir = path.join(phasesAbs, dirMatch);
|
|
1941
2004
|
const phaseFiles = fs.readdirSync(phaseDir);
|
|
1942
2005
|
const summaryFiles = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
1943
2006
|
|
|
@@ -2111,6 +2174,8 @@ module.exports = {
|
|
|
2111
2174
|
cmdJobsInsertPhaseGapFix,
|
|
2112
2175
|
cmdJobsCreateMilestone,
|
|
2113
2176
|
cmdJobsMilestonePreview,
|
|
2177
|
+
analyzeMilestonePhases,
|
|
2178
|
+
detectActiveMilestone,
|
|
2114
2179
|
cmdJobsListJobs,
|
|
2115
2180
|
cmdJobsCancelJob,
|
|
2116
2181
|
cmdJobsRecordStartShas,
|
|
@@ -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,105 @@ 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
|
+
|
|
1382
1482
|
// ─── findJobFile Tests ──────────────────────────────────────────────────
|
|
1383
1483
|
|
|
1384
1484
|
describe('findJobFile', () => {
|
|
@@ -2124,8 +2224,24 @@ Plans:
|
|
|
2124
2224
|
});
|
|
2125
2225
|
|
|
2126
2226
|
it('returns not_in_progress when job is in pending/', () => {
|
|
2227
|
+
// findJobFile's contract: frontmatter Status is authoritative, the
|
|
2228
|
+
// legacy directory is advisory only ("Frontmatter wins"). A genuinely
|
|
2229
|
+
// pending job therefore needs Status: pending in its frontmatter — using
|
|
2230
|
+
// a status:in-progress fixture here would (correctly) be cancellable.
|
|
2231
|
+
const PENDING_JOB = `# Milestone Job: v6.0
|
|
2232
|
+
|
|
2233
|
+
**Version:** v6.0
|
|
2234
|
+
**Created:** 2026-03-02T10:00:00Z
|
|
2235
|
+
**Status:** pending
|
|
2236
|
+
**Check:** true
|
|
2237
|
+
|
|
2238
|
+
## Steps
|
|
2239
|
+
|
|
2240
|
+
- [ ] \`/dgs:plan-phase 41\`
|
|
2241
|
+
- [ ] \`/dgs:execute-phase 41\`
|
|
2242
|
+
`;
|
|
2127
2243
|
fixture = createFixture({
|
|
2128
|
-
'jobs/pending/milestone-v6.0.md':
|
|
2244
|
+
'jobs/pending/milestone-v6.0.md': PENDING_JOB,
|
|
2129
2245
|
});
|
|
2130
2246
|
const result = cancelJob(fixture.cwd, 'v6.0');
|
|
2131
2247
|
|
|
@@ -2166,18 +2282,46 @@ Plans:
|
|
|
2166
2282
|
assert.equal(result.job_count, 0);
|
|
2167
2283
|
});
|
|
2168
2284
|
|
|
2169
|
-
it('
|
|
2285
|
+
it('does not create legacy subdirs and stays healthy when they are absent', () => {
|
|
2170
2286
|
fixture = createFixture({
|
|
2171
2287
|
'': null,
|
|
2172
2288
|
});
|
|
2173
2289
|
const result = healthCheck(fixture.cwd);
|
|
2174
2290
|
|
|
2175
|
-
//
|
|
2291
|
+
// An empty project (no legacy subdirs) is healthy with no jobs.
|
|
2292
|
+
assert.equal(result.healthy, true);
|
|
2293
|
+
assert.equal(result.job_count, 0);
|
|
2176
2294
|
assert.ok(Array.isArray(result.directories));
|
|
2177
|
-
//
|
|
2178
|
-
assert.
|
|
2179
|
-
assert.
|
|
2180
|
-
assert.
|
|
2295
|
+
// Legacy subdirs must NOT be auto-created.
|
|
2296
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'pending')), false);
|
|
2297
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'in-progress')), false);
|
|
2298
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'completed')), false);
|
|
2299
|
+
});
|
|
2300
|
+
|
|
2301
|
+
it('reports healthy: true for a flat jobs layout and does not create legacy subdirs', () => {
|
|
2302
|
+
fixture = createFixture({
|
|
2303
|
+
'jobs/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2304
|
+
});
|
|
2305
|
+
const result = healthCheck(fixture.cwd);
|
|
2306
|
+
|
|
2307
|
+
assert.equal(result.healthy, true);
|
|
2308
|
+
assert.equal(result.job_count, 1);
|
|
2309
|
+
assert.equal(result.issues.length, 0);
|
|
2310
|
+
// No legacy subdirs created for a flat layout.
|
|
2311
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'pending')), false);
|
|
2312
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'in-progress')), false);
|
|
2313
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'completed')), false);
|
|
2314
|
+
});
|
|
2315
|
+
|
|
2316
|
+
it('excludes job-*-SUMMARY.md files from job_count in a flat layout', () => {
|
|
2317
|
+
fixture = createFixture({
|
|
2318
|
+
'jobs/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2319
|
+
'jobs/job-v6.0-SUMMARY.md': '# Summary',
|
|
2320
|
+
});
|
|
2321
|
+
const result = healthCheck(fixture.cwd);
|
|
2322
|
+
|
|
2323
|
+
assert.equal(result.job_count, 1);
|
|
2324
|
+
assert.equal(result.healthy, true);
|
|
2181
2325
|
});
|
|
2182
2326
|
|
|
2183
2327
|
it('validates each job file parses successfully; reports parse failures as issues', () => {
|
|
@@ -2530,14 +2674,14 @@ describe('jobs root-layout', () => {
|
|
|
2530
2674
|
|
|
2531
2675
|
});
|
|
2532
2676
|
|
|
2533
|
-
it('healthCheck
|
|
2677
|
+
it('healthCheck does not create legacy subdirs at root layout', () => {
|
|
2534
2678
|
fixture = createTempProject({ layout: 'root' });
|
|
2535
2679
|
const result = healthCheck(fixture.cwd);
|
|
2536
|
-
assert.
|
|
2537
|
-
//
|
|
2538
|
-
assert.
|
|
2539
|
-
assert.
|
|
2540
|
-
assert.
|
|
2680
|
+
assert.equal(result.healthy, true);
|
|
2681
|
+
// Legacy subdirs must NOT be auto-created.
|
|
2682
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'pending')), false);
|
|
2683
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'in-progress')), false);
|
|
2684
|
+
assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'completed')), false);
|
|
2541
2685
|
});
|
|
2542
2686
|
|
|
2543
2687
|
it('listJobs works in root layout', () => {
|
|
@@ -423,6 +423,14 @@ describe('CLI: migrate command (MIG-08, OPT-02)', () => {
|
|
|
423
423
|
|
|
424
424
|
beforeEach(() => {
|
|
425
425
|
dir = makeGitDir();
|
|
426
|
+
// Real DGS repos gitignore config.local.json — the local half of the
|
|
427
|
+
// documented two-file config layout (config.json tracked / config.local.json
|
|
428
|
+
// gitignored). dgs-tools writes config.local.json on startup (e.g.
|
|
429
|
+
// { branching_migration_done: true }); without the .gitignore that real
|
|
430
|
+
// installs carry, that untracked write dirties the tree and trips migrate's
|
|
431
|
+
// (correct) clean-working-tree guard with exit 1. Mirror the real layout so
|
|
432
|
+
// the guard is exercised honestly rather than failing on a fixture artifact.
|
|
433
|
+
writeFile(dir, '.gitignore', 'config.local.json\n');
|
|
426
434
|
});
|
|
427
435
|
|
|
428
436
|
afterEach(() => {
|