@neurcode-ai/cli 0.8.8 → 0.8.13
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 +304 -52
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/services/project-knowledge-service.d.ts.map +1 -1
- package/dist/services/project-knowledge-service.js +14 -50
- package/dist/services/project-knowledge-service.js.map +1 -1
- package/dist/utils/RelevanceScorer.d.ts.map +1 -1
- package/dist/utils/RelevanceScorer.js +17 -17
- package/dist/utils/RelevanceScorer.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,
|
|
@@ -217,8 +282,10 @@ async function verifyCommand(options) {
|
|
|
217
282
|
else {
|
|
218
283
|
console.log(JSON.stringify({
|
|
219
284
|
grade: 'F',
|
|
220
|
-
|
|
285
|
+
score: 0,
|
|
221
286
|
verdict: 'FAIL',
|
|
287
|
+
violations: [],
|
|
288
|
+
adherenceScore: 0,
|
|
222
289
|
bloatCount: 0,
|
|
223
290
|
bloatFiles: [],
|
|
224
291
|
plannedFilesModified: 0,
|
|
@@ -229,11 +296,93 @@ async function verifyCommand(options) {
|
|
|
229
296
|
}
|
|
230
297
|
process.exit(0);
|
|
231
298
|
}
|
|
299
|
+
const ignoreFilter = (0, ignore_1.loadIgnore)(process.cwd());
|
|
232
300
|
if (!options.json) {
|
|
233
301
|
console.log(chalk.cyan('\n📊 Analyzing changes against plan...'));
|
|
234
302
|
console.log(chalk.dim(` Found ${summary.totalFiles} file(s) changed`));
|
|
235
303
|
console.log(chalk.dim(` ${summary.totalAdded} lines added, ${summary.totalRemoved} lines removed\n`));
|
|
236
304
|
}
|
|
305
|
+
// ============================================
|
|
306
|
+
// --policy-only: General Governance (policy only, no plan enforcement)
|
|
307
|
+
// ============================================
|
|
308
|
+
if (options.policyOnly) {
|
|
309
|
+
if (!options.json) {
|
|
310
|
+
console.log(chalk.cyan('🛡️ General Governance mode (policy only, no plan linked)\n'));
|
|
311
|
+
}
|
|
312
|
+
let policyViolations = [];
|
|
313
|
+
let policyDecision = 'allow';
|
|
314
|
+
const defaultPolicy = (0, policy_engine_1.createDefaultPolicy)();
|
|
315
|
+
let allRules = [...defaultPolicy.rules];
|
|
316
|
+
if (config.apiKey) {
|
|
317
|
+
try {
|
|
318
|
+
const customPolicies = await client.getActiveCustomPolicies();
|
|
319
|
+
const customRules = [];
|
|
320
|
+
for (const p of customPolicies) {
|
|
321
|
+
const r = customPolicyToRule(p);
|
|
322
|
+
if (r)
|
|
323
|
+
customRules.push(r);
|
|
324
|
+
}
|
|
325
|
+
allRules = [...defaultPolicy.rules, ...customRules];
|
|
326
|
+
if (!options.json && customRules.length > 0) {
|
|
327
|
+
console.log(chalk.dim(` Evaluating ${customRules.length} custom policy rule(s) from dashboard`));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
if (!options.json) {
|
|
332
|
+
console.log(chalk.dim(' Could not load custom policies, using default policy only'));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const diffFilesForPolicy = diffFiles.filter((f) => !ignoreFilter(f.path));
|
|
337
|
+
const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, allRules);
|
|
338
|
+
policyViolations = (policyResult.violations || []);
|
|
339
|
+
policyViolations = policyViolations.filter((v) => !ignoreFilter(v.file));
|
|
340
|
+
policyDecision = policyViolations.length > 0 ? policyResult.decision : 'allow';
|
|
341
|
+
const effectiveVerdict = policyDecision === 'block' ? 'FAIL' : policyDecision === 'warn' ? 'WARN' : 'PASS';
|
|
342
|
+
const grade = effectiveVerdict === 'PASS' ? 'A' : effectiveVerdict === 'WARN' ? 'C' : 'F';
|
|
343
|
+
const score = effectiveVerdict === 'PASS' ? 100 : effectiveVerdict === 'WARN' ? 50 : 0;
|
|
344
|
+
const violationsOutput = policyViolations.map((v) => ({
|
|
345
|
+
file: v.file,
|
|
346
|
+
rule: v.rule,
|
|
347
|
+
severity: v.severity,
|
|
348
|
+
message: v.message,
|
|
349
|
+
...(v.line != null ? { startLine: v.line } : {}),
|
|
350
|
+
}));
|
|
351
|
+
const message = effectiveVerdict === 'PASS'
|
|
352
|
+
? '✅ Policy check passed (General Governance mode)'
|
|
353
|
+
: policyViolations.length > 0
|
|
354
|
+
? `Policy violations: ${policyViolations.map((v) => `${v.file}: ${v.message || v.rule}`).join('; ')}`
|
|
355
|
+
: 'Policy check completed';
|
|
356
|
+
if (options.json) {
|
|
357
|
+
console.log(JSON.stringify({
|
|
358
|
+
grade,
|
|
359
|
+
score,
|
|
360
|
+
verdict: effectiveVerdict,
|
|
361
|
+
violations: violationsOutput,
|
|
362
|
+
message,
|
|
363
|
+
scopeGuardPassed: true, // N/A in policy-only mode
|
|
364
|
+
bloatCount: 0,
|
|
365
|
+
bloatFiles: [],
|
|
366
|
+
plannedFilesModified: 0,
|
|
367
|
+
totalPlannedFiles: 0,
|
|
368
|
+
adherenceScore: score,
|
|
369
|
+
policyOnly: true,
|
|
370
|
+
}, null, 2));
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
if (effectiveVerdict === 'PASS') {
|
|
374
|
+
console.log(chalk.green('✅ Policy check passed'));
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
console.log(chalk.red(`❌ Policy violations detected: ${policyViolations.length}`));
|
|
378
|
+
policyViolations.forEach((v) => {
|
|
379
|
+
console.log(chalk.red(` • ${v.file}: ${v.message || v.rule}`));
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
console.log(chalk.dim(`\n${message}`));
|
|
383
|
+
}
|
|
384
|
+
process.exit(effectiveVerdict === 'FAIL' ? 2 : effectiveVerdict === 'WARN' ? 1 : 0);
|
|
385
|
+
}
|
|
237
386
|
// Get planId: Priority 1: options flag, Priority 2: state file (.neurcode/config.json), Priority 3: legacy config
|
|
238
387
|
let planId = options.planId;
|
|
239
388
|
if (!planId) {
|
|
@@ -278,8 +427,10 @@ async function verifyCommand(options) {
|
|
|
278
427
|
else {
|
|
279
428
|
console.log(JSON.stringify({
|
|
280
429
|
grade: 'F',
|
|
281
|
-
|
|
430
|
+
score: 0,
|
|
282
431
|
verdict: 'FAIL',
|
|
432
|
+
violations: [],
|
|
433
|
+
adherenceScore: 0,
|
|
283
434
|
bloatCount: 0,
|
|
284
435
|
bloatFiles: [],
|
|
285
436
|
plannedFilesModified: 0,
|
|
@@ -353,19 +504,28 @@ async function verifyCommand(options) {
|
|
|
353
504
|
// Step C: The Intersection Logic
|
|
354
505
|
const approvedSet = new Set([...planFiles, ...allowedFiles]);
|
|
355
506
|
const violations = modifiedFiles.filter(f => !approvedSet.has(f));
|
|
356
|
-
|
|
357
|
-
|
|
507
|
+
const filteredViolations = violations.filter((p) => !ignoreFilter(p));
|
|
508
|
+
// Step D: The Block (only report scope violations for non-ignored files)
|
|
509
|
+
if (filteredViolations.length > 0) {
|
|
358
510
|
if (options.json) {
|
|
359
|
-
// Output JSON for scope violation BEFORE exit
|
|
511
|
+
// Output JSON for scope violation BEFORE exit. Must include violations for GitHub Action annotations.
|
|
512
|
+
const violationsOutput = filteredViolations.map((file) => ({
|
|
513
|
+
file,
|
|
514
|
+
rule: 'scope_guard',
|
|
515
|
+
severity: 'block',
|
|
516
|
+
message: 'File modified outside the plan',
|
|
517
|
+
}));
|
|
360
518
|
const jsonOutput = {
|
|
361
519
|
grade: 'F',
|
|
362
|
-
|
|
520
|
+
score: 0,
|
|
363
521
|
verdict: 'FAIL',
|
|
364
|
-
|
|
365
|
-
|
|
522
|
+
violations: violationsOutput,
|
|
523
|
+
adherenceScore: 0,
|
|
524
|
+
bloatCount: filteredViolations.length,
|
|
525
|
+
bloatFiles: filteredViolations,
|
|
366
526
|
plannedFilesModified: 0,
|
|
367
527
|
totalPlannedFiles: planFiles.length,
|
|
368
|
-
message: `Scope violation: ${
|
|
528
|
+
message: `Scope violation: ${filteredViolations.length} file(s) modified outside the plan`,
|
|
369
529
|
scopeGuardPassed: false,
|
|
370
530
|
};
|
|
371
531
|
// CRITICAL: Print JSON first, then exit
|
|
@@ -378,12 +538,12 @@ async function verifyCommand(options) {
|
|
|
378
538
|
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
379
539
|
console.log(chalk.red('The following files were modified but are not in the plan:'));
|
|
380
540
|
console.log('');
|
|
381
|
-
|
|
541
|
+
filteredViolations.forEach(file => {
|
|
382
542
|
console.log(chalk.red(` • ${file}`));
|
|
383
543
|
});
|
|
384
544
|
console.log('');
|
|
385
545
|
console.log(chalk.yellow('To unblock these files, run:'));
|
|
386
|
-
|
|
546
|
+
filteredViolations.forEach(file => {
|
|
387
547
|
console.log(chalk.dim(` neurcode allow ${file}`));
|
|
388
548
|
});
|
|
389
549
|
console.log('');
|
|
@@ -424,8 +584,10 @@ async function verifyCommand(options) {
|
|
|
424
584
|
else {
|
|
425
585
|
console.log(JSON.stringify({
|
|
426
586
|
grade: 'N/A',
|
|
427
|
-
|
|
587
|
+
score: 0,
|
|
428
588
|
verdict: 'INFO',
|
|
589
|
+
violations: [],
|
|
590
|
+
adherenceScore: 0,
|
|
429
591
|
bloatCount: 0,
|
|
430
592
|
bloatFiles: [],
|
|
431
593
|
plannedFilesModified: 0,
|
|
@@ -437,6 +599,33 @@ async function verifyCommand(options) {
|
|
|
437
599
|
}
|
|
438
600
|
process.exit(0);
|
|
439
601
|
}
|
|
602
|
+
let policyViolations = [];
|
|
603
|
+
let policyDecision = 'allow';
|
|
604
|
+
if (config.apiKey) {
|
|
605
|
+
try {
|
|
606
|
+
const customPolicies = await client.getActiveCustomPolicies();
|
|
607
|
+
const defaultPolicy = (0, policy_engine_1.createDefaultPolicy)();
|
|
608
|
+
const customRules = [];
|
|
609
|
+
for (const p of customPolicies) {
|
|
610
|
+
const r = customPolicyToRule(p);
|
|
611
|
+
if (r)
|
|
612
|
+
customRules.push(r);
|
|
613
|
+
}
|
|
614
|
+
const allRules = [...defaultPolicy.rules, ...customRules];
|
|
615
|
+
const diffFilesForPolicy = diffFiles.filter((f) => !ignoreFilter(f.path));
|
|
616
|
+
const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, allRules);
|
|
617
|
+
policyViolations = policyResult.violations.filter((v) => !ignoreFilter(v.file));
|
|
618
|
+
policyDecision = policyViolations.length > 0 ? policyResult.decision : 'allow';
|
|
619
|
+
if (!options.json && customRules.length > 0) {
|
|
620
|
+
console.log(chalk.dim(` Evaluating ${customRules.length} custom policy rule(s) from dashboard`));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
if (!options.json) {
|
|
625
|
+
console.log(chalk.dim(' Could not load custom policies, continuing without them'));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
440
629
|
// Prepare diff stats and changed files for API
|
|
441
630
|
const diffStats = {
|
|
442
631
|
totalAdded: summary.totalAdded,
|
|
@@ -478,7 +667,13 @@ async function verifyCommand(options) {
|
|
|
478
667
|
}
|
|
479
668
|
// Call verifyPlan with intentConstraints
|
|
480
669
|
const verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraints);
|
|
481
|
-
//
|
|
670
|
+
// Apply custom policy verdict: block from dashboard overrides API verdict
|
|
671
|
+
const policyBlock = policyDecision === 'block' && policyViolations.length > 0;
|
|
672
|
+
const effectiveVerdict = policyBlock ? 'FAIL' : verifyResult.verdict;
|
|
673
|
+
const effectiveMessage = policyBlock
|
|
674
|
+
? `Custom policy violations: ${policyViolations.map(v => `${v.file}: ${v.message || v.rule}`).join('; ')}. ${verifyResult.message}`
|
|
675
|
+
: verifyResult.message;
|
|
676
|
+
// Calculate grade from effective verdict and score
|
|
482
677
|
// CRITICAL: 0/0 planned files = 'F' (Incomplete), not 'B'
|
|
483
678
|
// Bloat automatically drops grade by at least one letter
|
|
484
679
|
let grade;
|
|
@@ -486,7 +681,7 @@ async function verifyCommand(options) {
|
|
|
486
681
|
if (verifyResult.totalPlannedFiles === 0 && verifyResult.plannedFilesModified === 0) {
|
|
487
682
|
grade = 'F';
|
|
488
683
|
}
|
|
489
|
-
else if (
|
|
684
|
+
else if (effectiveVerdict === 'PASS') {
|
|
490
685
|
grade = 'A';
|
|
491
686
|
// Log ROI event for PASS verification (Grade A) - non-blocking
|
|
492
687
|
try {
|
|
@@ -503,7 +698,7 @@ async function verifyCommand(options) {
|
|
|
503
698
|
// Silently ignore - ROI logging should never block user workflows
|
|
504
699
|
}
|
|
505
700
|
}
|
|
506
|
-
else if (
|
|
701
|
+
else if (effectiveVerdict === 'WARN') {
|
|
507
702
|
// Base grade calculation
|
|
508
703
|
let baseGrade = verifyResult.adherenceScore >= 70 ? 'B' : verifyResult.adherenceScore >= 50 ? 'C' : 'D';
|
|
509
704
|
// Bloat drops grade by one letter (B -> C, C -> D, D -> F)
|
|
@@ -522,49 +717,82 @@ async function verifyCommand(options) {
|
|
|
522
717
|
}
|
|
523
718
|
// If JSON output requested, output JSON and exit
|
|
524
719
|
if (options.json) {
|
|
720
|
+
const filteredBloatFiles = (verifyResult.bloatFiles || []).filter((f) => !ignoreFilter(f));
|
|
721
|
+
const scopeViolations = filteredBloatFiles.map((file) => ({
|
|
722
|
+
file,
|
|
723
|
+
rule: 'scope_guard',
|
|
724
|
+
severity: 'block',
|
|
725
|
+
message: 'File modified outside the plan',
|
|
726
|
+
}));
|
|
727
|
+
const policyViolationItems = policyViolations.map((v) => ({
|
|
728
|
+
file: v.file,
|
|
729
|
+
rule: v.rule,
|
|
730
|
+
severity: v.severity,
|
|
731
|
+
message: v.message,
|
|
732
|
+
...(v.line != null ? { startLine: v.line } : {}),
|
|
733
|
+
}));
|
|
734
|
+
const violations = [...scopeViolations, ...policyViolationItems];
|
|
525
735
|
const jsonOutput = {
|
|
526
736
|
grade,
|
|
737
|
+
score: verifyResult.adherenceScore,
|
|
738
|
+
verdict: effectiveVerdict,
|
|
739
|
+
violations,
|
|
740
|
+
message: effectiveMessage,
|
|
527
741
|
adherenceScore: verifyResult.adherenceScore,
|
|
528
|
-
|
|
529
|
-
bloatCount:
|
|
530
|
-
bloatFiles:
|
|
742
|
+
scopeGuardPassed,
|
|
743
|
+
bloatCount: filteredBloatFiles.length,
|
|
744
|
+
bloatFiles: filteredBloatFiles,
|
|
531
745
|
plannedFilesModified: verifyResult.plannedFilesModified,
|
|
532
746
|
totalPlannedFiles: verifyResult.totalPlannedFiles,
|
|
533
|
-
|
|
534
|
-
scopeGuardPassed,
|
|
747
|
+
...(policyViolations.length > 0 && { policyDecision }),
|
|
535
748
|
};
|
|
536
749
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
537
750
|
// Report to Neurcode Cloud if --record flag is set (after JSON output)
|
|
538
751
|
if (options.record && config.apiKey) {
|
|
539
|
-
const violations =
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
752
|
+
const violations = [
|
|
753
|
+
...filteredBloatFiles.map((file) => ({
|
|
754
|
+
rule: 'scope_guard',
|
|
755
|
+
file: file,
|
|
756
|
+
severity: 'block',
|
|
757
|
+
message: 'File modified outside the plan',
|
|
758
|
+
})),
|
|
759
|
+
...policyViolations.map(v => ({
|
|
760
|
+
rule: v.rule,
|
|
761
|
+
file: v.file,
|
|
762
|
+
severity: v.severity,
|
|
763
|
+
message: v.message,
|
|
764
|
+
})),
|
|
765
|
+
];
|
|
545
766
|
// Report in background (don't await to avoid blocking JSON output)
|
|
546
767
|
reportVerification(grade, violations, verifyResult, config.apiKey, config.apiUrl, projectId || undefined, true // jsonMode = true
|
|
547
768
|
).catch(() => {
|
|
548
769
|
// Error already logged in reportVerification
|
|
549
770
|
});
|
|
550
771
|
}
|
|
551
|
-
// Exit based on verdict (same logic as below)
|
|
552
|
-
if (scopeGuardPassed) {
|
|
772
|
+
// Exit based on effective verdict (same logic as below)
|
|
773
|
+
if (scopeGuardPassed && !policyBlock) {
|
|
553
774
|
process.exit(0);
|
|
554
775
|
}
|
|
555
|
-
if (
|
|
776
|
+
if (effectiveVerdict === 'FAIL') {
|
|
556
777
|
process.exit(2);
|
|
557
778
|
}
|
|
558
|
-
else if (
|
|
779
|
+
else if (effectiveVerdict === 'WARN') {
|
|
559
780
|
process.exit(1);
|
|
560
781
|
}
|
|
561
782
|
else {
|
|
562
783
|
process.exit(0);
|
|
563
784
|
}
|
|
564
785
|
}
|
|
565
|
-
// Display results (only if not in json mode)
|
|
786
|
+
// Display results (only if not in json mode; exclude ignored paths from bloat)
|
|
566
787
|
if (!options.json) {
|
|
567
|
-
|
|
788
|
+
const displayBloatFiles = (verifyResult.bloatFiles || []).filter((f) => !ignoreFilter(f));
|
|
789
|
+
displayVerifyResults({
|
|
790
|
+
...verifyResult,
|
|
791
|
+
verdict: effectiveVerdict,
|
|
792
|
+
message: effectiveMessage,
|
|
793
|
+
bloatFiles: displayBloatFiles,
|
|
794
|
+
bloatCount: displayBloatFiles.length,
|
|
795
|
+
}, policyViolations);
|
|
568
796
|
}
|
|
569
797
|
// Report to Neurcode Cloud if --record flag is set
|
|
570
798
|
if (options.record) {
|
|
@@ -575,23 +803,30 @@ async function verifyCommand(options) {
|
|
|
575
803
|
}
|
|
576
804
|
}
|
|
577
805
|
else {
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
806
|
+
// Include scope bloat and custom policy violations in the report (excluding .neurcodeignore'd paths)
|
|
807
|
+
const filteredBloatForReport = (verifyResult.bloatFiles || []).filter((f) => !ignoreFilter(f));
|
|
808
|
+
const violations = [
|
|
809
|
+
...filteredBloatForReport.map((file) => ({
|
|
810
|
+
rule: 'scope_guard',
|
|
811
|
+
file: file,
|
|
812
|
+
severity: 'block',
|
|
813
|
+
message: 'File modified outside the plan',
|
|
814
|
+
})),
|
|
815
|
+
...policyViolations.map(v => ({
|
|
816
|
+
rule: v.rule,
|
|
817
|
+
file: v.file,
|
|
818
|
+
severity: v.severity,
|
|
819
|
+
message: v.message,
|
|
820
|
+
})),
|
|
821
|
+
];
|
|
587
822
|
await reportVerification(grade, violations, verifyResult, config.apiKey, config.apiUrl, projectId || undefined, false // jsonMode = false
|
|
588
823
|
);
|
|
589
824
|
}
|
|
590
825
|
}
|
|
591
826
|
// 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') {
|
|
827
|
+
// If Scope Guard passed (all files approved or allowed) and no policy block, always PASS
|
|
828
|
+
if (scopeGuardPassed && !policyBlock) {
|
|
829
|
+
if ((verifyResult.verdict === 'FAIL' || verifyResult.verdict === 'WARN') && policyViolations.length === 0) {
|
|
595
830
|
if (!options.json) {
|
|
596
831
|
console.log(chalk.yellow('\n⚠️ Plan deviation allowed'));
|
|
597
832
|
console.log(chalk.dim(' Some files were modified outside the plan, but they were explicitly allowed.'));
|
|
@@ -600,12 +835,12 @@ async function verifyCommand(options) {
|
|
|
600
835
|
}
|
|
601
836
|
process.exit(0);
|
|
602
837
|
}
|
|
603
|
-
// If scope guard didn't pass (or failed to check), use
|
|
604
|
-
// Exit with appropriate code based on AI verification
|
|
605
|
-
if (
|
|
838
|
+
// If scope guard didn't pass (or failed to check) or policy blocked, use effective verdict
|
|
839
|
+
// Exit with appropriate code based on AI verification and custom policies
|
|
840
|
+
if (effectiveVerdict === 'FAIL') {
|
|
606
841
|
process.exit(2);
|
|
607
842
|
}
|
|
608
|
-
else if (
|
|
843
|
+
else if (effectiveVerdict === 'WARN') {
|
|
609
844
|
process.exit(1);
|
|
610
845
|
}
|
|
611
846
|
else {
|
|
@@ -617,8 +852,10 @@ async function verifyCommand(options) {
|
|
|
617
852
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
618
853
|
console.log(JSON.stringify({
|
|
619
854
|
grade: 'F',
|
|
620
|
-
|
|
855
|
+
score: 0,
|
|
621
856
|
verdict: 'FAIL',
|
|
857
|
+
violations: [],
|
|
858
|
+
adherenceScore: 0,
|
|
622
859
|
bloatCount: 0,
|
|
623
860
|
bloatFiles: [],
|
|
624
861
|
plannedFilesModified: 0,
|
|
@@ -650,8 +887,10 @@ async function verifyCommand(options) {
|
|
|
650
887
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
651
888
|
console.log(JSON.stringify({
|
|
652
889
|
grade: 'F',
|
|
653
|
-
|
|
890
|
+
score: 0,
|
|
654
891
|
verdict: 'FAIL',
|
|
892
|
+
violations: [],
|
|
893
|
+
adherenceScore: 0,
|
|
655
894
|
bloatCount: 0,
|
|
656
895
|
bloatFiles: [],
|
|
657
896
|
plannedFilesModified: 0,
|
|
@@ -812,7 +1051,7 @@ async function reportVerification(grade, violations, verifyResult, apiKey, apiUr
|
|
|
812
1051
|
/**
|
|
813
1052
|
* Display verification results in a formatted report card
|
|
814
1053
|
*/
|
|
815
|
-
function displayVerifyResults(result) {
|
|
1054
|
+
function displayVerifyResults(result, policyViolations) {
|
|
816
1055
|
// Calculate grade/score
|
|
817
1056
|
// CRITICAL: 0/0 planned files = 'F' (Incomplete)
|
|
818
1057
|
// Bloat automatically drops grade by at least one letter
|
|
@@ -898,6 +1137,19 @@ function displayVerifyResults(result) {
|
|
|
898
1137
|
console.log('');
|
|
899
1138
|
console.log(chalk.green('✅ No bloat detected - all changes match the plan'));
|
|
900
1139
|
}
|
|
1140
|
+
// Display custom policy violations from dashboard
|
|
1141
|
+
if (policyViolations && policyViolations.length > 0) {
|
|
1142
|
+
console.log('');
|
|
1143
|
+
const blockCount = policyViolations.filter(v => v.severity === 'block').length;
|
|
1144
|
+
const label = blockCount > 0
|
|
1145
|
+
? chalk.bold.red(`🚫 Custom Policy Violations: ${policyViolations.length} (${blockCount} blocking)`)
|
|
1146
|
+
: chalk.bold.yellow(`⚠️ Custom Policy Warnings: ${policyViolations.length}`);
|
|
1147
|
+
console.log(label);
|
|
1148
|
+
policyViolations.forEach(v => {
|
|
1149
|
+
const lineColor = v.severity === 'block' ? chalk.red : chalk.yellow;
|
|
1150
|
+
console.log(lineColor(` • ${v.file}: ${v.message || v.rule}`));
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
901
1153
|
console.log('');
|
|
902
1154
|
console.log('━'.repeat(50));
|
|
903
1155
|
console.log(chalk.dim(result.message));
|