specweave 1.0.486 → 1.0.487

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 (27) hide show
  1. package/dist/src/core/skill-gen/drift-detector.d.ts +5 -5
  2. package/dist/src/core/skill-gen/drift-detector.d.ts.map +1 -1
  3. package/dist/src/core/skill-gen/drift-detector.js +31 -34
  4. package/dist/src/core/skill-gen/drift-detector.js.map +1 -1
  5. package/dist/src/core/skill-gen/signal-collector.d.ts +11 -13
  6. package/dist/src/core/skill-gen/signal-collector.d.ts.map +1 -1
  7. package/dist/src/core/skill-gen/signal-collector.js +186 -132
  8. package/dist/src/core/skill-gen/signal-collector.js.map +1 -1
  9. package/dist/src/core/skill-gen/suggestion-engine.d.ts +0 -2
  10. package/dist/src/core/skill-gen/suggestion-engine.d.ts.map +1 -1
  11. package/dist/src/core/skill-gen/suggestion-engine.js +15 -22
  12. package/dist/src/core/skill-gen/suggestion-engine.js.map +1 -1
  13. package/dist/src/core/skill-gen/types.d.ts +23 -7
  14. package/dist/src/core/skill-gen/types.d.ts.map +1 -1
  15. package/dist/src/core/skill-gen/types.js.map +1 -1
  16. package/dist/src/core/skill-gen/utils.d.ts +73 -0
  17. package/dist/src/core/skill-gen/utils.d.ts.map +1 -0
  18. package/dist/src/core/skill-gen/utils.js +120 -0
  19. package/dist/src/core/skill-gen/utils.js.map +1 -0
  20. package/package.json +1 -1
  21. package/plugins/specweave/hooks/hooks.json +10 -10
  22. package/plugins/specweave/hooks/universal/run-hook.sh +20 -0
  23. package/plugins/specweave/skills/do/SKILL.md +2 -2
  24. package/plugins/specweave/skills/increment/SKILL.md +3 -3
  25. package/plugins/specweave/skills/team-build/SKILL.md +1 -1
  26. package/plugins/specweave/skills/team-lead/SKILL.md +1 -1
  27. package/plugins/specweave/skills/validate/SKILL.md +1 -1
@@ -1,24 +1,24 @@
1
1
  /**
2
2
  * Drift Detector — compares project-local skills against current living docs.
3
3
  *
4
- * Runs during living docs sync. Warns when skills reference modules or APIs
5
- * that no longer appear in the analysis output.
4
+ * Runs during living docs sync. Returns structured DriftResult[] when skills
5
+ * reference modules or APIs that no longer appear in the analysis output.
6
6
  *
7
7
  * Error-isolated: never throws, never blocks sync.
8
8
  *
9
9
  * @module core/skill-gen/drift-detector
10
10
  */
11
+ import type { DriftResult } from './types.js';
11
12
  export declare class DriftDetector {
12
13
  private projectRoot;
13
14
  constructor(projectRoot: string);
14
15
  /**
15
16
  * Check project-local skills for stale references.
16
- * Never throws.
17
+ * Returns structured DriftResult[] — never throws.
17
18
  */
18
- check(): Promise<void>;
19
+ check(): Promise<DriftResult[]>;
19
20
  private getSkillFiles;
20
21
  private loadDocsContent;
21
- private collectMarkdownFiles;
22
22
  private extractModuleReferences;
23
23
  }
24
24
  //# sourceMappingURL=drift-detector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"drift-detector.d.ts","sourceRoot":"","sources":["../../../../src/core/skill-gen/drift-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAWH,qBAAa,aAAa;IACxB,OAAO,CAAC,WAAW,CAAS;gBAEhB,WAAW,EAAE,MAAM;IAI/B;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAuCd,aAAa;YASb,eAAe;YAoBf,oBAAoB;IAkBlC,OAAO,CAAC,uBAAuB;CAchC"}
1
+ {"version":3,"file":"drift-detector.d.ts","sourceRoot":"","sources":["../../../../src/core/skill-gen/drift-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAsB9C,qBAAa,aAAa;IACxB,OAAO,CAAC,WAAW,CAAS;gBAEhB,WAAW,EAAE,MAAM;IAI/B;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YA0CvB,aAAa;YASb,eAAe;IAoB7B,OAAO,CAAC,uBAAuB;CAiBhC"}
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Drift Detector — compares project-local skills against current living docs.
3
3
  *
4
- * Runs during living docs sync. Warns when skills reference modules or APIs
5
- * that no longer appear in the analysis output.
4
+ * Runs during living docs sync. Returns structured DriftResult[] when skills
5
+ * reference modules or APIs that no longer appear in the analysis output.
6
6
  *
7
7
  * Error-isolated: never throws, never blocks sync.
8
8
  *
@@ -10,18 +10,31 @@
10
10
  */
11
11
  import { readFile, readdir, stat } from 'fs/promises';
12
12
  import { join } from 'path';
13
+ import { collectMarkdownFiles } from './utils.js';
13
14
  /**
14
15
  * Extracts capitalized multi-word identifiers that look like module/class names.
15
16
  * Matches PascalCase identifiers (e.g., AuthModule, OldModule, CoreService).
16
17
  */
17
18
  const MODULE_NAME_PATTERN = /\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g;
19
+ /**
20
+ * Common PascalCase words that are not project-specific module references.
21
+ * These are excluded from drift detection to reduce false positives.
22
+ */
23
+ const PASCAL_CASE_EXCLUSIONS = new Set([
24
+ 'TypeScript', 'JavaScript', 'SpecWeave', 'ReactComponent', 'NextJs',
25
+ 'NodeModule', 'ErrorBoundary', 'PascalCase', 'CamelCase', 'GraphQL',
26
+ 'TypeORM', 'PostgreSQL', 'MongoDB', 'CloudFlare', 'GitHub', 'GitLab',
27
+ 'BitBucket', 'WebSocket', 'OAuth', 'OpenAPI', 'AsyncIterator', 'EventEmitter',
28
+ 'ReadStream', 'WriteStream', 'AbortController', 'RegExp', 'ArrayBuffer',
29
+ 'SharedWorker', 'ServiceWorker', 'IndexedDB', 'LocalStorage', 'SessionStorage',
30
+ ]);
18
31
  export class DriftDetector {
19
32
  constructor(projectRoot) {
20
33
  this.projectRoot = projectRoot;
21
34
  }
22
35
  /**
23
36
  * Check project-local skills for stale references.
24
- * Never throws.
37
+ * Returns structured DriftResult[] — never throws.
25
38
  */
26
39
  async check() {
27
40
  try {
@@ -30,30 +43,32 @@ export class DriftDetector {
30
43
  try {
31
44
  const st = await stat(skillsDir);
32
45
  if (!st.isDirectory())
33
- return;
46
+ return [];
34
47
  }
35
48
  catch {
36
- return; // No skills directory
49
+ return []; // No skills directory
37
50
  }
38
51
  const skillFiles = await this.getSkillFiles(skillsDir);
39
52
  if (skillFiles.length === 0)
40
- return;
53
+ return [];
41
54
  const docsContent = await this.loadDocsContent();
42
55
  if (!docsContent)
43
- return;
56
+ return [];
44
57
  const docsLower = docsContent.toLowerCase();
58
+ const results = [];
45
59
  for (const skillFile of skillFiles) {
46
60
  const content = await readFile(join(skillsDir, skillFile), 'utf-8');
47
61
  const moduleRefs = this.extractModuleReferences(content);
48
62
  const staleRefs = moduleRefs.filter((ref) => !docsLower.includes(ref.toLowerCase()));
49
- if (staleRefs.length > 0) {
50
- console.warn(`[DriftDetector] Possible stale references in ${skillFile}: ${staleRefs.join(', ')}. These modules no longer appear in living docs.`);
63
+ const validRefs = moduleRefs.filter((ref) => docsLower.includes(ref.toLowerCase()));
64
+ if (staleRefs.length > 0 || validRefs.length > 0) {
65
+ results.push({ skillFile, staleRefs, validRefs });
51
66
  }
52
67
  }
68
+ return results;
53
69
  }
54
- catch (error) {
55
- const msg = error instanceof Error ? error.message : String(error);
56
- console.warn(`[DriftDetector] Warning: ${msg}`);
70
+ catch {
71
+ return [];
57
72
  }
58
73
  }
59
74
  async getSkillFiles(dir) {
@@ -68,7 +83,7 @@ export class DriftDetector {
68
83
  async loadDocsContent() {
69
84
  const docsDir = join(this.projectRoot, '.specweave', 'docs', 'internal');
70
85
  try {
71
- const files = await this.collectMarkdownFiles(docsDir);
86
+ const files = await collectMarkdownFiles(docsDir);
72
87
  if (files.length === 0)
73
88
  return null;
74
89
  const contents = [];
@@ -86,34 +101,16 @@ export class DriftDetector {
86
101
  return null;
87
102
  }
88
103
  }
89
- async collectMarkdownFiles(dir) {
90
- const results = [];
91
- try {
92
- const entries = await readdir(dir, { withFileTypes: true });
93
- for (const entry of entries) {
94
- const fullPath = join(dir, entry.name);
95
- if (entry.isDirectory()) {
96
- results.push(...(await this.collectMarkdownFiles(fullPath)));
97
- }
98
- else if (entry.name.endsWith('.md')) {
99
- results.push(fullPath);
100
- }
101
- }
102
- }
103
- catch {
104
- // Skip
105
- }
106
- return results;
107
- }
108
104
  extractModuleReferences(content) {
109
105
  const matches = new Set();
110
106
  let match;
111
107
  // Reset regex state
112
108
  MODULE_NAME_PATTERN.lastIndex = 0;
113
109
  while ((match = MODULE_NAME_PATTERN.exec(content)) !== null) {
114
- // Skip common false positives
115
110
  const name = match[1];
116
- if (!['README', 'SKILL', 'CHANGELOG', 'LICENSE', 'TODO'].includes(name.toUpperCase())) {
111
+ // Skip common false positives and excluded PascalCase words
112
+ if (!['README', 'SKILL', 'CHANGELOG', 'LICENSE', 'TODO'].includes(name.toUpperCase()) &&
113
+ !PASCAL_CASE_EXCLUSIONS.has(name)) {
117
114
  matches.add(name);
118
115
  }
119
116
  }
@@ -1 +1 @@
1
- {"version":3,"file":"drift-detector.js","sourceRoot":"","sources":["../../../../src/core/skill-gen/drift-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B;;;GAGG;AACH,MAAM,mBAAmB,GAAG,oCAAoC,CAAC;AAEjE,MAAM,OAAO,aAAa;IAGxB,YAAY,WAAmB;QAC7B,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,KAAK;QACT,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;YAE9D,mCAAmC;YACnC,IAAI,CAAC;gBACH,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC;gBACjC,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE;oBAAE,OAAO;YAChC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,CAAC,sBAAsB;YAChC,CAAC;YAED,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;YACvD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO;YAEpC,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC;YACjD,IAAI,CAAC,WAAW;gBAAE,OAAO;YAEzB,MAAM,SAAS,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC;YAE5C,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;gBACnC,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC;gBACpE,MAAM,UAAU,GAAG,IAAI,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC;gBACzD,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CACjC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,CAChD,CAAC;gBAEF,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBACzB,OAAO,CAAC,IAAI,CACV,gDAAgD,SAAS,KAAK,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,kDAAkD,CACrI,CAAC;gBACJ,CAAC;YACH,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACnE,OAAO,CAAC,IAAI,CAAC,4BAA4B,GAAG,EAAE,CAAC,CAAC;QAClD,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,aAAa,CAAC,GAAW;QACrC,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;YACnC,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,eAAe;QAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;QACzE,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC;YACvD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YAEpC,MAAM,QAAQ,GAAa,EAAE,CAAC;YAC9B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;gBACtB,IAAI,CAAC;oBACH,QAAQ,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;gBAC5C,CAAC;gBAAC,MAAM,CAAC;oBACP,wBAAwB;gBAC1B,CAAC;YACH,CAAC;YACD,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,oBAAoB,CAAC,GAAW;QAC5C,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;YAC5D,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;gBAC5B,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;gBACvC,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;oBACxB,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;gBAC/D,CAAC;qBAAM,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;oBACtC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACzB,CAAC;YACH,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;QACT,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAEO,uBAAuB,CAAC,OAAe;QAC7C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;QAClC,IAAI,KAA6B,CAAC;QAClC,oBAAoB;QACpB,mBAAmB,CAAC,SAAS,GAAG,CAAC,CAAC;QAClC,OAAO,CAAC,KAAK,GAAG,mBAAmB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YAC5D,8BAA8B;YAC9B,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACtB,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;gBACtF,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;QACD,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;CACF"}
1
+ {"version":3,"file":"drift-detector.js","sourceRoot":"","sources":["../../../../src/core/skill-gen/drift-detector.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Signal Collector — detects recurring patterns from living docs output.
2
+ * Signal Collector — detects recurring patterns from living docs using LLM analysis.
3
3
  *
4
4
  * Runs on every increment closure (wired into LifecycleHookDispatcher.onIncrementDone).
5
- * Reads markdown files from .specweave/docs/internal/, extracts pattern candidates,
6
- * and persists them to .specweave/state/skill-signals.json.
5
+ * Reads markdown files from .specweave/docs/internal/, sends them to the LLM for
6
+ * pattern extraction, and persists results to .specweave/state/skill-signals.json.
7
7
  *
8
8
  * Error-isolated: never throws, never blocks increment closure.
9
9
  *
@@ -24,21 +24,19 @@ export declare class SignalCollector {
24
24
  */
25
25
  collect(incrementId: string): Promise<void>;
26
26
  /**
27
- * Load signal store from disk, recovering from corruption.
27
+ * Seed mode: scan all living docs and populate the signal store
28
+ * without associating patterns with any increment.
28
29
  */
29
- private loadStore;
30
+ collectSeed(): Promise<void>;
30
31
  /**
31
- * Save signal store to disk.
32
+ * Detect patterns via LLM analysis of markdown files.
33
+ * Handles batching when total tokens exceed TOKEN_BUDGET.
32
34
  */
33
- private saveStore;
35
+ private detectPatternsLLM;
34
36
  /**
35
- * Detect patterns from living docs markdown files.
37
+ * Build the user prompt with <documents> block.
36
38
  */
37
- private detectPatterns;
38
- /**
39
- * Recursively collect markdown files from a directory.
40
- */
41
- private collectMarkdownFiles;
39
+ private buildPrompt;
42
40
  /**
43
41
  * Create or update a signal entry.
44
42
  */
@@ -1 +1 @@
1
- {"version":3,"file":"signal-collector.d.ts","sourceRoot":"","sources":["../../../../src/core/skill-gen/signal-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAwDH,UAAU,gBAAgB;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,cAAc,CAAS;gBAEnB,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB;IAM3D;;;OAGG;IACG,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBjD;;OAEG;YACW,SAAS;IAmBvB;;OAEG;YACW,SAAS;IAOvB;;OAEG;YACW,cAAc;IA8B5B;;OAEG;YACW,oBAAoB;IAkBlC;;OAEG;IACH,OAAO,CAAC,YAAY;IA4CpB;;OAEG;IACH,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,cAAc;CAGvB"}
1
+ {"version":3,"file":"signal-collector.d.ts","sourceRoot":"","sources":["../../../../src/core/skill-gen/signal-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAkBH,UAAU,gBAAgB;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAwBD,qBAAa,eAAe;IAC1B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,cAAc,CAAS;gBAEnB,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB;IAM3D;;;OAGG;IACG,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BjD;;;OAGG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAmDlC;;;OAGG;YACW,iBAAiB;IAgF/B;;OAEG;IACH,OAAO,CAAC,WAAW;IAenB;;OAEG;IACH,OAAO,CAAC,YAAY;IA8DpB;;OAEG;IACH,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,cAAc;CAGvB"}
@@ -1,64 +1,39 @@
1
1
  /**
2
- * Signal Collector — detects recurring patterns from living docs output.
2
+ * Signal Collector — detects recurring patterns from living docs using LLM analysis.
3
3
  *
4
4
  * Runs on every increment closure (wired into LifecycleHookDispatcher.onIncrementDone).
5
- * Reads markdown files from .specweave/docs/internal/, extracts pattern candidates,
6
- * and persists them to .specweave/state/skill-signals.json.
5
+ * Reads markdown files from .specweave/docs/internal/, sends them to the LLM for
6
+ * pattern extraction, and persists results to .specweave/state/skill-signals.json.
7
7
  *
8
8
  * Error-isolated: never throws, never blocks increment closure.
9
9
  *
10
10
  * @module core/skill-gen/signal-collector
11
11
  */
12
- import { readFile, writeFile, readdir, mkdir, copyFile } from 'fs/promises';
12
+ import { readFile } from 'fs/promises';
13
13
  import { join, relative } from 'path';
14
- import { EMPTY_SIGNAL_STORE, SKILL_GEN_DEFAULTS } from './types.js';
15
- /**
16
- * Pattern detection categories with keyword indicators.
17
- * A category is detected when 3+ keywords appear in a single file.
18
- */
19
- const PATTERN_CATEGORIES = {
20
- 'error-handling': {
21
- keywords: ['error-handling', 'error handling', 'try/catch', 'error boundar', 'catch block', 'error middleware'],
22
- description: 'Consistent error handling patterns',
23
- },
24
- 'naming-conventions': {
25
- keywords: ['naming convention', 'file naming', 'variable naming', 'kebab-case', 'camelCase', 'PascalCase'],
26
- description: 'Consistent naming convention patterns',
27
- },
28
- 'architecture-patterns': {
29
- keywords: ['architecture pattern', 'module organization', 'dependency injection', 'clean architecture', 'layered', 'hexagonal'],
30
- description: 'Architectural organization patterns',
31
- },
32
- 'testing-patterns': {
33
- keywords: ['test pattern', 'mock pattern', 'test organization', 'test fixture', 'test factory', 'test helper'],
34
- description: 'Testing convention patterns',
35
- },
36
- 'api-patterns': {
37
- keywords: ['api pattern', 'api boundary', 'endpoint pattern', 'route pattern', 'validation', 'zod', 'schema validation'],
38
- description: 'API design and validation patterns',
39
- },
40
- 'auth-patterns': {
41
- keywords: ['auth pattern', 'authentication', 'authorization', 'jwt', 'session', 'oauth', 'auth middleware'],
42
- description: 'Authentication and authorization patterns',
43
- },
44
- 'data-model': {
45
- keywords: ['data model', 'database', 'schema', 'migration', 'orm', 'prisma', 'typeorm', 'sequelize'],
46
- description: 'Data model and database patterns',
47
- },
48
- 'state-management': {
49
- keywords: ['state management', 'redux', 'zustand', 'context', 'store', 'global state'],
50
- description: 'State management patterns',
51
- },
52
- 'integration-patterns': {
53
- keywords: ['integration', 'external api', 'third-party', 'webhook', 'event-driven', 'message queue'],
54
- description: 'External integration patterns',
55
- },
56
- 'build-deploy': {
57
- keywords: ['build pattern', 'deploy', 'ci/cd', 'pipeline', 'docker', 'containeriz'],
58
- description: 'Build and deployment patterns',
14
+ import { SKILL_GEN_DEFAULTS } from './types.js';
15
+ import { collectMarkdownFiles, loadSignalStore, saveSignalStore, sanitizeString, estimateTokenCount, capEvidence, TOKEN_BUDGET, } from './utils.js';
16
+ import { loadLLMConfig, createProvider, hasLLMConfig } from '../llm/provider-factory.js';
17
+ const LLM_PATTERN_SCHEMA = {
18
+ type: 'object',
19
+ properties: {
20
+ patterns: {
21
+ type: 'array',
22
+ items: {
23
+ type: 'object',
24
+ properties: {
25
+ category: { type: 'string', description: 'Kebab-case category slug' },
26
+ name: { type: 'string', description: 'Short pattern name' },
27
+ description: { type: 'string', description: 'What and why' },
28
+ evidence: { type: 'array', items: { type: 'string' } },
29
+ },
30
+ required: ['category', 'name', 'description', 'evidence'],
31
+ },
32
+ },
59
33
  },
34
+ required: ['patterns'],
60
35
  };
61
- const MIN_KEYWORD_HITS = 3;
36
+ const SYSTEM_PROMPT = `You are a software architecture analyst. Given project documentation, identify recurring implementation patterns. Return structured JSON only.`;
62
37
  export class SignalCollector {
63
38
  constructor(projectRoot, options) {
64
39
  this.projectRoot = projectRoot;
@@ -71,137 +46,216 @@ export class SignalCollector {
71
46
  */
72
47
  async collect(incrementId) {
73
48
  try {
74
- const store = await this.loadStore();
49
+ if (!hasLLMConfig(this.projectRoot)) {
50
+ console.warn('[skill-gen] No LLM config found — skipping pattern detection');
51
+ return;
52
+ }
75
53
  const docsDir = join(this.projectRoot, '.specweave', 'docs', 'internal');
76
- const patterns = await this.detectPatterns(docsDir);
54
+ const files = await collectMarkdownFiles(docsDir);
55
+ if (files.length === 0)
56
+ return;
57
+ const config = loadLLMConfig(this.projectRoot);
58
+ if (!config)
59
+ return;
60
+ const provider = await createProvider(config);
61
+ const patterns = await this.detectPatternsLLM(files, provider);
62
+ const store = await loadSignalStore(this.getSignalsPath());
77
63
  for (const detected of patterns) {
78
- this.upsertSignal(store, detected, incrementId);
64
+ this.upsertSignal(store, detected, incrementId, files);
79
65
  }
80
66
  this.pruneIfNeeded(store);
81
- await this.saveStore(store);
67
+ await saveSignalStore(this.getSignalsPath(), store);
82
68
  }
83
69
  catch (error) {
84
- // Error-isolated: log and continue
85
70
  const msg = error instanceof Error ? error.message : String(error);
86
71
  console.warn(`[SignalCollector] Warning: ${msg}`);
87
72
  }
88
73
  }
89
74
  /**
90
- * Load signal store from disk, recovering from corruption.
75
+ * Seed mode: scan all living docs and populate the signal store
76
+ * without associating patterns with any increment.
91
77
  */
92
- async loadStore() {
93
- const signalsPath = this.getSignalsPath();
78
+ async collectSeed() {
94
79
  try {
95
- const content = await readFile(signalsPath, 'utf-8');
96
- return JSON.parse(content);
97
- }
98
- catch (error) {
99
- if (error?.code === 'ENOENT') {
100
- return { ...EMPTY_SIGNAL_STORE, signals: [] };
80
+ if (!hasLLMConfig(this.projectRoot)) {
81
+ console.warn('[skill-gen] No LLM config found — skipping seed');
82
+ return;
101
83
  }
102
- // Corrupted file backup and start fresh
103
- try {
104
- await copyFile(signalsPath, signalsPath + '.bak');
105
- }
106
- catch {
107
- // backup failed, continue anyway
84
+ const docsDir = join(this.projectRoot, '.specweave', 'docs', 'internal');
85
+ const files = await collectMarkdownFiles(docsDir);
86
+ if (files.length === 0)
87
+ return;
88
+ const config = loadLLMConfig(this.projectRoot);
89
+ if (!config)
90
+ return;
91
+ const provider = await createProvider(config);
92
+ const patterns = await this.detectPatternsLLM(files, provider);
93
+ const store = await loadSignalStore(this.getSignalsPath());
94
+ const now = new Date().toISOString();
95
+ for (const pattern of patterns) {
96
+ const existingKey = store.signals.find(s => s.category === pattern.category && s.pattern === sanitizeString(pattern.name));
97
+ if (existingKey)
98
+ continue; // Skip duplicates
99
+ const newSignal = {
100
+ id: `sig-${sanitizeString(pattern.category)}-${sanitizeString(pattern.name)}`,
101
+ pattern: sanitizeString(pattern.name),
102
+ category: sanitizeString(pattern.category),
103
+ description: sanitizeString(pattern.description),
104
+ incrementIds: [],
105
+ firstSeen: now,
106
+ lastSeen: now,
107
+ confidence: 0,
108
+ evidence: capEvidence(pattern.evidence.map(e => sanitizeString(e))),
109
+ uniqueSourceFiles: files.map(f => relative(this.projectRoot, f)),
110
+ suggested: false,
111
+ declined: false,
112
+ generated: false,
113
+ };
114
+ store.signals.push(newSignal);
108
115
  }
109
- return { ...EMPTY_SIGNAL_STORE, signals: [] };
116
+ await saveSignalStore(this.getSignalsPath(), store);
117
+ }
118
+ catch (error) {
119
+ const msg = error instanceof Error ? error.message : String(error);
120
+ console.warn(`[SignalCollector] Warning: ${msg}`);
110
121
  }
111
122
  }
112
123
  /**
113
- * Save signal store to disk.
114
- */
115
- async saveStore(store) {
116
- const signalsPath = this.getSignalsPath();
117
- const dir = join(this.projectRoot, '.specweave', 'state');
118
- await mkdir(dir, { recursive: true });
119
- await writeFile(signalsPath, JSON.stringify(store, null, 2));
120
- }
121
- /**
122
- * Detect patterns from living docs markdown files.
124
+ * Detect patterns via LLM analysis of markdown files.
125
+ * Handles batching when total tokens exceed TOKEN_BUDGET.
123
126
  */
124
- async detectPatterns(docsDir) {
125
- const files = await this.collectMarkdownFiles(docsDir);
126
- const detectedMap = new Map();
127
+ async detectPatternsLLM(files, provider) {
128
+ // Read files and estimate tokens
129
+ const docs = [];
127
130
  for (const filePath of files) {
128
131
  try {
129
- const content = (await readFile(filePath, 'utf-8')).toLowerCase();
132
+ const content = await readFile(filePath, 'utf-8');
130
133
  const relPath = relative(this.projectRoot, filePath);
131
- for (const [category, { keywords }] of Object.entries(PATTERN_CATEGORIES)) {
132
- const hits = keywords.filter((kw) => content.includes(kw.toLowerCase()));
133
- if (hits.length >= MIN_KEYWORD_HITS) {
134
- const existing = detectedMap.get(category) || [];
135
- if (!existing.includes(relPath)) {
136
- existing.push(relPath);
137
- }
138
- detectedMap.set(category, existing);
139
- }
140
- }
134
+ docs.push({ path: relPath, content, tokens: estimateTokenCount(content) });
141
135
  }
142
136
  catch {
143
137
  // Skip unreadable files
144
138
  }
145
139
  }
146
- return Array.from(detectedMap.entries()).map(([category, evidence]) => ({
147
- category,
148
- evidence,
149
- }));
150
- }
151
- /**
152
- * Recursively collect markdown files from a directory.
153
- */
154
- async collectMarkdownFiles(dir) {
155
- const results = [];
156
- try {
157
- const entries = await readdir(dir, { withFileTypes: true });
158
- for (const entry of entries) {
159
- const fullPath = join(dir, entry.name);
160
- if (entry.isDirectory()) {
161
- results.push(...(await this.collectMarkdownFiles(fullPath)));
162
- }
163
- else if (entry.name.endsWith('.md')) {
164
- results.push(fullPath);
165
- }
140
+ if (docs.length === 0)
141
+ return [];
142
+ const totalTokens = docs.reduce((sum, d) => sum + d.tokens, 0);
143
+ if (totalTokens <= TOKEN_BUDGET) {
144
+ // Single call
145
+ const prompt = this.buildPrompt(docs);
146
+ const result = await provider.analyzeStructured(prompt, {
147
+ schema: LLM_PATTERN_SCHEMA,
148
+ temperature: 0.1,
149
+ maxTokens: 4096,
150
+ timeout: 30000,
151
+ systemPrompt: SYSTEM_PROMPT,
152
+ });
153
+ return result.data.patterns;
154
+ }
155
+ // Batched chunking
156
+ const chunks = [];
157
+ let currentChunk = [];
158
+ let currentTokens = 0;
159
+ for (const doc of docs) {
160
+ if (currentTokens + doc.tokens > TOKEN_BUDGET && currentChunk.length > 0) {
161
+ chunks.push(currentChunk);
162
+ currentChunk = [];
163
+ currentTokens = 0;
166
164
  }
165
+ currentChunk.push(doc);
166
+ currentTokens += doc.tokens;
167
167
  }
168
- catch {
169
- // Directory doesn't exist or is unreadable
168
+ if (currentChunk.length > 0) {
169
+ chunks.push(currentChunk);
170
170
  }
171
- return results;
171
+ // Call LLM per chunk and merge
172
+ const allPatterns = [];
173
+ for (const chunk of chunks) {
174
+ try {
175
+ const prompt = this.buildPrompt(chunk);
176
+ const result = await provider.analyzeStructured(prompt, {
177
+ schema: LLM_PATTERN_SCHEMA,
178
+ temperature: 0.1,
179
+ maxTokens: 4096,
180
+ timeout: 30000,
181
+ systemPrompt: SYSTEM_PROMPT,
182
+ });
183
+ allPatterns.push(...result.data.patterns);
184
+ }
185
+ catch {
186
+ // Skip failed chunks, continue with others
187
+ }
188
+ }
189
+ // Deduplicate by category + name
190
+ const seen = new Map();
191
+ for (const p of allPatterns) {
192
+ const key = `${p.category}::${p.name}`;
193
+ if (!seen.has(key)) {
194
+ seen.set(key, p);
195
+ }
196
+ }
197
+ return Array.from(seen.values());
198
+ }
199
+ /**
200
+ * Build the user prompt with <documents> block.
201
+ */
202
+ buildPrompt(docs) {
203
+ const docBlocks = docs
204
+ .map(d => `--- file: ${d.path} ---\n${d.content}`)
205
+ .join('\n');
206
+ return `Analyze these project documents and identify recurring patterns.
207
+ For each pattern provide: category (kebab-case slug), name (short identifier),
208
+ description (1-2 sentences explaining what the pattern is and why it matters),
209
+ and evidence (list of relevant quotes or references from the docs, max 5 per pattern).
210
+
211
+ <documents>
212
+ ${docBlocks}
213
+ </documents>`;
172
214
  }
173
215
  /**
174
216
  * Create or update a signal entry.
175
217
  */
176
- upsertSignal(store, detected, incrementId) {
177
- const existing = store.signals.find((s) => s.category === detected.category);
218
+ upsertSignal(store, detected, incrementId, sourceFiles) {
219
+ const sanitizedName = sanitizeString(detected.name);
220
+ const sanitizedCategory = sanitizeString(detected.category);
221
+ const existing = store.signals.find(s => s.category === sanitizedCategory && s.pattern === sanitizedName);
178
222
  const now = new Date().toISOString();
223
+ const relSourceFiles = sourceFiles.map(f => relative(this.projectRoot, f));
179
224
  if (existing) {
180
225
  if (!existing.incrementIds.includes(incrementId)) {
181
226
  existing.incrementIds.push(incrementId);
182
227
  }
183
228
  existing.lastSeen = now;
184
- // Merge evidence (deduplicated)
229
+ // Update uniqueSourceFiles with Set semantics
230
+ const fileSet = new Set(existing.uniqueSourceFiles ?? []);
231
+ for (const f of relSourceFiles) {
232
+ fileSet.add(f);
233
+ }
234
+ existing.uniqueSourceFiles = Array.from(fileSet);
235
+ // Merge evidence (deduplicated) then cap
185
236
  for (const ev of detected.evidence) {
186
- if (!existing.evidence.includes(ev)) {
187
- existing.evidence.push(ev);
237
+ const sanitizedEv = sanitizeString(ev);
238
+ if (!existing.evidence.includes(sanitizedEv)) {
239
+ existing.evidence.push(sanitizedEv);
188
240
  }
189
241
  }
190
- // Recalculate confidence based on increment count
191
- existing.confidence = Math.min(1.0, existing.incrementIds.length / this.minSignalCount);
242
+ existing.evidence = capEvidence(existing.evidence);
243
+ // Confidence = uniqueSourceFiles.length / minSignalCount capped at 1.0
244
+ existing.confidence = Math.min(1.0, existing.uniqueSourceFiles.length / this.minSignalCount);
192
245
  }
193
246
  else {
194
- const meta = PATTERN_CATEGORIES[detected.category];
247
+ const uniqueFiles = Array.from(new Set(relSourceFiles));
195
248
  const newSignal = {
196
- id: `sig-${detected.category}`,
197
- pattern: `${detected.category}-pattern`,
198
- category: detected.category,
199
- description: meta?.description ?? `Detected ${detected.category} pattern`,
249
+ id: `sig-${sanitizedCategory}-${sanitizedName}`,
250
+ pattern: sanitizedName,
251
+ category: sanitizedCategory,
252
+ description: sanitizeString(detected.description),
200
253
  incrementIds: [incrementId],
201
254
  firstSeen: now,
202
255
  lastSeen: now,
203
- confidence: 1 / this.minSignalCount,
204
- evidence: detected.evidence,
256
+ confidence: Math.min(1.0, uniqueFiles.length / this.minSignalCount),
257
+ evidence: capEvidence(detected.evidence.map(e => sanitizeString(e))),
258
+ uniqueSourceFiles: uniqueFiles,
205
259
  suggested: false,
206
260
  declined: false,
207
261
  generated: false,