@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,230 @@
1
+ /**
2
+ * PHASE 21.3 — Observe Runner
3
+ *
4
+ * Extracted main traversal/interaction loop from observe/index.js
5
+ * Handles page traversal, interaction discovery, and execution
6
+ *
7
+ * NO file I/O - all artifacts written by caller
8
+ */
9
+
10
+ import { navigateToPage, discoverPageLinks, isAlreadyOnPage, markPageVisited } from './observers/navigation-observer.js';
11
+ import { discoverInteractions, checkAndSkipInteraction, executeInteraction } from './observers/interaction-observer.js';
12
+ import { checkBudget } from './observers/budget-observer.js';
13
+ import { createObserveContext } from './observe-context.js';
14
+
15
+ /**
16
+ * Run main traversal/interaction loop
17
+ *
18
+ * @param {Object} params - Parameters object
19
+ * @param {import('playwright').Page} params.page - Playwright page instance
20
+ * @param {string} params.url - Page URL
21
+ * @param {string} params.baseOrigin - Base origin for relative URLs
22
+ * @param {Object} params.scanBudget - Scan budget configuration
23
+ * @param {number} params.startTime - Scan start time
24
+ * @param {Object} params.frontier - Page frontier (URL queue)
25
+ * @param {Object|null} params.manifest - Manifest file data
26
+ * @param {Object|null} params.expectationResults - Expectation results
27
+ * @param {boolean} params.incrementalMode - Incremental mode flag
28
+ * @param {Object|null} params.oldSnapshot - Old snapshot data
29
+ * @param {Object|null} params.snapshotDiff - Snapshot diff
30
+ * @param {string} params.currentUrl - Current page URL
31
+ * @param {string} params.screenshotsDir - Screenshots directory
32
+ * @param {number} params.timestamp - Current timestamp
33
+ * @param {Object} params.decisionRecorder - DecisionRecorder instance
34
+ * @param {Object} params.silenceTracker - SilenceTracker instance
35
+ * @param {Array} params.traces - Interaction traces array
36
+ * @param {Array} params.skippedInteractions - Skipped interactions array
37
+ * @param {Array} params.observedExpectations - Observed expectations array
38
+ * @param {number} params.totalInteractionsDiscovered - Total discovered interactions
39
+ * @param {number} params.totalInteractionsExecuted - Total executed interactions
40
+ * @param {Array} params.remainingInteractionsGaps - Remaining interaction gaps
41
+ * @param {boolean} [params.allowWrites=false] - Allow file writes
42
+ * @param {boolean} [params.allowRiskyActions=false] - Allow risky actions
43
+ * @returns {Promise<Object>} { traces, skippedInteractions, observedExpectations, totalInteractionsDiscovered, totalInteractionsExecuted, remainingInteractionsGaps }
44
+ */
45
+ export async function runTraversalLoop(params) {
46
+ const {
47
+ page,
48
+ url: _url,
49
+ baseOrigin,
50
+ scanBudget,
51
+ startTime,
52
+ frontier,
53
+ manifest,
54
+ expectationResults,
55
+ incrementalMode,
56
+ oldSnapshot,
57
+ snapshotDiff,
58
+ currentUrl: _initialCurrentUrl,
59
+ screenshotsDir,
60
+ timestamp,
61
+ decisionRecorder,
62
+ silenceTracker,
63
+ traces,
64
+ skippedInteractions,
65
+ observedExpectations,
66
+ totalInteractionsDiscovered: initialTotalInteractionsDiscovered,
67
+ totalInteractionsExecuted: initialTotalInteractionsExecuted,
68
+ remainingInteractionsGaps,
69
+ allowWrites = false,
70
+ allowRiskyActions = false
71
+ } = params;
72
+
73
+ let totalInteractionsDiscovered = initialTotalInteractionsDiscovered;
74
+ let totalInteractionsExecuted = initialTotalInteractionsExecuted;
75
+ let nextPageUrl = frontier.getNextUrl();
76
+
77
+ // PHASE 21.3: Create observe context once (updated per page)
78
+ const baseContext = {
79
+ page,
80
+ baseOrigin,
81
+ scanBudget,
82
+ startTime,
83
+ frontier,
84
+ manifest,
85
+ expectationResults,
86
+ incrementalMode,
87
+ oldSnapshot,
88
+ snapshotDiff,
89
+ screenshotsDir,
90
+ timestamp,
91
+ decisionRecorder,
92
+ silenceTracker,
93
+ safetyFlags: { allowWrites, allowRiskyActions },
94
+ routeBudget: scanBudget // Updated per page
95
+ };
96
+
97
+ const runState = {
98
+ traces,
99
+ skippedInteractions,
100
+ observedExpectations,
101
+ totalInteractionsDiscovered,
102
+ totalInteractionsExecuted,
103
+ remainingInteractionsGaps,
104
+ navigatedToNewPage: false,
105
+ navigatedPageUrl: null
106
+ };
107
+
108
+ while (nextPageUrl && Date.now() - startTime < scanBudget.maxScanDurationMs) {
109
+ const currentUrl = page.url();
110
+ const context = createObserveContext({ ...baseContext, currentUrl, routeBudget: baseContext.routeBudget });
111
+
112
+ // PHASE 21.3: Check page limit using budget-observer
113
+ const pageLimitCheck = checkBudget(context, runState, {
114
+ remainingInteractions: 0,
115
+ currentTotalExecuted: 0,
116
+ limitType: 'pages'
117
+ });
118
+ if (pageLimitCheck.exceeded) break;
119
+
120
+ // Check if we're already on the target page (from navigation via link click)
121
+ const alreadyOnPageFlag = isAlreadyOnPage({ page, frontier }, nextPageUrl);
122
+
123
+ if (!alreadyOnPageFlag) {
124
+ // Navigate to next page
125
+ const navigated = await navigateToPage({ page, scanBudget, frontier, silenceTracker }, nextPageUrl);
126
+ if (!navigated) {
127
+ nextPageUrl = frontier.getNextUrl();
128
+ continue;
129
+ }
130
+ }
131
+
132
+ // Mark as visited and increment counter
133
+ markPageVisited({ frontier }, nextPageUrl, alreadyOnPageFlag);
134
+
135
+ // Discover ALL links on this page and add to frontier BEFORE executing interactions
136
+ await discoverPageLinks({ page, baseOrigin, frontier, silenceTracker });
137
+
138
+ // PHASE 21.3: Discover interactions using interaction-observer
139
+ const discoveryResult = await discoverInteractions({
140
+ page,
141
+ baseOrigin,
142
+ scanBudget,
143
+ manifest,
144
+ currentUrl
145
+ });
146
+ const { interactions: sortedInteractions, routeBudget } = discoveryResult;
147
+ runState.totalInteractionsDiscovered = totalInteractionsDiscovered += discoveryResult.totalDiscovered;
148
+ baseContext.routeBudget = routeBudget;
149
+ context.routeBudget = routeBudget;
150
+
151
+ // Execute discovered interactions on this page (sorted for determinism)
152
+ let navigatedToNewPage = false;
153
+ let navigatedPageUrl = null;
154
+ let remainingInteractionsStartIndex = 0;
155
+ let currentTotalInteractionsExecuted = totalInteractionsExecuted;
156
+
157
+ for (let i = 0; i < sortedInteractions.length; i++) {
158
+ // PHASE 21.3: Check budget limits using budget-observer
159
+ const remaining = sortedInteractions.length - i;
160
+ const budgetChecks = ['time', 'per_page', 'total'].map(limitType =>
161
+ checkBudget(context, runState, { limitType, remainingInteractions: remaining, currentTotalExecuted: currentTotalInteractionsExecuted })
162
+ );
163
+ if (budgetChecks.find(check => check.exceeded)) {
164
+ remainingInteractionsStartIndex = i;
165
+ break;
166
+ }
167
+
168
+ const interaction = sortedInteractions[i];
169
+
170
+ // PHASE 21.3: Check if interaction should be skipped
171
+ const skipResult = await checkAndSkipInteraction(
172
+ { page }, interaction, currentUrl, traces, skippedInteractions, silenceTracker,
173
+ frontier, incrementalMode, manifest, oldSnapshot, snapshotDiff, allowWrites, allowRiskyActions
174
+ );
175
+ if (skipResult.skip) continue;
176
+
177
+ // PHASE 21.3: Execute interaction
178
+ const executionResult = await executeInteraction(
179
+ { page, timestamp, screenshotsDir, routeBudget, silenceTracker },
180
+ interaction, currentTotalInteractionsExecuted, page.url(),
181
+ traces, observedExpectations, remainingInteractionsGaps, frontier, baseOrigin,
182
+ incrementalMode, expectationResults, startTime, scanBudget
183
+ );
184
+
185
+ // Update counters and handle navigation
186
+ if (executionResult.trace) currentTotalInteractionsExecuted++;
187
+ if (executionResult.repeatTrace) currentTotalInteractionsExecuted++;
188
+ runState.totalInteractionsExecuted = currentTotalInteractionsExecuted;
189
+
190
+ if (executionResult.navigated) {
191
+ navigatedToNewPage = true;
192
+ navigatedPageUrl = executionResult.navigatedUrl;
193
+ runState.navigatedToNewPage = true;
194
+ runState.navigatedPageUrl = executionResult.navigatedUrl;
195
+ break;
196
+ }
197
+ }
198
+
199
+ // Mark remaining interactions as COVERAGE_GAP if we stopped early
200
+ if (remainingInteractionsStartIndex > 0 && remainingInteractionsStartIndex < sortedInteractions.length && !navigatedToNewPage) {
201
+ const reason = currentTotalInteractionsExecuted >= scanBudget.maxTotalInteractions ? 'budget_exceeded' :
202
+ (currentTotalInteractionsExecuted >= routeBudget.maxInteractionsPerPage ? 'route_budget_exceeded' : 'budget_exceeded');
203
+ for (let j = remainingInteractionsStartIndex; j < sortedInteractions.length; j++) {
204
+ remainingInteractionsGaps.push({
205
+ interaction: { type: sortedInteractions[j].type, selector: sortedInteractions[j].selector, label: sortedInteractions[j].label },
206
+ reason, url: currentUrl
207
+ });
208
+ }
209
+ }
210
+
211
+ // If we navigated to a new page, stay on it and continue
212
+ if (navigatedToNewPage && navigatedPageUrl) {
213
+ nextPageUrl = navigatedPageUrl;
214
+ continue;
215
+ }
216
+ nextPageUrl = frontier.getNextUrl();
217
+ }
218
+
219
+ return {
220
+ traces: runState.traces,
221
+ skippedInteractions: runState.skippedInteractions,
222
+ observedExpectations: runState.observedExpectations,
223
+ totalInteractionsDiscovered: runState.totalInteractionsDiscovered,
224
+ totalInteractionsExecuted: runState.totalInteractionsExecuted,
225
+ remainingInteractionsGaps: runState.remainingInteractionsGaps
226
+ };
227
+ }
228
+
229
+ // PHASE 21.3: repeatObservedInteraction moved to interaction-observer.js
230
+
@@ -0,0 +1,185 @@
1
+ /**
2
+ * PHASE 21.3 — Budget Observer
3
+ *
4
+ * Responsibilities:
5
+ * - Budget limit checking
6
+ * - Truncation decision recording
7
+ * - Budget-related silence tracking
8
+ *
9
+ * NO file I/O
10
+ * NO side effects outside its scope
11
+ */
12
+
13
+ import { recordTruncation } from '../../core/determinism-model.js';
14
+
15
+ /**
16
+ * Check if budget limits are exceeded
17
+ *
18
+ * @param {Object} context - Observe context
19
+ * @param {Object} runState - Current run state
20
+ * @param {Object} options - Additional options
21
+ * @param {number} options.remainingInteractions - Remaining interactions count
22
+ * @param {number} options.currentTotalExecuted - Current total executed
23
+ * @param {string} options.limitType - Type of limit to check ('time', 'per_page', 'total', 'pages')
24
+ * @returns {Object} { exceeded: boolean, reason?: string, observation?: Object }
25
+ */
26
+ export function checkBudget(context, runState, options) {
27
+ const { scanBudget, startTime, routeBudget, decisionRecorder, silenceTracker, frontier, page } = context;
28
+ const { remainingInteractions = 0, currentTotalExecuted = 0, limitType } = options;
29
+ const now = Date.now();
30
+
31
+ if (limitType === 'time' && now - startTime > scanBudget.maxScanDurationMs) {
32
+ // PHASE 6: Record truncation decision
33
+ recordTruncation(decisionRecorder, 'time', {
34
+ limit: scanBudget.maxScanDurationMs,
35
+ elapsed: now - startTime
36
+ });
37
+
38
+ // Mark remaining interactions as COVERAGE_GAP
39
+ silenceTracker.record({
40
+ scope: 'interaction',
41
+ reason: 'scan_time_exceeded',
42
+ description: `Scan time limit (${scanBudget.maxScanDurationMs}ms) exceeded`,
43
+ context: {
44
+ elapsed: now - startTime,
45
+ maxDuration: scanBudget.maxScanDurationMs,
46
+ remainingInteractions
47
+ },
48
+ impact: 'blocks_nav',
49
+ count: remainingInteractions
50
+ });
51
+
52
+ return {
53
+ exceeded: true,
54
+ reason: 'scan_time_exceeded',
55
+ observation: {
56
+ type: 'budget_exceeded',
57
+ scope: 'scan',
58
+ data: {
59
+ limitType: 'time',
60
+ limit: scanBudget.maxScanDurationMs,
61
+ elapsed: now - startTime,
62
+ remainingInteractions
63
+ },
64
+ timestamp: now,
65
+ url: page.url()
66
+ }
67
+ };
68
+ }
69
+
70
+ if (limitType === 'per_page' && currentTotalExecuted >= routeBudget.maxInteractionsPerPage) {
71
+ // PHASE 6: Record truncation decision
72
+ recordTruncation(decisionRecorder, 'interactions', {
73
+ limit: routeBudget.maxInteractionsPerPage,
74
+ reached: currentTotalExecuted,
75
+ scope: 'per_page'
76
+ });
77
+
78
+ // Route-specific budget exceeded
79
+ silenceTracker.record({
80
+ scope: 'interaction',
81
+ reason: 'route_interaction_limit_exceeded',
82
+ description: `Reached max ${routeBudget.maxInteractionsPerPage} interactions per page`,
83
+ context: {
84
+ currentPage: page.url(),
85
+ executed: currentTotalExecuted,
86
+ maxPerPage: routeBudget.maxInteractionsPerPage,
87
+ remainingInteractions
88
+ },
89
+ impact: 'affects_expectations',
90
+ count: remainingInteractions
91
+ });
92
+
93
+ return {
94
+ exceeded: true,
95
+ reason: 'route_interaction_limit_exceeded',
96
+ observation: {
97
+ type: 'budget_exceeded',
98
+ scope: 'page',
99
+ data: {
100
+ limitType: 'per_page',
101
+ limit: routeBudget.maxInteractionsPerPage,
102
+ reached: currentTotalExecuted,
103
+ remainingInteractions
104
+ },
105
+ timestamp: now,
106
+ url: page.url()
107
+ }
108
+ };
109
+ }
110
+
111
+ if (limitType === 'total' && currentTotalExecuted >= scanBudget.maxTotalInteractions) {
112
+ // PHASE 6: Record truncation decision
113
+ recordTruncation(decisionRecorder, 'interactions', {
114
+ limit: scanBudget.maxTotalInteractions,
115
+ reached: currentTotalExecuted,
116
+ scope: 'total'
117
+ });
118
+
119
+ // Mark remaining interactions as COVERAGE_GAP with reason 'budget_exceeded'
120
+ silenceTracker.record({
121
+ scope: 'interaction',
122
+ reason: 'interaction_limit_exceeded',
123
+ description: `Reached max ${scanBudget.maxTotalInteractions} total interactions`,
124
+ context: {
125
+ executed: currentTotalExecuted,
126
+ maxTotal: scanBudget.maxTotalInteractions,
127
+ remainingInteractions
128
+ },
129
+ impact: 'blocks_nav',
130
+ count: remainingInteractions
131
+ });
132
+
133
+ return {
134
+ exceeded: true,
135
+ reason: 'interaction_limit_exceeded',
136
+ observation: {
137
+ type: 'budget_exceeded',
138
+ scope: 'scan',
139
+ data: {
140
+ limitType: 'total',
141
+ limit: scanBudget.maxTotalInteractions,
142
+ reached: currentTotalExecuted,
143
+ remainingInteractions
144
+ },
145
+ timestamp: now,
146
+ url: page.url()
147
+ }
148
+ };
149
+ }
150
+
151
+ if (limitType === 'pages' && frontier.isPageLimitExceeded()) {
152
+ // PHASE 6: Record truncation decision
153
+ recordTruncation(decisionRecorder, 'pages', {
154
+ limit: scanBudget.maxPages,
155
+ reached: frontier.pagesVisited
156
+ });
157
+
158
+ silenceTracker.record({
159
+ scope: 'page',
160
+ reason: 'page_limit_exceeded',
161
+ description: `Reached maximum of ${scanBudget.maxPages} pages visited`,
162
+ context: { pagesVisited: frontier.pagesVisited, maxPages: scanBudget.maxPages },
163
+ impact: 'blocks_nav'
164
+ });
165
+
166
+ return {
167
+ exceeded: true,
168
+ reason: 'page_limit_exceeded',
169
+ observation: {
170
+ type: 'budget_exceeded',
171
+ scope: 'page',
172
+ data: {
173
+ limitType: 'pages',
174
+ limit: scanBudget.maxPages,
175
+ reached: frontier.pagesVisited
176
+ },
177
+ timestamp: now,
178
+ url: page.url()
179
+ }
180
+ };
181
+ }
182
+
183
+ return { exceeded: false };
184
+ }
185
+
@@ -0,0 +1,102 @@
1
+ /**
2
+ * PHASE 21.3 — Console Observer
3
+ *
4
+ * Responsibilities:
5
+ * - Console error / warning capture
6
+ * - JS runtime error signals
7
+ *
8
+ * NO file I/O
9
+ * NO side effects outside its scope
10
+ */
11
+
12
+ import { ConsoleSensor } from '../console-sensor.js';
13
+
14
+ /**
15
+ * Observe console errors and warnings on current page
16
+ *
17
+ * @param {Object} context - Observe context
18
+ * @param {Object} _runState - Current run state
19
+ * @returns {Promise<Array<Object>>} Array of console observations
20
+ */
21
+ export async function observe(context, _runState) {
22
+ const { page, currentUrl, timestamp, silenceTracker } = context;
23
+ const observations = [];
24
+
25
+ try {
26
+ // Create a console sensor to observe current state
27
+ const consoleSensor = new ConsoleSensor();
28
+ const windowId = consoleSensor.startWindow(page);
29
+
30
+ // Wait a short time to capture any console messages
31
+ await page.waitForTimeout(100);
32
+
33
+ // Stop monitoring and get summary
34
+ const summary = consoleSensor.stopWindow(windowId, page);
35
+
36
+ // Create observation for console state
37
+ if (summary.hasErrors) {
38
+ observations.push({
39
+ type: 'console_errors',
40
+ scope: 'page',
41
+ data: {
42
+ errorCount: summary.errorCount,
43
+ consoleErrorCount: summary.consoleErrorCount,
44
+ pageErrorCount: summary.pageErrorCount,
45
+ unhandledRejectionCount: summary.unhandledRejectionCount,
46
+ lastErrors: summary.lastErrors
47
+ },
48
+ timestamp,
49
+ url: currentUrl
50
+ });
51
+
52
+ // Record console errors as silence
53
+ if (summary.consoleErrorCount > 0) {
54
+ silenceTracker.record({
55
+ scope: 'console',
56
+ reason: 'console_error',
57
+ description: `Console errors detected on page`,
58
+ context: {
59
+ errorCount: summary.consoleErrorCount,
60
+ pageUrl: currentUrl
61
+ },
62
+ impact: 'unknown_behavior',
63
+ count: summary.consoleErrorCount
64
+ });
65
+ }
66
+
67
+ if (summary.pageErrorCount > 0) {
68
+ silenceTracker.record({
69
+ scope: 'console',
70
+ reason: 'page_error',
71
+ description: `Page errors detected`,
72
+ context: {
73
+ errorCount: summary.pageErrorCount,
74
+ pageUrl: currentUrl
75
+ },
76
+ impact: 'unknown_behavior',
77
+ count: summary.pageErrorCount
78
+ });
79
+ }
80
+
81
+ if (summary.unhandledRejectionCount > 0) {
82
+ silenceTracker.record({
83
+ scope: 'console',
84
+ reason: 'unhandled_rejection',
85
+ description: `Unhandled promise rejections detected`,
86
+ context: {
87
+ errorCount: summary.unhandledRejectionCount,
88
+ pageUrl: currentUrl
89
+ },
90
+ impact: 'unknown_behavior',
91
+ count: summary.unhandledRejectionCount
92
+ });
93
+ }
94
+ }
95
+ } catch (error) {
96
+ // Propagate error - no silent catch
97
+ throw new Error(`Console observer failed: ${error.message}`);
98
+ }
99
+
100
+ return observations;
101
+ }
102
+
@@ -0,0 +1,107 @@
1
+ /**
2
+ * PHASE 21.3 — Coverage Observer
3
+ *
4
+ * Responsibilities:
5
+ * - Track coverage gaps
6
+ * - Build coverage summary
7
+ * - Track skipped interactions
8
+ * - NO file I/O
9
+ * - NO side effects outside its scope
10
+ */
11
+
12
+ import { recordTruncation as _recordTruncation } from '../../core/determinism-model.js';
13
+
14
+ /**
15
+ * Create coverage gap for remaining interactions
16
+ *
17
+ * @param {Object} context - Observe context
18
+ * @param {Array} remainingInteractions - Remaining interactions
19
+ * @param {number} startIndex - Start index of remaining interactions
20
+ * @param {string} reason - Reason for gap
21
+ * @returns {Array} Coverage gaps
22
+ */
23
+ export function createCoverageGaps(context, remainingInteractions, startIndex, reason) {
24
+ const { currentUrl } = context;
25
+ const gaps = [];
26
+
27
+ for (let j = startIndex; j < remainingInteractions.length; j++) {
28
+ gaps.push({
29
+ interaction: {
30
+ type: remainingInteractions[j].type,
31
+ selector: remainingInteractions[j].selector,
32
+ label: remainingInteractions[j].label
33
+ },
34
+ reason: reason,
35
+ url: currentUrl
36
+ });
37
+ }
38
+
39
+ return gaps;
40
+ }
41
+
42
+ /**
43
+ * Build coverage summary
44
+ *
45
+ * @param {Object} context - Observe context
46
+ * @param {number} totalDiscovered - Total interactions discovered
47
+ * @param {number} totalExecuted - Total interactions executed
48
+ * @param {number} skippedCount - Number of skipped interactions
49
+ * @param {Array} remainingGaps - Remaining interaction gaps
50
+ * @returns {Object} Coverage summary
51
+ */
52
+ export function buildCoverageSummary(context, totalDiscovered, totalExecuted, skippedCount, remainingGaps) {
53
+ const { scanBudget, frontier } = context;
54
+
55
+ return {
56
+ candidatesDiscovered: totalDiscovered,
57
+ candidatesSelected: totalExecuted,
58
+ cap: scanBudget.maxTotalInteractions,
59
+ capped: totalExecuted >= scanBudget.maxTotalInteractions || remainingGaps.length > 0,
60
+ pagesVisited: frontier.pagesVisited,
61
+ pagesDiscovered: frontier.pagesDiscovered,
62
+ skippedInteractions: skippedCount,
63
+ interactionsDiscovered: totalDiscovered,
64
+ interactionsExecuted: totalExecuted
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Create coverage gap for frontier capping
70
+ *
71
+ * @param {Object} context - Observe context
72
+ * @returns {Object} Coverage gap
73
+ */
74
+ export function createFrontierCappedGap(context) {
75
+ const { page, scanBudget } = context;
76
+
77
+ return {
78
+ expectationId: null,
79
+ type: 'navigation',
80
+ reason: 'frontier_capped',
81
+ fromPath: page.url(),
82
+ source: null,
83
+ evidence: {
84
+ message: `Frontier capped at ${scanBudget.maxUniqueUrls || 'unlimited'} unique URLs`
85
+ }
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Convert remaining interaction gaps to expectation coverage gaps
91
+ *
92
+ * @param {Array} remainingGaps - Remaining interaction gaps
93
+ * @returns {Array} Expectation coverage gaps
94
+ */
95
+ export function convertToExpectationCoverageGaps(remainingGaps) {
96
+ return remainingGaps.map(gap => ({
97
+ expectationId: null,
98
+ type: gap.interaction.type,
99
+ reason: gap.reason,
100
+ fromPath: gap.url,
101
+ source: null,
102
+ evidence: {
103
+ interaction: gap.interaction
104
+ }
105
+ }));
106
+ }
107
+