@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.
- package/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +24 -36
- package/src/cli/commands/default.js +681 -0
- package/src/cli/commands/doctor.js +197 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +586 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +297 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +110 -0
- package/src/cli/util/expectation-extractor.js +388 -0
- package/src/cli/util/findings-writer.js +32 -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 +412 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +30 -0
- package/src/cli/util/project-discovery.js +297 -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/runtime-budget.js +147 -0
- package/src/cli/util/summary-writer.js +43 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -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 +111 -0
- package/src/verax/cli/wizard.js +109 -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 +432 -0
- package/src/verax/core/incremental-store.js +245 -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 +523 -0
- package/src/verax/detect/comparison.js +7 -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 +127 -0
- package/src/verax/detect/expectation-model.js +241 -168
- 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 +41 -12
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +200 -288
- package/src/verax/detect/interactive-findings.js +612 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/verdict-engine.js +561 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +103 -15
- 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 +642 -0
- package/src/verax/intel/vue-router-extractor.js +325 -0
- package/src/verax/learn/action-contract-extractor.js +338 -104
- package/src/verax/learn/ast-contract-extractor.js +148 -6
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +122 -58
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +28 -97
- package/src/verax/learn/route-validator.js +8 -7
- 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 +119 -10
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +30 -6
- package/src/verax/observe/console-sensor.js +2 -18
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +513 -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 +660 -273
- package/src/verax/observe/index.js +910 -26
- package/src/verax/observe/interaction-discovery.js +378 -15
- package/src/verax/observe/interaction-runner.js +562 -197
- package/src/verax/observe/loading-sensor.js +145 -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 +38 -17
- package/src/verax/observe/state-sensor.js +393 -0
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +73 -21
- package/src/verax/observe/ui-signal-sensor.js +143 -17
- package/src/verax/scan-summary-writer.js +80 -15
- package/src/verax/shared/artifact-manager.js +111 -9
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +169 -0
- package/src/verax/shared/dynamic-route-utils.js +224 -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 +9 -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 +66 -0
- package/src/verax/validate/context-validator.js +244 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 5: DETERMINISTIC RUN IDENTIFICATION
|
|
3
|
+
*
|
|
4
|
+
* Generates stable, deterministic runId based on run parameters (NOT timestamps).
|
|
5
|
+
* Provides utilities for artifact path resolution.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { readFileSync } from 'fs';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { dirname } from 'path';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get VERAX version from package.json
|
|
19
|
+
*/
|
|
20
|
+
export function getVeraxVersion() {
|
|
21
|
+
try {
|
|
22
|
+
const packagePath = join(__dirname, '..', '..', '..', 'package.json');
|
|
23
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
24
|
+
return pkg.version || '0.1.0';
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return '0.1.0';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generate deterministic runId from run parameters
|
|
32
|
+
*
|
|
33
|
+
* CRITICAL: NO timestamps, NO random values
|
|
34
|
+
* Hash must be identical for identical inputs
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} params - Run parameters
|
|
37
|
+
* @param {string} params.url - Target URL
|
|
38
|
+
* @param {Object} params.safetyFlags - Safety flags (allowWrites, allowRiskyActions, allowCrossOrigin)
|
|
39
|
+
* @param {string} params.baseOrigin - Base origin
|
|
40
|
+
* @param {Object} params.scanBudget - Scan budget configuration
|
|
41
|
+
* @param {string} params.manifestPath - Optional manifest path
|
|
42
|
+
* @returns {string} Deterministic runId (hex hash)
|
|
43
|
+
*/
|
|
44
|
+
export function generateRunId(params) {
|
|
45
|
+
const { url, safetyFlags = {}, baseOrigin, scanBudget, manifestPath = null } = params;
|
|
46
|
+
|
|
47
|
+
// Sort flags deterministically
|
|
48
|
+
const sortedFlags = {
|
|
49
|
+
allowCrossOrigin: safetyFlags.allowCrossOrigin || false,
|
|
50
|
+
allowRiskyActions: safetyFlags.allowRiskyActions || false,
|
|
51
|
+
allowWrites: safetyFlags.allowWrites || false
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Create deterministic representation
|
|
55
|
+
const runConfig = {
|
|
56
|
+
url,
|
|
57
|
+
flags: sortedFlags,
|
|
58
|
+
baseOrigin,
|
|
59
|
+
budget: {
|
|
60
|
+
maxScanDurationMs: scanBudget.maxScanDurationMs,
|
|
61
|
+
maxInteractionsPerPage: scanBudget.maxInteractionsPerPage,
|
|
62
|
+
maxUniqueUrls: scanBudget.maxUniqueUrls,
|
|
63
|
+
interactionTimeoutMs: scanBudget.interactionTimeoutMs,
|
|
64
|
+
navigationTimeoutMs: scanBudget.navigationTimeoutMs
|
|
65
|
+
},
|
|
66
|
+
manifestPath,
|
|
67
|
+
veraxVersion: getVeraxVersion()
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Generate stable hash
|
|
71
|
+
// Sort keys at all levels for deterministic serialization
|
|
72
|
+
const configString = JSON.stringify(runConfig, (key, value) => {
|
|
73
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
74
|
+
return Object.keys(value).sort().reduce((sorted, k) => {
|
|
75
|
+
sorted[k] = value[k];
|
|
76
|
+
return sorted;
|
|
77
|
+
}, {});
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
});
|
|
81
|
+
const hash = createHash('sha256').update(configString).digest('hex');
|
|
82
|
+
|
|
83
|
+
// Return first 16 chars for readability (still collision-resistant)
|
|
84
|
+
return hash.substring(0, 16);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get artifact directory for a run
|
|
89
|
+
*
|
|
90
|
+
* @param {string} projectDir - Project directory
|
|
91
|
+
* @param {string} runId - Run identifier
|
|
92
|
+
* @returns {string} Absolute path to run artifact directory
|
|
93
|
+
*/
|
|
94
|
+
export function getRunArtifactDir(projectDir, runId) {
|
|
95
|
+
return join(projectDir, '.verax', 'runs', runId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get artifact file path
|
|
100
|
+
*
|
|
101
|
+
* @param {string} projectDir - Project directory
|
|
102
|
+
* @param {string} runId - Run identifier
|
|
103
|
+
* @param {string} artifactName - Artifact filename (e.g., 'traces.json', 'findings.json')
|
|
104
|
+
* @returns {string} Absolute path to artifact file
|
|
105
|
+
*/
|
|
106
|
+
export function getArtifactPath(projectDir, runId, artifactName) {
|
|
107
|
+
return join(getRunArtifactDir(projectDir, runId), artifactName);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get screenshot directory path
|
|
112
|
+
*
|
|
113
|
+
* @param {string} projectDir - Project directory
|
|
114
|
+
* @param {string} runId - Run identifier
|
|
115
|
+
* @returns {string} Absolute path to screenshots directory
|
|
116
|
+
*/
|
|
117
|
+
export function getScreenshotDir(projectDir, runId) {
|
|
118
|
+
return join(getRunArtifactDir(projectDir, runId), 'evidence', 'screenshots');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get all expected artifact paths for a run
|
|
123
|
+
*
|
|
124
|
+
* @param {string} projectDir - Project directory
|
|
125
|
+
* @param {string} runId - Run identifier
|
|
126
|
+
* @returns {Object} Map of artifact names to paths
|
|
127
|
+
*/
|
|
128
|
+
export function getExpectedArtifacts(projectDir, runId) {
|
|
129
|
+
const runDir = getRunArtifactDir(projectDir, runId);
|
|
130
|
+
return {
|
|
131
|
+
runManifest: join(runDir, 'run-manifest.json'),
|
|
132
|
+
manifest: join(runDir, 'manifest.json'),
|
|
133
|
+
traces: join(runDir, 'traces.json'),
|
|
134
|
+
findings: join(runDir, 'findings.json'),
|
|
135
|
+
silences: join(runDir, 'silences.json'),
|
|
136
|
+
screenshotDir: join(runDir, 'evidence', 'screenshots')
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Compute SHA256 hash of a file
|
|
142
|
+
*
|
|
143
|
+
* @param {string} filePath - Path to file
|
|
144
|
+
* @returns {string} Hex hash
|
|
145
|
+
*/
|
|
146
|
+
export function computeFileHash(filePath) {
|
|
147
|
+
try {
|
|
148
|
+
const content = readFileSync(filePath);
|
|
149
|
+
return createHash('sha256').update(content).digest('hex');
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Compute hashes for all artifacts in a run
|
|
157
|
+
*
|
|
158
|
+
* @param {string} projectDir - Project directory
|
|
159
|
+
* @param {string} runId - Run identifier
|
|
160
|
+
* @returns {Object} Map of artifact names to hashes
|
|
161
|
+
*/
|
|
162
|
+
export function computeArtifactHashes(projectDir, runId) {
|
|
163
|
+
const artifacts = getExpectedArtifacts(projectDir, runId);
|
|
164
|
+
const hashes = {};
|
|
165
|
+
|
|
166
|
+
for (const [name, path] of Object.entries(artifacts)) {
|
|
167
|
+
if (name === 'screenshotDir') continue; // Directory, not file
|
|
168
|
+
const hash = computeFileHash(path);
|
|
169
|
+
if (hash) {
|
|
170
|
+
hashes[name] = hash;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return hashes;
|
|
175
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 5: RUN MANIFEST
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for run metadata.
|
|
5
|
+
* Written FIRST, referenced by all later stages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { writeFileSync, mkdirSync, readFileSync } from 'fs';
|
|
9
|
+
import { getRunArtifactDir, getVeraxVersion } from './run-id.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create run manifest at start of execution
|
|
13
|
+
*
|
|
14
|
+
* @param {string} projectDir - Project directory
|
|
15
|
+
* @param {string} runId - Run identifier (deterministic)
|
|
16
|
+
* @param {Object} params - Run parameters
|
|
17
|
+
* @param {string} params.url - Target URL
|
|
18
|
+
* @param {Object} params.safetyFlags - Safety flags
|
|
19
|
+
* @param {string} params.baseOrigin - Base origin
|
|
20
|
+
* @param {Object} params.scanBudget - Scan budget
|
|
21
|
+
* @param {string} params.manifestPath - Optional manifest path
|
|
22
|
+
* @param {Array<string>} params.argv - Command line arguments
|
|
23
|
+
* @returns {Object} Run manifest data
|
|
24
|
+
*/
|
|
25
|
+
export function createRunManifest(projectDir, runId, params) {
|
|
26
|
+
const { url, safetyFlags, baseOrigin, scanBudget, manifestPath, argv = [] } = params;
|
|
27
|
+
|
|
28
|
+
const runManifest = {
|
|
29
|
+
runId,
|
|
30
|
+
veraxVersion: getVeraxVersion(),
|
|
31
|
+
nodeVersion: process.version,
|
|
32
|
+
// Playwright version would be detected from node_modules if needed
|
|
33
|
+
playwrightVersion: 'latest',
|
|
34
|
+
url,
|
|
35
|
+
baseOrigin,
|
|
36
|
+
flags: {
|
|
37
|
+
allowWrites: safetyFlags.allowWrites || false,
|
|
38
|
+
allowRiskyActions: safetyFlags.allowRiskyActions || false,
|
|
39
|
+
allowCrossOrigin: safetyFlags.allowCrossOrigin || false
|
|
40
|
+
},
|
|
41
|
+
safeMode: {
|
|
42
|
+
enabled: !safetyFlags.allowWrites || !safetyFlags.allowRiskyActions || !safetyFlags.allowCrossOrigin,
|
|
43
|
+
writesBlocked: !safetyFlags.allowWrites,
|
|
44
|
+
riskyActionsBlocked: !safetyFlags.allowRiskyActions,
|
|
45
|
+
crossOriginBlocked: !safetyFlags.allowCrossOrigin
|
|
46
|
+
},
|
|
47
|
+
budget: {
|
|
48
|
+
maxScanDurationMs: scanBudget.maxScanDurationMs,
|
|
49
|
+
maxInteractionsPerPage: scanBudget.maxInteractionsPerPage,
|
|
50
|
+
maxUniqueUrls: scanBudget.maxUniqueUrls,
|
|
51
|
+
interactionTimeoutMs: scanBudget.interactionTimeoutMs,
|
|
52
|
+
navigationTimeoutMs: scanBudget.navigationTimeoutMs,
|
|
53
|
+
stabilizationWindowMs: scanBudget.stabilizationWindowMs,
|
|
54
|
+
stabilizationSampleMidMs: scanBudget.stabilizationSampleMidMs,
|
|
55
|
+
stabilizationSampleEndMs: scanBudget.stabilizationSampleEndMs,
|
|
56
|
+
networkWaitMs: scanBudget.networkWaitMs,
|
|
57
|
+
settleTimeoutMs: scanBudget.settleTimeoutMs,
|
|
58
|
+
settleIdleMs: scanBudget.settleIdleMs,
|
|
59
|
+
settleDomStableMs: scanBudget.settleDomStableMs,
|
|
60
|
+
navigationStableWaitMs: scanBudget.navigationStableWaitMs
|
|
61
|
+
},
|
|
62
|
+
manifestPath,
|
|
63
|
+
command: argv.join(' '),
|
|
64
|
+
argv,
|
|
65
|
+
startTime: new Date().toISOString(),
|
|
66
|
+
artifactHashes: {} // Will be populated after artifacts are written
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Ensure run directory exists
|
|
70
|
+
const runDir = getRunArtifactDir(projectDir, runId);
|
|
71
|
+
mkdirSync(runDir, { recursive: true });
|
|
72
|
+
|
|
73
|
+
// Write run manifest
|
|
74
|
+
const runManifestPath = `${runDir}/run-manifest.json`;
|
|
75
|
+
writeFileSync(runManifestPath, JSON.stringify(runManifest, null, 2));
|
|
76
|
+
|
|
77
|
+
return runManifest;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Update run manifest with artifact hashes
|
|
82
|
+
*
|
|
83
|
+
* @param {string} projectDir - Project directory
|
|
84
|
+
* @param {string} runId - Run identifier
|
|
85
|
+
* @param {Object} hashes - Map of artifact names to hashes
|
|
86
|
+
*/
|
|
87
|
+
export function updateRunManifestHashes(projectDir, runId, hashes) {
|
|
88
|
+
const runDir = getRunArtifactDir(projectDir, runId);
|
|
89
|
+
const runManifestPath = `${runDir}/run-manifest.json`;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const runManifest = JSON.parse(readFileSync(runManifestPath, 'utf-8'));
|
|
93
|
+
runManifest.artifactHashes = hashes;
|
|
94
|
+
runManifest.endTime = new Date().toISOString();
|
|
95
|
+
writeFileSync(runManifestPath, JSON.stringify(runManifest, null, 2));
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// Run manifest not found or invalid - continue without update
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SILENCE IMPACT ACCOUNTING
|
|
3
|
+
*
|
|
4
|
+
* PHASE 4: Quantify how silence (unobserved/unevaluated states) affects confidence in our observations.
|
|
5
|
+
*
|
|
6
|
+
* PRINCIPLES:
|
|
7
|
+
* 1. Silence is never neutral - it reduces confidence in what we claim to observe
|
|
8
|
+
* 2. Different types of silence have different confidence impacts
|
|
9
|
+
* 3. Aggregate impacts show what confidence metric is weakened and by how much
|
|
10
|
+
* 4. Confidence impact is ALWAYS negative (silence cannot increase confidence)
|
|
11
|
+
* 5. Impacts are factual: based on type/scope, not on hypothetical outcomes
|
|
12
|
+
*
|
|
13
|
+
* Three confidence metrics affected by silence:
|
|
14
|
+
* - Coverage Confidence: How much of the codebase did we actually observe?
|
|
15
|
+
* - Promise Verification Confidence: How certain are we about promise verification?
|
|
16
|
+
* - Overall Observation Confidence: General confidence in observation completeness
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { SILENCE_TYPES, EVALUATION_STATUS } from './silence-model.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* SILENCE_IMPACT_PROFILES - How different silence types affect confidence
|
|
23
|
+
*
|
|
24
|
+
* Each profile defines the impact on three confidence dimensions.
|
|
25
|
+
* All values are NEGATIVE (silence reduces confidence).
|
|
26
|
+
*/
|
|
27
|
+
export const SILENCE_IMPACT_PROFILES = {
|
|
28
|
+
// Observation Infrastructure Failures - CRITICAL
|
|
29
|
+
// Sensor failure means we lost observability entirely for that area
|
|
30
|
+
[SILENCE_TYPES.SENSOR_FAILURE]: {
|
|
31
|
+
name: 'Sensor Failure',
|
|
32
|
+
severity: 'critical',
|
|
33
|
+
coverage: -20, // Major reduction: lost observability
|
|
34
|
+
promise_verification: -15, // Cannot verify promises without observation
|
|
35
|
+
overall: -18, // Very high impact
|
|
36
|
+
reasoning: 'Observation infrastructure failure - no data collected for affected area'
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
[SILENCE_TYPES.DISCOVERY_FAILURE]: {
|
|
40
|
+
name: 'Discovery Failure',
|
|
41
|
+
severity: 'critical',
|
|
42
|
+
coverage: -15, // Major reduction: didn't find items to evaluate
|
|
43
|
+
promise_verification: -10, // Unknown what promises existed
|
|
44
|
+
overall: -13, // Very high impact
|
|
45
|
+
reasoning: 'Could not discover items - evaluation incomplete for unknown scope'
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// Timing Failures - HIGH
|
|
49
|
+
// Timeouts mean interaction couldn't complete, promise unverified
|
|
50
|
+
[SILENCE_TYPES.NAVIGATION_TIMEOUT]: {
|
|
51
|
+
name: 'Navigation Timeout',
|
|
52
|
+
severity: 'high',
|
|
53
|
+
coverage: -10, // Moderate reduction: page not reachable
|
|
54
|
+
promise_verification: -20, // Complete failure for navigation promise
|
|
55
|
+
overall: -15, // High impact
|
|
56
|
+
reasoning: 'Navigation could not complete - route unreachable or page unresponsive'
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
[SILENCE_TYPES.INTERACTION_TIMEOUT]: {
|
|
60
|
+
name: 'Interaction Timeout',
|
|
61
|
+
severity: 'high',
|
|
62
|
+
coverage: -8, // Moderate reduction
|
|
63
|
+
promise_verification: -18, // Interaction promise verification failed
|
|
64
|
+
overall: -13, // High impact
|
|
65
|
+
reasoning: 'Interaction did not complete within time budget - outcome unknown'
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// Policy-Blocked Evaluation - MEDIUM
|
|
69
|
+
// Safety blocks prevent evaluation, but promise is known to exist
|
|
70
|
+
[SILENCE_TYPES.SAFETY_POLICY_BLOCK]: {
|
|
71
|
+
name: 'Safety Policy Block',
|
|
72
|
+
severity: 'medium',
|
|
73
|
+
coverage: -3, // Low reduction: coverage by skipping is intentional
|
|
74
|
+
promise_verification: -25, // Critical: promise cannot be verified due to safety policy
|
|
75
|
+
overall: -12, // Medium-high impact
|
|
76
|
+
reasoning: 'Promise verification blocked by safety policy (logout, destructive action) - cannot assert promise due to risk'
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
[SILENCE_TYPES.PROMISE_VERIFICATION_BLOCKED]: {
|
|
80
|
+
name: 'Promise Verification Blocked',
|
|
81
|
+
severity: 'medium',
|
|
82
|
+
coverage: -2, // Very low reduction: blocked verification only
|
|
83
|
+
promise_verification: -22, // Cannot verify due to external navigation or origin mismatch
|
|
84
|
+
overall: -10, // Medium impact
|
|
85
|
+
reasoning: 'Promise verification blocked - navigation leaves origin or enters external site'
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// Resource Constraints - MEDIUM
|
|
89
|
+
// Budget limits mean we didn't finish evaluation
|
|
90
|
+
[SILENCE_TYPES.BUDGET_LIMIT_EXCEEDED]: {
|
|
91
|
+
name: 'Budget Limit Exceeded',
|
|
92
|
+
severity: 'medium',
|
|
93
|
+
coverage: -12, // Moderate reduction: didn't evaluate all items
|
|
94
|
+
promise_verification: -8, // Some promises verified, others not
|
|
95
|
+
overall: -10, // Medium impact
|
|
96
|
+
reasoning: 'Evaluation terminated due to time/interaction budget - remaining items not evaluated'
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// Data Reuse - LOW
|
|
100
|
+
// Incremental reuse is safe by design (previous run passed)
|
|
101
|
+
[SILENCE_TYPES.INCREMENTAL_REUSE]: {
|
|
102
|
+
name: 'Incremental Reuse',
|
|
103
|
+
severity: 'low',
|
|
104
|
+
coverage: 0, // No impact: explicitly reusing validated baseline
|
|
105
|
+
promise_verification: 0, // Verified in previous run
|
|
106
|
+
overall: 0, // Intentional optimization
|
|
107
|
+
reasoning: 'Data from previous run reused - baseline still valid'
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// Ambiguous/Incomplete - LOW
|
|
111
|
+
// No expectation defined = no promise to verify anyway
|
|
112
|
+
[SILENCE_TYPES.PROMISE_NOT_EVALUATED]: {
|
|
113
|
+
name: 'Promise Not Evaluated',
|
|
114
|
+
severity: 'low',
|
|
115
|
+
coverage: 0, // No impact: covered but no expectation
|
|
116
|
+
promise_verification: -2, // Minor: cannot assert promise without expectation
|
|
117
|
+
overall: -1, // Minimal impact
|
|
118
|
+
reasoning: 'Interaction found but no expectation defined - cannot verify promise'
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Compute confidence impact for a single silence entry
|
|
124
|
+
*
|
|
125
|
+
* @param {Object} silence - SilenceEntry with silence_type, evaluation_status, context
|
|
126
|
+
* @returns {Object} Impact: { coverage: number, promise_verification: number, overall: number }
|
|
127
|
+
*/
|
|
128
|
+
export function computeSilenceImpact(silence) {
|
|
129
|
+
if (!silence) {
|
|
130
|
+
return { coverage: 0, promise_verification: 0, overall: 0 };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const profile = SILENCE_IMPACT_PROFILES[silence.silence_type];
|
|
134
|
+
|
|
135
|
+
if (!profile) {
|
|
136
|
+
// Unknown silence type - be conservative
|
|
137
|
+
return {
|
|
138
|
+
coverage: -5,
|
|
139
|
+
promise_verification: -5,
|
|
140
|
+
overall: -5,
|
|
141
|
+
unknown_type: silence.silence_type
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Start with base profile
|
|
146
|
+
let impact = {
|
|
147
|
+
coverage: profile.coverage,
|
|
148
|
+
promise_verification: profile.promise_verification,
|
|
149
|
+
overall: profile.overall
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Adjust for evaluation status (some statuses are worse than others)
|
|
153
|
+
if (silence.evaluation_status === EVALUATION_STATUS.BLOCKED) {
|
|
154
|
+
// Blocked state: intentional, but still reduces confidence
|
|
155
|
+
// Slightly less impact than timeout
|
|
156
|
+
impact.promise_verification = Math.max(-20, impact.promise_verification + 2);
|
|
157
|
+
} else if (silence.evaluation_status === EVALUATION_STATUS.TIMED_OUT) {
|
|
158
|
+
// Timed out: worse than blocked (unintentional failure)
|
|
159
|
+
impact.promise_verification = Math.min(-25, impact.promise_verification - 2);
|
|
160
|
+
} else if (silence.evaluation_status === EVALUATION_STATUS.AMBIGUOUS) {
|
|
161
|
+
// Ambiguous: cannot assert but also didn't try
|
|
162
|
+
impact.promise_verification = Math.max(-5, impact.promise_verification + 3);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Clamp to reasonable ranges
|
|
166
|
+
impact.coverage = Math.max(-100, impact.coverage);
|
|
167
|
+
impact.promise_verification = Math.max(-100, impact.promise_verification);
|
|
168
|
+
impact.overall = Math.max(-100, impact.overall);
|
|
169
|
+
|
|
170
|
+
return impact;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Aggregate impacts from multiple silences
|
|
175
|
+
*
|
|
176
|
+
* RULE: Impacts are cumulative but clamped at -100 (cannot be worse than complete loss)
|
|
177
|
+
*
|
|
178
|
+
* @param {Array} silences - Array of SilenceEntry objects
|
|
179
|
+
* @returns {Object} Aggregated impact with clamping
|
|
180
|
+
*/
|
|
181
|
+
export function aggregateSilenceImpacts(silences) {
|
|
182
|
+
const total = {
|
|
183
|
+
coverage: 0,
|
|
184
|
+
promise_verification: 0,
|
|
185
|
+
overall: 0
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (!silences || silences.length === 0) {
|
|
189
|
+
return total;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const silence of silences) {
|
|
193
|
+
const impact = computeSilenceImpact(silence);
|
|
194
|
+
total.coverage += impact.coverage;
|
|
195
|
+
total.promise_verification += impact.promise_verification;
|
|
196
|
+
total.overall += impact.overall;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Clamp to -100 to 0 range
|
|
200
|
+
return {
|
|
201
|
+
coverage: Math.max(-100, total.coverage),
|
|
202
|
+
promise_verification: Math.max(-100, total.promise_verification),
|
|
203
|
+
overall: Math.max(-100, total.overall)
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Categorize impacts by severity
|
|
209
|
+
*
|
|
210
|
+
* @param {Array} silences - Array of SilenceEntry objects
|
|
211
|
+
* @returns {Object} Impacts organized by severity
|
|
212
|
+
*/
|
|
213
|
+
export function categorizeSilencesByImpactSeverity(silences) {
|
|
214
|
+
const critical = [];
|
|
215
|
+
const high = [];
|
|
216
|
+
const medium = [];
|
|
217
|
+
const low = [];
|
|
218
|
+
|
|
219
|
+
if (!silences) return { critical, high, medium, low };
|
|
220
|
+
|
|
221
|
+
for (const silence of silences) {
|
|
222
|
+
const profile = SILENCE_IMPACT_PROFILES[silence.silence_type];
|
|
223
|
+
if (!profile) {
|
|
224
|
+
high.push(silence); // Default to high for unknown types
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
switch (profile.severity) {
|
|
229
|
+
case 'critical':
|
|
230
|
+
critical.push(silence);
|
|
231
|
+
break;
|
|
232
|
+
case 'high':
|
|
233
|
+
high.push(silence);
|
|
234
|
+
break;
|
|
235
|
+
case 'medium':
|
|
236
|
+
medium.push(silence);
|
|
237
|
+
break;
|
|
238
|
+
case 'low':
|
|
239
|
+
low.push(silence);
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { critical, high, medium, low };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Compute impact summary for output
|
|
249
|
+
*
|
|
250
|
+
* Shows:
|
|
251
|
+
* - Total impact by dimension
|
|
252
|
+
* - Most impactful silence types
|
|
253
|
+
* - Silences affecting each dimension
|
|
254
|
+
*
|
|
255
|
+
* @param {Array} silences - Array of SilenceEntry objects
|
|
256
|
+
* @returns {Object} Detailed impact summary
|
|
257
|
+
*/
|
|
258
|
+
export function createImpactSummary(silences) {
|
|
259
|
+
if (!silences || silences.length === 0) {
|
|
260
|
+
return {
|
|
261
|
+
total_silences: 0,
|
|
262
|
+
aggregated_impact: { coverage: 0, promise_verification: 0, overall: 0 },
|
|
263
|
+
by_severity: { critical: 0, high: 0, medium: 0, low: 0 },
|
|
264
|
+
most_impactful_types: [],
|
|
265
|
+
affected_dimensions: {
|
|
266
|
+
coverage: [],
|
|
267
|
+
promise_verification: [],
|
|
268
|
+
overall: []
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const aggregated = aggregateSilenceImpacts(silences);
|
|
274
|
+
const bySeverity = categorizeSilencesByImpactSeverity(silences);
|
|
275
|
+
|
|
276
|
+
// Find most impactful types
|
|
277
|
+
const typeImpacts = {};
|
|
278
|
+
for (const silence of silences) {
|
|
279
|
+
const impact = computeSilenceImpact(silence);
|
|
280
|
+
if (!typeImpacts[silence.silence_type]) {
|
|
281
|
+
typeImpacts[silence.silence_type] = { count: 0, total_impact: 0 };
|
|
282
|
+
}
|
|
283
|
+
typeImpacts[silence.silence_type].count++;
|
|
284
|
+
typeImpacts[silence.silence_type].total_impact += impact.overall;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const mostImpactful = Object.entries(typeImpacts)
|
|
288
|
+
.map(([type, data]) => ({
|
|
289
|
+
type,
|
|
290
|
+
count: data.count,
|
|
291
|
+
average_impact: Math.round(data.total_impact / data.count),
|
|
292
|
+
total_impact: data.total_impact
|
|
293
|
+
}))
|
|
294
|
+
.sort((a, b) => a.total_impact - b.total_impact)
|
|
295
|
+
.slice(0, 5);
|
|
296
|
+
|
|
297
|
+
// Find silences affecting each dimension most
|
|
298
|
+
const affectedDimensions = {
|
|
299
|
+
coverage: silences
|
|
300
|
+
.filter(s => computeSilenceImpact(s).coverage < 0)
|
|
301
|
+
.sort((a, b) => computeSilenceImpact(a).coverage - computeSilenceImpact(b).coverage)
|
|
302
|
+
.slice(0, 3)
|
|
303
|
+
.map(s => s.silence_type),
|
|
304
|
+
promise_verification: silences
|
|
305
|
+
.filter(s => computeSilenceImpact(s).promise_verification < 0)
|
|
306
|
+
.sort((a, b) => computeSilenceImpact(a).promise_verification - computeSilenceImpact(b).promise_verification)
|
|
307
|
+
.slice(0, 3)
|
|
308
|
+
.map(s => s.silence_type),
|
|
309
|
+
overall: silences
|
|
310
|
+
.filter(s => computeSilenceImpact(s).overall < 0)
|
|
311
|
+
.sort((a, b) => computeSilenceImpact(a).overall - computeSilenceImpact(b).overall)
|
|
312
|
+
.slice(0, 3)
|
|
313
|
+
.map(s => s.silence_type)
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
total_silences: silences.length,
|
|
318
|
+
aggregated_impact: aggregated,
|
|
319
|
+
by_severity: {
|
|
320
|
+
critical: bySeverity.critical.length,
|
|
321
|
+
high: bySeverity.high.length,
|
|
322
|
+
medium: bySeverity.medium.length,
|
|
323
|
+
low: bySeverity.low.length
|
|
324
|
+
},
|
|
325
|
+
most_impactful_types: mostImpactful,
|
|
326
|
+
affected_dimensions: affectedDimensions,
|
|
327
|
+
confidence_interpretation: generateConfidenceInterpretation(aggregated)
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Generate human-readable interpretation of confidence impacts
|
|
333
|
+
*
|
|
334
|
+
* @param {Object} aggregated - Aggregated impact object
|
|
335
|
+
* @returns {string} Human-readable interpretation
|
|
336
|
+
*/
|
|
337
|
+
function generateConfidenceInterpretation(aggregated) {
|
|
338
|
+
const { coverage: _coverage, promise_verification: _promise_verification, overall } = aggregated;
|
|
339
|
+
|
|
340
|
+
if (overall === 0) {
|
|
341
|
+
return 'No silence events - observation confidence is complete within evaluated scope';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (overall <= -80) {
|
|
345
|
+
return 'CRITICAL: Observation significantly incomplete - major silence events limit what we can assert';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (overall <= -50) {
|
|
349
|
+
return 'SIGNIFICANT: Multiple silence events reduce observation confidence - substantial unknowns remain';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (overall <= -25) {
|
|
353
|
+
return 'MODERATE: Some silence events reduce observation completeness - some unknowns remain';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (overall <= -10) {
|
|
357
|
+
return 'MINOR: Few silence events slightly reduce confidence - observation mostly complete';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return 'Very minor impact from silence events';
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export default {
|
|
364
|
+
SILENCE_IMPACT_PROFILES,
|
|
365
|
+
computeSilenceImpact,
|
|
366
|
+
aggregateSilenceImpacts,
|
|
367
|
+
categorizeSilencesByImpactSeverity,
|
|
368
|
+
createImpactSummary
|
|
369
|
+
};
|