@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.
- package/CHANGELOG.md +1303 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +517 -397
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +66 -18
- package/dist/hooks/pre-compact.js +27 -2
- package/dist/hooks/pre-delete-check.js +48 -22
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +24 -0
- package/dist/hooks/session-start.js +8710 -8460
- package/dist/hooks/user-prompt.js +24 -0
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/framework-supports.ts +0 -85
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1206
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- package/src/watch/state.ts +0 -178
package/src/prompt-analyzer.ts
DELETED
|
@@ -1,332 +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 type { ToolDefinition, ToolResult } from './tools.ts';
|
|
6
|
-
import { createHash } from 'crypto';
|
|
7
|
-
import { getConfig } from './config.ts';
|
|
8
|
-
import { escapeRegex, redactSensitiveContent } from './security-utils.ts';
|
|
9
|
-
|
|
10
|
-
// ============================================================
|
|
11
|
-
// Prompt Effectiveness Analysis
|
|
12
|
-
// ============================================================
|
|
13
|
-
|
|
14
|
-
/** Prefix a base tool name with the configured tool prefix. */
|
|
15
|
-
function p(baseName: string): string {
|
|
16
|
-
return `${getConfig().toolPrefix}_${baseName}`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Default success/failure indicators. Can be overridden via config.analytics.prompts */
|
|
20
|
-
const DEFAULT_SUCCESS_INDICATORS = ['committed', 'approved', 'looks good', 'perfect', 'great', 'thanks'];
|
|
21
|
-
const DEFAULT_FAILURE_INDICATORS = ['revert', 'wrong', "that's not", 'undo', 'incorrect'];
|
|
22
|
-
const DEFAULT_ABANDON_PATTERNS = /\b(nevermind|forget it|skip|let's move on|different|instead)\b/i;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Categorize a prompt by its intent.
|
|
26
|
-
*/
|
|
27
|
-
export function categorizePrompt(promptText: string): string {
|
|
28
|
-
const lower = promptText.toLowerCase();
|
|
29
|
-
|
|
30
|
-
if (/\b(fix|bug|error|broken|issue|crash|fail)\b/.test(lower)) return 'bugfix';
|
|
31
|
-
if (/\b(refactor|rename|move|extract|cleanup|reorganize)\b/.test(lower)) return 'refactor';
|
|
32
|
-
if (/\b(what|how|why|where|when|explain|describe|tell me)\b/.test(lower)) return 'question';
|
|
33
|
-
if (/^\/\w+/.test(promptText.trim())) return 'command';
|
|
34
|
-
if (/\b(add|create|implement|build|new|feature)\b/.test(lower)) return 'feature';
|
|
35
|
-
|
|
36
|
-
return 'feature'; // Default to feature for implementation requests
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Hash a prompt for deduplication/comparison.
|
|
41
|
-
* Normalizes whitespace and lowercases before hashing.
|
|
42
|
-
*/
|
|
43
|
-
export function hashPrompt(promptText: string): string {
|
|
44
|
-
const normalized = promptText.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
45
|
-
return createHash('sha256').update(normalized).digest('hex').slice(0, 16);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Detect outcome from subsequent conversation context.
|
|
50
|
-
* Heuristic based on what follows a prompt.
|
|
51
|
-
*/
|
|
52
|
-
export function detectOutcome(
|
|
53
|
-
followUpPrompts: string[],
|
|
54
|
-
assistantResponses: string[]
|
|
55
|
-
): { outcome: string; correctionsNeeded: number; followUpCount: number } {
|
|
56
|
-
let correctionsNeeded = 0;
|
|
57
|
-
let outcome = 'success';
|
|
58
|
-
|
|
59
|
-
const correctionPatterns = /\b(no|wrong|that's not|fix this|try again|revert|undo|incorrect|not what)\b/i;
|
|
60
|
-
|
|
61
|
-
const config = getConfig();
|
|
62
|
-
const successIndicators = config.analytics?.prompts?.success_indicators ?? DEFAULT_SUCCESS_INDICATORS;
|
|
63
|
-
// Escape regex special chars from config-provided indicators to prevent ReDoS
|
|
64
|
-
const escapedIndicators = successIndicators.map(escapeRegex);
|
|
65
|
-
const successRegex = new RegExp(`\\b(${escapedIndicators.join('|')})\\b`, 'i');
|
|
66
|
-
|
|
67
|
-
for (const prompt of followUpPrompts) {
|
|
68
|
-
if (correctionPatterns.test(prompt)) {
|
|
69
|
-
correctionsNeeded++;
|
|
70
|
-
}
|
|
71
|
-
if (DEFAULT_ABANDON_PATTERNS.test(prompt)) {
|
|
72
|
-
outcome = 'abandoned';
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Check assistant responses for failure signals
|
|
78
|
-
for (const response of assistantResponses) {
|
|
79
|
-
if (/\b(error|failed|cannot|unable to)\b/i.test(response) && response.length < 200) {
|
|
80
|
-
outcome = 'failure';
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Determine final outcome
|
|
85
|
-
if (outcome === 'abandoned') {
|
|
86
|
-
// Keep abandoned
|
|
87
|
-
} else if (correctionsNeeded >= 3) {
|
|
88
|
-
outcome = 'partial';
|
|
89
|
-
} else if (correctionsNeeded > 0) {
|
|
90
|
-
outcome = 'partial';
|
|
91
|
-
} else {
|
|
92
|
-
// Check for success signals in follow-ups
|
|
93
|
-
for (const prompt of followUpPrompts) {
|
|
94
|
-
if (successRegex.test(prompt)) {
|
|
95
|
-
outcome = 'success';
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
outcome,
|
|
103
|
-
correctionsNeeded,
|
|
104
|
-
followUpCount: followUpPrompts.length,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Analyze prompts from a session and store outcomes.
|
|
110
|
-
*/
|
|
111
|
-
export function analyzeSessionPrompts(db: Database.Database, sessionId: string): number {
|
|
112
|
-
const prompts = db.prepare(
|
|
113
|
-
'SELECT prompt_text, prompt_number FROM user_prompts WHERE session_id = ? ORDER BY prompt_number ASC'
|
|
114
|
-
).all(sessionId) as Array<{ prompt_text: string; prompt_number: number }>;
|
|
115
|
-
|
|
116
|
-
if (prompts.length === 0) return 0;
|
|
117
|
-
|
|
118
|
-
let stored = 0;
|
|
119
|
-
for (let i = 0; i < prompts.length; i++) {
|
|
120
|
-
const prompt = prompts[i];
|
|
121
|
-
const followUps = prompts.slice(i + 1, i + 4).map(p => p.prompt_text);
|
|
122
|
-
|
|
123
|
-
const category = categorizePrompt(prompt.prompt_text);
|
|
124
|
-
const hash = hashPrompt(prompt.prompt_text);
|
|
125
|
-
const { outcome, correctionsNeeded, followUpCount } = detectOutcome(followUps, []);
|
|
126
|
-
|
|
127
|
-
// Check if already analyzed
|
|
128
|
-
const existing = db.prepare(
|
|
129
|
-
'SELECT id FROM prompt_outcomes WHERE session_id = ? AND prompt_hash = ?'
|
|
130
|
-
).get(sessionId, hash);
|
|
131
|
-
if (existing) continue;
|
|
132
|
-
|
|
133
|
-
// Redact sensitive content (API keys, emails, tokens, paths) before storage
|
|
134
|
-
const redactedText = redactSensitiveContent(prompt.prompt_text.slice(0, 2000));
|
|
135
|
-
|
|
136
|
-
db.prepare(`
|
|
137
|
-
INSERT INTO prompt_outcomes
|
|
138
|
-
(session_id, prompt_hash, prompt_text, prompt_category, word_count, outcome,
|
|
139
|
-
corrections_needed, follow_up_prompts)
|
|
140
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
141
|
-
`).run(
|
|
142
|
-
sessionId, hash, redactedText, category,
|
|
143
|
-
prompt.prompt_text.split(/\s+/).length, outcome,
|
|
144
|
-
correctionsNeeded, followUpCount
|
|
145
|
-
);
|
|
146
|
-
stored++;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return stored;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ============================================================
|
|
153
|
-
// MCP Tool Definitions & Handlers
|
|
154
|
-
// ============================================================
|
|
155
|
-
|
|
156
|
-
export function getPromptToolDefinitions(): ToolDefinition[] {
|
|
157
|
-
return [
|
|
158
|
-
{
|
|
159
|
-
name: p('prompt_effectiveness'),
|
|
160
|
-
description: 'Prompt effectiveness statistics by category. Shows success rates, average corrections needed, and best-performing prompt patterns.',
|
|
161
|
-
inputSchema: {
|
|
162
|
-
type: 'object',
|
|
163
|
-
properties: {
|
|
164
|
-
category: {
|
|
165
|
-
type: 'string',
|
|
166
|
-
description: 'Filter by category: feature, bugfix, refactor, question, command',
|
|
167
|
-
},
|
|
168
|
-
days: { type: 'number', description: 'Days to look back (default: 30)' },
|
|
169
|
-
},
|
|
170
|
-
required: [],
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
name: p('prompt_suggestions'),
|
|
175
|
-
description: 'Suggest improvements for a prompt based on past outcomes. Finds similar prompts ranked by success rate.',
|
|
176
|
-
inputSchema: {
|
|
177
|
-
type: 'object',
|
|
178
|
-
properties: {
|
|
179
|
-
prompt: { type: 'string', description: 'The prompt text to analyze' },
|
|
180
|
-
},
|
|
181
|
-
required: ['prompt'],
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
];
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const PROMPT_BASE_NAMES = new Set(['prompt_effectiveness', 'prompt_suggestions']);
|
|
188
|
-
|
|
189
|
-
export function isPromptTool(name: string): boolean {
|
|
190
|
-
const pfx = getConfig().toolPrefix + '_';
|
|
191
|
-
const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
|
|
192
|
-
return PROMPT_BASE_NAMES.has(baseName);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export function handlePromptToolCall(
|
|
196
|
-
name: string,
|
|
197
|
-
args: Record<string, unknown>,
|
|
198
|
-
memoryDb: Database.Database
|
|
199
|
-
): ToolResult {
|
|
200
|
-
try {
|
|
201
|
-
const pfx = getConfig().toolPrefix + '_';
|
|
202
|
-
const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
|
|
203
|
-
|
|
204
|
-
switch (baseName) {
|
|
205
|
-
case 'prompt_effectiveness':
|
|
206
|
-
return handleEffectiveness(args, memoryDb);
|
|
207
|
-
case 'prompt_suggestions':
|
|
208
|
-
return handleSuggestions(args, memoryDb);
|
|
209
|
-
default:
|
|
210
|
-
return text(`Unknown prompt tool: ${name}`);
|
|
211
|
-
}
|
|
212
|
-
} catch (error) {
|
|
213
|
-
return text(`Error in ${name}: ${error instanceof Error ? error.message : String(error)}\n\nUsage: ${p('prompt_effectiveness')} { days: 30 }, ${p('prompt_suggestions')} { prompt: "..." }`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function handleEffectiveness(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
218
|
-
const category = args.category as string | undefined;
|
|
219
|
-
const days = (args.days as number) ?? 30;
|
|
220
|
-
|
|
221
|
-
let sql = `
|
|
222
|
-
SELECT prompt_category,
|
|
223
|
-
COUNT(*) as total,
|
|
224
|
-
SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) as successes,
|
|
225
|
-
SUM(CASE WHEN outcome = 'partial' THEN 1 ELSE 0 END) as partials,
|
|
226
|
-
SUM(CASE WHEN outcome = 'failure' THEN 1 ELSE 0 END) as failures,
|
|
227
|
-
SUM(CASE WHEN outcome = 'abandoned' THEN 1 ELSE 0 END) as abandoned,
|
|
228
|
-
AVG(corrections_needed) as avg_corrections,
|
|
229
|
-
AVG(word_count) as avg_word_count
|
|
230
|
-
FROM prompt_outcomes
|
|
231
|
-
WHERE created_at >= datetime('now', ?)
|
|
232
|
-
`;
|
|
233
|
-
const params: (string | number)[] = [`-${days} days`];
|
|
234
|
-
|
|
235
|
-
if (category) {
|
|
236
|
-
sql += ' AND prompt_category = ?';
|
|
237
|
-
params.push(category);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
sql += ' GROUP BY prompt_category ORDER BY total DESC';
|
|
241
|
-
|
|
242
|
-
const rows = db.prepare(sql).all(...params) as Array<Record<string, unknown>>;
|
|
243
|
-
|
|
244
|
-
if (rows.length === 0) {
|
|
245
|
-
return text(`No prompt outcomes found in the last ${days} days. Prompt analysis runs automatically at session end. Try a longer time range: ${p('prompt_effectiveness')} { days: 90 }, or use ${p('prompt_suggestions')} { prompt: "your text" } to analyze a prompt directly.`);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const lines = [
|
|
249
|
-
`## Prompt Effectiveness (${days} days)`,
|
|
250
|
-
'',
|
|
251
|
-
'| Category | Total | Success % | Partial | Failed | Abandoned | Avg Corrections | Avg Words |',
|
|
252
|
-
'|----------|-------|-----------|---------|--------|-----------|-----------------|-----------|',
|
|
253
|
-
];
|
|
254
|
-
|
|
255
|
-
for (const row of rows) {
|
|
256
|
-
const total = row.total as number;
|
|
257
|
-
const successRate = total > 0 ? Math.round(((row.successes as number) / total) * 100) : 0;
|
|
258
|
-
lines.push(
|
|
259
|
-
`| ${row.prompt_category} | ${total} | ${successRate}% | ${row.partials} | ${row.failures} | ${row.abandoned} | ${(row.avg_corrections as number).toFixed(1)} | ${Math.round(row.avg_word_count as number)} |`
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return text(lines.join('\n'));
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function handleSuggestions(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
267
|
-
const prompt = args.prompt as string;
|
|
268
|
-
if (!prompt) return text(`Usage: ${p('prompt_suggestions')} { prompt: "your prompt text here" } - Analyzes a prompt and suggests improvements based on past outcomes.`);
|
|
269
|
-
|
|
270
|
-
const category = categorizePrompt(prompt);
|
|
271
|
-
const wordCount = prompt.split(/\s+/).length;
|
|
272
|
-
|
|
273
|
-
// Find successful prompts in the same category with similar length
|
|
274
|
-
const similar = db.prepare(`
|
|
275
|
-
SELECT prompt_text, outcome, corrections_needed, word_count
|
|
276
|
-
FROM prompt_outcomes
|
|
277
|
-
WHERE prompt_category = ? AND outcome = 'success'
|
|
278
|
-
ORDER BY ABS(word_count - ?) ASC
|
|
279
|
-
LIMIT 5
|
|
280
|
-
`).all(category, wordCount) as Array<{
|
|
281
|
-
prompt_text: string;
|
|
282
|
-
outcome: string;
|
|
283
|
-
corrections_needed: number;
|
|
284
|
-
word_count: number;
|
|
285
|
-
}>;
|
|
286
|
-
|
|
287
|
-
const lines = [
|
|
288
|
-
`## Prompt Analysis`,
|
|
289
|
-
`Category: ${category}`,
|
|
290
|
-
`Word count: ${wordCount}`,
|
|
291
|
-
'',
|
|
292
|
-
];
|
|
293
|
-
|
|
294
|
-
// Suggestions based on patterns
|
|
295
|
-
if (wordCount < 10) {
|
|
296
|
-
lines.push('**Suggestion**: Short prompts often need follow-up corrections. Consider adding more context about:');
|
|
297
|
-
lines.push('- Expected behavior or output');
|
|
298
|
-
lines.push('- Specific files or components to modify');
|
|
299
|
-
lines.push('- Constraints or patterns to follow');
|
|
300
|
-
lines.push('');
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (similar.length > 0) {
|
|
304
|
-
lines.push('### Successful Similar Prompts');
|
|
305
|
-
for (const s of similar) {
|
|
306
|
-
lines.push(`- [${s.word_count} words] ${s.prompt_text.slice(0, 150)}...`);
|
|
307
|
-
}
|
|
308
|
-
} else {
|
|
309
|
-
lines.push('No similar successful prompts found in this category.');
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Category-specific stats
|
|
313
|
-
const stats = db.prepare(`
|
|
314
|
-
SELECT COUNT(*) as total,
|
|
315
|
-
SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) as successes,
|
|
316
|
-
AVG(corrections_needed) as avg_corrections
|
|
317
|
-
FROM prompt_outcomes WHERE prompt_category = ?
|
|
318
|
-
`).get(category) as { total: number; successes: number; avg_corrections: number };
|
|
319
|
-
|
|
320
|
-
if (stats.total > 0) {
|
|
321
|
-
lines.push('');
|
|
322
|
-
lines.push(`### Category Stats: ${category}`);
|
|
323
|
-
lines.push(`- Success rate: ${Math.round((stats.successes / stats.total) * 100)}%`);
|
|
324
|
-
lines.push(`- Avg corrections needed: ${stats.avg_corrections.toFixed(1)}`);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return text(lines.join('\n'));
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function text(content: string): ToolResult {
|
|
331
|
-
return { content: [{ type: 'text', text: content }] };
|
|
332
|
-
}
|
|
@@ -1,125 +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, readdirSync } from 'fs';
|
|
5
|
-
import { join, relative } from 'path';
|
|
6
|
-
import type Database from 'better-sqlite3';
|
|
7
|
-
import { getProjectRoot, getConfig } from '../config.ts';
|
|
8
|
-
import { t } from '../lib/sql-table-names.ts';
|
|
9
|
-
|
|
10
|
-
interface CouplingMatch {
|
|
11
|
-
frontendFile: string;
|
|
12
|
-
line: number;
|
|
13
|
-
callPattern: string;
|
|
14
|
-
routeId: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Scan frontend files for API calls matching Python routes.
|
|
19
|
-
* Stores matches in massu_py_route_callers.
|
|
20
|
-
*/
|
|
21
|
-
export function buildPythonCouplingIndex(dataDb: Database.Database): number {
|
|
22
|
-
const projectRoot = getProjectRoot();
|
|
23
|
-
const config = getConfig();
|
|
24
|
-
const srcDir = join(projectRoot, config.paths.source);
|
|
25
|
-
|
|
26
|
-
// Get all routes from DB
|
|
27
|
-
const routes = dataDb.prepare(`SELECT id, method, path FROM ${t('py_routes')}`).all() as {
|
|
28
|
-
id: number; method: string; path: string;
|
|
29
|
-
}[];
|
|
30
|
-
|
|
31
|
-
if (routes.length === 0) return 0;
|
|
32
|
-
|
|
33
|
-
// Clear existing callers
|
|
34
|
-
dataDb.exec(`DELETE FROM ${t('py_route_callers')}`);
|
|
35
|
-
|
|
36
|
-
// Walk frontend files (TS/TSX/JS/JSX)
|
|
37
|
-
const frontendFiles = walkFrontendFiles(srcDir);
|
|
38
|
-
|
|
39
|
-
const insertStmt = dataDb.prepare(
|
|
40
|
-
`INSERT INTO ${t('py_route_callers')} (route_id, frontend_file, line, call_pattern) VALUES (?, ?, ?, ?)`
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
let count = 0;
|
|
44
|
-
|
|
45
|
-
// API call patterns to detect
|
|
46
|
-
const apiPatterns = [
|
|
47
|
-
/fetch\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // fetch('/api/...')
|
|
48
|
-
/fetch\s*\(\s*[`'"]([^`'"]*\/api\/[^`'"]*)[`'"]/g, // fetch('http.../api/...')
|
|
49
|
-
/axios\.\w+\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // axios.get('/api/...')
|
|
50
|
-
/\.get\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // client.get('/api/...')
|
|
51
|
-
/\.post\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // client.post('/api/...')
|
|
52
|
-
/\.put\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // client.put('/api/...')
|
|
53
|
-
/\.delete\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // client.delete('/api/...')
|
|
54
|
-
/\.patch\s*\(\s*[`'"](\/api\/[^`'"]*)[`'"]/g, // client.patch('/api/...')
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
dataDb.transaction(() => {
|
|
58
|
-
for (const absFile of frontendFiles) {
|
|
59
|
-
const relFile = relative(projectRoot, absFile);
|
|
60
|
-
let source: string;
|
|
61
|
-
try { source = readFileSync(absFile, 'utf-8'); } catch { continue; }
|
|
62
|
-
|
|
63
|
-
const lines = source.split('\n');
|
|
64
|
-
for (let i = 0; i < lines.length; i++) {
|
|
65
|
-
const line = lines[i];
|
|
66
|
-
for (const pattern of apiPatterns) {
|
|
67
|
-
pattern.lastIndex = 0;
|
|
68
|
-
let match;
|
|
69
|
-
while ((match = pattern.exec(line)) !== null) {
|
|
70
|
-
const urlPath = match[1];
|
|
71
|
-
// Try to match against routes
|
|
72
|
-
const matchedRoute = findMatchingRoute(urlPath, routes);
|
|
73
|
-
if (matchedRoute) {
|
|
74
|
-
insertStmt.run(matchedRoute.id, relFile, i + 1, match[0].slice(0, 200));
|
|
75
|
-
count++;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
})();
|
|
82
|
-
|
|
83
|
-
return count;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function walkFrontendFiles(dir: string): string[] {
|
|
87
|
-
const files: string[] = [];
|
|
88
|
-
const exclude = ['node_modules', '.next', 'dist', '.git', '__pycache__', '.venv', 'venv'];
|
|
89
|
-
try {
|
|
90
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
91
|
-
for (const entry of entries) {
|
|
92
|
-
if (entry.isDirectory()) {
|
|
93
|
-
if (exclude.includes(entry.name)) continue;
|
|
94
|
-
files.push(...walkFrontendFiles(join(dir, entry.name)));
|
|
95
|
-
} else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
96
|
-
files.push(join(dir, entry.name));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
} catch { /* dir not readable, skip */ }
|
|
100
|
-
return files;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Match a URL path against route definitions.
|
|
105
|
-
* Handles path parameters: /api/users/{id} matches /api/users/123
|
|
106
|
-
*/
|
|
107
|
-
/**
|
|
108
|
-
* Escape special regex characters in a string.
|
|
109
|
-
*/
|
|
110
|
-
function escapeRegex(s: string): string {
|
|
111
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function findMatchingRoute(urlPath: string, routes: { id: number; method: string; path: string }[]): { id: number } | null {
|
|
115
|
-
for (const route of routes) {
|
|
116
|
-
// Escape regex-special chars in route path, then replace param placeholders
|
|
117
|
-
const escaped = escapeRegex(route.path);
|
|
118
|
-
const pattern = escaped.replace(/\\\{[^}]+\\\}/g, '[^/]+');
|
|
119
|
-
const routeRegex = new RegExp('^' + pattern + '$');
|
|
120
|
-
if (routeRegex.test(urlPath)) {
|
|
121
|
-
return { id: route.id };
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
@@ -1,84 +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 { getConfig } from '../config.ts';
|
|
6
|
-
import { t } from '../lib/sql-table-names.ts';
|
|
7
|
-
|
|
8
|
-
export interface DomainViolation {
|
|
9
|
-
sourceFile: string;
|
|
10
|
-
sourceDomain: string;
|
|
11
|
-
targetFile: string;
|
|
12
|
-
targetDomain: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Classify a Python file into its domain based on python.domains config.
|
|
17
|
-
*/
|
|
18
|
-
export function classifyPythonFileDomain(file: string): string {
|
|
19
|
-
const config = getConfig();
|
|
20
|
-
const domains = config.python?.domains || [];
|
|
21
|
-
const pythonRoot = config.python?.root || '';
|
|
22
|
-
|
|
23
|
-
let modulePath = file;
|
|
24
|
-
if (pythonRoot && modulePath.startsWith(pythonRoot + '/')) {
|
|
25
|
-
modulePath = modulePath.slice(pythonRoot.length + 1);
|
|
26
|
-
}
|
|
27
|
-
modulePath = modulePath.replace(/\/__init__\.py$/, '').replace(/\.py$/, '').replace(/\//g, '.');
|
|
28
|
-
|
|
29
|
-
for (const domain of domains) {
|
|
30
|
-
for (const pkg of domain.packages) {
|
|
31
|
-
if (modulePath.startsWith(pkg) || modulePath === pkg) {
|
|
32
|
-
return domain.name;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return 'Unknown';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Find all cross-domain import violations in the Python import graph.
|
|
41
|
-
*/
|
|
42
|
-
export function findPythonDomainViolations(dataDb: Database.Database): DomainViolation[] {
|
|
43
|
-
const config = getConfig();
|
|
44
|
-
const domains = config.python?.domains || [];
|
|
45
|
-
if (domains.length === 0) return [];
|
|
46
|
-
|
|
47
|
-
const imports = dataDb.prepare(
|
|
48
|
-
`SELECT source_file, target_file FROM ${t('py_imports')}`
|
|
49
|
-
).all() as { source_file: string; target_file: string }[];
|
|
50
|
-
|
|
51
|
-
const violations: DomainViolation[] = [];
|
|
52
|
-
|
|
53
|
-
for (const imp of imports) {
|
|
54
|
-
const srcDomain = classifyPythonFileDomain(imp.source_file);
|
|
55
|
-
const tgtDomain = classifyPythonFileDomain(imp.target_file);
|
|
56
|
-
|
|
57
|
-
if (srcDomain === tgtDomain || srcDomain === 'Unknown' || tgtDomain === 'Unknown') continue;
|
|
58
|
-
|
|
59
|
-
const srcConfig = domains.find(d => d.name === srcDomain);
|
|
60
|
-
if (srcConfig && !srcConfig.allowed_imports_from.includes(tgtDomain)) {
|
|
61
|
-
violations.push({
|
|
62
|
-
sourceFile: imp.source_file,
|
|
63
|
-
sourceDomain: srcDomain,
|
|
64
|
-
targetFile: imp.target_file,
|
|
65
|
-
targetDomain: tgtDomain,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return violations;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Get all Python files in a specific domain.
|
|
75
|
-
*/
|
|
76
|
-
export function getPythonFilesInDomain(dataDb: Database.Database, domainName: string): string[] {
|
|
77
|
-
const allFiles = dataDb.prepare(
|
|
78
|
-
`SELECT DISTINCT source_file as f FROM ${t('py_imports')} UNION SELECT DISTINCT target_file as f FROM ${t('py_imports')}`
|
|
79
|
-
).all() as { f: string }[];
|
|
80
|
-
|
|
81
|
-
return allFiles
|
|
82
|
-
.map(row => row.f)
|
|
83
|
-
.filter(f => classifyPythonFileDomain(f) === domainName);
|
|
84
|
-
}
|
|
@@ -1,96 +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 { classifyPythonFileDomain } from './domain-enforcer.ts';
|
|
6
|
-
import { t } from '../lib/sql-table-names.ts';
|
|
7
|
-
|
|
8
|
-
export interface PythonImpactReport {
|
|
9
|
-
file: string;
|
|
10
|
-
domain: string;
|
|
11
|
-
importedBy: string[];
|
|
12
|
-
routes: { method: string; path: string; functionName: string }[];
|
|
13
|
-
models: { className: string; tableName: string | null }[];
|
|
14
|
-
frontendCallers: string[];
|
|
15
|
-
domainCrossings: { file: string; domain: string }[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Full impact analysis for a Python file.
|
|
20
|
-
* Cross-references import graph, routes, models, and frontend coupling.
|
|
21
|
-
*/
|
|
22
|
-
export function analyzePythonImpact(dataDb: Database.Database, file: string): PythonImpactReport {
|
|
23
|
-
const domain = classifyPythonFileDomain(file);
|
|
24
|
-
|
|
25
|
-
// 1. Who imports this file (direct + transitive)
|
|
26
|
-
const importedBy = collectTransitiveImporters(dataDb, file, 5);
|
|
27
|
-
|
|
28
|
-
// 2. Routes defined in this file
|
|
29
|
-
const routes = dataDb.prepare(
|
|
30
|
-
`SELECT method, path, function_name FROM ${t('py_routes')} WHERE file = ?`
|
|
31
|
-
).all(file) as { method: string; path: string; function_name: string }[];
|
|
32
|
-
|
|
33
|
-
// 3. Models defined in this file
|
|
34
|
-
const models = dataDb.prepare(
|
|
35
|
-
`SELECT class_name, table_name FROM ${t('py_models')} WHERE file = ?`
|
|
36
|
-
).all(file) as { class_name: string; table_name: string | null }[];
|
|
37
|
-
|
|
38
|
-
// 4. Frontend callers (via routes in this file)
|
|
39
|
-
const routeIds = dataDb.prepare(`SELECT id FROM ${t('py_routes')} WHERE file = ?`).all(file) as { id: number }[];
|
|
40
|
-
const frontendCallers: string[] = [];
|
|
41
|
-
if (routeIds.length > 0) {
|
|
42
|
-
const placeholders = routeIds.map(() => '?').join(',');
|
|
43
|
-
const callers = dataDb.prepare(
|
|
44
|
-
`SELECT DISTINCT frontend_file FROM ${t('py_route_callers')} WHERE route_id IN (${placeholders})`
|
|
45
|
-
).all(...routeIds.map(r => r.id)) as { frontend_file: string }[];
|
|
46
|
-
frontendCallers.push(...callers.map(c => c.frontend_file));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// 5. Domain crossings
|
|
50
|
-
const imports = dataDb.prepare(
|
|
51
|
-
`SELECT target_file FROM ${t('py_imports')} WHERE source_file = ?`
|
|
52
|
-
).all(file) as { target_file: string }[];
|
|
53
|
-
|
|
54
|
-
const domainCrossings = imports
|
|
55
|
-
.map(imp => ({ file: imp.target_file, domain: classifyPythonFileDomain(imp.target_file) }))
|
|
56
|
-
.filter(imp => imp.domain !== domain && imp.domain !== 'Unknown');
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
file,
|
|
60
|
-
domain,
|
|
61
|
-
importedBy,
|
|
62
|
-
routes: routes.map(r => ({ method: r.method, path: r.path, functionName: r.function_name })),
|
|
63
|
-
models: models.map(m => ({ className: m.class_name, tableName: m.table_name })),
|
|
64
|
-
frontendCallers,
|
|
65
|
-
domainCrossings,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function collectTransitiveImporters(dataDb: Database.Database, file: string, maxDepth: number): string[] {
|
|
70
|
-
const visited = new Set<string>();
|
|
71
|
-
const queue = [file];
|
|
72
|
-
let depth = 0;
|
|
73
|
-
|
|
74
|
-
const importerStmt = dataDb.prepare(
|
|
75
|
-
`SELECT source_file FROM ${t('py_imports')} WHERE target_file = ?`
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
while (queue.length > 0 && depth < maxDepth) {
|
|
79
|
-
const batch = [...queue];
|
|
80
|
-
queue.length = 0;
|
|
81
|
-
for (const f of batch) {
|
|
82
|
-
if (visited.has(f)) continue;
|
|
83
|
-
visited.add(f);
|
|
84
|
-
const importers = importerStmt.all(f) as { source_file: string }[];
|
|
85
|
-
for (const imp of importers) {
|
|
86
|
-
if (!visited.has(imp.source_file)) {
|
|
87
|
-
queue.push(imp.source_file);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
depth++;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
visited.delete(file); // Don't include the file itself
|
|
95
|
-
return [...visited];
|
|
96
|
-
}
|