@neurcode-ai/cli 0.9.48 → 0.9.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/fix.d.ts +13 -0
- package/dist/commands/fix.d.ts.map +1 -0
- package/dist/commands/fix.js +785 -0
- package/dist/commands/fix.js.map +1 -0
- package/dist/commands/generate.d.ts +7 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +132 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/patch-apply.d.ts +7 -0
- package/dist/commands/patch-apply.d.ts.map +1 -0
- package/dist/commands/patch-apply.js +85 -0
- package/dist/commands/patch-apply.js.map +1 -0
- package/dist/commands/plan-show.d.ts +6 -0
- package/dist/commands/plan-show.d.ts.map +1 -0
- package/dist/commands/plan-show.js +33 -0
- package/dist/commands/plan-show.js.map +1 -0
- package/dist/commands/start-intent.d.ts +6 -0
- package/dist/commands/start-intent.d.ts.map +1 -0
- package/dist/commands/start-intent.js +80 -0
- package/dist/commands/start-intent.js.map +1 -0
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +703 -186
- package/dist/commands/verify.js.map +1 -1
- package/dist/context-engine/graph.d.ts +6 -0
- package/dist/context-engine/graph.d.ts.map +1 -0
- package/dist/context-engine/graph.js +55 -0
- package/dist/context-engine/graph.js.map +1 -0
- package/dist/context-engine/index.d.ts +14 -0
- package/dist/context-engine/index.d.ts.map +1 -0
- package/dist/context-engine/index.js +26 -0
- package/dist/context-engine/index.js.map +1 -0
- package/dist/context-engine/scanner.d.ts +6 -0
- package/dist/context-engine/scanner.d.ts.map +1 -0
- package/dist/context-engine/scanner.js +62 -0
- package/dist/context-engine/scanner.js.map +1 -0
- package/dist/context-engine/scorer.d.ts +9 -0
- package/dist/context-engine/scorer.d.ts.map +1 -0
- package/dist/context-engine/scorer.js +112 -0
- package/dist/context-engine/scorer.js.map +1 -0
- package/dist/context-engine/suggestions.d.ts +12 -0
- package/dist/context-engine/suggestions.d.ts.map +1 -0
- package/dist/context-engine/suggestions.js +22 -0
- package/dist/context-engine/suggestions.js.map +1 -0
- package/dist/index.js +129 -55
- package/dist/index.js.map +1 -1
- package/dist/mcp/context-injector.d.ts +45 -0
- package/dist/mcp/context-injector.d.ts.map +1 -0
- package/dist/mcp/context-injector.js +587 -0
- package/dist/mcp/context-injector.js.map +1 -0
- package/dist/mcp/proximity.d.ts +3 -0
- package/dist/mcp/proximity.d.ts.map +1 -0
- package/dist/mcp/proximity.js +135 -0
- package/dist/mcp/proximity.js.map +1 -0
- package/dist/patch-engine/diff.d.ts +12 -0
- package/dist/patch-engine/diff.d.ts.map +1 -0
- package/dist/patch-engine/diff.js +74 -0
- package/dist/patch-engine/diff.js.map +1 -0
- package/dist/patch-engine/generator.d.ts +13 -0
- package/dist/patch-engine/generator.d.ts.map +1 -0
- package/dist/patch-engine/generator.js +51 -0
- package/dist/patch-engine/generator.js.map +1 -0
- package/dist/patch-engine/index.d.ts +47 -0
- package/dist/patch-engine/index.d.ts.map +1 -0
- package/dist/patch-engine/index.js +182 -0
- package/dist/patch-engine/index.js.map +1 -0
- package/dist/patch-engine/patterns.d.ts +4 -0
- package/dist/patch-engine/patterns.d.ts.map +1 -0
- package/dist/patch-engine/patterns.js +99 -0
- package/dist/patch-engine/patterns.js.map +1 -0
- package/dist/utils/git.d.ts +8 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +55 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/plan-sync.d.ts +34 -0
- package/dist/utils/plan-sync.d.ts.map +1 -0
- package/dist/utils/plan-sync.js +265 -0
- package/dist/utils/plan-sync.js.map +1 -0
- package/package.json +1 -1
package/dist/commands/verify.js
CHANGED
|
@@ -55,6 +55,7 @@ const ignore_1 = require("../utils/ignore");
|
|
|
55
55
|
const project_root_1 = require("../utils/project-root");
|
|
56
56
|
const brain_context_1 = require("../utils/brain-context");
|
|
57
57
|
const scope_telemetry_1 = require("../utils/scope-telemetry");
|
|
58
|
+
const plan_sync_1 = require("../utils/plan-sync");
|
|
58
59
|
const policy_packs_1 = require("../utils/policy-packs");
|
|
59
60
|
const custom_policy_rules_1 = require("../utils/custom-policy-rules");
|
|
60
61
|
const policy_exceptions_1 = require("../utils/policy-exceptions");
|
|
@@ -672,6 +673,313 @@ function asNumberValue(value) {
|
|
|
672
673
|
function asStringValue(value) {
|
|
673
674
|
return typeof value === 'string' && value.trim().length > 0 ? value : null;
|
|
674
675
|
}
|
|
676
|
+
const EXPEDITE_FOLLOW_UP_CHECKLIST = [
|
|
677
|
+
'Add validation back',
|
|
678
|
+
'Move logic to proper layer',
|
|
679
|
+
'Remove temporary code',
|
|
680
|
+
];
|
|
681
|
+
function containsAnyToken(value, tokens) {
|
|
682
|
+
const normalized = value.toLowerCase();
|
|
683
|
+
return tokens.some((token) => normalized.includes(token));
|
|
684
|
+
}
|
|
685
|
+
function isSecurityOrAuthViolation(fileRaw, policyRaw, messageRaw) {
|
|
686
|
+
const combined = `${fileRaw} ${policyRaw} ${messageRaw}`.toLowerCase();
|
|
687
|
+
return containsAnyToken(combined, [
|
|
688
|
+
'auth',
|
|
689
|
+
'authentication',
|
|
690
|
+
'authorization',
|
|
691
|
+
'security',
|
|
692
|
+
'permission',
|
|
693
|
+
'access control',
|
|
694
|
+
'access_control',
|
|
695
|
+
'token',
|
|
696
|
+
'secret',
|
|
697
|
+
'credential',
|
|
698
|
+
'encryption',
|
|
699
|
+
'encrypt',
|
|
700
|
+
'decrypt',
|
|
701
|
+
'csrf',
|
|
702
|
+
'xss',
|
|
703
|
+
'sql injection',
|
|
704
|
+
'sqli',
|
|
705
|
+
'insecure',
|
|
706
|
+
'vulnerability',
|
|
707
|
+
]);
|
|
708
|
+
}
|
|
709
|
+
function isCriticalScopeBreach(fileRaw, messageRaw) {
|
|
710
|
+
const combined = `${fileRaw} ${messageRaw}`.toLowerCase();
|
|
711
|
+
return containsAnyToken(combined, [
|
|
712
|
+
'auth',
|
|
713
|
+
'security',
|
|
714
|
+
'secret',
|
|
715
|
+
'token',
|
|
716
|
+
'credential',
|
|
717
|
+
'permission',
|
|
718
|
+
'infra/terraform',
|
|
719
|
+
'terraform',
|
|
720
|
+
'k8s',
|
|
721
|
+
'helm',
|
|
722
|
+
'migration',
|
|
723
|
+
'database/migration',
|
|
724
|
+
'policy',
|
|
725
|
+
'contract',
|
|
726
|
+
]);
|
|
727
|
+
}
|
|
728
|
+
function resolveExpediteModeFromPayload(payload) {
|
|
729
|
+
const explicit = asBooleanFlag(payload.expediteMode);
|
|
730
|
+
if (explicit !== null) {
|
|
731
|
+
return explicit;
|
|
732
|
+
}
|
|
733
|
+
const message = asStringValue(payload.message) || '';
|
|
734
|
+
return containsAnyToken(message, ['hotfix', 'urgent', 'prod down', 'incident', 'expedite']);
|
|
735
|
+
}
|
|
736
|
+
function toVerifySeverity(value) {
|
|
737
|
+
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
738
|
+
if (normalized === 'critical' || normalized === 'block')
|
|
739
|
+
return 'critical';
|
|
740
|
+
if (normalized === 'high')
|
|
741
|
+
return 'high';
|
|
742
|
+
if (normalized === 'warn'
|
|
743
|
+
|| normalized === 'warning'
|
|
744
|
+
|| normalized === 'medium'
|
|
745
|
+
|| normalized === 'low') {
|
|
746
|
+
return 'warning';
|
|
747
|
+
}
|
|
748
|
+
return 'info';
|
|
749
|
+
}
|
|
750
|
+
function toVerifyVerdict(value) {
|
|
751
|
+
const normalized = typeof value === 'string' ? value.trim().toUpperCase() : '';
|
|
752
|
+
if (normalized === 'PASS' || normalized === 'WARN' || normalized === 'FAIL') {
|
|
753
|
+
return normalized;
|
|
754
|
+
}
|
|
755
|
+
return 'FAIL';
|
|
756
|
+
}
|
|
757
|
+
function normalizeScopeIssueMessage(rawMessage) {
|
|
758
|
+
const message = asStringValue(rawMessage);
|
|
759
|
+
return message || 'File modified outside intended scope';
|
|
760
|
+
}
|
|
761
|
+
function pushVerifyIssue(target, seen, key, value) {
|
|
762
|
+
if (seen.has(key))
|
|
763
|
+
return;
|
|
764
|
+
seen.add(key);
|
|
765
|
+
target.push(value);
|
|
766
|
+
}
|
|
767
|
+
function dedupeTriageItems(items) {
|
|
768
|
+
const seen = new Set();
|
|
769
|
+
const output = [];
|
|
770
|
+
for (const item of items) {
|
|
771
|
+
const key = `${item.source}|${item.file.toLowerCase()}|${item.policy.toLowerCase()}|${item.message.toLowerCase()}`;
|
|
772
|
+
if (seen.has(key))
|
|
773
|
+
continue;
|
|
774
|
+
seen.add(key);
|
|
775
|
+
output.push(item);
|
|
776
|
+
}
|
|
777
|
+
return output;
|
|
778
|
+
}
|
|
779
|
+
function toCanonicalVerifyOutput(payload) {
|
|
780
|
+
const verdict = toVerifyVerdict(payload.verdict);
|
|
781
|
+
const violations = [];
|
|
782
|
+
const warnings = [];
|
|
783
|
+
const scopeIssues = [];
|
|
784
|
+
const seenViolations = new Set();
|
|
785
|
+
const seenWarnings = new Set();
|
|
786
|
+
const seenScopeIssues = new Set();
|
|
787
|
+
const addScopeIssue = (fileRaw, messageRaw) => {
|
|
788
|
+
const file = asStringValue(fileRaw) || 'unknown';
|
|
789
|
+
const message = normalizeScopeIssueMessage(messageRaw);
|
|
790
|
+
const key = file.toLowerCase();
|
|
791
|
+
pushVerifyIssue(scopeIssues, seenScopeIssues, key, { file, message });
|
|
792
|
+
};
|
|
793
|
+
const addWarning = (fileRaw, messageRaw, policyRaw) => {
|
|
794
|
+
const file = asStringValue(fileRaw) || 'unknown';
|
|
795
|
+
const message = asStringValue(messageRaw) || 'Warning detected';
|
|
796
|
+
const policy = asStringValue(policyRaw) || 'warning';
|
|
797
|
+
const key = `${file.toLowerCase()}|${message.toLowerCase()}|${policy.toLowerCase()}`;
|
|
798
|
+
pushVerifyIssue(warnings, seenWarnings, key, { file, message, policy });
|
|
799
|
+
};
|
|
800
|
+
const addViolation = (fileRaw, messageRaw, policyRaw, severityRaw) => {
|
|
801
|
+
const file = asStringValue(fileRaw) || 'unknown';
|
|
802
|
+
const message = asStringValue(messageRaw) || 'Policy violation detected';
|
|
803
|
+
const policy = asStringValue(policyRaw) || 'unknown_policy';
|
|
804
|
+
const severity = toVerifySeverity(severityRaw);
|
|
805
|
+
const key = `${file.toLowerCase()}|${message.toLowerCase()}|${policy.toLowerCase()}|${severity}`;
|
|
806
|
+
pushVerifyIssue(violations, seenViolations, key, { file, message, policy, severity });
|
|
807
|
+
};
|
|
808
|
+
const rawScopeIssues = Array.isArray(payload.scopeIssues) ? payload.scopeIssues : [];
|
|
809
|
+
for (const item of rawScopeIssues) {
|
|
810
|
+
const record = asObjectRecord(item);
|
|
811
|
+
if (record) {
|
|
812
|
+
addScopeIssue(record.file, record.message);
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
addScopeIssue(item, null);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
const rawBloatFiles = Array.isArray(payload.bloatFiles) ? payload.bloatFiles : [];
|
|
819
|
+
for (const item of rawBloatFiles) {
|
|
820
|
+
addScopeIssue(item, null);
|
|
821
|
+
}
|
|
822
|
+
const rawWarnings = Array.isArray(payload.warnings) ? payload.warnings : [];
|
|
823
|
+
for (const item of rawWarnings) {
|
|
824
|
+
const record = asObjectRecord(item);
|
|
825
|
+
if (record) {
|
|
826
|
+
addWarning(record.file, record.message, record.policy ?? record.rule);
|
|
827
|
+
}
|
|
828
|
+
else if (typeof item === 'string') {
|
|
829
|
+
addWarning('unknown', item, 'warning');
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
const rawViolations = Array.isArray(payload.violations) ? payload.violations : [];
|
|
833
|
+
for (const item of rawViolations) {
|
|
834
|
+
const record = asObjectRecord(item);
|
|
835
|
+
if (!record)
|
|
836
|
+
continue;
|
|
837
|
+
const file = record.file;
|
|
838
|
+
const message = record.message;
|
|
839
|
+
const policy = record.policy ?? record.rule;
|
|
840
|
+
const severity = toVerifySeverity(record.severity);
|
|
841
|
+
const combined = `${String(policy || '').toLowerCase()} ${String(message || '').toLowerCase()}`;
|
|
842
|
+
const isScopeIssue = combined.includes('scope_guard')
|
|
843
|
+
|| combined.includes('scope')
|
|
844
|
+
|| combined.includes('outside the plan')
|
|
845
|
+
|| combined.includes('out of scope');
|
|
846
|
+
if (isScopeIssue) {
|
|
847
|
+
addScopeIssue(file, message);
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
// Artifact presence/signature checks are advisory — they must never block a PR.
|
|
851
|
+
// Real governance signal (policy violations, scope drift) should not be obscured
|
|
852
|
+
// by infrastructure setup state.
|
|
853
|
+
const policyStr = String(policy || '').toLowerCase();
|
|
854
|
+
const isArtifactCheck = policyStr === 'deterministic_artifacts_required'
|
|
855
|
+
|| policyStr === 'signed_artifacts_required';
|
|
856
|
+
if (isArtifactCheck) {
|
|
857
|
+
addWarning(file, message, policy);
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
if (severity === 'warning' || severity === 'info') {
|
|
861
|
+
addWarning(file, message, policy);
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
addViolation(file, message, policy, severity);
|
|
865
|
+
}
|
|
866
|
+
const payloadMessage = asStringValue(payload.message);
|
|
867
|
+
if (payloadMessage
|
|
868
|
+
&& violations.length === 0
|
|
869
|
+
&& warnings.length === 0
|
|
870
|
+
&& scopeIssues.length === 0) {
|
|
871
|
+
addWarning('unknown', payloadMessage, 'verify_result');
|
|
872
|
+
}
|
|
873
|
+
const summaryRecord = asObjectRecord(payload.summary);
|
|
874
|
+
const fileSet = new Set();
|
|
875
|
+
for (const violation of violations)
|
|
876
|
+
fileSet.add(violation.file);
|
|
877
|
+
for (const warning of warnings)
|
|
878
|
+
fileSet.add(warning.file);
|
|
879
|
+
for (const scopeIssue of scopeIssues)
|
|
880
|
+
fileSet.add(scopeIssue.file);
|
|
881
|
+
const totalFilesChanged = (() => {
|
|
882
|
+
const fromSummary = summaryRecord ? asNumberValue(summaryRecord.totalFilesChanged) : null;
|
|
883
|
+
if (fromSummary !== null)
|
|
884
|
+
return Math.max(0, Math.floor(fromSummary));
|
|
885
|
+
const blastRadius = asObjectRecord(payload.blastRadius);
|
|
886
|
+
const fromBlastRadius = blastRadius ? asNumberValue(blastRadius.filesChanged) : null;
|
|
887
|
+
if (fromBlastRadius !== null)
|
|
888
|
+
return Math.max(0, Math.floor(fromBlastRadius));
|
|
889
|
+
return fileSet.size;
|
|
890
|
+
})();
|
|
891
|
+
const driftScoreRaw = asNumberValue(payload.driftScore);
|
|
892
|
+
const driftScore = driftScoreRaw === null
|
|
893
|
+
? undefined
|
|
894
|
+
: Math.max(0, Math.min(100, Math.round(driftScoreRaw)));
|
|
895
|
+
const expediteModeUsed = resolveExpediteModeFromPayload(payload);
|
|
896
|
+
const scopeTriageItems = scopeIssues.map((item) => ({
|
|
897
|
+
file: item.file,
|
|
898
|
+
message: item.message,
|
|
899
|
+
policy: 'scope_guard',
|
|
900
|
+
severity: 'block',
|
|
901
|
+
source: 'scope',
|
|
902
|
+
}));
|
|
903
|
+
const violationTriageItems = violations.map((item) => ({
|
|
904
|
+
file: item.file,
|
|
905
|
+
message: item.message,
|
|
906
|
+
policy: item.policy,
|
|
907
|
+
severity: item.severity,
|
|
908
|
+
source: 'violation',
|
|
909
|
+
}));
|
|
910
|
+
const warningTriageItems = warnings.map((item) => ({
|
|
911
|
+
file: item.file,
|
|
912
|
+
message: item.message,
|
|
913
|
+
policy: item.policy,
|
|
914
|
+
severity: 'warning',
|
|
915
|
+
source: 'warning',
|
|
916
|
+
}));
|
|
917
|
+
const defaultBlockingItems = dedupeTriageItems([
|
|
918
|
+
...scopeTriageItems,
|
|
919
|
+
...violationTriageItems.filter((item) => item.severity === 'critical' || item.severity === 'high'),
|
|
920
|
+
]);
|
|
921
|
+
const defaultAdvisoryItems = dedupeTriageItems([
|
|
922
|
+
...warningTriageItems,
|
|
923
|
+
...violationTriageItems.filter((item) => item.severity === 'warning' || item.severity === 'info'),
|
|
924
|
+
]);
|
|
925
|
+
const expediteBlockingItems = dedupeTriageItems([
|
|
926
|
+
...scopeTriageItems.filter((item) => isCriticalScopeBreach(item.file, item.message)),
|
|
927
|
+
...violationTriageItems.filter((item) => isSecurityOrAuthViolation(item.file, item.policy, item.message)),
|
|
928
|
+
...warningTriageItems
|
|
929
|
+
.filter((item) => isSecurityOrAuthViolation(item.file, item.policy, item.message))
|
|
930
|
+
.map((item) => ({
|
|
931
|
+
...item,
|
|
932
|
+
source: 'violation',
|
|
933
|
+
})),
|
|
934
|
+
]);
|
|
935
|
+
const expediteItems = dedupeTriageItems([
|
|
936
|
+
...scopeTriageItems
|
|
937
|
+
.filter((item) => !isCriticalScopeBreach(item.file, item.message))
|
|
938
|
+
.map((item) => ({
|
|
939
|
+
...item,
|
|
940
|
+
source: 'expedite',
|
|
941
|
+
})),
|
|
942
|
+
...violationTriageItems
|
|
943
|
+
.filter((item) => !isSecurityOrAuthViolation(item.file, item.policy, item.message))
|
|
944
|
+
.map((item) => ({
|
|
945
|
+
...item,
|
|
946
|
+
source: 'expedite',
|
|
947
|
+
})),
|
|
948
|
+
...warningTriageItems
|
|
949
|
+
.filter((item) => !isSecurityOrAuthViolation(item.file, item.policy, item.message))
|
|
950
|
+
.map((item) => ({
|
|
951
|
+
...item,
|
|
952
|
+
source: 'expedite',
|
|
953
|
+
})),
|
|
954
|
+
]);
|
|
955
|
+
const blockingItems = expediteModeUsed ? expediteBlockingItems : defaultBlockingItems;
|
|
956
|
+
const advisoryItems = expediteModeUsed ? expediteItems : defaultAdvisoryItems;
|
|
957
|
+
return {
|
|
958
|
+
verdict,
|
|
959
|
+
summary: {
|
|
960
|
+
totalFilesChanged,
|
|
961
|
+
totalViolations: violations.length,
|
|
962
|
+
totalWarnings: warnings.length,
|
|
963
|
+
totalScopeIssues: scopeIssues.length,
|
|
964
|
+
},
|
|
965
|
+
violations,
|
|
966
|
+
warnings,
|
|
967
|
+
scopeIssues,
|
|
968
|
+
blockingCount: blockingItems.length,
|
|
969
|
+
advisoryCount: advisoryItems.length,
|
|
970
|
+
blockingItems,
|
|
971
|
+
advisoryItems,
|
|
972
|
+
expediteModeUsed,
|
|
973
|
+
expediteCount: expediteModeUsed ? expediteItems.length : 0,
|
|
974
|
+
expediteItems: expediteModeUsed ? expediteItems : [],
|
|
975
|
+
expediteFollowUpChecklist: expediteModeUsed ? [...EXPEDITE_FOLLOW_UP_CHECKLIST] : [],
|
|
976
|
+
...(expediteModeUsed ? { expediteNote: 'Expedite Mode used' } : {}),
|
|
977
|
+
...(typeof driftScore === 'number' ? { driftScore } : {}),
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function emitCanonicalVerifyJson(payload) {
|
|
981
|
+
console.log(JSON.stringify(toCanonicalVerifyOutput(payload), null, 2));
|
|
982
|
+
}
|
|
675
983
|
function buildDeterministicLayerSummary(payload) {
|
|
676
984
|
const verdict = asStringValue(payload.verdict) || 'UNKNOWN';
|
|
677
985
|
const mode = asStringValue(payload.mode) || 'unknown';
|
|
@@ -841,6 +1149,13 @@ function isGitRepository(cwd) {
|
|
|
841
1149
|
return false;
|
|
842
1150
|
}
|
|
843
1151
|
}
|
|
1152
|
+
function resolveVerifyExpediteMode(projectRoot) {
|
|
1153
|
+
if (isEnabledFlag(process.env.NEURCODE_EXPEDITE_MODE) || isEnabledFlag(process.env.NEURCODE_MCP_EXPEDITE_MODE)) {
|
|
1154
|
+
return true;
|
|
1155
|
+
}
|
|
1156
|
+
const branchName = (0, git_1.detectCurrentGitBranch)(projectRoot) || process.env.GITHUB_REF_NAME || '';
|
|
1157
|
+
return containsAnyToken(branchName, ['hotfix', 'urgent', 'prod-down', 'prod_down', 'prod down', 'incident', 'expedite']);
|
|
1158
|
+
}
|
|
844
1159
|
function isSignedAiLogsRequired(orgGovernanceSettings) {
|
|
845
1160
|
const explicitRequirement = isEnabledFlag(process.env.NEURCODE_GOVERNANCE_REQUIRE_SIGNED_LOGS) ||
|
|
846
1161
|
isEnabledFlag(process.env.NEURCODE_AI_LOG_REQUIRE_SIGNED);
|
|
@@ -1012,18 +1327,12 @@ async function recordVerificationIfRequested(options, config, payload) {
|
|
|
1012
1327
|
* Execute policy-only verification (General Governance mode)
|
|
1013
1328
|
* Returns the exit code to use
|
|
1014
1329
|
*/
|
|
1015
|
-
async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source, scopeTelemetry, projectId, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner, compiledPolicyArtifact, compiledPolicyMetadata, changeContractSummary) {
|
|
1330
|
+
async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source, scopeTelemetry, projectId, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner, expediteModeEnabled, compiledPolicyArtifact, compiledPolicyMetadata, changeContractSummary) {
|
|
1016
1331
|
const emitPolicyOnlyJson = (payload) => {
|
|
1017
|
-
|
|
1332
|
+
emitCanonicalVerifyJson({
|
|
1018
1333
|
...payload,
|
|
1019
|
-
|
|
1020
|
-
};
|
|
1021
|
-
console.log(JSON.stringify({
|
|
1022
|
-
...enrichedPayload,
|
|
1023
|
-
...(compiledPolicyMetadata ? { policyCompilation: compiledPolicyMetadata } : {}),
|
|
1024
|
-
changeContract: changeContractSummary,
|
|
1025
|
-
scope: scopeTelemetry,
|
|
1026
|
-
}, null, 2));
|
|
1334
|
+
expediteMode: expediteModeEnabled,
|
|
1335
|
+
});
|
|
1027
1336
|
};
|
|
1028
1337
|
const policyOnlyVerificationSource = 'policy_only';
|
|
1029
1338
|
const recordPolicyOnlyVerification = async (payload) => recordVerificationIfRequested(options, config, {
|
|
@@ -1350,6 +1659,9 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
1350
1659
|
if (!options.json && effectiveRules.policyPack && effectiveRules.policyPackRules.length > 0) {
|
|
1351
1660
|
console.log(chalk.dim(` Evaluating policy pack: ${effectiveRules.policyPack.packName} (${effectiveRules.policyPack.packId}@${effectiveRules.policyPack.version}, ${effectiveRules.policyPackRules.length} rule(s))`));
|
|
1352
1661
|
}
|
|
1662
|
+
else if (!options.json && !effectiveRules.policyPack) {
|
|
1663
|
+
console.log(chalk.dim(' No policy pack installed — run `neurcode policy install <pack>` to add governance rules'));
|
|
1664
|
+
}
|
|
1353
1665
|
const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, effectiveRules.allRules);
|
|
1354
1666
|
policyViolations = (policyResult.violations || []);
|
|
1355
1667
|
policyViolations = policyViolations.filter((v) => !ignoreFilter(v.file));
|
|
@@ -1582,14 +1894,15 @@ async function verifyCommand(options) {
|
|
|
1582
1894
|
try {
|
|
1583
1895
|
const rootResolution = (0, project_root_1.resolveNeurcodeProjectRootWithTrace)(process.cwd());
|
|
1584
1896
|
const projectRoot = rootResolution.projectRoot;
|
|
1897
|
+
const localPlanSync = (0, plan_sync_1.ensureLocalPlan)(projectRoot);
|
|
1898
|
+
const localPlanExpectedFiles = [...localPlanSync.expectedFiles];
|
|
1899
|
+
const expediteModeEnabled = resolveVerifyExpediteMode(projectRoot);
|
|
1585
1900
|
const scopeTelemetry = (0, scope_telemetry_1.buildScopeTelemetryPayload)(rootResolution);
|
|
1586
1901
|
const emitVerifyJson = (payload) => {
|
|
1587
|
-
|
|
1902
|
+
emitCanonicalVerifyJson({
|
|
1588
1903
|
...payload,
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
};
|
|
1592
|
-
console.log(JSON.stringify(jsonPayload, null, 2));
|
|
1904
|
+
expediteMode: expediteModeEnabled,
|
|
1905
|
+
});
|
|
1593
1906
|
};
|
|
1594
1907
|
if (!isGitRepository(projectRoot)) {
|
|
1595
1908
|
const message = 'Verify requires a git repository. Initialize git (`git init`) or run this command inside an existing git project.';
|
|
@@ -1630,10 +1943,10 @@ async function verifyCommand(options) {
|
|
|
1630
1943
|
const explicitStrictArtifactMode = options.strictArtifacts === true ||
|
|
1631
1944
|
isEnabledFlag(process.env.NEURCODE_VERIFY_STRICT_ARTIFACTS) ||
|
|
1632
1945
|
isEnabledFlag(process.env.NEURCODE_ENTERPRISE_MODE);
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
const strictArtifactMode = explicitStrictArtifactMode
|
|
1946
|
+
// Strict artifact mode is only engaged when explicitly requested.
|
|
1947
|
+
// Auto-enabling it in CI based on API key presence masked real violations
|
|
1948
|
+
// by blocking early on missing artifacts before policy checks could run.
|
|
1949
|
+
const strictArtifactMode = explicitStrictArtifactMode;
|
|
1637
1950
|
const runtimeGuardArtifactPath = (0, runtime_guard_1.resolveRuntimeGuardPath)(projectRoot, options.runtimeGuard);
|
|
1638
1951
|
const autoRuntimeGuardInStrict = strictArtifactMode
|
|
1639
1952
|
&& (0, fs_1.existsSync)(runtimeGuardArtifactPath)
|
|
@@ -1699,38 +2012,57 @@ async function verifyCommand(options) {
|
|
|
1699
2012
|
]
|
|
1700
2013
|
: [],
|
|
1701
2014
|
};
|
|
2015
|
+
// Artifact presence warnings (advisory — missing artifacts fall back to runtime compilation).
|
|
2016
|
+
// These must never cause an early exit; real governance signal should always be evaluated.
|
|
2017
|
+
const artifactPresenceWarnings = [];
|
|
1702
2018
|
if (strictArtifactMode) {
|
|
1703
|
-
const strictErrors = [];
|
|
1704
2019
|
if (!compiledPolicyRead.artifact) {
|
|
1705
|
-
|
|
2020
|
+
artifactPresenceWarnings.push(compiledPolicyRead.error
|
|
1706
2021
|
? `Compiled policy artifact invalid (${compiledPolicyRead.error})`
|
|
1707
2022
|
: `Compiled policy artifact missing (${compiledPolicyRead.path})`);
|
|
1708
2023
|
}
|
|
1709
2024
|
if (!changeContractRead.contract) {
|
|
1710
|
-
|
|
2025
|
+
artifactPresenceWarnings.push(changeContractRead.error
|
|
1711
2026
|
? `Change contract artifact invalid (${changeContractRead.error})`
|
|
1712
2027
|
: `Change contract artifact missing (${changeContractRead.path})`);
|
|
1713
2028
|
}
|
|
1714
|
-
if (
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
2029
|
+
if (!options.json && artifactPresenceWarnings.length > 0) {
|
|
2030
|
+
console.log(chalk.yellow('\n⚠️ Deterministic artifact(s) unavailable — falling back to runtime compilation'));
|
|
2031
|
+
artifactPresenceWarnings.forEach((entry) => {
|
|
2032
|
+
console.log(chalk.yellow(` • ${entry}`));
|
|
2033
|
+
});
|
|
2034
|
+
console.log(chalk.dim(' Governance will continue using runtime compilation. Artifact checks are advisory.\n'));
|
|
1718
2035
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
2036
|
+
}
|
|
2037
|
+
// Signature blocking distinguishes two cases:
|
|
2038
|
+
// - Artifact has a signature that is INVALID (present=true, valid=false): this is a tamper
|
|
2039
|
+
// indicator and blocks when requireSignedArtifacts is set.
|
|
2040
|
+
// - Artifact has NO signature (present=false): this is an unsigned artifact; advisory only,
|
|
2041
|
+
// never blocks — an unsigned artifact cannot be "tampered", only "not signed yet".
|
|
2042
|
+
if (strictArtifactMode) {
|
|
2043
|
+
const signatureBlockErrors = [];
|
|
2044
|
+
if (requireSignedArtifacts
|
|
2045
|
+
&& compiledPolicySignatureStatus
|
|
2046
|
+
&& compiledPolicySignatureStatus.present
|
|
2047
|
+
&& !compiledPolicySignatureStatus.valid) {
|
|
2048
|
+
signatureBlockErrors.push(`Compiled policy artifact signature validation failed (${compiledPolicySignatureStatus.issues.join('; ') || 'unknown issue'})`);
|
|
1723
2049
|
}
|
|
1724
|
-
if (
|
|
1725
|
-
|
|
2050
|
+
if (requireSignedArtifacts
|
|
2051
|
+
&& changeContractSignatureStatus
|
|
2052
|
+
&& changeContractSignatureStatus.present
|
|
2053
|
+
&& !changeContractSignatureStatus.valid) {
|
|
2054
|
+
signatureBlockErrors.push(`Change contract artifact signature validation failed (${changeContractSignatureStatus.issues.join('; ') || 'unknown issue'})`);
|
|
2055
|
+
}
|
|
2056
|
+
if (signatureBlockErrors.length > 0) {
|
|
2057
|
+
const message = `Signed artifact enforcement failed — tampered or invalid signatures detected.\n- ${signatureBlockErrors.join('\n- ')}`;
|
|
1726
2058
|
if (options.json) {
|
|
1727
2059
|
emitVerifyJson({
|
|
1728
2060
|
grade: 'F',
|
|
1729
2061
|
score: 0,
|
|
1730
2062
|
verdict: 'FAIL',
|
|
1731
|
-
violations:
|
|
2063
|
+
violations: signatureBlockErrors.map((entry) => ({
|
|
1732
2064
|
file: entry.toLowerCase().includes('compiled policy') ? compiledPolicyRead.path : changeContractRead.path,
|
|
1733
|
-
rule: '
|
|
2065
|
+
rule: 'signed_artifacts_required',
|
|
1734
2066
|
severity: 'block',
|
|
1735
2067
|
message: entry,
|
|
1736
2068
|
})),
|
|
@@ -1741,32 +2073,34 @@ async function verifyCommand(options) {
|
|
|
1741
2073
|
totalPlannedFiles: 0,
|
|
1742
2074
|
message,
|
|
1743
2075
|
scopeGuardPassed: false,
|
|
1744
|
-
mode: '
|
|
2076
|
+
mode: 'signed_artifacts_required',
|
|
1745
2077
|
policyOnly: options.policyOnly === true,
|
|
1746
2078
|
changeContract: changeContractSummary,
|
|
1747
2079
|
...(compiledPolicyMetadata ? { policyCompilation: compiledPolicyMetadata } : {}),
|
|
1748
2080
|
});
|
|
1749
2081
|
}
|
|
1750
2082
|
else {
|
|
1751
|
-
(0, scope_telemetry_1.printScopeTelemetry)(chalk, scopeTelemetry, {
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
console.log(chalk.red('\n⛔ Deterministic Artifact Requirements Failed'));
|
|
1755
|
-
strictErrors.forEach((entry) => {
|
|
2083
|
+
(0, scope_telemetry_1.printScopeTelemetry)(chalk, scopeTelemetry, { includeBlockedWarning: true });
|
|
2084
|
+
console.log(chalk.red('\n⛔ Signed Artifact Validation Failed'));
|
|
2085
|
+
signatureBlockErrors.forEach((entry) => {
|
|
1756
2086
|
console.log(chalk.red(` • ${entry}`));
|
|
1757
2087
|
});
|
|
1758
|
-
console.log(chalk.dim('\
|
|
1759
|
-
if (requireSignedArtifacts) {
|
|
1760
|
-
console.log(chalk.dim('Enable signing keys via NEURCODE_GOVERNANCE_SIGNING_KEY or NEURCODE_GOVERNANCE_SIGNING_KEYS to generate signed artifacts.\n'));
|
|
1761
|
-
}
|
|
1762
|
-
else {
|
|
1763
|
-
console.log('');
|
|
1764
|
-
}
|
|
2088
|
+
console.log(chalk.dim('\nRegenerate artifacts with valid signing keys: NEURCODE_GOVERNANCE_SIGNING_KEY or NEURCODE_GOVERNANCE_SIGNING_KEYS.\n'));
|
|
1765
2089
|
}
|
|
1766
2090
|
process.exit(2);
|
|
1767
2091
|
}
|
|
2092
|
+
// Advisory notice when artifact has a signature but signing is not required in this context.
|
|
2093
|
+
if (!options.json) {
|
|
2094
|
+
if (compiledPolicySignatureStatus && !compiledPolicySignatureStatus.valid && !requireSignedArtifacts) {
|
|
2095
|
+
console.log(chalk.yellow(` ⚠️ Compiled policy signature could not be verified (${compiledPolicySignatureStatus.issues.join('; ') || 'key unavailable'}) — advisory only`));
|
|
2096
|
+
}
|
|
2097
|
+
if (changeContractSignatureStatus && !changeContractSignatureStatus.valid && !requireSignedArtifacts) {
|
|
2098
|
+
console.log(chalk.yellow(` ⚠️ Change contract signature could not be verified (${changeContractSignatureStatus.issues.join('; ') || 'key unavailable'}) — advisory only`));
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
1768
2101
|
}
|
|
1769
2102
|
if (!strictArtifactMode && requireSignedArtifacts) {
|
|
2103
|
+
// Non-strict mode with signing required: same signature gate applies.
|
|
1770
2104
|
const signatureErrors = [];
|
|
1771
2105
|
if (compiledPolicyRead.artifact && compiledPolicySignatureStatus && !compiledPolicySignatureStatus.valid) {
|
|
1772
2106
|
signatureErrors.push(`Compiled policy artifact signature validation failed (${compiledPolicySignatureStatus.issues.join('; ') || 'unknown issue'})`);
|
|
@@ -1801,13 +2135,9 @@ async function verifyCommand(options) {
|
|
|
1801
2135
|
});
|
|
1802
2136
|
}
|
|
1803
2137
|
else {
|
|
1804
|
-
(0, scope_telemetry_1.printScopeTelemetry)(chalk, scopeTelemetry, {
|
|
1805
|
-
includeBlockedWarning: true,
|
|
1806
|
-
});
|
|
2138
|
+
(0, scope_telemetry_1.printScopeTelemetry)(chalk, scopeTelemetry, { includeBlockedWarning: true });
|
|
1807
2139
|
console.log(chalk.red('\n⛔ Signed Artifact Requirements Failed'));
|
|
1808
|
-
signatureErrors.forEach((entry) => {
|
|
1809
|
-
console.log(chalk.red(` • ${entry}`));
|
|
1810
|
-
});
|
|
2140
|
+
signatureErrors.forEach((entry) => console.log(chalk.red(` • ${entry}`)));
|
|
1811
2141
|
console.log(chalk.dim('\nEnable signing keys via NEURCODE_GOVERNANCE_SIGNING_KEY or NEURCODE_GOVERNANCE_SIGNING_KEYS and regenerate artifacts.\n'));
|
|
1812
2142
|
}
|
|
1813
2143
|
process.exit(2);
|
|
@@ -1845,9 +2175,6 @@ async function verifyCommand(options) {
|
|
|
1845
2175
|
console.log(chalk.yellow(` Change contract signature: invalid (${changeContractSignatureStatus.issues.join('; ') || 'unknown issue'})`));
|
|
1846
2176
|
}
|
|
1847
2177
|
}
|
|
1848
|
-
if (ciEnterpriseDefaultStrict && !explicitStrictArtifactMode) {
|
|
1849
|
-
console.log(chalk.dim(' CI enterprise mode detected: strict deterministic artifact enforcement is auto-enabled (set NEURCODE_VERIFY_ALLOW_NON_STRICT_CI=1 to opt out).'));
|
|
1850
|
-
}
|
|
1851
2178
|
if (autoRuntimeGuardInStrict && !options.requireRuntimeGuard && !isEnabledFlag(process.env.NEURCODE_VERIFY_REQUIRE_RUNTIME_GUARD)) {
|
|
1852
2179
|
console.log(chalk.dim(` Strict mode detected runtime guard artifact: auto-enforcing runtime guard (${runtimeGuardArtifactPath}).`));
|
|
1853
2180
|
}
|
|
@@ -2069,34 +2396,44 @@ async function verifyCommand(options) {
|
|
|
2069
2396
|
});
|
|
2070
2397
|
process.exit(2);
|
|
2071
2398
|
}
|
|
2072
|
-
// Determine which diff to capture
|
|
2399
|
+
// Determine which diff to capture.
|
|
2073
2400
|
let diffText;
|
|
2401
|
+
let diffContextLabel = '';
|
|
2074
2402
|
if (options.staged) {
|
|
2075
2403
|
diffText = (0, child_process_1.execSync)('git diff --cached', { maxBuffer: 1024 * 1024 * 1024, encoding: 'utf-8' });
|
|
2404
|
+
diffContextLabel = 'staged changes';
|
|
2076
2405
|
}
|
|
2077
2406
|
else if (options.base) {
|
|
2078
2407
|
diffText = (0, git_1.getDiffFromBase)(options.base);
|
|
2408
|
+
diffContextLabel = `working tree vs ${options.base}`;
|
|
2079
2409
|
}
|
|
2080
2410
|
else if (options.head) {
|
|
2081
2411
|
diffText = (0, child_process_1.execSync)('git diff HEAD', { maxBuffer: 1024 * 1024 * 1024, encoding: 'utf-8' });
|
|
2412
|
+
diffContextLabel = 'working tree vs HEAD';
|
|
2082
2413
|
}
|
|
2083
2414
|
else {
|
|
2084
|
-
// Default:
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
diffText =
|
|
2415
|
+
// Default: resolve a PR-like base context first (origin/main or origin/master).
|
|
2416
|
+
// Fallback to staged diff when base context cannot be resolved.
|
|
2417
|
+
const defaultContext = (0, git_1.resolveDefaultDiffContext)(projectRoot);
|
|
2418
|
+
if (defaultContext.mode === 'base' && defaultContext.baseRef) {
|
|
2419
|
+
diffText = (0, git_1.getDiffFromBase)(defaultContext.baseRef);
|
|
2420
|
+
diffContextLabel = defaultContext.currentBranch
|
|
2421
|
+
? `${defaultContext.currentBranch} vs ${defaultContext.baseRef}`
|
|
2422
|
+
: `working tree vs ${defaultContext.baseRef}`;
|
|
2089
2423
|
}
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2424
|
+
else {
|
|
2425
|
+
diffText = (0, child_process_1.execSync)('git diff --cached', { maxBuffer: 1024 * 1024 * 1024, encoding: 'utf-8' });
|
|
2426
|
+
diffContextLabel = 'staged changes (fallback)';
|
|
2093
2427
|
}
|
|
2094
2428
|
}
|
|
2429
|
+
if (!options.json && diffContextLabel) {
|
|
2430
|
+
console.log(chalk.dim(` Diff context: ${diffContextLabel}`));
|
|
2431
|
+
}
|
|
2095
2432
|
const untrackedDiffFiles = getUntrackedDiffFiles(projectRoot);
|
|
2096
2433
|
if (!diffText.trim() && untrackedDiffFiles.length === 0) {
|
|
2097
2434
|
if (!options.json) {
|
|
2098
|
-
console.log(chalk.yellow('⚠️ No changes detected'));
|
|
2099
|
-
console.log(chalk.dim('
|
|
2435
|
+
console.log(chalk.yellow('⚠️ No changes detected in current diff context.'));
|
|
2436
|
+
console.log(chalk.dim(' Tip: Ensure changes are staged or run against a base branch.'));
|
|
2100
2437
|
}
|
|
2101
2438
|
else {
|
|
2102
2439
|
emitVerifyJson({
|
|
@@ -2109,7 +2446,7 @@ async function verifyCommand(options) {
|
|
|
2109
2446
|
bloatFiles: [],
|
|
2110
2447
|
plannedFilesModified: 0,
|
|
2111
2448
|
totalPlannedFiles: 0,
|
|
2112
|
-
message: 'No changes detected',
|
|
2449
|
+
message: 'No changes detected in current diff context.',
|
|
2113
2450
|
scopeGuardPassed: false,
|
|
2114
2451
|
});
|
|
2115
2452
|
}
|
|
@@ -2139,7 +2476,8 @@ async function verifyCommand(options) {
|
|
|
2139
2476
|
const summary = (0, diff_parser_1.getDiffSummary)(diffFiles);
|
|
2140
2477
|
if (diffFiles.length === 0) {
|
|
2141
2478
|
if (!options.json) {
|
|
2142
|
-
console.log(chalk.yellow('⚠️ No
|
|
2479
|
+
console.log(chalk.yellow('⚠️ No changes detected in current diff context.'));
|
|
2480
|
+
console.log(chalk.dim(' Tip: Ensure changes are staged or run against a base branch.'));
|
|
2143
2481
|
}
|
|
2144
2482
|
else {
|
|
2145
2483
|
emitVerifyJson({
|
|
@@ -2152,7 +2490,7 @@ async function verifyCommand(options) {
|
|
|
2152
2490
|
bloatFiles: [],
|
|
2153
2491
|
plannedFilesModified: 0,
|
|
2154
2492
|
totalPlannedFiles: 0,
|
|
2155
|
-
message: 'No
|
|
2493
|
+
message: 'No changes detected in current diff context.',
|
|
2156
2494
|
scopeGuardPassed: false,
|
|
2157
2495
|
});
|
|
2158
2496
|
}
|
|
@@ -2382,7 +2720,7 @@ async function verifyCommand(options) {
|
|
|
2382
2720
|
}
|
|
2383
2721
|
}
|
|
2384
2722
|
const runPolicyOnlyModeAndExit = async (source) => {
|
|
2385
|
-
const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client, source, scopeTelemetry, projectId || undefined, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner, compiledPolicyRead.artifact, compiledPolicyMetadata, changeContractSummary);
|
|
2723
|
+
const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client, source, scopeTelemetry, projectId || undefined, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner, expediteModeEnabled, compiledPolicyRead.artifact, compiledPolicyMetadata, changeContractSummary);
|
|
2386
2724
|
const changedFiles = diffFiles.map((f) => f.path);
|
|
2387
2725
|
const verdict = exitCode === 2 ? 'FAIL' : exitCode === 1 ? 'WARN' : 'PASS';
|
|
2388
2726
|
recordVerifyEvent(verdict, `policy_only_source=${source};exit=${exitCode}`, changedFiles);
|
|
@@ -2397,6 +2735,7 @@ async function verifyCommand(options) {
|
|
|
2397
2735
|
const requirePlan = options.requirePlan === true
|
|
2398
2736
|
|| process.env.NEURCODE_VERIFY_REQUIRE_PLAN === '1'
|
|
2399
2737
|
|| strictArtifactMode;
|
|
2738
|
+
let useLocalPlanSync = false;
|
|
2400
2739
|
// Get planId: Priority 1: options flag, Priority 2: state file (.neurcode/config.json), Priority 3: legacy config
|
|
2401
2740
|
let planId = options.planId;
|
|
2402
2741
|
if (!planId) {
|
|
@@ -2429,6 +2768,19 @@ async function verifyCommand(options) {
|
|
|
2429
2768
|
}
|
|
2430
2769
|
}
|
|
2431
2770
|
}
|
|
2771
|
+
if (planId === 'local-plan-sync' && localPlanExpectedFiles.length > 0) {
|
|
2772
|
+
useLocalPlanSync = true;
|
|
2773
|
+
if (!options.json) {
|
|
2774
|
+
console.log(chalk.dim(` Using Plan Sync from .neurcode/plan.json (${localPlanExpectedFiles.length} expected file(s))`));
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
if (!planId && localPlanExpectedFiles.length > 0) {
|
|
2778
|
+
planId = 'local-plan-sync';
|
|
2779
|
+
useLocalPlanSync = true;
|
|
2780
|
+
if (!options.json) {
|
|
2781
|
+
console.log(chalk.dim(` Using Plan Sync from .neurcode/plan.json (${localPlanExpectedFiles.length} expected file(s))`));
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2432
2784
|
// If no planId found, either enforce strict requirement or fall back to policy-only mode.
|
|
2433
2785
|
if (!planId) {
|
|
2434
2786
|
if (requirePlan) {
|
|
@@ -2583,37 +2935,60 @@ async function verifyCommand(options) {
|
|
|
2583
2935
|
}
|
|
2584
2936
|
// Track if scope guard passed - this takes priority over AI grading
|
|
2585
2937
|
let scopeGuardPassed = false;
|
|
2938
|
+
let scopeGuardExpediteBypass = false;
|
|
2586
2939
|
let governanceResult = null;
|
|
2587
2940
|
let planFilesForVerification = [];
|
|
2588
2941
|
let intentConstraintsForVerification;
|
|
2589
2942
|
try {
|
|
2590
2943
|
// Step A: Get Modified Files (already have from diffFiles)
|
|
2591
2944
|
const modifiedFiles = diffFiles.map(f => f.path);
|
|
2592
|
-
// Step B:
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2945
|
+
// Step B: Resolve plan scope from remote plan or local Plan Sync.
|
|
2946
|
+
let originalIntent = '';
|
|
2947
|
+
let governanceTask = 'Plan verification';
|
|
2948
|
+
let planFiles = [];
|
|
2949
|
+
let planDependencies = [];
|
|
2950
|
+
let remotePlanSessionId = null;
|
|
2951
|
+
if (useLocalPlanSync) {
|
|
2952
|
+
const localIntent = (localPlanSync.intent || '').trim();
|
|
2953
|
+
const localConstraintText = localPlanSync.constraints.length > 0
|
|
2954
|
+
? localPlanSync.constraints.join('; ')
|
|
2955
|
+
: '';
|
|
2956
|
+
planFiles = [...localPlanExpectedFiles];
|
|
2957
|
+
originalIntent = localIntent || localConstraintText;
|
|
2958
|
+
governanceTask = localIntent
|
|
2959
|
+
? `Local Plan Sync: ${localIntent}`
|
|
2960
|
+
: 'Local Plan Sync verification';
|
|
2961
|
+
if (!options.json) {
|
|
2962
|
+
console.log(chalk.dim(` Plan Sync scope loaded: ${planFiles.length} file(s)`));
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
else {
|
|
2966
|
+
const planData = await client.getPlan(finalPlanId);
|
|
2967
|
+
// Extract original intent from plan (for constraint checking)
|
|
2968
|
+
originalIntent = planData.intent || '';
|
|
2969
|
+
const planTitle = typeof planData.content.title === 'string'
|
|
2970
|
+
? planData.content.title?.trim()
|
|
2971
|
+
: '';
|
|
2972
|
+
const planSummary = typeof planData.content.summary === 'string' ? planData.content.summary.trim() : '';
|
|
2973
|
+
governanceTask = planTitle || planSummary || originalIntent || 'Plan verification';
|
|
2974
|
+
// Get approved files from plan (only files with action CREATE or MODIFY)
|
|
2975
|
+
planFiles = planData.content.files
|
|
2976
|
+
.filter((f) => f.action === 'CREATE' || f.action === 'MODIFY')
|
|
2977
|
+
.map((f) => f.path);
|
|
2978
|
+
planDependencies = Array.isArray(planData.content.dependencies)
|
|
2979
|
+
? planData.content.dependencies.filter((item) => typeof item === 'string')
|
|
2980
|
+
: [];
|
|
2981
|
+
remotePlanSessionId = planData.sessionId || null;
|
|
2982
|
+
}
|
|
2983
|
+
planFilesForVerification = [...new Set([...planFiles, ...localPlanExpectedFiles])];
|
|
2606
2984
|
intentConstraintsForVerification = originalIntent || undefined;
|
|
2607
|
-
const planDependencies = Array.isArray(planData.content.dependencies)
|
|
2608
|
-
? planData.content.dependencies.filter((item) => typeof item === 'string')
|
|
2609
|
-
: [];
|
|
2610
2985
|
governanceResult = (0, governance_1.evaluateGovernance)({
|
|
2611
2986
|
projectRoot,
|
|
2612
2987
|
task: governanceTask,
|
|
2613
|
-
expectedFiles:
|
|
2988
|
+
expectedFiles: planFilesForVerification,
|
|
2614
2989
|
expectedDependencies: planDependencies,
|
|
2615
2990
|
diffFiles,
|
|
2616
|
-
contextCandidates:
|
|
2991
|
+
contextCandidates: planFilesForVerification,
|
|
2617
2992
|
orgGovernance: orgGovernanceSettings,
|
|
2618
2993
|
requireSignedAiLogs: signedLogsRequired,
|
|
2619
2994
|
signingKey: aiLogSigningKey,
|
|
@@ -2626,8 +3001,8 @@ async function verifyCommand(options) {
|
|
|
2626
3001
|
// This is the session_id string needed to fetch the session
|
|
2627
3002
|
let sessionIdString = (0, state_1.getSessionId)() || configData.sessionId || configData.lastSessionId || null;
|
|
2628
3003
|
// Fallback: Use sessionId from plan if not in config
|
|
2629
|
-
if (!sessionIdString &&
|
|
2630
|
-
sessionIdString =
|
|
3004
|
+
if (!sessionIdString && remotePlanSessionId) {
|
|
3005
|
+
sessionIdString = remotePlanSessionId;
|
|
2631
3006
|
if ((process.env.DEBUG || process.env.VERBOSE) && !options.json) {
|
|
2632
3007
|
console.log(chalk.dim(` Using sessionId from plan: ${sessionIdString.substring(0, 8)}...`));
|
|
2633
3008
|
}
|
|
@@ -2661,17 +3036,24 @@ async function verifyCommand(options) {
|
|
|
2661
3036
|
}
|
|
2662
3037
|
}
|
|
2663
3038
|
// Step C: The Intersection Logic
|
|
2664
|
-
const approvedSet = new Set([...
|
|
3039
|
+
const approvedSet = new Set([...planFilesForVerification, ...allowedFiles]);
|
|
2665
3040
|
const violations = modifiedFiles.filter(f => !approvedSet.has(f));
|
|
2666
3041
|
const filteredViolations = violations.filter((p) => !shouldIgnore(p));
|
|
2667
3042
|
// Step D: The Block (only report scope violations for non-ignored files)
|
|
2668
3043
|
if (filteredViolations.length > 0) {
|
|
3044
|
+
const criticalScopeViolations = expediteModeEnabled
|
|
3045
|
+
? filteredViolations.filter((file) => isCriticalScopeBreach(file, 'File modified outside the plan'))
|
|
3046
|
+
: filteredViolations;
|
|
3047
|
+
const expediteScopeViolations = expediteModeEnabled
|
|
3048
|
+
? filteredViolations.filter((file) => !criticalScopeViolations.includes(file))
|
|
3049
|
+
: [];
|
|
3050
|
+
const shouldBlockForScope = !expediteModeEnabled || criticalScopeViolations.length > 0;
|
|
2669
3051
|
const aiDebtSummaryForScope = toAiDebtSummary((0, ai_debt_budget_1.evaluateAiDebtBudget)({
|
|
2670
3052
|
diffFiles,
|
|
2671
3053
|
bloatCount: filteredViolations.length,
|
|
2672
3054
|
config: aiDebtConfig,
|
|
2673
3055
|
}));
|
|
2674
|
-
recordVerifyEvent('FAIL',
|
|
3056
|
+
recordVerifyEvent(shouldBlockForScope ? 'FAIL' : 'WARN', `${shouldBlockForScope ? 'scope_violation' : 'scope_expedite'}=${filteredViolations.length}`, modifiedFiles, finalPlanId);
|
|
2675
3057
|
const scopeViolationItems = filteredViolations.map((file) => ({
|
|
2676
3058
|
file,
|
|
2677
3059
|
rule: 'scope_guard',
|
|
@@ -2683,8 +3065,10 @@ async function verifyCommand(options) {
|
|
|
2683
3065
|
...scopeViolationItems,
|
|
2684
3066
|
...aiDebtViolationItems,
|
|
2685
3067
|
];
|
|
2686
|
-
const scopeViolationMessage =
|
|
2687
|
-
|
|
3068
|
+
const scopeViolationMessage = shouldBlockForScope
|
|
3069
|
+
? `Scope violation: ${criticalScopeViolations.length} critical file(s) modified outside the plan`
|
|
3070
|
+
: `Expedite scope warning: ${expediteScopeViolations.length} non-critical file(s) modified outside the plan`;
|
|
3071
|
+
if (shouldBlockForScope && options.json) {
|
|
2688
3072
|
// Output JSON for scope violation BEFORE exit. Must include violations for GitHub Action annotations.
|
|
2689
3073
|
const jsonOutput = {
|
|
2690
3074
|
grade: 'F',
|
|
@@ -2695,12 +3079,13 @@ async function verifyCommand(options) {
|
|
|
2695
3079
|
bloatCount: filteredViolations.length,
|
|
2696
3080
|
bloatFiles: filteredViolations,
|
|
2697
3081
|
plannedFilesModified: 0,
|
|
2698
|
-
totalPlannedFiles:
|
|
3082
|
+
totalPlannedFiles: planFilesForVerification.length,
|
|
2699
3083
|
message: scopeViolationMessage,
|
|
2700
3084
|
scopeGuardPassed: false,
|
|
2701
3085
|
mode: 'plan_enforced',
|
|
2702
3086
|
policyOnly: false,
|
|
2703
3087
|
aiDebt: aiDebtSummaryForScope,
|
|
3088
|
+
...(expediteModeEnabled ? { expediteMode: true } : {}),
|
|
2704
3089
|
...(governanceResult
|
|
2705
3090
|
? buildGovernancePayload(governanceResult, orgGovernanceSettings, {
|
|
2706
3091
|
changeContract: changeContractSummary,
|
|
@@ -2733,18 +3118,25 @@ async function verifyCommand(options) {
|
|
|
2733
3118
|
});
|
|
2734
3119
|
process.exit(1);
|
|
2735
3120
|
}
|
|
2736
|
-
else {
|
|
3121
|
+
else if (shouldBlockForScope) {
|
|
2737
3122
|
// Human-readable output only when NOT in json mode
|
|
2738
3123
|
console.log(chalk.red('\n⛔ SCOPE VIOLATION'));
|
|
2739
3124
|
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
2740
3125
|
console.log(chalk.red('The following files were modified but are not in the plan:'));
|
|
2741
3126
|
console.log('');
|
|
2742
|
-
|
|
3127
|
+
criticalScopeViolations.forEach(file => {
|
|
2743
3128
|
console.log(chalk.red(` • ${file}`));
|
|
2744
3129
|
});
|
|
3130
|
+
if (expediteModeEnabled && expediteScopeViolations.length > 0) {
|
|
3131
|
+
console.log('');
|
|
3132
|
+
console.log(chalk.yellow('Non-critical scope files (can be followed up under expedite mode):'));
|
|
3133
|
+
expediteScopeViolations.forEach((file) => {
|
|
3134
|
+
console.log(chalk.yellow(` • ${file}`));
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
2745
3137
|
console.log('');
|
|
2746
3138
|
console.log(chalk.yellow('To unblock these files, run:'));
|
|
2747
|
-
|
|
3139
|
+
criticalScopeViolations.forEach(file => {
|
|
2748
3140
|
console.log(chalk.dim(` neurcode allow ${file}`));
|
|
2749
3141
|
});
|
|
2750
3142
|
if (aiDebtSummaryForScope.mode !== 'off') {
|
|
@@ -2791,11 +3183,30 @@ async function verifyCommand(options) {
|
|
|
2791
3183
|
});
|
|
2792
3184
|
process.exit(1);
|
|
2793
3185
|
}
|
|
3186
|
+
else {
|
|
3187
|
+
scopeGuardExpediteBypass = true;
|
|
3188
|
+
if (!options.json) {
|
|
3189
|
+
console.log(chalk.yellow('\n⚠️ Expedite scope relaxation applied (non-critical scope only).'));
|
|
3190
|
+
expediteScopeViolations.forEach((file) => {
|
|
3191
|
+
console.log(chalk.yellow(` • ${file}`));
|
|
3192
|
+
});
|
|
3193
|
+
console.log(chalk.dim(' Follow-up checklist:'));
|
|
3194
|
+
EXPEDITE_FOLLOW_UP_CHECKLIST.forEach((item) => {
|
|
3195
|
+
console.log(chalk.dim(` - ${item}`));
|
|
3196
|
+
});
|
|
3197
|
+
console.log(chalk.dim(' Note: Expedite Mode used\n'));
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
2794
3200
|
}
|
|
2795
3201
|
// Scope guard passed - all files are approved or allowed
|
|
2796
3202
|
scopeGuardPassed = true;
|
|
2797
3203
|
if (!options.json) {
|
|
2798
|
-
|
|
3204
|
+
if (scopeGuardExpediteBypass) {
|
|
3205
|
+
console.log(chalk.green('✅ Scope guard passed with expedite relaxation for non-critical scope changes'));
|
|
3206
|
+
}
|
|
3207
|
+
else {
|
|
3208
|
+
console.log(chalk.green('✅ All modified files are approved or allowed'));
|
|
3209
|
+
}
|
|
2799
3210
|
console.log('');
|
|
2800
3211
|
}
|
|
2801
3212
|
}
|
|
@@ -2879,7 +3290,7 @@ async function verifyCommand(options) {
|
|
|
2879
3290
|
const lockViolationItems = toPolicyLockViolations(policyLockEvaluation.mismatches);
|
|
2880
3291
|
recordVerifyEvent('FAIL', 'policy_lock_mismatch', diffFiles.map((f) => f.path), finalPlanId);
|
|
2881
3292
|
if (options.json) {
|
|
2882
|
-
|
|
3293
|
+
emitVerifyJson({
|
|
2883
3294
|
grade: 'F',
|
|
2884
3295
|
score: 0,
|
|
2885
3296
|
verdict: 'FAIL',
|
|
@@ -2907,7 +3318,7 @@ async function verifyCommand(options) {
|
|
|
2907
3318
|
path: policyLockEvaluation.lockPath,
|
|
2908
3319
|
mismatches: policyLockEvaluation.mismatches,
|
|
2909
3320
|
},
|
|
2910
|
-
}
|
|
3321
|
+
});
|
|
2911
3322
|
}
|
|
2912
3323
|
else {
|
|
2913
3324
|
console.log(chalk.red('\n❌ Policy lock baseline mismatch'));
|
|
@@ -3139,6 +3550,9 @@ async function verifyCommand(options) {
|
|
|
3139
3550
|
if (!options.json && effectiveRules.policyPack && effectiveRules.policyPackRules.length > 0) {
|
|
3140
3551
|
console.log(chalk.dim(` Evaluating policy pack: ${effectiveRules.policyPack.packName} (${effectiveRules.policyPack.packId}@${effectiveRules.policyPack.version}, ${effectiveRules.policyPackRules.length} rule(s))`));
|
|
3141
3552
|
}
|
|
3553
|
+
else if (!options.json && !effectiveRules.policyPack) {
|
|
3554
|
+
console.log(chalk.dim(' No policy pack installed — run `neurcode policy install <pack>` to add governance rules'));
|
|
3555
|
+
}
|
|
3142
3556
|
// Prepare diff stats and changed files for API
|
|
3143
3557
|
const diffStats = {
|
|
3144
3558
|
totalAdded: summary.totalAdded,
|
|
@@ -3326,35 +3740,20 @@ async function verifyCommand(options) {
|
|
|
3326
3740
|
displayChangeContractDrift(changeContractSummary, { advisory: true });
|
|
3327
3741
|
}
|
|
3328
3742
|
}
|
|
3329
|
-
// Call verify API
|
|
3743
|
+
// Call verify API (or deterministic local evaluation for Plan Sync scope mode)
|
|
3330
3744
|
if (!options.json) {
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3745
|
+
if (useLocalPlanSync) {
|
|
3746
|
+
console.log(chalk.dim(' Using local Plan Sync deterministic verification (no API plan lookup).\n'));
|
|
3747
|
+
}
|
|
3748
|
+
else {
|
|
3749
|
+
console.log(chalk.dim(' Sending to Neurcode API...\n'));
|
|
3750
|
+
if (options.asyncMode) {
|
|
3751
|
+
console.log(chalk.dim(' Queue-backed verification enabled (async job mode).'));
|
|
3752
|
+
}
|
|
3334
3753
|
}
|
|
3335
3754
|
}
|
|
3336
3755
|
try {
|
|
3337
|
-
|
|
3338
|
-
let verifyResult;
|
|
3339
|
-
try {
|
|
3340
|
-
verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraintsForVerification, deterministicPolicyRules, 'api', compiledPolicyMetadata, {
|
|
3341
|
-
async: options.asyncMode === true,
|
|
3342
|
-
pollIntervalMs: Number.isFinite(options.verifyJobPollMs) ? options.verifyJobPollMs : undefined,
|
|
3343
|
-
timeoutMs: Number.isFinite(options.verifyJobTimeoutMs) ? options.verifyJobTimeoutMs : undefined,
|
|
3344
|
-
idempotencyKey: options.verifyIdempotencyKey,
|
|
3345
|
-
maxAttempts: Number.isFinite(options.verifyJobMaxAttempts) ? options.verifyJobMaxAttempts : undefined,
|
|
3346
|
-
});
|
|
3347
|
-
}
|
|
3348
|
-
catch (verifyApiError) {
|
|
3349
|
-
if (planFilesForVerification.length === 0) {
|
|
3350
|
-
throw verifyApiError;
|
|
3351
|
-
}
|
|
3352
|
-
verifySource = 'local_fallback';
|
|
3353
|
-
if (!options.json) {
|
|
3354
|
-
const fallbackReason = verifyApiError instanceof Error ? verifyApiError.message : String(verifyApiError);
|
|
3355
|
-
console.log(chalk.yellow('⚠️ Verify API unavailable, using local deterministic fallback.'));
|
|
3356
|
-
console.log(chalk.dim(` Reason: ${fallbackReason}`));
|
|
3357
|
-
}
|
|
3756
|
+
const runLocalDeterministicVerification = () => {
|
|
3358
3757
|
const localFileContents = {};
|
|
3359
3758
|
for (const file of changedFiles) {
|
|
3360
3759
|
const absolutePath = (0, path_1.join)(projectRoot, file.path);
|
|
@@ -3380,7 +3779,7 @@ async function verifyCommand(options) {
|
|
|
3380
3779
|
extraConstraintRules: hydratedCompiledPolicyRules.length > 0 ? hydratedCompiledPolicyRules : undefined,
|
|
3381
3780
|
fileContents: localFileContents,
|
|
3382
3781
|
});
|
|
3383
|
-
|
|
3782
|
+
return {
|
|
3384
3783
|
verificationId: `local-fallback-${Date.now()}`,
|
|
3385
3784
|
adherenceScore: localEvaluation.adherenceScore,
|
|
3386
3785
|
bloatCount: localEvaluation.bloatCount,
|
|
@@ -3391,6 +3790,35 @@ async function verifyCommand(options) {
|
|
|
3391
3790
|
diffSummary: localEvaluation.diffSummary,
|
|
3392
3791
|
message: localEvaluation.message,
|
|
3393
3792
|
};
|
|
3793
|
+
};
|
|
3794
|
+
let verifySource = 'api';
|
|
3795
|
+
let verifyResult;
|
|
3796
|
+
if (useLocalPlanSync) {
|
|
3797
|
+
verifySource = 'local_fallback';
|
|
3798
|
+
verifyResult = runLocalDeterministicVerification();
|
|
3799
|
+
}
|
|
3800
|
+
else {
|
|
3801
|
+
try {
|
|
3802
|
+
verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraintsForVerification, deterministicPolicyRules, 'api', compiledPolicyMetadata, {
|
|
3803
|
+
async: options.asyncMode === true,
|
|
3804
|
+
pollIntervalMs: Number.isFinite(options.verifyJobPollMs) ? options.verifyJobPollMs : undefined,
|
|
3805
|
+
timeoutMs: Number.isFinite(options.verifyJobTimeoutMs) ? options.verifyJobTimeoutMs : undefined,
|
|
3806
|
+
idempotencyKey: options.verifyIdempotencyKey,
|
|
3807
|
+
maxAttempts: Number.isFinite(options.verifyJobMaxAttempts) ? options.verifyJobMaxAttempts : undefined,
|
|
3808
|
+
});
|
|
3809
|
+
}
|
|
3810
|
+
catch (verifyApiError) {
|
|
3811
|
+
if (planFilesForVerification.length === 0) {
|
|
3812
|
+
throw verifyApiError;
|
|
3813
|
+
}
|
|
3814
|
+
verifySource = 'local_fallback';
|
|
3815
|
+
if (!options.json) {
|
|
3816
|
+
const fallbackReason = verifyApiError instanceof Error ? verifyApiError.message : String(verifyApiError);
|
|
3817
|
+
console.log(chalk.yellow('⚠️ Verify API unavailable, using local deterministic fallback.'));
|
|
3818
|
+
console.log(chalk.dim(` Reason: ${fallbackReason}`));
|
|
3819
|
+
}
|
|
3820
|
+
verifyResult = runLocalDeterministicVerification();
|
|
3821
|
+
}
|
|
3394
3822
|
}
|
|
3395
3823
|
const aiDebtEvaluation = (0, ai_debt_budget_1.evaluateAiDebtBudget)({
|
|
3396
3824
|
diffFiles,
|
|
@@ -3586,7 +4014,7 @@ async function verifyCommand(options) {
|
|
|
3586
4014
|
message: effectiveMessage,
|
|
3587
4015
|
bloatFiles: displayBloatFiles,
|
|
3588
4016
|
bloatCount: displayBloatFiles.length,
|
|
3589
|
-
}, policyViolations);
|
|
4017
|
+
}, policyViolations, expediteModeEnabled);
|
|
3590
4018
|
if (governanceResult) {
|
|
3591
4019
|
displayGovernanceInsights(governanceResult, { explain: options.explain });
|
|
3592
4020
|
}
|
|
@@ -3738,6 +4166,10 @@ async function verifyCommand(options) {
|
|
|
3738
4166
|
});
|
|
3739
4167
|
}
|
|
3740
4168
|
else {
|
|
4169
|
+
console.error(chalk.red('\n❌ Verification failed before completion.'));
|
|
4170
|
+
if (diffFiles.length > 0) {
|
|
4171
|
+
console.log(chalk.dim(` Partial context captured: ${diffFiles.length} changed file(s) in diff.`));
|
|
4172
|
+
}
|
|
3741
4173
|
if (error instanceof Error) {
|
|
3742
4174
|
if (error.message.includes('404') || error.message.includes('not found')) {
|
|
3743
4175
|
console.error(chalk.red(`❌ Error: Plan not found`));
|
|
@@ -3758,27 +4190,24 @@ async function verifyCommand(options) {
|
|
|
3758
4190
|
catch (error) {
|
|
3759
4191
|
if (options.json) {
|
|
3760
4192
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
3761
|
-
|
|
3762
|
-
grade: 'F',
|
|
3763
|
-
score: 0,
|
|
4193
|
+
emitCanonicalVerifyJson({
|
|
3764
4194
|
verdict: 'FAIL',
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
totalPlannedFiles: 0,
|
|
3771
|
-
message: `Unexpected error: ${errorMessage}`,
|
|
3772
|
-
scopeGuardPassed: false,
|
|
3773
|
-
scope: {
|
|
3774
|
-
scanRoot: process.cwd(),
|
|
3775
|
-
startDir: process.cwd(),
|
|
3776
|
-
gitRoot: null,
|
|
3777
|
-
linkedRepoOverrideUsed: false,
|
|
3778
|
-
linkedRepos: [],
|
|
3779
|
-
blockedOverride: null,
|
|
4195
|
+
summary: {
|
|
4196
|
+
totalFilesChanged: 0,
|
|
4197
|
+
totalViolations: 0,
|
|
4198
|
+
totalWarnings: 1,
|
|
4199
|
+
totalScopeIssues: 0,
|
|
3780
4200
|
},
|
|
3781
|
-
|
|
4201
|
+
violations: [],
|
|
4202
|
+
warnings: [
|
|
4203
|
+
{
|
|
4204
|
+
file: 'unknown',
|
|
4205
|
+
message: `Unexpected error: ${errorMessage}`,
|
|
4206
|
+
policy: 'verify_runtime',
|
|
4207
|
+
},
|
|
4208
|
+
],
|
|
4209
|
+
scopeIssues: [],
|
|
4210
|
+
});
|
|
3782
4211
|
}
|
|
3783
4212
|
else {
|
|
3784
4213
|
console.error(chalk.red('\n❌ Unexpected error:'));
|
|
@@ -4115,51 +4544,139 @@ function displayChangeContractDrift(summary, options = { advisory: false }) {
|
|
|
4115
4544
|
/**
|
|
4116
4545
|
* Display verification results in a formatted report card
|
|
4117
4546
|
*/
|
|
4118
|
-
function displayVerifyResults(result, policyViolations) {
|
|
4119
|
-
|
|
4120
|
-
|
|
4547
|
+
function displayVerifyResults(result, policyViolations, expediteModeUsed = false) {
|
|
4548
|
+
// ── Header ────────────────────────────────────────────────────────────────
|
|
4549
|
+
const headerLabel = result.verdict === 'PASS'
|
|
4550
|
+
? chalk.bold.green('\n✅ VERIFICATION PASSED')
|
|
4121
4551
|
: result.verdict === 'WARN'
|
|
4122
|
-
? chalk.yellow('
|
|
4123
|
-
: chalk.red('
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
const
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4552
|
+
? chalk.bold.yellow('\n⚠️ VERIFICATION PASSED WITH WARNINGS')
|
|
4553
|
+
: chalk.bold.red('\n❌ VERIFICATION FAILED');
|
|
4554
|
+
console.log(headerLabel);
|
|
4555
|
+
// ── Triage items ──────────────────────────────────────────────────────────
|
|
4556
|
+
const maxBlockingItems = 20;
|
|
4557
|
+
const maxAdvisoryItems = 8;
|
|
4558
|
+
const maxExpediteItems = 12;
|
|
4559
|
+
const policyItems = policyViolations || [];
|
|
4560
|
+
const isBlockingSeverity = (severityRaw) => {
|
|
4561
|
+
const normalized = String(severityRaw || '').toLowerCase();
|
|
4562
|
+
return normalized === 'block' || normalized === 'critical' || normalized === 'high';
|
|
4563
|
+
};
|
|
4564
|
+
const scopeItems = result.bloatFiles.map((file) => ({
|
|
4565
|
+
file,
|
|
4566
|
+
message: 'File modified outside intended scope',
|
|
4567
|
+
policy: 'scope_guard',
|
|
4568
|
+
}));
|
|
4569
|
+
const policyTriageItems = policyItems.map((item) => ({
|
|
4570
|
+
file: item.file,
|
|
4571
|
+
message: item.message || item.rule,
|
|
4572
|
+
policy: item.rule || 'policy_violation',
|
|
4573
|
+
severity: item.severity,
|
|
4574
|
+
}));
|
|
4575
|
+
let blockingItems = [
|
|
4576
|
+
...scopeItems.map((item) => ({
|
|
4577
|
+
file: item.file,
|
|
4578
|
+
message: item.message,
|
|
4579
|
+
})),
|
|
4580
|
+
...policyTriageItems
|
|
4581
|
+
.filter((item) => isBlockingSeverity(item.severity))
|
|
4582
|
+
.map((item) => ({
|
|
4583
|
+
file: item.file,
|
|
4584
|
+
message: item.message,
|
|
4585
|
+
})),
|
|
4586
|
+
];
|
|
4587
|
+
let advisoryItems = policyTriageItems
|
|
4588
|
+
.filter((item) => !isBlockingSeverity(item.severity))
|
|
4589
|
+
.map((item) => ({
|
|
4590
|
+
file: item.file,
|
|
4591
|
+
message: item.message,
|
|
4592
|
+
}));
|
|
4593
|
+
let expediteItems = [];
|
|
4594
|
+
if (expediteModeUsed) {
|
|
4595
|
+
blockingItems = [
|
|
4596
|
+
...scopeItems
|
|
4597
|
+
.filter((item) => isCriticalScopeBreach(item.file, item.message))
|
|
4598
|
+
.map((item) => ({ file: item.file, message: item.message })),
|
|
4599
|
+
...policyTriageItems
|
|
4600
|
+
.filter((item) => isSecurityOrAuthViolation(item.file, item.policy, item.message))
|
|
4601
|
+
.map((item) => ({ file: item.file, message: item.message })),
|
|
4602
|
+
];
|
|
4603
|
+
expediteItems = [
|
|
4604
|
+
...scopeItems
|
|
4605
|
+
.filter((item) => !isCriticalScopeBreach(item.file, item.message))
|
|
4606
|
+
.map((item) => ({ file: item.file, message: item.message })),
|
|
4607
|
+
...policyTriageItems
|
|
4608
|
+
.filter((item) => !isSecurityOrAuthViolation(item.file, item.policy, item.message))
|
|
4609
|
+
.map((item) => ({ file: item.file, message: item.message })),
|
|
4610
|
+
];
|
|
4611
|
+
advisoryItems = [];
|
|
4612
|
+
}
|
|
4613
|
+
// ── Counts ────────────────────────────────────────────────────────────────
|
|
4614
|
+
console.log(blockingItems.length > 0
|
|
4615
|
+
? chalk.red(`Blocking Issues: ${blockingItems.length}`)
|
|
4616
|
+
: chalk.dim('Blocking Issues: 0'));
|
|
4617
|
+
if (expediteModeUsed) {
|
|
4618
|
+
console.log(chalk.yellow(`Expedite Issues: ${expediteItems.length}`));
|
|
4619
|
+
}
|
|
4620
|
+
else {
|
|
4621
|
+
console.log(advisoryItems.length > 0
|
|
4622
|
+
? chalk.yellow(`Advisory Issues: ${advisoryItems.length}`)
|
|
4623
|
+
: chalk.dim('Advisory Issues: 0'));
|
|
4624
|
+
}
|
|
4625
|
+
console.log(chalk.dim(`Plan adherence: ${result.plannedFilesModified}/${result.totalPlannedFiles} files (${result.adherenceScore}%)`));
|
|
4626
|
+
// ── Top issues ────────────────────────────────────────────────────────────
|
|
4627
|
+
const topIssues = [
|
|
4628
|
+
...blockingItems,
|
|
4629
|
+
...(expediteModeUsed ? expediteItems : advisoryItems),
|
|
4630
|
+
].slice(0, 2);
|
|
4631
|
+
if (topIssues.length > 0) {
|
|
4632
|
+
console.log(chalk.bold('\nTop Issues:'));
|
|
4633
|
+
topIssues.forEach((item, i) => {
|
|
4634
|
+
console.log(` ${i + 1}. ${item.message} → ${chalk.cyan(item.file)}`);
|
|
4635
|
+
});
|
|
4636
|
+
}
|
|
4637
|
+
// ── Detailed lists ────────────────────────────────────────────────────────
|
|
4638
|
+
if (blockingItems.length > 0) {
|
|
4639
|
+
console.log(chalk.red(`\nBLOCKING (${blockingItems.length})`));
|
|
4640
|
+
blockingItems.slice(0, maxBlockingItems).forEach((item) => {
|
|
4641
|
+
console.log(` - ${item.file}: ${item.message}`);
|
|
4132
4642
|
});
|
|
4133
|
-
if (
|
|
4134
|
-
console.log(chalk.dim(` - ... ${
|
|
4643
|
+
if (blockingItems.length > maxBlockingItems) {
|
|
4644
|
+
console.log(chalk.dim(` - ... ${blockingItems.length - maxBlockingItems} more`));
|
|
4135
4645
|
}
|
|
4136
4646
|
}
|
|
4137
|
-
if (
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
});
|
|
4145
|
-
if (blocking.length > maxItems) {
|
|
4146
|
-
console.log(chalk.dim(` - ... ${blocking.length - maxItems} more`));
|
|
4147
|
-
}
|
|
4647
|
+
if (advisoryItems.length > 0) {
|
|
4648
|
+
console.log(chalk.yellow(`\nADVISORY (${advisoryItems.length})`));
|
|
4649
|
+
advisoryItems.slice(0, maxAdvisoryItems).forEach((item) => {
|
|
4650
|
+
console.log(` - ${item.file}: ${item.message}`);
|
|
4651
|
+
});
|
|
4652
|
+
if (advisoryItems.length > maxAdvisoryItems) {
|
|
4653
|
+
console.log(chalk.dim(` - ... ${advisoryItems.length - maxAdvisoryItems} more (summarized)`));
|
|
4148
4654
|
}
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
});
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
}
|
|
4655
|
+
}
|
|
4656
|
+
if (expediteModeUsed && expediteItems.length > 0) {
|
|
4657
|
+
console.log(chalk.yellow(`\nEXPEDITE (requires follow-up) (${expediteItems.length})`));
|
|
4658
|
+
expediteItems.slice(0, maxExpediteItems).forEach((item) => {
|
|
4659
|
+
console.log(` - ${item.file}: ${item.message}`);
|
|
4660
|
+
});
|
|
4661
|
+
if (expediteItems.length > maxExpediteItems) {
|
|
4662
|
+
console.log(chalk.dim(` - ... ${expediteItems.length - maxExpediteItems} more (summarized)`));
|
|
4157
4663
|
}
|
|
4664
|
+
console.log(chalk.dim(' Follow-up checklist:'));
|
|
4665
|
+
EXPEDITE_FOLLOW_UP_CHECKLIST.forEach((checkItem) => {
|
|
4666
|
+
console.log(chalk.dim(` - ${checkItem}`));
|
|
4667
|
+
});
|
|
4668
|
+
console.log(chalk.dim(' Note: Expedite Mode used'));
|
|
4669
|
+
}
|
|
4670
|
+
if (blockingItems.length === 0 && advisoryItems.length === 0 && expediteItems.length === 0) {
|
|
4671
|
+
console.log(chalk.green('\nNo issues detected.'));
|
|
4158
4672
|
}
|
|
4159
|
-
|
|
4160
|
-
|
|
4673
|
+
// ── Next step ─────────────────────────────────────────────────────────────
|
|
4674
|
+
if (blockingItems.length > 0 || advisoryItems.length > 0 || expediteItems.length > 0) {
|
|
4675
|
+
console.log(chalk.bold('\nNext step:'));
|
|
4676
|
+
console.log(` ${chalk.cyan('neurcode fix')}`);
|
|
4677
|
+
console.log(chalk.dim(' or: neurcode fix --apply-safe (auto-apply high-confidence patches)'));
|
|
4161
4678
|
}
|
|
4162
|
-
console.log(chalk.dim(`\
|
|
4679
|
+
console.log(chalk.dim(`\nDetails: ${result.message}\n`));
|
|
4163
4680
|
}
|
|
4164
4681
|
function printFirstRunAdvisoryMessage(demoMode) {
|
|
4165
4682
|
console.log(chalk.cyan('\nNeurcode first-run advisory mode'));
|