@vibecheckai/cli 3.7.0 → 3.9.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/README.md +135 -63
- package/bin/_deprecations.js +447 -19
- package/bin/_router.js +1 -1
- package/bin/registry.js +347 -280
- package/bin/runners/context/generators/cursor-enhanced.js +2439 -0
- package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
- package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
- package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
- package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
- package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
- package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
- package/bin/runners/lib/agent-firewall/index.js +200 -0
- package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
- package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
- package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +634 -0
- package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
- package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
- package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
- package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
- package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
- package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
- package/bin/runners/lib/agent-firewall/interceptor/base.js +7 -3
- package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
- package/bin/runners/lib/agent-firewall/session/index.js +26 -0
- package/bin/runners/lib/artifact-envelope.js +540 -0
- package/bin/runners/lib/auth-shared.js +977 -0
- package/bin/runners/lib/checkpoint.js +941 -0
- package/bin/runners/lib/cleanup/engine.js +571 -0
- package/bin/runners/lib/cleanup/index.js +53 -0
- package/bin/runners/lib/cleanup/output.js +375 -0
- package/bin/runners/lib/cleanup/rules.js +1060 -0
- package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
- package/bin/runners/lib/doctor/failure-signatures.js +526 -0
- package/bin/runners/lib/doctor/fix-script.js +336 -0
- package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
- package/bin/runners/lib/doctor/modules/index.js +62 -3
- package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
- package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
- package/bin/runners/lib/doctor/safe-repair.js +384 -0
- package/bin/runners/lib/engine/ast-cache.js +210 -210
- package/bin/runners/lib/engine/auth-extractor.js +211 -211
- package/bin/runners/lib/engine/billing-extractor.js +112 -112
- package/bin/runners/lib/engine/enforcement-extractor.js +100 -100
- package/bin/runners/lib/engine/env-extractor.js +207 -207
- package/bin/runners/lib/engine/express-extractor.js +208 -208
- package/bin/runners/lib/engine/extractors.js +849 -849
- package/bin/runners/lib/engine/index.js +207 -207
- package/bin/runners/lib/engine/repo-index.js +514 -514
- package/bin/runners/lib/engine/types.js +124 -124
- package/bin/runners/lib/engines/attack-detector.js +1192 -0
- package/bin/runners/lib/entitlements-v2.js +2 -2
- package/bin/runners/lib/missions/briefing.js +427 -0
- package/bin/runners/lib/missions/checkpoint.js +753 -0
- package/bin/runners/lib/missions/hardening.js +851 -0
- package/bin/runners/lib/missions/plan.js +421 -32
- package/bin/runners/lib/missions/safety-gates.js +645 -0
- package/bin/runners/lib/missions/schema.js +478 -0
- package/bin/runners/lib/packs/bundle.js +675 -0
- package/bin/runners/lib/packs/evidence-pack.js +671 -0
- package/bin/runners/lib/packs/pack-factory.js +837 -0
- package/bin/runners/lib/packs/permissions-pack.js +686 -0
- package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
- package/bin/runners/lib/safelist/index.js +96 -0
- package/bin/runners/lib/safelist/integration.js +334 -0
- package/bin/runners/lib/safelist/matcher.js +696 -0
- package/bin/runners/lib/safelist/schema.js +948 -0
- package/bin/runners/lib/safelist/store.js +438 -0
- package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
- package/bin/runners/lib/ship-gate.js +832 -0
- package/bin/runners/lib/ship-manifest.js +1153 -0
- package/bin/runners/lib/ship-output.js +1 -1
- package/bin/runners/lib/unified-cli-output.js +710 -383
- package/bin/runners/lib/upsell.js +3 -3
- package/bin/runners/lib/why-tree.js +650 -0
- package/bin/runners/runAllowlist.js +33 -4
- package/bin/runners/runApprove.js +240 -1122
- package/bin/runners/runAudit.js +692 -0
- package/bin/runners/runAuth.js +325 -29
- package/bin/runners/runCheckpoint.js +442 -494
- package/bin/runners/runCleanup.js +343 -0
- package/bin/runners/runDoctor.js +269 -19
- package/bin/runners/runFix.js +411 -32
- package/bin/runners/runForge.js +411 -0
- package/bin/runners/runIntent.js +906 -0
- package/bin/runners/runKickoff.js +878 -0
- package/bin/runners/runLaunch.js +2000 -0
- package/bin/runners/runLink.js +785 -0
- package/bin/runners/runMcp.js +1741 -837
- package/bin/runners/runPacks.js +2089 -0
- package/bin/runners/runPolish.js +41 -0
- package/bin/runners/runReality.js +178 -1
- package/bin/runners/runSafelist.js +1190 -0
- package/bin/runners/runScan.js +21 -9
- package/bin/runners/runShield.js +1282 -0
- package/bin/runners/runShip.js +395 -16
- package/bin/vibecheck.js +34 -6
- package/mcp-server/README.md +117 -158
- package/mcp-server/handlers/index.ts +2 -2
- package/mcp-server/handlers/tool-handler.ts +50 -11
- package/mcp-server/index.js +16 -0
- package/mcp-server/intent-firewall-interceptor.js +529 -0
- package/mcp-server/lib/executor.ts +5 -5
- package/mcp-server/lib/index.ts +14 -4
- package/mcp-server/lib/sandbox.test.ts +4 -4
- package/mcp-server/lib/sandbox.ts +2 -2
- package/mcp-server/manifest.json +473 -0
- package/mcp-server/package.json +1 -1
- package/mcp-server/registry/tool-registry.js +315 -523
- package/mcp-server/registry/tools.json +442 -428
- package/mcp-server/registry.test.ts +18 -12
- package/mcp-server/tier-auth.js +68 -11
- package/mcp-server/tools-v3.js +70 -16
- package/mcp-server/tsconfig.json +1 -0
- package/package.json +2 -1
- package/bin/runners/runProof.zip +0 -0
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
// bin/runners/lib/missions/plan.js
|
|
2
2
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
3
|
-
// MISSION PLANNING -
|
|
3
|
+
// MISSION PLANNING V2 - Enhanced with dependency-aware grouping, blast radius
|
|
4
|
+
// analysis, and risk-based batching. "Missions, not chaos."
|
|
4
5
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
6
|
|
|
7
|
+
const {
|
|
8
|
+
createMission,
|
|
9
|
+
calculateBlastRadius,
|
|
10
|
+
calculateRiskLevel,
|
|
11
|
+
RISK_LEVEL,
|
|
12
|
+
BLAST_RADIUS,
|
|
13
|
+
} = require('./schema');
|
|
14
|
+
const { templateForMissionType } = require('./templates');
|
|
15
|
+
const {
|
|
16
|
+
ValidationError,
|
|
17
|
+
validateFinding,
|
|
18
|
+
validateOptions,
|
|
19
|
+
getAuditTrail,
|
|
20
|
+
} = require('./hardening');
|
|
21
|
+
|
|
6
22
|
/**
|
|
7
23
|
* Score a finding for priority ordering
|
|
8
24
|
* Enhanced with confidence-based scoring
|
|
@@ -131,54 +147,239 @@ const MISSION_PRIORITY = {
|
|
|
131
147
|
};
|
|
132
148
|
|
|
133
149
|
/**
|
|
134
|
-
*
|
|
150
|
+
* Extract files from findings
|
|
151
|
+
* @param {Array} findings - Findings array
|
|
152
|
+
* @returns {string[]} Unique file paths
|
|
153
|
+
*/
|
|
154
|
+
function extractFilesFromFindings(findings) {
|
|
155
|
+
const files = new Set();
|
|
156
|
+
for (const f of findings) {
|
|
157
|
+
if (f.file) files.add(f.file);
|
|
158
|
+
for (const ev of (f.evidence || [])) {
|
|
159
|
+
if (ev.file) files.add(ev.file);
|
|
160
|
+
if (ev.path) files.add(ev.path);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return Array.from(files);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Build a simple import graph from truthpack
|
|
168
|
+
* @param {object} truthpack - Truthpack object
|
|
169
|
+
* @returns {Map} Map of file -> imported files
|
|
170
|
+
*/
|
|
171
|
+
function buildImportGraph(truthpack) {
|
|
172
|
+
const graph = new Map();
|
|
173
|
+
|
|
174
|
+
// Extract imports from truthpack if available
|
|
175
|
+
const imports = truthpack?.imports || truthpack?.dependencies?.imports || [];
|
|
176
|
+
for (const imp of imports) {
|
|
177
|
+
if (imp.from && imp.to) {
|
|
178
|
+
if (!graph.has(imp.from)) graph.set(imp.from, new Set());
|
|
179
|
+
graph.get(imp.from).add(imp.to);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return graph;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Find connected components in file graph
|
|
188
|
+
* Files that import each other should be grouped together
|
|
189
|
+
* @param {string[]} files - List of files
|
|
190
|
+
* @param {Map} importGraph - Import graph
|
|
191
|
+
* @returns {string[][]} Array of file clusters
|
|
192
|
+
*/
|
|
193
|
+
function findConnectedFileClusters(files, importGraph) {
|
|
194
|
+
const fileSet = new Set(files);
|
|
195
|
+
const visited = new Set();
|
|
196
|
+
const clusters = [];
|
|
197
|
+
|
|
198
|
+
function dfs(file, cluster) {
|
|
199
|
+
if (visited.has(file)) return;
|
|
200
|
+
visited.add(file);
|
|
201
|
+
cluster.push(file);
|
|
202
|
+
|
|
203
|
+
// Check files this one imports
|
|
204
|
+
const imports = importGraph.get(file) || new Set();
|
|
205
|
+
for (const imported of imports) {
|
|
206
|
+
if (fileSet.has(imported)) {
|
|
207
|
+
dfs(imported, cluster);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check files that import this one
|
|
212
|
+
for (const [from, toSet] of importGraph) {
|
|
213
|
+
if (toSet.has(file) && fileSet.has(from)) {
|
|
214
|
+
dfs(from, cluster);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const file of files) {
|
|
220
|
+
if (!visited.has(file)) {
|
|
221
|
+
const cluster = [];
|
|
222
|
+
dfs(file, cluster);
|
|
223
|
+
if (cluster.length > 0) {
|
|
224
|
+
clusters.push(cluster);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return clusters;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Calculate cluster risk score
|
|
234
|
+
* @param {Array} findings - Findings in cluster
|
|
235
|
+
* @returns {number} Risk score 0-100
|
|
236
|
+
*/
|
|
237
|
+
function calculateClusterRisk(findings) {
|
|
238
|
+
let score = 0;
|
|
239
|
+
|
|
240
|
+
for (const f of findings) {
|
|
241
|
+
// Severity contribution
|
|
242
|
+
if (f.severity === 'BLOCK') score += 30;
|
|
243
|
+
else if (f.severity === 'WARN') score += 15;
|
|
244
|
+
else score += 5;
|
|
245
|
+
|
|
246
|
+
// Confidence inverse (lower confidence = higher risk)
|
|
247
|
+
const confidence = f.confidence || 0.5;
|
|
248
|
+
score += Math.round((1 - confidence) * 10);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Normalize by finding count, cap at 100
|
|
252
|
+
return Math.min(100, score);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Create a mission from a finding using the new schema
|
|
135
257
|
* Enhanced with confidence and better metadata
|
|
136
258
|
*/
|
|
137
|
-
function missionFromFinding(f, relatedFindings = []) {
|
|
259
|
+
function missionFromFinding(f, relatedFindings = [], options = {}) {
|
|
138
260
|
const type = CATEGORY_TO_MISSION_TYPE[f.category] || "GENERIC_FIX";
|
|
139
261
|
const allFindingIds = [f.id, ...relatedFindings.map(r => r.id)];
|
|
262
|
+
const allFindings = [f, ...relatedFindings];
|
|
140
263
|
|
|
141
264
|
// Calculate mission confidence based on findings
|
|
142
265
|
const confidences = [f.confidence || 0.5, ...relatedFindings.map(r => r.confidence || 0.5)];
|
|
143
266
|
const avgConfidence = confidences.reduce((a, b) => a + b, 0) / confidences.length;
|
|
144
267
|
|
|
145
|
-
|
|
146
|
-
|
|
268
|
+
// Extract all files from findings
|
|
269
|
+
const allowedFiles = extractFilesFromFindings(allFindings);
|
|
270
|
+
|
|
271
|
+
// Get template for this mission type
|
|
272
|
+
const template = templateForMissionType(type);
|
|
273
|
+
|
|
274
|
+
// Use the new schema to create a proper mission object
|
|
275
|
+
return createMission({
|
|
147
276
|
type,
|
|
148
277
|
title: f.title,
|
|
149
278
|
severity: f.severity,
|
|
150
279
|
category: f.category,
|
|
151
|
-
confidence: avgConfidence,
|
|
152
|
-
successCriteria: [
|
|
153
|
-
`Finding ${f.id} no longer appears in ship results`,
|
|
154
|
-
...(relatedFindings.length > 0 ?
|
|
155
|
-
[`${relatedFindings.length} related finding(s) also resolved`] : []
|
|
156
|
-
)
|
|
157
|
-
],
|
|
158
280
|
targetFindingIds: allFindingIds,
|
|
159
|
-
|
|
160
|
-
|
|
281
|
+
template,
|
|
282
|
+
allowedFiles,
|
|
283
|
+
readOnlyContext: options.readOnlyContext || [],
|
|
284
|
+
confidence: avgConfidence,
|
|
161
285
|
evidence: f.evidence || [],
|
|
162
286
|
file: f.file || null,
|
|
163
|
-
};
|
|
287
|
+
});
|
|
164
288
|
}
|
|
165
289
|
|
|
166
290
|
/**
|
|
167
291
|
* Group related findings that can be fixed together
|
|
168
292
|
* E.g., multiple Dead UI issues in the same file
|
|
293
|
+
* Now with dependency-aware clustering
|
|
169
294
|
*/
|
|
170
|
-
function groupRelatedFindings(findings) {
|
|
295
|
+
function groupRelatedFindings(findings, options = {}) {
|
|
296
|
+
const { importGraph = new Map(), maxClusterRisk = 80 } = options;
|
|
171
297
|
const groups = new Map();
|
|
172
298
|
|
|
299
|
+
// First pass: group by category
|
|
300
|
+
const byCategory = new Map();
|
|
173
301
|
for (const f of findings) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
302
|
+
const cat = f.category || 'Unknown';
|
|
303
|
+
if (!byCategory.has(cat)) byCategory.set(cat, []);
|
|
304
|
+
byCategory.get(cat).push(f);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Second pass: within each category, cluster by file dependencies
|
|
308
|
+
for (const [category, catFindings] of byCategory) {
|
|
309
|
+
const files = extractFilesFromFindings(catFindings);
|
|
310
|
+
const clusters = findConnectedFileClusters(files, importGraph);
|
|
311
|
+
|
|
312
|
+
// Map files to their cluster index
|
|
313
|
+
const fileToCluster = new Map();
|
|
314
|
+
clusters.forEach((cluster, idx) => {
|
|
315
|
+
for (const file of cluster) {
|
|
316
|
+
fileToCluster.set(file, idx);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
177
319
|
|
|
178
|
-
|
|
179
|
-
|
|
320
|
+
// Group findings by their file's cluster
|
|
321
|
+
const clusterGroups = new Map();
|
|
322
|
+
for (const f of catFindings) {
|
|
323
|
+
const file = f.file || f.evidence?.[0]?.file || f.evidence?.[0]?.path || 'unknown';
|
|
324
|
+
const clusterIdx = fileToCluster.get(file) ?? -1;
|
|
325
|
+
const groupKey = `${category}:cluster_${clusterIdx}:${file}`;
|
|
326
|
+
|
|
327
|
+
if (!clusterGroups.has(groupKey)) {
|
|
328
|
+
clusterGroups.set(groupKey, []);
|
|
329
|
+
}
|
|
330
|
+
clusterGroups.get(groupKey).push(f);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Third pass: split groups that exceed risk threshold
|
|
334
|
+
for (const [groupKey, groupFindings] of clusterGroups) {
|
|
335
|
+
const risk = calculateClusterRisk(groupFindings);
|
|
336
|
+
|
|
337
|
+
if (risk > maxClusterRisk && groupFindings.length > 1) {
|
|
338
|
+
// Split into individual findings for high-risk groups
|
|
339
|
+
for (let i = 0; i < groupFindings.length; i++) {
|
|
340
|
+
groups.set(`${groupKey}:split_${i}`, [groupFindings[i]]);
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
groups.set(groupKey, groupFindings);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return groups;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Advanced grouping with blast radius analysis
|
|
353
|
+
* Groups findings while respecting blast radius limits
|
|
354
|
+
*/
|
|
355
|
+
function groupWithBlastRadiusLimit(findings, options = {}) {
|
|
356
|
+
const { maxBlastRadius = 5, importGraph = new Map() } = options;
|
|
357
|
+
const groups = new Map();
|
|
358
|
+
|
|
359
|
+
// Start with basic grouping
|
|
360
|
+
const basicGroups = groupRelatedFindings(findings, { importGraph });
|
|
361
|
+
|
|
362
|
+
// Check each group's blast radius
|
|
363
|
+
for (const [key, groupFindings] of basicGroups) {
|
|
364
|
+
const files = extractFilesFromFindings(groupFindings);
|
|
365
|
+
|
|
366
|
+
if (files.length > maxBlastRadius) {
|
|
367
|
+
// Split into smaller groups by file
|
|
368
|
+
const byFile = new Map();
|
|
369
|
+
for (const f of groupFindings) {
|
|
370
|
+
const file = f.file || f.evidence?.[0]?.file || 'unknown';
|
|
371
|
+
if (!byFile.has(file)) byFile.set(file, []);
|
|
372
|
+
byFile.get(file).push(f);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Create separate groups for each file
|
|
376
|
+
let idx = 0;
|
|
377
|
+
for (const [file, fileFindings] of byFile) {
|
|
378
|
+
groups.set(`${key}:file_${idx++}`, fileFindings);
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
groups.set(key, groupFindings);
|
|
180
382
|
}
|
|
181
|
-
groups.get(groupKey).push(f);
|
|
182
383
|
}
|
|
183
384
|
|
|
184
385
|
return groups;
|
|
@@ -189,11 +390,105 @@ function groupRelatedFindings(findings) {
|
|
|
189
390
|
*
|
|
190
391
|
* @param {Array} findings - List of findings from ship/scan
|
|
191
392
|
* @param {Object} options - Planning options
|
|
393
|
+
* @param {number} [options.maxMissions=12] - Maximum missions to plan
|
|
394
|
+
* @param {boolean} [options.blocksOnlyFirst=true] - Prioritize BLOCK findings
|
|
395
|
+
* @param {boolean} [options.groupRelated=true] - Group related findings
|
|
396
|
+
* @param {object} [options.truthpack] - Truthpack for dependency analysis
|
|
397
|
+
* @param {number} [options.maxBlastRadius=5] - Maximum files per mission
|
|
398
|
+
* @param {number} [options.maxClusterRisk=80] - Maximum cluster risk score
|
|
399
|
+
* @param {number} [options.minConfidence=0] - Minimum confidence threshold
|
|
192
400
|
* @returns {Array} Planned missions
|
|
193
401
|
*/
|
|
194
|
-
function planMissions(findings,
|
|
402
|
+
function planMissions(findings, options = {}) {
|
|
403
|
+
const audit = getAuditTrail();
|
|
404
|
+
|
|
405
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
406
|
+
// INPUT VALIDATION
|
|
407
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
408
|
+
|
|
409
|
+
// Handle null/undefined findings
|
|
410
|
+
if (!findings) {
|
|
411
|
+
audit.warn('plan_missions_no_findings', { findings });
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Handle non-array findings
|
|
416
|
+
if (!Array.isArray(findings)) {
|
|
417
|
+
audit.error('plan_missions_invalid_findings', { type: typeof findings });
|
|
418
|
+
throw new ValidationError('findings must be an array', 'findings', findings);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Handle empty findings
|
|
422
|
+
if (findings.length === 0) {
|
|
423
|
+
audit.info('plan_missions_empty_findings');
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Validate and sanitize options
|
|
428
|
+
const optionsSchema = {
|
|
429
|
+
maxMissions: { type: 'number', default: 12, min: 1, max: 100 },
|
|
430
|
+
blocksOnlyFirst: { type: 'boolean', default: true },
|
|
431
|
+
groupRelated: { type: 'boolean', default: true },
|
|
432
|
+
maxBlastRadius: { type: 'number', default: 5, min: 1, max: 50 },
|
|
433
|
+
maxClusterRisk: { type: 'number', default: 80, min: 0, max: 100 },
|
|
434
|
+
minConfidence: { type: 'number', default: 0, min: 0, max: 1 },
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const validatedOptions = validateOptions(options, optionsSchema);
|
|
438
|
+
if (!validatedOptions.valid) {
|
|
439
|
+
audit.warn('plan_missions_invalid_options', { errors: validatedOptions.errors });
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const {
|
|
443
|
+
maxMissions,
|
|
444
|
+
blocksOnlyFirst,
|
|
445
|
+
groupRelated,
|
|
446
|
+
maxBlastRadius,
|
|
447
|
+
maxClusterRisk,
|
|
448
|
+
minConfidence,
|
|
449
|
+
} = validatedOptions.sanitized;
|
|
450
|
+
|
|
451
|
+
const truthpack = options.truthpack || null;
|
|
452
|
+
|
|
453
|
+
audit.info('plan_missions_start', {
|
|
454
|
+
findingCount: findings.length,
|
|
455
|
+
maxMissions,
|
|
456
|
+
blocksOnlyFirst,
|
|
457
|
+
groupRelated,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Build import graph from truthpack if available
|
|
461
|
+
const importGraph = truthpack ? buildImportGraph(truthpack) : new Map();
|
|
462
|
+
|
|
463
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
464
|
+
// FINDING VALIDATION & FILTERING
|
|
465
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
466
|
+
|
|
467
|
+
// Validate and filter findings
|
|
468
|
+
const validFindings = [];
|
|
469
|
+
let invalidCount = 0;
|
|
470
|
+
|
|
471
|
+
for (const f of findings) {
|
|
472
|
+
const validation = validateFinding(f);
|
|
473
|
+
if (validation.valid) {
|
|
474
|
+
validFindings.push(f);
|
|
475
|
+
} else {
|
|
476
|
+
invalidCount++;
|
|
477
|
+
audit.debug('plan_missions_invalid_finding', { id: f?.id, errors: validation.errors });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (invalidCount > 0) {
|
|
482
|
+
audit.warn('plan_missions_invalid_findings_skipped', { invalidCount, validCount: validFindings.length });
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (validFindings.length === 0) {
|
|
486
|
+
audit.info('plan_missions_no_valid_findings');
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
489
|
+
|
|
195
490
|
// Step 1: Sort by score (severity + confidence + evidence)
|
|
196
|
-
const sorted = [...
|
|
491
|
+
const sorted = [...validFindings].sort((a, b) => scoreFinding(b) - scoreFinding(a));
|
|
197
492
|
|
|
198
493
|
// Step 2: Filter to BLOCKs only if we have them (cost control)
|
|
199
494
|
const hasBlocks = sorted.some(f => f.severity === "BLOCK");
|
|
@@ -217,43 +512,137 @@ function planMissions(findings, { maxMissions = 12, blocksOnlyFirst = true, grou
|
|
|
217
512
|
if (f.severity === "WARN" && seenFingerprints.has(nearDupeKey)) continue;
|
|
218
513
|
seenFingerprints.add(nearDupeKey);
|
|
219
514
|
|
|
515
|
+
// Filter by minimum confidence if specified
|
|
516
|
+
const confidence = f.confidence || 0.5;
|
|
517
|
+
if (confidence < minConfidence) continue;
|
|
518
|
+
|
|
220
519
|
deduplicated.push(f);
|
|
221
520
|
}
|
|
222
521
|
|
|
223
|
-
// Step 4: Group related findings
|
|
522
|
+
// Step 4: Group related findings with enhanced heuristics
|
|
224
523
|
let missions = [];
|
|
225
524
|
|
|
226
525
|
if (groupRelated) {
|
|
227
|
-
|
|
526
|
+
// Use blast radius-aware grouping
|
|
527
|
+
const groups = groupWithBlastRadiusLimit(deduplicated, {
|
|
528
|
+
maxBlastRadius,
|
|
529
|
+
importGraph,
|
|
530
|
+
maxClusterRisk,
|
|
531
|
+
});
|
|
228
532
|
|
|
229
533
|
for (const [groupKey, groupFindings] of groups) {
|
|
230
534
|
// Take the highest severity finding as primary
|
|
231
535
|
const primary = groupFindings[0]; // Already sorted by score
|
|
232
536
|
const related = groupFindings.slice(1, 5); // Limit related findings
|
|
233
537
|
|
|
234
|
-
missions.push(missionFromFinding(primary, related));
|
|
538
|
+
missions.push(missionFromFinding(primary, related, { importGraph }));
|
|
235
539
|
}
|
|
236
540
|
} else {
|
|
237
541
|
missions = deduplicated.map(f => missionFromFinding(f));
|
|
238
542
|
}
|
|
239
543
|
|
|
240
|
-
// Step 5: Sort by priority and
|
|
544
|
+
// Step 5: Sort by priority and risk
|
|
241
545
|
missions.sort((a, b) => {
|
|
242
546
|
const prioA = MISSION_PRIORITY[a.type] || 50;
|
|
243
547
|
const prioB = MISSION_PRIORITY[b.type] || 50;
|
|
244
548
|
if (prioA !== prioB) return prioA - prioB;
|
|
245
549
|
|
|
246
|
-
// Secondary sort by
|
|
247
|
-
|
|
550
|
+
// Secondary sort by risk level (lower risk first for safety)
|
|
551
|
+
const riskOrder = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
552
|
+
const riskA = riskOrder[a.safety?.riskLevel] ?? 1;
|
|
553
|
+
const riskB = riskOrder[b.safety?.riskLevel] ?? 1;
|
|
554
|
+
if (riskA !== riskB) return riskA - riskB;
|
|
555
|
+
|
|
556
|
+
// Tertiary sort by confidence (higher first)
|
|
557
|
+
return (b.safety?.confidence || 0.5) - (a.safety?.confidence || 0.5);
|
|
248
558
|
});
|
|
249
559
|
|
|
250
|
-
|
|
560
|
+
const result = missions.slice(0, maxMissions);
|
|
561
|
+
|
|
562
|
+
// Log planning results
|
|
563
|
+
audit.info('plan_missions_complete', {
|
|
564
|
+
inputFindings: findings.length,
|
|
565
|
+
validFindings: validFindings.length,
|
|
566
|
+
deduplicated: deduplicated.length,
|
|
567
|
+
missions: result.length,
|
|
568
|
+
missionTypes: result.map(m => m.type),
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Plan a single mission for a specific finding
|
|
576
|
+
* @param {object} finding - The finding to create a mission for
|
|
577
|
+
* @param {object} options - Planning options
|
|
578
|
+
* @returns {object} Mission object
|
|
579
|
+
*/
|
|
580
|
+
function planSingleMission(finding, options = {}) {
|
|
581
|
+
return missionFromFinding(finding, [], options);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Get mission statistics
|
|
586
|
+
* @param {Array} missions - Array of missions
|
|
587
|
+
* @returns {object} Statistics object
|
|
588
|
+
*/
|
|
589
|
+
function getMissionStats(missions) {
|
|
590
|
+
const stats = {
|
|
591
|
+
total: missions.length,
|
|
592
|
+
byType: {},
|
|
593
|
+
byRisk: { low: 0, medium: 0, high: 0, critical: 0 },
|
|
594
|
+
byBlastRadius: { low: 0, medium: 0, high: 0 },
|
|
595
|
+
totalFindings: 0,
|
|
596
|
+
avgConfidence: 0,
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
let totalConfidence = 0;
|
|
600
|
+
|
|
601
|
+
for (const m of missions) {
|
|
602
|
+
// By type
|
|
603
|
+
stats.byType[m.type] = (stats.byType[m.type] || 0) + 1;
|
|
604
|
+
|
|
605
|
+
// By risk
|
|
606
|
+
const risk = m.safety?.riskLevel || 'medium';
|
|
607
|
+
stats.byRisk[risk] = (stats.byRisk[risk] || 0) + 1;
|
|
608
|
+
|
|
609
|
+
// By blast radius
|
|
610
|
+
const blast = m.scope?.blastRadius || 'medium';
|
|
611
|
+
stats.byBlastRadius[blast] = (stats.byBlastRadius[blast] || 0) + 1;
|
|
612
|
+
|
|
613
|
+
// Finding count
|
|
614
|
+
stats.totalFindings += m.objective?.findingCount || 1;
|
|
615
|
+
|
|
616
|
+
// Confidence
|
|
617
|
+
totalConfidence += m.safety?.confidence || 0.5;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
stats.avgConfidence = missions.length > 0
|
|
621
|
+
? Math.round((totalConfidence / missions.length) * 100) / 100
|
|
622
|
+
: 0;
|
|
623
|
+
|
|
624
|
+
return stats;
|
|
251
625
|
}
|
|
252
626
|
|
|
253
627
|
module.exports = {
|
|
628
|
+
// Main planning functions
|
|
254
629
|
planMissions,
|
|
630
|
+
planSingleMission,
|
|
631
|
+
getMissionStats,
|
|
632
|
+
|
|
633
|
+
// Grouping functions
|
|
634
|
+
groupRelatedFindings,
|
|
635
|
+
groupWithBlastRadiusLimit,
|
|
636
|
+
|
|
637
|
+
// Utility functions
|
|
255
638
|
scoreFinding,
|
|
256
639
|
generateFingerprint,
|
|
640
|
+
extractFilesFromFindings,
|
|
641
|
+
buildImportGraph,
|
|
642
|
+
findConnectedFileClusters,
|
|
643
|
+
calculateClusterRisk,
|
|
644
|
+
|
|
645
|
+
// Constants
|
|
257
646
|
CATEGORY_TO_MISSION_TYPE,
|
|
258
|
-
MISSION_PRIORITY
|
|
647
|
+
MISSION_PRIORITY,
|
|
259
648
|
};
|