specweave 0.13.6 → 0.14.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.
Files changed (35) hide show
  1. package/CLAUDE.md +189 -0
  2. package/dist/cli/commands/init.js +1 -1
  3. package/dist/cli/commands/init.js.map +1 -1
  4. package/dist/cli/commands/status-line.d.ts +14 -0
  5. package/dist/cli/commands/status-line.d.ts.map +1 -0
  6. package/dist/cli/commands/status-line.js +75 -0
  7. package/dist/cli/commands/status-line.js.map +1 -0
  8. package/dist/core/status-line/status-line-manager.d.ts +62 -0
  9. package/dist/core/status-line/status-line-manager.d.ts.map +1 -0
  10. package/dist/core/status-line/status-line-manager.js +169 -0
  11. package/dist/core/status-line/status-line-manager.js.map +1 -0
  12. package/dist/core/status-line/types.d.ts +50 -0
  13. package/dist/core/status-line/types.d.ts.map +1 -0
  14. package/dist/core/status-line/types.js +17 -0
  15. package/dist/core/status-line/types.js.map +1 -0
  16. package/dist/utils/project-mapper.d.ts +74 -0
  17. package/dist/utils/project-mapper.d.ts.map +1 -0
  18. package/dist/utils/project-mapper.js +273 -0
  19. package/dist/utils/project-mapper.js.map +1 -0
  20. package/dist/utils/spec-splitter.d.ts +68 -0
  21. package/dist/utils/spec-splitter.d.ts.map +1 -0
  22. package/dist/utils/spec-splitter.js +314 -0
  23. package/dist/utils/spec-splitter.js.map +1 -0
  24. package/package.json +1 -1
  25. package/plugins/specweave/hooks/lib/update-status-line.sh +138 -0
  26. package/plugins/specweave/hooks/post-task-completion.sh +10 -0
  27. package/plugins/specweave/skills/multi-project-spec-mapper/SKILL.md +399 -0
  28. package/plugins/specweave-ado/lib/ado-multi-project-sync.js +453 -0
  29. package/plugins/specweave-ado/lib/ado-multi-project-sync.ts +633 -0
  30. package/plugins/specweave-docs/skills/docusaurus/SKILL.md +17 -3
  31. package/plugins/specweave-docs-preview/commands/preview.md +29 -4
  32. package/plugins/specweave-github/lib/github-multi-project-sync.js +340 -0
  33. package/plugins/specweave-github/lib/github-multi-project-sync.ts +461 -0
  34. package/plugins/specweave-jira/lib/jira-multi-project-sync.js +244 -0
  35. package/plugins/specweave-jira/lib/jira-multi-project-sync.ts +358 -0
@@ -0,0 +1,453 @@
1
+ import axios from "axios";
2
+ import {
3
+ getPrimaryProject,
4
+ suggestJiraItemType,
5
+ mapUserStoryToProjects
6
+ } from "../../../src/utils/project-mapper.js";
7
+ import { parseSpecFile } from "../../../src/utils/spec-splitter.js";
8
+ class AdoMultiProjectSync {
9
+ constructor(config) {
10
+ this.config = config;
11
+ this.client = axios.create({
12
+ baseURL: `https://dev.azure.com/${config.organization}`,
13
+ headers: {
14
+ "Content-Type": "application/json-patch+json",
15
+ "Authorization": `Basic ${Buffer.from(":" + config.pat).toString("base64")}`
16
+ }
17
+ });
18
+ }
19
+ /**
20
+ * Sync spec to ADO projects with intelligent mapping
21
+ *
22
+ * @param specPath Path to spec file
23
+ * @returns Array of sync results
24
+ */
25
+ async syncSpec(specPath) {
26
+ const results = [];
27
+ const parsedSpec = await parseSpecFile(specPath);
28
+ const isAreaPathBased = !!this.config.project && !!this.config.areaPaths;
29
+ if (isAreaPathBased) {
30
+ results.push(...await this.syncAreaPathBased(parsedSpec));
31
+ } else if (this.config.projects) {
32
+ results.push(...await this.syncMultipleProjects(parsedSpec));
33
+ } else {
34
+ throw new Error("Invalid config: Must specify projects[] or project+areaPaths[]");
35
+ }
36
+ return results;
37
+ }
38
+ /**
39
+ * Pattern 1: Sync to multiple ADO projects (simple)
40
+ *
41
+ * Each team → separate ADO project
42
+ * - FE user stories → FE-Project
43
+ * - BE user stories → BE-Project
44
+ * - MOBILE user stories → MOBILE-Project
45
+ */
46
+ async syncMultipleProjects(parsedSpec) {
47
+ const results = [];
48
+ const epicsByProject = /* @__PURE__ */ new Map();
49
+ if (this.config.autoCreateEpics !== false) {
50
+ for (const projectName of this.config.projects) {
51
+ const epicResult = await this.createEpicForProject(parsedSpec, projectName);
52
+ epicsByProject.set(projectName, epicResult.workItemId);
53
+ results.push(epicResult);
54
+ }
55
+ }
56
+ const projectStories = /* @__PURE__ */ new Map();
57
+ for (const userStory of parsedSpec.userStories) {
58
+ if (this.config.intelligentMapping !== false) {
59
+ const mappings = mapUserStoryToProjects(userStory);
60
+ if (mappings.length > 0 && mappings[0].confidence >= 0.3) {
61
+ const primary = mappings[0];
62
+ const projectName = this.findProjectForId(primary.projectId);
63
+ if (projectName) {
64
+ const existing = projectStories.get(projectName) || [];
65
+ existing.push({ story: userStory, confidence: primary.confidence });
66
+ projectStories.set(projectName, existing);
67
+ }
68
+ } else {
69
+ console.warn(`\u26A0\uFE0F Low confidence for ${userStory.id} (${(mappings[0]?.confidence || 0) * 100}%) - assigning to ${this.config.projects[0]}`);
70
+ const fallback = this.config.projects[0];
71
+ const existing = projectStories.get(fallback) || [];
72
+ existing.push({ story: userStory, confidence: mappings[0]?.confidence || 0 });
73
+ projectStories.set(fallback, existing);
74
+ }
75
+ }
76
+ }
77
+ for (const [projectName, stories] of projectStories.entries()) {
78
+ const epicId = epicsByProject.get(projectName);
79
+ for (const { story, confidence } of stories) {
80
+ const result = await this.createWorkItemForUserStory(projectName, story, epicId, confidence);
81
+ results.push(result);
82
+ }
83
+ }
84
+ return results;
85
+ }
86
+ /**
87
+ * Pattern 2: Sync to single project with area paths (advanced)
88
+ *
89
+ * - Single ADO project with area paths for teams
90
+ * - Epic-level: Root area path
91
+ * - Story-level: Team-specific area paths
92
+ *
93
+ * Example:
94
+ * ADO Project: Shared-Project
95
+ * Epic: User Authentication (Root area path)
96
+ * User Story: Login UI (Area Path: Shared-Project\FE)
97
+ * User Story: Auth API (Area Path: Shared-Project\BE)
98
+ * User Story: Mobile Auth (Area Path: Shared-Project\MOBILE)
99
+ */
100
+ async syncAreaPathBased(parsedSpec) {
101
+ const results = [];
102
+ if (!this.config.project || !this.config.areaPaths) {
103
+ throw new Error("Area path mode requires project and areaPaths");
104
+ }
105
+ const epicResult = await this.createEpicInRootArea(parsedSpec);
106
+ results.push(epicResult);
107
+ const areaPathStories = /* @__PURE__ */ new Map();
108
+ for (const userStory of parsedSpec.userStories) {
109
+ const primaryProject = getPrimaryProject(userStory);
110
+ if (primaryProject) {
111
+ const areaPath = this.findAreaPathForProjectId(primaryProject.projectId);
112
+ if (areaPath) {
113
+ const existing = areaPathStories.get(areaPath) || [];
114
+ existing.push(userStory);
115
+ areaPathStories.set(areaPath, existing);
116
+ }
117
+ }
118
+ }
119
+ for (const [areaPath, stories] of areaPathStories.entries()) {
120
+ for (const story of stories) {
121
+ const result = await this.createWorkItemInAreaPath(areaPath, story, epicResult.workItemId);
122
+ results.push(result);
123
+ }
124
+ }
125
+ return results;
126
+ }
127
+ /**
128
+ * Create epic for project (Pattern 1: Multiple Projects)
129
+ */
130
+ async createEpicForProject(parsedSpec, projectName) {
131
+ const title = `${parsedSpec.metadata.title} - ${projectName}`;
132
+ const description = `<h2>${projectName} Implementation</h2>
133
+
134
+ <strong>Status</strong>: ${parsedSpec.metadata.status}<br/>
135
+ <strong>Priority</strong>: ${parsedSpec.metadata.priority}<br/>
136
+ <strong>Estimated Effort</strong>: ${parsedSpec.metadata.estimatedEffort || parsedSpec.metadata.estimated_effort}
137
+
138
+ <h3>Executive Summary</h3>
139
+
140
+ ${parsedSpec.executiveSummary}
141
+
142
+ <h3>Scope (${projectName})</h3>
143
+
144
+ This epic covers all ${projectName}-related user stories for "${parsedSpec.metadata.title}".
145
+
146
+ User stories will be added as child work items.
147
+
148
+ ---
149
+
150
+ \u{1F916} Auto-generated by SpecWeave
151
+ `;
152
+ const workItem = await this.createWorkItem(projectName, this.config.workItemTypes?.epic || "Epic", {
153
+ "System.Title": title,
154
+ "System.Description": description,
155
+ "System.State": "New"
156
+ });
157
+ return {
158
+ project: projectName,
159
+ workItemId: workItem.id,
160
+ workItemType: "Epic",
161
+ title,
162
+ url: workItem.url,
163
+ action: "created"
164
+ };
165
+ }
166
+ /**
167
+ * Create epic in root area path (Pattern 2: Area Paths)
168
+ */
169
+ async createEpicInRootArea(parsedSpec) {
170
+ const title = parsedSpec.metadata.title;
171
+ const description = `<h2>${parsedSpec.metadata.title}</h2>
172
+
173
+ <strong>Status</strong>: ${parsedSpec.metadata.status}<br/>
174
+ <strong>Priority</strong>: ${parsedSpec.metadata.priority}<br/>
175
+ <strong>Estimated Effort</strong>: ${parsedSpec.metadata.estimatedEffort || parsedSpec.metadata.estimated_effort}
176
+
177
+ <h3>Executive Summary</h3>
178
+
179
+ ${parsedSpec.executiveSummary}
180
+
181
+ <h3>User Stories (${parsedSpec.userStories.length} total)</h3>
182
+
183
+ <ul>
184
+ ${parsedSpec.userStories.map((s, i) => `<li>${i + 1}. ${s.id}: ${s.title}</li>`).join("\n")}
185
+ </ul>
186
+
187
+ ---
188
+
189
+ \u{1F916} Auto-generated by SpecWeave
190
+ `;
191
+ const workItem = await this.createWorkItem(this.config.project, this.config.workItemTypes?.epic || "Epic", {
192
+ "System.Title": title,
193
+ "System.Description": description,
194
+ "System.AreaPath": this.config.project,
195
+ // Root area path
196
+ "System.State": "New"
197
+ });
198
+ return {
199
+ project: this.config.project,
200
+ workItemId: workItem.id,
201
+ workItemType: "Epic",
202
+ title,
203
+ url: workItem.url,
204
+ action: "created"
205
+ };
206
+ }
207
+ /**
208
+ * Create work item for user story (Pattern 1: Multiple Projects)
209
+ */
210
+ async createWorkItemForUserStory(projectName, userStory, epicId, confidence) {
211
+ const title = `${userStory.id}: ${userStory.title}`;
212
+ const itemType = this.mapItemTypeToAdo(suggestJiraItemType(userStory));
213
+ const description = `<h3>${userStory.title}</h3>
214
+
215
+ ${userStory.description}
216
+
217
+ <h4>Acceptance Criteria</h4>
218
+
219
+ <ul>
220
+ ${userStory.acceptanceCriteria.map((ac, i) => `<li>${ac}</li>`).join("\n")}
221
+ </ul>
222
+
223
+ ${userStory.technicalContext ? `<h4>Technical Context</h4>
224
+
225
+ ${userStory.technicalContext}
226
+ ` : ""}
227
+
228
+ ${confidence !== void 0 ? `<p><em>Classification confidence: ${(confidence * 100).toFixed(0)}%</em></p>
229
+ ` : ""}
230
+
231
+ <p>\u{1F916} Auto-generated by SpecWeave</p>
232
+ `;
233
+ const fields = {
234
+ "System.Title": title,
235
+ "System.Description": description,
236
+ "System.State": "New"
237
+ };
238
+ const workItem = await this.createWorkItem(projectName, itemType, fields);
239
+ if (epicId) {
240
+ await this.linkWorkItems(workItem.id, epicId, "System.LinkTypes.Hierarchy-Reverse");
241
+ }
242
+ return {
243
+ project: projectName,
244
+ workItemId: workItem.id,
245
+ workItemType: itemType,
246
+ title,
247
+ url: workItem.url,
248
+ action: "created",
249
+ confidence
250
+ };
251
+ }
252
+ /**
253
+ * Create work item in area path (Pattern 2: Area Paths)
254
+ */
255
+ async createWorkItemInAreaPath(areaPath, userStory, epicId) {
256
+ const title = `${userStory.id}: ${userStory.title}`;
257
+ const itemType = this.mapItemTypeToAdo(suggestJiraItemType(userStory));
258
+ const description = `<h3>${userStory.title}</h3>
259
+
260
+ ${userStory.description}
261
+
262
+ <h4>Acceptance Criteria</h4>
263
+
264
+ <ul>
265
+ ${userStory.acceptanceCriteria.map((ac, i) => `<li>${ac}</li>`).join("\n")}
266
+ </ul>
267
+
268
+ ${userStory.technicalContext ? `<h4>Technical Context</h4>
269
+
270
+ ${userStory.technicalContext}
271
+ ` : ""}
272
+
273
+ <p>\u{1F916} Auto-generated by SpecWeave</p>
274
+ `;
275
+ const fields = {
276
+ "System.Title": title,
277
+ "System.Description": description,
278
+ "System.AreaPath": `${this.config.project}\\${areaPath}`,
279
+ // Team-specific area path
280
+ "System.State": "New"
281
+ };
282
+ const workItem = await this.createWorkItem(this.config.project, itemType, fields);
283
+ if (epicId) {
284
+ await this.linkWorkItems(workItem.id, epicId, "System.LinkTypes.Hierarchy-Reverse");
285
+ }
286
+ return {
287
+ project: this.config.project,
288
+ workItemId: workItem.id,
289
+ workItemType: itemType,
290
+ title,
291
+ url: workItem.url,
292
+ action: "created"
293
+ };
294
+ }
295
+ /**
296
+ * Create work item via ADO REST API
297
+ */
298
+ async createWorkItem(project, workItemType, fields) {
299
+ const patchDocument = Object.entries(fields).map(([key, value]) => ({
300
+ op: "add",
301
+ path: `/fields/${key}`,
302
+ value
303
+ }));
304
+ const response = await this.client.post(
305
+ `/${project}/_apis/wit/workitems/$${workItemType}?api-version=7.0`,
306
+ patchDocument
307
+ );
308
+ return response.data;
309
+ }
310
+ /**
311
+ * Link work items (parent-child relationship)
312
+ */
313
+ async linkWorkItems(sourceId, targetId, linkType) {
314
+ const patchDocument = [
315
+ {
316
+ op: "add",
317
+ path: "/relations/-",
318
+ value: {
319
+ rel: linkType,
320
+ url: `https://dev.azure.com/${this.config.organization}/_apis/wit/workItems/${targetId}`
321
+ }
322
+ }
323
+ ];
324
+ await this.client.patch(
325
+ `/_apis/wit/workitems/${sourceId}?api-version=7.0`,
326
+ patchDocument
327
+ );
328
+ }
329
+ /**
330
+ * Map Jira-style item type to ADO work item type
331
+ */
332
+ mapItemTypeToAdo(itemType) {
333
+ const mapping = this.config.workItemTypes || {};
334
+ switch (itemType) {
335
+ case "Epic":
336
+ return mapping.epic || "Epic";
337
+ case "Story":
338
+ return mapping.story || "User Story";
339
+ case "Task":
340
+ return mapping.task || "Task";
341
+ case "Subtask":
342
+ return mapping.task || "Task";
343
+ // ADO doesn't have subtasks, use Task
344
+ default:
345
+ return "User Story";
346
+ }
347
+ }
348
+ /**
349
+ * Find ADO project name for project ID
350
+ *
351
+ * Maps project IDs to ADO project names:
352
+ * - FE → FE-Project
353
+ * - BE → BE-Project
354
+ * - MOBILE → MOBILE-Project
355
+ */
356
+ findProjectForId(projectId) {
357
+ if (!this.config.projects) return void 0;
358
+ let match = this.config.projects.find((project) => project.toLowerCase().includes(projectId.toLowerCase()));
359
+ if (!match) {
360
+ const fuzzyMap = {
361
+ FE: ["frontend", "web", "ui", "client", "fe"],
362
+ BE: ["backend", "api", "server", "be"],
363
+ MOBILE: ["mobile", "app", "ios", "android"],
364
+ INFRA: ["infra", "infrastructure", "devops", "platform"]
365
+ };
366
+ const keywords = fuzzyMap[projectId] || [];
367
+ match = this.config.projects.find(
368
+ (project) => keywords.some((keyword) => project.toLowerCase().includes(keyword))
369
+ );
370
+ }
371
+ return match;
372
+ }
373
+ /**
374
+ * Find area path for project ID
375
+ *
376
+ * Maps project IDs to area paths:
377
+ * - FE → FE
378
+ * - BE → BE
379
+ * - MOBILE → MOBILE
380
+ */
381
+ findAreaPathForProjectId(projectId) {
382
+ if (!this.config.areaPaths) return void 0;
383
+ let match = this.config.areaPaths.find((areaPath) => areaPath.toLowerCase() === projectId.toLowerCase());
384
+ if (!match) {
385
+ const fuzzyMap = {
386
+ FE: ["frontend", "web", "ui", "client", "fe"],
387
+ BE: ["backend", "api", "server", "be"],
388
+ MOBILE: ["mobile", "app", "ios", "android"],
389
+ INFRA: ["infra", "infrastructure", "devops", "platform"]
390
+ };
391
+ const keywords = fuzzyMap[projectId] || [];
392
+ match = this.config.areaPaths.find(
393
+ (areaPath) => keywords.some((keyword) => areaPath.toLowerCase().includes(keyword))
394
+ );
395
+ }
396
+ return match;
397
+ }
398
+ }
399
+ function formatAdoSyncResults(results) {
400
+ const lines = [];
401
+ lines.push("\u{1F4CA} Azure DevOps Multi-Project Sync Results:\n");
402
+ const byProject = /* @__PURE__ */ new Map();
403
+ for (const result of results) {
404
+ const existing = byProject.get(result.project) || [];
405
+ existing.push(result);
406
+ byProject.set(result.project, existing);
407
+ }
408
+ for (const [project, projectResults] of byProject.entries()) {
409
+ lines.push(`
410
+ **ADO Project ${project}**:`);
411
+ for (const result of projectResults) {
412
+ const icon = result.action === "created" ? "\u2705" : result.action === "updated" ? "\u{1F504}" : "\u23ED\uFE0F";
413
+ const confidence = result.confidence !== void 0 ? ` (${(result.confidence * 100).toFixed(0)}% confidence)` : "";
414
+ lines.push(` ${icon} #${result.workItemId} [${result.workItemType}]: ${result.title}${confidence}`);
415
+ lines.push(` ${result.url}`);
416
+ }
417
+ }
418
+ lines.push(`
419
+ \u2705 Total: ${results.length} work items synced
420
+ `);
421
+ const epicCount = results.filter((r) => r.workItemType === "Epic").length;
422
+ const featureCount = results.filter((r) => r.workItemType === "Feature").length;
423
+ const storyCount = results.filter((r) => r.workItemType === "User Story").length;
424
+ const taskCount = results.filter((r) => r.workItemType === "Task").length;
425
+ lines.push("\u{1F4C8} Work Item Type Distribution:");
426
+ if (epicCount > 0) lines.push(` - Epics: ${epicCount}`);
427
+ if (featureCount > 0) lines.push(` - Features: ${featureCount}`);
428
+ if (storyCount > 0) lines.push(` - User Stories: ${storyCount}`);
429
+ if (taskCount > 0) lines.push(` - Tasks: ${taskCount}`);
430
+ return lines.join("\n");
431
+ }
432
+ async function validateAdoProjects(config, projectNames) {
433
+ const missing = [];
434
+ const client = axios.create({
435
+ baseURL: `https://dev.azure.com/${config.organization}`,
436
+ headers: {
437
+ "Authorization": `Basic ${Buffer.from(":" + config.pat).toString("base64")}`
438
+ }
439
+ });
440
+ for (const name of projectNames) {
441
+ try {
442
+ await client.get(`/_apis/projects/${name}?api-version=7.0`);
443
+ } catch (error) {
444
+ missing.push(name);
445
+ }
446
+ }
447
+ return missing;
448
+ }
449
+ export {
450
+ AdoMultiProjectSync,
451
+ formatAdoSyncResults,
452
+ validateAdoProjects
453
+ };