@massu/core 1.3.0 → 1.4.0-soak.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 -8
- 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 +9906 -4133
- package/dist/hooks/auto-learning-pipeline.js +37 -2
- package/dist/hooks/classify-failure.js +37 -2
- package/dist/hooks/cost-tracker.js +37 -2
- package/dist/hooks/fix-detector.js +37 -2
- package/dist/hooks/incident-pipeline.js +37 -2
- package/dist/hooks/post-edit-context.js +37 -2
- package/dist/hooks/post-tool-use.js +37 -2
- package/dist/hooks/pre-compact.js +37 -2
- package/dist/hooks/pre-delete-check.js +37 -2
- package/dist/hooks/quality-event.js +37 -2
- package/dist/hooks/rule-enforcement-pipeline.js +37 -2
- package/dist/hooks/session-end.js +37 -2
- package/dist/hooks/session-start.js +4782 -406
- package/dist/hooks/user-prompt.js +37 -2
- package/package.json +10 -4
- package/src/cli.ts +22 -2
- package/src/commands/config-refresh.ts +88 -20
- package/src/commands/init.ts +130 -23
- package/src/commands/install-commands.ts +142 -26
- package/src/commands/refresh-log.ts +37 -0
- package/src/commands/template-engine.ts +262 -0
- package/src/commands/watch.ts +430 -0
- package/src/config.ts +63 -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 +348 -0
- package/src/detect/adapters/types.ts +174 -0
- package/src/detect/codebase-introspector.ts +190 -0
- package/src/detect/index.ts +28 -2
- 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 +89 -0
- package/src/lsp/client.ts +590 -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,127 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Plan 3b — Phase 4: LSP enrichment of AST adapter results.
|
|
6
|
+
*
|
|
7
|
+
* For each field in `result.conventions`, optionally enrich via LSP responses
|
|
8
|
+
* (e.g., resolve `Depends` → `fastapi.Depends` via `textDocument/definition`).
|
|
9
|
+
*
|
|
10
|
+
* Authority rule (per spec §4 / plan line 173-175):
|
|
11
|
+
* - AST is AUTHORITATIVE. LSP only refines (e.g., resolves alias → fully
|
|
12
|
+
* qualified import).
|
|
13
|
+
* - LSP unavailable / timeout / Zod-fail / version-mismatch → keep AST,
|
|
14
|
+
* log warning at field granularity, do NOT crash.
|
|
15
|
+
*
|
|
16
|
+
* Failure modes:
|
|
17
|
+
* - `lspClient === null` → return original result unchanged (no log; the
|
|
18
|
+
* "LSP not configured" path is the common case).
|
|
19
|
+
* - LSP method returns null (capability missing, timeout, Zod fail) →
|
|
20
|
+
* keep AST value, append a `_lsp_skipped: <reason>` note to provenance.
|
|
21
|
+
* - Per-field error → ignore that single field, keep others (per plan).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { AdapterResult, SourceFile } from '../detect/adapters/types.ts';
|
|
25
|
+
import { pathToFileURL } from 'url';
|
|
26
|
+
import type { LSPClient } from './client.ts';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Enrich an `AdapterResult` with LSP data when the client is available.
|
|
30
|
+
*
|
|
31
|
+
* The current v1 implementation is intentionally minimal: it walks each
|
|
32
|
+
* source file and asks the LSP for `textDocument/documentSymbol`. When the
|
|
33
|
+
* LSP returns a populated symbol list AND a convention field's value matches
|
|
34
|
+
* a symbol name, the convention's provenance is annotated with the LSP-
|
|
35
|
+
* provided detail (e.g., the canonical import path).
|
|
36
|
+
*
|
|
37
|
+
* AST values are NEVER overwritten — provenance is the only field touched.
|
|
38
|
+
*/
|
|
39
|
+
export async function enrichAdapterResult(
|
|
40
|
+
result: AdapterResult,
|
|
41
|
+
lspClient: LSPClient | null,
|
|
42
|
+
sourceFiles: SourceFile[]
|
|
43
|
+
): Promise<AdapterResult> {
|
|
44
|
+
if (!lspClient) return result;
|
|
45
|
+
if (sourceFiles.length === 0) return result;
|
|
46
|
+
|
|
47
|
+
// Build a map of convention-value → field-name(s) so we can match symbols.
|
|
48
|
+
const valueToFields = new Map<string, string[]>();
|
|
49
|
+
for (const [field, value] of Object.entries(result.conventions)) {
|
|
50
|
+
if (typeof value !== 'string') continue;
|
|
51
|
+
const arr = valueToFields.get(value) ?? [];
|
|
52
|
+
arr.push(field);
|
|
53
|
+
valueToFields.set(value, arr);
|
|
54
|
+
}
|
|
55
|
+
if (valueToFields.size === 0) return result;
|
|
56
|
+
|
|
57
|
+
const enrichedProvenance = [...result.provenance];
|
|
58
|
+
|
|
59
|
+
for (const file of sourceFiles) {
|
|
60
|
+
let symbols;
|
|
61
|
+
try {
|
|
62
|
+
const uri = pathToFileURL(file.path).toString();
|
|
63
|
+
symbols = await lspClient.documentSymbol(uri);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
process.stderr.write(
|
|
66
|
+
`[massu/lsp] WARN: documentSymbol threw on ${file.path} — skipping enrichment for this file. (${e instanceof Error ? e.message : String(e)})\n`
|
|
67
|
+
);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!symbols || !Array.isArray(symbols)) continue;
|
|
71
|
+
|
|
72
|
+
for (const sym of symbols) {
|
|
73
|
+
if (!sym || typeof sym !== 'object') continue;
|
|
74
|
+
const symObj = sym as Record<string, unknown>;
|
|
75
|
+
const name = typeof symObj.name === 'string' ? symObj.name : null;
|
|
76
|
+
if (!name) continue;
|
|
77
|
+
const matchedFields = valueToFields.get(name);
|
|
78
|
+
if (!matchedFields) continue;
|
|
79
|
+
|
|
80
|
+
// Append an LSP-sourced provenance entry for each matched field. The
|
|
81
|
+
// AST entry stays in place — LSP is enrichment-only.
|
|
82
|
+
const detail = typeof symObj.detail === 'string' ? symObj.detail : null;
|
|
83
|
+
const line = extractStartLine(symObj);
|
|
84
|
+
for (const field of matchedFields) {
|
|
85
|
+
enrichedProvenance.push({
|
|
86
|
+
field,
|
|
87
|
+
sourceFile: file.path,
|
|
88
|
+
line: line ?? 0,
|
|
89
|
+
query: detail ? `lsp:documentSymbol(${detail})` : 'lsp:documentSymbol',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
...result,
|
|
97
|
+
provenance: enrichedProvenance,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Pluck the start line from either DocumentSymbol shape (range.start.line) or
|
|
103
|
+
* SymbolInformation shape (location.range.start.line). Returns null if
|
|
104
|
+
* neither shape matches.
|
|
105
|
+
*/
|
|
106
|
+
function extractStartLine(sym: Record<string, unknown>): number | null {
|
|
107
|
+
// DocumentSymbol shape
|
|
108
|
+
const range = sym.range as Record<string, unknown> | undefined;
|
|
109
|
+
if (range && typeof range === 'object') {
|
|
110
|
+
const start = range.start as Record<string, unknown> | undefined;
|
|
111
|
+
if (start && typeof start.line === 'number') {
|
|
112
|
+
return start.line + 1; // convert 0-indexed to 1-indexed
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// SymbolInformation shape
|
|
116
|
+
const loc = sym.location as Record<string, unknown> | undefined;
|
|
117
|
+
if (loc && typeof loc === 'object') {
|
|
118
|
+
const locRange = loc.range as Record<string, unknown> | undefined;
|
|
119
|
+
if (locRange && typeof locRange === 'object') {
|
|
120
|
+
const start = locRange.start as Record<string, unknown> | undefined;
|
|
121
|
+
if (start && typeof start.line === 'number') {
|
|
122
|
+
return start.line + 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
package/src/lsp/types.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Plan 3b — Phase 4: LSP message TypeScript interfaces + Zod runtime schemas.
|
|
6
|
+
*
|
|
7
|
+
* Per audit-iter-4 fix DD: every `*Request` / `*Response` / `*Params` /
|
|
8
|
+
* `ServerCapabilities` type is paired with a co-located `*ResponseSchema` for
|
|
9
|
+
* runtime validation. `client.ts` imports from here only — no inline Zod
|
|
10
|
+
* definitions in `client.ts`.
|
|
11
|
+
*
|
|
12
|
+
* The schemas validate the LSP `result` payloads we consume (NOT the
|
|
13
|
+
* full envelope) — `LSPMessageEnvelopeSchema` covers the wire envelope.
|
|
14
|
+
*
|
|
15
|
+
* VR check (per plan): `grep -nE 'export (interface|type|const) .*(Schema|
|
|
16
|
+
* Request|Response|Params|Capabilities)' packages/core/src/lsp/types.ts` MUST
|
|
17
|
+
* return ≥8 hits after Phase 4.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
|
|
22
|
+
// ============================================================
|
|
23
|
+
// LSP error codes (subset we reference)
|
|
24
|
+
// ============================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* JSON-RPC + LSP error codes used by the client. Numeric values match the
|
|
28
|
+
* LSP 3.17 spec — `MethodNotFound = -32601` is the gatekeeper for the
|
|
29
|
+
* graceful-degrade path (per plan line 160).
|
|
30
|
+
*/
|
|
31
|
+
export const LSPErrorCode = {
|
|
32
|
+
ParseError: -32700,
|
|
33
|
+
InvalidRequest: -32600,
|
|
34
|
+
MethodNotFound: -32601,
|
|
35
|
+
InvalidParams: -32602,
|
|
36
|
+
InternalError: -32603,
|
|
37
|
+
ServerNotInitialized: -32002,
|
|
38
|
+
RequestFailed: -32803,
|
|
39
|
+
ServerCancelled: -32802,
|
|
40
|
+
} as const;
|
|
41
|
+
export type LSPErrorCodeValue = typeof LSPErrorCode[keyof typeof LSPErrorCode];
|
|
42
|
+
|
|
43
|
+
// ============================================================
|
|
44
|
+
// Wire envelope (every LSP/JSON-RPC message)
|
|
45
|
+
// ============================================================
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* JSON-RPC 2.0 / LSP envelope. `id` is required for request/response, absent
|
|
49
|
+
* for notifications. `method` is present on requests/notifications, absent on
|
|
50
|
+
* responses. `result` xor `error` on responses.
|
|
51
|
+
*/
|
|
52
|
+
export const LSPMessageEnvelopeSchema = z.object({
|
|
53
|
+
jsonrpc: z.literal('2.0'),
|
|
54
|
+
id: z.union([z.number(), z.string()]).optional(),
|
|
55
|
+
method: z.string().optional(),
|
|
56
|
+
params: z.unknown().optional(),
|
|
57
|
+
result: z.unknown().optional(),
|
|
58
|
+
error: z
|
|
59
|
+
.object({
|
|
60
|
+
code: z.number(),
|
|
61
|
+
message: z.string(),
|
|
62
|
+
data: z.unknown().optional(),
|
|
63
|
+
})
|
|
64
|
+
.optional(),
|
|
65
|
+
}).passthrough();
|
|
66
|
+
export type LSPMessageEnvelope = z.infer<typeof LSPMessageEnvelopeSchema>;
|
|
67
|
+
|
|
68
|
+
// ============================================================
|
|
69
|
+
// Initialize
|
|
70
|
+
// ============================================================
|
|
71
|
+
|
|
72
|
+
export interface InitializeRequest {
|
|
73
|
+
processId: number | null;
|
|
74
|
+
rootUri: string | null;
|
|
75
|
+
capabilities: Record<string, unknown>;
|
|
76
|
+
workspaceFolders?: Array<{ uri: string; name: string }> | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Subset of LSP `ServerCapabilities` we consume. Everything else is ignored.
|
|
81
|
+
* Each `*Provider` flag may be absent (treat as false), `true`, or an object
|
|
82
|
+
* (treat as true — server supports the method but with options we don't use).
|
|
83
|
+
*/
|
|
84
|
+
export interface ServerCapabilities {
|
|
85
|
+
documentSymbolProvider?: boolean | Record<string, unknown>;
|
|
86
|
+
workspaceSymbolProvider?: boolean | Record<string, unknown>;
|
|
87
|
+
definitionProvider?: boolean | Record<string, unknown>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const ServerCapabilitiesSchema = z
|
|
91
|
+
.object({
|
|
92
|
+
documentSymbolProvider: z.union([z.boolean(), z.record(z.string(), z.unknown())]).optional(),
|
|
93
|
+
workspaceSymbolProvider: z.union([z.boolean(), z.record(z.string(), z.unknown())]).optional(),
|
|
94
|
+
definitionProvider: z.union([z.boolean(), z.record(z.string(), z.unknown())]).optional(),
|
|
95
|
+
})
|
|
96
|
+
.passthrough();
|
|
97
|
+
|
|
98
|
+
export interface InitializeResponse {
|
|
99
|
+
capabilities: ServerCapabilities;
|
|
100
|
+
serverInfo?: { name: string; version?: string };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const InitializeResponseSchema = z
|
|
104
|
+
.object({
|
|
105
|
+
capabilities: ServerCapabilitiesSchema,
|
|
106
|
+
serverInfo: z
|
|
107
|
+
.object({
|
|
108
|
+
name: z.string(),
|
|
109
|
+
version: z.string().optional(),
|
|
110
|
+
})
|
|
111
|
+
.optional(),
|
|
112
|
+
})
|
|
113
|
+
.passthrough();
|
|
114
|
+
|
|
115
|
+
// ============================================================
|
|
116
|
+
// Position / Range / Location (shared)
|
|
117
|
+
// ============================================================
|
|
118
|
+
|
|
119
|
+
export const PositionSchema = z.object({
|
|
120
|
+
line: z.number().int().nonnegative(),
|
|
121
|
+
character: z.number().int().nonnegative(),
|
|
122
|
+
});
|
|
123
|
+
export type Position = z.infer<typeof PositionSchema>;
|
|
124
|
+
|
|
125
|
+
export const RangeSchema = z.object({
|
|
126
|
+
start: PositionSchema,
|
|
127
|
+
end: PositionSchema,
|
|
128
|
+
});
|
|
129
|
+
export type Range = z.infer<typeof RangeSchema>;
|
|
130
|
+
|
|
131
|
+
export const LocationSchema = z.object({
|
|
132
|
+
uri: z.string(),
|
|
133
|
+
range: RangeSchema,
|
|
134
|
+
});
|
|
135
|
+
export type Location = z.infer<typeof LocationSchema>;
|
|
136
|
+
|
|
137
|
+
// ============================================================
|
|
138
|
+
// textDocument/documentSymbol
|
|
139
|
+
// ============================================================
|
|
140
|
+
|
|
141
|
+
export interface DocumentSymbolParams {
|
|
142
|
+
textDocument: { uri: string };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Document symbols come in two shapes: hierarchical `DocumentSymbol[]` (LSP
|
|
147
|
+
* 3.10+) or flat `SymbolInformation[]` (legacy). We accept either at the
|
|
148
|
+
* Zod level — consumers in `enrich.ts` pick the shape they care about.
|
|
149
|
+
*/
|
|
150
|
+
const DocumentSymbolNodeSchema: z.ZodType<unknown> = z.lazy(() =>
|
|
151
|
+
z
|
|
152
|
+
.object({
|
|
153
|
+
name: z.string(),
|
|
154
|
+
kind: z.number(),
|
|
155
|
+
range: RangeSchema,
|
|
156
|
+
selectionRange: RangeSchema,
|
|
157
|
+
detail: z.string().optional(),
|
|
158
|
+
children: z.array(DocumentSymbolNodeSchema).optional(),
|
|
159
|
+
})
|
|
160
|
+
.passthrough()
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const SymbolInformationNodeSchema = z
|
|
164
|
+
.object({
|
|
165
|
+
name: z.string(),
|
|
166
|
+
kind: z.number(),
|
|
167
|
+
location: LocationSchema,
|
|
168
|
+
containerName: z.string().optional(),
|
|
169
|
+
})
|
|
170
|
+
.passthrough();
|
|
171
|
+
|
|
172
|
+
export const DocumentSymbolResponseSchema = z.union([
|
|
173
|
+
z.array(DocumentSymbolNodeSchema),
|
|
174
|
+
z.array(SymbolInformationNodeSchema),
|
|
175
|
+
z.null(),
|
|
176
|
+
]);
|
|
177
|
+
export type DocumentSymbolResponse = z.infer<typeof DocumentSymbolResponseSchema>;
|
|
178
|
+
|
|
179
|
+
// ============================================================
|
|
180
|
+
// workspace/symbol
|
|
181
|
+
// ============================================================
|
|
182
|
+
|
|
183
|
+
export interface WorkspaceSymbolParams {
|
|
184
|
+
query: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const WorkspaceSymbolResponseSchema = z.union([
|
|
188
|
+
z.array(SymbolInformationNodeSchema),
|
|
189
|
+
z.null(),
|
|
190
|
+
]);
|
|
191
|
+
export type WorkspaceSymbolResponse = z.infer<typeof WorkspaceSymbolResponseSchema>;
|
|
192
|
+
|
|
193
|
+
// ============================================================
|
|
194
|
+
// textDocument/definition
|
|
195
|
+
// ============================================================
|
|
196
|
+
|
|
197
|
+
export interface DefinitionParams {
|
|
198
|
+
textDocument: { uri: string };
|
|
199
|
+
position: Position;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Definition can be a single Location, an array of Locations, or null.
|
|
204
|
+
* (LSP also allows `LocationLink[]` — we accept it via passthrough but
|
|
205
|
+
* downstream consumers stick to `Location`.)
|
|
206
|
+
*/
|
|
207
|
+
export const DefinitionResponseSchema = z.union([
|
|
208
|
+
LocationSchema,
|
|
209
|
+
z.array(LocationSchema),
|
|
210
|
+
z.array(
|
|
211
|
+
z
|
|
212
|
+
.object({
|
|
213
|
+
targetUri: z.string(),
|
|
214
|
+
targetRange: RangeSchema,
|
|
215
|
+
targetSelectionRange: RangeSchema,
|
|
216
|
+
})
|
|
217
|
+
.passthrough()
|
|
218
|
+
),
|
|
219
|
+
z.null(),
|
|
220
|
+
]);
|
|
221
|
+
export type DefinitionResponse = z.infer<typeof DefinitionResponseSchema>;
|