@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.
- package/LICENSE +21 -0
- package/README.md +237 -0
- package/bin/verax.js +452 -0
- package/package.json +57 -0
- package/src/verax/detect/comparison.js +69 -0
- package/src/verax/detect/confidence-engine.js +498 -0
- package/src/verax/detect/evidence-validator.js +33 -0
- package/src/verax/detect/expectation-model.js +204 -0
- package/src/verax/detect/findings-writer.js +31 -0
- package/src/verax/detect/index.js +397 -0
- package/src/verax/detect/skip-classifier.js +202 -0
- package/src/verax/flow/flow-engine.js +265 -0
- package/src/verax/flow/flow-spec.js +145 -0
- package/src/verax/flow/redaction.js +74 -0
- package/src/verax/index.js +97 -0
- package/src/verax/learn/action-contract-extractor.js +281 -0
- package/src/verax/learn/ast-contract-extractor.js +255 -0
- package/src/verax/learn/index.js +18 -0
- package/src/verax/learn/manifest-writer.js +97 -0
- package/src/verax/learn/project-detector.js +87 -0
- package/src/verax/learn/react-router-extractor.js +73 -0
- package/src/verax/learn/route-extractor.js +122 -0
- package/src/verax/learn/route-validator.js +215 -0
- package/src/verax/learn/source-instrumenter.js +214 -0
- package/src/verax/learn/static-extractor.js +222 -0
- package/src/verax/learn/truth-assessor.js +96 -0
- package/src/verax/learn/ts-contract-resolver.js +395 -0
- package/src/verax/observe/browser.js +22 -0
- package/src/verax/observe/console-sensor.js +166 -0
- package/src/verax/observe/dom-signature.js +23 -0
- package/src/verax/observe/domain-boundary.js +38 -0
- package/src/verax/observe/evidence-capture.js +5 -0
- package/src/verax/observe/human-driver.js +376 -0
- package/src/verax/observe/index.js +67 -0
- package/src/verax/observe/interaction-discovery.js +269 -0
- package/src/verax/observe/interaction-runner.js +410 -0
- package/src/verax/observe/network-sensor.js +173 -0
- package/src/verax/observe/selector-generator.js +74 -0
- package/src/verax/observe/settle.js +155 -0
- package/src/verax/observe/state-ui-sensor.js +200 -0
- package/src/verax/observe/traces-writer.js +82 -0
- package/src/verax/observe/ui-signal-sensor.js +197 -0
- package/src/verax/resolve-workspace-root.js +173 -0
- package/src/verax/scan-summary-writer.js +41 -0
- package/src/verax/shared/artifact-manager.js +139 -0
- package/src/verax/shared/caching.js +104 -0
- package/src/verax/shared/expectation-proof.js +4 -0
- package/src/verax/shared/redaction.js +227 -0
- package/src/verax/shared/retry-policy.js +89 -0
- 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
|
+
|