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.
Files changed (2) hide show
  1. package/dist/index.js +682 -98
  2. 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 LOAD_BEARING_FAN_IN_THRESHOLD = 10;
129
- function deriveRiskTypes(f) {
130
- const kinds = new Set(f.smells.map((s) => s.kind));
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
- if (f.gravitySignals.cyclomatic > 15)
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
- types.push("god_object");
136
- if (f.gravitySignals.fanIn > 15)
137
- types.push("deep_coupling");
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("error_sink");
140
- if (f.gravitySignals.fanIn > 10 && f.gravitySignals.publicSurface > 8)
141
- types.push("mutation_hotspot");
142
- if (kinds.has("todo") && kinds.has("suppression"))
143
- types.push("tech_debt");
144
- return types.length > 0 ? types : ["complexity_hotspot"];
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 >= LOAD_BEARING_FAN_IN_THRESHOLD && f.gravity >= 40)
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 findRuntimeEntrypoints(relPath, files, entrypoints) {
154
- const found = [];
155
- const visited = /* @__PURE__ */ new Set();
156
- const queue = [relPath];
157
- while (queue.length > 0) {
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
- return found;
174
- }
175
- async function writeDeltaTargets(projectRoot, store, entrypoints = /* @__PURE__ */ new Set()) {
176
- const domain = (f) => f.pillarHint && !f.pillarHint.startsWith("community-") ? f.pillarHint : null;
177
- const targets = Object.values(store.files).filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity).map((f) => ({
178
- path: f.relativePath,
179
- gravity: f.gravity,
180
- isLoadBearing: f.gravitySignals.fanIn >= LOAD_BEARING_FAN_IN_THRESHOLD,
181
- blastRadius: f.importedBy,
182
- pillarHint: domain(f),
183
- domain: domain(f),
184
- riskTypes: deriveRiskTypes(f),
185
- severity: f.smells.length > 0 ? Math.max(...f.smells.map((s) => s.severity)) : 0,
186
- confidence: deriveConfidence(f),
187
- runtimeEntrypoints: findRuntimeEntrypoints(f.relativePath, store.files, entrypoints)
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 (!spec.startsWith("."))
590
- return null;
591
- const base = join4(dirname(fromAbs), spec);
592
- return tryJsCandidates(base, projectRoot, fileSet);
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, lang) {
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
- work.push({ abs: file, rel, lang, source, ast, importSpecs, pathDemote: pathDemoteReason(rel) });
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 target = resolveImport(spec, w.abs, w.lang, projectRoot, fileSet, basenameIndex);
1235
- if (target && target !== w.rel && importedBy.has(target)) {
1236
- importedBy.get(target).add(w.rel);
1237
- importsResolved.get(w.rel).add(target);
1238
- graph.edges.push({ from: w.rel, to: target });
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
- let bucket = "Core";
1445
- if (f.includes("app/") || f.includes("pages/") || f.includes("routes/"))
1446
- bucket = "Routing";
1447
- else if (f.includes("components/") || f.includes("ui/"))
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 if (f.includes("hooks/") || f.includes("lib/") || f.includes("utils/"))
2007
+ } else {
1450
2008
  bucket = "Logic";
1451
- const d = basename(dirname(f));
1452
- const key = `${p.name} (${bucket} - ${d})`;
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 = createHash("sha256").update(content).digest("hex");
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 createHash2 } from "crypto";
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 = createHash2("sha256").update(primaryContent).digest("hex");
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.3.1");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-splain",
3
- "version": "2.4.0",
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",