specweave 0.33.3 → 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.
- package/CLAUDE.md +77 -19
- package/dist/src/cli/cleanup-zombies.js +8 -5
- package/dist/src/cli/cleanup-zombies.js.map +1 -1
- package/dist/src/config/types.d.ts +203 -1208
- package/dist/src/config/types.d.ts.map +1 -1
- package/dist/src/importers/jira-importer.d.ts +10 -0
- package/dist/src/importers/jira-importer.d.ts.map +1 -1
- package/dist/src/importers/jira-importer.js +55 -5
- package/dist/src/importers/jira-importer.js.map +1 -1
- package/dist/src/init/architecture/types.d.ts +33 -140
- package/dist/src/init/architecture/types.d.ts.map +1 -1
- package/dist/src/init/compliance/types.d.ts +30 -27
- package/dist/src/init/compliance/types.d.ts.map +1 -1
- package/dist/src/init/repo/types.d.ts +11 -34
- package/dist/src/init/repo/types.d.ts.map +1 -1
- package/dist/src/init/research/src/config/types.d.ts +15 -82
- package/dist/src/init/research/src/config/types.d.ts.map +1 -1
- package/dist/src/init/research/types.d.ts +38 -93
- package/dist/src/init/research/types.d.ts.map +1 -1
- package/dist/src/init/team/types.d.ts +4 -42
- package/dist/src/init/team/types.d.ts.map +1 -1
- package/dist/src/sync/closure-metrics.d.ts +102 -0
- package/dist/src/sync/closure-metrics.d.ts.map +1 -0
- package/dist/src/sync/closure-metrics.js +267 -0
- package/dist/src/sync/closure-metrics.js.map +1 -0
- package/dist/src/sync/sync-coordinator.d.ts +29 -0
- package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
- package/dist/src/sync/sync-coordinator.js +153 -16
- package/dist/src/sync/sync-coordinator.js.map +1 -1
- package/dist/src/utils/notification-constants.d.ts +85 -0
- package/dist/src/utils/notification-constants.d.ts.map +1 -0
- package/dist/src/utils/notification-constants.js +129 -0
- package/dist/src/utils/notification-constants.js.map +1 -0
- package/dist/src/utils/platform-utils.d.ts +13 -3
- package/dist/src/utils/platform-utils.d.ts.map +1 -1
- package/dist/src/utils/platform-utils.js +17 -6
- package/dist/src/utils/platform-utils.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/commands/specweave-increment.md +46 -0
- package/plugins/specweave/commands/specweave-jobs.md +153 -8
- package/plugins/specweave/hooks/spec-project-validator.sh +24 -2
- package/plugins/specweave/hooks/universal/hook-wrapper.cmd +26 -26
- package/plugins/specweave/hooks/universal/session-start.cmd +16 -16
- package/plugins/specweave/hooks/universal/session-start.ps1 +16 -16
- package/plugins/specweave/scripts/session-watchdog.sh +278 -130
- package/plugins/specweave/skills/increment-planner/SKILL.md +48 -18
- package/plugins/specweave/skills/increment-planner/templates/spec-multi-project.md +27 -14
- package/plugins/specweave/skills/increment-planner/templates/spec-single-project.md +16 -5
- package/plugins/specweave/skills/spec-generator/SKILL.md +74 -15
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +0 -738
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +0 -1107
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Closure Metrics - Telemetry for External Tool Sync Operations (v0.34.0)
|
|
3
|
+
*
|
|
4
|
+
* Tracks closure success/failure rates for GitHub, JIRA, and ADO.
|
|
5
|
+
* Persists metrics to .specweave/state/closure-metrics.json
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const metrics = new ClosureMetrics(projectRoot);
|
|
9
|
+
* metrics.recordClosure('github', 100, true); // Success
|
|
10
|
+
* metrics.recordClosure('jira', 'TEST-123', false, 'Rate limited'); // Failure
|
|
11
|
+
* metrics.getSummary(); // Get aggregated stats
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
import { consoleLogger } from '../utils/logger.js';
|
|
16
|
+
const METRICS_VERSION = 1;
|
|
17
|
+
const MAX_RECORDS = 1000; // Keep last 1000 records
|
|
18
|
+
const MAX_RECENT_ERRORS = 10; // Keep last 10 errors per tool
|
|
19
|
+
export class ClosureMetrics {
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.operationStartTime = 0;
|
|
22
|
+
this.projectRoot = options.projectRoot;
|
|
23
|
+
this.currentIncrementId = options.incrementId || 'unknown';
|
|
24
|
+
this.logger = options.logger || consoleLogger;
|
|
25
|
+
const stateDir = join(this.projectRoot, '.specweave/state');
|
|
26
|
+
if (!existsSync(stateDir)) {
|
|
27
|
+
mkdirSync(stateDir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
this.metricsFile = join(stateDir, 'closure-metrics.json');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Start timing an operation
|
|
33
|
+
*/
|
|
34
|
+
startOperation() {
|
|
35
|
+
this.operationStartTime = Date.now();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Record a closure operation result
|
|
39
|
+
*/
|
|
40
|
+
recordClosure(tool, itemId, success, error) {
|
|
41
|
+
const durationMs = this.operationStartTime > 0
|
|
42
|
+
? Date.now() - this.operationStartTime
|
|
43
|
+
: undefined;
|
|
44
|
+
const record = {
|
|
45
|
+
tool,
|
|
46
|
+
itemId,
|
|
47
|
+
incrementId: this.currentIncrementId,
|
|
48
|
+
success,
|
|
49
|
+
error,
|
|
50
|
+
timestamp: new Date().toISOString(),
|
|
51
|
+
durationMs,
|
|
52
|
+
};
|
|
53
|
+
const metrics = this.loadMetrics();
|
|
54
|
+
metrics.records.push(record);
|
|
55
|
+
// Trim to max records
|
|
56
|
+
if (metrics.records.length > MAX_RECORDS) {
|
|
57
|
+
metrics.records = metrics.records.slice(-MAX_RECORDS);
|
|
58
|
+
}
|
|
59
|
+
// Update summary
|
|
60
|
+
this.updateSummary(metrics);
|
|
61
|
+
this.saveMetrics(metrics);
|
|
62
|
+
// Reset timer
|
|
63
|
+
this.operationStartTime = 0;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Record multiple closures in batch
|
|
67
|
+
*/
|
|
68
|
+
recordBatch(tool, results) {
|
|
69
|
+
const metrics = this.loadMetrics();
|
|
70
|
+
for (const result of results) {
|
|
71
|
+
const record = {
|
|
72
|
+
tool,
|
|
73
|
+
itemId: result.itemId,
|
|
74
|
+
incrementId: this.currentIncrementId,
|
|
75
|
+
success: result.success,
|
|
76
|
+
error: result.error,
|
|
77
|
+
timestamp: new Date().toISOString(),
|
|
78
|
+
};
|
|
79
|
+
metrics.records.push(record);
|
|
80
|
+
}
|
|
81
|
+
// Trim to max records
|
|
82
|
+
if (metrics.records.length > MAX_RECORDS) {
|
|
83
|
+
metrics.records = metrics.records.slice(-MAX_RECORDS);
|
|
84
|
+
}
|
|
85
|
+
this.updateSummary(metrics);
|
|
86
|
+
this.saveMetrics(metrics);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get metrics summary
|
|
90
|
+
*/
|
|
91
|
+
getSummary() {
|
|
92
|
+
const metrics = this.loadMetrics();
|
|
93
|
+
return metrics.summary;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get recent failures for alerting
|
|
97
|
+
*/
|
|
98
|
+
getRecentFailures(hours = 24) {
|
|
99
|
+
const metrics = this.loadMetrics();
|
|
100
|
+
const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
101
|
+
return metrics.records.filter(r => !r.success && r.timestamp >= cutoff);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Check if failure rate exceeds threshold (for alerting)
|
|
105
|
+
*/
|
|
106
|
+
isFailureRateHigh(tool, threshold = 0.2) {
|
|
107
|
+
const summary = this.getSummary();
|
|
108
|
+
const toolMetrics = summary[tool];
|
|
109
|
+
// Only alert if we have enough data (at least 5 attempts)
|
|
110
|
+
if (toolMetrics.totalAttempts < 5) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
const failureRate = 1 - toolMetrics.successRate;
|
|
114
|
+
return failureRate > threshold;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Format summary for display
|
|
118
|
+
*/
|
|
119
|
+
formatSummary() {
|
|
120
|
+
const summary = this.getSummary();
|
|
121
|
+
const lines = [];
|
|
122
|
+
lines.push('📊 Closure Sync Metrics');
|
|
123
|
+
lines.push('═══════════════════════════════════════');
|
|
124
|
+
const formatTool = (name, metrics) => {
|
|
125
|
+
const result = [];
|
|
126
|
+
if (metrics.totalAttempts === 0) {
|
|
127
|
+
result.push(` ${name}: No data`);
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
const successPct = (metrics.successRate * 100).toFixed(1);
|
|
131
|
+
const statusIcon = metrics.successRate >= 0.9 ? '✅' : metrics.successRate >= 0.7 ? '⚠️' : '❌';
|
|
132
|
+
result.push(` ${name}: ${statusIcon} ${successPct}% success (${metrics.successful}/${metrics.totalAttempts})`);
|
|
133
|
+
if (metrics.avgDurationMs) {
|
|
134
|
+
result.push(` Avg duration: ${metrics.avgDurationMs.toFixed(0)}ms`);
|
|
135
|
+
}
|
|
136
|
+
if (metrics.lastFailure && metrics.recentErrors.length > 0) {
|
|
137
|
+
result.push(` Last error: ${metrics.recentErrors[0]}`);
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
};
|
|
141
|
+
lines.push(...formatTool('GitHub', summary.github));
|
|
142
|
+
lines.push(...formatTool('JIRA ', summary.jira));
|
|
143
|
+
lines.push(...formatTool('ADO ', summary.ado));
|
|
144
|
+
lines.push('───────────────────────────────────────');
|
|
145
|
+
const overallPct = (summary.overall.successRate * 100).toFixed(1);
|
|
146
|
+
lines.push(` Overall: ${overallPct}% success (${summary.overall.successful}/${summary.overall.totalAttempts})`);
|
|
147
|
+
lines.push(` Last updated: ${summary.lastUpdated}`);
|
|
148
|
+
return lines.join('\n');
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Reset metrics (for testing or fresh start)
|
|
152
|
+
*/
|
|
153
|
+
reset() {
|
|
154
|
+
const metrics = this.createEmptyMetrics();
|
|
155
|
+
this.saveMetrics(metrics);
|
|
156
|
+
}
|
|
157
|
+
// ========================================================================
|
|
158
|
+
// Private Methods
|
|
159
|
+
// ========================================================================
|
|
160
|
+
loadMetrics() {
|
|
161
|
+
if (!existsSync(this.metricsFile)) {
|
|
162
|
+
return this.createEmptyMetrics();
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const content = readFileSync(this.metricsFile, 'utf-8');
|
|
166
|
+
const metrics = JSON.parse(content);
|
|
167
|
+
// Version migration if needed
|
|
168
|
+
if (metrics.version !== METRICS_VERSION) {
|
|
169
|
+
this.logger.log('📊 Migrating metrics to new version...');
|
|
170
|
+
return this.createEmptyMetrics();
|
|
171
|
+
}
|
|
172
|
+
return metrics;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
this.logger.log(`⚠️ Failed to load metrics: ${error}`);
|
|
176
|
+
return this.createEmptyMetrics();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
saveMetrics(metrics) {
|
|
180
|
+
try {
|
|
181
|
+
writeFileSync(this.metricsFile, JSON.stringify(metrics, null, 2));
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
this.logger.log(`⚠️ Failed to save metrics: ${error}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
createEmptyMetrics() {
|
|
188
|
+
const emptyToolMetrics = {
|
|
189
|
+
totalAttempts: 0,
|
|
190
|
+
successful: 0,
|
|
191
|
+
failed: 0,
|
|
192
|
+
successRate: 1,
|
|
193
|
+
recentErrors: [],
|
|
194
|
+
};
|
|
195
|
+
return {
|
|
196
|
+
version: METRICS_VERSION,
|
|
197
|
+
records: [],
|
|
198
|
+
summary: {
|
|
199
|
+
github: { ...emptyToolMetrics },
|
|
200
|
+
jira: { ...emptyToolMetrics },
|
|
201
|
+
ado: { ...emptyToolMetrics },
|
|
202
|
+
overall: {
|
|
203
|
+
totalAttempts: 0,
|
|
204
|
+
successful: 0,
|
|
205
|
+
failed: 0,
|
|
206
|
+
successRate: 1,
|
|
207
|
+
},
|
|
208
|
+
lastUpdated: new Date().toISOString(),
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
updateSummary(metrics) {
|
|
213
|
+
const tools = ['github', 'jira', 'ado'];
|
|
214
|
+
// Calculate per-tool metrics
|
|
215
|
+
for (const tool of tools) {
|
|
216
|
+
const toolRecords = metrics.records.filter(r => r.tool === tool);
|
|
217
|
+
const successful = toolRecords.filter(r => r.success).length;
|
|
218
|
+
const failed = toolRecords.filter(r => !r.success).length;
|
|
219
|
+
const total = toolRecords.length;
|
|
220
|
+
// Get durations for average calculation
|
|
221
|
+
const durations = toolRecords
|
|
222
|
+
.filter(r => r.durationMs !== undefined)
|
|
223
|
+
.map(r => r.durationMs);
|
|
224
|
+
const avgDurationMs = durations.length > 0
|
|
225
|
+
? durations.reduce((a, b) => a + b, 0) / durations.length
|
|
226
|
+
: undefined;
|
|
227
|
+
// Get recent errors
|
|
228
|
+
const recentErrors = toolRecords
|
|
229
|
+
.filter(r => !r.success && r.error)
|
|
230
|
+
.slice(-MAX_RECENT_ERRORS)
|
|
231
|
+
.map(r => r.error)
|
|
232
|
+
.reverse();
|
|
233
|
+
// Find last success/failure timestamps
|
|
234
|
+
const lastSuccessRecord = toolRecords.filter(r => r.success).pop();
|
|
235
|
+
const lastFailureRecord = toolRecords.filter(r => !r.success).pop();
|
|
236
|
+
metrics.summary[tool] = {
|
|
237
|
+
totalAttempts: total,
|
|
238
|
+
successful,
|
|
239
|
+
failed,
|
|
240
|
+
successRate: total > 0 ? successful / total : 1,
|
|
241
|
+
lastSuccess: lastSuccessRecord?.timestamp,
|
|
242
|
+
lastFailure: lastFailureRecord?.timestamp,
|
|
243
|
+
avgDurationMs,
|
|
244
|
+
recentErrors,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
// Calculate overall metrics
|
|
248
|
+
const allRecords = metrics.records;
|
|
249
|
+
const totalSuccessful = allRecords.filter(r => r.success).length;
|
|
250
|
+
const totalFailed = allRecords.filter(r => !r.success).length;
|
|
251
|
+
const totalAttempts = allRecords.length;
|
|
252
|
+
metrics.summary.overall = {
|
|
253
|
+
totalAttempts,
|
|
254
|
+
successful: totalSuccessful,
|
|
255
|
+
failed: totalFailed,
|
|
256
|
+
successRate: totalAttempts > 0 ? totalSuccessful / totalAttempts : 1,
|
|
257
|
+
};
|
|
258
|
+
metrics.summary.lastUpdated = new Date().toISOString();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Factory function for creating metrics instance
|
|
263
|
+
*/
|
|
264
|
+
export function createClosureMetrics(projectRoot, incrementId, logger) {
|
|
265
|
+
return new ClosureMetrics({ projectRoot, incrementId, logger });
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=closure-metrics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"closure-metrics.js","sourceRoot":"","sources":["../../../src/sync/closure-metrics.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { GitHubIssue } from '../../plugins/specweave-github/lib/types.js';
|
|
8
8
|
import { Logger } from '../utils/logger.js';
|
|
9
9
|
import { ResolvedAdoProfile } from '../integrations/ado/ado-client-factory.js';
|
|
10
|
+
import { ClosureMetrics } from './closure-metrics.js';
|
|
10
11
|
export interface SyncCoordinatorOptions {
|
|
11
12
|
projectRoot: string;
|
|
12
13
|
incrementId: string;
|
|
@@ -30,7 +31,19 @@ export declare class SyncCoordinator {
|
|
|
30
31
|
private frontmatterUpdater;
|
|
31
32
|
private projectId;
|
|
32
33
|
private adoProfile?;
|
|
34
|
+
private metrics;
|
|
33
35
|
constructor(options: SyncCoordinatorOptions);
|
|
36
|
+
/**
|
|
37
|
+
* Get closure sync metrics summary (v0.34.0)
|
|
38
|
+
*
|
|
39
|
+
* Returns aggregated metrics for all external tool closure operations.
|
|
40
|
+
* Useful for monitoring and alerting on sync health.
|
|
41
|
+
*/
|
|
42
|
+
getClosureMetrics(): ReturnType<ClosureMetrics['getSummary']>;
|
|
43
|
+
/**
|
|
44
|
+
* Get formatted closure metrics for display (v0.34.0)
|
|
45
|
+
*/
|
|
46
|
+
getFormattedClosureMetrics(): string;
|
|
34
47
|
/**
|
|
35
48
|
* Create GitHub issues for all User Stories in the feature (NEW in v0.25.0)
|
|
36
49
|
*
|
|
@@ -99,6 +112,22 @@ export declare class SyncCoordinator {
|
|
|
99
112
|
syncIncrementClosure(): Promise<SyncResult & {
|
|
100
113
|
closedIssues: number[];
|
|
101
114
|
}>;
|
|
115
|
+
/**
|
|
116
|
+
* Detect duplicate issues with different feature ID formats (v0.34.0)
|
|
117
|
+
*
|
|
118
|
+
* Searches for issues that match the user story but have a different feature ID format.
|
|
119
|
+
* This prevents creating duplicates like:
|
|
120
|
+
* - [FS-0128][US-001] (old bug format with leading zeros)
|
|
121
|
+
* - [FS-128][US-001] (correct format)
|
|
122
|
+
*
|
|
123
|
+
* The search uses a regex pattern that matches any FS-XXX format for the same US-XXX.
|
|
124
|
+
*
|
|
125
|
+
* @param client - GitHub client
|
|
126
|
+
* @param featureId - Current feature ID (e.g., "FS-128")
|
|
127
|
+
* @param userStoryId - User story ID (e.g., "US-001")
|
|
128
|
+
* @returns Duplicate info if found, null otherwise
|
|
129
|
+
*/
|
|
130
|
+
private detectDuplicateIssue;
|
|
102
131
|
/**
|
|
103
132
|
* Update issue if it has placeholder content
|
|
104
133
|
* Fetches issue from GitHub, checks for placeholder, and updates with rich content
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-coordinator.d.ts","sourceRoot":"","sources":["../../../src/sync/sync-coordinator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,OAAO,EAAE,WAAW,EAAE,MAAM,6CAA6C,CAAC;AAC1E,OAAO,EAAE,MAAM,EAAiB,MAAM,oBAAoB,CAAC;AAK3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"sync-coordinator.d.ts","sourceRoot":"","sources":["../../../src/sync/sync-coordinator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,OAAO,EAAE,WAAW,EAAE,MAAM,6CAA6C,CAAC;AAC1E,OAAO,EAAE,MAAM,EAAiB,MAAM,oBAAoB,CAAC;AAK3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2CAA2C,CAAC;AAG/E,OAAO,EAAE,cAAc,EAAwB,MAAM,sBAAsB,CAAC;AAE5E,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,UAAU,CAAC,EAAE,kBAAkB,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,cAAc,GAAG,WAAW,GAAG,WAAW,GAAG,aAAa,GAAG,kBAAkB,GAAG,mBAAmB,CAAC;IAChH,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAC,CAAqB;IACxC,OAAO,CAAC,OAAO,CAAiB;gBAEpB,OAAO,EAAE,sBAAsB;IAa3C;;;;;OAKG;IACH,iBAAiB,IAAI,UAAU,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;IAI7D;;OAEG;IACH,0BAA0B,IAAI,MAAM;IAIpC;;;;;;;;;;;;;OAaG;IACG,gCAAgC,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAqU3E;;;;;;;;;;;;;;;OAeG;IACG,+BAA+B,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IA0HrE;;;;;;;;OAQG;IACG,6BAA6B,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC;IA8EjE;;;;;;;;OAQG;IACG,+BAA+B,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC;IAyGnE;;;;;;;;;;;;OAYG;IACG,oBAAoB,IAAI,OAAO,CAAC,UAAU,GAAG;QAAE,YAAY,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAqI9E;;;;;;;;;;;;;;OAcG;YACW,oBAAoB;IAsDlC;;;OAGG;YACW,wBAAwB;IA2DtC;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAuB3B;;OAEG;IACG,uBAAuB,IAAI,OAAO,CAAC,UAAU,CAAC;IA6HpD;;OAEG;YACW,aAAa;IAwG3B;;OAEG;YACW,kBAAkB;IAmEhC;;OAEG;YACW,2BAA2B;IA6FzC;;OAEG;YACW,UAAU;IAWxB;;OAEG;YACW,gBAAgB;IA0B9B;;OAEG;IACH,OAAO,CAAC,0BAA0B;CAyCnC"}
|
|
@@ -17,6 +17,7 @@ import { UserStoryContentBuilder } from '../../plugins/specweave-github/lib/user
|
|
|
17
17
|
import { AdoClient } from '../integrations/ado/ado-client.js';
|
|
18
18
|
import { getAdoPat } from '../integrations/ado/ado-pat-provider.js';
|
|
19
19
|
import { deriveFeatureId } from '../utils/feature-id-derivation.js';
|
|
20
|
+
import { createClosureMetrics } from './closure-metrics.js';
|
|
20
21
|
export class SyncCoordinator {
|
|
21
22
|
constructor(options) {
|
|
22
23
|
this.projectRoot = options.projectRoot;
|
|
@@ -27,6 +28,23 @@ export class SyncCoordinator {
|
|
|
27
28
|
this.projectId = autoDetectProjectIdSync(this.projectRoot, { silent: true });
|
|
28
29
|
// Store resolved ADO profile for multi-project sync
|
|
29
30
|
this.adoProfile = options.adoProfile;
|
|
31
|
+
// Initialize closure metrics (v0.34.0)
|
|
32
|
+
this.metrics = createClosureMetrics(this.projectRoot, this.incrementId, this.logger);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get closure sync metrics summary (v0.34.0)
|
|
36
|
+
*
|
|
37
|
+
* Returns aggregated metrics for all external tool closure operations.
|
|
38
|
+
* Useful for monitoring and alerting on sync health.
|
|
39
|
+
*/
|
|
40
|
+
getClosureMetrics() {
|
|
41
|
+
return this.metrics.getSummary();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get formatted closure metrics for display (v0.34.0)
|
|
45
|
+
*/
|
|
46
|
+
getFormattedClosureMetrics() {
|
|
47
|
+
return this.metrics.formatSummary();
|
|
30
48
|
}
|
|
31
49
|
/**
|
|
32
50
|
* Create GitHub issues for all User Stories in the feature (NEW in v0.25.0)
|
|
@@ -191,7 +209,32 @@ export class SyncCoordinator {
|
|
|
191
209
|
}
|
|
192
210
|
continue;
|
|
193
211
|
}
|
|
194
|
-
// All 3 layers miss -
|
|
212
|
+
// All 3 layers miss - but check for DUPLICATES with wrong format first!
|
|
213
|
+
// ========================================================================
|
|
214
|
+
// DUPLICATE DETECTION (v0.34.0): Prevent FS-0128 vs FS-128 duplicates
|
|
215
|
+
// ========================================================================
|
|
216
|
+
// Before creating, search for issues with similar titles but different feature ID formats.
|
|
217
|
+
// This catches cases where an old bug created issues with leading zeros (FS-0128)
|
|
218
|
+
// but the correct format is without (FS-128).
|
|
219
|
+
const duplicateCheck = await this.detectDuplicateIssue(client, featureId, usFile.id);
|
|
220
|
+
if (duplicateCheck) {
|
|
221
|
+
this.logger.log(` ⚠️ DUPLICATE DETECTED: Issue #${duplicateCheck.number} exists with format "${duplicateCheck.format}"`);
|
|
222
|
+
this.logger.log(` Current format: [${featureId}][${usFile.id}]`);
|
|
223
|
+
this.logger.log(` Existing issue: ${duplicateCheck.title}`);
|
|
224
|
+
this.logger.log(` ⏭️ Skipping creation to avoid duplicate. Use existing issue #${duplicateCheck.number}`);
|
|
225
|
+
// Backfill metadata with the existing issue (even if wrong format)
|
|
226
|
+
await this.frontmatterUpdater.updateUserStoryFrontmatter({
|
|
227
|
+
projectRoot: this.projectRoot,
|
|
228
|
+
featureId,
|
|
229
|
+
userStoryId: usFile.id,
|
|
230
|
+
githubIssue: {
|
|
231
|
+
number: duplicateCheck.number,
|
|
232
|
+
url: duplicateCheck.url,
|
|
233
|
+
createdAt: new Date().toISOString(),
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
195
238
|
this.logger.log(` 📝 Creating GitHub issue for ${usFile.id}...`);
|
|
196
239
|
// Format issue body - use UserStoryContentBuilder for rich content with ACs
|
|
197
240
|
let issueBody;
|
|
@@ -369,9 +412,18 @@ Increment \`${this.incrementId}\` has been marked as **completed**.
|
|
|
369
412
|
|
|
370
413
|
---
|
|
371
414
|
🤖 Auto-closed by SpecWeave on increment completion`;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
415
|
+
// Track metrics (v0.34.0)
|
|
416
|
+
this.metrics.startOperation();
|
|
417
|
+
try {
|
|
418
|
+
await client.closeIssue(existingIssue.number, completionComment);
|
|
419
|
+
closedIssues.push(existingIssue.number);
|
|
420
|
+
this.metrics.recordClosure('github', existingIssue.number, true);
|
|
421
|
+
this.logger.log(` ✅ Closed issue #${existingIssue.number}`);
|
|
422
|
+
}
|
|
423
|
+
catch (closeError) {
|
|
424
|
+
this.metrics.recordClosure('github', existingIssue.number, false, String(closeError));
|
|
425
|
+
throw closeError;
|
|
426
|
+
}
|
|
375
427
|
}
|
|
376
428
|
catch (error) {
|
|
377
429
|
this.logger.error(` ❌ Failed to close issue for ${usFile.id}:`, error);
|
|
@@ -441,12 +493,21 @@ Increment \`${this.incrementId}\` has been marked as **completed**.
|
|
|
441
493
|
}
|
|
442
494
|
// Transition to Done
|
|
443
495
|
this.logger.log(` 🔒 Transitioning JIRA ${jiraKey} to ${targetStatus}...`);
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
496
|
+
// Track metrics (v0.34.0)
|
|
497
|
+
this.metrics.startOperation();
|
|
498
|
+
try {
|
|
499
|
+
await jiraClient.updateIssue({
|
|
500
|
+
key: jiraKey,
|
|
501
|
+
status: targetStatus
|
|
502
|
+
});
|
|
503
|
+
this.metrics.recordClosure('jira', jiraKey, true);
|
|
504
|
+
this.logger.log(` ✅ Transitioned ${jiraKey}`);
|
|
505
|
+
closedCount++;
|
|
506
|
+
}
|
|
507
|
+
catch (updateError) {
|
|
508
|
+
this.metrics.recordClosure('jira', jiraKey, false, String(updateError));
|
|
509
|
+
throw updateError;
|
|
510
|
+
}
|
|
450
511
|
}
|
|
451
512
|
catch (error) {
|
|
452
513
|
this.logger.error(` ❌ Failed to close JIRA issue for ${usFile.id}:`, error);
|
|
@@ -534,12 +595,21 @@ Increment \`${this.incrementId}\` has been marked as **completed**.
|
|
|
534
595
|
}
|
|
535
596
|
// Update to Closed state
|
|
536
597
|
this.logger.log(` 🔒 Closing ADO work item #${workItemId}...`);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
598
|
+
// Track metrics (v0.34.0)
|
|
599
|
+
this.metrics.startOperation();
|
|
600
|
+
try {
|
|
601
|
+
await adoClient.updateWorkItem({
|
|
602
|
+
id: workItemId,
|
|
603
|
+
state: targetState
|
|
604
|
+
});
|
|
605
|
+
this.metrics.recordClosure('ado', workItemId, true);
|
|
606
|
+
this.logger.log(` ✅ Closed #${workItemId}`);
|
|
607
|
+
closedCount++;
|
|
608
|
+
}
|
|
609
|
+
catch (updateError) {
|
|
610
|
+
this.metrics.recordClosure('ado', workItemId, false, String(updateError));
|
|
611
|
+
throw updateError;
|
|
612
|
+
}
|
|
543
613
|
}
|
|
544
614
|
catch (error) {
|
|
545
615
|
this.logger.error(` ❌ Failed to close ADO work item for ${usId}:`, error);
|
|
@@ -670,6 +740,13 @@ Increment \`${this.incrementId}\` has been marked as **completed**.
|
|
|
670
740
|
if (result.errors.length > 0) {
|
|
671
741
|
this.logger.log(` ⚠️ ${result.errors.length} error(s) occurred`);
|
|
672
742
|
}
|
|
743
|
+
// Check for high failure rate and warn (v0.34.0 metrics)
|
|
744
|
+
const tools = ['github', 'jira', 'ado'];
|
|
745
|
+
for (const tool of tools) {
|
|
746
|
+
if (this.metrics.isFailureRateHigh(tool)) {
|
|
747
|
+
this.logger.log(` ⚠️ ${tool.toUpperCase()} has high failure rate - check credentials/permissions`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
673
750
|
return result;
|
|
674
751
|
}
|
|
675
752
|
catch (error) {
|
|
@@ -678,6 +755,66 @@ Increment \`${this.incrementId}\` has been marked as **completed**.
|
|
|
678
755
|
return result;
|
|
679
756
|
}
|
|
680
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* Detect duplicate issues with different feature ID formats (v0.34.0)
|
|
760
|
+
*
|
|
761
|
+
* Searches for issues that match the user story but have a different feature ID format.
|
|
762
|
+
* This prevents creating duplicates like:
|
|
763
|
+
* - [FS-0128][US-001] (old bug format with leading zeros)
|
|
764
|
+
* - [FS-128][US-001] (correct format)
|
|
765
|
+
*
|
|
766
|
+
* The search uses a regex pattern that matches any FS-XXX format for the same US-XXX.
|
|
767
|
+
*
|
|
768
|
+
* @param client - GitHub client
|
|
769
|
+
* @param featureId - Current feature ID (e.g., "FS-128")
|
|
770
|
+
* @param userStoryId - User story ID (e.g., "US-001")
|
|
771
|
+
* @returns Duplicate info if found, null otherwise
|
|
772
|
+
*/
|
|
773
|
+
async detectDuplicateIssue(client, featureId, userStoryId) {
|
|
774
|
+
try {
|
|
775
|
+
// Extract the numeric part of the feature ID (e.g., "128" from "FS-128" or "FS-0128")
|
|
776
|
+
const featureNumMatch = featureId.match(/FS-0*(\d+)E?/i);
|
|
777
|
+
if (!featureNumMatch) {
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
const featureNum = featureNumMatch[1];
|
|
781
|
+
// Search for issues with ANY format of this feature ID + user story
|
|
782
|
+
// Patterns to check:
|
|
783
|
+
// - [FS-128][US-001] (correct, no leading zeros)
|
|
784
|
+
// - [FS-0128][US-001] (bug format, with leading zeros)
|
|
785
|
+
// - [FS-00128][US-001] (edge case, multiple leading zeros)
|
|
786
|
+
const searchPatterns = [
|
|
787
|
+
`[FS-${featureNum}][${userStoryId}]`, // FS-128
|
|
788
|
+
`[FS-0${featureNum}][${userStoryId}]`, // FS-0128
|
|
789
|
+
`[FS-00${featureNum}][${userStoryId}]`, // FS-00128
|
|
790
|
+
`[FS-${featureNum}E][${userStoryId}]`, // FS-128E (external)
|
|
791
|
+
`[FS-0${featureNum}E][${userStoryId}]`, // FS-0128E
|
|
792
|
+
];
|
|
793
|
+
// Filter out the exact current format (we already checked that)
|
|
794
|
+
const currentFormat = `[${featureId}][${userStoryId}]`;
|
|
795
|
+
const alternatePatterns = searchPatterns.filter(p => p !== currentFormat);
|
|
796
|
+
for (const pattern of alternatePatterns) {
|
|
797
|
+
const existingIssue = await client.searchIssueByTitle(pattern, true); // Include closed
|
|
798
|
+
if (existingIssue) {
|
|
799
|
+
// Extract the format from the issue title
|
|
800
|
+
const formatMatch = existingIssue.title.match(/\[(FS-\d+E?)\]/i);
|
|
801
|
+
const detectedFormat = formatMatch ? formatMatch[1] : 'unknown';
|
|
802
|
+
return {
|
|
803
|
+
number: existingIssue.number,
|
|
804
|
+
url: existingIssue.html_url,
|
|
805
|
+
title: existingIssue.title,
|
|
806
|
+
format: detectedFormat,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
catch (error) {
|
|
813
|
+
// Don't block issue creation on duplicate detection failure
|
|
814
|
+
this.logger.log(` ⚠️ Duplicate detection failed (non-blocking): ${error}`);
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
681
818
|
/**
|
|
682
819
|
* Update issue if it has placeholder content
|
|
683
820
|
* Fetches issue from GitHub, checks for placeholder, and updates with rich content
|