@veraxhq/verax 0.2.1 → 0.3.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 +14 -18
- package/bin/verax.js +7 -0
- package/package.json +3 -3
- package/src/cli/commands/baseline.js +104 -0
- package/src/cli/commands/default.js +79 -25
- package/src/cli/commands/ga.js +243 -0
- package/src/cli/commands/gates.js +95 -0
- package/src/cli/commands/inspect.js +131 -2
- package/src/cli/commands/release-check.js +213 -0
- package/src/cli/commands/run.js +246 -35
- package/src/cli/commands/security-check.js +211 -0
- package/src/cli/commands/truth.js +114 -0
- package/src/cli/entry.js +304 -67
- package/src/cli/util/angular-component-extractor.js +179 -0
- package/src/cli/util/angular-navigation-detector.js +141 -0
- package/src/cli/util/angular-network-detector.js +161 -0
- package/src/cli/util/angular-state-detector.js +162 -0
- package/src/cli/util/ast-interactive-detector.js +546 -0
- package/src/cli/util/ast-network-detector.js +603 -0
- package/src/cli/util/ast-usestate-detector.js +602 -0
- package/src/cli/util/bootstrap-guard.js +86 -0
- package/src/cli/util/determinism-runner.js +123 -0
- package/src/cli/util/determinism-writer.js +129 -0
- package/src/cli/util/env-url.js +4 -0
- package/src/cli/util/expectation-extractor.js +369 -73
- package/src/cli/util/findings-writer.js +126 -16
- package/src/cli/util/learn-writer.js +3 -1
- package/src/cli/util/observe-writer.js +3 -1
- package/src/cli/util/paths.js +3 -12
- package/src/cli/util/project-discovery.js +3 -0
- package/src/cli/util/project-writer.js +3 -1
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +1 -0
- package/src/cli/util/svelte-navigation-detector.js +163 -0
- package/src/cli/util/svelte-network-detector.js +80 -0
- package/src/cli/util/svelte-sfc-extractor.js +147 -0
- package/src/cli/util/svelte-state-detector.js +243 -0
- package/src/cli/util/vue-navigation-detector.js +177 -0
- package/src/cli/util/vue-sfc-extractor.js +162 -0
- package/src/cli/util/vue-state-detector.js +215 -0
- package/src/verax/cli/finding-explainer.js +56 -3
- package/src/verax/core/artifacts/registry.js +154 -0
- package/src/verax/core/artifacts/verifier.js +980 -0
- package/src/verax/core/baseline/baseline.enforcer.js +137 -0
- package/src/verax/core/baseline/baseline.snapshot.js +231 -0
- package/src/verax/core/capabilities/gates.js +499 -0
- package/src/verax/core/capabilities/registry.js +475 -0
- package/src/verax/core/confidence/confidence-compute.js +137 -0
- package/src/verax/core/confidence/confidence-invariants.js +234 -0
- package/src/verax/core/confidence/confidence-report-writer.js +112 -0
- package/src/verax/core/confidence/confidence-weights.js +44 -0
- package/src/verax/core/confidence/confidence.defaults.js +65 -0
- package/src/verax/core/confidence/confidence.loader.js +79 -0
- package/src/verax/core/confidence/confidence.schema.js +94 -0
- package/src/verax/core/confidence-engine-refactor.js +484 -0
- package/src/verax/core/confidence-engine.js +486 -0
- package/src/verax/core/confidence-engine.js.backup +471 -0
- package/src/verax/core/contracts/index.js +29 -0
- package/src/verax/core/contracts/types.js +185 -0
- package/src/verax/core/contracts/validators.js +381 -0
- package/src/verax/core/decision-snapshot.js +30 -3
- package/src/verax/core/decisions/decision.trace.js +276 -0
- package/src/verax/core/determinism/contract-writer.js +89 -0
- package/src/verax/core/determinism/contract.js +139 -0
- package/src/verax/core/determinism/diff.js +364 -0
- package/src/verax/core/determinism/engine.js +221 -0
- package/src/verax/core/determinism/finding-identity.js +148 -0
- package/src/verax/core/determinism/normalize.js +438 -0
- package/src/verax/core/determinism/report-writer.js +92 -0
- package/src/verax/core/determinism/run-fingerprint.js +118 -0
- package/src/verax/core/dynamic-route-intelligence.js +528 -0
- package/src/verax/core/evidence/evidence-capture-service.js +307 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
- package/src/verax/core/evidence-builder.js +487 -0
- package/src/verax/core/execution-mode-context.js +77 -0
- package/src/verax/core/execution-mode-detector.js +190 -0
- package/src/verax/core/failures/exit-codes.js +86 -0
- package/src/verax/core/failures/failure-summary.js +76 -0
- package/src/verax/core/failures/failure.factory.js +225 -0
- package/src/verax/core/failures/failure.ledger.js +132 -0
- package/src/verax/core/failures/failure.types.js +196 -0
- package/src/verax/core/failures/index.js +10 -0
- package/src/verax/core/ga/ga-report-writer.js +43 -0
- package/src/verax/core/ga/ga.artifact.js +49 -0
- package/src/verax/core/ga/ga.contract.js +434 -0
- package/src/verax/core/ga/ga.enforcer.js +86 -0
- package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
- package/src/verax/core/guardrails/policy.defaults.js +210 -0
- package/src/verax/core/guardrails/policy.loader.js +83 -0
- package/src/verax/core/guardrails/policy.schema.js +110 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
- package/src/verax/core/guardrails-engine.js +505 -0
- package/src/verax/core/observe/run-timeline.js +316 -0
- package/src/verax/core/perf/perf.contract.js +186 -0
- package/src/verax/core/perf/perf.display.js +65 -0
- package/src/verax/core/perf/perf.enforcer.js +91 -0
- package/src/verax/core/perf/perf.monitor.js +209 -0
- package/src/verax/core/perf/perf.report.js +198 -0
- package/src/verax/core/pipeline-tracker.js +238 -0
- package/src/verax/core/product-definition.js +127 -0
- package/src/verax/core/release/provenance.builder.js +271 -0
- package/src/verax/core/release/release-report-writer.js +40 -0
- package/src/verax/core/release/release.enforcer.js +159 -0
- package/src/verax/core/release/reproducibility.check.js +221 -0
- package/src/verax/core/release/sbom.builder.js +283 -0
- package/src/verax/core/report/cross-index.js +192 -0
- package/src/verax/core/report/human-summary.js +222 -0
- package/src/verax/core/route-intelligence.js +419 -0
- package/src/verax/core/security/secrets.scan.js +326 -0
- package/src/verax/core/security/security-report.js +50 -0
- package/src/verax/core/security/security.enforcer.js +124 -0
- package/src/verax/core/security/supplychain.defaults.json +38 -0
- package/src/verax/core/security/supplychain.policy.js +326 -0
- package/src/verax/core/security/vuln.scan.js +265 -0
- package/src/verax/core/truth/truth.certificate.js +250 -0
- package/src/verax/core/ui-feedback-intelligence.js +515 -0
- package/src/verax/detect/confidence-engine.js +628 -40
- package/src/verax/detect/confidence-helper.js +33 -0
- package/src/verax/detect/detection-engine.js +18 -1
- package/src/verax/detect/dynamic-route-findings.js +335 -0
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +3 -1
- package/src/verax/detect/findings-writer.js +141 -5
- package/src/verax/detect/index.js +229 -5
- package/src/verax/detect/journey-stall-detector.js +558 -0
- package/src/verax/detect/route-findings.js +218 -0
- package/src/verax/detect/ui-feedback-findings.js +207 -0
- package/src/verax/detect/verdict-engine.js +57 -3
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/index.js +413 -45
- package/src/verax/learn/action-contract-extractor.js +682 -64
- package/src/verax/learn/route-validator.js +4 -1
- package/src/verax/observe/index.js +88 -843
- package/src/verax/observe/interaction-runner.js +25 -8
- package/src/verax/observe/observe-context.js +205 -0
- package/src/verax/observe/observe-helpers.js +191 -0
- package/src/verax/observe/observe-runner.js +226 -0
- package/src/verax/observe/observers/budget-observer.js +185 -0
- package/src/verax/observe/observers/console-observer.js +102 -0
- package/src/verax/observe/observers/coverage-observer.js +107 -0
- package/src/verax/observe/observers/interaction-observer.js +471 -0
- package/src/verax/observe/observers/navigation-observer.js +132 -0
- package/src/verax/observe/observers/network-observer.js +87 -0
- package/src/verax/observe/observers/safety-observer.js +82 -0
- package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
- package/src/verax/observe/ui-feedback-detector.js +742 -0
- package/src/verax/observe/ui-signal-sensor.js +148 -2
- package/src/verax/scan-summary-writer.js +42 -8
- package/src/verax/shared/artifact-manager.js +8 -5
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/view-switch-rules.js +208 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VERAX Validators - Runtime contract enforcement
|
|
3
|
+
*
|
|
4
|
+
* Implements the Evidence Law:
|
|
5
|
+
* "A finding cannot be marked CONFIRMED without sufficient evidence."
|
|
6
|
+
*
|
|
7
|
+
* All validators follow the pattern:
|
|
8
|
+
* @returns {Object} { ok: boolean, errors: string[], downgrade?: string }
|
|
9
|
+
*
|
|
10
|
+
* If a finding violates contracts, it should be downgraded to SUSPECTED
|
|
11
|
+
* or dropped from the report if it's critical.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
CONFIDENCE_LEVEL,
|
|
16
|
+
FINDING_STATUS,
|
|
17
|
+
FINDING_TYPE,
|
|
18
|
+
IMPACT,
|
|
19
|
+
USER_RISK,
|
|
20
|
+
OWNERSHIP,
|
|
21
|
+
EVIDENCE_TYPE
|
|
22
|
+
} from './types.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate a Finding object against contracts
|
|
26
|
+
*
|
|
27
|
+
* Evidence Law Enforcement:
|
|
28
|
+
* - If confidence.level === CONFIRMED, evidence must be substantive
|
|
29
|
+
* - If evidence is missing or empty, finding cannot be CONFIRMED
|
|
30
|
+
* - Returns { ok, errors, shouldDowngrade, suggestedStatus }
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} finding - Finding object to validate
|
|
33
|
+
* @returns {Object} Validation result with enforcement recommendation
|
|
34
|
+
*/
|
|
35
|
+
export function validateFinding(finding) {
|
|
36
|
+
const errors = [];
|
|
37
|
+
let shouldDowngrade = false;
|
|
38
|
+
let suggestedStatus = null;
|
|
39
|
+
|
|
40
|
+
// Contract 1: Required top-level fields
|
|
41
|
+
if (!finding) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
errors: ['Finding is null or undefined'],
|
|
45
|
+
shouldDowngrade: false
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!finding.type) {
|
|
50
|
+
errors.push('Missing required field: type');
|
|
51
|
+
} else if (!Object.values(FINDING_TYPE).includes(finding.type)) {
|
|
52
|
+
errors.push(`Invalid type: ${finding.type}. Must be one of: ${Object.values(FINDING_TYPE).join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!finding.interaction || typeof finding.interaction !== 'object') {
|
|
56
|
+
errors.push('Missing or invalid required field: interaction (must be object)');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!finding.what_happened || typeof finding.what_happened !== 'string') {
|
|
60
|
+
errors.push('Missing or invalid required field: what_happened (must be non-empty string)');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!finding.what_was_expected || typeof finding.what_was_expected !== 'string') {
|
|
64
|
+
errors.push('Missing or invalid required field: what_was_expected (must be non-empty string)');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!finding.what_was_observed || typeof finding.what_was_observed !== 'string') {
|
|
68
|
+
errors.push('Missing or invalid required field: what_was_observed (must be non-empty string)');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Contract 2: Evidence validation (CRITICAL for Evidence Law)
|
|
72
|
+
const evidenceValidation = validateEvidence(finding.evidence);
|
|
73
|
+
if (!evidenceValidation.ok) {
|
|
74
|
+
errors.push(`Invalid evidence: ${evidenceValidation.errors.join('; ')}`);
|
|
75
|
+
shouldDowngrade = true;
|
|
76
|
+
suggestedStatus = FINDING_STATUS.SUSPECTED;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Contract 3: Confidence validation
|
|
80
|
+
const confidenceValidation = validateConfidence(finding.confidence);
|
|
81
|
+
if (!confidenceValidation.ok) {
|
|
82
|
+
errors.push(`Invalid confidence: ${confidenceValidation.errors.join('; ')}`);
|
|
83
|
+
shouldDowngrade = true;
|
|
84
|
+
suggestedStatus = FINDING_STATUS.SUSPECTED;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Contract 4: Signals validation
|
|
88
|
+
if (!finding.signals || typeof finding.signals !== 'object') {
|
|
89
|
+
errors.push('Missing or invalid required field: signals (must be object)');
|
|
90
|
+
} else {
|
|
91
|
+
const signalsValidation = validateSignals(finding.signals);
|
|
92
|
+
if (!signalsValidation.ok) {
|
|
93
|
+
errors.push(`Invalid signals: ${signalsValidation.errors.join('; ')}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// *** EVIDENCE LAW ENFORCEMENT ***
|
|
98
|
+
// PHASE 16: Check evidencePackage completeness for CONFIRMED findings
|
|
99
|
+
// PHASE 21.1: HARD LOCK - CONFIRMED without complete evidencePackage is IMPOSSIBLE
|
|
100
|
+
if (finding.status === FINDING_STATUS.CONFIRMED || finding.severity === 'CONFIRMED') {
|
|
101
|
+
// PHASE 21.1: Strict invariant - CONFIRMED findings MUST have complete evidencePackage
|
|
102
|
+
if (finding.evidencePackage) {
|
|
103
|
+
const missingFields = finding.evidencePackage.missingEvidence || [];
|
|
104
|
+
const isComplete = finding.evidencePackage.isComplete === true;
|
|
105
|
+
|
|
106
|
+
if (!isComplete || missingFields.length > 0) {
|
|
107
|
+
// PHASE 21.1: HARD FAILURE - do not downgrade, fail validation
|
|
108
|
+
errors.push(
|
|
109
|
+
`Evidence Law Violation (CRITICAL): Finding marked CONFIRMED but evidencePackage is incomplete. ` +
|
|
110
|
+
`Missing fields: ${missingFields.join(', ')}. ` +
|
|
111
|
+
`evidencePackage.isComplete=${isComplete}. ` +
|
|
112
|
+
`This finding MUST be dropped, not downgraded.`
|
|
113
|
+
);
|
|
114
|
+
// Do not set shouldDowngrade - this is a critical failure that should drop the finding
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
errors,
|
|
118
|
+
shouldDowngrade: false, // Fail closed - drop, don't downgrade
|
|
119
|
+
suggestedStatus: null
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
} else if (!isEvidenceSubstantive(finding.evidence)) {
|
|
123
|
+
// PHASE 21.1: CONFIRMED finding without evidencePackage and without substantive evidence → CRITICAL FAILURE
|
|
124
|
+
errors.push(
|
|
125
|
+
`Evidence Law Violation (CRITICAL): Finding marked CONFIRMED but lacks evidencePackage and evidence is insufficient. ` +
|
|
126
|
+
`This finding MUST be dropped, not downgraded.`
|
|
127
|
+
);
|
|
128
|
+
// Do not set shouldDowngrade - this is a critical failure that should drop the finding
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
errors,
|
|
132
|
+
shouldDowngrade: false, // Fail closed - drop, don't downgrade
|
|
133
|
+
suggestedStatus: null
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
ok: errors.length === 0,
|
|
140
|
+
errors,
|
|
141
|
+
shouldDowngrade,
|
|
142
|
+
suggestedStatus
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Validate an Evidence object
|
|
148
|
+
* Evidence is REQUIRED for any CONFIRMED finding.
|
|
149
|
+
*
|
|
150
|
+
* @param {Object} evidence - Evidence object to validate
|
|
151
|
+
* @returns {Object} { ok: boolean, errors: string[] }
|
|
152
|
+
*/
|
|
153
|
+
export function validateEvidence(evidence) {
|
|
154
|
+
const errors = [];
|
|
155
|
+
|
|
156
|
+
if (!evidence) {
|
|
157
|
+
errors.push('Evidence object is missing');
|
|
158
|
+
return { ok: false, errors };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (typeof evidence !== 'object') {
|
|
162
|
+
errors.push('Evidence must be an object');
|
|
163
|
+
return { ok: false, errors };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Evidence should contain at least one substantive field
|
|
167
|
+
const substantiveFields = [
|
|
168
|
+
'hasDomChange',
|
|
169
|
+
'hasUrlChange',
|
|
170
|
+
'hasNetworkActivity',
|
|
171
|
+
'hasStateChange',
|
|
172
|
+
'networkRequests',
|
|
173
|
+
'consoleLogs',
|
|
174
|
+
'before',
|
|
175
|
+
'after',
|
|
176
|
+
'beforeDom',
|
|
177
|
+
'afterDom'
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
const hasAtLeastOneField = substantiveFields.some(
|
|
181
|
+
field => evidence[field] !== undefined && evidence[field] !== null
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
if (!hasAtLeastOneField && !evidence.sensors) {
|
|
185
|
+
errors.push(
|
|
186
|
+
'Evidence object is empty. Must contain at least one of: ' +
|
|
187
|
+
substantiveFields.join(', ') + ', or sensors data'
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Optional: validate specific evidence fields if present
|
|
192
|
+
if (evidence.type && !Object.values(EVIDENCE_TYPE).includes(evidence.type)) {
|
|
193
|
+
errors.push(`Invalid evidence type: ${evidence.type}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
ok: errors.length === 0,
|
|
198
|
+
errors
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Validate a Confidence object
|
|
204
|
+
*
|
|
205
|
+
* @param {Object} confidence - Confidence object to validate
|
|
206
|
+
* @returns {Object} { ok: boolean, errors: string[] }
|
|
207
|
+
*/
|
|
208
|
+
export function validateConfidence(confidence) {
|
|
209
|
+
const errors = [];
|
|
210
|
+
|
|
211
|
+
if (!confidence || typeof confidence !== 'object') {
|
|
212
|
+
errors.push('Confidence must be a non-empty object');
|
|
213
|
+
return { ok: false, errors };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!confidence.level) {
|
|
217
|
+
errors.push('Missing required field: confidence.level');
|
|
218
|
+
} else if (!Object.values(CONFIDENCE_LEVEL).includes(confidence.level)) {
|
|
219
|
+
errors.push(
|
|
220
|
+
`Invalid confidence level: ${confidence.level}. ` +
|
|
221
|
+
`Must be one of: ${Object.values(CONFIDENCE_LEVEL).join(', ')}`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (confidence.score === undefined || confidence.score === null) {
|
|
226
|
+
errors.push('Missing required field: confidence.score');
|
|
227
|
+
} else if (typeof confidence.score !== 'number' || confidence.score < 0 || confidence.score > 100) {
|
|
228
|
+
errors.push(`Invalid confidence.score: ${confidence.score}. Must be a number 0-100`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
ok: errors.length === 0,
|
|
233
|
+
errors
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Validate a Signals object
|
|
239
|
+
*
|
|
240
|
+
* @param {Object} signals - Signals object to validate
|
|
241
|
+
* @returns {Object} { ok: boolean, errors: string[] }
|
|
242
|
+
*/
|
|
243
|
+
export function validateSignals(signals) {
|
|
244
|
+
const errors = [];
|
|
245
|
+
|
|
246
|
+
if (!signals || typeof signals !== 'object') {
|
|
247
|
+
errors.push('Signals must be a non-empty object');
|
|
248
|
+
return { ok: false, errors };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!signals.impact) {
|
|
252
|
+
errors.push('Missing required field: signals.impact');
|
|
253
|
+
} else if (!Object.values(IMPACT).includes(signals.impact)) {
|
|
254
|
+
errors.push(
|
|
255
|
+
`Invalid impact: ${signals.impact}. ` +
|
|
256
|
+
`Must be one of: ${Object.values(IMPACT).join(', ')}`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!signals.userRisk) {
|
|
261
|
+
errors.push('Missing required field: signals.userRisk');
|
|
262
|
+
} else if (!Object.values(USER_RISK).includes(signals.userRisk)) {
|
|
263
|
+
errors.push(
|
|
264
|
+
`Invalid userRisk: ${signals.userRisk}. ` +
|
|
265
|
+
`Must be one of: ${Object.values(USER_RISK).join(', ')}`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!signals.ownership) {
|
|
270
|
+
errors.push('Missing required field: signals.ownership');
|
|
271
|
+
} else if (!Object.values(OWNERSHIP).includes(signals.ownership)) {
|
|
272
|
+
errors.push(
|
|
273
|
+
`Invalid ownership: ${signals.ownership}. ` +
|
|
274
|
+
`Must be one of: ${Object.values(OWNERSHIP).join(', ')}`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!signals.grouping || typeof signals.grouping !== 'object') {
|
|
279
|
+
errors.push('Missing or invalid required field: signals.grouping (must be object)');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
ok: errors.length === 0,
|
|
284
|
+
errors
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* EVIDENCE LAW: Determine if evidence is sufficient for CONFIRMED status
|
|
290
|
+
*
|
|
291
|
+
* Substantive evidence means:
|
|
292
|
+
* - At least one positive signal (dom change, url change, network activity, etc.)
|
|
293
|
+
* - OR concrete sensor data from interaction
|
|
294
|
+
* - NOT just empty object or missing evidence
|
|
295
|
+
*
|
|
296
|
+
* @param {Object} evidence - Evidence object to evaluate
|
|
297
|
+
* @returns {boolean} True if evidence is substantive enough for CONFIRMED status
|
|
298
|
+
*/
|
|
299
|
+
export function isEvidenceSubstantive(evidence) {
|
|
300
|
+
if (!evidence || typeof evidence !== 'object') {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Check for positive signal indicators
|
|
305
|
+
const hasPositiveSignal =
|
|
306
|
+
evidence.hasDomChange === true ||
|
|
307
|
+
evidence.hasUrlChange === true ||
|
|
308
|
+
evidence.hasNetworkActivity === true ||
|
|
309
|
+
evidence.hasStateChange === true ||
|
|
310
|
+
(Array.isArray(evidence.networkRequests) && evidence.networkRequests.length > 0) ||
|
|
311
|
+
(Array.isArray(evidence.consoleLogs) && evidence.consoleLogs.length > 0) ||
|
|
312
|
+
(evidence.before && evidence.after);
|
|
313
|
+
|
|
314
|
+
if (hasPositiveSignal) {
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Check for sensor data
|
|
319
|
+
if (evidence.sensors && typeof evidence.sensors === 'object' && Object.keys(evidence.sensors).length > 0) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Enforce contracts on a finding array
|
|
328
|
+
*
|
|
329
|
+
* Returns findings with status downgraded if necessary, filters out
|
|
330
|
+
* findings that violate critical contracts.
|
|
331
|
+
*
|
|
332
|
+
* @param {Array} findings - Array of findings to validate
|
|
333
|
+
* @returns {Object} { valid: Array, dropped: Array, downgrades: Array }
|
|
334
|
+
*/
|
|
335
|
+
export function enforceContractsOnFindings(findings) {
|
|
336
|
+
if (!Array.isArray(findings)) {
|
|
337
|
+
return { valid: [], dropped: [], downgrades: [] };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const valid = [];
|
|
341
|
+
const dropped = [];
|
|
342
|
+
const downgrades = [];
|
|
343
|
+
|
|
344
|
+
for (const finding of findings) {
|
|
345
|
+
const validation = validateFinding(finding);
|
|
346
|
+
|
|
347
|
+
if (!validation.ok && !validation.shouldDowngrade) {
|
|
348
|
+
// Critical contract violation - drop
|
|
349
|
+
dropped.push({
|
|
350
|
+
finding,
|
|
351
|
+
reason: validation.errors.join('; ')
|
|
352
|
+
});
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (validation.shouldDowngrade && validation.suggestedStatus) {
|
|
357
|
+
// Downgrade the finding
|
|
358
|
+
const downgraded = { ...finding, status: validation.suggestedStatus };
|
|
359
|
+
downgrades.push({
|
|
360
|
+
original: finding,
|
|
361
|
+
downgraded,
|
|
362
|
+
reason: validation.errors.join('; ')
|
|
363
|
+
});
|
|
364
|
+
valid.push(downgraded);
|
|
365
|
+
} else {
|
|
366
|
+
// Valid finding
|
|
367
|
+
valid.push(finding);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return { valid, dropped, downgrades };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export default {
|
|
375
|
+
validateFinding,
|
|
376
|
+
validateEvidence,
|
|
377
|
+
validateConfidence,
|
|
378
|
+
validateSignals,
|
|
379
|
+
isEvidenceSubstantive,
|
|
380
|
+
enforceContractsOnFindings
|
|
381
|
+
};
|
|
@@ -176,9 +176,11 @@ function extractUnverified(detectTruth, observeTruth) {
|
|
|
176
176
|
* @param {Object} detectTruth - Detect phase truth
|
|
177
177
|
* @param {Object} observeTruth - Observe phase truth
|
|
178
178
|
* @param {Object} _silences - Silence data (unused parameter, kept for API compatibility)
|
|
179
|
-
* @
|
|
179
|
+
* @param {Object} options - Additional options { executionMode, executionModeCeiling }
|
|
180
|
+
* @returns {Object} - Decision snapshot answering 6 mandatory questions + execution mode info
|
|
180
181
|
*/
|
|
181
|
-
export function computeDecisionSnapshot(findings, detectTruth, observeTruth, _silences) {
|
|
182
|
+
export function computeDecisionSnapshot(findings, detectTruth, observeTruth, _silences, options = {}) {
|
|
183
|
+
const { executionMode = null, executionModeCeiling = 1.0 } = options;
|
|
182
184
|
// Question 1: Do we have confirmed SILENT FAILURES?
|
|
183
185
|
const confirmedFailures = findings.filter(f =>
|
|
184
186
|
f.outcome === 'broken' || f.type === 'silent_failure'
|
|
@@ -264,10 +266,35 @@ export function computeDecisionSnapshot(findings, detectTruth, observeTruth, _si
|
|
|
264
266
|
score: confidence.score,
|
|
265
267
|
coverageRatio: confidence.coverageRatio,
|
|
266
268
|
factors: confidence.factors
|
|
267
|
-
}
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
// EXECUTION MODE
|
|
272
|
+
executionMode: executionMode,
|
|
273
|
+
executionModeCeiling: executionModeCeiling,
|
|
274
|
+
executionModeExplanation: generateExecutionModeExplanation(executionMode, executionModeCeiling)
|
|
268
275
|
};
|
|
269
276
|
}
|
|
270
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Generate explanation for execution mode awareness
|
|
280
|
+
* @param {string} mode - Execution mode (PROJECT_SCAN or WEB_SCAN_LIMITED)
|
|
281
|
+
* @param {number} ceiling - Confidence ceiling (0..1)
|
|
282
|
+
* @returns {string} - Human-readable explanation
|
|
283
|
+
*/
|
|
284
|
+
function generateExecutionModeExplanation(mode, ceiling) {
|
|
285
|
+
if (!mode) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (mode === 'WEB_SCAN_LIMITED') {
|
|
290
|
+
return `Analysis limited to runtime behavior observation (no source code). Confidence capped at ${Math.round(ceiling * 100)}%. Use source code scanning for fuller analysis.`;
|
|
291
|
+
} else if (mode === 'PROJECT_SCAN') {
|
|
292
|
+
return `Full project analysis with source code. Confidence unrestricted based on evidence quality.`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
271
298
|
/**
|
|
272
299
|
* Format decision snapshot for human reading
|
|
273
300
|
* @param {Object} snapshot - Decision snapshot
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 21.10 — Decision Trace
|
|
3
|
+
*
|
|
4
|
+
* Traces why each finding was detected, which signals contributed,
|
|
5
|
+
* which guardrails applied, and why confidence/status decisions were made.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, existsSync, writeFileSync } from 'fs';
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build decision trace for findings
|
|
13
|
+
*
|
|
14
|
+
* @param {string} projectDir - Project directory
|
|
15
|
+
* @param {string} runId - Run ID
|
|
16
|
+
* @returns {Object} Decision trace
|
|
17
|
+
*/
|
|
18
|
+
export function buildDecisionTrace(projectDir, runId) {
|
|
19
|
+
const runDir = resolve(projectDir, '.verax', 'runs', runId);
|
|
20
|
+
|
|
21
|
+
if (!existsSync(runDir)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const findingsPath = resolve(runDir, 'findings.json');
|
|
26
|
+
if (!existsSync(findingsPath)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const findings = JSON.parse(readFileSync(findingsPath, 'utf-8'));
|
|
31
|
+
const traces = [];
|
|
32
|
+
|
|
33
|
+
if (!Array.isArray(findings.findings)) {
|
|
34
|
+
return {
|
|
35
|
+
runId,
|
|
36
|
+
findings: [],
|
|
37
|
+
summary: { total: 0 },
|
|
38
|
+
generatedAt: new Date().toISOString()
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const finding of findings.findings) {
|
|
43
|
+
const findingId = finding.findingId || finding.id || `finding-${traces.length}`;
|
|
44
|
+
|
|
45
|
+
// Why detected?
|
|
46
|
+
const detectionReasons = [];
|
|
47
|
+
if (finding.type) {
|
|
48
|
+
detectionReasons.push({
|
|
49
|
+
code: 'FINDING_TYPE',
|
|
50
|
+
reason: `Finding type: ${finding.type}`,
|
|
51
|
+
value: finding.type
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (finding.outcome) {
|
|
55
|
+
detectionReasons.push({
|
|
56
|
+
code: 'OUTCOME_CLASSIFICATION',
|
|
57
|
+
reason: `Outcome: ${finding.outcome}`,
|
|
58
|
+
value: finding.outcome
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (finding.promise?.type) {
|
|
62
|
+
detectionReasons.push({
|
|
63
|
+
code: 'PROMISE_TYPE',
|
|
64
|
+
reason: `Promise type: ${finding.promise.type}`,
|
|
65
|
+
value: finding.promise.type
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Which signals contributed?
|
|
70
|
+
const signals = [];
|
|
71
|
+
if (finding.evidence?.sensors) {
|
|
72
|
+
const sensors = finding.evidence.sensors;
|
|
73
|
+
|
|
74
|
+
if (sensors.network) {
|
|
75
|
+
signals.push({
|
|
76
|
+
type: 'NETWORK',
|
|
77
|
+
contributed: sensors.network.totalRequests > 0 || sensors.network.failedRequests > 0,
|
|
78
|
+
data: {
|
|
79
|
+
totalRequests: sensors.network.totalRequests || 0,
|
|
80
|
+
failedRequests: sensors.network.failedRequests || 0
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (sensors.console) {
|
|
86
|
+
signals.push({
|
|
87
|
+
type: 'CONSOLE',
|
|
88
|
+
contributed: (sensors.console.errors || 0) > 0 || (sensors.console.warnings || 0) > 0,
|
|
89
|
+
data: {
|
|
90
|
+
errors: sensors.console.errors || 0,
|
|
91
|
+
warnings: sensors.console.warnings || 0
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (sensors.uiSignals) {
|
|
97
|
+
signals.push({
|
|
98
|
+
type: 'UI_SIGNALS',
|
|
99
|
+
contributed: sensors.uiSignals.diff?.changed === true,
|
|
100
|
+
data: {
|
|
101
|
+
changed: sensors.uiSignals.diff?.changed || false
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Which guardrails applied?
|
|
108
|
+
const guardrailsApplied = [];
|
|
109
|
+
if (finding.guardrails?.appliedRules) {
|
|
110
|
+
for (const rule of finding.guardrails.appliedRules) {
|
|
111
|
+
guardrailsApplied.push({
|
|
112
|
+
ruleId: rule.id || rule,
|
|
113
|
+
category: rule.category || null,
|
|
114
|
+
action: rule.action || null,
|
|
115
|
+
matched: rule.matched || true
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Why confidence = X?
|
|
121
|
+
const confidenceReasons = [];
|
|
122
|
+
if (finding.confidenceLevel) {
|
|
123
|
+
confidenceReasons.push({
|
|
124
|
+
code: 'CONFIDENCE_LEVEL',
|
|
125
|
+
reason: `Confidence level: ${finding.confidenceLevel}`,
|
|
126
|
+
value: finding.confidenceLevel
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (finding.confidence !== undefined) {
|
|
130
|
+
confidenceReasons.push({
|
|
131
|
+
code: 'CONFIDENCE_SCORE',
|
|
132
|
+
reason: `Confidence score: ${finding.confidence}`,
|
|
133
|
+
value: finding.confidence
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (finding.confidenceReasons && Array.isArray(finding.confidenceReasons)) {
|
|
137
|
+
for (const reason of finding.confidenceReasons) {
|
|
138
|
+
confidenceReasons.push({
|
|
139
|
+
code: 'CONFIDENCE_FACTOR',
|
|
140
|
+
reason: reason,
|
|
141
|
+
value: reason
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Why status = CONFIRMED / SUSPECTED / DROPPED?
|
|
147
|
+
const statusReasons = [];
|
|
148
|
+
const status = finding.severity || finding.status || 'SUSPECTED';
|
|
149
|
+
|
|
150
|
+
statusReasons.push({
|
|
151
|
+
code: 'STATUS_ASSIGNED',
|
|
152
|
+
reason: `Status: ${status}`,
|
|
153
|
+
value: status
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (finding.evidencePackage) {
|
|
157
|
+
if (finding.evidencePackage.isComplete) {
|
|
158
|
+
statusReasons.push({
|
|
159
|
+
code: 'EVIDENCE_COMPLETE',
|
|
160
|
+
reason: 'Evidence package is complete',
|
|
161
|
+
value: true
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
statusReasons.push({
|
|
165
|
+
code: 'EVIDENCE_INCOMPLETE',
|
|
166
|
+
reason: 'Evidence package is incomplete',
|
|
167
|
+
value: false
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (finding.guardrails?.finalDecision) {
|
|
173
|
+
statusReasons.push({
|
|
174
|
+
code: 'GUARDRAILS_DECISION',
|
|
175
|
+
reason: `Guardrails decision: ${finding.guardrails.finalDecision}`,
|
|
176
|
+
value: finding.guardrails.finalDecision
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (status === 'CONFIRMED' && finding.evidencePackage && !finding.evidencePackage.isComplete) {
|
|
181
|
+
statusReasons.push({
|
|
182
|
+
code: 'EVIDENCE_LAW_VIOLATION',
|
|
183
|
+
reason: 'CONFIRMED finding with incomplete evidence violates Evidence Law',
|
|
184
|
+
value: 'VIOLATION'
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
traces.push({
|
|
189
|
+
findingId,
|
|
190
|
+
detection: {
|
|
191
|
+
why: detectionReasons,
|
|
192
|
+
signals: signals.filter(s => s.contributed),
|
|
193
|
+
expectationId: finding.expectationId || null,
|
|
194
|
+
interactionId: finding.interaction?.selector || null
|
|
195
|
+
},
|
|
196
|
+
guardrails: {
|
|
197
|
+
applied: guardrailsApplied,
|
|
198
|
+
finalDecision: finding.guardrails?.finalDecision || null,
|
|
199
|
+
contradictions: finding.guardrails?.contradictions || []
|
|
200
|
+
},
|
|
201
|
+
confidence: {
|
|
202
|
+
level: finding.confidenceLevel || null,
|
|
203
|
+
score: finding.confidence !== undefined ? finding.confidence : null,
|
|
204
|
+
why: confidenceReasons
|
|
205
|
+
},
|
|
206
|
+
status: {
|
|
207
|
+
value: status,
|
|
208
|
+
why: statusReasons
|
|
209
|
+
},
|
|
210
|
+
evidence: {
|
|
211
|
+
packageId: finding.evidencePackage?.id || null,
|
|
212
|
+
isComplete: finding.evidencePackage?.isComplete || false,
|
|
213
|
+
files: finding.evidencePackage?.files || []
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
runId,
|
|
220
|
+
findings: traces,
|
|
221
|
+
summary: {
|
|
222
|
+
total: traces.length,
|
|
223
|
+
byStatus: traces.reduce((acc, t) => {
|
|
224
|
+
const status = t.status.value;
|
|
225
|
+
acc[status] = (acc[status] || 0) + 1;
|
|
226
|
+
return acc;
|
|
227
|
+
}, {}),
|
|
228
|
+
byConfidence: traces.reduce((acc, t) => {
|
|
229
|
+
const level = t.confidence.level || 'UNKNOWN';
|
|
230
|
+
acc[level] = (acc[level] || 0) + 1;
|
|
231
|
+
return acc;
|
|
232
|
+
}, {}),
|
|
233
|
+
withGuardrails: traces.filter(t => t.guardrails.applied.length > 0).length,
|
|
234
|
+
withCompleteEvidence: traces.filter(t => t.evidence.isComplete).length
|
|
235
|
+
},
|
|
236
|
+
generatedAt: new Date().toISOString()
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Write decision trace to file
|
|
242
|
+
*
|
|
243
|
+
* @param {string} projectDir - Project directory
|
|
244
|
+
* @param {string} runId - Run ID
|
|
245
|
+
* @param {Object} trace - Decision trace
|
|
246
|
+
* @returns {string} Path to written file
|
|
247
|
+
*/
|
|
248
|
+
export function writeDecisionTrace(projectDir, runId, trace) {
|
|
249
|
+
const runDir = resolve(projectDir, '.verax', 'runs', runId);
|
|
250
|
+
const outputPath = resolve(runDir, 'decisions.trace.json');
|
|
251
|
+
writeFileSync(outputPath, JSON.stringify(trace, null, 2), 'utf-8');
|
|
252
|
+
return outputPath;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Load decision trace from file
|
|
257
|
+
*
|
|
258
|
+
* @param {string} projectDir - Project directory
|
|
259
|
+
* @param {string} runId - Run ID
|
|
260
|
+
* @returns {Object|null} Decision trace or null
|
|
261
|
+
*/
|
|
262
|
+
export function loadDecisionTrace(projectDir, runId) {
|
|
263
|
+
const runDir = resolve(projectDir, '.verax', 'runs', runId);
|
|
264
|
+
const tracePath = resolve(runDir, 'decisions.trace.json');
|
|
265
|
+
|
|
266
|
+
if (!existsSync(tracePath)) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
return JSON.parse(readFileSync(tracePath, 'utf-8'));
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|