@veraxhq/verax 0.2.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/package.json +14 -4
- package/src/cli/commands/default.js +244 -86
- package/src/cli/commands/doctor.js +36 -4
- package/src/cli/commands/run.js +253 -69
- package/src/cli/entry.js +5 -5
- package/src/cli/util/detection-engine.js +4 -3
- package/src/cli/util/events.js +76 -0
- package/src/cli/util/expectation-extractor.js +11 -1
- package/src/cli/util/findings-writer.js +1 -0
- package/src/cli/util/observation-engine.js +69 -23
- package/src/cli/util/paths.js +3 -2
- package/src/cli/util/project-discovery.js +20 -0
- package/src/cli/util/redact.js +2 -2
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/summary-writer.js +12 -1
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/doctor.js +2 -2
- package/src/verax/cli/init.js +1 -1
- package/src/verax/cli/url-safety.js +12 -2
- package/src/verax/cli/wizard.js +13 -2
- package/src/verax/core/budget-engine.js +1 -1
- package/src/verax/core/decision-snapshot.js +2 -2
- package/src/verax/core/determinism-model.js +35 -6
- package/src/verax/core/incremental-store.js +15 -7
- package/src/verax/core/replay-validator.js +4 -4
- package/src/verax/core/replay.js +1 -1
- package/src/verax/core/silence-impact.js +1 -1
- package/src/verax/core/silence-model.js +9 -7
- package/src/verax/detect/comparison.js +8 -3
- package/src/verax/detect/confidence-engine.js +17 -17
- package/src/verax/detect/detection-engine.js +1 -1
- package/src/verax/detect/evidence-index.js +15 -65
- package/src/verax/detect/expectation-model.js +54 -3
- package/src/verax/detect/explanation-helpers.js +1 -1
- package/src/verax/detect/finding-detector.js +2 -2
- package/src/verax/detect/findings-writer.js +9 -16
- package/src/verax/detect/flow-detector.js +4 -4
- package/src/verax/detect/index.js +37 -11
- package/src/verax/detect/interactive-findings.js +3 -4
- package/src/verax/detect/signal-mapper.js +2 -2
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/verdict-engine.js +4 -6
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +15 -3
- package/src/verax/intel/effect-detector.js +1 -1
- package/src/verax/intel/index.js +2 -2
- package/src/verax/intel/route-extractor.js +3 -3
- package/src/verax/intel/vue-navigation-extractor.js +81 -18
- package/src/verax/intel/vue-router-extractor.js +4 -2
- package/src/verax/learn/action-contract-extractor.js +3 -3
- package/src/verax/learn/ast-contract-extractor.js +53 -1
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +28 -14
- package/src/verax/learn/route-extractor.js +1 -1
- package/src/verax/learn/route-validator.js +8 -7
- package/src/verax/learn/state-extractor.js +1 -1
- package/src/verax/learn/static-extractor-navigation.js +1 -1
- package/src/verax/learn/static-extractor-validation.js +2 -2
- package/src/verax/learn/static-extractor.js +8 -7
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/browser.js +22 -3
- package/src/verax/observe/console-sensor.js +2 -2
- package/src/verax/observe/expectation-executor.js +2 -1
- package/src/verax/observe/focus-sensor.js +1 -1
- package/src/verax/observe/human-driver.js +29 -10
- package/src/verax/observe/index.js +10 -7
- package/src/verax/observe/interaction-discovery.js +27 -15
- package/src/verax/observe/interaction-runner.js +6 -6
- package/src/verax/observe/loading-sensor.js +6 -0
- package/src/verax/observe/navigation-sensor.js +1 -1
- package/src/verax/observe/settle.js +1 -0
- package/src/verax/observe/state-sensor.js +8 -4
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/traces-writer.js +27 -16
- package/src/verax/observe/ui-signal-sensor.js +7 -0
- package/src/verax/scan-summary-writer.js +5 -2
- package/src/verax/shared/artifact-manager.js +1 -1
- package/src/verax/shared/budget-profiles.js +2 -2
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/config-loader.js +1 -2
- package/src/verax/shared/dynamic-route-utils.js +12 -6
- package/src/verax/shared/retry-policy.js +1 -6
- package/src/verax/shared/root-artifacts.js +1 -1
- package/src/verax/shared/zip-artifacts.js +1 -0
- package/src/verax/validate/context-validator.js +1 -1
- package/src/verax/observe/index.js.backup +0 -1
- package/src/verax/validate/context-validator.js.bak +0 -0
|
@@ -11,24 +11,29 @@
|
|
|
11
11
|
import ts from 'typescript';
|
|
12
12
|
import { parseFile, findNodes, getStringLiteral, getNodeLocation } from './ts-program.js';
|
|
13
13
|
import { readFileSync, existsSync } from 'fs';
|
|
14
|
-
import { resolve, extname } from 'path';
|
|
14
|
+
import { resolve, extname, relative } from 'path';
|
|
15
|
+
import { globSync } from 'glob';
|
|
15
16
|
import { normalizeDynamicRoute, normalizeTemplateLiteral } from '../shared/dynamic-route-utils.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Extract navigation promises from Vue components.
|
|
19
20
|
*
|
|
20
|
-
* @param {Object}
|
|
21
|
-
* @param {string}
|
|
22
|
-
* @returns {
|
|
21
|
+
* @param {Object|string} programOrProjectRoot - TypeScript program object or project root path
|
|
22
|
+
* @param {string|Object} [maybeProjectRoot] - Project root string or program object (supports both call signatures)
|
|
23
|
+
* @returns {Array} - Array of navigation expectation objects
|
|
23
24
|
*/
|
|
24
|
-
export
|
|
25
|
-
|
|
25
|
+
export function extractVueNavigationPromises(programOrProjectRoot, maybeProjectRoot) {
|
|
26
|
+
// Accept both (program, projectRoot) and (projectRoot, program) call signatures
|
|
27
|
+
const program = programOrProjectRoot && programOrProjectRoot.program ? programOrProjectRoot : maybeProjectRoot;
|
|
28
|
+
const projectRoot = programOrProjectRoot && programOrProjectRoot.program ? (maybeProjectRoot || process.cwd()) : programOrProjectRoot;
|
|
26
29
|
|
|
27
|
-
if (!program || !program.program) return
|
|
30
|
+
if (!program || !program.program || !projectRoot) return [];
|
|
31
|
+
const expectations = [];
|
|
28
32
|
|
|
29
33
|
// Find Vue component files (.vue, .ts, .js, .tsx, .jsx)
|
|
30
34
|
const vueFiles = [];
|
|
31
|
-
const
|
|
35
|
+
const tsProgram = program.program || program;
|
|
36
|
+
const sourceFiles = tsProgram.getSourceFiles ? tsProgram.getSourceFiles() : (program.sourceFiles || []);
|
|
32
37
|
|
|
33
38
|
for (const sourceFile of sourceFiles) {
|
|
34
39
|
// sourceFiles can be either file paths (strings) or SourceFile objects
|
|
@@ -42,11 +47,9 @@ export async function extractVueNavigationPromises(program, projectRoot) {
|
|
|
42
47
|
|
|
43
48
|
// Also explicitly glob for .vue files since TypeScript might not include them
|
|
44
49
|
try {
|
|
45
|
-
const {
|
|
46
|
-
const vueGlobPattern = resolve(projectRoot, '**/*.vue');
|
|
47
|
-
const vueFilesFromGlob = await glob(vueGlobPattern, { cwd: projectRoot });
|
|
50
|
+
const vueFilesFromGlob = globSync('**/*.vue', { cwd: projectRoot, absolute: true, nodir: true });
|
|
48
51
|
for (const filePath of vueFilesFromGlob) {
|
|
49
|
-
const absPath = resolve(
|
|
52
|
+
const absPath = resolve(filePath);
|
|
50
53
|
if (!vueFiles.includes(absPath) && !vueFiles.find(f => f.endsWith(filePath))) {
|
|
51
54
|
vueFiles.push(absPath);
|
|
52
55
|
}
|
|
@@ -68,16 +71,54 @@ export async function extractVueNavigationPromises(program, projectRoot) {
|
|
|
68
71
|
*
|
|
69
72
|
* @param {string} filePath - File path
|
|
70
73
|
* @param {string} projectRoot - Project root
|
|
71
|
-
* @param {Object} program - TypeScript program
|
|
72
74
|
* @returns {Array} - Navigation expectations
|
|
73
75
|
*/
|
|
74
|
-
function extractFromFile(filePath, projectRoot,
|
|
76
|
+
function extractFromFile(filePath, projectRoot, _program) {
|
|
75
77
|
const expectations = [];
|
|
76
78
|
|
|
77
79
|
// Check if it's a .vue file (SFC)
|
|
78
80
|
if (extname(filePath) === '.vue') {
|
|
79
81
|
const sfcExpectations = extractFromVueSFC(filePath, projectRoot);
|
|
80
82
|
expectations.push(...sfcExpectations);
|
|
83
|
+
|
|
84
|
+
// For .vue files, also parse the script section as AST
|
|
85
|
+
// to handle template literals and complex navigation calls
|
|
86
|
+
try {
|
|
87
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
88
|
+
const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
|
89
|
+
if (scriptMatch) {
|
|
90
|
+
const scriptContent = scriptMatch[1];
|
|
91
|
+
// Parse script content as TypeScript/JavaScript
|
|
92
|
+
const ast = ts.createSourceFile(
|
|
93
|
+
filePath,
|
|
94
|
+
scriptContent,
|
|
95
|
+
ts.ScriptTarget.Latest,
|
|
96
|
+
true,
|
|
97
|
+
ts.ScriptKind.TS
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Extract router.push/replace calls
|
|
101
|
+
const routerCalls = findRouterCalls(ast);
|
|
102
|
+
for (const call of routerCalls) {
|
|
103
|
+
const expectation = extractFromRouterCall(call, ast, projectRoot, filePath);
|
|
104
|
+
if (expectation) {
|
|
105
|
+
// Check for duplicates (might already be extracted by regex)
|
|
106
|
+
const isDupe = expectations.some(e =>
|
|
107
|
+
e.targetPath === expectation.targetPath &&
|
|
108
|
+
e.navigationMethod === expectation.navigationMethod &&
|
|
109
|
+
e.sourceRef === expectation.sourceRef
|
|
110
|
+
);
|
|
111
|
+
if (!isDupe) {
|
|
112
|
+
expectations.push(expectation);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
// If script parsing fails, fall back to regex-based extraction (already done)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return expectations;
|
|
81
122
|
}
|
|
82
123
|
|
|
83
124
|
// Extract from script section (TypeScript/JavaScript)
|
|
@@ -114,7 +155,6 @@ function extractFromFile(filePath, projectRoot, program) {
|
|
|
114
155
|
*/
|
|
115
156
|
function extractFromVueSFC(filePath, projectRoot) {
|
|
116
157
|
const expectations = [];
|
|
117
|
-
const { relative } = require('path');
|
|
118
158
|
|
|
119
159
|
if (!existsSync(filePath)) return expectations;
|
|
120
160
|
|
|
@@ -141,8 +181,10 @@ function extractFromVueSFC(filePath, projectRoot) {
|
|
|
141
181
|
type: 'spa_navigation',
|
|
142
182
|
targetPath: normalized.examplePath,
|
|
143
183
|
originalPattern: normalized.originalPattern,
|
|
184
|
+
originalTarget: normalized.originalPattern,
|
|
144
185
|
isDynamic: true,
|
|
145
186
|
exampleExecution: true,
|
|
187
|
+
parameters: normalized.parameters || [],
|
|
146
188
|
matchAttribute: 'to',
|
|
147
189
|
proof: 'PROVEN_EXPECTATION',
|
|
148
190
|
sourceRef: `${relative(projectRoot, filePath).replace(/\\/g, '/')}:${line}`,
|
|
@@ -186,8 +228,10 @@ function extractFromVueSFC(filePath, projectRoot) {
|
|
|
186
228
|
type: 'spa_navigation',
|
|
187
229
|
targetPath: normalized.examplePath,
|
|
188
230
|
originalPattern: normalized.originalPattern,
|
|
231
|
+
originalTarget: normalized.originalPattern,
|
|
189
232
|
isDynamic: true,
|
|
190
233
|
exampleExecution: true,
|
|
234
|
+
parameters: normalized.parameters || [],
|
|
191
235
|
matchAttribute: 'to',
|
|
192
236
|
proof: 'PROVEN_EXPECTATION',
|
|
193
237
|
sourceRef: `${relative(projectRoot, filePath).replace(/\\/g, '/')}:${line}`,
|
|
@@ -223,6 +267,7 @@ function extractFromVueSFC(filePath, projectRoot) {
|
|
|
223
267
|
if (scriptMatch) {
|
|
224
268
|
const scriptContent = scriptMatch[1];
|
|
225
269
|
const routerPushRegex = /router\.(push|replace)\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
270
|
+
let match;
|
|
226
271
|
while ((match = routerPushRegex.exec(scriptContent)) !== null) {
|
|
227
272
|
const method = match[1];
|
|
228
273
|
const path = match[2];
|
|
@@ -236,8 +281,10 @@ function extractFromVueSFC(filePath, projectRoot) {
|
|
|
236
281
|
type: 'spa_navigation',
|
|
237
282
|
targetPath: normalized.examplePath,
|
|
238
283
|
originalPattern: normalized.originalPattern,
|
|
284
|
+
originalTarget: normalized.originalPattern,
|
|
239
285
|
isDynamic: true,
|
|
240
286
|
exampleExecution: true,
|
|
287
|
+
parameters: normalized.parameters || [],
|
|
241
288
|
navigationMethod: method,
|
|
242
289
|
proof: 'PROVEN_EXPECTATION',
|
|
243
290
|
sourceRef: `${relative(projectRoot, filePath).replace(/\\/g, '/')}:${line}`,
|
|
@@ -283,7 +330,7 @@ function extractFromVueSFC(filePath, projectRoot) {
|
|
|
283
330
|
* @returns {Array} - Call expression nodes
|
|
284
331
|
*/
|
|
285
332
|
function findRouterCalls(ast) {
|
|
286
|
-
const
|
|
333
|
+
const _calls = [];
|
|
287
334
|
|
|
288
335
|
const callExpressions = findNodes(ast, node => {
|
|
289
336
|
if (!ts.isCallExpression(node)) return false;
|
|
@@ -307,9 +354,10 @@ function findRouterCalls(ast) {
|
|
|
307
354
|
* @param {ts.CallExpression} call - Call expression
|
|
308
355
|
* @param {ts.SourceFile} ast - Source file
|
|
309
356
|
* @param {string} projectRoot - Project root
|
|
357
|
+
* @param {string} [filePathOverride] - Optional file path override for .vue files
|
|
310
358
|
* @returns {Object|null} - Expectation or null
|
|
311
359
|
*/
|
|
312
|
-
function extractFromRouterCall(call, ast, projectRoot) {
|
|
360
|
+
function extractFromRouterCall(call, ast, projectRoot, filePathOverride = null) {
|
|
313
361
|
const expr = call.expression;
|
|
314
362
|
if (!ts.isPropertyAccessExpression(expr)) return null;
|
|
315
363
|
|
|
@@ -321,6 +369,13 @@ function extractFromRouterCall(call, ast, projectRoot) {
|
|
|
321
369
|
const arg = call.arguments[0];
|
|
322
370
|
const location = getNodeLocation(ast, call, projectRoot);
|
|
323
371
|
|
|
372
|
+
// Override file path if provided (for .vue files)
|
|
373
|
+
if (filePathOverride) {
|
|
374
|
+
const relativePath = relative(projectRoot, filePathOverride);
|
|
375
|
+
location.file = relativePath.replace(/\\/g, '/');
|
|
376
|
+
location.sourceRef = `${relativePath.replace(/\\/g, '/')}:${location.line}`;
|
|
377
|
+
}
|
|
378
|
+
|
|
324
379
|
// String literal: router.push('/path')
|
|
325
380
|
const path = getStringLiteral(arg);
|
|
326
381
|
if (path && path.startsWith('/')) {
|
|
@@ -331,8 +386,10 @@ function extractFromRouterCall(call, ast, projectRoot) {
|
|
|
331
386
|
type: 'spa_navigation',
|
|
332
387
|
targetPath: normalized.examplePath,
|
|
333
388
|
originalPattern: normalized.originalPattern,
|
|
389
|
+
originalTarget: normalized.originalPattern,
|
|
334
390
|
isDynamic: true,
|
|
335
391
|
exampleExecution: true,
|
|
392
|
+
parameters: normalized.parameters || [],
|
|
336
393
|
navigationMethod: method,
|
|
337
394
|
proof: 'PROVEN_EXPECTATION',
|
|
338
395
|
sourceRef: location.sourceRef,
|
|
@@ -391,8 +448,10 @@ function extractFromRouterCall(call, ast, projectRoot) {
|
|
|
391
448
|
type: 'spa_navigation',
|
|
392
449
|
targetPath: normalized.examplePath,
|
|
393
450
|
originalPattern: normalized.originalPattern,
|
|
451
|
+
originalTarget: normalized.originalPattern,
|
|
394
452
|
isDynamic: true,
|
|
395
453
|
exampleExecution: true,
|
|
454
|
+
parameters: normalized.parameters || [],
|
|
396
455
|
navigationMethod: method,
|
|
397
456
|
proof: 'PROVEN_EXPECTATION',
|
|
398
457
|
sourceRef: location.sourceRef,
|
|
@@ -425,8 +484,10 @@ function extractFromRouterCall(call, ast, projectRoot) {
|
|
|
425
484
|
type: 'spa_navigation',
|
|
426
485
|
targetPath: normalized.examplePath,
|
|
427
486
|
originalPattern: normalized.originalPattern,
|
|
487
|
+
originalTarget: normalized.originalPattern,
|
|
428
488
|
isDynamic: true,
|
|
429
489
|
exampleExecution: true,
|
|
490
|
+
parameters: normalized.parameters || [],
|
|
430
491
|
navigationMethod: method,
|
|
431
492
|
proof: 'PROVEN_EXPECTATION',
|
|
432
493
|
sourceRef: location.sourceRef,
|
|
@@ -468,7 +529,7 @@ function extractFromRouterCall(call, ast, projectRoot) {
|
|
|
468
529
|
* @returns {Array} - JSX element nodes
|
|
469
530
|
*/
|
|
470
531
|
function findRouterLinkElements(ast) {
|
|
471
|
-
const
|
|
532
|
+
const _elements = [];
|
|
472
533
|
|
|
473
534
|
const jsxElements = findNodes(ast, node => {
|
|
474
535
|
if (!ts.isJsxOpeningElement(node) && !ts.isJsxSelfClosingElement(node)) return false;
|
|
@@ -545,8 +606,10 @@ function extractFromRouterLink(element, ast, projectRoot) {
|
|
|
545
606
|
type: 'spa_navigation',
|
|
546
607
|
targetPath: normalized.examplePath,
|
|
547
608
|
originalPattern: normalized.originalPattern,
|
|
609
|
+
originalTarget: normalized.originalPattern,
|
|
548
610
|
isDynamic: true,
|
|
549
611
|
exampleExecution: true,
|
|
612
|
+
parameters: normalized.parameters || [],
|
|
550
613
|
matchAttribute: 'to',
|
|
551
614
|
proof: 'PROVEN_EXPECTATION',
|
|
552
615
|
sourceRef: location.sourceRef,
|
|
@@ -298,7 +298,8 @@ function extractRouteFromObject(objNode, ast, projectRoot, parentPath) {
|
|
|
298
298
|
file: location.file,
|
|
299
299
|
line: location.line,
|
|
300
300
|
framework: 'vue-router',
|
|
301
|
-
public: !isInternalRoute(normalized.examplePath)
|
|
301
|
+
public: !isInternalRoute(normalized.examplePath),
|
|
302
|
+
children: null // Will be set below if children exist
|
|
302
303
|
};
|
|
303
304
|
} else {
|
|
304
305
|
// Static route
|
|
@@ -308,7 +309,8 @@ function extractRouteFromObject(objNode, ast, projectRoot, parentPath) {
|
|
|
308
309
|
file: location.file,
|
|
309
310
|
line: location.line,
|
|
310
311
|
framework: 'vue-router',
|
|
311
|
-
public: !isInternalRoute(fullPath)
|
|
312
|
+
public: !isInternalRoute(fullPath),
|
|
313
|
+
children: null // Will be set below if children exist
|
|
312
314
|
};
|
|
313
315
|
}
|
|
314
316
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { normalizeTemplateLiteral } from '../shared/dynamic-route-utils.js';import { parse } from '@babel/parser';
|
|
12
12
|
import traverse from '@babel/traverse';
|
|
13
13
|
import { readFileSync } from 'fs';
|
|
14
|
-
import {
|
|
14
|
+
import { relative, sep } from 'path';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Extract action contracts from a source file.
|
|
@@ -394,7 +394,7 @@ function findNetworkCallsInNode(node) {
|
|
|
394
394
|
|
|
395
395
|
// Recursively scan all properties
|
|
396
396
|
for (const key in n) {
|
|
397
|
-
if (
|
|
397
|
+
if (Object.prototype.hasOwnProperty.call(n, key)) {
|
|
398
398
|
const value = n[key];
|
|
399
399
|
if (Array.isArray(value)) {
|
|
400
400
|
value.forEach(item => scan(item));
|
|
@@ -445,7 +445,7 @@ function formatSourceRef(filePath, workspaceRoot, loc, lineOffset = 0) {
|
|
|
445
445
|
*
|
|
446
446
|
* @param {string} rootPath - Root directory to scan
|
|
447
447
|
* @param {string} workspaceRoot - Workspace root
|
|
448
|
-
* @returns {Array<Object
|
|
448
|
+
* @returns {Promise<Array<Object>>} - All contracts found
|
|
449
449
|
*/
|
|
450
450
|
export async function scanForContracts(rootPath, workspaceRoot) {
|
|
451
451
|
const contracts = [];
|
|
@@ -21,6 +21,11 @@ function extractStaticStringValue(node) {
|
|
|
21
21
|
if (node.type === 'StringLiteral') {
|
|
22
22
|
return node.value;
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
// Template literal without interpolation
|
|
26
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0 && node.quasis.length === 1) {
|
|
27
|
+
return node.quasis[0].value.cooked;
|
|
28
|
+
}
|
|
24
29
|
|
|
25
30
|
// JSX expression: href={'/about'} or href={`/about`}
|
|
26
31
|
if (node.type === 'JSXExpressionContainer') {
|
|
@@ -46,6 +51,22 @@ function extractStaticStringValue(node) {
|
|
|
46
51
|
return null;
|
|
47
52
|
}
|
|
48
53
|
|
|
54
|
+
function extractStaticPropValue(propsNode, propNames) {
|
|
55
|
+
if (!propsNode || propsNode.type !== 'ObjectExpression') return { attributeName: null, targetPath: null };
|
|
56
|
+
const names = new Set(propNames);
|
|
57
|
+
for (const prop of propsNode.properties || []) {
|
|
58
|
+
if (prop.type !== 'ObjectProperty') continue;
|
|
59
|
+
const key = prop.key;
|
|
60
|
+
const name = key.type === 'Identifier' ? key.name : (key.type === 'StringLiteral' ? key.value : null);
|
|
61
|
+
if (!name || !names.has(name)) continue;
|
|
62
|
+
const targetPath = extractStaticStringValue(prop.value);
|
|
63
|
+
if (targetPath) {
|
|
64
|
+
return { attributeName: name, targetPath };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { attributeName: null, targetPath: null };
|
|
68
|
+
}
|
|
69
|
+
|
|
49
70
|
/**
|
|
50
71
|
* Extracts template literal pattern from Babel TemplateLiteral node.
|
|
51
72
|
* Returns null if template has complex expressions that cannot be normalized.
|
|
@@ -176,6 +197,37 @@ function extractContractsFromFile(filePath, fileContent) {
|
|
|
176
197
|
// since we cannot reliably tie them to clicked elements
|
|
177
198
|
CallExpression(path) {
|
|
178
199
|
const callee = path.node.callee;
|
|
200
|
+
|
|
201
|
+
// React.createElement(Link, { to: '/about' }) or createElement('a', { href: '/about' })
|
|
202
|
+
const isCreateElement = (
|
|
203
|
+
(callee.type === 'MemberExpression' && callee.object.type === 'Identifier' && callee.object.name === 'React' && callee.property.type === 'Identifier' && callee.property.name === 'createElement') ||
|
|
204
|
+
(callee.type === 'Identifier' && callee.name === 'createElement')
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (isCreateElement) {
|
|
208
|
+
const [componentArg, propsArg] = path.node.arguments;
|
|
209
|
+
let elementName = null;
|
|
210
|
+
if (componentArg?.type === 'Identifier') {
|
|
211
|
+
elementName = componentArg.name;
|
|
212
|
+
} else if (componentArg?.type === 'StringLiteral') {
|
|
213
|
+
elementName = componentArg.value;
|
|
214
|
+
}
|
|
215
|
+
if (elementName) {
|
|
216
|
+
const { attributeName, targetPath } = extractStaticPropValue(propsArg, ['to', 'href']);
|
|
217
|
+
if (targetPath && !targetPath.startsWith('http://') && !targetPath.startsWith('https://') && !targetPath.startsWith('mailto:') && !targetPath.startsWith('tel:')) {
|
|
218
|
+
const normalized = targetPath.startsWith('/') ? targetPath : '/' + targetPath;
|
|
219
|
+
contracts.push({
|
|
220
|
+
kind: 'NAVIGATION',
|
|
221
|
+
targetPath: normalized,
|
|
222
|
+
sourceFile: filePath,
|
|
223
|
+
element: elementName,
|
|
224
|
+
attribute: attributeName,
|
|
225
|
+
proof: ExpectationProof.PROVEN_EXPECTATION,
|
|
226
|
+
line: path.node.loc?.start.line || null
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
179
231
|
|
|
180
232
|
// navigate("/about") - useNavigate hook
|
|
181
233
|
if (callee.type === 'Identifier' && callee.name === 'navigate') {
|
|
@@ -308,7 +360,7 @@ export async function extractASTContracts(projectDir) {
|
|
|
308
360
|
* Converts AST contracts to manifest expectations format.
|
|
309
361
|
* Only includes contracts that can be matched at runtime (excludes imperativeOnly).
|
|
310
362
|
*/
|
|
311
|
-
export function contractsToExpectations(contracts,
|
|
363
|
+
export function contractsToExpectations(contracts, _projectType) {
|
|
312
364
|
const expectations = [];
|
|
313
365
|
const seenPaths = new Set();
|
|
314
366
|
|
package/src/verax/learn/index.js
CHANGED
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
import { resolve } from 'path';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
3
|
import { detectProjectType } from './project-detector.js';
|
|
4
4
|
import { extractRoutes } from './route-extractor.js';
|
|
5
5
|
import { writeManifest } from './manifest-writer.js';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} LearnResult
|
|
9
|
+
* @property {number} version
|
|
10
|
+
* @property {string} learnedAt
|
|
11
|
+
* @property {string} projectDir
|
|
12
|
+
* @property {string} projectType
|
|
13
|
+
* @property {Array} routes
|
|
14
|
+
* @property {Array<string>} publicRoutes
|
|
15
|
+
* @property {Array<string>} internalRoutes
|
|
16
|
+
* @property {Array} [staticExpectations]
|
|
17
|
+
* @property {Array} [flows]
|
|
18
|
+
* @property {string} [expectationsStatus]
|
|
19
|
+
* @property {Array} [coverageGaps]
|
|
20
|
+
* @property {Array} notes
|
|
21
|
+
* @property {Object} [learnTruth]
|
|
22
|
+
* @property {string} [manifestPath] - Optional manifest path (added when loaded from file)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} projectDir
|
|
27
|
+
* @returns {Promise<LearnResult>}
|
|
28
|
+
*/
|
|
7
29
|
export async function learn(projectDir) {
|
|
8
30
|
const absoluteProjectDir = resolve(projectDir);
|
|
9
31
|
|
|
@@ -14,5 +36,17 @@ export async function learn(projectDir) {
|
|
|
14
36
|
const projectType = await detectProjectType(absoluteProjectDir);
|
|
15
37
|
const routes = await extractRoutes(absoluteProjectDir, projectType);
|
|
16
38
|
|
|
17
|
-
|
|
39
|
+
const manifest = await writeManifest(absoluteProjectDir, projectType, routes);
|
|
40
|
+
|
|
41
|
+
// Write manifest to disk and return path
|
|
42
|
+
const veraxDir = resolve(absoluteProjectDir, '.verax');
|
|
43
|
+
mkdirSync(veraxDir, { recursive: true });
|
|
44
|
+
|
|
45
|
+
const manifestPath = resolve(veraxDir, 'project.json');
|
|
46
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
...manifest,
|
|
50
|
+
manifestPath
|
|
51
|
+
};
|
|
18
52
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { writeFileSync, mkdirSync } from 'fs';
|
|
1
|
+
// resolve, writeFileSync, mkdirSync imports removed - currently unused
|
|
3
2
|
import { extractStaticExpectations } from './static-extractor.js';
|
|
4
3
|
import { assessLearnTruth } from './truth-assessor.js';
|
|
5
4
|
import { runCodeIntelligence } from '../intel/index.js';
|
|
@@ -8,6 +7,29 @@ import { extractFlows } from './flow-extractor.js';
|
|
|
8
7
|
import { createTSProgram } from '../intel/ts-program.js';
|
|
9
8
|
import { extractVueNavigationPromises } from '../intel/vue-navigation-extractor.js';
|
|
10
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} Manifest
|
|
12
|
+
* @property {number} version
|
|
13
|
+
* @property {string} learnedAt
|
|
14
|
+
* @property {string} projectDir
|
|
15
|
+
* @property {string} projectType
|
|
16
|
+
* @property {Array} routes
|
|
17
|
+
* @property {Array<string>} publicRoutes
|
|
18
|
+
* @property {Array<string>} internalRoutes
|
|
19
|
+
* @property {Array} [staticExpectations]
|
|
20
|
+
* @property {Array} [flows]
|
|
21
|
+
* @property {string} [expectationsStatus]
|
|
22
|
+
* @property {Array} [coverageGaps]
|
|
23
|
+
* @property {Array} notes
|
|
24
|
+
* @property {Object} [learnTruth]
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} projectDir
|
|
29
|
+
* @param {string} projectType
|
|
30
|
+
* @param {Array} routes
|
|
31
|
+
* @returns {Promise<Manifest>}
|
|
32
|
+
*/
|
|
11
33
|
export async function writeManifest(projectDir, projectType, routes) {
|
|
12
34
|
const publicRoutes = routes.filter(r => r.public).map(r => r.path);
|
|
13
35
|
const internalRoutes = routes.filter(r => !r.public).map(r => r.path);
|
|
@@ -44,7 +66,7 @@ export async function writeManifest(projectDir, projectType, routes) {
|
|
|
44
66
|
const program = createTSProgram(projectDir, { includeJs: true });
|
|
45
67
|
|
|
46
68
|
if (!program.error) {
|
|
47
|
-
const vueNavPromises = extractVueNavigationPromises(program, projectDir);
|
|
69
|
+
const vueNavPromises = await extractVueNavigationPromises(program, projectDir);
|
|
48
70
|
|
|
49
71
|
if (vueNavPromises && vueNavPromises.length > 0) {
|
|
50
72
|
intelExpectations = vueNavPromises.filter(exp => isProvenExpectation(exp));
|
|
@@ -132,16 +154,8 @@ export async function writeManifest(projectDir, projectType, routes) {
|
|
|
132
154
|
learn: learnTruth
|
|
133
155
|
});
|
|
134
156
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const manifestPath = resolve(manifestDir, 'site-manifest.json');
|
|
139
|
-
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
140
|
-
|
|
141
|
-
return {
|
|
142
|
-
...manifest,
|
|
143
|
-
manifestPath: manifestPath,
|
|
144
|
-
learnTruth: learnTruth
|
|
145
|
-
};
|
|
157
|
+
// Note: This function is still used by learn() function
|
|
158
|
+
// Direct usage is deprecated in favor of CLI learn.json writer
|
|
159
|
+
return manifest;
|
|
146
160
|
}
|
|
147
161
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
// resolve import removed - currently unused
|
|
2
2
|
import { extractStaticRoutes } from './static-extractor.js';
|
|
3
3
|
import { createTSProgram } from '../intel/ts-program.js';
|
|
4
4
|
import { extractRoutes as extractRoutesAST } from '../intel/route-extractor.js';
|
|
@@ -99,14 +99,15 @@ export async function validateRoutes(manifest, baseUrl) {
|
|
|
99
99
|
|
|
100
100
|
const request = response.request();
|
|
101
101
|
|
|
102
|
-
// Use redirectChain
|
|
102
|
+
// Use redirectChain property if available (Playwright API)
|
|
103
103
|
let redirectChain = [];
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
// @ts-expect-error - redirectChain exists in Playwright runtime but not in TypeScript types
|
|
105
|
+
if (request.redirectChain && Array.isArray(request.redirectChain)) {
|
|
106
|
+
// @ts-expect-error - redirectChain exists in Playwright runtime but not in TypeScript types
|
|
107
|
+
redirectChain = request.redirectChain;
|
|
108
|
+
} else if (request.redirectedFrom) {
|
|
109
|
+
// Fall back to redirectedFrom if redirectChain not available
|
|
110
|
+
redirectChain = [request.redirectedFrom];
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
// If redirectChain is empty or not available, build chain from redirectedFrom
|
|
@@ -11,7 +11,7 @@ const MAX_FILES_TO_SCAN = 200;
|
|
|
11
11
|
* Extracts static string value from call expression arguments.
|
|
12
12
|
* Returns null if value is dynamic.
|
|
13
13
|
*/
|
|
14
|
-
function
|
|
14
|
+
function _extractStaticActionName(node) {
|
|
15
15
|
if (!node) return null;
|
|
16
16
|
|
|
17
17
|
// String literal: dispatch('increment')
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Detects router.push/replace/navigate calls in inline scripts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
function extractNavigationExpectations(root, fromPath, file,
|
|
6
|
+
function extractNavigationExpectations(root, fromPath, file, _projectDir) {
|
|
7
7
|
const expectations = [];
|
|
8
8
|
|
|
9
9
|
// Extract from inline scripts
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Detects preventDefault() calls in onSubmit handlers.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
function extractValidationExpectations(root, fromPath, file,
|
|
6
|
+
function extractValidationExpectations(root, fromPath, file, _projectDir) {
|
|
7
7
|
const expectations = [];
|
|
8
8
|
|
|
9
9
|
// Extract from inline scripts
|
|
@@ -19,7 +19,7 @@ function extractValidationExpectations(root, fromPath, file, projectDir) {
|
|
|
19
19
|
// Check if this is in an onSubmit context
|
|
20
20
|
// Look for function definitions that might be onSubmit handlers
|
|
21
21
|
const beforeMatch = scriptContent.substring(0, match.index);
|
|
22
|
-
const
|
|
22
|
+
const _afterMatch = scriptContent.substring(match.index);
|
|
23
23
|
|
|
24
24
|
// Check if there's a function definition before this preventDefault
|
|
25
25
|
const functionMatch = beforeMatch.match(/function\s+(\w+)\s*\([^)]*event[^)]*\)/);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { glob } from 'glob';
|
|
2
|
-
import { resolve, dirname,
|
|
2
|
+
import { resolve, dirname, relative } from 'path';
|
|
3
3
|
import { readFileSync, existsSync } from 'fs';
|
|
4
4
|
import { parse } from 'node-html-parser';
|
|
5
5
|
|
|
6
6
|
const MAX_HTML_FILES = 200;
|
|
7
7
|
|
|
8
8
|
function htmlFileToRoute(file) {
|
|
9
|
-
let path = file.replace(/[
|
|
10
|
-
path = path.replace(/\/index
|
|
9
|
+
let path = file.replace(/[\\/]/g, '/');
|
|
10
|
+
path = path.replace(/\/index.html$/, '');
|
|
11
11
|
path = path.replace(/\.html$/, '');
|
|
12
12
|
path = path.replace(/^index$/, '');
|
|
13
13
|
|
|
@@ -159,7 +159,7 @@ function extractFormSubmissionExpectations(root, fromPath, file, routeMap, proje
|
|
|
159
159
|
return expectations;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
function extractNetworkExpectations(root, fromPath, file,
|
|
162
|
+
function extractNetworkExpectations(root, fromPath, file, _projectDir) {
|
|
163
163
|
const expectations = [];
|
|
164
164
|
|
|
165
165
|
// Extract from inline scripts
|
|
@@ -286,9 +286,10 @@ export async function extractStaticExpectations(projectDir, routes) {
|
|
|
286
286
|
const targetPath = resolveLinkPath(href, file, projectDir);
|
|
287
287
|
if (!targetPath) continue;
|
|
288
288
|
|
|
289
|
-
if
|
|
290
|
-
|
|
291
|
-
|
|
289
|
+
// Extract expectation even if target route doesn't exist yet
|
|
290
|
+
// This allows detection of broken links or prevented navigation
|
|
291
|
+
// (The route may not exist, but the link promises navigation)
|
|
292
|
+
const _linkText = link.textContent?.trim() || '';
|
|
292
293
|
const selectorHint = link.id ? `#${link.id}` : `a[href="${href}"]`;
|
|
293
294
|
|
|
294
295
|
expectations.push({
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import ts from 'typescript';
|
|
11
11
|
import { resolve, relative, dirname, sep, join } from 'path';
|
|
12
|
-
import {
|
|
12
|
+
import { existsSync, statSync } from 'fs';
|
|
13
13
|
import { glob } from 'glob';
|
|
14
14
|
|
|
15
15
|
const MAX_DEPTH = 3;
|
|
@@ -45,14 +45,8 @@ export async function resolveActionContracts(rootDir, workspaceRoot) {
|
|
|
45
45
|
const checker = program.getTypeChecker();
|
|
46
46
|
const contracts = [];
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!sourcePath.toLowerCase().startsWith(normalizedRoot.toLowerCase())) continue;
|
|
51
|
-
if (sourceFile.isDeclarationFile) continue;
|
|
52
|
-
|
|
53
|
-
const importMap = buildImportMap(sourceFile, rootDir, workspaceRoot);
|
|
54
|
-
|
|
55
|
-
function visit(node) {
|
|
48
|
+
function createVisitFunction(importMap, sourceFile) {
|
|
49
|
+
return function visit(node) {
|
|
56
50
|
if (ts.isJsxAttribute(node)) {
|
|
57
51
|
const name = node.name.getText();
|
|
58
52
|
if (name !== 'onClick' && name !== 'onSubmit') return;
|
|
@@ -64,7 +58,7 @@ export async function resolveActionContracts(rootDir, workspaceRoot) {
|
|
|
64
58
|
// We only handle identifier handlers (cross-file capable)
|
|
65
59
|
if (!ts.isIdentifier(expr)) return;
|
|
66
60
|
|
|
67
|
-
const
|
|
61
|
+
const _handlerName = expr.text;
|
|
68
62
|
const handlerRef = deriveHandlerRef(expr, importMap, sourceFile, workspaceRoot);
|
|
69
63
|
if (!handlerRef) return;
|
|
70
64
|
|
|
@@ -112,8 +106,16 @@ export async function resolveActionContracts(rootDir, workspaceRoot) {
|
|
|
112
106
|
}
|
|
113
107
|
}
|
|
114
108
|
ts.forEachChild(node, visit);
|
|
115
|
-
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
116
111
|
|
|
112
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
113
|
+
const sourcePath = resolve(sourceFile.fileName);
|
|
114
|
+
if (!sourcePath.toLowerCase().startsWith(normalizedRoot.toLowerCase())) continue;
|
|
115
|
+
if (sourceFile.isDeclarationFile) continue;
|
|
116
|
+
|
|
117
|
+
const importMap = buildImportMap(sourceFile, rootDir, workspaceRoot);
|
|
118
|
+
const visit = createVisitFunction(importMap, sourceFile);
|
|
117
119
|
visit(sourceFile);
|
|
118
120
|
}
|
|
119
121
|
|
|
@@ -313,7 +315,7 @@ function analyzeStateCall(node) {
|
|
|
313
315
|
|
|
314
316
|
// Zustand: store.set(...) or setState(...)
|
|
315
317
|
if (ts.isPropertyAccessExpression(callee)) {
|
|
316
|
-
const
|
|
318
|
+
const _obj = callee.expression;
|
|
317
319
|
const prop = callee.name;
|
|
318
320
|
// Common pattern: storeObj.set(...)
|
|
319
321
|
if (prop.text === 'set') {
|