@arvorco/relentless 0.1.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/.claude/commands/relentless.analyze.md +20 -0
- package/.claude/commands/relentless.checklist.md +15 -0
- package/.claude/commands/relentless.clarify.md +19 -0
- package/.claude/commands/relentless.constitution.md +78 -0
- package/.claude/commands/relentless.implement.md +15 -0
- package/.claude/commands/relentless.plan.md +22 -0
- package/.claude/commands/relentless.plan.old.md +89 -0
- package/.claude/commands/relentless.specify.md +254 -0
- package/.claude/commands/relentless.tasks.md +25 -0
- package/.claude/commands/relentless.taskstoissues.md +15 -0
- package/.claude/settings.local.json +23 -0
- package/.claude/skills/analyze/SKILL.md +149 -0
- package/.claude/skills/checklist/SKILL.md +173 -0
- package/.claude/skills/checklist/templates/checklist-template.md +40 -0
- package/.claude/skills/clarify/SKILL.md +174 -0
- package/.claude/skills/constitution/SKILL.md +150 -0
- package/.claude/skills/constitution/templates/constitution-template.md +228 -0
- package/.claude/skills/implement/SKILL.md +141 -0
- package/.claude/skills/plan/SKILL.md +179 -0
- package/.claude/skills/plan/templates/plan-template.md +104 -0
- package/.claude/skills/prd/SKILL.md +242 -0
- package/.claude/skills/relentless/SKILL.md +265 -0
- package/.claude/skills/specify/SKILL.md +220 -0
- package/.claude/skills/specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.claude/skills/specify/scripts/bash/common.sh +156 -0
- package/.claude/skills/specify/scripts/bash/create-new-feature.sh +305 -0
- package/.claude/skills/specify/scripts/bash/setup-plan.sh +61 -0
- package/.claude/skills/specify/scripts/bash/update-agent-context.sh +799 -0
- package/.claude/skills/specify/templates/spec-template.md +115 -0
- package/.claude/skills/tasks/SKILL.md +202 -0
- package/.claude/skills/tasks/templates/tasks-template.md +251 -0
- package/.claude/skills/taskstoissues/SKILL.md +97 -0
- package/.specify/memory/constitution.md +50 -0
- package/.specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.specify/scripts/bash/common.sh +156 -0
- package/.specify/scripts/bash/create-new-feature.sh +297 -0
- package/.specify/scripts/bash/setup-plan.sh +61 -0
- package/.specify/scripts/bash/update-agent-context.sh +799 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +40 -0
- package/.specify/templates/plan-template.md +104 -0
- package/.specify/templates/spec-template.md +115 -0
- package/.specify/templates/tasks-template.md +251 -0
- package/CHANGES_SUMMARY.md +255 -0
- package/CLAUDE.md +92 -0
- package/GEMINI_SETUP.md +256 -0
- package/LICENSE +21 -0
- package/README.md +1171 -0
- package/REFACTOR_SUMMARY.md +267 -0
- package/bin/relentless.ts +536 -0
- package/bun.lock +352 -0
- package/eslint.config.js +37 -0
- package/package.json +61 -0
- package/prd.json.example +64 -0
- package/prompt.md +108 -0
- package/ralph.sh +80 -0
- package/relentless/config.json +38 -0
- package/relentless/features/.gitkeep +0 -0
- package/relentless/features/ghsk-ideas/prd.json +229 -0
- package/relentless/features/ghsk-ideas/prd.md +191 -0
- package/relentless/features/ghsk-ideas/progress.txt +408 -0
- package/relentless/prompt.md +79 -0
- package/skills/checklist/SKILL.md +349 -0
- package/skills/clarify/SKILL.md +476 -0
- package/skills/prd/SKILL.md +242 -0
- package/skills/relentless/SKILL.md +268 -0
- package/skills/tasks/SKILL.md +577 -0
- package/src/agents/amp.ts +115 -0
- package/src/agents/claude.ts +185 -0
- package/src/agents/codex.ts +89 -0
- package/src/agents/droid.ts +90 -0
- package/src/agents/gemini.ts +109 -0
- package/src/agents/index.ts +16 -0
- package/src/agents/opencode.ts +88 -0
- package/src/agents/registry.ts +95 -0
- package/src/agents/types.ts +101 -0
- package/src/config/index.ts +8 -0
- package/src/config/loader.ts +237 -0
- package/src/config/schema.ts +115 -0
- package/src/execution/index.ts +8 -0
- package/src/execution/router.ts +49 -0
- package/src/execution/runner.ts +512 -0
- package/src/index.ts +11 -0
- package/src/init/index.ts +7 -0
- package/src/init/scaffolder.ts +377 -0
- package/src/prd/analyzer.ts +512 -0
- package/src/prd/index.ts +11 -0
- package/src/prd/issues.ts +249 -0
- package/src/prd/parser.ts +281 -0
- package/src/prd/progress.ts +198 -0
- package/src/prd/types.ts +170 -0
- package/src/tui/App.tsx +85 -0
- package/src/tui/TUIRunner.tsx +400 -0
- package/src/tui/components/AgentOutput.tsx +45 -0
- package/src/tui/components/AgentStatus.tsx +64 -0
- package/src/tui/components/CurrentStory.tsx +66 -0
- package/src/tui/components/Header.tsx +49 -0
- package/src/tui/components/ProgressBar.tsx +39 -0
- package/src/tui/components/StoryGrid.tsx +86 -0
- package/src/tui/hooks/useTUI.ts +147 -0
- package/src/tui/hooks/useTimer.ts +51 -0
- package/src/tui/index.tsx +17 -0
- package/src/tui/theme.ts +41 -0
- package/src/tui/types.ts +77 -0
- package/templates/constitution.md +228 -0
- package/templates/plan.md +273 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Artifact Consistency Analysis
|
|
3
|
+
*
|
|
4
|
+
* Checks consistency across PRD, JSON, and code to catch errors early
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PRD, UserStory } from "./types";
|
|
8
|
+
import { join, dirname } from "node:path";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
|
|
11
|
+
export interface ConsistencyIssue {
|
|
12
|
+
category: string;
|
|
13
|
+
severity: "critical" | "warning" | "info";
|
|
14
|
+
message: string;
|
|
15
|
+
recommendation?: string;
|
|
16
|
+
storyId?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AnalysisReport {
|
|
20
|
+
feature: string;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
issues: ConsistencyIssue[];
|
|
23
|
+
summary: {
|
|
24
|
+
total: number;
|
|
25
|
+
completed: number;
|
|
26
|
+
pending: number;
|
|
27
|
+
critical: number;
|
|
28
|
+
warnings: number;
|
|
29
|
+
info: number;
|
|
30
|
+
};
|
|
31
|
+
coverage: {
|
|
32
|
+
[key: string]: {
|
|
33
|
+
completed: number;
|
|
34
|
+
total: number;
|
|
35
|
+
percentage: number;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Analyze cross-artifact consistency
|
|
42
|
+
*/
|
|
43
|
+
export async function analyzeConsistency(
|
|
44
|
+
prdPath: string,
|
|
45
|
+
prd: PRD
|
|
46
|
+
): Promise<AnalysisReport> {
|
|
47
|
+
const issues: ConsistencyIssue[] = [];
|
|
48
|
+
const featureDir = dirname(prdPath);
|
|
49
|
+
const featureName = featureDir.split("/").pop() || "unknown";
|
|
50
|
+
|
|
51
|
+
// Category 1: PRD Schema Validation
|
|
52
|
+
await checkSchemaValidation(prd, issues);
|
|
53
|
+
|
|
54
|
+
// Category 2: Dependency Consistency
|
|
55
|
+
await checkDependencyConsistency(prd, issues);
|
|
56
|
+
|
|
57
|
+
// Category 3: File Existence
|
|
58
|
+
await checkFileExistence(featureDir, issues);
|
|
59
|
+
|
|
60
|
+
// Category 4: Story Completeness
|
|
61
|
+
await checkStoryCompleteness(prd, issues);
|
|
62
|
+
|
|
63
|
+
// Category 5: Progress Log Sync
|
|
64
|
+
await checkProgressLogSync(featureDir, prd, issues);
|
|
65
|
+
|
|
66
|
+
// Calculate summary
|
|
67
|
+
const total = prd.userStories.length;
|
|
68
|
+
const completed = prd.userStories.filter((s) => s.passes).length;
|
|
69
|
+
const pending = total - completed;
|
|
70
|
+
|
|
71
|
+
const critical = issues.filter((i) => i.severity === "critical").length;
|
|
72
|
+
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
73
|
+
const info = issues.filter((i) => i.severity === "info").length;
|
|
74
|
+
|
|
75
|
+
// Calculate coverage by category
|
|
76
|
+
const coverage = calculateCoverage(prd);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
feature: featureName,
|
|
80
|
+
timestamp: new Date().toISOString(),
|
|
81
|
+
issues,
|
|
82
|
+
summary: {
|
|
83
|
+
total,
|
|
84
|
+
completed,
|
|
85
|
+
pending,
|
|
86
|
+
critical,
|
|
87
|
+
warnings,
|
|
88
|
+
info,
|
|
89
|
+
},
|
|
90
|
+
coverage,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check PRD schema validation
|
|
96
|
+
*/
|
|
97
|
+
async function checkSchemaValidation(
|
|
98
|
+
prd: PRD,
|
|
99
|
+
issues: ConsistencyIssue[]
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
// Check for missing required fields
|
|
102
|
+
if (!prd.project || prd.project.trim() === "") {
|
|
103
|
+
issues.push({
|
|
104
|
+
category: "Schema Validation",
|
|
105
|
+
severity: "critical",
|
|
106
|
+
message: "PRD is missing project name",
|
|
107
|
+
recommendation: "Add a project name to the PRD",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!prd.branchName || prd.branchName.trim() === "") {
|
|
112
|
+
issues.push({
|
|
113
|
+
category: "Schema Validation",
|
|
114
|
+
severity: "critical",
|
|
115
|
+
message: "PRD is missing branch name",
|
|
116
|
+
recommendation: "Add a branch name to the PRD",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!prd.description || prd.description.trim() === "") {
|
|
121
|
+
issues.push({
|
|
122
|
+
category: "Schema Validation",
|
|
123
|
+
severity: "warning",
|
|
124
|
+
message: "PRD is missing description",
|
|
125
|
+
recommendation: "Add a description to help understand the feature",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check each user story
|
|
130
|
+
for (const story of prd.userStories) {
|
|
131
|
+
if (!story.id || story.id.trim() === "") {
|
|
132
|
+
issues.push({
|
|
133
|
+
category: "Schema Validation",
|
|
134
|
+
severity: "critical",
|
|
135
|
+
message: `Story has no ID`,
|
|
136
|
+
recommendation: "Assign a unique ID to the story (e.g., US-001)",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!story.title || story.title.trim() === "") {
|
|
141
|
+
issues.push({
|
|
142
|
+
category: "Schema Validation",
|
|
143
|
+
severity: "critical",
|
|
144
|
+
message: `Story ${story.id} has no title`,
|
|
145
|
+
storyId: story.id,
|
|
146
|
+
recommendation: "Add a descriptive title to the story",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!story.acceptanceCriteria || story.acceptanceCriteria.length === 0) {
|
|
151
|
+
issues.push({
|
|
152
|
+
category: "Schema Validation",
|
|
153
|
+
severity: "warning",
|
|
154
|
+
message: `Story ${story.id} has no acceptance criteria`,
|
|
155
|
+
storyId: story.id,
|
|
156
|
+
recommendation:
|
|
157
|
+
"Add acceptance criteria to define clear completion requirements",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check dependency consistency
|
|
165
|
+
*/
|
|
166
|
+
async function checkDependencyConsistency(
|
|
167
|
+
prd: PRD,
|
|
168
|
+
issues: ConsistencyIssue[]
|
|
169
|
+
): Promise<void> {
|
|
170
|
+
const storyMap = new Map<string, UserStory>();
|
|
171
|
+
for (const story of prd.userStories) {
|
|
172
|
+
storyMap.set(story.id, story);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check for invalid dependencies
|
|
176
|
+
for (const story of prd.userStories) {
|
|
177
|
+
if (story.dependencies) {
|
|
178
|
+
for (const depId of story.dependencies) {
|
|
179
|
+
if (!storyMap.has(depId)) {
|
|
180
|
+
issues.push({
|
|
181
|
+
category: "Dependency Consistency",
|
|
182
|
+
severity: "critical",
|
|
183
|
+
message: `Story ${story.id} depends on non-existent story ${depId}`,
|
|
184
|
+
storyId: story.id,
|
|
185
|
+
recommendation: `Remove the dependency or add story ${depId}`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check for circular dependencies
|
|
193
|
+
const visited = new Set<string>();
|
|
194
|
+
const recursionStack = new Set<string>();
|
|
195
|
+
|
|
196
|
+
function hasCycle(storyId: string, path: string[] = []): string | null {
|
|
197
|
+
if (recursionStack.has(storyId)) {
|
|
198
|
+
return [...path, storyId].join(" -> ");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (visited.has(storyId)) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
visited.add(storyId);
|
|
206
|
+
recursionStack.add(storyId);
|
|
207
|
+
|
|
208
|
+
const story = storyMap.get(storyId);
|
|
209
|
+
if (story?.dependencies) {
|
|
210
|
+
for (const depId of story.dependencies) {
|
|
211
|
+
const cycle = hasCycle(depId, [...path, storyId]);
|
|
212
|
+
if (cycle) {
|
|
213
|
+
return cycle;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
recursionStack.delete(storyId);
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const story of prd.userStories) {
|
|
223
|
+
const cycle = hasCycle(story.id);
|
|
224
|
+
if (cycle) {
|
|
225
|
+
issues.push({
|
|
226
|
+
category: "Dependency Consistency",
|
|
227
|
+
severity: "critical",
|
|
228
|
+
message: `Circular dependency detected: ${cycle}`,
|
|
229
|
+
recommendation: "Remove one of the dependencies to break the cycle",
|
|
230
|
+
});
|
|
231
|
+
break; // Only report once
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check for completed stories that depend on incomplete stories
|
|
236
|
+
for (const story of prd.userStories) {
|
|
237
|
+
if (story.passes && story.dependencies) {
|
|
238
|
+
for (const depId of story.dependencies) {
|
|
239
|
+
const depStory = storyMap.get(depId);
|
|
240
|
+
if (depStory && !depStory.passes) {
|
|
241
|
+
issues.push({
|
|
242
|
+
category: "Dependency Consistency",
|
|
243
|
+
severity: "warning",
|
|
244
|
+
message: `Story ${story.id} is marked complete but depends on incomplete story ${depId}`,
|
|
245
|
+
storyId: story.id,
|
|
246
|
+
recommendation: `Either mark ${depId} as complete or review ${story.id}'s completion status`,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Check file existence
|
|
256
|
+
*/
|
|
257
|
+
async function checkFileExistence(
|
|
258
|
+
featureDir: string,
|
|
259
|
+
issues: ConsistencyIssue[]
|
|
260
|
+
): Promise<void> {
|
|
261
|
+
const prdMdPath = join(featureDir, "prd.md");
|
|
262
|
+
const progressPath = join(featureDir, "progress.txt");
|
|
263
|
+
const planPath = join(featureDir, "plan.md");
|
|
264
|
+
const constitutionPath = join(dirname(featureDir), "..", "constitution.md");
|
|
265
|
+
|
|
266
|
+
if (!existsSync(prdMdPath)) {
|
|
267
|
+
issues.push({
|
|
268
|
+
category: "File Existence",
|
|
269
|
+
severity: "info",
|
|
270
|
+
message: "prd.md not found in feature directory",
|
|
271
|
+
recommendation:
|
|
272
|
+
"Consider keeping the source prd.md for documentation purposes",
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!existsSync(progressPath)) {
|
|
277
|
+
issues.push({
|
|
278
|
+
category: "File Existence",
|
|
279
|
+
severity: "warning",
|
|
280
|
+
message: "progress.txt not found",
|
|
281
|
+
recommendation: "Create progress.txt to track learnings across iterations",
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (existsSync(planPath)) {
|
|
286
|
+
issues.push({
|
|
287
|
+
category: "File Existence",
|
|
288
|
+
severity: "info",
|
|
289
|
+
message: "plan.md found - technical planning is in use",
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!existsSync(constitutionPath)) {
|
|
294
|
+
issues.push({
|
|
295
|
+
category: "File Existence",
|
|
296
|
+
severity: "info",
|
|
297
|
+
message: "constitution.md not found",
|
|
298
|
+
recommendation:
|
|
299
|
+
"Consider creating a constitution.md for project governance",
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Check story completeness
|
|
306
|
+
*/
|
|
307
|
+
async function checkStoryCompleteness(
|
|
308
|
+
prd: PRD,
|
|
309
|
+
issues: ConsistencyIssue[]
|
|
310
|
+
): Promise<void> {
|
|
311
|
+
// Check for stories with very few acceptance criteria
|
|
312
|
+
for (const story of prd.userStories) {
|
|
313
|
+
if (story.acceptanceCriteria.length < 2) {
|
|
314
|
+
issues.push({
|
|
315
|
+
category: "Story Completeness",
|
|
316
|
+
severity: "info",
|
|
317
|
+
message: `Story ${story.id} has only ${story.acceptanceCriteria.length} acceptance criteria`,
|
|
318
|
+
storyId: story.id,
|
|
319
|
+
recommendation:
|
|
320
|
+
"Consider adding more detailed acceptance criteria for clarity",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Check for stories with empty notes that are incomplete
|
|
326
|
+
for (const story of prd.userStories) {
|
|
327
|
+
if (!story.passes && story.notes && story.notes.trim() !== "") {
|
|
328
|
+
issues.push({
|
|
329
|
+
category: "Story Completeness",
|
|
330
|
+
severity: "info",
|
|
331
|
+
message: `Story ${story.id} is incomplete but has notes: "${story.notes}"`,
|
|
332
|
+
storyId: story.id,
|
|
333
|
+
recommendation: "Review notes to understand blockers or context",
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Check for duplicate story IDs
|
|
339
|
+
const idCounts = new Map<string, number>();
|
|
340
|
+
for (const story of prd.userStories) {
|
|
341
|
+
idCounts.set(story.id, (idCounts.get(story.id) || 0) + 1);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for (const [id, count] of idCounts.entries()) {
|
|
345
|
+
if (count > 1) {
|
|
346
|
+
issues.push({
|
|
347
|
+
category: "Story Completeness",
|
|
348
|
+
severity: "critical",
|
|
349
|
+
message: `Duplicate story ID detected: ${id} appears ${count} times`,
|
|
350
|
+
recommendation: "Ensure all story IDs are unique",
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Check progress log sync
|
|
358
|
+
*/
|
|
359
|
+
async function checkProgressLogSync(
|
|
360
|
+
featureDir: string,
|
|
361
|
+
prd: PRD,
|
|
362
|
+
issues: ConsistencyIssue[]
|
|
363
|
+
): Promise<void> {
|
|
364
|
+
const progressPath = join(featureDir, "progress.txt");
|
|
365
|
+
|
|
366
|
+
if (!existsSync(progressPath)) {
|
|
367
|
+
return; // Already reported in file existence check
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
const progressContent = await Bun.file(progressPath).text();
|
|
372
|
+
|
|
373
|
+
// Check if progress log mentions completed stories
|
|
374
|
+
const completedStories = prd.userStories.filter((s) => s.passes);
|
|
375
|
+
|
|
376
|
+
for (const story of completedStories) {
|
|
377
|
+
if (!progressContent.includes(story.id)) {
|
|
378
|
+
issues.push({
|
|
379
|
+
category: "Progress Log Sync",
|
|
380
|
+
severity: "warning",
|
|
381
|
+
message: `Story ${story.id} is marked complete but not mentioned in progress.txt`,
|
|
382
|
+
storyId: story.id,
|
|
383
|
+
recommendation:
|
|
384
|
+
"Ensure progress.txt is updated after completing each story",
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
} catch {
|
|
389
|
+
issues.push({
|
|
390
|
+
category: "Progress Log Sync",
|
|
391
|
+
severity: "warning",
|
|
392
|
+
message: "Failed to read progress.txt",
|
|
393
|
+
recommendation: "Check file permissions and format",
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Calculate coverage by category
|
|
400
|
+
*/
|
|
401
|
+
function calculateCoverage(prd: PRD): {
|
|
402
|
+
[key: string]: { completed: number; total: number; percentage: number };
|
|
403
|
+
} {
|
|
404
|
+
const coverage: {
|
|
405
|
+
[key: string]: { completed: number; total: number; percentage: number };
|
|
406
|
+
} = {};
|
|
407
|
+
|
|
408
|
+
// Group by phase if available
|
|
409
|
+
const phases = new Set<string>();
|
|
410
|
+
for (const story of prd.userStories) {
|
|
411
|
+
if (story.phase) {
|
|
412
|
+
phases.add(story.phase);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
for (const phase of phases) {
|
|
417
|
+
const storiesInPhase = prd.userStories.filter((s) => s.phase === phase);
|
|
418
|
+
const completedInPhase = storiesInPhase.filter((s) => s.passes);
|
|
419
|
+
|
|
420
|
+
coverage[phase] = {
|
|
421
|
+
completed: completedInPhase.length,
|
|
422
|
+
total: storiesInPhase.length,
|
|
423
|
+
percentage: Math.round(
|
|
424
|
+
(completedInPhase.length / storiesInPhase.length) * 100
|
|
425
|
+
),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// If no phases, just show overall
|
|
430
|
+
if (phases.size === 0) {
|
|
431
|
+
coverage["Overall"] = {
|
|
432
|
+
completed: prd.userStories.filter((s) => s.passes).length,
|
|
433
|
+
total: prd.userStories.length,
|
|
434
|
+
percentage: Math.round(
|
|
435
|
+
(prd.userStories.filter((s) => s.passes).length /
|
|
436
|
+
prd.userStories.length) *
|
|
437
|
+
100
|
|
438
|
+
),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return coverage;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Format analysis report for display
|
|
447
|
+
*/
|
|
448
|
+
export function formatReport(report: AnalysisReport): string {
|
|
449
|
+
const lines: string[] = [];
|
|
450
|
+
|
|
451
|
+
lines.push(`\n╔═══════════════════════════════════════════════════════╗`);
|
|
452
|
+
lines.push(`║ Cross-Artifact Consistency Analysis ║`);
|
|
453
|
+
lines.push(`╚═══════════════════════════════════════════════════════╝\n`);
|
|
454
|
+
|
|
455
|
+
lines.push(`Feature: ${report.feature}`);
|
|
456
|
+
lines.push(`Timestamp: ${report.timestamp}\n`);
|
|
457
|
+
|
|
458
|
+
// Summary
|
|
459
|
+
lines.push(`Summary:`);
|
|
460
|
+
lines.push(` Stories: ${report.summary.completed}/${report.summary.total} completed (${report.summary.pending} pending)`);
|
|
461
|
+
lines.push(` Issues: ${report.issues.length} total`);
|
|
462
|
+
lines.push(` Critical: ${report.summary.critical}`);
|
|
463
|
+
lines.push(` Warnings: ${report.summary.warnings}`);
|
|
464
|
+
lines.push(` Info: ${report.summary.info}\n`);
|
|
465
|
+
|
|
466
|
+
// Coverage
|
|
467
|
+
if (Object.keys(report.coverage).length > 0) {
|
|
468
|
+
lines.push(`Coverage by Phase:`);
|
|
469
|
+
for (const [phase, stats] of Object.entries(report.coverage)) {
|
|
470
|
+
const bar = "█".repeat(Math.floor(stats.percentage / 5));
|
|
471
|
+
const emptyBar = "░".repeat(20 - Math.floor(stats.percentage / 5));
|
|
472
|
+
lines.push(
|
|
473
|
+
` ${phase.padEnd(20)} ${bar}${emptyBar} ${stats.percentage}% (${stats.completed}/${stats.total})`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
lines.push("");
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Issues by category
|
|
480
|
+
if (report.issues.length > 0) {
|
|
481
|
+
const categories = new Set(report.issues.map((i) => i.category));
|
|
482
|
+
|
|
483
|
+
lines.push(`Issues by Category:\n`);
|
|
484
|
+
|
|
485
|
+
for (const category of categories) {
|
|
486
|
+
const categoryIssues = report.issues.filter((i) => i.category === category);
|
|
487
|
+
|
|
488
|
+
lines.push(`${category}:`);
|
|
489
|
+
|
|
490
|
+
for (const issue of categoryIssues) {
|
|
491
|
+
const severityIcon =
|
|
492
|
+
issue.severity === "critical"
|
|
493
|
+
? "🔴"
|
|
494
|
+
: issue.severity === "warning"
|
|
495
|
+
? "🟡"
|
|
496
|
+
: "🔵";
|
|
497
|
+
|
|
498
|
+
lines.push(` ${severityIcon} ${issue.message}`);
|
|
499
|
+
|
|
500
|
+
if (issue.recommendation) {
|
|
501
|
+
lines.push(` → ${issue.recommendation}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
lines.push("");
|
|
506
|
+
}
|
|
507
|
+
} else {
|
|
508
|
+
lines.push(`✅ No issues found!\n`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return lines.join("\n");
|
|
512
|
+
}
|