@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.
Files changed (152) hide show
  1. package/README.md +14 -18
  2. package/bin/verax.js +7 -0
  3. package/package.json +3 -3
  4. package/src/cli/commands/baseline.js +104 -0
  5. package/src/cli/commands/default.js +79 -25
  6. package/src/cli/commands/ga.js +243 -0
  7. package/src/cli/commands/gates.js +95 -0
  8. package/src/cli/commands/inspect.js +131 -2
  9. package/src/cli/commands/release-check.js +213 -0
  10. package/src/cli/commands/run.js +246 -35
  11. package/src/cli/commands/security-check.js +211 -0
  12. package/src/cli/commands/truth.js +114 -0
  13. package/src/cli/entry.js +304 -67
  14. package/src/cli/util/angular-component-extractor.js +179 -0
  15. package/src/cli/util/angular-navigation-detector.js +141 -0
  16. package/src/cli/util/angular-network-detector.js +161 -0
  17. package/src/cli/util/angular-state-detector.js +162 -0
  18. package/src/cli/util/ast-interactive-detector.js +546 -0
  19. package/src/cli/util/ast-network-detector.js +603 -0
  20. package/src/cli/util/ast-usestate-detector.js +602 -0
  21. package/src/cli/util/bootstrap-guard.js +86 -0
  22. package/src/cli/util/determinism-runner.js +123 -0
  23. package/src/cli/util/determinism-writer.js +129 -0
  24. package/src/cli/util/env-url.js +4 -0
  25. package/src/cli/util/expectation-extractor.js +369 -73
  26. package/src/cli/util/findings-writer.js +126 -16
  27. package/src/cli/util/learn-writer.js +3 -1
  28. package/src/cli/util/observe-writer.js +3 -1
  29. package/src/cli/util/paths.js +3 -12
  30. package/src/cli/util/project-discovery.js +3 -0
  31. package/src/cli/util/project-writer.js +3 -1
  32. package/src/cli/util/run-resolver.js +64 -0
  33. package/src/cli/util/source-requirement.js +55 -0
  34. package/src/cli/util/summary-writer.js +1 -0
  35. package/src/cli/util/svelte-navigation-detector.js +163 -0
  36. package/src/cli/util/svelte-network-detector.js +80 -0
  37. package/src/cli/util/svelte-sfc-extractor.js +147 -0
  38. package/src/cli/util/svelte-state-detector.js +243 -0
  39. package/src/cli/util/vue-navigation-detector.js +177 -0
  40. package/src/cli/util/vue-sfc-extractor.js +162 -0
  41. package/src/cli/util/vue-state-detector.js +215 -0
  42. package/src/verax/cli/finding-explainer.js +56 -3
  43. package/src/verax/core/artifacts/registry.js +154 -0
  44. package/src/verax/core/artifacts/verifier.js +980 -0
  45. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  46. package/src/verax/core/baseline/baseline.snapshot.js +231 -0
  47. package/src/verax/core/capabilities/gates.js +499 -0
  48. package/src/verax/core/capabilities/registry.js +475 -0
  49. package/src/verax/core/confidence/confidence-compute.js +137 -0
  50. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  51. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  52. package/src/verax/core/confidence/confidence-weights.js +44 -0
  53. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  54. package/src/verax/core/confidence/confidence.loader.js +79 -0
  55. package/src/verax/core/confidence/confidence.schema.js +94 -0
  56. package/src/verax/core/confidence-engine-refactor.js +484 -0
  57. package/src/verax/core/confidence-engine.js +486 -0
  58. package/src/verax/core/confidence-engine.js.backup +471 -0
  59. package/src/verax/core/contracts/index.js +29 -0
  60. package/src/verax/core/contracts/types.js +185 -0
  61. package/src/verax/core/contracts/validators.js +381 -0
  62. package/src/verax/core/decision-snapshot.js +30 -3
  63. package/src/verax/core/decisions/decision.trace.js +276 -0
  64. package/src/verax/core/determinism/contract-writer.js +89 -0
  65. package/src/verax/core/determinism/contract.js +139 -0
  66. package/src/verax/core/determinism/diff.js +364 -0
  67. package/src/verax/core/determinism/engine.js +221 -0
  68. package/src/verax/core/determinism/finding-identity.js +148 -0
  69. package/src/verax/core/determinism/normalize.js +438 -0
  70. package/src/verax/core/determinism/report-writer.js +92 -0
  71. package/src/verax/core/determinism/run-fingerprint.js +118 -0
  72. package/src/verax/core/dynamic-route-intelligence.js +528 -0
  73. package/src/verax/core/evidence/evidence-capture-service.js +307 -0
  74. package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
  75. package/src/verax/core/evidence-builder.js +487 -0
  76. package/src/verax/core/execution-mode-context.js +77 -0
  77. package/src/verax/core/execution-mode-detector.js +190 -0
  78. package/src/verax/core/failures/exit-codes.js +86 -0
  79. package/src/verax/core/failures/failure-summary.js +76 -0
  80. package/src/verax/core/failures/failure.factory.js +225 -0
  81. package/src/verax/core/failures/failure.ledger.js +132 -0
  82. package/src/verax/core/failures/failure.types.js +196 -0
  83. package/src/verax/core/failures/index.js +10 -0
  84. package/src/verax/core/ga/ga-report-writer.js +43 -0
  85. package/src/verax/core/ga/ga.artifact.js +49 -0
  86. package/src/verax/core/ga/ga.contract.js +434 -0
  87. package/src/verax/core/ga/ga.enforcer.js +86 -0
  88. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  89. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  90. package/src/verax/core/guardrails/policy.loader.js +83 -0
  91. package/src/verax/core/guardrails/policy.schema.js +110 -0
  92. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  93. package/src/verax/core/guardrails-engine.js +505 -0
  94. package/src/verax/core/observe/run-timeline.js +316 -0
  95. package/src/verax/core/perf/perf.contract.js +186 -0
  96. package/src/verax/core/perf/perf.display.js +65 -0
  97. package/src/verax/core/perf/perf.enforcer.js +91 -0
  98. package/src/verax/core/perf/perf.monitor.js +209 -0
  99. package/src/verax/core/perf/perf.report.js +198 -0
  100. package/src/verax/core/pipeline-tracker.js +238 -0
  101. package/src/verax/core/product-definition.js +127 -0
  102. package/src/verax/core/release/provenance.builder.js +271 -0
  103. package/src/verax/core/release/release-report-writer.js +40 -0
  104. package/src/verax/core/release/release.enforcer.js +159 -0
  105. package/src/verax/core/release/reproducibility.check.js +221 -0
  106. package/src/verax/core/release/sbom.builder.js +283 -0
  107. package/src/verax/core/report/cross-index.js +192 -0
  108. package/src/verax/core/report/human-summary.js +222 -0
  109. package/src/verax/core/route-intelligence.js +419 -0
  110. package/src/verax/core/security/secrets.scan.js +326 -0
  111. package/src/verax/core/security/security-report.js +50 -0
  112. package/src/verax/core/security/security.enforcer.js +124 -0
  113. package/src/verax/core/security/supplychain.defaults.json +38 -0
  114. package/src/verax/core/security/supplychain.policy.js +326 -0
  115. package/src/verax/core/security/vuln.scan.js +265 -0
  116. package/src/verax/core/truth/truth.certificate.js +250 -0
  117. package/src/verax/core/ui-feedback-intelligence.js +515 -0
  118. package/src/verax/detect/confidence-engine.js +628 -40
  119. package/src/verax/detect/confidence-helper.js +33 -0
  120. package/src/verax/detect/detection-engine.js +18 -1
  121. package/src/verax/detect/dynamic-route-findings.js +335 -0
  122. package/src/verax/detect/expectation-chain-detector.js +417 -0
  123. package/src/verax/detect/expectation-model.js +3 -1
  124. package/src/verax/detect/findings-writer.js +141 -5
  125. package/src/verax/detect/index.js +229 -5
  126. package/src/verax/detect/journey-stall-detector.js +558 -0
  127. package/src/verax/detect/route-findings.js +218 -0
  128. package/src/verax/detect/ui-feedback-findings.js +207 -0
  129. package/src/verax/detect/verdict-engine.js +57 -3
  130. package/src/verax/detect/view-switch-correlator.js +242 -0
  131. package/src/verax/index.js +413 -45
  132. package/src/verax/learn/action-contract-extractor.js +682 -64
  133. package/src/verax/learn/route-validator.js +4 -1
  134. package/src/verax/observe/index.js +88 -843
  135. package/src/verax/observe/interaction-runner.js +25 -8
  136. package/src/verax/observe/observe-context.js +205 -0
  137. package/src/verax/observe/observe-helpers.js +191 -0
  138. package/src/verax/observe/observe-runner.js +226 -0
  139. package/src/verax/observe/observers/budget-observer.js +185 -0
  140. package/src/verax/observe/observers/console-observer.js +102 -0
  141. package/src/verax/observe/observers/coverage-observer.js +107 -0
  142. package/src/verax/observe/observers/interaction-observer.js +471 -0
  143. package/src/verax/observe/observers/navigation-observer.js +132 -0
  144. package/src/verax/observe/observers/network-observer.js +87 -0
  145. package/src/verax/observe/observers/safety-observer.js +82 -0
  146. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  147. package/src/verax/observe/ui-feedback-detector.js +742 -0
  148. package/src/verax/observe/ui-signal-sensor.js +148 -2
  149. package/src/verax/scan-summary-writer.js +42 -8
  150. package/src/verax/shared/artifact-manager.js +8 -5
  151. package/src/verax/shared/css-spinner-rules.js +204 -0
  152. 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
+