@massu/core 1.10.8 → 1.12.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.
Files changed (178) hide show
  1. package/CHANGELOG.md +1303 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +517 -397
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +66 -18
  16. package/dist/hooks/pre-compact.js +27 -2
  17. package/dist/hooks/pre-delete-check.js +48 -22
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +24 -0
  23. package/dist/hooks/session-start.js +8710 -8460
  24. package/dist/hooks/user-prompt.js +24 -0
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/framework-supports.ts +0 -85
  110. package/src/lib/gitToplevel.ts +0 -22
  111. package/src/lib/hook-registry.ts +0 -43
  112. package/src/lib/installLock.ts +0 -66
  113. package/src/lib/memory-path.ts +0 -49
  114. package/src/lib/pidLiveness.ts +0 -67
  115. package/src/lib/settings-local.ts +0 -110
  116. package/src/lib/sql-table-names.ts +0 -71
  117. package/src/license.ts +0 -547
  118. package/src/lsp/auto-detect.ts +0 -98
  119. package/src/lsp/client.ts +0 -776
  120. package/src/lsp/enrich.ts +0 -127
  121. package/src/lsp/types.ts +0 -221
  122. package/src/memory-db.ts +0 -1614
  123. package/src/memory-file-ingest.ts +0 -128
  124. package/src/memory-tools.ts +0 -424
  125. package/src/middleware-tree.ts +0 -71
  126. package/src/observability-tools.ts +0 -343
  127. package/src/observation-extractor.ts +0 -418
  128. package/src/page-deps.ts +0 -285
  129. package/src/permissions.ts +0 -422
  130. package/src/prompt-analyzer.ts +0 -332
  131. package/src/python/coupling-detector.ts +0 -125
  132. package/src/python/domain-enforcer.ts +0 -84
  133. package/src/python/impact-analyzer.ts +0 -96
  134. package/src/python/import-parser.ts +0 -244
  135. package/src/python/import-resolver.ts +0 -136
  136. package/src/python/migration-indexer.ts +0 -116
  137. package/src/python/migration-parser.ts +0 -332
  138. package/src/python/model-indexer.ts +0 -71
  139. package/src/python/model-parser.ts +0 -279
  140. package/src/python/route-indexer.ts +0 -59
  141. package/src/python/route-parser.ts +0 -317
  142. package/src/python-tools.ts +0 -630
  143. package/src/regression-detector.ts +0 -319
  144. package/src/rules.ts +0 -57
  145. package/src/schema-mapper.ts +0 -232
  146. package/src/security/adapter-origin.ts +0 -130
  147. package/src/security/adapter-verifier.ts +0 -319
  148. package/src/security/atomic-write.ts +0 -164
  149. package/src/security/fetcher.ts +0 -200
  150. package/src/security/install-tracking.ts +0 -319
  151. package/src/security/license-pubkey.generated.ts +0 -30
  152. package/src/security/license-response-verifier.ts +0 -149
  153. package/src/security/local-fingerprint.ts +0 -225
  154. package/src/security/manifest-cache.ts +0 -333
  155. package/src/security/manifest-schema.ts +0 -129
  156. package/src/security/registry-pubkey.generated.ts +0 -35
  157. package/src/security/telemetry.ts +0 -320
  158. package/src/security-scorer.ts +0 -405
  159. package/src/security-utils.ts +0 -133
  160. package/src/sentinel-db.ts +0 -580
  161. package/src/sentinel-scanner.ts +0 -406
  162. package/src/sentinel-tools.ts +0 -513
  163. package/src/sentinel-types.ts +0 -140
  164. package/src/server-dispatch.ts +0 -225
  165. package/src/server.ts +0 -107
  166. package/src/session-archiver.ts +0 -111
  167. package/src/session-state-generator.ts +0 -174
  168. package/src/team-knowledge.ts +0 -407
  169. package/src/tool-db-needs.ts +0 -232
  170. package/src/tools.ts +0 -1206
  171. package/src/transcript-parser.ts +0 -458
  172. package/src/trpc-index.ts +0 -215
  173. package/src/validate-features-runner.ts +0 -107
  174. package/src/validation-engine.ts +0 -358
  175. package/src/watch/daemon.ts +0 -385
  176. package/src/watch/lockfile-detector.ts +0 -65
  177. package/src/watch/paths.ts +0 -279
  178. package/src/watch/state.ts +0 -178
@@ -1,708 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- import type Database from 'better-sqlite3';
5
- import { createHash } from 'crypto';
6
- import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
7
- import { resolve, relative, basename, extname } from 'path';
8
- import { getConfig, getResolvedPaths, getProjectRoot } from './config.ts';
9
-
10
- // ============================================================
11
- // Types
12
- // ============================================================
13
-
14
- interface IndexStats {
15
- filesIndexed: number;
16
- chunksCreated: number;
17
- edgesCreated: number;
18
- }
19
-
20
- interface CRRule {
21
- rule_id: string;
22
- rule_text: string;
23
- vr_type: string;
24
- reference_path: string;
25
- }
26
-
27
- interface VRType {
28
- vr_type: string;
29
- command: string;
30
- expected: string;
31
- use_when: string;
32
- catches?: string;
33
- category?: string;
34
- }
35
-
36
- interface IncidentRow {
37
- incident_num: number;
38
- date: string;
39
- type: string;
40
- gap_found: string;
41
- prevention: string;
42
- cr_added?: string;
43
- }
44
-
45
- interface SchemaMismatch {
46
- table_name: string;
47
- wrong_column: string;
48
- correct_column: string;
49
- }
50
-
51
- interface Section {
52
- heading: string;
53
- content: string;
54
- line_start: number;
55
- line_end: number;
56
- }
57
-
58
- // ============================================================
59
- // Resolved Knowledge Paths
60
- // ============================================================
61
-
62
- /**
63
- * Get resolved paths for knowledge indexing.
64
- * These are derived from config and project root, not hardcoded.
65
- */
66
- function getKnowledgePaths() {
67
- const resolved = getResolvedPaths();
68
- const config = getConfig();
69
- const root = getProjectRoot();
70
-
71
- return {
72
- /** .claude/ directory at project root (config-driven) */
73
- claudeDir: resolved.claudeDir,
74
-
75
- /** Claude memory directory (user-level, project-scoped, config-driven) */
76
- memoryDir: resolved.memoryDir,
77
-
78
- /** Plans directory (config-driven) */
79
- plansDir: resolved.plansDir,
80
-
81
- /** Docs directory (config-driven) */
82
- docsDir: resolved.docsDir,
83
-
84
- /** Knowledge database path (config-driven) */
85
- knowledgeDbPath: resolved.knowledgeDbPath,
86
-
87
- /** Project root */
88
- projectRoot: root,
89
-
90
- /** Project name */
91
- projectName: config.project.name,
92
- };
93
- }
94
-
95
- // ============================================================
96
- // File Discovery
97
- // ============================================================
98
-
99
- function discoverMarkdownFiles(baseDir: string): string[] {
100
- const files: string[] = [];
101
- function walk(dir: string): void {
102
- try {
103
- const entries = readdirSync(dir, { withFileTypes: true });
104
- for (const entry of entries) {
105
- const fullPath = resolve(dir, entry.name);
106
- if (entry.isDirectory()) {
107
- // Skip session-state/archive (ephemeral, 80+ files)
108
- if (entry.name === 'archive' && dir.includes('session-state')) continue;
109
- // Skip status/archive
110
- if (entry.name === 'archive' && dir.includes('status')) continue;
111
- // Skip node_modules
112
- if (entry.name === 'node_modules') continue;
113
- walk(fullPath);
114
- } else if (entry.isFile() && extname(entry.name) === '.md') {
115
- files.push(fullPath);
116
- }
117
- }
118
- } catch {
119
- // Directory may not exist
120
- }
121
- }
122
- walk(baseDir);
123
- return files;
124
- }
125
-
126
- export function categorizeFile(filePath: string): string {
127
- const paths = getKnowledgePaths();
128
-
129
- // Plan and docs paths checked FIRST — external dirs produce bad relative paths from .claude/
130
- if (filePath.startsWith(paths.plansDir)) return 'plan';
131
-
132
- // Categorize docs subdirectories
133
- if (filePath.startsWith(paths.docsDir)) {
134
- const relFromDocs = relative(paths.docsDir, filePath).replace(/\\/g, '/').toLowerCase();
135
- if (relFromDocs.startsWith('plans/')) return 'plan';
136
- if (relFromDocs.includes('architecture')) return 'architecture';
137
- if (relFromDocs.includes('security')) return 'security';
138
- if (relFromDocs.includes('deployment')) return 'deployment';
139
- if (relFromDocs.includes('testing')) return 'testing';
140
- if (relFromDocs.includes('database')) return 'database-docs';
141
- if (relFromDocs.includes('audits') || relFromDocs.includes('audit')) return 'audit';
142
- if (relFromDocs.includes('analysis')) return 'analysis';
143
- if (relFromDocs.includes('development-intelligence')) return 'dev-intelligence';
144
- if (relFromDocs.includes('reports')) return 'reports';
145
- if (relFromDocs.includes('strategy')) return 'strategy';
146
- return 'docs';
147
- }
148
-
149
- // Memory directory (user-level Claude memory)
150
- const claudeDirName = getConfig().conventions?.claudeDirName ?? '.claude';
151
- if (filePath.includes(`${claudeDirName}/projects/`) && filePath.includes('/memory/')) return 'memory';
152
-
153
- const rel = relative(paths.claudeDir, filePath).replace(/\\/g, '/');
154
- const firstDir = rel.split('/')[0];
155
- const knownCategories = getConfig().conventions?.knowledgeCategories ?? [
156
- 'patterns', 'commands', 'incidents', 'reference', 'protocols',
157
- 'checklists', 'playbooks', 'critical', 'scripts', 'status',
158
- 'templates', 'loop-state', 'session-state', 'agents',
159
- ];
160
- if (knownCategories.includes(firstDir)) return firstDir;
161
- // Files at .claude/ root (like CLAUDE.md)
162
- return 'root';
163
- }
164
-
165
- function hashContent(content: string): string {
166
- return createHash('sha256').update(content).digest('hex');
167
- }
168
-
169
- // ============================================================
170
- // Markdown Parsers
171
- // ============================================================
172
-
173
- export function parseCRTable(content: string): CRRule[] {
174
- const rules: CRRule[] = [];
175
- // Match CR table rows: | CR-N | Rule text | VR-* | reference |
176
- const tableRegex = /\|\s*(CR-\d+)\s*\|\s*([^|]+)\|\s*([^|]+)\|\s*\[?([^\]|]+)\]?[^|]*\|/g;
177
- let match: RegExpExecArray | null;
178
- while ((match = tableRegex.exec(content)) !== null) {
179
- rules.push({
180
- rule_id: match[1].trim(),
181
- rule_text: match[2].trim(),
182
- vr_type: match[3].trim(),
183
- reference_path: match[4].trim().replace(/\(.*\)/, '').trim(),
184
- });
185
- }
186
- return rules;
187
- }
188
-
189
- export function parseVRTable(content: string): VRType[] {
190
- const types: VRType[] = [];
191
- // Match VR table rows: | VR-* | `command` | expected | use when |
192
- const tableRegex = /\|\s*(VR-[\w-]+)\s*\|\s*`([^`]+)`\s*\|\s*([^|]+)\|\s*([^|]+)\|/g;
193
- let match: RegExpExecArray | null;
194
- while ((match = tableRegex.exec(content)) !== null) {
195
- types.push({
196
- vr_type: match[1].trim(),
197
- command: match[2].trim(),
198
- expected: match[3].trim(),
199
- use_when: match[4].trim(),
200
- });
201
- }
202
- return types;
203
- }
204
-
205
- export function parseIncidents(content: string): IncidentRow[] {
206
- const incidents: IncidentRow[] = [];
207
- // Match incident summary table rows: | N | date | type | gap | prevention |
208
- const tableRegex = /\|\s*(\d+)\s*\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|/g;
209
- let match: RegExpExecArray | null;
210
- while ((match = tableRegex.exec(content)) !== null) {
211
- const num = parseInt(match[1].trim(), 10);
212
- if (isNaN(num) || num === 0) continue; // Skip header
213
- incidents.push({
214
- incident_num: num,
215
- date: match[2].trim(),
216
- type: match[3].trim(),
217
- gap_found: match[4].trim(),
218
- prevention: match[5].trim(),
219
- });
220
- }
221
- return incidents;
222
- }
223
-
224
- export function parseSchemaMismatches(content: string): SchemaMismatch[] {
225
- const mismatches: SchemaMismatch[] = [];
226
- // Match: | table_name | wrong_column | correct_column |
227
- // Look for the specific "Known Schema Mismatches" section (stop at next H2/H3 heading, not at ---)
228
- const sectionMatch = content.match(/### Known Schema Mismatches[\s\S]*?(?=\n##\s|\n---\n|$)/);
229
- if (!sectionMatch) return mismatches;
230
-
231
- const section = sectionMatch[0];
232
- // Match table data rows: | word | word | word | (skips header/separator via word-char check)
233
- const rowRegex = /\|\s*(\w+)\s*\|\s*(\w+)\s*\|\s*(\w+)\s*\|/g;
234
- let match: RegExpExecArray | null;
235
- while ((match = rowRegex.exec(section)) !== null) {
236
- // Skip header row (has "Table" or "WRONG" etc.)
237
- if (match[1] === 'Table' || match[2] === 'WRONG' || match[1].startsWith('-')) continue;
238
- mismatches.push({
239
- table_name: match[1].trim(),
240
- wrong_column: match[2].trim(),
241
- correct_column: match[3].trim(),
242
- });
243
- }
244
- return mismatches;
245
- }
246
-
247
- export function parseSections(content: string, _filePath: string): Section[] {
248
- if (!content.trim()) return [];
249
-
250
- const sections: Section[] = [];
251
- const lines = content.split('\n');
252
- let currentHeading = '';
253
- let currentContent: string[] = [];
254
- let currentStart = 1;
255
-
256
- for (let i = 0; i < lines.length; i++) {
257
- const line = lines[i];
258
- const headingMatch = line.match(/^(#{2,3})\s+(.+)/);
259
- if (headingMatch) {
260
- // Save previous section
261
- if (currentContent.length > 0) {
262
- sections.push({
263
- heading: currentHeading,
264
- content: currentContent.join('\n').trim(),
265
- line_start: currentStart,
266
- line_end: i,
267
- });
268
- }
269
- currentHeading = headingMatch[2].trim();
270
- currentContent = [];
271
- currentStart = i + 1;
272
- } else {
273
- currentContent.push(line);
274
- }
275
- }
276
-
277
- // Last section
278
- if (currentContent.length > 0) {
279
- sections.push({
280
- heading: currentHeading,
281
- content: currentContent.join('\n').trim(),
282
- line_start: currentStart,
283
- line_end: lines.length,
284
- });
285
- }
286
-
287
- return sections;
288
- }
289
-
290
- // ============================================================
291
- // Corrections Parser
292
- // ============================================================
293
-
294
- export interface CorrectionEntry {
295
- date: string;
296
- title: string;
297
- wrong: string;
298
- correction: string;
299
- rule: string;
300
- cr_rule?: string;
301
- }
302
-
303
- export function parseCorrections(content: string): CorrectionEntry[] {
304
- const entries: CorrectionEntry[] = [];
305
- const entryRegex = /### (\d{4}-\d{2}-\d{2}) - ([^\n]+)\n([\s\S]*?)(?=\n### |\n## |$)/g;
306
- let match;
307
- while ((match = entryRegex.exec(content)) !== null) {
308
- const block = match[0];
309
- const date = match[1];
310
- const title = match[2];
311
- const wrong = block.match(/\*\*Wrong\*\*:\s*(.+)/)?.[1] || '';
312
- const correction = block.match(/\*\*Correction\*\*:\s*(.+)/)?.[1] || '';
313
- const rule = block.match(/\*\*Rule\*\*:\s*(.+)/)?.[1] || '';
314
- const cr = block.match(/\*\*CR\*\*:\s*(CR-\d+)/)?.[1];
315
- entries.push({ date, title, wrong, correction, rule, cr_rule: cr });
316
- }
317
- return entries;
318
- }
319
-
320
- function extractTitle(content: string, filePath: string): string {
321
- const h1Match = content.match(/^#\s+(.+)/m);
322
- if (h1Match) return h1Match[1].trim();
323
- return basename(filePath, '.md');
324
- }
325
-
326
- function extractDescription(content: string): string | null {
327
- // Try frontmatter description
328
- const fmMatch = content.match(/^---\s*\n[\s\S]*?description:\s*"?([^"\n]+)"?\s*\n[\s\S]*?---/);
329
- if (fmMatch) return fmMatch[1].trim();
330
- // First non-heading, non-empty paragraph
331
- const lines = content.split('\n');
332
- for (const line of lines) {
333
- const trimmed = line.trim();
334
- if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('---') && !trimmed.startsWith('|') && trimmed.length > 20) {
335
- return trimmed.substring(0, 200);
336
- }
337
- }
338
- return null;
339
- }
340
-
341
- // ============================================================
342
- // Cross-Reference Builder
343
- // ============================================================
344
-
345
- export function buildCrossReferences(db: Database.Database): number {
346
- let edgeCount = 0;
347
- const insertEdge = db.prepare(
348
- 'INSERT OR IGNORE INTO knowledge_edges (source_type, source_id, target_type, target_id, edge_type) VALUES (?, ?, ?, ?, ?)'
349
- );
350
-
351
- // CR -> VR edges (from knowledge_rules)
352
- const rules = db.prepare('SELECT rule_id, vr_type, reference_path FROM knowledge_rules LIMIT 10000').all() as CRRule[];
353
- for (const rule of rules) {
354
- if (rule.vr_type && rule.vr_type !== 'VR-*') {
355
- // Split compound VR types (e.g., "VR-SCHEMA" or "VR-*")
356
- const vrTypes = rule.vr_type.split(/[,\s]+/).filter(v => v.startsWith('VR-'));
357
- for (const vr of vrTypes) {
358
- insertEdge.run('cr', rule.rule_id, 'vr', vr, 'enforced_by');
359
- edgeCount++;
360
- }
361
- }
362
- if (rule.reference_path) {
363
- const patternName = basename(rule.reference_path, '.md');
364
- insertEdge.run('cr', rule.rule_id, 'pattern', patternName, 'references');
365
- edgeCount++;
366
- }
367
- }
368
-
369
- // Incident -> CR edges (from knowledge_incidents)
370
- const incidents = db.prepare('SELECT incident_num, cr_added FROM knowledge_incidents WHERE cr_added IS NOT NULL LIMIT 10000').all() as { incident_num: number; cr_added: string }[];
371
- for (const inc of incidents) {
372
- if (inc.cr_added) {
373
- const crIds = inc.cr_added.match(/CR-\d+/g) || [];
374
- for (const crId of crIds) {
375
- insertEdge.run('incident', String(inc.incident_num), 'cr', crId, 'caused');
376
- edgeCount++;
377
- }
378
- }
379
- }
380
-
381
- // Scan all chunks for cross-references
382
- const chunks = db.prepare('SELECT id, content, metadata FROM knowledge_chunks LIMIT 100000').all() as { id: number; content: string; metadata: string }[];
383
- for (const chunk of chunks) {
384
- const text = chunk.content;
385
-
386
- // Find CR references in content
387
- const crRefs = text.match(/CR-\d+/g);
388
- if (crRefs) {
389
- for (const cr of [...new Set(crRefs)]) {
390
- insertEdge.run('chunk', String(chunk.id), 'cr', cr, 'references');
391
- edgeCount++;
392
- }
393
- }
394
-
395
- // Find VR references
396
- const vrRefs = text.match(/VR-[\w-]+/g);
397
- if (vrRefs) {
398
- for (const vr of [...new Set(vrRefs)]) {
399
- insertEdge.run('chunk', String(chunk.id), 'vr', vr, 'references');
400
- edgeCount++;
401
- }
402
- }
403
-
404
- // Find incident references
405
- const incRefs = text.match(/Incident #(\d+)/gi);
406
- if (incRefs) {
407
- for (const ref of incRefs) {
408
- const numMatch = ref.match(/\d+/);
409
- if (numMatch) {
410
- insertEdge.run('chunk', String(chunk.id), 'incident', numMatch[0], 'references');
411
- edgeCount++;
412
- }
413
- }
414
- }
415
- }
416
-
417
- return edgeCount;
418
- }
419
-
420
- // ============================================================
421
- // Indexer Functions
422
- // ============================================================
423
-
424
- export function indexAllKnowledge(db: Database.Database): IndexStats {
425
- const stats: IndexStats = { filesIndexed: 0, chunksCreated: 0, edgesCreated: 0 };
426
- const paths = getKnowledgePaths();
427
-
428
- const insertDoc = db.prepare(
429
- 'INSERT INTO knowledge_documents (file_path, category, title, description, content_hash, indexed_at, indexed_at_epoch) VALUES (?, ?, ?, ?, ?, ?, ?)'
430
- );
431
- const insertChunk = db.prepare(
432
- 'INSERT INTO knowledge_chunks (document_id, chunk_type, heading, content, line_start, line_end, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)'
433
- );
434
- const insertRule = db.prepare(
435
- 'INSERT OR IGNORE INTO knowledge_rules (rule_id, rule_text, vr_type, reference_path) VALUES (?, ?, ?, ?)'
436
- );
437
- const insertVR = db.prepare(
438
- 'INSERT OR IGNORE INTO knowledge_verifications (vr_type, command, expected, use_when, catches, category) VALUES (?, ?, ?, ?, ?, ?)'
439
- );
440
- const insertIncident = db.prepare(
441
- 'INSERT OR IGNORE INTO knowledge_incidents (incident_num, date, type, gap_found, prevention) VALUES (?, ?, ?, ?, ?)'
442
- );
443
- // P-H013 (plan-stage-c-high-batch): source column now bound at insert time
444
- // (was a SQL-DEFAULT template-literal interpolation baked into the customer's
445
- // SQLite at schema creation time, ignoring later config changes).
446
- const insertMismatch = db.prepare(
447
- 'INSERT INTO knowledge_schema_mismatches (table_name, wrong_column, correct_column, source) VALUES (?, ?, ?, ?)'
448
- );
449
- const defaultSource = getConfig().conventions?.knowledgeSourceFiles?.[0] ?? 'CLAUDE.md';
450
-
451
- // Discover all .claude/ markdown files
452
- const files = discoverMarkdownFiles(paths.claudeDir);
453
-
454
- // Also index memory directory (different location)
455
- try {
456
- const memFiles = discoverMarkdownFiles(paths.memoryDir);
457
- files.push(...memFiles);
458
- } catch {
459
- // Memory dir may not exist
460
- }
461
-
462
- // Scan plan documents
463
- if (existsSync(paths.plansDir)) {
464
- const planFiles = discoverMarkdownFiles(paths.plansDir);
465
- files.push(...planFiles);
466
- }
467
-
468
- // Scan broader docs (skip plans/ since already scanned, skip configured exclude patterns)
469
- if (existsSync(paths.docsDir)) {
470
- const excludePatterns = getConfig().conventions?.excludePatterns ?? ['/ARCHIVE/', '/SESSION-HISTORY/'];
471
- const docsFiles = discoverMarkdownFiles(paths.docsDir)
472
- .filter(f => !f.includes('/plans/') && !excludePatterns.some(p => f.includes(p)));
473
- files.push(...docsFiles);
474
- }
475
-
476
- const now = new Date();
477
- const nowIso = now.toISOString();
478
- const nowEpoch = now.getTime();
479
-
480
- const transaction = db.transaction(() => {
481
- // Atomic reindex: clear everything inside transaction so rollback restores data on failure
482
- // Drop FTS5 triggers before bulk deletion to avoid trigger errors on empty FTS5 table
483
- db.exec('DROP TRIGGER IF EXISTS kc_fts_delete');
484
- db.exec('DROP TRIGGER IF EXISTS kc_fts_update');
485
- db.exec('DELETE FROM knowledge_edges');
486
- db.exec('DELETE FROM knowledge_fts');
487
- db.exec('DELETE FROM knowledge_chunks');
488
- db.exec('DELETE FROM knowledge_documents');
489
- db.exec('DELETE FROM knowledge_rules');
490
- db.exec('DELETE FROM knowledge_verifications');
491
- db.exec('DELETE FROM knowledge_incidents');
492
- db.exec('DELETE FROM knowledge_schema_mismatches');
493
-
494
- // Recreate FTS5 triggers for the insert phase
495
- try {
496
- db.exec(`
497
- CREATE TRIGGER IF NOT EXISTS kc_fts_insert AFTER INSERT ON knowledge_chunks BEGIN
498
- INSERT INTO knowledge_fts(rowid, heading, content, chunk_type, file_path)
499
- SELECT new.id, new.heading, new.content, new.chunk_type, kd.file_path
500
- FROM knowledge_documents kd WHERE kd.id = new.document_id;
501
- END;
502
- CREATE TRIGGER IF NOT EXISTS kc_fts_delete AFTER DELETE ON knowledge_chunks BEGIN
503
- INSERT INTO knowledge_fts(knowledge_fts, rowid, heading, content, chunk_type, file_path)
504
- SELECT 'delete', old.id, old.heading, old.content, old.chunk_type, kd.file_path
505
- FROM knowledge_documents kd WHERE kd.id = old.document_id;
506
- END;
507
- CREATE TRIGGER IF NOT EXISTS kc_fts_update AFTER UPDATE ON knowledge_chunks BEGIN
508
- INSERT INTO knowledge_fts(knowledge_fts, rowid, heading, content, chunk_type, file_path)
509
- SELECT 'delete', old.id, old.heading, old.content, old.chunk_type, kd.file_path
510
- FROM knowledge_documents kd WHERE kd.id = old.document_id;
511
- INSERT INTO knowledge_fts(rowid, heading, content, chunk_type, file_path)
512
- SELECT new.id, new.heading, new.content, new.chunk_type, kd.file_path
513
- FROM knowledge_documents kd WHERE kd.id = new.document_id;
514
- END;
515
- `);
516
- } catch { /* Triggers may already exist */ }
517
-
518
- for (const filePath of files) {
519
- if (!existsSync(filePath)) continue;
520
- const content = readFileSync(filePath, 'utf-8');
521
- const hash = hashContent(content);
522
- const relPath = filePath.startsWith(paths.claudeDir)
523
- ? relative(paths.claudeDir, filePath)
524
- : filePath.startsWith(paths.plansDir)
525
- ? 'plans/' + relative(paths.plansDir, filePath)
526
- : filePath.startsWith(paths.docsDir)
527
- ? 'docs/' + relative(paths.docsDir, filePath)
528
- : filePath.startsWith(paths.memoryDir)
529
- ? `memory/${relative(paths.memoryDir, filePath)}`
530
- : basename(filePath);
531
- const category = categorizeFile(filePath);
532
- const title = extractTitle(content, filePath);
533
- const description = extractDescription(content);
534
-
535
- // Insert document (documents FIRST — triggers need parent row)
536
- const result = insertDoc.run(relPath, category, title, description, hash, nowIso, nowEpoch);
537
- const docId = result.lastInsertRowid;
538
- stats.filesIndexed++;
539
-
540
- // Parse sections into chunks (triggers auto-populate FTS5)
541
- const sections = parseSections(content, filePath);
542
- for (const section of sections) {
543
- if (section.content.length > 10) { // Skip trivially small sections
544
- insertChunk.run(docId, 'section', section.heading, section.content, section.line_start, section.line_end, '{}');
545
- stats.chunksCreated++;
546
- }
547
- }
548
-
549
- // Special parsing for specific files
550
- const fileName = basename(filePath);
551
- const fileNameLower = fileName.toLowerCase();
552
- const relPathLower = relPath.toLowerCase();
553
-
554
- // Check if this file is the main CLAUDE.md (config-driven filename)
555
- const claudeMdName = basename(getResolvedPaths().claudeMdPath).toLowerCase();
556
- if (fileNameLower === claudeMdName || relPathLower.includes(claudeMdName)) {
557
- // Extract CR rules
558
- const crRules = parseCRTable(content);
559
- for (const rule of crRules) {
560
- insertRule.run(rule.rule_id, rule.rule_text, rule.vr_type, rule.reference_path);
561
- insertChunk.run(docId, 'rule', rule.rule_id, `${rule.rule_text} | VR: ${rule.vr_type}`, null, null, JSON.stringify({ cr_id: rule.rule_id, vr_type: rule.vr_type }));
562
- stats.chunksCreated++;
563
- }
564
-
565
- // Extract VR types
566
- const vrTypes = parseVRTable(content);
567
- for (const vr of vrTypes) {
568
- insertVR.run(vr.vr_type, vr.command, vr.expected, vr.use_when, vr.catches || null, vr.category || 'core');
569
- }
570
-
571
- // Extract schema mismatches
572
- const mismatches = parseSchemaMismatches(content);
573
- for (const m of mismatches) {
574
- insertMismatch.run(m.table_name, m.wrong_column, m.correct_column, defaultSource);
575
- insertChunk.run(docId, 'mismatch', m.table_name, `${m.table_name}: ${m.wrong_column} -> ${m.correct_column}`, null, null, JSON.stringify({ table: m.table_name }));
576
- stats.chunksCreated++;
577
- }
578
- }
579
-
580
- if (fileNameLower === 'incident-log.md') {
581
- const incidents = parseIncidents(content);
582
- for (const inc of incidents) {
583
- insertIncident.run(inc.incident_num, inc.date, inc.type, inc.gap_found, inc.prevention);
584
- insertChunk.run(docId, 'incident', `Incident #${inc.incident_num}`, `${inc.type}: ${inc.gap_found} | Prevention: ${inc.prevention}`, null, null, JSON.stringify({ incident_num: inc.incident_num }));
585
- stats.chunksCreated++;
586
- }
587
- }
588
-
589
- if (fileNameLower === 'vr-verification-reference.md') {
590
- const vrTypes = parseVRTable(content);
591
- for (const vr of vrTypes) {
592
- insertVR.run(vr.vr_type, vr.command, vr.expected, vr.use_when, vr.catches || null, vr.category || null);
593
- }
594
- }
595
-
596
- // Index commands
597
- if (category === 'commands' && fileName !== '_shared-preamble.md') {
598
- const cmdName = basename(filePath, '.md');
599
- insertChunk.run(docId, 'command', cmdName, content.substring(0, 1000), 1, null, JSON.stringify({ command_name: cmdName }));
600
- stats.chunksCreated++;
601
- }
602
-
603
- // Parse plan documents for structured metadata
604
- if (category === 'plan') {
605
- // Extract plan items (P1-001, P2-001, etc.)
606
- const planItemRegex = /^###\s+(P\d+-\d+):\s+(.+)$/gm;
607
- let planMatch;
608
- while ((planMatch = planItemRegex.exec(content)) !== null) {
609
- insertChunk.run(docId, 'pattern', planMatch[1], `${planMatch[1]}: ${planMatch[2]}`, null, null, JSON.stringify({ plan_item_id: planMatch[1] }));
610
- stats.chunksCreated++;
611
- }
612
-
613
- // Extract IMPLEMENTATION STATUS if present
614
- const statusMatch = content.match(/# IMPLEMENTATION STATUS[\s\S]*?\n(?=\n#[^#]|\n---|$)/);
615
- if (statusMatch) {
616
- insertChunk.run(docId, 'section', 'IMPLEMENTATION STATUS', statusMatch[0], null, null, '{}');
617
- stats.chunksCreated++;
618
- }
619
-
620
- // Extract file paths mentioned in plan (src/*, scripts/*)
621
- const fileRefRegex = /(?:src|scripts)\/[\w\-\/]+\.(?:ts|tsx|sql|md)/g;
622
- const fileRefs = [...new Set(content.match(fileRefRegex) || [])];
623
- if (fileRefs.length > 0) {
624
- const fileRefsChunk = fileRefs.join('\n');
625
- insertChunk.run(docId, 'section', 'Referenced Files', fileRefsChunk, null, null, JSON.stringify({ file_refs: fileRefs }));
626
- stats.chunksCreated++;
627
- }
628
- }
629
-
630
- // Parse corrections.md for structured correction entries
631
- if (fileNameLower === 'corrections.md') {
632
- const corrections = parseCorrections(content);
633
- for (const c of corrections) {
634
- insertChunk.run(docId, 'section', `Correction: ${c.title}`,
635
- `Wrong: ${c.wrong}\nCorrection: ${c.correction}\nRule: ${c.rule}`,
636
- null, null, JSON.stringify({ is_correction: true, date: c.date, cr_rule: c.cr_rule }));
637
- stats.chunksCreated++;
638
- if (c.cr_rule) {
639
- db.prepare('INSERT OR IGNORE INTO knowledge_edges (source_type, source_id, target_type, target_id, edge_type) VALUES (?, ?, ?, ?, ?)')
640
- .run('correction', c.title, 'cr', c.cr_rule, 'enforces');
641
- }
642
- }
643
- }
644
- }
645
-
646
- // Build cross-references after all data inserted
647
- stats.edgesCreated = buildCrossReferences(db);
648
-
649
- // Update staleness metadata — use current time AFTER indexing to avoid race conditions
650
- // where files modified during indexing appear stale immediately
651
- const finalNow = new Date();
652
- const finalIso = finalNow.toISOString();
653
- const finalEpoch = finalNow.getTime();
654
- db.prepare("INSERT OR REPLACE INTO knowledge_meta (key, value) VALUES ('last_index_time', ?)").run(finalIso);
655
- db.prepare("INSERT OR REPLACE INTO knowledge_meta (key, value) VALUES ('last_index_epoch', ?)").run(String(finalEpoch));
656
- db.prepare("INSERT OR REPLACE INTO knowledge_meta (key, value) VALUES ('files_indexed', ?)").run(String(stats.filesIndexed));
657
- });
658
-
659
- transaction();
660
- return stats;
661
- }
662
-
663
- export function isKnowledgeStale(db: Database.Database): boolean {
664
- const lastEpoch = db.prepare("SELECT value FROM knowledge_meta WHERE key = 'last_index_epoch'").get() as { value: string } | undefined;
665
- if (!lastEpoch) return true;
666
-
667
- const lastIndexTime = parseInt(lastEpoch.value, 10);
668
- if (isNaN(lastIndexTime)) return true;
669
-
670
- const paths = getKnowledgePaths();
671
-
672
- // Check if any .claude/ file has been modified since last index
673
- const files = discoverMarkdownFiles(paths.claudeDir);
674
-
675
- // Also check memory directory for staleness
676
- try {
677
- files.push(...discoverMarkdownFiles(paths.memoryDir));
678
- } catch { /* Memory dir may not exist */ }
679
-
680
- // Also check plans and docs directories for staleness
681
- if (existsSync(paths.plansDir)) {
682
- files.push(...discoverMarkdownFiles(paths.plansDir));
683
- }
684
- if (existsSync(paths.docsDir)) {
685
- const excludePatterns = getConfig().conventions?.excludePatterns ?? ['/ARCHIVE/', '/SESSION-HISTORY/'];
686
- const docsFiles = discoverMarkdownFiles(paths.docsDir)
687
- .filter(f => !f.includes('/plans/') && !excludePatterns.some(p => f.includes(p)));
688
- files.push(...docsFiles);
689
- }
690
-
691
- for (const filePath of files) {
692
- try {
693
- const stat = statSync(filePath);
694
- if (stat.mtimeMs > lastIndexTime) return true;
695
- } catch {
696
- continue;
697
- }
698
- }
699
-
700
- return false;
701
- }
702
-
703
- export function indexIfStale(db: Database.Database): IndexStats {
704
- if (isKnowledgeStale(db)) {
705
- return indexAllKnowledge(db);
706
- }
707
- return { filesIndexed: 0, chunksCreated: 0, edgesCreated: 0 };
708
- }