@neurcode-ai/cli 0.9.47 → 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/api-client.d.ts.map +1 -0
- package/dist/api-client.js.map +1 -0
- package/dist/commands/allow.d.ts.map +1 -0
- package/dist/commands/allow.js.map +1 -0
- package/dist/commands/apply.d.ts.map +1 -0
- package/dist/commands/apply.js.map +1 -0
- package/dist/commands/approve.d.ts.map +1 -0
- package/dist/commands/approve.js.map +1 -0
- package/dist/commands/ask.d.ts.map +1 -0
- package/dist/commands/ask.js.map +1 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/bootstrap.d.ts.map +1 -0
- package/dist/commands/bootstrap.js.map +1 -0
- package/dist/commands/brain.d.ts.map +1 -0
- package/dist/commands/brain.js.map +1 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/contract.d.ts.map +1 -0
- package/dist/commands/contract.js.map +1 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/feedback.d.ts.map +1 -0
- package/dist/commands/feedback.js.map +1 -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/guard.d.ts.map +1 -0
- package/dist/commands/guard.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/map.d.ts.map +1 -0
- package/dist/commands/map.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/plan-slo.d.ts.map +1 -0
- package/dist/commands/plan-slo.js.map +1 -0
- package/dist/commands/plan.d.ts.map +1 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/policy.d.ts.map +1 -0
- package/dist/commands/policy.js.map +1 -0
- package/dist/commands/prompt.d.ts.map +1 -0
- package/dist/commands/prompt.js.map +1 -0
- package/dist/commands/refactor.d.ts.map +1 -0
- package/dist/commands/refactor.js.map +1 -0
- package/dist/commands/remediate.d.ts.map +1 -0
- package/dist/commands/remediate.js.map +1 -0
- package/dist/commands/repo.d.ts.map +1 -0
- package/dist/commands/repo.js.map +1 -0
- package/dist/commands/revert.d.ts.map +1 -0
- package/dist/commands/revert.js.map +1 -0
- package/dist/commands/security.d.ts.map +1 -0
- package/dist/commands/security.js.map +1 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/ship.d.ts.map +1 -0
- package/dist/commands/ship.js.map +1 -0
- package/dist/commands/simulate.d.ts.map +1 -0
- package/dist/commands/simulate.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 -0
- package/dist/commands/verify.js +668 -145
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +115 -55
- package/dist/index.js.map +1 -0
- 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/rules.d.ts.map +1 -0
- package/dist/rules.js.map +1 -0
- package/dist/services/integrations/TicketService.d.ts.map +1 -0
- package/dist/services/integrations/TicketService.js.map +1 -0
- package/dist/services/mapper/ProjectScanner.d.ts.map +1 -0
- package/dist/services/mapper/ProjectScanner.js.map +1 -0
- package/dist/services/project-knowledge-service.d.ts.map +1 -0
- package/dist/services/project-knowledge-service.js.map +1 -0
- package/dist/services/security/SecurityGuard.d.ts.map +1 -0
- package/dist/services/security/SecurityGuard.js.map +1 -0
- package/dist/services/toolbox-service.d.ts.map +1 -0
- package/dist/services/toolbox-service.js.map +1 -0
- package/dist/services/watch/BlobStore.d.ts.map +1 -0
- package/dist/services/watch/BlobStore.js.map +1 -0
- package/dist/services/watch/CommandPoller.d.ts.map +1 -0
- package/dist/services/watch/CommandPoller.js.map +1 -0
- package/dist/services/watch/Journal.d.ts.map +1 -0
- package/dist/services/watch/Journal.js.map +1 -0
- package/dist/services/watch/Sentinel.d.ts.map +1 -0
- package/dist/services/watch/Sentinel.js.map +1 -0
- package/dist/services/watch/Syncer.d.ts.map +1 -0
- package/dist/services/watch/Syncer.js.map +1 -0
- package/dist/utils/ROILogger.d.ts.map +1 -0
- package/dist/utils/ROILogger.js.map +1 -0
- package/dist/utils/RelevanceScorer.d.ts.map +1 -0
- package/dist/utils/RelevanceScorer.js.map +1 -0
- package/dist/utils/advisory-signals.d.ts.map +1 -0
- package/dist/utils/advisory-signals.js.map +1 -0
- package/dist/utils/ai-debt-budget.d.ts.map +1 -0
- package/dist/utils/ai-debt-budget.js.map +1 -0
- package/dist/utils/artifact-signature.d.ts.map +1 -0
- package/dist/utils/artifact-signature.js.map +1 -0
- package/dist/utils/ask-cache.d.ts.map +1 -0
- package/dist/utils/ask-cache.js.map +1 -0
- package/dist/utils/box.d.ts.map +1 -0
- package/dist/utils/box.js.map +1 -0
- package/dist/utils/brain-context.d.ts.map +1 -0
- package/dist/utils/brain-context.js.map +1 -0
- package/dist/utils/breakage-simulator.d.ts.map +1 -0
- package/dist/utils/breakage-simulator.js.map +1 -0
- package/dist/utils/change-contract.d.ts.map +1 -0
- package/dist/utils/change-contract.js.map +1 -0
- package/dist/utils/cli-json.d.ts.map +1 -0
- package/dist/utils/cli-json.js.map +1 -0
- package/dist/utils/custom-policy-rules.d.ts.map +1 -0
- package/dist/utils/custom-policy-rules.js.map +1 -0
- package/dist/utils/diff-symbols.d.ts.map +1 -0
- package/dist/utils/diff-symbols.js.map +1 -0
- package/dist/utils/git.d.ts +8 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +55 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/gitignore.d.ts.map +1 -0
- package/dist/utils/gitignore.js.map +1 -0
- package/dist/utils/governance.d.ts.map +1 -0
- package/dist/utils/governance.js.map +1 -0
- package/dist/utils/ignore.d.ts.map +1 -0
- package/dist/utils/ignore.js.map +1 -0
- package/dist/utils/manual-approvals.d.ts.map +1 -0
- package/dist/utils/manual-approvals.js.map +1 -0
- package/dist/utils/messages.d.ts.map +1 -0
- package/dist/utils/messages.js.map +1 -0
- package/dist/utils/neurcode-context.d.ts.map +1 -0
- package/dist/utils/neurcode-context.js.map +1 -0
- package/dist/utils/plan-cache.d.ts.map +1 -0
- package/dist/utils/plan-cache.js.map +1 -0
- package/dist/utils/plan-slo.d.ts.map +1 -0
- package/dist/utils/plan-slo.js.map +1 -0
- package/dist/utils/plan-symbols.d.ts.map +1 -0
- package/dist/utils/plan-symbols.js.map +1 -0
- 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/dist/utils/policy-audit.d.ts.map +1 -0
- package/dist/utils/policy-audit.js.map +1 -0
- package/dist/utils/policy-compiler.d.ts.map +1 -0
- package/dist/utils/policy-compiler.js.map +1 -0
- package/dist/utils/policy-exceptions.d.ts.map +1 -0
- package/dist/utils/policy-exceptions.js.map +1 -0
- package/dist/utils/policy-governance.d.ts.map +1 -0
- package/dist/utils/policy-governance.js.map +1 -0
- package/dist/utils/policy-packs.d.ts.map +1 -0
- package/dist/utils/policy-packs.js.map +1 -0
- package/dist/utils/project-detector.d.ts.map +1 -0
- package/dist/utils/project-detector.js.map +1 -0
- package/dist/utils/project-root.d.ts.map +1 -0
- package/dist/utils/project-root.js.map +1 -0
- package/dist/utils/repo-links.d.ts.map +1 -0
- package/dist/utils/repo-links.js.map +1 -0
- package/dist/utils/restore.d.ts.map +1 -0
- package/dist/utils/restore.js.map +1 -0
- package/dist/utils/runtime-guard.d.ts.map +1 -0
- package/dist/utils/runtime-guard.js.map +1 -0
- package/dist/utils/scope-telemetry.d.ts.map +1 -0
- package/dist/utils/scope-telemetry.js.map +1 -0
- package/dist/utils/secret-masking.d.ts.map +1 -0
- package/dist/utils/secret-masking.js.map +1 -0
- package/dist/utils/state.d.ts.map +1 -0
- package/dist/utils/state.js.map +1 -0
- package/dist/utils/tier.d.ts.map +1 -0
- package/dist/utils/tier.js.map +1 -0
- package/dist/utils/user-context.d.ts.map +1 -0
- package/dist/utils/user-context.js.map +1 -0
- package/package.json +14 -6
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");
|
|
@@ -251,6 +252,15 @@ async function probeApiRuntimeCompatibility(apiUrl) {
|
|
|
251
252
|
*/
|
|
252
253
|
const IGNORED_METADATA_FILE_PATTERN = /(^|\/)neurcode\.config\.json$/i;
|
|
253
254
|
const IGNORED_DIRECTORIES = ['.git/', 'node_modules/'];
|
|
255
|
+
const IGNORED_GOVERNANCE_ARTIFACT_PATTERNS = [
|
|
256
|
+
/(^|\/)neurcode\.policy\.compiled\.json$/i,
|
|
257
|
+
/(^|\/)neurcode\.policy\.audit\.log\.jsonl$/i,
|
|
258
|
+
/(^|\/)neurcode\.policy\.lock\.json$/i,
|
|
259
|
+
/(^|\/)neurcode\.policy\.governance\.json$/i,
|
|
260
|
+
/(^|\/)neurcode\.policy\.exceptions\.json$/i,
|
|
261
|
+
/(^|\/)neurcode\.policy\.json$/i,
|
|
262
|
+
/(^|\/)neurcode\.rules\.json$/i,
|
|
263
|
+
];
|
|
254
264
|
function isExcludedFile(filePath) {
|
|
255
265
|
// Normalize path separators (handle both / and \)
|
|
256
266
|
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
@@ -262,6 +272,12 @@ function isExcludedFile(filePath) {
|
|
|
262
272
|
if (/(^|\/)\.neurcode\//.test(normalizedPath)) {
|
|
263
273
|
return true;
|
|
264
274
|
}
|
|
275
|
+
// Ignore generated / governance artifacts written by Neurcode itself
|
|
276
|
+
for (const pattern of IGNORED_GOVERNANCE_ARTIFACT_PATTERNS) {
|
|
277
|
+
if (pattern.test(normalizedPath)) {
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
265
281
|
// Check if path starts with any excluded prefix
|
|
266
282
|
const excludedPrefixes = [...IGNORED_DIRECTORIES];
|
|
267
283
|
// Check prefixes
|
|
@@ -657,6 +673,303 @@ function asNumberValue(value) {
|
|
|
657
673
|
function asStringValue(value) {
|
|
658
674
|
return typeof value === 'string' && value.trim().length > 0 ? value : null;
|
|
659
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
|
+
}
|
|
660
973
|
function buildDeterministicLayerSummary(payload) {
|
|
661
974
|
const verdict = asStringValue(payload.verdict) || 'UNKNOWN';
|
|
662
975
|
const mode = asStringValue(payload.mode) || 'unknown';
|
|
@@ -826,6 +1139,13 @@ function isGitRepository(cwd) {
|
|
|
826
1139
|
return false;
|
|
827
1140
|
}
|
|
828
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
|
+
}
|
|
829
1149
|
function isSignedAiLogsRequired(orgGovernanceSettings) {
|
|
830
1150
|
const explicitRequirement = isEnabledFlag(process.env.NEURCODE_GOVERNANCE_REQUIRE_SIGNED_LOGS) ||
|
|
831
1151
|
isEnabledFlag(process.env.NEURCODE_AI_LOG_REQUIRE_SIGNED);
|
|
@@ -997,18 +1317,12 @@ async function recordVerificationIfRequested(options, config, payload) {
|
|
|
997
1317
|
* Execute policy-only verification (General Governance mode)
|
|
998
1318
|
* Returns the exit code to use
|
|
999
1319
|
*/
|
|
1000
|
-
async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source, scopeTelemetry, projectId, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner, compiledPolicyMetadata, changeContractSummary) {
|
|
1320
|
+
async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRoot, config, client, source, scopeTelemetry, projectId, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner, expediteModeEnabled, compiledPolicyArtifact, compiledPolicyMetadata, changeContractSummary) {
|
|
1001
1321
|
const emitPolicyOnlyJson = (payload) => {
|
|
1002
|
-
|
|
1322
|
+
emitCanonicalVerifyJson({
|
|
1003
1323
|
...payload,
|
|
1004
|
-
|
|
1005
|
-
};
|
|
1006
|
-
console.log(JSON.stringify({
|
|
1007
|
-
...enrichedPayload,
|
|
1008
|
-
...(compiledPolicyMetadata ? { policyCompilation: compiledPolicyMetadata } : {}),
|
|
1009
|
-
changeContract: changeContractSummary,
|
|
1010
|
-
scope: scopeTelemetry,
|
|
1011
|
-
}, null, 2));
|
|
1324
|
+
expediteMode: expediteModeEnabled,
|
|
1325
|
+
});
|
|
1012
1326
|
};
|
|
1013
1327
|
const policyOnlyVerificationSource = 'policy_only';
|
|
1014
1328
|
const recordPolicyOnlyVerification = async (payload) => recordVerificationIfRequested(options, config, {
|
|
@@ -1022,13 +1336,15 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
1022
1336
|
if (!options.json) {
|
|
1023
1337
|
console.log(chalk.cyan('🛡️ General Governance mode (policy only, no plan linked)\n'));
|
|
1024
1338
|
}
|
|
1339
|
+
const diffFilesForPolicy = diffFiles.filter((f) => !ignoreFilter(f.path));
|
|
1340
|
+
const expectedPolicyOnlyFiles = diffFilesForPolicy.map((file) => file.path);
|
|
1025
1341
|
const signedLogsRequired = isSignedAiLogsRequired(orgGovernanceSettings);
|
|
1026
1342
|
const governanceAnalysis = (0, governance_1.evaluateGovernance)({
|
|
1027
1343
|
projectRoot,
|
|
1028
1344
|
task: 'Policy-only verification',
|
|
1029
|
-
expectedFiles:
|
|
1030
|
-
diffFiles,
|
|
1031
|
-
contextCandidates:
|
|
1345
|
+
expectedFiles: expectedPolicyOnlyFiles,
|
|
1346
|
+
diffFiles: diffFilesForPolicy,
|
|
1347
|
+
contextCandidates: expectedPolicyOnlyFiles,
|
|
1032
1348
|
orgGovernance: orgGovernanceSettings,
|
|
1033
1349
|
requireSignedAiLogs: signedLogsRequired,
|
|
1034
1350
|
signingKey: aiLogSigningKey,
|
|
@@ -1333,13 +1649,52 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
1333
1649
|
if (!options.json && effectiveRules.policyPack && effectiveRules.policyPackRules.length > 0) {
|
|
1334
1650
|
console.log(chalk.dim(` Evaluating policy pack: ${effectiveRules.policyPack.packName} (${effectiveRules.policyPack.packId}@${effectiveRules.policyPack.version}, ${effectiveRules.policyPackRules.length} rule(s))`));
|
|
1335
1651
|
}
|
|
1336
|
-
const diffFilesForPolicy = diffFiles.filter((f) => {
|
|
1337
|
-
const ignored = ignoreFilter(f.path);
|
|
1338
|
-
return !ignored;
|
|
1339
|
-
});
|
|
1340
1652
|
const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, effectiveRules.allRules);
|
|
1341
1653
|
policyViolations = (policyResult.violations || []);
|
|
1342
1654
|
policyViolations = policyViolations.filter((v) => !ignoreFilter(v.file));
|
|
1655
|
+
const compiledDeterministicRules = compiledPolicyArtifact
|
|
1656
|
+
? (0, policy_compiler_1.hydrateCompiledPolicyRules)(compiledPolicyArtifact)
|
|
1657
|
+
: [];
|
|
1658
|
+
const compiledPolicyRuleStatements = compiledPolicyArtifact
|
|
1659
|
+
? [...compiledPolicyArtifact.statements.policyRules]
|
|
1660
|
+
: [];
|
|
1661
|
+
if (compiledDeterministicRules.length > 0 || compiledPolicyRuleStatements.length > 0) {
|
|
1662
|
+
const fileContents = {};
|
|
1663
|
+
for (const file of diffFilesForPolicy) {
|
|
1664
|
+
const absolutePath = (0, path_1.join)(projectRoot, file.path);
|
|
1665
|
+
if (!(0, fs_1.existsSync)(absolutePath)) {
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1668
|
+
try {
|
|
1669
|
+
fileContents[file.path] = (0, fs_1.readFileSync)(absolutePath, 'utf-8');
|
|
1670
|
+
}
|
|
1671
|
+
catch {
|
|
1672
|
+
// Best-effort: deterministic checks can still run against diff lines.
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
const deterministicEvaluation = (0, governance_runtime_1.evaluatePlanVerification)({
|
|
1676
|
+
planFiles: diffFilesForPolicy.map((file) => ({
|
|
1677
|
+
path: file.path,
|
|
1678
|
+
action: 'MODIFY',
|
|
1679
|
+
})),
|
|
1680
|
+
changedFiles: diffFilesForPolicy,
|
|
1681
|
+
policyRules: compiledPolicyRuleStatements.length > 0 ? compiledPolicyRuleStatements : undefined,
|
|
1682
|
+
extraConstraintRules: compiledDeterministicRules.length > 0 ? compiledDeterministicRules : undefined,
|
|
1683
|
+
fileContents,
|
|
1684
|
+
});
|
|
1685
|
+
if (deterministicEvaluation.constraintViolations.length > 0) {
|
|
1686
|
+
const deterministicViolations = deterministicEvaluation.constraintViolations.map((message) => {
|
|
1687
|
+
const matchedFile = diffFilesForPolicy.find((file) => message.includes(file.path))?.path || '.neurcode/policy-compiled';
|
|
1688
|
+
return {
|
|
1689
|
+
file: matchedFile,
|
|
1690
|
+
rule: 'compiled_policy:deterministic_constraint',
|
|
1691
|
+
severity: 'block',
|
|
1692
|
+
message,
|
|
1693
|
+
};
|
|
1694
|
+
});
|
|
1695
|
+
policyViolations.push(...deterministicViolations);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1343
1698
|
const localPolicyGovernance = (0, policy_governance_1.readPolicyGovernanceConfig)(projectRoot);
|
|
1344
1699
|
const governance = (0, policy_governance_1.mergePolicyGovernanceWithOrgOverrides)(localPolicyGovernance, orgGovernanceSettings?.policyGovernance);
|
|
1345
1700
|
const auditIntegrity = (0, policy_audit_1.verifyPolicyAuditIntegrity)(projectRoot);
|
|
@@ -1526,14 +1881,15 @@ async function verifyCommand(options) {
|
|
|
1526
1881
|
try {
|
|
1527
1882
|
const rootResolution = (0, project_root_1.resolveNeurcodeProjectRootWithTrace)(process.cwd());
|
|
1528
1883
|
const projectRoot = rootResolution.projectRoot;
|
|
1884
|
+
const localPlanSync = (0, plan_sync_1.ensureLocalPlan)(projectRoot);
|
|
1885
|
+
const localPlanExpectedFiles = [...localPlanSync.expectedFiles];
|
|
1886
|
+
const expediteModeEnabled = resolveVerifyExpediteMode(projectRoot);
|
|
1529
1887
|
const scopeTelemetry = (0, scope_telemetry_1.buildScopeTelemetryPayload)(rootResolution);
|
|
1530
1888
|
const emitVerifyJson = (payload) => {
|
|
1531
|
-
|
|
1889
|
+
emitCanonicalVerifyJson({
|
|
1532
1890
|
...payload,
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
};
|
|
1536
|
-
console.log(JSON.stringify(jsonPayload, null, 2));
|
|
1891
|
+
expediteMode: expediteModeEnabled,
|
|
1892
|
+
});
|
|
1537
1893
|
};
|
|
1538
1894
|
if (!isGitRepository(projectRoot)) {
|
|
1539
1895
|
const message = 'Verify requires a git repository. Initialize git (`git init`) or run this command inside an existing git project.';
|
|
@@ -2013,34 +2369,44 @@ async function verifyCommand(options) {
|
|
|
2013
2369
|
});
|
|
2014
2370
|
process.exit(2);
|
|
2015
2371
|
}
|
|
2016
|
-
// Determine which diff to capture
|
|
2372
|
+
// Determine which diff to capture.
|
|
2017
2373
|
let diffText;
|
|
2374
|
+
let diffContextLabel = '';
|
|
2018
2375
|
if (options.staged) {
|
|
2019
2376
|
diffText = (0, child_process_1.execSync)('git diff --cached', { maxBuffer: 1024 * 1024 * 1024, encoding: 'utf-8' });
|
|
2377
|
+
diffContextLabel = 'staged changes';
|
|
2020
2378
|
}
|
|
2021
2379
|
else if (options.base) {
|
|
2022
2380
|
diffText = (0, git_1.getDiffFromBase)(options.base);
|
|
2381
|
+
diffContextLabel = `working tree vs ${options.base}`;
|
|
2023
2382
|
}
|
|
2024
2383
|
else if (options.head) {
|
|
2025
2384
|
diffText = (0, child_process_1.execSync)('git diff HEAD', { maxBuffer: 1024 * 1024 * 1024, encoding: 'utf-8' });
|
|
2385
|
+
diffContextLabel = 'working tree vs HEAD';
|
|
2026
2386
|
}
|
|
2027
2387
|
else {
|
|
2028
|
-
// Default:
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
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}`;
|
|
2033
2396
|
}
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2397
|
+
else {
|
|
2398
|
+
diffText = (0, child_process_1.execSync)('git diff --cached', { maxBuffer: 1024 * 1024 * 1024, encoding: 'utf-8' });
|
|
2399
|
+
diffContextLabel = 'staged changes (fallback)';
|
|
2037
2400
|
}
|
|
2038
2401
|
}
|
|
2402
|
+
if (!options.json && diffContextLabel) {
|
|
2403
|
+
console.log(chalk.dim(` Diff context: ${diffContextLabel}`));
|
|
2404
|
+
}
|
|
2039
2405
|
const untrackedDiffFiles = getUntrackedDiffFiles(projectRoot);
|
|
2040
2406
|
if (!diffText.trim() && untrackedDiffFiles.length === 0) {
|
|
2041
2407
|
if (!options.json) {
|
|
2042
|
-
console.log(chalk.yellow('⚠️ No changes detected'));
|
|
2043
|
-
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.'));
|
|
2044
2410
|
}
|
|
2045
2411
|
else {
|
|
2046
2412
|
emitVerifyJson({
|
|
@@ -2053,7 +2419,7 @@ async function verifyCommand(options) {
|
|
|
2053
2419
|
bloatFiles: [],
|
|
2054
2420
|
plannedFilesModified: 0,
|
|
2055
2421
|
totalPlannedFiles: 0,
|
|
2056
|
-
message: 'No changes detected',
|
|
2422
|
+
message: 'No changes detected in current diff context.',
|
|
2057
2423
|
scopeGuardPassed: false,
|
|
2058
2424
|
});
|
|
2059
2425
|
}
|
|
@@ -2083,7 +2449,8 @@ async function verifyCommand(options) {
|
|
|
2083
2449
|
const summary = (0, diff_parser_1.getDiffSummary)(diffFiles);
|
|
2084
2450
|
if (diffFiles.length === 0) {
|
|
2085
2451
|
if (!options.json) {
|
|
2086
|
-
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.'));
|
|
2087
2454
|
}
|
|
2088
2455
|
else {
|
|
2089
2456
|
emitVerifyJson({
|
|
@@ -2096,7 +2463,7 @@ async function verifyCommand(options) {
|
|
|
2096
2463
|
bloatFiles: [],
|
|
2097
2464
|
plannedFilesModified: 0,
|
|
2098
2465
|
totalPlannedFiles: 0,
|
|
2099
|
-
message: 'No
|
|
2466
|
+
message: 'No changes detected in current diff context.',
|
|
2100
2467
|
scopeGuardPassed: false,
|
|
2101
2468
|
});
|
|
2102
2469
|
}
|
|
@@ -2318,7 +2685,7 @@ async function verifyCommand(options) {
|
|
|
2318
2685
|
process.exit(2);
|
|
2319
2686
|
}
|
|
2320
2687
|
if (!options.json) {
|
|
2321
|
-
console.log(chalk.cyan('\n📊 Analyzing
|
|
2688
|
+
console.log(chalk.cyan('\n📊 Analyzing change set...'));
|
|
2322
2689
|
console.log(chalk.dim(` Found ${summary.totalFiles} file(s) changed`));
|
|
2323
2690
|
console.log(chalk.dim(` ${summary.totalAdded} lines added, ${summary.totalRemoved} lines removed\n`));
|
|
2324
2691
|
if (options.demo) {
|
|
@@ -2326,7 +2693,7 @@ async function verifyCommand(options) {
|
|
|
2326
2693
|
}
|
|
2327
2694
|
}
|
|
2328
2695
|
const runPolicyOnlyModeAndExit = async (source) => {
|
|
2329
|
-
const exitCode = await executePolicyOnlyMode(options, diffFiles, shouldIgnore, projectRoot, config, client, source, scopeTelemetry, projectId || undefined, orgGovernanceSettings, aiLogSigningKey, aiLogSigningKeyId, aiLogSigningKeys, aiLogSigner, 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);
|
|
2330
2697
|
const changedFiles = diffFiles.map((f) => f.path);
|
|
2331
2698
|
const verdict = exitCode === 2 ? 'FAIL' : exitCode === 1 ? 'WARN' : 'PASS';
|
|
2332
2699
|
recordVerifyEvent(verdict, `policy_only_source=${source};exit=${exitCode}`, changedFiles);
|
|
@@ -2341,6 +2708,7 @@ async function verifyCommand(options) {
|
|
|
2341
2708
|
const requirePlan = options.requirePlan === true
|
|
2342
2709
|
|| process.env.NEURCODE_VERIFY_REQUIRE_PLAN === '1'
|
|
2343
2710
|
|| strictArtifactMode;
|
|
2711
|
+
let useLocalPlanSync = false;
|
|
2344
2712
|
// Get planId: Priority 1: options flag, Priority 2: state file (.neurcode/config.json), Priority 3: legacy config
|
|
2345
2713
|
let planId = options.planId;
|
|
2346
2714
|
if (!planId) {
|
|
@@ -2373,6 +2741,19 @@ async function verifyCommand(options) {
|
|
|
2373
2741
|
}
|
|
2374
2742
|
}
|
|
2375
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
|
+
}
|
|
2376
2757
|
// If no planId found, either enforce strict requirement or fall back to policy-only mode.
|
|
2377
2758
|
if (!planId) {
|
|
2378
2759
|
if (requirePlan) {
|
|
@@ -2527,37 +2908,60 @@ async function verifyCommand(options) {
|
|
|
2527
2908
|
}
|
|
2528
2909
|
// Track if scope guard passed - this takes priority over AI grading
|
|
2529
2910
|
let scopeGuardPassed = false;
|
|
2911
|
+
let scopeGuardExpediteBypass = false;
|
|
2530
2912
|
let governanceResult = null;
|
|
2531
2913
|
let planFilesForVerification = [];
|
|
2532
2914
|
let intentConstraintsForVerification;
|
|
2533
2915
|
try {
|
|
2534
2916
|
// Step A: Get Modified Files (already have from diffFiles)
|
|
2535
2917
|
const modifiedFiles = diffFiles.map(f => f.path);
|
|
2536
|
-
// Step B:
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
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])];
|
|
2550
2957
|
intentConstraintsForVerification = originalIntent || undefined;
|
|
2551
|
-
const planDependencies = Array.isArray(planData.content.dependencies)
|
|
2552
|
-
? planData.content.dependencies.filter((item) => typeof item === 'string')
|
|
2553
|
-
: [];
|
|
2554
2958
|
governanceResult = (0, governance_1.evaluateGovernance)({
|
|
2555
2959
|
projectRoot,
|
|
2556
2960
|
task: governanceTask,
|
|
2557
|
-
expectedFiles:
|
|
2961
|
+
expectedFiles: planFilesForVerification,
|
|
2558
2962
|
expectedDependencies: planDependencies,
|
|
2559
2963
|
diffFiles,
|
|
2560
|
-
contextCandidates:
|
|
2964
|
+
contextCandidates: planFilesForVerification,
|
|
2561
2965
|
orgGovernance: orgGovernanceSettings,
|
|
2562
2966
|
requireSignedAiLogs: signedLogsRequired,
|
|
2563
2967
|
signingKey: aiLogSigningKey,
|
|
@@ -2570,8 +2974,8 @@ async function verifyCommand(options) {
|
|
|
2570
2974
|
// This is the session_id string needed to fetch the session
|
|
2571
2975
|
let sessionIdString = (0, state_1.getSessionId)() || configData.sessionId || configData.lastSessionId || null;
|
|
2572
2976
|
// Fallback: Use sessionId from plan if not in config
|
|
2573
|
-
if (!sessionIdString &&
|
|
2574
|
-
sessionIdString =
|
|
2977
|
+
if (!sessionIdString && remotePlanSessionId) {
|
|
2978
|
+
sessionIdString = remotePlanSessionId;
|
|
2575
2979
|
if ((process.env.DEBUG || process.env.VERBOSE) && !options.json) {
|
|
2576
2980
|
console.log(chalk.dim(` Using sessionId from plan: ${sessionIdString.substring(0, 8)}...`));
|
|
2577
2981
|
}
|
|
@@ -2605,17 +3009,24 @@ async function verifyCommand(options) {
|
|
|
2605
3009
|
}
|
|
2606
3010
|
}
|
|
2607
3011
|
// Step C: The Intersection Logic
|
|
2608
|
-
const approvedSet = new Set([...
|
|
3012
|
+
const approvedSet = new Set([...planFilesForVerification, ...allowedFiles]);
|
|
2609
3013
|
const violations = modifiedFiles.filter(f => !approvedSet.has(f));
|
|
2610
3014
|
const filteredViolations = violations.filter((p) => !shouldIgnore(p));
|
|
2611
3015
|
// Step D: The Block (only report scope violations for non-ignored files)
|
|
2612
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;
|
|
2613
3024
|
const aiDebtSummaryForScope = toAiDebtSummary((0, ai_debt_budget_1.evaluateAiDebtBudget)({
|
|
2614
3025
|
diffFiles,
|
|
2615
3026
|
bloatCount: filteredViolations.length,
|
|
2616
3027
|
config: aiDebtConfig,
|
|
2617
3028
|
}));
|
|
2618
|
-
recordVerifyEvent('FAIL',
|
|
3029
|
+
recordVerifyEvent(shouldBlockForScope ? 'FAIL' : 'WARN', `${shouldBlockForScope ? 'scope_violation' : 'scope_expedite'}=${filteredViolations.length}`, modifiedFiles, finalPlanId);
|
|
2619
3030
|
const scopeViolationItems = filteredViolations.map((file) => ({
|
|
2620
3031
|
file,
|
|
2621
3032
|
rule: 'scope_guard',
|
|
@@ -2627,8 +3038,10 @@ async function verifyCommand(options) {
|
|
|
2627
3038
|
...scopeViolationItems,
|
|
2628
3039
|
...aiDebtViolationItems,
|
|
2629
3040
|
];
|
|
2630
|
-
const scopeViolationMessage =
|
|
2631
|
-
|
|
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) {
|
|
2632
3045
|
// Output JSON for scope violation BEFORE exit. Must include violations for GitHub Action annotations.
|
|
2633
3046
|
const jsonOutput = {
|
|
2634
3047
|
grade: 'F',
|
|
@@ -2639,12 +3052,13 @@ async function verifyCommand(options) {
|
|
|
2639
3052
|
bloatCount: filteredViolations.length,
|
|
2640
3053
|
bloatFiles: filteredViolations,
|
|
2641
3054
|
plannedFilesModified: 0,
|
|
2642
|
-
totalPlannedFiles:
|
|
3055
|
+
totalPlannedFiles: planFilesForVerification.length,
|
|
2643
3056
|
message: scopeViolationMessage,
|
|
2644
3057
|
scopeGuardPassed: false,
|
|
2645
3058
|
mode: 'plan_enforced',
|
|
2646
3059
|
policyOnly: false,
|
|
2647
3060
|
aiDebt: aiDebtSummaryForScope,
|
|
3061
|
+
...(expediteModeEnabled ? { expediteMode: true } : {}),
|
|
2648
3062
|
...(governanceResult
|
|
2649
3063
|
? buildGovernancePayload(governanceResult, orgGovernanceSettings, {
|
|
2650
3064
|
changeContract: changeContractSummary,
|
|
@@ -2677,18 +3091,25 @@ async function verifyCommand(options) {
|
|
|
2677
3091
|
});
|
|
2678
3092
|
process.exit(1);
|
|
2679
3093
|
}
|
|
2680
|
-
else {
|
|
3094
|
+
else if (shouldBlockForScope) {
|
|
2681
3095
|
// Human-readable output only when NOT in json mode
|
|
2682
3096
|
console.log(chalk.red('\n⛔ SCOPE VIOLATION'));
|
|
2683
3097
|
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
2684
3098
|
console.log(chalk.red('The following files were modified but are not in the plan:'));
|
|
2685
3099
|
console.log('');
|
|
2686
|
-
|
|
3100
|
+
criticalScopeViolations.forEach(file => {
|
|
2687
3101
|
console.log(chalk.red(` • ${file}`));
|
|
2688
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
|
+
}
|
|
2689
3110
|
console.log('');
|
|
2690
3111
|
console.log(chalk.yellow('To unblock these files, run:'));
|
|
2691
|
-
|
|
3112
|
+
criticalScopeViolations.forEach(file => {
|
|
2692
3113
|
console.log(chalk.dim(` neurcode allow ${file}`));
|
|
2693
3114
|
});
|
|
2694
3115
|
if (aiDebtSummaryForScope.mode !== 'off') {
|
|
@@ -2735,11 +3156,30 @@ async function verifyCommand(options) {
|
|
|
2735
3156
|
});
|
|
2736
3157
|
process.exit(1);
|
|
2737
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
|
+
}
|
|
2738
3173
|
}
|
|
2739
3174
|
// Scope guard passed - all files are approved or allowed
|
|
2740
3175
|
scopeGuardPassed = true;
|
|
2741
3176
|
if (!options.json) {
|
|
2742
|
-
|
|
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
|
+
}
|
|
2743
3183
|
console.log('');
|
|
2744
3184
|
}
|
|
2745
3185
|
}
|
|
@@ -2823,7 +3263,7 @@ async function verifyCommand(options) {
|
|
|
2823
3263
|
const lockViolationItems = toPolicyLockViolations(policyLockEvaluation.mismatches);
|
|
2824
3264
|
recordVerifyEvent('FAIL', 'policy_lock_mismatch', diffFiles.map((f) => f.path), finalPlanId);
|
|
2825
3265
|
if (options.json) {
|
|
2826
|
-
|
|
3266
|
+
emitVerifyJson({
|
|
2827
3267
|
grade: 'F',
|
|
2828
3268
|
score: 0,
|
|
2829
3269
|
verdict: 'FAIL',
|
|
@@ -2851,7 +3291,7 @@ async function verifyCommand(options) {
|
|
|
2851
3291
|
path: policyLockEvaluation.lockPath,
|
|
2852
3292
|
mismatches: policyLockEvaluation.mismatches,
|
|
2853
3293
|
},
|
|
2854
|
-
}
|
|
3294
|
+
});
|
|
2855
3295
|
}
|
|
2856
3296
|
else {
|
|
2857
3297
|
console.log(chalk.red('\n❌ Policy lock baseline mismatch'));
|
|
@@ -3270,35 +3710,20 @@ async function verifyCommand(options) {
|
|
|
3270
3710
|
displayChangeContractDrift(changeContractSummary, { advisory: true });
|
|
3271
3711
|
}
|
|
3272
3712
|
}
|
|
3273
|
-
// Call verify API
|
|
3713
|
+
// Call verify API (or deterministic local evaluation for Plan Sync scope mode)
|
|
3274
3714
|
if (!options.json) {
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
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
|
+
}
|
|
3278
3723
|
}
|
|
3279
3724
|
}
|
|
3280
3725
|
try {
|
|
3281
|
-
|
|
3282
|
-
let verifyResult;
|
|
3283
|
-
try {
|
|
3284
|
-
verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraintsForVerification, deterministicPolicyRules, 'api', compiledPolicyMetadata, {
|
|
3285
|
-
async: options.asyncMode === true,
|
|
3286
|
-
pollIntervalMs: Number.isFinite(options.verifyJobPollMs) ? options.verifyJobPollMs : undefined,
|
|
3287
|
-
timeoutMs: Number.isFinite(options.verifyJobTimeoutMs) ? options.verifyJobTimeoutMs : undefined,
|
|
3288
|
-
idempotencyKey: options.verifyIdempotencyKey,
|
|
3289
|
-
maxAttempts: Number.isFinite(options.verifyJobMaxAttempts) ? options.verifyJobMaxAttempts : undefined,
|
|
3290
|
-
});
|
|
3291
|
-
}
|
|
3292
|
-
catch (verifyApiError) {
|
|
3293
|
-
if (planFilesForVerification.length === 0) {
|
|
3294
|
-
throw verifyApiError;
|
|
3295
|
-
}
|
|
3296
|
-
verifySource = 'local_fallback';
|
|
3297
|
-
if (!options.json) {
|
|
3298
|
-
const fallbackReason = verifyApiError instanceof Error ? verifyApiError.message : String(verifyApiError);
|
|
3299
|
-
console.log(chalk.yellow('⚠️ Verify API unavailable, using local deterministic fallback.'));
|
|
3300
|
-
console.log(chalk.dim(` Reason: ${fallbackReason}`));
|
|
3301
|
-
}
|
|
3726
|
+
const runLocalDeterministicVerification = () => {
|
|
3302
3727
|
const localFileContents = {};
|
|
3303
3728
|
for (const file of changedFiles) {
|
|
3304
3729
|
const absolutePath = (0, path_1.join)(projectRoot, file.path);
|
|
@@ -3324,7 +3749,7 @@ async function verifyCommand(options) {
|
|
|
3324
3749
|
extraConstraintRules: hydratedCompiledPolicyRules.length > 0 ? hydratedCompiledPolicyRules : undefined,
|
|
3325
3750
|
fileContents: localFileContents,
|
|
3326
3751
|
});
|
|
3327
|
-
|
|
3752
|
+
return {
|
|
3328
3753
|
verificationId: `local-fallback-${Date.now()}`,
|
|
3329
3754
|
adherenceScore: localEvaluation.adherenceScore,
|
|
3330
3755
|
bloatCount: localEvaluation.bloatCount,
|
|
@@ -3335,6 +3760,35 @@ async function verifyCommand(options) {
|
|
|
3335
3760
|
diffSummary: localEvaluation.diffSummary,
|
|
3336
3761
|
message: localEvaluation.message,
|
|
3337
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
|
+
}
|
|
3338
3792
|
}
|
|
3339
3793
|
const aiDebtEvaluation = (0, ai_debt_budget_1.evaluateAiDebtBudget)({
|
|
3340
3794
|
diffFiles,
|
|
@@ -3530,7 +3984,7 @@ async function verifyCommand(options) {
|
|
|
3530
3984
|
message: effectiveMessage,
|
|
3531
3985
|
bloatFiles: displayBloatFiles,
|
|
3532
3986
|
bloatCount: displayBloatFiles.length,
|
|
3533
|
-
}, policyViolations);
|
|
3987
|
+
}, policyViolations, expediteModeEnabled);
|
|
3534
3988
|
if (governanceResult) {
|
|
3535
3989
|
displayGovernanceInsights(governanceResult, { explain: options.explain });
|
|
3536
3990
|
}
|
|
@@ -3682,6 +4136,10 @@ async function verifyCommand(options) {
|
|
|
3682
4136
|
});
|
|
3683
4137
|
}
|
|
3684
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
|
+
}
|
|
3685
4143
|
if (error instanceof Error) {
|
|
3686
4144
|
if (error.message.includes('404') || error.message.includes('not found')) {
|
|
3687
4145
|
console.error(chalk.red(`❌ Error: Plan not found`));
|
|
@@ -3702,27 +4160,24 @@ async function verifyCommand(options) {
|
|
|
3702
4160
|
catch (error) {
|
|
3703
4161
|
if (options.json) {
|
|
3704
4162
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
3705
|
-
|
|
3706
|
-
grade: 'F',
|
|
3707
|
-
score: 0,
|
|
4163
|
+
emitCanonicalVerifyJson({
|
|
3708
4164
|
verdict: 'FAIL',
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
totalPlannedFiles: 0,
|
|
3715
|
-
message: `Unexpected error: ${errorMessage}`,
|
|
3716
|
-
scopeGuardPassed: false,
|
|
3717
|
-
scope: {
|
|
3718
|
-
scanRoot: process.cwd(),
|
|
3719
|
-
startDir: process.cwd(),
|
|
3720
|
-
gitRoot: null,
|
|
3721
|
-
linkedRepoOverrideUsed: false,
|
|
3722
|
-
linkedRepos: [],
|
|
3723
|
-
blockedOverride: null,
|
|
4165
|
+
summary: {
|
|
4166
|
+
totalFilesChanged: 0,
|
|
4167
|
+
totalViolations: 0,
|
|
4168
|
+
totalWarnings: 1,
|
|
4169
|
+
totalScopeIssues: 0,
|
|
3724
4170
|
},
|
|
3725
|
-
|
|
4171
|
+
violations: [],
|
|
4172
|
+
warnings: [
|
|
4173
|
+
{
|
|
4174
|
+
file: 'unknown',
|
|
4175
|
+
message: `Unexpected error: ${errorMessage}`,
|
|
4176
|
+
policy: 'verify_runtime',
|
|
4177
|
+
},
|
|
4178
|
+
],
|
|
4179
|
+
scopeIssues: [],
|
|
4180
|
+
});
|
|
3726
4181
|
}
|
|
3727
4182
|
else {
|
|
3728
4183
|
console.error(chalk.red('\n❌ Unexpected error:'));
|
|
@@ -4059,7 +4514,7 @@ function displayChangeContractDrift(summary, options = { advisory: false }) {
|
|
|
4059
4514
|
/**
|
|
4060
4515
|
* Display verification results in a formatted report card
|
|
4061
4516
|
*/
|
|
4062
|
-
function displayVerifyResults(result, policyViolations) {
|
|
4517
|
+
function displayVerifyResults(result, policyViolations, expediteModeUsed = false) {
|
|
4063
4518
|
const verdictLabel = result.verdict === 'PASS'
|
|
4064
4519
|
? chalk.green('PASS ✅')
|
|
4065
4520
|
: result.verdict === 'WARN'
|
|
@@ -4068,42 +4523,110 @@ function displayVerifyResults(result, policyViolations) {
|
|
|
4068
4523
|
const plannedText = `${result.plannedFilesModified}/${result.totalPlannedFiles}`;
|
|
4069
4524
|
console.log(`\n${verdictLabel}`);
|
|
4070
4525
|
console.log(chalk.dim(`Plan adherence: ${plannedText} files (${result.adherenceScore}%)`));
|
|
4071
|
-
const
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
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}`);
|
|
4076
4587
|
});
|
|
4077
|
-
if (
|
|
4078
|
-
console.log(chalk.dim(` - ... ${
|
|
4588
|
+
if (blockingItems.length > maxBlockingItems) {
|
|
4589
|
+
console.log(chalk.dim(` - ... ${blockingItems.length - maxBlockingItems} more`));
|
|
4079
4590
|
}
|
|
4080
4591
|
}
|
|
4081
|
-
if (
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
});
|
|
4089
|
-
if (blocking.length > maxItems) {
|
|
4090
|
-
console.log(chalk.dim(` - ... ${blocking.length - maxItems} more`));
|
|
4091
|
-
}
|
|
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)`));
|
|
4092
4599
|
}
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
});
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
}
|
|
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)`));
|
|
4101
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'));
|
|
4102
4614
|
}
|
|
4103
|
-
if (
|
|
4615
|
+
if (blockingItems.length === 0 && advisoryItems.length === 0 && expediteItems.length === 0) {
|
|
4104
4616
|
console.log(chalk.green('\nNo drift detected.'));
|
|
4105
4617
|
}
|
|
4106
|
-
|
|
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`));
|
|
4107
4630
|
}
|
|
4108
4631
|
function printFirstRunAdvisoryMessage(demoMode) {
|
|
4109
4632
|
console.log(chalk.cyan('\nNeurcode first-run advisory mode'));
|