@veraxhq/verax 0.2.0 → 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.
- package/README.md +14 -18
- package/bin/verax.js +7 -0
- package/package.json +15 -5
- package/src/cli/commands/baseline.js +104 -0
- package/src/cli/commands/default.js +323 -111
- package/src/cli/commands/doctor.js +36 -4
- package/src/cli/commands/ga.js +243 -0
- package/src/cli/commands/gates.js +95 -0
- package/src/cli/commands/inspect.js +131 -2
- package/src/cli/commands/release-check.js +213 -0
- package/src/cli/commands/run.js +498 -103
- package/src/cli/commands/security-check.js +211 -0
- package/src/cli/commands/truth.js +114 -0
- package/src/cli/entry.js +305 -68
- package/src/cli/util/angular-component-extractor.js +179 -0
- package/src/cli/util/angular-navigation-detector.js +141 -0
- package/src/cli/util/angular-network-detector.js +161 -0
- package/src/cli/util/angular-state-detector.js +162 -0
- package/src/cli/util/ast-interactive-detector.js +546 -0
- package/src/cli/util/ast-network-detector.js +603 -0
- package/src/cli/util/ast-usestate-detector.js +602 -0
- package/src/cli/util/bootstrap-guard.js +86 -0
- package/src/cli/util/detection-engine.js +4 -3
- package/src/cli/util/determinism-runner.js +123 -0
- package/src/cli/util/determinism-writer.js +129 -0
- package/src/cli/util/env-url.js +4 -0
- package/src/cli/util/events.js +76 -0
- package/src/cli/util/expectation-extractor.js +380 -74
- package/src/cli/util/findings-writer.js +126 -15
- package/src/cli/util/learn-writer.js +3 -1
- package/src/cli/util/observation-engine.js +69 -23
- package/src/cli/util/observe-writer.js +3 -1
- package/src/cli/util/paths.js +6 -14
- package/src/cli/util/project-discovery.js +23 -0
- package/src/cli/util/project-writer.js +3 -1
- package/src/cli/util/redact.js +2 -2
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +13 -1
- package/src/cli/util/svelte-navigation-detector.js +163 -0
- package/src/cli/util/svelte-network-detector.js +80 -0
- package/src/cli/util/svelte-sfc-extractor.js +147 -0
- package/src/cli/util/svelte-state-detector.js +243 -0
- package/src/cli/util/vue-navigation-detector.js +177 -0
- package/src/cli/util/vue-sfc-extractor.js +162 -0
- package/src/cli/util/vue-state-detector.js +215 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/doctor.js +2 -2
- package/src/verax/cli/finding-explainer.js +56 -3
- package/src/verax/cli/init.js +1 -1
- package/src/verax/cli/url-safety.js +12 -2
- package/src/verax/cli/wizard.js +13 -2
- package/src/verax/core/artifacts/registry.js +154 -0
- package/src/verax/core/artifacts/verifier.js +980 -0
- package/src/verax/core/baseline/baseline.enforcer.js +137 -0
- package/src/verax/core/baseline/baseline.snapshot.js +231 -0
- package/src/verax/core/budget-engine.js +1 -1
- package/src/verax/core/capabilities/gates.js +499 -0
- package/src/verax/core/capabilities/registry.js +475 -0
- package/src/verax/core/confidence/confidence-compute.js +137 -0
- package/src/verax/core/confidence/confidence-invariants.js +234 -0
- package/src/verax/core/confidence/confidence-report-writer.js +112 -0
- package/src/verax/core/confidence/confidence-weights.js +44 -0
- package/src/verax/core/confidence/confidence.defaults.js +65 -0
- package/src/verax/core/confidence/confidence.loader.js +79 -0
- package/src/verax/core/confidence/confidence.schema.js +94 -0
- package/src/verax/core/confidence-engine-refactor.js +484 -0
- package/src/verax/core/confidence-engine.js +486 -0
- package/src/verax/core/confidence-engine.js.backup +471 -0
- package/src/verax/core/contracts/index.js +29 -0
- package/src/verax/core/contracts/types.js +185 -0
- package/src/verax/core/contracts/validators.js +381 -0
- package/src/verax/core/decision-snapshot.js +31 -4
- package/src/verax/core/decisions/decision.trace.js +276 -0
- package/src/verax/core/determinism/contract-writer.js +89 -0
- package/src/verax/core/determinism/contract.js +139 -0
- package/src/verax/core/determinism/diff.js +364 -0
- package/src/verax/core/determinism/engine.js +221 -0
- package/src/verax/core/determinism/finding-identity.js +148 -0
- package/src/verax/core/determinism/normalize.js +438 -0
- package/src/verax/core/determinism/report-writer.js +92 -0
- package/src/verax/core/determinism/run-fingerprint.js +118 -0
- package/src/verax/core/determinism-model.js +35 -6
- package/src/verax/core/dynamic-route-intelligence.js +528 -0
- package/src/verax/core/evidence/evidence-capture-service.js +307 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
- package/src/verax/core/evidence-builder.js +487 -0
- package/src/verax/core/execution-mode-context.js +77 -0
- package/src/verax/core/execution-mode-detector.js +190 -0
- package/src/verax/core/failures/exit-codes.js +86 -0
- package/src/verax/core/failures/failure-summary.js +76 -0
- package/src/verax/core/failures/failure.factory.js +225 -0
- package/src/verax/core/failures/failure.ledger.js +132 -0
- package/src/verax/core/failures/failure.types.js +196 -0
- package/src/verax/core/failures/index.js +10 -0
- package/src/verax/core/ga/ga-report-writer.js +43 -0
- package/src/verax/core/ga/ga.artifact.js +49 -0
- package/src/verax/core/ga/ga.contract.js +434 -0
- package/src/verax/core/ga/ga.enforcer.js +86 -0
- package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
- package/src/verax/core/guardrails/policy.defaults.js +210 -0
- package/src/verax/core/guardrails/policy.loader.js +83 -0
- package/src/verax/core/guardrails/policy.schema.js +110 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
- package/src/verax/core/guardrails-engine.js +505 -0
- package/src/verax/core/incremental-store.js +15 -7
- package/src/verax/core/observe/run-timeline.js +316 -0
- package/src/verax/core/perf/perf.contract.js +186 -0
- package/src/verax/core/perf/perf.display.js +65 -0
- package/src/verax/core/perf/perf.enforcer.js +91 -0
- package/src/verax/core/perf/perf.monitor.js +209 -0
- package/src/verax/core/perf/perf.report.js +198 -0
- package/src/verax/core/pipeline-tracker.js +238 -0
- package/src/verax/core/product-definition.js +127 -0
- package/src/verax/core/release/provenance.builder.js +271 -0
- package/src/verax/core/release/release-report-writer.js +40 -0
- package/src/verax/core/release/release.enforcer.js +159 -0
- package/src/verax/core/release/reproducibility.check.js +221 -0
- package/src/verax/core/release/sbom.builder.js +283 -0
- package/src/verax/core/replay-validator.js +4 -4
- package/src/verax/core/replay.js +1 -1
- package/src/verax/core/report/cross-index.js +192 -0
- package/src/verax/core/report/human-summary.js +222 -0
- package/src/verax/core/route-intelligence.js +419 -0
- package/src/verax/core/security/secrets.scan.js +326 -0
- package/src/verax/core/security/security-report.js +50 -0
- package/src/verax/core/security/security.enforcer.js +124 -0
- package/src/verax/core/security/supplychain.defaults.json +38 -0
- package/src/verax/core/security/supplychain.policy.js +326 -0
- package/src/verax/core/security/vuln.scan.js +265 -0
- package/src/verax/core/silence-impact.js +1 -1
- package/src/verax/core/silence-model.js +9 -7
- package/src/verax/core/truth/truth.certificate.js +250 -0
- package/src/verax/core/ui-feedback-intelligence.js +515 -0
- package/src/verax/detect/comparison.js +8 -3
- package/src/verax/detect/confidence-engine.js +645 -57
- package/src/verax/detect/confidence-helper.js +33 -0
- package/src/verax/detect/detection-engine.js +19 -2
- package/src/verax/detect/dynamic-route-findings.js +335 -0
- package/src/verax/detect/evidence-index.js +15 -65
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +56 -3
- package/src/verax/detect/explanation-helpers.js +1 -1
- package/src/verax/detect/finding-detector.js +2 -2
- package/src/verax/detect/findings-writer.js +149 -20
- package/src/verax/detect/flow-detector.js +4 -4
- package/src/verax/detect/index.js +265 -15
- package/src/verax/detect/interactive-findings.js +3 -4
- package/src/verax/detect/journey-stall-detector.js +558 -0
- package/src/verax/detect/route-findings.js +218 -0
- package/src/verax/detect/signal-mapper.js +2 -2
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/ui-feedback-findings.js +207 -0
- package/src/verax/detect/verdict-engine.js +61 -9
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +413 -33
- package/src/verax/intel/effect-detector.js +1 -1
- package/src/verax/intel/index.js +2 -2
- package/src/verax/intel/route-extractor.js +3 -3
- package/src/verax/intel/vue-navigation-extractor.js +81 -18
- package/src/verax/intel/vue-router-extractor.js +4 -2
- package/src/verax/learn/action-contract-extractor.js +684 -66
- package/src/verax/learn/ast-contract-extractor.js +53 -1
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +28 -14
- package/src/verax/learn/route-extractor.js +1 -1
- package/src/verax/learn/route-validator.js +12 -8
- package/src/verax/learn/state-extractor.js +1 -1
- package/src/verax/learn/static-extractor-navigation.js +1 -1
- package/src/verax/learn/static-extractor-validation.js +2 -2
- package/src/verax/learn/static-extractor.js +8 -7
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/browser.js +22 -3
- package/src/verax/observe/console-sensor.js +2 -2
- package/src/verax/observe/expectation-executor.js +2 -1
- package/src/verax/observe/focus-sensor.js +1 -1
- package/src/verax/observe/human-driver.js +29 -10
- package/src/verax/observe/index.js +92 -844
- package/src/verax/observe/interaction-discovery.js +27 -15
- package/src/verax/observe/interaction-runner.js +31 -14
- package/src/verax/observe/loading-sensor.js +6 -0
- package/src/verax/observe/navigation-sensor.js +1 -1
- package/src/verax/observe/observe-context.js +205 -0
- package/src/verax/observe/observe-helpers.js +191 -0
- package/src/verax/observe/observe-runner.js +226 -0
- package/src/verax/observe/observers/budget-observer.js +185 -0
- package/src/verax/observe/observers/console-observer.js +102 -0
- package/src/verax/observe/observers/coverage-observer.js +107 -0
- package/src/verax/observe/observers/interaction-observer.js +471 -0
- package/src/verax/observe/observers/navigation-observer.js +132 -0
- package/src/verax/observe/observers/network-observer.js +87 -0
- package/src/verax/observe/observers/safety-observer.js +82 -0
- package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
- package/src/verax/observe/settle.js +1 -0
- package/src/verax/observe/state-sensor.js +8 -4
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/traces-writer.js +27 -16
- package/src/verax/observe/ui-feedback-detector.js +742 -0
- package/src/verax/observe/ui-signal-sensor.js +155 -2
- package/src/verax/scan-summary-writer.js +46 -9
- package/src/verax/shared/artifact-manager.js +9 -6
- package/src/verax/shared/budget-profiles.js +2 -2
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/config-loader.js +1 -2
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/dynamic-route-utils.js +12 -6
- package/src/verax/shared/retry-policy.js +1 -6
- package/src/verax/shared/root-artifacts.js +1 -1
- package/src/verax/shared/view-switch-rules.js +208 -0
- package/src/verax/shared/zip-artifacts.js +1 -0
- package/src/verax/validate/context-validator.js +1 -1
- package/src/verax/observe/index.js.backup +0 -1
- package/src/verax/validate/context-validator.js.bak +0 -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
|
+
|