@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.
- package/commands/README.md +23 -11
- package/commands/massu-deploy.python-docker.md +170 -0
- package/commands/massu-deploy.python-fly.md +189 -0
- package/commands/massu-deploy.python-launchd.md +144 -0
- package/commands/massu-deploy.python-systemd.md +163 -0
- package/commands/massu-scaffold-page.swift.md +10 -10
- package/commands/massu-scaffold-router.python-django.md +153 -0
- package/commands/massu-scaffold-router.python-fastapi.md +145 -0
- package/dist/cli.js +9914 -4133
- package/dist/hooks/auto-learning-pipeline.js +45 -2
- package/dist/hooks/classify-failure.js +45 -2
- package/dist/hooks/cost-tracker.js +45 -2
- package/dist/hooks/fix-detector.js +45 -2
- package/dist/hooks/incident-pipeline.js +45 -2
- package/dist/hooks/post-edit-context.js +45 -2
- package/dist/hooks/post-tool-use.js +45 -2
- package/dist/hooks/pre-compact.js +45 -2
- package/dist/hooks/pre-delete-check.js +45 -2
- package/dist/hooks/quality-event.js +45 -2
- package/dist/hooks/rule-enforcement-pipeline.js +45 -2
- package/dist/hooks/session-end.js +45 -2
- package/dist/hooks/session-start.js +4790 -406
- package/dist/hooks/user-prompt.js +45 -2
- package/package.json +13 -4
- package/src/cli.ts +22 -2
- package/src/commands/config-refresh.ts +91 -23
- package/src/commands/init.ts +131 -24
- package/src/commands/install-commands.ts +142 -26
- package/src/commands/refresh-log.ts +37 -0
- package/src/commands/template-engine.ts +260 -0
- package/src/commands/watch.ts +430 -0
- package/src/config.ts +71 -0
- package/src/detect/adapters/nextjs-trpc.ts +166 -0
- package/src/detect/adapters/parse-guard.ts +133 -0
- package/src/detect/adapters/python-django.ts +208 -0
- package/src/detect/adapters/python-fastapi.ts +223 -0
- package/src/detect/adapters/query-helpers.ts +170 -0
- package/src/detect/adapters/runner.ts +252 -0
- package/src/detect/adapters/swift-swiftui.ts +171 -0
- package/src/detect/adapters/tree-sitter-loader.ts +467 -0
- package/src/detect/adapters/types.ts +173 -0
- package/src/detect/codebase-introspector.ts +190 -0
- package/src/detect/index.ts +28 -2
- package/src/detect/migrate.ts +4 -4
- package/src/detect/regex-fallback.ts +449 -0
- package/src/hooks/session-start.ts +94 -3
- package/src/lib/gitToplevel.ts +22 -0
- package/src/lib/installLock.ts +179 -0
- package/src/lib/pidLiveness.ts +67 -0
- package/src/lsp/auto-detect.ts +98 -0
- package/src/lsp/client.ts +776 -0
- package/src/lsp/enrich.ts +127 -0
- package/src/lsp/types.ts +221 -0
- package/src/watch/daemon.ts +385 -0
- package/src/watch/lockfile-detector.ts +65 -0
- package/src/watch/paths.ts +279 -0
- 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
|
+
}
|
package/src/detect/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/detect/migrate.ts
CHANGED
|
@@ -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.,
|
|
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.,
|
|
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.,
|
|
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`
|
|
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`
|