vibe-splain 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,8 +9,10 @@ export class ArtifactBundleWriter {
9
9
  async writeBundle(artifacts) {
10
10
  const outputDir = join(this.projectRoot, '.vibe-splainer');
11
11
  const stagingDir = join(this.projectRoot, '.vibe-splainer.tmp');
12
+ const oldDir = join(this.projectRoot, '.vibe-splainer.old');
12
13
  try {
13
14
  await rm(stagingDir, { recursive: true, force: true });
15
+ await rm(oldDir, { recursive: true, force: true });
14
16
  const { existsSync } = await import('fs');
15
17
  const { cp } = await import('fs/promises');
16
18
  if (existsSync(outputDir)) {
@@ -40,12 +42,28 @@ export class ArtifactBundleWriter {
40
42
  artifacts: manifestArtifacts,
41
43
  };
42
44
  await writeFile(join(stagingDir, 'artifact_manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
43
- // Atomic rename.
44
- // Rename fails if destination is a non-empty directory.
45
- // So we first delete the existing outputDir.
46
- // Since it's inside the project root, it's safe to do rm -rf .vibe-splainer
47
- await rm(outputDir, { recursive: true, force: true });
48
- await rename(stagingDir, outputDir);
45
+ // Atomic swap pattern:
46
+ // 1. Rename current -> old
47
+ // 2. Rename staging -> current
48
+ // 3. Remove old
49
+ let swapped = false;
50
+ if (existsSync(outputDir)) {
51
+ await rename(outputDir, oldDir);
52
+ swapped = true;
53
+ }
54
+ try {
55
+ await rename(stagingDir, outputDir);
56
+ }
57
+ catch (err) {
58
+ // Rollback if possible
59
+ if (swapped) {
60
+ await rename(oldDir, outputDir);
61
+ }
62
+ throw err;
63
+ }
64
+ if (swapped) {
65
+ await rm(oldDir, { recursive: true, force: true });
66
+ }
49
67
  }
50
68
  catch (err) {
51
69
  await rm(stagingDir, { recursive: true, force: true });
@@ -1,4 +1,4 @@
1
- import { readAnalysis, RecommendationEngine } from '@vibe-splain/brain';
1
+ import { readAnalysis, readActionBindings, RecommendationEngine } from '@vibe-splain/brain';
2
2
  import { ArtifactBundleWriter } from './ArtifactBundleWriter.js';
3
3
  import { JsonRenderer } from './renderers/JsonRenderer.js';
4
4
  import { HtmlRenderer } from './renderers/HtmlRenderer.js';
@@ -17,6 +17,7 @@ export class ExportOrchestrator {
17
17
  if (!finalStore) {
18
18
  throw new Error('Analysis store not found. Scan the project first.');
19
19
  }
20
+ const bindings = await readActionBindings(this.projectRoot);
20
21
  // Aggressive Boilerplate Culling
21
22
  for (const p of dossier.pillars) {
22
23
  p.decisions = p.decisions.filter(c => !(c.severity === 1 && c.category === 'Convention'));
@@ -42,7 +43,7 @@ export class ExportOrchestrator {
42
43
  artifacts.push(...await new HtmlRenderer().render(viewModel, finalStore));
43
44
  }
44
45
  if (formats.includes('markdown')) {
45
- artifacts.push(...await new AgentMarkdownRenderer(options.budget).render(viewModel, finalStore));
46
+ artifacts.push(...await new AgentMarkdownRenderer(options.budget, bindings).render(viewModel, finalStore));
46
47
  }
47
48
  const writer = new ArtifactBundleWriter(this.projectRoot);
48
49
  await writer.writeBundle(artifacts);
@@ -1 +1 @@
1
- export declare function startWatcher(projectRoot: string, watchedPaths: string[]): void;
1
+ export declare function startWatcher(projectRoot: string, watchedPaths: string[]): Promise<void>;
@@ -4,7 +4,14 @@ import { readFile } from 'fs/promises';
4
4
  import { join } from 'path';
5
5
  import { readDossier } from '@vibe-splain/brain';
6
6
  import { ExportOrchestrator } from './ExportOrchestrator.js';
7
- export function startWatcher(projectRoot, watchedPaths) {
7
+ const activeWatchers = new Map();
8
+ export async function startWatcher(projectRoot, watchedPaths) {
9
+ // Clean up existing watcher for this project to prevent resource leaks
10
+ const existing = activeWatchers.get(projectRoot);
11
+ if (existing) {
12
+ await existing.close();
13
+ activeWatchers.delete(projectRoot);
14
+ }
8
15
  const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
9
16
  ignoreInitial: true,
10
17
  ignored: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.vibe-splainer/**'],
@@ -42,6 +49,7 @@ export function startWatcher(projectRoot, watchedPaths) {
42
49
  console.error('[vibe-splain] Watcher error:', err);
43
50
  }
44
51
  });
52
+ activeWatchers.set(projectRoot, watcher);
45
53
  console.error('[vibe-splain] File watcher started');
46
54
  }
47
55
  //# sourceMappingURL=Watcher.js.map
@@ -3,6 +3,7 @@ import type { Renderer } from './Renderer.js';
3
3
  import type { Artifact } from '../ArtifactBundleWriter.js';
4
4
  export declare class AgentMarkdownRenderer implements Renderer {
5
5
  private budget;
6
- constructor(budget?: number);
6
+ private bindings;
7
+ constructor(budget?: number, bindings?: any | null);
7
8
  render(viewModel: DossierViewModel, store: AnalysisStore): Artifact[];
8
9
  }
@@ -1,7 +1,9 @@
1
1
  export class AgentMarkdownRenderer {
2
2
  budget;
3
- constructor(budget = 8000) {
3
+ bindings;
4
+ constructor(budget = 8000, bindings = null) {
4
5
  this.budget = budget;
6
+ this.bindings = bindings;
5
7
  }
6
8
  render(viewModel, store) {
7
9
  let md = `# Architectural Dossier: ${viewModel.projectRoot}\n\n`;
@@ -52,6 +54,20 @@ export class AgentMarkdownRenderer {
52
54
  md += `**Severity**: ${card.severity} | **Category**: ${card.category}\n`;
53
55
  md += `**Narrative**: ${card.narrative}\n`;
54
56
  }
57
+ // Add Function-Level Action Bindings for Tier 1
58
+ if (this.bindings && this.bindings.files[path]) {
59
+ const fileBinding = this.bindings.files[path];
60
+ const criticalFunctions = fileBinding.functions.filter((fn) => fn.semanticActions.length > 0 || fn.isEntrypoint);
61
+ if (criticalFunctions.length > 0) {
62
+ md += `\n**Critical Functions**:\n`;
63
+ for (const fn of criticalFunctions) {
64
+ md += `- \`${fn.displayName}\` (lines ${fn.startLine}-${fn.endLine})${fn.isEntrypoint ? ' [Entrypoint]' : ''}\n`;
65
+ for (const action of fn.semanticActions) {
66
+ md += ` - **${action.actionKind}**${action.targetModel ? ` on ${action.targetModel}` : ''}: \`${action.calleeText}\` (line ${action.sourceLine})\n`;
67
+ }
68
+ }
69
+ }
70
+ }
55
71
  if (recs.length > 0) {
56
72
  md += `\n**Safe Patch Strategies**:\n`;
57
73
  for (const r of recs) {
@@ -17,13 +17,36 @@ function getAllFiles(dirPath, arrayOfFiles = []) {
17
17
  }
18
18
  export class HtmlRenderer {
19
19
  render(viewModel, _store) {
20
- let templateDir = join(__dirname, '..', '..', 'ui'); // from dist/export/renderers -> dist/ui
21
- if (!existsSync(templateDir)) {
22
- // Unbundled path (from cli/src/export/renderers -> cli/dist/ui)
23
- templateDir = join(__dirname, '..', '..', '..', '..', 'dist', 'ui');
20
+ // Robust template resolution for bundled/unbundled environments
21
+ const candidatePaths = [
22
+ join(__dirname, 'ui'), // bundled: dist/index.js -> dist/ui
23
+ join(__dirname, '..', '..', 'ui'), // unbundled: dist/export/renderers -> dist/ui
24
+ join(__dirname, '..', 'ui'), // alt bundle
25
+ join(__dirname, '..', '..', '..', 'ui', 'dist'), // dev: packages/cli/src/export/renderers -> packages/ui/dist
26
+ join(__dirname, '..', '..', 'packages', 'ui', 'dist'), // repo root -> packages/ui/dist
27
+ ];
28
+ let templateDir = '';
29
+ for (const p of candidatePaths) {
30
+ if (existsSync(p) && existsSync(join(p, 'index.html'))) {
31
+ // Double check it's not the source packages/ui (which has vite.config.ts)
32
+ // We want the BUILT UI in the dist folder.
33
+ if (!existsSync(join(p, 'vite.config.ts')) || p.endsWith('dist')) {
34
+ templateDir = p;
35
+ break;
36
+ }
37
+ }
38
+ }
39
+ // Fallback to any 'ui' folder that has index.html as a last resort
40
+ if (!templateDir) {
41
+ for (const p of candidatePaths) {
42
+ if (existsSync(join(p, 'index.html'))) {
43
+ templateDir = p;
44
+ break;
45
+ }
46
+ }
24
47
  }
25
- if (!existsSync(templateDir)) {
26
- console.error('[vibe-splain] UI template not found at', templateDir, '- skipping UI regeneration');
48
+ if (!templateDir) {
49
+ console.error('[vibe-splain] UI template not found. Checked:', candidatePaths);
27
50
  return [];
28
51
  }
29
52
  const artifacts = [];
package/dist/index.js CHANGED
@@ -2276,7 +2276,8 @@ async function runActionBinding(projectRoot, inv, res) {
2276
2276
  const startLine = node.startPosition.row + 1;
2277
2277
  const startCol = node.startPosition.column;
2278
2278
  const endLine = node.endPosition.row + 1;
2279
- const isDuplicate = functions.some((f) => f.startLine === startLine && f.endLine === endLine);
2279
+ const endCol = node.endPosition.column;
2280
+ const isDuplicate = functions.some((f) => f.startLine === startLine && f.startCol === startCol && f.endLine === endLine && f.functionKind === node.type);
2280
2281
  if (isDuplicate)
2281
2282
  continue;
2282
2283
  functionsExtracted++;
@@ -2538,13 +2539,30 @@ async function runActionBinding(projectRoot, inv, res) {
2538
2539
  for (const fileRec of Object.values(artifact.files)) {
2539
2540
  for (const fnRec of fileRec.functions) {
2540
2541
  for (const callRec of fnRec.calls) {
2541
- if (callRec.resolutionKind === "named_import_match" && callRec.resolvedFilePath) {
2542
- const targetFile = artifact.files[callRec.resolvedFilePath];
2543
- if (targetFile) {
2544
- const targetFn = targetFile.functions.find((f) => f.displayName === callRec.calleeRoot);
2545
- if (targetFn) {
2546
- callRec.resolvedTargetFunctionId = targetFn.functionId;
2547
- }
2542
+ if (callRec.resolvedTargetFunctionId)
2543
+ continue;
2544
+ if (!callRec.resolvedFilePath)
2545
+ continue;
2546
+ const targetFile = artifact.files[callRec.resolvedFilePath];
2547
+ if (!targetFile)
2548
+ continue;
2549
+ if (callRec.resolutionKind === "named_import_match") {
2550
+ const targetFn = targetFile.functions.find((f) => f.displayName === callRec.calleeRoot && f.isExported);
2551
+ if (targetFn) {
2552
+ callRec.resolvedTargetFunctionId = targetFn.functionId;
2553
+ callRec.confidence = "high";
2554
+ }
2555
+ } else if (callRec.resolutionKind === "namespace_import_property" && callRec.calleeProperty) {
2556
+ const targetFn = targetFile.functions.find((f) => f.displayName === callRec.calleeProperty && f.isExported);
2557
+ if (targetFn) {
2558
+ callRec.resolvedTargetFunctionId = targetFn.functionId;
2559
+ callRec.confidence = "high";
2560
+ }
2561
+ } else if (callRec.resolutionKind === "namespace_import_property" && !callRec.calleeProperty) {
2562
+ const defaultFn = targetFile.functions.find((f) => f.displayName === "default");
2563
+ if (defaultFn) {
2564
+ callRec.resolvedTargetFunctionId = defaultFn.functionId;
2565
+ callRec.confidence = "high";
2548
2566
  }
2549
2567
  }
2550
2568
  }
@@ -2591,14 +2609,15 @@ async function traverseCallChain(projectRoot, args) {
2591
2609
  const chain = [];
2592
2610
  const unresolvedEdges = [];
2593
2611
  const visited = /* @__PURE__ */ new Set();
2594
- const queue = seedFunctionIds.map((id) => ({ functionId: id, depth: 0 }));
2612
+ const queue = seedFunctionIds.map((id) => ({ functionId: id, callerFunctionId: null, depth: 0 }));
2595
2613
  let targetReached = false;
2596
2614
  let truncatedAtDepth = false;
2597
2615
  while (queue.length > 0) {
2598
- const { functionId, depth } = queue.shift();
2599
- if (visited.has(functionId))
2616
+ const { functionId, callerFunctionId, depth, callsite } = queue.shift();
2617
+ const visitKey = `${callerFunctionId}->${functionId}`;
2618
+ if (visited.has(visitKey))
2600
2619
  continue;
2601
- visited.add(functionId);
2620
+ visited.add(visitKey);
2602
2621
  const indexEntry = artifact.functionIndex[functionId];
2603
2622
  if (!indexEntry)
2604
2623
  continue;
@@ -2610,63 +2629,42 @@ async function traverseCallChain(projectRoot, args) {
2610
2629
  const fnRec = fileRec.functions.find((f) => f.functionId === functionId);
2611
2630
  if (!fnRec)
2612
2631
  continue;
2613
- for (const call of fnRec.calls) {
2614
- if (call.resolutionKind === "semantic_action_only")
2615
- continue;
2616
- if (call.resolvedTargetFunctionId) {
2617
- if (depth < maxDepth) {
2618
- queue.push({ functionId: call.resolvedTargetFunctionId, depth: depth + 1 });
2619
- let isTarget = false;
2620
- if (targetFunctionName && call.calleeRoot === targetFunctionName)
2621
- isTarget = true;
2622
- if (isTarget)
2623
- targetReached = true;
2624
- chain.push({
2625
- functionId: call.resolvedTargetFunctionId,
2626
- displayName: call.calleeRoot,
2627
- filePath: call.resolvedFilePath || "unknown",
2628
- startLine: call.sourceLine,
2629
- edgeKind: "call_edge",
2630
- confidence: call.confidence,
2631
- evidenceText: call.evidenceText,
2632
- isTarget,
2633
- depth
2634
- });
2635
- } else {
2636
- unresolvedEdges.push({
2637
- fromFunctionId: functionId,
2638
- calleeText: call.calleeText,
2639
- sourceLine: call.sourceLine,
2640
- reason: "depth limit reached"
2641
- });
2642
- truncatedAtDepth = true;
2643
- }
2644
- } else {
2645
- unresolvedEdges.push({
2646
- fromFunctionId: functionId,
2647
- calleeText: call.calleeText,
2648
- sourceLine: call.sourceLine,
2649
- reason: call.resolutionKind
2650
- });
2651
- }
2652
- }
2632
+ let isTarget = false;
2633
+ if (targetFunctionName && fnRec.displayName === targetFunctionName)
2634
+ isTarget = true;
2635
+ if (isTarget)
2636
+ targetReached = true;
2637
+ chain.push({
2638
+ functionId,
2639
+ callerFunctionId,
2640
+ displayName: fnRec.displayName,
2641
+ filePath: fnRec.filePath,
2642
+ startLine: fnRec.startLine,
2643
+ edgeKind: "call_edge",
2644
+ confidence: "high",
2645
+ evidenceText: fnRec.evidenceText,
2646
+ isTarget,
2647
+ depth,
2648
+ callsite
2649
+ });
2653
2650
  for (const action of fnRec.semanticActions) {
2654
- let isTarget = false;
2651
+ let isActionTarget = false;
2655
2652
  if (targetActionKind && action.actionKind === targetActionKind) {
2656
- isTarget = true;
2653
+ isActionTarget = true;
2657
2654
  if (targetModel && action.targetModel !== targetModel)
2658
- isTarget = false;
2655
+ isActionTarget = false;
2659
2656
  if (targetOperation && action.targetOperation !== targetOperation)
2660
- isTarget = false;
2657
+ isActionTarget = false;
2661
2658
  } else if (targetModel && action.targetModel === targetModel) {
2662
- isTarget = true;
2659
+ isActionTarget = true;
2663
2660
  if (targetOperation && action.targetOperation !== targetOperation)
2664
- isTarget = false;
2661
+ isActionTarget = false;
2665
2662
  }
2666
- if (isTarget)
2663
+ if (isActionTarget)
2667
2664
  targetReached = true;
2668
2665
  chain.push({
2669
- functionId: action.sourceFunctionId,
2666
+ functionId: action.actionId,
2667
+ callerFunctionId: functionId,
2670
2668
  displayName: action.calleeText,
2671
2669
  filePath: fileRec.filePath,
2672
2670
  startLine: action.sourceLine,
@@ -2676,10 +2674,35 @@ async function traverseCallChain(projectRoot, args) {
2676
2674
  targetOperation: action.targetOperation || void 0,
2677
2675
  confidence: action.confidence,
2678
2676
  evidenceText: action.evidenceText,
2679
- isTarget,
2680
- depth
2677
+ isTarget: isActionTarget,
2678
+ depth: depth + 1
2681
2679
  });
2682
2680
  }
2681
+ if (depth < maxDepth) {
2682
+ for (const call of fnRec.calls) {
2683
+ if (call.resolvedTargetFunctionId) {
2684
+ queue.push({
2685
+ functionId: call.resolvedTargetFunctionId,
2686
+ callerFunctionId: functionId,
2687
+ depth: depth + 1,
2688
+ callsite: {
2689
+ file: fileRec.filePath,
2690
+ line: call.sourceLine,
2691
+ text: call.calleeText
2692
+ }
2693
+ });
2694
+ } else {
2695
+ unresolvedEdges.push({
2696
+ fromFunctionId: functionId,
2697
+ calleeText: call.calleeText,
2698
+ sourceLine: call.sourceLine,
2699
+ reason: call.resolutionKind
2700
+ });
2701
+ }
2702
+ }
2703
+ } else if (fnRec.calls.length > 0) {
2704
+ truncatedAtDepth = true;
2705
+ }
2683
2706
  }
2684
2707
  return {
2685
2708
  targetReached,
@@ -2895,33 +2918,13 @@ async function buildValidationReport(store, deltaTargets, projectRoot, cr) {
2895
2918
  passCount++;
2896
2919
  }
2897
2920
  const PAYMENT_PROVIDER_PATH_TERMS = ["stripe", "paypal", "btcpay", "btcpayserver", "alby", "hitpay", "payment"];
2898
- const PAYMENT_CONTENT_TERMS = [
2899
- "constructEvent",
2900
- "checkoutSession",
2901
- "paymentIntent",
2902
- "stripe-signature",
2903
- "webhook-signature",
2904
- "payment_mutation",
2905
- "paymentStatus",
2906
- "invoicePaid",
2907
- "chargeSucceeded"
2908
- ];
2909
2921
  for (const [rel, pf] of Object.entries(store.files)) {
2910
2922
  if (!pf.isRealSource)
2911
2923
  continue;
2912
- const pathLower = rel.toLowerCase();
2913
- if (!pathLower.includes("webhook"))
2914
- continue;
2915
- const primaryTrigger = PAYMENT_PROVIDER_PATH_TERMS.some((t) => pathLower.includes(t));
2916
- let secondaryTrigger = false;
2917
- if (!primaryTrigger && pf.productDomain !== "payments_webhooks") {
2918
- try {
2919
- const src = await readFile5(join6(projectRoot, rel), "utf8");
2920
- secondaryTrigger = PAYMENT_CONTENT_TERMS.some((t) => src.includes(t));
2921
- } catch {
2922
- }
2923
- }
2924
- if (!primaryTrigger && !secondaryTrigger)
2924
+ const hasIntent = pf.writeIntents.includes("handle_payment_webhook");
2925
+ const hasEffects = pf.sideEffectProfile.includes("webhook_ingress") || pf.sideEffectProfile.includes("payment_mutation");
2926
+ const pathMentionsPayment = PAYMENT_PROVIDER_PATH_TERMS.some((t) => rel.toLowerCase().includes(t));
2927
+ if (!hasIntent && !(hasEffects && pathMentionsPayment))
2925
2928
  continue;
2926
2929
  const webhookChecks = [
2927
2930
  [
@@ -3087,6 +3090,15 @@ async function readAnalysis(projectRoot) {
3087
3090
  return null;
3088
3091
  }
3089
3092
  }
3093
+ async function readActionBindings(projectRoot) {
3094
+ const p = join8(projectRoot, ".vibe-splainer", "action_bindings.json");
3095
+ try {
3096
+ const raw = await readFile7(p, "utf8");
3097
+ return JSON.parse(raw);
3098
+ } catch {
3099
+ return null;
3100
+ }
3101
+ }
3090
3102
 
3091
3103
  // ../brain/dist/dossier.js
3092
3104
  import { join as join9 } from "path";
@@ -3210,8 +3222,10 @@ var ArtifactBundleWriter = class {
3210
3222
  async writeBundle(artifacts) {
3211
3223
  const outputDir = join10(this.projectRoot, ".vibe-splainer");
3212
3224
  const stagingDir = join10(this.projectRoot, ".vibe-splainer.tmp");
3225
+ const oldDir = join10(this.projectRoot, ".vibe-splainer.old");
3213
3226
  try {
3214
3227
  await rm(stagingDir, { recursive: true, force: true });
3228
+ await rm(oldDir, { recursive: true, force: true });
3215
3229
  const { existsSync: existsSync5 } = await import("fs");
3216
3230
  const { cp } = await import("fs/promises");
3217
3231
  if (existsSync5(outputDir)) {
@@ -3240,8 +3254,22 @@ var ArtifactBundleWriter = class {
3240
3254
  artifacts: manifestArtifacts
3241
3255
  };
3242
3256
  await writeFile7(join10(stagingDir, "artifact_manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
3243
- await rm(outputDir, { recursive: true, force: true });
3244
- await rename(stagingDir, outputDir);
3257
+ let swapped = false;
3258
+ if (existsSync5(outputDir)) {
3259
+ await rename(outputDir, oldDir);
3260
+ swapped = true;
3261
+ }
3262
+ try {
3263
+ await rename(stagingDir, outputDir);
3264
+ } catch (err) {
3265
+ if (swapped) {
3266
+ await rename(oldDir, outputDir);
3267
+ }
3268
+ throw err;
3269
+ }
3270
+ if (swapped) {
3271
+ await rm(oldDir, { recursive: true, force: true });
3272
+ }
3245
3273
  } catch (err) {
3246
3274
  await rm(stagingDir, { recursive: true, force: true });
3247
3275
  throw err;
@@ -3281,12 +3309,37 @@ function getAllFiles(dirPath, arrayOfFiles = []) {
3281
3309
  }
3282
3310
  var HtmlRenderer = class {
3283
3311
  render(viewModel, _store) {
3284
- let templateDir = join11(__dirname2, "..", "..", "ui");
3285
- if (!existsSync4(templateDir)) {
3286
- templateDir = join11(__dirname2, "..", "..", "..", "..", "dist", "ui");
3312
+ const candidatePaths = [
3313
+ join11(__dirname2, "ui"),
3314
+ // bundled: dist/index.js -> dist/ui
3315
+ join11(__dirname2, "..", "..", "ui"),
3316
+ // unbundled: dist/export/renderers -> dist/ui
3317
+ join11(__dirname2, "..", "ui"),
3318
+ // alt bundle
3319
+ join11(__dirname2, "..", "..", "..", "ui", "dist"),
3320
+ // dev: packages/cli/src/export/renderers -> packages/ui/dist
3321
+ join11(__dirname2, "..", "..", "packages", "ui", "dist")
3322
+ // repo root -> packages/ui/dist
3323
+ ];
3324
+ let templateDir = "";
3325
+ for (const p of candidatePaths) {
3326
+ if (existsSync4(p) && existsSync4(join11(p, "index.html"))) {
3327
+ if (!existsSync4(join11(p, "vite.config.ts")) || p.endsWith("dist")) {
3328
+ templateDir = p;
3329
+ break;
3330
+ }
3331
+ }
3332
+ }
3333
+ if (!templateDir) {
3334
+ for (const p of candidatePaths) {
3335
+ if (existsSync4(join11(p, "index.html"))) {
3336
+ templateDir = p;
3337
+ break;
3338
+ }
3339
+ }
3287
3340
  }
3288
- if (!existsSync4(templateDir)) {
3289
- console.error("[vibe-splain] UI template not found at", templateDir, "- skipping UI regeneration");
3341
+ if (!templateDir) {
3342
+ console.error("[vibe-splain] UI template not found. Checked:", candidatePaths);
3290
3343
  return [];
3291
3344
  }
3292
3345
  const artifacts = [];
@@ -3337,8 +3390,10 @@ var DeltaRenderer = class {
3337
3390
  // dist/export/renderers/AgentMarkdownRenderer.js
3338
3391
  var AgentMarkdownRenderer = class {
3339
3392
  budget;
3340
- constructor(budget = 8e3) {
3393
+ bindings;
3394
+ constructor(budget = 8e3, bindings = null) {
3341
3395
  this.budget = budget;
3396
+ this.bindings = bindings;
3342
3397
  }
3343
3398
  render(viewModel, store) {
3344
3399
  let md = `# Architectural Dossier: ${viewModel.projectRoot}
@@ -3401,6 +3456,23 @@ ${viewModel.map.brief}
3401
3456
  md += `**Narrative**: ${card.narrative}
3402
3457
  `;
3403
3458
  }
3459
+ if (this.bindings && this.bindings.files[path]) {
3460
+ const fileBinding = this.bindings.files[path];
3461
+ const criticalFunctions = fileBinding.functions.filter((fn) => fn.semanticActions.length > 0 || fn.isEntrypoint);
3462
+ if (criticalFunctions.length > 0) {
3463
+ md += `
3464
+ **Critical Functions**:
3465
+ `;
3466
+ for (const fn of criticalFunctions) {
3467
+ md += `- \`${fn.displayName}\` (lines ${fn.startLine}-${fn.endLine})${fn.isEntrypoint ? " [Entrypoint]" : ""}
3468
+ `;
3469
+ for (const action of fn.semanticActions) {
3470
+ md += ` - **${action.actionKind}**${action.targetModel ? ` on ${action.targetModel}` : ""}: \`${action.calleeText}\` (line ${action.sourceLine})
3471
+ `;
3472
+ }
3473
+ }
3474
+ }
3475
+ }
3404
3476
  if (recs.length > 0) {
3405
3477
  md += `
3406
3478
  **Safe Patch Strategies**:
@@ -3506,6 +3578,7 @@ var ExportOrchestrator = class {
3506
3578
  if (!finalStore) {
3507
3579
  throw new Error("Analysis store not found. Scan the project first.");
3508
3580
  }
3581
+ const bindings = await readActionBindings(this.projectRoot);
3509
3582
  for (const p of dossier.pillars) {
3510
3583
  p.decisions = p.decisions.filter((c) => !(c.severity === 1 && c.category === "Convention"));
3511
3584
  p.cardCount = p.decisions.length;
@@ -3528,7 +3601,7 @@ var ExportOrchestrator = class {
3528
3601
  artifacts.push(...await new HtmlRenderer().render(viewModel, finalStore));
3529
3602
  }
3530
3603
  if (formats.includes("markdown")) {
3531
- artifacts.push(...await new AgentMarkdownRenderer(options.budget).render(viewModel, finalStore));
3604
+ artifacts.push(...await new AgentMarkdownRenderer(options.budget, bindings).render(viewModel, finalStore));
3532
3605
  }
3533
3606
  const writer = new ArtifactBundleWriter(this.projectRoot);
3534
3607
  await writer.writeBundle(artifacts);
@@ -3561,7 +3634,13 @@ import chokidar from "chokidar";
3561
3634
  import { createHash as createHash2 } from "crypto";
3562
3635
  import { readFile as readFile9 } from "fs/promises";
3563
3636
  import { join as join12 } from "path";
3564
- function startWatcher(projectRoot, watchedPaths) {
3637
+ var activeWatchers = /* @__PURE__ */ new Map();
3638
+ async function startWatcher(projectRoot, watchedPaths) {
3639
+ const existing = activeWatchers.get(projectRoot);
3640
+ if (existing) {
3641
+ await existing.close();
3642
+ activeWatchers.delete(projectRoot);
3643
+ }
3565
3644
  const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
3566
3645
  ignoreInitial: true,
3567
3646
  ignored: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.vibe-splainer/**"],
@@ -3598,6 +3677,7 @@ function startWatcher(projectRoot, watchedPaths) {
3598
3677
  console.error("[vibe-splain] Watcher error:", err);
3599
3678
  }
3600
3679
  });
3680
+ activeWatchers.set(projectRoot, watcher);
3601
3681
  console.error("[vibe-splain] File watcher started");
3602
3682
  }
3603
3683
 
@@ -3652,7 +3732,7 @@ async function handleScanProject(args, options = {}) {
3652
3732
  budget: options.budget ? parseInt(options.budget, 10) : void 0,
3653
3733
  scope: options.scope
3654
3734
  }, result.store, result.graph);
3655
- startWatcher(projectRoot, result.files.map((f) => f.path));
3735
+ await startWatcher(projectRoot, result.files.map((f) => f.path));
3656
3736
  console.error(`[vibe-splain] Scan complete. ${result.totalFilesScanned} files, ${result.realSourceCount} real-source, ${result.wildCandidates.length} wild candidates.`);
3657
3737
  const validation = result.validation ?? { passed: true, errors: 0, warnings: 0, reportPath: ".vibe-splainer/validation_report.json" };
3658
3738
  let statusMsg = "Scan complete.";
@@ -54,7 +54,7 @@ export async function handleScanProject(args, options = {}) {
54
54
  scope: options.scope,
55
55
  }, result.store, result.graph);
56
56
  // Watch the real-source files for staleness.
57
- startWatcher(projectRoot, result.files.map(f => f.path));
57
+ await startWatcher(projectRoot, result.files.map(f => f.path));
58
58
  console.error(`[vibe-splain] Scan complete. ${result.totalFilesScanned} files, ${result.realSourceCount} real-source, ${result.wildCandidates.length} wild candidates.`);
59
59
  const validation = result.validation ?? { passed: true, errors: 0, warnings: 0, reportPath: '.vibe-splainer/validation_report.json' };
60
60
  let statusMsg = 'Scan complete.';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-splain",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Architectural mapping and behavioral call-chain engine. Built on a language-agnostic foundation with specialized optimization for TypeScript/JavaScript projects.",
5
5
  "type": "module",
6
6
  "license": "MIT",