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