@neurcode-ai/cli 0.9.34 → 0.9.36

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 (53) hide show
  1. package/README.md +2 -1
  2. package/dist/api-client.d.ts +59 -1
  3. package/dist/api-client.d.ts.map +1 -1
  4. package/dist/api-client.js +16 -1
  5. package/dist/api-client.js.map +1 -1
  6. package/dist/commands/ask.d.ts.map +1 -1
  7. package/dist/commands/ask.js +15 -2
  8. package/dist/commands/ask.js.map +1 -1
  9. package/dist/commands/plan.d.ts.map +1 -1
  10. package/dist/commands/plan.js +81 -2
  11. package/dist/commands/plan.js.map +1 -1
  12. package/dist/commands/policy.d.ts.map +1 -1
  13. package/dist/commands/policy.js +377 -20
  14. package/dist/commands/policy.js.map +1 -1
  15. package/dist/commands/verify.d.ts +8 -0
  16. package/dist/commands/verify.d.ts.map +1 -1
  17. package/dist/commands/verify.js +454 -59
  18. package/dist/commands/verify.js.map +1 -1
  19. package/dist/index.js +102 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/utils/change-contract.d.ts +55 -0
  22. package/dist/utils/change-contract.d.ts.map +1 -0
  23. package/dist/utils/change-contract.js +158 -0
  24. package/dist/utils/change-contract.js.map +1 -0
  25. package/dist/utils/policy-audit.d.ts +2 -2
  26. package/dist/utils/policy-audit.d.ts.map +1 -1
  27. package/dist/utils/policy-audit.js.map +1 -1
  28. package/dist/utils/policy-compiler.d.ts +68 -0
  29. package/dist/utils/policy-compiler.d.ts.map +1 -0
  30. package/dist/utils/policy-compiler.js +170 -0
  31. package/dist/utils/policy-compiler.js.map +1 -0
  32. package/dist/utils/policy-exceptions.d.ts +11 -1
  33. package/dist/utils/policy-exceptions.d.ts.map +1 -1
  34. package/dist/utils/policy-exceptions.js +94 -6
  35. package/dist/utils/policy-exceptions.js.map +1 -1
  36. package/dist/utils/policy-governance.d.ts +22 -1
  37. package/dist/utils/policy-governance.d.ts.map +1 -1
  38. package/dist/utils/policy-governance.js +178 -14
  39. package/dist/utils/policy-governance.js.map +1 -1
  40. package/dist/utils/policy-packs.d.ts +1 -1
  41. package/dist/utils/policy-packs.d.ts.map +1 -1
  42. package/dist/utils/policy-packs.js +185 -0
  43. package/dist/utils/policy-packs.js.map +1 -1
  44. package/dist/utils/project-root.d.ts +16 -0
  45. package/dist/utils/project-root.d.ts.map +1 -1
  46. package/dist/utils/project-root.js +123 -9
  47. package/dist/utils/project-root.js.map +1 -1
  48. package/dist/utils/scope-telemetry.d.ts +21 -0
  49. package/dist/utils/scope-telemetry.d.ts.map +1 -0
  50. package/dist/utils/scope-telemetry.js +35 -0
  51. package/dist/utils/scope-telemetry.js.map +1 -0
  52. package/package.json +15 -12
  53. package/LICENSE +0 -201
@@ -43,6 +43,7 @@ const child_process_1 = require("child_process");
43
43
  const git_1 = require("../utils/git");
44
44
  const diff_parser_1 = require("@neurcode-ai/diff-parser");
45
45
  const policy_engine_1 = require("@neurcode-ai/policy-engine");
46
+ const governance_runtime_1 = require("@neurcode-ai/governance-runtime");
46
47
  const config_1 = require("../config");
47
48
  const api_client_1 = require("../api-client");
48
49
  const path_1 = require("path");
@@ -53,12 +54,15 @@ const box_1 = require("../utils/box");
53
54
  const ignore_1 = require("../utils/ignore");
54
55
  const project_root_1 = require("../utils/project-root");
55
56
  const brain_context_1 = require("../utils/brain-context");
57
+ const scope_telemetry_1 = require("../utils/scope-telemetry");
56
58
  const policy_packs_1 = require("../utils/policy-packs");
57
59
  const custom_policy_rules_1 = require("../utils/custom-policy-rules");
58
60
  const policy_exceptions_1 = require("../utils/policy-exceptions");
59
61
  const policy_governance_1 = require("../utils/policy-governance");
60
62
  const policy_audit_1 = require("../utils/policy-audit");
61
63
  const governance_1 = require("../utils/governance");
64
+ const policy_compiler_1 = require("../utils/policy-compiler");
65
+ const change_contract_1 = require("../utils/change-contract");
62
66
  const policy_1 = require("@neurcode-ai/policy");
63
67
  // Import chalk with fallback
64
68
  let chalk;
@@ -80,6 +84,7 @@ catch {
80
84
  white: (str) => str,
81
85
  };
82
86
  }
87
+ ;
83
88
  /**
84
89
  * Check if a file path should be excluded from verification analysis
85
90
  * Excludes internal/system files that should not count towards plan adherence
@@ -234,6 +239,43 @@ async function buildEffectivePolicyRules(client, projectRoot, useDashboardPolici
234
239
  includeDashboardPolicies: useDashboardPolicies,
235
240
  };
236
241
  }
242
+ function resolveCompiledPolicyMetadata(artifact, path) {
243
+ if (!artifact || !path) {
244
+ return null;
245
+ }
246
+ return {
247
+ fingerprint: artifact.fingerprint,
248
+ deterministicRuleCount: artifact.compilation.deterministicRuleCount,
249
+ unmatchedStatements: artifact.compilation.unmatchedStatements.length,
250
+ sourcePath: path,
251
+ policyLockFingerprint: artifact.source.policyLockFingerprint,
252
+ };
253
+ }
254
+ function buildCompiledPolicyFromEffectiveRules(input) {
255
+ const policyStatements = [
256
+ ...input.effectiveRules.customPolicies.map((policy) => policy.rule_text),
257
+ ];
258
+ return (0, policy_compiler_1.buildCompiledPolicyArtifact)({
259
+ includeDashboardPolicies: input.effectiveRules.includeDashboardPolicies,
260
+ policyLockPath: input.policyLockEvaluation.lockPath,
261
+ policyLockFingerprint: input.policyLockEvaluation.lockPresent
262
+ ? (0, policy_packs_1.readPolicyLockFile)(input.projectRoot).lock?.effective.fingerprint || null
263
+ : null,
264
+ policyPack: input.effectiveRules.policyPack
265
+ ? {
266
+ id: input.effectiveRules.policyPack.packId,
267
+ name: input.effectiveRules.policyPack.packName,
268
+ version: input.effectiveRules.policyPack.version,
269
+ }
270
+ : null,
271
+ defaultRuleCount: (0, policy_engine_1.createDefaultPolicy)().rules.length,
272
+ policyPackRuleCount: input.effectiveRules.policyPackRules.length,
273
+ customRuleCount: input.effectiveRules.customRules.length,
274
+ effectiveRuleCount: input.effectiveRules.allRules.length,
275
+ intentConstraints: input.intentConstraints,
276
+ policyRules: policyStatements,
277
+ });
278
+ }
237
279
  const POLICY_AUDIT_FILE = 'neurcode.policy.audit.log.jsonl';
238
280
  function isEnabledFlag(value) {
239
281
  if (!value)
@@ -321,8 +363,14 @@ function resolvePolicyDecisionFromViolations(violations) {
321
363
  }
322
364
  function explainExceptionEligibilityReason(reason) {
323
365
  switch (reason) {
366
+ case 'reason_required':
367
+ return 'exception reason does not meet governance minimum length';
368
+ case 'duration_exceeds_max':
369
+ return 'exception expiry window exceeds governance maximum duration';
324
370
  case 'approval_required':
325
371
  return 'exception exists but approvals are required';
372
+ case 'critical_approvals_required':
373
+ return 'critical rule exception requires additional independent approvals';
326
374
  case 'insufficient_approvals':
327
375
  return 'exception exists but approval threshold is not met';
328
376
  case 'self_approval_only':
@@ -354,13 +402,30 @@ async function recordVerificationIfRequested(options, config, payload) {
354
402
  }
355
403
  return;
356
404
  }
357
- await reportVerification(payload.grade, payload.violations, payload.verifyResult, config.apiKey, config.apiUrl || 'https://api.neurcode.com', payload.projectId, payload.jsonMode, payload.governance);
405
+ await reportVerification(payload.grade, payload.violations, payload.verifyResult, config.apiKey, config.apiUrl || 'https://api.neurcode.com', payload.projectId, payload.jsonMode, payload.governance, payload.verificationSource);
358
406
  }
359
407
  /**
360
408
  * Execute policy-only verification (General Governance mode)
361
409
  * Returns the exit code to use
362
410
  */
363
- async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source, projectId, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner) {
411
+ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source, scopeTelemetry, projectId, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner, compiledPolicyMetadata, changeContractSummary) {
412
+ const emitPolicyOnlyJson = (payload) => {
413
+ console.log(JSON.stringify({
414
+ ...payload,
415
+ ...(compiledPolicyMetadata ? { policyCompilation: compiledPolicyMetadata } : {}),
416
+ changeContract: changeContractSummary,
417
+ scope: scopeTelemetry,
418
+ }, null, 2));
419
+ };
420
+ const policyOnlyVerificationSource = 'policy_only';
421
+ const recordPolicyOnlyVerification = async (payload) => recordVerificationIfRequested(options, config, {
422
+ ...payload,
423
+ verificationSource: policyOnlyVerificationSource,
424
+ verifyResult: {
425
+ ...payload.verifyResult,
426
+ verificationSource: policyOnlyVerificationSource,
427
+ },
428
+ });
364
429
  if (!options.json) {
365
430
  console.log(chalk.cyan('🛡️ General Governance mode (policy only, no plan linked)\n'));
366
431
  }
@@ -376,13 +441,16 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
376
441
  signingKeys: aiLogSigningKeys,
377
442
  signer: aiLogSigner,
378
443
  });
379
- const governancePayload = buildGovernancePayload(governanceAnalysis, orgGovernanceSettings);
444
+ const governancePayload = buildGovernancePayload(governanceAnalysis, orgGovernanceSettings, {
445
+ compiledPolicy: compiledPolicyMetadata,
446
+ changeContract: changeContractSummary,
447
+ });
380
448
  const contextPolicyViolations = governanceAnalysis.contextPolicy.violations.filter((item) => !ignoreFilter(item.file));
381
449
  const signedLogsRequired = isSignedAiLogsRequired(orgGovernanceSettings);
382
450
  if (signedLogsRequired && !governanceAnalysis.aiChangeLogIntegrity.valid) {
383
451
  const message = `AI change-log integrity check failed: ${governanceAnalysis.aiChangeLogIntegrity.issues.join('; ') || 'unknown issue'}`;
384
452
  if (options.json) {
385
- console.log(JSON.stringify({
453
+ emitPolicyOnlyJson({
386
454
  grade: 'F',
387
455
  score: 0,
388
456
  verdict: 'FAIL',
@@ -405,13 +473,13 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
405
473
  policyOnly: true,
406
474
  policyOnlySource: source,
407
475
  ...governancePayload,
408
- }, null, 2));
476
+ });
409
477
  }
410
478
  else {
411
479
  console.log(chalk.red('❌ AI change-log integrity validation failed (policy-only mode).'));
412
480
  console.log(chalk.red(` ${message}`));
413
481
  }
414
- await recordVerificationIfRequested(options, config, {
482
+ await recordPolicyOnlyVerification({
415
483
  grade: 'F',
416
484
  violations: [
417
485
  {
@@ -439,7 +507,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
439
507
  || 'Governance decision matrix returned BLOCK.';
440
508
  const reasonCodes = governanceAnalysis.governanceDecision.reasonCodes || [];
441
509
  if (options.json) {
442
- console.log(JSON.stringify({
510
+ emitPolicyOnlyJson({
443
511
  grade: 'F',
444
512
  score: 0,
445
513
  verdict: 'FAIL',
@@ -462,7 +530,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
462
530
  policyOnly: true,
463
531
  policyOnlySource: source,
464
532
  ...governancePayload,
465
- }, null, 2));
533
+ });
466
534
  }
467
535
  else {
468
536
  console.log(chalk.red('❌ Governance decision blocked this change set (policy-only mode).'));
@@ -471,7 +539,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
471
539
  }
472
540
  console.log(chalk.red(` ${message}`));
473
541
  }
474
- await recordVerificationIfRequested(options, config, {
542
+ await recordPolicyOnlyVerification({
475
543
  grade: 'F',
476
544
  violations: [
477
545
  {
@@ -503,7 +571,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
503
571
  message: item.reason,
504
572
  }));
505
573
  if (options.json) {
506
- console.log(JSON.stringify({
574
+ emitPolicyOnlyJson({
507
575
  grade: 'F',
508
576
  score: 0,
509
577
  verdict: 'FAIL',
@@ -519,7 +587,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
519
587
  policyOnly: true,
520
588
  policyOnlySource: source,
521
589
  ...governancePayload,
522
- }, null, 2));
590
+ });
523
591
  }
524
592
  else {
525
593
  console.log(chalk.red('❌ Context policy violation detected (policy-only mode).'));
@@ -528,7 +596,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
528
596
  });
529
597
  console.log(chalk.dim(`\n${message}`));
530
598
  }
531
- await recordVerificationIfRequested(options, config, {
599
+ await recordPolicyOnlyVerification({
532
600
  grade: 'F',
533
601
  violations: contextPolicyViolationItems,
534
602
  verifyResult: {
@@ -617,7 +685,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
617
685
  const message = policyLockMismatchMessage(policyLockEvaluation.mismatches);
618
686
  const lockViolationItems = toPolicyLockViolations(policyLockEvaluation.mismatches);
619
687
  if (options.json) {
620
- console.log(JSON.stringify({
688
+ emitPolicyOnlyJson({
621
689
  grade: 'F',
622
690
  score: 0,
623
691
  verdict: 'FAIL',
@@ -639,7 +707,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
639
707
  path: policyLockEvaluation.lockPath,
640
708
  mismatches: policyLockEvaluation.mismatches,
641
709
  },
642
- }, null, 2));
710
+ });
643
711
  }
644
712
  else {
645
713
  console.log(chalk.red('❌ Policy lock baseline mismatch.'));
@@ -649,7 +717,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
649
717
  });
650
718
  console.log(chalk.dim('\n If drift is intentional, regenerate baseline with `neurcode policy lock`.\n'));
651
719
  }
652
- await recordVerificationIfRequested(options, config, {
720
+ await recordPolicyOnlyVerification({
653
721
  grade: 'F',
654
722
  violations: lockViolationItems,
655
723
  verifyResult: {
@@ -687,6 +755,11 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
687
755
  minApprovals: governance.exceptionApprovals.minApprovals,
688
756
  disallowSelfApproval: governance.exceptionApprovals.disallowSelfApproval,
689
757
  allowedApprovers: governance.exceptionApprovals.allowedApprovers,
758
+ requireReason: governance.exceptionApprovals.requireReason,
759
+ minReasonLength: governance.exceptionApprovals.minReasonLength,
760
+ maxExpiryDays: governance.exceptionApprovals.maxExpiryDays,
761
+ criticalRulePatterns: governance.exceptionApprovals.criticalRulePatterns,
762
+ criticalMinApprovals: governance.exceptionApprovals.criticalMinApprovals,
690
763
  });
691
764
  const suppressedViolations = exceptionDecision.suppressedViolations.filter((item) => !ignoreFilter(item.file));
692
765
  const blockedViolations = exceptionDecision.blockedViolations
@@ -695,7 +768,10 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
695
768
  file: item.file,
696
769
  rule: item.rule,
697
770
  severity: 'block',
698
- message: `Exception ${item.exceptionId} cannot be applied: ${explainExceptionEligibilityReason(item.eligibilityReason)}`,
771
+ message: `Exception ${item.exceptionId} cannot be applied: ${explainExceptionEligibilityReason(item.eligibilityReason)}` +
772
+ (item.requiredApprovals > 0
773
+ ? ` (approvals ${item.effectiveApprovals}/${item.requiredApprovals}${item.critical ? ', critical rule gate' : ''})`
774
+ : ''),
699
775
  ...(item.line != null ? { line: item.line } : {}),
700
776
  }));
701
777
  policyViolations = [
@@ -763,7 +839,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
763
839
  },
764
840
  };
765
841
  if (options.json) {
766
- console.log(JSON.stringify({
842
+ emitPolicyOnlyJson({
767
843
  grade,
768
844
  score,
769
845
  verdict: effectiveVerdict,
@@ -797,7 +873,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
797
873
  },
798
874
  }
799
875
  : {}),
800
- }, null, 2));
876
+ });
801
877
  }
802
878
  else {
803
879
  if (effectiveVerdict === 'PASS') {
@@ -821,7 +897,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
821
897
  displayGovernanceInsights(governanceAnalysis, { explain: options.explain });
822
898
  console.log(chalk.dim(`\n${message}`));
823
899
  }
824
- await recordVerificationIfRequested(options, config, {
900
+ await recordPolicyOnlyVerification({
825
901
  grade,
826
902
  violations: violationsOutput,
827
903
  verifyResult: {
@@ -839,7 +915,108 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
839
915
  }
840
916
  async function verifyCommand(options) {
841
917
  try {
842
- const projectRoot = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
918
+ const rootResolution = (0, project_root_1.resolveNeurcodeProjectRootWithTrace)(process.cwd());
919
+ const projectRoot = rootResolution.projectRoot;
920
+ const scopeTelemetry = (0, scope_telemetry_1.buildScopeTelemetryPayload)(rootResolution);
921
+ const emitVerifyJson = (payload) => {
922
+ const jsonPayload = {
923
+ ...payload,
924
+ scope: scopeTelemetry,
925
+ };
926
+ console.log(JSON.stringify(jsonPayload, null, 2));
927
+ };
928
+ const enforceChangeContract = options.enforceChangeContract === true ||
929
+ isEnabledFlag(process.env.NEURCODE_VERIFY_ENFORCE_CHANGE_CONTRACT);
930
+ const strictArtifactMode = options.strictArtifacts === true ||
931
+ isEnabledFlag(process.env.NEURCODE_VERIFY_STRICT_ARTIFACTS) ||
932
+ isEnabledFlag(process.env.NEURCODE_ENTERPRISE_MODE);
933
+ const changeContractRead = (0, change_contract_1.readChangeContract)(projectRoot, options.changeContract);
934
+ const compiledPolicyRead = (0, policy_compiler_1.readCompiledPolicyArtifact)(projectRoot, options.compiledPolicy);
935
+ let compiledPolicyMetadata = resolveCompiledPolicyMetadata(compiledPolicyRead.artifact, compiledPolicyRead.exists ? compiledPolicyRead.path : null);
936
+ let changeContractSummary = {
937
+ path: changeContractRead.path,
938
+ exists: changeContractRead.exists,
939
+ enforced: enforceChangeContract,
940
+ valid: changeContractRead.contract ? null : changeContractRead.exists ? false : null,
941
+ planId: changeContractRead.contract?.planId || null,
942
+ contractId: changeContractRead.contract?.contractId || null,
943
+ violations: changeContractRead.error
944
+ ? [
945
+ {
946
+ code: 'CHANGE_CONTRACT_PARSE_ERROR',
947
+ message: changeContractRead.error,
948
+ },
949
+ ]
950
+ : [],
951
+ };
952
+ if (strictArtifactMode) {
953
+ const strictErrors = [];
954
+ if (!compiledPolicyRead.artifact) {
955
+ strictErrors.push(compiledPolicyRead.error
956
+ ? `Compiled policy artifact invalid (${compiledPolicyRead.error})`
957
+ : `Compiled policy artifact missing (${compiledPolicyRead.path})`);
958
+ }
959
+ if (!changeContractRead.contract) {
960
+ strictErrors.push(changeContractRead.error
961
+ ? `Change contract artifact invalid (${changeContractRead.error})`
962
+ : `Change contract artifact missing (${changeContractRead.path})`);
963
+ }
964
+ if (strictErrors.length > 0) {
965
+ const message = `Strict artifact mode requires deterministic compiled-policy + change-contract artifacts.\n- ${strictErrors.join('\n- ')}`;
966
+ if (options.json) {
967
+ emitVerifyJson({
968
+ grade: 'F',
969
+ score: 0,
970
+ verdict: 'FAIL',
971
+ violations: strictErrors.map((entry) => ({
972
+ file: entry.toLowerCase().includes('compiled policy') ? compiledPolicyRead.path : changeContractRead.path,
973
+ rule: 'deterministic_artifacts_required',
974
+ severity: 'block',
975
+ message: entry,
976
+ })),
977
+ adherenceScore: 0,
978
+ bloatCount: 0,
979
+ bloatFiles: [],
980
+ plannedFilesModified: 0,
981
+ totalPlannedFiles: 0,
982
+ message,
983
+ scopeGuardPassed: false,
984
+ mode: 'strict_artifacts_required',
985
+ policyOnly: options.policyOnly === true,
986
+ changeContract: changeContractSummary,
987
+ ...(compiledPolicyMetadata ? { policyCompilation: compiledPolicyMetadata } : {}),
988
+ });
989
+ }
990
+ else {
991
+ (0, scope_telemetry_1.printScopeTelemetry)(chalk, scopeTelemetry, {
992
+ includeBlockedWarning: true,
993
+ });
994
+ console.log(chalk.red('\n⛔ Deterministic Artifact Requirements Failed'));
995
+ strictErrors.forEach((entry) => {
996
+ console.log(chalk.red(` • ${entry}`));
997
+ });
998
+ console.log(chalk.dim('\nSet --compiled-policy and --change-contract with valid artifacts before verify.\n'));
999
+ }
1000
+ process.exit(2);
1001
+ }
1002
+ }
1003
+ if (!options.json) {
1004
+ (0, scope_telemetry_1.printScopeTelemetry)(chalk, scopeTelemetry, {
1005
+ includeBlockedWarning: true,
1006
+ });
1007
+ if (compiledPolicyRead.error) {
1008
+ console.log(chalk.yellow(` Compiled policy artifact unavailable (${compiledPolicyRead.error}); falling back to runtime compilation`));
1009
+ }
1010
+ else if (compiledPolicyRead.artifact) {
1011
+ console.log(chalk.dim(` Compiled policy loaded: ${compiledPolicyRead.path} (${compiledPolicyRead.artifact.compilation.deterministicRuleCount} deterministic rules)`));
1012
+ }
1013
+ if (changeContractRead.error) {
1014
+ console.log(chalk.yellow(` Change contract unavailable (${changeContractRead.error})`));
1015
+ }
1016
+ else if (changeContractRead.contract) {
1017
+ console.log(chalk.dim(` Change contract loaded: ${changeContractRead.path}`));
1018
+ }
1019
+ }
843
1020
  // Load configuration
844
1021
  const config = (0, config_1.loadConfig)();
845
1022
  // 🛑 FORCE PRIORITY: Env Var > Config
@@ -903,6 +1080,9 @@ async function verifyCommand(options) {
903
1080
  requireSignedAiLogs: remoteSettings.requireSignedAiLogs === true,
904
1081
  requireManualApproval: remoteSettings.requireManualApproval !== false,
905
1082
  minimumManualApprovals: Math.max(1, Math.min(5, Math.floor(remoteSettings.minimumManualApprovals || 1))),
1083
+ ...(remoteSettings.policyGovernance && typeof remoteSettings.policyGovernance === 'object'
1084
+ ? { policyGovernance: remoteSettings.policyGovernance }
1085
+ : {}),
906
1086
  updatedAt: remoteSettings.updatedAt,
907
1087
  };
908
1088
  }
@@ -941,7 +1121,7 @@ async function verifyCommand(options) {
941
1121
  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
1122
  recordVerifyEvent('FAIL', 'missing_signing_key_material');
943
1123
  if (options.json) {
944
- console.log(JSON.stringify({
1124
+ emitVerifyJson({
945
1125
  grade: 'F',
946
1126
  score: 0,
947
1127
  verdict: 'FAIL',
@@ -962,7 +1142,7 @@ async function verifyCommand(options) {
962
1142
  scopeGuardPassed: false,
963
1143
  mode: 'plan_enforced',
964
1144
  policyOnly: false,
965
- }, null, 2));
1145
+ });
966
1146
  }
967
1147
  else {
968
1148
  console.log(chalk.red('\n⛔ Governance Signing Key Missing'));
@@ -1021,7 +1201,7 @@ async function verifyCommand(options) {
1021
1201
  console.log(chalk.dim(' Make sure you have staged or unstaged changes to verify'));
1022
1202
  }
1023
1203
  else {
1024
- console.log(JSON.stringify({
1204
+ emitVerifyJson({
1025
1205
  grade: 'F',
1026
1206
  score: 0,
1027
1207
  verdict: 'FAIL',
@@ -1033,7 +1213,7 @@ async function verifyCommand(options) {
1033
1213
  totalPlannedFiles: 0,
1034
1214
  message: 'No changes detected',
1035
1215
  scopeGuardPassed: false,
1036
- }, null, 2));
1216
+ });
1037
1217
  }
1038
1218
  recordVerifyEvent('NO_CHANGES', 'diff=empty');
1039
1219
  process.exit(0);
@@ -1064,7 +1244,7 @@ async function verifyCommand(options) {
1064
1244
  console.log(chalk.yellow('⚠️ No file changes detected in diff'));
1065
1245
  }
1066
1246
  else {
1067
- console.log(JSON.stringify({
1247
+ emitVerifyJson({
1068
1248
  grade: 'F',
1069
1249
  score: 0,
1070
1250
  verdict: 'FAIL',
@@ -1076,7 +1256,7 @@ async function verifyCommand(options) {
1076
1256
  totalPlannedFiles: 0,
1077
1257
  message: 'No file changes detected in diff',
1078
1258
  scopeGuardPassed: false,
1079
- }, null, 2));
1259
+ });
1080
1260
  }
1081
1261
  recordVerifyEvent('NO_CHANGES', 'diff_files=0');
1082
1262
  process.exit(0);
@@ -1106,7 +1286,10 @@ async function verifyCommand(options) {
1106
1286
  signingKeys: aiLogSigningKeys,
1107
1287
  signer: aiLogSigner,
1108
1288
  });
1109
- const baselineGovernancePayload = buildGovernancePayload(baselineGovernance, orgGovernanceSettings);
1289
+ const baselineGovernancePayload = buildGovernancePayload(baselineGovernance, orgGovernanceSettings, {
1290
+ changeContract: changeContractSummary,
1291
+ compiledPolicy: compiledPolicyMetadata,
1292
+ });
1110
1293
  const message = `Context access policy violation: ${baselineContextViolations.map((item) => item.file).join(', ')}`;
1111
1294
  const baselineContextViolationItems = baselineContextViolations.map((item) => ({
1112
1295
  file: item.file,
@@ -1116,7 +1299,7 @@ async function verifyCommand(options) {
1116
1299
  }));
1117
1300
  recordVerifyEvent('FAIL', `context_policy_violations=${baselineContextViolations.length}`, diffFiles.map((f) => f.path));
1118
1301
  if (options.json) {
1119
- console.log(JSON.stringify({
1302
+ emitVerifyJson({
1120
1303
  grade: 'F',
1121
1304
  score: 0,
1122
1305
  verdict: 'FAIL',
@@ -1131,7 +1314,7 @@ async function verifyCommand(options) {
1131
1314
  ...baselineGovernancePayload,
1132
1315
  mode: 'policy_violation',
1133
1316
  policyOnly: false,
1134
- }, null, 2));
1317
+ });
1135
1318
  }
1136
1319
  else {
1137
1320
  console.log(chalk.red('\n⛔ Context Policy Violation'));
@@ -1163,7 +1346,7 @@ async function verifyCommand(options) {
1163
1346
  console.log(chalk.dim(` ${summary.totalAdded} lines added, ${summary.totalRemoved} lines removed\n`));
1164
1347
  }
1165
1348
  const runPolicyOnlyModeAndExit = async (source) => {
1166
- const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client, source, projectId || undefined, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner);
1349
+ const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client, source, scopeTelemetry, projectId || undefined, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner, compiledPolicyMetadata, changeContractSummary);
1167
1350
  const changedFiles = diffFiles.map((f) => f.path);
1168
1351
  const verdict = exitCode === 2 ? 'FAIL' : exitCode === 1 ? 'WARN' : 'PASS';
1169
1352
  recordVerifyEvent(verdict, `policy_only_source=${source};exit=${exitCode}`, changedFiles);
@@ -1215,7 +1398,7 @@ async function verifyCommand(options) {
1215
1398
  const message = 'Plan ID is required in strict mode. Run "neurcode plan" first or pass --plan-id.';
1216
1399
  recordVerifyEvent('FAIL', 'missing_plan_id;require_plan=true', changedFiles);
1217
1400
  if (options.json) {
1218
- console.log(JSON.stringify({
1401
+ emitVerifyJson({
1219
1402
  grade: 'F',
1220
1403
  score: 0,
1221
1404
  verdict: 'FAIL',
@@ -1229,7 +1412,7 @@ async function verifyCommand(options) {
1229
1412
  scopeGuardPassed: false,
1230
1413
  mode: 'plan_required',
1231
1414
  policyOnly: false,
1232
- }, null, 2));
1415
+ });
1233
1416
  }
1234
1417
  else {
1235
1418
  console.log(chalk.red('❌ Plan ID is required in strict mode.'));
@@ -1269,6 +1452,8 @@ async function verifyCommand(options) {
1269
1452
  // Track if scope guard passed - this takes priority over AI grading
1270
1453
  let scopeGuardPassed = false;
1271
1454
  let governanceResult = null;
1455
+ let planFilesForVerification = [];
1456
+ let intentConstraintsForVerification;
1272
1457
  try {
1273
1458
  // Step A: Get Modified Files (already have from diffFiles)
1274
1459
  const modifiedFiles = diffFiles.map(f => f.path);
@@ -1285,6 +1470,8 @@ async function verifyCommand(options) {
1285
1470
  const planFiles = planData.content.files
1286
1471
  .filter(f => f.action === 'CREATE' || f.action === 'MODIFY')
1287
1472
  .map(f => f.path);
1473
+ planFilesForVerification = [...planFiles];
1474
+ intentConstraintsForVerification = originalIntent || undefined;
1288
1475
  const planDependencies = Array.isArray(planData.content.dependencies)
1289
1476
  ? planData.content.dependencies.filter((item) => typeof item === 'string')
1290
1477
  : [];
@@ -1371,11 +1558,14 @@ async function verifyCommand(options) {
1371
1558
  mode: 'plan_enforced',
1372
1559
  policyOnly: false,
1373
1560
  ...(governanceResult
1374
- ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1561
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings, {
1562
+ changeContract: changeContractSummary,
1563
+ compiledPolicy: compiledPolicyMetadata,
1564
+ })
1375
1565
  : {}),
1376
1566
  };
1377
1567
  // CRITICAL: Print JSON first, then exit
1378
- console.log(JSON.stringify(jsonOutput, null, 2));
1568
+ emitVerifyJson(jsonOutput);
1379
1569
  await recordVerificationIfRequested(options, config, {
1380
1570
  grade: 'F',
1381
1571
  violations: scopeViolationItems,
@@ -1389,7 +1579,10 @@ async function verifyCommand(options) {
1389
1579
  projectId: projectId || undefined,
1390
1580
  jsonMode: true,
1391
1581
  governance: governanceResult
1392
- ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1582
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings, {
1583
+ changeContract: changeContractSummary,
1584
+ compiledPolicy: compiledPolicyMetadata,
1585
+ })
1393
1586
  : undefined,
1394
1587
  });
1395
1588
  process.exit(1);
@@ -1425,7 +1618,10 @@ async function verifyCommand(options) {
1425
1618
  projectId: projectId || undefined,
1426
1619
  jsonMode: false,
1427
1620
  governance: governanceResult
1428
- ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1621
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings, {
1622
+ changeContract: changeContractSummary,
1623
+ compiledPolicy: compiledPolicyMetadata,
1624
+ })
1429
1625
  : undefined,
1430
1626
  });
1431
1627
  process.exit(1);
@@ -1532,8 +1728,13 @@ async function verifyCommand(options) {
1532
1728
  scopeGuardPassed,
1533
1729
  mode: 'plan_enforced',
1534
1730
  policyOnly: false,
1731
+ changeContract: changeContractSummary,
1732
+ ...(compiledPolicyMetadata ? { policyCompilation: compiledPolicyMetadata } : {}),
1535
1733
  ...(governanceResult
1536
- ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1734
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings, {
1735
+ changeContract: changeContractSummary,
1736
+ compiledPolicy: compiledPolicyMetadata,
1737
+ })
1537
1738
  : {}),
1538
1739
  policyLock: {
1539
1740
  enforced: true,
@@ -1564,14 +1765,41 @@ async function verifyCommand(options) {
1564
1765
  projectId: projectId || undefined,
1565
1766
  jsonMode: Boolean(options.json),
1566
1767
  governance: governanceResult
1567
- ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1768
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings, {
1769
+ changeContract: changeContractSummary,
1770
+ compiledPolicy: compiledPolicyMetadata,
1771
+ })
1568
1772
  : undefined,
1569
1773
  });
1570
1774
  process.exit(2);
1571
1775
  }
1572
1776
  }
1777
+ let effectiveCompiledPolicy = compiledPolicyRead.artifact;
1778
+ if (!effectiveCompiledPolicy) {
1779
+ effectiveCompiledPolicy = buildCompiledPolicyFromEffectiveRules({
1780
+ projectRoot,
1781
+ policyLockEvaluation,
1782
+ effectiveRules,
1783
+ intentConstraints: intentConstraintsForVerification,
1784
+ });
1785
+ compiledPolicyMetadata = resolveCompiledPolicyMetadata(effectiveCompiledPolicy, (0, policy_compiler_1.resolveCompiledPolicyPath)(projectRoot, options.compiledPolicy));
1786
+ }
1787
+ const hydratedCompiledPolicyRules = effectiveCompiledPolicy
1788
+ ? (0, policy_compiler_1.hydrateCompiledPolicyRules)(effectiveCompiledPolicy)
1789
+ : [];
1790
+ if (effectiveCompiledPolicy?.source.policyLockFingerprint &&
1791
+ policyLockEvaluation.lockPresent &&
1792
+ effectiveCompiledPolicy.source.policyLockFingerprint !== (0, policy_packs_1.readPolicyLockFile)(projectRoot).lock?.effective.fingerprint) {
1793
+ if (!options.json) {
1794
+ console.log(chalk.yellow(' Compiled policy lock fingerprint differs from current lock; runtime checks will continue with latest lock state'));
1795
+ }
1796
+ }
1573
1797
  // Check user tier - Policy Compliance and A-F Grading are PRO features
1574
- const governance = (0, policy_governance_1.readPolicyGovernanceConfig)(projectRoot);
1798
+ const localPolicyGovernance = (0, policy_governance_1.readPolicyGovernanceConfig)(projectRoot);
1799
+ const governance = (0, policy_governance_1.mergePolicyGovernanceWithOrgOverrides)(localPolicyGovernance, orgGovernanceSettings?.policyGovernance);
1800
+ if (!options.json && orgGovernanceSettings?.policyGovernance) {
1801
+ console.log(chalk.dim(' Org policy governance controls active: local config merged with org-level enforcement floor'));
1802
+ }
1575
1803
  const auditIntegrity = (0, policy_audit_1.verifyPolicyAuditIntegrity)(projectRoot);
1576
1804
  const auditIntegrityStatus = resolveAuditIntegrityStatus(governance.audit.requireIntegrity, auditIntegrity);
1577
1805
  const { getUserTier } = await Promise.resolve().then(() => __importStar(require('../utils/tier')));
@@ -1589,7 +1817,7 @@ async function verifyCommand(options) {
1589
1817
  console.log(chalk.dim(' Upgrade at: https://www.neurcode.com/dashboard/purchase-plan\n'));
1590
1818
  }
1591
1819
  else {
1592
- console.log(JSON.stringify({
1820
+ emitVerifyJson({
1593
1821
  grade: 'N/A',
1594
1822
  score: 0,
1595
1823
  verdict: 'INFO',
@@ -1604,8 +1832,13 @@ async function verifyCommand(options) {
1604
1832
  mode: 'plan_enforced',
1605
1833
  policyOnly: false,
1606
1834
  tier: 'FREE',
1835
+ changeContract: changeContractSummary,
1836
+ ...(compiledPolicyMetadata ? { policyCompilation: compiledPolicyMetadata } : {}),
1607
1837
  ...(governanceResult
1608
- ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
1838
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings, {
1839
+ changeContract: changeContractSummary,
1840
+ compiledPolicy: compiledPolicyMetadata,
1841
+ })
1609
1842
  : {}),
1610
1843
  policyLock: {
1611
1844
  enforced: policyLockEvaluation.enforced,
@@ -1623,7 +1856,7 @@ async function verifyCommand(options) {
1623
1856
  eventCount: auditIntegrity.count,
1624
1857
  },
1625
1858
  },
1626
- }, null, 2));
1859
+ });
1627
1860
  }
1628
1861
  process.exit(0);
1629
1862
  }
@@ -1638,6 +1871,11 @@ async function verifyCommand(options) {
1638
1871
  minApprovals: governance.exceptionApprovals.minApprovals,
1639
1872
  disallowSelfApproval: governance.exceptionApprovals.disallowSelfApproval,
1640
1873
  allowedApprovers: governance.exceptionApprovals.allowedApprovers,
1874
+ requireReason: governance.exceptionApprovals.requireReason,
1875
+ minReasonLength: governance.exceptionApprovals.minReasonLength,
1876
+ maxExpiryDays: governance.exceptionApprovals.maxExpiryDays,
1877
+ criticalRulePatterns: governance.exceptionApprovals.criticalRulePatterns,
1878
+ criticalMinApprovals: governance.exceptionApprovals.criticalMinApprovals,
1641
1879
  });
1642
1880
  const suppressedPolicyViolations = exceptionDecision.suppressedViolations.filter((item) => !shouldIgnore(item.file));
1643
1881
  const blockedPolicyViolations = exceptionDecision.blockedViolations
@@ -1646,7 +1884,10 @@ async function verifyCommand(options) {
1646
1884
  file: item.file,
1647
1885
  rule: item.rule,
1648
1886
  severity: 'block',
1649
- message: `Exception ${item.exceptionId} cannot be applied: ${explainExceptionEligibilityReason(item.eligibilityReason)}`,
1887
+ message: `Exception ${item.exceptionId} cannot be applied: ${explainExceptionEligibilityReason(item.eligibilityReason)}` +
1888
+ (item.requiredApprovals > 0
1889
+ ? ` (approvals ${item.effectiveApprovals}/${item.requiredApprovals}${item.critical ? ', critical rule gate' : ''})`
1890
+ : ''),
1650
1891
  ...(item.line != null ? { line: item.line } : {}),
1651
1892
  }));
1652
1893
  policyViolations = [
@@ -1729,22 +1970,146 @@ async function verifyCommand(options) {
1729
1970
  })),
1730
1971
  })),
1731
1972
  }));
1973
+ const changeContractEvaluation = changeContractRead.contract
1974
+ ? (0, change_contract_1.evaluateChangeContract)(changeContractRead.contract, {
1975
+ planId: finalPlanId,
1976
+ changedFiles: changedFiles.map((file) => file.path),
1977
+ policyLockFingerprint: (0, policy_packs_1.readPolicyLockFile)(projectRoot).lock?.effective.fingerprint || null,
1978
+ compiledPolicyFingerprint: effectiveCompiledPolicy?.fingerprint || null,
1979
+ })
1980
+ : null;
1981
+ if (changeContractEvaluation) {
1982
+ changeContractSummary = {
1983
+ path: changeContractRead.path,
1984
+ exists: true,
1985
+ enforced: enforceChangeContract,
1986
+ valid: changeContractEvaluation.valid,
1987
+ planId: changeContractRead.contract?.planId || null,
1988
+ contractId: changeContractRead.contract?.contractId || null,
1989
+ coverage: changeContractEvaluation.coverage,
1990
+ violations: changeContractEvaluation.violations.map((item) => ({
1991
+ code: item.code,
1992
+ message: item.message,
1993
+ file: item.file,
1994
+ expected: item.expected,
1995
+ actual: item.actual,
1996
+ })),
1997
+ };
1998
+ if (!changeContractEvaluation.valid && enforceChangeContract) {
1999
+ const violations = changeContractEvaluation.violations.map((item) => ({
2000
+ file: item.file || '.neurcode/change-contract.json',
2001
+ rule: `change_contract:${item.code.toLowerCase()}`,
2002
+ severity: 'block',
2003
+ message: item.message,
2004
+ }));
2005
+ const message = `Change contract enforcement failed: ${changeContractEvaluation.violations
2006
+ .map((item) => item.message)
2007
+ .join('; ')}`;
2008
+ if (options.json) {
2009
+ emitVerifyJson({
2010
+ grade: 'F',
2011
+ score: 0,
2012
+ verdict: 'FAIL',
2013
+ violations,
2014
+ adherenceScore: 0,
2015
+ bloatCount: 0,
2016
+ bloatFiles: [],
2017
+ plannedFilesModified: 0,
2018
+ totalPlannedFiles: 0,
2019
+ message,
2020
+ scopeGuardPassed: false,
2021
+ mode: 'plan_enforced',
2022
+ policyOnly: false,
2023
+ changeContract: changeContractSummary,
2024
+ ...(compiledPolicyMetadata ? { policyCompilation: compiledPolicyMetadata } : {}),
2025
+ });
2026
+ }
2027
+ else {
2028
+ console.log(chalk.red('\n⛔ Change contract enforcement failed'));
2029
+ changeContractEvaluation.violations.forEach((item) => {
2030
+ console.log(chalk.red(` • ${item.message}`));
2031
+ });
2032
+ console.log(chalk.dim(` Contract path: ${changeContractRead.path}`));
2033
+ }
2034
+ await recordVerificationIfRequested(options, config, {
2035
+ grade: 'F',
2036
+ violations,
2037
+ verifyResult: {
2038
+ adherenceScore: 0,
2039
+ verdict: 'FAIL',
2040
+ bloatCount: 0,
2041
+ bloatFiles: [],
2042
+ message,
2043
+ },
2044
+ projectId: projectId || undefined,
2045
+ jsonMode: Boolean(options.json),
2046
+ governance: governanceResult
2047
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings, {
2048
+ changeContract: changeContractSummary,
2049
+ compiledPolicy: compiledPolicyMetadata,
2050
+ })
2051
+ : undefined,
2052
+ });
2053
+ process.exit(2);
2054
+ }
2055
+ else if (!changeContractEvaluation.valid && !options.json) {
2056
+ console.log(chalk.yellow('\n⚠️ Change contract drift detected (advisory mode)'));
2057
+ changeContractEvaluation.violations.slice(0, 5).forEach((item) => {
2058
+ console.log(chalk.yellow(` • ${item.message}`));
2059
+ });
2060
+ if (changeContractEvaluation.violations.length > 5) {
2061
+ console.log(chalk.dim(` ... ${changeContractEvaluation.violations.length - 5} more violation(s)`));
2062
+ }
2063
+ }
2064
+ }
1732
2065
  // Call verify API
1733
2066
  if (!options.json) {
1734
2067
  console.log(chalk.dim(' Sending to Neurcode API...\n'));
1735
2068
  }
1736
2069
  try {
1737
- // Extract original intent from plan for constraint checking
1738
- let intentConstraints;
2070
+ let verifySource = 'api';
2071
+ let verifyResult;
2072
+ const deterministicPolicyRules = effectiveCompiledPolicy
2073
+ ? [...effectiveCompiledPolicy.statements.policyRules]
2074
+ : effectiveRules.customPolicies
2075
+ .map((policy) => policy.rule_text)
2076
+ .filter((ruleText) => typeof ruleText === 'string' && ruleText.trim().length > 0);
1739
2077
  try {
1740
- const planData = await client.getPlan(finalPlanId);
1741
- intentConstraints = planData.intent || undefined;
2078
+ verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraintsForVerification, deterministicPolicyRules, 'api', compiledPolicyMetadata);
1742
2079
  }
1743
- catch {
1744
- // If we can't get plan, continue without constraints
2080
+ catch (verifyApiError) {
2081
+ if (planFilesForVerification.length === 0) {
2082
+ throw verifyApiError;
2083
+ }
2084
+ verifySource = 'local_fallback';
2085
+ if (!options.json) {
2086
+ const fallbackReason = verifyApiError instanceof Error ? verifyApiError.message : String(verifyApiError);
2087
+ console.log(chalk.yellow('⚠️ Verify API unavailable, using local deterministic fallback.'));
2088
+ console.log(chalk.dim(` Reason: ${fallbackReason}`));
2089
+ }
2090
+ const localEvaluation = (0, governance_runtime_1.evaluatePlanVerification)({
2091
+ planFiles: planFilesForVerification.map((path) => ({
2092
+ path,
2093
+ action: 'MODIFY',
2094
+ })),
2095
+ changedFiles,
2096
+ diffStats,
2097
+ intentConstraints: intentConstraintsForVerification,
2098
+ policyRules: deterministicPolicyRules,
2099
+ extraConstraintRules: hydratedCompiledPolicyRules.length > 0 ? hydratedCompiledPolicyRules : undefined,
2100
+ });
2101
+ verifyResult = {
2102
+ verificationId: `local-fallback-${Date.now()}`,
2103
+ adherenceScore: localEvaluation.adherenceScore,
2104
+ bloatCount: localEvaluation.bloatCount,
2105
+ bloatFiles: localEvaluation.bloatFiles,
2106
+ plannedFilesModified: localEvaluation.plannedFilesModified,
2107
+ totalPlannedFiles: localEvaluation.totalPlannedFiles,
2108
+ verdict: localEvaluation.verdict,
2109
+ diffSummary: localEvaluation.diffSummary,
2110
+ message: localEvaluation.message,
2111
+ };
1745
2112
  }
1746
- // Call verifyPlan with intentConstraints
1747
- const verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraints);
1748
2113
  // Apply custom policy verdict: block from dashboard overrides API verdict
1749
2114
  const policyBlock = policyDecision === 'block' && policyViolations.length > 0;
1750
2115
  const governanceDecisionBlock = governanceResult?.governanceDecision?.decision === 'block';
@@ -1846,10 +2211,16 @@ async function verifyCommand(options) {
1846
2211
  bloatFiles: filteredBloatFiles,
1847
2212
  plannedFilesModified: verifyResult.plannedFilesModified,
1848
2213
  totalPlannedFiles: verifyResult.totalPlannedFiles,
2214
+ verificationSource: verifySource,
1849
2215
  mode: 'plan_enforced',
1850
2216
  policyOnly: false,
2217
+ changeContract: changeContractSummary,
2218
+ ...(compiledPolicyMetadata ? { policyCompilation: compiledPolicyMetadata } : {}),
1851
2219
  ...(governanceResult
1852
- ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
2220
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings, {
2221
+ changeContract: changeContractSummary,
2222
+ compiledPolicy: compiledPolicyMetadata,
2223
+ })
1853
2224
  : {}),
1854
2225
  policyLock: {
1855
2226
  enforced: policyLockEvaluation.enforced,
@@ -1871,7 +2242,7 @@ async function verifyCommand(options) {
1871
2242
  }
1872
2243
  : {}),
1873
2244
  };
1874
- console.log(JSON.stringify(jsonOutput, null, 2));
2245
+ emitVerifyJson(jsonOutput);
1875
2246
  await recordVerificationIfRequested(options, config, {
1876
2247
  grade,
1877
2248
  violations: violations,
@@ -1881,11 +2252,16 @@ async function verifyCommand(options) {
1881
2252
  bloatCount: filteredBloatFiles.length,
1882
2253
  bloatFiles: filteredBloatFiles,
1883
2254
  message: effectiveMessage,
2255
+ verificationSource: verifySource,
1884
2256
  },
1885
2257
  projectId: projectId || undefined,
1886
2258
  jsonMode: true,
2259
+ verificationSource: verifySource,
1887
2260
  governance: governanceResult
1888
- ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
2261
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings, {
2262
+ changeContract: changeContractSummary,
2263
+ compiledPolicy: compiledPolicyMetadata,
2264
+ })
1889
2265
  : undefined,
1890
2266
  });
1891
2267
  // Exit based on effective verdict (same logic as below)
@@ -1960,11 +2336,16 @@ async function verifyCommand(options) {
1960
2336
  bloatCount: filteredBloatForReport.length,
1961
2337
  bloatFiles: filteredBloatForReport,
1962
2338
  message: effectiveMessage,
2339
+ verificationSource: verifySource,
1963
2340
  },
1964
2341
  projectId: projectId || undefined,
1965
2342
  jsonMode: false,
2343
+ verificationSource: verifySource,
1966
2344
  governance: governanceResult
1967
- ? buildGovernancePayload(governanceResult, orgGovernanceSettings)
2345
+ ? buildGovernancePayload(governanceResult, orgGovernanceSettings, {
2346
+ changeContract: changeContractSummary,
2347
+ compiledPolicy: compiledPolicyMetadata,
2348
+ })
1968
2349
  : undefined,
1969
2350
  });
1970
2351
  // Governance override: keep PASS only when scope guard passes and failure is due
@@ -2001,7 +2382,7 @@ async function verifyCommand(options) {
2001
2382
  recordVerifyEvent('FAIL', `verify_api_error=${error instanceof Error ? error.message : 'unknown'}`, changedFiles, finalPlanId);
2002
2383
  if (options.json) {
2003
2384
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
2004
- console.log(JSON.stringify({
2385
+ emitVerifyJson({
2005
2386
  grade: 'F',
2006
2387
  score: 0,
2007
2388
  verdict: 'FAIL',
@@ -2013,7 +2394,7 @@ async function verifyCommand(options) {
2013
2394
  totalPlannedFiles: 0,
2014
2395
  message: `Error: ${errorMessage}`,
2015
2396
  scopeGuardPassed: false,
2016
- }, null, 2));
2397
+ });
2017
2398
  }
2018
2399
  else {
2019
2400
  if (error instanceof Error) {
@@ -2048,6 +2429,14 @@ async function verifyCommand(options) {
2048
2429
  totalPlannedFiles: 0,
2049
2430
  message: `Unexpected error: ${errorMessage}`,
2050
2431
  scopeGuardPassed: false,
2432
+ scope: {
2433
+ scanRoot: process.cwd(),
2434
+ startDir: process.cwd(),
2435
+ gitRoot: null,
2436
+ linkedRepoOverrideUsed: false,
2437
+ linkedRepos: [],
2438
+ blockedOverride: null,
2439
+ },
2051
2440
  }, null, 2));
2052
2441
  }
2053
2442
  else {
@@ -2212,7 +2601,7 @@ function buildCompactVerificationPayload(payload) {
2212
2601
  /**
2213
2602
  * Report verification results to Neurcode Cloud
2214
2603
  */
2215
- async function reportVerification(grade, violations, verifyResult, apiKey, apiUrl, projectId, jsonMode, governance) {
2604
+ async function reportVerification(grade, violations, verifyResult, apiKey, apiUrl, projectId, jsonMode, governance, verificationSource) {
2216
2605
  try {
2217
2606
  const ciContext = collectCIContext();
2218
2607
  const payload = {
@@ -2229,6 +2618,7 @@ async function reportVerification(grade, violations, verifyResult, apiKey, apiUr
2229
2618
  workflowRunId: ciContext.workflowRunId,
2230
2619
  projectId,
2231
2620
  governance,
2621
+ verificationSource: verificationSource || verifyResult.verificationSource || 'api',
2232
2622
  };
2233
2623
  const postPayload = async (requestPayload) => fetch(`${apiUrl}/api/v1/action/verifications`, {
2234
2624
  method: 'POST',
@@ -2271,7 +2661,7 @@ async function reportVerification(grade, violations, verifyResult, apiKey, apiUr
2271
2661
  }
2272
2662
  }
2273
2663
  }
2274
- function buildGovernancePayload(governance, orgGovernanceSettings) {
2664
+ function buildGovernancePayload(governance, orgGovernanceSettings, options) {
2275
2665
  return {
2276
2666
  contextPolicy: governance.contextPolicy,
2277
2667
  blastRadius: governance.blastRadius,
@@ -2289,9 +2679,14 @@ function buildGovernancePayload(governance, orgGovernanceSettings) {
2289
2679
  requireSignedAiLogs: orgGovernanceSettings.requireSignedAiLogs,
2290
2680
  requireManualApproval: orgGovernanceSettings.requireManualApproval,
2291
2681
  minimumManualApprovals: orgGovernanceSettings.minimumManualApprovals,
2682
+ ...(orgGovernanceSettings.policyGovernance
2683
+ ? { policyGovernance: orgGovernanceSettings.policyGovernance }
2684
+ : {}),
2292
2685
  updatedAt: orgGovernanceSettings.updatedAt || null,
2293
2686
  }
2294
2687
  : null,
2688
+ ...(options?.compiledPolicy ? { policyCompilation: options.compiledPolicy } : {}),
2689
+ ...(options?.changeContract ? { changeContract: options.changeContract } : {}),
2295
2690
  };
2296
2691
  }
2297
2692
  function displayGovernanceInsights(governance, options = {}) {