@veraxhq/verax 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -18
- package/bin/verax.js +7 -0
- package/package.json +3 -3
- package/src/cli/commands/baseline.js +104 -0
- package/src/cli/commands/default.js +79 -25
- 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 +246 -35
- package/src/cli/commands/security-check.js +211 -0
- package/src/cli/commands/truth.js +114 -0
- package/src/cli/entry.js +304 -67
- 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/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/expectation-extractor.js +369 -73
- package/src/cli/util/findings-writer.js +126 -16
- package/src/cli/util/learn-writer.js +3 -1
- package/src/cli/util/observe-writer.js +3 -1
- package/src/cli/util/paths.js +3 -12
- package/src/cli/util/project-discovery.js +3 -0
- package/src/cli/util/project-writer.js +3 -1
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +1 -0
- 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/verax/cli/finding-explainer.js +56 -3
- 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/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 +30 -3
- 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/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/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/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/truth/truth.certificate.js +250 -0
- package/src/verax/core/ui-feedback-intelligence.js +515 -0
- package/src/verax/detect/confidence-engine.js +628 -40
- package/src/verax/detect/confidence-helper.js +33 -0
- package/src/verax/detect/detection-engine.js +18 -1
- package/src/verax/detect/dynamic-route-findings.js +335 -0
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +3 -1
- package/src/verax/detect/findings-writer.js +141 -5
- package/src/verax/detect/index.js +229 -5
- package/src/verax/detect/journey-stall-detector.js +558 -0
- package/src/verax/detect/route-findings.js +218 -0
- package/src/verax/detect/ui-feedback-findings.js +207 -0
- package/src/verax/detect/verdict-engine.js +57 -3
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/index.js +413 -45
- package/src/verax/learn/action-contract-extractor.js +682 -64
- package/src/verax/learn/route-validator.js +4 -1
- package/src/verax/observe/index.js +88 -843
- package/src/verax/observe/interaction-runner.js +25 -8
- 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/ui-feedback-detector.js +742 -0
- package/src/verax/observe/ui-signal-sensor.js +148 -2
- package/src/verax/scan-summary-writer.js +42 -8
- package/src/verax/shared/artifact-manager.js +8 -5
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/view-switch-rules.js +208 -0
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
import { resolve, dirname } from 'path';
|
|
2
|
-
import { mkdirSync
|
|
2
|
+
import { mkdirSync } from 'fs';
|
|
3
3
|
import { createBrowser, navigateToUrl, closeBrowser } from './browser.js';
|
|
4
|
-
import { discoverAllInteractions } from './interaction-discovery.js';
|
|
5
4
|
import { captureScreenshot } from './evidence-capture.js';
|
|
6
|
-
import {
|
|
7
|
-
import { writeTraces } from './traces-writer.js';
|
|
8
|
-
import { getBaseOrigin, isExternalUrl } from './domain-boundary.js';
|
|
9
|
-
|
|
5
|
+
import { getBaseOrigin } from './domain-boundary.js';
|
|
10
6
|
import { DEFAULT_SCAN_BUDGET } from '../shared/scan-budget.js';
|
|
11
|
-
import { executeProvenExpectations } from './expectation-executor.js';
|
|
12
|
-
import { isProvenExpectation } from '../shared/expectation-prover.js';
|
|
13
7
|
import { PageFrontier } from './page-frontier.js';
|
|
14
|
-
import { deriveObservedExpectation, shouldAttemptRepeatObservedExpectation, evaluateObservedExpectation } from './observed-expectation.js';
|
|
15
|
-
import { computeRouteBudget } from '../core/budget-engine.js';
|
|
16
|
-
import { loadPreviousSnapshot, saveSnapshot, buildSnapshot, compareSnapshots, shouldSkipInteractionIncremental } from '../core/incremental-store.js';
|
|
17
8
|
import SilenceTracker from '../core/silence-model.js';
|
|
18
|
-
import { DecisionRecorder, recordBudgetProfile, recordTimeoutConfig,
|
|
9
|
+
import { DecisionRecorder, recordBudgetProfile, recordTimeoutConfig, recordEnvironment } from '../core/determinism-model.js';
|
|
10
|
+
import { setupManifestAndExpectations, processTraversalResults, writeDeterminismArtifacts } from './observe-helpers.js';
|
|
19
11
|
|
|
20
12
|
/**
|
|
21
13
|
* OBSERVE PHASE - Execute interactions and capture runtime behavior
|
|
@@ -58,138 +50,51 @@ export async function observe(url, manifestPath = null, scanBudgetOverride = nul
|
|
|
58
50
|
recordEnvironment(decisionRecorder, { browserType: 'chromium', viewport: { width: 1280, height: 720 } });
|
|
59
51
|
|
|
60
52
|
// Phase 5: Detect projectDir if not provided (for backwards compatibility with tests)
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
// Use a local variable to avoid reassigning function parameter
|
|
54
|
+
let resolvedProjectDir = projectDir;
|
|
55
|
+
if (!resolvedProjectDir) {
|
|
56
|
+
resolvedProjectDir = process.cwd();
|
|
63
57
|
}
|
|
64
|
-
|
|
58
|
+
|
|
65
59
|
// Phase 4: Extract safety flags
|
|
66
60
|
const { allowWrites = false, allowRiskyActions = false, allowCrossOrigin = false } = safetyFlags;
|
|
67
61
|
let blockedNetworkWrites = [];
|
|
68
62
|
let blockedCrossOrigin = [];
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
await
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
blockedCrossOrigin.push({
|
|
83
|
-
url: requestUrl,
|
|
84
|
-
origin: reqOrigin,
|
|
85
|
-
method,
|
|
86
|
-
resourceType,
|
|
87
|
-
timestamp: Date.now()
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
silenceTracker.record({
|
|
91
|
-
scope: 'safety',
|
|
92
|
-
reason: 'cross_origin_blocked',
|
|
93
|
-
description: `Cross-origin request blocked: ${method} ${requestUrl}`,
|
|
94
|
-
context: { url: requestUrl, origin: reqOrigin, method, baseOrigin },
|
|
95
|
-
impact: 'request_blocked'
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
return route.abort('blockedbyclient');
|
|
99
|
-
}
|
|
100
|
-
} catch (e) {
|
|
101
|
-
// Invalid URL, allow and let browser handle
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Check write method blocking
|
|
106
|
-
if (!allowWrites && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
107
|
-
// Check if it's a GraphQL mutation (best-effort)
|
|
108
|
-
const isGraphQLMutation = requestUrl.includes('/graphql') && method === 'POST';
|
|
109
|
-
|
|
110
|
-
blockedNetworkWrites.push({
|
|
111
|
-
url: requestUrl,
|
|
112
|
-
method,
|
|
113
|
-
resourceType,
|
|
114
|
-
isGraphQLMutation,
|
|
115
|
-
timestamp: Date.now()
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
silenceTracker.record({
|
|
119
|
-
scope: 'safety',
|
|
120
|
-
reason: 'blocked_network_write',
|
|
121
|
-
description: `Network write blocked: ${method} ${requestUrl}${isGraphQLMutation ? ' (GraphQL mutation)' : ''}`,
|
|
122
|
-
context: { url: requestUrl, method, resourceType, isGraphQLMutation },
|
|
123
|
-
impact: 'write_blocked'
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
return route.abort('blockedbyclient');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Allow request
|
|
130
|
-
route.continue();
|
|
131
|
-
});
|
|
63
|
+
|
|
64
|
+
// PHASE 21.3: Setup network interception using safety observer
|
|
65
|
+
const { setupNetworkInterception } = await import('./observers/safety-observer.js');
|
|
66
|
+
// Create temporary context for network interception setup
|
|
67
|
+
const tempContext = {
|
|
68
|
+
page,
|
|
69
|
+
baseOrigin,
|
|
70
|
+
safetyFlags: { allowWrites, allowCrossOrigin },
|
|
71
|
+
silenceTracker,
|
|
72
|
+
blockedNetworkWrites,
|
|
73
|
+
blockedCrossOrigin
|
|
74
|
+
};
|
|
75
|
+
await setupNetworkInterception(tempContext);
|
|
132
76
|
|
|
133
77
|
try {
|
|
134
78
|
await navigateToUrl(page, url, scanBudget);
|
|
135
79
|
|
|
136
|
-
|
|
80
|
+
// If manifestPath is provided, derive projectDir from it (override default)
|
|
81
|
+
if (manifestPath) {
|
|
82
|
+
resolvedProjectDir = dirname(dirname(dirname(manifestPath)));
|
|
83
|
+
}
|
|
137
84
|
if (!runId) {
|
|
138
85
|
throw new Error('runId is required');
|
|
139
86
|
}
|
|
140
87
|
const { getScreenshotDir } = await import('../core/run-id.js');
|
|
141
|
-
const screenshotsDir = getScreenshotDir(
|
|
88
|
+
const screenshotsDir = getScreenshotDir(resolvedProjectDir, runId);
|
|
142
89
|
mkdirSync(screenshotsDir, { recursive: true });
|
|
143
90
|
|
|
144
91
|
const timestamp = Date.now();
|
|
145
92
|
const initialScreenshot = resolve(screenshotsDir, `initial-${timestamp}.png`);
|
|
146
93
|
await captureScreenshot(page, initialScreenshot);
|
|
147
94
|
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
let expectationCoverageGaps = [];
|
|
152
|
-
let incrementalMode = false;
|
|
153
|
-
let snapshotDiff = null;
|
|
154
|
-
let oldSnapshot = null;
|
|
155
|
-
|
|
156
|
-
if (manifestPath && existsSync(manifestPath)) {
|
|
157
|
-
try {
|
|
158
|
-
const manifestContent = readFileSync(manifestPath, 'utf-8');
|
|
159
|
-
manifest = JSON.parse(manifestContent);
|
|
160
|
-
|
|
161
|
-
// SCALE INTELLIGENCE: Load previous snapshot for incremental mode
|
|
162
|
-
oldSnapshot = loadPreviousSnapshot(projectDir);
|
|
163
|
-
if (oldSnapshot) {
|
|
164
|
-
const currentSnapshot = buildSnapshot(manifest, []);
|
|
165
|
-
snapshotDiff = compareSnapshots(oldSnapshot, currentSnapshot);
|
|
166
|
-
incrementalMode = !snapshotDiff.hasChanges; // Use incremental if nothing changed
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const provenCount = (manifest.staticExpectations || []).filter(exp => isProvenExpectation(exp)).length;
|
|
170
|
-
if (provenCount > 0) {
|
|
171
|
-
expectationResults = await executeProvenExpectations(
|
|
172
|
-
page,
|
|
173
|
-
manifest,
|
|
174
|
-
url,
|
|
175
|
-
screenshotsDir,
|
|
176
|
-
scanBudget,
|
|
177
|
-
startTime,
|
|
178
|
-
projectDir
|
|
179
|
-
);
|
|
180
|
-
expectationCoverageGaps = expectationResults.coverageGaps || [];
|
|
181
|
-
}
|
|
182
|
-
} catch (err) {
|
|
183
|
-
// Record manifest load/expectation execution failure as silence
|
|
184
|
-
silenceTracker.record({
|
|
185
|
-
scope: 'discovery',
|
|
186
|
-
reason: 'discovery_error',
|
|
187
|
-
description: 'Manifest load or expectation execution failed',
|
|
188
|
-
context: { error: err?.message },
|
|
189
|
-
impact: 'incomplete_check'
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
}
|
|
95
|
+
// PHASE 21.3: Setup manifest and expectations using helper
|
|
96
|
+
const { manifest, expectationResults, expectationCoverageGaps, incrementalMode, snapshotDiff, oldSnapshot } =
|
|
97
|
+
await setupManifestAndExpectations(manifestPath, resolvedProjectDir, page, url, screenshotsDir, scanBudget, startTime, silenceTracker);
|
|
193
98
|
|
|
194
99
|
// Reset to start URL before traversal
|
|
195
100
|
await navigateToUrl(page, url, scanBudget);
|
|
@@ -203,680 +108,72 @@ export async function observe(url, manifestPath = null, scanBudgetOverride = nul
|
|
|
203
108
|
let totalInteractionsExecuted = 0;
|
|
204
109
|
let remainingInteractionsGaps = [];
|
|
205
110
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
reason: 'navigation_timeout',
|
|
241
|
-
description: 'Navigation to page failed',
|
|
242
|
-
context: { targetUrl: nextPageUrl },
|
|
243
|
-
impact: 'blocks_nav'
|
|
244
|
-
});
|
|
245
|
-
const normalizedFailed = frontier.normalizeUrl(nextPageUrl);
|
|
246
|
-
if (!frontier.visited.has(normalizedFailed)) {
|
|
247
|
-
frontier.visited.add(normalizedFailed);
|
|
248
|
-
frontier.markVisited();
|
|
249
|
-
}
|
|
250
|
-
nextPageUrl = frontier.getNextUrl();
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Mark as visited and increment counter
|
|
256
|
-
// getNextUrl() marks as visited in the set but doesn't call markVisited() to increment counter
|
|
257
|
-
// So we need to call markVisited() here when we process a page
|
|
258
|
-
if (!alreadyOnPage) {
|
|
259
|
-
// We navigated via getNextUrl() - it already marked in visited set, now increment counter
|
|
260
|
-
// (getNextUrl() marks in visited set but doesn't increment pagesVisited)
|
|
261
|
-
frontier.markVisited();
|
|
262
|
-
} else {
|
|
263
|
-
// We navigated via link click (alreadyOnPage=true) - mark as visited and increment
|
|
264
|
-
if (!frontier.visited.has(normalizedNext)) {
|
|
265
|
-
frontier.visited.add(normalizedNext);
|
|
266
|
-
frontier.markVisited();
|
|
267
|
-
} else {
|
|
268
|
-
// Already marked as visited, but still increment counter since we're processing it
|
|
269
|
-
frontier.markVisited();
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Discover ALL links on this page and add to frontier BEFORE executing interactions
|
|
274
|
-
try {
|
|
275
|
-
const currentLinks = await page.locator('a[href]').all();
|
|
276
|
-
for (const link of currentLinks) {
|
|
277
|
-
try {
|
|
278
|
-
const href = await link.getAttribute('href');
|
|
279
|
-
if (href && !href.startsWith('#') && !href.startsWith('javascript:')) {
|
|
280
|
-
const resolvedUrl = href.startsWith('http') ? href : new URL(href, page.url()).href;
|
|
281
|
-
if (!isExternalUrl(resolvedUrl, baseOrigin)) {
|
|
282
|
-
frontier.addUrl(resolvedUrl);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
} catch (error) {
|
|
286
|
-
// Record invalid URL discovery as silence
|
|
287
|
-
silenceTracker.record({
|
|
288
|
-
scope: 'discovery',
|
|
289
|
-
reason: 'discovery_error',
|
|
290
|
-
description: 'Invalid or unreadable link during discovery',
|
|
291
|
-
context: { pageUrl: page.url() },
|
|
292
|
-
impact: 'incomplete_check'
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
} catch (error) {
|
|
297
|
-
// Record link discovery failure as silence
|
|
298
|
-
silenceTracker.record({
|
|
299
|
-
scope: 'discovery',
|
|
300
|
-
reason: 'discovery_error',
|
|
301
|
-
description: 'Link discovery failed on page',
|
|
302
|
-
context: { pageUrl: page.url() },
|
|
303
|
-
impact: 'incomplete_check'
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// SCALE INTELLIGENCE: Compute adaptive budget for this route
|
|
308
|
-
// Reuse currentUrl from above (already captured at line 95)
|
|
309
|
-
const routeBudget = manifest ? computeRouteBudget(manifest, currentUrl, scanBudget) : scanBudget;
|
|
310
|
-
|
|
311
|
-
// Discover ALL interactions on this page
|
|
312
|
-
// Note: discoverAllInteractions already returns sorted interactions deterministically
|
|
313
|
-
const { interactions } = await discoverAllInteractions(page, baseOrigin, routeBudget);
|
|
314
|
-
totalInteractionsDiscovered += interactions.length;
|
|
315
|
-
|
|
316
|
-
// SCALE INTELLIGENCE: Apply adaptive budget cap (interactions are already sorted deterministically)
|
|
317
|
-
// Stable sorting ensures determinism: same interactions → same order
|
|
318
|
-
const sortedInteractions = interactions.slice(0, routeBudget.maxInteractionsPerPage);
|
|
319
|
-
|
|
320
|
-
// Track if we navigated during interaction execution
|
|
321
|
-
let navigatedToNewPage = false;
|
|
322
|
-
let navigatedPageUrl = null;
|
|
323
|
-
let remainingInteractionsStartIndex = 0;
|
|
324
|
-
|
|
325
|
-
// Execute discovered interactions on this page (sorted for determinism)
|
|
326
|
-
for (let i = 0; i < sortedInteractions.length; i++) {
|
|
327
|
-
if (Date.now() - startTime > scanBudget.maxScanDurationMs) {
|
|
328
|
-
// PHASE 6: Record truncation decision
|
|
329
|
-
recordTruncation(decisionRecorder, 'time', {
|
|
330
|
-
limit: scanBudget.maxScanDurationMs,
|
|
331
|
-
elapsed: Date.now() - startTime
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
// Mark remaining interactions as COVERAGE_GAP
|
|
335
|
-
silenceTracker.record({
|
|
336
|
-
scope: 'interaction',
|
|
337
|
-
reason: 'scan_time_exceeded',
|
|
338
|
-
description: `Scan time limit (${scanBudget.maxScanDurationMs}ms) exceeded`,
|
|
339
|
-
context: {
|
|
340
|
-
elapsed: Date.now() - startTime,
|
|
341
|
-
maxDuration: scanBudget.maxScanDurationMs,
|
|
342
|
-
remainingInteractions: sortedInteractions.length - i
|
|
343
|
-
},
|
|
344
|
-
impact: 'blocks_nav',
|
|
345
|
-
count: sortedInteractions.length - i
|
|
346
|
-
});
|
|
347
|
-
remainingInteractionsStartIndex = i;
|
|
348
|
-
break;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (totalInteractionsExecuted >= routeBudget.maxInteractionsPerPage) {
|
|
352
|
-
// PHASE 6: Record truncation decision
|
|
353
|
-
recordTruncation(decisionRecorder, 'interactions', {
|
|
354
|
-
limit: routeBudget.maxInteractionsPerPage,
|
|
355
|
-
reached: totalInteractionsExecuted,
|
|
356
|
-
scope: 'per_page'
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
// Route-specific budget exceeded
|
|
360
|
-
silenceTracker.record({
|
|
361
|
-
scope: 'interaction',
|
|
362
|
-
reason: 'route_interaction_limit_exceeded',
|
|
363
|
-
description: `Reached max ${routeBudget.maxInteractionsPerPage} interactions per page`,
|
|
364
|
-
context: {
|
|
365
|
-
currentPage: page.url(),
|
|
366
|
-
executed: totalInteractionsExecuted,
|
|
367
|
-
maxPerPage: routeBudget.maxInteractionsPerPage,
|
|
368
|
-
remainingInteractions: sortedInteractions.length - i
|
|
369
|
-
},
|
|
370
|
-
impact: 'affects_expectations',
|
|
371
|
-
count: sortedInteractions.length - i
|
|
372
|
-
});
|
|
373
|
-
remainingInteractionsStartIndex = i;
|
|
374
|
-
break;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (totalInteractionsExecuted >= scanBudget.maxTotalInteractions) {
|
|
378
|
-
// PHASE 6: Record truncation decision
|
|
379
|
-
recordTruncation(decisionRecorder, 'interactions', {
|
|
380
|
-
limit: scanBudget.maxTotalInteractions,
|
|
381
|
-
reached: totalInteractionsExecuted,
|
|
382
|
-
scope: 'total'
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
// Mark remaining interactions as COVERAGE_GAP with reason 'budget_exceeded'
|
|
386
|
-
silenceTracker.record({
|
|
387
|
-
scope: 'interaction',
|
|
388
|
-
reason: 'interaction_limit_exceeded',
|
|
389
|
-
description: `Reached max ${scanBudget.maxTotalInteractions} total interactions`,
|
|
390
|
-
context: {
|
|
391
|
-
executed: totalInteractionsExecuted,
|
|
392
|
-
maxTotal: scanBudget.maxTotalInteractions,
|
|
393
|
-
remainingInteractions: sortedInteractions.length - i
|
|
394
|
-
},
|
|
395
|
-
impact: 'blocks_nav',
|
|
396
|
-
count: sortedInteractions.length - i
|
|
397
|
-
});
|
|
398
|
-
remainingInteractionsStartIndex = i;
|
|
399
|
-
break;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const interaction = sortedInteractions[i];
|
|
403
|
-
|
|
404
|
-
// SCALE INTELLIGENCE: Check if interaction should be skipped in incremental mode
|
|
405
|
-
if (incrementalMode && manifest && oldSnapshot && snapshotDiff) {
|
|
406
|
-
const shouldSkip = shouldSkipInteractionIncremental(interaction, currentUrl, oldSnapshot, snapshotDiff);
|
|
407
|
-
if (shouldSkip) {
|
|
408
|
-
// Create a trace for skipped interaction (marked as incremental - will not produce findings)
|
|
409
|
-
const skippedTrace = {
|
|
410
|
-
interaction: {
|
|
411
|
-
type: interaction.type,
|
|
412
|
-
selector: interaction.selector,
|
|
413
|
-
label: interaction.label
|
|
414
|
-
},
|
|
415
|
-
before: { url: currentUrl },
|
|
416
|
-
after: { url: currentUrl },
|
|
417
|
-
incremental: true, // Mark as skipped in incremental mode - detect phase will skip this
|
|
418
|
-
resultType: 'INCREMENTAL_SKIP'
|
|
419
|
-
};
|
|
420
|
-
traces.push(skippedTrace);
|
|
421
|
-
|
|
422
|
-
// Track incremental skip as silence
|
|
423
|
-
silenceTracker.record({
|
|
424
|
-
scope: 'interaction',
|
|
425
|
-
reason: 'incremental_unchanged',
|
|
426
|
-
description: `Skipped re-observation (unchanged in incremental mode): ${interaction.label}`,
|
|
427
|
-
context: {
|
|
428
|
-
currentPage: currentUrl,
|
|
429
|
-
selector: interaction.selector,
|
|
430
|
-
interactionLabel: interaction.label,
|
|
431
|
-
type: interaction.type
|
|
432
|
-
},
|
|
433
|
-
impact: 'affects_expectations'
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
skippedInteractions.push({
|
|
437
|
-
interaction: {
|
|
438
|
-
type: interaction.type,
|
|
439
|
-
selector: interaction.selector,
|
|
440
|
-
label: interaction.label,
|
|
441
|
-
text: interaction.text
|
|
442
|
-
},
|
|
443
|
-
outcome: 'SKIPPED',
|
|
444
|
-
reason: 'incremental_unchanged',
|
|
445
|
-
url: currentUrl,
|
|
446
|
-
evidence: {
|
|
447
|
-
selector: interaction.selector,
|
|
448
|
-
label: interaction.label,
|
|
449
|
-
incremental: true
|
|
450
|
-
}
|
|
451
|
-
});
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// Skip dangerous interactions (logout, delete, etc.) with explicit reason
|
|
457
|
-
const skipCheck = frontier.shouldSkipInteraction(interaction);
|
|
458
|
-
if (skipCheck.skip) {
|
|
459
|
-
// Track safety skip as silence
|
|
460
|
-
silenceTracker.record({
|
|
461
|
-
scope: 'interaction',
|
|
462
|
-
reason: skipCheck.reason === 'destructive' ? 'destructive_text' : 'unsafe_pattern',
|
|
463
|
-
description: `Skipped potentially dangerous interaction: ${interaction.label}`,
|
|
464
|
-
context: {
|
|
465
|
-
currentPage: page.url(),
|
|
466
|
-
selector: interaction.selector,
|
|
467
|
-
interactionLabel: interaction.label,
|
|
468
|
-
text: interaction.text,
|
|
469
|
-
skipReason: skipCheck.reason,
|
|
470
|
-
skipMessage: skipCheck.message
|
|
471
|
-
},
|
|
472
|
-
impact: 'unknown_behavior'
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
skippedInteractions.push({
|
|
476
|
-
interaction: {
|
|
477
|
-
type: interaction.type,
|
|
478
|
-
selector: interaction.selector,
|
|
479
|
-
label: interaction.label,
|
|
480
|
-
text: interaction.text
|
|
481
|
-
},
|
|
482
|
-
outcome: 'SKIPPED',
|
|
483
|
-
reason: skipCheck.reason || 'safety_policy',
|
|
484
|
-
url: page.url(),
|
|
485
|
-
evidence: {
|
|
486
|
-
selector: interaction.selector,
|
|
487
|
-
label: interaction.label,
|
|
488
|
-
text: interaction.text,
|
|
489
|
-
sourcePage: page.url()
|
|
490
|
-
}
|
|
491
|
-
});
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Phase 4: Check action classification and safety mode
|
|
496
|
-
const { shouldBlockAction } = await import('../core/action-classifier.js');
|
|
497
|
-
const blockCheck = shouldBlockAction(interaction, { allowWrites, allowRiskyActions });
|
|
498
|
-
|
|
499
|
-
if (blockCheck.shouldBlock) {
|
|
500
|
-
// Track blocked action as silence
|
|
501
|
-
silenceTracker.record({
|
|
502
|
-
scope: 'safety',
|
|
503
|
-
reason: 'blocked_action',
|
|
504
|
-
description: `Action blocked by safety mode: ${interaction.label} (${blockCheck.classification})`,
|
|
505
|
-
context: {
|
|
506
|
-
currentPage: page.url(),
|
|
507
|
-
selector: interaction.selector,
|
|
508
|
-
interactionLabel: interaction.label,
|
|
509
|
-
text: interaction.text,
|
|
510
|
-
classification: blockCheck.classification,
|
|
511
|
-
blockReason: blockCheck.reason
|
|
512
|
-
},
|
|
513
|
-
impact: 'action_blocked'
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
skippedInteractions.push({
|
|
517
|
-
interaction: {
|
|
518
|
-
type: interaction.type,
|
|
519
|
-
selector: interaction.selector,
|
|
520
|
-
label: interaction.label,
|
|
521
|
-
text: interaction.text
|
|
522
|
-
},
|
|
523
|
-
outcome: 'BLOCKED',
|
|
524
|
-
reason: 'safety_mode',
|
|
525
|
-
classification: blockCheck.classification,
|
|
526
|
-
url: page.url(),
|
|
527
|
-
evidence: {
|
|
528
|
-
selector: interaction.selector,
|
|
529
|
-
label: interaction.label,
|
|
530
|
-
text: interaction.text,
|
|
531
|
-
classification: blockCheck.classification,
|
|
532
|
-
sourcePage: page.url()
|
|
533
|
-
}
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
// Create a minimal trace for blocked interactions so they appear in output
|
|
537
|
-
const blockedTrace = {
|
|
538
|
-
interaction: {
|
|
539
|
-
type: interaction.type,
|
|
540
|
-
selector: interaction.selector,
|
|
541
|
-
label: interaction.label,
|
|
542
|
-
text: interaction.text
|
|
543
|
-
},
|
|
544
|
-
before: {
|
|
545
|
-
url: page.url(),
|
|
546
|
-
screenshot: null
|
|
547
|
-
},
|
|
548
|
-
after: {
|
|
549
|
-
url: page.url(),
|
|
550
|
-
screenshot: null
|
|
551
|
-
},
|
|
552
|
-
policy: {
|
|
553
|
-
actionBlocked: true,
|
|
554
|
-
classification: blockCheck.classification,
|
|
555
|
-
reason: blockCheck.reason
|
|
556
|
-
},
|
|
557
|
-
outcome: 'BLOCKED_BY_SAFETY_MODE',
|
|
558
|
-
timestamp: Date.now()
|
|
559
|
-
};
|
|
560
|
-
traces.push(blockedTrace);
|
|
561
|
-
|
|
562
|
-
continue;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const beforeUrl = page.url();
|
|
566
|
-
const interactionIndex = totalInteractionsExecuted;
|
|
567
|
-
const trace = await runInteraction(
|
|
568
|
-
page,
|
|
569
|
-
interaction,
|
|
570
|
-
timestamp,
|
|
571
|
-
interactionIndex,
|
|
572
|
-
screenshotsDir,
|
|
573
|
-
baseOrigin,
|
|
574
|
-
startTime,
|
|
575
|
-
routeBudget, // Use route-specific budget
|
|
576
|
-
null,
|
|
577
|
-
silenceTracker // Pass silence tracker
|
|
578
|
-
);
|
|
579
|
-
|
|
580
|
-
// Mark trace with incremental flag if applicable
|
|
581
|
-
if (incrementalMode && trace) {
|
|
582
|
-
trace.incremental = false; // This interaction was executed, not skipped
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
let repeatTrace = null;
|
|
586
|
-
|
|
587
|
-
if (trace) {
|
|
588
|
-
const matchingExpectation = expectationResults?.results?.find(r => r.trace?.interaction?.selector === trace.interaction.selector);
|
|
589
|
-
if (matchingExpectation) {
|
|
590
|
-
trace.expectationDriven = true;
|
|
591
|
-
trace.expectationId = matchingExpectation.expectationId;
|
|
592
|
-
trace.expectationOutcome = matchingExpectation.outcome;
|
|
593
|
-
} else {
|
|
594
|
-
const observedExpectation = deriveObservedExpectation(interaction, trace, baseOrigin);
|
|
595
|
-
if (observedExpectation) {
|
|
596
|
-
trace.observedExpectation = observedExpectation;
|
|
597
|
-
trace.resultType = 'OBSERVED_EXPECTATION';
|
|
598
|
-
observedExpectations.push(observedExpectation);
|
|
599
|
-
|
|
600
|
-
const repeatEligible = shouldAttemptRepeatObservedExpectation(observedExpectation, trace);
|
|
601
|
-
const budgetAllowsRepeat = repeatEligible &&
|
|
602
|
-
(Date.now() - startTime) < scanBudget.maxScanDurationMs &&
|
|
603
|
-
(totalInteractionsExecuted + 1) < scanBudget.maxTotalInteractions;
|
|
604
|
-
|
|
605
|
-
if (budgetAllowsRepeat) {
|
|
606
|
-
const repeatIndex = totalInteractionsExecuted + 1;
|
|
607
|
-
const repeatResult = await repeatObservedInteraction(
|
|
608
|
-
page,
|
|
609
|
-
interaction,
|
|
610
|
-
observedExpectation,
|
|
611
|
-
timestamp,
|
|
612
|
-
repeatIndex,
|
|
613
|
-
screenshotsDir,
|
|
614
|
-
baseOrigin,
|
|
615
|
-
startTime,
|
|
616
|
-
scanBudget
|
|
617
|
-
);
|
|
618
|
-
|
|
619
|
-
if (repeatResult) {
|
|
620
|
-
const repeatEvaluation = repeatResult.repeatEvaluation;
|
|
621
|
-
trace.observedExpectation.repeatAttempted = true;
|
|
622
|
-
trace.observedExpectation.repeated = repeatEvaluation.outcome === 'VERIFIED';
|
|
623
|
-
trace.observedExpectation.repeatOutcome = repeatEvaluation.outcome;
|
|
624
|
-
trace.observedExpectation.repeatReason = repeatEvaluation.reason;
|
|
625
|
-
|
|
626
|
-
if (repeatEvaluation.outcome === 'OBSERVED_BREAK') {
|
|
627
|
-
trace.observedExpectation.outcome = 'OBSERVED_BREAK';
|
|
628
|
-
trace.observedExpectation.reason = 'inconsistent_on_repeat';
|
|
629
|
-
trace.observedExpectation.confidenceLevel = 'LOW';
|
|
630
|
-
} else if (trace.observedExpectation.repeated && trace.observedExpectation.outcome === 'VERIFIED') {
|
|
631
|
-
trace.observedExpectation.confidenceLevel = 'MEDIUM';
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
repeatTrace = repeatResult.repeatTrace;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
} else {
|
|
638
|
-
trace.unprovenResult = true;
|
|
639
|
-
trace.resultType = 'UNPROVEN_RESULT';
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
traces.push(trace);
|
|
644
|
-
totalInteractionsExecuted++;
|
|
645
|
-
|
|
646
|
-
if (repeatTrace) {
|
|
647
|
-
traces.push(repeatTrace);
|
|
648
|
-
totalInteractionsExecuted++;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const afterUrl = trace.after?.url || page.url();
|
|
652
|
-
const navigatedSameOrigin = afterUrl && afterUrl !== beforeUrl && !isExternalUrl(afterUrl, baseOrigin);
|
|
653
|
-
if (navigatedSameOrigin && interaction.type === 'link') {
|
|
654
|
-
// Link navigation - add new page to frontier (if not already visited)
|
|
655
|
-
const normalizedAfter = frontier.normalizeUrl(afterUrl);
|
|
656
|
-
const wasAlreadyVisited = frontier.visited.has(normalizedAfter);
|
|
657
|
-
if (!wasAlreadyVisited) {
|
|
658
|
-
const added = frontier.addUrl(afterUrl);
|
|
659
|
-
// If frontier was capped, record coverage gap
|
|
660
|
-
if (!added && frontier.frontierCapped) {
|
|
661
|
-
remainingInteractionsGaps.push({
|
|
662
|
-
interaction: {
|
|
663
|
-
type: 'link',
|
|
664
|
-
selector: interaction.selector,
|
|
665
|
-
label: interaction.label
|
|
666
|
-
},
|
|
667
|
-
reason: 'frontier_capped',
|
|
668
|
-
url: afterUrl
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// Stay on the new page and continue executing interactions there
|
|
674
|
-
navigatedToNewPage = true;
|
|
675
|
-
navigatedPageUrl = afterUrl;
|
|
676
|
-
|
|
677
|
-
// Discover links on the new page immediately
|
|
678
|
-
try {
|
|
679
|
-
const newPageLinks = await page.locator('a[href]').all();
|
|
680
|
-
for (const link of newPageLinks) {
|
|
681
|
-
try {
|
|
682
|
-
const href = await link.getAttribute('href');
|
|
683
|
-
if (href && !href.startsWith('#') && !href.startsWith('javascript:')) {
|
|
684
|
-
const resolvedUrl = href.startsWith('http') ? href : new URL(href, page.url()).href;
|
|
685
|
-
if (!isExternalUrl(resolvedUrl, baseOrigin)) {
|
|
686
|
-
frontier.addUrl(resolvedUrl);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
} catch (error) {
|
|
690
|
-
// Record invalid URL discovery as silence
|
|
691
|
-
silenceTracker.record({
|
|
692
|
-
scope: 'discovery',
|
|
693
|
-
reason: 'discovery_error',
|
|
694
|
-
description: 'Invalid or unreadable link during discovery',
|
|
695
|
-
context: { pageUrl: page.url() },
|
|
696
|
-
impact: 'incomplete_check'
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
} catch (error) {
|
|
701
|
-
// Record link discovery failure as silence
|
|
702
|
-
silenceTracker.record({
|
|
703
|
-
scope: 'discovery',
|
|
704
|
-
reason: 'discovery_error',
|
|
705
|
-
description: 'Link discovery failed on page',
|
|
706
|
-
context: { pageUrl: page.url() },
|
|
707
|
-
impact: 'incomplete_check'
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// Break to restart loop on new page
|
|
712
|
-
break;
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
// Mark remaining interactions as COVERAGE_GAP if we stopped early
|
|
718
|
-
if (remainingInteractionsStartIndex > 0 && remainingInteractionsStartIndex < sortedInteractions.length && !navigatedToNewPage) {
|
|
719
|
-
for (let j = remainingInteractionsStartIndex; j < sortedInteractions.length; j++) {
|
|
720
|
-
const reason = totalInteractionsExecuted >= scanBudget.maxTotalInteractions ? 'budget_exceeded' :
|
|
721
|
-
(totalInteractionsExecuted >= routeBudget.maxInteractionsPerPage ? 'route_budget_exceeded' : 'budget_exceeded');
|
|
722
|
-
remainingInteractionsGaps.push({
|
|
723
|
-
interaction: {
|
|
724
|
-
type: sortedInteractions[j].type,
|
|
725
|
-
selector: sortedInteractions[j].selector,
|
|
726
|
-
label: sortedInteractions[j].label
|
|
727
|
-
},
|
|
728
|
-
reason: reason,
|
|
729
|
-
url: currentUrl
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// If we navigated to a new page, stay on it and continue (next iteration will handle it)
|
|
735
|
-
if (navigatedToNewPage && navigatedPageUrl) {
|
|
736
|
-
// Don't mark as visited yet - we'll do it at the start of next iteration
|
|
737
|
-
// This ensures the page counter is incremented when we process the page
|
|
738
|
-
// Set nextPageUrl to the navigated page so we process it in the next iteration
|
|
739
|
-
nextPageUrl = navigatedPageUrl;
|
|
740
|
-
continue;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// After executing all interactions on current page, move to next page in frontier
|
|
744
|
-
nextPageUrl = frontier.getNextUrl();
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// Combine all coverage gaps
|
|
748
|
-
if (remainingInteractionsGaps.length > 0) {
|
|
749
|
-
expectationCoverageGaps.push(...remainingInteractionsGaps.map(gap => ({
|
|
750
|
-
expectationId: null,
|
|
751
|
-
type: gap.interaction.type,
|
|
752
|
-
reason: gap.reason,
|
|
753
|
-
fromPath: gap.url,
|
|
754
|
-
source: null,
|
|
755
|
-
evidence: {
|
|
756
|
-
interaction: gap.interaction
|
|
757
|
-
}
|
|
758
|
-
})));
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
// Record frontier capping as coverage gap if it occurred
|
|
762
|
-
if (frontier.frontierCapped) {
|
|
763
|
-
expectationCoverageGaps.push({
|
|
764
|
-
expectationId: null,
|
|
765
|
-
type: 'navigation',
|
|
766
|
-
reason: 'frontier_capped',
|
|
767
|
-
fromPath: page.url(),
|
|
768
|
-
source: null,
|
|
769
|
-
evidence: {
|
|
770
|
-
message: `Frontier capped at ${scanBudget.maxUniqueUrls || 'unlimited'} unique URLs`
|
|
771
|
-
}
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
// Build coverage object matching writeTraces expected format
|
|
776
|
-
const coverage = {
|
|
777
|
-
candidatesDiscovered: totalInteractionsDiscovered,
|
|
778
|
-
candidatesSelected: totalInteractionsExecuted,
|
|
779
|
-
cap: scanBudget.maxTotalInteractions,
|
|
780
|
-
capped: totalInteractionsExecuted >= scanBudget.maxTotalInteractions || remainingInteractionsGaps.length > 0,
|
|
781
|
-
pagesVisited: frontier.pagesVisited,
|
|
782
|
-
pagesDiscovered: frontier.pagesDiscovered,
|
|
783
|
-
skippedInteractions: skippedInteractions.length,
|
|
784
|
-
interactionsDiscovered: totalInteractionsDiscovered,
|
|
785
|
-
interactionsExecuted: totalInteractionsExecuted
|
|
786
|
-
};
|
|
787
|
-
|
|
788
|
-
// Ensure we increment pagesVisited when we navigate via getNextUrl()
|
|
789
|
-
// getNextUrl() marks as visited but doesn't increment counter - we do it here
|
|
790
|
-
// BUT: when alreadyOnPage is true, we've already marked it, so don't double-count
|
|
791
|
-
|
|
792
|
-
// Record warnings
|
|
793
|
-
if (coverage.capped) {
|
|
794
|
-
observeWarnings.push({
|
|
795
|
-
code: 'INTERACTIONS_CAPPED',
|
|
796
|
-
message: `Interaction execution capped. Visited ${coverage.pagesVisited} pages, discovered ${coverage.pagesDiscovered}, executed ${coverage.candidatesSelected} of ${coverage.candidatesDiscovered} interactions. Coverage incomplete.`
|
|
797
|
-
});
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
if (skippedInteractions.length > 0) {
|
|
801
|
-
observeWarnings.push({
|
|
802
|
-
code: 'INTERACTIONS_SKIPPED',
|
|
803
|
-
message: `Skipped ${skippedInteractions.length} dangerous interactions`,
|
|
804
|
-
details: skippedInteractions
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// Append expectation traces for completeness
|
|
809
|
-
if (expectationResults && expectationResults.results) {
|
|
810
|
-
for (const result of expectationResults.results) {
|
|
811
|
-
if (result.trace) {
|
|
812
|
-
result.trace.expectationDriven = true;
|
|
813
|
-
result.trace.expectationId = result.expectationId;
|
|
814
|
-
result.trace.expectationOutcome = result.outcome;
|
|
815
|
-
traces.push(result.trace);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
const observation = writeTraces(projectDir, url, traces, coverage, observeWarnings, observedExpectations, silenceTracker, runId);
|
|
821
|
-
|
|
822
|
-
// Add silence tracking to observation result
|
|
823
|
-
observation.silences = silenceTracker.getDetailedSummary();
|
|
824
|
-
|
|
825
|
-
if (expectationResults) {
|
|
826
|
-
observation.expectationExecution = {
|
|
827
|
-
totalProvenExpectations: expectationResults.totalProvenExpectations,
|
|
828
|
-
executedCount: expectationResults.executedCount,
|
|
829
|
-
coverageGapsCount: expectationCoverageGaps.length,
|
|
830
|
-
results: expectationResults.results.map(r => ({
|
|
831
|
-
expectationId: r.expectationId,
|
|
832
|
-
type: r.type,
|
|
833
|
-
fromPath: r.fromPath,
|
|
834
|
-
outcome: r.outcome,
|
|
835
|
-
reason: r.reason
|
|
836
|
-
}))
|
|
837
|
-
};
|
|
838
|
-
observation.expectationCoverageGaps = expectationCoverageGaps;
|
|
839
|
-
}
|
|
111
|
+
// PHASE 21.3: Run main traversal loop
|
|
112
|
+
const { runTraversalLoop } = await import('./observe-runner.js');
|
|
113
|
+
const traversalResult = await runTraversalLoop({
|
|
114
|
+
page,
|
|
115
|
+
url,
|
|
116
|
+
baseOrigin,
|
|
117
|
+
scanBudget,
|
|
118
|
+
startTime,
|
|
119
|
+
frontier,
|
|
120
|
+
manifest,
|
|
121
|
+
expectationResults,
|
|
122
|
+
incrementalMode,
|
|
123
|
+
oldSnapshot,
|
|
124
|
+
snapshotDiff,
|
|
125
|
+
currentUrl: page.url(),
|
|
126
|
+
screenshotsDir,
|
|
127
|
+
timestamp,
|
|
128
|
+
decisionRecorder,
|
|
129
|
+
silenceTracker,
|
|
130
|
+
traces,
|
|
131
|
+
skippedInteractions,
|
|
132
|
+
observedExpectations,
|
|
133
|
+
totalInteractionsDiscovered,
|
|
134
|
+
totalInteractionsExecuted,
|
|
135
|
+
remainingInteractionsGaps,
|
|
136
|
+
allowWrites,
|
|
137
|
+
allowRiskyActions
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// PHASE 21.3: Process traversal results using helper
|
|
141
|
+
const finalTraces = [...traversalResult.traces];
|
|
142
|
+
const finalSkippedInteractions = [...traversalResult.skippedInteractions];
|
|
143
|
+
const finalObservedExpectations = [...traversalResult.observedExpectations];
|
|
144
|
+
remainingInteractionsGaps = [...traversalResult.remainingInteractionsGaps];
|
|
840
145
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
};
|
|
861
|
-
}
|
|
146
|
+
const observation = await processTraversalResults(
|
|
147
|
+
traversalResult,
|
|
148
|
+
expectationResults,
|
|
149
|
+
expectationCoverageGaps,
|
|
150
|
+
remainingInteractionsGaps,
|
|
151
|
+
frontier,
|
|
152
|
+
scanBudget,
|
|
153
|
+
page,
|
|
154
|
+
url,
|
|
155
|
+
finalTraces,
|
|
156
|
+
finalSkippedInteractions,
|
|
157
|
+
finalObservedExpectations,
|
|
158
|
+
silenceTracker,
|
|
159
|
+
manifest,
|
|
160
|
+
incrementalMode,
|
|
161
|
+
snapshotDiff,
|
|
162
|
+
resolvedProjectDir,
|
|
163
|
+
runId
|
|
164
|
+
);
|
|
862
165
|
|
|
863
166
|
await closeBrowser(browser);
|
|
864
167
|
|
|
865
|
-
// PHASE
|
|
866
|
-
|
|
867
|
-
const runsDir = resolve(projectDir, '.verax', 'runs', runId);
|
|
868
|
-
mkdirSync(runsDir, { recursive: true });
|
|
869
|
-
const decisionsPath = resolve(runsDir, 'decisions.json');
|
|
870
|
-
const decisionsData = JSON.stringify(decisionRecorder.export(), null, 2);
|
|
871
|
-
writeFileSync(decisionsPath, decisionsData, 'utf-8');
|
|
872
|
-
}
|
|
168
|
+
// PHASE 21.3: Write determinism artifacts using helper
|
|
169
|
+
await writeDeterminismArtifacts(resolvedProjectDir, runId, decisionRecorder);
|
|
873
170
|
|
|
874
171
|
// Phase 4: Add safety mode statistics
|
|
875
172
|
const safetyBlocks = {
|
|
876
|
-
actionsBlocked:
|
|
173
|
+
actionsBlocked: finalSkippedInteractions.filter(s => s.reason === 'safety_mode').length,
|
|
877
174
|
networkWritesBlocked: blockedNetworkWrites.length,
|
|
878
175
|
crossOriginBlocked: blockedCrossOrigin.length,
|
|
879
|
-
blockedActions:
|
|
176
|
+
blockedActions: finalSkippedInteractions.filter(s => s.reason === 'safety_mode').map(s => ({
|
|
880
177
|
label: s.interaction.label,
|
|
881
178
|
classification: s.classification,
|
|
882
179
|
url: s.url
|
|
@@ -896,56 +193,4 @@ export async function observe(url, manifestPath = null, scanBudgetOverride = nul
|
|
|
896
193
|
}
|
|
897
194
|
}
|
|
898
195
|
|
|
899
|
-
|
|
900
|
-
page,
|
|
901
|
-
interaction,
|
|
902
|
-
observedExpectation,
|
|
903
|
-
timestamp,
|
|
904
|
-
interactionIndex,
|
|
905
|
-
screenshotsDir,
|
|
906
|
-
baseOrigin,
|
|
907
|
-
startTime,
|
|
908
|
-
scanBudget
|
|
909
|
-
) {
|
|
910
|
-
const selector = observedExpectation.evidence?.selector || interaction.selector;
|
|
911
|
-
if (!selector) return null;
|
|
912
|
-
|
|
913
|
-
const locator = page.locator(selector).first();
|
|
914
|
-
const count = await locator.count();
|
|
915
|
-
if (count === 0) {
|
|
916
|
-
return null;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
const repeatInteraction = {
|
|
920
|
-
...interaction,
|
|
921
|
-
element: locator
|
|
922
|
-
};
|
|
923
|
-
|
|
924
|
-
const repeatTrace = await runInteraction(
|
|
925
|
-
page,
|
|
926
|
-
repeatInteraction,
|
|
927
|
-
timestamp,
|
|
928
|
-
interactionIndex,
|
|
929
|
-
screenshotsDir,
|
|
930
|
-
baseOrigin,
|
|
931
|
-
startTime,
|
|
932
|
-
scanBudget,
|
|
933
|
-
null,
|
|
934
|
-
null // No silence tracker for repeat executions (not counted as new silence)
|
|
935
|
-
);
|
|
936
|
-
|
|
937
|
-
if (!repeatTrace) {
|
|
938
|
-
return null;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
repeatTrace.repeatExecution = true;
|
|
942
|
-
repeatTrace.repeatOfObservedExpectationId = observedExpectation.id;
|
|
943
|
-
repeatTrace.resultType = 'OBSERVED_EXPECTATION_REPEAT';
|
|
944
|
-
|
|
945
|
-
const repeatEvaluation = evaluateObservedExpectation(observedExpectation, repeatTrace);
|
|
946
|
-
|
|
947
|
-
return {
|
|
948
|
-
repeatTrace,
|
|
949
|
-
repeatEvaluation
|
|
950
|
-
};
|
|
951
|
-
}
|
|
196
|
+
// PHASE 21.3: repeatObservedInteraction moved to observe-runner.js
|