@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,307 @@
1
+ /**
2
+ * EVIDENCE CAPTURE RELIABILITY LAYER
3
+ *
4
+ * Centralized evidence capture service with retries and failure tracking.
5
+ * Ensures evidence capture is resilient and failures are never silent.
6
+ */
7
+
8
+ import { captureScreenshot } from '../../observe/evidence-capture.js';
9
+ import { captureDomSignature } from '../../observe/dom-signature.js';
10
+
11
+ /**
12
+ * Evidence capture failure reason codes (stable)
13
+ */
14
+ export const EVIDENCE_CAPTURE_FAILURE_CODES = {
15
+ SCREENSHOT_FAILED: 'EVIDENCE_SCREENSHOT_FAILED',
16
+ SCREENSHOT_TIMEOUT: 'EVIDENCE_SCREENSHOT_TIMEOUT',
17
+ DOM_SIGNATURE_FAILED: 'EVIDENCE_DOM_SIGNATURE_FAILED',
18
+ URL_CAPTURE_FAILED: 'EVIDENCE_URL_CAPTURE_FAILED',
19
+ UISIGNALS_CAPTURE_FAILED: 'EVIDENCE_UISIGNALS_CAPTURE_FAILED',
20
+ NETWORK_CAPTURE_FAILED: 'EVIDENCE_NETWORK_CAPTURE_FAILED',
21
+ UNKNOWN_ERROR: 'EVIDENCE_UNKNOWN_ERROR'
22
+ };
23
+
24
+ /**
25
+ * Evidence capture stage codes (stable)
26
+ */
27
+ export const EVIDENCE_CAPTURE_STAGE = {
28
+ BEFORE_SCREENSHOT: 'BEFORE_SCREENSHOT',
29
+ AFTER_SCREENSHOT: 'AFTER_SCREENSHOT',
30
+ DOM_SIGNATURE: 'DOM_SIGNATURE',
31
+ URL: 'URL',
32
+ UISIGNALS: 'UISIGNALS',
33
+ NETWORK: 'NETWORK'
34
+ };
35
+
36
+ /**
37
+ * EvidenceCaptureFailure object (structured failure record)
38
+ */
39
+ export class EvidenceCaptureFailure {
40
+ constructor(stage, reasonCode, reason, stackSummary = null, attemptCount = 1) {
41
+ this.stage = stage;
42
+ this.reasonCode = reasonCode;
43
+ this.reason = reason;
44
+ this.stackSummary = stackSummary || this._extractStackSummary(new Error().stack);
45
+ this.attemptCount = attemptCount;
46
+ this.timestamp = new Date().toISOString();
47
+ }
48
+
49
+ _extractStackSummary(stack) {
50
+ if (!stack) return null;
51
+ const lines = stack.split('\n').slice(0, 5);
52
+ return lines.join('\n');
53
+ }
54
+
55
+ toJSON() {
56
+ return {
57
+ stage: this.stage,
58
+ reasonCode: this.reasonCode,
59
+ reason: this.reason,
60
+ stackSummary: this.stackSummary,
61
+ attemptCount: this.attemptCount,
62
+ timestamp: this.timestamp
63
+ };
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Screenshot capture with retries
69
+ *
70
+ * @param {Object} page - Playwright page object
71
+ * @param {string} filepath - Path to save screenshot
72
+ * @param {Object} options - Options { maxRetries: 2, retryDelayMs: 100 }
73
+ * @returns {Promise<{ success: boolean, filepath: string | null, failure: EvidenceCaptureFailure | null }>}
74
+ */
75
+ export async function captureScreenshotWithRetry(page, filepath, options = {}) {
76
+ const maxRetries = options.maxRetries || 2;
77
+ const retryDelayMs = options.retryDelayMs || 100;
78
+
79
+ let lastError = null;
80
+ let attemptCount = 0;
81
+
82
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
83
+ attemptCount = attempt + 1;
84
+ try {
85
+ await captureScreenshot(page, filepath);
86
+ // Verify file was created
87
+ const { existsSync } = await import('fs');
88
+ if (existsSync(filepath)) {
89
+ return { success: true, filepath, failure: null };
90
+ }
91
+ throw new Error('Screenshot file was not created');
92
+ } catch (error) {
93
+ lastError = error;
94
+
95
+ // If not last attempt, wait before retry
96
+ if (attempt < maxRetries) {
97
+ await new Promise(resolve => setTimeout(resolve, retryDelayMs));
98
+ }
99
+ }
100
+ }
101
+
102
+ // All retries failed
103
+ const failure = new EvidenceCaptureFailure(
104
+ filepath.includes('before') ? EVIDENCE_CAPTURE_STAGE.BEFORE_SCREENSHOT : EVIDENCE_CAPTURE_STAGE.AFTER_SCREENSHOT,
105
+ lastError?.message?.includes('timeout') ? EVIDENCE_CAPTURE_FAILURE_CODES.SCREENSHOT_TIMEOUT : EVIDENCE_CAPTURE_FAILURE_CODES.SCREENSHOT_FAILED,
106
+ lastError?.message || 'Screenshot capture failed after retries',
107
+ lastError?.stack,
108
+ attemptCount
109
+ );
110
+
111
+ return { success: false, filepath: null, failure };
112
+ }
113
+
114
+ /**
115
+ * Capture DOM signature with error handling
116
+ *
117
+ * @param {Object} page - Playwright page object
118
+ * @returns {Promise<{ success: boolean, domSignature: string | null, failure: EvidenceCaptureFailure | null }>}
119
+ */
120
+ export async function captureDomSignatureSafe(page) {
121
+ try {
122
+ const domSignature = await captureDomSignature(page);
123
+ return { success: true, domSignature, failure: null };
124
+ } catch (error) {
125
+ const failure = new EvidenceCaptureFailure(
126
+ EVIDENCE_CAPTURE_STAGE.DOM_SIGNATURE,
127
+ EVIDENCE_CAPTURE_FAILURE_CODES.DOM_SIGNATURE_FAILED,
128
+ error.message || 'DOM signature capture failed',
129
+ error.stack
130
+ );
131
+ return { success: false, domSignature: null, failure };
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Capture URL with error handling
137
+ *
138
+ * @param {Object} page - Playwright page object
139
+ * @returns {Promise<{ success: boolean, url: string | null, failure: EvidenceCaptureFailure | null }>}
140
+ */
141
+ export async function captureUrlSafe(page) {
142
+ try {
143
+ const url = page.url();
144
+ return { success: true, url, failure: null };
145
+ } catch (error) {
146
+ const failure = new EvidenceCaptureFailure(
147
+ EVIDENCE_CAPTURE_STAGE.URL,
148
+ EVIDENCE_CAPTURE_FAILURE_CODES.URL_CAPTURE_FAILED,
149
+ error.message || 'URL capture failed',
150
+ error.stack
151
+ );
152
+ return { success: false, url: null, failure };
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Capture UI signals snapshot with error handling
158
+ *
159
+ * @param {Object} uiSignalSensor - UISignalSensor instance
160
+ * @param {Object} page - Playwright page object
161
+ * @param {number} interactionTime - Optional interaction timestamp
162
+ * @param {Object} beforeSnapshot - Optional before snapshot
163
+ * @returns {Promise<{ success: boolean, uiSignals: Object | null, failure: EvidenceCaptureFailure | null }>}
164
+ */
165
+ export async function captureUiSignalsSafe(uiSignalSensor, page, interactionTime = null, beforeSnapshot = null) {
166
+ try {
167
+ const uiSignals = await uiSignalSensor.snapshot(page, interactionTime, beforeSnapshot);
168
+ return { success: true, uiSignals, failure: null };
169
+ } catch (error) {
170
+ const failure = new EvidenceCaptureFailure(
171
+ EVIDENCE_CAPTURE_STAGE.UISIGNALS,
172
+ EVIDENCE_CAPTURE_FAILURE_CODES.UISIGNALS_CAPTURE_FAILED,
173
+ error.message || 'UI signals capture failed',
174
+ error.stack
175
+ );
176
+ return { success: false, uiSignals: null, failure };
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Capture network snapshot with error handling
182
+ *
183
+ * @param {Object} networkSensor - NetworkSensor instance
184
+ * @param {string} windowId - Network window ID
185
+ * @returns {Promise<{ success: boolean, networkSummary: Object | null, failure: EvidenceCaptureFailure | null }>}
186
+ */
187
+ export async function captureNetworkSafe(networkSensor, windowId) {
188
+ try {
189
+ const networkSummary = networkSensor.stopWindow(windowId);
190
+ return { success: true, networkSummary, failure: null };
191
+ } catch (error) {
192
+ const failure = new EvidenceCaptureFailure(
193
+ EVIDENCE_CAPTURE_STAGE.NETWORK,
194
+ EVIDENCE_CAPTURE_FAILURE_CODES.NETWORK_CAPTURE_FAILED,
195
+ error.message || 'Network capture failed',
196
+ error.stack
197
+ );
198
+ return { success: false, networkSummary: null, failure };
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Comprehensive evidence capture for a finding
204
+ *
205
+ * @param {Object} params - Capture parameters
206
+ * @param {Object} params.page - Playwright page object
207
+ * @param {string} params.beforeScreenshotPath - Before screenshot path
208
+ * @param {string} params.afterScreenshotPath - After screenshot path
209
+ * @param {Object} params.uiSignalSensor - UISignalSensor instance
210
+ * @param {Object} params.networkSensor - NetworkSensor instance
211
+ * @param {string} params.networkWindowId - Network window ID
212
+ * @param {number} params.interactionTime - Interaction timestamp
213
+ * @param {Object} params.beforeSnapshot - Before snapshot
214
+ * @returns {Promise<{ evidence: Object, failures: Array<EvidenceCaptureFailure> }>}
215
+ */
216
+ export async function captureEvidenceComprehensive(params) {
217
+ const {
218
+ page,
219
+ beforeScreenshotPath,
220
+ afterScreenshotPath,
221
+ uiSignalSensor,
222
+ networkSensor,
223
+ networkWindowId,
224
+ interactionTime,
225
+ beforeSnapshot
226
+ } = params;
227
+
228
+ const failures = [];
229
+ const evidence = {
230
+ before: {},
231
+ after: {},
232
+ signals: {}
233
+ };
234
+
235
+ // Capture before screenshot
236
+ if (beforeScreenshotPath) {
237
+ const beforeScreenshot = await captureScreenshotWithRetry(page, beforeScreenshotPath);
238
+ if (beforeScreenshot.success) {
239
+ evidence.before.screenshot = beforeScreenshotPath;
240
+ } else {
241
+ failures.push(beforeScreenshot.failure);
242
+ }
243
+ }
244
+
245
+ // Capture after screenshot
246
+ if (afterScreenshotPath) {
247
+ const afterScreenshot = await captureScreenshotWithRetry(page, afterScreenshotPath);
248
+ if (afterScreenshot.success) {
249
+ evidence.after.screenshot = afterScreenshotPath;
250
+ } else {
251
+ failures.push(afterScreenshot.failure);
252
+ }
253
+ }
254
+
255
+ // Capture URLs
256
+ const beforeUrl = await captureUrlSafe(page);
257
+ if (beforeUrl.success) {
258
+ evidence.before.url = beforeUrl.url;
259
+ } else {
260
+ failures.push(beforeUrl.failure);
261
+ }
262
+
263
+ const afterUrl = await captureUrlSafe(page);
264
+ if (afterUrl.success) {
265
+ evidence.after.url = afterUrl.url;
266
+ } else {
267
+ failures.push(afterUrl.failure);
268
+ }
269
+
270
+ // Capture DOM signatures
271
+ const beforeDom = await captureDomSignatureSafe(page);
272
+ if (beforeDom.success) {
273
+ evidence.before.domSignature = beforeDom.domSignature;
274
+ } else {
275
+ failures.push(beforeDom.failure);
276
+ }
277
+
278
+ const afterDom = await captureDomSignatureSafe(page);
279
+ if (afterDom.success) {
280
+ evidence.after.domSignature = afterDom.domSignature;
281
+ } else {
282
+ failures.push(afterDom.failure);
283
+ }
284
+
285
+ // Capture UI signals
286
+ if (uiSignalSensor) {
287
+ const uiSignals = await captureUiSignalsSafe(uiSignalSensor, page, interactionTime, beforeSnapshot);
288
+ if (uiSignals.success) {
289
+ evidence.signals.uiSignals = uiSignals.uiSignals;
290
+ } else {
291
+ failures.push(uiSignals.failure);
292
+ }
293
+ }
294
+
295
+ // Capture network
296
+ if (networkSensor && networkWindowId) {
297
+ const network = await captureNetworkSafe(networkSensor, networkWindowId);
298
+ if (network.success) {
299
+ evidence.signals.network = network.networkSummary;
300
+ } else {
301
+ failures.push(network.failure);
302
+ }
303
+ }
304
+
305
+ return { evidence, failures };
306
+ }
307
+
@@ -0,0 +1,165 @@
1
+ /**
2
+ * EVIDENCE INTENT LEDGER
3
+ *
4
+ * Audit trail of evidence capture attempts per finding.
5
+ * Records what evidence was required, what was captured, and what failed.
6
+ * This is NOT proof; it is an audit trail proving we attempted to capture evidence.
7
+ */
8
+
9
+ import { writeFileSync, readFileSync, existsSync } from 'fs';
10
+ import { resolve } from 'path';
11
+ import { EVIDENCE_CAPTURE_STAGE, EVIDENCE_CAPTURE_FAILURE_CODES } from './evidence-capture-service.js';
12
+
13
+ /**
14
+ * Required evidence fields for CONFIRMED findings (stable)
15
+ */
16
+ export const REQUIRED_EVIDENCE_FIELDS = [
17
+ 'trigger.source',
18
+ 'before.screenshot',
19
+ 'after.screenshot',
20
+ 'before.url',
21
+ 'after.url',
22
+ 'action.interaction',
23
+ 'signals.network',
24
+ 'signals.uiSignals'
25
+ ];
26
+
27
+ /**
28
+ * Build evidence intent entry for a finding
29
+ *
30
+ * @param {Object} finding - Finding object
31
+ * @param {Object} evidencePackage - Evidence package
32
+ * @param {Array<Object>} captureFailures - Array of EvidenceCaptureFailure objects
33
+ * @returns {Object} Evidence intent entry
34
+ */
35
+ export function buildEvidenceIntentEntry(finding, evidencePackage, captureFailures = []) {
36
+ const findingIdentity = finding.findingId || finding.id || `finding-${Date.now()}`;
37
+
38
+ // Determine required fields based on finding severity
39
+ const requiredFields = finding.severity === 'CONFIRMED' || finding.status === 'CONFIRMED'
40
+ ? REQUIRED_EVIDENCE_FIELDS
41
+ : [];
42
+
43
+ // Check capture outcomes for each required field
44
+ const captureOutcomes = {};
45
+ const missingFields = [];
46
+
47
+ for (const field of requiredFields) {
48
+ const [section, key] = field.split('.');
49
+
50
+ let captured = false;
51
+ let failure = null;
52
+
53
+ // Check if field exists in evidence package
54
+ if (section === 'trigger' && evidencePackage.trigger?.[key]) {
55
+ captured = true;
56
+ } else if (section === 'before' && evidencePackage.before?.[key]) {
57
+ captured = true;
58
+ } else if (section === 'after' && evidencePackage.after?.[key]) {
59
+ captured = true;
60
+ } else if (section === 'action' && evidencePackage.action?.[key]) {
61
+ captured = true;
62
+ } else if (section === 'signals' && evidencePackage.signals?.[key]) {
63
+ captured = true;
64
+ }
65
+
66
+ // Check if there was a capture failure for this field
67
+ if (!captured) {
68
+ const fieldFailure = captureFailures.find(f => {
69
+ if (field.includes('screenshot') && f.stage.includes('SCREENSHOT')) return true;
70
+ if (field.includes('domSignature') && f.stage === EVIDENCE_CAPTURE_STAGE.DOM_SIGNATURE) return true;
71
+ if (field.includes('url') && f.stage === EVIDENCE_CAPTURE_STAGE.URL) return true;
72
+ if (field.includes('uiSignals') && f.stage === EVIDENCE_CAPTURE_STAGE.UISIGNALS) return true;
73
+ if (field.includes('network') && f.stage === EVIDENCE_CAPTURE_STAGE.NETWORK) return true;
74
+ return false;
75
+ });
76
+
77
+ if (fieldFailure) {
78
+ failure = {
79
+ stage: fieldFailure.stage,
80
+ reasonCode: fieldFailure.reasonCode,
81
+ reason: fieldFailure.reason
82
+ };
83
+ }
84
+
85
+ missingFields.push(field);
86
+ }
87
+
88
+ captureOutcomes[field] = {
89
+ required: true,
90
+ captured,
91
+ failure
92
+ };
93
+ }
94
+
95
+ return {
96
+ findingIdentity,
97
+ findingType: finding.type || 'finding',
98
+ severity: finding.severity || finding.status || 'SUSPECTED',
99
+ requiredFields,
100
+ captureOutcomes,
101
+ missingFields,
102
+ evidencePackageComplete: evidencePackage.isComplete === true,
103
+ timestamp: new Date().toISOString()
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Write evidence intent ledger
109
+ *
110
+ * @param {string} runDir - Run directory
111
+ * @param {Array<Object>} findings - Array of findings with evidence packages
112
+ * @param {Map<string, Array<Object>>} captureFailuresMap - Map of findingId -> capture failures
113
+ * @returns {string} Path to written evidence.intent.json
114
+ */
115
+ export function writeEvidenceIntentLedger(runDir, findings, captureFailuresMap = new Map()) {
116
+ const intentPath = resolve(runDir, 'evidence.intent.json');
117
+
118
+ // Build intent entries (deterministic ordering by findingIdentity)
119
+ const entries = [];
120
+
121
+ for (const finding of findings || []) {
122
+ const findingIdentity = finding.findingId || finding.id || `finding-${findings.indexOf(finding)}`;
123
+ const captureFailures = captureFailuresMap.get(findingIdentity) || [];
124
+ const evidencePackage = finding.evidencePackage || {};
125
+
126
+ const entry = buildEvidenceIntentEntry(finding, evidencePackage, captureFailures);
127
+ entries.push(entry);
128
+ }
129
+
130
+ // Sort by findingIdentity for deterministic ordering
131
+ entries.sort((a, b) => a.findingIdentity.localeCompare(b.findingIdentity));
132
+
133
+ const ledger = {
134
+ version: 1,
135
+ generatedAt: new Date().toISOString(),
136
+ totalFindings: entries.length,
137
+ entries
138
+ };
139
+
140
+ writeFileSync(intentPath, JSON.stringify(ledger, null, 2) + '\n');
141
+
142
+ return intentPath;
143
+ }
144
+
145
+ /**
146
+ * Read evidence intent ledger
147
+ *
148
+ * @param {string} runDir - Run directory
149
+ * @returns {Object | null} Evidence intent ledger or null if not found
150
+ */
151
+ export function readEvidenceIntentLedger(runDir) {
152
+ const intentPath = resolve(runDir, 'evidence.intent.json');
153
+
154
+ if (!existsSync(intentPath)) {
155
+ return null;
156
+ }
157
+
158
+ try {
159
+ const content = readFileSync(intentPath, 'utf-8');
160
+ return JSON.parse(content);
161
+ } catch (error) {
162
+ return null;
163
+ }
164
+ }
165
+