@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.
Files changed (152) hide show
  1. package/README.md +14 -18
  2. package/bin/verax.js +7 -0
  3. package/package.json +3 -3
  4. package/src/cli/commands/baseline.js +104 -0
  5. package/src/cli/commands/default.js +79 -25
  6. package/src/cli/commands/ga.js +243 -0
  7. package/src/cli/commands/gates.js +95 -0
  8. package/src/cli/commands/inspect.js +131 -2
  9. package/src/cli/commands/release-check.js +213 -0
  10. package/src/cli/commands/run.js +246 -35
  11. package/src/cli/commands/security-check.js +211 -0
  12. package/src/cli/commands/truth.js +114 -0
  13. package/src/cli/entry.js +304 -67
  14. package/src/cli/util/angular-component-extractor.js +179 -0
  15. package/src/cli/util/angular-navigation-detector.js +141 -0
  16. package/src/cli/util/angular-network-detector.js +161 -0
  17. package/src/cli/util/angular-state-detector.js +162 -0
  18. package/src/cli/util/ast-interactive-detector.js +546 -0
  19. package/src/cli/util/ast-network-detector.js +603 -0
  20. package/src/cli/util/ast-usestate-detector.js +602 -0
  21. package/src/cli/util/bootstrap-guard.js +86 -0
  22. package/src/cli/util/determinism-runner.js +123 -0
  23. package/src/cli/util/determinism-writer.js +129 -0
  24. package/src/cli/util/env-url.js +4 -0
  25. package/src/cli/util/expectation-extractor.js +369 -73
  26. package/src/cli/util/findings-writer.js +126 -16
  27. package/src/cli/util/learn-writer.js +3 -1
  28. package/src/cli/util/observe-writer.js +3 -1
  29. package/src/cli/util/paths.js +3 -12
  30. package/src/cli/util/project-discovery.js +3 -0
  31. package/src/cli/util/project-writer.js +3 -1
  32. package/src/cli/util/run-resolver.js +64 -0
  33. package/src/cli/util/source-requirement.js +55 -0
  34. package/src/cli/util/summary-writer.js +1 -0
  35. package/src/cli/util/svelte-navigation-detector.js +163 -0
  36. package/src/cli/util/svelte-network-detector.js +80 -0
  37. package/src/cli/util/svelte-sfc-extractor.js +147 -0
  38. package/src/cli/util/svelte-state-detector.js +243 -0
  39. package/src/cli/util/vue-navigation-detector.js +177 -0
  40. package/src/cli/util/vue-sfc-extractor.js +162 -0
  41. package/src/cli/util/vue-state-detector.js +215 -0
  42. package/src/verax/cli/finding-explainer.js +56 -3
  43. package/src/verax/core/artifacts/registry.js +154 -0
  44. package/src/verax/core/artifacts/verifier.js +980 -0
  45. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  46. package/src/verax/core/baseline/baseline.snapshot.js +231 -0
  47. package/src/verax/core/capabilities/gates.js +499 -0
  48. package/src/verax/core/capabilities/registry.js +475 -0
  49. package/src/verax/core/confidence/confidence-compute.js +137 -0
  50. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  51. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  52. package/src/verax/core/confidence/confidence-weights.js +44 -0
  53. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  54. package/src/verax/core/confidence/confidence.loader.js +79 -0
  55. package/src/verax/core/confidence/confidence.schema.js +94 -0
  56. package/src/verax/core/confidence-engine-refactor.js +484 -0
  57. package/src/verax/core/confidence-engine.js +486 -0
  58. package/src/verax/core/confidence-engine.js.backup +471 -0
  59. package/src/verax/core/contracts/index.js +29 -0
  60. package/src/verax/core/contracts/types.js +185 -0
  61. package/src/verax/core/contracts/validators.js +381 -0
  62. package/src/verax/core/decision-snapshot.js +30 -3
  63. package/src/verax/core/decisions/decision.trace.js +276 -0
  64. package/src/verax/core/determinism/contract-writer.js +89 -0
  65. package/src/verax/core/determinism/contract.js +139 -0
  66. package/src/verax/core/determinism/diff.js +364 -0
  67. package/src/verax/core/determinism/engine.js +221 -0
  68. package/src/verax/core/determinism/finding-identity.js +148 -0
  69. package/src/verax/core/determinism/normalize.js +438 -0
  70. package/src/verax/core/determinism/report-writer.js +92 -0
  71. package/src/verax/core/determinism/run-fingerprint.js +118 -0
  72. package/src/verax/core/dynamic-route-intelligence.js +528 -0
  73. package/src/verax/core/evidence/evidence-capture-service.js +307 -0
  74. package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
  75. package/src/verax/core/evidence-builder.js +487 -0
  76. package/src/verax/core/execution-mode-context.js +77 -0
  77. package/src/verax/core/execution-mode-detector.js +190 -0
  78. package/src/verax/core/failures/exit-codes.js +86 -0
  79. package/src/verax/core/failures/failure-summary.js +76 -0
  80. package/src/verax/core/failures/failure.factory.js +225 -0
  81. package/src/verax/core/failures/failure.ledger.js +132 -0
  82. package/src/verax/core/failures/failure.types.js +196 -0
  83. package/src/verax/core/failures/index.js +10 -0
  84. package/src/verax/core/ga/ga-report-writer.js +43 -0
  85. package/src/verax/core/ga/ga.artifact.js +49 -0
  86. package/src/verax/core/ga/ga.contract.js +434 -0
  87. package/src/verax/core/ga/ga.enforcer.js +86 -0
  88. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  89. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  90. package/src/verax/core/guardrails/policy.loader.js +83 -0
  91. package/src/verax/core/guardrails/policy.schema.js +110 -0
  92. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  93. package/src/verax/core/guardrails-engine.js +505 -0
  94. package/src/verax/core/observe/run-timeline.js +316 -0
  95. package/src/verax/core/perf/perf.contract.js +186 -0
  96. package/src/verax/core/perf/perf.display.js +65 -0
  97. package/src/verax/core/perf/perf.enforcer.js +91 -0
  98. package/src/verax/core/perf/perf.monitor.js +209 -0
  99. package/src/verax/core/perf/perf.report.js +198 -0
  100. package/src/verax/core/pipeline-tracker.js +238 -0
  101. package/src/verax/core/product-definition.js +127 -0
  102. package/src/verax/core/release/provenance.builder.js +271 -0
  103. package/src/verax/core/release/release-report-writer.js +40 -0
  104. package/src/verax/core/release/release.enforcer.js +159 -0
  105. package/src/verax/core/release/reproducibility.check.js +221 -0
  106. package/src/verax/core/release/sbom.builder.js +283 -0
  107. package/src/verax/core/report/cross-index.js +192 -0
  108. package/src/verax/core/report/human-summary.js +222 -0
  109. package/src/verax/core/route-intelligence.js +419 -0
  110. package/src/verax/core/security/secrets.scan.js +326 -0
  111. package/src/verax/core/security/security-report.js +50 -0
  112. package/src/verax/core/security/security.enforcer.js +124 -0
  113. package/src/verax/core/security/supplychain.defaults.json +38 -0
  114. package/src/verax/core/security/supplychain.policy.js +326 -0
  115. package/src/verax/core/security/vuln.scan.js +265 -0
  116. package/src/verax/core/truth/truth.certificate.js +250 -0
  117. package/src/verax/core/ui-feedback-intelligence.js +515 -0
  118. package/src/verax/detect/confidence-engine.js +628 -40
  119. package/src/verax/detect/confidence-helper.js +33 -0
  120. package/src/verax/detect/detection-engine.js +18 -1
  121. package/src/verax/detect/dynamic-route-findings.js +335 -0
  122. package/src/verax/detect/expectation-chain-detector.js +417 -0
  123. package/src/verax/detect/expectation-model.js +3 -1
  124. package/src/verax/detect/findings-writer.js +141 -5
  125. package/src/verax/detect/index.js +229 -5
  126. package/src/verax/detect/journey-stall-detector.js +558 -0
  127. package/src/verax/detect/route-findings.js +218 -0
  128. package/src/verax/detect/ui-feedback-findings.js +207 -0
  129. package/src/verax/detect/verdict-engine.js +57 -3
  130. package/src/verax/detect/view-switch-correlator.js +242 -0
  131. package/src/verax/index.js +413 -45
  132. package/src/verax/learn/action-contract-extractor.js +682 -64
  133. package/src/verax/learn/route-validator.js +4 -1
  134. package/src/verax/observe/index.js +88 -843
  135. package/src/verax/observe/interaction-runner.js +25 -8
  136. package/src/verax/observe/observe-context.js +205 -0
  137. package/src/verax/observe/observe-helpers.js +191 -0
  138. package/src/verax/observe/observe-runner.js +226 -0
  139. package/src/verax/observe/observers/budget-observer.js +185 -0
  140. package/src/verax/observe/observers/console-observer.js +102 -0
  141. package/src/verax/observe/observers/coverage-observer.js +107 -0
  142. package/src/verax/observe/observers/interaction-observer.js +471 -0
  143. package/src/verax/observe/observers/navigation-observer.js +132 -0
  144. package/src/verax/observe/observers/network-observer.js +87 -0
  145. package/src/verax/observe/observers/safety-observer.js +82 -0
  146. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  147. package/src/verax/observe/ui-feedback-detector.js +742 -0
  148. package/src/verax/observe/ui-signal-sensor.js +148 -2
  149. package/src/verax/scan-summary-writer.js +42 -8
  150. package/src/verax/shared/artifact-manager.js +8 -5
  151. package/src/verax/shared/css-spinner-rules.js +204 -0
  152. 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
- * @returns {Object} - Decision snapshot answering 6 mandatory questions
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
+