@neurcode-ai/cli 0.9.27 → 0.9.29
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 +58 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +38 -0
- package/dist/api-client.js.map +1 -1
- 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/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 +518 -42
- 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 +130 -26
- 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 +542 -115
- 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,34 +226,183 @@ 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
|
/**
|
|
289
294
|
* Execute policy-only verification (General Governance mode)
|
|
290
295
|
* Returns the exit code to use
|
|
291
296
|
*/
|
|
292
|
-
async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client) {
|
|
297
|
+
async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source) {
|
|
293
298
|
if (!options.json) {
|
|
294
299
|
console.log(chalk.cyan('🛡️ General Governance mode (policy only, no plan linked)\n'));
|
|
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
|
+
policyOnlySource: source,
|
|
388
|
+
policyLock: {
|
|
389
|
+
enforced: true,
|
|
390
|
+
matched: false,
|
|
391
|
+
path: policyLockEvaluation.lockPath,
|
|
392
|
+
mismatches: policyLockEvaluation.mismatches,
|
|
393
|
+
},
|
|
394
|
+
}, null, 2));
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
console.log(chalk.red('❌ Policy lock baseline mismatch.'));
|
|
398
|
+
console.log(chalk.dim(` Lock file: ${policyLockEvaluation.lockPath}`));
|
|
399
|
+
policyLockEvaluation.mismatches.forEach((item) => {
|
|
400
|
+
console.log(chalk.red(` • [${item.code}] ${item.message}`));
|
|
401
|
+
});
|
|
402
|
+
console.log(chalk.dim('\n If drift is intentional, regenerate baseline with `neurcode policy lock`.\n'));
|
|
403
|
+
}
|
|
404
|
+
return 2;
|
|
405
|
+
}
|
|
312
406
|
if (!options.json && effectiveRules.customRules.length > 0) {
|
|
313
407
|
console.log(chalk.dim(` Evaluating ${effectiveRules.customRules.length} custom policy rule(s) from dashboard`));
|
|
314
408
|
}
|
|
@@ -322,7 +416,39 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
322
416
|
const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, effectiveRules.allRules);
|
|
323
417
|
policyViolations = (policyResult.violations || []);
|
|
324
418
|
policyViolations = policyViolations.filter((v) => !ignoreFilter(v.file));
|
|
325
|
-
|
|
419
|
+
const governance = (0, policy_governance_1.readPolicyGovernanceConfig)(projectRoot);
|
|
420
|
+
const auditIntegrity = (0, policy_audit_1.verifyPolicyAuditIntegrity)(projectRoot);
|
|
421
|
+
const auditIntegrityStatus = resolveAuditIntegrityStatus(governance.audit.requireIntegrity, auditIntegrity);
|
|
422
|
+
const configuredPolicyExceptions = (0, policy_exceptions_1.readPolicyExceptions)(projectRoot);
|
|
423
|
+
const exceptionDecision = (0, policy_exceptions_1.applyPolicyExceptions)(policyViolations, configuredPolicyExceptions, {
|
|
424
|
+
requireApproval: governance.exceptionApprovals.required,
|
|
425
|
+
minApprovals: governance.exceptionApprovals.minApprovals,
|
|
426
|
+
disallowSelfApproval: governance.exceptionApprovals.disallowSelfApproval,
|
|
427
|
+
allowedApprovers: governance.exceptionApprovals.allowedApprovers,
|
|
428
|
+
});
|
|
429
|
+
const suppressedViolations = exceptionDecision.suppressedViolations.filter((item) => !ignoreFilter(item.file));
|
|
430
|
+
const blockedViolations = exceptionDecision.blockedViolations
|
|
431
|
+
.filter((item) => !ignoreFilter(item.file))
|
|
432
|
+
.map((item) => ({
|
|
433
|
+
file: item.file,
|
|
434
|
+
rule: item.rule,
|
|
435
|
+
severity: 'block',
|
|
436
|
+
message: `Exception ${item.exceptionId} cannot be applied: ${explainExceptionEligibilityReason(item.eligibilityReason)}`,
|
|
437
|
+
...(item.line != null ? { line: item.line } : {}),
|
|
438
|
+
}));
|
|
439
|
+
policyViolations = [
|
|
440
|
+
...exceptionDecision.remainingViolations.filter((item) => !ignoreFilter(item.file)),
|
|
441
|
+
...blockedViolations,
|
|
442
|
+
];
|
|
443
|
+
if (governance.audit.requireIntegrity && !auditIntegrityStatus.valid) {
|
|
444
|
+
policyViolations.push({
|
|
445
|
+
file: POLICY_AUDIT_FILE,
|
|
446
|
+
rule: 'policy_audit_integrity',
|
|
447
|
+
severity: 'block',
|
|
448
|
+
message: `Policy audit chain is invalid: ${auditIntegrityStatus.issues.join('; ') || 'unknown issue'}`,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
policyDecision = resolvePolicyDecisionFromViolations(policyViolations);
|
|
326
452
|
const effectiveVerdict = policyDecision === 'block' ? 'FAIL' : policyDecision === 'warn' ? 'WARN' : 'PASS';
|
|
327
453
|
const grade = effectiveVerdict === 'PASS' ? 'A' : effectiveVerdict === 'WARN' ? 'C' : 'F';
|
|
328
454
|
const score = effectiveVerdict === 'PASS' ? 100 : effectiveVerdict === 'WARN' ? 50 : 0;
|
|
@@ -338,6 +464,42 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
338
464
|
: policyViolations.length > 0
|
|
339
465
|
? `Policy violations: ${policyViolations.map((v) => `${v.file}: ${v.message || v.rule}`).join('; ')}`
|
|
340
466
|
: 'Policy check completed';
|
|
467
|
+
const policyExceptionsSummary = {
|
|
468
|
+
configured: configuredPolicyExceptions.length,
|
|
469
|
+
active: exceptionDecision.activeExceptions.length,
|
|
470
|
+
usable: exceptionDecision.usableExceptions.length,
|
|
471
|
+
matched: exceptionDecision.matchedExceptionIds.length,
|
|
472
|
+
suppressed: suppressedViolations.length,
|
|
473
|
+
blocked: blockedViolations.length,
|
|
474
|
+
matchedExceptionIds: exceptionDecision.matchedExceptionIds,
|
|
475
|
+
suppressedViolations: suppressedViolations.map((item) => ({
|
|
476
|
+
file: item.file,
|
|
477
|
+
rule: item.rule,
|
|
478
|
+
severity: item.severity,
|
|
479
|
+
message: item.message,
|
|
480
|
+
exceptionId: item.exceptionId,
|
|
481
|
+
reason: item.reason,
|
|
482
|
+
expiresAt: item.expiresAt,
|
|
483
|
+
...(item.line != null ? { startLine: item.line } : {}),
|
|
484
|
+
})),
|
|
485
|
+
blockedViolations: blockedViolations.map((item) => ({
|
|
486
|
+
file: item.file,
|
|
487
|
+
rule: item.rule,
|
|
488
|
+
severity: item.severity,
|
|
489
|
+
message: item.message,
|
|
490
|
+
...(item.line != null ? { startLine: item.line } : {}),
|
|
491
|
+
})),
|
|
492
|
+
};
|
|
493
|
+
const policyGovernanceSummary = {
|
|
494
|
+
exceptionApprovals: governance.exceptionApprovals,
|
|
495
|
+
audit: {
|
|
496
|
+
requireIntegrity: governance.audit.requireIntegrity,
|
|
497
|
+
valid: auditIntegrityStatus.valid,
|
|
498
|
+
issues: auditIntegrityStatus.issues,
|
|
499
|
+
lastHash: auditIntegrity.lastHash,
|
|
500
|
+
eventCount: auditIntegrity.count,
|
|
501
|
+
},
|
|
502
|
+
};
|
|
341
503
|
if (options.json) {
|
|
342
504
|
console.log(JSON.stringify({
|
|
343
505
|
grade,
|
|
@@ -351,7 +513,17 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
351
513
|
plannedFilesModified: 0,
|
|
352
514
|
totalPlannedFiles: 0,
|
|
353
515
|
adherenceScore: score,
|
|
516
|
+
mode: 'policy_only',
|
|
354
517
|
policyOnly: true,
|
|
518
|
+
policyOnlySource: source,
|
|
519
|
+
policyLock: {
|
|
520
|
+
enforced: policyLockEvaluation.enforced,
|
|
521
|
+
matched: policyLockEvaluation.matched,
|
|
522
|
+
path: policyLockEvaluation.lockPath,
|
|
523
|
+
mismatches: policyLockEvaluation.mismatches,
|
|
524
|
+
},
|
|
525
|
+
policyExceptions: policyExceptionsSummary,
|
|
526
|
+
policyGovernance: policyGovernanceSummary,
|
|
355
527
|
...(effectiveRules.policyPack
|
|
356
528
|
? {
|
|
357
529
|
policyPack: {
|
|
@@ -374,6 +546,15 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
374
546
|
console.log(chalk.red(` • ${v.file}: ${v.message || v.rule}`));
|
|
375
547
|
});
|
|
376
548
|
}
|
|
549
|
+
if (policyExceptionsSummary.suppressed > 0) {
|
|
550
|
+
console.log(chalk.yellow(` Policy exceptions applied: ${policyExceptionsSummary.suppressed}`));
|
|
551
|
+
}
|
|
552
|
+
if (policyExceptionsSummary.blocked > 0) {
|
|
553
|
+
console.log(chalk.red(` Policy exceptions blocked by approval governance: ${policyExceptionsSummary.blocked}`));
|
|
554
|
+
}
|
|
555
|
+
if (governance.audit.requireIntegrity && !auditIntegrityStatus.valid) {
|
|
556
|
+
console.log(chalk.red(' Policy audit integrity check failed'));
|
|
557
|
+
}
|
|
377
558
|
console.log(chalk.dim(`\n${message}`));
|
|
378
559
|
}
|
|
379
560
|
return effectiveVerdict === 'FAIL' ? 2 : effectiveVerdict === 'WARN' ? 1 : 0;
|
|
@@ -551,16 +732,20 @@ async function verifyCommand(options) {
|
|
|
551
732
|
console.log(chalk.dim(` Found ${summary.totalFiles} file(s) changed`));
|
|
552
733
|
console.log(chalk.dim(` ${summary.totalAdded} lines added, ${summary.totalRemoved} lines removed\n`));
|
|
553
734
|
}
|
|
735
|
+
const runPolicyOnlyModeAndExit = async (source) => {
|
|
736
|
+
const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client, source);
|
|
737
|
+
const changedFiles = diffFiles.map((f) => f.path);
|
|
738
|
+
const verdict = exitCode === 2 ? 'FAIL' : exitCode === 1 ? 'WARN' : 'PASS';
|
|
739
|
+
recordVerifyEvent(verdict, `policy_only_source=${source};exit=${exitCode}`, changedFiles);
|
|
740
|
+
process.exit(exitCode);
|
|
741
|
+
};
|
|
554
742
|
// ============================================
|
|
555
743
|
// --policy-only: General Governance (policy only, no plan enforcement)
|
|
556
744
|
// ============================================
|
|
557
745
|
if (options.policyOnly) {
|
|
558
|
-
|
|
559
|
-
const changedFiles = diffFiles.map((f) => f.path);
|
|
560
|
-
const verdict = exitCode === 2 ? 'FAIL' : exitCode === 1 ? 'WARN' : 'PASS';
|
|
561
|
-
recordVerifyEvent(verdict, `policy_only=true;exit=${exitCode}`, changedFiles);
|
|
562
|
-
process.exit(exitCode);
|
|
746
|
+
await runPolicyOnlyModeAndExit('explicit');
|
|
563
747
|
}
|
|
748
|
+
const requirePlan = options.requirePlan === true || process.env.NEURCODE_VERIFY_REQUIRE_PLAN === '1';
|
|
564
749
|
// Get planId: Priority 1: options flag, Priority 2: state file (.neurcode/config.json), Priority 3: legacy config
|
|
565
750
|
let planId = options.planId;
|
|
566
751
|
if (!planId) {
|
|
@@ -593,19 +778,43 @@ async function verifyCommand(options) {
|
|
|
593
778
|
}
|
|
594
779
|
}
|
|
595
780
|
}
|
|
596
|
-
// If no planId found, fall back to
|
|
781
|
+
// If no planId found, either enforce strict requirement or fall back to policy-only mode.
|
|
597
782
|
if (!planId) {
|
|
783
|
+
if (requirePlan) {
|
|
784
|
+
const changedFiles = diffFiles.map((f) => f.path);
|
|
785
|
+
recordVerifyEvent('FAIL', 'missing_plan_id;require_plan=true', changedFiles);
|
|
786
|
+
if (options.json) {
|
|
787
|
+
console.log(JSON.stringify({
|
|
788
|
+
grade: 'F',
|
|
789
|
+
score: 0,
|
|
790
|
+
verdict: 'FAIL',
|
|
791
|
+
violations: [],
|
|
792
|
+
adherenceScore: 0,
|
|
793
|
+
bloatCount: 0,
|
|
794
|
+
bloatFiles: [],
|
|
795
|
+
plannedFilesModified: 0,
|
|
796
|
+
totalPlannedFiles: 0,
|
|
797
|
+
message: 'Plan ID is required in strict mode. Run "neurcode plan" first or pass --plan-id.',
|
|
798
|
+
scopeGuardPassed: false,
|
|
799
|
+
mode: 'plan_required',
|
|
800
|
+
policyOnly: false,
|
|
801
|
+
}, null, 2));
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
console.log(chalk.red('❌ Plan ID is required in strict mode.'));
|
|
805
|
+
console.log(chalk.dim(' Run "neurcode plan" first or pass --plan-id <id>.'));
|
|
806
|
+
console.log(chalk.dim(' Use --policy-only only when intentionally running general governance checks.'));
|
|
807
|
+
}
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
598
810
|
if (!options.json) {
|
|
599
811
|
console.log(chalk.yellow('⚠️ No Plan ID found. Falling back to General Governance (Policy Only).'));
|
|
600
812
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
recordVerifyEvent(verdict, `policy_only=fallback;exit=${exitCode}`, changedFiles);
|
|
606
|
-
process.exit(exitCode);
|
|
813
|
+
await runPolicyOnlyModeAndExit('fallback_missing_plan');
|
|
814
|
+
}
|
|
815
|
+
if (!planId) {
|
|
816
|
+
throw new Error('Plan ID resolution failed unexpectedly');
|
|
607
817
|
}
|
|
608
|
-
// At this point, planId is guaranteed to be defined
|
|
609
818
|
const finalPlanId = planId;
|
|
610
819
|
// ============================================
|
|
611
820
|
// STRICT SCOPE GUARD - Deterministic Check
|
|
@@ -692,6 +901,8 @@ async function verifyCommand(options) {
|
|
|
692
901
|
totalPlannedFiles: planFiles.length,
|
|
693
902
|
message: `Scope violation: ${filteredViolations.length} file(s) modified outside the plan`,
|
|
694
903
|
scopeGuardPassed: false,
|
|
904
|
+
mode: 'plan_enforced',
|
|
905
|
+
policyOnly: false,
|
|
695
906
|
};
|
|
696
907
|
// CRITICAL: Print JSON first, then exit
|
|
697
908
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
@@ -731,7 +942,113 @@ async function verifyCommand(options) {
|
|
|
731
942
|
console.log('');
|
|
732
943
|
}
|
|
733
944
|
}
|
|
945
|
+
const requirePolicyLock = options.requirePolicyLock === true || isEnabledFlag(process.env.NEURCODE_VERIFY_REQUIRE_POLICY_LOCK);
|
|
946
|
+
const skipPolicyLock = options.skipPolicyLock === true || isEnabledFlag(process.env.NEURCODE_VERIFY_SKIP_POLICY_LOCK);
|
|
947
|
+
const lockRead = (0, policy_packs_1.readPolicyLockFile)(projectRoot);
|
|
948
|
+
const useDashboardPolicies = lockRead.lock
|
|
949
|
+
? lockRead.lock.customPolicies.mode === 'dashboard'
|
|
950
|
+
: Boolean(config.apiKey);
|
|
951
|
+
let effectiveRulesLoadError = null;
|
|
952
|
+
let effectiveRules = {
|
|
953
|
+
allRules: (0, policy_engine_1.createDefaultPolicy)().rules,
|
|
954
|
+
customRules: [],
|
|
955
|
+
customPolicies: [],
|
|
956
|
+
policyPackRules: [],
|
|
957
|
+
policyPack: null,
|
|
958
|
+
includeDashboardPolicies: useDashboardPolicies,
|
|
959
|
+
};
|
|
960
|
+
try {
|
|
961
|
+
effectiveRules = await buildEffectivePolicyRules(client, projectRoot, useDashboardPolicies);
|
|
962
|
+
}
|
|
963
|
+
catch (error) {
|
|
964
|
+
effectiveRulesLoadError = error instanceof Error ? error.message : 'Unknown error';
|
|
965
|
+
const installedPack = (0, policy_packs_1.getInstalledPolicyPackRules)(projectRoot);
|
|
966
|
+
const fallbackPolicyPackRules = installedPack?.rules ? [...installedPack.rules] : [];
|
|
967
|
+
effectiveRules = {
|
|
968
|
+
allRules: [...(0, policy_engine_1.createDefaultPolicy)().rules, ...fallbackPolicyPackRules],
|
|
969
|
+
customRules: [],
|
|
970
|
+
customPolicies: [],
|
|
971
|
+
policyPackRules: fallbackPolicyPackRules,
|
|
972
|
+
policyPack: installedPack,
|
|
973
|
+
includeDashboardPolicies: useDashboardPolicies,
|
|
974
|
+
};
|
|
975
|
+
if (!options.json) {
|
|
976
|
+
console.log(chalk.dim(' Could not load dashboard custom policies, continuing with local/default rules only'));
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
let policyLockEvaluation = {
|
|
980
|
+
enforced: false,
|
|
981
|
+
matched: true,
|
|
982
|
+
lockPresent: lockRead.lock !== null,
|
|
983
|
+
lockPath: lockRead.path,
|
|
984
|
+
mismatches: [],
|
|
985
|
+
};
|
|
986
|
+
if (!skipPolicyLock) {
|
|
987
|
+
const currentSnapshot = (0, policy_packs_1.buildPolicyStateSnapshot)({
|
|
988
|
+
policyPack: effectiveRules.policyPack,
|
|
989
|
+
policyPackRules: effectiveRules.policyPackRules,
|
|
990
|
+
customPolicies: effectiveRules.customPolicies,
|
|
991
|
+
customRules: effectiveRules.customRules,
|
|
992
|
+
includeDashboardPolicies: effectiveRules.includeDashboardPolicies,
|
|
993
|
+
});
|
|
994
|
+
const lockValidation = (0, policy_packs_1.evaluatePolicyLock)(projectRoot, currentSnapshot, {
|
|
995
|
+
requireLock: requirePolicyLock,
|
|
996
|
+
});
|
|
997
|
+
policyLockEvaluation = {
|
|
998
|
+
enforced: lockValidation.enforced,
|
|
999
|
+
matched: lockValidation.matched,
|
|
1000
|
+
lockPresent: lockValidation.lockPresent,
|
|
1001
|
+
lockPath: lockValidation.lockPath,
|
|
1002
|
+
mismatches: [...lockValidation.mismatches],
|
|
1003
|
+
};
|
|
1004
|
+
if (effectiveRulesLoadError && useDashboardPolicies) {
|
|
1005
|
+
policyLockEvaluation.mismatches.unshift({
|
|
1006
|
+
code: 'POLICY_LOCK_CUSTOM_POLICIES_MISMATCH',
|
|
1007
|
+
message: `Failed to load dashboard custom policies: ${effectiveRulesLoadError}`,
|
|
1008
|
+
});
|
|
1009
|
+
policyLockEvaluation.matched = false;
|
|
1010
|
+
}
|
|
1011
|
+
if (policyLockEvaluation.enforced && !policyLockEvaluation.matched) {
|
|
1012
|
+
const message = policyLockMismatchMessage(policyLockEvaluation.mismatches);
|
|
1013
|
+
recordVerifyEvent('FAIL', 'policy_lock_mismatch', diffFiles.map((f) => f.path), finalPlanId);
|
|
1014
|
+
if (options.json) {
|
|
1015
|
+
console.log(JSON.stringify({
|
|
1016
|
+
grade: 'F',
|
|
1017
|
+
score: 0,
|
|
1018
|
+
verdict: 'FAIL',
|
|
1019
|
+
violations: toPolicyLockViolations(policyLockEvaluation.mismatches),
|
|
1020
|
+
adherenceScore: 0,
|
|
1021
|
+
bloatCount: 0,
|
|
1022
|
+
bloatFiles: [],
|
|
1023
|
+
plannedFilesModified: 0,
|
|
1024
|
+
totalPlannedFiles: 0,
|
|
1025
|
+
message,
|
|
1026
|
+
scopeGuardPassed,
|
|
1027
|
+
mode: 'plan_enforced',
|
|
1028
|
+
policyOnly: false,
|
|
1029
|
+
policyLock: {
|
|
1030
|
+
enforced: true,
|
|
1031
|
+
matched: false,
|
|
1032
|
+
path: policyLockEvaluation.lockPath,
|
|
1033
|
+
mismatches: policyLockEvaluation.mismatches,
|
|
1034
|
+
},
|
|
1035
|
+
}, null, 2));
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
console.log(chalk.red('\n❌ Policy lock baseline mismatch'));
|
|
1039
|
+
console.log(chalk.dim(` Lock file: ${policyLockEvaluation.lockPath}`));
|
|
1040
|
+
policyLockEvaluation.mismatches.forEach((item) => {
|
|
1041
|
+
console.log(chalk.red(` • [${item.code}] ${item.message}`));
|
|
1042
|
+
});
|
|
1043
|
+
console.log(chalk.dim('\n If this drift is intentional, regenerate baseline with `neurcode policy lock`.\n'));
|
|
1044
|
+
}
|
|
1045
|
+
process.exit(2);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
734
1048
|
// Check user tier - Policy Compliance and A-F Grading are PRO features
|
|
1049
|
+
const governance = (0, policy_governance_1.readPolicyGovernanceConfig)(projectRoot);
|
|
1050
|
+
const auditIntegrity = (0, policy_audit_1.verifyPolicyAuditIntegrity)(projectRoot);
|
|
1051
|
+
const auditIntegrityStatus = resolveAuditIntegrityStatus(governance.audit.requireIntegrity, auditIntegrity);
|
|
735
1052
|
const { getUserTier } = await Promise.resolve().then(() => __importStar(require('../utils/tier')));
|
|
736
1053
|
const tier = await getUserTier();
|
|
737
1054
|
if (tier === 'FREE') {
|
|
@@ -759,36 +1076,105 @@ async function verifyCommand(options) {
|
|
|
759
1076
|
totalPlannedFiles: 0,
|
|
760
1077
|
message: 'Basic file change summary (PRO required for policy verification)',
|
|
761
1078
|
scopeGuardPassed: false,
|
|
1079
|
+
mode: 'plan_enforced',
|
|
1080
|
+
policyOnly: false,
|
|
762
1081
|
tier: 'FREE',
|
|
1082
|
+
policyLock: {
|
|
1083
|
+
enforced: policyLockEvaluation.enforced,
|
|
1084
|
+
matched: policyLockEvaluation.matched,
|
|
1085
|
+
path: policyLockEvaluation.lockPath,
|
|
1086
|
+
mismatches: policyLockEvaluation.mismatches,
|
|
1087
|
+
},
|
|
1088
|
+
policyGovernance: {
|
|
1089
|
+
exceptionApprovals: governance.exceptionApprovals,
|
|
1090
|
+
audit: {
|
|
1091
|
+
requireIntegrity: governance.audit.requireIntegrity,
|
|
1092
|
+
valid: auditIntegrityStatus.valid,
|
|
1093
|
+
issues: auditIntegrityStatus.issues,
|
|
1094
|
+
lastHash: auditIntegrity.lastHash,
|
|
1095
|
+
eventCount: auditIntegrity.count,
|
|
1096
|
+
},
|
|
1097
|
+
},
|
|
763
1098
|
}, null, 2));
|
|
764
1099
|
}
|
|
765
1100
|
process.exit(0);
|
|
766
1101
|
}
|
|
767
1102
|
let policyViolations = [];
|
|
768
1103
|
let policyDecision = 'allow';
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1104
|
+
const diffFilesForPolicy = diffFiles.filter((f) => !shouldIgnore(f.path));
|
|
1105
|
+
const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, effectiveRules.allRules);
|
|
1106
|
+
policyViolations = policyResult.violations.filter((v) => !shouldIgnore(v.file));
|
|
1107
|
+
const configuredPolicyExceptions = (0, policy_exceptions_1.readPolicyExceptions)(projectRoot);
|
|
1108
|
+
const exceptionDecision = (0, policy_exceptions_1.applyPolicyExceptions)(policyViolations, configuredPolicyExceptions, {
|
|
1109
|
+
requireApproval: governance.exceptionApprovals.required,
|
|
1110
|
+
minApprovals: governance.exceptionApprovals.minApprovals,
|
|
1111
|
+
disallowSelfApproval: governance.exceptionApprovals.disallowSelfApproval,
|
|
1112
|
+
allowedApprovers: governance.exceptionApprovals.allowedApprovers,
|
|
1113
|
+
});
|
|
1114
|
+
const suppressedPolicyViolations = exceptionDecision.suppressedViolations.filter((item) => !shouldIgnore(item.file));
|
|
1115
|
+
const blockedPolicyViolations = exceptionDecision.blockedViolations
|
|
1116
|
+
.filter((item) => !shouldIgnore(item.file))
|
|
1117
|
+
.map((item) => ({
|
|
1118
|
+
file: item.file,
|
|
1119
|
+
rule: item.rule,
|
|
1120
|
+
severity: 'block',
|
|
1121
|
+
message: `Exception ${item.exceptionId} cannot be applied: ${explainExceptionEligibilityReason(item.eligibilityReason)}`,
|
|
1122
|
+
...(item.line != null ? { line: item.line } : {}),
|
|
1123
|
+
}));
|
|
1124
|
+
policyViolations = [
|
|
1125
|
+
...exceptionDecision.remainingViolations.filter((item) => !shouldIgnore(item.file)),
|
|
1126
|
+
...blockedPolicyViolations,
|
|
1127
|
+
];
|
|
1128
|
+
if (governance.audit.requireIntegrity && !auditIntegrityStatus.valid) {
|
|
1129
|
+
policyViolations.push({
|
|
1130
|
+
file: POLICY_AUDIT_FILE,
|
|
1131
|
+
rule: 'policy_audit_integrity',
|
|
1132
|
+
severity: 'block',
|
|
1133
|
+
message: `Policy audit chain is invalid: ${auditIntegrityStatus.issues.join('; ') || 'unknown issue'}`,
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
policyDecision = resolvePolicyDecisionFromViolations(policyViolations);
|
|
1137
|
+
const policyExceptionsSummary = {
|
|
1138
|
+
configured: configuredPolicyExceptions.length,
|
|
1139
|
+
active: exceptionDecision.activeExceptions.length,
|
|
1140
|
+
usable: exceptionDecision.usableExceptions.length,
|
|
1141
|
+
matched: exceptionDecision.matchedExceptionIds.length,
|
|
1142
|
+
suppressed: suppressedPolicyViolations.length,
|
|
1143
|
+
blocked: blockedPolicyViolations.length,
|
|
1144
|
+
matchedExceptionIds: exceptionDecision.matchedExceptionIds,
|
|
1145
|
+
suppressedViolations: suppressedPolicyViolations.map((item) => ({
|
|
1146
|
+
file: item.file,
|
|
1147
|
+
rule: item.rule,
|
|
1148
|
+
severity: item.severity,
|
|
1149
|
+
message: item.message,
|
|
1150
|
+
exceptionId: item.exceptionId,
|
|
1151
|
+
reason: item.reason,
|
|
1152
|
+
expiresAt: item.expiresAt,
|
|
1153
|
+
...(item.line != null ? { startLine: item.line } : {}),
|
|
1154
|
+
})),
|
|
1155
|
+
blockedViolations: blockedPolicyViolations.map((item) => ({
|
|
1156
|
+
file: item.file,
|
|
1157
|
+
rule: item.rule,
|
|
1158
|
+
severity: item.severity,
|
|
1159
|
+
message: item.message,
|
|
1160
|
+
...(item.line != null ? { startLine: item.line } : {}),
|
|
1161
|
+
})),
|
|
774
1162
|
};
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}
|
|
1163
|
+
const policyGovernanceSummary = {
|
|
1164
|
+
exceptionApprovals: governance.exceptionApprovals,
|
|
1165
|
+
audit: {
|
|
1166
|
+
requireIntegrity: governance.audit.requireIntegrity,
|
|
1167
|
+
valid: auditIntegrityStatus.valid,
|
|
1168
|
+
issues: auditIntegrityStatus.issues,
|
|
1169
|
+
lastHash: auditIntegrity.lastHash,
|
|
1170
|
+
eventCount: auditIntegrity.count,
|
|
1171
|
+
},
|
|
1172
|
+
};
|
|
1173
|
+
if (!options.json && effectiveRules.customRules.length > 0) {
|
|
1174
|
+
console.log(chalk.dim(` Evaluating ${effectiveRules.customRules.length} custom policy rule(s) from dashboard`));
|
|
787
1175
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
console.log(chalk.dim(' Could not load dashboard custom policies, continuing with local/default rules only'));
|
|
791
|
-
}
|
|
1176
|
+
if (!options.json && effectiveRules.policyPack && effectiveRules.policyPackRules.length > 0) {
|
|
1177
|
+
console.log(chalk.dim(` Evaluating policy pack: ${effectiveRules.policyPack.packName} (${effectiveRules.policyPack.packId}@${effectiveRules.policyPack.version}, ${effectiveRules.policyPackRules.length} rule(s))`));
|
|
792
1178
|
}
|
|
793
1179
|
// Prepare diff stats and changed files for API
|
|
794
1180
|
const diffStats = {
|
|
@@ -834,9 +1220,12 @@ async function verifyCommand(options) {
|
|
|
834
1220
|
// Apply custom policy verdict: block from dashboard overrides API verdict
|
|
835
1221
|
const policyBlock = policyDecision === 'block' && policyViolations.length > 0;
|
|
836
1222
|
const effectiveVerdict = policyBlock ? 'FAIL' : verifyResult.verdict;
|
|
837
|
-
const
|
|
1223
|
+
const policyMessageBase = policyBlock
|
|
838
1224
|
? `Custom policy violations: ${policyViolations.map(v => `${v.file}: ${v.message || v.rule}`).join('; ')}. ${verifyResult.message}`
|
|
839
1225
|
: verifyResult.message;
|
|
1226
|
+
const effectiveMessage = policyExceptionsSummary.suppressed > 0
|
|
1227
|
+
? `${policyMessageBase} Policy exceptions suppressed ${policyExceptionsSummary.suppressed} violation(s).`
|
|
1228
|
+
: policyMessageBase;
|
|
840
1229
|
// Calculate grade from effective verdict and score
|
|
841
1230
|
// CRITICAL: 0/0 planned files = 'F' (Incomplete), not 'B'
|
|
842
1231
|
// Bloat automatically drops grade by at least one letter
|
|
@@ -882,7 +1271,13 @@ async function verifyCommand(options) {
|
|
|
882
1271
|
const changedPathsForBrain = diffFiles
|
|
883
1272
|
.flatMap((file) => [file.path, file.oldPath])
|
|
884
1273
|
.filter((value) => Boolean(value));
|
|
885
|
-
recordVerifyEvent(effectiveVerdict, `adherence=${verifyResult.adherenceScore};bloat=${verifyResult.bloatCount};scopeGuard=${scopeGuardPassed ? 1 : 0};policy=${policyDecision}`, changedPathsForBrain, finalPlanId);
|
|
1274
|
+
recordVerifyEvent(effectiveVerdict, `adherence=${verifyResult.adherenceScore};bloat=${verifyResult.bloatCount};scopeGuard=${scopeGuardPassed ? 1 : 0};policy=${policyDecision};policyExceptions=${policyExceptionsSummary.suppressed}`, changedPathsForBrain, finalPlanId);
|
|
1275
|
+
const shouldForceGovernancePass = scopeGuardPassed &&
|
|
1276
|
+
!policyBlock &&
|
|
1277
|
+
(effectiveVerdict === 'PASS' ||
|
|
1278
|
+
((verifyResult.verdict === 'FAIL' || verifyResult.verdict === 'WARN') &&
|
|
1279
|
+
policyViolations.length === 0 &&
|
|
1280
|
+
verifyResult.bloatCount > 0));
|
|
886
1281
|
// If JSON output requested, output JSON and exit
|
|
887
1282
|
if (options.json) {
|
|
888
1283
|
const filteredBloatFiles = (verifyResult.bloatFiles || []).filter((f) => !shouldIgnore(f));
|
|
@@ -912,6 +1307,16 @@ async function verifyCommand(options) {
|
|
|
912
1307
|
bloatFiles: filteredBloatFiles,
|
|
913
1308
|
plannedFilesModified: verifyResult.plannedFilesModified,
|
|
914
1309
|
totalPlannedFiles: verifyResult.totalPlannedFiles,
|
|
1310
|
+
mode: 'plan_enforced',
|
|
1311
|
+
policyOnly: false,
|
|
1312
|
+
policyLock: {
|
|
1313
|
+
enforced: policyLockEvaluation.enforced,
|
|
1314
|
+
matched: policyLockEvaluation.matched,
|
|
1315
|
+
path: policyLockEvaluation.lockPath,
|
|
1316
|
+
mismatches: policyLockEvaluation.mismatches,
|
|
1317
|
+
},
|
|
1318
|
+
policyExceptions: policyExceptionsSummary,
|
|
1319
|
+
policyGovernance: policyGovernanceSummary,
|
|
915
1320
|
...(policyViolations.length > 0 && { policyDecision }),
|
|
916
1321
|
...(effectiveRules.policyPack
|
|
917
1322
|
? {
|
|
@@ -948,7 +1353,7 @@ async function verifyCommand(options) {
|
|
|
948
1353
|
});
|
|
949
1354
|
}
|
|
950
1355
|
// Exit based on effective verdict (same logic as below)
|
|
951
|
-
if (
|
|
1356
|
+
if (shouldForceGovernancePass) {
|
|
952
1357
|
process.exit(0);
|
|
953
1358
|
}
|
|
954
1359
|
if (effectiveVerdict === 'FAIL') {
|
|
@@ -971,6 +1376,25 @@ async function verifyCommand(options) {
|
|
|
971
1376
|
bloatFiles: displayBloatFiles,
|
|
972
1377
|
bloatCount: displayBloatFiles.length,
|
|
973
1378
|
}, policyViolations);
|
|
1379
|
+
if (policyExceptionsSummary.suppressed > 0) {
|
|
1380
|
+
console.log(chalk.yellow(`\n⚠️ Policy exceptions applied: ${policyExceptionsSummary.suppressed}`));
|
|
1381
|
+
if (policyExceptionsSummary.matchedExceptionIds.length > 0) {
|
|
1382
|
+
console.log(chalk.dim(` Exception IDs: ${policyExceptionsSummary.matchedExceptionIds.join(', ')}`));
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
if (policyExceptionsSummary.blocked > 0) {
|
|
1386
|
+
console.log(chalk.red(`\n⛔ Policy exceptions blocked by approval governance: ${policyExceptionsSummary.blocked}`));
|
|
1387
|
+
const sample = policyExceptionsSummary.blockedViolations.slice(0, 5);
|
|
1388
|
+
sample.forEach((item) => {
|
|
1389
|
+
console.log(chalk.red(` • ${item.file}: ${item.message || item.rule}`));
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
if (governance.audit.requireIntegrity && !auditIntegrityStatus.valid) {
|
|
1393
|
+
console.log(chalk.red('\n⛔ Policy audit integrity enforcement is enabled and chain verification failed.'));
|
|
1394
|
+
auditIntegrityStatus.issues.slice(0, 5).forEach((issue) => {
|
|
1395
|
+
console.log(chalk.red(` • ${issue}`));
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
974
1398
|
}
|
|
975
1399
|
// Report to Neurcode Cloud if --record flag is set
|
|
976
1400
|
if (options.record) {
|
|
@@ -1001,9 +1425,9 @@ async function verifyCommand(options) {
|
|
|
1001
1425
|
);
|
|
1002
1426
|
}
|
|
1003
1427
|
}
|
|
1004
|
-
// Governance
|
|
1005
|
-
//
|
|
1006
|
-
if (
|
|
1428
|
+
// Governance override: keep PASS only when scope guard passes and failure is due
|
|
1429
|
+
// to server-side bloat mismatch (allowed files unknown to verify API).
|
|
1430
|
+
if (shouldForceGovernancePass) {
|
|
1007
1431
|
if ((verifyResult.verdict === 'FAIL' || verifyResult.verdict === 'WARN') && policyViolations.length === 0) {
|
|
1008
1432
|
if (!options.json) {
|
|
1009
1433
|
console.log(chalk.yellow('\n⚠️ Plan deviation allowed'));
|
|
@@ -1011,6 +1435,9 @@ async function verifyCommand(options) {
|
|
|
1011
1435
|
console.log(chalk.dim(' Governance check passed - proceeding with exit code 0.\n'));
|
|
1012
1436
|
}
|
|
1013
1437
|
}
|
|
1438
|
+
if (!options.json && policyExceptionsSummary.suppressed > 0) {
|
|
1439
|
+
console.log(chalk.yellow(` Policy exceptions applied: ${policyExceptionsSummary.suppressed}`));
|
|
1440
|
+
}
|
|
1014
1441
|
process.exit(0);
|
|
1015
1442
|
}
|
|
1016
1443
|
// If scope guard didn't pass (or failed to check) or policy blocked, use effective verdict
|