@veraxhq/verax 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -6
- package/bin/verax.js +11 -11
- package/package.json +29 -8
- package/src/cli/commands/baseline.js +103 -0
- package/src/cli/commands/default.js +51 -6
- package/src/cli/commands/doctor.js +29 -0
- package/src/cli/commands/ga.js +246 -0
- package/src/cli/commands/gates.js +95 -0
- package/src/cli/commands/inspect.js +4 -2
- package/src/cli/commands/release-check.js +215 -0
- package/src/cli/commands/run.js +45 -6
- package/src/cli/commands/security-check.js +212 -0
- package/src/cli/commands/truth.js +113 -0
- package/src/cli/entry.js +30 -20
- 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 +544 -0
- package/src/cli/util/ast-network-detector.js +603 -0
- package/src/cli/util/ast-promise-extractor.js +581 -0
- package/src/cli/util/ast-usestate-detector.js +602 -0
- package/src/cli/util/atomic-write.js +12 -1
- package/src/cli/util/bootstrap-guard.js +86 -0
- package/src/cli/util/console-reporter.js +72 -0
- package/src/cli/util/detection-engine.js +105 -41
- package/src/cli/util/determinism-runner.js +124 -0
- package/src/cli/util/determinism-writer.js +129 -0
- package/src/cli/util/digest-engine.js +359 -0
- package/src/cli/util/dom-diff.js +226 -0
- package/src/cli/util/evidence-engine.js +287 -0
- package/src/cli/util/expectation-extractor.js +151 -5
- package/src/cli/util/findings-writer.js +3 -0
- package/src/cli/util/framework-detector.js +572 -0
- package/src/cli/util/idgen.js +1 -1
- package/src/cli/util/interaction-planner.js +529 -0
- package/src/cli/util/learn-writer.js +2 -0
- package/src/cli/util/ledger-writer.js +110 -0
- package/src/cli/util/monorepo-resolver.js +162 -0
- package/src/cli/util/observation-engine.js +127 -278
- package/src/cli/util/observe-writer.js +2 -0
- package/src/cli/util/project-discovery.js +284 -0
- package/src/cli/util/project-writer.js +2 -0
- package/src/cli/util/run-id.js +23 -27
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/run-result.js +778 -0
- package/src/cli/util/selector-resolver.js +235 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +2 -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 +146 -0
- package/src/cli/util/svelte-state-detector.js +242 -0
- package/src/cli/util/trust-activation-integration.js +496 -0
- package/src/cli/util/trust-activation-wrapper.js +85 -0
- package/src/cli/util/trust-integration-hooks.js +164 -0
- package/src/cli/util/types.js +153 -0
- package/src/cli/util/url-validation.js +40 -0
- package/src/cli/util/vue-navigation-detector.js +178 -0
- package/src/cli/util/vue-sfc-extractor.js +161 -0
- package/src/cli/util/vue-state-detector.js +215 -0
- package/src/types/fs-augment.d.ts +23 -0
- package/src/types/global.d.ts +137 -0
- package/src/types/internal-types.d.ts +35 -0
- package/src/verax/cli/init.js +4 -18
- package/src/verax/core/action-classifier.js +4 -3
- package/src/verax/core/artifacts/registry.js +139 -0
- package/src/verax/core/artifacts/verifier.js +990 -0
- package/src/verax/core/baseline/baseline.enforcer.js +137 -0
- package/src/verax/core/baseline/baseline.snapshot.js +233 -0
- package/src/verax/core/capabilities/gates.js +505 -0
- package/src/verax/core/capabilities/registry.js +475 -0
- package/src/verax/core/confidence/confidence-compute.js +144 -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 +80 -0
- package/src/verax/core/confidence/confidence.schema.js +94 -0
- package/src/verax/core/confidence-engine-refactor.js +489 -0
- package/src/verax/core/confidence-engine.js +625 -0
- package/src/verax/core/contracts/index.js +29 -0
- package/src/verax/core/contracts/types.js +186 -0
- package/src/verax/core/contracts/validators.js +456 -0
- package/src/verax/core/decisions/decision.trace.js +278 -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 +405 -0
- package/src/verax/core/determinism/engine.js +222 -0
- package/src/verax/core/determinism/finding-identity.js +149 -0
- package/src/verax/core/determinism/normalize.js +466 -0
- package/src/verax/core/determinism/report-writer.js +93 -0
- package/src/verax/core/determinism/run-fingerprint.js +123 -0
- package/src/verax/core/dynamic-route-intelligence.js +529 -0
- package/src/verax/core/evidence/evidence-capture-service.js +308 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +166 -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 +192 -0
- package/src/verax/core/failures/exit-codes.js +88 -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 +133 -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 +435 -0
- package/src/verax/core/ga/ga.enforcer.js +87 -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 +84 -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 +1 -0
- package/src/verax/core/integrity/budget.js +138 -0
- package/src/verax/core/integrity/determinism.js +342 -0
- package/src/verax/core/integrity/integrity.js +208 -0
- package/src/verax/core/integrity/poisoning.js +108 -0
- package/src/verax/core/integrity/transaction.js +140 -0
- package/src/verax/core/observe/run-timeline.js +318 -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 +200 -0
- package/src/verax/core/pipeline-tracker.js +243 -0
- package/src/verax/core/product-definition.js +127 -0
- package/src/verax/core/release/provenance.builder.js +130 -0
- package/src/verax/core/release/release-report-writer.js +40 -0
- package/src/verax/core/release/release.enforcer.js +164 -0
- package/src/verax/core/release/reproducibility.check.js +222 -0
- package/src/verax/core/release/sbom.builder.js +292 -0
- package/src/verax/core/replay-validator.js +2 -0
- package/src/verax/core/replay.js +4 -0
- package/src/verax/core/report/cross-index.js +195 -0
- package/src/verax/core/report/human-summary.js +362 -0
- package/src/verax/core/route-intelligence.js +420 -0
- package/src/verax/core/run-id.js +6 -3
- package/src/verax/core/run-manifest.js +4 -3
- package/src/verax/core/security/secrets.scan.js +329 -0
- package/src/verax/core/security/security-report.js +50 -0
- package/src/verax/core/security/security.enforcer.js +128 -0
- package/src/verax/core/security/supplychain.defaults.json +38 -0
- package/src/verax/core/security/supplychain.policy.js +334 -0
- package/src/verax/core/security/vuln.scan.js +265 -0
- package/src/verax/core/truth/truth.certificate.js +252 -0
- package/src/verax/core/ui-feedback-intelligence.js +481 -0
- package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
- package/src/verax/detect/confidence-engine.js +62 -34
- package/src/verax/detect/confidence-helper.js +34 -0
- package/src/verax/detect/dynamic-route-findings.js +338 -0
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +2 -2
- package/src/verax/detect/failure-cause-inference.js +293 -0
- package/src/verax/detect/findings-writer.js +131 -35
- package/src/verax/detect/flow-detector.js +2 -2
- package/src/verax/detect/form-silent-failure.js +98 -0
- package/src/verax/detect/index.js +46 -5
- package/src/verax/detect/invariants-enforcer.js +147 -0
- package/src/verax/detect/journey-stall-detector.js +558 -0
- package/src/verax/detect/navigation-silent-failure.js +82 -0
- package/src/verax/detect/problem-aggregator.js +361 -0
- package/src/verax/detect/route-findings.js +219 -0
- package/src/verax/detect/summary-writer.js +477 -0
- package/src/verax/detect/test-failure-cause-inference.js +314 -0
- package/src/verax/detect/ui-feedback-findings.js +207 -0
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/flow/flow-engine.js +2 -1
- package/src/verax/flow/flow-spec.js +0 -6
- package/src/verax/index.js +4 -0
- package/src/verax/intel/ts-program.js +1 -0
- package/src/verax/intel/vue-navigation-extractor.js +3 -0
- package/src/verax/learn/action-contract-extractor.js +3 -0
- package/src/verax/learn/ast-contract-extractor.js +1 -1
- package/src/verax/learn/flow-extractor.js +1 -0
- package/src/verax/learn/project-detector.js +5 -0
- package/src/verax/learn/react-router-extractor.js +2 -0
- package/src/verax/learn/source-instrumenter.js +1 -0
- package/src/verax/learn/state-extractor.js +2 -1
- package/src/verax/learn/static-extractor.js +1 -0
- package/src/verax/observe/coverage-gaps.js +132 -0
- package/src/verax/observe/expectation-handler.js +126 -0
- package/src/verax/observe/incremental-skip.js +46 -0
- package/src/verax/observe/index.js +51 -155
- package/src/verax/observe/interaction-executor.js +192 -0
- package/src/verax/observe/interaction-runner.js +782 -513
- package/src/verax/observe/network-firewall.js +86 -0
- package/src/verax/observe/observation-builder.js +169 -0
- package/src/verax/observe/observe-context.js +205 -0
- package/src/verax/observe/observe-helpers.js +192 -0
- package/src/verax/observe/observe-runner.js +230 -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/page-traversal.js +138 -0
- package/src/verax/observe/snapshot-ops.js +94 -0
- package/src/verax/observe/ui-feedback-detector.js +742 -0
- package/src/verax/scan-summary-writer.js +2 -0
- package/src/verax/shared/artifact-manager.js +25 -5
- package/src/verax/shared/caching.js +1 -0
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/expectation-tracker.js +1 -0
- package/src/verax/shared/view-switch-rules.js +208 -0
- package/src/verax/shared/zip-artifacts.js +6 -0
- package/src/verax/shared/config-loader.js +0 -169
- /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import { resolveSelector, findSubmitButton, isElementInteractable } from './selector-resolver.js';
|
|
2
|
+
import { createEvidenceBundle } from './evidence-engine.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Interaction Planner
|
|
6
|
+
* Converts extracted promises into executable Playwright interactions
|
|
7
|
+
* with bounded timeouts and evidence capture
|
|
8
|
+
*
|
|
9
|
+
* H5: Added read-only safety mode by default
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const GLOBAL_TIMEOUT = 5 * 60 * 1000; // 5 minutes total
|
|
13
|
+
const _PER_PROMISE_TIMEOUT = 15 * 1000; // 15 seconds per promise
|
|
14
|
+
const WAIT_FOR_EFFECT_TIMEOUT = 3000; // 3 seconds to observe effect
|
|
15
|
+
|
|
16
|
+
const MUTATING_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];
|
|
17
|
+
|
|
18
|
+
export class InteractionPlanner {
|
|
19
|
+
constructor(page, evidencePath, _options = {}) {
|
|
20
|
+
this.page = page;
|
|
21
|
+
this.evidencePath = evidencePath;
|
|
22
|
+
this.startTime = Date.now();
|
|
23
|
+
this.networkEvents = [];
|
|
24
|
+
this.consoleEvents = [];
|
|
25
|
+
this.attempts = [];
|
|
26
|
+
|
|
27
|
+
// CONSTITUTIONAL: Read-only mode enforced (no writes allowed)
|
|
28
|
+
this.blockedRequests = [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if we're still within budget
|
|
33
|
+
*/
|
|
34
|
+
isWithinBudget() {
|
|
35
|
+
return Date.now() - this.startTime < GLOBAL_TIMEOUT;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* H5: Check if request method is mutating
|
|
40
|
+
*/
|
|
41
|
+
isMutatingRequest(method) {
|
|
42
|
+
return MUTATING_METHODS.includes((method || 'GET').toUpperCase());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* CONSTITUTIONAL: Block all mutating requests (read-only mode)
|
|
47
|
+
*/
|
|
48
|
+
shouldBlockRequest(method) {
|
|
49
|
+
return this.isMutatingRequest(method);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* H5: Get blocked requests
|
|
54
|
+
*/
|
|
55
|
+
getBlockedRequests() {
|
|
56
|
+
return this.blockedRequests;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Plan and execute a single promise
|
|
61
|
+
*/
|
|
62
|
+
async executeSinglePromise(promise, expNum) {
|
|
63
|
+
const attempt = {
|
|
64
|
+
id: promise.id,
|
|
65
|
+
expNum,
|
|
66
|
+
category: promise.category || promise.type,
|
|
67
|
+
attempted: false,
|
|
68
|
+
reason: null,
|
|
69
|
+
action: null,
|
|
70
|
+
evidence: null,
|
|
71
|
+
signals: null,
|
|
72
|
+
cause: null, // NEW: precise cause taxonomy
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (!this.isWithinBudget()) {
|
|
76
|
+
attempt.reason = 'global-timeout-exceeded';
|
|
77
|
+
attempt.cause = 'timeout';
|
|
78
|
+
this.attempts.push(attempt);
|
|
79
|
+
return attempt;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const _startTime = Date.now();
|
|
83
|
+
const bundle = createEvidenceBundle(promise, expNum, this.evidencePath);
|
|
84
|
+
bundle.timing.startedAt = new Date().toISOString();
|
|
85
|
+
bundle.actionStartTime = new Date().toISOString(); // For network correlation
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// Capture before state
|
|
89
|
+
await bundle.captureBeforeState(this.page);
|
|
90
|
+
const urlBefore = this.page.url();
|
|
91
|
+
const consoleErrorsCountBefore = this.consoleEvents.length;
|
|
92
|
+
|
|
93
|
+
// Execute interaction based on type
|
|
94
|
+
let actionResult = null;
|
|
95
|
+
|
|
96
|
+
if (promise.category === 'button' || promise.type === 'navigation') {
|
|
97
|
+
actionResult = await this.executeButtonClick(promise, bundle);
|
|
98
|
+
attempt.action = 'click';
|
|
99
|
+
} else if (promise.category === 'form') {
|
|
100
|
+
actionResult = await this.executeFormSubmission(promise, bundle);
|
|
101
|
+
attempt.action = 'submit';
|
|
102
|
+
} else if (promise.category === 'validation') {
|
|
103
|
+
actionResult = await this.executeValidation(promise, bundle);
|
|
104
|
+
attempt.action = 'observe';
|
|
105
|
+
} else if (promise.type === 'state') {
|
|
106
|
+
actionResult = await this.observeStateChange(promise, bundle);
|
|
107
|
+
attempt.action = 'observe';
|
|
108
|
+
} else if (promise.type === 'network') {
|
|
109
|
+
actionResult = await this.observeNetworkRequest(promise, bundle);
|
|
110
|
+
attempt.action = 'observe';
|
|
111
|
+
} else {
|
|
112
|
+
attempt.reason = 'unsupported-promise-type';
|
|
113
|
+
attempt.cause = 'blocked'; // Mark as blocked due to unsupported type
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Handle action result - could be boolean or object with details
|
|
117
|
+
let actionSuccess = false;
|
|
118
|
+
let actionReason = null;
|
|
119
|
+
|
|
120
|
+
if (typeof actionResult === 'object' && actionResult !== null) {
|
|
121
|
+
actionSuccess = actionResult.success;
|
|
122
|
+
actionReason = actionResult.reason;
|
|
123
|
+
if (actionResult.cause) {
|
|
124
|
+
attempt.cause = actionResult.cause;
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
actionSuccess = Boolean(actionResult);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Wait for effects
|
|
131
|
+
await this.page.waitForTimeout(500);
|
|
132
|
+
|
|
133
|
+
// Capture after state
|
|
134
|
+
await bundle.captureAfterState(this.page);
|
|
135
|
+
const urlAfter = this.page.url();
|
|
136
|
+
|
|
137
|
+
// Analyze changes
|
|
138
|
+
bundle.analyzeChanges(urlBefore, urlAfter);
|
|
139
|
+
bundle.correlateNetworkRequests(); // NEW: correlate network to action
|
|
140
|
+
|
|
141
|
+
// Collect new console errors
|
|
142
|
+
const newConsoleErrors = this.consoleEvents.slice(consoleErrorsCountBefore);
|
|
143
|
+
newConsoleErrors.forEach(err => bundle.recordConsoleError(err));
|
|
144
|
+
|
|
145
|
+
// Finalize bundle
|
|
146
|
+
bundle.finalize(this.page);
|
|
147
|
+
|
|
148
|
+
attempt.attempted = true;
|
|
149
|
+
attempt.evidence = bundle.getSummary();
|
|
150
|
+
attempt.signals = bundle.signals;
|
|
151
|
+
|
|
152
|
+
// Determine outcome based on strict binding rules
|
|
153
|
+
if (!attempt.cause) {
|
|
154
|
+
// Classify outcome based on expected behavior
|
|
155
|
+
attempt.cause = classifyOutcome(promise, bundle, actionSuccess, actionReason);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// If action succeeded and outcome matched, reason is null (observed)
|
|
159
|
+
if (actionSuccess && outcomeMatched(promise.expectedOutcome, bundle.signals)) {
|
|
160
|
+
attempt.reason = null;
|
|
161
|
+
} else if (!actionSuccess) {
|
|
162
|
+
// Action failed - use cause to explain why
|
|
163
|
+
attempt.reason = attempt.cause;
|
|
164
|
+
} else if (!outcomeMatched(promise.expectedOutcome, bundle.signals)) {
|
|
165
|
+
// Action succeeded but expected outcome not observed
|
|
166
|
+
attempt.reason = attempt.cause || 'outcome-not-met';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
} catch (error) {
|
|
170
|
+
attempt.reason = `error:${error.message}`;
|
|
171
|
+
attempt.cause = 'error';
|
|
172
|
+
attempt.attempted = true;
|
|
173
|
+
|
|
174
|
+
// Capture after even on error
|
|
175
|
+
try {
|
|
176
|
+
await bundle.captureAfterState(this.page);
|
|
177
|
+
bundle.finalize(this.page);
|
|
178
|
+
attempt.evidence = bundle.getSummary();
|
|
179
|
+
} catch (e) {
|
|
180
|
+
// Give up
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.attempts.push(attempt);
|
|
185
|
+
return attempt;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Execute button click
|
|
190
|
+
* Returns: { success: boolean, reason?: string, cause?: string }
|
|
191
|
+
*/
|
|
192
|
+
async executeButtonClick(promise, bundle) {
|
|
193
|
+
// Resolve selector
|
|
194
|
+
const resolution = await resolveSelector(this.page, promise);
|
|
195
|
+
|
|
196
|
+
if (!resolution.found) {
|
|
197
|
+
bundle.timing.endedAt = new Date().toISOString();
|
|
198
|
+
return { success: false, reason: 'selector-not-found', cause: 'not-found' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
// Check if interactable
|
|
203
|
+
const interactable = await isElementInteractable(this.page, resolution.selector);
|
|
204
|
+
if (!interactable) {
|
|
205
|
+
bundle.timing.endedAt = new Date().toISOString();
|
|
206
|
+
return { success: false, reason: 'element-not-interactable', cause: 'blocked' };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Perform click
|
|
210
|
+
await this.page.locator(resolution.selector).click({ timeout: 3000 });
|
|
211
|
+
|
|
212
|
+
// Wait for expected outcome
|
|
213
|
+
const outcomeReached = await this.waitForOutcome(promise.expectedOutcome || 'ui-change');
|
|
214
|
+
|
|
215
|
+
if (!outcomeReached) {
|
|
216
|
+
return { success: false, reason: 'outcome-timeout', cause: 'timeout' };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { success: true };
|
|
220
|
+
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return { success: false, reason: error.message, cause: 'error' };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Execute form submission
|
|
228
|
+
* Returns: { success: boolean, reason?: string, cause?: string }
|
|
229
|
+
*/
|
|
230
|
+
async executeFormSubmission(promise, _bundle) {
|
|
231
|
+
// Resolve form selector
|
|
232
|
+
const formResolution = await resolveSelector(this.page, promise);
|
|
233
|
+
|
|
234
|
+
if (!formResolution.found) {
|
|
235
|
+
return { success: false, reason: 'form-not-found', cause: 'not-found' };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
// Fill in required inputs with sample data
|
|
240
|
+
const requiredInputs = await this.page.locator(`${formResolution.selector} input[required]`).all();
|
|
241
|
+
|
|
242
|
+
for (const input of requiredInputs) {
|
|
243
|
+
const type = await input.getAttribute('type');
|
|
244
|
+
let value = 'test-data';
|
|
245
|
+
|
|
246
|
+
if (type === 'email') {
|
|
247
|
+
value = 'test@example.com';
|
|
248
|
+
} else if (type === 'number') {
|
|
249
|
+
value = '123';
|
|
250
|
+
} else if (type === 'date') {
|
|
251
|
+
value = '2025-01-14';
|
|
252
|
+
} else if (type === 'checkbox') {
|
|
253
|
+
const isChecked = await input.isChecked();
|
|
254
|
+
if (!isChecked) {
|
|
255
|
+
await input.check();
|
|
256
|
+
}
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
await input.fill(value);
|
|
262
|
+
} catch (e) {
|
|
263
|
+
// Skip if can't fill
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Find and click submit button
|
|
268
|
+
const submitBtn = await findSubmitButton(this.page, formResolution.selector);
|
|
269
|
+
|
|
270
|
+
let _submitted = false;
|
|
271
|
+
if (submitBtn) {
|
|
272
|
+
try {
|
|
273
|
+
await submitBtn.click({ timeout: 3000 });
|
|
274
|
+
_submitted = true;
|
|
275
|
+
} catch (e) {
|
|
276
|
+
return { success: false, reason: 'submit-button-click-failed', cause: 'blocked' };
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
// Try submitting with Enter on first input
|
|
280
|
+
const firstInput = await this.page.locator(`${formResolution.selector} input`).first();
|
|
281
|
+
if (await firstInput.count() > 0) {
|
|
282
|
+
try {
|
|
283
|
+
await firstInput.press('Enter');
|
|
284
|
+
_submitted = true;
|
|
285
|
+
} catch (e) {
|
|
286
|
+
return { success: false, reason: 'form-submit-failed', cause: 'blocked' };
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
return { success: false, reason: 'no-submit-mechanism', cause: 'blocked' };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Wait for expected outcome
|
|
294
|
+
const outcomeReached = await this.waitForOutcome(promise.expectedOutcome || 'ui-change');
|
|
295
|
+
|
|
296
|
+
if (!outcomeReached) {
|
|
297
|
+
return { success: false, reason: 'form-submit-prevented', cause: 'prevented-submit' };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { success: true };
|
|
301
|
+
|
|
302
|
+
} catch (error) {
|
|
303
|
+
return { success: false, reason: error.message, cause: 'error' };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Execute validation check
|
|
309
|
+
*/
|
|
310
|
+
async executeValidation(promise, _bundle) {
|
|
311
|
+
// Find required input
|
|
312
|
+
const resolution = await resolveSelector(this.page, promise);
|
|
313
|
+
|
|
314
|
+
if (!resolution.found) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
// Try to submit form without filling in this required field
|
|
320
|
+
// First, find the containing form
|
|
321
|
+
const form = await this.page.locator(`input[required]`).first().evaluate(el => {
|
|
322
|
+
let current = el;
|
|
323
|
+
while (current && current.tagName !== 'FORM') {
|
|
324
|
+
current = current.parentElement;
|
|
325
|
+
}
|
|
326
|
+
return current ? current.getAttribute('id') || current.className : null;
|
|
327
|
+
}).catch(() => null);
|
|
328
|
+
|
|
329
|
+
if (form) {
|
|
330
|
+
// Try to submit the form
|
|
331
|
+
const submitBtn = await findSubmitButton(this.page, `form`);
|
|
332
|
+
if (submitBtn) {
|
|
333
|
+
await submitBtn.click({ timeout: 2000 }).catch(() => {});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Wait briefly for validation feedback
|
|
338
|
+
await this.page.waitForTimeout(1000);
|
|
339
|
+
|
|
340
|
+
// Check if validation feedback appeared
|
|
341
|
+
const hasValidationUI = await this.page.locator('[aria-invalid="true"], [role="alert"], .error, .validation-error').count() > 0;
|
|
342
|
+
|
|
343
|
+
return hasValidationUI;
|
|
344
|
+
|
|
345
|
+
} catch (error) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Observe state changes
|
|
352
|
+
*/
|
|
353
|
+
async observeStateChange(_promise, _bundle) {
|
|
354
|
+
const htmlBefore = await this.page.content();
|
|
355
|
+
|
|
356
|
+
await this.page.waitForTimeout(1500);
|
|
357
|
+
|
|
358
|
+
const htmlAfter = await this.page.content();
|
|
359
|
+
|
|
360
|
+
return htmlBefore !== htmlAfter;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Observe network request
|
|
365
|
+
*/
|
|
366
|
+
async observeNetworkRequest(_promise, _bundle) {
|
|
367
|
+
const targetUrl = _promise.promise.value;
|
|
368
|
+
const networkCountBefore = this.networkEvents.length;
|
|
369
|
+
|
|
370
|
+
await this.page.waitForTimeout(2000);
|
|
371
|
+
|
|
372
|
+
const networkCountAfter = this.networkEvents.length;
|
|
373
|
+
|
|
374
|
+
if (networkCountAfter > networkCountBefore) {
|
|
375
|
+
// Check if any of the new events match the target
|
|
376
|
+
const newEvents = this.networkEvents.slice(networkCountBefore);
|
|
377
|
+
return newEvents.some(event => {
|
|
378
|
+
return event.url === targetUrl ||
|
|
379
|
+
event.url.includes(targetUrl) ||
|
|
380
|
+
targetUrl.includes(event.url);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Wait for expected outcome with timeout
|
|
389
|
+
*/
|
|
390
|
+
async waitForOutcome(expectedOutcome) {
|
|
391
|
+
const _startTime = Date.now();
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
if (expectedOutcome === 'navigation') {
|
|
395
|
+
// Wait for navigation or DOM change
|
|
396
|
+
await Promise.race([
|
|
397
|
+
this.page.waitForNavigation({ timeout: WAIT_FOR_EFFECT_TIMEOUT }).catch(() => {}),
|
|
398
|
+
this.page.waitForLoadState('domcontentloaded', { timeout: WAIT_FOR_EFFECT_TIMEOUT }).catch(() => {}),
|
|
399
|
+
this.page.waitForTimeout(WAIT_FOR_EFFECT_TIMEOUT),
|
|
400
|
+
]);
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (expectedOutcome === 'network') {
|
|
405
|
+
// Network request should be detected via networkEvents
|
|
406
|
+
await this.page.waitForTimeout(WAIT_FOR_EFFECT_TIMEOUT);
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (expectedOutcome === 'feedback') {
|
|
411
|
+
// Wait for feedback element to appear
|
|
412
|
+
const found = await Promise.race([
|
|
413
|
+
this.page.waitForSelector('[aria-live], [role="alert"], [role="status"], .toast',
|
|
414
|
+
{ timeout: WAIT_FOR_EFFECT_TIMEOUT }).catch(() => null),
|
|
415
|
+
new Promise(resolve => {
|
|
416
|
+
setTimeout(() => resolve(false), WAIT_FOR_EFFECT_TIMEOUT);
|
|
417
|
+
}),
|
|
418
|
+
]);
|
|
419
|
+
return !!found;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Default: ui-change
|
|
423
|
+
await this.page.waitForTimeout(WAIT_FOR_EFFECT_TIMEOUT);
|
|
424
|
+
return true;
|
|
425
|
+
|
|
426
|
+
} catch (e) {
|
|
427
|
+
// Timeout or error - still counts as attempted
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Record network event
|
|
434
|
+
*/
|
|
435
|
+
recordNetworkEvent(event) {
|
|
436
|
+
this.networkEvents.push(event);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Record console event
|
|
441
|
+
*/
|
|
442
|
+
recordConsoleEvent(event) {
|
|
443
|
+
this.consoleEvents.push(event);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Get all attempts
|
|
448
|
+
*/
|
|
449
|
+
getAttempts() {
|
|
450
|
+
return this.attempts;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Classify the outcome based on evidence and expected outcome
|
|
456
|
+
* Returns cause taxonomy: not-found | blocked | prevented-submit | timeout | no-change
|
|
457
|
+
*/
|
|
458
|
+
function classifyOutcome(promise, bundle, actionSuccess, actionReason) {
|
|
459
|
+
// If action itself failed
|
|
460
|
+
if (!actionSuccess && actionReason) {
|
|
461
|
+
const lower = actionReason.toLowerCase();
|
|
462
|
+
if (lower.includes('not-found') || lower.includes('not found')) {
|
|
463
|
+
return 'not-found';
|
|
464
|
+
}
|
|
465
|
+
if (lower.includes('interactable') || lower.includes('blocked')) {
|
|
466
|
+
return 'blocked';
|
|
467
|
+
}
|
|
468
|
+
if (lower.includes('prevented') || lower.includes('prevented-submit')) {
|
|
469
|
+
return 'prevented-submit';
|
|
470
|
+
}
|
|
471
|
+
if (lower.includes('timeout') || lower.includes('timed out')) {
|
|
472
|
+
return 'timeout';
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Action succeeded but outcome not met
|
|
477
|
+
const expected = promise.expectedOutcome || 'ui-change';
|
|
478
|
+
const matched = outcomeMatched(expected, bundle.signals);
|
|
479
|
+
|
|
480
|
+
if (!matched) {
|
|
481
|
+
// Check what didn't happen
|
|
482
|
+
if (expected === 'navigation' && !bundle.signals.navigationChanged) {
|
|
483
|
+
// Navigation was supposed to happen but didn't
|
|
484
|
+
return 'no-change';
|
|
485
|
+
}
|
|
486
|
+
if (expected === 'feedback' && !bundle.signals.feedbackSeen) {
|
|
487
|
+
// Feedback was supposed to appear but didn't
|
|
488
|
+
return 'no-change';
|
|
489
|
+
}
|
|
490
|
+
if (expected === 'network' && !bundle.signals.correlatedNetworkActivity) {
|
|
491
|
+
// Network request was supposed to happen but didn't
|
|
492
|
+
return 'no-change';
|
|
493
|
+
}
|
|
494
|
+
if (expected === 'ui-change' && !bundle.signals.meaningfulDomChange) {
|
|
495
|
+
// UI change was supposed to happen but didn't
|
|
496
|
+
return 'no-change';
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Default
|
|
501
|
+
return 'no-change';
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Check if the expected outcome has been satisfied by the signals
|
|
506
|
+
*/
|
|
507
|
+
function outcomeMatched(expectedOutcome, signals) {
|
|
508
|
+
if (!expectedOutcome || expectedOutcome === 'ui-change') {
|
|
509
|
+
// Any meaningful signal counts
|
|
510
|
+
return signals.navigationChanged ||
|
|
511
|
+
signals.meaningfulDomChange ||
|
|
512
|
+
signals.feedbackSeen ||
|
|
513
|
+
signals.correlatedNetworkActivity;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (expectedOutcome === 'navigation') {
|
|
517
|
+
return signals.navigationChanged;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (expectedOutcome === 'feedback') {
|
|
521
|
+
return signals.feedbackSeen;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (expectedOutcome === 'network') {
|
|
525
|
+
return signals.correlatedNetworkActivity || signals.networkActivity;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolve } from 'path';
|
|
2
2
|
import { atomicWriteJson } from './atomic-write.js';
|
|
3
3
|
import { compareExpectations } from './idgen.js';
|
|
4
|
+
import { ARTIFACT_REGISTRY } from '../../verax/core/artifacts/registry.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Write learn.json artifact
|
|
@@ -13,6 +14,7 @@ export function writeLearnJson(runPaths, expectations, skipped) {
|
|
|
13
14
|
const sortedExpectations = [...expectations].sort(compareExpectations);
|
|
14
15
|
|
|
15
16
|
const learnJson = {
|
|
17
|
+
contractVersion: ARTIFACT_REGISTRY.learn.contractVersion,
|
|
16
18
|
expectations: sortedExpectations,
|
|
17
19
|
stats: {
|
|
18
20
|
totalExpectations: sortedExpectations.length,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 5: Failure Ledger Writer
|
|
3
|
+
*
|
|
4
|
+
* Ensures durable failure evidence is ALWAYS written when analysis fails/is incomplete.
|
|
5
|
+
* This is the safety net - if we can't write primary artifacts, at least write this.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { atomicWriteJson } from './atomic-write.js';
|
|
9
|
+
import { readFileSync } from 'fs';
|
|
10
|
+
import { resolve, dirname } from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
function getVersion() {
|
|
17
|
+
try {
|
|
18
|
+
const pkgPath = resolve(__dirname, '../../../package.json');
|
|
19
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
20
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
21
|
+
return pkg.version;
|
|
22
|
+
} catch {
|
|
23
|
+
return '0.3.0';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Write failure ledger to run directory
|
|
29
|
+
*
|
|
30
|
+
* @param {string} runDir - Run directory path
|
|
31
|
+
* @param {object} failureInfo - Failure details
|
|
32
|
+
* @param {string} failureInfo.runId - Run ID
|
|
33
|
+
* @param {string} failureInfo.url - Target URL
|
|
34
|
+
* @param {string} failureInfo.src - Source directory
|
|
35
|
+
* @param {string} failureInfo.state - ANALYSIS_FAILED or ANALYSIS_INCOMPLETE
|
|
36
|
+
* @param {number} failureInfo.exitCode - Exit code (2 for FAILED, 66 for INCOMPLETE)
|
|
37
|
+
* @param {Error} failureInfo.primaryError - The error that caused failure
|
|
38
|
+
* @param {string} failureInfo.phase - Phase where failure occurred
|
|
39
|
+
* @param {string[]} failureInfo.notes - Additional context notes
|
|
40
|
+
* @returns {{ ok: boolean, path?: string, error?: Error }}
|
|
41
|
+
*/
|
|
42
|
+
export function writeLedger(runDir, failureInfo) {
|
|
43
|
+
const {
|
|
44
|
+
runId,
|
|
45
|
+
url,
|
|
46
|
+
src,
|
|
47
|
+
state,
|
|
48
|
+
exitCode,
|
|
49
|
+
primaryError,
|
|
50
|
+
phase = 'unknown',
|
|
51
|
+
notes = [],
|
|
52
|
+
} = failureInfo;
|
|
53
|
+
|
|
54
|
+
const ledgerPath = `${runDir}/ledger.json`;
|
|
55
|
+
|
|
56
|
+
const ledger = {
|
|
57
|
+
meta: {
|
|
58
|
+
runId,
|
|
59
|
+
url,
|
|
60
|
+
src,
|
|
61
|
+
timestamp: new Date().toISOString(),
|
|
62
|
+
version: getVersion(),
|
|
63
|
+
},
|
|
64
|
+
state, // "ANALYSIS_FAILED" | "ANALYSIS_INCOMPLETE"
|
|
65
|
+
exitCode,
|
|
66
|
+
primaryError: {
|
|
67
|
+
name: primaryError?.name || 'Error',
|
|
68
|
+
message: primaryError?.message || 'Unknown error',
|
|
69
|
+
stack: primaryError?.stack || null,
|
|
70
|
+
},
|
|
71
|
+
phase,
|
|
72
|
+
notes,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
atomicWriteJson(ledgerPath, ledger);
|
|
77
|
+
return { ok: true, path: ledgerPath };
|
|
78
|
+
} catch (writeError) {
|
|
79
|
+
// Ledger write failed - this is EXTREMELY BAD
|
|
80
|
+
// Print loud warning but don't throw - we're already in error handling
|
|
81
|
+
console.error('═══════════════════════════════════════════════════════');
|
|
82
|
+
console.error('🚨 CRITICAL: FAILURE LEDGER COULD NOT BE WRITTEN');
|
|
83
|
+
console.error('═══════════════════════════════════════════════════════');
|
|
84
|
+
console.error('Run directory:', runDir);
|
|
85
|
+
console.error('Ledger path:', ledgerPath);
|
|
86
|
+
console.error('Write error:', writeError?.message || 'Unknown');
|
|
87
|
+
console.error('═══════════════════════════════════════════════════════');
|
|
88
|
+
|
|
89
|
+
return { ok: false, error: writeError };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create integrity stamp for artifacts
|
|
95
|
+
*
|
|
96
|
+
* @param {string} status - "COMPLETE" | "PARTIAL" | "FAILED_WRITE"
|
|
97
|
+
* @param {boolean} atomic - Whether atomic write was used
|
|
98
|
+
* @param {string[]} notes - Additional notes
|
|
99
|
+
* @returns {object} Integrity object
|
|
100
|
+
*/
|
|
101
|
+
export function createIntegrityStamp(status, atomic = true, notes = []) {
|
|
102
|
+
return {
|
|
103
|
+
status,
|
|
104
|
+
atomic,
|
|
105
|
+
writer: 'verax',
|
|
106
|
+
writerVersion: getVersion(),
|
|
107
|
+
writtenAt: new Date().toISOString(),
|
|
108
|
+
notes,
|
|
109
|
+
};
|
|
110
|
+
}
|