@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.
- package/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +14 -36
- package/src/cli/commands/default.js +523 -0
- package/src/cli/commands/doctor.js +165 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +402 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +296 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +34 -0
- package/src/cli/util/expectation-extractor.js +378 -0
- package/src/cli/util/findings-writer.js +31 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +366 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +29 -0
- package/src/cli/util/project-discovery.js +277 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/summary-writer.js +32 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +101 -0
- package/src/verax/cli/wizard.js +98 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +403 -0
- package/src/verax/core/incremental-store.js +237 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +521 -0
- package/src/verax/detect/comparison.js +2 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +177 -0
- package/src/verax/detect/expectation-model.js +194 -172
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +44 -8
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +172 -286
- package/src/verax/detect/interactive-findings.js +613 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/verdict-engine.js +563 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/index.js +90 -14
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +579 -0
- package/src/verax/intel/vue-router-extractor.js +323 -0
- package/src/verax/learn/action-contract-extractor.js +335 -101
- package/src/verax/learn/ast-contract-extractor.js +95 -5
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/manifest-writer.js +97 -47
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +27 -96
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +112 -4
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +10 -5
- package/src/verax/observe/console-sensor.js +1 -17
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +512 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +643 -275
- package/src/verax/observe/index.js +908 -27
- package/src/verax/observe/index.js.backup +1 -0
- package/src/verax/observe/interaction-discovery.js +365 -14
- package/src/verax/observe/interaction-runner.js +563 -198
- package/src/verax/observe/loading-sensor.js +139 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +37 -17
- package/src/verax/observe/state-sensor.js +389 -0
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +61 -20
- package/src/verax/observe/ui-signal-sensor.js +136 -17
- package/src/verax/scan-summary-writer.js +77 -15
- package/src/verax/shared/artifact-manager.js +110 -8
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +170 -0
- package/src/verax/shared/dynamic-route-utils.js +218 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +14 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +65 -0
- package/src/verax/validate/context-validator.js +244 -0
- 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
|
-
|
|
21
|
-
|
|
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);
|