@ktpartners/dgs-platform 2.6.2
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/LICENSE +38 -0
- package/README.md +851 -0
- package/agents/dgs-codebase-cross-analyzer.md +183 -0
- package/agents/dgs-codebase-mapper.md +782 -0
- package/agents/dgs-codebase-synthesizer.md +156 -0
- package/agents/dgs-debugger.md +1256 -0
- package/agents/dgs-executor.md +550 -0
- package/agents/dgs-integration-checker.md +481 -0
- package/agents/dgs-nyquist-auditor.md +178 -0
- package/agents/dgs-phase-researcher.md +563 -0
- package/agents/dgs-phase-verifier.md +450 -0
- package/agents/dgs-plan-checker.md +708 -0
- package/agents/dgs-planner.md +1324 -0
- package/agents/dgs-project-researcher.md +631 -0
- package/agents/dgs-research-synthesizer.md +249 -0
- package/agents/dgs-roadmapper.md +652 -0
- package/agents/dgs-verifier.md +607 -0
- package/bin/install.js +2073 -0
- package/commands/dgs/add-doc.md +45 -0
- package/commands/dgs/add-idea.md +38 -0
- package/commands/dgs/add-phase.md +43 -0
- package/commands/dgs/add-repo.md +54 -0
- package/commands/dgs/add-tests.md +41 -0
- package/commands/dgs/add-todo.md +47 -0
- package/commands/dgs/approve-spec.md +38 -0
- package/commands/dgs/audit-milestone.md +36 -0
- package/commands/dgs/audit-phase.md +37 -0
- package/commands/dgs/cancel-job.md +23 -0
- package/commands/dgs/capture-principle.md +143 -0
- package/commands/dgs/check-todos.md +45 -0
- package/commands/dgs/cleanup.md +18 -0
- package/commands/dgs/complete-milestone.md +136 -0
- package/commands/dgs/complete-project.md +70 -0
- package/commands/dgs/consolidate-ideas.md +50 -0
- package/commands/dgs/create-milestone-job.md +37 -0
- package/commands/dgs/debug.md +164 -0
- package/commands/dgs/develop-idea.md +53 -0
- package/commands/dgs/discuss-idea.md +41 -0
- package/commands/dgs/discuss-phase.md +83 -0
- package/commands/dgs/execute-phase.md +41 -0
- package/commands/dgs/fast.md +38 -0
- package/commands/dgs/find-related-ideas.md +43 -0
- package/commands/dgs/health.md +28 -0
- package/commands/dgs/help.md +22 -0
- package/commands/dgs/import-spec.md +36 -0
- package/commands/dgs/init-product.md +28 -0
- package/commands/dgs/insert-phase.md +32 -0
- package/commands/dgs/join-discord.md +18 -0
- package/commands/dgs/list-docs.md +40 -0
- package/commands/dgs/list-ideas.md +42 -0
- package/commands/dgs/list-jobs.md +22 -0
- package/commands/dgs/list-phase-assumptions.md +46 -0
- package/commands/dgs/list-projects.md +57 -0
- package/commands/dgs/list-specs.md +40 -0
- package/commands/dgs/map-codebase.md +92 -0
- package/commands/dgs/new-milestone.md +44 -0
- package/commands/dgs/new-project.md +42 -0
- package/commands/dgs/node-repair.md +26 -0
- package/commands/dgs/overlap-check.md +20 -0
- package/commands/dgs/pause-work.md +38 -0
- package/commands/dgs/plan-milestone-gaps.md +34 -0
- package/commands/dgs/plan-phase.md +44 -0
- package/commands/dgs/progress.md +24 -0
- package/commands/dgs/quick.md +41 -0
- package/commands/dgs/reactivate-project.md +70 -0
- package/commands/dgs/reapply-patches.md +110 -0
- package/commands/dgs/refine-spec.md +38 -0
- package/commands/dgs/reject-idea.md +43 -0
- package/commands/dgs/remove-doc.md +44 -0
- package/commands/dgs/remove-phase.md +31 -0
- package/commands/dgs/remove-repo.md +69 -0
- package/commands/dgs/research-idea.md +43 -0
- package/commands/dgs/research-phase.md +189 -0
- package/commands/dgs/restore-idea.md +45 -0
- package/commands/dgs/resume-work.md +40 -0
- package/commands/dgs/rollback-job.md +24 -0
- package/commands/dgs/run-job.md +35 -0
- package/commands/dgs/search.md +40 -0
- package/commands/dgs/set-profile.md +34 -0
- package/commands/dgs/settings.md +38 -0
- package/commands/dgs/switch-project.md +58 -0
- package/commands/dgs/undo-consolidation.md +42 -0
- package/commands/dgs/update-idea.md +44 -0
- package/commands/dgs/update.md +37 -0
- package/commands/dgs/validate-phase.md +35 -0
- package/commands/dgs/verify-work.md +39 -0
- package/commands/dgs/write-spec.md +49 -0
- package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-01-SUMMARY.md +84 -0
- package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-02-SUMMARY.md +86 -0
- package/deliver-great-systems/.planning/phases/10-v1-to-v2-migration-flow/10-01-SUMMARY.md +85 -0
- package/deliver-great-systems/bin/dgs-tools.cjs +1444 -0
- package/deliver-great-systems/bin/lib/auto-test.cjs +1365 -0
- package/deliver-great-systems/bin/lib/commands.cjs +570 -0
- package/deliver-great-systems/bin/lib/config.cjs +417 -0
- package/deliver-great-systems/bin/lib/conflict-agent.cjs +1063 -0
- package/deliver-great-systems/bin/lib/conflict-agent.test.cjs +554 -0
- package/deliver-great-systems/bin/lib/context.cjs +929 -0
- package/deliver-great-systems/bin/lib/context.test.cjs +693 -0
- package/deliver-great-systems/bin/lib/core.cjs +744 -0
- package/deliver-great-systems/bin/lib/core.test.cjs +822 -0
- package/deliver-great-systems/bin/lib/docs.cjs +919 -0
- package/deliver-great-systems/bin/lib/docs.test.cjs +211 -0
- package/deliver-great-systems/bin/lib/execution.cjs +705 -0
- package/deliver-great-systems/bin/lib/execution.test.cjs +1472 -0
- package/deliver-great-systems/bin/lib/frontmatter.cjs +324 -0
- package/deliver-great-systems/bin/lib/ideas.cjs +1406 -0
- package/deliver-great-systems/bin/lib/ideas.test.cjs +1417 -0
- package/deliver-great-systems/bin/lib/identity.cjs +125 -0
- package/deliver-great-systems/bin/lib/init.cjs +1114 -0
- package/deliver-great-systems/bin/lib/init.test.cjs +1271 -0
- package/deliver-great-systems/bin/lib/jobs.cjs +2015 -0
- package/deliver-great-systems/bin/lib/jobs.test.cjs +2619 -0
- package/deliver-great-systems/bin/lib/merge-conflicts.cjs +654 -0
- package/deliver-great-systems/bin/lib/merge-conflicts.test.cjs +370 -0
- package/deliver-great-systems/bin/lib/migration.cjs +352 -0
- package/deliver-great-systems/bin/lib/migration.test.cjs +582 -0
- package/deliver-great-systems/bin/lib/milestone.cjs +243 -0
- package/deliver-great-systems/bin/lib/overlap.cjs +437 -0
- package/deliver-great-systems/bin/lib/overlap.test.cjs +747 -0
- package/deliver-great-systems/bin/lib/path-audit.test.cjs +384 -0
- package/deliver-great-systems/bin/lib/paths.cjs +144 -0
- package/deliver-great-systems/bin/lib/paths.test.cjs +486 -0
- package/deliver-great-systems/bin/lib/phase.cjs +910 -0
- package/deliver-great-systems/bin/lib/projects.cjs +691 -0
- package/deliver-great-systems/bin/lib/projects.test.cjs +871 -0
- package/deliver-great-systems/bin/lib/repos.cjs +1432 -0
- package/deliver-great-systems/bin/lib/repos.test.cjs +1882 -0
- package/deliver-great-systems/bin/lib/roadmap.cjs +305 -0
- package/deliver-great-systems/bin/lib/search.cjs +570 -0
- package/deliver-great-systems/bin/lib/specs.cjs +1303 -0
- package/deliver-great-systems/bin/lib/state.cjs +893 -0
- package/deliver-great-systems/bin/lib/template.cjs +228 -0
- package/deliver-great-systems/bin/lib/test-helpers.cjs +291 -0
- package/deliver-great-systems/bin/lib/verify.cjs +796 -0
- package/deliver-great-systems/references/checkpoints.md +776 -0
- package/deliver-great-systems/references/conflict-resolution.md +66 -0
- package/deliver-great-systems/references/context-tiers.md +166 -0
- package/deliver-great-systems/references/continuation-format.md +249 -0
- package/deliver-great-systems/references/decimal-phase-calculation.md +67 -0
- package/deliver-great-systems/references/git-integration.md +250 -0
- package/deliver-great-systems/references/git-planning-commit.md +40 -0
- package/deliver-great-systems/references/model-profile-resolution.md +36 -0
- package/deliver-great-systems/references/model-profiles.md +95 -0
- package/deliver-great-systems/references/phase-argument-parsing.md +61 -0
- package/deliver-great-systems/references/planning-config.md +224 -0
- package/deliver-great-systems/references/questioning.md +162 -0
- package/deliver-great-systems/references/spec-review-loop.md +177 -0
- package/deliver-great-systems/references/tdd.md +265 -0
- package/deliver-great-systems/references/ui-brand.md +160 -0
- package/deliver-great-systems/references/verification-patterns.md +612 -0
- package/deliver-great-systems/templates/DEBUG.md +166 -0
- package/deliver-great-systems/templates/UAT.md +251 -0
- package/deliver-great-systems/templates/VALIDATION.md +95 -0
- package/deliver-great-systems/templates/claude-md.md +74 -0
- package/deliver-great-systems/templates/codebase/architecture.md +257 -0
- package/deliver-great-systems/templates/codebase/concerns.md +312 -0
- package/deliver-great-systems/templates/codebase/conventions.md +309 -0
- package/deliver-great-systems/templates/codebase/integrations.md +282 -0
- package/deliver-great-systems/templates/codebase/stack.md +188 -0
- package/deliver-great-systems/templates/codebase/structure.md +287 -0
- package/deliver-great-systems/templates/codebase/testing.md +482 -0
- package/deliver-great-systems/templates/config.json +38 -0
- package/deliver-great-systems/templates/context.md +354 -0
- package/deliver-great-systems/templates/continue-here.md +80 -0
- package/deliver-great-systems/templates/debug-subagent-prompt.md +93 -0
- package/deliver-great-systems/templates/discovery.md +148 -0
- package/deliver-great-systems/templates/milestone-archive.md +125 -0
- package/deliver-great-systems/templates/milestone.md +117 -0
- package/deliver-great-systems/templates/phase-prompt.md +615 -0
- package/deliver-great-systems/templates/planner-subagent-prompt.md +119 -0
- package/deliver-great-systems/templates/project.md +186 -0
- package/deliver-great-systems/templates/requirements.md +233 -0
- package/deliver-great-systems/templates/research-project/ARCHITECTURE.md +206 -0
- package/deliver-great-systems/templates/research-project/FEATURES.md +149 -0
- package/deliver-great-systems/templates/research-project/PITFALLS.md +202 -0
- package/deliver-great-systems/templates/research-project/STACK.md +122 -0
- package/deliver-great-systems/templates/research-project/SUMMARY.md +172 -0
- package/deliver-great-systems/templates/research.md +554 -0
- package/deliver-great-systems/templates/retrospective.md +54 -0
- package/deliver-great-systems/templates/roadmap.md +204 -0
- package/deliver-great-systems/templates/state.md +178 -0
- package/deliver-great-systems/templates/summary-complex.md +59 -0
- package/deliver-great-systems/templates/summary-minimal.md +41 -0
- package/deliver-great-systems/templates/summary-standard.md +48 -0
- package/deliver-great-systems/templates/summary.md +253 -0
- package/deliver-great-systems/templates/user-setup.md +313 -0
- package/deliver-great-systems/templates/verification-report.md +324 -0
- package/deliver-great-systems/workflows/add-doc.md +151 -0
- package/deliver-great-systems/workflows/add-idea.md +96 -0
- package/deliver-great-systems/workflows/add-phase.md +120 -0
- package/deliver-great-systems/workflows/add-tests.md +359 -0
- package/deliver-great-systems/workflows/add-todo.md +162 -0
- package/deliver-great-systems/workflows/approve-spec.md +194 -0
- package/deliver-great-systems/workflows/audit-milestone.md +364 -0
- package/deliver-great-systems/workflows/audit-phase.md +462 -0
- package/deliver-great-systems/workflows/cancel-job.md +108 -0
- package/deliver-great-systems/workflows/check-todos.md +181 -0
- package/deliver-great-systems/workflows/cleanup.md +247 -0
- package/deliver-great-systems/workflows/codereview.md +526 -0
- package/deliver-great-systems/workflows/complete-milestone.md +1298 -0
- package/deliver-great-systems/workflows/consolidate-ideas.md +365 -0
- package/deliver-great-systems/workflows/create-milestone-job.md +177 -0
- package/deliver-great-systems/workflows/develop-idea.md +544 -0
- package/deliver-great-systems/workflows/diagnose-issues.md +231 -0
- package/deliver-great-systems/workflows/discovery-phase.md +301 -0
- package/deliver-great-systems/workflows/discuss-idea.md +263 -0
- package/deliver-great-systems/workflows/discuss-phase.md +733 -0
- package/deliver-great-systems/workflows/execute-phase.md +571 -0
- package/deliver-great-systems/workflows/execute-plan.md +592 -0
- package/deliver-great-systems/workflows/find-related-ideas.md +271 -0
- package/deliver-great-systems/workflows/health.md +173 -0
- package/deliver-great-systems/workflows/help.md +997 -0
- package/deliver-great-systems/workflows/import-spec.md +381 -0
- package/deliver-great-systems/workflows/init-product.md +767 -0
- package/deliver-great-systems/workflows/insert-phase.md +138 -0
- package/deliver-great-systems/workflows/list-docs.md +119 -0
- package/deliver-great-systems/workflows/list-ideas.md +154 -0
- package/deliver-great-systems/workflows/list-jobs.md +89 -0
- package/deliver-great-systems/workflows/list-phase-assumptions.md +192 -0
- package/deliver-great-systems/workflows/list-specs.md +101 -0
- package/deliver-great-systems/workflows/map-codebase.md +621 -0
- package/deliver-great-systems/workflows/new-milestone.md +591 -0
- package/deliver-great-systems/workflows/new-project.md +1113 -0
- package/deliver-great-systems/workflows/node-repair.md +94 -0
- package/deliver-great-systems/workflows/overlap-check.md +86 -0
- package/deliver-great-systems/workflows/pause-work.md +134 -0
- package/deliver-great-systems/workflows/plan-milestone-gaps.md +306 -0
- package/deliver-great-systems/workflows/plan-phase.md +698 -0
- package/deliver-great-systems/workflows/progress.md +386 -0
- package/deliver-great-systems/workflows/quick.md +845 -0
- package/deliver-great-systems/workflows/refine-spec.md +275 -0
- package/deliver-great-systems/workflows/reject-idea.md +109 -0
- package/deliver-great-systems/workflows/remove-doc.md +117 -0
- package/deliver-great-systems/workflows/remove-phase.md +163 -0
- package/deliver-great-systems/workflows/research-idea.md +325 -0
- package/deliver-great-systems/workflows/research-phase.md +81 -0
- package/deliver-great-systems/workflows/restore-idea.md +101 -0
- package/deliver-great-systems/workflows/resume-project.md +311 -0
- package/deliver-great-systems/workflows/rollback-job.md +130 -0
- package/deliver-great-systems/workflows/run-job.md +498 -0
- package/deliver-great-systems/workflows/search.md +130 -0
- package/deliver-great-systems/workflows/set-profile.md +83 -0
- package/deliver-great-systems/workflows/settings.md +470 -0
- package/deliver-great-systems/workflows/transition.md +563 -0
- package/deliver-great-systems/workflows/undo-consolidation.md +155 -0
- package/deliver-great-systems/workflows/update-idea.md +157 -0
- package/deliver-great-systems/workflows/update.md +242 -0
- package/deliver-great-systems/workflows/validate-phase.md +177 -0
- package/deliver-great-systems/workflows/verify-phase.md +253 -0
- package/deliver-great-systems/workflows/verify-work.md +671 -0
- package/deliver-great-systems/workflows/write-spec.md +450 -0
- package/hooks/dist/dgs-check-update.js +62 -0
- package/hooks/dist/dgs-context-monitor.js +141 -0
- package/hooks/dist/dgs-statusline.js +115 -0
- package/package.json +60 -0
- package/scripts/build-hooks.js +43 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Milestone — Milestone and requirements lifecycle operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { escapeRegex, getMilestonePhaseFilter, output, error } = require('./core.cjs');
|
|
8
|
+
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
9
|
+
const { getPlanningRoot } = require('./paths.cjs');
|
|
10
|
+
const { writeStateMd } = require('./state.cjs');
|
|
11
|
+
|
|
12
|
+
function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
|
|
13
|
+
if (!reqIdsRaw || reqIdsRaw.length === 0) {
|
|
14
|
+
error('requirement IDs required. Usage: requirements mark-complete REQ-01,REQ-02 or REQ-01 REQ-02');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Accept comma-separated, space-separated, or bracket-wrapped: [REQ-01, REQ-02]
|
|
18
|
+
const reqIds = reqIdsRaw
|
|
19
|
+
.join(' ')
|
|
20
|
+
.replace(/[\[\]]/g, '')
|
|
21
|
+
.split(/[,\s]+/)
|
|
22
|
+
.map(r => r.trim())
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
|
|
25
|
+
if (reqIds.length === 0) {
|
|
26
|
+
error('no valid requirement IDs found');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const reqPath = path.join(getPlanningRoot(cwd), 'REQUIREMENTS.md');
|
|
30
|
+
if (!fs.existsSync(reqPath)) {
|
|
31
|
+
output({ updated: false, reason: 'REQUIREMENTS.md not found', ids: reqIds }, raw, 'no requirements file');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let reqContent = fs.readFileSync(reqPath, 'utf-8');
|
|
36
|
+
const updated = [];
|
|
37
|
+
const notFound = [];
|
|
38
|
+
|
|
39
|
+
for (const reqId of reqIds) {
|
|
40
|
+
let found = false;
|
|
41
|
+
const reqEscaped = escapeRegex(reqId);
|
|
42
|
+
|
|
43
|
+
// Update checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**
|
|
44
|
+
const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi');
|
|
45
|
+
if (checkboxPattern.test(reqContent)) {
|
|
46
|
+
reqContent = reqContent.replace(checkboxPattern, '$1x$2');
|
|
47
|
+
found = true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Update traceability table: | REQ-ID | Phase N | Pending | → | REQ-ID | Phase N | Complete |
|
|
51
|
+
const tablePattern = new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi');
|
|
52
|
+
if (tablePattern.test(reqContent)) {
|
|
53
|
+
// Re-read since test() advances lastIndex for global regex
|
|
54
|
+
reqContent = reqContent.replace(
|
|
55
|
+
new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
|
|
56
|
+
'$1 Complete $2'
|
|
57
|
+
);
|
|
58
|
+
found = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (found) {
|
|
62
|
+
updated.push(reqId);
|
|
63
|
+
} else {
|
|
64
|
+
notFound.push(reqId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (updated.length > 0) {
|
|
69
|
+
fs.writeFileSync(reqPath, reqContent, 'utf-8');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
output({
|
|
73
|
+
updated: updated.length > 0,
|
|
74
|
+
marked_complete: updated,
|
|
75
|
+
not_found: notFound,
|
|
76
|
+
total: reqIds.length,
|
|
77
|
+
}, raw, `${updated.length}/${reqIds.length} requirements marked complete`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
81
|
+
if (!version) {
|
|
82
|
+
error('version required for milestone complete (e.g., v1.0)');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const planRoot = getPlanningRoot(cwd);
|
|
86
|
+
const roadmapPath = path.join(planRoot, 'ROADMAP.md');
|
|
87
|
+
const reqPath = path.join(planRoot, 'REQUIREMENTS.md');
|
|
88
|
+
const statePath = path.join(planRoot, 'STATE.md');
|
|
89
|
+
const milestonesPath = path.join(planRoot, 'MILESTONES.md');
|
|
90
|
+
const archiveDir = path.join(planRoot, 'milestones');
|
|
91
|
+
const phasesDir = path.join(planRoot, 'phases');
|
|
92
|
+
const today = new Date().toISOString().split('T')[0];
|
|
93
|
+
const milestoneName = options.name || version;
|
|
94
|
+
|
|
95
|
+
// Ensure archive directory exists
|
|
96
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
97
|
+
|
|
98
|
+
// Scope stats and accomplishments to only the phases belonging to the
|
|
99
|
+
// current milestone's ROADMAP. Uses the shared filter from core.cjs
|
|
100
|
+
// (same logic used by cmdPhasesList and other callers).
|
|
101
|
+
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
102
|
+
|
|
103
|
+
// Gather stats from phases (scoped to current milestone only)
|
|
104
|
+
let phaseCount = 0;
|
|
105
|
+
let totalPlans = 0;
|
|
106
|
+
let totalTasks = 0;
|
|
107
|
+
const accomplishments = [];
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
111
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
112
|
+
|
|
113
|
+
for (const dir of dirs) {
|
|
114
|
+
if (!isDirInMilestone(dir)) continue;
|
|
115
|
+
|
|
116
|
+
phaseCount++;
|
|
117
|
+
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
|
118
|
+
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
|
119
|
+
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
120
|
+
totalPlans += plans.length;
|
|
121
|
+
|
|
122
|
+
// Extract one-liners from summaries
|
|
123
|
+
for (const s of summaries) {
|
|
124
|
+
try {
|
|
125
|
+
const content = fs.readFileSync(path.join(phasesDir, dir, s), 'utf-8');
|
|
126
|
+
const fm = extractFrontmatter(content);
|
|
127
|
+
if (fm['one-liner']) {
|
|
128
|
+
accomplishments.push(fm['one-liner']);
|
|
129
|
+
}
|
|
130
|
+
// Count tasks
|
|
131
|
+
const taskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
|
|
132
|
+
totalTasks += taskMatches.length;
|
|
133
|
+
} catch {}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch {}
|
|
137
|
+
|
|
138
|
+
// Archive ROADMAP.md
|
|
139
|
+
if (fs.existsSync(roadmapPath)) {
|
|
140
|
+
const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
141
|
+
fs.writeFileSync(path.join(archiveDir, `${version}-ROADMAP.md`), roadmapContent, 'utf-8');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Archive REQUIREMENTS.md
|
|
145
|
+
if (fs.existsSync(reqPath)) {
|
|
146
|
+
const reqContent = fs.readFileSync(reqPath, 'utf-8');
|
|
147
|
+
const archiveHeader = `# Requirements Archive: ${version} ${milestoneName}\n\n**Archived:** ${today}\n**Status:** SHIPPED\n\nFor current requirements, see \`.planning/REQUIREMENTS.md\`.\n\n---\n\n`;
|
|
148
|
+
fs.writeFileSync(path.join(archiveDir, `${version}-REQUIREMENTS.md`), archiveHeader + reqContent, 'utf-8');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Archive audit file if exists
|
|
152
|
+
const auditFile = path.join(planRoot, `${version}-MILESTONE-AUDIT.md`);
|
|
153
|
+
if (fs.existsSync(auditFile)) {
|
|
154
|
+
fs.renameSync(auditFile, path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Create/append MILESTONES.md entry
|
|
158
|
+
const accomplishmentsList = accomplishments.map(a => `- ${a}`).join('\n');
|
|
159
|
+
const milestoneEntry = `## ${version} ${milestoneName} (Shipped: ${today})\n\n**Phases completed:** ${phaseCount} phases, ${totalPlans} plans, ${totalTasks} tasks\n\n**Key accomplishments:**\n${accomplishmentsList || '- (none recorded)'}\n\n---\n\n`;
|
|
160
|
+
|
|
161
|
+
if (fs.existsSync(milestonesPath)) {
|
|
162
|
+
const existing = fs.readFileSync(milestonesPath, 'utf-8');
|
|
163
|
+
if (!existing.trim()) {
|
|
164
|
+
// Empty file — treat like new
|
|
165
|
+
fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8');
|
|
166
|
+
} else {
|
|
167
|
+
// Insert after the header line(s) for reverse chronological order (newest first)
|
|
168
|
+
const headerMatch = existing.match(/^(#{1,3}\s+[^\n]*\n\n?)/);
|
|
169
|
+
if (headerMatch) {
|
|
170
|
+
const header = headerMatch[1];
|
|
171
|
+
const rest = existing.slice(header.length);
|
|
172
|
+
fs.writeFileSync(milestonesPath, header + milestoneEntry + rest, 'utf-8');
|
|
173
|
+
} else {
|
|
174
|
+
// No recognizable header — prepend the entry
|
|
175
|
+
fs.writeFileSync(milestonesPath, milestoneEntry + existing, 'utf-8');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Update STATE.md
|
|
183
|
+
if (fs.existsSync(statePath)) {
|
|
184
|
+
let stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
185
|
+
stateContent = stateContent.replace(
|
|
186
|
+
/(\*\*Status:\*\*\s*).*/,
|
|
187
|
+
`$1${version} milestone complete`
|
|
188
|
+
);
|
|
189
|
+
stateContent = stateContent.replace(
|
|
190
|
+
/(\*\*Last Activity:\*\*\s*).*/,
|
|
191
|
+
`$1${today}`
|
|
192
|
+
);
|
|
193
|
+
stateContent = stateContent.replace(
|
|
194
|
+
/(\*\*Last Activity Description:\*\*\s*).*/,
|
|
195
|
+
`$1${version} milestone completed and archived`
|
|
196
|
+
);
|
|
197
|
+
writeStateMd(statePath, stateContent, cwd);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Archive phase directories if requested
|
|
201
|
+
let phasesArchived = false;
|
|
202
|
+
if (options.archivePhases) {
|
|
203
|
+
try {
|
|
204
|
+
const phaseArchiveDir = path.join(archiveDir, `${version}-phases`);
|
|
205
|
+
fs.mkdirSync(phaseArchiveDir, { recursive: true });
|
|
206
|
+
|
|
207
|
+
const phaseEntries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
208
|
+
const phaseDirNames = phaseEntries.filter(e => e.isDirectory()).map(e => e.name);
|
|
209
|
+
let archivedCount = 0;
|
|
210
|
+
for (const dir of phaseDirNames) {
|
|
211
|
+
if (!isDirInMilestone(dir)) continue;
|
|
212
|
+
fs.renameSync(path.join(phasesDir, dir), path.join(phaseArchiveDir, dir));
|
|
213
|
+
archivedCount++;
|
|
214
|
+
}
|
|
215
|
+
phasesArchived = archivedCount > 0;
|
|
216
|
+
} catch {}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const result = {
|
|
220
|
+
version,
|
|
221
|
+
name: milestoneName,
|
|
222
|
+
date: today,
|
|
223
|
+
phases: phaseCount,
|
|
224
|
+
plans: totalPlans,
|
|
225
|
+
tasks: totalTasks,
|
|
226
|
+
accomplishments,
|
|
227
|
+
archived: {
|
|
228
|
+
roadmap: fs.existsSync(path.join(archiveDir, `${version}-ROADMAP.md`)),
|
|
229
|
+
requirements: fs.existsSync(path.join(archiveDir, `${version}-REQUIREMENTS.md`)),
|
|
230
|
+
audit: fs.existsSync(path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`)),
|
|
231
|
+
phases: phasesArchived,
|
|
232
|
+
},
|
|
233
|
+
milestones_updated: true,
|
|
234
|
+
state_updated: fs.existsSync(statePath),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
output(result, raw);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = {
|
|
241
|
+
cmdRequirementsMarkComplete,
|
|
242
|
+
cmdMilestoneComplete,
|
|
243
|
+
};
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overlap — Cross-project overlap detection at repo and file level
|
|
3
|
+
*
|
|
4
|
+
* Scans plan files across all active projects to detect which repos and files
|
|
5
|
+
* are touched by multiple projects simultaneously. Provides both repo-level
|
|
6
|
+
* and file-level overlap reporting.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { safeReadFile, getProjectFolders, output, error, getProjectDir } = require('./core.cjs');
|
|
12
|
+
const { parseProjectsMd, readProjectState } = require('./projects.cjs');
|
|
13
|
+
const { parseReposMd, resolveFileToRepo } = require('./repos.cjs');
|
|
14
|
+
|
|
15
|
+
// ─── Severity Order ─────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const SEVERITY_ORDER = { HIGH: 0, MEDIUM: 1, LOW: 2 };
|
|
18
|
+
|
|
19
|
+
// ─── Scan Project Plan Files ─────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Scan a project's plan files and extract repos and files from XML tags.
|
|
23
|
+
*
|
|
24
|
+
* Scans all *-PLAN.md files under .planning/projects/<slug>/phases/ subdirectories.
|
|
25
|
+
* For each plan file, extracts all <repos> and <files> tags.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} cwd - Working directory (product root)
|
|
28
|
+
* @param {string} slug - Project slug
|
|
29
|
+
* @returns {Array<{ plan: string, repos: string[], files: string[] }>}
|
|
30
|
+
*/
|
|
31
|
+
function scanProjectPlanFiles(cwd, slug) {
|
|
32
|
+
const phasesDir = path.join(getProjectDir(cwd, slug), 'phases');
|
|
33
|
+
const results = [];
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
37
|
+
.filter(e => e.isDirectory())
|
|
38
|
+
.map(e => e.name);
|
|
39
|
+
|
|
40
|
+
// Exclude archived phase directories
|
|
41
|
+
const activePhaseDirs = excludeArchivedPhases(
|
|
42
|
+
phaseDirs.map(d => path.join(phasesDir, d))
|
|
43
|
+
).map(d => path.basename(d));
|
|
44
|
+
|
|
45
|
+
for (const phaseDir of activePhaseDirs) {
|
|
46
|
+
const fullPhaseDir = path.join(phasesDir, phaseDir);
|
|
47
|
+
let files;
|
|
48
|
+
try {
|
|
49
|
+
files = fs.readdirSync(fullPhaseDir)
|
|
50
|
+
.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
|
51
|
+
} catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const planFile of files) {
|
|
56
|
+
const content = safeReadFile(path.join(fullPhaseDir, planFile));
|
|
57
|
+
if (!content) continue;
|
|
58
|
+
|
|
59
|
+
const repos = new Set();
|
|
60
|
+
const filesList = new Set();
|
|
61
|
+
|
|
62
|
+
// Extract <repos> tags
|
|
63
|
+
const reposMatches = content.match(/<repos>([^<]*)<\/repos>/g);
|
|
64
|
+
if (reposMatches) {
|
|
65
|
+
for (const match of reposMatches) {
|
|
66
|
+
const inner = match.replace(/<\/?repos>/g, '');
|
|
67
|
+
inner.split(',').map(n => n.trim()).filter(n => n).forEach(n => repos.add(n));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Extract <files> tags
|
|
72
|
+
const filesMatches = content.match(/<files>([^<]*)<\/files>/g);
|
|
73
|
+
if (filesMatches) {
|
|
74
|
+
for (const match of filesMatches) {
|
|
75
|
+
const inner = match.replace(/<\/?files>/g, '');
|
|
76
|
+
inner.split(/[,\n]/).map(n => n.trim()).filter(n => n).forEach(n => filesList.add(n));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
results.push({
|
|
81
|
+
plan: `${phaseDir}/${planFile}`,
|
|
82
|
+
repos: Array.from(repos).sort(),
|
|
83
|
+
files: Array.from(filesList).sort(),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// phases directory doesn't exist or other error
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Exclude Archived Phases ─────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Filter out phase directories that are in archived milestone paths.
|
|
98
|
+
*
|
|
99
|
+
* Archived paths match patterns like:
|
|
100
|
+
* .planning/milestones/v1.0-phases/01-setup
|
|
101
|
+
*
|
|
102
|
+
* @param {string[]} phaseDirs - Array of full phase directory paths
|
|
103
|
+
* @returns {string[]} Filtered array with archived paths removed
|
|
104
|
+
*/
|
|
105
|
+
function excludeArchivedPhases(phaseDirs) {
|
|
106
|
+
return phaseDirs.filter(dir => {
|
|
107
|
+
const normalized = dir.replace(/\\/g, '/');
|
|
108
|
+
return !normalized.includes('/milestones/');
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Classify Overlap Severity ───────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Classify the severity of an overlap between projects in a shared repo.
|
|
116
|
+
*
|
|
117
|
+
* - HIGH: Two or more projects modify the exact same file(s).
|
|
118
|
+
* - MEDIUM: No same-file conflicts, but projects share directories.
|
|
119
|
+
* - LOW: Projects touch different directories in the same repo.
|
|
120
|
+
*
|
|
121
|
+
* @param {{ file_conflicts: Array<{ file: string, projects: string[] }>, project_files: Object<string, string[]> }} overlapEntry
|
|
122
|
+
* - file_conflicts: files touched by 2+ projects
|
|
123
|
+
* - project_files: mapping of project slug -> array of file paths in this repo
|
|
124
|
+
* @returns {{ severity: string, reason: string }}
|
|
125
|
+
*/
|
|
126
|
+
function classifyOverlapSeverity(overlapEntry) {
|
|
127
|
+
const { file_conflicts, project_files } = overlapEntry;
|
|
128
|
+
|
|
129
|
+
// HIGH: actual file-level conflicts
|
|
130
|
+
if (file_conflicts && file_conflicts.length > 0) {
|
|
131
|
+
const fileList = file_conflicts.map(f => f.file).join(', ');
|
|
132
|
+
return {
|
|
133
|
+
severity: 'HIGH',
|
|
134
|
+
reason: `${file_conflicts.length} file(s) modified by multiple projects: ${fileList}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Compute per-project directory sets
|
|
139
|
+
if (project_files) {
|
|
140
|
+
const projectDirSets = {};
|
|
141
|
+
for (const [project, files] of Object.entries(project_files)) {
|
|
142
|
+
const dirs = new Set();
|
|
143
|
+
for (const file of files) {
|
|
144
|
+
dirs.add(path.dirname(file));
|
|
145
|
+
}
|
|
146
|
+
projectDirSets[project] = dirs;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check for directory intersection between any two projects
|
|
150
|
+
const projects = Object.keys(projectDirSets);
|
|
151
|
+
const sharedDirs = new Set();
|
|
152
|
+
for (let i = 0; i < projects.length; i++) {
|
|
153
|
+
for (let j = i + 1; j < projects.length; j++) {
|
|
154
|
+
for (const dir of projectDirSets[projects[i]]) {
|
|
155
|
+
if (projectDirSets[projects[j]].has(dir)) {
|
|
156
|
+
sharedDirs.add(dir);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (sharedDirs.size > 0) {
|
|
163
|
+
const dirList = Array.from(sharedDirs).sort().join(', ');
|
|
164
|
+
return {
|
|
165
|
+
severity: 'MEDIUM',
|
|
166
|
+
reason: `Projects share ${sharedDirs.size} directory(ies): ${dirList}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// LOW: different directories in same repo
|
|
172
|
+
return {
|
|
173
|
+
severity: 'LOW',
|
|
174
|
+
reason: 'Projects touch different directories in same repo',
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─── Get Active Project Slugs ────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get slugs of all active (non-completed) projects.
|
|
182
|
+
*
|
|
183
|
+
* Reads PROJECTS.md to get active project slugs. Falls back to scanning
|
|
184
|
+
* .planning/ subfolders if PROJECTS.md is missing. Applies ghost project
|
|
185
|
+
* guard (skips folders without STATE.md).
|
|
186
|
+
*
|
|
187
|
+
* @param {string} cwd - Working directory (product root)
|
|
188
|
+
* @returns {string[]} Array of active project slugs
|
|
189
|
+
*/
|
|
190
|
+
function getActiveProjectSlugs(cwd) {
|
|
191
|
+
// Try PROJECTS.md first
|
|
192
|
+
const projectsMd = parseProjectsMd(cwd);
|
|
193
|
+
if (projectsMd && projectsMd.active) {
|
|
194
|
+
// Filter to only those with folders on disk and valid STATE.md
|
|
195
|
+
return projectsMd.active
|
|
196
|
+
.map(p => p.name)
|
|
197
|
+
.filter(slug => {
|
|
198
|
+
const state = readProjectState(cwd, slug);
|
|
199
|
+
return state !== null;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Fallback: scan .planning/ subfolders
|
|
204
|
+
const folders = getProjectFolders(cwd);
|
|
205
|
+
return folders.filter(slug => {
|
|
206
|
+
const state = readProjectState(cwd, slug);
|
|
207
|
+
if (!state) return false;
|
|
208
|
+
// Exclude completed projects
|
|
209
|
+
return !state.status.toLowerCase().includes('completed');
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── Build Overlap Matrix ────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Scan all active projects and build the overlap matrix.
|
|
217
|
+
*
|
|
218
|
+
* For each active project, scans plan files and extracts repos/files tags.
|
|
219
|
+
* Builds a repo-to-projects mapping, then filters to repos touched by 2+ projects.
|
|
220
|
+
* For overlapping repos, identifies specific file-level conflicts.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} cwd - Working directory (product root)
|
|
223
|
+
* @returns {{ overlapping_repos: Array<{ repo: string, projects: string[], file_conflicts: Array<{ file: string, projects: string[] }> }>, warnings: string[] }}
|
|
224
|
+
*/
|
|
225
|
+
function buildOverlapMatrix(cwd) {
|
|
226
|
+
const warnings = [];
|
|
227
|
+
const activeSlugs = getActiveProjectSlugs(cwd);
|
|
228
|
+
|
|
229
|
+
// Load registered repos for centralized file-to-repo resolution
|
|
230
|
+
const reposMd = parseReposMd(cwd);
|
|
231
|
+
const registeredRepos = reposMd ? reposMd.repos : [];
|
|
232
|
+
|
|
233
|
+
// repo -> { projects: Set, files: Map<file, Set<project>>, project_files: Map<project, Set<file>> }
|
|
234
|
+
const repoMap = {};
|
|
235
|
+
|
|
236
|
+
for (const slug of activeSlugs) {
|
|
237
|
+
// Ghost project guard
|
|
238
|
+
const projectDir = getProjectDir(cwd, slug);
|
|
239
|
+
if (!fs.existsSync(projectDir)) {
|
|
240
|
+
warnings.push(`Ghost project: ${slug} (folder missing)`);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const planData = scanProjectPlanFiles(cwd, slug);
|
|
245
|
+
for (const plan of planData) {
|
|
246
|
+
for (const repo of plan.repos) {
|
|
247
|
+
if (!repoMap[repo]) {
|
|
248
|
+
repoMap[repo] = { projects: new Set(), files: {}, project_files: {} };
|
|
249
|
+
}
|
|
250
|
+
repoMap[repo].projects.add(slug);
|
|
251
|
+
}
|
|
252
|
+
// Map files to repos using plan's <repos> context (repo-relative paths
|
|
253
|
+
// can't be resolved via prefix-match alone)
|
|
254
|
+
for (const file of plan.files) {
|
|
255
|
+
// If plan has explicit repos, use them as context
|
|
256
|
+
const targetRepos = plan.repos.length > 0 ? plan.repos : [];
|
|
257
|
+
|
|
258
|
+
// Fallback: try resolveFileToRepo for legacy product-root-relative paths
|
|
259
|
+
if (targetRepos.length === 0) {
|
|
260
|
+
const resolved = resolveFileToRepo(file, registeredRepos);
|
|
261
|
+
const targetRepo = resolved ? resolved.name : '__unknown__';
|
|
262
|
+
if (!repoMap[targetRepo]) {
|
|
263
|
+
repoMap[targetRepo] = { projects: new Set(), files: {}, project_files: {} };
|
|
264
|
+
}
|
|
265
|
+
repoMap[targetRepo].projects.add(slug);
|
|
266
|
+
if (!repoMap[targetRepo].files[file]) {
|
|
267
|
+
repoMap[targetRepo].files[file] = new Set();
|
|
268
|
+
}
|
|
269
|
+
repoMap[targetRepo].files[file].add(slug);
|
|
270
|
+
if (!repoMap[targetRepo].project_files[slug]) {
|
|
271
|
+
repoMap[targetRepo].project_files[slug] = [];
|
|
272
|
+
}
|
|
273
|
+
if (!repoMap[targetRepo].project_files[slug].includes(file)) {
|
|
274
|
+
repoMap[targetRepo].project_files[slug].push(file);
|
|
275
|
+
}
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for (const repoName of targetRepos) {
|
|
280
|
+
if (!repoMap[repoName]) {
|
|
281
|
+
repoMap[repoName] = { projects: new Set(), files: {}, project_files: {} };
|
|
282
|
+
}
|
|
283
|
+
repoMap[repoName].projects.add(slug);
|
|
284
|
+
if (!repoMap[repoName].files[file]) {
|
|
285
|
+
repoMap[repoName].files[file] = new Set();
|
|
286
|
+
}
|
|
287
|
+
repoMap[repoName].files[file].add(slug);
|
|
288
|
+
if (!repoMap[repoName].project_files[slug]) {
|
|
289
|
+
repoMap[repoName].project_files[slug] = [];
|
|
290
|
+
}
|
|
291
|
+
if (!repoMap[repoName].project_files[slug].includes(file)) {
|
|
292
|
+
repoMap[repoName].project_files[slug].push(file);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Also check PROJECTS.md for ghost projects not caught above
|
|
300
|
+
const projectsMd = parseProjectsMd(cwd);
|
|
301
|
+
if (projectsMd && projectsMd.active) {
|
|
302
|
+
for (const p of projectsMd.active) {
|
|
303
|
+
const projectDir = getProjectDir(cwd, p.name);
|
|
304
|
+
if (!fs.existsSync(projectDir) && !warnings.some(w => w.includes(p.name))) {
|
|
305
|
+
warnings.push(`Ghost project: ${p.name} (folder missing)`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Filter to repos touched by 2+ projects
|
|
311
|
+
const overlapping_repos = [];
|
|
312
|
+
for (const [repo, data] of Object.entries(repoMap)) {
|
|
313
|
+
if (repo === '__unknown__') continue;
|
|
314
|
+
if (data.projects.size < 2) continue;
|
|
315
|
+
|
|
316
|
+
// Identify file-level conflicts (files touched by 2+ projects)
|
|
317
|
+
const file_conflicts = [];
|
|
318
|
+
for (const [file, projectSet] of Object.entries(data.files)) {
|
|
319
|
+
if (projectSet.size >= 2) {
|
|
320
|
+
file_conflicts.push({
|
|
321
|
+
file,
|
|
322
|
+
projects: Array.from(projectSet).sort(),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Classify severity based on file conflicts and project file locations
|
|
328
|
+
const severityResult = classifyOverlapSeverity({
|
|
329
|
+
file_conflicts,
|
|
330
|
+
project_files: data.project_files,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
overlapping_repos.push({
|
|
334
|
+
repo,
|
|
335
|
+
projects: Array.from(data.projects).sort(),
|
|
336
|
+
file_conflicts: file_conflicts.sort((a, b) => a.file.localeCompare(b.file)),
|
|
337
|
+
severity: severityResult.severity,
|
|
338
|
+
severity_reason: severityResult.reason,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Sort by severity (HIGH first, then MEDIUM, then LOW), then by repo name within same severity
|
|
343
|
+
overlapping_repos.sort((a, b) => {
|
|
344
|
+
const sevDiff = SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity];
|
|
345
|
+
if (sevDiff !== 0) return sevDiff;
|
|
346
|
+
return a.repo.localeCompare(b.repo);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
return { overlapping_repos, warnings };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ─── Format Overlap Report ───────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Format the overlap matrix for human-readable display.
|
|
356
|
+
*
|
|
357
|
+
* @param {{ overlapping_repos: Array, warnings: string[] }} matrix
|
|
358
|
+
* @returns {string} Formatted report text
|
|
359
|
+
*/
|
|
360
|
+
function formatOverlapReport(matrix) {
|
|
361
|
+
if (!matrix.overlapping_repos || matrix.overlapping_repos.length === 0) {
|
|
362
|
+
return 'No cross-project overlaps detected.\n\nEach active project touches unique repos.';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const lines = [];
|
|
366
|
+
const count = matrix.overlapping_repos.length;
|
|
367
|
+
lines.push(`${count} repo(s) touched by multiple active projects\n`);
|
|
368
|
+
|
|
369
|
+
// Repo-level table with severity column
|
|
370
|
+
lines.push('| Repo | Severity | Projects |');
|
|
371
|
+
lines.push('|------|----------|----------|');
|
|
372
|
+
for (const overlap of matrix.overlapping_repos) {
|
|
373
|
+
const severity = overlap.severity || 'LOW';
|
|
374
|
+
lines.push(`| ${overlap.repo} | ${severity} | ${overlap.projects.join(', ')} |`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// File-level drill-down only for HIGH severity repos
|
|
378
|
+
const highSeverityRepos = matrix.overlapping_repos.filter(
|
|
379
|
+
o => o.severity === 'HIGH' && o.file_conflicts && o.file_conflicts.length > 0
|
|
380
|
+
);
|
|
381
|
+
if (highSeverityRepos.length > 0) {
|
|
382
|
+
lines.push('');
|
|
383
|
+
lines.push('### File-Level Conflicts');
|
|
384
|
+
for (const overlap of highSeverityRepos) {
|
|
385
|
+
lines.push('');
|
|
386
|
+
lines.push(`**${overlap.repo}:**`);
|
|
387
|
+
for (const conflict of overlap.file_conflicts) {
|
|
388
|
+
lines.push(`- ${conflict.file} — ${conflict.projects.join(', ')}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
lines.push('');
|
|
394
|
+
lines.push('This is informational — overlapping repos don\'t block work.');
|
|
395
|
+
lines.push('Coordinate merges manually if projects touch the same files.');
|
|
396
|
+
|
|
397
|
+
if (matrix.warnings.length > 0) {
|
|
398
|
+
lines.push('');
|
|
399
|
+
lines.push('### Warnings');
|
|
400
|
+
for (const w of matrix.warnings) {
|
|
401
|
+
lines.push(`- ${w}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return lines.join('\n');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ─── CLI Command Function ────────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* CLI: Run overlap check and output results.
|
|
412
|
+
*
|
|
413
|
+
* @param {string} cwd - Working directory (product root)
|
|
414
|
+
* @param {boolean} raw - Raw output mode
|
|
415
|
+
*/
|
|
416
|
+
function cmdOverlapCheck(cwd, raw) {
|
|
417
|
+
const matrix = buildOverlapMatrix(cwd);
|
|
418
|
+
|
|
419
|
+
if (raw) {
|
|
420
|
+
output(matrix, raw);
|
|
421
|
+
} else {
|
|
422
|
+
const report = formatOverlapReport(matrix);
|
|
423
|
+
output({ ...matrix, report }, false);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ─── Exports ────────────────────────────────────────────────────────────────
|
|
428
|
+
|
|
429
|
+
module.exports = {
|
|
430
|
+
scanProjectPlanFiles,
|
|
431
|
+
buildOverlapMatrix,
|
|
432
|
+
excludeArchivedPhases,
|
|
433
|
+
formatOverlapReport,
|
|
434
|
+
getActiveProjectSlugs,
|
|
435
|
+
classifyOverlapSeverity,
|
|
436
|
+
cmdOverlapCheck,
|
|
437
|
+
};
|