@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.
Files changed (213) hide show
  1. package/README.md +10 -6
  2. package/bin/verax.js +11 -11
  3. package/package.json +29 -8
  4. package/src/cli/commands/baseline.js +103 -0
  5. package/src/cli/commands/default.js +51 -6
  6. package/src/cli/commands/doctor.js +29 -0
  7. package/src/cli/commands/ga.js +246 -0
  8. package/src/cli/commands/gates.js +95 -0
  9. package/src/cli/commands/inspect.js +4 -2
  10. package/src/cli/commands/release-check.js +215 -0
  11. package/src/cli/commands/run.js +45 -6
  12. package/src/cli/commands/security-check.js +212 -0
  13. package/src/cli/commands/truth.js +113 -0
  14. package/src/cli/entry.js +30 -20
  15. package/src/cli/util/angular-component-extractor.js +179 -0
  16. package/src/cli/util/angular-navigation-detector.js +141 -0
  17. package/src/cli/util/angular-network-detector.js +161 -0
  18. package/src/cli/util/angular-state-detector.js +162 -0
  19. package/src/cli/util/ast-interactive-detector.js +544 -0
  20. package/src/cli/util/ast-network-detector.js +603 -0
  21. package/src/cli/util/ast-promise-extractor.js +581 -0
  22. package/src/cli/util/ast-usestate-detector.js +602 -0
  23. package/src/cli/util/atomic-write.js +12 -1
  24. package/src/cli/util/bootstrap-guard.js +86 -0
  25. package/src/cli/util/console-reporter.js +72 -0
  26. package/src/cli/util/detection-engine.js +105 -41
  27. package/src/cli/util/determinism-runner.js +124 -0
  28. package/src/cli/util/determinism-writer.js +129 -0
  29. package/src/cli/util/digest-engine.js +359 -0
  30. package/src/cli/util/dom-diff.js +226 -0
  31. package/src/cli/util/evidence-engine.js +287 -0
  32. package/src/cli/util/expectation-extractor.js +151 -5
  33. package/src/cli/util/findings-writer.js +3 -0
  34. package/src/cli/util/framework-detector.js +572 -0
  35. package/src/cli/util/idgen.js +1 -1
  36. package/src/cli/util/interaction-planner.js +529 -0
  37. package/src/cli/util/learn-writer.js +2 -0
  38. package/src/cli/util/ledger-writer.js +110 -0
  39. package/src/cli/util/monorepo-resolver.js +162 -0
  40. package/src/cli/util/observation-engine.js +127 -278
  41. package/src/cli/util/observe-writer.js +2 -0
  42. package/src/cli/util/project-discovery.js +284 -0
  43. package/src/cli/util/project-writer.js +2 -0
  44. package/src/cli/util/run-id.js +23 -27
  45. package/src/cli/util/run-resolver.js +64 -0
  46. package/src/cli/util/run-result.js +778 -0
  47. package/src/cli/util/selector-resolver.js +235 -0
  48. package/src/cli/util/source-requirement.js +55 -0
  49. package/src/cli/util/summary-writer.js +2 -0
  50. package/src/cli/util/svelte-navigation-detector.js +163 -0
  51. package/src/cli/util/svelte-network-detector.js +80 -0
  52. package/src/cli/util/svelte-sfc-extractor.js +146 -0
  53. package/src/cli/util/svelte-state-detector.js +242 -0
  54. package/src/cli/util/trust-activation-integration.js +496 -0
  55. package/src/cli/util/trust-activation-wrapper.js +85 -0
  56. package/src/cli/util/trust-integration-hooks.js +164 -0
  57. package/src/cli/util/types.js +153 -0
  58. package/src/cli/util/url-validation.js +40 -0
  59. package/src/cli/util/vue-navigation-detector.js +178 -0
  60. package/src/cli/util/vue-sfc-extractor.js +161 -0
  61. package/src/cli/util/vue-state-detector.js +215 -0
  62. package/src/types/fs-augment.d.ts +23 -0
  63. package/src/types/global.d.ts +137 -0
  64. package/src/types/internal-types.d.ts +35 -0
  65. package/src/verax/cli/init.js +4 -18
  66. package/src/verax/core/action-classifier.js +4 -3
  67. package/src/verax/core/artifacts/registry.js +139 -0
  68. package/src/verax/core/artifacts/verifier.js +990 -0
  69. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  70. package/src/verax/core/baseline/baseline.snapshot.js +233 -0
  71. package/src/verax/core/capabilities/gates.js +505 -0
  72. package/src/verax/core/capabilities/registry.js +475 -0
  73. package/src/verax/core/confidence/confidence-compute.js +144 -0
  74. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  75. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  76. package/src/verax/core/confidence/confidence-weights.js +44 -0
  77. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  78. package/src/verax/core/confidence/confidence.loader.js +80 -0
  79. package/src/verax/core/confidence/confidence.schema.js +94 -0
  80. package/src/verax/core/confidence-engine-refactor.js +489 -0
  81. package/src/verax/core/confidence-engine.js +625 -0
  82. package/src/verax/core/contracts/index.js +29 -0
  83. package/src/verax/core/contracts/types.js +186 -0
  84. package/src/verax/core/contracts/validators.js +456 -0
  85. package/src/verax/core/decisions/decision.trace.js +278 -0
  86. package/src/verax/core/determinism/contract-writer.js +89 -0
  87. package/src/verax/core/determinism/contract.js +139 -0
  88. package/src/verax/core/determinism/diff.js +405 -0
  89. package/src/verax/core/determinism/engine.js +222 -0
  90. package/src/verax/core/determinism/finding-identity.js +149 -0
  91. package/src/verax/core/determinism/normalize.js +466 -0
  92. package/src/verax/core/determinism/report-writer.js +93 -0
  93. package/src/verax/core/determinism/run-fingerprint.js +123 -0
  94. package/src/verax/core/dynamic-route-intelligence.js +529 -0
  95. package/src/verax/core/evidence/evidence-capture-service.js +308 -0
  96. package/src/verax/core/evidence/evidence-intent-ledger.js +166 -0
  97. package/src/verax/core/evidence-builder.js +487 -0
  98. package/src/verax/core/execution-mode-context.js +77 -0
  99. package/src/verax/core/execution-mode-detector.js +192 -0
  100. package/src/verax/core/failures/exit-codes.js +88 -0
  101. package/src/verax/core/failures/failure-summary.js +76 -0
  102. package/src/verax/core/failures/failure.factory.js +225 -0
  103. package/src/verax/core/failures/failure.ledger.js +133 -0
  104. package/src/verax/core/failures/failure.types.js +196 -0
  105. package/src/verax/core/failures/index.js +10 -0
  106. package/src/verax/core/ga/ga-report-writer.js +43 -0
  107. package/src/verax/core/ga/ga.artifact.js +49 -0
  108. package/src/verax/core/ga/ga.contract.js +435 -0
  109. package/src/verax/core/ga/ga.enforcer.js +87 -0
  110. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  111. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  112. package/src/verax/core/guardrails/policy.loader.js +84 -0
  113. package/src/verax/core/guardrails/policy.schema.js +110 -0
  114. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  115. package/src/verax/core/guardrails-engine.js +505 -0
  116. package/src/verax/core/incremental-store.js +1 -0
  117. package/src/verax/core/integrity/budget.js +138 -0
  118. package/src/verax/core/integrity/determinism.js +342 -0
  119. package/src/verax/core/integrity/integrity.js +208 -0
  120. package/src/verax/core/integrity/poisoning.js +108 -0
  121. package/src/verax/core/integrity/transaction.js +140 -0
  122. package/src/verax/core/observe/run-timeline.js +318 -0
  123. package/src/verax/core/perf/perf.contract.js +186 -0
  124. package/src/verax/core/perf/perf.display.js +65 -0
  125. package/src/verax/core/perf/perf.enforcer.js +91 -0
  126. package/src/verax/core/perf/perf.monitor.js +209 -0
  127. package/src/verax/core/perf/perf.report.js +200 -0
  128. package/src/verax/core/pipeline-tracker.js +243 -0
  129. package/src/verax/core/product-definition.js +127 -0
  130. package/src/verax/core/release/provenance.builder.js +130 -0
  131. package/src/verax/core/release/release-report-writer.js +40 -0
  132. package/src/verax/core/release/release.enforcer.js +164 -0
  133. package/src/verax/core/release/reproducibility.check.js +222 -0
  134. package/src/verax/core/release/sbom.builder.js +292 -0
  135. package/src/verax/core/replay-validator.js +2 -0
  136. package/src/verax/core/replay.js +4 -0
  137. package/src/verax/core/report/cross-index.js +195 -0
  138. package/src/verax/core/report/human-summary.js +362 -0
  139. package/src/verax/core/route-intelligence.js +420 -0
  140. package/src/verax/core/run-id.js +6 -3
  141. package/src/verax/core/run-manifest.js +4 -3
  142. package/src/verax/core/security/secrets.scan.js +329 -0
  143. package/src/verax/core/security/security-report.js +50 -0
  144. package/src/verax/core/security/security.enforcer.js +128 -0
  145. package/src/verax/core/security/supplychain.defaults.json +38 -0
  146. package/src/verax/core/security/supplychain.policy.js +334 -0
  147. package/src/verax/core/security/vuln.scan.js +265 -0
  148. package/src/verax/core/truth/truth.certificate.js +252 -0
  149. package/src/verax/core/ui-feedback-intelligence.js +481 -0
  150. package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
  151. package/src/verax/detect/confidence-engine.js +62 -34
  152. package/src/verax/detect/confidence-helper.js +34 -0
  153. package/src/verax/detect/dynamic-route-findings.js +338 -0
  154. package/src/verax/detect/expectation-chain-detector.js +417 -0
  155. package/src/verax/detect/expectation-model.js +2 -2
  156. package/src/verax/detect/failure-cause-inference.js +293 -0
  157. package/src/verax/detect/findings-writer.js +131 -35
  158. package/src/verax/detect/flow-detector.js +2 -2
  159. package/src/verax/detect/form-silent-failure.js +98 -0
  160. package/src/verax/detect/index.js +46 -5
  161. package/src/verax/detect/invariants-enforcer.js +147 -0
  162. package/src/verax/detect/journey-stall-detector.js +558 -0
  163. package/src/verax/detect/navigation-silent-failure.js +82 -0
  164. package/src/verax/detect/problem-aggregator.js +361 -0
  165. package/src/verax/detect/route-findings.js +219 -0
  166. package/src/verax/detect/summary-writer.js +477 -0
  167. package/src/verax/detect/test-failure-cause-inference.js +314 -0
  168. package/src/verax/detect/ui-feedback-findings.js +207 -0
  169. package/src/verax/detect/view-switch-correlator.js +242 -0
  170. package/src/verax/flow/flow-engine.js +2 -1
  171. package/src/verax/flow/flow-spec.js +0 -6
  172. package/src/verax/index.js +4 -0
  173. package/src/verax/intel/ts-program.js +1 -0
  174. package/src/verax/intel/vue-navigation-extractor.js +3 -0
  175. package/src/verax/learn/action-contract-extractor.js +3 -0
  176. package/src/verax/learn/ast-contract-extractor.js +1 -1
  177. package/src/verax/learn/flow-extractor.js +1 -0
  178. package/src/verax/learn/project-detector.js +5 -0
  179. package/src/verax/learn/react-router-extractor.js +2 -0
  180. package/src/verax/learn/source-instrumenter.js +1 -0
  181. package/src/verax/learn/state-extractor.js +2 -1
  182. package/src/verax/learn/static-extractor.js +1 -0
  183. package/src/verax/observe/coverage-gaps.js +132 -0
  184. package/src/verax/observe/expectation-handler.js +126 -0
  185. package/src/verax/observe/incremental-skip.js +46 -0
  186. package/src/verax/observe/index.js +51 -155
  187. package/src/verax/observe/interaction-executor.js +192 -0
  188. package/src/verax/observe/interaction-runner.js +782 -513
  189. package/src/verax/observe/network-firewall.js +86 -0
  190. package/src/verax/observe/observation-builder.js +169 -0
  191. package/src/verax/observe/observe-context.js +205 -0
  192. package/src/verax/observe/observe-helpers.js +192 -0
  193. package/src/verax/observe/observe-runner.js +230 -0
  194. package/src/verax/observe/observers/budget-observer.js +185 -0
  195. package/src/verax/observe/observers/console-observer.js +102 -0
  196. package/src/verax/observe/observers/coverage-observer.js +107 -0
  197. package/src/verax/observe/observers/interaction-observer.js +471 -0
  198. package/src/verax/observe/observers/navigation-observer.js +132 -0
  199. package/src/verax/observe/observers/network-observer.js +87 -0
  200. package/src/verax/observe/observers/safety-observer.js +82 -0
  201. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  202. package/src/verax/observe/page-traversal.js +138 -0
  203. package/src/verax/observe/snapshot-ops.js +94 -0
  204. package/src/verax/observe/ui-feedback-detector.js +742 -0
  205. package/src/verax/scan-summary-writer.js +2 -0
  206. package/src/verax/shared/artifact-manager.js +25 -5
  207. package/src/verax/shared/caching.js +1 -0
  208. package/src/verax/shared/css-spinner-rules.js +204 -0
  209. package/src/verax/shared/expectation-tracker.js +1 -0
  210. package/src/verax/shared/view-switch-rules.js +208 -0
  211. package/src/verax/shared/zip-artifacts.js +6 -0
  212. package/src/verax/shared/config-loader.js +0 -169
  213. /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 { loadPreviousSnapshot, saveSnapshot, buildSnapshot, compareSnapshots, shouldSkipInteractionIncremental } from '../core/incremental-store.js';
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 unless flags allow
36
- * - Blocks POST/PUT/PATCH/DELETE unless --allow-writes
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 { allowWrites = false, allowRiskyActions = false, allowCrossOrigin = false } = safetyFlags;
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.route('**/*', (route) => {
72
- const request = route.request();
73
- const method = request.method();
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
- // SCALE INTELLIGENCE: Load previous snapshot for incremental mode
162
- oldSnapshot = loadPreviousSnapshot(projectDir);
163
- if (oldSnapshot) {
164
- const currentSnapshot = buildSnapshot(manifest, []);
165
- snapshotDiff = compareSnapshots(oldSnapshot, currentSnapshot);
166
- incrementalMode = !snapshotDiff.hasChanges; // Use incremental if nothing changed
167
- }
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
- if (remainingInteractionsGaps.length > 0) {
749
- expectationCoverageGaps.push(...remainingInteractionsGaps.map(gap => ({
750
- expectationId: null,
751
- type: gap.interaction.type,
752
- reason: gap.reason,
753
- fromPath: gap.url,
754
- source: null,
755
- evidence: {
756
- interaction: gap.interaction
757
- }
758
- })));
759
- }
760
-
761
- // Record frontier capping as coverage gap if it occurred
762
- if (frontier.frontierCapped) {
763
- expectationCoverageGaps.push({
764
- expectationId: null,
765
- type: 'navigation',
766
- reason: 'frontier_capped',
767
- fromPath: page.url(),
768
- source: null,
769
- evidence: {
770
- message: `Frontier capped at ${scanBudget.maxUniqueUrls || 'unlimited'} unique URLs`
771
- }
772
- });
773
- }
692
+ expectationCoverageGaps.push(...accumulateCoverageGaps(remainingInteractionsGaps, frontier, page.url(), scanBudget));
774
693
 
775
694
  // Build coverage object matching writeTraces expected format
776
- const coverage = {
777
- candidatesDiscovered: totalInteractionsDiscovered,
778
- candidatesSelected: totalInteractionsExecuted,
779
- cap: scanBudget.maxTotalInteractions,
780
- capped: totalInteractionsExecuted >= scanBudget.maxTotalInteractions || remainingInteractionsGaps.length > 0,
781
- pagesVisited: frontier.pagesVisited,
782
- pagesDiscovered: frontier.pagesDiscovered,
783
- skippedInteractions: skippedInteractions.length,
784
- interactionsDiscovered: totalInteractionsDiscovered,
785
- interactionsExecuted: totalInteractionsExecuted
786
- };
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
- if (coverage.capped) {
794
- observeWarnings.push({
795
- code: 'INTERACTIONS_CAPPED',
796
- message: `Interaction execution capped. Visited ${coverage.pagesVisited} pages, discovered ${coverage.pagesDiscovered}, executed ${coverage.candidatesSelected} of ${coverage.candidatesDiscovered} interactions. Coverage incomplete.`
797
- });
798
- }
799
-
800
- if (skippedInteractions.length > 0) {
801
- observeWarnings.push({
802
- code: 'INTERACTIONS_SKIPPED',
803
- message: `Skipped ${skippedInteractions.length} dangerous interactions`,
804
- details: skippedInteractions
805
- });
806
- }
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
- // SCALE INTELLIGENCE: Save snapshot for next incremental run
842
- if (manifest) {
843
- // Build snapshot from current run (extract interactions from traces)
844
- const observedInteractions = traces
845
- .filter(t => t.interaction && !t.incremental)
846
- .map(t => ({
847
- type: t.interaction?.type,
848
- selector: t.interaction?.selector,
849
- url: t.before?.url || url
850
- }));
851
-
852
- const currentSnapshot = buildSnapshot(manifest, observedInteractions);
853
- saveSnapshot(projectDir, currentSnapshot, runId);
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
+ }