@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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NETWORK SAFETY FIREWALL
|
|
3
|
+
*
|
|
4
|
+
* Handles network request interception and blocking:
|
|
5
|
+
* - Cross-origin blocking (unless --allow-cross-origin)
|
|
6
|
+
* - Read-only mode (blocks POST/PUT/PATCH/DELETE unconditionally)
|
|
7
|
+
* - Safety tracking via SilenceTracker
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Setup network interception firewall for safety mode
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} page - Playwright page object
|
|
14
|
+
* @param {string} baseOrigin - Base origin URL for cross-origin checks
|
|
15
|
+
* @param {boolean} allowCrossOrigin - Whether to allow cross-origin requests
|
|
16
|
+
* @param {Object} silenceTracker - Silence tracker instance
|
|
17
|
+
* @returns {Promise<{blockedNetworkWrites: Array, blockedCrossOrigin: Array}>}
|
|
18
|
+
*/
|
|
19
|
+
export async function setupNetworkFirewall(page, baseOrigin, allowCrossOrigin, silenceTracker) {
|
|
20
|
+
const blockedNetworkWrites = [];
|
|
21
|
+
const blockedCrossOrigin = [];
|
|
22
|
+
|
|
23
|
+
await page.route('**/*', (route) => {
|
|
24
|
+
const request = route.request();
|
|
25
|
+
const method = request.method();
|
|
26
|
+
const requestUrl = request.url();
|
|
27
|
+
const resourceType = request.resourceType();
|
|
28
|
+
|
|
29
|
+
// Check cross-origin blocking (skip for file:// URLs)
|
|
30
|
+
if (!allowCrossOrigin && !requestUrl.startsWith('file://')) {
|
|
31
|
+
try {
|
|
32
|
+
const reqOrigin = new URL(requestUrl).origin;
|
|
33
|
+
if (reqOrigin !== baseOrigin) {
|
|
34
|
+
blockedCrossOrigin.push({
|
|
35
|
+
url: requestUrl,
|
|
36
|
+
origin: reqOrigin,
|
|
37
|
+
method,
|
|
38
|
+
resourceType,
|
|
39
|
+
timestamp: Date.now()
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
silenceTracker.record({
|
|
43
|
+
scope: 'safety',
|
|
44
|
+
reason: 'cross_origin_blocked',
|
|
45
|
+
description: `Cross-origin request blocked: ${method} ${requestUrl}`,
|
|
46
|
+
context: { url: requestUrl, origin: reqOrigin, method, baseOrigin },
|
|
47
|
+
impact: 'request_blocked'
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return route.abort('blockedbyclient');
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
// Invalid URL, allow and let browser handle
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// CONSTITUTIONAL: Block all write methods (read-only mode enforced)
|
|
58
|
+
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
59
|
+
// Check if it's a GraphQL mutation (best-effort)
|
|
60
|
+
const isGraphQLMutation = requestUrl.includes('/graphql') && method === 'POST';
|
|
61
|
+
|
|
62
|
+
blockedNetworkWrites.push({
|
|
63
|
+
url: requestUrl,
|
|
64
|
+
method,
|
|
65
|
+
resourceType,
|
|
66
|
+
isGraphQLMutation,
|
|
67
|
+
timestamp: Date.now()
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
silenceTracker.record({
|
|
71
|
+
scope: 'safety',
|
|
72
|
+
reason: 'blocked_network_write',
|
|
73
|
+
description: `Network write blocked: ${method} ${requestUrl}${isGraphQLMutation ? ' (GraphQL mutation)' : ''}`,
|
|
74
|
+
context: { url: requestUrl, method, resourceType, isGraphQLMutation },
|
|
75
|
+
impact: 'write_blocked'
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return route.abort('blockedbyclient');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Allow request
|
|
82
|
+
route.continue();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return { blockedNetworkWrites, blockedCrossOrigin };
|
|
86
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OBSERVATION BUILDER
|
|
3
|
+
*
|
|
4
|
+
* Constructs observation object from traces, calculates coverage, and generates warnings.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build observation object from collected traces
|
|
9
|
+
*
|
|
10
|
+
* @param {Array} traces - Array of interaction traces
|
|
11
|
+
* @param {Array} coverage - Coverage information
|
|
12
|
+
* @param {Array} warnings - Warning messages
|
|
13
|
+
* @param {Object} safetyStats - Safety mode statistics
|
|
14
|
+
* @returns {Object}
|
|
15
|
+
*/
|
|
16
|
+
export function buildObservation(traces, coverage, warnings, safetyStats) {
|
|
17
|
+
const observation = {
|
|
18
|
+
timestamp: new Date().toISOString(),
|
|
19
|
+
traces: traces,
|
|
20
|
+
coverage: coverage || [],
|
|
21
|
+
warnings: warnings || [],
|
|
22
|
+
safetyStats: safetyStats || {}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return observation;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Calculate coverage gaps from traces
|
|
30
|
+
*
|
|
31
|
+
* @param {Array} traces - Array of interaction traces
|
|
32
|
+
* @param {Array} discoveredInteractions - All interactions discovered during scan
|
|
33
|
+
* @returns {Array}
|
|
34
|
+
*/
|
|
35
|
+
export function calculateCoverageGaps(traces, discoveredInteractions) {
|
|
36
|
+
if (!discoveredInteractions || discoveredInteractions.length === 0) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const executedSelectors = new Set(
|
|
41
|
+
traces
|
|
42
|
+
.filter(t => t.interaction && t.interaction.selector)
|
|
43
|
+
.map(t => t.interaction.selector)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const gaps = discoveredInteractions
|
|
47
|
+
.filter(interaction => !executedSelectors.has(interaction.selector))
|
|
48
|
+
.map(interaction => ({
|
|
49
|
+
selector: interaction.selector,
|
|
50
|
+
type: interaction.type,
|
|
51
|
+
reason: 'budget_exhausted_or_skipped'
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
return gaps;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Generate warnings from observation data
|
|
59
|
+
*
|
|
60
|
+
* @param {Object} frontier - Page frontier
|
|
61
|
+
* @param {Array} coverage - Coverage data
|
|
62
|
+
* @param {Object} safetyStats - Safety statistics
|
|
63
|
+
* @returns {Array}
|
|
64
|
+
*/
|
|
65
|
+
export function generateWarnings(frontier, coverage, safetyStats) {
|
|
66
|
+
const warnings = [];
|
|
67
|
+
|
|
68
|
+
// Warn if frontier has unexplored pages
|
|
69
|
+
if (frontier && frontier.queue && frontier.queue.length > 0) {
|
|
70
|
+
warnings.push({
|
|
71
|
+
type: 'unexplored_pages',
|
|
72
|
+
count: frontier.queue.length,
|
|
73
|
+
message: `${frontier.queue.length} pages in queue were not explored due to budget constraints`
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Warn if safety mode blocked actions
|
|
78
|
+
if (safetyStats && safetyStats.blockedNetworkWrites > 0) {
|
|
79
|
+
warnings.push({
|
|
80
|
+
type: 'safety_mode_active',
|
|
81
|
+
blockedWrites: safetyStats.blockedNetworkWrites,
|
|
82
|
+
message: `Safety mode prevented ${safetyStats.blockedNetworkWrites} write operations`
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (safetyStats && safetyStats.blockedCrossOrigin > 0) {
|
|
87
|
+
warnings.push({
|
|
88
|
+
type: 'cross_origin_blocked',
|
|
89
|
+
count: safetyStats.blockedCrossOrigin,
|
|
90
|
+
message: `${safetyStats.blockedCrossOrigin} cross-origin requests were blocked`
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return warnings;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build safety statistics object
|
|
99
|
+
*
|
|
100
|
+
* @param {number} blockedNetworkWrites - Number of blocked write operations
|
|
101
|
+
* @param {number} blockedCrossOrigin - Number of blocked cross-origin requests
|
|
102
|
+
* @param {Array} skippedInteractions - Interactions that were skipped
|
|
103
|
+
* @returns {Object}
|
|
104
|
+
*/
|
|
105
|
+
export function buildSafetyStatistics(blockedNetworkWrites, blockedCrossOrigin, skippedInteractions) {
|
|
106
|
+
return {
|
|
107
|
+
safetyModeEnabled: true,
|
|
108
|
+
blockedNetworkWrites: blockedNetworkWrites || 0,
|
|
109
|
+
blockedCrossOrigin: blockedCrossOrigin || 0,
|
|
110
|
+
skippedInteractions: skippedInteractions ? skippedInteractions.length : 0,
|
|
111
|
+
skippedReasons: skippedInteractions ? countSkipReasons(skippedInteractions) : {}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Count reasons for skipped interactions
|
|
117
|
+
*
|
|
118
|
+
* @param {Array} skippedInteractions - Skipped interactions
|
|
119
|
+
* @returns {Object}
|
|
120
|
+
*/
|
|
121
|
+
function countSkipReasons(skippedInteractions) {
|
|
122
|
+
const reasons = {};
|
|
123
|
+
for (const skip of skippedInteractions) {
|
|
124
|
+
const reason = skip.reason || 'unknown';
|
|
125
|
+
reasons[reason] = (reasons[reason] || 0) + 1;
|
|
126
|
+
}
|
|
127
|
+
return reasons;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Extract observedExpectations from traces
|
|
132
|
+
*
|
|
133
|
+
* @param {Array} traces - Array of interaction traces
|
|
134
|
+
* @returns {Array}
|
|
135
|
+
*/
|
|
136
|
+
export function extractObservedExpectations(traces) {
|
|
137
|
+
const expectations = [];
|
|
138
|
+
|
|
139
|
+
for (const trace of traces) {
|
|
140
|
+
if (trace.observedExpectation) {
|
|
141
|
+
expectations.push({
|
|
142
|
+
...trace.observedExpectation,
|
|
143
|
+
traceId: trace.id,
|
|
144
|
+
executionTimestamp: trace.timestamp
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return expectations;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Build skipped interactions summary
|
|
154
|
+
*
|
|
155
|
+
* @param {Array} interactions - Skipped interactions
|
|
156
|
+
* @returns {Array}
|
|
157
|
+
*/
|
|
158
|
+
export function buildSkippedInteractionsSummary(interactions) {
|
|
159
|
+
if (!interactions || interactions.length === 0) {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return interactions.map(skip => ({
|
|
164
|
+
selector: skip.selector,
|
|
165
|
+
type: skip.type,
|
|
166
|
+
reason: skip.reason,
|
|
167
|
+
element: skip.element
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
@@ -13,7 +13,7 @@ import { writeTraces } from './traces-writer.js';
|
|
|
13
13
|
*/
|
|
14
14
|
export async function setupManifestAndExpectations(manifestPath, projectDir, page, url, screenshotsDir, scanBudget, startTime, silenceTracker) {
|
|
15
15
|
const { readFileSync, existsSync } = await import('fs');
|
|
16
|
-
const { loadPreviousSnapshot, saveSnapshot, buildSnapshot, compareSnapshots } = await import('../core/incremental-store.js');
|
|
16
|
+
const { loadPreviousSnapshot, saveSnapshot: _saveSnapshot, buildSnapshot, compareSnapshots } = await import('../core/incremental-store.js');
|
|
17
17
|
const { executeProvenExpectations } = await import('./expectation-executor.js');
|
|
18
18
|
const { isProvenExpectation } = await import('../shared/expectation-prover.js');
|
|
19
19
|
|
|
@@ -27,6 +27,7 @@ export async function setupManifestAndExpectations(manifestPath, projectDir, pag
|
|
|
27
27
|
if (manifestPath && existsSync(manifestPath)) {
|
|
28
28
|
try {
|
|
29
29
|
const manifestContent = readFileSync(manifestPath, 'utf-8');
|
|
30
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
30
31
|
manifest = JSON.parse(manifestContent);
|
|
31
32
|
|
|
32
33
|
oldSnapshot = loadPreviousSnapshot(projectDir);
|
|
@@ -15,31 +15,31 @@ import { createObserveContext } from './observe-context.js';
|
|
|
15
15
|
/**
|
|
16
16
|
* Run main traversal/interaction loop
|
|
17
17
|
*
|
|
18
|
-
* @param {Object} params
|
|
19
|
-
* @param {import('playwright').Page} params.page
|
|
20
|
-
* @param {string} params.url
|
|
21
|
-
* @param {string} params.baseOrigin
|
|
22
|
-
* @param {Object} params.scanBudget
|
|
23
|
-
* @param {number} params.startTime
|
|
24
|
-
* @param {
|
|
25
|
-
* @param {Object|null} params.manifest
|
|
26
|
-
* @param {Object|null} params.expectationResults
|
|
27
|
-
* @param {boolean} params.incrementalMode
|
|
28
|
-
* @param {Object|null} params.oldSnapshot
|
|
29
|
-
* @param {Object|null} params.snapshotDiff
|
|
30
|
-
* @param {string} params.currentUrl
|
|
31
|
-
* @param {string} params.screenshotsDir
|
|
32
|
-
* @param {number} params.timestamp
|
|
18
|
+
* @param {Object} params - Parameters object
|
|
19
|
+
* @param {import('playwright').Page} params.page - Playwright page instance
|
|
20
|
+
* @param {string} params.url - Page URL
|
|
21
|
+
* @param {string} params.baseOrigin - Base origin for relative URLs
|
|
22
|
+
* @param {Object} params.scanBudget - Scan budget configuration
|
|
23
|
+
* @param {number} params.startTime - Scan start time
|
|
24
|
+
* @param {Object} params.frontier - Page frontier (URL queue)
|
|
25
|
+
* @param {Object|null} params.manifest - Manifest file data
|
|
26
|
+
* @param {Object|null} params.expectationResults - Expectation results
|
|
27
|
+
* @param {boolean} params.incrementalMode - Incremental mode flag
|
|
28
|
+
* @param {Object|null} params.oldSnapshot - Old snapshot data
|
|
29
|
+
* @param {Object|null} params.snapshotDiff - Snapshot diff
|
|
30
|
+
* @param {string} params.currentUrl - Current page URL
|
|
31
|
+
* @param {string} params.screenshotsDir - Screenshots directory
|
|
32
|
+
* @param {number} params.timestamp - Current timestamp
|
|
33
33
|
* @param {Object} params.decisionRecorder - DecisionRecorder instance
|
|
34
34
|
* @param {Object} params.silenceTracker - SilenceTracker instance
|
|
35
|
-
* @param {Array} params.traces
|
|
36
|
-
* @param {Array} params.skippedInteractions
|
|
37
|
-
* @param {Array} params.observedExpectations
|
|
38
|
-
* @param {number} params.totalInteractionsDiscovered
|
|
39
|
-
* @param {number} params.totalInteractionsExecuted
|
|
40
|
-
* @param {Array} params.remainingInteractionsGaps
|
|
41
|
-
* @param {boolean} [params.allowWrites]
|
|
42
|
-
* @param {boolean} [params.allowRiskyActions]
|
|
35
|
+
* @param {Array} params.traces - Interaction traces array
|
|
36
|
+
* @param {Array} params.skippedInteractions - Skipped interactions array
|
|
37
|
+
* @param {Array} params.observedExpectations - Observed expectations array
|
|
38
|
+
* @param {number} params.totalInteractionsDiscovered - Total discovered interactions
|
|
39
|
+
* @param {number} params.totalInteractionsExecuted - Total executed interactions
|
|
40
|
+
* @param {Array} params.remainingInteractionsGaps - Remaining interaction gaps
|
|
41
|
+
* @param {boolean} [params.allowWrites=false] - Allow file writes
|
|
42
|
+
* @param {boolean} [params.allowRiskyActions=false] - Allow risky actions
|
|
43
43
|
* @returns {Promise<Object>} { traces, skippedInteractions, observedExpectations, totalInteractionsDiscovered, totalInteractionsExecuted, remainingInteractionsGaps }
|
|
44
44
|
*/
|
|
45
45
|
export async function runTraversalLoop(params) {
|
|
@@ -110,7 +110,11 @@ export async function runTraversalLoop(params) {
|
|
|
110
110
|
const context = createObserveContext({ ...baseContext, currentUrl, routeBudget: baseContext.routeBudget });
|
|
111
111
|
|
|
112
112
|
// PHASE 21.3: Check page limit using budget-observer
|
|
113
|
-
const pageLimitCheck = checkBudget(context, runState, {
|
|
113
|
+
const pageLimitCheck = checkBudget(context, runState, {
|
|
114
|
+
remainingInteractions: 0,
|
|
115
|
+
currentTotalExecuted: 0,
|
|
116
|
+
limitType: 'pages'
|
|
117
|
+
});
|
|
114
118
|
if (pageLimitCheck.exceeded) break;
|
|
115
119
|
|
|
116
120
|
// Check if we're already on the target page (from navigation via link click)
|
|
@@ -15,13 +15,13 @@ import { recordTruncation } from '../../core/determinism-model.js';
|
|
|
15
15
|
/**
|
|
16
16
|
* Check if budget limits are exceeded
|
|
17
17
|
*
|
|
18
|
-
* @param {
|
|
19
|
-
* @param {
|
|
18
|
+
* @param {Object} context - Observe context
|
|
19
|
+
* @param {Object} runState - Current run state
|
|
20
20
|
* @param {Object} options - Additional options
|
|
21
21
|
* @param {number} options.remainingInteractions - Remaining interactions count
|
|
22
22
|
* @param {number} options.currentTotalExecuted - Current total executed
|
|
23
23
|
* @param {string} options.limitType - Type of limit to check ('time', 'per_page', 'total', 'pages')
|
|
24
|
-
* @returns {Object} { exceeded: boolean, reason?: string, observation?:
|
|
24
|
+
* @returns {Object} { exceeded: boolean, reason?: string, observation?: Object }
|
|
25
25
|
*/
|
|
26
26
|
export function checkBudget(context, runState, options) {
|
|
27
27
|
const { scanBudget, startTime, routeBudget, decisionRecorder, silenceTracker, frontier, page } = context;
|
|
@@ -14,11 +14,11 @@ import { ConsoleSensor } from '../console-sensor.js';
|
|
|
14
14
|
/**
|
|
15
15
|
* Observe console errors and warnings on current page
|
|
16
16
|
*
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {
|
|
19
|
-
* @returns {Promise<Array<
|
|
17
|
+
* @param {Object} context - Observe context
|
|
18
|
+
* @param {Object} _runState - Current run state
|
|
19
|
+
* @returns {Promise<Array<Object>>} Array of console observations
|
|
20
20
|
*/
|
|
21
|
-
export async function observe(context,
|
|
21
|
+
export async function observe(context, _runState) {
|
|
22
22
|
const { page, currentUrl, timestamp, silenceTracker } = context;
|
|
23
23
|
const observations = [];
|
|
24
24
|
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
* - NO side effects outside its scope
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { recordTruncation } from '../../core/determinism-model.js';
|
|
12
|
+
import { recordTruncation as _recordTruncation } from '../../core/determinism-model.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Create coverage gap for remaining interactions
|
|
16
16
|
*
|
|
17
|
-
* @param {
|
|
17
|
+
* @param {Object} context - Observe context
|
|
18
18
|
* @param {Array} remainingInteractions - Remaining interactions
|
|
19
19
|
* @param {number} startIndex - Start index of remaining interactions
|
|
20
20
|
* @param {string} reason - Reason for gap
|
|
@@ -42,7 +42,7 @@ export function createCoverageGaps(context, remainingInteractions, startIndex, r
|
|
|
42
42
|
/**
|
|
43
43
|
* Build coverage summary
|
|
44
44
|
*
|
|
45
|
-
* @param {
|
|
45
|
+
* @param {Object} context - Observe context
|
|
46
46
|
* @param {number} totalDiscovered - Total interactions discovered
|
|
47
47
|
* @param {number} totalExecuted - Total interactions executed
|
|
48
48
|
* @param {number} skippedCount - Number of skipped interactions
|
|
@@ -68,7 +68,7 @@ export function buildCoverageSummary(context, totalDiscovered, totalExecuted, sk
|
|
|
68
68
|
/**
|
|
69
69
|
* Create coverage gap for frontier capping
|
|
70
70
|
*
|
|
71
|
-
* @param {
|
|
71
|
+
* @param {Object} context - Observe context
|
|
72
72
|
* @returns {Object} Coverage gap
|
|
73
73
|
*/
|
|
74
74
|
export function createFrontierCappedGap(context) {
|
|
@@ -56,8 +56,8 @@ export async function discoverInteractions(context) {
|
|
|
56
56
|
* @param {string} currentUrl
|
|
57
57
|
* @param {Array} traces - Mutable array to push skipped traces
|
|
58
58
|
* @param {Array} skippedInteractions - Mutable array to push skipped interactions
|
|
59
|
-
* @param {
|
|
60
|
-
* @param {
|
|
59
|
+
* @param {Object} silenceTracker - Silence tracker instance
|
|
60
|
+
* @param {Object} frontier - Page frontier instance
|
|
61
61
|
* @param {boolean} incrementalMode
|
|
62
62
|
* @param {Object|null} manifest
|
|
63
63
|
* @param {Object|null} oldSnapshot
|
|
@@ -255,7 +255,7 @@ export async function checkAndSkipInteraction(
|
|
|
255
255
|
* @param {Array} traces - Mutable array to push traces
|
|
256
256
|
* @param {Array} observedExpectations - Mutable array to push observed expectations
|
|
257
257
|
* @param {Array} remainingInteractionsGaps - Mutable array to push gaps
|
|
258
|
-
* @param {
|
|
258
|
+
* @param {Object} frontier - Page frontier instance
|
|
259
259
|
* @param {string} baseOrigin
|
|
260
260
|
* @param {boolean} incrementalMode
|
|
261
261
|
* @param {Object|null} expectationResults
|
|
@@ -15,7 +15,7 @@ import { isExternalUrl } from '../domain-boundary.js';
|
|
|
15
15
|
/**
|
|
16
16
|
* Navigate to a URL
|
|
17
17
|
*
|
|
18
|
-
* @param {
|
|
18
|
+
* @param {Object} context - Observe context
|
|
19
19
|
* @param {string} targetUrl - URL to navigate to
|
|
20
20
|
* @returns {Promise<boolean>} True if navigation succeeded, false if failed
|
|
21
21
|
*/
|
|
@@ -48,7 +48,7 @@ export async function navigateToPage(context, targetUrl) {
|
|
|
48
48
|
/**
|
|
49
49
|
* Discover links on current page and add to frontier
|
|
50
50
|
*
|
|
51
|
-
* @param {
|
|
51
|
+
* @param {Object} context - Observe context
|
|
52
52
|
* @returns {Promise<void>}
|
|
53
53
|
*/
|
|
54
54
|
export async function discoverPageLinks(context) {
|
|
@@ -91,7 +91,7 @@ export async function discoverPageLinks(context) {
|
|
|
91
91
|
/**
|
|
92
92
|
* Check if we're already on the target page
|
|
93
93
|
*
|
|
94
|
-
* @param {
|
|
94
|
+
* @param {Object} context - Observe context
|
|
95
95
|
* @param {string} targetUrl - Target URL
|
|
96
96
|
* @returns {boolean} True if already on page
|
|
97
97
|
*/
|
|
@@ -106,7 +106,7 @@ export function isAlreadyOnPage(context, targetUrl) {
|
|
|
106
106
|
/**
|
|
107
107
|
* Mark page as visited in frontier
|
|
108
108
|
*
|
|
109
|
-
* @param {
|
|
109
|
+
* @param {Object} context - Observe context
|
|
110
110
|
* @param {string} targetUrl - Target URL
|
|
111
111
|
* @param {boolean} alreadyOnPage - Whether we're already on the page
|
|
112
112
|
* @returns {void}
|
|
@@ -16,11 +16,11 @@ import { NetworkSensor } from '../network-sensor.js';
|
|
|
16
16
|
/**
|
|
17
17
|
* Observe network state on current page
|
|
18
18
|
*
|
|
19
|
-
* @param {
|
|
20
|
-
* @param {
|
|
21
|
-
* @returns {Promise<Array<
|
|
19
|
+
* @param {Object} context - Observe context
|
|
20
|
+
* @param {Object} _runState - Current run state
|
|
21
|
+
* @returns {Promise<Array<Object>>} Array of network observations
|
|
22
22
|
*/
|
|
23
|
-
export async function observe(context,
|
|
23
|
+
export async function observe(context, _runState) {
|
|
24
24
|
const { page, currentUrl, timestamp } = context;
|
|
25
25
|
const observations = [];
|
|
26
26
|
|
|
@@ -16,11 +16,11 @@ import { captureDomSignature } from '../dom-signature.js';
|
|
|
16
16
|
/**
|
|
17
17
|
* Observe UI feedback and DOM state on current page
|
|
18
18
|
*
|
|
19
|
-
* @param {
|
|
20
|
-
* @param {
|
|
21
|
-
* @returns {Promise<Array<
|
|
19
|
+
* @param {Object} context - Observe context
|
|
20
|
+
* @param {Object} _runState - Current run state
|
|
21
|
+
* @returns {Promise<Array<Object>>} Array of UI feedback observations
|
|
22
22
|
*/
|
|
23
|
-
export async function observe(context,
|
|
23
|
+
export async function observe(context, _runState) {
|
|
24
24
|
const { page, currentUrl, timestamp } = context;
|
|
25
25
|
const observations = [];
|
|
26
26
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAGE TRAVERSAL ENGINE
|
|
3
|
+
*
|
|
4
|
+
* Manages page frontier, link discovery, and page-to-page navigation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { isExternalUrl } from './domain-boundary.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Discover links on current page
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} page - Playwright page
|
|
13
|
+
* @param {string} baseOrigin - Base origin URL
|
|
14
|
+
* @param {Object} silenceTracker - Silence tracker
|
|
15
|
+
* @returns {Promise<Array>}
|
|
16
|
+
*/
|
|
17
|
+
export async function discoverPageLinks(page, baseOrigin, silenceTracker) {
|
|
18
|
+
try {
|
|
19
|
+
// Discover all links on the page
|
|
20
|
+
const links = await page.locator('a[href]').all();
|
|
21
|
+
const linkData = [];
|
|
22
|
+
|
|
23
|
+
for (const link of links) {
|
|
24
|
+
const href = await link.getAttribute('href');
|
|
25
|
+
if (href) {
|
|
26
|
+
linkData.push({ href });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Filter to same-origin links
|
|
31
|
+
const sameOriginLinks = linkData.filter(link => {
|
|
32
|
+
if (!link.href) return false;
|
|
33
|
+
return !isExternalUrl(link.href, baseOrigin);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return sameOriginLinks;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
silenceTracker.record('link_discovery_error');
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get next page URL from frontier
|
|
45
|
+
*
|
|
46
|
+
* @param {Object} frontier - Page frontier object
|
|
47
|
+
* @returns {string|null}
|
|
48
|
+
*/
|
|
49
|
+
export function getNextPageUrl(frontier) {
|
|
50
|
+
if (!frontier || !frontier.queue || frontier.queue.length === 0) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const nextUrl = frontier.queue[0];
|
|
55
|
+
return nextUrl;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Mark page as visited in frontier
|
|
60
|
+
*
|
|
61
|
+
* @param {Object} frontier - Page frontier object
|
|
62
|
+
* @param {string} url - URL that was visited
|
|
63
|
+
*/
|
|
64
|
+
export function markPageVisited(frontier, url) {
|
|
65
|
+
if (!frontier) return;
|
|
66
|
+
|
|
67
|
+
// Remove from queue
|
|
68
|
+
if (frontier.queue && frontier.queue.length > 0) {
|
|
69
|
+
frontier.queue = frontier.queue.filter(u => u !== url);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Add to visited
|
|
73
|
+
if (!frontier.visited) {
|
|
74
|
+
frontier.visited = [];
|
|
75
|
+
}
|
|
76
|
+
if (!frontier.visited.includes(url)) {
|
|
77
|
+
frontier.visited.push(url);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Add discovered links to frontier queue
|
|
83
|
+
*
|
|
84
|
+
* @param {Object} frontier - Page frontier object
|
|
85
|
+
* @param {Array} links - Links to add
|
|
86
|
+
*/
|
|
87
|
+
export function addLinksToFrontier(frontier, links) {
|
|
88
|
+
if (!frontier || !links || links.length === 0) return;
|
|
89
|
+
|
|
90
|
+
if (!frontier.queue) {
|
|
91
|
+
frontier.queue = [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const link of links) {
|
|
95
|
+
if (link.href && !frontier.queue.includes(link.href) &&
|
|
96
|
+
(!frontier.visited || !frontier.visited.includes(link.href))) {
|
|
97
|
+
frontier.queue.push(link.href);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if page limit has been reached
|
|
104
|
+
*
|
|
105
|
+
* @param {number} pagesVisited - Number of pages visited
|
|
106
|
+
* @param {number} pageLimit - Maximum pages to visit
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
export function isPageLimitReached(pagesVisited, pageLimit) {
|
|
110
|
+
return pagesVisited >= pageLimit;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Cap frontier to maximum size
|
|
115
|
+
*
|
|
116
|
+
* @param {Object} frontier - Page frontier object
|
|
117
|
+
* @param {number} maxFrontierSize - Maximum frontier queue size
|
|
118
|
+
*/
|
|
119
|
+
export function capFrontier(frontier, maxFrontierSize) {
|
|
120
|
+
if (!frontier || !frontier.queue) return;
|
|
121
|
+
|
|
122
|
+
if (frontier.queue.length > maxFrontierSize) {
|
|
123
|
+
frontier.queue = frontier.queue.slice(0, maxFrontierSize);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Initialize frontier for traversal
|
|
129
|
+
*
|
|
130
|
+
* @param {string} baseUrl - Starting URL
|
|
131
|
+
* @returns {Object}
|
|
132
|
+
*/
|
|
133
|
+
export function initializeFrontier(baseUrl) {
|
|
134
|
+
return {
|
|
135
|
+
queue: [baseUrl],
|
|
136
|
+
visited: []
|
|
137
|
+
};
|
|
138
|
+
}
|