specweave 0.18.0 → 0.18.1
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/dist/locales/de/.gitkeep +0 -0
- package/dist/locales/de/cli.json +108 -0
- package/dist/locales/en/cli.json +287 -0
- package/dist/locales/en/errors.json +7 -0
- package/dist/locales/en/templates.json +6 -0
- package/dist/locales/es/.gitkeep +0 -0
- package/dist/locales/es/cli.json +41 -0
- package/dist/locales/fr/.gitkeep +0 -0
- package/dist/locales/fr/cli.json +108 -0
- package/dist/locales/ja/.gitkeep +0 -0
- package/dist/locales/ja/cli.json +108 -0
- package/dist/locales/ko/.gitkeep +0 -0
- package/dist/locales/ko/cli.json +108 -0
- package/dist/locales/pt/.gitkeep +0 -0
- package/dist/locales/pt/cli.json +108 -0
- package/dist/locales/ru/.gitkeep +0 -0
- package/dist/locales/ru/cli.json +269 -0
- package/dist/locales/zh/.gitkeep +0 -0
- package/dist/locales/zh/cli.json +108 -0
- package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts +25 -0
- package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js +191 -0
- package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts +63 -0
- package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/epic-content-builder.js +216 -0
- package/dist/plugins/specweave-github/lib/epic-content-builder.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +2 -2
- package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-epic-sync.js +19 -4
- package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts +26 -0
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js +195 -0
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js.map +1 -0
- package/dist/spec-parser.js +629 -0
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +107 -3
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/core/sync/enhanced-content-builder.d.ts +32 -54
- package/dist/src/core/sync/enhanced-content-builder.d.ts.map +1 -1
- package/dist/src/core/sync/enhanced-content-builder.js +141 -138
- package/dist/src/core/sync/enhanced-content-builder.js.map +1 -1
- package/dist/src/core/sync/spec-content-sync.d.ts +88 -0
- package/dist/src/core/sync/spec-content-sync.d.ts.map +1 -0
- package/dist/src/core/sync/spec-content-sync.js +5 -0
- package/dist/src/core/sync/spec-content-sync.js.map +1 -0
- package/dist/src/core/sync/types.d.ts +52 -0
- package/dist/src/core/sync/types.d.ts.map +1 -0
- package/dist/src/core/sync/types.js +5 -0
- package/dist/src/core/sync/types.js.map +1 -0
- package/dist/src/core/types/config.d.ts +31 -0
- package/dist/src/core/types/config.d.ts.map +1 -1
- package/dist/src/core/types/config.js +9 -0
- package/dist/src/core/types/config.js.map +1 -1
- package/dist/src/core/types/increment-metadata.d.ts +4 -0
- package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
- package/dist/src/core/types/increment-metadata.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +1 -1
- package/plugins/specweave/agents/pm/AGENT.md +159 -12
- package/plugins/specweave/commands/specweave.md +70 -405
- package/plugins/specweave/hooks/post-increment-planning.sh +26 -2
- package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
- package/plugins/specweave-github/hooks/post-task-completion.sh +32 -0
- package/plugins/specweave-github/lib/epic-content-builder.js +227 -0
- package/plugins/specweave-github/lib/epic-content-builder.ts +317 -0
- package/plugins/specweave-github/lib/github-epic-sync.js +23 -24
- package/plugins/specweave-github/lib/github-epic-sync.ts +29 -4
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { AdoClientV2 } from "./ado-client-v2.js";
|
|
2
|
+
import { EnhancedContentBuilder } from "../../../src/core/sync/enhanced-content-builder.js";
|
|
3
|
+
import { SpecIncrementMapper } from "../../../src/core/sync/spec-increment-mapper.js";
|
|
4
|
+
import { parseSpecContent } from "../../../src/core/spec-content-sync.js";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import fs from "fs/promises";
|
|
7
|
+
async function syncSpecToAdoWithEnhancedContent(options) {
|
|
8
|
+
const { specPath, organization, project, dryRun = false, verbose = false } = options;
|
|
9
|
+
try {
|
|
10
|
+
const baseSpec = await parseSpecContent(specPath);
|
|
11
|
+
if (!baseSpec) {
|
|
12
|
+
return {
|
|
13
|
+
success: false,
|
|
14
|
+
action: "error",
|
|
15
|
+
error: "Failed to parse spec content"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (verbose) {
|
|
19
|
+
console.log(`\u{1F4C4} Parsed spec: ${baseSpec.identifier.compact}`);
|
|
20
|
+
}
|
|
21
|
+
const specId = baseSpec.identifier.full || baseSpec.identifier.compact;
|
|
22
|
+
const rootDir = await findSpecWeaveRoot(specPath);
|
|
23
|
+
const mapper = new SpecIncrementMapper(rootDir);
|
|
24
|
+
const mapping = await mapper.mapSpecToIncrements(specId);
|
|
25
|
+
if (verbose) {
|
|
26
|
+
console.log(`\u{1F517} Found ${mapping.increments.length} related increments`);
|
|
27
|
+
}
|
|
28
|
+
const taskMapping = buildTaskMapping(mapping.increments, organization, project);
|
|
29
|
+
const architectureDocs = await findArchitectureDocs(rootDir, specId);
|
|
30
|
+
const enhancedSpec = {
|
|
31
|
+
...baseSpec,
|
|
32
|
+
summary: baseSpec.description,
|
|
33
|
+
taskMapping,
|
|
34
|
+
architectureDocs
|
|
35
|
+
};
|
|
36
|
+
const builder = new EnhancedContentBuilder();
|
|
37
|
+
const description = builder.buildExternalDescription(enhancedSpec);
|
|
38
|
+
if (verbose) {
|
|
39
|
+
console.log(`\u{1F4DD} Generated description: ${description.length} characters`);
|
|
40
|
+
}
|
|
41
|
+
if (dryRun) {
|
|
42
|
+
console.log("\u{1F50D} DRY RUN - Would create/update feature with:");
|
|
43
|
+
console.log(` Title: ${baseSpec.title}`);
|
|
44
|
+
console.log(` Description length: ${description.length}`);
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
action: "no-change",
|
|
48
|
+
tasksLinked: taskMapping?.tasks.length || 0
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (!organization || !project) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
action: "error",
|
|
55
|
+
error: "Azure DevOps organization/project not specified"
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const profile = {
|
|
59
|
+
provider: "ado",
|
|
60
|
+
displayName: `${organization}/${project}`,
|
|
61
|
+
config: {
|
|
62
|
+
organization,
|
|
63
|
+
project
|
|
64
|
+
},
|
|
65
|
+
timeRange: { default: "1M", max: "6M" }
|
|
66
|
+
};
|
|
67
|
+
const pat = process.env.AZURE_DEVOPS_PAT || "";
|
|
68
|
+
const client = new AdoClientV2(profile, pat);
|
|
69
|
+
const existingFeature = await findExistingFeature(client, baseSpec.identifier.compact);
|
|
70
|
+
let result;
|
|
71
|
+
if (existingFeature) {
|
|
72
|
+
await client.updateWorkItem(existingFeature.id, {
|
|
73
|
+
title: `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
|
|
74
|
+
description
|
|
75
|
+
});
|
|
76
|
+
result = {
|
|
77
|
+
success: true,
|
|
78
|
+
action: "updated",
|
|
79
|
+
featureId: existingFeature.id,
|
|
80
|
+
featureUrl: `https://dev.azure.com/${organization}/${project}/_workitems/edit/${existingFeature.id}`,
|
|
81
|
+
tasksLinked: taskMapping?.tasks.length || 0
|
|
82
|
+
};
|
|
83
|
+
} else {
|
|
84
|
+
const feature = await client.createEpic({
|
|
85
|
+
title: `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
|
|
86
|
+
description,
|
|
87
|
+
tags: ["spec", "external-tool-sync"]
|
|
88
|
+
});
|
|
89
|
+
result = {
|
|
90
|
+
success: true,
|
|
91
|
+
action: "created",
|
|
92
|
+
featureId: feature.id,
|
|
93
|
+
featureUrl: `https://dev.azure.com/${organization}/${project}/_workitems/edit/${feature.id}`,
|
|
94
|
+
tasksLinked: taskMapping?.tasks.length || 0
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (verbose) {
|
|
98
|
+
console.log(`\u2705 ${result.action === "created" ? "Created" : "Updated"} feature #${result.featureId}`);
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return {
|
|
103
|
+
success: false,
|
|
104
|
+
action: "error",
|
|
105
|
+
error: error.message
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function findSpecWeaveRoot(specPath) {
|
|
110
|
+
let currentDir = path.dirname(specPath);
|
|
111
|
+
while (true) {
|
|
112
|
+
const specweaveDir = path.join(currentDir, ".specweave");
|
|
113
|
+
try {
|
|
114
|
+
await fs.access(specweaveDir);
|
|
115
|
+
return currentDir;
|
|
116
|
+
} catch {
|
|
117
|
+
const parentDir = path.dirname(currentDir);
|
|
118
|
+
if (parentDir === currentDir) {
|
|
119
|
+
throw new Error(".specweave directory not found");
|
|
120
|
+
}
|
|
121
|
+
currentDir = parentDir;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function buildTaskMapping(increments, organization, project) {
|
|
126
|
+
if (increments.length === 0) return void 0;
|
|
127
|
+
const firstIncrement = increments[0];
|
|
128
|
+
const tasks = firstIncrement.tasks.map((task) => ({
|
|
129
|
+
id: task.id,
|
|
130
|
+
title: task.title,
|
|
131
|
+
userStories: task.userStories
|
|
132
|
+
}));
|
|
133
|
+
return {
|
|
134
|
+
incrementId: firstIncrement.id,
|
|
135
|
+
tasks,
|
|
136
|
+
tasksUrl: `https://dev.azure.com/${organization}/${project}/_git/repo?path=/.specweave/increments/${firstIncrement.id}/tasks.md`
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async function findArchitectureDocs(rootDir, specId) {
|
|
140
|
+
const docs = [];
|
|
141
|
+
const archDir = path.join(rootDir, ".specweave/docs/internal/architecture");
|
|
142
|
+
try {
|
|
143
|
+
const adrDir = path.join(archDir, "adr");
|
|
144
|
+
try {
|
|
145
|
+
const adrs = await fs.readdir(adrDir);
|
|
146
|
+
const relatedAdrs = adrs.filter((file) => file.includes(specId.replace("spec-", "")));
|
|
147
|
+
for (const adr of relatedAdrs) {
|
|
148
|
+
docs.push({
|
|
149
|
+
type: "adr",
|
|
150
|
+
path: path.join(adrDir, adr),
|
|
151
|
+
title: adr.replace(".md", "").replace(/-/g, " ")
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
return docs;
|
|
159
|
+
}
|
|
160
|
+
async function findExistingFeature(client, specId) {
|
|
161
|
+
try {
|
|
162
|
+
const features = await client.queryWorkItems(`[System.Title] Contains '[${specId}]' AND [System.WorkItemType] = 'Feature'`);
|
|
163
|
+
return features[0] || null;
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export {
|
|
169
|
+
syncSpecToAdoWithEnhancedContent
|
|
170
|
+
};
|
|
@@ -199,6 +199,38 @@ else
|
|
|
199
199
|
fi
|
|
200
200
|
fi
|
|
201
201
|
|
|
202
|
+
# ============================================================================
|
|
203
|
+
# EPIC GITHUB ISSUE SYNC (Update Epic issue with fresh task progress)
|
|
204
|
+
# ============================================================================
|
|
205
|
+
|
|
206
|
+
echo "[$(date)] [GitHub] 🔄 Checking for Epic GitHub issue update..." >> "$DEBUG_LOG" 2>/dev/null || true
|
|
207
|
+
|
|
208
|
+
# Find active increment ID
|
|
209
|
+
ACTIVE_INCREMENT=$(ls -t .specweave/increments/ | grep -v '^\.' | while read inc; do
|
|
210
|
+
if [ -f ".specweave/increments/$inc/metadata.json" ]; then
|
|
211
|
+
STATUS=$(grep -o '"status"[[:space:]]*:[[:space:]]*"[^"]*"' ".specweave/increments/$inc/metadata.json" 2>/dev/null | sed 's/.*"\([^"]*\)".*/\1/' || true)
|
|
212
|
+
if [ "$STATUS" = "active" ]; then
|
|
213
|
+
echo "$inc"
|
|
214
|
+
break
|
|
215
|
+
fi
|
|
216
|
+
fi
|
|
217
|
+
done | head -1)
|
|
218
|
+
|
|
219
|
+
if [ -n "$ACTIVE_INCREMENT" ]; then
|
|
220
|
+
echo "[$(date)] [GitHub] 🎯 Active increment: $ACTIVE_INCREMENT" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
221
|
+
|
|
222
|
+
# Run Epic sync script (silently, errors logged to debug log)
|
|
223
|
+
if [ -f "$PROJECT_ROOT/scripts/update-epic-github-issue.sh" ]; then
|
|
224
|
+
echo "[$(date)] [GitHub] 🚀 Updating Epic GitHub issue..." >> "$DEBUG_LOG" 2>/dev/null || true
|
|
225
|
+
"$PROJECT_ROOT/scripts/update-epic-github-issue.sh" "$ACTIVE_INCREMENT" >> "$DEBUG_LOG" 2>&1 || true
|
|
226
|
+
echo "[$(date)] [GitHub] ✅ Epic sync complete (see logs for details)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
227
|
+
else
|
|
228
|
+
echo "[$(date)] [GitHub] ⚠️ Epic sync script not found, skipping" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
229
|
+
fi
|
|
230
|
+
else
|
|
231
|
+
echo "[$(date)] [GitHub] ℹ️ No active increment found, skipping Epic sync" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
232
|
+
fi
|
|
233
|
+
|
|
202
234
|
# ============================================================================
|
|
203
235
|
# OUTPUT TO CLAUDE
|
|
204
236
|
# ============================================================================
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { readdir, readFile } from "fs/promises";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as yaml from "yaml";
|
|
5
|
+
class EpicContentBuilder {
|
|
6
|
+
constructor(epicFolder, projectRoot) {
|
|
7
|
+
this.epicFolder = epicFolder;
|
|
8
|
+
this.projectRoot = projectRoot;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Build hierarchical GitHub issue body
|
|
12
|
+
*
|
|
13
|
+
* Format:
|
|
14
|
+
* - Epic overview
|
|
15
|
+
* - User Stories section (checkable, with status + increment)
|
|
16
|
+
* - Tasks section (grouped by User Story)
|
|
17
|
+
*/
|
|
18
|
+
async buildIssueBody() {
|
|
19
|
+
const epicData = await this.readEpicMetadata();
|
|
20
|
+
const userStories = await this.readUserStories();
|
|
21
|
+
const overview = this.buildOverviewSection(epicData);
|
|
22
|
+
const userStoriesSection = this.buildUserStoriesSection(userStories);
|
|
23
|
+
const tasksSection = this.buildTasksSection(userStories);
|
|
24
|
+
return `${overview}
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
${userStoriesSection}
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
${tasksSection}
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
\u{1F916} Auto-created by SpecWeave Epic Sync`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Read Epic FEATURE.md frontmatter
|
|
40
|
+
*/
|
|
41
|
+
async readEpicMetadata() {
|
|
42
|
+
const featurePath = path.join(this.epicFolder, "FEATURE.md");
|
|
43
|
+
const content = await readFile(featurePath, "utf-8");
|
|
44
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
45
|
+
if (!match) {
|
|
46
|
+
throw new Error("FEATURE.md missing YAML frontmatter");
|
|
47
|
+
}
|
|
48
|
+
return yaml.parse(match[1]);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Read all user stories from us-*.md files
|
|
52
|
+
*/
|
|
53
|
+
async readUserStories() {
|
|
54
|
+
const files = await readdir(this.epicFolder);
|
|
55
|
+
const usFiles = files.filter((f) => f.startsWith("us-") && f.endsWith(".md"));
|
|
56
|
+
const userStories = [];
|
|
57
|
+
for (const file of usFiles.sort()) {
|
|
58
|
+
const filePath = path.join(this.epicFolder, file);
|
|
59
|
+
const content = await readFile(filePath, "utf-8");
|
|
60
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
61
|
+
if (!match) {
|
|
62
|
+
console.warn(` \u26A0\uFE0F ${file} missing frontmatter, skipping`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const frontmatter = yaml.parse(match[1]);
|
|
66
|
+
const bodyContent = content.slice(match[0].length).trim();
|
|
67
|
+
const incrementMatch = bodyContent.match(/\*\*Increment\*\*:\s*\[([^\]]+)\]/);
|
|
68
|
+
const increment = incrementMatch ? incrementMatch[1] : null;
|
|
69
|
+
const tasks = await this.extractTasksForUserStory(
|
|
70
|
+
frontmatter.id,
|
|
71
|
+
increment,
|
|
72
|
+
bodyContent
|
|
73
|
+
);
|
|
74
|
+
userStories.push({
|
|
75
|
+
id: frontmatter.id,
|
|
76
|
+
title: frontmatter.title,
|
|
77
|
+
status: this.normalizeStatus(frontmatter.status),
|
|
78
|
+
increment,
|
|
79
|
+
tasks
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return userStories;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Extract tasks for a user story from its Implementation section
|
|
86
|
+
*/
|
|
87
|
+
async extractTasksForUserStory(userStoryId, incrementId, content) {
|
|
88
|
+
if (!incrementId) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const incrementFolder = path.join(
|
|
92
|
+
this.projectRoot,
|
|
93
|
+
".specweave",
|
|
94
|
+
"increments",
|
|
95
|
+
incrementId
|
|
96
|
+
);
|
|
97
|
+
if (!existsSync(incrementFolder)) {
|
|
98
|
+
console.warn(` \u26A0\uFE0F Increment folder not found: ${incrementId}`);
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
const tasksPath = path.join(incrementFolder, "tasks.md");
|
|
102
|
+
if (!existsSync(tasksPath)) {
|
|
103
|
+
console.warn(` \u26A0\uFE0F tasks.md not found in ${incrementId}`);
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
const tasksContent = await readFile(tasksPath, "utf-8");
|
|
107
|
+
const taskLinkPattern = /- \[([T-\d]+):\s*([^\]]+)\]/g;
|
|
108
|
+
const taskLinks = [];
|
|
109
|
+
let match;
|
|
110
|
+
while ((match = taskLinkPattern.exec(content)) !== null) {
|
|
111
|
+
taskLinks.push({
|
|
112
|
+
id: match[1],
|
|
113
|
+
// e.g., "T-001"
|
|
114
|
+
title: match[2].trim()
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const tasks = [];
|
|
118
|
+
for (const taskLink of taskLinks) {
|
|
119
|
+
const taskPattern = new RegExp(
|
|
120
|
+
`###\\s+${taskLink.id}:\\s*([^\\n]+)[\\s\\S]*?\\*\\*Status\\*\\*:\\s*\\[([x\\s])\\]`,
|
|
121
|
+
"i"
|
|
122
|
+
);
|
|
123
|
+
const taskMatch = tasksContent.match(taskPattern);
|
|
124
|
+
const isCompleted = taskMatch ? taskMatch[2] === "x" : false;
|
|
125
|
+
tasks.push({
|
|
126
|
+
id: taskLink.id,
|
|
127
|
+
title: taskLink.title,
|
|
128
|
+
status: isCompleted,
|
|
129
|
+
userStoryId
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return tasks;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Build overview section
|
|
136
|
+
*/
|
|
137
|
+
buildOverviewSection(epic) {
|
|
138
|
+
return `# [${epic.id}] ${epic.title}
|
|
139
|
+
|
|
140
|
+
**Status**: ${epic.status}
|
|
141
|
+
**Created**: ${epic.created}
|
|
142
|
+
**Last Updated**: ${epic.last_updated}`;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Build User Stories section
|
|
146
|
+
*/
|
|
147
|
+
buildUserStoriesSection(userStories) {
|
|
148
|
+
const total = userStories.length;
|
|
149
|
+
const completed = userStories.filter((us) => us.status === "complete").length;
|
|
150
|
+
const percentage = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
151
|
+
let section = `## User Stories
|
|
152
|
+
|
|
153
|
+
Progress: ${completed}/${total} user stories complete (${percentage}%)
|
|
154
|
+
|
|
155
|
+
`;
|
|
156
|
+
for (const us of userStories) {
|
|
157
|
+
const checkbox = us.status === "complete" ? "[x]" : "[ ]";
|
|
158
|
+
const statusEmoji = this.getStatusEmoji(us.status);
|
|
159
|
+
const incrementLink = us.increment ? `[${us.increment}](../../increments/${us.increment}/)` : "TBD";
|
|
160
|
+
section += `- ${checkbox} **${us.id}: ${us.title}** (${statusEmoji} ${us.status} | Increment: ${incrementLink})
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
return section;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Build Tasks section (grouped by User Story)
|
|
167
|
+
*/
|
|
168
|
+
buildTasksSection(userStories) {
|
|
169
|
+
const totalTasks = userStories.reduce((sum, us) => sum + us.tasks.length, 0);
|
|
170
|
+
const completedTasks = userStories.reduce(
|
|
171
|
+
(sum, us) => sum + us.tasks.filter((t) => t.status).length,
|
|
172
|
+
0
|
|
173
|
+
);
|
|
174
|
+
const percentage = totalTasks > 0 ? Math.round(completedTasks / totalTasks * 100) : 0;
|
|
175
|
+
let section = `## Tasks by User Story
|
|
176
|
+
|
|
177
|
+
Progress: ${completedTasks}/${totalTasks} tasks complete (${percentage}%)
|
|
178
|
+
|
|
179
|
+
`;
|
|
180
|
+
for (const us of userStories) {
|
|
181
|
+
if (us.tasks.length === 0) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const incrementLink = us.increment ? `[${us.increment}](../../increments/${us.increment}/tasks.md)` : "TBD";
|
|
185
|
+
section += `### ${us.id}: ${us.title} (Increment: ${incrementLink})
|
|
186
|
+
|
|
187
|
+
`;
|
|
188
|
+
for (const task of us.tasks) {
|
|
189
|
+
const checkbox = task.status ? "[x]" : "[ ]";
|
|
190
|
+
section += `- ${checkbox} ${task.id}: ${task.title}
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
193
|
+
section += "\n";
|
|
194
|
+
}
|
|
195
|
+
return section;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Normalize status values
|
|
199
|
+
*/
|
|
200
|
+
normalizeStatus(status) {
|
|
201
|
+
const normalized = status.toLowerCase();
|
|
202
|
+
if (normalized === "complete" || normalized === "completed") return "complete";
|
|
203
|
+
if (normalized === "active" || normalized === "in-progress") return "active";
|
|
204
|
+
if (normalized === "planning") return "planning";
|
|
205
|
+
return "not-started";
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Get status emoji
|
|
209
|
+
*/
|
|
210
|
+
getStatusEmoji(status) {
|
|
211
|
+
switch (status) {
|
|
212
|
+
case "complete":
|
|
213
|
+
return "\u2705";
|
|
214
|
+
case "active":
|
|
215
|
+
return "\u{1F6A7}";
|
|
216
|
+
case "planning":
|
|
217
|
+
return "\u{1F4CB}";
|
|
218
|
+
case "not-started":
|
|
219
|
+
return "\u23F3";
|
|
220
|
+
default:
|
|
221
|
+
return "\u2753";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
export {
|
|
226
|
+
EpicContentBuilder
|
|
227
|
+
};
|