@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,450 @@
1
+ /**
2
+ * Finding Detector - Pure finding creation from trace analysis
3
+ *
4
+ * Contains logic to create finding objects from:
5
+ * - Expectation-driven execution outcomes
6
+ * - Observed expectation breaks
7
+ * - Flow failures
8
+ *
9
+ * PHASE 2: All findings include explicit outcome classification and purely factual wording.
10
+ * PHASE 3: All findings include Promise awareness - which promise was evaluated/unmet.
11
+ *
12
+ * All functions are pure (no file I/O, no side effects).
13
+ */
14
+
15
+ import { hasMeaningfulUrlChange, hasVisibleChange, hasDomChange } from './comparison.js';
16
+ import { computeConfidence } from './confidence-engine.js';
17
+ import { generateHumanSummary, generateActionHint, deriveConfidenceExplanation } from './explanation-helpers.js';
18
+ import { mapFindingTypeToOutcome } from '../core/canonical-outcomes.js';
19
+ import { inferPromiseFromInteraction } from '../core/promise-model.js';
20
+
21
+ /**
22
+ * Map finding type to confidence engine type.
23
+ */
24
+ export function mapFindingTypeToConfidenceType(findingType) {
25
+ if (findingType === 'silent_failure') {
26
+ return 'no_effect_silent_failure';
27
+ }
28
+ if (findingType === 'flow_silent_failure') {
29
+ return 'no_effect_silent_failure';
30
+ }
31
+ if (findingType === 'observed_break') {
32
+ return 'no_effect_silent_failure';
33
+ }
34
+ return findingType;
35
+ }
36
+
37
+ /**
38
+ * Compute confidence for a finding.
39
+ */
40
+ export function computeFindingConfidence(finding, matchedExpectation, trace, comparisons) {
41
+ const sensors = trace.sensors || {};
42
+ const findingType = mapFindingTypeToConfidenceType(finding.type);
43
+
44
+ const confidence = computeConfidence({
45
+ findingType,
46
+ expectation: matchedExpectation || {},
47
+ sensors: {
48
+ network: sensors.network || {},
49
+ console: sensors.console || {},
50
+ uiSignals: sensors.uiSignals || {}
51
+ },
52
+ comparisons: {
53
+ hasUrlChange: comparisons.hasUrlChange || false,
54
+ hasDomChange: comparisons.hasDomChange || false,
55
+ hasVisibleChange: comparisons.hasVisibleChange || false
56
+ },
57
+ attemptMeta: {}
58
+ });
59
+
60
+ return confidence;
61
+ }
62
+
63
+ /**
64
+ * Enrich finding with Phase 9 explanation fields: humanSummary, actionHint.
65
+ */
66
+ export function enrichFindingWithExplanations(finding, trace) {
67
+ // Add human summary
68
+ finding.humanSummary = generateHumanSummary(finding, trace);
69
+
70
+ // Add action hint
71
+ finding.actionHint = generateActionHint(finding, finding.confidence);
72
+
73
+ // Add confidence explanation (Phase 9)
74
+ finding.confidenceExplanation = deriveConfidenceExplanation(
75
+ finding.confidence || {},
76
+ finding.type
77
+ );
78
+
79
+ return finding;
80
+ }
81
+
82
+ /**
83
+ * PHASE 3: Bind Promise to finding
84
+ *
85
+ * Infers the promise from the interaction context and attaches it to the finding.
86
+ * All findings must have a promise descriptor.
87
+ */
88
+ export function bindPromiseToFinding(finding, trace) {
89
+ const interaction = trace.interaction || {};
90
+
91
+ // Infer promise from interaction type/label
92
+ const promise = inferPromiseFromInteraction(interaction);
93
+
94
+ if (promise) {
95
+ finding.promise = promise;
96
+ } else {
97
+ // No promise could be inferred - mark as unproven
98
+ finding.promise = {
99
+ type: 'UNPROVEN_INTERACTION',
100
+ source: 'unknown',
101
+ expected_signal: 'Unknown',
102
+ reason: 'Could not infer promise from interaction type'
103
+ };
104
+ }
105
+
106
+ return finding;
107
+ }
108
+
109
+ /**
110
+ * Create finding from expectation-driven execution outcome.
111
+ * Called when expectation execution resulted in SILENT_FAILURE.
112
+ */
113
+ export function createFindingFromExpectationOutcome(expectation, trace, beforeUrl, afterUrl, beforeScreenshot, afterScreenshot, projectDir, _manifest) {
114
+ const interaction = trace.interaction;
115
+ const sensors = trace.sensors || {};
116
+
117
+ if (expectation.type === 'navigation' || expectation.type === 'spa_navigation') {
118
+ const finding = {
119
+ outcome: mapFindingTypeToOutcome('navigation_silent_failure'),
120
+ type: 'navigation_silent_failure',
121
+ interaction: {
122
+ type: interaction.type,
123
+ selector: interaction.selector,
124
+ label: interaction.label
125
+ },
126
+ what_happened: 'User action executed',
127
+ what_was_expected: `Navigation to ${expectation.targetPath || expectation.expectedTarget || 'target URL'}`,
128
+ what_was_observed: 'No URL change, no user-visible feedback provided',
129
+ why_it_matters: 'User took action expecting navigation but received no confirmation of success or failure',
130
+ evidence: {
131
+ before: beforeScreenshot,
132
+ after: afterScreenshot,
133
+ beforeUrl: beforeUrl,
134
+ afterUrl: afterUrl,
135
+ expectedTarget: expectation.targetPath || expectation.expectedTarget || '',
136
+ targetReached: false,
137
+ urlChanged: sensors.navigation?.urlChanged === true,
138
+ historyLengthDelta: sensors.navigation?.historyLengthDelta || 0,
139
+ uiFeedback: sensors.uiSignals?.diff?.changed === true
140
+ }
141
+ };
142
+
143
+ const hasUrlChange = sensors.navigation?.urlChanged === true || hasMeaningfulUrlChange(beforeUrl, afterUrl);
144
+ finding.confidence = computeFindingConfidence(
145
+ finding,
146
+ expectation,
147
+ trace,
148
+ { hasUrlChange, hasDomChange: false, hasVisibleChange: false }
149
+ );
150
+
151
+ bindPromiseToFinding(finding, trace);
152
+ enrichFindingWithExplanations(finding, trace);
153
+ return finding;
154
+ }
155
+
156
+ if (expectation.type === 'network_action') {
157
+ const networkData = sensors.network || {};
158
+ const hasRequest = networkData.totalRequests > 0;
159
+ const hasFailed = networkData.failedRequests > 0;
160
+
161
+ if (!hasRequest) {
162
+ const finding = {
163
+ outcome: mapFindingTypeToOutcome('missing_network_action'),
164
+ type: 'missing_network_action',
165
+ interaction: {
166
+ type: interaction.type,
167
+ selector: interaction.selector,
168
+ label: interaction.label
169
+ },
170
+ what_happened: 'User action executed',
171
+ what_was_expected: `Network request to ${expectation.expectedTarget || expectation.urlPath || 'endpoint'} (${expectation.method || 'GET'})`,
172
+ what_was_observed: 'No network request was made',
173
+ why_it_matters: 'User took action expecting server communication but request was never sent',
174
+ evidence: {
175
+ before: beforeScreenshot,
176
+ after: afterScreenshot,
177
+ beforeUrl: beforeUrl,
178
+ afterUrl: afterUrl,
179
+ expectedEndpoint: expectation.expectedTarget || expectation.urlPath || '',
180
+ expectedMethod: expectation.method || 'GET',
181
+ totalRequests: networkData.totalRequests
182
+ },
183
+ confidence: computeFindingConfidence(
184
+ { type: 'missing_network_action' },
185
+ expectation,
186
+ trace,
187
+ { hasUrlChange: false, hasDomChange: false, hasVisibleChange: false }
188
+ )
189
+ };
190
+ bindPromiseToFinding(finding, trace);
191
+ enrichFindingWithExplanations(finding, trace);
192
+ return finding;
193
+ }
194
+
195
+ if (hasFailed && !sensors.uiSignals?.diff?.changed) {
196
+ const finding = {
197
+ outcome: mapFindingTypeToOutcome('network_silent_failure'),
198
+ type: 'network_silent_failure',
199
+ interaction: {
200
+ type: interaction.type,
201
+ selector: interaction.selector,
202
+ label: interaction.label
203
+ },
204
+ what_happened: 'User action executed; network request was sent but failed',
205
+ what_was_expected: `Request to ${expectation.expectedTarget || expectation.urlPath || 'endpoint'} succeeds with user feedback`,
206
+ what_was_observed: 'Request failed but no user-visible feedback provided',
207
+ why_it_matters: 'User took action expecting server communication; request failed but they received no error notification',
208
+ evidence: {
209
+ before: beforeScreenshot,
210
+ after: afterScreenshot,
211
+ beforeUrl: beforeUrl,
212
+ afterUrl: afterUrl,
213
+ expectedEndpoint: expectation.expectedTarget || expectation.urlPath || '',
214
+ expectedMethod: expectation.method || 'GET',
215
+ failedRequests: networkData.failedRequests,
216
+ uiFeedback: false
217
+ },
218
+ confidence: computeFindingConfidence(
219
+ { type: 'network_silent_failure' },
220
+ expectation,
221
+ trace,
222
+ { hasUrlChange: false, hasDomChange: false, hasVisibleChange: false }
223
+ )
224
+ };
225
+ bindPromiseToFinding(finding, trace);
226
+ enrichFindingWithExplanations(finding, trace);
227
+ return finding;
228
+ }
229
+ }
230
+
231
+ if (expectation.type === 'validation_block') {
232
+ const finding = {
233
+ outcome: mapFindingTypeToOutcome('validation_silent_failure'),
234
+ type: 'validation_silent_failure',
235
+ interaction: {
236
+ type: interaction.type,
237
+ selector: interaction.selector,
238
+ label: interaction.label
239
+ },
240
+ what_happened: 'User submitted form',
241
+ what_was_expected: 'Form validation fails with user-visible error message',
242
+ what_was_observed: 'Form submission was blocked but no user-visible feedback provided',
243
+ why_it_matters: 'User submitted form but received no indication why the submission was rejected',
244
+ evidence: {
245
+ before: beforeScreenshot,
246
+ after: afterScreenshot,
247
+ beforeUrl: beforeUrl,
248
+ afterUrl: afterUrl,
249
+ urlChanged: false,
250
+ networkRequests: sensors.network?.totalRequests || 0,
251
+ validationFeedbackDetected: sensors.uiSignals?.after?.validationFeedbackDetected === true,
252
+ sourceRef: expectation.sourceRef,
253
+ handlerRef: expectation.handlerRef
254
+ },
255
+ confidence: computeFindingConfidence(
256
+ { type: 'validation_silent_failure' },
257
+ expectation,
258
+ trace,
259
+ { hasUrlChange: false, hasDomChange: false, hasVisibleChange: false }
260
+ )
261
+ };
262
+ bindPromiseToFinding(finding, trace);
263
+ enrichFindingWithExplanations(finding, trace);
264
+ return finding;
265
+ }
266
+
267
+ if (expectation.type === 'state_action') {
268
+ const finding = {
269
+ outcome: mapFindingTypeToOutcome('missing_state_action'),
270
+ type: 'missing_state_action',
271
+ interaction: {
272
+ type: interaction.type,
273
+ selector: interaction.selector,
274
+ label: interaction.label
275
+ },
276
+ what_happened: 'User action executed',
277
+ what_was_expected: `Application state changes to: ${expectation.expectedTarget || ''} with user-visible feedback`,
278
+ what_was_observed: 'State did not change, or changed without user-visible feedback',
279
+ why_it_matters: 'User took action expecting app state to change but saw no confirmation',
280
+ evidence: {
281
+ before: beforeScreenshot,
282
+ after: afterScreenshot,
283
+ beforeUrl: beforeUrl,
284
+ afterUrl: afterUrl,
285
+ expectedStateKey: expectation.expectedTarget || '',
286
+ stateChanged: sensors.state?.changed?.length > 0,
287
+ stateKeysChanged: sensors.state?.changed || [],
288
+ uiFeedback: sensors.uiSignals?.diff?.changed === true
289
+ },
290
+ confidence: computeFindingConfidence(
291
+ { type: 'missing_state_action' },
292
+ expectation,
293
+ trace,
294
+ { hasUrlChange: false, hasDomChange: false, hasVisibleChange: false }
295
+ )
296
+ };
297
+ bindPromiseToFinding(finding, trace);
298
+ enrichFindingWithExplanations(finding, trace);
299
+ return finding;
300
+ }
301
+
302
+ // Generic silent failure
303
+ const finding = {
304
+ outcome: mapFindingTypeToOutcome('silent_failure'),
305
+ type: 'silent_failure',
306
+ interaction: {
307
+ type: interaction.type,
308
+ selector: interaction.selector,
309
+ label: interaction.label
310
+ },
311
+ what_happened: 'User action executed',
312
+ what_was_expected: 'User-visible change (URL, DOM, visual state)',
313
+ what_was_observed: 'No observable change occurred',
314
+ why_it_matters: 'User took action but system provided no visible confirmation of success or failure',
315
+ evidence: {
316
+ before: beforeScreenshot,
317
+ after: afterScreenshot,
318
+ beforeUrl: beforeUrl,
319
+ afterUrl: afterUrl
320
+ },
321
+ confidence: computeFindingConfidence(
322
+ { type: 'silent_failure' },
323
+ expectation,
324
+ trace,
325
+ {
326
+ hasUrlChange: hasMeaningfulUrlChange(beforeUrl, afterUrl),
327
+ hasDomChange: hasDomChange(trace),
328
+ hasVisibleChange: hasVisibleChange(beforeScreenshot, afterScreenshot, projectDir)
329
+ }
330
+ )
331
+ };
332
+ bindPromiseToFinding(finding, trace);
333
+ enrichFindingWithExplanations(finding, trace);
334
+ return finding;
335
+ }
336
+
337
+ /**
338
+ * Create finding from observed expectation that didn't repeat.
339
+ */
340
+ export function createObservedBreakFinding(trace, projectDir) {
341
+ const obs = trace.observedExpectation;
342
+ const interaction = trace.interaction || {};
343
+ const beforeUrl = trace.before?.url;
344
+ const afterUrl = trace.after?.url;
345
+ const beforeScreenshot = trace.before?.screenshot;
346
+ const afterScreenshot = trace.after?.screenshot;
347
+ const sensors = trace.sensors || {};
348
+
349
+ const comparisons = {
350
+ hasUrlChange: hasMeaningfulUrlChange(beforeUrl, afterUrl),
351
+ hasDomChange: hasDomChange(trace),
352
+ hasVisibleChange: hasVisibleChange(beforeScreenshot, afterScreenshot, projectDir)
353
+ };
354
+
355
+ const finding = {
356
+ outcome: mapFindingTypeToOutcome('observed_break'),
357
+ type: 'observed_break',
358
+ interaction: {
359
+ type: interaction.type,
360
+ selector: interaction.selector,
361
+ label: interaction.label
362
+ },
363
+ what_happened: 'User interaction was executed',
364
+ what_was_expected: `Previously observed outcome repeats: ${obs?.reason || 'observable change'}`,
365
+ what_was_observed: 'Same user interaction this time produced a different outcome',
366
+ why_it_matters: 'Interaction behavior changed between executions: system is non-deterministic',
367
+ evidence: {
368
+ before: beforeScreenshot,
369
+ after: afterScreenshot,
370
+ beforeUrl,
371
+ afterUrl,
372
+ observedExpectation: obs
373
+ }
374
+ };
375
+
376
+ finding.confidence = computeConfidence({
377
+ findingType: mapFindingTypeToConfidenceType('observed_break'),
378
+ expectation: obs || { expectationStrength: 'OBSERVED' },
379
+ sensors: {
380
+ network: sensors.network || {},
381
+ console: sensors.console || {},
382
+ uiSignals: sensors.uiSignals || {}
383
+ },
384
+ comparisons,
385
+ attemptMeta: { repeated: obs?.repeated === true }
386
+ });
387
+
388
+ bindPromiseToFinding(finding, trace);
389
+ enrichFindingWithExplanations(finding, trace);
390
+ return finding;
391
+ }
392
+
393
+ /**
394
+ * Compute confidence for OBSERVED expectations with strict caps.
395
+ * OBSERVED can be at most MEDIUM unless repeated twice.
396
+ */
397
+ export function computeObservedConfidence(finding, observedExp, trace, comparisons, repeated) {
398
+ // Base score for OBSERVED expectations (lower than PROVEN)
399
+ let baseScore = 50;
400
+
401
+ // Boost if repeated (confirms consistency)
402
+ if (repeated) {
403
+ baseScore += 15; // Boost for repetition
404
+ }
405
+
406
+ // Apply sensor presence penalties/boosts
407
+ const sensors = trace.sensors || {};
408
+ const sensorsPresent = {
409
+ network: Object.keys(sensors.network || {}).length > 0,
410
+ console: Object.keys(sensors.console || {}).length > 0,
411
+ ui: Object.keys(sensors.uiSignals || {}).length > 0
412
+ };
413
+
414
+ const allSensorsPresent = sensorsPresent.network && sensorsPresent.console && sensorsPresent.ui;
415
+
416
+ if (!allSensorsPresent) {
417
+ baseScore -= 10; // Penalty for missing sensors
418
+ }
419
+
420
+ // Clamp score
421
+ let score = Math.max(0, Math.min(100, baseScore));
422
+
423
+ // Determine level with strict caps
424
+ let level = 'LOW';
425
+
426
+ if (score >= 55 && repeated) {
427
+ // MEDIUM only if repeated
428
+ level = 'MEDIUM';
429
+ score = Math.min(score, 70); // Cap at 70 for OBSERVED
430
+ } else if (score >= 55) {
431
+ // Without repetition, cap at LOW
432
+ level = 'LOW';
433
+ score = Math.min(score, 54);
434
+ }
435
+
436
+ return {
437
+ score: Math.round(score),
438
+ level,
439
+ explain: [
440
+ `Expectation strength: OBSERVED (runtime-derived)`,
441
+ repeated ? 'Confirmed by repetition' : 'Single observation (not repeated)',
442
+ allSensorsPresent ? 'All sensors present' : 'Some sensors missing'
443
+ ],
444
+ factors: {
445
+ expectationStrength: 'OBSERVED',
446
+ repeated: repeated,
447
+ sensorsPresent: sensorsPresent
448
+ }
449
+ };
450
+ }
@@ -1,28 +1,57 @@
1
1
  import { resolve } from 'path';
2
2
  import { mkdirSync, writeFileSync } from 'fs';
3
+ import { CANONICAL_OUTCOMES } from '../core/canonical-outcomes.js';
3
4
 
4
- export function writeFindings(projectDir, url, findings, artifactPaths = null) {
5
- let findingsPath;
6
- if (artifactPaths) {
7
- // Use new artifact structure
8
- findingsPath = artifactPaths.findings;
9
- } else {
10
- // Legacy structure
11
- const detectDir = resolve(projectDir, '.veraxverax', 'detect');
12
- mkdirSync(detectDir, { recursive: true });
13
- findingsPath = resolve(detectDir, 'findings.json');
5
+ /**
6
+ * Write findings to canonical artifact root.
7
+ * Writes to .verax/runs/<runId>/findings.json.
8
+ *
9
+ * PHASE 2: Includes outcome classification summary.
10
+ * PHASE 3: Includes promise type summary.
11
+ *
12
+ * @param {string} projectDir
13
+ * @param {string} url
14
+ * @param {Array} findings
15
+ * @param {Array} coverageGaps
16
+ * @param {string} runDirOpt - Required absolute run directory path
17
+ */
18
+ export function writeFindings(projectDir, url, findings, coverageGaps = [], runDirOpt) {
19
+ if (!runDirOpt) {
20
+ throw new Error('runDirOpt is required');
14
21
  }
22
+ mkdirSync(runDirOpt, { recursive: true });
23
+ const findingsPath = resolve(runDirOpt, 'findings.json');
24
+
25
+ // PHASE 2: Compute outcome summary
26
+ const outcomeSummary = {};
27
+ Object.values(CANONICAL_OUTCOMES).forEach(outcome => {
28
+ outcomeSummary[outcome] = 0;
29
+ });
30
+
31
+ // PHASE 3: Compute promise summary
32
+ const promiseSummary = {};
15
33
 
34
+ for (const finding of (findings || [])) {
35
+ const outcome = finding.outcome || CANONICAL_OUTCOMES.SILENT_FAILURE;
36
+ outcomeSummary[outcome] = (outcomeSummary[outcome] || 0) + 1;
37
+
38
+ const promiseType = finding.promise?.type || 'UNKNOWN_PROMISE';
39
+ promiseSummary[promiseType] = (promiseSummary[promiseType] || 0) + 1;
40
+ }
41
+
16
42
  const findingsReport = {
17
43
  version: 1,
18
44
  detectedAt: new Date().toISOString(),
19
45
  url: url,
46
+ outcomeSummary: outcomeSummary, // PHASE 2
47
+ promiseSummary: promiseSummary, // PHASE 3
20
48
  findings: findings,
49
+ coverageGaps: coverageGaps,
21
50
  notes: []
22
51
  };
23
-
52
+
24
53
  writeFileSync(findingsPath, JSON.stringify(findingsReport, null, 2) + '\n');
25
-
54
+
26
55
  return {
27
56
  ...findingsReport,
28
57
  findingsPath: findingsPath