@veraxhq/verax 0.2.1 → 0.4.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 (213) hide show
  1. package/README.md +10 -6
  2. package/bin/verax.js +11 -11
  3. package/package.json +29 -8
  4. package/src/cli/commands/baseline.js +103 -0
  5. package/src/cli/commands/default.js +51 -6
  6. package/src/cli/commands/doctor.js +29 -0
  7. package/src/cli/commands/ga.js +246 -0
  8. package/src/cli/commands/gates.js +95 -0
  9. package/src/cli/commands/inspect.js +4 -2
  10. package/src/cli/commands/release-check.js +215 -0
  11. package/src/cli/commands/run.js +45 -6
  12. package/src/cli/commands/security-check.js +212 -0
  13. package/src/cli/commands/truth.js +113 -0
  14. package/src/cli/entry.js +30 -20
  15. package/src/cli/util/angular-component-extractor.js +179 -0
  16. package/src/cli/util/angular-navigation-detector.js +141 -0
  17. package/src/cli/util/angular-network-detector.js +161 -0
  18. package/src/cli/util/angular-state-detector.js +162 -0
  19. package/src/cli/util/ast-interactive-detector.js +544 -0
  20. package/src/cli/util/ast-network-detector.js +603 -0
  21. package/src/cli/util/ast-promise-extractor.js +581 -0
  22. package/src/cli/util/ast-usestate-detector.js +602 -0
  23. package/src/cli/util/atomic-write.js +12 -1
  24. package/src/cli/util/bootstrap-guard.js +86 -0
  25. package/src/cli/util/console-reporter.js +72 -0
  26. package/src/cli/util/detection-engine.js +105 -41
  27. package/src/cli/util/determinism-runner.js +124 -0
  28. package/src/cli/util/determinism-writer.js +129 -0
  29. package/src/cli/util/digest-engine.js +359 -0
  30. package/src/cli/util/dom-diff.js +226 -0
  31. package/src/cli/util/evidence-engine.js +287 -0
  32. package/src/cli/util/expectation-extractor.js +151 -5
  33. package/src/cli/util/findings-writer.js +3 -0
  34. package/src/cli/util/framework-detector.js +572 -0
  35. package/src/cli/util/idgen.js +1 -1
  36. package/src/cli/util/interaction-planner.js +529 -0
  37. package/src/cli/util/learn-writer.js +2 -0
  38. package/src/cli/util/ledger-writer.js +110 -0
  39. package/src/cli/util/monorepo-resolver.js +162 -0
  40. package/src/cli/util/observation-engine.js +127 -278
  41. package/src/cli/util/observe-writer.js +2 -0
  42. package/src/cli/util/project-discovery.js +284 -0
  43. package/src/cli/util/project-writer.js +2 -0
  44. package/src/cli/util/run-id.js +23 -27
  45. package/src/cli/util/run-resolver.js +64 -0
  46. package/src/cli/util/run-result.js +778 -0
  47. package/src/cli/util/selector-resolver.js +235 -0
  48. package/src/cli/util/source-requirement.js +55 -0
  49. package/src/cli/util/summary-writer.js +2 -0
  50. package/src/cli/util/svelte-navigation-detector.js +163 -0
  51. package/src/cli/util/svelte-network-detector.js +80 -0
  52. package/src/cli/util/svelte-sfc-extractor.js +146 -0
  53. package/src/cli/util/svelte-state-detector.js +242 -0
  54. package/src/cli/util/trust-activation-integration.js +496 -0
  55. package/src/cli/util/trust-activation-wrapper.js +85 -0
  56. package/src/cli/util/trust-integration-hooks.js +164 -0
  57. package/src/cli/util/types.js +153 -0
  58. package/src/cli/util/url-validation.js +40 -0
  59. package/src/cli/util/vue-navigation-detector.js +178 -0
  60. package/src/cli/util/vue-sfc-extractor.js +161 -0
  61. package/src/cli/util/vue-state-detector.js +215 -0
  62. package/src/types/fs-augment.d.ts +23 -0
  63. package/src/types/global.d.ts +137 -0
  64. package/src/types/internal-types.d.ts +35 -0
  65. package/src/verax/cli/init.js +4 -18
  66. package/src/verax/core/action-classifier.js +4 -3
  67. package/src/verax/core/artifacts/registry.js +139 -0
  68. package/src/verax/core/artifacts/verifier.js +990 -0
  69. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  70. package/src/verax/core/baseline/baseline.snapshot.js +233 -0
  71. package/src/verax/core/capabilities/gates.js +505 -0
  72. package/src/verax/core/capabilities/registry.js +475 -0
  73. package/src/verax/core/confidence/confidence-compute.js +144 -0
  74. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  75. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  76. package/src/verax/core/confidence/confidence-weights.js +44 -0
  77. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  78. package/src/verax/core/confidence/confidence.loader.js +80 -0
  79. package/src/verax/core/confidence/confidence.schema.js +94 -0
  80. package/src/verax/core/confidence-engine-refactor.js +489 -0
  81. package/src/verax/core/confidence-engine.js +625 -0
  82. package/src/verax/core/contracts/index.js +29 -0
  83. package/src/verax/core/contracts/types.js +186 -0
  84. package/src/verax/core/contracts/validators.js +456 -0
  85. package/src/verax/core/decisions/decision.trace.js +278 -0
  86. package/src/verax/core/determinism/contract-writer.js +89 -0
  87. package/src/verax/core/determinism/contract.js +139 -0
  88. package/src/verax/core/determinism/diff.js +405 -0
  89. package/src/verax/core/determinism/engine.js +222 -0
  90. package/src/verax/core/determinism/finding-identity.js +149 -0
  91. package/src/verax/core/determinism/normalize.js +466 -0
  92. package/src/verax/core/determinism/report-writer.js +93 -0
  93. package/src/verax/core/determinism/run-fingerprint.js +123 -0
  94. package/src/verax/core/dynamic-route-intelligence.js +529 -0
  95. package/src/verax/core/evidence/evidence-capture-service.js +308 -0
  96. package/src/verax/core/evidence/evidence-intent-ledger.js +166 -0
  97. package/src/verax/core/evidence-builder.js +487 -0
  98. package/src/verax/core/execution-mode-context.js +77 -0
  99. package/src/verax/core/execution-mode-detector.js +192 -0
  100. package/src/verax/core/failures/exit-codes.js +88 -0
  101. package/src/verax/core/failures/failure-summary.js +76 -0
  102. package/src/verax/core/failures/failure.factory.js +225 -0
  103. package/src/verax/core/failures/failure.ledger.js +133 -0
  104. package/src/verax/core/failures/failure.types.js +196 -0
  105. package/src/verax/core/failures/index.js +10 -0
  106. package/src/verax/core/ga/ga-report-writer.js +43 -0
  107. package/src/verax/core/ga/ga.artifact.js +49 -0
  108. package/src/verax/core/ga/ga.contract.js +435 -0
  109. package/src/verax/core/ga/ga.enforcer.js +87 -0
  110. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  111. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  112. package/src/verax/core/guardrails/policy.loader.js +84 -0
  113. package/src/verax/core/guardrails/policy.schema.js +110 -0
  114. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  115. package/src/verax/core/guardrails-engine.js +505 -0
  116. package/src/verax/core/incremental-store.js +1 -0
  117. package/src/verax/core/integrity/budget.js +138 -0
  118. package/src/verax/core/integrity/determinism.js +342 -0
  119. package/src/verax/core/integrity/integrity.js +208 -0
  120. package/src/verax/core/integrity/poisoning.js +108 -0
  121. package/src/verax/core/integrity/transaction.js +140 -0
  122. package/src/verax/core/observe/run-timeline.js +318 -0
  123. package/src/verax/core/perf/perf.contract.js +186 -0
  124. package/src/verax/core/perf/perf.display.js +65 -0
  125. package/src/verax/core/perf/perf.enforcer.js +91 -0
  126. package/src/verax/core/perf/perf.monitor.js +209 -0
  127. package/src/verax/core/perf/perf.report.js +200 -0
  128. package/src/verax/core/pipeline-tracker.js +243 -0
  129. package/src/verax/core/product-definition.js +127 -0
  130. package/src/verax/core/release/provenance.builder.js +130 -0
  131. package/src/verax/core/release/release-report-writer.js +40 -0
  132. package/src/verax/core/release/release.enforcer.js +164 -0
  133. package/src/verax/core/release/reproducibility.check.js +222 -0
  134. package/src/verax/core/release/sbom.builder.js +292 -0
  135. package/src/verax/core/replay-validator.js +2 -0
  136. package/src/verax/core/replay.js +4 -0
  137. package/src/verax/core/report/cross-index.js +195 -0
  138. package/src/verax/core/report/human-summary.js +362 -0
  139. package/src/verax/core/route-intelligence.js +420 -0
  140. package/src/verax/core/run-id.js +6 -3
  141. package/src/verax/core/run-manifest.js +4 -3
  142. package/src/verax/core/security/secrets.scan.js +329 -0
  143. package/src/verax/core/security/security-report.js +50 -0
  144. package/src/verax/core/security/security.enforcer.js +128 -0
  145. package/src/verax/core/security/supplychain.defaults.json +38 -0
  146. package/src/verax/core/security/supplychain.policy.js +334 -0
  147. package/src/verax/core/security/vuln.scan.js +265 -0
  148. package/src/verax/core/truth/truth.certificate.js +252 -0
  149. package/src/verax/core/ui-feedback-intelligence.js +481 -0
  150. package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
  151. package/src/verax/detect/confidence-engine.js +62 -34
  152. package/src/verax/detect/confidence-helper.js +34 -0
  153. package/src/verax/detect/dynamic-route-findings.js +338 -0
  154. package/src/verax/detect/expectation-chain-detector.js +417 -0
  155. package/src/verax/detect/expectation-model.js +2 -2
  156. package/src/verax/detect/failure-cause-inference.js +293 -0
  157. package/src/verax/detect/findings-writer.js +131 -35
  158. package/src/verax/detect/flow-detector.js +2 -2
  159. package/src/verax/detect/form-silent-failure.js +98 -0
  160. package/src/verax/detect/index.js +46 -5
  161. package/src/verax/detect/invariants-enforcer.js +147 -0
  162. package/src/verax/detect/journey-stall-detector.js +558 -0
  163. package/src/verax/detect/navigation-silent-failure.js +82 -0
  164. package/src/verax/detect/problem-aggregator.js +361 -0
  165. package/src/verax/detect/route-findings.js +219 -0
  166. package/src/verax/detect/summary-writer.js +477 -0
  167. package/src/verax/detect/test-failure-cause-inference.js +314 -0
  168. package/src/verax/detect/ui-feedback-findings.js +207 -0
  169. package/src/verax/detect/view-switch-correlator.js +242 -0
  170. package/src/verax/flow/flow-engine.js +2 -1
  171. package/src/verax/flow/flow-spec.js +0 -6
  172. package/src/verax/index.js +4 -0
  173. package/src/verax/intel/ts-program.js +1 -0
  174. package/src/verax/intel/vue-navigation-extractor.js +3 -0
  175. package/src/verax/learn/action-contract-extractor.js +3 -0
  176. package/src/verax/learn/ast-contract-extractor.js +1 -1
  177. package/src/verax/learn/flow-extractor.js +1 -0
  178. package/src/verax/learn/project-detector.js +5 -0
  179. package/src/verax/learn/react-router-extractor.js +2 -0
  180. package/src/verax/learn/source-instrumenter.js +1 -0
  181. package/src/verax/learn/state-extractor.js +2 -1
  182. package/src/verax/learn/static-extractor.js +1 -0
  183. package/src/verax/observe/coverage-gaps.js +132 -0
  184. package/src/verax/observe/expectation-handler.js +126 -0
  185. package/src/verax/observe/incremental-skip.js +46 -0
  186. package/src/verax/observe/index.js +51 -155
  187. package/src/verax/observe/interaction-executor.js +192 -0
  188. package/src/verax/observe/interaction-runner.js +782 -513
  189. package/src/verax/observe/network-firewall.js +86 -0
  190. package/src/verax/observe/observation-builder.js +169 -0
  191. package/src/verax/observe/observe-context.js +205 -0
  192. package/src/verax/observe/observe-helpers.js +192 -0
  193. package/src/verax/observe/observe-runner.js +230 -0
  194. package/src/verax/observe/observers/budget-observer.js +185 -0
  195. package/src/verax/observe/observers/console-observer.js +102 -0
  196. package/src/verax/observe/observers/coverage-observer.js +107 -0
  197. package/src/verax/observe/observers/interaction-observer.js +471 -0
  198. package/src/verax/observe/observers/navigation-observer.js +132 -0
  199. package/src/verax/observe/observers/network-observer.js +87 -0
  200. package/src/verax/observe/observers/safety-observer.js +82 -0
  201. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  202. package/src/verax/observe/page-traversal.js +138 -0
  203. package/src/verax/observe/snapshot-ops.js +94 -0
  204. package/src/verax/observe/ui-feedback-detector.js +742 -0
  205. package/src/verax/scan-summary-writer.js +2 -0
  206. package/src/verax/shared/artifact-manager.js +25 -5
  207. package/src/verax/shared/caching.js +1 -0
  208. package/src/verax/shared/css-spinner-rules.js +204 -0
  209. package/src/verax/shared/expectation-tracker.js +1 -0
  210. package/src/verax/shared/view-switch-rules.js +208 -0
  211. package/src/verax/shared/zip-artifacts.js +6 -0
  212. package/src/verax/shared/config-loader.js +0 -169
  213. /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
@@ -0,0 +1,405 @@
1
+ /**
2
+ * PHASE 18 — Determinism Diff Builder
3
+ *
4
+ * Generates structured diffs between normalized artifacts from different runs.
5
+ */
6
+
7
+ import { computeFindingIdentity as _computeFindingIdentity } from './finding-identity.js';
8
+
9
+ /**
10
+ * PHASE 18: Diff reason codes
11
+ * PHASE 25: Extended with new reason codes
12
+ */
13
+ export const DIFF_REASON = {
14
+ MISSING_ARTIFACT: 'DET_DIFF_MISSING_ARTIFACT',
15
+ SCHEMA_MISMATCH: 'DET_DIFF_SCHEMA_MISMATCH',
16
+ FINDING_ADDED: 'DET_DIFF_FINDING_ADDED',
17
+ FINDING_REMOVED: 'DET_DIFF_FINDING_REMOVED',
18
+ FINDING_STATUS_CHANGED: 'DET_DIFF_FINDING_STATUS_CHANGED',
19
+ FINDING_SEVERITY_CHANGED: 'DET_DIFF_FINDING_SEVERITY_CHANGED',
20
+ CONFIDENCE_CHANGED: 'DET_DIFF_CONFIDENCE_CHANGED',
21
+ CONFIDENCE_REASONS_CHANGED: 'DET_DIFF_CONFIDENCE_REASONS_CHANGED',
22
+ GUARDRAILS_CHANGED: 'DET_DIFF_GUARDRAILS_CHANGED',
23
+ EVIDENCE_COMPLETENESS_CHANGED: 'DET_DIFF_EVIDENCE_COMPLETENESS_CHANGED',
24
+ EVIDENCE_MISSING: 'DET_DIFF_EVIDENCE_MISSING',
25
+ OBSERVATION_COUNT_CHANGED: 'DET_DIFF_OBSERVATION_COUNT_CHANGED',
26
+ FIELD_VALUE_CHANGED: 'DET_DIFF_FIELD_VALUE_CHANGED',
27
+ RUN_FINGERPRINT_MISMATCH: 'DET_DIFF_RUN_FINGERPRINT_MISMATCH',
28
+ };
29
+
30
+ /**
31
+ * PHASE 18: Diff categories
32
+ */
33
+ export const DIFF_CATEGORY = {
34
+ FINDINGS: 'FINDINGS',
35
+ EXPECTATIONS: 'EXPECTATIONS',
36
+ OBSERVATIONS: 'OBSERVATIONS',
37
+ EVIDENCE: 'EVIDENCE',
38
+ STATUS: 'STATUS',
39
+ ARTIFACTS: 'ARTIFACTS',
40
+ };
41
+
42
+ /**
43
+ * PHASE 18: Diff severity
44
+ */
45
+ export const DIFF_SEVERITY = {
46
+ BLOCKER: 'BLOCKER',
47
+ WARN: 'WARN',
48
+ INFO: 'INFO',
49
+ };
50
+
51
+ function diffRunMeta(artifactA, artifactB) {
52
+ return diffGeneric(artifactA, artifactB, 'runMeta');
53
+ }
54
+
55
+ function diffDeterminismContract(artifactA, artifactB) {
56
+ return diffGeneric(artifactA, artifactB, 'determinismContract');
57
+ }
58
+
59
+ function diffReportArtifact(artifactA, artifactB, artifactName) {
60
+ return diffGeneric(artifactA, artifactB, artifactName);
61
+ }
62
+
63
+ /**
64
+ * PHASE 18: Diff artifacts
65
+ *
66
+ * @param {Object} artifactA - First artifact (normalized)
67
+ * @param {Object} artifactB - Second artifact (normalized)
68
+ * @param {string} artifactName - Name of artifact
69
+ * @param {Map} findingIdentityMap - Map of finding identity to finding (for matching)
70
+ * @returns {Array} Array of diff objects
71
+ */
72
+ export function diffArtifacts(artifactA, artifactB, artifactName, findingIdentityMap = null) {
73
+ const diffs = [];
74
+
75
+ // Check if artifacts exist
76
+ if (!artifactA && artifactB) {
77
+ diffs.push({
78
+ category: DIFF_CATEGORY.ARTIFACTS,
79
+ severity: DIFF_SEVERITY.BLOCKER,
80
+ reasonCode: DIFF_REASON.MISSING_ARTIFACT,
81
+ message: `Artifact ${artifactName} missing in first run`,
82
+ artifact: artifactName,
83
+ });
84
+ return diffs;
85
+ }
86
+
87
+ if (artifactA && !artifactB) {
88
+ diffs.push({
89
+ category: DIFF_CATEGORY.ARTIFACTS,
90
+ severity: DIFF_SEVERITY.BLOCKER,
91
+ reasonCode: DIFF_REASON.MISSING_ARTIFACT,
92
+ message: `Artifact ${artifactName} missing in second run`,
93
+ artifact: artifactName,
94
+ });
95
+ return diffs;
96
+ }
97
+
98
+ if (!artifactA && !artifactB) {
99
+ return diffs; // Both missing, no diff
100
+ }
101
+
102
+ // Artifact-specific diffing
103
+ if (artifactName === 'findings') {
104
+ diffs.push(...diffFindings(artifactA, artifactB, findingIdentityMap));
105
+ } else if (artifactName === 'runMeta') {
106
+ diffs.push(...diffRunMeta(artifactA, artifactB));
107
+ } else if (artifactName === 'determinismContract') {
108
+ diffs.push(...diffDeterminismContract(artifactA, artifactB));
109
+ } else if (artifactName === 'confidenceReport' || artifactName === 'guardrailsReport' || artifactName === 'evidenceIntent') {
110
+ diffs.push(...diffReportArtifact(artifactA, artifactB, artifactName));
111
+ } else {
112
+ // Generic diff for other artifacts
113
+ diffs.push(...diffGeneric(artifactA, artifactB, artifactName));
114
+ }
115
+
116
+ return diffs;
117
+ }
118
+
119
+ /**
120
+ * Diff findings artifacts
121
+ */
122
+ function diffFindings(artifactA, artifactB, findingIdentityMap) {
123
+ const diffs = [];
124
+
125
+ const findingsA = artifactA.findings || [];
126
+ const findingsB = artifactB.findings || [];
127
+
128
+ // Check counts
129
+ if (findingsA.length !== findingsB.length) {
130
+ diffs.push({
131
+ category: DIFF_CATEGORY.FINDINGS,
132
+ severity: DIFF_SEVERITY.BLOCKER,
133
+ reasonCode: DIFF_REASON.OBSERVATION_COUNT_CHANGED,
134
+ message: `Finding count changed: ${findingsA.length} → ${findingsB.length}`,
135
+ artifact: 'findings',
136
+ field: 'findings.length',
137
+ oldValue: findingsA.length,
138
+ newValue: findingsB.length,
139
+ });
140
+ }
141
+
142
+ // Build identity maps if not provided
143
+ const mapA = new Map();
144
+ const mapB = new Map();
145
+
146
+ for (const finding of findingsA) {
147
+ const identity = findingIdentityMap ? findingIdentityMap.get(finding) : computeFindingIdentitySimple(finding);
148
+ mapA.set(identity, finding);
149
+ }
150
+
151
+ for (const finding of findingsB) {
152
+ const identity = findingIdentityMap ? findingIdentityMap.get(finding) : computeFindingIdentitySimple(finding);
153
+ mapB.set(identity, finding);
154
+ }
155
+
156
+ // Find added findings
157
+ for (const [identity, finding] of mapB) {
158
+ if (!mapA.has(identity)) {
159
+ diffs.push({
160
+ category: DIFF_CATEGORY.FINDINGS,
161
+ severity: DIFF_SEVERITY.BLOCKER,
162
+ reasonCode: DIFF_REASON.FINDING_ADDED,
163
+ message: `Finding added: ${finding.type || 'unknown'}`,
164
+ artifact: 'findings',
165
+ findingIdentity: identity,
166
+ finding: finding,
167
+ });
168
+ }
169
+ }
170
+
171
+ // Find removed findings
172
+ for (const [identity, finding] of mapA) {
173
+ if (!mapB.has(identity)) {
174
+ diffs.push({
175
+ category: DIFF_CATEGORY.FINDINGS,
176
+ severity: DIFF_SEVERITY.BLOCKER,
177
+ reasonCode: DIFF_REASON.FINDING_REMOVED,
178
+ message: `Finding removed: ${finding.type || 'unknown'}`,
179
+ artifact: 'findings',
180
+ findingIdentity: identity,
181
+ finding: finding,
182
+ });
183
+ }
184
+ }
185
+
186
+ // Find changed findings
187
+ for (const [identity, findingA] of mapA) {
188
+ const findingB = mapB.get(identity);
189
+ if (findingB) {
190
+ diffs.push(...diffFinding(findingA, findingB, identity));
191
+ }
192
+ }
193
+
194
+ return diffs;
195
+ }
196
+
197
+ /**
198
+ * Diff individual finding
199
+ */
200
+ function diffFinding(findingA, findingB, identity) {
201
+ const diffs = [];
202
+
203
+ // Check status/severity
204
+ const statusA = findingA.severity || findingA.status;
205
+ const statusB = findingB.severity || findingB.status;
206
+ if (statusA !== statusB) {
207
+ diffs.push({
208
+ category: DIFF_CATEGORY.FINDINGS,
209
+ severity: DIFF_SEVERITY.BLOCKER,
210
+ reasonCode: DIFF_REASON.FINDING_SEVERITY_CHANGED,
211
+ message: `Finding severity changed: ${statusA} → ${statusB}`,
212
+ artifact: 'findings',
213
+ findingIdentity: identity,
214
+ field: 'severity',
215
+ oldValue: statusA,
216
+ newValue: statusB,
217
+ });
218
+ }
219
+
220
+ // Check confidence
221
+ const confA = findingA.confidence || 0;
222
+ const confB = findingB.confidence || 0;
223
+ if (Math.abs(confA - confB) > 0.001) {
224
+ diffs.push({
225
+ category: DIFF_CATEGORY.FINDINGS,
226
+ severity: DIFF_SEVERITY.WARN,
227
+ reasonCode: DIFF_REASON.CONFIDENCE_CHANGED,
228
+ message: `Finding confidence changed: ${confA.toFixed(3)} → ${confB.toFixed(3)}`,
229
+ artifact: 'findings',
230
+ findingIdentity: identity,
231
+ field: 'confidence',
232
+ oldValue: confA,
233
+ newValue: confB,
234
+ });
235
+ }
236
+
237
+ // Check confidence reasons
238
+ const reasonsA = findingA.confidenceReasons || [];
239
+ const reasonsB = findingB.confidenceReasons || [];
240
+ if (JSON.stringify(reasonsA.sort()) !== JSON.stringify(reasonsB.sort())) {
241
+ diffs.push({
242
+ category: DIFF_CATEGORY.FINDINGS,
243
+ severity: DIFF_SEVERITY.WARN,
244
+ reasonCode: DIFF_REASON.CONFIDENCE_REASONS_CHANGED,
245
+ message: `Finding confidence reasons changed`,
246
+ artifact: 'findings',
247
+ findingIdentity: identity,
248
+ field: 'confidenceReasons',
249
+ oldValue: reasonsA,
250
+ newValue: reasonsB,
251
+ });
252
+ }
253
+
254
+ // Check guardrails
255
+ const guardrailsA = findingA.guardrails;
256
+ const guardrailsB = findingB.guardrails;
257
+ if (guardrailsA || guardrailsB) {
258
+ if (!guardrailsA || !guardrailsB) {
259
+ diffs.push({
260
+ category: DIFF_CATEGORY.FINDINGS,
261
+ severity: DIFF_SEVERITY.WARN,
262
+ reasonCode: DIFF_REASON.GUARDRAILS_CHANGED,
263
+ message: `Finding guardrails presence changed`,
264
+ artifact: 'findings',
265
+ findingIdentity: identity,
266
+ field: 'guardrails',
267
+ });
268
+ } else if (guardrailsA.finalDecision !== guardrailsB.finalDecision) {
269
+ diffs.push({
270
+ category: DIFF_CATEGORY.FINDINGS,
271
+ severity: DIFF_SEVERITY.WARN,
272
+ reasonCode: DIFF_REASON.GUARDRAILS_CHANGED,
273
+ message: `Finding guardrails decision changed: ${guardrailsA.finalDecision} → ${guardrailsB.finalDecision}`,
274
+ artifact: 'findings',
275
+ findingIdentity: identity,
276
+ field: 'guardrails.finalDecision',
277
+ oldValue: guardrailsA.finalDecision,
278
+ newValue: guardrailsB.finalDecision,
279
+ });
280
+ }
281
+ }
282
+
283
+ // Check evidence completeness
284
+ const evidenceA = findingA.evidenceCompleteness;
285
+ const evidenceB = findingB.evidenceCompleteness;
286
+ if (evidenceA || evidenceB) {
287
+ if (!evidenceA || !evidenceB) {
288
+ diffs.push({
289
+ category: DIFF_CATEGORY.EVIDENCE,
290
+ severity: DIFF_SEVERITY.BLOCKER,
291
+ reasonCode: DIFF_REASON.EVIDENCE_COMPLETENESS_CHANGED,
292
+ message: `Finding evidence completeness presence changed`,
293
+ artifact: 'findings',
294
+ findingIdentity: identity,
295
+ field: 'evidenceCompleteness',
296
+ });
297
+ } else if (evidenceA.isComplete !== evidenceB.isComplete) {
298
+ diffs.push({
299
+ category: DIFF_CATEGORY.EVIDENCE,
300
+ severity: DIFF_SEVERITY.BLOCKER,
301
+ reasonCode: DIFF_REASON.EVIDENCE_COMPLETENESS_CHANGED,
302
+ message: `Finding evidence completeness changed: ${evidenceA.isComplete} → ${evidenceB.isComplete}`,
303
+ artifact: 'findings',
304
+ findingIdentity: identity,
305
+ field: 'evidenceCompleteness.isComplete',
306
+ oldValue: evidenceA.isComplete,
307
+ newValue: evidenceB.isComplete,
308
+ });
309
+ }
310
+ }
311
+
312
+ // Check evidencePackage presence
313
+ const evidencePackageA = findingA.evidencePackage;
314
+ const evidencePackageB = findingB.evidencePackage;
315
+ if (!evidencePackageA && evidencePackageB) {
316
+ diffs.push({
317
+ category: DIFF_CATEGORY.EVIDENCE,
318
+ severity: DIFF_SEVERITY.BLOCKER,
319
+ reasonCode: DIFF_REASON.EVIDENCE_MISSING,
320
+ message: `Finding evidence package missing in first run`,
321
+ artifact: 'findings',
322
+ findingIdentity: identity,
323
+ field: 'evidencePackage',
324
+ });
325
+ } else if (evidencePackageA && !evidencePackageB) {
326
+ diffs.push({
327
+ category: DIFF_CATEGORY.EVIDENCE,
328
+ severity: DIFF_SEVERITY.BLOCKER,
329
+ reasonCode: DIFF_REASON.EVIDENCE_MISSING,
330
+ message: `Finding evidence package missing in second run`,
331
+ artifact: 'findings',
332
+ findingIdentity: identity,
333
+ field: 'evidencePackage',
334
+ });
335
+ } else if (evidencePackageA && evidencePackageB) {
336
+ if (evidencePackageA.isComplete !== evidencePackageB.isComplete) {
337
+ diffs.push({
338
+ category: DIFF_CATEGORY.EVIDENCE,
339
+ severity: DIFF_SEVERITY.BLOCKER,
340
+ reasonCode: DIFF_REASON.EVIDENCE_COMPLETENESS_CHANGED,
341
+ message: `Evidence completeness changed: ${evidencePackageA.isComplete} → ${evidencePackageB.isComplete}`,
342
+ artifact: 'findings',
343
+ findingIdentity: identity,
344
+ field: 'evidencePackage.isComplete',
345
+ oldValue: evidencePackageA.isComplete,
346
+ newValue: evidencePackageB.isComplete,
347
+ });
348
+ }
349
+ const missingA = Array.isArray(evidencePackageA.missingEvidence) ? evidencePackageA.missingEvidence.sort() : [];
350
+ const missingB = Array.isArray(evidencePackageB.missingEvidence) ? evidencePackageB.missingEvidence.sort() : [];
351
+ if (missingA.join('|') !== missingB.join('|')) {
352
+ diffs.push({
353
+ category: DIFF_CATEGORY.EVIDENCE,
354
+ severity: DIFF_SEVERITY.BLOCKER,
355
+ reasonCode: DIFF_REASON.EVIDENCE_MISSING,
356
+ message: `Missing evidence changed`,
357
+ artifact: 'findings',
358
+ findingIdentity: identity,
359
+ field: 'evidencePackage.missingEvidence',
360
+ oldValue: missingA,
361
+ newValue: missingB,
362
+ });
363
+ }
364
+ }
365
+
366
+ return diffs;
367
+ }
368
+
369
+ /**
370
+ * Diff generic artifacts
371
+ */
372
+ function diffGeneric(artifactA, artifactB, artifactName) {
373
+ const diffs = [];
374
+
375
+ // Simple deep comparison
376
+ const jsonA = JSON.stringify(artifactA, null, 2);
377
+ const jsonB = JSON.stringify(artifactB, null, 2);
378
+
379
+ if (jsonA !== jsonB) {
380
+ diffs.push({
381
+ category: DIFF_CATEGORY.ARTIFACTS,
382
+ severity: DIFF_SEVERITY.WARN,
383
+ reasonCode: DIFF_REASON.FIELD_VALUE_CHANGED,
384
+ message: `Artifact ${artifactName} content changed`,
385
+ artifact: artifactName,
386
+ });
387
+ }
388
+
389
+ return diffs;
390
+ }
391
+
392
+ /**
393
+ * Simple finding identity computation (fallback)
394
+ */
395
+ function computeFindingIdentitySimple(finding) {
396
+ const parts = [
397
+ finding.type || 'unknown',
398
+ finding.interaction?.type || '',
399
+ finding.interaction?.selector || '',
400
+ finding.expectation?.targetPath || '',
401
+ finding.expectation?.urlPath || '',
402
+ ];
403
+ return parts.join('|');
404
+ }
405
+
@@ -0,0 +1,222 @@
1
+ /**
2
+ * PHASE 18 — Determinism Engine
3
+ * PHASE 21.2 — Determinism Truth Lock: Enforces HARD verdict
4
+ *
5
+ * Runs the same scan multiple times and compares results for determinism.
6
+ * PHASE 21.2: Also checks DecisionRecorder for adaptive events that break determinism.
7
+ */
8
+
9
+ import { readFileSync, existsSync } from 'fs';
10
+ import { join as _join, resolve } from 'path';
11
+ import { normalizeArtifact } from './normalize.js';
12
+ import { diffArtifacts } from './diff.js';
13
+ import { computeFindingIdentity } from './finding-identity.js';
14
+ import { computeDeterminismVerdict, DETERMINISM_VERDICT } from './contract.js';
15
+ import { DecisionRecorder } from '../determinism-model.js';
16
+
17
+ /**
18
+ * PHASE 18: Determinism verdict (re-exported from contract for backward compatibility)
19
+ */
20
+ export { DETERMINISM_VERDICT, DETERMINISM_REASON } from './contract.js';
21
+
22
+ /**
23
+ * PHASE 18: Run determinism check
24
+ *
25
+ * @param {Function} runFn - Function that executes a scan and returns artifact paths or in-memory artifacts
26
+ * @param {Object} options - Options
27
+ * @param {number} options.runs - Number of runs (default: 2)
28
+ * @param {Object} options.config - Configuration for runs
29
+ * @param {boolean} options.normalize - Whether to normalize artifacts (default: true)
30
+ * @returns {Promise<Object>} { verdict, summary, diffs, runsMeta }
31
+ */
32
+ export async function runDeterminismCheck(runFn, options = { runs: 2, config: {}, normalize: true }) {
33
+ const { runs = 2, config = {}, normalize = true } = options;
34
+
35
+ const runsMeta = [];
36
+ const runArtifacts = [];
37
+
38
+ // Execute runs
39
+ for (let i = 0; i < runs; i++) {
40
+ const runResult = await runFn(config);
41
+ runsMeta.push({
42
+ runIndex: i + 1,
43
+ runId: runResult.runId || null,
44
+ timestamp: new Date().toISOString(),
45
+ artifactPaths: runResult.artifactPaths || {},
46
+ artifacts: runResult.artifacts || {},
47
+ });
48
+
49
+ // Load artifacts if paths provided
50
+ const artifacts = {};
51
+ if (runResult.artifactPaths) {
52
+ for (const [key, path] of Object.entries(runResult.artifactPaths)) {
53
+ try {
54
+ const content = readFileSync(path, 'utf-8');
55
+ // @ts-expect-error - readFileSync with encoding returns string
56
+ artifacts[key] = JSON.parse(content);
57
+ } catch (error) {
58
+ // Artifact not found or invalid
59
+ }
60
+ }
61
+ } else if (runResult.artifacts) {
62
+ Object.assign(artifacts, runResult.artifacts);
63
+ }
64
+
65
+ runArtifacts.push(artifacts);
66
+ }
67
+
68
+ // Compare runs
69
+ const diffs = [];
70
+ const allArtifacts = new Set();
71
+
72
+ // Collect all artifact names
73
+ for (const artifacts of runArtifacts) {
74
+ for (const key of Object.keys(artifacts)) {
75
+ allArtifacts.add(key);
76
+ }
77
+ }
78
+
79
+ // Compare each artifact across runs
80
+ for (const artifactName of allArtifacts) {
81
+ const artifacts = runArtifacts.map(run => run[artifactName]);
82
+
83
+ // Normalize if requested
84
+ const normalizedArtifacts = normalize
85
+ ? artifacts.map(art => art ? normalizeArtifact(artifactName, art) : null)
86
+ : artifacts;
87
+
88
+ // Compare first run with all subsequent runs
89
+ for (let i = 1; i < normalizedArtifacts.length; i++) {
90
+ const artifactA = normalizedArtifacts[0];
91
+ const artifactB = normalizedArtifacts[i];
92
+
93
+ // Build finding identity map for findings artifacts
94
+ let findingIdentityMap = null;
95
+ if (artifactName === 'findings' && artifactA && artifactB) {
96
+ findingIdentityMap = buildFindingIdentityMap(artifactA, artifactB);
97
+ }
98
+
99
+ const artifactDiffs = diffArtifacts(artifactA, artifactB, artifactName, findingIdentityMap);
100
+
101
+ // Add run context to diffs
102
+ for (const diff of artifactDiffs) {
103
+ diff.runA = 1;
104
+ diff.runB = i + 1;
105
+ }
106
+
107
+ diffs.push(...artifactDiffs);
108
+ }
109
+ }
110
+
111
+ // PHASE 21.2: Check for adaptive events in DecisionRecorder (if available)
112
+ // HARD RULE: If adaptive events exist → verdict MUST be NON_DETERMINISTIC
113
+ let adaptiveVerdict = null;
114
+ let adaptiveReasons = [];
115
+ let adaptiveEvents = [];
116
+
117
+ // Try to load decisions.json from first run
118
+ if (runsMeta.length > 0 && runsMeta[0].artifactPaths?.runDir) {
119
+ const runDir = runsMeta[0].artifactPaths.runDir;
120
+ const decisionsPath = resolve(runDir, 'decisions.json');
121
+
122
+ if (existsSync(decisionsPath)) {
123
+ try {
124
+ // @ts-expect-error - readFileSync with encoding returns string
125
+ const decisionsData = JSON.parse(readFileSync(decisionsPath, 'utf-8'));
126
+ const decisionRecorder = DecisionRecorder.fromExport(decisionsData);
127
+ const adaptiveCheck = computeDeterminismVerdict(decisionRecorder);
128
+
129
+ adaptiveVerdict = adaptiveCheck.verdict;
130
+ adaptiveReasons = adaptiveCheck.reasons;
131
+ adaptiveEvents = adaptiveCheck.adaptiveEvents;
132
+ } catch (error) {
133
+ // Ignore errors reading decisions
134
+ }
135
+ }
136
+ }
137
+
138
+ // PHASE 21.2: Determine verdict
139
+ // HARD RULE: If adaptive events exist → verdict MUST be NON_DETERMINISTIC (even if artifacts match)
140
+ const blockerDiffs = diffs.filter(d => d.severity === 'BLOCKER');
141
+ const artifactVerdict = blockerDiffs.length === 0 ? DETERMINISM_VERDICT.DETERMINISTIC : DETERMINISM_VERDICT.NON_DETERMINISTIC;
142
+
143
+ // PHASE 21.2: Final verdict - adaptive events override artifact comparison
144
+ const verdict = (adaptiveVerdict === DETERMINISM_VERDICT.NON_DETERMINISTIC)
145
+ ? DETERMINISM_VERDICT.NON_DETERMINISTIC
146
+ : artifactVerdict;
147
+
148
+ // Build summary
149
+ const summary = buildSummary(diffs, runsMeta);
150
+
151
+ // PHASE 21.2: Include adaptive event information in summary
152
+ if (adaptiveVerdict === DETERMINISM_VERDICT.NON_DETERMINISTIC) {
153
+ summary.adaptiveEventsDetected = true;
154
+ summary.adaptiveEventCount = adaptiveEvents.length;
155
+ summary.adaptiveReasons = adaptiveReasons;
156
+ }
157
+
158
+ return {
159
+ verdict,
160
+ summary,
161
+ diffs,
162
+ runsMeta,
163
+ // PHASE 21.2: Include adaptive event information
164
+ adaptiveVerdict,
165
+ adaptiveReasons,
166
+ adaptiveEvents
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Build finding identity map for matching findings across runs
172
+ */
173
+ function buildFindingIdentityMap(artifactA, artifactB) {
174
+ const map = new Map();
175
+
176
+ const findingsA = artifactA.findings || [];
177
+ const findingsB = artifactB.findings || [];
178
+
179
+ // Build identity for findings in both runs
180
+ for (const finding of [...findingsA, ...findingsB]) {
181
+ const identity = computeFindingIdentity(finding);
182
+ map.set(finding, identity);
183
+ }
184
+
185
+ return map;
186
+ }
187
+
188
+ /**
189
+ * Build summary from diffs
190
+ */
191
+ function buildSummary(diffs, _runsMeta) {
192
+ const blockerCount = diffs.filter(d => d.severity === 'BLOCKER').length;
193
+ const warnCount = diffs.filter(d => d.severity === 'WARN').length;
194
+ const infoCount = diffs.filter(d => d.severity === 'INFO').length;
195
+
196
+ // Group by reason code
197
+ const reasonCounts = {};
198
+ for (const diff of diffs) {
199
+ const code = diff.reasonCode || 'UNKNOWN';
200
+ reasonCounts[code] = (reasonCounts[code] || 0) + 1;
201
+ }
202
+
203
+ // Top reasons
204
+ const topReasons = Object.entries(reasonCounts)
205
+ .sort((a, b) => b[1] - a[1])
206
+ .slice(0, 5)
207
+ .map(([code, count]) => ({ code, count }));
208
+
209
+ // Stability score (0..1)
210
+ const totalDiffs = diffs.length;
211
+ const stabilityScore = totalDiffs === 0 ? 1.0 : Math.max(0, 1.0 - (blockerCount * 0.5 + warnCount * 0.2 + infoCount * 0.1) / Math.max(1, totalDiffs));
212
+
213
+ return {
214
+ totalDiffs,
215
+ blockerCount,
216
+ warnCount,
217
+ infoCount,
218
+ topReasons,
219
+ stabilityScore,
220
+ };
221
+ }
222
+