@veraxhq/verax 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +14 -18
  2. package/bin/verax.js +7 -0
  3. package/package.json +3 -3
  4. package/src/cli/commands/baseline.js +104 -0
  5. package/src/cli/commands/default.js +79 -25
  6. package/src/cli/commands/ga.js +243 -0
  7. package/src/cli/commands/gates.js +95 -0
  8. package/src/cli/commands/inspect.js +131 -2
  9. package/src/cli/commands/release-check.js +213 -0
  10. package/src/cli/commands/run.js +246 -35
  11. package/src/cli/commands/security-check.js +211 -0
  12. package/src/cli/commands/truth.js +114 -0
  13. package/src/cli/entry.js +304 -67
  14. package/src/cli/util/angular-component-extractor.js +179 -0
  15. package/src/cli/util/angular-navigation-detector.js +141 -0
  16. package/src/cli/util/angular-network-detector.js +161 -0
  17. package/src/cli/util/angular-state-detector.js +162 -0
  18. package/src/cli/util/ast-interactive-detector.js +546 -0
  19. package/src/cli/util/ast-network-detector.js +603 -0
  20. package/src/cli/util/ast-usestate-detector.js +602 -0
  21. package/src/cli/util/bootstrap-guard.js +86 -0
  22. package/src/cli/util/determinism-runner.js +123 -0
  23. package/src/cli/util/determinism-writer.js +129 -0
  24. package/src/cli/util/env-url.js +4 -0
  25. package/src/cli/util/expectation-extractor.js +369 -73
  26. package/src/cli/util/findings-writer.js +126 -16
  27. package/src/cli/util/learn-writer.js +3 -1
  28. package/src/cli/util/observe-writer.js +3 -1
  29. package/src/cli/util/paths.js +3 -12
  30. package/src/cli/util/project-discovery.js +3 -0
  31. package/src/cli/util/project-writer.js +3 -1
  32. package/src/cli/util/run-resolver.js +64 -0
  33. package/src/cli/util/source-requirement.js +55 -0
  34. package/src/cli/util/summary-writer.js +1 -0
  35. package/src/cli/util/svelte-navigation-detector.js +163 -0
  36. package/src/cli/util/svelte-network-detector.js +80 -0
  37. package/src/cli/util/svelte-sfc-extractor.js +147 -0
  38. package/src/cli/util/svelte-state-detector.js +243 -0
  39. package/src/cli/util/vue-navigation-detector.js +177 -0
  40. package/src/cli/util/vue-sfc-extractor.js +162 -0
  41. package/src/cli/util/vue-state-detector.js +215 -0
  42. package/src/verax/cli/finding-explainer.js +56 -3
  43. package/src/verax/core/artifacts/registry.js +154 -0
  44. package/src/verax/core/artifacts/verifier.js +980 -0
  45. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  46. package/src/verax/core/baseline/baseline.snapshot.js +231 -0
  47. package/src/verax/core/capabilities/gates.js +499 -0
  48. package/src/verax/core/capabilities/registry.js +475 -0
  49. package/src/verax/core/confidence/confidence-compute.js +137 -0
  50. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  51. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  52. package/src/verax/core/confidence/confidence-weights.js +44 -0
  53. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  54. package/src/verax/core/confidence/confidence.loader.js +79 -0
  55. package/src/verax/core/confidence/confidence.schema.js +94 -0
  56. package/src/verax/core/confidence-engine-refactor.js +484 -0
  57. package/src/verax/core/confidence-engine.js +486 -0
  58. package/src/verax/core/confidence-engine.js.backup +471 -0
  59. package/src/verax/core/contracts/index.js +29 -0
  60. package/src/verax/core/contracts/types.js +185 -0
  61. package/src/verax/core/contracts/validators.js +381 -0
  62. package/src/verax/core/decision-snapshot.js +30 -3
  63. package/src/verax/core/decisions/decision.trace.js +276 -0
  64. package/src/verax/core/determinism/contract-writer.js +89 -0
  65. package/src/verax/core/determinism/contract.js +139 -0
  66. package/src/verax/core/determinism/diff.js +364 -0
  67. package/src/verax/core/determinism/engine.js +221 -0
  68. package/src/verax/core/determinism/finding-identity.js +148 -0
  69. package/src/verax/core/determinism/normalize.js +438 -0
  70. package/src/verax/core/determinism/report-writer.js +92 -0
  71. package/src/verax/core/determinism/run-fingerprint.js +118 -0
  72. package/src/verax/core/dynamic-route-intelligence.js +528 -0
  73. package/src/verax/core/evidence/evidence-capture-service.js +307 -0
  74. package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
  75. package/src/verax/core/evidence-builder.js +487 -0
  76. package/src/verax/core/execution-mode-context.js +77 -0
  77. package/src/verax/core/execution-mode-detector.js +190 -0
  78. package/src/verax/core/failures/exit-codes.js +86 -0
  79. package/src/verax/core/failures/failure-summary.js +76 -0
  80. package/src/verax/core/failures/failure.factory.js +225 -0
  81. package/src/verax/core/failures/failure.ledger.js +132 -0
  82. package/src/verax/core/failures/failure.types.js +196 -0
  83. package/src/verax/core/failures/index.js +10 -0
  84. package/src/verax/core/ga/ga-report-writer.js +43 -0
  85. package/src/verax/core/ga/ga.artifact.js +49 -0
  86. package/src/verax/core/ga/ga.contract.js +434 -0
  87. package/src/verax/core/ga/ga.enforcer.js +86 -0
  88. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  89. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  90. package/src/verax/core/guardrails/policy.loader.js +83 -0
  91. package/src/verax/core/guardrails/policy.schema.js +110 -0
  92. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  93. package/src/verax/core/guardrails-engine.js +505 -0
  94. package/src/verax/core/observe/run-timeline.js +316 -0
  95. package/src/verax/core/perf/perf.contract.js +186 -0
  96. package/src/verax/core/perf/perf.display.js +65 -0
  97. package/src/verax/core/perf/perf.enforcer.js +91 -0
  98. package/src/verax/core/perf/perf.monitor.js +209 -0
  99. package/src/verax/core/perf/perf.report.js +198 -0
  100. package/src/verax/core/pipeline-tracker.js +238 -0
  101. package/src/verax/core/product-definition.js +127 -0
  102. package/src/verax/core/release/provenance.builder.js +271 -0
  103. package/src/verax/core/release/release-report-writer.js +40 -0
  104. package/src/verax/core/release/release.enforcer.js +159 -0
  105. package/src/verax/core/release/reproducibility.check.js +221 -0
  106. package/src/verax/core/release/sbom.builder.js +283 -0
  107. package/src/verax/core/report/cross-index.js +192 -0
  108. package/src/verax/core/report/human-summary.js +222 -0
  109. package/src/verax/core/route-intelligence.js +419 -0
  110. package/src/verax/core/security/secrets.scan.js +326 -0
  111. package/src/verax/core/security/security-report.js +50 -0
  112. package/src/verax/core/security/security.enforcer.js +124 -0
  113. package/src/verax/core/security/supplychain.defaults.json +38 -0
  114. package/src/verax/core/security/supplychain.policy.js +326 -0
  115. package/src/verax/core/security/vuln.scan.js +265 -0
  116. package/src/verax/core/truth/truth.certificate.js +250 -0
  117. package/src/verax/core/ui-feedback-intelligence.js +515 -0
  118. package/src/verax/detect/confidence-engine.js +628 -40
  119. package/src/verax/detect/confidence-helper.js +33 -0
  120. package/src/verax/detect/detection-engine.js +18 -1
  121. package/src/verax/detect/dynamic-route-findings.js +335 -0
  122. package/src/verax/detect/expectation-chain-detector.js +417 -0
  123. package/src/verax/detect/expectation-model.js +3 -1
  124. package/src/verax/detect/findings-writer.js +141 -5
  125. package/src/verax/detect/index.js +229 -5
  126. package/src/verax/detect/journey-stall-detector.js +558 -0
  127. package/src/verax/detect/route-findings.js +218 -0
  128. package/src/verax/detect/ui-feedback-findings.js +207 -0
  129. package/src/verax/detect/verdict-engine.js +57 -3
  130. package/src/verax/detect/view-switch-correlator.js +242 -0
  131. package/src/verax/index.js +413 -45
  132. package/src/verax/learn/action-contract-extractor.js +682 -64
  133. package/src/verax/learn/route-validator.js +4 -1
  134. package/src/verax/observe/index.js +88 -843
  135. package/src/verax/observe/interaction-runner.js +25 -8
  136. package/src/verax/observe/observe-context.js +205 -0
  137. package/src/verax/observe/observe-helpers.js +191 -0
  138. package/src/verax/observe/observe-runner.js +226 -0
  139. package/src/verax/observe/observers/budget-observer.js +185 -0
  140. package/src/verax/observe/observers/console-observer.js +102 -0
  141. package/src/verax/observe/observers/coverage-observer.js +107 -0
  142. package/src/verax/observe/observers/interaction-observer.js +471 -0
  143. package/src/verax/observe/observers/navigation-observer.js +132 -0
  144. package/src/verax/observe/observers/network-observer.js +87 -0
  145. package/src/verax/observe/observers/safety-observer.js +82 -0
  146. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  147. package/src/verax/observe/ui-feedback-detector.js +742 -0
  148. package/src/verax/observe/ui-signal-sensor.js +148 -2
  149. package/src/verax/scan-summary-writer.js +42 -8
  150. package/src/verax/shared/artifact-manager.js +8 -5
  151. package/src/verax/shared/css-spinner-rules.js +204 -0
  152. package/src/verax/shared/view-switch-rules.js +208 -0
@@ -12,6 +12,7 @@ import { FocusSensor } from './focus-sensor.js';
12
12
  import { AriaSensor } from './aria-sensor.js';
13
13
  import { TimingSensor } from './timing-sensor.js';
14
14
  import { HumanBehaviorDriver } from './human-driver.js';
15
+ import { UIFeedbackDetector } from './ui-feedback-detector.js';
15
16
 
16
17
  // Import CLICK_TIMEOUT_MS from human-driver (re-export needed)
17
18
  const CLICK_TIMEOUT_MS = 2000;
@@ -131,6 +132,7 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
131
132
  freezeLikeThresholdMs: 3000
132
133
  });
133
134
  const humanDriver = new HumanBehaviorDriver({}, scanBudget);
135
+ const uiFeedbackDetector = new UIFeedbackDetector();
134
136
 
135
137
  let networkWindowId = null;
136
138
  let consoleWindowId = null;
@@ -175,7 +177,11 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
175
177
  }
176
178
  trace.page.beforeTitle = beforeTitle;
177
179
 
178
- uiBefore = await uiSignalSensor.snapshot(page).catch(() => ({}));
180
+ const interactionTimestamp = timestamp || Date.now();
181
+ uiBefore = await uiSignalSensor.snapshot(page, interactionTimestamp, null).catch(() => ({}));
182
+
183
+ // UI FEEDBACK INTELLIGENCE: Capture before state for feedback detection
184
+ await uiFeedbackDetector.captureBefore(page, { targetSelector: interaction.selector });
179
185
 
180
186
  // A11Y INTELLIGENCE: Capture focus and ARIA state before interaction
181
187
  await focusSensor.captureBefore(page);
@@ -228,7 +234,8 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
228
234
 
229
235
  const networkSummary = networkSensor.stopWindow(networkWindowId);
230
236
  const consoleSummary = consoleSensor.stopWindow(consoleWindowId, page);
231
- const uiAfter = await uiSignalSensor.snapshot(page);
237
+ const interactionTimestamp = timestamp || Date.now();
238
+ const uiAfter = await uiSignalSensor.snapshot(page, interactionTimestamp, uiBefore);
232
239
  const uiDiff = uiSignalSensor.diff(uiBefore, uiAfter);
233
240
 
234
241
  // STATE INTELLIGENCE: Capture after state and compute diff
@@ -417,7 +424,8 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
417
424
 
418
425
  // Record UI change if detected
419
426
  if (uiSignalSensor) {
420
- const currentUi = await uiSignalSensor.snapshot(page).catch(() => ({}));
427
+ const interactionTimestamp = timestamp || Date.now();
428
+ const currentUi = await uiSignalSensor.snapshot(page, interactionTimestamp, uiBefore).catch(() => ({}));
421
429
  const currentDiff = uiSignalSensor.diff(uiBefore, currentUi);
422
430
  if (currentDiff.changed) {
423
431
  timingSensor.recordUiChange();
@@ -473,7 +481,8 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
473
481
  }
474
482
  }
475
483
 
476
- const uiAfter = await uiSignalSensor.snapshot(page).catch(() => ({}));
484
+ const interactionTimestamp = timestamp || Date.now();
485
+ const uiAfter = await uiSignalSensor.snapshot(page, interactionTimestamp, uiBefore).catch(() => ({}));
477
486
  const uiDiff = uiSignalSensor.diff(uiBefore, uiAfter);
478
487
 
479
488
  // STATE INTELLIGENCE: Capture after state and compute diff
@@ -597,9 +606,14 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
597
606
  }
598
607
  }
599
608
 
600
- const uiAfter = await uiSignalSensor.snapshot(page);
609
+ const interactionTimestamp = timestamp || Date.now();
610
+ const uiAfter = await uiSignalSensor.snapshot(page, interactionTimestamp, uiBefore);
601
611
  const uiDiff = uiSignalSensor.diff(uiBefore, uiAfter);
602
612
 
613
+ // UI FEEDBACK INTELLIGENCE: Capture after state and compute feedback signals
614
+ await uiFeedbackDetector.captureAfter(page);
615
+ const uiFeedbackSignals = uiFeedbackDetector.computeFeedbackSignals();
616
+
603
617
  // PERFORMANCE INTELLIGENCE: Record UI change in timing sensor if detected
604
618
  if (timingSensor && uiDiff.changed) {
605
619
  timingSensor.recordUiChange();
@@ -638,7 +652,8 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
638
652
  available: stateDiff.available,
639
653
  changed: stateDiff.changed,
640
654
  storeType: storeType
641
- }
655
+ },
656
+ uiFeedback: uiFeedbackSignals // UI FEEDBACK INTELLIGENCE: Add feedback detection signals
642
657
  };
643
658
 
644
659
  return trace;
@@ -661,7 +676,8 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
661
676
  trace.sensors.console = consoleSensor.getEmptySummary();
662
677
  }
663
678
 
664
- const uiAfter = await uiSignalSensor.snapshot(page).catch(() => ({}));
679
+ const interactionTimestamp = timestamp || Date.now();
680
+ const uiAfter = await uiSignalSensor.snapshot(page, interactionTimestamp, uiBefore || {}).catch(() => ({}));
665
681
  const uiDiff = uiSignalSensor.diff(uiBefore || {}, uiAfter);
666
682
 
667
683
  // STATE INTELLIGENCE: Capture after state and compute diff
@@ -717,7 +733,8 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
717
733
  trace.sensors.state = { available: false, changed: [], storeType: null };
718
734
  }
719
735
 
720
- const uiAfter = await uiSignalSensor.snapshot(page).catch(() => ({}));
736
+ const interactionTimestamp = timestamp || Date.now();
737
+ const uiAfter = await uiSignalSensor.snapshot(page, interactionTimestamp, uiBefore || {}).catch(() => ({}));
721
738
  const uiDiff = uiSignalSensor.diff(uiBefore || {}, uiAfter || {});
722
739
  trace.sensors.uiSignals = {
723
740
  before: uiBefore || {},
@@ -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,191 @@
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, 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
+ manifest = JSON.parse(manifestContent);
31
+
32
+ oldSnapshot = loadPreviousSnapshot(projectDir);
33
+ if (oldSnapshot) {
34
+ const currentSnapshot = buildSnapshot(manifest, []);
35
+ snapshotDiff = compareSnapshots(oldSnapshot, currentSnapshot);
36
+ incrementalMode = !snapshotDiff.hasChanges;
37
+ }
38
+
39
+ const provenCount = (manifest.staticExpectations || []).filter(exp => isProvenExpectation(exp)).length;
40
+ if (provenCount > 0) {
41
+ expectationResults = await executeProvenExpectations(page, manifest, url, screenshotsDir, scanBudget, startTime, projectDir);
42
+ expectationCoverageGaps = expectationResults.coverageGaps || [];
43
+ }
44
+ } catch (err) {
45
+ silenceTracker.record({
46
+ scope: 'discovery',
47
+ reason: 'discovery_error',
48
+ description: 'Manifest load or expectation execution failed',
49
+ context: { error: err?.message },
50
+ impact: 'incomplete_check'
51
+ });
52
+ }
53
+ }
54
+
55
+ return { manifest, expectationResults, expectationCoverageGaps, incrementalMode, snapshotDiff, oldSnapshot };
56
+ }
57
+
58
+ /**
59
+ * Process traversal results and build observation
60
+ */
61
+ export async function processTraversalResults(traversalResult, expectationResults, expectationCoverageGaps, remainingInteractionsGaps, frontier, scanBudget, page, url, finalTraces, finalSkippedInteractions, finalObservedExpectations, silenceTracker, manifest, incrementalMode, snapshotDiff, projectDir, runId) {
62
+ // Combine all coverage gaps
63
+ const allCoverageGaps = [...expectationCoverageGaps];
64
+ if (remainingInteractionsGaps.length > 0) {
65
+ allCoverageGaps.push(...remainingInteractionsGaps.map(gap => ({
66
+ expectationId: null,
67
+ type: gap.interaction.type,
68
+ reason: gap.reason,
69
+ fromPath: gap.url,
70
+ source: null,
71
+ evidence: { interaction: gap.interaction }
72
+ })));
73
+ }
74
+
75
+ if (frontier.frontierCapped) {
76
+ allCoverageGaps.push({
77
+ expectationId: null,
78
+ type: 'navigation',
79
+ reason: 'frontier_capped',
80
+ fromPath: page.url(),
81
+ source: null,
82
+ evidence: { message: `Frontier capped at ${scanBudget.maxUniqueUrls || 'unlimited'} unique URLs` }
83
+ });
84
+ }
85
+
86
+ // Build coverage object
87
+ const coverage = {
88
+ candidatesDiscovered: traversalResult.totalInteractionsDiscovered,
89
+ candidatesSelected: traversalResult.totalInteractionsExecuted,
90
+ cap: scanBudget.maxTotalInteractions,
91
+ capped: traversalResult.totalInteractionsExecuted >= scanBudget.maxTotalInteractions || remainingInteractionsGaps.length > 0,
92
+ pagesVisited: frontier.pagesVisited,
93
+ pagesDiscovered: frontier.pagesDiscovered,
94
+ skippedInteractions: finalSkippedInteractions.length,
95
+ interactionsDiscovered: traversalResult.totalInteractionsDiscovered,
96
+ interactionsExecuted: traversalResult.totalInteractionsExecuted
97
+ };
98
+
99
+ // Build warnings
100
+ const observeWarnings = [];
101
+ if (coverage.capped) {
102
+ observeWarnings.push({
103
+ code: 'INTERACTIONS_CAPPED',
104
+ message: `Interaction execution capped. Visited ${coverage.pagesVisited} pages, discovered ${coverage.pagesDiscovered}, executed ${coverage.candidatesSelected} of ${coverage.candidatesDiscovered} interactions. Coverage incomplete.`
105
+ });
106
+ }
107
+ if (finalSkippedInteractions.length > 0) {
108
+ observeWarnings.push({
109
+ code: 'INTERACTIONS_SKIPPED',
110
+ message: `Skipped ${finalSkippedInteractions.length} dangerous interactions`,
111
+ details: finalSkippedInteractions
112
+ });
113
+ }
114
+
115
+ // Append expectation traces
116
+ if (expectationResults?.results) {
117
+ for (const result of expectationResults.results) {
118
+ if (result.trace) {
119
+ result.trace.expectationDriven = true;
120
+ result.trace.expectationId = result.expectationId;
121
+ result.trace.expectationOutcome = result.outcome;
122
+ finalTraces.push(result.trace);
123
+ }
124
+ }
125
+ }
126
+
127
+ // Write traces
128
+ const observation = writeTraces(projectDir, url, finalTraces, coverage, observeWarnings, finalObservedExpectations, silenceTracker, runId);
129
+ observation.silences = silenceTracker.getDetailedSummary();
130
+
131
+ // Add expectation execution results
132
+ if (expectationResults) {
133
+ observation.expectationExecution = {
134
+ totalProvenExpectations: expectationResults.totalProvenExpectations,
135
+ executedCount: expectationResults.executedCount,
136
+ coverageGapsCount: allCoverageGaps.length,
137
+ results: expectationResults.results.map(r => ({
138
+ expectationId: r.expectationId,
139
+ type: r.type,
140
+ fromPath: r.fromPath,
141
+ outcome: r.outcome,
142
+ reason: r.reason
143
+ }))
144
+ };
145
+ observation.expectationCoverageGaps = allCoverageGaps;
146
+ }
147
+
148
+ // Add incremental mode metadata
149
+ if (manifest) {
150
+ const { buildSnapshot, saveSnapshot } = await import('../core/incremental-store.js');
151
+ const observedInteractions = finalTraces
152
+ .filter(t => t.interaction && !t.incremental)
153
+ .map(t => ({
154
+ type: t.interaction?.type,
155
+ selector: t.interaction?.selector,
156
+ url: t.before?.url || url
157
+ }));
158
+
159
+ const currentSnapshot = buildSnapshot(manifest, observedInteractions);
160
+ saveSnapshot(projectDir, currentSnapshot, runId);
161
+
162
+ observation.incremental = {
163
+ enabled: incrementalMode,
164
+ snapshotDiff: snapshotDiff,
165
+ skippedInteractionsCount: finalSkippedInteractions.filter(s => s.reason === 'incremental_unchanged').length
166
+ };
167
+ }
168
+
169
+ return observation;
170
+ }
171
+
172
+ /**
173
+ * Write determinism artifacts
174
+ */
175
+ export async function writeDeterminismArtifacts(projectDir, runId, decisionRecorder) {
176
+ // PHASE 25: Write determinism contract
177
+ const { writeDeterminismContract } = await import('../core/determinism/contract-writer.js');
178
+ const { getRunArtifactDir } = await import('../core/run-id.js');
179
+ const runDir = getRunArtifactDir(projectDir, runId);
180
+ writeDeterminismContract(runDir, decisionRecorder);
181
+ if (!runId || !projectDir) return;
182
+
183
+ const runsDir = resolve(projectDir, '.verax', 'runs', runId);
184
+ mkdirSync(runsDir, { recursive: true });
185
+ const decisionsPath = resolve(runsDir, 'decisions.json');
186
+ writeFileSync(decisionsPath, JSON.stringify(decisionRecorder.export(), null, 2), 'utf-8');
187
+
188
+ const { writeDeterminismReport } = await import('../core/determinism/report-writer.js');
189
+ writeDeterminismReport(runsDir, decisionRecorder);
190
+ }
191
+