@massu/core 0.1.1 → 0.4.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/commands/_shared-preamble.md +76 -0
- package/commands/massu-audit-deps.md +211 -0
- package/commands/massu-changelog.md +174 -0
- package/commands/massu-cleanup.md +315 -0
- package/commands/massu-commit.md +481 -0
- package/commands/massu-create-plan.md +752 -0
- package/commands/massu-dead-code.md +131 -0
- package/commands/massu-debug.md +484 -0
- package/commands/massu-deploy.md +91 -0
- package/commands/massu-deps.md +374 -0
- package/commands/massu-doc-gen.md +279 -0
- package/commands/massu-docs.md +364 -0
- package/commands/massu-estimate.md +313 -0
- package/commands/massu-golden-path.md +973 -0
- package/commands/massu-guide.md +167 -0
- package/commands/massu-hotfix.md +480 -0
- package/commands/massu-loop-playwright.md +837 -0
- package/commands/massu-loop.md +775 -0
- package/commands/massu-new-feature.md +511 -0
- package/commands/massu-parity.md +214 -0
- package/commands/massu-plan.md +456 -0
- package/commands/massu-push-light.md +207 -0
- package/commands/massu-push.md +434 -0
- package/commands/massu-refactor.md +410 -0
- package/commands/massu-release.md +363 -0
- package/commands/massu-review.md +238 -0
- package/commands/massu-simplify.md +281 -0
- package/commands/massu-status.md +278 -0
- package/commands/massu-tdd.md +201 -0
- package/commands/massu-test.md +516 -0
- package/commands/massu-verify-playwright.md +281 -0
- package/commands/massu-verify.md +667 -0
- package/dist/cli.js +7772 -3140
- package/dist/hooks/cost-tracker.js +103 -40
- package/dist/hooks/post-edit-context.js +74 -8
- package/dist/hooks/post-tool-use.js +268 -106
- package/dist/hooks/pre-compact.js +167 -43
- package/dist/hooks/pre-delete-check.js +159 -42
- package/dist/hooks/quality-event.js +103 -40
- package/dist/hooks/security-gate.js +29 -0
- package/dist/hooks/session-end.js +143 -84
- package/dist/hooks/session-start.js +186 -49
- package/dist/hooks/user-prompt.js +189 -43
- package/package.json +10 -15
- package/src/adr-generator.ts +9 -2
- package/src/analytics.ts +9 -3
- package/src/audit-trail.ts +10 -3
- package/src/backfill-sessions.ts +5 -4
- package/src/cli.ts +6 -0
- package/src/cloud-sync.ts +14 -18
- package/src/commands/doctor.ts +193 -6
- package/src/commands/init.ts +230 -5
- package/src/commands/install-commands.ts +137 -0
- package/src/config.ts +68 -2
- package/src/cost-tracker.ts +11 -6
- package/src/db.ts +115 -2
- package/src/dependency-scorer.ts +9 -2
- package/src/docs-tools.ts +21 -16
- package/src/hooks/post-edit-context.ts +4 -4
- package/src/hooks/post-tool-use.ts +130 -0
- package/src/hooks/pre-compact.ts +23 -1
- package/src/hooks/pre-delete-check.ts +92 -4
- package/src/hooks/security-gate.ts +32 -0
- package/src/hooks/session-end.ts +3 -3
- package/src/hooks/session-start.ts +99 -6
- package/src/hooks/user-prompt.ts +46 -1
- package/src/import-resolver.ts +2 -1
- package/src/knowledge-db.ts +169 -0
- package/src/knowledge-indexer.ts +704 -0
- package/src/knowledge-tools.ts +1413 -0
- package/src/license.ts +482 -0
- package/src/memory-db.ts +1364 -23
- package/src/memory-tools.ts +14 -15
- package/src/observability-tools.ts +13 -2
- package/src/observation-extractor.ts +11 -4
- package/src/page-deps.ts +3 -2
- package/src/prompt-analyzer.ts +9 -2
- package/src/python/coupling-detector.ts +124 -0
- package/src/python/domain-enforcer.ts +83 -0
- package/src/python/impact-analyzer.ts +95 -0
- package/src/python/import-parser.ts +244 -0
- package/src/python/import-resolver.ts +135 -0
- package/src/python/migration-indexer.ts +115 -0
- package/src/python/migration-parser.ts +332 -0
- package/src/python/model-indexer.ts +70 -0
- package/src/python/model-parser.ts +279 -0
- package/src/python/route-indexer.ts +58 -0
- package/src/python/route-parser.ts +317 -0
- package/src/python-tools.ts +629 -0
- package/src/regression-detector.ts +9 -3
- package/src/security-scorer.ts +9 -2
- package/src/sentinel-db.ts +45 -89
- package/src/sentinel-tools.ts +8 -11
- package/src/server.ts +29 -7
- package/src/session-archiver.ts +4 -5
- package/src/team-knowledge.ts +9 -2
- package/src/tools.ts +1032 -44
- package/src/validate-features-runner.ts +0 -1
- package/src/validation-engine.ts +9 -2
- package/README.md +0 -40
- package/dist/server.js +0 -7008
- package/src/__tests__/adr-generator.test.ts +0 -260
- package/src/__tests__/analytics.test.ts +0 -282
- package/src/__tests__/audit-trail.test.ts +0 -382
- package/src/__tests__/backfill-sessions.test.ts +0 -690
- package/src/__tests__/cli.test.ts +0 -290
- package/src/__tests__/cloud-sync.test.ts +0 -261
- package/src/__tests__/config-sections.test.ts +0 -359
- package/src/__tests__/config.test.ts +0 -732
- package/src/__tests__/cost-tracker.test.ts +0 -348
- package/src/__tests__/db.test.ts +0 -177
- package/src/__tests__/dependency-scorer.test.ts +0 -325
- package/src/__tests__/docs-integration.test.ts +0 -178
- package/src/__tests__/docs-tools.test.ts +0 -199
- package/src/__tests__/domains.test.ts +0 -236
- package/src/__tests__/hooks.test.ts +0 -221
- package/src/__tests__/import-resolver.test.ts +0 -95
- package/src/__tests__/integration/path-traversal.test.ts +0 -134
- package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
- package/src/__tests__/integration/tool-registration.test.ts +0 -146
- package/src/__tests__/memory-db.test.ts +0 -404
- package/src/__tests__/memory-enhancements.test.ts +0 -316
- package/src/__tests__/memory-tools.test.ts +0 -199
- package/src/__tests__/middleware-tree.test.ts +0 -177
- package/src/__tests__/observability-tools.test.ts +0 -595
- package/src/__tests__/observability.test.ts +0 -437
- package/src/__tests__/observation-extractor.test.ts +0 -167
- package/src/__tests__/page-deps.test.ts +0 -60
- package/src/__tests__/prompt-analyzer.test.ts +0 -298
- package/src/__tests__/regression-detector.test.ts +0 -295
- package/src/__tests__/rules.test.ts +0 -87
- package/src/__tests__/schema-mapper.test.ts +0 -29
- package/src/__tests__/security-scorer.test.ts +0 -238
- package/src/__tests__/security-utils.test.ts +0 -175
- package/src/__tests__/sentinel-db.test.ts +0 -491
- package/src/__tests__/sentinel-scanner.test.ts +0 -750
- package/src/__tests__/sentinel-tools.test.ts +0 -324
- package/src/__tests__/sentinel-types.test.ts +0 -750
- package/src/__tests__/server.test.ts +0 -452
- package/src/__tests__/session-archiver.test.ts +0 -524
- package/src/__tests__/session-state-generator.test.ts +0 -900
- package/src/__tests__/team-knowledge.test.ts +0 -327
- package/src/__tests__/tools.test.ts +0 -340
- package/src/__tests__/transcript-parser.test.ts +0 -195
- package/src/__tests__/trpc-index.test.ts +0 -25
- package/src/__tests__/validate-features-runner.test.ts +0 -517
- package/src/__tests__/validation-engine.test.ts +0 -300
- package/src/core-tools.ts +0 -685
- package/src/memory-queries.ts +0 -804
- package/src/memory-schema.ts +0 -546
- package/src/tool-helpers.ts +0 -41
package/src/config.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { resolve, dirname } from 'path';
|
|
5
5
|
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
import { homedir } from 'os';
|
|
6
7
|
import { parse as parseYaml } from 'yaml';
|
|
7
8
|
import { z } from 'zod';
|
|
8
9
|
|
|
@@ -159,6 +160,45 @@ const CloudConfigSchema = z.object({
|
|
|
159
160
|
}).optional();
|
|
160
161
|
export type CloudConfig = z.infer<typeof CloudConfigSchema>;
|
|
161
162
|
|
|
163
|
+
// --- Conventions Config ---
|
|
164
|
+
const ConventionsConfigSchema = z.object({
|
|
165
|
+
claudeDirName: z.string().default('.claude').refine(
|
|
166
|
+
s => !s.includes('..') && !s.startsWith('/'),
|
|
167
|
+
{ message: 'claudeDirName must not contain ".." or start with "/"' }
|
|
168
|
+
),
|
|
169
|
+
sessionStatePath: z.string().default('.claude/session-state/CURRENT.md').refine(
|
|
170
|
+
s => !s.includes('..') && !s.startsWith('/'),
|
|
171
|
+
{ message: 'sessionStatePath must not contain ".." or start with "/"' }
|
|
172
|
+
),
|
|
173
|
+
sessionArchivePath: z.string().default('.claude/session-state/archive').refine(
|
|
174
|
+
s => !s.includes('..') && !s.startsWith('/'),
|
|
175
|
+
{ message: 'sessionArchivePath must not contain ".." or start with "/"' }
|
|
176
|
+
),
|
|
177
|
+
knowledgeCategories: z.array(z.string()).default([
|
|
178
|
+
'patterns', 'commands', 'incidents', 'reference', 'protocols',
|
|
179
|
+
'checklists', 'playbooks', 'critical', 'scripts', 'status',
|
|
180
|
+
'templates', 'loop-state', 'session-state', 'agents',
|
|
181
|
+
]),
|
|
182
|
+
knowledgeSourceFiles: z.array(z.string()).default(['CLAUDE.md', 'MEMORY.md', 'corrections.md']),
|
|
183
|
+
excludePatterns: z.array(z.string()).default(['/ARCHIVE/', '/SESSION-HISTORY/']),
|
|
184
|
+
}).optional();
|
|
185
|
+
export type ConventionsConfig = z.infer<typeof ConventionsConfigSchema>;
|
|
186
|
+
|
|
187
|
+
// --- Python Config ---
|
|
188
|
+
const PythonDomainConfigSchema = z.object({
|
|
189
|
+
name: z.string(),
|
|
190
|
+
packages: z.array(z.string()),
|
|
191
|
+
allowed_imports_from: z.array(z.string()).default([]),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const PythonConfigSchema = z.object({
|
|
195
|
+
root: z.string(),
|
|
196
|
+
alembic_dir: z.string().optional(),
|
|
197
|
+
domains: z.array(PythonDomainConfigSchema).default([]),
|
|
198
|
+
exclude_dirs: z.array(z.string()).default(['__pycache__', '.venv', 'venv', '.mypy_cache', '.pytest_cache']),
|
|
199
|
+
}).optional();
|
|
200
|
+
export type PythonConfig = z.infer<typeof PythonConfigSchema>;
|
|
201
|
+
|
|
162
202
|
// --- Paths Config ---
|
|
163
203
|
const PathsConfigSchema = z.object({
|
|
164
204
|
source: z.string().default('src'),
|
|
@@ -198,6 +238,8 @@ const RawConfigSchema = z.object({
|
|
|
198
238
|
team: TeamConfigSchema,
|
|
199
239
|
regression: RegressionConfigSchema,
|
|
200
240
|
cloud: CloudConfigSchema,
|
|
241
|
+
conventions: ConventionsConfigSchema,
|
|
242
|
+
python: PythonConfigSchema,
|
|
201
243
|
}).passthrough();
|
|
202
244
|
|
|
203
245
|
// --- Final Config interface (derived from Zod) ---
|
|
@@ -217,6 +259,8 @@ export interface Config {
|
|
|
217
259
|
team?: TeamConfig;
|
|
218
260
|
regression?: RegressionConfig;
|
|
219
261
|
cloud?: CloudConfig;
|
|
262
|
+
conventions?: ConventionsConfig;
|
|
263
|
+
python?: PythonConfig;
|
|
220
264
|
}
|
|
221
265
|
|
|
222
266
|
let _config: Config | null = null;
|
|
@@ -312,8 +356,20 @@ export function getConfig(): Config {
|
|
|
312
356
|
team: parsed.team,
|
|
313
357
|
regression: parsed.regression,
|
|
314
358
|
cloud: parsed.cloud,
|
|
359
|
+
conventions: parsed.conventions,
|
|
360
|
+
python: parsed.python,
|
|
315
361
|
};
|
|
316
362
|
|
|
363
|
+
// Allow environment variable override for API key (security best practice)
|
|
364
|
+
if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
|
|
365
|
+
_config.cloud = {
|
|
366
|
+
enabled: true,
|
|
367
|
+
sync: { memory: true, analytics: true, audit: true },
|
|
368
|
+
..._config.cloud,
|
|
369
|
+
apiKey: process.env.MASSU_API_KEY,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
317
373
|
return _config;
|
|
318
374
|
}
|
|
319
375
|
|
|
@@ -324,6 +380,7 @@ export function getConfig(): Config {
|
|
|
324
380
|
export function getResolvedPaths() {
|
|
325
381
|
const config = getConfig();
|
|
326
382
|
const root = getProjectRoot();
|
|
383
|
+
const claudeDirName = config.conventions?.claudeDirName ?? '.claude';
|
|
327
384
|
|
|
328
385
|
return {
|
|
329
386
|
codegraphDbPath: resolve(root, '.codegraph/codegraph.db'),
|
|
@@ -340,11 +397,20 @@ export function getResolvedPaths() {
|
|
|
340
397
|
) as Record<string, string>,
|
|
341
398
|
extensions: ['.ts', '.tsx', '.js', '.jsx'] as const,
|
|
342
399
|
indexFiles: ['index.ts', 'index.tsx', 'index.js', 'index.jsx'] as const,
|
|
343
|
-
patternsDir: resolve(root, '
|
|
344
|
-
claudeMdPath: resolve(root, '
|
|
400
|
+
patternsDir: resolve(root, claudeDirName, 'patterns'),
|
|
401
|
+
claudeMdPath: resolve(root, claudeDirName, 'CLAUDE.md'),
|
|
345
402
|
docsMapPath: resolve(root, '.massu/docs-map.json'),
|
|
346
403
|
helpSitePath: resolve(root, '../' + config.project.name + '-help'),
|
|
347
404
|
memoryDbPath: resolve(root, '.massu/memory.db'),
|
|
405
|
+
knowledgeDbPath: resolve(root, '.massu/knowledge.db'),
|
|
406
|
+
plansDir: resolve(root, 'docs/plans'),
|
|
407
|
+
docsDir: resolve(root, 'docs'),
|
|
408
|
+
claudeDir: resolve(root, claudeDirName),
|
|
409
|
+
memoryDir: resolve(homedir(), claudeDirName, 'projects', root.replace(/\//g, '-'), 'memory'),
|
|
410
|
+
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
411
|
+
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
412
|
+
mcpJsonPath: resolve(root, '.mcp.json'),
|
|
413
|
+
settingsLocalPath: resolve(root, claudeDirName, 'settings.local.json'),
|
|
348
414
|
};
|
|
349
415
|
}
|
|
350
416
|
|
package/src/cost-tracker.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
3
|
|
|
4
4
|
import type Database from 'better-sqlite3';
|
|
5
|
-
import type { ToolDefinition, ToolResult } from './
|
|
6
|
-
import { p, text } from './tool-helpers.ts';
|
|
5
|
+
import type { ToolDefinition, ToolResult } from './tools.ts';
|
|
7
6
|
import type { TranscriptEntry } from './transcript-parser.ts';
|
|
8
7
|
import { getConfig } from './config.ts';
|
|
9
8
|
|
|
@@ -11,13 +10,17 @@ import { getConfig } from './config.ts';
|
|
|
11
10
|
// Cost Attribution Tracking
|
|
12
11
|
// ============================================================
|
|
13
12
|
|
|
13
|
+
/** Prefix a base tool name with the configured tool prefix. */
|
|
14
|
+
function p(baseName: string): string {
|
|
15
|
+
return `${getConfig().toolPrefix}_${baseName}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
/** Default model pricing (Claude models). Can be overridden via config.analytics.cost.models */
|
|
15
19
|
const DEFAULT_MODEL_PRICING: Record<string, { input_per_million: number; output_per_million: number; cache_read_per_million?: number; cache_write_per_million?: number }> = {
|
|
16
|
-
'claude-opus-4-6': { input_per_million:
|
|
20
|
+
'claude-opus-4-6': { input_per_million: 15.00, output_per_million: 75.00, cache_read_per_million: 1.50, cache_write_per_million: 18.75 },
|
|
17
21
|
'claude-sonnet-4-6': { input_per_million: 3.00, output_per_million: 15.00, cache_read_per_million: 0.30, cache_write_per_million: 3.75 },
|
|
18
22
|
'claude-sonnet-4-5': { input_per_million: 3.00, output_per_million: 15.00, cache_read_per_million: 0.30, cache_write_per_million: 3.75 },
|
|
19
|
-
'claude-
|
|
20
|
-
'claude-haiku-4-5-20251001': { input_per_million: 1.00, output_per_million: 5.00, cache_read_per_million: 0.10, cache_write_per_million: 1.25 },
|
|
23
|
+
'claude-haiku-4-5-20251001': { input_per_million: 0.80, output_per_million: 4.00, cache_read_per_million: 0.08, cache_write_per_million: 1.00 },
|
|
21
24
|
'default': { input_per_million: 3.00, output_per_million: 15.00, cache_read_per_million: 0.30, cache_write_per_million: 3.75 },
|
|
22
25
|
};
|
|
23
26
|
|
|
@@ -134,7 +137,6 @@ export function backfillSessionCosts(db: Database.Database): number {
|
|
|
134
137
|
FROM sessions s
|
|
135
138
|
LEFT JOIN session_costs c ON s.session_id = c.session_id
|
|
136
139
|
WHERE c.session_id IS NULL
|
|
137
|
-
LIMIT 1000
|
|
138
140
|
`).all() as Array<{ session_id: string }>;
|
|
139
141
|
|
|
140
142
|
// Backfilling requires transcript data which may not be available
|
|
@@ -348,3 +350,6 @@ function handleCostFeature(args: Record<string, unknown>, db: Database.Database)
|
|
|
348
350
|
return text(lines.join('\n'));
|
|
349
351
|
}
|
|
350
352
|
|
|
353
|
+
function text(content: string): ToolResult {
|
|
354
|
+
return { content: [{ type: 'text', text: content }] };
|
|
355
|
+
}
|
package/src/db.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
3
|
|
|
4
4
|
import Database from 'better-sqlite3';
|
|
5
|
-
import { dirname } from 'path';
|
|
6
|
-
import { existsSync, mkdirSync } from 'fs';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
7
7
|
import { getResolvedPaths } from './config.ts';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -96,6 +96,84 @@ function initDataSchema(db: Database.Database): void {
|
|
|
96
96
|
value TEXT NOT NULL
|
|
97
97
|
);
|
|
98
98
|
|
|
99
|
+
-- ============================================================
|
|
100
|
+
-- Python Code Intelligence Tables
|
|
101
|
+
-- ============================================================
|
|
102
|
+
|
|
103
|
+
CREATE TABLE IF NOT EXISTS massu_py_imports (
|
|
104
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
105
|
+
source_file TEXT NOT NULL,
|
|
106
|
+
target_file TEXT NOT NULL,
|
|
107
|
+
import_type TEXT NOT NULL CHECK(import_type IN ('absolute', 'relative', 'from_absolute', 'from_relative')),
|
|
108
|
+
imported_names TEXT NOT NULL DEFAULT '[]',
|
|
109
|
+
line INTEGER NOT NULL DEFAULT 0
|
|
110
|
+
);
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_imports_source ON massu_py_imports(source_file);
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_imports_target ON massu_py_imports(target_file);
|
|
113
|
+
|
|
114
|
+
CREATE TABLE IF NOT EXISTS massu_py_meta (
|
|
115
|
+
key TEXT PRIMARY KEY,
|
|
116
|
+
value TEXT NOT NULL
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
CREATE TABLE IF NOT EXISTS massu_py_routes (
|
|
120
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
121
|
+
file TEXT NOT NULL,
|
|
122
|
+
method TEXT NOT NULL,
|
|
123
|
+
path TEXT NOT NULL,
|
|
124
|
+
function_name TEXT NOT NULL,
|
|
125
|
+
dependencies TEXT NOT NULL DEFAULT '[]',
|
|
126
|
+
request_model TEXT,
|
|
127
|
+
response_model TEXT,
|
|
128
|
+
is_authenticated INTEGER NOT NULL DEFAULT 0,
|
|
129
|
+
line INTEGER NOT NULL DEFAULT 0
|
|
130
|
+
);
|
|
131
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_routes_path ON massu_py_routes(path);
|
|
132
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_routes_file ON massu_py_routes(file);
|
|
133
|
+
|
|
134
|
+
CREATE TABLE IF NOT EXISTS massu_py_route_callers (
|
|
135
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
136
|
+
route_id INTEGER NOT NULL REFERENCES massu_py_routes(id) ON DELETE CASCADE,
|
|
137
|
+
frontend_file TEXT NOT NULL,
|
|
138
|
+
line INTEGER NOT NULL DEFAULT 0,
|
|
139
|
+
call_pattern TEXT NOT NULL
|
|
140
|
+
);
|
|
141
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_route_callers_route ON massu_py_route_callers(route_id);
|
|
142
|
+
|
|
143
|
+
CREATE TABLE IF NOT EXISTS massu_py_models (
|
|
144
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
145
|
+
class_name TEXT NOT NULL,
|
|
146
|
+
table_name TEXT,
|
|
147
|
+
file TEXT NOT NULL,
|
|
148
|
+
line INTEGER NOT NULL DEFAULT 0,
|
|
149
|
+
columns TEXT NOT NULL DEFAULT '[]',
|
|
150
|
+
relationships TEXT NOT NULL DEFAULT '[]',
|
|
151
|
+
foreign_keys TEXT NOT NULL DEFAULT '[]'
|
|
152
|
+
);
|
|
153
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_models_file ON massu_py_models(file);
|
|
154
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_models_table ON massu_py_models(table_name);
|
|
155
|
+
|
|
156
|
+
CREATE TABLE IF NOT EXISTS massu_py_fk_edges (
|
|
157
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
158
|
+
source_table TEXT NOT NULL,
|
|
159
|
+
source_column TEXT NOT NULL,
|
|
160
|
+
target_table TEXT NOT NULL,
|
|
161
|
+
target_column TEXT NOT NULL
|
|
162
|
+
);
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_fk_source ON massu_py_fk_edges(source_table);
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_fk_target ON massu_py_fk_edges(target_table);
|
|
165
|
+
|
|
166
|
+
CREATE TABLE IF NOT EXISTS massu_py_migrations (
|
|
167
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
168
|
+
revision TEXT NOT NULL UNIQUE,
|
|
169
|
+
down_revision TEXT,
|
|
170
|
+
file TEXT NOT NULL,
|
|
171
|
+
description TEXT,
|
|
172
|
+
operations TEXT NOT NULL DEFAULT '[]',
|
|
173
|
+
is_head INTEGER NOT NULL DEFAULT 0
|
|
174
|
+
);
|
|
175
|
+
CREATE INDEX IF NOT EXISTS idx_massu_py_migrations_rev ON massu_py_migrations(revision);
|
|
176
|
+
|
|
99
177
|
-- ============================================================
|
|
100
178
|
-- Sentinel: Feature Registry Tables
|
|
101
179
|
-- ============================================================
|
|
@@ -231,3 +309,38 @@ export function isDataStale(dataDb: Database.Database, codegraphDb: Database.Dat
|
|
|
231
309
|
export function updateBuildTimestamp(dataDb: Database.Database): void {
|
|
232
310
|
dataDb.prepare("INSERT OR REPLACE INTO massu_meta (key, value) VALUES ('last_build_time', ?)").run(new Date().toISOString());
|
|
233
311
|
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Check if Python indexes are stale based on massu_py_meta.last_build_time.
|
|
315
|
+
*/
|
|
316
|
+
export function isPythonDataStale(dataDb: Database.Database, pythonRoot: string): boolean {
|
|
317
|
+
const lastBuild = dataDb.prepare("SELECT value FROM massu_py_meta WHERE key = 'last_build_time'").get() as { value: string } | undefined;
|
|
318
|
+
if (!lastBuild) return true;
|
|
319
|
+
|
|
320
|
+
const lastBuildTime = new Date(lastBuild.value).getTime();
|
|
321
|
+
// Check if any .py file is newer than last build
|
|
322
|
+
function checkDir(dir: string): boolean {
|
|
323
|
+
try {
|
|
324
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
325
|
+
for (const entry of entries) {
|
|
326
|
+
const fullPath = join(dir, entry.name);
|
|
327
|
+
if (entry.isDirectory()) {
|
|
328
|
+
if (['__pycache__', '.venv', 'venv', 'node_modules', '.mypy_cache', '.pytest_cache'].includes(entry.name)) continue;
|
|
329
|
+
if (checkDir(fullPath)) return true;
|
|
330
|
+
} else if (entry.name.endsWith('.py')) {
|
|
331
|
+
if (statSync(fullPath).mtimeMs > lastBuildTime) return true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
} catch { /* directory may not exist */ }
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return checkDir(pythonRoot);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Update the Python build timestamp in massu_py_meta.
|
|
343
|
+
*/
|
|
344
|
+
export function updatePythonBuildTimestamp(dataDb: Database.Database): void {
|
|
345
|
+
dataDb.prepare("INSERT OR REPLACE INTO massu_py_meta (key, value) VALUES ('last_build_time', ?)").run(new Date().toISOString());
|
|
346
|
+
}
|
package/src/dependency-scorer.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
3
|
|
|
4
4
|
import type Database from 'better-sqlite3';
|
|
5
|
-
import type { ToolDefinition, ToolResult } from './
|
|
6
|
-
import { p, text } from './tool-helpers.ts';
|
|
5
|
+
import type { ToolDefinition, ToolResult } from './tools.ts';
|
|
7
6
|
import { getConfig } from './config.ts';
|
|
8
7
|
import { existsSync, readFileSync } from 'fs';
|
|
9
8
|
import { resolve } from 'path';
|
|
@@ -12,6 +11,11 @@ import { resolve } from 'path';
|
|
|
12
11
|
// Dependency Risk Scoring
|
|
13
12
|
// ============================================================
|
|
14
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
|
+
|
|
15
19
|
export interface DepRiskFactors {
|
|
16
20
|
vulnerabilities: number;
|
|
17
21
|
lastPublishDays: number | null;
|
|
@@ -328,3 +332,6 @@ function handleDepAlternatives(args: Record<string, unknown>, db: Database.Datab
|
|
|
328
332
|
return text(lines.filter(Boolean).join('\n'));
|
|
329
333
|
}
|
|
330
334
|
|
|
335
|
+
function text(content: string): ToolResult {
|
|
336
|
+
return { content: [{ type: 'text', text: content }] };
|
|
337
|
+
}
|
package/src/docs-tools.ts
CHANGED
|
@@ -3,23 +3,20 @@
|
|
|
3
3
|
|
|
4
4
|
import { readFileSync, existsSync } from 'fs';
|
|
5
5
|
import { resolve, basename } from 'path';
|
|
6
|
-
import { getConfig, getResolvedPaths } from './config.ts';
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
6
|
+
import { getConfig, getResolvedPaths, getProjectRoot } from './config.ts';
|
|
7
|
+
import { ensureWithinRoot } from './security-utils.ts';
|
|
8
|
+
import type { ToolDefinition, ToolResult } from './tools.ts';
|
|
9
|
+
|
|
10
|
+
/** Prefix a base tool name with the configured tool prefix. */
|
|
11
|
+
function p(baseName: string): string {
|
|
12
|
+
return `${getConfig().toolPrefix}_${baseName}`;
|
|
13
|
+
}
|
|
9
14
|
|
|
10
15
|
// ============================================================
|
|
11
16
|
// Help Site Auto-Sync: MCP Docs Tools
|
|
12
17
|
// docs_audit + docs_coverage
|
|
13
18
|
// ============================================================
|
|
14
19
|
|
|
15
|
-
const DOCS_BASE_NAMES = new Set(['docs_audit', 'docs_coverage']);
|
|
16
|
-
|
|
17
|
-
export function isDocsTool(name: string): boolean {
|
|
18
|
-
const pfx = getConfig().toolPrefix + '_';
|
|
19
|
-
const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
|
|
20
|
-
return DOCS_BASE_NAMES.has(baseName);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
20
|
interface DocsMapping {
|
|
24
21
|
id: string;
|
|
25
22
|
helpPage: string;
|
|
@@ -256,10 +253,11 @@ function extractFrontmatter(content: string): Record<string, string> | null {
|
|
|
256
253
|
* Extract procedure names from a router file.
|
|
257
254
|
*/
|
|
258
255
|
function extractProcedureNames(routerPath: string): string[] {
|
|
259
|
-
const
|
|
256
|
+
const root = getProjectRoot();
|
|
257
|
+
const absPath = ensureWithinRoot(resolve(getResolvedPaths().srcDir, '..', routerPath), root);
|
|
260
258
|
if (!existsSync(absPath)) {
|
|
261
259
|
// Try from project root
|
|
262
|
-
const altPath = resolve(getResolvedPaths().srcDir, '../server/api/routers', basename(routerPath));
|
|
260
|
+
const altPath = ensureWithinRoot(resolve(getResolvedPaths().srcDir, '../server/api/routers', basename(routerPath)), root);
|
|
263
261
|
if (!existsSync(altPath)) return [];
|
|
264
262
|
return extractProcedureNamesFromContent(readFileSync(altPath, 'utf-8'));
|
|
265
263
|
}
|
|
@@ -329,7 +327,7 @@ function handleDocsAudit(args: Record<string, unknown>): ToolResult {
|
|
|
329
327
|
const mapping = docsMap.mappings.find(m => m.id === mappingId);
|
|
330
328
|
if (!mapping) continue;
|
|
331
329
|
|
|
332
|
-
const helpPagePath = resolve(getResolvedPaths().helpSitePath, mapping.helpPage);
|
|
330
|
+
const helpPagePath = ensureWithinRoot(resolve(getResolvedPaths().helpSitePath, mapping.helpPage), getProjectRoot());
|
|
333
331
|
|
|
334
332
|
if (!existsSync(helpPagePath)) {
|
|
335
333
|
results.push({
|
|
@@ -398,7 +396,7 @@ function handleDocsAudit(args: Record<string, unknown>): ToolResult {
|
|
|
398
396
|
// Also flag inherited user guides
|
|
399
397
|
for (const [guideName, parentId] of Object.entries(docsMap.userGuideInheritance.examples)) {
|
|
400
398
|
if (parentId === mappingId) {
|
|
401
|
-
const guidePath = resolve(getResolvedPaths().helpSitePath, `pages/user-guides/${guideName}/index.mdx`);
|
|
399
|
+
const guidePath = ensureWithinRoot(resolve(getResolvedPaths().helpSitePath, `pages/user-guides/${guideName}/index.mdx`), getProjectRoot());
|
|
402
400
|
if (existsSync(guidePath)) {
|
|
403
401
|
const guideContent = readFileSync(guidePath, 'utf-8');
|
|
404
402
|
const guideFrontmatter = extractFrontmatter(guideContent);
|
|
@@ -443,7 +441,7 @@ function handleDocsCoverage(args: Record<string, unknown>): ToolResult {
|
|
|
443
441
|
: docsMap.mappings;
|
|
444
442
|
|
|
445
443
|
for (const mapping of mappings) {
|
|
446
|
-
const helpPagePath = resolve(getResolvedPaths().helpSitePath, mapping.helpPage);
|
|
444
|
+
const helpPagePath = ensureWithinRoot(resolve(getResolvedPaths().helpSitePath, mapping.helpPage), getProjectRoot());
|
|
447
445
|
const exists = existsSync(helpPagePath);
|
|
448
446
|
let hasContent = false;
|
|
449
447
|
let lineCount = 0;
|
|
@@ -512,3 +510,10 @@ function handleDocsCoverage(args: Record<string, unknown>): ToolResult {
|
|
|
512
510
|
return text(lines.join('\n'));
|
|
513
511
|
}
|
|
514
512
|
|
|
513
|
+
// ============================================================
|
|
514
|
+
// Utilities
|
|
515
|
+
// ============================================================
|
|
516
|
+
|
|
517
|
+
function text(content: string): ToolResult {
|
|
518
|
+
return { content: [{ type: 'text', text: content }] };
|
|
519
|
+
}
|
|
@@ -37,7 +37,7 @@ async function main(): Promise<void> {
|
|
|
37
37
|
const rel = filePath.startsWith(root + '/') ? filePath.slice(root.length + 1) : filePath;
|
|
38
38
|
|
|
39
39
|
// Only process src/ files
|
|
40
|
-
if (!rel.startsWith('src/')) {
|
|
40
|
+
if (!rel.startsWith('src/') && !rel.endsWith('.py')) {
|
|
41
41
|
process.exit(0);
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
@@ -54,12 +54,12 @@ async function main(): Promise<void> {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// 2. Check middleware tree membership
|
|
57
|
+
// 2. Check middleware tree membership
|
|
58
58
|
try {
|
|
59
59
|
const dataDb = new Database(getResolvedPaths().dataDbPath, { readonly: true });
|
|
60
60
|
try {
|
|
61
61
|
if (isInMiddlewareTree(dataDb, rel)) {
|
|
62
|
-
warnings.push('[CRITICAL]
|
|
62
|
+
warnings.push('[CRITICAL] This file is in the middleware import tree. No Node.js deps allowed.');
|
|
63
63
|
}
|
|
64
64
|
} finally {
|
|
65
65
|
dataDb.close();
|
|
@@ -70,7 +70,7 @@ async function main(): Promise<void> {
|
|
|
70
70
|
|
|
71
71
|
// 3. Output warnings if any
|
|
72
72
|
if (warnings.length > 0) {
|
|
73
|
-
console.log(`[
|
|
73
|
+
console.log(`[Massu] ${warnings.join(' | ')}`);
|
|
74
74
|
}
|
|
75
75
|
} catch (_e) {
|
|
76
76
|
// Best-effort: never block Claude Code
|
|
@@ -14,6 +14,9 @@ import { logAuditEntry } from '../audit-trail.ts';
|
|
|
14
14
|
import { trackModification } from '../regression-detector.ts';
|
|
15
15
|
import { validateFile, storeValidationResult } from '../validation-engine.ts';
|
|
16
16
|
import { scoreFileSecurity, storeSecurityScore } from '../security-scorer.ts';
|
|
17
|
+
import { readFileSync, existsSync } from 'fs';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
import { parse as parseYaml } from 'yaml';
|
|
17
20
|
|
|
18
21
|
interface HookInput {
|
|
19
22
|
session_id: string;
|
|
@@ -126,6 +129,41 @@ async function main(): Promise<void> {
|
|
|
126
129
|
} catch (_securityErr) {
|
|
127
130
|
// Best-effort: never block post-tool-use
|
|
128
131
|
}
|
|
132
|
+
|
|
133
|
+
// MEMORY.md integrity check on write
|
|
134
|
+
try {
|
|
135
|
+
if (tool_name === 'Edit' || tool_name === 'Write') {
|
|
136
|
+
const filePath = (tool_input.file_path as string) ?? '';
|
|
137
|
+
if (filePath && filePath.endsWith('MEMORY.md') && filePath.includes('/memory/')) {
|
|
138
|
+
const issues = checkMemoryFileIntegrity(filePath);
|
|
139
|
+
if (issues.length > 0) {
|
|
140
|
+
addObservation(db, session_id, 'incident_near_miss',
|
|
141
|
+
'MEMORY.md integrity issue detected',
|
|
142
|
+
issues.join('; '),
|
|
143
|
+
{ importance: 4 }
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch (_memoryErr) {
|
|
149
|
+
// Best-effort: never block post-tool-use
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Knowledge index staleness check on knowledge file edits
|
|
153
|
+
try {
|
|
154
|
+
if (tool_name === 'Edit' || tool_name === 'Write') {
|
|
155
|
+
const filePath = (tool_input.file_path as string) ?? '';
|
|
156
|
+
if (filePath && isKnowledgeSourceFile(filePath)) {
|
|
157
|
+
addObservation(db, session_id, 'discovery',
|
|
158
|
+
'Knowledge source file modified - index may be stale',
|
|
159
|
+
`Edited ${filePath.split('/').pop() ?? filePath}. Run knowledge re-index to update.`,
|
|
160
|
+
{ importance: 3 }
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} catch (_knowledgeErr) {
|
|
165
|
+
// Best-effort: never block post-tool-use
|
|
166
|
+
}
|
|
129
167
|
} finally {
|
|
130
168
|
db.close();
|
|
131
169
|
}
|
|
@@ -172,4 +210,96 @@ function readStdin(): Promise<string> {
|
|
|
172
210
|
});
|
|
173
211
|
}
|
|
174
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Read the conventions section from massu.config.yaml directly.
|
|
215
|
+
* Hooks are compiled with esbuild and cannot use getConfig() from config.ts.
|
|
216
|
+
* Falls back to sensible defaults if the config file is not found.
|
|
217
|
+
*/
|
|
218
|
+
function readConventions(cwd?: string): {
|
|
219
|
+
knowledgeSourceFiles: string[];
|
|
220
|
+
claudeDirName: string;
|
|
221
|
+
} {
|
|
222
|
+
const defaults = {
|
|
223
|
+
knowledgeSourceFiles: ['CLAUDE.md', 'MEMORY.md', 'corrections.md'],
|
|
224
|
+
claudeDirName: '.claude',
|
|
225
|
+
};
|
|
226
|
+
try {
|
|
227
|
+
const projectRoot = cwd ?? process.cwd();
|
|
228
|
+
const configPath = join(projectRoot, 'massu.config.yaml');
|
|
229
|
+
if (!existsSync(configPath)) return defaults;
|
|
230
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
231
|
+
const parsed = parseYaml(content) as Record<string, unknown> | null;
|
|
232
|
+
if (!parsed || typeof parsed !== 'object') return defaults;
|
|
233
|
+
const conventions = parsed.conventions as Record<string, unknown> | undefined;
|
|
234
|
+
if (!conventions || typeof conventions !== 'object') return defaults;
|
|
235
|
+
return {
|
|
236
|
+
knowledgeSourceFiles: Array.isArray(conventions.knowledgeSourceFiles)
|
|
237
|
+
? conventions.knowledgeSourceFiles as string[]
|
|
238
|
+
: defaults.knowledgeSourceFiles,
|
|
239
|
+
claudeDirName: typeof conventions.claudeDirName === 'string'
|
|
240
|
+
? conventions.claudeDirName
|
|
241
|
+
: defaults.claudeDirName,
|
|
242
|
+
};
|
|
243
|
+
} catch {
|
|
244
|
+
return defaults;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Check if a file path is a knowledge source file (CLAUDE.md, corrections.md,
|
|
250
|
+
* memory files, or knowledge system source files).
|
|
251
|
+
* When these are edited, the knowledge index may become stale.
|
|
252
|
+
*/
|
|
253
|
+
function isKnowledgeSourceFile(filePath: string): boolean {
|
|
254
|
+
const basename = filePath.split('/').pop() ?? '';
|
|
255
|
+
const conventions = readConventions();
|
|
256
|
+
const knowledgeSourcePatterns = [
|
|
257
|
+
...conventions.knowledgeSourceFiles,
|
|
258
|
+
'file-index.md',
|
|
259
|
+
'knowledge-db.ts',
|
|
260
|
+
'knowledge-indexer.ts',
|
|
261
|
+
'knowledge-tools.ts',
|
|
262
|
+
];
|
|
263
|
+
return knowledgeSourcePatterns.some(p => basename === p) ||
|
|
264
|
+
filePath.includes('/memory/') ||
|
|
265
|
+
filePath.includes(conventions.claudeDirName + '/');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Check MEMORY.md file integrity after a write.
|
|
270
|
+
* Verifies: file exists, has expected structure, and is under line limit.
|
|
271
|
+
* Returns array of issue descriptions (empty = all good).
|
|
272
|
+
*/
|
|
273
|
+
function checkMemoryFileIntegrity(filePath: string): string[] {
|
|
274
|
+
const issues: string[] = [];
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
if (!existsSync(filePath)) {
|
|
278
|
+
issues.push('MEMORY.md file does not exist after write');
|
|
279
|
+
return issues;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
283
|
+
const lines = content.split('\n');
|
|
284
|
+
|
|
285
|
+
// Check line count (CLAUDE.md truncates after ~200 lines)
|
|
286
|
+
const MAX_LINES = 200;
|
|
287
|
+
if (lines.length > MAX_LINES) {
|
|
288
|
+
issues.push(`MEMORY.md exceeds ${MAX_LINES} lines (currently ${lines.length}). Consider archiving old entries.`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Check required structure sections
|
|
292
|
+
const requiredSections = ['# Massu Memory', '## Key Learnings', '## Common Gotchas'];
|
|
293
|
+
for (const section of requiredSections) {
|
|
294
|
+
if (!content.includes(section)) {
|
|
295
|
+
issues.push(`Missing required section: "${section}"`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch (_e) {
|
|
299
|
+
// Graceful degradation: don't report issues if we can't check
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return issues;
|
|
303
|
+
}
|
|
304
|
+
|
|
175
305
|
main();
|
package/src/hooks/pre-compact.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Captures current session state into DB before compaction destroys context.
|
|
8
8
|
// ============================================================
|
|
9
9
|
|
|
10
|
-
import { getMemoryDb, addSummary, createSession } from '../memory-db.ts';
|
|
10
|
+
import { getMemoryDb, addSummary, createSession, addObservation } from '../memory-db.ts';
|
|
11
11
|
import { logAuditEntry } from '../audit-trail.ts';
|
|
12
12
|
import type { SessionSummary } from '../memory-db.ts';
|
|
13
13
|
|
|
@@ -45,6 +45,28 @@ async function main(): Promise<void> {
|
|
|
45
45
|
// 4. Store with pre_compact marker in plan_progress
|
|
46
46
|
addSummary(db, session_id, summary);
|
|
47
47
|
|
|
48
|
+
// 5. Preserve knowledge system state for post-compaction context
|
|
49
|
+
try {
|
|
50
|
+
const knowledgeObs = observations.filter(
|
|
51
|
+
o => (o.title as string)?.includes('knowledge') ||
|
|
52
|
+
(o.title as string)?.includes('Knowledge') ||
|
|
53
|
+
(o.detail as string)?.includes('knowledge')
|
|
54
|
+
);
|
|
55
|
+
if (knowledgeObs.length > 0) {
|
|
56
|
+
const knowledgeContext = knowledgeObs
|
|
57
|
+
.map(o => `[${o.type}] ${o.title}`)
|
|
58
|
+
.join('; ');
|
|
59
|
+
// Store as a high-importance observation so session-start picks it up
|
|
60
|
+
addObservation(db, session_id, 'discovery',
|
|
61
|
+
'Knowledge context preserved before compaction',
|
|
62
|
+
knowledgeContext,
|
|
63
|
+
{ importance: 4 }
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
} catch (_knowledgeErr) {
|
|
67
|
+
// Best-effort: never block compaction
|
|
68
|
+
}
|
|
69
|
+
|
|
48
70
|
// Log compaction event for audit trail continuity
|
|
49
71
|
try {
|
|
50
72
|
logAuditEntry(db, {
|