@veraxhq/verax 0.2.1 → 0.3.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 (152) hide show
  1. package/README.md +14 -18
  2. package/bin/verax.js +7 -0
  3. package/package.json +3 -3
  4. package/src/cli/commands/baseline.js +104 -0
  5. package/src/cli/commands/default.js +79 -25
  6. package/src/cli/commands/ga.js +243 -0
  7. package/src/cli/commands/gates.js +95 -0
  8. package/src/cli/commands/inspect.js +131 -2
  9. package/src/cli/commands/release-check.js +213 -0
  10. package/src/cli/commands/run.js +246 -35
  11. package/src/cli/commands/security-check.js +211 -0
  12. package/src/cli/commands/truth.js +114 -0
  13. package/src/cli/entry.js +304 -67
  14. package/src/cli/util/angular-component-extractor.js +179 -0
  15. package/src/cli/util/angular-navigation-detector.js +141 -0
  16. package/src/cli/util/angular-network-detector.js +161 -0
  17. package/src/cli/util/angular-state-detector.js +162 -0
  18. package/src/cli/util/ast-interactive-detector.js +546 -0
  19. package/src/cli/util/ast-network-detector.js +603 -0
  20. package/src/cli/util/ast-usestate-detector.js +602 -0
  21. package/src/cli/util/bootstrap-guard.js +86 -0
  22. package/src/cli/util/determinism-runner.js +123 -0
  23. package/src/cli/util/determinism-writer.js +129 -0
  24. package/src/cli/util/env-url.js +4 -0
  25. package/src/cli/util/expectation-extractor.js +369 -73
  26. package/src/cli/util/findings-writer.js +126 -16
  27. package/src/cli/util/learn-writer.js +3 -1
  28. package/src/cli/util/observe-writer.js +3 -1
  29. package/src/cli/util/paths.js +3 -12
  30. package/src/cli/util/project-discovery.js +3 -0
  31. package/src/cli/util/project-writer.js +3 -1
  32. package/src/cli/util/run-resolver.js +64 -0
  33. package/src/cli/util/source-requirement.js +55 -0
  34. package/src/cli/util/summary-writer.js +1 -0
  35. package/src/cli/util/svelte-navigation-detector.js +163 -0
  36. package/src/cli/util/svelte-network-detector.js +80 -0
  37. package/src/cli/util/svelte-sfc-extractor.js +147 -0
  38. package/src/cli/util/svelte-state-detector.js +243 -0
  39. package/src/cli/util/vue-navigation-detector.js +177 -0
  40. package/src/cli/util/vue-sfc-extractor.js +162 -0
  41. package/src/cli/util/vue-state-detector.js +215 -0
  42. package/src/verax/cli/finding-explainer.js +56 -3
  43. package/src/verax/core/artifacts/registry.js +154 -0
  44. package/src/verax/core/artifacts/verifier.js +980 -0
  45. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  46. package/src/verax/core/baseline/baseline.snapshot.js +231 -0
  47. package/src/verax/core/capabilities/gates.js +499 -0
  48. package/src/verax/core/capabilities/registry.js +475 -0
  49. package/src/verax/core/confidence/confidence-compute.js +137 -0
  50. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  51. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  52. package/src/verax/core/confidence/confidence-weights.js +44 -0
  53. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  54. package/src/verax/core/confidence/confidence.loader.js +79 -0
  55. package/src/verax/core/confidence/confidence.schema.js +94 -0
  56. package/src/verax/core/confidence-engine-refactor.js +484 -0
  57. package/src/verax/core/confidence-engine.js +486 -0
  58. package/src/verax/core/confidence-engine.js.backup +471 -0
  59. package/src/verax/core/contracts/index.js +29 -0
  60. package/src/verax/core/contracts/types.js +185 -0
  61. package/src/verax/core/contracts/validators.js +381 -0
  62. package/src/verax/core/decision-snapshot.js +30 -3
  63. package/src/verax/core/decisions/decision.trace.js +276 -0
  64. package/src/verax/core/determinism/contract-writer.js +89 -0
  65. package/src/verax/core/determinism/contract.js +139 -0
  66. package/src/verax/core/determinism/diff.js +364 -0
  67. package/src/verax/core/determinism/engine.js +221 -0
  68. package/src/verax/core/determinism/finding-identity.js +148 -0
  69. package/src/verax/core/determinism/normalize.js +438 -0
  70. package/src/verax/core/determinism/report-writer.js +92 -0
  71. package/src/verax/core/determinism/run-fingerprint.js +118 -0
  72. package/src/verax/core/dynamic-route-intelligence.js +528 -0
  73. package/src/verax/core/evidence/evidence-capture-service.js +307 -0
  74. package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
  75. package/src/verax/core/evidence-builder.js +487 -0
  76. package/src/verax/core/execution-mode-context.js +77 -0
  77. package/src/verax/core/execution-mode-detector.js +190 -0
  78. package/src/verax/core/failures/exit-codes.js +86 -0
  79. package/src/verax/core/failures/failure-summary.js +76 -0
  80. package/src/verax/core/failures/failure.factory.js +225 -0
  81. package/src/verax/core/failures/failure.ledger.js +132 -0
  82. package/src/verax/core/failures/failure.types.js +196 -0
  83. package/src/verax/core/failures/index.js +10 -0
  84. package/src/verax/core/ga/ga-report-writer.js +43 -0
  85. package/src/verax/core/ga/ga.artifact.js +49 -0
  86. package/src/verax/core/ga/ga.contract.js +434 -0
  87. package/src/verax/core/ga/ga.enforcer.js +86 -0
  88. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  89. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  90. package/src/verax/core/guardrails/policy.loader.js +83 -0
  91. package/src/verax/core/guardrails/policy.schema.js +110 -0
  92. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  93. package/src/verax/core/guardrails-engine.js +505 -0
  94. package/src/verax/core/observe/run-timeline.js +316 -0
  95. package/src/verax/core/perf/perf.contract.js +186 -0
  96. package/src/verax/core/perf/perf.display.js +65 -0
  97. package/src/verax/core/perf/perf.enforcer.js +91 -0
  98. package/src/verax/core/perf/perf.monitor.js +209 -0
  99. package/src/verax/core/perf/perf.report.js +198 -0
  100. package/src/verax/core/pipeline-tracker.js +238 -0
  101. package/src/verax/core/product-definition.js +127 -0
  102. package/src/verax/core/release/provenance.builder.js +271 -0
  103. package/src/verax/core/release/release-report-writer.js +40 -0
  104. package/src/verax/core/release/release.enforcer.js +159 -0
  105. package/src/verax/core/release/reproducibility.check.js +221 -0
  106. package/src/verax/core/release/sbom.builder.js +283 -0
  107. package/src/verax/core/report/cross-index.js +192 -0
  108. package/src/verax/core/report/human-summary.js +222 -0
  109. package/src/verax/core/route-intelligence.js +419 -0
  110. package/src/verax/core/security/secrets.scan.js +326 -0
  111. package/src/verax/core/security/security-report.js +50 -0
  112. package/src/verax/core/security/security.enforcer.js +124 -0
  113. package/src/verax/core/security/supplychain.defaults.json +38 -0
  114. package/src/verax/core/security/supplychain.policy.js +326 -0
  115. package/src/verax/core/security/vuln.scan.js +265 -0
  116. package/src/verax/core/truth/truth.certificate.js +250 -0
  117. package/src/verax/core/ui-feedback-intelligence.js +515 -0
  118. package/src/verax/detect/confidence-engine.js +628 -40
  119. package/src/verax/detect/confidence-helper.js +33 -0
  120. package/src/verax/detect/detection-engine.js +18 -1
  121. package/src/verax/detect/dynamic-route-findings.js +335 -0
  122. package/src/verax/detect/expectation-chain-detector.js +417 -0
  123. package/src/verax/detect/expectation-model.js +3 -1
  124. package/src/verax/detect/findings-writer.js +141 -5
  125. package/src/verax/detect/index.js +229 -5
  126. package/src/verax/detect/journey-stall-detector.js +558 -0
  127. package/src/verax/detect/route-findings.js +218 -0
  128. package/src/verax/detect/ui-feedback-findings.js +207 -0
  129. package/src/verax/detect/verdict-engine.js +57 -3
  130. package/src/verax/detect/view-switch-correlator.js +242 -0
  131. package/src/verax/index.js +413 -45
  132. package/src/verax/learn/action-contract-extractor.js +682 -64
  133. package/src/verax/learn/route-validator.js +4 -1
  134. package/src/verax/observe/index.js +88 -843
  135. package/src/verax/observe/interaction-runner.js +25 -8
  136. package/src/verax/observe/observe-context.js +205 -0
  137. package/src/verax/observe/observe-helpers.js +191 -0
  138. package/src/verax/observe/observe-runner.js +226 -0
  139. package/src/verax/observe/observers/budget-observer.js +185 -0
  140. package/src/verax/observe/observers/console-observer.js +102 -0
  141. package/src/verax/observe/observers/coverage-observer.js +107 -0
  142. package/src/verax/observe/observers/interaction-observer.js +471 -0
  143. package/src/verax/observe/observers/navigation-observer.js +132 -0
  144. package/src/verax/observe/observers/network-observer.js +87 -0
  145. package/src/verax/observe/observers/safety-observer.js +82 -0
  146. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  147. package/src/verax/observe/ui-feedback-detector.js +742 -0
  148. package/src/verax/observe/ui-signal-sensor.js +148 -2
  149. package/src/verax/scan-summary-writer.js +42 -8
  150. package/src/verax/shared/artifact-manager.js +8 -5
  151. package/src/verax/shared/css-spinner-rules.js +204 -0
  152. package/src/verax/shared/view-switch-rules.js +208 -0
@@ -0,0 +1,515 @@
1
+ /**
2
+ * PHASE 13 — UI Feedback Deepening
3
+ *
4
+ * Unified UI feedback detection and intelligence layer that:
5
+ * - Defines canonical UI feedback taxonomy
6
+ * - Scores feedback presence/absence deterministically
7
+ * - Correlates feedback with promises
8
+ * - Provides evidence-backed findings
9
+ */
10
+
11
+ /**
12
+ * PHASE 13: UI Feedback Taxonomy
13
+ */
14
+ export const FEEDBACK_TYPE = {
15
+ LOADING: 'loading',
16
+ DISABLED: 'disabled',
17
+ TOAST: 'toast',
18
+ MODAL: 'modal',
19
+ INLINE_MESSAGE: 'inline_message',
20
+ DOM_CHANGE: 'dom_change',
21
+ };
22
+
23
+ export const FEEDBACK_SCORE = {
24
+ CONFIRMED: 'FEEDBACK_CONFIRMED',
25
+ MISSING: 'FEEDBACK_MISSING',
26
+ AMBIGUOUS: 'FEEDBACK_AMBIGUOUS',
27
+ };
28
+
29
+ /**
30
+ * PHASE 13: Detect UI feedback signals from trace sensors
31
+ *
32
+ * @param {Object} trace - Interaction trace with sensors
33
+ * @returns {Array} Array of detected feedback signals
34
+ */
35
+ export function detectUIFeedbackSignals(trace) {
36
+ const signals = [];
37
+ const sensors = trace.sensors || {};
38
+ const uiSignals = sensors.uiSignals || {};
39
+ const uiFeedback = sensors.uiFeedback || {};
40
+ const before = trace.before || {};
41
+ const after = trace.after || {};
42
+
43
+ const beforeSignals = uiSignals.before || {};
44
+ const afterSignals = uiSignals.after || {};
45
+ const diff = uiSignals.diff || {};
46
+
47
+ // 1. Loading indicators
48
+ if (afterSignals.hasLoadingIndicator ||
49
+ uiFeedback.signals?.loading?.appeared === true ||
50
+ uiFeedback.signals?.loading?.disappeared === true) {
51
+ signals.push({
52
+ type: FEEDBACK_TYPE.LOADING,
53
+ selector: findLoadingSelector(afterSignals),
54
+ confidence: 0.9,
55
+ evidence: {
56
+ before: beforeSignals.hasLoadingIndicator || false,
57
+ after: afterSignals.hasLoadingIndicator || false,
58
+ appeared: uiFeedback.signals?.loading?.appeared === true,
59
+ disappeared: uiFeedback.signals?.loading?.disappeared === true,
60
+ },
61
+ });
62
+ }
63
+
64
+ // 2. Disabled/blocked states
65
+ const disabledChanged = diff.buttonStateChanged === true ||
66
+ (beforeSignals.disabledElements?.length || 0) !== (afterSignals.disabledElements?.length || 0) ||
67
+ uiFeedback.signals?.buttonStateTransition?.happened === true;
68
+
69
+ if (disabledChanged) {
70
+ signals.push({
71
+ type: FEEDBACK_TYPE.DISABLED,
72
+ selector: findDisabledSelector(afterSignals),
73
+ confidence: 0.85,
74
+ evidence: {
75
+ beforeCount: beforeSignals.disabledElements?.length || 0,
76
+ afterCount: afterSignals.disabledElements?.length || 0,
77
+ buttonStateChanged: diff.buttonStateChanged === true,
78
+ },
79
+ });
80
+ }
81
+
82
+ // 3. Toast/snackbar notifications
83
+ if (afterSignals.hasStatusSignal ||
84
+ afterSignals.hasLiveRegion ||
85
+ uiFeedback.signals?.notification?.happened === true) {
86
+ signals.push({
87
+ type: FEEDBACK_TYPE.TOAST,
88
+ selector: findToastSelector(afterSignals),
89
+ confidence: 0.9,
90
+ evidence: {
91
+ hasStatusSignal: afterSignals.hasStatusSignal || false,
92
+ hasLiveRegion: afterSignals.hasLiveRegion || false,
93
+ notification: uiFeedback.signals?.notification?.happened === true,
94
+ },
95
+ });
96
+ }
97
+
98
+ // 4. Modal/dialog confirmations
99
+ if (afterSignals.hasDialog ||
100
+ uiFeedback.signals?.domChange?.happened === true) {
101
+ // Check if dialog appeared
102
+ const dialogAppeared = !beforeSignals.hasDialog && afterSignals.hasDialog;
103
+
104
+ if (dialogAppeared) {
105
+ signals.push({
106
+ type: FEEDBACK_TYPE.MODAL,
107
+ selector: findDialogSelector(afterSignals),
108
+ confidence: 0.95,
109
+ evidence: {
110
+ before: beforeSignals.hasDialog || false,
111
+ after: afterSignals.hasDialog || false,
112
+ appeared: dialogAppeared,
113
+ },
114
+ });
115
+ }
116
+ }
117
+
118
+ // 5. Inline success/error messages
119
+ if (afterSignals.hasErrorSignal ||
120
+ afterSignals.validationFeedbackDetected ||
121
+ uiFeedback.signals?.domChange?.happened === true) {
122
+ signals.push({
123
+ type: FEEDBACK_TYPE.INLINE_MESSAGE,
124
+ selector: findInlineMessageSelector(afterSignals),
125
+ confidence: 0.85,
126
+ evidence: {
127
+ hasErrorSignal: afterSignals.hasErrorSignal || false,
128
+ validationFeedbackDetected: afterSignals.validationFeedbackDetected || false,
129
+ },
130
+ });
131
+ }
132
+
133
+ // 6. Meaningful DOM changes
134
+ const domChanged = trace.dom?.beforeHash !== trace.dom?.afterHash ||
135
+ uiFeedback.signals?.domChange?.happened === true ||
136
+ diff.changed === true;
137
+
138
+ if (domChanged) {
139
+ // Only count as feedback if it's a meaningful change (not just timestamps/random IDs)
140
+ const isMeaningful = isMeaningfulDOMChange(trace, uiFeedback);
141
+
142
+ if (isMeaningful) {
143
+ signals.push({
144
+ type: FEEDBACK_TYPE.DOM_CHANGE,
145
+ selector: null, // DOM change affects multiple elements
146
+ confidence: 0.7,
147
+ evidence: {
148
+ domHashChanged: trace.dom?.beforeHash !== trace.dom?.afterHash,
149
+ uiFeedbackDomChange: uiFeedback.signals?.domChange?.happened === true,
150
+ uiSignalsChanged: diff.changed === true,
151
+ },
152
+ });
153
+ }
154
+ }
155
+
156
+ return signals;
157
+ }
158
+
159
+ /**
160
+ * PHASE 13: Score feedback presence/absence
161
+ *
162
+ * @param {Array} signals - Detected feedback signals
163
+ * @param {Object} expectation - Promise/expectation that should have feedback
164
+ * @param {Object} trace - Interaction trace
165
+ * @returns {Object} Scoring result
166
+ */
167
+ export function scoreUIFeedback(signals, expectation, trace) {
168
+ const sensors = trace.sensors || {};
169
+ const networkSensor = sensors.network || {};
170
+ const uiFeedback = sensors.uiFeedback || {};
171
+
172
+ // Determine expected feedback type based on expectation
173
+ const expectedFeedbackTypes = inferExpectedFeedbackTypes(expectation);
174
+
175
+ // Check if any expected feedback types are present
176
+ const matchingSignals = signals.filter(s =>
177
+ expectedFeedbackTypes.includes(s.type)
178
+ );
179
+
180
+ // Overall UI feedback score from sensor
181
+ const overallScore = uiFeedback.overallUiFeedbackScore || 0;
182
+
183
+ // Network activity context
184
+ const hasNetworkActivity = networkSensor.hasNetworkActivity === true ||
185
+ (networkSensor.totalRequests || 0) > 0;
186
+ const hasNetworkFailure = networkSensor.failedRequests > 0 ||
187
+ networkSensor.topFailedUrls?.length > 0;
188
+
189
+ // Determine score
190
+ if (matchingSignals.length > 0 || overallScore > 0.5) {
191
+ return {
192
+ score: FEEDBACK_SCORE.CONFIRMED,
193
+ confidence: Math.max(...matchingSignals.map(s => s.confidence), overallScore),
194
+ explanation: buildFeedbackExplanation(matchingSignals, overallScore, 'confirmed'),
195
+ signals: matchingSignals,
196
+ topSignals: matchingSignals.slice(0, 3).map(s => ({
197
+ type: s.type,
198
+ confidence: s.confidence,
199
+ selector: s.selector,
200
+ })),
201
+ };
202
+ }
203
+
204
+ // If network activity but no feedback, likely missing
205
+ if (hasNetworkActivity && signals.length === 0 && overallScore < 0.3) {
206
+ return {
207
+ score: FEEDBACK_SCORE.MISSING,
208
+ confidence: hasNetworkFailure ? 0.9 : 0.7,
209
+ explanation: buildFeedbackExplanation([], overallScore, 'missing', {
210
+ hasNetworkActivity,
211
+ hasNetworkFailure,
212
+ }),
213
+ signals: [],
214
+ topSignals: [],
215
+ };
216
+ }
217
+
218
+ // Ambiguous case
219
+ if (signals.length > 0 && overallScore > 0 && overallScore < 0.5) {
220
+ return {
221
+ score: FEEDBACK_SCORE.AMBIGUOUS,
222
+ confidence: 0.6,
223
+ explanation: buildFeedbackExplanation(signals, overallScore, 'ambiguous'),
224
+ signals: signals,
225
+ topSignals: signals.slice(0, 3).map(s => ({
226
+ type: s.type,
227
+ confidence: s.confidence,
228
+ selector: s.selector,
229
+ })),
230
+ };
231
+ }
232
+
233
+ // Default: missing if no signals and low score
234
+ return {
235
+ score: FEEDBACK_SCORE.MISSING,
236
+ confidence: 0.5,
237
+ explanation: buildFeedbackExplanation([], overallScore, 'missing'),
238
+ signals: [],
239
+ topSignals: [],
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Infer expected feedback types from expectation
245
+ */
246
+ function inferExpectedFeedbackTypes(expectation) {
247
+ const types = [];
248
+
249
+ if (!expectation) return types;
250
+
251
+ // Network expectations → loading + toast/error
252
+ if (expectation.type === 'network_action' || expectation.type === 'network') {
253
+ types.push(FEEDBACK_TYPE.LOADING);
254
+ types.push(FEEDBACK_TYPE.TOAST);
255
+ types.push(FEEDBACK_TYPE.INLINE_MESSAGE);
256
+ }
257
+
258
+ // Navigation expectations → DOM change or modal
259
+ if (expectation.type === 'navigation' || expectation.type === 'spa_navigation') {
260
+ types.push(FEEDBACK_TYPE.DOM_CHANGE);
261
+ types.push(FEEDBACK_TYPE.MODAL);
262
+ }
263
+
264
+ // State expectations → DOM change or disabled state
265
+ if (expectation.type === 'state_action' || expectation.type === 'state') {
266
+ types.push(FEEDBACK_TYPE.DOM_CHANGE);
267
+ types.push(FEEDBACK_TYPE.DISABLED);
268
+ }
269
+
270
+ // Validation expectations → inline message
271
+ if (expectation.type === 'validation' || expectation.type === 'form_submission') {
272
+ types.push(FEEDBACK_TYPE.INLINE_MESSAGE);
273
+ types.push(FEEDBACK_TYPE.TOAST);
274
+ }
275
+
276
+ return types;
277
+ }
278
+
279
+ /**
280
+ * Build explanation for feedback score
281
+ */
282
+ function buildFeedbackExplanation(signals, overallScore, outcome, context = {}) {
283
+ const parts = [];
284
+
285
+ if (outcome === 'confirmed') {
286
+ if (signals.length > 0) {
287
+ parts.push(`Detected ${signals.length} feedback signal(s): ${signals.map(s => s.type).join(', ')}`);
288
+ }
289
+ if (overallScore > 0.5) {
290
+ parts.push(`Overall UI feedback score: ${overallScore.toFixed(2)}`);
291
+ }
292
+ } else if (outcome === 'missing') {
293
+ parts.push('No feedback signals detected');
294
+ if (context.hasNetworkActivity) {
295
+ parts.push('Network activity occurred but no feedback');
296
+ }
297
+ if (context.hasNetworkFailure) {
298
+ parts.push('Network failure occurred but no error feedback');
299
+ }
300
+ if (overallScore < 0.3) {
301
+ parts.push(`Low UI feedback score: ${overallScore.toFixed(2)}`);
302
+ }
303
+ } else if (outcome === 'ambiguous') {
304
+ parts.push('Feedback signals present but confidence is low');
305
+ if (signals.length > 0) {
306
+ parts.push(`Detected ${signals.length} signal(s) but overall score is ${overallScore.toFixed(2)}`);
307
+ }
308
+ }
309
+
310
+ return parts.join('. ');
311
+ }
312
+
313
+ /**
314
+ * Helper functions to find selectors
315
+ */
316
+ function findLoadingSelector(signals) {
317
+ // Return a generic selector hint
318
+ return '[aria-busy="true"], [data-loading], [role="status"]';
319
+ }
320
+
321
+ function findDisabledSelector(signals) {
322
+ return '[disabled], [aria-disabled="true"]';
323
+ }
324
+
325
+ function findToastSelector(signals) {
326
+ return '[role="alert"], [role="status"], [aria-live], .toast, .snackbar';
327
+ }
328
+
329
+ function findDialogSelector(signals) {
330
+ return '[role="dialog"], [aria-modal="true"]';
331
+ }
332
+
333
+ function findInlineMessageSelector(signals) {
334
+ return '[role="alert"], .error, .success, [class*="message"]';
335
+ }
336
+
337
+ /**
338
+ * Check if DOM change is meaningful (not just timestamps/random IDs)
339
+ */
340
+ function isMeaningfulDOMChange(trace, uiFeedback) {
341
+ // If UI feedback sensor detected meaningful change, trust it
342
+ if (uiFeedback.signals?.domChange?.happened === true) {
343
+ return true;
344
+ }
345
+
346
+ // If DOM hash changed, consider it meaningful
347
+ if (trace.dom?.beforeHash !== trace.dom?.afterHash) {
348
+ // Additional check: if UI signals changed, it's meaningful
349
+ const uiSignals = trace.sensors?.uiSignals || {};
350
+ if (uiSignals.diff?.changed === true) {
351
+ return true;
352
+ }
353
+ }
354
+
355
+ return false;
356
+ }
357
+
358
+ /**
359
+ * PHASE 13: Correlate promise with UI feedback
360
+ *
361
+ * @param {Object} expectation - Promise/expectation
362
+ * @param {Object} feedbackScore - Feedback scoring result
363
+ * @param {Object} trace - Interaction trace
364
+ * @returns {Object} Correlation result
365
+ */
366
+ export function correlatePromiseWithFeedback(expectation, feedbackScore, trace) {
367
+ const sensors = trace.sensors || {};
368
+ const networkSensor = sensors.network || {};
369
+ const hasNetworkFailure = networkSensor.failedRequests > 0 ||
370
+ networkSensor.topFailedUrls?.length > 0;
371
+
372
+ // Rule 1: Network failed AND feedback missing → CONFIRMED silent failure
373
+ if (expectation.type === 'network_action' || expectation.type === 'network') {
374
+ if (hasNetworkFailure && feedbackScore.score === FEEDBACK_SCORE.MISSING) {
375
+ return {
376
+ outcome: 'CONFIRMED',
377
+ confidence: 0.9,
378
+ reason: 'Network request failed but no error feedback provided to user',
379
+ requiresEvidence: true,
380
+ };
381
+ }
382
+
383
+ // Network succeeded but no feedback and no DOM change
384
+ const hasNetworkSuccess = networkSensor.successfulRequests > 0;
385
+ const hasDomChange = trace.dom?.beforeHash !== trace.dom?.afterHash;
386
+ const hasUrlChange = trace.sensors?.navigation?.urlChanged === true;
387
+
388
+ if (hasNetworkSuccess &&
389
+ feedbackScore.score === FEEDBACK_SCORE.MISSING &&
390
+ !hasDomChange &&
391
+ !hasUrlChange) {
392
+ return {
393
+ outcome: 'SUSPECTED',
394
+ confidence: 0.7,
395
+ reason: 'Network request succeeded but no feedback or visible change',
396
+ requiresEvidence: true,
397
+ };
398
+ }
399
+ }
400
+
401
+ // Rule 2: Navigation promised but URL/UI unchanged
402
+ if (expectation.type === 'navigation' || expectation.type === 'spa_navigation') {
403
+ const urlChanged = trace.sensors?.navigation?.urlChanged === true;
404
+ const hasDomChange = trace.dom?.beforeHash !== trace.dom?.afterHash;
405
+
406
+ if (!urlChanged && !hasDomChange && feedbackScore.score === FEEDBACK_SCORE.MISSING) {
407
+ return {
408
+ outcome: 'CONFIRMED',
409
+ confidence: 0.85,
410
+ reason: 'Navigation promise not fulfilled - no URL change, DOM change, or feedback',
411
+ requiresEvidence: true,
412
+ };
413
+ }
414
+ }
415
+
416
+ // Rule 3: Validation expected but no inline feedback
417
+ if (expectation.type === 'validation' || expectation.type === 'form_submission') {
418
+ if (feedbackScore.score === FEEDBACK_SCORE.MISSING) {
419
+ // Check if form was actually submitted
420
+ const formSubmitted = trace.interaction?.type === 'form';
421
+
422
+ if (formSubmitted) {
423
+ return {
424
+ outcome: 'SUSPECTED',
425
+ confidence: 0.7,
426
+ reason: 'Form submission expected validation feedback but none detected',
427
+ requiresEvidence: true,
428
+ };
429
+ }
430
+ }
431
+ }
432
+
433
+ // Rule 4: State action but no UI feedback
434
+ if (expectation.type === 'state_action' || expectation.type === 'state') {
435
+ if (feedbackScore.score === FEEDBACK_SCORE.MISSING) {
436
+ // Check if state actually changed
437
+ const stateChanged = trace.sensors?.state?.changed?.length > 0;
438
+
439
+ if (stateChanged) {
440
+ return {
441
+ outcome: 'SUSPECTED',
442
+ confidence: 0.75,
443
+ reason: 'State changed but no UI feedback detected',
444
+ requiresEvidence: true,
445
+ };
446
+ }
447
+ }
448
+ }
449
+
450
+ // Default: no correlation (feedback present or expectation doesn't require it)
451
+ return {
452
+ outcome: null,
453
+ confidence: 0,
454
+ reason: null,
455
+ requiresEvidence: false,
456
+ };
457
+ }
458
+
459
+ /**
460
+ * PHASE 13: Build evidence for UI feedback finding
461
+ *
462
+ * @param {Object} feedbackScore - Feedback scoring result
463
+ * @param {Object} correlation - Promise-feedback correlation
464
+ * @param {Object} trace - Interaction trace
465
+ * @param {Object} expectation - Promise/expectation
466
+ * @returns {Object} Evidence object
467
+ */
468
+ export function buildUIFeedbackEvidence(feedbackScore, correlation, trace, expectation) {
469
+ const evidence = {
470
+ feedback: {
471
+ score: feedbackScore.score,
472
+ confidence: feedbackScore.confidence,
473
+ explanation: feedbackScore.explanation,
474
+ signals: feedbackScore.signals.map(s => ({
475
+ type: s.type,
476
+ selector: s.selector,
477
+ confidence: s.confidence,
478
+ })),
479
+ topSignals: feedbackScore.topSignals,
480
+ },
481
+ beforeAfter: {
482
+ beforeScreenshot: trace.before?.screenshot || null,
483
+ afterScreenshot: trace.after?.screenshot || null,
484
+ beforeUrl: trace.before?.url || null,
485
+ afterUrl: trace.after?.url || null,
486
+ beforeDomHash: trace.dom?.beforeHash || null,
487
+ afterDomHash: trace.dom?.afterHash || null,
488
+ },
489
+ promise: {
490
+ type: expectation?.type || null,
491
+ value: expectation?.promise?.value || expectation?.targetPath || null,
492
+ source: expectation?.source || null,
493
+ context: expectation?.source?.context || null,
494
+ astSource: expectation?.source?.astSource || null,
495
+ },
496
+ sensors: {
497
+ uiSignals: trace.sensors?.uiSignals || null,
498
+ uiFeedback: trace.sensors?.uiFeedback || null,
499
+ network: trace.sensors?.network || null,
500
+ navigation: trace.sensors?.navigation || null,
501
+ },
502
+ correlation: {
503
+ outcome: correlation.outcome,
504
+ confidence: correlation.confidence,
505
+ reason: correlation.reason,
506
+ },
507
+ timing: {
508
+ // Timing window information if available
509
+ stabilizationWindow: trace.sensors?.uiFeedback?.timing || null,
510
+ },
511
+ };
512
+
513
+ return evidence;
514
+ }
515
+