@veraxhq/verax 0.2.1 → 0.3.0
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/README.md +14 -18
- package/bin/verax.js +7 -0
- package/package.json +3 -3
- package/src/cli/commands/baseline.js +104 -0
- package/src/cli/commands/default.js +79 -25
- package/src/cli/commands/ga.js +243 -0
- package/src/cli/commands/gates.js +95 -0
- package/src/cli/commands/inspect.js +131 -2
- package/src/cli/commands/release-check.js +213 -0
- package/src/cli/commands/run.js +246 -35
- package/src/cli/commands/security-check.js +211 -0
- package/src/cli/commands/truth.js +114 -0
- package/src/cli/entry.js +304 -67
- package/src/cli/util/angular-component-extractor.js +179 -0
- package/src/cli/util/angular-navigation-detector.js +141 -0
- package/src/cli/util/angular-network-detector.js +161 -0
- package/src/cli/util/angular-state-detector.js +162 -0
- package/src/cli/util/ast-interactive-detector.js +546 -0
- package/src/cli/util/ast-network-detector.js +603 -0
- package/src/cli/util/ast-usestate-detector.js +602 -0
- package/src/cli/util/bootstrap-guard.js +86 -0
- package/src/cli/util/determinism-runner.js +123 -0
- package/src/cli/util/determinism-writer.js +129 -0
- package/src/cli/util/env-url.js +4 -0
- package/src/cli/util/expectation-extractor.js +369 -73
- package/src/cli/util/findings-writer.js +126 -16
- package/src/cli/util/learn-writer.js +3 -1
- package/src/cli/util/observe-writer.js +3 -1
- package/src/cli/util/paths.js +3 -12
- package/src/cli/util/project-discovery.js +3 -0
- package/src/cli/util/project-writer.js +3 -1
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +1 -0
- package/src/cli/util/svelte-navigation-detector.js +163 -0
- package/src/cli/util/svelte-network-detector.js +80 -0
- package/src/cli/util/svelte-sfc-extractor.js +147 -0
- package/src/cli/util/svelte-state-detector.js +243 -0
- package/src/cli/util/vue-navigation-detector.js +177 -0
- package/src/cli/util/vue-sfc-extractor.js +162 -0
- package/src/cli/util/vue-state-detector.js +215 -0
- package/src/verax/cli/finding-explainer.js +56 -3
- package/src/verax/core/artifacts/registry.js +154 -0
- package/src/verax/core/artifacts/verifier.js +980 -0
- package/src/verax/core/baseline/baseline.enforcer.js +137 -0
- package/src/verax/core/baseline/baseline.snapshot.js +231 -0
- package/src/verax/core/capabilities/gates.js +499 -0
- package/src/verax/core/capabilities/registry.js +475 -0
- package/src/verax/core/confidence/confidence-compute.js +137 -0
- package/src/verax/core/confidence/confidence-invariants.js +234 -0
- package/src/verax/core/confidence/confidence-report-writer.js +112 -0
- package/src/verax/core/confidence/confidence-weights.js +44 -0
- package/src/verax/core/confidence/confidence.defaults.js +65 -0
- package/src/verax/core/confidence/confidence.loader.js +79 -0
- package/src/verax/core/confidence/confidence.schema.js +94 -0
- package/src/verax/core/confidence-engine-refactor.js +484 -0
- package/src/verax/core/confidence-engine.js +486 -0
- package/src/verax/core/confidence-engine.js.backup +471 -0
- package/src/verax/core/contracts/index.js +29 -0
- package/src/verax/core/contracts/types.js +185 -0
- package/src/verax/core/contracts/validators.js +381 -0
- package/src/verax/core/decision-snapshot.js +30 -3
- package/src/verax/core/decisions/decision.trace.js +276 -0
- package/src/verax/core/determinism/contract-writer.js +89 -0
- package/src/verax/core/determinism/contract.js +139 -0
- package/src/verax/core/determinism/diff.js +364 -0
- package/src/verax/core/determinism/engine.js +221 -0
- package/src/verax/core/determinism/finding-identity.js +148 -0
- package/src/verax/core/determinism/normalize.js +438 -0
- package/src/verax/core/determinism/report-writer.js +92 -0
- package/src/verax/core/determinism/run-fingerprint.js +118 -0
- package/src/verax/core/dynamic-route-intelligence.js +528 -0
- package/src/verax/core/evidence/evidence-capture-service.js +307 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
- package/src/verax/core/evidence-builder.js +487 -0
- package/src/verax/core/execution-mode-context.js +77 -0
- package/src/verax/core/execution-mode-detector.js +190 -0
- package/src/verax/core/failures/exit-codes.js +86 -0
- package/src/verax/core/failures/failure-summary.js +76 -0
- package/src/verax/core/failures/failure.factory.js +225 -0
- package/src/verax/core/failures/failure.ledger.js +132 -0
- package/src/verax/core/failures/failure.types.js +196 -0
- package/src/verax/core/failures/index.js +10 -0
- package/src/verax/core/ga/ga-report-writer.js +43 -0
- package/src/verax/core/ga/ga.artifact.js +49 -0
- package/src/verax/core/ga/ga.contract.js +434 -0
- package/src/verax/core/ga/ga.enforcer.js +86 -0
- package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
- package/src/verax/core/guardrails/policy.defaults.js +210 -0
- package/src/verax/core/guardrails/policy.loader.js +83 -0
- package/src/verax/core/guardrails/policy.schema.js +110 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
- package/src/verax/core/guardrails-engine.js +505 -0
- package/src/verax/core/observe/run-timeline.js +316 -0
- package/src/verax/core/perf/perf.contract.js +186 -0
- package/src/verax/core/perf/perf.display.js +65 -0
- package/src/verax/core/perf/perf.enforcer.js +91 -0
- package/src/verax/core/perf/perf.monitor.js +209 -0
- package/src/verax/core/perf/perf.report.js +198 -0
- package/src/verax/core/pipeline-tracker.js +238 -0
- package/src/verax/core/product-definition.js +127 -0
- package/src/verax/core/release/provenance.builder.js +271 -0
- package/src/verax/core/release/release-report-writer.js +40 -0
- package/src/verax/core/release/release.enforcer.js +159 -0
- package/src/verax/core/release/reproducibility.check.js +221 -0
- package/src/verax/core/release/sbom.builder.js +283 -0
- package/src/verax/core/report/cross-index.js +192 -0
- package/src/verax/core/report/human-summary.js +222 -0
- package/src/verax/core/route-intelligence.js +419 -0
- package/src/verax/core/security/secrets.scan.js +326 -0
- package/src/verax/core/security/security-report.js +50 -0
- package/src/verax/core/security/security.enforcer.js +124 -0
- package/src/verax/core/security/supplychain.defaults.json +38 -0
- package/src/verax/core/security/supplychain.policy.js +326 -0
- package/src/verax/core/security/vuln.scan.js +265 -0
- package/src/verax/core/truth/truth.certificate.js +250 -0
- package/src/verax/core/ui-feedback-intelligence.js +515 -0
- package/src/verax/detect/confidence-engine.js +628 -40
- package/src/verax/detect/confidence-helper.js +33 -0
- package/src/verax/detect/detection-engine.js +18 -1
- package/src/verax/detect/dynamic-route-findings.js +335 -0
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +3 -1
- package/src/verax/detect/findings-writer.js +141 -5
- package/src/verax/detect/index.js +229 -5
- package/src/verax/detect/journey-stall-detector.js +558 -0
- package/src/verax/detect/route-findings.js +218 -0
- package/src/verax/detect/ui-feedback-findings.js +207 -0
- package/src/verax/detect/verdict-engine.js +57 -3
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/index.js +413 -45
- package/src/verax/learn/action-contract-extractor.js +682 -64
- package/src/verax/learn/route-validator.js +4 -1
- package/src/verax/observe/index.js +88 -843
- package/src/verax/observe/interaction-runner.js +25 -8
- package/src/verax/observe/observe-context.js +205 -0
- package/src/verax/observe/observe-helpers.js +191 -0
- package/src/verax/observe/observe-runner.js +226 -0
- package/src/verax/observe/observers/budget-observer.js +185 -0
- package/src/verax/observe/observers/console-observer.js +102 -0
- package/src/verax/observe/observers/coverage-observer.js +107 -0
- package/src/verax/observe/observers/interaction-observer.js +471 -0
- package/src/verax/observe/observers/navigation-observer.js +132 -0
- package/src/verax/observe/observers/network-observer.js +87 -0
- package/src/verax/observe/observers/safety-observer.js +82 -0
- package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
- package/src/verax/observe/ui-feedback-detector.js +742 -0
- package/src/verax/observe/ui-signal-sensor.js +148 -2
- package/src/verax/scan-summary-writer.js +42 -8
- package/src/verax/shared/artifact-manager.js +8 -5
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/view-switch-rules.js +208 -0
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 6 — Artifact Verifier (The Judge)
|
|
3
|
+
*
|
|
4
|
+
* Authoritative verifier that evaluates the integrity and completeness of a VERAX run.
|
|
5
|
+
*
|
|
6
|
+
* This module performs strict validation of:
|
|
7
|
+
* - Artifact existence and completeness
|
|
8
|
+
* - Contract version compliance
|
|
9
|
+
* - Evidence Law enforcement
|
|
10
|
+
* - Cross-artifact consistency
|
|
11
|
+
*
|
|
12
|
+
* @module verifier
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, statSync } from 'fs';
|
|
16
|
+
import { join, resolve } from 'path';
|
|
17
|
+
import { ARTIFACT_REGISTRY, getArtifactVersions } from './registry.js';
|
|
18
|
+
import { FINDING_STATUS } from '../contracts/types.js';
|
|
19
|
+
import { isEvidenceSubstantive } from '../contracts/validators.js';
|
|
20
|
+
import { validateEvidencePackage, validateEvidencePackageStrict } from '../evidence-builder.js';
|
|
21
|
+
import { checkConfidenceInvariants, CONFIDENCE_RANGES } from '../confidence/confidence-invariants.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Verifies a VERAX run directory against the artifact registry.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} runDir - Absolute path to the run directory (.verax/runs/<runId>)
|
|
27
|
+
* @param {Object} [registrySnapshot] - Optional snapshot of artifact versions (defaults to current registry)
|
|
28
|
+
* @returns {Object} Verdict object with ok, errors, warnings, and detailed findings
|
|
29
|
+
*/
|
|
30
|
+
export function verifyRun(runDir, registrySnapshot = null) {
|
|
31
|
+
const errors = [];
|
|
32
|
+
const warnings = [];
|
|
33
|
+
const missingArtifacts = [];
|
|
34
|
+
const invalidArtifacts = [];
|
|
35
|
+
const contractVersionMismatches = [];
|
|
36
|
+
|
|
37
|
+
// Use provided snapshot or current registry
|
|
38
|
+
const expectedVersions = registrySnapshot || getArtifactVersions();
|
|
39
|
+
|
|
40
|
+
// Track enforcement summary
|
|
41
|
+
const enforcementSummary = {
|
|
42
|
+
totalFindings: 0,
|
|
43
|
+
confirmedFindings: 0,
|
|
44
|
+
suspectedFindings: 0,
|
|
45
|
+
findingsWithoutEvidence: 0,
|
|
46
|
+
enforcementApplied: false
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Check 1: All required artifacts exist
|
|
50
|
+
for (const [key, def] of Object.entries(ARTIFACT_REGISTRY)) {
|
|
51
|
+
const artifactPath = join(runDir, def.filename);
|
|
52
|
+
|
|
53
|
+
if (def.type === 'file') {
|
|
54
|
+
if (!existsSync(artifactPath)) {
|
|
55
|
+
missingArtifacts.push({
|
|
56
|
+
key,
|
|
57
|
+
filename: def.filename,
|
|
58
|
+
path: artifactPath,
|
|
59
|
+
reason: 'File does not exist'
|
|
60
|
+
});
|
|
61
|
+
errors.push(`Missing required artifact: ${def.filename}`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check if it's a valid file (not a directory)
|
|
66
|
+
try {
|
|
67
|
+
const stats = statSync(artifactPath);
|
|
68
|
+
if (!stats.isFile()) {
|
|
69
|
+
invalidArtifacts.push({
|
|
70
|
+
key,
|
|
71
|
+
filename: def.filename,
|
|
72
|
+
path: artifactPath,
|
|
73
|
+
reason: 'Expected file but found directory or other type'
|
|
74
|
+
});
|
|
75
|
+
errors.push(`Invalid artifact type: ${def.filename} is not a file`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
} catch (statError) {
|
|
79
|
+
invalidArtifacts.push({
|
|
80
|
+
key,
|
|
81
|
+
filename: def.filename,
|
|
82
|
+
path: artifactPath,
|
|
83
|
+
reason: `Cannot stat file: ${statError.message}`
|
|
84
|
+
});
|
|
85
|
+
errors.push(`Cannot access artifact: ${def.filename}`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// For JSON files, validate structure and metadata
|
|
90
|
+
if (def.filename.endsWith('.json')) {
|
|
91
|
+
try {
|
|
92
|
+
const content = readFileSync(artifactPath, 'utf-8');
|
|
93
|
+
const data = JSON.parse(content);
|
|
94
|
+
|
|
95
|
+
// Check contractVersion
|
|
96
|
+
if (data.contractVersion === undefined || data.contractVersion === null) {
|
|
97
|
+
contractVersionMismatches.push({
|
|
98
|
+
key,
|
|
99
|
+
filename: def.filename,
|
|
100
|
+
expected: def.contractVersion,
|
|
101
|
+
found: data.contractVersion,
|
|
102
|
+
reason: 'Missing contractVersion field'
|
|
103
|
+
});
|
|
104
|
+
errors.push(`Artifact ${def.filename} missing contractVersion`);
|
|
105
|
+
} else if (data.contractVersion !== def.contractVersion) {
|
|
106
|
+
contractVersionMismatches.push({
|
|
107
|
+
key,
|
|
108
|
+
filename: def.filename,
|
|
109
|
+
expected: def.contractVersion,
|
|
110
|
+
found: data.contractVersion,
|
|
111
|
+
reason: `Contract version mismatch: expected ${def.contractVersion}, found ${data.contractVersion}`
|
|
112
|
+
});
|
|
113
|
+
errors.push(`Artifact ${def.filename} has contractVersion ${data.contractVersion}, expected ${def.contractVersion}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check artifactVersions map (if present)
|
|
117
|
+
if (data.artifactVersions) {
|
|
118
|
+
const artifactVersions = data.artifactVersions;
|
|
119
|
+
for (const [regKey, regDef] of Object.entries(ARTIFACT_REGISTRY)) {
|
|
120
|
+
if (artifactVersions[regKey] !== undefined) {
|
|
121
|
+
if (artifactVersions[regKey] !== regDef.contractVersion) {
|
|
122
|
+
warnings.push(
|
|
123
|
+
`Artifact ${def.filename} has artifactVersions[${regKey}]=${artifactVersions[regKey]}, ` +
|
|
124
|
+
`expected ${regDef.contractVersion}`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Special validation for findings.json
|
|
132
|
+
if (key === 'findings') {
|
|
133
|
+
const findingsValidation = validateFindingsArtifact(data, runDir);
|
|
134
|
+
if (findingsValidation.errors.length > 0) {
|
|
135
|
+
errors.push(...findingsValidation.errors);
|
|
136
|
+
invalidArtifacts.push({
|
|
137
|
+
key,
|
|
138
|
+
filename: def.filename,
|
|
139
|
+
path: artifactPath,
|
|
140
|
+
reason: `Findings validation failed: ${findingsValidation.errors.join('; ')}`
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (findingsValidation.warnings.length > 0) {
|
|
144
|
+
warnings.push(...findingsValidation.warnings);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Update enforcement summary
|
|
148
|
+
if (data.enforcement) {
|
|
149
|
+
enforcementSummary.enforcementApplied = true;
|
|
150
|
+
enforcementSummary.droppedCount = data.enforcement.droppedCount || 0;
|
|
151
|
+
enforcementSummary.downgradedCount = data.enforcement.downgradedCount || 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// PHASE 16: Count findings by status and validate evidencePackage
|
|
155
|
+
// PHASE 22: Validate evidence intent ledger for CONFIRMED findings
|
|
156
|
+
if (Array.isArray(data.findings)) {
|
|
157
|
+
enforcementSummary.totalFindings = data.findings.length;
|
|
158
|
+
|
|
159
|
+
// PHASE 22: Read evidence intent ledger
|
|
160
|
+
const evidenceIntentPath = join(runDir, ARTIFACT_REGISTRY.evidenceIntent.filename);
|
|
161
|
+
let evidenceIntentLedger = null;
|
|
162
|
+
if (existsSync(evidenceIntentPath)) {
|
|
163
|
+
try {
|
|
164
|
+
const intentContent = readFileSync(evidenceIntentPath, 'utf-8');
|
|
165
|
+
evidenceIntentLedger = JSON.parse(intentContent);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
// Will be caught by artifact validation
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (let i = 0; i < data.findings.length; i++) {
|
|
172
|
+
const finding = data.findings[i];
|
|
173
|
+
const severity = finding.severity || finding.status;
|
|
174
|
+
if (severity === FINDING_STATUS.CONFIRMED || severity === 'CONFIRMED') {
|
|
175
|
+
enforcementSummary.confirmedFindings++;
|
|
176
|
+
|
|
177
|
+
// PHASE 16: Check evidencePackage completeness
|
|
178
|
+
// PHASE 21.1: HARD LOCK - CONFIRMED without complete evidencePackage → INVALID (blocking error)
|
|
179
|
+
if (finding.evidencePackage) {
|
|
180
|
+
try {
|
|
181
|
+
// PHASE 21.1: Strict validation - throws if incomplete
|
|
182
|
+
validateEvidencePackageStrict(finding.evidencePackage, severity);
|
|
183
|
+
// If we get here, evidencePackage is complete
|
|
184
|
+
|
|
185
|
+
// PHASE 22: Validate evidence intent ledger for CONFIRMED findings
|
|
186
|
+
if (evidenceIntentLedger) {
|
|
187
|
+
const findingIdentity = finding.findingId || finding.id || `finding-${i}`;
|
|
188
|
+
const intentEntry = evidenceIntentLedger.entries?.find(e => e.findingIdentity === findingIdentity);
|
|
189
|
+
|
|
190
|
+
if (intentEntry) {
|
|
191
|
+
// Check that all required fields have successful capture outcomes
|
|
192
|
+
const failedFields = [];
|
|
193
|
+
for (const [field, outcome] of Object.entries(intentEntry.captureOutcomes || {})) {
|
|
194
|
+
if (outcome.required && !outcome.captured && outcome.failure) {
|
|
195
|
+
failedFields.push(field);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (failedFields.length > 0) {
|
|
200
|
+
// PHASE 22: CONFIRMED finding with capture failures in intent ledger → VERIFIED_WITH_ERRORS
|
|
201
|
+
errors.push(
|
|
202
|
+
`EVIDENCE_INTENT_MISMATCH: Finding ${findingIdentity} is CONFIRMED but evidence.intent.json shows capture failures: ${failedFields.join(', ')}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
// PHASE 22: CONFIRMED finding missing from evidence intent ledger → warning
|
|
207
|
+
warnings.push(
|
|
208
|
+
`EVIDENCE_INTENT_MISSING: Finding ${findingIdentity} is CONFIRMED but missing from evidence.intent.json`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
// PHASE 21.1: HARD FAILURE - blocking error, not warning
|
|
214
|
+
enforcementSummary.findingsWithoutEvidence++;
|
|
215
|
+
errors.push(
|
|
216
|
+
`EVIDENCE_LAW_VIOLATION: Finding marked CONFIRMED but evidencePackage is incomplete. ` +
|
|
217
|
+
`Missing fields: ${error.missingFields?.join(', ') || 'unknown'}. ` +
|
|
218
|
+
`(finding type: ${finding.type || 'unknown'}, index: ${i})`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
} else if (!isEvidenceSubstantive(finding.evidence)) {
|
|
222
|
+
// PHASE 21.1: CONFIRMED without evidencePackage and without substantive evidence → HARD FAILURE
|
|
223
|
+
enforcementSummary.findingsWithoutEvidence++;
|
|
224
|
+
errors.push(
|
|
225
|
+
`EVIDENCE_LAW_VIOLATION: Finding marked CONFIRMED but lacks evidencePackage and evidence is insufficient. ` +
|
|
226
|
+
`(finding type: ${finding.type || 'unknown'}, index: ${i})`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
} else if (severity === FINDING_STATUS.SUSPECTED || severity === 'SUSPECTED') {
|
|
230
|
+
enforcementSummary.suspectedFindings++;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// PHASE 24: Check confidence invariants
|
|
234
|
+
const findingIdentity = finding.findingId || finding.id || `finding-${i}`;
|
|
235
|
+
const findingConfidence = finding.confidence !== undefined ? finding.confidence : 0;
|
|
236
|
+
const invariantCheck = checkConfidenceInvariants(findingConfidence, severity, {
|
|
237
|
+
expectationProof: finding.expectation?.proof,
|
|
238
|
+
verificationStatus: null, // Will be set after verification
|
|
239
|
+
guardrailsOutcome: finding.guardrails
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (invariantCheck.violated) {
|
|
243
|
+
errors.push(
|
|
244
|
+
`CONFIDENCE_INVARIANT_VIOLATION: Finding ${findingIdentity} (${severity}) has confidence ${findingConfidence} which violates invariants: ${invariantCheck.violations.map(v => v.code).join(', ')}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Specific invariant checks
|
|
249
|
+
if (severity === 'CONFIRMED' && findingConfidence < CONFIDENCE_RANGES.CONFIRMED.min) {
|
|
250
|
+
errors.push(
|
|
251
|
+
`CONFIDENCE_INVARIANT_VIOLATION: CONFIRMED finding ${findingIdentity} has confidence ${findingConfidence} < ${CONFIDENCE_RANGES.CONFIRMED.min}`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (severity === 'SUSPECTED' && findingConfidence >= CONFIDENCE_RANGES.SUSPECTED.max + 0.01) {
|
|
256
|
+
errors.push(
|
|
257
|
+
`CONFIDENCE_INVARIANT_VIOLATION: SUSPECTED finding ${findingIdentity} has confidence ${findingConfidence} >= ${CONFIDENCE_RANGES.SUSPECTED.max + 0.01}`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// PHASE 22: Special validation for evidence.intent.json
|
|
265
|
+
if (key === 'evidenceIntent') {
|
|
266
|
+
const intentValidation = validateEvidenceIntentArtifact(data, runDir);
|
|
267
|
+
if (intentValidation.errors.length > 0) {
|
|
268
|
+
errors.push(...intentValidation.errors);
|
|
269
|
+
invalidArtifacts.push({
|
|
270
|
+
key,
|
|
271
|
+
filename: def.filename,
|
|
272
|
+
path: artifactPath,
|
|
273
|
+
reason: `Evidence intent validation failed: ${intentValidation.errors.join('; ')}`
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (intentValidation.warnings.length > 0) {
|
|
277
|
+
warnings.push(...intentValidation.warnings);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// PHASE 23: Special validation for guardrails.report.json
|
|
282
|
+
if (key === 'guardrailsReport') {
|
|
283
|
+
const guardrailsValidation = validateGuardrailsReportArtifact(data, runDir);
|
|
284
|
+
if (guardrailsValidation.errors.length > 0) {
|
|
285
|
+
errors.push(...guardrailsValidation.errors);
|
|
286
|
+
invalidArtifacts.push({
|
|
287
|
+
key,
|
|
288
|
+
filename: def.filename,
|
|
289
|
+
path: artifactPath,
|
|
290
|
+
reason: `Guardrails report validation failed: ${guardrailsValidation.errors.join('; ')}`
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (guardrailsValidation.warnings.length > 0) {
|
|
294
|
+
warnings.push(...guardrailsValidation.warnings);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// PHASE 24: Special validation for confidence.report.json
|
|
299
|
+
if (key === 'confidenceReport') {
|
|
300
|
+
const confidenceValidation = validateConfidenceReportArtifact(data, runDir);
|
|
301
|
+
if (confidenceValidation.errors.length > 0) {
|
|
302
|
+
errors.push(...confidenceValidation.errors);
|
|
303
|
+
invalidArtifacts.push({
|
|
304
|
+
key,
|
|
305
|
+
filename: def.filename,
|
|
306
|
+
path: artifactPath,
|
|
307
|
+
reason: `Confidence report validation failed: ${confidenceValidation.errors.join('; ')}`
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (confidenceValidation.warnings.length > 0) {
|
|
311
|
+
warnings.push(...confidenceValidation.warnings);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Special validation for run.status.json
|
|
316
|
+
if (key === 'runStatus') {
|
|
317
|
+
const statusValidation = validateRunStatusArtifact(data, expectedVersions);
|
|
318
|
+
if (statusValidation.errors.length > 0) {
|
|
319
|
+
errors.push(...statusValidation.errors);
|
|
320
|
+
}
|
|
321
|
+
if (statusValidation.warnings.length > 0) {
|
|
322
|
+
warnings.push(...statusValidation.warnings);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
} catch (parseError) {
|
|
327
|
+
invalidArtifacts.push({
|
|
328
|
+
key,
|
|
329
|
+
filename: def.filename,
|
|
330
|
+
path: artifactPath,
|
|
331
|
+
reason: `Invalid JSON: ${parseError.message}`
|
|
332
|
+
});
|
|
333
|
+
errors.push(`Artifact ${def.filename} contains invalid JSON: ${parseError.message}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} else if (def.type === 'directory') {
|
|
337
|
+
if (!existsSync(artifactPath)) {
|
|
338
|
+
missingArtifacts.push({
|
|
339
|
+
key,
|
|
340
|
+
filename: def.filename,
|
|
341
|
+
path: artifactPath,
|
|
342
|
+
reason: 'Directory does not exist'
|
|
343
|
+
});
|
|
344
|
+
// Directories are optional, so this is a warning, not an error
|
|
345
|
+
warnings.push(`Missing artifact directory: ${def.filename}`);
|
|
346
|
+
} else {
|
|
347
|
+
try {
|
|
348
|
+
const stats = statSync(artifactPath);
|
|
349
|
+
if (!stats.isDirectory()) {
|
|
350
|
+
invalidArtifacts.push({
|
|
351
|
+
key,
|
|
352
|
+
filename: def.filename,
|
|
353
|
+
path: artifactPath,
|
|
354
|
+
reason: 'Expected directory but found file or other type'
|
|
355
|
+
});
|
|
356
|
+
errors.push(`Invalid artifact type: ${def.filename} is not a directory`);
|
|
357
|
+
}
|
|
358
|
+
} catch (statError) {
|
|
359
|
+
warnings.push(`Cannot access artifact directory: ${def.filename} (${statError.message})`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Check 2: Cross-artifact consistency
|
|
366
|
+
const consistencyChecks = checkCrossArtifactConsistency(runDir);
|
|
367
|
+
if (consistencyChecks.errors.length > 0) {
|
|
368
|
+
errors.push(...consistencyChecks.errors);
|
|
369
|
+
}
|
|
370
|
+
if (consistencyChecks.warnings.length > 0) {
|
|
371
|
+
warnings.push(...consistencyChecks.warnings);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Determine overall verdict
|
|
375
|
+
// PHASE 21.1: EVIDENCE_LAW_VIOLATION errors are blocking - do not allow VALID_WITH_WARNINGS
|
|
376
|
+
// PHASE 22: EVIDENCE_INTENT_MISMATCH errors mark run as VERIFIED_WITH_ERRORS
|
|
377
|
+
// PHASE 23: GUARDRAILS_REPORT_MISMATCH errors mark run as VERIFIED_WITH_ERRORS
|
|
378
|
+
// PHASE 24: CONFIDENCE_INVARIANT_VIOLATION and CONFIDENCE_REPORT_MISMATCH errors mark run as VERIFIED_WITH_ERRORS
|
|
379
|
+
const hasEvidenceLawViolations = errors.some(e => e.includes('EVIDENCE_LAW_VIOLATION'));
|
|
380
|
+
const hasEvidenceIntentMismatches = errors.some(e => e.includes('EVIDENCE_INTENT_MISMATCH'));
|
|
381
|
+
const hasGuardrailsReportMismatches = errors.some(e => e.includes('GUARDRAILS_REPORT_MISMATCH'));
|
|
382
|
+
const hasConfidenceInvariantViolations = errors.some(e => e.includes('CONFIDENCE_INVARIANT_VIOLATION'));
|
|
383
|
+
const hasConfidenceReportMismatches = errors.some(e => e.includes('CONFIDENCE_REPORT_MISMATCH'));
|
|
384
|
+
const ok = errors.length === 0;
|
|
385
|
+
|
|
386
|
+
// PHASE 22/23/24: If evidence intent, guardrails report, confidence invariant violations, or confidence report mismatches found, mark as VERIFIED_WITH_ERRORS
|
|
387
|
+
const verdictStatus = ok ? 'VERIFIED' :
|
|
388
|
+
(hasEvidenceIntentMismatches || hasGuardrailsReportMismatches || hasConfidenceInvariantViolations || hasConfidenceReportMismatches ? 'VERIFIED_WITH_ERRORS' : 'INVALID');
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
ok,
|
|
392
|
+
verdictStatus, // PHASE 22: Explicit verdict status
|
|
393
|
+
errors,
|
|
394
|
+
warnings,
|
|
395
|
+
missingArtifacts,
|
|
396
|
+
invalidArtifacts,
|
|
397
|
+
contractVersionMismatches,
|
|
398
|
+
enforcementSummary,
|
|
399
|
+
verifiedAt: new Date().toISOString(),
|
|
400
|
+
// PHASE 21.1: Track evidence law violations separately
|
|
401
|
+
evidenceLawViolations: hasEvidenceLawViolations ? errors.filter(e => e.includes('EVIDENCE_LAW_VIOLATION')) : [],
|
|
402
|
+
// PHASE 22: Track evidence intent mismatches separately
|
|
403
|
+
evidenceIntentMismatches: hasEvidenceIntentMismatches ? errors.filter(e => e.includes('EVIDENCE_INTENT_MISMATCH')) : [],
|
|
404
|
+
// PHASE 23: Track guardrails report mismatches separately
|
|
405
|
+
guardrailsReportMismatches: hasGuardrailsReportMismatches ? errors.filter(e => e.includes('GUARDRAILS_REPORT_MISMATCH')) : [],
|
|
406
|
+
// PHASE 24: Track confidence invariant violations separately
|
|
407
|
+
confidenceInvariantViolations: hasConfidenceInvariantViolations ? errors.filter(e => e.includes('CONFIDENCE_INVARIANT_VIOLATION')) : [],
|
|
408
|
+
// PHASE 24: Track confidence report mismatches separately
|
|
409
|
+
confidenceReportMismatches: hasConfidenceReportMismatches ? errors.filter(e => e.includes('CONFIDENCE_REPORT_MISMATCH')) : []
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Validates the findings artifact for Evidence Law compliance.
|
|
415
|
+
*
|
|
416
|
+
* @param {Object} findingsData - Parsed findings.json data
|
|
417
|
+
* @param {string} runDir - Run directory path
|
|
418
|
+
* @returns {Object} { errors: [], warnings: [] }
|
|
419
|
+
*/
|
|
420
|
+
function validateFindingsArtifact(findingsData, runDir) {
|
|
421
|
+
const errors = [];
|
|
422
|
+
const warnings = [];
|
|
423
|
+
|
|
424
|
+
// Check required top-level keys
|
|
425
|
+
if (!findingsData.findings || !Array.isArray(findingsData.findings)) {
|
|
426
|
+
errors.push('findings.json missing or invalid findings array');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Check enforcement metadata exists
|
|
430
|
+
if (!findingsData.enforcement) {
|
|
431
|
+
warnings.push('findings.json missing enforcement metadata (Evidence Law may not have been applied)');
|
|
432
|
+
} else {
|
|
433
|
+
if (typeof findingsData.enforcement.droppedCount !== 'number') {
|
|
434
|
+
warnings.push('findings.json enforcement.droppedCount is not a number');
|
|
435
|
+
}
|
|
436
|
+
if (typeof findingsData.enforcement.downgradedCount !== 'number') {
|
|
437
|
+
warnings.push('findings.json enforcement.downgradedCount is not a number');
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Validate each finding
|
|
442
|
+
if (Array.isArray(findingsData.findings)) {
|
|
443
|
+
for (let i = 0; i < findingsData.findings.length; i++) {
|
|
444
|
+
const finding = findingsData.findings[i];
|
|
445
|
+
|
|
446
|
+
// PHASE 16: Check for CONFIRMED findings without complete evidencePackage
|
|
447
|
+
// PHASE 21.1: HARD LOCK - CONFIRMED without complete evidencePackage → INVALID (blocking error)
|
|
448
|
+
const severity = finding.severity || finding.status;
|
|
449
|
+
if (severity === FINDING_STATUS.CONFIRMED || severity === 'CONFIRMED') {
|
|
450
|
+
if (finding.evidencePackage) {
|
|
451
|
+
try {
|
|
452
|
+
// PHASE 21.1: Strict validation - throws if incomplete
|
|
453
|
+
validateEvidencePackageStrict(finding.evidencePackage, severity);
|
|
454
|
+
// If we get here, evidencePackage is complete
|
|
455
|
+
} catch (error) {
|
|
456
|
+
// PHASE 21.1: HARD FAILURE - blocking error with EVIDENCE_LAW_VIOLATION code
|
|
457
|
+
errors.push(
|
|
458
|
+
`EVIDENCE_LAW_VIOLATION: Finding at index ${i} is marked CONFIRMED but evidencePackage is incomplete. ` +
|
|
459
|
+
`Missing fields: ${error.missingFields?.join(', ') || 'unknown'}. ` +
|
|
460
|
+
`(type: ${finding.type || 'unknown'})`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
} else if (!finding.evidence || !isEvidenceSubstantive(finding.evidence)) {
|
|
464
|
+
// PHASE 21.1: CONFIRMED without evidencePackage and without substantive evidence → HARD FAILURE
|
|
465
|
+
errors.push(
|
|
466
|
+
`EVIDENCE_LAW_VIOLATION: Finding at index ${i} is marked CONFIRMED but lacks evidencePackage and evidence is insufficient. ` +
|
|
467
|
+
`(type: ${finding.type || 'unknown'})`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Check required fields
|
|
473
|
+
if (!finding.type) {
|
|
474
|
+
errors.push(`Finding at index ${i} missing required field: type`);
|
|
475
|
+
}
|
|
476
|
+
if (!finding.what_happened) {
|
|
477
|
+
errors.push(`Finding at index ${i} missing required field: what_happened`);
|
|
478
|
+
}
|
|
479
|
+
if (!finding.what_was_expected) {
|
|
480
|
+
errors.push(`Finding at index ${i} missing required field: what_was_expected`);
|
|
481
|
+
}
|
|
482
|
+
if (!finding.what_was_observed) {
|
|
483
|
+
errors.push(`Finding at index ${i} missing required field: what_was_observed`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return { errors, warnings };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* PHASE 22: Validates the evidence.intent.json artifact.
|
|
493
|
+
*
|
|
494
|
+
* @param {Object} intentData - Parsed evidence.intent.json data
|
|
495
|
+
* @param {string} runDir - Run directory path
|
|
496
|
+
* @returns {Object} { errors: [], warnings: [] }
|
|
497
|
+
*/
|
|
498
|
+
function validateEvidenceIntentArtifact(intentData, runDir) {
|
|
499
|
+
const errors = [];
|
|
500
|
+
const warnings = [];
|
|
501
|
+
|
|
502
|
+
// Check required top-level keys
|
|
503
|
+
if (typeof intentData.version !== 'number') {
|
|
504
|
+
errors.push('evidence.intent.json missing or invalid version field');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (!intentData.entries || !Array.isArray(intentData.entries)) {
|
|
508
|
+
errors.push('evidence.intent.json missing or invalid entries array');
|
|
509
|
+
return { errors, warnings };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Validate schema for each entry
|
|
513
|
+
for (let i = 0; i < intentData.entries.length; i++) {
|
|
514
|
+
const entry = intentData.entries[i];
|
|
515
|
+
|
|
516
|
+
if (!entry.findingIdentity) {
|
|
517
|
+
errors.push(`evidence.intent.json entry at index ${i} missing findingIdentity`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (!entry.requiredFields || !Array.isArray(entry.requiredFields)) {
|
|
521
|
+
errors.push(`evidence.intent.json entry at index ${i} missing or invalid requiredFields`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (!entry.captureOutcomes || typeof entry.captureOutcomes !== 'object') {
|
|
525
|
+
errors.push(`evidence.intent.json entry at index ${i} missing or invalid captureOutcomes`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (!Array.isArray(entry.missingFields)) {
|
|
529
|
+
errors.push(`evidence.intent.json entry at index ${i} missing or invalid missingFields`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Check deterministic ordering (entries should be sorted by findingIdentity)
|
|
533
|
+
if (i > 0) {
|
|
534
|
+
const prevIdentity = intentData.entries[i - 1].findingIdentity;
|
|
535
|
+
const currIdentity = entry.findingIdentity;
|
|
536
|
+
if (prevIdentity && currIdentity && prevIdentity.localeCompare(currIdentity) > 0) {
|
|
537
|
+
warnings.push(
|
|
538
|
+
`evidence.intent.json entries not in deterministic order: entry ${i} (${currIdentity}) should come before entry ${i - 1} (${prevIdentity})`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return { errors, warnings };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Validates the run.status.json artifact.
|
|
549
|
+
*
|
|
550
|
+
* @param {Object} statusData - Parsed run.status.json data
|
|
551
|
+
* @param {Object} expectedVersions - Expected artifact versions map
|
|
552
|
+
* @returns {Object} { errors: [], warnings: [] }
|
|
553
|
+
*/
|
|
554
|
+
function validateRunStatusArtifact(statusData, expectedVersions) {
|
|
555
|
+
const errors = [];
|
|
556
|
+
const warnings = [];
|
|
557
|
+
|
|
558
|
+
// Check required fields
|
|
559
|
+
if (!statusData.status) {
|
|
560
|
+
errors.push('run.status.json missing required field: status');
|
|
561
|
+
} else if (!['RUNNING', 'COMPLETE', 'FAILED'].includes(statusData.status)) {
|
|
562
|
+
warnings.push(`run.status.json has unexpected status: ${statusData.status}`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Check artifactVersions matches registry
|
|
566
|
+
if (statusData.artifactVersions) {
|
|
567
|
+
for (const [key, expectedVersion] of Object.entries(expectedVersions)) {
|
|
568
|
+
if (statusData.artifactVersions[key] === undefined) {
|
|
569
|
+
warnings.push(`run.status.json artifactVersions missing key: ${key}`);
|
|
570
|
+
} else if (statusData.artifactVersions[key] !== expectedVersion) {
|
|
571
|
+
warnings.push(
|
|
572
|
+
`run.status.json artifactVersions[${key}]=${statusData.artifactVersions[key]}, ` +
|
|
573
|
+
`expected ${expectedVersion}`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
} else {
|
|
578
|
+
warnings.push('run.status.json missing artifactVersions map');
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return { errors, warnings };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* PHASE 23: Validates the guardrails.report.json artifact.
|
|
586
|
+
*
|
|
587
|
+
* @param {Object} reportData - Parsed guardrails.report.json data
|
|
588
|
+
* @param {string} runDir - Run directory path
|
|
589
|
+
* @returns {Object} { errors: [], warnings: [] }
|
|
590
|
+
*/
|
|
591
|
+
function validateGuardrailsReportArtifact(reportData, runDir) {
|
|
592
|
+
const errors = [];
|
|
593
|
+
const warnings = [];
|
|
594
|
+
|
|
595
|
+
// Check required top-level keys
|
|
596
|
+
if (typeof reportData.version !== 'number') {
|
|
597
|
+
errors.push('guardrails.report.json missing or invalid version field');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (!reportData.summary || typeof reportData.summary !== 'object') {
|
|
601
|
+
errors.push('guardrails.report.json missing or invalid summary object');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (!reportData.perFinding || typeof reportData.perFinding !== 'object') {
|
|
605
|
+
errors.push('guardrails.report.json missing or invalid perFinding object');
|
|
606
|
+
return { errors, warnings };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Validate summary structure
|
|
610
|
+
if (reportData.summary) {
|
|
611
|
+
if (typeof reportData.summary.totalFindings !== 'number') {
|
|
612
|
+
errors.push('guardrails.report.json summary missing or invalid totalFindings');
|
|
613
|
+
}
|
|
614
|
+
if (!reportData.summary.byFinalDecision || typeof reportData.summary.byFinalDecision !== 'object') {
|
|
615
|
+
errors.push('guardrails.report.json summary missing or invalid byFinalDecision');
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Validate perFinding entries
|
|
620
|
+
const findingIdentities = Object.keys(reportData.perFinding).sort();
|
|
621
|
+
for (let i = 0; i < findingIdentities.length; i++) {
|
|
622
|
+
const findingIdentity = findingIdentities[i];
|
|
623
|
+
const entry = reportData.perFinding[findingIdentity];
|
|
624
|
+
|
|
625
|
+
if (!entry.finalDecision) {
|
|
626
|
+
errors.push(`guardrails.report.json perFinding[${findingIdentity}] missing finalDecision`);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (!Array.isArray(entry.appliedRules)) {
|
|
630
|
+
errors.push(`guardrails.report.json perFinding[${findingIdentity}] missing or invalid appliedRules`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (!Array.isArray(entry.contradictions)) {
|
|
634
|
+
errors.push(`guardrails.report.json perFinding[${findingIdentity}] missing or invalid contradictions`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (typeof entry.confidenceDelta !== 'number') {
|
|
638
|
+
errors.push(`guardrails.report.json perFinding[${findingIdentity}] missing or invalid confidenceDelta`);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (!Array.isArray(entry.reconciliationReasons)) {
|
|
642
|
+
errors.push(`guardrails.report.json perFinding[${findingIdentity}] missing or invalid reconciliationReasons`);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Check deterministic ordering (entries should be sorted by findingIdentity)
|
|
646
|
+
if (i > 0) {
|
|
647
|
+
const prevIdentity = findingIdentities[i - 1];
|
|
648
|
+
if (prevIdentity.localeCompare(findingIdentity) > 0) {
|
|
649
|
+
warnings.push(
|
|
650
|
+
`guardrails.report.json perFinding entries not in deterministic order: ${findingIdentity} should come before ${prevIdentity}`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// PHASE 23: Cross-validate with findings.json
|
|
657
|
+
const findingsPath = join(runDir, ARTIFACT_REGISTRY.findings.filename);
|
|
658
|
+
if (existsSync(findingsPath)) {
|
|
659
|
+
try {
|
|
660
|
+
const findingsContent = readFileSync(findingsPath, 'utf-8');
|
|
661
|
+
const findingsData = JSON.parse(findingsContent);
|
|
662
|
+
|
|
663
|
+
if (findingsData.findings && Array.isArray(findingsData.findings)) {
|
|
664
|
+
for (const finding of findingsData.findings) {
|
|
665
|
+
const findingIdentity = finding.findingId || finding.id || null;
|
|
666
|
+
if (findingIdentity && !reportData.perFinding[findingIdentity]) {
|
|
667
|
+
errors.push(
|
|
668
|
+
`GUARDRAILS_REPORT_MISMATCH: Finding ${findingIdentity} in findings.json missing from guardrails.report.json`
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
} catch (e) {
|
|
674
|
+
// findings.json validation errors are handled elsewhere
|
|
675
|
+
warnings.push(`Could not cross-validate guardrails.report.json with findings.json: ${e.message}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return { errors, warnings };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* PHASE 24: Validates the confidence.report.json artifact.
|
|
684
|
+
*
|
|
685
|
+
* @param {Object} reportData - Parsed confidence.report.json data
|
|
686
|
+
* @param {string} runDir - Run directory path
|
|
687
|
+
* @returns {Object} { errors: [], warnings: [] }
|
|
688
|
+
*/
|
|
689
|
+
function validateConfidenceReportArtifact(reportData, runDir) {
|
|
690
|
+
const errors = [];
|
|
691
|
+
const warnings = [];
|
|
692
|
+
|
|
693
|
+
// Check required top-level keys
|
|
694
|
+
if (typeof reportData.version !== 'number') {
|
|
695
|
+
errors.push('confidence.report.json missing or invalid version field');
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (!reportData.summary || typeof reportData.summary !== 'object') {
|
|
699
|
+
errors.push('confidence.report.json missing or invalid summary object');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (!reportData.perFinding || typeof reportData.perFinding !== 'object') {
|
|
703
|
+
errors.push('confidence.report.json missing or invalid perFinding object');
|
|
704
|
+
return { errors, warnings };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Validate summary structure
|
|
708
|
+
if (reportData.summary) {
|
|
709
|
+
if (typeof reportData.summary.totalFindings !== 'number') {
|
|
710
|
+
errors.push('confidence.report.json summary missing or invalid totalFindings');
|
|
711
|
+
}
|
|
712
|
+
if (!reportData.summary.byConfidenceLevel || typeof reportData.summary.byConfidenceLevel !== 'object') {
|
|
713
|
+
errors.push('confidence.report.json summary missing or invalid byConfidenceLevel');
|
|
714
|
+
}
|
|
715
|
+
if (!reportData.summary.byTruthStatus || typeof reportData.summary.byTruthStatus !== 'object') {
|
|
716
|
+
errors.push('confidence.report.json summary missing or invalid byTruthStatus');
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Validate perFinding entries
|
|
721
|
+
const findingIdentities = Object.keys(reportData.perFinding).sort();
|
|
722
|
+
for (let i = 0; i < findingIdentities.length; i++) {
|
|
723
|
+
const findingIdentity = findingIdentities[i];
|
|
724
|
+
const entry = reportData.perFinding[findingIdentity];
|
|
725
|
+
|
|
726
|
+
if (typeof entry.confidenceBefore !== 'number') {
|
|
727
|
+
errors.push(`confidence.report.json perFinding[${findingIdentity}] missing or invalid confidenceBefore`);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (typeof entry.confidenceAfter !== 'number') {
|
|
731
|
+
errors.push(`confidence.report.json perFinding[${findingIdentity}] missing or invalid confidenceAfter`);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (!entry.truthStatus) {
|
|
735
|
+
errors.push(`confidence.report.json perFinding[${findingIdentity}] missing truthStatus`);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (!Array.isArray(entry.appliedInvariants)) {
|
|
739
|
+
errors.push(`confidence.report.json perFinding[${findingIdentity}] missing or invalid appliedInvariants`);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (!Array.isArray(entry.invariantViolations)) {
|
|
743
|
+
errors.push(`confidence.report.json perFinding[${findingIdentity}] missing or invalid invariantViolations`);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (!Array.isArray(entry.explanation)) {
|
|
747
|
+
errors.push(`confidence.report.json perFinding[${findingIdentity}] missing or invalid explanation`);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Check deterministic ordering
|
|
751
|
+
if (i > 0) {
|
|
752
|
+
const prevIdentity = findingIdentities[i - 1];
|
|
753
|
+
if (prevIdentity.localeCompare(findingIdentity) > 0) {
|
|
754
|
+
warnings.push(
|
|
755
|
+
`confidence.report.json perFinding entries not in deterministic order: ${findingIdentity} should come before ${prevIdentity}`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// PHASE 24: Cross-validate with findings.json
|
|
762
|
+
const findingsPath = join(runDir, ARTIFACT_REGISTRY.findings.filename);
|
|
763
|
+
if (existsSync(findingsPath)) {
|
|
764
|
+
try {
|
|
765
|
+
const findingsContent = readFileSync(findingsPath, 'utf-8');
|
|
766
|
+
const findingsData = JSON.parse(findingsContent);
|
|
767
|
+
|
|
768
|
+
if (findingsData.findings && Array.isArray(findingsData.findings)) {
|
|
769
|
+
for (const finding of findingsData.findings) {
|
|
770
|
+
const findingIdentity = finding.findingId || finding.id || null;
|
|
771
|
+
if (findingIdentity && !reportData.perFinding[findingIdentity]) {
|
|
772
|
+
errors.push(
|
|
773
|
+
`CONFIDENCE_REPORT_MISMATCH: Finding ${findingIdentity} in findings.json missing from confidence.report.json`
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Validate confidence consistency
|
|
778
|
+
if (findingIdentity && reportData.perFinding[findingIdentity]) {
|
|
779
|
+
const reportEntry = reportData.perFinding[findingIdentity];
|
|
780
|
+
const findingConfidence = finding.confidence !== undefined ? finding.confidence : 0;
|
|
781
|
+
const findingStatus = finding.severity || finding.status || 'SUSPECTED';
|
|
782
|
+
|
|
783
|
+
if (Math.abs(reportEntry.confidenceAfter - findingConfidence) > 0.001) {
|
|
784
|
+
errors.push(
|
|
785
|
+
`CONFIDENCE_REPORT_MISMATCH: Finding ${findingIdentity} confidence mismatch: findings.json=${findingConfidence}, confidence.report.json=${reportEntry.confidenceAfter}`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (reportEntry.truthStatus !== findingStatus) {
|
|
790
|
+
errors.push(
|
|
791
|
+
`CONFIDENCE_REPORT_MISMATCH: Finding ${findingIdentity} status mismatch: findings.json=${findingStatus}, confidence.report.json=${reportEntry.truthStatus}`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
} catch (e) {
|
|
798
|
+
warnings.push(`Could not cross-validate confidence.report.json with findings.json: ${e.message}`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return { errors, warnings };
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Checks cross-artifact consistency (runId, timestamps, etc.).
|
|
807
|
+
*
|
|
808
|
+
* @param {string} runDir - Run directory path
|
|
809
|
+
* @returns {Object} { errors: [], warnings: [] }
|
|
810
|
+
*/
|
|
811
|
+
function checkCrossArtifactConsistency(runDir) {
|
|
812
|
+
const errors = [];
|
|
813
|
+
const warnings = [];
|
|
814
|
+
|
|
815
|
+
try {
|
|
816
|
+
// Read run.status.json for runId and timestamps
|
|
817
|
+
const statusPath = join(runDir, ARTIFACT_REGISTRY.runStatus.filename);
|
|
818
|
+
const timestamps = [];
|
|
819
|
+
let runId = null;
|
|
820
|
+
|
|
821
|
+
if (existsSync(statusPath)) {
|
|
822
|
+
try {
|
|
823
|
+
const statusData = JSON.parse(readFileSync(statusPath, 'utf-8'));
|
|
824
|
+
runId = statusData.runId;
|
|
825
|
+
|
|
826
|
+
// Collect timestamps
|
|
827
|
+
if (statusData.startedAt) timestamps.push({ artifact: 'runStatus', time: statusData.startedAt });
|
|
828
|
+
if (statusData.completedAt) timestamps.push({ artifact: 'runStatus', time: statusData.completedAt });
|
|
829
|
+
|
|
830
|
+
if (runId) {
|
|
831
|
+
// Check findings.json has matching runId (if it has one)
|
|
832
|
+
const findingsPath = join(runDir, ARTIFACT_REGISTRY.findings.filename);
|
|
833
|
+
if (existsSync(findingsPath)) {
|
|
834
|
+
try {
|
|
835
|
+
const findingsData = JSON.parse(readFileSync(findingsPath, 'utf-8'));
|
|
836
|
+
// Findings don't always have runId, so this is optional
|
|
837
|
+
if (findingsData.runId && findingsData.runId !== runId) {
|
|
838
|
+
warnings.push(
|
|
839
|
+
`runId mismatch: run.status.json has ${runId}, findings.json has ${findingsData.runId}`
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
} catch (e) {
|
|
843
|
+
// Already reported as invalid artifact
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Check summary.json has matching runId (if it has one)
|
|
848
|
+
const summaryPath = join(runDir, ARTIFACT_REGISTRY.summary.filename);
|
|
849
|
+
if (existsSync(summaryPath)) {
|
|
850
|
+
try {
|
|
851
|
+
const summaryData = JSON.parse(readFileSync(summaryPath, 'utf-8'));
|
|
852
|
+
if (summaryData.runId && summaryData.runId !== runId) {
|
|
853
|
+
warnings.push(
|
|
854
|
+
`runId mismatch: run.status.json has ${runId}, summary.json has ${summaryData.runId}`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
} catch (e) {
|
|
858
|
+
// Already reported as invalid artifact
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
} catch (e) {
|
|
863
|
+
// Already reported as invalid artifact
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Check timestamp monotonicity (soft warning) - reuse findingsPath if already read
|
|
868
|
+
if (runId) {
|
|
869
|
+
// findingsPath was already read above, but we need to check for detectedAt
|
|
870
|
+
const findingsPathForTimestamp = join(runDir, ARTIFACT_REGISTRY.findings.filename);
|
|
871
|
+
if (existsSync(findingsPathForTimestamp)) {
|
|
872
|
+
try {
|
|
873
|
+
const findingsDataForTimestamp = JSON.parse(readFileSync(findingsPathForTimestamp, 'utf-8'));
|
|
874
|
+
if (findingsDataForTimestamp.detectedAt) timestamps.push({ artifact: 'findings', time: findingsDataForTimestamp.detectedAt });
|
|
875
|
+
} catch (e) {
|
|
876
|
+
// Ignore
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
} else {
|
|
880
|
+
// If runId wasn't found, still try to read findings for timestamp
|
|
881
|
+
const findingsPathForTimestamp = join(runDir, ARTIFACT_REGISTRY.findings.filename);
|
|
882
|
+
if (existsSync(findingsPathForTimestamp)) {
|
|
883
|
+
try {
|
|
884
|
+
const findingsDataForTimestamp = JSON.parse(readFileSync(findingsPathForTimestamp, 'utf-8'));
|
|
885
|
+
if (findingsDataForTimestamp.detectedAt) timestamps.push({ artifact: 'findings', time: findingsDataForTimestamp.detectedAt });
|
|
886
|
+
} catch (e) {
|
|
887
|
+
// Ignore
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Sort timestamps and check monotonicity
|
|
893
|
+
if (timestamps.length > 1) {
|
|
894
|
+
timestamps.sort((a, b) => new Date(a.time) - new Date(b.time));
|
|
895
|
+
for (let i = 1; i < timestamps.length; i++) {
|
|
896
|
+
if (new Date(timestamps[i].time) < new Date(timestamps[i - 1].time)) {
|
|
897
|
+
warnings.push(
|
|
898
|
+
`Timestamp non-monotonic: ${timestamps[i].artifact} timestamp ${timestamps[i].time} ` +
|
|
899
|
+
`is before ${timestamps[i - 1].artifact} timestamp ${timestamps[i - 1].time}`
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
} catch (error) {
|
|
905
|
+
warnings.push(`Cross-artifact consistency check failed: ${error.message}`);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
return { errors, warnings };
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Formats verification verdict for CLI output.
|
|
913
|
+
*
|
|
914
|
+
* @param {Object} verdict - Verdict from verifyRun
|
|
915
|
+
* @param {boolean} verbose - Whether to include detailed output
|
|
916
|
+
* @returns {string} Formatted output string
|
|
917
|
+
*/
|
|
918
|
+
export function formatVerificationOutput(verdict, verbose = false) {
|
|
919
|
+
const lines = [];
|
|
920
|
+
|
|
921
|
+
// PHASE 22: Use verdictStatus if available
|
|
922
|
+
const status = verdict.verdictStatus || (verdict.ok ? 'VERIFIED' : 'INVALID');
|
|
923
|
+
|
|
924
|
+
if (status === 'VERIFIED' || status === 'VALID') {
|
|
925
|
+
if (verdict.warnings.length === 0) {
|
|
926
|
+
lines.push(`Run verification: ${status}`);
|
|
927
|
+
} else {
|
|
928
|
+
lines.push(`Run verification: ${status} WITH WARNINGS`);
|
|
929
|
+
if (verbose || verdict.warnings.length <= 3) {
|
|
930
|
+
lines.push('');
|
|
931
|
+
lines.push('Warnings:');
|
|
932
|
+
verdict.warnings.slice(0, verbose ? undefined : 3).forEach(w => {
|
|
933
|
+
lines.push(` - ${w}`);
|
|
934
|
+
});
|
|
935
|
+
if (!verbose && verdict.warnings.length > 3) {
|
|
936
|
+
lines.push(` ... and ${verdict.warnings.length - 3} more warnings`);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
} else if (status === 'VERIFIED_WITH_ERRORS') {
|
|
941
|
+
lines.push('Run verification: VERIFIED_WITH_ERRORS');
|
|
942
|
+
lines.push('');
|
|
943
|
+
lines.push('Blocking errors:');
|
|
944
|
+
verdict.errors.slice(0, verbose ? undefined : 3).forEach(e => {
|
|
945
|
+
lines.push(` - ${e}`);
|
|
946
|
+
});
|
|
947
|
+
if (!verbose && verdict.errors.length > 3) {
|
|
948
|
+
lines.push(` ... and ${verdict.errors.length - 3} more errors`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (verbose) {
|
|
953
|
+
lines.push('');
|
|
954
|
+
lines.push('Enforcement Summary:');
|
|
955
|
+
lines.push(` Total findings: ${verdict.enforcementSummary.totalFindings}`);
|
|
956
|
+
lines.push(` CONFIRMED: ${verdict.enforcementSummary.confirmedFindings}`);
|
|
957
|
+
lines.push(` SUSPECTED: ${verdict.enforcementSummary.suspectedFindings}`);
|
|
958
|
+
lines.push(` Findings without evidence: ${verdict.enforcementSummary.findingsWithoutEvidence}`);
|
|
959
|
+
lines.push(` Enforcement applied: ${verdict.enforcementSummary.enforcementApplied ? 'Yes' : 'No'}`);
|
|
960
|
+
|
|
961
|
+
if (verdict.missingArtifacts.length > 0) {
|
|
962
|
+
lines.push('');
|
|
963
|
+
lines.push('Missing artifacts:');
|
|
964
|
+
verdict.missingArtifacts.forEach(a => {
|
|
965
|
+
lines.push(` - ${a.filename} (${a.reason})`);
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (verdict.invalidArtifacts.length > 0) {
|
|
970
|
+
lines.push('');
|
|
971
|
+
lines.push('Invalid artifacts:');
|
|
972
|
+
verdict.invalidArtifacts.forEach(a => {
|
|
973
|
+
lines.push(` - ${a.filename} (${a.reason})`);
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return lines.join('\n');
|
|
979
|
+
}
|
|
980
|
+
|