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.
@@ -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
+ };