specweave 0.33.2 → 0.33.4

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 (101) hide show
  1. package/CLAUDE.md +133 -19
  2. package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts +120 -0
  3. package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts.map +1 -0
  4. package/dist/plugins/specweave-ado/lib/per-us-sync.js +276 -0
  5. package/dist/plugins/specweave-ado/lib/per-us-sync.js.map +1 -0
  6. package/dist/plugins/specweave-github/lib/github-client-v2.d.ts +4 -1
  7. package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
  8. package/dist/plugins/specweave-github/lib/github-client-v2.js +13 -3
  9. package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
  10. package/dist/plugins/specweave-github/lib/per-us-sync.d.ts +97 -0
  11. package/dist/plugins/specweave-github/lib/per-us-sync.d.ts.map +1 -0
  12. package/dist/plugins/specweave-github/lib/per-us-sync.js +274 -0
  13. package/dist/plugins/specweave-github/lib/per-us-sync.js.map +1 -0
  14. package/dist/plugins/specweave-jira/lib/per-us-sync.d.ts +113 -0
  15. package/dist/plugins/specweave-jira/lib/per-us-sync.d.ts.map +1 -0
  16. package/dist/plugins/specweave-jira/lib/per-us-sync.js +254 -0
  17. package/dist/plugins/specweave-jira/lib/per-us-sync.js.map +1 -0
  18. package/dist/src/cli/cleanup-zombies.js +8 -5
  19. package/dist/src/cli/cleanup-zombies.js.map +1 -1
  20. package/dist/src/config/types.d.ts +203 -1208
  21. package/dist/src/config/types.d.ts.map +1 -1
  22. package/dist/src/core/config/config-manager.d.ts.map +1 -1
  23. package/dist/src/core/config/config-manager.js +58 -0
  24. package/dist/src/core/config/config-manager.js.map +1 -1
  25. package/dist/src/core/config/types.d.ts +80 -0
  26. package/dist/src/core/config/types.d.ts.map +1 -1
  27. package/dist/src/core/config/types.js.map +1 -1
  28. package/dist/src/core/living-docs/cross-project-sync.d.ts +87 -15
  29. package/dist/src/core/living-docs/cross-project-sync.d.ts.map +1 -1
  30. package/dist/src/core/living-docs/cross-project-sync.js +147 -28
  31. package/dist/src/core/living-docs/cross-project-sync.js.map +1 -1
  32. package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
  33. package/dist/src/core/living-docs/living-docs-sync.js +26 -22
  34. package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
  35. package/dist/src/core/living-docs/types.d.ts +24 -3
  36. package/dist/src/core/living-docs/types.d.ts.map +1 -1
  37. package/dist/src/core/types/config.d.ts +79 -0
  38. package/dist/src/core/types/config.d.ts.map +1 -1
  39. package/dist/src/core/types/config.js.map +1 -1
  40. package/dist/src/importers/jira-importer.d.ts +10 -0
  41. package/dist/src/importers/jira-importer.d.ts.map +1 -1
  42. package/dist/src/importers/jira-importer.js +55 -5
  43. package/dist/src/importers/jira-importer.js.map +1 -1
  44. package/dist/src/init/architecture/types.d.ts +33 -140
  45. package/dist/src/init/architecture/types.d.ts.map +1 -1
  46. package/dist/src/init/compliance/types.d.ts +30 -27
  47. package/dist/src/init/compliance/types.d.ts.map +1 -1
  48. package/dist/src/init/repo/types.d.ts +11 -34
  49. package/dist/src/init/repo/types.d.ts.map +1 -1
  50. package/dist/src/init/research/src/config/types.d.ts +15 -82
  51. package/dist/src/init/research/src/config/types.d.ts.map +1 -1
  52. package/dist/src/init/research/types.d.ts +38 -93
  53. package/dist/src/init/research/types.d.ts.map +1 -1
  54. package/dist/src/init/team/types.d.ts +4 -42
  55. package/dist/src/init/team/types.d.ts.map +1 -1
  56. package/dist/src/sync/closure-metrics.d.ts +102 -0
  57. package/dist/src/sync/closure-metrics.d.ts.map +1 -0
  58. package/dist/src/sync/closure-metrics.js +267 -0
  59. package/dist/src/sync/closure-metrics.js.map +1 -0
  60. package/dist/src/sync/sync-coordinator.d.ts +49 -0
  61. package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
  62. package/dist/src/sync/sync-coordinator.js +399 -37
  63. package/dist/src/sync/sync-coordinator.js.map +1 -1
  64. package/dist/src/utils/notification-constants.d.ts +85 -0
  65. package/dist/src/utils/notification-constants.d.ts.map +1 -0
  66. package/dist/src/utils/notification-constants.js +129 -0
  67. package/dist/src/utils/notification-constants.js.map +1 -0
  68. package/dist/src/utils/platform-utils.d.ts +13 -3
  69. package/dist/src/utils/platform-utils.d.ts.map +1 -1
  70. package/dist/src/utils/platform-utils.js +17 -6
  71. package/dist/src/utils/platform-utils.js.map +1 -1
  72. package/dist/src/utils/project-resolver.d.ts +156 -0
  73. package/dist/src/utils/project-resolver.d.ts.map +1 -0
  74. package/dist/src/utils/project-resolver.js +587 -0
  75. package/dist/src/utils/project-resolver.js.map +1 -0
  76. package/package.json +1 -1
  77. package/plugins/specweave/commands/specweave-increment.md +46 -0
  78. package/plugins/specweave/commands/specweave-jobs.md +153 -8
  79. package/plugins/specweave/hooks/hooks.json +10 -0
  80. package/plugins/specweave/hooks/spec-project-validator.sh +24 -2
  81. package/plugins/specweave/hooks/universal/hook-wrapper.cmd +26 -26
  82. package/plugins/specweave/hooks/universal/session-start.cmd +16 -16
  83. package/plugins/specweave/hooks/universal/session-start.ps1 +16 -16
  84. package/plugins/specweave/hooks/user-prompt-submit.sh +105 -3
  85. package/plugins/specweave/hooks/v2/guards/per-us-project-validator.sh +281 -0
  86. package/plugins/specweave/hooks/v2/handlers/living-specs-handler.sh +29 -0
  87. package/plugins/specweave/scripts/session-watchdog.sh +278 -130
  88. package/plugins/specweave/skills/increment-planner/SKILL.md +48 -18
  89. package/plugins/specweave/skills/increment-planner/templates/spec-multi-project.md +27 -14
  90. package/plugins/specweave/skills/increment-planner/templates/spec-single-project.md +16 -5
  91. package/plugins/specweave/skills/spec-generator/SKILL.md +74 -15
  92. package/plugins/specweave-ado/lib/per-us-sync.js +247 -0
  93. package/plugins/specweave-ado/lib/per-us-sync.ts +410 -0
  94. package/plugins/specweave-github/lib/github-client-v2.js +10 -3
  95. package/plugins/specweave-github/lib/github-client-v2.ts +15 -3
  96. package/plugins/specweave-github/lib/per-us-sync.js +241 -0
  97. package/plugins/specweave-github/lib/per-us-sync.ts +375 -0
  98. package/plugins/specweave-jira/lib/per-us-sync.js +224 -0
  99. package/plugins/specweave-jira/lib/per-us-sync.ts +366 -0
  100. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +0 -738
  101. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +0 -1107
@@ -0,0 +1,224 @@
1
+ import { consoleLogger } from "../../../src/utils/logger.js";
2
+ class PerUSJiraSync {
3
+ constructor(jiraClient, projectMappings, options = {}) {
4
+ this.jiraClient = jiraClient;
5
+ this.projectMappings = projectMappings;
6
+ this.logger = options.logger ?? consoleLogger;
7
+ }
8
+ /**
9
+ * Sync all user stories to their respective JIRA projects
10
+ *
11
+ * @param userStories - User stories with explicit project fields
12
+ * @param featureId - Feature ID (e.g., "FS-137")
13
+ * @param options - Sync options
14
+ */
15
+ async syncUserStories(userStories, featureId, options = {}) {
16
+ const synced = [];
17
+ const failed = [];
18
+ const externalRefs = {};
19
+ const groups = this.groupByProject(userStories, options.defaultProject);
20
+ this.logger.log(`\u{1F4E1} Per-US JIRA Sync: ${userStories.length} USs across ${groups.size} projects`);
21
+ for (const [projectId, stories] of groups) {
22
+ const mapping = this.projectMappings[projectId]?.jira;
23
+ if (!mapping) {
24
+ this.logger.warn(` \u26A0\uFE0F No JIRA mapping for project "${projectId}" - skipping ${stories.length} USs`);
25
+ for (const story of stories) {
26
+ failed.push({
27
+ usId: story.id,
28
+ projectId,
29
+ jiraProject: "N/A",
30
+ issueKey: "",
31
+ url: "",
32
+ action: "skipped",
33
+ error: `No JIRA mapping for project "${projectId}"`
34
+ });
35
+ }
36
+ continue;
37
+ }
38
+ for (const story of stories) {
39
+ try {
40
+ const result = await this.syncUserStory(story, mapping, featureId, options);
41
+ synced.push({
42
+ ...result,
43
+ projectId
44
+ });
45
+ if (!options.dryRun && result.action !== "skipped") {
46
+ externalRefs[story.id] = {
47
+ jira: {
48
+ provider: "jira",
49
+ issueNumber: result.issueKey,
50
+ url: result.url,
51
+ targetProject: projectId,
52
+ lastSynced: (/* @__PURE__ */ new Date()).toISOString()
53
+ }
54
+ };
55
+ }
56
+ } catch (error) {
57
+ failed.push({
58
+ usId: story.id,
59
+ projectId,
60
+ jiraProject: mapping.project,
61
+ issueKey: "",
62
+ url: "",
63
+ action: "skipped",
64
+ error: error instanceof Error ? error.message : String(error)
65
+ });
66
+ }
67
+ }
68
+ }
69
+ const created = synced.filter((r) => r.action === "created").length;
70
+ const updated = synced.filter((r) => r.action === "updated").length;
71
+ const skipped = synced.filter((r) => r.action === "skipped").length;
72
+ return {
73
+ success: failed.length === 0,
74
+ synced,
75
+ failed,
76
+ externalRefs,
77
+ summary: {
78
+ total: userStories.length,
79
+ created,
80
+ updated,
81
+ skipped,
82
+ failed: failed.length
83
+ }
84
+ };
85
+ }
86
+ /**
87
+ * Sync a single user story to JIRA
88
+ */
89
+ async syncUserStory(story, mapping, featureId, options) {
90
+ const summary = `[${featureId}][${story.id}] ${story.title}`;
91
+ const description = this.buildIssueDescription(story, featureId);
92
+ if (options.dryRun) {
93
+ this.logger.log(` \u{1F50D} [DRY-RUN] Would sync ${story.id} to JIRA project ${mapping.project}`);
94
+ return {
95
+ usId: story.id,
96
+ projectId: story.project || "unknown",
97
+ jiraProject: mapping.project,
98
+ issueKey: "",
99
+ url: "",
100
+ action: "skipped"
101
+ };
102
+ }
103
+ const existingIssue = await this.findExistingIssue(mapping.project, story.id);
104
+ if (existingIssue) {
105
+ await this.jiraClient.updateIssue(existingIssue.key, summary, description);
106
+ this.logger.log(` \u{1F504} Updated ${story.id} \u2192 ${existingIssue.key}`);
107
+ return {
108
+ usId: story.id,
109
+ projectId: story.project || "unknown",
110
+ jiraProject: mapping.project,
111
+ issueKey: existingIssue.key,
112
+ url: this.jiraClient.getIssueUrl(existingIssue.key),
113
+ action: "updated"
114
+ };
115
+ } else {
116
+ const newIssue = await this.jiraClient.createIssue(
117
+ mapping.project,
118
+ "Story",
119
+ summary,
120
+ description
121
+ );
122
+ this.logger.log(` \u2705 Created ${story.id} \u2192 ${newIssue.key}`);
123
+ return {
124
+ usId: story.id,
125
+ projectId: story.project || "unknown",
126
+ jiraProject: mapping.project,
127
+ issueKey: newIssue.key,
128
+ url: this.jiraClient.getIssueUrl(newIssue.key),
129
+ action: "created"
130
+ };
131
+ }
132
+ }
133
+ /**
134
+ * Find existing issue by US ID in summary
135
+ */
136
+ async findExistingIssue(project, usId) {
137
+ try {
138
+ const jql = `project = "${project}" AND summary ~ "[${usId}]"`;
139
+ const results = await this.jiraClient.searchIssues(jql);
140
+ return results.length > 0 ? { key: results[0].key } : null;
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+ /**
146
+ * Build issue description from user story
147
+ */
148
+ buildIssueDescription(story, featureId) {
149
+ const lines = [];
150
+ lines.push(`h1. ${story.title}`);
151
+ lines.push("");
152
+ if (story.description) {
153
+ lines.push(story.description);
154
+ lines.push("");
155
+ }
156
+ if (story.acceptanceCriteria && story.acceptanceCriteria.length > 0) {
157
+ lines.push("h2. Acceptance Criteria");
158
+ lines.push("");
159
+ for (const ac of story.acceptanceCriteria) {
160
+ lines.push(`* ${ac}`);
161
+ }
162
+ lines.push("");
163
+ }
164
+ lines.push("----");
165
+ lines.push("");
166
+ lines.push(`*Feature*: ${featureId}`);
167
+ lines.push(`*User Story*: ${story.id}`);
168
+ if (story.project) {
169
+ lines.push(`*Project*: ${story.project}`);
170
+ }
171
+ if (story.board) {
172
+ lines.push(`*Board*: ${story.board}`);
173
+ }
174
+ lines.push("");
175
+ lines.push("_Auto-generated by SpecWeave_");
176
+ return lines.join("\n");
177
+ }
178
+ /**
179
+ * Group user stories by their explicit project field
180
+ */
181
+ groupByProject(userStories, defaultProject) {
182
+ const groups = /* @__PURE__ */ new Map();
183
+ for (const story of userStories) {
184
+ const project = story.project || defaultProject || "default";
185
+ if (!groups.has(project)) {
186
+ groups.set(project, []);
187
+ }
188
+ groups.get(project).push(story);
189
+ }
190
+ return groups;
191
+ }
192
+ }
193
+ function formatPerUSSyncResults(result) {
194
+ const lines = [];
195
+ lines.push("");
196
+ lines.push("\u{1F4CA} Per-US JIRA Sync Results");
197
+ lines.push("");
198
+ const byProject = /* @__PURE__ */ new Map();
199
+ for (const r of [...result.synced, ...result.failed]) {
200
+ const existing = byProject.get(r.projectId) || [];
201
+ existing.push(r);
202
+ byProject.set(r.projectId, existing);
203
+ }
204
+ for (const [projectId, results] of byProject) {
205
+ lines.push(`**${projectId}** (\u2192 ${results[0]?.jiraProject || "N/A"}):`);
206
+ for (const r of results) {
207
+ const icon = r.action === "created" ? "\u2705" : r.action === "updated" ? "\u{1F504}" : r.error ? "\u274C" : "\u23ED\uFE0F";
208
+ if (r.issueKey) {
209
+ lines.push(` ${icon} ${r.usId} \u2192 ${r.issueKey}`);
210
+ } else if (r.error) {
211
+ lines.push(` ${icon} ${r.usId}: ${r.error}`);
212
+ } else {
213
+ lines.push(` ${icon} ${r.usId} (${r.action})`);
214
+ }
215
+ }
216
+ lines.push("");
217
+ }
218
+ lines.push(`\u{1F4C8} Summary: ${result.summary.created} created, ${result.summary.updated} updated, ${result.summary.failed} failed`);
219
+ return lines.join("\n");
220
+ }
221
+ export {
222
+ PerUSJiraSync,
223
+ formatPerUSSyncResults
224
+ };
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Per-US JIRA Sync (v0.34.0+)
3
+ *
4
+ * Syncs each User Story to its explicitly declared project's JIRA project.
5
+ * Uses the **Project**: field in spec.md (NOT keyword-based classification).
6
+ *
7
+ * Key difference from multi-project-sync:
8
+ * - Multi-project sync uses keyword/heuristic classification
9
+ * - Per-US sync uses EXPLICIT **Project**: field from spec.md
10
+ *
11
+ * @module per-us-sync
12
+ * @since v0.34.0
13
+ */
14
+
15
+ import type { UserStoryData } from '../../../src/core/living-docs/types.js';
16
+ import type { ProjectMappings, JiraMapping } from '../../../src/core/types/config.js';
17
+ import type { USExternalRefsMap } from '../../../src/core/types/increment-metadata.js';
18
+ import { Logger, consoleLogger } from '../../../src/utils/logger.js';
19
+
20
+ /**
21
+ * Result of syncing a single US to JIRA
22
+ */
23
+ export interface USSyncResult {
24
+ usId: string;
25
+ projectId: string;
26
+ jiraProject: string;
27
+ issueKey: string;
28
+ url: string;
29
+ action: 'created' | 'updated' | 'skipped';
30
+ error?: string;
31
+ }
32
+
33
+ /**
34
+ * Result of syncing all USs in an increment
35
+ */
36
+ export interface PerUSSyncResult {
37
+ success: boolean;
38
+ synced: USSyncResult[];
39
+ failed: USSyncResult[];
40
+ externalRefs: USExternalRefsMap;
41
+ summary: {
42
+ total: number;
43
+ created: number;
44
+ updated: number;
45
+ skipped: number;
46
+ failed: number;
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Options for per-US sync
52
+ */
53
+ export interface PerUSSyncOptions {
54
+ dryRun?: boolean;
55
+ force?: boolean;
56
+ defaultProject?: string;
57
+ logger?: Logger;
58
+ }
59
+
60
+ /**
61
+ * JIRA client interface (to be injected)
62
+ */
63
+ export interface JiraClient {
64
+ createIssue(project: string, issueType: string, summary: string, description: string): Promise<{ key: string; self: string }>;
65
+ updateIssue(issueKey: string, summary: string, description: string): Promise<void>;
66
+ searchIssues(jql: string): Promise<Array<{ key: string; fields: { summary: string } }>>;
67
+ getIssueUrl(issueKey: string): string;
68
+ }
69
+
70
+ /**
71
+ * Per-US JIRA Sync
72
+ *
73
+ * Syncs each US to its declared project's JIRA project.
74
+ */
75
+ export class PerUSJiraSync {
76
+ private projectMappings: ProjectMappings;
77
+ private jiraClient: JiraClient;
78
+ private logger: Logger;
79
+
80
+ constructor(
81
+ jiraClient: JiraClient,
82
+ projectMappings: ProjectMappings,
83
+ options: { logger?: Logger } = {}
84
+ ) {
85
+ this.jiraClient = jiraClient;
86
+ this.projectMappings = projectMappings;
87
+ this.logger = options.logger ?? consoleLogger;
88
+ }
89
+
90
+ /**
91
+ * Sync all user stories to their respective JIRA projects
92
+ *
93
+ * @param userStories - User stories with explicit project fields
94
+ * @param featureId - Feature ID (e.g., "FS-137")
95
+ * @param options - Sync options
96
+ */
97
+ async syncUserStories(
98
+ userStories: UserStoryData[],
99
+ featureId: string,
100
+ options: PerUSSyncOptions = {}
101
+ ): Promise<PerUSSyncResult> {
102
+ const synced: USSyncResult[] = [];
103
+ const failed: USSyncResult[] = [];
104
+ const externalRefs: USExternalRefsMap = {};
105
+
106
+ // Group USs by their declared project
107
+ const groups = this.groupByProject(userStories, options.defaultProject);
108
+
109
+ this.logger.log(`📡 Per-US JIRA Sync: ${userStories.length} USs across ${groups.size} projects`);
110
+
111
+ for (const [projectId, stories] of groups) {
112
+ // Get JIRA mapping for this project
113
+ const mapping = this.projectMappings[projectId]?.jira;
114
+
115
+ if (!mapping) {
116
+ // No JIRA mapping for this project
117
+ this.logger.warn(` ⚠️ No JIRA mapping for project "${projectId}" - skipping ${stories.length} USs`);
118
+ for (const story of stories) {
119
+ failed.push({
120
+ usId: story.id,
121
+ projectId,
122
+ jiraProject: 'N/A',
123
+ issueKey: '',
124
+ url: '',
125
+ action: 'skipped',
126
+ error: `No JIRA mapping for project "${projectId}"`
127
+ });
128
+ }
129
+ continue;
130
+ }
131
+
132
+ // Sync each US to this project's JIRA project
133
+ for (const story of stories) {
134
+ try {
135
+ const result = await this.syncUserStory(story, mapping, featureId, options);
136
+ synced.push({
137
+ ...result,
138
+ projectId
139
+ });
140
+
141
+ // Build external ref
142
+ if (!options.dryRun && result.action !== 'skipped') {
143
+ externalRefs[story.id] = {
144
+ jira: {
145
+ provider: 'jira',
146
+ issueNumber: result.issueKey,
147
+ url: result.url,
148
+ targetProject: projectId,
149
+ lastSynced: new Date().toISOString()
150
+ }
151
+ };
152
+ }
153
+ } catch (error) {
154
+ failed.push({
155
+ usId: story.id,
156
+ projectId,
157
+ jiraProject: mapping.project,
158
+ issueKey: '',
159
+ url: '',
160
+ action: 'skipped',
161
+ error: error instanceof Error ? error.message : String(error)
162
+ });
163
+ }
164
+ }
165
+ }
166
+
167
+ // Calculate summary
168
+ const created = synced.filter(r => r.action === 'created').length;
169
+ const updated = synced.filter(r => r.action === 'updated').length;
170
+ const skipped = synced.filter(r => r.action === 'skipped').length;
171
+
172
+ return {
173
+ success: failed.length === 0,
174
+ synced,
175
+ failed,
176
+ externalRefs,
177
+ summary: {
178
+ total: userStories.length,
179
+ created,
180
+ updated,
181
+ skipped,
182
+ failed: failed.length
183
+ }
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Sync a single user story to JIRA
189
+ */
190
+ private async syncUserStory(
191
+ story: UserStoryData,
192
+ mapping: JiraMapping,
193
+ featureId: string,
194
+ options: PerUSSyncOptions
195
+ ): Promise<USSyncResult> {
196
+ const summary = `[${featureId}][${story.id}] ${story.title}`;
197
+ const description = this.buildIssueDescription(story, featureId);
198
+
199
+ if (options.dryRun) {
200
+ this.logger.log(` 🔍 [DRY-RUN] Would sync ${story.id} to JIRA project ${mapping.project}`);
201
+ return {
202
+ usId: story.id,
203
+ projectId: story.project || 'unknown',
204
+ jiraProject: mapping.project,
205
+ issueKey: '',
206
+ url: '',
207
+ action: 'skipped'
208
+ };
209
+ }
210
+
211
+ // Check for existing issue
212
+ const existingIssue = await this.findExistingIssue(mapping.project, story.id);
213
+
214
+ if (existingIssue) {
215
+ // Update existing issue
216
+ await this.jiraClient.updateIssue(existingIssue.key, summary, description);
217
+
218
+ this.logger.log(` 🔄 Updated ${story.id} → ${existingIssue.key}`);
219
+
220
+ return {
221
+ usId: story.id,
222
+ projectId: story.project || 'unknown',
223
+ jiraProject: mapping.project,
224
+ issueKey: existingIssue.key,
225
+ url: this.jiraClient.getIssueUrl(existingIssue.key),
226
+ action: 'updated'
227
+ };
228
+ } else {
229
+ // Create new issue
230
+ const newIssue = await this.jiraClient.createIssue(
231
+ mapping.project,
232
+ 'Story',
233
+ summary,
234
+ description
235
+ );
236
+
237
+ this.logger.log(` ✅ Created ${story.id} → ${newIssue.key}`);
238
+
239
+ return {
240
+ usId: story.id,
241
+ projectId: story.project || 'unknown',
242
+ jiraProject: mapping.project,
243
+ issueKey: newIssue.key,
244
+ url: this.jiraClient.getIssueUrl(newIssue.key),
245
+ action: 'created'
246
+ };
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Find existing issue by US ID in summary
252
+ */
253
+ private async findExistingIssue(
254
+ project: string,
255
+ usId: string
256
+ ): Promise<{ key: string } | null> {
257
+ try {
258
+ const jql = `project = "${project}" AND summary ~ "[${usId}]"`;
259
+ const results = await this.jiraClient.searchIssues(jql);
260
+
261
+ return results.length > 0 ? { key: results[0].key } : null;
262
+ } catch {
263
+ return null;
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Build issue description from user story
269
+ */
270
+ private buildIssueDescription(story: UserStoryData, featureId: string): string {
271
+ const lines: string[] = [];
272
+
273
+ lines.push(`h1. ${story.title}`);
274
+ lines.push('');
275
+
276
+ if (story.description) {
277
+ lines.push(story.description);
278
+ lines.push('');
279
+ }
280
+
281
+ if (story.acceptanceCriteria && story.acceptanceCriteria.length > 0) {
282
+ lines.push('h2. Acceptance Criteria');
283
+ lines.push('');
284
+ for (const ac of story.acceptanceCriteria) {
285
+ lines.push(`* ${ac}`);
286
+ }
287
+ lines.push('');
288
+ }
289
+
290
+ lines.push('----');
291
+ lines.push('');
292
+ lines.push(`*Feature*: ${featureId}`);
293
+ lines.push(`*User Story*: ${story.id}`);
294
+ if (story.project) {
295
+ lines.push(`*Project*: ${story.project}`);
296
+ }
297
+ if (story.board) {
298
+ lines.push(`*Board*: ${story.board}`);
299
+ }
300
+ lines.push('');
301
+ lines.push('_Auto-generated by SpecWeave_');
302
+
303
+ return lines.join('\n');
304
+ }
305
+
306
+ /**
307
+ * Group user stories by their explicit project field
308
+ */
309
+ private groupByProject(
310
+ userStories: UserStoryData[],
311
+ defaultProject?: string
312
+ ): Map<string, UserStoryData[]> {
313
+ const groups = new Map<string, UserStoryData[]>();
314
+
315
+ for (const story of userStories) {
316
+ const project = story.project || defaultProject || 'default';
317
+
318
+ if (!groups.has(project)) {
319
+ groups.set(project, []);
320
+ }
321
+ groups.get(project)!.push(story);
322
+ }
323
+
324
+ return groups;
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Format per-US sync results for display
330
+ */
331
+ export function formatPerUSSyncResults(result: PerUSSyncResult): string {
332
+ const lines: string[] = [];
333
+
334
+ lines.push('');
335
+ lines.push('📊 Per-US JIRA Sync Results');
336
+ lines.push('');
337
+
338
+ // Group by project
339
+ const byProject = new Map<string, USSyncResult[]>();
340
+ for (const r of [...result.synced, ...result.failed]) {
341
+ const existing = byProject.get(r.projectId) || [];
342
+ existing.push(r);
343
+ byProject.set(r.projectId, existing);
344
+ }
345
+
346
+ for (const [projectId, results] of byProject) {
347
+ lines.push(`**${projectId}** (→ ${results[0]?.jiraProject || 'N/A'}):`);
348
+ for (const r of results) {
349
+ const icon = r.action === 'created' ? '✅' :
350
+ r.action === 'updated' ? '🔄' :
351
+ r.error ? '❌' : '⏭️';
352
+ if (r.issueKey) {
353
+ lines.push(` ${icon} ${r.usId} → ${r.issueKey}`);
354
+ } else if (r.error) {
355
+ lines.push(` ${icon} ${r.usId}: ${r.error}`);
356
+ } else {
357
+ lines.push(` ${icon} ${r.usId} (${r.action})`);
358
+ }
359
+ }
360
+ lines.push('');
361
+ }
362
+
363
+ lines.push(`📈 Summary: ${result.summary.created} created, ${result.summary.updated} updated, ${result.summary.failed} failed`);
364
+
365
+ return lines.join('\n');
366
+ }