@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.
Files changed (89) hide show
  1. package/package.json +14 -4
  2. package/src/cli/commands/default.js +244 -86
  3. package/src/cli/commands/doctor.js +36 -4
  4. package/src/cli/commands/run.js +253 -69
  5. package/src/cli/entry.js +5 -5
  6. package/src/cli/util/detection-engine.js +4 -3
  7. package/src/cli/util/events.js +76 -0
  8. package/src/cli/util/expectation-extractor.js +11 -1
  9. package/src/cli/util/findings-writer.js +1 -0
  10. package/src/cli/util/observation-engine.js +69 -23
  11. package/src/cli/util/paths.js +3 -2
  12. package/src/cli/util/project-discovery.js +20 -0
  13. package/src/cli/util/redact.js +2 -2
  14. package/src/cli/util/runtime-budget.js +147 -0
  15. package/src/cli/util/summary-writer.js +12 -1
  16. package/src/types/global.d.ts +28 -0
  17. package/src/types/ts-ast.d.ts +24 -0
  18. package/src/verax/cli/doctor.js +2 -2
  19. package/src/verax/cli/init.js +1 -1
  20. package/src/verax/cli/url-safety.js +12 -2
  21. package/src/verax/cli/wizard.js +13 -2
  22. package/src/verax/core/budget-engine.js +1 -1
  23. package/src/verax/core/decision-snapshot.js +2 -2
  24. package/src/verax/core/determinism-model.js +35 -6
  25. package/src/verax/core/incremental-store.js +15 -7
  26. package/src/verax/core/replay-validator.js +4 -4
  27. package/src/verax/core/replay.js +1 -1
  28. package/src/verax/core/silence-impact.js +1 -1
  29. package/src/verax/core/silence-model.js +9 -7
  30. package/src/verax/detect/comparison.js +8 -3
  31. package/src/verax/detect/confidence-engine.js +17 -17
  32. package/src/verax/detect/detection-engine.js +1 -1
  33. package/src/verax/detect/evidence-index.js +15 -65
  34. package/src/verax/detect/expectation-model.js +54 -3
  35. package/src/verax/detect/explanation-helpers.js +1 -1
  36. package/src/verax/detect/finding-detector.js +2 -2
  37. package/src/verax/detect/findings-writer.js +9 -16
  38. package/src/verax/detect/flow-detector.js +4 -4
  39. package/src/verax/detect/index.js +37 -11
  40. package/src/verax/detect/interactive-findings.js +3 -4
  41. package/src/verax/detect/signal-mapper.js +2 -2
  42. package/src/verax/detect/skip-classifier.js +4 -4
  43. package/src/verax/detect/verdict-engine.js +4 -6
  44. package/src/verax/flow/flow-engine.js +3 -2
  45. package/src/verax/flow/flow-spec.js +1 -2
  46. package/src/verax/index.js +15 -3
  47. package/src/verax/intel/effect-detector.js +1 -1
  48. package/src/verax/intel/index.js +2 -2
  49. package/src/verax/intel/route-extractor.js +3 -3
  50. package/src/verax/intel/vue-navigation-extractor.js +81 -18
  51. package/src/verax/intel/vue-router-extractor.js +4 -2
  52. package/src/verax/learn/action-contract-extractor.js +3 -3
  53. package/src/verax/learn/ast-contract-extractor.js +53 -1
  54. package/src/verax/learn/index.js +36 -2
  55. package/src/verax/learn/manifest-writer.js +28 -14
  56. package/src/verax/learn/route-extractor.js +1 -1
  57. package/src/verax/learn/route-validator.js +8 -7
  58. package/src/verax/learn/state-extractor.js +1 -1
  59. package/src/verax/learn/static-extractor-navigation.js +1 -1
  60. package/src/verax/learn/static-extractor-validation.js +2 -2
  61. package/src/verax/learn/static-extractor.js +8 -7
  62. package/src/verax/learn/ts-contract-resolver.js +14 -12
  63. package/src/verax/observe/browser.js +22 -3
  64. package/src/verax/observe/console-sensor.js +2 -2
  65. package/src/verax/observe/expectation-executor.js +2 -1
  66. package/src/verax/observe/focus-sensor.js +1 -1
  67. package/src/verax/observe/human-driver.js +29 -10
  68. package/src/verax/observe/index.js +10 -7
  69. package/src/verax/observe/interaction-discovery.js +27 -15
  70. package/src/verax/observe/interaction-runner.js +6 -6
  71. package/src/verax/observe/loading-sensor.js +6 -0
  72. package/src/verax/observe/navigation-sensor.js +1 -1
  73. package/src/verax/observe/settle.js +1 -0
  74. package/src/verax/observe/state-sensor.js +8 -4
  75. package/src/verax/observe/state-ui-sensor.js +7 -1
  76. package/src/verax/observe/traces-writer.js +27 -16
  77. package/src/verax/observe/ui-signal-sensor.js +7 -0
  78. package/src/verax/scan-summary-writer.js +5 -2
  79. package/src/verax/shared/artifact-manager.js +1 -1
  80. package/src/verax/shared/budget-profiles.js +2 -2
  81. package/src/verax/shared/caching.js +1 -1
  82. package/src/verax/shared/config-loader.js +1 -2
  83. package/src/verax/shared/dynamic-route-utils.js +12 -6
  84. package/src/verax/shared/retry-policy.js +1 -6
  85. package/src/verax/shared/root-artifacts.js +1 -1
  86. package/src/verax/shared/zip-artifacts.js +1 -0
  87. package/src/verax/validate/context-validator.js +1 -1
  88. package/src/verax/observe/index.js.backup +0 -1
  89. 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} program - TypeScript program
21
- * @param {string} projectRoot - Project root
22
- * @returns {Promise<Array>} - Array of navigation expectation objects
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 async function extractVueNavigationPromises(program, projectRoot) {
25
- const expectations = [];
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 expectations;
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 sourceFiles = program.getSourceFiles ? program.getSourceFiles() : (program.sourceFiles || []);
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 { glob } = await import('glob');
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(projectRoot, filePath);
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, program) {
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 calls = [];
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 elements = [];
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 { resolve, relative, sep } from 'path';
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 (n.hasOwnProperty(key)) {
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>} - All contracts found
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, projectType) {
363
+ export function contractsToExpectations(contracts, _projectType) {
312
364
  const expectations = [];
313
365
  const seenPaths = new Set();
314
366
 
@@ -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
- return await writeManifest(absoluteProjectDir, projectType, routes);
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
- import { resolve } from 'path';
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
- const manifestDir = resolve(projectDir, '.veraxverax', 'learn');
136
- mkdirSync(manifestDir, { recursive: true });
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 { resolve } from 'path';
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() if available (preferred), otherwise use redirectedFrom()
102
+ // Use redirectChain property if available (Playwright API)
103
103
  let redirectChain = [];
104
- if (typeof request.redirectChain === 'function') {
105
- try {
106
- redirectChain = request.redirectChain();
107
- } catch (e) {
108
- // redirectChain may not be available, fall back to redirectedFrom
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 extractStaticActionName(node) {
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, projectDir) {
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, projectDir) {
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 afterMatch = scriptContent.substring(match.index);
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, join, relative } from 'path';
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(/[\\\/]/g, '/');
10
- path = path.replace(/\/index\.html$/, '');
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, projectDir) {
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 (!routeMap.has(targetPath)) continue;
290
-
291
- const linkText = link.textContent?.trim() || '';
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 { readFileSync, existsSync, statSync } from 'fs';
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
- for (const sourceFile of program.getSourceFiles()) {
49
- const sourcePath = resolve(sourceFile.fileName);
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 handlerName = expr.text;
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 obj = callee.expression;
318
+ const _obj = callee.expression;
317
319
  const prop = callee.name;
318
320
  // Common pattern: storeObj.set(...)
319
321
  if (prop.text === 'set') {