@veraxhq/verax 0.2.0 → 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 (217) hide show
  1. package/README.md +14 -18
  2. package/bin/verax.js +7 -0
  3. package/package.json +15 -5
  4. package/src/cli/commands/baseline.js +104 -0
  5. package/src/cli/commands/default.js +323 -111
  6. package/src/cli/commands/doctor.js +36 -4
  7. package/src/cli/commands/ga.js +243 -0
  8. package/src/cli/commands/gates.js +95 -0
  9. package/src/cli/commands/inspect.js +131 -2
  10. package/src/cli/commands/release-check.js +213 -0
  11. package/src/cli/commands/run.js +498 -103
  12. package/src/cli/commands/security-check.js +211 -0
  13. package/src/cli/commands/truth.js +114 -0
  14. package/src/cli/entry.js +305 -68
  15. package/src/cli/util/angular-component-extractor.js +179 -0
  16. package/src/cli/util/angular-navigation-detector.js +141 -0
  17. package/src/cli/util/angular-network-detector.js +161 -0
  18. package/src/cli/util/angular-state-detector.js +162 -0
  19. package/src/cli/util/ast-interactive-detector.js +546 -0
  20. package/src/cli/util/ast-network-detector.js +603 -0
  21. package/src/cli/util/ast-usestate-detector.js +602 -0
  22. package/src/cli/util/bootstrap-guard.js +86 -0
  23. package/src/cli/util/detection-engine.js +4 -3
  24. package/src/cli/util/determinism-runner.js +123 -0
  25. package/src/cli/util/determinism-writer.js +129 -0
  26. package/src/cli/util/env-url.js +4 -0
  27. package/src/cli/util/events.js +76 -0
  28. package/src/cli/util/expectation-extractor.js +380 -74
  29. package/src/cli/util/findings-writer.js +126 -15
  30. package/src/cli/util/learn-writer.js +3 -1
  31. package/src/cli/util/observation-engine.js +69 -23
  32. package/src/cli/util/observe-writer.js +3 -1
  33. package/src/cli/util/paths.js +6 -14
  34. package/src/cli/util/project-discovery.js +23 -0
  35. package/src/cli/util/project-writer.js +3 -1
  36. package/src/cli/util/redact.js +2 -2
  37. package/src/cli/util/run-resolver.js +64 -0
  38. package/src/cli/util/runtime-budget.js +147 -0
  39. package/src/cli/util/source-requirement.js +55 -0
  40. package/src/cli/util/summary-writer.js +13 -1
  41. package/src/cli/util/svelte-navigation-detector.js +163 -0
  42. package/src/cli/util/svelte-network-detector.js +80 -0
  43. package/src/cli/util/svelte-sfc-extractor.js +147 -0
  44. package/src/cli/util/svelte-state-detector.js +243 -0
  45. package/src/cli/util/vue-navigation-detector.js +177 -0
  46. package/src/cli/util/vue-sfc-extractor.js +162 -0
  47. package/src/cli/util/vue-state-detector.js +215 -0
  48. package/src/types/global.d.ts +28 -0
  49. package/src/types/ts-ast.d.ts +24 -0
  50. package/src/verax/cli/doctor.js +2 -2
  51. package/src/verax/cli/finding-explainer.js +56 -3
  52. package/src/verax/cli/init.js +1 -1
  53. package/src/verax/cli/url-safety.js +12 -2
  54. package/src/verax/cli/wizard.js +13 -2
  55. package/src/verax/core/artifacts/registry.js +154 -0
  56. package/src/verax/core/artifacts/verifier.js +980 -0
  57. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  58. package/src/verax/core/baseline/baseline.snapshot.js +231 -0
  59. package/src/verax/core/budget-engine.js +1 -1
  60. package/src/verax/core/capabilities/gates.js +499 -0
  61. package/src/verax/core/capabilities/registry.js +475 -0
  62. package/src/verax/core/confidence/confidence-compute.js +137 -0
  63. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  64. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  65. package/src/verax/core/confidence/confidence-weights.js +44 -0
  66. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  67. package/src/verax/core/confidence/confidence.loader.js +79 -0
  68. package/src/verax/core/confidence/confidence.schema.js +94 -0
  69. package/src/verax/core/confidence-engine-refactor.js +484 -0
  70. package/src/verax/core/confidence-engine.js +486 -0
  71. package/src/verax/core/confidence-engine.js.backup +471 -0
  72. package/src/verax/core/contracts/index.js +29 -0
  73. package/src/verax/core/contracts/types.js +185 -0
  74. package/src/verax/core/contracts/validators.js +381 -0
  75. package/src/verax/core/decision-snapshot.js +31 -4
  76. package/src/verax/core/decisions/decision.trace.js +276 -0
  77. package/src/verax/core/determinism/contract-writer.js +89 -0
  78. package/src/verax/core/determinism/contract.js +139 -0
  79. package/src/verax/core/determinism/diff.js +364 -0
  80. package/src/verax/core/determinism/engine.js +221 -0
  81. package/src/verax/core/determinism/finding-identity.js +148 -0
  82. package/src/verax/core/determinism/normalize.js +438 -0
  83. package/src/verax/core/determinism/report-writer.js +92 -0
  84. package/src/verax/core/determinism/run-fingerprint.js +118 -0
  85. package/src/verax/core/determinism-model.js +35 -6
  86. package/src/verax/core/dynamic-route-intelligence.js +528 -0
  87. package/src/verax/core/evidence/evidence-capture-service.js +307 -0
  88. package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
  89. package/src/verax/core/evidence-builder.js +487 -0
  90. package/src/verax/core/execution-mode-context.js +77 -0
  91. package/src/verax/core/execution-mode-detector.js +190 -0
  92. package/src/verax/core/failures/exit-codes.js +86 -0
  93. package/src/verax/core/failures/failure-summary.js +76 -0
  94. package/src/verax/core/failures/failure.factory.js +225 -0
  95. package/src/verax/core/failures/failure.ledger.js +132 -0
  96. package/src/verax/core/failures/failure.types.js +196 -0
  97. package/src/verax/core/failures/index.js +10 -0
  98. package/src/verax/core/ga/ga-report-writer.js +43 -0
  99. package/src/verax/core/ga/ga.artifact.js +49 -0
  100. package/src/verax/core/ga/ga.contract.js +434 -0
  101. package/src/verax/core/ga/ga.enforcer.js +86 -0
  102. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  103. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  104. package/src/verax/core/guardrails/policy.loader.js +83 -0
  105. package/src/verax/core/guardrails/policy.schema.js +110 -0
  106. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  107. package/src/verax/core/guardrails-engine.js +505 -0
  108. package/src/verax/core/incremental-store.js +15 -7
  109. package/src/verax/core/observe/run-timeline.js +316 -0
  110. package/src/verax/core/perf/perf.contract.js +186 -0
  111. package/src/verax/core/perf/perf.display.js +65 -0
  112. package/src/verax/core/perf/perf.enforcer.js +91 -0
  113. package/src/verax/core/perf/perf.monitor.js +209 -0
  114. package/src/verax/core/perf/perf.report.js +198 -0
  115. package/src/verax/core/pipeline-tracker.js +238 -0
  116. package/src/verax/core/product-definition.js +127 -0
  117. package/src/verax/core/release/provenance.builder.js +271 -0
  118. package/src/verax/core/release/release-report-writer.js +40 -0
  119. package/src/verax/core/release/release.enforcer.js +159 -0
  120. package/src/verax/core/release/reproducibility.check.js +221 -0
  121. package/src/verax/core/release/sbom.builder.js +283 -0
  122. package/src/verax/core/replay-validator.js +4 -4
  123. package/src/verax/core/replay.js +1 -1
  124. package/src/verax/core/report/cross-index.js +192 -0
  125. package/src/verax/core/report/human-summary.js +222 -0
  126. package/src/verax/core/route-intelligence.js +419 -0
  127. package/src/verax/core/security/secrets.scan.js +326 -0
  128. package/src/verax/core/security/security-report.js +50 -0
  129. package/src/verax/core/security/security.enforcer.js +124 -0
  130. package/src/verax/core/security/supplychain.defaults.json +38 -0
  131. package/src/verax/core/security/supplychain.policy.js +326 -0
  132. package/src/verax/core/security/vuln.scan.js +265 -0
  133. package/src/verax/core/silence-impact.js +1 -1
  134. package/src/verax/core/silence-model.js +9 -7
  135. package/src/verax/core/truth/truth.certificate.js +250 -0
  136. package/src/verax/core/ui-feedback-intelligence.js +515 -0
  137. package/src/verax/detect/comparison.js +8 -3
  138. package/src/verax/detect/confidence-engine.js +645 -57
  139. package/src/verax/detect/confidence-helper.js +33 -0
  140. package/src/verax/detect/detection-engine.js +19 -2
  141. package/src/verax/detect/dynamic-route-findings.js +335 -0
  142. package/src/verax/detect/evidence-index.js +15 -65
  143. package/src/verax/detect/expectation-chain-detector.js +417 -0
  144. package/src/verax/detect/expectation-model.js +56 -3
  145. package/src/verax/detect/explanation-helpers.js +1 -1
  146. package/src/verax/detect/finding-detector.js +2 -2
  147. package/src/verax/detect/findings-writer.js +149 -20
  148. package/src/verax/detect/flow-detector.js +4 -4
  149. package/src/verax/detect/index.js +265 -15
  150. package/src/verax/detect/interactive-findings.js +3 -4
  151. package/src/verax/detect/journey-stall-detector.js +558 -0
  152. package/src/verax/detect/route-findings.js +218 -0
  153. package/src/verax/detect/signal-mapper.js +2 -2
  154. package/src/verax/detect/skip-classifier.js +4 -4
  155. package/src/verax/detect/ui-feedback-findings.js +207 -0
  156. package/src/verax/detect/verdict-engine.js +61 -9
  157. package/src/verax/detect/view-switch-correlator.js +242 -0
  158. package/src/verax/flow/flow-engine.js +3 -2
  159. package/src/verax/flow/flow-spec.js +1 -2
  160. package/src/verax/index.js +413 -33
  161. package/src/verax/intel/effect-detector.js +1 -1
  162. package/src/verax/intel/index.js +2 -2
  163. package/src/verax/intel/route-extractor.js +3 -3
  164. package/src/verax/intel/vue-navigation-extractor.js +81 -18
  165. package/src/verax/intel/vue-router-extractor.js +4 -2
  166. package/src/verax/learn/action-contract-extractor.js +684 -66
  167. package/src/verax/learn/ast-contract-extractor.js +53 -1
  168. package/src/verax/learn/index.js +36 -2
  169. package/src/verax/learn/manifest-writer.js +28 -14
  170. package/src/verax/learn/route-extractor.js +1 -1
  171. package/src/verax/learn/route-validator.js +12 -8
  172. package/src/verax/learn/state-extractor.js +1 -1
  173. package/src/verax/learn/static-extractor-navigation.js +1 -1
  174. package/src/verax/learn/static-extractor-validation.js +2 -2
  175. package/src/verax/learn/static-extractor.js +8 -7
  176. package/src/verax/learn/ts-contract-resolver.js +14 -12
  177. package/src/verax/observe/browser.js +22 -3
  178. package/src/verax/observe/console-sensor.js +2 -2
  179. package/src/verax/observe/expectation-executor.js +2 -1
  180. package/src/verax/observe/focus-sensor.js +1 -1
  181. package/src/verax/observe/human-driver.js +29 -10
  182. package/src/verax/observe/index.js +92 -844
  183. package/src/verax/observe/interaction-discovery.js +27 -15
  184. package/src/verax/observe/interaction-runner.js +31 -14
  185. package/src/verax/observe/loading-sensor.js +6 -0
  186. package/src/verax/observe/navigation-sensor.js +1 -1
  187. package/src/verax/observe/observe-context.js +205 -0
  188. package/src/verax/observe/observe-helpers.js +191 -0
  189. package/src/verax/observe/observe-runner.js +226 -0
  190. package/src/verax/observe/observers/budget-observer.js +185 -0
  191. package/src/verax/observe/observers/console-observer.js +102 -0
  192. package/src/verax/observe/observers/coverage-observer.js +107 -0
  193. package/src/verax/observe/observers/interaction-observer.js +471 -0
  194. package/src/verax/observe/observers/navigation-observer.js +132 -0
  195. package/src/verax/observe/observers/network-observer.js +87 -0
  196. package/src/verax/observe/observers/safety-observer.js +82 -0
  197. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  198. package/src/verax/observe/settle.js +1 -0
  199. package/src/verax/observe/state-sensor.js +8 -4
  200. package/src/verax/observe/state-ui-sensor.js +7 -1
  201. package/src/verax/observe/traces-writer.js +27 -16
  202. package/src/verax/observe/ui-feedback-detector.js +742 -0
  203. package/src/verax/observe/ui-signal-sensor.js +155 -2
  204. package/src/verax/scan-summary-writer.js +46 -9
  205. package/src/verax/shared/artifact-manager.js +9 -6
  206. package/src/verax/shared/budget-profiles.js +2 -2
  207. package/src/verax/shared/caching.js +1 -1
  208. package/src/verax/shared/config-loader.js +1 -2
  209. package/src/verax/shared/css-spinner-rules.js +204 -0
  210. package/src/verax/shared/dynamic-route-utils.js +12 -6
  211. package/src/verax/shared/retry-policy.js +1 -6
  212. package/src/verax/shared/root-artifacts.js +1 -1
  213. package/src/verax/shared/view-switch-rules.js +208 -0
  214. package/src/verax/shared/zip-artifacts.js +1 -0
  215. package/src/verax/validate/context-validator.js +1 -1
  216. package/src/verax/observe/index.js.backup +0 -1
  217. package/src/verax/validate/context-validator.js.bak +0 -0
@@ -222,10 +222,12 @@ export class DecisionRecorder {
222
222
  * @param {Object} budget
223
223
  */
224
224
  export function recordBudgetProfile(recorder, profileName, budget) {
225
+ const now = Date.now();
225
226
  recorder.recordBatch([
226
227
  {
227
228
  decision_id: DECISION_IDS.BUDGET_PROFILE_SELECTED,
228
229
  category: 'BUDGET',
230
+ timestamp: now,
229
231
  inputs: { env_var: process.env.VERAX_BUDGET_PROFILE || 'STANDARD' },
230
232
  chosen_value: profileName,
231
233
  reason: `Budget profile selected: ${profileName}`
@@ -233,6 +235,7 @@ export function recordBudgetProfile(recorder, profileName, budget) {
233
235
  {
234
236
  decision_id: DECISION_IDS.BUDGET_MAX_INTERACTIONS,
235
237
  category: 'BUDGET',
238
+ timestamp: now,
236
239
  inputs: { profile: profileName },
237
240
  chosen_value: budget.maxInteractionsPerPage,
238
241
  reason: `Max interactions per page from ${profileName} profile`
@@ -240,6 +243,7 @@ export function recordBudgetProfile(recorder, profileName, budget) {
240
243
  {
241
244
  decision_id: DECISION_IDS.BUDGET_MAX_PAGES,
242
245
  category: 'BUDGET',
246
+ timestamp: now,
243
247
  inputs: { profile: profileName },
244
248
  chosen_value: budget.maxPages,
245
249
  reason: `Max pages from ${profileName} profile`
@@ -247,6 +251,7 @@ export function recordBudgetProfile(recorder, profileName, budget) {
247
251
  {
248
252
  decision_id: DECISION_IDS.BUDGET_SCAN_DURATION,
249
253
  category: 'BUDGET',
254
+ timestamp: now,
250
255
  inputs: { profile: profileName },
251
256
  chosen_value: budget.maxScanDurationMs,
252
257
  reason: `Scan duration limit from ${profileName} profile`
@@ -260,10 +265,12 @@ export function recordBudgetProfile(recorder, profileName, budget) {
260
265
  * @param {Object} budget
261
266
  */
262
267
  export function recordTimeoutConfig(recorder, budget) {
268
+ const now = Date.now();
263
269
  recorder.recordBatch([
264
270
  {
265
271
  decision_id: DECISION_IDS.TIMEOUT_NAVIGATION,
266
272
  category: 'TIMEOUT',
273
+ timestamp: now,
267
274
  inputs: { budget_config: true },
268
275
  chosen_value: budget.navigationTimeoutMs,
269
276
  reason: 'Navigation timeout from budget configuration'
@@ -271,6 +278,7 @@ export function recordTimeoutConfig(recorder, budget) {
271
278
  {
272
279
  decision_id: DECISION_IDS.TIMEOUT_INTERACTION,
273
280
  category: 'TIMEOUT',
281
+ timestamp: now,
274
282
  inputs: { budget_config: true },
275
283
  chosen_value: budget.interactionTimeoutMs,
276
284
  reason: 'Interaction timeout from budget configuration'
@@ -278,6 +286,7 @@ export function recordTimeoutConfig(recorder, budget) {
278
286
  {
279
287
  decision_id: DECISION_IDS.TIMEOUT_SETTLE,
280
288
  category: 'TIMEOUT',
289
+ timestamp: now,
281
290
  inputs: { budget_config: true },
282
291
  chosen_value: budget.settleTimeoutMs,
283
292
  reason: 'Settle timeout from budget configuration'
@@ -285,6 +294,7 @@ export function recordTimeoutConfig(recorder, budget) {
285
294
  {
286
295
  decision_id: DECISION_IDS.TIMEOUT_STABILIZATION,
287
296
  category: 'TIMEOUT',
297
+ timestamp: now,
288
298
  inputs: { budget_config: true },
289
299
  chosen_value: budget.stabilizationWindowMs,
290
300
  reason: 'Stabilization window from budget configuration'
@@ -301,9 +311,11 @@ export function recordTimeoutConfig(recorder, budget) {
301
311
  * @param {string} reason
302
312
  */
303
313
  export function recordAdaptiveStabilization(recorder, enabled, wasExtended = false, extensionMs = 0, reason = '') {
314
+ const now = Date.now();
304
315
  recorder.record({
305
316
  decision_id: DECISION_IDS.ADAPTIVE_STABILIZATION_ENABLED,
306
317
  category: 'ADAPTIVE_STABILIZATION',
318
+ timestamp: now,
307
319
  inputs: { budget_config: true },
308
320
  chosen_value: enabled,
309
321
  reason: enabled ? 'Adaptive stabilization enabled by budget profile' : 'Adaptive stabilization disabled'
@@ -313,6 +325,7 @@ export function recordAdaptiveStabilization(recorder, enabled, wasExtended = fal
313
325
  recorder.record({
314
326
  decision_id: DECISION_IDS.ADAPTIVE_STABILIZATION_EXTENDED,
315
327
  category: 'ADAPTIVE_STABILIZATION',
328
+ timestamp: now,
316
329
  inputs: { dom_changing: true, network_active: true },
317
330
  chosen_value: extensionMs,
318
331
  reason: reason || `Extended stabilization by ${extensionMs}ms due to ongoing changes`
@@ -333,10 +346,12 @@ export function recordRetryAttempt(recorder, operationType, attemptNumber, delay
333
346
  DECISION_IDS.RETRY_NAVIGATION_ATTEMPTED :
334
347
  DECISION_IDS.RETRY_INTERACTION_ATTEMPTED;
335
348
 
349
+ const now = Date.now();
336
350
  recorder.recordBatch([
337
351
  {
338
352
  decision_id: decisionId,
339
353
  category: 'RETRY',
354
+ timestamp: now,
340
355
  inputs: { attempt: attemptNumber, error_type: errorType },
341
356
  chosen_value: true,
342
357
  reason: `Retry attempt ${attemptNumber} for ${operationType} due to ${errorType}`
@@ -344,6 +359,7 @@ export function recordRetryAttempt(recorder, operationType, attemptNumber, delay
344
359
  {
345
360
  decision_id: DECISION_IDS.RETRY_BACKOFF_DELAY,
346
361
  category: 'RETRY',
362
+ timestamp: now,
347
363
  inputs: { attempt: attemptNumber },
348
364
  chosen_value: delayMs,
349
365
  reason: `Exponential backoff delay: ${delayMs}ms`
@@ -355,22 +371,32 @@ export function recordRetryAttempt(recorder, operationType, attemptNumber, delay
355
371
  * Record budget truncation
356
372
  * @param {DecisionRecorder} recorder
357
373
  * @param {string} truncationType - 'interactions' | 'pages' | 'scan_time'
358
- * @param {number} limit
359
- * @param {number} actual
374
+ * @param {Object|number} limitOrOptions - Either a number (limit) or object with {limit, reached/elapsed, scope?}
375
+ * @param {number} [actual] - Actual value (only used if limitOrOptions is a number)
360
376
  */
361
- export function recordTruncation(recorder, truncationType, limit, actual) {
377
+ export function recordTruncation(recorder, truncationType, limitOrOptions, actual = null) {
362
378
  const decisionIdMap = {
363
379
  interactions: DECISION_IDS.TRUNCATION_INTERACTIONS_CAPPED,
364
380
  pages: DECISION_IDS.TRUNCATION_PAGES_CAPPED,
365
381
  scan_time: DECISION_IDS.TRUNCATION_SCAN_TIME_EXCEEDED
366
382
  };
367
383
 
384
+ let limit, actualValue;
385
+ if (typeof limitOrOptions === 'object' && limitOrOptions !== null) {
386
+ limit = limitOrOptions.limit;
387
+ actualValue = limitOrOptions.reached || limitOrOptions.elapsed || limitOrOptions.actual || 0;
388
+ } else {
389
+ limit = limitOrOptions;
390
+ actualValue = actual || 0;
391
+ }
392
+
368
393
  recorder.record({
369
394
  decision_id: decisionIdMap[truncationType] || DECISION_IDS.TRUNCATION_BUDGET_EXCEEDED,
370
395
  category: 'TRUNCATION',
371
- inputs: { limit, actual },
372
- chosen_value: actual,
373
- reason: `Budget exceeded: ${truncationType} capped at ${limit} (attempted ${actual})`
396
+ timestamp: Date.now(),
397
+ inputs: { limit, actual: actualValue },
398
+ chosen_value: actualValue,
399
+ reason: `Budget exceeded: ${truncationType} capped at ${limit} (attempted ${actualValue})`
374
400
  });
375
401
  }
376
402
 
@@ -381,11 +407,13 @@ export function recordTruncation(recorder, truncationType, limit, actual) {
381
407
  */
382
408
  export function recordEnvironment(recorder, environment) {
383
409
  const { browserType = 'unknown', viewport = { width: 1280, height: 720 } } = environment;
410
+ const now = Date.now();
384
411
 
385
412
  recorder.recordBatch([
386
413
  {
387
414
  decision_id: DECISION_IDS.ENV_BROWSER_DETECTED,
388
415
  category: 'ENVIRONMENT',
416
+ timestamp: now,
389
417
  inputs: { detected: true },
390
418
  chosen_value: browserType,
391
419
  reason: `Browser type: ${browserType}`
@@ -393,6 +421,7 @@ export function recordEnvironment(recorder, environment) {
393
421
  {
394
422
  decision_id: DECISION_IDS.ENV_VIEWPORT_SIZE,
395
423
  category: 'ENVIRONMENT',
424
+ timestamp: now,
396
425
  inputs: { default_viewport: true },
397
426
  chosen_value: viewport,
398
427
  reason: `Viewport size: ${viewport.width}x${viewport.height}`
@@ -0,0 +1,528 @@
1
+ /**
2
+ * PHASE 14 — Dynamic Routes: Truth, Intent & Safe Support
3
+ *
4
+ * Dynamic route intelligence layer that:
5
+ * - Classifies dynamic routes by verifiability
6
+ * - Correlates navigation promises with route definitions and UI outcomes
7
+ * - Produces evidence-backed findings or explicit ambiguity
8
+ * - Prevents false positives and false promises
9
+ */
10
+
11
+ import { isDynamicPath, normalizeDynamicRoute, normalizeNavigationTarget } from '../shared/dynamic-route-utils.js';
12
+ import { correlateNavigationWithRoute, evaluateRouteNavigation } from './route-intelligence.js';
13
+ import { scoreUIFeedback, detectUIFeedbackSignals } from './ui-feedback-intelligence.js';
14
+
15
+ /**
16
+ * PHASE 14: Dynamic Route Verifiability Classification
17
+ */
18
+ export const DYNAMIC_ROUTE_VERIFIABILITY = {
19
+ STATIC: 'STATIC',
20
+ VERIFIED_DYNAMIC: 'VERIFIED_DYNAMIC',
21
+ AMBIGUOUS_DYNAMIC: 'AMBIGUOUS_DYNAMIC',
22
+ UNVERIFIABLE_DYNAMIC: 'UNVERIFIABLE_DYNAMIC',
23
+ };
24
+
25
+ /**
26
+ * PHASE 14: Route Verdict
27
+ */
28
+ export const ROUTE_VERDICT = {
29
+ VERIFIED: 'VERIFIED',
30
+ SILENT_FAILURE: 'SILENT_FAILURE',
31
+ ROUTE_MISMATCH: 'ROUTE_MISMATCH',
32
+ AMBIGUOUS: 'AMBIGUOUS',
33
+ };
34
+
35
+ /**
36
+ * PHASE 14: Classify dynamic route by verifiability
37
+ *
38
+ * @param {Object} routeModel - Route model from route intelligence
39
+ * @param {Object} trace - Interaction trace (optional, for runtime analysis)
40
+ * @returns {Object} Classification result
41
+ */
42
+ export function classifyDynamicRoute(routeModel, trace = null) {
43
+ const path = routeModel.path || '';
44
+ const originalPattern = routeModel.originalPattern || path;
45
+
46
+ // STATIC: No dynamic parameters
47
+ if (!isDynamicPath(path) && !isDynamicPath(originalPattern)) {
48
+ return {
49
+ verifiability: DYNAMIC_ROUTE_VERIFIABILITY.STATIC,
50
+ reason: 'Route contains no dynamic parameters',
51
+ confidence: 1.0,
52
+ };
53
+ }
54
+
55
+ // Check if route can be normalized
56
+ const normalized = normalizeDynamicRoute(originalPattern);
57
+ if (!normalized || !normalized.examplePath) {
58
+ return {
59
+ verifiability: DYNAMIC_ROUTE_VERIFIABILITY.UNVERIFIABLE_DYNAMIC,
60
+ reason: 'Dynamic route pattern cannot be normalized to example path',
61
+ confidence: 0.9,
62
+ };
63
+ }
64
+
65
+ // Check route characteristics that affect verifiability
66
+ const isAuthGated = isAuthGatedRoute(routeModel, trace);
67
+ const isSSROnly = isSSROnlyRoute(routeModel, trace);
68
+ const isRuntimeOnly = isRuntimeOnlyRoute(routeModel, trace);
69
+ const hasObservableSignals = hasObservableSignals(routeModel, trace);
70
+
71
+ // UNVERIFIABLE: Auth-gated, SSR-only, or runtime-only without observable signals
72
+ if (isAuthGated || isSSROnly || (isRuntimeOnly && !hasObservableSignals)) {
73
+ return {
74
+ verifiability: DYNAMIC_ROUTE_VERIFIABILITY.UNVERIFIABLE_DYNAMIC,
75
+ reason: buildUnverifiableReason(isAuthGated, isSSROnly, isRuntimeOnly, hasObservableSignals),
76
+ confidence: 0.9,
77
+ };
78
+ }
79
+
80
+ // Check if we can verify the outcome
81
+ if (trace) {
82
+ const canVerify = canVerifyRouteOutcome(routeModel, trace);
83
+
84
+ if (canVerify.verifiable) {
85
+ return {
86
+ verifiability: DYNAMIC_ROUTE_VERIFIABILITY.VERIFIED_DYNAMIC,
87
+ reason: canVerify.reason,
88
+ confidence: canVerify.confidence,
89
+ };
90
+ } else {
91
+ return {
92
+ verifiability: DYNAMIC_ROUTE_VERIFIABILITY.AMBIGUOUS_DYNAMIC,
93
+ reason: canVerify.reason || 'Route pattern known but outcome unclear',
94
+ confidence: 0.6,
95
+ };
96
+ }
97
+ }
98
+
99
+ // Default: AMBIGUOUS if pattern is known but we can't verify yet
100
+ return {
101
+ verifiability: DYNAMIC_ROUTE_VERIFIABILITY.AMBIGUOUS_DYNAMIC,
102
+ reason: 'Route pattern known but outcome cannot be verified without trace data',
103
+ confidence: 0.6,
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Check if route is auth-gated
109
+ */
110
+ function isAuthGatedRoute(routeModel, trace) {
111
+ // Check route path patterns
112
+ const path = routeModel.path || '';
113
+ const authPatterns = ['/admin', '/dashboard', '/account', '/settings', '/profile', '/user'];
114
+
115
+ if (authPatterns.some(pattern => path.includes(pattern))) {
116
+ return true;
117
+ }
118
+
119
+ // Check trace for auth-related signals
120
+ if (trace) {
121
+ const sensors = trace.sensors || {};
122
+ const navSensor = sensors.navigation || {};
123
+
124
+ // If navigation was blocked or redirected to login
125
+ if (navSensor.blockedNavigations?.length > 0) {
126
+ return true;
127
+ }
128
+ }
129
+
130
+ return false;
131
+ }
132
+
133
+ /**
134
+ * Check if route is SSR-only
135
+ */
136
+ function isSSROnlyRoute(routeModel, trace) {
137
+ // Next.js app router with dynamic segments might be SSR-only
138
+ if (routeModel.framework === 'next-app' && routeModel.isDynamic) {
139
+ // Check if route has getServerSideProps or similar indicators
140
+ // For now, we'll be conservative and not mark as SSR-only without evidence
141
+ return false;
142
+ }
143
+
144
+ return false;
145
+ }
146
+
147
+ /**
148
+ * Check if route is runtime-only (no static analysis possible)
149
+ */
150
+ function isRuntimeOnlyRoute(routeModel, trace) {
151
+ // Routes with complex template literals or runtime variables
152
+ const originalPattern = routeModel.originalPattern || '';
153
+
154
+ // Pure variable references like ${path} or ${id} without pattern
155
+ if (originalPattern.includes('${') && !originalPattern.match(/\/[^$]+\$\{[^}]+\}/)) {
156
+ return true;
157
+ }
158
+
159
+ return false;
160
+ }
161
+
162
+ /**
163
+ * Check if route has observable signals
164
+ */
165
+ function hasObservableSignals(routeModel, trace) {
166
+ if (!trace) return false;
167
+
168
+ const sensors = trace.sensors || {};
169
+ const navSensor = sensors.navigation || {};
170
+ const uiSignals = sensors.uiSignals || {};
171
+ const uiFeedback = sensors.uiFeedback || {};
172
+
173
+ // Check for URL change
174
+ if (navSensor.urlChanged === true) {
175
+ return true;
176
+ }
177
+
178
+ // Check for UI feedback
179
+ if (uiSignals.diff?.changed === true || uiFeedback.overallUiFeedbackScore > 0.3) {
180
+ return true;
181
+ }
182
+
183
+ // Check for DOM change
184
+ if (trace.dom?.beforeHash !== trace.dom?.afterHash) {
185
+ return true;
186
+ }
187
+
188
+ return false;
189
+ }
190
+
191
+ /**
192
+ * Check if route outcome can be verified
193
+ */
194
+ function canVerifyRouteOutcome(routeModel, trace) {
195
+ const sensors = trace.sensors || {};
196
+ const navSensor = sensors.navigation || {};
197
+ const beforeUrl = trace.before?.url || navSensor.beforeUrl || '';
198
+ const afterUrl = trace.after?.url || navSensor.afterUrl || '';
199
+
200
+ // Check URL change
201
+ const urlChanged = navSensor.urlChanged === true || (beforeUrl && afterUrl && beforeUrl !== afterUrl);
202
+
203
+ // Check if URL matches route pattern
204
+ const afterPath = extractPathFromUrl(afterUrl);
205
+ const routeMatched = matchDynamicPattern(afterPath, routeModel.originalPattern || routeModel.path);
206
+
207
+ // Check UI feedback
208
+ const uiSignals = detectUIFeedbackSignals(trace);
209
+ const hasUIFeedback = uiSignals.length > 0;
210
+
211
+ // Check DOM change
212
+ const domChanged = trace.dom?.beforeHash !== trace.dom?.afterHash;
213
+
214
+ if (urlChanged && routeMatched && (hasUIFeedback || domChanged)) {
215
+ return {
216
+ verifiable: true,
217
+ reason: 'URL changed, route pattern matched, and UI feedback or DOM change observed',
218
+ confidence: 0.9,
219
+ };
220
+ }
221
+
222
+ if (urlChanged && routeMatched) {
223
+ return {
224
+ verifiable: true,
225
+ reason: 'URL changed and route pattern matched',
226
+ confidence: 0.8,
227
+ };
228
+ }
229
+
230
+ if (urlChanged && !routeMatched) {
231
+ return {
232
+ verifiable: false,
233
+ reason: 'URL changed but does not match route pattern',
234
+ confidence: 0.7,
235
+ };
236
+ }
237
+
238
+ return {
239
+ verifiable: false,
240
+ reason: 'No URL change or observable signals',
241
+ confidence: 0.5,
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Build reason for unverifiable route
247
+ */
248
+ function buildUnverifiableReason(isAuthGated, isSSROnly, isRuntimeOnly, hasObservableSignals) {
249
+ const reasons = [];
250
+
251
+ if (isAuthGated) {
252
+ reasons.push('auth-gated');
253
+ }
254
+ if (isSSROnly) {
255
+ reasons.push('SSR-only');
256
+ }
257
+ if (isRuntimeOnly) {
258
+ reasons.push('runtime-only');
259
+ }
260
+ if (!hasObservableSignals) {
261
+ reasons.push('no observable signals');
262
+ }
263
+
264
+ return `Route is ${reasons.join(', ')}`;
265
+ }
266
+
267
+ /**
268
+ * Match dynamic pattern against actual path
269
+ */
270
+ function matchDynamicPattern(actualPath, pattern) {
271
+ if (!actualPath || !pattern) return false;
272
+
273
+ // Convert pattern to regex
274
+ let regexPattern = pattern;
275
+
276
+ // Replace :param with (\w+)
277
+ regexPattern = regexPattern.replace(/:(\w+)/g, '(\\w+)');
278
+
279
+ // Replace [param] with (\w+)
280
+ regexPattern = regexPattern.replace(/\[(\w+)\]/g, '(\\w+)');
281
+
282
+ // Escape other special characters
283
+ regexPattern = regexPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
284
+
285
+ // Restore the capture groups
286
+ regexPattern = regexPattern.replace(/\\\(\\\\w\+\\\)/g, '(\\w+)');
287
+
288
+ const regex = new RegExp(`^${regexPattern}$`);
289
+ return regex.test(actualPath);
290
+ }
291
+
292
+ /**
293
+ * Extract path from URL
294
+ */
295
+ function extractPathFromUrl(url) {
296
+ if (!url || typeof url !== 'string') return '';
297
+
298
+ try {
299
+ const urlObj = new URL(url);
300
+ return urlObj.pathname;
301
+ } catch {
302
+ // Relative URL
303
+ const pathMatch = url.match(/^([^?#]+)/);
304
+ return pathMatch ? pathMatch[1] : url;
305
+ }
306
+ }
307
+
308
+ /**
309
+ * PHASE 14: Correlate navigation promise with dynamic route and UI feedback
310
+ *
311
+ * @param {Object} expectation - Navigation expectation
312
+ * @param {Object} routeModel - Route model
313
+ * @param {Object} trace - Interaction trace
314
+ * @returns {Object} Correlation result with verdict
315
+ */
316
+ export function correlateDynamicRouteNavigation(expectation, routeModel, trace) {
317
+ const classification = classifyDynamicRoute(routeModel, trace);
318
+
319
+ // If route is UNVERIFIABLE, return skip
320
+ if (classification.verifiability === DYNAMIC_ROUTE_VERIFIABILITY.UNVERIFIABLE_DYNAMIC) {
321
+ return {
322
+ verdict: null,
323
+ skip: true,
324
+ skipReason: classification.reason,
325
+ confidence: classification.confidence,
326
+ };
327
+ }
328
+
329
+ // Normalize navigation target
330
+ const navigationTarget = expectation.targetPath || expectation.expectedTarget || '';
331
+ const normalized = normalizeNavigationTarget(navigationTarget);
332
+ const targetToMatch = normalized.exampleTarget || navigationTarget;
333
+
334
+ // Match route pattern
335
+ const routeMatched = matchDynamicPattern(targetToMatch, routeModel.originalPattern || routeModel.path);
336
+
337
+ // Get route evaluation from route intelligence
338
+ const correlation = correlateNavigationWithRoute(navigationTarget, [routeModel]);
339
+ const routeEvaluation = correlation ? evaluateRouteNavigation(correlation, trace, trace.before?.url || '', trace.after?.url || '') : null;
340
+
341
+ // Get UI feedback score
342
+ const uiSignals = detectUIFeedbackSignals(trace);
343
+ const feedbackScore = scoreUIFeedback(uiSignals, expectation, trace);
344
+
345
+ // Determine verdict
346
+ const sensors = trace.sensors || {};
347
+ const navSensor = sensors.navigation || {};
348
+ const urlChanged = navSensor.urlChanged === true;
349
+ const afterPath = extractPathFromUrl(trace.after?.url || navSensor.afterUrl || '');
350
+
351
+ // VERIFIED: URL changed, route matched, and UI feedback present
352
+ if (urlChanged && routeMatched && feedbackScore.score === 'FEEDBACK_CONFIRMED') {
353
+ return {
354
+ verdict: ROUTE_VERDICT.VERIFIED,
355
+ skip: false,
356
+ confidence: 0.9,
357
+ reason: 'Navigation successful: URL changed, route matched, and UI feedback confirmed',
358
+ evidence: {
359
+ urlChanged: true,
360
+ routeMatched: true,
361
+ uiFeedback: feedbackScore.score,
362
+ afterPath,
363
+ },
364
+ };
365
+ }
366
+
367
+ // VERIFIED: URL changed and route matched (even without explicit UI feedback)
368
+ if (urlChanged && routeMatched) {
369
+ return {
370
+ verdict: ROUTE_VERDICT.VERIFIED,
371
+ skip: false,
372
+ confidence: 0.85,
373
+ reason: 'Navigation successful: URL changed and route matched',
374
+ evidence: {
375
+ urlChanged: true,
376
+ routeMatched: true,
377
+ afterPath,
378
+ },
379
+ };
380
+ }
381
+
382
+ // ROUTE_MISMATCH: URL changed but doesn't match route
383
+ if (urlChanged && !routeMatched) {
384
+ return {
385
+ verdict: ROUTE_VERDICT.ROUTE_MISMATCH,
386
+ skip: false,
387
+ confidence: 0.8,
388
+ reason: `Navigation occurred but target route does not match. Expected pattern: ${routeModel.originalPattern}, Actual: ${afterPath}`,
389
+ evidence: {
390
+ urlChanged: true,
391
+ routeMatched: false,
392
+ expectedPattern: routeModel.originalPattern,
393
+ actualPath: afterPath,
394
+ },
395
+ };
396
+ }
397
+
398
+ // SILENT_FAILURE: No URL change, no route match, no UI feedback
399
+ if (!urlChanged && !routeMatched && feedbackScore.score === 'FEEDBACK_MISSING') {
400
+ return {
401
+ verdict: ROUTE_VERDICT.SILENT_FAILURE,
402
+ skip: false,
403
+ confidence: 0.85,
404
+ reason: 'Navigation promise not fulfilled: no URL change, route mismatch, and no UI feedback',
405
+ evidence: {
406
+ urlChanged: false,
407
+ routeMatched: false,
408
+ uiFeedback: feedbackScore.score,
409
+ },
410
+ };
411
+ }
412
+
413
+ // AMBIGUOUS: Unclear outcome
414
+ if (classification.verifiability === DYNAMIC_ROUTE_VERIFIABILITY.AMBIGUOUS_DYNAMIC) {
415
+ return {
416
+ verdict: ROUTE_VERDICT.AMBIGUOUS,
417
+ skip: false,
418
+ confidence: 0.6,
419
+ reason: classification.reason || 'Dynamic route outcome is ambiguous',
420
+ evidence: {
421
+ classification: classification.verifiability,
422
+ urlChanged,
423
+ routeMatched,
424
+ uiFeedback: feedbackScore.score,
425
+ },
426
+ };
427
+ }
428
+
429
+ // Default: AMBIGUOUS
430
+ return {
431
+ verdict: ROUTE_VERDICT.AMBIGUOUS,
432
+ skip: false,
433
+ confidence: 0.5,
434
+ reason: 'Route navigation outcome unclear',
435
+ evidence: {
436
+ urlChanged,
437
+ routeMatched,
438
+ uiFeedback: feedbackScore.score,
439
+ },
440
+ };
441
+ }
442
+
443
+ /**
444
+ * PHASE 14: Build evidence for dynamic route finding
445
+ *
446
+ * @param {Object} expectation - Navigation expectation
447
+ * @param {Object} routeModel - Route model
448
+ * @param {Object} correlation - Correlation result
449
+ * @param {Object} trace - Interaction trace
450
+ * @returns {Object} Evidence object
451
+ */
452
+ export function buildDynamicRouteEvidence(expectation, routeModel, correlation, trace) {
453
+ const classification = classifyDynamicRoute(routeModel, trace);
454
+ const uiSignals = detectUIFeedbackSignals(trace);
455
+ const feedbackScore = scoreUIFeedback(uiSignals, expectation, trace);
456
+
457
+ const evidence = {
458
+ routeDefinition: {
459
+ path: routeModel.path,
460
+ originalPattern: routeModel.originalPattern || routeModel.path,
461
+ type: routeModel.type,
462
+ stability: routeModel.stability,
463
+ source: routeModel.source,
464
+ sourceRef: routeModel.sourceRef,
465
+ verifiability: classification.verifiability,
466
+ verifiabilityReason: classification.reason,
467
+ },
468
+ navigationTrigger: {
469
+ target: expectation.targetPath || expectation.expectedTarget || null,
470
+ method: expectation.promise?.method || null,
471
+ astSource: expectation.source?.astSource || null,
472
+ context: expectation.source?.context || null,
473
+ },
474
+ beforeAfter: {
475
+ beforeUrl: trace.before?.url || trace.sensors?.navigation?.beforeUrl || null,
476
+ afterUrl: trace.after?.url || trace.sensors?.navigation?.afterUrl || null,
477
+ beforeScreenshot: trace.before?.screenshot || null,
478
+ afterScreenshot: trace.after?.screenshot || null,
479
+ beforeDomHash: trace.dom?.beforeHash || null,
480
+ afterDomHash: trace.dom?.afterHash || null,
481
+ },
482
+ signals: {
483
+ urlChanged: correlation.evidence?.urlChanged || false,
484
+ routeMatched: correlation.evidence?.routeMatched || false,
485
+ uiFeedback: feedbackScore.score,
486
+ uiFeedbackSignals: uiSignals.map(s => ({
487
+ type: s.type,
488
+ confidence: s.confidence,
489
+ })),
490
+ domChanged: trace.dom?.beforeHash !== trace.dom?.afterHash,
491
+ },
492
+ correlation: {
493
+ verdict: correlation.verdict,
494
+ confidence: correlation.confidence,
495
+ reason: correlation.reason,
496
+ skip: correlation.skip || false,
497
+ skipReason: correlation.skipReason || null,
498
+ },
499
+ };
500
+
501
+ return evidence;
502
+ }
503
+
504
+ /**
505
+ * PHASE 14: Check if route should be skipped (unverifiable)
506
+ *
507
+ * @param {Object} routeModel - Route model
508
+ * @param {Object} trace - Interaction trace
509
+ * @returns {Object} Skip decision
510
+ */
511
+ export function shouldSkipDynamicRoute(routeModel, trace) {
512
+ const classification = classifyDynamicRoute(routeModel, trace);
513
+
514
+ if (classification.verifiability === DYNAMIC_ROUTE_VERIFIABILITY.UNVERIFIABLE_DYNAMIC) {
515
+ return {
516
+ skip: true,
517
+ reason: classification.reason,
518
+ confidence: classification.confidence,
519
+ };
520
+ }
521
+
522
+ return {
523
+ skip: false,
524
+ reason: null,
525
+ confidence: 0,
526
+ };
527
+ }
528
+