@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,604 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lawbook Invariant Evaluator
|
|
3
|
+
*
|
|
4
|
+
* Evaluates change proposals against organizational invariant rules.
|
|
5
|
+
* Returns violations with severity and remediation suggestions.
|
|
6
|
+
*
|
|
7
|
+
* Codename: Lawbook
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
const { minimatch } = require("minimatch");
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
INVARIANT_TYPES,
|
|
18
|
+
INVARIANT_SEVERITY,
|
|
19
|
+
validateInvariant,
|
|
20
|
+
} = require("./schema.js");
|
|
21
|
+
const { lawbookLogger: log, getErrorMessage } = require("../logger.js");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} Violation
|
|
25
|
+
* @property {string} invariantId - Invariant that was violated
|
|
26
|
+
* @property {string} rule - Rule type
|
|
27
|
+
* @property {string} severity - Violation severity
|
|
28
|
+
* @property {string} file - File where violation occurred
|
|
29
|
+
* @property {number} line - Line number (if applicable)
|
|
30
|
+
* @property {string} message - Human-readable message
|
|
31
|
+
* @property {string} evidence - Evidence of the violation
|
|
32
|
+
* @property {Object} suggestion - Suggested fix
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {Object} EvaluationResult
|
|
37
|
+
* @property {boolean} passed - Did evaluation pass (no blocking violations)
|
|
38
|
+
* @property {Violation[]} violations - All violations found
|
|
39
|
+
* @property {Object} summary - Summary by severity
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Invariant Evaluator class
|
|
44
|
+
*/
|
|
45
|
+
class InvariantEvaluator {
|
|
46
|
+
constructor(options = {}) {
|
|
47
|
+
this.invariants = [];
|
|
48
|
+
this.envRegistry = null;
|
|
49
|
+
this.projectRoot = options.projectRoot || process.cwd();
|
|
50
|
+
this.strictMode = options.strictMode || false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Load invariants from a lawbook
|
|
55
|
+
* @param {Object} lawbook - Parsed lawbook
|
|
56
|
+
*/
|
|
57
|
+
loadLawbook(lawbook) {
|
|
58
|
+
if (!lawbook || !lawbook.invariants) return;
|
|
59
|
+
|
|
60
|
+
for (const invariant of lawbook.invariants) {
|
|
61
|
+
const validation = validateInvariant(invariant);
|
|
62
|
+
if (validation.valid) {
|
|
63
|
+
this.invariants.push(invariant);
|
|
64
|
+
} else if (this.strictMode) {
|
|
65
|
+
throw new Error(`Invalid invariant ${invariant.id}: ${validation.errors.map(e => e.message).join(", ")}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Load environment variable registry for env-must-be-registered rules
|
|
72
|
+
* @param {string} registryPath - Path to registry file
|
|
73
|
+
*/
|
|
74
|
+
loadEnvRegistry(registryPath) {
|
|
75
|
+
try {
|
|
76
|
+
const fullPath = path.resolve(this.projectRoot, registryPath);
|
|
77
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
78
|
+
|
|
79
|
+
// Parse .env.example style file
|
|
80
|
+
const registry = new Set();
|
|
81
|
+
for (const line of content.split("\n")) {
|
|
82
|
+
const trimmed = line.trim();
|
|
83
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
84
|
+
|
|
85
|
+
const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=/);
|
|
86
|
+
if (match) {
|
|
87
|
+
registry.add(match[1]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.envRegistry = registry;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
log.warn(`Failed to load env registry: ${getErrorMessage(error)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Evaluate a change proposal against all invariants
|
|
99
|
+
* @param {Object} proposal - Change proposal
|
|
100
|
+
* @returns {EvaluationResult} Evaluation result
|
|
101
|
+
*/
|
|
102
|
+
evaluate(proposal) {
|
|
103
|
+
const violations = [];
|
|
104
|
+
|
|
105
|
+
for (const invariant of this.invariants) {
|
|
106
|
+
const invariantViolations = this.evaluateInvariant(invariant, proposal);
|
|
107
|
+
violations.push(...invariantViolations);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Calculate summary
|
|
111
|
+
const summary = {
|
|
112
|
+
total: violations.length,
|
|
113
|
+
block: violations.filter(v => v.severity === INVARIANT_SEVERITY.BLOCK).length,
|
|
114
|
+
error: violations.filter(v => v.severity === INVARIANT_SEVERITY.ERROR).length,
|
|
115
|
+
warning: violations.filter(v => v.severity === INVARIANT_SEVERITY.WARNING).length,
|
|
116
|
+
info: violations.filter(v => v.severity === INVARIANT_SEVERITY.INFO).length,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
passed: summary.block === 0 && summary.error === 0,
|
|
121
|
+
violations,
|
|
122
|
+
summary,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Evaluate a single invariant against a proposal
|
|
128
|
+
* @param {Object} invariant - Invariant to check
|
|
129
|
+
* @param {Object} proposal - Change proposal
|
|
130
|
+
* @returns {Violation[]} Violations found
|
|
131
|
+
*/
|
|
132
|
+
evaluateInvariant(invariant, proposal) {
|
|
133
|
+
switch (invariant.rule) {
|
|
134
|
+
case INVARIANT_TYPES.NO_MODIFY:
|
|
135
|
+
return this.evaluateNoModify(invariant, proposal);
|
|
136
|
+
|
|
137
|
+
case INVARIANT_TYPES.NO_DELETE:
|
|
138
|
+
return this.evaluateNoDelete(invariant, proposal);
|
|
139
|
+
|
|
140
|
+
case INVARIANT_TYPES.NO_CREATE:
|
|
141
|
+
return this.evaluateNoCreate(invariant, proposal);
|
|
142
|
+
|
|
143
|
+
case INVARIANT_TYPES.NEVER:
|
|
144
|
+
return this.evaluateNever(invariant, proposal);
|
|
145
|
+
|
|
146
|
+
case INVARIANT_TYPES.ALWAYS:
|
|
147
|
+
return this.evaluateAlways(invariant, proposal);
|
|
148
|
+
|
|
149
|
+
case INVARIANT_TYPES.ALL_THROUGH:
|
|
150
|
+
return this.evaluateAllThrough(invariant, proposal);
|
|
151
|
+
|
|
152
|
+
case INVARIANT_TYPES.NO_DIRECT:
|
|
153
|
+
return this.evaluateNoDirect(invariant, proposal);
|
|
154
|
+
|
|
155
|
+
case INVARIANT_TYPES.ENV_MUST_BE_REGISTERED:
|
|
156
|
+
return this.evaluateEnvMustBeRegistered(invariant, proposal);
|
|
157
|
+
|
|
158
|
+
case INVARIANT_TYPES.REQUIRE_APPROVAL:
|
|
159
|
+
return this.evaluateRequireApproval(invariant, proposal);
|
|
160
|
+
|
|
161
|
+
default:
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if a file matches an invariant's scope
|
|
168
|
+
* @param {string} filePath - File path to check
|
|
169
|
+
* @param {Object} invariant - Invariant with scope
|
|
170
|
+
* @returns {boolean} Does file match scope
|
|
171
|
+
*/
|
|
172
|
+
matchesScope(filePath, invariant) {
|
|
173
|
+
if (!invariant.scope) return true;
|
|
174
|
+
|
|
175
|
+
const relativePath = path.relative(this.projectRoot, filePath).replace(/\\/g, "/");
|
|
176
|
+
|
|
177
|
+
// Check if matches scope
|
|
178
|
+
const matchesInclude = minimatch(relativePath, invariant.scope, { dot: true });
|
|
179
|
+
|
|
180
|
+
// Check if excluded
|
|
181
|
+
if (matchesInclude && invariant.exclude) {
|
|
182
|
+
const excludes = Array.isArray(invariant.exclude) ? invariant.exclude : [invariant.exclude];
|
|
183
|
+
for (const exclude of excludes) {
|
|
184
|
+
if (minimatch(relativePath, exclude, { dot: true })) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return matchesInclude;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Create a violation object
|
|
195
|
+
* @param {Object} invariant - Violated invariant
|
|
196
|
+
* @param {Object} details - Violation details
|
|
197
|
+
* @returns {Violation} Violation object
|
|
198
|
+
*/
|
|
199
|
+
createViolation(invariant, details) {
|
|
200
|
+
return {
|
|
201
|
+
invariantId: invariant.id,
|
|
202
|
+
rule: invariant.rule,
|
|
203
|
+
severity: invariant.severity || INVARIANT_SEVERITY.ERROR,
|
|
204
|
+
file: details.file || null,
|
|
205
|
+
line: details.line || null,
|
|
206
|
+
message: details.message || invariant.message || invariant.description,
|
|
207
|
+
evidence: details.evidence || null,
|
|
208
|
+
suggestion: details.suggestion || null,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
213
|
+
// RULE EVALUATORS
|
|
214
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Evaluate no-modify rule
|
|
218
|
+
*/
|
|
219
|
+
evaluateNoModify(invariant, proposal) {
|
|
220
|
+
const violations = [];
|
|
221
|
+
|
|
222
|
+
for (const op of proposal.operations || []) {
|
|
223
|
+
if (op.type === "modify" || op.type === "update") {
|
|
224
|
+
const filePath = op.path || op.file;
|
|
225
|
+
if (filePath && this.matchesScope(filePath, invariant)) {
|
|
226
|
+
violations.push(this.createViolation(invariant, {
|
|
227
|
+
file: filePath,
|
|
228
|
+
message: `File is protected by '${invariant.id}' - modifications not allowed`,
|
|
229
|
+
evidence: `Attempted to modify: ${filePath}`,
|
|
230
|
+
suggestion: {
|
|
231
|
+
action: "remove_operation",
|
|
232
|
+
reason: invariant.description || "This file/path is protected",
|
|
233
|
+
incident: invariant.incident,
|
|
234
|
+
},
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return violations;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Evaluate no-delete rule
|
|
245
|
+
*/
|
|
246
|
+
evaluateNoDelete(invariant, proposal) {
|
|
247
|
+
const violations = [];
|
|
248
|
+
|
|
249
|
+
for (const op of proposal.operations || []) {
|
|
250
|
+
if (op.type === "delete" || op.type === "remove") {
|
|
251
|
+
const filePath = op.path || op.file;
|
|
252
|
+
if (filePath && this.matchesScope(filePath, invariant)) {
|
|
253
|
+
violations.push(this.createViolation(invariant, {
|
|
254
|
+
file: filePath,
|
|
255
|
+
message: `File is protected by '${invariant.id}' - deletion not allowed`,
|
|
256
|
+
evidence: `Attempted to delete: ${filePath}`,
|
|
257
|
+
suggestion: {
|
|
258
|
+
action: "remove_operation",
|
|
259
|
+
reason: invariant.description || "This file/path cannot be deleted",
|
|
260
|
+
},
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return violations;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Evaluate no-create rule
|
|
271
|
+
*/
|
|
272
|
+
evaluateNoCreate(invariant, proposal) {
|
|
273
|
+
const violations = [];
|
|
274
|
+
|
|
275
|
+
for (const op of proposal.operations || []) {
|
|
276
|
+
if (op.type === "create" || op.type === "add") {
|
|
277
|
+
const filePath = op.path || op.file;
|
|
278
|
+
if (filePath && this.matchesScope(filePath, invariant)) {
|
|
279
|
+
violations.push(this.createViolation(invariant, {
|
|
280
|
+
file: filePath,
|
|
281
|
+
message: `Creating files matching '${invariant.scope}' is not allowed`,
|
|
282
|
+
evidence: `Attempted to create: ${filePath}`,
|
|
283
|
+
suggestion: {
|
|
284
|
+
action: "remove_operation",
|
|
285
|
+
reason: invariant.description || "This pattern is forbidden",
|
|
286
|
+
},
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return violations;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Evaluate never rule (pattern must not appear)
|
|
297
|
+
*/
|
|
298
|
+
evaluateNever(invariant, proposal) {
|
|
299
|
+
const violations = [];
|
|
300
|
+
const pattern = new RegExp(invariant.pattern, "gm");
|
|
301
|
+
|
|
302
|
+
for (const op of proposal.operations || []) {
|
|
303
|
+
const filePath = op.path || op.file;
|
|
304
|
+
if (!filePath || !this.matchesScope(filePath, invariant)) continue;
|
|
305
|
+
|
|
306
|
+
const content = op.content || op.newContent;
|
|
307
|
+
if (!content) continue;
|
|
308
|
+
|
|
309
|
+
const lines = content.split("\n");
|
|
310
|
+
for (let i = 0; i < lines.length; i++) {
|
|
311
|
+
const line = lines[i];
|
|
312
|
+
const matches = line.match(pattern);
|
|
313
|
+
|
|
314
|
+
if (matches) {
|
|
315
|
+
violations.push(this.createViolation(invariant, {
|
|
316
|
+
file: filePath,
|
|
317
|
+
line: i + 1,
|
|
318
|
+
message: `Pattern '${invariant.pattern}' is forbidden by '${invariant.id}'`,
|
|
319
|
+
evidence: line.trim(),
|
|
320
|
+
suggestion: {
|
|
321
|
+
action: "remove_pattern",
|
|
322
|
+
pattern: invariant.pattern,
|
|
323
|
+
reason: invariant.description,
|
|
324
|
+
},
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return violations;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Evaluate always rule (pattern must be present)
|
|
335
|
+
*/
|
|
336
|
+
evaluateAlways(invariant, proposal) {
|
|
337
|
+
const violations = [];
|
|
338
|
+
const pattern = new RegExp(invariant.pattern);
|
|
339
|
+
|
|
340
|
+
for (const op of proposal.operations || []) {
|
|
341
|
+
const filePath = op.path || op.file;
|
|
342
|
+
if (!filePath || !this.matchesScope(filePath, invariant)) continue;
|
|
343
|
+
|
|
344
|
+
// Only check newly created or modified files
|
|
345
|
+
if (op.type !== "create" && op.type !== "modify") continue;
|
|
346
|
+
|
|
347
|
+
const content = op.content || op.newContent;
|
|
348
|
+
if (!content) continue;
|
|
349
|
+
|
|
350
|
+
if (!pattern.test(content)) {
|
|
351
|
+
violations.push(this.createViolation(invariant, {
|
|
352
|
+
file: filePath,
|
|
353
|
+
message: `Required pattern '${invariant.pattern}' is missing (required by '${invariant.id}')`,
|
|
354
|
+
evidence: `Pattern not found in file`,
|
|
355
|
+
suggestion: {
|
|
356
|
+
action: "add_pattern",
|
|
357
|
+
pattern: invariant.pattern,
|
|
358
|
+
reason: invariant.description,
|
|
359
|
+
},
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return violations;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Evaluate all-through rule (all X must go through Y)
|
|
369
|
+
*/
|
|
370
|
+
evaluateAllThrough(invariant, proposal) {
|
|
371
|
+
const violations = [];
|
|
372
|
+
|
|
373
|
+
if (!invariant.violations || !invariant.target) return violations;
|
|
374
|
+
|
|
375
|
+
for (const op of proposal.operations || []) {
|
|
376
|
+
const filePath = op.path || op.file;
|
|
377
|
+
if (!filePath) continue;
|
|
378
|
+
|
|
379
|
+
// Skip the target file itself
|
|
380
|
+
const relativePath = path.relative(this.projectRoot, filePath).replace(/\\/g, "/");
|
|
381
|
+
if (relativePath === invariant.target || minimatch(relativePath, invariant.target, { dot: true })) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const content = op.content || op.newContent;
|
|
386
|
+
if (!content) continue;
|
|
387
|
+
|
|
388
|
+
// Check each violation pattern
|
|
389
|
+
for (const violationDef of invariant.violations) {
|
|
390
|
+
// Skip if file is excluded
|
|
391
|
+
if (violationDef.exclude) {
|
|
392
|
+
const excludes = Array.isArray(violationDef.exclude) ? violationDef.exclude : [violationDef.exclude];
|
|
393
|
+
let excluded = false;
|
|
394
|
+
for (const exclude of excludes) {
|
|
395
|
+
if (minimatch(relativePath, exclude, { dot: true })) {
|
|
396
|
+
excluded = true;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (excluded) continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const pattern = new RegExp(violationDef.pattern, "gm");
|
|
404
|
+
const lines = content.split("\n");
|
|
405
|
+
|
|
406
|
+
for (let i = 0; i < lines.length; i++) {
|
|
407
|
+
if (pattern.test(lines[i])) {
|
|
408
|
+
violations.push(this.createViolation(invariant, {
|
|
409
|
+
file: filePath,
|
|
410
|
+
line: i + 1,
|
|
411
|
+
message: violationDef.message || `Must use ${invariant.target} instead of direct access`,
|
|
412
|
+
evidence: lines[i].trim(),
|
|
413
|
+
suggestion: {
|
|
414
|
+
action: "use_approved_path",
|
|
415
|
+
target: invariant.target,
|
|
416
|
+
reason: invariant.description,
|
|
417
|
+
},
|
|
418
|
+
}));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return violations;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Evaluate no-direct rule (must use approved pattern)
|
|
429
|
+
*/
|
|
430
|
+
evaluateNoDirect(invariant, proposal) {
|
|
431
|
+
const violations = [];
|
|
432
|
+
const pattern = new RegExp(invariant.pattern, "gm");
|
|
433
|
+
|
|
434
|
+
for (const op of proposal.operations || []) {
|
|
435
|
+
const filePath = op.path || op.file;
|
|
436
|
+
if (!filePath || !this.matchesScope(filePath, invariant)) continue;
|
|
437
|
+
|
|
438
|
+
const content = op.content || op.newContent;
|
|
439
|
+
if (!content) continue;
|
|
440
|
+
|
|
441
|
+
const lines = content.split("\n");
|
|
442
|
+
for (let i = 0; i < lines.length; i++) {
|
|
443
|
+
if (pattern.test(lines[i])) {
|
|
444
|
+
violations.push(this.createViolation(invariant, {
|
|
445
|
+
file: filePath,
|
|
446
|
+
line: i + 1,
|
|
447
|
+
message: `Direct access to '${invariant.pattern}' is forbidden`,
|
|
448
|
+
evidence: lines[i].trim(),
|
|
449
|
+
suggestion: {
|
|
450
|
+
action: "use_abstraction",
|
|
451
|
+
reason: invariant.description,
|
|
452
|
+
},
|
|
453
|
+
}));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return violations;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Evaluate env-must-be-registered rule
|
|
463
|
+
*/
|
|
464
|
+
evaluateEnvMustBeRegistered(invariant, proposal) {
|
|
465
|
+
const violations = [];
|
|
466
|
+
|
|
467
|
+
// Load registry if not loaded
|
|
468
|
+
if (!this.envRegistry && invariant.registry) {
|
|
469
|
+
this.loadEnvRegistry(invariant.registry);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!this.envRegistry) return violations;
|
|
473
|
+
|
|
474
|
+
// Pattern to find env var usage
|
|
475
|
+
const envPattern = /process\.env\.([A-Z_][A-Z0-9_]*)|import\.meta\.env\.([A-Z_][A-Z0-9_]*)/g;
|
|
476
|
+
|
|
477
|
+
for (const op of proposal.operations || []) {
|
|
478
|
+
const filePath = op.path || op.file;
|
|
479
|
+
if (!filePath || !this.matchesScope(filePath, invariant)) continue;
|
|
480
|
+
|
|
481
|
+
const content = op.content || op.newContent;
|
|
482
|
+
if (!content) continue;
|
|
483
|
+
|
|
484
|
+
let match;
|
|
485
|
+
const lines = content.split("\n");
|
|
486
|
+
|
|
487
|
+
for (let i = 0; i < lines.length; i++) {
|
|
488
|
+
while ((match = envPattern.exec(lines[i])) !== null) {
|
|
489
|
+
const envVar = match[1] || match[2];
|
|
490
|
+
|
|
491
|
+
if (!this.envRegistry.has(envVar)) {
|
|
492
|
+
violations.push(this.createViolation(invariant, {
|
|
493
|
+
file: filePath,
|
|
494
|
+
line: i + 1,
|
|
495
|
+
message: `Environment variable '${envVar}' is not registered in ${invariant.registry}`,
|
|
496
|
+
evidence: lines[i].trim(),
|
|
497
|
+
suggestion: {
|
|
498
|
+
action: "register_env_var",
|
|
499
|
+
variable: envVar,
|
|
500
|
+
registry: invariant.registry,
|
|
501
|
+
},
|
|
502
|
+
}));
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return violations;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Evaluate require-approval rule
|
|
513
|
+
*/
|
|
514
|
+
evaluateRequireApproval(invariant, proposal) {
|
|
515
|
+
const violations = [];
|
|
516
|
+
|
|
517
|
+
for (const op of proposal.operations || []) {
|
|
518
|
+
const filePath = op.path || op.file;
|
|
519
|
+
if (!filePath || !this.matchesScope(filePath, invariant)) continue;
|
|
520
|
+
|
|
521
|
+
// Check if proposal has required approval
|
|
522
|
+
const hasApproval = proposal.approvals?.some(a =>
|
|
523
|
+
a.invariantId === invariant.id && a.approved
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
if (!hasApproval) {
|
|
527
|
+
violations.push(this.createViolation(invariant, {
|
|
528
|
+
file: filePath,
|
|
529
|
+
message: `Changes to '${filePath}' require approval`,
|
|
530
|
+
evidence: `Operation type: ${op.type}`,
|
|
531
|
+
suggestion: {
|
|
532
|
+
action: "request_approval",
|
|
533
|
+
owner: invariant.owner,
|
|
534
|
+
reason: invariant.description,
|
|
535
|
+
},
|
|
536
|
+
}));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return violations;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Get all invariants
|
|
545
|
+
* @returns {Object[]} All loaded invariants
|
|
546
|
+
*/
|
|
547
|
+
getInvariants() {
|
|
548
|
+
return [...this.invariants];
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Get invariant by ID
|
|
553
|
+
* @param {string} id - Invariant ID
|
|
554
|
+
* @returns {Object|null} Invariant or null
|
|
555
|
+
*/
|
|
556
|
+
getInvariant(id) {
|
|
557
|
+
return this.invariants.find(i => i.id === id) || null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Add a single invariant
|
|
562
|
+
* @param {Object} invariant - Invariant to add
|
|
563
|
+
*/
|
|
564
|
+
addInvariant(invariant) {
|
|
565
|
+
const validation = validateInvariant(invariant);
|
|
566
|
+
if (!validation.valid) {
|
|
567
|
+
throw new Error(`Invalid invariant: ${validation.errors.map(e => e.message).join(", ")}`);
|
|
568
|
+
}
|
|
569
|
+
this.invariants.push(invariant);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Remove an invariant
|
|
574
|
+
* @param {string} id - Invariant ID to remove
|
|
575
|
+
* @returns {boolean} Success
|
|
576
|
+
*/
|
|
577
|
+
removeInvariant(id) {
|
|
578
|
+
const index = this.invariants.findIndex(i => i.id === id);
|
|
579
|
+
if (index !== -1) {
|
|
580
|
+
this.invariants.splice(index, 1);
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Clear all invariants
|
|
588
|
+
*/
|
|
589
|
+
clear() {
|
|
590
|
+
this.invariants = [];
|
|
591
|
+
this.envRegistry = null;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Create an evaluator instance
|
|
597
|
+
* @param {Object} options - Options
|
|
598
|
+
* @returns {InvariantEvaluator} Evaluator instance
|
|
599
|
+
*/
|
|
600
|
+
function createEvaluator(options = {}) {
|
|
601
|
+
return new InvariantEvaluator(options);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
module.exports = { InvariantEvaluator, createEvaluator };
|