specweave 0.16.7 → 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/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,469 @@
|
|
|
1
|
+
import * as fs from "fs-extra";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
const PROJECT_KEYWORDS = {
|
|
4
|
+
"AuthService": [
|
|
5
|
+
"authentication",
|
|
6
|
+
"auth",
|
|
7
|
+
"login",
|
|
8
|
+
"logout",
|
|
9
|
+
"oauth",
|
|
10
|
+
"jwt",
|
|
11
|
+
"token",
|
|
12
|
+
"session",
|
|
13
|
+
"password",
|
|
14
|
+
"credential",
|
|
15
|
+
"sso",
|
|
16
|
+
"saml",
|
|
17
|
+
"ldap",
|
|
18
|
+
"mfa",
|
|
19
|
+
"2fa",
|
|
20
|
+
"totp"
|
|
21
|
+
],
|
|
22
|
+
"UserService": [
|
|
23
|
+
"user",
|
|
24
|
+
"profile",
|
|
25
|
+
"account",
|
|
26
|
+
"registration",
|
|
27
|
+
"preferences",
|
|
28
|
+
"settings",
|
|
29
|
+
"avatar",
|
|
30
|
+
"username",
|
|
31
|
+
"email",
|
|
32
|
+
"verification",
|
|
33
|
+
"onboarding",
|
|
34
|
+
"demographics",
|
|
35
|
+
"personalization"
|
|
36
|
+
],
|
|
37
|
+
"PaymentService": [
|
|
38
|
+
"payment",
|
|
39
|
+
"billing",
|
|
40
|
+
"stripe",
|
|
41
|
+
"paypal",
|
|
42
|
+
"invoice",
|
|
43
|
+
"subscription",
|
|
44
|
+
"charge",
|
|
45
|
+
"refund",
|
|
46
|
+
"credit card",
|
|
47
|
+
"transaction",
|
|
48
|
+
"checkout",
|
|
49
|
+
"cart",
|
|
50
|
+
"pricing",
|
|
51
|
+
"plan",
|
|
52
|
+
"tier"
|
|
53
|
+
],
|
|
54
|
+
"NotificationService": [
|
|
55
|
+
"notification",
|
|
56
|
+
"email",
|
|
57
|
+
"sms",
|
|
58
|
+
"push",
|
|
59
|
+
"alert",
|
|
60
|
+
"message",
|
|
61
|
+
"webhook",
|
|
62
|
+
"queue",
|
|
63
|
+
"sendgrid",
|
|
64
|
+
"twilio",
|
|
65
|
+
"template",
|
|
66
|
+
"broadcast",
|
|
67
|
+
"digest",
|
|
68
|
+
"reminder"
|
|
69
|
+
],
|
|
70
|
+
"Platform": [
|
|
71
|
+
"infrastructure",
|
|
72
|
+
"deployment",
|
|
73
|
+
"monitoring",
|
|
74
|
+
"logging",
|
|
75
|
+
"metrics",
|
|
76
|
+
"kubernetes",
|
|
77
|
+
"docker",
|
|
78
|
+
"ci/cd",
|
|
79
|
+
"pipeline",
|
|
80
|
+
"terraform",
|
|
81
|
+
"ansible",
|
|
82
|
+
"helm",
|
|
83
|
+
"grafana",
|
|
84
|
+
"prometheus"
|
|
85
|
+
],
|
|
86
|
+
"DataService": [
|
|
87
|
+
"database",
|
|
88
|
+
"data",
|
|
89
|
+
"analytics",
|
|
90
|
+
"etl",
|
|
91
|
+
"warehouse",
|
|
92
|
+
"pipeline",
|
|
93
|
+
"kafka",
|
|
94
|
+
"spark",
|
|
95
|
+
"hadoop",
|
|
96
|
+
"bigquery",
|
|
97
|
+
"redshift",
|
|
98
|
+
"snowflake",
|
|
99
|
+
"datalake",
|
|
100
|
+
"streaming"
|
|
101
|
+
],
|
|
102
|
+
"ApiGateway": [
|
|
103
|
+
"gateway",
|
|
104
|
+
"api",
|
|
105
|
+
"proxy",
|
|
106
|
+
"routing",
|
|
107
|
+
"load balancer",
|
|
108
|
+
"rate limiting",
|
|
109
|
+
"throttling",
|
|
110
|
+
"circuit breaker",
|
|
111
|
+
"cors",
|
|
112
|
+
"authentication proxy",
|
|
113
|
+
"service mesh",
|
|
114
|
+
"envoy",
|
|
115
|
+
"kong"
|
|
116
|
+
],
|
|
117
|
+
"WebApp": [
|
|
118
|
+
"frontend",
|
|
119
|
+
"ui",
|
|
120
|
+
"react",
|
|
121
|
+
"angular",
|
|
122
|
+
"vue",
|
|
123
|
+
"component",
|
|
124
|
+
"responsive",
|
|
125
|
+
"mobile-first",
|
|
126
|
+
"spa",
|
|
127
|
+
"ssr",
|
|
128
|
+
"next.js",
|
|
129
|
+
"gatsby",
|
|
130
|
+
"webpack",
|
|
131
|
+
"css",
|
|
132
|
+
"sass",
|
|
133
|
+
"styled-components"
|
|
134
|
+
],
|
|
135
|
+
"MobileApp": [
|
|
136
|
+
"ios",
|
|
137
|
+
"android",
|
|
138
|
+
"react native",
|
|
139
|
+
"flutter",
|
|
140
|
+
"swift",
|
|
141
|
+
"kotlin",
|
|
142
|
+
"objective-c",
|
|
143
|
+
"java",
|
|
144
|
+
"push notification",
|
|
145
|
+
"app store",
|
|
146
|
+
"play store",
|
|
147
|
+
"mobile",
|
|
148
|
+
"tablet",
|
|
149
|
+
"responsive"
|
|
150
|
+
]
|
|
151
|
+
};
|
|
152
|
+
const FILE_PATTERNS = {
|
|
153
|
+
"AuthService": [
|
|
154
|
+
/auth\//i,
|
|
155
|
+
/login\//i,
|
|
156
|
+
/security\//i,
|
|
157
|
+
/oauth\//i,
|
|
158
|
+
/jwt\//i
|
|
159
|
+
],
|
|
160
|
+
"UserService": [
|
|
161
|
+
/users?\//i,
|
|
162
|
+
/profiles?\//i,
|
|
163
|
+
/accounts?\//i,
|
|
164
|
+
/members?\//i
|
|
165
|
+
],
|
|
166
|
+
"PaymentService": [
|
|
167
|
+
/payment\//i,
|
|
168
|
+
/billing\//i,
|
|
169
|
+
/checkout\//i,
|
|
170
|
+
/stripe\//i,
|
|
171
|
+
/subscription\//i
|
|
172
|
+
],
|
|
173
|
+
"NotificationService": [
|
|
174
|
+
/notification\//i,
|
|
175
|
+
/email\//i,
|
|
176
|
+
/messaging\//i,
|
|
177
|
+
/templates?\//i
|
|
178
|
+
],
|
|
179
|
+
"Platform": [
|
|
180
|
+
/infrastructure\//i,
|
|
181
|
+
/terraform\//i,
|
|
182
|
+
/kubernetes\//i,
|
|
183
|
+
/k8s\//i,
|
|
184
|
+
/\.github\/workflows\//i
|
|
185
|
+
],
|
|
186
|
+
"WebApp": [
|
|
187
|
+
/frontend\//i,
|
|
188
|
+
/src\/components\//i,
|
|
189
|
+
/src\/pages\//i,
|
|
190
|
+
/public\//i,
|
|
191
|
+
/styles?\//i
|
|
192
|
+
],
|
|
193
|
+
"MobileApp": [
|
|
194
|
+
/ios\//i,
|
|
195
|
+
/android\//i,
|
|
196
|
+
/mobile\//i,
|
|
197
|
+
/app\//i
|
|
198
|
+
]
|
|
199
|
+
};
|
|
200
|
+
class AdoProjectDetector {
|
|
201
|
+
constructor(strategy, availableProjects, customKeywords, customPatterns) {
|
|
202
|
+
this.strategy = strategy;
|
|
203
|
+
this.availableProjects = availableProjects;
|
|
204
|
+
this.projectKeywords = { ...PROJECT_KEYWORDS, ...customKeywords };
|
|
205
|
+
this.filePatterns = { ...FILE_PATTERNS, ...customPatterns };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Detect project from spec file path
|
|
209
|
+
*/
|
|
210
|
+
async detectFromSpecPath(specPath) {
|
|
211
|
+
if (this.strategy === "project-per-team") {
|
|
212
|
+
const pathParts = specPath.split(path.sep);
|
|
213
|
+
const specsIndex = pathParts.indexOf("specs");
|
|
214
|
+
if (specsIndex !== -1 && specsIndex < pathParts.length - 1) {
|
|
215
|
+
const projectFolder = pathParts[specsIndex + 1];
|
|
216
|
+
const matchedProject = this.availableProjects.find(
|
|
217
|
+
(p) => p.toLowerCase() === projectFolder.toLowerCase()
|
|
218
|
+
);
|
|
219
|
+
if (matchedProject) {
|
|
220
|
+
return {
|
|
221
|
+
primary: matchedProject,
|
|
222
|
+
confidence: 1,
|
|
223
|
+
strategy: this.strategy
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const content = await fs.readFile(specPath, "utf-8");
|
|
229
|
+
return this.detectFromContent(content);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Detect project from spec content
|
|
233
|
+
*/
|
|
234
|
+
async detectFromContent(content) {
|
|
235
|
+
const candidates = this.analyzeContent(content);
|
|
236
|
+
if (candidates[0]?.confidence > 0.7) {
|
|
237
|
+
return {
|
|
238
|
+
primary: candidates[0].project,
|
|
239
|
+
confidence: candidates[0].confidence,
|
|
240
|
+
strategy: this.strategy
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (candidates[0]?.confidence > 0.4) {
|
|
244
|
+
const secondary = candidates.slice(1).filter((c) => c.confidence > 0.3).map((c) => c.project);
|
|
245
|
+
return {
|
|
246
|
+
primary: candidates[0].project,
|
|
247
|
+
secondary: secondary.length > 0 ? secondary : void 0,
|
|
248
|
+
confidence: candidates[0].confidence,
|
|
249
|
+
strategy: this.strategy
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
primary: this.availableProjects[0] || "Unknown",
|
|
254
|
+
confidence: 0,
|
|
255
|
+
strategy: this.strategy
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Analyze content and return project candidates with confidence scores
|
|
260
|
+
*/
|
|
261
|
+
analyzeContent(content) {
|
|
262
|
+
const results = [];
|
|
263
|
+
const lowerContent = content.toLowerCase();
|
|
264
|
+
for (const project of this.availableProjects) {
|
|
265
|
+
let confidence = 0;
|
|
266
|
+
const reasons = [];
|
|
267
|
+
if (lowerContent.includes(project.toLowerCase())) {
|
|
268
|
+
confidence += 0.5;
|
|
269
|
+
reasons.push(`Project name "${project}" found in content`);
|
|
270
|
+
}
|
|
271
|
+
const keywords = this.projectKeywords[project] || [];
|
|
272
|
+
let keywordMatches = 0;
|
|
273
|
+
for (const keyword of keywords) {
|
|
274
|
+
if (lowerContent.includes(keyword.toLowerCase())) {
|
|
275
|
+
keywordMatches++;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (keywordMatches > 0) {
|
|
279
|
+
const keywordScore = Math.min(keywordMatches * 0.1, 0.4);
|
|
280
|
+
confidence += keywordScore;
|
|
281
|
+
reasons.push(`Found ${keywordMatches} keyword matches`);
|
|
282
|
+
}
|
|
283
|
+
const patterns = this.filePatterns[project] || [];
|
|
284
|
+
let patternMatches = 0;
|
|
285
|
+
for (const pattern of patterns) {
|
|
286
|
+
if (pattern.test(content)) {
|
|
287
|
+
patternMatches++;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (patternMatches > 0) {
|
|
291
|
+
const patternScore = Math.min(patternMatches * 0.15, 0.3);
|
|
292
|
+
confidence += patternScore;
|
|
293
|
+
reasons.push(`Found ${patternMatches} file pattern matches`);
|
|
294
|
+
}
|
|
295
|
+
const projectAssignment = new RegExp(`project:\\s*${project}`, "i");
|
|
296
|
+
if (projectAssignment.test(content)) {
|
|
297
|
+
confidence = 1;
|
|
298
|
+
reasons.push("Explicit project assignment in frontmatter");
|
|
299
|
+
}
|
|
300
|
+
results.push({ project, confidence, reasons });
|
|
301
|
+
}
|
|
302
|
+
return results.sort((a, b) => b.confidence - a.confidence);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Detect projects for multi-project spec
|
|
306
|
+
*/
|
|
307
|
+
async detectMultiProject(content) {
|
|
308
|
+
const candidates = this.analyzeContent(content);
|
|
309
|
+
const significantProjects = candidates.filter((c) => c.confidence > 0.3);
|
|
310
|
+
if (significantProjects.length === 0) {
|
|
311
|
+
return {
|
|
312
|
+
primary: this.availableProjects[0] || "Unknown",
|
|
313
|
+
confidence: 0,
|
|
314
|
+
strategy: this.strategy
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const primary = significantProjects[0];
|
|
318
|
+
const secondary = significantProjects.slice(1).map((c) => c.project);
|
|
319
|
+
return {
|
|
320
|
+
primary: primary.project,
|
|
321
|
+
secondary: secondary.length > 0 ? secondary : void 0,
|
|
322
|
+
confidence: primary.confidence,
|
|
323
|
+
strategy: this.strategy
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Map spec to area path (for area-path-based strategy)
|
|
328
|
+
*/
|
|
329
|
+
mapToAreaPath(content, project) {
|
|
330
|
+
const areaPaths = process.env.AZURE_DEVOPS_AREA_PATHS?.split(",").map((a) => a.trim()) || [];
|
|
331
|
+
for (const areaPath of areaPaths) {
|
|
332
|
+
if (content.toLowerCase().includes(areaPath.toLowerCase())) {
|
|
333
|
+
return `${project}\\${areaPath}`;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return project;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Assign to team (for team-based strategy)
|
|
340
|
+
*/
|
|
341
|
+
assignToTeam(content) {
|
|
342
|
+
const teams = process.env.AZURE_DEVOPS_TEAMS?.split(",").map((t) => t.trim()) || [];
|
|
343
|
+
const teamMatch = content.match(/team:\s*([^\n]+)/i);
|
|
344
|
+
if (teamMatch) {
|
|
345
|
+
const assignedTeam = teamMatch[1].trim();
|
|
346
|
+
if (teams.includes(assignedTeam)) {
|
|
347
|
+
return assignedTeam;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const teamKeywords = {
|
|
351
|
+
"Frontend": ["ui", "react", "component", "css", "design"],
|
|
352
|
+
"Backend": ["api", "database", "server", "endpoint", "query"],
|
|
353
|
+
"Mobile": ["ios", "android", "app", "native", "push"],
|
|
354
|
+
"DevOps": ["deploy", "ci/cd", "kubernetes", "docker", "pipeline"],
|
|
355
|
+
"Data": ["analytics", "etl", "warehouse", "bigquery", "spark"]
|
|
356
|
+
};
|
|
357
|
+
for (const team of teams) {
|
|
358
|
+
const keywords = teamKeywords[team] || [];
|
|
359
|
+
for (const keyword of keywords) {
|
|
360
|
+
if (content.toLowerCase().includes(keyword)) {
|
|
361
|
+
return team;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return teams[0] || "Default Team";
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function getProjectDetectorFromEnv() {
|
|
369
|
+
const strategy = process.env.AZURE_DEVOPS_STRATEGY || "team-based";
|
|
370
|
+
let projects = [];
|
|
371
|
+
switch (strategy) {
|
|
372
|
+
case "project-per-team":
|
|
373
|
+
projects = process.env.AZURE_DEVOPS_PROJECTS?.split(",").map((p) => p.trim()) || [];
|
|
374
|
+
break;
|
|
375
|
+
case "area-path-based":
|
|
376
|
+
case "team-based":
|
|
377
|
+
const project = process.env.AZURE_DEVOPS_PROJECT;
|
|
378
|
+
if (project) {
|
|
379
|
+
projects = [project];
|
|
380
|
+
}
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
return new AdoProjectDetector(strategy, projects);
|
|
384
|
+
}
|
|
385
|
+
async function createProjectFolders(baseDir, strategy, projects) {
|
|
386
|
+
const specsPath = path.join(baseDir, ".specweave", "docs", "internal", "specs");
|
|
387
|
+
switch (strategy) {
|
|
388
|
+
case "project-per-team":
|
|
389
|
+
for (const project2 of projects) {
|
|
390
|
+
const projectPath = path.join(specsPath, project2);
|
|
391
|
+
await fs.ensureDir(projectPath);
|
|
392
|
+
await createProjectReadme(projectPath, project2);
|
|
393
|
+
}
|
|
394
|
+
break;
|
|
395
|
+
case "area-path-based":
|
|
396
|
+
const areaPaths = process.env.AZURE_DEVOPS_AREA_PATHS?.split(",").map((a) => a.trim()) || [];
|
|
397
|
+
const project = projects[0];
|
|
398
|
+
if (project) {
|
|
399
|
+
const projectPath = path.join(specsPath, project);
|
|
400
|
+
await fs.ensureDir(projectPath);
|
|
401
|
+
for (const area of areaPaths) {
|
|
402
|
+
const areaPath = path.join(projectPath, area);
|
|
403
|
+
await fs.ensureDir(areaPath);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
break;
|
|
407
|
+
case "team-based":
|
|
408
|
+
const teams = process.env.AZURE_DEVOPS_TEAMS?.split(",").map((t) => t.trim()) || [];
|
|
409
|
+
const proj = projects[0];
|
|
410
|
+
if (proj) {
|
|
411
|
+
const projectPath = path.join(specsPath, proj);
|
|
412
|
+
await fs.ensureDir(projectPath);
|
|
413
|
+
for (const team of teams) {
|
|
414
|
+
const teamPath = path.join(projectPath, team);
|
|
415
|
+
await fs.ensureDir(teamPath);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async function createProjectReadme(projectPath, projectName) {
|
|
422
|
+
const readmePath = path.join(projectPath, "README.md");
|
|
423
|
+
if (await fs.pathExists(readmePath)) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const content = `# ${projectName} Specifications
|
|
427
|
+
|
|
428
|
+
## Overview
|
|
429
|
+
|
|
430
|
+
This folder contains specifications for the ${projectName} project.
|
|
431
|
+
|
|
432
|
+
## Azure DevOps
|
|
433
|
+
|
|
434
|
+
- Organization: ${process.env.AZURE_DEVOPS_ORG || "TBD"}
|
|
435
|
+
- Project: ${projectName}
|
|
436
|
+
- URL: https://dev.azure.com/${process.env.AZURE_DEVOPS_ORG || "org"}/${projectName}
|
|
437
|
+
|
|
438
|
+
## Specifications
|
|
439
|
+
|
|
440
|
+
_No specifications yet. Specs will appear here as they are created._
|
|
441
|
+
|
|
442
|
+
## Team
|
|
443
|
+
|
|
444
|
+
- Lead: TBD
|
|
445
|
+
- Members: TBD
|
|
446
|
+
|
|
447
|
+
## Keywords
|
|
448
|
+
|
|
449
|
+
${PROJECT_KEYWORDS[projectName]?.join(", ") || "TBD"}
|
|
450
|
+
|
|
451
|
+
## Getting Started
|
|
452
|
+
|
|
453
|
+
1. Create a new spec: \`/specweave:increment "feature-name"\`
|
|
454
|
+
2. Specs will be organized here automatically
|
|
455
|
+
3. Sync to Azure DevOps: \`/specweave-ado:sync-spec spec-001\`
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
_Generated by SpecWeave_
|
|
460
|
+
`;
|
|
461
|
+
await fs.writeFile(readmePath, content);
|
|
462
|
+
}
|
|
463
|
+
export {
|
|
464
|
+
AdoProjectDetector,
|
|
465
|
+
FILE_PATTERNS,
|
|
466
|
+
PROJECT_KEYWORDS,
|
|
467
|
+
createProjectFolders,
|
|
468
|
+
getProjectDetectorFromEnv
|
|
469
|
+
};
|