@neurcode-ai/cli 0.9.48 → 0.9.49
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/LICENSE +201 -0
- package/dist/commands/fix.d.ts +12 -0
- package/dist/commands/fix.d.ts.map +1 -0
- package/dist/commands/fix.js +380 -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 +117 -0
- package/dist/commands/generate.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 +65 -0
- package/dist/commands/start-intent.js.map +1 -0
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +604 -137
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +115 -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/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 +7 -8
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,303 @@ 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
|
+
if (severity === 'warning' || severity === 'info') {
|
|
851
|
+
addWarning(file, message, policy);
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
addViolation(file, message, policy, severity);
|
|
855
|
+
}
|
|
856
|
+
const payloadMessage = asStringValue(payload.message);
|
|
857
|
+
if (payloadMessage
|
|
858
|
+
&& violations.length === 0
|
|
859
|
+
&& warnings.length === 0
|
|
860
|
+
&& scopeIssues.length === 0) {
|
|
861
|
+
addWarning('unknown', payloadMessage, 'verify_result');
|
|
862
|
+
}
|
|
863
|
+
const summaryRecord = asObjectRecord(payload.summary);
|
|
864
|
+
const fileSet = new Set();
|
|
865
|
+
for (const violation of violations)
|
|
866
|
+
fileSet.add(violation.file);
|
|
867
|
+
for (const warning of warnings)
|
|
868
|
+
fileSet.add(warning.file);
|
|
869
|
+
for (const scopeIssue of scopeIssues)
|
|
870
|
+
fileSet.add(scopeIssue.file);
|
|
871
|
+
const totalFilesChanged = (() => {
|
|
872
|
+
const fromSummary = summaryRecord ? asNumberValue(summaryRecord.totalFilesChanged) : null;
|
|
873
|
+
if (fromSummary !== null)
|
|
874
|
+
return Math.max(0, Math.floor(fromSummary));
|
|
875
|
+
const blastRadius = asObjectRecord(payload.blastRadius);
|
|
876
|
+
const fromBlastRadius = blastRadius ? asNumberValue(blastRadius.filesChanged) : null;
|
|
877
|
+
if (fromBlastRadius !== null)
|
|
878
|
+
return Math.max(0, Math.floor(fromBlastRadius));
|
|
879
|
+
return fileSet.size;
|
|
880
|
+
})();
|
|
881
|
+
const driftScoreRaw = asNumberValue(payload.driftScore);
|
|
882
|
+
const driftScore = driftScoreRaw === null
|
|
883
|
+
? undefined
|
|
884
|
+
: Math.max(0, Math.min(100, Math.round(driftScoreRaw)));
|
|
885
|
+
const expediteModeUsed = resolveExpediteModeFromPayload(payload);
|
|
886
|
+
const scopeTriageItems = scopeIssues.map((item) => ({
|
|
887
|
+
file: item.file,
|
|
888
|
+
message: item.message,
|
|
889
|
+
policy: 'scope_guard',
|
|
890
|
+
severity: 'block',
|
|
891
|
+
source: 'scope',
|
|
892
|
+
}));
|
|
893
|
+
const violationTriageItems = violations.map((item) => ({
|
|
894
|
+
file: item.file,
|
|
895
|
+
message: item.message,
|
|
896
|
+
policy: item.policy,
|
|
897
|
+
severity: item.severity,
|
|
898
|
+
source: 'violation',
|
|
899
|
+
}));
|
|
900
|
+
const warningTriageItems = warnings.map((item) => ({
|
|
901
|
+
file: item.file,
|
|
902
|
+
message: item.message,
|
|
903
|
+
policy: item.policy,
|
|
904
|
+
severity: 'warning',
|
|
905
|
+
source: 'warning',
|
|
906
|
+
}));
|
|
907
|
+
const defaultBlockingItems = dedupeTriageItems([
|
|
908
|
+
...scopeTriageItems,
|
|
909
|
+
...violationTriageItems.filter((item) => item.severity === 'critical' || item.severity === 'high'),
|
|
910
|
+
]);
|
|
911
|
+
const defaultAdvisoryItems = dedupeTriageItems([
|
|
912
|
+
...warningTriageItems,
|
|
913
|
+
...violationTriageItems.filter((item) => item.severity === 'warning' || item.severity === 'info'),
|
|
914
|
+
]);
|
|
915
|
+
const expediteBlockingItems = dedupeTriageItems([
|
|
916
|
+
...scopeTriageItems.filter((item) => isCriticalScopeBreach(item.file, item.message)),
|
|
917
|
+
...violationTriageItems.filter((item) => isSecurityOrAuthViolation(item.file, item.policy, item.message)),
|
|
918
|
+
...warningTriageItems
|
|
919
|
+
.filter((item) => isSecurityOrAuthViolation(item.file, item.policy, item.message))
|
|
920
|
+
.map((item) => ({
|
|
921
|
+
...item,
|
|
922
|
+
source: 'violation',
|
|
923
|
+
})),
|
|
924
|
+
]);
|
|
925
|
+
const expediteItems = dedupeTriageItems([
|
|
926
|
+
...scopeTriageItems
|
|
927
|
+
.filter((item) => !isCriticalScopeBreach(item.file, item.message))
|
|
928
|
+
.map((item) => ({
|
|
929
|
+
...item,
|
|
930
|
+
source: 'expedite',
|
|
931
|
+
})),
|
|
932
|
+
...violationTriageItems
|
|
933
|
+
.filter((item) => !isSecurityOrAuthViolation(item.file, item.policy, item.message))
|
|
934
|
+
.map((item) => ({
|
|
935
|
+
...item,
|
|
936
|
+
source: 'expedite',
|
|
937
|
+
})),
|
|
938
|
+
...warningTriageItems
|
|
939
|
+
.filter((item) => !isSecurityOrAuthViolation(item.file, item.policy, item.message))
|
|
940
|
+
.map((item) => ({
|
|
941
|
+
...item,
|
|
942
|
+
source: 'expedite',
|
|
943
|
+
})),
|
|
944
|
+
]);
|
|
945
|
+
const blockingItems = expediteModeUsed ? expediteBlockingItems : defaultBlockingItems;
|
|
946
|
+
const advisoryItems = expediteModeUsed ? expediteItems : defaultAdvisoryItems;
|
|
947
|
+
return {
|
|
948
|
+
verdict,
|
|
949
|
+
summary: {
|
|
950
|
+
totalFilesChanged,
|
|
951
|
+
totalViolations: violations.length,
|
|
952
|
+
totalWarnings: warnings.length,
|
|
953
|
+
totalScopeIssues: scopeIssues.length,
|
|
954
|
+
},
|
|
955
|
+
violations,
|
|
956
|
+
warnings,
|
|
957
|
+
scopeIssues,
|
|
958
|
+
blockingCount: blockingItems.length,
|
|
959
|
+
advisoryCount: advisoryItems.length,
|
|
960
|
+
blockingItems,
|
|
961
|
+
advisoryItems,
|
|
962
|
+
expediteModeUsed,
|
|
963
|
+
expediteCount: expediteModeUsed ? expediteItems.length : 0,
|
|
964
|
+
expediteItems: expediteModeUsed ? expediteItems : [],
|
|
965
|
+
expediteFollowUpChecklist: expediteModeUsed ? [...EXPEDITE_FOLLOW_UP_CHECKLIST] : [],
|
|
966
|
+
...(expediteModeUsed ? { expediteNote: 'Expedite Mode used' } : {}),
|
|
967
|
+
...(typeof driftScore === 'number' ? { driftScore } : {}),
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
function emitCanonicalVerifyJson(payload) {
|
|
971
|
+
console.log(JSON.stringify(toCanonicalVerifyOutput(payload), null, 2));
|
|
972
|
+
}
|
|
675
973
|
function buildDeterministicLayerSummary(payload) {
|
|
676
974
|
const verdict = asStringValue(payload.verdict) || 'UNKNOWN';
|
|
677
975
|
const mode = asStringValue(payload.mode) || 'unknown';
|
|
@@ -841,6 +1139,13 @@ function isGitRepository(cwd) {
|
|
|
841
1139
|
return false;
|
|
842
1140
|
}
|
|
843
1141
|
}
|
|
1142
|
+
function resolveVerifyExpediteMode(projectRoot) {
|
|
1143
|
+
if (isEnabledFlag(process.env.NEURCODE_EXPEDITE_MODE) || isEnabledFlag(process.env.NEURCODE_MCP_EXPEDITE_MODE)) {
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
const branchName = (0, git_1.detectCurrentGitBranch)(projectRoot) || process.env.GITHUB_REF_NAME || '';
|
|
1147
|
+
return containsAnyToken(branchName, ['hotfix', 'urgent', 'prod-down', 'prod_down', 'prod down', 'incident', 'expedite']);
|
|
1148
|
+
}
|
|
844
1149
|
function isSignedAiLogsRequired(orgGovernanceSettings) {
|
|
845
1150
|
const explicitRequirement = isEnabledFlag(process.env.NEURCODE_GOVERNANCE_REQUIRE_SIGNED_LOGS) ||
|
|
846
1151
|
isEnabledFlag(process.env.NEURCODE_AI_LOG_REQUIRE_SIGNED);
|
|
@@ -1012,18 +1317,12 @@ async function recordVerificationIfRequested(options, config, payload) {
|
|
|
1012
1317
|
* Execute policy-only verification (General Governance mode)
|
|
1013
1318
|
* Returns the exit code to use
|
|
1014
1319
|
*/
|
|
1015
|
-
async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source, scopeTelemetry, projectId, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner, compiledPolicyArtifact, compiledPolicyMetadata, changeContractSummary) {
|
|
1320
|
+
async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source, scopeTelemetry, projectId, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner, expediteModeEnabled, compiledPolicyArtifact, compiledPolicyMetadata, changeContractSummary) {
|
|
1016
1321
|
const emitPolicyOnlyJson = (payload) => {
|
|
1017
|
-
|
|
1322
|
+
emitCanonicalVerifyJson({
|
|
1018
1323
|
...payload,
|
|
1019
|
-
|
|
1020
|
-
};
|
|
1021
|
-
console.log(JSON.stringify({
|
|
1022
|
-
...enrichedPayload,
|
|
1023
|
-
...(compiledPolicyMetadata ? { policyCompilation: compiledPolicyMetadata } : {}),
|
|
1024
|
-
changeContract: changeContractSummary,
|
|
1025
|
-
scope: scopeTelemetry,
|
|
1026
|
-
}, null, 2));
|
|
1324
|
+
expediteMode: expediteModeEnabled,
|
|
1325
|
+
});
|
|
1027
1326
|
};
|
|
1028
1327
|
const policyOnlyVerificationSource = 'policy_only';
|
|
1029
1328
|
const recordPolicyOnlyVerification = async (payload) => recordVerificationIfRequested(options, config, {
|
|
@@ -1582,14 +1881,15 @@ async function verifyCommand(options) {
|
|
|
1582
1881
|
try {
|
|
1583
1882
|
const rootResolution = (0, project_root_1.resolveNeurcodeProjectRootWithTrace)(process.cwd());
|
|
1584
1883
|
const projectRoot = rootResolution.projectRoot;
|
|
1884
|
+
const localPlanSync = (0, plan_sync_1.ensureLocalPlan)(projectRoot);
|
|
1885
|
+
const localPlanExpectedFiles = [...localPlanSync.expectedFiles];
|
|
1886
|
+
const expediteModeEnabled = resolveVerifyExpediteMode(projectRoot);
|
|
1585
1887
|
const scopeTelemetry = (0, scope_telemetry_1.buildScopeTelemetryPayload)(rootResolution);
|
|
1586
1888
|
const emitVerifyJson = (payload) => {
|
|
1587
|
-
|
|
1889
|
+
emitCanonicalVerifyJson({
|
|
1588
1890
|
...payload,
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
};
|
|
1592
|
-
console.log(JSON.stringify(jsonPayload, null, 2));
|
|
1891
|
+
expediteMode: expediteModeEnabled,
|
|
1892
|
+
});
|
|
1593
1893
|
};
|
|
1594
1894
|
if (!isGitRepository(projectRoot)) {
|
|
1595
1895
|
const message = 'Verify requires a git repository. Initialize git (`git init`) or run this command inside an existing git project.';
|
|
@@ -2069,34 +2369,44 @@ async function verifyCommand(options) {
|
|
|
2069
2369
|
});
|
|
2070
2370
|
process.exit(2);
|
|
2071
2371
|
}
|
|
2072
|
-
// Determine which diff to capture
|
|
2372
|
+
// Determine which diff to capture.
|
|
2073
2373
|
let diffText;
|
|
2374
|
+
let diffContextLabel = '';
|
|
2074
2375
|
if (options.staged) {
|
|
2075
2376
|
diffText = (0, child_process_1.execSync)('git diff --cached', { maxBuffer: 1024 * 1024 * 1024, encoding: 'utf-8' });
|
|
2377
|
+
diffContextLabel = 'staged changes';
|
|
2076
2378
|
}
|
|
2077
2379
|
else if (options.base) {
|
|
2078
2380
|
diffText = (0, git_1.getDiffFromBase)(options.base);
|
|
2381
|
+
diffContextLabel = `working tree vs ${options.base}`;
|
|
2079
2382
|
}
|
|
2080
2383
|
else if (options.head) {
|
|
2081
2384
|
diffText = (0, child_process_1.execSync)('git diff HEAD', { maxBuffer: 1024 * 1024 * 1024, encoding: 'utf-8' });
|
|
2385
|
+
diffContextLabel = 'working tree vs HEAD';
|
|
2082
2386
|
}
|
|
2083
2387
|
else {
|
|
2084
|
-
// Default:
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
diffText =
|
|
2388
|
+
// Default: resolve a PR-like base context first (origin/main or origin/master).
|
|
2389
|
+
// Fallback to staged diff when base context cannot be resolved.
|
|
2390
|
+
const defaultContext = (0, git_1.resolveDefaultDiffContext)(projectRoot);
|
|
2391
|
+
if (defaultContext.mode === 'base' && defaultContext.baseRef) {
|
|
2392
|
+
diffText = (0, git_1.getDiffFromBase)(defaultContext.baseRef);
|
|
2393
|
+
diffContextLabel = defaultContext.currentBranch
|
|
2394
|
+
? `${defaultContext.currentBranch} vs ${defaultContext.baseRef}`
|
|
2395
|
+
: `working tree vs ${defaultContext.baseRef}`;
|
|
2089
2396
|
}
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2397
|
+
else {
|
|
2398
|
+
diffText = (0, child_process_1.execSync)('git diff --cached', { maxBuffer: 1024 * 1024 * 1024, encoding: 'utf-8' });
|
|
2399
|
+
diffContextLabel = 'staged changes (fallback)';
|
|
2093
2400
|
}
|
|
2094
2401
|
}
|
|
2402
|
+
if (!options.json && diffContextLabel) {
|
|
2403
|
+
console.log(chalk.dim(` Diff context: ${diffContextLabel}`));
|
|
2404
|
+
}
|
|
2095
2405
|
const untrackedDiffFiles = getUntrackedDiffFiles(projectRoot);
|
|
2096
2406
|
if (!diffText.trim() && untrackedDiffFiles.length === 0) {
|
|
2097
2407
|
if (!options.json) {
|
|
2098
|
-
console.log(chalk.yellow('⚠️ No changes detected'));
|
|
2099
|
-
console.log(chalk.dim('
|
|
2408
|
+
console.log(chalk.yellow('⚠️ No changes detected in current diff context.'));
|
|
2409
|
+
console.log(chalk.dim(' Tip: Ensure changes are staged or run against a base branch.'));
|
|
2100
2410
|
}
|
|
2101
2411
|
else {
|
|
2102
2412
|
emitVerifyJson({
|
|
@@ -2109,7 +2419,7 @@ async function verifyCommand(options) {
|
|
|
2109
2419
|
bloatFiles: [],
|
|
2110
2420
|
plannedFilesModified: 0,
|
|
2111
2421
|
totalPlannedFiles: 0,
|
|
2112
|
-
message: 'No changes detected',
|
|
2422
|
+
message: 'No changes detected in current diff context.',
|
|
2113
2423
|
scopeGuardPassed: false,
|
|
2114
2424
|
});
|
|
2115
2425
|
}
|
|
@@ -2139,7 +2449,8 @@ async function verifyCommand(options) {
|
|
|
2139
2449
|
const summary = (0, diff_parser_1.getDiffSummary)(diffFiles);
|
|
2140
2450
|
if (diffFiles.length === 0) {
|
|
2141
2451
|
if (!options.json) {
|
|
2142
|
-
console.log(chalk.yellow('⚠️ No
|
|
2452
|
+
console.log(chalk.yellow('⚠️ No changes detected in current diff context.'));
|
|
2453
|
+
console.log(chalk.dim(' Tip: Ensure changes are staged or run against a base branch.'));
|
|
2143
2454
|
}
|
|
2144
2455
|
else {
|
|
2145
2456
|
emitVerifyJson({
|
|
@@ -2152,7 +2463,7 @@ async function verifyCommand(options) {
|
|
|
2152
2463
|
bloatFiles: [],
|
|
2153
2464
|
plannedFilesModified: 0,
|
|
2154
2465
|
totalPlannedFiles: 0,
|
|
2155
|
-
message: 'No
|
|
2466
|
+
message: 'No changes detected in current diff context.',
|
|
2156
2467
|
scopeGuardPassed: false,
|
|
2157
2468
|
});
|
|
2158
2469
|
}
|
|
@@ -2382,7 +2693,7 @@ async function verifyCommand(options) {
|
|
|
2382
2693
|
}
|
|
2383
2694
|
}
|
|
2384
2695
|
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);
|
|
2696
|
+
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
2697
|
const changedFiles = diffFiles.map((f) => f.path);
|
|
2387
2698
|
const verdict = exitCode === 2 ? 'FAIL' : exitCode === 1 ? 'WARN' : 'PASS';
|
|
2388
2699
|
recordVerifyEvent(verdict, `policy_only_source=${source};exit=${exitCode}`, changedFiles);
|
|
@@ -2397,6 +2708,7 @@ async function verifyCommand(options) {
|
|
|
2397
2708
|
const requirePlan = options.requirePlan === true
|
|
2398
2709
|
|| process.env.NEURCODE_VERIFY_REQUIRE_PLAN === '1'
|
|
2399
2710
|
|| strictArtifactMode;
|
|
2711
|
+
let useLocalPlanSync = false;
|
|
2400
2712
|
// Get planId: Priority 1: options flag, Priority 2: state file (.neurcode/config.json), Priority 3: legacy config
|
|
2401
2713
|
let planId = options.planId;
|
|
2402
2714
|
if (!planId) {
|
|
@@ -2429,6 +2741,19 @@ async function verifyCommand(options) {
|
|
|
2429
2741
|
}
|
|
2430
2742
|
}
|
|
2431
2743
|
}
|
|
2744
|
+
if (planId === 'local-plan-sync' && localPlanExpectedFiles.length > 0) {
|
|
2745
|
+
useLocalPlanSync = true;
|
|
2746
|
+
if (!options.json) {
|
|
2747
|
+
console.log(chalk.dim(` Using Plan Sync from .neurcode/plan.json (${localPlanExpectedFiles.length} expected file(s))`));
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
if (!planId && localPlanExpectedFiles.length > 0) {
|
|
2751
|
+
planId = 'local-plan-sync';
|
|
2752
|
+
useLocalPlanSync = true;
|
|
2753
|
+
if (!options.json) {
|
|
2754
|
+
console.log(chalk.dim(` Using Plan Sync from .neurcode/plan.json (${localPlanExpectedFiles.length} expected file(s))`));
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2432
2757
|
// If no planId found, either enforce strict requirement or fall back to policy-only mode.
|
|
2433
2758
|
if (!planId) {
|
|
2434
2759
|
if (requirePlan) {
|
|
@@ -2583,37 +2908,60 @@ async function verifyCommand(options) {
|
|
|
2583
2908
|
}
|
|
2584
2909
|
// Track if scope guard passed - this takes priority over AI grading
|
|
2585
2910
|
let scopeGuardPassed = false;
|
|
2911
|
+
let scopeGuardExpediteBypass = false;
|
|
2586
2912
|
let governanceResult = null;
|
|
2587
2913
|
let planFilesForVerification = [];
|
|
2588
2914
|
let intentConstraintsForVerification;
|
|
2589
2915
|
try {
|
|
2590
2916
|
// Step A: Get Modified Files (already have from diffFiles)
|
|
2591
2917
|
const modifiedFiles = diffFiles.map(f => f.path);
|
|
2592
|
-
// Step B:
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2918
|
+
// Step B: Resolve plan scope from remote plan or local Plan Sync.
|
|
2919
|
+
let originalIntent = '';
|
|
2920
|
+
let governanceTask = 'Plan verification';
|
|
2921
|
+
let planFiles = [];
|
|
2922
|
+
let planDependencies = [];
|
|
2923
|
+
let remotePlanSessionId = null;
|
|
2924
|
+
if (useLocalPlanSync) {
|
|
2925
|
+
const localIntent = (localPlanSync.intent || '').trim();
|
|
2926
|
+
const localConstraintText = localPlanSync.constraints.length > 0
|
|
2927
|
+
? localPlanSync.constraints.join('; ')
|
|
2928
|
+
: '';
|
|
2929
|
+
planFiles = [...localPlanExpectedFiles];
|
|
2930
|
+
originalIntent = localIntent || localConstraintText;
|
|
2931
|
+
governanceTask = localIntent
|
|
2932
|
+
? `Local Plan Sync: ${localIntent}`
|
|
2933
|
+
: 'Local Plan Sync verification';
|
|
2934
|
+
if (!options.json) {
|
|
2935
|
+
console.log(chalk.dim(` Plan Sync scope loaded: ${planFiles.length} file(s)`));
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
else {
|
|
2939
|
+
const planData = await client.getPlan(finalPlanId);
|
|
2940
|
+
// Extract original intent from plan (for constraint checking)
|
|
2941
|
+
originalIntent = planData.intent || '';
|
|
2942
|
+
const planTitle = typeof planData.content.title === 'string'
|
|
2943
|
+
? planData.content.title?.trim()
|
|
2944
|
+
: '';
|
|
2945
|
+
const planSummary = typeof planData.content.summary === 'string' ? planData.content.summary.trim() : '';
|
|
2946
|
+
governanceTask = planTitle || planSummary || originalIntent || 'Plan verification';
|
|
2947
|
+
// Get approved files from plan (only files with action CREATE or MODIFY)
|
|
2948
|
+
planFiles = planData.content.files
|
|
2949
|
+
.filter((f) => f.action === 'CREATE' || f.action === 'MODIFY')
|
|
2950
|
+
.map((f) => f.path);
|
|
2951
|
+
planDependencies = Array.isArray(planData.content.dependencies)
|
|
2952
|
+
? planData.content.dependencies.filter((item) => typeof item === 'string')
|
|
2953
|
+
: [];
|
|
2954
|
+
remotePlanSessionId = planData.sessionId || null;
|
|
2955
|
+
}
|
|
2956
|
+
planFilesForVerification = [...new Set([...planFiles, ...localPlanExpectedFiles])];
|
|
2606
2957
|
intentConstraintsForVerification = originalIntent || undefined;
|
|
2607
|
-
const planDependencies = Array.isArray(planData.content.dependencies)
|
|
2608
|
-
? planData.content.dependencies.filter((item) => typeof item === 'string')
|
|
2609
|
-
: [];
|
|
2610
2958
|
governanceResult = (0, governance_1.evaluateGovernance)({
|
|
2611
2959
|
projectRoot,
|
|
2612
2960
|
task: governanceTask,
|
|
2613
|
-
expectedFiles:
|
|
2961
|
+
expectedFiles: planFilesForVerification,
|
|
2614
2962
|
expectedDependencies: planDependencies,
|
|
2615
2963
|
diffFiles,
|
|
2616
|
-
contextCandidates:
|
|
2964
|
+
contextCandidates: planFilesForVerification,
|
|
2617
2965
|
orgGovernance: orgGovernanceSettings,
|
|
2618
2966
|
requireSignedAiLogs: signedLogsRequired,
|
|
2619
2967
|
signingKey: aiLogSigningKey,
|
|
@@ -2626,8 +2974,8 @@ async function verifyCommand(options) {
|
|
|
2626
2974
|
// This is the session_id string needed to fetch the session
|
|
2627
2975
|
let sessionIdString = (0, state_1.getSessionId)() || configData.sessionId || configData.lastSessionId || null;
|
|
2628
2976
|
// Fallback: Use sessionId from plan if not in config
|
|
2629
|
-
if (!sessionIdString &&
|
|
2630
|
-
sessionIdString =
|
|
2977
|
+
if (!sessionIdString && remotePlanSessionId) {
|
|
2978
|
+
sessionIdString = remotePlanSessionId;
|
|
2631
2979
|
if ((process.env.DEBUG || process.env.VERBOSE) && !options.json) {
|
|
2632
2980
|
console.log(chalk.dim(` Using sessionId from plan: ${sessionIdString.substring(0, 8)}...`));
|
|
2633
2981
|
}
|
|
@@ -2661,17 +3009,24 @@ async function verifyCommand(options) {
|
|
|
2661
3009
|
}
|
|
2662
3010
|
}
|
|
2663
3011
|
// Step C: The Intersection Logic
|
|
2664
|
-
const approvedSet = new Set([...
|
|
3012
|
+
const approvedSet = new Set([...planFilesForVerification, ...allowedFiles]);
|
|
2665
3013
|
const violations = modifiedFiles.filter(f => !approvedSet.has(f));
|
|
2666
3014
|
const filteredViolations = violations.filter((p) => !shouldIgnore(p));
|
|
2667
3015
|
// Step D: The Block (only report scope violations for non-ignored files)
|
|
2668
3016
|
if (filteredViolations.length > 0) {
|
|
3017
|
+
const criticalScopeViolations = expediteModeEnabled
|
|
3018
|
+
? filteredViolations.filter((file) => isCriticalScopeBreach(file, 'File modified outside the plan'))
|
|
3019
|
+
: filteredViolations;
|
|
3020
|
+
const expediteScopeViolations = expediteModeEnabled
|
|
3021
|
+
? filteredViolations.filter((file) => !criticalScopeViolations.includes(file))
|
|
3022
|
+
: [];
|
|
3023
|
+
const shouldBlockForScope = !expediteModeEnabled || criticalScopeViolations.length > 0;
|
|
2669
3024
|
const aiDebtSummaryForScope = toAiDebtSummary((0, ai_debt_budget_1.evaluateAiDebtBudget)({
|
|
2670
3025
|
diffFiles,
|
|
2671
3026
|
bloatCount: filteredViolations.length,
|
|
2672
3027
|
config: aiDebtConfig,
|
|
2673
3028
|
}));
|
|
2674
|
-
recordVerifyEvent('FAIL',
|
|
3029
|
+
recordVerifyEvent(shouldBlockForScope ? 'FAIL' : 'WARN', `${shouldBlockForScope ? 'scope_violation' : 'scope_expedite'}=${filteredViolations.length}`, modifiedFiles, finalPlanId);
|
|
2675
3030
|
const scopeViolationItems = filteredViolations.map((file) => ({
|
|
2676
3031
|
file,
|
|
2677
3032
|
rule: 'scope_guard',
|
|
@@ -2683,8 +3038,10 @@ async function verifyCommand(options) {
|
|
|
2683
3038
|
...scopeViolationItems,
|
|
2684
3039
|
...aiDebtViolationItems,
|
|
2685
3040
|
];
|
|
2686
|
-
const scopeViolationMessage =
|
|
2687
|
-
|
|
3041
|
+
const scopeViolationMessage = shouldBlockForScope
|
|
3042
|
+
? `Scope violation: ${criticalScopeViolations.length} critical file(s) modified outside the plan`
|
|
3043
|
+
: `Expedite scope warning: ${expediteScopeViolations.length} non-critical file(s) modified outside the plan`;
|
|
3044
|
+
if (shouldBlockForScope && options.json) {
|
|
2688
3045
|
// Output JSON for scope violation BEFORE exit. Must include violations for GitHub Action annotations.
|
|
2689
3046
|
const jsonOutput = {
|
|
2690
3047
|
grade: 'F',
|
|
@@ -2695,12 +3052,13 @@ async function verifyCommand(options) {
|
|
|
2695
3052
|
bloatCount: filteredViolations.length,
|
|
2696
3053
|
bloatFiles: filteredViolations,
|
|
2697
3054
|
plannedFilesModified: 0,
|
|
2698
|
-
totalPlannedFiles:
|
|
3055
|
+
totalPlannedFiles: planFilesForVerification.length,
|
|
2699
3056
|
message: scopeViolationMessage,
|
|
2700
3057
|
scopeGuardPassed: false,
|
|
2701
3058
|
mode: 'plan_enforced',
|
|
2702
3059
|
policyOnly: false,
|
|
2703
3060
|
aiDebt: aiDebtSummaryForScope,
|
|
3061
|
+
...(expediteModeEnabled ? { expediteMode: true } : {}),
|
|
2704
3062
|
...(governanceResult
|
|
2705
3063
|
? buildGovernancePayload(governanceResult, orgGovernanceSettings, {
|
|
2706
3064
|
changeContract: changeContractSummary,
|
|
@@ -2733,18 +3091,25 @@ async function verifyCommand(options) {
|
|
|
2733
3091
|
});
|
|
2734
3092
|
process.exit(1);
|
|
2735
3093
|
}
|
|
2736
|
-
else {
|
|
3094
|
+
else if (shouldBlockForScope) {
|
|
2737
3095
|
// Human-readable output only when NOT in json mode
|
|
2738
3096
|
console.log(chalk.red('\n⛔ SCOPE VIOLATION'));
|
|
2739
3097
|
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
2740
3098
|
console.log(chalk.red('The following files were modified but are not in the plan:'));
|
|
2741
3099
|
console.log('');
|
|
2742
|
-
|
|
3100
|
+
criticalScopeViolations.forEach(file => {
|
|
2743
3101
|
console.log(chalk.red(` • ${file}`));
|
|
2744
3102
|
});
|
|
3103
|
+
if (expediteModeEnabled && expediteScopeViolations.length > 0) {
|
|
3104
|
+
console.log('');
|
|
3105
|
+
console.log(chalk.yellow('Non-critical scope files (can be followed up under expedite mode):'));
|
|
3106
|
+
expediteScopeViolations.forEach((file) => {
|
|
3107
|
+
console.log(chalk.yellow(` • ${file}`));
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
2745
3110
|
console.log('');
|
|
2746
3111
|
console.log(chalk.yellow('To unblock these files, run:'));
|
|
2747
|
-
|
|
3112
|
+
criticalScopeViolations.forEach(file => {
|
|
2748
3113
|
console.log(chalk.dim(` neurcode allow ${file}`));
|
|
2749
3114
|
});
|
|
2750
3115
|
if (aiDebtSummaryForScope.mode !== 'off') {
|
|
@@ -2791,11 +3156,30 @@ async function verifyCommand(options) {
|
|
|
2791
3156
|
});
|
|
2792
3157
|
process.exit(1);
|
|
2793
3158
|
}
|
|
3159
|
+
else {
|
|
3160
|
+
scopeGuardExpediteBypass = true;
|
|
3161
|
+
if (!options.json) {
|
|
3162
|
+
console.log(chalk.yellow('\n⚠️ Expedite scope relaxation applied (non-critical scope only).'));
|
|
3163
|
+
expediteScopeViolations.forEach((file) => {
|
|
3164
|
+
console.log(chalk.yellow(` • ${file}`));
|
|
3165
|
+
});
|
|
3166
|
+
console.log(chalk.dim(' Follow-up checklist:'));
|
|
3167
|
+
EXPEDITE_FOLLOW_UP_CHECKLIST.forEach((item) => {
|
|
3168
|
+
console.log(chalk.dim(` - ${item}`));
|
|
3169
|
+
});
|
|
3170
|
+
console.log(chalk.dim(' Note: Expedite Mode used\n'));
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
2794
3173
|
}
|
|
2795
3174
|
// Scope guard passed - all files are approved or allowed
|
|
2796
3175
|
scopeGuardPassed = true;
|
|
2797
3176
|
if (!options.json) {
|
|
2798
|
-
|
|
3177
|
+
if (scopeGuardExpediteBypass) {
|
|
3178
|
+
console.log(chalk.green('✅ Scope guard passed with expedite relaxation for non-critical scope changes'));
|
|
3179
|
+
}
|
|
3180
|
+
else {
|
|
3181
|
+
console.log(chalk.green('✅ All modified files are approved or allowed'));
|
|
3182
|
+
}
|
|
2799
3183
|
console.log('');
|
|
2800
3184
|
}
|
|
2801
3185
|
}
|
|
@@ -2879,7 +3263,7 @@ async function verifyCommand(options) {
|
|
|
2879
3263
|
const lockViolationItems = toPolicyLockViolations(policyLockEvaluation.mismatches);
|
|
2880
3264
|
recordVerifyEvent('FAIL', 'policy_lock_mismatch', diffFiles.map((f) => f.path), finalPlanId);
|
|
2881
3265
|
if (options.json) {
|
|
2882
|
-
|
|
3266
|
+
emitVerifyJson({
|
|
2883
3267
|
grade: 'F',
|
|
2884
3268
|
score: 0,
|
|
2885
3269
|
verdict: 'FAIL',
|
|
@@ -2907,7 +3291,7 @@ async function verifyCommand(options) {
|
|
|
2907
3291
|
path: policyLockEvaluation.lockPath,
|
|
2908
3292
|
mismatches: policyLockEvaluation.mismatches,
|
|
2909
3293
|
},
|
|
2910
|
-
}
|
|
3294
|
+
});
|
|
2911
3295
|
}
|
|
2912
3296
|
else {
|
|
2913
3297
|
console.log(chalk.red('\n❌ Policy lock baseline mismatch'));
|
|
@@ -3326,35 +3710,20 @@ async function verifyCommand(options) {
|
|
|
3326
3710
|
displayChangeContractDrift(changeContractSummary, { advisory: true });
|
|
3327
3711
|
}
|
|
3328
3712
|
}
|
|
3329
|
-
// Call verify API
|
|
3713
|
+
// Call verify API (or deterministic local evaluation for Plan Sync scope mode)
|
|
3330
3714
|
if (!options.json) {
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3715
|
+
if (useLocalPlanSync) {
|
|
3716
|
+
console.log(chalk.dim(' Using local Plan Sync deterministic verification (no API plan lookup).\n'));
|
|
3717
|
+
}
|
|
3718
|
+
else {
|
|
3719
|
+
console.log(chalk.dim(' Sending to Neurcode API...\n'));
|
|
3720
|
+
if (options.asyncMode) {
|
|
3721
|
+
console.log(chalk.dim(' Queue-backed verification enabled (async job mode).'));
|
|
3722
|
+
}
|
|
3334
3723
|
}
|
|
3335
3724
|
}
|
|
3336
3725
|
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
|
-
}
|
|
3726
|
+
const runLocalDeterministicVerification = () => {
|
|
3358
3727
|
const localFileContents = {};
|
|
3359
3728
|
for (const file of changedFiles) {
|
|
3360
3729
|
const absolutePath = (0, path_1.join)(projectRoot, file.path);
|
|
@@ -3380,7 +3749,7 @@ async function verifyCommand(options) {
|
|
|
3380
3749
|
extraConstraintRules: hydratedCompiledPolicyRules.length > 0 ? hydratedCompiledPolicyRules : undefined,
|
|
3381
3750
|
fileContents: localFileContents,
|
|
3382
3751
|
});
|
|
3383
|
-
|
|
3752
|
+
return {
|
|
3384
3753
|
verificationId: `local-fallback-${Date.now()}`,
|
|
3385
3754
|
adherenceScore: localEvaluation.adherenceScore,
|
|
3386
3755
|
bloatCount: localEvaluation.bloatCount,
|
|
@@ -3391,6 +3760,35 @@ async function verifyCommand(options) {
|
|
|
3391
3760
|
diffSummary: localEvaluation.diffSummary,
|
|
3392
3761
|
message: localEvaluation.message,
|
|
3393
3762
|
};
|
|
3763
|
+
};
|
|
3764
|
+
let verifySource = 'api';
|
|
3765
|
+
let verifyResult;
|
|
3766
|
+
if (useLocalPlanSync) {
|
|
3767
|
+
verifySource = 'local_fallback';
|
|
3768
|
+
verifyResult = runLocalDeterministicVerification();
|
|
3769
|
+
}
|
|
3770
|
+
else {
|
|
3771
|
+
try {
|
|
3772
|
+
verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraintsForVerification, deterministicPolicyRules, 'api', compiledPolicyMetadata, {
|
|
3773
|
+
async: options.asyncMode === true,
|
|
3774
|
+
pollIntervalMs: Number.isFinite(options.verifyJobPollMs) ? options.verifyJobPollMs : undefined,
|
|
3775
|
+
timeoutMs: Number.isFinite(options.verifyJobTimeoutMs) ? options.verifyJobTimeoutMs : undefined,
|
|
3776
|
+
idempotencyKey: options.verifyIdempotencyKey,
|
|
3777
|
+
maxAttempts: Number.isFinite(options.verifyJobMaxAttempts) ? options.verifyJobMaxAttempts : undefined,
|
|
3778
|
+
});
|
|
3779
|
+
}
|
|
3780
|
+
catch (verifyApiError) {
|
|
3781
|
+
if (planFilesForVerification.length === 0) {
|
|
3782
|
+
throw verifyApiError;
|
|
3783
|
+
}
|
|
3784
|
+
verifySource = 'local_fallback';
|
|
3785
|
+
if (!options.json) {
|
|
3786
|
+
const fallbackReason = verifyApiError instanceof Error ? verifyApiError.message : String(verifyApiError);
|
|
3787
|
+
console.log(chalk.yellow('⚠️ Verify API unavailable, using local deterministic fallback.'));
|
|
3788
|
+
console.log(chalk.dim(` Reason: ${fallbackReason}`));
|
|
3789
|
+
}
|
|
3790
|
+
verifyResult = runLocalDeterministicVerification();
|
|
3791
|
+
}
|
|
3394
3792
|
}
|
|
3395
3793
|
const aiDebtEvaluation = (0, ai_debt_budget_1.evaluateAiDebtBudget)({
|
|
3396
3794
|
diffFiles,
|
|
@@ -3586,7 +3984,7 @@ async function verifyCommand(options) {
|
|
|
3586
3984
|
message: effectiveMessage,
|
|
3587
3985
|
bloatFiles: displayBloatFiles,
|
|
3588
3986
|
bloatCount: displayBloatFiles.length,
|
|
3589
|
-
}, policyViolations);
|
|
3987
|
+
}, policyViolations, expediteModeEnabled);
|
|
3590
3988
|
if (governanceResult) {
|
|
3591
3989
|
displayGovernanceInsights(governanceResult, { explain: options.explain });
|
|
3592
3990
|
}
|
|
@@ -3738,6 +4136,10 @@ async function verifyCommand(options) {
|
|
|
3738
4136
|
});
|
|
3739
4137
|
}
|
|
3740
4138
|
else {
|
|
4139
|
+
console.error(chalk.red('\n❌ Verification failed before completion.'));
|
|
4140
|
+
if (diffFiles.length > 0) {
|
|
4141
|
+
console.log(chalk.dim(` Partial context captured: ${diffFiles.length} changed file(s) in diff.`));
|
|
4142
|
+
}
|
|
3741
4143
|
if (error instanceof Error) {
|
|
3742
4144
|
if (error.message.includes('404') || error.message.includes('not found')) {
|
|
3743
4145
|
console.error(chalk.red(`❌ Error: Plan not found`));
|
|
@@ -3758,27 +4160,24 @@ async function verifyCommand(options) {
|
|
|
3758
4160
|
catch (error) {
|
|
3759
4161
|
if (options.json) {
|
|
3760
4162
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
3761
|
-
|
|
3762
|
-
grade: 'F',
|
|
3763
|
-
score: 0,
|
|
4163
|
+
emitCanonicalVerifyJson({
|
|
3764
4164
|
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,
|
|
4165
|
+
summary: {
|
|
4166
|
+
totalFilesChanged: 0,
|
|
4167
|
+
totalViolations: 0,
|
|
4168
|
+
totalWarnings: 1,
|
|
4169
|
+
totalScopeIssues: 0,
|
|
3780
4170
|
},
|
|
3781
|
-
|
|
4171
|
+
violations: [],
|
|
4172
|
+
warnings: [
|
|
4173
|
+
{
|
|
4174
|
+
file: 'unknown',
|
|
4175
|
+
message: `Unexpected error: ${errorMessage}`,
|
|
4176
|
+
policy: 'verify_runtime',
|
|
4177
|
+
},
|
|
4178
|
+
],
|
|
4179
|
+
scopeIssues: [],
|
|
4180
|
+
});
|
|
3782
4181
|
}
|
|
3783
4182
|
else {
|
|
3784
4183
|
console.error(chalk.red('\n❌ Unexpected error:'));
|
|
@@ -4115,7 +4514,7 @@ function displayChangeContractDrift(summary, options = { advisory: false }) {
|
|
|
4115
4514
|
/**
|
|
4116
4515
|
* Display verification results in a formatted report card
|
|
4117
4516
|
*/
|
|
4118
|
-
function displayVerifyResults(result, policyViolations) {
|
|
4517
|
+
function displayVerifyResults(result, policyViolations, expediteModeUsed = false) {
|
|
4119
4518
|
const verdictLabel = result.verdict === 'PASS'
|
|
4120
4519
|
? chalk.green('PASS ✅')
|
|
4121
4520
|
: result.verdict === 'WARN'
|
|
@@ -4124,42 +4523,110 @@ function displayVerifyResults(result, policyViolations) {
|
|
|
4124
4523
|
const plannedText = `${result.plannedFilesModified}/${result.totalPlannedFiles}`;
|
|
4125
4524
|
console.log(`\n${verdictLabel}`);
|
|
4126
4525
|
console.log(chalk.dim(`Plan adherence: ${plannedText} files (${result.adherenceScore}%)`));
|
|
4127
|
-
const
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4526
|
+
const maxBlockingItems = 20;
|
|
4527
|
+
const maxAdvisoryItems = 8;
|
|
4528
|
+
const maxExpediteItems = 12;
|
|
4529
|
+
const policyItems = policyViolations || [];
|
|
4530
|
+
const isBlockingSeverity = (severityRaw) => {
|
|
4531
|
+
const normalized = String(severityRaw || '').toLowerCase();
|
|
4532
|
+
return normalized === 'block' || normalized === 'critical' || normalized === 'high';
|
|
4533
|
+
};
|
|
4534
|
+
const scopeItems = result.bloatFiles.map((file) => ({
|
|
4535
|
+
file,
|
|
4536
|
+
message: 'File modified outside intended scope',
|
|
4537
|
+
policy: 'scope_guard',
|
|
4538
|
+
}));
|
|
4539
|
+
const policyTriageItems = policyItems.map((item) => ({
|
|
4540
|
+
file: item.file,
|
|
4541
|
+
message: item.message || item.rule,
|
|
4542
|
+
policy: item.rule || 'policy_violation',
|
|
4543
|
+
severity: item.severity,
|
|
4544
|
+
}));
|
|
4545
|
+
let blockingItems = [
|
|
4546
|
+
...scopeItems.map((item) => ({
|
|
4547
|
+
file: item.file,
|
|
4548
|
+
message: item.message,
|
|
4549
|
+
})),
|
|
4550
|
+
...policyTriageItems
|
|
4551
|
+
.filter((item) => isBlockingSeverity(item.severity))
|
|
4552
|
+
.map((item) => ({
|
|
4553
|
+
file: item.file,
|
|
4554
|
+
message: item.message,
|
|
4555
|
+
})),
|
|
4556
|
+
];
|
|
4557
|
+
let advisoryItems = policyTriageItems
|
|
4558
|
+
.filter((item) => !isBlockingSeverity(item.severity))
|
|
4559
|
+
.map((item) => ({
|
|
4560
|
+
file: item.file,
|
|
4561
|
+
message: item.message,
|
|
4562
|
+
}));
|
|
4563
|
+
let expediteItems = [];
|
|
4564
|
+
if (expediteModeUsed) {
|
|
4565
|
+
blockingItems = [
|
|
4566
|
+
...scopeItems
|
|
4567
|
+
.filter((item) => isCriticalScopeBreach(item.file, item.message))
|
|
4568
|
+
.map((item) => ({ file: item.file, message: item.message })),
|
|
4569
|
+
...policyTriageItems
|
|
4570
|
+
.filter((item) => isSecurityOrAuthViolation(item.file, item.policy, item.message))
|
|
4571
|
+
.map((item) => ({ file: item.file, message: item.message })),
|
|
4572
|
+
];
|
|
4573
|
+
expediteItems = [
|
|
4574
|
+
...scopeItems
|
|
4575
|
+
.filter((item) => !isCriticalScopeBreach(item.file, item.message))
|
|
4576
|
+
.map((item) => ({ file: item.file, message: item.message })),
|
|
4577
|
+
...policyTriageItems
|
|
4578
|
+
.filter((item) => !isSecurityOrAuthViolation(item.file, item.policy, item.message))
|
|
4579
|
+
.map((item) => ({ file: item.file, message: item.message })),
|
|
4580
|
+
];
|
|
4581
|
+
advisoryItems = [];
|
|
4582
|
+
}
|
|
4583
|
+
if (blockingItems.length > 0) {
|
|
4584
|
+
console.log(chalk.red(`\nBLOCKING (${blockingItems.length})`));
|
|
4585
|
+
blockingItems.slice(0, maxBlockingItems).forEach((item) => {
|
|
4586
|
+
console.log(` - ${item.file}: ${item.message}`);
|
|
4132
4587
|
});
|
|
4133
|
-
if (
|
|
4134
|
-
console.log(chalk.dim(` - ... ${
|
|
4588
|
+
if (blockingItems.length > maxBlockingItems) {
|
|
4589
|
+
console.log(chalk.dim(` - ... ${blockingItems.length - maxBlockingItems} more`));
|
|
4135
4590
|
}
|
|
4136
4591
|
}
|
|
4137
|
-
if (
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
});
|
|
4145
|
-
if (blocking.length > maxItems) {
|
|
4146
|
-
console.log(chalk.dim(` - ... ${blocking.length - maxItems} more`));
|
|
4147
|
-
}
|
|
4592
|
+
if (advisoryItems.length > 0) {
|
|
4593
|
+
console.log(chalk.yellow(`\nADVISORY (${advisoryItems.length})`));
|
|
4594
|
+
advisoryItems.slice(0, maxAdvisoryItems).forEach((item) => {
|
|
4595
|
+
console.log(` - ${item.file}: ${item.message}`);
|
|
4596
|
+
});
|
|
4597
|
+
if (advisoryItems.length > maxAdvisoryItems) {
|
|
4598
|
+
console.log(chalk.dim(` - ... ${advisoryItems.length - maxAdvisoryItems} more (summarized)`));
|
|
4148
4599
|
}
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
});
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
}
|
|
4600
|
+
}
|
|
4601
|
+
if (expediteModeUsed && expediteItems.length > 0) {
|
|
4602
|
+
console.log(chalk.yellow(`\nEXPEDITE (requires follow-up) (${expediteItems.length})`));
|
|
4603
|
+
expediteItems.slice(0, maxExpediteItems).forEach((item) => {
|
|
4604
|
+
console.log(` - ${item.file}: ${item.message}`);
|
|
4605
|
+
});
|
|
4606
|
+
if (expediteItems.length > maxExpediteItems) {
|
|
4607
|
+
console.log(chalk.dim(` - ... ${expediteItems.length - maxExpediteItems} more (summarized)`));
|
|
4157
4608
|
}
|
|
4609
|
+
console.log(chalk.dim(' Follow-up checklist:'));
|
|
4610
|
+
EXPEDITE_FOLLOW_UP_CHECKLIST.forEach((checkItem) => {
|
|
4611
|
+
console.log(chalk.dim(` - ${checkItem}`));
|
|
4612
|
+
});
|
|
4613
|
+
console.log(chalk.dim(' Note: Expedite Mode used'));
|
|
4158
4614
|
}
|
|
4159
|
-
if (
|
|
4615
|
+
if (blockingItems.length === 0 && advisoryItems.length === 0 && expediteItems.length === 0) {
|
|
4160
4616
|
console.log(chalk.green('\nNo drift detected.'));
|
|
4161
4617
|
}
|
|
4162
|
-
|
|
4618
|
+
const filesTouched = new Set([
|
|
4619
|
+
...blockingItems.map((item) => item.file),
|
|
4620
|
+
...advisoryItems.map((item) => item.file),
|
|
4621
|
+
...expediteItems.map((item) => item.file),
|
|
4622
|
+
]).size;
|
|
4623
|
+
if (expediteModeUsed) {
|
|
4624
|
+
console.log(chalk.dim(`\nSummary: ${blockingItems.length} blocking issues, ${expediteItems.length} expedite issues across ${filesTouched} files`));
|
|
4625
|
+
}
|
|
4626
|
+
else {
|
|
4627
|
+
console.log(chalk.dim(`\nSummary: ${blockingItems.length} blocking issues, ${advisoryItems.length} advisory issues across ${filesTouched} files`));
|
|
4628
|
+
}
|
|
4629
|
+
console.log(chalk.dim(`Details: ${result.message}\n`));
|
|
4163
4630
|
}
|
|
4164
4631
|
function printFirstRunAdvisoryMessage(demoMode) {
|
|
4165
4632
|
console.log(chalk.cyan('\nNeurcode first-run advisory mode'));
|