@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,418 +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 { ParsedToolCall, TranscriptEntry, ExtractedDecision, ExtractedFailedAttempt } from './transcript-parser.ts';
5
- import {
6
- extractToolCalls,
7
- extractDecisions,
8
- extractFailedAttempts,
9
- extractFileOperations,
10
- extractVerificationCommands,
11
- extractUserMessages,
12
- estimateTokens,
13
- } from './transcript-parser.ts';
14
- import type { AddObservationOpts } from './memory-db.ts';
15
- import { assignImportance } from './memory-db.ts';
16
- import { detectDecisionPatterns } from './adr-generator.ts';
17
- import { getProjectRoot, getConfig, getResolvedPaths } from './config.ts';
18
- import { homedir } from 'os';
19
-
20
- // ============================================================
21
- // P2-002: Observation Extractor
22
- // ============================================================
23
-
24
- /**
25
- * Visibility classification for observations.
26
- * - 'public': Safe to share across teams and sync to cloud (no secrets, no absolute paths)
27
- * - 'private': Contains potentially sensitive data (file paths, env vars, credentials)
28
- */
29
- export type ObservationVisibility = 'public' | 'private';
30
-
31
- /**
32
- * A structured observation ready for DB insertion.
33
- */
34
- export interface ExtractedObservation {
35
- type: string;
36
- title: string;
37
- detail: string | null;
38
- visibility: ObservationVisibility;
39
- opts: AddObservationOpts;
40
- }
41
-
42
- /**
43
- * Patterns that indicate private/sensitive content.
44
- * If any of these match the observation title or detail, it is classified as 'private'.
45
- */
46
- const PRIVATE_PATTERNS = [
47
- /\/Users\/\w+/, // Absolute macOS paths
48
- /\/home\/\w+/, // Absolute Linux paths
49
- /[A-Z]:\\/, // Windows paths
50
- /\b(api[_-]?key|secret|token|password|credential|dsn)\b/i, // Secrets
51
- /\b(STRIPE_|SUPABASE_|SENTRY_|AWS_|DATABASE_URL)\b/, // Env var names
52
- /\.(env|pem|key|cert)\b/, // Sensitive file extensions
53
- /Bearer\s+\S+/, // Auth tokens
54
- /sk_live_|sk_test_|whsec_/, // Stripe keys
55
- ];
56
-
57
- /**
58
- * Classify whether an observation is safe for public sharing or should remain private.
59
- */
60
- export function classifyVisibility(title: string, detail: string | null): ObservationVisibility {
61
- const text = `${title} ${detail ?? ''}`;
62
- for (const pattern of PRIVATE_PATTERNS) {
63
- if (pattern.test(text)) return 'private';
64
- }
65
- return 'public';
66
- }
67
-
68
- /**
69
- * Noise filtering rules - these tool calls add no memory value.
70
- */
71
- export function isNoisyToolCall(tc: ParsedToolCall, seenReads: Set<string>): boolean {
72
- // Glob/Grep tool calls (search operations, not actions)
73
- if (tc.toolName === 'Glob' || tc.toolName === 'Grep') return true;
74
-
75
- // Duplicate Read calls to the same file within same session
76
- if (tc.toolName === 'Read') {
77
- const filePath = tc.input.file_path as string ?? '';
78
- if (seenReads.has(filePath)) return true;
79
- seenReads.add(filePath);
80
-
81
- // Read of node_modules files
82
- if (filePath.includes('node_modules')) return true;
83
- }
84
-
85
- // Bash with trivial commands
86
- if (tc.toolName === 'Bash') {
87
- const cmd = (tc.input.command as string ?? '').trim();
88
- const trivialPatterns = /^(ls|pwd|echo|cat\s|head\s|tail\s|wc\s)/;
89
- if (trivialPatterns.test(cmd)) return true;
90
- }
91
-
92
- // Empty or error-only tool responses
93
- if (!tc.result || tc.result.trim() === '') return true;
94
-
95
- return false;
96
- }
97
-
98
- /**
99
- * Extract all observations from transcript entries.
100
- * This is the main entry point for both real-time (P3-002) and backfill (P6-001).
101
- */
102
- export function extractObservationsFromEntries(entries: TranscriptEntry[]): ExtractedObservation[] {
103
- const observations: ExtractedObservation[] = [];
104
- const seenReads = new Set<string>();
105
-
106
- // 1. Extract from tool calls
107
- const toolCalls = extractToolCalls(entries);
108
- for (const tc of toolCalls) {
109
- if (isNoisyToolCall(tc, seenReads)) continue;
110
- const obs = classifyToolCall(tc);
111
- if (obs) observations.push(obs);
112
- }
113
-
114
- // 2. Extract decisions from assistant text
115
- const decisions = extractDecisions(entries);
116
- for (const decision of decisions) {
117
- const title = decision.text.slice(0, 200);
118
- const detail = decision.context;
119
- observations.push({
120
- type: 'decision',
121
- title,
122
- detail,
123
- visibility: classifyVisibility(title, detail),
124
- opts: {
125
- importance: assignImportance('decision'),
126
- originalTokens: estimateTokens(decision.text + decision.context),
127
- },
128
- });
129
- }
130
-
131
- // 3. Extract failed attempts from assistant text
132
- const failures = extractFailedAttempts(entries);
133
- for (const failure of failures) {
134
- const title = failure.text.slice(0, 200);
135
- const detail = failure.context;
136
- observations.push({
137
- type: 'failed_attempt',
138
- title,
139
- detail,
140
- visibility: classifyVisibility(title, detail),
141
- opts: {
142
- importance: assignImportance('failed_attempt'),
143
- originalTokens: estimateTokens(failure.text + failure.context),
144
- },
145
- });
146
- }
147
-
148
- // 4. Extract verification commands
149
- const verifications = extractVerificationCommands(toolCalls);
150
- for (const vr of verifications) {
151
- const title = `${vr.vrType}: ${vr.passed ? 'PASS' : 'FAIL'}`;
152
- const detail = vr.command;
153
- observations.push({
154
- type: 'vr_check',
155
- title,
156
- detail,
157
- visibility: classifyVisibility(title, detail),
158
- opts: {
159
- vrType: vr.vrType,
160
- evidence: vr.result,
161
- importance: assignImportance('vr_check', vr.passed ? 'PASS' : 'FAIL'),
162
- originalTokens: estimateTokens(vr.result),
163
- },
164
- });
165
- }
166
-
167
- return observations;
168
- }
169
-
170
- /**
171
- * Classify a single tool call into an observation (or null if not observation-worthy).
172
- */
173
- function classifyToolCall(tc: ParsedToolCall): ExtractedObservation | null {
174
- const result = tc.result ?? '';
175
-
176
- switch (tc.toolName) {
177
- case 'Write': {
178
- const filePath = tc.input.file_path as string ?? 'unknown';
179
- const title = `Created/wrote: ${shortenPath(filePath)}`;
180
- return {
181
- type: 'file_change',
182
- title,
183
- detail: null,
184
- visibility: classifyVisibility(title, filePath),
185
- opts: {
186
- filesInvolved: [filePath],
187
- importance: assignImportance('file_change'),
188
- originalTokens: estimateTokens(result),
189
- ...extractLinkedReferences(result + filePath),
190
- },
191
- };
192
- }
193
-
194
- case 'Edit': {
195
- const filePath = tc.input.file_path as string ?? 'unknown';
196
- const title = `Edited: ${shortenPath(filePath)}`;
197
- return {
198
- type: 'file_change',
199
- title,
200
- detail: null,
201
- visibility: classifyVisibility(title, filePath),
202
- opts: {
203
- filesInvolved: [filePath],
204
- importance: assignImportance('file_change'),
205
- originalTokens: estimateTokens(result),
206
- ...extractLinkedReferences(result + filePath),
207
- },
208
- };
209
- }
210
-
211
- case 'Read': {
212
- const filePath = tc.input.file_path as string ?? 'unknown';
213
- // Only keep reads of interesting files (plan files, knowledge source files, etc.)
214
- const knowledgeSourceFiles = getConfig().conventions?.knowledgeSourceFiles ?? ['CLAUDE.md', 'MEMORY.md', 'corrections.md'];
215
- const plansDir = getResolvedPaths().plansDir;
216
- if (filePath.includes(plansDir) || knowledgeSourceFiles.some(f => filePath.includes(f))) {
217
- const title = `Read: ${shortenPath(filePath)}`;
218
- return {
219
- type: 'discovery',
220
- title,
221
- detail: null,
222
- visibility: classifyVisibility(title, filePath),
223
- opts: {
224
- filesInvolved: [filePath],
225
- importance: assignImportance('discovery'),
226
- originalTokens: estimateTokens(result),
227
- },
228
- };
229
- }
230
- return null;
231
- }
232
-
233
- case 'Bash': {
234
- const cmd = (tc.input.command as string ?? '').trim();
235
-
236
- // Git commit
237
- if (cmd.includes('git commit')) {
238
- const commitMsg = extractCommitMessage(cmd);
239
- const isfix = commitMsg.toLowerCase().includes('fix');
240
- const title = `Commit: ${commitMsg.slice(0, 150)}`;
241
- return {
242
- type: isfix ? 'bugfix' : 'feature',
243
- title,
244
- detail: cmd,
245
- visibility: classifyVisibility(title, cmd),
246
- opts: {
247
- importance: assignImportance(isfix ? 'bugfix' : 'feature'),
248
- originalTokens: estimateTokens(result),
249
- },
250
- };
251
- }
252
-
253
- // Pattern scanner
254
- if (cmd.includes('pattern-scanner')) {
255
- const passed = !result.includes('FAIL') && !result.includes('BLOCKED');
256
- const title = `Pattern Scanner: ${passed ? 'PASS' : 'FAIL'}`;
257
- const detail = result.slice(0, 500);
258
- return {
259
- type: 'pattern_compliance',
260
- title,
261
- detail,
262
- visibility: classifyVisibility(title, detail),
263
- opts: {
264
- evidence: result.slice(0, 500),
265
- importance: assignImportance('pattern_compliance', passed ? 'PASS' : 'FAIL'),
266
- originalTokens: estimateTokens(result),
267
- },
268
- };
269
- }
270
-
271
- // npm test / vitest
272
- if (cmd.includes('npm test') || cmd.includes('vitest')) {
273
- const passed = !tc.isError && !result.includes('FAIL');
274
- const title = `Tests: ${passed ? 'PASS' : 'FAIL'}`;
275
- return {
276
- type: 'vr_check',
277
- title,
278
- detail: cmd,
279
- visibility: classifyVisibility(title, cmd),
280
- opts: {
281
- vrType: 'VR-TEST',
282
- evidence: result.slice(0, 500),
283
- importance: assignImportance('vr_check', passed ? 'PASS' : 'FAIL'),
284
- originalTokens: estimateTokens(result),
285
- },
286
- };
287
- }
288
-
289
- // npm run build / tsc
290
- if (cmd.includes('npm run build') || cmd.includes('tsc --noEmit')) {
291
- const vrType = cmd.includes('tsc') ? 'VR-TYPE' : 'VR-BUILD';
292
- const passed = !tc.isError && !result.includes('error');
293
- const title = `${vrType}: ${passed ? 'PASS' : 'FAIL'}`;
294
- return {
295
- type: 'vr_check',
296
- title,
297
- detail: cmd,
298
- visibility: classifyVisibility(title, cmd),
299
- opts: {
300
- vrType,
301
- evidence: result.slice(0, 500),
302
- importance: assignImportance('vr_check', passed ? 'PASS' : 'FAIL'),
303
- originalTokens: estimateTokens(result),
304
- },
305
- };
306
- }
307
-
308
- return null;
309
- }
310
-
311
- default:
312
- return null;
313
- }
314
- }
315
-
316
- /**
317
- * Extract CR rule, VR type, and plan item references from text.
318
- */
319
- function extractLinkedReferences(text: string): Partial<AddObservationOpts> {
320
- const result: Partial<AddObservationOpts> = {};
321
-
322
- const crMatch = text.match(/CR-(\d+)/);
323
- if (crMatch) result.crRule = `CR-${crMatch[1]}`;
324
-
325
- const vrMatch = text.match(/VR-([A-Z_]+)/);
326
- if (vrMatch) result.vrType = `VR-${vrMatch[1]}`;
327
-
328
- const planMatch = text.match(/P(\d+)-(\d+)/);
329
- if (planMatch) result.planItem = `P${planMatch[1]}-${planMatch[2]}`;
330
-
331
- return result;
332
- }
333
-
334
- /**
335
- * Extract commit message from a git commit command.
336
- */
337
- function extractCommitMessage(cmd: string): string {
338
- // Match -m "message" or -m 'message'
339
- const match = cmd.match(/-m\s+["'](.+?)["']/);
340
- if (match) return match[1];
341
-
342
- // Match heredoc pattern
343
- const heredocMatch = cmd.match(/<<['"]?EOF['"]?\s*\n?([\s\S]*?)EOF/);
344
- if (heredocMatch) return heredocMatch[1].trim().split('\n')[0];
345
-
346
- return 'Unknown commit';
347
- }
348
-
349
- /**
350
- * Shorten a file path for display.
351
- */
352
- function shortenPath(filePath: string): string {
353
- // Remove project root prefix, then common home dir prefix
354
- const root = getProjectRoot();
355
- if (filePath.startsWith(root + '/')) {
356
- return filePath.slice(root.length + 1);
357
- }
358
- const home = homedir();
359
- if (filePath.startsWith(home + '/')) {
360
- return '~/' + filePath.slice(home.length + 1);
361
- }
362
- return filePath;
363
- }
364
-
365
- /**
366
- * Classify a single tool call for real-time capture (used by PostToolUse hook P3-002).
367
- * Applies noise filtering.
368
- */
369
- export function classifyRealTimeToolCall(
370
- toolName: string,
371
- toolInput: Record<string, unknown>,
372
- toolResponse: string,
373
- seenReads: Set<string>
374
- ): ExtractedObservation | null {
375
- const tc: ParsedToolCall = {
376
- toolName,
377
- toolUseId: '',
378
- input: toolInput,
379
- result: toolResponse,
380
- isError: false,
381
- };
382
-
383
- if (isNoisyToolCall(tc, seenReads)) return null;
384
-
385
- // P2-003: Detect architecture decision patterns in tool responses
386
- if (toolResponse && detectDecisionPatterns(toolResponse)) {
387
- const firstLine = toolResponse.split('\n')[0].slice(0, 200);
388
- const title = `Architecture decision: ${firstLine}`;
389
- const detail = toolResponse.slice(0, 1000);
390
- return {
391
- type: 'decision',
392
- title,
393
- detail,
394
- visibility: classifyVisibility(title, detail),
395
- opts: {
396
- importance: assignImportance('decision'),
397
- originalTokens: estimateTokens(toolResponse),
398
- ...extractLinkedReferences(toolResponse),
399
- },
400
- };
401
- }
402
-
403
- return classifyToolCall(tc);
404
- }
405
-
406
- /**
407
- * Detect plan progress references in tool responses.
408
- * Returns plan items that appear to be completed.
409
- */
410
- export function detectPlanProgress(toolResponse: string): Array<{ planItem: string; status: string }> {
411
- const results: Array<{ planItem: string; status: string }> = [];
412
- const progressPattern = /(P\d+-\d+)\s*[:\-]?\s*(COMPLETE|PASS|DONE|complete|pass|done)/g;
413
- let match;
414
- while ((match = progressPattern.exec(toolResponse)) !== null) {
415
- results.push({ planItem: match[1], status: 'complete' });
416
- }
417
- return results;
418
- }
package/src/page-deps.ts DELETED
@@ -1,285 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- import { readFileSync, existsSync } from 'fs';
5
- import { resolve } from 'path';
6
- import type Database from 'better-sqlite3';
7
- import { getConfig, getProjectRoot } from './config.ts';
8
- import { ensureWithinRoot } from './security-utils.ts';
9
- import { t } from './lib/sql-table-names.ts';
10
-
11
- export interface PageChain {
12
- page: string;
13
- route: string;
14
- portal: string;
15
- components: string[];
16
- hooks: string[];
17
- routers: string[];
18
- tables: string[];
19
- }
20
-
21
- /**
22
- * Derive the URL route from a Next.js page file path.
23
- * e.g., src/app/orders/page.tsx -> /orders
24
- * e.g., src/app/orders/[id]/page.tsx -> /orders/[id]
25
- */
26
- export function deriveRoute(pageFile: string): string {
27
- let route = pageFile
28
- .replace(/^src\/app/, '')
29
- .replace(/\/page\.tsx?$/, '')
30
- .replace(/\/page\.jsx?$/, '');
31
- return route || '/';
32
- }
33
-
34
- /**
35
- * Determine which portal/scope a page belongs to based on its route.
36
- * Uses accessScopes from config if available, otherwise infers from route prefix.
37
- */
38
- export function derivePortal(route: string): string {
39
- const scopes = getConfig().accessScopes;
40
- if (scopes && scopes.length > 0) {
41
- for (const scope of scopes) {
42
- if (route.startsWith('/' + scope)) return scope;
43
- }
44
- }
45
- // Fallback: use the first path segment as the scope
46
- const parts = route.split('/').filter(Boolean);
47
- return parts[0] ?? 'default';
48
- }
49
-
50
- /**
51
- * Recursively trace imports from a file, collecting components and hooks.
52
- */
53
- function traceImports(
54
- startFile: string,
55
- dataDb: Database.Database,
56
- visited: Set<string>,
57
- components: Set<string>,
58
- hooks: Set<string>,
59
- maxDepth: number = 5
60
- ): void {
61
- if (maxDepth <= 0 || visited.has(startFile)) return;
62
- visited.add(startFile);
63
-
64
- const imports = dataDb.prepare(
65
- `SELECT target_file, imported_names FROM ${t('imports')} WHERE source_file = ?`
66
- ).all(startFile) as { target_file: string; imported_names: string }[];
67
-
68
- for (const imp of imports) {
69
- const target = imp.target_file;
70
-
71
- // Classify the import
72
- if (target.includes('/components/')) {
73
- components.add(target);
74
- }
75
- if (target.includes('/hooks/') || target.match(/use[A-Z]/)) {
76
- hooks.add(target);
77
- }
78
-
79
- // Recurse into local imports (not node_modules)
80
- if (target.startsWith('src/')) {
81
- traceImports(target, dataDb, visited, components, hooks, maxDepth - 1);
82
- }
83
- }
84
- }
85
-
86
- /**
87
- * Find routers called by hooks/components via api.* patterns.
88
- */
89
- function findRouterCalls(files: string[]): string[] {
90
- const routers = new Set<string>();
91
- const projectRoot = getProjectRoot();
92
-
93
- for (const file of files) {
94
- const absPath = ensureWithinRoot(resolve(projectRoot, file), projectRoot);
95
- if (!existsSync(absPath)) continue;
96
-
97
- try {
98
- const source = readFileSync(absPath, 'utf-8');
99
- const apiCallRegex = /api\.(\w+)\.\w+/g;
100
- let match;
101
- while ((match = apiCallRegex.exec(source)) !== null) {
102
- routers.add(match[1]);
103
- }
104
- } catch {
105
- // Skip unreadable component files
106
- }
107
- }
108
-
109
- return [...routers];
110
- }
111
-
112
- /**
113
- * Find database tables touched by routers.
114
- */
115
- function findTablesFromRouters(routerNames: string[], dataDb: Database.Database): string[] {
116
- const tables = new Set<string>();
117
-
118
- // Look up router files from the tRPC index
119
- for (const routerName of routerNames) {
120
- const procs = dataDb.prepare(
121
- `SELECT DISTINCT router_file FROM ${t('trpc_procedures')} WHERE router_name = ?`
122
- ).all(routerName) as { router_file: string }[];
123
-
124
- for (const proc of procs) {
125
- const absPath = ensureWithinRoot(resolve(getProjectRoot(), proc.router_file), getProjectRoot());
126
- if (!existsSync(absPath)) continue;
127
-
128
- try {
129
- const source = readFileSync(absPath, 'utf-8');
130
- // Match database access patterns from config (default: ctx.db.{table})
131
- const dbPattern = getConfig().dbAccessPattern ?? 'ctx.db.{table}';
132
- const regexStr = dbPattern
133
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
134
- .replace('\\{table\\}', '(\\w+)');
135
- const tableRegex = new RegExp(regexStr + '\\.', 'g');
136
- let match;
137
- while ((match = tableRegex.exec(source)) !== null) {
138
- tables.add(match[1]);
139
- }
140
- } catch {
141
- // Skip unreadable source files
142
- }
143
- }
144
- }
145
-
146
- return [...tables];
147
- }
148
-
149
- /**
150
- * Build page dependency chains for all page.tsx files.
151
- */
152
- export function buildPageDeps(dataDb: Database.Database, codegraphDb: Database.Database): number {
153
- // Clear existing data
154
- dataDb.exec(`DELETE FROM ${t('page_deps')}`);
155
-
156
- // Find all page.tsx files from CodeGraph
157
- const pages = codegraphDb.prepare(
158
- "SELECT path FROM files WHERE path LIKE 'src/app/%/page.tsx' OR path = 'src/app/page.tsx'"
159
- ).all() as { path: string }[];
160
-
161
- const insertStmt = dataDb.prepare(
162
- `INSERT INTO ${t('page_deps')} (page_file, route, portal, components, hooks, routers, tables_touched) VALUES (?, ?, ?, ?, ?, ?, ?)`
163
- );
164
-
165
- let count = 0;
166
-
167
- const insertAll = dataDb.transaction(() => {
168
- for (const page of pages) {
169
- const route = deriveRoute(page.path);
170
- const portal = derivePortal(route);
171
-
172
- const visited = new Set<string>();
173
- const components = new Set<string>();
174
- const hooks = new Set<string>();
175
-
176
- traceImports(page.path, dataDb, visited, components, hooks);
177
-
178
- const allFiles = [...visited];
179
- const routers = findRouterCalls(allFiles);
180
- const tables = findTablesFromRouters(routers, dataDb);
181
-
182
- insertStmt.run(
183
- page.path,
184
- route,
185
- portal,
186
- JSON.stringify([...components]),
187
- JSON.stringify([...hooks]),
188
- JSON.stringify(routers),
189
- JSON.stringify(tables)
190
- );
191
-
192
- count++;
193
- }
194
- });
195
-
196
- insertAll();
197
- return count;
198
- }
199
-
200
- /**
201
- * Get the dependency chain for a specific page.
202
- */
203
- export function getPageChain(dataDb: Database.Database, pageFile: string): PageChain | null {
204
- const row = dataDb.prepare(`SELECT * FROM ${t('page_deps')} WHERE page_file = ?`).get(pageFile) as {
205
- page_file: string; route: string; portal: string;
206
- components: string; hooks: string; routers: string; tables_touched: string;
207
- } | undefined;
208
-
209
- if (!row) return null;
210
-
211
- return {
212
- page: row.page_file,
213
- route: row.route,
214
- portal: row.portal,
215
- components: JSON.parse(row.components),
216
- hooks: JSON.parse(row.hooks),
217
- routers: JSON.parse(row.routers),
218
- tables: JSON.parse(row.tables_touched),
219
- };
220
- }
221
-
222
- /**
223
- * Find all pages affected by a given file (reverse lookup).
224
- */
225
- export function findAffectedPages(dataDb: Database.Database, file: string): PageChain[] {
226
- // Check if this file is directly a page
227
- const directPage = getPageChain(dataDb, file);
228
- if (directPage) return [directPage];
229
-
230
- // Find all pages that import this file (directly or transitively)
231
- // First, find who imports this file
232
- const importers = dataDb.prepare(
233
- `SELECT source_file FROM ${t('imports')} WHERE target_file = ?`
234
- ).all(file) as { source_file: string }[];
235
-
236
- const affectedFiles = new Set<string>([file, ...importers.map(i => i.source_file)]);
237
-
238
- // Walk up the import tree to find pages
239
- let frontier = [...importers.map(i => i.source_file)];
240
- const visited = new Set(frontier);
241
- const maxDepth = 10;
242
- let depth = 0;
243
-
244
- while (frontier.length > 0 && depth < maxDepth) {
245
- const next: string[] = [];
246
- for (const f of frontier) {
247
- const upstreamImporters = dataDb.prepare(
248
- `SELECT source_file FROM ${t('imports')} WHERE target_file = ?`
249
- ).all(f) as { source_file: string }[];
250
-
251
- for (const imp of upstreamImporters) {
252
- if (!visited.has(imp.source_file)) {
253
- visited.add(imp.source_file);
254
- affectedFiles.add(imp.source_file);
255
- next.push(imp.source_file);
256
- }
257
- }
258
- }
259
- frontier = next;
260
- depth++;
261
- }
262
-
263
- // Now find which pages are in the affected set
264
- const allPages = dataDb.prepare(`SELECT * FROM ${t('page_deps')}`).all() as {
265
- page_file: string; route: string; portal: string;
266
- components: string; hooks: string; routers: string; tables_touched: string;
267
- }[];
268
-
269
- const results: PageChain[] = [];
270
- for (const row of allPages) {
271
- if (affectedFiles.has(row.page_file)) {
272
- results.push({
273
- page: row.page_file,
274
- route: row.route,
275
- portal: row.portal,
276
- components: JSON.parse(row.components),
277
- hooks: JSON.parse(row.hooks),
278
- routers: JSON.parse(row.routers),
279
- tables: JSON.parse(row.tables_touched),
280
- });
281
- }
282
- }
283
-
284
- return results;
285
- }