@veraxhq/verax 0.1.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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +237 -0
  3. package/bin/verax.js +452 -0
  4. package/package.json +57 -0
  5. package/src/verax/detect/comparison.js +69 -0
  6. package/src/verax/detect/confidence-engine.js +498 -0
  7. package/src/verax/detect/evidence-validator.js +33 -0
  8. package/src/verax/detect/expectation-model.js +204 -0
  9. package/src/verax/detect/findings-writer.js +31 -0
  10. package/src/verax/detect/index.js +397 -0
  11. package/src/verax/detect/skip-classifier.js +202 -0
  12. package/src/verax/flow/flow-engine.js +265 -0
  13. package/src/verax/flow/flow-spec.js +145 -0
  14. package/src/verax/flow/redaction.js +74 -0
  15. package/src/verax/index.js +97 -0
  16. package/src/verax/learn/action-contract-extractor.js +281 -0
  17. package/src/verax/learn/ast-contract-extractor.js +255 -0
  18. package/src/verax/learn/index.js +18 -0
  19. package/src/verax/learn/manifest-writer.js +97 -0
  20. package/src/verax/learn/project-detector.js +87 -0
  21. package/src/verax/learn/react-router-extractor.js +73 -0
  22. package/src/verax/learn/route-extractor.js +122 -0
  23. package/src/verax/learn/route-validator.js +215 -0
  24. package/src/verax/learn/source-instrumenter.js +214 -0
  25. package/src/verax/learn/static-extractor.js +222 -0
  26. package/src/verax/learn/truth-assessor.js +96 -0
  27. package/src/verax/learn/ts-contract-resolver.js +395 -0
  28. package/src/verax/observe/browser.js +22 -0
  29. package/src/verax/observe/console-sensor.js +166 -0
  30. package/src/verax/observe/dom-signature.js +23 -0
  31. package/src/verax/observe/domain-boundary.js +38 -0
  32. package/src/verax/observe/evidence-capture.js +5 -0
  33. package/src/verax/observe/human-driver.js +376 -0
  34. package/src/verax/observe/index.js +67 -0
  35. package/src/verax/observe/interaction-discovery.js +269 -0
  36. package/src/verax/observe/interaction-runner.js +410 -0
  37. package/src/verax/observe/network-sensor.js +173 -0
  38. package/src/verax/observe/selector-generator.js +74 -0
  39. package/src/verax/observe/settle.js +155 -0
  40. package/src/verax/observe/state-ui-sensor.js +200 -0
  41. package/src/verax/observe/traces-writer.js +82 -0
  42. package/src/verax/observe/ui-signal-sensor.js +197 -0
  43. package/src/verax/resolve-workspace-root.js +173 -0
  44. package/src/verax/scan-summary-writer.js +41 -0
  45. package/src/verax/shared/artifact-manager.js +139 -0
  46. package/src/verax/shared/caching.js +104 -0
  47. package/src/verax/shared/expectation-proof.js +4 -0
  48. package/src/verax/shared/redaction.js +227 -0
  49. package/src/verax/shared/retry-policy.js +89 -0
  50. package/src/verax/shared/timing-metrics.js +44 -0
@@ -0,0 +1,197 @@
1
+ /**
2
+ * WAVE 3: UI Signal Sensor
3
+ * Detects user-visible feedback signals: loading states, dialogs, error messages
4
+ * Conservative: only count signals with accessibility semantics or explicit attributes
5
+ */
6
+
7
+ export class UISignalSensor {
8
+ /**
9
+ * Snapshot current UI signals on the page.
10
+ * Returns: { hasLoadingIndicator, hasDialog, buttonStateChanged, errorSignals, explanation }
11
+ */
12
+ async snapshot(page) {
13
+ const signals = await page.evaluate(() => {
14
+ const result = {
15
+ hasLoadingIndicator: false,
16
+ hasDialog: false,
17
+ hasErrorSignal: false,
18
+ hasStatusSignal: false,
19
+ hasLiveRegion: false,
20
+ disabledElements: [],
21
+ explanation: []
22
+ };
23
+
24
+ // Check for loading indicators with accessibility semantics
25
+ // aria-busy="true"
26
+ const ariaBusy = document.querySelector('[aria-busy="true"]');
27
+ if (ariaBusy) {
28
+ result.hasLoadingIndicator = true;
29
+ result.explanation.push('Found [aria-busy="true"]');
30
+ }
31
+
32
+ // [data-loading] or [aria-label] with "loading" text
33
+ const dataLoading = document.querySelector('[data-loading]');
34
+ if (dataLoading) {
35
+ result.hasLoadingIndicator = true;
36
+ result.explanation.push('Found [data-loading]');
37
+ }
38
+
39
+ // role=status or role=alert with aria-live
40
+ const statusRegions = document.querySelectorAll('[role="status"], [role="alert"]');
41
+ if (statusRegions.length > 0) {
42
+ result.hasStatusSignal = true;
43
+ result.explanation.push(`Found ${statusRegions.length} status/alert region(s)`);
44
+ }
45
+
46
+ // aria-live region
47
+ const liveRegions = document.querySelectorAll('[aria-live]');
48
+ if (liveRegions.length > 0) {
49
+ result.hasLiveRegion = true;
50
+ result.explanation.push(`Found ${liveRegions.length} aria-live region(s)`);
51
+ }
52
+
53
+ // Check for dialogs
54
+ const dialog = document.querySelector('[role="dialog"], [aria-modal="true"]');
55
+ if (dialog && dialog.offsetParent !== null) {
56
+ // offsetParent is null if element is hidden
57
+ result.hasDialog = true;
58
+ result.explanation.push('Found dialog/modal');
59
+ }
60
+
61
+ // Check for disabled/loading buttons
62
+ const disabledButtons = document.querySelectorAll('button[disabled], button[aria-busy="true"]');
63
+ disabledButtons.forEach((btn) => {
64
+ result.disabledElements.push({
65
+ type: 'button',
66
+ text: (btn.textContent || '').trim().slice(0, 50),
67
+ attributes: {
68
+ disabled: btn.hasAttribute('disabled'),
69
+ ariaBusy: btn.getAttribute('aria-busy'),
70
+ class: btn.className.slice(0, 100)
71
+ }
72
+ });
73
+ });
74
+
75
+ if (result.disabledElements.length > 0) {
76
+ result.explanation.push(
77
+ `Found ${result.disabledElements.length} disabled/loading button(s)`
78
+ );
79
+ }
80
+
81
+ // Check for error signals: aria-invalid visible elements
82
+ const invalidElements = document.querySelectorAll('[aria-invalid="true"]');
83
+ if (invalidElements.length > 0) {
84
+ result.hasErrorSignal = true;
85
+ result.explanation.push(`Found ${invalidElements.length} invalid element(s)`);
86
+ }
87
+
88
+ // Check for common error message patterns with accessibility attributes
89
+ const errorMessages = document.querySelectorAll(
90
+ '[role="alert"], [class*="error"], [class*="danger"]'
91
+ );
92
+ if (errorMessages.length > 0) {
93
+ for (const elem of errorMessages) {
94
+ const text = elem.textContent.trim().slice(0, 50);
95
+ if (text && (text.toLowerCase().includes('error') || text.toLowerCase().includes('fail'))) {
96
+ result.hasErrorSignal = true;
97
+ result.explanation.push(`Found error message: "${text}"`);
98
+ break;
99
+ }
100
+ }
101
+ }
102
+
103
+ return result;
104
+ });
105
+
106
+ return signals;
107
+ }
108
+
109
+ /**
110
+ * Compute diff between two snapshots.
111
+ * Returns: { changed: boolean, explanation: string[], summary: {...} }
112
+ */
113
+ diff(before, after) {
114
+ const result = {
115
+ changed: false,
116
+ explanation: [],
117
+ summary: {
118
+ loadingStateChanged: before.hasLoadingIndicator !== after.hasLoadingIndicator,
119
+ dialogStateChanged: before.hasDialog !== after.hasDialog,
120
+ errorSignalChanged: before.hasErrorSignal !== after.hasErrorSignal,
121
+ statusSignalChanged: before.hasStatusSignal !== after.hasStatusSignal,
122
+ liveRegionStateChanged: before.hasLiveRegion !== after.hasLiveRegion,
123
+ disabledButtonsChanged: before.disabledElements.length !== after.disabledElements.length
124
+ }
125
+ };
126
+
127
+ // Check what changed
128
+ if (result.summary.loadingStateChanged) {
129
+ result.changed = true;
130
+ result.explanation.push(
131
+ `Loading indicator: ${before.hasLoadingIndicator} → ${after.hasLoadingIndicator}`
132
+ );
133
+ }
134
+
135
+ if (result.summary.dialogStateChanged) {
136
+ result.changed = true;
137
+ result.explanation.push(
138
+ `Dialog present: ${before.hasDialog} → ${after.hasDialog}`
139
+ );
140
+ }
141
+
142
+ if (result.summary.errorSignalChanged) {
143
+ result.changed = true;
144
+ result.explanation.push(
145
+ `Error signal: ${before.hasErrorSignal} → ${after.hasErrorSignal}`
146
+ );
147
+ }
148
+
149
+ if (result.summary.statusSignalChanged) {
150
+ result.changed = true;
151
+ result.explanation.push(
152
+ `Status signal: ${before.hasStatusSignal} → ${after.hasStatusSignal}`
153
+ );
154
+ }
155
+
156
+ if (result.summary.liveRegionStateChanged) {
157
+ result.changed = true;
158
+ result.explanation.push(
159
+ `Live region: ${before.hasLiveRegion} → ${after.hasLiveRegion}`
160
+ );
161
+ }
162
+
163
+ if (result.summary.disabledButtonsChanged) {
164
+ result.changed = true;
165
+ result.explanation.push(
166
+ `Disabled buttons: ${before.disabledElements.length} → ${after.disabledElements.length}`
167
+ );
168
+ }
169
+
170
+ return result;
171
+ }
172
+
173
+ /**
174
+ * Check if any feedback signal is present.
175
+ */
176
+ hasAnyFeedback(signals) {
177
+ return (
178
+ signals.hasLoadingIndicator ||
179
+ signals.hasDialog ||
180
+ signals.hasErrorSignal ||
181
+ signals.hasStatusSignal ||
182
+ signals.hasLiveRegion ||
183
+ signals.disabledElements.length > 0
184
+ );
185
+ }
186
+
187
+ /**
188
+ * Check if any error/failure feedback is present.
189
+ */
190
+ hasErrorFeedback(signals) {
191
+ return (
192
+ signals.hasErrorSignal ||
193
+ signals.hasDialog || // Dialog might be error confirmation
194
+ (signals.hasStatusSignal && signals.explanation.some((ex) => ex.includes('error')))
195
+ );
196
+ }
197
+ }
@@ -0,0 +1,173 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { dirname, join, resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ /**
9
+ * Determines if a path looks like the verax repository root
10
+ * (contains src/verax directory)
11
+ */
12
+ function isVeraxRepoRoot(dir) {
13
+ try {
14
+ const veraxPath = join(dir, 'src', 'verax');
15
+ const indexPath = join(veraxPath, 'index.js');
16
+ return existsSync(indexPath);
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Detects project type by looking for marker files/directories
24
+ */
25
+ function detectProjectMarker(dir) {
26
+ const markers = [
27
+ { file: 'package.json', check: (content) => {
28
+ try {
29
+ const pkg = JSON.parse(content);
30
+ if (pkg.private === false && pkg.homepage) return true;
31
+ if (pkg.scripts && pkg.scripts.start && (pkg.dependencies?.react || pkg.dependencies?.next)) return true;
32
+ if (pkg.scripts && pkg.scripts.dev) return true;
33
+ return !!pkg.name && pkg.name !== '@verax/verax';
34
+ } catch {
35
+ return false;
36
+ }
37
+ }},
38
+ { file: 'index.html', check: () => true },
39
+ { file: 'next.config.js', check: () => true },
40
+ { file: 'next.config.mjs', check: () => true },
41
+ { file: 'next.config.ts', check: () => true },
42
+ { file: 'vite.config.js', check: () => true },
43
+ { file: 'vite.config.ts', check: () => true },
44
+ { file: 'app.js', check: () => true },
45
+ { file: 'App.tsx', check: () => true },
46
+ { file: 'App.jsx', check: () => true },
47
+ { dir: 'src', check: () => true }
48
+ ];
49
+
50
+ for (const marker of markers) {
51
+ const markerPath = join(dir, marker.file || marker.dir);
52
+ if (existsSync(markerPath)) {
53
+ if (marker.file) {
54
+ try {
55
+ const content = readFileSync(markerPath, 'utf-8');
56
+ if (marker.check(content)) {
57
+ return true;
58
+ }
59
+ } catch {
60
+ // Continue if we can't read the file
61
+ continue;
62
+ }
63
+ } else {
64
+ // Directory marker
65
+ if (marker.check()) {
66
+ return true;
67
+ }
68
+ }
69
+ }
70
+ }
71
+ return false;
72
+ }
73
+
74
+ /**
75
+ * Resolves the workspace root (project directory) for verax scanning.
76
+ *
77
+ * Priority:
78
+ * 1. If --project-dir is provided → use it (absolute path)
79
+ * 2. Else → auto-detect nearest parent directory containing project markers
80
+ * 3. If no project marker found and cwd is not repo root → return cwd
81
+ * 4. If no project marker found and cwd IS repo root → raise error
82
+ *
83
+ * @param {string} projectDirArg - The value of --project-dir if provided
84
+ * @param {string} currentWorkingDir - Current working directory (defaults to process.cwd())
85
+ * @returns {object} { workspaceRoot, autoDetected, isRepoRoot }
86
+ * @throws {Error} If unable to resolve a valid workspace root
87
+ */
88
+ export function resolveWorkspaceRoot(projectDirArg = null, currentWorkingDir = process.cwd()) {
89
+ const cwd = resolve(currentWorkingDir);
90
+
91
+ // 1. If --project-dir is provided, use it
92
+ if (projectDirArg) {
93
+ const projectDir = resolve(projectDirArg);
94
+ if (!existsSync(projectDir)) {
95
+ throw new Error(`Project directory does not exist: ${projectDir}`);
96
+ }
97
+ return {
98
+ workspaceRoot: projectDir,
99
+ autoDetected: false,
100
+ isRepoRoot: isVeraxRepoRoot(projectDir)
101
+ };
102
+ }
103
+
104
+ // 2. Auto-detect: search upward from cwd for project markers
105
+ let searchDir = cwd;
106
+ const repoRootPath = isVeraxRepoRoot(cwd) ? cwd : null;
107
+
108
+ // Search upward from cwd, but stop at repo root if we find it
109
+ while (searchDir !== dirname(searchDir)) {
110
+ // If we detect a project marker, use this directory
111
+ if (detectProjectMarker(searchDir)) {
112
+ const isRepoRoot = isVeraxRepoRoot(searchDir);
113
+
114
+ // CRITICAL GUARD: If this is the repo root AND we found a marker (e.g., shared test artifact),
115
+ // refuse to use repo root as workspace root
116
+ if (isRepoRoot && searchDir === repoRootPath) {
117
+ throw new Error(
118
+ 'verax verax: Refusing to write artifacts in repository root. ' +
119
+ 'Use --project-dir to specify the target project directory.'
120
+ );
121
+ }
122
+
123
+ return {
124
+ workspaceRoot: searchDir,
125
+ autoDetected: true,
126
+ isRepoRoot: false
127
+ };
128
+ }
129
+
130
+ searchDir = dirname(searchDir);
131
+
132
+ // If we hit the repo root during search, stop (don't use repo root implicitly)
133
+ if (isVeraxRepoRoot(searchDir) && searchDir !== cwd) {
134
+ break;
135
+ }
136
+ }
137
+
138
+ // 3. Fallback: if we found repo root in upward search, refuse it
139
+ if (isVeraxRepoRoot(cwd)) {
140
+ throw new Error(
141
+ 'verax verax: Refusing to write artifacts in repository root. ' +
142
+ 'Use --project-dir to specify the target project directory.'
143
+ );
144
+ }
145
+
146
+ // 4. Use cwd if no markers found (last resort for edge cases)
147
+ return {
148
+ workspaceRoot: cwd,
149
+ autoDetected: false,
150
+ isRepoRoot: false
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Validates that an artifact path is within the workspace root
156
+ * @throws {Error} If artifact path is outside workspace root
157
+ */
158
+ export function assertArtifactPathInWorkspace(artifactPath, workspaceRoot) {
159
+ const resolvedArtifact = resolve(artifactPath);
160
+ const resolvedWorkspace = resolve(workspaceRoot);
161
+
162
+ // Normalize paths for comparison
163
+ const artifactNorm = resolvedArtifact.replace(/\\/g, '/');
164
+ const workspaceNorm = resolvedWorkspace.replace(/\\/g, '/');
165
+
166
+ if (!artifactNorm.startsWith(workspaceNorm + '/') && artifactNorm !== workspaceNorm) {
167
+ throw new Error(
168
+ `verax verax: Artifact path outside workspace root.\n` +
169
+ `Workspace: ${workspaceRoot}\n` +
170
+ `Artifact: ${artifactPath}`
171
+ );
172
+ }
173
+ }
@@ -0,0 +1,41 @@
1
+ import { resolve } from 'path';
2
+ import { writeFileSync, mkdirSync } from 'fs';
3
+
4
+ export function writeScanSummary(projectDir, url, projectType, learnTruth, observeTruth, detectTruth, manifestPath, tracesPath, findingsPath, artifactPaths = null) {
5
+ const summary = {
6
+ version: 1,
7
+ scannedAt: new Date().toISOString(),
8
+ url: url,
9
+ projectType: projectType,
10
+ truth: {
11
+ learn: learnTruth,
12
+ observe: observeTruth,
13
+ detect: detectTruth
14
+ },
15
+ paths: {
16
+ manifest: manifestPath,
17
+ traces: tracesPath,
18
+ findings: findingsPath
19
+ }
20
+ };
21
+
22
+ let summaryPath;
23
+ if (artifactPaths) {
24
+ // Use new artifact structure
25
+ summaryPath = artifactPaths.summary;
26
+ // Write the scan summary with truth data
27
+ writeFileSync(summaryPath, JSON.stringify(summary, null, 2) + '\n');
28
+ } else {
29
+ // Legacy structure
30
+ const scanDir = resolve(projectDir, '.veraxverax', 'scan');
31
+ mkdirSync(scanDir, { recursive: true });
32
+ summaryPath = resolve(scanDir, 'scan-summary.json');
33
+ writeFileSync(summaryPath, JSON.stringify(summary, null, 2) + '\n');
34
+ }
35
+
36
+ return {
37
+ ...summary,
38
+ summaryPath: summaryPath
39
+ };
40
+ }
41
+
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Wave 9 — Artifact Manager
3
+ *
4
+ * Manages the new artifact directory structure:
5
+ * .verax/runs/<runId>/
6
+ * - summary.json (overall scan results, metrics)
7
+ * - findings.json (all findings)
8
+ * - traces.jsonl (one JSON per line, per interaction)
9
+ * - evidence/ (screenshots, network logs, etc.)
10
+ * - flows/ (flow diagrams if enabled)
11
+ *
12
+ * All artifacts are redacted before writing.
13
+ */
14
+
15
+ import { existsSync, mkdirSync, writeFileSync, appendFileSync } from 'fs';
16
+ import { resolve, join } from 'path';
17
+ import { randomBytes } from 'crypto';
18
+
19
+ /**
20
+ * Generate a unique run ID.
21
+ * @returns {string} - 8-character hex ID
22
+ */
23
+ export function generateRunId() {
24
+ return randomBytes(4).toString('hex');
25
+ }
26
+
27
+ /**
28
+ * Create artifact directory structure.
29
+ * @param {string} projectRoot - Project root directory
30
+ * @param {string} runId - Run identifier
31
+ * @returns {Object} - Paths to each artifact location
32
+ */
33
+ export function initArtifactPaths(projectRoot, runId = null) {
34
+ const id = runId || generateRunId();
35
+ const runDir = resolve(projectRoot, '.verax', 'runs', id);
36
+
37
+ const paths = {
38
+ runId: id,
39
+ runDir,
40
+ summary: resolve(runDir, 'summary.json'),
41
+ findings: resolve(runDir, 'findings.json'),
42
+ traces: resolve(runDir, 'traces.jsonl'),
43
+ evidence: resolve(runDir, 'evidence'),
44
+ flows: resolve(runDir, 'flows'),
45
+ artifacts: resolve(projectRoot, '.verax', 'artifacts') // Legacy compat
46
+ };
47
+
48
+ // Create directories
49
+ [runDir, paths.evidence, paths.flows].forEach(dir => {
50
+ if (!existsSync(dir)) {
51
+ mkdirSync(dir, { recursive: true });
52
+ }
53
+ });
54
+
55
+ return paths;
56
+ }
57
+
58
+ /**
59
+ * Write summary.json with metadata and metrics.
60
+ * @param {Object} paths - Artifact paths from initArtifactPaths
61
+ * @param {Object} summary - Summary data { url, duration, findings, metrics }
62
+ */
63
+ export function writeSummary(paths, summary) {
64
+ const data = {
65
+ runId: paths.runId,
66
+ timestamp: new Date().toISOString(),
67
+ url: summary.url,
68
+ projectRoot: summary.projectRoot,
69
+ metrics: summary.metrics || {
70
+ parseMs: 0,
71
+ resolveMs: 0,
72
+ observeMs: 0,
73
+ detectMs: 0,
74
+ totalMs: 0
75
+ },
76
+ findingsCounts: summary.findingsCounts || {
77
+ HIGH: 0,
78
+ MEDIUM: 0,
79
+ LOW: 0,
80
+ UNKNOWN: 0
81
+ },
82
+ topFindings: summary.topFindings || [],
83
+ cacheStats: summary.cacheStats || {}
84
+ };
85
+
86
+ writeFileSync(paths.summary, JSON.stringify(data, null, 2) + '\n');
87
+ }
88
+
89
+ /**
90
+ * Write findings.json with all detected issues.
91
+ * @param {Object} paths - Artifact paths
92
+ * @param {Array} findings - Array of finding objects
93
+ */
94
+ export function writeFindings(paths, findings) {
95
+ const data = {
96
+ runId: paths.runId,
97
+ timestamp: new Date().toISOString(),
98
+ total: findings.length,
99
+ findings
100
+ };
101
+
102
+ writeFileSync(paths.findings, JSON.stringify(data, null, 2) + '\n');
103
+ }
104
+
105
+ /**
106
+ * Append a trace to traces.jsonl (one JSON object per line).
107
+ * @param {Object} paths - Artifact paths
108
+ * @param {Object} trace - Trace object to append
109
+ */
110
+ export function appendTrace(paths, trace) {
111
+ const line = JSON.stringify(trace) + '\n';
112
+ appendFileSync(paths.traces, line);
113
+ }
114
+
115
+ /**
116
+ * Write evidence file (screenshot, network log, etc.).
117
+ * @param {Object} paths - Artifact paths
118
+ * @param {string} filename - Evidence filename
119
+ * @param {*} data - Data to write (string or buffer)
120
+ */
121
+ export function writeEvidence(paths, filename, data) {
122
+ const filepath = resolve(paths.evidence, filename);
123
+
124
+ if (typeof data === 'string') {
125
+ writeFileSync(filepath, data);
126
+ } else {
127
+ writeFileSync(filepath, data);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Get all artifact paths for a given run.
133
+ * @param {string} projectRoot - Project root
134
+ * @param {string} runId - Run identifier
135
+ * @returns {Object} - Paths object
136
+ */
137
+ export function getArtifactPaths(projectRoot, runId) {
138
+ return initArtifactPaths(projectRoot, runId);
139
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Wave 9 — Performance Caching Layer
3
+ *
4
+ * Provides in-memory and optional disk caching for TS Program, symbol resolution,
5
+ * and AST extraction results. Deterministic cache keys based on file content hashes.
6
+ *
7
+ * Cache is keyed by: projectRoot + tsconfig path + file mtimes hash
8
+ * Entries are computed only once per unique key within a single run.
9
+ */
10
+
11
+ import { createHash } from 'crypto';
12
+ import { readFileSync, existsSync, statSync } from 'fs';
13
+ import { resolve, join } from 'path';
14
+
15
+ const memoryCache = new Map(); // Global in-memory cache
16
+
17
+ /**
18
+ * Compute a deterministic cache key from project root, tsconfig, and source files.
19
+ * @param {string} projectRoot - Project root directory
20
+ * @param {string} tsconfigPath - Path to tsconfig.json (optional)
21
+ * @param {Array<string>} sourceFiles - Array of source file paths
22
+ * @returns {string} - Deterministic cache key
23
+ */
24
+ export function computeCacheKey(projectRoot, tsconfigPath, sourceFiles = []) {
25
+ const hash = createHash('sha256');
26
+
27
+ // Hash the project root
28
+ hash.update(projectRoot);
29
+
30
+ // Hash the tsconfig if present
31
+ if (tsconfigPath && existsSync(tsconfigPath)) {
32
+ try {
33
+ const tsconfigContent = readFileSync(tsconfigPath, 'utf-8');
34
+ hash.update(tsconfigContent);
35
+ } catch (e) {
36
+ // If tsconfig can't be read, just use the path
37
+ hash.update(tsconfigPath);
38
+ }
39
+ }
40
+
41
+ // Hash file modification times (more efficient than file content)
42
+ for (const filePath of sourceFiles) {
43
+ try {
44
+ if (existsSync(filePath)) {
45
+ const stats = statSync(filePath);
46
+ hash.update(filePath + ':' + stats.mtimeMs);
47
+ }
48
+ } catch (e) {
49
+ // Skip files that can't be stat'd
50
+ }
51
+ }
52
+
53
+ return hash.digest('hex').substring(0, 16); // Use first 16 chars for brevity
54
+ }
55
+
56
+ /**
57
+ * Get a value from cache. Calls computeFn if not cached.
58
+ * @param {string} key - Cache key
59
+ * @param {Function} computeFn - Function to compute value if not cached
60
+ * @returns {*} - Cached or computed value
61
+ */
62
+ export function getOrCompute(key, computeFn) {
63
+ if (memoryCache.has(key)) {
64
+ return memoryCache.get(key);
65
+ }
66
+
67
+ const value = computeFn();
68
+ memoryCache.set(key, value);
69
+ return value;
70
+ }
71
+
72
+ /**
73
+ * Clear the in-memory cache (useful between test runs).
74
+ */
75
+ export function clearCache() {
76
+ memoryCache.clear();
77
+ }
78
+
79
+ /**
80
+ * Get cache statistics (for diagnostics).
81
+ * @returns {Object} - { size, hitRate, entries }
82
+ */
83
+ export function getCacheStats() {
84
+ return {
85
+ size: memoryCache.size,
86
+ keys: Array.from(memoryCache.keys()).slice(0, 5) // First 5 keys for inspection
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Create a cache key specifically for TS Program resolution.
92
+ */
93
+ export function getTSProgramCacheKey(rootDir, files) {
94
+ const tsconfigPath = resolve(rootDir, 'tsconfig.json');
95
+ return `ts-program:${computeCacheKey(rootDir, tsconfigPath, files)}`;
96
+ }
97
+
98
+ /**
99
+ * Create a cache key specifically for AST extraction.
100
+ */
101
+ export function getASTCacheKey(projectDir, files) {
102
+ const tsconfigPath = resolve(projectDir, 'tsconfig.json');
103
+ return `ast:${computeCacheKey(projectDir, tsconfigPath, files)}`;
104
+ }
@@ -0,0 +1,4 @@
1
+ export const ExpectationProof = {
2
+ PROVEN_EXPECTATION: 'PROVEN_EXPECTATION',
3
+ UNKNOWN_EXPECTATION: 'UNKNOWN_EXPECTATION'
4
+ };