@veraxhq/verax 0.2.1 → 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 +10 -6
- package/bin/verax.js +11 -11
- package/package.json +29 -8
- package/src/cli/commands/baseline.js +103 -0
- package/src/cli/commands/default.js +51 -6
- package/src/cli/commands/doctor.js +29 -0
- package/src/cli/commands/ga.js +246 -0
- package/src/cli/commands/gates.js +95 -0
- package/src/cli/commands/inspect.js +4 -2
- package/src/cli/commands/release-check.js +215 -0
- package/src/cli/commands/run.js +45 -6
- package/src/cli/commands/security-check.js +212 -0
- package/src/cli/commands/truth.js +113 -0
- package/src/cli/entry.js +30 -20
- 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 +544 -0
- package/src/cli/util/ast-network-detector.js +603 -0
- package/src/cli/util/ast-promise-extractor.js +581 -0
- package/src/cli/util/ast-usestate-detector.js +602 -0
- package/src/cli/util/atomic-write.js +12 -1
- package/src/cli/util/bootstrap-guard.js +86 -0
- 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 +124 -0
- package/src/cli/util/determinism-writer.js +129 -0
- package/src/cli/util/digest-engine.js +359 -0
- package/src/cli/util/dom-diff.js +226 -0
- package/src/cli/util/evidence-engine.js +287 -0
- package/src/cli/util/expectation-extractor.js +151 -5
- package/src/cli/util/findings-writer.js +3 -0
- 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 -0
- 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 -0
- package/src/cli/util/project-discovery.js +284 -0
- package/src/cli/util/project-writer.js +2 -0
- package/src/cli/util/run-id.js +23 -27
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/run-result.js +778 -0
- package/src/cli/util/selector-resolver.js +235 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +2 -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 +146 -0
- package/src/cli/util/svelte-state-detector.js +242 -0
- 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 +178 -0
- package/src/cli/util/vue-sfc-extractor.js +161 -0
- package/src/cli/util/vue-state-detector.js +215 -0
- 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/init.js +4 -18
- package/src/verax/core/action-classifier.js +4 -3
- package/src/verax/core/artifacts/registry.js +139 -0
- package/src/verax/core/artifacts/verifier.js +990 -0
- package/src/verax/core/baseline/baseline.enforcer.js +137 -0
- package/src/verax/core/baseline/baseline.snapshot.js +233 -0
- package/src/verax/core/capabilities/gates.js +505 -0
- package/src/verax/core/capabilities/registry.js +475 -0
- package/src/verax/core/confidence/confidence-compute.js +144 -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 +80 -0
- package/src/verax/core/confidence/confidence.schema.js +94 -0
- package/src/verax/core/confidence-engine-refactor.js +489 -0
- package/src/verax/core/confidence-engine.js +625 -0
- package/src/verax/core/contracts/index.js +29 -0
- package/src/verax/core/contracts/types.js +186 -0
- package/src/verax/core/contracts/validators.js +456 -0
- package/src/verax/core/decisions/decision.trace.js +278 -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 +405 -0
- package/src/verax/core/determinism/engine.js +222 -0
- package/src/verax/core/determinism/finding-identity.js +149 -0
- package/src/verax/core/determinism/normalize.js +466 -0
- package/src/verax/core/determinism/report-writer.js +93 -0
- package/src/verax/core/determinism/run-fingerprint.js +123 -0
- package/src/verax/core/dynamic-route-intelligence.js +529 -0
- package/src/verax/core/evidence/evidence-capture-service.js +308 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +166 -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 +192 -0
- package/src/verax/core/failures/exit-codes.js +88 -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 +133 -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 +435 -0
- package/src/verax/core/ga/ga.enforcer.js +87 -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 +84 -0
- package/src/verax/core/guardrails/policy.schema.js +110 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
- package/src/verax/core/guardrails-engine.js +505 -0
- package/src/verax/core/incremental-store.js +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 +318 -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 +200 -0
- package/src/verax/core/pipeline-tracker.js +243 -0
- package/src/verax/core/product-definition.js +127 -0
- package/src/verax/core/release/provenance.builder.js +130 -0
- package/src/verax/core/release/release-report-writer.js +40 -0
- package/src/verax/core/release/release.enforcer.js +164 -0
- package/src/verax/core/release/reproducibility.check.js +222 -0
- package/src/verax/core/release/sbom.builder.js +292 -0
- 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 +195 -0
- package/src/verax/core/report/human-summary.js +362 -0
- package/src/verax/core/route-intelligence.js +420 -0
- 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 +329 -0
- package/src/verax/core/security/security-report.js +50 -0
- package/src/verax/core/security/security.enforcer.js +128 -0
- package/src/verax/core/security/supplychain.defaults.json +38 -0
- package/src/verax/core/security/supplychain.policy.js +334 -0
- package/src/verax/core/security/vuln.scan.js +265 -0
- package/src/verax/core/truth/truth.certificate.js +252 -0
- package/src/verax/core/ui-feedback-intelligence.js +481 -0
- package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
- package/src/verax/detect/confidence-engine.js +62 -34
- package/src/verax/detect/confidence-helper.js +34 -0
- package/src/verax/detect/dynamic-route-findings.js +338 -0
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +2 -2
- package/src/verax/detect/failure-cause-inference.js +293 -0
- package/src/verax/detect/findings-writer.js +131 -35
- 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 +46 -5
- package/src/verax/detect/invariants-enforcer.js +147 -0
- package/src/verax/detect/journey-stall-detector.js +558 -0
- 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 +219 -0
- 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 +207 -0
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/flow/flow-engine.js +2 -1
- package/src/verax/flow/flow-spec.js +0 -6
- package/src/verax/index.js +4 -0
- 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 +3 -0
- 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/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 +51 -155
- package/src/verax/observe/interaction-executor.js +192 -0
- package/src/verax/observe/interaction-runner.js +782 -513
- 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 +205 -0
- package/src/verax/observe/observe-helpers.js +192 -0
- package/src/verax/observe/observe-runner.js +230 -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/page-traversal.js +138 -0
- package/src/verax/observe/snapshot-ops.js +94 -0
- package/src/verax/observe/ui-feedback-detector.js +742 -0
- package/src/verax/scan-summary-writer.js +2 -0
- package/src/verax/shared/artifact-manager.js +25 -5
- package/src/verax/shared/caching.js +1 -0
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/expectation-tracker.js +1 -0
- package/src/verax/shared/view-switch-rules.js +208 -0
- package/src/verax/shared/zip-artifacts.js +6 -0
- 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
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 21.3 — Observe Context Contract
|
|
3
|
+
*
|
|
4
|
+
* HARD CONTRACT: Defines the interface between observe-runner and observers
|
|
5
|
+
*
|
|
6
|
+
* RULES:
|
|
7
|
+
* - Observers MUST only access fields defined in this contract
|
|
8
|
+
* - Observers MUST NOT import from outside observe/*
|
|
9
|
+
* - Observers MUST NOT read files
|
|
10
|
+
* - Observers MUST NOT write artifacts directly
|
|
11
|
+
* - Observers MUST NOT mutate global state
|
|
12
|
+
* - Observers MUST propagate all errors (no silent catches)
|
|
13
|
+
*
|
|
14
|
+
* Runtime invariant checks enforce these rules.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ObserveContext — The context passed to all observers
|
|
19
|
+
*
|
|
20
|
+
* @typedef {Object} ObserveContext
|
|
21
|
+
* @property {import('playwright').Page} page - Playwright page instance
|
|
22
|
+
* @property {string} baseOrigin - Base origin for same-origin checks
|
|
23
|
+
* @property {Object} scanBudget - Scan budget configuration
|
|
24
|
+
* @property {number} startTime - Start time of the scan (timestamp)
|
|
25
|
+
* @property {Object} frontier - PageFrontier instance
|
|
26
|
+
* @property {Object|null} manifest - Manifest object (if available)
|
|
27
|
+
* @property {Object|null} expectationResults - Expectation execution results
|
|
28
|
+
* @property {boolean} incrementalMode - Whether incremental mode is enabled
|
|
29
|
+
* @property {Object|null} oldSnapshot - Previous snapshot (if available)
|
|
30
|
+
* @property {Object|null} snapshotDiff - Snapshot diff (if available)
|
|
31
|
+
* @property {string} currentUrl - Current page URL
|
|
32
|
+
* @property {string} screenshotsDir - Directory for screenshots
|
|
33
|
+
* @property {number} timestamp - Timestamp for this observation
|
|
34
|
+
* @property {Object} decisionRecorder - DecisionRecorder instance
|
|
35
|
+
* @property {Object} silenceTracker - SilenceTracker instance
|
|
36
|
+
* @property {Object} safetyFlags - Safety flags { allowWrites, allowRiskyActions, allowCrossOrigin }
|
|
37
|
+
* @property {Object} routeBudget - Route-specific budget (computed)
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* RunState — Mutable state passed between observers
|
|
42
|
+
*
|
|
43
|
+
* @typedef {Object} RunState
|
|
44
|
+
* @property {Array} traces - Array of interaction traces
|
|
45
|
+
* @property {Array} skippedInteractions - Array of skipped interactions
|
|
46
|
+
* @property {Array} observedExpectations - Array of observed expectations
|
|
47
|
+
* @property {number} totalInteractionsDiscovered - Total interactions discovered
|
|
48
|
+
* @property {number} totalInteractionsExecuted - Total interactions executed
|
|
49
|
+
* @property {Array} remainingInteractionsGaps - Remaining interaction gaps
|
|
50
|
+
* @property {boolean} navigatedToNewPage - Whether navigation occurred
|
|
51
|
+
* @property {string|null} navigatedPageUrl - URL of navigated page (if any)
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Observation — Result returned by an observer
|
|
56
|
+
*
|
|
57
|
+
* @typedef {Object} Observation
|
|
58
|
+
* @property {string} type - Type of observation (e.g., 'network_idle', 'console_error', 'ui_feedback')
|
|
59
|
+
* @property {string} scope - Scope of observation (e.g., 'page', 'interaction', 'navigation')
|
|
60
|
+
* @property {Object} data - Observation data
|
|
61
|
+
* @property {number} timestamp - Timestamp of observation
|
|
62
|
+
* @property {string} [url] - URL where observation occurred
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Forbidden imports that observers MUST NOT use
|
|
67
|
+
*/
|
|
68
|
+
const _FORBIDDEN_IMPORTS = [
|
|
69
|
+
'fs',
|
|
70
|
+
'path',
|
|
71
|
+
'../core/determinism/report-writer',
|
|
72
|
+
'../core/scan-summary-writer',
|
|
73
|
+
'./traces-writer',
|
|
74
|
+
'./expectation-executor'
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Forbidden context fields that observers MUST NOT access
|
|
79
|
+
*/
|
|
80
|
+
const FORBIDDEN_CONTEXT_FIELDS = [
|
|
81
|
+
'projectDir',
|
|
82
|
+
'runId',
|
|
83
|
+
'writeFileSync',
|
|
84
|
+
'readFileSync',
|
|
85
|
+
'mkdirSync'
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validate that an observer result is a valid Observation
|
|
90
|
+
*
|
|
91
|
+
* @param {Object} observation - Observation to validate
|
|
92
|
+
* @param {string} observerName - Name of observer for error messages
|
|
93
|
+
* @throws {Error} If observation is invalid
|
|
94
|
+
*/
|
|
95
|
+
export function validateObservation(observation, observerName) {
|
|
96
|
+
if (!observation) {
|
|
97
|
+
throw new Error(`${observerName}: Observer returned null/undefined observation`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (typeof observation !== 'object') {
|
|
101
|
+
throw new Error(`${observerName}: Observer returned non-object observation: ${typeof observation}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!observation.type || typeof observation.type !== 'string') {
|
|
105
|
+
throw new Error(`${observerName}: Observation missing or invalid 'type' field`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!observation.scope || typeof observation.scope !== 'string') {
|
|
109
|
+
throw new Error(`${observerName}: Observation missing or invalid 'scope' field`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!observation.data || typeof observation.data !== 'object') {
|
|
113
|
+
throw new Error(`${observerName}: Observation missing or invalid 'data' field`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (typeof observation.timestamp !== 'number') {
|
|
117
|
+
throw new Error(`${observerName}: Observation missing or invalid 'timestamp' field`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Validate that context contains only allowed fields
|
|
123
|
+
*
|
|
124
|
+
* @param {Object} context - Context to validate
|
|
125
|
+
* @throws {Error} If context contains forbidden fields
|
|
126
|
+
*/
|
|
127
|
+
export function validateContext(context) {
|
|
128
|
+
for (const field of FORBIDDEN_CONTEXT_FIELDS) {
|
|
129
|
+
if (field in context) {
|
|
130
|
+
throw new Error(`ObserveContext contains forbidden field: ${field}. Observers must not access file I/O or project directories.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create a safe context for observers (removes forbidden fields)
|
|
137
|
+
*
|
|
138
|
+
* @param {Object} rawContext - Raw context from observe-runner
|
|
139
|
+
* @returns {ObserveContext} Safe context for observers
|
|
140
|
+
*/
|
|
141
|
+
export function createObserveContext(rawContext) {
|
|
142
|
+
const {
|
|
143
|
+
page,
|
|
144
|
+
baseOrigin,
|
|
145
|
+
scanBudget,
|
|
146
|
+
startTime,
|
|
147
|
+
frontier,
|
|
148
|
+
manifest,
|
|
149
|
+
expectationResults,
|
|
150
|
+
incrementalMode,
|
|
151
|
+
oldSnapshot,
|
|
152
|
+
snapshotDiff,
|
|
153
|
+
currentUrl,
|
|
154
|
+
screenshotsDir,
|
|
155
|
+
timestamp,
|
|
156
|
+
decisionRecorder,
|
|
157
|
+
silenceTracker,
|
|
158
|
+
safetyFlags,
|
|
159
|
+
routeBudget
|
|
160
|
+
} = rawContext;
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
page,
|
|
164
|
+
baseOrigin,
|
|
165
|
+
scanBudget,
|
|
166
|
+
startTime,
|
|
167
|
+
frontier,
|
|
168
|
+
manifest,
|
|
169
|
+
expectationResults,
|
|
170
|
+
incrementalMode,
|
|
171
|
+
oldSnapshot,
|
|
172
|
+
snapshotDiff,
|
|
173
|
+
currentUrl,
|
|
174
|
+
screenshotsDir,
|
|
175
|
+
timestamp,
|
|
176
|
+
decisionRecorder,
|
|
177
|
+
silenceTracker,
|
|
178
|
+
safetyFlags: safetyFlags || { allowWrites: false, allowRiskyActions: false, allowCrossOrigin: false },
|
|
179
|
+
routeBudget
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Observer execution order (FIXED - must not change)
|
|
185
|
+
*
|
|
186
|
+
* This order is critical for determinism and correctness.
|
|
187
|
+
*/
|
|
188
|
+
export const OBSERVER_ORDER = [
|
|
189
|
+
'navigation-observer', // 1. Navigation decisions first
|
|
190
|
+
'budget-observer', // 2. Budget checks before interactions
|
|
191
|
+
'interaction-observer', // 3. Interaction discovery and execution
|
|
192
|
+
'network-observer', // 4. Network state observation
|
|
193
|
+
'ui-feedback-observer', // 5. UI state observation
|
|
194
|
+
'console-observer', // 6. Console error observation
|
|
195
|
+
'coverage-observer' // 7. Coverage gap tracking
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get observer execution order
|
|
200
|
+
*
|
|
201
|
+
* @returns {Array<string>} Ordered list of observer names
|
|
202
|
+
*/
|
|
203
|
+
export function getObserverOrder() {
|
|
204
|
+
return [...OBSERVER_ORDER];
|
|
205
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 21.3 — Observe Helpers
|
|
3
|
+
*
|
|
4
|
+
* Helper functions extracted from observe/index.js to keep it slim
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { resolve } from 'path';
|
|
8
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
9
|
+
import { writeTraces } from './traces-writer.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Setup manifest and expectations
|
|
13
|
+
*/
|
|
14
|
+
export async function setupManifestAndExpectations(manifestPath, projectDir, page, url, screenshotsDir, scanBudget, startTime, silenceTracker) {
|
|
15
|
+
const { readFileSync, existsSync } = await import('fs');
|
|
16
|
+
const { loadPreviousSnapshot, saveSnapshot: _saveSnapshot, buildSnapshot, compareSnapshots } = await import('../core/incremental-store.js');
|
|
17
|
+
const { executeProvenExpectations } = await import('./expectation-executor.js');
|
|
18
|
+
const { isProvenExpectation } = await import('../shared/expectation-prover.js');
|
|
19
|
+
|
|
20
|
+
let manifest = null;
|
|
21
|
+
let expectationResults = null;
|
|
22
|
+
let expectationCoverageGaps = [];
|
|
23
|
+
let incrementalMode = false;
|
|
24
|
+
let snapshotDiff = null;
|
|
25
|
+
let oldSnapshot = null;
|
|
26
|
+
|
|
27
|
+
if (manifestPath && existsSync(manifestPath)) {
|
|
28
|
+
try {
|
|
29
|
+
const manifestContent = readFileSync(manifestPath, 'utf-8');
|
|
30
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
31
|
+
manifest = JSON.parse(manifestContent);
|
|
32
|
+
|
|
33
|
+
oldSnapshot = loadPreviousSnapshot(projectDir);
|
|
34
|
+
if (oldSnapshot) {
|
|
35
|
+
const currentSnapshot = buildSnapshot(manifest, []);
|
|
36
|
+
snapshotDiff = compareSnapshots(oldSnapshot, currentSnapshot);
|
|
37
|
+
incrementalMode = !snapshotDiff.hasChanges;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const provenCount = (manifest.staticExpectations || []).filter(exp => isProvenExpectation(exp)).length;
|
|
41
|
+
if (provenCount > 0) {
|
|
42
|
+
expectationResults = await executeProvenExpectations(page, manifest, url, screenshotsDir, scanBudget, startTime, projectDir);
|
|
43
|
+
expectationCoverageGaps = expectationResults.coverageGaps || [];
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
silenceTracker.record({
|
|
47
|
+
scope: 'discovery',
|
|
48
|
+
reason: 'discovery_error',
|
|
49
|
+
description: 'Manifest load or expectation execution failed',
|
|
50
|
+
context: { error: err?.message },
|
|
51
|
+
impact: 'incomplete_check'
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { manifest, expectationResults, expectationCoverageGaps, incrementalMode, snapshotDiff, oldSnapshot };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Process traversal results and build observation
|
|
61
|
+
*/
|
|
62
|
+
export async function processTraversalResults(traversalResult, expectationResults, expectationCoverageGaps, remainingInteractionsGaps, frontier, scanBudget, page, url, finalTraces, finalSkippedInteractions, finalObservedExpectations, silenceTracker, manifest, incrementalMode, snapshotDiff, projectDir, runId) {
|
|
63
|
+
// Combine all coverage gaps
|
|
64
|
+
const allCoverageGaps = [...expectationCoverageGaps];
|
|
65
|
+
if (remainingInteractionsGaps.length > 0) {
|
|
66
|
+
allCoverageGaps.push(...remainingInteractionsGaps.map(gap => ({
|
|
67
|
+
expectationId: null,
|
|
68
|
+
type: gap.interaction.type,
|
|
69
|
+
reason: gap.reason,
|
|
70
|
+
fromPath: gap.url,
|
|
71
|
+
source: null,
|
|
72
|
+
evidence: { interaction: gap.interaction }
|
|
73
|
+
})));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (frontier.frontierCapped) {
|
|
77
|
+
allCoverageGaps.push({
|
|
78
|
+
expectationId: null,
|
|
79
|
+
type: 'navigation',
|
|
80
|
+
reason: 'frontier_capped',
|
|
81
|
+
fromPath: page.url(),
|
|
82
|
+
source: null,
|
|
83
|
+
evidence: { message: `Frontier capped at ${scanBudget.maxUniqueUrls || 'unlimited'} unique URLs` }
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Build coverage object
|
|
88
|
+
const coverage = {
|
|
89
|
+
candidatesDiscovered: traversalResult.totalInteractionsDiscovered,
|
|
90
|
+
candidatesSelected: traversalResult.totalInteractionsExecuted,
|
|
91
|
+
cap: scanBudget.maxTotalInteractions,
|
|
92
|
+
capped: traversalResult.totalInteractionsExecuted >= scanBudget.maxTotalInteractions || remainingInteractionsGaps.length > 0,
|
|
93
|
+
pagesVisited: frontier.pagesVisited,
|
|
94
|
+
pagesDiscovered: frontier.pagesDiscovered,
|
|
95
|
+
skippedInteractions: finalSkippedInteractions.length,
|
|
96
|
+
interactionsDiscovered: traversalResult.totalInteractionsDiscovered,
|
|
97
|
+
interactionsExecuted: traversalResult.totalInteractionsExecuted
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Build warnings
|
|
101
|
+
const observeWarnings = [];
|
|
102
|
+
if (coverage.capped) {
|
|
103
|
+
observeWarnings.push({
|
|
104
|
+
code: 'INTERACTIONS_CAPPED',
|
|
105
|
+
message: `Interaction execution capped. Visited ${coverage.pagesVisited} pages, discovered ${coverage.pagesDiscovered}, executed ${coverage.candidatesSelected} of ${coverage.candidatesDiscovered} interactions. Coverage incomplete.`
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (finalSkippedInteractions.length > 0) {
|
|
109
|
+
observeWarnings.push({
|
|
110
|
+
code: 'INTERACTIONS_SKIPPED',
|
|
111
|
+
message: `Skipped ${finalSkippedInteractions.length} dangerous interactions`,
|
|
112
|
+
details: finalSkippedInteractions
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Append expectation traces
|
|
117
|
+
if (expectationResults?.results) {
|
|
118
|
+
for (const result of expectationResults.results) {
|
|
119
|
+
if (result.trace) {
|
|
120
|
+
result.trace.expectationDriven = true;
|
|
121
|
+
result.trace.expectationId = result.expectationId;
|
|
122
|
+
result.trace.expectationOutcome = result.outcome;
|
|
123
|
+
finalTraces.push(result.trace);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Write traces
|
|
129
|
+
const observation = writeTraces(projectDir, url, finalTraces, coverage, observeWarnings, finalObservedExpectations, silenceTracker, runId);
|
|
130
|
+
observation.silences = silenceTracker.getDetailedSummary();
|
|
131
|
+
|
|
132
|
+
// Add expectation execution results
|
|
133
|
+
if (expectationResults) {
|
|
134
|
+
observation.expectationExecution = {
|
|
135
|
+
totalProvenExpectations: expectationResults.totalProvenExpectations,
|
|
136
|
+
executedCount: expectationResults.executedCount,
|
|
137
|
+
coverageGapsCount: allCoverageGaps.length,
|
|
138
|
+
results: expectationResults.results.map(r => ({
|
|
139
|
+
expectationId: r.expectationId,
|
|
140
|
+
type: r.type,
|
|
141
|
+
fromPath: r.fromPath,
|
|
142
|
+
outcome: r.outcome,
|
|
143
|
+
reason: r.reason
|
|
144
|
+
}))
|
|
145
|
+
};
|
|
146
|
+
observation.expectationCoverageGaps = allCoverageGaps;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Add incremental mode metadata
|
|
150
|
+
if (manifest) {
|
|
151
|
+
const { buildSnapshot, saveSnapshot } = await import('../core/incremental-store.js');
|
|
152
|
+
const observedInteractions = finalTraces
|
|
153
|
+
.filter(t => t.interaction && !t.incremental)
|
|
154
|
+
.map(t => ({
|
|
155
|
+
type: t.interaction?.type,
|
|
156
|
+
selector: t.interaction?.selector,
|
|
157
|
+
url: t.before?.url || url
|
|
158
|
+
}));
|
|
159
|
+
|
|
160
|
+
const currentSnapshot = buildSnapshot(manifest, observedInteractions);
|
|
161
|
+
saveSnapshot(projectDir, currentSnapshot, runId);
|
|
162
|
+
|
|
163
|
+
observation.incremental = {
|
|
164
|
+
enabled: incrementalMode,
|
|
165
|
+
snapshotDiff: snapshotDiff,
|
|
166
|
+
skippedInteractionsCount: finalSkippedInteractions.filter(s => s.reason === 'incremental_unchanged').length
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return observation;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Write determinism artifacts
|
|
175
|
+
*/
|
|
176
|
+
export async function writeDeterminismArtifacts(projectDir, runId, decisionRecorder) {
|
|
177
|
+
// PHASE 25: Write determinism contract
|
|
178
|
+
const { writeDeterminismContract } = await import('../core/determinism/contract-writer.js');
|
|
179
|
+
const { getRunArtifactDir } = await import('../core/run-id.js');
|
|
180
|
+
const runDir = getRunArtifactDir(projectDir, runId);
|
|
181
|
+
writeDeterminismContract(runDir, decisionRecorder);
|
|
182
|
+
if (!runId || !projectDir) return;
|
|
183
|
+
|
|
184
|
+
const runsDir = resolve(projectDir, '.verax', 'runs', runId);
|
|
185
|
+
mkdirSync(runsDir, { recursive: true });
|
|
186
|
+
const decisionsPath = resolve(runsDir, 'decisions.json');
|
|
187
|
+
writeFileSync(decisionsPath, JSON.stringify(decisionRecorder.export(), null, 2), 'utf-8');
|
|
188
|
+
|
|
189
|
+
const { writeDeterminismReport } = await import('../core/determinism/report-writer.js');
|
|
190
|
+
writeDeterminismReport(runsDir, decisionRecorder);
|
|
191
|
+
}
|
|
192
|
+
|