vibe-splain 2.3.1 → 2.4.1
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 +714 -59
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -106,6 +106,7 @@ async function writeGraph(projectRoot, graph) {
|
|
|
106
106
|
|
|
107
107
|
// ../brain/dist/analysis.js
|
|
108
108
|
import { join as join3 } from "path";
|
|
109
|
+
import { createHash } from "crypto";
|
|
109
110
|
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
110
111
|
async function readAnalysis(projectRoot) {
|
|
111
112
|
const p = join3(projectRoot, ".vibe-splainer", "analysis.json");
|
|
@@ -125,16 +126,437 @@ async function writeAnalysis(projectRoot, store) {
|
|
|
125
126
|
const { rename } = await import("fs/promises");
|
|
126
127
|
await rename(tmp, dest);
|
|
127
128
|
}
|
|
128
|
-
var
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
var ENTRYPOINT_ROLES = /* @__PURE__ */ new Set([
|
|
130
|
+
"app_route_page",
|
|
131
|
+
"app_route_handler",
|
|
132
|
+
"pages_route",
|
|
133
|
+
"pages_api_route",
|
|
134
|
+
"trpc_api_route"
|
|
135
|
+
]);
|
|
136
|
+
function findRuntimeEntrypoints(relPath, files, maxDepth = 8) {
|
|
137
|
+
const results = [];
|
|
138
|
+
const seen = /* @__PURE__ */ new Set();
|
|
139
|
+
const queue = [{ path: relPath, depth: 0 }];
|
|
140
|
+
while (queue.length > 0) {
|
|
141
|
+
const current = queue.shift();
|
|
142
|
+
if (seen.has(current.path))
|
|
143
|
+
continue;
|
|
144
|
+
seen.add(current.path);
|
|
145
|
+
if (current.path !== relPath) {
|
|
146
|
+
const meta2 = files[current.path];
|
|
147
|
+
if (meta2 && ENTRYPOINT_ROLES.has(meta2.frameworkRole)) {
|
|
148
|
+
results.push({
|
|
149
|
+
path: current.path,
|
|
150
|
+
frameworkRole: meta2.frameworkRole,
|
|
151
|
+
productDomain: meta2.productDomain,
|
|
152
|
+
distance: current.depth
|
|
153
|
+
});
|
|
154
|
+
if (results.length >= 8)
|
|
155
|
+
break;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (current.depth >= maxDepth)
|
|
160
|
+
continue;
|
|
161
|
+
const meta = files[current.path];
|
|
162
|
+
if (!meta)
|
|
163
|
+
continue;
|
|
164
|
+
for (const importer of meta.importedBy) {
|
|
165
|
+
if (!seen.has(importer)) {
|
|
166
|
+
queue.push({ path: importer, depth: current.depth + 1 });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
171
|
+
for (const r of results) {
|
|
172
|
+
const existing = byPath.get(r.path);
|
|
173
|
+
if (!existing || r.distance < existing.distance)
|
|
174
|
+
byPath.set(r.path, r);
|
|
175
|
+
}
|
|
176
|
+
return [...byPath.values()].sort((a, b) => a.distance - b.distance);
|
|
177
|
+
}
|
|
178
|
+
function deriveEntrypointTraceStatus(entrypoints, unresolved) {
|
|
179
|
+
if (entrypoints.length > 0 && unresolved.length === 0)
|
|
180
|
+
return "complete";
|
|
181
|
+
if (entrypoints.length > 0)
|
|
182
|
+
return "partial";
|
|
183
|
+
if (unresolved.length > 0)
|
|
184
|
+
return "blocked_by_alias_resolution";
|
|
185
|
+
return "no_runtime_entrypoint_found";
|
|
186
|
+
}
|
|
187
|
+
function computeLoadBearingScore(f, entrypoints) {
|
|
188
|
+
let score = 0;
|
|
189
|
+
if (f.gravity >= 85)
|
|
190
|
+
score += 2;
|
|
191
|
+
if (f.heat >= 60)
|
|
192
|
+
score += 1;
|
|
193
|
+
if (entrypoints.length >= 2)
|
|
194
|
+
score += 2;
|
|
195
|
+
if (f.importedBy.length >= 3)
|
|
196
|
+
score += 1;
|
|
197
|
+
if (f.sideEffectProfile.includes("database_write"))
|
|
198
|
+
score += 3;
|
|
199
|
+
if (f.sideEffectProfile.includes("booking_mutation"))
|
|
200
|
+
score += 3;
|
|
201
|
+
if (f.sideEffectProfile.includes("payment_mutation"))
|
|
202
|
+
score += 3;
|
|
203
|
+
if (f.sideEffectProfile.includes("auth_token_mutation"))
|
|
204
|
+
score += 3;
|
|
205
|
+
if (f.sideEffectProfile.includes("webhook_delivery"))
|
|
206
|
+
score += 2;
|
|
207
|
+
if (f.sideEffectProfile.includes("webhook_ingress"))
|
|
208
|
+
score += 2;
|
|
209
|
+
if (f.sideEffectProfile.includes("calendar_mutation"))
|
|
210
|
+
score += 2;
|
|
211
|
+
if (f.sideEffectProfile.includes("redirect"))
|
|
212
|
+
score += 1;
|
|
213
|
+
if (f.sideEffectProfile.includes("analytics_event"))
|
|
214
|
+
score += 1;
|
|
215
|
+
const highImpactDomains = [
|
|
216
|
+
"booking_creation",
|
|
217
|
+
"payments",
|
|
218
|
+
"auth_oauth",
|
|
219
|
+
"webhooks",
|
|
220
|
+
"payments_webhooks"
|
|
221
|
+
];
|
|
222
|
+
if (highImpactDomains.includes(f.productDomain))
|
|
223
|
+
score += 2;
|
|
224
|
+
const maxSeverity = f.smells.length > 0 ? Math.max(...f.smells.map((s) => s.severity)) : 0;
|
|
225
|
+
if (maxSeverity === 5)
|
|
226
|
+
score += 3;
|
|
227
|
+
return score;
|
|
228
|
+
}
|
|
229
|
+
function computeSeverity(f, entrypoints) {
|
|
230
|
+
let score = 0;
|
|
231
|
+
if (f.sideEffectProfile.includes("database_write"))
|
|
232
|
+
score += 3;
|
|
233
|
+
if (f.sideEffectProfile.includes("booking_mutation"))
|
|
234
|
+
score += 4;
|
|
235
|
+
if (f.sideEffectProfile.includes("payment_mutation"))
|
|
236
|
+
score += 4;
|
|
237
|
+
if (f.sideEffectProfile.includes("auth_token_mutation"))
|
|
238
|
+
score += 4;
|
|
239
|
+
if (f.sideEffectProfile.includes("webhook_delivery"))
|
|
240
|
+
score += 3;
|
|
241
|
+
if (f.sideEffectProfile.includes("webhook_ingress"))
|
|
242
|
+
score += 3;
|
|
243
|
+
if (f.sideEffectProfile.includes("calendar_mutation"))
|
|
244
|
+
score += 3;
|
|
245
|
+
if (f.productDomain === "booking_creation")
|
|
246
|
+
score += 3;
|
|
247
|
+
if (f.productDomain === "payments" || f.productDomain === "payments_webhooks")
|
|
248
|
+
score += 3;
|
|
249
|
+
if (f.productDomain === "auth_oauth")
|
|
250
|
+
score += 3;
|
|
251
|
+
if (f.productDomain === "webhooks")
|
|
252
|
+
score += 2;
|
|
253
|
+
if (f.gravity >= 85)
|
|
254
|
+
score += 2;
|
|
255
|
+
if (f.heat >= 70)
|
|
256
|
+
score += 2;
|
|
257
|
+
if (f.heatSignals.maxNesting >= 4)
|
|
258
|
+
score += 1;
|
|
259
|
+
if (f.heatSignals.longFunctions >= 1)
|
|
260
|
+
score += 1;
|
|
261
|
+
if (f.heatSignals.swallowedCatches >= 1)
|
|
262
|
+
score += 1;
|
|
263
|
+
if (entrypoints.length >= 2)
|
|
264
|
+
score += 2;
|
|
265
|
+
if (score >= 10)
|
|
266
|
+
return 5;
|
|
267
|
+
if (score >= 7)
|
|
268
|
+
return 4;
|
|
269
|
+
if (score >= 4)
|
|
270
|
+
return 3;
|
|
271
|
+
if (score >= 2)
|
|
272
|
+
return 2;
|
|
273
|
+
return 1;
|
|
274
|
+
}
|
|
275
|
+
function inferRiskTypes(f) {
|
|
276
|
+
const types = [];
|
|
277
|
+
const kinds = new Set(f.smells.map((s) => s.kind));
|
|
278
|
+
if (f.gravitySignals.cyclomatic > 20)
|
|
279
|
+
types.push("state_machine");
|
|
280
|
+
if (kinds.has("god-file")) {
|
|
281
|
+
if (f.frameworkRole === "hook")
|
|
282
|
+
types.push("god_hook");
|
|
283
|
+
else
|
|
284
|
+
types.push("god_component");
|
|
285
|
+
}
|
|
286
|
+
if (f.sideEffectProfile.length > 3 && !f.sideEffectProfile.includes("none_detected")) {
|
|
287
|
+
types.push("side_effect_coupling");
|
|
288
|
+
}
|
|
289
|
+
if (f.productDomain === "forms" && f.gravitySignals.fanIn > 5 && f.gravitySignals.publicSurface > 8)
|
|
290
|
+
types.push("registry_bottleneck");
|
|
291
|
+
if (f.sideEffectProfile.some((s) => ["booking_mutation", "payment_mutation", "auth_token_mutation"].includes(s)) && f.gravitySignals.cyclomatic > 10)
|
|
292
|
+
types.push("mutation_orchestration");
|
|
293
|
+
if (ENTRYPOINT_ROLES.has(f.frameworkRole) && f.sideEffectProfile.includes("database_write"))
|
|
294
|
+
types.push("route_handler_write_path");
|
|
295
|
+
if (kinds.has("swallowed-catch"))
|
|
296
|
+
types.push("error_swallowing");
|
|
297
|
+
if (f.sideEffectProfile.includes("local_storage") || f.sideEffectProfile.includes("indexed_db"))
|
|
298
|
+
types.push("storage_persistence_risk");
|
|
299
|
+
if (types.length === 0)
|
|
300
|
+
types.push("complexity_hotspot");
|
|
301
|
+
return types;
|
|
302
|
+
}
|
|
303
|
+
function inferObservableOutputs(f) {
|
|
304
|
+
const outputs = [];
|
|
305
|
+
if (f.sideEffectProfile.includes("redirect"))
|
|
306
|
+
outputs.push("redirect_url");
|
|
307
|
+
if (ENTRYPOINT_ROLES.has(f.frameworkRole))
|
|
308
|
+
outputs.push("http_status");
|
|
309
|
+
if (f.frameworkRole === "app_route_handler" || f.frameworkRole === "pages_api_route") {
|
|
310
|
+
outputs.push("json_response_shape");
|
|
311
|
+
}
|
|
312
|
+
if (f.productDomain === "booking_creation" || f.productDomain === "booking_management") {
|
|
313
|
+
outputs.push("booking_uid");
|
|
314
|
+
}
|
|
315
|
+
if (f.productDomain === "payments" || f.productDomain === "payments_webhooks") {
|
|
316
|
+
outputs.push("payment_status");
|
|
317
|
+
}
|
|
318
|
+
if (f.productDomain === "auth_oauth") {
|
|
319
|
+
outputs.push("auth_token");
|
|
320
|
+
}
|
|
321
|
+
if (f.sideEffectProfile.includes("webhook_delivery") || f.sideEffectProfile.includes("webhook_ingress")) {
|
|
322
|
+
outputs.push("webhook_payload");
|
|
323
|
+
}
|
|
324
|
+
if (f.sideEffectProfile.includes("calendar_mutation")) {
|
|
325
|
+
outputs.push("calendar_event_id");
|
|
326
|
+
}
|
|
327
|
+
if (f.sideEffectProfile.includes("email_send")) {
|
|
328
|
+
outputs.push("email_payload");
|
|
329
|
+
}
|
|
330
|
+
if (f.sideEffectProfile.includes("analytics_event")) {
|
|
331
|
+
outputs.push("sdk_event_name");
|
|
332
|
+
}
|
|
333
|
+
if (f.frameworkRole === "hook" || f.frameworkRole === "store") {
|
|
334
|
+
outputs.push("ui_state_transition");
|
|
335
|
+
}
|
|
336
|
+
return [...new Set(outputs)];
|
|
337
|
+
}
|
|
338
|
+
function inferWriteIntents(f) {
|
|
339
|
+
const intents = [];
|
|
340
|
+
if (f.productDomain === "booking_creation") {
|
|
341
|
+
intents.push("create_booking");
|
|
342
|
+
if (f.relativePath.includes("reschedule") || f.relativePath.includes("Reschedule"))
|
|
343
|
+
intents.push("reschedule_booking");
|
|
344
|
+
if (f.relativePath.includes("recurring") || f.relativePath.includes("Recurring")) {
|
|
345
|
+
intents.push("create_recurring_booking");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (f.productDomain === "booking_management") {
|
|
349
|
+
intents.push("cancel_booking");
|
|
350
|
+
}
|
|
351
|
+
if (f.productDomain === "event_type_configuration") {
|
|
352
|
+
intents.push("update_event_type");
|
|
353
|
+
}
|
|
354
|
+
if (f.productDomain === "availability") {
|
|
355
|
+
intents.push("update_availability");
|
|
356
|
+
}
|
|
357
|
+
if (f.productDomain === "payments") {
|
|
358
|
+
intents.push("create_payment");
|
|
359
|
+
}
|
|
360
|
+
if (f.productDomain === "payments_webhooks") {
|
|
361
|
+
intents.push("handle_payment_webhook");
|
|
362
|
+
}
|
|
363
|
+
if (f.productDomain === "auth_oauth") {
|
|
364
|
+
intents.push("issue_auth_token");
|
|
365
|
+
intents.push("refresh_auth_token");
|
|
366
|
+
}
|
|
367
|
+
if (f.sideEffectProfile.includes("webhook_delivery")) {
|
|
368
|
+
intents.push("send_webhook");
|
|
369
|
+
}
|
|
370
|
+
if (f.productDomain === "settings") {
|
|
371
|
+
intents.push("update_user_settings");
|
|
372
|
+
}
|
|
373
|
+
if (f.sideEffectProfile.includes("local_storage") || f.sideEffectProfile.includes("indexed_db")) {
|
|
374
|
+
intents.push("persist_local_state");
|
|
375
|
+
}
|
|
376
|
+
return intents.length > 0 ? intents : ["none_detected"];
|
|
377
|
+
}
|
|
378
|
+
function inferPatchRisk(f, score, riskTypes) {
|
|
379
|
+
if (score >= 12 || f.productDomain === "booking_creation" && riskTypes.includes("mutation_orchestration")) {
|
|
380
|
+
return {
|
|
381
|
+
level: "critical",
|
|
382
|
+
reason: `${f.productDomain} domain with ${riskTypes.join(", ")} \u2014 any patch risks breaking live booking, payment, or auth flows.`
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (score >= 8 || f.sideEffectProfile.includes("payment_mutation") || f.sideEffectProfile.includes("auth_token_mutation")) {
|
|
386
|
+
return {
|
|
387
|
+
level: "high",
|
|
388
|
+
reason: `${f.productDomain} writes to external state (${f.sideEffectProfile.filter((s) => ["payment_mutation", "auth_token_mutation", "database_write", "webhook_delivery"].includes(s)).join(", ") || "database"}). Changes require integration testing.`
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (score >= 5 || f.importedBy.length >= 5) {
|
|
392
|
+
return {
|
|
393
|
+
level: "medium",
|
|
394
|
+
reason: `Imported by ${f.importedBy.length} files. Interface changes will cascade.`
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
level: "low",
|
|
399
|
+
reason: "Locally contained \u2014 limited blast radius."
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function inferSafePatchStrategy(f, riskTypes) {
|
|
403
|
+
if (riskTypes.includes("mutation_orchestration")) {
|
|
404
|
+
return "Do not rewrite inline. Extract pure decision logic into a tested reducer or state machine first. Preserve all side-effect call sites (redirect URLs, SDK event names, response shapes) as invariants.";
|
|
405
|
+
}
|
|
406
|
+
if (riskTypes.includes("registry_bottleneck")) {
|
|
407
|
+
return "Add new entries without removing existing keys. Treat the registry map as append-only until all consumers are verified.";
|
|
408
|
+
}
|
|
409
|
+
if (riskTypes.includes("route_handler_write_path")) {
|
|
410
|
+
return "Add integration tests covering success and failure paths before modifying. Verify HTTP status codes and response shapes are preserved.";
|
|
411
|
+
}
|
|
412
|
+
if (riskTypes.includes("god_component") || riskTypes.includes("god_hook")) {
|
|
413
|
+
return "Extract sub-concerns into separate modules first. Only refactor the extraction points after tests confirm equivalence.";
|
|
414
|
+
}
|
|
415
|
+
if (f.sideEffectProfile.includes("database_write")) {
|
|
416
|
+
return "Wrap changes in a transaction or use a feature flag. Run against a staging database before production.";
|
|
417
|
+
}
|
|
418
|
+
return "Review importedBy before patching. Run affected integration tests.";
|
|
419
|
+
}
|
|
420
|
+
function inferDoNotTouch(f) {
|
|
421
|
+
const items = [];
|
|
422
|
+
if (f.sideEffectProfile.includes("payment_mutation"))
|
|
423
|
+
items.push("payment flow branch");
|
|
424
|
+
if (f.sideEffectProfile.includes("auth_token_mutation"))
|
|
425
|
+
items.push("token issuance / refresh branch");
|
|
426
|
+
if (f.sideEffectProfile.includes("webhook_delivery") || f.sideEffectProfile.includes("webhook_ingress")) {
|
|
427
|
+
items.push("webhook payload shape");
|
|
428
|
+
}
|
|
429
|
+
if (f.sideEffectProfile.includes("redirect"))
|
|
430
|
+
items.push("redirect URL strings");
|
|
431
|
+
if (f.sideEffectProfile.includes("analytics_event"))
|
|
432
|
+
items.push("SDK event names");
|
|
433
|
+
if (f.sideEffectProfile.includes("booking_mutation")) {
|
|
434
|
+
items.push("booking success response shape", "recurring booking branch");
|
|
435
|
+
}
|
|
436
|
+
if (f.productDomain === "auth_oauth")
|
|
437
|
+
items.push("OAuth callback URLs", "token scopes");
|
|
438
|
+
return items;
|
|
439
|
+
}
|
|
440
|
+
function inferTestProbes(f, writeIntents, observableOutputs) {
|
|
441
|
+
const probes = [];
|
|
442
|
+
if (writeIntents.includes("create_booking")) {
|
|
443
|
+
probes.push({
|
|
444
|
+
name: "standard booking success",
|
|
445
|
+
scenario: "create a standard booking and assert success redirect and booking uid",
|
|
446
|
+
expectedObservable: ["booking_uid", "redirect_url", "sdk_event_name"].filter((o) => observableOutputs.includes(o))
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (writeIntents.includes("reschedule_booking")) {
|
|
450
|
+
probes.push({
|
|
451
|
+
name: "reschedule booking",
|
|
452
|
+
scenario: "reschedule an existing booking and assert reschedule event path",
|
|
453
|
+
expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
if (writeIntents.includes("create_recurring_booking")) {
|
|
457
|
+
probes.push({
|
|
458
|
+
name: "recurring booking",
|
|
459
|
+
scenario: "create recurring booking and assert recurring success behavior",
|
|
460
|
+
expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
if (writeIntents.includes("handle_payment_webhook")) {
|
|
464
|
+
probes.push({
|
|
465
|
+
name: "payment webhook ingestion",
|
|
466
|
+
scenario: "send a valid payment webhook and assert booking/payment state updated",
|
|
467
|
+
expectedObservable: ["payment_status", "booking_uid", "http_status"].filter((o) => observableOutputs.includes(o))
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (writeIntents.includes("issue_auth_token")) {
|
|
471
|
+
probes.push({
|
|
472
|
+
name: "token issuance",
|
|
473
|
+
scenario: "complete OAuth flow and assert access token issued with correct scopes",
|
|
474
|
+
expectedObservable: ["auth_token", "http_status"].filter((o) => observableOutputs.includes(o))
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return probes;
|
|
478
|
+
}
|
|
479
|
+
function buildRawEvidence(f) {
|
|
480
|
+
return f.hotSpans.map((span) => ({
|
|
481
|
+
file: f.relativePath,
|
|
482
|
+
startLine: span.startLine,
|
|
483
|
+
endLine: span.endLine,
|
|
484
|
+
rawSourceExcerpt: span.snippet,
|
|
485
|
+
evidenceHash: createHash("sha256").update(span.snippet).digest("hex").slice(0, 12)
|
|
137
486
|
}));
|
|
487
|
+
}
|
|
488
|
+
function deriveConfidence(f) {
|
|
489
|
+
if (f.gravitySignals.fanIn >= 10 && f.gravity >= 40)
|
|
490
|
+
return "high";
|
|
491
|
+
if (f.gravitySignals.fanIn >= 5 || f.gravity >= 25)
|
|
492
|
+
return "medium";
|
|
493
|
+
return "low";
|
|
494
|
+
}
|
|
495
|
+
function validateTarget(target) {
|
|
496
|
+
const warn = (msg) => console.error(`[vibe-splain] WARN ${target.path}: ${msg}`);
|
|
497
|
+
const err = (msg) => console.error(`[vibe-splain] ERR ${target.path}: ${msg}`);
|
|
498
|
+
if (target.severity >= 4 && target.runtimeEntrypoints.length === 0) {
|
|
499
|
+
warn("high severity target has no runtime entrypoints \u2014 check alias resolution");
|
|
500
|
+
}
|
|
501
|
+
if (target.severity === 5 && !target.isLoadBearing) {
|
|
502
|
+
err("severity 5 target must be load bearing");
|
|
503
|
+
}
|
|
504
|
+
if (target.productDomain === "routing_infrastructure" && !target.path.includes("middleware") && !target.path.includes("router") && !target.path.includes("Navigation")) {
|
|
505
|
+
warn("possible over-classification as routing_infrastructure");
|
|
506
|
+
}
|
|
507
|
+
if ((target.path.includes("payment") || target.path.includes("stripe") || target.path.includes("paypal")) && !target.sideEffectProfile.includes("payment_mutation") && !target.sideEffectProfile.includes("webhook_ingress")) {
|
|
508
|
+
warn("payment file missing payment side effect classification");
|
|
509
|
+
}
|
|
510
|
+
if (target.rawEvidence.some((e) => e.rawSourceExcerpt.includes("// ..") || e.rawSourceExcerpt.includes("/* ..."))) {
|
|
511
|
+
err("raw evidence appears summarized or annotated");
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async function writeDeltaTargets(projectRoot, store, _entrypoints = /* @__PURE__ */ new Set()) {
|
|
515
|
+
const targets = Object.values(store.files).filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity).map((f) => {
|
|
516
|
+
const runtimeEntrypoints = findRuntimeEntrypoints(f.relativePath, store.files);
|
|
517
|
+
const entrypointTraceStatus = deriveEntrypointTraceStatus(runtimeEntrypoints, f.importsUnresolved);
|
|
518
|
+
const loadBearingScore = computeLoadBearingScore(f, runtimeEntrypoints);
|
|
519
|
+
const severity = computeSeverity(f, runtimeEntrypoints);
|
|
520
|
+
const riskTypes = inferRiskTypes(f);
|
|
521
|
+
const observableOutputs = inferObservableOutputs(f);
|
|
522
|
+
const writeIntents = inferWriteIntents(f);
|
|
523
|
+
const patchRisk = inferPatchRisk(f, loadBearingScore, riskTypes);
|
|
524
|
+
const rawEvidence = buildRawEvidence(f);
|
|
525
|
+
const confidence = deriveConfidence(f);
|
|
526
|
+
const fileHashInput = f.hotSpans.map((h) => h.snippet).join("");
|
|
527
|
+
const fileHash = createHash("sha256").update(fileHashInput || f.relativePath).digest("hex").slice(0, 12);
|
|
528
|
+
const target = {
|
|
529
|
+
path: f.relativePath,
|
|
530
|
+
frameworkRole: f.frameworkRole,
|
|
531
|
+
productDomain: f.productDomain,
|
|
532
|
+
gravity: Math.round(f.gravity),
|
|
533
|
+
heat: Math.round(f.heat),
|
|
534
|
+
severity,
|
|
535
|
+
confidence,
|
|
536
|
+
isLoadBearing: loadBearingScore >= 5,
|
|
537
|
+
loadBearingScore,
|
|
538
|
+
riskTypes,
|
|
539
|
+
sideEffectProfile: f.sideEffectProfile,
|
|
540
|
+
blastRadius: f.importedBy,
|
|
541
|
+
runtimeEntrypoints,
|
|
542
|
+
entrypointTraceStatus,
|
|
543
|
+
blockedImports: f.importsUnresolved,
|
|
544
|
+
observableOutputs,
|
|
545
|
+
writeIntents,
|
|
546
|
+
patchRisk,
|
|
547
|
+
safePatchStrategy: inferSafePatchStrategy(f, riskTypes),
|
|
548
|
+
doNotTouch: inferDoNotTouch(f),
|
|
549
|
+
testProbes: inferTestProbes(f, writeIntents, observableOutputs),
|
|
550
|
+
rawEvidence,
|
|
551
|
+
analysisAnnotation: `${f.frameworkRole} in ${f.productDomain} domain. fanIn=${f.gravitySignals.fanIn} cyclomatic=${f.gravitySignals.cyclomatic} loc=${f.gravitySignals.loc}`,
|
|
552
|
+
hashes: {
|
|
553
|
+
fileHash,
|
|
554
|
+
evidenceHash: rawEvidence.map((e) => e.evidenceHash).join("-")
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
validateTarget(target);
|
|
558
|
+
return target;
|
|
559
|
+
});
|
|
138
560
|
const dir = join3(projectRoot, ".vibe-splainer");
|
|
139
561
|
await mkdir2(dir, { recursive: true });
|
|
140
562
|
const dest = join3(dir, "delta_targets.json");
|
|
@@ -256,7 +678,14 @@ var DEMOTE_SEGMENTS = /* @__PURE__ */ new Set([
|
|
|
256
678
|
"fixtures",
|
|
257
679
|
"fixture",
|
|
258
680
|
"__generated__",
|
|
259
|
-
"__mocks__"
|
|
681
|
+
"__mocks__",
|
|
682
|
+
"playwright",
|
|
683
|
+
"e2e",
|
|
684
|
+
"__tests__",
|
|
685
|
+
"cypress",
|
|
686
|
+
"storybook",
|
|
687
|
+
"stories",
|
|
688
|
+
".storybook"
|
|
260
689
|
]);
|
|
261
690
|
var VENDOR_SEGMENTS = /* @__PURE__ */ new Set([
|
|
262
691
|
"node_modules",
|
|
@@ -318,20 +747,6 @@ var PILLAR_KEYWORDS = {
|
|
|
318
747
|
"paddle",
|
|
319
748
|
"lemon-squeezy"
|
|
320
749
|
],
|
|
321
|
-
"Routing": [
|
|
322
|
-
"express",
|
|
323
|
-
"fastify",
|
|
324
|
-
"koa",
|
|
325
|
-
"koa-router",
|
|
326
|
-
"next/router",
|
|
327
|
-
"next/navigation",
|
|
328
|
-
"react-router",
|
|
329
|
-
"@remix-run/",
|
|
330
|
-
"hono",
|
|
331
|
-
"express-rate-limit",
|
|
332
|
-
"cors",
|
|
333
|
-
"helmet"
|
|
334
|
-
],
|
|
335
750
|
"Queue": [
|
|
336
751
|
"bull",
|
|
337
752
|
"bullmq",
|
|
@@ -382,7 +797,6 @@ var PILLAR_PATH_PATTERNS = {
|
|
|
382
797
|
"Auth": /(?:^|[\/\\])(?:auth|login|signup|register|session|oauth)(?:[\/\\]|$)/i,
|
|
383
798
|
"Database": /(?:^|[\/\\])(?:db|database|models?|schema|migrations?|seeds?)(?:[\/\\]|$)/i,
|
|
384
799
|
"Payments": /(?:^|[\/\\])(?:pay|payments?|billing|checkout|subscriptions?|stripe)(?:[\/\\]|$)/i,
|
|
385
|
-
"Routing": /(?:^|[\/\\])(?:routes?|router|middleware|api)(?:[\/\\]|$)/i,
|
|
386
800
|
"Queue": /(?:^|[\/\\])(?:queues?|workers?|jobs?|consumers?|producers?)(?:[\/\\]|$)/i,
|
|
387
801
|
"Storage": /(?:^|[\/\\])(?:storage|uploads?|s3|blobs?|media)(?:[\/\\]|$)/i,
|
|
388
802
|
"Config": /(?:^|[\/\\])(?:config|env|settings?)(?:[\/\\]|$)/i,
|
|
@@ -423,6 +837,144 @@ function matchPillarByPath(relPath) {
|
|
|
423
837
|
}
|
|
424
838
|
return null;
|
|
425
839
|
}
|
|
840
|
+
function inferFrameworkRole(relPath) {
|
|
841
|
+
const p = relPath.replace(/\\/g, "/");
|
|
842
|
+
if (/\.test\.|\.spec\./.test(p))
|
|
843
|
+
return "test";
|
|
844
|
+
if (/\.generated\.|__generated__|\.prisma\//.test(p))
|
|
845
|
+
return "generated";
|
|
846
|
+
if (/(?:^|\/)app\/.*\/page\.tsx?$/.test(p))
|
|
847
|
+
return "app_route_page";
|
|
848
|
+
if (/(?:^|\/)app\/.*\/layout\.tsx?$/.test(p))
|
|
849
|
+
return "app_route_layout";
|
|
850
|
+
if (/(?:^|\/)app\/.*\/route\.tsx?$/.test(p))
|
|
851
|
+
return "app_route_handler";
|
|
852
|
+
if (/(?:^|\/)app\/.*\/loading\.tsx?$/.test(p))
|
|
853
|
+
return "app_loading_boundary";
|
|
854
|
+
if (/(?:^|\/)app\/.*\/error\.tsx?$/.test(p))
|
|
855
|
+
return "app_error_boundary";
|
|
856
|
+
if (/(?:^|\/)pages\/api\/trpc\//.test(p))
|
|
857
|
+
return "trpc_api_route";
|
|
858
|
+
if (/(?:^|\/)pages\/api\//.test(p))
|
|
859
|
+
return "pages_api_route";
|
|
860
|
+
if (/(?:^|\/)pages\//.test(p))
|
|
861
|
+
return "pages_route";
|
|
862
|
+
if (/\/hooks\/|\/use[A-Z][^/]*\.(ts|tsx)$/.test(p))
|
|
863
|
+
return "hook";
|
|
864
|
+
if (/\/stores?\/|[Ss]tore\.(ts|tsx)$/.test(p))
|
|
865
|
+
return "store";
|
|
866
|
+
if (/[Pp]rovider\.(tsx?|jsx?)$|\/providers?\//.test(p))
|
|
867
|
+
return "provider";
|
|
868
|
+
if (/\.types\.ts$|\/types\.ts$|\/types\/[^/]+\.ts$/.test(p))
|
|
869
|
+
return "type_definition";
|
|
870
|
+
if (/\.(tsx|jsx)$/.test(p))
|
|
871
|
+
return "component";
|
|
872
|
+
if (/\.(ts|js|mjs|cjs)$/.test(p))
|
|
873
|
+
return "utility";
|
|
874
|
+
return "unknown";
|
|
875
|
+
}
|
|
876
|
+
function inferProductDomain(relPath, importSpecs) {
|
|
877
|
+
const p = relPath.toLowerCase().replace(/\\/g, "/");
|
|
878
|
+
if (/\.test\.|\.spec\.|__tests__|\/e2e\/|\/playwright\/|\/cypress\//.test(p)) {
|
|
879
|
+
return "test_infrastructure";
|
|
880
|
+
}
|
|
881
|
+
if (/\.generated\.|__generated__|\.prisma\//.test(p)) {
|
|
882
|
+
return "generated_noise";
|
|
883
|
+
}
|
|
884
|
+
if (p.includes("booking-audit") || p.includes("bookingaudit"))
|
|
885
|
+
return "booking_audit";
|
|
886
|
+
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"))
|
|
887
|
+
return "booking_creation";
|
|
888
|
+
if (p.includes("modules/bookings") || p.includes("components/booking/actions") || p.includes("/bookings/[status]") || p.includes("/booking/[uid]") || p.includes("/bookings/"))
|
|
889
|
+
return "booking_management";
|
|
890
|
+
if (p.includes("event-types") || p.includes("eventtypes") || p.includes("eventavailabilitytab") || p.includes("eventadvancedtab") || p.includes("eventlimits") || p.includes("eventrecurring"))
|
|
891
|
+
return "event_type_configuration";
|
|
892
|
+
if (p.includes("availability") || p.includes("/schedules/") || p.includes("/slots/"))
|
|
893
|
+
return "availability";
|
|
894
|
+
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")))
|
|
895
|
+
return "auth_oauth";
|
|
896
|
+
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/")))
|
|
897
|
+
return "auth";
|
|
898
|
+
if ((p.includes("stripe") || p.includes("paypal") || p.includes("btcpay") || p.includes("alby") || p.includes("payment")) && (p.includes("webhook") || p.includes("hook")))
|
|
899
|
+
return "payments_webhooks";
|
|
900
|
+
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/")))
|
|
901
|
+
return "payments";
|
|
902
|
+
if (p.includes("webhook"))
|
|
903
|
+
return "webhooks";
|
|
904
|
+
if (p.includes("app-store") || p.includes("appstore") || p.includes("/apps/") || p.includes("modules/apps"))
|
|
905
|
+
return "apps_marketplace";
|
|
906
|
+
if (p.includes("calendar") || p.includes("selected-calendars") || importSpecs.some((s) => s.includes("googleapis") || s.includes("@google-cloud/")))
|
|
907
|
+
return "calendar_integrations";
|
|
908
|
+
if (p.includes("video") || p.includes("calvideo") || p.includes("daily.co"))
|
|
909
|
+
return "video";
|
|
910
|
+
if (p.includes("onboarding") || p.includes("getting-started"))
|
|
911
|
+
return "onboarding";
|
|
912
|
+
if (p.includes("/settings/") || p.includes("/settings."))
|
|
913
|
+
return "settings";
|
|
914
|
+
if (p.includes("/admin/") || p.includes("/admin."))
|
|
915
|
+
return "admin";
|
|
916
|
+
if (p.includes("data-table") || p.includes("datatable") || p.includes("datasegment") || p.includes("segment"))
|
|
917
|
+
return "data_table";
|
|
918
|
+
if (p.includes("shell/navigation") || p.includes("navigationitem") || p.includes("/shell/") || p.includes("sidebar") || p.includes("topnav") || p.includes("mainnav"))
|
|
919
|
+
return "shell_navigation";
|
|
920
|
+
if (p.includes("form-builder") || p.includes("formbuilder") || p.includes("/forms/") || p.includes("routingforms"))
|
|
921
|
+
return "forms";
|
|
922
|
+
if (p.includes("embed"))
|
|
923
|
+
return "embed";
|
|
924
|
+
if (p.includes("notification") || p.includes("/email/") || p.includes("/emails/") || importSpecs.some((s) => s.includes("nodemailer") || s.includes("resend") || s.includes("@sendgrid/")))
|
|
925
|
+
return "notifications";
|
|
926
|
+
if (p.includes("middleware") && !p.includes("pages/api/") || p.includes("/router.") || p.includes("routerconfig"))
|
|
927
|
+
return "routing_infrastructure";
|
|
928
|
+
return "unknown";
|
|
929
|
+
}
|
|
930
|
+
function inferSideEffectProfile(source, importSpecs) {
|
|
931
|
+
const effects = /* @__PURE__ */ new Set();
|
|
932
|
+
if (/router\.(push|replace|back)\(|redirect\(|notFound\(|permanentRedirect\(/.test(source)) {
|
|
933
|
+
effects.add("redirect");
|
|
934
|
+
}
|
|
935
|
+
if (/["']use server["']/.test(source))
|
|
936
|
+
effects.add("server_action");
|
|
937
|
+
if (/useMutation\b|\.mutate\b|\.mutateAsync\b/.test(source))
|
|
938
|
+
effects.add("trpc_mutation");
|
|
939
|
+
if (/sdkActionManager\.fire|telemetry\.|posthog\.|mixpanel\.|amplitude\.|ga\(/.test(source) || importSpecs.some((s) => /analytics|telemetry|posthog|mixpanel|amplitude/.test(s)))
|
|
940
|
+
effects.add("analytics_event");
|
|
941
|
+
if (/prisma\s*[.?]\s*\w+\s*[.?]\s*(create|update|upsert|delete|deleteMany|updateMany|createMany|transaction|executeRaw|queryRaw)\b/.test(source)) {
|
|
942
|
+
effects.add("database_write");
|
|
943
|
+
}
|
|
944
|
+
if (/prisma\s*[.?]\s*\w+\s*[.?]\s*(findMany|findUnique|findFirst|findFirstOrThrow|findUniqueOrThrow|count|aggregate|groupBy)\b/.test(source)) {
|
|
945
|
+
effects.add("database_read");
|
|
946
|
+
}
|
|
947
|
+
if (/createBooking|handleNewBooking|cancelBooking|rescheduleBooking|handleBooking|createRecurring/.test(source)) {
|
|
948
|
+
effects.add("booking_mutation");
|
|
949
|
+
}
|
|
950
|
+
if (importSpecs.some((s) => /stripe|paypal|btcpay|alby/.test(s.toLowerCase())) || /stripe\.|paymentIntent|createPaymentIntent|confirmPayment|createCharge/.test(source))
|
|
951
|
+
effects.add("payment_mutation");
|
|
952
|
+
if (/signIn\b|signOut\b|createSession|destroySession|issueToken|refreshToken|getToken/.test(source)) {
|
|
953
|
+
effects.add("auth_token_mutation");
|
|
954
|
+
}
|
|
955
|
+
if (/triggerWebhook|sendWebhook|webhook\.send\b/.test(source))
|
|
956
|
+
effects.add("webhook_delivery");
|
|
957
|
+
if (/webhookSecret|stripe\.webhooks\.constructEvent|validateWebhook|verifyWebhook/.test(source)) {
|
|
958
|
+
effects.add("webhook_ingress");
|
|
959
|
+
}
|
|
960
|
+
if (/sendEmail|sendMail\b|mailer\./.test(source) || importSpecs.some((s) => /nodemailer|resend|sendgrid|postmark|mailgun/.test(s)))
|
|
961
|
+
effects.add("email_send");
|
|
962
|
+
if (/createCalendarEvent|updateCalendarEvent|deleteCalendarEvent|calendar\.events\.(insert|update|delete|patch)/.test(source)) {
|
|
963
|
+
effects.add("calendar_mutation");
|
|
964
|
+
}
|
|
965
|
+
if (/revalidatePath\b|revalidateTag\b/.test(source))
|
|
966
|
+
effects.add("cache_revalidation");
|
|
967
|
+
if (/localStorage\.|sessionStorage\./.test(source))
|
|
968
|
+
effects.add("local_storage");
|
|
969
|
+
if (/indexedDB\b|new Dexie|idb\./.test(source))
|
|
970
|
+
effects.add("indexed_db");
|
|
971
|
+
if (/\bfetch\s*\(|axios\.(get|post|put|patch|delete)\b/.test(source)) {
|
|
972
|
+
effects.add("external_api_call");
|
|
973
|
+
}
|
|
974
|
+
if (effects.size === 0)
|
|
975
|
+
effects.add("none_detected");
|
|
976
|
+
return [...effects];
|
|
977
|
+
}
|
|
426
978
|
async function collectFiles(dir, projectRoot, acc) {
|
|
427
979
|
let entries;
|
|
428
980
|
try {
|
|
@@ -522,18 +1074,46 @@ function extractImports(source, lang) {
|
|
|
522
1074
|
}
|
|
523
1075
|
return specs;
|
|
524
1076
|
}
|
|
1077
|
+
var MONOREPO_ALIASES = [
|
|
1078
|
+
{ prefix: "~/", replacement: "" },
|
|
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
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
525
1098
|
var JS_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
526
1099
|
function resolveImport(spec, fromAbs, lang, projectRoot, fileSet, basenameIndex) {
|
|
527
1100
|
if (lang === "python") {
|
|
528
|
-
return resolvePython(spec, fromAbs, projectRoot, fileSet);
|
|
1101
|
+
return { resolved: resolvePython(spec, fromAbs, projectRoot, fileSet), isAlias: false };
|
|
529
1102
|
}
|
|
530
1103
|
if (lang === "typescript" || lang === "tsx" || lang === "javascript") {
|
|
531
|
-
if (
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
1104
|
+
if (spec.startsWith(".")) {
|
|
1105
|
+
const base = join4(dirname(fromAbs), spec);
|
|
1106
|
+
return { resolved: tryJsCandidates(base, projectRoot, fileSet), isAlias: false };
|
|
1107
|
+
}
|
|
1108
|
+
const aliasResolved = resolveAlias(spec);
|
|
1109
|
+
if (aliasResolved !== null) {
|
|
1110
|
+
const base = join4(projectRoot, aliasResolved);
|
|
1111
|
+
const resolved = tryJsCandidates(base, projectRoot, fileSet);
|
|
1112
|
+
return { resolved, isAlias: true };
|
|
1113
|
+
}
|
|
1114
|
+
return { resolved: null, isAlias: false };
|
|
535
1115
|
}
|
|
536
|
-
return resolveGeneric(spec, projectRoot, fileSet, basenameIndex);
|
|
1116
|
+
return { resolved: resolveGeneric(spec, projectRoot, fileSet, basenameIndex), isAlias: false };
|
|
537
1117
|
}
|
|
538
1118
|
function tryJsCandidates(base, projectRoot, fileSet) {
|
|
539
1119
|
const candidates = [];
|
|
@@ -567,10 +1147,6 @@ function resolvePython(spec, fromAbs, projectRoot, fileSet) {
|
|
|
567
1147
|
if (fileSet.has(rel))
|
|
568
1148
|
return rel;
|
|
569
1149
|
}
|
|
570
|
-
if (!spec.startsWith(".")) {
|
|
571
|
-
const last = spec.split(".").pop();
|
|
572
|
-
void last;
|
|
573
|
-
}
|
|
574
1150
|
return null;
|
|
575
1151
|
}
|
|
576
1152
|
function resolveGeneric(spec, projectRoot, fileSet, basenameIndex) {
|
|
@@ -748,7 +1324,7 @@ function collectFunctionNodes(root) {
|
|
|
748
1324
|
walk(root);
|
|
749
1325
|
return out;
|
|
750
1326
|
}
|
|
751
|
-
function catchIsSwallowed(node,
|
|
1327
|
+
function catchIsSwallowed(node, _lang) {
|
|
752
1328
|
const bodyText = node.text;
|
|
753
1329
|
const inner = bodyText.replace(/^[^{:]*[{:]/, "");
|
|
754
1330
|
const meaningful = inner.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("#") && l !== "}" && l !== "pass");
|
|
@@ -1094,6 +1670,19 @@ async function detectStackAndEntrypoints(projectRoot, files) {
|
|
|
1094
1670
|
if (base === "main.rs" || base === "lib.rs")
|
|
1095
1671
|
entrypoints.add(r);
|
|
1096
1672
|
}
|
|
1673
|
+
if (stack.has("Next.js")) {
|
|
1674
|
+
const appRouterNames = /* @__PURE__ */ new Set(["page", "layout", "route", "loading", "error", "not-found", "template", "default"]);
|
|
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
|
+
}
|
|
1685
|
+
}
|
|
1097
1686
|
return { stack: [...stack], entrypoints };
|
|
1098
1687
|
}
|
|
1099
1688
|
var SMELL_WEIGHT = {
|
|
@@ -1147,24 +1736,42 @@ async function scanProject(projectRoot) {
|
|
|
1147
1736
|
const ast = analyzeAst(source, lang, tree);
|
|
1148
1737
|
const importSpecs = extractImports(source, lang);
|
|
1149
1738
|
graph.nodes[rel] = { imports: importSpecs };
|
|
1150
|
-
|
|
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
|
|
1753
|
+
});
|
|
1151
1754
|
}
|
|
1152
1755
|
const importedBy = /* @__PURE__ */ new Map();
|
|
1153
1756
|
const importsResolved = /* @__PURE__ */ new Map();
|
|
1757
|
+
const importsUnresolved = /* @__PURE__ */ new Map();
|
|
1154
1758
|
const fanOut = /* @__PURE__ */ new Map();
|
|
1155
1759
|
for (const w of work) {
|
|
1156
1760
|
importedBy.set(w.rel, /* @__PURE__ */ new Set());
|
|
1157
1761
|
importsResolved.set(w.rel, /* @__PURE__ */ new Set());
|
|
1762
|
+
importsUnresolved.set(w.rel, /* @__PURE__ */ new Set());
|
|
1158
1763
|
}
|
|
1159
1764
|
for (const w of work) {
|
|
1160
1765
|
const distinctModules = /* @__PURE__ */ new Set();
|
|
1161
1766
|
for (const spec of w.importSpecs) {
|
|
1162
1767
|
distinctModules.add(spec);
|
|
1163
|
-
const
|
|
1164
|
-
if (
|
|
1165
|
-
importedBy.get(
|
|
1166
|
-
importsResolved.get(w.rel).add(
|
|
1167
|
-
graph.edges.push({ from: w.rel, to:
|
|
1768
|
+
const { resolved, isAlias } = resolveImport(spec, w.abs, w.lang, projectRoot, fileSet, basenameIndex);
|
|
1769
|
+
if (resolved && resolved !== w.rel && importedBy.has(resolved)) {
|
|
1770
|
+
importedBy.get(resolved).add(w.rel);
|
|
1771
|
+
importsResolved.get(w.rel).add(resolved);
|
|
1772
|
+
graph.edges.push({ from: w.rel, to: resolved });
|
|
1773
|
+
} else if (resolved === null && isAlias) {
|
|
1774
|
+
importsUnresolved.get(w.rel).add(spec);
|
|
1168
1775
|
}
|
|
1169
1776
|
}
|
|
1170
1777
|
fanOut.set(w.rel, distinctModules.size);
|
|
@@ -1259,7 +1866,10 @@ async function scanProject(projectRoot) {
|
|
|
1259
1866
|
gravitySignals,
|
|
1260
1867
|
heatSignals,
|
|
1261
1868
|
smells: w.ast.smells,
|
|
1262
|
-
pillarHint
|
|
1869
|
+
pillarHint,
|
|
1870
|
+
frameworkRole: w.frameworkRole,
|
|
1871
|
+
productDomain: w.productDomain,
|
|
1872
|
+
sideEffectProfile: w.sideEffectProfile
|
|
1263
1873
|
};
|
|
1264
1874
|
analyses.push(fa);
|
|
1265
1875
|
persisted[w.rel] = {
|
|
@@ -1274,12 +1884,17 @@ async function scanProject(projectRoot) {
|
|
|
1274
1884
|
smells: w.ast.smells,
|
|
1275
1885
|
pillarHint,
|
|
1276
1886
|
importedBy: [...importedBy.get(w.rel)].filter((src) => isRealSource.get(src)),
|
|
1277
|
-
imports: [...importsResolved.get(w.rel)]
|
|
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
|
|
1278
1893
|
};
|
|
1279
1894
|
}
|
|
1280
1895
|
const realAnalyses = analyses.filter((a) => a.isRealSource).sort((a, b) => b.gravity - a.gravity);
|
|
1281
1896
|
const wildCandidates = realAnalyses.filter((a) => a.heat >= 60 || a.smells.some((s) => s.severity >= 4)).sort((a, b) => b.heat - a.heat);
|
|
1282
|
-
const pillars = buildPillars(realAnalyses, communities, stack);
|
|
1897
|
+
const pillars = buildPillars(realAnalyses, persisted, communities, stack);
|
|
1283
1898
|
const topGravity = realAnalyses.slice(0, 12).map((a) => a.relativePath);
|
|
1284
1899
|
const topHeat = wildCandidates.slice(0, 12).map((a) => a.relativePath);
|
|
1285
1900
|
const map = {
|
|
@@ -1295,7 +1910,7 @@ async function scanProject(projectRoot) {
|
|
|
1295
1910
|
await writeGraph(projectRoot, graph);
|
|
1296
1911
|
const analysisStore = { files: persisted };
|
|
1297
1912
|
await writeAnalysis(projectRoot, analysisStore);
|
|
1298
|
-
await writeDeltaTargets(projectRoot, analysisStore);
|
|
1913
|
+
await writeDeltaTargets(projectRoot, analysisStore, entrypoints);
|
|
1299
1914
|
const uiUrl = `file://${join4(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
|
|
1300
1915
|
return {
|
|
1301
1916
|
projectRoot,
|
|
@@ -1308,7 +1923,7 @@ async function scanProject(projectRoot) {
|
|
|
1308
1923
|
graph
|
|
1309
1924
|
};
|
|
1310
1925
|
}
|
|
1311
|
-
function buildPillars(real, communities, _stack) {
|
|
1926
|
+
function buildPillars(real, persisted, communities, _stack) {
|
|
1312
1927
|
const keywordGroups = /* @__PURE__ */ new Map();
|
|
1313
1928
|
const unlabeled = [];
|
|
1314
1929
|
for (const a of real) {
|
|
@@ -1370,15 +1985,29 @@ function buildPillars(real, communities, _stack) {
|
|
|
1370
1985
|
if (p.memberFiles.length > 15) {
|
|
1371
1986
|
const groups = /* @__PURE__ */ new Map();
|
|
1372
1987
|
for (const f of p.memberFiles) {
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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") {
|
|
1377
2006
|
bucket = "Components";
|
|
1378
|
-
else
|
|
2007
|
+
} else {
|
|
1379
2008
|
bucket = "Logic";
|
|
1380
|
-
|
|
1381
|
-
const key = `${p.name} (${bucket}
|
|
2009
|
+
}
|
|
2010
|
+
const key = `${p.name} (${bucket})`;
|
|
1382
2011
|
if (!groups.has(key))
|
|
1383
2012
|
groups.set(key, []);
|
|
1384
2013
|
groups.get(key).push(f);
|
|
@@ -1407,6 +2036,32 @@ function buildPillars(real, communities, _stack) {
|
|
|
1407
2036
|
}
|
|
1408
2037
|
return finalPillars;
|
|
1409
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
|
+
};
|
|
2063
|
+
return labels[domain] || titleCase(domain.replace(/_/g, " "));
|
|
2064
|
+
}
|
|
1410
2065
|
function pillarNameFromCluster(files) {
|
|
1411
2066
|
const hintCounts = /* @__PURE__ */ new Map();
|
|
1412
2067
|
for (const f of files) {
|
|
@@ -1551,7 +2206,7 @@ function validateMermaidNodeCount(diagram) {
|
|
|
1551
2206
|
|
|
1552
2207
|
// ../brain/dist/watcher.js
|
|
1553
2208
|
import chokidar from "chokidar";
|
|
1554
|
-
import { createHash } from "crypto";
|
|
2209
|
+
import { createHash as createHash2 } from "crypto";
|
|
1555
2210
|
import { readFile as readFile6 } from "fs/promises";
|
|
1556
2211
|
import { join as join6 } from "path";
|
|
1557
2212
|
function startWatcher(projectRoot, watchedPaths) {
|
|
@@ -1566,7 +2221,7 @@ function startWatcher(projectRoot, watchedPaths) {
|
|
|
1566
2221
|
if (!dossier)
|
|
1567
2222
|
return;
|
|
1568
2223
|
const content = await readFile6(filepath, "utf8");
|
|
1569
|
-
const newHash =
|
|
2224
|
+
const newHash = createHash2("sha256").update(content).digest("hex");
|
|
1570
2225
|
let mutated = false;
|
|
1571
2226
|
for (const pillar of dossier.pillars) {
|
|
1572
2227
|
for (const card of pillar.decisions) {
|
|
@@ -1780,7 +2435,7 @@ async function handleGetFileContext(args) {
|
|
|
1780
2435
|
|
|
1781
2436
|
// dist/mcp/tools/write_decision_card.js
|
|
1782
2437
|
import { v4 as uuidv4 } from "uuid";
|
|
1783
|
-
import { createHash as
|
|
2438
|
+
import { createHash as createHash3 } from "crypto";
|
|
1784
2439
|
import { readFile as readFile8 } from "fs/promises";
|
|
1785
2440
|
import { join as join8 } from "path";
|
|
1786
2441
|
var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
|
|
@@ -1877,7 +2532,7 @@ async function handleWriteDecisionCard(args) {
|
|
|
1877
2532
|
primaryContent = await readFile8(join8(projectRoot, primaryFile), "utf8");
|
|
1878
2533
|
} catch {
|
|
1879
2534
|
}
|
|
1880
|
-
const hash =
|
|
2535
|
+
const hash = createHash3("sha256").update(primaryContent).digest("hex");
|
|
1881
2536
|
const card = {
|
|
1882
2537
|
id: uuidv4(),
|
|
1883
2538
|
pillar,
|
|
@@ -2243,7 +2898,7 @@ async function serveCommand() {
|
|
|
2243
2898
|
|
|
2244
2899
|
// dist/index.js
|
|
2245
2900
|
var program = new Command();
|
|
2246
|
-
program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("2.
|
|
2901
|
+
program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("2.4.1");
|
|
2247
2902
|
program.command("install").description("Patch coding agent MCP config files to register vibe-splain").action(installCommand);
|
|
2248
2903
|
program.command("serve").description("Start the MCP server (called by the coding agent, not by you)").action(serveCommand);
|
|
2249
2904
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibe-splain",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "Architectural dossier engine for vibe-coded TypeScript/JavaScript projects. Runs as an MCP server inside your coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"node": ">=18"
|
|
31
31
|
},
|
|
32
32
|
"bin": {
|
|
33
|
-
"vibe-splain": "
|
|
33
|
+
"vibe-splain": "dist/index.js"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsc && node build.mjs"
|