vibe-splain 3.0.0 → 3.2.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/commands/bundle.d.ts +4 -0
- package/dist/commands/bundle.js +68 -0
- package/dist/commands/gc.d.ts +3 -0
- package/dist/commands/gc.js +59 -0
- package/dist/commands/importBundle.d.ts +4 -0
- package/dist/commands/importBundle.js +80 -0
- package/dist/export/ArtifactBundleWriter.js +24 -6
- package/dist/export/ExportOrchestrator.d.ts +19 -1
- package/dist/export/ExportOrchestrator.js +90 -3
- 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 +1671 -129
- package/dist/mcp/BudgetGuard.d.ts +13 -0
- package/dist/mcp/BudgetGuard.js +55 -0
- package/dist/mcp/SessionScope.d.ts +26 -0
- package/dist/mcp/SessionScope.js +56 -0
- package/dist/mcp/server.js +38 -0
- package/dist/mcp/tools/apply_patch.d.ts +37 -0
- package/dist/mcp/tools/apply_patch.js +103 -0
- package/dist/mcp/tools/get_file_skeleton.d.ts +23 -0
- package/dist/mcp/tools/get_file_skeleton.js +124 -0
- package/dist/mcp/tools/hydration/get_evidence_slice.d.ts +31 -0
- package/dist/mcp/tools/hydration/get_evidence_slice.js +59 -0
- package/dist/mcp/tools/hydration/get_project_summary.d.ts +23 -0
- package/dist/mcp/tools/hydration/get_project_summary.js +58 -0
- package/dist/mcp/tools/hydration/get_start_here.d.ts +23 -0
- package/dist/mcp/tools/hydration/get_start_here.js +52 -0
- package/dist/mcp/tools/read_file.d.ts +31 -0
- package/dist/mcp/tools/read_file.js +90 -0
- package/dist/mcp/tools/scan_project.js +6 -3
- package/dist/mcp/tools/set_session_scope.d.ts +19 -0
- package/dist/mcp/tools/set_session_scope.js +40 -0
- package/dist/mcp/tools/submit_receipt.d.ts +68 -0
- package/dist/mcp/tools/submit_receipt.js +94 -0
- package/dist/mcp/tools/work_orders.d.ts +79 -0
- package/dist/mcp/tools/work_orders.js +126 -0
- package/dist/mcp/tools/yield_for_scope_expansion.d.ts +29 -0
- package/dist/mcp/tools/yield_for_scope_expansion.js +59 -0
- package/dist/store/BlobStore.d.ts +22 -0
- package/dist/store/BlobStore.js +96 -0
- package/dist/store/PointerStore.d.ts +52 -0
- package/dist/store/PointerStore.js +138 -0
- package/package.json +8 -1
package/dist/index.js
CHANGED
|
@@ -1201,10 +1201,10 @@ async function discoverAllTsConfigs(dir, projectRoot, maxDepth = 4) {
|
|
|
1201
1201
|
const result = {};
|
|
1202
1202
|
if (maxDepth < 0)
|
|
1203
1203
|
return result;
|
|
1204
|
-
const { readdir:
|
|
1204
|
+
const { readdir: readdir3 } = await import("fs/promises");
|
|
1205
1205
|
let entries = [];
|
|
1206
1206
|
try {
|
|
1207
|
-
entries = await
|
|
1207
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
1208
1208
|
} catch {
|
|
1209
1209
|
return result;
|
|
1210
1210
|
}
|
|
@@ -1284,10 +1284,10 @@ async function discoverWorkspacePackages(projectRoot) {
|
|
|
1284
1284
|
const absPrefix = join3(projectRoot, prefix);
|
|
1285
1285
|
if (!existsSync3(absPrefix))
|
|
1286
1286
|
continue;
|
|
1287
|
-
const { readdir:
|
|
1287
|
+
const { readdir: readdir3 } = await import("fs/promises");
|
|
1288
1288
|
let entries = [];
|
|
1289
1289
|
try {
|
|
1290
|
-
const dirents = await
|
|
1290
|
+
const dirents = await readdir3(absPrefix, { withFileTypes: true });
|
|
1291
1291
|
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1292
1292
|
} catch {
|
|
1293
1293
|
continue;
|
|
@@ -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";
|
|
@@ -3198,10 +3210,93 @@ var RecommendationEngine = class {
|
|
|
3198
3210
|
}
|
|
3199
3211
|
};
|
|
3200
3212
|
|
|
3213
|
+
// ../brain/dist/ProofValidator.js
|
|
3214
|
+
import { createHash } from "crypto";
|
|
3215
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
3216
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3217
|
+
var ProofValidator = class {
|
|
3218
|
+
/**
|
|
3219
|
+
* Full validation of a WorkerReceipt against a Work Order's required proofs.
|
|
3220
|
+
* Checks all 8 required conditions from the spec.
|
|
3221
|
+
*
|
|
3222
|
+
* @param receipt - The WorkerReceipt to validate
|
|
3223
|
+
* @param requiredProof - Proof descriptors from the Work Order
|
|
3224
|
+
* @param isAllowedFile - Predicate built by the CLI layer (handles globs/allowedFiles)
|
|
3225
|
+
* @param blobDir - Path to .vibe-splainer/blobs/
|
|
3226
|
+
*/
|
|
3227
|
+
static async validate(receipt, requiredProof, isAllowedFile, blobDir) {
|
|
3228
|
+
const errors = [];
|
|
3229
|
+
const warnings = [];
|
|
3230
|
+
for (const req of requiredProof) {
|
|
3231
|
+
const found = receipt.proofPointers.find((p) => p.schemaName === req.schemaName || p.pointer.includes(req.proofId));
|
|
3232
|
+
if (!found) {
|
|
3233
|
+
errors.push(`MissingProof: required proof "${req.proofId}" (schema: ${req.schemaName}) not in receipt`);
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
for (const proof of receipt.proofPointers) {
|
|
3237
|
+
const blobPath = resolveBlobPath(blobDir, proof.contentHash);
|
|
3238
|
+
if (!existsSync4(blobPath)) {
|
|
3239
|
+
errors.push(`UnresolvablePointer: blob not found for pointer ${proof.pointer} (hash: ${proof.contentHash})`);
|
|
3240
|
+
continue;
|
|
3241
|
+
}
|
|
3242
|
+
const actualHash = await hashBlob(blobPath);
|
|
3243
|
+
if (actualHash !== proof.contentHash) {
|
|
3244
|
+
errors.push(`HashMismatch: proof ${proof.pointer} expected ${proof.contentHash}, got ${actualHash}`);
|
|
3245
|
+
continue;
|
|
3246
|
+
}
|
|
3247
|
+
const reqDescriptor = requiredProof.find((r) => r.schemaName === proof.schemaName);
|
|
3248
|
+
if (!reqDescriptor) {
|
|
3249
|
+
warnings.push(`UnknownSchema: proof ${proof.pointer} has schema "${proof.schemaName}" not listed in requiredProof`);
|
|
3250
|
+
}
|
|
3251
|
+
if (proof.schemaName.startsWith("test_report")) {
|
|
3252
|
+
try {
|
|
3253
|
+
const blobContent = await readFile9(blobPath, "utf8");
|
|
3254
|
+
const report = JSON.parse(blobContent);
|
|
3255
|
+
if (report.status !== "pass" && report.passed !== true && report.success !== true) {
|
|
3256
|
+
errors.push(`TestFailed: proof ${proof.pointer} (schema: ${proof.schemaName}) reports status "${report.status ?? "unknown"}"`);
|
|
3257
|
+
}
|
|
3258
|
+
} catch {
|
|
3259
|
+
errors.push(`UnreadableProof: cannot parse proof blob for ${proof.pointer}`);
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
for (const changed of receipt.changedFiles) {
|
|
3264
|
+
if (!isAllowedFile(changed.path)) {
|
|
3265
|
+
errors.push(`ScopeViolation: patch touched out-of-scope file "${changed.path}"`);
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
for (const changed of receipt.changedFiles) {
|
|
3269
|
+
if (!changed.prePatchHash.startsWith("sha256:") && changed.prePatchHash !== "sha256:new") {
|
|
3270
|
+
errors.push(`InvalidHash: prePatchHash for ${changed.path} is not sha256 format`);
|
|
3271
|
+
}
|
|
3272
|
+
if (!changed.postPatchHash.startsWith("sha256:")) {
|
|
3273
|
+
errors.push(`InvalidHash: postPatchHash for ${changed.path} is not sha256 format`);
|
|
3274
|
+
}
|
|
3275
|
+
const matchingPatch = receipt.proofPointers.find((p) => p.schemaName === "patch_hash" && p.contentHash === changed.postPatchHash);
|
|
3276
|
+
if (!matchingPatch) {
|
|
3277
|
+
warnings.push(`NoMatchingPatchProof: no patch_hash proof matches postPatchHash for ${changed.path}`);
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
return {
|
|
3281
|
+
valid: errors.length === 0,
|
|
3282
|
+
errors,
|
|
3283
|
+
warnings
|
|
3284
|
+
};
|
|
3285
|
+
}
|
|
3286
|
+
};
|
|
3287
|
+
function resolveBlobPath(blobDir, contentHash) {
|
|
3288
|
+
const hex = contentHash.replace("sha256:", "");
|
|
3289
|
+
return `${blobDir}/sha256_${hex}`;
|
|
3290
|
+
}
|
|
3291
|
+
async function hashBlob(blobPath) {
|
|
3292
|
+
const buf = await readFile9(blobPath);
|
|
3293
|
+
return `sha256:${createHash("sha256").update(buf).digest("hex")}`;
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3201
3296
|
// dist/export/ArtifactBundleWriter.js
|
|
3202
3297
|
import { join as join10 } from "path";
|
|
3203
3298
|
import { writeFile as writeFile7, mkdir as mkdir6, rm, rename } from "fs/promises";
|
|
3204
|
-
import { createHash } from "crypto";
|
|
3299
|
+
import { createHash as createHash2 } from "crypto";
|
|
3205
3300
|
var ArtifactBundleWriter = class {
|
|
3206
3301
|
projectRoot;
|
|
3207
3302
|
constructor(projectRoot) {
|
|
@@ -3210,11 +3305,13 @@ var ArtifactBundleWriter = class {
|
|
|
3210
3305
|
async writeBundle(artifacts) {
|
|
3211
3306
|
const outputDir = join10(this.projectRoot, ".vibe-splainer");
|
|
3212
3307
|
const stagingDir = join10(this.projectRoot, ".vibe-splainer.tmp");
|
|
3308
|
+
const oldDir = join10(this.projectRoot, ".vibe-splainer.old");
|
|
3213
3309
|
try {
|
|
3214
3310
|
await rm(stagingDir, { recursive: true, force: true });
|
|
3215
|
-
|
|
3311
|
+
await rm(oldDir, { recursive: true, force: true });
|
|
3312
|
+
const { existsSync: existsSync9 } = await import("fs");
|
|
3216
3313
|
const { cp } = await import("fs/promises");
|
|
3217
|
-
if (
|
|
3314
|
+
if (existsSync9(outputDir)) {
|
|
3218
3315
|
await cp(outputDir, stagingDir, { recursive: true });
|
|
3219
3316
|
} else {
|
|
3220
3317
|
await mkdir6(stagingDir, { recursive: true });
|
|
@@ -3229,7 +3326,7 @@ var ArtifactBundleWriter = class {
|
|
|
3229
3326
|
manifestArtifacts.push({
|
|
3230
3327
|
type: artifact.type,
|
|
3231
3328
|
path: artifact.path,
|
|
3232
|
-
checksum: "sha256:" +
|
|
3329
|
+
checksum: "sha256:" + createHash2("sha256").update(buffer).digest("hex"),
|
|
3233
3330
|
sizeBytes: buffer.length
|
|
3234
3331
|
});
|
|
3235
3332
|
}
|
|
@@ -3240,8 +3337,22 @@ var ArtifactBundleWriter = class {
|
|
|
3240
3337
|
artifacts: manifestArtifacts
|
|
3241
3338
|
};
|
|
3242
3339
|
await writeFile7(join10(stagingDir, "artifact_manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
3243
|
-
|
|
3244
|
-
|
|
3340
|
+
let swapped = false;
|
|
3341
|
+
if (existsSync9(outputDir)) {
|
|
3342
|
+
await rename(outputDir, oldDir);
|
|
3343
|
+
swapped = true;
|
|
3344
|
+
}
|
|
3345
|
+
try {
|
|
3346
|
+
await rename(stagingDir, outputDir);
|
|
3347
|
+
} catch (err) {
|
|
3348
|
+
if (swapped) {
|
|
3349
|
+
await rename(oldDir, outputDir);
|
|
3350
|
+
}
|
|
3351
|
+
throw err;
|
|
3352
|
+
}
|
|
3353
|
+
if (swapped) {
|
|
3354
|
+
await rm(oldDir, { recursive: true, force: true });
|
|
3355
|
+
}
|
|
3245
3356
|
} catch (err) {
|
|
3246
3357
|
await rm(stagingDir, { recursive: true, force: true });
|
|
3247
3358
|
throw err;
|
|
@@ -3265,7 +3376,7 @@ var JsonRenderer = class {
|
|
|
3265
3376
|
// dist/export/renderers/HtmlRenderer.js
|
|
3266
3377
|
import { join as join11, dirname as dirname3, relative as relative3 } from "path";
|
|
3267
3378
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3268
|
-
import { existsSync as
|
|
3379
|
+
import { existsSync as existsSync5, readFileSync, readdirSync, statSync } from "fs";
|
|
3269
3380
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
3270
3381
|
function getAllFiles(dirPath, arrayOfFiles = []) {
|
|
3271
3382
|
const files = readdirSync(dirPath);
|
|
@@ -3281,12 +3392,37 @@ function getAllFiles(dirPath, arrayOfFiles = []) {
|
|
|
3281
3392
|
}
|
|
3282
3393
|
var HtmlRenderer = class {
|
|
3283
3394
|
render(viewModel, _store) {
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3395
|
+
const candidatePaths = [
|
|
3396
|
+
join11(__dirname2, "ui"),
|
|
3397
|
+
// bundled: dist/index.js -> dist/ui
|
|
3398
|
+
join11(__dirname2, "..", "..", "ui"),
|
|
3399
|
+
// unbundled: dist/export/renderers -> dist/ui
|
|
3400
|
+
join11(__dirname2, "..", "ui"),
|
|
3401
|
+
// alt bundle
|
|
3402
|
+
join11(__dirname2, "..", "..", "..", "ui", "dist"),
|
|
3403
|
+
// dev: packages/cli/src/export/renderers -> packages/ui/dist
|
|
3404
|
+
join11(__dirname2, "..", "..", "packages", "ui", "dist")
|
|
3405
|
+
// repo root -> packages/ui/dist
|
|
3406
|
+
];
|
|
3407
|
+
let templateDir = "";
|
|
3408
|
+
for (const p of candidatePaths) {
|
|
3409
|
+
if (existsSync5(p) && existsSync5(join11(p, "index.html"))) {
|
|
3410
|
+
if (!existsSync5(join11(p, "vite.config.ts")) || p.endsWith("dist")) {
|
|
3411
|
+
templateDir = p;
|
|
3412
|
+
break;
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3287
3415
|
}
|
|
3288
|
-
if (!
|
|
3289
|
-
|
|
3416
|
+
if (!templateDir) {
|
|
3417
|
+
for (const p of candidatePaths) {
|
|
3418
|
+
if (existsSync5(join11(p, "index.html"))) {
|
|
3419
|
+
templateDir = p;
|
|
3420
|
+
break;
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
if (!templateDir) {
|
|
3425
|
+
console.error("[vibe-splain] UI template not found. Checked:", candidatePaths);
|
|
3290
3426
|
return [];
|
|
3291
3427
|
}
|
|
3292
3428
|
const artifacts = [];
|
|
@@ -3337,8 +3473,10 @@ var DeltaRenderer = class {
|
|
|
3337
3473
|
// dist/export/renderers/AgentMarkdownRenderer.js
|
|
3338
3474
|
var AgentMarkdownRenderer = class {
|
|
3339
3475
|
budget;
|
|
3340
|
-
|
|
3476
|
+
bindings;
|
|
3477
|
+
constructor(budget = 8e3, bindings = null) {
|
|
3341
3478
|
this.budget = budget;
|
|
3479
|
+
this.bindings = bindings;
|
|
3342
3480
|
}
|
|
3343
3481
|
render(viewModel, store) {
|
|
3344
3482
|
let md = `# Architectural Dossier: ${viewModel.projectRoot}
|
|
@@ -3401,6 +3539,23 @@ ${viewModel.map.brief}
|
|
|
3401
3539
|
md += `**Narrative**: ${card.narrative}
|
|
3402
3540
|
`;
|
|
3403
3541
|
}
|
|
3542
|
+
if (this.bindings && this.bindings.files[path]) {
|
|
3543
|
+
const fileBinding = this.bindings.files[path];
|
|
3544
|
+
const criticalFunctions = fileBinding.functions.filter((fn) => fn.semanticActions.length > 0 || fn.isEntrypoint);
|
|
3545
|
+
if (criticalFunctions.length > 0) {
|
|
3546
|
+
md += `
|
|
3547
|
+
**Critical Functions**:
|
|
3548
|
+
`;
|
|
3549
|
+
for (const fn of criticalFunctions) {
|
|
3550
|
+
md += `- \`${fn.displayName}\` (lines ${fn.startLine}-${fn.endLine})${fn.isEntrypoint ? " [Entrypoint]" : ""}
|
|
3551
|
+
`;
|
|
3552
|
+
for (const action of fn.semanticActions) {
|
|
3553
|
+
md += ` - **${action.actionKind}**${action.targetModel ? ` on ${action.targetModel}` : ""}: \`${action.calleeText}\` (line ${action.sourceLine})
|
|
3554
|
+
`;
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3404
3559
|
if (recs.length > 0) {
|
|
3405
3560
|
md += `
|
|
3406
3561
|
**Safe Patch Strategies**:
|
|
@@ -3495,17 +3650,239 @@ var GraphRenderer = class {
|
|
|
3495
3650
|
}
|
|
3496
3651
|
};
|
|
3497
3652
|
|
|
3653
|
+
// dist/store/BlobStore.js
|
|
3654
|
+
import { createHash as createHash3 } from "crypto";
|
|
3655
|
+
import { mkdir as mkdir7, writeFile as writeFile8, open, rename as rename2, stat } from "fs/promises";
|
|
3656
|
+
import { existsSync as existsSync6 } from "fs";
|
|
3657
|
+
import { join as join12 } from "path";
|
|
3658
|
+
var BlobStore = class {
|
|
3659
|
+
blobsDir;
|
|
3660
|
+
tmpDir;
|
|
3661
|
+
constructor(projectRoot) {
|
|
3662
|
+
this.blobsDir = join12(projectRoot, ".vibe-splainer", "blobs");
|
|
3663
|
+
this.tmpDir = join12(projectRoot, ".vibe-splainer", "tmp");
|
|
3664
|
+
}
|
|
3665
|
+
async ensureDirs() {
|
|
3666
|
+
await mkdir7(this.blobsDir, { recursive: true });
|
|
3667
|
+
await mkdir7(this.tmpDir, { recursive: true });
|
|
3668
|
+
}
|
|
3669
|
+
async writeAtomic(payload) {
|
|
3670
|
+
await this.ensureDirs();
|
|
3671
|
+
const buf = typeof payload === "string" ? Buffer.from(payload, "utf8") : payload;
|
|
3672
|
+
const hex = createHash3("sha256").update(buf).digest("hex");
|
|
3673
|
+
const contentHash = `sha256:${hex}`;
|
|
3674
|
+
const blobPath = join12(this.blobsDir, `sha256_${hex}`);
|
|
3675
|
+
if (existsSync6(blobPath)) {
|
|
3676
|
+
return { contentHash, blobPath };
|
|
3677
|
+
}
|
|
3678
|
+
const tmpPath = join12(this.tmpDir, `tmp_${hex}_${Date.now()}`);
|
|
3679
|
+
await writeFile8(tmpPath, buf);
|
|
3680
|
+
const fh = await open(tmpPath, "r");
|
|
3681
|
+
try {
|
|
3682
|
+
await fh.datasync();
|
|
3683
|
+
} finally {
|
|
3684
|
+
await fh.close();
|
|
3685
|
+
}
|
|
3686
|
+
await rename2(tmpPath, blobPath);
|
|
3687
|
+
return { contentHash, blobPath };
|
|
3688
|
+
}
|
|
3689
|
+
async readBlob(blobPath) {
|
|
3690
|
+
const { readFile: readFile16 } = await import("fs/promises");
|
|
3691
|
+
return readFile16(blobPath);
|
|
3692
|
+
}
|
|
3693
|
+
async blobExists(contentHash) {
|
|
3694
|
+
const hex = contentHash.replace("sha256:", "");
|
|
3695
|
+
const blobPath = join12(this.blobsDir, `sha256_${hex}`);
|
|
3696
|
+
return existsSync6(blobPath);
|
|
3697
|
+
}
|
|
3698
|
+
blobPathForHash(contentHash) {
|
|
3699
|
+
const hex = contentHash.replace("sha256:", "");
|
|
3700
|
+
return join12(this.blobsDir, `sha256_${hex}`);
|
|
3701
|
+
}
|
|
3702
|
+
async verifyIntegrity(blobPath, expectedHash) {
|
|
3703
|
+
try {
|
|
3704
|
+
const { readFile: readFile16 } = await import("fs/promises");
|
|
3705
|
+
const buf = await readFile16(blobPath);
|
|
3706
|
+
const hex = createHash3("sha256").update(buf).digest("hex");
|
|
3707
|
+
return `sha256:${hex}` === expectedHash;
|
|
3708
|
+
} catch {
|
|
3709
|
+
return false;
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
/** List all blob paths for GC reference counting */
|
|
3713
|
+
async listBlobPaths() {
|
|
3714
|
+
try {
|
|
3715
|
+
const { readdir: readdir3 } = await import("fs/promises");
|
|
3716
|
+
const files = await readdir3(this.blobsDir);
|
|
3717
|
+
return files.filter((f) => f.startsWith("sha256_")).map((f) => join12(this.blobsDir, f));
|
|
3718
|
+
} catch {
|
|
3719
|
+
return [];
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
async getBlobSize(blobPath) {
|
|
3723
|
+
try {
|
|
3724
|
+
const info = await stat(blobPath);
|
|
3725
|
+
return info.size;
|
|
3726
|
+
} catch {
|
|
3727
|
+
return 0;
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
};
|
|
3731
|
+
async function hashFile(filePath) {
|
|
3732
|
+
const { readFile: readFile16 } = await import("fs/promises");
|
|
3733
|
+
const buf = await readFile16(filePath);
|
|
3734
|
+
return `sha256:${createHash3("sha256").update(buf).digest("hex")}`;
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
// dist/store/PointerStore.js
|
|
3738
|
+
import Database from "better-sqlite3";
|
|
3739
|
+
import { join as join13 } from "path";
|
|
3740
|
+
import { mkdirSync } from "fs";
|
|
3741
|
+
import { Mutex } from "async-mutex";
|
|
3742
|
+
var instance = null;
|
|
3743
|
+
var PointerStore = class _PointerStore {
|
|
3744
|
+
db;
|
|
3745
|
+
writeMutex = new Mutex();
|
|
3746
|
+
constructor(projectRoot) {
|
|
3747
|
+
const dir = join13(projectRoot, ".vibe-splainer");
|
|
3748
|
+
mkdirSync(dir, { recursive: true });
|
|
3749
|
+
this.db = new Database(join13(dir, "pointer_store.db"));
|
|
3750
|
+
this.db.pragma("busy_timeout = 5000");
|
|
3751
|
+
this.db.pragma("journal_mode = WAL");
|
|
3752
|
+
this.db.pragma("foreign_keys = ON");
|
|
3753
|
+
this._migrate();
|
|
3754
|
+
}
|
|
3755
|
+
static open(projectRoot) {
|
|
3756
|
+
if (!instance)
|
|
3757
|
+
instance = new _PointerStore(projectRoot);
|
|
3758
|
+
return instance;
|
|
3759
|
+
}
|
|
3760
|
+
static reset() {
|
|
3761
|
+
instance = null;
|
|
3762
|
+
}
|
|
3763
|
+
_migrate() {
|
|
3764
|
+
this.db.exec(`
|
|
3765
|
+
CREATE TABLE IF NOT EXISTS pointers (
|
|
3766
|
+
pointerId TEXT PRIMARY KEY,
|
|
3767
|
+
scanId TEXT NOT NULL,
|
|
3768
|
+
artifactName TEXT NOT NULL,
|
|
3769
|
+
contentHash TEXT NOT NULL,
|
|
3770
|
+
blobPath TEXT NOT NULL,
|
|
3771
|
+
schemaVersion TEXT NOT NULL DEFAULT '1.0.0',
|
|
3772
|
+
createdAt INTEGER NOT NULL,
|
|
3773
|
+
expiresAt INTEGER
|
|
3774
|
+
);
|
|
3775
|
+
CREATE INDEX IF NOT EXISTS idx_pointers_scan ON pointers(scanId);
|
|
3776
|
+
CREATE INDEX IF NOT EXISTS idx_pointers_hash ON pointers(contentHash);
|
|
3777
|
+
|
|
3778
|
+
CREATE TABLE IF NOT EXISTS work_orders (
|
|
3779
|
+
workOrderId TEXT PRIMARY KEY,
|
|
3780
|
+
intent TEXT NOT NULL,
|
|
3781
|
+
allowedFiles TEXT NOT NULL DEFAULT '[]',
|
|
3782
|
+
allowedGlobs TEXT NOT NULL DEFAULT '[]',
|
|
3783
|
+
deniedGlobs TEXT NOT NULL DEFAULT '[]',
|
|
3784
|
+
requiredProof TEXT NOT NULL DEFAULT '[]',
|
|
3785
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
3786
|
+
createdAt INTEGER NOT NULL
|
|
3787
|
+
);
|
|
3788
|
+
|
|
3789
|
+
CREATE TABLE IF NOT EXISTS receipts (
|
|
3790
|
+
receiptId TEXT PRIMARY KEY,
|
|
3791
|
+
workOrderId TEXT NOT NULL REFERENCES work_orders(workOrderId),
|
|
3792
|
+
status TEXT NOT NULL,
|
|
3793
|
+
proofPointers TEXT NOT NULL DEFAULT '[]',
|
|
3794
|
+
changedFiles TEXT NOT NULL DEFAULT '[]',
|
|
3795
|
+
summary TEXT NOT NULL DEFAULT '',
|
|
3796
|
+
createdAt INTEGER NOT NULL,
|
|
3797
|
+
FOREIGN KEY (workOrderId) REFERENCES work_orders(workOrderId)
|
|
3798
|
+
);
|
|
3799
|
+
`);
|
|
3800
|
+
}
|
|
3801
|
+
async insertPointer(row) {
|
|
3802
|
+
await this.writeMutex.runExclusive(() => {
|
|
3803
|
+
this.db.prepare(`
|
|
3804
|
+
INSERT OR REPLACE INTO pointers
|
|
3805
|
+
(pointerId, scanId, artifactName, contentHash, blobPath, schemaVersion, createdAt, expiresAt)
|
|
3806
|
+
VALUES
|
|
3807
|
+
(@pointerId, @scanId, @artifactName, @contentHash, @blobPath, @schemaVersion, @createdAt, @expiresAt)
|
|
3808
|
+
`).run(row);
|
|
3809
|
+
});
|
|
3810
|
+
}
|
|
3811
|
+
getPointer(pointerId) {
|
|
3812
|
+
return this.db.prepare("SELECT * FROM pointers WHERE pointerId = ?").get(pointerId) ?? null;
|
|
3813
|
+
}
|
|
3814
|
+
listPointersByScan(scanId) {
|
|
3815
|
+
return this.db.prepare("SELECT * FROM pointers WHERE scanId = ?").all(scanId);
|
|
3816
|
+
}
|
|
3817
|
+
async insertWorkOrder(row) {
|
|
3818
|
+
await this.writeMutex.runExclusive(() => {
|
|
3819
|
+
this.db.prepare(`
|
|
3820
|
+
INSERT OR REPLACE INTO work_orders
|
|
3821
|
+
(workOrderId, intent, allowedFiles, allowedGlobs, deniedGlobs, requiredProof, status, createdAt)
|
|
3822
|
+
VALUES
|
|
3823
|
+
(@workOrderId, @intent, @allowedFiles, @allowedGlobs, @deniedGlobs, @requiredProof, @status, @createdAt)
|
|
3824
|
+
`).run(row);
|
|
3825
|
+
});
|
|
3826
|
+
}
|
|
3827
|
+
getWorkOrder(workOrderId) {
|
|
3828
|
+
return this.db.prepare("SELECT * FROM work_orders WHERE workOrderId = ?").get(workOrderId) ?? null;
|
|
3829
|
+
}
|
|
3830
|
+
async updateWorkOrderStatus(workOrderId, status) {
|
|
3831
|
+
await this.writeMutex.runExclusive(() => {
|
|
3832
|
+
this.db.prepare("UPDATE work_orders SET status = ? WHERE workOrderId = ?").run(status, workOrderId);
|
|
3833
|
+
});
|
|
3834
|
+
}
|
|
3835
|
+
async insertReceipt(receipt) {
|
|
3836
|
+
await this.writeMutex.runExclusive(() => {
|
|
3837
|
+
this.db.prepare(`
|
|
3838
|
+
INSERT OR REPLACE INTO receipts
|
|
3839
|
+
(receiptId, workOrderId, status, proofPointers, changedFiles, summary, createdAt)
|
|
3840
|
+
VALUES
|
|
3841
|
+
(@receiptId, @workOrderId, @status, @proofPointers, @changedFiles, @summary, @createdAt)
|
|
3842
|
+
`).run({
|
|
3843
|
+
...receipt,
|
|
3844
|
+
proofPointers: JSON.stringify(receipt.proofPointers),
|
|
3845
|
+
changedFiles: JSON.stringify(receipt.changedFiles),
|
|
3846
|
+
createdAt: Date.now()
|
|
3847
|
+
});
|
|
3848
|
+
});
|
|
3849
|
+
}
|
|
3850
|
+
/** GC: delete pointers older than cutoffMs and not pinned, return deleted count */
|
|
3851
|
+
async gcScanPointers(keepScanIds) {
|
|
3852
|
+
return await this.writeMutex.runExclusive(() => {
|
|
3853
|
+
const placeholders = keepScanIds.map(() => "?").join(",");
|
|
3854
|
+
const whereClause = keepScanIds.length > 0 ? `WHERE scanId NOT IN (${placeholders})` : "";
|
|
3855
|
+
const result = this.db.prepare(`DELETE FROM pointers ${whereClause}`).run(...keepScanIds);
|
|
3856
|
+
return result.changes;
|
|
3857
|
+
});
|
|
3858
|
+
}
|
|
3859
|
+
listAllScanIds() {
|
|
3860
|
+
const rows = this.db.prepare("SELECT DISTINCT scanId FROM pointers").all();
|
|
3861
|
+
return rows.map((r) => r.scanId);
|
|
3862
|
+
}
|
|
3863
|
+
countPointers() {
|
|
3864
|
+
const row = this.db.prepare("SELECT COUNT(*) as cnt FROM pointers").get();
|
|
3865
|
+
return row.cnt;
|
|
3866
|
+
}
|
|
3867
|
+
close() {
|
|
3868
|
+
this.db.close();
|
|
3869
|
+
instance = null;
|
|
3870
|
+
}
|
|
3871
|
+
};
|
|
3872
|
+
|
|
3498
3873
|
// dist/export/ExportOrchestrator.js
|
|
3874
|
+
import { v4 as uuidv4 } from "uuid";
|
|
3499
3875
|
var ExportOrchestrator = class {
|
|
3500
3876
|
projectRoot;
|
|
3501
3877
|
constructor(projectRoot) {
|
|
3502
3878
|
this.projectRoot = projectRoot;
|
|
3503
3879
|
}
|
|
3504
|
-
async writeBundle(dossier, options = {}, store, graph) {
|
|
3880
|
+
async writeBundle(dossier, options = {}, store, graph, scanId) {
|
|
3505
3881
|
const finalStore = store || await readAnalysis(this.projectRoot);
|
|
3506
3882
|
if (!finalStore) {
|
|
3507
3883
|
throw new Error("Analysis store not found. Scan the project first.");
|
|
3508
3884
|
}
|
|
3885
|
+
const bindings = await readActionBindings(this.projectRoot);
|
|
3509
3886
|
for (const p of dossier.pillars) {
|
|
3510
3887
|
p.decisions = p.decisions.filter((c) => !(c.severity === 1 && c.category === "Convention"));
|
|
3511
3888
|
p.cardCount = p.decisions.length;
|
|
@@ -3528,10 +3905,87 @@ var ExportOrchestrator = class {
|
|
|
3528
3905
|
artifacts.push(...await new HtmlRenderer().render(viewModel, finalStore));
|
|
3529
3906
|
}
|
|
3530
3907
|
if (formats.includes("markdown")) {
|
|
3531
|
-
artifacts.push(...await new AgentMarkdownRenderer(options.budget).render(viewModel, finalStore));
|
|
3908
|
+
artifacts.push(...await new AgentMarkdownRenderer(options.budget, bindings).render(viewModel, finalStore));
|
|
3532
3909
|
}
|
|
3533
3910
|
const writer = new ArtifactBundleWriter(this.projectRoot);
|
|
3534
3911
|
await writer.writeBundle(artifacts);
|
|
3912
|
+
const effectiveScanId = scanId ?? `scan_${Date.now()}`;
|
|
3913
|
+
const blobStore = new BlobStore(this.projectRoot);
|
|
3914
|
+
const pointerStore = PointerStore.open(this.projectRoot);
|
|
3915
|
+
const now = Date.now();
|
|
3916
|
+
const manifestEntries = [];
|
|
3917
|
+
for (const artifact of artifacts) {
|
|
3918
|
+
const content = typeof artifact.content === "string" ? Buffer.from(artifact.content, "utf8") : artifact.content;
|
|
3919
|
+
const { contentHash, blobPath } = await blobStore.writeAtomic(content);
|
|
3920
|
+
const pointerId = `ptr_${uuidv4().replace(/-/g, "").slice(0, 16)}`;
|
|
3921
|
+
await pointerStore.insertPointer({
|
|
3922
|
+
pointerId,
|
|
3923
|
+
scanId: effectiveScanId,
|
|
3924
|
+
artifactName: artifact.type,
|
|
3925
|
+
contentHash,
|
|
3926
|
+
blobPath,
|
|
3927
|
+
schemaVersion: "1.0.0",
|
|
3928
|
+
createdAt: now,
|
|
3929
|
+
expiresAt: null
|
|
3930
|
+
});
|
|
3931
|
+
const entry = {
|
|
3932
|
+
name: artifact.type,
|
|
3933
|
+
pointer: pointerId,
|
|
3934
|
+
contentHash,
|
|
3935
|
+
sizeBytes: content.length
|
|
3936
|
+
};
|
|
3937
|
+
if (artifact.type === "analysis") {
|
|
3938
|
+
entry.hydrators = ["get_project_summary", "get_start_here"];
|
|
3939
|
+
const analysisIndex = {
|
|
3940
|
+
schemaVersion: "1.0.0",
|
|
3941
|
+
scanId: effectiveScanId,
|
|
3942
|
+
startHere: dossier.map.topGravity.slice(0, 12),
|
|
3943
|
+
topHeat: dossier.map.topHeat.slice(0, 12),
|
|
3944
|
+
pillarSummary: dossier.map.pillars.map((p) => ({
|
|
3945
|
+
name: p.name,
|
|
3946
|
+
fileCount: p.memberFiles?.length ?? 0
|
|
3947
|
+
})),
|
|
3948
|
+
totalFiles: Object.keys(finalStore.files).length,
|
|
3949
|
+
realSourceFiles: Object.values(finalStore.files).filter((f) => f.isRealSource).length
|
|
3950
|
+
};
|
|
3951
|
+
const indexContent = Buffer.from(JSON.stringify(analysisIndex, null, 2), "utf8");
|
|
3952
|
+
const indexWrite = await blobStore.writeAtomic(indexContent);
|
|
3953
|
+
const indexPointerId = `ptr_${uuidv4().replace(/-/g, "").slice(0, 16)}`;
|
|
3954
|
+
await pointerStore.insertPointer({
|
|
3955
|
+
pointerId: indexPointerId,
|
|
3956
|
+
scanId: effectiveScanId,
|
|
3957
|
+
artifactName: "analysis.index",
|
|
3958
|
+
contentHash: indexWrite.contentHash,
|
|
3959
|
+
blobPath: indexWrite.blobPath,
|
|
3960
|
+
schemaVersion: "1.0.0",
|
|
3961
|
+
createdAt: now,
|
|
3962
|
+
expiresAt: null
|
|
3963
|
+
});
|
|
3964
|
+
entry.indexes = { startHere: indexPointerId };
|
|
3965
|
+
}
|
|
3966
|
+
manifestEntries.push(entry);
|
|
3967
|
+
}
|
|
3968
|
+
const manifest = {
|
|
3969
|
+
schemaVersion: "2.0.0",
|
|
3970
|
+
scanId: effectiveScanId,
|
|
3971
|
+
generatedAt: new Date(now).toISOString(),
|
|
3972
|
+
projectRoot: this.projectRoot,
|
|
3973
|
+
artifacts: manifestEntries
|
|
3974
|
+
};
|
|
3975
|
+
const manifestContent = Buffer.from(JSON.stringify(manifest, null, 2), "utf8");
|
|
3976
|
+
const manifestWrite = await blobStore.writeAtomic(manifestContent);
|
|
3977
|
+
const manifestPointerId = `ptr_manifest_${effectiveScanId}`;
|
|
3978
|
+
await pointerStore.insertPointer({
|
|
3979
|
+
pointerId: manifestPointerId,
|
|
3980
|
+
scanId: effectiveScanId,
|
|
3981
|
+
artifactName: "artifact_manifest",
|
|
3982
|
+
contentHash: manifestWrite.contentHash,
|
|
3983
|
+
blobPath: manifestWrite.blobPath,
|
|
3984
|
+
schemaVersion: "2.0.0",
|
|
3985
|
+
createdAt: now,
|
|
3986
|
+
expiresAt: null
|
|
3987
|
+
});
|
|
3988
|
+
return { scanId: effectiveScanId, manifestPointer: manifestPointerId };
|
|
3535
3989
|
}
|
|
3536
3990
|
buildViewModel(dossier, store) {
|
|
3537
3991
|
const recommendations = {};
|
|
@@ -3558,10 +4012,16 @@ var ExportOrchestrator = class {
|
|
|
3558
4012
|
|
|
3559
4013
|
// dist/export/Watcher.js
|
|
3560
4014
|
import chokidar from "chokidar";
|
|
3561
|
-
import { createHash as
|
|
3562
|
-
import { readFile as
|
|
3563
|
-
import { join as
|
|
3564
|
-
|
|
4015
|
+
import { createHash as createHash4 } from "crypto";
|
|
4016
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
4017
|
+
import { join as join14 } from "path";
|
|
4018
|
+
var activeWatchers = /* @__PURE__ */ new Map();
|
|
4019
|
+
async function startWatcher(projectRoot, watchedPaths) {
|
|
4020
|
+
const existing = activeWatchers.get(projectRoot);
|
|
4021
|
+
if (existing) {
|
|
4022
|
+
await existing.close();
|
|
4023
|
+
activeWatchers.delete(projectRoot);
|
|
4024
|
+
}
|
|
3565
4025
|
const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
|
|
3566
4026
|
ignoreInitial: true,
|
|
3567
4027
|
ignored: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.vibe-splainer/**"],
|
|
@@ -3572,14 +4032,14 @@ function startWatcher(projectRoot, watchedPaths) {
|
|
|
3572
4032
|
const dossier = await readDossier(projectRoot);
|
|
3573
4033
|
if (!dossier)
|
|
3574
4034
|
return;
|
|
3575
|
-
const content = await
|
|
3576
|
-
const newHash =
|
|
4035
|
+
const content = await readFile10(filepath, "utf8");
|
|
4036
|
+
const newHash = createHash4("sha256").update(content).digest("hex");
|
|
3577
4037
|
let mutated = false;
|
|
3578
4038
|
for (const pillar of dossier.pillars) {
|
|
3579
4039
|
for (const card of pillar.decisions) {
|
|
3580
4040
|
if (!card.primaryFile)
|
|
3581
4041
|
continue;
|
|
3582
|
-
const absMatch = filepath ===
|
|
4042
|
+
const absMatch = filepath === join14(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
|
|
3583
4043
|
if (absMatch && card.lastScannedHash !== newHash) {
|
|
3584
4044
|
card.status = "stale";
|
|
3585
4045
|
const rel = card.primaryFile;
|
|
@@ -3598,6 +4058,7 @@ function startWatcher(projectRoot, watchedPaths) {
|
|
|
3598
4058
|
console.error("[vibe-splain] Watcher error:", err);
|
|
3599
4059
|
}
|
|
3600
4060
|
});
|
|
4061
|
+
activeWatchers.set(projectRoot, watcher);
|
|
3601
4062
|
console.error("[vibe-splain] File watcher started");
|
|
3602
4063
|
}
|
|
3603
4064
|
|
|
@@ -3646,13 +4107,14 @@ async function handleScanProject(args, options = {}) {
|
|
|
3646
4107
|
dossier.pillars.push({ name: def.name, cardCount: 0, decisions: [] });
|
|
3647
4108
|
}
|
|
3648
4109
|
}
|
|
4110
|
+
const scanId = `scan_${Date.now()}`;
|
|
3649
4111
|
const orchestrator = new ExportOrchestrator(projectRoot);
|
|
3650
|
-
await orchestrator.writeBundle(dossier, {
|
|
4112
|
+
const { manifestPointer } = await orchestrator.writeBundle(dossier, {
|
|
3651
4113
|
format: options.format,
|
|
3652
4114
|
budget: options.budget ? parseInt(options.budget, 10) : void 0,
|
|
3653
4115
|
scope: options.scope
|
|
3654
|
-
}, result.store, result.graph);
|
|
3655
|
-
startWatcher(projectRoot, result.files.map((f) => f.path));
|
|
4116
|
+
}, result.store, result.graph, scanId);
|
|
4117
|
+
await startWatcher(projectRoot, result.files.map((f) => f.path));
|
|
3656
4118
|
console.error(`[vibe-splain] Scan complete. ${result.totalFilesScanned} files, ${result.realSourceCount} real-source, ${result.wildCandidates.length} wild candidates.`);
|
|
3657
4119
|
const validation = result.validation ?? { passed: true, errors: 0, warnings: 0, reportPath: ".vibe-splainer/validation_report.json" };
|
|
3658
4120
|
let statusMsg = "Scan complete.";
|
|
@@ -3662,6 +4124,8 @@ async function handleScanProject(args, options = {}) {
|
|
|
3662
4124
|
return {
|
|
3663
4125
|
ok: true,
|
|
3664
4126
|
message: statusMsg,
|
|
4127
|
+
scanId,
|
|
4128
|
+
manifestPointer,
|
|
3665
4129
|
validation: {
|
|
3666
4130
|
passed: validation.passed,
|
|
3667
4131
|
errors: validation.errors,
|
|
@@ -3775,8 +4239,8 @@ async function handleSetProjectBrief(args, options = {}) {
|
|
|
3775
4239
|
}
|
|
3776
4240
|
|
|
3777
4241
|
// dist/mcp/tools/get_file_context.js
|
|
3778
|
-
import { readFile as
|
|
3779
|
-
import { join as
|
|
4242
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
4243
|
+
import { join as join15, relative as relative4, isAbsolute } from "path";
|
|
3780
4244
|
var getFileContextTool = {
|
|
3781
4245
|
name: "get_file_context",
|
|
3782
4246
|
description: "Returns PRE-EXTRACTED evidence for a file so you do not have to read the whole thing and paraphrase its header comment. Returns: gravity/heat scores + signals, importedBy (named fan-in \u2014 use this for blastRadius), hotSpans (the gnarliest function bodies, comment-stripped, each with a reason), smellSpans (located tech debt with \xB13 lines of context), and signature (the exported API surface). Base your evidence on hotSpans/smellSpans \u2014 NEVER on header comments. Pass { full: true } only if you truly need the raw source.",
|
|
@@ -3796,7 +4260,7 @@ async function handleGetFileContext(args) {
|
|
|
3796
4260
|
const full = args.full === true;
|
|
3797
4261
|
if (!projectRoot || !filePath)
|
|
3798
4262
|
throw new Error("projectRoot and filePath are required");
|
|
3799
|
-
const fullPath = isAbsolute(filePath) ? filePath :
|
|
4263
|
+
const fullPath = isAbsolute(filePath) ? filePath : join15(projectRoot, filePath);
|
|
3800
4264
|
const relPath = relative4(projectRoot, fullPath);
|
|
3801
4265
|
const evidence = await getFileAnalysis(fullPath);
|
|
3802
4266
|
if (!evidence) {
|
|
@@ -3821,16 +4285,16 @@ async function handleGetFileContext(args) {
|
|
|
3821
4285
|
smellSpans: evidence.smellSpans
|
|
3822
4286
|
};
|
|
3823
4287
|
if (full) {
|
|
3824
|
-
result.source = await
|
|
4288
|
+
result.source = await readFile11(fullPath, "utf8");
|
|
3825
4289
|
}
|
|
3826
4290
|
return result;
|
|
3827
4291
|
}
|
|
3828
4292
|
|
|
3829
4293
|
// dist/mcp/tools/write_decision_card.js
|
|
3830
|
-
import { v4 as
|
|
3831
|
-
import { createHash as
|
|
3832
|
-
import { readFile as
|
|
3833
|
-
import { join as
|
|
4294
|
+
import { v4 as uuidv42 } from "uuid";
|
|
4295
|
+
import { createHash as createHash5 } from "crypto";
|
|
4296
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
4297
|
+
import { join as join16 } from "path";
|
|
3834
4298
|
var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
|
|
3835
4299
|
function normalizeSnippet(s) {
|
|
3836
4300
|
let out = (s ?? "").replace(/\r\n/g, "\n");
|
|
@@ -3927,12 +4391,12 @@ async function handleWriteDecisionCard(args, options = {}) {
|
|
|
3927
4391
|
const heat = persisted ? Math.round(persisted.heat) : void 0;
|
|
3928
4392
|
let primaryContent = "";
|
|
3929
4393
|
try {
|
|
3930
|
-
primaryContent = await
|
|
4394
|
+
primaryContent = await readFile12(join16(projectRoot, primaryFile), "utf8");
|
|
3931
4395
|
} catch {
|
|
3932
4396
|
}
|
|
3933
|
-
const hash =
|
|
4397
|
+
const hash = createHash5("sha256").update(primaryContent).digest("hex");
|
|
3934
4398
|
const card = {
|
|
3935
|
-
id:
|
|
4399
|
+
id: uuidv42(),
|
|
3936
4400
|
pillar,
|
|
3937
4401
|
title,
|
|
3938
4402
|
thesis,
|
|
@@ -4206,6 +4670,856 @@ async function handleGetCallChain(args) {
|
|
|
4206
4670
|
}
|
|
4207
4671
|
}
|
|
4208
4672
|
|
|
4673
|
+
// dist/mcp/tools/get_file_skeleton.js
|
|
4674
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
4675
|
+
import { join as join17 } from "path";
|
|
4676
|
+
|
|
4677
|
+
// dist/mcp/SessionScope.js
|
|
4678
|
+
import { minimatch } from "minimatch";
|
|
4679
|
+
var ScopeViolation = class extends Error {
|
|
4680
|
+
path;
|
|
4681
|
+
workOrderId;
|
|
4682
|
+
constructor(path, workOrderId, reason) {
|
|
4683
|
+
super(`ScopeViolation [${workOrderId}]: ${reason} \u2014 path: ${path}`);
|
|
4684
|
+
this.path = path;
|
|
4685
|
+
this.workOrderId = workOrderId;
|
|
4686
|
+
this.name = "ScopeViolation";
|
|
4687
|
+
}
|
|
4688
|
+
};
|
|
4689
|
+
var activeScope = null;
|
|
4690
|
+
var SessionScope = {
|
|
4691
|
+
set(policy) {
|
|
4692
|
+
activeScope = policy;
|
|
4693
|
+
},
|
|
4694
|
+
clear() {
|
|
4695
|
+
activeScope = null;
|
|
4696
|
+
},
|
|
4697
|
+
get() {
|
|
4698
|
+
return activeScope;
|
|
4699
|
+
},
|
|
4700
|
+
/**
|
|
4701
|
+
* Enforce scope for a file path.
|
|
4702
|
+
* Throws ScopeViolation if:
|
|
4703
|
+
* - a scope is active AND the path is not allowed
|
|
4704
|
+
* If no scope is active, all paths are permitted.
|
|
4705
|
+
*/
|
|
4706
|
+
enforce(filePath) {
|
|
4707
|
+
if (!activeScope)
|
|
4708
|
+
return;
|
|
4709
|
+
const { workOrderId, allowedFiles, allowedGlobs, deniedGlobs } = activeScope;
|
|
4710
|
+
const inAllowedFiles = allowedFiles.some((f) => filePath === f || filePath.endsWith("/" + f) || filePath.endsWith(f));
|
|
4711
|
+
const inAllowedGlobs = allowedGlobs.some((g) => minimatch(filePath, g, { matchBase: true }));
|
|
4712
|
+
if (!inAllowedFiles && !inAllowedGlobs) {
|
|
4713
|
+
throw new ScopeViolation(filePath, workOrderId, "path not in allowedFiles or allowedGlobs");
|
|
4714
|
+
}
|
|
4715
|
+
const isDenied = deniedGlobs.some((g) => minimatch(filePath, g, { matchBase: true }));
|
|
4716
|
+
if (isDenied) {
|
|
4717
|
+
throw new ScopeViolation(filePath, workOrderId, "path matches deniedGlobs");
|
|
4718
|
+
}
|
|
4719
|
+
},
|
|
4720
|
+
fromWorkOrderRow(row) {
|
|
4721
|
+
return {
|
|
4722
|
+
workOrderId: row.workOrderId,
|
|
4723
|
+
allowedFiles: JSON.parse(row.allowedFiles),
|
|
4724
|
+
allowedGlobs: JSON.parse(row.allowedGlobs),
|
|
4725
|
+
deniedGlobs: JSON.parse(row.deniedGlobs),
|
|
4726
|
+
requiredProof: JSON.parse(row.requiredProof)
|
|
4727
|
+
};
|
|
4728
|
+
}
|
|
4729
|
+
};
|
|
4730
|
+
|
|
4731
|
+
// dist/mcp/BudgetGuard.js
|
|
4732
|
+
import { v4 as uuidv43 } from "uuid";
|
|
4733
|
+
var BUDGET_CHARS = 8e3;
|
|
4734
|
+
async function applyBudgetGuard(projectRoot, scanId, artifactName, output) {
|
|
4735
|
+
const serialized = JSON.stringify(output, null, 2);
|
|
4736
|
+
if (serialized.length <= BUDGET_CHARS)
|
|
4737
|
+
return output;
|
|
4738
|
+
const blobStore = new BlobStore(projectRoot);
|
|
4739
|
+
const pointerStore = PointerStore.open(projectRoot);
|
|
4740
|
+
const { contentHash, blobPath } = await blobStore.writeAtomic(serialized);
|
|
4741
|
+
const pointerId = `ptr_${uuidv43().replace(/-/g, "").slice(0, 16)}`;
|
|
4742
|
+
await pointerStore.insertPointer({
|
|
4743
|
+
pointerId,
|
|
4744
|
+
scanId,
|
|
4745
|
+
artifactName,
|
|
4746
|
+
contentHash,
|
|
4747
|
+
blobPath,
|
|
4748
|
+
schemaVersion: "1.0.0",
|
|
4749
|
+
createdAt: Date.now(),
|
|
4750
|
+
expiresAt: null
|
|
4751
|
+
});
|
|
4752
|
+
const result = {
|
|
4753
|
+
pointerId,
|
|
4754
|
+
contentHash,
|
|
4755
|
+
sizeBytes: serialized.length,
|
|
4756
|
+
summary: `Output exceeded context budget (${serialized.length} chars). Written to artifact blob.`,
|
|
4757
|
+
hydrators: ["get_evidence_slice", "get_start_here", "get_project_summary"]
|
|
4758
|
+
};
|
|
4759
|
+
return result;
|
|
4760
|
+
}
|
|
4761
|
+
async function hydratePointer(projectRoot, pointerId) {
|
|
4762
|
+
const pointerStore = PointerStore.open(projectRoot);
|
|
4763
|
+
const row = pointerStore.getPointer(pointerId);
|
|
4764
|
+
if (!row) {
|
|
4765
|
+
throw new Error(`ArtifactNotFound: pointer ${pointerId} does not exist`);
|
|
4766
|
+
}
|
|
4767
|
+
if (row.expiresAt !== null && row.expiresAt < Date.now()) {
|
|
4768
|
+
throw new Error(`ArtifactCollectedError: pointer ${pointerId} has expired`);
|
|
4769
|
+
}
|
|
4770
|
+
const SUPPORTED_VERSIONS = ["1.0.0", "2.0.0"];
|
|
4771
|
+
if (!SUPPORTED_VERSIONS.includes(row.schemaVersion)) {
|
|
4772
|
+
throw new Error(`UnsupportedSchema: pointer ${pointerId} has schema version ${row.schemaVersion}`);
|
|
4773
|
+
}
|
|
4774
|
+
const blobStore = new BlobStore(projectRoot);
|
|
4775
|
+
const content = await blobStore.readBlob(row.blobPath);
|
|
4776
|
+
const valid = await blobStore.verifyIntegrity(row.blobPath, row.contentHash);
|
|
4777
|
+
if (!valid) {
|
|
4778
|
+
throw new Error(`IntegrityError: blob for pointer ${pointerId} failed hash verification`);
|
|
4779
|
+
}
|
|
4780
|
+
return { content, row };
|
|
4781
|
+
}
|
|
4782
|
+
|
|
4783
|
+
// dist/mcp/tools/get_file_skeleton.js
|
|
4784
|
+
import { v4 as uuidv44 } from "uuid";
|
|
4785
|
+
var getFileSkeletonTool = {
|
|
4786
|
+
name: "get_file_skeleton",
|
|
4787
|
+
description: "Returns a content-addressed skeleton view of a source file (function signatures, class names, exported symbols). Enforces active workOrder scope. Results are content-addressed \u2014 repeated calls on unchanged files return cached pointers.",
|
|
4788
|
+
inputSchema: {
|
|
4789
|
+
type: "object",
|
|
4790
|
+
properties: {
|
|
4791
|
+
projectRoot: { type: "string", description: "Absolute project root" },
|
|
4792
|
+
filePath: { type: "string", description: "Path relative to projectRoot" },
|
|
4793
|
+
scanId: { type: "string", description: "Current scan ID for pointer registration" }
|
|
4794
|
+
},
|
|
4795
|
+
required: ["projectRoot", "filePath", "scanId"]
|
|
4796
|
+
}
|
|
4797
|
+
};
|
|
4798
|
+
async function handleGetFileSkeleton(args) {
|
|
4799
|
+
const projectRoot = args.projectRoot;
|
|
4800
|
+
const filePath = args.filePath;
|
|
4801
|
+
const scanId = args.scanId;
|
|
4802
|
+
if (!projectRoot || !filePath || !scanId) {
|
|
4803
|
+
throw new Error("projectRoot, filePath, and scanId are required");
|
|
4804
|
+
}
|
|
4805
|
+
try {
|
|
4806
|
+
SessionScope.enforce(filePath);
|
|
4807
|
+
} catch (e) {
|
|
4808
|
+
if (e instanceof ScopeViolation)
|
|
4809
|
+
throw e;
|
|
4810
|
+
throw e;
|
|
4811
|
+
}
|
|
4812
|
+
const absolutePath = filePath.startsWith("/") ? filePath : join17(projectRoot, filePath);
|
|
4813
|
+
let currentHash;
|
|
4814
|
+
try {
|
|
4815
|
+
currentHash = await hashFile(absolutePath);
|
|
4816
|
+
} catch {
|
|
4817
|
+
throw new Error(`FileNotFound: cannot read ${filePath}`);
|
|
4818
|
+
}
|
|
4819
|
+
const parserVersion = "1.0.0";
|
|
4820
|
+
const cacheKey = `skeleton:${currentHash}:${parserVersion}`;
|
|
4821
|
+
const pointerStore = PointerStore.open(projectRoot);
|
|
4822
|
+
const existingPointers = pointerStore.listPointersByScan(scanId);
|
|
4823
|
+
const cached = existingPointers.find((p) => p.artifactName === "file_skeleton" && p.contentHash === cacheKey);
|
|
4824
|
+
if (cached) {
|
|
4825
|
+
return {
|
|
4826
|
+
pointerId: cached.pointerId,
|
|
4827
|
+
contentHash: currentHash,
|
|
4828
|
+
cached: true,
|
|
4829
|
+
filePath
|
|
4830
|
+
};
|
|
4831
|
+
}
|
|
4832
|
+
const source = await readFile13(absolutePath, "utf8");
|
|
4833
|
+
const skeleton = extractSkeleton(source, filePath);
|
|
4834
|
+
const skeletonPayload = {
|
|
4835
|
+
filePath,
|
|
4836
|
+
sourceHash: currentHash,
|
|
4837
|
+
parserVersion,
|
|
4838
|
+
skeleton
|
|
4839
|
+
};
|
|
4840
|
+
const blobStore = new BlobStore(projectRoot);
|
|
4841
|
+
const serialized = JSON.stringify(skeletonPayload, null, 2);
|
|
4842
|
+
const { contentHash: skeletonHash, blobPath } = await blobStore.writeAtomic(serialized);
|
|
4843
|
+
const pointerId = `ptr_skel_${uuidv44().replace(/-/g, "").slice(0, 12)}`;
|
|
4844
|
+
await pointerStore.insertPointer({
|
|
4845
|
+
pointerId,
|
|
4846
|
+
scanId,
|
|
4847
|
+
artifactName: "file_skeleton",
|
|
4848
|
+
contentHash: cacheKey,
|
|
4849
|
+
// cache key encodes source hash + parser version
|
|
4850
|
+
blobPath,
|
|
4851
|
+
schemaVersion: "1.0.0",
|
|
4852
|
+
createdAt: Date.now(),
|
|
4853
|
+
expiresAt: null
|
|
4854
|
+
});
|
|
4855
|
+
const result = {
|
|
4856
|
+
filePath,
|
|
4857
|
+
sourceHash: currentHash,
|
|
4858
|
+
pointerId,
|
|
4859
|
+
skeleton
|
|
4860
|
+
};
|
|
4861
|
+
return await applyBudgetGuard(projectRoot, scanId, "file_skeleton", result);
|
|
4862
|
+
}
|
|
4863
|
+
function extractSkeleton(source, filePath) {
|
|
4864
|
+
const lines = source.split("\n");
|
|
4865
|
+
const skeleton = [];
|
|
4866
|
+
const ext = filePath.split(".").pop() ?? "";
|
|
4867
|
+
const isTS = ["ts", "tsx"].includes(ext);
|
|
4868
|
+
const isJS = ["js", "jsx", "mjs", "cjs"].includes(ext);
|
|
4869
|
+
if (isTS || isJS) {
|
|
4870
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4871
|
+
const line = lines[i].trim();
|
|
4872
|
+
if (/^(export\s+)?(async\s+)?function\b/.test(line) || /^(export\s+)?(abstract\s+)?class\b/.test(line) || /^(export\s+)?interface\b/.test(line) || /^(export\s+)?type\s+\w+/.test(line) || /^(export\s+)?enum\b/.test(line) || /^(export\s+)?const\s+\w+\s*[:=(]/.test(line) || /^(export\s+)?let\s+\w+\s*[:=(]/.test(line) || /^(export\s+)?(default\s+)/.test(line) || /^\s*(public|private|protected|static|readonly|abstract)\s+/.test(line) || /^import\b/.test(line)) {
|
|
4873
|
+
skeleton.push(`L${i + 1}: ${lines[i]}`);
|
|
4874
|
+
}
|
|
4875
|
+
}
|
|
4876
|
+
} else {
|
|
4877
|
+
return lines.slice(0, 80).map((l, i) => `L${i + 1}: ${l}`);
|
|
4878
|
+
}
|
|
4879
|
+
return skeleton;
|
|
4880
|
+
}
|
|
4881
|
+
|
|
4882
|
+
// dist/mcp/tools/read_file.js
|
|
4883
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
4884
|
+
import { join as join18 } from "path";
|
|
4885
|
+
import { v4 as uuidv45 } from "uuid";
|
|
4886
|
+
var readFileTool = {
|
|
4887
|
+
name: "read_file",
|
|
4888
|
+
description: "Reads a file within the active workOrder scope. Enforces allowedFiles/allowedGlobs/deniedGlobs. Records content hash. Output is budgeted.",
|
|
4889
|
+
inputSchema: {
|
|
4890
|
+
type: "object",
|
|
4891
|
+
properties: {
|
|
4892
|
+
projectRoot: { type: "string", description: "Absolute project root" },
|
|
4893
|
+
filePath: { type: "string", description: "Path relative to projectRoot" },
|
|
4894
|
+
scanId: { type: "string", description: "Current scan ID for pointer registration" },
|
|
4895
|
+
startLine: { type: "number", description: "Optional: 1-based start line to return a slice" },
|
|
4896
|
+
endLine: { type: "number", description: "Optional: 1-based end line (inclusive). Capped at startLine+500." }
|
|
4897
|
+
},
|
|
4898
|
+
required: ["projectRoot", "filePath", "scanId"]
|
|
4899
|
+
}
|
|
4900
|
+
};
|
|
4901
|
+
async function handleReadFile(args) {
|
|
4902
|
+
const projectRoot = args.projectRoot;
|
|
4903
|
+
const filePath = args.filePath;
|
|
4904
|
+
const scanId = args.scanId;
|
|
4905
|
+
const startLine = args.startLine !== void 0 ? Number(args.startLine) : void 0;
|
|
4906
|
+
const endLine = args.endLine !== void 0 ? Math.min(Number(args.endLine), (startLine ?? 1) + 500) : void 0;
|
|
4907
|
+
if (!projectRoot || !filePath || !scanId) {
|
|
4908
|
+
throw new Error("projectRoot, filePath, and scanId are required");
|
|
4909
|
+
}
|
|
4910
|
+
try {
|
|
4911
|
+
SessionScope.enforce(filePath);
|
|
4912
|
+
} catch (e) {
|
|
4913
|
+
if (e instanceof ScopeViolation)
|
|
4914
|
+
throw e;
|
|
4915
|
+
throw e;
|
|
4916
|
+
}
|
|
4917
|
+
const absolutePath = filePath.startsWith("/") ? filePath : join18(projectRoot, filePath);
|
|
4918
|
+
let content;
|
|
4919
|
+
try {
|
|
4920
|
+
content = await readFile14(absolutePath, "utf8");
|
|
4921
|
+
} catch {
|
|
4922
|
+
throw new Error(`FileNotFound: cannot read ${filePath}`);
|
|
4923
|
+
}
|
|
4924
|
+
const contentHash = await hashFile(absolutePath);
|
|
4925
|
+
let output = content;
|
|
4926
|
+
let sliceInfo;
|
|
4927
|
+
if (startLine !== void 0) {
|
|
4928
|
+
const lines = content.split("\n");
|
|
4929
|
+
const end = endLine ?? lines.length;
|
|
4930
|
+
output = lines.slice(startLine - 1, end).join("\n");
|
|
4931
|
+
sliceInfo = { startLine, endLine: end, totalLines: lines.length };
|
|
4932
|
+
}
|
|
4933
|
+
const blobStore = new BlobStore(projectRoot);
|
|
4934
|
+
const pointerStore = PointerStore.open(projectRoot);
|
|
4935
|
+
const { blobPath } = await blobStore.writeAtomic(content);
|
|
4936
|
+
const pointerId = `ptr_file_${uuidv45().replace(/-/g, "").slice(0, 12)}`;
|
|
4937
|
+
await pointerStore.insertPointer({
|
|
4938
|
+
pointerId,
|
|
4939
|
+
scanId,
|
|
4940
|
+
artifactName: "file_read",
|
|
4941
|
+
contentHash,
|
|
4942
|
+
blobPath,
|
|
4943
|
+
schemaVersion: "1.0.0",
|
|
4944
|
+
createdAt: Date.now(),
|
|
4945
|
+
expiresAt: null
|
|
4946
|
+
});
|
|
4947
|
+
const result = {
|
|
4948
|
+
filePath,
|
|
4949
|
+
contentHash,
|
|
4950
|
+
pointerId,
|
|
4951
|
+
content: output
|
|
4952
|
+
};
|
|
4953
|
+
if (sliceInfo)
|
|
4954
|
+
result.slice = sliceInfo;
|
|
4955
|
+
return await applyBudgetGuard(projectRoot, scanId, "file_read", result);
|
|
4956
|
+
}
|
|
4957
|
+
|
|
4958
|
+
// dist/mcp/tools/apply_patch.js
|
|
4959
|
+
import { writeFile as writeFile9, rename as rename3, mkdir as mkdir8 } from "fs/promises";
|
|
4960
|
+
import { join as join19, dirname as dirname4 } from "path";
|
|
4961
|
+
import { v4 as uuidv46 } from "uuid";
|
|
4962
|
+
var StalePatchError = class extends Error {
|
|
4963
|
+
filePath;
|
|
4964
|
+
expectedHash;
|
|
4965
|
+
actualHash;
|
|
4966
|
+
constructor(filePath, expectedHash, actualHash) {
|
|
4967
|
+
super(`StalePatchError: ${filePath} hash mismatch \u2014 expected ${expectedHash}, got ${actualHash}. File was modified since the expectedPrePatchHash was computed. Re-read the file and regenerate the patch.`);
|
|
4968
|
+
this.filePath = filePath;
|
|
4969
|
+
this.expectedHash = expectedHash;
|
|
4970
|
+
this.actualHash = actualHash;
|
|
4971
|
+
this.name = "StalePatchError";
|
|
4972
|
+
}
|
|
4973
|
+
};
|
|
4974
|
+
var applyPatchTool = {
|
|
4975
|
+
name: "apply_patch",
|
|
4976
|
+
description: "Applies a text patch to a file within the active workOrder scope. Requires expectedPrePatchHash to prevent stale-patch corruption. Records pre- and post-patch hashes.",
|
|
4977
|
+
inputSchema: {
|
|
4978
|
+
type: "object",
|
|
4979
|
+
properties: {
|
|
4980
|
+
projectRoot: { type: "string", description: "Absolute project root" },
|
|
4981
|
+
filePath: { type: "string", description: "Path relative to projectRoot" },
|
|
4982
|
+
newContent: { type: "string", description: "Full new content of the file after the patch" },
|
|
4983
|
+
expectedPrePatchHash: {
|
|
4984
|
+
type: "string",
|
|
4985
|
+
description: "sha256:<hex> hash of the file BEFORE patching. Obtain via hashFile or the sourceHash from get_file_skeleton."
|
|
4986
|
+
},
|
|
4987
|
+
scanId: { type: "string", description: "Current scan ID for pointer registration" }
|
|
4988
|
+
},
|
|
4989
|
+
required: ["projectRoot", "filePath", "newContent", "expectedPrePatchHash", "scanId"]
|
|
4990
|
+
}
|
|
4991
|
+
};
|
|
4992
|
+
async function handleApplyPatch(args) {
|
|
4993
|
+
const projectRoot = args.projectRoot;
|
|
4994
|
+
const filePath = args.filePath;
|
|
4995
|
+
const newContent = args.newContent;
|
|
4996
|
+
const expectedPrePatchHash = args.expectedPrePatchHash;
|
|
4997
|
+
const scanId = args.scanId;
|
|
4998
|
+
if (!projectRoot || !filePath || !newContent || !expectedPrePatchHash || !scanId) {
|
|
4999
|
+
throw new Error("projectRoot, filePath, newContent, expectedPrePatchHash, and scanId are all required");
|
|
5000
|
+
}
|
|
5001
|
+
try {
|
|
5002
|
+
SessionScope.enforce(filePath);
|
|
5003
|
+
} catch (e) {
|
|
5004
|
+
if (e instanceof ScopeViolation)
|
|
5005
|
+
throw e;
|
|
5006
|
+
throw e;
|
|
5007
|
+
}
|
|
5008
|
+
const absolutePath = filePath.startsWith("/") ? filePath : join19(projectRoot, filePath);
|
|
5009
|
+
let actualPreHash;
|
|
5010
|
+
try {
|
|
5011
|
+
actualPreHash = await hashFile(absolutePath);
|
|
5012
|
+
} catch {
|
|
5013
|
+
actualPreHash = "sha256:new";
|
|
5014
|
+
}
|
|
5015
|
+
if (actualPreHash !== expectedPrePatchHash) {
|
|
5016
|
+
throw new StalePatchError(filePath, expectedPrePatchHash, actualPreHash);
|
|
5017
|
+
}
|
|
5018
|
+
const dir = dirname4(absolutePath);
|
|
5019
|
+
await mkdir8(dir, { recursive: true });
|
|
5020
|
+
const tmpPath = absolutePath + `.tmp_${Date.now()}`;
|
|
5021
|
+
await writeFile9(tmpPath, newContent, "utf8");
|
|
5022
|
+
await rename3(tmpPath, absolutePath);
|
|
5023
|
+
const postPatchHash = await hashFile(absolutePath);
|
|
5024
|
+
const blobStore = new BlobStore(projectRoot);
|
|
5025
|
+
const pointerStore = PointerStore.open(projectRoot);
|
|
5026
|
+
const { blobPath } = await blobStore.writeAtomic(newContent);
|
|
5027
|
+
const pointerId = `ptr_patch_${uuidv46().replace(/-/g, "").slice(0, 12)}`;
|
|
5028
|
+
await pointerStore.insertPointer({
|
|
5029
|
+
pointerId,
|
|
5030
|
+
scanId,
|
|
5031
|
+
artifactName: "patch_record",
|
|
5032
|
+
contentHash: postPatchHash,
|
|
5033
|
+
blobPath,
|
|
5034
|
+
schemaVersion: "1.0.0",
|
|
5035
|
+
createdAt: Date.now(),
|
|
5036
|
+
expiresAt: null
|
|
5037
|
+
});
|
|
5038
|
+
const result = {
|
|
5039
|
+
ok: true,
|
|
5040
|
+
filePath,
|
|
5041
|
+
prePatchHash: actualPreHash,
|
|
5042
|
+
postPatchHash,
|
|
5043
|
+
pointerId,
|
|
5044
|
+
message: `Patch applied to ${filePath}`
|
|
5045
|
+
};
|
|
5046
|
+
return await applyBudgetGuard(projectRoot, scanId, "patch_record", result);
|
|
5047
|
+
}
|
|
5048
|
+
|
|
5049
|
+
// dist/mcp/tools/work_orders.js
|
|
5050
|
+
import { v4 as uuidv47 } from "uuid";
|
|
5051
|
+
var createWorkOrderTool = {
|
|
5052
|
+
name: "create_work_order",
|
|
5053
|
+
description: "Creates a new Work Order defining intent, allowed file scope, and required verifiable proof. Returns the workOrderId and a manifestPointer for use with spawn_worker.",
|
|
5054
|
+
inputSchema: {
|
|
5055
|
+
type: "object",
|
|
5056
|
+
properties: {
|
|
5057
|
+
projectRoot: { type: "string", description: "Absolute project root" },
|
|
5058
|
+
intent: { type: "string", description: "Plain-language description of what the worker should do" },
|
|
5059
|
+
allowedFiles: {
|
|
5060
|
+
type: "array",
|
|
5061
|
+
items: { type: "string" },
|
|
5062
|
+
description: "Explicit file paths (relative to projectRoot) the worker may read/write"
|
|
5063
|
+
},
|
|
5064
|
+
allowedGlobs: {
|
|
5065
|
+
type: "array",
|
|
5066
|
+
items: { type: "string" },
|
|
5067
|
+
description: "Glob patterns for allowed files"
|
|
5068
|
+
},
|
|
5069
|
+
deniedGlobs: {
|
|
5070
|
+
type: "array",
|
|
5071
|
+
items: { type: "string" },
|
|
5072
|
+
description: "Glob patterns that override allowedFiles/allowedGlobs"
|
|
5073
|
+
},
|
|
5074
|
+
requiredProof: {
|
|
5075
|
+
type: "array",
|
|
5076
|
+
items: {
|
|
5077
|
+
type: "object",
|
|
5078
|
+
properties: {
|
|
5079
|
+
proofId: { type: "string" },
|
|
5080
|
+
schemaName: { type: "string", description: "e.g. test_report.v1, patch_hash" },
|
|
5081
|
+
description: { type: "string" }
|
|
5082
|
+
},
|
|
5083
|
+
required: ["proofId", "schemaName", "description"]
|
|
5084
|
+
},
|
|
5085
|
+
description: "Machine-verifiable evidence the worker must provide"
|
|
5086
|
+
}
|
|
5087
|
+
},
|
|
5088
|
+
required: ["projectRoot", "intent", "allowedFiles"]
|
|
5089
|
+
}
|
|
5090
|
+
};
|
|
5091
|
+
async function handleCreateWorkOrder(args) {
|
|
5092
|
+
const projectRoot = args.projectRoot;
|
|
5093
|
+
const intent = args.intent;
|
|
5094
|
+
const allowedFiles = args.allowedFiles ?? [];
|
|
5095
|
+
const allowedGlobs = args.allowedGlobs ?? [];
|
|
5096
|
+
const deniedGlobs = args.deniedGlobs ?? [];
|
|
5097
|
+
const requiredProof = args.requiredProof ?? [];
|
|
5098
|
+
if (!projectRoot || !intent) {
|
|
5099
|
+
throw new Error("projectRoot and intent are required");
|
|
5100
|
+
}
|
|
5101
|
+
const workOrderId = `wo_${uuidv47().replace(/-/g, "").slice(0, 16)}`;
|
|
5102
|
+
const pointerStore = PointerStore.open(projectRoot);
|
|
5103
|
+
await pointerStore.insertWorkOrder({
|
|
5104
|
+
workOrderId,
|
|
5105
|
+
intent,
|
|
5106
|
+
allowedFiles: JSON.stringify(allowedFiles),
|
|
5107
|
+
allowedGlobs: JSON.stringify(allowedGlobs),
|
|
5108
|
+
deniedGlobs: JSON.stringify(deniedGlobs),
|
|
5109
|
+
requiredProof: JSON.stringify(requiredProof),
|
|
5110
|
+
status: "pending",
|
|
5111
|
+
createdAt: Date.now()
|
|
5112
|
+
});
|
|
5113
|
+
return {
|
|
5114
|
+
ok: true,
|
|
5115
|
+
workOrderId,
|
|
5116
|
+
intent,
|
|
5117
|
+
allowedFiles,
|
|
5118
|
+
allowedGlobs,
|
|
5119
|
+
deniedGlobs,
|
|
5120
|
+
requiredProof,
|
|
5121
|
+
nextStep: `Call spawn_worker with workOrderId "${workOrderId}" to generate a DelegationRequest`
|
|
5122
|
+
};
|
|
5123
|
+
}
|
|
5124
|
+
var spawnWorkerTool = {
|
|
5125
|
+
name: "spawn_worker",
|
|
5126
|
+
description: "Generates a DelegationRequest from a Work Order. The Client Orchestrator uses this object to spawn an isolated Worker session. The MCP server does NOT spawn any subprocess.",
|
|
5127
|
+
inputSchema: {
|
|
5128
|
+
type: "object",
|
|
5129
|
+
properties: {
|
|
5130
|
+
projectRoot: { type: "string", description: "Absolute project root" },
|
|
5131
|
+
workOrderId: { type: "string", description: "ID returned by create_work_order" }
|
|
5132
|
+
},
|
|
5133
|
+
required: ["projectRoot", "workOrderId"]
|
|
5134
|
+
}
|
|
5135
|
+
};
|
|
5136
|
+
async function handleSpawnWorker(args) {
|
|
5137
|
+
const projectRoot = args.projectRoot;
|
|
5138
|
+
const workOrderId = args.workOrderId;
|
|
5139
|
+
if (!projectRoot || !workOrderId) {
|
|
5140
|
+
throw new Error("projectRoot and workOrderId are required");
|
|
5141
|
+
}
|
|
5142
|
+
const pointerStore = PointerStore.open(projectRoot);
|
|
5143
|
+
const row = pointerStore.getWorkOrder(workOrderId);
|
|
5144
|
+
if (!row) {
|
|
5145
|
+
throw new Error(`WorkOrderNotFound: ${workOrderId}`);
|
|
5146
|
+
}
|
|
5147
|
+
if (row.status === "completed" || row.status === "failed" || row.status === "active") {
|
|
5148
|
+
throw new Error(`WorkOrderClosed: ${workOrderId} is already ${row.status}`);
|
|
5149
|
+
}
|
|
5150
|
+
await pointerStore.updateWorkOrderStatus(workOrderId, "active");
|
|
5151
|
+
const delegationRequest = {
|
|
5152
|
+
schemaVersion: "1.0.0",
|
|
5153
|
+
workOrderId: row.workOrderId,
|
|
5154
|
+
intent: row.intent,
|
|
5155
|
+
sessionScope: {
|
|
5156
|
+
allowedFiles: JSON.parse(row.allowedFiles),
|
|
5157
|
+
allowedGlobs: JSON.parse(row.allowedGlobs),
|
|
5158
|
+
deniedGlobs: JSON.parse(row.deniedGlobs)
|
|
5159
|
+
},
|
|
5160
|
+
requiredProof: JSON.parse(row.requiredProof),
|
|
5161
|
+
instructions: [
|
|
5162
|
+
`1. Call set_session_scope with workOrderId "${workOrderId}" before any file operations.`,
|
|
5163
|
+
"2. Only read/write files within the sessionScope.",
|
|
5164
|
+
"3. If you need a file outside scope, call yield_for_scope_expansion \u2014 do NOT proceed.",
|
|
5165
|
+
"4. On completion, call submit_receipt with proof for every requiredProof entry."
|
|
5166
|
+
]
|
|
5167
|
+
};
|
|
5168
|
+
return {
|
|
5169
|
+
ok: true,
|
|
5170
|
+
delegationRequest,
|
|
5171
|
+
note: "The Client Orchestrator must spawn the Worker session using this DelegationRequest. The MCP server does not spawn subprocesses."
|
|
5172
|
+
};
|
|
5173
|
+
}
|
|
5174
|
+
|
|
5175
|
+
// dist/mcp/tools/submit_receipt.js
|
|
5176
|
+
import { join as join20 } from "path";
|
|
5177
|
+
import { minimatch as minimatch2 } from "minimatch";
|
|
5178
|
+
import { v4 as uuidv48 } from "uuid";
|
|
5179
|
+
var submitReceiptTool = {
|
|
5180
|
+
name: "submit_receipt",
|
|
5181
|
+
description: "Worker submits a WorkerReceipt for a completed Work Order. The ProofValidator checks all 8 proof conditions. Returns accept/reject with detailed errors.",
|
|
5182
|
+
inputSchema: {
|
|
5183
|
+
type: "object",
|
|
5184
|
+
properties: {
|
|
5185
|
+
projectRoot: { type: "string", description: "Absolute project root" },
|
|
5186
|
+
receipt: {
|
|
5187
|
+
type: "object",
|
|
5188
|
+
description: "WorkerReceipt object",
|
|
5189
|
+
properties: {
|
|
5190
|
+
workOrderId: { type: "string" },
|
|
5191
|
+
status: { type: "string", enum: ["completed", "failed", "blocked"] },
|
|
5192
|
+
proofPointers: {
|
|
5193
|
+
type: "array",
|
|
5194
|
+
items: {
|
|
5195
|
+
type: "object",
|
|
5196
|
+
properties: {
|
|
5197
|
+
pointer: { type: "string" },
|
|
5198
|
+
schemaName: { type: "string" },
|
|
5199
|
+
contentHash: { type: "string" }
|
|
5200
|
+
},
|
|
5201
|
+
required: ["pointer", "schemaName", "contentHash"]
|
|
5202
|
+
}
|
|
5203
|
+
},
|
|
5204
|
+
changedFiles: {
|
|
5205
|
+
type: "array",
|
|
5206
|
+
items: {
|
|
5207
|
+
type: "object",
|
|
5208
|
+
properties: {
|
|
5209
|
+
path: { type: "string" },
|
|
5210
|
+
prePatchHash: { type: "string" },
|
|
5211
|
+
postPatchHash: { type: "string" }
|
|
5212
|
+
},
|
|
5213
|
+
required: ["path", "prePatchHash", "postPatchHash"]
|
|
5214
|
+
}
|
|
5215
|
+
},
|
|
5216
|
+
summary: { type: "string" }
|
|
5217
|
+
},
|
|
5218
|
+
required: ["workOrderId", "status", "proofPointers", "changedFiles", "summary"]
|
|
5219
|
+
}
|
|
5220
|
+
},
|
|
5221
|
+
required: ["projectRoot", "receipt"]
|
|
5222
|
+
}
|
|
5223
|
+
};
|
|
5224
|
+
async function handleSubmitReceipt(args) {
|
|
5225
|
+
const projectRoot = args.projectRoot;
|
|
5226
|
+
const receipt = args.receipt;
|
|
5227
|
+
if (!projectRoot || !receipt) {
|
|
5228
|
+
throw new Error("projectRoot and receipt are required");
|
|
5229
|
+
}
|
|
5230
|
+
const pointerStore = PointerStore.open(projectRoot);
|
|
5231
|
+
const workOrder = pointerStore.getWorkOrder(receipt.workOrderId);
|
|
5232
|
+
if (!workOrder) {
|
|
5233
|
+
throw new Error(`WorkOrderNotFound: ${receipt.workOrderId}`);
|
|
5234
|
+
}
|
|
5235
|
+
if (workOrder.status !== "active") {
|
|
5236
|
+
throw new Error(`WorkOrderNotActive: ${receipt.workOrderId} is "${workOrder.status}", expected "active"`);
|
|
5237
|
+
}
|
|
5238
|
+
const allowedFiles = JSON.parse(workOrder.allowedFiles);
|
|
5239
|
+
const allowedGlobs = JSON.parse(workOrder.allowedGlobs);
|
|
5240
|
+
const requiredProof = JSON.parse(workOrder.requiredProof);
|
|
5241
|
+
const blobDir = join20(projectRoot, ".vibe-splainer", "blobs");
|
|
5242
|
+
const isAllowedFile = (filePath) => {
|
|
5243
|
+
const inExplicit = allowedFiles.some((f) => filePath === f || filePath.endsWith("/" + f) || filePath.endsWith(f));
|
|
5244
|
+
const inGlobs = allowedGlobs.some((g) => minimatch2(filePath, g, { matchBase: true }));
|
|
5245
|
+
return inExplicit || inGlobs;
|
|
5246
|
+
};
|
|
5247
|
+
const validation = await ProofValidator.validate(receipt, requiredProof, isAllowedFile, blobDir);
|
|
5248
|
+
const receiptId = `rcpt_${uuidv48().replace(/-/g, "").slice(0, 16)}`;
|
|
5249
|
+
const finalStatus = validation.valid ? receipt.status : "failed";
|
|
5250
|
+
await pointerStore.insertReceipt({
|
|
5251
|
+
receiptId,
|
|
5252
|
+
workOrderId: receipt.workOrderId,
|
|
5253
|
+
status: finalStatus,
|
|
5254
|
+
proofPointers: receipt.proofPointers,
|
|
5255
|
+
changedFiles: receipt.changedFiles,
|
|
5256
|
+
summary: receipt.summary
|
|
5257
|
+
});
|
|
5258
|
+
await pointerStore.updateWorkOrderStatus(receipt.workOrderId, validation.valid ? receipt.status === "completed" ? "completed" : "failed" : "failed");
|
|
5259
|
+
return {
|
|
5260
|
+
receiptId,
|
|
5261
|
+
accepted: validation.valid,
|
|
5262
|
+
workOrderId: receipt.workOrderId,
|
|
5263
|
+
validation,
|
|
5264
|
+
finalStatus
|
|
5265
|
+
};
|
|
5266
|
+
}
|
|
5267
|
+
|
|
5268
|
+
// dist/mcp/tools/set_session_scope.js
|
|
5269
|
+
var setSessionScopeTool = {
|
|
5270
|
+
name: "set_session_scope",
|
|
5271
|
+
description: "Sets the active session scope from a Work Order. All subsequent file tools (read_file, get_file_skeleton, apply_patch) will enforce this scope until overwritten or the server restarts.",
|
|
5272
|
+
inputSchema: {
|
|
5273
|
+
type: "object",
|
|
5274
|
+
properties: {
|
|
5275
|
+
projectRoot: { type: "string", description: "Absolute project root" },
|
|
5276
|
+
workOrderId: { type: "string", description: "Work Order ID to load scope from" }
|
|
5277
|
+
},
|
|
5278
|
+
required: ["projectRoot", "workOrderId"]
|
|
5279
|
+
}
|
|
5280
|
+
};
|
|
5281
|
+
async function handleSetSessionScope(args) {
|
|
5282
|
+
const projectRoot = args.projectRoot;
|
|
5283
|
+
const workOrderId = args.workOrderId;
|
|
5284
|
+
if (!projectRoot || !workOrderId) {
|
|
5285
|
+
throw new Error("projectRoot and workOrderId are required");
|
|
5286
|
+
}
|
|
5287
|
+
const pointerStore = PointerStore.open(projectRoot);
|
|
5288
|
+
const row = pointerStore.getWorkOrder(workOrderId);
|
|
5289
|
+
if (!row) {
|
|
5290
|
+
throw new Error(`WorkOrderNotFound: ${workOrderId}`);
|
|
5291
|
+
}
|
|
5292
|
+
const policy = SessionScope.fromWorkOrderRow(row);
|
|
5293
|
+
SessionScope.set(policy);
|
|
5294
|
+
return {
|
|
5295
|
+
ok: true,
|
|
5296
|
+
workOrderId,
|
|
5297
|
+
scope: {
|
|
5298
|
+
allowedFiles: policy.allowedFiles,
|
|
5299
|
+
allowedGlobs: policy.allowedGlobs,
|
|
5300
|
+
deniedGlobs: policy.deniedGlobs,
|
|
5301
|
+
requiredProofCount: policy.requiredProof.length
|
|
5302
|
+
},
|
|
5303
|
+
message: `Session scope set from work order ${workOrderId}. All file tools will enforce this scope.`
|
|
5304
|
+
};
|
|
5305
|
+
}
|
|
5306
|
+
|
|
5307
|
+
// dist/mcp/tools/yield_for_scope_expansion.js
|
|
5308
|
+
var yieldForScopeExpansionTool = {
|
|
5309
|
+
name: "yield_for_scope_expansion",
|
|
5310
|
+
description: "Worker signals that it needs to access files outside its current scope. Immediately terminates the active scope and returns a Blocked receipt. The Manager must evaluate the evidence and decide whether to spawn a new Worker with expanded scope.",
|
|
5311
|
+
inputSchema: {
|
|
5312
|
+
type: "object",
|
|
5313
|
+
properties: {
|
|
5314
|
+
requestedPaths: {
|
|
5315
|
+
type: "array",
|
|
5316
|
+
items: { type: "string" },
|
|
5317
|
+
description: "Paths the worker needs but cannot access under current scope"
|
|
5318
|
+
},
|
|
5319
|
+
reason: {
|
|
5320
|
+
type: "string",
|
|
5321
|
+
description: "Why these paths are needed \u2014 root cause found in out-of-scope file"
|
|
5322
|
+
},
|
|
5323
|
+
evidencePointers: {
|
|
5324
|
+
type: "array",
|
|
5325
|
+
items: { type: "string" },
|
|
5326
|
+
description: "Pointer IDs for artifacts that justify the expansion request"
|
|
5327
|
+
}
|
|
5328
|
+
},
|
|
5329
|
+
required: ["requestedPaths", "reason"]
|
|
5330
|
+
}
|
|
5331
|
+
};
|
|
5332
|
+
async function handleYieldForScopeExpansion(args) {
|
|
5333
|
+
const requestedPaths = args.requestedPaths ?? [];
|
|
5334
|
+
const reason = args.reason;
|
|
5335
|
+
const evidencePointers = args.evidencePointers ?? [];
|
|
5336
|
+
if (!requestedPaths.length || !reason) {
|
|
5337
|
+
throw new Error("requestedPaths and reason are required");
|
|
5338
|
+
}
|
|
5339
|
+
const currentScope = SessionScope.get();
|
|
5340
|
+
const workOrderId = currentScope?.workOrderId ?? "unknown";
|
|
5341
|
+
SessionScope.clear();
|
|
5342
|
+
return {
|
|
5343
|
+
status: "blocked",
|
|
5344
|
+
workOrderId,
|
|
5345
|
+
requestedPaths,
|
|
5346
|
+
reason,
|
|
5347
|
+
evidencePointers,
|
|
5348
|
+
receipt: {
|
|
5349
|
+
workOrderId,
|
|
5350
|
+
status: "blocked",
|
|
5351
|
+
proofPointers: [],
|
|
5352
|
+
changedFiles: [],
|
|
5353
|
+
summary: `Worker blocked: scope expansion required. Reason: ${reason}`
|
|
5354
|
+
},
|
|
5355
|
+
managerInstructions: [
|
|
5356
|
+
"Worker has been terminated. Active session scope has been cleared.",
|
|
5357
|
+
"Evaluate the evidence pointers and reason.",
|
|
5358
|
+
"If expansion is warranted, create a new Work Order with the expanded allowedFiles and spawn a new Worker.",
|
|
5359
|
+
"If expansion is NOT warranted, the task is failed \u2014 do not retry with same scope."
|
|
5360
|
+
]
|
|
5361
|
+
};
|
|
5362
|
+
}
|
|
5363
|
+
|
|
5364
|
+
// dist/mcp/tools/hydration/get_start_here.js
|
|
5365
|
+
var getStartHereTool = {
|
|
5366
|
+
name: "get_start_here",
|
|
5367
|
+
description: "Hydrates the start-here index for a scan manifest pointer. Returns the top 5 highest-gravity files. Pointer must be valid and unexpired.",
|
|
5368
|
+
inputSchema: {
|
|
5369
|
+
type: "object",
|
|
5370
|
+
properties: {
|
|
5371
|
+
projectRoot: { type: "string", description: "Absolute project root" },
|
|
5372
|
+
manifestPointer: { type: "string", description: "Pointer ID for the scan manifest or analysis.index artifact" },
|
|
5373
|
+
scanId: { type: "string", description: "Current scan ID for budget pointer registration" }
|
|
5374
|
+
},
|
|
5375
|
+
required: ["projectRoot", "manifestPointer", "scanId"]
|
|
5376
|
+
}
|
|
5377
|
+
};
|
|
5378
|
+
async function handleGetStartHere(args) {
|
|
5379
|
+
const projectRoot = args.projectRoot;
|
|
5380
|
+
const manifestPointer = args.manifestPointer;
|
|
5381
|
+
const scanId = args.scanId;
|
|
5382
|
+
if (!projectRoot || !manifestPointer || !scanId) {
|
|
5383
|
+
throw new Error("projectRoot, manifestPointer, and scanId are required");
|
|
5384
|
+
}
|
|
5385
|
+
const { content, row } = await hydratePointer(projectRoot, manifestPointer);
|
|
5386
|
+
const payload = JSON.parse(content.toString("utf8"));
|
|
5387
|
+
if (row.artifactName === "artifact_manifest") {
|
|
5388
|
+
const manifest = payload;
|
|
5389
|
+
const analysisEntry = manifest.artifacts.find((a) => a.name === "analysis" || a.name === "analysis.index");
|
|
5390
|
+
if (!analysisEntry?.indexes?.startHere) {
|
|
5391
|
+
throw new Error("Manifest has no analysis.index entry \u2014 rescan to regenerate");
|
|
5392
|
+
}
|
|
5393
|
+
const { content: indexContent } = await hydratePointer(projectRoot, analysisEntry.indexes.startHere);
|
|
5394
|
+
const index = JSON.parse(indexContent.toString("utf8"));
|
|
5395
|
+
const result = {
|
|
5396
|
+
startHere: index.startHere.slice(0, 5),
|
|
5397
|
+
schemaVersion: index.schemaVersion,
|
|
5398
|
+
scanId: index.scanId
|
|
5399
|
+
};
|
|
5400
|
+
return await applyBudgetGuard(projectRoot, scanId, "get_start_here_result", result);
|
|
5401
|
+
}
|
|
5402
|
+
if (row.artifactName === "analysis.index") {
|
|
5403
|
+
const result = {
|
|
5404
|
+
startHere: payload.startHere.slice(0, 5),
|
|
5405
|
+
schemaVersion: payload.schemaVersion,
|
|
5406
|
+
scanId: payload.scanId
|
|
5407
|
+
};
|
|
5408
|
+
return await applyBudgetGuard(projectRoot, scanId, "get_start_here_result", result);
|
|
5409
|
+
}
|
|
5410
|
+
throw new Error(`Unsupported artifact type for get_start_here: ${row.artifactName}`);
|
|
5411
|
+
}
|
|
5412
|
+
|
|
5413
|
+
// dist/mcp/tools/hydration/get_project_summary.js
|
|
5414
|
+
var getProjectSummaryTool = {
|
|
5415
|
+
name: "get_project_summary",
|
|
5416
|
+
description: "Returns high-level project metrics from a scan manifest pointer: file counts, pillar summary, stack. Token-safe. Pointer must be valid and unexpired.",
|
|
5417
|
+
inputSchema: {
|
|
5418
|
+
type: "object",
|
|
5419
|
+
properties: {
|
|
5420
|
+
projectRoot: { type: "string", description: "Absolute project root" },
|
|
5421
|
+
manifestPointer: { type: "string", description: "Pointer ID for the scan manifest" },
|
|
5422
|
+
scanId: { type: "string", description: "Current scan ID for budget pointer registration" }
|
|
5423
|
+
},
|
|
5424
|
+
required: ["projectRoot", "manifestPointer", "scanId"]
|
|
5425
|
+
}
|
|
5426
|
+
};
|
|
5427
|
+
async function handleGetProjectSummary(args) {
|
|
5428
|
+
const projectRoot = args.projectRoot;
|
|
5429
|
+
const manifestPointer = args.manifestPointer;
|
|
5430
|
+
const scanId = args.scanId;
|
|
5431
|
+
if (!projectRoot || !manifestPointer || !scanId) {
|
|
5432
|
+
throw new Error("projectRoot, manifestPointer, and scanId are required");
|
|
5433
|
+
}
|
|
5434
|
+
const { content, row } = await hydratePointer(projectRoot, manifestPointer);
|
|
5435
|
+
const payload = JSON.parse(content.toString("utf8"));
|
|
5436
|
+
if (row.artifactName === "artifact_manifest") {
|
|
5437
|
+
const manifest = payload;
|
|
5438
|
+
const analysisEntry = manifest.artifacts.find((a) => a.name === "analysis" && a.indexes?.startHere);
|
|
5439
|
+
let indexData = {};
|
|
5440
|
+
if (analysisEntry?.indexes?.startHere) {
|
|
5441
|
+
const { content: ic } = await hydratePointer(projectRoot, analysisEntry.indexes.startHere);
|
|
5442
|
+
indexData = JSON.parse(ic.toString("utf8"));
|
|
5443
|
+
}
|
|
5444
|
+
const result = {
|
|
5445
|
+
scanId: manifest.scanId,
|
|
5446
|
+
generatedAt: manifest.generatedAt,
|
|
5447
|
+
artifactCount: manifest.artifacts.length,
|
|
5448
|
+
totalArtifactBytes: manifest.artifacts.reduce((s, a) => s + (a.sizeBytes ?? 0), 0),
|
|
5449
|
+
startHere: indexData.startHere,
|
|
5450
|
+
topHeat: indexData.topHeat,
|
|
5451
|
+
pillarSummary: indexData.pillarSummary,
|
|
5452
|
+
totalFiles: indexData.totalFiles,
|
|
5453
|
+
realSourceFiles: indexData.realSourceFiles
|
|
5454
|
+
};
|
|
5455
|
+
return await applyBudgetGuard(projectRoot, scanId, "get_project_summary_result", result);
|
|
5456
|
+
}
|
|
5457
|
+
if (row.artifactName === "analysis.index") {
|
|
5458
|
+
const result = {
|
|
5459
|
+
scanId: payload.scanId,
|
|
5460
|
+
startHere: payload.startHere,
|
|
5461
|
+
topHeat: payload.topHeat,
|
|
5462
|
+
pillarSummary: payload.pillarSummary,
|
|
5463
|
+
totalFiles: payload.totalFiles,
|
|
5464
|
+
realSourceFiles: payload.realSourceFiles
|
|
5465
|
+
};
|
|
5466
|
+
return await applyBudgetGuard(projectRoot, scanId, "get_project_summary_result", result);
|
|
5467
|
+
}
|
|
5468
|
+
throw new Error(`Unsupported artifact type for get_project_summary: ${row.artifactName}`);
|
|
5469
|
+
}
|
|
5470
|
+
|
|
5471
|
+
// dist/mcp/tools/hydration/get_evidence_slice.js
|
|
5472
|
+
var getEvidenceSliceTool = {
|
|
5473
|
+
name: "get_evidence_slice",
|
|
5474
|
+
description: "Raw fallback: returns a line-range slice from a blob artifact. Pointer must be valid and unexpired. Output is budgeted.",
|
|
5475
|
+
inputSchema: {
|
|
5476
|
+
type: "object",
|
|
5477
|
+
properties: {
|
|
5478
|
+
projectRoot: { type: "string", description: "Absolute project root" },
|
|
5479
|
+
pointerId: { type: "string", description: "Pointer ID for the target artifact" },
|
|
5480
|
+
startLine: { type: "number", description: "Inclusive start line (1-based)" },
|
|
5481
|
+
endLine: { type: "number", description: "Inclusive end line (1-based). Capped at startLine+200." },
|
|
5482
|
+
scanId: { type: "string", description: "Current scan ID for budget pointer registration" }
|
|
5483
|
+
},
|
|
5484
|
+
required: ["projectRoot", "pointerId", "startLine", "endLine", "scanId"]
|
|
5485
|
+
}
|
|
5486
|
+
};
|
|
5487
|
+
async function handleGetEvidenceSlice(args) {
|
|
5488
|
+
const projectRoot = args.projectRoot;
|
|
5489
|
+
const pointerId = args.pointerId;
|
|
5490
|
+
const startLine = Number(args.startLine);
|
|
5491
|
+
const endLine = Math.min(Number(args.endLine), startLine + 200);
|
|
5492
|
+
const scanId = args.scanId;
|
|
5493
|
+
if (!projectRoot || !pointerId || !scanId) {
|
|
5494
|
+
throw new Error("projectRoot, pointerId, startLine, endLine, and scanId are required");
|
|
5495
|
+
}
|
|
5496
|
+
const { content, row } = await hydratePointer(projectRoot, pointerId);
|
|
5497
|
+
const rawText = content.toString("utf8");
|
|
5498
|
+
const scope = SessionScope.get();
|
|
5499
|
+
if (scope && (row.artifactName === "file_read" || row.artifactName === "file_skeleton")) {
|
|
5500
|
+
try {
|
|
5501
|
+
const parsed = JSON.parse(rawText);
|
|
5502
|
+
if (parsed.filePath) {
|
|
5503
|
+
SessionScope.enforce(parsed.filePath);
|
|
5504
|
+
}
|
|
5505
|
+
} catch (e) {
|
|
5506
|
+
if (e.name === "ScopeViolation")
|
|
5507
|
+
throw e;
|
|
5508
|
+
}
|
|
5509
|
+
}
|
|
5510
|
+
const lines = rawText.split("\n");
|
|
5511
|
+
const sliced = lines.slice(startLine - 1, endLine);
|
|
5512
|
+
const result = {
|
|
5513
|
+
pointerId,
|
|
5514
|
+
artifactName: row.artifactName,
|
|
5515
|
+
startLine,
|
|
5516
|
+
endLine,
|
|
5517
|
+
totalLines: lines.length,
|
|
5518
|
+
slice: sliced
|
|
5519
|
+
};
|
|
5520
|
+
return await applyBudgetGuard(projectRoot, scanId, "evidence_slice", result);
|
|
5521
|
+
}
|
|
5522
|
+
|
|
4209
5523
|
// dist/mcp/server.js
|
|
4210
5524
|
var ALL_TOOLS = [
|
|
4211
5525
|
scanProjectTool,
|
|
@@ -4217,7 +5531,21 @@ var ALL_TOOLS = [
|
|
|
4217
5531
|
getStrategicOverviewTool,
|
|
4218
5532
|
inspectPillarTool,
|
|
4219
5533
|
getWildDiscoveriesTool,
|
|
4220
|
-
markStaleTool
|
|
5534
|
+
markStaleTool,
|
|
5535
|
+
// Phase 2: Skeletons + Hydration
|
|
5536
|
+
getFileSkeletonTool,
|
|
5537
|
+
readFileTool,
|
|
5538
|
+
getStartHereTool,
|
|
5539
|
+
getProjectSummaryTool,
|
|
5540
|
+
getEvidenceSliceTool,
|
|
5541
|
+
// Phase 3: Delegation & Proof
|
|
5542
|
+
createWorkOrderTool,
|
|
5543
|
+
spawnWorkerTool,
|
|
5544
|
+
applyPatchTool,
|
|
5545
|
+
submitReceiptTool,
|
|
5546
|
+
// Phase 4: Scope & Escalation
|
|
5547
|
+
setSessionScopeTool,
|
|
5548
|
+
yieldForScopeExpansionTool
|
|
4221
5549
|
];
|
|
4222
5550
|
var TOOL_HANDLERS = {
|
|
4223
5551
|
scan_project: handleScanProject,
|
|
@@ -4229,7 +5557,21 @@ var TOOL_HANDLERS = {
|
|
|
4229
5557
|
get_strategic_overview: handleGetStrategicOverview,
|
|
4230
5558
|
inspect_pillar: handleInspectPillar,
|
|
4231
5559
|
get_wild_discoveries: handleGetWildDiscoveries,
|
|
4232
|
-
mark_stale: handleMarkStale
|
|
5560
|
+
mark_stale: handleMarkStale,
|
|
5561
|
+
// Phase 2
|
|
5562
|
+
get_file_skeleton: handleGetFileSkeleton,
|
|
5563
|
+
read_file: handleReadFile,
|
|
5564
|
+
get_start_here: handleGetStartHere,
|
|
5565
|
+
get_project_summary: handleGetProjectSummary,
|
|
5566
|
+
get_evidence_slice: handleGetEvidenceSlice,
|
|
5567
|
+
// Phase 3
|
|
5568
|
+
create_work_order: handleCreateWorkOrder,
|
|
5569
|
+
spawn_worker: handleSpawnWorker,
|
|
5570
|
+
apply_patch: handleApplyPatch,
|
|
5571
|
+
submit_receipt: handleSubmitReceipt,
|
|
5572
|
+
// Phase 4
|
|
5573
|
+
set_session_scope: handleSetSessionScope,
|
|
5574
|
+
yield_for_scope_expansion: handleYieldForScopeExpansion
|
|
4233
5575
|
};
|
|
4234
5576
|
async function startMCPServer(options = {}) {
|
|
4235
5577
|
await initParser2();
|
|
@@ -4373,10 +5715,210 @@ async function exportCommand(projectRoot, options) {
|
|
|
4373
5715
|
console.error("[vibe-splain] Export complete.");
|
|
4374
5716
|
}
|
|
4375
5717
|
|
|
5718
|
+
// dist/commands/gc.js
|
|
5719
|
+
import { join as join21 } from "path";
|
|
5720
|
+
import { rm as rm2, readdir as readdir2 } from "fs/promises";
|
|
5721
|
+
var DEFAULT_KEEP_SCANS = 3;
|
|
5722
|
+
async function gcCommand(projectRoot, opts = {}) {
|
|
5723
|
+
const root = projectRoot ?? process.cwd();
|
|
5724
|
+
const keepScans = opts.keepScans ?? DEFAULT_KEEP_SCANS;
|
|
5725
|
+
console.error(`[vibe-splain gc] Running GC on ${root} (keeping last ${keepScans} scans)`);
|
|
5726
|
+
const pointerStore = PointerStore.open(root);
|
|
5727
|
+
const blobStore = new BlobStore(root);
|
|
5728
|
+
const allScanIds = pointerStore.listAllScanIds();
|
|
5729
|
+
console.error(`[vibe-splain gc] Found ${allScanIds.length} scans`);
|
|
5730
|
+
const sorted = [...allScanIds].sort().reverse();
|
|
5731
|
+
const keepIds = sorted.slice(0, keepScans);
|
|
5732
|
+
const deleteIds = sorted.slice(keepScans);
|
|
5733
|
+
if (deleteIds.length === 0) {
|
|
5734
|
+
console.error("[vibe-splain gc] Nothing to collect");
|
|
5735
|
+
return;
|
|
5736
|
+
}
|
|
5737
|
+
const keptPointers = keepIds.flatMap((id) => pointerStore.listPointersByScan(id));
|
|
5738
|
+
const referencedBlobs = new Set(keptPointers.map((p) => p.blobPath));
|
|
5739
|
+
const deleted = await pointerStore.gcScanPointers(keepIds);
|
|
5740
|
+
console.error(`[vibe-splain gc] Deleted ${deleted} pointer rows`);
|
|
5741
|
+
const allBlobs = await blobStore.listBlobPaths();
|
|
5742
|
+
let blobsDeleted = 0;
|
|
5743
|
+
for (const blobPath of allBlobs) {
|
|
5744
|
+
if (!referencedBlobs.has(blobPath)) {
|
|
5745
|
+
try {
|
|
5746
|
+
await rm2(blobPath);
|
|
5747
|
+
blobsDeleted++;
|
|
5748
|
+
} catch {
|
|
5749
|
+
}
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5752
|
+
console.error(`[vibe-splain gc] Deleted ${blobsDeleted} unreferenced blobs`);
|
|
5753
|
+
const tmpDir = join21(root, ".vibe-splainer", "tmp");
|
|
5754
|
+
try {
|
|
5755
|
+
const tmpFiles = await readdir2(tmpDir);
|
|
5756
|
+
for (const f of tmpFiles) {
|
|
5757
|
+
await rm2(join21(tmpDir, f), { force: true });
|
|
5758
|
+
}
|
|
5759
|
+
console.error(`[vibe-splain gc] Cleaned ${tmpFiles.length} tmp files`);
|
|
5760
|
+
} catch {
|
|
5761
|
+
}
|
|
5762
|
+
console.error("[vibe-splain gc] Done");
|
|
5763
|
+
}
|
|
5764
|
+
|
|
5765
|
+
// dist/commands/bundle.js
|
|
5766
|
+
import { join as join22 } from "path";
|
|
5767
|
+
import { writeFile as writeFile10, mkdir as mkdir9, copyFile, rm as rm3 } from "fs/promises";
|
|
5768
|
+
import { existsSync as existsSync7 } from "fs";
|
|
5769
|
+
import * as tar from "tar";
|
|
5770
|
+
async function bundleCommand(scanId, opts = {}) {
|
|
5771
|
+
const root = opts.projectRoot ?? process.cwd();
|
|
5772
|
+
const outputPath = opts.output ?? join22(root, `vibe-bundle-${scanId}.tar.gz`);
|
|
5773
|
+
console.error(`[vibe-splain bundle] Bundling scan ${scanId} from ${root}`);
|
|
5774
|
+
const pointerStore = PointerStore.open(root);
|
|
5775
|
+
const blobStore = new BlobStore(root);
|
|
5776
|
+
const pointers = pointerStore.listPointersByScan(scanId);
|
|
5777
|
+
if (pointers.length === 0) {
|
|
5778
|
+
throw new Error(`No pointers found for scanId "${scanId}"`);
|
|
5779
|
+
}
|
|
5780
|
+
const stagingDir = join22(root, ".vibe-splainer", "tmp", `bundle-stage-${scanId}`);
|
|
5781
|
+
const blobsStageDir = join22(stagingDir, "blobs");
|
|
5782
|
+
await mkdir9(blobsStageDir, { recursive: true });
|
|
5783
|
+
try {
|
|
5784
|
+
const bundleManifest = {
|
|
5785
|
+
schemaVersion: "1.0.0",
|
|
5786
|
+
scanId,
|
|
5787
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5788
|
+
projectRoot: root,
|
|
5789
|
+
pointers: pointers.map((p) => ({
|
|
5790
|
+
pointerId: p.pointerId,
|
|
5791
|
+
scanId: p.scanId,
|
|
5792
|
+
artifactName: p.artifactName,
|
|
5793
|
+
contentHash: p.contentHash,
|
|
5794
|
+
blobFile: `blobs/${p.contentHash.replace("sha256:", "sha256_")}`,
|
|
5795
|
+
schemaVersion: p.schemaVersion,
|
|
5796
|
+
createdAt: p.createdAt,
|
|
5797
|
+
expiresAt: p.expiresAt
|
|
5798
|
+
}))
|
|
5799
|
+
};
|
|
5800
|
+
await writeFile10(join22(stagingDir, "bundle-manifest.json"), JSON.stringify(bundleManifest, null, 2), "utf8");
|
|
5801
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5802
|
+
for (const p of pointers) {
|
|
5803
|
+
const hex = p.contentHash.replace("sha256:", "");
|
|
5804
|
+
if (seen.has(hex))
|
|
5805
|
+
continue;
|
|
5806
|
+
seen.add(hex);
|
|
5807
|
+
const srcPath = p.blobPath;
|
|
5808
|
+
if (!existsSync7(srcPath)) {
|
|
5809
|
+
console.error(`[vibe-splain bundle] Warning: blob missing for ${p.pointerId}: ${srcPath}`);
|
|
5810
|
+
continue;
|
|
5811
|
+
}
|
|
5812
|
+
await copyFile(srcPath, join22(blobsStageDir, `sha256_${hex}`));
|
|
5813
|
+
}
|
|
5814
|
+
await tar.create({
|
|
5815
|
+
gzip: true,
|
|
5816
|
+
file: outputPath,
|
|
5817
|
+
cwd: stagingDir,
|
|
5818
|
+
portable: true
|
|
5819
|
+
}, ["."]);
|
|
5820
|
+
console.error(`[vibe-splain bundle] Bundle written: ${outputPath}`);
|
|
5821
|
+
console.error(`[vibe-splain bundle] ${pointers.length} pointers, ${seen.size} blobs`);
|
|
5822
|
+
} finally {
|
|
5823
|
+
await rm3(stagingDir, { recursive: true, force: true });
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
|
|
5827
|
+
// dist/commands/importBundle.js
|
|
5828
|
+
import { join as join23 } from "path";
|
|
5829
|
+
import { readFile as readFile15, mkdir as mkdir10, rm as rm4 } from "fs/promises";
|
|
5830
|
+
import { existsSync as existsSync8 } from "fs";
|
|
5831
|
+
import * as tar2 from "tar";
|
|
5832
|
+
import { createHash as createHash6 } from "crypto";
|
|
5833
|
+
async function importBundleCommand(tarballPath, opts = {}) {
|
|
5834
|
+
const root = opts.projectRoot ?? process.cwd();
|
|
5835
|
+
const namespace = opts.namespace ?? `imported_${Date.now()}`;
|
|
5836
|
+
console.error(`[vibe-splain import] Importing ${tarballPath} into ${root} (namespace: ${namespace})`);
|
|
5837
|
+
if (!existsSync8(tarballPath)) {
|
|
5838
|
+
throw new Error(`Tarball not found: ${tarballPath}`);
|
|
5839
|
+
}
|
|
5840
|
+
const extractDir = join23(root, ".vibe-splainer", "tmp", `import-${namespace}`);
|
|
5841
|
+
await mkdir10(extractDir, { recursive: true });
|
|
5842
|
+
try {
|
|
5843
|
+
await tar2.extract({
|
|
5844
|
+
file: tarballPath,
|
|
5845
|
+
cwd: extractDir
|
|
5846
|
+
});
|
|
5847
|
+
const manifestPath = join23(extractDir, "bundle-manifest.json");
|
|
5848
|
+
if (!existsSync8(manifestPath)) {
|
|
5849
|
+
throw new Error("Invalid bundle: missing bundle-manifest.json");
|
|
5850
|
+
}
|
|
5851
|
+
const manifestRaw = await readFile15(manifestPath, "utf8");
|
|
5852
|
+
const manifest = JSON.parse(manifestRaw);
|
|
5853
|
+
if (manifest.schemaVersion !== "1.0.0") {
|
|
5854
|
+
throw new Error(`Unsupported bundle schema version: ${manifest.schemaVersion}`);
|
|
5855
|
+
}
|
|
5856
|
+
const blobStore = new BlobStore(root);
|
|
5857
|
+
const pointerStore = PointerStore.open(root);
|
|
5858
|
+
await blobStore.ensureDirs();
|
|
5859
|
+
let imported = 0;
|
|
5860
|
+
let hashErrors = 0;
|
|
5861
|
+
for (const entry of manifest.pointers) {
|
|
5862
|
+
const blobSrcPath = join23(extractDir, entry.blobFile);
|
|
5863
|
+
if (!existsSync8(blobSrcPath)) {
|
|
5864
|
+
console.error(`[vibe-splain import] Missing blob for pointer ${entry.pointerId}: ${entry.blobFile}`);
|
|
5865
|
+
hashErrors++;
|
|
5866
|
+
continue;
|
|
5867
|
+
}
|
|
5868
|
+
const content = await readFile15(blobSrcPath);
|
|
5869
|
+
const actualHash = `sha256:${createHash6("sha256").update(content).digest("hex")}`;
|
|
5870
|
+
if (actualHash !== entry.contentHash) {
|
|
5871
|
+
console.error(`[vibe-splain import] Hash mismatch for ${entry.pointerId}: expected ${entry.contentHash}, got ${actualHash}`);
|
|
5872
|
+
hashErrors++;
|
|
5873
|
+
continue;
|
|
5874
|
+
}
|
|
5875
|
+
const { blobPath } = await blobStore.writeAtomic(content);
|
|
5876
|
+
const namespacedPointerId = `${namespace}::${entry.pointerId}`;
|
|
5877
|
+
const namespacedScanId = `${namespace}::${entry.scanId}`;
|
|
5878
|
+
await pointerStore.insertPointer({
|
|
5879
|
+
pointerId: namespacedPointerId,
|
|
5880
|
+
scanId: namespacedScanId,
|
|
5881
|
+
artifactName: entry.artifactName,
|
|
5882
|
+
contentHash: entry.contentHash,
|
|
5883
|
+
blobPath,
|
|
5884
|
+
schemaVersion: entry.schemaVersion,
|
|
5885
|
+
createdAt: entry.createdAt,
|
|
5886
|
+
expiresAt: entry.expiresAt
|
|
5887
|
+
});
|
|
5888
|
+
imported++;
|
|
5889
|
+
}
|
|
5890
|
+
if (hashErrors > 0) {
|
|
5891
|
+
console.error(`[vibe-splain import] Warning: ${hashErrors} blobs failed hash verification and were skipped`);
|
|
5892
|
+
}
|
|
5893
|
+
console.error(`[vibe-splain import] Imported ${imported}/${manifest.pointers.length} pointers under namespace "${namespace}"`);
|
|
5894
|
+
console.error(`[vibe-splain import] Original scanId: ${manifest.scanId} \u2192 namespaced as: ${namespace}::${manifest.scanId}`);
|
|
5895
|
+
} finally {
|
|
5896
|
+
await rm4(extractDir, { recursive: true, force: true });
|
|
5897
|
+
}
|
|
5898
|
+
}
|
|
5899
|
+
|
|
4376
5900
|
// dist/index.js
|
|
4377
5901
|
var program = new Command();
|
|
4378
|
-
program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("3.
|
|
5902
|
+
program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("3.2.0");
|
|
4379
5903
|
program.command("install").description("Patch coding agent MCP config files to register vibe-splain").action(installCommand);
|
|
4380
5904
|
program.command("serve").description("Start the MCP server (called by the coding agent, not by you)").option("--format <format>", "Export format (html, markdown, etc.)").option("--budget <budget>", "Token budget for markdown").option("--scope <scope>", "Scope for export").action((options) => serveCommand(options));
|
|
4381
5905
|
program.command("export [projectRoot]").description("Manually trigger bundle generation").option("--format <format>", "Export format (html, markdown, etc.)").option("--budget <budget>", "Token budget for markdown").option("--scope <scope>", "Scope for export").action(exportCommand);
|
|
5906
|
+
program.command("gc [projectRoot]").description("Garbage-collect old scan artifacts, keeping the last N scans").option("--keep-scans <n>", "Number of scans to keep (default: 3)", "3").action((projectRoot, options) => {
|
|
5907
|
+
gcCommand(projectRoot, { keepScans: parseInt(options.keepScans, 10) }).catch((err) => {
|
|
5908
|
+
console.error("[vibe-splain gc] Error:", err.message);
|
|
5909
|
+
process.exit(1);
|
|
5910
|
+
});
|
|
5911
|
+
});
|
|
5912
|
+
program.command("bundle <scanId>").description("Bundle a scan into a portable vibe-bundle.tar.gz").option("--output <path>", "Output tarball path").option("--project-root <path>", "Project root (default: cwd)").action((scanId, options) => {
|
|
5913
|
+
bundleCommand(scanId, { output: options.output, projectRoot: options.projectRoot }).catch((err) => {
|
|
5914
|
+
console.error("[vibe-splain bundle] Error:", err.message);
|
|
5915
|
+
process.exit(1);
|
|
5916
|
+
});
|
|
5917
|
+
});
|
|
5918
|
+
program.command("import <tarball>").description("Import a vibe-bundle.tar.gz into the local pointer store").option("--namespace <ns>", "Bundle namespace alias (default: imported_<timestamp>)").option("--project-root <path>", "Project root (default: cwd)").action((tarball, options) => {
|
|
5919
|
+
importBundleCommand(tarball, { namespace: options.namespace, projectRoot: options.projectRoot }).catch((err) => {
|
|
5920
|
+
console.error("[vibe-splain import] Error:", err.message);
|
|
5921
|
+
process.exit(1);
|
|
5922
|
+
});
|
|
5923
|
+
});
|
|
4382
5924
|
program.parse();
|