@massu/core 1.3.0 → 1.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.
Files changed (57) hide show
  1. package/commands/README.md +23 -11
  2. package/commands/massu-deploy.python-docker.md +170 -0
  3. package/commands/massu-deploy.python-fly.md +189 -0
  4. package/commands/massu-deploy.python-launchd.md +144 -0
  5. package/commands/massu-deploy.python-systemd.md +163 -0
  6. package/commands/massu-scaffold-page.swift.md +10 -10
  7. package/commands/massu-scaffold-router.python-django.md +153 -0
  8. package/commands/massu-scaffold-router.python-fastapi.md +145 -0
  9. package/dist/cli.js +9914 -4133
  10. package/dist/hooks/auto-learning-pipeline.js +45 -2
  11. package/dist/hooks/classify-failure.js +45 -2
  12. package/dist/hooks/cost-tracker.js +45 -2
  13. package/dist/hooks/fix-detector.js +45 -2
  14. package/dist/hooks/incident-pipeline.js +45 -2
  15. package/dist/hooks/post-edit-context.js +45 -2
  16. package/dist/hooks/post-tool-use.js +45 -2
  17. package/dist/hooks/pre-compact.js +45 -2
  18. package/dist/hooks/pre-delete-check.js +45 -2
  19. package/dist/hooks/quality-event.js +45 -2
  20. package/dist/hooks/rule-enforcement-pipeline.js +45 -2
  21. package/dist/hooks/session-end.js +45 -2
  22. package/dist/hooks/session-start.js +4790 -406
  23. package/dist/hooks/user-prompt.js +45 -2
  24. package/package.json +13 -4
  25. package/src/cli.ts +22 -2
  26. package/src/commands/config-refresh.ts +91 -23
  27. package/src/commands/init.ts +131 -24
  28. package/src/commands/install-commands.ts +142 -26
  29. package/src/commands/refresh-log.ts +37 -0
  30. package/src/commands/template-engine.ts +260 -0
  31. package/src/commands/watch.ts +430 -0
  32. package/src/config.ts +71 -0
  33. package/src/detect/adapters/nextjs-trpc.ts +166 -0
  34. package/src/detect/adapters/parse-guard.ts +133 -0
  35. package/src/detect/adapters/python-django.ts +208 -0
  36. package/src/detect/adapters/python-fastapi.ts +223 -0
  37. package/src/detect/adapters/query-helpers.ts +170 -0
  38. package/src/detect/adapters/runner.ts +252 -0
  39. package/src/detect/adapters/swift-swiftui.ts +171 -0
  40. package/src/detect/adapters/tree-sitter-loader.ts +467 -0
  41. package/src/detect/adapters/types.ts +173 -0
  42. package/src/detect/codebase-introspector.ts +190 -0
  43. package/src/detect/index.ts +28 -2
  44. package/src/detect/migrate.ts +4 -4
  45. package/src/detect/regex-fallback.ts +449 -0
  46. package/src/hooks/session-start.ts +94 -3
  47. package/src/lib/gitToplevel.ts +22 -0
  48. package/src/lib/installLock.ts +179 -0
  49. package/src/lib/pidLiveness.ts +67 -0
  50. package/src/lsp/auto-detect.ts +98 -0
  51. package/src/lsp/client.ts +776 -0
  52. package/src/lsp/enrich.ts +127 -0
  53. package/src/lsp/types.ts +221 -0
  54. package/src/watch/daemon.ts +385 -0
  55. package/src/watch/lockfile-detector.ts +65 -0
  56. package/src/watch/paths.ts +279 -0
  57. package/src/watch/state.ts +178 -0
@@ -0,0 +1,190 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ /**
5
+ * Codebase Introspector — 2-tier dispatcher (Plan 3b Phase 1)
6
+ * ============================================================
7
+ *
8
+ * Public signature: `introspect(detection, projectRoot): DetectedConventions`.
9
+ * Byte-for-byte unchanged from Plan #2 — `detect/index.ts` calls this
10
+ * function as before.
11
+ *
12
+ * Internal change (Plan 3b Phase 1): the function is now a 2-tier dispatcher.
13
+ *
14
+ * Tier 1 — AST adapters (preferred). Run via `runner.ts` against the four
15
+ * first-party adapters: python-fastapi, python-django, nextjs-trpc,
16
+ * swift-swiftui. Each writes to its own `detected.<adapter.id>` block
17
+ * alongside the regex blocks. AST adapters use Tree-sitter S-expression
18
+ * queries — never regex. Confidence is per-field.
19
+ *
20
+ * Tier 2 — Regex fallback. For fields the AST adapters returned 'none'
21
+ * confidence on, the regex helpers in `regex-fallback.ts` (verbatim moved
22
+ * from this file's previous incarnation) take over. AST-wins rule: when
23
+ * both tiers produce a value for the same `detected.<lang>.<field>` slot,
24
+ * AST wins; the runner records both in provenance.
25
+ *
26
+ * Tier 3 — null. The template engine's `| default("...")` then takes over.
27
+ *
28
+ * The AST tier may degrade silently to regex-only when:
29
+ * - The Tree-sitter grammar is unavailable offline AND uncached
30
+ * - The adapter throws (per-adapter try/catch in `runner.ts`)
31
+ * - The grammar SHA-256 doesn't match the manifest (refused by loader)
32
+ *
33
+ * Exception: if a Tree-sitter query is malformed (developer bug), the loader
34
+ * propagates `InvalidQueryError` so we don't silently mask it.
35
+ */
36
+
37
+ import type { DetectionResult } from './index.ts';
38
+ import {
39
+ introspectPython,
40
+ introspectSwift,
41
+ introspectTypeScript,
42
+ type DetectedPython,
43
+ type DetectedSwift,
44
+ type DetectedTypeScript,
45
+ } from './regex-fallback.ts';
46
+ import { runAdapters, buildDetectionSignals } from './adapters/runner.ts';
47
+ import { pythonFastApiAdapter } from './adapters/python-fastapi.ts';
48
+ import { pythonDjangoAdapter } from './adapters/python-django.ts';
49
+ import { nextjsTrpcAdapter } from './adapters/nextjs-trpc.ts';
50
+ import { swiftSwiftUiAdapter } from './adapters/swift-swiftui.ts';
51
+ import type { CodebaseAdapter, AdapterResolved } from './adapters/types.ts';
52
+
53
+ // ============================================================
54
+ // Public types — unchanged from Plan #2 to preserve consumers
55
+ // ============================================================
56
+
57
+ export type { DetectedPython, DetectedSwift, DetectedTypeScript };
58
+
59
+ export interface DetectedConventions {
60
+ python?: DetectedPython;
61
+ swift?: DetectedSwift;
62
+ typescript?: DetectedTypeScript;
63
+ /**
64
+ * AST adapter blocks live here (Plan 3b). Keys are adapter ids
65
+ * (`python-fastapi`, etc.). Values include both extracted conventions and
66
+ * a `_provenance` map.
67
+ */
68
+ [adapterId: string]: unknown;
69
+ }
70
+
71
+ // ============================================================
72
+ // Static adapter list (v1 — first-party only, per spec §6)
73
+ // ============================================================
74
+
75
+ const FIRST_PARTY_ADAPTERS: CodebaseAdapter[] = [
76
+ pythonFastApiAdapter,
77
+ pythonDjangoAdapter,
78
+ nextjsTrpcAdapter,
79
+ swiftSwiftUiAdapter,
80
+ ];
81
+
82
+ // ============================================================
83
+ // Public entry point
84
+ // ============================================================
85
+
86
+ /**
87
+ * Introspect the project's source files. Returns per-language conventions or
88
+ * an empty object if nothing was extracted.
89
+ *
90
+ * Synchronous signature is preserved — AST adapter execution is intentionally
91
+ * fire-and-forget at this layer. Phase 1 wires the adapter pipeline behind
92
+ * the existing sync function so `detect/index.ts` is byte-for-byte unchanged
93
+ * (plan line 137-142). The async adapter orchestration lives entirely inside
94
+ * `runIntrospect()`, which `introspect()` does NOT await — adapters either
95
+ * have their grammars cached (fast path, no async needed at the JS level by
96
+ * Phase 4 wiring) or degrade to regex.
97
+ *
98
+ * For Phase 1 Tier 1 to actually contribute values, callers must use the
99
+ * async variant `introspectAsync()`. The sync `introspect()` runs the regex
100
+ * tier ONLY for Phase 1; Phase 4 callers (LSP enrichment + adapter wiring)
101
+ * will switch to async.
102
+ */
103
+ export function introspect(
104
+ detection: DetectionResult,
105
+ projectRoot: string,
106
+ ): DetectedConventions {
107
+ const out: DetectedConventions = {};
108
+ const languages = Array.from(
109
+ new Set(detection.manifests.map(m => m.language)),
110
+ );
111
+
112
+ // Tier 2 — regex fallback. Always runs. AST adapters in the async variant
113
+ // (`introspectAsync`) populate `detected.<adapter-id>` alongside; for the
114
+ // sync entry point, only the regex tier participates so Plan #2 callers
115
+ // see no behavior change.
116
+ if (languages.includes('python')) {
117
+ const python = introspectPython(detection, projectRoot);
118
+ if (python !== null) out.python = python;
119
+ }
120
+
121
+ if (languages.includes('swift')) {
122
+ const swift = introspectSwift(detection, projectRoot);
123
+ if (swift !== null) out.swift = swift;
124
+ }
125
+
126
+ if (languages.includes('typescript') || languages.includes('javascript')) {
127
+ const ts = introspectTypeScript(detection, projectRoot);
128
+ if (ts !== null) out.typescript = ts;
129
+ }
130
+
131
+ return out;
132
+ }
133
+
134
+ // ============================================================
135
+ // Async variant — used by callers who want AST tier participation
136
+ // ============================================================
137
+
138
+ /**
139
+ * Async introspect: runs AST adapters first (Tier 1), then regex fallback
140
+ * (Tier 2) for fields the adapters returned 'none' on.
141
+ *
142
+ * Returns the same `DetectedConventions` shape as the sync `introspect()`,
143
+ * plus per-adapter blocks under their ids.
144
+ *
145
+ * Callers who can `await` (CLI commands, tests, etc.) should prefer this
146
+ * variant. The session-start hook keeps using sync `introspect()` for its
147
+ * 5s budget reason (P4-006).
148
+ */
149
+ export async function introspectAsync(
150
+ detection: DetectionResult,
151
+ projectRoot: string,
152
+ ): Promise<DetectedConventions> {
153
+ const out: DetectedConventions = introspect(detection, projectRoot);
154
+
155
+ // Build signals + run AST adapters
156
+ const signals = buildDetectionSignals(projectRoot);
157
+ let merged;
158
+ try {
159
+ merged = await runAdapters(FIRST_PARTY_ADAPTERS, projectRoot, signals, {
160
+ sampleFiles: async (_adapter, _root) => {
161
+ // Phase 1 placeholder: file sampling for adapters is wired in
162
+ // dedicated harnesses (per-adapter tests inject SourceFile[] directly).
163
+ // The introspector tier doesn't yet sample for AST adapters — that
164
+ // wiring lands together with Phase 4 LSP enrichment so the same path
165
+ // serves both. For now, returning [] keeps adapters at 'none' which
166
+ // means `out` is regex-only — consistent with the sync path and the
167
+ // pre-Phase-1 baseline test suite.
168
+ return [];
169
+ },
170
+ });
171
+ } catch {
172
+ return out;
173
+ }
174
+
175
+ for (const [adapterId, resolved] of Object.entries(merged.byAdapter)) {
176
+ if (resolved.confidence === 'none') continue;
177
+ out[adapterId] = serializeAdapterBlock(resolved);
178
+ }
179
+
180
+ return out;
181
+ }
182
+
183
+ function serializeAdapterBlock(r: AdapterResolved): Record<string, unknown> {
184
+ const block: Record<string, unknown> = { ...r.conventions };
185
+ if (Object.keys(r._provenance).length > 0) {
186
+ block._provenance = r._provenance;
187
+ }
188
+ block._confidence = r.confidence;
189
+ return block;
190
+ }
@@ -54,6 +54,10 @@ import {
54
54
  type UserVerificationEntry,
55
55
  } from './vr-command-map.ts';
56
56
  import { inferDomains } from './domain-inferrer.ts';
57
+ import {
58
+ introspect,
59
+ type DetectedConventions,
60
+ } from './codebase-introspector.ts';
57
61
 
58
62
  export type {
59
63
  PackageManifest,
@@ -97,6 +101,18 @@ export interface DetectionResult {
97
101
  verificationCommands: Partial<Record<SupportedLanguage, VRCommandSet>>;
98
102
  /** Non-fatal warnings collected across all detectors. */
99
103
  warnings: DetectionWarning[];
104
+ /** Plan #2 P3-001: per-language conventions sampled from existing source. */
105
+ detected?: DetectedConventions;
106
+ }
107
+
108
+ /** Plan #2 P3-002: opt-out of the codebase introspector. */
109
+ export interface RunDetectionOptions {
110
+ /**
111
+ * When true, skip the codebase introspector pass. Used by the session-start
112
+ * hook (P4-006) to keep its 5-second budget intact — the drift banner only
113
+ * needs the fingerprint, not introspection detail.
114
+ */
115
+ skipIntrospect?: boolean;
100
116
  }
101
117
 
102
118
  function dominantDir(
@@ -122,7 +138,8 @@ function dominantDir(
122
138
  */
123
139
  export async function runDetection(
124
140
  projectRoot: string,
125
- overrides?: DetectionConfigOverrides
141
+ overrides?: DetectionConfigOverrides,
142
+ options?: RunDetectionOptions,
126
143
  ): Promise<DetectionResult> {
127
144
  // 1. packages
128
145
  const pkg = detectPackageManifests(projectRoot);
@@ -170,7 +187,7 @@ export async function runDetection(
170
187
  verificationCommands[lang] = getVRCommands(lang, fw, dir, userOverride);
171
188
  }
172
189
 
173
- return {
190
+ const result: DetectionResult = {
174
191
  projectRoot,
175
192
  manifests: pkg.manifests,
176
193
  frameworks,
@@ -180,4 +197,13 @@ export async function runDetection(
180
197
  verificationCommands,
181
198
  warnings: pkg.warnings,
182
199
  };
200
+
201
+ // P3-002: codebase introspector pass. Skipped when the caller opts out
202
+ // (the session-start hook at hooks/session-start.ts:272 passes
203
+ // `{ skipIntrospect: true }` to keep its 5s budget intact — see P4-006).
204
+ if (!options?.skipIntrospect) {
205
+ result.detected = introspect(result, projectRoot);
206
+ }
207
+
208
+ return result;
183
209
  }
@@ -192,7 +192,7 @@ export function migrateV1ToV2(
192
192
  framework.languages = languageEntries;
193
193
  }
194
194
  // P1-004: preserve any v1Framework subkey the explicit rebuild didn't emit
195
- // (e.g., hedge's `framework.{python, rust, swift, typescript}` language sub-blocks).
195
+ // (e.g., a multi-runtime monorepo's `framework.{python, rust, swift, typescript}` language sub-blocks).
196
196
  preserveNestedSubkeys(v1Framework, framework);
197
197
 
198
198
  // Paths: preserve user-set fields; fill `source` from detection if user had 'src' default.
@@ -234,13 +234,13 @@ export function migrateV1ToV2(
234
234
  if (typeof v1Paths[k] === 'string') paths[k] = v1Paths[k];
235
235
  }
236
236
  // P1-005: preserve any v1Paths subkey the explicit rebuild didn't emit
237
- // (e.g., hedge's 19 custom `paths.*` entries like adr, plans, monorepo_root).
237
+ // (e.g., a downstream consumer's 19 custom `paths.*` entries like adr, plans, monorepo_root).
238
238
  preserveNestedSubkeys(v1Paths, paths);
239
239
 
240
240
  const verification = buildVerificationBlock(detection, v1Verification);
241
241
 
242
242
  // P1-006: build project block with nested passthrough so custom subkeys
243
- // (e.g., hedge's `project.description`) survive the migration.
243
+ // (e.g., a downstream consumer's `project.description`) survive the migration.
244
244
  const project: Record<string, unknown> = {
245
245
  name: typeof v1Project.name === 'string' ? v1Project.name : 'my-project',
246
246
  root: typeof v1Project.root === 'string' ? v1Project.root : 'auto',
@@ -265,7 +265,7 @@ export function migrateV1ToV2(
265
265
 
266
266
  // P1-001: preserve any v1 top-level key not already handled by the explicit
267
267
  // migrator. This is the generalization of PRESERVED_FIELDS — custom sections
268
- // like `services`, `workflow`, `north_stars` (hedge) now pass through.
268
+ // like `services`, `workflow`, `north_stars` now pass through.
269
269
  //
270
270
  // `detection` is intentionally NOT in handledTopLevel: when a v2 config is
271
271
  // fed back in (idempotence check at migrate.ts:16), the existing `detection`