@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
@@ -25,7 +25,9 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
25
25
  const manifestContent = readFileSync(manifestPath, 'utf-8');
26
26
  const tracesContent = readFileSync(tracesPath, 'utf-8');
27
27
 
28
+ // @ts-expect-error - readFileSync with encoding returns string
28
29
  const manifest = JSON.parse(manifestContent);
30
+ // @ts-expect-error - readFileSync with encoding returns string
29
31
  const observation = JSON.parse(tracesContent);
30
32
 
31
33
  const projectDir = manifest.projectDir;
@@ -98,20 +100,20 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
98
100
  selectorHint.includes(interactionSelector) ||
99
101
  interactionSelector.includes(normalizedSelectorHint) ||
100
102
  normalizedSelectorHint === normalizedInteractionSelector) {
101
- if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
103
+ if ((expectation.type === 'navigation' || expectation.type === 'spa_navigation') && (interaction.type === 'link' || interaction.type === 'button')) {
102
104
  matchingExpectations.push(expectation);
103
105
  } else if (expectation.type === 'form_submission' && interaction.type === 'form') {
104
106
  matchingExpectations.push(expectation);
105
107
  }
106
108
  } else {
107
- if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
109
+ if ((expectation.type === 'navigation' || expectation.type === 'spa_navigation') && (interaction.type === 'link' || interaction.type === 'button')) {
108
110
  selectorMismatch = true;
109
111
  } else if (expectation.type === 'form_submission' && interaction.type === 'form') {
110
112
  selectorMismatch = true;
111
113
  }
112
114
  }
113
115
  } else if (!selectorHint && !interactionSelector) {
114
- if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
116
+ if ((expectation.type === 'navigation' || expectation.type === 'spa_navigation') && (interaction.type === 'link' || interaction.type === 'button')) {
115
117
  matchingExpectations.push(expectation);
116
118
  } else if (expectation.type === 'form_submission' && interaction.type === 'form') {
117
119
  matchingExpectations.push(expectation);
@@ -122,9 +124,24 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
122
124
 
123
125
  if (matchingExpectations.length > 1) {
124
126
  multipleMatches = true;
127
+ // VISION TRANSPARENCY: Record ambiguity explicitly (not silent skip)
128
+ if (_silenceTracker) {
129
+ _silenceTracker.record({
130
+ scope: 'expectation',
131
+ reason: 'ambiguous_promise',
132
+ description: `Multiple expectations match interaction "${interaction.label}" (${matchingExpectations.length} candidates). Cannot determine intent without guessing.`,
133
+ context: {
134
+ interaction: { type: interaction.type, selector: interaction.selector, label: interaction.label },
135
+ candidateCount: matchingExpectations.length,
136
+ candidates: matchingExpectations.map(e => e.targetPath)
137
+ },
138
+ impact: 'interaction_not_evaluated',
139
+ outcome: 'UNPROVEN_INTERACTION'
140
+ });
141
+ }
125
142
  } else if (matchingExpectations.length === 1) {
126
143
  expectedTargetPath = matchingExpectations[0].targetPath;
127
- expectationType = matchingExpectations[0].type;
144
+ expectationType = matchingExpectations[0].type === 'spa_navigation' ? 'navigation' : matchingExpectations[0].type;
128
145
  } else if (selectorMismatch) {
129
146
  skips.push({
130
147
  code: 'SELECTOR_MISMATCH',
@@ -169,6 +186,21 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
169
186
  }
170
187
 
171
188
  if (matchingRoutes.length > 1) {
189
+ // VISION TRANSPARENCY: Record ambiguity explicitly
190
+ if (_silenceTracker) {
191
+ _silenceTracker.record({
192
+ scope: 'expectation',
193
+ reason: 'ambiguous_promise',
194
+ description: `Multiple routes match interaction "${interaction.label}" (${matchingRoutes.length} candidates). Conservative approach requires single clear match.`,
195
+ context: {
196
+ interaction: { type: interaction.type, selector: interaction.selector, label: interaction.label },
197
+ candidateCount: matchingRoutes.length,
198
+ candidates: matchingRoutes
199
+ },
200
+ impact: 'interaction_not_evaluated',
201
+ outcome: 'UNPROVEN_INTERACTION'
202
+ });
203
+ }
172
204
  skips.push({
173
205
  code: 'AMBIGUOUS_MATCH',
174
206
  message: 'Multiple expectations could match; conservative approach requires single clear match',
@@ -187,6 +219,7 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
187
219
  }
188
220
 
189
221
  if (multipleMatches) {
222
+ // VISION TRANSPARENCY: Ambiguity already recorded in silence tracker above
190
223
  skips.push({
191
224
  code: 'AMBIGUOUS_MATCH',
192
225
  message: 'Multiple expectations could match; conservative approach requires single clear match',
@@ -243,13 +276,17 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
243
276
 
244
277
  if (!hasEffect) {
245
278
  findings.push({
246
- type: 'silent_failure',
279
+ type: 'navigation_silent_failure',
247
280
  interaction: {
248
281
  type: interaction.type,
249
282
  selector: interaction.selector,
250
283
  label: interaction.label
251
284
  },
252
285
  reason: 'Expected user-visible outcome did not occur',
286
+ what_happened: 'Navigation attempt produced no visible effect',
287
+ what_was_expected: `Navigate to ${normalizedTarget || 'target page'}`,
288
+ what_was_observed: 'URL, DOM, and visuals remained unchanged',
289
+ why_it_matters: 'Users cannot reach the intended destination despite interacting',
253
290
  evidence: {
254
291
  before: beforeScreenshot,
255
292
  after: afterScreenshot,
@@ -269,6 +306,10 @@ export async function detect(manifestPath, tracesPath, validation = null, _expec
269
306
  label: interaction.label
270
307
  },
271
308
  reason: 'Expected user-visible outcome did not occur',
309
+ what_happened: 'User action produced no visible effect',
310
+ what_was_expected: 'Some user-visible change after interaction',
311
+ what_was_observed: 'URL, DOM, and visuals remained unchanged',
312
+ why_it_matters: 'Users cannot complete the intended action',
272
313
  evidence: {
273
314
  before: beforeScreenshot,
274
315
  after: afterScreenshot,
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Phase 4: Final Output Invariants (Trust Lock)
3
+ *
4
+ * Strict gate applied only to user-facing outputs (REPORT.json, SUMMARY.md, console).
5
+ * Findings violating any invariant are dropped silently. Purity and determinism are required.
6
+ */
7
+
8
+ const KNOWN_IMPACTS = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO', 'UNKNOWN'];
9
+ const VALID_STATUSES = ['CONFIRMED', 'SUSPECTED', 'INFORMATIONAL'];
10
+
11
+ function isValidEvidence(evidence) {
12
+ if (!evidence || typeof evidence !== 'object') return false;
13
+ const keys = Object.keys(evidence);
14
+ if (keys.length === 0) return false;
15
+
16
+ const signals = Object.values(evidence).filter(v => {
17
+ if (v === true || v === false) return true;
18
+ if (typeof v === 'number' && v !== 0) return true;
19
+ if (typeof v === 'string' && v.length > 0) return true;
20
+ if (Array.isArray(v)) return v.length > 0; // arrays must contain at least one element
21
+ if (v && typeof v === 'object') return Object.keys(v).length > 0;
22
+ return false;
23
+ });
24
+
25
+ return signals.length > 0;
26
+ }
27
+
28
+ function isValidConfidence(confidence) {
29
+ if (confidence === 0) return true;
30
+ if (!confidence) return false;
31
+
32
+ if (typeof confidence === 'number') {
33
+ return confidence >= 0 && confidence <= 1;
34
+ }
35
+
36
+ if (typeof confidence === 'object') {
37
+ const hasLevel = (typeof confidence.level === 'string' && confidence.level.length > 0) || typeof confidence.level === 'number';
38
+ const hasScore = typeof confidence.score === 'number';
39
+ return hasLevel || hasScore;
40
+ }
41
+
42
+ return false;
43
+ }
44
+
45
+ function isValidPromise(promise) {
46
+ if (!promise || typeof promise !== 'object') return false;
47
+
48
+ const { kind, value, type, expected, actual, expected_signal } = promise;
49
+
50
+ const kindValueValid = typeof kind === 'string' && kind.length > 0 && typeof value === 'string' && value.length > 0;
51
+ const typeValid = typeof type === 'string' && type.length > 0;
52
+ const expectationValid = (typeof expected === 'string' && expected.length > 0) ||
53
+ (typeof actual === 'string' && actual.length > 0) ||
54
+ (typeof expected_signal === 'string' && expected_signal.length > 0);
55
+
56
+ return kindValueValid || (typeValid && expectationValid);
57
+ }
58
+
59
+ function isInternalErrorFlag(finding) {
60
+ const internalMarkers = [
61
+ 'INTERNAL_ERROR',
62
+ 'internal-error',
63
+ 'internalError',
64
+ 'TIMEOUT_ERROR',
65
+ 'CRASH',
66
+ 'FATAL',
67
+ 'BROWSER_CRASH'
68
+ ];
69
+
70
+ if (internalMarkers.some(marker =>
71
+ finding.reason?.includes?.(marker) ||
72
+ finding.errorMessage?.includes?.(marker) ||
73
+ finding.errorStack?.includes?.(marker)
74
+ )) {
75
+ return true;
76
+ }
77
+
78
+ if (finding.reason?.toLowerCase?.().includes('internal error')) {
79
+ return true;
80
+ }
81
+
82
+ return false;
83
+ }
84
+
85
+ function isValidCause(cause, evidence) {
86
+ if (!cause || typeof cause !== 'object') return false;
87
+ if (!cause.id || !cause.statement) return false;
88
+
89
+ if (Array.isArray(cause.evidence_refs) && cause.evidence_refs.length > 0) {
90
+ if (!evidence || typeof evidence !== 'object') return false;
91
+ return cause.evidence_refs.every(ref => typeof ref === 'string' && ref in evidence);
92
+ }
93
+
94
+ return true;
95
+ }
96
+
97
+ export function filterCausesWithEvidence(evidence, causes) {
98
+ if (!Array.isArray(causes)) return [];
99
+ return causes.filter(cause => isValidCause(cause, evidence));
100
+ }
101
+
102
+ function enforceFinalInvariant(finding) {
103
+ if (!finding) return null;
104
+
105
+ if (!isValidEvidence(finding.evidence)) return null;
106
+ if (!isValidPromise(finding.promise)) return null;
107
+ if (!isValidConfidence(finding.confidence)) return null;
108
+ if (finding.impact && !KNOWN_IMPACTS.includes(finding.impact)) return null;
109
+ if (isInternalErrorFlag(finding)) return null;
110
+ if (!finding.id || typeof finding.id !== 'string' || finding.id.length === 0) return null;
111
+ if (finding.status && !VALID_STATUSES.includes(finding.status)) return null;
112
+
113
+ const causes = filterCausesWithEvidence(finding.evidence, finding.causes);
114
+
115
+ return { ...finding, causes };
116
+ }
117
+
118
+ export function deduplicateFindings(findings) {
119
+ const seen = new Set();
120
+ const deduped = [];
121
+
122
+ for (const finding of findings) {
123
+ const promiseKey = finding.promise ? JSON.stringify(finding.promise) : 'nopromise';
124
+ const key = `${finding.id || 'unknown'}|${finding.location || 'unknown'}|${promiseKey}`;
125
+ if (seen.has(key)) continue;
126
+ seen.add(key);
127
+ deduped.push(finding);
128
+ }
129
+
130
+ return deduped;
131
+ }
132
+
133
+ export function enforceFinalInvariants(findings) {
134
+ if (!Array.isArray(findings)) return [];
135
+
136
+ const validFindings = findings
137
+ .map(enforceFinalInvariant)
138
+ .filter(Boolean);
139
+
140
+ const deduped = deduplicateFindings(validFindings);
141
+
142
+ return deduped.sort((a, b) => {
143
+ const aId = (a.id || '').toString();
144
+ const bId = (b.id || '').toString();
145
+ return aId.localeCompare(bId);
146
+ });
147
+ }