@veraxhq/verax 0.1.0 → 0.2.1

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 (135) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +24 -36
  4. package/src/cli/commands/default.js +681 -0
  5. package/src/cli/commands/doctor.js +197 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +586 -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 +297 -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 +110 -0
  14. package/src/cli/util/expectation-extractor.js +388 -0
  15. package/src/cli/util/findings-writer.js +32 -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 +412 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +30 -0
  21. package/src/cli/util/project-discovery.js +297 -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/runtime-budget.js +147 -0
  26. package/src/cli/util/summary-writer.js +43 -0
  27. package/src/types/global.d.ts +28 -0
  28. package/src/types/ts-ast.d.ts +24 -0
  29. package/src/verax/cli/ci-summary.js +35 -0
  30. package/src/verax/cli/context-explanation.js +89 -0
  31. package/src/verax/cli/doctor.js +277 -0
  32. package/src/verax/cli/error-normalizer.js +154 -0
  33. package/src/verax/cli/explain-output.js +105 -0
  34. package/src/verax/cli/finding-explainer.js +130 -0
  35. package/src/verax/cli/init.js +237 -0
  36. package/src/verax/cli/run-overview.js +163 -0
  37. package/src/verax/cli/url-safety.js +111 -0
  38. package/src/verax/cli/wizard.js +109 -0
  39. package/src/verax/cli/zero-findings-explainer.js +57 -0
  40. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  41. package/src/verax/core/action-classifier.js +86 -0
  42. package/src/verax/core/budget-engine.js +218 -0
  43. package/src/verax/core/canonical-outcomes.js +157 -0
  44. package/src/verax/core/decision-snapshot.js +335 -0
  45. package/src/verax/core/determinism-model.js +432 -0
  46. package/src/verax/core/incremental-store.js +245 -0
  47. package/src/verax/core/invariants.js +356 -0
  48. package/src/verax/core/promise-model.js +230 -0
  49. package/src/verax/core/replay-validator.js +350 -0
  50. package/src/verax/core/replay.js +222 -0
  51. package/src/verax/core/run-id.js +175 -0
  52. package/src/verax/core/run-manifest.js +99 -0
  53. package/src/verax/core/silence-impact.js +369 -0
  54. package/src/verax/core/silence-model.js +523 -0
  55. package/src/verax/detect/comparison.js +7 -34
  56. package/src/verax/detect/confidence-engine.js +764 -329
  57. package/src/verax/detect/detection-engine.js +293 -0
  58. package/src/verax/detect/evidence-index.js +127 -0
  59. package/src/verax/detect/expectation-model.js +241 -168
  60. package/src/verax/detect/explanation-helpers.js +187 -0
  61. package/src/verax/detect/finding-detector.js +450 -0
  62. package/src/verax/detect/findings-writer.js +41 -12
  63. package/src/verax/detect/flow-detector.js +366 -0
  64. package/src/verax/detect/index.js +200 -288
  65. package/src/verax/detect/interactive-findings.js +612 -0
  66. package/src/verax/detect/signal-mapper.js +308 -0
  67. package/src/verax/detect/skip-classifier.js +4 -4
  68. package/src/verax/detect/verdict-engine.js +561 -0
  69. package/src/verax/evidence-index-writer.js +61 -0
  70. package/src/verax/flow/flow-engine.js +3 -2
  71. package/src/verax/flow/flow-spec.js +1 -2
  72. package/src/verax/index.js +103 -15
  73. package/src/verax/intel/effect-detector.js +368 -0
  74. package/src/verax/intel/handler-mapper.js +249 -0
  75. package/src/verax/intel/index.js +281 -0
  76. package/src/verax/intel/route-extractor.js +280 -0
  77. package/src/verax/intel/ts-program.js +256 -0
  78. package/src/verax/intel/vue-navigation-extractor.js +642 -0
  79. package/src/verax/intel/vue-router-extractor.js +325 -0
  80. package/src/verax/learn/action-contract-extractor.js +338 -104
  81. package/src/verax/learn/ast-contract-extractor.js +148 -6
  82. package/src/verax/learn/flow-extractor.js +172 -0
  83. package/src/verax/learn/index.js +36 -2
  84. package/src/verax/learn/manifest-writer.js +122 -58
  85. package/src/verax/learn/project-detector.js +40 -0
  86. package/src/verax/learn/route-extractor.js +28 -97
  87. package/src/verax/learn/route-validator.js +8 -7
  88. package/src/verax/learn/state-extractor.js +212 -0
  89. package/src/verax/learn/static-extractor-navigation.js +114 -0
  90. package/src/verax/learn/static-extractor-validation.js +88 -0
  91. package/src/verax/learn/static-extractor.js +119 -10
  92. package/src/verax/learn/truth-assessor.js +24 -21
  93. package/src/verax/learn/ts-contract-resolver.js +14 -12
  94. package/src/verax/observe/aria-sensor.js +211 -0
  95. package/src/verax/observe/browser.js +30 -6
  96. package/src/verax/observe/console-sensor.js +2 -18
  97. package/src/verax/observe/domain-boundary.js +10 -1
  98. package/src/verax/observe/expectation-executor.js +513 -0
  99. package/src/verax/observe/flow-matcher.js +143 -0
  100. package/src/verax/observe/focus-sensor.js +196 -0
  101. package/src/verax/observe/human-driver.js +660 -273
  102. package/src/verax/observe/index.js +910 -26
  103. package/src/verax/observe/interaction-discovery.js +378 -15
  104. package/src/verax/observe/interaction-runner.js +562 -197
  105. package/src/verax/observe/loading-sensor.js +145 -0
  106. package/src/verax/observe/navigation-sensor.js +255 -0
  107. package/src/verax/observe/network-sensor.js +55 -7
  108. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  109. package/src/verax/observe/observed-expectation.js +305 -0
  110. package/src/verax/observe/page-frontier.js +234 -0
  111. package/src/verax/observe/settle.js +38 -17
  112. package/src/verax/observe/state-sensor.js +393 -0
  113. package/src/verax/observe/state-ui-sensor.js +7 -1
  114. package/src/verax/observe/timing-sensor.js +228 -0
  115. package/src/verax/observe/traces-writer.js +73 -21
  116. package/src/verax/observe/ui-signal-sensor.js +143 -17
  117. package/src/verax/scan-summary-writer.js +80 -15
  118. package/src/verax/shared/artifact-manager.js +111 -9
  119. package/src/verax/shared/budget-profiles.js +136 -0
  120. package/src/verax/shared/caching.js +1 -1
  121. package/src/verax/shared/ci-detection.js +39 -0
  122. package/src/verax/shared/config-loader.js +169 -0
  123. package/src/verax/shared/dynamic-route-utils.js +224 -0
  124. package/src/verax/shared/expectation-coverage.js +44 -0
  125. package/src/verax/shared/expectation-prover.js +81 -0
  126. package/src/verax/shared/expectation-tracker.js +201 -0
  127. package/src/verax/shared/expectations-writer.js +60 -0
  128. package/src/verax/shared/first-run.js +44 -0
  129. package/src/verax/shared/progress-reporter.js +171 -0
  130. package/src/verax/shared/retry-policy.js +9 -1
  131. package/src/verax/shared/root-artifacts.js +49 -0
  132. package/src/verax/shared/scan-budget.js +86 -0
  133. package/src/verax/shared/url-normalizer.js +162 -0
  134. package/src/verax/shared/zip-artifacts.js +66 -0
  135. package/src/verax/validate/context-validator.js +244 -0
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * VERAX CLI Entry Point
5
+ *
6
+ * Commands:
7
+ * - verax (smart default with URL detection/prompting)
8
+ * - verax run --url <url> (strict, non-interactive)
9
+ * - verax inspect <runPath> (read and display run summary)
10
+ *
11
+ * Exit codes:
12
+ * - 0: success
13
+ * - 2: internal crash
14
+ * - 64: invalid CLI usage
15
+ * - 65: invalid input data
16
+ */
17
+
18
+ import { fileURLToPath } from 'url';
19
+ import { dirname, resolve } from 'path';
20
+ import { readFileSync } from 'fs';
21
+ import { defaultCommand } from './commands/default.js';
22
+ import { runCommand } from './commands/run.js';
23
+ import { inspectCommand } from './commands/inspect.js';
24
+ import { doctorCommand } from './commands/doctor.js';
25
+ import { getExitCode, UsageError } from './util/errors.js';
26
+
27
+ const __filename = fileURLToPath(import.meta.url);
28
+ const __dirname = dirname(__filename);
29
+
30
+ // Read package.json for version
31
+ function getVersion() {
32
+ try {
33
+ const pkgPath = resolve(__dirname, '../../package.json');
34
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
35
+ return pkg.version;
36
+ } catch {
37
+ return 'unknown';
38
+ }
39
+ }
40
+
41
+ async function main() {
42
+ const args = process.argv.slice(2);
43
+
44
+ try {
45
+ // Handle --version
46
+ if (args.includes('--version') || args.includes('-v')) {
47
+ console.log(`verax ${getVersion()}`);
48
+ process.exit(0);
49
+ }
50
+
51
+ // Handle explicit --help
52
+ if (args.includes('--help') || args.includes('-h')) {
53
+ showHelp();
54
+ process.exit(0);
55
+ }
56
+
57
+ // If no args, run default command
58
+ if (args.length === 0) {
59
+ await defaultCommand({});
60
+ process.exit(0);
61
+ }
62
+
63
+ const command = args[0];
64
+
65
+ // Handle 'run' command
66
+ if (command === 'run') {
67
+ const url = parseArg(args, '--url');
68
+ const src = parseArg(args, '--src') || '.';
69
+ const out = parseArg(args, '--out') || '.verax';
70
+ const json = args.includes('--json');
71
+ const verbose = args.includes('--verbose');
72
+
73
+ if (!url) {
74
+ throw new UsageError('run command requires --url <url> argument');
75
+ }
76
+
77
+ await runCommand({ url, src, out, json, verbose });
78
+ process.exit(0);
79
+ }
80
+
81
+ // Handle 'inspect' command
82
+ if (command === 'inspect') {
83
+ if (args.length < 2) {
84
+ throw new UsageError('inspect command requires a run path argument');
85
+ }
86
+
87
+ const runPath = args[1];
88
+ const json = args.includes('--json');
89
+
90
+ await inspectCommand(runPath, { json });
91
+ process.exit(0);
92
+ }
93
+
94
+ // Handle 'doctor' command
95
+ if (command === 'doctor') {
96
+ const allowedFlags = new Set(['--json']);
97
+ const extraFlags = args.slice(1).filter((a) => a.startsWith('-') && !allowedFlags.has(a));
98
+ const json = args.includes('--json');
99
+ await doctorCommand({ json, extraFlags });
100
+ process.exit(0);
101
+ }
102
+
103
+ // Handle 'help' command
104
+ if (command === 'help' || command === '--help' || command === '-h') {
105
+ showHelp();
106
+ process.exit(0);
107
+ }
108
+
109
+ // Default command: smart scanning mode
110
+ // Options can be passed as flags before/after the default command position
111
+ const url = parseArg(args, '--url');
112
+ const src = parseArg(args, '--src') || '.';
113
+ const out = parseArg(args, '--out') || '.verax';
114
+ const json = args.includes('--json');
115
+ const verbose = args.includes('--verbose');
116
+
117
+ await defaultCommand({ url, src, out, json, verbose });
118
+ process.exit(0);
119
+ } catch (error) {
120
+ // Print error message
121
+ if (error.message) {
122
+ console.error(`Error: ${error.message}`);
123
+ }
124
+
125
+ // Get exit code
126
+ const exitCode = getExitCode(error);
127
+ process.exit(exitCode);
128
+ }
129
+ }
130
+
131
+ function showHelp() {
132
+ const version = getVersion();
133
+ console.log(`
134
+ verax ${version}
135
+ VERAX — Silent failure detection for websites
136
+
137
+ USAGE:
138
+ verax [options] Smart mode (detects/prompts for URL)
139
+ verax run --url <url> [options] Strict mode (non-interactive, CI-friendly)
140
+ verax inspect <runPath> [--json] Inspect an existing run
141
+ verax doctor [--json] Diagnose local environment
142
+ verax --version Show version
143
+ verax --help Show this help
144
+
145
+ OPTIONS:
146
+ --url <url> Target URL to scan
147
+ --src <path> Source directory (default: .)
148
+ --out <path> Output directory for artifacts (default: .verax)
149
+ --json Output as JSON lines (progress events)
150
+ --verbose Verbose output
151
+ --help Show this help
152
+ --version Show version
153
+
154
+ EXAMPLES:
155
+ # Smart mode (interactive if needed)
156
+ verax
157
+
158
+ # Smart mode with explicit URL
159
+ verax --url https://example.com
160
+
161
+ # Strict mode (CI-friendly, non-interactive)
162
+ verax run --url https://example.com --src . --out .verax
163
+
164
+ # Inspect previous run
165
+ verax inspect .verax/runs/2026-01-11T00-59-12Z_4f2a9c
166
+
167
+ EXIT CODES:
168
+ 0 Success (tool executed)
169
+ 2 Internal crash
170
+ 64 Invalid CLI usage (missing args, invalid flags)
171
+ 65 Invalid input data (bad JSON, unreadable folder, etc.)
172
+
173
+ ARTIFACTS:
174
+ Artifacts are written to: <out>/runs/<runId>/
175
+ Required files:
176
+ - run.status.json Run status lifecycle
177
+ - run.meta.json Metadata about the run
178
+ - summary.json Summary of results
179
+ - findings.json Array of findings
180
+ - traces.jsonl JSONL traces of execution
181
+ - evidence/ Directory for evidence files
182
+ `);
183
+ }
184
+
185
+ function parseArg(args, name) {
186
+ const index = args.indexOf(name);
187
+ if (index !== -1 && index + 1 < args.length) {
188
+ return args[index + 1];
189
+ }
190
+ return null;
191
+ }
192
+
193
+ main().catch((error) => {
194
+ console.error(`Fatal error: ${error.message}`);
195
+ process.exit(2);
196
+ });
@@ -0,0 +1,37 @@
1
+ import { writeFileSync, renameSync, mkdirSync } from 'fs';
2
+ import { dirname } from 'path';
3
+
4
+ /**
5
+ * Atomic write for JSON files
6
+ * Writes to a temp file and renames to prevent partial writes
7
+ */
8
+ export function atomicWriteJson(filePath, data) {
9
+ const tempPath = `${filePath}.tmp`;
10
+ const dirPath = dirname(filePath);
11
+
12
+ // Ensure directory exists
13
+ mkdirSync(dirPath, { recursive: true });
14
+
15
+ // Write to temp file
16
+ writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf8');
17
+
18
+ // Atomic rename
19
+ renameSync(tempPath, filePath);
20
+ }
21
+
22
+ /**
23
+ * Atomic write for text files (JSONL, logs, etc.)
24
+ */
25
+ export function atomicWriteText(filePath, content) {
26
+ const tempPath = `${filePath}.tmp`;
27
+ const dirPath = dirname(filePath);
28
+
29
+ // Ensure directory exists
30
+ mkdirSync(dirPath, { recursive: true });
31
+
32
+ // Write to temp file
33
+ writeFileSync(tempPath, content, 'utf8');
34
+
35
+ // Atomic rename
36
+ renameSync(tempPath, filePath);
37
+ }
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Detection Engine
3
+ * Compares learned expectations against observations to detect silent failures.
4
+ * Produces exactly one finding per expectation with deterministic confidence and impact.
5
+ */
6
+
7
+ export async function detectFindings(learnData, observeData, projectPath, onProgress, options = {}) {
8
+ const log = options.silent ? () => {} : console.log;
9
+ const findings = [];
10
+ const stats = {
11
+ total: 0,
12
+ silentFailures: 0,
13
+ observed: 0,
14
+ coverageGaps: 0,
15
+ unproven: 0,
16
+ informational: 0,
17
+ };
18
+
19
+ const observationMap = indexObservations(observeData);
20
+ const expectations = learnData?.expectations || [];
21
+
22
+ const narration = [];
23
+
24
+ for (let i = 0; i < expectations.length; i++) {
25
+ const expectation = expectations[i];
26
+ const index = i + 1;
27
+
28
+ if (onProgress) {
29
+ onProgress({
30
+ event: 'detect:attempt',
31
+ index,
32
+ total: expectations.length,
33
+ expectationId: expectation.id,
34
+ type: expectation.type,
35
+ value: expectation?.promise?.value,
36
+ });
37
+ }
38
+
39
+ const observation = getObservationForExpectation(expectation, observationMap);
40
+ const finding = classifyExpectation(expectation, observation);
41
+
42
+ findings.push(finding);
43
+ stats.total++;
44
+ stats[findingStatKey(finding.classification)]++;
45
+
46
+ const icon = classificationIcon(finding.classification);
47
+ narration.push(`${icon} ${finding.classification.toUpperCase()} ${finding.promise?.value || ''}`.trim());
48
+
49
+ if (onProgress) {
50
+ onProgress({
51
+ event: 'detect:classified',
52
+ index,
53
+ classification: finding.classification,
54
+ impact: finding.impact,
55
+ confidence: finding.confidence,
56
+ expectationId: expectation.id,
57
+ });
58
+ }
59
+ }
60
+
61
+ // Terminal narration (one line per finding + summary)
62
+ narration.forEach((line) => log(line));
63
+ log(`SUMMARY findings=${findings.length} observed=${stats.observed} silent-failure=${stats.silentFailures} coverage-gap=${stats.coverageGaps} unproven=${stats.unproven}`);
64
+
65
+ // Emit completion event
66
+ if (onProgress) {
67
+ onProgress({
68
+ event: 'detect:completed',
69
+ total: findings.length,
70
+ stats,
71
+ });
72
+ }
73
+
74
+ return {
75
+ findings,
76
+ stats,
77
+ detectedAt: new Date().toISOString(),
78
+ };
79
+ }
80
+
81
+ function indexObservations(observeData) {
82
+ const map = new Map();
83
+ if (!observeData || !Array.isArray(observeData.observations)) return map;
84
+
85
+ observeData.observations.forEach((obs) => {
86
+ if (obs == null) return;
87
+ const keys = [];
88
+ if (obs.id) keys.push(obs.id);
89
+ if (obs.expectationId) keys.push(obs.expectationId);
90
+ keys.forEach((key) => {
91
+ if (!map.has(key)) map.set(key, obs);
92
+ });
93
+ });
94
+ return map;
95
+ }
96
+
97
+ function getObservationForExpectation(expectation, observationMap) {
98
+ if (!expectation || !expectation.id) return null;
99
+ return observationMap.get(expectation.id) || null;
100
+ }
101
+
102
+ function classificationIcon(classification) {
103
+ switch (classification) {
104
+ case 'observed':
105
+ return '✓';
106
+ case 'silent-failure':
107
+ return '✗';
108
+ case 'coverage-gap':
109
+ return '⚠';
110
+ case 'unproven':
111
+ return '⚠';
112
+ default:
113
+ return '•';
114
+ }
115
+ }
116
+
117
+ function findingStatKey(classification) {
118
+ switch (classification) {
119
+ case 'silent-failure':
120
+ return 'silentFailures';
121
+ case 'observed':
122
+ return 'observed';
123
+ case 'coverage-gap':
124
+ return 'coverageGaps';
125
+ case 'unproven':
126
+ return 'unproven';
127
+ default:
128
+ return 'informational';
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Classify a single expectation according to deterministic rules.
134
+ */
135
+ function classifyExpectation(expectation, observation) {
136
+ const finding = {
137
+ id: expectation.id,
138
+ type: expectation.type,
139
+ promise: expectation.promise,
140
+ source: expectation.source,
141
+ classification: 'informational',
142
+ impact: 'LOW',
143
+ confidence: 0,
144
+ evidence: [],
145
+ reason: null,
146
+ };
147
+
148
+ const attempted = Boolean(observation?.attempted);
149
+ const observed = observation?.observed === true;
150
+ const reason = observation?.reason || null;
151
+
152
+ const evidence = normalizeEvidence(observation?.evidenceFiles || []);
153
+ finding.evidence = evidence;
154
+
155
+ const evidenceSignals = analyzeEvidenceSignals(observation, evidence);
156
+
157
+ // 1) observed
158
+ if (observed) {
159
+ finding.classification = 'observed';
160
+ finding.reason = 'Expectation observed at runtime';
161
+ finding.impact = getImpact(expectation);
162
+ finding.confidence = 1.0;
163
+ return finding;
164
+ }
165
+
166
+ // 2) coverage-gap (not attempted or explicitly blocked)
167
+ if (!attempted || isSafetySkip(reason)) {
168
+ finding.classification = 'coverage-gap';
169
+ finding.reason = reason || 'No observation attempt recorded';
170
+ finding.impact = 'LOW';
171
+ finding.confidence = 0;
172
+ return finding;
173
+ }
174
+
175
+ // 3) silent-failure (attempted, observed === false, no safety skip)
176
+ if (attempted && observation?.observed === false && !isSafetySkip(reason)) {
177
+ finding.classification = 'silent-failure';
178
+ finding.reason = reason || 'Expected behavior not observed';
179
+ finding.impact = getImpact(expectation);
180
+ finding.confidence = calculateConfidence(expectation, evidenceSignals, 'silent-failure');
181
+ return finding;
182
+ }
183
+
184
+ // 4) unproven (attempted, ambiguous evidence)
185
+ if (attempted && !observed) {
186
+ finding.classification = 'unproven';
187
+ finding.reason = reason || 'Attempted but evidence insufficient';
188
+ finding.impact = 'MEDIUM';
189
+ finding.confidence = calculateConfidence(expectation, evidenceSignals, 'unproven');
190
+ return finding;
191
+ }
192
+
193
+ // 5) informational fallback
194
+ finding.classification = 'informational';
195
+ finding.reason = reason || 'No classification rule matched';
196
+ finding.impact = 'LOW';
197
+ finding.confidence = calculateConfidence(expectation, evidenceSignals, 'informational');
198
+ return finding;
199
+ }
200
+
201
+ function isSafetySkip(reason) {
202
+ if (!reason) return false;
203
+ const lower = reason.toLowerCase();
204
+ const safetyIndicators = ['blocked', 'timeout', 'not found', 'safety', 'permission', 'denied', 'captcha', 'forbidden'];
205
+ return safetyIndicators.some((indicator) => lower.includes(indicator));
206
+ }
207
+
208
+ /**
209
+ * Deterministic confidence calculation.
210
+ * Screenshots + network + DOM change => >=0.8
211
+ * Screenshots only => ~0.6
212
+ * Weak signals => <0.5
213
+ */
214
+ function calculateConfidence(expectation, evidenceSignals, classification) {
215
+ if (classification === 'observed') return 1.0;
216
+ if (classification === 'coverage-gap') return 0;
217
+
218
+ const { hasScreenshots, hasNetworkLogs, hasDomChange } = evidenceSignals;
219
+
220
+ if (classification === 'silent-failure') {
221
+ if (hasScreenshots && hasNetworkLogs && hasDomChange) return 0.85;
222
+ if (hasScreenshots && hasNetworkLogs) return 0.75;
223
+ if (hasScreenshots && hasDomChange) return 0.7;
224
+ if (hasScreenshots) return 0.6;
225
+ if (hasNetworkLogs || hasDomChange) return 0.5;
226
+ return 0.4; // weak signals, attempted but no evidence
227
+ }
228
+
229
+ if (classification === 'unproven') {
230
+ if (hasScreenshots || hasNetworkLogs || hasDomChange) return 0.45;
231
+ return 0.3;
232
+ }
233
+
234
+ return 0.3;
235
+ }
236
+
237
+ function analyzeEvidenceSignals(observation, evidence) {
238
+ const hasScreenshots = evidence.some((e) => e.type === 'screenshot');
239
+ const hasNetworkLogs = evidence.some((e) => e.type === 'network-log');
240
+ const hasDomChange = Boolean(
241
+ observation?.domChanged ||
242
+ observation?.domChange === true ||
243
+ observation?.sensors?.uiSignals?.diff?.changed
244
+ );
245
+
246
+ return { hasScreenshots, hasNetworkLogs, hasDomChange };
247
+ }
248
+
249
+ function normalizeEvidence(evidenceFiles) {
250
+ if (!Array.isArray(evidenceFiles)) return [];
251
+ return evidenceFiles
252
+ .filter(Boolean)
253
+ .map((file) => ({
254
+ type: getEvidenceType(file),
255
+ path: file,
256
+ available: true,
257
+ }));
258
+ }
259
+
260
+ /**
261
+ * Impact classification per product spec.
262
+ */
263
+ function getImpact(expectation) {
264
+ const type = expectation?.type;
265
+ const value = expectation?.promise?.value || '';
266
+ const valueStr = String(value).toLowerCase();
267
+
268
+ if (type === 'navigation') {
269
+ const primaryRoutes = ['/', '/home', '/about', '/contact', '/products', '/pricing', '/features', '/login', '/signup'];
270
+ if (primaryRoutes.includes(value)) return 'HIGH';
271
+ if (valueStr.includes('admin') || valueStr.includes('dashboard') || valueStr.includes('settings')) return 'MEDIUM';
272
+ if (valueStr.includes('privacy') || valueStr.includes('terms') || valueStr.includes('footer')) return 'LOW';
273
+ return 'MEDIUM';
274
+ }
275
+
276
+ if (type === 'network') {
277
+ if (valueStr.includes('/api/auth') || valueStr.includes('/api/payment') || valueStr.includes('/api/user')) return 'HIGH';
278
+ if (valueStr.includes('api.')) return 'MEDIUM';
279
+ if (valueStr.includes('/api/') && !valueStr.includes('/api/analytics')) return 'MEDIUM';
280
+ return 'LOW';
281
+ }
282
+
283
+ if (type === 'state') {
284
+ return 'MEDIUM';
285
+ }
286
+
287
+ return 'LOW';
288
+ }
289
+
290
+ function getEvidenceType(filename) {
291
+ if (!filename) return 'unknown';
292
+ const lower = filename.toLowerCase();
293
+ if (lower.endsWith('.png') || lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'screenshot';
294
+ if (lower.endsWith('.json')) return 'network-log';
295
+ if (lower.endsWith('.log') || lower.endsWith('.txt')) return 'console-log';
296
+ return 'artifact';
297
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Attempt to infer URL from environment variables and common dev configs
3
+ */
4
+ export function tryResolveUrlFromEnv() {
5
+ // Check common environment variables
6
+ const candidates = [
7
+ process.env.VERCEL_URL,
8
+ process.env.NEXT_PUBLIC_SITE_URL,
9
+ process.env.SITE_URL,
10
+ process.env.PUBLIC_URL,
11
+ ];
12
+
13
+ for (const candidate of candidates) {
14
+ if (candidate) {
15
+ // Ensure it's a valid URL
16
+ if (candidate.startsWith('http://') || candidate.startsWith('https://')) {
17
+ return candidate;
18
+ }
19
+ // If it's a bare domain, assume https
20
+ if (candidate.includes('.') && !candidate.includes(' ')) {
21
+ return `https://${candidate}`;
22
+ }
23
+ }
24
+ }
25
+
26
+ // Check for localhost/PORT
27
+ if (process.env.PORT) {
28
+ const port = process.env.PORT;
29
+ return `http://localhost:${port}`;
30
+ }
31
+
32
+ return null;
33
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * CLI Error System
3
+ * Maps errors to exit codes:
4
+ * - 0: success (tool executed)
5
+ * - 2: internal crash
6
+ * - 64: invalid CLI usage
7
+ * - 65: invalid input data
8
+ */
9
+
10
+ export class CLIError extends Error {
11
+ constructor(message, exitCode = 2) {
12
+ super(message);
13
+ this.name = 'CLIError';
14
+ this.exitCode = exitCode;
15
+ }
16
+ }
17
+
18
+ export class UsageError extends CLIError {
19
+ constructor(message) {
20
+ super(message, 64);
21
+ this.name = 'UsageError';
22
+ }
23
+ }
24
+
25
+ export class DataError extends CLIError {
26
+ constructor(message) {
27
+ super(message, 65);
28
+ this.name = 'DataError';
29
+ }
30
+ }
31
+
32
+ export class CrashError extends CLIError {
33
+ constructor(message) {
34
+ super(message, 2);
35
+ this.name = 'CrashError';
36
+ }
37
+ }
38
+
39
+ export function getExitCode(error) {
40
+ if (error instanceof CLIError) {
41
+ return error.exitCode;
42
+ }
43
+ return 2; // default to crash
44
+ }