@veraxhq/verax 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -20
- package/bin/verax.js +11 -18
- package/package.json +28 -7
- package/src/cli/commands/baseline.js +1 -2
- package/src/cli/commands/default.js +72 -81
- package/src/cli/commands/doctor.js +29 -0
- package/src/cli/commands/ga.js +3 -0
- package/src/cli/commands/gates.js +1 -1
- package/src/cli/commands/inspect.js +6 -133
- package/src/cli/commands/release-check.js +2 -0
- package/src/cli/commands/run.js +74 -246
- package/src/cli/commands/security-check.js +2 -1
- package/src/cli/commands/truth.js +0 -1
- package/src/cli/entry.js +82 -309
- package/src/cli/util/angular-component-extractor.js +2 -2
- package/src/cli/util/angular-navigation-detector.js +2 -2
- package/src/cli/util/ast-interactive-detector.js +4 -6
- package/src/cli/util/ast-network-detector.js +3 -3
- package/src/cli/util/ast-promise-extractor.js +581 -0
- package/src/cli/util/ast-usestate-detector.js +3 -3
- package/src/cli/util/atomic-write.js +12 -1
- package/src/cli/util/console-reporter.js +72 -0
- package/src/cli/util/detection-engine.js +105 -41
- package/src/cli/util/determinism-runner.js +2 -1
- package/src/cli/util/determinism-writer.js +1 -1
- package/src/cli/util/digest-engine.js +359 -0
- package/src/cli/util/dom-diff.js +226 -0
- package/src/cli/util/env-url.js +0 -4
- package/src/cli/util/evidence-engine.js +287 -0
- package/src/cli/util/expectation-extractor.js +217 -367
- package/src/cli/util/findings-writer.js +19 -126
- package/src/cli/util/framework-detector.js +572 -0
- package/src/cli/util/idgen.js +1 -1
- package/src/cli/util/interaction-planner.js +529 -0
- package/src/cli/util/learn-writer.js +2 -2
- package/src/cli/util/ledger-writer.js +110 -0
- package/src/cli/util/monorepo-resolver.js +162 -0
- package/src/cli/util/observation-engine.js +127 -278
- package/src/cli/util/observe-writer.js +2 -2
- package/src/cli/util/paths.js +12 -3
- package/src/cli/util/project-discovery.js +284 -3
- package/src/cli/util/project-writer.js +2 -2
- package/src/cli/util/run-id.js +23 -27
- package/src/cli/util/run-result.js +778 -0
- package/src/cli/util/selector-resolver.js +235 -0
- package/src/cli/util/summary-writer.js +2 -1
- package/src/cli/util/svelte-navigation-detector.js +3 -3
- package/src/cli/util/svelte-sfc-extractor.js +0 -1
- package/src/cli/util/svelte-state-detector.js +1 -2
- package/src/cli/util/trust-activation-integration.js +496 -0
- package/src/cli/util/trust-activation-wrapper.js +85 -0
- package/src/cli/util/trust-integration-hooks.js +164 -0
- package/src/cli/util/types.js +153 -0
- package/src/cli/util/url-validation.js +40 -0
- package/src/cli/util/vue-navigation-detector.js +4 -3
- package/src/cli/util/vue-sfc-extractor.js +1 -2
- package/src/cli/util/vue-state-detector.js +1 -1
- package/src/types/fs-augment.d.ts +23 -0
- package/src/types/global.d.ts +137 -0
- package/src/types/internal-types.d.ts +35 -0
- package/src/verax/cli/finding-explainer.js +3 -56
- package/src/verax/cli/init.js +4 -18
- package/src/verax/core/action-classifier.js +4 -3
- package/src/verax/core/artifacts/registry.js +0 -15
- package/src/verax/core/artifacts/verifier.js +18 -8
- package/src/verax/core/baseline/baseline.snapshot.js +2 -0
- package/src/verax/core/capabilities/gates.js +7 -1
- package/src/verax/core/confidence/confidence-compute.js +14 -7
- package/src/verax/core/confidence/confidence.loader.js +1 -0
- package/src/verax/core/confidence-engine-refactor.js +8 -3
- package/src/verax/core/confidence-engine.js +162 -23
- package/src/verax/core/contracts/types.js +1 -0
- package/src/verax/core/contracts/validators.js +79 -4
- package/src/verax/core/decision-snapshot.js +3 -30
- package/src/verax/core/decisions/decision.trace.js +2 -0
- package/src/verax/core/determinism/contract-writer.js +2 -2
- package/src/verax/core/determinism/contract.js +1 -1
- package/src/verax/core/determinism/diff.js +42 -1
- package/src/verax/core/determinism/engine.js +7 -6
- package/src/verax/core/determinism/finding-identity.js +3 -2
- package/src/verax/core/determinism/normalize.js +32 -4
- package/src/verax/core/determinism/report-writer.js +1 -0
- package/src/verax/core/determinism/run-fingerprint.js +7 -2
- package/src/verax/core/dynamic-route-intelligence.js +8 -7
- package/src/verax/core/evidence/evidence-capture-service.js +1 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +2 -1
- package/src/verax/core/evidence-builder.js +2 -2
- package/src/verax/core/execution-mode-context.js +1 -1
- package/src/verax/core/execution-mode-detector.js +5 -3
- package/src/verax/core/failures/exit-codes.js +39 -37
- package/src/verax/core/failures/failure-summary.js +1 -1
- package/src/verax/core/failures/failure.factory.js +3 -3
- package/src/verax/core/failures/failure.ledger.js +3 -2
- package/src/verax/core/ga/ga.artifact.js +1 -1
- package/src/verax/core/ga/ga.contract.js +3 -2
- package/src/verax/core/ga/ga.enforcer.js +1 -0
- package/src/verax/core/guardrails/policy.loader.js +1 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +1 -1
- package/src/verax/core/guardrails-engine.js +2 -2
- package/src/verax/core/incremental-store.js +1 -0
- package/src/verax/core/integrity/budget.js +138 -0
- package/src/verax/core/integrity/determinism.js +342 -0
- package/src/verax/core/integrity/integrity.js +208 -0
- package/src/verax/core/integrity/poisoning.js +108 -0
- package/src/verax/core/integrity/transaction.js +140 -0
- package/src/verax/core/observe/run-timeline.js +2 -0
- package/src/verax/core/perf/perf.report.js +2 -0
- package/src/verax/core/pipeline-tracker.js +5 -0
- package/src/verax/core/release/provenance.builder.js +73 -214
- package/src/verax/core/release/release.enforcer.js +14 -9
- package/src/verax/core/release/reproducibility.check.js +1 -0
- package/src/verax/core/release/sbom.builder.js +32 -23
- package/src/verax/core/replay-validator.js +2 -0
- package/src/verax/core/replay.js +4 -0
- package/src/verax/core/report/cross-index.js +6 -3
- package/src/verax/core/report/human-summary.js +141 -1
- package/src/verax/core/route-intelligence.js +4 -3
- package/src/verax/core/run-id.js +6 -3
- package/src/verax/core/run-manifest.js +4 -3
- package/src/verax/core/security/secrets.scan.js +10 -7
- package/src/verax/core/security/security.enforcer.js +4 -0
- package/src/verax/core/security/supplychain.policy.js +9 -1
- package/src/verax/core/security/vuln.scan.js +2 -2
- package/src/verax/core/truth/truth.certificate.js +3 -1
- package/src/verax/core/ui-feedback-intelligence.js +12 -46
- package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
- package/src/verax/detect/confidence-engine.js +100 -660
- package/src/verax/detect/confidence-helper.js +1 -0
- package/src/verax/detect/detection-engine.js +1 -18
- package/src/verax/detect/dynamic-route-findings.js +17 -14
- package/src/verax/detect/expectation-chain-detector.js +1 -1
- package/src/verax/detect/expectation-model.js +3 -5
- package/src/verax/detect/failure-cause-inference.js +293 -0
- package/src/verax/detect/findings-writer.js +126 -166
- package/src/verax/detect/flow-detector.js +2 -2
- package/src/verax/detect/form-silent-failure.js +98 -0
- package/src/verax/detect/index.js +51 -234
- package/src/verax/detect/invariants-enforcer.js +147 -0
- package/src/verax/detect/journey-stall-detector.js +4 -4
- package/src/verax/detect/navigation-silent-failure.js +82 -0
- package/src/verax/detect/problem-aggregator.js +361 -0
- package/src/verax/detect/route-findings.js +7 -6
- package/src/verax/detect/summary-writer.js +477 -0
- package/src/verax/detect/test-failure-cause-inference.js +314 -0
- package/src/verax/detect/ui-feedback-findings.js +18 -18
- package/src/verax/detect/verdict-engine.js +3 -57
- package/src/verax/detect/view-switch-correlator.js +2 -2
- package/src/verax/flow/flow-engine.js +2 -1
- package/src/verax/flow/flow-spec.js +0 -6
- package/src/verax/index.js +48 -412
- package/src/verax/intel/ts-program.js +1 -0
- package/src/verax/intel/vue-navigation-extractor.js +3 -0
- package/src/verax/learn/action-contract-extractor.js +67 -682
- package/src/verax/learn/ast-contract-extractor.js +1 -1
- package/src/verax/learn/flow-extractor.js +1 -0
- package/src/verax/learn/project-detector.js +5 -0
- package/src/verax/learn/react-router-extractor.js +2 -0
- package/src/verax/learn/route-validator.js +1 -4
- package/src/verax/learn/source-instrumenter.js +1 -0
- package/src/verax/learn/state-extractor.js +2 -1
- package/src/verax/learn/static-extractor.js +1 -0
- package/src/verax/observe/coverage-gaps.js +132 -0
- package/src/verax/observe/expectation-handler.js +126 -0
- package/src/verax/observe/incremental-skip.js +46 -0
- package/src/verax/observe/index.js +735 -84
- package/src/verax/observe/interaction-executor.js +192 -0
- package/src/verax/observe/interaction-runner.js +782 -530
- package/src/verax/observe/network-firewall.js +86 -0
- package/src/verax/observe/observation-builder.js +169 -0
- package/src/verax/observe/observe-context.js +1 -1
- package/src/verax/observe/observe-helpers.js +2 -1
- package/src/verax/observe/observe-runner.js +28 -24
- package/src/verax/observe/observers/budget-observer.js +3 -3
- package/src/verax/observe/observers/console-observer.js +4 -4
- package/src/verax/observe/observers/coverage-observer.js +4 -4
- package/src/verax/observe/observers/interaction-observer.js +3 -3
- package/src/verax/observe/observers/navigation-observer.js +4 -4
- package/src/verax/observe/observers/network-observer.js +4 -4
- package/src/verax/observe/observers/safety-observer.js +1 -1
- package/src/verax/observe/observers/ui-feedback-observer.js +4 -4
- package/src/verax/observe/page-traversal.js +138 -0
- package/src/verax/observe/snapshot-ops.js +94 -0
- package/src/verax/observe/ui-signal-sensor.js +2 -148
- package/src/verax/scan-summary-writer.js +10 -42
- package/src/verax/shared/artifact-manager.js +30 -13
- package/src/verax/shared/caching.js +1 -0
- package/src/verax/shared/expectation-tracker.js +1 -0
- package/src/verax/shared/zip-artifacts.js +6 -0
- package/src/verax/core/confidence-engine.js.backup +0 -471
- package/src/verax/shared/config-loader.js +0 -169
- /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
import { dirname, basename
|
|
2
|
+
import { dirname, basename } from 'path';
|
|
3
3
|
import { expectsNavigation } from './expectation-model.js';
|
|
4
4
|
import { hasMeaningfulUrlChange, hasVisibleChange, hasDomChange } from './comparison.js';
|
|
5
5
|
import { writeFindings } from './findings-writer.js';
|
|
6
6
|
import { getUrlPath } from './evidence-validator.js';
|
|
7
7
|
import { classifySkipReason, collectSkipReasons } from './skip-classifier.js';
|
|
8
8
|
import { detectInteractiveFindings } from './interactive-findings.js';
|
|
9
|
-
import { detectRouteFindings } from './route-findings.js';
|
|
10
|
-
import { detectUIFeedbackFindings } from './ui-feedback-findings.js';
|
|
11
|
-
import { detectDynamicRouteFindings } from './dynamic-route-findings.js';
|
|
12
|
-
import { addUnifiedConfidence } from './confidence-helper.js';
|
|
13
|
-
import { applyGuardrails } from '../core/guardrails-engine.js';
|
|
14
|
-
import { finalizeFindingTruth } from '../core/guardrails/truth-reconciliation.js';
|
|
15
|
-
import { writeGuardrailsReport } from '../core/guardrails/guardrails-report-writer.js';
|
|
16
|
-
import { computeFinalConfidence } from '../core/confidence/confidence-compute.js';
|
|
17
|
-
import { enforceConfidenceInvariants } from '../core/confidence/confidence-invariants.js';
|
|
18
|
-
import { writeConfidenceReport } from '../core/confidence/confidence-report-writer.js';
|
|
19
9
|
|
|
20
10
|
/**
|
|
21
11
|
* @param {string} manifestPath
|
|
@@ -35,7 +25,9 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
|
|
|
35
25
|
const manifestContent = readFileSync(manifestPath, 'utf-8');
|
|
36
26
|
const tracesContent = readFileSync(tracesPath, 'utf-8');
|
|
37
27
|
|
|
28
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
38
29
|
const manifest = JSON.parse(manifestContent);
|
|
30
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
39
31
|
const observation = JSON.parse(tracesContent);
|
|
40
32
|
|
|
41
33
|
const projectDir = manifest.projectDir;
|
|
@@ -108,20 +100,20 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
|
|
|
108
100
|
selectorHint.includes(interactionSelector) ||
|
|
109
101
|
interactionSelector.includes(normalizedSelectorHint) ||
|
|
110
102
|
normalizedSelectorHint === normalizedInteractionSelector) {
|
|
111
|
-
if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
|
|
103
|
+
if ((expectation.type === 'navigation' || expectation.type === 'spa_navigation') && (interaction.type === 'link' || interaction.type === 'button')) {
|
|
112
104
|
matchingExpectations.push(expectation);
|
|
113
105
|
} else if (expectation.type === 'form_submission' && interaction.type === 'form') {
|
|
114
106
|
matchingExpectations.push(expectation);
|
|
115
107
|
}
|
|
116
108
|
} else {
|
|
117
|
-
if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
|
|
109
|
+
if ((expectation.type === 'navigation' || expectation.type === 'spa_navigation') && (interaction.type === 'link' || interaction.type === 'button')) {
|
|
118
110
|
selectorMismatch = true;
|
|
119
111
|
} else if (expectation.type === 'form_submission' && interaction.type === 'form') {
|
|
120
112
|
selectorMismatch = true;
|
|
121
113
|
}
|
|
122
114
|
}
|
|
123
115
|
} else if (!selectorHint && !interactionSelector) {
|
|
124
|
-
if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
|
|
116
|
+
if ((expectation.type === 'navigation' || expectation.type === 'spa_navigation') && (interaction.type === 'link' || interaction.type === 'button')) {
|
|
125
117
|
matchingExpectations.push(expectation);
|
|
126
118
|
} else if (expectation.type === 'form_submission' && interaction.type === 'form') {
|
|
127
119
|
matchingExpectations.push(expectation);
|
|
@@ -132,9 +124,24 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
|
|
|
132
124
|
|
|
133
125
|
if (matchingExpectations.length > 1) {
|
|
134
126
|
multipleMatches = true;
|
|
127
|
+
// VISION TRANSPARENCY: Record ambiguity explicitly (not silent skip)
|
|
128
|
+
if (_silenceTracker) {
|
|
129
|
+
_silenceTracker.record({
|
|
130
|
+
scope: 'expectation',
|
|
131
|
+
reason: 'ambiguous_promise',
|
|
132
|
+
description: `Multiple expectations match interaction "${interaction.label}" (${matchingExpectations.length} candidates). Cannot determine intent without guessing.`,
|
|
133
|
+
context: {
|
|
134
|
+
interaction: { type: interaction.type, selector: interaction.selector, label: interaction.label },
|
|
135
|
+
candidateCount: matchingExpectations.length,
|
|
136
|
+
candidates: matchingExpectations.map(e => e.targetPath)
|
|
137
|
+
},
|
|
138
|
+
impact: 'interaction_not_evaluated',
|
|
139
|
+
outcome: 'UNPROVEN_INTERACTION'
|
|
140
|
+
});
|
|
141
|
+
}
|
|
135
142
|
} else if (matchingExpectations.length === 1) {
|
|
136
143
|
expectedTargetPath = matchingExpectations[0].targetPath;
|
|
137
|
-
expectationType = matchingExpectations[0].type;
|
|
144
|
+
expectationType = matchingExpectations[0].type === 'spa_navigation' ? 'navigation' : matchingExpectations[0].type;
|
|
138
145
|
} else if (selectorMismatch) {
|
|
139
146
|
skips.push({
|
|
140
147
|
code: 'SELECTOR_MISMATCH',
|
|
@@ -179,6 +186,21 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
|
|
|
179
186
|
}
|
|
180
187
|
|
|
181
188
|
if (matchingRoutes.length > 1) {
|
|
189
|
+
// VISION TRANSPARENCY: Record ambiguity explicitly
|
|
190
|
+
if (_silenceTracker) {
|
|
191
|
+
_silenceTracker.record({
|
|
192
|
+
scope: 'expectation',
|
|
193
|
+
reason: 'ambiguous_promise',
|
|
194
|
+
description: `Multiple routes match interaction "${interaction.label}" (${matchingRoutes.length} candidates). Conservative approach requires single clear match.`,
|
|
195
|
+
context: {
|
|
196
|
+
interaction: { type: interaction.type, selector: interaction.selector, label: interaction.label },
|
|
197
|
+
candidateCount: matchingRoutes.length,
|
|
198
|
+
candidates: matchingRoutes
|
|
199
|
+
},
|
|
200
|
+
impact: 'interaction_not_evaluated',
|
|
201
|
+
outcome: 'UNPROVEN_INTERACTION'
|
|
202
|
+
});
|
|
203
|
+
}
|
|
182
204
|
skips.push({
|
|
183
205
|
code: 'AMBIGUOUS_MATCH',
|
|
184
206
|
message: 'Multiple expectations could match; conservative approach requires single clear match',
|
|
@@ -197,6 +219,7 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
|
|
|
197
219
|
}
|
|
198
220
|
|
|
199
221
|
if (multipleMatches) {
|
|
222
|
+
// VISION TRANSPARENCY: Ambiguity already recorded in silence tracker above
|
|
200
223
|
skips.push({
|
|
201
224
|
code: 'AMBIGUOUS_MATCH',
|
|
202
225
|
message: 'Multiple expectations could match; conservative approach requires single clear match',
|
|
@@ -231,7 +254,7 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
|
|
|
231
254
|
|
|
232
255
|
if (expectationType === 'form_submission') {
|
|
233
256
|
if (normalizedAfter !== normalizedTarget && !hasUrlChange && !hasDomChangeResult) {
|
|
234
|
-
|
|
257
|
+
findings.push({
|
|
235
258
|
type: 'silent_failure',
|
|
236
259
|
interaction: {
|
|
237
260
|
type: interaction.type,
|
|
@@ -245,27 +268,7 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
|
|
|
245
268
|
beforeUrl: beforeUrl,
|
|
246
269
|
afterUrl: afterUrl
|
|
247
270
|
}
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
// PHASE 15: Add unified confidence
|
|
251
|
-
const findingWithConfidence = addUnifiedConfidence(finding, {
|
|
252
|
-
expectation: { targetPath: expectedTargetPath, type: expectationType },
|
|
253
|
-
sensors: trace.sensors || {},
|
|
254
|
-
comparisons: {
|
|
255
|
-
urlChanged: hasUrlChange,
|
|
256
|
-
domChanged: hasDomChangeResult,
|
|
257
|
-
},
|
|
258
|
-
evidence: {
|
|
259
|
-
beforeAfter: {
|
|
260
|
-
beforeScreenshot,
|
|
261
|
-
afterScreenshot,
|
|
262
|
-
beforeUrl,
|
|
263
|
-
afterUrl,
|
|
264
|
-
},
|
|
265
|
-
},
|
|
266
271
|
});
|
|
267
|
-
|
|
268
|
-
findings.push(findingWithConfidence);
|
|
269
272
|
}
|
|
270
273
|
} else if (expectationType === 'navigation') {
|
|
271
274
|
const urlMatchesTarget = normalizedAfter === normalizedTarget;
|
|
@@ -273,13 +276,17 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
|
|
|
273
276
|
|
|
274
277
|
if (!hasEffect) {
|
|
275
278
|
findings.push({
|
|
276
|
-
type: '
|
|
279
|
+
type: 'navigation_silent_failure',
|
|
277
280
|
interaction: {
|
|
278
281
|
type: interaction.type,
|
|
279
282
|
selector: interaction.selector,
|
|
280
283
|
label: interaction.label
|
|
281
284
|
},
|
|
282
285
|
reason: 'Expected user-visible outcome did not occur',
|
|
286
|
+
what_happened: 'Navigation attempt produced no visible effect',
|
|
287
|
+
what_was_expected: `Navigate to ${normalizedTarget || 'target page'}`,
|
|
288
|
+
what_was_observed: 'URL, DOM, and visuals remained unchanged',
|
|
289
|
+
why_it_matters: 'Users cannot reach the intended destination despite interacting',
|
|
283
290
|
evidence: {
|
|
284
291
|
before: beforeScreenshot,
|
|
285
292
|
after: afterScreenshot,
|
|
@@ -291,7 +298,7 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
|
|
|
291
298
|
}
|
|
292
299
|
} else {
|
|
293
300
|
if (!hasUrlChange && !hasVisibleChangeResult && !hasDomChangeResult) {
|
|
294
|
-
|
|
301
|
+
findings.push({
|
|
295
302
|
type: 'silent_failure',
|
|
296
303
|
interaction: {
|
|
297
304
|
type: interaction.type,
|
|
@@ -299,34 +306,17 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
|
|
|
299
306
|
label: interaction.label
|
|
300
307
|
},
|
|
301
308
|
reason: 'Expected user-visible outcome did not occur',
|
|
309
|
+
what_happened: 'User action produced no visible effect',
|
|
310
|
+
what_was_expected: 'Some user-visible change after interaction',
|
|
311
|
+
what_was_observed: 'URL, DOM, and visuals remained unchanged',
|
|
312
|
+
why_it_matters: 'Users cannot complete the intended action',
|
|
302
313
|
evidence: {
|
|
303
314
|
before: beforeScreenshot,
|
|
304
315
|
after: afterScreenshot,
|
|
305
316
|
beforeUrl: beforeUrl,
|
|
306
317
|
afterUrl: afterUrl
|
|
307
318
|
}
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
// PHASE 15: Add unified confidence
|
|
311
|
-
const findingWithConfidence = addUnifiedConfidence(finding, {
|
|
312
|
-
expectation: null,
|
|
313
|
-
sensors: trace.sensors || {},
|
|
314
|
-
comparisons: {
|
|
315
|
-
urlChanged: hasUrlChange,
|
|
316
|
-
domChanged: hasDomChangeResult,
|
|
317
|
-
visibleChanged: hasVisibleChangeResult,
|
|
318
|
-
},
|
|
319
|
-
evidence: {
|
|
320
|
-
beforeAfter: {
|
|
321
|
-
beforeScreenshot,
|
|
322
|
-
afterScreenshot,
|
|
323
|
-
beforeUrl,
|
|
324
|
-
afterUrl,
|
|
325
|
-
},
|
|
326
|
-
},
|
|
327
319
|
});
|
|
328
|
-
|
|
329
|
-
findings.push(findingWithConfidence);
|
|
330
320
|
}
|
|
331
321
|
}
|
|
332
322
|
}
|
|
@@ -334,187 +324,16 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
|
|
|
334
324
|
// Interactive and accessibility intelligence
|
|
335
325
|
detectInteractiveFindings(observation.traces, manifest, findings);
|
|
336
326
|
|
|
337
|
-
//
|
|
338
|
-
const routeFindings = detectRouteFindings(observation.traces, manifest, findings);
|
|
339
|
-
findings.push(...routeFindings);
|
|
340
|
-
|
|
341
|
-
// PHASE 13: UI feedback findings
|
|
342
|
-
const uiFeedbackFindings = detectUIFeedbackFindings(observation.traces, manifest, findings);
|
|
343
|
-
findings.push(...uiFeedbackFindings);
|
|
344
|
-
|
|
345
|
-
// PHASE 14: Dynamic route findings
|
|
346
|
-
const dynamicRouteResult = detectDynamicRouteFindings(observation.traces, manifest, findings);
|
|
347
|
-
findings.push(...dynamicRouteResult.findings);
|
|
348
|
-
// Note: skips are handled separately and should be included in skip summary
|
|
349
|
-
|
|
350
|
-
// PHASE 23: Apply guardrails + truth reconciliation (AFTER evidence builder, BEFORE writing artifacts)
|
|
351
|
-
const guardrailsSummary = {
|
|
352
|
-
totalFindingsProcessed: 0,
|
|
353
|
-
preventedConfirmedCount: 0,
|
|
354
|
-
downgradedCount: 0,
|
|
355
|
-
informationalCount: 0,
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
const truthDecisions = {}; // Map of findingIdentity -> truthDecision
|
|
359
|
-
|
|
360
|
-
const findingsWithGuardrails = findings.map(finding => {
|
|
361
|
-
guardrailsSummary.totalFindingsProcessed++;
|
|
362
|
-
|
|
363
|
-
// Capture initial confidence before guardrails
|
|
364
|
-
const initialConfidence = finding.confidence || 0;
|
|
365
|
-
const initialConfidenceLevel = finding.confidenceLevel ||
|
|
366
|
-
(initialConfidence >= 0.8 ? 'HIGH' : initialConfidence >= 0.5 ? 'MEDIUM' : initialConfidence >= 0.2 ? 'LOW' : 'UNPROVEN');
|
|
367
|
-
|
|
368
|
-
// Build context for guardrails
|
|
369
|
-
const context = {
|
|
370
|
-
evidencePackage: finding.evidencePackage,
|
|
371
|
-
signals: finding.evidencePackage?.signals || finding.evidence || {},
|
|
372
|
-
confidenceReasons: finding.confidenceReasons || [],
|
|
373
|
-
promiseType: finding.expectation?.type || finding.promise?.type || null,
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
// Apply guardrails
|
|
377
|
-
const guardrailsResult = applyGuardrails(finding, context);
|
|
378
|
-
|
|
379
|
-
// Finalize truth (reconcile confidence with guardrails outcome)
|
|
380
|
-
const { finalFinding, truthDecision } = finalizeFindingTruth(
|
|
381
|
-
guardrailsResult.finding,
|
|
382
|
-
guardrailsResult,
|
|
383
|
-
{
|
|
384
|
-
initialConfidence,
|
|
385
|
-
initialConfidenceLevel
|
|
386
|
-
}
|
|
387
|
-
);
|
|
388
|
-
|
|
389
|
-
// Store truth decision for report
|
|
390
|
-
const findingIdentity = finalFinding.findingId || finalFinding.id || `finding-${findings.indexOf(finding)}`;
|
|
391
|
-
truthDecisions[findingIdentity] = truthDecision;
|
|
392
|
-
|
|
393
|
-
// Track guardrails impact
|
|
394
|
-
const originalSeverity = finding.severity || 'SUSPECTED';
|
|
395
|
-
const finalSeverity = truthDecision.finalStatus;
|
|
396
|
-
|
|
397
|
-
if (finalSeverity !== originalSeverity && originalSeverity === 'CONFIRMED') {
|
|
398
|
-
guardrailsSummary.preventedConfirmedCount++;
|
|
399
|
-
}
|
|
400
|
-
if (finalSeverity === 'SUSPECTED' && originalSeverity === 'CONFIRMED') {
|
|
401
|
-
guardrailsSummary.downgradedCount++;
|
|
402
|
-
}
|
|
403
|
-
if (finalSeverity === 'INFORMATIONAL') {
|
|
404
|
-
guardrailsSummary.informationalCount++;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return finalFinding;
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
// Replace findings with guardrails-applied + truth-reconciled findings
|
|
411
|
-
findings.length = 0;
|
|
412
|
-
findings.push(...findingsWithGuardrails);
|
|
413
|
-
|
|
414
|
-
// PHASE 24: Apply confidence invariants and compute final confidence
|
|
415
|
-
const confidenceData = {}; // Map of findingIdentity -> confidence computation result
|
|
416
|
-
|
|
417
|
-
// Load evidence intent if available
|
|
418
|
-
let evidenceIntentLedger = null;
|
|
327
|
+
// Infer canonical run directory from tracesPath when available
|
|
419
328
|
let runDir = null;
|
|
420
329
|
try {
|
|
421
330
|
runDir = dirname(tracesPath);
|
|
422
|
-
const evidenceIntentPath = join(runDir, 'evidence.intent.json');
|
|
423
|
-
if (existsSync(evidenceIntentPath)) {
|
|
424
|
-
try {
|
|
425
|
-
const intentContent = readFileSync(evidenceIntentPath, 'utf-8');
|
|
426
|
-
evidenceIntentLedger = JSON.parse(intentContent);
|
|
427
|
-
} catch {
|
|
428
|
-
// Ignore parse errors
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
331
|
} catch {
|
|
432
332
|
// Ignore path parsing errors
|
|
433
333
|
}
|
|
434
|
-
|
|
435
|
-
const findingsWithConfidence = findings.map(finding => {
|
|
436
|
-
const findingIdentity = finding.findingId || finding.id || `finding-${findings.indexOf(finding)}`;
|
|
437
|
-
|
|
438
|
-
// Get evidence intent entry for this finding
|
|
439
|
-
const evidenceIntentEntry = evidenceIntentLedger?.entries?.find(e => e.findingIdentity === findingIdentity) || null;
|
|
440
|
-
|
|
441
|
-
// Get guardrails outcome
|
|
442
|
-
const guardrailsOutcome = finding.guardrails || truthDecisions[findingIdentity] || null;
|
|
443
|
-
|
|
444
|
-
// Compute final confidence
|
|
445
|
-
const confidenceResult = computeFinalConfidence({
|
|
446
|
-
findingType: finding.type || 'unknown',
|
|
447
|
-
rawSignals: finding.evidencePackage?.signals || finding.evidence || {},
|
|
448
|
-
evidenceIntent: evidenceIntentEntry,
|
|
449
|
-
guardrailsOutcome,
|
|
450
|
-
truthStatus: finding.severity || finding.status || 'SUSPECTED',
|
|
451
|
-
expectation: finding.expectation || null,
|
|
452
|
-
sensors: finding.evidencePackage?.signals || finding.evidence || {},
|
|
453
|
-
comparisons: {},
|
|
454
|
-
evidence: finding.evidence || {},
|
|
455
|
-
options: {
|
|
456
|
-
verificationStatus: null // Will be set by verifier
|
|
457
|
-
}
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
// Store confidence data for report
|
|
461
|
-
confidenceData[findingIdentity] = confidenceResult;
|
|
462
|
-
|
|
463
|
-
// Enforce invariants
|
|
464
|
-
const invariantResult = enforceConfidenceInvariants(finding, {
|
|
465
|
-
expectationProof: confidenceResult.expectationProof,
|
|
466
|
-
verificationStatus: confidenceResult.verificationStatus,
|
|
467
|
-
guardrailsOutcome
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
// Update finding with final confidence
|
|
471
|
-
const finalFinding = {
|
|
472
|
-
...invariantResult.finding,
|
|
473
|
-
confidence: confidenceResult.confidenceAfter,
|
|
474
|
-
confidenceLevel: confidenceResult.confidenceLevel,
|
|
475
|
-
confidenceReasons: confidenceResult.explanation
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
return finalFinding;
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
// Replace findings with confidence-enforced findings
|
|
482
|
-
findings.length = 0;
|
|
483
|
-
findings.push(...findingsWithConfidence);
|
|
484
|
-
|
|
485
|
-
// Infer canonical run directory from tracesPath when available
|
|
486
|
-
if (!runDir) {
|
|
487
|
-
try {
|
|
488
|
-
runDir = dirname(tracesPath);
|
|
489
|
-
} catch {
|
|
490
|
-
// Ignore path parsing errors
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
334
|
|
|
494
335
|
const findingsResult = writeFindings(projectDir, observation.url, findings, [], runDir);
|
|
495
336
|
|
|
496
|
-
// PHASE 23: Write guardrails report
|
|
497
|
-
let guardrailsReportPath = null;
|
|
498
|
-
if (runDir) {
|
|
499
|
-
try {
|
|
500
|
-
guardrailsReportPath = writeGuardrailsReport(runDir, findings, truthDecisions);
|
|
501
|
-
} catch (error) {
|
|
502
|
-
// Log but don't fail the run
|
|
503
|
-
console.error('Failed to write guardrails report:', error.message);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// PHASE 24: Write confidence report
|
|
508
|
-
let confidenceReportPath = null;
|
|
509
|
-
if (runDir) {
|
|
510
|
-
try {
|
|
511
|
-
confidenceReportPath = writeConfidenceReport(runDir, findings, confidenceData);
|
|
512
|
-
} catch (error) {
|
|
513
|
-
// Log but don't fail the run
|
|
514
|
-
console.error('Failed to write confidence report:', error.message);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
337
|
const skipSummary = collectSkipReasons(skips);
|
|
519
338
|
|
|
520
339
|
const detectTruth = {
|
|
@@ -526,8 +345,6 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
|
|
|
526
345
|
|
|
527
346
|
return {
|
|
528
347
|
...findingsResult,
|
|
529
|
-
detectTruth: detectTruth
|
|
530
|
-
guardrailsSummary: guardrailsSummary, // PHASE 17: Include guardrails summary
|
|
531
|
-
guardrailsReportPath: guardrailsReportPath // PHASE 23: Include guardrails report path
|
|
348
|
+
detectTruth: detectTruth
|
|
532
349
|
};
|
|
533
350
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4: Final Output Invariants (Trust Lock)
|
|
3
|
+
*
|
|
4
|
+
* Strict gate applied only to user-facing outputs (REPORT.json, SUMMARY.md, console).
|
|
5
|
+
* Findings violating any invariant are dropped silently. Purity and determinism are required.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const KNOWN_IMPACTS = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO', 'UNKNOWN'];
|
|
9
|
+
const VALID_STATUSES = ['CONFIRMED', 'SUSPECTED', 'INFORMATIONAL'];
|
|
10
|
+
|
|
11
|
+
function isValidEvidence(evidence) {
|
|
12
|
+
if (!evidence || typeof evidence !== 'object') return false;
|
|
13
|
+
const keys = Object.keys(evidence);
|
|
14
|
+
if (keys.length === 0) return false;
|
|
15
|
+
|
|
16
|
+
const signals = Object.values(evidence).filter(v => {
|
|
17
|
+
if (v === true || v === false) return true;
|
|
18
|
+
if (typeof v === 'number' && v !== 0) return true;
|
|
19
|
+
if (typeof v === 'string' && v.length > 0) return true;
|
|
20
|
+
if (Array.isArray(v)) return v.length > 0; // arrays must contain at least one element
|
|
21
|
+
if (v && typeof v === 'object') return Object.keys(v).length > 0;
|
|
22
|
+
return false;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return signals.length > 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isValidConfidence(confidence) {
|
|
29
|
+
if (confidence === 0) return true;
|
|
30
|
+
if (!confidence) return false;
|
|
31
|
+
|
|
32
|
+
if (typeof confidence === 'number') {
|
|
33
|
+
return confidence >= 0 && confidence <= 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof confidence === 'object') {
|
|
37
|
+
const hasLevel = (typeof confidence.level === 'string' && confidence.level.length > 0) || typeof confidence.level === 'number';
|
|
38
|
+
const hasScore = typeof confidence.score === 'number';
|
|
39
|
+
return hasLevel || hasScore;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isValidPromise(promise) {
|
|
46
|
+
if (!promise || typeof promise !== 'object') return false;
|
|
47
|
+
|
|
48
|
+
const { kind, value, type, expected, actual, expected_signal } = promise;
|
|
49
|
+
|
|
50
|
+
const kindValueValid = typeof kind === 'string' && kind.length > 0 && typeof value === 'string' && value.length > 0;
|
|
51
|
+
const typeValid = typeof type === 'string' && type.length > 0;
|
|
52
|
+
const expectationValid = (typeof expected === 'string' && expected.length > 0) ||
|
|
53
|
+
(typeof actual === 'string' && actual.length > 0) ||
|
|
54
|
+
(typeof expected_signal === 'string' && expected_signal.length > 0);
|
|
55
|
+
|
|
56
|
+
return kindValueValid || (typeValid && expectationValid);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isInternalErrorFlag(finding) {
|
|
60
|
+
const internalMarkers = [
|
|
61
|
+
'INTERNAL_ERROR',
|
|
62
|
+
'internal-error',
|
|
63
|
+
'internalError',
|
|
64
|
+
'TIMEOUT_ERROR',
|
|
65
|
+
'CRASH',
|
|
66
|
+
'FATAL',
|
|
67
|
+
'BROWSER_CRASH'
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
if (internalMarkers.some(marker =>
|
|
71
|
+
finding.reason?.includes?.(marker) ||
|
|
72
|
+
finding.errorMessage?.includes?.(marker) ||
|
|
73
|
+
finding.errorStack?.includes?.(marker)
|
|
74
|
+
)) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (finding.reason?.toLowerCase?.().includes('internal error')) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isValidCause(cause, evidence) {
|
|
86
|
+
if (!cause || typeof cause !== 'object') return false;
|
|
87
|
+
if (!cause.id || !cause.statement) return false;
|
|
88
|
+
|
|
89
|
+
if (Array.isArray(cause.evidence_refs) && cause.evidence_refs.length > 0) {
|
|
90
|
+
if (!evidence || typeof evidence !== 'object') return false;
|
|
91
|
+
return cause.evidence_refs.every(ref => typeof ref === 'string' && ref in evidence);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function filterCausesWithEvidence(evidence, causes) {
|
|
98
|
+
if (!Array.isArray(causes)) return [];
|
|
99
|
+
return causes.filter(cause => isValidCause(cause, evidence));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function enforceFinalInvariant(finding) {
|
|
103
|
+
if (!finding) return null;
|
|
104
|
+
|
|
105
|
+
if (!isValidEvidence(finding.evidence)) return null;
|
|
106
|
+
if (!isValidPromise(finding.promise)) return null;
|
|
107
|
+
if (!isValidConfidence(finding.confidence)) return null;
|
|
108
|
+
if (finding.impact && !KNOWN_IMPACTS.includes(finding.impact)) return null;
|
|
109
|
+
if (isInternalErrorFlag(finding)) return null;
|
|
110
|
+
if (!finding.id || typeof finding.id !== 'string' || finding.id.length === 0) return null;
|
|
111
|
+
if (finding.status && !VALID_STATUSES.includes(finding.status)) return null;
|
|
112
|
+
|
|
113
|
+
const causes = filterCausesWithEvidence(finding.evidence, finding.causes);
|
|
114
|
+
|
|
115
|
+
return { ...finding, causes };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function deduplicateFindings(findings) {
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
const deduped = [];
|
|
121
|
+
|
|
122
|
+
for (const finding of findings) {
|
|
123
|
+
const promiseKey = finding.promise ? JSON.stringify(finding.promise) : 'nopromise';
|
|
124
|
+
const key = `${finding.id || 'unknown'}|${finding.location || 'unknown'}|${promiseKey}`;
|
|
125
|
+
if (seen.has(key)) continue;
|
|
126
|
+
seen.add(key);
|
|
127
|
+
deduped.push(finding);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return deduped;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function enforceFinalInvariants(findings) {
|
|
134
|
+
if (!Array.isArray(findings)) return [];
|
|
135
|
+
|
|
136
|
+
const validFindings = findings
|
|
137
|
+
.map(enforceFinalInvariant)
|
|
138
|
+
.filter(Boolean);
|
|
139
|
+
|
|
140
|
+
const deduped = deduplicateFindings(validFindings);
|
|
141
|
+
|
|
142
|
+
return deduped.sort((a, b) => {
|
|
143
|
+
const aId = (a.id || '').toString();
|
|
144
|
+
const bId = (b.id || '').toString();
|
|
145
|
+
return aId.localeCompare(bId);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -86,7 +86,7 @@ export class JourneyStallDetector {
|
|
|
86
86
|
// 2. Next trace shows successful navigation (end of journey segment)
|
|
87
87
|
// 3. We're at the end of traces
|
|
88
88
|
const isLastTrace = i === traces.length - 1;
|
|
89
|
-
const
|
|
89
|
+
const _nextTrace = !isLastTrace ? traces[i + 1] : null;
|
|
90
90
|
const navigationOccurred = trace.sensors?.navigation?.urlChanged === true;
|
|
91
91
|
|
|
92
92
|
if (
|
|
@@ -138,7 +138,7 @@ export class JourneyStallDetector {
|
|
|
138
138
|
* Detect if there's a stall between two consecutive steps
|
|
139
139
|
* @private
|
|
140
140
|
*/
|
|
141
|
-
_detectStallBetweenSteps(current, next, stepIndex,
|
|
141
|
+
_detectStallBetweenSteps(current, next, stepIndex, _sequence) {
|
|
142
142
|
const reasons = [];
|
|
143
143
|
const evidence = [];
|
|
144
144
|
|
|
@@ -427,7 +427,7 @@ export class JourneyStallDetector {
|
|
|
427
427
|
* Calculate severity level for stall
|
|
428
428
|
* @private
|
|
429
429
|
*/
|
|
430
|
-
_calculateSeverity(reasons,
|
|
430
|
+
_calculateSeverity(reasons, _current, _next) {
|
|
431
431
|
let score = 0;
|
|
432
432
|
|
|
433
433
|
// Multiple reasons = higher severity
|
|
@@ -459,7 +459,7 @@ export class JourneyStallDetector {
|
|
|
459
459
|
* @private
|
|
460
460
|
*/
|
|
461
461
|
_generateStallFinding(sequence, stallPoints) {
|
|
462
|
-
const
|
|
462
|
+
const _firstTrace = sequence[0].trace;
|
|
463
463
|
const lastTrace = sequence[sequence.length - 1].trace;
|
|
464
464
|
|
|
465
465
|
const id = `journey-stall-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation Silent Failure Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects navigation actions where:
|
|
5
|
+
* - URL changes to expected route
|
|
6
|
+
* - AND no meaningful DOM change occurs (loading state never resolves)
|
|
7
|
+
* - AND expected anchor (h1, title, route container) does not appear
|
|
8
|
+
*
|
|
9
|
+
* CONFIDENCE: HIGH (URL + DOM evidence)
|
|
10
|
+
* PARTIAL SUPPORT: Relies on heuristic anchor detection
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { hasMeaningfulUrlChange, hasDomChange } from './comparison.js';
|
|
14
|
+
import { enrichFindingWithExplanations } from './finding-detector.js';
|
|
15
|
+
|
|
16
|
+
export function detectNavigationSilentFailures(traces, manifest, findings) {
|
|
17
|
+
// Parameters:
|
|
18
|
+
// traces - array of interaction traces from observation
|
|
19
|
+
// manifest - project manifest (contains expectations)
|
|
20
|
+
// findings - array to append new findings to (mutated in-place)
|
|
21
|
+
|
|
22
|
+
for (const trace of traces) {
|
|
23
|
+
const interaction = trace.interaction || {};
|
|
24
|
+
|
|
25
|
+
// Only analyze navigation interactions
|
|
26
|
+
if (interaction.type !== 'navigation' && interaction.type !== 'link') {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const beforeUrl = trace.before?.url || trace.beforeUrl || '';
|
|
31
|
+
const afterUrl = trace.after?.url || trace.afterUrl || '';
|
|
32
|
+
const beforePage = trace.page?.beforeTitle || trace.before?.title || '';
|
|
33
|
+
const afterPage = trace.page?.afterTitle || trace.after?.title || '';
|
|
34
|
+
|
|
35
|
+
const urlChanged = hasMeaningfulUrlChange(beforeUrl, afterUrl);
|
|
36
|
+
const domChanged = hasDomChange(trace);
|
|
37
|
+
|
|
38
|
+
// Detection logic:
|
|
39
|
+
// URL changed (navigation happened) but:
|
|
40
|
+
// 1. DOM signature didn't change (content not rendered)
|
|
41
|
+
// 2. Page title is the same or empty (no new page loaded)
|
|
42
|
+
if (urlChanged && !domChanged && beforePage === afterPage) {
|
|
43
|
+
const evidence = {
|
|
44
|
+
before: trace.before?.screenshot || trace.beforeScreenshot || '',
|
|
45
|
+
after: trace.after?.screenshot || trace.afterScreenshot || '',
|
|
46
|
+
beforeUrl,
|
|
47
|
+
afterUrl,
|
|
48
|
+
beforePageTitle: beforePage,
|
|
49
|
+
afterPageTitle: afterPage,
|
|
50
|
+
domChanged,
|
|
51
|
+
urlChanged,
|
|
52
|
+
reason: 'URL changed but content did not render (likely loading state)'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const finding = {
|
|
56
|
+
type: 'navigation_silent_failure',
|
|
57
|
+
description: `Navigation to ${afterUrl} succeeded but content did not render`,
|
|
58
|
+
summary: `URL updated to route but component content remains unrendered (likely stuck in loading state)`,
|
|
59
|
+
explanation: `The navigation route changed from ${beforeUrl} to ${afterUrl}, but the DOM signature remained identical and page title unchanged. This suggests the component is stuck in a loading state.`,
|
|
60
|
+
evidence,
|
|
61
|
+
confidence: {
|
|
62
|
+
level: 0.85, // HIGH - URL + DOM evidence
|
|
63
|
+
reasons: [
|
|
64
|
+
'URL changed to expected route',
|
|
65
|
+
'DOM signature unchanged (content not rendered)',
|
|
66
|
+
'Page title unchanged'
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
promise: {
|
|
70
|
+
type: 'navigation',
|
|
71
|
+
expected: `Navigate to ${afterUrl} and render content`,
|
|
72
|
+
actual: 'Navigation succeeded but content did not render'
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Enrich with explanations
|
|
77
|
+
enrichFindingWithExplanations(finding, trace);
|
|
78
|
+
findings.push(finding);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|