@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
|
@@ -73,7 +73,6 @@ async function extractLabel(element) {
|
|
|
73
73
|
|
|
74
74
|
export async function discoverInteractions(page, baseOrigin, scanBudget = DEFAULT_SCAN_BUDGET) {
|
|
75
75
|
const currentUrl = page.url();
|
|
76
|
-
const interactions = [];
|
|
77
76
|
const seenElements = new Set();
|
|
78
77
|
|
|
79
78
|
const allInteractions = [];
|
|
@@ -246,7 +245,6 @@ export async function discoverInteractions(page, baseOrigin, scanBudget = DEFAUL
|
|
|
246
245
|
if (item.type === 'button' || item.type === 'link') {
|
|
247
246
|
const text = (item.text || '').trim().toLowerCase();
|
|
248
247
|
const label = (item.label || '').trim().toLowerCase();
|
|
249
|
-
const combined = `${text} ${label}`;
|
|
250
248
|
|
|
251
249
|
const isLogout = /^(logout|sign\s*out|signout|log\s*out)$/i.test(text) ||
|
|
252
250
|
/^(logout|sign\s*out|signout|log\s*out)$/i.test(label) ||
|
|
@@ -419,12 +417,16 @@ export async function discoverInteractions(page, baseOrigin, scanBudget = DEFAUL
|
|
|
419
417
|
try {
|
|
420
418
|
const box = await item.element.boundingBox();
|
|
421
419
|
if (box) {
|
|
420
|
+
// @ts-expect-error - Adding runtime properties to interaction object
|
|
422
421
|
item.boundingY = box.y;
|
|
422
|
+
// @ts-expect-error - Adding runtime properties to interaction object
|
|
423
423
|
item.boundingAvailable = true;
|
|
424
424
|
}
|
|
425
425
|
} catch (error) {
|
|
426
|
+
// @ts-expect-error - Adding runtime properties to interaction object
|
|
426
427
|
item.boundingAvailable = false;
|
|
427
428
|
}
|
|
429
|
+
// @ts-expect-error - Adding runtime properties to interaction object
|
|
428
430
|
item.priority = computePriority(item, viewportHeight);
|
|
429
431
|
}
|
|
430
432
|
|
|
@@ -457,7 +459,7 @@ export async function discoverInteractions(page, baseOrigin, scanBudget = DEFAUL
|
|
|
457
459
|
* Discover ALL interactions on a page (no priority cap).
|
|
458
460
|
* Used for full-site coverage traversal.
|
|
459
461
|
*/
|
|
460
|
-
export async function discoverAllInteractions(page, baseOrigin,
|
|
462
|
+
export async function discoverAllInteractions(page, baseOrigin, _scanBudget = DEFAULT_SCAN_BUDGET) {
|
|
461
463
|
const currentUrl = page.url();
|
|
462
464
|
const seenElements = new Set();
|
|
463
465
|
const allInteractions = [];
|
|
@@ -597,18 +599,28 @@ export async function discoverAllInteractions(page, baseOrigin, scanBudget = DEF
|
|
|
597
599
|
|
|
598
600
|
// Return ALL interactions (no priority cap)
|
|
599
601
|
return {
|
|
600
|
-
interactions: ordered.map(item =>
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
602
|
+
interactions: ordered.map(item => {
|
|
603
|
+
const mapped = {
|
|
604
|
+
type: item.type,
|
|
605
|
+
selector: item.selector,
|
|
606
|
+
label: item.label,
|
|
607
|
+
element: item.element,
|
|
608
|
+
isExternal: item.isExternal || false,
|
|
609
|
+
href: item.href,
|
|
610
|
+
text: item.text,
|
|
611
|
+
dataHref: item.dataHref,
|
|
612
|
+
// @ts-expect-error - dataDanger and dataDestructive are optional runtime properties on interaction objects
|
|
613
|
+
dataDanger: item.dataDanger || false,
|
|
614
|
+
// @ts-expect-error - dataDestructive is an optional runtime property on interaction objects
|
|
615
|
+
dataDestructive: item.dataDestructive || false
|
|
616
|
+
};
|
|
617
|
+
// hasPasswordInput only exists on form types
|
|
618
|
+
if (item.type === 'form' || item.type === 'login') {
|
|
619
|
+
// @ts-expect-error - hasPasswordInput is only on form/login types at runtime
|
|
620
|
+
mapped.hasPasswordInput = item.hasPasswordInput || false;
|
|
621
|
+
}
|
|
622
|
+
return mapped;
|
|
623
|
+
}),
|
|
612
624
|
coverage: {
|
|
613
625
|
candidatesDiscovered: allInteractions.length,
|
|
614
626
|
candidatesSelected: allInteractions.length,
|
|
@@ -12,7 +12,10 @@ 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 {
|
|
15
|
+
import { UIFeedbackDetector } from './ui-feedback-detector.js';
|
|
16
|
+
|
|
17
|
+
// Import CLICK_TIMEOUT_MS from human-driver (re-export needed)
|
|
18
|
+
const CLICK_TIMEOUT_MS = 2000;
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* SILENCE TRACKING: Mark timeout and record to silence tracker.
|
|
@@ -129,6 +132,7 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
|
|
|
129
132
|
freezeLikeThresholdMs: 3000
|
|
130
133
|
});
|
|
131
134
|
const humanDriver = new HumanBehaviorDriver({}, scanBudget);
|
|
135
|
+
const uiFeedbackDetector = new UIFeedbackDetector();
|
|
132
136
|
|
|
133
137
|
let networkWindowId = null;
|
|
134
138
|
let consoleWindowId = null;
|
|
@@ -136,13 +140,11 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
|
|
|
136
140
|
let loadingWindowData = null;
|
|
137
141
|
|
|
138
142
|
let uiBefore = {};
|
|
139
|
-
let stateBefore = null;
|
|
140
|
-
let sessionStateBefore = null;
|
|
141
143
|
|
|
142
144
|
try {
|
|
143
145
|
// Capture session state before interaction for auth-aware interactions
|
|
144
146
|
if (interaction.type === 'login' || interaction.type === 'logout') {
|
|
145
|
-
|
|
147
|
+
await humanDriver.captureSessionState(page);
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
if (Date.now() - startTime > scanBudget.maxScanDurationMs) {
|
|
@@ -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
|
|
@@ -263,7 +270,7 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
|
|
|
263
270
|
const locator = interaction.element;
|
|
264
271
|
// On file:// origins, avoid long navigation waits for simple link clicks
|
|
265
272
|
const isFileOrigin = baseOrigin && baseOrigin.startsWith('file:');
|
|
266
|
-
|
|
273
|
+
let shouldWaitForNavigation = (interaction.type === 'link' || interaction.type === 'form') && !isFileOrigin;
|
|
267
274
|
let navigationResult = null;
|
|
268
275
|
|
|
269
276
|
try {
|
|
@@ -300,7 +307,7 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
|
|
|
300
307
|
}
|
|
301
308
|
} else if (interaction.type === 'logout') {
|
|
302
309
|
// Logout action: click logout and observe session changes
|
|
303
|
-
const logoutResult = await humanDriver.
|
|
310
|
+
const logoutResult = await humanDriver.performLogout(page);
|
|
304
311
|
const sessionStateAfter = await humanDriver.captureSessionState(page);
|
|
305
312
|
trace.logout = {
|
|
306
313
|
clicked: logoutResult.clicked,
|
|
@@ -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 || {},
|
|
@@ -84,6 +84,12 @@ export class LoadingSensor {
|
|
|
84
84
|
// Set up interval to check loading state (every 100ms for deterministic detection)
|
|
85
85
|
const intervalId = setInterval(checkLoading, 100);
|
|
86
86
|
|
|
87
|
+
// CRITICAL: Unref the interval so it doesn't keep the process alive
|
|
88
|
+
// This allows tests to exit cleanly even if stopWindow() is not called
|
|
89
|
+
if (intervalId && intervalId.unref) {
|
|
90
|
+
intervalId.unref();
|
|
91
|
+
}
|
|
92
|
+
|
|
87
93
|
// Immediately check once
|
|
88
94
|
checkLoading();
|
|
89
95
|
|
|
@@ -109,7 +109,7 @@ export class NavigationSensor {
|
|
|
109
109
|
*
|
|
110
110
|
* @param {number} windowId - Window ID
|
|
111
111
|
* @param {Object} page - Playwright page
|
|
112
|
-
* @returns {
|
|
112
|
+
* @returns {Promise<any>} - Navigation summary
|
|
113
113
|
*/
|
|
114
114
|
async stopWindow(windowId, page) {
|
|
115
115
|
const state = this.windows.get(windowId);
|
|
@@ -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
|
+
|