@veraxhq/verax 0.2.0 → 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 (217) hide show
  1. package/README.md +14 -18
  2. package/bin/verax.js +7 -0
  3. package/package.json +15 -5
  4. package/src/cli/commands/baseline.js +104 -0
  5. package/src/cli/commands/default.js +323 -111
  6. package/src/cli/commands/doctor.js +36 -4
  7. package/src/cli/commands/ga.js +243 -0
  8. package/src/cli/commands/gates.js +95 -0
  9. package/src/cli/commands/inspect.js +131 -2
  10. package/src/cli/commands/release-check.js +213 -0
  11. package/src/cli/commands/run.js +498 -103
  12. package/src/cli/commands/security-check.js +211 -0
  13. package/src/cli/commands/truth.js +114 -0
  14. package/src/cli/entry.js +305 -68
  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 +546 -0
  20. package/src/cli/util/ast-network-detector.js +603 -0
  21. package/src/cli/util/ast-usestate-detector.js +602 -0
  22. package/src/cli/util/bootstrap-guard.js +86 -0
  23. package/src/cli/util/detection-engine.js +4 -3
  24. package/src/cli/util/determinism-runner.js +123 -0
  25. package/src/cli/util/determinism-writer.js +129 -0
  26. package/src/cli/util/env-url.js +4 -0
  27. package/src/cli/util/events.js +76 -0
  28. package/src/cli/util/expectation-extractor.js +380 -74
  29. package/src/cli/util/findings-writer.js +126 -15
  30. package/src/cli/util/learn-writer.js +3 -1
  31. package/src/cli/util/observation-engine.js +69 -23
  32. package/src/cli/util/observe-writer.js +3 -1
  33. package/src/cli/util/paths.js +6 -14
  34. package/src/cli/util/project-discovery.js +23 -0
  35. package/src/cli/util/project-writer.js +3 -1
  36. package/src/cli/util/redact.js +2 -2
  37. package/src/cli/util/run-resolver.js +64 -0
  38. package/src/cli/util/runtime-budget.js +147 -0
  39. package/src/cli/util/source-requirement.js +55 -0
  40. package/src/cli/util/summary-writer.js +13 -1
  41. package/src/cli/util/svelte-navigation-detector.js +163 -0
  42. package/src/cli/util/svelte-network-detector.js +80 -0
  43. package/src/cli/util/svelte-sfc-extractor.js +147 -0
  44. package/src/cli/util/svelte-state-detector.js +243 -0
  45. package/src/cli/util/vue-navigation-detector.js +177 -0
  46. package/src/cli/util/vue-sfc-extractor.js +162 -0
  47. package/src/cli/util/vue-state-detector.js +215 -0
  48. package/src/types/global.d.ts +28 -0
  49. package/src/types/ts-ast.d.ts +24 -0
  50. package/src/verax/cli/doctor.js +2 -2
  51. package/src/verax/cli/finding-explainer.js +56 -3
  52. package/src/verax/cli/init.js +1 -1
  53. package/src/verax/cli/url-safety.js +12 -2
  54. package/src/verax/cli/wizard.js +13 -2
  55. package/src/verax/core/artifacts/registry.js +154 -0
  56. package/src/verax/core/artifacts/verifier.js +980 -0
  57. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  58. package/src/verax/core/baseline/baseline.snapshot.js +231 -0
  59. package/src/verax/core/budget-engine.js +1 -1
  60. package/src/verax/core/capabilities/gates.js +499 -0
  61. package/src/verax/core/capabilities/registry.js +475 -0
  62. package/src/verax/core/confidence/confidence-compute.js +137 -0
  63. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  64. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  65. package/src/verax/core/confidence/confidence-weights.js +44 -0
  66. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  67. package/src/verax/core/confidence/confidence.loader.js +79 -0
  68. package/src/verax/core/confidence/confidence.schema.js +94 -0
  69. package/src/verax/core/confidence-engine-refactor.js +484 -0
  70. package/src/verax/core/confidence-engine.js +486 -0
  71. package/src/verax/core/confidence-engine.js.backup +471 -0
  72. package/src/verax/core/contracts/index.js +29 -0
  73. package/src/verax/core/contracts/types.js +185 -0
  74. package/src/verax/core/contracts/validators.js +381 -0
  75. package/src/verax/core/decision-snapshot.js +31 -4
  76. package/src/verax/core/decisions/decision.trace.js +276 -0
  77. package/src/verax/core/determinism/contract-writer.js +89 -0
  78. package/src/verax/core/determinism/contract.js +139 -0
  79. package/src/verax/core/determinism/diff.js +364 -0
  80. package/src/verax/core/determinism/engine.js +221 -0
  81. package/src/verax/core/determinism/finding-identity.js +148 -0
  82. package/src/verax/core/determinism/normalize.js +438 -0
  83. package/src/verax/core/determinism/report-writer.js +92 -0
  84. package/src/verax/core/determinism/run-fingerprint.js +118 -0
  85. package/src/verax/core/determinism-model.js +35 -6
  86. package/src/verax/core/dynamic-route-intelligence.js +528 -0
  87. package/src/verax/core/evidence/evidence-capture-service.js +307 -0
  88. package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
  89. package/src/verax/core/evidence-builder.js +487 -0
  90. package/src/verax/core/execution-mode-context.js +77 -0
  91. package/src/verax/core/execution-mode-detector.js +190 -0
  92. package/src/verax/core/failures/exit-codes.js +86 -0
  93. package/src/verax/core/failures/failure-summary.js +76 -0
  94. package/src/verax/core/failures/failure.factory.js +225 -0
  95. package/src/verax/core/failures/failure.ledger.js +132 -0
  96. package/src/verax/core/failures/failure.types.js +196 -0
  97. package/src/verax/core/failures/index.js +10 -0
  98. package/src/verax/core/ga/ga-report-writer.js +43 -0
  99. package/src/verax/core/ga/ga.artifact.js +49 -0
  100. package/src/verax/core/ga/ga.contract.js +434 -0
  101. package/src/verax/core/ga/ga.enforcer.js +86 -0
  102. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  103. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  104. package/src/verax/core/guardrails/policy.loader.js +83 -0
  105. package/src/verax/core/guardrails/policy.schema.js +110 -0
  106. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  107. package/src/verax/core/guardrails-engine.js +505 -0
  108. package/src/verax/core/incremental-store.js +15 -7
  109. package/src/verax/core/observe/run-timeline.js +316 -0
  110. package/src/verax/core/perf/perf.contract.js +186 -0
  111. package/src/verax/core/perf/perf.display.js +65 -0
  112. package/src/verax/core/perf/perf.enforcer.js +91 -0
  113. package/src/verax/core/perf/perf.monitor.js +209 -0
  114. package/src/verax/core/perf/perf.report.js +198 -0
  115. package/src/verax/core/pipeline-tracker.js +238 -0
  116. package/src/verax/core/product-definition.js +127 -0
  117. package/src/verax/core/release/provenance.builder.js +271 -0
  118. package/src/verax/core/release/release-report-writer.js +40 -0
  119. package/src/verax/core/release/release.enforcer.js +159 -0
  120. package/src/verax/core/release/reproducibility.check.js +221 -0
  121. package/src/verax/core/release/sbom.builder.js +283 -0
  122. package/src/verax/core/replay-validator.js +4 -4
  123. package/src/verax/core/replay.js +1 -1
  124. package/src/verax/core/report/cross-index.js +192 -0
  125. package/src/verax/core/report/human-summary.js +222 -0
  126. package/src/verax/core/route-intelligence.js +419 -0
  127. package/src/verax/core/security/secrets.scan.js +326 -0
  128. package/src/verax/core/security/security-report.js +50 -0
  129. package/src/verax/core/security/security.enforcer.js +124 -0
  130. package/src/verax/core/security/supplychain.defaults.json +38 -0
  131. package/src/verax/core/security/supplychain.policy.js +326 -0
  132. package/src/verax/core/security/vuln.scan.js +265 -0
  133. package/src/verax/core/silence-impact.js +1 -1
  134. package/src/verax/core/silence-model.js +9 -7
  135. package/src/verax/core/truth/truth.certificate.js +250 -0
  136. package/src/verax/core/ui-feedback-intelligence.js +515 -0
  137. package/src/verax/detect/comparison.js +8 -3
  138. package/src/verax/detect/confidence-engine.js +645 -57
  139. package/src/verax/detect/confidence-helper.js +33 -0
  140. package/src/verax/detect/detection-engine.js +19 -2
  141. package/src/verax/detect/dynamic-route-findings.js +335 -0
  142. package/src/verax/detect/evidence-index.js +15 -65
  143. package/src/verax/detect/expectation-chain-detector.js +417 -0
  144. package/src/verax/detect/expectation-model.js +56 -3
  145. package/src/verax/detect/explanation-helpers.js +1 -1
  146. package/src/verax/detect/finding-detector.js +2 -2
  147. package/src/verax/detect/findings-writer.js +149 -20
  148. package/src/verax/detect/flow-detector.js +4 -4
  149. package/src/verax/detect/index.js +265 -15
  150. package/src/verax/detect/interactive-findings.js +3 -4
  151. package/src/verax/detect/journey-stall-detector.js +558 -0
  152. package/src/verax/detect/route-findings.js +218 -0
  153. package/src/verax/detect/signal-mapper.js +2 -2
  154. package/src/verax/detect/skip-classifier.js +4 -4
  155. package/src/verax/detect/ui-feedback-findings.js +207 -0
  156. package/src/verax/detect/verdict-engine.js +61 -9
  157. package/src/verax/detect/view-switch-correlator.js +242 -0
  158. package/src/verax/flow/flow-engine.js +3 -2
  159. package/src/verax/flow/flow-spec.js +1 -2
  160. package/src/verax/index.js +413 -33
  161. package/src/verax/intel/effect-detector.js +1 -1
  162. package/src/verax/intel/index.js +2 -2
  163. package/src/verax/intel/route-extractor.js +3 -3
  164. package/src/verax/intel/vue-navigation-extractor.js +81 -18
  165. package/src/verax/intel/vue-router-extractor.js +4 -2
  166. package/src/verax/learn/action-contract-extractor.js +684 -66
  167. package/src/verax/learn/ast-contract-extractor.js +53 -1
  168. package/src/verax/learn/index.js +36 -2
  169. package/src/verax/learn/manifest-writer.js +28 -14
  170. package/src/verax/learn/route-extractor.js +1 -1
  171. package/src/verax/learn/route-validator.js +12 -8
  172. package/src/verax/learn/state-extractor.js +1 -1
  173. package/src/verax/learn/static-extractor-navigation.js +1 -1
  174. package/src/verax/learn/static-extractor-validation.js +2 -2
  175. package/src/verax/learn/static-extractor.js +8 -7
  176. package/src/verax/learn/ts-contract-resolver.js +14 -12
  177. package/src/verax/observe/browser.js +22 -3
  178. package/src/verax/observe/console-sensor.js +2 -2
  179. package/src/verax/observe/expectation-executor.js +2 -1
  180. package/src/verax/observe/focus-sensor.js +1 -1
  181. package/src/verax/observe/human-driver.js +29 -10
  182. package/src/verax/observe/index.js +92 -844
  183. package/src/verax/observe/interaction-discovery.js +27 -15
  184. package/src/verax/observe/interaction-runner.js +31 -14
  185. package/src/verax/observe/loading-sensor.js +6 -0
  186. package/src/verax/observe/navigation-sensor.js +1 -1
  187. package/src/verax/observe/observe-context.js +205 -0
  188. package/src/verax/observe/observe-helpers.js +191 -0
  189. package/src/verax/observe/observe-runner.js +226 -0
  190. package/src/verax/observe/observers/budget-observer.js +185 -0
  191. package/src/verax/observe/observers/console-observer.js +102 -0
  192. package/src/verax/observe/observers/coverage-observer.js +107 -0
  193. package/src/verax/observe/observers/interaction-observer.js +471 -0
  194. package/src/verax/observe/observers/navigation-observer.js +132 -0
  195. package/src/verax/observe/observers/network-observer.js +87 -0
  196. package/src/verax/observe/observers/safety-observer.js +82 -0
  197. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  198. package/src/verax/observe/settle.js +1 -0
  199. package/src/verax/observe/state-sensor.js +8 -4
  200. package/src/verax/observe/state-ui-sensor.js +7 -1
  201. package/src/verax/observe/traces-writer.js +27 -16
  202. package/src/verax/observe/ui-feedback-detector.js +742 -0
  203. package/src/verax/observe/ui-signal-sensor.js +155 -2
  204. package/src/verax/scan-summary-writer.js +46 -9
  205. package/src/verax/shared/artifact-manager.js +9 -6
  206. package/src/verax/shared/budget-profiles.js +2 -2
  207. package/src/verax/shared/caching.js +1 -1
  208. package/src/verax/shared/config-loader.js +1 -2
  209. package/src/verax/shared/css-spinner-rules.js +204 -0
  210. package/src/verax/shared/dynamic-route-utils.js +12 -6
  211. package/src/verax/shared/retry-policy.js +1 -6
  212. package/src/verax/shared/root-artifacts.js +1 -1
  213. package/src/verax/shared/view-switch-rules.js +208 -0
  214. package/src/verax/shared/zip-artifacts.js +1 -0
  215. package/src/verax/validate/context-validator.js +1 -1
  216. package/src/verax/observe/index.js.backup +0 -1
  217. package/src/verax/validate/context-validator.js.bak +0 -0
@@ -2,14 +2,30 @@
2
2
  * WAVE 3: UI Signal Sensor
3
3
  * Detects user-visible feedback signals: loading states, dialogs, error messages
4
4
  * Conservative: only count signals with accessibility semantics or explicit attributes
5
+ *
6
+ * CSS SPINNER DETECTION: Detects CSS-only loading indicators without semantic attributes
5
7
  */
6
8
 
9
+ import {
10
+ isBorderSpinnerPattern,
11
+ isRotationAnimation,
12
+ isPulseAnimation,
13
+ isSpinnerSize,
14
+ isDecorativeElement,
15
+ CSS_SPINNER_REASON_CODES
16
+ } from '../shared/css-spinner-rules.js';
17
+
7
18
  export class UISignalSensor {
8
19
  /**
9
20
  * Snapshot current UI signals on the page.
10
- * Returns: { hasLoadingIndicator, hasDialog, buttonStateChanged, errorSignals, explanation }
21
+ * Returns: { hasLoadingIndicator, hasDialog, buttonStateChanged, errorSignals, explanation, cssSpinners }
22
+ *
23
+ * @param {Object} page - Playwright page object
24
+ * @param {number} interactionTime - Optional: timestamp of interaction (for timing window)
25
+ * @param {Object} beforeSnapshot - Optional: snapshot before interaction (for correlation)
26
+ * @returns {Promise<Object>} UI signals snapshot
11
27
  */
12
- async snapshot(page) {
28
+ async snapshot(page, interactionTime = null, beforeSnapshot = null) {
13
29
  const signals = await page.evaluate(() => {
14
30
  const result = {
15
31
  hasLoadingIndicator: false,
@@ -41,6 +57,7 @@ export class UISignalSensor {
41
57
  const statusRegions = Array.from(document.querySelectorAll('[role="status"], [role="alert"]'));
42
58
  const visibleStatusRegions = statusRegions.filter((el) => {
43
59
  const style = window.getComputedStyle(el);
60
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
44
61
  return el.offsetParent !== null && style.visibility !== 'hidden' && style.display !== 'none' && style.opacity !== '0';
45
62
  });
46
63
  if (visibleStatusRegions.length > 0) {
@@ -57,6 +74,7 @@ export class UISignalSensor {
57
74
 
58
75
  // Check for dialogs
59
76
  const dialog = document.querySelector('[role="dialog"], [aria-modal="true"]');
77
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
60
78
  if (dialog && dialog.offsetParent !== null) {
61
79
  // offsetParent is null if element is hidden
62
80
  result.hasDialog = true;
@@ -83,6 +101,136 @@ export class UISignalSensor {
83
101
  );
84
102
  }
85
103
 
104
+ // CSS SPINNER DETECTION: Detect CSS-only loading indicators
105
+ result.cssSpinners = [];
106
+ result.cssSpinnerDetected = false;
107
+
108
+ const allElements = Array.from(document.querySelectorAll('*'));
109
+ const currentTime = Date.now();
110
+ const MAX_SPINNER_SIZE = 100;
111
+ const MIN_SPINNER_SIZE = 8;
112
+ const SPINNER_TIMING_WINDOW_MS = 2000;
113
+
114
+ // Helper functions (inlined for browser context)
115
+ const isBorderSpinnerPattern = (style) => {
116
+ const borderWidth = parseFloat(style.borderWidth) || 0;
117
+ const borderTopWidth = parseFloat(style.borderTopWidth) || 0;
118
+ if (borderWidth < 2 && borderTopWidth < 2) return false;
119
+
120
+ const borderColor = style.borderColor || '';
121
+ const borderTopColor = style.borderTopColor || '';
122
+ const hasDifferentTopColor = borderTopColor && borderColor && borderTopColor !== borderColor && borderTopColor !== 'rgba(0, 0, 0, 0)';
123
+
124
+ const borderRadius = style.borderRadius || '';
125
+ const isCircular = borderRadius.includes('50%') || borderRadius.includes('999') || parseFloat(borderRadius) > 20;
126
+
127
+ return hasDifferentTopColor && isCircular;
128
+ };
129
+
130
+ const isRotationAnimation = (style) => {
131
+ const animationName = (style.animationName || '').toLowerCase();
132
+ const animation = (style.animation || '').toLowerCase();
133
+ const transform = style.transform || '';
134
+ const animationDuration = style.animationDuration || '';
135
+
136
+ const spinnerKeywords = ['spin', 'rotate', 'loading', 'loader'];
137
+ const hasSpinnerKeyword = spinnerKeywords.some(k => animationName.includes(k) || animation.includes(k));
138
+ const hasRotation = transform.includes('rotate');
139
+ const isAnimated = animationDuration && animationDuration !== '0s' && !animationDuration.includes('none');
140
+
141
+ return (hasRotation || hasSpinnerKeyword) && isAnimated;
142
+ };
143
+
144
+ const isPulseAnimation = (style) => {
145
+ const animationName = (style.animationName || '').toLowerCase();
146
+ const animation = (style.animation || '').toLowerCase();
147
+ const animationDuration = style.animationDuration || '';
148
+ const opacity = parseFloat(style.opacity) || 1;
149
+
150
+ const pulseKeywords = ['pulse', 'fade', 'loading'];
151
+ const hasPulseKeyword = pulseKeywords.some(k => animationName.includes(k) || animation.includes(k));
152
+ const isAnimated = animationDuration && animationDuration !== '0s' && !animationDuration.includes('none');
153
+ const hasOpacityVariation = opacity < 1;
154
+
155
+ return hasPulseKeyword && isAnimated && hasOpacityVariation;
156
+ };
157
+
158
+ const isSpinnerSize = (width, height) => {
159
+ const maxDim = Math.max(width, height);
160
+ const minDim = Math.min(width, height);
161
+ if (maxDim > MAX_SPINNER_SIZE || minDim < MIN_SPINNER_SIZE) return false;
162
+ const aspectRatio = maxDim / (minDim || 1);
163
+ return aspectRatio <= 2.0;
164
+ };
165
+
166
+ for (const el of allElements) {
167
+ const style = window.getComputedStyle(el);
168
+ // @ts-expect-error
169
+ if (el.offsetParent === null || style.visibility === 'hidden' || style.display === 'none' || style.opacity === '0') {
170
+ continue;
171
+ }
172
+
173
+ // Skip if element has semantic loading attributes
174
+ if (el.hasAttribute('aria-busy') || el.hasAttribute('data-loading') || el.getAttribute('role') === 'progressbar') {
175
+ continue;
176
+ }
177
+
178
+ const width = el.offsetWidth || 0;
179
+ const height = el.offsetHeight || 0;
180
+ const elementId = el.id || `${el.tagName}-${el.className}`;
181
+
182
+ // Check size bounds
183
+ if (!isSpinnerSize(width, height)) {
184
+ continue;
185
+ }
186
+
187
+ // Check for spinner patterns
188
+ let spinnerType = null;
189
+ let reasonCode = null;
190
+
191
+ if (isBorderSpinnerPattern(style)) {
192
+ spinnerType = 'border-spinner';
193
+ reasonCode = 'UI_CSS_SPINNER_DETECTED_BORDER';
194
+ } else if (isRotationAnimation(style)) {
195
+ spinnerType = 'rotation-spinner';
196
+ reasonCode = 'UI_CSS_SPINNER_DETECTED_ROTATION';
197
+ } else if (isPulseAnimation(style)) {
198
+ spinnerType = 'pulse-spinner';
199
+ reasonCode = 'UI_CSS_SPINNER_DETECTED_PULSE';
200
+ }
201
+
202
+ if (spinnerType) {
203
+ result.cssSpinners.push({
204
+ type: spinnerType,
205
+ reasonCode,
206
+ elementId,
207
+ width,
208
+ height
209
+ });
210
+ result.cssSpinnerDetected = true;
211
+ }
212
+ }
213
+
214
+ // Require 2+ independent signals for CONFIRMED CSS spinner feedback
215
+ if (result.cssSpinnerDetected) {
216
+ const hasDisabledButton = result.disabledElements.length > 0;
217
+ const hasPointerEventsDisabled = allElements.some(el => {
218
+ const style = window.getComputedStyle(el);
219
+ return style.pointerEvents === 'none';
220
+ });
221
+
222
+ const corroboratingSignals = [hasDisabledButton, hasPointerEventsDisabled].filter(Boolean).length;
223
+
224
+ if (corroboratingSignals >= 1) {
225
+ result.hasLoadingIndicator = true;
226
+ result.explanation.push(`CSS spinner detected with ${corroboratingSignals} corroborating signal(s)`);
227
+ result.cssSpinnerReasonCode = 'UI_CSS_SPINNER_ACCEPTED_WITH_CORROBORATION';
228
+ } else {
229
+ result.explanation.push('CSS spinner detected but no corroborating signals (SUSPECTED)');
230
+ result.cssSpinnerReasonCode = 'UI_CSS_SPINNER_REJECTED_NO_CORROBORATION';
231
+ }
232
+ }
233
+
86
234
  // VALIDATION INTELLIGENCE v1: Detect visible validation feedback
87
235
  // Check for aria-invalid="true" with visible error text nearby
88
236
  const invalidElements = Array.from(document.querySelectorAll('[aria-invalid="true"]'));
@@ -90,6 +238,7 @@ export class UISignalSensor {
90
238
 
91
239
  for (const invalidEl of invalidElements) {
92
240
  const style = window.getComputedStyle(invalidEl);
241
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
93
242
  const isVisible = invalidEl.offsetParent !== null &&
94
243
  style.visibility !== 'hidden' &&
95
244
  style.display !== 'none' &&
@@ -120,6 +269,7 @@ export class UISignalSensor {
120
269
  const errorText = Array.from(parent.querySelectorAll('[role="alert"], .error, .invalid-feedback'))
121
270
  .find(el => {
122
271
  const elStyle = window.getComputedStyle(el);
272
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
123
273
  return el.offsetParent !== null &&
124
274
  elStyle.visibility !== 'hidden' &&
125
275
  elStyle.display !== 'none' &&
@@ -138,6 +288,7 @@ export class UISignalSensor {
138
288
  const alertRegions = Array.from(document.querySelectorAll('[role="alert"], [role="status"]'));
139
289
  const visibleAlertRegions = alertRegions.filter((el) => {
140
290
  const style = window.getComputedStyle(el);
291
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
141
292
  const isVisible = el.offsetParent !== null &&
142
293
  style.visibility !== 'hidden' &&
143
294
  style.display !== 'none' &&
@@ -149,6 +300,7 @@ export class UISignalSensor {
149
300
  const liveRegions = Array.from(document.querySelectorAll('[aria-live]'));
150
301
  const visibleLiveRegions = liveRegions.filter((el) => {
151
302
  const style = window.getComputedStyle(el);
303
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
152
304
  const isVisible = el.offsetParent !== null &&
153
305
  style.visibility !== 'hidden' &&
154
306
  style.display !== 'none' &&
@@ -176,6 +328,7 @@ export class UISignalSensor {
176
328
  '[role="alert"], [class*="error"], [class*="danger"]'
177
329
  );
178
330
  if (errorMessages.length > 0) {
331
+ // @ts-expect-error - NodeListOf is iterable in browser context
179
332
  for (const elem of errorMessages) {
180
333
  const text = elem.textContent.trim().slice(0, 50);
181
334
  if (text && (text.toLowerCase().includes('error') || text.toLowerCase().includes('fail'))) {
@@ -3,9 +3,15 @@ import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'fs';
3
3
  import { computeExpectationsSummary } from './shared/artifact-manager.js';
4
4
  import { createImpactSummary } from './core/silence-impact.js';
5
5
  import { computeDecisionSnapshot } from './core/decision-snapshot.js';
6
+ import { VERAX_PRODUCT_DEFINITION } from './core/product-definition.js';
7
+ import { ARTIFACT_REGISTRY, getArtifactVersions } from './core/artifacts/registry.js';
8
+ import { generateHumanSummary } from './core/report/human-summary.js';
6
9
 
7
- export function writeScanSummary(projectDir, url, projectType, learnTruth, observeTruth, detectTruth, manifestPath, tracesPath, findingsPath, runDirOpt = null, findingsArray = null) {
8
- const scanDir = runDirOpt ? resolve(runDirOpt) : resolve(projectDir, '.veraxverax', 'scan');
10
+ export async function writeScanSummary(projectDir, url, projectType, learnTruth, observeTruth, detectTruth, manifestPath, tracesPath, findingsPath, runDirOpt, findingsArray = null) {
11
+ if (!runDirOpt) {
12
+ throw new Error('runDirOpt is required');
13
+ }
14
+ const scanDir = resolve(runDirOpt);
9
15
  mkdirSync(scanDir, { recursive: true });
10
16
 
11
17
  // Compute expectations summary from manifest
@@ -26,20 +32,30 @@ export function writeScanSummary(projectDir, url, projectType, learnTruth, obser
26
32
  }
27
33
 
28
34
  // PHASE 6: Compute determinism summary from decisions.json
35
+ // PHASE 21.2: Use HARD verdict from determinism contract
29
36
  let determinismSummary = null;
30
37
  if (runDirOpt && observeTruth?.runId) {
31
38
  const decisionsPath = resolve(runDirOpt, 'decisions.json');
32
39
  if (existsSync(decisionsPath)) {
33
40
  try {
34
41
  const decisions = JSON.parse(readFileSync(decisionsPath, 'utf-8'));
35
- const { DecisionRecorder } = require('./core/determinism-model.js');
42
+ const { DecisionRecorder } = await import('./core/determinism-model.js');
36
43
  const recorder = DecisionRecorder.fromExport(decisions);
37
44
  const summary = recorder.getSummary();
38
45
 
46
+ // PHASE 21.2: Compute HARD verdict from adaptive events
47
+ const { computeDeterminismVerdict } = await import('./core/determinism/contract.js');
48
+ const verdict = computeDeterminismVerdict(recorder);
49
+
39
50
  determinismSummary = {
40
- isDeterministic: summary.isDeterministic,
41
- totalDecisions: summary.totalDecisions,
42
- decisionsByCategory: summary.decisionsByCategory,
51
+ verdict: verdict.verdict, // PHASE 21.2: HARD verdict (DETERMINISTIC or NON_DETERMINISTIC)
52
+ message: verdict.message,
53
+ reasons: verdict.reasons,
54
+ adaptiveEventsCount: verdict.adaptiveEvents.length,
55
+ // Legacy fields for backward compatibility
56
+ isDeterministic: verdict.verdict === 'DETERMINISTIC',
57
+ totalDecisions: summary.total,
58
+ decisionsByCategory: summary.byCategory,
43
59
  decisionsPath: decisionsPath
44
60
  };
45
61
  } catch (error) {
@@ -55,19 +71,37 @@ export function writeScanSummary(projectDir, url, projectType, learnTruth, obser
55
71
  decisionSnapshot = computeDecisionSnapshot(findingsArray, detectTruth, observeTruth, silences);
56
72
  }
57
73
 
74
+ // PHASE 21.10: Generate human summary
75
+ let humanSummary = null;
76
+ if (observeTruth?.runId) {
77
+ try {
78
+ humanSummary = await generateHumanSummary(projectDir, observeTruth.runId);
79
+ } catch {
80
+ // Ignore errors generating human summary
81
+ }
82
+ }
83
+
58
84
  const summary = {
59
85
  version: 1,
86
+ contractVersion: 1, // PHASE 0: Track schema changes
60
87
  scannedAt: new Date().toISOString(),
61
88
  url: url,
62
89
  projectType: projectType,
63
90
  expectationsSummary: expectationsSummary,
91
+ // PHASE 0: Include Evidence Law statement
92
+ evidenceLaw: {
93
+ statement: VERAX_PRODUCT_DEFINITION.evidenceLaw.statement,
94
+ description: VERAX_PRODUCT_DEFINITION.evidenceLaw.definition,
95
+ enforcement: VERAX_PRODUCT_DEFINITION.evidenceLaw.enforcement
96
+ },
64
97
  // PHASE 7: Decision snapshot first (most important for human decision-making)
65
98
  decisionSnapshot: decisionSnapshot,
66
99
  // PHASE 7: Misinterpretation guards (explicit warnings)
67
100
  interpretationGuards: {
68
101
  zeroFindings: 'Zero findings does NOT mean no problems. Check unverified count and confidence level.',
69
102
  deterministicRun: 'Deterministic run does NOT mean correct site. Only means scan was reproducible.',
70
- highSilenceImpact: 'High silence impact does NOT mean failures exist. Only means unknowns affect confidence.'
103
+ highSilenceImpact: 'High silence impact does NOT mean failures exist. Only means unknowns affect confidence.',
104
+ evidenceLaw: 'Not all findings are CONFIRMED. Some may be SUSPECTED (insufficient evidence). Only CONFIRMED findings are actionable.'
71
105
  },
72
106
  truth: {
73
107
  learn: learnTruth,
@@ -85,14 +119,17 @@ export function writeScanSummary(projectDir, url, projectType, learnTruth, obser
85
119
  } : null,
86
120
  // PHASE 6: Add determinism summary
87
121
  determinism: determinismSummary,
122
+ // PHASE 21.10: Add human summary
123
+ humanSummary: humanSummary,
88
124
  paths: {
89
125
  manifest: manifestPath,
90
126
  traces: tracesPath,
91
127
  findings: findingsPath
92
- }
128
+ },
129
+ artifactVersions: getArtifactVersions()
93
130
  };
94
131
 
95
- const summaryPath = resolve(scanDir, 'scan-summary.json');
132
+ const summaryPath = resolve(scanDir, ARTIFACT_REGISTRY.scanSummary.filename);
96
133
  writeFileSync(summaryPath, JSON.stringify(summary, null, 2) + '\n');
97
134
 
98
135
  return {
@@ -13,8 +13,9 @@
13
13
  */
14
14
 
15
15
  import { existsSync, mkdirSync, writeFileSync, appendFileSync } from 'fs';
16
- import { resolve, join } from 'path';
16
+ import { resolve } from 'path';
17
17
  import { randomBytes } from 'crypto';
18
+ import { buildRunArtifactPaths } from '../core/artifacts/registry.js';
18
19
 
19
20
  /**
20
21
  * Generate a unique run ID.
@@ -33,17 +34,19 @@ export function generateRunId() {
33
34
  export function initArtifactPaths(projectRoot, runId = null) {
34
35
  const id = runId || generateRunId();
35
36
  const runDir = resolve(projectRoot, '.verax', 'runs', id);
37
+ const registryPaths = buildRunArtifactPaths(runDir);
36
38
 
37
39
  const paths = {
38
40
  runId: id,
39
41
  runDir,
40
- summary: resolve(runDir, 'summary.json'),
41
- findings: resolve(runDir, 'findings.json'),
42
+ summary: registryPaths.summaryJson,
43
+ findings: registryPaths.findingsJson,
42
44
  expectations: resolve(runDir, 'expectations.json'),
43
- traces: resolve(runDir, 'traces.jsonl'),
44
- evidence: resolve(runDir, 'evidence'),
45
+ traces: registryPaths.tracesJsonl,
46
+ evidence: registryPaths.evidenceDir,
45
47
  flows: resolve(runDir, 'flows'),
46
- artifacts: resolve(projectRoot, '.verax', 'artifacts') // Legacy compat
48
+ artifacts: resolve(projectRoot, '.verax', 'artifacts'), // Legacy compat
49
+ registry: registryPaths.artifactVersions
47
50
  };
48
51
 
49
52
  // Create directories
@@ -11,7 +11,7 @@
11
11
  * - EXHAUSTIVE: 300 seconds, maximum coverage (deep audit)
12
12
  */
13
13
 
14
- import { DEFAULT_SCAN_BUDGET, createScanBudget } from './scan-budget.js';
14
+ import { createScanBudget } from './scan-budget.js';
15
15
 
16
16
  /**
17
17
  * QUICK profile: Fast feedback for development
@@ -107,7 +107,7 @@ export function getActiveBudgetProfile() {
107
107
 
108
108
  /**
109
109
  * Create a scan budget with the active profile applied.
110
- * @returns {ScanBudget} Complete scan budget with profile applied
110
+ * @returns {Object} Complete scan budget with profile applied
111
111
  */
112
112
  export function createScanBudgetWithProfile() {
113
113
  const profile = getActiveBudgetProfile();
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { createHash } from 'crypto';
12
12
  import { readFileSync, existsSync, statSync } from 'fs';
13
- import { resolve, join } from 'path';
13
+ import { resolve } from 'path';
14
14
 
15
15
  const memoryCache = new Map(); // Global in-memory cache
16
16
 
@@ -5,8 +5,7 @@
5
5
  */
6
6
 
7
7
  import { readFileSync, existsSync } from 'fs';
8
- import { resolve, dirname, join } from 'path';
9
- import { fileURLToPath } from 'url';
8
+ import { resolve } from 'path';
10
9
 
11
10
  /**
12
11
  * Default config values
@@ -0,0 +1,204 @@
1
+ /**
2
+ * CSS SPINNER DETECTION RULES
3
+ *
4
+ * Truth boundary for detecting CSS-only loading indicators (spinners)
5
+ * without semantic attributes (aria-busy, data-loading, role).
6
+ *
7
+ * Hard rules encoded as constants and predicates. No prose, only code.
8
+ */
9
+
10
+ /**
11
+ * Reason codes for CSS spinner detection decisions
12
+ */
13
+ export const CSS_SPINNER_REASON_CODES = {
14
+ DETECTED_BORDER_SPINNER: 'UI_CSS_SPINNER_DETECTED_BORDER',
15
+ DETECTED_ROTATION_ANIMATION: 'UI_CSS_SPINNER_DETECTED_ROTATION',
16
+ DETECTED_PULSE_ANIMATION: 'UI_CSS_SPINNER_DETECTED_PULSE',
17
+ REJECTED_DECORATIVE: 'UI_CSS_SPINNER_REJECTED_DECORATIVE',
18
+ REJECTED_TOO_LARGE: 'UI_CSS_SPINNER_REJECTED_TOO_LARGE',
19
+ REJECTED_ALWAYS_PRESENT: 'UI_CSS_SPINNER_REJECTED_ALWAYS_PRESENT',
20
+ REJECTED_NO_CORROBORATION: 'UI_CSS_SPINNER_REJECTED_NO_CORROBORATION',
21
+ REJECTED_TIMING_WINDOW: 'UI_CSS_SPINNER_TIMED',
22
+ ACCEPTED_WITH_CORROBORATION: 'UI_CSS_SPINNER_ACCEPTED_WITH_CORROBORATION'
23
+ };
24
+
25
+ /**
26
+ * Maximum size for a spinner element (in pixels)
27
+ * Larger elements are likely decorative, not loading indicators
28
+ */
29
+ export const MAX_SPINNER_SIZE = 100; // pixels (width or height)
30
+
31
+ /**
32
+ * Minimum size for a spinner element (in pixels)
33
+ * Very small elements might be decorative dots
34
+ */
35
+ export const MIN_SPINNER_SIZE = 8; // pixels
36
+
37
+ /**
38
+ * Timing window for spinner appearance (milliseconds)
39
+ * Spinner must appear within this window after interaction to count as loading feedback
40
+ */
41
+ export const SPINNER_TIMING_WINDOW_MS = 2000; // 2 seconds
42
+
43
+ /**
44
+ * Check if an element has border-based spinner pattern
45
+ * Pattern: border + border-top (or border-left) with different color, creating circular spinner
46
+ *
47
+ * @param {Object} computedStyle - Computed style object
48
+ * @returns {boolean}
49
+ */
50
+ export function isBorderSpinnerPattern(computedStyle) {
51
+ if (!computedStyle) return false;
52
+
53
+ const borderWidth = parseFloat(computedStyle.borderWidth) || 0;
54
+ const borderTopWidth = parseFloat(computedStyle.borderTopWidth) || 0;
55
+ const borderLeftWidth = parseFloat(computedStyle.borderLeftWidth) || 0;
56
+
57
+ // Must have border
58
+ if (borderWidth < 2 && borderTopWidth < 2 && borderLeftWidth < 2) {
59
+ return false;
60
+ }
61
+
62
+ // Check for different border colors (spinner pattern)
63
+ const borderColor = computedStyle.borderColor || '';
64
+ const borderTopColor = computedStyle.borderTopColor || '';
65
+ const borderLeftColor = computedStyle.borderLeftColor || '';
66
+
67
+ // Border-top or border-left should have different color than main border
68
+ const hasDifferentTopColor = borderTopColor && borderColor && borderTopColor !== borderColor && borderTopColor !== 'rgba(0, 0, 0, 0)';
69
+ const hasDifferentLeftColor = borderLeftColor && borderColor && borderLeftColor !== borderColor && borderLeftColor !== 'rgba(0, 0, 0, 0)';
70
+
71
+ // Check for circular shape (border-radius >= 50%)
72
+ const borderRadius = computedStyle.borderRadius || '';
73
+ const isCircular = borderRadius.includes('50%') || borderRadius.includes('999') || parseFloat(borderRadius) > 20;
74
+
75
+ return (hasDifferentTopColor || hasDifferentLeftColor) && isCircular;
76
+ }
77
+
78
+ /**
79
+ * Check if an element has rotation animation
80
+ * Pattern: animation-name includes rotation, or transform: rotate() in keyframes
81
+ *
82
+ * @param {Object} computedStyle - Computed style object
83
+ * @param {Object} element - DOM element
84
+ * @returns {boolean}
85
+ */
86
+ export function isRotationAnimation(computedStyle, element) {
87
+ if (!computedStyle || !element) return false;
88
+
89
+ const animationName = computedStyle.animationName || '';
90
+ const animation = computedStyle.animation || '';
91
+
92
+ // Check animation name for spinner-related keywords (but don't rely only on names)
93
+ const spinnerKeywords = ['spin', 'rotate', 'loading', 'loader'];
94
+ const hasSpinnerKeyword = spinnerKeywords.some(keyword =>
95
+ animationName.toLowerCase().includes(keyword) || animation.toLowerCase().includes(keyword)
96
+ );
97
+
98
+ // Check for transform: rotate() in computed style (indicates rotation)
99
+ const transform = computedStyle.transform || '';
100
+ const hasRotation = transform.includes('rotate');
101
+
102
+ // Check for animation-duration (must be animated)
103
+ const animationDuration = computedStyle.animationDuration || '';
104
+ const isAnimated = animationDuration && animationDuration !== '0s' && !animationDuration.includes('none');
105
+
106
+ // Must have rotation AND be animated
107
+ return (hasRotation || hasSpinnerKeyword) && isAnimated;
108
+ }
109
+
110
+ /**
111
+ * Check if an element has pulse animation (opacity or scale pulsing)
112
+ * Pattern: animation that changes opacity or scale repeatedly
113
+ *
114
+ * @param {Object} computedStyle - Computed style object
115
+ * @returns {boolean}
116
+ */
117
+ export function isPulseAnimation(computedStyle) {
118
+ if (!computedStyle) return false;
119
+
120
+ const animationName = computedStyle.animationName || '';
121
+ const animation = computedStyle.animation || '';
122
+
123
+ // Check for pulse-related keywords
124
+ const pulseKeywords = ['pulse', 'fade', 'loading'];
125
+ const hasPulseKeyword = pulseKeywords.some(keyword =>
126
+ animationName.toLowerCase().includes(keyword) || animation.toLowerCase().includes(keyword)
127
+ );
128
+
129
+ // Check for animation-duration
130
+ const animationDuration = computedStyle.animationDuration || '';
131
+ const isAnimated = animationDuration && animationDuration !== '0s' && !animationDuration.includes('none');
132
+
133
+ // Check for opacity animation (common in pulse patterns)
134
+ const opacity = parseFloat(computedStyle.opacity) || 1;
135
+ const hasOpacityVariation = opacity < 1; // Partially transparent suggests pulsing
136
+
137
+ return hasPulseKeyword && isAnimated && hasOpacityVariation;
138
+ }
139
+
140
+ /**
141
+ * Check if element size is within spinner bounds
142
+ *
143
+ * @param {number} width - Element width in pixels
144
+ * @param {number} height - Element height in pixels
145
+ * @returns {boolean}
146
+ */
147
+ export function isSpinnerSize(width, height) {
148
+ const maxDim = Math.max(width, height);
149
+ const minDim = Math.min(width, height);
150
+
151
+ // Must be within size bounds
152
+ if (maxDim > MAX_SPINNER_SIZE || minDim < MIN_SPINNER_SIZE) {
153
+ return false;
154
+ }
155
+
156
+ // Should be roughly square (spinners are usually circular/square)
157
+ const aspectRatio = maxDim / (minDim || 1);
158
+ return aspectRatio <= 2.0; // Allow some rectangular tolerance
159
+ }
160
+
161
+ /**
162
+ * Check if spinner appeared within timing window
163
+ *
164
+ * @param {number} appearanceTime - Time when spinner appeared (ms since epoch)
165
+ * @param {number} interactionTime - Time when interaction occurred (ms since epoch)
166
+ * @returns {boolean}
167
+ */
168
+ export function isWithinTimingWindow(appearanceTime, interactionTime) {
169
+ if (!appearanceTime || !interactionTime) return false;
170
+ const timeDiff = appearanceTime - interactionTime;
171
+ return timeDiff >= 0 && timeDiff <= SPINNER_TIMING_WINDOW_MS;
172
+ }
173
+
174
+ /**
175
+ * Check if element is likely decorative (always present, large, not near interaction)
176
+ *
177
+ * @param {Object} element - DOM element
178
+ * @param {Object} computedStyle - Computed style object
179
+ * @param {boolean} wasPresentBefore - Whether element existed before interaction
180
+ * @returns {boolean}
181
+ */
182
+ export function isDecorativeElement(element, computedStyle, wasPresentBefore) {
183
+ if (!element || !computedStyle) return false;
184
+
185
+ // If element was always present, likely decorative
186
+ if (wasPresentBefore) {
187
+ return true;
188
+ }
189
+
190
+ // Check size
191
+ const width = element.offsetWidth || 0;
192
+ const height = element.offsetHeight || 0;
193
+ if (!isSpinnerSize(width, height)) {
194
+ return true;
195
+ }
196
+
197
+ // Check if element is very large (likely decorative)
198
+ if (width > MAX_SPINNER_SIZE || height > MAX_SPINNER_SIZE) {
199
+ return true;
200
+ }
201
+
202
+ return false;
203
+ }
204
+
@@ -88,23 +88,27 @@ export function createExamplePath(originalPath) {
88
88
  }
89
89
 
90
90
  let examplePath = originalPath;
91
- let isDynamic = false;
91
+ let _isDynamic = false;
92
+ const parameters = new Set();
92
93
 
93
94
  // Replace React/Vue :param
94
95
  examplePath = examplePath.replace(/:(\w+)/g, (match, paramName) => {
95
- isDynamic = true;
96
+ _isDynamic = true;
97
+ parameters.add(paramName);
96
98
  return getExampleValue(paramName);
97
99
  });
98
100
 
99
101
  // Replace Next.js [param]
100
102
  examplePath = examplePath.replace(/\[(\w+)\]/g, (match, paramName) => {
101
- isDynamic = true;
103
+ _isDynamic = true;
104
+ parameters.add(paramName);
102
105
  return getExampleValue(paramName);
103
106
  });
104
107
 
105
108
  // Replace template ${param}
106
109
  examplePath = examplePath.replace(/\$\{(\w+)\}/g, (match, paramName) => {
107
- isDynamic = true;
110
+ _isDynamic = true;
111
+ parameters.add(paramName);
108
112
  return getExampleValue(paramName);
109
113
  });
110
114
 
@@ -112,7 +116,8 @@ export function createExamplePath(originalPath) {
112
116
  examplePath,
113
117
  originalPattern: originalPath,
114
118
  isDynamic: true,
115
- exampleExecution: true
119
+ exampleExecution: true,
120
+ parameters: Array.from(parameters)
116
121
  };
117
122
  }
118
123
 
@@ -210,7 +215,8 @@ export function normalizeTemplateLiteral(template) {
210
215
  originalPattern: result.originalTarget,
211
216
  examplePath: result.exampleTarget,
212
217
  isDynamic: true,
213
- exampleExecution: true
218
+ exampleExecution: true,
219
+ parameters: result.parameters
214
220
  };
215
221
  }
216
222