@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
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,33 @@ 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
|
+
|
|
24
|
+
## [3.5.1] - 2026-06-28
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **`dgs-tools state reconcile-milestone` (quick-260627-m3k)** — self-heals a project whose milestone shipped (a `## <version>` heading in MILESTONES.md AND a matching git tag both present) but whose STATE.md was never flipped, so `/dgs:list-projects` stops showing a stale in-progress phase or false `executing` status. Conservative (no-op unless both shipped markers are present), idempotent, and current-project scoped.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- **Dashboard staleness after milestone completion (quick-260627-m3k)** — `markMilestoneComplete` now resets the STATE.md `Phase:` line (via a shared `_finalizeMilestoneStateBody` helper) instead of leaving it frozen at the last in-progress value, so a shipped milestone no longer surfaces a stale phase / `executing` status on the dashboard.
|
|
31
|
+
- **Per-milestone finalize matcher (quick-260628-ikd)** — `roadmapUpdatePlanProgressInternal` is now format-agnostic: it updates progress-table rows keyed in the composite `<version>/N.` form (and bare `N.`) and preserves the optional Milestone column, so `phase finalize` / `plan finalize` update per-milestone ROADMAPs instead of silently no-op'ing.
|
|
32
|
+
- **Branch-bounded diff-ref and self-check lookups (quick-260628-mlk)** — the codereview diff-ref, phase-prefix self-check probes, and the milestone-stats query are now bounded to the milestone branch, so per-milestone commit-prefix collisions (`feat(NN-NN):` reused across milestones) no longer reach unrelated commits from a different milestone.
|
|
33
|
+
- **Jobs healthCheck flat-first (quick-260628-kxr-01)** — reworked the jobs `healthCheck` to be flat-first.
|
|
34
|
+
|
|
35
|
+
### Documentation
|
|
36
|
+
- **Stale-dashboard recovery guide (quick-260627-kvy)** — added a "Recovering a stale milestone dashboard" section to `docs/USER-GUIDE.md` documenting `state reconcile-milestone` and the per-project recovery playbook.
|
|
37
|
+
|
|
11
38
|
## [3.5.0] - 2026-06-27
|
|
12
39
|
|
|
13
40
|
### Added — v25.0 Per-Milestone Phase Numbering via Directory Namespacing (Phases 163-170)
|
package/bin/install.js
CHANGED
|
@@ -615,6 +615,22 @@ function convertClaudeToGeminiToml(content) {
|
|
|
615
615
|
return toml;
|
|
616
616
|
}
|
|
617
617
|
|
|
618
|
+
/**
|
|
619
|
+
* Rewrite HOME-relative ".claude" references to the target config dir basename.
|
|
620
|
+
* Swaps ONLY the ".claude" basename, preserving the $HOME / process.env.HOME prefix.
|
|
621
|
+
* For a default ".claude" install cfgBase === ".claude" => both replaces are no-ops.
|
|
622
|
+
* @param {string} content - file content
|
|
623
|
+
* @param {string} cfgBase - target config dir basename, e.g. ".claude-v25-test" or ".claude"
|
|
624
|
+
*/
|
|
625
|
+
function rewriteHomeRelativeConfigPaths(content, cfgBase) {
|
|
626
|
+
// shell form: $HOME/.claude/ -> $HOME/<cfgBase>/
|
|
627
|
+
content = content.replace(/\$HOME\/\.claude\//g, '$HOME/' + cfgBase + '/');
|
|
628
|
+
// JS concat form: process.env.HOME + '/.claude/' (both quote styles)
|
|
629
|
+
// matches a quote char followed by /.claude/ ; $1 backreference preserves the quote
|
|
630
|
+
content = content.replace(/(["'])\/\.claude\//g, '$1/' + cfgBase + '/');
|
|
631
|
+
return content;
|
|
632
|
+
}
|
|
633
|
+
|
|
618
634
|
/**
|
|
619
635
|
* Copy commands to a flat structure for OpenCode
|
|
620
636
|
* OpenCode expects: command/dgs-help.md (invoked as /dgs-help)
|
|
@@ -664,6 +680,8 @@ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
|
|
|
664
680
|
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
665
681
|
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
|
|
666
682
|
content = content.replace(opencodeDirRegex, pathPrefix);
|
|
683
|
+
const cfgBase = pathPrefix.replace(/\/$/, '').split('/').pop();
|
|
684
|
+
content = rewriteHomeRelativeConfigPaths(content, cfgBase);
|
|
667
685
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
668
686
|
content = convertClaudeToOpencodeFrontmatter(content);
|
|
669
687
|
|
|
@@ -705,6 +723,8 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
705
723
|
const localClaudeRegex = /\.\/\.claude\//g;
|
|
706
724
|
content = content.replace(globalClaudeRegex, pathPrefix);
|
|
707
725
|
content = content.replace(localClaudeRegex, `./${dirName}/`);
|
|
726
|
+
const cfgBase = pathPrefix.replace(/\/$/, '').split('/').pop();
|
|
727
|
+
content = rewriteHomeRelativeConfigPaths(content, cfgBase);
|
|
708
728
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
709
729
|
|
|
710
730
|
// Convert frontmatter for opencode compatibility
|
|
@@ -1657,6 +1677,8 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1657
1677
|
// Always replace ~/.claude/ as it is the source of truth in the repo
|
|
1658
1678
|
const dirRegex = /~\/\.claude\//g;
|
|
1659
1679
|
content = content.replace(dirRegex, pathPrefix);
|
|
1680
|
+
const cfgBase = pathPrefix.replace(/\/$/, '').split('/').pop();
|
|
1681
|
+
content = rewriteHomeRelativeConfigPaths(content, cfgBase);
|
|
1660
1682
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
1661
1683
|
// Convert frontmatter for runtime compatibility
|
|
1662
1684
|
if (isOpencode) {
|
|
@@ -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
|
+
});
|