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