@massu/core 0.9.2 → 1.1.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/dist/cli.js +11182 -1559
- package/dist/hooks/auto-learning-pipeline.js +99 -19
- package/dist/hooks/classify-failure.js +99 -19
- package/dist/hooks/cost-tracker.js +97 -11
- package/dist/hooks/fix-detector.js +99 -19
- package/dist/hooks/incident-pipeline.js +97 -11
- package/dist/hooks/post-edit-context.js +97 -11
- package/dist/hooks/post-tool-use.js +101 -20
- package/dist/hooks/pre-compact.js +97 -11
- package/dist/hooks/pre-delete-check.js +97 -11
- package/dist/hooks/quality-event.js +97 -11
- package/dist/hooks/rule-enforcement-pipeline.js +97 -11
- package/dist/hooks/session-end.js +97 -11
- package/dist/hooks/session-start.js +8803 -782
- package/dist/hooks/user-prompt.js +98 -43
- package/package.json +13 -3
- package/reference/hook-execution-order.md +17 -25
- package/src/cli.ts +81 -2
- package/src/commands/config-check-drift.ts +132 -0
- package/src/commands/config-refresh.ts +224 -0
- package/src/commands/config-upgrade.ts +126 -0
- package/src/commands/doctor.ts +1 -29
- package/src/commands/init.ts +756 -216
- package/src/config.ts +168 -12
- package/src/detect/domain-inferrer.ts +142 -0
- package/src/detect/drift.ts +199 -0
- package/src/detect/framework-detector.ts +281 -0
- package/src/detect/index.ts +174 -0
- package/src/detect/migrate.ts +278 -0
- package/src/detect/monorepo-detector.ts +347 -0
- package/src/detect/package-detector.ts +728 -0
- package/src/detect/source-dir-detector.ts +264 -0
- package/src/detect/vr-command-map.ts +167 -0
- package/src/hooks/auto-learning-pipeline.ts +2 -2
- package/src/hooks/classify-failure.ts +2 -2
- package/src/hooks/fix-detector.ts +2 -2
- package/src/hooks/session-start.ts +43 -2
- package/src/hooks/user-prompt.ts +1 -21
- package/src/knowledge-indexer.ts +1 -1
- package/src/license.ts +1 -2
- package/src/memory-db.ts +0 -5
- package/src/memory-file-ingest.ts +6 -13
- package/src/tools.ts +0 -8
- package/templates/multi-runtime/massu.config.yaml +80 -0
- package/templates/python-django/massu.config.yaml +51 -0
- package/templates/python-fastapi/massu.config.yaml +50 -0
- package/templates/rust-actix/massu.config.yaml +38 -0
- package/templates/swift-ios/massu.config.yaml +37 -0
- package/templates/ts-nestjs/massu.config.yaml +43 -0
- package/templates/ts-nextjs/massu.config.yaml +43 -0
- package/README.md +0 -40
- package/src/claude-md-templates.ts +0 -342
- package/src/mcp-bridge-tools.ts +0 -458
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Framework Detector (P1-002)
|
|
6
|
+
* ============================
|
|
7
|
+
*
|
|
8
|
+
* Takes `PackageManifest[]` from P1-001 and infers each language's web
|
|
9
|
+
* framework, test framework, ORM, and UI library by matching declared
|
|
10
|
+
* dependencies against an inline `DETECTION_RULES` table.
|
|
11
|
+
*
|
|
12
|
+
* The `DETECTION_RULES` table ships as built-ins. Users may ADD extra entries
|
|
13
|
+
* via `massu.config.yaml` under `detection.rules[<language>][<framework>]`
|
|
14
|
+
* (see P2-008). With `detection.disable_builtin: true`, user entries replace
|
|
15
|
+
* built-ins entirely.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { detectFrameworks } from './detect/framework-detector.ts';
|
|
20
|
+
* const map = detectFrameworks(manifests, config.detection);
|
|
21
|
+
* map.python // => { framework: 'fastapi', test_framework: 'pytest', orm: 'sqlalchemy', ... }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { PackageManifest, SupportedLanguage } from './package-detector.ts';
|
|
26
|
+
|
|
27
|
+
export interface FrameworkInfo {
|
|
28
|
+
/** Inferred framework name (e.g., 'fastapi', 'next', 'actix-web'). */
|
|
29
|
+
framework: string | null;
|
|
30
|
+
/** Framework version if declared in manifest. */
|
|
31
|
+
version: string | null;
|
|
32
|
+
/** Inferred test framework (e.g., 'pytest', 'vitest', 'cargo'). */
|
|
33
|
+
test_framework: string | null;
|
|
34
|
+
/** Inferred ORM. */
|
|
35
|
+
orm: string | null;
|
|
36
|
+
/** Inferred UI library (for TS/JS). */
|
|
37
|
+
ui_library: string | null;
|
|
38
|
+
/** Inferred router (for TS/JS — trpc/graphql/express/fastify). */
|
|
39
|
+
router: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type FrameworkMap = Partial<Record<SupportedLanguage, FrameworkInfo>>;
|
|
43
|
+
|
|
44
|
+
type RuleKind = 'framework' | 'test_framework' | 'orm' | 'ui_library' | 'router';
|
|
45
|
+
|
|
46
|
+
interface DetectionRule {
|
|
47
|
+
language: SupportedLanguage;
|
|
48
|
+
kind: RuleKind;
|
|
49
|
+
/** Lowercase dependency keyword to search (exact match on dep name). */
|
|
50
|
+
keyword: string;
|
|
51
|
+
/** Value set in FrameworkInfo when matched. */
|
|
52
|
+
value: string;
|
|
53
|
+
/** Higher wins; defaults to 0. */
|
|
54
|
+
priority?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Built-in detection rules. Exported so users (and tests) can inspect.
|
|
59
|
+
* User overrides/additions come from `config.detection.rules`.
|
|
60
|
+
*/
|
|
61
|
+
export const DETECTION_RULES: DetectionRule[] = [
|
|
62
|
+
// Python frameworks
|
|
63
|
+
{ language: 'python', kind: 'framework', keyword: 'fastapi', value: 'fastapi', priority: 10 },
|
|
64
|
+
{ language: 'python', kind: 'framework', keyword: 'flask', value: 'flask', priority: 9 },
|
|
65
|
+
{ language: 'python', kind: 'framework', keyword: 'django', value: 'django', priority: 9 },
|
|
66
|
+
{ language: 'python', kind: 'framework', keyword: 'aiohttp', value: 'aiohttp', priority: 8 },
|
|
67
|
+
{ language: 'python', kind: 'framework', keyword: 'sanic', value: 'sanic', priority: 8 },
|
|
68
|
+
{ language: 'python', kind: 'framework', keyword: 'starlette', value: 'starlette', priority: 7 },
|
|
69
|
+
// Python test
|
|
70
|
+
{ language: 'python', kind: 'test_framework', keyword: 'pytest', value: 'pytest', priority: 10 },
|
|
71
|
+
{ language: 'python', kind: 'test_framework', keyword: 'pytest-asyncio', value: 'pytest', priority: 9 },
|
|
72
|
+
// Python ORM
|
|
73
|
+
{ language: 'python', kind: 'orm', keyword: 'sqlalchemy', value: 'sqlalchemy', priority: 10 },
|
|
74
|
+
{ language: 'python', kind: 'orm', keyword: 'django-orm', value: 'django-orm', priority: 9 },
|
|
75
|
+
{ language: 'python', kind: 'orm', keyword: 'peewee', value: 'peewee', priority: 8 },
|
|
76
|
+
{ language: 'python', kind: 'orm', keyword: 'tortoise-orm', value: 'tortoise-orm', priority: 8 },
|
|
77
|
+
|
|
78
|
+
// TypeScript / JavaScript frameworks
|
|
79
|
+
{ language: 'typescript', kind: 'framework', keyword: 'next', value: 'next', priority: 10 },
|
|
80
|
+
{ language: 'typescript', kind: 'framework', keyword: '@nestjs/core', value: 'nestjs', priority: 10 },
|
|
81
|
+
{ language: 'typescript', kind: 'framework', keyword: 'fastify', value: 'fastify', priority: 9 },
|
|
82
|
+
{ language: 'typescript', kind: 'framework', keyword: 'express', value: 'express', priority: 9 },
|
|
83
|
+
{ language: 'typescript', kind: 'framework', keyword: 'hono', value: 'hono', priority: 9 },
|
|
84
|
+
{ language: 'typescript', kind: 'framework', keyword: '@sveltejs/kit', value: 'sveltekit', priority: 10 },
|
|
85
|
+
{ language: 'typescript', kind: 'framework', keyword: 'nuxt', value: 'nuxt', priority: 10 },
|
|
86
|
+
{ language: 'typescript', kind: 'framework', keyword: '@angular/core', value: 'angular', priority: 10 },
|
|
87
|
+
{ language: 'typescript', kind: 'framework', keyword: 'react', value: 'react', priority: 5 },
|
|
88
|
+
{ language: 'typescript', kind: 'framework', keyword: 'vue', value: 'vue', priority: 5 },
|
|
89
|
+
// Mirror for javascript
|
|
90
|
+
{ language: 'javascript', kind: 'framework', keyword: 'next', value: 'next', priority: 10 },
|
|
91
|
+
{ language: 'javascript', kind: 'framework', keyword: 'express', value: 'express', priority: 9 },
|
|
92
|
+
{ language: 'javascript', kind: 'framework', keyword: 'fastify', value: 'fastify', priority: 9 },
|
|
93
|
+
{ language: 'javascript', kind: 'framework', keyword: 'react', value: 'react', priority: 5 },
|
|
94
|
+
// TS/JS test
|
|
95
|
+
{ language: 'typescript', kind: 'test_framework', keyword: 'vitest', value: 'vitest', priority: 10 },
|
|
96
|
+
{ language: 'typescript', kind: 'test_framework', keyword: 'jest', value: 'jest', priority: 9 },
|
|
97
|
+
{ language: 'typescript', kind: 'test_framework', keyword: 'mocha', value: 'mocha', priority: 8 },
|
|
98
|
+
{ language: 'typescript', kind: 'test_framework', keyword: '@playwright/test', value: 'playwright', priority: 7 },
|
|
99
|
+
{ language: 'javascript', kind: 'test_framework', keyword: 'vitest', value: 'vitest', priority: 10 },
|
|
100
|
+
{ language: 'javascript', kind: 'test_framework', keyword: 'jest', value: 'jest', priority: 9 },
|
|
101
|
+
{ language: 'javascript', kind: 'test_framework', keyword: 'mocha', value: 'mocha', priority: 8 },
|
|
102
|
+
// TS/JS ORM
|
|
103
|
+
{ language: 'typescript', kind: 'orm', keyword: '@prisma/client', value: 'prisma', priority: 10 },
|
|
104
|
+
{ language: 'typescript', kind: 'orm', keyword: 'prisma', value: 'prisma', priority: 9 },
|
|
105
|
+
{ language: 'typescript', kind: 'orm', keyword: 'drizzle-orm', value: 'drizzle', priority: 10 },
|
|
106
|
+
{ language: 'typescript', kind: 'orm', keyword: 'typeorm', value: 'typeorm', priority: 9 },
|
|
107
|
+
{ language: 'typescript', kind: 'orm', keyword: 'mongoose', value: 'mongoose', priority: 9 },
|
|
108
|
+
{ language: 'typescript', kind: 'orm', keyword: 'sequelize', value: 'sequelize', priority: 8 },
|
|
109
|
+
{ language: 'javascript', kind: 'orm', keyword: '@prisma/client', value: 'prisma', priority: 10 },
|
|
110
|
+
{ language: 'javascript', kind: 'orm', keyword: 'mongoose', value: 'mongoose', priority: 9 },
|
|
111
|
+
// TS/JS UI
|
|
112
|
+
{ language: 'typescript', kind: 'ui_library', keyword: 'next', value: 'next', priority: 9 },
|
|
113
|
+
{ language: 'typescript', kind: 'ui_library', keyword: 'react', value: 'react', priority: 8 },
|
|
114
|
+
{ language: 'typescript', kind: 'ui_library', keyword: 'vue', value: 'vue', priority: 8 },
|
|
115
|
+
{ language: 'typescript', kind: 'ui_library', keyword: '@sveltejs/kit', value: 'svelte', priority: 9 },
|
|
116
|
+
{ language: 'javascript', kind: 'ui_library', keyword: 'react', value: 'react', priority: 8 },
|
|
117
|
+
// TS/JS router
|
|
118
|
+
{ language: 'typescript', kind: 'router', keyword: '@trpc/server', value: 'trpc', priority: 10 },
|
|
119
|
+
{ language: 'typescript', kind: 'router', keyword: '@apollo/server', value: 'graphql', priority: 9 },
|
|
120
|
+
{ language: 'typescript', kind: 'router', keyword: 'graphql', value: 'graphql', priority: 8 },
|
|
121
|
+
{ language: 'typescript', kind: 'router', keyword: 'express', value: 'express', priority: 7 },
|
|
122
|
+
{ language: 'typescript', kind: 'router', keyword: 'fastify', value: 'fastify', priority: 7 },
|
|
123
|
+
{ language: 'typescript', kind: 'router', keyword: 'hono', value: 'hono', priority: 7 },
|
|
124
|
+
|
|
125
|
+
// Rust
|
|
126
|
+
{ language: 'rust', kind: 'framework', keyword: 'actix-web', value: 'actix-web', priority: 10 },
|
|
127
|
+
{ language: 'rust', kind: 'framework', keyword: 'axum', value: 'axum', priority: 10 },
|
|
128
|
+
{ language: 'rust', kind: 'framework', keyword: 'rocket', value: 'rocket', priority: 10 },
|
|
129
|
+
{ language: 'rust', kind: 'framework', keyword: 'warp', value: 'warp', priority: 9 },
|
|
130
|
+
{ language: 'rust', kind: 'framework', keyword: 'tokio', value: 'tokio', priority: 5 },
|
|
131
|
+
{ language: 'rust', kind: 'test_framework', keyword: 'cargo', value: 'cargo', priority: 1 },
|
|
132
|
+
{ language: 'rust', kind: 'orm', keyword: 'diesel', value: 'diesel', priority: 10 },
|
|
133
|
+
{ language: 'rust', kind: 'orm', keyword: 'sqlx', value: 'sqlx', priority: 10 },
|
|
134
|
+
{ language: 'rust', kind: 'orm', keyword: 'sea-orm', value: 'sea-orm', priority: 10 },
|
|
135
|
+
|
|
136
|
+
// Go
|
|
137
|
+
{ language: 'go', kind: 'framework', keyword: 'github.com/gin-gonic/gin', value: 'gin', priority: 10 },
|
|
138
|
+
{ language: 'go', kind: 'framework', keyword: 'github.com/labstack/echo', value: 'echo', priority: 10 },
|
|
139
|
+
{ language: 'go', kind: 'framework', keyword: 'github.com/gofiber/fiber', value: 'fiber', priority: 10 },
|
|
140
|
+
{ language: 'go', kind: 'framework', keyword: 'github.com/go-chi/chi', value: 'chi', priority: 9 },
|
|
141
|
+
{ language: 'go', kind: 'test_framework', keyword: 'github.com/stretchr/testify', value: 'testify', priority: 8 },
|
|
142
|
+
{ language: 'go', kind: 'orm', keyword: 'gorm.io/gorm', value: 'gorm', priority: 10 },
|
|
143
|
+
|
|
144
|
+
// Swift (SPM dependency names, best-effort)
|
|
145
|
+
{ language: 'swift', kind: 'framework', keyword: 'vapor', value: 'vapor', priority: 10 },
|
|
146
|
+
{ language: 'swift', kind: 'framework', keyword: 'swift-nio', value: 'swift-nio', priority: 7 },
|
|
147
|
+
{ language: 'swift', kind: 'test_framework', keyword: 'xctest', value: 'xctest', priority: 5 },
|
|
148
|
+
|
|
149
|
+
// Java
|
|
150
|
+
{ language: 'java', kind: 'framework', keyword: 'spring-boot-starter', value: 'spring-boot', priority: 10 },
|
|
151
|
+
{ language: 'java', kind: 'framework', keyword: 'spring-boot-starter-web', value: 'spring-boot', priority: 10 },
|
|
152
|
+
{ language: 'java', kind: 'test_framework', keyword: 'junit', value: 'junit', priority: 10 },
|
|
153
|
+
{ language: 'java', kind: 'test_framework', keyword: 'junit-jupiter', value: 'junit', priority: 10 },
|
|
154
|
+
|
|
155
|
+
// Ruby
|
|
156
|
+
{ language: 'ruby', kind: 'framework', keyword: 'rails', value: 'rails', priority: 10 },
|
|
157
|
+
{ language: 'ruby', kind: 'framework', keyword: 'sinatra', value: 'sinatra', priority: 9 },
|
|
158
|
+
{ language: 'ruby', kind: 'test_framework', keyword: 'rspec', value: 'rspec', priority: 10 },
|
|
159
|
+
{ language: 'ruby', kind: 'orm', keyword: 'activerecord', value: 'activerecord', priority: 10 },
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* User-supplied detection overrides, matching the P2-008 schema shape:
|
|
164
|
+
* detection.rules[language][framework] = { signals: string[], priority?: number }
|
|
165
|
+
*/
|
|
166
|
+
export interface UserDetectionRules {
|
|
167
|
+
rules?: Record<
|
|
168
|
+
string,
|
|
169
|
+
Record<string, { signals: string[]; priority?: number }>
|
|
170
|
+
>;
|
|
171
|
+
disable_builtin?: boolean;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Find the highest-priority rule for a given (language, kind) by scanning
|
|
176
|
+
* dep list case-insensitively.
|
|
177
|
+
*/
|
|
178
|
+
function matchRule(
|
|
179
|
+
rules: DetectionRule[],
|
|
180
|
+
language: SupportedLanguage,
|
|
181
|
+
kind: RuleKind,
|
|
182
|
+
deps: Set<string>
|
|
183
|
+
): { value: string; priority: number } | null {
|
|
184
|
+
let best: { value: string; priority: number } | null = null;
|
|
185
|
+
for (const r of rules) {
|
|
186
|
+
if (r.language !== language) continue;
|
|
187
|
+
if (r.kind !== kind) continue;
|
|
188
|
+
if (!deps.has(r.keyword.toLowerCase())) continue;
|
|
189
|
+
const pr = r.priority ?? 0;
|
|
190
|
+
if (!best || pr > best.priority) {
|
|
191
|
+
best = { value: r.value, priority: pr };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return best;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Match user-supplied framework rules (signals) against dep list.
|
|
199
|
+
* Each signal string is treated as a dep keyword (case-insensitive exact match
|
|
200
|
+
* on the dep name). If any signal matches, the framework is a candidate.
|
|
201
|
+
*/
|
|
202
|
+
function matchUserFrameworkRules(
|
|
203
|
+
userRules: UserDetectionRules['rules'] | undefined,
|
|
204
|
+
language: string,
|
|
205
|
+
deps: Set<string>
|
|
206
|
+
): { framework: string; priority: number } | null {
|
|
207
|
+
if (!userRules) return null;
|
|
208
|
+
const byLang = userRules[language];
|
|
209
|
+
if (!byLang) return null;
|
|
210
|
+
let best: { framework: string; priority: number } | null = null;
|
|
211
|
+
for (const [framework, entry] of Object.entries(byLang)) {
|
|
212
|
+
const signals = entry.signals ?? [];
|
|
213
|
+
const priority = entry.priority ?? 100; // user rules outrank built-ins by default
|
|
214
|
+
for (const sig of signals) {
|
|
215
|
+
if (deps.has(sig.toLowerCase())) {
|
|
216
|
+
if (!best || priority > best.priority) {
|
|
217
|
+
best = { framework, priority };
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return best;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Detect frameworks/test-frameworks/ORMs per language.
|
|
228
|
+
*
|
|
229
|
+
* @param manifests - output of P1-001 detectPackageManifests
|
|
230
|
+
* @param userDetection - optional massu.config.yaml `detection` block
|
|
231
|
+
*/
|
|
232
|
+
export function detectFrameworks(
|
|
233
|
+
manifests: PackageManifest[],
|
|
234
|
+
userDetection?: UserDetectionRules
|
|
235
|
+
): FrameworkMap {
|
|
236
|
+
// Merge deps across all manifests of the same language (monorepo aggregate).
|
|
237
|
+
const byLang = new Map<SupportedLanguage, { deps: Set<string>; versionOf: Map<string, string> }>();
|
|
238
|
+
for (const m of manifests) {
|
|
239
|
+
const entry = byLang.get(m.language) ?? {
|
|
240
|
+
deps: new Set<string>(),
|
|
241
|
+
versionOf: new Map<string, string>(),
|
|
242
|
+
};
|
|
243
|
+
for (const d of m.dependencies) entry.deps.add(d.toLowerCase());
|
|
244
|
+
for (const d of m.devDependencies) entry.deps.add(d.toLowerCase());
|
|
245
|
+
byLang.set(m.language, entry);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const rules: DetectionRule[] = userDetection?.disable_builtin
|
|
249
|
+
? []
|
|
250
|
+
: [...DETECTION_RULES];
|
|
251
|
+
|
|
252
|
+
const out: FrameworkMap = {};
|
|
253
|
+
for (const [language, { deps }] of byLang.entries()) {
|
|
254
|
+
const fw = matchRule(rules, language, 'framework', deps);
|
|
255
|
+
const userFw = matchUserFrameworkRules(
|
|
256
|
+
userDetection?.rules,
|
|
257
|
+
language,
|
|
258
|
+
deps
|
|
259
|
+
);
|
|
260
|
+
// Pick user rule when it has higher effective priority.
|
|
261
|
+
let frameworkValue: string | null = null;
|
|
262
|
+
if (userFw && (!fw || userFw.priority > fw.priority)) {
|
|
263
|
+
frameworkValue = userFw.framework;
|
|
264
|
+
} else if (fw) {
|
|
265
|
+
frameworkValue = fw.value;
|
|
266
|
+
}
|
|
267
|
+
const info: FrameworkInfo = {
|
|
268
|
+
framework: frameworkValue,
|
|
269
|
+
version: null,
|
|
270
|
+
test_framework:
|
|
271
|
+
matchRule(rules, language, 'test_framework', deps)?.value ?? null,
|
|
272
|
+
orm: matchRule(rules, language, 'orm', deps)?.value ?? null,
|
|
273
|
+
ui_library:
|
|
274
|
+
matchRule(rules, language, 'ui_library', deps)?.value ?? null,
|
|
275
|
+
router: matchRule(rules, language, 'router', deps)?.value ?? null,
|
|
276
|
+
};
|
|
277
|
+
out[language] = info;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Detection Orchestrator (P1-007)
|
|
6
|
+
* ===============================
|
|
7
|
+
*
|
|
8
|
+
* Single entry point: `runDetection(projectRoot, configOverrides?)`.
|
|
9
|
+
*
|
|
10
|
+
* Composes P1-001..P1-006 results into a fully-typed `DetectionResult`.
|
|
11
|
+
* All callers (massu init, config refresh, doctor, drift) should import
|
|
12
|
+
* from here — no direct imports of individual detectors into init/CLI code.
|
|
13
|
+
*
|
|
14
|
+
* Filesystem-only. No DB handles. No network. No child processes.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { runDetection } from './detect/index.ts';
|
|
19
|
+
* const result = await runDetection('/repo');
|
|
20
|
+
* result.monorepo.type // 'turbo' | 'pnpm' | 'single' | ...
|
|
21
|
+
* result.frameworks.python // { framework: 'fastapi', test_framework: 'pytest', ... }
|
|
22
|
+
* result.verificationCommands.python.test // 'cd apps/ai-service && python3 -m pytest -q'
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { DomainConfig } from '../config.ts';
|
|
27
|
+
import {
|
|
28
|
+
detectPackageManifests,
|
|
29
|
+
type PackageManifest,
|
|
30
|
+
type PackageDetectionResult,
|
|
31
|
+
type SupportedLanguage,
|
|
32
|
+
type DetectionWarning,
|
|
33
|
+
} from './package-detector.ts';
|
|
34
|
+
import {
|
|
35
|
+
detectFrameworks,
|
|
36
|
+
type FrameworkMap,
|
|
37
|
+
type FrameworkInfo,
|
|
38
|
+
type UserDetectionRules,
|
|
39
|
+
} from './framework-detector.ts';
|
|
40
|
+
import {
|
|
41
|
+
detectSourceDirs,
|
|
42
|
+
type SourceDirMap,
|
|
43
|
+
type SourceDirInfo,
|
|
44
|
+
} from './source-dir-detector.ts';
|
|
45
|
+
import {
|
|
46
|
+
detectMonorepo,
|
|
47
|
+
type MonorepoInfo,
|
|
48
|
+
type MonorepoKind,
|
|
49
|
+
type WorkspacePackage,
|
|
50
|
+
} from './monorepo-detector.ts';
|
|
51
|
+
import {
|
|
52
|
+
getVRCommands,
|
|
53
|
+
type VRCommandSet,
|
|
54
|
+
type UserVerificationEntry,
|
|
55
|
+
} from './vr-command-map.ts';
|
|
56
|
+
import { inferDomains } from './domain-inferrer.ts';
|
|
57
|
+
|
|
58
|
+
export type {
|
|
59
|
+
PackageManifest,
|
|
60
|
+
PackageDetectionResult,
|
|
61
|
+
SupportedLanguage,
|
|
62
|
+
DetectionWarning,
|
|
63
|
+
FrameworkMap,
|
|
64
|
+
FrameworkInfo,
|
|
65
|
+
UserDetectionRules,
|
|
66
|
+
SourceDirMap,
|
|
67
|
+
SourceDirInfo,
|
|
68
|
+
MonorepoInfo,
|
|
69
|
+
MonorepoKind,
|
|
70
|
+
WorkspacePackage,
|
|
71
|
+
VRCommandSet,
|
|
72
|
+
UserVerificationEntry,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Shape a caller passes in to plug user overrides. */
|
|
76
|
+
export interface DetectionConfigOverrides {
|
|
77
|
+
/** `config.detection` — user additions to the framework detection rule table. */
|
|
78
|
+
detection?: UserDetectionRules;
|
|
79
|
+
/** `config.verification` — per-language VR-* command overrides. */
|
|
80
|
+
verification?: Record<string, UserVerificationEntry>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface DetectionResult {
|
|
84
|
+
/** Absolute project root that was scanned. */
|
|
85
|
+
projectRoot: string;
|
|
86
|
+
/** All package manifests discovered (P1-001). */
|
|
87
|
+
manifests: PackageManifest[];
|
|
88
|
+
/** Inferred framework/test/ORM/router/ui per language (P1-002). */
|
|
89
|
+
frameworks: FrameworkMap;
|
|
90
|
+
/** Source + test directories per language (P1-003). */
|
|
91
|
+
sourceDirs: SourceDirMap;
|
|
92
|
+
/** Monorepo layout info (P1-004). */
|
|
93
|
+
monorepo: MonorepoInfo;
|
|
94
|
+
/** Suggested domains (P1-006). */
|
|
95
|
+
domains: DomainConfig[];
|
|
96
|
+
/** VR-* commands per language (P1-005). */
|
|
97
|
+
verificationCommands: Partial<Record<SupportedLanguage, VRCommandSet>>;
|
|
98
|
+
/** Non-fatal warnings collected across all detectors. */
|
|
99
|
+
warnings: DetectionWarning[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function dominantDir(
|
|
103
|
+
lang: SupportedLanguage,
|
|
104
|
+
sourceDirs: SourceDirMap,
|
|
105
|
+
monorepo: MonorepoInfo
|
|
106
|
+
): string {
|
|
107
|
+
const info = sourceDirs[lang];
|
|
108
|
+
if (info && info.source_dirs.length > 0) return info.source_dirs[0];
|
|
109
|
+
// Fall back to the first monorepo workspace path if present.
|
|
110
|
+
if (monorepo.packages.length > 0) return monorepo.packages[0].path;
|
|
111
|
+
return '.';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Run all detectors in order and compose a `DetectionResult`.
|
|
116
|
+
*
|
|
117
|
+
* Order (per plan):
|
|
118
|
+
* 1. packages
|
|
119
|
+
* 2. frameworks (depends on packages)
|
|
120
|
+
* 3. sourceDirs + monorepo (parallel, independent)
|
|
121
|
+
* 4. domains + verificationCommands (depend on 1-3)
|
|
122
|
+
*/
|
|
123
|
+
export async function runDetection(
|
|
124
|
+
projectRoot: string,
|
|
125
|
+
overrides?: DetectionConfigOverrides
|
|
126
|
+
): Promise<DetectionResult> {
|
|
127
|
+
// 1. packages
|
|
128
|
+
const pkg = detectPackageManifests(projectRoot);
|
|
129
|
+
|
|
130
|
+
// 2. frameworks (depends on packages)
|
|
131
|
+
const frameworks = detectFrameworks(pkg.manifests, overrides?.detection);
|
|
132
|
+
|
|
133
|
+
// 3a. sourceDirs (depends on the language list discovered in packages)
|
|
134
|
+
const languages = Array.from(
|
|
135
|
+
new Set(pkg.manifests.map((m) => m.language))
|
|
136
|
+
) as SupportedLanguage[];
|
|
137
|
+
|
|
138
|
+
// 3b. run source-dir + monorepo detection in parallel (both pure fs).
|
|
139
|
+
const [sourceDirs, monorepo] = await Promise.all([
|
|
140
|
+
Promise.resolve(detectSourceDirs(projectRoot, languages)),
|
|
141
|
+
Promise.resolve(detectMonorepo(projectRoot)),
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
// 4a. domains
|
|
145
|
+
const domains = inferDomains(projectRoot, monorepo, sourceDirs);
|
|
146
|
+
|
|
147
|
+
// 4b. VR commands per language
|
|
148
|
+
const verificationCommands: Partial<Record<SupportedLanguage, VRCommandSet>> =
|
|
149
|
+
{};
|
|
150
|
+
for (const lang of languages) {
|
|
151
|
+
const fw: FrameworkInfo = frameworks[lang] ?? {
|
|
152
|
+
framework: null,
|
|
153
|
+
version: null,
|
|
154
|
+
test_framework: null,
|
|
155
|
+
orm: null,
|
|
156
|
+
ui_library: null,
|
|
157
|
+
router: null,
|
|
158
|
+
};
|
|
159
|
+
const dir = dominantDir(lang, sourceDirs, monorepo);
|
|
160
|
+
const userOverride = overrides?.verification?.[lang];
|
|
161
|
+
verificationCommands[lang] = getVRCommands(lang, fw, dir, userOverride);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
projectRoot,
|
|
166
|
+
manifests: pkg.manifests,
|
|
167
|
+
frameworks,
|
|
168
|
+
sourceDirs,
|
|
169
|
+
monorepo,
|
|
170
|
+
domains,
|
|
171
|
+
verificationCommands,
|
|
172
|
+
warnings: pkg.warnings,
|
|
173
|
+
};
|
|
174
|
+
}
|