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.
- package/dist/export/ArtifactBundleWriter.js +24 -6
- package/dist/export/ExportOrchestrator.js +3 -2
- package/dist/export/Watcher.d.ts +1 -1
- package/dist/export/Watcher.js +9 -1
- package/dist/export/renderers/AgentMarkdownRenderer.d.ts +2 -1
- package/dist/export/renderers/AgentMarkdownRenderer.js +17 -1
- package/dist/export/renderers/HtmlRenderer.js +29 -6
- package/dist/index.js +177 -97
- package/dist/mcp/tools/scan_project.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
44
|
-
// Rename
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
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);
|
package/dist/export/Watcher.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function startWatcher(projectRoot: string, watchedPaths: string[]): void
|
|
1
|
+
export declare function startWatcher(projectRoot: string, watchedPaths: string[]): Promise<void>;
|
package/dist/export/Watcher.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 (!
|
|
26
|
-
console.error('[vibe-splain] UI template not found
|
|
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
|
|
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.
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
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
|
-
|
|
2616
|
+
const { functionId, callerFunctionId, depth, callsite } = queue.shift();
|
|
2617
|
+
const visitKey = `${callerFunctionId}->${functionId}`;
|
|
2618
|
+
if (visited.has(visitKey))
|
|
2600
2619
|
continue;
|
|
2601
|
-
visited.add(
|
|
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
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
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
|
|
2651
|
+
let isActionTarget = false;
|
|
2655
2652
|
if (targetActionKind && action.actionKind === targetActionKind) {
|
|
2656
|
-
|
|
2653
|
+
isActionTarget = true;
|
|
2657
2654
|
if (targetModel && action.targetModel !== targetModel)
|
|
2658
|
-
|
|
2655
|
+
isActionTarget = false;
|
|
2659
2656
|
if (targetOperation && action.targetOperation !== targetOperation)
|
|
2660
|
-
|
|
2657
|
+
isActionTarget = false;
|
|
2661
2658
|
} else if (targetModel && action.targetModel === targetModel) {
|
|
2662
|
-
|
|
2659
|
+
isActionTarget = true;
|
|
2663
2660
|
if (targetOperation && action.targetOperation !== targetOperation)
|
|
2664
|
-
|
|
2661
|
+
isActionTarget = false;
|
|
2665
2662
|
}
|
|
2666
|
-
if (
|
|
2663
|
+
if (isActionTarget)
|
|
2667
2664
|
targetReached = true;
|
|
2668
2665
|
chain.push({
|
|
2669
|
-
functionId: action.
|
|
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
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
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
|
-
|
|
3244
|
-
|
|
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
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
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 (!
|
|
3289
|
-
console.error("[vibe-splain] UI template not found
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|