specweave 1.0.486 → 1.0.488
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/dist/src/core/skill-gen/drift-detector.d.ts +5 -5
- package/dist/src/core/skill-gen/drift-detector.d.ts.map +1 -1
- package/dist/src/core/skill-gen/drift-detector.js +31 -34
- package/dist/src/core/skill-gen/drift-detector.js.map +1 -1
- package/dist/src/core/skill-gen/signal-collector.d.ts +11 -13
- package/dist/src/core/skill-gen/signal-collector.d.ts.map +1 -1
- package/dist/src/core/skill-gen/signal-collector.js +186 -132
- package/dist/src/core/skill-gen/signal-collector.js.map +1 -1
- package/dist/src/core/skill-gen/suggestion-engine.d.ts +0 -2
- package/dist/src/core/skill-gen/suggestion-engine.d.ts.map +1 -1
- package/dist/src/core/skill-gen/suggestion-engine.js +15 -22
- package/dist/src/core/skill-gen/suggestion-engine.js.map +1 -1
- package/dist/src/core/skill-gen/types.d.ts +23 -7
- package/dist/src/core/skill-gen/types.d.ts.map +1 -1
- package/dist/src/core/skill-gen/types.js.map +1 -1
- package/dist/src/core/skill-gen/utils.d.ts +73 -0
- package/dist/src/core/skill-gen/utils.d.ts.map +1 -0
- package/dist/src/core/skill-gen/utils.js +120 -0
- package/dist/src/core/skill-gen/utils.js.map +1 -0
- package/package.json +1 -1
- package/plugins/specweave/hooks/hooks.json +10 -10
- package/plugins/specweave/hooks/stop-sync.sh +16 -0
- package/plugins/specweave/hooks/universal/run-hook.sh +20 -0
- package/plugins/specweave/skills/do/SKILL.md +2 -2
- package/plugins/specweave/skills/increment/SKILL.md +3 -3
- package/plugins/specweave/skills/team-build/SKILL.md +1 -1
- package/plugins/specweave/skills/team-lead/SKILL.md +1 -1
- 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.
|
|
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
|
-
*
|
|
17
|
+
* Returns structured DriftResult[] — never throws.
|
|
17
18
|
*/
|
|
18
|
-
check(): Promise<
|
|
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;
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
|
|
50
|
-
|
|
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
|
|
55
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
|
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
|
|
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/,
|
|
6
|
-
* and persists
|
|
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
|
-
*
|
|
27
|
+
* Seed mode: scan all living docs and populate the signal store
|
|
28
|
+
* without associating patterns with any increment.
|
|
28
29
|
*/
|
|
29
|
-
|
|
30
|
+
collectSeed(): Promise<void>;
|
|
30
31
|
/**
|
|
31
|
-
*
|
|
32
|
+
* Detect patterns via LLM analysis of markdown files.
|
|
33
|
+
* Handles batching when total tokens exceed TOKEN_BUDGET.
|
|
32
34
|
*/
|
|
33
|
-
private
|
|
35
|
+
private detectPatternsLLM;
|
|
34
36
|
/**
|
|
35
|
-
*
|
|
37
|
+
* Build the user prompt with <documents> block.
|
|
36
38
|
*/
|
|
37
|
-
private
|
|
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;
|
|
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
|
|
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/,
|
|
6
|
-
* and persists
|
|
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
|
|
12
|
+
import { readFile } from 'fs/promises';
|
|
13
13
|
import { join, relative } from 'path';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
*
|
|
75
|
+
* Seed mode: scan all living docs and populate the signal store
|
|
76
|
+
* without associating patterns with any increment.
|
|
91
77
|
*/
|
|
92
|
-
async
|
|
93
|
-
const signalsPath = this.getSignalsPath();
|
|
78
|
+
async collectSeed() {
|
|
94
79
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
125
|
-
|
|
126
|
-
const
|
|
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 =
|
|
132
|
+
const content = await readFile(filePath, 'utf-8');
|
|
130
133
|
const relPath = relative(this.projectRoot, filePath);
|
|
131
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
169
|
-
|
|
168
|
+
if (currentChunk.length > 0) {
|
|
169
|
+
chunks.push(currentChunk);
|
|
170
170
|
}
|
|
171
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
187
|
-
|
|
237
|
+
const sanitizedEv = sanitizeString(ev);
|
|
238
|
+
if (!existing.evidence.includes(sanitizedEv)) {
|
|
239
|
+
existing.evidence.push(sanitizedEv);
|
|
188
240
|
}
|
|
189
241
|
}
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
247
|
+
const uniqueFiles = Array.from(new Set(relSourceFiles));
|
|
195
248
|
const newSignal = {
|
|
196
|
-
id: `sig-${
|
|
197
|
-
pattern:
|
|
198
|
-
category:
|
|
199
|
-
description:
|
|
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,
|