@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,481 @@
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
+ // Evidence-only: no inferred feedback expectations
173
+ const expectedFeedbackTypes = [];
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: matchingSignals.length > 0
194
+ ? Math.max(...matchingSignals.map(s => s.confidence))
195
+ : overallScore,
196
+ explanation: buildFeedbackExplanation(matchingSignals, overallScore, 'confirmed'),
197
+ signals: matchingSignals,
198
+ topSignals: matchingSignals.slice(0, 3).map(s => ({
199
+ type: s.type,
200
+ confidence: s.confidence,
201
+ selector: s.selector,
202
+ })),
203
+ };
204
+ }
205
+
206
+ // If network activity but no feedback, likely missing
207
+ if (hasNetworkActivity && signals.length === 0 && overallScore < 0.3) {
208
+ return {
209
+ score: FEEDBACK_SCORE.MISSING,
210
+ confidence: hasNetworkFailure ? 0.9 : 0.7,
211
+ explanation: buildFeedbackExplanation([], overallScore, 'missing', {
212
+ hasNetworkActivity,
213
+ hasNetworkFailure,
214
+ }),
215
+ signals: [],
216
+ topSignals: [],
217
+ };
218
+ }
219
+
220
+ // Ambiguous case
221
+ if (signals.length > 0 && overallScore > 0 && overallScore < 0.5) {
222
+ return {
223
+ score: FEEDBACK_SCORE.AMBIGUOUS,
224
+ confidence: 0.6,
225
+ explanation: buildFeedbackExplanation(signals, overallScore, 'ambiguous'),
226
+ signals: signals,
227
+ topSignals: signals.slice(0, 3).map(s => ({
228
+ type: s.type,
229
+ confidence: s.confidence,
230
+ selector: s.selector,
231
+ })),
232
+ };
233
+ }
234
+
235
+ // Default: missing if no signals and low score
236
+ return {
237
+ score: FEEDBACK_SCORE.MISSING,
238
+ confidence: 0.5,
239
+ explanation: buildFeedbackExplanation([], overallScore, 'missing'),
240
+ signals: [],
241
+ topSignals: [],
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Build explanation for feedback score
247
+ */
248
+ function buildFeedbackExplanation(signals, overallScore, outcome, context = {}) {
249
+ const parts = [];
250
+
251
+ if (outcome === 'confirmed') {
252
+ if (signals.length > 0) {
253
+ parts.push(`Detected ${signals.length} feedback signal(s): ${signals.map(s => s.type).join(', ')}`);
254
+ }
255
+ if (overallScore > 0.5) {
256
+ parts.push(`Overall UI feedback score: ${overallScore.toFixed(2)}`);
257
+ }
258
+ } else if (outcome === 'missing') {
259
+ parts.push('No feedback signals detected');
260
+ if (context.hasNetworkActivity) {
261
+ parts.push('Network activity occurred but no feedback');
262
+ }
263
+ if (context.hasNetworkFailure) {
264
+ parts.push('Network failure occurred but no error feedback');
265
+ }
266
+ if (overallScore < 0.3) {
267
+ parts.push(`Low UI feedback score: ${overallScore.toFixed(2)}`);
268
+ }
269
+ } else if (outcome === 'ambiguous') {
270
+ parts.push('Feedback signals present but confidence is low');
271
+ if (signals.length > 0) {
272
+ parts.push(`Detected ${signals.length} signal(s) but overall score is ${overallScore.toFixed(2)}`);
273
+ }
274
+ }
275
+
276
+ return parts.join('. ');
277
+ }
278
+
279
+ /**
280
+ * Helper functions to find selectors
281
+ */
282
+ function findLoadingSelector(_signals) {
283
+ // Return a generic selector hint
284
+ return '[aria-busy="true"], [data-loading], [role="status"]';
285
+ }
286
+
287
+ function findDisabledSelector(_signals) {
288
+ return '[disabled], [aria-disabled="true"]';
289
+ }
290
+
291
+ function findToastSelector(_signals) {
292
+ return '[role="alert"], [role="status"], [aria-live], .toast, .snackbar';
293
+ }
294
+
295
+ function findDialogSelector(_signals) {
296
+ return '[role="dialog"], [aria-modal="true"]';
297
+ }
298
+
299
+ function findInlineMessageSelector(_signals) {
300
+ return '[role="alert"], .error, .success, [class*="message"]';
301
+ }
302
+
303
+ /**
304
+ * Check if DOM change is meaningful (not just timestamps/random IDs)
305
+ */
306
+ function isMeaningfulDOMChange(trace, uiFeedback) {
307
+ // If UI feedback sensor detected meaningful change, trust it
308
+ if (uiFeedback.signals?.domChange?.happened === true) {
309
+ return true;
310
+ }
311
+
312
+ // If DOM hash changed, consider it meaningful
313
+ if (trace.dom?.beforeHash !== trace.dom?.afterHash) {
314
+ // Additional check: if UI signals changed, it's meaningful
315
+ const uiSignals = trace.sensors?.uiSignals || {};
316
+ if (uiSignals.diff?.changed === true) {
317
+ return true;
318
+ }
319
+ }
320
+
321
+ return false;
322
+ }
323
+
324
+ /**
325
+ * PHASE 13: Correlate promise with UI feedback
326
+ *
327
+ * @param {Object} expectation - Promise/expectation
328
+ * @param {Object} feedbackScore - Feedback scoring result
329
+ * @param {Object} trace - Interaction trace
330
+ * @returns {Object} Correlation result
331
+ */
332
+ export function correlatePromiseWithFeedback(expectation, feedbackScore, trace) {
333
+ const sensors = trace.sensors || {};
334
+ const networkSensor = sensors.network || {};
335
+ const hasNetworkFailure = networkSensor.failedRequests > 0 ||
336
+ networkSensor.topFailedUrls?.length > 0;
337
+
338
+ // Rule 1: Network failed AND feedback missing → CONFIRMED silent failure
339
+ if (expectation.type === 'network_action' || expectation.type === 'network') {
340
+ if (hasNetworkFailure && feedbackScore.score === FEEDBACK_SCORE.MISSING) {
341
+ return {
342
+ outcome: 'CONFIRMED',
343
+ confidence: 0.9,
344
+ reason: 'Network request failed but no error feedback provided to user',
345
+ requiresEvidence: true,
346
+ };
347
+ }
348
+
349
+ // Network succeeded but no feedback and no DOM change
350
+ const hasNetworkSuccess = networkSensor.successfulRequests > 0;
351
+ const hasDomChange = trace.dom?.beforeHash !== trace.dom?.afterHash;
352
+ const hasUrlChange = trace.sensors?.navigation?.urlChanged === true;
353
+
354
+ if (hasNetworkSuccess &&
355
+ feedbackScore.score === FEEDBACK_SCORE.MISSING &&
356
+ !hasDomChange &&
357
+ !hasUrlChange) {
358
+ return {
359
+ outcome: 'SUSPECTED',
360
+ confidence: 0.7,
361
+ reason: 'Network request succeeded but no feedback or visible change',
362
+ requiresEvidence: true,
363
+ };
364
+ }
365
+ }
366
+
367
+ // Rule 2: Navigation promised but URL/UI unchanged
368
+ if (expectation.type === 'navigation' || expectation.type === 'spa_navigation') {
369
+ const urlChanged = trace.sensors?.navigation?.urlChanged === true;
370
+ const hasDomChange = trace.dom?.beforeHash !== trace.dom?.afterHash;
371
+
372
+ if (!urlChanged && !hasDomChange && feedbackScore.score === FEEDBACK_SCORE.MISSING) {
373
+ return {
374
+ outcome: 'CONFIRMED',
375
+ confidence: 0.85,
376
+ reason: 'Navigation promise not fulfilled - no URL change, DOM change, or feedback',
377
+ requiresEvidence: true,
378
+ };
379
+ }
380
+ }
381
+
382
+ // Rule 3: Validation expected but no inline feedback
383
+ if (expectation.type === 'validation' || expectation.type === 'form_submission') {
384
+ if (feedbackScore.score === FEEDBACK_SCORE.MISSING) {
385
+ // Check if form was actually submitted
386
+ const formSubmitted = trace.interaction?.type === 'form';
387
+
388
+ if (formSubmitted) {
389
+ return {
390
+ outcome: 'SUSPECTED',
391
+ confidence: 0.7,
392
+ reason: 'Form submission expected validation feedback but none detected',
393
+ requiresEvidence: true,
394
+ };
395
+ }
396
+ }
397
+ }
398
+
399
+ // Rule 4: State action but no UI feedback
400
+ if (expectation.type === 'state_action' || expectation.type === 'state') {
401
+ if (feedbackScore.score === FEEDBACK_SCORE.MISSING) {
402
+ // Check if state actually changed
403
+ const stateChanged = trace.sensors?.state?.changed?.length > 0;
404
+
405
+ if (stateChanged) {
406
+ return {
407
+ outcome: 'SUSPECTED',
408
+ confidence: 0.75,
409
+ reason: 'State changed but no UI feedback detected',
410
+ requiresEvidence: true,
411
+ };
412
+ }
413
+ }
414
+ }
415
+
416
+ // Default: no correlation (feedback present or expectation doesn't require it)
417
+ return {
418
+ outcome: null,
419
+ confidence: 0,
420
+ reason: null,
421
+ requiresEvidence: false,
422
+ };
423
+ }
424
+
425
+ /**
426
+ * PHASE 13: Build evidence for UI feedback finding
427
+ *
428
+ * @param {Object} feedbackScore - Feedback scoring result
429
+ * @param {Object} correlation - Promise-feedback correlation
430
+ * @param {Object} trace - Interaction trace
431
+ * @param {Object} expectation - Promise/expectation
432
+ * @returns {Object} Evidence object
433
+ */
434
+ export function buildUIFeedbackEvidence(feedbackScore, correlation, trace, expectation) {
435
+ const evidence = {
436
+ feedback: {
437
+ score: feedbackScore.score,
438
+ confidence: feedbackScore.confidence,
439
+ explanation: feedbackScore.explanation,
440
+ signals: feedbackScore.signals.map(s => ({
441
+ type: s.type,
442
+ selector: s.selector,
443
+ confidence: s.confidence,
444
+ })),
445
+ topSignals: feedbackScore.topSignals,
446
+ },
447
+ beforeAfter: {
448
+ beforeScreenshot: trace.before?.screenshot || null,
449
+ afterScreenshot: trace.after?.screenshot || null,
450
+ beforeUrl: trace.before?.url || null,
451
+ afterUrl: trace.after?.url || null,
452
+ beforeDomHash: trace.dom?.beforeHash || null,
453
+ afterDomHash: trace.dom?.afterHash || null,
454
+ },
455
+ promise: {
456
+ type: expectation?.type || null,
457
+ value: expectation?.promise?.value || expectation?.targetPath || null,
458
+ source: expectation?.source || null,
459
+ context: expectation?.source?.context || null,
460
+ astSource: expectation?.source?.astSource || null,
461
+ },
462
+ sensors: {
463
+ uiSignals: trace.sensors?.uiSignals || null,
464
+ uiFeedback: trace.sensors?.uiFeedback || null,
465
+ network: trace.sensors?.network || null,
466
+ navigation: trace.sensors?.navigation || null,
467
+ },
468
+ correlation: {
469
+ outcome: correlation.outcome,
470
+ confidence: correlation.confidence,
471
+ reason: correlation.reason,
472
+ },
473
+ timing: {
474
+ // Timing window information if available
475
+ stabilizationWindow: trace.sensors?.uiFeedback?.timing || null,
476
+ },
477
+ };
478
+
479
+ return evidence;
480
+ }
481
+
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Conditional UI Stale State Detection
3
+ *
4
+ * Detects UI elements that should update/disappear when state changes but don't:
5
+ * - State mutation observed (via state sensor or console logs)
6
+ * - Dependent UI element remains unchanged
7
+ * - Expected element missing or still visible
8
+ *
9
+ * CONFIDENCE: MEDIUM (heuristic-based)
10
+ * PARTIAL SUPPORT: Heuristic detection, not semantic analysis
11
+ *
12
+ * Note: Does NOT detect async race conditions (UNSUPPORTED - too many false positives)
13
+ */
14
+
15
+ import { hasDomChange } from './comparison.js';
16
+ import { enrichFindingWithExplanations } from './finding-detector.js';
17
+
18
+ export function detectConditionalUiSilentFailures(traces, manifest, findings) {
19
+ // Parameters:
20
+ // traces - array of interaction traces from observation
21
+ // manifest - project manifest (contains expectations)
22
+ // findings - array to append new findings to (mutated in-place)
23
+
24
+ for (const trace of traces) {
25
+ const sensors = trace.sensors || {};
26
+ const stateSignals = sensors.state || {};
27
+ const uiSignals = sensors.uiSignals || {};
28
+ const uiDiff = uiSignals.diff || {};
29
+ const domChanged = hasDomChange(trace);
30
+
31
+ // Heuristic: State changed but UI didn't
32
+ // This suggests a stale UI or conditional rendering bug
33
+ const stateChanged = stateSignals.changed === true ||
34
+ (stateSignals.changed && stateSignals.changed.length > 0);
35
+ const uiChanged = uiDiff.changed === true;
36
+
37
+ // Detection logic:
38
+ // State changed but UI did not
39
+ // This is a common pattern for stale UI bugs in React/Vue
40
+ if (stateChanged && !uiChanged && !domChanged) {
41
+ const interaction = trace.interaction || {};
42
+
43
+ // Skip if it's a form input (expected state-only change)
44
+ if (interaction.type === 'interaction' && interaction.category === 'button') {
45
+ const evidence = {
46
+ before: trace.before?.screenshot || trace.beforeScreenshot || '',
47
+ after: trace.after?.screenshot || trace.afterScreenshot || '',
48
+ beforeUrl: trace.before?.url || trace.beforeUrl || '',
49
+ afterUrl: trace.after?.url || trace.afterUrl || '',
50
+ stateChanged,
51
+ uiChanged,
52
+ domChanged,
53
+ reason: 'State updated but UI did not reflect the change (stale UI)'
54
+ };
55
+
56
+ const finding = {
57
+ type: 'conditional_ui_silent_failure',
58
+ description: `Conditional UI element did not update when state changed`,
59
+ summary: `Button/interaction caused state change but UI did not update (stale UI pattern)`,
60
+ explanation: `State mutation was detected but the UI elements dependent on that state did not update. This is a common pattern in React/Vue applications where conditional rendering or dynamic classes don't update properly.`,
61
+ evidence,
62
+ confidence: {
63
+ level: 0.60, // MEDIUM - heuristic-based
64
+ reasons: [
65
+ 'State mutation observed',
66
+ 'UI elements did not update to reflect new state',
67
+ 'Likely stale UI or conditional rendering bug'
68
+ ]
69
+ },
70
+ promise: {
71
+ type: 'conditional_ui_update',
72
+ expected: 'State change triggers UI update (conditional render, class change, etc.)',
73
+ actual: 'State changed but UI remained unchanged'
74
+ },
75
+ capabilityNote: 'PARTIAL SUPPORT: Detection based on state sensor data and heuristics. Does not perform semantic analysis of UI logic. Async race conditions are UNSUPPORTED due to high false positive rate.'
76
+ };
77
+
78
+ // Enrich with explanations
79
+ enrichFindingWithExplanations(finding, trace);
80
+ findings.push(finding);
81
+ }
82
+ }
83
+ }
84
+ }