@massu/core 1.10.8 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/CHANGELOG.md +1303 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +517 -397
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +66 -18
  16. package/dist/hooks/pre-compact.js +27 -2
  17. package/dist/hooks/pre-delete-check.js +48 -22
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +24 -0
  23. package/dist/hooks/session-start.js +8710 -8460
  24. package/dist/hooks/user-prompt.js +24 -0
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/framework-supports.ts +0 -85
  110. package/src/lib/gitToplevel.ts +0 -22
  111. package/src/lib/hook-registry.ts +0 -43
  112. package/src/lib/installLock.ts +0 -66
  113. package/src/lib/memory-path.ts +0 -49
  114. package/src/lib/pidLiveness.ts +0 -67
  115. package/src/lib/settings-local.ts +0 -110
  116. package/src/lib/sql-table-names.ts +0 -71
  117. package/src/license.ts +0 -547
  118. package/src/lsp/auto-detect.ts +0 -98
  119. package/src/lsp/client.ts +0 -776
  120. package/src/lsp/enrich.ts +0 -127
  121. package/src/lsp/types.ts +0 -221
  122. package/src/memory-db.ts +0 -1614
  123. package/src/memory-file-ingest.ts +0 -128
  124. package/src/memory-tools.ts +0 -424
  125. package/src/middleware-tree.ts +0 -71
  126. package/src/observability-tools.ts +0 -343
  127. package/src/observation-extractor.ts +0 -418
  128. package/src/page-deps.ts +0 -285
  129. package/src/permissions.ts +0 -422
  130. package/src/prompt-analyzer.ts +0 -332
  131. package/src/python/coupling-detector.ts +0 -125
  132. package/src/python/domain-enforcer.ts +0 -84
  133. package/src/python/impact-analyzer.ts +0 -96
  134. package/src/python/import-parser.ts +0 -244
  135. package/src/python/import-resolver.ts +0 -136
  136. package/src/python/migration-indexer.ts +0 -116
  137. package/src/python/migration-parser.ts +0 -332
  138. package/src/python/model-indexer.ts +0 -71
  139. package/src/python/model-parser.ts +0 -279
  140. package/src/python/route-indexer.ts +0 -59
  141. package/src/python/route-parser.ts +0 -317
  142. package/src/python-tools.ts +0 -630
  143. package/src/regression-detector.ts +0 -319
  144. package/src/rules.ts +0 -57
  145. package/src/schema-mapper.ts +0 -232
  146. package/src/security/adapter-origin.ts +0 -130
  147. package/src/security/adapter-verifier.ts +0 -319
  148. package/src/security/atomic-write.ts +0 -164
  149. package/src/security/fetcher.ts +0 -200
  150. package/src/security/install-tracking.ts +0 -319
  151. package/src/security/license-pubkey.generated.ts +0 -30
  152. package/src/security/license-response-verifier.ts +0 -149
  153. package/src/security/local-fingerprint.ts +0 -225
  154. package/src/security/manifest-cache.ts +0 -333
  155. package/src/security/manifest-schema.ts +0 -129
  156. package/src/security/registry-pubkey.generated.ts +0 -35
  157. package/src/security/telemetry.ts +0 -320
  158. package/src/security-scorer.ts +0 -405
  159. package/src/security-utils.ts +0 -133
  160. package/src/sentinel-db.ts +0 -580
  161. package/src/sentinel-scanner.ts +0 -406
  162. package/src/sentinel-tools.ts +0 -513
  163. package/src/sentinel-types.ts +0 -140
  164. package/src/server-dispatch.ts +0 -225
  165. package/src/server.ts +0 -107
  166. package/src/session-archiver.ts +0 -111
  167. package/src/session-state-generator.ts +0 -174
  168. package/src/team-knowledge.ts +0 -407
  169. package/src/tool-db-needs.ts +0 -232
  170. package/src/tools.ts +0 -1206
  171. package/src/transcript-parser.ts +0 -458
  172. package/src/trpc-index.ts +0 -215
  173. package/src/validate-features-runner.ts +0 -107
  174. package/src/validation-engine.ts +0 -358
  175. package/src/watch/daemon.ts +0 -385
  176. package/src/watch/lockfile-detector.ts +0 -65
  177. package/src/watch/paths.ts +0 -279
  178. package/src/watch/state.ts +0 -178
@@ -1,199 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- /**
5
- * Drift detection logic (P7-004).
6
- *
7
- * Phase 5 runtime (drift runner / auto-rerun) is NOT in the MVP cut, but the
8
- * pure detection LOGIC is tested here so the runtime layer lands without bugs.
9
- *
10
- * Two primary entry points:
11
- *
12
- * 1. `computeFingerprint(detectionResult)` — deterministic SHA-256 of the
13
- * normalized JSON form of (languages, frameworks, source_dirs, manifest
14
- * paths sorted). Any meaningful change in the detected stack changes the
15
- * fingerprint.
16
- *
17
- * 2. `detectDrift(currentConfig, actualDetection)` — returns
18
- * `{ drifted: boolean, changes: Array<{field, before, after}> }`.
19
- *
20
- * No filesystem / network / child_process.
21
- */
22
-
23
- import { createHash } from 'crypto';
24
- import type { AnyConfig } from './migrate.ts';
25
- import type { DetectionResult, SupportedLanguage } from './index.ts';
26
-
27
- export interface DriftChange {
28
- field: string;
29
- before: unknown;
30
- after: unknown;
31
- }
32
-
33
- export interface DriftReport {
34
- drifted: boolean;
35
- changes: DriftChange[];
36
- }
37
-
38
- /** Stable-sorted, normalized summary of a DetectionResult used for hashing. */
39
- interface DetectionFingerprintData {
40
- languages: string[];
41
- frameworks: Record<string, { framework: string | null; test_framework: string | null; orm: string | null }>;
42
- source_dirs: Record<string, string[]>;
43
- manifests: string[];
44
- monorepo: string;
45
- workspaces: string[];
46
- }
47
-
48
- function summarizeDetection(det: DetectionResult): DetectionFingerprintData {
49
- const languages = Array.from(new Set(det.manifests.map((m) => m.language))).sort();
50
- const frameworks: DetectionFingerprintData['frameworks'] = {};
51
- for (const lang of languages) {
52
- const fw = det.frameworks[lang as SupportedLanguage];
53
- frameworks[lang] = {
54
- framework: fw?.framework ?? null,
55
- test_framework: fw?.test_framework ?? null,
56
- orm: fw?.orm ?? null,
57
- };
58
- }
59
- const sourceDirs: Record<string, string[]> = {};
60
- for (const lang of languages) {
61
- const info = det.sourceDirs[lang as SupportedLanguage];
62
- sourceDirs[lang] = [...(info?.source_dirs ?? [])].sort();
63
- }
64
- const manifests = [...det.manifests.map((m) => m.relativePath)].sort();
65
- const workspaces = [...det.monorepo.packages.map((p) => p.path)].sort();
66
- return {
67
- languages,
68
- frameworks,
69
- source_dirs: sourceDirs,
70
- manifests,
71
- monorepo: det.monorepo.type,
72
- workspaces,
73
- };
74
- }
75
-
76
- /** Deterministic SHA-256 of a detection result. Stable across runs for the
77
- * same inputs. */
78
- export function computeFingerprint(det: DetectionResult): string {
79
- const data = summarizeDetection(det);
80
- const stable = JSON.stringify(data, Object.keys(data).sort());
81
- return createHash('sha256').update(stable).digest('hex');
82
- }
83
-
84
- function stringOf(v: unknown): string | null {
85
- if (typeof v === 'string') return v;
86
- if (v === null || v === undefined) return null;
87
- return String(v);
88
- }
89
-
90
- /**
91
- * Compute drift between a currently-loaded config and a freshly-run detection.
92
- *
93
- * A non-empty `changes[]` means fields in the config no longer agree with
94
- * what the repository actually contains. Callers decide whether to auto-repair
95
- * (via `migrateV1ToV2`) or surface to the user.
96
- */
97
- export function detectDrift(
98
- currentConfig: AnyConfig,
99
- actualDetection: DetectionResult
100
- ): DriftReport {
101
- const changes: DriftChange[] = [];
102
-
103
- const configFw = (currentConfig.framework && typeof currentConfig.framework === 'object')
104
- ? (currentConfig.framework as Record<string, unknown>)
105
- : {};
106
- const configLanguages = (configFw.languages && typeof configFw.languages === 'object')
107
- ? (configFw.languages as Record<string, Record<string, unknown>>)
108
- : {};
109
-
110
- const detectedLanguages = Array.from(
111
- new Set(actualDetection.manifests.map((m) => m.language))
112
- ) as SupportedLanguage[];
113
-
114
- const configLangKeys = Object.keys(configLanguages).sort();
115
- const detectedLangKeys = [...detectedLanguages].sort();
116
-
117
- // 1. Language set drift.
118
- if (JSON.stringify(configLangKeys) !== JSON.stringify(detectedLangKeys)) {
119
- changes.push({
120
- field: 'framework.languages',
121
- before: configLangKeys,
122
- after: detectedLangKeys,
123
- });
124
- }
125
-
126
- // 2. Per-language framework/test_framework drift.
127
- for (const lang of detectedLanguages) {
128
- const detFw = actualDetection.frameworks[lang];
129
- const cfgEntry = configLanguages[lang];
130
- if (!cfgEntry) continue;
131
- const cfgFramework = stringOf(cfgEntry.framework);
132
- const detFramework = detFw?.framework ?? null;
133
- if (cfgFramework !== detFramework && detFramework !== null) {
134
- changes.push({
135
- field: `framework.languages.${lang}.framework`,
136
- before: cfgFramework,
137
- after: detFramework,
138
- });
139
- }
140
- const cfgTest = stringOf(cfgEntry.test_framework);
141
- const detTest = detFw?.test_framework ?? null;
142
- if (cfgTest !== detTest && detTest !== null) {
143
- changes.push({
144
- field: `framework.languages.${lang}.test_framework`,
145
- before: cfgTest,
146
- after: detTest,
147
- });
148
- }
149
- }
150
-
151
- // 3. Manifest set drift: new/removed manifest files.
152
- const detectedManifestPaths = new Set(actualDetection.manifests.map((m) => m.relativePath));
153
- const declaredManifestPaths = new Set<string>();
154
- // v2 configs don't record manifest paths directly; we look at
155
- // `canonical_paths.manifest_paths` (string comma-separated) OR a `manifests`
156
- // top-level array. When neither is present we skip this check.
157
- const canonical = currentConfig.canonical_paths as Record<string, unknown> | undefined;
158
- if (canonical && typeof canonical.manifest_paths === 'string') {
159
- for (const p of (canonical.manifest_paths as string).split(',').map((s) => s.trim())) {
160
- if (p) declaredManifestPaths.add(p);
161
- }
162
- }
163
- if (Array.isArray(currentConfig.manifests)) {
164
- for (const p of currentConfig.manifests as unknown[]) {
165
- if (typeof p === 'string') declaredManifestPaths.add(p);
166
- }
167
- }
168
- if (declaredManifestPaths.size > 0) {
169
- const added = [...detectedManifestPaths].filter((p) => !declaredManifestPaths.has(p)).sort();
170
- const removed = [...declaredManifestPaths].filter((p) => !detectedManifestPaths.has(p)).sort();
171
- if (added.length > 0) {
172
- changes.push({ field: 'manifests.added', before: [], after: added });
173
- }
174
- if (removed.length > 0) {
175
- changes.push({ field: 'manifests.removed', before: removed, after: [] });
176
- }
177
- }
178
-
179
- // 4. Monorepo workspace set drift.
180
- const configWorkspaces: string[] = [];
181
- if (Array.isArray((currentConfig.monorepo as Record<string, unknown> | undefined)?.workspaces)) {
182
- for (const w of ((currentConfig.monorepo as Record<string, unknown>).workspaces as unknown[])) {
183
- if (typeof w === 'string') configWorkspaces.push(w);
184
- }
185
- }
186
- const detectedWorkspaces = actualDetection.monorepo.packages.map((p) => p.path).sort();
187
- if (configWorkspaces.length > 0) {
188
- const cfgSorted = [...configWorkspaces].sort();
189
- if (JSON.stringify(cfgSorted) !== JSON.stringify(detectedWorkspaces)) {
190
- changes.push({
191
- field: 'monorepo.workspaces',
192
- before: cfgSorted,
193
- after: detectedWorkspaces,
194
- });
195
- }
196
- }
197
-
198
- return { drifted: changes.length > 0, changes };
199
- }
@@ -1,307 +0,0 @@
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
- // chi versioned import paths (Go convention: github.com/<org>/<name>/v<N>).
142
- // matchRule does exact case-insensitive set lookup, so the unversioned and
143
- // each major-version path each need their own rule.
144
- { language: 'go', kind: 'framework', keyword: 'github.com/go-chi/chi/v2', value: 'chi', priority: 9 },
145
- { language: 'go', kind: 'framework', keyword: 'github.com/go-chi/chi/v3', value: 'chi', priority: 9 },
146
- { language: 'go', kind: 'framework', keyword: 'github.com/go-chi/chi/v4', value: 'chi', priority: 9 },
147
- { language: 'go', kind: 'framework', keyword: 'github.com/go-chi/chi/v5', value: 'chi', priority: 9 },
148
- { language: 'go', kind: 'test_framework', keyword: 'github.com/stretchr/testify', value: 'testify', priority: 8 },
149
- { language: 'go', kind: 'orm', keyword: 'gorm.io/gorm', value: 'gorm', priority: 10 },
150
-
151
- // Swift (SPM dependency names, best-effort)
152
- { language: 'swift', kind: 'framework', keyword: 'vapor', value: 'vapor', priority: 10 },
153
- { language: 'swift', kind: 'framework', keyword: 'swift-nio', value: 'swift-nio', priority: 7 },
154
- { language: 'swift', kind: 'test_framework', keyword: 'xctest', value: 'xctest', priority: 5 },
155
-
156
- // Java
157
- { language: 'java', kind: 'framework', keyword: 'spring-boot-starter', value: 'spring-boot', priority: 10 },
158
- { language: 'java', kind: 'framework', keyword: 'spring-boot-starter-web', value: 'spring-boot', priority: 10 },
159
- { language: 'java', kind: 'test_framework', keyword: 'junit', value: 'junit', priority: 10 },
160
- { language: 'java', kind: 'test_framework', keyword: 'junit-jupiter', value: 'junit', priority: 10 },
161
-
162
- // Ruby
163
- { language: 'ruby', kind: 'framework', keyword: 'rails', value: 'rails', priority: 10 },
164
- { language: 'ruby', kind: 'framework', keyword: 'sinatra', value: 'sinatra', priority: 9 },
165
- { language: 'ruby', kind: 'test_framework', keyword: 'rspec', value: 'rspec', priority: 10 },
166
- { language: 'ruby', kind: 'orm', keyword: 'activerecord', value: 'activerecord', priority: 10 },
167
- // Plan 1.5.1: elixir + csharp framework rules. Closes the CR-39 gap
168
- // where Phoenix + ASP.NET projects produced `framework.languages.<lang>`
169
- // entries WITHOUT a `framework:` value, which prevented variant
170
- // templates from being looked up.
171
- { language: 'elixir', kind: 'framework', keyword: 'phoenix', value: 'phoenix', priority: 10 },
172
- { language: 'elixir', kind: 'test_framework', keyword: 'ex_unit', value: 'ex-unit', priority: 10 },
173
- { language: 'elixir', kind: 'orm', keyword: 'ecto', value: 'ecto', priority: 10 },
174
- // ASP.NET Core surfaces via several PackageReference names; the canonical
175
- // ones in modern .NET projects are .App and .Mvc. matchRule does exact
176
- // (case-insensitive) lookup against the deps set parseCsproj extracts.
177
- { language: 'csharp', kind: 'framework', keyword: 'Microsoft.AspNetCore.App', value: 'aspnet-core', priority: 10 },
178
- { language: 'csharp', kind: 'framework', keyword: 'Microsoft.AspNetCore.Mvc', value: 'aspnet-core', priority: 10 },
179
- { language: 'csharp', kind: 'framework', keyword: 'Microsoft.AspNetCore', value: 'aspnet-core', priority: 9 },
180
- // SDK-style projects: `<Project Sdk="Microsoft.NET.Sdk.Web">` is the
181
- // canonical ASP.NET Core declaration in modern .NET. parseCsproj
182
- // surfaces the Sdk attribute as a dep so this rule can match.
183
- { language: 'csharp', kind: 'framework', keyword: 'Microsoft.NET.Sdk.Web', value: 'aspnet-core', priority: 10 },
184
- { language: 'csharp', kind: 'test_framework', keyword: 'xunit', value: 'xunit', priority: 10 },
185
- { language: 'csharp', kind: 'orm', keyword: 'EntityFrameworkCore', value: 'ef-core', priority: 10 },
186
- ];
187
-
188
- /**
189
- * User-supplied detection overrides, matching the P2-008 schema shape:
190
- * detection.rules[language][framework] = { signals: string[], priority?: number }
191
- */
192
- export interface UserDetectionRules {
193
- rules?: Record<
194
- string,
195
- Record<string, { signals: string[]; priority?: number }>
196
- >;
197
- disable_builtin?: boolean;
198
- }
199
-
200
- /**
201
- * Find the highest-priority rule for a given (language, kind) by scanning
202
- * dep list case-insensitively.
203
- */
204
- function matchRule(
205
- rules: DetectionRule[],
206
- language: SupportedLanguage,
207
- kind: RuleKind,
208
- deps: Set<string>
209
- ): { value: string; priority: number } | null {
210
- let best: { value: string; priority: number } | null = null;
211
- for (const r of rules) {
212
- if (r.language !== language) continue;
213
- if (r.kind !== kind) continue;
214
- if (!deps.has(r.keyword.toLowerCase())) continue;
215
- const pr = r.priority ?? 0;
216
- if (!best || pr > best.priority) {
217
- best = { value: r.value, priority: pr };
218
- }
219
- }
220
- return best;
221
- }
222
-
223
- /**
224
- * Match user-supplied framework rules (signals) against dep list.
225
- * Each signal string is treated as a dep keyword (case-insensitive exact match
226
- * on the dep name). If any signal matches, the framework is a candidate.
227
- */
228
- function matchUserFrameworkRules(
229
- userRules: UserDetectionRules['rules'] | undefined,
230
- language: string,
231
- deps: Set<string>
232
- ): { framework: string; priority: number } | null {
233
- if (!userRules) return null;
234
- const byLang = userRules[language];
235
- if (!byLang) return null;
236
- let best: { framework: string; priority: number } | null = null;
237
- for (const [framework, entry] of Object.entries(byLang)) {
238
- const signals = entry.signals ?? [];
239
- const priority = entry.priority ?? 100; // user rules outrank built-ins by default
240
- for (const sig of signals) {
241
- if (deps.has(sig.toLowerCase())) {
242
- if (!best || priority > best.priority) {
243
- best = { framework, priority };
244
- }
245
- break;
246
- }
247
- }
248
- }
249
- return best;
250
- }
251
-
252
- /**
253
- * Detect frameworks/test-frameworks/ORMs per language.
254
- *
255
- * @param manifests - output of P1-001 detectPackageManifests
256
- * @param userDetection - optional massu.config.yaml `detection` block
257
- */
258
- export function detectFrameworks(
259
- manifests: PackageManifest[],
260
- userDetection?: UserDetectionRules
261
- ): FrameworkMap {
262
- // Merge deps across all manifests of the same language (monorepo aggregate).
263
- const byLang = new Map<SupportedLanguage, { deps: Set<string>; versionOf: Map<string, string> }>();
264
- for (const m of manifests) {
265
- const entry = byLang.get(m.language) ?? {
266
- deps: new Set<string>(),
267
- versionOf: new Map<string, string>(),
268
- };
269
- for (const d of m.dependencies) entry.deps.add(d.toLowerCase());
270
- for (const d of m.devDependencies) entry.deps.add(d.toLowerCase());
271
- byLang.set(m.language, entry);
272
- }
273
-
274
- const rules: DetectionRule[] = userDetection?.disable_builtin
275
- ? []
276
- : [...DETECTION_RULES];
277
-
278
- const out: FrameworkMap = {};
279
- for (const [language, { deps }] of byLang.entries()) {
280
- const fw = matchRule(rules, language, 'framework', deps);
281
- const userFw = matchUserFrameworkRules(
282
- userDetection?.rules,
283
- language,
284
- deps
285
- );
286
- // Pick user rule when it has higher effective priority.
287
- let frameworkValue: string | null = null;
288
- if (userFw && (!fw || userFw.priority > fw.priority)) {
289
- frameworkValue = userFw.framework;
290
- } else if (fw) {
291
- frameworkValue = fw.value;
292
- }
293
- const info: FrameworkInfo = {
294
- framework: frameworkValue,
295
- version: null,
296
- test_framework:
297
- matchRule(rules, language, 'test_framework', deps)?.value ?? null,
298
- orm: matchRule(rules, language, 'orm', deps)?.value ?? null,
299
- ui_library:
300
- matchRule(rules, language, 'ui_library', deps)?.value ?? null,
301
- router: matchRule(rules, language, 'router', deps)?.value ?? null,
302
- };
303
- out[language] = info;
304
- }
305
-
306
- return out;
307
- }