@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,314 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Test: Failure Cause Inference (FCI)
5
+ *
6
+ * Tests:
7
+ * 1. No evidence -> no causes (Evidence Law enforcement)
8
+ * 2. Determinism: same input -> same causes, same ordering, same wording
9
+ * 3. Cause inference for C2 (state mutation no UI)
10
+ * 4. Cause inference for C3 (dead click)
11
+ * 5. Cause inference for C7 (network silent)
12
+ */
13
+
14
+ import {
15
+ inferCauses,
16
+ inferCausesForFindings,
17
+ findingsWithCauses,
18
+ attachCausesToFinding
19
+ } from './failure-cause-inference.js';
20
+
21
+ let testCount = 0;
22
+ let passCount = 0;
23
+
24
+ function test(name, fn) {
25
+ testCount++;
26
+ try {
27
+ fn();
28
+ console.log(`✓ Test ${testCount}: ${name}`);
29
+ passCount++;
30
+ } catch (e) {
31
+ console.error(`✗ Test ${testCount}: ${name}`);
32
+ console.error(` ${e.message}`);
33
+ }
34
+ }
35
+
36
+ function assert(condition, message) {
37
+ if (!condition) {
38
+ throw new Error(message);
39
+ }
40
+ }
41
+
42
+ // Test 1: No evidence -> no causes
43
+ test('No evidence => no causes (Evidence Law)', () => {
44
+ const finding = {
45
+ id: 'test-1',
46
+ type: 'silent_failure',
47
+ evidence: {}
48
+ };
49
+ const causes = inferCauses(finding);
50
+ assert(Array.isArray(causes), 'Should return array');
51
+ assert(causes.length === 0, `Expected 0 causes, got ${causes.length}`);
52
+ });
53
+
54
+ // Test 2: Null/undefined finding
55
+ test('Null finding => no causes', () => {
56
+ const causes = inferCauses(null);
57
+ assert(causes.length === 0, 'Should return empty array for null');
58
+ });
59
+
60
+ // Test 3: Missing evidence field
61
+ test('Missing evidence field => no causes', () => {
62
+ const finding = {
63
+ id: 'test-2',
64
+ type: 'silent_failure'
65
+ };
66
+ const causes = inferCauses(finding);
67
+ assert(causes.length === 0, 'Should return empty array');
68
+ });
69
+
70
+ // Test 4: Determinism - run twice, get identical output
71
+ test('Determinism: same finding => same causes', () => {
72
+ const finding = {
73
+ id: 'test-det-1',
74
+ type: 'silent_failure',
75
+ evidence: {
76
+ stateMutation: true,
77
+ domChanged: false,
78
+ navigationOccurred: false,
79
+ uiFeedback: false
80
+ }
81
+ };
82
+ const run1 = inferCauses(finding);
83
+ const run2 = inferCauses(finding);
84
+
85
+ assert(JSON.stringify(run1) === JSON.stringify(run2), 'Runs should be identical');
86
+ assert(run1.length > 0, 'Should detect C2 cause');
87
+ assert(run1[0].id === 'C2_STATE_MUTATION_NO_UI', 'Should detect C2');
88
+ });
89
+
90
+ // Test 5: Determinism - ordering
91
+ test('Determinism: causes ordered by id', () => {
92
+ const finding = {
93
+ id: 'test-ord-1',
94
+ type: 'silent_failure',
95
+ evidence: {
96
+ stateMutation: true,
97
+ domChanged: false,
98
+ navigationOccurred: false,
99
+ uiFeedback: false,
100
+ interactionPerformed: true,
101
+ networkActivity: false,
102
+ userFeedback: false
103
+ }
104
+ };
105
+ const causes = inferCauses(finding);
106
+ // Should have both C2 and C3
107
+ assert(causes.length >= 1, 'Should detect at least 1 cause');
108
+
109
+ // Check ordering
110
+ for (let i = 1; i < causes.length; i++) {
111
+ assert(
112
+ causes[i].id.localeCompare(causes[i-1].id) > 0,
113
+ `Causes not ordered: ${causes[i].id} should come after ${causes[i-1].id}`
114
+ );
115
+ }
116
+ });
117
+
118
+ // Test 6: C2 inference - state mutation no UI
119
+ test('C2: State mutation + no DOM change + no feedback', () => {
120
+ const finding = {
121
+ id: 'c2-test',
122
+ type: 'state_action',
123
+ evidence: {
124
+ stateMutation: true,
125
+ domChanged: false,
126
+ navigationOccurred: false,
127
+ uiFeedback: false
128
+ }
129
+ };
130
+ const causes = inferCauses(finding);
131
+ assert(causes.length >= 1, 'Should find at least one cause');
132
+ assert(causes.some(c => c.id === 'C2_STATE_MUTATION_NO_UI'), 'Should find C2');
133
+
134
+ const c2 = causes.find(c => c.id === 'C2_STATE_MUTATION_NO_UI');
135
+ assert(c2.statement.includes('Likely cause:'), 'Should start with "Likely cause:"');
136
+ assert(c2.confidence === 'MEDIUM', 'Should be MEDIUM confidence');
137
+ assert(Array.isArray(c2.evidence_refs), 'Should have evidence_refs array');
138
+ assert(c2.evidence_refs.length > 0, 'Should have evidence references');
139
+ });
140
+
141
+ // Test 7: C3 inference - dead click
142
+ test('C3: Interaction but no network/nav/DOM/feedback', () => {
143
+ const finding = {
144
+ id: 'c3-test',
145
+ type: 'silent_failure',
146
+ evidence: {
147
+ interactionPerformed: true,
148
+ networkActivity: false,
149
+ navigationOccurred: false,
150
+ domChanged: false,
151
+ userFeedback: false
152
+ }
153
+ };
154
+ const causes = inferCauses(finding);
155
+ assert(causes.some(c => c.id === 'C3_DEAD_CLICK'), 'Should find C3');
156
+
157
+ const c3 = causes.find(c => c.id === 'C3_DEAD_CLICK');
158
+ assert(c3.title.includes('dead'), 'Title should mention dead');
159
+ assert(c3.confidence === 'MEDIUM', 'Should be MEDIUM confidence');
160
+ });
161
+
162
+ // Test 8: C7 inference - network silent
163
+ test('C7: Network failure + no feedback', () => {
164
+ const finding = {
165
+ id: 'c7-test',
166
+ type: 'network_silent_failure',
167
+ evidence: {
168
+ networkFailure: true,
169
+ uiFeedback: false,
170
+ domChanged: false
171
+ }
172
+ };
173
+ const causes = inferCauses(finding);
174
+ assert(causes.some(c => c.id === 'C7_NETWORK_SILENT'), 'Should find C7');
175
+
176
+ const c7 = causes.find(c => c.id === 'C7_NETWORK_SILENT');
177
+ assert(c7.statement.includes('network'), 'Should mention network');
178
+ assert(c7.confidence === 'MEDIUM', 'Should be MEDIUM confidence');
179
+ });
180
+
181
+ // Test 9: Batch inference
182
+ test('Batch inference on multiple findings', () => {
183
+ const findings = [
184
+ {
185
+ id: 'batch-1',
186
+ type: 'silent_failure',
187
+ evidence: { }
188
+ },
189
+ {
190
+ id: 'batch-2',
191
+ type: 'state_action',
192
+ evidence: {
193
+ stateMutation: true,
194
+ domChanged: false,
195
+ navigationOccurred: false,
196
+ uiFeedback: false
197
+ }
198
+ }
199
+ ];
200
+ const causesMap = inferCausesForFindings(findings);
201
+ assert('batch-1' in causesMap === false, 'batch-1 has no evidence, should not be in map');
202
+ assert('batch-2' in causesMap, 'batch-2 should be in map');
203
+ assert(causesMap['batch-2'].some(c => c.id === 'C2_STATE_MUTATION_NO_UI'), 'batch-2 should have C2');
204
+ });
205
+
206
+ // Test 10: findingsWithCauses filter
207
+ test('findingsWithCauses filters correctly', () => {
208
+ const findings = [
209
+ {
210
+ id: 'with-evidence',
211
+ type: 'state_action',
212
+ evidence: {
213
+ stateMutation: true,
214
+ domChanged: false,
215
+ navigationOccurred: false,
216
+ uiFeedback: false
217
+ }
218
+ },
219
+ {
220
+ id: 'no-evidence',
221
+ type: 'silent_failure',
222
+ evidence: { }
223
+ }
224
+ ];
225
+ const filtered = findingsWithCauses(findings);
226
+ assert(filtered.length === 1, `Expected 1 finding with causes, got ${filtered.length}`);
227
+ assert(filtered[0].id === 'with-evidence', 'Should only include finding with causes');
228
+ });
229
+
230
+ // Test 11: attachCausesToFinding mutation
231
+ test('attachCausesToFinding mutates finding', () => {
232
+ const finding = {
233
+ id: 'mutate-test',
234
+ type: 'state_action',
235
+ evidence: {
236
+ stateMutation: true,
237
+ domChanged: false,
238
+ navigationOccurred: false,
239
+ uiFeedback: false
240
+ }
241
+ };
242
+ assert(!('causes' in finding), 'Should not have causes before');
243
+ attachCausesToFinding(finding);
244
+ assert('causes' in finding, 'Should have causes after');
245
+ assert(finding.causes.length > 0, 'Should have non-empty causes array');
246
+ });
247
+
248
+ // Test 12: Cause statement format
249
+ test('Cause statements start with "Likely cause:"', () => {
250
+ const finding = {
251
+ id: 'format-test',
252
+ type: 'state_action',
253
+ evidence: {
254
+ stateMutation: true,
255
+ domChanged: false,
256
+ navigationOccurred: false,
257
+ uiFeedback: false
258
+ }
259
+ };
260
+ const causes = inferCauses(finding);
261
+ causes.forEach(cause => {
262
+ assert(
263
+ cause.statement.startsWith('Likely cause:'),
264
+ `Statement should start with "Likely cause:", got: ${cause.statement}`
265
+ );
266
+ });
267
+ });
268
+
269
+ // Test 13: Confidence never HIGH
270
+ test('Confidence is never HIGH', () => {
271
+ const findings = [
272
+ {
273
+ id: 't1',
274
+ type: 'state_action',
275
+ evidence: {
276
+ stateMutation: true,
277
+ domChanged: false,
278
+ navigationOccurred: false,
279
+ uiFeedback: false
280
+ }
281
+ },
282
+ {
283
+ id: 't2',
284
+ type: 'silent_failure',
285
+ evidence: {
286
+ interactionPerformed: true,
287
+ networkActivity: false,
288
+ navigationOccurred: false,
289
+ domChanged: false,
290
+ userFeedback: false
291
+ }
292
+ }
293
+ ];
294
+
295
+ findings.forEach(finding => {
296
+ const causes = inferCauses(finding);
297
+ causes.forEach(cause => {
298
+ assert(
299
+ cause.confidence === 'LOW' || cause.confidence === 'MEDIUM',
300
+ `Confidence should be LOW or MEDIUM, got ${cause.confidence}`
301
+ );
302
+ });
303
+ });
304
+ });
305
+
306
+ // Results
307
+ console.log(`\n${passCount}/${testCount} tests passed`);
308
+ if (passCount === testCount) {
309
+ console.log('✓ All tests passed');
310
+ process.exit(0);
311
+ } else {
312
+ console.log(`✗ ${testCount - passCount} test(s) failed`);
313
+ process.exit(1);
314
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * PHASE 13 — UI Feedback Findings Detector
3
+ *
4
+ * Detects UI feedback-related silent failures by correlating promises
5
+ * with feedback signals and evaluating outcomes.
6
+ */
7
+
8
+ import {
9
+ detectUIFeedbackSignals,
10
+ scoreUIFeedback,
11
+ correlatePromiseWithFeedback,
12
+ buildUIFeedbackEvidence,
13
+ FEEDBACK_SCORE,
14
+ } from '../core/ui-feedback-intelligence.js';
15
+ import { computeConfidence } from './confidence-engine.js';
16
+ import { computeConfidenceForFinding } from '../core/confidence-engine.js';
17
+ import { buildAndEnforceEvidencePackage } from '../core/evidence-builder.js';
18
+ import { applyGuardrails } from '../core/guardrails-engine.js';
19
+
20
+ /**
21
+ * PHASE 13: Detect UI feedback-related findings
22
+ *
23
+ * @param {Array} traces - Interaction traces
24
+ * @param {Object} manifest - Project manifest with expectations
25
+ * @param {Array} _findings - Findings array to append to
26
+ * @ts-expect-error - JSDoc param documented but unused
27
+ * @returns {Array} UI feedback-related findings
28
+ */
29
+ export function detectUIFeedbackFindings(traces, manifest, _findings) {
30
+ const feedbackFindings = [];
31
+
32
+ // Process each trace
33
+ for (const trace of traces) {
34
+ const interaction = trace.interaction || {};
35
+
36
+ // Find expectations for this interaction
37
+ const expectations = findExpectationsForInteraction(manifest, interaction, trace);
38
+
39
+ for (const expectation of expectations) {
40
+ // Detect UI feedback signals
41
+ const signals = detectUIFeedbackSignals(trace);
42
+
43
+ // Score feedback presence/absence
44
+ const feedbackScore = scoreUIFeedback(signals, expectation, trace);
45
+
46
+ // Correlate promise with feedback
47
+ const correlation = correlatePromiseWithFeedback(expectation, feedbackScore, trace);
48
+
49
+ // Generate finding if correlation indicates silent failure
50
+ if (correlation.outcome === 'CONFIRMED' || correlation.outcome === 'SUSPECTED') {
51
+ // Build evidence
52
+ const evidence = buildUIFeedbackEvidence(feedbackScore, correlation, trace, expectation);
53
+
54
+ // PHASE 13: Evidence Law - require sufficient evidence for CONFIRMED
55
+ const hasSufficientEvidence = evidence.beforeAfter.beforeScreenshot &&
56
+ evidence.beforeAfter.afterScreenshot &&
57
+ (evidence.feedback.signals.length > 0 ||
58
+ evidence.feedback.score === FEEDBACK_SCORE.MISSING);
59
+
60
+ // Determine finding type early (before use in confidence call)
61
+ let findingType = 'ui_feedback_silent_failure';
62
+ if (expectation.type === 'network_action' || expectation.type === 'network') {
63
+ findingType = 'network_feedback_missing';
64
+ } else if (expectation.type === 'navigation' || expectation.type === 'spa_navigation') {
65
+ findingType = 'navigation_feedback_missing';
66
+ } else if (expectation.type === 'validation' || expectation.type === 'form_submission') {
67
+ findingType = 'validation_feedback_missing';
68
+ }
69
+
70
+ // PHASE 15: Compute unified confidence
71
+ const unifiedConfidence = computeConfidenceForFinding({
72
+ findingType: findingType,
73
+ expectation,
74
+ sensors: trace.sensors || {},
75
+ comparisons: {},
76
+ evidence,
77
+ options: {}
78
+ });
79
+
80
+ // Legacy confidence for backward compatibility
81
+ const _confidence = computeConfidence({
82
+ findingType: 'ui_feedback_silent_failure',
83
+ expectation,
84
+ sensors: trace.sensors || {},
85
+ comparisons: {},
86
+ attemptMeta: {},
87
+ });
88
+
89
+ // Determine severity based on evidence
90
+ const severity = hasSufficientEvidence && correlation.outcome === 'CONFIRMED' && (unifiedConfidence.score01 || unifiedConfidence.score || 0) >= 0.8
91
+ ? 'CONFIRMED'
92
+ : 'SUSPECTED';
93
+
94
+ const finding = {
95
+ type: findingType,
96
+ severity,
97
+ confidence: unifiedConfidence.score01 || unifiedConfidence.score || 0, // Contract v1: score01 canonical
98
+ confidenceLevel: unifiedConfidence.level, // PHASE 15: Add confidence level
99
+ confidenceReasons: unifiedConfidence.topReasons || unifiedConfidence.reasons || [], // Contract v1: topReasons
100
+ interaction: {
101
+ type: interaction.type,
102
+ selector: interaction.selector,
103
+ label: interaction.label,
104
+ },
105
+ reason: correlation.reason || feedbackScore.explanation,
106
+ evidence,
107
+ source: {
108
+ file: expectation.source?.file || null,
109
+ line: expectation.source?.line || null,
110
+ column: expectation.source?.column || null,
111
+ context: expectation.source?.context || null,
112
+ astSource: expectation.source?.astSource || null,
113
+ },
114
+ };
115
+
116
+ // PHASE 16: Build and enforce evidence package
117
+ const findingWithEvidence = buildAndEnforceEvidencePackage(finding, {
118
+ expectation,
119
+ trace,
120
+ evidence,
121
+ confidence: unifiedConfidence,
122
+ });
123
+
124
+ // PHASE 17: Apply guardrails (AFTER evidence builder)
125
+ const context = {
126
+ evidencePackage: findingWithEvidence.evidencePackage,
127
+ signals: findingWithEvidence.evidencePackage?.signals || evidence.signals || {},
128
+ confidenceReasons: unifiedConfidence.reasons || [],
129
+ promiseType: expectation?.type || null,
130
+ };
131
+ const { finding: findingWithGuardrails } = applyGuardrails(findingWithEvidence, context);
132
+
133
+ feedbackFindings.push(findingWithGuardrails);
134
+ }
135
+ }
136
+ }
137
+
138
+ return feedbackFindings;
139
+ }
140
+
141
+ /**
142
+ * Find expectations matching the interaction
143
+ */
144
+ function findExpectationsForInteraction(manifest, interaction, trace) {
145
+ const expectations = [];
146
+
147
+ // Check static expectations
148
+ if (manifest.staticExpectations) {
149
+ const beforeUrl = trace.before?.url || trace.sensors?.navigation?.beforeUrl || '';
150
+ const beforePath = extractPathFromUrl(beforeUrl);
151
+
152
+ if (beforePath) {
153
+ const normalizedBefore = beforePath.replace(/\/$/, '') || '/';
154
+
155
+ for (const expectation of manifest.staticExpectations) {
156
+ const normalizedFrom = (expectation.fromPath || '').replace(/\/$/, '') || '/';
157
+ if (normalizedFrom === normalizedBefore) {
158
+ // Check selector match
159
+ const selectorHint = expectation.selectorHint || '';
160
+ const interactionSelector = interaction.selector || '';
161
+
162
+ if (!selectorHint || !interactionSelector ||
163
+ selectorHint === interactionSelector ||
164
+ selectorHint.includes(interactionSelector) ||
165
+ interactionSelector.includes(selectorHint)) {
166
+ expectations.push(expectation);
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ // Check expectations from intel (AST-based)
174
+ if (manifest.expectations) {
175
+ for (const expectation of manifest.expectations) {
176
+ // Match by selector or label
177
+ const selectorHint = expectation.selectorHint || '';
178
+ const interactionSelector = interaction.selector || '';
179
+ const interactionLabel = (interaction.label || '').toLowerCase();
180
+ const expectationLabel = (expectation.promise?.value || '').toLowerCase();
181
+
182
+ if (selectorHint === interactionSelector ||
183
+ (expectationLabel && interactionLabel && expectationLabel.includes(interactionLabel))) {
184
+ expectations.push(expectation);
185
+ }
186
+ }
187
+ }
188
+
189
+ return expectations;
190
+ }
191
+
192
+ /**
193
+ * Extract path from URL
194
+ */
195
+ function extractPathFromUrl(url) {
196
+ if (!url || typeof url !== 'string') return '';
197
+
198
+ try {
199
+ const urlObj = new URL(url);
200
+ return urlObj.pathname;
201
+ } catch {
202
+ // Relative URL
203
+ const pathMatch = url.match(/^([^?#]+)/);
204
+ return pathMatch ? pathMatch[1] : url;
205
+ }
206
+ }
207
+