@veraxhq/verax 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +14 -18
  2. package/bin/verax.js +7 -0
  3. package/package.json +3 -3
  4. package/src/cli/commands/baseline.js +104 -0
  5. package/src/cli/commands/default.js +79 -25
  6. package/src/cli/commands/ga.js +243 -0
  7. package/src/cli/commands/gates.js +95 -0
  8. package/src/cli/commands/inspect.js +131 -2
  9. package/src/cli/commands/release-check.js +213 -0
  10. package/src/cli/commands/run.js +246 -35
  11. package/src/cli/commands/security-check.js +211 -0
  12. package/src/cli/commands/truth.js +114 -0
  13. package/src/cli/entry.js +304 -67
  14. package/src/cli/util/angular-component-extractor.js +179 -0
  15. package/src/cli/util/angular-navigation-detector.js +141 -0
  16. package/src/cli/util/angular-network-detector.js +161 -0
  17. package/src/cli/util/angular-state-detector.js +162 -0
  18. package/src/cli/util/ast-interactive-detector.js +546 -0
  19. package/src/cli/util/ast-network-detector.js +603 -0
  20. package/src/cli/util/ast-usestate-detector.js +602 -0
  21. package/src/cli/util/bootstrap-guard.js +86 -0
  22. package/src/cli/util/determinism-runner.js +123 -0
  23. package/src/cli/util/determinism-writer.js +129 -0
  24. package/src/cli/util/env-url.js +4 -0
  25. package/src/cli/util/expectation-extractor.js +369 -73
  26. package/src/cli/util/findings-writer.js +126 -16
  27. package/src/cli/util/learn-writer.js +3 -1
  28. package/src/cli/util/observe-writer.js +3 -1
  29. package/src/cli/util/paths.js +3 -12
  30. package/src/cli/util/project-discovery.js +3 -0
  31. package/src/cli/util/project-writer.js +3 -1
  32. package/src/cli/util/run-resolver.js +64 -0
  33. package/src/cli/util/source-requirement.js +55 -0
  34. package/src/cli/util/summary-writer.js +1 -0
  35. package/src/cli/util/svelte-navigation-detector.js +163 -0
  36. package/src/cli/util/svelte-network-detector.js +80 -0
  37. package/src/cli/util/svelte-sfc-extractor.js +147 -0
  38. package/src/cli/util/svelte-state-detector.js +243 -0
  39. package/src/cli/util/vue-navigation-detector.js +177 -0
  40. package/src/cli/util/vue-sfc-extractor.js +162 -0
  41. package/src/cli/util/vue-state-detector.js +215 -0
  42. package/src/verax/cli/finding-explainer.js +56 -3
  43. package/src/verax/core/artifacts/registry.js +154 -0
  44. package/src/verax/core/artifacts/verifier.js +980 -0
  45. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  46. package/src/verax/core/baseline/baseline.snapshot.js +231 -0
  47. package/src/verax/core/capabilities/gates.js +499 -0
  48. package/src/verax/core/capabilities/registry.js +475 -0
  49. package/src/verax/core/confidence/confidence-compute.js +137 -0
  50. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  51. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  52. package/src/verax/core/confidence/confidence-weights.js +44 -0
  53. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  54. package/src/verax/core/confidence/confidence.loader.js +79 -0
  55. package/src/verax/core/confidence/confidence.schema.js +94 -0
  56. package/src/verax/core/confidence-engine-refactor.js +484 -0
  57. package/src/verax/core/confidence-engine.js +486 -0
  58. package/src/verax/core/confidence-engine.js.backup +471 -0
  59. package/src/verax/core/contracts/index.js +29 -0
  60. package/src/verax/core/contracts/types.js +185 -0
  61. package/src/verax/core/contracts/validators.js +381 -0
  62. package/src/verax/core/decision-snapshot.js +30 -3
  63. package/src/verax/core/decisions/decision.trace.js +276 -0
  64. package/src/verax/core/determinism/contract-writer.js +89 -0
  65. package/src/verax/core/determinism/contract.js +139 -0
  66. package/src/verax/core/determinism/diff.js +364 -0
  67. package/src/verax/core/determinism/engine.js +221 -0
  68. package/src/verax/core/determinism/finding-identity.js +148 -0
  69. package/src/verax/core/determinism/normalize.js +438 -0
  70. package/src/verax/core/determinism/report-writer.js +92 -0
  71. package/src/verax/core/determinism/run-fingerprint.js +118 -0
  72. package/src/verax/core/dynamic-route-intelligence.js +528 -0
  73. package/src/verax/core/evidence/evidence-capture-service.js +307 -0
  74. package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
  75. package/src/verax/core/evidence-builder.js +487 -0
  76. package/src/verax/core/execution-mode-context.js +77 -0
  77. package/src/verax/core/execution-mode-detector.js +190 -0
  78. package/src/verax/core/failures/exit-codes.js +86 -0
  79. package/src/verax/core/failures/failure-summary.js +76 -0
  80. package/src/verax/core/failures/failure.factory.js +225 -0
  81. package/src/verax/core/failures/failure.ledger.js +132 -0
  82. package/src/verax/core/failures/failure.types.js +196 -0
  83. package/src/verax/core/failures/index.js +10 -0
  84. package/src/verax/core/ga/ga-report-writer.js +43 -0
  85. package/src/verax/core/ga/ga.artifact.js +49 -0
  86. package/src/verax/core/ga/ga.contract.js +434 -0
  87. package/src/verax/core/ga/ga.enforcer.js +86 -0
  88. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  89. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  90. package/src/verax/core/guardrails/policy.loader.js +83 -0
  91. package/src/verax/core/guardrails/policy.schema.js +110 -0
  92. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  93. package/src/verax/core/guardrails-engine.js +505 -0
  94. package/src/verax/core/observe/run-timeline.js +316 -0
  95. package/src/verax/core/perf/perf.contract.js +186 -0
  96. package/src/verax/core/perf/perf.display.js +65 -0
  97. package/src/verax/core/perf/perf.enforcer.js +91 -0
  98. package/src/verax/core/perf/perf.monitor.js +209 -0
  99. package/src/verax/core/perf/perf.report.js +198 -0
  100. package/src/verax/core/pipeline-tracker.js +238 -0
  101. package/src/verax/core/product-definition.js +127 -0
  102. package/src/verax/core/release/provenance.builder.js +271 -0
  103. package/src/verax/core/release/release-report-writer.js +40 -0
  104. package/src/verax/core/release/release.enforcer.js +159 -0
  105. package/src/verax/core/release/reproducibility.check.js +221 -0
  106. package/src/verax/core/release/sbom.builder.js +283 -0
  107. package/src/verax/core/report/cross-index.js +192 -0
  108. package/src/verax/core/report/human-summary.js +222 -0
  109. package/src/verax/core/route-intelligence.js +419 -0
  110. package/src/verax/core/security/secrets.scan.js +326 -0
  111. package/src/verax/core/security/security-report.js +50 -0
  112. package/src/verax/core/security/security.enforcer.js +124 -0
  113. package/src/verax/core/security/supplychain.defaults.json +38 -0
  114. package/src/verax/core/security/supplychain.policy.js +326 -0
  115. package/src/verax/core/security/vuln.scan.js +265 -0
  116. package/src/verax/core/truth/truth.certificate.js +250 -0
  117. package/src/verax/core/ui-feedback-intelligence.js +515 -0
  118. package/src/verax/detect/confidence-engine.js +628 -40
  119. package/src/verax/detect/confidence-helper.js +33 -0
  120. package/src/verax/detect/detection-engine.js +18 -1
  121. package/src/verax/detect/dynamic-route-findings.js +335 -0
  122. package/src/verax/detect/expectation-chain-detector.js +417 -0
  123. package/src/verax/detect/expectation-model.js +3 -1
  124. package/src/verax/detect/findings-writer.js +141 -5
  125. package/src/verax/detect/index.js +229 -5
  126. package/src/verax/detect/journey-stall-detector.js +558 -0
  127. package/src/verax/detect/route-findings.js +218 -0
  128. package/src/verax/detect/ui-feedback-findings.js +207 -0
  129. package/src/verax/detect/verdict-engine.js +57 -3
  130. package/src/verax/detect/view-switch-correlator.js +242 -0
  131. package/src/verax/index.js +413 -45
  132. package/src/verax/learn/action-contract-extractor.js +682 -64
  133. package/src/verax/learn/route-validator.js +4 -1
  134. package/src/verax/observe/index.js +88 -843
  135. package/src/verax/observe/interaction-runner.js +25 -8
  136. package/src/verax/observe/observe-context.js +205 -0
  137. package/src/verax/observe/observe-helpers.js +191 -0
  138. package/src/verax/observe/observe-runner.js +226 -0
  139. package/src/verax/observe/observers/budget-observer.js +185 -0
  140. package/src/verax/observe/observers/console-observer.js +102 -0
  141. package/src/verax/observe/observers/coverage-observer.js +107 -0
  142. package/src/verax/observe/observers/interaction-observer.js +471 -0
  143. package/src/verax/observe/observers/navigation-observer.js +132 -0
  144. package/src/verax/observe/observers/network-observer.js +87 -0
  145. package/src/verax/observe/observers/safety-observer.js +82 -0
  146. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  147. package/src/verax/observe/ui-feedback-detector.js +742 -0
  148. package/src/verax/observe/ui-signal-sensor.js +148 -2
  149. package/src/verax/scan-summary-writer.js +42 -8
  150. package/src/verax/shared/artifact-manager.js +8 -5
  151. package/src/verax/shared/css-spinner-rules.js +204 -0
  152. package/src/verax/shared/view-switch-rules.js +208 -0
@@ -0,0 +1,222 @@
1
+ /**
2
+ * PHASE 21.10 — Human Summary
3
+ *
4
+ * Generates human-readable summary for Enterprise UX.
5
+ * Clear, direct, no marketing.
6
+ */
7
+
8
+ import { readFileSync, existsSync } from 'fs';
9
+ import { resolve } from 'path';
10
+
11
+ /**
12
+ * Load artifact JSON
13
+ */
14
+ function loadArtifact(runDir, filename) {
15
+ const path = resolve(runDir, filename);
16
+ if (!existsSync(path)) {
17
+ return null;
18
+ }
19
+ try {
20
+ return JSON.parse(readFileSync(path, 'utf-8'));
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Generate human summary
28
+ *
29
+ * @param {string} projectDir - Project directory
30
+ * @param {string} runId - Run ID
31
+ * @returns {Object} Human summary
32
+ */
33
+ export async function generateHumanSummary(projectDir, runId) {
34
+ const runDir = resolve(projectDir, '.verax', 'runs', runId);
35
+
36
+ if (!existsSync(runDir)) {
37
+ return null;
38
+ }
39
+
40
+ const summary = loadArtifact(runDir, 'summary.json');
41
+ const findings = loadArtifact(runDir, 'findings.json');
42
+ const determinism = loadArtifact(runDir, 'decisions.json');
43
+ const performanceReport = loadArtifact(runDir, 'performance.report.json');
44
+
45
+ // Security reports are in release/ directory (project root)
46
+ // Use projectDir parameter directly (already resolved)
47
+ const releaseDir = resolve(projectDir, 'release');
48
+ const securitySecrets = loadArtifact(releaseDir, 'security.secrets.report.json');
49
+ const securityVuln = loadArtifact(releaseDir, 'security.vuln.report.json');
50
+
51
+ const gaStatus = loadArtifact(runDir, 'ga.status.json');
52
+
53
+ if (!summary) {
54
+ return null;
55
+ }
56
+
57
+ const findingsArray = Array.isArray(findings?.findings) ? findings.findings : [];
58
+ const confirmedFindings = findingsArray.filter(f => (f.severity || f.status) === 'CONFIRMED');
59
+ const suspectedFindings = findingsArray.filter(f => (f.severity || f.status) === 'SUSPECTED');
60
+
61
+ // What VERAX is confident about
62
+ const confident = {
63
+ findings: confirmedFindings.length,
64
+ message: confirmedFindings.length > 0
65
+ ? `${confirmedFindings.length} finding(s) with complete evidence`
66
+ : 'No findings with complete evidence',
67
+ details: confirmedFindings.map(f => ({
68
+ type: f.type,
69
+ outcome: f.outcome,
70
+ confidence: f.confidenceLevel || 'UNKNOWN'
71
+ }))
72
+ };
73
+
74
+ // What VERAX is NOT confident about
75
+ const notConfident = {
76
+ findings: suspectedFindings.length,
77
+ message: suspectedFindings.length > 0
78
+ ? `${suspectedFindings.length} finding(s) with incomplete evidence (SUSPECTED)`
79
+ : 'No findings with incomplete evidence',
80
+ details: suspectedFindings.map(f => ({
81
+ type: f.type,
82
+ outcome: f.outcome,
83
+ confidence: f.confidenceLevel || 'UNKNOWN',
84
+ missingEvidence: f.evidencePackage?.isComplete === false
85
+ }))
86
+ };
87
+
88
+ // Why some things were skipped
89
+ const skips = [];
90
+ if (summary.truth?.observe?.skips) {
91
+ for (const skip of summary.truth.observe.skips) {
92
+ skips.push({
93
+ reason: skip.reason || skip.code || 'UNKNOWN',
94
+ count: skip.count || 1,
95
+ message: skip.message || `Skipped: ${skip.reason || skip.code}`
96
+ });
97
+ }
98
+ }
99
+
100
+ // Determinism verdict
101
+ let determinismVerdict = 'UNKNOWN';
102
+ if (determinism) {
103
+ try {
104
+ const { DecisionRecorder } = await import('../../../core/determinism-model.js');
105
+ const recorder = DecisionRecorder.fromExport(determinism);
106
+ const { computeDeterminismVerdict } = await import('../../../core/determinism/contract.js');
107
+ const verdict = computeDeterminismVerdict(recorder);
108
+ determinismVerdict = verdict.verdict;
109
+ } catch {
110
+ determinismVerdict = summary.determinism?.verdict || 'UNKNOWN';
111
+ }
112
+ } else if (summary.determinism) {
113
+ determinismVerdict = summary.determinism.verdict || 'UNKNOWN';
114
+ }
115
+
116
+ // Performance verdict
117
+ const performanceVerdict = performanceReport?.verdict || 'UNKNOWN';
118
+ const performanceOk = performanceReport?.ok !== false;
119
+
120
+ // Security verdict
121
+ const securityOk = !securitySecrets?.hasSecrets &&
122
+ !securityVuln?.blocking &&
123
+ (securitySecrets !== null || securityVuln !== null); // At least one report exists
124
+
125
+ // GA verdict
126
+ const gaReady = gaStatus?.gaReady === true;
127
+ const gaVerdict = gaReady ? 'GA-READY' : (gaStatus ? 'GA-BLOCKED' : 'UNKNOWN');
128
+
129
+ return {
130
+ runId,
131
+ whatWeKnow: {
132
+ confident: confident,
133
+ notConfident: notConfident,
134
+ skips: skips.length > 0 ? {
135
+ total: skips.reduce((sum, s) => sum + s.count, 0),
136
+ reasons: skips
137
+ } : null
138
+ },
139
+ verdicts: {
140
+ determinism: {
141
+ verdict: determinismVerdict,
142
+ message: determinismVerdict === 'DETERMINISTIC'
143
+ ? 'Run was reproducible (same inputs = same outputs)'
144
+ : determinismVerdict === 'NON_DETERMINISTIC'
145
+ ? 'Run was not reproducible (adaptive events detected)'
146
+ : 'Determinism not evaluated'
147
+ },
148
+ performance: {
149
+ verdict: performanceVerdict,
150
+ ok: performanceOk,
151
+ message: performanceOk
152
+ ? 'Performance within budget'
153
+ : performanceReport?.violations?.length > 0
154
+ ? `${performanceReport.violations.length} BLOCKING performance violation(s)`
155
+ : 'Performance not evaluated'
156
+ },
157
+ security: {
158
+ ok: securityOk,
159
+ message: securityOk
160
+ ? 'Security baseline passed'
161
+ : securitySecrets?.hasSecrets
162
+ ? 'Secrets detected'
163
+ : securityVuln?.blocking
164
+ ? 'Critical vulnerabilities detected'
165
+ : 'Security not evaluated'
166
+ },
167
+ ga: {
168
+ verdict: gaVerdict,
169
+ ready: gaReady,
170
+ message: gaReady
171
+ ? 'GA-READY: All gates passed'
172
+ : gaStatus
173
+ ? `GA-BLOCKED: ${gaStatus.blockers?.length || 0} blocker(s)`
174
+ : 'GA not evaluated'
175
+ }
176
+ },
177
+ generatedAt: new Date().toISOString()
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Format human summary for CLI display
183
+ *
184
+ * @param {Object} summary - Human summary
185
+ * @returns {string} Formatted string
186
+ */
187
+ export function formatHumanSummary(summary) {
188
+ if (!summary) {
189
+ return 'Summary: Not available';
190
+ }
191
+
192
+ const lines = [];
193
+ lines.push('\n' + '='.repeat(80));
194
+ lines.push('HUMAN SUMMARY');
195
+ lines.push('='.repeat(80));
196
+
197
+ // What we know
198
+ lines.push('\nWhat VERAX is confident about:');
199
+ lines.push(` ${summary.whatWeKnow.confident.message}`);
200
+
201
+ lines.push('\nWhat VERAX is NOT confident about:');
202
+ lines.push(` ${summary.whatWeKnow.notConfident.message}`);
203
+
204
+ if (summary.whatWeKnow.skips) {
205
+ lines.push('\nWhy some things were skipped:');
206
+ for (const skip of summary.whatWeKnow.skips.reasons) {
207
+ lines.push(` - ${skip.message} (${skip.count}x)`);
208
+ }
209
+ }
210
+
211
+ // Verdicts
212
+ lines.push('\nVerdicts:');
213
+ lines.push(` Determinism: ${summary.verdicts.determinism.verdict} - ${summary.verdicts.determinism.message}`);
214
+ lines.push(` Performance: ${summary.verdicts.performance.verdict} - ${summary.verdicts.performance.message}`);
215
+ lines.push(` Security: ${summary.verdicts.security.ok ? 'OK' : 'BLOCKED'} - ${summary.verdicts.security.message}`);
216
+ lines.push(` GA: ${summary.verdicts.ga.verdict} - ${summary.verdicts.ga.message}`);
217
+
218
+ lines.push('='.repeat(80) + '\n');
219
+
220
+ return lines.join('\n');
221
+ }
222
+
@@ -0,0 +1,419 @@
1
+ /**
2
+ * PHASE 12 — Route Intelligence & Deep Route Detection
3
+ *
4
+ * Unified route model and intelligence layer that:
5
+ * - Defines route taxonomy (static, dynamic, ambiguous)
6
+ * - Correlates navigation promises with routes
7
+ * - Handles dynamic routes honestly
8
+ * - Provides evidence for route-related findings
9
+ */
10
+
11
+ import { normalizeDynamicRoute, isDynamicPath, normalizeNavigationTarget } from '../shared/dynamic-route-utils.js';
12
+
13
+ /**
14
+ * Route Taxonomy
15
+ */
16
+ export const ROUTE_TYPE = {
17
+ STATIC: 'static',
18
+ DYNAMIC: 'dynamic',
19
+ AMBIGUOUS: 'ambiguous',
20
+ PROGRAMMATIC: 'programmatic'
21
+ };
22
+
23
+ export const ROUTE_STABILITY = {
24
+ STATIC: 'static', // Fully static route
25
+ DYNAMIC: 'dynamic', // Dynamic but can be normalized
26
+ AMBIGUOUS: 'ambiguous' // Cannot be deterministically validated
27
+ };
28
+
29
+ export const ROUTE_SOURCE = {
30
+ FILE_SYSTEM: 'file_system', // Next.js pages/app router
31
+ JSX: 'jsx', // React Router <Route>
32
+ CONFIG: 'config', // Vue Router config
33
+ PROGRAMMATIC: 'programmatic' // navigate(), router.push()
34
+ };
35
+
36
+ /**
37
+ * PHASE 12: Unified Route Model
38
+ *
39
+ * @typedef {Object} RouteModel
40
+ * @property {string} path - Normalized route path
41
+ * @property {string} type - ROUTE_TYPE (static, dynamic, ambiguous, programmatic)
42
+ * @property {string} stability - ROUTE_STABILITY (static, dynamic, ambiguous)
43
+ * @property {string} source - ROUTE_SOURCE (file_system, jsx, config, programmatic)
44
+ * @property {string} framework - Framework identifier
45
+ * @property {string} sourceRef - Source reference (file:line:col)
46
+ * @property {string} file - Source file path
47
+ * @property {number} line - Source line number
48
+ * @property {string} [originalPattern] - Original dynamic pattern (if dynamic)
49
+ * @property {string[]} [parameters] - Dynamic parameters (if dynamic)
50
+ * @property {boolean} [isDynamic] - Whether route is dynamic
51
+ * @property {boolean} [exampleExecution] - Whether example path was generated
52
+ */
53
+
54
+ /**
55
+ * Build unified route model from extracted routes
56
+ *
57
+ * @param {Array} extractedRoutes - Routes from route extractors
58
+ * @returns {Array<RouteModel>} Unified route models
59
+ */
60
+ export function buildRouteModels(extractedRoutes) {
61
+ const routeModels = [];
62
+
63
+ for (const route of extractedRoutes) {
64
+ const path = route.path || '';
65
+ const isDynamicRoute = route.isDynamic || isDynamicPath(path);
66
+
67
+ // Determine route type
68
+ let type = ROUTE_TYPE.STATIC;
69
+ let stability = ROUTE_STABILITY.STATIC;
70
+
71
+ if (isDynamicRoute) {
72
+ type = ROUTE_TYPE.DYNAMIC;
73
+
74
+ // Check if dynamic route can be normalized
75
+ if (route.originalPattern || route.exampleExecution) {
76
+ stability = ROUTE_STABILITY.DYNAMIC; // Can be normalized
77
+ } else {
78
+ stability = ROUTE_STABILITY.AMBIGUOUS; // Cannot be deterministically validated
79
+ }
80
+ }
81
+
82
+ // Determine source
83
+ let source = ROUTE_SOURCE.PROGRAMMATIC;
84
+ if (route.framework === 'next-pages' || route.framework === 'next-app') {
85
+ source = ROUTE_SOURCE.FILE_SYSTEM;
86
+ } else if (route.framework === 'react-router') {
87
+ source = ROUTE_SOURCE.JSX;
88
+ } else if (route.framework === 'vue-router') {
89
+ source = ROUTE_SOURCE.CONFIG;
90
+ }
91
+
92
+ const routeModel = {
93
+ path,
94
+ type,
95
+ stability,
96
+ source,
97
+ framework: route.framework || 'unknown',
98
+ sourceRef: route.sourceRef || `${route.file || 'unknown'}:${route.line || 1}`,
99
+ file: route.file || null,
100
+ line: route.line || null,
101
+ };
102
+
103
+ // Add dynamic route metadata
104
+ if (isDynamicRoute) {
105
+ routeModel.originalPattern = route.originalPattern || path;
106
+ routeModel.isDynamic = true;
107
+ routeModel.exampleExecution = route.exampleExecution || false;
108
+ routeModel.parameters = route.parameters || extractParameters(path);
109
+ }
110
+
111
+ routeModels.push(routeModel);
112
+ }
113
+
114
+ return routeModels;
115
+ }
116
+
117
+ /**
118
+ * Extract parameter names from dynamic route path
119
+ */
120
+ function extractParameters(path) {
121
+ const parameters = [];
122
+
123
+ // React/Vue Router :param
124
+ const reactMatches = path.matchAll(/:(\w+)/g);
125
+ for (const match of reactMatches) {
126
+ parameters.push(match[1]);
127
+ }
128
+
129
+ // Next.js [param]
130
+ const nextMatches = path.matchAll(/\[(\w+)\]/g);
131
+ for (const match of nextMatches) {
132
+ parameters.push(match[1]);
133
+ }
134
+
135
+ return parameters;
136
+ }
137
+
138
+ /**
139
+ * PHASE 12: Correlate navigation promise with route
140
+ *
141
+ * @param {string} navigationTarget - Navigation target from promise
142
+ * @param {Array<RouteModel>} routeModels - Available route models
143
+ * @returns {Object|null} Correlation result or null
144
+ */
145
+ export function correlateNavigationWithRoute(navigationTarget, routeModels) {
146
+ if (!navigationTarget || typeof navigationTarget !== 'string') {
147
+ return null;
148
+ }
149
+
150
+ // Normalize navigation target (handle dynamic targets)
151
+ const normalized = normalizeNavigationTarget(navigationTarget);
152
+ const targetToMatch = normalized.exampleTarget || navigationTarget;
153
+
154
+ // Try exact match first
155
+ let matchedRoute = routeModels.find(r => r.path === targetToMatch);
156
+
157
+ if (matchedRoute) {
158
+ return {
159
+ route: matchedRoute,
160
+ matchType: 'exact',
161
+ confidence: 1.0,
162
+ normalizedTarget: targetToMatch,
163
+ originalTarget: navigationTarget,
164
+ isDynamic: normalized.isDynamic,
165
+ };
166
+ }
167
+
168
+ // Try prefix match for dynamic routes
169
+ for (const route of routeModels) {
170
+ if (route.isDynamic && route.originalPattern) {
171
+ // Check if target matches dynamic pattern
172
+ const patternMatch = matchDynamicPattern(targetToMatch, route.originalPattern);
173
+ if (patternMatch) {
174
+ return {
175
+ route: route,
176
+ matchType: 'pattern',
177
+ confidence: 0.9,
178
+ normalizedTarget: targetToMatch,
179
+ originalTarget: navigationTarget,
180
+ isDynamic: true,
181
+ patternMatch: patternMatch,
182
+ };
183
+ }
184
+ }
185
+ }
186
+
187
+ // No match found
188
+ return null;
189
+ }
190
+
191
+ /**
192
+ * Match a target path against a dynamic route pattern
193
+ */
194
+ function matchDynamicPattern(target, pattern) {
195
+ // Convert pattern to regex
196
+ // React Router: /users/:id -> /users/(\w+)
197
+ // Next.js: /users/[id] -> /users/(\w+)
198
+
199
+ let regexPattern = pattern;
200
+
201
+ // Replace :param with (\w+)
202
+ regexPattern = regexPattern.replace(/:(\w+)/g, '(\\w+)');
203
+
204
+ // Replace [param] with (\w+)
205
+ regexPattern = regexPattern.replace(/\[(\w+)\]/g, '(\\w+)');
206
+
207
+ // Escape other special characters
208
+ regexPattern = regexPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
209
+
210
+ // Restore the capture groups
211
+ regexPattern = regexPattern.replace(/\\\(\\\\w\+\\\)/g, '(\\w+)');
212
+
213
+ const regex = new RegExp(`^${regexPattern}$`);
214
+ const match = target.match(regex);
215
+
216
+ return match ? { matched: true, groups: match.slice(1) } : null;
217
+ }
218
+
219
+ /**
220
+ * PHASE 12: Evaluate route navigation outcome
221
+ *
222
+ * @param {Object} correlation - Route correlation result
223
+ * @param {Object} trace - Interaction trace
224
+ * @param {string} beforeUrl - URL before interaction
225
+ * @param {string} afterUrl - URL after interaction
226
+ * @returns {Object} Evaluation result
227
+ */
228
+ export function evaluateRouteNavigation(correlation, trace, beforeUrl, afterUrl) {
229
+ if (!correlation) {
230
+ return {
231
+ outcome: 'NO_ROUTE_MATCH',
232
+ confidence: 0.0,
233
+ reason: 'Navigation target does not match any known route',
234
+ };
235
+ }
236
+
237
+ const { route, normalizedTarget } = correlation;
238
+ const sensors = trace.sensors || {};
239
+ const navSensor = sensors.navigation || {};
240
+
241
+ // Check URL change
242
+ const urlChanged = navSensor.urlChanged === true ||
243
+ (beforeUrl && afterUrl && beforeUrl !== afterUrl);
244
+
245
+ // Check if URL matches route
246
+ const afterPath = extractPathFromUrl(afterUrl);
247
+ const routeMatched = afterPath === route.path ||
248
+ afterPath === normalizedTarget ||
249
+ (route.isDynamic && matchDynamicPattern(afterPath, route.originalPattern));
250
+
251
+ // Check router state change (for SPAs)
252
+ const routerStateChanged = navSensor.routerStateChanged === true ||
253
+ navSensor.routeChanged === true ||
254
+ (navSensor.routerEvents && navSensor.routerEvents.length > 0);
255
+
256
+ // Check UI change
257
+ const uiChanged = sensors.uiSignals?.diff?.changed === true;
258
+ const domChanged = trace.after?.dom !== trace.before?.dom;
259
+
260
+ // PHASE 12: Evidence Law - require before/after + supporting signal
261
+ const hasEvidence = (beforeUrl && afterUrl) &&
262
+ (urlChanged || routerStateChanged || uiChanged || domChanged);
263
+
264
+ if (routeMatched && hasEvidence) {
265
+ return {
266
+ outcome: 'VERIFIED',
267
+ confidence: route.stability === ROUTE_STABILITY.STATIC ? 1.0 : 0.9,
268
+ reason: null,
269
+ evidence: {
270
+ urlChanged,
271
+ routerStateChanged,
272
+ uiChanged,
273
+ domChanged,
274
+ routeMatched: true,
275
+ },
276
+ };
277
+ }
278
+
279
+ if (!routeMatched && urlChanged) {
280
+ return {
281
+ outcome: 'ROUTE_MISMATCH',
282
+ confidence: 0.8,
283
+ reason: 'Navigation occurred but target route does not match',
284
+ evidence: {
285
+ urlChanged: true,
286
+ expectedRoute: route.path,
287
+ actualPath: afterPath,
288
+ },
289
+ };
290
+ }
291
+
292
+ if (!hasEvidence) {
293
+ return {
294
+ outcome: 'SILENT_FAILURE',
295
+ confidence: correlation.confidence,
296
+ reason: 'Navigation promise not fulfilled - no URL change, router state change, or UI change',
297
+ evidence: {
298
+ urlChanged: false,
299
+ routerStateChanged: false,
300
+ uiChanged: false,
301
+ domChanged: false,
302
+ },
303
+ };
304
+ }
305
+
306
+ // Ambiguous case
307
+ if (route.stability === ROUTE_STABILITY.AMBIGUOUS) {
308
+ return {
309
+ outcome: 'SUSPECTED',
310
+ confidence: 0.6,
311
+ reason: 'Dynamic route cannot be deterministically validated',
312
+ evidence: {
313
+ routeStability: 'ambiguous',
314
+ urlChanged,
315
+ routerStateChanged,
316
+ uiChanged,
317
+ },
318
+ };
319
+ }
320
+
321
+ return {
322
+ outcome: 'UNKNOWN',
323
+ confidence: 0.5,
324
+ reason: 'Route navigation outcome unclear',
325
+ };
326
+ }
327
+
328
+ /**
329
+ * Extract path from URL
330
+ */
331
+ function extractPathFromUrl(url) {
332
+ if (!url || typeof url !== 'string') return '';
333
+
334
+ try {
335
+ const urlObj = new URL(url);
336
+ return urlObj.pathname;
337
+ } catch {
338
+ // Relative URL
339
+ const pathMatch = url.match(/^([^?#]+)/);
340
+ return pathMatch ? pathMatch[1] : url;
341
+ }
342
+ }
343
+
344
+ /**
345
+ * PHASE 12: Build evidence for route-related finding
346
+ *
347
+ * @param {Object} correlation - Route correlation
348
+ * @param {Object} navigationPromise - Navigation promise from expectation
349
+ * @param {Object} evaluation - Route evaluation result
350
+ * @param {Object} trace - Interaction trace
351
+ * @returns {Object} Evidence object
352
+ */
353
+ export function buildRouteEvidence(correlation, navigationPromise, evaluation, trace) {
354
+ const evidence = {
355
+ routeDefinition: {
356
+ path: correlation?.route?.path || null,
357
+ type: correlation?.route?.type || null,
358
+ stability: correlation?.route?.stability || null,
359
+ source: correlation?.route?.source || null,
360
+ sourceRef: correlation?.route?.sourceRef || null,
361
+ },
362
+ navigationTrigger: {
363
+ target: navigationPromise?.target || null,
364
+ method: navigationPromise?.method || null,
365
+ astSource: navigationPromise?.astSource || null,
366
+ context: navigationPromise?.context || null,
367
+ },
368
+ beforeAfter: {
369
+ beforeUrl: trace.before?.url || null,
370
+ afterUrl: trace.after?.url || null,
371
+ beforeScreenshot: trace.before?.screenshot || null,
372
+ afterScreenshot: trace.after?.screenshot || null,
373
+ },
374
+ signals: {
375
+ urlChanged: evaluation.evidence?.urlChanged || false,
376
+ routerStateChanged: evaluation.evidence?.routerStateChanged || false,
377
+ uiChanged: evaluation.evidence?.uiChanged || false,
378
+ domChanged: evaluation.evidence?.domChanged || false,
379
+ routeMatched: evaluation.evidence?.routeMatched || false,
380
+ },
381
+ evaluation: {
382
+ outcome: evaluation.outcome,
383
+ confidence: evaluation.confidence,
384
+ reason: evaluation.reason,
385
+ },
386
+ };
387
+
388
+ return evidence;
389
+ }
390
+
391
+ /**
392
+ * PHASE 12: Check if route change is false positive
393
+ *
394
+ * @param {Object} trace - Interaction trace
395
+ * @param {Object} correlation - Route correlation
396
+ * @returns {boolean} True if should be ignored (false positive)
397
+ */
398
+ export function isRouteChangeFalsePositive(trace, correlation) {
399
+ const sensors = trace.sensors || {};
400
+ const navSensor = sensors.navigation || {};
401
+
402
+ // Internal state-only route changes (shallow routing)
403
+ if (navSensor.shallowRouting === true && !navSensor.urlChanged) {
404
+ return true;
405
+ }
406
+
407
+ // Analytics-only navigation
408
+ const networkSensor = sensors.network || {};
409
+ const hasAnalyticsRequest = networkSensor.observedRequestUrls?.some(url =>
410
+ url && typeof url === 'string' && url.includes('/api/analytics')
411
+ );
412
+
413
+ if (hasAnalyticsRequest && !navSensor.urlChanged && !sensors.uiSignals?.diff?.changed) {
414
+ return true;
415
+ }
416
+
417
+ return false;
418
+ }
419
+