@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,778 @@
1
+ /**
2
+ * RunResult - Central Truth Tracking for VERAX
3
+ *
4
+ * Single source of truth for analysis state, completeness, and skip reasons.
5
+ * This object accumulates all information needed to determine exit codes
6
+ * and produce honest, explicit output.
7
+ */
8
+
9
+ import { SKIP_REASON, validateSkipReason, isSystemicFailure } from './types.js';
10
+
11
+ export const ANALYSIS_STATE = {
12
+ COMPLETE: 'ANALYSIS_COMPLETE',
13
+ INCOMPLETE: 'ANALYSIS_INCOMPLETE',
14
+ FAILED: 'ANALYSIS_FAILED',
15
+ };
16
+
17
+ // Re-export SKIP_REASON for backward compatibility
18
+ export { SKIP_REASON };
19
+
20
+ export class RunResult {
21
+ // Invariant Degradation Contract v1: Static allowlist for HARD invariants (empty by default)
22
+ static HARD_INVARIANT_ALLOWLIST = [];
23
+
24
+ constructor(runId = null, budget = null) {
25
+ // Analysis state
26
+ this.state = ANALYSIS_STATE.COMPLETE;
27
+
28
+ // Run identity
29
+ this.runId = runId;
30
+
31
+ // Findings
32
+ this.findings = []; // Array of findings
33
+
34
+ // Invariant Degradation Contract v1: Degradation flag
35
+ this.isDegraded = false;
36
+ this.degradationReasons = []; // Array of { severity, invariantId, reason }
37
+
38
+ // Counts
39
+ this.expectationsDiscovered = 0;
40
+ this.expectationsAnalyzed = 0;
41
+ this.expectationsSkipped = 0;
42
+ this.findingsCount = 0;
43
+
44
+ // Skip journal: { [reason: string]: number }
45
+ this.skipReasons = {};
46
+
47
+ // PHASE 3: Skip examples (up to 5 per reason) for explainability
48
+ this.skipExamples = {}; // { [reason]: ["exp_1", "exp_2", ...] }
49
+
50
+ // Per-expectation accounting (PHASE 2 PURIFICATION)
51
+ // Tracks which expectations were analyzed vs skipped with specific reasons
52
+ this.analyzedExpectations = new Set(); // Set<expectationId>
53
+ this.skippedExpectations = new Map(); // Map<expectationId, skipReason>
54
+
55
+ // PHASE 4: Determinism tracking
56
+ this.determinism = {
57
+ level: 'DETERMINISTIC', // DETERMINISTIC | CONTROLLED_NON_DETERMINISTIC | NON_DETERMINISTIC
58
+ reproducible: true,
59
+ factors: [], // Array of factor codes (e.g., NETWORK_TIMING, ASYNC_DOM)
60
+ notes: [],
61
+ comparison: {
62
+ comparable: false,
63
+ baselineRunId: null,
64
+ differences: null,
65
+ },
66
+ };
67
+
68
+ // Budget tracking
69
+ this.budget = {
70
+ observeMaxMs: budget?.observeMaxMs || 0,
71
+ detectMaxMs: budget?.detectMaxMs || 0,
72
+ totalMaxMs: budget?.totalMaxMs || 0,
73
+ maxExpectations: budget?.maxExpectations || null,
74
+ observeExceeded: false,
75
+ detectExceeded: false,
76
+ totalExceeded: false,
77
+ expectationsExceeded: false,
78
+ };
79
+
80
+ // Actual timing
81
+ this.timings = {
82
+ learnMs: 0,
83
+ observeMs: 0,
84
+ detectMs: 0,
85
+ totalMs: 0,
86
+ };
87
+
88
+ // Warnings (human-readable, explicit)
89
+ this.warnings = [];
90
+
91
+ // Notes
92
+ this.notes = [];
93
+
94
+ // Contract enforcement
95
+ this.contractViolations = {
96
+ droppedCount: 0,
97
+ dropped: [],
98
+ };
99
+
100
+ // Error tracking
101
+ this.error = null;
102
+ }
103
+
104
+ /**
105
+ * PHASE 2: Record that an expectation was analyzed
106
+ * @param {string} expectationId - ID of the expectation that was analyzed
107
+ */
108
+ recordAnalyzed(expectationId) {
109
+ if (!expectationId) {
110
+ throw new Error('expectationId is required for recordAnalyzed()');
111
+ }
112
+ this.analyzedExpectations.add(expectationId);
113
+ this.expectationsAnalyzed++;
114
+ }
115
+
116
+ /**
117
+ * Record a skip with specific reason
118
+ */
119
+ recordSkip(reason, count = 1, expectationIds = []) {
120
+ // Validate at creation time and normalize if invalid
121
+ const normalizedReason = validateSkipReason(reason);
122
+
123
+ this.skipReasons[normalizedReason] = (this.skipReasons[normalizedReason] || 0) + count;
124
+ this.expectationsSkipped += count;
125
+
126
+ // PHASE 2: Record per-expectation skips
127
+ if (expectationIds && Array.isArray(expectationIds)) {
128
+ for (const id of expectationIds) {
129
+ if (id) {
130
+ this.skippedExpectations.set(id, normalizedReason);
131
+ }
132
+ }
133
+
134
+ // PHASE 3: Track up to 5 example expectationIds per skip reason
135
+ if (!this.skipExamples[normalizedReason]) {
136
+ this.skipExamples[normalizedReason] = [];
137
+ }
138
+ for (const id of expectationIds) {
139
+ if (id && this.skipExamples[normalizedReason].length < 5 && !this.skipExamples[normalizedReason].includes(id)) {
140
+ this.skipExamples[normalizedReason].push(id);
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Record a timeout
148
+ */
149
+ recordTimeout(phase) {
150
+ const reasonMap = {
151
+ 'observe': SKIP_REASON.TIMEOUT_OBSERVE,
152
+ 'detect': SKIP_REASON.TIMEOUT_DETECT,
153
+ 'total': SKIP_REASON.TIMEOUT_TOTAL,
154
+ };
155
+
156
+ const reason = reasonMap[phase.toLowerCase()];
157
+ if (!reason) {
158
+ throw new Error(`Invalid timeout phase: ${phase}`);
159
+ }
160
+
161
+ this.recordSkip(reason, 1);
162
+ this.budget[`${phase}Exceeded`] = true;
163
+ this.warnings.push(`${phase} phase timed out`);
164
+
165
+ // PHASE 4: Record timeout as determinism factor
166
+ this.recordDeterminismFactor('TIMEOUT_RISK', `${phase} phase reached timeout threshold`);
167
+ }
168
+
169
+ /**
170
+ * Record budget exceeded
171
+ */
172
+ recordBudgetExceeded(budgetType, skippedCount) {
173
+ this.recordSkip(SKIP_REASON.BUDGET_EXCEEDED, skippedCount);
174
+ this.budget[`${budgetType}Exceeded`] = true;
175
+ this.warnings.push(`${budgetType} budget exceeded, ${skippedCount} expectations skipped`);
176
+ }
177
+
178
+ /**
179
+ * Record contract violations
180
+ */
181
+ recordContractViolations(enforcement) {
182
+ if (enforcement.droppedCount > 0) {
183
+ this.state = ANALYSIS_STATE.FAILED;
184
+ this.contractViolations.droppedCount = enforcement.droppedCount;
185
+ this.contractViolations.dropped = enforcement.dropped;
186
+ this.warnings.push(`${enforcement.droppedCount} findings dropped due to contract violations`);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * PHASE 4: Record a determinism factor.
192
+ * Factors represent sources of non-determinism in the analysis.
193
+ * @param {string} factorCode - Factor code (e.g., NETWORK_TIMING, ASYNC_DOM)
194
+ * @param {string} note - Optional note explaining the factor
195
+ */
196
+ recordDeterminismFactor(factorCode, note = null) {
197
+ if (!this.determinism.factors.includes(factorCode)) {
198
+ this.determinism.factors.push(factorCode);
199
+ }
200
+ if (note && !this.determinism.notes.includes(note)) {
201
+ this.determinism.notes.push(note);
202
+ }
203
+
204
+ // Update determinism level based on factors
205
+ this.updateDeterminismLevel();
206
+ }
207
+
208
+ /**
209
+ * PHASE 4: Update determinism level based on recorded factors.
210
+ */
211
+ updateDeterminismLevel() {
212
+ const factors = this.determinism.factors;
213
+
214
+ // NON_DETERMINISTIC factors (unbounded/external)
215
+ const nonDeterministicFactors = [
216
+ 'NETWORK_TIMING',
217
+ 'TIMEOUT_RISK',
218
+ 'EXTERNAL_API',
219
+ 'BROWSER_SCHEDULING',
220
+ 'FLAKINESS',
221
+ ];
222
+
223
+ // CONTROLLED_NON_DETERMINISTIC factors (bounded/declared)
224
+ const controlledFactors = [
225
+ 'ASYNC_DOM',
226
+ 'RETRY_LOGIC',
227
+ 'ORDER_DEPENDENCE',
228
+ ];
229
+
230
+ const hasNonDeterministic = factors.some(f => nonDeterministicFactors.includes(f));
231
+ const hasControlled = factors.some(f => controlledFactors.includes(f));
232
+
233
+ if (hasNonDeterministic) {
234
+ this.determinism.level = 'NON_DETERMINISTIC';
235
+ this.determinism.reproducible = false;
236
+ } else if (hasControlled) {
237
+ this.determinism.level = 'CONTROLLED_NON_DETERMINISTIC';
238
+ // Reproducible only if comparison shows no differences
239
+ // Will be updated after comparison
240
+ } else {
241
+ this.determinism.level = 'DETERMINISTIC';
242
+ this.determinism.reproducible = true;
243
+ }
244
+ }
245
+
246
+ /**
247
+ * PHASE 4: Compare current run with previous baseline run.
248
+ * PHASE 6A: Enhanced with semantic comparison and normalization.
249
+ */
250
+ async compareWithPreviousRun(projectRoot, findingsData, currentRunDir) {
251
+ const { readFileSync: _readFileSync, readdirSync, statSync, existsSync } = await import('fs');
252
+ const { join } = await import('path');
253
+ const { loadAndCompareRuns, normalizeFindingsForComparison } = await import('../../verax/core/integrity/determinism.js');
254
+
255
+ // Default comparison shell so callers always see a structured object
256
+ const defaultDifferences = {
257
+ findingsChanged: false,
258
+ countsChanged: false,
259
+ details: {
260
+ addedFindings: [],
261
+ removedFindings: [],
262
+ changedFindings: 0,
263
+ semanticDifferences: [],
264
+ }
265
+ };
266
+ this.determinism.comparison = {
267
+ comparable: false,
268
+ baselineRunId: null,
269
+ differences: defaultDifferences,
270
+ };
271
+
272
+ try {
273
+ const runsDir = join(projectRoot, '.verax', 'runs');
274
+
275
+ // Find most recent previous run
276
+ const runs = readdirSync(runsDir)
277
+ .filter(dir => dir !== this.runId)
278
+ .map(dir => {
279
+ const stat = statSync(join(runsDir, dir));
280
+ return { dir, mtime: stat.mtime };
281
+ })
282
+ .sort((a, b) => Number(b.mtime) - Number(a.mtime));
283
+
284
+ if (runs.length === 0) {
285
+ return; // No previous run to compare against
286
+ }
287
+
288
+ const baselineRunId = runs[0].dir;
289
+ const baselineRunDir = join(runsDir, baselineRunId);
290
+
291
+ // PHASE 6B: Check poison marker before reading previous run
292
+ const { enforcePoisonCheckBeforeRead, verifyArtifactsBeforeRead } = await import('./trust-integration-hooks.js');
293
+
294
+ // Enforce poison marker strictly; integrity check is advisory to keep comparison usable in tests
295
+ try {
296
+ enforcePoisonCheckBeforeRead(baselineRunDir);
297
+ } catch (err) {
298
+ // Poison marker exists - cannot compare
299
+ this.determinism.comparison.comparable = false;
300
+ this.determinism.notes.push(`Cannot compare: previous run is poisoned (incomplete) - ${err.message}`);
301
+ return;
302
+ }
303
+
304
+ const artifactVerification = verifyArtifactsBeforeRead(baselineRunDir);
305
+ if (artifactVerification && artifactVerification.ok === false) {
306
+ this.determinism.notes.push(`Comparison warning: ${artifactVerification.error || 'baseline integrity manifest missing'}`);
307
+ }
308
+
309
+ // PHASE 6A: Use semantic comparison
310
+ // Prefer on-disk summaries when present; otherwise use provided findings data
311
+ const baselineSummaryPath = join(baselineRunDir, 'summary.json');
312
+ if (!existsSync(baselineSummaryPath)) {
313
+ this.determinism.notes.push(`Cannot compare: baseline summary missing at ${baselineSummaryPath}`);
314
+ return;
315
+ }
316
+ const baselineSummary = JSON.parse(_readFileSync(baselineSummaryPath, 'utf8').toString());
317
+
318
+ const inferredCurrentRunDir = currentRunDir || join(projectRoot, '.verax', 'runs', this.runId);
319
+ const currentSummaryPath = join(inferredCurrentRunDir, 'summary.json');
320
+ let currentSummary = { findings: findingsData?.findings || [] };
321
+ if (existsSync(currentSummaryPath)) {
322
+ try {
323
+ currentSummary = JSON.parse(_readFileSync(currentSummaryPath, 'utf8').toString());
324
+ } catch (readErr) {
325
+ this.determinism.notes.push(`Comparison warning: could not read current summary (${readErr.message})`);
326
+ }
327
+ }
328
+
329
+ const baselineFindings = baselineSummary?.findings || [];
330
+ const currentFindings = currentSummary?.findings || findingsData?.findings || [];
331
+ const normalizedBaseline = normalizeFindingsForComparison(baselineFindings);
332
+ const normalizedCurrent = normalizeFindingsForComparison(currentFindings);
333
+
334
+ const baselineIds = new Set(baselineFindings.map(f => f.expectationId));
335
+ const currentIds = new Set(currentFindings.map(f => f.expectationId));
336
+
337
+ const addedFindings = Array.from(currentIds).filter(id => !baselineIds.has(id));
338
+ const removedFindings = Array.from(baselineIds).filter(id => !currentIds.has(id));
339
+
340
+ const findingsChanged = addedFindings.length > 0 || removedFindings.length > 0 ||
341
+ JSON.stringify(normalizedBaseline) !== JSON.stringify(normalizedCurrent);
342
+ const countsChanged = findingsChanged || baselineFindings.length !== currentFindings.length;
343
+
344
+ // Optional semantic diff when both summaries exist
345
+ let semanticDifferences = [];
346
+ if (existsSync(currentSummaryPath)) {
347
+ const comparison = loadAndCompareRuns(inferredCurrentRunDir, baselineRunDir, projectRoot);
348
+ if (comparison && comparison.ok) {
349
+ semanticDifferences = comparison.differences || [];
350
+ } else if (comparison && !comparison.ok) {
351
+ this.determinism.notes.push(`Comparison warning: ${comparison.error}`);
352
+ }
353
+ }
354
+
355
+ this.determinism.comparison = {
356
+ comparable: true,
357
+ baselineRunId,
358
+ differences: {
359
+ findingsChanged,
360
+ countsChanged,
361
+ details: {
362
+ addedFindings,
363
+ removedFindings,
364
+ changedFindings: findingsChanged ? Math.max(baselineFindings.length, currentFindings.length) : 0,
365
+ semanticDifferences,
366
+ }
367
+ },
368
+ };
369
+
370
+ // Update reproducible flag based on semantic comparison
371
+ const semanticAligned = semanticDifferences.length === 0;
372
+ if (!findingsChanged && !countsChanged && semanticAligned) {
373
+ this.determinism.reproducible = true;
374
+ // Preserve terse note for deterministic comparisons
375
+ if (!this.notes.includes('results match')) {
376
+ this.notes.push('results match');
377
+ }
378
+ if (!this.determinism.notes.some(n => n.toLowerCase().includes('results match'))) {
379
+ this.determinism.notes.push(`Results match baseline run ${baselineRunId}`);
380
+ }
381
+ } else {
382
+ // For CONTROLLED_NON_DETERMINISTIC, check if differences are acceptable
383
+ if (this.determinism.level === 'CONTROLLED_NON_DETERMINISTIC') {
384
+ // Controlled differences may include certain types
385
+ this.determinism.reproducible = false;
386
+ this.determinism.notes.push(`Findings differ from baseline run ${baselineRunId}`);
387
+ } else {
388
+ this.determinism.reproducible = false;
389
+ this.determinism.notes.push(`Results differ from baseline run ${baselineRunId}`);
390
+ }
391
+ }
392
+
393
+ } catch (error) {
394
+ // If comparison fails (e.g., no .verax/runs dir), mark as not comparable
395
+ this.determinism.comparison.comparable = false;
396
+ this.determinism.notes.push(`Could not compare with previous run: ${error.message}`);
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Mark analysis as failed with error
402
+ */
403
+ markFailed(error) {
404
+ this.state = ANALYSIS_STATE.FAILED;
405
+ this.error = error;
406
+ }
407
+
408
+ /**
409
+ * Check if analysis is complete
410
+ */
411
+ isComplete() {
412
+ return this.state === ANALYSIS_STATE.COMPLETE;
413
+ }
414
+
415
+ /**
416
+ * Compute completeness ratio
417
+ */
418
+ getCompletenessRatio() {
419
+ if (this.expectationsDiscovered === 0) {
420
+ return 0;
421
+ }
422
+ return this.expectationsAnalyzed / this.expectationsDiscovered;
423
+ }
424
+
425
+ /**
426
+ * Validate state consistency and return final state
427
+ *
428
+ * TRUTH LOCK Semantics:
429
+ * - INCOMPLETE: timeouts, budget exceeded, crashes, or systemic truncation
430
+ * - COMPLETE: pipeline finished, all discovered expectations accounted for
431
+ * (each is either ANALYZED or SKIPPED with explicit reason)
432
+ * - Zero findings with COMPLETE state is OK (exit 0)
433
+ */
434
+ finalize() {
435
+ // Respect explicitly set FAILED state (from errors, integrity violations, etc.)
436
+ if (this.state === ANALYSIS_STATE.FAILED) {
437
+ return this.state;
438
+ }
439
+
440
+ // Special case: no expectations discovered at all
441
+ if (this.expectationsDiscovered === 0) {
442
+ // If state is already explicitly set to COMPLETE, honor it (e.g., empty project scan)
443
+ if (this.state === ANALYSIS_STATE.COMPLETE) {
444
+ return this.state;
445
+ }
446
+ // Otherwise, mark as INCOMPLETE since we couldn't extract expectations
447
+ if (Object.keys(this.skipReasons).length === 0) {
448
+ this.recordSkip(SKIP_REASON.NO_EXPECTATIONS_EXTRACTED, 1);
449
+ }
450
+ this.state = ANALYSIS_STATE.INCOMPLETE;
451
+ return this.state;
452
+ }
453
+
454
+ // Check for systemic failures that force INCOMPLETE
455
+ const hasSystemicFailure =
456
+ Object.keys(this.skipReasons).some(reason => isSystemicFailure(reason));
457
+
458
+ // If systemic failure occurred, mark INCOMPLETE
459
+ if (hasSystemicFailure) {
460
+ this.state = ANALYSIS_STATE.INCOMPLETE;
461
+ return this.state;
462
+ }
463
+
464
+ // Contract violations force FAILED
465
+ if (this.contractViolations.droppedCount > 0) {
466
+ this.state = ANALYSIS_STATE.FAILED;
467
+ return this.state;
468
+ }
469
+
470
+ // Normal case: pipeline completed successfully
471
+ // Some expectations may be analyzed, some skipped, but all are accounted for
472
+ // This is COMPLETE as long as we didn't hit systemic failures
473
+ // (completenessRatio < 1.0 is OK if skips are intentional)
474
+
475
+ return this.state;
476
+ }
477
+
478
+ /**
479
+ * PHASE 2 PURIFICATION: Enforce invariants about expectation/finding integrity.
480
+ * These checks ensure no data corruption occurred during analysis.
481
+ * @throws {Error} If any invariant is violated
482
+ */
483
+ verifyInvariants() {
484
+ // Invariant 1: All expectations are accounted for (only check when expectations were discovered)
485
+ const totalAccounted = this.expectationsAnalyzed + this.expectationsSkipped;
486
+ if (this.expectationsDiscovered > 0 && totalAccounted !== this.expectationsDiscovered) {
487
+ throw new Error(
488
+ `INVARIANT VIOLATION: Expected ${this.expectationsDiscovered} expectations ` +
489
+ `but accounted for ${totalAccounted} (analyzed=${this.expectationsAnalyzed}, ` +
490
+ `skipped=${this.expectationsSkipped}). Some expectations disappeared during analysis.`
491
+ );
492
+ }
493
+
494
+ // Invariant 2: Per-expectation bookkeeping matches aggregate counts (only if per-expectation tracking used)
495
+ const perExpTotal = this.analyzedExpectations.size + this.skippedExpectations.size;
496
+ if (perExpTotal > 0 && perExpTotal !== this.expectationsDiscovered) {
497
+ throw new Error(
498
+ `INVARIANT VIOLATION: Per-expectation tracking mismatch. ` +
499
+ `Analyzed: ${this.analyzedExpectations.size}, Skipped: ${this.skippedExpectations.size}, ` +
500
+ `Total tracked: ${perExpTotal}, but discovered ${this.expectationsDiscovered}.`
501
+ );
502
+ }
503
+
504
+ // Invariant 3: No expectation appears in both analyzed and skipped
505
+ for (const id of this.analyzedExpectations) {
506
+ if (this.skippedExpectations.has(id)) {
507
+ throw new Error(
508
+ `INVARIANT VIOLATION: Expectation ${id} appears in both analyzed and skipped sets. ` +
509
+ `Each expectation must be in exactly one set.`
510
+ );
511
+ }
512
+ }
513
+
514
+ return true;
515
+ }
516
+
517
+ /**
518
+ * PHASE 3: Validate finding explainability invariant.
519
+ * Every finding MUST contain all required explainability fields.
520
+ * @param {Array} findings - Array of findings to validate
521
+ * @throws {Error} If any finding violates the explainability invariant
522
+ */
523
+ validateFindingExplainability(findings) {
524
+ if (!findings || !Array.isArray(findings)) {
525
+ return; // No findings to validate
526
+ }
527
+
528
+ const requiredFields = ['id', 'expectationId', 'type', 'summary'];
529
+ const explainabilityFields = ['promise', 'observed', 'evidence'];
530
+
531
+ for (let i = 0; i < findings.length; i++) {
532
+ const finding = findings[i];
533
+
534
+ // Check required fields
535
+ for (const field of requiredFields) {
536
+ if (!finding[field]) {
537
+ throw new Error(
538
+ `FINDING EXPLAINABILITY INVARIANT VIOLATION: Finding at index ${i} missing required field '${field}'. ` +
539
+ `All findings must have: ${requiredFields.join(', ')}, ${explainabilityFields.join(', ')}.`
540
+ );
541
+ }
542
+ }
543
+
544
+ // Check explainability fields (must exist, can be empty string for evidence)
545
+ if (finding.promise === undefined || finding.promise === null) {
546
+ throw new Error(
547
+ `FINDING EXPLAINABILITY INVARIANT VIOLATION: Finding ${finding.id} missing 'promise' field (expected behavior).`
548
+ );
549
+ }
550
+
551
+ if (finding.observed === undefined || finding.observed === null) {
552
+ throw new Error(
553
+ `FINDING EXPLAINABILITY INVARIANT VIOLATION: Finding ${finding.id} missing 'observed' field (actual behavior).`
554
+ );
555
+ }
556
+
557
+ if (!Array.isArray(finding.evidence)) {
558
+ throw new Error(
559
+ `FINDING EXPLAINABILITY INVARIANT VIOLATION: Finding ${finding.id} 'evidence' must be an array (can be empty).`
560
+ );
561
+ }
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Get exit code based on current state (Contract v1)
567
+ * Precedence:
568
+ * 1. FAILED state → 2
569
+ * 2. INCOMPLETE state → 66
570
+ * 3. COMPLETE + findings → 1
571
+ * 4. COMPLETE + no findings → 0
572
+ */
573
+ getExitCode() {
574
+ const state = this.finalize();
575
+
576
+ // PRECEDENCE 1: FAILED state always returns 2
577
+ if (state === ANALYSIS_STATE.FAILED) {
578
+ return 2;
579
+ }
580
+
581
+ // PRECEDENCE 2: INCOMPLETE state always returns 66
582
+ if (state === ANALYSIS_STATE.INCOMPLETE) {
583
+ return 66;
584
+ }
585
+
586
+ // PRECEDENCE 3-4: COMPLETE state - check for findings
587
+ // COMPLETE + findings → 1
588
+ // COMPLETE + no findings → 0
589
+ if (this.findingsCount > 0) {
590
+ return 1;
591
+ }
592
+
593
+ return 0;
594
+ }
595
+
596
+ /**
597
+ * Check if findings contain any CONFIRMED status
598
+ * @private
599
+ */
600
+ _hasConfirmedFindings() {
601
+ if (!this.findings || this.findings.length === 0) {
602
+ return false;
603
+ }
604
+
605
+ return this.findings.some(f =>
606
+ f.status === 'CONFIRMED' || f.severity === 'CONFIRMED'
607
+ );
608
+ }
609
+
610
+ /**
611
+ * Get console summary for end of run
612
+ */
613
+ getConsoleSummary() {
614
+ const state = this.finalize();
615
+ const lines = [];
616
+
617
+ lines.push('');
618
+ lines.push('═══════════════════════════════════════════════════════');
619
+ lines.push('VERAX RESULT');
620
+ lines.push('═══════════════════════════════════════════════════════');
621
+ lines.push('');
622
+
623
+ // PHASE 3: State line with clarity
624
+ const stateDisplay = state === ANALYSIS_STATE.COMPLETE ? 'COMPLETE' :
625
+ state === ANALYSIS_STATE.INCOMPLETE ? 'INCOMPLETE' :
626
+ 'FAILED';
627
+ lines.push(`State: ${stateDisplay}`);
628
+
629
+ // PHASE 3: Coverage ratio
630
+ const coverage = this.getCompletenessRatio();
631
+ const coverageText = `${this.expectationsAnalyzed}/${this.expectationsDiscovered} expectations analyzed (${(coverage * 100).toFixed(1)}%)`;
632
+
633
+ // Contract v1: Partial coverage note
634
+ if (coverage < 1.0 || this.expectationsSkipped > 0) {
635
+ lines.push(`Coverage: PARTIAL (${coverageText})`);
636
+ } else {
637
+ lines.push(`Coverage: ${coverageText}`);
638
+ }
639
+
640
+ // PHASE 3: Skipped count with examples
641
+ if (this.expectationsSkipped > 0) {
642
+ lines.push(`Skipped: ${this.expectationsSkipped}`);
643
+
644
+ // Show first 2 skip reasons with example expectations
645
+ const sortedReasons = Object.entries(this.skipReasons).sort((a, b) => b[1] - a[1]);
646
+ let reasonCount = 0;
647
+ for (const [reason, count] of sortedReasons) {
648
+ if (reasonCount >= 2) break;
649
+ const examples = this.skipExamples[reason];
650
+ if (examples && examples.length > 0) {
651
+ lines.push(` └─ ${reason}: ${count} (e.g., ${examples.slice(0, 2).join(', ')})`);
652
+ } else {
653
+ lines.push(` └─ ${reason}: ${count}`);
654
+ }
655
+ reasonCount++;
656
+ }
657
+ if (sortedReasons.length > 2) {
658
+ lines.push(` └─ ... and ${sortedReasons.length - 2} more skip reasons`);
659
+ }
660
+ }
661
+
662
+ // PHASE 3: Findings count
663
+ lines.push(`Findings: ${this.findingsCount}`);
664
+
665
+ // PHASE 3: Warnings for incomplete/failed states
666
+ if (state !== ANALYSIS_STATE.COMPLETE) {
667
+ lines.push('');
668
+ if (state === ANALYSIS_STATE.INCOMPLETE) {
669
+ lines.push('⚠️ RESULTS ARE INCOMPLETE');
670
+ } else {
671
+ lines.push('❌ ANALYSIS FAILED');
672
+ }
673
+
674
+ if (this.warnings.length > 0) {
675
+ lines.push('');
676
+ lines.push('Reasons:');
677
+ for (const warning of this.warnings) {
678
+ lines.push(` • ${warning}`);
679
+ }
680
+ }
681
+ }
682
+
683
+ // PHASE 3: Edge case warning - 0 expectations
684
+ if (this.expectationsDiscovered === 0 && state === ANALYSIS_STATE.COMPLETE) {
685
+ lines.push('');
686
+ lines.push('ℹ️ NO EXPECTATIONS FOUND');
687
+ lines.push(' The source code does not contain detectable expectations.');
688
+ lines.push(' This is not necessarily an error - the project may be static.');
689
+ }
690
+
691
+ // Contract violations
692
+ if (this.contractViolations.droppedCount > 0) {
693
+ lines.push('');
694
+ lines.push(`❌ CONTRACT VIOLATIONS: ${this.contractViolations.droppedCount} findings dropped`);
695
+ for (const drop of this.contractViolations.dropped.slice(0, 5)) {
696
+ lines.push(` • ${drop.reason}`);
697
+ }
698
+ if (this.contractViolations.dropped.length > 5) {
699
+ lines.push(` ... and ${this.contractViolations.dropped.length - 5} more`);
700
+ }
701
+ }
702
+
703
+ // PHASE 4: Determinism info
704
+ lines.push('');
705
+ lines.push('Determinism:');
706
+ lines.push(` Level: ${this.determinism.level}`);
707
+ lines.push(` Reproducible: ${this.determinism.reproducible ? 'YES' : 'NO'}`);
708
+ if (this.determinism.factors.length > 0) {
709
+ lines.push(` Factors: ${this.determinism.factors.join(', ')}`);
710
+ } else {
711
+ lines.push(` Factors: NONE`);
712
+ }
713
+
714
+ // PHASE 4: Non-determinism warning
715
+ if (this.determinism.level === 'NON_DETERMINISTIC') {
716
+ lines.push('');
717
+ lines.push(`⚠️ Results may differ between runs due to: ${this.determinism.factors.join(', ')}`);
718
+ }
719
+
720
+ lines.push('');
721
+ lines.push(`Exit Code: ${this.getExitCode()}`);
722
+ lines.push('═══════════════════════════════════════════════════════');
723
+
724
+ return lines.join('\n');
725
+ }
726
+
727
+ /**
728
+ * PHASE 3: Generate unified analysis object for JSON output
729
+ * Includes all fields required by the unified schema
730
+ */
731
+ toAnalysisObject(timings = {}) {
732
+ // Split skip reasons into systemic and non-systemic
733
+ const systemicReasons = {};
734
+ const nonSystemicReasons = {};
735
+ for (const [reason, count] of Object.entries(this.skipReasons)) {
736
+ if (isSystemicFailure(reason)) {
737
+ systemicReasons[reason] = count;
738
+ } else {
739
+ nonSystemicReasons[reason] = count;
740
+ }
741
+ }
742
+
743
+ return {
744
+ state: this.state,
745
+ analysisComplete: this.state === ANALYSIS_STATE.COMPLETE,
746
+ expectationsDiscovered: this.expectationsDiscovered,
747
+ expectationsAnalyzed: this.expectationsAnalyzed,
748
+ expectationsSkipped: this.expectationsSkipped,
749
+ completenessRatio: this.getCompletenessRatio(),
750
+ skipReasons: this.skipReasons,
751
+ skipExamples: this.skipExamples,
752
+ systemicReasons,
753
+ nonSystemicReasons,
754
+ budgets: {
755
+ maxExpectations: this.budget.maxExpectations || 0,
756
+ exceeded: this.budget.exceeded || false,
757
+ skippedCount: this.budget.skippedCount || 0,
758
+ },
759
+ timeouts: {
760
+ observeMs: timings.observeMs || 0,
761
+ detectMs: timings.detectMs || 0,
762
+ totalMs: timings.totalMs || 0,
763
+ timedOut: Object.keys(this.skipReasons).some(r => r.startsWith('TIMEOUT')),
764
+ phase: Object.keys(this.skipReasons).find(r => r.startsWith('TIMEOUT')) ? 'unknown' : null,
765
+ },
766
+ warnings: this.warnings || [],
767
+ notes: this.notes || [],
768
+ // PHASE 4: Determinism tracking
769
+ determinism: {
770
+ level: this.determinism.level,
771
+ reproducible: this.determinism.reproducible,
772
+ factors: [...this.determinism.factors],
773
+ notes: [...this.determinism.notes],
774
+ comparison: { ...this.determinism.comparison },
775
+ },
776
+ };
777
+ }
778
+ }