@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
@@ -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,
@@ -85,6 +101,136 @@ export class UISignalSensor {
85
101
  );
86
102
  }
87
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
+
88
234
  // VALIDATION INTELLIGENCE v1: Detect visible validation feedback
89
235
  // Check for aria-invalid="true" with visible error text nearby
90
236
  const invalidElements = Array.from(document.querySelectorAll('[aria-invalid="true"]'));
@@ -3,8 +3,11 @@ 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, findingsArray = null) {
10
+ export async function writeScanSummary(projectDir, url, projectType, learnTruth, observeTruth, detectTruth, manifestPath, tracesPath, findingsPath, runDirOpt, findingsArray = null) {
8
11
  if (!runDirOpt) {
9
12
  throw new Error('runDirOpt is required');
10
13
  }
@@ -29,20 +32,30 @@ export function writeScanSummary(projectDir, url, projectType, learnTruth, obser
29
32
  }
30
33
 
31
34
  // PHASE 6: Compute determinism summary from decisions.json
35
+ // PHASE 21.2: Use HARD verdict from determinism contract
32
36
  let determinismSummary = null;
33
37
  if (runDirOpt && observeTruth?.runId) {
34
38
  const decisionsPath = resolve(runDirOpt, 'decisions.json');
35
39
  if (existsSync(decisionsPath)) {
36
40
  try {
37
41
  const decisions = JSON.parse(readFileSync(decisionsPath, 'utf-8'));
38
- const { DecisionRecorder } = require('./core/determinism-model.js');
42
+ const { DecisionRecorder } = await import('./core/determinism-model.js');
39
43
  const recorder = DecisionRecorder.fromExport(decisions);
40
44
  const summary = recorder.getSummary();
41
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
+
42
50
  determinismSummary = {
43
- isDeterministic: summary.isDeterministic,
44
- totalDecisions: summary.totalDecisions,
45
- 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,
46
59
  decisionsPath: decisionsPath
47
60
  };
48
61
  } catch (error) {
@@ -58,19 +71,37 @@ export function writeScanSummary(projectDir, url, projectType, learnTruth, obser
58
71
  decisionSnapshot = computeDecisionSnapshot(findingsArray, detectTruth, observeTruth, silences);
59
72
  }
60
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
+
61
84
  const summary = {
62
85
  version: 1,
86
+ contractVersion: 1, // PHASE 0: Track schema changes
63
87
  scannedAt: new Date().toISOString(),
64
88
  url: url,
65
89
  projectType: projectType,
66
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
+ },
67
97
  // PHASE 7: Decision snapshot first (most important for human decision-making)
68
98
  decisionSnapshot: decisionSnapshot,
69
99
  // PHASE 7: Misinterpretation guards (explicit warnings)
70
100
  interpretationGuards: {
71
101
  zeroFindings: 'Zero findings does NOT mean no problems. Check unverified count and confidence level.',
72
102
  deterministicRun: 'Deterministic run does NOT mean correct site. Only means scan was reproducible.',
73
- 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.'
74
105
  },
75
106
  truth: {
76
107
  learn: learnTruth,
@@ -88,14 +119,17 @@ export function writeScanSummary(projectDir, url, projectType, learnTruth, obser
88
119
  } : null,
89
120
  // PHASE 6: Add determinism summary
90
121
  determinism: determinismSummary,
122
+ // PHASE 21.10: Add human summary
123
+ humanSummary: humanSummary,
91
124
  paths: {
92
125
  manifest: manifestPath,
93
126
  traces: tracesPath,
94
127
  findings: findingsPath
95
- }
128
+ },
129
+ artifactVersions: getArtifactVersions()
96
130
  };
97
131
 
98
- const summaryPath = resolve(scanDir, 'scan-summary.json');
132
+ const summaryPath = resolve(scanDir, ARTIFACT_REGISTRY.scanSummary.filename);
99
133
  writeFileSync(summaryPath, JSON.stringify(summary, null, 2) + '\n');
100
134
 
101
135
  return {
@@ -15,6 +15,7 @@
15
15
  import { existsSync, mkdirSync, writeFileSync, appendFileSync } from 'fs';
16
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
@@ -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
+