@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,529 @@
1
+ import { resolveSelector, findSubmitButton, isElementInteractable } from './selector-resolver.js';
2
+ import { createEvidenceBundle } from './evidence-engine.js';
3
+
4
+ /**
5
+ * Interaction Planner
6
+ * Converts extracted promises into executable Playwright interactions
7
+ * with bounded timeouts and evidence capture
8
+ *
9
+ * H5: Added read-only safety mode by default
10
+ */
11
+
12
+ const GLOBAL_TIMEOUT = 5 * 60 * 1000; // 5 minutes total
13
+ const _PER_PROMISE_TIMEOUT = 15 * 1000; // 15 seconds per promise
14
+ const WAIT_FOR_EFFECT_TIMEOUT = 3000; // 3 seconds to observe effect
15
+
16
+ const MUTATING_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];
17
+
18
+ export class InteractionPlanner {
19
+ constructor(page, evidencePath, _options = {}) {
20
+ this.page = page;
21
+ this.evidencePath = evidencePath;
22
+ this.startTime = Date.now();
23
+ this.networkEvents = [];
24
+ this.consoleEvents = [];
25
+ this.attempts = [];
26
+
27
+ // CONSTITUTIONAL: Read-only mode enforced (no writes allowed)
28
+ this.blockedRequests = [];
29
+ }
30
+
31
+ /**
32
+ * Check if we're still within budget
33
+ */
34
+ isWithinBudget() {
35
+ return Date.now() - this.startTime < GLOBAL_TIMEOUT;
36
+ }
37
+
38
+ /**
39
+ * H5: Check if request method is mutating
40
+ */
41
+ isMutatingRequest(method) {
42
+ return MUTATING_METHODS.includes((method || 'GET').toUpperCase());
43
+ }
44
+
45
+ /**
46
+ * CONSTITUTIONAL: Block all mutating requests (read-only mode)
47
+ */
48
+ shouldBlockRequest(method) {
49
+ return this.isMutatingRequest(method);
50
+ }
51
+
52
+ /**
53
+ * H5: Get blocked requests
54
+ */
55
+ getBlockedRequests() {
56
+ return this.blockedRequests;
57
+ }
58
+
59
+ /**
60
+ * Plan and execute a single promise
61
+ */
62
+ async executeSinglePromise(promise, expNum) {
63
+ const attempt = {
64
+ id: promise.id,
65
+ expNum,
66
+ category: promise.category || promise.type,
67
+ attempted: false,
68
+ reason: null,
69
+ action: null,
70
+ evidence: null,
71
+ signals: null,
72
+ cause: null, // NEW: precise cause taxonomy
73
+ };
74
+
75
+ if (!this.isWithinBudget()) {
76
+ attempt.reason = 'global-timeout-exceeded';
77
+ attempt.cause = 'timeout';
78
+ this.attempts.push(attempt);
79
+ return attempt;
80
+ }
81
+
82
+ const _startTime = Date.now();
83
+ const bundle = createEvidenceBundle(promise, expNum, this.evidencePath);
84
+ bundle.timing.startedAt = new Date().toISOString();
85
+ bundle.actionStartTime = new Date().toISOString(); // For network correlation
86
+
87
+ try {
88
+ // Capture before state
89
+ await bundle.captureBeforeState(this.page);
90
+ const urlBefore = this.page.url();
91
+ const consoleErrorsCountBefore = this.consoleEvents.length;
92
+
93
+ // Execute interaction based on type
94
+ let actionResult = null;
95
+
96
+ if (promise.category === 'button' || promise.type === 'navigation') {
97
+ actionResult = await this.executeButtonClick(promise, bundle);
98
+ attempt.action = 'click';
99
+ } else if (promise.category === 'form') {
100
+ actionResult = await this.executeFormSubmission(promise, bundle);
101
+ attempt.action = 'submit';
102
+ } else if (promise.category === 'validation') {
103
+ actionResult = await this.executeValidation(promise, bundle);
104
+ attempt.action = 'observe';
105
+ } else if (promise.type === 'state') {
106
+ actionResult = await this.observeStateChange(promise, bundle);
107
+ attempt.action = 'observe';
108
+ } else if (promise.type === 'network') {
109
+ actionResult = await this.observeNetworkRequest(promise, bundle);
110
+ attempt.action = 'observe';
111
+ } else {
112
+ attempt.reason = 'unsupported-promise-type';
113
+ attempt.cause = 'blocked'; // Mark as blocked due to unsupported type
114
+ }
115
+
116
+ // Handle action result - could be boolean or object with details
117
+ let actionSuccess = false;
118
+ let actionReason = null;
119
+
120
+ if (typeof actionResult === 'object' && actionResult !== null) {
121
+ actionSuccess = actionResult.success;
122
+ actionReason = actionResult.reason;
123
+ if (actionResult.cause) {
124
+ attempt.cause = actionResult.cause;
125
+ }
126
+ } else {
127
+ actionSuccess = Boolean(actionResult);
128
+ }
129
+
130
+ // Wait for effects
131
+ await this.page.waitForTimeout(500);
132
+
133
+ // Capture after state
134
+ await bundle.captureAfterState(this.page);
135
+ const urlAfter = this.page.url();
136
+
137
+ // Analyze changes
138
+ bundle.analyzeChanges(urlBefore, urlAfter);
139
+ bundle.correlateNetworkRequests(); // NEW: correlate network to action
140
+
141
+ // Collect new console errors
142
+ const newConsoleErrors = this.consoleEvents.slice(consoleErrorsCountBefore);
143
+ newConsoleErrors.forEach(err => bundle.recordConsoleError(err));
144
+
145
+ // Finalize bundle
146
+ bundle.finalize(this.page);
147
+
148
+ attempt.attempted = true;
149
+ attempt.evidence = bundle.getSummary();
150
+ attempt.signals = bundle.signals;
151
+
152
+ // Determine outcome based on strict binding rules
153
+ if (!attempt.cause) {
154
+ // Classify outcome based on expected behavior
155
+ attempt.cause = classifyOutcome(promise, bundle, actionSuccess, actionReason);
156
+ }
157
+
158
+ // If action succeeded and outcome matched, reason is null (observed)
159
+ if (actionSuccess && outcomeMatched(promise.expectedOutcome, bundle.signals)) {
160
+ attempt.reason = null;
161
+ } else if (!actionSuccess) {
162
+ // Action failed - use cause to explain why
163
+ attempt.reason = attempt.cause;
164
+ } else if (!outcomeMatched(promise.expectedOutcome, bundle.signals)) {
165
+ // Action succeeded but expected outcome not observed
166
+ attempt.reason = attempt.cause || 'outcome-not-met';
167
+ }
168
+
169
+ } catch (error) {
170
+ attempt.reason = `error:${error.message}`;
171
+ attempt.cause = 'error';
172
+ attempt.attempted = true;
173
+
174
+ // Capture after even on error
175
+ try {
176
+ await bundle.captureAfterState(this.page);
177
+ bundle.finalize(this.page);
178
+ attempt.evidence = bundle.getSummary();
179
+ } catch (e) {
180
+ // Give up
181
+ }
182
+ }
183
+
184
+ this.attempts.push(attempt);
185
+ return attempt;
186
+ }
187
+
188
+ /**
189
+ * Execute button click
190
+ * Returns: { success: boolean, reason?: string, cause?: string }
191
+ */
192
+ async executeButtonClick(promise, bundle) {
193
+ // Resolve selector
194
+ const resolution = await resolveSelector(this.page, promise);
195
+
196
+ if (!resolution.found) {
197
+ bundle.timing.endedAt = new Date().toISOString();
198
+ return { success: false, reason: 'selector-not-found', cause: 'not-found' };
199
+ }
200
+
201
+ try {
202
+ // Check if interactable
203
+ const interactable = await isElementInteractable(this.page, resolution.selector);
204
+ if (!interactable) {
205
+ bundle.timing.endedAt = new Date().toISOString();
206
+ return { success: false, reason: 'element-not-interactable', cause: 'blocked' };
207
+ }
208
+
209
+ // Perform click
210
+ await this.page.locator(resolution.selector).click({ timeout: 3000 });
211
+
212
+ // Wait for expected outcome
213
+ const outcomeReached = await this.waitForOutcome(promise.expectedOutcome || 'ui-change');
214
+
215
+ if (!outcomeReached) {
216
+ return { success: false, reason: 'outcome-timeout', cause: 'timeout' };
217
+ }
218
+
219
+ return { success: true };
220
+
221
+ } catch (error) {
222
+ return { success: false, reason: error.message, cause: 'error' };
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Execute form submission
228
+ * Returns: { success: boolean, reason?: string, cause?: string }
229
+ */
230
+ async executeFormSubmission(promise, _bundle) {
231
+ // Resolve form selector
232
+ const formResolution = await resolveSelector(this.page, promise);
233
+
234
+ if (!formResolution.found) {
235
+ return { success: false, reason: 'form-not-found', cause: 'not-found' };
236
+ }
237
+
238
+ try {
239
+ // Fill in required inputs with sample data
240
+ const requiredInputs = await this.page.locator(`${formResolution.selector} input[required]`).all();
241
+
242
+ for (const input of requiredInputs) {
243
+ const type = await input.getAttribute('type');
244
+ let value = 'test-data';
245
+
246
+ if (type === 'email') {
247
+ value = 'test@example.com';
248
+ } else if (type === 'number') {
249
+ value = '123';
250
+ } else if (type === 'date') {
251
+ value = '2025-01-14';
252
+ } else if (type === 'checkbox') {
253
+ const isChecked = await input.isChecked();
254
+ if (!isChecked) {
255
+ await input.check();
256
+ }
257
+ continue;
258
+ }
259
+
260
+ try {
261
+ await input.fill(value);
262
+ } catch (e) {
263
+ // Skip if can't fill
264
+ }
265
+ }
266
+
267
+ // Find and click submit button
268
+ const submitBtn = await findSubmitButton(this.page, formResolution.selector);
269
+
270
+ let _submitted = false;
271
+ if (submitBtn) {
272
+ try {
273
+ await submitBtn.click({ timeout: 3000 });
274
+ _submitted = true;
275
+ } catch (e) {
276
+ return { success: false, reason: 'submit-button-click-failed', cause: 'blocked' };
277
+ }
278
+ } else {
279
+ // Try submitting with Enter on first input
280
+ const firstInput = await this.page.locator(`${formResolution.selector} input`).first();
281
+ if (await firstInput.count() > 0) {
282
+ try {
283
+ await firstInput.press('Enter');
284
+ _submitted = true;
285
+ } catch (e) {
286
+ return { success: false, reason: 'form-submit-failed', cause: 'blocked' };
287
+ }
288
+ } else {
289
+ return { success: false, reason: 'no-submit-mechanism', cause: 'blocked' };
290
+ }
291
+ }
292
+
293
+ // Wait for expected outcome
294
+ const outcomeReached = await this.waitForOutcome(promise.expectedOutcome || 'ui-change');
295
+
296
+ if (!outcomeReached) {
297
+ return { success: false, reason: 'form-submit-prevented', cause: 'prevented-submit' };
298
+ }
299
+
300
+ return { success: true };
301
+
302
+ } catch (error) {
303
+ return { success: false, reason: error.message, cause: 'error' };
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Execute validation check
309
+ */
310
+ async executeValidation(promise, _bundle) {
311
+ // Find required input
312
+ const resolution = await resolveSelector(this.page, promise);
313
+
314
+ if (!resolution.found) {
315
+ return false;
316
+ }
317
+
318
+ try {
319
+ // Try to submit form without filling in this required field
320
+ // First, find the containing form
321
+ const form = await this.page.locator(`input[required]`).first().evaluate(el => {
322
+ let current = el;
323
+ while (current && current.tagName !== 'FORM') {
324
+ current = current.parentElement;
325
+ }
326
+ return current ? current.getAttribute('id') || current.className : null;
327
+ }).catch(() => null);
328
+
329
+ if (form) {
330
+ // Try to submit the form
331
+ const submitBtn = await findSubmitButton(this.page, `form`);
332
+ if (submitBtn) {
333
+ await submitBtn.click({ timeout: 2000 }).catch(() => {});
334
+ }
335
+ }
336
+
337
+ // Wait briefly for validation feedback
338
+ await this.page.waitForTimeout(1000);
339
+
340
+ // Check if validation feedback appeared
341
+ const hasValidationUI = await this.page.locator('[aria-invalid="true"], [role="alert"], .error, .validation-error').count() > 0;
342
+
343
+ return hasValidationUI;
344
+
345
+ } catch (error) {
346
+ return false;
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Observe state changes
352
+ */
353
+ async observeStateChange(_promise, _bundle) {
354
+ const htmlBefore = await this.page.content();
355
+
356
+ await this.page.waitForTimeout(1500);
357
+
358
+ const htmlAfter = await this.page.content();
359
+
360
+ return htmlBefore !== htmlAfter;
361
+ }
362
+
363
+ /**
364
+ * Observe network request
365
+ */
366
+ async observeNetworkRequest(_promise, _bundle) {
367
+ const targetUrl = _promise.promise.value;
368
+ const networkCountBefore = this.networkEvents.length;
369
+
370
+ await this.page.waitForTimeout(2000);
371
+
372
+ const networkCountAfter = this.networkEvents.length;
373
+
374
+ if (networkCountAfter > networkCountBefore) {
375
+ // Check if any of the new events match the target
376
+ const newEvents = this.networkEvents.slice(networkCountBefore);
377
+ return newEvents.some(event => {
378
+ return event.url === targetUrl ||
379
+ event.url.includes(targetUrl) ||
380
+ targetUrl.includes(event.url);
381
+ });
382
+ }
383
+
384
+ return false;
385
+ }
386
+
387
+ /**
388
+ * Wait for expected outcome with timeout
389
+ */
390
+ async waitForOutcome(expectedOutcome) {
391
+ const _startTime = Date.now();
392
+
393
+ try {
394
+ if (expectedOutcome === 'navigation') {
395
+ // Wait for navigation or DOM change
396
+ await Promise.race([
397
+ this.page.waitForNavigation({ timeout: WAIT_FOR_EFFECT_TIMEOUT }).catch(() => {}),
398
+ this.page.waitForLoadState('domcontentloaded', { timeout: WAIT_FOR_EFFECT_TIMEOUT }).catch(() => {}),
399
+ this.page.waitForTimeout(WAIT_FOR_EFFECT_TIMEOUT),
400
+ ]);
401
+ return true;
402
+ }
403
+
404
+ if (expectedOutcome === 'network') {
405
+ // Network request should be detected via networkEvents
406
+ await this.page.waitForTimeout(WAIT_FOR_EFFECT_TIMEOUT);
407
+ return true;
408
+ }
409
+
410
+ if (expectedOutcome === 'feedback') {
411
+ // Wait for feedback element to appear
412
+ const found = await Promise.race([
413
+ this.page.waitForSelector('[aria-live], [role="alert"], [role="status"], .toast',
414
+ { timeout: WAIT_FOR_EFFECT_TIMEOUT }).catch(() => null),
415
+ new Promise(resolve => {
416
+ setTimeout(() => resolve(false), WAIT_FOR_EFFECT_TIMEOUT);
417
+ }),
418
+ ]);
419
+ return !!found;
420
+ }
421
+
422
+ // Default: ui-change
423
+ await this.page.waitForTimeout(WAIT_FOR_EFFECT_TIMEOUT);
424
+ return true;
425
+
426
+ } catch (e) {
427
+ // Timeout or error - still counts as attempted
428
+ return false;
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Record network event
434
+ */
435
+ recordNetworkEvent(event) {
436
+ this.networkEvents.push(event);
437
+ }
438
+
439
+ /**
440
+ * Record console event
441
+ */
442
+ recordConsoleEvent(event) {
443
+ this.consoleEvents.push(event);
444
+ }
445
+
446
+ /**
447
+ * Get all attempts
448
+ */
449
+ getAttempts() {
450
+ return this.attempts;
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Classify the outcome based on evidence and expected outcome
456
+ * Returns cause taxonomy: not-found | blocked | prevented-submit | timeout | no-change
457
+ */
458
+ function classifyOutcome(promise, bundle, actionSuccess, actionReason) {
459
+ // If action itself failed
460
+ if (!actionSuccess && actionReason) {
461
+ const lower = actionReason.toLowerCase();
462
+ if (lower.includes('not-found') || lower.includes('not found')) {
463
+ return 'not-found';
464
+ }
465
+ if (lower.includes('interactable') || lower.includes('blocked')) {
466
+ return 'blocked';
467
+ }
468
+ if (lower.includes('prevented') || lower.includes('prevented-submit')) {
469
+ return 'prevented-submit';
470
+ }
471
+ if (lower.includes('timeout') || lower.includes('timed out')) {
472
+ return 'timeout';
473
+ }
474
+ }
475
+
476
+ // Action succeeded but outcome not met
477
+ const expected = promise.expectedOutcome || 'ui-change';
478
+ const matched = outcomeMatched(expected, bundle.signals);
479
+
480
+ if (!matched) {
481
+ // Check what didn't happen
482
+ if (expected === 'navigation' && !bundle.signals.navigationChanged) {
483
+ // Navigation was supposed to happen but didn't
484
+ return 'no-change';
485
+ }
486
+ if (expected === 'feedback' && !bundle.signals.feedbackSeen) {
487
+ // Feedback was supposed to appear but didn't
488
+ return 'no-change';
489
+ }
490
+ if (expected === 'network' && !bundle.signals.correlatedNetworkActivity) {
491
+ // Network request was supposed to happen but didn't
492
+ return 'no-change';
493
+ }
494
+ if (expected === 'ui-change' && !bundle.signals.meaningfulDomChange) {
495
+ // UI change was supposed to happen but didn't
496
+ return 'no-change';
497
+ }
498
+ }
499
+
500
+ // Default
501
+ return 'no-change';
502
+ }
503
+
504
+ /**
505
+ * Check if the expected outcome has been satisfied by the signals
506
+ */
507
+ function outcomeMatched(expectedOutcome, signals) {
508
+ if (!expectedOutcome || expectedOutcome === 'ui-change') {
509
+ // Any meaningful signal counts
510
+ return signals.navigationChanged ||
511
+ signals.meaningfulDomChange ||
512
+ signals.feedbackSeen ||
513
+ signals.correlatedNetworkActivity;
514
+ }
515
+
516
+ if (expectedOutcome === 'navigation') {
517
+ return signals.navigationChanged;
518
+ }
519
+
520
+ if (expectedOutcome === 'feedback') {
521
+ return signals.feedbackSeen;
522
+ }
523
+
524
+ if (expectedOutcome === 'network') {
525
+ return signals.correlatedNetworkActivity || signals.networkActivity;
526
+ }
527
+
528
+ return false;
529
+ }
@@ -1,6 +1,7 @@
1
1
  import { resolve } from 'path';
2
2
  import { atomicWriteJson } from './atomic-write.js';
3
3
  import { compareExpectations } from './idgen.js';
4
+ import { ARTIFACT_REGISTRY } from '../../verax/core/artifacts/registry.js';
4
5
 
5
6
  /**
6
7
  * Write learn.json artifact
@@ -13,6 +14,7 @@ export function writeLearnJson(runPaths, expectations, skipped) {
13
14
  const sortedExpectations = [...expectations].sort(compareExpectations);
14
15
 
15
16
  const learnJson = {
17
+ contractVersion: ARTIFACT_REGISTRY.learn.contractVersion,
16
18
  expectations: sortedExpectations,
17
19
  stats: {
18
20
  totalExpectations: sortedExpectations.length,
@@ -0,0 +1,110 @@
1
+ /**
2
+ * PHASE 5: Failure Ledger Writer
3
+ *
4
+ * Ensures durable failure evidence is ALWAYS written when analysis fails/is incomplete.
5
+ * This is the safety net - if we can't write primary artifacts, at least write this.
6
+ */
7
+
8
+ import { atomicWriteJson } from './atomic-write.js';
9
+ import { readFileSync } from 'fs';
10
+ import { resolve, dirname } from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+
16
+ function getVersion() {
17
+ try {
18
+ const pkgPath = resolve(__dirname, '../../../package.json');
19
+ // @ts-expect-error - readFileSync with encoding returns string
20
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
21
+ return pkg.version;
22
+ } catch {
23
+ return '0.3.0';
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Write failure ledger to run directory
29
+ *
30
+ * @param {string} runDir - Run directory path
31
+ * @param {object} failureInfo - Failure details
32
+ * @param {string} failureInfo.runId - Run ID
33
+ * @param {string} failureInfo.url - Target URL
34
+ * @param {string} failureInfo.src - Source directory
35
+ * @param {string} failureInfo.state - ANALYSIS_FAILED or ANALYSIS_INCOMPLETE
36
+ * @param {number} failureInfo.exitCode - Exit code (2 for FAILED, 66 for INCOMPLETE)
37
+ * @param {Error} failureInfo.primaryError - The error that caused failure
38
+ * @param {string} failureInfo.phase - Phase where failure occurred
39
+ * @param {string[]} failureInfo.notes - Additional context notes
40
+ * @returns {{ ok: boolean, path?: string, error?: Error }}
41
+ */
42
+ export function writeLedger(runDir, failureInfo) {
43
+ const {
44
+ runId,
45
+ url,
46
+ src,
47
+ state,
48
+ exitCode,
49
+ primaryError,
50
+ phase = 'unknown',
51
+ notes = [],
52
+ } = failureInfo;
53
+
54
+ const ledgerPath = `${runDir}/ledger.json`;
55
+
56
+ const ledger = {
57
+ meta: {
58
+ runId,
59
+ url,
60
+ src,
61
+ timestamp: new Date().toISOString(),
62
+ version: getVersion(),
63
+ },
64
+ state, // "ANALYSIS_FAILED" | "ANALYSIS_INCOMPLETE"
65
+ exitCode,
66
+ primaryError: {
67
+ name: primaryError?.name || 'Error',
68
+ message: primaryError?.message || 'Unknown error',
69
+ stack: primaryError?.stack || null,
70
+ },
71
+ phase,
72
+ notes,
73
+ };
74
+
75
+ try {
76
+ atomicWriteJson(ledgerPath, ledger);
77
+ return { ok: true, path: ledgerPath };
78
+ } catch (writeError) {
79
+ // Ledger write failed - this is EXTREMELY BAD
80
+ // Print loud warning but don't throw - we're already in error handling
81
+ console.error('═══════════════════════════════════════════════════════');
82
+ console.error('🚨 CRITICAL: FAILURE LEDGER COULD NOT BE WRITTEN');
83
+ console.error('═══════════════════════════════════════════════════════');
84
+ console.error('Run directory:', runDir);
85
+ console.error('Ledger path:', ledgerPath);
86
+ console.error('Write error:', writeError?.message || 'Unknown');
87
+ console.error('═══════════════════════════════════════════════════════');
88
+
89
+ return { ok: false, error: writeError };
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Create integrity stamp for artifacts
95
+ *
96
+ * @param {string} status - "COMPLETE" | "PARTIAL" | "FAILED_WRITE"
97
+ * @param {boolean} atomic - Whether atomic write was used
98
+ * @param {string[]} notes - Additional notes
99
+ * @returns {object} Integrity object
100
+ */
101
+ export function createIntegrityStamp(status, atomic = true, notes = []) {
102
+ return {
103
+ status,
104
+ atomic,
105
+ writer: 'verax',
106
+ writerVersion: getVersion(),
107
+ writtenAt: new Date().toISOString(),
108
+ notes,
109
+ };
110
+ }