@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,438 @@
1
+ /**
2
+ * PHASE 18 — Determinism Normalization Layer
3
+ *
4
+ * Normalizes artifacts for deterministic comparison by:
5
+ * - Stripping volatile fields (timestamps, runId, absolute paths, temp dirs)
6
+ * - Normalizing ordering (sort arrays by stable keys)
7
+ * - Normalizing evidence paths but preserving evidence presence/absence
8
+ * - Normalizing floating scores with fixed rounding
9
+ */
10
+
11
+ /**
12
+ * PHASE 18: Normalize artifact for comparison
13
+ *
14
+ * @param {string} artifactName - Name of artifact (findings, runStatus, etc.)
15
+ * @param {Object} json - Artifact JSON object
16
+ * @returns {Object} Normalized artifact
17
+ */
18
+ export function normalizeArtifact(artifactName, json) {
19
+ if (!json || typeof json !== 'object') {
20
+ return json;
21
+ }
22
+
23
+ // Deep clone to avoid mutating original
24
+ const normalized = JSON.parse(JSON.stringify(json));
25
+
26
+ // Apply artifact-specific normalization
27
+ switch (artifactName) {
28
+ case 'findings':
29
+ return normalizeFindings(normalized);
30
+ case 'runStatus':
31
+ return normalizeRunStatus(normalized);
32
+ case 'summary':
33
+ return normalizeSummary(normalized);
34
+ case 'learn':
35
+ return normalizeLearn(normalized);
36
+ case 'evidenceIntent':
37
+ return normalizeEvidenceIntent(normalized);
38
+ case 'guardrailsReport':
39
+ return normalizeGuardrailsReport(normalized);
40
+ case 'confidenceReport':
41
+ return normalizeConfidenceReport(normalized);
42
+ case 'determinismContract':
43
+ return normalizeDeterminismContract(normalized);
44
+ case 'runMeta':
45
+ return normalizeRunMeta(normalized);
46
+ case 'observe':
47
+ case 'traces':
48
+ return normalizeTraces(normalized);
49
+ case 'performanceReport':
50
+ return normalizePerformanceReport(normalized);
51
+ case 'securityReport':
52
+ return normalizeSecurityReport(normalized);
53
+ case 'gaReport':
54
+ return normalizeGAReport(normalized);
55
+ case 'releaseReport':
56
+ return normalizeReleaseReport(normalized);
57
+ default:
58
+ return normalizeGeneric(normalized);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Normalize findings artifact
64
+ */
65
+ function normalizeFindings(artifact) {
66
+ const normalized = { ...artifact };
67
+
68
+ // Remove volatile top-level fields
69
+ delete normalized.detectedAt;
70
+ delete normalized.runId;
71
+ delete normalized.timestamp;
72
+
73
+ // Normalize findings array
74
+ if (Array.isArray(normalized.findings)) {
75
+ normalized.findings = normalized.findings.map(f => normalizeFinding(f));
76
+ // Sort by stable identity (if we had it, but for now sort by type + interaction)
77
+ normalized.findings.sort((a, b) => {
78
+ const keyA = `${a.type || ''}|${a.interaction?.selector || ''}|${a.interaction?.type || ''}`;
79
+ const keyB = `${b.type || ''}|${b.interaction?.selector || ''}|${b.interaction?.type || ''}`;
80
+ return keyA.localeCompare(keyB);
81
+ });
82
+ }
83
+
84
+ // Normalize enforcement metadata (keep structure, remove timestamps)
85
+ if (normalized.enforcement) {
86
+ const enforcement = { ...normalized.enforcement };
87
+ // Keep enforcement data but normalize any timestamps
88
+ normalized.enforcement = enforcement;
89
+ }
90
+
91
+ return normalized;
92
+ }
93
+
94
+ /**
95
+ * Normalize individual finding
96
+ */
97
+ function normalizeFinding(finding) {
98
+ const normalized = { ...finding };
99
+
100
+ // Remove volatile fields
101
+ delete normalized.id;
102
+ delete normalized.findingId;
103
+ delete normalized.timestamp;
104
+ delete normalized.detectedAt;
105
+
106
+ // Normalize confidence (round to 3 decimals)
107
+ if (typeof normalized.confidence === 'number') {
108
+ normalized.confidence = Math.round(normalized.confidence * 1000) / 1000;
109
+ }
110
+
111
+ // Normalize evidence paths (keep structure, normalize paths)
112
+ if (normalized.evidence) {
113
+ normalized.evidence = normalizeEvidence(normalized.evidence);
114
+ }
115
+
116
+ // Normalize evidencePackage
117
+ if (normalized.evidencePackage) {
118
+ normalized.evidencePackage = normalizeEvidencePackage(normalized.evidencePackage);
119
+ }
120
+
121
+ // Normalize guardrails (keep structure, normalize confidence deltas)
122
+ if (normalized.guardrails) {
123
+ const guardrails = { ...normalized.guardrails };
124
+ if (typeof guardrails.confidenceDelta === 'number') {
125
+ guardrails.confidenceDelta = Math.round(guardrails.confidenceDelta * 1000) / 1000;
126
+ }
127
+ normalized.guardrails = guardrails;
128
+ }
129
+
130
+ return normalized;
131
+ }
132
+
133
+ /**
134
+ * Normalize evidence
135
+ */
136
+ function normalizeEvidence(evidence) {
137
+ const normalized = { ...evidence };
138
+
139
+ // Normalize screenshot paths (keep presence, normalize path)
140
+ if (normalized.before) {
141
+ normalized.before = normalizePath(normalized.before);
142
+ }
143
+ if (normalized.after) {
144
+ normalized.after = normalizePath(normalized.after);
145
+ }
146
+
147
+ // Normalize URLs (remove query params, hash)
148
+ if (normalized.beforeUrl) {
149
+ normalized.beforeUrl = normalizeUrl(normalized.beforeUrl);
150
+ }
151
+ if (normalized.afterUrl) {
152
+ normalized.afterUrl = normalizeUrl(normalized.afterUrl);
153
+ }
154
+
155
+ // Normalize source paths
156
+ if (normalized.source && typeof normalized.source === 'string') {
157
+ normalized.source = normalizePath(normalized.source);
158
+ }
159
+
160
+ return normalized;
161
+ }
162
+
163
+ /**
164
+ * Normalize evidencePackage
165
+ */
166
+ function normalizeEvidencePackage(evidencePackage) {
167
+ const normalized = { ...evidencePackage };
168
+
169
+ // Normalize before/after paths
170
+ if (normalized.before) {
171
+ normalized.before = {
172
+ ...normalized.before,
173
+ screenshot: normalized.before.screenshot ? normalizePath(normalized.before.screenshot) : null,
174
+ url: normalized.before.url ? normalizeUrl(normalized.before.url) : null,
175
+ };
176
+ }
177
+
178
+ if (normalized.after) {
179
+ normalized.after = {
180
+ ...normalized.after,
181
+ screenshot: normalized.after.screenshot ? normalizePath(normalized.after.screenshot) : null,
182
+ url: normalized.after.url ? normalizeUrl(normalized.after.url) : null,
183
+ };
184
+ }
185
+
186
+ // Normalize trigger source paths
187
+ if (normalized.trigger?.source?.file) {
188
+ normalized.trigger.source.file = normalizePath(normalized.trigger.source.file);
189
+ }
190
+
191
+ return normalized;
192
+ }
193
+
194
+ /**
195
+ * Normalize runStatus artifact
196
+ */
197
+ function normalizeRunStatus(artifact) {
198
+ const normalized = { ...artifact };
199
+
200
+ // Remove volatile fields
201
+ delete normalized.startedAt;
202
+ delete normalized.completedAt;
203
+ delete normalized.timestamp;
204
+ delete normalized.runId;
205
+
206
+ // Keep structure but remove timestamps from nested objects
207
+ if (normalized.verification) {
208
+ const verification = { ...normalized.verification };
209
+ delete verification.verifiedAt;
210
+ normalized.verification = verification;
211
+ }
212
+
213
+ return normalized;
214
+ }
215
+
216
+ /**
217
+ * Normalize summary artifact
218
+ */
219
+ function normalizeSummary(artifact) {
220
+ const normalized = { ...artifact };
221
+
222
+ // Remove volatile fields
223
+ delete normalized.timestamp;
224
+ delete normalized.runId;
225
+
226
+ // Normalize metrics (round durations)
227
+ if (normalized.metrics) {
228
+ const metrics = { ...normalized.metrics };
229
+ for (const key in metrics) {
230
+ if (typeof metrics[key] === 'number' && key.includes('Ms')) {
231
+ metrics[key] = Math.round(metrics[key]);
232
+ }
233
+ }
234
+ normalized.metrics = metrics;
235
+ }
236
+
237
+ return normalized;
238
+ }
239
+
240
+ /**
241
+ * Normalize learn artifact
242
+ */
243
+ function normalizeLearn(artifact) {
244
+ const normalized = { ...artifact };
245
+
246
+ // Remove volatile fields
247
+ delete normalized.learnedAt;
248
+ delete normalized.timestamp;
249
+ delete normalized.runId;
250
+
251
+ // Normalize routes array (sort by path)
252
+ if (Array.isArray(normalized.routes)) {
253
+ normalized.routes = [...normalized.routes].sort((a, b) => {
254
+ const pathA = a.path || '';
255
+ const pathB = b.path || '';
256
+ return pathA.localeCompare(pathB);
257
+ });
258
+ }
259
+
260
+ // Normalize expectations array (sort by type + target)
261
+ if (Array.isArray(normalized.expectations)) {
262
+ normalized.expectations = [...normalized.expectations].sort((a, b) => {
263
+ const keyA = `${a.type || ''}|${a.targetPath || ''}`;
264
+ const keyB = `${b.type || ''}|${b.targetPath || ''}`;
265
+ return keyA.localeCompare(keyB);
266
+ });
267
+ }
268
+
269
+ return normalized;
270
+ }
271
+
272
+ /**
273
+ * PHASE 22: Normalize evidence intent ledger
274
+ */
275
+ function normalizeEvidenceIntent(artifact) {
276
+ const normalized = { ...artifact };
277
+
278
+ // Remove volatile fields
279
+ delete normalized.generatedAt;
280
+
281
+ // Normalize entries (already sorted by findingIdentity, but normalize timestamps)
282
+ if (Array.isArray(normalized.entries)) {
283
+ normalized.entries = normalized.entries.map(entry => {
284
+ const normalizedEntry = { ...entry };
285
+ delete normalizedEntry.timestamp;
286
+
287
+ // Normalize capture outcomes (preserve pass/fail, normalize failure details)
288
+ if (normalizedEntry.captureOutcomes) {
289
+ const outcomes = {};
290
+ for (const [field, outcome] of Object.entries(normalizedEntry.captureOutcomes)) {
291
+ outcomes[field] = {
292
+ required: outcome.required,
293
+ captured: outcome.captured,
294
+ failure: outcome.failure ? {
295
+ stage: outcome.failure.stage,
296
+ reasonCode: outcome.failure.reasonCode,
297
+ reason: outcome.failure.reason
298
+ // Exclude stackSummary and timestamp from failure for determinism
299
+ } : null
300
+ };
301
+ }
302
+ normalizedEntry.captureOutcomes = outcomes;
303
+ }
304
+
305
+ return normalizedEntry;
306
+ });
307
+ }
308
+
309
+ return normalized;
310
+ }
311
+
312
+ /**
313
+ * PHASE 23: Normalize guardrails report
314
+ */
315
+ function normalizeGuardrailsReport(artifact) {
316
+ const normalized = { ...artifact };
317
+
318
+ // Remove volatile fields
319
+ delete normalized.generatedAt;
320
+
321
+ // Normalize summary (preserve counts, normalize topRules ordering)
322
+ if (normalized.summary && normalized.summary.topRules) {
323
+ normalized.summary.topRules = normalized.summary.topRules.map(r => ({
324
+ code: r.code,
325
+ count: r.count
326
+ }));
327
+ }
328
+
329
+ // Normalize perFinding (already sorted by findingIdentity, but normalize timestamps if any)
330
+ if (normalized.perFinding && typeof normalized.perFinding === 'object') {
331
+ const perFindingNormalized = {};
332
+ const sortedKeys = Object.keys(normalized.perFinding).sort();
333
+ for (const key of sortedKeys) {
334
+ const entry = normalized.perFinding[key];
335
+ perFindingNormalized[key] = {
336
+ ...entry,
337
+ // Remove any volatile fields from entry
338
+ };
339
+ }
340
+ normalized.perFinding = perFindingNormalized;
341
+ }
342
+
343
+ return normalized;
344
+ }
345
+
346
+ /**
347
+ * PHASE 24: Normalize confidence report
348
+ */
349
+ function normalizeConfidenceReport(artifact) {
350
+ const normalized = { ...artifact };
351
+
352
+ // Remove volatile fields
353
+ delete normalized.generatedAt;
354
+
355
+ // Normalize summary (preserve counts)
356
+ if (normalized.summary) {
357
+ // Summary counts are already deterministic, no normalization needed
358
+ }
359
+
360
+ // Normalize perFinding (already sorted by findingIdentity, but normalize timestamps if any)
361
+ if (normalized.perFinding && typeof normalized.perFinding === 'object') {
362
+ const perFindingNormalized = {};
363
+ const sortedKeys = Object.keys(normalized.perFinding).sort();
364
+ for (const key of sortedKeys) {
365
+ const entry = normalized.perFinding[key];
366
+ perFindingNormalized[key] = {
367
+ ...entry,
368
+ // Remove any volatile fields from entry
369
+ // Round confidence values to 3 decimal places for determinism
370
+ confidenceBefore: Math.round(entry.confidenceBefore * 1000) / 1000,
371
+ confidenceAfter: Math.round(entry.confidenceAfter * 1000) / 1000
372
+ };
373
+ }
374
+ normalized.perFinding = perFindingNormalized;
375
+ }
376
+
377
+ return normalized;
378
+ }
379
+
380
+ /**
381
+ * Normalize generic artifact
382
+ */
383
+ function normalizeGeneric(artifact) {
384
+ const normalized = { ...artifact };
385
+
386
+ // Remove common volatile fields
387
+ delete normalized.timestamp;
388
+ delete normalized.runId;
389
+ delete normalized.detectedAt;
390
+ delete normalized.startedAt;
391
+ delete normalized.completedAt;
392
+ delete normalized.verifiedAt;
393
+ delete normalized.learnedAt;
394
+
395
+ // Recursively normalize nested objects
396
+ for (const key in normalized) {
397
+ if (normalized[key] && typeof normalized[key] === 'object') {
398
+ if (Array.isArray(normalized[key])) {
399
+ // Sort arrays if they contain objects with stable keys
400
+ normalized[key] = [...normalized[key]];
401
+ } else {
402
+ normalized[key] = normalizeGeneric(normalized[key]);
403
+ }
404
+ }
405
+ }
406
+
407
+ return normalized;
408
+ }
409
+
410
+ /**
411
+ * Normalize path (remove absolute paths, normalize separators)
412
+ */
413
+ function normalizePath(path) {
414
+ if (!path || typeof path !== 'string') return path;
415
+ let normalized = path.replace(/\\/g, '/');
416
+ // Remove absolute path prefixes
417
+ normalized = normalized.replace(/^[A-Z]:\/[^\/]+/, '');
418
+ normalized = normalized.replace(/^\/[^\/]+/, '');
419
+ // Remove temp dirs
420
+ normalized = normalized.replace(/\/tmp\/[^\/]+/g, '/tmp/...');
421
+ normalized = normalized.replace(/\/\.verax\/runs\/[^\/]+/g, '/.verax/runs/...');
422
+ return normalized;
423
+ }
424
+
425
+ /**
426
+ * Normalize URL (remove query params, hash, normalize domain)
427
+ */
428
+ function normalizeUrl(url) {
429
+ if (!url || typeof url !== 'string') return url;
430
+ try {
431
+ const urlObj = new URL(url);
432
+ // Keep only pathname for comparison
433
+ return urlObj.pathname;
434
+ } catch {
435
+ return url;
436
+ }
437
+ }
438
+
@@ -0,0 +1,92 @@
1
+ /**
2
+ * PHASE 21.2 — Determinism Report Writer
3
+ *
4
+ * Writes determinism.report.json with HARD TRUTH about determinism.
5
+ * No marketing language. Only binary verdict: DETERMINISTIC or NON_DETERMINISTIC.
6
+ */
7
+
8
+ import { writeFileSync, readFileSync, existsSync } from 'fs';
9
+ import { resolve } from 'path';
10
+ import { ARTIFACT_REGISTRY, getArtifactVersions } from '../artifacts/registry.js';
11
+ import { computeDeterminismVerdict, DETERMINISM_VERDICT, DETERMINISM_REASON } from './contract.js';
12
+ import { DecisionRecorder } from '../determinism-model.js';
13
+
14
+ /**
15
+ * PHASE 21.2: Write determinism report from DecisionRecorder
16
+ *
17
+ * @param {string} runDir - Run directory path
18
+ * @param {DecisionRecorder} decisionRecorder - Decision recorder instance
19
+ * @returns {string} Path to determinism report
20
+ */
21
+ export function writeDeterminismReport(runDir, decisionRecorder) {
22
+ const reportPath = resolve(runDir, ARTIFACT_REGISTRY.determinismReport.filename);
23
+
24
+ // PHASE 21.2: Compute HARD verdict from adaptive events
25
+ const verdict = computeDeterminismVerdict(decisionRecorder);
26
+
27
+ const report = {
28
+ version: 1,
29
+ contractVersion: 1,
30
+ artifactVersions: getArtifactVersions(),
31
+ generatedAt: new Date().toISOString(),
32
+ // PHASE 21.2: HARD TRUTH - binary verdict only
33
+ verdict: verdict.verdict,
34
+ message: verdict.message,
35
+ reasons: verdict.reasons,
36
+ adaptiveEvents: verdict.adaptiveEvents,
37
+ // PHASE 21.2: Decision summary for transparency
38
+ decisionSummary: decisionRecorder ? decisionRecorder.getSummary() : null,
39
+ // PHASE 21.2: Contract definition
40
+ contract: {
41
+ deterministic: 'Same inputs + same environment + same config → identical normalized artifacts',
42
+ nonDeterministic: 'Any adaptive behavior (adaptive stabilization, retries, truncations) → NON_DETERMINISTIC',
43
+ tracking: 'Tracking adaptive decisions is NOT determinism. Only absence of adaptive events = DETERMINISTIC.'
44
+ }
45
+ };
46
+
47
+ writeFileSync(reportPath, JSON.stringify(report, null, 2) + '\n');
48
+
49
+ return reportPath;
50
+ }
51
+
52
+ /**
53
+ * PHASE 21.2: Write determinism report from decisions.json file
54
+ *
55
+ * @param {string} runDir - Run directory path
56
+ * @returns {string|null} Path to determinism report, or null if decisions.json not found
57
+ */
58
+ export function writeDeterminismReportFromFile(runDir) {
59
+ const decisionsPath = resolve(runDir, 'decisions.json');
60
+
61
+ if (!existsSync(decisionsPath)) {
62
+ return null;
63
+ }
64
+
65
+ try {
66
+ const decisionsData = JSON.parse(readFileSync(decisionsPath, 'utf-8'));
67
+ const decisionRecorder = DecisionRecorder.fromExport(decisionsData);
68
+ return writeDeterminismReport(runDir, decisionRecorder);
69
+ } catch (error) {
70
+ // If we can't read decisions, we can't determine determinism
71
+ const reportPath = resolve(runDir, ARTIFACT_REGISTRY.determinismReport.filename);
72
+ const report = {
73
+ version: 1,
74
+ contractVersion: 1,
75
+ artifactVersions: getArtifactVersions(),
76
+ generatedAt: new Date().toISOString(),
77
+ verdict: DETERMINISM_VERDICT.NON_DETERMINISTIC,
78
+ message: `Cannot determine determinism: ${error.message}`,
79
+ reasons: [DETERMINISM_REASON.ENVIRONMENT_VARIANCE],
80
+ adaptiveEvents: [],
81
+ decisionSummary: null,
82
+ contract: {
83
+ deterministic: 'Same inputs + same environment + same config → identical normalized artifacts',
84
+ nonDeterministic: 'Any adaptive behavior (adaptive stabilization, retries, truncations) → NON_DETERMINISTIC',
85
+ tracking: 'Tracking adaptive decisions is NOT determinism. Only absence of adaptive events = DETERMINISTIC.'
86
+ }
87
+ };
88
+ writeFileSync(reportPath, JSON.stringify(report, null, 2) + '\n');
89
+ return reportPath;
90
+ }
91
+ }
92
+
@@ -0,0 +1,118 @@
1
+ /**
2
+ * PHASE 25 — Run Fingerprint
3
+ *
4
+ * Computes stable run fingerprint from deterministic inputs.
5
+ * Used to verify that repeated runs have identical inputs.
6
+ */
7
+
8
+ import { createHash } from 'crypto';
9
+ import { readFileSync, existsSync } from 'fs';
10
+ import { resolve, dirname } from 'path';
11
+ import { getVeraxVersion } from '../run-id.js';
12
+
13
+ /**
14
+ * Compute run fingerprint from deterministic inputs
15
+ *
16
+ * @param {Object} params - Run parameters
17
+ * @param {string} params.url - Target URL
18
+ * @param {string} params.projectDir - Project directory
19
+ * @param {string} params.manifestPath - Manifest path (optional)
20
+ * @param {Object} params.policyHash - Policy hash (optional)
21
+ * @param {string} params.fixtureId - Fixture ID (optional)
22
+ * @returns {string} Run fingerprint (hex hash)
23
+ */
24
+ export function computeRunFingerprint(params) {
25
+ const {
26
+ url,
27
+ projectDir,
28
+ manifestPath = null,
29
+ policyHash = null,
30
+ fixtureId = null
31
+ } = params;
32
+
33
+ // Compute source hash (hash of all source files)
34
+ const srcHash = computeSourceHash(projectDir);
35
+
36
+ // Compute policy hash if not provided
37
+ const computedPolicyHash = policyHash || computePolicyHash(projectDir);
38
+
39
+ // Get VERAX version
40
+ const veraxVersion = getVeraxVersion();
41
+
42
+ // Build fingerprint input
43
+ const fingerprintInput = {
44
+ url: normalizeUrl(url),
45
+ srcHash,
46
+ policyHash: computedPolicyHash,
47
+ veraxVersion,
48
+ fixtureId: fixtureId || null
49
+ };
50
+
51
+ // Generate stable hash
52
+ const configString = JSON.stringify(fingerprintInput, Object.keys(fingerprintInput).sort());
53
+ const hash = createHash('sha256').update(configString).digest('hex');
54
+
55
+ return hash.substring(0, 32); // 32 chars for readability
56
+ }
57
+
58
+ /**
59
+ * Compute source hash (hash of all source files in project)
60
+ */
61
+ function computeSourceHash(projectDir) {
62
+ try {
63
+ // For now, use a simple approach: hash package.json + src directory structure
64
+ // In production, this should hash all source files
65
+ const packagePath = resolve(projectDir, 'package.json');
66
+ if (existsSync(packagePath)) {
67
+ const pkgContent = readFileSync(packagePath, 'utf-8');
68
+ return createHash('sha256').update(pkgContent).digest('hex').substring(0, 16);
69
+ }
70
+ return 'no-source';
71
+ } catch {
72
+ return 'unknown';
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Compute policy hash (hash of guardrails/confidence policies)
78
+ */
79
+ function computePolicyHash(projectDir) {
80
+ try {
81
+ // Hash default policies (in production, should hash all policy files)
82
+ const policyPaths = [
83
+ resolve(projectDir, '.verax', 'guardrails.policy.json'),
84
+ resolve(projectDir, '.verax', 'confidence.policy.json')
85
+ ];
86
+
87
+ const hashes = [];
88
+ for (const path of policyPaths) {
89
+ if (existsSync(path)) {
90
+ const content = readFileSync(path, 'utf-8');
91
+ hashes.push(createHash('sha256').update(content).digest('hex').substring(0, 8));
92
+ }
93
+ }
94
+
95
+ if (hashes.length > 0) {
96
+ return createHash('sha256').update(hashes.join('|')).digest('hex').substring(0, 16);
97
+ }
98
+
99
+ return 'default-policy';
100
+ } catch {
101
+ return 'unknown-policy';
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Normalize URL for fingerprint
107
+ */
108
+ function normalizeUrl(url) {
109
+ if (!url || typeof url !== 'string') return '';
110
+ try {
111
+ const urlObj = new URL(url);
112
+ // Normalize: remove trailing slash, lowercase host
113
+ return `${urlObj.protocol}//${urlObj.host.toLowerCase()}${urlObj.pathname.replace(/\/$/, '') || '/'}`;
114
+ } catch {
115
+ return url;
116
+ }
117
+ }
118
+