@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,356 @@
1
+ /**
2
+ * Global Invariants Enforcement
3
+ *
4
+ * Production lock: ensures VERAX philosophy cannot be violated.
5
+ * Any finding that violates invariants is dropped silently.
6
+ *
7
+ * Invariants:
8
+ * 1. Every finding MUST have evidence, confidence, and signals
9
+ * 2. Every finding MUST be derived from proven expectation OR observable sensor
10
+ * 3. No finding may exist with missing evidence or contradictory signals
11
+ * 4. Ambiguous findings (low confidence + conflicting signals) are dropped
12
+ */
13
+
14
+ import { isProvenExpectation } from '../shared/expectation-prover.js';
15
+
16
+ /**
17
+ * Production lock flag - when enabled, enforces all invariants strictly
18
+ */
19
+ export const VERAX_PRODUCTION_LOCK = true;
20
+
21
+ /**
22
+ * Minimum confidence threshold for ambiguous findings
23
+ */
24
+ const AMBIGUITY_CONFIDENCE_THRESHOLD = 60; // Score, not level
25
+
26
+ /**
27
+ * Enforce all invariants on a finding
28
+ * @param {Object} finding - Finding object to validate
29
+ * @param {Object} trace - Associated trace (for sensor evidence)
30
+ * @param {Object} matchedExpectation - Matched expectation if any
31
+ * @returns {Object} { shouldDrop: boolean, reason: string }
32
+ */
33
+ export function enforceInvariants(finding, trace = {}, matchedExpectation = null) {
34
+ if (!VERAX_PRODUCTION_LOCK) {
35
+ // Production lock disabled - skip checks (development mode only)
36
+ return { shouldDrop: false, reason: null };
37
+ }
38
+
39
+ // INVARIANT 1: Every finding MUST have evidence
40
+ if (!finding.evidence || typeof finding.evidence !== 'object') {
41
+ return {
42
+ shouldDrop: true,
43
+ reason: 'missing_evidence',
44
+ message: 'Finding lacks evidence object'
45
+ };
46
+ }
47
+
48
+ // INVARIANT 2: Every finding MUST have confidence
49
+ if (!finding.confidence || typeof finding.confidence !== 'object') {
50
+ return {
51
+ shouldDrop: true,
52
+ reason: 'missing_confidence',
53
+ message: 'Finding lacks confidence object'
54
+ };
55
+ }
56
+
57
+ // INVARIANT 3: Every finding MUST have signals (Phase 9)
58
+ if (!finding.signals || typeof finding.signals !== 'object') {
59
+ return {
60
+ shouldDrop: true,
61
+ reason: 'missing_signals',
62
+ message: 'Finding lacks signals object (impact, userRisk, ownership)'
63
+ };
64
+ }
65
+
66
+ // INVARIANT 4: Required signal fields must exist
67
+ const requiredSignals = ['impact', 'userRisk', 'ownership', 'grouping'];
68
+ for (const field of requiredSignals) {
69
+ if (!finding.signals[field]) {
70
+ return {
71
+ shouldDrop: true,
72
+ reason: `missing_signal_${field}`,
73
+ message: `Finding signals missing required field: ${field}`
74
+ };
75
+ }
76
+ }
77
+
78
+ // INVARIANT 5: Impact, userRisk, ownership must be valid enums
79
+ const validImpacts = ['LOW', 'MEDIUM', 'HIGH'];
80
+ if (!validImpacts.includes(finding.signals.impact)) {
81
+ return {
82
+ shouldDrop: true,
83
+ reason: 'invalid_impact',
84
+ message: `Finding has invalid impact: ${finding.signals.impact}`
85
+ };
86
+ }
87
+
88
+ const validUserRisks = ['BLOCKS', 'CONFUSES', 'DEGRADES'];
89
+ if (!validUserRisks.includes(finding.signals.userRisk)) {
90
+ return {
91
+ shouldDrop: true,
92
+ reason: 'invalid_userRisk',
93
+ message: `Finding has invalid userRisk: ${finding.signals.userRisk}`
94
+ };
95
+ }
96
+
97
+ const validOwnership = ['FRONTEND', 'BACKEND', 'INTEGRATION', 'ACCESSIBILITY', 'PERFORMANCE'];
98
+ if (!validOwnership.includes(finding.signals.ownership)) {
99
+ return {
100
+ shouldDrop: true,
101
+ reason: 'invalid_ownership',
102
+ message: `Finding has invalid ownership: ${finding.signals.ownership}`
103
+ };
104
+ }
105
+
106
+ // INVARIANT 6: Finding MUST be derived from proven expectation OR observable sensor
107
+ // Check for proven expectation using the canonical isProvenExpectation function
108
+ const hasProvenExpectation = matchedExpectation && isProvenExpectation(matchedExpectation);
109
+
110
+ // Check for observed expectation (runtime-derived, also valid as it comes from observable behavior)
111
+ const hasObservedExpectation = trace?.observedExpectation || finding.expectationId;
112
+
113
+ // Check for observable sensor evidence (at least one sensor must have activity)
114
+ const sensors = trace?.sensors || {};
115
+ const findingEvidence = finding.evidence || {};
116
+ const hasObservableSensor = (
117
+ (sensors.network?.totalRequests || 0) > 0 ||
118
+ (sensors.console?.errors?.length || 0) > 0 ||
119
+ sensors.uiSignals?.diff?.changed === true ||
120
+ sensors.focus ||
121
+ sensors.aria ||
122
+ sensors.timing ||
123
+ sensors.loading ||
124
+ sensors.state ||
125
+ findingEvidence.beforeUrl ||
126
+ findingEvidence.afterUrl ||
127
+ findingEvidence.beforeScreenshot ||
128
+ findingEvidence.afterScreenshot ||
129
+ findingEvidence.before ||
130
+ findingEvidence.after
131
+ );
132
+
133
+ // Finding must have at least one grounding: proven expectation, observed expectation, or sensor evidence
134
+ if (!hasProvenExpectation && !hasObservedExpectation && !hasObservableSensor) {
135
+ return {
136
+ shouldDrop: true,
137
+ reason: 'ungrounded_finding',
138
+ message: 'Finding not derived from proven expectation, observed expectation, or observable sensor'
139
+ };
140
+ }
141
+
142
+ // INVARIANT 7: Ambiguity guard - low confidence + conflicting signals = drop
143
+ const confidenceScore = finding.confidence?.score || 0;
144
+ const confidenceLevel = finding.confidence?.level || 'UNKNOWN';
145
+
146
+ // Only apply ambiguity guard if confidence is below threshold AND level is LOW
147
+ // High/MEDIUM confidence findings are trusted even if score is slightly below threshold
148
+ if (confidenceScore < AMBIGUITY_CONFIDENCE_THRESHOLD && confidenceLevel === 'LOW') {
149
+ // Check for conflicting sensor signals
150
+ const hasConflictingSignals = detectConflictingSignals(finding, trace);
151
+
152
+ if (hasConflictingSignals) {
153
+ return {
154
+ shouldDrop: true,
155
+ reason: 'ambiguous_finding',
156
+ message: `Low confidence finding (score: ${confidenceScore}, level: ${confidenceLevel}) with conflicting sensor signals - dropped for safety`
157
+ };
158
+ }
159
+ }
160
+
161
+ // INVARIANT 8: Contradictory signals check
162
+ const hasContradiction = detectContradictorySignals(finding);
163
+ if (hasContradiction) {
164
+ return {
165
+ shouldDrop: true,
166
+ reason: 'contradictory_signals',
167
+ message: 'Finding has contradictory signals (e.g., HIGH impact but LOW confidence with no evidence)'
168
+ };
169
+ }
170
+
171
+ // All invariants passed
172
+ return {
173
+ shouldDrop: false,
174
+ reason: null
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Detect conflicting sensor signals (ambiguity indicator)
180
+ * Returns true if signals conflict (ambiguous finding)
181
+ */
182
+ function detectConflictingSignals(finding, trace) {
183
+ const sensors = trace?.sensors || {};
184
+ const evidence = finding.evidence || {};
185
+ const confidenceScore = finding.confidence?.score || 0;
186
+
187
+ // Only check conflicts for low-confidence findings (high confidence findings are trusted)
188
+ if (confidenceScore >= AMBIGUITY_CONFIDENCE_THRESHOLD) {
189
+ return false;
190
+ }
191
+
192
+ // Conflict: Network success but no UI change (partial success)
193
+ // This is actually a valid finding type - not a conflict
194
+ if (finding.type === 'partial_success_silent_failure') {
195
+ return false; // Explicitly allowed finding type
196
+ }
197
+
198
+ // Conflict: High impact but insufficient evidence for low confidence
199
+ if (finding.signals?.impact === 'HIGH') {
200
+ const hasSensorActivity = (
201
+ (sensors.network?.totalRequests || 0) > 0 ||
202
+ (sensors.console?.errors?.length || 0) > 0 ||
203
+ sensors.uiSignals?.diff?.changed === true ||
204
+ sensors.focus ||
205
+ sensors.aria ||
206
+ sensors.timing ||
207
+ sensors.loading
208
+ );
209
+
210
+ // For HIGH impact + LOW confidence, need strong evidence (not just beforeUrl)
211
+ const hasStrongEvidence = (
212
+ evidence.afterUrl ||
213
+ evidence.beforeScreenshot ||
214
+ evidence.afterScreenshot ||
215
+ (evidence.networkRequests || 0) > 0 ||
216
+ evidence.urlChanged === false || // Explicitly checked
217
+ evidence.domChanged === false ||
218
+ evidence.uiChanged === false
219
+ );
220
+
221
+ // If only beforeUrl exists without sensor activity or strong evidence, it's ambiguous for HIGH impact
222
+ if (!hasSensorActivity && !hasStrongEvidence && evidence.beforeUrl) {
223
+ // HIGH impact with only beforeUrl (weak evidence) and no sensor activity is ambiguous
224
+ return true;
225
+ }
226
+
227
+ // No evidence at all
228
+ if (!hasSensorActivity && !hasStrongEvidence && !evidence.beforeUrl) {
229
+ return true;
230
+ }
231
+ }
232
+
233
+ // Conflict: BLOCKS userRisk but no blocking evidence
234
+ if (finding.signals?.userRisk === 'BLOCKS') {
235
+ const hasBlockingEvidence = (
236
+ finding.type?.includes('navigation') ||
237
+ finding.type?.includes('auth') ||
238
+ finding.type?.includes('loading_stuck') ||
239
+ finding.type?.includes('freeze_like') ||
240
+ finding.type === 'observed_break' ||
241
+ (sensors.network?.totalRequests || 0) > 0 ||
242
+ evidence.urlChanged === false || // Explicitly checked and failed
243
+ (evidence.networkRequests || 0) > 0 ||
244
+ evidence.afterUrl // Navigation happened
245
+ );
246
+
247
+ // BLOCKS requires blocking-type evidence, not just beforeUrl
248
+ if (!hasBlockingEvidence) {
249
+ // BLOCKS claim without blocking evidence - ambiguous
250
+ return true;
251
+ }
252
+ }
253
+
254
+ // Conflict: BACKEND ownership but no network evidence
255
+ if (finding.signals?.ownership === 'BACKEND' && confidenceScore < AMBIGUITY_CONFIDENCE_THRESHOLD) {
256
+ const hasNetworkEvidence = (
257
+ sensors.network?.totalRequests > 0 ||
258
+ evidence.networkRequests > 0 ||
259
+ finding.type?.includes('network') ||
260
+ finding.type?.includes('auth')
261
+ );
262
+
263
+ if (!hasNetworkEvidence) {
264
+ // BACKEND ownership without network evidence - ambiguous
265
+ return true;
266
+ }
267
+ }
268
+
269
+ return false;
270
+ }
271
+
272
+ /**
273
+ * Detect contradictory signals within finding itself
274
+ */
275
+ function detectContradictorySignals(finding) {
276
+ const evidence = finding.evidence || {};
277
+
278
+ // Contradiction: HIGH impact but LOW confidence with weak evidence
279
+ if (finding.signals?.impact === 'HIGH' && finding.confidence?.level === 'LOW') {
280
+ // For HIGH impact + LOW confidence, need strong evidence (multiple pieces or actionable evidence)
281
+ const hasStrongEvidence = (
282
+ (evidence.afterUrl && evidence.beforeUrl) || // Navigation occurred
283
+ (evidence.beforeScreenshot && evidence.afterScreenshot) || // Both screenshots
284
+ (evidence.networkRequests || 0) > 0 ||
285
+ evidence.urlChanged === false || // Explicitly checked
286
+ evidence.domChanged === false ||
287
+ evidence.uiChanged === false
288
+ );
289
+
290
+ // Only beforeUrl alone is weak evidence for HIGH impact
291
+ if (!hasStrongEvidence) {
292
+ // HIGH impact + LOW confidence + weak evidence = contradiction
293
+ return true;
294
+ }
295
+ }
296
+
297
+ // Contradiction: BACKEND ownership but no network evidence
298
+ if (finding.signals?.ownership === 'BACKEND') {
299
+ const hasNetworkEvidence = (
300
+ (evidence.networkRequests || 0) > 0 ||
301
+ finding.type?.includes('network') ||
302
+ finding.type?.includes('auth')
303
+ );
304
+
305
+ if (!hasNetworkEvidence) {
306
+ // BACKEND ownership without network evidence
307
+ return true;
308
+ }
309
+ }
310
+
311
+ // Contradiction: PERFORMANCE ownership but no timing/loading evidence
312
+ if (finding.signals?.ownership === 'PERFORMANCE') {
313
+ const hasPerformanceEvidence = (
314
+ finding.type?.includes('loading') ||
315
+ finding.type?.includes('freeze') ||
316
+ finding.type?.includes('feedback_gap') ||
317
+ finding.evidence?.timingBreakdown
318
+ );
319
+
320
+ if (!hasPerformanceEvidence) {
321
+ // PERFORMANCE ownership without performance evidence
322
+ return true;
323
+ }
324
+ }
325
+
326
+ return false;
327
+ }
328
+
329
+ /**
330
+ * Validate finding structure (non-destructive check)
331
+ * Returns validation result without modifying finding
332
+ */
333
+ export function validateFindingStructure(finding) {
334
+ const errors = [];
335
+
336
+ if (!finding.type) {
337
+ errors.push('Missing type field');
338
+ }
339
+
340
+ if (!finding.evidence) {
341
+ errors.push('Missing evidence object');
342
+ }
343
+
344
+ if (!finding.confidence) {
345
+ errors.push('Missing confidence object');
346
+ }
347
+
348
+ if (!finding.signals) {
349
+ errors.push('Missing signals object');
350
+ }
351
+
352
+ return {
353
+ valid: errors.length === 0,
354
+ errors
355
+ };
356
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * PROMISE MODEL - Canonical representation of what interactions promise
3
+ *
4
+ * A Promise is a technical contract between code and user:
5
+ * "When user executes this interaction, this observable signal will change"
6
+ *
7
+ * CRITICAL: Promises are derived ONLY from observable technical signals.
8
+ * No business logic, no semantics, no guessing what "should" happen.
9
+ */
10
+
11
+ /**
12
+ * Canonical Promise Types - What observable outcome is promised
13
+ */
14
+ export const PROMISE_TYPES = {
15
+ // URL or route path changes (including client-side routing)
16
+ NAVIGATION_PROMISE: 'NAVIGATION_PROMISE',
17
+
18
+ // HTTP request sent + success/error response handling
19
+ SUBMISSION_PROMISE: 'SUBMISSION_PROMISE',
20
+
21
+ // DOM or application state changes (not just network)
22
+ STATE_CHANGE_PROMISE: 'STATE_CHANGE_PROMISE',
23
+
24
+ // Visual or textual feedback to user (spinner, toast, modal, etc.)
25
+ FEEDBACK_PROMISE: 'FEEDBACK_PROMISE'
26
+ };
27
+
28
+ /**
29
+ * Promise Type Definitions
30
+ */
31
+ export const PROMISE_DEFINITIONS = {
32
+ NAVIGATION_PROMISE: {
33
+ title: 'Navigation',
34
+ description: 'Interaction implies a change in page URL or client-side route',
35
+ example: 'Clicking a link; navigating to next step in wizard',
36
+ expected_signal: 'window.location changes OR route state changes'
37
+ },
38
+
39
+ SUBMISSION_PROMISE: {
40
+ title: 'Submission',
41
+ description: 'Interaction implies form/data submission with response handling',
42
+ example: 'Clicking "Send", "Submit", "Save"; HTTP request with success/error handling',
43
+ expected_signal: 'HTTP request sent AND response triggers success feedback OR error message'
44
+ },
45
+
46
+ STATE_CHANGE_PROMISE: {
47
+ title: 'State Change',
48
+ description: 'Interaction implies application/DOM state change (cart, filters, view)',
49
+ example: 'Adding to cart; toggling visibility; filtering; sorting',
50
+ expected_signal: 'DOM change or app state variable changes'
51
+ },
52
+
53
+ FEEDBACK_PROMISE: {
54
+ title: 'Feedback',
55
+ description: 'Interaction implies user-visible feedback (confirmation, error, info)',
56
+ example: 'Click should trigger spinner, toast, modal, or message',
57
+ expected_signal: 'Visual/textual feedback appears: spinner, toast, error, success message'
58
+ }
59
+ };
60
+
61
+ /**
62
+ * PromiseDescriptor - What a specific interaction promises
63
+ *
64
+ * @typedef {Object} PromiseDescriptor
65
+ * @property {string} type - One of PROMISE_TYPES
66
+ * @property {string} source - What interaction type implied this promise (button_click, form_submit, link_click, etc.)
67
+ * @property {string} expected_signal - What observable signal would satisfy this promise
68
+ * @property {Object} context - Additional context about the promise
69
+ * - {string} [target] - Target URL/route for NAVIGATION
70
+ * - {string} [endpoint] - Endpoint for SUBMISSION
71
+ * - {string} [stateKey] - State variable for STATE_CHANGE
72
+ * - {Array} [feedbackTypes] - Types of feedback for FEEDBACK (spinner, toast, modal, message)
73
+ * @property {string} [reason] - If UNPROVEN, why promise is ambiguous
74
+ */
75
+
76
+ /**
77
+ * Infer Promise from interaction type and context
78
+ *
79
+ * Returns a PromiseDescriptor or null if no promise can be inferred
80
+ */
81
+ export function inferPromiseFromInteraction(interaction) {
82
+ if (!interaction) return null;
83
+
84
+ const type = interaction.type?.toLowerCase() || '';
85
+ const label = interaction.label?.toLowerCase() || '';
86
+ const ariaLabel = interaction.ariaLabel?.toLowerCase() || '';
87
+ const allText = `${type} ${label} ${ariaLabel}`.toLowerCase();
88
+
89
+ // NAVIGATION_PROMISE: links, navigation buttons
90
+ if (type === 'link' || type === 'navigation_link') {
91
+ return {
92
+ type: PROMISE_TYPES.NAVIGATION_PROMISE,
93
+ source: 'link_click',
94
+ expected_signal: 'URL or client-side route changes',
95
+ context: { target: interaction.href }
96
+ };
97
+ }
98
+
99
+ // NAVIGATION_PROMISE: "next", "go to", "navigate" buttons
100
+ if (type === 'button' && (
101
+ allText.includes('next') ||
102
+ allText.includes('goto') ||
103
+ allText.includes('go to') ||
104
+ allText.includes('navigate') ||
105
+ allText.includes('back') ||
106
+ allText.includes('previous')
107
+ )) {
108
+ return {
109
+ type: PROMISE_TYPES.NAVIGATION_PROMISE,
110
+ source: 'navigation_button',
111
+ expected_signal: 'URL or client-side route changes',
112
+ context: {}
113
+ };
114
+ }
115
+
116
+ // SUBMISSION_PROMISE: form submit, "send", "submit", "save" buttons
117
+ if (type === 'form_submit' || (type === 'button' && (
118
+ allText.includes('submit') ||
119
+ allText.includes('send') ||
120
+ allText.includes('save') ||
121
+ allText.includes('apply') ||
122
+ allText.includes('confirm') ||
123
+ allText.includes('post')
124
+ ))) {
125
+ return {
126
+ type: PROMISE_TYPES.SUBMISSION_PROMISE,
127
+ source: 'form_submit',
128
+ expected_signal: 'HTTP request sent AND success or error feedback displayed',
129
+ context: {}
130
+ };
131
+ }
132
+
133
+ // FEEDBACK_PROMISE: buttons with generic labels (click, press, tap)
134
+ // These usually expect some feedback but promise type is ambiguous
135
+ if (type === 'button' && (
136
+ allText.includes('click') ||
137
+ allText.includes('tap') ||
138
+ allText.includes('press') ||
139
+ allText.length === 0
140
+ )) {
141
+ return {
142
+ type: PROMISE_TYPES.FEEDBACK_PROMISE,
143
+ source: 'generic_button',
144
+ expected_signal: 'Visual or textual feedback appears',
145
+ context: {},
146
+ reason: 'Generic button label; promise type cannot be determined'
147
+ };
148
+ }
149
+
150
+ // STATE_CHANGE_PROMISE: toggle, add, remove, filter, sort
151
+ if (allText.includes('toggle') ||
152
+ allText.includes('add') ||
153
+ allText.includes('remove') ||
154
+ allText.includes('delete') ||
155
+ allText.includes('filter') ||
156
+ allText.includes('sort') ||
157
+ allText.includes('show') ||
158
+ allText.includes('hide') ||
159
+ allText.includes('cart')) {
160
+ return {
161
+ type: PROMISE_TYPES.STATE_CHANGE_PROMISE,
162
+ source: 'state_action_button',
163
+ expected_signal: 'DOM or application state changes',
164
+ context: {}
165
+ };
166
+ }
167
+
168
+ // Default to FEEDBACK_PROMISE if interaction is a button but type unclear
169
+ if (type === 'button') {
170
+ return {
171
+ type: PROMISE_TYPES.FEEDBACK_PROMISE,
172
+ source: 'button',
173
+ expected_signal: 'User-visible feedback or state change',
174
+ context: {},
175
+ reason: 'Button intent unclear; assuming feedback expected'
176
+ };
177
+ }
178
+
179
+ // No promise can be inferred
180
+ return null;
181
+ }
182
+
183
+ /**
184
+ * Validate that a promise descriptor is well-formed
185
+ */
186
+ export function isValidPromiseDescriptor(promise) {
187
+ return promise &&
188
+ Object.values(PROMISE_TYPES).includes(promise.type) &&
189
+ typeof promise.source === 'string' &&
190
+ typeof promise.expected_signal === 'string';
191
+ }
192
+
193
+ /**
194
+ * Format Promise for human display
195
+ */
196
+ export function formatPromiseForDisplay(promise) {
197
+ if (!promise) return 'No promise';
198
+
199
+ const def = PROMISE_DEFINITIONS[promise.type] || {};
200
+ return {
201
+ type: promise.type,
202
+ title: def.title || promise.type,
203
+ expected: promise.expected_signal,
204
+ source: promise.source
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Create a promise descriptor explicitly
210
+ */
211
+ export function createPromiseDescriptor(type, source, expected_signal, context = {}, reason = null) {
212
+ if (!Object.values(PROMISE_TYPES).includes(type)) {
213
+ throw new Error(`Invalid promise type: ${type}`);
214
+ }
215
+
216
+ const descriptor = {
217
+ type,
218
+ source,
219
+ expected_signal,
220
+ context
221
+ };
222
+
223
+ if (reason) {
224
+ descriptor.reason = reason;
225
+ }
226
+
227
+ return descriptor;
228
+ }
229
+
230
+ export default PROMISE_TYPES;