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,358 @@
1
+ /**
2
+ * JIRA Multi-Project Sync with Intelligent Mapping
3
+ *
4
+ * Automatically maps user stories to JIRA projects based on content analysis:
5
+ * - FE user stories → JIRA Project FE
6
+ * - BE user stories → JIRA Project BE
7
+ * - MOBILE user stories → JIRA Project MOBILE
8
+ *
9
+ * Supports hierarchical issue types:
10
+ * - Epic (> 13 story points): Large feature area
11
+ * - Story (3-13 story points): Standard user story
12
+ * - Task (1-2 story points): Small implementation task
13
+ * - Subtask (< 1 story point): Granular work item
14
+ *
15
+ * @module jira-multi-project-sync
16
+ */
17
+
18
+ import { JiraClient, JiraIssue } from '../../../src/integrations/jira/jira-client.js';
19
+ import {
20
+ UserStory,
21
+ getPrimaryProject,
22
+ suggestJiraItemType,
23
+ mapUserStoryToProjects
24
+ } from '../../../src/utils/project-mapper.js';
25
+ import { parseSpecFile } from '../../../src/utils/spec-splitter.js';
26
+
27
+ export interface JiraMultiProjectConfig {
28
+ domain: string;
29
+ email: string;
30
+ apiToken: string;
31
+
32
+ // Simple: List of JIRA projects
33
+ projects: string[]; // ['FE', 'BE', 'MOBILE']
34
+
35
+ // Item type mapping (optional)
36
+ itemTypeMapping?: {
37
+ epic: string; // Default: 'Epic'
38
+ story: string; // Default: 'Story'
39
+ task: string; // Default: 'Task'
40
+ subtask: string; // Default: 'Sub-task'
41
+ };
42
+
43
+ // Settings
44
+ intelligentMapping?: boolean; // Default: true (auto-classify user stories)
45
+ autoCreateEpics?: boolean; // Default: true (create epic per project)
46
+ }
47
+
48
+ export interface JiraSyncResult {
49
+ project: string;
50
+ issueKey: string;
51
+ issueType: string;
52
+ summary: string;
53
+ url: string;
54
+ action: 'created' | 'updated' | 'skipped';
55
+ confidence?: number; // Classification confidence (0.0-1.0)
56
+ }
57
+
58
+ /**
59
+ * JIRA Multi-Project Sync Client
60
+ */
61
+ export class JiraMultiProjectSync {
62
+ private client: JiraClient;
63
+ private config: JiraMultiProjectConfig;
64
+
65
+ constructor(config: JiraMultiProjectConfig) {
66
+ this.config = config;
67
+ this.client = new JiraClient({
68
+ domain: config.domain,
69
+ email: config.email,
70
+ apiToken: config.apiToken
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Sync spec to JIRA projects with intelligent mapping
76
+ *
77
+ * @param specPath Path to spec file
78
+ * @returns Array of sync results
79
+ */
80
+ async syncSpec(specPath: string): Promise<JiraSyncResult[]> {
81
+ const results: JiraSyncResult[] = [];
82
+
83
+ // Parse spec
84
+ const parsedSpec = await parseSpecFile(specPath);
85
+
86
+ // Step 1: Create epic per project (if enabled)
87
+ const epicsByProject = new Map<string, string>(); // projectId → epicKey
88
+
89
+ if (this.config.autoCreateEpics !== false) {
90
+ for (const project of this.config.projects) {
91
+ const epicResult = await this.createEpicForProject(parsedSpec, project);
92
+ epicsByProject.set(project, epicResult.issueKey);
93
+ results.push(epicResult);
94
+ }
95
+ }
96
+
97
+ // Step 2: Classify user stories by project
98
+ const projectStories = new Map<string, Array<{ story: UserStory; confidence: number }>>();
99
+
100
+ for (const userStory of parsedSpec.userStories) {
101
+ if (this.config.intelligentMapping !== false) {
102
+ // Intelligent mapping (default)
103
+ const mappings = mapUserStoryToProjects(userStory);
104
+
105
+ if (mappings.length > 0 && mappings[0].confidence >= 0.3) {
106
+ const primary = mappings[0];
107
+ const existing = projectStories.get(primary.projectId) || [];
108
+ existing.push({ story: userStory, confidence: primary.confidence });
109
+ projectStories.set(primary.projectId, existing);
110
+ } else {
111
+ // No confident match - assign to first project or skip
112
+ console.warn(`⚠️ Low confidence for ${userStory.id} (${(mappings[0]?.confidence || 0) * 100}%) - assigning to ${this.config.projects[0]}`);
113
+ const fallback = this.config.projects[0];
114
+ const existing = projectStories.get(fallback) || [];
115
+ existing.push({ story: userStory, confidence: mappings[0]?.confidence || 0 });
116
+ projectStories.set(fallback, existing);
117
+ }
118
+ } else {
119
+ // Manual mapping (user specified project in frontmatter)
120
+ const projectHint = this.extractProjectHint(userStory);
121
+
122
+ if (projectHint && this.config.projects.includes(projectHint)) {
123
+ const existing = projectStories.get(projectHint) || [];
124
+ existing.push({ story: userStory, confidence: 1.0 });
125
+ projectStories.set(projectHint, existing);
126
+ } else {
127
+ // No hint - assign to first project
128
+ const fallback = this.config.projects[0];
129
+ const existing = projectStories.get(fallback) || [];
130
+ existing.push({ story: userStory, confidence: 0 });
131
+ projectStories.set(fallback, existing);
132
+ }
133
+ }
134
+ }
135
+
136
+ // Step 3: Create issues in each project
137
+ for (const [projectId, stories] of projectStories.entries()) {
138
+ const epicKey = epicsByProject.get(projectId);
139
+
140
+ for (const { story, confidence } of stories) {
141
+ const result = await this.createIssueForUserStory(projectId, story, epicKey, confidence);
142
+ results.push(result);
143
+ }
144
+ }
145
+
146
+ return results;
147
+ }
148
+
149
+ /**
150
+ * Create epic for project
151
+ */
152
+ private async createEpicForProject(parsedSpec: any, projectId: string): Promise<JiraSyncResult> {
153
+ const summary = `${parsedSpec.metadata.title} - ${projectId}`;
154
+
155
+ const description = `h2. ${projectId} Implementation
156
+
157
+ *Status*: ${parsedSpec.metadata.status}
158
+ *Priority*: ${parsedSpec.metadata.priority}
159
+ *Estimated Effort*: ${parsedSpec.metadata.estimatedEffort || parsedSpec.metadata.estimated_effort}
160
+
161
+ h3. Executive Summary
162
+
163
+ ${parsedSpec.executiveSummary}
164
+
165
+ h3. Scope (${projectId})
166
+
167
+ This epic covers all ${projectId}-related user stories for "${parsedSpec.metadata.title}".
168
+
169
+ User stories will be added as child issues.
170
+
171
+ ---
172
+
173
+ 🤖 Auto-generated by SpecWeave
174
+ `;
175
+
176
+ const issue = await this.client.createIssue({
177
+ project: { key: projectId },
178
+ summary,
179
+ description,
180
+ issuetype: { name: this.config.itemTypeMapping?.epic || 'Epic' }
181
+ });
182
+
183
+ return {
184
+ project: projectId,
185
+ issueKey: issue.key,
186
+ issueType: 'Epic',
187
+ summary,
188
+ url: `https://${this.config.domain}/browse/${issue.key}`,
189
+ action: 'created'
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Create issue for user story
195
+ */
196
+ private async createIssueForUserStory(
197
+ projectId: string,
198
+ userStory: UserStory,
199
+ epicKey?: string,
200
+ confidence?: number
201
+ ): Promise<JiraSyncResult> {
202
+ const summary = `${userStory.id}: ${userStory.title}`;
203
+
204
+ // Determine issue type based on story points
205
+ const itemType = suggestJiraItemType(userStory);
206
+
207
+ const description = `h3. ${userStory.title}
208
+
209
+ ${userStory.description}
210
+
211
+ h4. Acceptance Criteria
212
+
213
+ ${userStory.acceptanceCriteria.map((ac, i) => `* ${ac}`).join('\n')}
214
+
215
+ ${userStory.technicalContext ? `\nh4. Technical Context\n\n${userStory.technicalContext}\n` : ''}
216
+
217
+ ${confidence !== undefined ? `\n_Classification confidence: ${(confidence * 100).toFixed(0)}%_\n` : ''}
218
+
219
+ 🤖 Auto-generated by SpecWeave
220
+ `;
221
+
222
+ const issueData: any = {
223
+ project: { key: projectId },
224
+ summary,
225
+ description,
226
+ issuetype: { name: this.getIssueTypeName(itemType) }
227
+ };
228
+
229
+ // Link to epic if provided
230
+ if (epicKey) {
231
+ issueData.parent = { key: epicKey };
232
+ }
233
+
234
+ const issue = await this.client.createIssue(issueData);
235
+
236
+ return {
237
+ project: projectId,
238
+ issueKey: issue.key,
239
+ issueType: itemType,
240
+ summary,
241
+ url: `https://${this.config.domain}/browse/${issue.key}`,
242
+ action: 'created',
243
+ confidence
244
+ };
245
+ }
246
+
247
+ /**
248
+ * Get JIRA issue type name from suggested type
249
+ */
250
+ private getIssueTypeName(itemType: 'Epic' | 'Story' | 'Task' | 'Subtask'): string {
251
+ const mapping = this.config.itemTypeMapping || {};
252
+
253
+ switch (itemType) {
254
+ case 'Epic':
255
+ return mapping.epic || 'Epic';
256
+ case 'Story':
257
+ return mapping.story || 'Story';
258
+ case 'Task':
259
+ return mapping.task || 'Task';
260
+ case 'Subtask':
261
+ return mapping.subtask || 'Sub-task';
262
+ default:
263
+ return 'Story';
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Extract project hint from user story (manual override)
269
+ *
270
+ * Looks for hints like:
271
+ * - Title: "[FE] Login UI"
272
+ * - Description: "Project: FE"
273
+ * - Technical context: "Frontend: React"
274
+ */
275
+ private extractProjectHint(userStory: UserStory): string | undefined {
276
+ // Check title for [PROJECT] prefix
277
+ const titleMatch = userStory.title.match(/^\[([A-Z]+)\]/);
278
+ if (titleMatch) {
279
+ return titleMatch[1];
280
+ }
281
+
282
+ // Check description for "Project: XXX"
283
+ const descMatch = userStory.description.match(/Project:\s*([A-Z]+)/i);
284
+ if (descMatch) {
285
+ return descMatch[1].toUpperCase();
286
+ }
287
+
288
+ return undefined;
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Format JIRA sync results for display
294
+ */
295
+ export function formatJiraSyncResults(results: JiraSyncResult[]): string {
296
+ const lines: string[] = [];
297
+
298
+ lines.push('📊 JIRA Multi-Project Sync Results:\n');
299
+
300
+ const byProject = new Map<string, JiraSyncResult[]>();
301
+
302
+ for (const result of results) {
303
+ const existing = byProject.get(result.project) || [];
304
+ existing.push(result);
305
+ byProject.set(result.project, existing);
306
+ }
307
+
308
+ for (const [project, projectResults] of byProject.entries()) {
309
+ lines.push(`\n**JIRA Project ${project}**:`);
310
+
311
+ for (const result of projectResults) {
312
+ const icon = result.action === 'created' ? '✅' : result.action === 'updated' ? '🔄' : '⏭️';
313
+ const confidence = result.confidence !== undefined ? ` (${(result.confidence * 100).toFixed(0)}% confidence)` : '';
314
+ lines.push(` ${icon} ${result.issueKey} [${result.issueType}]: ${result.summary}${confidence}`);
315
+ lines.push(` ${result.url}`);
316
+ }
317
+ }
318
+
319
+ lines.push(`\n✅ Total: ${results.length} issues synced\n`);
320
+
321
+ // Show classification summary
322
+ const epicCount = results.filter(r => r.issueType === 'Epic').length;
323
+ const storyCount = results.filter(r => r.issueType === 'Story').length;
324
+ const taskCount = results.filter(r => r.issueType === 'Task').length;
325
+ const subtaskCount = results.filter(r => r.issueType === 'Subtask').length;
326
+
327
+ lines.push('📈 Item Type Distribution:');
328
+ if (epicCount > 0) lines.push(` - Epics: ${epicCount}`);
329
+ if (storyCount > 0) lines.push(` - Stories: ${storyCount}`);
330
+ if (taskCount > 0) lines.push(` - Tasks: ${taskCount}`);
331
+ if (subtaskCount > 0) lines.push(` - Subtasks: ${subtaskCount}`);
332
+
333
+ return lines.join('\n');
334
+ }
335
+
336
+ /**
337
+ * Validate JIRA projects exist
338
+ *
339
+ * @param client JIRA client
340
+ * @param projectKeys Array of project keys to validate
341
+ * @returns Validation results (missing projects)
342
+ */
343
+ export async function validateJiraProjects(
344
+ client: JiraClient,
345
+ projectKeys: string[]
346
+ ): Promise<string[]> {
347
+ const missing: string[] = [];
348
+
349
+ for (const key of projectKeys) {
350
+ try {
351
+ await client.getProject(key);
352
+ } catch (error) {
353
+ missing.push(key);
354
+ }
355
+ }
356
+
357
+ return missing;
358
+ }