@veraxhq/verax 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -20
- package/bin/verax.js +11 -18
- package/package.json +28 -7
- package/src/cli/commands/baseline.js +1 -2
- package/src/cli/commands/default.js +72 -81
- package/src/cli/commands/doctor.js +29 -0
- package/src/cli/commands/ga.js +3 -0
- package/src/cli/commands/gates.js +1 -1
- package/src/cli/commands/inspect.js +6 -133
- package/src/cli/commands/release-check.js +2 -0
- package/src/cli/commands/run.js +74 -246
- package/src/cli/commands/security-check.js +2 -1
- package/src/cli/commands/truth.js +0 -1
- package/src/cli/entry.js +82 -309
- package/src/cli/util/angular-component-extractor.js +2 -2
- package/src/cli/util/angular-navigation-detector.js +2 -2
- package/src/cli/util/ast-interactive-detector.js +4 -6
- package/src/cli/util/ast-network-detector.js +3 -3
- package/src/cli/util/ast-promise-extractor.js +581 -0
- package/src/cli/util/ast-usestate-detector.js +3 -3
- package/src/cli/util/atomic-write.js +12 -1
- package/src/cli/util/console-reporter.js +72 -0
- package/src/cli/util/detection-engine.js +105 -41
- package/src/cli/util/determinism-runner.js +2 -1
- package/src/cli/util/determinism-writer.js +1 -1
- package/src/cli/util/digest-engine.js +359 -0
- package/src/cli/util/dom-diff.js +226 -0
- package/src/cli/util/env-url.js +0 -4
- package/src/cli/util/evidence-engine.js +287 -0
- package/src/cli/util/expectation-extractor.js +217 -367
- package/src/cli/util/findings-writer.js +19 -126
- package/src/cli/util/framework-detector.js +572 -0
- package/src/cli/util/idgen.js +1 -1
- package/src/cli/util/interaction-planner.js +529 -0
- package/src/cli/util/learn-writer.js +2 -2
- package/src/cli/util/ledger-writer.js +110 -0
- package/src/cli/util/monorepo-resolver.js +162 -0
- package/src/cli/util/observation-engine.js +127 -278
- package/src/cli/util/observe-writer.js +2 -2
- package/src/cli/util/paths.js +12 -3
- package/src/cli/util/project-discovery.js +284 -3
- package/src/cli/util/project-writer.js +2 -2
- package/src/cli/util/run-id.js +23 -27
- package/src/cli/util/run-result.js +778 -0
- package/src/cli/util/selector-resolver.js +235 -0
- package/src/cli/util/summary-writer.js +2 -1
- package/src/cli/util/svelte-navigation-detector.js +3 -3
- package/src/cli/util/svelte-sfc-extractor.js +0 -1
- package/src/cli/util/svelte-state-detector.js +1 -2
- package/src/cli/util/trust-activation-integration.js +496 -0
- package/src/cli/util/trust-activation-wrapper.js +85 -0
- package/src/cli/util/trust-integration-hooks.js +164 -0
- package/src/cli/util/types.js +153 -0
- package/src/cli/util/url-validation.js +40 -0
- package/src/cli/util/vue-navigation-detector.js +4 -3
- package/src/cli/util/vue-sfc-extractor.js +1 -2
- package/src/cli/util/vue-state-detector.js +1 -1
- package/src/types/fs-augment.d.ts +23 -0
- package/src/types/global.d.ts +137 -0
- package/src/types/internal-types.d.ts +35 -0
- package/src/verax/cli/finding-explainer.js +3 -56
- package/src/verax/cli/init.js +4 -18
- package/src/verax/core/action-classifier.js +4 -3
- package/src/verax/core/artifacts/registry.js +0 -15
- package/src/verax/core/artifacts/verifier.js +18 -8
- package/src/verax/core/baseline/baseline.snapshot.js +2 -0
- package/src/verax/core/capabilities/gates.js +7 -1
- package/src/verax/core/confidence/confidence-compute.js +14 -7
- package/src/verax/core/confidence/confidence.loader.js +1 -0
- package/src/verax/core/confidence-engine-refactor.js +8 -3
- package/src/verax/core/confidence-engine.js +162 -23
- package/src/verax/core/contracts/types.js +1 -0
- package/src/verax/core/contracts/validators.js +79 -4
- package/src/verax/core/decision-snapshot.js +3 -30
- package/src/verax/core/decisions/decision.trace.js +2 -0
- package/src/verax/core/determinism/contract-writer.js +2 -2
- package/src/verax/core/determinism/contract.js +1 -1
- package/src/verax/core/determinism/diff.js +42 -1
- package/src/verax/core/determinism/engine.js +7 -6
- package/src/verax/core/determinism/finding-identity.js +3 -2
- package/src/verax/core/determinism/normalize.js +32 -4
- package/src/verax/core/determinism/report-writer.js +1 -0
- package/src/verax/core/determinism/run-fingerprint.js +7 -2
- package/src/verax/core/dynamic-route-intelligence.js +8 -7
- package/src/verax/core/evidence/evidence-capture-service.js +1 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +2 -1
- package/src/verax/core/evidence-builder.js +2 -2
- package/src/verax/core/execution-mode-context.js +1 -1
- package/src/verax/core/execution-mode-detector.js +5 -3
- package/src/verax/core/failures/exit-codes.js +39 -37
- package/src/verax/core/failures/failure-summary.js +1 -1
- package/src/verax/core/failures/failure.factory.js +3 -3
- package/src/verax/core/failures/failure.ledger.js +3 -2
- package/src/verax/core/ga/ga.artifact.js +1 -1
- package/src/verax/core/ga/ga.contract.js +3 -2
- package/src/verax/core/ga/ga.enforcer.js +1 -0
- package/src/verax/core/guardrails/policy.loader.js +1 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +1 -1
- package/src/verax/core/guardrails-engine.js +2 -2
- package/src/verax/core/incremental-store.js +1 -0
- package/src/verax/core/integrity/budget.js +138 -0
- package/src/verax/core/integrity/determinism.js +342 -0
- package/src/verax/core/integrity/integrity.js +208 -0
- package/src/verax/core/integrity/poisoning.js +108 -0
- package/src/verax/core/integrity/transaction.js +140 -0
- package/src/verax/core/observe/run-timeline.js +2 -0
- package/src/verax/core/perf/perf.report.js +2 -0
- package/src/verax/core/pipeline-tracker.js +5 -0
- package/src/verax/core/release/provenance.builder.js +73 -214
- package/src/verax/core/release/release.enforcer.js +14 -9
- package/src/verax/core/release/reproducibility.check.js +1 -0
- package/src/verax/core/release/sbom.builder.js +32 -23
- package/src/verax/core/replay-validator.js +2 -0
- package/src/verax/core/replay.js +4 -0
- package/src/verax/core/report/cross-index.js +6 -3
- package/src/verax/core/report/human-summary.js +141 -1
- package/src/verax/core/route-intelligence.js +4 -3
- package/src/verax/core/run-id.js +6 -3
- package/src/verax/core/run-manifest.js +4 -3
- package/src/verax/core/security/secrets.scan.js +10 -7
- package/src/verax/core/security/security.enforcer.js +4 -0
- package/src/verax/core/security/supplychain.policy.js +9 -1
- package/src/verax/core/security/vuln.scan.js +2 -2
- package/src/verax/core/truth/truth.certificate.js +3 -1
- package/src/verax/core/ui-feedback-intelligence.js +12 -46
- package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
- package/src/verax/detect/confidence-engine.js +100 -660
- package/src/verax/detect/confidence-helper.js +1 -0
- package/src/verax/detect/detection-engine.js +1 -18
- package/src/verax/detect/dynamic-route-findings.js +17 -14
- package/src/verax/detect/expectation-chain-detector.js +1 -1
- package/src/verax/detect/expectation-model.js +3 -5
- package/src/verax/detect/failure-cause-inference.js +293 -0
- package/src/verax/detect/findings-writer.js +126 -166
- package/src/verax/detect/flow-detector.js +2 -2
- package/src/verax/detect/form-silent-failure.js +98 -0
- package/src/verax/detect/index.js +51 -234
- package/src/verax/detect/invariants-enforcer.js +147 -0
- package/src/verax/detect/journey-stall-detector.js +4 -4
- package/src/verax/detect/navigation-silent-failure.js +82 -0
- package/src/verax/detect/problem-aggregator.js +361 -0
- package/src/verax/detect/route-findings.js +7 -6
- package/src/verax/detect/summary-writer.js +477 -0
- package/src/verax/detect/test-failure-cause-inference.js +314 -0
- package/src/verax/detect/ui-feedback-findings.js +18 -18
- package/src/verax/detect/verdict-engine.js +3 -57
- package/src/verax/detect/view-switch-correlator.js +2 -2
- package/src/verax/flow/flow-engine.js +2 -1
- package/src/verax/flow/flow-spec.js +0 -6
- package/src/verax/index.js +48 -412
- package/src/verax/intel/ts-program.js +1 -0
- package/src/verax/intel/vue-navigation-extractor.js +3 -0
- package/src/verax/learn/action-contract-extractor.js +67 -682
- package/src/verax/learn/ast-contract-extractor.js +1 -1
- package/src/verax/learn/flow-extractor.js +1 -0
- package/src/verax/learn/project-detector.js +5 -0
- package/src/verax/learn/react-router-extractor.js +2 -0
- package/src/verax/learn/route-validator.js +1 -4
- package/src/verax/learn/source-instrumenter.js +1 -0
- package/src/verax/learn/state-extractor.js +2 -1
- package/src/verax/learn/static-extractor.js +1 -0
- package/src/verax/observe/coverage-gaps.js +132 -0
- package/src/verax/observe/expectation-handler.js +126 -0
- package/src/verax/observe/incremental-skip.js +46 -0
- package/src/verax/observe/index.js +735 -84
- package/src/verax/observe/interaction-executor.js +192 -0
- package/src/verax/observe/interaction-runner.js +782 -530
- package/src/verax/observe/network-firewall.js +86 -0
- package/src/verax/observe/observation-builder.js +169 -0
- package/src/verax/observe/observe-context.js +1 -1
- package/src/verax/observe/observe-helpers.js +2 -1
- package/src/verax/observe/observe-runner.js +28 -24
- package/src/verax/observe/observers/budget-observer.js +3 -3
- package/src/verax/observe/observers/console-observer.js +4 -4
- package/src/verax/observe/observers/coverage-observer.js +4 -4
- package/src/verax/observe/observers/interaction-observer.js +3 -3
- package/src/verax/observe/observers/navigation-observer.js +4 -4
- package/src/verax/observe/observers/network-observer.js +4 -4
- package/src/verax/observe/observers/safety-observer.js +1 -1
- package/src/verax/observe/observers/ui-feedback-observer.js +4 -4
- package/src/verax/observe/page-traversal.js +138 -0
- package/src/verax/observe/snapshot-ops.js +94 -0
- package/src/verax/observe/ui-signal-sensor.js +2 -148
- package/src/verax/scan-summary-writer.js +10 -42
- package/src/verax/shared/artifact-manager.js +30 -13
- package/src/verax/shared/caching.js +1 -0
- package/src/verax/shared/expectation-tracker.js +1 -0
- package/src/verax/shared/zip-artifacts.js +6 -0
- package/src/verax/core/confidence-engine.js.backup +0 -471
- package/src/verax/shared/config-loader.js +0 -169
- /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
|
@@ -14,6 +14,7 @@ import { computeConfidenceForFinding } from '../core/confidence-engine.js';
|
|
|
14
14
|
* @returns {Object} Finding with unified confidence fields
|
|
15
15
|
*/
|
|
16
16
|
export function addUnifiedConfidence(finding, params) {
|
|
17
|
+
// @ts-expect-error - Optional params structure
|
|
17
18
|
const unifiedConfidence = computeConfidenceForFinding({
|
|
18
19
|
findingType: finding.type || 'unknown',
|
|
19
20
|
expectation: params.expectation || null,
|
|
@@ -2,19 +2,11 @@
|
|
|
2
2
|
* Detection Engine: Core of VERAX
|
|
3
3
|
* Compares learn.json and observe.json to produce evidence-backed findings
|
|
4
4
|
* with deterministic classification and confidence calculation
|
|
5
|
-
*
|
|
6
|
-
* PHASE 11: EXPECTATION CONTINUITY
|
|
7
|
-
* - Detects journey-level stalls (individual steps OK, overall journey stalls)
|
|
8
|
-
* - Tracks interaction sequences and infers expected progression signals
|
|
9
|
-
* - Emits "journey-stall-silent-failure" findings with high-confidence context
|
|
10
5
|
*/
|
|
11
6
|
|
|
12
|
-
import JourneyStallDetector from './journey-stall-detector.js';
|
|
13
|
-
|
|
14
7
|
class DetectionEngine {
|
|
15
8
|
constructor(options = {}) {
|
|
16
9
|
this.options = options;
|
|
17
|
-
this.journeyStallDetector = new JourneyStallDetector(options.journeyStall || {});
|
|
18
10
|
}
|
|
19
11
|
|
|
20
12
|
/**
|
|
@@ -30,7 +22,6 @@ class DetectionEngine {
|
|
|
30
22
|
|
|
31
23
|
const expectations = learnData.expectations || [];
|
|
32
24
|
const observations = observeData.observations || [];
|
|
33
|
-
const traces = observeData.traces || [];
|
|
34
25
|
|
|
35
26
|
// Index observations for fast lookup
|
|
36
27
|
const observationMap = this._indexObservations(observations);
|
|
@@ -40,10 +31,6 @@ class DetectionEngine {
|
|
|
40
31
|
return this._classifyExpectation(expectation, observationMap, observations);
|
|
41
32
|
});
|
|
42
33
|
|
|
43
|
-
// PHASE 11: Detect journey-level stalls
|
|
44
|
-
const journeyStallFindings = this.journeyStallDetector.detectStalls(traces);
|
|
45
|
-
findings.push(...journeyStallFindings);
|
|
46
|
-
|
|
47
34
|
// Calculate stats
|
|
48
35
|
const stats = this._calculateStats(findings);
|
|
49
36
|
|
|
@@ -51,11 +38,7 @@ class DetectionEngine {
|
|
|
51
38
|
findings,
|
|
52
39
|
stats,
|
|
53
40
|
detectedAt: new Date().toISOString(),
|
|
54
|
-
version: '1.
|
|
55
|
-
phaseFeatures: {
|
|
56
|
-
expectationContinuity: true,
|
|
57
|
-
journeyStallDetection: true
|
|
58
|
-
}
|
|
41
|
+
version: '1.0.0'
|
|
59
42
|
};
|
|
60
43
|
}
|
|
61
44
|
|
|
@@ -24,10 +24,11 @@ import { applyGuardrails } from '../core/guardrails-engine.js';
|
|
|
24
24
|
*
|
|
25
25
|
* @param {Array} traces - Interaction traces
|
|
26
26
|
* @param {Object} manifest - Project manifest with routes and expectations
|
|
27
|
-
* @param {Array}
|
|
27
|
+
* @param {Array} _findings - Findings array to append to
|
|
28
|
+
* @ts-expect-error - JSDoc param documented but unused
|
|
28
29
|
* @returns {Object} { findings: Array, skips: Array }
|
|
29
30
|
*/
|
|
30
|
-
export function detectDynamicRouteFindings(traces, manifest,
|
|
31
|
+
export function detectDynamicRouteFindings(traces, manifest, _findings) {
|
|
31
32
|
const dynamicRouteFindings = [];
|
|
32
33
|
const skips = [];
|
|
33
34
|
|
|
@@ -117,6 +118,7 @@ export function detectDynamicRouteFindings(traces, manifest, findings) {
|
|
|
117
118
|
|
|
118
119
|
// Build evidence
|
|
119
120
|
const evidence = buildDynamicRouteEvidence(expectation, matchingRoute, correlation, trace);
|
|
121
|
+
const classificationReason = classification.reason || correlation.reason || null;
|
|
120
122
|
|
|
121
123
|
// PHASE 14: Evidence Law - require sufficient evidence for CONFIRMED
|
|
122
124
|
const hasSufficientEvidence = evidence.beforeAfter.beforeUrl &&
|
|
@@ -126,6 +128,14 @@ export function detectDynamicRouteFindings(traces, manifest, findings) {
|
|
|
126
128
|
evidence.signals.uiFeedback !== 'FEEDBACK_MISSING' ||
|
|
127
129
|
evidence.signals.domChanged);
|
|
128
130
|
|
|
131
|
+
// Determine finding type early (before use in confidence call)
|
|
132
|
+
let findingType = 'dynamic_route_silent_failure';
|
|
133
|
+
if (correlation.verdict === ROUTE_VERDICT.ROUTE_MISMATCH) {
|
|
134
|
+
findingType = 'dynamic_route_mismatch';
|
|
135
|
+
} else if (correlation.verdict === ROUTE_VERDICT.AMBIGUOUS) {
|
|
136
|
+
findingType = 'dynamic_route_ambiguous';
|
|
137
|
+
}
|
|
138
|
+
|
|
129
139
|
// PHASE 15: Compute unified confidence
|
|
130
140
|
const unifiedConfidence = computeConfidenceForFinding({
|
|
131
141
|
findingType: findingType,
|
|
@@ -133,10 +143,11 @@ export function detectDynamicRouteFindings(traces, manifest, findings) {
|
|
|
133
143
|
sensors: trace.sensors || {},
|
|
134
144
|
comparisons: {},
|
|
135
145
|
evidence,
|
|
146
|
+
options: {}
|
|
136
147
|
});
|
|
137
148
|
|
|
138
149
|
// Legacy confidence for backward compatibility
|
|
139
|
-
const
|
|
150
|
+
const _confidence = computeConfidence({
|
|
140
151
|
findingType: 'dynamic_route_silent_failure',
|
|
141
152
|
expectation,
|
|
142
153
|
sensors: trace.sensors || {},
|
|
@@ -146,26 +157,18 @@ export function detectDynamicRouteFindings(traces, manifest, findings) {
|
|
|
146
157
|
|
|
147
158
|
// Determine severity based on evidence and verdict
|
|
148
159
|
let severity = 'SUSPECTED';
|
|
149
|
-
if (hasSufficientEvidence && correlation.verdict === ROUTE_VERDICT.SILENT_FAILURE && unifiedConfidence.score >= 0.8) {
|
|
160
|
+
if (hasSufficientEvidence && correlation.verdict === ROUTE_VERDICT.SILENT_FAILURE && (unifiedConfidence.score01 || unifiedConfidence.score || 0) >= 0.8) {
|
|
150
161
|
severity = 'CONFIRMED';
|
|
151
162
|
} else if (correlation.verdict === ROUTE_VERDICT.ROUTE_MISMATCH && hasSufficientEvidence) {
|
|
152
163
|
severity = 'CONFIRMED';
|
|
153
164
|
}
|
|
154
165
|
|
|
155
|
-
// Determine finding type
|
|
156
|
-
let findingType = 'dynamic_route_silent_failure';
|
|
157
|
-
if (correlation.verdict === ROUTE_VERDICT.ROUTE_MISMATCH) {
|
|
158
|
-
findingType = 'dynamic_route_mismatch';
|
|
159
|
-
} else if (correlation.verdict === ROUTE_VERDICT.AMBIGUOUS) {
|
|
160
|
-
findingType = 'dynamic_route_ambiguous';
|
|
161
|
-
}
|
|
162
|
-
|
|
163
166
|
const finding = {
|
|
164
167
|
type: findingType,
|
|
165
168
|
severity,
|
|
166
|
-
confidence: unifiedConfidence.score, //
|
|
169
|
+
confidence: unifiedConfidence.score01 || unifiedConfidence.score || 0, // Contract v1: score01 canonical
|
|
167
170
|
confidenceLevel: unifiedConfidence.level, // PHASE 15: Add confidence level
|
|
168
|
-
confidenceReasons: unifiedConfidence.reasons, //
|
|
171
|
+
confidenceReasons: unifiedConfidence.topReasons || unifiedConfidence.reasons || [], // Contract v1: topReasons
|
|
169
172
|
interaction: {
|
|
170
173
|
type: interaction.type,
|
|
171
174
|
selector: interaction.selector,
|
|
@@ -125,7 +125,7 @@ export class ExpectationChainDetector {
|
|
|
125
125
|
// Check each step in the chain
|
|
126
126
|
for (let i = 0; i < chain.length; i++) {
|
|
127
127
|
const step = chain[i];
|
|
128
|
-
const
|
|
128
|
+
const _stepType = step.type;
|
|
129
129
|
|
|
130
130
|
// Find matching trace for this step
|
|
131
131
|
const matchingTrace = traces.find(trace =>
|
|
@@ -93,10 +93,10 @@ export function matchExpectation(expectation, interaction, beforeUrl) {
|
|
|
93
93
|
if (!beforePath) return null;
|
|
94
94
|
|
|
95
95
|
const normalizedBefore = beforePath.replace(/\/$/, '') || '/';
|
|
96
|
-
const normalizedFrom = expectation.fromPath.replace(/\/$/, '') || '/';
|
|
96
|
+
const normalizedFrom = expectation.fromPath ? expectation.fromPath.replace(/\/$/, '') || '/' : normalizedBefore;
|
|
97
97
|
const normalizedType = normalizeExpectationType(expectation.type);
|
|
98
98
|
|
|
99
|
-
if (normalizedFrom !== normalizedBefore) return null;
|
|
99
|
+
if (expectation.fromPath && normalizedFrom !== normalizedBefore) return null;
|
|
100
100
|
|
|
101
101
|
if (!typesCompatible(normalizedType, interaction.type)) return null;
|
|
102
102
|
|
|
@@ -256,11 +256,9 @@ export function getExpectation(manifest, interaction, beforeUrl, attemptMeta = {
|
|
|
256
256
|
for (const contract of manifest.actionContracts) {
|
|
257
257
|
if (contract.source === sourceRef) {
|
|
258
258
|
const expectationType = contract.kind === 'NETWORK_ACTION' ? 'network_action' : 'action';
|
|
259
|
-
// Dynamic URLs never produce PROVEN_EXPECTATION (truth boundary)
|
|
260
|
-
const proof = contract.isDynamic ? 'UNPROVEN_EXPECTATION' : 'PROVEN_EXPECTATION';
|
|
261
259
|
return {
|
|
262
260
|
hasExpectation: true,
|
|
263
|
-
proof,
|
|
261
|
+
proof: 'PROVEN_EXPECTATION',
|
|
264
262
|
expectationType,
|
|
265
263
|
method: contract.method,
|
|
266
264
|
urlPath: contract.urlPath,
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Failure Cause Inference (FCI) - Evidence-backed cause attribution
|
|
3
|
+
*
|
|
4
|
+
* Pure, deterministic module that infers likely causes from confirmed findings.
|
|
5
|
+
* Each cause has explicit evidence conditions and is never guessed.
|
|
6
|
+
*
|
|
7
|
+
* Rules:
|
|
8
|
+
* 1) No evidence -> no cause
|
|
9
|
+
* 2) Causes phrased as "Likely cause:" statements
|
|
10
|
+
* 3) Internal errors never reported as user bugs
|
|
11
|
+
* 4) Deterministic output: same input => same causes, ordering, wording
|
|
12
|
+
* 5) Confidence: LOW|MEDIUM only (never HIGH)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Catalog order: causes are sorted by this order first, then by id as tie-breaker.
|
|
17
|
+
* This ensures stable, predictable cause ordering across all scenarios.
|
|
18
|
+
*/
|
|
19
|
+
const CATALOG_ORDER = ['C1_SELECTOR_MISMATCH', 'C2_STATE_MUTATION_NO_UI', 'C3_DEAD_CLICK', 'C4_NAVIGATION_NO_RENDER', 'C5_FORM_NO_FEEDBACK', 'C6_VALIDATION_NOT_SHOWN', 'C7_NETWORK_SILENT'];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Catalog of detectable causes with evidence conditions
|
|
23
|
+
* Ordered by catalog position, then id for deterministic processing
|
|
24
|
+
*/
|
|
25
|
+
const CAUSE_CATALOG = [
|
|
26
|
+
{
|
|
27
|
+
id: 'C1_SELECTOR_MISMATCH',
|
|
28
|
+
title: 'Element not found or selector mismatch',
|
|
29
|
+
condition: (finding) => {
|
|
30
|
+
const ev = finding.evidence || {};
|
|
31
|
+
return (
|
|
32
|
+
(ev.targetElementMissing === true) ||
|
|
33
|
+
(ev.staleHandle === true) ||
|
|
34
|
+
(ev.clickAttempted === true && ev.targetElement === false) ||
|
|
35
|
+
(ev.locatorResolution === 0) ||
|
|
36
|
+
(ev.domSnapshotMissing &&
|
|
37
|
+
(ev.domSnapshotMissing.includes('id') || ev.domSnapshotMissing.includes('class') || ev.domSnapshotMissing.includes('text')))
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
statement: () =>
|
|
41
|
+
'Likely cause: The UI element being interacted with could not be found or was stale at interaction time.',
|
|
42
|
+
evidence_refs: (finding) => {
|
|
43
|
+
const refs = [];
|
|
44
|
+
if (finding.evidence?.targetElementMissing === true) refs.push('evidence.targetElementMissing=true');
|
|
45
|
+
if (finding.evidence?.staleHandle === true) refs.push('evidence.staleHandle=true');
|
|
46
|
+
if (finding.evidence?.locatorResolution === 0) refs.push('evidence.locatorResolution=0');
|
|
47
|
+
if (finding.evidence?.domSnapshotMissing) refs.push('evidence.domSnapshotMissing');
|
|
48
|
+
return refs;
|
|
49
|
+
},
|
|
50
|
+
confidenceScore: (finding) => {
|
|
51
|
+
// MEDIUM if multiple signals, LOW otherwise
|
|
52
|
+
const count = [
|
|
53
|
+
finding.evidence?.targetElementMissing,
|
|
54
|
+
finding.evidence?.staleHandle,
|
|
55
|
+
finding.evidence?.locatorResolution === 0
|
|
56
|
+
].filter(Boolean).length;
|
|
57
|
+
return count >= 2 ? 'MEDIUM' : 'LOW';
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
{
|
|
62
|
+
id: 'C2_STATE_MUTATION_NO_UI',
|
|
63
|
+
title: 'State changed but UI did not update',
|
|
64
|
+
condition: (finding) => {
|
|
65
|
+
const ev = finding.evidence || {};
|
|
66
|
+
return (
|
|
67
|
+
ev.stateMutation === true &&
|
|
68
|
+
ev.domChanged === false &&
|
|
69
|
+
ev.navigationOccurred !== true &&
|
|
70
|
+
ev.uiFeedback === false
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
statement: () =>
|
|
74
|
+
'Likely cause: Application state changed internally, but the UI did not re-render or reflect the change.',
|
|
75
|
+
evidence_refs: (_finding) =>
|
|
76
|
+
['evidence.stateMutation=true', 'evidence.domChanged=false', 'evidence.uiFeedback=false'],
|
|
77
|
+
confidenceScore: () => 'MEDIUM'
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
{
|
|
81
|
+
id: 'C3_DEAD_CLICK',
|
|
82
|
+
title: 'Interaction ran but produced no observable outcome',
|
|
83
|
+
condition: (finding) => {
|
|
84
|
+
const ev = finding.evidence || {};
|
|
85
|
+
return (
|
|
86
|
+
ev.interactionPerformed === true &&
|
|
87
|
+
ev.networkActivity === false &&
|
|
88
|
+
ev.navigationOccurred !== true &&
|
|
89
|
+
ev.domChanged === false &&
|
|
90
|
+
ev.userFeedback === false
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
statement: () =>
|
|
94
|
+
'Likely cause: The interaction ran but had no handler or the handler did nothing (dead/no-op click).',
|
|
95
|
+
evidence_refs: (_finding) =>
|
|
96
|
+
['evidence.interactionPerformed=true', 'evidence.networkActivity=false', 'evidence.domChanged=false', 'evidence.userFeedback=false'],
|
|
97
|
+
confidenceScore: () => 'MEDIUM'
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
id: 'C4_NAVIGATION_NO_RENDER',
|
|
102
|
+
title: 'Navigation attempted but content did not load',
|
|
103
|
+
condition: (finding) => {
|
|
104
|
+
const ev = finding.evidence || {};
|
|
105
|
+
return (
|
|
106
|
+
(ev.navigationAttempted === true || ev.urlChangeAttempted === true || ev.linkClicked === true) &&
|
|
107
|
+
(ev.urlChanged === false || (ev.urlChanged === true && ev.contentStillLoading === true)) &&
|
|
108
|
+
(ev.mainContentChanged === false || ev.mainContentBlank === true)
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
statement: () =>
|
|
112
|
+
'Likely cause: Navigation was triggered but the target route either did not change or did not render visible content.',
|
|
113
|
+
evidence_refs: (finding) => {
|
|
114
|
+
const refs = [];
|
|
115
|
+
if (finding.evidence?.navigationAttempted || finding.evidence?.urlChangeAttempted || finding.evidence?.linkClicked) {
|
|
116
|
+
refs.push('evidence.navigationAttempted|urlChangeAttempted|linkClicked=true');
|
|
117
|
+
}
|
|
118
|
+
if (finding.evidence?.urlChanged === false) refs.push('evidence.urlChanged=false');
|
|
119
|
+
if (finding.evidence?.mainContentChanged === false) refs.push('evidence.mainContentChanged=false');
|
|
120
|
+
return refs;
|
|
121
|
+
},
|
|
122
|
+
confidenceScore: () => 'MEDIUM'
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
{
|
|
126
|
+
id: 'C5_FORM_NO_FEEDBACK',
|
|
127
|
+
title: 'Form submitted but no success or error message shown',
|
|
128
|
+
condition: (finding) => {
|
|
129
|
+
const ev = finding.evidence || {};
|
|
130
|
+
return (
|
|
131
|
+
ev.submitInteraction === true &&
|
|
132
|
+
(ev.networkRequestOccurred === true || ev.submitEventDetected === true) &&
|
|
133
|
+
ev.successFeedback === false &&
|
|
134
|
+
ev.errorFeedback === false &&
|
|
135
|
+
ev.navigationAfterSubmit !== true
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
statement: () =>
|
|
139
|
+
'Likely cause: Form submission was sent to the server, but the UI did not show a success or error message.',
|
|
140
|
+
evidence_refs: (_finding) =>
|
|
141
|
+
['evidence.submitInteraction=true', 'evidence.successFeedback=false', 'evidence.errorFeedback=false'],
|
|
142
|
+
confidenceScore: () => 'MEDIUM'
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
{
|
|
146
|
+
id: 'C6_VALIDATION_NOT_SHOWN',
|
|
147
|
+
title: 'Validation expected but feedback not displayed',
|
|
148
|
+
condition: (finding) => {
|
|
149
|
+
const ev = finding.evidence || {};
|
|
150
|
+
return (
|
|
151
|
+
ev.formOrValidationPromise === true &&
|
|
152
|
+
ev.invalidSubmitAttempted === true &&
|
|
153
|
+
ev.inlineValidationFeedback === false
|
|
154
|
+
);
|
|
155
|
+
},
|
|
156
|
+
statement: () =>
|
|
157
|
+
'Likely cause: Form field validation was expected to show inline feedback, but no error message appeared.',
|
|
158
|
+
evidence_refs: (_finding) =>
|
|
159
|
+
['evidence.formOrValidationPromise=true', 'evidence.invalidSubmitAttempted=true', 'evidence.inlineValidationFeedback=false'],
|
|
160
|
+
confidenceScore: () => 'LOW'
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
{
|
|
164
|
+
id: 'C7_NETWORK_SILENT',
|
|
165
|
+
title: 'Network request failed silently without user feedback',
|
|
166
|
+
condition: (finding) => {
|
|
167
|
+
const ev = finding.evidence || {};
|
|
168
|
+
return (
|
|
169
|
+
(ev.networkFailure === true || ev.httpError === true || ev.fetchError === true) &&
|
|
170
|
+
ev.uiFeedback === false &&
|
|
171
|
+
ev.domChanged === false
|
|
172
|
+
);
|
|
173
|
+
},
|
|
174
|
+
statement: () =>
|
|
175
|
+
'Likely cause: A network request failed (4xx/5xx or connection error), but the UI showed no error message.',
|
|
176
|
+
evidence_refs: (finding) => {
|
|
177
|
+
const refs = [];
|
|
178
|
+
if (finding.evidence?.networkFailure === true) refs.push('evidence.networkFailure=true');
|
|
179
|
+
if (finding.evidence?.httpError === true) refs.push('evidence.httpError=true');
|
|
180
|
+
if (finding.evidence?.fetchError === true) refs.push('evidence.fetchError=true');
|
|
181
|
+
refs.push('evidence.uiFeedback=false');
|
|
182
|
+
return refs;
|
|
183
|
+
},
|
|
184
|
+
confidenceScore: () => 'MEDIUM'
|
|
185
|
+
}
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Infer likely causes for a finding.
|
|
190
|
+
* Only returns causes where evidence conditions are met.
|
|
191
|
+
* Causes are ordered deterministically by catalog order.
|
|
192
|
+
*
|
|
193
|
+
* @param {Object} finding - Finding object with evidence property
|
|
194
|
+
* @returns {Array} Array of cause objects, sorted by id. Empty if no evidence.
|
|
195
|
+
*/
|
|
196
|
+
export function inferCauses(finding) {
|
|
197
|
+
if (!finding) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Enforce Evidence Law: no evidence -> no causes
|
|
202
|
+
if (!finding.evidence || Object.keys(finding.evidence).length === 0) {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const detectedCauses = [];
|
|
207
|
+
|
|
208
|
+
for (const causeDef of CAUSE_CATALOG) {
|
|
209
|
+
if (causeDef.condition(finding)) {
|
|
210
|
+
const cause = {
|
|
211
|
+
id: causeDef.id,
|
|
212
|
+
title: causeDef.title,
|
|
213
|
+
statement: causeDef.statement(),
|
|
214
|
+
evidence_refs: causeDef.evidence_refs(finding),
|
|
215
|
+
confidence: causeDef.confidenceScore(finding)
|
|
216
|
+
};
|
|
217
|
+
detectedCauses.push(cause);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Sort deterministically by catalog order, then by id
|
|
222
|
+
detectedCauses.sort((a, b) => {
|
|
223
|
+
const aPos = CATALOG_ORDER.indexOf(a.id);
|
|
224
|
+
const bPos = CATALOG_ORDER.indexOf(b.id);
|
|
225
|
+
if (aPos !== bPos) {
|
|
226
|
+
return aPos - bPos;
|
|
227
|
+
}
|
|
228
|
+
return a.id.localeCompare(b.id);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return detectedCauses;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Batch infer causes for all findings in a report.
|
|
236
|
+
* Pure function: same input => same causes in same order.
|
|
237
|
+
*
|
|
238
|
+
* @param {Array} findings - Array of finding objects
|
|
239
|
+
* @returns {Object} Map of finding.id -> causes array
|
|
240
|
+
*/
|
|
241
|
+
export function inferCausesForFindings(findings) {
|
|
242
|
+
const causesMap = {};
|
|
243
|
+
|
|
244
|
+
if (!findings || !Array.isArray(findings)) {
|
|
245
|
+
return causesMap;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (const finding of findings) {
|
|
249
|
+
const causes = inferCauses(finding);
|
|
250
|
+
if (causes.length > 0) {
|
|
251
|
+
causesMap[finding.id] = causes;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return causesMap;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Attach causes to a finding object (pure function).
|
|
260
|
+
* Returns a new finding object with causes attached.
|
|
261
|
+
* Original finding is not mutated.
|
|
262
|
+
*
|
|
263
|
+
* @param {Object} finding - Finding to augment
|
|
264
|
+
* @returns {Object} New finding object with causes array added
|
|
265
|
+
*/
|
|
266
|
+
export function attachCausesToFinding(finding) {
|
|
267
|
+
if (!finding) {
|
|
268
|
+
return finding;
|
|
269
|
+
}
|
|
270
|
+
const causes = inferCauses(finding);
|
|
271
|
+
return {
|
|
272
|
+
...finding,
|
|
273
|
+
causes
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Filter findings to only those with causes.
|
|
279
|
+
* Useful for reporting only findings with explanation.
|
|
280
|
+
*
|
|
281
|
+
* @param {Array} findings - Array of findings
|
|
282
|
+
* @returns {Array} Findings with non-empty causes array
|
|
283
|
+
*/
|
|
284
|
+
export function findingsWithCauses(findings) {
|
|
285
|
+
if (!findings || !Array.isArray(findings)) {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return findings.filter(f => {
|
|
290
|
+
const causes = inferCauses(f);
|
|
291
|
+
return causes.length > 0;
|
|
292
|
+
});
|
|
293
|
+
}
|