specweave 0.16.5 → 0.17.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.
- package/bin/fix-marketplace-errors.sh +136 -0
- package/dist/cli/helpers/issue-tracker/index.d.ts.map +1 -1
- package/dist/cli/helpers/issue-tracker/index.js +21 -0
- package/dist/cli/helpers/issue-tracker/index.js.map +1 -1
- package/package.json +2 -2
- package/plugins/specweave-ado/agents/ado-multi-project-mapper/AGENT.md +521 -0
- package/plugins/specweave-ado/agents/ado-sync-judge/AGENT.md +418 -0
- package/plugins/specweave-ado/hooks/post-living-docs-update.sh +353 -0
- package/plugins/specweave-ado/lib/ado-project-detector.js +469 -0
- package/plugins/specweave-ado/lib/ado-project-detector.ts +510 -0
- package/plugins/specweave-ado/lib/conflict-resolver.js +297 -0
- package/plugins/specweave-ado/lib/conflict-resolver.ts +443 -0
- package/plugins/specweave-ado/skills/ado-multi-project/SKILL.md +541 -0
- package/plugins/specweave-ado/skills/ado-resource-validator/SKILL.md +719 -0
- package/src/templates/CLAUDE.md.template +24 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict Resolver for Living Docs Synchronization
|
|
3
|
+
*
|
|
4
|
+
* CRITICAL PRINCIPLE: External tool status ALWAYS wins in conflicts!
|
|
5
|
+
* This ensures that QA and stakeholder decisions in external tools
|
|
6
|
+
* take precedence over local development status.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from 'fs-extra';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as yaml from 'yaml';
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Types
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
export interface ConflictResolution {
|
|
18
|
+
field: string;
|
|
19
|
+
localValue: any;
|
|
20
|
+
externalValue: any;
|
|
21
|
+
resolution: 'external' | 'local';
|
|
22
|
+
resolvedValue: any;
|
|
23
|
+
reason: string;
|
|
24
|
+
timestamp: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SpecMetadata {
|
|
28
|
+
id: string;
|
|
29
|
+
title: string;
|
|
30
|
+
status: SpecStatus;
|
|
31
|
+
priority?: Priority;
|
|
32
|
+
externalLinks?: {
|
|
33
|
+
ado?: {
|
|
34
|
+
featureId: number;
|
|
35
|
+
featureUrl: string;
|
|
36
|
+
syncedAt?: string;
|
|
37
|
+
lastExternalStatus?: string;
|
|
38
|
+
};
|
|
39
|
+
jira?: {
|
|
40
|
+
issueKey: string;
|
|
41
|
+
issueUrl: string;
|
|
42
|
+
syncedAt?: string;
|
|
43
|
+
lastExternalStatus?: string;
|
|
44
|
+
};
|
|
45
|
+
github?: {
|
|
46
|
+
issue: number;
|
|
47
|
+
url: string;
|
|
48
|
+
syncedAt?: string;
|
|
49
|
+
lastExternalStatus?: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type SpecStatus =
|
|
55
|
+
| 'draft'
|
|
56
|
+
| 'in-progress'
|
|
57
|
+
| 'implemented'
|
|
58
|
+
| 'in-qa'
|
|
59
|
+
| 'complete'
|
|
60
|
+
| 'blocked'
|
|
61
|
+
| 'cancelled';
|
|
62
|
+
|
|
63
|
+
export type Priority = 'P0' | 'P1' | 'P2' | 'P3';
|
|
64
|
+
|
|
65
|
+
export interface ExternalStatus {
|
|
66
|
+
tool: 'ado' | 'jira' | 'github';
|
|
67
|
+
status: string;
|
|
68
|
+
mappedStatus: SpecStatus;
|
|
69
|
+
priority?: string;
|
|
70
|
+
lastModified: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Status Mapping
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
const STATUS_MAPPING = {
|
|
78
|
+
ado: {
|
|
79
|
+
'New': 'draft' as SpecStatus,
|
|
80
|
+
'Active': 'in-progress' as SpecStatus,
|
|
81
|
+
'Resolved': 'implemented' as SpecStatus,
|
|
82
|
+
'Closed': 'complete' as SpecStatus,
|
|
83
|
+
'In Review': 'in-qa' as SpecStatus,
|
|
84
|
+
'In QA': 'in-qa' as SpecStatus,
|
|
85
|
+
'Blocked': 'blocked' as SpecStatus,
|
|
86
|
+
'Removed': 'cancelled' as SpecStatus
|
|
87
|
+
},
|
|
88
|
+
jira: {
|
|
89
|
+
'To Do': 'draft' as SpecStatus,
|
|
90
|
+
'In Progress': 'in-progress' as SpecStatus,
|
|
91
|
+
'Code Review': 'implemented' as SpecStatus,
|
|
92
|
+
'In Review': 'implemented' as SpecStatus,
|
|
93
|
+
'QA': 'in-qa' as SpecStatus,
|
|
94
|
+
'Testing': 'in-qa' as SpecStatus,
|
|
95
|
+
'Done': 'complete' as SpecStatus,
|
|
96
|
+
'Closed': 'complete' as SpecStatus,
|
|
97
|
+
'Blocked': 'blocked' as SpecStatus,
|
|
98
|
+
'Cancelled': 'cancelled' as SpecStatus
|
|
99
|
+
},
|
|
100
|
+
github: {
|
|
101
|
+
'open': 'in-progress' as SpecStatus,
|
|
102
|
+
'closed': 'complete' as SpecStatus
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const REVERSE_STATUS_MAPPING = {
|
|
107
|
+
ado: {
|
|
108
|
+
'draft': 'New',
|
|
109
|
+
'in-progress': 'Active',
|
|
110
|
+
'implemented': 'Resolved',
|
|
111
|
+
'in-qa': 'In QA',
|
|
112
|
+
'complete': 'Closed',
|
|
113
|
+
'blocked': 'Blocked',
|
|
114
|
+
'cancelled': 'Removed'
|
|
115
|
+
},
|
|
116
|
+
jira: {
|
|
117
|
+
'draft': 'To Do',
|
|
118
|
+
'in-progress': 'In Progress',
|
|
119
|
+
'implemented': 'Code Review',
|
|
120
|
+
'in-qa': 'QA',
|
|
121
|
+
'complete': 'Done',
|
|
122
|
+
'blocked': 'Blocked',
|
|
123
|
+
'cancelled': 'Cancelled'
|
|
124
|
+
},
|
|
125
|
+
github: {
|
|
126
|
+
'draft': 'open',
|
|
127
|
+
'in-progress': 'open',
|
|
128
|
+
'implemented': 'open',
|
|
129
|
+
'in-qa': 'open',
|
|
130
|
+
'complete': 'closed',
|
|
131
|
+
'blocked': 'open',
|
|
132
|
+
'cancelled': 'closed'
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Conflict Resolver Class
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
export class ConflictResolver {
|
|
141
|
+
private resolutionLog: ConflictResolution[] = [];
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Map external status to local SpecWeave status
|
|
145
|
+
*/
|
|
146
|
+
public mapExternalStatus(tool: 'ado' | 'jira' | 'github', externalStatus: string): SpecStatus {
|
|
147
|
+
const mapping = STATUS_MAPPING[tool];
|
|
148
|
+
return mapping[externalStatus] || 'unknown' as SpecStatus;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Map local status to external tool status
|
|
153
|
+
*/
|
|
154
|
+
public mapLocalStatus(tool: 'ado' | 'jira' | 'github', localStatus: SpecStatus): string {
|
|
155
|
+
const mapping = REVERSE_STATUS_MAPPING[tool];
|
|
156
|
+
return mapping[localStatus] || 'Active';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* CRITICAL: Resolve status conflict - EXTERNAL ALWAYS WINS
|
|
161
|
+
*/
|
|
162
|
+
public resolveStatusConflict(
|
|
163
|
+
localStatus: SpecStatus,
|
|
164
|
+
externalStatus: ExternalStatus
|
|
165
|
+
): ConflictResolution {
|
|
166
|
+
const resolution: ConflictResolution = {
|
|
167
|
+
field: 'status',
|
|
168
|
+
localValue: localStatus,
|
|
169
|
+
externalValue: externalStatus.status,
|
|
170
|
+
resolution: 'external', // ALWAYS external for status
|
|
171
|
+
resolvedValue: externalStatus.mappedStatus,
|
|
172
|
+
reason: 'External tool reflects QA and stakeholder decisions',
|
|
173
|
+
timestamp: new Date().toISOString()
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Log the resolution
|
|
177
|
+
console.log(`📊 Status Conflict Detected:`);
|
|
178
|
+
console.log(` Local: ${localStatus}`);
|
|
179
|
+
console.log(` External: ${externalStatus.status} (${externalStatus.tool})`);
|
|
180
|
+
console.log(` ✅ Resolution: EXTERNAL WINS - ${externalStatus.mappedStatus}`);
|
|
181
|
+
|
|
182
|
+
this.resolutionLog.push(resolution);
|
|
183
|
+
return resolution;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Resolve priority conflict - EXTERNAL WINS
|
|
188
|
+
*/
|
|
189
|
+
public resolvePriorityConflict(
|
|
190
|
+
localPriority: Priority | undefined,
|
|
191
|
+
externalPriority: string | undefined
|
|
192
|
+
): ConflictResolution {
|
|
193
|
+
const resolution: ConflictResolution = {
|
|
194
|
+
field: 'priority',
|
|
195
|
+
localValue: localPriority,
|
|
196
|
+
externalValue: externalPriority,
|
|
197
|
+
resolution: 'external',
|
|
198
|
+
resolvedValue: externalPriority || localPriority,
|
|
199
|
+
reason: 'External tool reflects stakeholder prioritization',
|
|
200
|
+
timestamp: new Date().toISOString()
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
if (localPriority !== externalPriority && externalPriority) {
|
|
204
|
+
console.log(`📊 Priority Conflict Detected:`);
|
|
205
|
+
console.log(` Local: ${localPriority}`);
|
|
206
|
+
console.log(` External: ${externalPriority}`);
|
|
207
|
+
console.log(` ✅ Resolution: EXTERNAL WINS - ${externalPriority}`);
|
|
208
|
+
this.resolutionLog.push(resolution);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return resolution;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Apply conflict resolutions to spec
|
|
216
|
+
*/
|
|
217
|
+
public async applyResolutions(
|
|
218
|
+
specPath: string,
|
|
219
|
+
resolutions: ConflictResolution[]
|
|
220
|
+
): Promise<void> {
|
|
221
|
+
const content = await fs.readFile(specPath, 'utf-8');
|
|
222
|
+
const lines = content.split('\n');
|
|
223
|
+
let inFrontmatter = false;
|
|
224
|
+
let frontmatterEnd = -1;
|
|
225
|
+
|
|
226
|
+
// Find frontmatter boundaries
|
|
227
|
+
for (let i = 0; i < lines.length; i++) {
|
|
228
|
+
if (lines[i] === '---') {
|
|
229
|
+
if (!inFrontmatter) {
|
|
230
|
+
inFrontmatter = true;
|
|
231
|
+
} else {
|
|
232
|
+
frontmatterEnd = i;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Apply resolutions
|
|
239
|
+
for (const resolution of resolutions) {
|
|
240
|
+
if (resolution.field === 'status') {
|
|
241
|
+
// Update status in frontmatter
|
|
242
|
+
for (let i = 1; i < frontmatterEnd; i++) {
|
|
243
|
+
if (lines[i].startsWith('status:')) {
|
|
244
|
+
lines[i] = `status: ${resolution.resolvedValue}`;
|
|
245
|
+
console.log(`✅ Applied status resolution: ${resolution.resolvedValue}`);
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Add sync metadata
|
|
251
|
+
const syncTimestamp = new Date().toISOString();
|
|
252
|
+
let syncedAtFound = false;
|
|
253
|
+
|
|
254
|
+
for (let i = 1; i < frontmatterEnd; i++) {
|
|
255
|
+
if (lines[i].includes('syncedAt:')) {
|
|
256
|
+
lines[i] = ` syncedAt: "${syncTimestamp}"`;
|
|
257
|
+
syncedAtFound = true;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!syncedAtFound) {
|
|
263
|
+
// Add syncedAt after externalLinks
|
|
264
|
+
for (let i = 1; i < frontmatterEnd; i++) {
|
|
265
|
+
if (lines[i].includes('externalLinks:')) {
|
|
266
|
+
// Find the external tool section
|
|
267
|
+
for (let j = i + 1; j < frontmatterEnd; j++) {
|
|
268
|
+
if (lines[j].includes('ado:') || lines[j].includes('jira:') || lines[j].includes('github:')) {
|
|
269
|
+
// Insert after the URL line
|
|
270
|
+
for (let k = j + 1; k < frontmatterEnd; k++) {
|
|
271
|
+
if (lines[k].includes('Url:')) {
|
|
272
|
+
lines.splice(k + 1, 0, ` syncedAt: "${syncTimestamp}"`);
|
|
273
|
+
frontmatterEnd++; // Adjust for inserted line
|
|
274
|
+
syncedAtFound = true;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (syncedAtFound) break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (syncedAtFound) break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} else if (resolution.field === 'priority' && resolution.resolvedValue) {
|
|
286
|
+
// Update priority in frontmatter
|
|
287
|
+
for (let i = 1; i < frontmatterEnd; i++) {
|
|
288
|
+
if (lines[i].startsWith('priority:')) {
|
|
289
|
+
lines[i] = `priority: ${resolution.resolvedValue}`;
|
|
290
|
+
console.log(`✅ Applied priority resolution: ${resolution.resolvedValue}`);
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Write updated content
|
|
298
|
+
await fs.writeFile(specPath, lines.join('\n'));
|
|
299
|
+
console.log(`✅ Resolutions applied to ${path.basename(specPath)}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Validate that external status wins in implementation
|
|
304
|
+
*/
|
|
305
|
+
public validateImplementation(
|
|
306
|
+
implementationCode: string
|
|
307
|
+
): { valid: boolean; violations: string[] } {
|
|
308
|
+
const violations: string[] = [];
|
|
309
|
+
|
|
310
|
+
// Check for incorrect patterns
|
|
311
|
+
const incorrectPatterns = [
|
|
312
|
+
{
|
|
313
|
+
pattern: /if.*conflict.*\{[^}]*spec\.status\s*=\s*localStatus/,
|
|
314
|
+
message: 'Local status should never win in conflicts'
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
pattern: /resolution\s*:\s*['"]local['"]/,
|
|
318
|
+
message: 'Resolution should be "external" for status conflicts'
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
pattern: /prefer.*local.*status/i,
|
|
322
|
+
message: 'Should prefer external status'
|
|
323
|
+
}
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
for (const { pattern, message } of incorrectPatterns) {
|
|
327
|
+
if (pattern.test(implementationCode)) {
|
|
328
|
+
violations.push(message);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Check for correct patterns
|
|
333
|
+
const requiredPatterns = [
|
|
334
|
+
{
|
|
335
|
+
pattern: /external.*wins|EXTERNAL.*WINS|externalStatus.*applied/i,
|
|
336
|
+
message: 'Missing confirmation that external wins'
|
|
337
|
+
}
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
for (const { pattern, message } of requiredPatterns) {
|
|
341
|
+
if (!pattern.test(implementationCode)) {
|
|
342
|
+
violations.push(message);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
valid: violations.length === 0,
|
|
348
|
+
violations
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Get resolution history
|
|
354
|
+
*/
|
|
355
|
+
public getResolutionLog(): ConflictResolution[] {
|
|
356
|
+
return this.resolutionLog;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Generate resolution report
|
|
361
|
+
*/
|
|
362
|
+
public generateReport(): string {
|
|
363
|
+
const report = [];
|
|
364
|
+
report.push('# Conflict Resolution Report');
|
|
365
|
+
report.push(`\n**Generated**: ${new Date().toISOString()}`);
|
|
366
|
+
report.push(`**Total Resolutions**: ${this.resolutionLog.length}`);
|
|
367
|
+
report.push('\n## Resolutions\n');
|
|
368
|
+
|
|
369
|
+
for (const resolution of this.resolutionLog) {
|
|
370
|
+
report.push(`### ${resolution.field}`);
|
|
371
|
+
report.push(`- **Local Value**: ${resolution.localValue}`);
|
|
372
|
+
report.push(`- **External Value**: ${resolution.externalValue}`);
|
|
373
|
+
report.push(`- **Resolution**: ${resolution.resolution.toUpperCase()} WINS`);
|
|
374
|
+
report.push(`- **Resolved To**: ${resolution.resolvedValue}`);
|
|
375
|
+
report.push(`- **Reason**: ${resolution.reason}`);
|
|
376
|
+
report.push(`- **Time**: ${resolution.timestamp}\n`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
report.push('## Validation');
|
|
380
|
+
report.push('✅ All conflicts resolved with external tool priority');
|
|
381
|
+
|
|
382
|
+
return report.join('\n');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ============================================================================
|
|
387
|
+
// Helper Functions
|
|
388
|
+
// ============================================================================
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Load spec metadata from file
|
|
392
|
+
*/
|
|
393
|
+
export async function loadSpecMetadata(specPath: string): Promise<SpecMetadata> {
|
|
394
|
+
const content = await fs.readFile(specPath, 'utf-8');
|
|
395
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
396
|
+
|
|
397
|
+
if (!frontmatterMatch) {
|
|
398
|
+
throw new Error(`No frontmatter found in ${specPath}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return yaml.parse(frontmatterMatch[1]) as SpecMetadata;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Perform bidirectional sync with conflict resolution
|
|
406
|
+
*/
|
|
407
|
+
export async function performBidirectionalSync(
|
|
408
|
+
specPath: string,
|
|
409
|
+
externalStatus: ExternalStatus
|
|
410
|
+
): Promise<void> {
|
|
411
|
+
const resolver = new ConflictResolver();
|
|
412
|
+
const spec = await loadSpecMetadata(specPath);
|
|
413
|
+
const resolutions: ConflictResolution[] = [];
|
|
414
|
+
|
|
415
|
+
// Check for status conflict
|
|
416
|
+
if (spec.status !== externalStatus.mappedStatus) {
|
|
417
|
+
const statusResolution = resolver.resolveStatusConflict(
|
|
418
|
+
spec.status,
|
|
419
|
+
externalStatus
|
|
420
|
+
);
|
|
421
|
+
resolutions.push(statusResolution);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Apply resolutions if any conflicts found
|
|
425
|
+
if (resolutions.length > 0) {
|
|
426
|
+
await resolver.applyResolutions(specPath, resolutions);
|
|
427
|
+
|
|
428
|
+
// Generate and save report
|
|
429
|
+
const report = resolver.generateReport();
|
|
430
|
+
const reportPath = specPath.replace('.md', '-sync-report.md');
|
|
431
|
+
await fs.writeFile(reportPath, report);
|
|
432
|
+
|
|
433
|
+
console.log(`📄 Sync report saved to ${path.basename(reportPath)}`);
|
|
434
|
+
} else {
|
|
435
|
+
console.log('✅ No conflicts detected - spec in sync with external tool');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ============================================================================
|
|
440
|
+
// Export for testing
|
|
441
|
+
// ============================================================================
|
|
442
|
+
|
|
443
|
+
export default ConflictResolver;
|