@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
@@ -0,0 +1,226 @@
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
19
+ * @param {import('playwright').Page} params.page
20
+ * @param {string} params.url
21
+ * @param {string} params.baseOrigin
22
+ * @param {Object} params.scanBudget
23
+ * @param {number} params.startTime
24
+ * @param {PageFrontier} params.frontier
25
+ * @param {Object|null} params.manifest
26
+ * @param {Object|null} params.expectationResults
27
+ * @param {boolean} params.incrementalMode
28
+ * @param {Object|null} params.oldSnapshot
29
+ * @param {Object|null} params.snapshotDiff
30
+ * @param {string} params.currentUrl
31
+ * @param {string} params.screenshotsDir
32
+ * @param {number} params.timestamp
33
+ * @param {Object} params.decisionRecorder - DecisionRecorder instance
34
+ * @param {Object} params.silenceTracker - SilenceTracker instance
35
+ * @param {Array} params.traces
36
+ * @param {Array} params.skippedInteractions
37
+ * @param {Array} params.observedExpectations
38
+ * @param {number} params.totalInteractionsDiscovered
39
+ * @param {number} params.totalInteractionsExecuted
40
+ * @param {Array} params.remainingInteractionsGaps
41
+ * @param {boolean} [params.allowWrites]
42
+ * @param {boolean} [params.allowRiskyActions]
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, { limitType: 'pages' });
114
+ if (pageLimitCheck.exceeded) break;
115
+
116
+ // Check if we're already on the target page (from navigation via link click)
117
+ const alreadyOnPageFlag = isAlreadyOnPage({ page, frontier }, nextPageUrl);
118
+
119
+ if (!alreadyOnPageFlag) {
120
+ // Navigate to next page
121
+ const navigated = await navigateToPage({ page, scanBudget, frontier, silenceTracker }, nextPageUrl);
122
+ if (!navigated) {
123
+ nextPageUrl = frontier.getNextUrl();
124
+ continue;
125
+ }
126
+ }
127
+
128
+ // Mark as visited and increment counter
129
+ markPageVisited({ frontier }, nextPageUrl, alreadyOnPageFlag);
130
+
131
+ // Discover ALL links on this page and add to frontier BEFORE executing interactions
132
+ await discoverPageLinks({ page, baseOrigin, frontier, silenceTracker });
133
+
134
+ // PHASE 21.3: Discover interactions using interaction-observer
135
+ const discoveryResult = await discoverInteractions({
136
+ page,
137
+ baseOrigin,
138
+ scanBudget,
139
+ manifest,
140
+ currentUrl
141
+ });
142
+ const { interactions: sortedInteractions, routeBudget } = discoveryResult;
143
+ runState.totalInteractionsDiscovered = totalInteractionsDiscovered += discoveryResult.totalDiscovered;
144
+ baseContext.routeBudget = routeBudget;
145
+ context.routeBudget = routeBudget;
146
+
147
+ // Execute discovered interactions on this page (sorted for determinism)
148
+ let navigatedToNewPage = false;
149
+ let navigatedPageUrl = null;
150
+ let remainingInteractionsStartIndex = 0;
151
+ let currentTotalInteractionsExecuted = totalInteractionsExecuted;
152
+
153
+ for (let i = 0; i < sortedInteractions.length; i++) {
154
+ // PHASE 21.3: Check budget limits using budget-observer
155
+ const remaining = sortedInteractions.length - i;
156
+ const budgetChecks = ['time', 'per_page', 'total'].map(limitType =>
157
+ checkBudget(context, runState, { limitType, remainingInteractions: remaining, currentTotalExecuted: currentTotalInteractionsExecuted })
158
+ );
159
+ if (budgetChecks.find(check => check.exceeded)) {
160
+ remainingInteractionsStartIndex = i;
161
+ break;
162
+ }
163
+
164
+ const interaction = sortedInteractions[i];
165
+
166
+ // PHASE 21.3: Check if interaction should be skipped
167
+ const skipResult = await checkAndSkipInteraction(
168
+ { page }, interaction, currentUrl, traces, skippedInteractions, silenceTracker,
169
+ frontier, incrementalMode, manifest, oldSnapshot, snapshotDiff, allowWrites, allowRiskyActions
170
+ );
171
+ if (skipResult.skip) continue;
172
+
173
+ // PHASE 21.3: Execute interaction
174
+ const executionResult = await executeInteraction(
175
+ { page, timestamp, screenshotsDir, routeBudget, silenceTracker },
176
+ interaction, currentTotalInteractionsExecuted, page.url(),
177
+ traces, observedExpectations, remainingInteractionsGaps, frontier, baseOrigin,
178
+ incrementalMode, expectationResults, startTime, scanBudget
179
+ );
180
+
181
+ // Update counters and handle navigation
182
+ if (executionResult.trace) currentTotalInteractionsExecuted++;
183
+ if (executionResult.repeatTrace) currentTotalInteractionsExecuted++;
184
+ runState.totalInteractionsExecuted = currentTotalInteractionsExecuted;
185
+
186
+ if (executionResult.navigated) {
187
+ navigatedToNewPage = true;
188
+ navigatedPageUrl = executionResult.navigatedUrl;
189
+ runState.navigatedToNewPage = true;
190
+ runState.navigatedPageUrl = executionResult.navigatedUrl;
191
+ break;
192
+ }
193
+ }
194
+
195
+ // Mark remaining interactions as COVERAGE_GAP if we stopped early
196
+ if (remainingInteractionsStartIndex > 0 && remainingInteractionsStartIndex < sortedInteractions.length && !navigatedToNewPage) {
197
+ const reason = currentTotalInteractionsExecuted >= scanBudget.maxTotalInteractions ? 'budget_exceeded' :
198
+ (currentTotalInteractionsExecuted >= routeBudget.maxInteractionsPerPage ? 'route_budget_exceeded' : 'budget_exceeded');
199
+ for (let j = remainingInteractionsStartIndex; j < sortedInteractions.length; j++) {
200
+ remainingInteractionsGaps.push({
201
+ interaction: { type: sortedInteractions[j].type, selector: sortedInteractions[j].selector, label: sortedInteractions[j].label },
202
+ reason, url: currentUrl
203
+ });
204
+ }
205
+ }
206
+
207
+ // If we navigated to a new page, stay on it and continue
208
+ if (navigatedToNewPage && navigatedPageUrl) {
209
+ nextPageUrl = navigatedPageUrl;
210
+ continue;
211
+ }
212
+ nextPageUrl = frontier.getNextUrl();
213
+ }
214
+
215
+ return {
216
+ traces: runState.traces,
217
+ skippedInteractions: runState.skippedInteractions,
218
+ observedExpectations: runState.observedExpectations,
219
+ totalInteractionsDiscovered: runState.totalInteractionsDiscovered,
220
+ totalInteractionsExecuted: runState.totalInteractionsExecuted,
221
+ remainingInteractionsGaps: runState.remainingInteractionsGaps
222
+ };
223
+ }
224
+
225
+ // PHASE 21.3: repeatObservedInteraction moved to interaction-observer.js
226
+
@@ -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 {ObserveContext} context - Observe context
19
+ * @param {RunState} 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?: Observation }
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 {ObserveContext} context - Observe context
18
+ * @param {RunState} runState - Current run state
19
+ * @returns {Promise<Array<Observation>>} 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 } from '../../core/determinism-model.js';
13
+
14
+ /**
15
+ * Create coverage gap for remaining interactions
16
+ *
17
+ * @param {ObserveContext} 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 {ObserveContext} 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 {ObserveContext} 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
+