@vibecheckai/cli 3.2.6 → 3.3.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/bin/registry.js +192 -5
- package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
- package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
- package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
- package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
- package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
- package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
- package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
- package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
- package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
- package/bin/runners/lib/agent-firewall/logger.js +141 -0
- package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
- package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
- package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
- package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
- package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
- package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
- package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
- package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
- package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
- package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
- package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
- package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
- package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
- package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
- package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
- package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
- package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
- package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
- package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
- package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
- package/bin/runners/lib/analyzers.js +81 -18
- package/bin/runners/lib/authority-badge.js +425 -0
- package/bin/runners/lib/cli-output.js +7 -1
- package/bin/runners/lib/error-handler.js +16 -9
- package/bin/runners/lib/exit-codes.js +275 -0
- package/bin/runners/lib/global-flags.js +37 -0
- package/bin/runners/lib/help-formatter.js +413 -0
- package/bin/runners/lib/logger.js +38 -0
- package/bin/runners/lib/unified-cli-output.js +604 -0
- package/bin/runners/lib/upsell.js +148 -0
- package/bin/runners/runApprove.js +1200 -0
- package/bin/runners/runAuth.js +324 -95
- package/bin/runners/runCheckpoint.js +39 -21
- package/bin/runners/runClassify.js +859 -0
- package/bin/runners/runContext.js +136 -24
- package/bin/runners/runDoctor.js +108 -68
- package/bin/runners/runFix.js +6 -5
- package/bin/runners/runGuard.js +212 -118
- package/bin/runners/runInit.js +3 -2
- package/bin/runners/runMcp.js +130 -52
- package/bin/runners/runPolish.js +43 -20
- package/bin/runners/runProve.js +1 -2
- package/bin/runners/runReport.js +3 -2
- package/bin/runners/runScan.js +63 -44
- package/bin/runners/runShip.js +3 -4
- package/bin/runners/runValidate.js +19 -2
- package/bin/runners/runWatch.js +104 -53
- package/bin/vibecheck.js +106 -19
- package/mcp-server/HARDENING_SUMMARY.md +299 -0
- package/mcp-server/agent-firewall-interceptor.js +367 -31
- package/mcp-server/authority-tools.js +569 -0
- package/mcp-server/conductor/conflict-resolver.js +588 -0
- package/mcp-server/conductor/execution-planner.js +544 -0
- package/mcp-server/conductor/index.js +377 -0
- package/mcp-server/conductor/lock-manager.js +615 -0
- package/mcp-server/conductor/request-queue.js +550 -0
- package/mcp-server/conductor/session-manager.js +500 -0
- package/mcp-server/conductor/tools.js +510 -0
- package/mcp-server/index.js +1149 -243
- package/mcp-server/lib/{api-client.js → api-client.cjs} +40 -4
- package/mcp-server/lib/logger.cjs +30 -0
- package/mcp-server/logger.js +173 -0
- package/mcp-server/package.json +2 -2
- package/mcp-server/premium-tools.js +2 -2
- package/mcp-server/tier-auth.js +245 -35
- package/mcp-server/truth-firewall-tools.js +145 -15
- package/mcp-server/vibecheck-tools.js +2 -2
- package/package.json +2 -3
- package/mcp-server/index.old.js +0 -4137
- package/mcp-server/package-lock.json +0 -165
|
@@ -0,0 +1,1200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibecheck approve - Authority Approval Command
|
|
3
|
+
*
|
|
4
|
+
* Execute authorities to get structured verdicts with proofs.
|
|
5
|
+
* Requires STARTER tier for advisory verdicts.
|
|
6
|
+
* Requires PRO tier for enforcement.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* vibecheck approve <authority-id>
|
|
10
|
+
* vibecheck approve safe-consolidation
|
|
11
|
+
* vibecheck approve --list
|
|
12
|
+
*
|
|
13
|
+
* Part of the Authority System - "The AI That Says No"
|
|
14
|
+
*
|
|
15
|
+
* Production Features:
|
|
16
|
+
* - Rate limiting to prevent abuse
|
|
17
|
+
* - Input validation for all parameters
|
|
18
|
+
* - Timeout enforcement for long-running analyses
|
|
19
|
+
* - Audit trail for all executions
|
|
20
|
+
* - Signed verdicts (PRO tier)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const path = require("path");
|
|
24
|
+
const fs = require("fs");
|
|
25
|
+
const { withErrorHandling, createUserError } = require("./lib/error-handler");
|
|
26
|
+
const { parseGlobalFlags, shouldShowBanner } = require("./lib/global-flags");
|
|
27
|
+
const { EXIT } = require("./lib/exit-codes");
|
|
28
|
+
|
|
29
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
30
|
+
// TERMINAL UI
|
|
31
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
ansi,
|
|
35
|
+
colors,
|
|
36
|
+
Spinner,
|
|
37
|
+
} = require("./lib/terminal-ui");
|
|
38
|
+
|
|
39
|
+
const BANNER = `
|
|
40
|
+
${ansi.rgb(0, 200, 255)} ██╗ ██╗██╗██████╗ ███████╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗${ansi.reset}
|
|
41
|
+
${ansi.rgb(30, 180, 255)} ██║ ██║██║██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝${ansi.reset}
|
|
42
|
+
${ansi.rgb(60, 160, 255)} ██║ ██║██║██████╔╝█████╗ ██║ ███████║█████╗ ██║ █████╔╝ ${ansi.reset}
|
|
43
|
+
${ansi.rgb(90, 140, 255)} ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ${ansi.reset}
|
|
44
|
+
${ansi.rgb(120, 120, 255)} ╚████╔╝ ██║██████╔╝███████╗╚██████╗██║ ██║███████╗╚██████╗██║ ██╗${ansi.reset}
|
|
45
|
+
${ansi.rgb(150, 100, 255)} ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝${ansi.reset}
|
|
46
|
+
|
|
47
|
+
${ansi.dim} ┌─────────────────────────────────────────────────────────────────────┐${ansi.reset}
|
|
48
|
+
${ansi.dim} │${ansi.reset} ${ansi.rgb(255, 255, 255)}${ansi.bold}Authority System${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(200, 200, 200)}Approve${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(150, 150, 150)}Verdicts with Proofs${ansi.reset} ${ansi.dim}│${ansi.reset}
|
|
49
|
+
${ansi.dim} └─────────────────────────────────────────────────────────────────────┘${ansi.reset}
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
function printBanner() {
|
|
53
|
+
console.log(BANNER);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
57
|
+
// ARGS PARSER
|
|
58
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
59
|
+
|
|
60
|
+
function parseArgs(args) {
|
|
61
|
+
const { flags: globalFlags, cleanArgs } = parseGlobalFlags(args);
|
|
62
|
+
|
|
63
|
+
const opts = {
|
|
64
|
+
path: globalFlags.path || process.cwd(),
|
|
65
|
+
json: globalFlags.json || false,
|
|
66
|
+
verbose: globalFlags.verbose || false,
|
|
67
|
+
help: globalFlags.help || false,
|
|
68
|
+
noBanner: globalFlags.noBanner || false,
|
|
69
|
+
ci: globalFlags.ci || false,
|
|
70
|
+
quiet: globalFlags.quiet || false,
|
|
71
|
+
// Authority options
|
|
72
|
+
authority: null, // Authority ID to execute
|
|
73
|
+
list: false, // List available authorities
|
|
74
|
+
// Output options
|
|
75
|
+
output: null, // Output file path
|
|
76
|
+
badge: false, // Generate badge for PROCEED
|
|
77
|
+
// Dry run
|
|
78
|
+
dryRun: false, // Don't save results, just analyze
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < cleanArgs.length; i++) {
|
|
82
|
+
const arg = cleanArgs[i];
|
|
83
|
+
|
|
84
|
+
if (arg === '--list' || arg === '-l') opts.list = true;
|
|
85
|
+
else if (arg === '--output' || arg === '-o') opts.output = cleanArgs[++i];
|
|
86
|
+
else if (arg === '--badge' || arg === '-b') opts.badge = true;
|
|
87
|
+
else if (arg === '--dry-run') opts.dryRun = true;
|
|
88
|
+
else if (arg === '--path' || arg === '-p') opts.path = cleanArgs[++i] || process.cwd();
|
|
89
|
+
else if (arg.startsWith('--path=')) opts.path = arg.split('=')[1];
|
|
90
|
+
else if (!arg.startsWith('-') && !opts.authority) opts.authority = arg;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return opts;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function printHelp(showBanner = true) {
|
|
97
|
+
if (showBanner && shouldShowBanner({})) {
|
|
98
|
+
printBanner();
|
|
99
|
+
}
|
|
100
|
+
console.log(`
|
|
101
|
+
${ansi.bold}USAGE${ansi.reset}
|
|
102
|
+
${colors.accent}vibecheck approve${ansi.reset} <authority> [options]
|
|
103
|
+
${colors.accent}vibecheck approve${ansi.reset} --list
|
|
104
|
+
|
|
105
|
+
${ansi.dim}Requires: STARTER tier (advisory) or PRO tier (enforcement)${ansi.reset}
|
|
106
|
+
|
|
107
|
+
Execute an authority to get a structured verdict with proofs.
|
|
108
|
+
Authorities are policy engines that analyze your code and produce
|
|
109
|
+
PROCEED/STOP/DEFER verdicts with evidence.
|
|
110
|
+
|
|
111
|
+
${ansi.bold}AUTHORITIES${ansi.reset}
|
|
112
|
+
${colors.accent}safe-consolidation${ansi.reset} Zero-behavior-change code cleanup
|
|
113
|
+
${colors.accent}inventory${ansi.reset} Read-only duplication/legacy maps ${ansi.dim}(use 'classify')${ansi.reset}
|
|
114
|
+
|
|
115
|
+
${ansi.bold}OPTIONS${ansi.reset}
|
|
116
|
+
${colors.accent}--list, -l${ansi.reset} List all available authorities
|
|
117
|
+
${colors.accent}--badge, -b${ansi.reset} Generate badge for PROCEED verdicts
|
|
118
|
+
${colors.accent}--dry-run${ansi.reset} Analyze without saving results
|
|
119
|
+
${colors.accent}--output, -o <file>${ansi.reset} Save verdict to file
|
|
120
|
+
|
|
121
|
+
${ansi.bold}GLOBAL OPTIONS${ansi.reset}
|
|
122
|
+
${colors.accent}--json${ansi.reset} Output verdict as JSON
|
|
123
|
+
${colors.accent}--path, -p <dir>${ansi.reset} Run in specified directory
|
|
124
|
+
${colors.accent}--verbose, -v${ansi.reset} Show detailed analysis
|
|
125
|
+
${colors.accent}--quiet, -q${ansi.reset} Suppress non-essential output
|
|
126
|
+
${colors.accent}--ci${ansi.reset} CI mode (quiet + exit codes)
|
|
127
|
+
${colors.accent}--help, -h${ansi.reset} Show this help
|
|
128
|
+
|
|
129
|
+
${ansi.bold}💡 EXAMPLES${ansi.reset}
|
|
130
|
+
|
|
131
|
+
${ansi.dim}# Run safe-consolidation authority${ansi.reset}
|
|
132
|
+
vibecheck approve safe-consolidation
|
|
133
|
+
|
|
134
|
+
${ansi.dim}# JSON output for CI integration${ansi.reset}
|
|
135
|
+
vibecheck approve safe-consolidation --json --ci
|
|
136
|
+
|
|
137
|
+
${ansi.dim}# Generate badge for PROCEED verdicts${ansi.reset}
|
|
138
|
+
vibecheck approve safe-consolidation --badge
|
|
139
|
+
|
|
140
|
+
${ansi.dim}# List available authorities${ansi.reset}
|
|
141
|
+
vibecheck approve --list
|
|
142
|
+
|
|
143
|
+
${ansi.bold}📊 VERDICT ACTIONS${ansi.reset}
|
|
144
|
+
${colors.success}PROCEED${ansi.reset} Safe to continue, all proofs satisfied (exit 0)
|
|
145
|
+
${colors.warning}DEFER${ansi.reset} Manual review required (exit 1)
|
|
146
|
+
${colors.error}STOP${ansi.reset} Unsafe, hard stops triggered (exit 2)
|
|
147
|
+
|
|
148
|
+
${ansi.bold}🔗 RELATED COMMANDS${ansi.reset}
|
|
149
|
+
${colors.accent}vibecheck classify${ansi.reset} Read-only inventory ${ansi.dim}(FREE)${ansi.reset}
|
|
150
|
+
${colors.accent}vibecheck scan${ansi.reset} Full code analysis
|
|
151
|
+
${colors.accent}vibecheck ship${ansi.reset} SHIP/WARN/BLOCK verdict
|
|
152
|
+
|
|
153
|
+
${ansi.dim}─────────────────────────────────────────────────────────────${ansi.reset}
|
|
154
|
+
${ansi.dim}Documentation: https://docs.vibecheckai.dev/cli/approve${ansi.reset}
|
|
155
|
+
`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
159
|
+
// AUTHORITY DEFINITIONS (Inline for now - will use packages/core in production)
|
|
160
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
161
|
+
|
|
162
|
+
const AUTHORITIES = {
|
|
163
|
+
'safe-consolidation': {
|
|
164
|
+
id: 'safe-consolidation',
|
|
165
|
+
version: '1.0.0',
|
|
166
|
+
description: 'Zero-behavior-change cleanup of duplicated and legacy code',
|
|
167
|
+
tier: 'starter',
|
|
168
|
+
scope: {
|
|
169
|
+
allowedActions: ['analyze', 'propose_diff'],
|
|
170
|
+
disallowedActions: ['delete', 'rename_public_exports', 'force_publish', 'modify_env', 'modify_migrations'],
|
|
171
|
+
allowedRisk: ['LOW'],
|
|
172
|
+
excludedPaths: [
|
|
173
|
+
'packages/cli/**',
|
|
174
|
+
'**/migrations/**',
|
|
175
|
+
'**/prisma/migrations/**',
|
|
176
|
+
'**/*.env*',
|
|
177
|
+
'**/node_modules/**',
|
|
178
|
+
'**/dist/**',
|
|
179
|
+
'**/build/**',
|
|
180
|
+
'**/.git/**',
|
|
181
|
+
'**/auth/**',
|
|
182
|
+
'**/secrets/**',
|
|
183
|
+
'**/billing/**',
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
permissions: {
|
|
187
|
+
readRepo: true,
|
|
188
|
+
writeProposals: true,
|
|
189
|
+
applyChanges: false,
|
|
190
|
+
enforceCI: true,
|
|
191
|
+
},
|
|
192
|
+
requiredProofs: {
|
|
193
|
+
reachability: 'Static + dynamic analysis proving no runtime imports',
|
|
194
|
+
compatibility: 'Import path preservation via aliases/re-exports',
|
|
195
|
+
behaviorLock: 'Observable behavior unchanged (errors, timing, side effects)',
|
|
196
|
+
rollback: 'Single git revert restores previous state',
|
|
197
|
+
},
|
|
198
|
+
hardStops: {
|
|
199
|
+
dynamicImportDetected: true,
|
|
200
|
+
migrationFlagDetected: true,
|
|
201
|
+
behaviorChangeUncertain: true,
|
|
202
|
+
securitySensitive: true,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
'security-remediation': {
|
|
206
|
+
id: 'security-remediation',
|
|
207
|
+
version: '1.0.0',
|
|
208
|
+
description: 'Verified security fixes with proof of safety',
|
|
209
|
+
tier: 'pro',
|
|
210
|
+
scope: {
|
|
211
|
+
allowedActions: ['analyze', 'propose_diff'],
|
|
212
|
+
disallowedActions: [
|
|
213
|
+
'delete_auth_code',
|
|
214
|
+
'modify_crypto_primitives',
|
|
215
|
+
'alter_permission_checks',
|
|
216
|
+
'remove_rate_limits',
|
|
217
|
+
'expose_internal_apis',
|
|
218
|
+
],
|
|
219
|
+
allowedRisk: ['LOW', 'MEDIUM'],
|
|
220
|
+
excludedPaths: [
|
|
221
|
+
'**/node_modules/**',
|
|
222
|
+
'**/dist/**',
|
|
223
|
+
'**/build/**',
|
|
224
|
+
'**/.git/**',
|
|
225
|
+
'**/migrations/**',
|
|
226
|
+
'**/*.env*',
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
permissions: {
|
|
230
|
+
readRepo: true,
|
|
231
|
+
writeProposals: true,
|
|
232
|
+
applyChanges: false,
|
|
233
|
+
enforceCI: true,
|
|
234
|
+
},
|
|
235
|
+
requiredProofs: {
|
|
236
|
+
reachability: 'Proof that vulnerable code path is reachable and fix addresses it',
|
|
237
|
+
compatibility: 'Proof that fix does not break existing functionality',
|
|
238
|
+
behaviorLock: 'Proof that security behavior is strengthened, not weakened',
|
|
239
|
+
rollback: 'Proof that fix can be reverted without leaving system vulnerable',
|
|
240
|
+
},
|
|
241
|
+
hardStops: {
|
|
242
|
+
dynamicImportDetected: true,
|
|
243
|
+
migrationFlagDetected: false,
|
|
244
|
+
behaviorChangeUncertain: true,
|
|
245
|
+
securitySensitive: false,
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
'inventory': {
|
|
249
|
+
id: 'inventory',
|
|
250
|
+
version: '1.0.0',
|
|
251
|
+
description: 'Read-only inventory of duplication and legacy code',
|
|
252
|
+
tier: 'free',
|
|
253
|
+
note: 'Use "vibecheck classify" for this authority',
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
258
|
+
// TIER CHECKING
|
|
259
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
260
|
+
|
|
261
|
+
async function checkTierAccess(authorityTier) {
|
|
262
|
+
try {
|
|
263
|
+
const entitlementsV2 = require("./lib/entitlements-v2");
|
|
264
|
+
const access = await entitlementsV2.enforce(`authority.${authorityTier}`, {
|
|
265
|
+
silent: true,
|
|
266
|
+
});
|
|
267
|
+
return access;
|
|
268
|
+
} catch (err) {
|
|
269
|
+
// Fallback to free tier if entitlements fail
|
|
270
|
+
return {
|
|
271
|
+
allowed: authorityTier === 'free',
|
|
272
|
+
tier: 'free',
|
|
273
|
+
reason: authorityTier === 'free' ? 'Free tier access' : 'Upgrade required',
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
279
|
+
// SAFE CONSOLIDATION EXECUTOR
|
|
280
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Check for unsafe patterns in file content
|
|
284
|
+
*/
|
|
285
|
+
function checkUnsafePatterns(content) {
|
|
286
|
+
const patterns = [
|
|
287
|
+
{ pattern: /import\s*\(/, reason: 'Dynamic import detected' },
|
|
288
|
+
{ pattern: /require\s*\((?!['"])/, reason: 'Dynamic require detected' },
|
|
289
|
+
{ pattern: /process\.env\./, reason: 'Environment-dependent behavior' },
|
|
290
|
+
{ pattern: /FEATURE_FLAG/i, reason: 'Feature flag detected' },
|
|
291
|
+
{ pattern: /feature\s*toggle/i, reason: 'Feature toggle detected' },
|
|
292
|
+
{ pattern: /eval\s*\(/, reason: 'Eval detected' },
|
|
293
|
+
{ pattern: /Function\s*\(/, reason: 'Dynamic function construction' },
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
const triggered = [];
|
|
297
|
+
for (const { pattern, reason } of patterns) {
|
|
298
|
+
if (pattern.test(content)) {
|
|
299
|
+
triggered.push(reason);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return triggered;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Check if file is in excluded paths
|
|
308
|
+
*/
|
|
309
|
+
function isExcludedPath(filePath, excludedPaths) {
|
|
310
|
+
const minimatch = require('minimatch');
|
|
311
|
+
|
|
312
|
+
for (const pattern of excludedPaths) {
|
|
313
|
+
if (minimatch(filePath, pattern, { matchBase: true })) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Analyze files for safe consolidation
|
|
323
|
+
*/
|
|
324
|
+
async function analyzeSafeConsolidation(projectPath, authority, opts, spinner) {
|
|
325
|
+
const startTime = Date.now();
|
|
326
|
+
const findings = {
|
|
327
|
+
safeToConsolidate: [],
|
|
328
|
+
needsReview: [],
|
|
329
|
+
blocked: [],
|
|
330
|
+
};
|
|
331
|
+
const hardStopsTriggered = [];
|
|
332
|
+
const filesTouched = [];
|
|
333
|
+
|
|
334
|
+
// Get inventory first
|
|
335
|
+
const { runClassify } = require("./runClassify");
|
|
336
|
+
|
|
337
|
+
spinner.update('Running inventory analysis...');
|
|
338
|
+
|
|
339
|
+
// Run classify in quiet mode to get data
|
|
340
|
+
const inventoryResult = await runInventoryForApprove(projectPath, opts);
|
|
341
|
+
|
|
342
|
+
spinner.update('Analyzing consolidation safety...');
|
|
343
|
+
|
|
344
|
+
// Analyze each duplicate group
|
|
345
|
+
for (const dupGroup of inventoryResult.duplicationMap) {
|
|
346
|
+
const primary = dupGroup.primary;
|
|
347
|
+
filesTouched.push(primary);
|
|
348
|
+
|
|
349
|
+
// Skip if in excluded paths
|
|
350
|
+
if (isExcludedPath(primary, authority.scope.excludedPaths)) {
|
|
351
|
+
findings.blocked.push({
|
|
352
|
+
file: primary,
|
|
353
|
+
reason: 'In excluded path',
|
|
354
|
+
recommendation: 'No action needed - file is intentionally excluded',
|
|
355
|
+
});
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Read primary file content
|
|
360
|
+
const primaryPath = path.join(projectPath, primary);
|
|
361
|
+
let content = '';
|
|
362
|
+
try {
|
|
363
|
+
content = await fs.promises.readFile(primaryPath, 'utf-8');
|
|
364
|
+
} catch (err) {
|
|
365
|
+
findings.blocked.push({
|
|
366
|
+
file: primary,
|
|
367
|
+
reason: 'Unable to read file',
|
|
368
|
+
recommendation: 'Check file permissions',
|
|
369
|
+
});
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Check for unsafe patterns
|
|
374
|
+
const unsafeReasons = checkUnsafePatterns(content);
|
|
375
|
+
if (unsafeReasons.length > 0) {
|
|
376
|
+
findings.blocked.push({
|
|
377
|
+
file: primary,
|
|
378
|
+
reason: unsafeReasons.join(', '),
|
|
379
|
+
recommendation: 'Manual review required - contains patterns that prevent safe automated consolidation',
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
if (authority.hardStops.dynamicImportDetected && unsafeReasons.some(r => r.includes('import'))) {
|
|
383
|
+
hardStopsTriggered.push(`Dynamic import in ${primary}`);
|
|
384
|
+
}
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Check similarity level
|
|
389
|
+
if (dupGroup.similarity >= 0.95) {
|
|
390
|
+
findings.safeToConsolidate.push({
|
|
391
|
+
primary: primary,
|
|
392
|
+
duplicates: dupGroup.duplicates,
|
|
393
|
+
similarity: dupGroup.similarity,
|
|
394
|
+
type: dupGroup.type,
|
|
395
|
+
action: 'Create re-export from duplicates to primary',
|
|
396
|
+
linesSaved: dupGroup.lineCount * dupGroup.duplicates.length,
|
|
397
|
+
});
|
|
398
|
+
} else {
|
|
399
|
+
findings.needsReview.push({
|
|
400
|
+
file: primary,
|
|
401
|
+
duplicates: dupGroup.duplicates,
|
|
402
|
+
similarity: dupGroup.similarity,
|
|
403
|
+
reason: `Similarity ${Math.round(dupGroup.similarity * 100)}% below threshold for auto-consolidation`,
|
|
404
|
+
recommendation: 'Review differences manually before consolidating',
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Analyze legacy code
|
|
410
|
+
for (const legacyEntry of inventoryResult.legacyMap) {
|
|
411
|
+
filesTouched.push(legacyEntry.file);
|
|
412
|
+
|
|
413
|
+
if (isExcludedPath(legacyEntry.file, authority.scope.excludedPaths)) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (legacyEntry.type === 'backup' && legacyEntry.confidence >= 0.9) {
|
|
418
|
+
findings.safeToConsolidate.push({
|
|
419
|
+
primary: legacyEntry.file,
|
|
420
|
+
duplicates: [],
|
|
421
|
+
type: 'legacy-backup',
|
|
422
|
+
action: 'Archive or remove backup file',
|
|
423
|
+
evidence: legacyEntry.evidence,
|
|
424
|
+
});
|
|
425
|
+
} else {
|
|
426
|
+
findings.needsReview.push({
|
|
427
|
+
file: legacyEntry.file,
|
|
428
|
+
type: legacyEntry.type,
|
|
429
|
+
confidence: legacyEntry.confidence,
|
|
430
|
+
reason: 'Legacy code - requires manual review',
|
|
431
|
+
recommendation: 'Verify no active references before removal',
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Generate proofs
|
|
437
|
+
const proofs = {
|
|
438
|
+
reachability: hardStopsTriggered.length > 0
|
|
439
|
+
? `FAILED: ${hardStopsTriggered.join('; ')}`
|
|
440
|
+
: 'PASSED: No dynamic imports detected in safe files',
|
|
441
|
+
compatibility: findings.safeToConsolidate.length > 0
|
|
442
|
+
? 'PASSED: Re-exports preserve import paths'
|
|
443
|
+
: 'N/A: No consolidations proposed',
|
|
444
|
+
rollback: 'PASSED: Single git revert restores state',
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// Determine verdict
|
|
448
|
+
let action = 'PROCEED';
|
|
449
|
+
let confidence = 0.95;
|
|
450
|
+
|
|
451
|
+
if (hardStopsTriggered.length > 0) {
|
|
452
|
+
action = 'STOP';
|
|
453
|
+
confidence = 1.0;
|
|
454
|
+
} else if (findings.needsReview.length > findings.safeToConsolidate.length) {
|
|
455
|
+
action = 'DEFER';
|
|
456
|
+
confidence = 0.6;
|
|
457
|
+
} else if (findings.blocked.length > 0) {
|
|
458
|
+
action = 'DEFER';
|
|
459
|
+
confidence = 0.7;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
authority: authority.id,
|
|
464
|
+
version: authority.version,
|
|
465
|
+
timestamp: new Date().toISOString(),
|
|
466
|
+
action,
|
|
467
|
+
riskLevel: action === 'STOP' ? 'HIGH' : action === 'DEFER' ? 'MEDIUM' : 'LOW',
|
|
468
|
+
exitCode: action === 'PROCEED' ? 0 : action === 'DEFER' ? 1 : 2,
|
|
469
|
+
filesTouched,
|
|
470
|
+
proofs,
|
|
471
|
+
behaviorChange: action === 'STOP',
|
|
472
|
+
confidence,
|
|
473
|
+
hardStopsTriggered,
|
|
474
|
+
notes: generateNotes(action, findings, hardStopsTriggered),
|
|
475
|
+
analysis: {
|
|
476
|
+
safeToConsolidate: findings.safeToConsolidate,
|
|
477
|
+
needsReview: findings.needsReview,
|
|
478
|
+
blocked: findings.blocked,
|
|
479
|
+
summary: {
|
|
480
|
+
safeCount: findings.safeToConsolidate.length,
|
|
481
|
+
reviewCount: findings.needsReview.length,
|
|
482
|
+
blockedCount: findings.blocked.length,
|
|
483
|
+
totalLinesSavings: findings.safeToConsolidate.reduce((sum, f) => sum + (f.linesSaved || 0), 0),
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
analysisTimeMs: Date.now() - startTime,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Run inventory analysis for approve command
|
|
492
|
+
*/
|
|
493
|
+
async function runInventoryForApprove(projectPath, opts) {
|
|
494
|
+
const { runClassify } = require("./runClassify");
|
|
495
|
+
|
|
496
|
+
// Create a minimal spinner that captures output
|
|
497
|
+
const inventoryOpts = {
|
|
498
|
+
path: projectPath,
|
|
499
|
+
json: true,
|
|
500
|
+
quiet: true,
|
|
501
|
+
includeNear: true,
|
|
502
|
+
includeSemantic: false,
|
|
503
|
+
threshold: 0.8,
|
|
504
|
+
maxFiles: 5000,
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// Directly run the inventory analysis logic
|
|
508
|
+
const crypto = require("crypto");
|
|
509
|
+
|
|
510
|
+
const EXCLUDED_DIRS = new Set([
|
|
511
|
+
'node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.vibecheck',
|
|
512
|
+
]);
|
|
513
|
+
|
|
514
|
+
async function findFiles(rootPath, maxFiles) {
|
|
515
|
+
const files = [];
|
|
516
|
+
const extensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
|
517
|
+
|
|
518
|
+
async function walk(dir, depth = 0) {
|
|
519
|
+
if (depth > 20 || files.length >= maxFiles) return;
|
|
520
|
+
try {
|
|
521
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
522
|
+
for (const entry of entries) {
|
|
523
|
+
if (files.length >= maxFiles) break;
|
|
524
|
+
const fullPath = path.join(dir, entry.name);
|
|
525
|
+
const relativePath = path.relative(rootPath, fullPath);
|
|
526
|
+
if (entry.isDirectory()) {
|
|
527
|
+
if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
528
|
+
await walk(fullPath, depth + 1);
|
|
529
|
+
}
|
|
530
|
+
} else if (entry.isFile()) {
|
|
531
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
532
|
+
if (extensions.has(ext)) {
|
|
533
|
+
files.push({ path: fullPath, relativePath, name: entry.name, ext });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
} catch (err) { /* skip */ }
|
|
538
|
+
}
|
|
539
|
+
await walk(rootPath);
|
|
540
|
+
return files;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const files = await findFiles(projectPath, 5000);
|
|
544
|
+
const fileContents = new Map();
|
|
545
|
+
const fileHashes = new Map();
|
|
546
|
+
|
|
547
|
+
for (const file of files) {
|
|
548
|
+
try {
|
|
549
|
+
const content = await fs.promises.readFile(file.path, 'utf-8');
|
|
550
|
+
const lines = content.split('\n').length;
|
|
551
|
+
fileContents.set(file.relativePath, { content, lines });
|
|
552
|
+
fileHashes.set(file.relativePath, crypto.createHash('sha256').update(content).digest('hex').slice(0, 16));
|
|
553
|
+
} catch (err) { /* skip */ }
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Find duplicates
|
|
557
|
+
const hashGroups = new Map();
|
|
558
|
+
for (const [filePath, hash] of fileHashes.entries()) {
|
|
559
|
+
if (!hashGroups.has(hash)) hashGroups.set(hash, []);
|
|
560
|
+
hashGroups.get(hash).push(filePath);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const duplicationMap = [];
|
|
564
|
+
for (const [hash, files] of hashGroups.entries()) {
|
|
565
|
+
if (files.length > 1) {
|
|
566
|
+
const { lines } = fileContents.get(files[0]) || { lines: 0 };
|
|
567
|
+
duplicationMap.push({
|
|
568
|
+
primary: files[0],
|
|
569
|
+
duplicates: files.slice(1),
|
|
570
|
+
similarity: 1.0,
|
|
571
|
+
type: 'exact',
|
|
572
|
+
lineCount: lines,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Find legacy code
|
|
578
|
+
const LEGACY_PATTERNS = [
|
|
579
|
+
{ pattern: /\.old\.(js|ts|tsx|jsx)$/i, type: 'backup', confidence: 0.9 },
|
|
580
|
+
{ pattern: /\.bak\.(js|ts|tsx|jsx)$/i, type: 'backup', confidence: 0.95 },
|
|
581
|
+
{ pattern: /\.backup\.(js|ts|tsx|jsx)$/i, type: 'backup', confidence: 0.95 },
|
|
582
|
+
{ pattern: /\.deprecated\.(js|ts|tsx|jsx)$/i, type: 'deprecated', confidence: 0.9 },
|
|
583
|
+
];
|
|
584
|
+
|
|
585
|
+
const legacyMap = [];
|
|
586
|
+
for (const [filePath, { content }] of fileContents.entries()) {
|
|
587
|
+
for (const { pattern, type, confidence } of LEGACY_PATTERNS) {
|
|
588
|
+
if (pattern.test(filePath)) {
|
|
589
|
+
legacyMap.push({
|
|
590
|
+
file: filePath,
|
|
591
|
+
type,
|
|
592
|
+
evidence: [`File name matches pattern: ${pattern}`],
|
|
593
|
+
confidence,
|
|
594
|
+
});
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Check content for @deprecated
|
|
599
|
+
if (/@deprecated/i.test(content)) {
|
|
600
|
+
const existing = legacyMap.find(l => l.file === filePath);
|
|
601
|
+
if (!existing) {
|
|
602
|
+
legacyMap.push({
|
|
603
|
+
file: filePath,
|
|
604
|
+
type: 'deprecated',
|
|
605
|
+
evidence: ['Contains @deprecated annotation'],
|
|
606
|
+
confidence: 0.85,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return {
|
|
613
|
+
duplicationMap,
|
|
614
|
+
legacyMap,
|
|
615
|
+
summary: {
|
|
616
|
+
totalFiles: files.length,
|
|
617
|
+
duplicatedFiles: duplicationMap.reduce((sum, d) => sum + 1 + d.duplicates.length, 0),
|
|
618
|
+
legacyFiles: legacyMap.length,
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Generate human-readable notes for verdict
|
|
625
|
+
*/
|
|
626
|
+
function generateNotes(action, findings, hardStopsTriggered) {
|
|
627
|
+
if (action === 'STOP') {
|
|
628
|
+
return `BLOCKED: Hard stops triggered. ${hardStopsTriggered.join('. ')}. Manual intervention required.`;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (action === 'DEFER') {
|
|
632
|
+
const reviewCount = findings.needsReview.length;
|
|
633
|
+
const blockedCount = findings.blocked.length;
|
|
634
|
+
return `REVIEW NEEDED: ${reviewCount} items need manual review, ${blockedCount} items are blocked. Safe consolidations: ${findings.safeToConsolidate.length}.`;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const linesSaved = findings.safeToConsolidate.reduce((sum, f) => sum + (f.linesSaved || 0), 0);
|
|
638
|
+
return `SAFE TO PROCEED: ${findings.safeToConsolidate.length} consolidations identified. Estimated ${linesSaved} lines can be reduced.`;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
642
|
+
// OUTPUT FORMATTERS
|
|
643
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
644
|
+
|
|
645
|
+
function formatVerdictOutput(verdict, projectPath) {
|
|
646
|
+
const lines = [];
|
|
647
|
+
|
|
648
|
+
const actionSymbol = verdict.action === 'PROCEED' ? '✓' : verdict.action === 'DEFER' ? '⚠' : '✗';
|
|
649
|
+
const actionColor = verdict.action === 'PROCEED' ? colors.success : verdict.action === 'DEFER' ? colors.warning : colors.error;
|
|
650
|
+
|
|
651
|
+
lines.push('');
|
|
652
|
+
lines.push('┌────────────────────────────────────────────────────────────────────┐');
|
|
653
|
+
lines.push(`│ ${actionColor}${actionSymbol}${ansi.reset} Authority Verdict: ${ansi.bold}${verdict.action}${ansi.reset} │`);
|
|
654
|
+
lines.push('├────────────────────────────────────────────────────────────────────┤');
|
|
655
|
+
lines.push(`│ Authority: ${verdict.authority} v${verdict.version}`.padEnd(68) + '│');
|
|
656
|
+
lines.push(`│ Risk Level: ${verdict.riskLevel}`.padEnd(68) + '│');
|
|
657
|
+
lines.push(`│ Confidence: ${Math.round(verdict.confidence * 100)}%`.padEnd(68) + '│');
|
|
658
|
+
lines.push(`│ Exit Code: ${verdict.exitCode}`.padEnd(68) + '│');
|
|
659
|
+
lines.push('└────────────────────────────────────────────────────────────────────┘');
|
|
660
|
+
lines.push('');
|
|
661
|
+
|
|
662
|
+
// Hard stops
|
|
663
|
+
if (verdict.hardStopsTriggered.length > 0) {
|
|
664
|
+
lines.push(`${ansi.bold}${colors.error}Hard Stops Triggered:${ansi.reset}`);
|
|
665
|
+
for (const stop of verdict.hardStopsTriggered) {
|
|
666
|
+
lines.push(` ${colors.error}✗${ansi.reset} ${stop}`);
|
|
667
|
+
}
|
|
668
|
+
lines.push('');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Proofs
|
|
672
|
+
lines.push(`${ansi.bold}Proofs:${ansi.reset}`);
|
|
673
|
+
for (const [key, value] of Object.entries(verdict.proofs)) {
|
|
674
|
+
const icon = value.startsWith('PASSED') ? colors.success + '✓' : value.startsWith('FAILED') ? colors.error + '✗' : colors.warning + '○';
|
|
675
|
+
lines.push(` ${icon}${ansi.reset} ${key}: ${ansi.dim}${value}${ansi.reset}`);
|
|
676
|
+
}
|
|
677
|
+
lines.push('');
|
|
678
|
+
|
|
679
|
+
// Analysis summary
|
|
680
|
+
if (verdict.analysis) {
|
|
681
|
+
const { summary } = verdict.analysis;
|
|
682
|
+
lines.push(`${ansi.bold}Analysis Summary:${ansi.reset}`);
|
|
683
|
+
lines.push(` ${colors.success}✓${ansi.reset} Safe to consolidate: ${summary.safeCount}`);
|
|
684
|
+
lines.push(` ${colors.warning}⚠${ansi.reset} Needs review: ${summary.reviewCount}`);
|
|
685
|
+
lines.push(` ${colors.error}✗${ansi.reset} Blocked: ${summary.blockedCount}`);
|
|
686
|
+
lines.push(` ${ansi.dim}Estimated lines saved: ${summary.totalLinesSavings}${ansi.reset}`);
|
|
687
|
+
lines.push('');
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Notes
|
|
691
|
+
lines.push(`${ansi.bold}Notes:${ansi.reset}`);
|
|
692
|
+
lines.push(` ${verdict.notes}`);
|
|
693
|
+
lines.push('');
|
|
694
|
+
|
|
695
|
+
// Timestamp
|
|
696
|
+
lines.push(`${ansi.dim}${verdict.timestamp}${ansi.reset}`);
|
|
697
|
+
lines.push('');
|
|
698
|
+
|
|
699
|
+
return lines.join('\n');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Generate authority approved badge SVG
|
|
704
|
+
*/
|
|
705
|
+
function generateBadge(verdict) {
|
|
706
|
+
const color = verdict.action === 'PROCEED' ? '#22C55E' : verdict.action === 'DEFER' ? '#F59E0B' : '#EF4444';
|
|
707
|
+
const text = verdict.action;
|
|
708
|
+
const conf = Math.round(verdict.confidence * 100);
|
|
709
|
+
|
|
710
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="240" height="20">
|
|
711
|
+
<linearGradient id="b" x2="0" y2="100%">
|
|
712
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
713
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
714
|
+
</linearGradient>
|
|
715
|
+
<clipPath id="a">
|
|
716
|
+
<rect width="240" height="20" rx="3" fill="#fff"/>
|
|
717
|
+
</clipPath>
|
|
718
|
+
<g clip-path="url(#a)">
|
|
719
|
+
<path fill="#555" d="M0 0h150v20H0z"/>
|
|
720
|
+
<path fill="${color}" d="M150 0h90v20H150z"/>
|
|
721
|
+
<path fill="url(#b)" d="M0 0h240v20H0z"/>
|
|
722
|
+
</g>
|
|
723
|
+
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
|
724
|
+
<text x="75" y="15" fill="#010101" fill-opacity=".3">Authority Approved</text>
|
|
725
|
+
<text x="75" y="14">Authority Approved</text>
|
|
726
|
+
<text x="195" y="15" fill="#010101" fill-opacity=".3">${text} ${conf}%</text>
|
|
727
|
+
<text x="195" y="14">${text} ${conf}%</text>
|
|
728
|
+
</g>
|
|
729
|
+
</svg>`;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
733
|
+
// HARDENING CONSTANTS
|
|
734
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
735
|
+
|
|
736
|
+
/** Maximum execution time for an authority (ms) */
|
|
737
|
+
const MAX_EXECUTION_TIME_MS = 5 * 60 * 1000; // 5 minutes
|
|
738
|
+
|
|
739
|
+
/** Rate limit tracking */
|
|
740
|
+
const rateLimitStore = new Map();
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Check rate limit for user
|
|
744
|
+
*/
|
|
745
|
+
function checkRateLimit(userId, windowMs, maxRequests) {
|
|
746
|
+
const key = `${userId}:${windowMs}`;
|
|
747
|
+
const now = Date.now();
|
|
748
|
+
|
|
749
|
+
let entry = rateLimitStore.get(key);
|
|
750
|
+
|
|
751
|
+
if (!entry || entry.resetAt < now) {
|
|
752
|
+
entry = { count: 0, resetAt: now + windowMs };
|
|
753
|
+
rateLimitStore.set(key, entry);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const allowed = entry.count < maxRequests;
|
|
757
|
+
if (allowed) {
|
|
758
|
+
entry.count++;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return {
|
|
762
|
+
allowed,
|
|
763
|
+
remaining: Math.max(0, maxRequests - entry.count),
|
|
764
|
+
resetAt: new Date(entry.resetAt),
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Validate authority ID format
|
|
770
|
+
*/
|
|
771
|
+
function validateAuthorityId(id) {
|
|
772
|
+
const pattern = /^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$/;
|
|
773
|
+
|
|
774
|
+
if (!id || typeof id !== 'string') {
|
|
775
|
+
return { valid: false, error: 'Authority ID must be a non-empty string' };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (id.length < 2 || id.length > 64) {
|
|
779
|
+
return { valid: false, error: 'Authority ID must be 2-64 characters' };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (!pattern.test(id)) {
|
|
783
|
+
return { valid: false, error: 'Authority ID must be lowercase alphanumeric with hyphens' };
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return { valid: true };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Validate project path for security
|
|
791
|
+
*/
|
|
792
|
+
function validateProjectPath(projectPath) {
|
|
793
|
+
if (!projectPath || typeof projectPath !== 'string') {
|
|
794
|
+
return { valid: false, error: 'Project path must be a non-empty string' };
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Check for path traversal attempts
|
|
798
|
+
if (projectPath.includes('..') || projectPath.includes('\0')) {
|
|
799
|
+
return { valid: false, error: 'Invalid characters in project path' };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Must be absolute path
|
|
803
|
+
const isAbsolute = projectPath.startsWith('/') || /^[A-Za-z]:[\\/]/.test(projectPath);
|
|
804
|
+
if (!isAbsolute) {
|
|
805
|
+
return { valid: false, error: 'Project path must be absolute' };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return { valid: true };
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Execute with timeout
|
|
813
|
+
*/
|
|
814
|
+
async function withTimeout(fn, timeoutMs, timeoutError = 'Operation timed out') {
|
|
815
|
+
let timeoutId;
|
|
816
|
+
|
|
817
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
818
|
+
timeoutId = setTimeout(() => {
|
|
819
|
+
reject(new Error(timeoutError));
|
|
820
|
+
}, timeoutMs);
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
try {
|
|
824
|
+
const result = await Promise.race([fn(), timeoutPromise]);
|
|
825
|
+
clearTimeout(timeoutId);
|
|
826
|
+
return result;
|
|
827
|
+
} catch (error) {
|
|
828
|
+
clearTimeout(timeoutId);
|
|
829
|
+
throw error;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Sanitize error for safe logging
|
|
835
|
+
*/
|
|
836
|
+
function sanitizeError(error) {
|
|
837
|
+
if (error instanceof Error) {
|
|
838
|
+
let message = error.message;
|
|
839
|
+
// Remove file paths
|
|
840
|
+
message = message.replace(/[A-Za-z]:[\\\/][^\s]+/g, '[PATH]');
|
|
841
|
+
message = message.replace(/\/[^\s]+/g, '[PATH]');
|
|
842
|
+
// Remove potential secrets
|
|
843
|
+
message = message.replace(/['"][^'"]{20,}['"]/g, '[REDACTED]');
|
|
844
|
+
|
|
845
|
+
return {
|
|
846
|
+
message: message.slice(0, 200),
|
|
847
|
+
code: error.name || 'Error',
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return {
|
|
852
|
+
message: 'An unexpected error occurred',
|
|
853
|
+
code: 'UNKNOWN_ERROR',
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
858
|
+
// MAIN COMMAND
|
|
859
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
860
|
+
|
|
861
|
+
async function runApprove(args) {
|
|
862
|
+
const opts = parseArgs(args);
|
|
863
|
+
const executionId = `exec_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
864
|
+
const startTime = Date.now();
|
|
865
|
+
|
|
866
|
+
// Show help
|
|
867
|
+
if (opts.help) {
|
|
868
|
+
printHelp(shouldShowBanner(opts));
|
|
869
|
+
return 0;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Print banner
|
|
873
|
+
if (shouldShowBanner(opts)) {
|
|
874
|
+
printBanner();
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// List authorities
|
|
878
|
+
if (opts.list) {
|
|
879
|
+
console.log(`\n ${ansi.bold}Available Authorities:${ansi.reset}\n`);
|
|
880
|
+
|
|
881
|
+
for (const [id, auth] of Object.entries(AUTHORITIES)) {
|
|
882
|
+
const tierLabel = auth.tier === 'free' ? colors.success + '[FREE]' :
|
|
883
|
+
auth.tier === 'starter' ? colors.accent + '[STARTER]' :
|
|
884
|
+
auth.tier === 'pro' ? colors.warning + '[PRO]' :
|
|
885
|
+
colors.error + '[ENTERPRISE]';
|
|
886
|
+
console.log(` ${colors.accent}${id}${ansi.reset} ${tierLabel}${ansi.reset}`);
|
|
887
|
+
console.log(` ${ansi.dim}${auth.description}${ansi.reset}`);
|
|
888
|
+
if (auth.note) {
|
|
889
|
+
console.log(` ${colors.warning}Note: ${auth.note}${ansi.reset}`);
|
|
890
|
+
}
|
|
891
|
+
console.log();
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
console.log(` ${ansi.dim}Usage: vibecheck approve <authority-id>${ansi.reset}\n`);
|
|
895
|
+
return 0;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Require authority argument
|
|
899
|
+
if (!opts.authority) {
|
|
900
|
+
console.error(`\n ${colors.error}✗${ansi.reset} Authority ID required\n`);
|
|
901
|
+
console.log(` ${ansi.dim}Usage: vibecheck approve <authority-id>${ansi.reset}`);
|
|
902
|
+
console.log(` ${ansi.dim}List: vibecheck approve --list${ansi.reset}\n`);
|
|
903
|
+
return EXIT.USER_ERROR;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// HARDENING: Validate authority ID format
|
|
907
|
+
const idValidation = validateAuthorityId(opts.authority);
|
|
908
|
+
if (!idValidation.valid) {
|
|
909
|
+
console.error(`\n ${colors.error}✗${ansi.reset} Invalid authority ID: ${idValidation.error}\n`);
|
|
910
|
+
return EXIT.USER_ERROR;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Check authority exists
|
|
914
|
+
const authority = AUTHORITIES[opts.authority];
|
|
915
|
+
if (!authority) {
|
|
916
|
+
console.error(`\n ${colors.error}✗${ansi.reset} Unknown authority: ${opts.authority}\n`);
|
|
917
|
+
console.log(` ${ansi.dim}Available: ${Object.keys(AUTHORITIES).join(', ')}${ansi.reset}\n`);
|
|
918
|
+
return EXIT.USER_ERROR;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Special case for inventory - redirect to classify
|
|
922
|
+
if (opts.authority === 'inventory') {
|
|
923
|
+
console.log(`\n ${colors.warning}⚠${ansi.reset} Use 'vibecheck classify' for the inventory authority\n`);
|
|
924
|
+
return EXIT.USER_ERROR;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// HARDENING: Rate limiting (10 requests per minute)
|
|
928
|
+
const userId = process.env.VIBECHECK_USER_ID || 'anonymous';
|
|
929
|
+
const rateCheck = checkRateLimit(userId, 60 * 1000, 10);
|
|
930
|
+
if (!rateCheck.allowed) {
|
|
931
|
+
console.error(`\n ${colors.error}✗${ansi.reset} Rate limit exceeded`);
|
|
932
|
+
console.log(` ${ansi.dim}Try again at: ${rateCheck.resetAt.toISOString()}${ansi.reset}\n`);
|
|
933
|
+
return EXIT.RATE_LIMITED || 429;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Check tier access
|
|
937
|
+
const access = await checkTierAccess(authority.tier);
|
|
938
|
+
if (!access.allowed) {
|
|
939
|
+
console.log(`\n ${colors.error}✗${ansi.reset} ${ansi.bold}${authority.tier.toUpperCase()} tier required${ansi.reset}`);
|
|
940
|
+
console.log(` ${ansi.dim}Current tier: ${access.tier || 'FREE'}${ansi.reset}`);
|
|
941
|
+
console.log(` ${ansi.dim}Upgrade at: https://vibecheckai.dev/pricing${ansi.reset}\n`);
|
|
942
|
+
return EXIT.TIER_REQUIRED;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const projectPath = path.resolve(opts.path);
|
|
946
|
+
|
|
947
|
+
// HARDENING: Validate project path
|
|
948
|
+
const pathValidation = validateProjectPath(projectPath);
|
|
949
|
+
if (!pathValidation.valid) {
|
|
950
|
+
throw createUserError(pathValidation.error, "SecurityError");
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Validate project path exists
|
|
954
|
+
if (!fs.existsSync(projectPath)) {
|
|
955
|
+
throw createUserError(`Project path does not exist: ${projectPath}`, "ValidationError");
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (!opts.quiet) {
|
|
959
|
+
console.log(` ${ansi.dim}Execution:${ansi.reset} ${ansi.dim}${executionId}${ansi.reset}`);
|
|
960
|
+
console.log(` ${ansi.dim}Project:${ansi.reset} ${ansi.bold}${path.basename(projectPath)}${ansi.reset}`);
|
|
961
|
+
console.log(` ${ansi.dim}Authority:${ansi.reset} ${colors.accent}${authority.id}${ansi.reset} v${authority.version}`);
|
|
962
|
+
console.log(` ${ansi.dim}Tier:${ansi.reset} ${authority.tier.toUpperCase()}`);
|
|
963
|
+
console.log();
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Run analysis with timeout
|
|
967
|
+
const spinner = new Spinner({ color: colors.primary });
|
|
968
|
+
spinner.start('Executing authority...');
|
|
969
|
+
|
|
970
|
+
try {
|
|
971
|
+
let verdict;
|
|
972
|
+
|
|
973
|
+
// HARDENING: Execute with timeout
|
|
974
|
+
verdict = await withTimeout(
|
|
975
|
+
async () => {
|
|
976
|
+
if (opts.authority === 'safe-consolidation') {
|
|
977
|
+
return await analyzeSafeConsolidation(projectPath, authority, opts, spinner);
|
|
978
|
+
} else if (opts.authority === 'security-remediation') {
|
|
979
|
+
// Forward to security analysis
|
|
980
|
+
return await analyzeSecurityRemediation(projectPath, authority, opts, spinner);
|
|
981
|
+
} else {
|
|
982
|
+
throw createUserError(`Authority executor not implemented: ${opts.authority}`, "NotImplemented");
|
|
983
|
+
}
|
|
984
|
+
},
|
|
985
|
+
MAX_EXECUTION_TIME_MS,
|
|
986
|
+
`Authority execution timed out after ${MAX_EXECUTION_TIME_MS / 1000}s`
|
|
987
|
+
);
|
|
988
|
+
|
|
989
|
+
// Add execution metadata
|
|
990
|
+
verdict.executionId = executionId;
|
|
991
|
+
verdict.analysisTimeMs = Date.now() - startTime;
|
|
992
|
+
|
|
993
|
+
spinner.succeed(`Analysis complete (${verdict.analysisTimeMs}ms)`);
|
|
994
|
+
|
|
995
|
+
// Output
|
|
996
|
+
if (opts.json) {
|
|
997
|
+
console.log(JSON.stringify(verdict, null, 2));
|
|
998
|
+
} else {
|
|
999
|
+
console.log(formatVerdictOutput(verdict, projectPath));
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Save to file if requested
|
|
1003
|
+
if (opts.output && !opts.dryRun) {
|
|
1004
|
+
const outputPath = path.resolve(opts.output);
|
|
1005
|
+
await fs.promises.writeFile(outputPath, JSON.stringify(verdict, null, 2));
|
|
1006
|
+
|
|
1007
|
+
if (!opts.quiet && !opts.json) {
|
|
1008
|
+
console.log(` ${colors.success}✓${ansi.reset} Verdict saved to: ${outputPath}`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Generate badge if requested
|
|
1013
|
+
if (opts.badge && verdict.action === 'PROCEED' && !opts.dryRun) {
|
|
1014
|
+
const badgePath = path.join(projectPath, '.vibecheck', 'badges', `${authority.id}-badge.svg`);
|
|
1015
|
+
const badgeDir = path.dirname(badgePath);
|
|
1016
|
+
|
|
1017
|
+
if (!fs.existsSync(badgeDir)) {
|
|
1018
|
+
fs.mkdirSync(badgeDir, { recursive: true });
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
await fs.promises.writeFile(badgePath, generateBadge(verdict));
|
|
1022
|
+
|
|
1023
|
+
if (!opts.quiet && !opts.json) {
|
|
1024
|
+
console.log(` ${colors.success}✓${ansi.reset} Badge saved to: ${badgePath}`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Log execution summary (for audit)
|
|
1029
|
+
if (opts.verbose) {
|
|
1030
|
+
console.log(`\n ${ansi.dim}Execution Summary:${ansi.reset}`);
|
|
1031
|
+
console.log(` ${ansi.dim}├─ ID: ${executionId}${ansi.reset}`);
|
|
1032
|
+
console.log(` ${ansi.dim}├─ Duration: ${verdict.analysisTimeMs}ms${ansi.reset}`);
|
|
1033
|
+
console.log(` ${ansi.dim}├─ Files Analyzed: ${verdict.filesTouched.length}${ansi.reset}`);
|
|
1034
|
+
console.log(` ${ansi.dim}└─ Verdict: ${verdict.action}${ansi.reset}\n`);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return verdict.exitCode;
|
|
1038
|
+
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
const sanitized = sanitizeError(error);
|
|
1041
|
+
spinner.fail(`Authority execution failed: ${sanitized.message}`);
|
|
1042
|
+
|
|
1043
|
+
// Log sanitized error details
|
|
1044
|
+
if (opts.verbose) {
|
|
1045
|
+
console.error(` ${ansi.dim}Error Code: ${sanitized.code}${ansi.reset}`);
|
|
1046
|
+
console.error(` ${ansi.dim}Execution ID: ${executionId}${ansi.reset}`);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
throw error;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Security remediation authority executor
|
|
1055
|
+
*/
|
|
1056
|
+
async function analyzeSecurityRemediation(projectPath, authority, opts, spinner) {
|
|
1057
|
+
const startTime = Date.now();
|
|
1058
|
+
spinner.update('Scanning for security vulnerabilities...');
|
|
1059
|
+
|
|
1060
|
+
// Security patterns to detect
|
|
1061
|
+
const SECURITY_PATTERNS = {
|
|
1062
|
+
COMMAND_INJECTION: {
|
|
1063
|
+
pattern: /execSync\s*\(\s*[`$]/,
|
|
1064
|
+
severity: 'CRITICAL',
|
|
1065
|
+
description: 'Command injection via shell interpolation',
|
|
1066
|
+
},
|
|
1067
|
+
SQL_INJECTION: {
|
|
1068
|
+
pattern: /query\s*\(\s*[`$]/,
|
|
1069
|
+
severity: 'CRITICAL',
|
|
1070
|
+
description: 'SQL injection via string interpolation',
|
|
1071
|
+
},
|
|
1072
|
+
EVAL_USAGE: {
|
|
1073
|
+
pattern: /eval\s*\(/,
|
|
1074
|
+
severity: 'HIGH',
|
|
1075
|
+
description: 'Unsafe eval usage',
|
|
1076
|
+
},
|
|
1077
|
+
HARDCODED_SECRET: {
|
|
1078
|
+
pattern: /process\.env\.\w+\s*\|\|\s*['"][^'"]{8,}['"]/,
|
|
1079
|
+
severity: 'HIGH',
|
|
1080
|
+
description: 'Hardcoded fallback secret',
|
|
1081
|
+
},
|
|
1082
|
+
PATH_TRAVERSAL: {
|
|
1083
|
+
pattern: /path\.join\s*\([^)]*req\./,
|
|
1084
|
+
severity: 'HIGH',
|
|
1085
|
+
description: 'Path traversal via user input',
|
|
1086
|
+
},
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
const findings = [];
|
|
1090
|
+
const filesTouched = [];
|
|
1091
|
+
|
|
1092
|
+
// Scan files
|
|
1093
|
+
const extensions = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
1094
|
+
const excludedDirs = new Set(['node_modules', '.git', 'dist', 'build']);
|
|
1095
|
+
|
|
1096
|
+
async function walkDir(dir, depth = 0) {
|
|
1097
|
+
if (depth > 15) return;
|
|
1098
|
+
|
|
1099
|
+
try {
|
|
1100
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
1101
|
+
|
|
1102
|
+
for (const entry of entries) {
|
|
1103
|
+
const fullPath = path.join(dir, entry.name);
|
|
1104
|
+
const relativePath = path.relative(projectPath, fullPath);
|
|
1105
|
+
|
|
1106
|
+
if (entry.isDirectory()) {
|
|
1107
|
+
if (!excludedDirs.has(entry.name) && !entry.name.startsWith('.')) {
|
|
1108
|
+
await walkDir(fullPath, depth + 1);
|
|
1109
|
+
}
|
|
1110
|
+
} else if (entry.isFile()) {
|
|
1111
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
1112
|
+
if (extensions.has(ext)) {
|
|
1113
|
+
try {
|
|
1114
|
+
const content = await fs.promises.readFile(fullPath, 'utf-8');
|
|
1115
|
+
filesTouched.push(relativePath);
|
|
1116
|
+
|
|
1117
|
+
for (const [name, { pattern, severity, description }] of Object.entries(SECURITY_PATTERNS)) {
|
|
1118
|
+
if (pattern.test(content)) {
|
|
1119
|
+
const match = content.match(pattern);
|
|
1120
|
+
const beforeMatch = content.slice(0, match.index);
|
|
1121
|
+
const line = (beforeMatch.match(/\n/g) || []).length + 1;
|
|
1122
|
+
|
|
1123
|
+
findings.push({
|
|
1124
|
+
file: relativePath,
|
|
1125
|
+
line,
|
|
1126
|
+
type: name,
|
|
1127
|
+
severity,
|
|
1128
|
+
description,
|
|
1129
|
+
evidence: match[0].slice(0, 50),
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
} catch (err) {
|
|
1134
|
+
// Skip unreadable files
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
} catch (err) {
|
|
1140
|
+
// Skip inaccessible directories
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
await walkDir(projectPath);
|
|
1145
|
+
|
|
1146
|
+
// Determine verdict
|
|
1147
|
+
const criticalCount = findings.filter(f => f.severity === 'CRITICAL').length;
|
|
1148
|
+
const highCount = findings.filter(f => f.severity === 'HIGH').length;
|
|
1149
|
+
|
|
1150
|
+
let action = 'PROCEED';
|
|
1151
|
+
let riskLevel = 'LOW';
|
|
1152
|
+
let confidence = 0.9;
|
|
1153
|
+
|
|
1154
|
+
if (criticalCount > 0) {
|
|
1155
|
+
action = 'STOP';
|
|
1156
|
+
riskLevel = 'CRITICAL';
|
|
1157
|
+
confidence = 1.0;
|
|
1158
|
+
} else if (highCount > 0) {
|
|
1159
|
+
action = 'DEFER';
|
|
1160
|
+
riskLevel = 'HIGH';
|
|
1161
|
+
confidence = 0.8;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
return {
|
|
1165
|
+
authority: authority.id,
|
|
1166
|
+
version: authority.version,
|
|
1167
|
+
timestamp: new Date().toISOString(),
|
|
1168
|
+
action,
|
|
1169
|
+
riskLevel,
|
|
1170
|
+
exitCode: action === 'PROCEED' ? 0 : action === 'DEFER' ? 1 : 2,
|
|
1171
|
+
filesTouched,
|
|
1172
|
+
proofs: {
|
|
1173
|
+
reachability: 'Static analysis - pattern matching',
|
|
1174
|
+
compatibility: 'N/A - read-only scan',
|
|
1175
|
+
rollback: 'N/A - no changes made',
|
|
1176
|
+
},
|
|
1177
|
+
behaviorChange: false,
|
|
1178
|
+
confidence,
|
|
1179
|
+
hardStopsTriggered: criticalCount > 0 ? [`${criticalCount} CRITICAL vulnerabilities`] : [],
|
|
1180
|
+
notes: criticalCount > 0
|
|
1181
|
+
? `BLOCKED: ${criticalCount} critical vulnerabilities found. Fix before deployment.`
|
|
1182
|
+
: highCount > 0
|
|
1183
|
+
? `REVIEW: ${highCount} high severity issues found. Manual review recommended.`
|
|
1184
|
+
: `PASSED: No critical security issues detected.`,
|
|
1185
|
+
analysis: {
|
|
1186
|
+
findings,
|
|
1187
|
+
summary: {
|
|
1188
|
+
critical: criticalCount,
|
|
1189
|
+
high: highCount,
|
|
1190
|
+
total: findings.length,
|
|
1191
|
+
filesScanned: filesTouched.length,
|
|
1192
|
+
},
|
|
1193
|
+
},
|
|
1194
|
+
analysisTimeMs: Date.now() - startTime,
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
module.exports = {
|
|
1199
|
+
runApprove: withErrorHandling(runApprove, "Approve failed"),
|
|
1200
|
+
};
|