specweave 0.34.4 → 0.34.6
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/bin/fix-marketplace-errors.sh +55 -7
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.js +3 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +11 -2
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/living-docs.js +4 -4
- package/dist/src/cli/commands/living-docs.js.map +1 -1
- package/dist/src/cli/helpers/init/brownfield-analysis.js +15 -15
- package/dist/src/cli/helpers/init/brownfield-analysis.js.map +1 -1
- package/dist/src/cli/helpers/init/living-docs-preflight.js +7 -7
- package/dist/src/cli/helpers/init/living-docs-preflight.js.map +1 -1
- package/dist/src/cli/helpers/init/plugin-installer.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/plugin-installer.js +61 -9
- package/dist/src/cli/helpers/init/plugin-installer.js.map +1 -1
- package/dist/src/core/background/types.d.ts +3 -0
- package/dist/src/core/background/types.d.ts.map +1 -1
- package/dist/src/core/living-docs/delivery/delivery-generator.d.ts +58 -0
- package/dist/src/core/living-docs/delivery/delivery-generator.d.ts.map +1 -0
- package/dist/src/core/living-docs/delivery/delivery-generator.js +501 -0
- package/dist/src/core/living-docs/delivery/delivery-generator.js.map +1 -0
- package/dist/src/core/living-docs/delivery/index.d.ts +8 -0
- package/dist/src/core/living-docs/delivery/index.d.ts.map +1 -0
- package/dist/src/core/living-docs/delivery/index.js +7 -0
- package/dist/src/core/living-docs/delivery/index.js.map +1 -0
- package/dist/src/core/living-docs/diagrams/index.d.ts +8 -0
- package/dist/src/core/living-docs/diagrams/index.d.ts.map +1 -0
- package/dist/src/core/living-docs/diagrams/index.js +7 -0
- package/dist/src/core/living-docs/diagrams/index.js.map +1 -0
- package/dist/src/core/living-docs/diagrams/mermaid-generator.d.ts +103 -0
- package/dist/src/core/living-docs/diagrams/mermaid-generator.d.ts.map +1 -0
- package/dist/src/core/living-docs/diagrams/mermaid-generator.js +515 -0
- package/dist/src/core/living-docs/diagrams/mermaid-generator.js.map +1 -0
- package/dist/src/core/living-docs/enterprise/enterprise-generator.d.ts +85 -0
- package/dist/src/core/living-docs/enterprise/enterprise-generator.d.ts.map +1 -0
- package/dist/src/core/living-docs/enterprise/enterprise-generator.js +556 -0
- package/dist/src/core/living-docs/enterprise/enterprise-generator.js.map +1 -0
- package/dist/src/core/living-docs/enterprise/history-analyzer.d.ts +91 -0
- package/dist/src/core/living-docs/enterprise/history-analyzer.d.ts.map +1 -0
- package/dist/src/core/living-docs/enterprise/history-analyzer.js +321 -0
- package/dist/src/core/living-docs/enterprise/history-analyzer.js.map +1 -0
- package/dist/src/core/living-docs/enterprise/index.d.ts +18 -0
- package/dist/src/core/living-docs/enterprise/index.d.ts.map +1 -0
- package/dist/src/core/living-docs/enterprise/index.js +14 -0
- package/dist/src/core/living-docs/enterprise/index.js.map +1 -0
- package/dist/src/core/living-docs/enterprise/relationship-mapper.d.ts +58 -0
- package/dist/src/core/living-docs/enterprise/relationship-mapper.d.ts.map +1 -0
- package/dist/src/core/living-docs/enterprise/relationship-mapper.js +227 -0
- package/dist/src/core/living-docs/enterprise/relationship-mapper.js.map +1 -0
- package/dist/src/core/living-docs/enterprise/spec-loader.d.ts +161 -0
- package/dist/src/core/living-docs/enterprise/spec-loader.d.ts.map +1 -0
- package/dist/src/core/living-docs/enterprise/spec-loader.js +470 -0
- package/dist/src/core/living-docs/enterprise/spec-loader.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/inconsistency-detector.d.ts +31 -1
- package/dist/src/core/living-docs/intelligent-analyzer/inconsistency-detector.d.ts.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/inconsistency-detector.js +626 -14
- package/dist/src/core/living-docs/intelligent-analyzer/inconsistency-detector.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/index.d.ts +8 -0
- package/dist/src/core/living-docs/intelligent-analyzer/index.d.ts.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/index.js +87 -4
- package/dist/src/core/living-docs/intelligent-analyzer/index.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts +3 -1
- package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts.map +1 -1
- package/dist/src/core/living-docs/operations/index.d.ts +8 -0
- package/dist/src/core/living-docs/operations/index.d.ts.map +1 -0
- package/dist/src/core/living-docs/operations/index.js +7 -0
- package/dist/src/core/living-docs/operations/index.js.map +1 -0
- package/dist/src/core/living-docs/operations/ops-generator.d.ts +53 -0
- package/dist/src/core/living-docs/operations/ops-generator.d.ts.map +1 -0
- package/dist/src/core/living-docs/operations/ops-generator.js +462 -0
- package/dist/src/core/living-docs/operations/ops-generator.js.map +1 -0
- package/package.json +1 -1
- package/plugins/specweave/commands/living-docs.md +168 -39
- package/plugins/specweave-github/lib/github-feature-sync.js +1 -1
- package/plugins/specweave-github/lib/github-feature-sync.ts +3 -1
|
@@ -3,33 +3,487 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Finds inconsistencies, duplicates, ownership gaps,
|
|
5
5
|
* and generates questions for CTO/PO.
|
|
6
|
+
*
|
|
7
|
+
* Enhanced with:
|
|
8
|
+
* - Severity categorization (P0-P3)
|
|
9
|
+
* - Broken link detection
|
|
10
|
+
* - Spec-code gap detection
|
|
11
|
+
* - Orphaned document detection
|
|
6
12
|
*/
|
|
7
13
|
import * as fs from 'fs';
|
|
8
14
|
import * as path from 'path';
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
import { glob } from 'glob';
|
|
16
|
+
export async function detectInconsistencies(repoAnalyses, orgResult, llmProvider, onProgress, log, projectPath) {
|
|
17
|
+
log('PHASE E: Inconsistency Detection (Enhanced)');
|
|
11
18
|
onProgress('inconsistency', 0, 100, 'Analyzing patterns');
|
|
12
19
|
const issues = [];
|
|
20
|
+
const enhancedIssues = [];
|
|
13
21
|
const techDebt = [];
|
|
14
|
-
|
|
22
|
+
const byPriority = new Map([
|
|
23
|
+
['P0', []],
|
|
24
|
+
['P1', []],
|
|
25
|
+
['P2', []],
|
|
26
|
+
['P3', []],
|
|
27
|
+
]);
|
|
28
|
+
// Phase E.1: Pattern Inconsistencies
|
|
29
|
+
onProgress('inconsistency', 10, 100, 'Checking pattern consistency');
|
|
15
30
|
const patternIssues = detectPatternInconsistencies(repoAnalyses);
|
|
16
31
|
issues.push(...patternIssues);
|
|
32
|
+
const enhancedPatterns = patternIssues.map(i => enhanceIssue(i, 'inconsistency'));
|
|
33
|
+
enhancedIssues.push(...enhancedPatterns);
|
|
17
34
|
log(' Found ' + patternIssues.length + ' pattern inconsistencies');
|
|
18
|
-
|
|
35
|
+
// Phase E.2: Potential Duplicates
|
|
36
|
+
onProgress('inconsistency', 20, 100, 'Finding duplicates');
|
|
19
37
|
const duplicates = detectPotentialDuplicates(repoAnalyses);
|
|
20
38
|
issues.push(...duplicates);
|
|
39
|
+
const enhancedDups = duplicates.map(i => enhanceIssue(i, 'duplicate'));
|
|
40
|
+
enhancedIssues.push(...enhancedDups);
|
|
21
41
|
log(' Found ' + duplicates.length + ' potential duplicates');
|
|
22
|
-
|
|
42
|
+
// Phase E.3: Ownership Gaps
|
|
43
|
+
onProgress('inconsistency', 30, 100, 'Checking ownership');
|
|
23
44
|
const ownershipIssues = detectOwnershipGaps(repoAnalyses, orgResult);
|
|
24
45
|
issues.push(...ownershipIssues);
|
|
46
|
+
const enhancedOwn = ownershipIssues.map(i => enhanceIssue(i, 'ownership'));
|
|
47
|
+
enhancedIssues.push(...enhancedOwn);
|
|
25
48
|
log(' Found ' + ownershipIssues.length + ' ownership issues');
|
|
26
|
-
|
|
49
|
+
// Phase E.4: Broken Links (new)
|
|
50
|
+
let brokenLinks = [];
|
|
51
|
+
if (projectPath) {
|
|
52
|
+
onProgress('inconsistency', 40, 100, 'Detecting broken links');
|
|
53
|
+
brokenLinks = await detectBrokenLinks(projectPath);
|
|
54
|
+
log(' Found ' + brokenLinks.length + ' broken links');
|
|
55
|
+
// Convert to issues
|
|
56
|
+
if (brokenLinks.length > 0) {
|
|
57
|
+
const brokenLinkIssue = createBrokenLinkIssue(brokenLinks);
|
|
58
|
+
issues.push(brokenLinkIssue);
|
|
59
|
+
enhancedIssues.push(enhanceIssue(brokenLinkIssue, 'broken-link'));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Phase E.5: Spec-Code Gaps (new)
|
|
63
|
+
let specCodeGaps = [];
|
|
64
|
+
if (projectPath) {
|
|
65
|
+
onProgress('inconsistency', 55, 100, 'Detecting spec-code gaps');
|
|
66
|
+
specCodeGaps = await detectSpecCodeGaps(projectPath);
|
|
67
|
+
log(' Found ' + specCodeGaps.length + ' spec-code gaps');
|
|
68
|
+
// Convert to issues
|
|
69
|
+
for (const gap of specCodeGaps) {
|
|
70
|
+
const gapIssue = createSpecCodeGapIssue(gap);
|
|
71
|
+
issues.push(gapIssue);
|
|
72
|
+
enhancedIssues.push(enhanceIssue(gapIssue, 'spec-gap'));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Phase E.6: Orphaned Documents (new)
|
|
76
|
+
let orphanedDocs = [];
|
|
77
|
+
if (projectPath) {
|
|
78
|
+
onProgress('inconsistency', 70, 100, 'Detecting orphaned documents');
|
|
79
|
+
orphanedDocs = await detectOrphanedDocs(projectPath);
|
|
80
|
+
log(' Found ' + orphanedDocs.length + ' orphaned documents');
|
|
81
|
+
// Convert to issue
|
|
82
|
+
if (orphanedDocs.length > 0) {
|
|
83
|
+
const orphanIssue = createOrphanedDocsIssue(orphanedDocs);
|
|
84
|
+
issues.push(orphanIssue);
|
|
85
|
+
enhancedIssues.push(enhanceIssue(orphanIssue, 'orphaned'));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Phase E.7: Tech Debt
|
|
89
|
+
onProgress('inconsistency', 85, 100, 'Cataloging tech debt');
|
|
27
90
|
const debt = extractTechDebt(repoAnalyses);
|
|
28
91
|
techDebt.push(...debt);
|
|
29
92
|
log(' Identified ' + debt.length + ' tech debt items');
|
|
93
|
+
// Categorize by priority
|
|
94
|
+
for (const issue of enhancedIssues) {
|
|
95
|
+
byPriority.get(issue.priority).push(issue);
|
|
96
|
+
}
|
|
30
97
|
const questionsForCTO = generateCTOQuestions(issues, techDebt, orgResult);
|
|
31
98
|
const questionsForPO = generatePOQuestions(issues, repoAnalyses);
|
|
32
|
-
|
|
99
|
+
onProgress('inconsistency', 100, 100, 'Detection complete');
|
|
100
|
+
return {
|
|
101
|
+
issues,
|
|
102
|
+
enhancedIssues,
|
|
103
|
+
techDebt,
|
|
104
|
+
questionsForCTO,
|
|
105
|
+
questionsForPO,
|
|
106
|
+
byPriority,
|
|
107
|
+
brokenLinks,
|
|
108
|
+
specCodeGaps,
|
|
109
|
+
orphanedDocs,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Enhance a basic issue with priority, remediation, and category
|
|
114
|
+
*/
|
|
115
|
+
function enhanceIssue(issue, category) {
|
|
116
|
+
const priority = determinePriority(issue, category);
|
|
117
|
+
const remediation = generateRemediation(issue, category);
|
|
118
|
+
return {
|
|
119
|
+
...issue,
|
|
120
|
+
priority,
|
|
121
|
+
remediation,
|
|
122
|
+
category,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Determine issue priority based on type and severity
|
|
127
|
+
*/
|
|
128
|
+
function determinePriority(issue, category) {
|
|
129
|
+
// P0: Critical - blocks production or security risk
|
|
130
|
+
if (category === 'spec-gap' && issue.description.includes('ghost_completion')) {
|
|
131
|
+
return 'P0'; // Ghost completions are critical - shows we think something is done but isn't
|
|
132
|
+
}
|
|
133
|
+
if (category === 'broken-link' && issue.description.includes('critical')) {
|
|
134
|
+
return 'P0';
|
|
135
|
+
}
|
|
136
|
+
// P1: High - significant issues needing attention
|
|
137
|
+
if (issue.severity === 'critical')
|
|
138
|
+
return 'P1';
|
|
139
|
+
if (category === 'ownership' && issue.evidence && issue.evidence.length > 5) {
|
|
140
|
+
return 'P1'; // Many repos without owners is serious
|
|
141
|
+
}
|
|
142
|
+
if (category === 'duplicate' && issue.evidence && issue.evidence.length > 3) {
|
|
143
|
+
return 'P1'; // Many duplicates suggest bigger architecture issue
|
|
144
|
+
}
|
|
145
|
+
// P2: Medium - should fix but not urgent
|
|
146
|
+
if (issue.severity === 'important')
|
|
147
|
+
return 'P2';
|
|
148
|
+
if (category === 'inconsistency')
|
|
149
|
+
return 'P2';
|
|
150
|
+
if (category === 'orphaned')
|
|
151
|
+
return 'P2';
|
|
152
|
+
// P3: Low - nice to have
|
|
153
|
+
return 'P3';
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Generate actionable remediation steps
|
|
157
|
+
*/
|
|
158
|
+
function generateRemediation(issue, category) {
|
|
159
|
+
const steps = [];
|
|
160
|
+
switch (category) {
|
|
161
|
+
case 'inconsistency':
|
|
162
|
+
steps.push('1. Review the conflicting patterns across repositories');
|
|
163
|
+
steps.push('2. Create an ADR documenting the standard pattern to use');
|
|
164
|
+
steps.push('3. Create migration tickets for non-compliant repos');
|
|
165
|
+
steps.push('4. Update linting rules to enforce the standard');
|
|
166
|
+
break;
|
|
167
|
+
case 'duplicate':
|
|
168
|
+
steps.push('1. Compare functionality of both services/repos');
|
|
169
|
+
steps.push('2. Identify which is the canonical source');
|
|
170
|
+
steps.push('3. Plan consolidation or document why both exist');
|
|
171
|
+
steps.push('4. Update documentation to clarify boundaries');
|
|
172
|
+
break;
|
|
173
|
+
case 'ownership':
|
|
174
|
+
steps.push('1. Review each unassigned repository');
|
|
175
|
+
steps.push('2. Determine appropriate team based on domain');
|
|
176
|
+
steps.push('3. Update CODEOWNERS file');
|
|
177
|
+
steps.push('4. Add ownership to team charter documents');
|
|
178
|
+
break;
|
|
179
|
+
case 'broken-link':
|
|
180
|
+
steps.push('1. Identify source files with broken links');
|
|
181
|
+
steps.push('2. Update or remove broken references');
|
|
182
|
+
steps.push('3. Add link validation to CI/CD pipeline');
|
|
183
|
+
steps.push('4. Consider using relative links where possible');
|
|
184
|
+
break;
|
|
185
|
+
case 'spec-gap':
|
|
186
|
+
steps.push('1. Compare spec completion claims with actual code');
|
|
187
|
+
steps.push('2. Update spec status to reflect reality');
|
|
188
|
+
steps.push('3. Create tickets for missing implementations');
|
|
189
|
+
steps.push('4. Review completion verification process');
|
|
190
|
+
break;
|
|
191
|
+
case 'orphaned':
|
|
192
|
+
steps.push('1. Review each orphaned document');
|
|
193
|
+
steps.push('2. Archive if no longer relevant');
|
|
194
|
+
steps.push('3. Link to appropriate parent if still relevant');
|
|
195
|
+
steps.push('4. Update documentation structure');
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
return steps;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Detect broken links in documentation
|
|
202
|
+
*/
|
|
203
|
+
async function detectBrokenLinks(projectPath) {
|
|
204
|
+
const brokenLinks = [];
|
|
205
|
+
const docsPath = path.join(projectPath, '.specweave/docs');
|
|
206
|
+
if (!fs.existsSync(docsPath))
|
|
207
|
+
return brokenLinks;
|
|
208
|
+
const mdFiles = await glob('**/*.md', { cwd: docsPath, absolute: true });
|
|
209
|
+
// Link patterns to detect
|
|
210
|
+
const linkPatterns = [
|
|
211
|
+
/\[([^\]]+)\]\(([^)]+)\)/g, // Markdown links [text](url)
|
|
212
|
+
/\[\[([^\]]+)\]\]/g, // Wiki-style links [[page]]
|
|
213
|
+
/href="([^"]+)"/g, // HTML href
|
|
214
|
+
];
|
|
215
|
+
for (const file of mdFiles) {
|
|
216
|
+
try {
|
|
217
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
218
|
+
const lines = content.split('\n');
|
|
219
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
220
|
+
const line = lines[lineNum];
|
|
221
|
+
for (const pattern of linkPatterns) {
|
|
222
|
+
let match;
|
|
223
|
+
pattern.lastIndex = 0; // Reset regex
|
|
224
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
225
|
+
const link = match[2] || match[1];
|
|
226
|
+
// Skip external URLs and anchors
|
|
227
|
+
if (link.startsWith('http') || link.startsWith('#') || link.startsWith('mailto:')) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
// Check if local file exists
|
|
231
|
+
const brokenReason = checkLinkValidity(file, link, projectPath);
|
|
232
|
+
if (brokenReason) {
|
|
233
|
+
brokenLinks.push({
|
|
234
|
+
file: path.relative(projectPath, file),
|
|
235
|
+
line: lineNum + 1,
|
|
236
|
+
link,
|
|
237
|
+
reason: brokenReason,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// Skip files that can't be read
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return brokenLinks;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Check if a link is valid
|
|
252
|
+
*/
|
|
253
|
+
function checkLinkValidity(sourceFile, link, projectPath) {
|
|
254
|
+
// Remove anchor from link
|
|
255
|
+
const linkPath = link.split('#')[0];
|
|
256
|
+
if (!linkPath)
|
|
257
|
+
return null; // Pure anchor link
|
|
258
|
+
// Resolve relative to source file or project root
|
|
259
|
+
let targetPath;
|
|
260
|
+
if (linkPath.startsWith('/')) {
|
|
261
|
+
targetPath = path.join(projectPath, linkPath);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
targetPath = path.resolve(path.dirname(sourceFile), linkPath);
|
|
265
|
+
}
|
|
266
|
+
// Check if target exists
|
|
267
|
+
if (!fs.existsSync(targetPath)) {
|
|
268
|
+
// Try with .md extension
|
|
269
|
+
if (!fs.existsSync(targetPath + '.md')) {
|
|
270
|
+
return 'File not found: ' + linkPath;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Detect gaps between specifications and code
|
|
277
|
+
*/
|
|
278
|
+
async function detectSpecCodeGaps(projectPath) {
|
|
279
|
+
const gaps = [];
|
|
280
|
+
const specsPath = path.join(projectPath, '.specweave/docs/internal/specs');
|
|
281
|
+
const incrementsPath = path.join(projectPath, '.specweave/increments');
|
|
282
|
+
if (!fs.existsSync(specsPath))
|
|
283
|
+
return gaps;
|
|
284
|
+
// Find all spec files
|
|
285
|
+
const specFiles = await glob('**/spec.md', { cwd: incrementsPath, absolute: true });
|
|
286
|
+
for (const specFile of specFiles) {
|
|
287
|
+
try {
|
|
288
|
+
const content = fs.readFileSync(specFile, 'utf-8');
|
|
289
|
+
const incrementFolder = path.dirname(specFile);
|
|
290
|
+
const metadataPath = path.join(incrementFolder, 'metadata.json');
|
|
291
|
+
if (!fs.existsSync(metadataPath))
|
|
292
|
+
continue;
|
|
293
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
294
|
+
// Check for ghost completions: status = completed but ACs unchecked
|
|
295
|
+
if (metadata.status === 'completed') {
|
|
296
|
+
const acPattern = /- \[ \] \*\*AC-/g;
|
|
297
|
+
const uncheckedACs = content.match(acPattern);
|
|
298
|
+
if (uncheckedACs && uncheckedACs.length > 0) {
|
|
299
|
+
gaps.push({
|
|
300
|
+
specId: metadata.id || path.basename(incrementFolder),
|
|
301
|
+
specFile: path.relative(projectPath, specFile),
|
|
302
|
+
type: 'ghost_completion',
|
|
303
|
+
confidence: 0.9,
|
|
304
|
+
details: `Increment marked complete but ${uncheckedACs.length} ACs unchecked`,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Check for missing implementations: status = active but no recent activity
|
|
309
|
+
if (metadata.status === 'active') {
|
|
310
|
+
const lastActivity = metadata.updatedAt ? new Date(metadata.updatedAt) : null;
|
|
311
|
+
const daysSinceActivity = lastActivity
|
|
312
|
+
? (Date.now() - lastActivity.getTime()) / (1000 * 60 * 60 * 24)
|
|
313
|
+
: 999;
|
|
314
|
+
if (daysSinceActivity > 30) {
|
|
315
|
+
gaps.push({
|
|
316
|
+
specId: metadata.id || path.basename(incrementFolder),
|
|
317
|
+
specFile: path.relative(projectPath, specFile),
|
|
318
|
+
type: 'missing_impl',
|
|
319
|
+
confidence: 0.7,
|
|
320
|
+
details: `Active increment with no activity for ${Math.floor(daysSinceActivity)} days`,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Check for outdated specs: tasks completed but spec not updated
|
|
325
|
+
const tasksPath = path.join(incrementFolder, 'tasks.md');
|
|
326
|
+
if (fs.existsSync(tasksPath)) {
|
|
327
|
+
const tasksContent = fs.readFileSync(tasksPath, 'utf-8');
|
|
328
|
+
const completedTasks = (tasksContent.match(/\[x\]/gi) || []).length;
|
|
329
|
+
const totalTasks = (tasksContent.match(/### T-\d+/g) || []).length;
|
|
330
|
+
if (completedTasks > 0 && totalTasks > 0) {
|
|
331
|
+
const specCompletedACs = (content.match(/- \[x\] \*\*AC-/g) || []).length;
|
|
332
|
+
const specTotalACs = (content.match(/- \[[ x]\] \*\*AC-/g) || []).length;
|
|
333
|
+
// If tasks are mostly done but ACs aren't updated
|
|
334
|
+
if (completedTasks / totalTasks > 0.7 && specTotalACs > 0 && specCompletedACs / specTotalACs < 0.3) {
|
|
335
|
+
gaps.push({
|
|
336
|
+
specId: metadata.id || path.basename(incrementFolder),
|
|
337
|
+
specFile: path.relative(projectPath, specFile),
|
|
338
|
+
type: 'outdated_spec',
|
|
339
|
+
confidence: 0.8,
|
|
340
|
+
details: `${completedTasks}/${totalTasks} tasks done but only ${specCompletedACs}/${specTotalACs} ACs checked`,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// Skip files that can't be processed
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return gaps;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Detect orphaned documents without proper links
|
|
354
|
+
*/
|
|
355
|
+
async function detectOrphanedDocs(projectPath) {
|
|
356
|
+
const orphaned = [];
|
|
357
|
+
const docsPath = path.join(projectPath, '.specweave/docs');
|
|
358
|
+
if (!fs.existsSync(docsPath))
|
|
359
|
+
return orphaned;
|
|
360
|
+
const mdFiles = await glob('**/*.md', { cwd: docsPath, absolute: true });
|
|
361
|
+
// Build a map of all documents and their references
|
|
362
|
+
const docRefs = new Map();
|
|
363
|
+
const allDocs = new Set();
|
|
364
|
+
for (const file of mdFiles) {
|
|
365
|
+
const relativePath = path.relative(docsPath, file);
|
|
366
|
+
allDocs.add(relativePath);
|
|
367
|
+
docRefs.set(relativePath, new Set());
|
|
368
|
+
try {
|
|
369
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
370
|
+
// Find all internal links
|
|
371
|
+
const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
372
|
+
let match;
|
|
373
|
+
while ((match = linkPattern.exec(content)) !== null) {
|
|
374
|
+
const link = match[2];
|
|
375
|
+
if (!link.startsWith('http') && !link.startsWith('#')) {
|
|
376
|
+
// Normalize link path
|
|
377
|
+
const linkPath = link.split('#')[0];
|
|
378
|
+
if (linkPath) {
|
|
379
|
+
const absPath = path.resolve(path.dirname(file), linkPath);
|
|
380
|
+
const relPath = path.relative(docsPath, absPath);
|
|
381
|
+
docRefs.get(relativePath).add(relPath);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// Skip unreadable files
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Find documents that are never referenced (except by themselves)
|
|
391
|
+
const referencedDocs = new Set();
|
|
392
|
+
for (const [source, refs] of docRefs) {
|
|
393
|
+
for (const ref of refs) {
|
|
394
|
+
if (ref !== source) {
|
|
395
|
+
referencedDocs.add(ref);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Index files and special files are not orphaned
|
|
400
|
+
const specialPatterns = [
|
|
401
|
+
/^index\.md$/i,
|
|
402
|
+
/^readme\.md$/i,
|
|
403
|
+
/^_/,
|
|
404
|
+
/overview/i,
|
|
405
|
+
/^internal\/specs\//, // Specs are linked from increments
|
|
406
|
+
];
|
|
407
|
+
for (const doc of allDocs) {
|
|
408
|
+
if (!referencedDocs.has(doc)) {
|
|
409
|
+
// Check if it's a special file
|
|
410
|
+
const isSpecial = specialPatterns.some(p => p.test(doc));
|
|
411
|
+
if (!isSpecial) {
|
|
412
|
+
orphaned.push(doc);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return orphaned;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Create an issue for broken links
|
|
420
|
+
*/
|
|
421
|
+
function createBrokenLinkIssue(brokenLinks) {
|
|
422
|
+
return {
|
|
423
|
+
id: 'BLK-001',
|
|
424
|
+
type: 'broken-link',
|
|
425
|
+
severity: brokenLinks.length > 10 ? 'critical' : 'important',
|
|
426
|
+
title: brokenLinks.length + ' broken links in documentation',
|
|
427
|
+
description: 'Found broken internal links that need to be fixed:\n' +
|
|
428
|
+
brokenLinks.slice(0, 10).map(l => `- ${l.file}:${l.line}: ${l.link} (${l.reason})`).join('\n') +
|
|
429
|
+
(brokenLinks.length > 10 ? `\n... and ${brokenLinks.length - 10} more` : ''),
|
|
430
|
+
evidence: brokenLinks.slice(0, 5).map(l => ({
|
|
431
|
+
repo: 'docs',
|
|
432
|
+
files: [l.file],
|
|
433
|
+
details: l.reason,
|
|
434
|
+
})),
|
|
435
|
+
suggestedAction: 'Fix broken links in documentation',
|
|
436
|
+
targetAudience: 'team',
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Create an issue for spec-code gaps
|
|
441
|
+
*/
|
|
442
|
+
function createSpecCodeGapIssue(gap) {
|
|
443
|
+
const typeLabels = {
|
|
444
|
+
ghost_completion: 'Ghost Completion',
|
|
445
|
+
missing_impl: 'Missing Implementation',
|
|
446
|
+
outdated_spec: 'Outdated Specification',
|
|
447
|
+
};
|
|
448
|
+
return {
|
|
449
|
+
id: 'GAP-' + gap.specId,
|
|
450
|
+
type: 'spec-gap',
|
|
451
|
+
severity: gap.type === 'ghost_completion' ? 'critical' : 'important',
|
|
452
|
+
title: typeLabels[gap.type] + ': ' + gap.specId,
|
|
453
|
+
description: gap.details,
|
|
454
|
+
evidence: [{
|
|
455
|
+
repo: 'increments',
|
|
456
|
+
files: [gap.specFile],
|
|
457
|
+
details: `Confidence: ${Math.round(gap.confidence * 100)}%`,
|
|
458
|
+
}],
|
|
459
|
+
suggestedAction: gap.type === 'ghost_completion'
|
|
460
|
+
? 'Review and update increment status or complete missing work'
|
|
461
|
+
: gap.type === 'missing_impl'
|
|
462
|
+
? 'Resume work on stalled increment or archive if no longer needed'
|
|
463
|
+
: 'Update spec.md to reflect completed work',
|
|
464
|
+
targetAudience: 'team',
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Create an issue for orphaned documents
|
|
469
|
+
*/
|
|
470
|
+
function createOrphanedDocsIssue(orphanedDocs) {
|
|
471
|
+
return {
|
|
472
|
+
id: 'ORP-001',
|
|
473
|
+
type: 'orphaned',
|
|
474
|
+
severity: 'minor',
|
|
475
|
+
title: orphanedDocs.length + ' orphaned documents without references',
|
|
476
|
+
description: 'These documents are not linked from anywhere:\n' +
|
|
477
|
+
orphanedDocs.slice(0, 15).map(d => `- ${d}`).join('\n') +
|
|
478
|
+
(orphanedDocs.length > 15 ? `\n... and ${orphanedDocs.length - 15} more` : ''),
|
|
479
|
+
evidence: orphanedDocs.slice(0, 5).map(d => ({
|
|
480
|
+
repo: 'docs',
|
|
481
|
+
files: [d],
|
|
482
|
+
details: 'No incoming links',
|
|
483
|
+
})),
|
|
484
|
+
suggestedAction: 'Review and either link, archive, or delete orphaned documents',
|
|
485
|
+
targetAudience: 'team',
|
|
486
|
+
};
|
|
33
487
|
}
|
|
34
488
|
function detectPatternInconsistencies(analyses) {
|
|
35
489
|
const issues = [];
|
|
@@ -185,6 +639,7 @@ export async function saveReviewNeeded(projectPath, result) {
|
|
|
185
639
|
const savedFiles = [];
|
|
186
640
|
const reviewPath = path.join(projectPath, '.specweave/docs/internal/review-needed');
|
|
187
641
|
fs.mkdirSync(reviewPath, { recursive: true });
|
|
642
|
+
// Save questions for CTO
|
|
188
643
|
if (result.questionsForCTO.length > 0) {
|
|
189
644
|
const ctoFile = path.join(reviewPath, 'questions-for-cto.md');
|
|
190
645
|
fs.writeFileSync(ctoFile, [
|
|
@@ -196,6 +651,7 @@ export async function saveReviewNeeded(projectPath, result) {
|
|
|
196
651
|
].join('\n'));
|
|
197
652
|
savedFiles.push(ctoFile);
|
|
198
653
|
}
|
|
654
|
+
// Save questions for PO
|
|
199
655
|
if (result.questionsForPO.length > 0) {
|
|
200
656
|
const poFile = path.join(reviewPath, 'questions-for-po.md');
|
|
201
657
|
fs.writeFileSync(poFile, [
|
|
@@ -207,35 +663,191 @@ export async function saveReviewNeeded(projectPath, result) {
|
|
|
207
663
|
].join('\n'));
|
|
208
664
|
savedFiles.push(poFile);
|
|
209
665
|
}
|
|
210
|
-
|
|
666
|
+
// Save CRITICAL-ISSUES.md (P0 and P1 only)
|
|
667
|
+
const criticalIssues = result.enhancedIssues.filter(i => i.priority === 'P0' || i.priority === 'P1');
|
|
668
|
+
if (criticalIssues.length > 0) {
|
|
669
|
+
const critFile = path.join(reviewPath, 'CRITICAL-ISSUES.md');
|
|
670
|
+
const lines = [
|
|
671
|
+
'# 🚨 Critical Issues',
|
|
672
|
+
'',
|
|
673
|
+
'*Auto-generated by Intelligent Analyzer*',
|
|
674
|
+
'',
|
|
675
|
+
`> **${criticalIssues.filter(i => i.priority === 'P0').length} P0** and **${criticalIssues.filter(i => i.priority === 'P1').length} P1** issues require immediate attention.`,
|
|
676
|
+
'',
|
|
677
|
+
];
|
|
678
|
+
// P0 first
|
|
679
|
+
const p0Issues = criticalIssues.filter(i => i.priority === 'P0');
|
|
680
|
+
if (p0Issues.length > 0) {
|
|
681
|
+
lines.push('## P0 - Critical (Immediate Action Required)', '');
|
|
682
|
+
for (const issue of p0Issues) {
|
|
683
|
+
lines.push('### ' + issue.id + ': ' + issue.title, '', '**Category**: ' + issue.category, '', issue.description, '', '**Remediation Steps**:', ...issue.remediation, '');
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// P1 next
|
|
687
|
+
const p1Issues = criticalIssues.filter(i => i.priority === 'P1');
|
|
688
|
+
if (p1Issues.length > 0) {
|
|
689
|
+
lines.push('## P1 - High Priority', '');
|
|
690
|
+
for (const issue of p1Issues) {
|
|
691
|
+
lines.push('### ' + issue.id + ': ' + issue.title, '', '**Category**: ' + issue.category, '', issue.description, '', '**Remediation Steps**:', ...issue.remediation, '');
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
fs.writeFileSync(critFile, lines.join('\n'));
|
|
695
|
+
savedFiles.push(critFile);
|
|
696
|
+
}
|
|
697
|
+
// Save inconsistencies.md
|
|
698
|
+
const inconsistencies = result.enhancedIssues.filter(i => i.category === 'inconsistency');
|
|
211
699
|
if (inconsistencies.length > 0) {
|
|
212
700
|
const incFile = path.join(reviewPath, 'inconsistencies.md');
|
|
213
|
-
const lines = ['# Inconsistencies', '', '*Auto-generated by Intelligent Analyzer*', ''];
|
|
701
|
+
const lines = ['# Pattern Inconsistencies', '', '*Auto-generated by Intelligent Analyzer*', ''];
|
|
214
702
|
for (const issue of inconsistencies) {
|
|
215
|
-
lines.push('## ' + issue.id + ': ' + issue.title, '', issue.description, '', '**
|
|
703
|
+
lines.push('## ' + issue.id + ': ' + issue.title, '', '**Priority**: ' + issue.priority, '', issue.description, '', '**Remediation**:', ...issue.remediation, '');
|
|
216
704
|
}
|
|
217
705
|
fs.writeFileSync(incFile, lines.join('\n'));
|
|
218
706
|
savedFiles.push(incFile);
|
|
219
707
|
}
|
|
220
|
-
|
|
708
|
+
// Save potential-duplicates.md
|
|
709
|
+
const duplicates = result.enhancedIssues.filter(i => i.category === 'duplicate');
|
|
221
710
|
if (duplicates.length > 0) {
|
|
222
711
|
const dupFile = path.join(reviewPath, 'potential-duplicates.md');
|
|
223
712
|
const lines = ['# Potential Duplicates', '', '*Auto-generated by Intelligent Analyzer*', ''];
|
|
224
713
|
for (const issue of duplicates) {
|
|
225
|
-
lines.push('## ' + issue.id + ': ' + issue.title, '', issue.description, '', '**
|
|
714
|
+
lines.push('## ' + issue.id + ': ' + issue.title, '', '**Priority**: ' + issue.priority, '', issue.description, '', '**Remediation**:', ...issue.remediation, '');
|
|
226
715
|
}
|
|
227
716
|
fs.writeFileSync(dupFile, lines.join('\n'));
|
|
228
717
|
savedFiles.push(dupFile);
|
|
229
718
|
}
|
|
719
|
+
// Save BROKEN-LINKS.md (new)
|
|
720
|
+
if (result.brokenLinks.length > 0) {
|
|
721
|
+
const blFile = path.join(reviewPath, 'BROKEN-LINKS.md');
|
|
722
|
+
const lines = [
|
|
723
|
+
'# Broken Links',
|
|
724
|
+
'',
|
|
725
|
+
'*Auto-generated by Intelligent Analyzer*',
|
|
726
|
+
'',
|
|
727
|
+
`Found **${result.brokenLinks.length}** broken links in documentation.`,
|
|
728
|
+
'',
|
|
729
|
+
'| File | Line | Link | Reason |',
|
|
730
|
+
'|------|------|------|--------|',
|
|
731
|
+
...result.brokenLinks.map(l => `| ${l.file} | ${l.line} | \`${l.link}\` | ${l.reason} |`),
|
|
732
|
+
'',
|
|
733
|
+
'## How to Fix',
|
|
734
|
+
'',
|
|
735
|
+
'1. Update each broken link to point to the correct file',
|
|
736
|
+
'2. Remove links to deleted content',
|
|
737
|
+
'3. Use relative paths where possible',
|
|
738
|
+
'4. Run `/sw:organize-docs` to regenerate navigation indexes',
|
|
739
|
+
];
|
|
740
|
+
fs.writeFileSync(blFile, lines.join('\n'));
|
|
741
|
+
savedFiles.push(blFile);
|
|
742
|
+
}
|
|
743
|
+
// Save SPEC-CODE-GAPS.md (new)
|
|
744
|
+
if (result.specCodeGaps.length > 0) {
|
|
745
|
+
const gapFile = path.join(reviewPath, 'SPEC-CODE-GAPS.md');
|
|
746
|
+
const lines = [
|
|
747
|
+
'# Spec-Code Gaps',
|
|
748
|
+
'',
|
|
749
|
+
'*Auto-generated by Intelligent Analyzer*',
|
|
750
|
+
'',
|
|
751
|
+
'Detected gaps between specifications and actual implementation status.',
|
|
752
|
+
'',
|
|
753
|
+
];
|
|
754
|
+
// Group by type
|
|
755
|
+
const ghostCompletions = result.specCodeGaps.filter(g => g.type === 'ghost_completion');
|
|
756
|
+
const missingImpl = result.specCodeGaps.filter(g => g.type === 'missing_impl');
|
|
757
|
+
const outdatedSpecs = result.specCodeGaps.filter(g => g.type === 'outdated_spec');
|
|
758
|
+
if (ghostCompletions.length > 0) {
|
|
759
|
+
lines.push('## 🚨 Ghost Completions (Critical)', '', 'These increments are marked as completed but have unchecked acceptance criteria:', '', '| Spec ID | File | Details | Confidence |', '|---------|------|---------|------------|', ...ghostCompletions.map(g => `| ${g.specId} | ${g.specFile} | ${g.details} | ${Math.round(g.confidence * 100)}% |`), '');
|
|
760
|
+
}
|
|
761
|
+
if (missingImpl.length > 0) {
|
|
762
|
+
lines.push('## ⚠️ Missing Implementations', '', 'These active increments have had no recent activity:', '', '| Spec ID | File | Details | Confidence |', '|---------|------|---------|------------|', ...missingImpl.map(g => `| ${g.specId} | ${g.specFile} | ${g.details} | ${Math.round(g.confidence * 100)}% |`), '');
|
|
763
|
+
}
|
|
764
|
+
if (outdatedSpecs.length > 0) {
|
|
765
|
+
lines.push('## 📝 Outdated Specifications', '', 'These specs have completed tasks but haven\'t updated their acceptance criteria:', '', '| Spec ID | File | Details | Confidence |', '|---------|------|---------|------------|', ...outdatedSpecs.map(g => `| ${g.specId} | ${g.specFile} | ${g.details} | ${Math.round(g.confidence * 100)}% |`), '');
|
|
766
|
+
}
|
|
767
|
+
fs.writeFileSync(gapFile, lines.join('\n'));
|
|
768
|
+
savedFiles.push(gapFile);
|
|
769
|
+
}
|
|
770
|
+
// Save ORPHANED-DOCS.md (new)
|
|
771
|
+
if (result.orphanedDocs.length > 0) {
|
|
772
|
+
const orphanFile = path.join(reviewPath, 'ORPHANED-DOCS.md');
|
|
773
|
+
const lines = [
|
|
774
|
+
'# Orphaned Documents',
|
|
775
|
+
'',
|
|
776
|
+
'*Auto-generated by Intelligent Analyzer*',
|
|
777
|
+
'',
|
|
778
|
+
`Found **${result.orphanedDocs.length}** documents with no incoming links.`,
|
|
779
|
+
'',
|
|
780
|
+
'## Documents to Review',
|
|
781
|
+
'',
|
|
782
|
+
...result.orphanedDocs.map(d => `- ${d}`),
|
|
783
|
+
'',
|
|
784
|
+
'## Recommended Actions',
|
|
785
|
+
'',
|
|
786
|
+
'1. **Link** - Add links from parent documents if still relevant',
|
|
787
|
+
'2. **Archive** - Move to archive if no longer needed',
|
|
788
|
+
'3. **Delete** - Remove if completely obsolete',
|
|
789
|
+
'4. **Consolidate** - Merge with related documents',
|
|
790
|
+
];
|
|
791
|
+
fs.writeFileSync(orphanFile, lines.join('\n'));
|
|
792
|
+
savedFiles.push(orphanFile);
|
|
793
|
+
}
|
|
794
|
+
// Save tech-debt.md
|
|
230
795
|
if (result.techDebt.length > 0) {
|
|
231
796
|
const debtFile = path.join(reviewPath, 'tech-debt.md');
|
|
232
|
-
const lines = ['# Tech Debt', '', '*Auto-generated by Intelligent Analyzer*', ''];
|
|
797
|
+
const lines = ['# Tech Debt Catalog', '', '*Auto-generated by Intelligent Analyzer*', ''];
|
|
798
|
+
// Group by severity
|
|
799
|
+
const bySeverity = new Map();
|
|
233
800
|
for (const item of result.techDebt) {
|
|
234
|
-
|
|
801
|
+
if (!bySeverity.has(item.severity))
|
|
802
|
+
bySeverity.set(item.severity, []);
|
|
803
|
+
bySeverity.get(item.severity).push(item);
|
|
804
|
+
}
|
|
805
|
+
for (const severity of ['high', 'medium', 'low']) {
|
|
806
|
+
const items = bySeverity.get(severity);
|
|
807
|
+
if (items && items.length > 0) {
|
|
808
|
+
lines.push('## ' + severity.charAt(0).toUpperCase() + severity.slice(1) + ' Priority', '');
|
|
809
|
+
for (const item of items) {
|
|
810
|
+
lines.push('### ' + item.id + ': ' + item.title, '', item.description, '', '- **Effort**: ' + item.effort, '- **Category**: ' + item.category, '- **Repos**: ' + item.repos.join(', '), '');
|
|
811
|
+
}
|
|
812
|
+
}
|
|
235
813
|
}
|
|
236
814
|
fs.writeFileSync(debtFile, lines.join('\n'));
|
|
237
815
|
savedFiles.push(debtFile);
|
|
238
816
|
}
|
|
817
|
+
// Save summary index
|
|
818
|
+
const indexFile = path.join(reviewPath, 'index.md');
|
|
819
|
+
const summary = [
|
|
820
|
+
'# Review Needed - Summary',
|
|
821
|
+
'',
|
|
822
|
+
'*Auto-generated by Intelligent Analyzer*',
|
|
823
|
+
'',
|
|
824
|
+
'## Overview',
|
|
825
|
+
'',
|
|
826
|
+
'| Category | Count | Priority |',
|
|
827
|
+
'|----------|-------|----------|',
|
|
828
|
+
`| Critical Issues | ${result.enhancedIssues.filter(i => i.priority === 'P0' || i.priority === 'P1').length} | P0/P1 |`,
|
|
829
|
+
`| Pattern Inconsistencies | ${result.enhancedIssues.filter(i => i.category === 'inconsistency').length} | P2 |`,
|
|
830
|
+
`| Potential Duplicates | ${result.enhancedIssues.filter(i => i.category === 'duplicate').length} | P2 |`,
|
|
831
|
+
`| Broken Links | ${result.brokenLinks.length} | P2/P3 |`,
|
|
832
|
+
`| Spec-Code Gaps | ${result.specCodeGaps.length} | P0-P2 |`,
|
|
833
|
+
`| Orphaned Docs | ${result.orphanedDocs.length} | P3 |`,
|
|
834
|
+
`| Tech Debt Items | ${result.techDebt.length} | Various |`,
|
|
835
|
+
'',
|
|
836
|
+
'## Priority Breakdown',
|
|
837
|
+
'',
|
|
838
|
+
`- **P0 (Critical)**: ${result.byPriority.get('P0')?.length || 0} issues`,
|
|
839
|
+
`- **P1 (High)**: ${result.byPriority.get('P1')?.length || 0} issues`,
|
|
840
|
+
`- **P2 (Medium)**: ${result.byPriority.get('P2')?.length || 0} issues`,
|
|
841
|
+
`- **P3 (Low)**: ${result.byPriority.get('P3')?.length || 0} issues`,
|
|
842
|
+
'',
|
|
843
|
+
'## Documents',
|
|
844
|
+
'',
|
|
845
|
+
savedFiles.length > 0
|
|
846
|
+
? savedFiles.map(f => `- [${path.basename(f)}](./${path.basename(f)})`).join('\n')
|
|
847
|
+
: '*No issues found*',
|
|
848
|
+
];
|
|
849
|
+
fs.writeFileSync(indexFile, summary.join('\n'));
|
|
850
|
+
savedFiles.push(indexFile);
|
|
239
851
|
return savedFiles;
|
|
240
852
|
}
|
|
241
853
|
//# sourceMappingURL=inconsistency-detector.js.map
|