@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
@@ -1,204 +1,277 @@
1
1
  import { getUrlPath } from './evidence-validator.js';
2
- import { ExpectationProof } from '../shared/expectation-proof.js';
2
+
3
+ function routeMatchesUrl(routePath, url) {
4
+ const urlPath = getUrlPath(url);
5
+ if (!urlPath) return false;
6
+
7
+ const normalizedRoute = routePath.replace(/\/$/, '') || '/';
8
+ const normalizedUrl = urlPath.replace(/\/$/, '') || '/';
9
+
10
+ if (normalizedRoute === normalizedUrl) return true;
11
+
12
+ if (normalizedRoute.includes(':')) {
13
+ const routeParts = normalizedRoute.split('/');
14
+ const urlParts = normalizedUrl.split('/');
15
+
16
+ if (routeParts.length !== urlParts.length) return false;
17
+
18
+ for (let i = 0; i < routeParts.length; i++) {
19
+ if (routeParts[i].startsWith(':')) continue;
20
+ if (routeParts[i] !== urlParts[i]) return false;
21
+ }
22
+ return true;
23
+ }
24
+
25
+ return false;
26
+ }
27
+
28
+ function normalizeExpectationType(expectationType) {
29
+ if (expectationType === 'spa_navigation') {
30
+ return 'navigation';
31
+ }
32
+ return expectationType;
33
+ }
34
+
35
+ /**
36
+ * Normalize selector for comparison.
37
+ * Removes brackets, parentheses, and normalizes whitespace.
38
+ */
39
+ function normalizeSelector(selector) {
40
+ if (!selector) return '';
41
+ return selector.replace(/[[\]()]/g, '').trim();
42
+ }
3
43
 
4
44
  /**
5
- * Extracts href value from interaction selector.
6
- * Used to match SPA expectations by href attribute.
45
+ * Check if two selectors match.
46
+ * Returns true if:
47
+ * - Exact match, OR
48
+ * - One contains the other (after normalization)
7
49
  */
8
- function extractHrefFromSelector(selector) {
9
- if (!selector) return null;
50
+ function selectorsMatch(selector1, selector2) {
51
+ if (!selector1 || !selector2) return false;
10
52
 
11
- // Match href="/about" or href='/about' in selector
12
- const hrefMatch = selector.match(/href=["']([^"']+)["']/);
13
- if (hrefMatch) {
14
- return hrefMatch[1];
15
- }
53
+ const norm1 = normalizeSelector(selector1);
54
+ const norm2 = normalizeSelector(selector2);
16
55
 
17
- return null;
56
+ if (selector1 === selector2) return true;
57
+ if (norm1 === norm2) return true;
58
+ if (selector1.includes(selector2)) return true;
59
+ if (selector2.includes(selector1)) return true;
60
+ if (norm1.includes(norm2)) return true;
61
+ if (norm2.includes(norm1)) return true;
62
+
63
+ return false;
18
64
  }
19
65
 
20
66
  /**
21
- * Returns expectation info for an interaction at a given URL.
22
- * Wave 0: PROVEN expectations from staticExpectations (HTML-derived)
23
- * Wave 1: PROVEN expectations from spaExpectations (AST-derived JSX contracts)
24
- * Wave 5: PROVEN expectations from actionContracts (AST-derived network actions)
25
- * Wave 8: PROVEN expectations from actionContracts (AST-derived state actions)
26
- *
27
- * @returns {{ hasExpectation: boolean, proof: string, expectedTargetPath?: string, expectationType?: string, method?: string, urlPath?: string, stateKind?: string }}
67
+ * Check if interaction type is compatible with expectation type.
28
68
  */
29
- export function getExpectation(manifest, interaction, beforeUrl, attemptMeta = {}) {
30
- // Wave 5/6/8 - ACTION CONTRACTS: Check network and state action expectations
31
- if (manifest.actionContracts && manifest.actionContracts.length > 0) {
32
- // Priority: handlerRef (cross-file) sourceRef (inline)
33
- const handlerMatch = attemptMeta.handlerRef
34
- ? manifest.actionContracts.find(
35
- (contract) => contract.handlerRef === attemptMeta.handlerRef
36
- )
37
- : null;
38
- if (handlerMatch) {
39
- if (handlerMatch.kind === 'NETWORK_ACTION') {
40
- return {
41
- hasExpectation: true,
42
- proof: ExpectationProof.PROVEN_EXPECTATION,
43
- expectationType: 'network_action',
44
- method: handlerMatch.method,
45
- urlPath: handlerMatch.urlPath
46
- };
47
- } else if (handlerMatch.kind === 'STATE_ACTION') {
48
- return {
49
- hasExpectation: true,
50
- proof: ExpectationProof.PROVEN_EXPECTATION,
51
- expectationType: 'state_action',
52
- stateKind: handlerMatch.stateKind
53
- };
54
- }
69
+ function typesCompatible(expectationType, interactionType) {
70
+ const normalizedType = normalizeExpectationType(expectationType);
71
+ if (normalizedType === 'navigation') {
72
+ return interactionType === 'link' || interactionType === 'button';
73
+ }
74
+ if (normalizedType === 'form_submission') {
75
+ return interactionType === 'form';
76
+ }
77
+ // VALIDATION INTELLIGENCE v1: Validation blocks are triggered by forms
78
+ if (normalizedType === 'validation_block') {
79
+ return interactionType === 'form';
80
+ }
81
+ if (normalizedType === 'network_action') {
82
+ return interactionType === 'button' || interactionType === 'form';
83
+ }
84
+ return false;
85
+ }
86
+
87
+ /**
88
+ * Match expectation to interaction.
89
+ * Returns the matched expectation or null.
90
+ */
91
+ export function matchExpectation(expectation, interaction, beforeUrl) {
92
+ const beforePath = getUrlPath(beforeUrl);
93
+ if (!beforePath) return null;
94
+
95
+ const normalizedBefore = beforePath.replace(/\/$/, '') || '/';
96
+ const normalizedFrom = expectation.fromPath.replace(/\/$/, '') || '/';
97
+ const normalizedType = normalizeExpectationType(expectation.type);
98
+
99
+ if (normalizedFrom !== normalizedBefore) return null;
100
+
101
+ if (!typesCompatible(normalizedType, interaction.type)) return null;
102
+
103
+ const selectorHint = expectation.evidence?.selectorHint || '';
104
+ const interactionSelector = interaction.selector || '';
105
+
106
+ if (selectorHint && interactionSelector) {
107
+ if (selectorsMatch(selectorHint, interactionSelector)) {
108
+ return { ...expectation, type: normalizedType };
55
109
  }
110
+ } else if (!selectorHint && !interactionSelector) {
111
+ return { ...expectation, type: normalizedType };
112
+ }
113
+
114
+ return null;
115
+ }
56
116
 
57
- const sourceMatch = attemptMeta.sourceRef
58
- ? manifest.actionContracts.find(
59
- (contract) => contract.source === attemptMeta.sourceRef
60
- )
61
- : null;
62
- if (sourceMatch) {
63
- if (sourceMatch.kind === 'NETWORK_ACTION') {
64
- return {
65
- hasExpectation: true,
66
- proof: ExpectationProof.PROVEN_EXPECTATION,
67
- expectationType: 'network_action',
68
- method: sourceMatch.method,
69
- urlPath: sourceMatch.urlPath
70
- };
71
- } else if (sourceMatch.kind === 'STATE_ACTION') {
72
- return {
73
- hasExpectation: true,
74
- proof: ExpectationProof.PROVEN_EXPECTATION,
75
- expectationType: 'state_action',
76
- stateKind: sourceMatch.stateKind
77
- };
117
+ function _matchesStaticExpectation(expectation, interaction, afterUrl) {
118
+ if (interaction.type !== 'link') return false;
119
+
120
+ const afterPath = getUrlPath(afterUrl);
121
+ if (!afterPath) return false;
122
+
123
+ const normalizedTarget = expectation.targetPath.replace(/\/$/, '') || '/';
124
+ const normalizedAfter = afterPath.replace(/\/$/, '') || '/';
125
+
126
+ if (normalizedAfter === normalizedTarget) {
127
+ return true;
128
+ }
129
+
130
+ const selectorHint = expectation.evidence.selectorHint || '';
131
+ const interactionSelector = interaction.selector || '';
132
+ const _interactionLabel = (interaction.label || '').toLowerCase().trim();
133
+
134
+ if (selectorHint && interactionSelector) {
135
+ if (selectorHint.includes(interactionSelector) || interactionSelector.includes(selectorHint.replace(/[\]()]/g, ''))) {
136
+ return true;
137
+ }
138
+ }
139
+
140
+ return false;
141
+ }
142
+
143
+ export function expectsNavigation(manifest, interaction, beforeUrl) {
144
+ if (manifest.staticExpectations && manifest.staticExpectations.length > 0) {
145
+ for (const expectation of manifest.staticExpectations) {
146
+ const matched = matchExpectation(expectation, interaction, beforeUrl);
147
+ if (matched) {
148
+ return true;
78
149
  }
79
150
  }
80
151
  }
81
152
 
82
- // Wave 1 - CODE TRUTH ENGINE: Check SPA expectations (AST-derived contracts)
83
- if (manifest.spaExpectations && manifest.spaExpectations.length > 0) {
84
- // For link interactions, extract href and match against expectations
85
- if (interaction.type === 'link') {
86
- const href = extractHrefFromSelector(interaction.selector);
87
-
88
- if (href) {
89
- // Normalize href (remove trailing slash, ensure leading slash)
90
- const normalizedHref = (href.startsWith('/') ? href : '/' + href).replace(/\/$/, '') || '/';
91
-
92
- for (const expectation of manifest.spaExpectations) {
93
- if (expectation.proof !== ExpectationProof.PROVEN_EXPECTATION) {
94
- continue;
95
- }
96
-
97
- const normalizedTarget = expectation.targetPath.replace(/\/$/, '') || '/';
98
-
99
- // Match by href attribute value
100
- if (expectation.matchAttribute === 'href' && normalizedHref === normalizedTarget) {
101
- return {
102
- hasExpectation: true,
103
- proof: ExpectationProof.PROVEN_EXPECTATION,
104
- expectedTargetPath: expectation.targetPath,
105
- expectationType: 'spa_navigation'
106
- };
107
- }
153
+ if (manifest.projectType === 'react_spa' && interaction.type === 'link') {
154
+ const beforePath = getUrlPath(beforeUrl);
155
+ if (beforePath) {
156
+ const href = interaction.selector ? interaction.selector.match(/href=["']([^"']+)["']/) : null;
157
+ if (!href) {
158
+ for (const route of (manifest.routes || [])) {
159
+ if (!route.public) continue;
160
+ const routePath = route.path.toLowerCase();
161
+ const routeName = routePath.split('/').pop() || 'home';
162
+ const interactionLabel = (interaction.label || '').toLowerCase().trim();
108
163
 
109
- // Also match 'to' attribute for React Router
110
- if (expectation.matchAttribute === 'to' && normalizedHref === normalizedTarget) {
111
- return {
112
- hasExpectation: true,
113
- proof: ExpectationProof.PROVEN_EXPECTATION,
114
- expectedTargetPath: expectation.targetPath,
115
- expectationType: 'spa_navigation'
116
- };
164
+ if (interactionLabel.includes(routeName) || routeName.includes(interactionLabel)) {
165
+ return true;
117
166
  }
118
167
  }
119
168
  }
120
169
  }
121
170
  }
122
171
 
123
- // Wave 0: Static HTML expectations
124
- if (manifest.staticExpectations && manifest.staticExpectations.length > 0) {
125
- const beforePath = getUrlPath(beforeUrl);
126
- if (beforePath) {
127
- const normalizedBefore = beforePath.replace(/\/$/, '') || '/';
172
+ if (interaction.type === 'link') {
173
+ const label = (interaction.label || '').toLowerCase().trim();
174
+
175
+ for (const route of (manifest.routes || [])) {
176
+ if (!route.public) continue;
177
+
178
+ const routePath = route.path.toLowerCase();
179
+ const routeName = routePath.split('/').pop() || 'home';
128
180
 
129
- for (const expectation of manifest.staticExpectations) {
130
- if (expectation.proof !== ExpectationProof.PROVEN_EXPECTATION) {
131
- continue; // Only consider proven expectations
181
+ if (label.includes(routeName) || routeName.includes(label)) {
182
+ return true;
183
+ }
184
+
185
+ if (routeMatchesUrl(route.path, beforeUrl + route.path)) {
186
+ return true;
187
+ }
188
+ }
189
+ }
190
+
191
+ if (interaction.type === 'button' || interaction.type === 'form') {
192
+ const label = (interaction.label || '').toLowerCase().trim();
193
+
194
+ const navigationKeywords = ['go', 'navigate', 'next', 'continue', 'submit', 'save'];
195
+ const hasNavKeyword = navigationKeywords.some(keyword => label.includes(keyword));
196
+
197
+ if (hasNavKeyword) {
198
+ for (const route of (manifest.routes || [])) {
199
+ if (!route.public) continue;
200
+ if (route.path === '/' && label.includes('home')) {
201
+ return true;
132
202
  }
133
-
134
- if (expectation.type === 'navigation') {
135
- const normalizedFrom = expectation.fromPath.replace(/\/$/, '') || '/';
136
-
137
- if (normalizedFrom === normalizedBefore) {
138
- if (interaction.type === 'link' || interaction.type === 'button') {
139
- const selectorHint = expectation.evidence.selectorHint || '';
140
- const interactionSelector = interaction.selector || '';
141
-
142
- if (selectorHint && interactionSelector) {
143
- const normalizedSelectorHint = selectorHint.replace(/[\[\]()]/g, '');
144
- const normalizedInteractionSelector = interactionSelector.replace(/[\[\]()]/g, '');
145
-
146
- if (selectorHint === interactionSelector ||
147
- selectorHint.includes(interactionSelector) ||
148
- interactionSelector.includes(normalizedSelectorHint) ||
149
- normalizedSelectorHint === normalizedInteractionSelector) {
150
- return {
151
- hasExpectation: true,
152
- proof: ExpectationProof.PROVEN_EXPECTATION,
153
- expectedTargetPath: expectation.targetPath,
154
- expectationType: 'navigation'
155
- };
156
- }
157
- }
158
- }
159
- }
160
- } else if (expectation.type === 'form_submission') {
161
- const normalizedFrom = expectation.fromPath.replace(/\/$/, '') || '/';
162
-
163
- if (normalizedFrom === normalizedBefore && interaction.type === 'form') {
164
- const selectorHint = expectation.evidence.selectorHint || '';
165
- const interactionSelector = interaction.selector || '';
166
-
167
- if (selectorHint && interactionSelector) {
168
- const normalizedSelectorHint = selectorHint.replace(/[\[\]()]/g, '');
169
- const normalizedInteractionSelector = interactionSelector.replace(/[\[\]()]/g, '');
170
-
171
- if (selectorHint === interactionSelector ||
172
- selectorHint.includes(interactionSelector) ||
173
- interactionSelector.includes(normalizedSelectorHint) ||
174
- normalizedSelectorHint === normalizedInteractionSelector) {
175
- return {
176
- hasExpectation: true,
177
- proof: ExpectationProof.PROVEN_EXPECTATION,
178
- expectedTargetPath: expectation.targetPath,
179
- expectationType: 'form_submission'
180
- };
181
- }
182
- }
183
- }
203
+ if (route.path !== '/') {
204
+ return true;
184
205
  }
185
206
  }
186
207
  }
208
+
209
+ for (const route of (manifest.routes || [])) {
210
+ if (!route.public) continue;
211
+ const routePath = route.path.toLowerCase();
212
+ const routeName = routePath.split('/').pop() || 'home';
213
+
214
+ if (routeName === 'home' && (label.includes('home') || label.includes('main') || label.includes('index'))) {
215
+ return true;
216
+ }
217
+
218
+ if (label.includes(routeName) || routeName.includes(label)) {
219
+ return true;
220
+ }
221
+ }
187
222
  }
188
223
 
189
- // No proven expectation found - all heuristic matching removed
190
- return {
191
- hasExpectation: false,
192
- proof: ExpectationProof.UNKNOWN_EXPECTATION
193
- };
224
+ return false;
194
225
  }
195
226
 
196
227
  /**
197
- * Legacy function for backward compatibility.
198
- * Now delegates to getExpectation and returns boolean.
228
+ * Get expectation for interaction from manifest (unified API for static and action contracts)
229
+ * @param {Object} manifest - Project manifest
230
+ * @param {Object} interaction - Interaction object
231
+ * @param {string} beforeUrl - URL before interaction
232
+ * @param {Object} [attemptMeta] - Optional metadata about the interaction attempt
233
+ * @returns {Object} { hasExpectation: boolean, proof: string, ...expectationData }
199
234
  */
200
- export function expectsNavigation(manifest, interaction, beforeUrl) {
201
- const expectation = getExpectation(manifest, interaction, beforeUrl);
202
- return expectation.hasExpectation && expectation.proof === ExpectationProof.PROVEN_EXPECTATION;
235
+ export function getExpectation(manifest, interaction, beforeUrl, attemptMeta = {}) {
236
+ // Check static expectations first (for static sites)
237
+ if (manifest.staticExpectations && manifest.staticExpectations.length > 0) {
238
+ for (const expectation of manifest.staticExpectations) {
239
+ const matched = matchExpectation(expectation, interaction, beforeUrl);
240
+ if (matched) {
241
+ return {
242
+ hasExpectation: true,
243
+ proof: expectation.proof || 'PROVEN_EXPECTATION',
244
+ expectationType: expectation.type,
245
+ expectedTargetPath: expectation.targetPath,
246
+ ...expectation
247
+ };
248
+ }
249
+ }
250
+ }
251
+
252
+ // Check action contracts (for apps with source-level contracts)
253
+ if (manifest.actionContracts && manifest.actionContracts.length > 0) {
254
+ const sourceRef = attemptMeta?.sourceRef;
255
+ if (sourceRef) {
256
+ for (const contract of manifest.actionContracts) {
257
+ if (contract.source === sourceRef) {
258
+ const expectationType = contract.kind === 'NETWORK_ACTION' ? 'network_action' : 'action';
259
+ return {
260
+ hasExpectation: true,
261
+ proof: 'PROVEN_EXPECTATION',
262
+ expectationType,
263
+ method: contract.method,
264
+ urlPath: contract.urlPath,
265
+ ...contract
266
+ };
267
+ }
268
+ }
269
+ }
270
+ }
271
+
272
+ return {
273
+ hasExpectation: false,
274
+ proof: 'UNKNOWN_EXPECTATION'
275
+ };
203
276
  }
204
277
 
@@ -0,0 +1,187 @@
1
+ /**
2
+ * PHASE 9: Reality Confidence & Explanation Layer
3
+ *
4
+ * Helper functions to generate human-readable explanations for findings.
5
+ */
6
+
7
+ /**
8
+ * Generate a human summary - one short sentence describing what the USER experienced.
9
+ * NO technical jargon.
10
+ */
11
+ export function generateHumanSummary(finding, _trace) {
12
+ const findingType = finding.type || '';
13
+ const interaction = finding.interaction || {};
14
+ const label = interaction.label || interaction.text || 'the button';
15
+ const evidence = finding.evidence || {};
16
+
17
+ switch (findingType) {
18
+ case 'navigation_silent_failure':
19
+ if (evidence.urlChanged) {
20
+ return `The ${label} changed the page URL, but the visible content did not change.`;
21
+ }
22
+ return `Clicking ${label} should have navigated to a different page, but nothing happened.`;
23
+
24
+ case 'network_silent_failure':
25
+ return `The ${label} was clicked, but the expected network request was not sent.`;
26
+
27
+ case 'validation_silent_failure':
28
+ return `Submitting the form did not show any validation feedback, even though validation should have occurred.`;
29
+
30
+ case 'state_silent_failure':
31
+ return `The ${label} was clicked, but the expected state change did not happen.`;
32
+
33
+ case 'reality_dead_interaction':
34
+ return `The ${label} was clickable, but nothing happened after clicking it.`;
35
+
36
+ case 'partial_navigation_failure':
37
+ return `The ${label} started navigating, but did not reach the expected destination.`;
38
+
39
+ case 'silent_failure':
40
+ case 'flow_silent_failure':
41
+ return `The ${label} was clicked, but the expected result did not occur.`;
42
+
43
+ case 'observed_break':
44
+ return `The ${label} behaved differently than expected based on previous observations.`;
45
+
46
+ default:
47
+ return `An unexpected issue occurred with ${label}.`;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Generate action hint for a finding.
53
+ * Returns: { recommendedAction: 'FIX' | 'REVIEW' | 'IGNORE', reason, suggestedNextStep }
54
+ */
55
+ export function generateActionHint(finding, confidence) {
56
+ const findingType = finding.type || '';
57
+ // Handle both full confidence object and simple confidence object
58
+ const confidenceLevel = confidence?.level || finding.confidence?.level || 'UNKNOWN';
59
+ const expectationStrength = confidence?.factors?.expectationStrength ||
60
+ finding.confidence?.factors?.expectationStrength ||
61
+ 'UNKNOWN';
62
+
63
+ // Rules:
64
+ // - PROVEN silent_failure (MEDIUM/HIGH) → FIX
65
+ // - reality_dead_interaction → REVIEW
66
+ // - coverage_gap → IGNORE
67
+ // - LOW confidence findings → REVIEW
68
+ // - HIGH confidence findings → FIX
69
+
70
+ if (findingType === 'reality_dead_interaction') {
71
+ return {
72
+ recommendedAction: 'REVIEW',
73
+ reason: 'This is a dead interaction that may be intentional or a design issue',
74
+ suggestedNextStep: 'Check if this button is supposed to do something. If yes, add a handler. If no, remove it or disable it.'
75
+ };
76
+ }
77
+
78
+ // Coverage gaps are informational only
79
+ if (findingType.includes('coverage') || findingType === 'coverage_gap') {
80
+ return {
81
+ recommendedAction: 'IGNORE',
82
+ reason: 'This is a coverage gap, not a user-facing failure',
83
+ suggestedNextStep: 'No action needed - this indicates incomplete test coverage, not a broken feature'
84
+ };
85
+ }
86
+
87
+ // PROVEN expectations with MEDIUM/HIGH confidence → FIX
88
+ if (expectationStrength === 'PROVEN' && (confidenceLevel === 'HIGH' || confidenceLevel === 'MEDIUM')) {
89
+ return {
90
+ recommendedAction: 'FIX',
91
+ reason: 'Your code explicitly promises this behavior, and evidence shows it failed',
92
+ suggestedNextStep: 'Fix the implementation to match what your code promises, or update the code if the promise changed'
93
+ };
94
+ }
95
+
96
+ // HIGH confidence (even if not PROVEN) → FIX
97
+ if (confidenceLevel === 'HIGH') {
98
+ return {
99
+ recommendedAction: 'FIX',
100
+ reason: 'Strong evidence indicates this is a real failure',
101
+ suggestedNextStep: 'Investigate and fix the issue'
102
+ };
103
+ }
104
+
105
+ // MEDIUM confidence → REVIEW
106
+ if (confidenceLevel === 'MEDIUM') {
107
+ return {
108
+ recommendedAction: 'REVIEW',
109
+ reason: 'Evidence suggests a failure, but some uncertainty remains',
110
+ suggestedNextStep: 'Review the finding and evidence to determine if this is a real issue or a false positive'
111
+ };
112
+ }
113
+
114
+ // LOW confidence → REVIEW
115
+ if (confidenceLevel === 'LOW') {
116
+ return {
117
+ recommendedAction: 'REVIEW',
118
+ reason: 'Limited evidence - this may be a false positive',
119
+ suggestedNextStep: 'Review manually to confirm if this is a real issue'
120
+ };
121
+ }
122
+
123
+ // Default for unknown
124
+ return {
125
+ recommendedAction: 'REVIEW',
126
+ reason: 'Insufficient information to determine action',
127
+ suggestedNextStep: 'Review the finding and evidence manually'
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Derive a confidence explanation structure for findings.
133
+ * Ensures required fields exist even if upstream confidence missed them.
134
+ */
135
+ export function deriveConfidenceExplanation(confidence = {}, findingType = 'unknown') {
136
+ const base = confidence.confidenceExplanation || {};
137
+ const factors = confidence.factors || {};
138
+ const expectationStrength = factors.expectationStrength || 'UNKNOWN';
139
+ const sensorsPresent = factors.sensorsPresent || {};
140
+
141
+ const why = Array.isArray(base.whyThisConfidence) && base.whyThisConfidence.length > 0
142
+ ? base.whyThisConfidence
143
+ : [
144
+ expectationStrength === 'PROVEN'
145
+ ? 'Expectation is proven and failed in practice'
146
+ : `Expectation strength is ${expectationStrength}, adding uncertainty`,
147
+ sensorsPresent.network || sensorsPresent.console || sensorsPresent.ui
148
+ ? 'Some evidence was captured from available sensors'
149
+ : 'Limited evidence captured from sensors'
150
+ ];
151
+
152
+ const whatIncrease = Array.isArray(base.whatWouldIncreaseConfidence) && base.whatWouldIncreaseConfidence.length > 0
153
+ ? base.whatWouldIncreaseConfidence
154
+ : [
155
+ expectationStrength === 'PROVEN'
156
+ ? 'Capture more sensor evidence (network/console/UI) for this interaction'
157
+ : 'Make this expectation PROVEN or capture more evidence to remove uncertainty'
158
+ ];
159
+
160
+ const whatReduce = Array.isArray(base.whatWouldReduceConfidence) && base.whatWouldReduceConfidence.length > 0
161
+ ? base.whatWouldReduceConfidence
162
+ : ['Confidence would drop if sensors were missing or expectation weakened'];
163
+
164
+ return {
165
+ whyThisConfidence: dedupeStrings(why),
166
+ whatWouldIncreaseConfidence: dedupeStrings(whatIncrease),
167
+ whatWouldReduceConfidence: dedupeStrings(whatReduce),
168
+ expectationStrength,
169
+ sensorsPresent,
170
+ findingType
171
+ };
172
+ }
173
+
174
+ function dedupeStrings(list = []) {
175
+ const seen = new Set();
176
+ const output = [];
177
+ for (const item of list) {
178
+ const val = String(item || '').trim();
179
+ if (!val) continue;
180
+ if (!seen.has(val)) {
181
+ seen.add(val);
182
+ output.push(val);
183
+ }
184
+ }
185
+ return output;
186
+ }
187
+