vibe-splain 2.4.0 → 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 +682 -98
- package/package.json +1 -1
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,67 +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
|
-
|
|
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) {
|
|
131
276
|
const types = [];
|
|
132
|
-
|
|
277
|
+
const kinds = new Set(f.smells.map((s) => s.kind));
|
|
278
|
+
if (f.gravitySignals.cyclomatic > 20)
|
|
133
279
|
types.push("state_machine");
|
|
134
|
-
if (kinds.has("god-file"))
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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");
|
|
138
295
|
if (kinds.has("swallowed-catch"))
|
|
139
|
-
types.push("
|
|
140
|
-
if (f.
|
|
141
|
-
types.push("
|
|
142
|
-
if (
|
|
143
|
-
types.push("
|
|
144
|
-
return types
|
|
296
|
+
types.push("error_swallowing");
|
|
297
|
+
if (f.sideEffectProfile.includes("local_storage") || f.sideEffectProfile.includes("indexed_db"))
|
|
298
|
+
types.push("storage_persistence_risk");
|
|
299
|
+
if (types.length === 0)
|
|
300
|
+
types.push("complexity_hotspot");
|
|
301
|
+
return types;
|
|
302
|
+
}
|
|
303
|
+
function inferObservableOutputs(f) {
|
|
304
|
+
const outputs = [];
|
|
305
|
+
if (f.sideEffectProfile.includes("redirect"))
|
|
306
|
+
outputs.push("redirect_url");
|
|
307
|
+
if (ENTRYPOINT_ROLES.has(f.frameworkRole))
|
|
308
|
+
outputs.push("http_status");
|
|
309
|
+
if (f.frameworkRole === "app_route_handler" || f.frameworkRole === "pages_api_route") {
|
|
310
|
+
outputs.push("json_response_shape");
|
|
311
|
+
}
|
|
312
|
+
if (f.productDomain === "booking_creation" || f.productDomain === "booking_management") {
|
|
313
|
+
outputs.push("booking_uid");
|
|
314
|
+
}
|
|
315
|
+
if (f.productDomain === "payments" || f.productDomain === "payments_webhooks") {
|
|
316
|
+
outputs.push("payment_status");
|
|
317
|
+
}
|
|
318
|
+
if (f.productDomain === "auth_oauth") {
|
|
319
|
+
outputs.push("auth_token");
|
|
320
|
+
}
|
|
321
|
+
if (f.sideEffectProfile.includes("webhook_delivery") || f.sideEffectProfile.includes("webhook_ingress")) {
|
|
322
|
+
outputs.push("webhook_payload");
|
|
323
|
+
}
|
|
324
|
+
if (f.sideEffectProfile.includes("calendar_mutation")) {
|
|
325
|
+
outputs.push("calendar_event_id");
|
|
326
|
+
}
|
|
327
|
+
if (f.sideEffectProfile.includes("email_send")) {
|
|
328
|
+
outputs.push("email_payload");
|
|
329
|
+
}
|
|
330
|
+
if (f.sideEffectProfile.includes("analytics_event")) {
|
|
331
|
+
outputs.push("sdk_event_name");
|
|
332
|
+
}
|
|
333
|
+
if (f.frameworkRole === "hook" || f.frameworkRole === "store") {
|
|
334
|
+
outputs.push("ui_state_transition");
|
|
335
|
+
}
|
|
336
|
+
return [...new Set(outputs)];
|
|
337
|
+
}
|
|
338
|
+
function inferWriteIntents(f) {
|
|
339
|
+
const intents = [];
|
|
340
|
+
if (f.productDomain === "booking_creation") {
|
|
341
|
+
intents.push("create_booking");
|
|
342
|
+
if (f.relativePath.includes("reschedule") || f.relativePath.includes("Reschedule"))
|
|
343
|
+
intents.push("reschedule_booking");
|
|
344
|
+
if (f.relativePath.includes("recurring") || f.relativePath.includes("Recurring")) {
|
|
345
|
+
intents.push("create_recurring_booking");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (f.productDomain === "booking_management") {
|
|
349
|
+
intents.push("cancel_booking");
|
|
350
|
+
}
|
|
351
|
+
if (f.productDomain === "event_type_configuration") {
|
|
352
|
+
intents.push("update_event_type");
|
|
353
|
+
}
|
|
354
|
+
if (f.productDomain === "availability") {
|
|
355
|
+
intents.push("update_availability");
|
|
356
|
+
}
|
|
357
|
+
if (f.productDomain === "payments") {
|
|
358
|
+
intents.push("create_payment");
|
|
359
|
+
}
|
|
360
|
+
if (f.productDomain === "payments_webhooks") {
|
|
361
|
+
intents.push("handle_payment_webhook");
|
|
362
|
+
}
|
|
363
|
+
if (f.productDomain === "auth_oauth") {
|
|
364
|
+
intents.push("issue_auth_token");
|
|
365
|
+
intents.push("refresh_auth_token");
|
|
366
|
+
}
|
|
367
|
+
if (f.sideEffectProfile.includes("webhook_delivery")) {
|
|
368
|
+
intents.push("send_webhook");
|
|
369
|
+
}
|
|
370
|
+
if (f.productDomain === "settings") {
|
|
371
|
+
intents.push("update_user_settings");
|
|
372
|
+
}
|
|
373
|
+
if (f.sideEffectProfile.includes("local_storage") || f.sideEffectProfile.includes("indexed_db")) {
|
|
374
|
+
intents.push("persist_local_state");
|
|
375
|
+
}
|
|
376
|
+
return intents.length > 0 ? intents : ["none_detected"];
|
|
377
|
+
}
|
|
378
|
+
function inferPatchRisk(f, score, riskTypes) {
|
|
379
|
+
if (score >= 12 || f.productDomain === "booking_creation" && riskTypes.includes("mutation_orchestration")) {
|
|
380
|
+
return {
|
|
381
|
+
level: "critical",
|
|
382
|
+
reason: `${f.productDomain} domain with ${riskTypes.join(", ")} \u2014 any patch risks breaking live booking, payment, or auth flows.`
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (score >= 8 || f.sideEffectProfile.includes("payment_mutation") || f.sideEffectProfile.includes("auth_token_mutation")) {
|
|
386
|
+
return {
|
|
387
|
+
level: "high",
|
|
388
|
+
reason: `${f.productDomain} writes to external state (${f.sideEffectProfile.filter((s) => ["payment_mutation", "auth_token_mutation", "database_write", "webhook_delivery"].includes(s)).join(", ") || "database"}). Changes require integration testing.`
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (score >= 5 || f.importedBy.length >= 5) {
|
|
392
|
+
return {
|
|
393
|
+
level: "medium",
|
|
394
|
+
reason: `Imported by ${f.importedBy.length} files. Interface changes will cascade.`
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
level: "low",
|
|
399
|
+
reason: "Locally contained \u2014 limited blast radius."
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function inferSafePatchStrategy(f, riskTypes) {
|
|
403
|
+
if (riskTypes.includes("mutation_orchestration")) {
|
|
404
|
+
return "Do not rewrite inline. Extract pure decision logic into a tested reducer or state machine first. Preserve all side-effect call sites (redirect URLs, SDK event names, response shapes) as invariants.";
|
|
405
|
+
}
|
|
406
|
+
if (riskTypes.includes("registry_bottleneck")) {
|
|
407
|
+
return "Add new entries without removing existing keys. Treat the registry map as append-only until all consumers are verified.";
|
|
408
|
+
}
|
|
409
|
+
if (riskTypes.includes("route_handler_write_path")) {
|
|
410
|
+
return "Add integration tests covering success and failure paths before modifying. Verify HTTP status codes and response shapes are preserved.";
|
|
411
|
+
}
|
|
412
|
+
if (riskTypes.includes("god_component") || riskTypes.includes("god_hook")) {
|
|
413
|
+
return "Extract sub-concerns into separate modules first. Only refactor the extraction points after tests confirm equivalence.";
|
|
414
|
+
}
|
|
415
|
+
if (f.sideEffectProfile.includes("database_write")) {
|
|
416
|
+
return "Wrap changes in a transaction or use a feature flag. Run against a staging database before production.";
|
|
417
|
+
}
|
|
418
|
+
return "Review importedBy before patching. Run affected integration tests.";
|
|
419
|
+
}
|
|
420
|
+
function inferDoNotTouch(f) {
|
|
421
|
+
const items = [];
|
|
422
|
+
if (f.sideEffectProfile.includes("payment_mutation"))
|
|
423
|
+
items.push("payment flow branch");
|
|
424
|
+
if (f.sideEffectProfile.includes("auth_token_mutation"))
|
|
425
|
+
items.push("token issuance / refresh branch");
|
|
426
|
+
if (f.sideEffectProfile.includes("webhook_delivery") || f.sideEffectProfile.includes("webhook_ingress")) {
|
|
427
|
+
items.push("webhook payload shape");
|
|
428
|
+
}
|
|
429
|
+
if (f.sideEffectProfile.includes("redirect"))
|
|
430
|
+
items.push("redirect URL strings");
|
|
431
|
+
if (f.sideEffectProfile.includes("analytics_event"))
|
|
432
|
+
items.push("SDK event names");
|
|
433
|
+
if (f.sideEffectProfile.includes("booking_mutation")) {
|
|
434
|
+
items.push("booking success response shape", "recurring booking branch");
|
|
435
|
+
}
|
|
436
|
+
if (f.productDomain === "auth_oauth")
|
|
437
|
+
items.push("OAuth callback URLs", "token scopes");
|
|
438
|
+
return items;
|
|
439
|
+
}
|
|
440
|
+
function inferTestProbes(f, writeIntents, observableOutputs) {
|
|
441
|
+
const probes = [];
|
|
442
|
+
if (writeIntents.includes("create_booking")) {
|
|
443
|
+
probes.push({
|
|
444
|
+
name: "standard booking success",
|
|
445
|
+
scenario: "create a standard booking and assert success redirect and booking uid",
|
|
446
|
+
expectedObservable: ["booking_uid", "redirect_url", "sdk_event_name"].filter((o) => observableOutputs.includes(o))
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (writeIntents.includes("reschedule_booking")) {
|
|
450
|
+
probes.push({
|
|
451
|
+
name: "reschedule booking",
|
|
452
|
+
scenario: "reschedule an existing booking and assert reschedule event path",
|
|
453
|
+
expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
if (writeIntents.includes("create_recurring_booking")) {
|
|
457
|
+
probes.push({
|
|
458
|
+
name: "recurring booking",
|
|
459
|
+
scenario: "create recurring booking and assert recurring success behavior",
|
|
460
|
+
expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
if (writeIntents.includes("handle_payment_webhook")) {
|
|
464
|
+
probes.push({
|
|
465
|
+
name: "payment webhook ingestion",
|
|
466
|
+
scenario: "send a valid payment webhook and assert booking/payment state updated",
|
|
467
|
+
expectedObservable: ["payment_status", "booking_uid", "http_status"].filter((o) => observableOutputs.includes(o))
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (writeIntents.includes("issue_auth_token")) {
|
|
471
|
+
probes.push({
|
|
472
|
+
name: "token issuance",
|
|
473
|
+
scenario: "complete OAuth flow and assert access token issued with correct scopes",
|
|
474
|
+
expectedObservable: ["auth_token", "http_status"].filter((o) => observableOutputs.includes(o))
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return probes;
|
|
478
|
+
}
|
|
479
|
+
function buildRawEvidence(f) {
|
|
480
|
+
return f.hotSpans.map((span) => ({
|
|
481
|
+
file: f.relativePath,
|
|
482
|
+
startLine: span.startLine,
|
|
483
|
+
endLine: span.endLine,
|
|
484
|
+
rawSourceExcerpt: span.snippet,
|
|
485
|
+
evidenceHash: createHash("sha256").update(span.snippet).digest("hex").slice(0, 12)
|
|
486
|
+
}));
|
|
145
487
|
}
|
|
146
488
|
function deriveConfidence(f) {
|
|
147
|
-
if (f.gravitySignals.fanIn >=
|
|
489
|
+
if (f.gravitySignals.fanIn >= 10 && f.gravity >= 40)
|
|
148
490
|
return "high";
|
|
149
491
|
if (f.gravitySignals.fanIn >= 5 || f.gravity >= 25)
|
|
150
492
|
return "medium";
|
|
151
493
|
return "low";
|
|
152
494
|
}
|
|
153
|
-
function
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const curr = queue.shift();
|
|
159
|
-
if (visited.has(curr))
|
|
160
|
-
continue;
|
|
161
|
-
visited.add(curr);
|
|
162
|
-
if (entrypoints.has(curr) && curr !== relPath)
|
|
163
|
-
found.push(curr);
|
|
164
|
-
if (found.length >= 5)
|
|
165
|
-
break;
|
|
166
|
-
const f = files[curr];
|
|
167
|
-
if (f) {
|
|
168
|
-
for (const importer of f.importedBy)
|
|
169
|
-
if (!visited.has(importer))
|
|
170
|
-
queue.push(importer);
|
|
171
|
-
}
|
|
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");
|
|
172
500
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
+
});
|
|
189
560
|
const dir = join3(projectRoot, ".vibe-splainer");
|
|
190
561
|
await mkdir2(dir, { recursive: true });
|
|
191
562
|
const dest = join3(dir, "delta_targets.json");
|
|
@@ -376,20 +747,6 @@ var PILLAR_KEYWORDS = {
|
|
|
376
747
|
"paddle",
|
|
377
748
|
"lemon-squeezy"
|
|
378
749
|
],
|
|
379
|
-
"Routing": [
|
|
380
|
-
"express",
|
|
381
|
-
"fastify",
|
|
382
|
-
"koa",
|
|
383
|
-
"koa-router",
|
|
384
|
-
"next/router",
|
|
385
|
-
"next/navigation",
|
|
386
|
-
"react-router",
|
|
387
|
-
"@remix-run/",
|
|
388
|
-
"hono",
|
|
389
|
-
"express-rate-limit",
|
|
390
|
-
"cors",
|
|
391
|
-
"helmet"
|
|
392
|
-
],
|
|
393
750
|
"Queue": [
|
|
394
751
|
"bull",
|
|
395
752
|
"bullmq",
|
|
@@ -440,7 +797,6 @@ var PILLAR_PATH_PATTERNS = {
|
|
|
440
797
|
"Auth": /(?:^|[\/\\])(?:auth|login|signup|register|session|oauth)(?:[\/\\]|$)/i,
|
|
441
798
|
"Database": /(?:^|[\/\\])(?:db|database|models?|schema|migrations?|seeds?)(?:[\/\\]|$)/i,
|
|
442
799
|
"Payments": /(?:^|[\/\\])(?:pay|payments?|billing|checkout|subscriptions?|stripe)(?:[\/\\]|$)/i,
|
|
443
|
-
"Routing": /(?:^|[\/\\])(?:routes?|router|middleware|api)(?:[\/\\]|$)/i,
|
|
444
800
|
"Queue": /(?:^|[\/\\])(?:queues?|workers?|jobs?|consumers?|producers?)(?:[\/\\]|$)/i,
|
|
445
801
|
"Storage": /(?:^|[\/\\])(?:storage|uploads?|s3|blobs?|media)(?:[\/\\]|$)/i,
|
|
446
802
|
"Config": /(?:^|[\/\\])(?:config|env|settings?)(?:[\/\\]|$)/i,
|
|
@@ -481,6 +837,144 @@ function matchPillarByPath(relPath) {
|
|
|
481
837
|
}
|
|
482
838
|
return null;
|
|
483
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
|
+
}
|
|
484
978
|
async function collectFiles(dir, projectRoot, acc) {
|
|
485
979
|
let entries;
|
|
486
980
|
try {
|
|
@@ -580,18 +1074,46 @@ function extractImports(source, lang) {
|
|
|
580
1074
|
}
|
|
581
1075
|
return specs;
|
|
582
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
|
+
}
|
|
583
1098
|
var JS_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
584
1099
|
function resolveImport(spec, fromAbs, lang, projectRoot, fileSet, basenameIndex) {
|
|
585
1100
|
if (lang === "python") {
|
|
586
|
-
return resolvePython(spec, fromAbs, projectRoot, fileSet);
|
|
1101
|
+
return { resolved: resolvePython(spec, fromAbs, projectRoot, fileSet), isAlias: false };
|
|
587
1102
|
}
|
|
588
1103
|
if (lang === "typescript" || lang === "tsx" || lang === "javascript") {
|
|
589
|
-
if (
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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 };
|
|
593
1115
|
}
|
|
594
|
-
return resolveGeneric(spec, projectRoot, fileSet, basenameIndex);
|
|
1116
|
+
return { resolved: resolveGeneric(spec, projectRoot, fileSet, basenameIndex), isAlias: false };
|
|
595
1117
|
}
|
|
596
1118
|
function tryJsCandidates(base, projectRoot, fileSet) {
|
|
597
1119
|
const candidates = [];
|
|
@@ -625,10 +1147,6 @@ function resolvePython(spec, fromAbs, projectRoot, fileSet) {
|
|
|
625
1147
|
if (fileSet.has(rel))
|
|
626
1148
|
return rel;
|
|
627
1149
|
}
|
|
628
|
-
if (!spec.startsWith(".")) {
|
|
629
|
-
const last = spec.split(".").pop();
|
|
630
|
-
void last;
|
|
631
|
-
}
|
|
632
1150
|
return null;
|
|
633
1151
|
}
|
|
634
1152
|
function resolveGeneric(spec, projectRoot, fileSet, basenameIndex) {
|
|
@@ -806,7 +1324,7 @@ function collectFunctionNodes(root) {
|
|
|
806
1324
|
walk(root);
|
|
807
1325
|
return out;
|
|
808
1326
|
}
|
|
809
|
-
function catchIsSwallowed(node,
|
|
1327
|
+
function catchIsSwallowed(node, _lang) {
|
|
810
1328
|
const bodyText = node.text;
|
|
811
1329
|
const inner = bodyText.replace(/^[^{:]*[{:]/, "");
|
|
812
1330
|
const meaningful = inner.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("#") && l !== "}" && l !== "pass");
|
|
@@ -1218,24 +1736,42 @@ async function scanProject(projectRoot) {
|
|
|
1218
1736
|
const ast = analyzeAst(source, lang, tree);
|
|
1219
1737
|
const importSpecs = extractImports(source, lang);
|
|
1220
1738
|
graph.nodes[rel] = { imports: importSpecs };
|
|
1221
|
-
|
|
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
|
+
});
|
|
1222
1754
|
}
|
|
1223
1755
|
const importedBy = /* @__PURE__ */ new Map();
|
|
1224
1756
|
const importsResolved = /* @__PURE__ */ new Map();
|
|
1757
|
+
const importsUnresolved = /* @__PURE__ */ new Map();
|
|
1225
1758
|
const fanOut = /* @__PURE__ */ new Map();
|
|
1226
1759
|
for (const w of work) {
|
|
1227
1760
|
importedBy.set(w.rel, /* @__PURE__ */ new Set());
|
|
1228
1761
|
importsResolved.set(w.rel, /* @__PURE__ */ new Set());
|
|
1762
|
+
importsUnresolved.set(w.rel, /* @__PURE__ */ new Set());
|
|
1229
1763
|
}
|
|
1230
1764
|
for (const w of work) {
|
|
1231
1765
|
const distinctModules = /* @__PURE__ */ new Set();
|
|
1232
1766
|
for (const spec of w.importSpecs) {
|
|
1233
1767
|
distinctModules.add(spec);
|
|
1234
|
-
const
|
|
1235
|
-
if (
|
|
1236
|
-
importedBy.get(
|
|
1237
|
-
importsResolved.get(w.rel).add(
|
|
1238
|
-
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);
|
|
1239
1775
|
}
|
|
1240
1776
|
}
|
|
1241
1777
|
fanOut.set(w.rel, distinctModules.size);
|
|
@@ -1330,7 +1866,10 @@ async function scanProject(projectRoot) {
|
|
|
1330
1866
|
gravitySignals,
|
|
1331
1867
|
heatSignals,
|
|
1332
1868
|
smells: w.ast.smells,
|
|
1333
|
-
pillarHint
|
|
1869
|
+
pillarHint,
|
|
1870
|
+
frameworkRole: w.frameworkRole,
|
|
1871
|
+
productDomain: w.productDomain,
|
|
1872
|
+
sideEffectProfile: w.sideEffectProfile
|
|
1334
1873
|
};
|
|
1335
1874
|
analyses.push(fa);
|
|
1336
1875
|
persisted[w.rel] = {
|
|
@@ -1345,12 +1884,17 @@ async function scanProject(projectRoot) {
|
|
|
1345
1884
|
smells: w.ast.smells,
|
|
1346
1885
|
pillarHint,
|
|
1347
1886
|
importedBy: [...importedBy.get(w.rel)].filter((src) => isRealSource.get(src)),
|
|
1348
|
-
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
|
|
1349
1893
|
};
|
|
1350
1894
|
}
|
|
1351
1895
|
const realAnalyses = analyses.filter((a) => a.isRealSource).sort((a, b) => b.gravity - a.gravity);
|
|
1352
1896
|
const wildCandidates = realAnalyses.filter((a) => a.heat >= 60 || a.smells.some((s) => s.severity >= 4)).sort((a, b) => b.heat - a.heat);
|
|
1353
|
-
const pillars = buildPillars(realAnalyses, communities, stack);
|
|
1897
|
+
const pillars = buildPillars(realAnalyses, persisted, communities, stack);
|
|
1354
1898
|
const topGravity = realAnalyses.slice(0, 12).map((a) => a.relativePath);
|
|
1355
1899
|
const topHeat = wildCandidates.slice(0, 12).map((a) => a.relativePath);
|
|
1356
1900
|
const map = {
|
|
@@ -1379,7 +1923,7 @@ async function scanProject(projectRoot) {
|
|
|
1379
1923
|
graph
|
|
1380
1924
|
};
|
|
1381
1925
|
}
|
|
1382
|
-
function buildPillars(real, communities, _stack) {
|
|
1926
|
+
function buildPillars(real, persisted, communities, _stack) {
|
|
1383
1927
|
const keywordGroups = /* @__PURE__ */ new Map();
|
|
1384
1928
|
const unlabeled = [];
|
|
1385
1929
|
for (const a of real) {
|
|
@@ -1441,15 +1985,29 @@ function buildPillars(real, communities, _stack) {
|
|
|
1441
1985
|
if (p.memberFiles.length > 15) {
|
|
1442
1986
|
const groups = /* @__PURE__ */ new Map();
|
|
1443
1987
|
for (const f of p.memberFiles) {
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
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") {
|
|
1448
2006
|
bucket = "Components";
|
|
1449
|
-
else
|
|
2007
|
+
} else {
|
|
1450
2008
|
bucket = "Logic";
|
|
1451
|
-
|
|
1452
|
-
const key = `${p.name} (${bucket}
|
|
2009
|
+
}
|
|
2010
|
+
const key = `${p.name} (${bucket})`;
|
|
1453
2011
|
if (!groups.has(key))
|
|
1454
2012
|
groups.set(key, []);
|
|
1455
2013
|
groups.get(key).push(f);
|
|
@@ -1478,6 +2036,32 @@ function buildPillars(real, communities, _stack) {
|
|
|
1478
2036
|
}
|
|
1479
2037
|
return finalPillars;
|
|
1480
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
|
+
}
|
|
1481
2065
|
function pillarNameFromCluster(files) {
|
|
1482
2066
|
const hintCounts = /* @__PURE__ */ new Map();
|
|
1483
2067
|
for (const f of files) {
|
|
@@ -1622,7 +2206,7 @@ function validateMermaidNodeCount(diagram) {
|
|
|
1622
2206
|
|
|
1623
2207
|
// ../brain/dist/watcher.js
|
|
1624
2208
|
import chokidar from "chokidar";
|
|
1625
|
-
import { createHash } from "crypto";
|
|
2209
|
+
import { createHash as createHash2 } from "crypto";
|
|
1626
2210
|
import { readFile as readFile6 } from "fs/promises";
|
|
1627
2211
|
import { join as join6 } from "path";
|
|
1628
2212
|
function startWatcher(projectRoot, watchedPaths) {
|
|
@@ -1637,7 +2221,7 @@ function startWatcher(projectRoot, watchedPaths) {
|
|
|
1637
2221
|
if (!dossier)
|
|
1638
2222
|
return;
|
|
1639
2223
|
const content = await readFile6(filepath, "utf8");
|
|
1640
|
-
const newHash =
|
|
2224
|
+
const newHash = createHash2("sha256").update(content).digest("hex");
|
|
1641
2225
|
let mutated = false;
|
|
1642
2226
|
for (const pillar of dossier.pillars) {
|
|
1643
2227
|
for (const card of pillar.decisions) {
|
|
@@ -1851,7 +2435,7 @@ async function handleGetFileContext(args) {
|
|
|
1851
2435
|
|
|
1852
2436
|
// dist/mcp/tools/write_decision_card.js
|
|
1853
2437
|
import { v4 as uuidv4 } from "uuid";
|
|
1854
|
-
import { createHash as
|
|
2438
|
+
import { createHash as createHash3 } from "crypto";
|
|
1855
2439
|
import { readFile as readFile8 } from "fs/promises";
|
|
1856
2440
|
import { join as join8 } from "path";
|
|
1857
2441
|
var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
|
|
@@ -1948,7 +2532,7 @@ async function handleWriteDecisionCard(args) {
|
|
|
1948
2532
|
primaryContent = await readFile8(join8(projectRoot, primaryFile), "utf8");
|
|
1949
2533
|
} catch {
|
|
1950
2534
|
}
|
|
1951
|
-
const hash =
|
|
2535
|
+
const hash = createHash3("sha256").update(primaryContent).digest("hex");
|
|
1952
2536
|
const card = {
|
|
1953
2537
|
id: uuidv4(),
|
|
1954
2538
|
pillar,
|
|
@@ -2314,7 +2898,7 @@ async function serveCommand() {
|
|
|
2314
2898
|
|
|
2315
2899
|
// dist/index.js
|
|
2316
2900
|
var program = new Command();
|
|
2317
|
-
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");
|
|
2318
2902
|
program.command("install").description("Patch coding agent MCP config files to register vibe-splain").action(installCommand);
|
|
2319
2903
|
program.command("serve").description("Start the MCP server (called by the coding agent, not by you)").action(serveCommand);
|
|
2320
2904
|
program.parse();
|
package/package.json
CHANGED