@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.
- package/README.md +14 -18
- package/bin/verax.js +7 -0
- package/package.json +15 -5
- package/src/cli/commands/baseline.js +104 -0
- package/src/cli/commands/default.js +323 -111
- package/src/cli/commands/doctor.js +36 -4
- package/src/cli/commands/ga.js +243 -0
- package/src/cli/commands/gates.js +95 -0
- package/src/cli/commands/inspect.js +131 -2
- package/src/cli/commands/release-check.js +213 -0
- package/src/cli/commands/run.js +498 -103
- package/src/cli/commands/security-check.js +211 -0
- package/src/cli/commands/truth.js +114 -0
- package/src/cli/entry.js +305 -68
- package/src/cli/util/angular-component-extractor.js +179 -0
- package/src/cli/util/angular-navigation-detector.js +141 -0
- package/src/cli/util/angular-network-detector.js +161 -0
- package/src/cli/util/angular-state-detector.js +162 -0
- package/src/cli/util/ast-interactive-detector.js +546 -0
- package/src/cli/util/ast-network-detector.js +603 -0
- package/src/cli/util/ast-usestate-detector.js +602 -0
- package/src/cli/util/bootstrap-guard.js +86 -0
- package/src/cli/util/detection-engine.js +4 -3
- package/src/cli/util/determinism-runner.js +123 -0
- package/src/cli/util/determinism-writer.js +129 -0
- package/src/cli/util/env-url.js +4 -0
- package/src/cli/util/events.js +76 -0
- package/src/cli/util/expectation-extractor.js +380 -74
- package/src/cli/util/findings-writer.js +126 -15
- package/src/cli/util/learn-writer.js +3 -1
- package/src/cli/util/observation-engine.js +69 -23
- package/src/cli/util/observe-writer.js +3 -1
- package/src/cli/util/paths.js +6 -14
- package/src/cli/util/project-discovery.js +23 -0
- package/src/cli/util/project-writer.js +3 -1
- package/src/cli/util/redact.js +2 -2
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +13 -1
- package/src/cli/util/svelte-navigation-detector.js +163 -0
- package/src/cli/util/svelte-network-detector.js +80 -0
- package/src/cli/util/svelte-sfc-extractor.js +147 -0
- package/src/cli/util/svelte-state-detector.js +243 -0
- package/src/cli/util/vue-navigation-detector.js +177 -0
- package/src/cli/util/vue-sfc-extractor.js +162 -0
- package/src/cli/util/vue-state-detector.js +215 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/doctor.js +2 -2
- package/src/verax/cli/finding-explainer.js +56 -3
- package/src/verax/cli/init.js +1 -1
- package/src/verax/cli/url-safety.js +12 -2
- package/src/verax/cli/wizard.js +13 -2
- package/src/verax/core/artifacts/registry.js +154 -0
- package/src/verax/core/artifacts/verifier.js +980 -0
- package/src/verax/core/baseline/baseline.enforcer.js +137 -0
- package/src/verax/core/baseline/baseline.snapshot.js +231 -0
- package/src/verax/core/budget-engine.js +1 -1
- package/src/verax/core/capabilities/gates.js +499 -0
- package/src/verax/core/capabilities/registry.js +475 -0
- package/src/verax/core/confidence/confidence-compute.js +137 -0
- package/src/verax/core/confidence/confidence-invariants.js +234 -0
- package/src/verax/core/confidence/confidence-report-writer.js +112 -0
- package/src/verax/core/confidence/confidence-weights.js +44 -0
- package/src/verax/core/confidence/confidence.defaults.js +65 -0
- package/src/verax/core/confidence/confidence.loader.js +79 -0
- package/src/verax/core/confidence/confidence.schema.js +94 -0
- package/src/verax/core/confidence-engine-refactor.js +484 -0
- package/src/verax/core/confidence-engine.js +486 -0
- package/src/verax/core/confidence-engine.js.backup +471 -0
- package/src/verax/core/contracts/index.js +29 -0
- package/src/verax/core/contracts/types.js +185 -0
- package/src/verax/core/contracts/validators.js +381 -0
- package/src/verax/core/decision-snapshot.js +31 -4
- package/src/verax/core/decisions/decision.trace.js +276 -0
- package/src/verax/core/determinism/contract-writer.js +89 -0
- package/src/verax/core/determinism/contract.js +139 -0
- package/src/verax/core/determinism/diff.js +364 -0
- package/src/verax/core/determinism/engine.js +221 -0
- package/src/verax/core/determinism/finding-identity.js +148 -0
- package/src/verax/core/determinism/normalize.js +438 -0
- package/src/verax/core/determinism/report-writer.js +92 -0
- package/src/verax/core/determinism/run-fingerprint.js +118 -0
- package/src/verax/core/determinism-model.js +35 -6
- package/src/verax/core/dynamic-route-intelligence.js +528 -0
- package/src/verax/core/evidence/evidence-capture-service.js +307 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
- package/src/verax/core/evidence-builder.js +487 -0
- package/src/verax/core/execution-mode-context.js +77 -0
- package/src/verax/core/execution-mode-detector.js +190 -0
- package/src/verax/core/failures/exit-codes.js +86 -0
- package/src/verax/core/failures/failure-summary.js +76 -0
- package/src/verax/core/failures/failure.factory.js +225 -0
- package/src/verax/core/failures/failure.ledger.js +132 -0
- package/src/verax/core/failures/failure.types.js +196 -0
- package/src/verax/core/failures/index.js +10 -0
- package/src/verax/core/ga/ga-report-writer.js +43 -0
- package/src/verax/core/ga/ga.artifact.js +49 -0
- package/src/verax/core/ga/ga.contract.js +434 -0
- package/src/verax/core/ga/ga.enforcer.js +86 -0
- package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
- package/src/verax/core/guardrails/policy.defaults.js +210 -0
- package/src/verax/core/guardrails/policy.loader.js +83 -0
- package/src/verax/core/guardrails/policy.schema.js +110 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
- package/src/verax/core/guardrails-engine.js +505 -0
- package/src/verax/core/incremental-store.js +15 -7
- package/src/verax/core/observe/run-timeline.js +316 -0
- package/src/verax/core/perf/perf.contract.js +186 -0
- package/src/verax/core/perf/perf.display.js +65 -0
- package/src/verax/core/perf/perf.enforcer.js +91 -0
- package/src/verax/core/perf/perf.monitor.js +209 -0
- package/src/verax/core/perf/perf.report.js +198 -0
- package/src/verax/core/pipeline-tracker.js +238 -0
- package/src/verax/core/product-definition.js +127 -0
- package/src/verax/core/release/provenance.builder.js +271 -0
- package/src/verax/core/release/release-report-writer.js +40 -0
- package/src/verax/core/release/release.enforcer.js +159 -0
- package/src/verax/core/release/reproducibility.check.js +221 -0
- package/src/verax/core/release/sbom.builder.js +283 -0
- package/src/verax/core/replay-validator.js +4 -4
- package/src/verax/core/replay.js +1 -1
- package/src/verax/core/report/cross-index.js +192 -0
- package/src/verax/core/report/human-summary.js +222 -0
- package/src/verax/core/route-intelligence.js +419 -0
- package/src/verax/core/security/secrets.scan.js +326 -0
- package/src/verax/core/security/security-report.js +50 -0
- package/src/verax/core/security/security.enforcer.js +124 -0
- package/src/verax/core/security/supplychain.defaults.json +38 -0
- package/src/verax/core/security/supplychain.policy.js +326 -0
- package/src/verax/core/security/vuln.scan.js +265 -0
- package/src/verax/core/silence-impact.js +1 -1
- package/src/verax/core/silence-model.js +9 -7
- package/src/verax/core/truth/truth.certificate.js +250 -0
- package/src/verax/core/ui-feedback-intelligence.js +515 -0
- package/src/verax/detect/comparison.js +8 -3
- package/src/verax/detect/confidence-engine.js +645 -57
- package/src/verax/detect/confidence-helper.js +33 -0
- package/src/verax/detect/detection-engine.js +19 -2
- package/src/verax/detect/dynamic-route-findings.js +335 -0
- package/src/verax/detect/evidence-index.js +15 -65
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +56 -3
- package/src/verax/detect/explanation-helpers.js +1 -1
- package/src/verax/detect/finding-detector.js +2 -2
- package/src/verax/detect/findings-writer.js +149 -20
- package/src/verax/detect/flow-detector.js +4 -4
- package/src/verax/detect/index.js +265 -15
- package/src/verax/detect/interactive-findings.js +3 -4
- package/src/verax/detect/journey-stall-detector.js +558 -0
- package/src/verax/detect/route-findings.js +218 -0
- package/src/verax/detect/signal-mapper.js +2 -2
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/ui-feedback-findings.js +207 -0
- package/src/verax/detect/verdict-engine.js +61 -9
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +413 -33
- package/src/verax/intel/effect-detector.js +1 -1
- package/src/verax/intel/index.js +2 -2
- package/src/verax/intel/route-extractor.js +3 -3
- package/src/verax/intel/vue-navigation-extractor.js +81 -18
- package/src/verax/intel/vue-router-extractor.js +4 -2
- package/src/verax/learn/action-contract-extractor.js +684 -66
- package/src/verax/learn/ast-contract-extractor.js +53 -1
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +28 -14
- package/src/verax/learn/route-extractor.js +1 -1
- package/src/verax/learn/route-validator.js +12 -8
- package/src/verax/learn/state-extractor.js +1 -1
- package/src/verax/learn/static-extractor-navigation.js +1 -1
- package/src/verax/learn/static-extractor-validation.js +2 -2
- package/src/verax/learn/static-extractor.js +8 -7
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/browser.js +22 -3
- package/src/verax/observe/console-sensor.js +2 -2
- package/src/verax/observe/expectation-executor.js +2 -1
- package/src/verax/observe/focus-sensor.js +1 -1
- package/src/verax/observe/human-driver.js +29 -10
- package/src/verax/observe/index.js +92 -844
- package/src/verax/observe/interaction-discovery.js +27 -15
- package/src/verax/observe/interaction-runner.js +31 -14
- package/src/verax/observe/loading-sensor.js +6 -0
- package/src/verax/observe/navigation-sensor.js +1 -1
- package/src/verax/observe/observe-context.js +205 -0
- package/src/verax/observe/observe-helpers.js +191 -0
- package/src/verax/observe/observe-runner.js +226 -0
- package/src/verax/observe/observers/budget-observer.js +185 -0
- package/src/verax/observe/observers/console-observer.js +102 -0
- package/src/verax/observe/observers/coverage-observer.js +107 -0
- package/src/verax/observe/observers/interaction-observer.js +471 -0
- package/src/verax/observe/observers/navigation-observer.js +132 -0
- package/src/verax/observe/observers/network-observer.js +87 -0
- package/src/verax/observe/observers/safety-observer.js +82 -0
- package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
- package/src/verax/observe/settle.js +1 -0
- package/src/verax/observe/state-sensor.js +8 -4
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/traces-writer.js +27 -16
- package/src/verax/observe/ui-feedback-detector.js +742 -0
- package/src/verax/observe/ui-signal-sensor.js +155 -2
- package/src/verax/scan-summary-writer.js +46 -9
- package/src/verax/shared/artifact-manager.js +9 -6
- package/src/verax/shared/budget-profiles.js +2 -2
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/config-loader.js +1 -2
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/dynamic-route-utils.js +12 -6
- package/src/verax/shared/retry-policy.js +1 -6
- package/src/verax/shared/root-artifacts.js +1 -1
- package/src/verax/shared/view-switch-rules.js +208 -0
- package/src/verax/shared/zip-artifacts.js +1 -0
- package/src/verax/validate/context-validator.js +1 -1
- package/src/verax/observe/index.js.backup +0 -1
- 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,
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
+
|