@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,214 @@
1
+ /**
2
+ * Wave 5 — Source Instrumentation
3
+ *
4
+ * Babel transform to inject data-verax-source attributes into JSX elements
5
+ * with onClick/onSubmit handlers for runtime attribution.
6
+ *
7
+ * This allows matching runtime interactions to source code locations.
8
+ */
9
+
10
+ import { parse } from '@babel/parser';
11
+ import traverse from '@babel/traverse';
12
+ import generate from '@babel/generator';
13
+ import * as t from '@babel/types';
14
+ import { relative, sep, resolve as pathResolve, dirname as pathDirname, join as pathJoin } from 'path';
15
+ import { existsSync } from 'fs';
16
+
17
+ /**
18
+ * Instrument JSX code with data-verax-source attributes.
19
+ *
20
+ * @param {string} code - Source code to instrument
21
+ * @param {string} filePath - Absolute path to source file
22
+ * @param {string} workspaceRoot - Workspace root for relative paths
23
+ * @returns {string} - Instrumented code
24
+ */
25
+ export function instrumentJSX(code, filePath, workspaceRoot) {
26
+ try {
27
+ const ast = parse(code, {
28
+ sourceType: 'module',
29
+ plugins: ['jsx', 'typescript'],
30
+ });
31
+
32
+ const importMap = buildImportMap(ast, filePath, workspaceRoot);
33
+
34
+ traverse.default(ast, {
35
+ JSXOpeningElement(path) {
36
+ const attributes = path.node.attributes;
37
+ const elementName = path.node.name;
38
+
39
+ // Check if this element has onClick or onSubmit
40
+ const hasHandler = attributes.some((attr) => {
41
+ return (
42
+ attr.type === 'JSXAttribute' &&
43
+ attr.name &&
44
+ attr.name.name &&
45
+ (attr.name.name === 'onClick' || attr.name.name === 'onSubmit')
46
+ );
47
+ });
48
+
49
+ // Also check if it's a Link element with navigation props
50
+ const isLinkWithNav =
51
+ elementName.type === 'JSXIdentifier' &&
52
+ elementName.name === 'Link' &&
53
+ attributes.some((attr) => {
54
+ return (
55
+ attr.type === 'JSXAttribute' &&
56
+ attr.name &&
57
+ attr.name.name &&
58
+ (attr.name.name === 'href' || attr.name.name === 'to')
59
+ );
60
+ });
61
+
62
+ if (!hasHandler && !isLinkWithNav) return;
63
+
64
+ // Check if already instrumented
65
+ const alreadyInstrumented = attributes.some((attr) => {
66
+ return (
67
+ attr.type === 'JSXAttribute' &&
68
+ attr.name &&
69
+ attr.name.name === 'data-verax-source'
70
+ );
71
+ });
72
+
73
+ // Add sourceRef if missing
74
+ if (!alreadyInstrumented) {
75
+ const loc = path.node.loc;
76
+ const sourceRef = formatSourceRef(filePath, workspaceRoot, loc);
77
+ const sourceAttr = t.jsxAttribute(
78
+ t.jsxIdentifier('data-verax-source'),
79
+ t.stringLiteral(sourceRef)
80
+ );
81
+ path.node.attributes.push(sourceAttr);
82
+ }
83
+
84
+ // Add handlerRef for identifier handlers
85
+ const handlerAttr = attributes.find((attr) => (
86
+ attr.type === 'JSXAttribute' &&
87
+ attr.name?.name &&
88
+ (attr.name.name === 'onClick' || attr.name.name === 'onSubmit')
89
+ ));
90
+ if (handlerAttr && handlerAttr.value && handlerAttr.value.type === 'JSXExpressionContainer') {
91
+ const expr = handlerAttr.value.expression;
92
+ if (expr && expr.type === 'Identifier') {
93
+ const handlerRef = deriveHandlerRef(expr.name, importMap, filePath, workspaceRoot);
94
+ if (handlerRef) {
95
+ const hasHandlerRef = attributes.some((attr) => attr.type === 'JSXAttribute' && attr.name?.name === 'data-verax-handler');
96
+ if (!hasHandlerRef) {
97
+ const handlerAttrNode = t.jsxAttribute(
98
+ t.jsxIdentifier('data-verax-handler'),
99
+ t.stringLiteral(handlerRef)
100
+ );
101
+ path.node.attributes.push(handlerAttrNode);
102
+ }
103
+ }
104
+ }
105
+ }
106
+ },
107
+ });
108
+
109
+ const output = generate.default(ast, {
110
+ retainLines: true,
111
+ compact: false,
112
+ });
113
+
114
+ return output.code;
115
+ } catch (err) {
116
+ console.warn(`Failed to instrument ${filePath}: ${err.message}`);
117
+ return code; // Return original code on error
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Format source reference as "file:line:col"
123
+ * Normalizes Windows paths to use forward slashes.
124
+ *
125
+ * @param {string} filePath - Absolute file path
126
+ * @param {string} workspaceRoot - Workspace root
127
+ * @param {Object} loc - Location object from AST
128
+ * @returns {string} - Formatted source reference
129
+ */
130
+ function formatSourceRef(filePath, workspaceRoot, loc) {
131
+ let relPath = relative(workspaceRoot, filePath);
132
+
133
+ // Normalize to forward slashes
134
+ relPath = relPath.split(sep).join('/');
135
+
136
+ const line = loc.start.line;
137
+ const col = loc.start.column;
138
+
139
+ return `${relPath}:${line}:${col}`;
140
+ }
141
+
142
+ /**
143
+ * Instrument a file and write to output path.
144
+ *
145
+ * @param {string} inputPath - Input file path
146
+ * @param {string} outputPath - Output file path
147
+ * @param {string} workspaceRoot - Workspace root
148
+ */
149
+ export async function instrumentFile(inputPath, outputPath, workspaceRoot) {
150
+ const { readFileSync, writeFileSync } = await import('fs');
151
+ const { dirname } = await import('path');
152
+ const { mkdirSync } = await import('fs');
153
+
154
+ const code = readFileSync(inputPath, 'utf-8');
155
+ const instrumented = instrumentJSX(code, inputPath, workspaceRoot);
156
+
157
+ // Ensure output directory exists
158
+ mkdirSync(dirname(outputPath), { recursive: true });
159
+
160
+ writeFileSync(outputPath, instrumented, 'utf-8');
161
+ }
162
+
163
+ // === Handler Reference Helpers ===
164
+
165
+ const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
166
+
167
+ function buildImportMap(ast, filePath, workspaceRoot) {
168
+ const map = new Map(); // localName -> { modulePath, exportName }
169
+ const fileDir = pathDirname(filePath);
170
+
171
+ traverse.default(ast, {
172
+ ImportDeclaration(path) {
173
+ const source = path.node.source.value;
174
+ const resolved = resolveModulePath(source, fileDir, workspaceRoot);
175
+ if (!resolved) return;
176
+
177
+ const specifiers = path.node.specifiers || [];
178
+ for (const spec of specifiers) {
179
+ if (spec.type === 'ImportDefaultSpecifier') {
180
+ map.set(spec.local.name, { modulePath: resolved, exportName: 'default' });
181
+ } else if (spec.type === 'ImportSpecifier') {
182
+ const imported = spec.imported.name;
183
+ map.set(spec.local.name, { modulePath: resolved, exportName: imported });
184
+ }
185
+ }
186
+ }
187
+ });
188
+
189
+ return map;
190
+ }
191
+
192
+ function resolveModulePath(specifier, fromDir, workspaceRoot) {
193
+ if (specifier.startsWith('.') || specifier.startsWith('/')) {
194
+ const base = specifier.startsWith('.') ? pathResolve(fromDir, specifier) : pathResolve(workspaceRoot, specifier);
195
+ const candidates = [base, ...EXTENSIONS.map(ext => base + ext), ...EXTENSIONS.map(ext => pathJoin(base, 'index' + ext))];
196
+ for (const candidate of candidates) {
197
+ if (existsSync(candidate)) {
198
+ return relative(workspaceRoot, candidate).split(sep).join('/');
199
+ }
200
+ }
201
+ return null;
202
+ }
203
+ // External module - not resolved
204
+ return null;
205
+ }
206
+
207
+ function deriveHandlerRef(name, importMap, filePath, workspaceRoot) {
208
+ const entry = importMap.get(name);
209
+ if (entry) {
210
+ return `${entry.modulePath}#${entry.exportName}`;
211
+ }
212
+ const localPath = relative(workspaceRoot, filePath).split(sep).join('/');
213
+ return `${localPath}#${name}`;
214
+ }
@@ -0,0 +1,222 @@
1
+ import { glob } from 'glob';
2
+ import { resolve, dirname, join, relative } from 'path';
3
+ import { readFileSync, existsSync } from 'fs';
4
+ import { parse } from 'node-html-parser';
5
+ import { ExpectationProof } from '../shared/expectation-proof.js';
6
+
7
+ const MAX_HTML_FILES = 200;
8
+
9
+ function htmlFileToRoute(file) {
10
+ let path = file.replace(/[\\\/]/g, '/');
11
+ path = path.replace(/\/index\.html$/, '');
12
+ path = path.replace(/\.html$/, '');
13
+ path = path.replace(/^index$/, '');
14
+
15
+ if (path === '' || path === '/') {
16
+ return '/';
17
+ }
18
+ if (!path.startsWith('/')) {
19
+ path = '/' + path;
20
+ }
21
+
22
+ return path;
23
+ }
24
+
25
+ function resolveLinkPath(href, fromFile, projectDir) {
26
+ if (!href || href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('javascript:')) {
27
+ return null;
28
+ }
29
+
30
+ if (href.startsWith('http://') || href.startsWith('https://')) {
31
+ return null;
32
+ }
33
+
34
+ try {
35
+ if (href.startsWith('/')) {
36
+ return htmlFileToRoute(href);
37
+ }
38
+
39
+ const fromDir = dirname(resolve(projectDir, fromFile));
40
+ const resolved = resolve(fromDir, href);
41
+ const relativePath = relative(projectDir, resolved);
42
+
43
+ if (relativePath.startsWith('..')) {
44
+ return null;
45
+ }
46
+
47
+ return htmlFileToRoute(relativePath);
48
+ } catch (error) {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ export async function extractStaticRoutes(projectDir) {
54
+ const routes = [];
55
+ const routeSet = new Set();
56
+
57
+ const htmlFiles = await glob('**/*.html', {
58
+ cwd: projectDir,
59
+ absolute: false,
60
+ ignore: ['node_modules/**']
61
+ });
62
+
63
+ for (const file of htmlFiles.slice(0, MAX_HTML_FILES)) {
64
+ const routePath = htmlFileToRoute(file);
65
+ const routeKey = routePath;
66
+
67
+ if (!routeSet.has(routeKey)) {
68
+ routeSet.add(routeKey);
69
+ routes.push({
70
+ path: routePath,
71
+ source: file,
72
+ public: true
73
+ });
74
+ }
75
+ }
76
+
77
+ routes.sort((a, b) => a.path.localeCompare(b.path));
78
+
79
+ return routes;
80
+ }
81
+
82
+ function extractButtonNavigationExpectations(root, fromPath, file, routeMap, projectDir) {
83
+ const expectations = [];
84
+
85
+ const buttons = root.querySelectorAll('button');
86
+ for (const button of buttons) {
87
+ let targetPath = null;
88
+ let selectorHint = null;
89
+
90
+ const buttonId = button.getAttribute('id');
91
+ const dataHref = button.getAttribute('data-href');
92
+ const onclick = button.getAttribute('onclick') || '';
93
+
94
+ if (dataHref) {
95
+ targetPath = resolveLinkPath(dataHref, file, projectDir);
96
+ if (targetPath && routeMap.has(targetPath)) {
97
+ selectorHint = buttonId ? `#${buttonId}` : `button[data-href="${dataHref}"]`;
98
+ }
99
+ } else if (onclick) {
100
+ const locationMatch = onclick.match(/window\.location\s*=\s*['"]([^'"]+)['"]|document\.location\s*=\s*['"]([^'"]+)['"]/);
101
+ if (locationMatch) {
102
+ const href = locationMatch[1] || locationMatch[2];
103
+ targetPath = resolveLinkPath(href, file, projectDir);
104
+ if (targetPath && routeMap.has(targetPath)) {
105
+ selectorHint = buttonId ? `#${buttonId}` : `button[onclick*="${href}"]`;
106
+ }
107
+ }
108
+ } else {
109
+ const parentLink = button.closest('a[href]');
110
+ if (parentLink) {
111
+ const href = parentLink.getAttribute('href');
112
+ targetPath = resolveLinkPath(href, file, projectDir);
113
+ if (targetPath && routeMap.has(targetPath)) {
114
+ selectorHint = buttonId ? `#${buttonId}` : `button`;
115
+ }
116
+ }
117
+ }
118
+
119
+ if (targetPath && selectorHint) {
120
+ expectations.push({
121
+ fromPath: fromPath,
122
+ type: 'navigation',
123
+ targetPath: targetPath,
124
+ proof: ExpectationProof.PROVEN_EXPECTATION,
125
+ evidence: {
126
+ source: file,
127
+ selectorHint: selectorHint
128
+ }
129
+ });
130
+ }
131
+ }
132
+
133
+ return expectations;
134
+ }
135
+
136
+ function extractFormSubmissionExpectations(root, fromPath, file, routeMap, projectDir) {
137
+ const expectations = [];
138
+
139
+ const forms = root.querySelectorAll('form[action]');
140
+ for (const form of forms) {
141
+ const action = form.getAttribute('action');
142
+ if (!action) continue;
143
+
144
+ const targetPath = resolveLinkPath(action, file, projectDir);
145
+ if (!targetPath || !routeMap.has(targetPath)) continue;
146
+
147
+ const formId = form.getAttribute('id');
148
+ const selectorHint = formId ? `#${formId}` : `form[action="${action}"]`;
149
+
150
+ expectations.push({
151
+ fromPath: fromPath,
152
+ type: 'form_submission',
153
+ targetPath: targetPath,
154
+ proof: ExpectationProof.PROVEN_EXPECTATION,
155
+ evidence: {
156
+ source: file,
157
+ selectorHint: selectorHint
158
+ }
159
+ });
160
+ }
161
+
162
+ return expectations;
163
+ }
164
+
165
+ export async function extractStaticExpectations(projectDir, routes) {
166
+ const expectations = [];
167
+ const routeMap = new Map(routes.map(r => [r.path, r]));
168
+
169
+ const htmlFiles = await glob('**/*.html', {
170
+ cwd: projectDir,
171
+ absolute: false,
172
+ ignore: ['node_modules/**']
173
+ });
174
+
175
+ for (const file of htmlFiles.slice(0, MAX_HTML_FILES)) {
176
+ const fromPath = htmlFileToRoute(file);
177
+ const filePath = resolve(projectDir, file);
178
+
179
+ if (!existsSync(filePath)) continue;
180
+
181
+ try {
182
+ const content = readFileSync(filePath, 'utf-8');
183
+ const root = parse(content);
184
+
185
+ const links = root.querySelectorAll('a[href]');
186
+ for (const link of links) {
187
+ const href = link.getAttribute('href');
188
+ if (!href) continue;
189
+
190
+ const targetPath = resolveLinkPath(href, file, projectDir);
191
+ if (!targetPath) continue;
192
+
193
+ if (!routeMap.has(targetPath)) continue;
194
+
195
+ const linkText = link.textContent?.trim() || '';
196
+ const selectorHint = link.id ? `#${link.id}` : `a[href="${href}"]`;
197
+
198
+ expectations.push({
199
+ fromPath: fromPath,
200
+ type: 'navigation',
201
+ targetPath: targetPath,
202
+ proof: ExpectationProof.PROVEN_EXPECTATION,
203
+ evidence: {
204
+ source: file,
205
+ selectorHint: selectorHint
206
+ }
207
+ });
208
+ }
209
+
210
+ const buttonExpectations = extractButtonNavigationExpectations(root, fromPath, file, routeMap, projectDir);
211
+ expectations.push(...buttonExpectations);
212
+
213
+ const formExpectations = extractFormSubmissionExpectations(root, fromPath, file, routeMap, projectDir);
214
+ expectations.push(...formExpectations);
215
+ } catch (error) {
216
+ continue;
217
+ }
218
+ }
219
+
220
+ return expectations;
221
+ }
222
+
@@ -0,0 +1,96 @@
1
+ import { glob } from 'glob';
2
+ import { hasReactRouterDom } from './project-detector.js';
3
+
4
+ const MAX_HTML_FILES = 200;
5
+
6
+ export async function assessLearnTruth(projectDir, projectType, routes, staticExpectations, spaExpectations = null) {
7
+ const truth = {
8
+ routesDiscovered: routes.length,
9
+ routesSource: 'none',
10
+ routesConfidence: 'LOW',
11
+ expectationsDiscovered: 0,
12
+ expectationsStrong: 0,
13
+ expectationsWeak: 0,
14
+ warnings: [],
15
+ limitations: []
16
+ };
17
+
18
+ if (projectType === 'nextjs_app_router' || projectType === 'nextjs_pages_router') {
19
+ truth.routesSource = 'nextjs_fs';
20
+ truth.routesConfidence = 'HIGH';
21
+
22
+ // Wave 1 - CODE TRUTH ENGINE: Count AST-derived expectations
23
+ if (spaExpectations && spaExpectations.length > 0) {
24
+ truth.expectationsDiscovered = spaExpectations.length;
25
+ truth.expectationsStrong = spaExpectations.length;
26
+ truth.expectationsWeak = 0;
27
+ truth.expectationsSource = 'ast_contracts';
28
+ }
29
+ } else if (projectType === 'static') {
30
+ truth.routesSource = 'static_html';
31
+
32
+ const htmlFiles = await glob('**/*.html', {
33
+ cwd: projectDir,
34
+ absolute: false,
35
+ ignore: ['node_modules/**']
36
+ });
37
+
38
+ if (htmlFiles.length <= MAX_HTML_FILES) {
39
+ truth.routesConfidence = 'HIGH';
40
+ } else {
41
+ truth.routesConfidence = 'MEDIUM';
42
+ truth.warnings.push({
43
+ code: 'HTML_SCAN_CAPPED',
44
+ message: `HTML file count (${htmlFiles.length}) exceeds cap (${MAX_HTML_FILES}). Some routes may be missed.`
45
+ });
46
+ }
47
+
48
+ if (staticExpectations) {
49
+ truth.expectationsDiscovered = staticExpectations.length;
50
+ truth.expectationsStrong = staticExpectations.filter(e =>
51
+ e.type === 'navigation' || e.type === 'form_submission'
52
+ ).length;
53
+ truth.expectationsWeak = 0;
54
+ }
55
+ } else if (projectType === 'react_spa') {
56
+ truth.routesSource = 'react_router_regex';
57
+ truth.routesConfidence = 'MEDIUM';
58
+
59
+ const hasRouter = await hasReactRouterDom(projectDir);
60
+ if (!hasRouter || routes.length === 0) {
61
+ truth.routesConfidence = 'LOW';
62
+ truth.limitations.push({
63
+ code: 'REACT_ROUTES_NOT_FOUND',
64
+ message: hasRouter
65
+ ? 'React Router detected but no routes extracted. Route extraction may have failed.'
66
+ : 'react-router-dom not detected in package.json. Routes cannot be extracted.'
67
+ });
68
+ }
69
+
70
+ // Wave 1 - CODE TRUTH ENGINE: Report AST-based expectations instead of routes
71
+ if (spaExpectations && spaExpectations.length > 0) {
72
+ truth.expectationsDiscovered = spaExpectations.length;
73
+ truth.expectationsStrong = spaExpectations.length;
74
+ truth.expectationsWeak = 0;
75
+ truth.expectationsSource = 'ast_contracts';
76
+ } else {
77
+ truth.expectationsDiscovered = 0;
78
+ truth.expectationsStrong = 0;
79
+ truth.expectationsWeak = 0;
80
+ truth.warnings.push({
81
+ code: 'NO_AST_CONTRACTS_FOUND',
82
+ message: 'No JSX Link/NavLink elements with static href/to found. No PROVEN expectations available.'
83
+ });
84
+ }
85
+ } else if (projectType === 'unknown') {
86
+ truth.routesSource = 'none';
87
+ truth.routesConfidence = 'LOW';
88
+ truth.limitations.push({
89
+ code: 'PROJECT_TYPE_UNKNOWN',
90
+ message: 'Project type could not be determined. No routes or expectations can be extracted.'
91
+ });
92
+ }
93
+
94
+ return truth;
95
+ }
96
+