@vibecheckai/cli 3.0.2 → 3.0.3
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/package.json +9 -1
- package/bin/cli-hygiene.js +0 -241
- package/bin/guardrail.js +0 -834
- package/bin/runners/cli-utils.js +0 -1070
- package/bin/runners/context/ai-task-decomposer.js +0 -337
- package/bin/runners/context/analyzer.js +0 -462
- package/bin/runners/context/api-contracts.js +0 -427
- package/bin/runners/context/context-diff.js +0 -342
- package/bin/runners/context/context-pruner.js +0 -291
- package/bin/runners/context/dependency-graph.js +0 -414
- package/bin/runners/context/generators/claude.js +0 -107
- package/bin/runners/context/generators/codex.js +0 -108
- package/bin/runners/context/generators/copilot.js +0 -119
- package/bin/runners/context/generators/cursor.js +0 -514
- package/bin/runners/context/generators/mcp.js +0 -151
- package/bin/runners/context/generators/windsurf.js +0 -180
- package/bin/runners/context/git-context.js +0 -302
- package/bin/runners/context/index.js +0 -1042
- package/bin/runners/context/insights.js +0 -173
- package/bin/runners/context/mcp-server/generate-rules.js +0 -337
- package/bin/runners/context/mcp-server/index.js +0 -1176
- package/bin/runners/context/mcp-server/package.json +0 -24
- package/bin/runners/context/memory.js +0 -200
- package/bin/runners/context/monorepo.js +0 -215
- package/bin/runners/context/multi-repo-federation.js +0 -404
- package/bin/runners/context/patterns.js +0 -253
- package/bin/runners/context/proof-context.js +0 -972
- package/bin/runners/context/security-scanner.js +0 -303
- package/bin/runners/context/semantic-search.js +0 -350
- package/bin/runners/context/shared.js +0 -264
- package/bin/runners/context/team-conventions.js +0 -310
- package/bin/runners/lib/ai-bridge.js +0 -416
- package/bin/runners/lib/analysis-core.js +0 -271
- package/bin/runners/lib/analyzers.js +0 -541
- package/bin/runners/lib/audit-bridge.js +0 -391
- package/bin/runners/lib/auth-truth.js +0 -193
- package/bin/runners/lib/auth.js +0 -215
- package/bin/runners/lib/backup.js +0 -62
- package/bin/runners/lib/billing.js +0 -107
- package/bin/runners/lib/claims.js +0 -118
- package/bin/runners/lib/cli-ui.js +0 -540
- package/bin/runners/lib/compliance-bridge-new.js +0 -0
- package/bin/runners/lib/compliance-bridge.js +0 -165
- package/bin/runners/lib/contracts/auth-contract.js +0 -194
- package/bin/runners/lib/contracts/env-contract.js +0 -178
- package/bin/runners/lib/contracts/external-contract.js +0 -198
- package/bin/runners/lib/contracts/guard.js +0 -168
- package/bin/runners/lib/contracts/index.js +0 -89
- package/bin/runners/lib/contracts/plan-validator.js +0 -311
- package/bin/runners/lib/contracts/route-contract.js +0 -192
- package/bin/runners/lib/detect.js +0 -89
- package/bin/runners/lib/doctor/autofix.js +0 -254
- package/bin/runners/lib/doctor/index.js +0 -37
- package/bin/runners/lib/doctor/modules/dependencies.js +0 -325
- package/bin/runners/lib/doctor/modules/index.js +0 -46
- package/bin/runners/lib/doctor/modules/network.js +0 -250
- package/bin/runners/lib/doctor/modules/project.js +0 -312
- package/bin/runners/lib/doctor/modules/runtime.js +0 -224
- package/bin/runners/lib/doctor/modules/security.js +0 -348
- package/bin/runners/lib/doctor/modules/system.js +0 -213
- package/bin/runners/lib/doctor/modules/vibecheck.js +0 -394
- package/bin/runners/lib/doctor/reporter.js +0 -262
- package/bin/runners/lib/doctor/service.js +0 -262
- package/bin/runners/lib/doctor/types.js +0 -113
- package/bin/runners/lib/doctor/ui.js +0 -263
- package/bin/runners/lib/doctor-enhanced.js +0 -233
- package/bin/runners/lib/doctor-v2.js +0 -608
- package/bin/runners/lib/enforcement.js +0 -72
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
const https = require("https");
|
|
4
|
-
|
|
5
|
-
// Cache for package existence checks
|
|
6
|
-
const packageCache = new Map();
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Check if a package exists in the NPM registry
|
|
10
|
-
*/
|
|
11
|
-
function checkNpmPackage(name) {
|
|
12
|
-
return new Promise((resolve) => {
|
|
13
|
-
if (packageCache.has(name)) return resolve(packageCache.get(name));
|
|
14
|
-
|
|
15
|
-
// Handle scoped packages and regular packages
|
|
16
|
-
const url = `https://registry.npmjs.org/${name}`;
|
|
17
|
-
|
|
18
|
-
const req = https.request(url, { method: "HEAD" }, (res) => {
|
|
19
|
-
const exists = res.statusCode === 200;
|
|
20
|
-
packageCache.set(name, exists);
|
|
21
|
-
resolve(exists);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
req.on("error", () => resolve(false)); // Assume false on error or offline
|
|
25
|
-
req.end();
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Extract imports from file content
|
|
31
|
-
*/
|
|
32
|
-
function extractImports(content) {
|
|
33
|
-
const imports = new Set();
|
|
34
|
-
|
|
35
|
-
// ESM imports
|
|
36
|
-
const importMatches = content.matchAll(
|
|
37
|
-
/import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g,
|
|
38
|
-
);
|
|
39
|
-
for (const match of importMatches) {
|
|
40
|
-
if (match[1] && !match[1].startsWith(".") && !match[1].startsWith("/")) {
|
|
41
|
-
// Extract package name (handle scoped packages)
|
|
42
|
-
const parts = match[1].split("/");
|
|
43
|
-
const pkgName = match[1].startsWith("@")
|
|
44
|
-
? `${parts[0]}/${parts[1]}`
|
|
45
|
-
: parts[0];
|
|
46
|
-
imports.add(pkgName);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// CommonJS requires
|
|
51
|
-
const requireMatches = content.matchAll(
|
|
52
|
-
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
53
|
-
);
|
|
54
|
-
for (const match of requireMatches) {
|
|
55
|
-
if (match[1] && !match[1].startsWith(".") && !match[1].startsWith("/")) {
|
|
56
|
-
const parts = match[1].split("/");
|
|
57
|
-
const pkgName = match[1].startsWith("@")
|
|
58
|
-
? `${parts[0]}/${parts[1]}`
|
|
59
|
-
: parts[0];
|
|
60
|
-
imports.add(pkgName);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return Array.from(imports);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Walk directory to find source files
|
|
69
|
-
*/
|
|
70
|
-
function walkDir(dir, fileList = []) {
|
|
71
|
-
if (!fs.existsSync(dir)) return fileList;
|
|
72
|
-
|
|
73
|
-
const files = fs.readdirSync(dir);
|
|
74
|
-
for (const file of files) {
|
|
75
|
-
if (
|
|
76
|
-
["node_modules", ".git", "dist", "build", ".vibecheck", ".next"].includes(
|
|
77
|
-
file,
|
|
78
|
-
)
|
|
79
|
-
)
|
|
80
|
-
continue;
|
|
81
|
-
|
|
82
|
-
const filePath = path.join(dir, file);
|
|
83
|
-
try {
|
|
84
|
-
const stat = fs.statSync(filePath);
|
|
85
|
-
if (stat.isDirectory()) {
|
|
86
|
-
walkDir(filePath, fileList);
|
|
87
|
-
} else if (/\.(ts|js|tsx|jsx)$/.test(file) && !file.endsWith(".d.ts")) {
|
|
88
|
-
fileList.push(filePath);
|
|
89
|
-
}
|
|
90
|
-
} catch (e) {}
|
|
91
|
-
}
|
|
92
|
-
return fileList;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function runHallucinationCheck(projectPath) {
|
|
96
|
-
const issues = [];
|
|
97
|
-
let score = 100;
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
// 1. Check package.json dependencies
|
|
101
|
-
const pkgPath = path.join(projectPath, "package.json");
|
|
102
|
-
if (fs.existsSync(pkgPath)) {
|
|
103
|
-
const content = fs.readFileSync(pkgPath, "utf8");
|
|
104
|
-
if (content.trim()) {
|
|
105
|
-
try {
|
|
106
|
-
const pkg = JSON.parse(content);
|
|
107
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
108
|
-
|
|
109
|
-
// Limit checks to avoid rate limiting
|
|
110
|
-
const depNames = Object.keys(deps).slice(0, 20);
|
|
111
|
-
|
|
112
|
-
for (const dep of depNames) {
|
|
113
|
-
// Skip internal/scoped packages for now if they look private
|
|
114
|
-
if (dep.startsWith("@vibecheck") || dep.startsWith("@internal"))
|
|
115
|
-
continue;
|
|
116
|
-
|
|
117
|
-
const exists = await checkNpmPackage(dep);
|
|
118
|
-
if (!exists) {
|
|
119
|
-
issues.push({
|
|
120
|
-
severity: "critical",
|
|
121
|
-
type: "Hallucination",
|
|
122
|
-
message: `Dependency '${dep}' listed in package.json does not exist in public npm registry`,
|
|
123
|
-
file: "package.json",
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
} catch (e) {
|
|
128
|
-
console.warn("Failed to parse package.json:", e.message);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 2. Scan source files for imports (if scanning a full project)
|
|
134
|
-
// For specific file validation in runValidate, we might want to check that specific file's imports too.
|
|
135
|
-
// However, runHallucinationCheck here is designed for project-level scanning.
|
|
136
|
-
} catch (err) {
|
|
137
|
-
console.error("AI Bridge Error:", err.message);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Deduct score
|
|
141
|
-
if (issues.length > 0) {
|
|
142
|
-
score = Math.max(0, 100 - issues.length * 20);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
score,
|
|
147
|
-
issues,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// --- Intent Matcher Logic (Ported from packages/ai-vibechecks) ---
|
|
152
|
-
|
|
153
|
-
function extractCodeIntent(code) {
|
|
154
|
-
const entities = [];
|
|
155
|
-
const operations = [];
|
|
156
|
-
|
|
157
|
-
// Extract function/class names
|
|
158
|
-
const functionMatches = code.matchAll(
|
|
159
|
-
/(?:function|const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g,
|
|
160
|
-
);
|
|
161
|
-
for (const match of functionMatches) {
|
|
162
|
-
if (match[1]) entities.push(match[1]);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const classMatches = code.matchAll(/class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g);
|
|
166
|
-
for (const match of classMatches) {
|
|
167
|
-
if (match[1]) entities.push(match[1]);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Detect operations
|
|
171
|
-
if (code.includes("fetch") || code.includes("axios") || code.includes("http"))
|
|
172
|
-
operations.push("API call");
|
|
173
|
-
if (
|
|
174
|
-
code.includes("fs.") ||
|
|
175
|
-
code.includes("writeFile") ||
|
|
176
|
-
code.includes("readFile")
|
|
177
|
-
)
|
|
178
|
-
operations.push("File I/O");
|
|
179
|
-
if (
|
|
180
|
-
code.includes("database") ||
|
|
181
|
-
code.includes("prisma") ||
|
|
182
|
-
code.includes("mongoose")
|
|
183
|
-
)
|
|
184
|
-
operations.push("Database operation");
|
|
185
|
-
if (
|
|
186
|
-
code.includes("map") ||
|
|
187
|
-
code.includes("filter") ||
|
|
188
|
-
code.includes("reduce")
|
|
189
|
-
)
|
|
190
|
-
operations.push("Data transformation");
|
|
191
|
-
if (code.includes("useState") || code.includes("useEffect"))
|
|
192
|
-
operations.push("React hooks");
|
|
193
|
-
|
|
194
|
-
return { entities, operations };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function parseRequestIntent(request) {
|
|
198
|
-
const lowerRequest = request.toLowerCase();
|
|
199
|
-
|
|
200
|
-
// Extract goal (simplified)
|
|
201
|
-
let goal = "Unknown";
|
|
202
|
-
if (lowerRequest.includes("create") || lowerRequest.includes("build"))
|
|
203
|
-
goal = "Create new functionality";
|
|
204
|
-
else if (lowerRequest.includes("fix") || lowerRequest.includes("debug"))
|
|
205
|
-
goal = "Fix issue";
|
|
206
|
-
|
|
207
|
-
// Extract constraints
|
|
208
|
-
const constraints = [];
|
|
209
|
-
if (lowerRequest.includes("without")) {
|
|
210
|
-
const match = lowerRequest.match(/without\s+([^.,;]+)/);
|
|
211
|
-
if (match) constraints.push(`Avoid: ${match[1].trim()}`);
|
|
212
|
-
}
|
|
213
|
-
if (lowerRequest.includes("using")) {
|
|
214
|
-
const match = lowerRequest.match(/using\s+([^.,;]+)/);
|
|
215
|
-
if (match) constraints.push(`Use: ${match[1].trim()}`);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Extract expected entities
|
|
219
|
-
const expectedEntities = [];
|
|
220
|
-
const commonFrameworks = [
|
|
221
|
-
"react",
|
|
222
|
-
"vue",
|
|
223
|
-
"angular",
|
|
224
|
-
"express",
|
|
225
|
-
"fastify",
|
|
226
|
-
"next",
|
|
227
|
-
"prisma",
|
|
228
|
-
"postgres",
|
|
229
|
-
"mongo",
|
|
230
|
-
];
|
|
231
|
-
for (const fw of commonFrameworks) {
|
|
232
|
-
if (lowerRequest.includes(fw)) expectedEntities.push(fw);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Extract expected operations
|
|
236
|
-
const expectedOperations = [];
|
|
237
|
-
if (lowerRequest.includes("api") || lowerRequest.includes("fetch"))
|
|
238
|
-
expectedOperations.push("API call");
|
|
239
|
-
if (lowerRequest.includes("database") || lowerRequest.includes("store"))
|
|
240
|
-
expectedOperations.push("Database operation");
|
|
241
|
-
if (lowerRequest.includes("file")) expectedOperations.push("File I/O");
|
|
242
|
-
|
|
243
|
-
return { goal, constraints, expectedEntities, expectedOperations };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function compareIntents(requested, actual) {
|
|
247
|
-
const matches = [];
|
|
248
|
-
const mismatches = [];
|
|
249
|
-
let score = 0;
|
|
250
|
-
|
|
251
|
-
// Check expected entities
|
|
252
|
-
for (const expected of requested.expectedEntities) {
|
|
253
|
-
const found =
|
|
254
|
-
actual.entities.some((e) =>
|
|
255
|
-
e.toLowerCase().includes(expected.toLowerCase()),
|
|
256
|
-
) ||
|
|
257
|
-
// Also check imports for entities (e.g. react)
|
|
258
|
-
actual.entities.some((e) =>
|
|
259
|
-
e.toLowerCase().includes(expected.toLowerCase()),
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
if (found) {
|
|
263
|
-
matches.push(`Expected entity '${expected}' found`);
|
|
264
|
-
score += 20;
|
|
265
|
-
} else {
|
|
266
|
-
mismatches.push(`Expected entity '${expected}' not found in structure`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Check expected operations
|
|
271
|
-
for (const expectedOp of requested.expectedOperations) {
|
|
272
|
-
const found = actual.operations.some((o) =>
|
|
273
|
-
o.toLowerCase().includes(expectedOp.toLowerCase()),
|
|
274
|
-
);
|
|
275
|
-
if (found) {
|
|
276
|
-
matches.push(`Expected operation '${expectedOp}' found`);
|
|
277
|
-
score += 20;
|
|
278
|
-
} else {
|
|
279
|
-
mismatches.push(`Expected operation '${expectedOp}' not found`);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Check constraints
|
|
284
|
-
for (const constraint of requested.constraints) {
|
|
285
|
-
if (constraint.startsWith("Avoid:")) {
|
|
286
|
-
const toAvoid = constraint.replace("Avoid:", "").trim().toLowerCase();
|
|
287
|
-
const found = actual.entities.some((e) =>
|
|
288
|
-
e.toLowerCase().includes(toAvoid),
|
|
289
|
-
);
|
|
290
|
-
if (!found) {
|
|
291
|
-
matches.push(`Successfully avoided '${toAvoid}'`);
|
|
292
|
-
score += 10;
|
|
293
|
-
} else {
|
|
294
|
-
mismatches.push(`Constraint violated: used '${toAvoid}'`);
|
|
295
|
-
score -= 20;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
if (constraint.startsWith("Use:")) {
|
|
299
|
-
const toUse = constraint.replace("Use:", "").trim().toLowerCase();
|
|
300
|
-
const found = actual.entities.some((e) =>
|
|
301
|
-
e.toLowerCase().includes(toUse),
|
|
302
|
-
);
|
|
303
|
-
if (found) {
|
|
304
|
-
matches.push(`Required technology '${toUse}' used`);
|
|
305
|
-
score += 15;
|
|
306
|
-
} else {
|
|
307
|
-
mismatches.push(`Required technology '${toUse}' not used`);
|
|
308
|
-
score -= 15;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Baseline score if nothing specific requested but code looks structured
|
|
314
|
-
if (
|
|
315
|
-
requested.expectedEntities.length === 0 &&
|
|
316
|
-
requested.expectedOperations.length === 0 &&
|
|
317
|
-
requested.constraints.length === 0
|
|
318
|
-
) {
|
|
319
|
-
// If request is generic, and we found SOME entities, give it a pass
|
|
320
|
-
if (actual.entities.length > 0) score = 100;
|
|
321
|
-
else score = 50;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return {
|
|
325
|
-
alignmentScore: Math.max(0, Math.min(100, score)),
|
|
326
|
-
matches,
|
|
327
|
-
mismatches,
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Validate code against an intent
|
|
333
|
-
*/
|
|
334
|
-
function validateIntent(code, intent) {
|
|
335
|
-
const issues = [];
|
|
336
|
-
|
|
337
|
-
if (!intent) return { score: 100, issues: [] };
|
|
338
|
-
|
|
339
|
-
const codeIntent = extractCodeIntent(code);
|
|
340
|
-
const requestIntent = parseRequestIntent(intent);
|
|
341
|
-
|
|
342
|
-
// Supplement codeIntent entities with imports for better matching (Bridge enhancement)
|
|
343
|
-
const imports = extractImports(code);
|
|
344
|
-
codeIntent.entities.push(...imports);
|
|
345
|
-
|
|
346
|
-
const comparison = compareIntents(requestIntent, codeIntent);
|
|
347
|
-
|
|
348
|
-
if (comparison.alignmentScore < 60) {
|
|
349
|
-
issues.push({
|
|
350
|
-
severity: "medium",
|
|
351
|
-
type: "Intent Mismatch",
|
|
352
|
-
message: `Code alignment score: ${comparison.alignmentScore}/100. ${comparison.mismatches.join(", ")}`,
|
|
353
|
-
file: "generated-code",
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return {
|
|
358
|
-
score: comparison.alignmentScore,
|
|
359
|
-
issues,
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Static Analysis/Quality Check
|
|
365
|
-
*/
|
|
366
|
-
function validateQuality(code) {
|
|
367
|
-
const issues = [];
|
|
368
|
-
let score = 100;
|
|
369
|
-
|
|
370
|
-
// Check for hardcoded secrets
|
|
371
|
-
if (
|
|
372
|
-
/['"][a-zA-Z0-9]{20,}['"]/.test(code) &&
|
|
373
|
-
(code.includes("key") || code.includes("token") || code.includes("secret"))
|
|
374
|
-
) {
|
|
375
|
-
issues.push({
|
|
376
|
-
severity: "high",
|
|
377
|
-
type: "Security",
|
|
378
|
-
message: "Potential hardcoded secret detected",
|
|
379
|
-
file: "generated-code",
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Check for syntax errors (basic)
|
|
384
|
-
const openBraces = (code.match(/{/g) || []).length;
|
|
385
|
-
const closeBraces = (code.match(/}/g) || []).length;
|
|
386
|
-
if (openBraces !== closeBraces) {
|
|
387
|
-
issues.push({
|
|
388
|
-
severity: "critical",
|
|
389
|
-
type: "Syntax",
|
|
390
|
-
message: "Unbalanced braces detected",
|
|
391
|
-
file: "generated-code",
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Check for console.log
|
|
396
|
-
if (code.includes("console.log")) {
|
|
397
|
-
issues.push({
|
|
398
|
-
severity: "low",
|
|
399
|
-
type: "Quality",
|
|
400
|
-
message: "Console.log statement detected",
|
|
401
|
-
file: "generated-code",
|
|
402
|
-
});
|
|
403
|
-
score -= 5;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
return {
|
|
407
|
-
score: Math.max(0, Math.min(100, score - issues.length * 10)),
|
|
408
|
-
issues,
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
module.exports = {
|
|
413
|
-
runHallucinationCheck,
|
|
414
|
-
validateIntent,
|
|
415
|
-
validateQuality,
|
|
416
|
-
};
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Analysis Core - Shared analysis engine for Ship and Scan
|
|
3
|
-
*
|
|
4
|
-
* Consolidates common logic:
|
|
5
|
-
* - Truthpack generation
|
|
6
|
-
* - Finding collection
|
|
7
|
-
* - Verdict calculation
|
|
8
|
-
* - Output formatting
|
|
9
|
-
*
|
|
10
|
-
* Ship = Fast path (core analyzers only)
|
|
11
|
-
* Scan = Deep path (core + extended analyzers + optional layers)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const path = require("path");
|
|
15
|
-
const fs = require("fs");
|
|
16
|
-
const { buildTruthpack, writeTruthpack, detectFastifyEntry } = require("./truth");
|
|
17
|
-
const {
|
|
18
|
-
findMissingRoutes,
|
|
19
|
-
findEnvGaps,
|
|
20
|
-
findFakeSuccess,
|
|
21
|
-
findGhostAuth,
|
|
22
|
-
findStripeWebhookViolations,
|
|
23
|
-
findPaidSurfaceNotEnforced,
|
|
24
|
-
findOwnerModeBypass
|
|
25
|
-
} = require("./analyzers");
|
|
26
|
-
const { findingsFromReality } = require("./reality-findings");
|
|
27
|
-
|
|
28
|
-
// ============================================================================
|
|
29
|
-
// CORE ANALYSIS (Used by both Ship and Scan)
|
|
30
|
-
// ============================================================================
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Run core analyzers that apply to all projects
|
|
34
|
-
* @param {string} root - Project root path
|
|
35
|
-
* @param {object} truthpack - Generated truthpack
|
|
36
|
-
* @returns {Array} - Array of findings
|
|
37
|
-
*/
|
|
38
|
-
function runCoreAnalyzers(root, truthpack) {
|
|
39
|
-
return [
|
|
40
|
-
...findMissingRoutes(truthpack),
|
|
41
|
-
...findEnvGaps(truthpack),
|
|
42
|
-
...findFakeSuccess(root),
|
|
43
|
-
...findGhostAuth(truthpack, root),
|
|
44
|
-
...findStripeWebhookViolations(truthpack),
|
|
45
|
-
...findPaidSurfaceNotEnforced(truthpack),
|
|
46
|
-
...findOwnerModeBypass(root),
|
|
47
|
-
...findingsFromReality(root)
|
|
48
|
-
];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Calculate verdict from findings
|
|
53
|
-
* @param {Array} findings - Array of findings
|
|
54
|
-
* @returns {string} - SHIP | WARN | BLOCK
|
|
55
|
-
*/
|
|
56
|
-
function calculateVerdict(findings) {
|
|
57
|
-
if (findings.some(f => f.severity === "BLOCK")) return "BLOCK";
|
|
58
|
-
if (findings.some(f => f.severity === "WARN")) return "WARN";
|
|
59
|
-
return "SHIP";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Build proof graph from findings
|
|
64
|
-
* @param {Array} findings - Array of findings
|
|
65
|
-
* @param {object} truthpack - Truthpack data
|
|
66
|
-
* @param {string} root - Project root path
|
|
67
|
-
* @returns {object} - Proof graph
|
|
68
|
-
*/
|
|
69
|
-
function buildProofGraph(findings, truthpack, root) {
|
|
70
|
-
const claims = [];
|
|
71
|
-
let claimId = 0;
|
|
72
|
-
|
|
73
|
-
for (const finding of findings) {
|
|
74
|
-
const claim = {
|
|
75
|
-
id: `claim-${++claimId}`,
|
|
76
|
-
type: getClaimType(finding.category),
|
|
77
|
-
assertion: finding.title || finding.message,
|
|
78
|
-
verified: finding.severity !== 'BLOCK',
|
|
79
|
-
confidence: finding.confidence === 'high' ? 0.9 : finding.confidence === 'medium' ? 0.7 : 0.5,
|
|
80
|
-
evidence: (finding.evidence || []).map((e, i) => ({
|
|
81
|
-
id: `evidence-${claimId}-${i}`,
|
|
82
|
-
type: 'file_citation',
|
|
83
|
-
file: e.file,
|
|
84
|
-
line: parseInt(e.lines?.split('-')[0]) || e.line || 1,
|
|
85
|
-
snippet: e.snippetHash || '',
|
|
86
|
-
strength: 0.8,
|
|
87
|
-
verifiedAt: new Date().toISOString(),
|
|
88
|
-
method: 'static'
|
|
89
|
-
})),
|
|
90
|
-
gaps: [{
|
|
91
|
-
id: `gap-${claimId}`,
|
|
92
|
-
type: getGapType(finding.category),
|
|
93
|
-
description: finding.why || finding.title,
|
|
94
|
-
severity: finding.severity === 'BLOCK' ? 'critical' : finding.severity === 'WARN' ? 'medium' : 'low',
|
|
95
|
-
suggestion: (finding.fixHints || [])[0] || ''
|
|
96
|
-
}],
|
|
97
|
-
severity: finding.severity === 'BLOCK' ? 'critical' : finding.severity === 'WARN' ? 'medium' : 'low',
|
|
98
|
-
file: finding.evidence?.[0]?.file || '',
|
|
99
|
-
line: parseInt(finding.evidence?.[0]?.lines?.split('-')[0]) || 1
|
|
100
|
-
};
|
|
101
|
-
claims.push(claim);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const verifiedClaims = claims.filter(c => c.verified);
|
|
105
|
-
const failedClaims = claims.filter(c => !c.verified);
|
|
106
|
-
const allGaps = claims.flatMap(c => c.gaps);
|
|
107
|
-
|
|
108
|
-
const riskScore = Math.min(100, failedClaims.reduce((sum, c) => {
|
|
109
|
-
if (c.severity === 'critical') return sum + 30;
|
|
110
|
-
if (c.severity === 'high') return sum + 20;
|
|
111
|
-
if (c.severity === 'medium') return sum + 10;
|
|
112
|
-
return sum + 5;
|
|
113
|
-
}, 0));
|
|
114
|
-
|
|
115
|
-
const confidence = claims.length > 0
|
|
116
|
-
? claims.reduce((sum, c) => sum + c.confidence, 0) / claims.length
|
|
117
|
-
: 1.0;
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
version: '1.0.0',
|
|
121
|
-
generatedAt: new Date().toISOString(),
|
|
122
|
-
projectPath: root,
|
|
123
|
-
claims,
|
|
124
|
-
summary: {
|
|
125
|
-
totalClaims: claims.length,
|
|
126
|
-
verifiedClaims: verifiedClaims.length,
|
|
127
|
-
failedClaims: failedClaims.length,
|
|
128
|
-
gaps: allGaps.length,
|
|
129
|
-
riskScore,
|
|
130
|
-
confidence
|
|
131
|
-
},
|
|
132
|
-
verdict: calculateVerdict(findings),
|
|
133
|
-
topBlockers: failedClaims.filter(c => c.severity === 'critical' || c.severity === 'high').slice(0, 5),
|
|
134
|
-
topGaps: allGaps.filter(g => g.severity === 'critical' || g.severity === 'high').slice(0, 5)
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function getClaimType(category) {
|
|
139
|
-
const map = {
|
|
140
|
-
'MissingRoute': 'route_exists',
|
|
141
|
-
'EnvContract': 'env_declared',
|
|
142
|
-
'FakeSuccess': 'success_verified',
|
|
143
|
-
'GhostAuth': 'auth_protected',
|
|
144
|
-
'StripeWebhook': 'billing_enforced',
|
|
145
|
-
'PaidSurface': 'billing_enforced',
|
|
146
|
-
'OwnerModeBypass': 'billing_enforced',
|
|
147
|
-
'DeadUI': 'ui_wired'
|
|
148
|
-
};
|
|
149
|
-
return map[category] || 'ui_wired';
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function getGapType(category) {
|
|
153
|
-
const map = {
|
|
154
|
-
'MissingRoute': 'missing_handler',
|
|
155
|
-
'EnvContract': 'missing_verification',
|
|
156
|
-
'FakeSuccess': 'missing_verification',
|
|
157
|
-
'GhostAuth': 'missing_gate',
|
|
158
|
-
'StripeWebhook': 'missing_verification',
|
|
159
|
-
'PaidSurface': 'missing_gate',
|
|
160
|
-
'OwnerModeBypass': 'missing_gate',
|
|
161
|
-
'DeadUI': 'missing_handler'
|
|
162
|
-
};
|
|
163
|
-
return map[category] || 'untested_path';
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ============================================================================
|
|
167
|
-
// UNIFIED ANALYSIS RUNNER
|
|
168
|
-
// ============================================================================
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Run unified analysis (used by both ship and scan)
|
|
172
|
-
* @param {object} options - Analysis options
|
|
173
|
-
* @returns {object} - Analysis result with findings, verdict, proofGraph
|
|
174
|
-
*/
|
|
175
|
-
async function runAnalysis({
|
|
176
|
-
repoRoot = process.cwd(),
|
|
177
|
-
fastifyEntry = null,
|
|
178
|
-
extended = false, // If true, run extended analyzers (scan mode)
|
|
179
|
-
noWrite = false
|
|
180
|
-
} = {}) {
|
|
181
|
-
const root = repoRoot;
|
|
182
|
-
const fastEntry = fastifyEntry || detectFastifyEntry(root);
|
|
183
|
-
|
|
184
|
-
// Build truthpack
|
|
185
|
-
const truthpack = await buildTruthpack({ repoRoot: root, fastifyEntry: fastEntry });
|
|
186
|
-
if (!noWrite) {
|
|
187
|
-
writeTruthpack(root, truthpack);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Run core analyzers
|
|
191
|
-
const findings = runCoreAnalyzers(root, truthpack);
|
|
192
|
-
|
|
193
|
-
// Calculate verdict
|
|
194
|
-
const verdict = calculateVerdict(findings);
|
|
195
|
-
|
|
196
|
-
// Build proof graph
|
|
197
|
-
const proofGraph = buildProofGraph(findings, truthpack, root);
|
|
198
|
-
|
|
199
|
-
// Build report
|
|
200
|
-
const report = {
|
|
201
|
-
meta: {
|
|
202
|
-
generatedAt: new Date().toISOString(),
|
|
203
|
-
verdict,
|
|
204
|
-
mode: extended ? 'scan' : 'ship'
|
|
205
|
-
},
|
|
206
|
-
truthpackHash: truthpack.index?.hashes?.truthpackHash,
|
|
207
|
-
findings,
|
|
208
|
-
proofGraph: {
|
|
209
|
-
summary: proofGraph.summary,
|
|
210
|
-
topBlockers: proofGraph.topBlockers.slice(0, 5),
|
|
211
|
-
topGaps: proofGraph.topGaps.slice(0, 5)
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
// Write outputs
|
|
216
|
-
if (!noWrite) {
|
|
217
|
-
const outDir = path.join(root, ".vibecheck");
|
|
218
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
219
|
-
fs.writeFileSync(path.join(outDir, "last_ship.json"), JSON.stringify(report, null, 2));
|
|
220
|
-
fs.writeFileSync(path.join(outDir, "proof-graph.json"), JSON.stringify(proofGraph, null, 2));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return { report, truthpack, verdict, proofGraph, findings };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// ============================================================================
|
|
227
|
-
// FINDING UTILITIES
|
|
228
|
-
// ============================================================================
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Group findings by category
|
|
232
|
-
*/
|
|
233
|
-
function groupFindings(findings) {
|
|
234
|
-
const groups = {};
|
|
235
|
-
for (const f of findings) {
|
|
236
|
-
const cat = f.category || 'Other';
|
|
237
|
-
if (!groups[cat]) groups[cat] = [];
|
|
238
|
-
groups[cat].push(f);
|
|
239
|
-
}
|
|
240
|
-
return groups;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Count findings by severity
|
|
245
|
-
*/
|
|
246
|
-
function countBySeverity(findings) {
|
|
247
|
-
return {
|
|
248
|
-
block: findings.filter(f => f.severity === 'BLOCK').length,
|
|
249
|
-
warn: findings.filter(f => f.severity === 'WARN').length,
|
|
250
|
-
info: findings.filter(f => f.severity === 'INFO').length
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Get top N blockers
|
|
256
|
-
*/
|
|
257
|
-
function getTopBlockers(findings, n = 5) {
|
|
258
|
-
return findings
|
|
259
|
-
.filter(f => f.severity === 'BLOCK')
|
|
260
|
-
.slice(0, n);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
module.exports = {
|
|
264
|
-
runCoreAnalyzers,
|
|
265
|
-
calculateVerdict,
|
|
266
|
-
buildProofGraph,
|
|
267
|
-
runAnalysis,
|
|
268
|
-
groupFindings,
|
|
269
|
-
countBySeverity,
|
|
270
|
-
getTopBlockers
|
|
271
|
-
};
|