@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.
Files changed (126) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +14 -36
  4. package/src/cli/commands/default.js +523 -0
  5. package/src/cli/commands/doctor.js +165 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +402 -0
  8. package/src/cli/entry.js +196 -0
  9. package/src/cli/util/atomic-write.js +37 -0
  10. package/src/cli/util/detection-engine.js +296 -0
  11. package/src/cli/util/env-url.js +33 -0
  12. package/src/cli/util/errors.js +44 -0
  13. package/src/cli/util/events.js +34 -0
  14. package/src/cli/util/expectation-extractor.js +378 -0
  15. package/src/cli/util/findings-writer.js +31 -0
  16. package/src/cli/util/idgen.js +87 -0
  17. package/src/cli/util/learn-writer.js +39 -0
  18. package/src/cli/util/observation-engine.js +366 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +29 -0
  21. package/src/cli/util/project-discovery.js +277 -0
  22. package/src/cli/util/project-writer.js +26 -0
  23. package/src/cli/util/redact.js +128 -0
  24. package/src/cli/util/run-id.js +30 -0
  25. package/src/cli/util/summary-writer.js +32 -0
  26. package/src/verax/cli/ci-summary.js +35 -0
  27. package/src/verax/cli/context-explanation.js +89 -0
  28. package/src/verax/cli/doctor.js +277 -0
  29. package/src/verax/cli/error-normalizer.js +154 -0
  30. package/src/verax/cli/explain-output.js +105 -0
  31. package/src/verax/cli/finding-explainer.js +130 -0
  32. package/src/verax/cli/init.js +237 -0
  33. package/src/verax/cli/run-overview.js +163 -0
  34. package/src/verax/cli/url-safety.js +101 -0
  35. package/src/verax/cli/wizard.js +98 -0
  36. package/src/verax/cli/zero-findings-explainer.js +57 -0
  37. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  38. package/src/verax/core/action-classifier.js +86 -0
  39. package/src/verax/core/budget-engine.js +218 -0
  40. package/src/verax/core/canonical-outcomes.js +157 -0
  41. package/src/verax/core/decision-snapshot.js +335 -0
  42. package/src/verax/core/determinism-model.js +403 -0
  43. package/src/verax/core/incremental-store.js +237 -0
  44. package/src/verax/core/invariants.js +356 -0
  45. package/src/verax/core/promise-model.js +230 -0
  46. package/src/verax/core/replay-validator.js +350 -0
  47. package/src/verax/core/replay.js +222 -0
  48. package/src/verax/core/run-id.js +175 -0
  49. package/src/verax/core/run-manifest.js +99 -0
  50. package/src/verax/core/silence-impact.js +369 -0
  51. package/src/verax/core/silence-model.js +521 -0
  52. package/src/verax/detect/comparison.js +2 -34
  53. package/src/verax/detect/confidence-engine.js +764 -329
  54. package/src/verax/detect/detection-engine.js +293 -0
  55. package/src/verax/detect/evidence-index.js +177 -0
  56. package/src/verax/detect/expectation-model.js +194 -172
  57. package/src/verax/detect/explanation-helpers.js +187 -0
  58. package/src/verax/detect/finding-detector.js +450 -0
  59. package/src/verax/detect/findings-writer.js +44 -8
  60. package/src/verax/detect/flow-detector.js +366 -0
  61. package/src/verax/detect/index.js +172 -286
  62. package/src/verax/detect/interactive-findings.js +613 -0
  63. package/src/verax/detect/signal-mapper.js +308 -0
  64. package/src/verax/detect/verdict-engine.js +563 -0
  65. package/src/verax/evidence-index-writer.js +61 -0
  66. package/src/verax/index.js +90 -14
  67. package/src/verax/intel/effect-detector.js +368 -0
  68. package/src/verax/intel/handler-mapper.js +249 -0
  69. package/src/verax/intel/index.js +281 -0
  70. package/src/verax/intel/route-extractor.js +280 -0
  71. package/src/verax/intel/ts-program.js +256 -0
  72. package/src/verax/intel/vue-navigation-extractor.js +579 -0
  73. package/src/verax/intel/vue-router-extractor.js +323 -0
  74. package/src/verax/learn/action-contract-extractor.js +335 -101
  75. package/src/verax/learn/ast-contract-extractor.js +95 -5
  76. package/src/verax/learn/flow-extractor.js +172 -0
  77. package/src/verax/learn/manifest-writer.js +97 -47
  78. package/src/verax/learn/project-detector.js +40 -0
  79. package/src/verax/learn/route-extractor.js +27 -96
  80. package/src/verax/learn/state-extractor.js +212 -0
  81. package/src/verax/learn/static-extractor-navigation.js +114 -0
  82. package/src/verax/learn/static-extractor-validation.js +88 -0
  83. package/src/verax/learn/static-extractor.js +112 -4
  84. package/src/verax/learn/truth-assessor.js +24 -21
  85. package/src/verax/observe/aria-sensor.js +211 -0
  86. package/src/verax/observe/browser.js +10 -5
  87. package/src/verax/observe/console-sensor.js +1 -17
  88. package/src/verax/observe/domain-boundary.js +10 -1
  89. package/src/verax/observe/expectation-executor.js +512 -0
  90. package/src/verax/observe/flow-matcher.js +143 -0
  91. package/src/verax/observe/focus-sensor.js +196 -0
  92. package/src/verax/observe/human-driver.js +643 -275
  93. package/src/verax/observe/index.js +908 -27
  94. package/src/verax/observe/index.js.backup +1 -0
  95. package/src/verax/observe/interaction-discovery.js +365 -14
  96. package/src/verax/observe/interaction-runner.js +563 -198
  97. package/src/verax/observe/loading-sensor.js +139 -0
  98. package/src/verax/observe/navigation-sensor.js +255 -0
  99. package/src/verax/observe/network-sensor.js +55 -7
  100. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  101. package/src/verax/observe/observed-expectation.js +305 -0
  102. package/src/verax/observe/page-frontier.js +234 -0
  103. package/src/verax/observe/settle.js +37 -17
  104. package/src/verax/observe/state-sensor.js +389 -0
  105. package/src/verax/observe/timing-sensor.js +228 -0
  106. package/src/verax/observe/traces-writer.js +61 -20
  107. package/src/verax/observe/ui-signal-sensor.js +136 -17
  108. package/src/verax/scan-summary-writer.js +77 -15
  109. package/src/verax/shared/artifact-manager.js +110 -8
  110. package/src/verax/shared/budget-profiles.js +136 -0
  111. package/src/verax/shared/ci-detection.js +39 -0
  112. package/src/verax/shared/config-loader.js +170 -0
  113. package/src/verax/shared/dynamic-route-utils.js +218 -0
  114. package/src/verax/shared/expectation-coverage.js +44 -0
  115. package/src/verax/shared/expectation-prover.js +81 -0
  116. package/src/verax/shared/expectation-tracker.js +201 -0
  117. package/src/verax/shared/expectations-writer.js +60 -0
  118. package/src/verax/shared/first-run.js +44 -0
  119. package/src/verax/shared/progress-reporter.js +171 -0
  120. package/src/verax/shared/retry-policy.js +14 -1
  121. package/src/verax/shared/root-artifacts.js +49 -0
  122. package/src/verax/shared/scan-budget.js +86 -0
  123. package/src/verax/shared/url-normalizer.js +162 -0
  124. package/src/verax/shared/zip-artifacts.js +65 -0
  125. package/src/verax/validate/context-validator.js +244 -0
  126. 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
+