@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.
- package/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +24 -36
- package/src/cli/commands/default.js +681 -0
- package/src/cli/commands/doctor.js +197 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +586 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +297 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +110 -0
- package/src/cli/util/expectation-extractor.js +388 -0
- package/src/cli/util/findings-writer.js +32 -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 +412 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +30 -0
- package/src/cli/util/project-discovery.js +297 -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/runtime-budget.js +147 -0
- package/src/cli/util/summary-writer.js +43 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -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 +111 -0
- package/src/verax/cli/wizard.js +109 -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 +432 -0
- package/src/verax/core/incremental-store.js +245 -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 +523 -0
- package/src/verax/detect/comparison.js +7 -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 +127 -0
- package/src/verax/detect/expectation-model.js +241 -168
- 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 +41 -12
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +200 -288
- package/src/verax/detect/interactive-findings.js +612 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/verdict-engine.js +561 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +103 -15
- 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 +642 -0
- package/src/verax/intel/vue-router-extractor.js +325 -0
- package/src/verax/learn/action-contract-extractor.js +338 -104
- package/src/verax/learn/ast-contract-extractor.js +148 -6
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +122 -58
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +28 -97
- package/src/verax/learn/route-validator.js +8 -7
- 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 +119 -10
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +30 -6
- package/src/verax/observe/console-sensor.js +2 -18
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +513 -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 +660 -273
- package/src/verax/observe/index.js +910 -26
- package/src/verax/observe/interaction-discovery.js +378 -15
- package/src/verax/observe/interaction-runner.js +562 -197
- package/src/verax/observe/loading-sensor.js +145 -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 +38 -17
- package/src/verax/observe/state-sensor.js +393 -0
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +73 -21
- package/src/verax/observe/ui-signal-sensor.js +143 -17
- package/src/verax/scan-summary-writer.js +80 -15
- package/src/verax/shared/artifact-manager.js +111 -9
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +169 -0
- package/src/verax/shared/dynamic-route-utils.js +224 -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 +9 -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 +66 -0
- package/src/verax/validate/context-validator.js +244 -0
package/src/cli/entry.js
ADDED
|
@@ -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
|
+
}
|