@veraxhq/verax 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +14 -36
  4. package/src/cli/commands/default.js +523 -0
  5. package/src/cli/commands/doctor.js +165 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +402 -0
  8. package/src/cli/entry.js +196 -0
  9. package/src/cli/util/atomic-write.js +37 -0
  10. package/src/cli/util/detection-engine.js +296 -0
  11. package/src/cli/util/env-url.js +33 -0
  12. package/src/cli/util/errors.js +44 -0
  13. package/src/cli/util/events.js +34 -0
  14. package/src/cli/util/expectation-extractor.js +378 -0
  15. package/src/cli/util/findings-writer.js +31 -0
  16. package/src/cli/util/idgen.js +87 -0
  17. package/src/cli/util/learn-writer.js +39 -0
  18. package/src/cli/util/observation-engine.js +366 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +29 -0
  21. package/src/cli/util/project-discovery.js +277 -0
  22. package/src/cli/util/project-writer.js +26 -0
  23. package/src/cli/util/redact.js +128 -0
  24. package/src/cli/util/run-id.js +30 -0
  25. package/src/cli/util/summary-writer.js +32 -0
  26. package/src/verax/cli/ci-summary.js +35 -0
  27. package/src/verax/cli/context-explanation.js +89 -0
  28. package/src/verax/cli/doctor.js +277 -0
  29. package/src/verax/cli/error-normalizer.js +154 -0
  30. package/src/verax/cli/explain-output.js +105 -0
  31. package/src/verax/cli/finding-explainer.js +130 -0
  32. package/src/verax/cli/init.js +237 -0
  33. package/src/verax/cli/run-overview.js +163 -0
  34. package/src/verax/cli/url-safety.js +101 -0
  35. package/src/verax/cli/wizard.js +98 -0
  36. package/src/verax/cli/zero-findings-explainer.js +57 -0
  37. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  38. package/src/verax/core/action-classifier.js +86 -0
  39. package/src/verax/core/budget-engine.js +218 -0
  40. package/src/verax/core/canonical-outcomes.js +157 -0
  41. package/src/verax/core/decision-snapshot.js +335 -0
  42. package/src/verax/core/determinism-model.js +403 -0
  43. package/src/verax/core/incremental-store.js +237 -0
  44. package/src/verax/core/invariants.js +356 -0
  45. package/src/verax/core/promise-model.js +230 -0
  46. package/src/verax/core/replay-validator.js +350 -0
  47. package/src/verax/core/replay.js +222 -0
  48. package/src/verax/core/run-id.js +175 -0
  49. package/src/verax/core/run-manifest.js +99 -0
  50. package/src/verax/core/silence-impact.js +369 -0
  51. package/src/verax/core/silence-model.js +521 -0
  52. package/src/verax/detect/comparison.js +2 -34
  53. package/src/verax/detect/confidence-engine.js +764 -329
  54. package/src/verax/detect/detection-engine.js +293 -0
  55. package/src/verax/detect/evidence-index.js +177 -0
  56. package/src/verax/detect/expectation-model.js +194 -172
  57. package/src/verax/detect/explanation-helpers.js +187 -0
  58. package/src/verax/detect/finding-detector.js +450 -0
  59. package/src/verax/detect/findings-writer.js +44 -8
  60. package/src/verax/detect/flow-detector.js +366 -0
  61. package/src/verax/detect/index.js +172 -286
  62. package/src/verax/detect/interactive-findings.js +613 -0
  63. package/src/verax/detect/signal-mapper.js +308 -0
  64. package/src/verax/detect/verdict-engine.js +563 -0
  65. package/src/verax/evidence-index-writer.js +61 -0
  66. package/src/verax/index.js +90 -14
  67. package/src/verax/intel/effect-detector.js +368 -0
  68. package/src/verax/intel/handler-mapper.js +249 -0
  69. package/src/verax/intel/index.js +281 -0
  70. package/src/verax/intel/route-extractor.js +280 -0
  71. package/src/verax/intel/ts-program.js +256 -0
  72. package/src/verax/intel/vue-navigation-extractor.js +579 -0
  73. package/src/verax/intel/vue-router-extractor.js +323 -0
  74. package/src/verax/learn/action-contract-extractor.js +335 -101
  75. package/src/verax/learn/ast-contract-extractor.js +95 -5
  76. package/src/verax/learn/flow-extractor.js +172 -0
  77. package/src/verax/learn/manifest-writer.js +97 -47
  78. package/src/verax/learn/project-detector.js +40 -0
  79. package/src/verax/learn/route-extractor.js +27 -96
  80. package/src/verax/learn/state-extractor.js +212 -0
  81. package/src/verax/learn/static-extractor-navigation.js +114 -0
  82. package/src/verax/learn/static-extractor-validation.js +88 -0
  83. package/src/verax/learn/static-extractor.js +112 -4
  84. package/src/verax/learn/truth-assessor.js +24 -21
  85. package/src/verax/observe/aria-sensor.js +211 -0
  86. package/src/verax/observe/browser.js +10 -5
  87. package/src/verax/observe/console-sensor.js +1 -17
  88. package/src/verax/observe/domain-boundary.js +10 -1
  89. package/src/verax/observe/expectation-executor.js +512 -0
  90. package/src/verax/observe/flow-matcher.js +143 -0
  91. package/src/verax/observe/focus-sensor.js +196 -0
  92. package/src/verax/observe/human-driver.js +643 -275
  93. package/src/verax/observe/index.js +908 -27
  94. package/src/verax/observe/index.js.backup +1 -0
  95. package/src/verax/observe/interaction-discovery.js +365 -14
  96. package/src/verax/observe/interaction-runner.js +563 -198
  97. package/src/verax/observe/loading-sensor.js +139 -0
  98. package/src/verax/observe/navigation-sensor.js +255 -0
  99. package/src/verax/observe/network-sensor.js +55 -7
  100. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  101. package/src/verax/observe/observed-expectation.js +305 -0
  102. package/src/verax/observe/page-frontier.js +234 -0
  103. package/src/verax/observe/settle.js +37 -17
  104. package/src/verax/observe/state-sensor.js +389 -0
  105. package/src/verax/observe/timing-sensor.js +228 -0
  106. package/src/verax/observe/traces-writer.js +61 -20
  107. package/src/verax/observe/ui-signal-sensor.js +136 -17
  108. package/src/verax/scan-summary-writer.js +77 -15
  109. package/src/verax/shared/artifact-manager.js +110 -8
  110. package/src/verax/shared/budget-profiles.js +136 -0
  111. package/src/verax/shared/ci-detection.js +39 -0
  112. package/src/verax/shared/config-loader.js +170 -0
  113. package/src/verax/shared/dynamic-route-utils.js +218 -0
  114. package/src/verax/shared/expectation-coverage.js +44 -0
  115. package/src/verax/shared/expectation-prover.js +81 -0
  116. package/src/verax/shared/expectation-tracker.js +201 -0
  117. package/src/verax/shared/expectations-writer.js +60 -0
  118. package/src/verax/shared/first-run.js +44 -0
  119. package/src/verax/shared/progress-reporter.js +171 -0
  120. package/src/verax/shared/retry-policy.js +14 -1
  121. package/src/verax/shared/root-artifacts.js +49 -0
  122. package/src/verax/shared/scan-budget.js +86 -0
  123. package/src/verax/shared/url-normalizer.js +162 -0
  124. package/src/verax/shared/zip-artifacts.js +65 -0
  125. package/src/verax/validate/context-validator.js +244 -0
  126. package/src/verax/validate/context-validator.js.bak +0 -0
@@ -0,0 +1,172 @@
1
+ /**
2
+ * FLOW INTELLIGENCE v1 — Flow Extractor
3
+ *
4
+ * Extracts multi-step flows from PROVEN expectations.
5
+ * NO HEURISTICS: Only sequences with explicit PROVEN expectations qualify as flows.
6
+ */
7
+
8
+ import { createHash } from 'crypto';
9
+ import { isProvenExpectation } from '../shared/expectation-prover.js';
10
+
11
+ /**
12
+ * Generate stable flow ID from ordered steps
13
+ */
14
+ function generateFlowId(steps) {
15
+ const hashInput = steps.map(s => `${s.expectationType}:${s.source || s.handlerRef}`).join('|');
16
+ const hash = createHash('sha256').update(hashInput).digest('hex');
17
+ return `flow-${hash.substring(0, 8)}`;
18
+ }
19
+
20
+ /**
21
+ * Determine if an expectation can be part of a flow.
22
+ * Only PROVEN expectations with clear sequential intent qualify.
23
+ */
24
+ function isFlowableExpectation(exp) {
25
+ // Navigation expectations: always flowable (user journey)
26
+ if (exp.expectationType === 'navigation') return true;
27
+
28
+ // Network actions: flowable (form submissions, API calls)
29
+ if (exp.kind === 'NETWORK_ACTION') return true;
30
+
31
+ // State actions: flowable (state updates in sequence)
32
+ if (exp.kind === 'STATE_ACTION') return true;
33
+
34
+ // Validation blocks: NOT flowable alone (they block, don't progress)
35
+ if (exp.kind === 'VALIDATION_BLOCK') return false;
36
+
37
+ return false;
38
+ }
39
+
40
+ /**
41
+ * Extract flows from manifest expectations.
42
+ *
43
+ * Flow definition:
44
+ * - Sequence of 2-5 PROVEN expectations
45
+ * - Temporally ordered within same route/page
46
+ * - Each step must be deterministically executable
47
+ *
48
+ * @param {Object} manifest - Learned manifest
49
+ * @returns {Array} Array of flow definitions
50
+ */
51
+ export function extractFlows(manifest) {
52
+ const flows = [];
53
+
54
+ // Collect all flowable expectations
55
+ const flowableExpectations = [];
56
+
57
+ // SPA expectations (navigation)
58
+ if (manifest.spaExpectations && manifest.spaExpectations.length > 0) {
59
+ for (const exp of manifest.spaExpectations) {
60
+ if (isFlowableExpectation(exp)) {
61
+ flowableExpectations.push({
62
+ source: exp.source || exp.sourceFile,
63
+ expectationType: 'navigation',
64
+ targetPath: exp.targetPath,
65
+ proof: 'PROVEN_EXPECTATION',
66
+ raw: exp
67
+ });
68
+ }
69
+ }
70
+ }
71
+
72
+ // Action contracts (network, state)
73
+ if (manifest.actionContracts && manifest.actionContracts.length > 0) {
74
+ for (const contract of manifest.actionContracts) {
75
+ if (isFlowableExpectation(contract)) {
76
+ flowableExpectations.push({
77
+ source: contract.source,
78
+ handlerRef: contract.handlerRef,
79
+ expectationType: contract.kind === 'NETWORK_ACTION' ? 'network_action' : 'state_action',
80
+ method: contract.method,
81
+ urlPath: contract.urlPath,
82
+ proof: 'PROVEN_EXPECTATION',
83
+ raw: contract
84
+ });
85
+ }
86
+ }
87
+ }
88
+
89
+ // Static expectations (navigation, network_action, form submissions)
90
+ if (manifest.staticExpectations && manifest.staticExpectations.length > 0) {
91
+ for (const exp of manifest.staticExpectations) {
92
+ // FLOW INTELLIGENCE v1: Only include PROVEN expectations
93
+ if (!isProvenExpectation(exp)) continue;
94
+
95
+ let expType = null;
96
+ if (exp.type === 'navigation' || exp.type === 'spa_navigation') {
97
+ expType = 'navigation';
98
+ } else if (exp.type === 'network_action') {
99
+ expType = 'network_action';
100
+ } else if (exp.type === 'form_submission') {
101
+ expType = 'network_action'; // Form submissions are network actions
102
+ } else {
103
+ continue; // Skip non-flowable types
104
+ }
105
+
106
+ flowableExpectations.push({
107
+ source: exp.evidence?.source || exp.fromPath || exp.metadata?.elementFile,
108
+ handlerRef: exp.handlerRef || exp.metadata?.handlerFile,
109
+ expectationType: expType,
110
+ targetPath: exp.targetPath || exp.expectedTarget,
111
+ urlPath: exp.urlPath || exp.expectedTarget,
112
+ method: exp.method,
113
+ proof: 'PROVEN_EXPECTATION',
114
+ raw: exp
115
+ });
116
+ }
117
+ }
118
+
119
+ // DETERMINISTIC FLOW DETECTION: Group by source page
120
+ // Flows are sequences on the same page (same source file/route)
121
+ const bySource = new Map();
122
+
123
+ for (const exp of flowableExpectations) {
124
+ // Extract file name from source (e.g., "index.html" from "index.html:56:68")
125
+ let sourceKey = 'unknown';
126
+ if (exp.source) {
127
+ sourceKey = exp.source.split(':')[0]; // Get filename part
128
+ } else if (exp.handlerRef) {
129
+ sourceKey = exp.handlerRef.split(':')[0];
130
+ }
131
+
132
+ if (!bySource.has(sourceKey)) {
133
+ bySource.set(sourceKey, []);
134
+ }
135
+ bySource.get(sourceKey).push(exp);
136
+ }
137
+
138
+ // For each source, create flows of 2-5 sequential expectations
139
+ for (const [source, expectations] of bySource.entries()) {
140
+ if (expectations.length < 2) continue; // Need at least 2 for a flow
141
+
142
+ // Take sequences of 2-5 expectations as potential flows
143
+ const maxFlowLength = Math.min(5, expectations.length);
144
+
145
+ for (let length = 2; length <= maxFlowLength; length++) {
146
+ // Only create one flow per source (the full sequence up to 5 steps)
147
+ if (length === Math.min(expectations.length, 5)) {
148
+ const flowSteps = expectations.slice(0, length).map((exp, idx) => ({
149
+ stepIndex: idx,
150
+ expectationType: exp.expectationType,
151
+ source: exp.source,
152
+ handlerRef: exp.handlerRef,
153
+ targetPath: exp.targetPath,
154
+ method: exp.method,
155
+ urlPath: exp.urlPath
156
+ }));
157
+
158
+ const flowId = generateFlowId(flowSteps);
159
+
160
+ flows.push({
161
+ flowId,
162
+ source,
163
+ stepCount: length,
164
+ steps: flowSteps,
165
+ proof: 'PROVEN_EXPECTATION'
166
+ });
167
+ }
168
+ }
169
+ }
170
+
171
+ return flows;
172
+ }
@@ -2,10 +2,11 @@ import { resolve } from 'path';
2
2
  import { writeFileSync, mkdirSync } from 'fs';
3
3
  import { extractStaticExpectations } from './static-extractor.js';
4
4
  import { assessLearnTruth } from './truth-assessor.js';
5
- import { ExpectationProof } from '../shared/expectation-proof.js';
6
- import { extractASTContracts, contractsToExpectations } from './ast-contract-extractor.js';
7
- import { scanForContracts } from './action-contract-extractor.js';
8
- import { resolveActionContracts } from './ts-contract-resolver.js';
5
+ import { runCodeIntelligence } from '../intel/index.js';
6
+ import { isProvenExpectation } from '../shared/expectation-prover.js';
7
+ import { extractFlows } from './flow-extractor.js';
8
+ import { createTSProgram } from '../intel/ts-program.js';
9
+ import { extractVueNavigationPromises } from '../intel/vue-navigation-extractor.js';
9
10
 
10
11
  export async function writeManifest(projectDir, projectType, routes) {
11
12
  const publicRoutes = routes.filter(r => r.public).map(r => r.path);
@@ -19,7 +20,8 @@ export async function writeManifest(projectDir, projectType, routes) {
19
20
  routes: routes.map(r => ({
20
21
  path: r.path,
21
22
  source: r.source,
22
- public: r.public
23
+ public: r.public,
24
+ sourceRef: r.sourceRef || null
23
25
  })),
24
26
  publicRoutes: publicRoutes,
25
27
  internalRoutes: internalRoutes,
@@ -27,49 +29,110 @@ export async function writeManifest(projectDir, projectType, routes) {
27
29
  };
28
30
 
29
31
  let staticExpectations = null;
30
- let spaExpectations = null;
31
- let actionContracts = null;
32
+ let intelExpectations = [];
33
+ let allExpectations = [];
32
34
 
35
+ // Static sites: extract from HTML
33
36
  if (projectType === 'static' && routes.length > 0) {
34
37
  staticExpectations = await extractStaticExpectations(projectDir, routes);
35
38
  manifest.staticExpectations = staticExpectations;
36
- manifest.expectationProof = ExpectationProof.PROVEN_EXPECTATION;
37
- } else if (projectType === 'react_spa' || projectType.startsWith('nextjs_')) {
38
- // Wave 1 - CODE TRUTH ENGINE: Extract PROVEN contracts from AST
39
- const contracts = await extractASTContracts(projectDir);
40
- spaExpectations = contractsToExpectations(contracts, projectType);
39
+ allExpectations = staticExpectations || [];
40
+ }
41
+
42
+ // Vue projects: extract navigation promises from code
43
+ if (projectType === 'vue_router' || projectType === 'vue_spa') {
44
+ const program = createTSProgram(projectDir, { includeJs: true });
41
45
 
42
- if (spaExpectations.length > 0) {
43
- manifest.spaExpectations = spaExpectations;
44
- manifest.expectationProof = ExpectationProof.PROVEN_EXPECTATION;
45
- } else {
46
- manifest.expectationProof = ExpectationProof.UNKNOWN_EXPECTATION;
46
+ if (!program.error) {
47
+ const vueNavPromises = extractVueNavigationPromises(program, projectDir);
48
+
49
+ if (vueNavPromises && vueNavPromises.length > 0) {
50
+ intelExpectations = vueNavPromises.filter(exp => isProvenExpectation(exp));
51
+
52
+ if (!manifest.staticExpectations) {
53
+ manifest.staticExpectations = [];
54
+ }
55
+ manifest.staticExpectations.push(...intelExpectations);
56
+ allExpectations = intelExpectations;
57
+ }
47
58
  }
48
- } else {
49
- manifest.expectationProof = ExpectationProof.UNKNOWN_EXPECTATION;
50
59
  }
51
60
 
52
- // Wave 5/6 - ACTION CONTRACTS: Extract network action contracts (Babel inline + TS cross-file)
53
- const shouldExtractActions = projectType === 'react_spa' || projectType.startsWith('nextjs_');
54
- if (shouldExtractActions) {
55
- const [babelContracts, tsContracts] = await Promise.all([
56
- scanForContracts(projectDir, projectDir),
57
- resolveActionContracts(projectDir, projectDir).catch(() => [])
58
- ]);
59
- const merged = dedupeActionContracts([...(babelContracts || []), ...(tsContracts || [])]);
60
- if (merged.length > 0) {
61
- actionContracts = merged;
62
- manifest.actionContracts = actionContracts;
61
+ // React SPAs, Next.js, and Vue: use code intelligence (AST-based) - NO FALLBACKS
62
+ if (projectType === 'react_spa' ||
63
+ projectType === 'nextjs_app_router' ||
64
+ projectType === 'nextjs_pages_router' ||
65
+ projectType === 'vue_router' ||
66
+ projectType === 'vue_spa') {
67
+ const intelResult = await runCodeIntelligence(projectDir);
68
+
69
+ if (!intelResult.error && intelResult.expectations) {
70
+ // ZERO-HEURISTIC: Only include expectations that pass isProvenExpectation()
71
+ intelExpectations = intelResult.expectations.filter(exp => isProvenExpectation(exp));
72
+
73
+ // STATE INTELLIGENCE: Also extract state expectations from AST
74
+ const { extractStateExpectationsFromAST } = await import('./state-extractor.js');
75
+ const stateResult = await extractStateExpectationsFromAST(projectDir);
76
+
77
+ if (stateResult.expectations && stateResult.expectations.length > 0) {
78
+ // Convert state expectations to manifest format with sourceRef
79
+ const stateExpectations = stateResult.expectations.map(exp => ({
80
+ type: 'state_action',
81
+ expectedTarget: exp.expectedTarget,
82
+ storeType: exp.storeType,
83
+ proof: 'PROVEN_EXPECTATION',
84
+ sourceRef: exp.line ? `${exp.sourceFile}:${exp.line}` : exp.sourceFile,
85
+ selectorHint: null, // State actions don't have selector hints from AST
86
+ metadata: {
87
+ sourceFile: exp.sourceFile,
88
+ line: exp.line
89
+ }
90
+ })).filter(exp => isProvenExpectation(exp));
91
+
92
+ intelExpectations.push(...stateExpectations);
93
+ }
94
+
95
+ // Add intel expectations to manifest
96
+ if (!manifest.staticExpectations) {
97
+ manifest.staticExpectations = [];
98
+ }
99
+ manifest.staticExpectations.push(...intelExpectations);
100
+ allExpectations = intelExpectations;
63
101
  }
64
102
  }
65
-
66
- const learnTruth = await assessLearnTruth(projectDir, projectType, routes, staticExpectations, spaExpectations);
103
+
104
+ // ZERO-HEURISTIC: Set expectationsStatus and coverageGaps
105
+ const provenCount = allExpectations.filter(exp => isProvenExpectation(exp)).length;
106
+
107
+ if (provenCount === 0 && (projectType === 'react_spa' ||
108
+ projectType === 'nextjs_app_router' ||
109
+ projectType === 'nextjs_pages_router' ||
110
+ projectType === 'vue_router' ||
111
+ projectType === 'vue_spa')) {
112
+ manifest.expectationsStatus = 'NO_PROVEN_EXPECTATIONS';
113
+ manifest.coverageGaps = [{
114
+ reason: 'NO_PROVEN_EXPECTATIONS_AVAILABLE',
115
+ message: 'Code intelligence found no extractable literal effects (navigation, network, validation) with static string literals',
116
+ projectType: projectType
117
+ }];
118
+ } else {
119
+ manifest.expectationsStatus = 'PROVEN_EXPECTATIONS_AVAILABLE';
120
+ manifest.coverageGaps = [];
121
+ }
122
+
123
+ // FLOW INTELLIGENCE v1: Extract flows from PROVEN expectations
124
+ const flows = extractFlows(manifest);
125
+ if (flows.length > 0) {
126
+ manifest.flows = flows;
127
+ }
128
+
129
+ const learnTruth = await assessLearnTruth(projectDir, projectType, routes, allExpectations);
67
130
  manifest.notes.push({
68
131
  type: 'truth',
69
132
  learn: learnTruth
70
133
  });
71
134
 
72
- const manifestDir = resolve(projectDir, '.verax', 'learn');
135
+ const manifestDir = resolve(projectDir, '.veraxverax', 'learn');
73
136
  mkdirSync(manifestDir, { recursive: true });
74
137
 
75
138
  const manifestPath = resolve(manifestDir, 'site-manifest.json');
@@ -78,20 +141,7 @@ export async function writeManifest(projectDir, projectType, routes) {
78
141
  return {
79
142
  ...manifest,
80
143
  manifestPath: manifestPath,
81
- learnTruth: learnTruth,
82
- actionContracts
144
+ learnTruth: learnTruth
83
145
  };
84
146
  }
85
147
 
86
- function dedupeActionContracts(contracts) {
87
- const seen = new Set();
88
- const out = [];
89
- for (const c of contracts) {
90
- const key = `${c.kind}|${c.method}|${c.urlPath}|${c.source || ''}|${c.handlerRef || ''}`;
91
- if (seen.has(key)) continue;
92
- seen.add(key);
93
- out.push(c);
94
- }
95
- return out;
96
- }
97
-
@@ -44,6 +44,34 @@ async function hasReactRouter(projectDir) {
44
44
  }
45
45
  }
46
46
 
47
+ async function hasVue(projectDir) {
48
+ try {
49
+ const packageJsonPath = resolve(projectDir, 'package.json');
50
+ if (!existsSync(packageJsonPath)) return false;
51
+
52
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
53
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
54
+
55
+ return !!deps.vue;
56
+ } catch (error) {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ async function hasVueRouter(projectDir) {
62
+ try {
63
+ const packageJsonPath = resolve(projectDir, 'package.json');
64
+ if (!existsSync(packageJsonPath)) return false;
65
+
66
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
67
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
68
+
69
+ return !!deps['vue-router'];
70
+ } catch (error) {
71
+ return false;
72
+ }
73
+ }
74
+
47
75
  export async function detectProjectType(projectDir) {
48
76
  const appDir = resolve(projectDir, 'app');
49
77
  const pagesDir = resolve(projectDir, 'pages');
@@ -62,6 +90,17 @@ export async function detectProjectType(projectDir) {
62
90
  return 'nextjs_pages_router';
63
91
  }
64
92
  }
93
+
94
+ const hasVueDep = await hasVue(projectDir);
95
+ const hasVueRouterDep = await hasVueRouter(projectDir);
96
+
97
+ if (hasVueDep && hasVueRouterDep) {
98
+ return 'vue_router';
99
+ }
100
+
101
+ if (hasVueDep && !hasVueRouterDep) {
102
+ return 'vue_spa';
103
+ }
65
104
 
66
105
  const hasReact = await hasReactDependency(projectDir);
67
106
  const hasNext = await hasNextJs(projectDir);
@@ -85,3 +124,4 @@ export async function hasReactRouterDom(projectDir) {
85
124
  return await hasReactRouter(projectDir);
86
125
  }
87
126
 
127
+
@@ -1,5 +1,7 @@
1
- import { glob } from 'glob';
2
1
  import { resolve } from 'path';
2
+ import { extractStaticRoutes } from './static-extractor.js';
3
+ import { createTSProgram } from '../intel/ts-program.js';
4
+ import { extractRoutes as extractRoutesAST } from '../intel/route-extractor.js';
3
5
 
4
6
  const INTERNAL_PATH_PATTERNS = [
5
7
  /^\/admin/,
@@ -14,109 +16,38 @@ function isInternalRoute(path) {
14
16
  return INTERNAL_PATH_PATTERNS.some(pattern => pattern.test(path));
15
17
  }
16
18
 
17
- function fileToAppRouterPath(file) {
18
- let path = file.replace(/[\\\/]/g, '/');
19
- path = path.replace(/(^|\/)page\.(js|jsx|ts|tsx)$/, '');
20
- path = path.replace(/^\./, '');
21
-
22
- if (path === '' || path === '/') {
23
- return '/';
24
- }
25
-
26
- if (!path.startsWith('/')) {
27
- path = '/' + path;
28
- }
29
-
30
- path = path.replace(/\[([^\]]+)\]/g, ':$1');
31
- path = path.replace(/\([^)]+\)/g, '');
32
-
33
- return path || '/';
34
- }
35
-
36
- function fileToPagesRouterPath(file) {
37
- let path = file.replace(/[\\\/]/g, '/');
38
- path = path.replace(/\.(js|jsx|ts|tsx)$/, '');
39
- path = path.replace(/^index$/, '');
40
- path = path.replace(/^\./, '');
41
-
42
- if (path === '' || path === '/') {
43
- return '/';
44
- }
45
- if (!path.startsWith('/')) {
46
- path = '/' + path;
47
- }
48
-
49
- path = path.replace(/\[([^\]]+)\]/g, ':$1');
50
-
51
- return path || '/';
52
- }
53
-
54
- import { extractStaticRoutes } from './static-extractor.js';
55
- import { extractReactRouterRoutes } from './react-router-extractor.js';
56
- import { hasReactRouterDom } from './project-detector.js';
57
-
58
19
  export async function extractRoutes(projectDir, projectType) {
59
- const routes = [];
60
- const routeSet = new Set();
61
-
20
+ // Static sites: use file-based extractor (no regex, just file system)
62
21
  if (projectType === 'static') {
63
22
  return await extractStaticRoutes(projectDir);
64
23
  }
65
-
66
- if (projectType === 'react_spa') {
67
- const hasRouter = await hasReactRouterDom(projectDir);
68
- if (hasRouter) {
69
- return await extractReactRouterRoutes(projectDir);
70
- }
71
- return [];
72
- }
73
-
74
- if (projectType === 'nextjs_app_router') {
75
- const appDir = resolve(projectDir, 'app');
76
- const pageFiles = await glob('**/page.{js,jsx,ts,tsx}', {
77
- cwd: appDir,
78
- absolute: false,
79
- ignore: ['node_modules/**']
80
- });
24
+
25
+ // React SPAs, Next.js, and Vue: use AST-based intel module (NO REGEX)
26
+ if (projectType === 'react_spa' ||
27
+ projectType === 'nextjs_app_router' ||
28
+ projectType === 'nextjs_pages_router' ||
29
+ projectType === 'vue_router' ||
30
+ projectType === 'vue_spa') {
31
+ const program = createTSProgram(projectDir, { includeJs: true });
81
32
 
82
- for (const file of pageFiles) {
83
- const routePath = fileToAppRouterPath(file);
84
- const routeKey = routePath;
85
-
86
- if (!routeSet.has(routeKey)) {
87
- routeSet.add(routeKey);
88
- routes.push({
89
- path: routePath,
90
- source: `app/${file}`,
91
- public: !isInternalRoute(routePath)
92
- });
93
- }
33
+ if (program.error) {
34
+ // Fallback: return empty routes if TS program creation fails
35
+ return [];
94
36
  }
95
- } else if (projectType === 'nextjs_pages_router') {
96
- const pagesDir = resolve(projectDir, 'pages');
97
- const pageFiles = await glob('**/*.{js,jsx,ts,tsx}', {
98
- cwd: pagesDir,
99
- absolute: false,
100
- ignore: ['node_modules/**', '_app.*', '_document.*', '_error.*']
101
- });
102
37
 
103
- for (const file of pageFiles) {
104
- const routePath = fileToPagesRouterPath(file);
105
- const routeKey = routePath;
106
-
107
- if (!routeSet.has(routeKey)) {
108
- routeSet.add(routeKey);
109
- routes.push({
110
- path: routePath,
111
- source: `pages/${file}`,
112
- public: !isInternalRoute(routePath)
113
- });
114
- }
115
- }
38
+ const astRoutes = extractRoutesAST(projectDir, program);
39
+
40
+ // Convert AST routes to manifest format and sort for determinism
41
+ const routes = astRoutes.map(r => ({
42
+ path: r.path,
43
+ source: r.sourceRef || r.file || 'unknown',
44
+ public: r.public !== undefined ? r.public : !isInternalRoute(r.path),
45
+ sourceRef: r.sourceRef
46
+ }));
47
+ routes.sort((a, b) => a.path.localeCompare(b.path));
48
+ return routes;
116
49
  }
117
50
 
118
- routes.sort((a, b) => a.path.localeCompare(b.path));
119
-
120
- return routes;
51
+ return [];
121
52
  }
122
53