@ktpartners/dgs-platform 3.4.2 → 3.5.0
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 +14 -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 +12 -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 +16 -10
- package/deliver-great-systems/bin/lib/jobs.test.cjs +17 -1
- 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 +9 -6
- 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/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/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
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,20 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [3.5.0] - 2026-06-27
|
|
12
|
+
|
|
13
|
+
### Added — v25.0 Per-Milestone Phase Numbering via Directory Namespacing (Phases 163-170)
|
|
14
|
+
- **Phase numbers now restart at `01` per milestone**, stored in version-namespaced directories `phases/<version>/NN-slug/` instead of one global flat `phases/`. The product-global version sequence (`vX.Y`) is unchanged — only phase *numbers* became milestone-local. Pre-existing flat-layout projects keep working (dual-mode), so no migration is forced.
|
|
15
|
+
- **Authoritative version signal** — the active milestone version is sourced from STATE.md `current_milestone` (grammar-validated `^v\d+\.\d+$`, fail-loud, never silently defaulting to `v1.0`); the ROADMAP marker is advisory, and on mismatch the resolver warns and trusts STATE.md. `new-milestone` + `init.cjs` are the sole setters.
|
|
16
|
+
- **Canonical version-aware resolver** — a single `phasesDir(cwd)` returns `phases/<version>/` when that directory exists and falls back to flat `phases/` otherwise, consolidating phase-path logic that was previously duplicated across the init/commands/context/state/phase/roadmap/jobs/governance/milestone modules.
|
|
17
|
+
- **Version-aware lookup, state context, and structural archival**, with dual-mode (versioned + legacy-flat) test fixtures and the full library test suite greened.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **Ad-hoc milestones support the full phase lifecycle (quick-260627-mv4)** — `milestone create-adhoc` now atomically seeds the planning scaffolding it previously skipped: it writes `current_milestone` to STATE.md, creates the versioned `phases/<version>/` directory, and seeds an `## Active Milestone` ROADMAP section. A hand-added phase therefore flows through `/dgs:add-phase` → `plan-phase` → `execute-phase` inside an ad-hoc container, with no requirements/roadmapper ceremony. `abandon-milestone` mirrors the teardown (removes the seeded versioned dir and restores the docs), and the three new artifacts fold into create-adhoc's existing atomic rollback so a failed creation leaves no residue.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **Workflow-discipline hook self-heals stale installs (quick-260627-o37)** — the installer's PreToolUse registration now upgrades an existing `dgs-enforce-discipline` entry whose matcher lacks `Bash` by appending `|Bash` in place, instead of only adding the entry when absent. Previously a machine that installed before the `Bash` matcher existed kept the stale `Edit|Write|Skill` matcher forever — the marker-on-init branch never fired for inline `/dgs:*` flows, so Edit/Write was falsely blocked, and `/dgs:update` could not fix it. Idempotent once `Bash` is present.
|
|
24
|
+
|
|
11
25
|
## [3.4.2] - 2026-06-25
|
|
12
26
|
|
|
13
27
|
### Fixed
|
package/README.md
CHANGED
|
@@ -612,6 +612,8 @@ See the [User Guide](docs/USER-GUIDE.md#context-tiers) for the complete command-
|
|
|
612
612
|
|
|
613
613
|
### Testing & Dependency Scanning
|
|
614
614
|
|
|
615
|
+
Run the full automated test suite (repo-root `tests/` plus the nested `deliver-great-systems/bin/lib/` library tree) with a single command: `npm test`. To run only the library tree: `npm run test:lib`.
|
|
616
|
+
|
|
615
617
|
| Command | What it does |
|
|
616
618
|
|---------|--------------|
|
|
617
619
|
| `/dgs:package-scan [--threshold critical\|high\|medium\|low] [--repo <name>] [--json] [--include-dev-deps\|--no-include-dev-deps]` | Scan every registered repo + product root for known dependency vulnerabilities and licence issues. Cascades Snyk → OSV-Scanner → ecosystem-native tool (`npm audit`, `pip-audit`, `govulncheck`, `bundler-audit`). Report is committed to the active phase dir, active milestone dir, or a timestamped project-root file. See `deliver-great-systems/references/package-scan-config.md` for config keys and installation. |
|
|
@@ -11,7 +11,7 @@ You are a DGS codebase cross-analyzer. You read per-repo codebase documents and
|
|
|
11
11
|
You are spawned by `/dgs:map-codebase` after all synthesizer agents have completed. There is one cross-analyzer instance per mapping run.
|
|
12
12
|
|
|
13
13
|
You receive these prompt variables from the orchestrator:
|
|
14
|
-
- **codebase_dir**: Path to the codebase directory (e.g.,
|
|
14
|
+
- **codebase_dir**: Path to the codebase directory (e.g., `codebase/` under the planning root)
|
|
15
15
|
- **repo_names**: JSON array of repo names that were mapped (e.g., `["business", "newarch"]`)
|
|
16
16
|
|
|
17
17
|
Your job: Read per-repo maps, compare across repos, write CROSS-REPO.md with comparison tables, return confirmation only.
|
|
@@ -156,7 +156,7 @@ Read key files identified during exploration. Use Glob and Grep with paths under
|
|
|
156
156
|
<step name="write_documents">
|
|
157
157
|
Write document(s) to `${codebase_dir}/` using the templates below.
|
|
158
158
|
|
|
159
|
-
**Note:** `${codebase_dir}` is set by the orchestrator and already includes the repo subdirectory path (e.g.,
|
|
159
|
+
**Note:** `${codebase_dir}` is set by the orchestrator and already includes the repo subdirectory path (e.g., `codebase/business/`). Write directly to it.
|
|
160
160
|
|
|
161
161
|
**Document naming:** UPPERCASE.md (e.g., STACK.md, ARCHITECTURE.md)
|
|
162
162
|
|
|
@@ -11,7 +11,7 @@ You are a DGS codebase synthesizer. You read per-repo codebase documents and pro
|
|
|
11
11
|
You are spawned by `/dgs:map-codebase` after all per-repo mapper agents have completed. Each synthesizer instance handles one document type.
|
|
12
12
|
|
|
13
13
|
You receive these prompt variables from the orchestrator:
|
|
14
|
-
- **codebase_dir**: Path to the codebase directory (e.g.,
|
|
14
|
+
- **codebase_dir**: Path to the codebase directory (e.g., `codebase/` under the planning root)
|
|
15
15
|
- **repo_names**: List of repo names that were mapped (e.g., `["business", "newarch"]`)
|
|
16
16
|
- **doc_type**: Which document to synthesize (one of: ARCHITECTURE, STACK, STRUCTURE, CONVENTIONS, TESTING, INTEGRATIONS, CONCERNS)
|
|
17
17
|
|
|
@@ -306,7 +306,7 @@ Verified patterns from official sources:
|
|
|
306
306
|
|
|
307
307
|
## Validation Architecture
|
|
308
308
|
|
|
309
|
-
> Skip this section entirely if workflow.nyquist_validation is explicitly set to false in
|
|
309
|
+
> Skip this section entirely if workflow.nyquist_validation is explicitly set to false in `${config_path}`. If the key is absent, treat as enabled.
|
|
310
310
|
|
|
311
311
|
### Test Framework
|
|
312
312
|
| Property | Value |
|
package/bin/install.js
CHANGED
|
@@ -1816,11 +1816,11 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1816
1816
|
settings.hooks.PreToolUse = [];
|
|
1817
1817
|
}
|
|
1818
1818
|
|
|
1819
|
-
const
|
|
1819
|
+
const disciplineHookEntry = settings.hooks.PreToolUse.find(entry =>
|
|
1820
1820
|
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('dgs-enforce-discipline'))
|
|
1821
1821
|
);
|
|
1822
1822
|
|
|
1823
|
-
if (!
|
|
1823
|
+
if (!disciplineHookEntry) {
|
|
1824
1824
|
settings.hooks.PreToolUse.push({
|
|
1825
1825
|
matcher: 'Edit|Write|Skill|Bash',
|
|
1826
1826
|
hooks: [
|
|
@@ -1831,6 +1831,16 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1831
1831
|
]
|
|
1832
1832
|
});
|
|
1833
1833
|
console.log(` ${green}✓${reset} Configured workflow discipline hook`);
|
|
1834
|
+
} else if (
|
|
1835
|
+
typeof disciplineHookEntry.matcher === 'string' &&
|
|
1836
|
+
!disciplineHookEntry.matcher.split('|').includes('Bash')
|
|
1837
|
+
) {
|
|
1838
|
+
// Self-heal stale installs: older versions registered this matcher without
|
|
1839
|
+
// Bash, so the marker-on-init branch never fired for inline /dgs:* flows and
|
|
1840
|
+
// Edit/Write got falsely blocked. Upgrade the existing entry in place so a
|
|
1841
|
+
// plain /dgs:update fixes it. Idempotent once Bash is present.
|
|
1842
|
+
disciplineHookEntry.matcher = disciplineHookEntry.matcher + '|Bash';
|
|
1843
|
+
console.log(` ${green}✓${reset} Upgraded workflow discipline hook matcher (added Bash)`);
|
|
1834
1844
|
}
|
|
1835
1845
|
|
|
1836
1846
|
// Configure PostToolUse hook for discipline marker cleanup (Skill completion)
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* state patch --field val ... Batch update STATE.md fields
|
|
16
16
|
* state archive-quick-tasks Archive excess quick task rows to HISTORY.md
|
|
17
17
|
* state mark-milestone-complete Mark milestone complete in STATE.md
|
|
18
|
+
* state reconcile-milestone Self-heal a shipped-but-not-flipped STATE.md
|
|
18
19
|
* resolve-model <agent-type> Get model for agent based on profile
|
|
19
20
|
* find-phase <phase> Find phase directory by number
|
|
20
21
|
* commit <message> [--files f1 f2] Commit planning docs
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
* Phase Operations:
|
|
38
39
|
* phase next-decimal <phase> Calculate next decimal phase number
|
|
39
40
|
* phase add <description> Append new phase to roadmap + create dir
|
|
41
|
+
* phase init-versioned-dir Create current milestone's phases/<version>/ (fail-loud; new-milestone only)
|
|
40
42
|
* phase insert <after> <description> Insert decimal phase after existing
|
|
41
43
|
* phase remove <phase> [--force] Remove phase, renumber all subsequent
|
|
42
44
|
* phase complete <phase> Mark phase done, update state + roadmap
|
|
@@ -708,6 +710,8 @@ async function main() {
|
|
|
708
710
|
state.cmdStateArchiveQuickTasks(cwd, raw);
|
|
709
711
|
} else if (subcommand === 'mark-milestone-complete') {
|
|
710
712
|
state.cmdMarkMilestoneComplete(cwd, raw);
|
|
713
|
+
} else if (subcommand === 'reconcile-milestone') {
|
|
714
|
+
state.cmdReconcileMilestone(cwd, raw);
|
|
711
715
|
} else if (subcommand === 'read-adhoc') {
|
|
712
716
|
state.cmdStateReadAdhoc(cwd, raw);
|
|
713
717
|
} else if (subcommand === 'adhoc-readiness') {
|
|
@@ -1178,6 +1182,8 @@ async function main() {
|
|
|
1178
1182
|
phase.cmdPhaseNextDecimal(cwd, args[2], raw);
|
|
1179
1183
|
} else if (subcommand === 'add') {
|
|
1180
1184
|
phase.cmdPhaseAdd(cwd, args.slice(2).join(' '), raw);
|
|
1185
|
+
} else if (subcommand === 'init-versioned-dir') {
|
|
1186
|
+
phase.cmdPhaseInitVersionedDir(cwd, raw);
|
|
1181
1187
|
} else if (subcommand === 'insert') {
|
|
1182
1188
|
phase.cmdPhaseInsert(cwd, args[2], args.slice(3).join(' '), raw);
|
|
1183
1189
|
} else if (subcommand === 'remove') {
|
|
@@ -1189,7 +1195,7 @@ async function main() {
|
|
|
1189
1195
|
const push = args.includes('--push');
|
|
1190
1196
|
phase.cmdPhaseFinalize(cwd, args[2], { push }, raw);
|
|
1191
1197
|
} else {
|
|
1192
|
-
error('Unknown phase subcommand. Available: next-decimal, add, insert, remove, complete, finalize');
|
|
1198
|
+
error('Unknown phase subcommand. Available: next-decimal, add, init-versioned-dir, insert, remove, complete, finalize');
|
|
1193
1199
|
}
|
|
1194
1200
|
break;
|
|
1195
1201
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { execSync } = require('child_process');
|
|
7
|
-
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getProjectRoot, resolveModelInternal, MODEL_PROFILES, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs');
|
|
7
|
+
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getProjectRoot, phasesDir, resolveMilestoneVersion, resolveModelInternal, MODEL_PROFILES, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter, spliceFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
|
|
9
9
|
const { getPlanningRoot } = require('./paths.cjs');
|
|
10
10
|
|
|
@@ -146,15 +146,35 @@ function cmdVerifyPathExists(cwd, targetPath, raw) {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
function cmdHistoryDigest(cwd, raw) {
|
|
149
|
-
let
|
|
149
|
+
let phasesAbs;
|
|
150
150
|
try {
|
|
151
|
-
|
|
152
|
-
phasesDir = path.join(cwd, projectRoot, 'phases');
|
|
151
|
+
phasesAbs = path.join(cwd, phasesDir(cwd));
|
|
153
152
|
} catch {
|
|
154
|
-
|
|
153
|
+
phasesAbs = path.join(getPlanningRoot(cwd), 'phases');
|
|
155
154
|
}
|
|
156
155
|
const digest = { phases: {}, decisions: [], tech_stack: new Set() };
|
|
157
156
|
|
|
157
|
+
// Output shape (LOOK-02, composite {milestone, phase} key):
|
|
158
|
+
// Phases are keyed by a composite of milestone + phase number so a restarted
|
|
159
|
+
// phase number that exists in two milestones is NEVER conflated into one bucket.
|
|
160
|
+
// - Milestone-qualified entries (archived dirs, and current-phase entries once
|
|
161
|
+
// the current milestone is resolvable) → key = "<milestone>/<phase>"
|
|
162
|
+
// e.g. digest.phases['v4.0/03']
|
|
163
|
+
// - Entries with no resolvable milestone (flat-layout repos) → key = "<phase>"
|
|
164
|
+
// e.g. digest.phases['03'] — byte-identical to the pre-LOOK-02 shape, so
|
|
165
|
+
// flat installs see no data loss and no key change.
|
|
166
|
+
// decisions[] entries carry both `phase` (readable) and `milestone` so two
|
|
167
|
+
// milestones' same-numbered decisions are unambiguous.
|
|
168
|
+
|
|
169
|
+
// Resolve the current milestone once (non-required: validated vN.N or null,
|
|
170
|
+
// never throws on a read path — flat repos must keep working).
|
|
171
|
+
let currentMilestone = null;
|
|
172
|
+
try {
|
|
173
|
+
currentMilestone = resolveMilestoneVersion(cwd);
|
|
174
|
+
} catch {
|
|
175
|
+
currentMilestone = null;
|
|
176
|
+
}
|
|
177
|
+
|
|
158
178
|
// Collect all phase directories: archived + current
|
|
159
179
|
const allPhaseDirs = [];
|
|
160
180
|
|
|
@@ -164,15 +184,16 @@ function cmdHistoryDigest(cwd, raw) {
|
|
|
164
184
|
allPhaseDirs.push({ name: a.name, fullPath: a.fullPath, milestone: a.milestone });
|
|
165
185
|
}
|
|
166
186
|
|
|
167
|
-
// Add current phases
|
|
168
|
-
|
|
187
|
+
// Add current phases — assign the current milestone (archived entries already
|
|
188
|
+
// carry their own milestone; current entries are milestone: null until now).
|
|
189
|
+
if (fs.existsSync(phasesAbs)) {
|
|
169
190
|
try {
|
|
170
|
-
const currentDirs = fs.readdirSync(
|
|
191
|
+
const currentDirs = fs.readdirSync(phasesAbs, { withFileTypes: true })
|
|
171
192
|
.filter(e => e.isDirectory())
|
|
172
193
|
.map(e => e.name)
|
|
173
194
|
.sort();
|
|
174
195
|
for (const dir of currentDirs) {
|
|
175
|
-
allPhaseDirs.push({ name: dir, fullPath: path.join(
|
|
196
|
+
allPhaseDirs.push({ name: dir, fullPath: path.join(phasesAbs, dir), milestone: currentMilestone });
|
|
176
197
|
}
|
|
177
198
|
} catch {}
|
|
178
199
|
}
|
|
@@ -184,7 +205,7 @@ function cmdHistoryDigest(cwd, raw) {
|
|
|
184
205
|
}
|
|
185
206
|
|
|
186
207
|
try {
|
|
187
|
-
for (const { name: dir, fullPath: dirPath } of allPhaseDirs) {
|
|
208
|
+
for (const { name: dir, fullPath: dirPath, milestone } of allPhaseDirs) {
|
|
188
209
|
const summaries = fs.readdirSync(dirPath).filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
189
210
|
|
|
190
211
|
for (const summary of summaries) {
|
|
@@ -193,9 +214,12 @@ function cmdHistoryDigest(cwd, raw) {
|
|
|
193
214
|
const fm = extractFrontmatter(content);
|
|
194
215
|
|
|
195
216
|
const phaseNum = fm.phase || dir.split('-')[0];
|
|
217
|
+
// Composite key: "<milestone>/<phase>" when a milestone is present,
|
|
218
|
+
// else the bare phase number (flat-layout byte-identical behaviour).
|
|
219
|
+
const phaseKey = milestone ? `${milestone}/${phaseNum}` : phaseNum;
|
|
196
220
|
|
|
197
|
-
if (!digest.phases[
|
|
198
|
-
digest.phases[
|
|
221
|
+
if (!digest.phases[phaseKey]) {
|
|
222
|
+
digest.phases[phaseKey] = {
|
|
199
223
|
name: fm.name || dir.split('-').slice(1).join(' ') || 'Unknown',
|
|
200
224
|
provides: new Set(),
|
|
201
225
|
affects: new Set(),
|
|
@@ -205,25 +229,26 @@ function cmdHistoryDigest(cwd, raw) {
|
|
|
205
229
|
|
|
206
230
|
// Merge provides
|
|
207
231
|
if (fm['dependency-graph'] && fm['dependency-graph'].provides) {
|
|
208
|
-
fm['dependency-graph'].provides.forEach(p => digest.phases[
|
|
232
|
+
fm['dependency-graph'].provides.forEach(p => digest.phases[phaseKey].provides.add(p));
|
|
209
233
|
} else if (fm.provides) {
|
|
210
|
-
fm.provides.forEach(p => digest.phases[
|
|
234
|
+
fm.provides.forEach(p => digest.phases[phaseKey].provides.add(p));
|
|
211
235
|
}
|
|
212
236
|
|
|
213
237
|
// Merge affects
|
|
214
238
|
if (fm['dependency-graph'] && fm['dependency-graph'].affects) {
|
|
215
|
-
fm['dependency-graph'].affects.forEach(a => digest.phases[
|
|
239
|
+
fm['dependency-graph'].affects.forEach(a => digest.phases[phaseKey].affects.add(a));
|
|
216
240
|
}
|
|
217
241
|
|
|
218
242
|
// Merge patterns
|
|
219
243
|
if (fm['patterns-established']) {
|
|
220
|
-
fm['patterns-established'].forEach(p => digest.phases[
|
|
244
|
+
fm['patterns-established'].forEach(p => digest.phases[phaseKey].patterns.add(p));
|
|
221
245
|
}
|
|
222
246
|
|
|
223
|
-
// Merge decisions
|
|
247
|
+
// Merge decisions — tag with milestone so two milestones' same-numbered
|
|
248
|
+
// decisions are unambiguous; keep `phase` for readability.
|
|
224
249
|
if (fm['key-decisions']) {
|
|
225
250
|
fm['key-decisions'].forEach(d => {
|
|
226
|
-
digest.decisions.push({ phase: phaseNum, decision: d });
|
|
251
|
+
digest.decisions.push({ milestone: milestone || null, phase: phaseNum, decision: d });
|
|
227
252
|
});
|
|
228
253
|
}
|
|
229
254
|
|
|
@@ -622,7 +647,10 @@ function cmdPlanFinalize(cwd, phaseNum, planNum, options, raw) {
|
|
|
622
647
|
|
|
623
648
|
// Resolve phase dir via findPhaseInternal (returns phases/NN-name relative path)
|
|
624
649
|
const phaseInfo = findPhaseInternal(cwd, phaseNum);
|
|
625
|
-
|
|
650
|
+
// Guard on `found`: a LOOK-01 ambiguity object is truthy but has no .directory,
|
|
651
|
+
// so a bare `!phaseInfo` guard would deref undefined and crash. Surface its
|
|
652
|
+
// milestone-qualified message instead.
|
|
653
|
+
if (!phaseInfo || !phaseInfo.found) error(phaseInfo?.message || `Phase ${phaseNum} not found`);
|
|
626
654
|
const phaseDir = path.join(cwd, phaseInfo.directory);
|
|
627
655
|
|
|
628
656
|
// Locate PLAN.md + SUMMARY.md within the phase dir using `${phaseNum}-${planNum}` prefix
|
|
@@ -862,15 +890,15 @@ async function cmdWebsearch(query, options, raw) {
|
|
|
862
890
|
}
|
|
863
891
|
|
|
864
892
|
function cmdProgressRender(cwd, format, raw) {
|
|
865
|
-
let
|
|
893
|
+
let phasesAbs, roadmapPath;
|
|
866
894
|
try {
|
|
867
895
|
const projectRoot = getProjectRoot(cwd);
|
|
868
896
|
const projectAbs = path.join(cwd, projectRoot);
|
|
869
|
-
|
|
897
|
+
phasesAbs = path.join(cwd, phasesDir(cwd));
|
|
870
898
|
roadmapPath = path.join(projectAbs, 'ROADMAP.md');
|
|
871
899
|
} catch {
|
|
872
900
|
const planRoot = getPlanningRoot(cwd);
|
|
873
|
-
|
|
901
|
+
phasesAbs = path.join(planRoot, 'phases');
|
|
874
902
|
roadmapPath = path.join(planRoot, 'ROADMAP.md');
|
|
875
903
|
}
|
|
876
904
|
const milestone = getMilestoneInfo(cwd);
|
|
@@ -880,14 +908,14 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
880
908
|
let totalSummaries = 0;
|
|
881
909
|
|
|
882
910
|
try {
|
|
883
|
-
const entries = fs.readdirSync(
|
|
911
|
+
const entries = fs.readdirSync(phasesAbs, { withFileTypes: true });
|
|
884
912
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
885
913
|
|
|
886
914
|
for (const dir of dirs) {
|
|
887
915
|
const dm = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
|
|
888
916
|
const phaseNum = dm ? dm[1] : dir;
|
|
889
917
|
const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
|
|
890
|
-
const phaseFiles = fs.readdirSync(path.join(
|
|
918
|
+
const phaseFiles = fs.readdirSync(path.join(phasesAbs, dir));
|
|
891
919
|
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
|
|
892
920
|
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
|
|
893
921
|
|
|
@@ -1029,10 +1057,12 @@ function cmdScaffold(cwd, type, options, raw) {
|
|
|
1029
1057
|
|
|
1030
1058
|
// Find phase directory
|
|
1031
1059
|
const phaseInfo = phase ? findPhaseInternal(cwd, phase) : null;
|
|
1032
|
-
|
|
1060
|
+
// Gate phaseDir on `found`+`directory`: a LOOK-01 ambiguity object is truthy
|
|
1061
|
+
// but carries no .directory, so `path.join(cwd, undefined)` would throw.
|
|
1062
|
+
const phaseDir = (phaseInfo && phaseInfo.found && phaseInfo.directory) ? path.join(cwd, phaseInfo.directory) : null;
|
|
1033
1063
|
|
|
1034
1064
|
if (phase && !phaseDir && type !== 'phase-dir') {
|
|
1035
|
-
error(`Phase ${phase} directory not found`);
|
|
1065
|
+
error(phaseInfo?.message || `Phase ${phase} directory not found`);
|
|
1036
1066
|
}
|
|
1037
1067
|
|
|
1038
1068
|
let filePath, content;
|
|
@@ -1059,12 +1089,19 @@ function cmdScaffold(cwd, type, options, raw) {
|
|
|
1059
1089
|
}
|
|
1060
1090
|
const slug = generateSlugInternal(name);
|
|
1061
1091
|
const dirName = `${padded}-${slug}`;
|
|
1062
|
-
const
|
|
1092
|
+
const planRootRel = path.relative(cwd, getPlanningRoot(cwd)) || '.';
|
|
1093
|
+
// RSLV-03 residue (Phase 170): create the phase dir through the canonical
|
|
1094
|
+
// version-aware resolver so the dir we CREATE is the dir we REPORT. Under a
|
|
1095
|
+
// versioned layout this mkdir's phases/<version>/NN-slug/, not the flat root.
|
|
1096
|
+
let phasesRel;
|
|
1097
|
+
try { phasesRel = phasesDir(cwd); }
|
|
1098
|
+
catch { phasesRel = path.join(planRootRel, 'phases'); }
|
|
1099
|
+
const phasesParent = path.join(cwd, phasesRel);
|
|
1063
1100
|
fs.mkdirSync(phasesParent, { recursive: true });
|
|
1064
1101
|
const dirPath = path.join(phasesParent, dirName);
|
|
1065
1102
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
1066
|
-
const
|
|
1067
|
-
output({ created: true, directory:
|
|
1103
|
+
const dirRel = path.join(phasesRel, dirName);
|
|
1104
|
+
output({ created: true, directory: dirRel, path: dirPath }, raw, dirPath);
|
|
1068
1105
|
return;
|
|
1069
1106
|
}
|
|
1070
1107
|
default:
|
|
@@ -11,7 +11,7 @@ const fs = require('fs');
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const { execSync } = require('child_process');
|
|
13
13
|
|
|
14
|
-
const { createTempProject } = require('./test-helpers.cjs');
|
|
14
|
+
const { createTempProject, createFixture } = require('./test-helpers.cjs');
|
|
15
15
|
|
|
16
16
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
17
17
|
|
|
@@ -774,3 +774,223 @@ describe('cmdListTodos flat-first scanning', () => {
|
|
|
774
774
|
}
|
|
775
775
|
});
|
|
776
776
|
});
|
|
777
|
+
|
|
778
|
+
// ─── findPhaseInternal ambiguity surfacing (LOOK-01 callers) ──────────────────
|
|
779
|
+
|
|
780
|
+
describe('findPhaseInternal ambiguity surfacing (LOOK-01 callers)', () => {
|
|
781
|
+
// Captures stderr + the error()-driven process.exit so a guarded caller's
|
|
782
|
+
// milestone-qualified message is inspectable without killing the test worker.
|
|
783
|
+
// error() (core.cjs) writes to process.stderr then process.exit(1).
|
|
784
|
+
function captureError(fn) {
|
|
785
|
+
const chunks = [];
|
|
786
|
+
const origStderr = process.stderr.write.bind(process.stderr);
|
|
787
|
+
const origStdout = process.stdout.write.bind(process.stdout);
|
|
788
|
+
const origExit = process.exit;
|
|
789
|
+
let exitCode = null;
|
|
790
|
+
let threw = null;
|
|
791
|
+
process.stderr.write = (data) => { chunks.push(String(data)); return true; };
|
|
792
|
+
process.stdout.write = (data) => { chunks.push(String(data)); return true; };
|
|
793
|
+
process.exit = (code) => { exitCode = code == null ? 0 : code; throw new Error('__EXIT__'); };
|
|
794
|
+
try {
|
|
795
|
+
fn();
|
|
796
|
+
} catch (e) {
|
|
797
|
+
if (e && e.message !== '__EXIT__') threw = e;
|
|
798
|
+
} finally {
|
|
799
|
+
process.stderr.write = origStderr;
|
|
800
|
+
process.stdout.write = origStdout;
|
|
801
|
+
process.exit = origExit;
|
|
802
|
+
}
|
|
803
|
+
return { output: chunks.join(''), exitCode, threw };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Build a v2 project where bare "03" is NOT an active phase but exists in TWO
|
|
807
|
+
// milestone archives — the cross-milestone collision case.
|
|
808
|
+
function ambiguousFixture() {
|
|
809
|
+
return createFixture({
|
|
810
|
+
'config.json': JSON.stringify({}),
|
|
811
|
+
'config.local.json': JSON.stringify({ current_project: 'auth-overhaul' }),
|
|
812
|
+
'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
|
|
813
|
+
'REPOS.md': '# Repos\n\n| Name | Path |\n',
|
|
814
|
+
'projects/auth-overhaul/PROJECT.md': '# Project',
|
|
815
|
+
'projects/auth-overhaul/STATE.md': '---\ncurrent_milestone: v25.0\n---\n# State',
|
|
816
|
+
'projects/auth-overhaul/ROADMAP.md': '# Roadmap',
|
|
817
|
+
'projects/auth-overhaul/phases/': null,
|
|
818
|
+
'milestones/v3.0-phases/03-alpha/01-PLAN.md': '# Plan',
|
|
819
|
+
'milestones/v4.0-phases/03-beta/01-PLAN.md': '# Plan',
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
it('cmdPlanFinalize (commands.cjs:623 path) surfaces the milestone-qualified message, not a TypeError or silent resolution', () => {
|
|
824
|
+
const commands = require('./commands.cjs');
|
|
825
|
+
const fixture = ambiguousFixture();
|
|
826
|
+
try {
|
|
827
|
+
const { output, exitCode, threw } = captureError(() =>
|
|
828
|
+
commands.cmdPlanFinalize(fixture.cwd, '03', '01', {}, true)
|
|
829
|
+
);
|
|
830
|
+
// Must NOT crash with a TypeError (the pre-hardening failure mode).
|
|
831
|
+
assert.equal(threw, null, threw ? `unexpected throw: ${threw.stack}` : 'no throw');
|
|
832
|
+
// The guard fired with error() → exit(1).
|
|
833
|
+
assert.equal(exitCode, 1, 'guarded caller exits via error()');
|
|
834
|
+
// The milestone-qualified ambiguity message surfaced, naming both versions.
|
|
835
|
+
assert.ok(output.includes('v3.0'), `output names v3.0: ${output}`);
|
|
836
|
+
assert.ok(output.includes('v4.0'), `output names v4.0: ${output}`);
|
|
837
|
+
assert.ok(/ambiguous/i.test(output), `output flags ambiguity: ${output}`);
|
|
838
|
+
// NOT silently resolved to one archive directory.
|
|
839
|
+
assert.ok(!output.includes('03-alpha'), 'must not silently resolve to v3.0 dir');
|
|
840
|
+
assert.ok(!output.includes('03-beta'), 'must not silently resolve to v4.0 dir');
|
|
841
|
+
} finally {
|
|
842
|
+
fixture.cleanup();
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
// ─── cmdHistoryDigest composite key (LOOK-02) ──────────────────────────────────
|
|
848
|
+
//
|
|
849
|
+
// Documented output shape (composite {milestone, phase} key, string encoding):
|
|
850
|
+
// - When an entry carries a milestone (archived dirs, or current-phase entries
|
|
851
|
+
// once the current milestone is resolvable via resolveMilestoneVersion), the
|
|
852
|
+
// bucket key is `"<milestone>/<phase>"` (e.g. `digest.phases['v4.0/03']`).
|
|
853
|
+
// - When an entry has no resolvable milestone (flat-layout repos), the key is the
|
|
854
|
+
// bare phase number (e.g. `digest.phases['03']`) — byte-identical to the
|
|
855
|
+
// pre-LOOK-02 shape, so flat installs see no data loss and no key change.
|
|
856
|
+
// - decisions[] entries carry both `phase` and `milestone` so two milestones'
|
|
857
|
+
// same-numbered decisions are never ambiguous.
|
|
858
|
+
describe('cmdHistoryDigest composite key (LOOK-02)', () => {
|
|
859
|
+
const { cmdHistoryDigest } = require('./commands.cjs');
|
|
860
|
+
|
|
861
|
+
// Build a fixture with TWO milestone archives that BOTH contain a phase "03",
|
|
862
|
+
// plus a current active phase. STATE.md carries current_milestone so the
|
|
863
|
+
// current-phase entry resolves to that milestone.
|
|
864
|
+
function twoMilestoneFixture(currentMilestone) {
|
|
865
|
+
const structure = {
|
|
866
|
+
'config.json': JSON.stringify({}),
|
|
867
|
+
'config.local.json': JSON.stringify({ planningRoot: '.' }),
|
|
868
|
+
'PROJECT.md': '# Project\n',
|
|
869
|
+
'ROADMAP.md': '# Roadmap\n',
|
|
870
|
+
'PROJECTS.md': '# Projects\n',
|
|
871
|
+
'REPOS.md': '# Repos\n',
|
|
872
|
+
'STATE.md':
|
|
873
|
+
'---\n' +
|
|
874
|
+
'dgs_state_version: 1.0\n' +
|
|
875
|
+
(currentMilestone ? `current_milestone: ${currentMilestone}\n` : '') +
|
|
876
|
+
'milestone: v5.0\n' +
|
|
877
|
+
'---\n\n# Project State\n\nPhase: 7\n',
|
|
878
|
+
// v3.0 archive, phase 03 → provides A
|
|
879
|
+
'milestones/v3.0-phases/v3.0-ROADMAP.md': '# v3.0\n',
|
|
880
|
+
'milestones/v3.0-phases/03-alpha/03-SUMMARY.md':
|
|
881
|
+
'---\nphase: "03"\nname: "Alpha"\nprovides:\n - "A"\nkey-decisions:\n - "Decision Alpha"\n---\n',
|
|
882
|
+
// v4.0 archive, phase 03 → provides B
|
|
883
|
+
'milestones/v4.0-phases/v4.0-ROADMAP.md': '# v4.0\n',
|
|
884
|
+
'milestones/v4.0-phases/03-beta/03-SUMMARY.md':
|
|
885
|
+
'---\nphase: "03"\nname: "Beta"\nprovides:\n - "B"\nkey-decisions:\n - "Decision Beta"\n---\n',
|
|
886
|
+
// current active phase 07 → provides Current
|
|
887
|
+
'phases/07-current/07-SUMMARY.md':
|
|
888
|
+
'---\nphase: "07"\nname: "Current"\nprovides:\n - "Current"\n---\n',
|
|
889
|
+
};
|
|
890
|
+
return createFixture(structure);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function runDigest(cwd) {
|
|
894
|
+
const { json } = captureStdout(() => cmdHistoryDigest(cwd, false));
|
|
895
|
+
assert.ok(json, 'digest JSON emitted');
|
|
896
|
+
return json;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
it('Test 1 (no conflation): restarted "03" from two milestones lands in DISTINCT buckets', () => {
|
|
900
|
+
const fixture = twoMilestoneFixture('v5.0');
|
|
901
|
+
try {
|
|
902
|
+
const digest = runDigest(fixture.cwd);
|
|
903
|
+
const v3 = digest.phases['v3.0/03'];
|
|
904
|
+
const v4 = digest.phases['v4.0/03'];
|
|
905
|
+
assert.ok(v3, "v3.0/03 bucket exists");
|
|
906
|
+
assert.ok(v4, "v4.0/03 bucket exists");
|
|
907
|
+
// v3.0 bucket has A, NOT B; v4.0 bucket has B, NOT A.
|
|
908
|
+
assert.ok(v3.provides.includes('A'), `v3.0/03 provides A: ${JSON.stringify(v3.provides)}`);
|
|
909
|
+
assert.ok(!v3.provides.includes('B'), 'v3.0/03 must NOT contain B');
|
|
910
|
+
assert.ok(v4.provides.includes('B'), `v4.0/03 provides B: ${JSON.stringify(v4.provides)}`);
|
|
911
|
+
assert.ok(!v4.provides.includes('A'), 'v4.0/03 must NOT contain A');
|
|
912
|
+
// The old conflated bare bucket must NOT exist for these milestone entries.
|
|
913
|
+
assert.ok(!digest.phases['03'], 'no conflated bare "03" bucket for milestone entries');
|
|
914
|
+
} finally {
|
|
915
|
+
fixture.cleanup();
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
it('Test 2 (current phase carries milestone): current-phase entry keyed under current milestone', () => {
|
|
920
|
+
const fixture = twoMilestoneFixture('v5.0');
|
|
921
|
+
try {
|
|
922
|
+
const digest = runDigest(fixture.cwd);
|
|
923
|
+
assert.ok(digest.phases['v5.0/07'], 'current phase 07 keyed under v5.0');
|
|
924
|
+
assert.ok(
|
|
925
|
+
digest.phases['v5.0/07'].provides.includes('Current'),
|
|
926
|
+
'current phase provides surfaced under its milestone bucket'
|
|
927
|
+
);
|
|
928
|
+
// Must NOT be keyed under bare "07" (would mean milestone was not assigned).
|
|
929
|
+
assert.ok(!digest.phases['07'], 'current phase must not land in a bare "07" bucket');
|
|
930
|
+
} finally {
|
|
931
|
+
fixture.cleanup();
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
it('Test 3 (documented shape): keys use the "<milestone>/<phase>" encoding', () => {
|
|
936
|
+
const fixture = twoMilestoneFixture('v5.0');
|
|
937
|
+
try {
|
|
938
|
+
const digest = runDigest(fixture.cwd);
|
|
939
|
+
const keys = Object.keys(digest.phases);
|
|
940
|
+
assert.ok(keys.includes('v3.0/03'), `keys include v3.0/03: ${keys}`);
|
|
941
|
+
assert.ok(keys.includes('v4.0/03'), `keys include v4.0/03: ${keys}`);
|
|
942
|
+
assert.ok(keys.includes('v5.0/07'), `keys include v5.0/07: ${keys}`);
|
|
943
|
+
// Every milestone-qualified key matches the documented grammar.
|
|
944
|
+
for (const k of keys) {
|
|
945
|
+
assert.ok(/^(v\d+\.\d+\/.+|[^/]+)$/.test(k), `key '${k}' matches documented shape`);
|
|
946
|
+
}
|
|
947
|
+
} finally {
|
|
948
|
+
fixture.cleanup();
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
it('Test 4 (decisions carry milestone): decisions tagged so two milestones are not ambiguous', () => {
|
|
953
|
+
const fixture = twoMilestoneFixture('v5.0');
|
|
954
|
+
try {
|
|
955
|
+
const digest = runDigest(fixture.cwd);
|
|
956
|
+
const alpha = digest.decisions.find(d => d.decision === 'Decision Alpha');
|
|
957
|
+
const beta = digest.decisions.find(d => d.decision === 'Decision Beta');
|
|
958
|
+
assert.ok(alpha, 'Decision Alpha present');
|
|
959
|
+
assert.ok(beta, 'Decision Beta present');
|
|
960
|
+
assert.strictEqual(alpha.milestone, 'v3.0', 'Decision Alpha tagged with v3.0');
|
|
961
|
+
assert.strictEqual(beta.milestone, 'v4.0', 'Decision Beta tagged with v4.0');
|
|
962
|
+
assert.strictEqual(alpha.phase, '03', 'Decision Alpha keeps readable phase');
|
|
963
|
+
} finally {
|
|
964
|
+
fixture.cleanup();
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
it('Test 5 (flat-layout backward-compat): no milestone → bare phase key preserved', () => {
|
|
969
|
+
// Flat repo: no milestones/ archives, no current_milestone. Current phases
|
|
970
|
+
// must keep the byte-identical bare-number key shape (information preserved).
|
|
971
|
+
const fixture = createFixture({
|
|
972
|
+
'config.json': JSON.stringify({}),
|
|
973
|
+
'config.local.json': JSON.stringify({ planningRoot: '.' }),
|
|
974
|
+
'PROJECT.md': '# Project\n',
|
|
975
|
+
'ROADMAP.md': '# Roadmap\n',
|
|
976
|
+
'PROJECTS.md': '# Projects\n',
|
|
977
|
+
'REPOS.md': '# Repos\n',
|
|
978
|
+
'STATE.md': '# Project State\n\nPhase: 1\n',
|
|
979
|
+
'phases/01-foundation/01-SUMMARY.md':
|
|
980
|
+
'---\nphase: "01"\nname: "Foundation"\nprovides:\n - "DB"\n---\n',
|
|
981
|
+
'phases/02-api/02-SUMMARY.md':
|
|
982
|
+
'---\nphase: "02"\nname: "API"\nprovides:\n - "REST"\n---\n',
|
|
983
|
+
});
|
|
984
|
+
try {
|
|
985
|
+
const digest = runDigest(fixture.cwd);
|
|
986
|
+
assert.ok(digest.phases['01'], 'flat phase 01 keyed bare');
|
|
987
|
+
assert.ok(digest.phases['02'], 'flat phase 02 keyed bare');
|
|
988
|
+
assert.ok(digest.phases['01'].provides.includes('DB'), 'flat 01 provides preserved');
|
|
989
|
+
assert.ok(digest.phases['02'].provides.includes('REST'), 'flat 02 provides preserved');
|
|
990
|
+
// No spurious milestone-qualified keys in a flat repo.
|
|
991
|
+
assert.ok(!Object.keys(digest.phases).some(k => k.includes('/')), 'no milestone-qualified keys in flat repo');
|
|
992
|
+
} finally {
|
|
993
|
+
fixture.cleanup();
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
});
|
|
@@ -526,15 +526,15 @@ function getMilestoneSummaries(cwd, planningRoot, currentPhaseNum) {
|
|
|
526
526
|
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
527
527
|
|
|
528
528
|
// Scan phases directory for milestone phases with summaries
|
|
529
|
-
|
|
529
|
+
const { phasesDir: resolvePhasesDir } = require('./core.cjs');
|
|
530
|
+
let phasesRel;
|
|
530
531
|
try {
|
|
531
|
-
|
|
532
|
-
projectRoot = getProjectRoot(cwd);
|
|
532
|
+
phasesRel = resolvePhasesDir(cwd);
|
|
533
533
|
} catch {
|
|
534
|
-
|
|
534
|
+
phasesRel = path.join(path.relative(cwd, planningRoot) || '.', 'phases');
|
|
535
535
|
}
|
|
536
536
|
|
|
537
|
-
const phasesDir = path.join(cwd,
|
|
537
|
+
const phasesDir = path.join(cwd, phasesRel);
|
|
538
538
|
if (!fs.existsSync(phasesDir)) return results;
|
|
539
539
|
|
|
540
540
|
try {
|
|
@@ -561,7 +561,7 @@ function getMilestoneSummaries(cwd, planningRoot, currentPhaseNum) {
|
|
|
561
561
|
const phaseFiles = fs.readdirSync(phasePath);
|
|
562
562
|
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
563
563
|
for (const summary of summaries) {
|
|
564
|
-
const relPath = toPosixPath(path.join(
|
|
564
|
+
const relPath = toPosixPath(path.join(phasesRel, dir, summary));
|
|
565
565
|
results.push({ path: relPath, category: 'milestone-summary' });
|
|
566
566
|
}
|
|
567
567
|
} catch {}
|