@veraxhq/verax 0.1.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 (135) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +24 -36
  4. package/src/cli/commands/default.js +681 -0
  5. package/src/cli/commands/doctor.js +197 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +586 -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 +297 -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 +110 -0
  14. package/src/cli/util/expectation-extractor.js +388 -0
  15. package/src/cli/util/findings-writer.js +32 -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 +412 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +30 -0
  21. package/src/cli/util/project-discovery.js +297 -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/runtime-budget.js +147 -0
  26. package/src/cli/util/summary-writer.js +43 -0
  27. package/src/types/global.d.ts +28 -0
  28. package/src/types/ts-ast.d.ts +24 -0
  29. package/src/verax/cli/ci-summary.js +35 -0
  30. package/src/verax/cli/context-explanation.js +89 -0
  31. package/src/verax/cli/doctor.js +277 -0
  32. package/src/verax/cli/error-normalizer.js +154 -0
  33. package/src/verax/cli/explain-output.js +105 -0
  34. package/src/verax/cli/finding-explainer.js +130 -0
  35. package/src/verax/cli/init.js +237 -0
  36. package/src/verax/cli/run-overview.js +163 -0
  37. package/src/verax/cli/url-safety.js +111 -0
  38. package/src/verax/cli/wizard.js +109 -0
  39. package/src/verax/cli/zero-findings-explainer.js +57 -0
  40. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  41. package/src/verax/core/action-classifier.js +86 -0
  42. package/src/verax/core/budget-engine.js +218 -0
  43. package/src/verax/core/canonical-outcomes.js +157 -0
  44. package/src/verax/core/decision-snapshot.js +335 -0
  45. package/src/verax/core/determinism-model.js +432 -0
  46. package/src/verax/core/incremental-store.js +245 -0
  47. package/src/verax/core/invariants.js +356 -0
  48. package/src/verax/core/promise-model.js +230 -0
  49. package/src/verax/core/replay-validator.js +350 -0
  50. package/src/verax/core/replay.js +222 -0
  51. package/src/verax/core/run-id.js +175 -0
  52. package/src/verax/core/run-manifest.js +99 -0
  53. package/src/verax/core/silence-impact.js +369 -0
  54. package/src/verax/core/silence-model.js +523 -0
  55. package/src/verax/detect/comparison.js +7 -34
  56. package/src/verax/detect/confidence-engine.js +764 -329
  57. package/src/verax/detect/detection-engine.js +293 -0
  58. package/src/verax/detect/evidence-index.js +127 -0
  59. package/src/verax/detect/expectation-model.js +241 -168
  60. package/src/verax/detect/explanation-helpers.js +187 -0
  61. package/src/verax/detect/finding-detector.js +450 -0
  62. package/src/verax/detect/findings-writer.js +41 -12
  63. package/src/verax/detect/flow-detector.js +366 -0
  64. package/src/verax/detect/index.js +200 -288
  65. package/src/verax/detect/interactive-findings.js +612 -0
  66. package/src/verax/detect/signal-mapper.js +308 -0
  67. package/src/verax/detect/skip-classifier.js +4 -4
  68. package/src/verax/detect/verdict-engine.js +561 -0
  69. package/src/verax/evidence-index-writer.js +61 -0
  70. package/src/verax/flow/flow-engine.js +3 -2
  71. package/src/verax/flow/flow-spec.js +1 -2
  72. package/src/verax/index.js +103 -15
  73. package/src/verax/intel/effect-detector.js +368 -0
  74. package/src/verax/intel/handler-mapper.js +249 -0
  75. package/src/verax/intel/index.js +281 -0
  76. package/src/verax/intel/route-extractor.js +280 -0
  77. package/src/verax/intel/ts-program.js +256 -0
  78. package/src/verax/intel/vue-navigation-extractor.js +642 -0
  79. package/src/verax/intel/vue-router-extractor.js +325 -0
  80. package/src/verax/learn/action-contract-extractor.js +338 -104
  81. package/src/verax/learn/ast-contract-extractor.js +148 -6
  82. package/src/verax/learn/flow-extractor.js +172 -0
  83. package/src/verax/learn/index.js +36 -2
  84. package/src/verax/learn/manifest-writer.js +122 -58
  85. package/src/verax/learn/project-detector.js +40 -0
  86. package/src/verax/learn/route-extractor.js +28 -97
  87. package/src/verax/learn/route-validator.js +8 -7
  88. package/src/verax/learn/state-extractor.js +212 -0
  89. package/src/verax/learn/static-extractor-navigation.js +114 -0
  90. package/src/verax/learn/static-extractor-validation.js +88 -0
  91. package/src/verax/learn/static-extractor.js +119 -10
  92. package/src/verax/learn/truth-assessor.js +24 -21
  93. package/src/verax/learn/ts-contract-resolver.js +14 -12
  94. package/src/verax/observe/aria-sensor.js +211 -0
  95. package/src/verax/observe/browser.js +30 -6
  96. package/src/verax/observe/console-sensor.js +2 -18
  97. package/src/verax/observe/domain-boundary.js +10 -1
  98. package/src/verax/observe/expectation-executor.js +513 -0
  99. package/src/verax/observe/flow-matcher.js +143 -0
  100. package/src/verax/observe/focus-sensor.js +196 -0
  101. package/src/verax/observe/human-driver.js +660 -273
  102. package/src/verax/observe/index.js +910 -26
  103. package/src/verax/observe/interaction-discovery.js +378 -15
  104. package/src/verax/observe/interaction-runner.js +562 -197
  105. package/src/verax/observe/loading-sensor.js +145 -0
  106. package/src/verax/observe/navigation-sensor.js +255 -0
  107. package/src/verax/observe/network-sensor.js +55 -7
  108. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  109. package/src/verax/observe/observed-expectation.js +305 -0
  110. package/src/verax/observe/page-frontier.js +234 -0
  111. package/src/verax/observe/settle.js +38 -17
  112. package/src/verax/observe/state-sensor.js +393 -0
  113. package/src/verax/observe/state-ui-sensor.js +7 -1
  114. package/src/verax/observe/timing-sensor.js +228 -0
  115. package/src/verax/observe/traces-writer.js +73 -21
  116. package/src/verax/observe/ui-signal-sensor.js +143 -17
  117. package/src/verax/scan-summary-writer.js +80 -15
  118. package/src/verax/shared/artifact-manager.js +111 -9
  119. package/src/verax/shared/budget-profiles.js +136 -0
  120. package/src/verax/shared/caching.js +1 -1
  121. package/src/verax/shared/ci-detection.js +39 -0
  122. package/src/verax/shared/config-loader.js +169 -0
  123. package/src/verax/shared/dynamic-route-utils.js +224 -0
  124. package/src/verax/shared/expectation-coverage.js +44 -0
  125. package/src/verax/shared/expectation-prover.js +81 -0
  126. package/src/verax/shared/expectation-tracker.js +201 -0
  127. package/src/verax/shared/expectations-writer.js +60 -0
  128. package/src/verax/shared/first-run.js +44 -0
  129. package/src/verax/shared/progress-reporter.js +171 -0
  130. package/src/verax/shared/retry-policy.js +9 -1
  131. package/src/verax/shared/root-artifacts.js +49 -0
  132. package/src/verax/shared/scan-budget.js +86 -0
  133. package/src/verax/shared/url-normalizer.js +162 -0
  134. package/src/verax/shared/zip-artifacts.js +66 -0
  135. package/src/verax/validate/context-validator.js +244 -0
@@ -4,6 +4,7 @@ import { readFileSync } from 'fs';
4
4
  import { glob } from 'glob';
5
5
  import { resolve } from 'path';
6
6
  import { ExpectationProof } from '../shared/expectation-proof.js';
7
+ import { normalizeTemplateLiteral } from '../shared/dynamic-route-utils.js';
7
8
 
8
9
  const MAX_FILES_TO_SCAN = 200;
9
10
 
@@ -20,6 +21,11 @@ function extractStaticStringValue(node) {
20
21
  if (node.type === 'StringLiteral') {
21
22
  return node.value;
22
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
+ }
23
29
 
24
30
  // JSX expression: href={'/about'} or href={`/about`}
25
31
  if (node.type === 'JSXExpressionContainer') {
@@ -45,18 +51,89 @@ function extractStaticStringValue(node) {
45
51
  return null;
46
52
  }
47
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
+
70
+ /**
71
+ * Extracts template literal pattern from Babel TemplateLiteral node.
72
+ * Returns null if template has complex expressions that cannot be normalized.
73
+ *
74
+ * Examples:
75
+ * - `${id}` → { pattern: '${id}', examplePath: '/1' }
76
+ * - `/users/${id}` → { pattern: '/users/${id}', examplePath: '/users/1' }
77
+ * - `/posts/${slug}` → { pattern: '/posts/${slug}', examplePath: '/posts/example' }
78
+ */
79
+ function extractTemplatePattern(templateNode) {
80
+ if (!templateNode || templateNode.type !== 'TemplateLiteral') {
81
+ return null;
82
+ }
83
+
84
+ // Build the template string with ${} placeholders
85
+ let templateStr = templateNode.quasis[0]?.value?.cooked || '';
86
+
87
+ for (let i = 0; i < templateNode.expressions.length; i++) {
88
+ const expr = templateNode.expressions[i];
89
+
90
+ // Only support simple identifiers: ${id}, ${slug}
91
+ if (expr.type === 'Identifier') {
92
+ templateStr += '${' + expr.name + '}';
93
+ } else {
94
+ // Complex expressions like function calls - cannot normalize
95
+ return null;
96
+ }
97
+
98
+ // Add the next quasi
99
+ if (templateNode.quasis[i + 1]) {
100
+ templateStr += templateNode.quasis[i + 1].value.cooked || '';
101
+ }
102
+ }
103
+
104
+ // Must be a valid route pattern (start with /)
105
+ if (!templateStr.startsWith('/')) {
106
+ return null;
107
+ }
108
+
109
+ // Normalize to example path
110
+ const normalized = normalizeTemplateLiteral(templateStr);
111
+ if (normalized) {
112
+ return {
113
+ pattern: templateStr,
114
+ examplePath: normalized.examplePath,
115
+ isDynamic: true,
116
+ originalPattern: normalized.originalPattern
117
+ };
118
+ }
119
+
120
+ return null;
121
+ }
122
+
48
123
  /**
49
124
  * Extracts PROVEN navigation contracts from JSX elements and imperative calls.
50
125
  *
51
- * Supported patterns (all require static string literals):
52
- * - Next.js: <Link href="/about">
53
- * - React Router: <Link to="/about"> or <NavLink to="/about">
126
+ * Supported patterns (all require static string literals or template patterns):
127
+ * - Next.js: <Link href="/about"> or <Link href={`/about`}>
128
+ * - React Router: <Link to="/about"> or <RouterLink :to="`/users/${id}`">
54
129
  * - Plain JSX: <a href="/about">
55
- * - Imperative: navigate("/about"), router.push("/about")
130
+ * - Imperative: navigate("/about"), router.push("/about"), router.push(`/users/${id}`)
56
131
  *
57
132
  * Returns array of contracts with:
58
133
  * - kind: 'NAVIGATION'
59
- * - targetPath: string
134
+ * - targetPath: string (example path for dynamic routes)
135
+ * - originalPattern: string (original pattern for dynamic routes)
136
+ * - isDynamic: boolean (true if dynamic route)
60
137
  * - sourceFile: string
61
138
  * - element: 'Link' | 'NavLink' | 'a' | 'navigate' | 'router'
62
139
  * - attribute: 'href' | 'to' (for runtime matching)
@@ -120,6 +197,37 @@ function extractContractsFromFile(filePath, fileContent) {
120
197
  // since we cannot reliably tie them to clicked elements
121
198
  CallExpression(path) {
122
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
+ }
123
231
 
124
232
  // navigate("/about") - useNavigate hook
125
233
  if (callee.type === 'Identifier' && callee.name === 'navigate') {
@@ -138,6 +246,23 @@ function extractContractsFromFile(filePath, fileContent) {
138
246
  imperativeOnly: true // Cannot match to DOM element
139
247
  });
140
248
  }
249
+ } else if (firstArg && firstArg.type === 'TemplateLiteral') {
250
+ // Template literal: navigate(`/users/${id}`)
251
+ const templatePattern = extractTemplatePattern(firstArg);
252
+ if (templatePattern) {
253
+ contracts.push({
254
+ kind: 'NAVIGATION',
255
+ targetPath: templatePattern.examplePath,
256
+ originalPattern: templatePattern.pattern,
257
+ isDynamic: true,
258
+ sourceFile: filePath,
259
+ element: 'navigate',
260
+ attribute: null,
261
+ proof: ExpectationProof.PROVEN_EXPECTATION,
262
+ line: path.node.loc?.start.line || null,
263
+ imperativeOnly: true // Cannot match to DOM element
264
+ });
265
+ }
141
266
  }
142
267
  }
143
268
 
@@ -162,6 +287,23 @@ function extractContractsFromFile(filePath, fileContent) {
162
287
  imperativeOnly: true // Cannot match to DOM element
163
288
  });
164
289
  }
290
+ } else if (firstArg && firstArg.type === 'TemplateLiteral') {
291
+ // Template literal: router.push(`/users/${id}`)
292
+ const templatePattern = extractTemplatePattern(firstArg);
293
+ if (templatePattern) {
294
+ contracts.push({
295
+ kind: 'NAVIGATION',
296
+ targetPath: templatePattern.examplePath,
297
+ originalPattern: templatePattern.pattern,
298
+ isDynamic: true,
299
+ sourceFile: filePath,
300
+ element: 'router',
301
+ attribute: null,
302
+ proof: ExpectationProof.PROVEN_EXPECTATION,
303
+ line: path.node.loc?.start.line || null,
304
+ imperativeOnly: true // Cannot match to DOM element
305
+ });
306
+ }
165
307
  }
166
308
  }
167
309
  }
@@ -218,7 +360,7 @@ export async function extractASTContracts(projectDir) {
218
360
  * Converts AST contracts to manifest expectations format.
219
361
  * Only includes contracts that can be matched at runtime (excludes imperativeOnly).
220
362
  */
221
- export function contractsToExpectations(contracts, projectType) {
363
+ export function contractsToExpectations(contracts, _projectType) {
222
364
  const expectations = [];
223
365
  const seenPaths = new Set();
224
366
 
@@ -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
+ }
@@ -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,12 +1,35 @@
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
- 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';
4
+ import { runCodeIntelligence } from '../intel/index.js';
5
+ import { isProvenExpectation } from '../shared/expectation-prover.js';
6
+ import { extractFlows } from './flow-extractor.js';
7
+ import { createTSProgram } from '../intel/ts-program.js';
8
+ import { extractVueNavigationPromises } from '../intel/vue-navigation-extractor.js';
9
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
+ */
10
33
  export async function writeManifest(projectDir, projectType, routes) {
11
34
  const publicRoutes = routes.filter(r => r.public).map(r => r.path);
12
35
  const internalRoutes = routes.filter(r => !r.public).map(r => r.path);
@@ -19,7 +42,8 @@ export async function writeManifest(projectDir, projectType, routes) {
19
42
  routes: routes.map(r => ({
20
43
  path: r.path,
21
44
  source: r.source,
22
- public: r.public
45
+ public: r.public,
46
+ sourceRef: r.sourceRef || null
23
47
  })),
24
48
  publicRoutes: publicRoutes,
25
49
  internalRoutes: internalRoutes,
@@ -27,71 +51,111 @@ export async function writeManifest(projectDir, projectType, routes) {
27
51
  };
28
52
 
29
53
  let staticExpectations = null;
30
- let spaExpectations = null;
31
- let actionContracts = null;
54
+ let intelExpectations = [];
55
+ let allExpectations = [];
32
56
 
57
+ // Static sites: extract from HTML
33
58
  if (projectType === 'static' && routes.length > 0) {
34
59
  staticExpectations = await extractStaticExpectations(projectDir, routes);
35
60
  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);
61
+ allExpectations = staticExpectations || [];
62
+ }
63
+
64
+ // Vue projects: extract navigation promises from code
65
+ if (projectType === 'vue_router' || projectType === 'vue_spa') {
66
+ const program = createTSProgram(projectDir, { includeJs: true });
41
67
 
42
- if (spaExpectations.length > 0) {
43
- manifest.spaExpectations = spaExpectations;
44
- manifest.expectationProof = ExpectationProof.PROVEN_EXPECTATION;
45
- } else {
46
- manifest.expectationProof = ExpectationProof.UNKNOWN_EXPECTATION;
68
+ if (!program.error) {
69
+ const vueNavPromises = await extractVueNavigationPromises(program, projectDir);
70
+
71
+ if (vueNavPromises && vueNavPromises.length > 0) {
72
+ intelExpectations = vueNavPromises.filter(exp => isProvenExpectation(exp));
73
+
74
+ if (!manifest.staticExpectations) {
75
+ manifest.staticExpectations = [];
76
+ }
77
+ manifest.staticExpectations.push(...intelExpectations);
78
+ allExpectations = intelExpectations;
79
+ }
47
80
  }
48
- } else {
49
- manifest.expectationProof = ExpectationProof.UNKNOWN_EXPECTATION;
50
81
  }
51
82
 
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;
83
+ // React SPAs, Next.js, and Vue: use code intelligence (AST-based) - NO FALLBACKS
84
+ if (projectType === 'react_spa' ||
85
+ projectType === 'nextjs_app_router' ||
86
+ projectType === 'nextjs_pages_router' ||
87
+ projectType === 'vue_router' ||
88
+ projectType === 'vue_spa') {
89
+ const intelResult = await runCodeIntelligence(projectDir);
90
+
91
+ if (!intelResult.error && intelResult.expectations) {
92
+ // ZERO-HEURISTIC: Only include expectations that pass isProvenExpectation()
93
+ intelExpectations = intelResult.expectations.filter(exp => isProvenExpectation(exp));
94
+
95
+ // STATE INTELLIGENCE: Also extract state expectations from AST
96
+ const { extractStateExpectationsFromAST } = await import('./state-extractor.js');
97
+ const stateResult = await extractStateExpectationsFromAST(projectDir);
98
+
99
+ if (stateResult.expectations && stateResult.expectations.length > 0) {
100
+ // Convert state expectations to manifest format with sourceRef
101
+ const stateExpectations = stateResult.expectations.map(exp => ({
102
+ type: 'state_action',
103
+ expectedTarget: exp.expectedTarget,
104
+ storeType: exp.storeType,
105
+ proof: 'PROVEN_EXPECTATION',
106
+ sourceRef: exp.line ? `${exp.sourceFile}:${exp.line}` : exp.sourceFile,
107
+ selectorHint: null, // State actions don't have selector hints from AST
108
+ metadata: {
109
+ sourceFile: exp.sourceFile,
110
+ line: exp.line
111
+ }
112
+ })).filter(exp => isProvenExpectation(exp));
113
+
114
+ intelExpectations.push(...stateExpectations);
115
+ }
116
+
117
+ // Add intel expectations to manifest
118
+ if (!manifest.staticExpectations) {
119
+ manifest.staticExpectations = [];
120
+ }
121
+ manifest.staticExpectations.push(...intelExpectations);
122
+ allExpectations = intelExpectations;
63
123
  }
64
124
  }
65
-
66
- const learnTruth = await assessLearnTruth(projectDir, projectType, routes, staticExpectations, spaExpectations);
125
+
126
+ // ZERO-HEURISTIC: Set expectationsStatus and coverageGaps
127
+ const provenCount = allExpectations.filter(exp => isProvenExpectation(exp)).length;
128
+
129
+ if (provenCount === 0 && (projectType === 'react_spa' ||
130
+ projectType === 'nextjs_app_router' ||
131
+ projectType === 'nextjs_pages_router' ||
132
+ projectType === 'vue_router' ||
133
+ projectType === 'vue_spa')) {
134
+ manifest.expectationsStatus = 'NO_PROVEN_EXPECTATIONS';
135
+ manifest.coverageGaps = [{
136
+ reason: 'NO_PROVEN_EXPECTATIONS_AVAILABLE',
137
+ message: 'Code intelligence found no extractable literal effects (navigation, network, validation) with static string literals',
138
+ projectType: projectType
139
+ }];
140
+ } else {
141
+ manifest.expectationsStatus = 'PROVEN_EXPECTATIONS_AVAILABLE';
142
+ manifest.coverageGaps = [];
143
+ }
144
+
145
+ // FLOW INTELLIGENCE v1: Extract flows from PROVEN expectations
146
+ const flows = extractFlows(manifest);
147
+ if (flows.length > 0) {
148
+ manifest.flows = flows;
149
+ }
150
+
151
+ const learnTruth = await assessLearnTruth(projectDir, projectType, routes, allExpectations);
67
152
  manifest.notes.push({
68
153
  type: 'truth',
69
154
  learn: learnTruth
70
155
  });
71
156
 
72
- const manifestDir = resolve(projectDir, '.verax', 'learn');
73
- mkdirSync(manifestDir, { recursive: true });
74
-
75
- const manifestPath = resolve(manifestDir, 'site-manifest.json');
76
- writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
77
-
78
- return {
79
- ...manifest,
80
- manifestPath: manifestPath,
81
- learnTruth: learnTruth,
82
- actionContracts
83
- };
84
- }
85
-
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;
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;
96
160
  }
97
161