@neurcode-ai/cli 0.8.9 → 0.8.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-client.d.ts +13 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +12 -0
- package/dist/api-client.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +52 -21
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/verify.d.ts +2 -0
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +321 -52
- package/dist/commands/verify.js.map +1 -1
- package/dist/critical_violation.d.ts +2 -0
- package/dist/critical_violation.d.ts.map +1 -0
- package/dist/critical_violation.js +9 -0
- package/dist/critical_violation.js.map +1 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/ignore.d.ts +12 -0
- package/dist/utils/ignore.d.ts.map +1 -0
- package/dist/utils/ignore.js +53 -0
- package/dist/utils/ignore.js.map +1 -0
- package/package.json +3 -1
package/dist/commands/verify.js
CHANGED
|
@@ -41,6 +41,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
41
41
|
exports.verifyCommand = verifyCommand;
|
|
42
42
|
const child_process_1 = require("child_process");
|
|
43
43
|
const diff_parser_1 = require("@neurcode-ai/diff-parser");
|
|
44
|
+
const policy_engine_1 = require("@neurcode-ai/policy-engine");
|
|
44
45
|
const config_1 = require("../config");
|
|
45
46
|
const api_client_1 = require("../api-client");
|
|
46
47
|
const path_1 = require("path");
|
|
@@ -48,6 +49,7 @@ const fs_1 = require("fs");
|
|
|
48
49
|
const state_1 = require("../utils/state");
|
|
49
50
|
const ROILogger_1 = require("../utils/ROILogger");
|
|
50
51
|
const box_1 = require("../utils/box");
|
|
52
|
+
const ignore_1 = require("../utils/ignore");
|
|
51
53
|
// Import chalk with fallback
|
|
52
54
|
let chalk;
|
|
53
55
|
try {
|
|
@@ -112,6 +114,67 @@ function isExcludedFile(filePath) {
|
|
|
112
114
|
}
|
|
113
115
|
return false;
|
|
114
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Map a dashboard custom policy (natural language) to a policy-engine Rule.
|
|
119
|
+
* Supports "No console.log", "No debugger", and generic line patterns.
|
|
120
|
+
*/
|
|
121
|
+
function customPolicyToRule(p) {
|
|
122
|
+
const sev = p.severity === 'high' ? 'block' : 'warn';
|
|
123
|
+
const text = (p.rule_text || '').trim().toLowerCase();
|
|
124
|
+
if (!text)
|
|
125
|
+
return null;
|
|
126
|
+
// Known patterns -> suspicious-keywords (checks added lines for these strings)
|
|
127
|
+
if (/console\.log|no\s+console\.log|do not use console\.log|ban console\.log/.test(text)) {
|
|
128
|
+
return {
|
|
129
|
+
id: `custom-${p.id}`,
|
|
130
|
+
name: 'No console.log',
|
|
131
|
+
description: p.rule_text,
|
|
132
|
+
enabled: true,
|
|
133
|
+
severity: sev,
|
|
134
|
+
type: 'suspicious-keywords',
|
|
135
|
+
keywords: ['console.log'],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (/debugger|no\s+debugger|do not use debugger/.test(text)) {
|
|
139
|
+
return {
|
|
140
|
+
id: `custom-${p.id}`,
|
|
141
|
+
name: 'No debugger',
|
|
142
|
+
description: p.rule_text,
|
|
143
|
+
enabled: true,
|
|
144
|
+
severity: sev,
|
|
145
|
+
type: 'suspicious-keywords',
|
|
146
|
+
keywords: ['debugger'],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (/eval\s*\(|no\s+eval|do not use eval/.test(text)) {
|
|
150
|
+
return {
|
|
151
|
+
id: `custom-${p.id}`,
|
|
152
|
+
name: 'No eval',
|
|
153
|
+
description: p.rule_text,
|
|
154
|
+
enabled: true,
|
|
155
|
+
severity: sev,
|
|
156
|
+
type: 'suspicious-keywords',
|
|
157
|
+
keywords: ['eval('],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// Fallback: line-pattern on added lines using a safe regex from the rule text
|
|
161
|
+
// Use first quoted phrase or first alphanumeric phrase as pattern
|
|
162
|
+
const quoted = /['"`]([^'"`]+)['"`]/.exec(p.rule_text);
|
|
163
|
+
const phrase = quoted?.[1] ?? p.rule_text.replace(/^(no|don't|do not use|ban|avoid)\s+/i, '').trim().slice(0, 80);
|
|
164
|
+
if (!phrase)
|
|
165
|
+
return null;
|
|
166
|
+
const escaped = phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
167
|
+
return {
|
|
168
|
+
id: `custom-${p.id}`,
|
|
169
|
+
name: `Custom: ${p.rule_text.slice(0, 50)}`,
|
|
170
|
+
description: p.rule_text,
|
|
171
|
+
enabled: true,
|
|
172
|
+
severity: sev,
|
|
173
|
+
type: 'line-pattern',
|
|
174
|
+
pattern: escaped,
|
|
175
|
+
matchType: 'added',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
115
178
|
async function verifyCommand(options) {
|
|
116
179
|
try {
|
|
117
180
|
// Load configuration
|
|
@@ -187,8 +250,10 @@ async function verifyCommand(options) {
|
|
|
187
250
|
else {
|
|
188
251
|
console.log(JSON.stringify({
|
|
189
252
|
grade: 'F',
|
|
190
|
-
|
|
253
|
+
score: 0,
|
|
191
254
|
verdict: 'FAIL',
|
|
255
|
+
violations: [],
|
|
256
|
+
adherenceScore: 0,
|
|
192
257
|
bloatCount: 0,
|
|
193
258
|
bloatFiles: [],
|
|
194
259
|
plannedFilesModified: 0,
|
|
@@ -201,12 +266,17 @@ async function verifyCommand(options) {
|
|
|
201
266
|
}
|
|
202
267
|
// Parse the diff
|
|
203
268
|
const allDiffFiles = (0, diff_parser_1.parseDiff)(diffText);
|
|
269
|
+
console.log('DEBUG: Raw Diff Files:', JSON.stringify(allDiffFiles.map(f => f.path), null, 2));
|
|
204
270
|
// Filter out internal/system files before analysis
|
|
205
271
|
// This prevents self-interference where the tool flags its own files as bloat
|
|
206
272
|
const diffFiles = allDiffFiles.filter(file => {
|
|
207
273
|
// Check both path and oldPath (for renames) against exclusion list
|
|
208
274
|
const excludePath = isExcludedFile(file.path);
|
|
209
275
|
const excludeOldPath = file.oldPath ? isExcludedFile(file.oldPath) : false;
|
|
276
|
+
if (excludePath)
|
|
277
|
+
console.log(`DEBUG: System Excluded: ${file.path}`);
|
|
278
|
+
if (excludeOldPath)
|
|
279
|
+
console.log(`DEBUG: System Excluded (oldPath): ${file.oldPath}`);
|
|
210
280
|
return !excludePath && !excludeOldPath;
|
|
211
281
|
});
|
|
212
282
|
const summary = (0, diff_parser_1.getDiffSummary)(diffFiles);
|
|
@@ -217,8 +287,10 @@ async function verifyCommand(options) {
|
|
|
217
287
|
else {
|
|
218
288
|
console.log(JSON.stringify({
|
|
219
289
|
grade: 'F',
|
|
220
|
-
|
|
290
|
+
score: 0,
|
|
221
291
|
verdict: 'FAIL',
|
|
292
|
+
violations: [],
|
|
293
|
+
adherenceScore: 0,
|
|
222
294
|
bloatCount: 0,
|
|
223
295
|
bloatFiles: [],
|
|
224
296
|
plannedFilesModified: 0,
|
|
@@ -229,11 +301,99 @@ async function verifyCommand(options) {
|
|
|
229
301
|
}
|
|
230
302
|
process.exit(0);
|
|
231
303
|
}
|
|
304
|
+
const ignoreFilter = (0, ignore_1.loadIgnore)(process.cwd());
|
|
232
305
|
if (!options.json) {
|
|
233
306
|
console.log(chalk.cyan('\n📊 Analyzing changes against plan...'));
|
|
234
307
|
console.log(chalk.dim(` Found ${summary.totalFiles} file(s) changed`));
|
|
235
308
|
console.log(chalk.dim(` ${summary.totalAdded} lines added, ${summary.totalRemoved} lines removed\n`));
|
|
236
309
|
}
|
|
310
|
+
// ============================================
|
|
311
|
+
// --policy-only: General Governance (policy only, no plan enforcement)
|
|
312
|
+
// ============================================
|
|
313
|
+
if (options.policyOnly) {
|
|
314
|
+
if (!options.json) {
|
|
315
|
+
console.log(chalk.cyan('🛡️ General Governance mode (policy only, no plan linked)\n'));
|
|
316
|
+
}
|
|
317
|
+
let policyViolations = [];
|
|
318
|
+
let policyDecision = 'allow';
|
|
319
|
+
const defaultPolicy = (0, policy_engine_1.createDefaultPolicy)();
|
|
320
|
+
let allRules = [...defaultPolicy.rules];
|
|
321
|
+
if (config.apiKey) {
|
|
322
|
+
try {
|
|
323
|
+
const customPolicies = await client.getActiveCustomPolicies();
|
|
324
|
+
const customRules = [];
|
|
325
|
+
for (const p of customPolicies) {
|
|
326
|
+
const r = customPolicyToRule(p);
|
|
327
|
+
if (r)
|
|
328
|
+
customRules.push(r);
|
|
329
|
+
}
|
|
330
|
+
allRules = [...defaultPolicy.rules, ...customRules];
|
|
331
|
+
if (!options.json && customRules.length > 0) {
|
|
332
|
+
console.log(chalk.dim(` Evaluating ${customRules.length} custom policy rule(s) from dashboard`));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
if (!options.json) {
|
|
337
|
+
console.log(chalk.dim(' Could not load custom policies, using default policy only'));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const diffFilesForPolicy = diffFiles.filter((f) => {
|
|
342
|
+
const ignored = ignoreFilter(f.path);
|
|
343
|
+
if (ignored)
|
|
344
|
+
console.log(`DEBUG: User Ignored: ${f.path}`);
|
|
345
|
+
return !ignored;
|
|
346
|
+
});
|
|
347
|
+
console.log('DEBUG: Files sending to Policy Engine:', JSON.stringify(diffFilesForPolicy.map(f => f.path), null, 2));
|
|
348
|
+
const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, allRules);
|
|
349
|
+
policyViolations = (policyResult.violations || []);
|
|
350
|
+
policyViolations = policyViolations.filter((v) => !ignoreFilter(v.file));
|
|
351
|
+
policyDecision = policyViolations.length > 0 ? policyResult.decision : 'allow';
|
|
352
|
+
const effectiveVerdict = policyDecision === 'block' ? 'FAIL' : policyDecision === 'warn' ? 'WARN' : 'PASS';
|
|
353
|
+
const grade = effectiveVerdict === 'PASS' ? 'A' : effectiveVerdict === 'WARN' ? 'C' : 'F';
|
|
354
|
+
const score = effectiveVerdict === 'PASS' ? 100 : effectiveVerdict === 'WARN' ? 50 : 0;
|
|
355
|
+
const violationsOutput = policyViolations.map((v) => ({
|
|
356
|
+
file: v.file,
|
|
357
|
+
rule: v.rule,
|
|
358
|
+
severity: v.severity,
|
|
359
|
+
message: v.message,
|
|
360
|
+
...(v.line != null ? { startLine: v.line } : {}),
|
|
361
|
+
}));
|
|
362
|
+
const message = effectiveVerdict === 'PASS'
|
|
363
|
+
? '✅ Policy check passed (General Governance mode)'
|
|
364
|
+
: policyViolations.length > 0
|
|
365
|
+
? `Policy violations: ${policyViolations.map((v) => `${v.file}: ${v.message || v.rule}`).join('; ')}`
|
|
366
|
+
: 'Policy check completed';
|
|
367
|
+
if (options.json) {
|
|
368
|
+
console.log(JSON.stringify({
|
|
369
|
+
grade,
|
|
370
|
+
score,
|
|
371
|
+
verdict: effectiveVerdict,
|
|
372
|
+
violations: violationsOutput,
|
|
373
|
+
message,
|
|
374
|
+
scopeGuardPassed: true, // N/A in policy-only mode
|
|
375
|
+
bloatCount: 0,
|
|
376
|
+
bloatFiles: [],
|
|
377
|
+
plannedFilesModified: 0,
|
|
378
|
+
totalPlannedFiles: 0,
|
|
379
|
+
adherenceScore: score,
|
|
380
|
+
policyOnly: true,
|
|
381
|
+
}, null, 2));
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
if (effectiveVerdict === 'PASS') {
|
|
385
|
+
console.log(chalk.green('✅ Policy check passed'));
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
console.log(chalk.red(`❌ Policy violations detected: ${policyViolations.length}`));
|
|
389
|
+
policyViolations.forEach((v) => {
|
|
390
|
+
console.log(chalk.red(` • ${v.file}: ${v.message || v.rule}`));
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
console.log(chalk.dim(`\n${message}`));
|
|
394
|
+
}
|
|
395
|
+
process.exit(effectiveVerdict === 'FAIL' ? 2 : effectiveVerdict === 'WARN' ? 1 : 0);
|
|
396
|
+
}
|
|
237
397
|
// Get planId: Priority 1: options flag, Priority 2: state file (.neurcode/config.json), Priority 3: legacy config
|
|
238
398
|
let planId = options.planId;
|
|
239
399
|
if (!planId) {
|
|
@@ -278,8 +438,10 @@ async function verifyCommand(options) {
|
|
|
278
438
|
else {
|
|
279
439
|
console.log(JSON.stringify({
|
|
280
440
|
grade: 'F',
|
|
281
|
-
|
|
441
|
+
score: 0,
|
|
282
442
|
verdict: 'FAIL',
|
|
443
|
+
violations: [],
|
|
444
|
+
adherenceScore: 0,
|
|
283
445
|
bloatCount: 0,
|
|
284
446
|
bloatFiles: [],
|
|
285
447
|
plannedFilesModified: 0,
|
|
@@ -353,19 +515,28 @@ async function verifyCommand(options) {
|
|
|
353
515
|
// Step C: The Intersection Logic
|
|
354
516
|
const approvedSet = new Set([...planFiles, ...allowedFiles]);
|
|
355
517
|
const violations = modifiedFiles.filter(f => !approvedSet.has(f));
|
|
356
|
-
|
|
357
|
-
|
|
518
|
+
const filteredViolations = violations.filter((p) => !ignoreFilter(p));
|
|
519
|
+
// Step D: The Block (only report scope violations for non-ignored files)
|
|
520
|
+
if (filteredViolations.length > 0) {
|
|
358
521
|
if (options.json) {
|
|
359
|
-
// Output JSON for scope violation BEFORE exit
|
|
522
|
+
// Output JSON for scope violation BEFORE exit. Must include violations for GitHub Action annotations.
|
|
523
|
+
const violationsOutput = filteredViolations.map((file) => ({
|
|
524
|
+
file,
|
|
525
|
+
rule: 'scope_guard',
|
|
526
|
+
severity: 'block',
|
|
527
|
+
message: 'File modified outside the plan',
|
|
528
|
+
}));
|
|
360
529
|
const jsonOutput = {
|
|
361
530
|
grade: 'F',
|
|
362
|
-
|
|
531
|
+
score: 0,
|
|
363
532
|
verdict: 'FAIL',
|
|
364
|
-
|
|
365
|
-
|
|
533
|
+
violations: violationsOutput,
|
|
534
|
+
adherenceScore: 0,
|
|
535
|
+
bloatCount: filteredViolations.length,
|
|
536
|
+
bloatFiles: filteredViolations,
|
|
366
537
|
plannedFilesModified: 0,
|
|
367
538
|
totalPlannedFiles: planFiles.length,
|
|
368
|
-
message: `Scope violation: ${
|
|
539
|
+
message: `Scope violation: ${filteredViolations.length} file(s) modified outside the plan`,
|
|
369
540
|
scopeGuardPassed: false,
|
|
370
541
|
};
|
|
371
542
|
// CRITICAL: Print JSON first, then exit
|
|
@@ -378,12 +549,12 @@ async function verifyCommand(options) {
|
|
|
378
549
|
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
379
550
|
console.log(chalk.red('The following files were modified but are not in the plan:'));
|
|
380
551
|
console.log('');
|
|
381
|
-
|
|
552
|
+
filteredViolations.forEach(file => {
|
|
382
553
|
console.log(chalk.red(` • ${file}`));
|
|
383
554
|
});
|
|
384
555
|
console.log('');
|
|
385
556
|
console.log(chalk.yellow('To unblock these files, run:'));
|
|
386
|
-
|
|
557
|
+
filteredViolations.forEach(file => {
|
|
387
558
|
console.log(chalk.dim(` neurcode allow ${file}`));
|
|
388
559
|
});
|
|
389
560
|
console.log('');
|
|
@@ -424,8 +595,10 @@ async function verifyCommand(options) {
|
|
|
424
595
|
else {
|
|
425
596
|
console.log(JSON.stringify({
|
|
426
597
|
grade: 'N/A',
|
|
427
|
-
|
|
598
|
+
score: 0,
|
|
428
599
|
verdict: 'INFO',
|
|
600
|
+
violations: [],
|
|
601
|
+
adherenceScore: 0,
|
|
429
602
|
bloatCount: 0,
|
|
430
603
|
bloatFiles: [],
|
|
431
604
|
plannedFilesModified: 0,
|
|
@@ -437,6 +610,39 @@ async function verifyCommand(options) {
|
|
|
437
610
|
}
|
|
438
611
|
process.exit(0);
|
|
439
612
|
}
|
|
613
|
+
let policyViolations = [];
|
|
614
|
+
let policyDecision = 'allow';
|
|
615
|
+
if (config.apiKey) {
|
|
616
|
+
try {
|
|
617
|
+
const customPolicies = await client.getActiveCustomPolicies();
|
|
618
|
+
const defaultPolicy = (0, policy_engine_1.createDefaultPolicy)();
|
|
619
|
+
const customRules = [];
|
|
620
|
+
for (const p of customPolicies) {
|
|
621
|
+
const r = customPolicyToRule(p);
|
|
622
|
+
if (r)
|
|
623
|
+
customRules.push(r);
|
|
624
|
+
}
|
|
625
|
+
const allRules = [...defaultPolicy.rules, ...customRules];
|
|
626
|
+
const diffFilesForPolicy = diffFiles.filter((f) => {
|
|
627
|
+
const ignored = ignoreFilter(f.path);
|
|
628
|
+
if (ignored)
|
|
629
|
+
console.log(`DEBUG: User Ignored: ${f.path}`);
|
|
630
|
+
return !ignored;
|
|
631
|
+
});
|
|
632
|
+
console.log('DEBUG: Files sending to Policy Engine:', JSON.stringify(diffFilesForPolicy.map(f => f.path), null, 2));
|
|
633
|
+
const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, allRules);
|
|
634
|
+
policyViolations = policyResult.violations.filter((v) => !ignoreFilter(v.file));
|
|
635
|
+
policyDecision = policyViolations.length > 0 ? policyResult.decision : 'allow';
|
|
636
|
+
if (!options.json && customRules.length > 0) {
|
|
637
|
+
console.log(chalk.dim(` Evaluating ${customRules.length} custom policy rule(s) from dashboard`));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
if (!options.json) {
|
|
642
|
+
console.log(chalk.dim(' Could not load custom policies, continuing without them'));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
440
646
|
// Prepare diff stats and changed files for API
|
|
441
647
|
const diffStats = {
|
|
442
648
|
totalAdded: summary.totalAdded,
|
|
@@ -478,7 +684,13 @@ async function verifyCommand(options) {
|
|
|
478
684
|
}
|
|
479
685
|
// Call verifyPlan with intentConstraints
|
|
480
686
|
const verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraints);
|
|
481
|
-
//
|
|
687
|
+
// Apply custom policy verdict: block from dashboard overrides API verdict
|
|
688
|
+
const policyBlock = policyDecision === 'block' && policyViolations.length > 0;
|
|
689
|
+
const effectiveVerdict = policyBlock ? 'FAIL' : verifyResult.verdict;
|
|
690
|
+
const effectiveMessage = policyBlock
|
|
691
|
+
? `Custom policy violations: ${policyViolations.map(v => `${v.file}: ${v.message || v.rule}`).join('; ')}. ${verifyResult.message}`
|
|
692
|
+
: verifyResult.message;
|
|
693
|
+
// Calculate grade from effective verdict and score
|
|
482
694
|
// CRITICAL: 0/0 planned files = 'F' (Incomplete), not 'B'
|
|
483
695
|
// Bloat automatically drops grade by at least one letter
|
|
484
696
|
let grade;
|
|
@@ -486,7 +698,7 @@ async function verifyCommand(options) {
|
|
|
486
698
|
if (verifyResult.totalPlannedFiles === 0 && verifyResult.plannedFilesModified === 0) {
|
|
487
699
|
grade = 'F';
|
|
488
700
|
}
|
|
489
|
-
else if (
|
|
701
|
+
else if (effectiveVerdict === 'PASS') {
|
|
490
702
|
grade = 'A';
|
|
491
703
|
// Log ROI event for PASS verification (Grade A) - non-blocking
|
|
492
704
|
try {
|
|
@@ -503,7 +715,7 @@ async function verifyCommand(options) {
|
|
|
503
715
|
// Silently ignore - ROI logging should never block user workflows
|
|
504
716
|
}
|
|
505
717
|
}
|
|
506
|
-
else if (
|
|
718
|
+
else if (effectiveVerdict === 'WARN') {
|
|
507
719
|
// Base grade calculation
|
|
508
720
|
let baseGrade = verifyResult.adherenceScore >= 70 ? 'B' : verifyResult.adherenceScore >= 50 ? 'C' : 'D';
|
|
509
721
|
// Bloat drops grade by one letter (B -> C, C -> D, D -> F)
|
|
@@ -522,49 +734,82 @@ async function verifyCommand(options) {
|
|
|
522
734
|
}
|
|
523
735
|
// If JSON output requested, output JSON and exit
|
|
524
736
|
if (options.json) {
|
|
737
|
+
const filteredBloatFiles = (verifyResult.bloatFiles || []).filter((f) => !ignoreFilter(f));
|
|
738
|
+
const scopeViolations = filteredBloatFiles.map((file) => ({
|
|
739
|
+
file,
|
|
740
|
+
rule: 'scope_guard',
|
|
741
|
+
severity: 'block',
|
|
742
|
+
message: 'File modified outside the plan',
|
|
743
|
+
}));
|
|
744
|
+
const policyViolationItems = policyViolations.map((v) => ({
|
|
745
|
+
file: v.file,
|
|
746
|
+
rule: v.rule,
|
|
747
|
+
severity: v.severity,
|
|
748
|
+
message: v.message,
|
|
749
|
+
...(v.line != null ? { startLine: v.line } : {}),
|
|
750
|
+
}));
|
|
751
|
+
const violations = [...scopeViolations, ...policyViolationItems];
|
|
525
752
|
const jsonOutput = {
|
|
526
753
|
grade,
|
|
754
|
+
score: verifyResult.adherenceScore,
|
|
755
|
+
verdict: effectiveVerdict,
|
|
756
|
+
violations,
|
|
757
|
+
message: effectiveMessage,
|
|
527
758
|
adherenceScore: verifyResult.adherenceScore,
|
|
528
|
-
|
|
529
|
-
bloatCount:
|
|
530
|
-
bloatFiles:
|
|
759
|
+
scopeGuardPassed,
|
|
760
|
+
bloatCount: filteredBloatFiles.length,
|
|
761
|
+
bloatFiles: filteredBloatFiles,
|
|
531
762
|
plannedFilesModified: verifyResult.plannedFilesModified,
|
|
532
763
|
totalPlannedFiles: verifyResult.totalPlannedFiles,
|
|
533
|
-
|
|
534
|
-
scopeGuardPassed,
|
|
764
|
+
...(policyViolations.length > 0 && { policyDecision }),
|
|
535
765
|
};
|
|
536
766
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
537
767
|
// Report to Neurcode Cloud if --record flag is set (after JSON output)
|
|
538
768
|
if (options.record && config.apiKey) {
|
|
539
|
-
const violations =
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
769
|
+
const violations = [
|
|
770
|
+
...filteredBloatFiles.map((file) => ({
|
|
771
|
+
rule: 'scope_guard',
|
|
772
|
+
file: file,
|
|
773
|
+
severity: 'block',
|
|
774
|
+
message: 'File modified outside the plan',
|
|
775
|
+
})),
|
|
776
|
+
...policyViolations.map(v => ({
|
|
777
|
+
rule: v.rule,
|
|
778
|
+
file: v.file,
|
|
779
|
+
severity: v.severity,
|
|
780
|
+
message: v.message,
|
|
781
|
+
})),
|
|
782
|
+
];
|
|
545
783
|
// Report in background (don't await to avoid blocking JSON output)
|
|
546
784
|
reportVerification(grade, violations, verifyResult, config.apiKey, config.apiUrl, projectId || undefined, true // jsonMode = true
|
|
547
785
|
).catch(() => {
|
|
548
786
|
// Error already logged in reportVerification
|
|
549
787
|
});
|
|
550
788
|
}
|
|
551
|
-
// Exit based on verdict (same logic as below)
|
|
552
|
-
if (scopeGuardPassed) {
|
|
789
|
+
// Exit based on effective verdict (same logic as below)
|
|
790
|
+
if (scopeGuardPassed && !policyBlock) {
|
|
553
791
|
process.exit(0);
|
|
554
792
|
}
|
|
555
|
-
if (
|
|
793
|
+
if (effectiveVerdict === 'FAIL') {
|
|
556
794
|
process.exit(2);
|
|
557
795
|
}
|
|
558
|
-
else if (
|
|
796
|
+
else if (effectiveVerdict === 'WARN') {
|
|
559
797
|
process.exit(1);
|
|
560
798
|
}
|
|
561
799
|
else {
|
|
562
800
|
process.exit(0);
|
|
563
801
|
}
|
|
564
802
|
}
|
|
565
|
-
// Display results (only if not in json mode)
|
|
803
|
+
// Display results (only if not in json mode; exclude ignored paths from bloat)
|
|
566
804
|
if (!options.json) {
|
|
567
|
-
|
|
805
|
+
const displayBloatFiles = (verifyResult.bloatFiles || []).filter((f) => !ignoreFilter(f));
|
|
806
|
+
displayVerifyResults({
|
|
807
|
+
...verifyResult,
|
|
808
|
+
verdict: effectiveVerdict,
|
|
809
|
+
message: effectiveMessage,
|
|
810
|
+
bloatFiles: displayBloatFiles,
|
|
811
|
+
bloatCount: displayBloatFiles.length,
|
|
812
|
+
}, policyViolations);
|
|
568
813
|
}
|
|
569
814
|
// Report to Neurcode Cloud if --record flag is set
|
|
570
815
|
if (options.record) {
|
|
@@ -575,23 +820,30 @@ async function verifyCommand(options) {
|
|
|
575
820
|
}
|
|
576
821
|
}
|
|
577
822
|
else {
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
823
|
+
// Include scope bloat and custom policy violations in the report (excluding .neurcodeignore'd paths)
|
|
824
|
+
const filteredBloatForReport = (verifyResult.bloatFiles || []).filter((f) => !ignoreFilter(f));
|
|
825
|
+
const violations = [
|
|
826
|
+
...filteredBloatForReport.map((file) => ({
|
|
827
|
+
rule: 'scope_guard',
|
|
828
|
+
file: file,
|
|
829
|
+
severity: 'block',
|
|
830
|
+
message: 'File modified outside the plan',
|
|
831
|
+
})),
|
|
832
|
+
...policyViolations.map(v => ({
|
|
833
|
+
rule: v.rule,
|
|
834
|
+
file: v.file,
|
|
835
|
+
severity: v.severity,
|
|
836
|
+
message: v.message,
|
|
837
|
+
})),
|
|
838
|
+
];
|
|
587
839
|
await reportVerification(grade, violations, verifyResult, config.apiKey, config.apiUrl, projectId || undefined, false // jsonMode = false
|
|
588
840
|
);
|
|
589
841
|
}
|
|
590
842
|
}
|
|
591
843
|
// Governance takes priority over Grading
|
|
592
|
-
// If Scope Guard passed (all files approved or allowed), always PASS
|
|
593
|
-
if (scopeGuardPassed) {
|
|
594
|
-
if (verifyResult.verdict === 'FAIL' || verifyResult.verdict === 'WARN') {
|
|
844
|
+
// If Scope Guard passed (all files approved or allowed) and no policy block, always PASS
|
|
845
|
+
if (scopeGuardPassed && !policyBlock) {
|
|
846
|
+
if ((verifyResult.verdict === 'FAIL' || verifyResult.verdict === 'WARN') && policyViolations.length === 0) {
|
|
595
847
|
if (!options.json) {
|
|
596
848
|
console.log(chalk.yellow('\n⚠️ Plan deviation allowed'));
|
|
597
849
|
console.log(chalk.dim(' Some files were modified outside the plan, but they were explicitly allowed.'));
|
|
@@ -600,12 +852,12 @@ async function verifyCommand(options) {
|
|
|
600
852
|
}
|
|
601
853
|
process.exit(0);
|
|
602
854
|
}
|
|
603
|
-
// If scope guard didn't pass (or failed to check), use
|
|
604
|
-
// Exit with appropriate code based on AI verification
|
|
605
|
-
if (
|
|
855
|
+
// If scope guard didn't pass (or failed to check) or policy blocked, use effective verdict
|
|
856
|
+
// Exit with appropriate code based on AI verification and custom policies
|
|
857
|
+
if (effectiveVerdict === 'FAIL') {
|
|
606
858
|
process.exit(2);
|
|
607
859
|
}
|
|
608
|
-
else if (
|
|
860
|
+
else if (effectiveVerdict === 'WARN') {
|
|
609
861
|
process.exit(1);
|
|
610
862
|
}
|
|
611
863
|
else {
|
|
@@ -617,8 +869,10 @@ async function verifyCommand(options) {
|
|
|
617
869
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
618
870
|
console.log(JSON.stringify({
|
|
619
871
|
grade: 'F',
|
|
620
|
-
|
|
872
|
+
score: 0,
|
|
621
873
|
verdict: 'FAIL',
|
|
874
|
+
violations: [],
|
|
875
|
+
adherenceScore: 0,
|
|
622
876
|
bloatCount: 0,
|
|
623
877
|
bloatFiles: [],
|
|
624
878
|
plannedFilesModified: 0,
|
|
@@ -650,8 +904,10 @@ async function verifyCommand(options) {
|
|
|
650
904
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
651
905
|
console.log(JSON.stringify({
|
|
652
906
|
grade: 'F',
|
|
653
|
-
|
|
907
|
+
score: 0,
|
|
654
908
|
verdict: 'FAIL',
|
|
909
|
+
violations: [],
|
|
910
|
+
adherenceScore: 0,
|
|
655
911
|
bloatCount: 0,
|
|
656
912
|
bloatFiles: [],
|
|
657
913
|
plannedFilesModified: 0,
|
|
@@ -812,7 +1068,7 @@ async function reportVerification(grade, violations, verifyResult, apiKey, apiUr
|
|
|
812
1068
|
/**
|
|
813
1069
|
* Display verification results in a formatted report card
|
|
814
1070
|
*/
|
|
815
|
-
function displayVerifyResults(result) {
|
|
1071
|
+
function displayVerifyResults(result, policyViolations) {
|
|
816
1072
|
// Calculate grade/score
|
|
817
1073
|
// CRITICAL: 0/0 planned files = 'F' (Incomplete)
|
|
818
1074
|
// Bloat automatically drops grade by at least one letter
|
|
@@ -898,6 +1154,19 @@ function displayVerifyResults(result) {
|
|
|
898
1154
|
console.log('');
|
|
899
1155
|
console.log(chalk.green('✅ No bloat detected - all changes match the plan'));
|
|
900
1156
|
}
|
|
1157
|
+
// Display custom policy violations from dashboard
|
|
1158
|
+
if (policyViolations && policyViolations.length > 0) {
|
|
1159
|
+
console.log('');
|
|
1160
|
+
const blockCount = policyViolations.filter(v => v.severity === 'block').length;
|
|
1161
|
+
const label = blockCount > 0
|
|
1162
|
+
? chalk.bold.red(`🚫 Custom Policy Violations: ${policyViolations.length} (${blockCount} blocking)`)
|
|
1163
|
+
: chalk.bold.yellow(`⚠️ Custom Policy Warnings: ${policyViolations.length}`);
|
|
1164
|
+
console.log(label);
|
|
1165
|
+
policyViolations.forEach(v => {
|
|
1166
|
+
const lineColor = v.severity === 'block' ? chalk.red : chalk.yellow;
|
|
1167
|
+
console.log(lineColor(` • ${v.file}: ${v.message || v.rule}`));
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
901
1170
|
console.log('');
|
|
902
1171
|
console.log('━'.repeat(50));
|
|
903
1172
|
console.log(chalk.dim(result.message));
|