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