@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,293 @@
1
+ /**
2
+ * Failure Cause Inference (FCI) - Evidence-backed cause attribution
3
+ *
4
+ * Pure, deterministic module that infers likely causes from confirmed findings.
5
+ * Each cause has explicit evidence conditions and is never guessed.
6
+ *
7
+ * Rules:
8
+ * 1) No evidence -> no cause
9
+ * 2) Causes phrased as "Likely cause:" statements
10
+ * 3) Internal errors never reported as user bugs
11
+ * 4) Deterministic output: same input => same causes, ordering, wording
12
+ * 5) Confidence: LOW|MEDIUM only (never HIGH)
13
+ */
14
+
15
+ /**
16
+ * Catalog order: causes are sorted by this order first, then by id as tie-breaker.
17
+ * This ensures stable, predictable cause ordering across all scenarios.
18
+ */
19
+ const CATALOG_ORDER = ['C1_SELECTOR_MISMATCH', 'C2_STATE_MUTATION_NO_UI', 'C3_DEAD_CLICK', 'C4_NAVIGATION_NO_RENDER', 'C5_FORM_NO_FEEDBACK', 'C6_VALIDATION_NOT_SHOWN', 'C7_NETWORK_SILENT'];
20
+
21
+ /**
22
+ * Catalog of detectable causes with evidence conditions
23
+ * Ordered by catalog position, then id for deterministic processing
24
+ */
25
+ const CAUSE_CATALOG = [
26
+ {
27
+ id: 'C1_SELECTOR_MISMATCH',
28
+ title: 'Element not found or selector mismatch',
29
+ condition: (finding) => {
30
+ const ev = finding.evidence || {};
31
+ return (
32
+ (ev.targetElementMissing === true) ||
33
+ (ev.staleHandle === true) ||
34
+ (ev.clickAttempted === true && ev.targetElement === false) ||
35
+ (ev.locatorResolution === 0) ||
36
+ (ev.domSnapshotMissing &&
37
+ (ev.domSnapshotMissing.includes('id') || ev.domSnapshotMissing.includes('class') || ev.domSnapshotMissing.includes('text')))
38
+ );
39
+ },
40
+ statement: () =>
41
+ 'Likely cause: The UI element being interacted with could not be found or was stale at interaction time.',
42
+ evidence_refs: (finding) => {
43
+ const refs = [];
44
+ if (finding.evidence?.targetElementMissing === true) refs.push('evidence.targetElementMissing=true');
45
+ if (finding.evidence?.staleHandle === true) refs.push('evidence.staleHandle=true');
46
+ if (finding.evidence?.locatorResolution === 0) refs.push('evidence.locatorResolution=0');
47
+ if (finding.evidence?.domSnapshotMissing) refs.push('evidence.domSnapshotMissing');
48
+ return refs;
49
+ },
50
+ confidenceScore: (finding) => {
51
+ // MEDIUM if multiple signals, LOW otherwise
52
+ const count = [
53
+ finding.evidence?.targetElementMissing,
54
+ finding.evidence?.staleHandle,
55
+ finding.evidence?.locatorResolution === 0
56
+ ].filter(Boolean).length;
57
+ return count >= 2 ? 'MEDIUM' : 'LOW';
58
+ }
59
+ },
60
+
61
+ {
62
+ id: 'C2_STATE_MUTATION_NO_UI',
63
+ title: 'State changed but UI did not update',
64
+ condition: (finding) => {
65
+ const ev = finding.evidence || {};
66
+ return (
67
+ ev.stateMutation === true &&
68
+ ev.domChanged === false &&
69
+ ev.navigationOccurred !== true &&
70
+ ev.uiFeedback === false
71
+ );
72
+ },
73
+ statement: () =>
74
+ 'Likely cause: Application state changed internally, but the UI did not re-render or reflect the change.',
75
+ evidence_refs: (_finding) =>
76
+ ['evidence.stateMutation=true', 'evidence.domChanged=false', 'evidence.uiFeedback=false'],
77
+ confidenceScore: () => 'MEDIUM'
78
+ },
79
+
80
+ {
81
+ id: 'C3_DEAD_CLICK',
82
+ title: 'Interaction ran but produced no observable outcome',
83
+ condition: (finding) => {
84
+ const ev = finding.evidence || {};
85
+ return (
86
+ ev.interactionPerformed === true &&
87
+ ev.networkActivity === false &&
88
+ ev.navigationOccurred !== true &&
89
+ ev.domChanged === false &&
90
+ ev.userFeedback === false
91
+ );
92
+ },
93
+ statement: () =>
94
+ 'Likely cause: The interaction ran but had no handler or the handler did nothing (dead/no-op click).',
95
+ evidence_refs: (_finding) =>
96
+ ['evidence.interactionPerformed=true', 'evidence.networkActivity=false', 'evidence.domChanged=false', 'evidence.userFeedback=false'],
97
+ confidenceScore: () => 'MEDIUM'
98
+ },
99
+
100
+ {
101
+ id: 'C4_NAVIGATION_NO_RENDER',
102
+ title: 'Navigation attempted but content did not load',
103
+ condition: (finding) => {
104
+ const ev = finding.evidence || {};
105
+ return (
106
+ (ev.navigationAttempted === true || ev.urlChangeAttempted === true || ev.linkClicked === true) &&
107
+ (ev.urlChanged === false || (ev.urlChanged === true && ev.contentStillLoading === true)) &&
108
+ (ev.mainContentChanged === false || ev.mainContentBlank === true)
109
+ );
110
+ },
111
+ statement: () =>
112
+ 'Likely cause: Navigation was triggered but the target route either did not change or did not render visible content.',
113
+ evidence_refs: (finding) => {
114
+ const refs = [];
115
+ if (finding.evidence?.navigationAttempted || finding.evidence?.urlChangeAttempted || finding.evidence?.linkClicked) {
116
+ refs.push('evidence.navigationAttempted|urlChangeAttempted|linkClicked=true');
117
+ }
118
+ if (finding.evidence?.urlChanged === false) refs.push('evidence.urlChanged=false');
119
+ if (finding.evidence?.mainContentChanged === false) refs.push('evidence.mainContentChanged=false');
120
+ return refs;
121
+ },
122
+ confidenceScore: () => 'MEDIUM'
123
+ },
124
+
125
+ {
126
+ id: 'C5_FORM_NO_FEEDBACK',
127
+ title: 'Form submitted but no success or error message shown',
128
+ condition: (finding) => {
129
+ const ev = finding.evidence || {};
130
+ return (
131
+ ev.submitInteraction === true &&
132
+ (ev.networkRequestOccurred === true || ev.submitEventDetected === true) &&
133
+ ev.successFeedback === false &&
134
+ ev.errorFeedback === false &&
135
+ ev.navigationAfterSubmit !== true
136
+ );
137
+ },
138
+ statement: () =>
139
+ 'Likely cause: Form submission was sent to the server, but the UI did not show a success or error message.',
140
+ evidence_refs: (_finding) =>
141
+ ['evidence.submitInteraction=true', 'evidence.successFeedback=false', 'evidence.errorFeedback=false'],
142
+ confidenceScore: () => 'MEDIUM'
143
+ },
144
+
145
+ {
146
+ id: 'C6_VALIDATION_NOT_SHOWN',
147
+ title: 'Validation expected but feedback not displayed',
148
+ condition: (finding) => {
149
+ const ev = finding.evidence || {};
150
+ return (
151
+ ev.formOrValidationPromise === true &&
152
+ ev.invalidSubmitAttempted === true &&
153
+ ev.inlineValidationFeedback === false
154
+ );
155
+ },
156
+ statement: () =>
157
+ 'Likely cause: Form field validation was expected to show inline feedback, but no error message appeared.',
158
+ evidence_refs: (_finding) =>
159
+ ['evidence.formOrValidationPromise=true', 'evidence.invalidSubmitAttempted=true', 'evidence.inlineValidationFeedback=false'],
160
+ confidenceScore: () => 'LOW'
161
+ },
162
+
163
+ {
164
+ id: 'C7_NETWORK_SILENT',
165
+ title: 'Network request failed silently without user feedback',
166
+ condition: (finding) => {
167
+ const ev = finding.evidence || {};
168
+ return (
169
+ (ev.networkFailure === true || ev.httpError === true || ev.fetchError === true) &&
170
+ ev.uiFeedback === false &&
171
+ ev.domChanged === false
172
+ );
173
+ },
174
+ statement: () =>
175
+ 'Likely cause: A network request failed (4xx/5xx or connection error), but the UI showed no error message.',
176
+ evidence_refs: (finding) => {
177
+ const refs = [];
178
+ if (finding.evidence?.networkFailure === true) refs.push('evidence.networkFailure=true');
179
+ if (finding.evidence?.httpError === true) refs.push('evidence.httpError=true');
180
+ if (finding.evidence?.fetchError === true) refs.push('evidence.fetchError=true');
181
+ refs.push('evidence.uiFeedback=false');
182
+ return refs;
183
+ },
184
+ confidenceScore: () => 'MEDIUM'
185
+ }
186
+ ];
187
+
188
+ /**
189
+ * Infer likely causes for a finding.
190
+ * Only returns causes where evidence conditions are met.
191
+ * Causes are ordered deterministically by catalog order.
192
+ *
193
+ * @param {Object} finding - Finding object with evidence property
194
+ * @returns {Array} Array of cause objects, sorted by id. Empty if no evidence.
195
+ */
196
+ export function inferCauses(finding) {
197
+ if (!finding) {
198
+ return [];
199
+ }
200
+
201
+ // Enforce Evidence Law: no evidence -> no causes
202
+ if (!finding.evidence || Object.keys(finding.evidence).length === 0) {
203
+ return [];
204
+ }
205
+
206
+ const detectedCauses = [];
207
+
208
+ for (const causeDef of CAUSE_CATALOG) {
209
+ if (causeDef.condition(finding)) {
210
+ const cause = {
211
+ id: causeDef.id,
212
+ title: causeDef.title,
213
+ statement: causeDef.statement(),
214
+ evidence_refs: causeDef.evidence_refs(finding),
215
+ confidence: causeDef.confidenceScore(finding)
216
+ };
217
+ detectedCauses.push(cause);
218
+ }
219
+ }
220
+
221
+ // Sort deterministically by catalog order, then by id
222
+ detectedCauses.sort((a, b) => {
223
+ const aPos = CATALOG_ORDER.indexOf(a.id);
224
+ const bPos = CATALOG_ORDER.indexOf(b.id);
225
+ if (aPos !== bPos) {
226
+ return aPos - bPos;
227
+ }
228
+ return a.id.localeCompare(b.id);
229
+ });
230
+
231
+ return detectedCauses;
232
+ }
233
+
234
+ /**
235
+ * Batch infer causes for all findings in a report.
236
+ * Pure function: same input => same causes in same order.
237
+ *
238
+ * @param {Array} findings - Array of finding objects
239
+ * @returns {Object} Map of finding.id -> causes array
240
+ */
241
+ export function inferCausesForFindings(findings) {
242
+ const causesMap = {};
243
+
244
+ if (!findings || !Array.isArray(findings)) {
245
+ return causesMap;
246
+ }
247
+
248
+ for (const finding of findings) {
249
+ const causes = inferCauses(finding);
250
+ if (causes.length > 0) {
251
+ causesMap[finding.id] = causes;
252
+ }
253
+ }
254
+
255
+ return causesMap;
256
+ }
257
+
258
+ /**
259
+ * Attach causes to a finding object (pure function).
260
+ * Returns a new finding object with causes attached.
261
+ * Original finding is not mutated.
262
+ *
263
+ * @param {Object} finding - Finding to augment
264
+ * @returns {Object} New finding object with causes array added
265
+ */
266
+ export function attachCausesToFinding(finding) {
267
+ if (!finding) {
268
+ return finding;
269
+ }
270
+ const causes = inferCauses(finding);
271
+ return {
272
+ ...finding,
273
+ causes
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Filter findings to only those with causes.
279
+ * Useful for reporting only findings with explanation.
280
+ *
281
+ * @param {Array} findings - Array of findings
282
+ * @returns {Array} Findings with non-empty causes array
283
+ */
284
+ export function findingsWithCauses(findings) {
285
+ if (!findings || !Array.isArray(findings)) {
286
+ return [];
287
+ }
288
+
289
+ return findings.filter(f => {
290
+ const causes = inferCauses(f);
291
+ return causes.length > 0;
292
+ });
293
+ }
@@ -1,60 +1,156 @@
1
1
  import { resolve } from 'path';
2
2
  import { mkdirSync, writeFileSync } from 'fs';
3
3
  import { CANONICAL_OUTCOMES } from '../core/canonical-outcomes.js';
4
+ import { ARTIFACT_REGISTRY } from '../core/artifacts/registry.js';
4
5
 
5
- /**
6
- * Write findings to canonical artifact root.
7
- * Writes to .verax/runs/<runId>/findings.json.
8
- *
9
- * PHASE 2: Includes outcome classification summary.
10
- * PHASE 3: Includes promise type summary.
11
- *
12
- * @param {string} projectDir
13
- * @param {string} url
14
- * @param {Array} findings
15
- * @param {Array} coverageGaps
16
- * @param {string} runDirOpt - Required absolute run directory path
17
- */
18
- export function writeFindings(projectDir, url, findings, coverageGaps = [], runDirOpt) {
19
- if (!runDirOpt) {
20
- throw new Error('runDirOpt is required');
21
- }
22
- mkdirSync(runDirOpt, { recursive: true });
23
- const findingsPath = resolve(runDirOpt, 'findings.json');
6
+ const DEFAULT_CLOCK = () => new Date().toISOString();
24
7
 
25
- // PHASE 2: Compute outcome summary
8
+ // Pure: builds deterministic report object from provided data and timestamp
9
+ export function buildFindingsReport({ url, findings = [], coverageGaps = [], detectedAt }) {
26
10
  const outcomeSummary = {};
27
11
  Object.values(CANONICAL_OUTCOMES).forEach(outcome => {
28
12
  outcomeSummary[outcome] = 0;
29
13
  });
30
-
31
- // PHASE 3: Compute promise summary
14
+
32
15
  const promiseSummary = {};
33
-
34
- for (const finding of (findings || [])) {
16
+
17
+ // Contract enforcement: separate findings into valid, downgradable, and droppable
18
+ const downgrades = [];
19
+ const droppedFindingIds = [];
20
+ const enforcedFindings = [];
21
+
22
+ for (const finding of findings) {
23
+ // Check for critical missing fields (drop these)
24
+ const hasCriticalNarrativeFields =
25
+ finding.what_happened &&
26
+ finding.what_was_expected &&
27
+ finding.what_was_observed &&
28
+ finding.why_it_matters;
29
+
30
+ const hasRequiredType = finding.type;
31
+
32
+ if (!hasRequiredType || !hasCriticalNarrativeFields) {
33
+ droppedFindingIds.push(finding.id || 'unknown');
34
+ continue;
35
+ }
36
+
37
+ // Check for Evidence Law violation (downgrade)
38
+ if (finding.status === 'CONFIRMED' && (!finding.evidence || Object.keys(finding.evidence).length === 0)) {
39
+ const downgradedFinding = {
40
+ ...finding,
41
+ status: 'SUSPECTED',
42
+ reason: (finding.reason || '') + ' (Evidence Law enforced - no evidence exists for CONFIRMED status)'
43
+ };
44
+ downgrades.push({
45
+ id: finding.id || 'unknown',
46
+ originalStatus: 'CONFIRMED',
47
+ downgradeToStatus: 'SUSPECTED',
48
+ reason: 'Evidence Law enforced - no evidence exists for CONFIRMED status'
49
+ });
50
+ enforcedFindings.push(downgradedFinding);
51
+ } else {
52
+ // Valid finding
53
+ enforcedFindings.push(finding);
54
+ }
55
+ }
56
+
57
+ // Build outcome and promise summary from enforced findings
58
+ for (const finding of enforcedFindings) {
35
59
  const outcome = finding.outcome || CANONICAL_OUTCOMES.SILENT_FAILURE;
36
60
  outcomeSummary[outcome] = (outcomeSummary[outcome] || 0) + 1;
37
-
61
+
38
62
  const promiseType = finding.promise?.type || 'UNKNOWN_PROMISE';
39
63
  promiseSummary[promiseType] = (promiseSummary[promiseType] || 0) + 1;
40
64
  }
41
65
 
42
- const findingsReport = {
66
+ // Sort findings deterministically by id
67
+ const sortedFindings = enforcedFindings.sort((a, b) => (a.id || '').localeCompare(b.id || ''));
68
+
69
+ // Production report: exclude enforcement metadata (Trust Lock)
70
+ return {
43
71
  version: 1,
44
- detectedAt: new Date().toISOString(),
45
- url: url,
46
- outcomeSummary: outcomeSummary, // PHASE 2
47
- promiseSummary: promiseSummary, // PHASE 3
48
- findings: findings,
49
- coverageGaps: coverageGaps,
72
+ contractVersion: ARTIFACT_REGISTRY.findings.contractVersion,
73
+ detectedAt: detectedAt,
74
+ url,
75
+ outcomeSummary, // PHASE 2
76
+ promiseSummary, // PHASE 3
77
+ findings: sortedFindings,
78
+ coverageGaps,
50
79
  notes: []
51
80
  };
81
+ }
82
+
83
+ // Internal helper: builds report with enforcement metadata for disk persistence
84
+ function buildFindingsReportWithEnforcement({ url, findings = [], coverageGaps = [], detectedAt }) {
85
+ const report = buildFindingsReport({ url, findings, coverageGaps, detectedAt });
86
+
87
+ const downgrades = [];
88
+ const droppedFindingIds = [];
52
89
 
53
- writeFileSync(findingsPath, JSON.stringify(findingsReport, null, 2) + '\n');
90
+ for (const finding of findings) {
91
+ const hasCriticalNarrativeFields =
92
+ finding.what_happened &&
93
+ finding.what_was_expected &&
94
+ finding.what_was_observed &&
95
+ finding.why_it_matters;
96
+
97
+ const hasRequiredType = finding.type;
98
+
99
+ if (!hasRequiredType || !hasCriticalNarrativeFields) {
100
+ droppedFindingIds.push(finding.id || 'unknown');
101
+ continue;
102
+ }
103
+
104
+ if (finding.status === 'CONFIRMED' && (!finding.evidence || Object.keys(finding.evidence).length === 0)) {
105
+ downgrades.push({
106
+ id: finding.id || 'unknown',
107
+ originalStatus: 'CONFIRMED',
108
+ downgradeToStatus: 'SUSPECTED',
109
+ reason: 'Evidence Law enforced - no evidence exists for CONFIRMED status'
110
+ });
111
+ }
112
+ }
54
113
 
55
114
  return {
56
- ...findingsReport,
57
- findingsPath: findingsPath
115
+ ...report,
116
+ enforcement: {
117
+ evidenceLawEnforced: true,
118
+ contractVersion: 1,
119
+ timestamp: detectedAt,
120
+ droppedCount: droppedFindingIds.length,
121
+ downgradedCount: downgrades.length,
122
+ downgrades: downgrades
123
+ }
58
124
  };
59
125
  }
60
126
 
127
+ // Side-effectful: persists a fully built report to disk
128
+ export function persistFindingsReport(runDir, report) {
129
+ if (!runDir) {
130
+ throw new Error('runDirOpt is required');
131
+ }
132
+ mkdirSync(runDir, { recursive: true });
133
+ const findingsPath = resolve(runDir, 'findings.json');
134
+ writeFileSync(findingsPath, JSON.stringify(report, null, 2) + '\n');
135
+ return { ...report, findingsPath };
136
+ }
137
+
138
+ /**
139
+ * Write findings to canonical artifact root.
140
+ * Writes to .verax/runs/<runId>/findings.json.
141
+ *
142
+ * PHASE 2: Includes outcome classification summary.
143
+ * PHASE 3: Includes promise type summary.
144
+ *
145
+ * @param {string} projectDir
146
+ * @param {string} url
147
+ * @param {Array} findings
148
+ * @param {Array} coverageGaps
149
+ * @param {string} runDirOpt - Required absolute run directory path
150
+ */
151
+ export function writeFindings(projectDir, url, findings, coverageGaps = [], runDirOpt) {
152
+ const detectedAt = DEFAULT_CLOCK();
153
+ const report = buildFindingsReportWithEnforcement({ url, findings: findings || [], coverageGaps: coverageGaps || [], detectedAt });
154
+ return persistFindingsReport(runDirOpt, report);
155
+ }
156
+
@@ -153,7 +153,7 @@ export function detectFlowSilentFailures(traces, manifest, findings, coverageGap
153
153
  // Check navigation expectation
154
154
  if (expectsNavigation(manifest, interaction, beforeUrl)) {
155
155
  const navExp = manifest.staticExpectations?.find(e =>
156
- e.type === 'navigation' &&
156
+ (e.type === 'navigation' || e.type === 'spa_navigation') &&
157
157
  isProvenExpectation(e) &&
158
158
  e.fromPath && getUrlPath(beforeUrl) &&
159
159
  e.fromPath.replace(/\/$/, '') === getUrlPath(beforeUrl).replace(/\/$/, '') &&
@@ -223,7 +223,7 @@ export function detectFlowSilentFailures(traces, manifest, findings, coverageGap
223
223
  }
224
224
 
225
225
  // Also check network expectation even if navigation was matched (for step 2 of flow)
226
- if (matchedExpectation && matchedExpectation.type === 'navigation' && manifest.staticExpectations) {
226
+ if (matchedExpectation && (matchedExpectation.type === 'navigation' || matchedExpectation.type === 'spa_navigation') && manifest.staticExpectations) {
227
227
  const networkExp = manifest.staticExpectations.find(e =>
228
228
  e.type === 'network_action' &&
229
229
  isProvenExpectation(e) &&
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Form Silent Failure Detection
3
+ *
4
+ * Detects form submissions where:
5
+ * - Form submit handler executes
6
+ * - Network request completes with 2xx status
7
+ * - AND no success UI feedback appears (no toast, modal, or DOM change)
8
+ * - AND no navigation occurs
9
+ *
10
+ * CONFIDENCE: HIGH (network + UI evidence)
11
+ * Note: Does NOT attempt to parse response content (unsupported)
12
+ */
13
+
14
+ import { hasMeaningfulUrlChange, hasDomChange } from './comparison.js';
15
+ import { enrichFindingWithExplanations } from './finding-detector.js';
16
+
17
+ export function detectFormSilentFailures(traces, manifest, findings) {
18
+ // Parameters:
19
+ // traces - array of interaction traces from observation
20
+ // manifest - project manifest (contains expectations)
21
+ // findings - array to append new findings to (mutated in-place)
22
+
23
+ for (const trace of traces) {
24
+ const interaction = trace.interaction || {};
25
+
26
+ // Only analyze form interactions
27
+ if (interaction.type !== 'form' && interaction.category !== 'form') {
28
+ continue;
29
+ }
30
+
31
+ const beforeUrl = trace.before?.url || trace.beforeUrl || '';
32
+ const afterUrl = trace.after?.url || trace.afterUrl || '';
33
+ const sensors = trace.sensors || {};
34
+ const network = sensors.network || {};
35
+ const uiSignals = sensors.uiSignals || {};
36
+ const uiDiff = uiSignals.diff || {};
37
+
38
+ const urlChanged = hasMeaningfulUrlChange(beforeUrl, afterUrl);
39
+ const domChanged = hasDomChange(trace);
40
+ const uiChanged = uiDiff.changed === true;
41
+
42
+ // Check for successful network requests (2xx)
43
+ const successNetworkRequest = (network.requests || []).some(req => {
44
+ const status = req.status || req.statusCode || 0;
45
+ return status >= 200 && status < 300;
46
+ });
47
+
48
+ // Detection logic:
49
+ // Form submitted with successful network response but:
50
+ // 1. No navigation (URL unchanged)
51
+ // 2. No DOM change (no new content loaded)
52
+ // 3. No UI feedback (no toast, modal, highlight, etc.)
53
+ if (successNetworkRequest && !urlChanged && !domChanged && !uiChanged) {
54
+ const evidence = {
55
+ before: trace.before?.screenshot || trace.beforeScreenshot || '',
56
+ after: trace.after?.screenshot || trace.afterScreenshot || '',
57
+ beforeUrl,
58
+ afterUrl,
59
+ networkRequests: network.requests || [],
60
+ successRequests: (network.requests || []).filter(r => {
61
+ const status = r.status || r.statusCode || 0;
62
+ return status >= 200 && status < 300;
63
+ }).length,
64
+ uiChanged,
65
+ domChanged,
66
+ urlChanged,
67
+ reason: 'Form submitted successfully but provided no visual feedback'
68
+ };
69
+
70
+ const finding = {
71
+ type: 'form_silent_failure',
72
+ description: `Form submission succeeded with no success feedback to user`,
73
+ summary: `Form submitted successfully (2xx response) but no UI feedback (no toast, message, or redirect)`,
74
+ explanation: `The form submission was completed by the server (2xx status code received), but the application provided no visual feedback to the user. The page remained unchanged, with no success message, toast notification, or redirect.`,
75
+ evidence,
76
+ confidence: {
77
+ level: 0.90, // HIGH - network + UI evidence
78
+ reasons: [
79
+ 'Form submitted and network request returned 2xx (success)',
80
+ 'No UI feedback appeared (no toast, modal, or message)',
81
+ 'No page navigation (user stayed on same page)',
82
+ 'No DOM change (no new content loaded)'
83
+ ]
84
+ },
85
+ promise: {
86
+ type: 'form_submission',
87
+ expected: 'Submit form and display success feedback',
88
+ actual: 'Form submitted successfully but no feedback provided'
89
+ },
90
+ capabilityNote: 'Detection based on network status and visual feedback only. Does not parse response body or validate success semantics (UNSUPPORTED).'
91
+ };
92
+
93
+ // Enrich with explanations
94
+ enrichFindingWithExplanations(finding, trace);
95
+ findings.push(finding);
96
+ }
97
+ }
98
+ }