@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.
Files changed (191) hide show
  1. package/README.md +28 -20
  2. package/bin/verax.js +11 -18
  3. package/package.json +28 -7
  4. package/src/cli/commands/baseline.js +1 -2
  5. package/src/cli/commands/default.js +72 -81
  6. package/src/cli/commands/doctor.js +29 -0
  7. package/src/cli/commands/ga.js +3 -0
  8. package/src/cli/commands/gates.js +1 -1
  9. package/src/cli/commands/inspect.js +6 -133
  10. package/src/cli/commands/release-check.js +2 -0
  11. package/src/cli/commands/run.js +74 -246
  12. package/src/cli/commands/security-check.js +2 -1
  13. package/src/cli/commands/truth.js +0 -1
  14. package/src/cli/entry.js +82 -309
  15. package/src/cli/util/angular-component-extractor.js +2 -2
  16. package/src/cli/util/angular-navigation-detector.js +2 -2
  17. package/src/cli/util/ast-interactive-detector.js +4 -6
  18. package/src/cli/util/ast-network-detector.js +3 -3
  19. package/src/cli/util/ast-promise-extractor.js +581 -0
  20. package/src/cli/util/ast-usestate-detector.js +3 -3
  21. package/src/cli/util/atomic-write.js +12 -1
  22. package/src/cli/util/console-reporter.js +72 -0
  23. package/src/cli/util/detection-engine.js +105 -41
  24. package/src/cli/util/determinism-runner.js +2 -1
  25. package/src/cli/util/determinism-writer.js +1 -1
  26. package/src/cli/util/digest-engine.js +359 -0
  27. package/src/cli/util/dom-diff.js +226 -0
  28. package/src/cli/util/env-url.js +0 -4
  29. package/src/cli/util/evidence-engine.js +287 -0
  30. package/src/cli/util/expectation-extractor.js +217 -367
  31. package/src/cli/util/findings-writer.js +19 -126
  32. package/src/cli/util/framework-detector.js +572 -0
  33. package/src/cli/util/idgen.js +1 -1
  34. package/src/cli/util/interaction-planner.js +529 -0
  35. package/src/cli/util/learn-writer.js +2 -2
  36. package/src/cli/util/ledger-writer.js +110 -0
  37. package/src/cli/util/monorepo-resolver.js +162 -0
  38. package/src/cli/util/observation-engine.js +127 -278
  39. package/src/cli/util/observe-writer.js +2 -2
  40. package/src/cli/util/paths.js +12 -3
  41. package/src/cli/util/project-discovery.js +284 -3
  42. package/src/cli/util/project-writer.js +2 -2
  43. package/src/cli/util/run-id.js +23 -27
  44. package/src/cli/util/run-result.js +778 -0
  45. package/src/cli/util/selector-resolver.js +235 -0
  46. package/src/cli/util/summary-writer.js +2 -1
  47. package/src/cli/util/svelte-navigation-detector.js +3 -3
  48. package/src/cli/util/svelte-sfc-extractor.js +0 -1
  49. package/src/cli/util/svelte-state-detector.js +1 -2
  50. package/src/cli/util/trust-activation-integration.js +496 -0
  51. package/src/cli/util/trust-activation-wrapper.js +85 -0
  52. package/src/cli/util/trust-integration-hooks.js +164 -0
  53. package/src/cli/util/types.js +153 -0
  54. package/src/cli/util/url-validation.js +40 -0
  55. package/src/cli/util/vue-navigation-detector.js +4 -3
  56. package/src/cli/util/vue-sfc-extractor.js +1 -2
  57. package/src/cli/util/vue-state-detector.js +1 -1
  58. package/src/types/fs-augment.d.ts +23 -0
  59. package/src/types/global.d.ts +137 -0
  60. package/src/types/internal-types.d.ts +35 -0
  61. package/src/verax/cli/finding-explainer.js +3 -56
  62. package/src/verax/cli/init.js +4 -18
  63. package/src/verax/core/action-classifier.js +4 -3
  64. package/src/verax/core/artifacts/registry.js +0 -15
  65. package/src/verax/core/artifacts/verifier.js +18 -8
  66. package/src/verax/core/baseline/baseline.snapshot.js +2 -0
  67. package/src/verax/core/capabilities/gates.js +7 -1
  68. package/src/verax/core/confidence/confidence-compute.js +14 -7
  69. package/src/verax/core/confidence/confidence.loader.js +1 -0
  70. package/src/verax/core/confidence-engine-refactor.js +8 -3
  71. package/src/verax/core/confidence-engine.js +162 -23
  72. package/src/verax/core/contracts/types.js +1 -0
  73. package/src/verax/core/contracts/validators.js +79 -4
  74. package/src/verax/core/decision-snapshot.js +3 -30
  75. package/src/verax/core/decisions/decision.trace.js +2 -0
  76. package/src/verax/core/determinism/contract-writer.js +2 -2
  77. package/src/verax/core/determinism/contract.js +1 -1
  78. package/src/verax/core/determinism/diff.js +42 -1
  79. package/src/verax/core/determinism/engine.js +7 -6
  80. package/src/verax/core/determinism/finding-identity.js +3 -2
  81. package/src/verax/core/determinism/normalize.js +32 -4
  82. package/src/verax/core/determinism/report-writer.js +1 -0
  83. package/src/verax/core/determinism/run-fingerprint.js +7 -2
  84. package/src/verax/core/dynamic-route-intelligence.js +8 -7
  85. package/src/verax/core/evidence/evidence-capture-service.js +1 -0
  86. package/src/verax/core/evidence/evidence-intent-ledger.js +2 -1
  87. package/src/verax/core/evidence-builder.js +2 -2
  88. package/src/verax/core/execution-mode-context.js +1 -1
  89. package/src/verax/core/execution-mode-detector.js +5 -3
  90. package/src/verax/core/failures/exit-codes.js +39 -37
  91. package/src/verax/core/failures/failure-summary.js +1 -1
  92. package/src/verax/core/failures/failure.factory.js +3 -3
  93. package/src/verax/core/failures/failure.ledger.js +3 -2
  94. package/src/verax/core/ga/ga.artifact.js +1 -1
  95. package/src/verax/core/ga/ga.contract.js +3 -2
  96. package/src/verax/core/ga/ga.enforcer.js +1 -0
  97. package/src/verax/core/guardrails/policy.loader.js +1 -0
  98. package/src/verax/core/guardrails/truth-reconciliation.js +1 -1
  99. package/src/verax/core/guardrails-engine.js +2 -2
  100. package/src/verax/core/incremental-store.js +1 -0
  101. package/src/verax/core/integrity/budget.js +138 -0
  102. package/src/verax/core/integrity/determinism.js +342 -0
  103. package/src/verax/core/integrity/integrity.js +208 -0
  104. package/src/verax/core/integrity/poisoning.js +108 -0
  105. package/src/verax/core/integrity/transaction.js +140 -0
  106. package/src/verax/core/observe/run-timeline.js +2 -0
  107. package/src/verax/core/perf/perf.report.js +2 -0
  108. package/src/verax/core/pipeline-tracker.js +5 -0
  109. package/src/verax/core/release/provenance.builder.js +73 -214
  110. package/src/verax/core/release/release.enforcer.js +14 -9
  111. package/src/verax/core/release/reproducibility.check.js +1 -0
  112. package/src/verax/core/release/sbom.builder.js +32 -23
  113. package/src/verax/core/replay-validator.js +2 -0
  114. package/src/verax/core/replay.js +4 -0
  115. package/src/verax/core/report/cross-index.js +6 -3
  116. package/src/verax/core/report/human-summary.js +141 -1
  117. package/src/verax/core/route-intelligence.js +4 -3
  118. package/src/verax/core/run-id.js +6 -3
  119. package/src/verax/core/run-manifest.js +4 -3
  120. package/src/verax/core/security/secrets.scan.js +10 -7
  121. package/src/verax/core/security/security.enforcer.js +4 -0
  122. package/src/verax/core/security/supplychain.policy.js +9 -1
  123. package/src/verax/core/security/vuln.scan.js +2 -2
  124. package/src/verax/core/truth/truth.certificate.js +3 -1
  125. package/src/verax/core/ui-feedback-intelligence.js +12 -46
  126. package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
  127. package/src/verax/detect/confidence-engine.js +100 -660
  128. package/src/verax/detect/confidence-helper.js +1 -0
  129. package/src/verax/detect/detection-engine.js +1 -18
  130. package/src/verax/detect/dynamic-route-findings.js +17 -14
  131. package/src/verax/detect/expectation-chain-detector.js +1 -1
  132. package/src/verax/detect/expectation-model.js +3 -5
  133. package/src/verax/detect/failure-cause-inference.js +293 -0
  134. package/src/verax/detect/findings-writer.js +126 -166
  135. package/src/verax/detect/flow-detector.js +2 -2
  136. package/src/verax/detect/form-silent-failure.js +98 -0
  137. package/src/verax/detect/index.js +51 -234
  138. package/src/verax/detect/invariants-enforcer.js +147 -0
  139. package/src/verax/detect/journey-stall-detector.js +4 -4
  140. package/src/verax/detect/navigation-silent-failure.js +82 -0
  141. package/src/verax/detect/problem-aggregator.js +361 -0
  142. package/src/verax/detect/route-findings.js +7 -6
  143. package/src/verax/detect/summary-writer.js +477 -0
  144. package/src/verax/detect/test-failure-cause-inference.js +314 -0
  145. package/src/verax/detect/ui-feedback-findings.js +18 -18
  146. package/src/verax/detect/verdict-engine.js +3 -57
  147. package/src/verax/detect/view-switch-correlator.js +2 -2
  148. package/src/verax/flow/flow-engine.js +2 -1
  149. package/src/verax/flow/flow-spec.js +0 -6
  150. package/src/verax/index.js +48 -412
  151. package/src/verax/intel/ts-program.js +1 -0
  152. package/src/verax/intel/vue-navigation-extractor.js +3 -0
  153. package/src/verax/learn/action-contract-extractor.js +67 -682
  154. package/src/verax/learn/ast-contract-extractor.js +1 -1
  155. package/src/verax/learn/flow-extractor.js +1 -0
  156. package/src/verax/learn/project-detector.js +5 -0
  157. package/src/verax/learn/react-router-extractor.js +2 -0
  158. package/src/verax/learn/route-validator.js +1 -4
  159. package/src/verax/learn/source-instrumenter.js +1 -0
  160. package/src/verax/learn/state-extractor.js +2 -1
  161. package/src/verax/learn/static-extractor.js +1 -0
  162. package/src/verax/observe/coverage-gaps.js +132 -0
  163. package/src/verax/observe/expectation-handler.js +126 -0
  164. package/src/verax/observe/incremental-skip.js +46 -0
  165. package/src/verax/observe/index.js +735 -84
  166. package/src/verax/observe/interaction-executor.js +192 -0
  167. package/src/verax/observe/interaction-runner.js +782 -530
  168. package/src/verax/observe/network-firewall.js +86 -0
  169. package/src/verax/observe/observation-builder.js +169 -0
  170. package/src/verax/observe/observe-context.js +1 -1
  171. package/src/verax/observe/observe-helpers.js +2 -1
  172. package/src/verax/observe/observe-runner.js +28 -24
  173. package/src/verax/observe/observers/budget-observer.js +3 -3
  174. package/src/verax/observe/observers/console-observer.js +4 -4
  175. package/src/verax/observe/observers/coverage-observer.js +4 -4
  176. package/src/verax/observe/observers/interaction-observer.js +3 -3
  177. package/src/verax/observe/observers/navigation-observer.js +4 -4
  178. package/src/verax/observe/observers/network-observer.js +4 -4
  179. package/src/verax/observe/observers/safety-observer.js +1 -1
  180. package/src/verax/observe/observers/ui-feedback-observer.js +4 -4
  181. package/src/verax/observe/page-traversal.js +138 -0
  182. package/src/verax/observe/snapshot-ops.js +94 -0
  183. package/src/verax/observe/ui-signal-sensor.js +2 -148
  184. package/src/verax/scan-summary-writer.js +10 -42
  185. package/src/verax/shared/artifact-manager.js +30 -13
  186. package/src/verax/shared/caching.js +1 -0
  187. package/src/verax/shared/expectation-tracker.js +1 -0
  188. package/src/verax/shared/zip-artifacts.js +6 -0
  189. package/src/verax/core/confidence-engine.js.backup +0 -471
  190. package/src/verax/shared/config-loader.js +0 -169
  191. /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.1.0',
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} findings - Findings array to append to
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, findings) {
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 confidence = computeConfidence({
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, // PHASE 15: Use unified confidence score (0..1)
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, // PHASE 15: Add stable reason codes
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 stepType = step.type;
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
+ }