@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,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EXPECTATION HANDLER
|
|
3
|
+
*
|
|
4
|
+
* Manages manifest loading, snapshot comparison, and proven expectation execution.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync as readFileSyncWithEncoding, existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Load and execute proven expectations from manifest
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} page - Playwright page
|
|
14
|
+
* @param {string} manifestPath - Path to manifest file
|
|
15
|
+
* @param {string} projectDir - Project directory
|
|
16
|
+
* @param {Object} silenceTracker - Silence tracker
|
|
17
|
+
* @returns {Promise<{success: boolean, results: Array|null}>}
|
|
18
|
+
*/
|
|
19
|
+
export async function loadAndExecuteProvenExpectations(page, manifestPath, projectDir, silenceTracker) {
|
|
20
|
+
try {
|
|
21
|
+
if (!existsSync(manifestPath)) {
|
|
22
|
+
silenceTracker.record('expectation_manifest_not_found');
|
|
23
|
+
return { success: false, results: null };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const manifestContent = readFileSyncWithEncoding(manifestPath, 'utf8');
|
|
27
|
+
const manifest = JSON.parse(typeof manifestContent === 'string' ? manifestContent : manifestContent.toString());
|
|
28
|
+
|
|
29
|
+
if (!manifest.expectations || !Array.isArray(manifest.expectations)) {
|
|
30
|
+
silenceTracker.record('expectation_manifest_invalid_format');
|
|
31
|
+
return { success: false, results: null };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Return empty results for now (expectation execution handled in main observe function)
|
|
35
|
+
return { success: true, results: [] };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
silenceTracker.record('expectation_manifest_load_error');
|
|
38
|
+
return { success: false, results: null };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load and compare snapshot with previous observation
|
|
44
|
+
*
|
|
45
|
+
* @param {string} projectDir - Project directory
|
|
46
|
+
* @param {Object} silenceTracker - Silence tracker
|
|
47
|
+
* @returns {Promise<{currentSnapshot: Object|null, previousSnapshot: Object|null}>}
|
|
48
|
+
*/
|
|
49
|
+
export async function loadAndCompareSnapshot(projectDir, silenceTracker) {
|
|
50
|
+
try {
|
|
51
|
+
const snapshotDir = join(projectDir, '.verax', 'snapshots');
|
|
52
|
+
|
|
53
|
+
if (!existsSync(snapshotDir)) {
|
|
54
|
+
mkdirSync(snapshotDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Load previous snapshot if exists
|
|
58
|
+
const previousSnapshotPath = join(snapshotDir, 'previous.json');
|
|
59
|
+
let previousSnapshot = null;
|
|
60
|
+
if (existsSync(previousSnapshotPath)) {
|
|
61
|
+
try {
|
|
62
|
+
const content = readFileSyncWithEncoding(previousSnapshotPath, 'utf8');
|
|
63
|
+
previousSnapshot = JSON.parse(typeof content === 'string' ? content : content.toString());
|
|
64
|
+
} catch {
|
|
65
|
+
silenceTracker.record('snapshot_previous_load_error');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Load current snapshot if exists
|
|
70
|
+
const currentSnapshotPath = join(snapshotDir, 'current.json');
|
|
71
|
+
let currentSnapshot = null;
|
|
72
|
+
if (existsSync(currentSnapshotPath)) {
|
|
73
|
+
try {
|
|
74
|
+
const content = readFileSyncWithEncoding(currentSnapshotPath, 'utf8');
|
|
75
|
+
currentSnapshot = JSON.parse(typeof content === 'string' ? content : content.toString());
|
|
76
|
+
} catch {
|
|
77
|
+
silenceTracker.record('snapshot_current_load_error');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { currentSnapshot, previousSnapshot };
|
|
82
|
+
} catch (error) {
|
|
83
|
+
silenceTracker.record('snapshot_comparison_error');
|
|
84
|
+
return { currentSnapshot: null, previousSnapshot: null };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build snapshot object from observation data
|
|
90
|
+
*
|
|
91
|
+
* @param {Array} traces - Array of interaction traces
|
|
92
|
+
* @param {string} baseOrigin - Base origin URL
|
|
93
|
+
* @returns {Object}
|
|
94
|
+
*/
|
|
95
|
+
export function buildSnapshot(traces, baseOrigin) {
|
|
96
|
+
const snapshot = {
|
|
97
|
+
timestamp: new Date().toISOString(),
|
|
98
|
+
baseOrigin,
|
|
99
|
+
totalTraces: traces.length,
|
|
100
|
+
verifiedExpectations: traces.filter(t => t.expectationDriven).length,
|
|
101
|
+
observedExpectations: traces.filter(t => t.observedExpectation).length,
|
|
102
|
+
unprovable: traces.filter(t => t.unprovenResult).length
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return snapshot;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Save snapshot for future comparison
|
|
110
|
+
*
|
|
111
|
+
* @param {Object} snapshot - Snapshot object
|
|
112
|
+
* @param {string} projectDir - Project directory
|
|
113
|
+
*/
|
|
114
|
+
export function saveSnapshot(snapshot, projectDir) {
|
|
115
|
+
try {
|
|
116
|
+
const snapshotDir = join(projectDir, '.verax', 'snapshots');
|
|
117
|
+
if (!existsSync(snapshotDir)) {
|
|
118
|
+
mkdirSync(snapshotDir, { recursive: true });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const snapshotPath = join(snapshotDir, 'current.json');
|
|
122
|
+
writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2));
|
|
123
|
+
} catch (error) {
|
|
124
|
+
// Silently fail on snapshot save
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental Skip Phantom Trace Builder
|
|
3
|
+
*
|
|
4
|
+
* Extracted from observe/index.js (STAGE D2.4)
|
|
5
|
+
*
|
|
6
|
+
* Builds the minimal phantom trace object that represents a skipped
|
|
7
|
+
* interaction in incremental mode. The phantom trace preserves interaction
|
|
8
|
+
* metadata but has no execution result (before/after URLs are identical).
|
|
9
|
+
*
|
|
10
|
+
* Preserves 100% of original behavior:
|
|
11
|
+
* - Same 5 top-level properties
|
|
12
|
+
* - Same nested object shapes
|
|
13
|
+
* - incremental flag always true
|
|
14
|
+
* - resultType always 'INCREMENTAL_SKIP'
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build a phantom trace for an incremental skip.
|
|
19
|
+
*
|
|
20
|
+
* When an interaction is skipped in incremental mode (because it was
|
|
21
|
+
* unchanged from the previous run), a phantom trace is created to:
|
|
22
|
+
* 1. Preserve the interaction metadata for the observation record
|
|
23
|
+
* 2. Mark the trace as incremental (via incremental: true) for filtering
|
|
24
|
+
* 3. Enable transparent output (trace appears in JSON but marked as skipped)
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} params - Function parameters
|
|
27
|
+
* @param {Object} params.interaction - The interaction that was skipped
|
|
28
|
+
* @param {string} params.interaction.type - Interaction type (e.g., 'click')
|
|
29
|
+
* @param {string} params.interaction.selector - CSS selector
|
|
30
|
+
* @param {string} params.interaction.label - Human-readable label
|
|
31
|
+
* @param {string} params.currentUrl - Current page URL (used for before/after)
|
|
32
|
+
* @returns {Object} Phantom trace object with incremental flag set
|
|
33
|
+
*/
|
|
34
|
+
export function buildIncrementalPhantomTrace({ interaction, currentUrl }) {
|
|
35
|
+
return {
|
|
36
|
+
interaction: {
|
|
37
|
+
type: interaction.type,
|
|
38
|
+
selector: interaction.selector,
|
|
39
|
+
label: interaction.label
|
|
40
|
+
},
|
|
41
|
+
before: { url: currentUrl },
|
|
42
|
+
after: { url: currentUrl },
|
|
43
|
+
incremental: true,
|
|
44
|
+
resultType: 'INCREMENTAL_SKIP'
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -7,13 +7,25 @@ import { runInteraction } from './interaction-runner.js';
|
|
|
7
7
|
import { writeTraces } from './traces-writer.js';
|
|
8
8
|
import { getBaseOrigin, isExternalUrl } from './domain-boundary.js';
|
|
9
9
|
|
|
10
|
+
// STAGE D1: Extracted modules for observe orchestration
|
|
11
|
+
import { setupNetworkFirewall } from './network-firewall.js';
|
|
12
|
+
|
|
13
|
+
// STAGE D2.1: Extracted modules for snapshot operations
|
|
14
|
+
import { initializeSnapshot, finalizeSnapshot } from './snapshot-ops.js';
|
|
15
|
+
|
|
16
|
+
// STAGE D2.2: Extracted modules for coverage gap accumulation and warnings
|
|
17
|
+
import { accumulateCoverageGaps, buildCoverageObject, generateCoverageWarnings } from './coverage-gaps.js';
|
|
18
|
+
|
|
19
|
+
// STAGE D2.4: Extracted modules for incremental skip handling
|
|
20
|
+
import { buildIncrementalPhantomTrace } from './incremental-skip.js';
|
|
21
|
+
|
|
10
22
|
import { DEFAULT_SCAN_BUDGET } from '../shared/scan-budget.js';
|
|
11
23
|
import { executeProvenExpectations } from './expectation-executor.js';
|
|
12
24
|
import { isProvenExpectation } from '../shared/expectation-prover.js';
|
|
13
25
|
import { PageFrontier } from './page-frontier.js';
|
|
14
26
|
import { deriveObservedExpectation, shouldAttemptRepeatObservedExpectation, evaluateObservedExpectation } from './observed-expectation.js';
|
|
15
27
|
import { computeRouteBudget } from '../core/budget-engine.js';
|
|
16
|
-
import {
|
|
28
|
+
import { shouldSkipInteractionIncremental } from '../core/incremental-store.js';
|
|
17
29
|
import SilenceTracker from '../core/silence-model.js';
|
|
18
30
|
import { DecisionRecorder, recordBudgetProfile, recordTimeoutConfig, recordTruncation, recordEnvironment } from '../core/determinism-model.js';
|
|
19
31
|
|
|
@@ -32,8 +44,8 @@ import { DecisionRecorder, recordBudgetProfile, recordTimeoutConfig, recordTrunc
|
|
|
32
44
|
* All silence is explicit in output - no silent success.
|
|
33
45
|
*
|
|
34
46
|
* PHASE 4: Safety mode enabled by default
|
|
35
|
-
* - Blocks risky/write actions
|
|
36
|
-
* - Blocks POST/PUT/PATCH/DELETE
|
|
47
|
+
* - Blocks risky/write actions unconditionally
|
|
48
|
+
* - Blocks POST/PUT/PATCH/DELETE (read-only mode enforced)
|
|
37
49
|
* - Blocks cross-origin unless --allow-cross-origin
|
|
38
50
|
*
|
|
39
51
|
* PHASE 5: Deterministic artifact paths
|
|
@@ -63,72 +75,14 @@ export async function observe(url, manifestPath = null, scanBudgetOverride = nul
|
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
// Phase 4: Extract safety flags
|
|
66
|
-
const {
|
|
78
|
+
const { allowRiskyActions = false, allowCrossOrigin = false } = safetyFlags;
|
|
67
79
|
let blockedNetworkWrites = [];
|
|
68
80
|
let blockedCrossOrigin = [];
|
|
69
81
|
|
|
70
|
-
// Phase 4: Setup network interception firewall
|
|
71
|
-
await page
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const requestUrl = request.url();
|
|
75
|
-
const resourceType = request.resourceType();
|
|
76
|
-
|
|
77
|
-
// Check cross-origin blocking (skip for file:// URLs)
|
|
78
|
-
if (!allowCrossOrigin && !requestUrl.startsWith('file://')) {
|
|
79
|
-
try {
|
|
80
|
-
const reqOrigin = new URL(requestUrl).origin;
|
|
81
|
-
if (reqOrigin !== baseOrigin) {
|
|
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
|
-
});
|
|
82
|
+
// Phase 4: Setup network interception firewall (STAGE D1: moved to module)
|
|
83
|
+
const firewallResult = await setupNetworkFirewall(page, baseOrigin, allowCrossOrigin, silenceTracker);
|
|
84
|
+
blockedNetworkWrites = firewallResult.blockedNetworkWrites;
|
|
85
|
+
blockedCrossOrigin = firewallResult.blockedCrossOrigin;
|
|
132
86
|
|
|
133
87
|
try {
|
|
134
88
|
await navigateToUrl(page, url, scanBudget);
|
|
@@ -156,15 +110,14 @@ export async function observe(url, manifestPath = null, scanBudgetOverride = nul
|
|
|
156
110
|
if (manifestPath && existsSync(manifestPath)) {
|
|
157
111
|
try {
|
|
158
112
|
const manifestContent = readFileSync(manifestPath, 'utf-8');
|
|
113
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
159
114
|
manifest = JSON.parse(manifestContent);
|
|
160
115
|
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
incrementalMode = !snapshotDiff.hasChanges; // Use incremental if nothing changed
|
|
167
|
-
}
|
|
116
|
+
// STAGE D2.1: Snapshot initialization (moved to snapshot-ops module)
|
|
117
|
+
const snapshotResult = await initializeSnapshot(projectDir, manifest);
|
|
118
|
+
oldSnapshot = snapshotResult.oldSnapshot;
|
|
119
|
+
snapshotDiff = snapshotResult.snapshotDiff;
|
|
120
|
+
incrementalMode = snapshotResult.incrementalMode;
|
|
168
121
|
|
|
169
122
|
const provenCount = (manifest.staticExpectations || []).filter(exp => isProvenExpectation(exp)).length;
|
|
170
123
|
if (provenCount > 0) {
|
|
@@ -406,17 +359,7 @@ export async function observe(url, manifestPath = null, scanBudgetOverride = nul
|
|
|
406
359
|
const shouldSkip = shouldSkipInteractionIncremental(interaction, currentUrl, oldSnapshot, snapshotDiff);
|
|
407
360
|
if (shouldSkip) {
|
|
408
361
|
// 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
|
-
};
|
|
362
|
+
const skippedTrace = buildIncrementalPhantomTrace({ interaction, currentUrl });
|
|
420
363
|
traces.push(skippedTrace);
|
|
421
364
|
|
|
422
365
|
// Track incremental skip as silence
|
|
@@ -494,6 +437,7 @@ export async function observe(url, manifestPath = null, scanBudgetOverride = nul
|
|
|
494
437
|
|
|
495
438
|
// Phase 4: Check action classification and safety mode
|
|
496
439
|
const { shouldBlockAction } = await import('../core/action-classifier.js');
|
|
440
|
+
const allowWrites = allowRiskyActions;
|
|
497
441
|
const blockCheck = shouldBlockAction(interaction, { allowWrites, allowRiskyActions });
|
|
498
442
|
|
|
499
443
|
if (blockCheck.shouldBlock) {
|
|
@@ -745,65 +689,24 @@ export async function observe(url, manifestPath = null, scanBudgetOverride = nul
|
|
|
745
689
|
}
|
|
746
690
|
|
|
747
691
|
// Combine all coverage gaps
|
|
748
|
-
|
|
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
|
-
}
|
|
692
|
+
expectationCoverageGaps.push(...accumulateCoverageGaps(remainingInteractionsGaps, frontier, page.url(), scanBudget));
|
|
774
693
|
|
|
775
694
|
// Build coverage object matching writeTraces expected format
|
|
776
|
-
const coverage =
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
interactionsDiscovered: totalInteractionsDiscovered,
|
|
785
|
-
interactionsExecuted: totalInteractionsExecuted
|
|
786
|
-
};
|
|
695
|
+
const coverage = buildCoverageObject(
|
|
696
|
+
totalInteractionsDiscovered,
|
|
697
|
+
totalInteractionsExecuted,
|
|
698
|
+
scanBudget,
|
|
699
|
+
frontier,
|
|
700
|
+
skippedInteractions,
|
|
701
|
+
remainingInteractionsGaps
|
|
702
|
+
);
|
|
787
703
|
|
|
788
704
|
// Ensure we increment pagesVisited when we navigate via getNextUrl()
|
|
789
705
|
// getNextUrl() marks as visited but doesn't increment counter - we do it here
|
|
790
706
|
// BUT: when alreadyOnPage is true, we've already marked it, so don't double-count
|
|
791
707
|
|
|
792
708
|
// Record warnings
|
|
793
|
-
|
|
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
|
-
}
|
|
709
|
+
observeWarnings.push(...generateCoverageWarnings(coverage, skippedInteractions));
|
|
807
710
|
|
|
808
711
|
// Append expectation traces for completeness
|
|
809
712
|
if (expectationResults && expectationResults.results) {
|
|
@@ -838,26 +741,19 @@ export async function observe(url, manifestPath = null, scanBudgetOverride = nul
|
|
|
838
741
|
observation.expectationCoverageGaps = expectationCoverageGaps;
|
|
839
742
|
}
|
|
840
743
|
|
|
841
|
-
//
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
// Add incremental mode metadata to observation
|
|
856
|
-
observation.incremental = {
|
|
857
|
-
enabled: incrementalMode,
|
|
858
|
-
snapshotDiff: snapshotDiff,
|
|
859
|
-
skippedInteractionsCount: skippedInteractions.filter(s => s.reason === 'incremental_unchanged').length
|
|
860
|
-
};
|
|
744
|
+
// STAGE D2.1: Snapshot finalization (moved to snapshot-ops module)
|
|
745
|
+
const incrementalMetadata = await finalizeSnapshot(
|
|
746
|
+
manifest,
|
|
747
|
+
traces,
|
|
748
|
+
skippedInteractions,
|
|
749
|
+
incrementalMode,
|
|
750
|
+
snapshotDiff,
|
|
751
|
+
projectDir,
|
|
752
|
+
runId,
|
|
753
|
+
url
|
|
754
|
+
);
|
|
755
|
+
if (incrementalMetadata) {
|
|
756
|
+
observation.incremental = incrementalMetadata;
|
|
861
757
|
}
|
|
862
758
|
|
|
863
759
|
await closeBrowser(browser);
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* INTERACTION EXECUTION ENGINE
|
|
3
|
+
*
|
|
4
|
+
* Handles execution of interactions on pages, evidence capture, and tracing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { runInteraction } from './interaction-runner.js';
|
|
8
|
+
import { deriveObservedExpectation, shouldAttemptRepeatObservedExpectation, evaluateObservedExpectation } from './observed-expectation.js';
|
|
9
|
+
import { isExternalUrl } from './domain-boundary.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Execute a single interaction and capture results
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} page - Playwright page
|
|
15
|
+
* @param {Object} interaction - Interaction to execute
|
|
16
|
+
* @param {number} timestamp - Execution timestamp
|
|
17
|
+
* @param {number} interactionIndex - Index in execution sequence
|
|
18
|
+
* @param {string} screenshotsDir - Directory for screenshots
|
|
19
|
+
* @param {string} baseOrigin - Base origin for URL checking
|
|
20
|
+
* @param {number} startTime - Scan start time
|
|
21
|
+
* @param {Object} routeBudget - Route-specific budget
|
|
22
|
+
* @param {Object} expectationResults - Results from proven expectations
|
|
23
|
+
* @param {Object} silenceTracker - Silence tracker
|
|
24
|
+
* @returns {Promise<{trace: Object, totalExecuted: number, navigatedToNewPage: boolean, newPageUrl: string|null}>}
|
|
25
|
+
*/
|
|
26
|
+
export async function executeInteraction(
|
|
27
|
+
page,
|
|
28
|
+
interaction,
|
|
29
|
+
timestamp,
|
|
30
|
+
interactionIndex,
|
|
31
|
+
screenshotsDir,
|
|
32
|
+
baseOrigin,
|
|
33
|
+
startTime,
|
|
34
|
+
routeBudget,
|
|
35
|
+
expectationResults,
|
|
36
|
+
silenceTracker
|
|
37
|
+
) {
|
|
38
|
+
const beforeUrl = page.url();
|
|
39
|
+
|
|
40
|
+
const trace = await runInteraction(
|
|
41
|
+
page,
|
|
42
|
+
interaction,
|
|
43
|
+
timestamp,
|
|
44
|
+
interactionIndex,
|
|
45
|
+
screenshotsDir,
|
|
46
|
+
baseOrigin,
|
|
47
|
+
startTime,
|
|
48
|
+
routeBudget,
|
|
49
|
+
null,
|
|
50
|
+
silenceTracker
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
let totalExecuted = 1;
|
|
54
|
+
|
|
55
|
+
if (trace) {
|
|
56
|
+
// Check if this matched a proven expectation
|
|
57
|
+
const matchingExpectation = expectationResults?.results?.find(
|
|
58
|
+
r => r.trace?.interaction?.selector === trace.interaction.selector
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (matchingExpectation) {
|
|
62
|
+
trace.expectationDriven = true;
|
|
63
|
+
trace.expectationId = matchingExpectation.expectationId;
|
|
64
|
+
trace.expectationOutcome = matchingExpectation.outcome;
|
|
65
|
+
} else {
|
|
66
|
+
// Derive observed expectation from trace
|
|
67
|
+
const observedExpectation = deriveObservedExpectation(interaction, trace, baseOrigin);
|
|
68
|
+
if (observedExpectation) {
|
|
69
|
+
trace.observedExpectation = observedExpectation;
|
|
70
|
+
trace.resultType = 'OBSERVED_EXPECTATION';
|
|
71
|
+
|
|
72
|
+
// Attempt repeat if eligible and budget allows
|
|
73
|
+
const repeatEligible = shouldAttemptRepeatObservedExpectation(observedExpectation, trace);
|
|
74
|
+
const budgetAllowsRepeat = repeatEligible &&
|
|
75
|
+
(Date.now() - startTime) < routeBudget.maxScanDurationMs;
|
|
76
|
+
|
|
77
|
+
if (budgetAllowsRepeat) {
|
|
78
|
+
const repeatIndex = interactionIndex + 1;
|
|
79
|
+
const repeatResult = await repeatObservedInteraction(
|
|
80
|
+
page,
|
|
81
|
+
interaction,
|
|
82
|
+
observedExpectation,
|
|
83
|
+
timestamp,
|
|
84
|
+
repeatIndex,
|
|
85
|
+
screenshotsDir,
|
|
86
|
+
baseOrigin,
|
|
87
|
+
startTime,
|
|
88
|
+
routeBudget
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (repeatResult) {
|
|
92
|
+
const repeatEvaluation = repeatResult.repeatEvaluation;
|
|
93
|
+
trace.observedExpectation.repeatAttempted = true;
|
|
94
|
+
trace.observedExpectation.repeated = repeatEvaluation.outcome === 'VERIFIED';
|
|
95
|
+
trace.observedExpectation.repeatOutcome = repeatEvaluation.outcome;
|
|
96
|
+
trace.observedExpectation.repeatReason = repeatEvaluation.reason;
|
|
97
|
+
|
|
98
|
+
if (repeatEvaluation.outcome === 'OBSERVED_BREAK') {
|
|
99
|
+
trace.observedExpectation.outcome = 'OBSERVED_BREAK';
|
|
100
|
+
trace.observedExpectation.reason = 'inconsistent_on_repeat';
|
|
101
|
+
trace.observedExpectation.confidenceLevel = 'LOW';
|
|
102
|
+
} else if (trace.observedExpectation.repeated && trace.observedExpectation.outcome === 'VERIFIED') {
|
|
103
|
+
trace.observedExpectation.confidenceLevel = 'MEDIUM';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
totalExecuted = 2;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
trace.unprovenResult = true;
|
|
111
|
+
trace.resultType = 'UNPROVEN_RESULT';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check for same-origin navigation
|
|
117
|
+
let navigatedToNewPage = false;
|
|
118
|
+
let newPageUrl = null;
|
|
119
|
+
|
|
120
|
+
if (trace) {
|
|
121
|
+
const afterUrl = trace.after?.url || page.url();
|
|
122
|
+
const navigatedSameOrigin = afterUrl && afterUrl !== beforeUrl && !isExternalUrl(afterUrl, baseOrigin);
|
|
123
|
+
if (navigatedSameOrigin && interaction.type === 'link') {
|
|
124
|
+
navigatedToNewPage = true;
|
|
125
|
+
newPageUrl = afterUrl;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
trace,
|
|
131
|
+
totalExecuted,
|
|
132
|
+
navigatedToNewPage,
|
|
133
|
+
newPageUrl
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Repeat an observed interaction to verify consistency
|
|
139
|
+
*/
|
|
140
|
+
async function repeatObservedInteraction(
|
|
141
|
+
page,
|
|
142
|
+
interaction,
|
|
143
|
+
observedExpectation,
|
|
144
|
+
timestamp,
|
|
145
|
+
interactionIndex,
|
|
146
|
+
screenshotsDir,
|
|
147
|
+
baseOrigin,
|
|
148
|
+
startTime,
|
|
149
|
+
scanBudget
|
|
150
|
+
) {
|
|
151
|
+
const selector = observedExpectation.evidence?.selector || interaction.selector;
|
|
152
|
+
if (!selector) return null;
|
|
153
|
+
|
|
154
|
+
const locator = page.locator(selector).first();
|
|
155
|
+
const count = await locator.count();
|
|
156
|
+
if (count === 0) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const repeatInteraction = {
|
|
161
|
+
...interaction,
|
|
162
|
+
element: locator
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const repeatTrace = await runInteraction(
|
|
166
|
+
page,
|
|
167
|
+
repeatInteraction,
|
|
168
|
+
timestamp,
|
|
169
|
+
interactionIndex,
|
|
170
|
+
screenshotsDir,
|
|
171
|
+
baseOrigin,
|
|
172
|
+
startTime,
|
|
173
|
+
scanBudget,
|
|
174
|
+
null,
|
|
175
|
+
null // No silence tracker for repeat executions
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (!repeatTrace) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
repeatTrace.repeatExecution = true;
|
|
183
|
+
repeatTrace.repeatOfObservedExpectationId = observedExpectation.id;
|
|
184
|
+
repeatTrace.resultType = 'OBSERVED_EXPECTATION_REPEAT';
|
|
185
|
+
|
|
186
|
+
const repeatEvaluation = evaluateObservedExpectation(observedExpectation, repeatTrace);
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
repeatTrace,
|
|
190
|
+
repeatEvaluation
|
|
191
|
+
};
|
|
192
|
+
}
|