@neurcode-ai/cli 0.9.26 → 0.9.28
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/commands/allow.d.ts.map +1 -1
- package/dist/commands/allow.js +5 -19
- package/dist/commands/allow.js.map +1 -1
- package/dist/commands/apply.d.ts +1 -0
- package/dist/commands/apply.d.ts.map +1 -1
- package/dist/commands/apply.js +105 -46
- package/dist/commands/apply.js.map +1 -1
- package/dist/commands/ask.d.ts.map +1 -1
- package/dist/commands/ask.js +1849 -1783
- package/dist/commands/ask.js.map +1 -1
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +83 -24
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/plan.d.ts +4 -0
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +344 -48
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/policy.d.ts.map +1 -1
- package/dist/commands/policy.js +629 -0
- package/dist/commands/policy.js.map +1 -1
- package/dist/commands/prompt.d.ts +7 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +106 -25
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/ship.d.ts +32 -0
- package/dist/commands/ship.d.ts.map +1 -1
- package/dist/commands/ship.js +1404 -75
- package/dist/commands/ship.js.map +1 -1
- package/dist/commands/verify.d.ts +6 -0
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +527 -102
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +89 -3
- package/dist/index.js.map +1 -1
- package/dist/utils/custom-policy-rules.d.ts +21 -0
- package/dist/utils/custom-policy-rules.d.ts.map +1 -0
- package/dist/utils/custom-policy-rules.js +71 -0
- package/dist/utils/custom-policy-rules.js.map +1 -0
- package/dist/utils/plan-cache.d.ts.map +1 -1
- package/dist/utils/plan-cache.js +4 -0
- package/dist/utils/plan-cache.js.map +1 -1
- package/dist/utils/policy-audit.d.ts +29 -0
- package/dist/utils/policy-audit.d.ts.map +1 -0
- package/dist/utils/policy-audit.js +208 -0
- package/dist/utils/policy-audit.js.map +1 -0
- package/dist/utils/policy-exceptions.d.ts +96 -0
- package/dist/utils/policy-exceptions.d.ts.map +1 -0
- package/dist/utils/policy-exceptions.js +389 -0
- package/dist/utils/policy-exceptions.js.map +1 -0
- package/dist/utils/policy-governance.d.ts +24 -0
- package/dist/utils/policy-governance.d.ts.map +1 -0
- package/dist/utils/policy-governance.js +124 -0
- package/dist/utils/policy-governance.js.map +1 -0
- package/dist/utils/policy-packs.d.ts +72 -1
- package/dist/utils/policy-packs.d.ts.map +1 -1
- package/dist/utils/policy-packs.js +285 -0
- package/dist/utils/policy-packs.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/verify.js
CHANGED
|
@@ -54,6 +54,10 @@ const ignore_1 = require("../utils/ignore");
|
|
|
54
54
|
const project_root_1 = require("../utils/project-root");
|
|
55
55
|
const brain_context_1 = require("../utils/brain-context");
|
|
56
56
|
const policy_packs_1 = require("../utils/policy-packs");
|
|
57
|
+
const custom_policy_rules_1 = require("../utils/custom-policy-rules");
|
|
58
|
+
const policy_exceptions_1 = require("../utils/policy-exceptions");
|
|
59
|
+
const policy_governance_1 = require("../utils/policy-governance");
|
|
60
|
+
const policy_audit_1 = require("../utils/policy-audit");
|
|
57
61
|
// Import chalk with fallback
|
|
58
62
|
let chalk;
|
|
59
63
|
try {
|
|
@@ -78,21 +82,21 @@ catch {
|
|
|
78
82
|
* Check if a file path should be excluded from verification analysis
|
|
79
83
|
* Excludes internal/system files that should not count towards plan adherence
|
|
80
84
|
*/
|
|
81
|
-
const
|
|
82
|
-
const IGNORED_DIRECTORIES = ['.
|
|
85
|
+
const IGNORED_METADATA_FILE_PATTERN = /(^|\/)neurcode\.config\.json$/i;
|
|
86
|
+
const IGNORED_DIRECTORIES = ['.git/', 'node_modules/'];
|
|
83
87
|
function isExcludedFile(filePath) {
|
|
84
88
|
// Normalize path separators (handle both / and \)
|
|
85
89
|
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
86
90
|
// Ignore specific metadata files (these should never be scope/policy violations)
|
|
87
|
-
if (
|
|
91
|
+
if (IGNORED_METADATA_FILE_PATTERN.test(normalizedPath)) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
// Ignore .neurcode internals at any nesting level (monorepo-safe)
|
|
95
|
+
if (/(^|\/)\.neurcode\//.test(normalizedPath)) {
|
|
88
96
|
return true;
|
|
89
97
|
}
|
|
90
98
|
// Check if path starts with any excluded prefix
|
|
91
|
-
const excludedPrefixes = [
|
|
92
|
-
...IGNORED_DIRECTORIES,
|
|
93
|
-
'.git/',
|
|
94
|
-
'node_modules/',
|
|
95
|
-
];
|
|
99
|
+
const excludedPrefixes = [...IGNORED_DIRECTORIES];
|
|
96
100
|
// Check prefixes
|
|
97
101
|
for (const prefix of excludedPrefixes) {
|
|
98
102
|
if (normalizedPath.startsWith(prefix)) {
|
|
@@ -203,76 +207,17 @@ function getRuntimeIgnoreSetFromEnv() {
|
|
|
203
207
|
.map((item) => toUnixPath(item.trim()))
|
|
204
208
|
.filter(Boolean));
|
|
205
209
|
}
|
|
206
|
-
/**
|
|
207
|
-
* Map a dashboard custom policy (natural language) to a policy-engine Rule.
|
|
208
|
-
* Supports "No console.log", "No debugger", and generic line patterns.
|
|
209
|
-
*/
|
|
210
|
-
function customPolicyToRule(p) {
|
|
211
|
-
const sev = p.severity === 'high' ? 'block' : 'warn';
|
|
212
|
-
const text = (p.rule_text || '').trim().toLowerCase();
|
|
213
|
-
if (!text)
|
|
214
|
-
return null;
|
|
215
|
-
// Known patterns -> suspicious-keywords (checks added lines for these strings)
|
|
216
|
-
if (/console\.log|no\s+console\.log|do not use console\.log|ban console\.log/.test(text)) {
|
|
217
|
-
return {
|
|
218
|
-
id: `custom-${p.id}`,
|
|
219
|
-
name: 'No console.log',
|
|
220
|
-
description: p.rule_text,
|
|
221
|
-
enabled: true,
|
|
222
|
-
severity: sev,
|
|
223
|
-
type: 'suspicious-keywords',
|
|
224
|
-
keywords: ['console.log'],
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
if (/debugger|no\s+debugger|do not use debugger/.test(text)) {
|
|
228
|
-
return {
|
|
229
|
-
id: `custom-${p.id}`,
|
|
230
|
-
name: 'No debugger',
|
|
231
|
-
description: p.rule_text,
|
|
232
|
-
enabled: true,
|
|
233
|
-
severity: sev,
|
|
234
|
-
type: 'suspicious-keywords',
|
|
235
|
-
keywords: ['debugger'],
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
if (/eval\s*\(|no\s+eval|do not use eval/.test(text)) {
|
|
239
|
-
return {
|
|
240
|
-
id: `custom-${p.id}`,
|
|
241
|
-
name: 'No eval',
|
|
242
|
-
description: p.rule_text,
|
|
243
|
-
enabled: true,
|
|
244
|
-
severity: sev,
|
|
245
|
-
type: 'suspicious-keywords',
|
|
246
|
-
keywords: ['eval('],
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
// Fallback: line-pattern on added lines using a safe regex from the rule text
|
|
250
|
-
// Use first quoted phrase or first alphanumeric phrase as pattern
|
|
251
|
-
const quoted = /['"`]([^'"`]+)['"`]/.exec(p.rule_text);
|
|
252
|
-
const phrase = quoted?.[1] ?? p.rule_text.replace(/^(no|don't|do not use|ban|avoid)\s+/i, '').trim().slice(0, 80);
|
|
253
|
-
if (!phrase)
|
|
254
|
-
return null;
|
|
255
|
-
const escaped = phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
256
|
-
return {
|
|
257
|
-
id: `custom-${p.id}`,
|
|
258
|
-
name: `Custom: ${p.rule_text.slice(0, 50)}`,
|
|
259
|
-
description: p.rule_text,
|
|
260
|
-
enabled: true,
|
|
261
|
-
severity: sev,
|
|
262
|
-
type: 'line-pattern',
|
|
263
|
-
pattern: escaped,
|
|
264
|
-
matchType: 'added',
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
210
|
async function buildEffectivePolicyRules(client, projectRoot, useDashboardPolicies) {
|
|
268
211
|
const defaultPolicy = (0, policy_engine_1.createDefaultPolicy)();
|
|
269
212
|
const customRules = [];
|
|
213
|
+
const customPolicies = [];
|
|
270
214
|
const installedPack = (0, policy_packs_1.getInstalledPolicyPackRules)(projectRoot);
|
|
271
215
|
const policyPackRules = installedPack?.rules ? [...installedPack.rules] : [];
|
|
272
216
|
if (useDashboardPolicies) {
|
|
273
|
-
const
|
|
274
|
-
for (const p of
|
|
275
|
-
|
|
217
|
+
const loadedCustomPolicies = await client.getActiveCustomPolicies();
|
|
218
|
+
for (const p of loadedCustomPolicies) {
|
|
219
|
+
customPolicies.push(p);
|
|
220
|
+
const r = (0, custom_policy_rules_1.customPolicyToRule)(p);
|
|
276
221
|
if (r) {
|
|
277
222
|
customRules.push(r);
|
|
278
223
|
}
|
|
@@ -281,8 +226,68 @@ async function buildEffectivePolicyRules(client, projectRoot, useDashboardPolici
|
|
|
281
226
|
return {
|
|
282
227
|
allRules: [...defaultPolicy.rules, ...policyPackRules, ...customRules],
|
|
283
228
|
customRules,
|
|
229
|
+
customPolicies,
|
|
284
230
|
policyPackRules,
|
|
285
231
|
policyPack: installedPack,
|
|
232
|
+
includeDashboardPolicies: useDashboardPolicies,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const POLICY_AUDIT_FILE = 'neurcode.policy.audit.log.jsonl';
|
|
236
|
+
function isEnabledFlag(value) {
|
|
237
|
+
if (!value)
|
|
238
|
+
return false;
|
|
239
|
+
const normalized = value.trim().toLowerCase();
|
|
240
|
+
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
|
241
|
+
}
|
|
242
|
+
function policyLockMismatchMessage(mismatches) {
|
|
243
|
+
if (mismatches.length === 0) {
|
|
244
|
+
return 'Policy lock baseline check failed';
|
|
245
|
+
}
|
|
246
|
+
return `Policy lock mismatch: ${mismatches.map((item) => `[${item.code}] ${item.message}`).join('; ')}`;
|
|
247
|
+
}
|
|
248
|
+
function toPolicyLockViolations(mismatches) {
|
|
249
|
+
return mismatches.map((item) => ({
|
|
250
|
+
file: 'neurcode.policy.lock.json',
|
|
251
|
+
rule: `policy_lock:${item.code.toLowerCase()}`,
|
|
252
|
+
severity: 'block',
|
|
253
|
+
message: item.message,
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
function resolvePolicyDecisionFromViolations(violations) {
|
|
257
|
+
let hasWarn = false;
|
|
258
|
+
for (const violation of violations) {
|
|
259
|
+
const severity = String(violation.severity || '').toLowerCase();
|
|
260
|
+
if (severity === 'block') {
|
|
261
|
+
return 'block';
|
|
262
|
+
}
|
|
263
|
+
if (severity === 'warn') {
|
|
264
|
+
hasWarn = true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return hasWarn ? 'warn' : 'allow';
|
|
268
|
+
}
|
|
269
|
+
function explainExceptionEligibilityReason(reason) {
|
|
270
|
+
switch (reason) {
|
|
271
|
+
case 'approval_required':
|
|
272
|
+
return 'exception exists but approvals are required';
|
|
273
|
+
case 'insufficient_approvals':
|
|
274
|
+
return 'exception exists but approval threshold is not met';
|
|
275
|
+
case 'self_approval_only':
|
|
276
|
+
return 'exception only has requester self-approval';
|
|
277
|
+
case 'approver_not_allowed':
|
|
278
|
+
return 'exception approvals are from non-allowlisted approvers';
|
|
279
|
+
default:
|
|
280
|
+
return 'exception is inactive or expired';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function resolveAuditIntegrityStatus(requireIntegrity, auditIntegrity) {
|
|
284
|
+
const issues = [...auditIntegrity.issues];
|
|
285
|
+
if (requireIntegrity && auditIntegrity.count === 0) {
|
|
286
|
+
issues.push('audit chain has no events; commit neurcode.policy.audit.log.jsonl');
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
valid: issues.length === 0,
|
|
290
|
+
issues,
|
|
286
291
|
};
|
|
287
292
|
}
|
|
288
293
|
/**
|
|
@@ -295,20 +300,108 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
295
300
|
}
|
|
296
301
|
let policyViolations = [];
|
|
297
302
|
let policyDecision = 'allow';
|
|
303
|
+
const requirePolicyLock = options.requirePolicyLock === true || isEnabledFlag(process.env.NEURCODE_VERIFY_REQUIRE_POLICY_LOCK);
|
|
304
|
+
const skipPolicyLock = options.skipPolicyLock === true || isEnabledFlag(process.env.NEURCODE_VERIFY_SKIP_POLICY_LOCK);
|
|
305
|
+
const lockRead = (0, policy_packs_1.readPolicyLockFile)(projectRoot);
|
|
306
|
+
const includeDashboardPolicies = lockRead.lock
|
|
307
|
+
? lockRead.lock.customPolicies.mode === 'dashboard'
|
|
308
|
+
: Boolean(config.apiKey);
|
|
309
|
+
let effectiveRulesLoadError = null;
|
|
298
310
|
let effectiveRules = {
|
|
299
311
|
allRules: (0, policy_engine_1.createDefaultPolicy)().rules,
|
|
300
312
|
customRules: [],
|
|
313
|
+
customPolicies: [],
|
|
301
314
|
policyPackRules: [],
|
|
302
315
|
policyPack: null,
|
|
316
|
+
includeDashboardPolicies,
|
|
303
317
|
};
|
|
304
318
|
try {
|
|
305
|
-
effectiveRules = await buildEffectivePolicyRules(client, projectRoot,
|
|
319
|
+
effectiveRules = await buildEffectivePolicyRules(client, projectRoot, includeDashboardPolicies);
|
|
306
320
|
}
|
|
307
321
|
catch (error) {
|
|
322
|
+
effectiveRulesLoadError = error instanceof Error ? error.message : 'Unknown error';
|
|
323
|
+
const installedPack = (0, policy_packs_1.getInstalledPolicyPackRules)(projectRoot);
|
|
324
|
+
const fallbackPolicyPackRules = installedPack?.rules ? [...installedPack.rules] : [];
|
|
325
|
+
effectiveRules = {
|
|
326
|
+
allRules: [...(0, policy_engine_1.createDefaultPolicy)().rules, ...fallbackPolicyPackRules],
|
|
327
|
+
customRules: [],
|
|
328
|
+
customPolicies: [],
|
|
329
|
+
policyPackRules: fallbackPolicyPackRules,
|
|
330
|
+
policyPack: installedPack,
|
|
331
|
+
includeDashboardPolicies,
|
|
332
|
+
};
|
|
308
333
|
if (!options.json) {
|
|
309
334
|
console.log(chalk.dim(' Could not load dashboard custom policies, using local/default policy rules only'));
|
|
310
335
|
}
|
|
311
336
|
}
|
|
337
|
+
let policyLockEvaluation = {
|
|
338
|
+
enforced: false,
|
|
339
|
+
matched: true,
|
|
340
|
+
lockPresent: lockRead.lock !== null,
|
|
341
|
+
lockPath: lockRead.path,
|
|
342
|
+
mismatches: [],
|
|
343
|
+
};
|
|
344
|
+
if (!skipPolicyLock) {
|
|
345
|
+
const currentSnapshot = (0, policy_packs_1.buildPolicyStateSnapshot)({
|
|
346
|
+
policyPack: effectiveRules.policyPack,
|
|
347
|
+
policyPackRules: effectiveRules.policyPackRules,
|
|
348
|
+
customPolicies: effectiveRules.customPolicies,
|
|
349
|
+
customRules: effectiveRules.customRules,
|
|
350
|
+
includeDashboardPolicies: effectiveRules.includeDashboardPolicies,
|
|
351
|
+
});
|
|
352
|
+
const lockValidation = (0, policy_packs_1.evaluatePolicyLock)(projectRoot, currentSnapshot, {
|
|
353
|
+
requireLock: requirePolicyLock,
|
|
354
|
+
});
|
|
355
|
+
policyLockEvaluation = {
|
|
356
|
+
enforced: lockValidation.enforced,
|
|
357
|
+
matched: lockValidation.matched,
|
|
358
|
+
lockPresent: lockValidation.lockPresent,
|
|
359
|
+
lockPath: lockValidation.lockPath,
|
|
360
|
+
mismatches: [...lockValidation.mismatches],
|
|
361
|
+
};
|
|
362
|
+
if (effectiveRulesLoadError && includeDashboardPolicies) {
|
|
363
|
+
policyLockEvaluation.mismatches.unshift({
|
|
364
|
+
code: 'POLICY_LOCK_CUSTOM_POLICIES_MISMATCH',
|
|
365
|
+
message: `Failed to load dashboard custom policies: ${effectiveRulesLoadError}`,
|
|
366
|
+
});
|
|
367
|
+
policyLockEvaluation.matched = false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (policyLockEvaluation.enforced && !policyLockEvaluation.matched) {
|
|
371
|
+
const message = policyLockMismatchMessage(policyLockEvaluation.mismatches);
|
|
372
|
+
if (options.json) {
|
|
373
|
+
console.log(JSON.stringify({
|
|
374
|
+
grade: 'F',
|
|
375
|
+
score: 0,
|
|
376
|
+
verdict: 'FAIL',
|
|
377
|
+
violations: toPolicyLockViolations(policyLockEvaluation.mismatches),
|
|
378
|
+
message,
|
|
379
|
+
scopeGuardPassed: true,
|
|
380
|
+
bloatCount: 0,
|
|
381
|
+
bloatFiles: [],
|
|
382
|
+
plannedFilesModified: 0,
|
|
383
|
+
totalPlannedFiles: 0,
|
|
384
|
+
adherenceScore: 0,
|
|
385
|
+
mode: 'policy_only',
|
|
386
|
+
policyOnly: true,
|
|
387
|
+
policyLock: {
|
|
388
|
+
enforced: true,
|
|
389
|
+
matched: false,
|
|
390
|
+
path: policyLockEvaluation.lockPath,
|
|
391
|
+
mismatches: policyLockEvaluation.mismatches,
|
|
392
|
+
},
|
|
393
|
+
}, null, 2));
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
console.log(chalk.red('❌ Policy lock baseline mismatch.'));
|
|
397
|
+
console.log(chalk.dim(` Lock file: ${policyLockEvaluation.lockPath}`));
|
|
398
|
+
policyLockEvaluation.mismatches.forEach((item) => {
|
|
399
|
+
console.log(chalk.red(` • [${item.code}] ${item.message}`));
|
|
400
|
+
});
|
|
401
|
+
console.log(chalk.dim('\n If drift is intentional, regenerate baseline with `neurcode policy lock`.\n'));
|
|
402
|
+
}
|
|
403
|
+
return 2;
|
|
404
|
+
}
|
|
312
405
|
if (!options.json && effectiveRules.customRules.length > 0) {
|
|
313
406
|
console.log(chalk.dim(` Evaluating ${effectiveRules.customRules.length} custom policy rule(s) from dashboard`));
|
|
314
407
|
}
|
|
@@ -322,7 +415,39 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
322
415
|
const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, effectiveRules.allRules);
|
|
323
416
|
policyViolations = (policyResult.violations || []);
|
|
324
417
|
policyViolations = policyViolations.filter((v) => !ignoreFilter(v.file));
|
|
325
|
-
|
|
418
|
+
const governance = (0, policy_governance_1.readPolicyGovernanceConfig)(projectRoot);
|
|
419
|
+
const auditIntegrity = (0, policy_audit_1.verifyPolicyAuditIntegrity)(projectRoot);
|
|
420
|
+
const auditIntegrityStatus = resolveAuditIntegrityStatus(governance.audit.requireIntegrity, auditIntegrity);
|
|
421
|
+
const configuredPolicyExceptions = (0, policy_exceptions_1.readPolicyExceptions)(projectRoot);
|
|
422
|
+
const exceptionDecision = (0, policy_exceptions_1.applyPolicyExceptions)(policyViolations, configuredPolicyExceptions, {
|
|
423
|
+
requireApproval: governance.exceptionApprovals.required,
|
|
424
|
+
minApprovals: governance.exceptionApprovals.minApprovals,
|
|
425
|
+
disallowSelfApproval: governance.exceptionApprovals.disallowSelfApproval,
|
|
426
|
+
allowedApprovers: governance.exceptionApprovals.allowedApprovers,
|
|
427
|
+
});
|
|
428
|
+
const suppressedViolations = exceptionDecision.suppressedViolations.filter((item) => !ignoreFilter(item.file));
|
|
429
|
+
const blockedViolations = exceptionDecision.blockedViolations
|
|
430
|
+
.filter((item) => !ignoreFilter(item.file))
|
|
431
|
+
.map((item) => ({
|
|
432
|
+
file: item.file,
|
|
433
|
+
rule: item.rule,
|
|
434
|
+
severity: 'block',
|
|
435
|
+
message: `Exception ${item.exceptionId} cannot be applied: ${explainExceptionEligibilityReason(item.eligibilityReason)}`,
|
|
436
|
+
...(item.line != null ? { line: item.line } : {}),
|
|
437
|
+
}));
|
|
438
|
+
policyViolations = [
|
|
439
|
+
...exceptionDecision.remainingViolations.filter((item) => !ignoreFilter(item.file)),
|
|
440
|
+
...blockedViolations,
|
|
441
|
+
];
|
|
442
|
+
if (governance.audit.requireIntegrity && !auditIntegrityStatus.valid) {
|
|
443
|
+
policyViolations.push({
|
|
444
|
+
file: POLICY_AUDIT_FILE,
|
|
445
|
+
rule: 'policy_audit_integrity',
|
|
446
|
+
severity: 'block',
|
|
447
|
+
message: `Policy audit chain is invalid: ${auditIntegrityStatus.issues.join('; ') || 'unknown issue'}`,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
policyDecision = resolvePolicyDecisionFromViolations(policyViolations);
|
|
326
451
|
const effectiveVerdict = policyDecision === 'block' ? 'FAIL' : policyDecision === 'warn' ? 'WARN' : 'PASS';
|
|
327
452
|
const grade = effectiveVerdict === 'PASS' ? 'A' : effectiveVerdict === 'WARN' ? 'C' : 'F';
|
|
328
453
|
const score = effectiveVerdict === 'PASS' ? 100 : effectiveVerdict === 'WARN' ? 50 : 0;
|
|
@@ -338,6 +463,42 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
338
463
|
: policyViolations.length > 0
|
|
339
464
|
? `Policy violations: ${policyViolations.map((v) => `${v.file}: ${v.message || v.rule}`).join('; ')}`
|
|
340
465
|
: 'Policy check completed';
|
|
466
|
+
const policyExceptionsSummary = {
|
|
467
|
+
configured: configuredPolicyExceptions.length,
|
|
468
|
+
active: exceptionDecision.activeExceptions.length,
|
|
469
|
+
usable: exceptionDecision.usableExceptions.length,
|
|
470
|
+
matched: exceptionDecision.matchedExceptionIds.length,
|
|
471
|
+
suppressed: suppressedViolations.length,
|
|
472
|
+
blocked: blockedViolations.length,
|
|
473
|
+
matchedExceptionIds: exceptionDecision.matchedExceptionIds,
|
|
474
|
+
suppressedViolations: suppressedViolations.map((item) => ({
|
|
475
|
+
file: item.file,
|
|
476
|
+
rule: item.rule,
|
|
477
|
+
severity: item.severity,
|
|
478
|
+
message: item.message,
|
|
479
|
+
exceptionId: item.exceptionId,
|
|
480
|
+
reason: item.reason,
|
|
481
|
+
expiresAt: item.expiresAt,
|
|
482
|
+
...(item.line != null ? { startLine: item.line } : {}),
|
|
483
|
+
})),
|
|
484
|
+
blockedViolations: blockedViolations.map((item) => ({
|
|
485
|
+
file: item.file,
|
|
486
|
+
rule: item.rule,
|
|
487
|
+
severity: item.severity,
|
|
488
|
+
message: item.message,
|
|
489
|
+
...(item.line != null ? { startLine: item.line } : {}),
|
|
490
|
+
})),
|
|
491
|
+
};
|
|
492
|
+
const policyGovernanceSummary = {
|
|
493
|
+
exceptionApprovals: governance.exceptionApprovals,
|
|
494
|
+
audit: {
|
|
495
|
+
requireIntegrity: governance.audit.requireIntegrity,
|
|
496
|
+
valid: auditIntegrityStatus.valid,
|
|
497
|
+
issues: auditIntegrityStatus.issues,
|
|
498
|
+
lastHash: auditIntegrity.lastHash,
|
|
499
|
+
eventCount: auditIntegrity.count,
|
|
500
|
+
},
|
|
501
|
+
};
|
|
341
502
|
if (options.json) {
|
|
342
503
|
console.log(JSON.stringify({
|
|
343
504
|
grade,
|
|
@@ -351,7 +512,16 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
351
512
|
plannedFilesModified: 0,
|
|
352
513
|
totalPlannedFiles: 0,
|
|
353
514
|
adherenceScore: score,
|
|
515
|
+
mode: 'policy_only',
|
|
354
516
|
policyOnly: true,
|
|
517
|
+
policyLock: {
|
|
518
|
+
enforced: policyLockEvaluation.enforced,
|
|
519
|
+
matched: policyLockEvaluation.matched,
|
|
520
|
+
path: policyLockEvaluation.lockPath,
|
|
521
|
+
mismatches: policyLockEvaluation.mismatches,
|
|
522
|
+
},
|
|
523
|
+
policyExceptions: policyExceptionsSummary,
|
|
524
|
+
policyGovernance: policyGovernanceSummary,
|
|
355
525
|
...(effectiveRules.policyPack
|
|
356
526
|
? {
|
|
357
527
|
policyPack: {
|
|
@@ -374,6 +544,15 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
374
544
|
console.log(chalk.red(` • ${v.file}: ${v.message || v.rule}`));
|
|
375
545
|
});
|
|
376
546
|
}
|
|
547
|
+
if (policyExceptionsSummary.suppressed > 0) {
|
|
548
|
+
console.log(chalk.yellow(` Policy exceptions applied: ${policyExceptionsSummary.suppressed}`));
|
|
549
|
+
}
|
|
550
|
+
if (policyExceptionsSummary.blocked > 0) {
|
|
551
|
+
console.log(chalk.red(` Policy exceptions blocked by approval governance: ${policyExceptionsSummary.blocked}`));
|
|
552
|
+
}
|
|
553
|
+
if (governance.audit.requireIntegrity && !auditIntegrityStatus.valid) {
|
|
554
|
+
console.log(chalk.red(' Policy audit integrity check failed'));
|
|
555
|
+
}
|
|
377
556
|
console.log(chalk.dim(`\n${message}`));
|
|
378
557
|
}
|
|
379
558
|
return effectiveVerdict === 'FAIL' ? 2 : effectiveVerdict === 'WARN' ? 1 : 0;
|
|
@@ -561,6 +740,7 @@ async function verifyCommand(options) {
|
|
|
561
740
|
recordVerifyEvent(verdict, `policy_only=true;exit=${exitCode}`, changedFiles);
|
|
562
741
|
process.exit(exitCode);
|
|
563
742
|
}
|
|
743
|
+
const requirePlan = options.requirePlan === true || process.env.NEURCODE_VERIFY_REQUIRE_PLAN === '1';
|
|
564
744
|
// Get planId: Priority 1: options flag, Priority 2: state file (.neurcode/config.json), Priority 3: legacy config
|
|
565
745
|
let planId = options.planId;
|
|
566
746
|
if (!planId) {
|
|
@@ -593,8 +773,35 @@ async function verifyCommand(options) {
|
|
|
593
773
|
}
|
|
594
774
|
}
|
|
595
775
|
}
|
|
596
|
-
// If no planId found, fall back to
|
|
776
|
+
// If no planId found, either enforce strict requirement or fall back to policy-only mode.
|
|
597
777
|
if (!planId) {
|
|
778
|
+
if (requirePlan) {
|
|
779
|
+
const changedFiles = diffFiles.map((f) => f.path);
|
|
780
|
+
recordVerifyEvent('FAIL', 'missing_plan_id;require_plan=true', changedFiles);
|
|
781
|
+
if (options.json) {
|
|
782
|
+
console.log(JSON.stringify({
|
|
783
|
+
grade: 'F',
|
|
784
|
+
score: 0,
|
|
785
|
+
verdict: 'FAIL',
|
|
786
|
+
violations: [],
|
|
787
|
+
adherenceScore: 0,
|
|
788
|
+
bloatCount: 0,
|
|
789
|
+
bloatFiles: [],
|
|
790
|
+
plannedFilesModified: 0,
|
|
791
|
+
totalPlannedFiles: 0,
|
|
792
|
+
message: 'Plan ID is required in strict mode. Run "neurcode plan" first or pass --plan-id.',
|
|
793
|
+
scopeGuardPassed: false,
|
|
794
|
+
mode: 'plan_required',
|
|
795
|
+
policyOnly: false,
|
|
796
|
+
}, null, 2));
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
console.log(chalk.red('❌ Plan ID is required in strict mode.'));
|
|
800
|
+
console.log(chalk.dim(' Run "neurcode plan" first or pass --plan-id <id>.'));
|
|
801
|
+
console.log(chalk.dim(' Use --policy-only only when intentionally running general governance checks.'));
|
|
802
|
+
}
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
598
805
|
if (!options.json) {
|
|
599
806
|
console.log(chalk.yellow('⚠️ No Plan ID found. Falling back to General Governance (Policy Only).'));
|
|
600
807
|
}
|
|
@@ -692,6 +899,8 @@ async function verifyCommand(options) {
|
|
|
692
899
|
totalPlannedFiles: planFiles.length,
|
|
693
900
|
message: `Scope violation: ${filteredViolations.length} file(s) modified outside the plan`,
|
|
694
901
|
scopeGuardPassed: false,
|
|
902
|
+
mode: 'plan_enforced',
|
|
903
|
+
policyOnly: false,
|
|
695
904
|
};
|
|
696
905
|
// CRITICAL: Print JSON first, then exit
|
|
697
906
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
@@ -731,7 +940,113 @@ async function verifyCommand(options) {
|
|
|
731
940
|
console.log('');
|
|
732
941
|
}
|
|
733
942
|
}
|
|
943
|
+
const requirePolicyLock = options.requirePolicyLock === true || isEnabledFlag(process.env.NEURCODE_VERIFY_REQUIRE_POLICY_LOCK);
|
|
944
|
+
const skipPolicyLock = options.skipPolicyLock === true || isEnabledFlag(process.env.NEURCODE_VERIFY_SKIP_POLICY_LOCK);
|
|
945
|
+
const lockRead = (0, policy_packs_1.readPolicyLockFile)(projectRoot);
|
|
946
|
+
const useDashboardPolicies = lockRead.lock
|
|
947
|
+
? lockRead.lock.customPolicies.mode === 'dashboard'
|
|
948
|
+
: Boolean(config.apiKey);
|
|
949
|
+
let effectiveRulesLoadError = null;
|
|
950
|
+
let effectiveRules = {
|
|
951
|
+
allRules: (0, policy_engine_1.createDefaultPolicy)().rules,
|
|
952
|
+
customRules: [],
|
|
953
|
+
customPolicies: [],
|
|
954
|
+
policyPackRules: [],
|
|
955
|
+
policyPack: null,
|
|
956
|
+
includeDashboardPolicies: useDashboardPolicies,
|
|
957
|
+
};
|
|
958
|
+
try {
|
|
959
|
+
effectiveRules = await buildEffectivePolicyRules(client, projectRoot, useDashboardPolicies);
|
|
960
|
+
}
|
|
961
|
+
catch (error) {
|
|
962
|
+
effectiveRulesLoadError = error instanceof Error ? error.message : 'Unknown error';
|
|
963
|
+
const installedPack = (0, policy_packs_1.getInstalledPolicyPackRules)(projectRoot);
|
|
964
|
+
const fallbackPolicyPackRules = installedPack?.rules ? [...installedPack.rules] : [];
|
|
965
|
+
effectiveRules = {
|
|
966
|
+
allRules: [...(0, policy_engine_1.createDefaultPolicy)().rules, ...fallbackPolicyPackRules],
|
|
967
|
+
customRules: [],
|
|
968
|
+
customPolicies: [],
|
|
969
|
+
policyPackRules: fallbackPolicyPackRules,
|
|
970
|
+
policyPack: installedPack,
|
|
971
|
+
includeDashboardPolicies: useDashboardPolicies,
|
|
972
|
+
};
|
|
973
|
+
if (!options.json) {
|
|
974
|
+
console.log(chalk.dim(' Could not load dashboard custom policies, continuing with local/default rules only'));
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
let policyLockEvaluation = {
|
|
978
|
+
enforced: false,
|
|
979
|
+
matched: true,
|
|
980
|
+
lockPresent: lockRead.lock !== null,
|
|
981
|
+
lockPath: lockRead.path,
|
|
982
|
+
mismatches: [],
|
|
983
|
+
};
|
|
984
|
+
if (!skipPolicyLock) {
|
|
985
|
+
const currentSnapshot = (0, policy_packs_1.buildPolicyStateSnapshot)({
|
|
986
|
+
policyPack: effectiveRules.policyPack,
|
|
987
|
+
policyPackRules: effectiveRules.policyPackRules,
|
|
988
|
+
customPolicies: effectiveRules.customPolicies,
|
|
989
|
+
customRules: effectiveRules.customRules,
|
|
990
|
+
includeDashboardPolicies: effectiveRules.includeDashboardPolicies,
|
|
991
|
+
});
|
|
992
|
+
const lockValidation = (0, policy_packs_1.evaluatePolicyLock)(projectRoot, currentSnapshot, {
|
|
993
|
+
requireLock: requirePolicyLock,
|
|
994
|
+
});
|
|
995
|
+
policyLockEvaluation = {
|
|
996
|
+
enforced: lockValidation.enforced,
|
|
997
|
+
matched: lockValidation.matched,
|
|
998
|
+
lockPresent: lockValidation.lockPresent,
|
|
999
|
+
lockPath: lockValidation.lockPath,
|
|
1000
|
+
mismatches: [...lockValidation.mismatches],
|
|
1001
|
+
};
|
|
1002
|
+
if (effectiveRulesLoadError && useDashboardPolicies) {
|
|
1003
|
+
policyLockEvaluation.mismatches.unshift({
|
|
1004
|
+
code: 'POLICY_LOCK_CUSTOM_POLICIES_MISMATCH',
|
|
1005
|
+
message: `Failed to load dashboard custom policies: ${effectiveRulesLoadError}`,
|
|
1006
|
+
});
|
|
1007
|
+
policyLockEvaluation.matched = false;
|
|
1008
|
+
}
|
|
1009
|
+
if (policyLockEvaluation.enforced && !policyLockEvaluation.matched) {
|
|
1010
|
+
const message = policyLockMismatchMessage(policyLockEvaluation.mismatches);
|
|
1011
|
+
recordVerifyEvent('FAIL', 'policy_lock_mismatch', diffFiles.map((f) => f.path), finalPlanId);
|
|
1012
|
+
if (options.json) {
|
|
1013
|
+
console.log(JSON.stringify({
|
|
1014
|
+
grade: 'F',
|
|
1015
|
+
score: 0,
|
|
1016
|
+
verdict: 'FAIL',
|
|
1017
|
+
violations: toPolicyLockViolations(policyLockEvaluation.mismatches),
|
|
1018
|
+
adherenceScore: 0,
|
|
1019
|
+
bloatCount: 0,
|
|
1020
|
+
bloatFiles: [],
|
|
1021
|
+
plannedFilesModified: 0,
|
|
1022
|
+
totalPlannedFiles: 0,
|
|
1023
|
+
message,
|
|
1024
|
+
scopeGuardPassed,
|
|
1025
|
+
mode: 'plan_enforced',
|
|
1026
|
+
policyOnly: false,
|
|
1027
|
+
policyLock: {
|
|
1028
|
+
enforced: true,
|
|
1029
|
+
matched: false,
|
|
1030
|
+
path: policyLockEvaluation.lockPath,
|
|
1031
|
+
mismatches: policyLockEvaluation.mismatches,
|
|
1032
|
+
},
|
|
1033
|
+
}, null, 2));
|
|
1034
|
+
}
|
|
1035
|
+
else {
|
|
1036
|
+
console.log(chalk.red('\n❌ Policy lock baseline mismatch'));
|
|
1037
|
+
console.log(chalk.dim(` Lock file: ${policyLockEvaluation.lockPath}`));
|
|
1038
|
+
policyLockEvaluation.mismatches.forEach((item) => {
|
|
1039
|
+
console.log(chalk.red(` • [${item.code}] ${item.message}`));
|
|
1040
|
+
});
|
|
1041
|
+
console.log(chalk.dim('\n If this drift is intentional, regenerate baseline with `neurcode policy lock`.\n'));
|
|
1042
|
+
}
|
|
1043
|
+
process.exit(2);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
734
1046
|
// Check user tier - Policy Compliance and A-F Grading are PRO features
|
|
1047
|
+
const governance = (0, policy_governance_1.readPolicyGovernanceConfig)(projectRoot);
|
|
1048
|
+
const auditIntegrity = (0, policy_audit_1.verifyPolicyAuditIntegrity)(projectRoot);
|
|
1049
|
+
const auditIntegrityStatus = resolveAuditIntegrityStatus(governance.audit.requireIntegrity, auditIntegrity);
|
|
735
1050
|
const { getUserTier } = await Promise.resolve().then(() => __importStar(require('../utils/tier')));
|
|
736
1051
|
const tier = await getUserTier();
|
|
737
1052
|
if (tier === 'FREE') {
|
|
@@ -759,36 +1074,105 @@ async function verifyCommand(options) {
|
|
|
759
1074
|
totalPlannedFiles: 0,
|
|
760
1075
|
message: 'Basic file change summary (PRO required for policy verification)',
|
|
761
1076
|
scopeGuardPassed: false,
|
|
1077
|
+
mode: 'plan_enforced',
|
|
1078
|
+
policyOnly: false,
|
|
762
1079
|
tier: 'FREE',
|
|
1080
|
+
policyLock: {
|
|
1081
|
+
enforced: policyLockEvaluation.enforced,
|
|
1082
|
+
matched: policyLockEvaluation.matched,
|
|
1083
|
+
path: policyLockEvaluation.lockPath,
|
|
1084
|
+
mismatches: policyLockEvaluation.mismatches,
|
|
1085
|
+
},
|
|
1086
|
+
policyGovernance: {
|
|
1087
|
+
exceptionApprovals: governance.exceptionApprovals,
|
|
1088
|
+
audit: {
|
|
1089
|
+
requireIntegrity: governance.audit.requireIntegrity,
|
|
1090
|
+
valid: auditIntegrityStatus.valid,
|
|
1091
|
+
issues: auditIntegrityStatus.issues,
|
|
1092
|
+
lastHash: auditIntegrity.lastHash,
|
|
1093
|
+
eventCount: auditIntegrity.count,
|
|
1094
|
+
},
|
|
1095
|
+
},
|
|
763
1096
|
}, null, 2));
|
|
764
1097
|
}
|
|
765
1098
|
process.exit(0);
|
|
766
1099
|
}
|
|
767
1100
|
let policyViolations = [];
|
|
768
1101
|
let policyDecision = 'allow';
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1102
|
+
const diffFilesForPolicy = diffFiles.filter((f) => !shouldIgnore(f.path));
|
|
1103
|
+
const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, effectiveRules.allRules);
|
|
1104
|
+
policyViolations = policyResult.violations.filter((v) => !shouldIgnore(v.file));
|
|
1105
|
+
const configuredPolicyExceptions = (0, policy_exceptions_1.readPolicyExceptions)(projectRoot);
|
|
1106
|
+
const exceptionDecision = (0, policy_exceptions_1.applyPolicyExceptions)(policyViolations, configuredPolicyExceptions, {
|
|
1107
|
+
requireApproval: governance.exceptionApprovals.required,
|
|
1108
|
+
minApprovals: governance.exceptionApprovals.minApprovals,
|
|
1109
|
+
disallowSelfApproval: governance.exceptionApprovals.disallowSelfApproval,
|
|
1110
|
+
allowedApprovers: governance.exceptionApprovals.allowedApprovers,
|
|
1111
|
+
});
|
|
1112
|
+
const suppressedPolicyViolations = exceptionDecision.suppressedViolations.filter((item) => !shouldIgnore(item.file));
|
|
1113
|
+
const blockedPolicyViolations = exceptionDecision.blockedViolations
|
|
1114
|
+
.filter((item) => !shouldIgnore(item.file))
|
|
1115
|
+
.map((item) => ({
|
|
1116
|
+
file: item.file,
|
|
1117
|
+
rule: item.rule,
|
|
1118
|
+
severity: 'block',
|
|
1119
|
+
message: `Exception ${item.exceptionId} cannot be applied: ${explainExceptionEligibilityReason(item.eligibilityReason)}`,
|
|
1120
|
+
...(item.line != null ? { line: item.line } : {}),
|
|
1121
|
+
}));
|
|
1122
|
+
policyViolations = [
|
|
1123
|
+
...exceptionDecision.remainingViolations.filter((item) => !shouldIgnore(item.file)),
|
|
1124
|
+
...blockedPolicyViolations,
|
|
1125
|
+
];
|
|
1126
|
+
if (governance.audit.requireIntegrity && !auditIntegrityStatus.valid) {
|
|
1127
|
+
policyViolations.push({
|
|
1128
|
+
file: POLICY_AUDIT_FILE,
|
|
1129
|
+
rule: 'policy_audit_integrity',
|
|
1130
|
+
severity: 'block',
|
|
1131
|
+
message: `Policy audit chain is invalid: ${auditIntegrityStatus.issues.join('; ') || 'unknown issue'}`,
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
policyDecision = resolvePolicyDecisionFromViolations(policyViolations);
|
|
1135
|
+
const policyExceptionsSummary = {
|
|
1136
|
+
configured: configuredPolicyExceptions.length,
|
|
1137
|
+
active: exceptionDecision.activeExceptions.length,
|
|
1138
|
+
usable: exceptionDecision.usableExceptions.length,
|
|
1139
|
+
matched: exceptionDecision.matchedExceptionIds.length,
|
|
1140
|
+
suppressed: suppressedPolicyViolations.length,
|
|
1141
|
+
blocked: blockedPolicyViolations.length,
|
|
1142
|
+
matchedExceptionIds: exceptionDecision.matchedExceptionIds,
|
|
1143
|
+
suppressedViolations: suppressedPolicyViolations.map((item) => ({
|
|
1144
|
+
file: item.file,
|
|
1145
|
+
rule: item.rule,
|
|
1146
|
+
severity: item.severity,
|
|
1147
|
+
message: item.message,
|
|
1148
|
+
exceptionId: item.exceptionId,
|
|
1149
|
+
reason: item.reason,
|
|
1150
|
+
expiresAt: item.expiresAt,
|
|
1151
|
+
...(item.line != null ? { startLine: item.line } : {}),
|
|
1152
|
+
})),
|
|
1153
|
+
blockedViolations: blockedPolicyViolations.map((item) => ({
|
|
1154
|
+
file: item.file,
|
|
1155
|
+
rule: item.rule,
|
|
1156
|
+
severity: item.severity,
|
|
1157
|
+
message: item.message,
|
|
1158
|
+
...(item.line != null ? { startLine: item.line } : {}),
|
|
1159
|
+
})),
|
|
774
1160
|
};
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}
|
|
1161
|
+
const policyGovernanceSummary = {
|
|
1162
|
+
exceptionApprovals: governance.exceptionApprovals,
|
|
1163
|
+
audit: {
|
|
1164
|
+
requireIntegrity: governance.audit.requireIntegrity,
|
|
1165
|
+
valid: auditIntegrityStatus.valid,
|
|
1166
|
+
issues: auditIntegrityStatus.issues,
|
|
1167
|
+
lastHash: auditIntegrity.lastHash,
|
|
1168
|
+
eventCount: auditIntegrity.count,
|
|
1169
|
+
},
|
|
1170
|
+
};
|
|
1171
|
+
if (!options.json && effectiveRules.customRules.length > 0) {
|
|
1172
|
+
console.log(chalk.dim(` Evaluating ${effectiveRules.customRules.length} custom policy rule(s) from dashboard`));
|
|
787
1173
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
console.log(chalk.dim(' Could not load dashboard custom policies, continuing with local/default rules only'));
|
|
791
|
-
}
|
|
1174
|
+
if (!options.json && effectiveRules.policyPack && effectiveRules.policyPackRules.length > 0) {
|
|
1175
|
+
console.log(chalk.dim(` Evaluating policy pack: ${effectiveRules.policyPack.packName} (${effectiveRules.policyPack.packId}@${effectiveRules.policyPack.version}, ${effectiveRules.policyPackRules.length} rule(s))`));
|
|
792
1176
|
}
|
|
793
1177
|
// Prepare diff stats and changed files for API
|
|
794
1178
|
const diffStats = {
|
|
@@ -834,9 +1218,12 @@ async function verifyCommand(options) {
|
|
|
834
1218
|
// Apply custom policy verdict: block from dashboard overrides API verdict
|
|
835
1219
|
const policyBlock = policyDecision === 'block' && policyViolations.length > 0;
|
|
836
1220
|
const effectiveVerdict = policyBlock ? 'FAIL' : verifyResult.verdict;
|
|
837
|
-
const
|
|
1221
|
+
const policyMessageBase = policyBlock
|
|
838
1222
|
? `Custom policy violations: ${policyViolations.map(v => `${v.file}: ${v.message || v.rule}`).join('; ')}. ${verifyResult.message}`
|
|
839
1223
|
: verifyResult.message;
|
|
1224
|
+
const effectiveMessage = policyExceptionsSummary.suppressed > 0
|
|
1225
|
+
? `${policyMessageBase} Policy exceptions suppressed ${policyExceptionsSummary.suppressed} violation(s).`
|
|
1226
|
+
: policyMessageBase;
|
|
840
1227
|
// Calculate grade from effective verdict and score
|
|
841
1228
|
// CRITICAL: 0/0 planned files = 'F' (Incomplete), not 'B'
|
|
842
1229
|
// Bloat automatically drops grade by at least one letter
|
|
@@ -882,7 +1269,13 @@ async function verifyCommand(options) {
|
|
|
882
1269
|
const changedPathsForBrain = diffFiles
|
|
883
1270
|
.flatMap((file) => [file.path, file.oldPath])
|
|
884
1271
|
.filter((value) => Boolean(value));
|
|
885
|
-
recordVerifyEvent(effectiveVerdict, `adherence=${verifyResult.adherenceScore};bloat=${verifyResult.bloatCount};scopeGuard=${scopeGuardPassed ? 1 : 0};policy=${policyDecision}`, changedPathsForBrain, finalPlanId);
|
|
1272
|
+
recordVerifyEvent(effectiveVerdict, `adherence=${verifyResult.adherenceScore};bloat=${verifyResult.bloatCount};scopeGuard=${scopeGuardPassed ? 1 : 0};policy=${policyDecision};policyExceptions=${policyExceptionsSummary.suppressed}`, changedPathsForBrain, finalPlanId);
|
|
1273
|
+
const shouldForceGovernancePass = scopeGuardPassed &&
|
|
1274
|
+
!policyBlock &&
|
|
1275
|
+
(effectiveVerdict === 'PASS' ||
|
|
1276
|
+
((verifyResult.verdict === 'FAIL' || verifyResult.verdict === 'WARN') &&
|
|
1277
|
+
policyViolations.length === 0 &&
|
|
1278
|
+
verifyResult.bloatCount > 0));
|
|
886
1279
|
// If JSON output requested, output JSON and exit
|
|
887
1280
|
if (options.json) {
|
|
888
1281
|
const filteredBloatFiles = (verifyResult.bloatFiles || []).filter((f) => !shouldIgnore(f));
|
|
@@ -912,6 +1305,16 @@ async function verifyCommand(options) {
|
|
|
912
1305
|
bloatFiles: filteredBloatFiles,
|
|
913
1306
|
plannedFilesModified: verifyResult.plannedFilesModified,
|
|
914
1307
|
totalPlannedFiles: verifyResult.totalPlannedFiles,
|
|
1308
|
+
mode: 'plan_enforced',
|
|
1309
|
+
policyOnly: false,
|
|
1310
|
+
policyLock: {
|
|
1311
|
+
enforced: policyLockEvaluation.enforced,
|
|
1312
|
+
matched: policyLockEvaluation.matched,
|
|
1313
|
+
path: policyLockEvaluation.lockPath,
|
|
1314
|
+
mismatches: policyLockEvaluation.mismatches,
|
|
1315
|
+
},
|
|
1316
|
+
policyExceptions: policyExceptionsSummary,
|
|
1317
|
+
policyGovernance: policyGovernanceSummary,
|
|
915
1318
|
...(policyViolations.length > 0 && { policyDecision }),
|
|
916
1319
|
...(effectiveRules.policyPack
|
|
917
1320
|
? {
|
|
@@ -948,7 +1351,7 @@ async function verifyCommand(options) {
|
|
|
948
1351
|
});
|
|
949
1352
|
}
|
|
950
1353
|
// Exit based on effective verdict (same logic as below)
|
|
951
|
-
if (
|
|
1354
|
+
if (shouldForceGovernancePass) {
|
|
952
1355
|
process.exit(0);
|
|
953
1356
|
}
|
|
954
1357
|
if (effectiveVerdict === 'FAIL') {
|
|
@@ -971,6 +1374,25 @@ async function verifyCommand(options) {
|
|
|
971
1374
|
bloatFiles: displayBloatFiles,
|
|
972
1375
|
bloatCount: displayBloatFiles.length,
|
|
973
1376
|
}, policyViolations);
|
|
1377
|
+
if (policyExceptionsSummary.suppressed > 0) {
|
|
1378
|
+
console.log(chalk.yellow(`\n⚠️ Policy exceptions applied: ${policyExceptionsSummary.suppressed}`));
|
|
1379
|
+
if (policyExceptionsSummary.matchedExceptionIds.length > 0) {
|
|
1380
|
+
console.log(chalk.dim(` Exception IDs: ${policyExceptionsSummary.matchedExceptionIds.join(', ')}`));
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
if (policyExceptionsSummary.blocked > 0) {
|
|
1384
|
+
console.log(chalk.red(`\n⛔ Policy exceptions blocked by approval governance: ${policyExceptionsSummary.blocked}`));
|
|
1385
|
+
const sample = policyExceptionsSummary.blockedViolations.slice(0, 5);
|
|
1386
|
+
sample.forEach((item) => {
|
|
1387
|
+
console.log(chalk.red(` • ${item.file}: ${item.message || item.rule}`));
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
if (governance.audit.requireIntegrity && !auditIntegrityStatus.valid) {
|
|
1391
|
+
console.log(chalk.red('\n⛔ Policy audit integrity enforcement is enabled and chain verification failed.'));
|
|
1392
|
+
auditIntegrityStatus.issues.slice(0, 5).forEach((issue) => {
|
|
1393
|
+
console.log(chalk.red(` • ${issue}`));
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
974
1396
|
}
|
|
975
1397
|
// Report to Neurcode Cloud if --record flag is set
|
|
976
1398
|
if (options.record) {
|
|
@@ -1001,9 +1423,9 @@ async function verifyCommand(options) {
|
|
|
1001
1423
|
);
|
|
1002
1424
|
}
|
|
1003
1425
|
}
|
|
1004
|
-
// Governance
|
|
1005
|
-
//
|
|
1006
|
-
if (
|
|
1426
|
+
// Governance override: keep PASS only when scope guard passes and failure is due
|
|
1427
|
+
// to server-side bloat mismatch (allowed files unknown to verify API).
|
|
1428
|
+
if (shouldForceGovernancePass) {
|
|
1007
1429
|
if ((verifyResult.verdict === 'FAIL' || verifyResult.verdict === 'WARN') && policyViolations.length === 0) {
|
|
1008
1430
|
if (!options.json) {
|
|
1009
1431
|
console.log(chalk.yellow('\n⚠️ Plan deviation allowed'));
|
|
@@ -1011,6 +1433,9 @@ async function verifyCommand(options) {
|
|
|
1011
1433
|
console.log(chalk.dim(' Governance check passed - proceeding with exit code 0.\n'));
|
|
1012
1434
|
}
|
|
1013
1435
|
}
|
|
1436
|
+
if (!options.json && policyExceptionsSummary.suppressed > 0) {
|
|
1437
|
+
console.log(chalk.yellow(` Policy exceptions applied: ${policyExceptionsSummary.suppressed}`));
|
|
1438
|
+
}
|
|
1014
1439
|
process.exit(0);
|
|
1015
1440
|
}
|
|
1016
1441
|
// If scope guard didn't pass (or failed to check) or policy blocked, use effective verdict
|