@ktpartners/dgs-platform 3.5.1 → 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 +13 -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 +78 -26
- package/deliver-great-systems/bin/lib/jobs.test.cjs +132 -3
- 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 +14 -0
- 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/workflows/audit-milestone.md +1 -1
- package/deliver-great-systems/workflows/cleanup.md +1 -1
- package/deliver-great-systems/workflows/complete-milestone.md +2 -2
- package/deliver-great-systems/workflows/discuss-phase.md +5 -1
- 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
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,19 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [3.5.3] - 2026-06-28
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **Flat-first jobs tests (quick-260628-r5i)** — aligned the root `tests/jobs.test.cjs` with the flat-first `listJobs` behavior introduced in 3.5.2 (which had updated `bin/lib/jobs.test.cjs` but left the root copy stale, leaving the suite 61/2 on that file). The grouped-jobs test now writes flat `jobs/milestone-*.md` files grouped by frontmatter status, and the directory test asserts the legacy `jobs/pending|in-progress|completed/` subdirs are NOT auto-created. Suite back to green.
|
|
15
|
+
|
|
16
|
+
## [3.5.2] - 2026-06-28
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **Flat-store readers flat-first (quick-260628-otc)** — `search.cjs scanIdeas`, `jobs.cjs listJobs`, and `ideas.cjs cmdIdeasFindRelated` now read the migrated flat frontmatter-status layout (`ideas/*.md`, `jobs/milestone-*.md`) first, so `/dgs:search`, `/dgs:list-jobs`, `/dgs:diff-report`, and `/dgs:find-related-ideas` no longer silently return nothing on flat installs. `listJobs` is now read-only (stops recreating abandoned `jobs/{pending,in-progress,completed}` subdirs) and `cmdIdeasUpdate` returns the file's real flat path. Legacy status-subdirs remain an existence-gated fallback.
|
|
20
|
+
- **Versioned phases-dir scanners (quick-260628-p59)** — `verify.cjs` (consistency + health W010), `overlap.cjs`, `projects.cjs`, and `package-scan-report.cjs` now descend `phases/<version>/NN-slug/` (via `core.phasesDir` / `isValidMilestoneVersion`) instead of treating the `vN.N` version dir as a phase dir, so consistency validation, `/dgs:overlap-check`, repos-touched listings, and package-scan active-phase reports work under per-milestone numbering. Flat layout still supported.
|
|
21
|
+
- **Versioned phases-dir workflow globs (quick-260628-pn3)** — 9 phases-dir globs/heuristics across 7 workflows (`complete-milestone`, `audit-milestone`, `plan-phase`, `discuss-phase`, `pause-work`, `resume-project`, `cleanup`) are now dual-layout (flat + `phases/<version>/`), so milestone-completion accomplishment extraction, SUMMARY cross-checks, prior-phase continuity, pause/resume detection, and cleanup no longer come up empty on versioned milestones. Also drops a GNU-only `grep -oP`.
|
|
22
|
+
- **Roadmap milestone parsers recognize all formats (quick-260628-pxx)** — `detectActiveMilestone`, `getMilestoneInfo`, `parseProjectMilestone`, `cmdPhaseAdd` range-bump, and `cmdRoadmapAnalyze` now recognize the ad-hoc `## Active Milestone: vX.Y`, single-milestone bold-line `**Milestone:** vX.Y`, and per-milestone-namespaced `Phases vX.Y/N-M` formats (additively, milestone-name-preserving), gated against multi-milestone over-match. Fixes blank dashboard milestone, `create-milestone` "No active milestone found", and stale `add-phase` ranges on the newer roadmap formats.
|
|
23
|
+
|
|
11
24
|
## [3.5.1] - 2026-06-28
|
|
12
25
|
|
|
13
26
|
### Added
|
|
@@ -534,6 +534,27 @@ function getMilestoneInfo(cwd) {
|
|
|
534
534
|
name: headingMatch[2].trim(),
|
|
535
535
|
};
|
|
536
536
|
}
|
|
537
|
+
// Ad-hoc container format: "## Active Milestone: v25.1 add-prs" — version
|
|
538
|
+
// and real NAME live on the heading. Runs after the heading branch so a
|
|
539
|
+
// legacy "### vX.Y Name" heading still wins; the literal "Active Milestone:"
|
|
540
|
+
// prefix is specific enough to run un-gated.
|
|
541
|
+
const adhoc = cleaned.match(/^##\s+Active Milestone:\s*v(\d+\.\d+)\s+(.+?)\s*$/im);
|
|
542
|
+
if (adhoc) {
|
|
543
|
+
return { version: 'v' + adhoc[1], name: adhoc[2].trim() };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Bold-line single-milestone format: "**Milestone:** v30.1 — Name".
|
|
547
|
+
// GATED single-milestone (mirror analyzeMilestonePhases): only when at most
|
|
548
|
+
// one version-bearing heading exists, so a stray bold line never hijacks a
|
|
549
|
+
// multi-milestone heading roadmap.
|
|
550
|
+
const versionHeadings = cleaned.match(/^#{2,4}\s+.*v\d+\.\d+/gim) || [];
|
|
551
|
+
if (versionHeadings.length <= 1) {
|
|
552
|
+
const bold = cleaned.match(/^\*\*Milestone:\*\*\s*v(\d+\.\d+)\s*(?:[—-]\s*)?(.*)$/im);
|
|
553
|
+
if (bold) {
|
|
554
|
+
return { version: 'v' + bold[1], name: bold[2].trim() || 'milestone' };
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
537
558
|
// Fallback: try bare version match
|
|
538
559
|
const versionMatch = cleaned.match(/v(\d+\.\d+)/);
|
|
539
560
|
return {
|
|
@@ -28,6 +28,7 @@ const {
|
|
|
28
28
|
resolveModelInternal,
|
|
29
29
|
MODEL_PROFILES,
|
|
30
30
|
findPhaseInternal,
|
|
31
|
+
getMilestoneInfo,
|
|
31
32
|
} = require('./core.cjs');
|
|
32
33
|
|
|
33
34
|
// ─── Root layout (no v2 markers) Tests ───────────────────────────────────────
|
|
@@ -1157,3 +1158,68 @@ describe('MODEL_PROFILES dgs-idea-researcher tiering', () => {
|
|
|
1157
1158
|
});
|
|
1158
1159
|
|
|
1159
1160
|
});
|
|
1161
|
+
|
|
1162
|
+
// ─── getMilestoneInfo format coverage ───────────────────────────────────────
|
|
1163
|
+
describe('getMilestoneInfo all-format coverage', () => {
|
|
1164
|
+
let fixture;
|
|
1165
|
+
|
|
1166
|
+
afterEach(() => {
|
|
1167
|
+
if (fixture) fixture.cleanup();
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
const ADHOC_ROADMAP = `# Roadmap
|
|
1171
|
+
|
|
1172
|
+
## Active Milestone: v25.1 add-prs
|
|
1173
|
+
|
|
1174
|
+
**Goal:** add-prs
|
|
1175
|
+
**Status:** In progress (ad-hoc container — phases added on demand via /dgs:add-phase)
|
|
1176
|
+
|
|
1177
|
+
Phases:
|
|
1178
|
+
|
|
1179
|
+
---
|
|
1180
|
+
`;
|
|
1181
|
+
|
|
1182
|
+
const BOLD_LINE_ROADMAP = `# Roadmap: Tenant data-quality metrics in Admin UI (v30.1)
|
|
1183
|
+
|
|
1184
|
+
**Milestone:** v30.1 — Tenant data-quality metrics in Admin UI
|
|
1185
|
+
|
|
1186
|
+
## Overview
|
|
1187
|
+
|
|
1188
|
+
Single-milestone roadmap. Version is only in the title and the bold line.
|
|
1189
|
+
`;
|
|
1190
|
+
|
|
1191
|
+
// Multi-milestone heading roadmap WITH a stray bold line: the heading branch
|
|
1192
|
+
// must win, the gated bold-line branch must stay inert.
|
|
1193
|
+
const MULTI_MILESTONE_WITH_STRAY_BOLD = `# Roadmap
|
|
1194
|
+
|
|
1195
|
+
**Milestone:** v9.9 — stray bold line
|
|
1196
|
+
|
|
1197
|
+
## v1.0 Foundation
|
|
1198
|
+
Some shipped work.
|
|
1199
|
+
|
|
1200
|
+
## v2.0 Next
|
|
1201
|
+
Current work here.
|
|
1202
|
+
`;
|
|
1203
|
+
|
|
1204
|
+
it('returns the real name for an ad-hoc roadmap (not generic milestone)', () => {
|
|
1205
|
+
fixture = createFixture({ 'ROADMAP.md': ADHOC_ROADMAP });
|
|
1206
|
+
assert.deepEqual(getMilestoneInfo(fixture.cwd), { version: 'v25.1', name: 'add-prs' });
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
it('returns the real name for a bold-line single-milestone roadmap', () => {
|
|
1210
|
+
fixture = createFixture({ 'ROADMAP.md': BOLD_LINE_ROADMAP });
|
|
1211
|
+
assert.deepEqual(getMilestoneInfo(fixture.cwd), {
|
|
1212
|
+
version: 'v30.1',
|
|
1213
|
+
name: 'Tenant data-quality metrics in Admin UI',
|
|
1214
|
+
});
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
it('resolves a multi-milestone heading roadmap via the heading branch (bold line does not hijack)', () => {
|
|
1218
|
+
fixture = createFixture({ 'ROADMAP.md': MULTI_MILESTONE_WITH_STRAY_BOLD });
|
|
1219
|
+
const info = getMilestoneInfo(fixture.cwd);
|
|
1220
|
+
// Heading branch wins → version is the first heading's (v1.0), NOT the stray
|
|
1221
|
+
// bold line's v9.9. The key assertion is that the bold line did not hijack.
|
|
1222
|
+
assert.notEqual(info.version, 'v9.9');
|
|
1223
|
+
assert.equal(info.version, 'v1.0');
|
|
1224
|
+
});
|
|
1225
|
+
});
|
|
@@ -475,11 +475,12 @@ function cmdIdeasUpdate(cwd, idOrFilename, field, value, raw, author) {
|
|
|
475
475
|
}
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
-
const planRootRel = path.relative(cwd, getPlanningRoot(cwd)) || '.';
|
|
479
478
|
const result = {
|
|
480
479
|
id: fm.id,
|
|
481
480
|
filename: newFilename,
|
|
482
|
-
|
|
481
|
+
// Derive the returned path from the file's REAL location so it is correct
|
|
482
|
+
// for both flat (dirname is ideas/) and legacy (dirname is ideas/<state>/).
|
|
483
|
+
path: path.relative(cwd, path.join(path.dirname(idea.path), newFilename)),
|
|
483
484
|
field,
|
|
484
485
|
old_value: oldValue,
|
|
485
486
|
new_value: value,
|
|
@@ -1294,23 +1295,50 @@ function cmdIdeasFindRelated(cwd, options, raw) {
|
|
|
1294
1295
|
|
|
1295
1296
|
const anchorTags = anchor.frontmatter.tags || [];
|
|
1296
1297
|
|
|
1297
|
-
// 2. Load all other pending ideas
|
|
1298
|
-
|
|
1299
|
-
|
|
1298
|
+
// 2. Load all other pending ideas — flat-first (mirror cmdIdeasList's flat
|
|
1299
|
+
// scan), with legacy ideas/pending/ as an existence-gated dedup fallback.
|
|
1300
|
+
const candidateContents = [];
|
|
1301
|
+
const seenIds = new Set();
|
|
1302
|
+
|
|
1303
|
+
const flatDir = path.join(getPlanningRoot(cwd), 'ideas');
|
|
1300
1304
|
try {
|
|
1301
|
-
|
|
1305
|
+
const flatFiles = fs.readdirSync(flatDir).filter(f => f.endsWith('.md') && !fs.statSync(path.join(flatDir, f)).isDirectory());
|
|
1306
|
+
for (const file of flatFiles) {
|
|
1307
|
+
const content = safeReadFile(path.join(flatDir, file));
|
|
1308
|
+
if (!content) continue;
|
|
1309
|
+
const parsed = parseIdeaFrontmatter(content);
|
|
1310
|
+
if ((parsed.frontmatter.status || 'pending') !== 'pending') continue;
|
|
1311
|
+
candidateContents.push(content);
|
|
1312
|
+
seenIds.add(parsed.frontmatter.id);
|
|
1313
|
+
}
|
|
1302
1314
|
} catch {
|
|
1303
|
-
|
|
1315
|
+
// Flat directory may not exist — continue to legacy scan
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// Legacy fallback: ideas/pending/ (existence-gated, deduped by id)
|
|
1319
|
+
const pendingDir = path.join(getPlanningRoot(cwd), 'ideas', 'pending');
|
|
1320
|
+
if (fs.existsSync(pendingDir)) {
|
|
1321
|
+
let pendingFiles;
|
|
1322
|
+
try {
|
|
1323
|
+
pendingFiles = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
|
|
1324
|
+
} catch {
|
|
1325
|
+
pendingFiles = [];
|
|
1326
|
+
}
|
|
1327
|
+
for (const file of pendingFiles) {
|
|
1328
|
+
const content = safeReadFile(path.join(pendingDir, file));
|
|
1329
|
+
if (!content) continue;
|
|
1330
|
+
const parsed = parseIdeaFrontmatter(content);
|
|
1331
|
+
if (seenIds.has(parsed.frontmatter.id)) continue;
|
|
1332
|
+
candidateContents.push(content);
|
|
1333
|
+
seenIds.add(parsed.frontmatter.id);
|
|
1334
|
+
}
|
|
1304
1335
|
}
|
|
1305
1336
|
|
|
1306
1337
|
// 3. Score each candidate for tag overlap
|
|
1307
1338
|
const matchLevelOrder = { HIGH: 0, MEDIUM: 1, LOW: 2, NONE: 3 };
|
|
1308
1339
|
const candidates = [];
|
|
1309
1340
|
|
|
1310
|
-
for (const
|
|
1311
|
-
const content = safeReadFile(path.join(pendingDir, file));
|
|
1312
|
-
if (!content) continue;
|
|
1313
|
-
|
|
1341
|
+
for (const content of candidateContents) {
|
|
1314
1342
|
const parsed = parseIdeaFrontmatter(content);
|
|
1315
1343
|
const candidateId = parsed.frontmatter.id;
|
|
1316
1344
|
|
|
@@ -10,7 +10,7 @@ const path = require('path');
|
|
|
10
10
|
const { execSync } = require('child_process');
|
|
11
11
|
const { createTempDir, cleanupDir, createTempProject, initGitRepo } = require('./test-helpers.cjs');
|
|
12
12
|
const { resetPaths } = require('./paths.cjs');
|
|
13
|
-
const { findIdeaFile, parseIdeaFrontmatter, ensureIdeasDirs, cmdIdeasCreate, cmdIdeasList, loadManifest, IDEA_STATES, IDEA_STATUSES, setIdeaStatus, buildIdeaContent, cmdIdeasConsolidate, cmdIdeasFindRelated, cmdIdeasUndoConsolidation } = require('./ideas.cjs');
|
|
13
|
+
const { findIdeaFile, parseIdeaFrontmatter, ensureIdeasDirs, cmdIdeasCreate, cmdIdeasList, cmdIdeasUpdate, loadManifest, IDEA_STATES, IDEA_STATUSES, setIdeaStatus, buildIdeaContent, cmdIdeasConsolidate, cmdIdeasFindRelated, cmdIdeasUndoConsolidation } = require('./ideas.cjs');
|
|
14
14
|
|
|
15
15
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
16
16
|
|
|
@@ -949,8 +949,9 @@ describe('cmdIdeasFindRelated', () => {
|
|
|
949
949
|
const filename = `${paddedId}-${slug}.md`;
|
|
950
950
|
const now = new Date().toISOString();
|
|
951
951
|
const tagsStr = tags.length > 0 ? `[${tags.map(t => `"${t}"`).join(', ')}]` : '[]';
|
|
952
|
-
const content = `---\nid: ${id}\ntitle: "${title}"\ntags: ${tagsStr}\ncreated: ${now}\nupdated: ${now}\n---\n\n${body || title + ' body.'}\n`;
|
|
953
|
-
|
|
952
|
+
const content = `---\nid: ${id}\ntitle: "${title}"\nstatus: pending\ntags: ${tagsStr}\ncreated: ${now}\nupdated: ${now}\n---\n\n${body || title + ' body.'}\n`;
|
|
953
|
+
// Flat layout: ideas/<paddedId>-<slug>.md (status in frontmatter)
|
|
954
|
+
const dir2 = path.join(dir, 'ideas');
|
|
954
955
|
fs.mkdirSync(dir2, { recursive: true });
|
|
955
956
|
fs.writeFileSync(path.join(dir2, filename), content, 'utf-8');
|
|
956
957
|
// Update manifest so allocateId returns next ID correctly
|
|
@@ -1168,6 +1169,34 @@ describe('cmdIdeasFindRelated', () => {
|
|
|
1168
1169
|
`match_level should be HIGH, MEDIUM, or LOW, got: ${r.match_level}`);
|
|
1169
1170
|
}
|
|
1170
1171
|
});
|
|
1172
|
+
|
|
1173
|
+
it('finds flat-layout pending candidates', () => {
|
|
1174
|
+
// createPendingIdea now seeds FLAT ideas/*.md with status: pending.
|
|
1175
|
+
createPendingIdea(tmpDir, 1, 'Anchor idea', ['api', 'reliability']);
|
|
1176
|
+
createPendingIdea(tmpDir, 2, 'Flat candidate', ['api', 'reliability']);
|
|
1177
|
+
const output = captureOutput(() => {
|
|
1178
|
+
cmdIdeasFindRelated(tmpDir, { id: '1', threshold: 'low' }, true);
|
|
1179
|
+
});
|
|
1180
|
+
const result = JSON.parse(output);
|
|
1181
|
+
assert.ok(result.results.length >= 1, 'should find at least one flat candidate');
|
|
1182
|
+
assert.strictEqual(result.results[0].id, 2);
|
|
1183
|
+
assert.strictEqual(result.counts.total, 1);
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
it('ideas update returns the real flat path (no state subdir segment)', () => {
|
|
1187
|
+
// Seed a FLAT pending idea, then rename via title update.
|
|
1188
|
+
createPendingIdea(tmpDir, 1, 'Original title', ['api']);
|
|
1189
|
+
const output = captureOutput(() => {
|
|
1190
|
+
cmdIdeasUpdate(tmpDir, '1', 'title', 'Renamed title', true);
|
|
1191
|
+
});
|
|
1192
|
+
const result = JSON.parse(output);
|
|
1193
|
+
// Path must point to the flat location ideas/<file>, never ideas/pending/<file>.
|
|
1194
|
+
assert.ok(!result.path.includes(`${path.sep}pending${path.sep}`),
|
|
1195
|
+
`path should not contain a pending/ segment, got: ${result.path}`);
|
|
1196
|
+
assert.match(result.path, /^ideas[\\/]001-renamed-title\.md$/);
|
|
1197
|
+
assert.ok(fs.existsSync(path.join(tmpDir, result.path)),
|
|
1198
|
+
`renamed file should exist at ${result.path}`);
|
|
1199
|
+
});
|
|
1171
1200
|
});
|
|
1172
1201
|
|
|
1173
1202
|
// ─── cmdIdeasUndoConsolidation ────────────────────────────────────────────────
|
|
@@ -1472,6 +1472,29 @@ function parseProjectMilestone(roadmapPath) {
|
|
|
1472
1472
|
return { milestone_version: 'v' + headingMatch[1], milestone_name: headingMatch[2].trim() };
|
|
1473
1473
|
}
|
|
1474
1474
|
|
|
1475
|
+
// Ad-hoc container: "## Active Milestone: v25.1 add-prs" (mirror getMilestoneInfo)
|
|
1476
|
+
const adhoc = cleaned.match(/^##\s+Active Milestone:\s*v(\d+\.\d+)\s+(.+?)\s*$/im);
|
|
1477
|
+
if (adhoc) {
|
|
1478
|
+
return { milestone_version: 'v' + adhoc[1], milestone_name: adhoc[2].trim() };
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// Bold-line single-milestone: "**Milestone:** v30.1 — Name", GATED to <=1
|
|
1482
|
+
// version-bearing heading so a stray bold line never hijacks a multi-milestone
|
|
1483
|
+
// roadmap (mirror getMilestoneInfo / analyzeMilestonePhases).
|
|
1484
|
+
const versionHeadings = cleaned.match(/^#{2,4}\s+.*v\d+\.\d+/gim) || [];
|
|
1485
|
+
if (versionHeadings.length <= 1) {
|
|
1486
|
+
const bold = cleaned.match(/^\*\*Milestone:\*\*\s*v(\d+\.\d+)\s*(?:[—-]\s*)?(.*)$/im);
|
|
1487
|
+
if (bold) {
|
|
1488
|
+
return { milestone_version: 'v' + bold[1], milestone_name: bold[2].trim() || 'milestone' };
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// Bare-version fallback (mirror getMilestoneInfo): any vX.Y rather than {null,null}.
|
|
1493
|
+
const versionMatch = cleaned.match(/v(\d+\.\d+)/);
|
|
1494
|
+
if (versionMatch) {
|
|
1495
|
+
return { milestone_version: versionMatch[0], milestone_name: 'milestone' };
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1475
1498
|
return { milestone_version: null, milestone_name: null };
|
|
1476
1499
|
}
|
|
1477
1500
|
|
|
@@ -2255,3 +2255,81 @@ describe('init execute-phase coerces ambiguous archive number to not-found (LOOK
|
|
|
2255
2255
|
}
|
|
2256
2256
|
});
|
|
2257
2257
|
});
|
|
2258
|
+
|
|
2259
|
+
// ─── progress-all milestone parsing (parseProjectMilestone) ─────────────────
|
|
2260
|
+
describe('init progress-all: parseProjectMilestone format coverage', () => {
|
|
2261
|
+
let fixture;
|
|
2262
|
+
|
|
2263
|
+
afterEach(() => {
|
|
2264
|
+
if (fixture) fixture.cleanup();
|
|
2265
|
+
});
|
|
2266
|
+
|
|
2267
|
+
// Build a v2 fixture whose single active project's ROADMAP.md carries the
|
|
2268
|
+
// supplied content, so progress-all → parseProjectMilestone exercises it.
|
|
2269
|
+
function v2FixtureWithRoadmap(roadmapContent) {
|
|
2270
|
+
return createFixture({
|
|
2271
|
+
'config.json': JSON.stringify({ current_project: 'test-project' }),
|
|
2272
|
+
'PROJECTS.md': '# Projects\n\n| Project | Status |\n|---------|--------|\n| test-project | Active |\n',
|
|
2273
|
+
'REPOS.md': '# Repos\n\n| Name | Path |\n|------|------|\n',
|
|
2274
|
+
'projects/test-project/STATE.md': '# State',
|
|
2275
|
+
'projects/test-project/ROADMAP.md': roadmapContent,
|
|
2276
|
+
'projects/test-project/REQUIREMENTS.md': '# Requirements',
|
|
2277
|
+
'projects/test-project/PROJECT.md': '# Project',
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
const ADHOC_ROADMAP = `# Roadmap
|
|
2282
|
+
|
|
2283
|
+
## Active Milestone: v25.1 add-prs
|
|
2284
|
+
|
|
2285
|
+
**Goal:** add-prs
|
|
2286
|
+
**Status:** In progress (ad-hoc container — phases added on demand via /dgs:add-phase)
|
|
2287
|
+
|
|
2288
|
+
Phases:
|
|
2289
|
+
|
|
2290
|
+
---
|
|
2291
|
+
`;
|
|
2292
|
+
|
|
2293
|
+
const BOLD_LINE_ROADMAP = `# Roadmap: Tenant data-quality metrics in Admin UI (v30.1)
|
|
2294
|
+
|
|
2295
|
+
**Milestone:** v30.1 — Tenant data-quality metrics in Admin UI
|
|
2296
|
+
|
|
2297
|
+
## Overview
|
|
2298
|
+
|
|
2299
|
+
Single-milestone roadmap.
|
|
2300
|
+
`;
|
|
2301
|
+
|
|
2302
|
+
// Only a bare vX.Y appears anywhere — exercises the new bare-version fallback.
|
|
2303
|
+
const BARE_VERSION_ROADMAP = `# Roadmap
|
|
2304
|
+
|
|
2305
|
+
Some prose mentioning v42.7 without any heading or bold milestone line.
|
|
2306
|
+
`;
|
|
2307
|
+
|
|
2308
|
+
function projectEntry(result) {
|
|
2309
|
+
return (result.projects || []).find(p => p.name === 'test-project');
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
it('populates milestone_version + milestone_name for an ad-hoc project roadmap', () => {
|
|
2313
|
+
fixture = v2FixtureWithRoadmap(ADHOC_ROADMAP);
|
|
2314
|
+
const p = projectEntry(runInit(fixture.cwd, 'progress-all --raw'));
|
|
2315
|
+
assert.ok(p, 'expected the active project in progress-all output');
|
|
2316
|
+
assert.equal(p.milestone_version, 'v25.1');
|
|
2317
|
+
assert.equal(p.milestone_name, 'add-prs');
|
|
2318
|
+
});
|
|
2319
|
+
|
|
2320
|
+
it('populates milestone_version + milestone_name for a bold-line project roadmap', () => {
|
|
2321
|
+
fixture = v2FixtureWithRoadmap(BOLD_LINE_ROADMAP);
|
|
2322
|
+
const p = projectEntry(runInit(fixture.cwd, 'progress-all --raw'));
|
|
2323
|
+
assert.ok(p, 'expected the active project in progress-all output');
|
|
2324
|
+
assert.equal(p.milestone_version, 'v30.1');
|
|
2325
|
+
assert.equal(p.milestone_name, 'Tenant data-quality metrics in Admin UI');
|
|
2326
|
+
});
|
|
2327
|
+
|
|
2328
|
+
it('bare-version fallback returns the version (no longer null) when only vX.Y appears', () => {
|
|
2329
|
+
fixture = v2FixtureWithRoadmap(BARE_VERSION_ROADMAP);
|
|
2330
|
+
const p = projectEntry(runInit(fixture.cwd, 'progress-all --raw'));
|
|
2331
|
+
assert.ok(p, 'expected the active project in progress-all output');
|
|
2332
|
+
assert.equal(p.milestone_version, 'v42.7');
|
|
2333
|
+
assert.notEqual(p.milestone_version, null);
|
|
2334
|
+
});
|
|
2335
|
+
});
|
|
@@ -1298,6 +1298,12 @@ function detectActiveMilestone(content) {
|
|
|
1298
1298
|
// Fallback: milestone list item with (in progress)
|
|
1299
1299
|
const listMatch = content.match(/-\s+.*?(v\d+\.\d+).*?\((?:in\s+progress)\)/i);
|
|
1300
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];
|
|
1301
1307
|
// Final fallback: newer single-milestone format with a `**Milestone:** vX.Y`
|
|
1302
1308
|
// bold line. Runs LAST so legacy In-Progress heading/list signals win when
|
|
1303
1309
|
// present on multi-milestone roadmaps.
|
|
@@ -1333,7 +1339,14 @@ function cmdJobsCreateMilestone(cwd, version, check, raw) {
|
|
|
1333
1339
|
throw new Error('No active milestone found in ROADMAP.md');
|
|
1334
1340
|
}
|
|
1335
1341
|
} else {
|
|
1336
|
-
// 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.
|
|
1337
1350
|
const escapedV = resolvedVersion.replace(/\./g, '\\.');
|
|
1338
1351
|
const versionPattern = new RegExp(escapedV + '\\s+', 'i');
|
|
1339
1352
|
if (!versionPattern.test(roadmapContent)) {
|
|
@@ -1452,47 +1465,86 @@ function cmdJobsMilestonePreview(cwd, version, check, raw) {
|
|
|
1452
1465
|
function listJobs(cwd) {
|
|
1453
1466
|
const jobsDir = path.join(getPlanningRoot(cwd), 'jobs');
|
|
1454
1467
|
const groups = { in_progress: [], pending: [], completed: [] };
|
|
1455
|
-
const
|
|
1468
|
+
const statusToGroup = { 'in-progress': 'in_progress', 'pending': 'pending', 'completed': 'completed' };
|
|
1456
1469
|
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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
|
+
}
|
|
1493
|
+
|
|
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
|
|
1461
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;
|
|
1462
1519
|
|
|
1463
1520
|
let files;
|
|
1464
1521
|
try {
|
|
1465
|
-
files = fs.readdirSync(dirPath).filter(
|
|
1522
|
+
files = fs.readdirSync(dirPath).filter(isJobFile);
|
|
1466
1523
|
} catch {
|
|
1467
1524
|
continue;
|
|
1468
1525
|
}
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
const fileStats = files.map(f => {
|
|
1472
|
-
const fullPath = path.join(dirPath, f);
|
|
1473
|
-
let mtime = 0;
|
|
1474
|
-
try { mtime = fs.statSync(fullPath).mtimeMs; } catch {}
|
|
1475
|
-
return { file: f, fullPath, mtime };
|
|
1476
|
-
});
|
|
1477
|
-
fileStats.sort((a, b) => b.mtime - a.mtime);
|
|
1478
|
-
|
|
1479
|
-
for (const { file, fullPath } of fileStats) {
|
|
1526
|
+
for (const file of files) {
|
|
1527
|
+
const fullPath = path.join(dirPath, file);
|
|
1480
1528
|
try {
|
|
1481
1529
|
const parsed = parseJobFile(fullPath);
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
file: fullPath,
|
|
1489
|
-
});
|
|
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);
|
|
1490
1536
|
} catch {
|
|
1491
1537
|
// Skip unparseable files
|
|
1492
1538
|
}
|
|
1493
1539
|
}
|
|
1494
1540
|
}
|
|
1495
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
|
+
|
|
1496
1548
|
return groups;
|
|
1497
1549
|
}
|
|
1498
1550
|
|
|
@@ -1479,6 +1479,100 @@ Plans:
|
|
|
1479
1479
|
});
|
|
1480
1480
|
});
|
|
1481
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
|
+
|
|
1482
1576
|
// ─── findJobFile Tests ──────────────────────────────────────────────────
|
|
1483
1577
|
|
|
1484
1578
|
describe('findJobFile', () => {
|
|
@@ -2086,10 +2180,11 @@ Plans:
|
|
|
2086
2180
|
assert.deepEqual(result.completed, []);
|
|
2087
2181
|
});
|
|
2088
2182
|
|
|
2089
|
-
it('
|
|
2183
|
+
it('groups FLAT jobs by frontmatter status with correct fields', () => {
|
|
2184
|
+
// Flat layout: jobs/milestone-*.md grouped by parsed status header.
|
|
2090
2185
|
fixture = createFixture({
|
|
2091
|
-
'jobs/
|
|
2092
|
-
'jobs/
|
|
2186
|
+
'jobs/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2187
|
+
'jobs/milestone-v5.0.md': ALL_COMPLETED_JOB,
|
|
2093
2188
|
});
|
|
2094
2189
|
const result = listJobs(fixture.cwd);
|
|
2095
2190
|
|
|
@@ -2111,6 +2206,40 @@ Plans:
|
|
|
2111
2206
|
assert.equal(comp.progress, '2/2');
|
|
2112
2207
|
});
|
|
2113
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
|
+
|
|
2114
2243
|
it('shows check flag false when --no-check was used', () => {
|
|
2115
2244
|
fixture = createFixture({
|
|
2116
2245
|
'jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
|