@veraxhq/verax 0.1.0 → 0.2.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 +123 -88
- package/bin/verax.js +11 -452
- package/package.json +14 -36
- package/src/cli/commands/default.js +523 -0
- package/src/cli/commands/doctor.js +165 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +402 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +296 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +34 -0
- package/src/cli/util/expectation-extractor.js +378 -0
- package/src/cli/util/findings-writer.js +31 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +366 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +29 -0
- package/src/cli/util/project-discovery.js +277 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/summary-writer.js +32 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +101 -0
- package/src/verax/cli/wizard.js +98 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +403 -0
- package/src/verax/core/incremental-store.js +237 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +521 -0
- package/src/verax/detect/comparison.js +2 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +177 -0
- package/src/verax/detect/expectation-model.js +194 -172
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +44 -8
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +172 -286
- package/src/verax/detect/interactive-findings.js +613 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/verdict-engine.js +563 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/index.js +90 -14
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +579 -0
- package/src/verax/intel/vue-router-extractor.js +323 -0
- package/src/verax/learn/action-contract-extractor.js +335 -101
- package/src/verax/learn/ast-contract-extractor.js +95 -5
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/manifest-writer.js +97 -47
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +27 -96
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +112 -4
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +10 -5
- package/src/verax/observe/console-sensor.js +1 -17
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +512 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +643 -275
- package/src/verax/observe/index.js +908 -27
- package/src/verax/observe/index.js.backup +1 -0
- package/src/verax/observe/interaction-discovery.js +365 -14
- package/src/verax/observe/interaction-runner.js +563 -198
- package/src/verax/observe/loading-sensor.js +139 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +37 -17
- package/src/verax/observe/state-sensor.js +389 -0
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +61 -20
- package/src/verax/observe/ui-signal-sensor.js +136 -17
- package/src/verax/scan-summary-writer.js +77 -15
- package/src/verax/shared/artifact-manager.js +110 -8
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +170 -0
- package/src/verax/shared/dynamic-route-utils.js +218 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +14 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +65 -0
- package/src/verax/validate/context-validator.js +244 -0
- package/src/verax/validate/context-validator.js.bak +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 2 — Expectation Tracker
|
|
3
|
+
*
|
|
4
|
+
* Generates stable expectation IDs and tracks expectation usage.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createHash } from 'crypto';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate stable expectation ID from expectation data
|
|
11
|
+
* @param {Object} expectation - Expectation object
|
|
12
|
+
* @param {string} type - Expectation type (navigation, network_action, state_action)
|
|
13
|
+
* @returns {string} Stable ID (8-char hex hash)
|
|
14
|
+
*/
|
|
15
|
+
export function generateExpectationId(expectation, type) {
|
|
16
|
+
// Create deterministic hash from type + source + target
|
|
17
|
+
const source = expectation.source || expectation.sourceFile || expectation.evidence?.source || '';
|
|
18
|
+
const target = expectation.targetPath || expectation.urlPath || '';
|
|
19
|
+
const method = expectation.method || '';
|
|
20
|
+
const stateKind = expectation.stateKind || '';
|
|
21
|
+
const handlerRef = expectation.handlerRef || '';
|
|
22
|
+
const sourceRef = expectation.source || '';
|
|
23
|
+
|
|
24
|
+
// Build hash input string
|
|
25
|
+
const hashInput = `${type}|${source}|${target}|${method}|${stateKind}|${handlerRef}|${sourceRef}`;
|
|
26
|
+
|
|
27
|
+
// Generate stable 8-character hex hash
|
|
28
|
+
const hash = createHash('sha256').update(hashInput).digest('hex');
|
|
29
|
+
return hash.substring(0, 8);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Normalize expectation for tracking
|
|
34
|
+
* @param {Object} expectation - Raw expectation from manifest
|
|
35
|
+
* @param {string} type - Expectation type
|
|
36
|
+
* @returns {Object} Normalized expectation with ID
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeExpectation(expectation, type) {
|
|
39
|
+
const id = generateExpectationId(expectation, type);
|
|
40
|
+
|
|
41
|
+
// Extract source location
|
|
42
|
+
let sourceFile = null;
|
|
43
|
+
let sourceLine = null;
|
|
44
|
+
let sourceColumn = null;
|
|
45
|
+
|
|
46
|
+
if (expectation.evidence?.source) {
|
|
47
|
+
sourceFile = expectation.evidence.source;
|
|
48
|
+
} else if (expectation.sourceFile) {
|
|
49
|
+
sourceFile = expectation.sourceFile;
|
|
50
|
+
} else if (expectation.source) {
|
|
51
|
+
// sourceRef format: "file:line:col"
|
|
52
|
+
const sourceMatch = expectation.source.match(/^(.+):(\d+):(\d+)$/);
|
|
53
|
+
if (sourceMatch) {
|
|
54
|
+
sourceFile = sourceMatch[1];
|
|
55
|
+
sourceLine = parseInt(sourceMatch[2], 10);
|
|
56
|
+
sourceColumn = parseInt(sourceMatch[3], 10);
|
|
57
|
+
} else {
|
|
58
|
+
sourceFile = expectation.source;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (expectation.line !== undefined && expectation.line !== null) {
|
|
63
|
+
sourceLine = expectation.line;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Determine reason for PROVEN vs UNKNOWN
|
|
67
|
+
let reason = 'unknown';
|
|
68
|
+
if (expectation.proof === 'PROVEN_EXPECTATION') {
|
|
69
|
+
if (type === 'navigation') {
|
|
70
|
+
if (expectation.evidence) {
|
|
71
|
+
reason = 'static route literal';
|
|
72
|
+
} else if (expectation.matchAttribute) {
|
|
73
|
+
reason = 'ast-derived jsx contract';
|
|
74
|
+
}
|
|
75
|
+
} else if (type === 'network_action') {
|
|
76
|
+
if (expectation.source || expectation.handlerRef) {
|
|
77
|
+
reason = 'ast-derived network contract';
|
|
78
|
+
}
|
|
79
|
+
} else if (type === 'state_action') {
|
|
80
|
+
if (expectation.source || expectation.handlerRef) {
|
|
81
|
+
reason = 'ast-derived state contract';
|
|
82
|
+
}
|
|
83
|
+
} else if (type === 'validation_block') {
|
|
84
|
+
if (expectation.source || expectation.handlerRef) {
|
|
85
|
+
reason = 'ast-derived validation contract';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
reason = 'unproven expectation';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
id,
|
|
94
|
+
type,
|
|
95
|
+
proof: expectation.proof || 'UNKNOWN_EXPECTATION',
|
|
96
|
+
reason,
|
|
97
|
+
source: {
|
|
98
|
+
file: sourceFile,
|
|
99
|
+
line: sourceLine,
|
|
100
|
+
column: sourceColumn
|
|
101
|
+
},
|
|
102
|
+
used: false,
|
|
103
|
+
usedReason: null,
|
|
104
|
+
raw: expectation // Keep raw data for reference
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Track all expectations from manifest
|
|
110
|
+
* @param {Object} manifest - Manifest object
|
|
111
|
+
* @returns {Array} Array of normalized expectations
|
|
112
|
+
*/
|
|
113
|
+
export function trackExpectations(manifest) {
|
|
114
|
+
const expectations = [];
|
|
115
|
+
|
|
116
|
+
// Track static expectations
|
|
117
|
+
if (manifest.staticExpectations && manifest.staticExpectations.length > 0) {
|
|
118
|
+
for (const exp of manifest.staticExpectations) {
|
|
119
|
+
const type = exp.type === 'form_submission' ? 'network_action' : 'navigation';
|
|
120
|
+
expectations.push(normalizeExpectation(exp, type));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Track SPA expectations
|
|
125
|
+
if (manifest.spaExpectations && manifest.spaExpectations.length > 0) {
|
|
126
|
+
for (const exp of manifest.spaExpectations) {
|
|
127
|
+
expectations.push(normalizeExpectation(exp, 'navigation'));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Track action contracts (network, state, and validation)
|
|
132
|
+
if (manifest.actionContracts && manifest.actionContracts.length > 0) {
|
|
133
|
+
for (const contract of manifest.actionContracts) {
|
|
134
|
+
if (contract.kind === 'NETWORK_ACTION' || contract.kind === 'network') {
|
|
135
|
+
expectations.push(normalizeExpectation(contract, 'network_action'));
|
|
136
|
+
} else if (contract.kind === 'STATE_ACTION' || contract.kind === 'state') {
|
|
137
|
+
expectations.push(normalizeExpectation(contract, 'state_action'));
|
|
138
|
+
} else if (contract.kind === 'VALIDATION_BLOCK') {
|
|
139
|
+
expectations.push(normalizeExpectation(contract, 'validation_block'));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return expectations;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Find expectation by ID
|
|
149
|
+
* @param {Array} expectations - Tracked expectations
|
|
150
|
+
* @param {string} expectationId - ID to find
|
|
151
|
+
* @returns {Object|null} Matching expectation or null
|
|
152
|
+
*/
|
|
153
|
+
export function findExpectationById(expectations, expectationId) {
|
|
154
|
+
return expectations.find(e => e.id === expectationId) || null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Find expectation by matching criteria (for detect phase)
|
|
159
|
+
* @param {Array} expectations - Tracked expectations
|
|
160
|
+
* @param {Object} criteria - Matching criteria
|
|
161
|
+
* @returns {Object|null} Matching expectation or null
|
|
162
|
+
*/
|
|
163
|
+
export function findExpectationByCriteria(expectations, criteria) {
|
|
164
|
+
const { type, source, target, method, stateKind, handlerRef } = criteria;
|
|
165
|
+
|
|
166
|
+
for (const exp of expectations) {
|
|
167
|
+
if (exp.type !== type) continue;
|
|
168
|
+
|
|
169
|
+
// Match by source
|
|
170
|
+
const expSource = exp.raw.source || exp.raw.sourceFile || exp.raw.evidence?.source;
|
|
171
|
+
if (source && expSource && expSource.includes(source)) {
|
|
172
|
+
// Check target for navigation
|
|
173
|
+
if (type === 'navigation') {
|
|
174
|
+
const expTarget = exp.raw.targetPath;
|
|
175
|
+
if (expTarget === target) {
|
|
176
|
+
return exp;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Check method/urlPath for network_action
|
|
180
|
+
else if (type === 'network_action') {
|
|
181
|
+
if (method && exp.raw.method === method && exp.raw.urlPath === target) {
|
|
182
|
+
return exp;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Check stateKind for state_action
|
|
186
|
+
else if (type === 'state_action') {
|
|
187
|
+
if (stateKind && exp.raw.stateKind === stateKind) {
|
|
188
|
+
return exp;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Match by handlerRef
|
|
194
|
+
if (handlerRef && exp.raw.handlerRef === handlerRef) {
|
|
195
|
+
return exp;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 2 — Expectations Writer
|
|
3
|
+
*
|
|
4
|
+
* Writes expectations.json artifact with full explainability.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { writeFileSync } from 'fs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Write expectations.json artifact
|
|
11
|
+
* @param {Object} paths - Artifact paths
|
|
12
|
+
* @param {Array} expectations - Tracked expectations
|
|
13
|
+
*/
|
|
14
|
+
export function writeExpectations(paths, expectations) {
|
|
15
|
+
// Count by type
|
|
16
|
+
const byType = {
|
|
17
|
+
navigation: 0,
|
|
18
|
+
network_action: 0,
|
|
19
|
+
state_action: 0
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let skipped = 0;
|
|
23
|
+
|
|
24
|
+
const expectationsList = expectations.map(exp => {
|
|
25
|
+
byType[exp.type] = (byType[exp.type] || 0) + 1;
|
|
26
|
+
|
|
27
|
+
if (!exp.used) {
|
|
28
|
+
skipped++;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
id: exp.id,
|
|
33
|
+
type: exp.type,
|
|
34
|
+
proof: exp.proof,
|
|
35
|
+
reason: exp.reason,
|
|
36
|
+
source: {
|
|
37
|
+
file: exp.source.file,
|
|
38
|
+
line: exp.source.line,
|
|
39
|
+
column: exp.source.column
|
|
40
|
+
},
|
|
41
|
+
used: exp.used,
|
|
42
|
+
usedReason: exp.usedReason || null
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const data = {
|
|
47
|
+
summary: {
|
|
48
|
+
total: expectations.length,
|
|
49
|
+
byType: byType,
|
|
50
|
+
skipped: skipped
|
|
51
|
+
},
|
|
52
|
+
expectations: expectationsList
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const expectationsPath = paths.expectations || paths.summary.replace('summary.json', 'expectations.json');
|
|
56
|
+
writeFileSync(expectationsPath, JSON.stringify(data, null, 2) + '\n');
|
|
57
|
+
|
|
58
|
+
return expectationsPath;
|
|
59
|
+
}
|
|
60
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 9 — First-Run Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects and tracks first-ever CLI run.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { resolve, dirname } from 'path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get first-run marker path
|
|
12
|
+
* @param {string} projectRoot - Project root directory
|
|
13
|
+
* @returns {string} Marker file path
|
|
14
|
+
*/
|
|
15
|
+
function getMarkerPath(projectRoot) {
|
|
16
|
+
const veraxDir = resolve(projectRoot, '.verax');
|
|
17
|
+
return resolve(veraxDir, '.first-run-complete');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if this is the first run
|
|
22
|
+
* @param {string} projectRoot - Project root directory
|
|
23
|
+
* @returns {boolean} True if first run
|
|
24
|
+
*/
|
|
25
|
+
export function isFirstRun(projectRoot) {
|
|
26
|
+
const markerPath = getMarkerPath(projectRoot);
|
|
27
|
+
return !existsSync(markerPath);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Mark first run as complete
|
|
32
|
+
* @param {string} projectRoot - Project root directory
|
|
33
|
+
*/
|
|
34
|
+
export function markFirstRunComplete(projectRoot) {
|
|
35
|
+
const markerPath = getMarkerPath(projectRoot);
|
|
36
|
+
const veraxDir = dirname(markerPath);
|
|
37
|
+
|
|
38
|
+
// Ensure .verax directory exists
|
|
39
|
+
mkdirSync(veraxDir, { recursive: true });
|
|
40
|
+
|
|
41
|
+
// Write marker file (empty file is sufficient)
|
|
42
|
+
writeFileSync(markerPath, '', 'utf-8');
|
|
43
|
+
}
|
|
44
|
+
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 4 — Progress Reporter
|
|
3
|
+
*
|
|
4
|
+
* Reports real-time progress during scan phases with actual counts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Progress reporter that tracks real progress metrics
|
|
9
|
+
*/
|
|
10
|
+
export class ProgressReporter {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.output = options.output || process.stderr;
|
|
13
|
+
this.silent = options.silent || false;
|
|
14
|
+
this.explain = options.explain || false;
|
|
15
|
+
|
|
16
|
+
// Phase tracking
|
|
17
|
+
this.currentPhase = null;
|
|
18
|
+
this.phaseProgress = {
|
|
19
|
+
learn: { current: 0, total: 0, details: [] },
|
|
20
|
+
validate: { current: 0, total: 0, details: [] },
|
|
21
|
+
observe: { current: 0, total: 0, details: [] },
|
|
22
|
+
detect: { current: 0, total: 0, details: [] }
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Interaction stats
|
|
26
|
+
this.interactionStats = {
|
|
27
|
+
discovered: 0,
|
|
28
|
+
executed: 0,
|
|
29
|
+
skipped: 0,
|
|
30
|
+
skippedByReason: {}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Expectation stats
|
|
34
|
+
this.expectationStats = {
|
|
35
|
+
total: 0,
|
|
36
|
+
used: 0,
|
|
37
|
+
unused: 0,
|
|
38
|
+
unusedByReason: {}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Report phase start
|
|
44
|
+
*/
|
|
45
|
+
startPhase(phase, message) {
|
|
46
|
+
this.currentPhase = phase;
|
|
47
|
+
if (!this.silent) {
|
|
48
|
+
this.output.write(`[VERAX] [${phase}] ${message}\n`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Report learn phase progress
|
|
54
|
+
*/
|
|
55
|
+
reportLearnProgress(current, total, details = []) {
|
|
56
|
+
this.phaseProgress.learn.current = current;
|
|
57
|
+
this.phaseProgress.learn.total = total;
|
|
58
|
+
this.phaseProgress.learn.details = details;
|
|
59
|
+
|
|
60
|
+
if (!this.silent && total > 0) {
|
|
61
|
+
this.output.write(`[VERAX] [1/4] Learn: ${current}/${total} expectations discovered\n`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Report validate phase progress
|
|
67
|
+
*/
|
|
68
|
+
reportValidateProgress(current, total) {
|
|
69
|
+
this.phaseProgress.validate.current = current;
|
|
70
|
+
this.phaseProgress.validate.total = total;
|
|
71
|
+
|
|
72
|
+
if (!this.silent && total > 0) {
|
|
73
|
+
this.output.write(`[VERAX] [2/4] Validate: ${current}/${total} routes checked\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Report observe phase progress
|
|
79
|
+
*/
|
|
80
|
+
reportObserveProgress(discovered, executed, skipped = 0, skippedByReason = {}) {
|
|
81
|
+
this.interactionStats.discovered = discovered;
|
|
82
|
+
this.interactionStats.executed = executed;
|
|
83
|
+
this.interactionStats.skipped = skipped;
|
|
84
|
+
this.interactionStats.skippedByReason = skippedByReason;
|
|
85
|
+
|
|
86
|
+
if (!this.silent) {
|
|
87
|
+
this.output.write(`[VERAX] [3/4] Observe: ${executed}/${discovered} interactions executed`);
|
|
88
|
+
if (skipped > 0) {
|
|
89
|
+
const reasons = Object.entries(skippedByReason)
|
|
90
|
+
.map(([reason, count]) => `${reason}=${count}`)
|
|
91
|
+
.join(', ');
|
|
92
|
+
this.output.write(` (skipped: ${skipped} [${reasons}])\n`);
|
|
93
|
+
} else {
|
|
94
|
+
this.output.write('\n');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Report detect phase progress
|
|
101
|
+
*/
|
|
102
|
+
reportDetectProgress(evaluated, total, used = 0, unused = 0, unusedByReason = {}) {
|
|
103
|
+
this.phaseProgress.detect.current = evaluated;
|
|
104
|
+
this.phaseProgress.detect.total = total;
|
|
105
|
+
this.expectationStats.total = total;
|
|
106
|
+
this.expectationStats.used = used;
|
|
107
|
+
this.expectationStats.unused = unused;
|
|
108
|
+
this.expectationStats.unusedByReason = unusedByReason;
|
|
109
|
+
|
|
110
|
+
if (!this.silent && total > 0) {
|
|
111
|
+
this.output.write(`[VERAX] [4/4] Detect: ${evaluated}/${total} expectations evaluated`);
|
|
112
|
+
if (unused > 0) {
|
|
113
|
+
const reasons = Object.entries(unusedByReason)
|
|
114
|
+
.map(([reason, count]) => `${reason}=${count}`)
|
|
115
|
+
.join(', ');
|
|
116
|
+
this.output.write(` (unused: ${unused} [${reasons}])\n`);
|
|
117
|
+
} else {
|
|
118
|
+
this.output.write('\n');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get progress statistics
|
|
125
|
+
*/
|
|
126
|
+
getProgressStats() {
|
|
127
|
+
return {
|
|
128
|
+
learn: {
|
|
129
|
+
current: this.phaseProgress.learn.current,
|
|
130
|
+
total: this.phaseProgress.learn.total
|
|
131
|
+
},
|
|
132
|
+
validate: {
|
|
133
|
+
current: this.phaseProgress.validate.current,
|
|
134
|
+
total: this.phaseProgress.validate.total
|
|
135
|
+
},
|
|
136
|
+
observe: {
|
|
137
|
+
current: this.interactionStats.executed,
|
|
138
|
+
total: this.interactionStats.discovered
|
|
139
|
+
},
|
|
140
|
+
detect: {
|
|
141
|
+
current: this.phaseProgress.detect.current,
|
|
142
|
+
total: this.phaseProgress.detect.total
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get interaction statistics
|
|
149
|
+
*/
|
|
150
|
+
getInteractionStats() {
|
|
151
|
+
return {
|
|
152
|
+
discovered: this.interactionStats.discovered,
|
|
153
|
+
executed: this.interactionStats.executed,
|
|
154
|
+
skipped: this.interactionStats.skipped,
|
|
155
|
+
skippedByReason: this.interactionStats.skippedByReason
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get expectation usage statistics
|
|
161
|
+
*/
|
|
162
|
+
getExpectationUsageStats() {
|
|
163
|
+
return {
|
|
164
|
+
total: this.expectationStats.total,
|
|
165
|
+
used: this.expectationStats.used,
|
|
166
|
+
unused: this.expectationStats.unused,
|
|
167
|
+
unusedByReason: this.expectationStats.unusedByReason
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
@@ -42,9 +42,10 @@ export function isRetryableError(error) {
|
|
|
42
42
|
* Retry an operation with exponential backoff.
|
|
43
43
|
* @param {Function} fn - Async function to retry
|
|
44
44
|
* @param {string} operationName - For logging
|
|
45
|
+
* @param {Object} decisionRecorder - Optional DecisionRecorder for Phase 6 determinism tracking
|
|
45
46
|
* @returns {Promise<{result: *, retriesUsed: number}>}
|
|
46
47
|
*/
|
|
47
|
-
export async function retryOperation(fn, operationName = 'operation') {
|
|
48
|
+
export async function retryOperation(fn, operationName = 'operation', decisionRecorder = null) {
|
|
48
49
|
let lastError = null;
|
|
49
50
|
let retriesUsed = 0;
|
|
50
51
|
|
|
@@ -59,6 +60,18 @@ export async function retryOperation(fn, operationName = 'operation') {
|
|
|
59
60
|
if (attempt < MAX_RETRIES && isRetryableError(error)) {
|
|
60
61
|
retriesUsed++;
|
|
61
62
|
const delayMs = RETRY_DELAYS[attempt];
|
|
63
|
+
|
|
64
|
+
// PHASE 6: Record retry decision for determinism tracking
|
|
65
|
+
if (decisionRecorder && decisionRecorder.record) {
|
|
66
|
+
const { recordRetryAttempt } = await import('../core/determinism-model.js');
|
|
67
|
+
recordRetryAttempt(decisionRecorder, {
|
|
68
|
+
attempt: attempt + 1,
|
|
69
|
+
errorType: lastError.message,
|
|
70
|
+
backoffMs: delayMs,
|
|
71
|
+
operationName
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
62
75
|
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
63
76
|
// Continue to next attempt
|
|
64
77
|
} else {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root-level artifacts helper
|
|
3
|
+
* Enforces strict routing of outputs to artifacts/* subdirectories.
|
|
4
|
+
*/
|
|
5
|
+
import { resolve, join } from 'path';
|
|
6
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
7
|
+
|
|
8
|
+
const BASE = 'artifacts';
|
|
9
|
+
const TYPE_DIRS = {
|
|
10
|
+
'test-runs': 'test-runs',
|
|
11
|
+
'tmp': 'tmp',
|
|
12
|
+
'debug': 'debug',
|
|
13
|
+
'logs': 'logs',
|
|
14
|
+
'legacy': 'legacy',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Ensure directory exists
|
|
19
|
+
*/
|
|
20
|
+
function ensureDir(dirPath) {
|
|
21
|
+
if (!existsSync(dirPath)) {
|
|
22
|
+
mkdirSync(dirPath, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get an absolute path for an artifact output, enforcing isolation.
|
|
28
|
+
* @param {('test-runs'|'tmp'|'debug'|'logs'|'legacy')} type
|
|
29
|
+
* @param {string} name - filename or subpath (no leading slashes)
|
|
30
|
+
* @returns {string} absolute path to write into
|
|
31
|
+
*/
|
|
32
|
+
export function getArtifactPath(type, name) {
|
|
33
|
+
const subDir = TYPE_DIRS[type];
|
|
34
|
+
if (!subDir) throw new Error(`Unknown artifact type: ${type}`);
|
|
35
|
+
const dir = resolve(BASE, subDir);
|
|
36
|
+
ensureDir(dir);
|
|
37
|
+
return resolve(dir, name);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get directory for a given artifact type.
|
|
42
|
+
*/
|
|
43
|
+
export function getArtifactDir(type) {
|
|
44
|
+
const subDir = TYPE_DIRS[type];
|
|
45
|
+
if (!subDir) throw new Error(`Unknown artifact type: ${type}`);
|
|
46
|
+
const dir = resolve(BASE, subDir);
|
|
47
|
+
ensureDir(dir);
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScanBudget - Single source of truth for all execution limits
|
|
3
|
+
*
|
|
4
|
+
* All execution-related limits are controlled through this object.
|
|
5
|
+
* No hardcoded limits should exist in execution code paths.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} ScanBudget
|
|
10
|
+
* @property {number} maxTotalInteractions - Maximum total interactions across all pages
|
|
11
|
+
* @property {number} maxInteractionsPerPage - Maximum interactions to discover/execute per page
|
|
12
|
+
* @property {number} maxPages - Maximum pages to visit (currently not used, but reserved for future)
|
|
13
|
+
* @property {number} maxFlows - Maximum flows to execute per run
|
|
14
|
+
* @property {number} maxFlowSteps - Maximum steps per flow
|
|
15
|
+
* @property {number} maxScanDurationMs - Maximum total scan duration in milliseconds
|
|
16
|
+
* @property {number} interactionTimeoutMs - Timeout for individual interaction execution
|
|
17
|
+
* @property {number} navigationTimeoutMs - Timeout for navigation waits
|
|
18
|
+
* @property {number} stabilizationWindowMs - Total stabilization window (mid + end + network wait)
|
|
19
|
+
* @property {number} stabilizationSampleMidMs - First stabilization sample delay
|
|
20
|
+
* @property {number} stabilizationSampleEndMs - Second stabilization sample delay
|
|
21
|
+
* @property {number} networkWaitMs - Additional wait for slow network requests
|
|
22
|
+
* @property {number} navigationStableWaitMs - Wait after navigation before interaction
|
|
23
|
+
* @property {number} initialNavigationTimeoutMs - Timeout for initial page.goto() navigation
|
|
24
|
+
* @property {number} settleTimeoutMs - Timeout for settle operations
|
|
25
|
+
* @property {number} settleIdleMs - Network idle threshold for settle
|
|
26
|
+
* @property {number} settleDomStableMs - DOM stability window for settle
|
|
27
|
+
* @property {number} maxUniqueUrls - Maximum unique normalized URLs (frontier cap)
|
|
28
|
+
* @property {boolean} adaptiveStabilization - Enable adaptive settle extension in THOROUGH/EXHAUSTIVE
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Default scan budget that reproduces current behavior exactly.
|
|
33
|
+
*
|
|
34
|
+
* Current values:
|
|
35
|
+
* - maxInteractionsPerPage: 30 (from interaction-discovery.js)
|
|
36
|
+
* - maxScanDurationMs: 60000 (60 seconds, from observe/index.js)
|
|
37
|
+
* - maxFlowSteps: 5 (from observe/index.js)
|
|
38
|
+
* - maxFlows: 3 (from observe/index.js)
|
|
39
|
+
* - interactionTimeoutMs: 10000 (10 seconds, from interaction-runner.js)
|
|
40
|
+
* - navigationTimeoutMs: 15000 (15 seconds, from interaction-runner.js)
|
|
41
|
+
* - stabilizationSampleMidMs: 500 (from interaction-runner.js)
|
|
42
|
+
* - stabilizationSampleEndMs: 1500 (from interaction-runner.js)
|
|
43
|
+
* - networkWaitMs: 1000 (from interaction-runner.js line 49)
|
|
44
|
+
* - navigationStableWaitMs: 2000 (from browser.js STABLE_WAIT_MS)
|
|
45
|
+
* - initialNavigationTimeoutMs: 30000 (from browser.js page.goto timeout)
|
|
46
|
+
* - settleTimeoutMs: 30000 (from settle.js default)
|
|
47
|
+
* - settleIdleMs: 1500 (from settle.js default)
|
|
48
|
+
* - settleDomStableMs: 2000 (from settle.js default)
|
|
49
|
+
* - stabilizationWindowMs: 3000 (500 + 1000 + 1000 + 500 = total stabilization time)
|
|
50
|
+
* - maxTotalInteractions: unlimited (controlled by maxScanDurationMs)
|
|
51
|
+
* - maxPages: 1 (current implementation only scans starting page)
|
|
52
|
+
*/
|
|
53
|
+
export const DEFAULT_SCAN_BUDGET = {
|
|
54
|
+
maxTotalInteractions: Infinity, // Controlled by maxScanDurationMs in practice
|
|
55
|
+
maxInteractionsPerPage: 30,
|
|
56
|
+
maxPages: 50, // Allow multi-page traversal by default
|
|
57
|
+
maxFlows: 3,
|
|
58
|
+
maxFlowSteps: 5,
|
|
59
|
+
maxScanDurationMs: 60000,
|
|
60
|
+
interactionTimeoutMs: 10000,
|
|
61
|
+
navigationTimeoutMs: 15000,
|
|
62
|
+
stabilizationWindowMs: 3000, // Total: 500 + 1000 + 1000 + 500
|
|
63
|
+
stabilizationSampleMidMs: 500,
|
|
64
|
+
stabilizationSampleEndMs: 1500,
|
|
65
|
+
networkWaitMs: 1000,
|
|
66
|
+
navigationStableWaitMs: 2000,
|
|
67
|
+
initialNavigationTimeoutMs: 30000,
|
|
68
|
+
settleTimeoutMs: 30000,
|
|
69
|
+
settleIdleMs: 1500,
|
|
70
|
+
settleDomStableMs: 2000,
|
|
71
|
+
maxUniqueUrls: 500, // Prevent infinite frontier growth on large sites
|
|
72
|
+
adaptiveStabilization: false // Disabled by default, enabled in THOROUGH/EXHAUSTIVE
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a scan budget with custom values, falling back to defaults.
|
|
77
|
+
* @param {Partial<ScanBudget>} overrides - Partial budget to override defaults
|
|
78
|
+
* @returns {ScanBudget} Complete scan budget
|
|
79
|
+
*/
|
|
80
|
+
export function createScanBudget(overrides = {}) {
|
|
81
|
+
return {
|
|
82
|
+
...DEFAULT_SCAN_BUDGET,
|
|
83
|
+
...overrides
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|