@neurcode-ai/cli 0.9.29 → 0.9.31

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.
@@ -58,6 +58,8 @@ const custom_policy_rules_1 = require("../utils/custom-policy-rules");
58
58
  const policy_exceptions_1 = require("../utils/policy-exceptions");
59
59
  const policy_governance_1 = require("../utils/policy-governance");
60
60
  const policy_audit_1 = require("../utils/policy-audit");
61
+ const governance_1 = require("../utils/governance");
62
+ const policy_1 = require("@neurcode-ai/policy");
61
63
  // Import chalk with fallback
62
64
  let chalk;
63
65
  try {
@@ -290,14 +292,89 @@ function resolveAuditIntegrityStatus(requireIntegrity, auditIntegrity) {
290
292
  issues,
291
293
  };
292
294
  }
295
+ async function recordVerificationIfRequested(options, config, payload) {
296
+ if (!options.record) {
297
+ return;
298
+ }
299
+ if (!config.apiKey) {
300
+ if (!payload.jsonMode) {
301
+ console.log(chalk.yellow('\n⚠️ --record flag requires API key'));
302
+ console.log(chalk.dim(' Set NEURCODE_API_KEY environment variable or use --api-key flag'));
303
+ }
304
+ return;
305
+ }
306
+ await reportVerification(payload.grade, payload.violations, payload.verifyResult, config.apiKey, config.apiUrl || 'https://api.neurcode.com', payload.projectId, payload.jsonMode, payload.governance);
307
+ }
293
308
  /**
294
309
  * Execute policy-only verification (General Governance mode)
295
310
  * Returns the exit code to use
296
311
  */
297
- async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source) {
312
+ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source, projectId, orgGovernanceSettings, aiLogSigningKey, aiLogSigner) {
298
313
  if (!options.json) {
299
314
  console.log(chalk.cyan('🛡️ General Governance mode (policy only, no plan linked)\n'));
300
315
  }
316
+ const governanceAnalysis = (0, governance_1.evaluateGovernance)({
317
+ projectRoot,
318
+ task: 'Policy-only verification',
319
+ expectedFiles: [],
320
+ diffFiles,
321
+ contextCandidates: diffFiles.map((file) => file.path),
322
+ orgGovernance: orgGovernanceSettings,
323
+ signingKey: aiLogSigningKey,
324
+ signer: aiLogSigner,
325
+ });
326
+ const governancePayload = buildGovernancePayload(governanceAnalysis, orgGovernanceSettings);
327
+ const contextPolicyViolations = governanceAnalysis.contextPolicy.violations.filter((item) => !ignoreFilter(item.file));
328
+ if (contextPolicyViolations.length > 0) {
329
+ const message = `Context policy violation: ${contextPolicyViolations.map((item) => item.file).join(', ')}`;
330
+ const contextPolicyViolationItems = contextPolicyViolations.map((item) => ({
331
+ file: item.file,
332
+ rule: `context_policy:${item.rule}`,
333
+ severity: 'block',
334
+ message: item.reason,
335
+ }));
336
+ if (options.json) {
337
+ console.log(JSON.stringify({
338
+ grade: 'F',
339
+ score: 0,
340
+ verdict: 'FAIL',
341
+ violations: contextPolicyViolationItems,
342
+ message,
343
+ scopeGuardPassed: false,
344
+ bloatCount: 0,
345
+ bloatFiles: [],
346
+ plannedFilesModified: 0,
347
+ totalPlannedFiles: 0,
348
+ adherenceScore: 0,
349
+ mode: 'policy_only',
350
+ policyOnly: true,
351
+ policyOnlySource: source,
352
+ ...governancePayload,
353
+ }, null, 2));
354
+ }
355
+ else {
356
+ console.log(chalk.red('❌ Context policy violation detected (policy-only mode).'));
357
+ contextPolicyViolations.forEach((item) => {
358
+ console.log(chalk.red(` • ${item.file}: ${item.reason}`));
359
+ });
360
+ console.log(chalk.dim(`\n${message}`));
361
+ }
362
+ await recordVerificationIfRequested(options, config, {
363
+ grade: 'F',
364
+ violations: contextPolicyViolationItems,
365
+ verifyResult: {
366
+ adherenceScore: 0,
367
+ verdict: 'FAIL',
368
+ bloatCount: 0,
369
+ bloatFiles: [],
370
+ message,
371
+ },
372
+ projectId,
373
+ jsonMode: Boolean(options.json),
374
+ governance: governancePayload,
375
+ });
376
+ return 2;
377
+ }
301
378
  let policyViolations = [];
302
379
  let policyDecision = 'allow';
303
380
  const requirePolicyLock = options.requirePolicyLock === true || isEnabledFlag(process.env.NEURCODE_VERIFY_REQUIRE_POLICY_LOCK);
@@ -369,12 +446,13 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
369
446
  }
370
447
  if (policyLockEvaluation.enforced && !policyLockEvaluation.matched) {
371
448
  const message = policyLockMismatchMessage(policyLockEvaluation.mismatches);
449
+ const lockViolationItems = toPolicyLockViolations(policyLockEvaluation.mismatches);
372
450
  if (options.json) {
373
451
  console.log(JSON.stringify({
374
452
  grade: 'F',
375
453
  score: 0,
376
454
  verdict: 'FAIL',
377
- violations: toPolicyLockViolations(policyLockEvaluation.mismatches),
455
+ violations: lockViolationItems,
378
456
  message,
379
457
  scopeGuardPassed: true,
380
458
  bloatCount: 0,
@@ -385,6 +463,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
385
463
  mode: 'policy_only',
386
464
  policyOnly: true,
387
465
  policyOnlySource: source,
466
+ ...governancePayload,
388
467
  policyLock: {
389
468
  enforced: true,
390
469
  matched: false,
@@ -401,6 +480,20 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
401
480
  });
402
481
  console.log(chalk.dim('\n If drift is intentional, regenerate baseline with `neurcode policy lock`.\n'));
403
482
  }
483
+ await recordVerificationIfRequested(options, config, {
484
+ grade: 'F',
485
+ violations: lockViolationItems,
486
+ verifyResult: {
487
+ adherenceScore: 0,
488
+ verdict: 'FAIL',
489
+ bloatCount: 0,
490
+ bloatFiles: [],
491
+ message,
492
+ },
493
+ projectId,
494
+ jsonMode: Boolean(options.json),
495
+ governance: governancePayload,
496
+ });
404
497
  return 2;
405
498
  }
406
499
  if (!options.json && effectiveRules.customRules.length > 0) {
@@ -516,6 +609,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
516
609
  mode: 'policy_only',
517
610
  policyOnly: true,
518
611
  policyOnlySource: source,
612
+ ...governancePayload,
519
613
  policyLock: {
520
614
  enforced: policyLockEvaluation.enforced,
521
615
  matched: policyLockEvaluation.matched,
@@ -555,8 +649,23 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
555
649
  if (governance.audit.requireIntegrity && !auditIntegrityStatus.valid) {
556
650
  console.log(chalk.red(' Policy audit integrity check failed'));
557
651
  }
652
+ displayGovernanceInsights(governanceAnalysis, { explain: options.explain });
558
653
  console.log(chalk.dim(`\n${message}`));
559
654
  }
655
+ await recordVerificationIfRequested(options, config, {
656
+ grade,
657
+ violations: violationsOutput,
658
+ verifyResult: {
659
+ adherenceScore: score,
660
+ verdict: effectiveVerdict,
661
+ bloatCount: 0,
662
+ bloatFiles: [],
663
+ message,
664
+ },
665
+ projectId,
666
+ jsonMode: Boolean(options.json),
667
+ governance: governancePayload,
668
+ });
560
669
  return effectiveVerdict === 'FAIL' ? 2 : effectiveVerdict === 'WARN' ? 1 : 0;
561
670
  }
562
671
  async function verifyCommand(options) {
@@ -610,6 +719,31 @@ async function verifyCommand(options) {
610
719
  orgId: (0, state_1.getOrgId)(),
611
720
  projectId: projectId || null,
612
721
  };
722
+ const aiLogSigningKey = process.env.NEURCODE_GOVERNANCE_SIGNING_KEY ||
723
+ process.env.NEURCODE_AI_LOG_SIGNING_KEY ||
724
+ null;
725
+ const aiLogSigner = process.env.NEURCODE_GOVERNANCE_SIGNER || process.env.USER || 'neurcode-cli';
726
+ let orgGovernanceSettings = null;
727
+ if (config.apiKey) {
728
+ try {
729
+ const remoteSettings = await client.getOrgGovernanceSettings();
730
+ if (remoteSettings) {
731
+ orgGovernanceSettings = {
732
+ contextPolicy: (0, policy_1.normalizeContextPolicy)(remoteSettings.contextPolicy),
733
+ requireSignedAiLogs: remoteSettings.requireSignedAiLogs === true,
734
+ requireManualApproval: remoteSettings.requireManualApproval !== false,
735
+ minimumManualApprovals: Math.max(1, Math.min(5, Math.floor(remoteSettings.minimumManualApprovals || 1))),
736
+ updatedAt: remoteSettings.updatedAt,
737
+ };
738
+ }
739
+ }
740
+ catch (error) {
741
+ if (!options.json) {
742
+ const message = error instanceof Error ? error.message : 'Unknown error';
743
+ console.log(chalk.dim(` Org governance settings unavailable, using local policy only (${message})`));
744
+ }
745
+ }
746
+ }
613
747
  const recordVerifyEvent = (verdict, note, changedFiles, planId) => {
614
748
  if (!brainScope.orgId || !brainScope.projectId) {
615
749
  return;
@@ -727,13 +861,81 @@ async function verifyCommand(options) {
727
861
  const normalized = toUnixPath(filePath || '');
728
862
  return ignoreFilter(normalized) || runtimeIgnoreSet.has(normalized);
729
863
  };
864
+ const baselineContextPolicyLocal = (0, policy_1.loadContextPolicy)(projectRoot);
865
+ const baselineContextPolicy = orgGovernanceSettings?.contextPolicy
866
+ ? (0, policy_1.mergeContextPolicies)(baselineContextPolicyLocal, orgGovernanceSettings.contextPolicy)
867
+ : baselineContextPolicyLocal;
868
+ const baselineContextPolicyEvaluation = (0, policy_1.evaluateContextPolicyForChanges)(diffFiles.map((file) => file.path), baselineContextPolicy, diffFiles.map((file) => file.path));
869
+ const baselineContextViolations = baselineContextPolicyEvaluation.violations.filter((item) => !shouldIgnore(item.file));
870
+ if (baselineContextViolations.length > 0) {
871
+ const baselineGovernance = (0, governance_1.evaluateGovernance)({
872
+ projectRoot,
873
+ task: 'Context policy validation',
874
+ expectedFiles: [],
875
+ diffFiles,
876
+ contextCandidates: diffFiles.map((file) => file.path),
877
+ orgGovernance: orgGovernanceSettings,
878
+ signingKey: aiLogSigningKey,
879
+ signer: aiLogSigner,
880
+ });
881
+ const baselineGovernancePayload = buildGovernancePayload(baselineGovernance, orgGovernanceSettings);
882
+ const message = `Context access policy violation: ${baselineContextViolations.map((item) => item.file).join(', ')}`;
883
+ const baselineContextViolationItems = baselineContextViolations.map((item) => ({
884
+ file: item.file,
885
+ rule: `context_policy:${item.rule}`,
886
+ severity: 'block',
887
+ message: item.reason,
888
+ }));
889
+ recordVerifyEvent('FAIL', `context_policy_violations=${baselineContextViolations.length}`, diffFiles.map((f) => f.path));
890
+ if (options.json) {
891
+ console.log(JSON.stringify({
892
+ grade: 'F',
893
+ score: 0,
894
+ verdict: 'FAIL',
895
+ violations: baselineContextViolationItems,
896
+ adherenceScore: 0,
897
+ bloatCount: 0,
898
+ bloatFiles: [],
899
+ plannedFilesModified: 0,
900
+ totalPlannedFiles: 0,
901
+ message,
902
+ scopeGuardPassed: false,
903
+ ...baselineGovernancePayload,
904
+ mode: 'policy_violation',
905
+ policyOnly: false,
906
+ }, null, 2));
907
+ }
908
+ else {
909
+ console.log(chalk.red('\n⛔ Context Policy Violation'));
910
+ baselineContextViolations.forEach((item) => {
911
+ console.log(chalk.red(` • ${item.file}: ${item.reason}`));
912
+ });
913
+ console.log(chalk.dim(`\nRisk level: ${baselineGovernance.blastRadius.riskScore.toUpperCase()}`));
914
+ console.log(chalk.red('\nAction blocked.\n'));
915
+ }
916
+ await recordVerificationIfRequested(options, config, {
917
+ grade: 'F',
918
+ violations: baselineContextViolationItems,
919
+ verifyResult: {
920
+ adherenceScore: 0,
921
+ verdict: 'FAIL',
922
+ bloatCount: 0,
923
+ bloatFiles: [],
924
+ message,
925
+ },
926
+ projectId: projectId || undefined,
927
+ jsonMode: Boolean(options.json),
928
+ governance: baselineGovernancePayload,
929
+ });
930
+ process.exit(2);
931
+ }
730
932
  if (!options.json) {
731
933
  console.log(chalk.cyan('\n📊 Analyzing changes against plan...'));
732
934
  console.log(chalk.dim(` Found ${summary.totalFiles} file(s) changed`));
733
935
  console.log(chalk.dim(` ${summary.totalAdded} lines added, ${summary.totalRemoved} lines removed\n`));
734
936
  }
735
937
  const runPolicyOnlyModeAndExit = async (source) => {
736
- const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client, source);
938
+ const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client, source, projectId || undefined, orgGovernanceSettings, aiLogSigningKey, aiLogSigner);
737
939
  const changedFiles = diffFiles.map((f) => f.path);
738
940
  const verdict = exitCode === 2 ? 'FAIL' : exitCode === 1 ? 'WARN' : 'PASS';
739
941
  recordVerifyEvent(verdict, `policy_only_source=${source};exit=${exitCode}`, changedFiles);
@@ -782,6 +984,7 @@ async function verifyCommand(options) {
782
984
  if (!planId) {
783
985
  if (requirePlan) {
784
986
  const changedFiles = diffFiles.map((f) => f.path);
987
+ const message = 'Plan ID is required in strict mode. Run "neurcode plan" first or pass --plan-id.';
785
988
  recordVerifyEvent('FAIL', 'missing_plan_id;require_plan=true', changedFiles);
786
989
  if (options.json) {
787
990
  console.log(JSON.stringify({
@@ -794,7 +997,7 @@ async function verifyCommand(options) {
794
997
  bloatFiles: [],
795
998
  plannedFilesModified: 0,
796
999
  totalPlannedFiles: 0,
797
- message: 'Plan ID is required in strict mode. Run "neurcode plan" first or pass --plan-id.',
1000
+ message,
798
1001
  scopeGuardPassed: false,
799
1002
  mode: 'plan_required',
800
1003
  policyOnly: false,
@@ -805,6 +1008,19 @@ async function verifyCommand(options) {
805
1008
  console.log(chalk.dim(' Run "neurcode plan" first or pass --plan-id <id>.'));
806
1009
  console.log(chalk.dim(' Use --policy-only only when intentionally running general governance checks.'));
807
1010
  }
1011
+ await recordVerificationIfRequested(options, config, {
1012
+ grade: 'F',
1013
+ violations: [],
1014
+ verifyResult: {
1015
+ adherenceScore: 0,
1016
+ verdict: 'FAIL',
1017
+ bloatCount: 0,
1018
+ bloatFiles: [],
1019
+ message,
1020
+ },
1021
+ projectId: projectId || undefined,
1022
+ jsonMode: Boolean(options.json),
1023
+ });
808
1024
  process.exit(1);
809
1025
  }
810
1026
  if (!options.json) {
@@ -824,6 +1040,7 @@ async function verifyCommand(options) {
824
1040
  }
825
1041
  // Track if scope guard passed - this takes priority over AI grading
826
1042
  let scopeGuardPassed = false;
1043
+ let governanceResult = null;
827
1044
  try {
828
1045
  // Step A: Get Modified Files (already have from diffFiles)
829
1046
  const modifiedFiles = diffFiles.map(f => f.path);
@@ -831,10 +1048,29 @@ async function verifyCommand(options) {
831
1048
  const planData = await client.getPlan(finalPlanId);
832
1049
  // Extract original intent from plan (for constraint checking)
833
1050
  const originalIntent = planData.intent || '';
1051
+ const planTitle = typeof planData.content.title === 'string'
1052
+ ? planData.content.title?.trim()
1053
+ : '';
1054
+ const planSummary = typeof planData.content.summary === 'string' ? planData.content.summary.trim() : '';
1055
+ const governanceTask = planTitle || planSummary || originalIntent || 'Plan verification';
834
1056
  // Get approved files from plan (only files with action CREATE or MODIFY)
835
1057
  const planFiles = planData.content.files
836
1058
  .filter(f => f.action === 'CREATE' || f.action === 'MODIFY')
837
1059
  .map(f => f.path);
1060
+ const planDependencies = Array.isArray(planData.content.dependencies)
1061
+ ? planData.content.dependencies.filter((item) => typeof item === 'string')
1062
+ : [];
1063
+ governanceResult = (0, governance_1.evaluateGovernance)({
1064
+ projectRoot,
1065
+ task: governanceTask,
1066
+ expectedFiles: planFiles,
1067
+ expectedDependencies: planDependencies,
1068
+ diffFiles,
1069
+ contextCandidates: planFiles,
1070
+ orgGovernance: orgGovernanceSettings,
1071
+ signingKey: aiLogSigningKey,
1072
+ signer: aiLogSigner,
1073
+ });
838
1074
  // Get sessionId from state file (.neurcode/state.json) first, then fallback to config
839
1075
  // Fallback to sessionId from plan if not in state/config
840
1076
  // This is the session_id string needed to fetch the session
@@ -881,31 +1117,51 @@ async function verifyCommand(options) {
881
1117
  // Step D: The Block (only report scope violations for non-ignored files)
882
1118
  if (filteredViolations.length > 0) {
883
1119
  recordVerifyEvent('FAIL', `scope_violation=${filteredViolations.length}`, modifiedFiles, finalPlanId);
1120
+ const scopeViolationItems = filteredViolations.map((file) => ({
1121
+ file,
1122
+ rule: 'scope_guard',
1123
+ severity: 'block',
1124
+ message: 'File modified outside the plan',
1125
+ }));
1126
+ const scopeViolationMessage = `Scope violation: ${filteredViolations.length} file(s) modified outside the plan`;
884
1127
  if (options.json) {
885
1128
  // Output JSON for scope violation BEFORE exit. Must include violations for GitHub Action annotations.
886
- const violationsOutput = filteredViolations.map((file) => ({
887
- file,
888
- rule: 'scope_guard',
889
- severity: 'block',
890
- message: 'File modified outside the plan',
891
- }));
892
1129
  const jsonOutput = {
893
1130
  grade: 'F',
894
1131
  score: 0,
895
1132
  verdict: 'FAIL',
896
- violations: violationsOutput,
1133
+ violations: scopeViolationItems,
897
1134
  adherenceScore: 0,
898
1135
  bloatCount: filteredViolations.length,
899
1136
  bloatFiles: filteredViolations,
900
1137
  plannedFilesModified: 0,
901
1138
  totalPlannedFiles: planFiles.length,
902
- message: `Scope violation: ${filteredViolations.length} file(s) modified outside the plan`,
1139
+ message: scopeViolationMessage,
903
1140
  scopeGuardPassed: false,
904
1141
  mode: 'plan_enforced',
905
1142
  policyOnly: false,
1143
+ ...(governanceResult
1144
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1145
+ : {}),
906
1146
  };
907
1147
  // CRITICAL: Print JSON first, then exit
908
1148
  console.log(JSON.stringify(jsonOutput, null, 2));
1149
+ await recordVerificationIfRequested(options, config, {
1150
+ grade: 'F',
1151
+ violations: scopeViolationItems,
1152
+ verifyResult: {
1153
+ adherenceScore: 0,
1154
+ verdict: 'FAIL',
1155
+ bloatCount: filteredViolations.length,
1156
+ bloatFiles: filteredViolations,
1157
+ message: scopeViolationMessage,
1158
+ },
1159
+ projectId: projectId || undefined,
1160
+ jsonMode: true,
1161
+ governance: governanceResult
1162
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1163
+ : undefined,
1164
+ });
909
1165
  process.exit(1);
910
1166
  }
911
1167
  else {
@@ -922,7 +1178,26 @@ async function verifyCommand(options) {
922
1178
  filteredViolations.forEach(file => {
923
1179
  console.log(chalk.dim(` neurcode allow ${file}`));
924
1180
  });
1181
+ if (governanceResult) {
1182
+ displayGovernanceInsights(governanceResult, { explain: options.explain });
1183
+ }
925
1184
  console.log('');
1185
+ await recordVerificationIfRequested(options, config, {
1186
+ grade: 'F',
1187
+ violations: scopeViolationItems,
1188
+ verifyResult: {
1189
+ adherenceScore: 0,
1190
+ verdict: 'FAIL',
1191
+ bloatCount: filteredViolations.length,
1192
+ bloatFiles: filteredViolations,
1193
+ message: scopeViolationMessage,
1194
+ },
1195
+ projectId: projectId || undefined,
1196
+ jsonMode: false,
1197
+ governance: governanceResult
1198
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1199
+ : undefined,
1200
+ });
926
1201
  process.exit(1);
927
1202
  }
928
1203
  }
@@ -1010,13 +1285,14 @@ async function verifyCommand(options) {
1010
1285
  }
1011
1286
  if (policyLockEvaluation.enforced && !policyLockEvaluation.matched) {
1012
1287
  const message = policyLockMismatchMessage(policyLockEvaluation.mismatches);
1288
+ const lockViolationItems = toPolicyLockViolations(policyLockEvaluation.mismatches);
1013
1289
  recordVerifyEvent('FAIL', 'policy_lock_mismatch', diffFiles.map((f) => f.path), finalPlanId);
1014
1290
  if (options.json) {
1015
1291
  console.log(JSON.stringify({
1016
1292
  grade: 'F',
1017
1293
  score: 0,
1018
1294
  verdict: 'FAIL',
1019
- violations: toPolicyLockViolations(policyLockEvaluation.mismatches),
1295
+ violations: lockViolationItems,
1020
1296
  adherenceScore: 0,
1021
1297
  bloatCount: 0,
1022
1298
  bloatFiles: [],
@@ -1026,6 +1302,9 @@ async function verifyCommand(options) {
1026
1302
  scopeGuardPassed,
1027
1303
  mode: 'plan_enforced',
1028
1304
  policyOnly: false,
1305
+ ...(governanceResult
1306
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1307
+ : {}),
1029
1308
  policyLock: {
1030
1309
  enforced: true,
1031
1310
  matched: false,
@@ -1042,6 +1321,22 @@ async function verifyCommand(options) {
1042
1321
  });
1043
1322
  console.log(chalk.dim('\n If this drift is intentional, regenerate baseline with `neurcode policy lock`.\n'));
1044
1323
  }
1324
+ await recordVerificationIfRequested(options, config, {
1325
+ grade: 'F',
1326
+ violations: lockViolationItems,
1327
+ verifyResult: {
1328
+ adherenceScore: 0,
1329
+ verdict: 'FAIL',
1330
+ bloatCount: 0,
1331
+ bloatFiles: [],
1332
+ message,
1333
+ },
1334
+ projectId: projectId || undefined,
1335
+ jsonMode: Boolean(options.json),
1336
+ governance: governanceResult
1337
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1338
+ : undefined,
1339
+ });
1045
1340
  process.exit(2);
1046
1341
  }
1047
1342
  }
@@ -1079,6 +1374,9 @@ async function verifyCommand(options) {
1079
1374
  mode: 'plan_enforced',
1080
1375
  policyOnly: false,
1081
1376
  tier: 'FREE',
1377
+ ...(governanceResult
1378
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1379
+ : {}),
1082
1380
  policyLock: {
1083
1381
  enforced: policyLockEvaluation.enforced,
1084
1382
  matched: policyLockEvaluation.matched,
@@ -1309,6 +1607,9 @@ async function verifyCommand(options) {
1309
1607
  totalPlannedFiles: verifyResult.totalPlannedFiles,
1310
1608
  mode: 'plan_enforced',
1311
1609
  policyOnly: false,
1610
+ ...(governanceResult
1611
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1612
+ : {}),
1312
1613
  policyLock: {
1313
1614
  enforced: policyLockEvaluation.enforced,
1314
1615
  matched: policyLockEvaluation.matched,
@@ -1330,28 +1631,22 @@ async function verifyCommand(options) {
1330
1631
  : {}),
1331
1632
  };
1332
1633
  console.log(JSON.stringify(jsonOutput, null, 2));
1333
- // Report to Neurcode Cloud if --record flag is set (after JSON output)
1334
- if (options.record && config.apiKey) {
1335
- const violations = [
1336
- ...filteredBloatFiles.map((file) => ({
1337
- rule: 'scope_guard',
1338
- file: file,
1339
- severity: 'block',
1340
- message: 'File modified outside the plan',
1341
- })),
1342
- ...policyViolations.map(v => ({
1343
- rule: v.rule,
1344
- file: v.file,
1345
- severity: v.severity,
1346
- message: v.message,
1347
- })),
1348
- ];
1349
- // Report in background (don't await to avoid blocking JSON output)
1350
- reportVerification(grade, violations, verifyResult, config.apiKey, config.apiUrl, projectId || undefined, true // jsonMode = true
1351
- ).catch(() => {
1352
- // Error already logged in reportVerification
1353
- });
1354
- }
1634
+ await recordVerificationIfRequested(options, config, {
1635
+ grade,
1636
+ violations: violations,
1637
+ verifyResult: {
1638
+ adherenceScore: verifyResult.adherenceScore,
1639
+ verdict: effectiveVerdict,
1640
+ bloatCount: filteredBloatFiles.length,
1641
+ bloatFiles: filteredBloatFiles,
1642
+ message: effectiveMessage,
1643
+ },
1644
+ projectId: projectId || undefined,
1645
+ jsonMode: true,
1646
+ governance: governanceResult
1647
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1648
+ : undefined,
1649
+ });
1355
1650
  // Exit based on effective verdict (same logic as below)
1356
1651
  if (shouldForceGovernancePass) {
1357
1652
  process.exit(0);
@@ -1376,6 +1671,9 @@ async function verifyCommand(options) {
1376
1671
  bloatFiles: displayBloatFiles,
1377
1672
  bloatCount: displayBloatFiles.length,
1378
1673
  }, policyViolations);
1674
+ if (governanceResult) {
1675
+ displayGovernanceInsights(governanceResult, { explain: options.explain });
1676
+ }
1379
1677
  if (policyExceptionsSummary.suppressed > 0) {
1380
1678
  console.log(chalk.yellow(`\n⚠️ Policy exceptions applied: ${policyExceptionsSummary.suppressed}`));
1381
1679
  if (policyExceptionsSummary.matchedExceptionIds.length > 0) {
@@ -1397,34 +1695,37 @@ async function verifyCommand(options) {
1397
1695
  }
1398
1696
  }
1399
1697
  // Report to Neurcode Cloud if --record flag is set
1400
- if (options.record) {
1401
- if (!config.apiKey) {
1402
- if (!options.json) {
1403
- console.log(chalk.yellow('\n⚠️ --record flag requires API key'));
1404
- console.log(chalk.dim(' Set NEURCODE_API_KEY environment variable or use --api-key flag'));
1405
- }
1406
- }
1407
- else {
1408
- // Include scope bloat and custom policy violations in the report (excluding .neurcodeignore'd paths)
1409
- const filteredBloatForReport = (verifyResult.bloatFiles || []).filter((f) => !shouldIgnore(f));
1410
- const violations = [
1411
- ...filteredBloatForReport.map((file) => ({
1412
- rule: 'scope_guard',
1413
- file: file,
1414
- severity: 'block',
1415
- message: 'File modified outside the plan',
1416
- })),
1417
- ...policyViolations.map(v => ({
1418
- rule: v.rule,
1419
- file: v.file,
1420
- severity: v.severity,
1421
- message: v.message,
1422
- })),
1423
- ];
1424
- await reportVerification(grade, violations, verifyResult, config.apiKey, config.apiUrl, projectId || undefined, false // jsonMode = false
1425
- );
1426
- }
1427
- }
1698
+ const filteredBloatForReport = (verifyResult.bloatFiles || []).filter((f) => !shouldIgnore(f));
1699
+ const reportViolations = [
1700
+ ...filteredBloatForReport.map((file) => ({
1701
+ rule: 'scope_guard',
1702
+ file: file,
1703
+ severity: 'block',
1704
+ message: 'File modified outside the plan',
1705
+ })),
1706
+ ...policyViolations.map((v) => ({
1707
+ rule: v.rule,
1708
+ file: v.file,
1709
+ severity: v.severity,
1710
+ message: v.message,
1711
+ })),
1712
+ ];
1713
+ await recordVerificationIfRequested(options, config, {
1714
+ grade,
1715
+ violations: reportViolations,
1716
+ verifyResult: {
1717
+ adherenceScore: verifyResult.adherenceScore,
1718
+ verdict: effectiveVerdict,
1719
+ bloatCount: filteredBloatForReport.length,
1720
+ bloatFiles: filteredBloatForReport,
1721
+ message: effectiveMessage,
1722
+ },
1723
+ projectId: projectId || undefined,
1724
+ jsonMode: false,
1725
+ governance: governanceResult
1726
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1727
+ : undefined,
1728
+ });
1428
1729
  // Governance override: keep PASS only when scope guard passes and failure is due
1429
1730
  // to server-side bloat mismatch (allowed files unknown to verify API).
1430
1731
  if (shouldForceGovernancePass) {
@@ -1610,10 +1911,67 @@ function collectCIContext() {
1610
1911
  }
1611
1912
  return context;
1612
1913
  }
1914
+ const REPORT_MAX_ARRAY_ITEMS = 120;
1915
+ const REPORT_MAX_STRING_LENGTH = 4000;
1916
+ const REPORT_MAX_OBJECT_DEPTH = 6;
1917
+ function compactReportValue(value, depth = 0, seen = new WeakSet()) {
1918
+ if (value == null) {
1919
+ return value;
1920
+ }
1921
+ if (typeof value === 'string') {
1922
+ return value.length > REPORT_MAX_STRING_LENGTH
1923
+ ? `${value.slice(0, REPORT_MAX_STRING_LENGTH)}...[truncated]`
1924
+ : value;
1925
+ }
1926
+ if (typeof value !== 'object') {
1927
+ return value;
1928
+ }
1929
+ if (depth >= REPORT_MAX_OBJECT_DEPTH) {
1930
+ return '[truncated]';
1931
+ }
1932
+ if (Array.isArray(value)) {
1933
+ const items = value.slice(0, REPORT_MAX_ARRAY_ITEMS).map((item) => compactReportValue(item, depth + 1, seen));
1934
+ if (value.length > REPORT_MAX_ARRAY_ITEMS) {
1935
+ items.push(`[truncated ${value.length - REPORT_MAX_ARRAY_ITEMS} item(s)]`);
1936
+ }
1937
+ return items;
1938
+ }
1939
+ if (seen.has(value)) {
1940
+ return '[circular]';
1941
+ }
1942
+ seen.add(value);
1943
+ const compacted = {};
1944
+ for (const [key, nestedValue] of Object.entries(value)) {
1945
+ compacted[key] = compactReportValue(nestedValue, depth + 1, seen);
1946
+ }
1947
+ return compacted;
1948
+ }
1949
+ function buildCompactVerificationPayload(payload) {
1950
+ const compactViolations = payload.violations.slice(0, REPORT_MAX_ARRAY_ITEMS);
1951
+ if (payload.violations.length > REPORT_MAX_ARRAY_ITEMS) {
1952
+ compactViolations.push({
1953
+ rule: 'report_payload_compaction',
1954
+ file: '__meta__',
1955
+ severity: 'warn',
1956
+ message: `Truncated ${payload.violations.length - REPORT_MAX_ARRAY_ITEMS} additional violation(s) for upload`,
1957
+ });
1958
+ }
1959
+ return {
1960
+ ...payload,
1961
+ violations: compactViolations,
1962
+ bloatFiles: payload.bloatFiles.slice(0, REPORT_MAX_ARRAY_ITEMS),
1963
+ message: payload.message.length > REPORT_MAX_STRING_LENGTH
1964
+ ? `${payload.message.slice(0, REPORT_MAX_STRING_LENGTH)}...[truncated]`
1965
+ : payload.message,
1966
+ governance: payload.governance
1967
+ ? compactReportValue(payload.governance)
1968
+ : undefined,
1969
+ };
1970
+ }
1613
1971
  /**
1614
1972
  * Report verification results to Neurcode Cloud
1615
1973
  */
1616
- async function reportVerification(grade, violations, verifyResult, apiKey, apiUrl, projectId, jsonMode) {
1974
+ async function reportVerification(grade, violations, verifyResult, apiKey, apiUrl, projectId, jsonMode, governance) {
1617
1975
  try {
1618
1976
  const ciContext = collectCIContext();
1619
1977
  const payload = {
@@ -1629,23 +1987,38 @@ async function reportVerification(grade, violations, verifyResult, apiKey, apiUr
1629
1987
  branch: ciContext.branch,
1630
1988
  workflowRunId: ciContext.workflowRunId,
1631
1989
  projectId,
1990
+ governance,
1632
1991
  };
1633
- const response = await fetch(`${apiUrl}/api/v1/action/verifications`, {
1992
+ const postPayload = async (requestPayload) => fetch(`${apiUrl}/api/v1/action/verifications`, {
1634
1993
  method: 'POST',
1635
1994
  headers: {
1636
1995
  'Content-Type': 'application/json',
1637
1996
  'Authorization': `Bearer ${apiKey}`,
1638
1997
  },
1639
- body: JSON.stringify(payload),
1998
+ body: JSON.stringify(requestPayload),
1640
1999
  });
2000
+ let response = await postPayload(payload);
2001
+ let compactedUpload = false;
2002
+ if (response.status === 413) {
2003
+ response = await postPayload(buildCompactVerificationPayload(payload));
2004
+ compactedUpload = true;
2005
+ }
1641
2006
  if (!response.ok) {
1642
2007
  const errorText = await response.text();
1643
- throw new Error(`HTTP ${response.status}: ${errorText}`);
2008
+ const compactError = errorText.replace(/\s+/g, ' ').trim().slice(0, 400);
2009
+ throw new Error(`HTTP ${response.status}: ${compactError}`);
2010
+ }
2011
+ let result = {};
2012
+ try {
2013
+ result = (await response.json());
2014
+ }
2015
+ catch {
2016
+ // Some proxies may return empty success bodies; treat as recorded.
1644
2017
  }
1645
- const result = await response.json();
1646
2018
  // Only log if not in json mode to avoid polluting stdout
1647
2019
  if (!jsonMode) {
1648
- console.log(chalk.dim(`\n✅ Verification result reported to Neurcode Cloud (ID: ${result.id})`));
2020
+ const suffix = compactedUpload ? ' (compact payload)' : '';
2021
+ console.log(chalk.dim(`\n✅ Verification result reported to Neurcode Cloud (ID: ${result.id || 'ok'})${suffix}`));
1649
2022
  }
1650
2023
  }
1651
2024
  catch (error) {
@@ -1657,6 +2030,68 @@ async function reportVerification(grade, violations, verifyResult, apiKey, apiUr
1657
2030
  }
1658
2031
  }
1659
2032
  }
2033
+ function buildGovernancePayload(governance, orgGovernanceSettings) {
2034
+ return {
2035
+ contextPolicy: governance.contextPolicy,
2036
+ blastRadius: governance.blastRadius,
2037
+ suspiciousChange: governance.suspiciousChange,
2038
+ changeJustification: governance.changeJustification,
2039
+ governanceDecision: governance.governanceDecision,
2040
+ aiChangeLog: {
2041
+ path: governance.aiChangeLogPath,
2042
+ auditPath: governance.aiChangeLogAuditPath,
2043
+ integrity: governance.aiChangeLogIntegrity,
2044
+ },
2045
+ policySources: governance.policySources,
2046
+ orgGovernance: orgGovernanceSettings
2047
+ ? {
2048
+ requireSignedAiLogs: orgGovernanceSettings.requireSignedAiLogs,
2049
+ requireManualApproval: orgGovernanceSettings.requireManualApproval,
2050
+ minimumManualApprovals: orgGovernanceSettings.minimumManualApprovals,
2051
+ updatedAt: orgGovernanceSettings.updatedAt || null,
2052
+ }
2053
+ : null,
2054
+ };
2055
+ }
2056
+ function displayGovernanceInsights(governance, options = {}) {
2057
+ const maxUnexpectedFiles = options.maxUnexpectedFiles ?? 20;
2058
+ const decision = governance.governanceDecision;
2059
+ console.log(chalk.bold.white('\nBlast Radius:'));
2060
+ console.log(chalk.dim(` Files touched: ${governance.blastRadius.filesChanged}`));
2061
+ console.log(chalk.dim(` Functions impacted: ${governance.blastRadius.functionsAffected}`));
2062
+ console.log(chalk.dim(` Modules impacted: ${governance.blastRadius.modulesAffected.join(', ') || 'none'}`));
2063
+ if (governance.blastRadius.dependenciesAdded.length > 0) {
2064
+ console.log(chalk.dim(` Dependencies added: ${governance.blastRadius.dependenciesAdded.join(', ')}`));
2065
+ }
2066
+ console.log(chalk.dim(` Risk level: ${governance.blastRadius.riskScore.toUpperCase()}`));
2067
+ console.log(chalk.dim(` Governance decision: ${decision.decision.toUpperCase().replace('_', ' ')} | Avg relevance: ${decision.averageRelevanceScore}`));
2068
+ console.log(chalk.dim(` Policy source: ${governance.policySources.mode}${governance.policySources.orgPolicy ? ' (org + local)' : ' (local)'}`));
2069
+ console.log(governance.aiChangeLogIntegrity.valid
2070
+ ? chalk.dim(` AI change-log integrity: valid (${governance.aiChangeLogIntegrity.signed ? 'signed' : 'unsigned'})`)
2071
+ : chalk.red(` AI change-log integrity: invalid (${governance.aiChangeLogIntegrity.issues.join('; ') || 'unknown'})`));
2072
+ if (governance.suspiciousChange.flagged) {
2073
+ console.log(chalk.red('\nSuspicious Change Detected'));
2074
+ console.log(chalk.red(` Plan expected files: ${governance.suspiciousChange.expectedFiles} | AI modified files: ${governance.suspiciousChange.actualFiles}`));
2075
+ governance.suspiciousChange.unexpectedFiles.slice(0, maxUnexpectedFiles).forEach((filePath) => {
2076
+ console.log(chalk.red(` • ${filePath}`));
2077
+ });
2078
+ console.log(chalk.red(` Confidence: ${governance.suspiciousChange.confidence}`));
2079
+ }
2080
+ if (decision.lowRelevanceFiles.length > 0) {
2081
+ console.log(chalk.yellow('\nLow Relevance Files'));
2082
+ decision.lowRelevanceFiles.slice(0, 10).forEach((item) => {
2083
+ console.log(chalk.yellow(` • ${item.file} (score ${item.relevanceScore}, ${item.planLink.replace('_', ' ')})`));
2084
+ });
2085
+ }
2086
+ if (options.explain) {
2087
+ console.log(chalk.bold.white('\nAI Change Justification:'));
2088
+ console.log(chalk.dim(` Task: ${governance.changeJustification.task}`));
2089
+ governance.changeJustification.changes.forEach((item) => {
2090
+ const relevance = typeof item.relevanceScore === 'number' ? ` [score ${item.relevanceScore}]` : '';
2091
+ console.log(chalk.dim(` • ${item.file} — ${item.reason}${relevance}`));
2092
+ });
2093
+ }
2094
+ }
1660
2095
  /**
1661
2096
  * Display verification results in a formatted report card
1662
2097
  */