@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.
- package/README.md +14 -18
- package/bin/verax.js +7 -0
- package/package.json +3 -3
- package/src/cli/commands/baseline.js +104 -0
- package/src/cli/commands/default.js +79 -25
- package/src/cli/commands/ga.js +243 -0
- package/src/cli/commands/gates.js +95 -0
- package/src/cli/commands/inspect.js +131 -2
- package/src/cli/commands/release-check.js +213 -0
- package/src/cli/commands/run.js +246 -35
- package/src/cli/commands/security-check.js +211 -0
- package/src/cli/commands/truth.js +114 -0
- package/src/cli/entry.js +304 -67
- package/src/cli/util/angular-component-extractor.js +179 -0
- package/src/cli/util/angular-navigation-detector.js +141 -0
- package/src/cli/util/angular-network-detector.js +161 -0
- package/src/cli/util/angular-state-detector.js +162 -0
- package/src/cli/util/ast-interactive-detector.js +546 -0
- package/src/cli/util/ast-network-detector.js +603 -0
- package/src/cli/util/ast-usestate-detector.js +602 -0
- package/src/cli/util/bootstrap-guard.js +86 -0
- package/src/cli/util/determinism-runner.js +123 -0
- package/src/cli/util/determinism-writer.js +129 -0
- package/src/cli/util/env-url.js +4 -0
- package/src/cli/util/expectation-extractor.js +369 -73
- package/src/cli/util/findings-writer.js +126 -16
- package/src/cli/util/learn-writer.js +3 -1
- package/src/cli/util/observe-writer.js +3 -1
- package/src/cli/util/paths.js +3 -12
- package/src/cli/util/project-discovery.js +3 -0
- package/src/cli/util/project-writer.js +3 -1
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +1 -0
- package/src/cli/util/svelte-navigation-detector.js +163 -0
- package/src/cli/util/svelte-network-detector.js +80 -0
- package/src/cli/util/svelte-sfc-extractor.js +147 -0
- package/src/cli/util/svelte-state-detector.js +243 -0
- package/src/cli/util/vue-navigation-detector.js +177 -0
- package/src/cli/util/vue-sfc-extractor.js +162 -0
- package/src/cli/util/vue-state-detector.js +215 -0
- package/src/verax/cli/finding-explainer.js +56 -3
- package/src/verax/core/artifacts/registry.js +154 -0
- package/src/verax/core/artifacts/verifier.js +980 -0
- package/src/verax/core/baseline/baseline.enforcer.js +137 -0
- package/src/verax/core/baseline/baseline.snapshot.js +231 -0
- package/src/verax/core/capabilities/gates.js +499 -0
- package/src/verax/core/capabilities/registry.js +475 -0
- package/src/verax/core/confidence/confidence-compute.js +137 -0
- package/src/verax/core/confidence/confidence-invariants.js +234 -0
- package/src/verax/core/confidence/confidence-report-writer.js +112 -0
- package/src/verax/core/confidence/confidence-weights.js +44 -0
- package/src/verax/core/confidence/confidence.defaults.js +65 -0
- package/src/verax/core/confidence/confidence.loader.js +79 -0
- package/src/verax/core/confidence/confidence.schema.js +94 -0
- package/src/verax/core/confidence-engine-refactor.js +484 -0
- package/src/verax/core/confidence-engine.js +486 -0
- package/src/verax/core/confidence-engine.js.backup +471 -0
- package/src/verax/core/contracts/index.js +29 -0
- package/src/verax/core/contracts/types.js +185 -0
- package/src/verax/core/contracts/validators.js +381 -0
- package/src/verax/core/decision-snapshot.js +30 -3
- package/src/verax/core/decisions/decision.trace.js +276 -0
- package/src/verax/core/determinism/contract-writer.js +89 -0
- package/src/verax/core/determinism/contract.js +139 -0
- package/src/verax/core/determinism/diff.js +364 -0
- package/src/verax/core/determinism/engine.js +221 -0
- package/src/verax/core/determinism/finding-identity.js +148 -0
- package/src/verax/core/determinism/normalize.js +438 -0
- package/src/verax/core/determinism/report-writer.js +92 -0
- package/src/verax/core/determinism/run-fingerprint.js +118 -0
- package/src/verax/core/dynamic-route-intelligence.js +528 -0
- package/src/verax/core/evidence/evidence-capture-service.js +307 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
- package/src/verax/core/evidence-builder.js +487 -0
- package/src/verax/core/execution-mode-context.js +77 -0
- package/src/verax/core/execution-mode-detector.js +190 -0
- package/src/verax/core/failures/exit-codes.js +86 -0
- package/src/verax/core/failures/failure-summary.js +76 -0
- package/src/verax/core/failures/failure.factory.js +225 -0
- package/src/verax/core/failures/failure.ledger.js +132 -0
- package/src/verax/core/failures/failure.types.js +196 -0
- package/src/verax/core/failures/index.js +10 -0
- package/src/verax/core/ga/ga-report-writer.js +43 -0
- package/src/verax/core/ga/ga.artifact.js +49 -0
- package/src/verax/core/ga/ga.contract.js +434 -0
- package/src/verax/core/ga/ga.enforcer.js +86 -0
- package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
- package/src/verax/core/guardrails/policy.defaults.js +210 -0
- package/src/verax/core/guardrails/policy.loader.js +83 -0
- package/src/verax/core/guardrails/policy.schema.js +110 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
- package/src/verax/core/guardrails-engine.js +505 -0
- package/src/verax/core/observe/run-timeline.js +316 -0
- package/src/verax/core/perf/perf.contract.js +186 -0
- package/src/verax/core/perf/perf.display.js +65 -0
- package/src/verax/core/perf/perf.enforcer.js +91 -0
- package/src/verax/core/perf/perf.monitor.js +209 -0
- package/src/verax/core/perf/perf.report.js +198 -0
- package/src/verax/core/pipeline-tracker.js +238 -0
- package/src/verax/core/product-definition.js +127 -0
- package/src/verax/core/release/provenance.builder.js +271 -0
- package/src/verax/core/release/release-report-writer.js +40 -0
- package/src/verax/core/release/release.enforcer.js +159 -0
- package/src/verax/core/release/reproducibility.check.js +221 -0
- package/src/verax/core/release/sbom.builder.js +283 -0
- package/src/verax/core/report/cross-index.js +192 -0
- package/src/verax/core/report/human-summary.js +222 -0
- package/src/verax/core/route-intelligence.js +419 -0
- package/src/verax/core/security/secrets.scan.js +326 -0
- package/src/verax/core/security/security-report.js +50 -0
- package/src/verax/core/security/security.enforcer.js +124 -0
- package/src/verax/core/security/supplychain.defaults.json +38 -0
- package/src/verax/core/security/supplychain.policy.js +326 -0
- package/src/verax/core/security/vuln.scan.js +265 -0
- package/src/verax/core/truth/truth.certificate.js +250 -0
- package/src/verax/core/ui-feedback-intelligence.js +515 -0
- package/src/verax/detect/confidence-engine.js +628 -40
- package/src/verax/detect/confidence-helper.js +33 -0
- package/src/verax/detect/detection-engine.js +18 -1
- package/src/verax/detect/dynamic-route-findings.js +335 -0
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +3 -1
- package/src/verax/detect/findings-writer.js +141 -5
- package/src/verax/detect/index.js +229 -5
- package/src/verax/detect/journey-stall-detector.js +558 -0
- package/src/verax/detect/route-findings.js +218 -0
- package/src/verax/detect/ui-feedback-findings.js +207 -0
- package/src/verax/detect/verdict-engine.js +57 -3
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/index.js +413 -45
- package/src/verax/learn/action-contract-extractor.js +682 -64
- package/src/verax/learn/route-validator.js +4 -1
- package/src/verax/observe/index.js +88 -843
- package/src/verax/observe/interaction-runner.js +25 -8
- package/src/verax/observe/observe-context.js +205 -0
- package/src/verax/observe/observe-helpers.js +191 -0
- package/src/verax/observe/observe-runner.js +226 -0
- package/src/verax/observe/observers/budget-observer.js +185 -0
- package/src/verax/observe/observers/console-observer.js +102 -0
- package/src/verax/observe/observers/coverage-observer.js +107 -0
- package/src/verax/observe/observers/interaction-observer.js +471 -0
- package/src/verax/observe/observers/navigation-observer.js +132 -0
- package/src/verax/observe/observers/network-observer.js +87 -0
- package/src/verax/observe/observers/safety-observer.js +82 -0
- package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
- package/src/verax/observe/ui-feedback-detector.js +742 -0
- package/src/verax/observe/ui-signal-sensor.js +148 -2
- package/src/verax/scan-summary-writer.js +42 -8
- package/src/verax/shared/artifact-manager.js +8 -5
- package/src/verax/shared/css-spinner-rules.js +204 -0
- 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 } =
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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,
|
|
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:
|
|
41
|
-
findings:
|
|
42
|
+
summary: registryPaths.summaryJson,
|
|
43
|
+
findings: registryPaths.findingsJson,
|
|
42
44
|
expectations: resolve(runDir, 'expectations.json'),
|
|
43
|
-
traces:
|
|
44
|
-
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
|
+
|