@neurcode-ai/cli 0.9.30 → 0.9.32

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.
Files changed (54) hide show
  1. package/README.md +22 -0
  2. package/dist/api-client.d.ts.map +1 -1
  3. package/dist/api-client.js +24 -8
  4. package/dist/api-client.js.map +1 -1
  5. package/dist/commands/apply.d.ts.map +1 -1
  6. package/dist/commands/apply.js +45 -3
  7. package/dist/commands/apply.js.map +1 -1
  8. package/dist/commands/map.d.ts.map +1 -1
  9. package/dist/commands/map.js +78 -1
  10. package/dist/commands/map.js.map +1 -1
  11. package/dist/commands/plan-slo.d.ts +7 -0
  12. package/dist/commands/plan-slo.d.ts.map +1 -0
  13. package/dist/commands/plan-slo.js +205 -0
  14. package/dist/commands/plan-slo.js.map +1 -0
  15. package/dist/commands/plan.d.ts.map +1 -1
  16. package/dist/commands/plan.js +665 -29
  17. package/dist/commands/plan.js.map +1 -1
  18. package/dist/commands/repo.d.ts +3 -0
  19. package/dist/commands/repo.d.ts.map +1 -0
  20. package/dist/commands/repo.js +166 -0
  21. package/dist/commands/repo.js.map +1 -0
  22. package/dist/commands/ship.d.ts.map +1 -1
  23. package/dist/commands/ship.js +29 -0
  24. package/dist/commands/ship.js.map +1 -1
  25. package/dist/commands/verify.d.ts.map +1 -1
  26. package/dist/commands/verify.js +548 -94
  27. package/dist/commands/verify.js.map +1 -1
  28. package/dist/index.js +17 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/services/mapper/ProjectScanner.d.ts +76 -2
  31. package/dist/services/mapper/ProjectScanner.d.ts.map +1 -1
  32. package/dist/services/mapper/ProjectScanner.js +545 -40
  33. package/dist/services/mapper/ProjectScanner.js.map +1 -1
  34. package/dist/services/security/SecurityGuard.d.ts +21 -2
  35. package/dist/services/security/SecurityGuard.d.ts.map +1 -1
  36. package/dist/services/security/SecurityGuard.js +130 -27
  37. package/dist/services/security/SecurityGuard.js.map +1 -1
  38. package/dist/utils/governance.d.ts +2 -0
  39. package/dist/utils/governance.d.ts.map +1 -1
  40. package/dist/utils/governance.js +2 -0
  41. package/dist/utils/governance.js.map +1 -1
  42. package/dist/utils/plan-slo.d.ts +73 -0
  43. package/dist/utils/plan-slo.d.ts.map +1 -0
  44. package/dist/utils/plan-slo.js +271 -0
  45. package/dist/utils/plan-slo.js.map +1 -0
  46. package/dist/utils/project-root.d.ts +5 -4
  47. package/dist/utils/project-root.d.ts.map +1 -1
  48. package/dist/utils/project-root.js +82 -7
  49. package/dist/utils/project-root.js.map +1 -1
  50. package/dist/utils/repo-links.d.ts +17 -0
  51. package/dist/utils/repo-links.d.ts.map +1 -0
  52. package/dist/utils/repo-links.js +136 -0
  53. package/dist/utils/repo-links.js.map +1 -0
  54. package/package.json +4 -4
@@ -241,6 +241,57 @@ function isEnabledFlag(value) {
241
241
  const normalized = value.trim().toLowerCase();
242
242
  return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
243
243
  }
244
+ function parseSigningKeyRing(raw) {
245
+ if (!raw || !raw.trim()) {
246
+ return {};
247
+ }
248
+ const out = {};
249
+ for (const token of raw.split(/[,\n;]+/)) {
250
+ const trimmed = token.trim();
251
+ if (!trimmed)
252
+ continue;
253
+ const separator = trimmed.indexOf('=');
254
+ if (separator <= 0)
255
+ continue;
256
+ const keyId = trimmed.slice(0, separator).trim();
257
+ const key = trimmed.slice(separator + 1).trim();
258
+ if (!keyId || !key)
259
+ continue;
260
+ out[keyId] = key;
261
+ }
262
+ return out;
263
+ }
264
+ function resolveGovernanceSigningConfig() {
265
+ const signingKeys = parseSigningKeyRing(process.env.NEURCODE_GOVERNANCE_SIGNING_KEYS);
266
+ const envSigningKey = process.env.NEURCODE_GOVERNANCE_SIGNING_KEY?.trim() ||
267
+ process.env.NEURCODE_AI_LOG_SIGNING_KEY?.trim() ||
268
+ '';
269
+ const requestedKeyId = process.env.NEURCODE_GOVERNANCE_SIGNING_KEY_ID?.trim() || '';
270
+ const signer = process.env.NEURCODE_GOVERNANCE_SIGNER || process.env.USER || 'neurcode-cli';
271
+ let signingKey = envSigningKey || null;
272
+ let signingKeyId = requestedKeyId || null;
273
+ if (!signingKey && Object.keys(signingKeys).length > 0) {
274
+ if (signingKeyId && signingKeys[signingKeyId]) {
275
+ signingKey = signingKeys[signingKeyId];
276
+ }
277
+ else {
278
+ const fallbackKeyId = Object.keys(signingKeys).sort((a, b) => a.localeCompare(b))[0];
279
+ signingKey = signingKeys[fallbackKeyId];
280
+ signingKeyId = signingKeyId || fallbackKeyId;
281
+ }
282
+ }
283
+ return {
284
+ signingKey,
285
+ signingKeyId,
286
+ signingKeys,
287
+ signer,
288
+ };
289
+ }
290
+ function isSignedAiLogsRequired(orgGovernanceSettings) {
291
+ return (orgGovernanceSettings?.requireSignedAiLogs === true ||
292
+ isEnabledFlag(process.env.NEURCODE_GOVERNANCE_REQUIRE_SIGNED_LOGS) ||
293
+ isEnabledFlag(process.env.NEURCODE_AI_LOG_REQUIRE_SIGNED));
294
+ }
244
295
  function policyLockMismatchMessage(mismatches) {
245
296
  if (mismatches.length === 0) {
246
297
  return 'Policy lock baseline check failed';
@@ -292,11 +343,24 @@ function resolveAuditIntegrityStatus(requireIntegrity, auditIntegrity) {
292
343
  issues,
293
344
  };
294
345
  }
346
+ async function recordVerificationIfRequested(options, config, payload) {
347
+ if (!options.record) {
348
+ return;
349
+ }
350
+ if (!config.apiKey) {
351
+ if (!payload.jsonMode) {
352
+ console.log(chalk.yellow('\n⚠️ --record flag requires API key'));
353
+ console.log(chalk.dim(' Set NEURCODE_API_KEY environment variable or use --api-key flag'));
354
+ }
355
+ return;
356
+ }
357
+ await reportVerification(payload.grade, payload.violations, payload.verifyResult, config.apiKey, config.apiUrl || 'https://api.neurcode.com', payload.projectId, payload.jsonMode, payload.governance);
358
+ }
295
359
  /**
296
360
  * Execute policy-only verification (General Governance mode)
297
361
  * Returns the exit code to use
298
362
  */
299
- async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source, orgGovernanceSettings, aiLogSigningKey, aiLogSigner) {
363
+ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source, projectId, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner) {
300
364
  if (!options.json) {
301
365
  console.log(chalk.cyan('🛡️ General Governance mode (policy only, no plan linked)\n'));
302
366
  }
@@ -308,22 +372,142 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
308
372
  contextCandidates: diffFiles.map((file) => file.path),
309
373
  orgGovernance: orgGovernanceSettings,
310
374
  signingKey: aiLogSigningKey,
375
+ signingKeyId: aiLogSigningKeyId,
376
+ signingKeys: aiLogSigningKeys,
311
377
  signer: aiLogSigner,
312
378
  });
379
+ const governancePayload = buildGovernancePayload(governanceAnalysis, orgGovernanceSettings);
313
380
  const contextPolicyViolations = governanceAnalysis.contextPolicy.violations.filter((item) => !ignoreFilter(item.file));
381
+ const signedLogsRequired = isSignedAiLogsRequired(orgGovernanceSettings);
382
+ if (signedLogsRequired && !governanceAnalysis.aiChangeLogIntegrity.valid) {
383
+ const message = `AI change-log integrity check failed: ${governanceAnalysis.aiChangeLogIntegrity.issues.join('; ') || 'unknown issue'}`;
384
+ if (options.json) {
385
+ console.log(JSON.stringify({
386
+ grade: 'F',
387
+ score: 0,
388
+ verdict: 'FAIL',
389
+ violations: [
390
+ {
391
+ file: '.neurcode/ai-change-log.json',
392
+ rule: 'ai_change_log_integrity',
393
+ severity: 'block',
394
+ message,
395
+ },
396
+ ],
397
+ message,
398
+ scopeGuardPassed: false,
399
+ bloatCount: 0,
400
+ bloatFiles: [],
401
+ plannedFilesModified: 0,
402
+ totalPlannedFiles: 0,
403
+ adherenceScore: 0,
404
+ mode: 'policy_only',
405
+ policyOnly: true,
406
+ policyOnlySource: source,
407
+ ...governancePayload,
408
+ }, null, 2));
409
+ }
410
+ else {
411
+ console.log(chalk.red('❌ AI change-log integrity validation failed (policy-only mode).'));
412
+ console.log(chalk.red(` ${message}`));
413
+ }
414
+ await recordVerificationIfRequested(options, config, {
415
+ grade: 'F',
416
+ violations: [
417
+ {
418
+ file: '.neurcode/ai-change-log.json',
419
+ rule: 'ai_change_log_integrity',
420
+ severity: 'block',
421
+ message,
422
+ },
423
+ ],
424
+ verifyResult: {
425
+ adherenceScore: 0,
426
+ verdict: 'FAIL',
427
+ bloatCount: 0,
428
+ bloatFiles: [],
429
+ message,
430
+ },
431
+ projectId,
432
+ jsonMode: Boolean(options.json),
433
+ governance: governancePayload,
434
+ });
435
+ return 2;
436
+ }
437
+ if (governanceAnalysis.governanceDecision.decision === 'block') {
438
+ const message = governanceAnalysis.governanceDecision.summary
439
+ || 'Governance decision matrix returned BLOCK.';
440
+ const reasonCodes = governanceAnalysis.governanceDecision.reasonCodes || [];
441
+ if (options.json) {
442
+ console.log(JSON.stringify({
443
+ grade: 'F',
444
+ score: 0,
445
+ verdict: 'FAIL',
446
+ violations: [
447
+ {
448
+ file: '.neurcode/ai-change-log.json',
449
+ rule: 'governance_decision_block',
450
+ severity: 'block',
451
+ message,
452
+ },
453
+ ],
454
+ message,
455
+ scopeGuardPassed: false,
456
+ bloatCount: 0,
457
+ bloatFiles: [],
458
+ plannedFilesModified: 0,
459
+ totalPlannedFiles: 0,
460
+ adherenceScore: 0,
461
+ mode: 'policy_only',
462
+ policyOnly: true,
463
+ policyOnlySource: source,
464
+ ...governancePayload,
465
+ }, null, 2));
466
+ }
467
+ else {
468
+ console.log(chalk.red('❌ Governance decision blocked this change set (policy-only mode).'));
469
+ if (reasonCodes.length > 0) {
470
+ console.log(chalk.red(` Reasons: ${reasonCodes.join(', ')}`));
471
+ }
472
+ console.log(chalk.red(` ${message}`));
473
+ }
474
+ await recordVerificationIfRequested(options, config, {
475
+ grade: 'F',
476
+ violations: [
477
+ {
478
+ file: '.neurcode/ai-change-log.json',
479
+ rule: 'governance_decision_block',
480
+ severity: 'block',
481
+ message,
482
+ },
483
+ ],
484
+ verifyResult: {
485
+ adherenceScore: 0,
486
+ verdict: 'FAIL',
487
+ bloatCount: 0,
488
+ bloatFiles: [],
489
+ message,
490
+ },
491
+ projectId,
492
+ jsonMode: Boolean(options.json),
493
+ governance: governancePayload,
494
+ });
495
+ return 2;
496
+ }
314
497
  if (contextPolicyViolations.length > 0) {
315
498
  const message = `Context policy violation: ${contextPolicyViolations.map((item) => item.file).join(', ')}`;
499
+ const contextPolicyViolationItems = contextPolicyViolations.map((item) => ({
500
+ file: item.file,
501
+ rule: `context_policy:${item.rule}`,
502
+ severity: 'block',
503
+ message: item.reason,
504
+ }));
316
505
  if (options.json) {
317
506
  console.log(JSON.stringify({
318
507
  grade: 'F',
319
508
  score: 0,
320
509
  verdict: 'FAIL',
321
- violations: contextPolicyViolations.map((item) => ({
322
- file: item.file,
323
- rule: `context_policy:${item.rule}`,
324
- severity: 'block',
325
- message: item.reason,
326
- })),
510
+ violations: contextPolicyViolationItems,
327
511
  message,
328
512
  scopeGuardPassed: false,
329
513
  bloatCount: 0,
@@ -334,7 +518,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
334
518
  mode: 'policy_only',
335
519
  policyOnly: true,
336
520
  policyOnlySource: source,
337
- ...buildGovernancePayload(governanceAnalysis, orgGovernanceSettings),
521
+ ...governancePayload,
338
522
  }, null, 2));
339
523
  }
340
524
  else {
@@ -344,6 +528,20 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
344
528
  });
345
529
  console.log(chalk.dim(`\n${message}`));
346
530
  }
531
+ await recordVerificationIfRequested(options, config, {
532
+ grade: 'F',
533
+ violations: contextPolicyViolationItems,
534
+ verifyResult: {
535
+ adherenceScore: 0,
536
+ verdict: 'FAIL',
537
+ bloatCount: 0,
538
+ bloatFiles: [],
539
+ message,
540
+ },
541
+ projectId,
542
+ jsonMode: Boolean(options.json),
543
+ governance: governancePayload,
544
+ });
347
545
  return 2;
348
546
  }
349
547
  let policyViolations = [];
@@ -417,12 +615,13 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
417
615
  }
418
616
  if (policyLockEvaluation.enforced && !policyLockEvaluation.matched) {
419
617
  const message = policyLockMismatchMessage(policyLockEvaluation.mismatches);
618
+ const lockViolationItems = toPolicyLockViolations(policyLockEvaluation.mismatches);
420
619
  if (options.json) {
421
620
  console.log(JSON.stringify({
422
621
  grade: 'F',
423
622
  score: 0,
424
623
  verdict: 'FAIL',
425
- violations: toPolicyLockViolations(policyLockEvaluation.mismatches),
624
+ violations: lockViolationItems,
426
625
  message,
427
626
  scopeGuardPassed: true,
428
627
  bloatCount: 0,
@@ -433,7 +632,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
433
632
  mode: 'policy_only',
434
633
  policyOnly: true,
435
634
  policyOnlySource: source,
436
- ...buildGovernancePayload(governanceAnalysis, orgGovernanceSettings),
635
+ ...governancePayload,
437
636
  policyLock: {
438
637
  enforced: true,
439
638
  matched: false,
@@ -450,6 +649,20 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
450
649
  });
451
650
  console.log(chalk.dim('\n If drift is intentional, regenerate baseline with `neurcode policy lock`.\n'));
452
651
  }
652
+ await recordVerificationIfRequested(options, config, {
653
+ grade: 'F',
654
+ violations: lockViolationItems,
655
+ verifyResult: {
656
+ adherenceScore: 0,
657
+ verdict: 'FAIL',
658
+ bloatCount: 0,
659
+ bloatFiles: [],
660
+ message,
661
+ },
662
+ projectId,
663
+ jsonMode: Boolean(options.json),
664
+ governance: governancePayload,
665
+ });
453
666
  return 2;
454
667
  }
455
668
  if (!options.json && effectiveRules.customRules.length > 0) {
@@ -565,7 +778,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
565
778
  mode: 'policy_only',
566
779
  policyOnly: true,
567
780
  policyOnlySource: source,
568
- ...buildGovernancePayload(governanceAnalysis, orgGovernanceSettings),
781
+ ...governancePayload,
569
782
  policyLock: {
570
783
  enforced: policyLockEvaluation.enforced,
571
784
  matched: policyLockEvaluation.matched,
@@ -608,6 +821,20 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
608
821
  displayGovernanceInsights(governanceAnalysis, { explain: options.explain });
609
822
  console.log(chalk.dim(`\n${message}`));
610
823
  }
824
+ await recordVerificationIfRequested(options, config, {
825
+ grade,
826
+ violations: violationsOutput,
827
+ verifyResult: {
828
+ adherenceScore: score,
829
+ verdict: effectiveVerdict,
830
+ bloatCount: 0,
831
+ bloatFiles: [],
832
+ message,
833
+ },
834
+ projectId,
835
+ jsonMode: Boolean(options.json),
836
+ governance: governancePayload,
837
+ });
611
838
  return effectiveVerdict === 'FAIL' ? 2 : effectiveVerdict === 'WARN' ? 1 : 0;
612
839
  }
613
840
  async function verifyCommand(options) {
@@ -661,10 +888,11 @@ async function verifyCommand(options) {
661
888
  orgId: (0, state_1.getOrgId)(),
662
889
  projectId: projectId || null,
663
890
  };
664
- const aiLogSigningKey = process.env.NEURCODE_GOVERNANCE_SIGNING_KEY ||
665
- process.env.NEURCODE_AI_LOG_SIGNING_KEY ||
666
- null;
667
- const aiLogSigner = process.env.NEURCODE_GOVERNANCE_SIGNER || process.env.USER || 'neurcode-cli';
891
+ const signingConfig = resolveGovernanceSigningConfig();
892
+ const aiLogSigningKey = signingConfig.signingKey;
893
+ const aiLogSigningKeyId = signingConfig.signingKeyId;
894
+ const aiLogSigningKeys = signingConfig.signingKeys;
895
+ const aiLogSigner = signingConfig.signer;
668
896
  let orgGovernanceSettings = null;
669
897
  if (config.apiKey) {
670
898
  try {
@@ -686,6 +914,8 @@ async function verifyCommand(options) {
686
914
  }
687
915
  }
688
916
  }
917
+ const signedLogsRequired = isSignedAiLogsRequired(orgGovernanceSettings);
918
+ const hasSigningMaterial = Boolean(aiLogSigningKey) || Object.keys(aiLogSigningKeys).length > 0;
689
919
  const recordVerifyEvent = (verdict, note, changedFiles, planId) => {
690
920
  if (!brainScope.orgId || !brainScope.projectId) {
691
921
  return;
@@ -707,6 +937,60 @@ async function verifyCommand(options) {
707
937
  // Never block verify flow on Brain persistence failures.
708
938
  }
709
939
  };
940
+ if (signedLogsRequired && !hasSigningMaterial) {
941
+ const message = 'Signed AI change-logs are required but no signing key is configured. Set NEURCODE_GOVERNANCE_SIGNING_KEY or NEURCODE_GOVERNANCE_SIGNING_KEYS.';
942
+ recordVerifyEvent('FAIL', 'missing_signing_key_material');
943
+ if (options.json) {
944
+ console.log(JSON.stringify({
945
+ grade: 'F',
946
+ score: 0,
947
+ verdict: 'FAIL',
948
+ violations: [
949
+ {
950
+ file: '.neurcode/ai-change-log.json',
951
+ rule: 'ai_change_log_signing_required',
952
+ severity: 'block',
953
+ message,
954
+ },
955
+ ],
956
+ adherenceScore: 0,
957
+ bloatCount: 0,
958
+ bloatFiles: [],
959
+ plannedFilesModified: 0,
960
+ totalPlannedFiles: 0,
961
+ message,
962
+ scopeGuardPassed: false,
963
+ mode: 'plan_enforced',
964
+ policyOnly: false,
965
+ }, null, 2));
966
+ }
967
+ else {
968
+ console.log(chalk.red('\n⛔ Governance Signing Key Missing'));
969
+ console.log(chalk.red(` ${message}`));
970
+ console.log(chalk.dim(' Recommended: set NEURCODE_GOVERNANCE_SIGNING_KEY_ID and key ring via NEURCODE_GOVERNANCE_SIGNING_KEYS.'));
971
+ }
972
+ await recordVerificationIfRequested(options, config, {
973
+ grade: 'F',
974
+ violations: [
975
+ {
976
+ file: '.neurcode/ai-change-log.json',
977
+ rule: 'ai_change_log_signing_required',
978
+ severity: 'block',
979
+ message,
980
+ },
981
+ ],
982
+ verifyResult: {
983
+ adherenceScore: 0,
984
+ verdict: 'FAIL',
985
+ bloatCount: 0,
986
+ bloatFiles: [],
987
+ message,
988
+ },
989
+ projectId: projectId || undefined,
990
+ jsonMode: Boolean(options.json),
991
+ });
992
+ process.exit(2);
993
+ }
710
994
  // Determine which diff to capture (staged + unstaged for full current work)
711
995
  let diffText;
712
996
  if (options.staged) {
@@ -818,21 +1102,25 @@ async function verifyCommand(options) {
818
1102
  contextCandidates: diffFiles.map((file) => file.path),
819
1103
  orgGovernance: orgGovernanceSettings,
820
1104
  signingKey: aiLogSigningKey,
1105
+ signingKeyId: aiLogSigningKeyId,
1106
+ signingKeys: aiLogSigningKeys,
821
1107
  signer: aiLogSigner,
822
1108
  });
1109
+ const baselineGovernancePayload = buildGovernancePayload(baselineGovernance, orgGovernanceSettings);
823
1110
  const message = `Context access policy violation: ${baselineContextViolations.map((item) => item.file).join(', ')}`;
1111
+ const baselineContextViolationItems = baselineContextViolations.map((item) => ({
1112
+ file: item.file,
1113
+ rule: `context_policy:${item.rule}`,
1114
+ severity: 'block',
1115
+ message: item.reason,
1116
+ }));
824
1117
  recordVerifyEvent('FAIL', `context_policy_violations=${baselineContextViolations.length}`, diffFiles.map((f) => f.path));
825
1118
  if (options.json) {
826
1119
  console.log(JSON.stringify({
827
1120
  grade: 'F',
828
1121
  score: 0,
829
1122
  verdict: 'FAIL',
830
- violations: baselineContextViolations.map((item) => ({
831
- file: item.file,
832
- rule: `context_policy:${item.rule}`,
833
- severity: 'block',
834
- message: item.reason,
835
- })),
1123
+ violations: baselineContextViolationItems,
836
1124
  adherenceScore: 0,
837
1125
  bloatCount: 0,
838
1126
  bloatFiles: [],
@@ -840,7 +1128,7 @@ async function verifyCommand(options) {
840
1128
  totalPlannedFiles: 0,
841
1129
  message,
842
1130
  scopeGuardPassed: false,
843
- ...buildGovernancePayload(baselineGovernance, orgGovernanceSettings),
1131
+ ...baselineGovernancePayload,
844
1132
  mode: 'policy_violation',
845
1133
  policyOnly: false,
846
1134
  }, null, 2));
@@ -853,6 +1141,20 @@ async function verifyCommand(options) {
853
1141
  console.log(chalk.dim(`\nRisk level: ${baselineGovernance.blastRadius.riskScore.toUpperCase()}`));
854
1142
  console.log(chalk.red('\nAction blocked.\n'));
855
1143
  }
1144
+ await recordVerificationIfRequested(options, config, {
1145
+ grade: 'F',
1146
+ violations: baselineContextViolationItems,
1147
+ verifyResult: {
1148
+ adherenceScore: 0,
1149
+ verdict: 'FAIL',
1150
+ bloatCount: 0,
1151
+ bloatFiles: [],
1152
+ message,
1153
+ },
1154
+ projectId: projectId || undefined,
1155
+ jsonMode: Boolean(options.json),
1156
+ governance: baselineGovernancePayload,
1157
+ });
856
1158
  process.exit(2);
857
1159
  }
858
1160
  if (!options.json) {
@@ -861,7 +1163,7 @@ async function verifyCommand(options) {
861
1163
  console.log(chalk.dim(` ${summary.totalAdded} lines added, ${summary.totalRemoved} lines removed\n`));
862
1164
  }
863
1165
  const runPolicyOnlyModeAndExit = async (source) => {
864
- const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client, source, orgGovernanceSettings, aiLogSigningKey, aiLogSigner);
1166
+ const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client, source, projectId || undefined, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner);
865
1167
  const changedFiles = diffFiles.map((f) => f.path);
866
1168
  const verdict = exitCode === 2 ? 'FAIL' : exitCode === 1 ? 'WARN' : 'PASS';
867
1169
  recordVerifyEvent(verdict, `policy_only_source=${source};exit=${exitCode}`, changedFiles);
@@ -910,6 +1212,7 @@ async function verifyCommand(options) {
910
1212
  if (!planId) {
911
1213
  if (requirePlan) {
912
1214
  const changedFiles = diffFiles.map((f) => f.path);
1215
+ const message = 'Plan ID is required in strict mode. Run "neurcode plan" first or pass --plan-id.';
913
1216
  recordVerifyEvent('FAIL', 'missing_plan_id;require_plan=true', changedFiles);
914
1217
  if (options.json) {
915
1218
  console.log(JSON.stringify({
@@ -922,7 +1225,7 @@ async function verifyCommand(options) {
922
1225
  bloatFiles: [],
923
1226
  plannedFilesModified: 0,
924
1227
  totalPlannedFiles: 0,
925
- message: 'Plan ID is required in strict mode. Run "neurcode plan" first or pass --plan-id.',
1228
+ message,
926
1229
  scopeGuardPassed: false,
927
1230
  mode: 'plan_required',
928
1231
  policyOnly: false,
@@ -933,6 +1236,19 @@ async function verifyCommand(options) {
933
1236
  console.log(chalk.dim(' Run "neurcode plan" first or pass --plan-id <id>.'));
934
1237
  console.log(chalk.dim(' Use --policy-only only when intentionally running general governance checks.'));
935
1238
  }
1239
+ await recordVerificationIfRequested(options, config, {
1240
+ grade: 'F',
1241
+ violations: [],
1242
+ verifyResult: {
1243
+ adherenceScore: 0,
1244
+ verdict: 'FAIL',
1245
+ bloatCount: 0,
1246
+ bloatFiles: [],
1247
+ message,
1248
+ },
1249
+ projectId: projectId || undefined,
1250
+ jsonMode: Boolean(options.json),
1251
+ });
936
1252
  process.exit(1);
937
1253
  }
938
1254
  if (!options.json) {
@@ -981,6 +1297,8 @@ async function verifyCommand(options) {
981
1297
  contextCandidates: planFiles,
982
1298
  orgGovernance: orgGovernanceSettings,
983
1299
  signingKey: aiLogSigningKey,
1300
+ signingKeyId: aiLogSigningKeyId,
1301
+ signingKeys: aiLogSigningKeys,
984
1302
  signer: aiLogSigner,
985
1303
  });
986
1304
  // Get sessionId from state file (.neurcode/state.json) first, then fallback to config
@@ -1029,25 +1347,26 @@ async function verifyCommand(options) {
1029
1347
  // Step D: The Block (only report scope violations for non-ignored files)
1030
1348
  if (filteredViolations.length > 0) {
1031
1349
  recordVerifyEvent('FAIL', `scope_violation=${filteredViolations.length}`, modifiedFiles, finalPlanId);
1350
+ const scopeViolationItems = filteredViolations.map((file) => ({
1351
+ file,
1352
+ rule: 'scope_guard',
1353
+ severity: 'block',
1354
+ message: 'File modified outside the plan',
1355
+ }));
1356
+ const scopeViolationMessage = `Scope violation: ${filteredViolations.length} file(s) modified outside the plan`;
1032
1357
  if (options.json) {
1033
1358
  // Output JSON for scope violation BEFORE exit. Must include violations for GitHub Action annotations.
1034
- const violationsOutput = filteredViolations.map((file) => ({
1035
- file,
1036
- rule: 'scope_guard',
1037
- severity: 'block',
1038
- message: 'File modified outside the plan',
1039
- }));
1040
1359
  const jsonOutput = {
1041
1360
  grade: 'F',
1042
1361
  score: 0,
1043
1362
  verdict: 'FAIL',
1044
- violations: violationsOutput,
1363
+ violations: scopeViolationItems,
1045
1364
  adherenceScore: 0,
1046
1365
  bloatCount: filteredViolations.length,
1047
1366
  bloatFiles: filteredViolations,
1048
1367
  plannedFilesModified: 0,
1049
1368
  totalPlannedFiles: planFiles.length,
1050
- message: `Scope violation: ${filteredViolations.length} file(s) modified outside the plan`,
1369
+ message: scopeViolationMessage,
1051
1370
  scopeGuardPassed: false,
1052
1371
  mode: 'plan_enforced',
1053
1372
  policyOnly: false,
@@ -1057,6 +1376,22 @@ async function verifyCommand(options) {
1057
1376
  };
1058
1377
  // CRITICAL: Print JSON first, then exit
1059
1378
  console.log(JSON.stringify(jsonOutput, null, 2));
1379
+ await recordVerificationIfRequested(options, config, {
1380
+ grade: 'F',
1381
+ violations: scopeViolationItems,
1382
+ verifyResult: {
1383
+ adherenceScore: 0,
1384
+ verdict: 'FAIL',
1385
+ bloatCount: filteredViolations.length,
1386
+ bloatFiles: filteredViolations,
1387
+ message: scopeViolationMessage,
1388
+ },
1389
+ projectId: projectId || undefined,
1390
+ jsonMode: true,
1391
+ governance: governanceResult
1392
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1393
+ : undefined,
1394
+ });
1060
1395
  process.exit(1);
1061
1396
  }
1062
1397
  else {
@@ -1077,6 +1412,22 @@ async function verifyCommand(options) {
1077
1412
  displayGovernanceInsights(governanceResult, { explain: options.explain });
1078
1413
  }
1079
1414
  console.log('');
1415
+ await recordVerificationIfRequested(options, config, {
1416
+ grade: 'F',
1417
+ violations: scopeViolationItems,
1418
+ verifyResult: {
1419
+ adherenceScore: 0,
1420
+ verdict: 'FAIL',
1421
+ bloatCount: filteredViolations.length,
1422
+ bloatFiles: filteredViolations,
1423
+ message: scopeViolationMessage,
1424
+ },
1425
+ projectId: projectId || undefined,
1426
+ jsonMode: false,
1427
+ governance: governanceResult
1428
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1429
+ : undefined,
1430
+ });
1080
1431
  process.exit(1);
1081
1432
  }
1082
1433
  }
@@ -1164,13 +1515,14 @@ async function verifyCommand(options) {
1164
1515
  }
1165
1516
  if (policyLockEvaluation.enforced && !policyLockEvaluation.matched) {
1166
1517
  const message = policyLockMismatchMessage(policyLockEvaluation.mismatches);
1518
+ const lockViolationItems = toPolicyLockViolations(policyLockEvaluation.mismatches);
1167
1519
  recordVerifyEvent('FAIL', 'policy_lock_mismatch', diffFiles.map((f) => f.path), finalPlanId);
1168
1520
  if (options.json) {
1169
1521
  console.log(JSON.stringify({
1170
1522
  grade: 'F',
1171
1523
  score: 0,
1172
1524
  verdict: 'FAIL',
1173
- violations: toPolicyLockViolations(policyLockEvaluation.mismatches),
1525
+ violations: lockViolationItems,
1174
1526
  adherenceScore: 0,
1175
1527
  bloatCount: 0,
1176
1528
  bloatFiles: [],
@@ -1199,6 +1551,22 @@ async function verifyCommand(options) {
1199
1551
  });
1200
1552
  console.log(chalk.dim('\n If this drift is intentional, regenerate baseline with `neurcode policy lock`.\n'));
1201
1553
  }
1554
+ await recordVerificationIfRequested(options, config, {
1555
+ grade: 'F',
1556
+ violations: lockViolationItems,
1557
+ verifyResult: {
1558
+ adherenceScore: 0,
1559
+ verdict: 'FAIL',
1560
+ bloatCount: 0,
1561
+ bloatFiles: [],
1562
+ message,
1563
+ },
1564
+ projectId: projectId || undefined,
1565
+ jsonMode: Boolean(options.json),
1566
+ governance: governanceResult
1567
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1568
+ : undefined,
1569
+ });
1202
1570
  process.exit(2);
1203
1571
  }
1204
1572
  }
@@ -1379,13 +1747,23 @@ async function verifyCommand(options) {
1379
1747
  const verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraints);
1380
1748
  // Apply custom policy verdict: block from dashboard overrides API verdict
1381
1749
  const policyBlock = policyDecision === 'block' && policyViolations.length > 0;
1382
- const effectiveVerdict = policyBlock ? 'FAIL' : verifyResult.verdict;
1750
+ const governanceDecisionBlock = governanceResult?.governanceDecision?.decision === 'block';
1751
+ const governanceIntegrityBlock = signedLogsRequired && governanceResult ? governanceResult.aiChangeLogIntegrity.valid !== true : false;
1752
+ const governanceHardBlock = governanceDecisionBlock || governanceIntegrityBlock;
1753
+ const effectiveVerdict = policyBlock || governanceHardBlock
1754
+ ? 'FAIL'
1755
+ : verifyResult.verdict;
1383
1756
  const policyMessageBase = policyBlock
1384
1757
  ? `Custom policy violations: ${policyViolations.map(v => `${v.file}: ${v.message || v.rule}`).join('; ')}. ${verifyResult.message}`
1385
1758
  : verifyResult.message;
1386
- const effectiveMessage = policyExceptionsSummary.suppressed > 0
1759
+ const governanceBlockReason = governanceIntegrityBlock
1760
+ ? `AI change-log integrity failed: ${governanceResult?.aiChangeLogIntegrity?.issues?.join('; ') || 'unknown issue'}`
1761
+ : governanceDecisionBlock
1762
+ ? governanceResult?.governanceDecision?.summary || 'Governance decision matrix returned BLOCK.'
1763
+ : null;
1764
+ const effectiveMessage = (policyExceptionsSummary.suppressed > 0
1387
1765
  ? `${policyMessageBase} Policy exceptions suppressed ${policyExceptionsSummary.suppressed} violation(s).`
1388
- : policyMessageBase;
1766
+ : policyMessageBase) + (governanceBlockReason ? ` ${governanceBlockReason}` : '');
1389
1767
  // Calculate grade from effective verdict and score
1390
1768
  // CRITICAL: 0/0 planned files = 'F' (Incomplete), not 'B'
1391
1769
  // Bloat automatically drops grade by at least one letter
@@ -1434,6 +1812,7 @@ async function verifyCommand(options) {
1434
1812
  recordVerifyEvent(effectiveVerdict, `adherence=${verifyResult.adherenceScore};bloat=${verifyResult.bloatCount};scopeGuard=${scopeGuardPassed ? 1 : 0};policy=${policyDecision};policyExceptions=${policyExceptionsSummary.suppressed}`, changedPathsForBrain, finalPlanId);
1435
1813
  const shouldForceGovernancePass = scopeGuardPassed &&
1436
1814
  !policyBlock &&
1815
+ !governanceHardBlock &&
1437
1816
  (effectiveVerdict === 'PASS' ||
1438
1817
  ((verifyResult.verdict === 'FAIL' || verifyResult.verdict === 'WARN') &&
1439
1818
  policyViolations.length === 0 &&
@@ -1493,30 +1872,22 @@ async function verifyCommand(options) {
1493
1872
  : {}),
1494
1873
  };
1495
1874
  console.log(JSON.stringify(jsonOutput, null, 2));
1496
- // Report to Neurcode Cloud if --record flag is set (after JSON output)
1497
- if (options.record && config.apiKey) {
1498
- const violations = [
1499
- ...filteredBloatFiles.map((file) => ({
1500
- rule: 'scope_guard',
1501
- file: file,
1502
- severity: 'block',
1503
- message: 'File modified outside the plan',
1504
- })),
1505
- ...policyViolations.map(v => ({
1506
- rule: v.rule,
1507
- file: v.file,
1508
- severity: v.severity,
1509
- message: v.message,
1510
- })),
1511
- ];
1512
- // Report in background (don't await to avoid blocking JSON output)
1513
- reportVerification(grade, violations, verifyResult, config.apiKey, config.apiUrl, projectId || undefined, true, // jsonMode = true
1514
- governanceResult
1875
+ await recordVerificationIfRequested(options, config, {
1876
+ grade,
1877
+ violations: violations,
1878
+ verifyResult: {
1879
+ adherenceScore: verifyResult.adherenceScore,
1880
+ verdict: effectiveVerdict,
1881
+ bloatCount: filteredBloatFiles.length,
1882
+ bloatFiles: filteredBloatFiles,
1883
+ message: effectiveMessage,
1884
+ },
1885
+ projectId: projectId || undefined,
1886
+ jsonMode: true,
1887
+ governance: governanceResult
1515
1888
  ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1516
- : undefined).catch(() => {
1517
- // Error already logged in reportVerification
1518
- });
1519
- }
1889
+ : undefined,
1890
+ });
1520
1891
  // Exit based on effective verdict (same logic as below)
1521
1892
  if (shouldForceGovernancePass) {
1522
1893
  process.exit(0);
@@ -1565,36 +1936,37 @@ async function verifyCommand(options) {
1565
1936
  }
1566
1937
  }
1567
1938
  // Report to Neurcode Cloud if --record flag is set
1568
- if (options.record) {
1569
- if (!config.apiKey) {
1570
- if (!options.json) {
1571
- console.log(chalk.yellow('\n⚠️ --record flag requires API key'));
1572
- console.log(chalk.dim(' Set NEURCODE_API_KEY environment variable or use --api-key flag'));
1573
- }
1574
- }
1575
- else {
1576
- // Include scope bloat and custom policy violations in the report (excluding .neurcodeignore'd paths)
1577
- const filteredBloatForReport = (verifyResult.bloatFiles || []).filter((f) => !shouldIgnore(f));
1578
- const violations = [
1579
- ...filteredBloatForReport.map((file) => ({
1580
- rule: 'scope_guard',
1581
- file: file,
1582
- severity: 'block',
1583
- message: 'File modified outside the plan',
1584
- })),
1585
- ...policyViolations.map(v => ({
1586
- rule: v.rule,
1587
- file: v.file,
1588
- severity: v.severity,
1589
- message: v.message,
1590
- })),
1591
- ];
1592
- await reportVerification(grade, violations, verifyResult, config.apiKey, config.apiUrl, projectId || undefined, false, // jsonMode = false
1593
- governanceResult
1594
- ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1595
- : undefined);
1596
- }
1597
- }
1939
+ const filteredBloatForReport = (verifyResult.bloatFiles || []).filter((f) => !shouldIgnore(f));
1940
+ const reportViolations = [
1941
+ ...filteredBloatForReport.map((file) => ({
1942
+ rule: 'scope_guard',
1943
+ file: file,
1944
+ severity: 'block',
1945
+ message: 'File modified outside the plan',
1946
+ })),
1947
+ ...policyViolations.map((v) => ({
1948
+ rule: v.rule,
1949
+ file: v.file,
1950
+ severity: v.severity,
1951
+ message: v.message,
1952
+ })),
1953
+ ];
1954
+ await recordVerificationIfRequested(options, config, {
1955
+ grade,
1956
+ violations: reportViolations,
1957
+ verifyResult: {
1958
+ adherenceScore: verifyResult.adherenceScore,
1959
+ verdict: effectiveVerdict,
1960
+ bloatCount: filteredBloatForReport.length,
1961
+ bloatFiles: filteredBloatForReport,
1962
+ message: effectiveMessage,
1963
+ },
1964
+ projectId: projectId || undefined,
1965
+ jsonMode: false,
1966
+ governance: governanceResult
1967
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1968
+ : undefined,
1969
+ });
1598
1970
  // Governance override: keep PASS only when scope guard passes and failure is due
1599
1971
  // to server-side bloat mismatch (allowed files unknown to verify API).
1600
1972
  if (shouldForceGovernancePass) {
@@ -1780,6 +2152,63 @@ function collectCIContext() {
1780
2152
  }
1781
2153
  return context;
1782
2154
  }
2155
+ const REPORT_MAX_ARRAY_ITEMS = 120;
2156
+ const REPORT_MAX_STRING_LENGTH = 4000;
2157
+ const REPORT_MAX_OBJECT_DEPTH = 6;
2158
+ function compactReportValue(value, depth = 0, seen = new WeakSet()) {
2159
+ if (value == null) {
2160
+ return value;
2161
+ }
2162
+ if (typeof value === 'string') {
2163
+ return value.length > REPORT_MAX_STRING_LENGTH
2164
+ ? `${value.slice(0, REPORT_MAX_STRING_LENGTH)}...[truncated]`
2165
+ : value;
2166
+ }
2167
+ if (typeof value !== 'object') {
2168
+ return value;
2169
+ }
2170
+ if (depth >= REPORT_MAX_OBJECT_DEPTH) {
2171
+ return '[truncated]';
2172
+ }
2173
+ if (Array.isArray(value)) {
2174
+ const items = value.slice(0, REPORT_MAX_ARRAY_ITEMS).map((item) => compactReportValue(item, depth + 1, seen));
2175
+ if (value.length > REPORT_MAX_ARRAY_ITEMS) {
2176
+ items.push(`[truncated ${value.length - REPORT_MAX_ARRAY_ITEMS} item(s)]`);
2177
+ }
2178
+ return items;
2179
+ }
2180
+ if (seen.has(value)) {
2181
+ return '[circular]';
2182
+ }
2183
+ seen.add(value);
2184
+ const compacted = {};
2185
+ for (const [key, nestedValue] of Object.entries(value)) {
2186
+ compacted[key] = compactReportValue(nestedValue, depth + 1, seen);
2187
+ }
2188
+ return compacted;
2189
+ }
2190
+ function buildCompactVerificationPayload(payload) {
2191
+ const compactViolations = payload.violations.slice(0, REPORT_MAX_ARRAY_ITEMS);
2192
+ if (payload.violations.length > REPORT_MAX_ARRAY_ITEMS) {
2193
+ compactViolations.push({
2194
+ rule: 'report_payload_compaction',
2195
+ file: '__meta__',
2196
+ severity: 'warn',
2197
+ message: `Truncated ${payload.violations.length - REPORT_MAX_ARRAY_ITEMS} additional violation(s) for upload`,
2198
+ });
2199
+ }
2200
+ return {
2201
+ ...payload,
2202
+ violations: compactViolations,
2203
+ bloatFiles: payload.bloatFiles.slice(0, REPORT_MAX_ARRAY_ITEMS),
2204
+ message: payload.message.length > REPORT_MAX_STRING_LENGTH
2205
+ ? `${payload.message.slice(0, REPORT_MAX_STRING_LENGTH)}...[truncated]`
2206
+ : payload.message,
2207
+ governance: payload.governance
2208
+ ? compactReportValue(payload.governance)
2209
+ : undefined,
2210
+ };
2211
+ }
1783
2212
  /**
1784
2213
  * Report verification results to Neurcode Cloud
1785
2214
  */
@@ -1801,22 +2230,36 @@ async function reportVerification(grade, violations, verifyResult, apiKey, apiUr
1801
2230
  projectId,
1802
2231
  governance,
1803
2232
  };
1804
- const response = await fetch(`${apiUrl}/api/v1/action/verifications`, {
2233
+ const postPayload = async (requestPayload) => fetch(`${apiUrl}/api/v1/action/verifications`, {
1805
2234
  method: 'POST',
1806
2235
  headers: {
1807
2236
  'Content-Type': 'application/json',
1808
2237
  'Authorization': `Bearer ${apiKey}`,
1809
2238
  },
1810
- body: JSON.stringify(payload),
2239
+ body: JSON.stringify(requestPayload),
1811
2240
  });
2241
+ let response = await postPayload(payload);
2242
+ let compactedUpload = false;
2243
+ if (response.status === 413) {
2244
+ response = await postPayload(buildCompactVerificationPayload(payload));
2245
+ compactedUpload = true;
2246
+ }
1812
2247
  if (!response.ok) {
1813
2248
  const errorText = await response.text();
1814
- throw new Error(`HTTP ${response.status}: ${errorText}`);
2249
+ const compactError = errorText.replace(/\s+/g, ' ').trim().slice(0, 400);
2250
+ throw new Error(`HTTP ${response.status}: ${compactError}`);
2251
+ }
2252
+ let result = {};
2253
+ try {
2254
+ result = (await response.json());
2255
+ }
2256
+ catch {
2257
+ // Some proxies may return empty success bodies; treat as recorded.
1815
2258
  }
1816
- const result = await response.json();
1817
2259
  // Only log if not in json mode to avoid polluting stdout
1818
2260
  if (!jsonMode) {
1819
- console.log(chalk.dim(`\n✅ Verification result reported to Neurcode Cloud (ID: ${result.id})`));
2261
+ const suffix = compactedUpload ? ' (compact payload)' : '';
2262
+ console.log(chalk.dim(`\n✅ Verification result reported to Neurcode Cloud (ID: ${result.id || 'ok'})${suffix}`));
1820
2263
  }
1821
2264
  }
1822
2265
  catch (error) {
@@ -1867,6 +2310,17 @@ function displayGovernanceInsights(governance, options = {}) {
1867
2310
  console.log(governance.aiChangeLogIntegrity.valid
1868
2311
  ? chalk.dim(` AI change-log integrity: valid (${governance.aiChangeLogIntegrity.signed ? 'signed' : 'unsigned'})`)
1869
2312
  : chalk.red(` AI change-log integrity: invalid (${governance.aiChangeLogIntegrity.issues.join('; ') || 'unknown'})`));
2313
+ if (governance.aiChangeLogIntegrity.signed) {
2314
+ const keyId = typeof governance.aiChangeLogIntegrity.keyId === 'string'
2315
+ ? governance.aiChangeLogIntegrity.keyId
2316
+ : null;
2317
+ const verifiedWithKeyId = typeof governance.aiChangeLogIntegrity.verifiedWithKeyId === 'string'
2318
+ ? governance.aiChangeLogIntegrity.verifiedWithKeyId
2319
+ : null;
2320
+ if (keyId || verifiedWithKeyId) {
2321
+ console.log(chalk.dim(` Signing key: ${keyId || 'n/a'}${verifiedWithKeyId ? ` (verified via ${verifiedWithKeyId})` : ''}`));
2322
+ }
2323
+ }
1870
2324
  if (governance.suspiciousChange.flagged) {
1871
2325
  console.log(chalk.red('\nSuspicious Change Detected'));
1872
2326
  console.log(chalk.red(` Plan expected files: ${governance.suspiciousChange.expectedFiles} | AI modified files: ${governance.suspiciousChange.actualFiles}`));