vibe-splain 2.4.1 → 2.6.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/commands/install.js +3 -2
- package/dist/index.js +2302 -1752
- package/dist/mcp/tools/scan_project.js +15 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -19,8 +19,9 @@ function expandPath(p) {
|
|
|
19
19
|
return p;
|
|
20
20
|
}
|
|
21
21
|
var AGENT_CONFIGS = [
|
|
22
|
-
{ name: "Claude Code", path: "~/.claude/
|
|
23
|
-
{ name: "Claude
|
|
22
|
+
{ name: "Claude Code CLI", path: "~/.claude/settings.json", format: "claude" },
|
|
23
|
+
{ name: "Claude Desktop", path: "~/.claude/claude_desktop_config.json", format: "claude" },
|
|
24
|
+
{ name: "Claude Desktop (Windows)", path: "%APPDATA%/Claude/claude_desktop_config.json", format: "claude" },
|
|
24
25
|
{ name: "Gemini CLI", path: "~/.gemini/settings.json", format: "gemini" },
|
|
25
26
|
{ name: "Cursor", path: "~/.cursor/mcp.json", format: "cursor" },
|
|
26
27
|
{ name: "Windsurf", path: "~/.codeium/windsurf/mcp_config.json", format: "cursor" }
|
|
@@ -87,12 +88,11 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
87
88
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
88
89
|
|
|
89
90
|
// ../brain/dist/scanner.js
|
|
90
|
-
import
|
|
91
|
-
import {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
import {
|
|
95
|
-
import { existsSync as existsSync2 } from "fs";
|
|
91
|
+
import { extname as extname4 } from "path";
|
|
92
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
93
|
+
|
|
94
|
+
// ../brain/dist/pipeline/orchestrator.js
|
|
95
|
+
import { join as join8 } from "path";
|
|
96
96
|
|
|
97
97
|
// ../brain/dist/graph.js
|
|
98
98
|
import { join as join2 } from "path";
|
|
@@ -106,7 +106,6 @@ async function writeGraph(projectRoot, graph) {
|
|
|
106
106
|
|
|
107
107
|
// ../brain/dist/analysis.js
|
|
108
108
|
import { join as join3 } from "path";
|
|
109
|
-
import { createHash } from "crypto";
|
|
110
109
|
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
111
110
|
async function readAnalysis(projectRoot) {
|
|
112
111
|
const p = join3(projectRoot, ".vibe-splainer", "analysis.json");
|
|
@@ -126,575 +125,279 @@ async function writeAnalysis(projectRoot, store) {
|
|
|
126
125
|
const { rename } = await import("fs/promises");
|
|
127
126
|
await rename(tmp, dest);
|
|
128
127
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
128
|
+
|
|
129
|
+
// ../brain/dist/pipeline/inventory.js
|
|
130
|
+
import Parser from "web-tree-sitter";
|
|
131
|
+
import { join as join4, dirname, relative, extname, basename, sep } from "path";
|
|
132
|
+
import { fileURLToPath } from "url";
|
|
133
|
+
import { createRequire } from "module";
|
|
134
|
+
import { readFile as readFile4, readdir, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
|
|
135
|
+
import { existsSync as existsSync2 } from "fs";
|
|
136
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
137
|
+
var require2 = createRequire(import.meta.url);
|
|
138
|
+
var _parser = null;
|
|
139
|
+
var langCache = /* @__PURE__ */ new Map();
|
|
140
|
+
var EXT_LANG = {
|
|
141
|
+
".ts": "typescript",
|
|
142
|
+
".tsx": "tsx",
|
|
143
|
+
".js": "javascript",
|
|
144
|
+
".jsx": "tsx",
|
|
145
|
+
".mjs": "javascript",
|
|
146
|
+
".cjs": "javascript",
|
|
147
|
+
".py": "python",
|
|
148
|
+
".go": "go",
|
|
149
|
+
".rs": "rust",
|
|
150
|
+
".java": "java"
|
|
151
|
+
};
|
|
152
|
+
var LANG_WASM = {
|
|
153
|
+
typescript: "tree-sitter-typescript.wasm",
|
|
154
|
+
tsx: "tree-sitter-tsx.wasm",
|
|
155
|
+
javascript: "tree-sitter-javascript.wasm",
|
|
156
|
+
python: "tree-sitter-python.wasm",
|
|
157
|
+
go: "tree-sitter-go.wasm",
|
|
158
|
+
rust: "tree-sitter-rust.wasm",
|
|
159
|
+
java: "tree-sitter-java.wasm"
|
|
160
|
+
};
|
|
161
|
+
var SUPPORTED_EXTENSIONS = new Set(Object.keys(EXT_LANG));
|
|
162
|
+
function resolveWasm(file) {
|
|
163
|
+
try {
|
|
164
|
+
const wasmsDir = dirname(require2.resolve("tree-sitter-wasms/package.json"));
|
|
165
|
+
const p = join4(wasmsDir, "out", file);
|
|
166
|
+
if (existsSync2(p))
|
|
167
|
+
return p;
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
const local = join4(__dirname, "../../wasm", file);
|
|
171
|
+
return existsSync2(local) ? local : null;
|
|
172
|
+
}
|
|
173
|
+
async function getLanguage(lang) {
|
|
174
|
+
const cached = langCache.get(lang);
|
|
175
|
+
if (cached)
|
|
176
|
+
return cached;
|
|
177
|
+
const wasm = resolveWasm(LANG_WASM[lang]);
|
|
178
|
+
if (!wasm) {
|
|
179
|
+
console.error(`[vibe-splain] grammar missing for ${lang} (${LANG_WASM[lang]}); skipping`);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const loaded = await Parser.Language.load(wasm);
|
|
184
|
+
langCache.set(lang, loaded);
|
|
185
|
+
return loaded;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
console.error(`[vibe-splain] failed to load grammar for ${lang}:`, err instanceof Error ? err.message : err);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function initParser() {
|
|
192
|
+
if (_parser)
|
|
193
|
+
return _parser;
|
|
194
|
+
await Parser.init();
|
|
195
|
+
_parser = new Parser();
|
|
196
|
+
const ts = await getLanguage("typescript");
|
|
197
|
+
if (ts)
|
|
198
|
+
_parser.setLanguage(ts);
|
|
199
|
+
return _parser;
|
|
200
|
+
}
|
|
201
|
+
async function parseAs(lang, source) {
|
|
202
|
+
const p = await initParser();
|
|
203
|
+
const language = await getLanguage(lang);
|
|
204
|
+
if (!language)
|
|
205
|
+
return null;
|
|
206
|
+
p.setLanguage(language);
|
|
207
|
+
try {
|
|
208
|
+
return p.parse(source);
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
|
|
214
|
+
"node_modules",
|
|
215
|
+
"dist",
|
|
216
|
+
"build",
|
|
217
|
+
".next",
|
|
218
|
+
"out",
|
|
219
|
+
".vibe-splainer",
|
|
220
|
+
".git",
|
|
221
|
+
".venv",
|
|
222
|
+
"venv",
|
|
223
|
+
"env",
|
|
224
|
+
"__pycache__",
|
|
225
|
+
".idea",
|
|
226
|
+
".vscode",
|
|
227
|
+
".cache",
|
|
228
|
+
"site-packages",
|
|
229
|
+
"target",
|
|
230
|
+
".tox",
|
|
231
|
+
".mypy_cache",
|
|
232
|
+
".pytest_cache"
|
|
135
233
|
]);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
234
|
+
var EXCLUDE_FILE_PATTERNS = [/\.lock$/, /\.min\.[a-z]+$/, /\.d\.ts$/];
|
|
235
|
+
var DEMOTE_SEGMENTS = /* @__PURE__ */ new Set([
|
|
236
|
+
"docs",
|
|
237
|
+
"doc",
|
|
238
|
+
"examples",
|
|
239
|
+
"example",
|
|
240
|
+
"samples",
|
|
241
|
+
"sample",
|
|
242
|
+
"mockup",
|
|
243
|
+
"mockups",
|
|
244
|
+
"fixtures",
|
|
245
|
+
"fixture",
|
|
246
|
+
"__generated__",
|
|
247
|
+
"__mocks__",
|
|
248
|
+
"playwright",
|
|
249
|
+
"e2e",
|
|
250
|
+
"__tests__",
|
|
251
|
+
"cypress",
|
|
252
|
+
"storybook",
|
|
253
|
+
"stories",
|
|
254
|
+
".storybook"
|
|
255
|
+
]);
|
|
256
|
+
var VENDOR_SEGMENTS = /* @__PURE__ */ new Set([
|
|
257
|
+
"node_modules",
|
|
258
|
+
"vendor",
|
|
259
|
+
"vendored",
|
|
260
|
+
"site-packages",
|
|
261
|
+
"third_party",
|
|
262
|
+
"third-party"
|
|
263
|
+
]);
|
|
264
|
+
async function collectFiles(dir, projectRoot, acc) {
|
|
265
|
+
let entries;
|
|
266
|
+
try {
|
|
267
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
268
|
+
} catch {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
if (entry.name.startsWith(".") && entry.name !== ".") {
|
|
273
|
+
if (entry.isDirectory())
|
|
156
274
|
continue;
|
|
157
|
-
}
|
|
158
275
|
}
|
|
159
|
-
if (
|
|
160
|
-
continue;
|
|
161
|
-
const meta = files[current.path];
|
|
162
|
-
if (!meta)
|
|
276
|
+
if (EXCLUDE_DIRS.has(entry.name))
|
|
163
277
|
continue;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
278
|
+
const fullPath = join4(dir, entry.name);
|
|
279
|
+
if (entry.isDirectory()) {
|
|
280
|
+
await collectFiles(fullPath, projectRoot, acc);
|
|
281
|
+
} else if (entry.isFile()) {
|
|
282
|
+
const ext = extname(entry.name);
|
|
283
|
+
if (!SUPPORTED_EXTENSIONS.has(ext))
|
|
284
|
+
continue;
|
|
285
|
+
if (EXCLUDE_FILE_PATTERNS.some((p) => p.test(entry.name)))
|
|
286
|
+
continue;
|
|
287
|
+
acc.push(fullPath);
|
|
168
288
|
}
|
|
169
289
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
290
|
+
}
|
|
291
|
+
function pathDemoteReason(relPath) {
|
|
292
|
+
const segs = relPath.split(sep);
|
|
293
|
+
for (const s of segs) {
|
|
294
|
+
if (VENDOR_SEGMENTS.has(s))
|
|
295
|
+
return `vendored code (${s})`;
|
|
296
|
+
if (s.endsWith(".venv") || s === "venv" || s === "env")
|
|
297
|
+
return "virtual environment";
|
|
175
298
|
}
|
|
176
|
-
|
|
299
|
+
for (const s of segs) {
|
|
300
|
+
if (DEMOTE_SEGMENTS.has(s.toLowerCase()))
|
|
301
|
+
return `non-application path segment (${s})`;
|
|
302
|
+
}
|
|
303
|
+
const b = basename(relPath);
|
|
304
|
+
if (/\.min\./.test(b))
|
|
305
|
+
return "minified bundle";
|
|
306
|
+
if (/\.generated\./.test(b))
|
|
307
|
+
return "generated file";
|
|
308
|
+
return null;
|
|
177
309
|
}
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
310
|
+
function inferFrameworkRole(relPath) {
|
|
311
|
+
const p = relPath.replace(/\\/g, "/");
|
|
312
|
+
if (/\.test\.|\.spec\./.test(p))
|
|
313
|
+
return "test";
|
|
314
|
+
if (/\.generated\.|__generated__|\.prisma\//.test(p))
|
|
315
|
+
return "generated";
|
|
316
|
+
if (/(?:^|\/)app\/.*\/page\.tsx?$/.test(p))
|
|
317
|
+
return "app_route_page";
|
|
318
|
+
if (/(?:^|\/)app\/.*\/layout\.tsx?$/.test(p))
|
|
319
|
+
return "app_route_layout";
|
|
320
|
+
if (/(?:^|\/)app\/.*\/route\.tsx?$/.test(p))
|
|
321
|
+
return "app_route_handler";
|
|
322
|
+
if (/(?:^|\/)app\/.*\/loading\.tsx?$/.test(p))
|
|
323
|
+
return "app_loading_boundary";
|
|
324
|
+
if (/(?:^|\/)app\/.*\/error\.tsx?$/.test(p))
|
|
325
|
+
return "app_error_boundary";
|
|
326
|
+
if (/(?:^|\/)pages\/api\/trpc\//.test(p))
|
|
327
|
+
return "trpc_api_route";
|
|
328
|
+
if (/(?:^|\/)pages\/api\//.test(p))
|
|
329
|
+
return "pages_api_route";
|
|
330
|
+
if (/(?:^|\/)pages\//.test(p))
|
|
331
|
+
return "pages_route";
|
|
332
|
+
if (/\/hooks\/|\/use[A-Z][^/]*\.(ts|tsx)$/.test(p))
|
|
333
|
+
return "hook";
|
|
334
|
+
if (/\/stores?\/|[Ss]tore\.(ts|tsx)$/.test(p))
|
|
335
|
+
return "store";
|
|
336
|
+
if (/[Pp]rovider\.(tsx?|jsx?)$|\/providers?\//.test(p))
|
|
337
|
+
return "provider";
|
|
338
|
+
if (/\.types\.ts$|\/types\.ts$|\/types\/[^/]+\.ts$/.test(p))
|
|
339
|
+
return "type_definition";
|
|
340
|
+
if (/\.(tsx|jsx)$/.test(p))
|
|
341
|
+
return "component";
|
|
342
|
+
if (/\.(ts|js|mjs|cjs)$/.test(p))
|
|
343
|
+
return "utility";
|
|
344
|
+
return "unknown";
|
|
186
345
|
}
|
|
187
|
-
function
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
if (f.sideEffectProfile.includes("webhook_delivery"))
|
|
206
|
-
score += 2;
|
|
207
|
-
if (f.sideEffectProfile.includes("webhook_ingress"))
|
|
208
|
-
score += 2;
|
|
209
|
-
if (f.sideEffectProfile.includes("calendar_mutation"))
|
|
210
|
-
score += 2;
|
|
211
|
-
if (f.sideEffectProfile.includes("redirect"))
|
|
212
|
-
score += 1;
|
|
213
|
-
if (f.sideEffectProfile.includes("analytics_event"))
|
|
214
|
-
score += 1;
|
|
215
|
-
const highImpactDomains = [
|
|
216
|
-
"booking_creation",
|
|
217
|
-
"payments",
|
|
218
|
-
"auth_oauth",
|
|
219
|
-
"webhooks",
|
|
220
|
-
"payments_webhooks"
|
|
221
|
-
];
|
|
222
|
-
if (highImpactDomains.includes(f.productDomain))
|
|
223
|
-
score += 2;
|
|
224
|
-
const maxSeverity = f.smells.length > 0 ? Math.max(...f.smells.map((s) => s.severity)) : 0;
|
|
225
|
-
if (maxSeverity === 5)
|
|
226
|
-
score += 3;
|
|
227
|
-
return score;
|
|
228
|
-
}
|
|
229
|
-
function computeSeverity(f, entrypoints) {
|
|
230
|
-
let score = 0;
|
|
231
|
-
if (f.sideEffectProfile.includes("database_write"))
|
|
232
|
-
score += 3;
|
|
233
|
-
if (f.sideEffectProfile.includes("booking_mutation"))
|
|
234
|
-
score += 4;
|
|
235
|
-
if (f.sideEffectProfile.includes("payment_mutation"))
|
|
236
|
-
score += 4;
|
|
237
|
-
if (f.sideEffectProfile.includes("auth_token_mutation"))
|
|
238
|
-
score += 4;
|
|
239
|
-
if (f.sideEffectProfile.includes("webhook_delivery"))
|
|
240
|
-
score += 3;
|
|
241
|
-
if (f.sideEffectProfile.includes("webhook_ingress"))
|
|
242
|
-
score += 3;
|
|
243
|
-
if (f.sideEffectProfile.includes("calendar_mutation"))
|
|
244
|
-
score += 3;
|
|
245
|
-
if (f.productDomain === "booking_creation")
|
|
246
|
-
score += 3;
|
|
247
|
-
if (f.productDomain === "payments" || f.productDomain === "payments_webhooks")
|
|
248
|
-
score += 3;
|
|
249
|
-
if (f.productDomain === "auth_oauth")
|
|
250
|
-
score += 3;
|
|
251
|
-
if (f.productDomain === "webhooks")
|
|
252
|
-
score += 2;
|
|
253
|
-
if (f.gravity >= 85)
|
|
254
|
-
score += 2;
|
|
255
|
-
if (f.heat >= 70)
|
|
256
|
-
score += 2;
|
|
257
|
-
if (f.heatSignals.maxNesting >= 4)
|
|
258
|
-
score += 1;
|
|
259
|
-
if (f.heatSignals.longFunctions >= 1)
|
|
260
|
-
score += 1;
|
|
261
|
-
if (f.heatSignals.swallowedCatches >= 1)
|
|
262
|
-
score += 1;
|
|
263
|
-
if (entrypoints.length >= 2)
|
|
264
|
-
score += 2;
|
|
265
|
-
if (score >= 10)
|
|
266
|
-
return 5;
|
|
267
|
-
if (score >= 7)
|
|
268
|
-
return 4;
|
|
269
|
-
if (score >= 4)
|
|
270
|
-
return 3;
|
|
271
|
-
if (score >= 2)
|
|
272
|
-
return 2;
|
|
273
|
-
return 1;
|
|
274
|
-
}
|
|
275
|
-
function inferRiskTypes(f) {
|
|
276
|
-
const types = [];
|
|
277
|
-
const kinds = new Set(f.smells.map((s) => s.kind));
|
|
278
|
-
if (f.gravitySignals.cyclomatic > 20)
|
|
279
|
-
types.push("state_machine");
|
|
280
|
-
if (kinds.has("god-file")) {
|
|
281
|
-
if (f.frameworkRole === "hook")
|
|
282
|
-
types.push("god_hook");
|
|
283
|
-
else
|
|
284
|
-
types.push("god_component");
|
|
285
|
-
}
|
|
286
|
-
if (f.sideEffectProfile.length > 3 && !f.sideEffectProfile.includes("none_detected")) {
|
|
287
|
-
types.push("side_effect_coupling");
|
|
288
|
-
}
|
|
289
|
-
if (f.productDomain === "forms" && f.gravitySignals.fanIn > 5 && f.gravitySignals.publicSurface > 8)
|
|
290
|
-
types.push("registry_bottleneck");
|
|
291
|
-
if (f.sideEffectProfile.some((s) => ["booking_mutation", "payment_mutation", "auth_token_mutation"].includes(s)) && f.gravitySignals.cyclomatic > 10)
|
|
292
|
-
types.push("mutation_orchestration");
|
|
293
|
-
if (ENTRYPOINT_ROLES.has(f.frameworkRole) && f.sideEffectProfile.includes("database_write"))
|
|
294
|
-
types.push("route_handler_write_path");
|
|
295
|
-
if (kinds.has("swallowed-catch"))
|
|
296
|
-
types.push("error_swallowing");
|
|
297
|
-
if (f.sideEffectProfile.includes("local_storage") || f.sideEffectProfile.includes("indexed_db"))
|
|
298
|
-
types.push("storage_persistence_risk");
|
|
299
|
-
if (types.length === 0)
|
|
300
|
-
types.push("complexity_hotspot");
|
|
301
|
-
return types;
|
|
302
|
-
}
|
|
303
|
-
function inferObservableOutputs(f) {
|
|
304
|
-
const outputs = [];
|
|
305
|
-
if (f.sideEffectProfile.includes("redirect"))
|
|
306
|
-
outputs.push("redirect_url");
|
|
307
|
-
if (ENTRYPOINT_ROLES.has(f.frameworkRole))
|
|
308
|
-
outputs.push("http_status");
|
|
309
|
-
if (f.frameworkRole === "app_route_handler" || f.frameworkRole === "pages_api_route") {
|
|
310
|
-
outputs.push("json_response_shape");
|
|
311
|
-
}
|
|
312
|
-
if (f.productDomain === "booking_creation" || f.productDomain === "booking_management") {
|
|
313
|
-
outputs.push("booking_uid");
|
|
314
|
-
}
|
|
315
|
-
if (f.productDomain === "payments" || f.productDomain === "payments_webhooks") {
|
|
316
|
-
outputs.push("payment_status");
|
|
317
|
-
}
|
|
318
|
-
if (f.productDomain === "auth_oauth") {
|
|
319
|
-
outputs.push("auth_token");
|
|
320
|
-
}
|
|
321
|
-
if (f.sideEffectProfile.includes("webhook_delivery") || f.sideEffectProfile.includes("webhook_ingress")) {
|
|
322
|
-
outputs.push("webhook_payload");
|
|
323
|
-
}
|
|
324
|
-
if (f.sideEffectProfile.includes("calendar_mutation")) {
|
|
325
|
-
outputs.push("calendar_event_id");
|
|
326
|
-
}
|
|
327
|
-
if (f.sideEffectProfile.includes("email_send")) {
|
|
328
|
-
outputs.push("email_payload");
|
|
329
|
-
}
|
|
330
|
-
if (f.sideEffectProfile.includes("analytics_event")) {
|
|
331
|
-
outputs.push("sdk_event_name");
|
|
332
|
-
}
|
|
333
|
-
if (f.frameworkRole === "hook" || f.frameworkRole === "store") {
|
|
334
|
-
outputs.push("ui_state_transition");
|
|
335
|
-
}
|
|
336
|
-
return [...new Set(outputs)];
|
|
337
|
-
}
|
|
338
|
-
function inferWriteIntents(f) {
|
|
339
|
-
const intents = [];
|
|
340
|
-
if (f.productDomain === "booking_creation") {
|
|
341
|
-
intents.push("create_booking");
|
|
342
|
-
if (f.relativePath.includes("reschedule") || f.relativePath.includes("Reschedule"))
|
|
343
|
-
intents.push("reschedule_booking");
|
|
344
|
-
if (f.relativePath.includes("recurring") || f.relativePath.includes("Recurring")) {
|
|
345
|
-
intents.push("create_recurring_booking");
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
if (f.productDomain === "booking_management") {
|
|
349
|
-
intents.push("cancel_booking");
|
|
350
|
-
}
|
|
351
|
-
if (f.productDomain === "event_type_configuration") {
|
|
352
|
-
intents.push("update_event_type");
|
|
353
|
-
}
|
|
354
|
-
if (f.productDomain === "availability") {
|
|
355
|
-
intents.push("update_availability");
|
|
356
|
-
}
|
|
357
|
-
if (f.productDomain === "payments") {
|
|
358
|
-
intents.push("create_payment");
|
|
359
|
-
}
|
|
360
|
-
if (f.productDomain === "payments_webhooks") {
|
|
361
|
-
intents.push("handle_payment_webhook");
|
|
362
|
-
}
|
|
363
|
-
if (f.productDomain === "auth_oauth") {
|
|
364
|
-
intents.push("issue_auth_token");
|
|
365
|
-
intents.push("refresh_auth_token");
|
|
366
|
-
}
|
|
367
|
-
if (f.sideEffectProfile.includes("webhook_delivery")) {
|
|
368
|
-
intents.push("send_webhook");
|
|
369
|
-
}
|
|
370
|
-
if (f.productDomain === "settings") {
|
|
371
|
-
intents.push("update_user_settings");
|
|
372
|
-
}
|
|
373
|
-
if (f.sideEffectProfile.includes("local_storage") || f.sideEffectProfile.includes("indexed_db")) {
|
|
374
|
-
intents.push("persist_local_state");
|
|
375
|
-
}
|
|
376
|
-
return intents.length > 0 ? intents : ["none_detected"];
|
|
377
|
-
}
|
|
378
|
-
function inferPatchRisk(f, score, riskTypes) {
|
|
379
|
-
if (score >= 12 || f.productDomain === "booking_creation" && riskTypes.includes("mutation_orchestration")) {
|
|
380
|
-
return {
|
|
381
|
-
level: "critical",
|
|
382
|
-
reason: `${f.productDomain} domain with ${riskTypes.join(", ")} \u2014 any patch risks breaking live booking, payment, or auth flows.`
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
if (score >= 8 || f.sideEffectProfile.includes("payment_mutation") || f.sideEffectProfile.includes("auth_token_mutation")) {
|
|
386
|
-
return {
|
|
387
|
-
level: "high",
|
|
388
|
-
reason: `${f.productDomain} writes to external state (${f.sideEffectProfile.filter((s) => ["payment_mutation", "auth_token_mutation", "database_write", "webhook_delivery"].includes(s)).join(", ") || "database"}). Changes require integration testing.`
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
if (score >= 5 || f.importedBy.length >= 5) {
|
|
392
|
-
return {
|
|
393
|
-
level: "medium",
|
|
394
|
-
reason: `Imported by ${f.importedBy.length} files. Interface changes will cascade.`
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
return {
|
|
398
|
-
level: "low",
|
|
399
|
-
reason: "Locally contained \u2014 limited blast radius."
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
function inferSafePatchStrategy(f, riskTypes) {
|
|
403
|
-
if (riskTypes.includes("mutation_orchestration")) {
|
|
404
|
-
return "Do not rewrite inline. Extract pure decision logic into a tested reducer or state machine first. Preserve all side-effect call sites (redirect URLs, SDK event names, response shapes) as invariants.";
|
|
405
|
-
}
|
|
406
|
-
if (riskTypes.includes("registry_bottleneck")) {
|
|
407
|
-
return "Add new entries without removing existing keys. Treat the registry map as append-only until all consumers are verified.";
|
|
408
|
-
}
|
|
409
|
-
if (riskTypes.includes("route_handler_write_path")) {
|
|
410
|
-
return "Add integration tests covering success and failure paths before modifying. Verify HTTP status codes and response shapes are preserved.";
|
|
411
|
-
}
|
|
412
|
-
if (riskTypes.includes("god_component") || riskTypes.includes("god_hook")) {
|
|
413
|
-
return "Extract sub-concerns into separate modules first. Only refactor the extraction points after tests confirm equivalence.";
|
|
414
|
-
}
|
|
415
|
-
if (f.sideEffectProfile.includes("database_write")) {
|
|
416
|
-
return "Wrap changes in a transaction or use a feature flag. Run against a staging database before production.";
|
|
417
|
-
}
|
|
418
|
-
return "Review importedBy before patching. Run affected integration tests.";
|
|
419
|
-
}
|
|
420
|
-
function inferDoNotTouch(f) {
|
|
421
|
-
const items = [];
|
|
422
|
-
if (f.sideEffectProfile.includes("payment_mutation"))
|
|
423
|
-
items.push("payment flow branch");
|
|
424
|
-
if (f.sideEffectProfile.includes("auth_token_mutation"))
|
|
425
|
-
items.push("token issuance / refresh branch");
|
|
426
|
-
if (f.sideEffectProfile.includes("webhook_delivery") || f.sideEffectProfile.includes("webhook_ingress")) {
|
|
427
|
-
items.push("webhook payload shape");
|
|
428
|
-
}
|
|
429
|
-
if (f.sideEffectProfile.includes("redirect"))
|
|
430
|
-
items.push("redirect URL strings");
|
|
431
|
-
if (f.sideEffectProfile.includes("analytics_event"))
|
|
432
|
-
items.push("SDK event names");
|
|
433
|
-
if (f.sideEffectProfile.includes("booking_mutation")) {
|
|
434
|
-
items.push("booking success response shape", "recurring booking branch");
|
|
435
|
-
}
|
|
436
|
-
if (f.productDomain === "auth_oauth")
|
|
437
|
-
items.push("OAuth callback URLs", "token scopes");
|
|
438
|
-
return items;
|
|
439
|
-
}
|
|
440
|
-
function inferTestProbes(f, writeIntents, observableOutputs) {
|
|
441
|
-
const probes = [];
|
|
442
|
-
if (writeIntents.includes("create_booking")) {
|
|
443
|
-
probes.push({
|
|
444
|
-
name: "standard booking success",
|
|
445
|
-
scenario: "create a standard booking and assert success redirect and booking uid",
|
|
446
|
-
expectedObservable: ["booking_uid", "redirect_url", "sdk_event_name"].filter((o) => observableOutputs.includes(o))
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
if (writeIntents.includes("reschedule_booking")) {
|
|
450
|
-
probes.push({
|
|
451
|
-
name: "reschedule booking",
|
|
452
|
-
scenario: "reschedule an existing booking and assert reschedule event path",
|
|
453
|
-
expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
if (writeIntents.includes("create_recurring_booking")) {
|
|
457
|
-
probes.push({
|
|
458
|
-
name: "recurring booking",
|
|
459
|
-
scenario: "create recurring booking and assert recurring success behavior",
|
|
460
|
-
expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
if (writeIntents.includes("handle_payment_webhook")) {
|
|
464
|
-
probes.push({
|
|
465
|
-
name: "payment webhook ingestion",
|
|
466
|
-
scenario: "send a valid payment webhook and assert booking/payment state updated",
|
|
467
|
-
expectedObservable: ["payment_status", "booking_uid", "http_status"].filter((o) => observableOutputs.includes(o))
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
if (writeIntents.includes("issue_auth_token")) {
|
|
471
|
-
probes.push({
|
|
472
|
-
name: "token issuance",
|
|
473
|
-
scenario: "complete OAuth flow and assert access token issued with correct scopes",
|
|
474
|
-
expectedObservable: ["auth_token", "http_status"].filter((o) => observableOutputs.includes(o))
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
return probes;
|
|
478
|
-
}
|
|
479
|
-
function buildRawEvidence(f) {
|
|
480
|
-
return f.hotSpans.map((span) => ({
|
|
481
|
-
file: f.relativePath,
|
|
482
|
-
startLine: span.startLine,
|
|
483
|
-
endLine: span.endLine,
|
|
484
|
-
rawSourceExcerpt: span.snippet,
|
|
485
|
-
evidenceHash: createHash("sha256").update(span.snippet).digest("hex").slice(0, 12)
|
|
486
|
-
}));
|
|
487
|
-
}
|
|
488
|
-
function deriveConfidence(f) {
|
|
489
|
-
if (f.gravitySignals.fanIn >= 10 && f.gravity >= 40)
|
|
490
|
-
return "high";
|
|
491
|
-
if (f.gravitySignals.fanIn >= 5 || f.gravity >= 25)
|
|
492
|
-
return "medium";
|
|
493
|
-
return "low";
|
|
494
|
-
}
|
|
495
|
-
function validateTarget(target) {
|
|
496
|
-
const warn = (msg) => console.error(`[vibe-splain] WARN ${target.path}: ${msg}`);
|
|
497
|
-
const err = (msg) => console.error(`[vibe-splain] ERR ${target.path}: ${msg}`);
|
|
498
|
-
if (target.severity >= 4 && target.runtimeEntrypoints.length === 0) {
|
|
499
|
-
warn("high severity target has no runtime entrypoints \u2014 check alias resolution");
|
|
500
|
-
}
|
|
501
|
-
if (target.severity === 5 && !target.isLoadBearing) {
|
|
502
|
-
err("severity 5 target must be load bearing");
|
|
503
|
-
}
|
|
504
|
-
if (target.productDomain === "routing_infrastructure" && !target.path.includes("middleware") && !target.path.includes("router") && !target.path.includes("Navigation")) {
|
|
505
|
-
warn("possible over-classification as routing_infrastructure");
|
|
506
|
-
}
|
|
507
|
-
if ((target.path.includes("payment") || target.path.includes("stripe") || target.path.includes("paypal")) && !target.sideEffectProfile.includes("payment_mutation") && !target.sideEffectProfile.includes("webhook_ingress")) {
|
|
508
|
-
warn("payment file missing payment side effect classification");
|
|
509
|
-
}
|
|
510
|
-
if (target.rawEvidence.some((e) => e.rawSourceExcerpt.includes("// ..") || e.rawSourceExcerpt.includes("/* ..."))) {
|
|
511
|
-
err("raw evidence appears summarized or annotated");
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
async function writeDeltaTargets(projectRoot, store, _entrypoints = /* @__PURE__ */ new Set()) {
|
|
515
|
-
const targets = Object.values(store.files).filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity).map((f) => {
|
|
516
|
-
const runtimeEntrypoints = findRuntimeEntrypoints(f.relativePath, store.files);
|
|
517
|
-
const entrypointTraceStatus = deriveEntrypointTraceStatus(runtimeEntrypoints, f.importsUnresolved);
|
|
518
|
-
const loadBearingScore = computeLoadBearingScore(f, runtimeEntrypoints);
|
|
519
|
-
const severity = computeSeverity(f, runtimeEntrypoints);
|
|
520
|
-
const riskTypes = inferRiskTypes(f);
|
|
521
|
-
const observableOutputs = inferObservableOutputs(f);
|
|
522
|
-
const writeIntents = inferWriteIntents(f);
|
|
523
|
-
const patchRisk = inferPatchRisk(f, loadBearingScore, riskTypes);
|
|
524
|
-
const rawEvidence = buildRawEvidence(f);
|
|
525
|
-
const confidence = deriveConfidence(f);
|
|
526
|
-
const fileHashInput = f.hotSpans.map((h) => h.snippet).join("");
|
|
527
|
-
const fileHash = createHash("sha256").update(fileHashInput || f.relativePath).digest("hex").slice(0, 12);
|
|
528
|
-
const target = {
|
|
529
|
-
path: f.relativePath,
|
|
530
|
-
frameworkRole: f.frameworkRole,
|
|
531
|
-
productDomain: f.productDomain,
|
|
532
|
-
gravity: Math.round(f.gravity),
|
|
533
|
-
heat: Math.round(f.heat),
|
|
534
|
-
severity,
|
|
535
|
-
confidence,
|
|
536
|
-
isLoadBearing: loadBearingScore >= 5,
|
|
537
|
-
loadBearingScore,
|
|
538
|
-
riskTypes,
|
|
539
|
-
sideEffectProfile: f.sideEffectProfile,
|
|
540
|
-
blastRadius: f.importedBy,
|
|
541
|
-
runtimeEntrypoints,
|
|
542
|
-
entrypointTraceStatus,
|
|
543
|
-
blockedImports: f.importsUnresolved,
|
|
544
|
-
observableOutputs,
|
|
545
|
-
writeIntents,
|
|
546
|
-
patchRisk,
|
|
547
|
-
safePatchStrategy: inferSafePatchStrategy(f, riskTypes),
|
|
548
|
-
doNotTouch: inferDoNotTouch(f),
|
|
549
|
-
testProbes: inferTestProbes(f, writeIntents, observableOutputs),
|
|
550
|
-
rawEvidence,
|
|
551
|
-
analysisAnnotation: `${f.frameworkRole} in ${f.productDomain} domain. fanIn=${f.gravitySignals.fanIn} cyclomatic=${f.gravitySignals.cyclomatic} loc=${f.gravitySignals.loc}`,
|
|
552
|
-
hashes: {
|
|
553
|
-
fileHash,
|
|
554
|
-
evidenceHash: rawEvidence.map((e) => e.evidenceHash).join("-")
|
|
555
|
-
}
|
|
556
|
-
};
|
|
557
|
-
validateTarget(target);
|
|
558
|
-
return target;
|
|
559
|
-
});
|
|
560
|
-
const dir = join3(projectRoot, ".vibe-splainer");
|
|
561
|
-
await mkdir2(dir, { recursive: true });
|
|
562
|
-
const dest = join3(dir, "delta_targets.json");
|
|
563
|
-
const tmp = dest + ".tmp";
|
|
564
|
-
await writeFile3(tmp, JSON.stringify(targets, null, 2), "utf8");
|
|
565
|
-
const { rename } = await import("fs/promises");
|
|
566
|
-
await rename(tmp, dest);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// ../brain/dist/scanner.js
|
|
570
|
-
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
571
|
-
var require2 = createRequire(import.meta.url);
|
|
572
|
-
var parser = null;
|
|
573
|
-
var langCache = /* @__PURE__ */ new Map();
|
|
574
|
-
var EXT_LANG = {
|
|
575
|
-
".ts": "typescript",
|
|
576
|
-
".tsx": "tsx",
|
|
577
|
-
".js": "javascript",
|
|
578
|
-
".jsx": "tsx",
|
|
579
|
-
".mjs": "javascript",
|
|
580
|
-
".cjs": "javascript",
|
|
581
|
-
".py": "python",
|
|
582
|
-
".go": "go",
|
|
583
|
-
".rs": "rust",
|
|
584
|
-
".java": "java"
|
|
585
|
-
};
|
|
586
|
-
var LANG_WASM = {
|
|
587
|
-
typescript: "tree-sitter-typescript.wasm",
|
|
588
|
-
tsx: "tree-sitter-tsx.wasm",
|
|
589
|
-
javascript: "tree-sitter-javascript.wasm",
|
|
590
|
-
python: "tree-sitter-python.wasm",
|
|
591
|
-
go: "tree-sitter-go.wasm",
|
|
592
|
-
rust: "tree-sitter-rust.wasm",
|
|
593
|
-
java: "tree-sitter-java.wasm"
|
|
594
|
-
};
|
|
595
|
-
var SUPPORTED_EXTENSIONS = new Set(Object.keys(EXT_LANG));
|
|
596
|
-
function resolveWasm(file) {
|
|
597
|
-
try {
|
|
598
|
-
const wasmsDir = dirname(require2.resolve("tree-sitter-wasms/package.json"));
|
|
599
|
-
const p = join4(wasmsDir, "out", file);
|
|
600
|
-
if (existsSync2(p))
|
|
601
|
-
return p;
|
|
602
|
-
} catch {
|
|
603
|
-
}
|
|
604
|
-
const local = join4(__dirname, "../wasm", file);
|
|
605
|
-
return existsSync2(local) ? local : null;
|
|
606
|
-
}
|
|
607
|
-
async function getLanguage(lang) {
|
|
608
|
-
const cached = langCache.get(lang);
|
|
609
|
-
if (cached)
|
|
610
|
-
return cached;
|
|
611
|
-
const wasm = resolveWasm(LANG_WASM[lang]);
|
|
612
|
-
if (!wasm) {
|
|
613
|
-
console.error(`[vibe-splain] grammar missing for ${lang} (${LANG_WASM[lang]}); skipping language`);
|
|
614
|
-
return null;
|
|
615
|
-
}
|
|
616
|
-
try {
|
|
617
|
-
const loaded = await Parser.Language.load(wasm);
|
|
618
|
-
langCache.set(lang, loaded);
|
|
619
|
-
return loaded;
|
|
620
|
-
} catch (err) {
|
|
621
|
-
console.error(`[vibe-splain] failed to load grammar for ${lang}:`, err instanceof Error ? err.message : err);
|
|
622
|
-
return null;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
async function initParser() {
|
|
626
|
-
if (parser)
|
|
627
|
-
return parser;
|
|
628
|
-
await Parser.init();
|
|
629
|
-
parser = new Parser();
|
|
630
|
-
const ts = await getLanguage("typescript");
|
|
631
|
-
if (ts)
|
|
632
|
-
parser.setLanguage(ts);
|
|
633
|
-
return parser;
|
|
634
|
-
}
|
|
635
|
-
async function parseAs(lang, source) {
|
|
636
|
-
const p = await initParser();
|
|
637
|
-
const language = await getLanguage(lang);
|
|
638
|
-
if (!language)
|
|
639
|
-
return null;
|
|
640
|
-
p.setLanguage(language);
|
|
641
|
-
try {
|
|
642
|
-
return p.parse(source);
|
|
643
|
-
} catch {
|
|
644
|
-
return null;
|
|
346
|
+
function inferProductDomain(relPath, importSpecs) {
|
|
347
|
+
const p = relPath.toLowerCase().replace(/\\/g, "/");
|
|
348
|
+
if (/\.test\.|\.spec\.|__tests__|\/e2e\/|\/playwright\/|\/cypress\//.test(p)) {
|
|
349
|
+
return "test_infrastructure";
|
|
350
|
+
}
|
|
351
|
+
if (/\.generated\.|__generated__|\.prisma\//.test(p)) {
|
|
352
|
+
return "generated_noise";
|
|
353
|
+
}
|
|
354
|
+
if (p.includes("booking-audit") || p.includes("bookingaudit"))
|
|
355
|
+
return "booking_audit";
|
|
356
|
+
if (p.includes("bookeventform") || p.includes("availabletimes") || p.includes("availabletimeslots") || p.includes("usebookings") || p.includes("/pages/api/book/") || p.includes("/api/book/") || p.includes("/booking-successful/") || p.includes("/reschedule/") || p.includes("booking-page-wrapper") || p.includes("/book/") && !p.includes("booking-audit"))
|
|
357
|
+
return "booking_creation";
|
|
358
|
+
if (p.includes("modules/bookings") || p.includes("components/booking/actions") || p.includes("/bookings/[status]") || p.includes("/booking/[uid]") || p.includes("/bookings/"))
|
|
359
|
+
return "booking_management";
|
|
360
|
+
if (p.includes("event-types") || p.includes("eventtypes") || p.includes("eventavailabilitytab") || p.includes("eventadvancedtab") || p.includes("eventlimits") || p.includes("eventrecurring"))
|
|
361
|
+
return "event_type_configuration";
|
|
362
|
+
if (p.includes("availability") || p.includes("/schedules/") || p.includes("/slots/")) {
|
|
363
|
+
return "availability";
|
|
645
364
|
}
|
|
365
|
+
if (p.includes("oauth") || p.includes("nextauth") || p.includes("/auth/oauth") || p.includes("/api/auth/") || importSpecs.some((s) => s.includes("arctic") || s.includes("@auth/core")))
|
|
366
|
+
return "auth_oauth";
|
|
367
|
+
if (p.includes("/auth/") || p.includes("signup") || p.includes("login") || p.includes("forgot-password") || p.includes("reset-password") || p.includes("two-factor") || p.includes("verify-email") || importSpecs.some((s) => s.includes("next-auth") || s.includes("@clerk/")))
|
|
368
|
+
return "auth";
|
|
369
|
+
if ((p.includes("stripe") || p.includes("paypal") || p.includes("btcpay") || p.includes("alby") || p.includes("payment")) && (p.includes("webhook") || p.includes("hook")))
|
|
370
|
+
return "payments_webhooks";
|
|
371
|
+
if (p.includes("stripe") || p.includes("paypal") || p.includes("btcpay") || p.includes("alby") || p.includes("payment") || p.includes("billing") || p.includes("checkout") || p.includes("subscription") || importSpecs.some((s) => s.includes("stripe") || s.includes("@stripe/")))
|
|
372
|
+
return "payments";
|
|
373
|
+
if (p.includes("webhook"))
|
|
374
|
+
return "webhooks";
|
|
375
|
+
if (p.includes("app-store") || p.includes("appstore") || p.includes("/apps/") || p.includes("modules/apps"))
|
|
376
|
+
return "apps_marketplace";
|
|
377
|
+
if (p.includes("calendar") || p.includes("selected-calendars") || importSpecs.some((s) => s.includes("googleapis") || s.includes("@google-cloud/")))
|
|
378
|
+
return "calendar_integrations";
|
|
379
|
+
if (p.includes("video") || p.includes("calvideo") || p.includes("daily.co"))
|
|
380
|
+
return "video";
|
|
381
|
+
if (p.includes("onboarding") || p.includes("getting-started"))
|
|
382
|
+
return "onboarding";
|
|
383
|
+
if (p.includes("/settings/") || p.includes("/settings."))
|
|
384
|
+
return "settings";
|
|
385
|
+
if (p.includes("/admin/") || p.includes("/admin."))
|
|
386
|
+
return "admin";
|
|
387
|
+
if (p.includes("data-table") || p.includes("datatable") || p.includes("datasegment") || p.includes("segment"))
|
|
388
|
+
return "data_table";
|
|
389
|
+
if (p.includes("shell/navigation") || p.includes("navigationitem") || p.includes("/shell/") || p.includes("sidebar") || p.includes("topnav") || p.includes("mainnav"))
|
|
390
|
+
return "shell_navigation";
|
|
391
|
+
if (p.includes("form-builder") || p.includes("formbuilder") || p.includes("/forms/") || p.includes("routingforms"))
|
|
392
|
+
return "forms";
|
|
393
|
+
if (p.includes("embed"))
|
|
394
|
+
return "embed";
|
|
395
|
+
if (p.includes("notification") || p.includes("/email/") || p.includes("/emails/") || importSpecs.some((s) => s.includes("nodemailer") || s.includes("resend") || s.includes("@sendgrid/")))
|
|
396
|
+
return "notifications";
|
|
397
|
+
if (p.includes("middleware") && !p.includes("pages/api/") || p.includes("/router.") || p.includes("routerconfig"))
|
|
398
|
+
return "routing_infrastructure";
|
|
399
|
+
return "unknown";
|
|
646
400
|
}
|
|
647
|
-
var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
|
|
648
|
-
"node_modules",
|
|
649
|
-
"dist",
|
|
650
|
-
"build",
|
|
651
|
-
".next",
|
|
652
|
-
"out",
|
|
653
|
-
".vibe-splainer",
|
|
654
|
-
".git",
|
|
655
|
-
".venv",
|
|
656
|
-
"venv",
|
|
657
|
-
"env",
|
|
658
|
-
"__pycache__",
|
|
659
|
-
".idea",
|
|
660
|
-
".vscode",
|
|
661
|
-
".cache",
|
|
662
|
-
"site-packages",
|
|
663
|
-
"target",
|
|
664
|
-
".tox",
|
|
665
|
-
".mypy_cache",
|
|
666
|
-
".pytest_cache"
|
|
667
|
-
]);
|
|
668
|
-
var EXCLUDE_FILE_PATTERNS = [/\.lock$/, /\.min\.[a-z]+$/, /\.d\.ts$/];
|
|
669
|
-
var DEMOTE_SEGMENTS = /* @__PURE__ */ new Set([
|
|
670
|
-
"docs",
|
|
671
|
-
"doc",
|
|
672
|
-
"examples",
|
|
673
|
-
"example",
|
|
674
|
-
"samples",
|
|
675
|
-
"sample",
|
|
676
|
-
"mockup",
|
|
677
|
-
"mockups",
|
|
678
|
-
"fixtures",
|
|
679
|
-
"fixture",
|
|
680
|
-
"__generated__",
|
|
681
|
-
"__mocks__",
|
|
682
|
-
"playwright",
|
|
683
|
-
"e2e",
|
|
684
|
-
"__tests__",
|
|
685
|
-
"cypress",
|
|
686
|
-
"storybook",
|
|
687
|
-
"stories",
|
|
688
|
-
".storybook"
|
|
689
|
-
]);
|
|
690
|
-
var VENDOR_SEGMENTS = /* @__PURE__ */ new Set([
|
|
691
|
-
"node_modules",
|
|
692
|
-
"vendor",
|
|
693
|
-
"vendored",
|
|
694
|
-
"site-packages",
|
|
695
|
-
"third_party",
|
|
696
|
-
"third-party"
|
|
697
|
-
]);
|
|
698
401
|
var PILLAR_KEYWORDS = {
|
|
699
402
|
"Auth": [
|
|
700
403
|
"passport",
|
|
@@ -769,29 +472,9 @@ var PILLAR_KEYWORDS = {
|
|
|
769
472
|
"sharp",
|
|
770
473
|
"imagekit"
|
|
771
474
|
],
|
|
772
|
-
"Config": [
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
"env-var",
|
|
776
|
-
"@t3-oss/env",
|
|
777
|
-
"envalid"
|
|
778
|
-
],
|
|
779
|
-
"Email": [
|
|
780
|
-
"nodemailer",
|
|
781
|
-
"resend",
|
|
782
|
-
"@sendgrid/",
|
|
783
|
-
"postmark",
|
|
784
|
-
"@resend/",
|
|
785
|
-
"mailgun"
|
|
786
|
-
],
|
|
787
|
-
"Realtime": [
|
|
788
|
-
"socket.io",
|
|
789
|
-
"ws",
|
|
790
|
-
"pusher",
|
|
791
|
-
"ably",
|
|
792
|
-
"@supabase/realtime",
|
|
793
|
-
"socket.io-client"
|
|
794
|
-
]
|
|
475
|
+
"Config": ["dotenv", "convict", "env-var", "@t3-oss/env", "envalid"],
|
|
476
|
+
"Email": ["nodemailer", "resend", "@sendgrid/", "postmark", "@resend/", "mailgun"],
|
|
477
|
+
"Realtime": ["socket.io", "ws", "pusher", "ably", "@supabase/realtime", "socket.io-client"]
|
|
795
478
|
};
|
|
796
479
|
var PILLAR_PATH_PATTERNS = {
|
|
797
480
|
"Auth": /(?:^|[\/\\])(?:auth|login|signup|register|session|oauth)(?:[\/\\]|$)/i,
|
|
@@ -817,264 +500,711 @@ var MEANINGLESS_SEGMENTS = /* @__PURE__ */ new Set([
|
|
|
817
500
|
"pkg",
|
|
818
501
|
"packages"
|
|
819
502
|
]);
|
|
820
|
-
function matchPillarByImports(importSpecs) {
|
|
821
|
-
const scores = /* @__PURE__ */ new Map();
|
|
822
|
-
for (const spec of importSpecs) {
|
|
823
|
-
for (const [pillar, keywords] of Object.entries(PILLAR_KEYWORDS)) {
|
|
824
|
-
if (keywords.some((kw) => spec === kw || spec.startsWith(kw + "/"))) {
|
|
825
|
-
scores.set(pillar, (scores.get(pillar) || 0) + 1);
|
|
826
|
-
}
|
|
503
|
+
function matchPillarByImports(importSpecs) {
|
|
504
|
+
const scores = /* @__PURE__ */ new Map();
|
|
505
|
+
for (const spec of importSpecs) {
|
|
506
|
+
for (const [pillar, keywords] of Object.entries(PILLAR_KEYWORDS)) {
|
|
507
|
+
if (keywords.some((kw) => spec === kw || spec.startsWith(kw + "/"))) {
|
|
508
|
+
scores.set(pillar, (scores.get(pillar) || 0) + 1);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (scores.size === 0)
|
|
513
|
+
return null;
|
|
514
|
+
return [...scores.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
515
|
+
}
|
|
516
|
+
function matchPillarByPath(relPath) {
|
|
517
|
+
for (const [pillar, pattern] of Object.entries(PILLAR_PATH_PATTERNS)) {
|
|
518
|
+
if (pattern.test(relPath))
|
|
519
|
+
return pillar;
|
|
520
|
+
}
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
function extractImports(source, lang) {
|
|
524
|
+
const specs = [];
|
|
525
|
+
if (lang === "python") {
|
|
526
|
+
const re2 = /^[ \t]*(?:from[ \t]+([.\w]+)[ \t]+import|import[ \t]+([.\w][.\w ,]*))/gm;
|
|
527
|
+
let m2;
|
|
528
|
+
while ((m2 = re2.exec(source)) !== null) {
|
|
529
|
+
if (m2[1]) {
|
|
530
|
+
specs.push(m2[1]);
|
|
531
|
+
} else if (m2[2]) {
|
|
532
|
+
for (const part of m2[2].split(",")) {
|
|
533
|
+
const name = part.trim().split(/\s+as\s+/)[0].trim();
|
|
534
|
+
if (name)
|
|
535
|
+
specs.push(name);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return specs;
|
|
540
|
+
}
|
|
541
|
+
if (lang === "go") {
|
|
542
|
+
const re2 = /"([^"]+)"/g;
|
|
543
|
+
const importBlock = source.match(/import\s*\(([\s\S]*?)\)/g) || [];
|
|
544
|
+
for (const block of importBlock) {
|
|
545
|
+
let m3;
|
|
546
|
+
while ((m3 = re2.exec(block)) !== null)
|
|
547
|
+
specs.push(m3[1]);
|
|
548
|
+
}
|
|
549
|
+
const single = /import\s+(?:\w+\s+)?"([^"]+)"/g;
|
|
550
|
+
let m2;
|
|
551
|
+
while ((m2 = single.exec(source)) !== null)
|
|
552
|
+
specs.push(m2[1]);
|
|
553
|
+
return specs;
|
|
554
|
+
}
|
|
555
|
+
if (lang === "rust") {
|
|
556
|
+
const re2 = /\b(?:use|mod)\s+([\w:]+)/g;
|
|
557
|
+
let m2;
|
|
558
|
+
while ((m2 = re2.exec(source)) !== null)
|
|
559
|
+
specs.push(m2[1]);
|
|
560
|
+
return specs;
|
|
561
|
+
}
|
|
562
|
+
if (lang === "java") {
|
|
563
|
+
const re2 = /import\s+(?:static\s+)?([\w.]+)/g;
|
|
564
|
+
let m2;
|
|
565
|
+
while ((m2 = re2.exec(source)) !== null)
|
|
566
|
+
specs.push(m2[1]);
|
|
567
|
+
return specs;
|
|
568
|
+
}
|
|
569
|
+
const re = /(?:import|export)\s[^;]*?from\s*['"]([^'"]+)['"]|(?:import|require)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
570
|
+
let m;
|
|
571
|
+
while ((m = re.exec(source)) !== null)
|
|
572
|
+
specs.push(m[1] || m[2]);
|
|
573
|
+
return specs;
|
|
574
|
+
}
|
|
575
|
+
async function detectStackAndEntrypoints(projectRoot, files) {
|
|
576
|
+
const stack = /* @__PURE__ */ new Set();
|
|
577
|
+
const entrypoints = /* @__PURE__ */ new Set();
|
|
578
|
+
const rel = (abs) => relative(projectRoot, abs);
|
|
579
|
+
const pkgPath = join4(projectRoot, "package.json");
|
|
580
|
+
if (existsSync2(pkgPath)) {
|
|
581
|
+
try {
|
|
582
|
+
const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
|
|
583
|
+
stack.add("Node.js");
|
|
584
|
+
const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
|
|
585
|
+
for (const known of ["react", "next", "vue", "svelte", "express", "fastify", "typescript", "vite"]) {
|
|
586
|
+
if (deps[known])
|
|
587
|
+
stack.add(known === "next" ? "Next.js" : known[0].toUpperCase() + known.slice(1));
|
|
588
|
+
}
|
|
589
|
+
const addEntry = (p) => {
|
|
590
|
+
if (!p)
|
|
591
|
+
return;
|
|
592
|
+
const abs = join4(projectRoot, p);
|
|
593
|
+
const r = relative(projectRoot, abs);
|
|
594
|
+
if (files.includes(abs))
|
|
595
|
+
entrypoints.add(r);
|
|
596
|
+
};
|
|
597
|
+
addEntry(pkg.main);
|
|
598
|
+
if (typeof pkg.bin === "string")
|
|
599
|
+
addEntry(pkg.bin);
|
|
600
|
+
else if (pkg.bin)
|
|
601
|
+
for (const v of Object.values(pkg.bin))
|
|
602
|
+
addEntry(v);
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const pyproject = join4(projectRoot, "pyproject.toml");
|
|
607
|
+
const setupPy = join4(projectRoot, "setup.py");
|
|
608
|
+
const requirements = join4(projectRoot, "requirements.txt");
|
|
609
|
+
if (existsSync2(pyproject) || existsSync2(setupPy) || existsSync2(requirements)) {
|
|
610
|
+
stack.add("Python");
|
|
611
|
+
let reqText = "";
|
|
612
|
+
for (const f of [pyproject, requirements]) {
|
|
613
|
+
if (existsSync2(f)) {
|
|
614
|
+
try {
|
|
615
|
+
reqText += await readFile4(f, "utf8");
|
|
616
|
+
} catch {
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
for (const known of ["pygame", "PySide6", "PyQt5", "PyQt6", "flask", "django", "fastapi", "numpy", "pandas", "torch", "tensorflow"]) {
|
|
621
|
+
if (new RegExp(known, "i").test(reqText))
|
|
622
|
+
stack.add(known);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (existsSync2(join4(projectRoot, "go.mod")))
|
|
626
|
+
stack.add("Go");
|
|
627
|
+
if (existsSync2(join4(projectRoot, "Cargo.toml")))
|
|
628
|
+
stack.add("Rust");
|
|
629
|
+
if (existsSync2(join4(projectRoot, "pom.xml")) || existsSync2(join4(projectRoot, "build.gradle")))
|
|
630
|
+
stack.add("Java");
|
|
631
|
+
for (const abs of files) {
|
|
632
|
+
const r = rel(abs);
|
|
633
|
+
const b = basename(r);
|
|
634
|
+
if (b === "main.py" || b === "__main__.py")
|
|
635
|
+
entrypoints.add(r);
|
|
636
|
+
if (/^index\.(ts|tsx|js|jsx|mjs|cjs)$/.test(b) && dirname(r).split(sep).length <= 2)
|
|
637
|
+
entrypoints.add(r);
|
|
638
|
+
if (b === "main.go" && r.includes("cmd" + sep))
|
|
639
|
+
entrypoints.add(r);
|
|
640
|
+
if (b === "main.go" && !r.includes(sep))
|
|
641
|
+
entrypoints.add(r);
|
|
642
|
+
if (b === "main.rs" || b === "lib.rs")
|
|
643
|
+
entrypoints.add(r);
|
|
644
|
+
}
|
|
645
|
+
if (stack.has("Next.js")) {
|
|
646
|
+
const appRouterNames = /* @__PURE__ */ new Set(["page", "layout", "route", "loading", "error", "not-found", "template", "default"]);
|
|
647
|
+
for (const abs of files) {
|
|
648
|
+
const r = rel(abs);
|
|
649
|
+
const stem = basename(r, extname(r));
|
|
650
|
+
if (/(?:^|[/\\])app[/\\]/.test(r) && appRouterNames.has(stem))
|
|
651
|
+
entrypoints.add(r);
|
|
652
|
+
if (/(?:^|[/\\])pages[/\\]/.test(r) && !stem.startsWith("_"))
|
|
653
|
+
entrypoints.add(r);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return { stack: [...stack], entrypoints };
|
|
657
|
+
}
|
|
658
|
+
var FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
659
|
+
"function_declaration",
|
|
660
|
+
"function",
|
|
661
|
+
"function_expression",
|
|
662
|
+
"arrow_function",
|
|
663
|
+
"method_definition",
|
|
664
|
+
"function_definition",
|
|
665
|
+
"method_declaration",
|
|
666
|
+
"func_literal",
|
|
667
|
+
"function_item",
|
|
668
|
+
"closure_expression",
|
|
669
|
+
"constructor_declaration",
|
|
670
|
+
"generator_function_declaration",
|
|
671
|
+
"generator_function"
|
|
672
|
+
]);
|
|
673
|
+
var NESTING_TYPES = /* @__PURE__ */ new Set([
|
|
674
|
+
"function_declaration",
|
|
675
|
+
"function",
|
|
676
|
+
"arrow_function",
|
|
677
|
+
"function_expression",
|
|
678
|
+
"method_definition",
|
|
679
|
+
"function_definition",
|
|
680
|
+
"method_declaration",
|
|
681
|
+
"function_item",
|
|
682
|
+
"class_declaration",
|
|
683
|
+
"class",
|
|
684
|
+
"class_definition",
|
|
685
|
+
"class_item",
|
|
686
|
+
"if_statement",
|
|
687
|
+
"if_expression",
|
|
688
|
+
"for_statement",
|
|
689
|
+
"for_in_statement",
|
|
690
|
+
"for_expression",
|
|
691
|
+
"enhanced_for_statement",
|
|
692
|
+
"while_statement",
|
|
693
|
+
"while_expression",
|
|
694
|
+
"do_statement",
|
|
695
|
+
"switch_statement",
|
|
696
|
+
"match_expression",
|
|
697
|
+
"match_arm",
|
|
698
|
+
"try_statement",
|
|
699
|
+
"catch_clause",
|
|
700
|
+
"except_clause",
|
|
701
|
+
"loop_expression",
|
|
702
|
+
"block"
|
|
703
|
+
]);
|
|
704
|
+
var DECISION_TYPES = /* @__PURE__ */ new Set([
|
|
705
|
+
"if_statement",
|
|
706
|
+
"if_expression",
|
|
707
|
+
"elif_clause",
|
|
708
|
+
"for_statement",
|
|
709
|
+
"for_in_statement",
|
|
710
|
+
"for_expression",
|
|
711
|
+
"enhanced_for_statement",
|
|
712
|
+
"while_statement",
|
|
713
|
+
"while_expression",
|
|
714
|
+
"do_statement",
|
|
715
|
+
"loop_expression",
|
|
716
|
+
"case",
|
|
717
|
+
"switch_case",
|
|
718
|
+
"case_clause",
|
|
719
|
+
"match_arm",
|
|
720
|
+
"catch_clause",
|
|
721
|
+
"except_clause",
|
|
722
|
+
"communication_case",
|
|
723
|
+
"conditional_expression",
|
|
724
|
+
"ternary_expression"
|
|
725
|
+
]);
|
|
726
|
+
var CATCH_TYPES = /* @__PURE__ */ new Set(["catch_clause", "except_clause"]);
|
|
727
|
+
var LONG_FN_LOC = 60;
|
|
728
|
+
var DEEP_NESTING = 5;
|
|
729
|
+
var GOD_FILE_LOC = 400;
|
|
730
|
+
var GOD_FILE_EXPORTS = 8;
|
|
731
|
+
function nodeLOC(node) {
|
|
732
|
+
return node.endPosition.row - node.startPosition.row + 1;
|
|
733
|
+
}
|
|
734
|
+
function countDecisions(node) {
|
|
735
|
+
let count = 0;
|
|
736
|
+
const walk = (n) => {
|
|
737
|
+
if (DECISION_TYPES.has(n.type))
|
|
738
|
+
count++;
|
|
739
|
+
if (n.type === "binary_expression") {
|
|
740
|
+
const op = n.children.find((c) => c.type === "&&" || c.type === "||");
|
|
741
|
+
if (op)
|
|
742
|
+
count++;
|
|
827
743
|
}
|
|
744
|
+
if (n.type === "boolean_operator")
|
|
745
|
+
count++;
|
|
746
|
+
for (const c of n.children)
|
|
747
|
+
walk(c);
|
|
748
|
+
};
|
|
749
|
+
walk(node);
|
|
750
|
+
return count;
|
|
751
|
+
}
|
|
752
|
+
function computeNesting(node, depth) {
|
|
753
|
+
let maxDepth = depth;
|
|
754
|
+
for (const child of node.children) {
|
|
755
|
+
const nextDepth = NESTING_TYPES.has(child.type) ? depth + 1 : depth;
|
|
756
|
+
maxDepth = Math.max(maxDepth, computeNesting(child, nextDepth));
|
|
828
757
|
}
|
|
829
|
-
|
|
830
|
-
return null;
|
|
831
|
-
return [...scores.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
758
|
+
return maxDepth;
|
|
832
759
|
}
|
|
833
|
-
function
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
760
|
+
function firstLine(s) {
|
|
761
|
+
return s.split("\n")[0];
|
|
762
|
+
}
|
|
763
|
+
function stripLeadingComments(snippet) {
|
|
764
|
+
const lines = snippet.split("\n");
|
|
765
|
+
let i = 0;
|
|
766
|
+
let inBlock = false;
|
|
767
|
+
while (i < lines.length) {
|
|
768
|
+
const t = lines[i].trim();
|
|
769
|
+
if (inBlock) {
|
|
770
|
+
if (t.includes("*/"))
|
|
771
|
+
inBlock = false;
|
|
772
|
+
i++;
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
if (t === "") {
|
|
776
|
+
i++;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
if (t.startsWith("//") || t.startsWith("#")) {
|
|
780
|
+
i++;
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
if (t.startsWith("/*")) {
|
|
784
|
+
inBlock = !t.includes("*/");
|
|
785
|
+
i++;
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
if (t.startsWith('"""') || t.startsWith("'''")) {
|
|
789
|
+
const q = t.slice(0, 3);
|
|
790
|
+
if (t.length > 3 && t.endsWith(q)) {
|
|
791
|
+
i++;
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
i++;
|
|
795
|
+
while (i < lines.length && !lines[i].includes(q))
|
|
796
|
+
i++;
|
|
797
|
+
i++;
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
break;
|
|
837
801
|
}
|
|
838
|
-
return
|
|
802
|
+
return lines.slice(i).join("\n");
|
|
839
803
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
if (/(?:^|\/)app\/.*\/loading\.tsx?$/.test(p))
|
|
853
|
-
return "app_loading_boundary";
|
|
854
|
-
if (/(?:^|\/)app\/.*\/error\.tsx?$/.test(p))
|
|
855
|
-
return "app_error_boundary";
|
|
856
|
-
if (/(?:^|\/)pages\/api\/trpc\//.test(p))
|
|
857
|
-
return "trpc_api_route";
|
|
858
|
-
if (/(?:^|\/)pages\/api\//.test(p))
|
|
859
|
-
return "pages_api_route";
|
|
860
|
-
if (/(?:^|\/)pages\//.test(p))
|
|
861
|
-
return "pages_route";
|
|
862
|
-
if (/\/hooks\/|\/use[A-Z][^/]*\.(ts|tsx)$/.test(p))
|
|
863
|
-
return "hook";
|
|
864
|
-
if (/\/stores?\/|[Ss]tore\.(ts|tsx)$/.test(p))
|
|
865
|
-
return "store";
|
|
866
|
-
if (/[Pp]rovider\.(tsx?|jsx?)$|\/providers?\//.test(p))
|
|
867
|
-
return "provider";
|
|
868
|
-
if (/\.types\.ts$|\/types\.ts$|\/types\/[^/]+\.ts$/.test(p))
|
|
869
|
-
return "type_definition";
|
|
870
|
-
if (/\.(tsx|jsx)$/.test(p))
|
|
871
|
-
return "component";
|
|
872
|
-
if (/\.(ts|js|mjs|cjs)$/.test(p))
|
|
873
|
-
return "utility";
|
|
874
|
-
return "unknown";
|
|
804
|
+
var TODO_RE = /\b(TODO|FIXME|HACK|XXX|KLUDGE)\b|@deprecated/;
|
|
805
|
+
var SUPPRESS_RE = /@ts-ignore|@ts-nocheck|eslint-disable|:\s*any\b|#\s*type:\s*ignore|type:\s*ignore|#\s*nosec/;
|
|
806
|
+
function collectFunctionNodes(root) {
|
|
807
|
+
const out = [];
|
|
808
|
+
const walk = (n) => {
|
|
809
|
+
if (FUNCTION_TYPES.has(n.type))
|
|
810
|
+
out.push(n);
|
|
811
|
+
for (const c of n.children)
|
|
812
|
+
walk(c);
|
|
813
|
+
};
|
|
814
|
+
walk(root);
|
|
815
|
+
return out;
|
|
875
816
|
}
|
|
876
|
-
function
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
|
|
817
|
+
function catchIsSwallowed(node) {
|
|
818
|
+
const bodyText = node.text;
|
|
819
|
+
const inner = bodyText.replace(/^[^{:]*[{:]/, "");
|
|
820
|
+
const meaningful = inner.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("#") && l !== "}" && l !== "pass");
|
|
821
|
+
if (meaningful.length === 0)
|
|
822
|
+
return true;
|
|
823
|
+
return meaningful.every((l) => /^(console\.(log|error|warn|info)|print|println!?|System\.out|logger?\.)/.test(l) || l === "pass" || l === "{" || l === "});" || l === ")" || l === "`");
|
|
824
|
+
}
|
|
825
|
+
function collectExports(root, lang) {
|
|
826
|
+
const out = [];
|
|
827
|
+
const seen = /* @__PURE__ */ new Set();
|
|
828
|
+
const push = (name, node) => {
|
|
829
|
+
if (!name || seen.has(name))
|
|
830
|
+
return;
|
|
831
|
+
seen.add(name);
|
|
832
|
+
out.push({ name, text: firstLine(node.text).trim().slice(0, 200) });
|
|
833
|
+
};
|
|
834
|
+
if (lang === "python") {
|
|
835
|
+
for (const c of root.children) {
|
|
836
|
+
if (c.type === "function_definition" || c.type === "class_definition") {
|
|
837
|
+
const name = c.childForFieldName("name")?.text;
|
|
838
|
+
if (name && !name.startsWith("_"))
|
|
839
|
+
push(name, c);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return out;
|
|
880
843
|
}
|
|
881
|
-
if (
|
|
882
|
-
|
|
844
|
+
if (lang === "go") {
|
|
845
|
+
const walk2 = (n) => {
|
|
846
|
+
if (n.type === "function_declaration" || n.type === "method_declaration" || n.type === "type_declaration") {
|
|
847
|
+
const name = n.childForFieldName("name")?.text;
|
|
848
|
+
if (name && /^[A-Z]/.test(name))
|
|
849
|
+
push(name, n);
|
|
850
|
+
}
|
|
851
|
+
for (const c of n.children)
|
|
852
|
+
walk2(c);
|
|
853
|
+
};
|
|
854
|
+
walk2(root);
|
|
855
|
+
return out;
|
|
856
|
+
}
|
|
857
|
+
if (lang === "rust") {
|
|
858
|
+
const walk2 = (n) => {
|
|
859
|
+
if (/_item$/.test(n.type) && n.children.some((c) => c.type === "visibility_modifier")) {
|
|
860
|
+
const name = n.childForFieldName("name")?.text;
|
|
861
|
+
push(name, n);
|
|
862
|
+
}
|
|
863
|
+
for (const c of n.children)
|
|
864
|
+
walk2(c);
|
|
865
|
+
};
|
|
866
|
+
walk2(root);
|
|
867
|
+
return out;
|
|
868
|
+
}
|
|
869
|
+
if (lang === "java") {
|
|
870
|
+
const walk2 = (n) => {
|
|
871
|
+
if ((n.type === "method_declaration" || n.type === "class_declaration") && /\bpublic\b/.test(firstLine(n.text))) {
|
|
872
|
+
const name = n.childForFieldName("name")?.text;
|
|
873
|
+
push(name, n);
|
|
874
|
+
}
|
|
875
|
+
for (const c of n.children)
|
|
876
|
+
walk2(c);
|
|
877
|
+
};
|
|
878
|
+
walk2(root);
|
|
879
|
+
return out;
|
|
883
880
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
if (p.includes("video") || p.includes("calvideo") || p.includes("daily.co"))
|
|
909
|
-
return "video";
|
|
910
|
-
if (p.includes("onboarding") || p.includes("getting-started"))
|
|
911
|
-
return "onboarding";
|
|
912
|
-
if (p.includes("/settings/") || p.includes("/settings."))
|
|
913
|
-
return "settings";
|
|
914
|
-
if (p.includes("/admin/") || p.includes("/admin."))
|
|
915
|
-
return "admin";
|
|
916
|
-
if (p.includes("data-table") || p.includes("datatable") || p.includes("datasegment") || p.includes("segment"))
|
|
917
|
-
return "data_table";
|
|
918
|
-
if (p.includes("shell/navigation") || p.includes("navigationitem") || p.includes("/shell/") || p.includes("sidebar") || p.includes("topnav") || p.includes("mainnav"))
|
|
919
|
-
return "shell_navigation";
|
|
920
|
-
if (p.includes("form-builder") || p.includes("formbuilder") || p.includes("/forms/") || p.includes("routingforms"))
|
|
921
|
-
return "forms";
|
|
922
|
-
if (p.includes("embed"))
|
|
923
|
-
return "embed";
|
|
924
|
-
if (p.includes("notification") || p.includes("/email/") || p.includes("/emails/") || importSpecs.some((s) => s.includes("nodemailer") || s.includes("resend") || s.includes("@sendgrid/")))
|
|
925
|
-
return "notifications";
|
|
926
|
-
if (p.includes("middleware") && !p.includes("pages/api/") || p.includes("/router.") || p.includes("routerconfig"))
|
|
927
|
-
return "routing_infrastructure";
|
|
928
|
-
return "unknown";
|
|
881
|
+
const walk = (n) => {
|
|
882
|
+
if (n.type === "export_statement") {
|
|
883
|
+
const decl = n.childForFieldName("declaration");
|
|
884
|
+
if (decl) {
|
|
885
|
+
const name = decl.childForFieldName("name")?.text;
|
|
886
|
+
if (name)
|
|
887
|
+
push(name, decl);
|
|
888
|
+
for (const c of decl.namedChildren) {
|
|
889
|
+
const dn = c.childForFieldName("name")?.text;
|
|
890
|
+
if (dn)
|
|
891
|
+
push(dn, c);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
for (const spec of n.descendantsOfType("export_specifier")) {
|
|
895
|
+
push(spec.childForFieldName("name")?.text, spec);
|
|
896
|
+
}
|
|
897
|
+
if (n.text.includes("export default"))
|
|
898
|
+
push("default", n);
|
|
899
|
+
}
|
|
900
|
+
for (const c of n.children)
|
|
901
|
+
walk(c);
|
|
902
|
+
};
|
|
903
|
+
walk(root);
|
|
904
|
+
return out;
|
|
929
905
|
}
|
|
930
|
-
function
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
906
|
+
function analyzeAst(source, lang, tree) {
|
|
907
|
+
const root = tree.rootNode;
|
|
908
|
+
const lines = source.split("\n");
|
|
909
|
+
const loc = lines.length;
|
|
910
|
+
const cyclomatic = countDecisions(root);
|
|
911
|
+
const maxNesting = computeNesting(root, 0);
|
|
912
|
+
const smells = [];
|
|
913
|
+
let todos = 0, suppressions = 0;
|
|
914
|
+
for (let i = 0; i < lines.length; i++) {
|
|
915
|
+
const line = lines[i];
|
|
916
|
+
if (TODO_RE.test(line)) {
|
|
917
|
+
todos++;
|
|
918
|
+
smells.push({ kind: "todo", line: i + 1, endLine: i + 1, text: line.trim().slice(0, 200), severity: 2, note: "unfinished / known-bad marker" });
|
|
919
|
+
}
|
|
920
|
+
if (SUPPRESS_RE.test(line)) {
|
|
921
|
+
suppressions++;
|
|
922
|
+
smells.push({ kind: "suppression", line: i + 1, endLine: i + 1, text: line.trim().slice(0, 200), severity: 3, note: "type/lint safety suppressed" });
|
|
923
|
+
}
|
|
934
924
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
925
|
+
let magicNumbers = 0;
|
|
926
|
+
const magicWalk = (n) => {
|
|
927
|
+
if (n.type === "number" || n.type === "integer_literal" || n.type === "float_literal" || n.type === "int_literal") {
|
|
928
|
+
const v = n.text.replace(/_/g, "");
|
|
929
|
+
if (!["0", "1", "2", "-1", "100", "1000"].includes(v) && /^\d{2,}$/.test(v))
|
|
930
|
+
magicNumbers++;
|
|
931
|
+
}
|
|
932
|
+
for (const c of n.children)
|
|
933
|
+
magicWalk(c);
|
|
934
|
+
};
|
|
935
|
+
magicWalk(root);
|
|
936
|
+
if (magicNumbers > 6) {
|
|
937
|
+
smells.push({ kind: "magic-number", line: 1, endLine: 1, text: `${magicNumbers} unexplained numeric literals`, severity: 2, note: "many magic numbers \u2014 extract named constants" });
|
|
943
938
|
}
|
|
944
|
-
|
|
945
|
-
|
|
939
|
+
let swallowedCatches = 0;
|
|
940
|
+
const catchWalk = (n) => {
|
|
941
|
+
if (CATCH_TYPES.has(n.type) && catchIsSwallowed(n)) {
|
|
942
|
+
swallowedCatches++;
|
|
943
|
+
smells.push({
|
|
944
|
+
kind: "swallowed-catch",
|
|
945
|
+
line: n.startPosition.row + 1,
|
|
946
|
+
endLine: n.endPosition.row + 1,
|
|
947
|
+
text: firstLine(n.text).trim().slice(0, 200),
|
|
948
|
+
severity: 4,
|
|
949
|
+
note: "catch block swallows error silently"
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
for (const c of n.children)
|
|
953
|
+
catchWalk(c);
|
|
954
|
+
};
|
|
955
|
+
catchWalk(root);
|
|
956
|
+
const fnNodes = collectFunctionNodes(root);
|
|
957
|
+
let longFunctions = 0;
|
|
958
|
+
const scored = [];
|
|
959
|
+
for (const fn of fnNodes) {
|
|
960
|
+
const bodyLOC = nodeLOC(fn);
|
|
961
|
+
const decisions = countDecisions(fn);
|
|
962
|
+
scored.push({ node: fn, decisions, bodyLOC, score: decisions + bodyLOC });
|
|
963
|
+
if (bodyLOC > LONG_FN_LOC) {
|
|
964
|
+
longFunctions++;
|
|
965
|
+
smells.push({
|
|
966
|
+
kind: "long-function",
|
|
967
|
+
line: fn.startPosition.row + 1,
|
|
968
|
+
endLine: fn.endPosition.row + 1,
|
|
969
|
+
text: firstLine(fn.text).trim().slice(0, 200),
|
|
970
|
+
severity: 3,
|
|
971
|
+
note: `function body is ${bodyLOC} lines`
|
|
972
|
+
});
|
|
973
|
+
}
|
|
946
974
|
}
|
|
947
|
-
if (
|
|
948
|
-
|
|
975
|
+
if (maxNesting > DEEP_NESTING) {
|
|
976
|
+
smells.push({ kind: "deep-nesting", line: 1, endLine: 1, text: `nesting depth ${maxNesting}`, severity: 3, note: `control flow nested ${maxNesting} levels deep` });
|
|
949
977
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
978
|
+
const exported = collectExports(root, lang);
|
|
979
|
+
const publicSurface = exported.length;
|
|
980
|
+
const signature = exported.map((e) => e.text).join("\n").slice(0, 4e3);
|
|
981
|
+
if (loc > GOD_FILE_LOC && publicSurface > GOD_FILE_EXPORTS) {
|
|
982
|
+
smells.push({
|
|
983
|
+
kind: "god-file",
|
|
984
|
+
line: 1,
|
|
985
|
+
endLine: 1,
|
|
986
|
+
text: `${loc} LOC, ${publicSurface} exports`,
|
|
987
|
+
severity: 4,
|
|
988
|
+
note: `god-file: ${loc} lines exporting ${publicSurface} symbols`
|
|
989
|
+
});
|
|
954
990
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
991
|
+
scored.sort((a, b) => b.score - a.score);
|
|
992
|
+
const hotSpans = scored.slice(0, 3).filter((s) => s.bodyLOC >= 4).map((s) => {
|
|
993
|
+
const rawExcerpt = source.split("\n").slice(s.node.startPosition.row, s.node.endPosition.row + 1).join("\n");
|
|
994
|
+
const snippet = stripLeadingComments(rawExcerpt).slice(0, 2e3);
|
|
995
|
+
return {
|
|
996
|
+
startLine: s.node.startPosition.row + 1,
|
|
997
|
+
endLine: s.node.endPosition.row + 1,
|
|
998
|
+
rawExcerpt,
|
|
999
|
+
snippet,
|
|
1000
|
+
reason: `high complexity: ${s.decisions} decision branches across ${s.bodyLOC} lines`
|
|
1001
|
+
};
|
|
1002
|
+
});
|
|
1003
|
+
return {
|
|
1004
|
+
language: lang,
|
|
1005
|
+
loc,
|
|
1006
|
+
cyclomatic,
|
|
1007
|
+
maxNesting,
|
|
1008
|
+
publicSurface,
|
|
1009
|
+
exportedNames: exported.map((e) => e.name),
|
|
1010
|
+
signature,
|
|
1011
|
+
longFunctions,
|
|
1012
|
+
magicNumbers,
|
|
1013
|
+
swallowedCatches,
|
|
1014
|
+
smells,
|
|
1015
|
+
hotSpans
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
var SMELL_WEIGHT = {
|
|
1019
|
+
"todo": 3,
|
|
1020
|
+
"suppression": 5,
|
|
1021
|
+
"swallowed-catch": 10,
|
|
1022
|
+
"deep-nesting": 6,
|
|
1023
|
+
"long-function": 5,
|
|
1024
|
+
"magic-number": 3,
|
|
1025
|
+
"god-file": 14
|
|
1026
|
+
};
|
|
1027
|
+
function computeHeat(smells) {
|
|
1028
|
+
let sum = 0;
|
|
1029
|
+
for (const s of smells)
|
|
1030
|
+
sum += s.severity * SMELL_WEIGHT[s.kind];
|
|
1031
|
+
return Math.min(100, sum);
|
|
1032
|
+
}
|
|
1033
|
+
async function runInventory(projectRoot) {
|
|
1034
|
+
await initParser();
|
|
1035
|
+
const abs = [];
|
|
1036
|
+
await collectFiles(projectRoot, projectRoot, abs);
|
|
1037
|
+
const fileSet = new Set(abs.map((f) => relative(projectRoot, f)));
|
|
1038
|
+
const basenameIndex = /* @__PURE__ */ new Map();
|
|
1039
|
+
for (const rel of fileSet) {
|
|
1040
|
+
const b = basename(rel).slice(0, basename(rel).length - extname(rel).length);
|
|
1041
|
+
if (!basenameIndex.has(b))
|
|
1042
|
+
basenameIndex.set(b, []);
|
|
1043
|
+
basenameIndex.get(b).push(rel);
|
|
959
1044
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1045
|
+
const { stack, entrypoints } = await detectStackAndEntrypoints(projectRoot, abs);
|
|
1046
|
+
const work = [];
|
|
1047
|
+
for (const file of abs) {
|
|
1048
|
+
const rel = relative(projectRoot, file);
|
|
1049
|
+
const ext = extname(file);
|
|
1050
|
+
const lang = EXT_LANG[ext];
|
|
1051
|
+
if (!lang)
|
|
1052
|
+
continue;
|
|
1053
|
+
let source;
|
|
1054
|
+
try {
|
|
1055
|
+
source = await readFile4(file, "utf8");
|
|
1056
|
+
} catch {
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
if (/if\s+__name__\s*==\s*['"]__main__['"]/.test(source) || /^#![^\n]*\b(node|python\d?)\b/.test(source)) {
|
|
1060
|
+
entrypoints.add(rel);
|
|
1061
|
+
}
|
|
1062
|
+
const tree = await parseAs(lang, source);
|
|
1063
|
+
if (!tree)
|
|
1064
|
+
continue;
|
|
1065
|
+
const ast = analyzeAst(source, lang, tree);
|
|
1066
|
+
const importSpecs = extractImports(source, lang);
|
|
1067
|
+
const frameworkRole = inferFrameworkRole(rel);
|
|
1068
|
+
const productDomain = inferProductDomain(rel, importSpecs);
|
|
1069
|
+
work.push({
|
|
1070
|
+
abs: file,
|
|
1071
|
+
rel,
|
|
1072
|
+
lang,
|
|
1073
|
+
source,
|
|
1074
|
+
ast,
|
|
1075
|
+
importSpecs,
|
|
1076
|
+
pathDemote: pathDemoteReason(rel),
|
|
1077
|
+
frameworkRole,
|
|
1078
|
+
productDomain
|
|
1079
|
+
});
|
|
964
1080
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1081
|
+
const dir = join4(projectRoot, ".vibe-splainer");
|
|
1082
|
+
await mkdir3(dir, { recursive: true });
|
|
1083
|
+
const stage01 = {
|
|
1084
|
+
files: work.map((w) => ({
|
|
1085
|
+
absPath: w.abs,
|
|
1086
|
+
relPath: w.rel,
|
|
1087
|
+
language: w.lang,
|
|
1088
|
+
demoteReason: w.pathDemote
|
|
1089
|
+
})),
|
|
1090
|
+
totalCount: work.length,
|
|
1091
|
+
realSourceCount: work.filter((w) => !w.pathDemote).length
|
|
1092
|
+
};
|
|
1093
|
+
await writeFile4(join4(dir, "stage-01-inventory.json"), JSON.stringify(stage01, null, 2), "utf8");
|
|
1094
|
+
const stage02 = Object.fromEntries(work.map((w) => [w.rel, w.frameworkRole]));
|
|
1095
|
+
await writeFile4(join4(dir, "stage-02-framework-roles.json"), JSON.stringify(stage02, null, 2), "utf8");
|
|
1096
|
+
const stage03 = Object.fromEntries(work.map((w) => [w.rel, w.productDomain]));
|
|
1097
|
+
await writeFile4(join4(dir, "stage-03-domains.json"), JSON.stringify(stage03, null, 2), "utf8");
|
|
1098
|
+
return { projectRoot, work, stack, entrypoints, fileSet, basenameIndex };
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// ../brain/dist/pipeline/resolution.js
|
|
1102
|
+
import { join as join5, dirname as dirname2, relative as relative2, extname as extname2, sep as sep2 } from "path";
|
|
1103
|
+
import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
|
|
1104
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1105
|
+
function parseJsonLenient(text) {
|
|
1106
|
+
const stripped = text.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
1107
|
+
try {
|
|
1108
|
+
return JSON.parse(stripped);
|
|
1109
|
+
} catch {
|
|
1110
|
+
return null;
|
|
973
1111
|
}
|
|
974
|
-
if (effects.size === 0)
|
|
975
|
-
effects.add("none_detected");
|
|
976
|
-
return [...effects];
|
|
977
1112
|
}
|
|
978
|
-
async function
|
|
979
|
-
|
|
1113
|
+
async function extractTsConfigPaths(tsconfigPath, projectRoot, depth = 0) {
|
|
1114
|
+
if (depth > 3 || !existsSync3(tsconfigPath))
|
|
1115
|
+
return {};
|
|
1116
|
+
let raw;
|
|
980
1117
|
try {
|
|
981
|
-
|
|
1118
|
+
raw = await readFile5(tsconfigPath, "utf8");
|
|
982
1119
|
} catch {
|
|
983
|
-
return;
|
|
1120
|
+
return {};
|
|
1121
|
+
}
|
|
1122
|
+
const parsed = parseJsonLenient(raw);
|
|
1123
|
+
if (!parsed)
|
|
1124
|
+
return {};
|
|
1125
|
+
const result = {};
|
|
1126
|
+
if (typeof parsed.extends === "string") {
|
|
1127
|
+
const baseFile = join5(dirname2(tsconfigPath), parsed.extends);
|
|
1128
|
+
const base = await extractTsConfigPaths(baseFile, projectRoot, depth + 1);
|
|
1129
|
+
Object.assign(result, base);
|
|
1130
|
+
}
|
|
1131
|
+
const opts = parsed.compilerOptions || {};
|
|
1132
|
+
const baseUrl = typeof opts.baseUrl === "string" ? join5(dirname2(tsconfigPath), opts.baseUrl) : dirname2(tsconfigPath);
|
|
1133
|
+
const paths = opts.paths || {};
|
|
1134
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
1135
|
+
if (!Array.isArray(targets) || targets.length === 0)
|
|
1136
|
+
continue;
|
|
1137
|
+
const first = targets[0].replace(/\/\*$/, "");
|
|
1138
|
+
const resolved = relative2(projectRoot, join5(baseUrl, first));
|
|
1139
|
+
const key = alias.replace(/\/\*$/, "");
|
|
1140
|
+
result[key] = resolved;
|
|
984
1141
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1142
|
+
return result;
|
|
1143
|
+
}
|
|
1144
|
+
async function discoverWorkspacePackages(projectRoot) {
|
|
1145
|
+
const packages = {};
|
|
1146
|
+
const pkgPath = join5(projectRoot, "package.json");
|
|
1147
|
+
if (!existsSync3(pkgPath))
|
|
1148
|
+
return packages;
|
|
1149
|
+
let rootPkg;
|
|
1150
|
+
try {
|
|
1151
|
+
rootPkg = JSON.parse(await readFile5(pkgPath, "utf8"));
|
|
1152
|
+
} catch {
|
|
1153
|
+
return packages;
|
|
1154
|
+
}
|
|
1155
|
+
const workspaces = rootPkg.workspaces;
|
|
1156
|
+
const globs = Array.isArray(workspaces) ? workspaces : Array.isArray(workspaces?.packages) ? workspaces.packages : [];
|
|
1157
|
+
for (const glob of globs) {
|
|
1158
|
+
const prefix = glob.replace(/\/\*$/, "");
|
|
1159
|
+
const absPrefix = join5(projectRoot, prefix);
|
|
1160
|
+
if (!existsSync3(absPrefix))
|
|
991
1161
|
continue;
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1162
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
1163
|
+
let entries = [];
|
|
1164
|
+
try {
|
|
1165
|
+
const dirents = await readdir2(absPrefix, { withFileTypes: true });
|
|
1166
|
+
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1167
|
+
} catch {
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
for (const entry of entries) {
|
|
1171
|
+
const wsPkgPath = join5(absPrefix, entry, "package.json");
|
|
1172
|
+
if (!existsSync3(wsPkgPath))
|
|
998
1173
|
continue;
|
|
999
|
-
|
|
1174
|
+
try {
|
|
1175
|
+
const wsPkg = JSON.parse(await readFile5(wsPkgPath, "utf8"));
|
|
1176
|
+
if (typeof wsPkg.name === "string") {
|
|
1177
|
+
packages[wsPkg.name] = relative2(projectRoot, join5(absPrefix, entry));
|
|
1178
|
+
}
|
|
1179
|
+
} catch {
|
|
1000
1180
|
continue;
|
|
1001
|
-
|
|
1181
|
+
}
|
|
1002
1182
|
}
|
|
1003
1183
|
}
|
|
1184
|
+
return packages;
|
|
1004
1185
|
}
|
|
1005
|
-
function
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
if (
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
return "minified bundle";
|
|
1020
|
-
if (/\.generated\./.test(base))
|
|
1021
|
-
return "generated file";
|
|
1022
|
-
return null;
|
|
1023
|
-
}
|
|
1024
|
-
function extractImports(source, lang) {
|
|
1025
|
-
const specs = [];
|
|
1026
|
-
if (lang === "python") {
|
|
1027
|
-
const re2 = /^[ \t]*(?:from[ \t]+([.\w]+)[ \t]+import|import[ \t]+([.\w][.\w ,]*))/gm;
|
|
1028
|
-
let m2;
|
|
1029
|
-
while ((m2 = re2.exec(source)) !== null) {
|
|
1030
|
-
if (m2[1]) {
|
|
1031
|
-
specs.push(m2[1]);
|
|
1032
|
-
} else if (m2[2]) {
|
|
1033
|
-
for (const part of m2[2].split(",")) {
|
|
1034
|
-
const name = part.trim().split(/\s+as\s+/)[0].trim();
|
|
1035
|
-
if (name)
|
|
1036
|
-
specs.push(name);
|
|
1037
|
-
}
|
|
1186
|
+
async function discoverAppTsConfigPaths(projectRoot) {
|
|
1187
|
+
const result = {};
|
|
1188
|
+
const scanDirs = ["apps", "packages"];
|
|
1189
|
+
for (const scanDir of scanDirs) {
|
|
1190
|
+
const absDir = join5(projectRoot, scanDir);
|
|
1191
|
+
if (!existsSync3(absDir))
|
|
1192
|
+
continue;
|
|
1193
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
1194
|
+
try {
|
|
1195
|
+
const entries = await readdir2(absDir, { withFileTypes: true });
|
|
1196
|
+
for (const entry of entries.filter((e) => e.isDirectory())) {
|
|
1197
|
+
const tsconfig = join5(absDir, entry.name, "tsconfig.json");
|
|
1198
|
+
const paths = await extractTsConfigPaths(tsconfig, projectRoot);
|
|
1199
|
+
Object.assign(result, paths);
|
|
1038
1200
|
}
|
|
1201
|
+
} catch {
|
|
1202
|
+
continue;
|
|
1039
1203
|
}
|
|
1040
|
-
return specs;
|
|
1041
|
-
}
|
|
1042
|
-
if (lang === "go") {
|
|
1043
|
-
const re2 = /"([^"]+)"/g;
|
|
1044
|
-
const importBlock = source.match(/import\s*\(([\s\S]*?)\)/g) || [];
|
|
1045
|
-
for (const block of importBlock) {
|
|
1046
|
-
let m3;
|
|
1047
|
-
while ((m3 = re2.exec(block)) !== null)
|
|
1048
|
-
specs.push(m3[1]);
|
|
1049
|
-
}
|
|
1050
|
-
const single = /import\s+(?:\w+\s+)?"([^"]+)"/g;
|
|
1051
|
-
let m2;
|
|
1052
|
-
while ((m2 = single.exec(source)) !== null)
|
|
1053
|
-
specs.push(m2[1]);
|
|
1054
|
-
return specs;
|
|
1055
|
-
}
|
|
1056
|
-
if (lang === "rust") {
|
|
1057
|
-
const re2 = /\b(?:use|mod)\s+([\w:]+)/g;
|
|
1058
|
-
let m2;
|
|
1059
|
-
while ((m2 = re2.exec(source)) !== null)
|
|
1060
|
-
specs.push(m2[1]);
|
|
1061
|
-
return specs;
|
|
1062
|
-
}
|
|
1063
|
-
if (lang === "java") {
|
|
1064
|
-
const re2 = /import\s+(?:static\s+)?([\w.]+)/g;
|
|
1065
|
-
let m2;
|
|
1066
|
-
while ((m2 = re2.exec(source)) !== null)
|
|
1067
|
-
specs.push(m2[1]);
|
|
1068
|
-
return specs;
|
|
1069
|
-
}
|
|
1070
|
-
const re = /(?:import|export)\s[^;]*?from\s*['"]([^'"]+)['"]|(?:import|require)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
1071
|
-
let m;
|
|
1072
|
-
while ((m = re.exec(source)) !== null) {
|
|
1073
|
-
specs.push(m[1] || m[2]);
|
|
1074
1204
|
}
|
|
1075
|
-
return
|
|
1205
|
+
return result;
|
|
1076
1206
|
}
|
|
1077
|
-
var
|
|
1207
|
+
var CONVENTIONAL_ALIASES = [
|
|
1078
1208
|
{ prefix: "~/", replacement: "" },
|
|
1079
1209
|
{ prefix: "@components/", replacement: "components/" },
|
|
1080
1210
|
{ prefix: "@lib/", replacement: "lib/" },
|
|
@@ -1087,43 +1217,28 @@ var MONOREPO_ALIASES = [
|
|
|
1087
1217
|
{ prefix: "@calcom/ui/", replacement: "../packages/ui/" },
|
|
1088
1218
|
{ prefix: "@calcom/emails/", replacement: "../packages/emails/" }
|
|
1089
1219
|
];
|
|
1090
|
-
function
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1220
|
+
async function buildAliasMap(projectRoot) {
|
|
1221
|
+
const rootPaths = await extractTsConfigPaths(join5(projectRoot, "tsconfig.json"), projectRoot);
|
|
1222
|
+
const workspacePackages = await discoverWorkspacePackages(projectRoot);
|
|
1223
|
+
const appPaths = await discoverAppTsConfigPaths(projectRoot);
|
|
1224
|
+
const resolvedAliases = { ...appPaths, ...rootPaths };
|
|
1225
|
+
for (const [pkgName, pkgDir] of Object.entries(workspacePackages)) {
|
|
1226
|
+
if (!(pkgName in resolvedAliases)) {
|
|
1227
|
+
resolvedAliases[pkgName] = pkgDir;
|
|
1094
1228
|
}
|
|
1095
1229
|
}
|
|
1096
|
-
return
|
|
1230
|
+
return { resolvedAliases, workspacePackages };
|
|
1097
1231
|
}
|
|
1098
1232
|
var JS_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
1099
|
-
function resolveImport(spec, fromAbs, lang, projectRoot, fileSet, basenameIndex) {
|
|
1100
|
-
if (lang === "python") {
|
|
1101
|
-
return { resolved: resolvePython(spec, fromAbs, projectRoot, fileSet), isAlias: false };
|
|
1102
|
-
}
|
|
1103
|
-
if (lang === "typescript" || lang === "tsx" || lang === "javascript") {
|
|
1104
|
-
if (spec.startsWith(".")) {
|
|
1105
|
-
const base = join4(dirname(fromAbs), spec);
|
|
1106
|
-
return { resolved: tryJsCandidates(base, projectRoot, fileSet), isAlias: false };
|
|
1107
|
-
}
|
|
1108
|
-
const aliasResolved = resolveAlias(spec);
|
|
1109
|
-
if (aliasResolved !== null) {
|
|
1110
|
-
const base = join4(projectRoot, aliasResolved);
|
|
1111
|
-
const resolved = tryJsCandidates(base, projectRoot, fileSet);
|
|
1112
|
-
return { resolved, isAlias: true };
|
|
1113
|
-
}
|
|
1114
|
-
return { resolved: null, isAlias: false };
|
|
1115
|
-
}
|
|
1116
|
-
return { resolved: resolveGeneric(spec, projectRoot, fileSet, basenameIndex), isAlias: false };
|
|
1117
|
-
}
|
|
1118
1233
|
function tryJsCandidates(base, projectRoot, fileSet) {
|
|
1119
1234
|
const candidates = [];
|
|
1235
|
+
candidates.unshift(base);
|
|
1120
1236
|
for (const ext of JS_EXTS)
|
|
1121
1237
|
candidates.push(base + ext);
|
|
1122
1238
|
for (const ext of JS_EXTS)
|
|
1123
|
-
candidates.push(
|
|
1124
|
-
candidates.unshift(base);
|
|
1239
|
+
candidates.push(join5(base, "index" + ext));
|
|
1125
1240
|
for (const c of candidates) {
|
|
1126
|
-
const rel =
|
|
1241
|
+
const rel = relative2(projectRoot, c);
|
|
1127
1242
|
if (fileSet.has(rel))
|
|
1128
1243
|
return rel;
|
|
1129
1244
|
}
|
|
@@ -1133,19 +1248,17 @@ function resolvePython(spec, fromAbs, projectRoot, fileSet) {
|
|
|
1133
1248
|
let modulePath;
|
|
1134
1249
|
if (spec.startsWith(".")) {
|
|
1135
1250
|
const dots = spec.match(/^\.+/)[0].length;
|
|
1136
|
-
let dir =
|
|
1251
|
+
let dir = dirname2(fromAbs);
|
|
1137
1252
|
for (let i = 1; i < dots; i++)
|
|
1138
|
-
dir =
|
|
1139
|
-
const rest = spec.slice(dots).replace(/\./g,
|
|
1140
|
-
modulePath = rest ?
|
|
1253
|
+
dir = dirname2(dir);
|
|
1254
|
+
const rest = spec.slice(dots).replace(/\./g, sep2);
|
|
1255
|
+
modulePath = rest ? join5(dir, rest) : dir;
|
|
1141
1256
|
} else {
|
|
1142
|
-
modulePath =
|
|
1257
|
+
modulePath = join5(projectRoot, spec.replace(/\./g, sep2));
|
|
1143
1258
|
}
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
if (fileSet.has(rel))
|
|
1148
|
-
return rel;
|
|
1259
|
+
for (const c of [modulePath + ".py", join5(modulePath, "__init__.py")]) {
|
|
1260
|
+
if (fileSet.has(relative2(projectRoot, c)))
|
|
1261
|
+
return relative2(projectRoot, c);
|
|
1149
1262
|
}
|
|
1150
1263
|
return null;
|
|
1151
1264
|
}
|
|
@@ -1156,8 +1269,8 @@ function resolveGeneric(spec, projectRoot, fileSet, basenameIndex) {
|
|
|
1156
1269
|
return null;
|
|
1157
1270
|
const last = parts[parts.length - 1];
|
|
1158
1271
|
for (const rel of fileSet) {
|
|
1159
|
-
const noExt = rel.slice(0, rel.length -
|
|
1160
|
-
if (noExt.endsWith(parts.join(
|
|
1272
|
+
const noExt = rel.slice(0, rel.length - extname2(rel).length);
|
|
1273
|
+
if (noExt.endsWith(parts.join(sep2)))
|
|
1161
1274
|
return rel;
|
|
1162
1275
|
}
|
|
1163
1276
|
const byBase = basenameIndex.get(last);
|
|
@@ -1165,942 +1278,1365 @@ function resolveGeneric(spec, projectRoot, fileSet, basenameIndex) {
|
|
|
1165
1278
|
return byBase[0];
|
|
1166
1279
|
return null;
|
|
1167
1280
|
}
|
|
1168
|
-
|
|
1169
|
-
"
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
"
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
"do_statement",
|
|
1205
|
-
"switch_statement",
|
|
1206
|
-
"match_expression",
|
|
1207
|
-
"match_arm",
|
|
1208
|
-
"try_statement",
|
|
1209
|
-
"catch_clause",
|
|
1210
|
-
"except_clause",
|
|
1211
|
-
"loop_expression",
|
|
1212
|
-
"block"
|
|
1213
|
-
]);
|
|
1214
|
-
var DECISION_TYPES = /* @__PURE__ */ new Set([
|
|
1215
|
-
"if_statement",
|
|
1216
|
-
"if_expression",
|
|
1217
|
-
"elif_clause",
|
|
1218
|
-
"for_statement",
|
|
1219
|
-
"for_in_statement",
|
|
1220
|
-
"for_expression",
|
|
1221
|
-
"enhanced_for_statement",
|
|
1222
|
-
"while_statement",
|
|
1223
|
-
"while_expression",
|
|
1224
|
-
"do_statement",
|
|
1225
|
-
"loop_expression",
|
|
1226
|
-
"case",
|
|
1227
|
-
"switch_case",
|
|
1228
|
-
"case_clause",
|
|
1229
|
-
"match_arm",
|
|
1230
|
-
"catch_clause",
|
|
1231
|
-
"except_clause",
|
|
1232
|
-
"communication_case",
|
|
1233
|
-
"conditional_expression",
|
|
1234
|
-
"ternary_expression"
|
|
1235
|
-
]);
|
|
1236
|
-
var CATCH_TYPES = /* @__PURE__ */ new Set(["catch_clause", "except_clause"]);
|
|
1237
|
-
var LONG_FN_LOC = 60;
|
|
1238
|
-
var DEEP_NESTING = 5;
|
|
1239
|
-
var GOD_FILE_LOC = 400;
|
|
1240
|
-
var GOD_FILE_EXPORTS = 8;
|
|
1241
|
-
function nodeLOC(node) {
|
|
1242
|
-
return node.endPosition.row - node.startPosition.row + 1;
|
|
1281
|
+
function resolveImportWithAliasMap(spec, fromAbs, lang, projectRoot, fileSet, basenameIndex, aliasMap) {
|
|
1282
|
+
if (lang === "python") {
|
|
1283
|
+
return { resolved: resolvePython(spec, fromAbs, projectRoot, fileSet), isAlias: false };
|
|
1284
|
+
}
|
|
1285
|
+
if (lang === "typescript" || lang === "tsx" || lang === "javascript") {
|
|
1286
|
+
if (spec.startsWith(".")) {
|
|
1287
|
+
const base = join5(dirname2(fromAbs), spec);
|
|
1288
|
+
return { resolved: tryJsCandidates(base, projectRoot, fileSet), isAlias: false };
|
|
1289
|
+
}
|
|
1290
|
+
for (const [prefix, replacement] of Object.entries(aliasMap.resolvedAliases)) {
|
|
1291
|
+
if (spec === prefix || spec.startsWith(prefix + "/")) {
|
|
1292
|
+
const rest = spec.slice(prefix.length).replace(/^\//, "");
|
|
1293
|
+
const base = join5(projectRoot, replacement, rest);
|
|
1294
|
+
const resolved = tryJsCandidates(base, projectRoot, fileSet);
|
|
1295
|
+
return { resolved, isAlias: true, reason: resolved ? void 0 : `alias '${prefix}' found but path '${replacement}/${rest}' not in file set` };
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
for (const [pkgName, pkgDir] of Object.entries(aliasMap.workspacePackages)) {
|
|
1299
|
+
if (spec === pkgName || spec.startsWith(pkgName + "/")) {
|
|
1300
|
+
const rest = spec.slice(pkgName.length).replace(/^\//, "");
|
|
1301
|
+
const base = join5(projectRoot, pkgDir, rest);
|
|
1302
|
+
const resolved = tryJsCandidates(base, projectRoot, fileSet);
|
|
1303
|
+
return { resolved, isAlias: true, reason: resolved ? void 0 : `workspace package '${pkgName}' found but subpath '${rest}' not in file set` };
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
for (const { prefix, replacement } of CONVENTIONAL_ALIASES) {
|
|
1307
|
+
if (spec.startsWith(prefix)) {
|
|
1308
|
+
const rest = replacement + spec.slice(prefix.length);
|
|
1309
|
+
const base = join5(projectRoot, rest);
|
|
1310
|
+
const resolved = tryJsCandidates(base, projectRoot, fileSet);
|
|
1311
|
+
return { resolved, isAlias: true, reason: resolved ? void 0 : `conventional alias '${prefix}' \u2192 path not found` };
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return { resolved: null, isAlias: false };
|
|
1315
|
+
}
|
|
1316
|
+
return { resolved: resolveGeneric(spec, projectRoot, fileSet, basenameIndex), isAlias: false };
|
|
1243
1317
|
}
|
|
1244
|
-
function
|
|
1245
|
-
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1318
|
+
async function runResolution(projectRoot, inv) {
|
|
1319
|
+
const aliasMap = await buildAliasMap(projectRoot);
|
|
1320
|
+
const { work, fileSet, basenameIndex } = inv;
|
|
1321
|
+
const importedBy = /* @__PURE__ */ new Map();
|
|
1322
|
+
const importsResolved = /* @__PURE__ */ new Map();
|
|
1323
|
+
const importsUnresolved = /* @__PURE__ */ new Map();
|
|
1324
|
+
const fanOut = /* @__PURE__ */ new Map();
|
|
1325
|
+
for (const w of work) {
|
|
1326
|
+
importedBy.set(w.rel, /* @__PURE__ */ new Set());
|
|
1327
|
+
importsResolved.set(w.rel, /* @__PURE__ */ new Set());
|
|
1328
|
+
importsUnresolved.set(w.rel, /* @__PURE__ */ new Set());
|
|
1329
|
+
}
|
|
1330
|
+
const graph = { nodes: {}, edges: [] };
|
|
1331
|
+
for (const w of work) {
|
|
1332
|
+
graph.nodes[w.rel] = { imports: w.importSpecs };
|
|
1333
|
+
}
|
|
1334
|
+
const resolutionFailuresByFile = {};
|
|
1335
|
+
const resolutionFailureReasons = {};
|
|
1336
|
+
const unresolvedSet = /* @__PURE__ */ new Set();
|
|
1337
|
+
for (const w of work) {
|
|
1338
|
+
const distinctModules = /* @__PURE__ */ new Set();
|
|
1339
|
+
for (const spec of w.importSpecs) {
|
|
1340
|
+
distinctModules.add(spec);
|
|
1341
|
+
const { resolved, isAlias, reason } = resolveImportWithAliasMap(spec, w.abs, w.lang, projectRoot, fileSet, basenameIndex, aliasMap);
|
|
1342
|
+
if (resolved && resolved !== w.rel && importedBy.has(resolved)) {
|
|
1343
|
+
importedBy.get(resolved).add(w.rel);
|
|
1344
|
+
importsResolved.get(w.rel).add(resolved);
|
|
1345
|
+
graph.edges.push({ from: w.rel, to: resolved });
|
|
1346
|
+
} else if (resolved === null && isAlias) {
|
|
1347
|
+
importsUnresolved.get(w.rel).add(spec);
|
|
1348
|
+
unresolvedSet.add(spec);
|
|
1349
|
+
if (reason) {
|
|
1350
|
+
if (!resolutionFailuresByFile[w.rel])
|
|
1351
|
+
resolutionFailuresByFile[w.rel] = [];
|
|
1352
|
+
resolutionFailuresByFile[w.rel].push(spec);
|
|
1353
|
+
if (!resolutionFailureReasons[spec])
|
|
1354
|
+
resolutionFailureReasons[spec] = reason;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1253
1357
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1358
|
+
fanOut.set(w.rel, distinctModules.size);
|
|
1359
|
+
}
|
|
1360
|
+
const unresolvedImports = [...unresolvedSet];
|
|
1361
|
+
const dir = join5(projectRoot, ".vibe-splainer");
|
|
1362
|
+
await mkdir4(dir, { recursive: true });
|
|
1363
|
+
const stage04 = {
|
|
1364
|
+
resolvedAliases: aliasMap.resolvedAliases,
|
|
1365
|
+
workspacePackages: aliasMap.workspacePackages,
|
|
1366
|
+
unresolvedImports,
|
|
1367
|
+
resolutionFailuresByFile,
|
|
1368
|
+
resolutionFailureReasons
|
|
1369
|
+
};
|
|
1370
|
+
await writeFile5(join5(dir, "stage-04-aliases.json"), JSON.stringify(stage04, null, 2), "utf8");
|
|
1371
|
+
return {
|
|
1372
|
+
aliasMap,
|
|
1373
|
+
importedBy,
|
|
1374
|
+
importsResolved,
|
|
1375
|
+
importsUnresolved,
|
|
1376
|
+
fanOut,
|
|
1377
|
+
graph,
|
|
1378
|
+
unresolvedImports,
|
|
1379
|
+
resolutionFailuresByFile,
|
|
1380
|
+
resolutionFailureReasons
|
|
1258
1381
|
};
|
|
1259
|
-
walk(node);
|
|
1260
|
-
return count;
|
|
1261
1382
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1383
|
+
|
|
1384
|
+
// ../brain/dist/pipeline/classification.js
|
|
1385
|
+
import { join as join6, basename as basename2, extname as extname3, sep as sep3 } from "path";
|
|
1386
|
+
import { writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
|
|
1387
|
+
function inferSideEffectProfile(source, importSpecs, productDomain, frameworkRole) {
|
|
1388
|
+
const effects = /* @__PURE__ */ new Set();
|
|
1389
|
+
if (/router\.(push|replace|back)\(|redirect\(|notFound\(|permanentRedirect\(/.test(source)) {
|
|
1390
|
+
effects.add("redirect");
|
|
1267
1391
|
}
|
|
1268
|
-
|
|
1392
|
+
if (/["']use server["']/.test(source))
|
|
1393
|
+
effects.add("server_action");
|
|
1394
|
+
if (/useMutation\b|\.mutate\b|\.mutateAsync\b/.test(source))
|
|
1395
|
+
effects.add("trpc_mutation");
|
|
1396
|
+
if (/sdkActionManager\.fire|telemetry\.|posthog\.|mixpanel\.|amplitude\.|ga\(/.test(source) || importSpecs.some((s) => /analytics|telemetry|posthog|mixpanel|amplitude/.test(s)))
|
|
1397
|
+
effects.add("analytics_event");
|
|
1398
|
+
if (/prisma\s*[.?]\s*\w+\s*[.?]\s*(create|update|upsert|delete|deleteMany|updateMany|createMany|transaction|executeRaw|queryRaw)\b/.test(source)) {
|
|
1399
|
+
effects.add("database_write");
|
|
1400
|
+
}
|
|
1401
|
+
if (/prisma\s*[.?]\s*\w+\s*[.?]\s*(findMany|findUnique|findFirst|findFirstOrThrow|findUniqueOrThrow|count|aggregate|groupBy)\b/.test(source)) {
|
|
1402
|
+
effects.add("database_read");
|
|
1403
|
+
}
|
|
1404
|
+
if (/createBooking|handleNewBooking|cancelBooking|rescheduleBooking|handleBooking|createRecurring/.test(source) || productDomain === "booking_creation" && /useMutation\b|\.mutate\b|\.mutateAsync\b/.test(source))
|
|
1405
|
+
effects.add("booking_mutation");
|
|
1406
|
+
if (/stripe\.webhooks\.(constructEvent|constructEventAsync)|webhookSecret|validateWebhook|verifyWebhook|verifySignature/.test(source) || productDomain === "payments_webhooks" && frameworkRole === "pages_api_route")
|
|
1407
|
+
effects.add("webhook_ingress");
|
|
1408
|
+
if (importSpecs.some((s) => /stripe|paypal|btcpay|alby/.test(s.toLowerCase())) || /stripe\.|paymentIntent|createPaymentIntent|confirmPayment|createCharge/.test(source) || productDomain === "payments_webhooks" && effects.has("webhook_ingress"))
|
|
1409
|
+
effects.add("payment_mutation");
|
|
1410
|
+
if (/signIn\b|signOut\b|createSession|destroySession|issueToken|refreshToken|getToken/.test(source)) {
|
|
1411
|
+
effects.add("auth_token_mutation");
|
|
1412
|
+
}
|
|
1413
|
+
if (/triggerWebhook|sendWebhook|webhook\.send\b/.test(source))
|
|
1414
|
+
effects.add("webhook_delivery");
|
|
1415
|
+
if (/sendEmail|sendMail\b|mailer\./.test(source) || importSpecs.some((s) => /nodemailer|resend|sendgrid|postmark|mailgun/.test(s)))
|
|
1416
|
+
effects.add("email_send");
|
|
1417
|
+
if (/createCalendarEvent|updateCalendarEvent|deleteCalendarEvent|calendar\.events\.(insert|update|delete|patch)/.test(source)) {
|
|
1418
|
+
effects.add("calendar_mutation");
|
|
1419
|
+
}
|
|
1420
|
+
if (/revalidatePath\b|revalidateTag\b/.test(source))
|
|
1421
|
+
effects.add("cache_revalidation");
|
|
1422
|
+
if (/localStorage\.|sessionStorage\./.test(source))
|
|
1423
|
+
effects.add("local_storage");
|
|
1424
|
+
if (/indexedDB\b|new Dexie|idb\./.test(source))
|
|
1425
|
+
effects.add("indexed_db");
|
|
1426
|
+
if (/\bfetch\s*\(|axios\.(get|post|put|patch|delete)\b/.test(source)) {
|
|
1427
|
+
effects.add("external_api_call");
|
|
1428
|
+
}
|
|
1429
|
+
if (effects.size === 0)
|
|
1430
|
+
effects.add("none_detected");
|
|
1431
|
+
return [...effects];
|
|
1269
1432
|
}
|
|
1270
|
-
function
|
|
1271
|
-
|
|
1433
|
+
function inferWriteIntents(productDomain, relPath, sideEffectProfile) {
|
|
1434
|
+
const intents = [];
|
|
1435
|
+
if (productDomain === "booking_creation") {
|
|
1436
|
+
intents.push("create_booking");
|
|
1437
|
+
if (relPath.includes("reschedule") || relPath.includes("Reschedule"))
|
|
1438
|
+
intents.push("reschedule_booking");
|
|
1439
|
+
if (relPath.includes("recurring") || relPath.includes("Recurring"))
|
|
1440
|
+
intents.push("create_recurring_booking");
|
|
1441
|
+
}
|
|
1442
|
+
if (productDomain === "booking_management")
|
|
1443
|
+
intents.push("cancel_booking");
|
|
1444
|
+
if (productDomain === "event_type_configuration")
|
|
1445
|
+
intents.push("update_event_type");
|
|
1446
|
+
if (productDomain === "availability")
|
|
1447
|
+
intents.push("update_availability");
|
|
1448
|
+
if (productDomain === "payments")
|
|
1449
|
+
intents.push("create_payment");
|
|
1450
|
+
if (productDomain === "payments_webhooks")
|
|
1451
|
+
intents.push("handle_payment_webhook");
|
|
1452
|
+
if (productDomain === "auth_oauth") {
|
|
1453
|
+
intents.push("issue_auth_token");
|
|
1454
|
+
intents.push("refresh_auth_token");
|
|
1455
|
+
}
|
|
1456
|
+
if (sideEffectProfile.includes("webhook_delivery"))
|
|
1457
|
+
intents.push("send_webhook");
|
|
1458
|
+
if (productDomain === "settings")
|
|
1459
|
+
intents.push("update_user_settings");
|
|
1460
|
+
if (sideEffectProfile.includes("local_storage") || sideEffectProfile.includes("indexed_db")) {
|
|
1461
|
+
intents.push("persist_local_state");
|
|
1462
|
+
}
|
|
1463
|
+
return intents.length > 0 ? intents : ["none_detected"];
|
|
1272
1464
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1465
|
+
var ENTRYPOINT_ROLES = /* @__PURE__ */ new Set([
|
|
1466
|
+
"app_route_page",
|
|
1467
|
+
"app_route_handler",
|
|
1468
|
+
"pages_route",
|
|
1469
|
+
"pages_api_route",
|
|
1470
|
+
"trpc_api_route"
|
|
1471
|
+
]);
|
|
1472
|
+
function inferRiskTypesPass1(rel, frameworkRole, productDomain, sideEffectProfile, gravitySignals, smellKinds) {
|
|
1473
|
+
const types = [];
|
|
1474
|
+
const smThreshold = ["provider", "store"].includes(frameworkRole) ? 8 : 20;
|
|
1475
|
+
if (gravitySignals.cyclomatic > smThreshold)
|
|
1476
|
+
types.push("state_machine");
|
|
1477
|
+
if (smellKinds.has("god-file")) {
|
|
1478
|
+
if (frameworkRole === "hook")
|
|
1479
|
+
types.push("god_hook");
|
|
1480
|
+
else
|
|
1481
|
+
types.push("god_component");
|
|
1482
|
+
}
|
|
1483
|
+
if (sideEffectProfile.length > 3 && !sideEffectProfile.includes("none_detected")) {
|
|
1484
|
+
types.push("side_effect_coupling");
|
|
1485
|
+
}
|
|
1486
|
+
if (productDomain === "forms" && (gravitySignals.fanIn > 3 || gravitySignals.publicSurface > 5))
|
|
1487
|
+
types.push("registry_bottleneck");
|
|
1488
|
+
if (sideEffectProfile.some((s) => ["booking_mutation", "payment_mutation", "auth_token_mutation"].includes(s)) && gravitySignals.cyclomatic > 10)
|
|
1489
|
+
types.push("mutation_orchestration");
|
|
1490
|
+
if (ENTRYPOINT_ROLES.has(frameworkRole) && sideEffectProfile.includes("database_write")) {
|
|
1491
|
+
types.push("route_handler_write_path");
|
|
1492
|
+
}
|
|
1493
|
+
if (smellKinds.has("swallowed-catch"))
|
|
1494
|
+
types.push("error_swallowing");
|
|
1495
|
+
if (sideEffectProfile.includes("local_storage") || sideEffectProfile.includes("indexed_db")) {
|
|
1496
|
+
types.push("storage_persistence_risk");
|
|
1497
|
+
}
|
|
1498
|
+
return types;
|
|
1499
|
+
}
|
|
1500
|
+
var DOMAIN_SURFACE_PATTERNS = {
|
|
1501
|
+
booking_creation: {
|
|
1502
|
+
expected: [/book/i, /booking/i, /reschedule/i, /booking-success/i, /api\/book/i, /create-booking/i],
|
|
1503
|
+
wrong: [/event-type/i, /event-types/i, /eventtypes/i, /availability/i, /schedule/i]
|
|
1504
|
+
},
|
|
1505
|
+
payments_webhooks: {
|
|
1506
|
+
expected: [/webhook/i, /stripe/i, /payment/i],
|
|
1507
|
+
wrong: [/settings/i, /onboarding/i, /profile/i]
|
|
1508
|
+
},
|
|
1509
|
+
auth_oauth: {
|
|
1510
|
+
expected: [/oauth/i, /callback/i, /auth/i, /signin/i, /login/i],
|
|
1511
|
+
wrong: [/booking/i, /payment/i, /settings/i]
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
function findRuntimeEntrypoints(relPath, importedByMap, persisted, maxDepth = 8) {
|
|
1515
|
+
const results = [];
|
|
1516
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1517
|
+
const queue = [{ path: relPath, depth: 0 }];
|
|
1518
|
+
while (queue.length > 0) {
|
|
1519
|
+
const current = queue.shift();
|
|
1520
|
+
if (seen.has(current.path))
|
|
1283
1521
|
continue;
|
|
1522
|
+
seen.add(current.path);
|
|
1523
|
+
if (current.path !== relPath) {
|
|
1524
|
+
const meta = persisted.get(current.path);
|
|
1525
|
+
if (meta && ENTRYPOINT_ROLES.has(meta.frameworkRole)) {
|
|
1526
|
+
results.push({
|
|
1527
|
+
path: current.path,
|
|
1528
|
+
frameworkRole: meta.frameworkRole,
|
|
1529
|
+
productDomain: meta.productDomain,
|
|
1530
|
+
distance: current.depth
|
|
1531
|
+
});
|
|
1532
|
+
if (results.length >= 8)
|
|
1533
|
+
break;
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1284
1536
|
}
|
|
1285
|
-
if (
|
|
1286
|
-
i++;
|
|
1537
|
+
if (current.depth >= maxDepth)
|
|
1287
1538
|
continue;
|
|
1288
|
-
|
|
1289
|
-
if (
|
|
1290
|
-
i++;
|
|
1539
|
+
const importers = importedByMap.get(current.path);
|
|
1540
|
+
if (!importers)
|
|
1291
1541
|
continue;
|
|
1542
|
+
for (const importer of importers) {
|
|
1543
|
+
if (!seen.has(importer))
|
|
1544
|
+
queue.push({ path: importer, depth: current.depth + 1 });
|
|
1292
1545
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1546
|
+
}
|
|
1547
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
1548
|
+
for (const r of results) {
|
|
1549
|
+
const existing = byPath.get(r.path);
|
|
1550
|
+
if (!existing || r.distance < existing.distance)
|
|
1551
|
+
byPath.set(r.path, r);
|
|
1552
|
+
}
|
|
1553
|
+
return [...byPath.values()].sort((a, b) => a.distance - b.distance);
|
|
1554
|
+
}
|
|
1555
|
+
function deriveEntrypointTraceStatus(domain, entrypoints, unresolved) {
|
|
1556
|
+
if (entrypoints.length === 0 && unresolved.length > 0)
|
|
1557
|
+
return "blocked_by_alias_resolution";
|
|
1558
|
+
if (entrypoints.length === 0)
|
|
1559
|
+
return "no_runtime_entrypoint_found";
|
|
1560
|
+
const patterns = DOMAIN_SURFACE_PATTERNS[domain];
|
|
1561
|
+
if (patterns) {
|
|
1562
|
+
const allWrong = entrypoints.every((e) => patterns.wrong.some((p) => p.test(e.path)) && !patterns.expected.some((p) => p.test(e.path)));
|
|
1563
|
+
if (allWrong)
|
|
1564
|
+
return "partial_wrong_surface";
|
|
1565
|
+
}
|
|
1566
|
+
return unresolved.length === 0 ? "complete" : "partial";
|
|
1567
|
+
}
|
|
1568
|
+
function computeLoadBearingScore(gravity, heat, importedByCount, sideEffectProfile, productDomain, smellMaxSeverity, runtimeEntrypoints) {
|
|
1569
|
+
let score = 0;
|
|
1570
|
+
if (gravity >= 85)
|
|
1571
|
+
score += 2;
|
|
1572
|
+
if (heat >= 60)
|
|
1573
|
+
score += 1;
|
|
1574
|
+
if (runtimeEntrypoints.length >= 2)
|
|
1575
|
+
score += 2;
|
|
1576
|
+
if (importedByCount >= 3)
|
|
1577
|
+
score += 1;
|
|
1578
|
+
if (sideEffectProfile.includes("database_write"))
|
|
1579
|
+
score += 3;
|
|
1580
|
+
if (sideEffectProfile.includes("booking_mutation"))
|
|
1581
|
+
score += 3;
|
|
1582
|
+
if (sideEffectProfile.includes("payment_mutation"))
|
|
1583
|
+
score += 3;
|
|
1584
|
+
if (sideEffectProfile.includes("auth_token_mutation"))
|
|
1585
|
+
score += 3;
|
|
1586
|
+
if (sideEffectProfile.includes("webhook_delivery"))
|
|
1587
|
+
score += 2;
|
|
1588
|
+
if (sideEffectProfile.includes("webhook_ingress"))
|
|
1589
|
+
score += 2;
|
|
1590
|
+
if (sideEffectProfile.includes("calendar_mutation"))
|
|
1591
|
+
score += 2;
|
|
1592
|
+
if (sideEffectProfile.includes("redirect"))
|
|
1593
|
+
score += 1;
|
|
1594
|
+
if (sideEffectProfile.includes("analytics_event"))
|
|
1595
|
+
score += 1;
|
|
1596
|
+
const highImpactDomains = [
|
|
1597
|
+
"booking_creation",
|
|
1598
|
+
"payments",
|
|
1599
|
+
"auth_oauth",
|
|
1600
|
+
"webhooks",
|
|
1601
|
+
"payments_webhooks"
|
|
1602
|
+
];
|
|
1603
|
+
if (highImpactDomains.includes(productDomain))
|
|
1604
|
+
score += 2;
|
|
1605
|
+
if (smellMaxSeverity === 5)
|
|
1606
|
+
score += 3;
|
|
1607
|
+
return score;
|
|
1608
|
+
}
|
|
1609
|
+
function pageRank(nodes, outEdges, damping = 0.85, iters = 20) {
|
|
1610
|
+
const n = nodes.length;
|
|
1611
|
+
const rank = /* @__PURE__ */ new Map();
|
|
1612
|
+
if (n === 0)
|
|
1613
|
+
return rank;
|
|
1614
|
+
for (const node of nodes)
|
|
1615
|
+
rank.set(node, 1 / n);
|
|
1616
|
+
const inEdges = /* @__PURE__ */ new Map();
|
|
1617
|
+
for (const node of nodes)
|
|
1618
|
+
inEdges.set(node, []);
|
|
1619
|
+
const outCount = /* @__PURE__ */ new Map();
|
|
1620
|
+
for (const [from, tos] of outEdges) {
|
|
1621
|
+
const valid = [...tos].filter((t) => rank.has(t));
|
|
1622
|
+
outCount.set(from, valid.length);
|
|
1623
|
+
for (const to of valid)
|
|
1624
|
+
inEdges.get(to).push(from);
|
|
1625
|
+
}
|
|
1626
|
+
for (let it = 0; it < iters; it++) {
|
|
1627
|
+
const next = /* @__PURE__ */ new Map();
|
|
1628
|
+
let dangling = 0;
|
|
1629
|
+
for (const node of nodes) {
|
|
1630
|
+
if ((outCount.get(node) || 0) === 0)
|
|
1631
|
+
dangling += rank.get(node);
|
|
1297
1632
|
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1633
|
+
for (const node of nodes) {
|
|
1634
|
+
let sum = 0;
|
|
1635
|
+
for (const from of inEdges.get(node)) {
|
|
1636
|
+
sum += rank.get(from) / (outCount.get(from) || 1);
|
|
1637
|
+
}
|
|
1638
|
+
next.set(node, (1 - damping) / n + damping * (sum + dangling / n));
|
|
1639
|
+
}
|
|
1640
|
+
for (const node of nodes)
|
|
1641
|
+
rank.set(node, next.get(node));
|
|
1642
|
+
}
|
|
1643
|
+
let max = 0;
|
|
1644
|
+
for (const v of rank.values())
|
|
1645
|
+
max = Math.max(max, v);
|
|
1646
|
+
if (max > 0)
|
|
1647
|
+
for (const node of nodes)
|
|
1648
|
+
rank.set(node, rank.get(node) / max);
|
|
1649
|
+
return rank;
|
|
1650
|
+
}
|
|
1651
|
+
function detectCommunities(nodes, adjacency) {
|
|
1652
|
+
const label = /* @__PURE__ */ new Map();
|
|
1653
|
+
nodes.forEach((node, i) => label.set(node, i));
|
|
1654
|
+
for (let pass = 0; pass < 10; pass++) {
|
|
1655
|
+
let changed = false;
|
|
1656
|
+
for (const node of nodes) {
|
|
1657
|
+
const neighbors = adjacency.get(node);
|
|
1658
|
+
if (!neighbors || neighbors.size === 0)
|
|
1302
1659
|
continue;
|
|
1660
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1661
|
+
for (const [nb, weight] of neighbors) {
|
|
1662
|
+
const l = label.get(nb);
|
|
1663
|
+
counts.set(l, (counts.get(l) || 0) + weight);
|
|
1664
|
+
}
|
|
1665
|
+
let best = label.get(node), bestCount = -1;
|
|
1666
|
+
for (const [l, c] of counts) {
|
|
1667
|
+
if (c > bestCount || c === bestCount && l < best) {
|
|
1668
|
+
best = l;
|
|
1669
|
+
bestCount = c;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
if (best !== label.get(node)) {
|
|
1673
|
+
label.set(node, best);
|
|
1674
|
+
changed = true;
|
|
1303
1675
|
}
|
|
1304
|
-
i++;
|
|
1305
|
-
while (i < lines.length && !lines[i].includes(q))
|
|
1306
|
-
i++;
|
|
1307
|
-
i++;
|
|
1308
|
-
continue;
|
|
1309
1676
|
}
|
|
1310
|
-
|
|
1677
|
+
if (!changed)
|
|
1678
|
+
break;
|
|
1311
1679
|
}
|
|
1312
|
-
return
|
|
1680
|
+
return label;
|
|
1313
1681
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
function collectFunctionNodes(root) {
|
|
1317
|
-
const out = [];
|
|
1318
|
-
const walk = (n) => {
|
|
1319
|
-
if (FUNCTION_TYPES.has(n.type))
|
|
1320
|
-
out.push(n);
|
|
1321
|
-
for (const c of n.children)
|
|
1322
|
-
walk(c);
|
|
1323
|
-
};
|
|
1324
|
-
walk(root);
|
|
1325
|
-
return out;
|
|
1682
|
+
function titleCase(s) {
|
|
1683
|
+
return s.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1326
1684
|
}
|
|
1327
|
-
function
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1685
|
+
function domainToGroupLabel(domain) {
|
|
1686
|
+
const labels = {
|
|
1687
|
+
booking_creation: "Booking",
|
|
1688
|
+
booking_management: "Booking",
|
|
1689
|
+
booking_audit: "Booking Audit",
|
|
1690
|
+
event_type_configuration: "Event Types",
|
|
1691
|
+
availability: "Availability",
|
|
1692
|
+
auth: "Auth",
|
|
1693
|
+
auth_oauth: "Auth OAuth",
|
|
1694
|
+
payments: "Payments",
|
|
1695
|
+
payments_webhooks: "Payment Webhooks",
|
|
1696
|
+
webhooks: "Webhooks",
|
|
1697
|
+
apps_marketplace: "Apps",
|
|
1698
|
+
calendar_integrations: "Calendar",
|
|
1699
|
+
video: "Video",
|
|
1700
|
+
onboarding: "Onboarding",
|
|
1701
|
+
settings: "Settings",
|
|
1702
|
+
admin: "Admin",
|
|
1703
|
+
data_table: "Data Table",
|
|
1704
|
+
shell_navigation: "Shell",
|
|
1705
|
+
forms: "Forms",
|
|
1706
|
+
embed: "Embed",
|
|
1707
|
+
notifications: "Notifications"
|
|
1708
|
+
};
|
|
1709
|
+
return labels[domain] || titleCase(domain.replace(/_/g, " "));
|
|
1335
1710
|
}
|
|
1336
|
-
function
|
|
1337
|
-
const
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
const maxNesting = computeNesting(root, 0);
|
|
1342
|
-
const smells = [];
|
|
1343
|
-
let todos = 0, suppressions = 0;
|
|
1344
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1345
|
-
const line = lines[i];
|
|
1346
|
-
if (TODO_RE.test(line)) {
|
|
1347
|
-
todos++;
|
|
1348
|
-
smells.push({ kind: "todo", line: i + 1, endLine: i + 1, text: line.trim().slice(0, 200), severity: 2, note: "unfinished / known-bad marker" });
|
|
1711
|
+
function pillarNameFromCluster(files) {
|
|
1712
|
+
const hintCounts = /* @__PURE__ */ new Map();
|
|
1713
|
+
for (const f of files) {
|
|
1714
|
+
if (f.pillarHint && !f.pillarHint.startsWith("community-")) {
|
|
1715
|
+
hintCounts.set(f.pillarHint, (hintCounts.get(f.pillarHint) || 0) + 1);
|
|
1349
1716
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1717
|
+
}
|
|
1718
|
+
if (hintCounts.size > 0) {
|
|
1719
|
+
const best = [...hintCounts.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
1720
|
+
if (best[1] >= files.length * 0.4)
|
|
1721
|
+
return best[0];
|
|
1722
|
+
}
|
|
1723
|
+
const dirs = files.map((f) => dirname_simple(f.rel)).filter((d) => d && d !== ".");
|
|
1724
|
+
if (dirs.length) {
|
|
1725
|
+
const segCounts = /* @__PURE__ */ new Map();
|
|
1726
|
+
for (const d of dirs) {
|
|
1727
|
+
const segments = d.split(sep3).filter((s) => !MEANINGLESS_SEGMENTS.has(s.toLowerCase()));
|
|
1728
|
+
const meaningful = segments.pop();
|
|
1729
|
+
if (meaningful)
|
|
1730
|
+
segCounts.set(meaningful, (segCounts.get(meaningful) || 0) + 1);
|
|
1353
1731
|
}
|
|
1732
|
+
const top = [...segCounts.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
1733
|
+
if (top)
|
|
1734
|
+
return titleCase(top[0]);
|
|
1354
1735
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1736
|
+
const topFile = basename2(files[0].rel, extname3(files[0].rel));
|
|
1737
|
+
return titleCase(topFile);
|
|
1738
|
+
}
|
|
1739
|
+
function dirname_simple(p) {
|
|
1740
|
+
const idx = p.lastIndexOf(sep3);
|
|
1741
|
+
if (idx < 0)
|
|
1742
|
+
return ".";
|
|
1743
|
+
return p.slice(0, idx);
|
|
1744
|
+
}
|
|
1745
|
+
function buildPillars(classified, communities) {
|
|
1746
|
+
const real = classified.filter((f) => f.isRealSource);
|
|
1747
|
+
const keywordGroups = /* @__PURE__ */ new Map();
|
|
1748
|
+
const unlabeled = [];
|
|
1749
|
+
for (const f of real) {
|
|
1750
|
+
if (f.pillarHint && !f.pillarHint.startsWith("community-")) {
|
|
1751
|
+
if (!keywordGroups.has(f.pillarHint))
|
|
1752
|
+
keywordGroups.set(f.pillarHint, []);
|
|
1753
|
+
keywordGroups.get(f.pillarHint).push(f);
|
|
1754
|
+
} else {
|
|
1755
|
+
unlabeled.push(f);
|
|
1362
1756
|
}
|
|
1363
|
-
for (const c of n.children)
|
|
1364
|
-
magicWalk(c);
|
|
1365
|
-
};
|
|
1366
|
-
magicWalk(root);
|
|
1367
|
-
if (magicNumbers > 6) {
|
|
1368
|
-
smells.push({ kind: "magic-number", line: 1, endLine: 1, text: `${magicNumbers} unexplained numeric literals`, severity: 2, note: "many magic numbers \u2014 extract named constants" });
|
|
1369
1757
|
}
|
|
1370
|
-
|
|
1371
|
-
const
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1758
|
+
const pillars = [];
|
|
1759
|
+
for (const [name, files] of keywordGroups) {
|
|
1760
|
+
const sorted = [...files].sort((a, b) => b.gravity - a.gravity);
|
|
1761
|
+
pillars.push({
|
|
1762
|
+
name,
|
|
1763
|
+
description: `${name} subsystem: ${files.length} file${files.length > 1 ? "s" : ""} centered on ${basename2(sorted[0].rel)}.`,
|
|
1764
|
+
memberFiles: sorted.map((f) => f.rel)
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
if (unlabeled.length > 0) {
|
|
1768
|
+
const communityGroups = /* @__PURE__ */ new Map();
|
|
1769
|
+
for (const f of unlabeled) {
|
|
1770
|
+
const c = communities.get(f.rel);
|
|
1771
|
+
if (c === void 0)
|
|
1772
|
+
continue;
|
|
1773
|
+
if (!communityGroups.has(c))
|
|
1774
|
+
communityGroups.set(c, []);
|
|
1775
|
+
communityGroups.get(c).push(f);
|
|
1382
1776
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
endLine: fn.endPosition.row + 1,
|
|
1400
|
-
text: firstLine(fn.text).trim().slice(0, 200),
|
|
1401
|
-
severity: 3,
|
|
1402
|
-
note: `function body is ${bodyLOC} lines`
|
|
1403
|
-
});
|
|
1777
|
+
const remainingSlots = Math.max(0, 6 - pillars.length);
|
|
1778
|
+
const sorted = [...communityGroups.entries()].map(([id, files]) => ({ id, files, weight: files.reduce((s, f) => s + f.gravity, 0) })).filter((g) => g.files.length >= 2).sort((a, b) => b.weight - a.weight).slice(0, remainingSlots);
|
|
1779
|
+
for (const g of sorted) {
|
|
1780
|
+
const top = [...g.files].sort((a, b) => b.gravity - a.gravity);
|
|
1781
|
+
const name = pillarNameFromCluster(top.map((f) => ({ rel: f.rel, pillarHint: f.pillarHint })));
|
|
1782
|
+
const existing = pillars.find((p) => p.name === name);
|
|
1783
|
+
if (existing) {
|
|
1784
|
+
existing.memberFiles.push(...top.map((f) => f.rel));
|
|
1785
|
+
existing.description = `${name} subsystem: ${existing.memberFiles.length} files.`;
|
|
1786
|
+
} else {
|
|
1787
|
+
pillars.push({
|
|
1788
|
+
name,
|
|
1789
|
+
description: `${g.files.length} files centered on ${basename2(top[0].rel)}.`,
|
|
1790
|
+
memberFiles: top.map((f) => f.rel)
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1404
1793
|
}
|
|
1405
1794
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1795
|
+
pillars.sort((a, b) => {
|
|
1796
|
+
const gravA = real.filter((f) => a.memberFiles.includes(f.rel)).reduce((s, f) => s + f.gravity, 0);
|
|
1797
|
+
const gravB = real.filter((f) => b.memberFiles.includes(f.rel)).reduce((s, f) => s + f.gravity, 0);
|
|
1798
|
+
return gravB - gravA;
|
|
1799
|
+
});
|
|
1800
|
+
if (pillars.length === 0 && real.length > 0) {
|
|
1801
|
+
pillars.push({ name: "Core", description: "Primary application code.", memberFiles: real.slice(0, 20).map((f) => f.rel) });
|
|
1408
1802
|
}
|
|
1409
|
-
const
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1803
|
+
const finalPillars = [];
|
|
1804
|
+
for (const p of pillars) {
|
|
1805
|
+
if (p.memberFiles.length > 15) {
|
|
1806
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1807
|
+
for (const rel of p.memberFiles) {
|
|
1808
|
+
const f = classified.find((c) => c.rel === rel);
|
|
1809
|
+
const role = f?.frameworkRole || "unknown";
|
|
1810
|
+
const domain = f?.productDomain || "unknown";
|
|
1811
|
+
let bucket;
|
|
1812
|
+
if (domain !== "unknown" && domain !== "routing_infrastructure" && domain !== "test_infrastructure" && domain !== "generated_noise") {
|
|
1813
|
+
bucket = domainToGroupLabel(domain);
|
|
1814
|
+
} else if (role === "hook") {
|
|
1815
|
+
bucket = "Hooks";
|
|
1816
|
+
} else if (["app_route_page", "app_route_handler", "app_route_layout", "pages_route", "pages_api_route", "trpc_api_route"].includes(role)) {
|
|
1817
|
+
bucket = "Routes";
|
|
1818
|
+
} else if (role === "component") {
|
|
1819
|
+
bucket = "Components";
|
|
1820
|
+
} else {
|
|
1821
|
+
bucket = "Logic";
|
|
1822
|
+
}
|
|
1823
|
+
const key = `${p.name} (${bucket})`;
|
|
1824
|
+
if (!groups.has(key))
|
|
1825
|
+
groups.set(key, []);
|
|
1826
|
+
groups.get(key).push(rel);
|
|
1827
|
+
}
|
|
1828
|
+
for (const [key, files] of groups) {
|
|
1829
|
+
if (files.length > 0)
|
|
1830
|
+
finalPillars.push({ name: key, description: `Subdivided from ${p.name}`, memberFiles: files });
|
|
1831
|
+
}
|
|
1832
|
+
} else {
|
|
1833
|
+
finalPillars.push(p);
|
|
1834
|
+
}
|
|
1421
1835
|
}
|
|
1422
|
-
scored.sort((a, b) => b.score - a.score);
|
|
1423
|
-
const hotSpans = scored.slice(0, 3).filter((s) => s.bodyLOC >= 4).map((s) => {
|
|
1424
|
-
const raw = source.split("\n").slice(s.node.startPosition.row, s.node.endPosition.row + 1).join("\n");
|
|
1425
|
-
const snippet = stripLeadingComments(raw).slice(0, 2e3);
|
|
1426
|
-
return {
|
|
1427
|
-
startLine: s.node.startPosition.row + 1,
|
|
1428
|
-
endLine: s.node.endPosition.row + 1,
|
|
1429
|
-
snippet,
|
|
1430
|
-
reason: `high complexity: ${s.decisions} decision branches across ${s.bodyLOC} lines`
|
|
1431
|
-
};
|
|
1432
|
-
});
|
|
1433
|
-
return {
|
|
1434
|
-
language: lang,
|
|
1435
|
-
loc,
|
|
1436
|
-
cyclomatic,
|
|
1437
|
-
maxNesting,
|
|
1438
|
-
publicSurface,
|
|
1439
|
-
exportedNames: exported.map((e) => e.name),
|
|
1440
|
-
signature,
|
|
1441
|
-
longFunctions,
|
|
1442
|
-
magicNumbers,
|
|
1443
|
-
swallowedCatches,
|
|
1444
|
-
smells,
|
|
1445
|
-
hotSpans
|
|
1446
|
-
};
|
|
1447
|
-
}
|
|
1448
|
-
function collectExports(root, lang) {
|
|
1449
|
-
const out = [];
|
|
1450
1836
|
const seen = /* @__PURE__ */ new Set();
|
|
1451
|
-
const
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1837
|
+
for (const p of finalPillars) {
|
|
1838
|
+
let n = p.name, i = 2;
|
|
1839
|
+
while (seen.has(n)) {
|
|
1840
|
+
n = `${p.name} ${i++}`;
|
|
1841
|
+
}
|
|
1842
|
+
p.name = n;
|
|
1843
|
+
seen.add(n);
|
|
1844
|
+
}
|
|
1845
|
+
return finalPillars;
|
|
1846
|
+
}
|
|
1847
|
+
async function runClassification(projectRoot, inv, res) {
|
|
1848
|
+
const { work, entrypoints } = inv;
|
|
1849
|
+
const { importedBy, importsResolved, importsUnresolved, fanOut } = res;
|
|
1850
|
+
const isRealSource = /* @__PURE__ */ new Map();
|
|
1851
|
+
const demoteReason = /* @__PURE__ */ new Map();
|
|
1852
|
+
for (const w of work) {
|
|
1853
|
+
if (w.pathDemote) {
|
|
1854
|
+
isRealSource.set(w.rel, false);
|
|
1855
|
+
demoteReason.set(w.rel, w.pathDemote);
|
|
1856
|
+
} else {
|
|
1857
|
+
isRealSource.set(w.rel, true);
|
|
1858
|
+
demoteReason.set(w.rel, null);
|
|
1464
1859
|
}
|
|
1465
|
-
return out;
|
|
1466
1860
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
}
|
|
1477
|
-
walk2(root);
|
|
1478
|
-
return out;
|
|
1861
|
+
for (const w of work) {
|
|
1862
|
+
if (!isRealSource.get(w.rel))
|
|
1863
|
+
continue;
|
|
1864
|
+
if (entrypoints.has(w.rel))
|
|
1865
|
+
continue;
|
|
1866
|
+
const inbound = [...importedBy.get(w.rel) || []].filter((src) => isRealSource.get(src));
|
|
1867
|
+
if (inbound.length === 0) {
|
|
1868
|
+
isRealSource.set(w.rel, false);
|
|
1869
|
+
demoteReason.set(w.rel, "no inbound references from application code");
|
|
1870
|
+
}
|
|
1479
1871
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
walk2(c);
|
|
1488
|
-
};
|
|
1489
|
-
walk2(root);
|
|
1490
|
-
return out;
|
|
1872
|
+
const realNodes = work.filter((w) => isRealSource.get(w.rel)).map((w) => w.rel);
|
|
1873
|
+
const realSet = new Set(realNodes);
|
|
1874
|
+
const outEdges = /* @__PURE__ */ new Map();
|
|
1875
|
+
const undirected = /* @__PURE__ */ new Map();
|
|
1876
|
+
for (const node of realNodes) {
|
|
1877
|
+
outEdges.set(node, /* @__PURE__ */ new Set());
|
|
1878
|
+
undirected.set(node, /* @__PURE__ */ new Map());
|
|
1491
1879
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1880
|
+
for (const w of work) {
|
|
1881
|
+
if (!realSet.has(w.rel))
|
|
1882
|
+
continue;
|
|
1883
|
+
for (const target of importsResolved.get(w.rel) || /* @__PURE__ */ new Set()) {
|
|
1884
|
+
if (!realSet.has(target))
|
|
1885
|
+
continue;
|
|
1886
|
+
outEdges.get(w.rel).add(target);
|
|
1887
|
+
const wDir = w.rel.split(sep3)[0];
|
|
1888
|
+
const tDir = target.split(sep3)[0];
|
|
1889
|
+
const weight = wDir === tDir ? 1 : 0.5;
|
|
1890
|
+
undirected.get(w.rel).set(target, weight);
|
|
1891
|
+
undirected.get(target).set(w.rel, weight);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
const ranks = pageRank(realNodes, outEdges);
|
|
1895
|
+
const communities = detectCommunities(realNodes, undirected);
|
|
1896
|
+
const metaLookup = /* @__PURE__ */ new Map();
|
|
1897
|
+
const sideEffectsByFile = /* @__PURE__ */ new Map();
|
|
1898
|
+
const writeIntentsByFile = /* @__PURE__ */ new Map();
|
|
1899
|
+
for (const w of work) {
|
|
1900
|
+
const effects = inferSideEffectProfile(w.source, w.importSpecs, w.productDomain, w.frameworkRole);
|
|
1901
|
+
sideEffectsByFile.set(w.rel, effects);
|
|
1902
|
+
writeIntentsByFile.set(w.rel, inferWriteIntents(w.productDomain, w.rel, effects));
|
|
1903
|
+
metaLookup.set(w.rel, { frameworkRole: w.frameworkRole, productDomain: w.productDomain });
|
|
1904
|
+
}
|
|
1905
|
+
const riskTypesByFile = /* @__PURE__ */ new Map();
|
|
1906
|
+
const gravityByFile = /* @__PURE__ */ new Map();
|
|
1907
|
+
const heatByFile = /* @__PURE__ */ new Map();
|
|
1908
|
+
const fanInByFile = /* @__PURE__ */ new Map();
|
|
1909
|
+
const centralityByFile = /* @__PURE__ */ new Map();
|
|
1910
|
+
const gravitySignalsByFile = /* @__PURE__ */ new Map();
|
|
1911
|
+
for (const w of work) {
|
|
1912
|
+
const real = isRealSource.get(w.rel);
|
|
1913
|
+
const fanIn = [...importedBy.get(w.rel) || []].filter((src) => isRealSource.get(src)).length;
|
|
1914
|
+
const centrality = real ? ranks.get(w.rel) || 0 : 0;
|
|
1915
|
+
const gs = {
|
|
1916
|
+
fanIn,
|
|
1917
|
+
fanOut: fanOut.get(w.rel) || 0,
|
|
1918
|
+
centrality,
|
|
1919
|
+
cyclomatic: w.ast.cyclomatic,
|
|
1920
|
+
publicSurface: w.ast.publicSurface,
|
|
1921
|
+
loc: w.ast.loc
|
|
1500
1922
|
};
|
|
1501
|
-
|
|
1502
|
-
|
|
1923
|
+
const depthRatio = (w.ast.cyclomatic + w.ast.maxNesting * 2) / Math.max(1, w.ast.publicSurface);
|
|
1924
|
+
const depthFactor = Math.min(1, Math.log2(depthRatio + 1) / 3);
|
|
1925
|
+
const adjustedCentrality = centrality * (0.3 + 0.7 * depthFactor);
|
|
1926
|
+
let gravityRaw = adjustedCentrality * 50 + Math.log2(fanIn + 1) * 6 + Math.log2(w.ast.cyclomatic + 1) * 7 + Math.log2(w.ast.publicSurface + 1) * 2 + (w.ast.maxNesting >= 4 ? 5 : 0);
|
|
1927
|
+
if (!real)
|
|
1928
|
+
gravityRaw *= 0.2;
|
|
1929
|
+
const gravity = Math.max(0, Math.min(100, gravityRaw));
|
|
1930
|
+
const heat = real ? computeHeat(w.ast.smells) : 0;
|
|
1931
|
+
gravityByFile.set(w.rel, gravity);
|
|
1932
|
+
heatByFile.set(w.rel, heat);
|
|
1933
|
+
fanInByFile.set(w.rel, fanIn);
|
|
1934
|
+
centralityByFile.set(w.rel, centrality);
|
|
1935
|
+
gravitySignalsByFile.set(w.rel, gs);
|
|
1503
1936
|
}
|
|
1504
|
-
const
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1937
|
+
for (const w of work) {
|
|
1938
|
+
const gs = gravitySignalsByFile.get(w.rel);
|
|
1939
|
+
const smellKinds = new Set(w.ast.smells.map((s) => s.kind));
|
|
1940
|
+
const effects = sideEffectsByFile.get(w.rel);
|
|
1941
|
+
const types = inferRiskTypesPass1(w.rel, w.frameworkRole, w.productDomain, effects, gs, smellKinds);
|
|
1942
|
+
riskTypesByFile.set(w.rel, types);
|
|
1943
|
+
}
|
|
1944
|
+
for (const w of work) {
|
|
1945
|
+
if (w.productDomain === "forms" && (w.frameworkRole === "component" || w.frameworkRole === "hook")) {
|
|
1946
|
+
const importsResolved_w = importsResolved.get(w.rel) || /* @__PURE__ */ new Set();
|
|
1947
|
+
const importsAny = [...importsResolved_w, ...w.importSpecs.filter((s) => s.startsWith("@"))];
|
|
1948
|
+
const consumesBottleneck = importsAny.some((dep) => {
|
|
1949
|
+
const types2 = riskTypesByFile.get(dep);
|
|
1950
|
+
return types2?.includes("registry_bottleneck");
|
|
1951
|
+
});
|
|
1952
|
+
if (consumesBottleneck) {
|
|
1953
|
+
const existing = riskTypesByFile.get(w.rel);
|
|
1954
|
+
if (!existing.includes("registry_consumer"))
|
|
1955
|
+
existing.push("registry_consumer");
|
|
1956
|
+
if (!existing.includes("type_boundary_leak"))
|
|
1957
|
+
existing.push("type_boundary_leak");
|
|
1958
|
+
const idx = existing.indexOf("complexity_hotspot");
|
|
1959
|
+
if (idx >= 0)
|
|
1960
|
+
existing.splice(idx, 1);
|
|
1519
1961
|
}
|
|
1520
|
-
if (n.text.includes("export default"))
|
|
1521
|
-
push("default", n);
|
|
1522
1962
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1963
|
+
const types = riskTypesByFile.get(w.rel);
|
|
1964
|
+
if (types.length === 0)
|
|
1965
|
+
types.push("complexity_hotspot");
|
|
1966
|
+
}
|
|
1967
|
+
const classified = [];
|
|
1968
|
+
for (const w of work) {
|
|
1969
|
+
const real = isRealSource.get(w.rel);
|
|
1970
|
+
const fanIn = fanInByFile.get(w.rel);
|
|
1971
|
+
const gravity = gravityByFile.get(w.rel);
|
|
1972
|
+
const heat = heatByFile.get(w.rel);
|
|
1973
|
+
const gs = gravitySignalsByFile.get(w.rel);
|
|
1974
|
+
const effects = sideEffectsByFile.get(w.rel);
|
|
1975
|
+
const writeIntents = writeIntentsByFile.get(w.rel);
|
|
1976
|
+
const riskTypes = riskTypesByFile.get(w.rel);
|
|
1977
|
+
const hs = {
|
|
1978
|
+
todos: w.ast.smells.filter((s) => s.kind === "todo").length,
|
|
1979
|
+
suppressions: w.ast.smells.filter((s) => s.kind === "suppression").length,
|
|
1980
|
+
swallowedCatches: w.ast.swallowedCatches,
|
|
1981
|
+
maxNesting: w.ast.maxNesting,
|
|
1982
|
+
longFunctions: w.ast.longFunctions,
|
|
1983
|
+
magicNumbers: w.ast.magicNumbers
|
|
1984
|
+
};
|
|
1985
|
+
const keywordPillar = matchPillarByImports(w.importSpecs);
|
|
1986
|
+
const pathPillar = matchPillarByPath(w.rel);
|
|
1987
|
+
const pillarHint = real ? keywordPillar || pathPillar || `community-${communities.get(w.rel)}` : null;
|
|
1988
|
+
const importedByReal = [...importedBy.get(w.rel) || []].filter((src) => isRealSource.get(src));
|
|
1989
|
+
const imports = [...importsResolved.get(w.rel) || /* @__PURE__ */ new Set()];
|
|
1990
|
+
const importsUnresolvedArr = [...importsUnresolved.get(w.rel) || /* @__PURE__ */ new Set()];
|
|
1991
|
+
const runtimeEntrypoints = findRuntimeEntrypoints(w.rel, importedBy, metaLookup);
|
|
1992
|
+
const entrypointTraceStatus = deriveEntrypointTraceStatus(w.productDomain, runtimeEntrypoints, importsUnresolvedArr);
|
|
1993
|
+
const smellMaxSeverity = w.ast.smells.length > 0 ? Math.max(...w.ast.smells.map((s) => s.severity)) : 0;
|
|
1994
|
+
const loadBearingScore = computeLoadBearingScore(gravity, heat, fanIn, effects, w.productDomain, smellMaxSeverity, runtimeEntrypoints);
|
|
1995
|
+
classified.push({
|
|
1996
|
+
rel: w.rel,
|
|
1997
|
+
abs: w.abs,
|
|
1998
|
+
lang: w.lang,
|
|
1999
|
+
isRealSource: real,
|
|
2000
|
+
demoteReason: demoteReason.get(w.rel) || null,
|
|
2001
|
+
gravity,
|
|
2002
|
+
heat,
|
|
2003
|
+
gravitySignals: gs,
|
|
2004
|
+
heatSignals: hs,
|
|
2005
|
+
smells: w.ast.smells,
|
|
2006
|
+
pillarHint,
|
|
2007
|
+
importedBy: importedByReal,
|
|
2008
|
+
imports,
|
|
2009
|
+
importsUnresolved: importsUnresolvedArr,
|
|
2010
|
+
frameworkRole: w.frameworkRole,
|
|
2011
|
+
productDomain: w.productDomain,
|
|
2012
|
+
sideEffectProfile: effects,
|
|
2013
|
+
writeIntents,
|
|
2014
|
+
riskTypes,
|
|
2015
|
+
runtimeEntrypoints,
|
|
2016
|
+
entrypointTraceStatus,
|
|
2017
|
+
blockedImports: importsUnresolvedArr,
|
|
2018
|
+
loadBearingScore,
|
|
2019
|
+
hotSpans: w.ast.hotSpans,
|
|
2020
|
+
source: w.source
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
const dir = join6(projectRoot, ".vibe-splainer");
|
|
2024
|
+
await mkdir5(dir, { recursive: true });
|
|
2025
|
+
const stage05 = Object.fromEntries(classified.map((f) => [f.rel, f.sideEffectProfile]));
|
|
2026
|
+
await writeFile6(join6(dir, "stage-05-side-effects.json"), JSON.stringify(stage05, null, 2), "utf8");
|
|
2027
|
+
const stage06 = Object.fromEntries(classified.map((f) => [f.rel, f.writeIntents]));
|
|
2028
|
+
await writeFile6(join6(dir, "stage-06-write-intents.json"), JSON.stringify(stage06, null, 2), "utf8");
|
|
2029
|
+
const stage07 = Object.fromEntries(classified.map((f) => [f.rel, f.riskTypes]));
|
|
2030
|
+
await writeFile6(join6(dir, "stage-07-risk-types.json"), JSON.stringify(stage07, null, 2), "utf8");
|
|
2031
|
+
const stage08 = Object.fromEntries(classified.map((f) => [f.rel, {
|
|
2032
|
+
isLoadBearing: f.loadBearingScore >= 5,
|
|
2033
|
+
loadBearingScore: f.loadBearingScore,
|
|
2034
|
+
runtimeEntrypoints: f.runtimeEntrypoints.length,
|
|
2035
|
+
entrypointTraceStatus: f.entrypointTraceStatus
|
|
2036
|
+
}]));
|
|
2037
|
+
await writeFile6(join6(dir, "stage-08-load-bearing.json"), JSON.stringify(stage08, null, 2), "utf8");
|
|
2038
|
+
const realClassified = classified.filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity);
|
|
2039
|
+
const wildCandidates = realClassified.filter((f) => f.heat >= 60 || f.smells.some((s) => s.severity >= 4));
|
|
2040
|
+
const pillars = buildPillars(classified, communities);
|
|
2041
|
+
const map = {
|
|
2042
|
+
stack: inv.stack,
|
|
2043
|
+
entrypoints: [...entrypoints],
|
|
2044
|
+
pillars,
|
|
2045
|
+
fileCount: work.length,
|
|
2046
|
+
realSourceCount: realClassified.length,
|
|
2047
|
+
topGravity: realClassified.slice(0, 12).map((f) => f.rel),
|
|
2048
|
+
topHeat: wildCandidates.slice(0, 12).map((f) => f.rel),
|
|
2049
|
+
brief: null
|
|
1525
2050
|
};
|
|
1526
|
-
|
|
1527
|
-
return out;
|
|
2051
|
+
return { projectRoot, classified, stack: inv.stack, entrypoints, map, communities };
|
|
1528
2052
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
2053
|
+
|
|
2054
|
+
// ../brain/dist/pipeline/scoring.js
|
|
2055
|
+
import { join as join7 } from "path";
|
|
2056
|
+
import { writeFile as writeFile7, mkdir as mkdir6, readFile as readFile6 } from "fs/promises";
|
|
2057
|
+
import { createHash } from "crypto";
|
|
2058
|
+
function computeSeverity(sideEffectProfile, productDomain, gravity, heat, maxNesting, hasLongFunctions, swallowedCatches, runtimeEntrypoints) {
|
|
2059
|
+
let score = 0;
|
|
2060
|
+
if (sideEffectProfile.includes("database_write"))
|
|
2061
|
+
score += 3;
|
|
2062
|
+
if (sideEffectProfile.includes("booking_mutation"))
|
|
2063
|
+
score += 4;
|
|
2064
|
+
if (sideEffectProfile.includes("payment_mutation"))
|
|
2065
|
+
score += 4;
|
|
2066
|
+
if (sideEffectProfile.includes("auth_token_mutation"))
|
|
2067
|
+
score += 4;
|
|
2068
|
+
if (sideEffectProfile.includes("webhook_delivery"))
|
|
2069
|
+
score += 3;
|
|
2070
|
+
if (sideEffectProfile.includes("webhook_ingress"))
|
|
2071
|
+
score += 3;
|
|
2072
|
+
if (sideEffectProfile.includes("calendar_mutation"))
|
|
2073
|
+
score += 3;
|
|
2074
|
+
if (productDomain === "booking_creation")
|
|
2075
|
+
score += 3;
|
|
2076
|
+
if (productDomain === "payments" || productDomain === "payments_webhooks")
|
|
2077
|
+
score += 3;
|
|
2078
|
+
if (productDomain === "auth_oauth")
|
|
2079
|
+
score += 3;
|
|
2080
|
+
if (productDomain === "webhooks")
|
|
2081
|
+
score += 2;
|
|
2082
|
+
if (gravity >= 85)
|
|
2083
|
+
score += 2;
|
|
2084
|
+
if (heat >= 70)
|
|
2085
|
+
score += 2;
|
|
2086
|
+
if (maxNesting >= 4)
|
|
2087
|
+
score += 1;
|
|
2088
|
+
if (hasLongFunctions)
|
|
2089
|
+
score += 1;
|
|
2090
|
+
if (swallowedCatches >= 1)
|
|
2091
|
+
score += 1;
|
|
2092
|
+
if (runtimeEntrypoints.length >= 2)
|
|
2093
|
+
score += 2;
|
|
2094
|
+
if (score >= 10)
|
|
2095
|
+
return 5;
|
|
2096
|
+
if (score >= 7)
|
|
2097
|
+
return 4;
|
|
2098
|
+
if (score >= 4)
|
|
2099
|
+
return 3;
|
|
2100
|
+
if (score >= 2)
|
|
2101
|
+
return 2;
|
|
2102
|
+
return 1;
|
|
2103
|
+
}
|
|
2104
|
+
function applyCorrections(file) {
|
|
2105
|
+
if (file.writeIntents.includes("handle_payment_webhook")) {
|
|
2106
|
+
if (!file.sideEffectProfile.includes("payment_mutation"))
|
|
2107
|
+
file.sideEffectProfile.push("payment_mutation");
|
|
2108
|
+
if (!file.sideEffectProfile.includes("webhook_ingress"))
|
|
2109
|
+
file.sideEffectProfile.push("webhook_ingress");
|
|
2110
|
+
file.sideEffectProfile = file.sideEffectProfile.filter((s) => s !== "none_detected");
|
|
1545
2111
|
}
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
sum += rank.get(from) / (outCount.get(from) || 1);
|
|
1557
|
-
}
|
|
1558
|
-
next.set(node, (1 - damping) / n + damping * (sum + dangling / n));
|
|
1559
|
-
}
|
|
1560
|
-
for (const node of nodes)
|
|
1561
|
-
rank.set(node, next.get(node));
|
|
2112
|
+
if (file.sideEffectProfile.includes("payment_mutation") || file.sideEffectProfile.includes("booking_mutation")) {
|
|
2113
|
+
if (file.canonicalSeverity < 4)
|
|
2114
|
+
file.canonicalSeverity = 4;
|
|
2115
|
+
}
|
|
2116
|
+
if (file.canonicalSeverity === 5)
|
|
2117
|
+
file.canonicalLoadBearing = true;
|
|
2118
|
+
if (file.riskTypes.includes("registry_bottleneck")) {
|
|
2119
|
+
if (file.canonicalSeverity < 4)
|
|
2120
|
+
file.canonicalSeverity = 4;
|
|
2121
|
+
file.canonicalLoadBearing = true;
|
|
1562
2122
|
}
|
|
1563
|
-
let max = 0;
|
|
1564
|
-
for (const v of rank.values())
|
|
1565
|
-
max = Math.max(max, v);
|
|
1566
|
-
if (max > 0)
|
|
1567
|
-
for (const node of nodes)
|
|
1568
|
-
rank.set(node, rank.get(node) / max);
|
|
1569
|
-
return rank;
|
|
1570
2123
|
}
|
|
1571
|
-
function
|
|
1572
|
-
const
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
best = l;
|
|
1590
|
-
bestCount = c;
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
if (best !== label.get(node)) {
|
|
1594
|
-
label.set(node, best);
|
|
1595
|
-
changed = true;
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
if (!changed)
|
|
1599
|
-
break;
|
|
2124
|
+
function inferObservableOutputs(frameworkRole, productDomain, sideEffectProfile) {
|
|
2125
|
+
const outputs = [];
|
|
2126
|
+
const ENTRYPOINT_ROLES2 = /* @__PURE__ */ new Set(["app_route_page", "app_route_handler", "pages_route", "pages_api_route", "trpc_api_route"]);
|
|
2127
|
+
if (sideEffectProfile.includes("redirect"))
|
|
2128
|
+
outputs.push("redirect_url");
|
|
2129
|
+
if (ENTRYPOINT_ROLES2.has(frameworkRole))
|
|
2130
|
+
outputs.push("http_status");
|
|
2131
|
+
if (frameworkRole === "app_route_handler" || frameworkRole === "pages_api_route") {
|
|
2132
|
+
outputs.push("json_response_shape");
|
|
2133
|
+
}
|
|
2134
|
+
if (productDomain === "booking_creation" || productDomain === "booking_management")
|
|
2135
|
+
outputs.push("booking_uid");
|
|
2136
|
+
if (productDomain === "payments" || productDomain === "payments_webhooks")
|
|
2137
|
+
outputs.push("payment_status");
|
|
2138
|
+
if (productDomain === "auth_oauth")
|
|
2139
|
+
outputs.push("auth_token");
|
|
2140
|
+
if (sideEffectProfile.includes("webhook_delivery") || sideEffectProfile.includes("webhook_ingress")) {
|
|
2141
|
+
outputs.push("webhook_payload");
|
|
1600
2142
|
}
|
|
1601
|
-
|
|
2143
|
+
if (sideEffectProfile.includes("calendar_mutation"))
|
|
2144
|
+
outputs.push("calendar_event_id");
|
|
2145
|
+
if (sideEffectProfile.includes("email_send"))
|
|
2146
|
+
outputs.push("email_payload");
|
|
2147
|
+
if (sideEffectProfile.includes("analytics_event"))
|
|
2148
|
+
outputs.push("sdk_event_name");
|
|
2149
|
+
if (frameworkRole === "hook" || frameworkRole === "store")
|
|
2150
|
+
outputs.push("ui_state_transition");
|
|
2151
|
+
if (productDomain === "data_table" && frameworkRole === "provider") {
|
|
2152
|
+
outputs.push("ui_state_transition", "filter_state", "selected_segment");
|
|
2153
|
+
}
|
|
2154
|
+
return [...new Set(outputs)];
|
|
1602
2155
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
try {
|
|
1610
|
-
const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
|
|
1611
|
-
stack.add("Node.js");
|
|
1612
|
-
const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
|
|
1613
|
-
for (const known of ["react", "next", "vue", "svelte", "express", "fastify", "typescript", "vite"]) {
|
|
1614
|
-
if (deps[known])
|
|
1615
|
-
stack.add(known === "next" ? "Next.js" : known[0].toUpperCase() + known.slice(1));
|
|
1616
|
-
}
|
|
1617
|
-
const addEntry = (p) => {
|
|
1618
|
-
if (!p)
|
|
1619
|
-
return;
|
|
1620
|
-
const abs = join4(projectRoot, p);
|
|
1621
|
-
const r = relative(projectRoot, abs);
|
|
1622
|
-
if (files.includes(abs))
|
|
1623
|
-
entrypoints.add(r);
|
|
1624
|
-
};
|
|
1625
|
-
addEntry(pkg.main);
|
|
1626
|
-
if (typeof pkg.bin === "string")
|
|
1627
|
-
addEntry(pkg.bin);
|
|
1628
|
-
else if (pkg.bin)
|
|
1629
|
-
for (const v of Object.values(pkg.bin))
|
|
1630
|
-
addEntry(v);
|
|
1631
|
-
} catch {
|
|
1632
|
-
}
|
|
2156
|
+
function inferPatchRisk(productDomain, riskTypes, sideEffectProfile, importedByCount, loadBearingScore) {
|
|
2157
|
+
if (loadBearingScore >= 12 || productDomain === "booking_creation" && riskTypes.includes("mutation_orchestration")) {
|
|
2158
|
+
return {
|
|
2159
|
+
level: "critical",
|
|
2160
|
+
reason: `${productDomain} domain with ${riskTypes.join(", ")} \u2014 any patch risks breaking live booking, payment, or auth flows.`
|
|
2161
|
+
};
|
|
1633
2162
|
}
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
for (const f of [pyproject, requirements]) {
|
|
1641
|
-
if (existsSync2(f)) {
|
|
1642
|
-
try {
|
|
1643
|
-
reqText += await readFile4(f, "utf8");
|
|
1644
|
-
} catch {
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
for (const known of ["pygame", "PySide6", "PyQt5", "PyQt6", "flask", "django", "fastapi", "numpy", "pandas", "torch", "tensorflow"]) {
|
|
1649
|
-
if (new RegExp(known, "i").test(reqText))
|
|
1650
|
-
stack.add(known);
|
|
1651
|
-
}
|
|
2163
|
+
if (loadBearingScore >= 8 || sideEffectProfile.includes("payment_mutation") || sideEffectProfile.includes("auth_token_mutation")) {
|
|
2164
|
+
const external = sideEffectProfile.filter((s) => ["payment_mutation", "auth_token_mutation", "database_write", "webhook_delivery"].includes(s));
|
|
2165
|
+
return {
|
|
2166
|
+
level: "high",
|
|
2167
|
+
reason: `${productDomain} writes to external state (${external.join(", ") || "database"}). Changes require integration testing.`
|
|
2168
|
+
};
|
|
1652
2169
|
}
|
|
1653
|
-
if (
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
stack.add("Java");
|
|
1659
|
-
for (const abs of files) {
|
|
1660
|
-
const r = rel(abs);
|
|
1661
|
-
const base = basename(r);
|
|
1662
|
-
if (base === "main.py" || base === "__main__.py")
|
|
1663
|
-
entrypoints.add(r);
|
|
1664
|
-
if (/^index\.(ts|tsx|js|jsx|mjs|cjs)$/.test(base) && dirname(r).split(sep).length <= 2)
|
|
1665
|
-
entrypoints.add(r);
|
|
1666
|
-
if (base === "main.go" && r.includes("cmd" + sep))
|
|
1667
|
-
entrypoints.add(r);
|
|
1668
|
-
if (base === "main.go" && !r.includes(sep))
|
|
1669
|
-
entrypoints.add(r);
|
|
1670
|
-
if (base === "main.rs" || base === "lib.rs")
|
|
1671
|
-
entrypoints.add(r);
|
|
2170
|
+
if (riskTypes.includes("registry_bottleneck")) {
|
|
2171
|
+
return {
|
|
2172
|
+
level: "high",
|
|
2173
|
+
reason: "registry_bottleneck: central dispatch point \u2014 blast radius not measurable by fan-in alone."
|
|
2174
|
+
};
|
|
1672
2175
|
}
|
|
1673
|
-
if (
|
|
1674
|
-
|
|
1675
|
-
for (const abs of files) {
|
|
1676
|
-
const r = rel(abs);
|
|
1677
|
-
const stem = basename(r, extname(r));
|
|
1678
|
-
const underApp = /(?:^|[/\\])app[/\\]/.test(r);
|
|
1679
|
-
const underPages = /(?:^|[/\\])pages[/\\]/.test(r);
|
|
1680
|
-
if (underApp && appRouterNames.has(stem))
|
|
1681
|
-
entrypoints.add(r);
|
|
1682
|
-
if (underPages && !stem.startsWith("_"))
|
|
1683
|
-
entrypoints.add(r);
|
|
1684
|
-
}
|
|
2176
|
+
if (loadBearingScore >= 5 || importedByCount >= 5) {
|
|
2177
|
+
return { level: "medium", reason: `Imported by ${importedByCount} files. Interface changes will cascade.` };
|
|
1685
2178
|
}
|
|
1686
|
-
|
|
2179
|
+
if (productDomain === "data_table" && riskTypes.includes("state_machine")) {
|
|
2180
|
+
return {
|
|
2181
|
+
level: "medium",
|
|
2182
|
+
reason: "data_table state machine: controls user-visible workflow state (filters, segments, pagination) \u2014 regression risk not captured by mutation scoring."
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
return { level: "low", reason: "Locally contained \u2014 limited blast radius." };
|
|
1687
2186
|
}
|
|
1688
|
-
|
|
1689
|
-
"
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
"
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
"
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
2187
|
+
function inferSafePatchStrategy(riskTypes, sideEffectProfile) {
|
|
2188
|
+
if (riskTypes.includes("mutation_orchestration")) {
|
|
2189
|
+
return "Do not rewrite inline. Extract pure decision logic into a tested reducer or state machine first. Preserve all side-effect call sites (redirect URLs, SDK event names, response shapes) as invariants.";
|
|
2190
|
+
}
|
|
2191
|
+
if (riskTypes.includes("registry_bottleneck")) {
|
|
2192
|
+
return "Add new entries without removing existing keys. Treat the registry map as append-only until all consumers are verified.";
|
|
2193
|
+
}
|
|
2194
|
+
if (riskTypes.includes("registry_consumer")) {
|
|
2195
|
+
return "Verify the registry contract (Components.tsx) before patching. Changes to field types must be reflected in both the registry and all rendering paths.";
|
|
2196
|
+
}
|
|
2197
|
+
if (riskTypes.includes("route_handler_write_path")) {
|
|
2198
|
+
return "Add integration tests covering success and failure paths before modifying. Verify HTTP status codes and response shapes are preserved.";
|
|
2199
|
+
}
|
|
2200
|
+
if (riskTypes.includes("god_component") || riskTypes.includes("god_hook")) {
|
|
2201
|
+
return "Extract sub-concerns into separate modules first. Only refactor the extraction points after tests confirm equivalence.";
|
|
2202
|
+
}
|
|
2203
|
+
if (sideEffectProfile.includes("database_write")) {
|
|
2204
|
+
return "Wrap changes in a transaction or use a feature flag. Run against a staging database before production.";
|
|
2205
|
+
}
|
|
2206
|
+
return "Review importedBy before patching. Run affected integration tests.";
|
|
1702
2207
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
if (!basenameIndex.has(b))
|
|
1712
|
-
basenameIndex.set(b, []);
|
|
1713
|
-
basenameIndex.get(b).push(rel);
|
|
2208
|
+
function inferDoNotTouch(sideEffectProfile, productDomain) {
|
|
2209
|
+
const items = [];
|
|
2210
|
+
if (sideEffectProfile.includes("payment_mutation"))
|
|
2211
|
+
items.push("payment flow branch");
|
|
2212
|
+
if (sideEffectProfile.includes("auth_token_mutation"))
|
|
2213
|
+
items.push("token issuance / refresh branch");
|
|
2214
|
+
if (sideEffectProfile.includes("webhook_delivery") || sideEffectProfile.includes("webhook_ingress")) {
|
|
2215
|
+
items.push("webhook payload shape");
|
|
1714
2216
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
const tree = await parseAs(lang, source);
|
|
1734
|
-
if (!tree)
|
|
1735
|
-
continue;
|
|
1736
|
-
const ast = analyzeAst(source, lang, tree);
|
|
1737
|
-
const importSpecs = extractImports(source, lang);
|
|
1738
|
-
graph.nodes[rel] = { imports: importSpecs };
|
|
1739
|
-
const frameworkRole = inferFrameworkRole(rel);
|
|
1740
|
-
const productDomain = inferProductDomain(rel, importSpecs);
|
|
1741
|
-
const sideEffectProfile = inferSideEffectProfile(source, importSpecs);
|
|
1742
|
-
work.push({
|
|
1743
|
-
abs: file,
|
|
1744
|
-
rel,
|
|
1745
|
-
lang,
|
|
1746
|
-
source,
|
|
1747
|
-
ast,
|
|
1748
|
-
importSpecs,
|
|
1749
|
-
pathDemote: pathDemoteReason(rel),
|
|
1750
|
-
frameworkRole,
|
|
1751
|
-
productDomain,
|
|
1752
|
-
sideEffectProfile
|
|
2217
|
+
if (sideEffectProfile.includes("redirect"))
|
|
2218
|
+
items.push("redirect URL strings");
|
|
2219
|
+
if (sideEffectProfile.includes("analytics_event"))
|
|
2220
|
+
items.push("SDK event names");
|
|
2221
|
+
if (sideEffectProfile.includes("booking_mutation")) {
|
|
2222
|
+
items.push("booking success response shape", "recurring booking branch");
|
|
2223
|
+
}
|
|
2224
|
+
if (productDomain === "auth_oauth")
|
|
2225
|
+
items.push("OAuth callback URLs", "token scopes");
|
|
2226
|
+
return items;
|
|
2227
|
+
}
|
|
2228
|
+
function inferTestProbes(writeIntents, observableOutputs) {
|
|
2229
|
+
const probes = [];
|
|
2230
|
+
if (writeIntents.includes("create_booking")) {
|
|
2231
|
+
probes.push({
|
|
2232
|
+
name: "standard booking success",
|
|
2233
|
+
scenario: "create a standard booking and assert success redirect and booking uid",
|
|
2234
|
+
expectedObservable: ["booking_uid", "redirect_url", "sdk_event_name"].filter((o) => observableOutputs.includes(o))
|
|
1753
2235
|
});
|
|
1754
2236
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
importsResolved.set(w.rel, /* @__PURE__ */ new Set());
|
|
1762
|
-
importsUnresolved.set(w.rel, /* @__PURE__ */ new Set());
|
|
2237
|
+
if (writeIntents.includes("reschedule_booking")) {
|
|
2238
|
+
probes.push({
|
|
2239
|
+
name: "reschedule booking",
|
|
2240
|
+
scenario: "reschedule an existing booking and assert reschedule event path",
|
|
2241
|
+
expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
|
|
2242
|
+
});
|
|
1763
2243
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
2244
|
+
if (writeIntents.includes("create_recurring_booking")) {
|
|
2245
|
+
probes.push({
|
|
2246
|
+
name: "recurring booking",
|
|
2247
|
+
scenario: "create recurring booking and assert recurring success behavior",
|
|
2248
|
+
expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
if (writeIntents.includes("handle_payment_webhook")) {
|
|
2252
|
+
probes.push({
|
|
2253
|
+
name: "payment webhook ingestion",
|
|
2254
|
+
scenario: "send a valid payment webhook and assert booking/payment state updated",
|
|
2255
|
+
expectedObservable: ["payment_status", "booking_uid", "http_status"].filter((o) => observableOutputs.includes(o))
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
if (writeIntents.includes("issue_auth_token")) {
|
|
2259
|
+
probes.push({
|
|
2260
|
+
name: "token issuance",
|
|
2261
|
+
scenario: "complete OAuth flow and assert access token issued with correct scopes",
|
|
2262
|
+
expectedObservable: ["auth_token", "http_status"].filter((o) => observableOutputs.includes(o))
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
return probes;
|
|
2266
|
+
}
|
|
2267
|
+
function deriveConfidence(fanIn, gravity) {
|
|
2268
|
+
if (fanIn >= 10 && gravity >= 40)
|
|
2269
|
+
return "high";
|
|
2270
|
+
if (fanIn >= 5 || gravity >= 25)
|
|
2271
|
+
return "medium";
|
|
2272
|
+
return "low";
|
|
2273
|
+
}
|
|
2274
|
+
async function runScoring(projectRoot, cr) {
|
|
2275
|
+
const dir = join7(projectRoot, ".vibe-splainer");
|
|
2276
|
+
await mkdir6(dir, { recursive: true });
|
|
2277
|
+
const persisted = {};
|
|
2278
|
+
const severityBreakdowns = {};
|
|
2279
|
+
for (const f of cr.classified) {
|
|
2280
|
+
const severity = computeSeverity(f.sideEffectProfile, f.productDomain, f.gravity, f.heat, f.heatSignals.maxNesting, f.heatSignals.longFunctions > 0, f.heatSignals.swallowedCatches, f.runtimeEntrypoints);
|
|
2281
|
+
const isLoadBearing = f.loadBearingScore >= 5;
|
|
2282
|
+
const pf = {
|
|
2283
|
+
relativePath: f.rel,
|
|
2284
|
+
language: f.lang,
|
|
2285
|
+
isRealSource: f.isRealSource,
|
|
2286
|
+
demoteReason: f.demoteReason,
|
|
2287
|
+
gravity: Math.round(f.gravity),
|
|
2288
|
+
heat: Math.round(f.heat),
|
|
2289
|
+
gravitySignals: f.gravitySignals,
|
|
2290
|
+
heatSignals: f.heatSignals,
|
|
2291
|
+
smells: f.smells,
|
|
2292
|
+
pillarHint: f.pillarHint,
|
|
2293
|
+
importedBy: f.importedBy,
|
|
2294
|
+
imports: f.imports,
|
|
2295
|
+
importsUnresolved: f.importsUnresolved,
|
|
2296
|
+
frameworkRole: f.frameworkRole,
|
|
2297
|
+
productDomain: f.productDomain,
|
|
2298
|
+
sideEffectProfile: f.sideEffectProfile,
|
|
2299
|
+
hotSpans: f.hotSpans,
|
|
2300
|
+
riskTypes: f.riskTypes,
|
|
2301
|
+
writeIntents: f.writeIntents,
|
|
2302
|
+
canonicalSeverity: severity,
|
|
2303
|
+
canonicalLoadBearing: isLoadBearing
|
|
2304
|
+
};
|
|
2305
|
+
applyCorrections(pf);
|
|
2306
|
+
persisted[f.rel] = pf;
|
|
2307
|
+
severityBreakdowns[f.rel] = `severity=${pf.canonicalSeverity} loadBearing=${pf.canonicalLoadBearing} effects=${pf.sideEffectProfile.join(",")} domain=${pf.productDomain}`;
|
|
2308
|
+
}
|
|
2309
|
+
const stage09 = Object.fromEntries(Object.entries(persisted).filter(([, pf]) => pf.isRealSource).map(([rel, pf]) => [rel, { canonicalSeverity: pf.canonicalSeverity, canonicalLoadBearing: pf.canonicalLoadBearing, scoreBreakdown: severityBreakdowns[rel] }]));
|
|
2310
|
+
await writeFile7(join7(dir, "stage-09-severity.json"), JSON.stringify(stage09, null, 2), "utf8");
|
|
2311
|
+
const store = { files: persisted };
|
|
2312
|
+
const importedByMapForDelta = /* @__PURE__ */ new Map();
|
|
2313
|
+
for (const [rel, pf] of Object.entries(persisted)) {
|
|
2314
|
+
importedByMapForDelta.set(rel, new Set(pf.importedBy));
|
|
2315
|
+
}
|
|
2316
|
+
const metaForDelta = new Map(Object.entries(persisted).map(([rel, pf]) => [rel, { frameworkRole: pf.frameworkRole, productDomain: pf.productDomain }]));
|
|
2317
|
+
const deltaTargets = Object.values(persisted).filter((pf) => pf.isRealSource).sort((a, b) => b.gravity - a.gravity).map((pf) => {
|
|
2318
|
+
const runtimeEntrypoints = findRuntimeEntrypoints(pf.relativePath, importedByMapForDelta, metaForDelta);
|
|
2319
|
+
const entrypointTraceStatus = deriveEntrypointTraceStatus(pf.productDomain, runtimeEntrypoints, pf.importsUnresolved);
|
|
2320
|
+
const smellMaxSeverity = pf.smells.length > 0 ? Math.max(...pf.smells.map((s) => s.severity)) : 0;
|
|
2321
|
+
const loadBearingScore = computeLoadBearingScore(pf.gravity, pf.heat, pf.importedBy.length, pf.sideEffectProfile, pf.productDomain, smellMaxSeverity, runtimeEntrypoints);
|
|
2322
|
+
const observableOutputs = inferObservableOutputs(pf.frameworkRole, pf.productDomain, pf.sideEffectProfile);
|
|
2323
|
+
const patchRisk = inferPatchRisk(pf.productDomain, pf.riskTypes, pf.sideEffectProfile, pf.importedBy.length, loadBearingScore);
|
|
2324
|
+
const confidence = deriveConfidence(pf.gravitySignals.fanIn, pf.gravity);
|
|
2325
|
+
const fileHashInput = pf.hotSpans.map((h) => h.snippet).join("");
|
|
2326
|
+
const fileHash = createHash("sha256").update(fileHashInput || pf.relativePath).digest("hex").slice(0, 12);
|
|
2327
|
+
const rawEvidence = pf.hotSpans.map((span) => ({
|
|
2328
|
+
file: pf.relativePath,
|
|
2329
|
+
startLine: span.startLine,
|
|
2330
|
+
endLine: span.endLine,
|
|
2331
|
+
rawSourceExcerpt: span.rawExcerpt,
|
|
2332
|
+
evidenceHash: createHash("sha256").update(span.rawExcerpt).digest("hex").slice(0, 12)
|
|
2333
|
+
}));
|
|
2334
|
+
const displayEvidence = pf.hotSpans.map((span) => ({
|
|
2335
|
+
file: pf.relativePath,
|
|
2336
|
+
startLine: span.startLine,
|
|
2337
|
+
endLine: span.endLine,
|
|
2338
|
+
excerpt: span.snippet,
|
|
2339
|
+
isTruncated: span.rawExcerpt.length > 2e3
|
|
2340
|
+
}));
|
|
2341
|
+
return {
|
|
2342
|
+
path: pf.relativePath,
|
|
2343
|
+
frameworkRole: pf.frameworkRole,
|
|
2344
|
+
productDomain: pf.productDomain,
|
|
2345
|
+
gravity: Math.round(pf.gravity),
|
|
2346
|
+
heat: Math.round(pf.heat),
|
|
2347
|
+
severity: pf.canonicalSeverity,
|
|
2348
|
+
confidence,
|
|
2349
|
+
isLoadBearing: pf.canonicalLoadBearing || loadBearingScore >= 5,
|
|
2350
|
+
loadBearingScore,
|
|
2351
|
+
riskTypes: pf.riskTypes,
|
|
2352
|
+
sideEffectProfile: pf.sideEffectProfile,
|
|
2353
|
+
blastRadius: pf.importedBy,
|
|
2354
|
+
runtimeEntrypoints,
|
|
2355
|
+
entrypointTraceStatus,
|
|
2356
|
+
blockedImports: pf.importsUnresolved,
|
|
2357
|
+
observableOutputs,
|
|
2358
|
+
writeIntents: pf.writeIntents,
|
|
2359
|
+
patchRisk,
|
|
2360
|
+
safePatchStrategy: inferSafePatchStrategy(pf.riskTypes, pf.sideEffectProfile),
|
|
2361
|
+
doNotTouch: inferDoNotTouch(pf.sideEffectProfile, pf.productDomain),
|
|
2362
|
+
testProbes: inferTestProbes(pf.writeIntents, observableOutputs),
|
|
2363
|
+
rawEvidence,
|
|
2364
|
+
displayEvidence,
|
|
2365
|
+
analysisAnnotation: `${pf.frameworkRole} in ${pf.productDomain} domain. fanIn=${pf.gravitySignals.fanIn} cyclomatic=${pf.gravitySignals.cyclomatic} loc=${pf.gravitySignals.loc}`,
|
|
2366
|
+
hashes: { fileHash, evidenceHash: rawEvidence.map((e) => e.evidenceHash).join("-") }
|
|
2367
|
+
};
|
|
2368
|
+
});
|
|
2369
|
+
const dest = join7(dir, "delta_targets.json");
|
|
2370
|
+
const tmp = dest + ".tmp";
|
|
2371
|
+
await writeFile7(tmp, JSON.stringify(deltaTargets, null, 2), "utf8");
|
|
2372
|
+
const { rename } = await import("fs/promises");
|
|
2373
|
+
await rename(tmp, dest);
|
|
2374
|
+
const validationReport = await buildValidationReport(store, deltaTargets, projectRoot);
|
|
2375
|
+
await writeFile7(join7(dir, "validation_report.json"), JSON.stringify(validationReport, null, 2), "utf8");
|
|
2376
|
+
for (const e of validationReport.errors) {
|
|
2377
|
+
console.error(`[vibe-splain] VALIDATION ERROR [${e.rule}] ${e.file}: ${e.detail}`);
|
|
2378
|
+
}
|
|
2379
|
+
for (const w of validationReport.warnings) {
|
|
2380
|
+
console.error(`[vibe-splain] VALIDATION WARN [${w.rule}] ${w.file}: ${w.detail}`);
|
|
2381
|
+
}
|
|
2382
|
+
return { store, deltaTargets, validationReport };
|
|
2383
|
+
}
|
|
2384
|
+
async function buildValidationReport(store, deltaTargets, projectRoot) {
|
|
2385
|
+
const errors = [];
|
|
2386
|
+
const warnings = [];
|
|
2387
|
+
let passCount = 0;
|
|
2388
|
+
const deltaByPath = new Map(deltaTargets.map((d) => [d.path, d]));
|
|
2389
|
+
for (const [, pf] of Object.entries(store.files)) {
|
|
2390
|
+
if (!pf.isRealSource)
|
|
2391
|
+
continue;
|
|
2392
|
+
const delta = deltaByPath.get(pf.relativePath);
|
|
2393
|
+
if (pf.canonicalSeverity === 5 && !pf.canonicalLoadBearing) {
|
|
2394
|
+
errors.push({
|
|
2395
|
+
file: pf.relativePath,
|
|
2396
|
+
rule: "severity_5_not_load_bearing",
|
|
2397
|
+
detail: "severity=5 but canonicalLoadBearing=false \u2014 post-correction invariant violated",
|
|
2398
|
+
expected: "canonicalLoadBearing=true",
|
|
2399
|
+
actual: "canonicalLoadBearing=false"
|
|
2400
|
+
});
|
|
2401
|
+
continue;
|
|
1776
2402
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
isRealSource.set(w.rel, true);
|
|
1787
|
-
demoteReason.set(w.rel, null);
|
|
2403
|
+
if (pf.writeIntents.includes("handle_payment_webhook") && pf.sideEffectProfile.includes("none_detected")) {
|
|
2404
|
+
errors.push({
|
|
2405
|
+
file: pf.relativePath,
|
|
2406
|
+
rule: "payment_webhook_no_effects",
|
|
2407
|
+
detail: "writeIntents includes handle_payment_webhook but sideEffectProfile is none_detected",
|
|
2408
|
+
expected: "payment_mutation + webhook_ingress",
|
|
2409
|
+
actual: "none_detected"
|
|
2410
|
+
});
|
|
2411
|
+
continue;
|
|
1788
2412
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
2413
|
+
if (pf.productDomain === "booking_creation" && delta?.entrypointTraceStatus === "no_runtime_entrypoint_found" && pf.importsUnresolved.length === 0) {
|
|
2414
|
+
errors.push({
|
|
2415
|
+
file: pf.relativePath,
|
|
2416
|
+
rule: "booking_creation_no_entrypoint_no_blockers",
|
|
2417
|
+
detail: "booking_creation domain with no entrypoint found and no blocked imports \u2014 classification may be wrong"
|
|
2418
|
+
});
|
|
1792
2419
|
continue;
|
|
1793
|
-
|
|
2420
|
+
}
|
|
2421
|
+
if (delta && delta.severity !== pf.canonicalSeverity) {
|
|
2422
|
+
errors.push({
|
|
2423
|
+
file: pf.relativePath,
|
|
2424
|
+
rule: "severity_mismatch_delta",
|
|
2425
|
+
detail: "DeltaTarget severity does not match canonicalSeverity",
|
|
2426
|
+
expected: String(pf.canonicalSeverity),
|
|
2427
|
+
actual: String(delta.severity)
|
|
2428
|
+
});
|
|
1794
2429
|
continue;
|
|
1795
|
-
const inbound = [...importedBy.get(w.rel)].filter((src) => isRealSource.get(src));
|
|
1796
|
-
if (inbound.length === 0) {
|
|
1797
|
-
isRealSource.set(w.rel, false);
|
|
1798
|
-
demoteReason.set(w.rel, "no inbound references from application code");
|
|
1799
2430
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
outEdges.set(node, /* @__PURE__ */ new Set());
|
|
1807
|
-
undirected.set(node, /* @__PURE__ */ new Map());
|
|
1808
|
-
}
|
|
1809
|
-
for (const w of work) {
|
|
1810
|
-
if (!realSet.has(w.rel))
|
|
2431
|
+
if (pf.canonicalSeverity >= 4 && (delta?.rawEvidence.length ?? 0) === 0 && pf.hotSpans.length === 0) {
|
|
2432
|
+
errors.push({
|
|
2433
|
+
file: pf.relativePath,
|
|
2434
|
+
rule: "high_severity_no_evidence",
|
|
2435
|
+
detail: `severity=${pf.canonicalSeverity} but rawEvidence is empty`
|
|
2436
|
+
});
|
|
1811
2437
|
continue;
|
|
1812
|
-
for (const target of importsResolved.get(w.rel)) {
|
|
1813
|
-
if (!realSet.has(target))
|
|
1814
|
-
continue;
|
|
1815
|
-
outEdges.get(w.rel).add(target);
|
|
1816
|
-
const wDir = w.rel.split(sep)[0];
|
|
1817
|
-
const tDir = target.split(sep)[0];
|
|
1818
|
-
const weight = wDir === tDir ? 1 : 0.5;
|
|
1819
|
-
undirected.get(w.rel).set(target, weight);
|
|
1820
|
-
undirected.get(target).set(w.rel, weight);
|
|
1821
2438
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
const real = isRealSource.get(w.rel);
|
|
1829
|
-
const fanIn = [...importedBy.get(w.rel)].filter((src) => isRealSource.get(src)).length;
|
|
1830
|
-
const centrality = real ? ranks.get(w.rel) || 0 : 0;
|
|
1831
|
-
const gravitySignals = {
|
|
1832
|
-
fanIn,
|
|
1833
|
-
fanOut: fanOut.get(w.rel) || 0,
|
|
1834
|
-
centrality,
|
|
1835
|
-
cyclomatic: w.ast.cyclomatic,
|
|
1836
|
-
publicSurface: w.ast.publicSurface,
|
|
1837
|
-
loc: w.ast.loc
|
|
1838
|
-
};
|
|
1839
|
-
const depthRatio = (w.ast.cyclomatic + w.ast.maxNesting * 2) / Math.max(1, w.ast.publicSurface);
|
|
1840
|
-
const depthFactor = Math.min(1, Math.log2(depthRatio + 1) / 3);
|
|
1841
|
-
const adjustedCentrality = centrality * (0.3 + 0.7 * depthFactor);
|
|
1842
|
-
let gravityRaw = adjustedCentrality * 50 + Math.log2(fanIn + 1) * 6 + Math.log2(w.ast.cyclomatic + 1) * 7 + Math.log2(w.ast.publicSurface + 1) * 2 + (w.ast.maxNesting >= 4 ? 5 : 0);
|
|
1843
|
-
if (!real)
|
|
1844
|
-
gravityRaw *= 0.2;
|
|
1845
|
-
const gravity = Math.max(0, Math.min(100, gravityRaw));
|
|
1846
|
-
const heatSignals = {
|
|
1847
|
-
todos: w.ast.smells.filter((s) => s.kind === "todo").length,
|
|
1848
|
-
suppressions: w.ast.smells.filter((s) => s.kind === "suppression").length,
|
|
1849
|
-
swallowedCatches: w.ast.swallowedCatches,
|
|
1850
|
-
maxNesting: w.ast.maxNesting,
|
|
1851
|
-
longFunctions: w.ast.longFunctions,
|
|
1852
|
-
magicNumbers: w.ast.magicNumbers
|
|
1853
|
-
};
|
|
1854
|
-
const heat = real ? computeHeat(w.ast.smells) : 0;
|
|
1855
|
-
const keywordPillar = matchPillarByImports(w.importSpecs);
|
|
1856
|
-
const pathPillar = matchPillarByPath(w.rel);
|
|
1857
|
-
const pillarHint = real ? keywordPillar || pathPillar || `community-${communities.get(w.rel)}` : null;
|
|
1858
|
-
const fa = {
|
|
1859
|
-
path: w.abs,
|
|
1860
|
-
relativePath: w.rel,
|
|
1861
|
-
language: w.lang,
|
|
1862
|
-
isRealSource: real,
|
|
1863
|
-
demoteReason: demoteReason.get(w.rel) || null,
|
|
1864
|
-
gravity,
|
|
1865
|
-
heat,
|
|
1866
|
-
gravitySignals,
|
|
1867
|
-
heatSignals,
|
|
1868
|
-
smells: w.ast.smells,
|
|
1869
|
-
pillarHint,
|
|
1870
|
-
frameworkRole: w.frameworkRole,
|
|
1871
|
-
productDomain: w.productDomain,
|
|
1872
|
-
sideEffectProfile: w.sideEffectProfile
|
|
1873
|
-
};
|
|
1874
|
-
analyses.push(fa);
|
|
1875
|
-
persisted[w.rel] = {
|
|
1876
|
-
relativePath: w.rel,
|
|
1877
|
-
language: w.lang,
|
|
1878
|
-
isRealSource: real,
|
|
1879
|
-
demoteReason: demoteReason.get(w.rel) || null,
|
|
1880
|
-
gravity,
|
|
1881
|
-
heat,
|
|
1882
|
-
gravitySignals,
|
|
1883
|
-
heatSignals,
|
|
1884
|
-
smells: w.ast.smells,
|
|
1885
|
-
pillarHint,
|
|
1886
|
-
importedBy: [...importedBy.get(w.rel)].filter((src) => isRealSource.get(src)),
|
|
1887
|
-
imports: [...importsResolved.get(w.rel)],
|
|
1888
|
-
importsUnresolved: [...importsUnresolved.get(w.rel)],
|
|
1889
|
-
frameworkRole: w.frameworkRole,
|
|
1890
|
-
productDomain: w.productDomain,
|
|
1891
|
-
sideEffectProfile: w.sideEffectProfile,
|
|
1892
|
-
hotSpans: w.ast.hotSpans
|
|
1893
|
-
};
|
|
1894
|
-
}
|
|
1895
|
-
const realAnalyses = analyses.filter((a) => a.isRealSource).sort((a, b) => b.gravity - a.gravity);
|
|
1896
|
-
const wildCandidates = realAnalyses.filter((a) => a.heat >= 60 || a.smells.some((s) => s.severity >= 4)).sort((a, b) => b.heat - a.heat);
|
|
1897
|
-
const pillars = buildPillars(realAnalyses, persisted, communities, stack);
|
|
1898
|
-
const topGravity = realAnalyses.slice(0, 12).map((a) => a.relativePath);
|
|
1899
|
-
const topHeat = wildCandidates.slice(0, 12).map((a) => a.relativePath);
|
|
1900
|
-
const map = {
|
|
1901
|
-
stack,
|
|
1902
|
-
entrypoints: [...entrypoints],
|
|
1903
|
-
pillars,
|
|
1904
|
-
fileCount: work.length,
|
|
1905
|
-
realSourceCount: realAnalyses.length,
|
|
1906
|
-
topGravity,
|
|
1907
|
-
topHeat,
|
|
1908
|
-
brief: null
|
|
1909
|
-
};
|
|
1910
|
-
await writeGraph(projectRoot, graph);
|
|
1911
|
-
const analysisStore = { files: persisted };
|
|
1912
|
-
await writeAnalysis(projectRoot, analysisStore);
|
|
1913
|
-
await writeDeltaTargets(projectRoot, analysisStore, entrypoints);
|
|
1914
|
-
const uiUrl = `file://${join4(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
|
|
1915
|
-
return {
|
|
1916
|
-
projectRoot,
|
|
1917
|
-
totalFilesScanned: work.length,
|
|
1918
|
-
realSourceCount: realAnalyses.length,
|
|
1919
|
-
files: realAnalyses,
|
|
1920
|
-
map,
|
|
1921
|
-
wildCandidates,
|
|
1922
|
-
uiUrl,
|
|
1923
|
-
graph
|
|
1924
|
-
};
|
|
1925
|
-
}
|
|
1926
|
-
function buildPillars(real, persisted, communities, _stack) {
|
|
1927
|
-
const keywordGroups = /* @__PURE__ */ new Map();
|
|
1928
|
-
const unlabeled = [];
|
|
1929
|
-
for (const a of real) {
|
|
1930
|
-
if (a.pillarHint && !a.pillarHint.startsWith("community-")) {
|
|
1931
|
-
if (!keywordGroups.has(a.pillarHint))
|
|
1932
|
-
keywordGroups.set(a.pillarHint, []);
|
|
1933
|
-
keywordGroups.get(a.pillarHint).push(a);
|
|
1934
|
-
} else {
|
|
1935
|
-
unlabeled.push(a);
|
|
2439
|
+
if (pf.canonicalSeverity >= 4 && (delta?.runtimeEntrypoints.length ?? 0) === 0) {
|
|
2440
|
+
warnings.push({
|
|
2441
|
+
file: pf.relativePath,
|
|
2442
|
+
rule: "high_severity_no_entrypoints",
|
|
2443
|
+
detail: `severity=${pf.canonicalSeverity} but no runtime entrypoints found \u2014 check alias resolution`
|
|
2444
|
+
});
|
|
1936
2445
|
}
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
memberFiles: sorted.map((f) => f.relativePath)
|
|
1945
|
-
});
|
|
1946
|
-
}
|
|
1947
|
-
if (unlabeled.length > 0) {
|
|
1948
|
-
const communityGroups = /* @__PURE__ */ new Map();
|
|
1949
|
-
for (const a of unlabeled) {
|
|
1950
|
-
const c = communities.get(a.relativePath);
|
|
1951
|
-
if (c === void 0)
|
|
1952
|
-
continue;
|
|
1953
|
-
if (!communityGroups.has(c))
|
|
1954
|
-
communityGroups.set(c, []);
|
|
1955
|
-
communityGroups.get(c).push(a);
|
|
2446
|
+
if (delta?.entrypointTraceStatus === "partial_wrong_surface") {
|
|
2447
|
+
const foundPaths = delta.runtimeEntrypoints.map((e) => e.path).join(", ");
|
|
2448
|
+
warnings.push({
|
|
2449
|
+
file: pf.relativePath,
|
|
2450
|
+
rule: "partial_wrong_surface",
|
|
2451
|
+
detail: `Entrypoints found but domain surface mismatch for ${pf.productDomain}. Found: ${foundPaths}`
|
|
2452
|
+
});
|
|
1956
2453
|
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
2454
|
+
if (pf.riskTypes.includes("registry_bottleneck")) {
|
|
2455
|
+
if (pf.canonicalSeverity < 4)
|
|
2456
|
+
errors.push({
|
|
2457
|
+
file: pf.relativePath,
|
|
2458
|
+
rule: "registry_bottleneck_severity",
|
|
2459
|
+
detail: "registry_bottleneck file must have severity >= 4",
|
|
2460
|
+
expected: ">=4",
|
|
2461
|
+
actual: String(pf.canonicalSeverity)
|
|
2462
|
+
});
|
|
2463
|
+
if (!pf.canonicalLoadBearing)
|
|
2464
|
+
errors.push({
|
|
2465
|
+
file: pf.relativePath,
|
|
2466
|
+
rule: "registry_bottleneck_load_bearing",
|
|
2467
|
+
detail: "registry_bottleneck file must be load-bearing",
|
|
2468
|
+
expected: "true",
|
|
2469
|
+
actual: "false"
|
|
2470
|
+
});
|
|
2471
|
+
if (delta && delta.patchRisk.level !== "high" && delta.patchRisk.level !== "critical")
|
|
2472
|
+
errors.push({
|
|
2473
|
+
file: pf.relativePath,
|
|
2474
|
+
rule: "registry_bottleneck_patch_risk",
|
|
2475
|
+
detail: "registry_bottleneck file must have patch risk high or critical",
|
|
2476
|
+
expected: "high|critical",
|
|
2477
|
+
actual: delta?.patchRisk.level ?? "unknown"
|
|
1971
2478
|
});
|
|
1972
|
-
}
|
|
1973
2479
|
}
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
}
|
|
1983
|
-
const
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
bucket = "Logic";
|
|
2009
|
-
}
|
|
2010
|
-
const key = `${p.name} (${bucket})`;
|
|
2011
|
-
if (!groups.has(key))
|
|
2012
|
-
groups.set(key, []);
|
|
2013
|
-
groups.get(key).push(f);
|
|
2014
|
-
}
|
|
2015
|
-
for (const [key, files] of groups) {
|
|
2016
|
-
if (files.length > 0) {
|
|
2017
|
-
finalPillars.push({
|
|
2018
|
-
name: key,
|
|
2019
|
-
description: `Subdivided from ${p.name}`,
|
|
2020
|
-
memberFiles: files
|
|
2021
|
-
});
|
|
2022
|
-
}
|
|
2480
|
+
if (pf.productDomain === "data_table" && pf.riskTypes.includes("state_machine") && delta?.patchRisk.level === "low") {
|
|
2481
|
+
warnings.push({
|
|
2482
|
+
file: pf.relativePath,
|
|
2483
|
+
rule: "data_table_state_machine_risk",
|
|
2484
|
+
detail: "data_table state machine should have at least medium patch risk"
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
2487
|
+
passCount++;
|
|
2488
|
+
}
|
|
2489
|
+
const PAYMENT_PROVIDER_PATH_TERMS = ["stripe", "paypal", "btcpay", "btcpayserver", "alby", "hitpay", "payment"];
|
|
2490
|
+
const PAYMENT_CONTENT_TERMS = [
|
|
2491
|
+
"constructEvent",
|
|
2492
|
+
"checkoutSession",
|
|
2493
|
+
"paymentIntent",
|
|
2494
|
+
"stripe-signature",
|
|
2495
|
+
"webhook-signature",
|
|
2496
|
+
"payment_mutation",
|
|
2497
|
+
"paymentStatus",
|
|
2498
|
+
"invoicePaid",
|
|
2499
|
+
"chargeSucceeded"
|
|
2500
|
+
];
|
|
2501
|
+
for (const [rel, pf] of Object.entries(store.files)) {
|
|
2502
|
+
if (!pf.isRealSource)
|
|
2503
|
+
continue;
|
|
2504
|
+
const pathLower = rel.toLowerCase();
|
|
2505
|
+
if (!pathLower.includes("webhook"))
|
|
2506
|
+
continue;
|
|
2507
|
+
const primaryTrigger = PAYMENT_PROVIDER_PATH_TERMS.some((t) => pathLower.includes(t));
|
|
2508
|
+
let secondaryTrigger = false;
|
|
2509
|
+
if (!primaryTrigger && pf.productDomain !== "payments_webhooks") {
|
|
2510
|
+
try {
|
|
2511
|
+
const src = await readFile6(join7(projectRoot, rel), "utf8");
|
|
2512
|
+
secondaryTrigger = PAYMENT_CONTENT_TERMS.some((t) => src.includes(t));
|
|
2513
|
+
} catch {
|
|
2023
2514
|
}
|
|
2024
|
-
} else {
|
|
2025
|
-
finalPillars.push(p);
|
|
2026
2515
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2516
|
+
if (!primaryTrigger && !secondaryTrigger)
|
|
2517
|
+
continue;
|
|
2518
|
+
const delta = deltaByPath.get(rel);
|
|
2519
|
+
const triggerLabel = primaryTrigger ? "path" : "content";
|
|
2520
|
+
const webhookChecks = [
|
|
2521
|
+
[
|
|
2522
|
+
pf.productDomain !== "payments_webhooks",
|
|
2523
|
+
"webhook_domain",
|
|
2524
|
+
`Payment webhook (${triggerLabel} trigger) not classified as payments_webhooks`
|
|
2525
|
+
],
|
|
2526
|
+
[
|
|
2527
|
+
!pf.sideEffectProfile.includes("webhook_ingress"),
|
|
2528
|
+
"webhook_ingress_missing",
|
|
2529
|
+
`Payment webhook (${triggerLabel} trigger) missing webhook_ingress side effect`
|
|
2530
|
+
],
|
|
2531
|
+
[
|
|
2532
|
+
!pf.sideEffectProfile.includes("payment_mutation"),
|
|
2533
|
+
"webhook_payment_mutation_missing",
|
|
2534
|
+
`Payment webhook (${triggerLabel} trigger) missing payment_mutation side effect`
|
|
2535
|
+
],
|
|
2536
|
+
[
|
|
2537
|
+
!pf.writeIntents.includes("handle_payment_webhook"),
|
|
2538
|
+
"webhook_write_intent_missing",
|
|
2539
|
+
`Payment webhook (${triggerLabel} trigger) missing handle_payment_webhook write intent`
|
|
2540
|
+
],
|
|
2541
|
+
[
|
|
2542
|
+
!!delta && delta.patchRisk.level !== "high" && delta.patchRisk.level !== "critical",
|
|
2543
|
+
"webhook_patch_risk",
|
|
2544
|
+
`Payment webhook (${triggerLabel} trigger) patchRisk must be high or critical`
|
|
2545
|
+
],
|
|
2546
|
+
[
|
|
2547
|
+
!pf.canonicalLoadBearing,
|
|
2548
|
+
"webhook_load_bearing",
|
|
2549
|
+
`Payment webhook (${triggerLabel} trigger) must be load-bearing`
|
|
2550
|
+
]
|
|
2551
|
+
];
|
|
2552
|
+
for (const [condition, rule, detail] of webhookChecks) {
|
|
2553
|
+
if (condition)
|
|
2554
|
+
errors.push({ file: rel, rule, detail });
|
|
2033
2555
|
}
|
|
2034
|
-
p.name = n;
|
|
2035
|
-
seen.add(n);
|
|
2036
2556
|
}
|
|
2037
|
-
return
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
booking_audit: "Booking Audit",
|
|
2044
|
-
event_type_configuration: "Event Types",
|
|
2045
|
-
availability: "Availability",
|
|
2046
|
-
auth: "Auth",
|
|
2047
|
-
auth_oauth: "Auth OAuth",
|
|
2048
|
-
payments: "Payments",
|
|
2049
|
-
payments_webhooks: "Payment Webhooks",
|
|
2050
|
-
webhooks: "Webhooks",
|
|
2051
|
-
apps_marketplace: "Apps",
|
|
2052
|
-
calendar_integrations: "Calendar",
|
|
2053
|
-
video: "Video",
|
|
2054
|
-
onboarding: "Onboarding",
|
|
2055
|
-
settings: "Settings",
|
|
2056
|
-
admin: "Admin",
|
|
2057
|
-
data_table: "Data Table",
|
|
2058
|
-
shell_navigation: "Shell",
|
|
2059
|
-
forms: "Forms",
|
|
2060
|
-
embed: "Embed",
|
|
2061
|
-
notifications: "Notifications"
|
|
2557
|
+
return {
|
|
2558
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2559
|
+
passed: errors.length === 0,
|
|
2560
|
+
errors,
|
|
2561
|
+
warnings,
|
|
2562
|
+
summary: { errorCount: errors.length, warningCount: warnings.length, passCount }
|
|
2062
2563
|
};
|
|
2063
|
-
return labels[domain] || titleCase(domain.replace(/_/g, " "));
|
|
2064
2564
|
}
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2565
|
+
|
|
2566
|
+
// ../brain/dist/pipeline/orchestrator.js
|
|
2567
|
+
async function runPipeline(projectRoot) {
|
|
2568
|
+
const inv = await runInventory(projectRoot);
|
|
2569
|
+
const res = await runResolution(projectRoot, inv);
|
|
2570
|
+
const cr = await runClassification(projectRoot, inv, res);
|
|
2571
|
+
const scoring = await runScoring(projectRoot, cr);
|
|
2572
|
+
await writeGraph(projectRoot, res.graph);
|
|
2573
|
+
await writeAnalysis(projectRoot, scoring.store);
|
|
2574
|
+
const files = cr.classified.filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity).map((f) => ({
|
|
2575
|
+
path: f.abs,
|
|
2576
|
+
relativePath: f.rel,
|
|
2577
|
+
language: f.lang,
|
|
2578
|
+
isRealSource: f.isRealSource,
|
|
2579
|
+
demoteReason: f.demoteReason,
|
|
2580
|
+
gravity: Math.round(f.gravity),
|
|
2581
|
+
heat: Math.round(f.heat),
|
|
2582
|
+
gravitySignals: f.gravitySignals,
|
|
2583
|
+
heatSignals: f.heatSignals,
|
|
2584
|
+
smells: f.smells,
|
|
2585
|
+
pillarHint: f.pillarHint,
|
|
2586
|
+
frameworkRole: f.frameworkRole,
|
|
2587
|
+
productDomain: f.productDomain,
|
|
2588
|
+
sideEffectProfile: f.sideEffectProfile
|
|
2589
|
+
}));
|
|
2590
|
+
const wildCandidates = cr.classified.filter((f) => f.isRealSource && (f.heat >= 60 || f.smells.some((s) => s.severity >= 4))).sort((a, b) => b.heat - a.heat).map((f) => ({
|
|
2591
|
+
path: f.abs,
|
|
2592
|
+
relativePath: f.rel,
|
|
2593
|
+
language: f.lang,
|
|
2594
|
+
isRealSource: f.isRealSource,
|
|
2595
|
+
demoteReason: f.demoteReason,
|
|
2596
|
+
gravity: Math.round(f.gravity),
|
|
2597
|
+
heat: Math.round(f.heat),
|
|
2598
|
+
gravitySignals: f.gravitySignals,
|
|
2599
|
+
heatSignals: f.heatSignals,
|
|
2600
|
+
smells: f.smells,
|
|
2601
|
+
pillarHint: f.pillarHint,
|
|
2602
|
+
frameworkRole: f.frameworkRole,
|
|
2603
|
+
productDomain: f.productDomain,
|
|
2604
|
+
sideEffectProfile: f.sideEffectProfile
|
|
2605
|
+
}));
|
|
2606
|
+
const uiUrl = `file://${join8(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
|
|
2607
|
+
return {
|
|
2608
|
+
projectRoot,
|
|
2609
|
+
totalFilesScanned: cr.classified.length,
|
|
2610
|
+
realSourceCount: files.length,
|
|
2611
|
+
files,
|
|
2612
|
+
map: cr.map,
|
|
2613
|
+
wildCandidates,
|
|
2614
|
+
uiUrl,
|
|
2615
|
+
graph: res.graph,
|
|
2616
|
+
validation: {
|
|
2617
|
+
passed: scoring.validationReport.passed,
|
|
2618
|
+
errors: scoring.validationReport.summary.errorCount,
|
|
2619
|
+
warnings: scoring.validationReport.summary.warningCount,
|
|
2620
|
+
reportPath: ".vibe-splainer/validation_report.json"
|
|
2085
2621
|
}
|
|
2086
|
-
|
|
2087
|
-
if (top)
|
|
2088
|
-
return titleCase(top[0]);
|
|
2089
|
-
}
|
|
2090
|
-
const topFile = basename(files[0].relativePath, extname(files[0].relativePath));
|
|
2091
|
-
return titleCase(topFile);
|
|
2622
|
+
};
|
|
2092
2623
|
}
|
|
2093
|
-
|
|
2094
|
-
|
|
2624
|
+
|
|
2625
|
+
// ../brain/dist/scanner.js
|
|
2626
|
+
async function initParser2() {
|
|
2627
|
+
return initParser();
|
|
2628
|
+
}
|
|
2629
|
+
async function scanProject(projectRoot) {
|
|
2630
|
+
return runPipeline(projectRoot);
|
|
2095
2631
|
}
|
|
2096
2632
|
async function getFileAnalysis(absPath) {
|
|
2097
|
-
const ext =
|
|
2633
|
+
const ext = extname4(absPath);
|
|
2098
2634
|
const lang = EXT_LANG[ext];
|
|
2099
2635
|
if (!lang)
|
|
2100
2636
|
return null;
|
|
2101
2637
|
let source;
|
|
2102
2638
|
try {
|
|
2103
|
-
source = await
|
|
2639
|
+
source = await readFile7(absPath, "utf8");
|
|
2104
2640
|
} catch {
|
|
2105
2641
|
return null;
|
|
2106
2642
|
}
|
|
@@ -2119,20 +2655,19 @@ async function getFileAnalysis(absPath) {
|
|
|
2119
2655
|
reason: `${s.kind}: ${s.note}`
|
|
2120
2656
|
};
|
|
2121
2657
|
});
|
|
2122
|
-
const heatSignals = {
|
|
2123
|
-
todos: ast.smells.filter((s) => s.kind === "todo").length,
|
|
2124
|
-
suppressions: ast.smells.filter((s) => s.kind === "suppression").length,
|
|
2125
|
-
swallowedCatches: ast.swallowedCatches,
|
|
2126
|
-
maxNesting: ast.maxNesting,
|
|
2127
|
-
longFunctions: ast.longFunctions,
|
|
2128
|
-
magicNumbers: ast.magicNumbers
|
|
2129
|
-
};
|
|
2130
2658
|
return {
|
|
2131
2659
|
language: lang,
|
|
2132
2660
|
signature: ast.signature,
|
|
2133
2661
|
hotSpans: ast.hotSpans,
|
|
2134
2662
|
smellSpans,
|
|
2135
|
-
heatSignals
|
|
2663
|
+
heatSignals: {
|
|
2664
|
+
todos: ast.smells.filter((s) => s.kind === "todo").length,
|
|
2665
|
+
suppressions: ast.smells.filter((s) => s.kind === "suppression").length,
|
|
2666
|
+
swallowedCatches: ast.swallowedCatches,
|
|
2667
|
+
maxNesting: ast.maxNesting,
|
|
2668
|
+
longFunctions: ast.longFunctions,
|
|
2669
|
+
magicNumbers: ast.magicNumbers
|
|
2670
|
+
},
|
|
2136
2671
|
loc: ast.loc,
|
|
2137
2672
|
cyclomatic: ast.cyclomatic
|
|
2138
2673
|
};
|
|
@@ -2140,16 +2675,16 @@ async function getFileAnalysis(absPath) {
|
|
|
2140
2675
|
|
|
2141
2676
|
// ../brain/dist/dossier.js
|
|
2142
2677
|
import { Mutex } from "async-mutex";
|
|
2143
|
-
import { join as
|
|
2678
|
+
import { join as join9, dirname as dirname3 } from "path";
|
|
2144
2679
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2145
|
-
import { readFile as
|
|
2146
|
-
import { existsSync as
|
|
2147
|
-
var __dirname2 =
|
|
2680
|
+
import { readFile as readFile8, writeFile as writeFile8, mkdir as mkdir7 } from "fs/promises";
|
|
2681
|
+
import { existsSync as existsSync4, cpSync } from "fs";
|
|
2682
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
2148
2683
|
var dossierMutex = new Mutex();
|
|
2149
2684
|
async function readDossier(projectRoot) {
|
|
2150
|
-
const dossierPath =
|
|
2685
|
+
const dossierPath = join9(projectRoot, ".vibe-splainer", "dossier.json");
|
|
2151
2686
|
try {
|
|
2152
|
-
const raw = await
|
|
2687
|
+
const raw = await readFile8(dossierPath, "utf8");
|
|
2153
2688
|
return JSON.parse(raw);
|
|
2154
2689
|
} catch {
|
|
2155
2690
|
return null;
|
|
@@ -2161,33 +2696,33 @@ async function writeDossier(projectRoot, dossier) {
|
|
|
2161
2696
|
p.decisions = p.decisions.filter((c) => !(c.severity === 1 && c.category === "Convention"));
|
|
2162
2697
|
p.cardCount = p.decisions.length;
|
|
2163
2698
|
}
|
|
2164
|
-
const dir =
|
|
2165
|
-
await
|
|
2166
|
-
const dossierPath =
|
|
2699
|
+
const dir = join9(projectRoot, ".vibe-splainer");
|
|
2700
|
+
await mkdir7(dir, { recursive: true });
|
|
2701
|
+
const dossierPath = join9(dir, "dossier.json");
|
|
2167
2702
|
const tmp = dossierPath + ".tmp";
|
|
2168
|
-
await
|
|
2703
|
+
await writeFile8(tmp, JSON.stringify(dossier, null, 2), "utf8");
|
|
2169
2704
|
const { rename } = await import("fs/promises");
|
|
2170
2705
|
await rename(tmp, dossierPath);
|
|
2171
2706
|
await regenerateUI(projectRoot, dossier);
|
|
2172
2707
|
});
|
|
2173
2708
|
}
|
|
2174
2709
|
async function regenerateUI(projectRoot, dossier) {
|
|
2175
|
-
const uiDir =
|
|
2176
|
-
await
|
|
2177
|
-
let templateDir =
|
|
2178
|
-
if (!
|
|
2179
|
-
templateDir =
|
|
2710
|
+
const uiDir = join9(projectRoot, ".vibe-splainer", "ui");
|
|
2711
|
+
await mkdir7(uiDir, { recursive: true });
|
|
2712
|
+
let templateDir = join9(__dirname2, "ui");
|
|
2713
|
+
if (!existsSync4(templateDir)) {
|
|
2714
|
+
templateDir = join9(__dirname2, "../../cli/dist/ui");
|
|
2180
2715
|
}
|
|
2181
|
-
if (!
|
|
2716
|
+
if (!existsSync4(templateDir)) {
|
|
2182
2717
|
console.error("[vibe-splain] UI template not found at", templateDir, "- skipping UI regeneration");
|
|
2183
2718
|
return;
|
|
2184
2719
|
}
|
|
2185
2720
|
cpSync(templateDir, uiDir, { recursive: true });
|
|
2186
|
-
let html = await
|
|
2721
|
+
let html = await readFile8(join9(templateDir, "index.html"), "utf8");
|
|
2187
2722
|
const injection = `<script>window.__VIBE_DOSSIER__ = ${JSON.stringify(dossier)};</script>`;
|
|
2188
2723
|
html = html.replace("<!-- VIBE_DOSSIER_INJECTION_POINT -->", injection);
|
|
2189
|
-
await
|
|
2190
|
-
console.error("[vibe-splain] UI regenerated at",
|
|
2724
|
+
await writeFile8(join9(uiDir, "index.html"), html, "utf8");
|
|
2725
|
+
console.error("[vibe-splain] UI regenerated at", join9(uiDir, "index.html"));
|
|
2191
2726
|
}
|
|
2192
2727
|
function validateMermaidNodeCount(diagram) {
|
|
2193
2728
|
if (!diagram)
|
|
@@ -2207,8 +2742,8 @@ function validateMermaidNodeCount(diagram) {
|
|
|
2207
2742
|
// ../brain/dist/watcher.js
|
|
2208
2743
|
import chokidar from "chokidar";
|
|
2209
2744
|
import { createHash as createHash2 } from "crypto";
|
|
2210
|
-
import { readFile as
|
|
2211
|
-
import { join as
|
|
2745
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
2746
|
+
import { join as join10 } from "path";
|
|
2212
2747
|
function startWatcher(projectRoot, watchedPaths) {
|
|
2213
2748
|
const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
|
|
2214
2749
|
ignoreInitial: true,
|
|
@@ -2220,14 +2755,14 @@ function startWatcher(projectRoot, watchedPaths) {
|
|
|
2220
2755
|
const dossier = await readDossier(projectRoot);
|
|
2221
2756
|
if (!dossier)
|
|
2222
2757
|
return;
|
|
2223
|
-
const content = await
|
|
2758
|
+
const content = await readFile9(filepath, "utf8");
|
|
2224
2759
|
const newHash = createHash2("sha256").update(content).digest("hex");
|
|
2225
2760
|
let mutated = false;
|
|
2226
2761
|
for (const pillar of dossier.pillars) {
|
|
2227
2762
|
for (const card of pillar.decisions) {
|
|
2228
2763
|
if (!card.primaryFile)
|
|
2229
2764
|
continue;
|
|
2230
|
-
const absMatch = filepath ===
|
|
2765
|
+
const absMatch = filepath === join10(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
|
|
2231
2766
|
if (absMatch && card.lastScannedHash !== newHash) {
|
|
2232
2767
|
card.status = "stale";
|
|
2233
2768
|
const rel = card.primaryFile;
|
|
@@ -2286,7 +2821,22 @@ async function handleScanProject(args) {
|
|
|
2286
2821
|
await writeDossier(projectRoot, dossier);
|
|
2287
2822
|
startWatcher(projectRoot, result.files.map((f) => f.path));
|
|
2288
2823
|
console.error(`[vibe-splain] Scan complete. ${result.totalFilesScanned} files, ${result.realSourceCount} real-source, ${result.wildCandidates.length} wild candidates.`);
|
|
2824
|
+
const validation = result.validation ?? { passed: true, errors: 0, warnings: 0, reportPath: ".vibe-splainer/validation_report.json" };
|
|
2289
2825
|
return {
|
|
2826
|
+
ok: true,
|
|
2827
|
+
validation: {
|
|
2828
|
+
passed: validation.passed,
|
|
2829
|
+
errors: validation.errors,
|
|
2830
|
+
warnings: validation.warnings,
|
|
2831
|
+
reportPath: validation.reportPath
|
|
2832
|
+
},
|
|
2833
|
+
artifacts: {
|
|
2834
|
+
analysis: ".vibe-splainer/analysis.json",
|
|
2835
|
+
deltaTargets: ".vibe-splainer/delta_targets.json",
|
|
2836
|
+
dossier: ".vibe-splainer/dossier.json",
|
|
2837
|
+
graph: ".vibe-splainer/graph.json",
|
|
2838
|
+
html: ".vibe-splainer/ui/index.html"
|
|
2839
|
+
},
|
|
2290
2840
|
projectRoot: result.projectRoot,
|
|
2291
2841
|
totalFilesScanned: result.totalFilesScanned,
|
|
2292
2842
|
realSourceCount: result.realSourceCount,
|
|
@@ -2382,8 +2932,8 @@ async function handleSetProjectBrief(args) {
|
|
|
2382
2932
|
}
|
|
2383
2933
|
|
|
2384
2934
|
// dist/mcp/tools/get_file_context.js
|
|
2385
|
-
import { readFile as
|
|
2386
|
-
import { join as
|
|
2935
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2936
|
+
import { join as join11, relative as relative3, isAbsolute } from "path";
|
|
2387
2937
|
var getFileContextTool = {
|
|
2388
2938
|
name: "get_file_context",
|
|
2389
2939
|
description: "Returns PRE-EXTRACTED evidence for a file so you do not have to read the whole thing and paraphrase its header comment. Returns: gravity/heat scores + signals, importedBy (named fan-in \u2014 use this for blastRadius), hotSpans (the gnarliest function bodies, comment-stripped, each with a reason), smellSpans (located tech debt with \xB13 lines of context), and signature (the exported API surface). Base your evidence on hotSpans/smellSpans \u2014 NEVER on header comments. Pass { full: true } only if you truly need the raw source.",
|
|
@@ -2403,8 +2953,8 @@ async function handleGetFileContext(args) {
|
|
|
2403
2953
|
const full = args.full === true;
|
|
2404
2954
|
if (!projectRoot || !filePath)
|
|
2405
2955
|
throw new Error("projectRoot and filePath are required");
|
|
2406
|
-
const fullPath = isAbsolute(filePath) ? filePath :
|
|
2407
|
-
const relPath =
|
|
2956
|
+
const fullPath = isAbsolute(filePath) ? filePath : join11(projectRoot, filePath);
|
|
2957
|
+
const relPath = relative3(projectRoot, fullPath);
|
|
2408
2958
|
const evidence = await getFileAnalysis(fullPath);
|
|
2409
2959
|
if (!evidence) {
|
|
2410
2960
|
throw new Error(`Could not analyze ${relPath} (unsupported language or parse failure).`);
|
|
@@ -2428,7 +2978,7 @@ async function handleGetFileContext(args) {
|
|
|
2428
2978
|
smellSpans: evidence.smellSpans
|
|
2429
2979
|
};
|
|
2430
2980
|
if (full) {
|
|
2431
|
-
result.source = await
|
|
2981
|
+
result.source = await readFile10(fullPath, "utf8");
|
|
2432
2982
|
}
|
|
2433
2983
|
return result;
|
|
2434
2984
|
}
|
|
@@ -2436,8 +2986,8 @@ async function handleGetFileContext(args) {
|
|
|
2436
2986
|
// dist/mcp/tools/write_decision_card.js
|
|
2437
2987
|
import { v4 as uuidv4 } from "uuid";
|
|
2438
2988
|
import { createHash as createHash3 } from "crypto";
|
|
2439
|
-
import { readFile as
|
|
2440
|
-
import { join as
|
|
2989
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
2990
|
+
import { join as join12 } from "path";
|
|
2441
2991
|
var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
|
|
2442
2992
|
function normalizeSnippet(s) {
|
|
2443
2993
|
let out = (s ?? "").replace(/\r\n/g, "\n");
|
|
@@ -2529,7 +3079,7 @@ async function handleWriteDecisionCard(args) {
|
|
|
2529
3079
|
const heat = persisted ? Math.round(persisted.heat) : void 0;
|
|
2530
3080
|
let primaryContent = "";
|
|
2531
3081
|
try {
|
|
2532
|
-
primaryContent = await
|
|
3082
|
+
primaryContent = await readFile11(join12(projectRoot, primaryFile), "utf8");
|
|
2533
3083
|
} catch {
|
|
2534
3084
|
}
|
|
2535
3085
|
const hash = createHash3("sha256").update(primaryContent).digest("hex");
|
|
@@ -2773,7 +3323,7 @@ var TOOL_HANDLERS = {
|
|
|
2773
3323
|
mark_stale: handleMarkStale
|
|
2774
3324
|
};
|
|
2775
3325
|
async function startMCPServer() {
|
|
2776
|
-
await
|
|
3326
|
+
await initParser2();
|
|
2777
3327
|
console.error("[vibe-splain] Tree-Sitter parser initialized");
|
|
2778
3328
|
const server = new Server({ name: "vibe-splain", version: "2.0.0" }, { capabilities: { tools: {}, prompts: {} } });
|
|
2779
3329
|
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
@@ -2898,7 +3448,7 @@ async function serveCommand() {
|
|
|
2898
3448
|
|
|
2899
3449
|
// dist/index.js
|
|
2900
3450
|
var program = new Command();
|
|
2901
|
-
program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("2.
|
|
3451
|
+
program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("2.5.0");
|
|
2902
3452
|
program.command("install").description("Patch coding agent MCP config files to register vibe-splain").action(installCommand);
|
|
2903
3453
|
program.command("serve").description("Start the MCP server (called by the coding agent, not by you)").action(serveCommand);
|
|
2904
3454
|
program.parse();
|