@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,523 @@
1
+ /**
2
+ * SILENCE MODEL - Unified tracking for all unobserved/unevaluated/skipped states
3
+ *
4
+ * CRITICAL PRINCIPLE: Nothing unobserved is allowed to disappear.
5
+ * Every skip, cap, drop, timeout, and gap MUST be tracked and reported.
6
+ *
7
+ * NEVER return "nothing to report" when data is absent.
8
+ * ALWAYS report why data is absent.
9
+ *
10
+ * PHASE 2: All silences now include explicit outcome classification.
11
+ * PHASE 4: All silences include explicit lifecycle: type, promise association, trigger, status, impact.
12
+ */
13
+
14
+ import { mapSilenceReasonToOutcome } from './canonical-outcomes.js';
15
+
16
+ /**
17
+ * SilenceEntry - Unified structure for tracking all silence/gaps/unknowns
18
+ *
19
+ * PHASE 4: Enhanced with lifecycle model
20
+ *
21
+ * Note: Fields marked as optional are auto-inferred by record() if not provided
22
+ *
23
+ * @typedef {Object} SilenceEntry
24
+ * @property {string} [outcome] - Canonical outcome: SILENT_FAILURE | COVERAGE_GAP | UNPROVEN_INTERACTION | SAFETY_BLOCK | INFORMATIONAL (auto-inferred if missing)
25
+ * @property {string} scope - What was not evaluated: page | interaction | expectation | sensor | navigation | settle
26
+ * @property {string} reason - Why it wasn't evaluated: timeout | cap | budget_exceeded | incremental_reuse | safety_skip | no_expectation | discovery_failed | sensor_failed
27
+ * @property {string} description - Human-readable description of what wasn't observed
28
+ * @property {Object} context - Additional context: { page, interaction, expectation, etc }
29
+ * @property {string} impact - Why this matters for observation: blocks_nav | affects_expectations | unknown_behavior | incomplete_check
30
+ * @property {number} [count] - Number of items affected by this silence (optional)
31
+ * @property {string} [evidenceUrl] - URL where this silence occurred (optional)
32
+ *
33
+ * PHASE 4 Lifecycle Fields (all auto-inferred if missing):
34
+ * @property {string} [silence_type] - Technical classification: interaction_not_executed | promise_not_evaluated | sensor_failure | timeout | budget_limit | safety_block | discovery_failure (auto-inferred)
35
+ * @property {Object} [related_promise] - Promise that could not be evaluated (if applicable) or null with reason
36
+ * @property {string} [trigger] - What triggered this silence: user_action_blocked | navigation_timeout | budget_exhausted | safety_policy | selector_not_found | etc (auto-inferred)
37
+ * @property {string} [evaluation_status] - blocked | ambiguous | skipped | timed_out | incomplete (auto-inferred)
38
+ * @property {Object} [confidence_impact] - Which confidence metrics are affected: { coverage: -X%, promise_verification: -Y%, overall: -Z% } (auto-inferred)
39
+ */
40
+
41
+ /**
42
+ * SilenceCategory - Grouping of related silence entries
43
+ */
44
+ export const SILENCE_CATEGORIES = {
45
+ BUDGET: 'budget', // Scan terminated due to time/interaction limit
46
+ TIMEOUT: 'timeout', // Navigation/interaction/settle timeout
47
+ SAFETY: 'safety', // Intentionally skipped (logout, delete, etc)
48
+ INCREMENTAL: 'incremental', // Reused previous run data
49
+ DISCOVERY: 'discovery', // Failed to discover items
50
+ SENSOR: 'sensor', // Sensor failed or returned empty
51
+ EXPECTATION: 'expectation', // No expectation exists for interaction
52
+ NAVIGATION: 'navigation', // Navigation blocked/failed
53
+ };
54
+
55
+ export const SILENCE_REASONS = {
56
+ // Budget-related
57
+ SCAN_TIME_EXCEEDED: 'scan_time_exceeded',
58
+ PAGE_LIMIT_EXCEEDED: 'page_limit_exceeded',
59
+ INTERACTION_LIMIT_EXCEEDED: 'interaction_limit_exceeded',
60
+ ROUTE_LIMIT_EXCEEDED: 'route_limit_exceeded',
61
+
62
+ // Timeout-related
63
+ NAVIGATION_TIMEOUT: 'navigation_timeout',
64
+ INTERACTION_TIMEOUT: 'interaction_timeout',
65
+ SETTLE_TIMEOUT: 'settle_timeout',
66
+ LOAD_TIMEOUT: 'load_timeout',
67
+
68
+ // Safety-related
69
+ DESTRUCTIVE_TEXT: 'destructive_text', // logout, delete, unsubscribe
70
+ EXTERNAL_NAVIGATION: 'external_navigation', // leaves origin
71
+ UNSAFE_PATTERN: 'unsafe_pattern',
72
+
73
+ // Incremental
74
+ INCREMENTAL_UNCHANGED: 'incremental_unchanged', // Previous run data reused
75
+
76
+ // Discovery
77
+ DISCOVERY_ERROR: 'discovery_error',
78
+ NO_MATCHING_SELECTOR: 'no_matching_selector',
79
+
80
+ // Expectation
81
+ NO_EXPECTATION: 'no_expectation', // Interaction found, no expectation to verify
82
+ EXPECTATION_NOT_REACHABLE: 'expectation_not_reachable',
83
+
84
+ // Navigation
85
+ EXTERNAL_BLOCKED: 'external_blocked',
86
+ ORIGIN_MISMATCH: 'origin_mismatch',
87
+
88
+ // Sensor
89
+ SENSOR_UNAVAILABLE: 'sensor_unavailable',
90
+ SENSOR_FAILED: 'sensor_failed',
91
+ };
92
+
93
+ /**
94
+ * SILENCE_TYPES - Technical classification of silence events (Phase 4)
95
+ * Maps to specific scenarios where interactions/promises cannot be evaluated
96
+ */
97
+ export const SILENCE_TYPES = {
98
+ // Promise not evaluated
99
+ INTERACTION_NOT_EXECUTED: 'interaction_not_executed', // User action blocked/skipped
100
+ PROMISE_NOT_EVALUATED: 'promise_not_evaluated', // Promise type cannot be inferred/assessed
101
+ PROMISE_VERIFICATION_BLOCKED: 'promise_verification_blocked', // Promise blocked by safety/policy
102
+
103
+ // Sensor/observation failures
104
+ SENSOR_FAILURE: 'sensor_failure', // Sensor unavailable or failed
105
+ SELECTOR_NOT_FOUND: 'selector_not_found', // Cannot find elements to interact with
106
+ DISCOVERY_FAILURE: 'discovery_failure', // Failed to discover page/routes/interactions
107
+
108
+ // Timing/resource constraints
109
+ INTERACTION_TIMEOUT: 'interaction_timeout', // Interaction timed out
110
+ NAVIGATION_TIMEOUT: 'navigation_timeout', // Navigation timed out
111
+ SETTLE_TIMEOUT: 'settle_timeout', // Settling/stabilization timed out
112
+
113
+ // Resource/policy constraints
114
+ BUDGET_LIMIT_EXCEEDED: 'budget_limit_exceeded', // Interaction/time budget exhausted
115
+ SAFETY_POLICY_BLOCK: 'safety_policy_block', // Blocked by safety rules (logout, delete, etc)
116
+ INCREMENTAL_REUSE: 'incremental_reuse', // Data from previous run reused
117
+ };
118
+
119
+ /**
120
+ * EVALUATION_STATUS - Lifecycle state of a silence (Phase 4)
121
+ * How the unobserved state should be interpreted
122
+ */
123
+ export const EVALUATION_STATUS = {
124
+ BLOCKED: 'blocked', // Intentionally blocked (safety policy, external nav)
125
+ AMBIGUOUS: 'ambiguous', // Cannot determine what would happen (selector, promise type unclear)
126
+ SKIPPED: 'skipped', // Deferred by policy (budget, incremental reuse)
127
+ TIMED_OUT: 'timed_out', // Exceeded time budget
128
+ INCOMPLETE: 'incomplete', // Partially evaluated (some expectations checked, others not)
129
+ };
130
+
131
+ /**
132
+ * SilenceTracker - Collects all silence entries across observe/detect phases
133
+ */
134
+ export class SilenceTracker {
135
+ constructor() {
136
+ this.entries = [];
137
+ this.byCategory = {};
138
+ this.byReason = {};
139
+ Object.keys(SILENCE_CATEGORIES).forEach(cat => {
140
+ this.byCategory[SILENCE_CATEGORIES[cat]] = [];
141
+ });
142
+ Object.keys(SILENCE_REASONS).forEach(reason => {
143
+ this.byReason[SILENCE_REASONS[reason]] = [];
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Record a silence/gap/unknown
149
+ * PHASE 4: Validates and infers lifecycle fields (silence_type, trigger, evaluation_status, promise)
150
+ * @param {SilenceEntry} entry
151
+ */
152
+ record(entry) {
153
+ if (!entry.scope || !entry.reason || !entry.description) {
154
+ throw new Error(`Invalid silence entry: missing required fields. Got: ${JSON.stringify(entry)}`);
155
+ }
156
+
157
+ // PHASE 2: Auto-compute and assign outcome if not provided
158
+ if (!entry.outcome) {
159
+ entry.outcome = mapSilenceReasonToOutcome(entry.reason);
160
+ }
161
+
162
+ // PHASE 4: Infer lifecycle fields if not provided
163
+ if (!entry.silence_type) {
164
+ entry.silence_type = this._inferSilenceType(entry.reason);
165
+ }
166
+ if (!entry.trigger) {
167
+ entry.trigger = this._inferTrigger(entry.reason);
168
+ }
169
+ if (!entry.evaluation_status) {
170
+ entry.evaluation_status = this._inferEvaluationStatus(entry.reason);
171
+ }
172
+ if (!entry.confidence_impact) {
173
+ entry.confidence_impact = this._inferConfidenceImpact(entry.reason, entry.scope);
174
+ }
175
+ // related_promise remains null with reason unless explicitly set
176
+ if (!entry.related_promise && entry.related_promise !== null) {
177
+ entry.related_promise = null; // Will be populated by verdict-engine if promise can be inferred
178
+ }
179
+
180
+ this.entries.push(entry);
181
+
182
+ // Track by category (infer from reason)
183
+ const category = this._getCategoryForReason(entry.reason);
184
+ if (category) {
185
+ this.byCategory[category].push(entry);
186
+ }
187
+
188
+ // Track by reason
189
+ if (this.byReason[entry.reason]) {
190
+ this.byReason[entry.reason].push(entry);
191
+ }
192
+ }
193
+
194
+ /**
195
+ * PHASE 4: Infer silence_type from reason
196
+ */
197
+ _inferSilenceType(reason) {
198
+ if (reason === SILENCE_REASONS.NAVIGATION_TIMEOUT || reason === SILENCE_REASONS.INTERACTION_TIMEOUT || reason === SILENCE_REASONS.SETTLE_TIMEOUT) {
199
+ return SILENCE_TYPES.INTERACTION_TIMEOUT;
200
+ }
201
+ if (reason === SILENCE_REASONS.LOAD_TIMEOUT) {
202
+ return SILENCE_TYPES.NAVIGATION_TIMEOUT;
203
+ }
204
+ if (reason === SILENCE_REASONS.DESTRUCTIVE_TEXT || reason === SILENCE_REASONS.UNSAFE_PATTERN) {
205
+ return SILENCE_TYPES.SAFETY_POLICY_BLOCK;
206
+ }
207
+ if (reason === SILENCE_REASONS.EXTERNAL_NAVIGATION || reason === SILENCE_REASONS.EXTERNAL_BLOCKED) {
208
+ return SILENCE_TYPES.PROMISE_VERIFICATION_BLOCKED;
209
+ }
210
+ if (reason.includes('budget') || reason.includes('limit') || reason.includes('exceeded')) {
211
+ return SILENCE_TYPES.BUDGET_LIMIT_EXCEEDED;
212
+ }
213
+ if (reason === SILENCE_REASONS.DISCOVERY_ERROR || reason === SILENCE_REASONS.NO_MATCHING_SELECTOR) {
214
+ return SILENCE_TYPES.DISCOVERY_FAILURE;
215
+ }
216
+ if (reason === SILENCE_REASONS.SENSOR_FAILED || reason === SILENCE_REASONS.SENSOR_UNAVAILABLE) {
217
+ return SILENCE_TYPES.SENSOR_FAILURE;
218
+ }
219
+ if (reason === SILENCE_REASONS.INCREMENTAL_UNCHANGED) {
220
+ return SILENCE_TYPES.INCREMENTAL_REUSE;
221
+ }
222
+ if (reason === SILENCE_REASONS.NO_EXPECTATION) {
223
+ return SILENCE_TYPES.PROMISE_NOT_EVALUATED;
224
+ }
225
+ return SILENCE_TYPES.PROMISE_NOT_EVALUATED; // Conservative default
226
+ }
227
+
228
+ /**
229
+ * PHASE 4: Infer trigger from reason
230
+ */
231
+ _inferTrigger(reason) {
232
+ const triggers = {
233
+ [SILENCE_REASONS.NAVIGATION_TIMEOUT]: 'navigation_timeout',
234
+ [SILENCE_REASONS.INTERACTION_TIMEOUT]: 'interaction_timeout',
235
+ [SILENCE_REASONS.SETTLE_TIMEOUT]: 'settle_timeout',
236
+ [SILENCE_REASONS.LOAD_TIMEOUT]: 'load_timeout',
237
+ [SILENCE_REASONS.DESTRUCTIVE_TEXT]: 'destructive_text_block',
238
+ [SILENCE_REASONS.EXTERNAL_NAVIGATION]: 'external_navigation',
239
+ [SILENCE_REASONS.EXTERNAL_BLOCKED]: 'external_origin_blocked',
240
+ [SILENCE_REASONS.UNSAFE_PATTERN]: 'unsafe_pattern_block',
241
+ [SILENCE_REASONS.SCAN_TIME_EXCEEDED]: 'scan_time_limit_exceeded',
242
+ [SILENCE_REASONS.PAGE_LIMIT_EXCEEDED]: 'page_limit_exceeded',
243
+ [SILENCE_REASONS.INTERACTION_LIMIT_EXCEEDED]: 'interaction_limit_exceeded',
244
+ [SILENCE_REASONS.ROUTE_LIMIT_EXCEEDED]: 'route_limit_exceeded',
245
+ [SILENCE_REASONS.DISCOVERY_ERROR]: 'discovery_failed',
246
+ [SILENCE_REASONS.NO_MATCHING_SELECTOR]: 'selector_not_found',
247
+ [SILENCE_REASONS.SENSOR_FAILED]: 'sensor_failure',
248
+ [SILENCE_REASONS.SENSOR_UNAVAILABLE]: 'sensor_unavailable',
249
+ [SILENCE_REASONS.INCREMENTAL_UNCHANGED]: 'incremental_data_reuse',
250
+ [SILENCE_REASONS.NO_EXPECTATION]: 'no_expectation_defined',
251
+ };
252
+ return triggers[reason] || reason;
253
+ }
254
+
255
+ /**
256
+ * PHASE 4: Infer evaluation_status from reason
257
+ */
258
+ _inferEvaluationStatus(reason) {
259
+ if (reason.includes('timeout')) {
260
+ return EVALUATION_STATUS.TIMED_OUT;
261
+ }
262
+ if (reason === SILENCE_REASONS.DESTRUCTIVE_TEXT || reason === SILENCE_REASONS.UNSAFE_PATTERN || reason === SILENCE_REASONS.EXTERNAL_NAVIGATION) {
263
+ return EVALUATION_STATUS.BLOCKED;
264
+ }
265
+ if (reason === SILENCE_REASONS.NO_EXPECTATION || reason === SILENCE_REASONS.NO_MATCHING_SELECTOR) {
266
+ return EVALUATION_STATUS.AMBIGUOUS;
267
+ }
268
+ if (reason === SILENCE_REASONS.INCREMENTAL_UNCHANGED || reason.includes('budget') || reason.includes('limit')) {
269
+ return EVALUATION_STATUS.SKIPPED;
270
+ }
271
+ return EVALUATION_STATUS.INCOMPLETE;
272
+ }
273
+
274
+ /**
275
+ * PHASE 4: Infer confidence impact from reason and scope
276
+ */
277
+ _inferConfidenceImpact(reason, _scope) {
278
+ // Map reason to which confidence metric(s) are affected
279
+ const impact = {
280
+ coverage: 0,
281
+ promise_verification: 0,
282
+ overall: 0
283
+ };
284
+
285
+ if (reason.includes('budget') || reason.includes('limit') || reason.includes('timeout')) {
286
+ impact.coverage = -5;
287
+ impact.promise_verification = -10;
288
+ impact.overall = -7;
289
+ }
290
+ if (reason === SILENCE_REASONS.DESTRUCTIVE_TEXT || reason === SILENCE_REASONS.UNSAFE_PATTERN) {
291
+ impact.promise_verification = -15; // Safety blocks reduce promise confidence most
292
+ impact.overall = -8;
293
+ }
294
+ if (reason === SILENCE_REASONS.DISCOVERY_ERROR || reason === SILENCE_REASONS.NO_MATCHING_SELECTOR) {
295
+ impact.coverage = -10;
296
+ impact.promise_verification = -5;
297
+ impact.overall = -8;
298
+ }
299
+ if (reason === SILENCE_REASONS.SENSOR_FAILED || reason === SILENCE_REASONS.SENSOR_UNAVAILABLE) {
300
+ impact.coverage = -15; // Sensor failures affect coverage most
301
+ impact.promise_verification = -10;
302
+ impact.overall = -12;
303
+ }
304
+ if (reason === SILENCE_REASONS.NO_EXPECTATION) {
305
+ impact.promise_verification = -3; // Minor impact - just ambiguous assertion
306
+ impact.overall = -1;
307
+ }
308
+
309
+ return impact;
310
+ }
311
+
312
+ /**
313
+ * Record multiple entries at once
314
+ */
315
+ recordBatch(entries) {
316
+ entries.forEach(e => this.record(e));
317
+ }
318
+
319
+ /**
320
+ * Get all entries in a category
321
+ */
322
+ getCategory(category) {
323
+ return this.byCategory[category] || [];
324
+ }
325
+
326
+ /**
327
+ * Get all entries for a reason
328
+ */
329
+ getReason(reason) {
330
+ return this.byReason[reason] || [];
331
+ }
332
+
333
+ /**
334
+ * PHASE 4: Query silences by type
335
+ */
336
+ getSilencesByType(silenceType) {
337
+ return this.entries.filter(e => e.silence_type === silenceType);
338
+ }
339
+
340
+ /**
341
+ * PHASE 4: Query silences by promise (only those with related_promise set)
342
+ */
343
+ getSilencesByPromise(promise) {
344
+ return this.entries.filter(e => e.related_promise && e.related_promise.type === promise.type);
345
+ }
346
+
347
+ /**
348
+ * PHASE 4: Query silences by evaluation status
349
+ */
350
+ getSilencesByEvalStatus(status) {
351
+ return this.entries.filter(e => e.evaluation_status === status);
352
+ }
353
+
354
+ /**
355
+ * PHASE 4: Aggregate confidence impact across all silences
356
+ */
357
+ getAggregatedConfidenceImpact() {
358
+ const total = {
359
+ coverage: 0,
360
+ promise_verification: 0,
361
+ overall: 0
362
+ };
363
+
364
+ this.entries.forEach(entry => {
365
+ if (entry.confidence_impact) {
366
+ total.coverage += entry.confidence_impact.coverage;
367
+ total.promise_verification += entry.confidence_impact.promise_verification;
368
+ total.overall += entry.confidence_impact.overall;
369
+ }
370
+ });
371
+
372
+ // Clamp to -100 to 0 range
373
+ return {
374
+ coverage: Math.max(-100, total.coverage),
375
+ promise_verification: Math.max(-100, total.promise_verification),
376
+ overall: Math.max(-100, total.overall)
377
+ };
378
+ }
379
+
380
+ /**
381
+ * PHASE 4: Get silences that affect promise verification
382
+ */
383
+ getPromiseVerificationBlockers() {
384
+ return this.entries.filter(e =>
385
+ e.evaluation_status === EVALUATION_STATUS.BLOCKED ||
386
+ e.evaluation_status === EVALUATION_STATUS.TIMED_OUT ||
387
+ (e.silence_type === SILENCE_TYPES.PROMISE_VERIFICATION_BLOCKED)
388
+ );
389
+ }
390
+
391
+ /**
392
+ * PHASE 4: Get silences that affect coverage
393
+ */
394
+ getCoverageGaps() {
395
+ return this.entries.filter(e =>
396
+ e.outcome === 'COVERAGE_GAP' ||
397
+ e.evaluation_status === EVALUATION_STATUS.SKIPPED ||
398
+ (e.silence_type === SILENCE_TYPES.BUDGET_LIMIT_EXCEEDED)
399
+ );
400
+ }
401
+
402
+ /**
403
+ * Get summary statistics
404
+ * PHASE 4: Added lifecycle metrics (type, status, promise association)
405
+ */
406
+ getSummary() {
407
+ const summary = {
408
+ totalSilences: this.entries.length,
409
+ byOutcome: {}, // PHASE 2: Added outcome grouping
410
+ byCategory: {},
411
+ byReason: {},
412
+ scopes: {},
413
+ // PHASE 4: Lifecycle metrics
414
+ byType: {},
415
+ byEvaluationStatus: {},
416
+ withPromiseAssociation: 0,
417
+ confidenceImpact: this.getAggregatedConfidenceImpact()
418
+ };
419
+
420
+ // Count by outcome (PHASE 2)
421
+ this.entries.forEach(entry => {
422
+ if (entry.outcome) {
423
+ summary.byOutcome[entry.outcome] = (summary.byOutcome[entry.outcome] || 0) + 1;
424
+ }
425
+ });
426
+
427
+ // Count by category
428
+ Object.entries(this.byCategory).forEach(([cat, entries]) => {
429
+ if (entries.length > 0) {
430
+ summary.byCategory[cat] = entries.length;
431
+ }
432
+ });
433
+
434
+ // Count by reason
435
+ Object.entries(this.byReason).forEach(([reason, entries]) => {
436
+ if (entries.length > 0) {
437
+ summary.byReason[reason] = entries.length;
438
+ }
439
+ });
440
+
441
+ // Count by scope
442
+ this.entries.forEach(entry => {
443
+ summary.scopes[entry.scope] = (summary.scopes[entry.scope] || 0) + 1;
444
+ });
445
+
446
+ // PHASE 4: Count by type and status
447
+ this.entries.forEach(entry => {
448
+ if (entry.silence_type) {
449
+ summary.byType[entry.silence_type] = (summary.byType[entry.silence_type] || 0) + 1;
450
+ }
451
+ if (entry.evaluation_status) {
452
+ summary.byEvaluationStatus[entry.evaluation_status] = (summary.byEvaluationStatus[entry.evaluation_status] || 0) + 1;
453
+ }
454
+ if (entry.related_promise) {
455
+ summary.withPromiseAssociation++;
456
+ }
457
+ });
458
+
459
+ return summary;
460
+ }
461
+
462
+ /**
463
+ * Get detailed summary for output
464
+ */
465
+ getDetailedSummary() {
466
+ return {
467
+ total: this.entries.length,
468
+ entries: this.entries,
469
+ summary: this.getSummary()
470
+ };
471
+ }
472
+
473
+ /**
474
+ * Export all silence data for serialization (used by writeTraces)
475
+ * PHASE 5: Entries sorted deterministically for replay consistency
476
+ */
477
+ export() {
478
+ // PHASE 5: Sort entries deterministically by: scope, reason, description
479
+ const sortedEntries = [...this.entries].sort((a, b) => {
480
+ if (a.scope !== b.scope) return a.scope.localeCompare(b.scope);
481
+ if (a.reason !== b.reason) return a.reason.localeCompare(b.reason);
482
+ return (a.description || '').localeCompare(b.description || '');
483
+ });
484
+
485
+ return {
486
+ total: sortedEntries.length,
487
+ entries: sortedEntries,
488
+ byCategory: this.byCategory,
489
+ byReason: this.byReason,
490
+ summary: this.getSummary()
491
+ };
492
+ }
493
+
494
+ _getCategoryForReason(reason) {
495
+ if (reason.includes('budget') || reason.includes('limit') || reason.includes('exceeded')) {
496
+ return SILENCE_CATEGORIES.BUDGET;
497
+ }
498
+ if (reason.includes('timeout')) {
499
+ return SILENCE_CATEGORIES.TIMEOUT;
500
+ }
501
+ if (reason.includes('destructive') || reason.includes('external') || reason.includes('unsafe')) {
502
+ return SILENCE_CATEGORIES.SAFETY;
503
+ }
504
+ if (reason.includes('incremental')) {
505
+ return SILENCE_CATEGORIES.INCREMENTAL;
506
+ }
507
+ if (reason.includes('discovery')) {
508
+ return SILENCE_CATEGORIES.DISCOVERY;
509
+ }
510
+ if (reason.includes('sensor')) {
511
+ return SILENCE_CATEGORIES.SENSOR;
512
+ }
513
+ if (reason.includes('expectation') || reason.includes('no_expectation')) {
514
+ return SILENCE_CATEGORIES.EXPECTATION;
515
+ }
516
+ if (reason.includes('navigation') || reason.includes('origin')) {
517
+ return SILENCE_CATEGORIES.NAVIGATION;
518
+ }
519
+ return null;
520
+ }
521
+ }
522
+
523
+ export default SilenceTracker;
@@ -1,6 +1,6 @@
1
1
  import { resolve } from 'path';
2
- import { existsSync, readdirSync, statSync } from 'fs';
3
2
  import { getUrlPath, getScreenshotHash } from './evidence-validator.js';
3
+ import { getScreenshotDir } from '../core/run-id.js';
4
4
 
5
5
  export function hasMeaningfulUrlChange(beforeUrl, afterUrl) {
6
6
  const beforePath = getUrlPath(beforeUrl);
@@ -16,40 +16,13 @@ export function hasMeaningfulUrlChange(beforeUrl, afterUrl) {
16
16
  return beforeNormalized !== afterNormalized;
17
17
  }
18
18
 
19
- export function hasVisibleChange(beforeScreenshot, afterScreenshot, projectDir) {
20
- // Screenshots are stored as relative paths like "screenshots/before-..."
21
- // Try new structure first (.verax/runs/<runId>/evidence/), then legacy
22
- let beforePath, afterPath;
23
- const newEvidencePath = resolve(projectDir, '.verax', 'runs');
24
- const legacyObservePath = resolve(projectDir, '.veraxverax', 'observe');
25
-
26
- // Check if new structure exists and find latest run
27
- if (existsSync(newEvidencePath)) {
28
- try {
29
- const runs = readdirSync(newEvidencePath)
30
- .map(name => ({ name, time: statSync(resolve(newEvidencePath, name)).mtimeMs }))
31
- .sort((a, b) => b.time - a.time);
32
- if (runs.length > 0) {
33
- const runEvidencePath = resolve(newEvidencePath, runs[0].name, 'evidence', beforeScreenshot);
34
- if (existsSync(runEvidencePath)) {
35
- beforePath = resolve(newEvidencePath, runs[0].name, 'evidence', beforeScreenshot);
36
- afterPath = resolve(newEvidencePath, runs[0].name, 'evidence', afterScreenshot);
37
- } else {
38
- beforePath = resolve(legacyObservePath, beforeScreenshot);
39
- afterPath = resolve(legacyObservePath, afterScreenshot);
40
- }
41
- } else {
42
- beforePath = resolve(legacyObservePath, beforeScreenshot);
43
- afterPath = resolve(legacyObservePath, afterScreenshot);
44
- }
45
- } catch {
46
- beforePath = resolve(legacyObservePath, beforeScreenshot);
47
- afterPath = resolve(legacyObservePath, afterScreenshot);
48
- }
49
- } else {
50
- beforePath = resolve(legacyObservePath, beforeScreenshot);
51
- afterPath = resolve(legacyObservePath, afterScreenshot);
19
+ export function hasVisibleChange(beforeScreenshot, afterScreenshot, projectDir, runId) {
20
+ if (!runId) {
21
+ throw new Error('runId is required for hasVisibleChange');
52
22
  }
23
+ const screenshotsDir = getScreenshotDir(projectDir, runId);
24
+ const beforePath = resolve(screenshotsDir, beforeScreenshot);
25
+ const afterPath = resolve(screenshotsDir, afterScreenshot);
53
26
 
54
27
  const beforeHash = getScreenshotHash(beforePath);
55
28
  const afterHash = getScreenshotHash(afterPath);