@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
|
@@ -1,8 +1,36 @@
|
|
|
1
1
|
import { resolve } from 'path';
|
|
2
2
|
import { writeFileSync, mkdirSync } from 'fs';
|
|
3
|
-
import {
|
|
3
|
+
import { getArtifactPath, getRunArtifactDir } from '../core/run-id.js';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* SILENCE TRACKING: Write observation traces with explicit silence tracking.
|
|
7
|
+
* All gaps, skips, caps, and unknowns must be recorded and surfaced.
|
|
8
|
+
*
|
|
9
|
+
* PHASE 5: Writes to deterministic artifact path .verax/runs/<runId>/traces.json
|
|
10
|
+
*
|
|
11
|
+
* @param {string} projectDir - Project directory
|
|
12
|
+
* @param {string} url - URL observed
|
|
13
|
+
* @param {Array} traces - Execution traces
|
|
14
|
+
* @param {Object} coverage - Coverage data (if capped, this is a silence)
|
|
15
|
+
* @param {Array} warnings - Warnings (caps are silences)
|
|
16
|
+
* @param {Array} observedExpectations - Observed expectations
|
|
17
|
+
* @param {Object} silenceTracker - Silence tracker (optional)
|
|
18
|
+
* @param {string} runId - Run identifier (Phase 5)
|
|
19
|
+
*/
|
|
20
|
+
export function writeTraces(projectDir, url, traces, coverage = null, warnings = [], observedExpectations = [], silenceTracker = null, runId = null) {
|
|
21
|
+
// PHASE 5: Use deterministic artifact path if runId provided, otherwise fall back to old path
|
|
22
|
+
let observeDir, tracesPath;
|
|
23
|
+
if (runId) {
|
|
24
|
+
observeDir = getRunArtifactDir(projectDir, runId);
|
|
25
|
+
tracesPath = getArtifactPath(projectDir, runId, 'traces.json');
|
|
26
|
+
} else {
|
|
27
|
+
// Backwards compatibility for tests
|
|
28
|
+
observeDir = resolve(projectDir, '.veraxverax', 'observe');
|
|
29
|
+
tracesPath = resolve(observeDir, 'observation-traces.json');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
mkdirSync(observeDir, { recursive: true });
|
|
33
|
+
|
|
6
34
|
const observation = {
|
|
7
35
|
version: 1,
|
|
8
36
|
observedAt: new Date().toISOString(),
|
|
@@ -10,6 +38,10 @@ export function writeTraces(projectDir, url, traces, coverage = null, warnings =
|
|
|
10
38
|
traces: traces
|
|
11
39
|
};
|
|
12
40
|
|
|
41
|
+
if (observedExpectations && observedExpectations.length > 0) {
|
|
42
|
+
observation.observedExpectations = observedExpectations;
|
|
43
|
+
}
|
|
44
|
+
|
|
13
45
|
if (coverage) {
|
|
14
46
|
observation.coverage = coverage;
|
|
15
47
|
}
|
|
@@ -17,23 +49,7 @@ export function writeTraces(projectDir, url, traces, coverage = null, warnings =
|
|
|
17
49
|
observation.warnings = warnings;
|
|
18
50
|
}
|
|
19
51
|
|
|
20
|
-
|
|
21
|
-
if (artifactPaths) {
|
|
22
|
-
// Use new artifact structure
|
|
23
|
-
// Write JSONL format (one trace per line) for artifact structure
|
|
24
|
-
traces.forEach(trace => appendTrace(artifactPaths, trace));
|
|
25
|
-
|
|
26
|
-
// Also write full observation JSON for detect() compatibility
|
|
27
|
-
const jsonPath = resolve(artifactPaths.evidence, 'observation-traces.json');
|
|
28
|
-
writeFileSync(jsonPath, JSON.stringify(observation, null, 2) + '\n');
|
|
29
|
-
tracesPath = jsonPath;
|
|
30
|
-
} else {
|
|
31
|
-
// Legacy structure
|
|
32
|
-
const observeDir = resolve(projectDir, '.veraxverax', 'observe');
|
|
33
|
-
mkdirSync(observeDir, { recursive: true });
|
|
34
|
-
tracesPath = resolve(observeDir, 'observation-traces.json');
|
|
35
|
-
writeFileSync(tracesPath, JSON.stringify(observation, null, 2) + '\n');
|
|
36
|
-
}
|
|
52
|
+
writeFileSync(tracesPath, JSON.stringify(observation, null, 2) + '\n');
|
|
37
53
|
|
|
38
54
|
let externalNavigationBlockedCount = 0;
|
|
39
55
|
let timeoutsCount = 0;
|
|
@@ -63,9 +79,29 @@ export function writeTraces(projectDir, url, traces, coverage = null, warnings =
|
|
|
63
79
|
|
|
64
80
|
if (coverage) {
|
|
65
81
|
observeTruth.coverage = coverage;
|
|
82
|
+
// SILENCE TRACKING: Track budget exceeded as silence (cap = unevaluated interactions)
|
|
66
83
|
if (coverage.capped) {
|
|
84
|
+
observeTruth.budgetExceeded = true;
|
|
67
85
|
if (!warnings || warnings.length === 0) {
|
|
68
|
-
warnings = [{ code: 'INTERACTIONS_CAPPED', message:
|
|
86
|
+
warnings = [{ code: 'INTERACTIONS_CAPPED', message: `Interaction discovery reached the cap (${coverage.cap}). Scan coverage is incomplete.` }];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Record budget cap as silence
|
|
90
|
+
if (silenceTracker) {
|
|
91
|
+
const unevaluatedCount = (coverage.candidatesDiscovered || 0) - (coverage.candidatesSelected || 0);
|
|
92
|
+
silenceTracker.record({
|
|
93
|
+
scope: 'interaction',
|
|
94
|
+
reason: 'interaction_limit_exceeded',
|
|
95
|
+
description: `Budget cap reached: ${unevaluatedCount} interactions not evaluated`,
|
|
96
|
+
context: {
|
|
97
|
+
cap: coverage.cap,
|
|
98
|
+
discovered: coverage.candidatesDiscovered,
|
|
99
|
+
evaluated: coverage.candidatesSelected,
|
|
100
|
+
unevaluated: unevaluatedCount
|
|
101
|
+
},
|
|
102
|
+
impact: 'affects_expectations',
|
|
103
|
+
count: unevaluatedCount
|
|
104
|
+
});
|
|
69
105
|
}
|
|
70
106
|
}
|
|
71
107
|
}
|
|
@@ -73,6 +109,11 @@ export function writeTraces(projectDir, url, traces, coverage = null, warnings =
|
|
|
73
109
|
observeTruth.warnings = warnings;
|
|
74
110
|
}
|
|
75
111
|
|
|
112
|
+
// SILENCE TRACKING: Attach silence entries to observation for detect phase
|
|
113
|
+
if (silenceTracker && silenceTracker.entries.length > 0) {
|
|
114
|
+
observation.silences = silenceTracker.export();
|
|
115
|
+
}
|
|
116
|
+
|
|
76
117
|
return {
|
|
77
118
|
...observation,
|
|
78
119
|
tracesPath: tracesPath,
|
|
@@ -17,6 +17,7 @@ export class UISignalSensor {
|
|
|
17
17
|
hasErrorSignal: false,
|
|
18
18
|
hasStatusSignal: false,
|
|
19
19
|
hasLiveRegion: false,
|
|
20
|
+
validationFeedbackDetected: false,
|
|
20
21
|
disabledElements: [],
|
|
21
22
|
explanation: []
|
|
22
23
|
};
|
|
@@ -36,18 +37,22 @@ export class UISignalSensor {
|
|
|
36
37
|
result.explanation.push('Found [data-loading]');
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
// role=status or role=alert with aria-live
|
|
40
|
-
const statusRegions = document.querySelectorAll('[role="status"], [role="alert"]');
|
|
41
|
-
|
|
40
|
+
// role=status or role=alert with aria-live (visible only)
|
|
41
|
+
const statusRegions = Array.from(document.querySelectorAll('[role="status"], [role="alert"]'));
|
|
42
|
+
const visibleStatusRegions = statusRegions.filter((el) => {
|
|
43
|
+
const style = window.getComputedStyle(el);
|
|
44
|
+
return el.offsetParent !== null && style.visibility !== 'hidden' && style.display !== 'none' && style.opacity !== '0';
|
|
45
|
+
});
|
|
46
|
+
if (visibleStatusRegions.length > 0) {
|
|
42
47
|
result.hasStatusSignal = true;
|
|
43
|
-
result.explanation.push(`Found ${
|
|
48
|
+
result.explanation.push(`Found ${visibleStatusRegions.length} visible status/alert region(s)`);
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
// aria-live region
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
51
|
+
// aria-live region (legacy check - will be checked again for visibility below)
|
|
52
|
+
const allLiveRegions = document.querySelectorAll('[aria-live]');
|
|
53
|
+
if (allLiveRegions.length > 0) {
|
|
49
54
|
result.hasLiveRegion = true;
|
|
50
|
-
result.explanation.push(`Found ${
|
|
55
|
+
result.explanation.push(`Found ${allLiveRegions.length} aria-live region(s)`);
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
// Check for dialogs
|
|
@@ -78,8 +83,89 @@ export class UISignalSensor {
|
|
|
78
83
|
);
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
//
|
|
82
|
-
|
|
86
|
+
// VALIDATION INTELLIGENCE v1: Detect visible validation feedback
|
|
87
|
+
// Check for aria-invalid="true" with visible error text nearby
|
|
88
|
+
const invalidElements = Array.from(document.querySelectorAll('[aria-invalid="true"]'));
|
|
89
|
+
let hasVisibleValidationError = false;
|
|
90
|
+
|
|
91
|
+
for (const invalidEl of invalidElements) {
|
|
92
|
+
const style = window.getComputedStyle(invalidEl);
|
|
93
|
+
const isVisible = invalidEl.offsetParent !== null &&
|
|
94
|
+
style.visibility !== 'hidden' &&
|
|
95
|
+
style.display !== 'none' &&
|
|
96
|
+
style.opacity !== '0';
|
|
97
|
+
|
|
98
|
+
if (isVisible) {
|
|
99
|
+
// Check for visible error text near this input
|
|
100
|
+
// Look in parent, next sibling, or aria-describedby target
|
|
101
|
+
const describedBy = invalidEl.getAttribute('aria-describedby');
|
|
102
|
+
if (describedBy) {
|
|
103
|
+
const errorTarget = document.getElementById(describedBy);
|
|
104
|
+
if (errorTarget) {
|
|
105
|
+
const targetStyle = window.getComputedStyle(errorTarget);
|
|
106
|
+
const targetVisible = errorTarget.offsetParent !== null &&
|
|
107
|
+
targetStyle.visibility !== 'hidden' &&
|
|
108
|
+
targetStyle.display !== 'none' &&
|
|
109
|
+
targetStyle.opacity !== '0';
|
|
110
|
+
if (targetVisible && errorTarget.textContent.trim().length > 0) {
|
|
111
|
+
hasVisibleValidationError = true;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check parent for error text
|
|
118
|
+
const parent = invalidEl.parentElement;
|
|
119
|
+
if (parent) {
|
|
120
|
+
const errorText = Array.from(parent.querySelectorAll('[role="alert"], .error, .invalid-feedback'))
|
|
121
|
+
.find(el => {
|
|
122
|
+
const elStyle = window.getComputedStyle(el);
|
|
123
|
+
return el.offsetParent !== null &&
|
|
124
|
+
elStyle.visibility !== 'hidden' &&
|
|
125
|
+
elStyle.display !== 'none' &&
|
|
126
|
+
elStyle.opacity !== '0' &&
|
|
127
|
+
el.textContent.trim().length > 0;
|
|
128
|
+
});
|
|
129
|
+
if (errorText) {
|
|
130
|
+
hasVisibleValidationError = true;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check for visible role="alert" or role="status" regions
|
|
138
|
+
const alertRegions = Array.from(document.querySelectorAll('[role="alert"], [role="status"]'));
|
|
139
|
+
const visibleAlertRegions = alertRegions.filter((el) => {
|
|
140
|
+
const style = window.getComputedStyle(el);
|
|
141
|
+
const isVisible = el.offsetParent !== null &&
|
|
142
|
+
style.visibility !== 'hidden' &&
|
|
143
|
+
style.display !== 'none' &&
|
|
144
|
+
style.opacity !== '0';
|
|
145
|
+
return isVisible && el.textContent.trim().length > 0;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Check for visible aria-live regions with content
|
|
149
|
+
const liveRegions = Array.from(document.querySelectorAll('[aria-live]'));
|
|
150
|
+
const visibleLiveRegions = liveRegions.filter((el) => {
|
|
151
|
+
const style = window.getComputedStyle(el);
|
|
152
|
+
const isVisible = el.offsetParent !== null &&
|
|
153
|
+
style.visibility !== 'hidden' &&
|
|
154
|
+
style.display !== 'none' &&
|
|
155
|
+
style.opacity !== '0';
|
|
156
|
+
return isVisible && el.textContent.trim().length > 0;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// VALIDATION INTELLIGENCE v1: Set validationFeedbackDetected
|
|
160
|
+
result.validationFeedbackDetected = hasVisibleValidationError ||
|
|
161
|
+
visibleAlertRegions.length > 0 ||
|
|
162
|
+
visibleLiveRegions.length > 0;
|
|
163
|
+
|
|
164
|
+
if (result.validationFeedbackDetected) {
|
|
165
|
+
result.explanation.push('Visible validation feedback detected');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Legacy: Check for error signals
|
|
83
169
|
if (invalidElements.length > 0) {
|
|
84
170
|
result.hasErrorSignal = true;
|
|
85
171
|
result.explanation.push(`Found ${invalidElements.length} invalid element(s)`);
|
|
@@ -111,16 +197,30 @@ export class UISignalSensor {
|
|
|
111
197
|
* Returns: { changed: boolean, explanation: string[], summary: {...} }
|
|
112
198
|
*/
|
|
113
199
|
diff(before, after) {
|
|
200
|
+
const defaults = {
|
|
201
|
+
hasLoadingIndicator: false,
|
|
202
|
+
hasDialog: false,
|
|
203
|
+
hasErrorSignal: false,
|
|
204
|
+
hasStatusSignal: false,
|
|
205
|
+
hasLiveRegion: false,
|
|
206
|
+
disabledElements: [],
|
|
207
|
+
validationFeedbackDetected: false
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const safeBefore = { ...defaults, ...(before || {}) };
|
|
211
|
+
const safeAfter = { ...defaults, ...(after || {}) };
|
|
212
|
+
|
|
114
213
|
const result = {
|
|
115
214
|
changed: false,
|
|
116
215
|
explanation: [],
|
|
117
216
|
summary: {
|
|
118
|
-
loadingStateChanged:
|
|
119
|
-
dialogStateChanged:
|
|
120
|
-
errorSignalChanged:
|
|
121
|
-
statusSignalChanged:
|
|
122
|
-
liveRegionStateChanged:
|
|
123
|
-
disabledButtonsChanged:
|
|
217
|
+
loadingStateChanged: safeBefore.hasLoadingIndicator !== safeAfter.hasLoadingIndicator,
|
|
218
|
+
dialogStateChanged: safeBefore.hasDialog !== safeAfter.hasDialog,
|
|
219
|
+
errorSignalChanged: safeBefore.hasErrorSignal !== safeAfter.hasErrorSignal,
|
|
220
|
+
statusSignalChanged: safeBefore.hasStatusSignal !== safeAfter.hasStatusSignal,
|
|
221
|
+
liveRegionStateChanged: safeBefore.hasLiveRegion !== safeAfter.hasLiveRegion,
|
|
222
|
+
disabledButtonsChanged: safeBefore.disabledElements.length !== safeAfter.disabledElements.length,
|
|
223
|
+
validationFeedbackChanged: safeBefore.validationFeedbackDetected !== safeAfter.validationFeedbackDetected // VALIDATION INTELLIGENCE v1
|
|
124
224
|
}
|
|
125
225
|
};
|
|
126
226
|
|
|
@@ -159,6 +259,14 @@ export class UISignalSensor {
|
|
|
159
259
|
`Live region: ${before.hasLiveRegion} → ${after.hasLiveRegion}`
|
|
160
260
|
);
|
|
161
261
|
}
|
|
262
|
+
|
|
263
|
+
// Also check if status signal content changed (text added to role=status)
|
|
264
|
+
if (!result.changed && before.hasStatusSignal && after.hasStatusSignal) {
|
|
265
|
+
// Both have status signals, but content might have changed
|
|
266
|
+
// This is a conservative check - if status signal exists and is visible, consider it feedback
|
|
267
|
+
result.changed = true;
|
|
268
|
+
result.explanation.push('Status signal content changed');
|
|
269
|
+
}
|
|
162
270
|
|
|
163
271
|
if (result.summary.disabledButtonsChanged) {
|
|
164
272
|
result.changed = true;
|
|
@@ -167,7 +275,18 @@ export class UISignalSensor {
|
|
|
167
275
|
);
|
|
168
276
|
}
|
|
169
277
|
|
|
170
|
-
|
|
278
|
+
// VALIDATION INTELLIGENCE v1: Check for validation feedback changes
|
|
279
|
+
if (result.summary.validationFeedbackChanged) {
|
|
280
|
+
result.changed = true;
|
|
281
|
+
result.explanation.push(
|
|
282
|
+
`Validation feedback: ${before.validationFeedbackDetected} → ${after.validationFeedbackDetected}`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
...result,
|
|
288
|
+
explanation: result.explanation.join(' | ')
|
|
289
|
+
};
|
|
171
290
|
}
|
|
172
291
|
|
|
173
292
|
/**
|
|
@@ -1,17 +1,90 @@
|
|
|
1
1
|
import { resolve } from 'path';
|
|
2
|
-
import { writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { computeExpectationsSummary } from './shared/artifact-manager.js';
|
|
4
|
+
import { createImpactSummary } from './core/silence-impact.js';
|
|
5
|
+
import { computeDecisionSnapshot } from './core/decision-snapshot.js';
|
|
3
6
|
|
|
4
|
-
export function writeScanSummary(projectDir, url, projectType, learnTruth, observeTruth, detectTruth, manifestPath, tracesPath, findingsPath,
|
|
7
|
+
export function writeScanSummary(projectDir, url, projectType, learnTruth, observeTruth, detectTruth, manifestPath, tracesPath, findingsPath, runDirOpt = null, findingsArray = null) {
|
|
8
|
+
const scanDir = runDirOpt ? resolve(runDirOpt) : resolve(projectDir, '.veraxverax', 'scan');
|
|
9
|
+
mkdirSync(scanDir, { recursive: true });
|
|
10
|
+
|
|
11
|
+
// Compute expectations summary from manifest
|
|
12
|
+
let expectationsSummary = { total: 0, navigation: 0, networkActions: 0, stateActions: 0 };
|
|
13
|
+
if (manifestPath && existsSync(manifestPath)) {
|
|
14
|
+
try {
|
|
15
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
16
|
+
expectationsSummary = computeExpectationsSummary(manifest);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
// Ignore errors reading manifest
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// PHASE 4: Compute silence impact summary
|
|
23
|
+
let silenceImpactSummary = null;
|
|
24
|
+
if (detectTruth?.silences?.entries) {
|
|
25
|
+
silenceImpactSummary = createImpactSummary(detectTruth.silences.entries);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// PHASE 6: Compute determinism summary from decisions.json
|
|
29
|
+
let determinismSummary = null;
|
|
30
|
+
if (runDirOpt && observeTruth?.runId) {
|
|
31
|
+
const decisionsPath = resolve(runDirOpt, 'decisions.json');
|
|
32
|
+
if (existsSync(decisionsPath)) {
|
|
33
|
+
try {
|
|
34
|
+
const decisions = JSON.parse(readFileSync(decisionsPath, 'utf-8'));
|
|
35
|
+
const { DecisionRecorder } = require('./core/determinism-model.js');
|
|
36
|
+
const recorder = DecisionRecorder.fromExport(decisions);
|
|
37
|
+
const summary = recorder.getSummary();
|
|
38
|
+
|
|
39
|
+
determinismSummary = {
|
|
40
|
+
isDeterministic: summary.isDeterministic,
|
|
41
|
+
totalDecisions: summary.totalDecisions,
|
|
42
|
+
decisionsByCategory: summary.decisionsByCategory,
|
|
43
|
+
decisionsPath: decisionsPath
|
|
44
|
+
};
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// Ignore errors reading decisions
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// PHASE 7: Compute decision snapshot (answers 6 mandatory questions)
|
|
52
|
+
let decisionSnapshot = null;
|
|
53
|
+
if (findingsArray && detectTruth && observeTruth) {
|
|
54
|
+
const silences = detectTruth.silences;
|
|
55
|
+
decisionSnapshot = computeDecisionSnapshot(findingsArray, detectTruth, observeTruth, silences);
|
|
56
|
+
}
|
|
57
|
+
|
|
5
58
|
const summary = {
|
|
6
59
|
version: 1,
|
|
7
60
|
scannedAt: new Date().toISOString(),
|
|
8
61
|
url: url,
|
|
9
62
|
projectType: projectType,
|
|
63
|
+
expectationsSummary: expectationsSummary,
|
|
64
|
+
// PHASE 7: Decision snapshot first (most important for human decision-making)
|
|
65
|
+
decisionSnapshot: decisionSnapshot,
|
|
66
|
+
// PHASE 7: Misinterpretation guards (explicit warnings)
|
|
67
|
+
interpretationGuards: {
|
|
68
|
+
zeroFindings: 'Zero findings does NOT mean no problems. Check unverified count and confidence level.',
|
|
69
|
+
deterministicRun: 'Deterministic run does NOT mean correct site. Only means scan was reproducible.',
|
|
70
|
+
highSilenceImpact: 'High silence impact does NOT mean failures exist. Only means unknowns affect confidence.'
|
|
71
|
+
},
|
|
10
72
|
truth: {
|
|
11
73
|
learn: learnTruth,
|
|
12
74
|
observe: observeTruth,
|
|
13
75
|
detect: detectTruth
|
|
14
76
|
},
|
|
77
|
+
// PHASE 4: Add silence lifecycle and impact summary
|
|
78
|
+
silenceLifecycle: detectTruth?.silences ? {
|
|
79
|
+
total: detectTruth.silences.total || 0,
|
|
80
|
+
byType: detectTruth.silences.summary?.byType || {},
|
|
81
|
+
byEvaluationStatus: detectTruth.silences.summary?.byEvaluationStatus || {},
|
|
82
|
+
byOutcome: detectTruth.silences.summary?.byOutcome || {},
|
|
83
|
+
withPromiseAssociation: detectTruth.silences.summary?.withPromiseAssociation || 0,
|
|
84
|
+
impactSummary: silenceImpactSummary
|
|
85
|
+
} : null,
|
|
86
|
+
// PHASE 6: Add determinism summary
|
|
87
|
+
determinism: determinismSummary,
|
|
15
88
|
paths: {
|
|
16
89
|
manifest: manifestPath,
|
|
17
90
|
traces: tracesPath,
|
|
@@ -19,19 +92,8 @@ export function writeScanSummary(projectDir, url, projectType, learnTruth, obser
|
|
|
19
92
|
}
|
|
20
93
|
};
|
|
21
94
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// Use new artifact structure
|
|
25
|
-
summaryPath = artifactPaths.summary;
|
|
26
|
-
// Write the scan summary with truth data
|
|
27
|
-
writeFileSync(summaryPath, JSON.stringify(summary, null, 2) + '\n');
|
|
28
|
-
} else {
|
|
29
|
-
// Legacy structure
|
|
30
|
-
const scanDir = resolve(projectDir, '.veraxverax', 'scan');
|
|
31
|
-
mkdirSync(scanDir, { recursive: true });
|
|
32
|
-
summaryPath = resolve(scanDir, 'scan-summary.json');
|
|
33
|
-
writeFileSync(summaryPath, JSON.stringify(summary, null, 2) + '\n');
|
|
34
|
-
}
|
|
95
|
+
const summaryPath = resolve(scanDir, 'scan-summary.json');
|
|
96
|
+
writeFileSync(summaryPath, JSON.stringify(summary, null, 2) + '\n');
|
|
35
97
|
|
|
36
98
|
return {
|
|
37
99
|
...summary,
|
|
@@ -39,6 +39,7 @@ export function initArtifactPaths(projectRoot, runId = null) {
|
|
|
39
39
|
runDir,
|
|
40
40
|
summary: resolve(runDir, 'summary.json'),
|
|
41
41
|
findings: resolve(runDir, 'findings.json'),
|
|
42
|
+
expectations: resolve(runDir, 'expectations.json'),
|
|
42
43
|
traces: resolve(runDir, 'traces.jsonl'),
|
|
43
44
|
evidence: resolve(runDir, 'evidence'),
|
|
44
45
|
flows: resolve(runDir, 'flows'),
|
|
@@ -55,23 +56,118 @@ export function initArtifactPaths(projectRoot, runId = null) {
|
|
|
55
56
|
return paths;
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Compute expectations summary from manifest
|
|
61
|
+
* @param {Object} manifest - Manifest object
|
|
62
|
+
* @returns {Object} Expectations summary
|
|
63
|
+
*/
|
|
64
|
+
export function computeExpectationsSummary(manifest) {
|
|
65
|
+
const staticExpectations = manifest.staticExpectations || [];
|
|
66
|
+
const spaExpectations = manifest.spaExpectations || [];
|
|
67
|
+
const actionContracts = manifest.actionContracts || [];
|
|
68
|
+
const proven = (item) => item && item.proof === 'PROVEN_EXPECTATION';
|
|
69
|
+
|
|
70
|
+
// Count by type
|
|
71
|
+
let navigation = 0;
|
|
72
|
+
let networkActions = 0;
|
|
73
|
+
let stateActions = 0;
|
|
74
|
+
|
|
75
|
+
// Static expectations: navigation, form_submission, network_action, state_action
|
|
76
|
+
for (const exp of staticExpectations) {
|
|
77
|
+
if (!proven(exp)) continue;
|
|
78
|
+
if (exp.type === 'navigation' || exp.type === 'spa_navigation') {
|
|
79
|
+
navigation++;
|
|
80
|
+
} else if (exp.type === 'form_submission') {
|
|
81
|
+
networkActions++; // Form submissions are network actions
|
|
82
|
+
} else if (exp.type === 'network_action') {
|
|
83
|
+
networkActions++;
|
|
84
|
+
} else if (exp.type === 'state_action') {
|
|
85
|
+
stateActions++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// SPA expectations: navigation
|
|
90
|
+
for (const exp of spaExpectations) {
|
|
91
|
+
if (!proven(exp)) continue;
|
|
92
|
+
if (exp.type === 'navigation') {
|
|
93
|
+
navigation++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Action contracts: network and state actions
|
|
98
|
+
for (const contract of actionContracts) {
|
|
99
|
+
if (!proven(contract)) continue;
|
|
100
|
+
if (contract.kind === 'NETWORK_ACTION' || contract.kind === 'network' || contract.kind === 'fetch' || contract.kind === 'axios') {
|
|
101
|
+
networkActions++;
|
|
102
|
+
} else if (contract.kind === 'STATE_ACTION' || contract.kind === 'state' || contract.kind === 'redux' || contract.kind === 'zustand') {
|
|
103
|
+
stateActions++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const total = navigation + networkActions + stateActions;
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
total,
|
|
111
|
+
navigation,
|
|
112
|
+
networkActions,
|
|
113
|
+
stateActions
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
58
117
|
/**
|
|
59
118
|
* Write summary.json with metadata and metrics.
|
|
60
119
|
* @param {Object} paths - Artifact paths from initArtifactPaths
|
|
61
|
-
* @param {Object} summary - Summary data { url, duration, findings, metrics }
|
|
120
|
+
* @param {Object} summary - Summary data { url, duration, findings, metrics, manifest, contextCheck }
|
|
62
121
|
*/
|
|
63
122
|
export function writeSummary(paths, summary) {
|
|
123
|
+
const expectationsSummary = summary.manifest
|
|
124
|
+
? computeExpectationsSummary(summary.manifest)
|
|
125
|
+
: { total: 0, navigation: 0, networkActions: 0, stateActions: 0 };
|
|
126
|
+
|
|
127
|
+
const contextCheck = summary.contextCheck || {
|
|
128
|
+
ran: false,
|
|
129
|
+
forced: false,
|
|
130
|
+
matchedRoutesCount: 0,
|
|
131
|
+
matchedLinksCount: 0,
|
|
132
|
+
sampleMatched: []
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const metrics = summary.metrics || {
|
|
136
|
+
learnMs: 0,
|
|
137
|
+
validateMs: 0,
|
|
138
|
+
observeMs: 0,
|
|
139
|
+
detectMs: 0,
|
|
140
|
+
totalMs: 0
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const safety = summary.safety || {
|
|
144
|
+
publicUrlConfirmed: false,
|
|
145
|
+
usedYesFlag: false
|
|
146
|
+
};
|
|
147
|
+
|
|
64
148
|
const data = {
|
|
65
149
|
runId: paths.runId,
|
|
66
150
|
timestamp: new Date().toISOString(),
|
|
67
151
|
url: summary.url,
|
|
68
152
|
projectRoot: summary.projectRoot,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
153
|
+
expectationsSummary: expectationsSummary,
|
|
154
|
+
contextCheck: {
|
|
155
|
+
ran: contextCheck.ran,
|
|
156
|
+
forced: contextCheck.forced || false,
|
|
157
|
+
matchedRoutesCount: contextCheck.matchedRoutesCount || 0,
|
|
158
|
+
matchedLinksCount: contextCheck.matchedLinksCount || 0,
|
|
159
|
+
sampleMatched: (contextCheck.sampleMatched || []).slice(0, 5)
|
|
160
|
+
},
|
|
161
|
+
safety: {
|
|
162
|
+
publicUrlConfirmed: safety.publicUrlConfirmed || false,
|
|
163
|
+
usedYesFlag: safety.usedYesFlag || false
|
|
164
|
+
},
|
|
165
|
+
metrics: {
|
|
166
|
+
learnMs: metrics.learnMs || 0,
|
|
167
|
+
validateMs: metrics.validateMs || 0,
|
|
168
|
+
observeMs: metrics.observeMs || 0,
|
|
169
|
+
detectMs: metrics.detectMs || 0,
|
|
170
|
+
totalMs: metrics.totalMs || 0
|
|
75
171
|
},
|
|
76
172
|
findingsCounts: summary.findingsCounts || {
|
|
77
173
|
HIGH: 0,
|
|
@@ -80,7 +176,13 @@ export function writeSummary(paths, summary) {
|
|
|
80
176
|
UNKNOWN: 0
|
|
81
177
|
},
|
|
82
178
|
topFindings: summary.topFindings || [],
|
|
83
|
-
cacheStats: summary.cacheStats || {}
|
|
179
|
+
cacheStats: summary.cacheStats || {},
|
|
180
|
+
progressStats: summary.progressStats || null,
|
|
181
|
+
interactionStats: summary.interactionStats || null,
|
|
182
|
+
expectationUsageStats: summary.expectationUsageStats || null,
|
|
183
|
+
runOverview: summary.runOverview || null,
|
|
184
|
+
coverage: summary.coverage || null,
|
|
185
|
+
coverageGaps: summary.coverageGaps || []
|
|
84
186
|
};
|
|
85
187
|
|
|
86
188
|
writeFileSync(paths.summary, JSON.stringify(data, null, 2) + '\n');
|