@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.
- package/README.md +14 -18
- package/bin/verax.js +7 -0
- package/package.json +3 -3
- package/src/cli/commands/baseline.js +104 -0
- package/src/cli/commands/default.js +79 -25
- 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 +246 -35
- package/src/cli/commands/security-check.js +211 -0
- package/src/cli/commands/truth.js +114 -0
- package/src/cli/entry.js +304 -67
- 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/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/expectation-extractor.js +369 -73
- package/src/cli/util/findings-writer.js +126 -16
- package/src/cli/util/learn-writer.js +3 -1
- package/src/cli/util/observe-writer.js +3 -1
- package/src/cli/util/paths.js +3 -12
- package/src/cli/util/project-discovery.js +3 -0
- package/src/cli/util/project-writer.js +3 -1
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +1 -0
- 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/verax/cli/finding-explainer.js +56 -3
- 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/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 +30 -3
- 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/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/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/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/truth/truth.certificate.js +250 -0
- package/src/verax/core/ui-feedback-intelligence.js +515 -0
- package/src/verax/detect/confidence-engine.js +628 -40
- package/src/verax/detect/confidence-helper.js +33 -0
- package/src/verax/detect/detection-engine.js +18 -1
- package/src/verax/detect/dynamic-route-findings.js +335 -0
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +3 -1
- package/src/verax/detect/findings-writer.js +141 -5
- package/src/verax/detect/index.js +229 -5
- package/src/verax/detect/journey-stall-detector.js +558 -0
- package/src/verax/detect/route-findings.js +218 -0
- package/src/verax/detect/ui-feedback-findings.js +207 -0
- package/src/verax/detect/verdict-engine.js +57 -3
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/index.js +413 -45
- package/src/verax/learn/action-contract-extractor.js +682 -64
- package/src/verax/learn/route-validator.js +4 -1
- package/src/verax/observe/index.js +88 -843
- package/src/verax/observe/interaction-runner.js +25 -8
- 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/ui-feedback-detector.js +742 -0
- package/src/verax/observe/ui-signal-sensor.js +148 -2
- package/src/verax/scan-summary-writer.js +42 -8
- package/src/verax/shared/artifact-manager.js +8 -5
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/view-switch-rules.js +208 -0
|
@@ -12,6 +12,7 @@ import { FocusSensor } from './focus-sensor.js';
|
|
|
12
12
|
import { AriaSensor } from './aria-sensor.js';
|
|
13
13
|
import { TimingSensor } from './timing-sensor.js';
|
|
14
14
|
import { HumanBehaviorDriver } from './human-driver.js';
|
|
15
|
+
import { UIFeedbackDetector } from './ui-feedback-detector.js';
|
|
15
16
|
|
|
16
17
|
// Import CLICK_TIMEOUT_MS from human-driver (re-export needed)
|
|
17
18
|
const CLICK_TIMEOUT_MS = 2000;
|
|
@@ -131,6 +132,7 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
|
|
|
131
132
|
freezeLikeThresholdMs: 3000
|
|
132
133
|
});
|
|
133
134
|
const humanDriver = new HumanBehaviorDriver({}, scanBudget);
|
|
135
|
+
const uiFeedbackDetector = new UIFeedbackDetector();
|
|
134
136
|
|
|
135
137
|
let networkWindowId = null;
|
|
136
138
|
let consoleWindowId = null;
|
|
@@ -175,7 +177,11 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
|
|
|
175
177
|
}
|
|
176
178
|
trace.page.beforeTitle = beforeTitle;
|
|
177
179
|
|
|
178
|
-
|
|
180
|
+
const interactionTimestamp = timestamp || Date.now();
|
|
181
|
+
uiBefore = await uiSignalSensor.snapshot(page, interactionTimestamp, null).catch(() => ({}));
|
|
182
|
+
|
|
183
|
+
// UI FEEDBACK INTELLIGENCE: Capture before state for feedback detection
|
|
184
|
+
await uiFeedbackDetector.captureBefore(page, { targetSelector: interaction.selector });
|
|
179
185
|
|
|
180
186
|
// A11Y INTELLIGENCE: Capture focus and ARIA state before interaction
|
|
181
187
|
await focusSensor.captureBefore(page);
|
|
@@ -228,7 +234,8 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
|
|
|
228
234
|
|
|
229
235
|
const networkSummary = networkSensor.stopWindow(networkWindowId);
|
|
230
236
|
const consoleSummary = consoleSensor.stopWindow(consoleWindowId, page);
|
|
231
|
-
const
|
|
237
|
+
const interactionTimestamp = timestamp || Date.now();
|
|
238
|
+
const uiAfter = await uiSignalSensor.snapshot(page, interactionTimestamp, uiBefore);
|
|
232
239
|
const uiDiff = uiSignalSensor.diff(uiBefore, uiAfter);
|
|
233
240
|
|
|
234
241
|
// STATE INTELLIGENCE: Capture after state and compute diff
|
|
@@ -417,7 +424,8 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
|
|
|
417
424
|
|
|
418
425
|
// Record UI change if detected
|
|
419
426
|
if (uiSignalSensor) {
|
|
420
|
-
const
|
|
427
|
+
const interactionTimestamp = timestamp || Date.now();
|
|
428
|
+
const currentUi = await uiSignalSensor.snapshot(page, interactionTimestamp, uiBefore).catch(() => ({}));
|
|
421
429
|
const currentDiff = uiSignalSensor.diff(uiBefore, currentUi);
|
|
422
430
|
if (currentDiff.changed) {
|
|
423
431
|
timingSensor.recordUiChange();
|
|
@@ -473,7 +481,8 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
|
|
|
473
481
|
}
|
|
474
482
|
}
|
|
475
483
|
|
|
476
|
-
const
|
|
484
|
+
const interactionTimestamp = timestamp || Date.now();
|
|
485
|
+
const uiAfter = await uiSignalSensor.snapshot(page, interactionTimestamp, uiBefore).catch(() => ({}));
|
|
477
486
|
const uiDiff = uiSignalSensor.diff(uiBefore, uiAfter);
|
|
478
487
|
|
|
479
488
|
// STATE INTELLIGENCE: Capture after state and compute diff
|
|
@@ -597,9 +606,14 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
|
|
|
597
606
|
}
|
|
598
607
|
}
|
|
599
608
|
|
|
600
|
-
const
|
|
609
|
+
const interactionTimestamp = timestamp || Date.now();
|
|
610
|
+
const uiAfter = await uiSignalSensor.snapshot(page, interactionTimestamp, uiBefore);
|
|
601
611
|
const uiDiff = uiSignalSensor.diff(uiBefore, uiAfter);
|
|
602
612
|
|
|
613
|
+
// UI FEEDBACK INTELLIGENCE: Capture after state and compute feedback signals
|
|
614
|
+
await uiFeedbackDetector.captureAfter(page);
|
|
615
|
+
const uiFeedbackSignals = uiFeedbackDetector.computeFeedbackSignals();
|
|
616
|
+
|
|
603
617
|
// PERFORMANCE INTELLIGENCE: Record UI change in timing sensor if detected
|
|
604
618
|
if (timingSensor && uiDiff.changed) {
|
|
605
619
|
timingSensor.recordUiChange();
|
|
@@ -638,7 +652,8 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
|
|
|
638
652
|
available: stateDiff.available,
|
|
639
653
|
changed: stateDiff.changed,
|
|
640
654
|
storeType: storeType
|
|
641
|
-
}
|
|
655
|
+
},
|
|
656
|
+
uiFeedback: uiFeedbackSignals // UI FEEDBACK INTELLIGENCE: Add feedback detection signals
|
|
642
657
|
};
|
|
643
658
|
|
|
644
659
|
return trace;
|
|
@@ -661,7 +676,8 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
|
|
|
661
676
|
trace.sensors.console = consoleSensor.getEmptySummary();
|
|
662
677
|
}
|
|
663
678
|
|
|
664
|
-
const
|
|
679
|
+
const interactionTimestamp = timestamp || Date.now();
|
|
680
|
+
const uiAfter = await uiSignalSensor.snapshot(page, interactionTimestamp, uiBefore || {}).catch(() => ({}));
|
|
665
681
|
const uiDiff = uiSignalSensor.diff(uiBefore || {}, uiAfter);
|
|
666
682
|
|
|
667
683
|
// STATE INTELLIGENCE: Capture after state and compute diff
|
|
@@ -717,7 +733,8 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
|
|
|
717
733
|
trace.sensors.state = { available: false, changed: [], storeType: null };
|
|
718
734
|
}
|
|
719
735
|
|
|
720
|
-
const
|
|
736
|
+
const interactionTimestamp = timestamp || Date.now();
|
|
737
|
+
const uiAfter = await uiSignalSensor.snapshot(page, interactionTimestamp, uiBefore || {}).catch(() => ({}));
|
|
721
738
|
const uiDiff = uiSignalSensor.diff(uiBefore || {}, uiAfter || {});
|
|
722
739
|
trace.sensors.uiSignals = {
|
|
723
740
|
before: uiBefore || {},
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 21.3 — Observe Context Contract
|
|
3
|
+
*
|
|
4
|
+
* HARD CONTRACT: Defines the interface between observe-runner and observers
|
|
5
|
+
*
|
|
6
|
+
* RULES:
|
|
7
|
+
* - Observers MUST only access fields defined in this contract
|
|
8
|
+
* - Observers MUST NOT import from outside observe/*
|
|
9
|
+
* - Observers MUST NOT read files
|
|
10
|
+
* - Observers MUST NOT write artifacts directly
|
|
11
|
+
* - Observers MUST NOT mutate global state
|
|
12
|
+
* - Observers MUST propagate all errors (no silent catches)
|
|
13
|
+
*
|
|
14
|
+
* Runtime invariant checks enforce these rules.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ObserveContext — The context passed to all observers
|
|
19
|
+
*
|
|
20
|
+
* @typedef {Object} ObserveContext
|
|
21
|
+
* @property {import('playwright').Page} page - Playwright page instance
|
|
22
|
+
* @property {string} baseOrigin - Base origin for same-origin checks
|
|
23
|
+
* @property {Object} scanBudget - Scan budget configuration
|
|
24
|
+
* @property {number} startTime - Start time of the scan (timestamp)
|
|
25
|
+
* @property {Object} frontier - PageFrontier instance
|
|
26
|
+
* @property {Object|null} manifest - Manifest object (if available)
|
|
27
|
+
* @property {Object|null} expectationResults - Expectation execution results
|
|
28
|
+
* @property {boolean} incrementalMode - Whether incremental mode is enabled
|
|
29
|
+
* @property {Object|null} oldSnapshot - Previous snapshot (if available)
|
|
30
|
+
* @property {Object|null} snapshotDiff - Snapshot diff (if available)
|
|
31
|
+
* @property {string} currentUrl - Current page URL
|
|
32
|
+
* @property {string} screenshotsDir - Directory for screenshots
|
|
33
|
+
* @property {number} timestamp - Timestamp for this observation
|
|
34
|
+
* @property {Object} decisionRecorder - DecisionRecorder instance
|
|
35
|
+
* @property {Object} silenceTracker - SilenceTracker instance
|
|
36
|
+
* @property {Object} safetyFlags - Safety flags { allowWrites, allowRiskyActions, allowCrossOrigin }
|
|
37
|
+
* @property {Object} routeBudget - Route-specific budget (computed)
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* RunState — Mutable state passed between observers
|
|
42
|
+
*
|
|
43
|
+
* @typedef {Object} RunState
|
|
44
|
+
* @property {Array} traces - Array of interaction traces
|
|
45
|
+
* @property {Array} skippedInteractions - Array of skipped interactions
|
|
46
|
+
* @property {Array} observedExpectations - Array of observed expectations
|
|
47
|
+
* @property {number} totalInteractionsDiscovered - Total interactions discovered
|
|
48
|
+
* @property {number} totalInteractionsExecuted - Total interactions executed
|
|
49
|
+
* @property {Array} remainingInteractionsGaps - Remaining interaction gaps
|
|
50
|
+
* @property {boolean} navigatedToNewPage - Whether navigation occurred
|
|
51
|
+
* @property {string|null} navigatedPageUrl - URL of navigated page (if any)
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Observation — Result returned by an observer
|
|
56
|
+
*
|
|
57
|
+
* @typedef {Object} Observation
|
|
58
|
+
* @property {string} type - Type of observation (e.g., 'network_idle', 'console_error', 'ui_feedback')
|
|
59
|
+
* @property {string} scope - Scope of observation (e.g., 'page', 'interaction', 'navigation')
|
|
60
|
+
* @property {Object} data - Observation data
|
|
61
|
+
* @property {number} timestamp - Timestamp of observation
|
|
62
|
+
* @property {string} [url] - URL where observation occurred
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Forbidden imports that observers MUST NOT use
|
|
67
|
+
*/
|
|
68
|
+
const FORBIDDEN_IMPORTS = [
|
|
69
|
+
'fs',
|
|
70
|
+
'path',
|
|
71
|
+
'../core/determinism/report-writer',
|
|
72
|
+
'../core/scan-summary-writer',
|
|
73
|
+
'./traces-writer',
|
|
74
|
+
'./expectation-executor'
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Forbidden context fields that observers MUST NOT access
|
|
79
|
+
*/
|
|
80
|
+
const FORBIDDEN_CONTEXT_FIELDS = [
|
|
81
|
+
'projectDir',
|
|
82
|
+
'runId',
|
|
83
|
+
'writeFileSync',
|
|
84
|
+
'readFileSync',
|
|
85
|
+
'mkdirSync'
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validate that an observer result is a valid Observation
|
|
90
|
+
*
|
|
91
|
+
* @param {Object} observation - Observation to validate
|
|
92
|
+
* @param {string} observerName - Name of observer for error messages
|
|
93
|
+
* @throws {Error} If observation is invalid
|
|
94
|
+
*/
|
|
95
|
+
export function validateObservation(observation, observerName) {
|
|
96
|
+
if (!observation) {
|
|
97
|
+
throw new Error(`${observerName}: Observer returned null/undefined observation`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (typeof observation !== 'object') {
|
|
101
|
+
throw new Error(`${observerName}: Observer returned non-object observation: ${typeof observation}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!observation.type || typeof observation.type !== 'string') {
|
|
105
|
+
throw new Error(`${observerName}: Observation missing or invalid 'type' field`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!observation.scope || typeof observation.scope !== 'string') {
|
|
109
|
+
throw new Error(`${observerName}: Observation missing or invalid 'scope' field`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!observation.data || typeof observation.data !== 'object') {
|
|
113
|
+
throw new Error(`${observerName}: Observation missing or invalid 'data' field`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (typeof observation.timestamp !== 'number') {
|
|
117
|
+
throw new Error(`${observerName}: Observation missing or invalid 'timestamp' field`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Validate that context contains only allowed fields
|
|
123
|
+
*
|
|
124
|
+
* @param {Object} context - Context to validate
|
|
125
|
+
* @throws {Error} If context contains forbidden fields
|
|
126
|
+
*/
|
|
127
|
+
export function validateContext(context) {
|
|
128
|
+
for (const field of FORBIDDEN_CONTEXT_FIELDS) {
|
|
129
|
+
if (field in context) {
|
|
130
|
+
throw new Error(`ObserveContext contains forbidden field: ${field}. Observers must not access file I/O or project directories.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create a safe context for observers (removes forbidden fields)
|
|
137
|
+
*
|
|
138
|
+
* @param {Object} rawContext - Raw context from observe-runner
|
|
139
|
+
* @returns {ObserveContext} Safe context for observers
|
|
140
|
+
*/
|
|
141
|
+
export function createObserveContext(rawContext) {
|
|
142
|
+
const {
|
|
143
|
+
page,
|
|
144
|
+
baseOrigin,
|
|
145
|
+
scanBudget,
|
|
146
|
+
startTime,
|
|
147
|
+
frontier,
|
|
148
|
+
manifest,
|
|
149
|
+
expectationResults,
|
|
150
|
+
incrementalMode,
|
|
151
|
+
oldSnapshot,
|
|
152
|
+
snapshotDiff,
|
|
153
|
+
currentUrl,
|
|
154
|
+
screenshotsDir,
|
|
155
|
+
timestamp,
|
|
156
|
+
decisionRecorder,
|
|
157
|
+
silenceTracker,
|
|
158
|
+
safetyFlags,
|
|
159
|
+
routeBudget
|
|
160
|
+
} = rawContext;
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
page,
|
|
164
|
+
baseOrigin,
|
|
165
|
+
scanBudget,
|
|
166
|
+
startTime,
|
|
167
|
+
frontier,
|
|
168
|
+
manifest,
|
|
169
|
+
expectationResults,
|
|
170
|
+
incrementalMode,
|
|
171
|
+
oldSnapshot,
|
|
172
|
+
snapshotDiff,
|
|
173
|
+
currentUrl,
|
|
174
|
+
screenshotsDir,
|
|
175
|
+
timestamp,
|
|
176
|
+
decisionRecorder,
|
|
177
|
+
silenceTracker,
|
|
178
|
+
safetyFlags: safetyFlags || { allowWrites: false, allowRiskyActions: false, allowCrossOrigin: false },
|
|
179
|
+
routeBudget
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Observer execution order (FIXED - must not change)
|
|
185
|
+
*
|
|
186
|
+
* This order is critical for determinism and correctness.
|
|
187
|
+
*/
|
|
188
|
+
export const OBSERVER_ORDER = [
|
|
189
|
+
'navigation-observer', // 1. Navigation decisions first
|
|
190
|
+
'budget-observer', // 2. Budget checks before interactions
|
|
191
|
+
'interaction-observer', // 3. Interaction discovery and execution
|
|
192
|
+
'network-observer', // 4. Network state observation
|
|
193
|
+
'ui-feedback-observer', // 5. UI state observation
|
|
194
|
+
'console-observer', // 6. Console error observation
|
|
195
|
+
'coverage-observer' // 7. Coverage gap tracking
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get observer execution order
|
|
200
|
+
*
|
|
201
|
+
* @returns {Array<string>} Ordered list of observer names
|
|
202
|
+
*/
|
|
203
|
+
export function getObserverOrder() {
|
|
204
|
+
return [...OBSERVER_ORDER];
|
|
205
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 21.3 — Observe Helpers
|
|
3
|
+
*
|
|
4
|
+
* Helper functions extracted from observe/index.js to keep it slim
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { resolve } from 'path';
|
|
8
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
9
|
+
import { writeTraces } from './traces-writer.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Setup manifest and expectations
|
|
13
|
+
*/
|
|
14
|
+
export async function setupManifestAndExpectations(manifestPath, projectDir, page, url, screenshotsDir, scanBudget, startTime, silenceTracker) {
|
|
15
|
+
const { readFileSync, existsSync } = await import('fs');
|
|
16
|
+
const { loadPreviousSnapshot, saveSnapshot, buildSnapshot, compareSnapshots } = await import('../core/incremental-store.js');
|
|
17
|
+
const { executeProvenExpectations } = await import('./expectation-executor.js');
|
|
18
|
+
const { isProvenExpectation } = await import('../shared/expectation-prover.js');
|
|
19
|
+
|
|
20
|
+
let manifest = null;
|
|
21
|
+
let expectationResults = null;
|
|
22
|
+
let expectationCoverageGaps = [];
|
|
23
|
+
let incrementalMode = false;
|
|
24
|
+
let snapshotDiff = null;
|
|
25
|
+
let oldSnapshot = null;
|
|
26
|
+
|
|
27
|
+
if (manifestPath && existsSync(manifestPath)) {
|
|
28
|
+
try {
|
|
29
|
+
const manifestContent = readFileSync(manifestPath, 'utf-8');
|
|
30
|
+
manifest = JSON.parse(manifestContent);
|
|
31
|
+
|
|
32
|
+
oldSnapshot = loadPreviousSnapshot(projectDir);
|
|
33
|
+
if (oldSnapshot) {
|
|
34
|
+
const currentSnapshot = buildSnapshot(manifest, []);
|
|
35
|
+
snapshotDiff = compareSnapshots(oldSnapshot, currentSnapshot);
|
|
36
|
+
incrementalMode = !snapshotDiff.hasChanges;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const provenCount = (manifest.staticExpectations || []).filter(exp => isProvenExpectation(exp)).length;
|
|
40
|
+
if (provenCount > 0) {
|
|
41
|
+
expectationResults = await executeProvenExpectations(page, manifest, url, screenshotsDir, scanBudget, startTime, projectDir);
|
|
42
|
+
expectationCoverageGaps = expectationResults.coverageGaps || [];
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
silenceTracker.record({
|
|
46
|
+
scope: 'discovery',
|
|
47
|
+
reason: 'discovery_error',
|
|
48
|
+
description: 'Manifest load or expectation execution failed',
|
|
49
|
+
context: { error: err?.message },
|
|
50
|
+
impact: 'incomplete_check'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { manifest, expectationResults, expectationCoverageGaps, incrementalMode, snapshotDiff, oldSnapshot };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Process traversal results and build observation
|
|
60
|
+
*/
|
|
61
|
+
export async function processTraversalResults(traversalResult, expectationResults, expectationCoverageGaps, remainingInteractionsGaps, frontier, scanBudget, page, url, finalTraces, finalSkippedInteractions, finalObservedExpectations, silenceTracker, manifest, incrementalMode, snapshotDiff, projectDir, runId) {
|
|
62
|
+
// Combine all coverage gaps
|
|
63
|
+
const allCoverageGaps = [...expectationCoverageGaps];
|
|
64
|
+
if (remainingInteractionsGaps.length > 0) {
|
|
65
|
+
allCoverageGaps.push(...remainingInteractionsGaps.map(gap => ({
|
|
66
|
+
expectationId: null,
|
|
67
|
+
type: gap.interaction.type,
|
|
68
|
+
reason: gap.reason,
|
|
69
|
+
fromPath: gap.url,
|
|
70
|
+
source: null,
|
|
71
|
+
evidence: { interaction: gap.interaction }
|
|
72
|
+
})));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (frontier.frontierCapped) {
|
|
76
|
+
allCoverageGaps.push({
|
|
77
|
+
expectationId: null,
|
|
78
|
+
type: 'navigation',
|
|
79
|
+
reason: 'frontier_capped',
|
|
80
|
+
fromPath: page.url(),
|
|
81
|
+
source: null,
|
|
82
|
+
evidence: { message: `Frontier capped at ${scanBudget.maxUniqueUrls || 'unlimited'} unique URLs` }
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Build coverage object
|
|
87
|
+
const coverage = {
|
|
88
|
+
candidatesDiscovered: traversalResult.totalInteractionsDiscovered,
|
|
89
|
+
candidatesSelected: traversalResult.totalInteractionsExecuted,
|
|
90
|
+
cap: scanBudget.maxTotalInteractions,
|
|
91
|
+
capped: traversalResult.totalInteractionsExecuted >= scanBudget.maxTotalInteractions || remainingInteractionsGaps.length > 0,
|
|
92
|
+
pagesVisited: frontier.pagesVisited,
|
|
93
|
+
pagesDiscovered: frontier.pagesDiscovered,
|
|
94
|
+
skippedInteractions: finalSkippedInteractions.length,
|
|
95
|
+
interactionsDiscovered: traversalResult.totalInteractionsDiscovered,
|
|
96
|
+
interactionsExecuted: traversalResult.totalInteractionsExecuted
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Build warnings
|
|
100
|
+
const observeWarnings = [];
|
|
101
|
+
if (coverage.capped) {
|
|
102
|
+
observeWarnings.push({
|
|
103
|
+
code: 'INTERACTIONS_CAPPED',
|
|
104
|
+
message: `Interaction execution capped. Visited ${coverage.pagesVisited} pages, discovered ${coverage.pagesDiscovered}, executed ${coverage.candidatesSelected} of ${coverage.candidatesDiscovered} interactions. Coverage incomplete.`
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (finalSkippedInteractions.length > 0) {
|
|
108
|
+
observeWarnings.push({
|
|
109
|
+
code: 'INTERACTIONS_SKIPPED',
|
|
110
|
+
message: `Skipped ${finalSkippedInteractions.length} dangerous interactions`,
|
|
111
|
+
details: finalSkippedInteractions
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Append expectation traces
|
|
116
|
+
if (expectationResults?.results) {
|
|
117
|
+
for (const result of expectationResults.results) {
|
|
118
|
+
if (result.trace) {
|
|
119
|
+
result.trace.expectationDriven = true;
|
|
120
|
+
result.trace.expectationId = result.expectationId;
|
|
121
|
+
result.trace.expectationOutcome = result.outcome;
|
|
122
|
+
finalTraces.push(result.trace);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Write traces
|
|
128
|
+
const observation = writeTraces(projectDir, url, finalTraces, coverage, observeWarnings, finalObservedExpectations, silenceTracker, runId);
|
|
129
|
+
observation.silences = silenceTracker.getDetailedSummary();
|
|
130
|
+
|
|
131
|
+
// Add expectation execution results
|
|
132
|
+
if (expectationResults) {
|
|
133
|
+
observation.expectationExecution = {
|
|
134
|
+
totalProvenExpectations: expectationResults.totalProvenExpectations,
|
|
135
|
+
executedCount: expectationResults.executedCount,
|
|
136
|
+
coverageGapsCount: allCoverageGaps.length,
|
|
137
|
+
results: expectationResults.results.map(r => ({
|
|
138
|
+
expectationId: r.expectationId,
|
|
139
|
+
type: r.type,
|
|
140
|
+
fromPath: r.fromPath,
|
|
141
|
+
outcome: r.outcome,
|
|
142
|
+
reason: r.reason
|
|
143
|
+
}))
|
|
144
|
+
};
|
|
145
|
+
observation.expectationCoverageGaps = allCoverageGaps;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Add incremental mode metadata
|
|
149
|
+
if (manifest) {
|
|
150
|
+
const { buildSnapshot, saveSnapshot } = await import('../core/incremental-store.js');
|
|
151
|
+
const observedInteractions = finalTraces
|
|
152
|
+
.filter(t => t.interaction && !t.incremental)
|
|
153
|
+
.map(t => ({
|
|
154
|
+
type: t.interaction?.type,
|
|
155
|
+
selector: t.interaction?.selector,
|
|
156
|
+
url: t.before?.url || url
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
const currentSnapshot = buildSnapshot(manifest, observedInteractions);
|
|
160
|
+
saveSnapshot(projectDir, currentSnapshot, runId);
|
|
161
|
+
|
|
162
|
+
observation.incremental = {
|
|
163
|
+
enabled: incrementalMode,
|
|
164
|
+
snapshotDiff: snapshotDiff,
|
|
165
|
+
skippedInteractionsCount: finalSkippedInteractions.filter(s => s.reason === 'incremental_unchanged').length
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return observation;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Write determinism artifacts
|
|
174
|
+
*/
|
|
175
|
+
export async function writeDeterminismArtifacts(projectDir, runId, decisionRecorder) {
|
|
176
|
+
// PHASE 25: Write determinism contract
|
|
177
|
+
const { writeDeterminismContract } = await import('../core/determinism/contract-writer.js');
|
|
178
|
+
const { getRunArtifactDir } = await import('../core/run-id.js');
|
|
179
|
+
const runDir = getRunArtifactDir(projectDir, runId);
|
|
180
|
+
writeDeterminismContract(runDir, decisionRecorder);
|
|
181
|
+
if (!runId || !projectDir) return;
|
|
182
|
+
|
|
183
|
+
const runsDir = resolve(projectDir, '.verax', 'runs', runId);
|
|
184
|
+
mkdirSync(runsDir, { recursive: true });
|
|
185
|
+
const decisionsPath = resolve(runsDir, 'decisions.json');
|
|
186
|
+
writeFileSync(decisionsPath, JSON.stringify(decisionRecorder.export(), null, 2), 'utf-8');
|
|
187
|
+
|
|
188
|
+
const { writeDeterminismReport } = await import('../core/determinism/report-writer.js');
|
|
189
|
+
writeDeterminismReport(runsDir, decisionRecorder);
|
|
190
|
+
}
|
|
191
|
+
|