@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
@@ -0,0 +1,249 @@
1
+ /**
2
+ * CODE INTELLIGENCE v1 — JSX Handler Mapper
3
+ *
4
+ * Maps JSX event handlers (onClick, onSubmit) to function declarations.
5
+ * Resolves handler identifiers to actual function bodies.
6
+ * Captures sourceRef for both element and handler.
7
+ */
8
+
9
+ import ts from 'typescript';
10
+ import { parseFile, findNodes, resolveIdentifier, isFunctionNode, getNodeLocation } from './ts-program.js';
11
+
12
+ /**
13
+ * Extract handler mappings from JSX elements.
14
+ *
15
+ * @param {string} projectRoot - Project root
16
+ * @param {Object} program - TypeScript program
17
+ * @returns {Array} - Handler mappings { element, handlerName, handlerNode, elementSourceRef, handlerSourceRef }
18
+ */
19
+ export function extractHandlerMappings(projectRoot, program) {
20
+ const mappings = [];
21
+
22
+ if (!program || !program.program || !program.typeChecker) return mappings;
23
+
24
+ for (const filePath of program.sourceFiles) {
25
+ const ast = parseFile(filePath, true);
26
+ if (!ast) continue;
27
+
28
+ // Find JSX elements with event handlers
29
+ const jsxElements = findNodes(ast, node => {
30
+ return ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node);
31
+ });
32
+
33
+ for (const element of jsxElements) {
34
+ const tagName = getTagName(element);
35
+ const attributes = element.attributes;
36
+ if (!attributes || !attributes.properties) continue;
37
+
38
+ for (const attr of attributes.properties) {
39
+ if (!ts.isJsxAttribute(attr)) continue;
40
+
41
+ const attrName = attr.name;
42
+ if (!ts.isIdentifier(attrName)) continue;
43
+
44
+ const eventType = attrName.text;
45
+ if (!['onClick', 'onSubmit', 'onChange', 'onBlur'].includes(eventType)) continue;
46
+
47
+ const initializer = attr.initializer;
48
+ if (!initializer) continue;
49
+
50
+ let handlerNode = null;
51
+ let handlerName = null;
52
+
53
+ // JsxExpression: onClick={handleClick}
54
+ if (ts.isJsxExpression(initializer)) {
55
+ const expr = initializer.expression;
56
+
57
+ if (ts.isIdentifier(expr)) {
58
+ // Reference to function: onClick={handleClick}
59
+ handlerName = expr.text;
60
+ const declaration = resolveIdentifier(program.typeChecker, expr);
61
+ if (declaration && isFunctionNode(declaration)) {
62
+ handlerNode = declaration;
63
+ } else if (declaration && ts.isVariableDeclaration(declaration)) {
64
+ // const handleClick = () => {...}
65
+ const init = declaration.initializer;
66
+ if (init && isFunctionNode(init)) {
67
+ handlerNode = init;
68
+ }
69
+ } else {
70
+ // Fallback for JS/JSX where typeChecker cannot resolve identifiers.
71
+ // Search the file for a matching function/variable declaration manually.
72
+ const localMatch = findNodes(ast, n => {
73
+ if (ts.isFunctionDeclaration(n) && n.name?.text === handlerName) return true;
74
+ if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name) && n.name.text === handlerName) {
75
+ return isFunctionNode(n.initializer || n);
76
+ }
77
+ return false;
78
+ })[0];
79
+
80
+ if (localMatch) {
81
+ if (isFunctionNode(localMatch)) {
82
+ handlerNode = localMatch;
83
+ } else if (ts.isVariableDeclaration(localMatch)) {
84
+ const init = localMatch.initializer;
85
+ if (init && isFunctionNode(init)) {
86
+ handlerNode = init;
87
+ }
88
+ }
89
+ }
90
+ }
91
+ } else if (isFunctionNode(expr)) {
92
+ // Inline arrow: onClick={() => {...}}
93
+ handlerNode = expr;
94
+ handlerName = 'inline';
95
+ }
96
+ }
97
+
98
+ if (handlerNode) {
99
+ const elementLoc = getNodeLocation(ast, element, projectRoot);
100
+ const handlerSourceFile = program.program.getSourceFile(handlerNode.getSourceFile().fileName);
101
+ const handlerLoc = handlerSourceFile
102
+ ? getNodeLocation(handlerSourceFile, handlerNode, projectRoot)
103
+ : null;
104
+
105
+ // Capture simple attribute map for matching hints (e.g., href/to)
106
+ const attrMap = {};
107
+ for (const ap of attributes.properties) {
108
+ if (!ts.isJsxAttribute(ap)) continue;
109
+ if (!ts.isIdentifier(ap.name)) continue;
110
+ const aname = ap.name.text;
111
+ const init = ap.initializer;
112
+ if (!init) continue;
113
+ if (ts.isStringLiteral(init)) {
114
+ attrMap[aname] = init.text;
115
+ } else if (ts.isJsxExpression(init) && init.expression && ts.isStringLiteral(init.expression)) {
116
+ attrMap[aname] = init.expression.text;
117
+ }
118
+ }
119
+
120
+ mappings.push({
121
+ element: {
122
+ tag: tagName,
123
+ event: eventType,
124
+ attrs: attrMap,
125
+ sourceRef: elementLoc.sourceRef,
126
+ file: elementLoc.file,
127
+ line: elementLoc.line
128
+ },
129
+ handler: {
130
+ name: handlerName,
131
+ node: handlerNode,
132
+ sourceRef: handlerLoc?.sourceRef || elementLoc.sourceRef,
133
+ file: handlerLoc?.file || elementLoc.file,
134
+ line: handlerLoc?.line || elementLoc.line
135
+ }
136
+ });
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ return mappings;
143
+ }
144
+
145
+ /**
146
+ * Extract navigation-capable JSX elements (Link/NavLink/a) with static href/to.
147
+ * Returns minimal element records with attrs and sourceRef, independent of handlers.
148
+ */
149
+ export function extractNavElements(projectRoot, program) {
150
+ const elements = [];
151
+ if (!program || !program.program || !program.typeChecker) return elements;
152
+ for (const filePath of program.sourceFiles) {
153
+ const ast = parseFile(filePath, true);
154
+ if (!ast) continue;
155
+ const jsxElements = findNodes(ast, node => ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node));
156
+ for (const element of jsxElements) {
157
+ const tagName = getTagName(element);
158
+ if (!['Link', 'NavLink', 'a'].includes(tagName)) continue;
159
+ const attributes = element.attributes;
160
+ if (!attributes || !attributes.properties) continue;
161
+ let href = null, to = null;
162
+ for (const attr of attributes.properties) {
163
+ if (!ts.isJsxAttribute(attr)) continue;
164
+ if (!ts.isIdentifier(attr.name)) continue;
165
+ const an = attr.name.text;
166
+ const init = attr.initializer;
167
+ if (!init) continue;
168
+ if (an === 'href' || an === 'to') {
169
+ if (ts.isStringLiteral(init)) {
170
+ if (an === 'href') href = init.text; else to = init.text;
171
+ } else if (ts.isJsxExpression(init) && init.expression && ts.isStringLiteral(init.expression)) {
172
+ if (an === 'href') href = init.expression.text; else to = init.expression.text;
173
+ } else if (ts.isJsxExpression(init) && init.expression && ts.isNoSubstitutionTemplateLiteral(init.expression)) {
174
+ if (an === 'href') href = init.expression.text; else to = init.expression.text;
175
+ }
176
+ }
177
+ }
178
+ const target = to || href;
179
+ if (typeof target === 'string' && target.startsWith('/') && !target.startsWith('//')) {
180
+ const loc = getNodeLocation(ast, element, projectRoot);
181
+ elements.push({
182
+ tag: tagName,
183
+ attrs: { href, to },
184
+ sourceRef: loc.sourceRef,
185
+ file: loc.file,
186
+ line: loc.line
187
+ });
188
+ }
189
+ }
190
+ }
191
+ return elements;
192
+ }
193
+
194
+ /**
195
+ * Get tag name from JSX element.
196
+ *
197
+ * @param {ts.Node} element - JSX element node
198
+ * @returns {string} - Tag name
199
+ */
200
+ function getTagName(element) {
201
+ const tagName = element.tagName;
202
+ if (ts.isIdentifier(tagName)) {
203
+ return tagName.text;
204
+ }
205
+ return 'unknown';
206
+ }
207
+
208
+ /**
209
+ * Extract selector hint from JSX element for stable identification.
210
+ *
211
+ * @param {ts.Node} element - JSX element node
212
+ * @returns {string|null} - Selector hint (id, data-testid, or null)
213
+ */
214
+ export function extractSelectorHint(element) {
215
+ const attributes = element.attributes;
216
+ if (!attributes || !attributes.properties) return null;
217
+
218
+ // Priority: id > data-testid > data-cy > role
219
+ for (const attr of attributes.properties) {
220
+ if (!ts.isJsxAttribute(attr)) continue;
221
+
222
+ const name = attr.name;
223
+ if (!ts.isIdentifier(name)) continue;
224
+
225
+ const attrName = name.text;
226
+ const initializer = attr.initializer;
227
+
228
+ if (attrName === 'id' && initializer && ts.isStringLiteral(initializer)) {
229
+ return `#${initializer.text}`;
230
+ }
231
+
232
+ if (attrName === 'data-testid' && initializer) {
233
+ if (ts.isStringLiteral(initializer)) {
234
+ return `[data-testid="${initializer.text}"]`;
235
+ } else if (ts.isJsxExpression(initializer)) {
236
+ const expr = initializer.expression;
237
+ if (expr && ts.isStringLiteral(expr)) {
238
+ return `[data-testid="${expr.text}"]`;
239
+ }
240
+ }
241
+ }
242
+
243
+ if (attrName === 'data-cy' && initializer && ts.isStringLiteral(initializer)) {
244
+ return `[data-cy="${initializer.text}"]`;
245
+ }
246
+ }
247
+
248
+ return null;
249
+ }
@@ -0,0 +1,281 @@
1
+ /**
2
+ * CODE INTELLIGENCE v1 — Main Orchestrator
3
+ *
4
+ * Combines all intelligence modules to emit PROVEN expectations:
5
+ * - Routes from AST
6
+ * - Handlers from JSX
7
+ * - Effects from function bodies
8
+ *
9
+ * Every expectation has sourceRef. NO GUESSING.
10
+ */
11
+
12
+ import { createTSProgram } from './ts-program.js';
13
+ import { extractRoutes } from './route-extractor.js';
14
+ import { extractHandlerMappings, extractSelectorHint, extractNavElements } from './handler-mapper.js';
15
+ import { detectEffects } from './effect-detector.js';
16
+ import { extractVueNavigationPromises } from './vue-navigation-extractor.js';
17
+ import { normalizeDynamicRoute, normalizeTemplateLiteral } from '../shared/dynamic-route-utils.js';
18
+
19
+ /**
20
+ * Run code intelligence analysis.
21
+ *
22
+ * @param {string} projectRoot - Project root
23
+ * @returns {Promise<Object>} - { routes, handlerMappings, expectations, stats }
24
+ */
25
+ export async function runCodeIntelligence(projectRoot) {
26
+ const result = {
27
+ routes: [],
28
+ handlerMappings: [],
29
+ expectations: [],
30
+ stats: {
31
+ routesFound: 0,
32
+ handlersFound: 0,
33
+ effectsFound: 0,
34
+ expectationsGenerated: 0,
35
+ expectationsProven: 0
36
+ },
37
+ error: null
38
+ };
39
+
40
+ try {
41
+ // Step 1: Create TypeScript Program
42
+ const program = createTSProgram(projectRoot, { includeJs: true });
43
+
44
+ if (program.error) {
45
+ result.error = program.error;
46
+ return result;
47
+ }
48
+
49
+ // Step 2: Extract routes (AST-based, no regex)
50
+ const routes = extractRoutes(projectRoot, program);
51
+ result.routes = routes;
52
+ result.stats.routesFound = routes.length;
53
+
54
+ // Step 3: Extract handler mappings (JSX → Function)
55
+ const handlerMappings = extractHandlerMappings(projectRoot, program);
56
+ result.handlerMappings = handlerMappings;
57
+ result.stats.handlersFound = handlerMappings.length;
58
+
59
+ // Step 4: For each handler, detect effects
60
+ for (const mapping of handlerMappings) {
61
+ const handlerSourceFile = mapping.handler.node.getSourceFile();
62
+ // VALIDATION INTELLIGENCE v1: Pass eventType to detectEffects
63
+ const eventType = mapping.element?.event || null;
64
+ const effects = detectEffects(
65
+ mapping.handler.node,
66
+ handlerSourceFile,
67
+ projectRoot,
68
+ eventType
69
+ );
70
+
71
+ mapping.effects = effects;
72
+ result.stats.effectsFound += effects.length;
73
+
74
+ // Step 5a: Generate expectations from (element + handler + effect)
75
+ for (const effect of effects) {
76
+ const expectation = generateExpectation(mapping, effect);
77
+ if (expectation) {
78
+ result.expectations.push(expectation);
79
+ result.stats.expectationsGenerated++;
80
+ if (expectation.proof === 'PROVEN_EXPECTATION') {
81
+ result.stats.expectationsProven++;
82
+ }
83
+ }
84
+ }
85
+
86
+ // Step 5b: Attribute-driven navigation expectations (href/to literal)
87
+ if ((effects?.length || 0) === 0) {
88
+ const attrs = mapping.element?.attrs || {};
89
+ const target = attrs.to || attrs.href || null;
90
+ if (target && typeof target === 'string' && target.startsWith('/')) {
91
+ const expectation = {
92
+ type: 'spa_navigation',
93
+ targetPath: target,
94
+ matchAttribute: attrs.to ? 'to' : (attrs.href ? 'href' : null),
95
+ proof: 'PROVEN_EXPECTATION',
96
+ sourceRef: mapping.element.sourceRef,
97
+ selectorHint: extractSelectorHint(mapping.element) || `${mapping.element.tag}`,
98
+ metadata: {
99
+ elementFile: mapping.element.file,
100
+ elementLine: mapping.element.line,
101
+ handlerName: mapping.handler.name,
102
+ handlerFile: mapping.handler.file,
103
+ handlerLine: mapping.handler.line,
104
+ eventType: mapping.element.event
105
+ }
106
+ };
107
+ result.expectations.push(expectation);
108
+ result.stats.expectationsGenerated++;
109
+ result.stats.expectationsProven++;
110
+ }
111
+ }
112
+ }
113
+
114
+ // Step 5c: Generate expectations from plain Link/NavLink/a elements with static href/to
115
+ const navElements = extractNavElements(projectRoot, program);
116
+ for (const el of navElements) {
117
+ const target = el.attrs.to || el.attrs.href || null;
118
+ if (!target) continue;
119
+ const expectation = {
120
+ type: 'spa_navigation',
121
+ targetPath: target,
122
+ matchAttribute: el.attrs.to ? 'to' : (el.attrs.href ? 'href' : null),
123
+ proof: 'PROVEN_EXPECTATION',
124
+ sourceRef: el.sourceRef,
125
+ selectorHint: extractSelectorHint(/** @type {any} */ ({ attributes: { properties: [] } })) || `${el.tag}`,
126
+ metadata: {
127
+ elementFile: el.file,
128
+ elementLine: el.line,
129
+ handlerName: null,
130
+ handlerFile: null,
131
+ handlerLine: null,
132
+ eventType: 'click'
133
+ }
134
+ };
135
+ result.expectations.push(expectation);
136
+ result.stats.expectationsGenerated++;
137
+ result.stats.expectationsProven++;
138
+ }
139
+
140
+ // Step 5d: Extract Vue navigation promises (router-link, RouterLink, router.push/replace)
141
+ const vueNavPromises = await extractVueNavigationPromises(projectRoot, program);
142
+ for (const promise of vueNavPromises) {
143
+ result.expectations.push(promise);
144
+ result.stats.expectationsGenerated++;
145
+ if (promise.proof === 'PROVEN_EXPECTATION') {
146
+ result.stats.expectationsProven++;
147
+ }
148
+ }
149
+
150
+ } catch (err) {
151
+ result.error = err.message || 'Unknown error';
152
+ }
153
+
154
+ return result;
155
+ }
156
+
157
+ /**
158
+ * Generate expectation from handler mapping and effect.
159
+ *
160
+ * @param {Object} mapping - Handler mapping
161
+ * @param {Object} effect - Detected effect
162
+ * @returns {Object|null} - Expectation object
163
+ */
164
+ function generateExpectation(mapping, effect) {
165
+ if (!effect || !effect.type) return null;
166
+
167
+ const selectorHint = extractSelectorHint(mapping.element);
168
+
169
+ const base = {
170
+ selectorHint: selectorHint || `${mapping.element.tag}`,
171
+ proof: 'PROVEN_EXPECTATION',
172
+ sourceRef: effect.sourceRef,
173
+ metadata: {
174
+ elementFile: mapping.element.file,
175
+ elementLine: mapping.element.line,
176
+ handlerName: mapping.handler.name,
177
+ handlerFile: mapping.handler.file,
178
+ handlerLine: mapping.handler.line,
179
+ effectFile: effect.file,
180
+ effectLine: effect.line,
181
+ eventType: mapping.element.event
182
+ }
183
+ };
184
+
185
+ // Navigation → spa_navigation expectation with matchAttribute
186
+ if (effect.type === 'navigation' && effect.target) {
187
+ const matchAttribute = mapping.element?.attrs?.to
188
+ ? 'to'
189
+ : (mapping.element?.attrs?.href ? 'href' : null);
190
+ // Extract navigation method (push/replace/navigate) from effect
191
+ const navMethod = effect.method || 'push'; // Default to push if not specified
192
+
193
+ // Normalize dynamic routes to example paths
194
+ const normalized = normalizeDynamicRoute(effect.target) || normalizeTemplateLiteral(effect.target);
195
+
196
+ if (normalized) {
197
+ return {
198
+ ...base,
199
+ type: 'spa_navigation',
200
+ targetPath: normalized.examplePath,
201
+ originalPattern: normalized.originalPattern,
202
+ isDynamic: true,
203
+ exampleExecution: true,
204
+ matchAttribute,
205
+ expectedTarget: normalized.examplePath,
206
+ navigationMethod: navMethod
207
+ };
208
+ }
209
+
210
+ return {
211
+ ...base,
212
+ type: 'spa_navigation',
213
+ targetPath: effect.target,
214
+ matchAttribute,
215
+ expectedTarget: effect.target,
216
+ navigationMethod: navMethod
217
+ };
218
+ }
219
+
220
+ // Network → network_action expectation
221
+ if (effect.type === 'network') {
222
+ return {
223
+ ...base,
224
+ type: 'network_action',
225
+ method: effect.method || 'GET',
226
+ expectedTarget: effect.target || null,
227
+ urlPath: effect.target || null // Alias for compatibility
228
+ };
229
+ }
230
+
231
+ // VALIDATION INTELLIGENCE v1: validation_block → validation_block expectation
232
+ if (effect.type === 'validation_block') {
233
+ return {
234
+ ...base,
235
+ type: 'validation_block',
236
+ proof: 'PROVEN_EXPECTATION',
237
+ handlerRef: base.metadata.handlerFile ? `${base.metadata.handlerFile}:${base.metadata.handlerLine}` : null
238
+ };
239
+ }
240
+
241
+ // Validation → form submission expectation (legacy)
242
+ if (effect.type === 'validation') {
243
+ return {
244
+ ...base,
245
+ type: 'form_submission',
246
+ expectedTarget: effect.target || null
247
+ };
248
+ }
249
+
250
+ // State → state_action expectation
251
+ if (effect.type === 'state' && effect.target) {
252
+ return {
253
+ ...base,
254
+ type: 'state_action',
255
+ expectedTarget: effect.target,
256
+ storeType: effect.storeType || 'unknown',
257
+ method: effect.method || null
258
+ };
259
+ }
260
+
261
+ return null;
262
+ }
263
+
264
+ /**
265
+ * Determine expectation type from effect.
266
+ *
267
+ * @param {Object} effect - Effect object
268
+ * @returns {string} - Expectation type
269
+ */
270
+ function _determineExpectationType(effect) {
271
+ switch (effect.type) {
272
+ case 'navigation':
273
+ return 'navigation';
274
+ case 'network':
275
+ return 'network_action';
276
+ case 'validation':
277
+ return 'form_submission'; // preventDefault typically in forms
278
+ default:
279
+ return 'unknown';
280
+ }
281
+ }