@slicedearth/whoisleuth-cli 1.47.4 → 2.0.1

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 (236) hide show
  1. package/README.md +74 -177
  2. package/SECURITY.md +6 -3
  3. package/bin/whoisleuth.mjs +2 -1
  4. package/cli/archive-inspect.mjs +28 -10
  5. package/cli/arguments.mjs +204 -174
  6. package/cli/artifact-structure.mjs +837 -163
  7. package/cli/artifact-verify.mjs +90 -61
  8. package/cli/bounded-json.mjs +1 -1
  9. package/cli/bulk-checkpoint.mjs +33 -20
  10. package/cli/bulk.mjs +9 -2
  11. package/cli/case-pack.mjs +180 -576
  12. package/cli/ci-report.mjs +6 -1
  13. package/cli/collection-preflight.mjs +2 -2
  14. package/cli/command-catalogue.mjs +12 -3
  15. package/cli/command-reference.mjs +910 -92
  16. package/cli/completion.mjs +504 -226
  17. package/cli/config-profile.mjs +13 -3
  18. package/cli/discover.mjs +7 -1
  19. package/cli/discovery-observation-snapshot.mjs +24 -28
  20. package/cli/discovery-scan-command-runner.mjs +9 -9
  21. package/cli/discovery-scan.mjs +17 -3
  22. package/cli/discovery-snapshot.mjs +15 -7
  23. package/cli/domain-control-monitor.mjs +203 -30
  24. package/cli/domain-control-observations.mjs +15 -21
  25. package/cli/errors.mjs +8 -3
  26. package/cli/evidence-signing.mjs +10 -20
  27. package/cli/external-observation-mapping.mjs +3 -3
  28. package/cli/formatters/json.mjs +20 -32
  29. package/cli/formatters/terminal.mjs +583 -22
  30. package/cli/input.mjs +7 -2
  31. package/cli/interactive-launcher.mjs +226 -0
  32. package/cli/interchange-report.mjs +19 -19
  33. package/cli/investigation-manifest.mjs +1 -1
  34. package/cli/investigation-plan.mjs +167 -16
  35. package/cli/investigation-run.mjs +9 -1
  36. package/cli/lookalike-calibration.mjs +3 -1
  37. package/cli/lookup-brief.mjs +13 -8
  38. package/cli/lookup-browser.mjs +844 -0
  39. package/cli/lookup-command-runner.mjs +94 -31
  40. package/cli/lookup-diff.mjs +2 -2
  41. package/cli/lookup-plan.mjs +1 -0
  42. package/cli/mail-review.mjs +206 -94
  43. package/cli/manual.mjs +5 -5
  44. package/cli/offline-evidence-review.mjs +18 -7
  45. package/cli/open-asset-model-bridge.mjs +1 -1
  46. package/cli/output-file.mjs +70 -14
  47. package/cli/page-compare.mjs +11 -6
  48. package/cli/progress-events.mjs +2 -1
  49. package/cli/progress.mjs +3 -3
  50. package/cli/registry-cohort.mjs +9 -51
  51. package/cli/registry-doctor.mjs +67 -28
  52. package/cli/retained-artifact-diff.mjs +7 -8
  53. package/cli/risk-calibration.mjs +85 -38
  54. package/cli/runner.mjs +189 -75
  55. package/cli/saved-lookup.mjs +424 -57
  56. package/cli/sharing-review.mjs +6 -1
  57. package/cli/source-reliability.mjs +75 -36
  58. package/cli/terminal-presentation.mjs +43 -18
  59. package/docs/cli-reference.md +125 -1708
  60. package/docs/cli.md +86 -254
  61. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
  62. package/frontend/src/lib/analysis/domain-control-records.js +3 -88
  63. package/lib/availability.mjs +58 -40
  64. package/lib/bounded-contract-normalizers.mjs +2 -4
  65. package/lib/bounded-file.mjs +78 -4
  66. package/lib/bounded-json.mjs +51 -13
  67. package/lib/browser-library-profile.mjs +1 -1
  68. package/lib/classify.mjs +56 -5
  69. package/lib/client-behavior-profile.mjs +1 -1
  70. package/lib/credential-surface-profile.mjs +1 -1
  71. package/lib/cryptographic-assurance.mjs +35 -17
  72. package/lib/ct-query.mjs +3 -3
  73. package/lib/ct-search.mjs +37 -21
  74. package/lib/distributed-operation-budget.mjs +4 -2
  75. package/lib/dns-delegation-health.mjs +9 -6
  76. package/lib/dns-intelligence.mjs +1 -1
  77. package/lib/dnssec-chain-validation.mjs +2 -3
  78. package/lib/dnssec-evidence-validation.mjs +13 -7
  79. package/lib/domain-assurance.mjs +12 -12
  80. package/lib/domain-change-packet.mjs +1 -1
  81. package/lib/domain-control-flight-recorder.mjs +442 -78
  82. package/lib/domain-control-manifest.mjs +314 -59
  83. package/lib/domain-posture-analysis.mjs +5 -2
  84. package/lib/encrypted-dns-contract.mjs +2 -3
  85. package/lib/evidence-export.mjs +518 -36
  86. package/lib/external-intelligence-risk.mjs +26 -10
  87. package/lib/feature-policy.mjs +1 -1
  88. package/lib/homepage-metadata-contract.mjs +290 -0
  89. package/lib/hostname.mjs +1 -1
  90. package/lib/html-signals.mjs +89 -5
  91. package/lib/http-intelligence.mjs +331 -2
  92. package/lib/interchange-fidelity-registry.mjs +28 -26
  93. package/lib/lookup-cache.mjs +6 -4
  94. package/lib/lookup.mjs +25 -2
  95. package/lib/observed-network-context.mjs +1 -1
  96. package/lib/page-fingerprints.mjs +2 -2
  97. package/lib/page-role-profile.mjs +1 -1
  98. package/lib/portable-generator.mjs +8 -0
  99. package/lib/project-metadata.mjs +1 -1
  100. package/lib/provider-policy-admission.mjs +3 -1
  101. package/lib/rdap-bootstrap.mjs +76 -16
  102. package/lib/rdap-client.mjs +20 -12
  103. package/lib/rdap-normalization.mjs +306 -75
  104. package/lib/rdap-transport.mjs +2 -2
  105. package/lib/rdap-validation.mjs +65 -14
  106. package/lib/rdap.mjs +2 -2
  107. package/lib/registrable-domain.mjs +36 -0
  108. package/lib/registry-insights.mjs +7 -0
  109. package/lib/response-policy.mjs +27 -4
  110. package/lib/risk-calibration-summary.mjs +35 -33
  111. package/lib/risk-scoring.mjs +4 -3
  112. package/lib/safe-fetch.mjs +62 -40
  113. package/lib/security-txt.mjs +1 -1
  114. package/lib/smtp-transport-review.mjs +8 -5
  115. package/lib/sslbl-intelligence.mjs +6 -8
  116. package/lib/static-html-analysis.mjs +301 -4
  117. package/lib/structured-data-identity.mjs +51 -19
  118. package/lib/threat-intelligence-fixtures.mjs +2 -4
  119. package/lib/threat-intelligence-manifests.mjs +2 -7
  120. package/lib/threat-intelligence-runtime.mjs +3 -5
  121. package/lib/threat-intelligence-types.mjs +3 -0
  122. package/lib/threatfox-intelligence.mjs +7 -3
  123. package/lib/tls-intelligence.mjs +4 -4
  124. package/lib/urlhaus-intelligence.mjs +4 -2
  125. package/lib/urlscan-intelligence.mjs +6 -5
  126. package/lib/website-security-posture.mjs +1 -1
  127. package/lib/website-technology.mjs +1 -1
  128. package/lib/whois-authority.mjs +43 -8
  129. package/lib/whois-chain.mjs +25 -3
  130. package/lib/whois-normalization.mjs +3 -1
  131. package/lib/whois-transport.mjs +12 -3
  132. package/lib/zone-intent-review.mjs +1 -1
  133. package/package.json +13 -5
  134. package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
  135. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
  136. package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
  137. package/packages/cases/case-migration-model.mjs +476 -0
  138. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  139. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
  140. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  141. package/packages/cases/case-record-model.mjs +9 -0
  142. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
  143. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
  144. package/packages/cases/case-response-model.mjs +1604 -0
  145. package/packages/cases/case-response-packet.mjs +1312 -0
  146. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  147. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  148. package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
  149. package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
  150. package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
  151. package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
  152. package/packages/comparison/favicon-similarity.mjs +78 -0
  153. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  154. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  155. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  156. package/packages/contracts/analyst-review-state.mjs +50 -0
  157. package/packages/contracts/case-portability.mjs +1310 -0
  158. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  159. package/packages/contracts/cli-lookup.mjs +394 -0
  160. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  161. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  162. package/packages/contracts/domain-control-monitor.mjs +310 -0
  163. package/packages/contracts/domain-control-review.mjs +828 -0
  164. package/packages/contracts/domain-name.mjs +4 -0
  165. package/packages/contracts/external-observation-interchange.mjs +224 -0
  166. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  167. package/packages/contracts/investigation-portability.mjs +331 -0
  168. package/packages/contracts/offline-comparison.mjs +160 -0
  169. package/packages/contracts/risk-calibration.mjs +1288 -0
  170. package/packages/contracts/schema-compatibility.mjs +71 -0
  171. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  172. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  173. package/packages/contracts/workspace-portability.mjs +1347 -0
  174. package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
  175. package/packages/evidence/decision-fact.mjs +735 -0
  176. package/packages/evidence/domain-control-runtime.mjs +547 -0
  177. package/packages/evidence/domain-name.mjs +41 -0
  178. package/packages/evidence/observation.mjs +384 -0
  179. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
  180. package/packages/interchange/retained-text.mjs +8 -0
  181. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
  182. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
  183. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
  184. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
  185. package/packages/investigation/bulk-review-policy.mjs +1 -0
  186. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
  187. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  188. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  189. package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
  190. package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
  191. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  192. package/packages/monitoring/analyst-review-state.mjs +680 -0
  193. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +71 -48
  194. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
  195. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
  196. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
  197. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  198. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  199. package/packages/workspace/hostile-input.mjs +172 -0
  200. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
  201. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
  202. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  203. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  204. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  205. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  206. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  207. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
  208. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  209. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
  210. package/third-party-notices.txt +14 -14
  211. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  212. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  213. package/frontend/src/lib/analysis/case-migration-model.js +0 -305
  214. package/frontend/src/lib/analysis/case-record-model.js +0 -9
  215. package/frontend/src/lib/analysis/case-response-model.js +0 -728
  216. package/frontend/src/lib/analysis/case-response-packet.js +0 -583
  217. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
  218. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  219. package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
  220. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  221. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  222. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  223. package/frontend/src/lib/analysis/scoring.js +0 -55
  224. package/frontend/src/lib/analysis/utils.js +0 -255
  225. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  226. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  227. package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
  228. package/frontend/src/lib/analysis/visualization-models.js +0 -338
  229. package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
  230. package/lib/bounded-json-response.mjs +0 -170
  231. package/lib/lookup-response-contract.mjs +0 -1034
  232. package/lib/observation.mjs +0 -115
  233. /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
  234. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  235. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  236. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -0,0 +1,735 @@
1
+ // Dependency-neutral canonical Decision Fact v1 model. Facts remain a bounded
2
+ // derived runtime model; the separately versioned projection is the portable
3
+ // nested contract used by decision-surface exports.
4
+ export const DECISION_FACT_VERSION = 1;
5
+ export const MAX_DECISION_FACTS = 40;
6
+ export const MAX_DECISION_FACT_CONTRIBUTORS = 24;
7
+ export const MAX_DECISION_FACT_REFERENCES = 24;
8
+ export const MAX_DECISION_FACT_CONTRADICTIONS = 16;
9
+ export const MAX_DECISION_FACT_LIMITATIONS = 8;
10
+ export const MAX_DECISION_FACT_NEXT_ACTIONS = 6;
11
+ export const DECISION_FACT_PROJECTION_VERSION = 1;
12
+ export const MAX_DECISION_FACT_PROJECTION_FACTS = 12;
13
+ export const MAX_DECISION_FACT_PROJECTION_SOURCES = 8;
14
+ export const MAX_DECISION_FACT_PROJECTION_REFERENCES = 12;
15
+ export const MAX_DECISION_FACT_PROJECTION_SOURCE_REFERENCES = 6;
16
+ export const MAX_DECISION_FACT_PROJECTION_SOURCE_LIMITATIONS = 4;
17
+ export const MAX_DECISION_FACT_PROJECTION_CONTRADICTIONS = 8;
18
+ export const MAX_DECISION_FACT_PROJECTION_LIMITATIONS = 8;
19
+ export const MAX_DECISION_FACT_PROJECTION_NEXT_ACTIONS = 4;
20
+ export const MAX_DECISION_FACT_PROJECTION_BYTES = 48 * 1024;
21
+ export const DECISION_FACT_EVIDENCE_STATES = Object.freeze([
22
+ 'observed',
23
+ 'not_observed_in_bounded_evidence',
24
+ 'not_collected',
25
+ 'partial',
26
+ 'unsupported',
27
+ 'unavailable',
28
+ 'unknown',
29
+ ]);
30
+ export const DECISION_FACT_FRESHNESS_STATES = Object.freeze([
31
+ 'current',
32
+ 'stale',
33
+ 'unknown',
34
+ 'not_applicable',
35
+ ]);
36
+ export const DECISION_FACT_CONSISTENCY_STATES = Object.freeze([
37
+ 'consistent',
38
+ 'contradictory',
39
+ 'unknown',
40
+ 'not_applicable',
41
+ ]);
42
+ export const DECISION_FACT_PROVENANCE_STATES = Object.freeze([
43
+ 'direct_observation',
44
+ 'provider_reported',
45
+ 'analyst_supplied',
46
+ 'derived',
47
+ ]);
48
+ function presentationDescriptor(value) {
49
+ return Object.freeze(value);
50
+ }
51
+ export const DECISION_FACT_PRESENTATION_DESCRIPTORS = Object.freeze({
52
+ evidenceState: Object.freeze({
53
+ observed: presentationDescriptor({
54
+ label: 'Observed',
55
+ explanation: 'The bounded evidence contains an observation for this fact.',
56
+ tone: 'neutral',
57
+ icon: 'evidence-observed',
58
+ assistiveText: 'Bounded evidence contains an observation for this fact. The observation does not establish safety, legitimacy, ownership, or a favourable result.',
59
+ }),
60
+ not_observed_in_bounded_evidence: presentationDescriptor({
61
+ label: 'Not observed in bounded evidence',
62
+ explanation: 'The bounded collection did not retain a matching observation; this is not a generic absence claim.',
63
+ tone: 'caution',
64
+ icon: 'bounded-non-observation',
65
+ assistiveText: 'No matching observation was retained within the bounded collection. Evidence outside that scope remains unknown.',
66
+ }),
67
+ not_collected: presentationDescriptor({
68
+ label: 'Not collected',
69
+ explanation: 'The source or check was not run for this fact.',
70
+ tone: 'caution',
71
+ icon: 'collection-not-run',
72
+ assistiveText: 'This evidence was not collected, so there is no collection result to interpret.',
73
+ }),
74
+ partial: presentationDescriptor({
75
+ label: 'Partial',
76
+ explanation: 'The retained evidence is incomplete and its stated limitations still apply.',
77
+ tone: 'caution',
78
+ icon: 'evidence-limited',
79
+ assistiveText: 'Only partial evidence is available. Review the attributed limitations before drawing a conclusion.',
80
+ }),
81
+ unsupported: presentationDescriptor({
82
+ label: 'Unsupported',
83
+ explanation: 'The selected source path does not support this evidence family.',
84
+ tone: 'caution',
85
+ icon: 'source-unsupported',
86
+ assistiveText: 'This evidence family is unsupported by the selected source path and provides no result for interpretation.',
87
+ }),
88
+ unavailable: presentationDescriptor({
89
+ label: 'Unavailable',
90
+ explanation: 'The source or check could not provide a retained result.',
91
+ tone: 'caution',
92
+ icon: 'source-unavailable',
93
+ assistiveText: 'The evidence source was unavailable. Its unavailable state does not establish a result about the target.',
94
+ }),
95
+ unknown: presentationDescriptor({
96
+ label: 'Unknown',
97
+ explanation: 'The retained evidence does not determine this state.',
98
+ tone: 'caution',
99
+ icon: 'state-unknown',
100
+ assistiveText: 'The evidence state is unknown and supports no positive or negative conclusion.',
101
+ }),
102
+ }),
103
+ freshness: Object.freeze({
104
+ current: presentationDescriptor({
105
+ label: 'Current',
106
+ explanation: 'The observation is within the applicable freshness window.',
107
+ tone: 'neutral',
108
+ icon: 'observation-current',
109
+ assistiveText: 'The observation is current under the configured freshness policy. Current evidence does not establish safety, correctness, or a favourable result.',
110
+ }),
111
+ stale: presentationDescriptor({
112
+ label: 'Stale',
113
+ explanation: 'The observation is outside the applicable freshness window.',
114
+ tone: 'caution',
115
+ icon: 'observation-stale',
116
+ assistiveText: 'The observation is stale under the configured freshness policy and may no longer describe the current state.',
117
+ }),
118
+ unknown: presentationDescriptor({
119
+ label: 'Freshness unknown',
120
+ explanation: 'The retained evidence does not establish when this fact was observed.',
121
+ tone: 'caution',
122
+ icon: 'state-unknown',
123
+ assistiveText: 'Freshness is unknown, so the retained evidence cannot establish whether this fact is current.',
124
+ }),
125
+ not_applicable: presentationDescriptor({
126
+ label: 'Freshness not applicable',
127
+ explanation: 'A freshness assessment does not apply to this fact state.',
128
+ tone: 'neutral',
129
+ icon: 'state-not-applicable',
130
+ assistiveText: 'Freshness is not applicable to this fact state; this does not supply a missing observation.',
131
+ }),
132
+ }),
133
+ consistency: Object.freeze({
134
+ consistent: presentationDescriptor({
135
+ label: 'Consistent',
136
+ explanation: 'The separately attributed observations agree for this comparison.',
137
+ tone: 'neutral',
138
+ icon: 'source-agreement',
139
+ assistiveText: 'The attributed observations are consistent for this comparison. Agreement does not establish correctness, authority, or safety.',
140
+ }),
141
+ contradictory: presentationDescriptor({
142
+ label: 'Contradictory',
143
+ explanation: 'Separately attributed observations disagree for this comparison.',
144
+ tone: 'conflict',
145
+ icon: 'source-disagreement',
146
+ assistiveText: 'The attributed observations disagree. Source ordering does not decide which observation should govern the review.',
147
+ }),
148
+ unknown: presentationDescriptor({
149
+ label: 'Consistency unknown',
150
+ explanation: 'The retained evidence does not support an agreement or disagreement conclusion.',
151
+ tone: 'caution',
152
+ icon: 'state-unknown',
153
+ assistiveText: 'Consistency remains unknown because the attributed evidence is incomplete or indeterminate.',
154
+ }),
155
+ not_applicable: presentationDescriptor({
156
+ label: 'Consistency not applicable',
157
+ explanation: 'A source-consistency comparison does not apply to this fact.',
158
+ tone: 'neutral',
159
+ icon: 'state-not-applicable',
160
+ assistiveText: 'Consistency is not applicable because this fact is not a retained source comparison.',
161
+ }),
162
+ }),
163
+ provenance: Object.freeze({
164
+ direct_observation: presentationDescriptor({
165
+ label: 'Direct observation',
166
+ explanation: 'The contributor records a direct bounded observation.',
167
+ tone: 'neutral',
168
+ icon: 'evidence-direct',
169
+ assistiveText: 'This contributor records a direct bounded observation with its own scope and limitations.',
170
+ }),
171
+ provider_reported: presentationDescriptor({
172
+ label: 'Provider reported',
173
+ explanation: 'The contributor reports data published by its named provider.',
174
+ tone: 'neutral',
175
+ icon: 'evidence-reported',
176
+ assistiveText: 'This contributor retains provider-reported evidence without treating the provider statement as independently verified.',
177
+ }),
178
+ analyst_supplied: presentationDescriptor({
179
+ label: 'Analyst supplied',
180
+ explanation: 'The contributor was supplied through analyst-controlled context.',
181
+ tone: 'neutral',
182
+ icon: 'evidence-analyst-supplied',
183
+ assistiveText: 'This contributor came from analyst-supplied context and retains that attribution.',
184
+ }),
185
+ derived: presentationDescriptor({
186
+ label: 'Derived',
187
+ explanation: 'The contributor was derived from retained inputs.',
188
+ tone: 'neutral',
189
+ icon: 'evidence-derived',
190
+ assistiveText: 'This contributor is derived from retained inputs and does not gain authority beyond those inputs.',
191
+ }),
192
+ }),
193
+ });
194
+ export const DECISION_FACT_PRESENTATION_LABELS = Object.freeze({
195
+ evidenceState: Object.freeze({
196
+ observed: DECISION_FACT_PRESENTATION_DESCRIPTORS.evidenceState.observed.label,
197
+ not_observed_in_bounded_evidence: DECISION_FACT_PRESENTATION_DESCRIPTORS.evidenceState.not_observed_in_bounded_evidence.label,
198
+ not_collected: DECISION_FACT_PRESENTATION_DESCRIPTORS.evidenceState.not_collected.label,
199
+ partial: DECISION_FACT_PRESENTATION_DESCRIPTORS.evidenceState.partial.label,
200
+ unsupported: DECISION_FACT_PRESENTATION_DESCRIPTORS.evidenceState.unsupported.label,
201
+ unavailable: DECISION_FACT_PRESENTATION_DESCRIPTORS.evidenceState.unavailable.label,
202
+ unknown: DECISION_FACT_PRESENTATION_DESCRIPTORS.evidenceState.unknown.label,
203
+ }),
204
+ freshness: Object.freeze({
205
+ current: DECISION_FACT_PRESENTATION_DESCRIPTORS.freshness.current.label,
206
+ stale: DECISION_FACT_PRESENTATION_DESCRIPTORS.freshness.stale.label,
207
+ unknown: DECISION_FACT_PRESENTATION_DESCRIPTORS.freshness.unknown.label,
208
+ not_applicable: DECISION_FACT_PRESENTATION_DESCRIPTORS.freshness.not_applicable.label,
209
+ }),
210
+ consistency: Object.freeze({
211
+ consistent: DECISION_FACT_PRESENTATION_DESCRIPTORS.consistency.consistent.label,
212
+ contradictory: DECISION_FACT_PRESENTATION_DESCRIPTORS.consistency.contradictory.label,
213
+ unknown: DECISION_FACT_PRESENTATION_DESCRIPTORS.consistency.unknown.label,
214
+ not_applicable: DECISION_FACT_PRESENTATION_DESCRIPTORS.consistency.not_applicable.label,
215
+ }),
216
+ provenance: Object.freeze({
217
+ direct_observation: DECISION_FACT_PRESENTATION_DESCRIPTORS.provenance.direct_observation.label,
218
+ provider_reported: DECISION_FACT_PRESENTATION_DESCRIPTORS.provenance.provider_reported.label,
219
+ analyst_supplied: DECISION_FACT_PRESENTATION_DESCRIPTORS.provenance.analyst_supplied.label,
220
+ derived: DECISION_FACT_PRESENTATION_DESCRIPTORS.provenance.derived.label,
221
+ }),
222
+ importance: Object.freeze({
223
+ high: 'High',
224
+ medium: 'Medium',
225
+ low: 'Low',
226
+ }),
227
+ completeness: Object.freeze({
228
+ complete: 'Complete bounded collection',
229
+ partial: 'Partial',
230
+ not_collected: 'Not collected',
231
+ unsupported: 'Unsupported',
232
+ unavailable: 'Unavailable',
233
+ unknown: 'Unknown',
234
+ }),
235
+ });
236
+ const MAX_IDENTIFIER_LENGTH = 200;
237
+ const MAX_LABEL_LENGTH = 160;
238
+ const MAX_QUESTION_LENGTH = 320;
239
+ const MAX_CONCLUSION_LENGTH = 640;
240
+ const MAX_REFERENCE_LENGTH = 200;
241
+ const MAX_LIMITATION_LENGTH = 280;
242
+ const MAX_ACTION_DETAIL_LENGTH = 320;
243
+ const MAX_TIMESTAMP_LENGTH = 64;
244
+ const MAX_TEXT_INPUT_FACTOR = 4;
245
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/gu;
246
+ const HAS_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/u;
247
+ const IDENTIFIER_PATTERN = /^[a-z0-9](?:[a-z0-9._:-]{0,199})$/u;
248
+ const HREF_PATTERN = /^#[a-z0-9](?:[a-z0-9._:-]{0,159})$/u;
249
+ const ISO_DATE_TIME_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{1,43})?(Z|[+-]\d{2}:\d{2})$/u;
250
+ const UTF8_ENCODER = new TextEncoder();
251
+ const EVIDENCE_STATES = new Set(DECISION_FACT_EVIDENCE_STATES);
252
+ const FRESHNESS_STATES = new Set(DECISION_FACT_FRESHNESS_STATES);
253
+ const CONSISTENCY_STATES = new Set(DECISION_FACT_CONSISTENCY_STATES);
254
+ const PROVENANCE_STATES = new Set(DECISION_FACT_PROVENANCE_STATES);
255
+ const IMPORTANCE_STATES = new Set(['high', 'medium', 'low']);
256
+ function compareCodeUnits(left, right) {
257
+ return left < right ? -1 : left > right ? 1 : 0;
258
+ }
259
+ function descriptorsMatch(left, right) {
260
+ if (!left || !right)
261
+ return left === right;
262
+ if (left.configurable !== right.configurable || left.enumerable !== right.enumerable)
263
+ return false;
264
+ const leftIsData = 'value' in left;
265
+ const rightIsData = 'value' in right;
266
+ if (leftIsData !== rightIsData)
267
+ return false;
268
+ return leftIsData
269
+ ? left.writable === right.writable && Object.is(left.value, right.value)
270
+ : Object.is(left.get, right.get) && Object.is(left.set, right.set);
271
+ }
272
+ function stableOwnDescriptor(value, key, label) {
273
+ try {
274
+ const first = Object.getOwnPropertyDescriptor(value, key);
275
+ const second = Object.getOwnPropertyDescriptor(value, key);
276
+ if (!descriptorsMatch(first, second))
277
+ throw new TypeError('unstable descriptor');
278
+ return first;
279
+ }
280
+ catch {
281
+ throw new TypeError(`${label} must expose stable ordinary data descriptors.`);
282
+ }
283
+ }
284
+ function stablePrototype(value, expected, label) {
285
+ try {
286
+ const first = Object.getPrototypeOf(value);
287
+ const second = Object.getPrototypeOf(value);
288
+ if (first !== second || first !== expected)
289
+ throw new TypeError('unexpected prototype');
290
+ }
291
+ catch {
292
+ throw new TypeError(`${label} must use an ordinary prototype.`);
293
+ }
294
+ }
295
+ function dataRecord(value, fields, label) {
296
+ let isArray = false;
297
+ try {
298
+ isArray = Array.isArray(value);
299
+ }
300
+ catch {
301
+ throw new TypeError(`${label} must be a plain object.`);
302
+ }
303
+ if (!value || typeof value !== 'object' || isArray)
304
+ throw new TypeError(`${label} must be a plain object.`);
305
+ const prototype = (() => {
306
+ try {
307
+ return Object.getPrototypeOf(value);
308
+ }
309
+ catch {
310
+ throw new TypeError(`${label} must use an ordinary prototype.`);
311
+ }
312
+ })();
313
+ if (prototype !== Object.prototype && prototype !== null)
314
+ throw new TypeError(`${label} must be a plain object.`);
315
+ stablePrototype(value, prototype, label);
316
+ for (const behaviour of ['toJSON', Symbol.iterator]) {
317
+ if (stableOwnDescriptor(value, behaviour, label)) {
318
+ throw new TypeError(`${label} must not define custom serialisation or iteration.`);
319
+ }
320
+ }
321
+ const output = Object.create(null);
322
+ for (const field of fields) {
323
+ const descriptor = stableOwnDescriptor(value, field, label);
324
+ if (!descriptor)
325
+ continue;
326
+ if (!descriptor.enumerable || !('value' in descriptor)) {
327
+ throw new TypeError(`${label} must use enumerable ordinary data fields.`);
328
+ }
329
+ output[field] = descriptor.value;
330
+ }
331
+ return output;
332
+ }
333
+ function boundedDataArray(value, maximum, label) {
334
+ let isArray = false;
335
+ try {
336
+ isArray = Array.isArray(value);
337
+ }
338
+ catch {
339
+ throw new TypeError(`${label} must be an ordinary array.`);
340
+ }
341
+ if (!isArray || !value || typeof value !== 'object')
342
+ throw new TypeError(`${label} must be an ordinary array.`);
343
+ stablePrototype(value, Array.prototype, label);
344
+ if (stableOwnDescriptor(value, Symbol.iterator, label) || stableOwnDescriptor(value, 'toJSON', label)) {
345
+ throw new TypeError(`${label} must not define custom iteration or serialisation.`);
346
+ }
347
+ const lengthDescriptor = stableOwnDescriptor(value, 'length', label);
348
+ const length = lengthDescriptor && 'value' in lengthDescriptor ? lengthDescriptor.value : null;
349
+ if (!lengthDescriptor
350
+ || lengthDescriptor.enumerable
351
+ || !Number.isSafeInteger(length)
352
+ || Number(length) < 0) {
353
+ throw new TypeError(`${label} must have a stable non-negative length.`);
354
+ }
355
+ const retainedLength = Math.min(Number(length), maximum);
356
+ const output = [];
357
+ for (let index = 0; index < retainedLength; index += 1) {
358
+ const descriptor = stableOwnDescriptor(value, String(index), label);
359
+ if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
360
+ throw new TypeError(`${label} must contain dense ordinary indexed data.`);
361
+ }
362
+ output.push(descriptor.value);
363
+ }
364
+ return output;
365
+ }
366
+ function boundedText(value, maximum, label) {
367
+ if (typeof value !== 'string' || value.length > maximum * MAX_TEXT_INPUT_FACTOR) {
368
+ throw new TypeError(`${label} must be bounded text.`);
369
+ }
370
+ const normalized = value
371
+ .replace(CONTROL_CHARACTERS, ' ')
372
+ .replace(/\s+/gu, ' ')
373
+ .trim()
374
+ .slice(0, maximum)
375
+ .trim();
376
+ if (!normalized)
377
+ throw new TypeError(`${label} must be non-empty bounded text.`);
378
+ return normalized;
379
+ }
380
+ function identifier(value, label) {
381
+ if (typeof value !== 'string' || value.length > MAX_IDENTIFIER_LENGTH) {
382
+ throw new TypeError(`${label} must be a bounded stable identifier.`);
383
+ }
384
+ const normalized = boundedText(value, MAX_IDENTIFIER_LENGTH, label);
385
+ if (!IDENTIFIER_PATTERN.test(normalized))
386
+ throw new TypeError(`${label} must be a stable identifier.`);
387
+ return normalized;
388
+ }
389
+ function stateValue(value, allowed, label) {
390
+ if (typeof value !== 'string' || !allowed.has(value)) {
391
+ throw new TypeError(`${label} has an unsupported state.`);
392
+ }
393
+ return value;
394
+ }
395
+ function canonicalTimestamp(value) {
396
+ if (value === undefined || value === null)
397
+ return null;
398
+ if (typeof value !== 'string'
399
+ || value.length === 0
400
+ || value.length > MAX_TIMESTAMP_LENGTH
401
+ || HAS_CONTROL_CHARACTERS.test(value))
402
+ return null;
403
+ const match = ISO_DATE_TIME_PATTERN.exec(value);
404
+ if (!match)
405
+ return null;
406
+ const year = Number(match[1]);
407
+ const month = Number(match[2]);
408
+ const day = Number(match[3]);
409
+ const hour = Number(match[4]);
410
+ const minute = Number(match[5]);
411
+ const second = Number(match[6]);
412
+ if (year < 1 || month < 1 || month > 12 || day < 1 || hour > 23 || minute > 59 || second > 59)
413
+ return null;
414
+ const calendar = new Date(0);
415
+ calendar.setUTCFullYear(year, month - 1, day);
416
+ calendar.setUTCHours(hour, minute, second, 0);
417
+ if (calendar.getUTCFullYear() !== year
418
+ || calendar.getUTCMonth() !== month - 1
419
+ || calendar.getUTCDate() !== day
420
+ || calendar.getUTCHours() !== hour
421
+ || calendar.getUTCMinutes() !== minute
422
+ || calendar.getUTCSeconds() !== second)
423
+ return null;
424
+ const zone = match[8] ?? '';
425
+ if (zone !== 'Z') {
426
+ const zoneHours = Number(zone.slice(1, 3));
427
+ const zoneMinutes = Number(zone.slice(4, 6));
428
+ if (zoneHours > 23 || zoneMinutes > 59)
429
+ return null;
430
+ }
431
+ const parsed = Date.parse(value);
432
+ if (!Number.isFinite(parsed))
433
+ return null;
434
+ const normalized = new Date(parsed);
435
+ return normalized.getUTCFullYear() >= 1 && normalized.getUTCFullYear() <= 9_999
436
+ ? normalized.toISOString()
437
+ : null;
438
+ }
439
+ function normalizedTextList(value, maximumItems, maximumText, label) {
440
+ if (value === undefined)
441
+ return Object.freeze([]);
442
+ const source = boundedDataArray(value, maximumItems, label);
443
+ const retained = source.map((item) => boundedText(item, maximumText, `${label} entry`));
444
+ return Object.freeze([...new Set(retained)].sort(compareCodeUnits));
445
+ }
446
+ function sameCanonicalValue(left, right) {
447
+ return JSON.stringify(left) === JSON.stringify(right);
448
+ }
449
+ function normalizeContributor(value) {
450
+ const source = dataRecord(value, [
451
+ 'id',
452
+ 'label',
453
+ 'provenance',
454
+ 'evidenceState',
455
+ 'references',
456
+ 'observedAt',
457
+ 'limitations',
458
+ ], 'Decision Fact contributor');
459
+ return Object.freeze({
460
+ id: identifier(source.id, 'Decision Fact contributor identifier'),
461
+ label: boundedText(source.label, MAX_LABEL_LENGTH, 'Decision Fact contributor label'),
462
+ provenance: stateValue(source.provenance, PROVENANCE_STATES, 'Decision Fact contributor provenance'),
463
+ evidenceState: stateValue(source.evidenceState, EVIDENCE_STATES, 'Decision Fact contributor evidence'),
464
+ references: normalizedTextList(source.references, MAX_DECISION_FACT_REFERENCES, MAX_REFERENCE_LENGTH, 'Decision Fact contributor references'),
465
+ observedAt: canonicalTimestamp(source.observedAt),
466
+ limitations: normalizedTextList(source.limitations, MAX_DECISION_FACT_LIMITATIONS, MAX_LIMITATION_LENGTH, 'Decision Fact contributor limitations'),
467
+ });
468
+ }
469
+ function normalizeContributors(value) {
470
+ if (value === undefined)
471
+ return Object.freeze([]);
472
+ const source = boundedDataArray(value, MAX_DECISION_FACT_CONTRIBUTORS, 'Decision Fact contributors');
473
+ const retained = new Map();
474
+ for (const item of source) {
475
+ const contributor = normalizeContributor(item);
476
+ const existing = retained.get(contributor.id);
477
+ if (existing && !sameCanonicalValue(existing, contributor)) {
478
+ throw new TypeError(`Decision Fact contributor identifier is ambiguous: ${contributor.id}.`);
479
+ }
480
+ if (!existing)
481
+ retained.set(contributor.id, contributor);
482
+ }
483
+ return Object.freeze([...retained.values()].sort((left, right) => compareCodeUnits(left.id, right.id)));
484
+ }
485
+ function normalizeNextAction(value) {
486
+ const source = dataRecord(value, [
487
+ 'id',
488
+ 'label',
489
+ 'reason',
490
+ 'expectedOutcome',
491
+ 'href',
492
+ 'importance',
493
+ ], 'Decision Fact next action');
494
+ const href = boundedText(source.href, 160, 'Decision Fact next action destination');
495
+ if (!HREF_PATTERN.test(href)) {
496
+ throw new TypeError('Decision Fact next action destination must be an internal review fragment.');
497
+ }
498
+ return Object.freeze({
499
+ id: identifier(source.id, 'Decision Fact next action identifier'),
500
+ label: boundedText(source.label, MAX_LABEL_LENGTH, 'Decision Fact next action label'),
501
+ reason: boundedText(source.reason, MAX_ACTION_DETAIL_LENGTH, 'Decision Fact next action reason'),
502
+ expectedOutcome: boundedText(source.expectedOutcome, MAX_ACTION_DETAIL_LENGTH, 'Decision Fact next action expected outcome'),
503
+ href: href,
504
+ importance: stateValue(source.importance, IMPORTANCE_STATES, 'Decision Fact next action importance'),
505
+ });
506
+ }
507
+ function normalizeNextActions(value) {
508
+ if (value === undefined)
509
+ return Object.freeze([]);
510
+ const source = boundedDataArray(value, MAX_DECISION_FACT_NEXT_ACTIONS, 'Decision Fact next actions');
511
+ const retained = new Map();
512
+ for (const item of source) {
513
+ const action = normalizeNextAction(item);
514
+ const existing = retained.get(action.id);
515
+ if (existing && !sameCanonicalValue(existing, action)) {
516
+ throw new TypeError(`Decision Fact next action identifier is ambiguous: ${action.id}.`);
517
+ }
518
+ if (!existing)
519
+ retained.set(action.id, action);
520
+ }
521
+ return Object.freeze([...retained.values()].sort((left, right) => compareCodeUnits(left.id, right.id)));
522
+ }
523
+ export function createDecisionFact(value) {
524
+ const source = dataRecord(value, [
525
+ 'id',
526
+ 'question',
527
+ 'conclusion',
528
+ 'importance',
529
+ 'evidenceState',
530
+ 'freshness',
531
+ 'consistency',
532
+ 'contributors',
533
+ 'references',
534
+ 'contradictions',
535
+ 'limitations',
536
+ 'nextActions',
537
+ ], 'Decision Fact');
538
+ const contributors = normalizeContributors(source.contributors);
539
+ return Object.freeze({
540
+ version: DECISION_FACT_VERSION,
541
+ id: identifier(source.id, 'Decision Fact identifier'),
542
+ question: boundedText(source.question, MAX_QUESTION_LENGTH, 'Decision Fact question'),
543
+ conclusion: boundedText(source.conclusion, MAX_CONCLUSION_LENGTH, 'Decision Fact conclusion'),
544
+ importance: stateValue(source.importance, IMPORTANCE_STATES, 'Decision Fact importance'),
545
+ evidenceState: stateValue(source.evidenceState, EVIDENCE_STATES, 'Decision Fact evidence'),
546
+ freshness: stateValue(source.freshness, FRESHNESS_STATES, 'Decision Fact freshness'),
547
+ consistency: stateValue(source.consistency, CONSISTENCY_STATES, 'Decision Fact consistency'),
548
+ contributors,
549
+ contributorCount: contributors.length,
550
+ references: normalizedTextList(source.references, MAX_DECISION_FACT_REFERENCES, MAX_REFERENCE_LENGTH, 'Decision Fact references'),
551
+ contradictions: normalizedTextList(source.contradictions, MAX_DECISION_FACT_CONTRADICTIONS, MAX_CONCLUSION_LENGTH, 'Decision Fact contradictions'),
552
+ limitations: normalizedTextList(source.limitations, MAX_DECISION_FACT_LIMITATIONS, MAX_LIMITATION_LENGTH, 'Decision Fact limitations'),
553
+ nextActions: normalizeNextActions(source.nextActions),
554
+ });
555
+ }
556
+ export function buildDecisionFacts(value) {
557
+ const source = boundedDataArray(value, MAX_DECISION_FACTS, 'Decision Facts');
558
+ const retained = new Map();
559
+ for (const item of source) {
560
+ const fact = createDecisionFact(item);
561
+ const existing = retained.get(fact.id);
562
+ if (existing && !sameCanonicalValue(existing, fact)) {
563
+ throw new TypeError(`Decision Fact identifier is ambiguous: ${fact.id}.`);
564
+ }
565
+ if (!existing)
566
+ retained.set(fact.id, fact);
567
+ }
568
+ return Object.freeze([...retained.values()].sort((left, right) => compareCodeUnits(left.id, right.id)));
569
+ }
570
+ function canonicalFactShape(fact) {
571
+ return JSON.stringify({
572
+ version: fact.version,
573
+ id: fact.id,
574
+ question: fact.question,
575
+ conclusion: fact.conclusion,
576
+ importance: fact.importance,
577
+ evidenceState: fact.evidenceState,
578
+ freshness: fact.freshness,
579
+ consistency: fact.consistency,
580
+ contributors: fact.contributors.map((contributor) => ({
581
+ id: contributor.id,
582
+ label: contributor.label,
583
+ provenance: contributor.provenance,
584
+ evidenceState: contributor.evidenceState,
585
+ references: [...contributor.references],
586
+ observedAt: contributor.observedAt,
587
+ limitations: [...contributor.limitations],
588
+ })),
589
+ contributorCount: fact.contributorCount,
590
+ references: [...fact.references],
591
+ contradictions: [...fact.contradictions],
592
+ limitations: [...fact.limitations],
593
+ nextActions: fact.nextActions.map((action) => ({
594
+ id: action.id,
595
+ label: action.label,
596
+ reason: action.reason,
597
+ expectedOutcome: action.expectedOutcome,
598
+ href: action.href,
599
+ importance: action.importance,
600
+ })),
601
+ });
602
+ }
603
+ export function canonicalDecisionFacts(value) {
604
+ const canonical = buildDecisionFacts(value);
605
+ if (value.length > MAX_DECISION_FACTS || canonical.length !== value.length) {
606
+ throw new TypeError('Decision Facts must not contain duplicate or over-limit identifiers.');
607
+ }
608
+ const canonicalById = new Map(canonical.map((fact) => [fact.id, fact]));
609
+ for (const fact of value) {
610
+ const rebuilt = canonicalById.get(fact.id);
611
+ if (!rebuilt
612
+ || fact.version !== DECISION_FACT_VERSION
613
+ || !Number.isSafeInteger(fact.contributorCount)
614
+ || fact.contributorCount !== fact.contributors.length
615
+ || fact.contributors.length !== rebuilt.contributors.length
616
+ || fact.references.length !== rebuilt.references.length
617
+ || fact.contradictions.length !== rebuilt.contradictions.length
618
+ || fact.limitations.length !== rebuilt.limitations.length
619
+ || fact.nextActions.length !== rebuilt.nextActions.length
620
+ || fact.contributors.some((contributor, index) => {
621
+ const canonicalContributor = rebuilt.contributors[index];
622
+ return !canonicalContributor
623
+ || contributor.references.length !== canonicalContributor.references.length
624
+ || contributor.limitations.length !== canonicalContributor.limitations.length;
625
+ })
626
+ || canonicalFactShape(fact) !== canonicalFactShape(rebuilt)) {
627
+ throw new TypeError('Decision Facts must be canonical Decision Fact values.');
628
+ }
629
+ }
630
+ return canonical;
631
+ }
632
+ export function decisionFactCompleteness(state) {
633
+ if (state === 'observed' || state === 'not_observed_in_bounded_evidence')
634
+ return 'complete';
635
+ if (state === 'partial')
636
+ return 'partial';
637
+ if (state === 'not_collected')
638
+ return 'not_collected';
639
+ if (state === 'unsupported')
640
+ return 'unsupported';
641
+ if (state === 'unavailable')
642
+ return 'unavailable';
643
+ return 'unknown';
644
+ }
645
+ function projectionCollection(values, maximum, project) {
646
+ const items = Object.freeze(values.slice(0, maximum).map(project));
647
+ const total = values.length;
648
+ const displayed = items.length;
649
+ const omitted = total - displayed;
650
+ if (total !== displayed + omitted) {
651
+ throw new RangeError('Decision Fact projection counts did not reconcile.');
652
+ }
653
+ return Object.freeze({ total, displayed, omitted, items });
654
+ }
655
+ function sourceProjection(source) {
656
+ return Object.freeze({
657
+ id: source.id,
658
+ label: source.label,
659
+ provenance: source.provenance,
660
+ evidenceState: source.evidenceState,
661
+ observedAt: source.observedAt,
662
+ references: projectionCollection(source.references, MAX_DECISION_FACT_PROJECTION_SOURCE_REFERENCES, (reference) => reference),
663
+ limitations: projectionCollection(source.limitations, MAX_DECISION_FACT_PROJECTION_SOURCE_LIMITATIONS, (limitation) => limitation),
664
+ });
665
+ }
666
+ function factProjection(fact) {
667
+ const sources = projectionCollection(fact.contributors, MAX_DECISION_FACT_PROJECTION_SOURCES, sourceProjection);
668
+ const dependencies = projectionCollection(fact.contributors.map((source) => source.id), MAX_DECISION_FACT_PROJECTION_SOURCES, (dependency) => dependency);
669
+ if (sources.total !== dependencies.total
670
+ || sources.displayed !== dependencies.displayed
671
+ || sources.omitted !== dependencies.omitted) {
672
+ throw new RangeError(`Decision Fact ${fact.id} source and dependency counts did not reconcile.`);
673
+ }
674
+ return Object.freeze({
675
+ version: fact.version,
676
+ id: fact.id,
677
+ question: fact.question,
678
+ conclusion: fact.conclusion,
679
+ importance: fact.importance,
680
+ evidenceState: fact.evidenceState,
681
+ completeness: decisionFactCompleteness(fact.evidenceState),
682
+ freshness: fact.freshness,
683
+ consistency: fact.consistency,
684
+ dependencies,
685
+ sourceReferences: projectionCollection(fact.references, MAX_DECISION_FACT_PROJECTION_REFERENCES, (reference) => reference),
686
+ sources,
687
+ contradictions: projectionCollection(fact.contradictions, MAX_DECISION_FACT_PROJECTION_CONTRADICTIONS, (contradiction) => contradiction),
688
+ limitations: projectionCollection(fact.limitations, MAX_DECISION_FACT_PROJECTION_LIMITATIONS, (limitation) => limitation),
689
+ safeNextActions: projectionCollection(fact.nextActions, MAX_DECISION_FACT_PROJECTION_NEXT_ACTIONS, (action) => Object.freeze({ ...action })),
690
+ });
691
+ }
692
+ export function projectDecisionFacts(value) {
693
+ const canonical = canonicalDecisionFacts(value);
694
+ const total = canonical.length;
695
+ const contradictory = canonical.filter((fact) => fact.consistency === 'contradictory').length;
696
+ const unresolved = canonical.filter((fact) => fact.consistency === 'unknown').length;
697
+ const retained = [];
698
+ for (const fact of canonical) {
699
+ if (retained.length >= MAX_DECISION_FACT_PROJECTION_FACTS)
700
+ break;
701
+ const projected = factProjection(fact);
702
+ const candidateFacts = [...retained, projected];
703
+ const candidate = {
704
+ version: DECISION_FACT_PROJECTION_VERSION,
705
+ total,
706
+ displayed: candidateFacts.length,
707
+ omitted: total - candidateFacts.length,
708
+ contradictory,
709
+ unresolved,
710
+ facts: candidateFacts,
711
+ };
712
+ if (UTF8_ENCODER.encode(JSON.stringify(candidate)).byteLength <= MAX_DECISION_FACT_PROJECTION_BYTES) {
713
+ retained.push(projected);
714
+ }
715
+ }
716
+ const facts = Object.freeze(retained);
717
+ const displayed = retained.length;
718
+ const omitted = total - displayed;
719
+ if (total !== displayed + omitted) {
720
+ throw new RangeError('Decision Fact projection totals did not reconcile.');
721
+ }
722
+ const projection = Object.freeze({
723
+ version: DECISION_FACT_PROJECTION_VERSION,
724
+ total,
725
+ displayed,
726
+ omitted,
727
+ contradictory,
728
+ unresolved,
729
+ facts,
730
+ });
731
+ if (UTF8_ENCODER.encode(JSON.stringify(projection)).byteLength > MAX_DECISION_FACT_PROJECTION_BYTES) {
732
+ throw new RangeError('Decision Fact projection exceeded its byte limit.');
733
+ }
734
+ return projection;
735
+ }