@real-life/trust-protocol 0.3.2 → 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 (264) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +187 -78
  3. package/dist/ack.d.ts +84 -0
  4. package/dist/ack.d.ts.map +1 -0
  5. package/dist/ack.js +114 -0
  6. package/dist/ack.js.map +1 -0
  7. package/dist/adapters/vti-mediator.d.ts +154 -0
  8. package/dist/adapters/vti-mediator.d.ts.map +1 -0
  9. package/dist/adapters/vti-mediator.js +621 -0
  10. package/dist/adapters/vti-mediator.js.map +1 -0
  11. package/dist/anchor.d.ts +226 -0
  12. package/dist/anchor.d.ts.map +1 -0
  13. package/dist/anchor.js +347 -0
  14. package/dist/anchor.js.map +1 -0
  15. package/dist/{holder.d.ts → carrier-holder.d.ts} +1 -1
  16. package/dist/carrier-holder.d.ts.map +1 -0
  17. package/dist/{holder.js → carrier-holder.js} +5 -4
  18. package/dist/carrier-holder.js.map +1 -0
  19. package/dist/carrier-identity.js +4 -4
  20. package/dist/carrier-identity.js.map +1 -1
  21. package/dist/carrier.d.ts.map +1 -1
  22. package/dist/carrier.js +4 -3
  23. package/dist/carrier.js.map +1 -1
  24. package/dist/ceremony.d.ts +3 -2
  25. package/dist/ceremony.d.ts.map +1 -1
  26. package/dist/ceremony.js +16 -26
  27. package/dist/ceremony.js.map +1 -1
  28. package/dist/codec.d.ts +46 -0
  29. package/dist/codec.d.ts.map +1 -0
  30. package/dist/codec.js +410 -0
  31. package/dist/codec.js.map +1 -0
  32. package/dist/continuity.d.ts +67 -0
  33. package/dist/continuity.d.ts.map +1 -0
  34. package/dist/continuity.js +110 -0
  35. package/dist/continuity.js.map +1 -0
  36. package/dist/core.d.ts +4 -0
  37. package/dist/core.d.ts.map +1 -1
  38. package/dist/core.js.map +1 -1
  39. package/dist/crypto.d.ts +23 -7
  40. package/dist/crypto.d.ts.map +1 -1
  41. package/dist/crypto.js +54 -20
  42. package/dist/crypto.js.map +1 -1
  43. package/dist/delivery.d.ts +31 -3
  44. package/dist/delivery.d.ts.map +1 -1
  45. package/dist/delivery.js +75 -28
  46. package/dist/delivery.js.map +1 -1
  47. package/dist/encounter/ceremony.d.ts +37 -0
  48. package/dist/encounter/ceremony.d.ts.map +1 -0
  49. package/dist/encounter/ceremony.js +662 -0
  50. package/dist/encounter/ceremony.js.map +1 -0
  51. package/dist/encounter.d.ts +91 -1
  52. package/dist/encounter.d.ts.map +1 -1
  53. package/dist/encounter.js +191 -12
  54. package/dist/encounter.js.map +1 -1
  55. package/dist/engine.d.ts +291 -0
  56. package/dist/engine.d.ts.map +1 -0
  57. package/dist/engine.js +1395 -0
  58. package/dist/engine.js.map +1 -0
  59. package/dist/entropy.d.ts +28 -0
  60. package/dist/entropy.d.ts.map +1 -0
  61. package/dist/entropy.js +96 -0
  62. package/dist/entropy.js.map +1 -0
  63. package/dist/events.d.ts +177 -0
  64. package/dist/events.d.ts.map +1 -0
  65. package/dist/events.js +2 -0
  66. package/dist/events.js.map +1 -0
  67. package/dist/fixtures.d.ts +30 -0
  68. package/dist/fixtures.d.ts.map +1 -0
  69. package/dist/fixtures.js +155 -0
  70. package/dist/fixtures.js.map +1 -0
  71. package/dist/forge.d.ts +47 -0
  72. package/dist/forge.d.ts.map +1 -0
  73. package/dist/forge.js +20 -0
  74. package/dist/forge.js.map +1 -0
  75. package/dist/group-star.d.ts +251 -0
  76. package/dist/group-star.d.ts.map +1 -0
  77. package/dist/group-star.js +348 -0
  78. package/dist/group-star.js.map +1 -0
  79. package/dist/holder-api.d.ts +137 -0
  80. package/dist/holder-api.d.ts.map +1 -0
  81. package/dist/holder-api.js +132 -0
  82. package/dist/holder-api.js.map +1 -0
  83. package/dist/holder-state-v1.d.ts +801 -0
  84. package/dist/holder-state-v1.d.ts.map +1 -0
  85. package/dist/holder-state-v1.js +2 -0
  86. package/dist/holder-state-v1.js.map +1 -0
  87. package/dist/identity-state.d.ts +43 -0
  88. package/dist/identity-state.d.ts.map +1 -0
  89. package/dist/identity-state.js +345 -0
  90. package/dist/identity-state.js.map +1 -0
  91. package/dist/identity.d.ts +24 -2
  92. package/dist/identity.d.ts.map +1 -1
  93. package/dist/identity.js +70 -11
  94. package/dist/identity.js.map +1 -1
  95. package/dist/index.d.ts +26 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +77 -29
  98. package/dist/index.js.map +1 -1
  99. package/dist/internal.d.ts +36 -0
  100. package/dist/internal.d.ts.map +1 -0
  101. package/dist/internal.js +22 -0
  102. package/dist/internal.js.map +1 -0
  103. package/dist/introduction/holder.d.ts +18 -0
  104. package/dist/introduction/holder.d.ts.map +1 -0
  105. package/dist/introduction/holder.js +757 -0
  106. package/dist/introduction/holder.js.map +1 -0
  107. package/dist/introduction.d.ts +101 -0
  108. package/dist/introduction.d.ts.map +1 -0
  109. package/dist/introduction.js +132 -0
  110. package/dist/introduction.js.map +1 -0
  111. package/dist/invariants.d.ts +43 -0
  112. package/dist/invariants.d.ts.map +1 -0
  113. package/dist/invariants.js +883 -0
  114. package/dist/invariants.js.map +1 -0
  115. package/dist/keys-seed.d.ts +7 -0
  116. package/dist/keys-seed.d.ts.map +1 -0
  117. package/dist/keys-seed.js +133 -0
  118. package/dist/keys-seed.js.map +1 -0
  119. package/dist/log-memory.d.ts +12 -0
  120. package/dist/log-memory.d.ts.map +1 -0
  121. package/dist/log-memory.js +101 -0
  122. package/dist/log-memory.js.map +1 -0
  123. package/dist/membership/holder.d.ts +35 -0
  124. package/dist/membership/holder.d.ts.map +1 -0
  125. package/dist/membership/holder.js +852 -0
  126. package/dist/membership/holder.js.map +1 -0
  127. package/dist/membership.d.ts +92 -0
  128. package/dist/membership.d.ts.map +1 -0
  129. package/dist/membership.js +157 -0
  130. package/dist/membership.js.map +1 -0
  131. package/dist/ports.d.ts +237 -0
  132. package/dist/ports.d.ts.map +1 -0
  133. package/dist/ports.js +2 -0
  134. package/dist/ports.js.map +1 -0
  135. package/dist/profile.d.ts +56 -0
  136. package/dist/profile.d.ts.map +1 -0
  137. package/dist/profile.js +193 -0
  138. package/dist/profile.js.map +1 -0
  139. package/dist/prompts.d.ts +14 -0
  140. package/dist/prompts.d.ts.map +1 -0
  141. package/dist/prompts.js +69 -0
  142. package/dist/prompts.js.map +1 -0
  143. package/dist/receive.d.ts +133 -0
  144. package/dist/receive.d.ts.map +1 -0
  145. package/dist/receive.js +298 -0
  146. package/dist/receive.js.map +1 -0
  147. package/dist/results.d.ts +158 -0
  148. package/dist/results.d.ts.map +1 -0
  149. package/dist/results.js +21 -0
  150. package/dist/results.js.map +1 -0
  151. package/dist/schemas.d.ts.map +1 -1
  152. package/dist/schemas.js +42 -38
  153. package/dist/schemas.js.map +1 -1
  154. package/dist/sha256.d.ts +3 -0
  155. package/dist/sha256.d.ts.map +1 -0
  156. package/dist/sha256.js +69 -0
  157. package/dist/sha256.js.map +1 -0
  158. package/dist/star.d.ts +31 -0
  159. package/dist/star.d.ts.map +1 -0
  160. package/dist/star.js +56 -0
  161. package/dist/star.js.map +1 -0
  162. package/dist/state-builder.d.ts +42 -0
  163. package/dist/state-builder.d.ts.map +1 -0
  164. package/dist/state-builder.js +100 -0
  165. package/dist/state-builder.js.map +1 -0
  166. package/dist/state.d.ts +853 -0
  167. package/dist/state.d.ts.map +1 -0
  168. package/dist/state.js +36 -0
  169. package/dist/state.js.map +1 -0
  170. package/dist/store-contract.d.ts +11 -0
  171. package/dist/store-contract.d.ts.map +1 -0
  172. package/dist/store-contract.js +189 -0
  173. package/dist/store-contract.js.map +1 -0
  174. package/dist/store-memory.d.ts +8 -0
  175. package/dist/store-memory.d.ts.map +1 -0
  176. package/dist/store-memory.js +17 -0
  177. package/dist/store-memory.js.map +1 -0
  178. package/dist/testing.d.ts +22 -0
  179. package/dist/testing.d.ts.map +1 -0
  180. package/dist/testing.js +21 -0
  181. package/dist/testing.js.map +1 -0
  182. package/dist/trust/continuity.d.ts +71 -0
  183. package/dist/trust/continuity.d.ts.map +1 -0
  184. package/dist/trust/continuity.js +1216 -0
  185. package/dist/trust/continuity.js.map +1 -0
  186. package/dist/trust/duties.d.ts +67 -0
  187. package/dist/trust/duties.d.ts.map +1 -0
  188. package/dist/trust/duties.js +179 -0
  189. package/dist/trust/duties.js.map +1 -0
  190. package/dist/trust/entries.d.ts +139 -0
  191. package/dist/trust/entries.d.ts.map +1 -0
  192. package/dist/trust/entries.js +694 -0
  193. package/dist/trust/entries.js.map +1 -0
  194. package/dist/trust/group-star.d.ts +43 -0
  195. package/dist/trust/group-star.d.ts.map +1 -0
  196. package/dist/trust/group-star.js +188 -0
  197. package/dist/trust/group-star.js.map +1 -0
  198. package/dist/trust/mapping.d.ts +56 -0
  199. package/dist/trust/mapping.d.ts.map +1 -0
  200. package/dist/trust/mapping.js +584 -0
  201. package/dist/trust/mapping.js.map +1 -0
  202. package/dist/trust/profile.d.ts +25 -0
  203. package/dist/trust/profile.d.ts.map +1 -0
  204. package/dist/trust/profile.js +249 -0
  205. package/dist/trust/profile.js.map +1 -0
  206. package/dist/trust/star.d.ts +208 -0
  207. package/dist/trust/star.d.ts.map +1 -0
  208. package/dist/trust/star.js +588 -0
  209. package/dist/trust/star.js.map +1 -0
  210. package/dist/trust/state.d.ts +83 -0
  211. package/dist/trust/state.d.ts.map +1 -0
  212. package/dist/trust/state.js +154 -0
  213. package/dist/trust/state.js.map +1 -0
  214. package/dist/util-freeze.d.ts +3 -0
  215. package/dist/util-freeze.d.ts.map +1 -0
  216. package/dist/util-freeze.js +11 -0
  217. package/dist/util-freeze.js.map +1 -0
  218. package/dist/views.d.ts +147 -0
  219. package/dist/views.d.ts.map +1 -0
  220. package/dist/views.js +268 -0
  221. package/dist/views.js.map +1 -0
  222. package/dist/visibility.d.ts +5 -5
  223. package/dist/visibility.d.ts.map +1 -1
  224. package/dist/visibility.js +22 -28
  225. package/dist/visibility.js.map +1 -1
  226. package/dist/wire.d.ts +178 -84
  227. package/dist/wire.d.ts.map +1 -1
  228. package/dist/wire.js.map +1 -1
  229. package/package.json +56 -10
  230. package/schemas/holder-state-v1.schema.json +3880 -0
  231. package/dist/holder.d.ts.map +0 -1
  232. package/dist/holder.js.map +0 -1
  233. package/dist/probe/acks.d.ts +0 -27
  234. package/dist/probe/acks.d.ts.map +0 -1
  235. package/dist/probe/acks.js +0 -152
  236. package/dist/probe/acks.js.map +0 -1
  237. package/dist/probe/continuity.d.ts +0 -20
  238. package/dist/probe/continuity.d.ts.map +0 -1
  239. package/dist/probe/continuity.js +0 -805
  240. package/dist/probe/continuity.js.map +0 -1
  241. package/dist/probe/deps.d.ts +0 -61
  242. package/dist/probe/deps.d.ts.map +0 -1
  243. package/dist/probe/deps.js +0 -54
  244. package/dist/probe/deps.js.map +0 -1
  245. package/dist/probe/encounter.d.ts +0 -26
  246. package/dist/probe/encounter.d.ts.map +0 -1
  247. package/dist/probe/encounter.js +0 -938
  248. package/dist/probe/encounter.js.map +0 -1
  249. package/dist/probe/introduce.d.ts +0 -62
  250. package/dist/probe/introduce.d.ts.map +0 -1
  251. package/dist/probe/introduce.js +0 -306
  252. package/dist/probe/introduce.js.map +0 -1
  253. package/dist/probe/membership.d.ts +0 -96
  254. package/dist/probe/membership.d.ts.map +0 -1
  255. package/dist/probe/membership.js +0 -467
  256. package/dist/probe/membership.js.map +0 -1
  257. package/dist/probe/trust.d.ts +0 -156
  258. package/dist/probe/trust.d.ts.map +0 -1
  259. package/dist/probe/trust.js +0 -1233
  260. package/dist/probe/trust.js.map +0 -1
  261. package/dist/probe.d.ts +0 -7
  262. package/dist/probe.d.ts.map +0 -1
  263. package/dist/probe.js +0 -42
  264. package/dist/probe.js.map +0 -1
@@ -0,0 +1,694 @@
1
+ import { CLASSES, anchorSet, classOnToward, conflicts, entryOf, entryRelationships, feeding, findContact, joins, lags, liveRel, newestHeads, newestOf, supersededIn, } from './state.js';
2
+ import { viewDigest } from './star.js';
3
+ import { dropOff, followClasses } from './duties.js';
4
+ import { isKnownDuty } from '../codec.js';
5
+ import { jcs } from '../core.js';
6
+ const jcsEq = (a, b) => jcs(a) === jcs(b);
7
+ const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
8
+ const cmpDec = (a, b) => { const x = BigInt(a), y = BigInt(b); return x < y ? -1 : x > y ? 1 : 0; };
9
+ const byPos = (a, b) => cmpDec(a.pos, b.pos) || cmp(a.id, b.id);
10
+ const unique = (xs) => [...new Set(xs)];
11
+ /** The trust module's entry commands. */
12
+ export const ENTRY_COMMANDS = Object.freeze({ merge: 'trust.merge', depart: 'trust.depart' });
13
+ // ── the anchor head and admission ──────────────────────────────────────
14
+ /**
15
+ * The entry's anchor head from what its relationships hold (6560): the
16
+ * newest of the heads its feeding relationships hold; among several
17
+ * (a manual merge before its split) the one it holds, else `prefer`, else
18
+ * the smallest. Where a relationship of it feeds another entry (2145), the
19
+ * head only moves forward: with no feeding head, or none beyond it, it
20
+ * stays. With neither, the entry has none.
21
+ */
22
+ export function settleHead(state, c, prefer = null) {
23
+ const rels = feeding(state, c);
24
+ const all = entryRelationships(state, c);
25
+ const newest = newestHeads(rels);
26
+ if (!newest.length) {
27
+ if (all.length > rels.length)
28
+ return c;
29
+ return c.anchorHead === null ? c : { ...c, anchorHead: null };
30
+ }
31
+ // 2145: a chain of this entry feeds the other one; what the rest feeds
32
+ // never moves the head back — it stays unless a feeding head lies
33
+ // beyond it along the links the entry's relationships hold
34
+ if (all.length > rels.length && c.anchorHead !== null && newestOf([...newest, c.anchorHead], all.flatMap((r) => r.links)).includes(c.anchorHead))
35
+ return c;
36
+ const head = newest.length === 1 ? newest[0]
37
+ : c.anchorHead !== null && newest.includes(c.anchorHead) ? c.anchorHead
38
+ : prefer !== null && newest.includes(prefer) ? prefer
39
+ : [...newest].sort(cmp)[0];
40
+ return head === c.anchorHead ? c : { ...c, anchorHead: head };
41
+ }
42
+ /** 2250: pending entries join the set in position order while it has room. */
43
+ export function admitPending(state, bound) {
44
+ let admitted = BigInt(Object.values(state.contacts).filter((x) => x.status === 'admitted').length);
45
+ const pending = Object.values(state.contacts).filter((x) => x.status === 'pending' && x.pos !== null).sort(byPos);
46
+ let contacts = null;
47
+ for (const c of pending) {
48
+ if (admitted >= bound)
49
+ break;
50
+ contacts ??= { ...state.contacts };
51
+ contacts[c.id] = { ...c, status: 'admitted' };
52
+ admitted += 1n;
53
+ }
54
+ return contacts ? { ...state, contacts } : state;
55
+ }
56
+ /** Rename members of one star scope's views, re-sorted and re-digested. */
57
+ function renameView(view, from, to) {
58
+ if (!view.some((p) => from(p.member)))
59
+ return null;
60
+ return view.map((p) => (from(p.member) ? { ...p, member: to } : p)).sort((a, b) => cmp(a.member, b.member));
61
+ }
62
+ function renameBaselines(t, from, to) {
63
+ const o = t.starOut;
64
+ let changed = false;
65
+ let snapshot = o.snapshot;
66
+ let snapshotDigest = o.snapshotDigest;
67
+ if (snapshot) {
68
+ const v = renameView(snapshot, from, to);
69
+ if (v) {
70
+ snapshot = v;
71
+ snapshotDigest = viewDigest(v);
72
+ changed = true;
73
+ }
74
+ }
75
+ const inflight = {};
76
+ for (const [salt, f] of Object.entries(o.inflight)) {
77
+ const v = renameView(f.view, from, to);
78
+ if (v) {
79
+ inflight[salt] = { ...f, view: v, viewDigest: viewDigest(v) };
80
+ changed = true;
81
+ }
82
+ else
83
+ inflight[salt] = f;
84
+ }
85
+ return changed ? { ...t, starOut: { ...o, snapshot, snapshotDigest, inflight } } : t;
86
+ }
87
+ /** The tuple the record a prompt asks about was made on: a group-invite's
88
+ * invite by its own rkid, an introduction's offer or the target's act by
89
+ * its pin toward the mediator; null where none is held (an
90
+ * encounter-confirm names the fresh relationship's own identity). */
91
+ function promptTuple(state, p) {
92
+ if (p.kind === 'group-invite') {
93
+ const i = Object.values(state.joining.invites).find((x) => x.promptId === p.id);
94
+ return i ? { rkid: i.rkid } : null;
95
+ }
96
+ if (p.kind === 'introduction') {
97
+ if (Object.hasOwn(state.encounter.offers, p.id))
98
+ return { label: state.encounter.offers[p.id].pin };
99
+ const a = Object.values(state.encounter.introductions).find((x) => x.role === 'target' && x.promptId === p.id);
100
+ return a?.pins.mediator !== undefined ? { label: a.pins.mediator } : null;
101
+ }
102
+ return null;
103
+ }
104
+ /**
105
+ * Every holder-local key that resolved through an identity in `ids` now
106
+ * resolves through `to` (2100, 2140), within `opts`: tuples' entryId,
107
+ * duties, the recipients of issued documents, and with `global` the
108
+ * matches of accepted stars and merge duties; and the contact references
109
+ * of the membership and introduction state and of prompts, each with the
110
+ * relationship of the tuple it was made on (2080): by the own rkid it
111
+ * arrived or went on (joining preludes and invites, groups' preludes,
112
+ * invitations and roster) or the own pair label toward the mediator
113
+ * (encounter.offers by their pin, the introducer provenance.via by
114
+ * viaPin, memory.vouchedBy by memory.vouchedPin), a prompt with the
115
+ * record it asks about; one that names no tuple follows with `global`.
116
+ * Contacts are the caller's: their ids
117
+ * and aliases are what the rewrite follows. The stored acks follow
118
+ * through the engine (opts.renames): only the core writes the effect
119
+ * cache, and their sealed envelopes stay as they are. Parts without a
120
+ * change keep their identity.
121
+ */
122
+ export function rewriteIdentity(state, ids, to, opts = {}) {
123
+ const from = (x) => ids.includes(x);
124
+ const inRel = opts.rels ?? (() => true);
125
+ // the own rkids and pair labels of the tuples within `opts`
126
+ const rkids = new Set(), labels = new Set();
127
+ for (const [id, r] of Object.entries(state.relationships)) {
128
+ if (inRel(id))
129
+ for (const t of r.tuples) {
130
+ rkids.add(t.rkid);
131
+ labels.add(t.ownPairLabel);
132
+ }
133
+ }
134
+ const byRkid = (rkid) => (rkid === undefined ? !!opts.global : !opts.rels || rkids.has(rkid));
135
+ const byLabel = (label) => (typeof label !== 'string' ? !!opts.global : !opts.rels || labels.has(label));
136
+ const re = (x, tied) => x !== undefined && from(x) && tied;
137
+ let relationships = null;
138
+ for (const [id, r] of Object.entries(state.relationships)) {
139
+ const mine = inRel(id);
140
+ let changed = false;
141
+ const tuples = r.tuples.map((t) => {
142
+ let x = t;
143
+ if (mine && from(t.entryId))
144
+ x = { ...x, entryId: to };
145
+ const acc = x.starIn.accepted;
146
+ if (opts.global && acc && acc.matches.some(from)) {
147
+ x = { ...x, starIn: { ...x.starIn, accepted: { ...acc, matches: unique(acc.matches.map((m) => (from(m) ? to : m))).sort(cmp) } } };
148
+ }
149
+ if (opts.baselines)
150
+ x = renameBaselines(x, from, to);
151
+ if (x !== t)
152
+ changed = true;
153
+ return x;
154
+ });
155
+ // the introducer with the relationship toward it the act was pinned to
156
+ const provenance = re(r.provenance.via, byLabel(r.provenance.viaPin)) ? { ...r.provenance, via: to } : r.provenance;
157
+ if (changed || provenance !== r.provenance)
158
+ (relationships ??= { ...state.relationships })[id] = { ...r, tuples, provenance };
159
+ }
160
+ opts.renames?.push({ from: [...ids], to, rkids: opts.rels ? [...rkids].sort() : null });
161
+ let duties = null;
162
+ for (const [id, d] of Object.entries(state.duties)) {
163
+ if (!isKnownDuty(d))
164
+ continue; // a kind or version this library does not handle stays as it is
165
+ const x = d;
166
+ let nd = d;
167
+ if (d.kind === 'merge' && opts.global && Array.isArray(x.entryIds) && x.entryIds.some(from)) {
168
+ nd = { ...d, entryIds: unique(x.entryIds.map((e) => (from(e) ? to : e))).sort(cmp) };
169
+ }
170
+ else if (d.kind !== 'merge' && typeof x.contactId === 'string' && from(x.contactId) && (typeof x.relId !== 'string' || inRel(x.relId))) {
171
+ nd = { ...d, contactId: to };
172
+ }
173
+ if (nd !== d)
174
+ (duties ??= { ...state.duties })[id] = nd;
175
+ }
176
+ // membership, introduction and prompts: each contact reference with the
177
+ // relationship of the tuple it was made on, where it names one (2080)
178
+ let prompts = null;
179
+ for (const [id, p] of Object.entries(state.prompts)) {
180
+ const at = promptTuple(state, p);
181
+ const tied = at === null ? !!opts.global : 'rkid' in at ? byRkid(at.rkid) : byLabel(at.label);
182
+ if (re(p.contactId, tied))
183
+ (prompts ??= { ...state.prompts })[id] = { ...p, contactId: to };
184
+ }
185
+ let joining = null;
186
+ for (const [k, p] of Object.entries(state.joining.preludes)) {
187
+ if (re(p.contactId, byRkid(p.rkid)))
188
+ (joining ??= { ...state.joining, preludes: { ...state.joining.preludes } }).preludes[k] = { ...p, contactId: to };
189
+ }
190
+ for (const [k, i] of Object.entries(state.joining.invites)) {
191
+ if (re(i.contactId, byRkid(i.rkid))) {
192
+ joining ??= { ...state.joining, preludes: { ...state.joining.preludes } };
193
+ joining.invites = { ...joining.invites, [k]: { ...i, contactId: to } };
194
+ }
195
+ }
196
+ let groups = null;
197
+ for (const [gid, g] of Object.entries(state.groups)) {
198
+ const preludes = Object.fromEntries(Object.entries(g.preludes).map(([k, p]) => [k, re(p.contactId, byRkid(p.rkid)) ? { ...p, contactId: to } : p]));
199
+ const invitations = Object.fromEntries(Object.entries(g.invitations).map(([k, i]) => [k, re(i.contactId, byRkid(i.rkid)) ? { ...i, contactId: to } : i]));
200
+ const roster = Object.fromEntries(Object.entries(g.roster).map(([k, m]) => [k, re(m.contactId, byRkid(m.rkid)) ? { ...m, contactId: to } : m]));
201
+ if (jcsEq(preludes, g.preludes) && jcsEq(invitations, g.invitations) && jcsEq(roster, g.roster))
202
+ continue;
203
+ (groups ??= { ...state.groups })[gid] = { ...g, preludes, invitations, roster };
204
+ }
205
+ let encounter = null;
206
+ for (const [k, o] of Object.entries(state.encounter.offers)) {
207
+ if (re(o.mediatorContactId, byLabel(o.pin)))
208
+ (encounter ??= { ...state.encounter, offers: { ...state.encounter.offers } }).offers[k] = { ...o, mediatorContactId: to };
209
+ }
210
+ // the voucher mark names the mediator that vouched: it follows the
211
+ // relationship toward that mediator the voucher came on (memory.vouchedPin),
212
+ // not the entry it was filed under
213
+ for (const [id, r] of Object.entries(relationships ?? state.relationships)) {
214
+ const v = r.memory.vouchedBy;
215
+ if (typeof v === 'string' && re(v, byLabel(r.memory.vouchedPin)))
216
+ (relationships ??= { ...state.relationships })[id] = { ...r, memory: { ...r.memory, vouchedBy: to } };
217
+ }
218
+ let outbox = null;
219
+ for (const [d, it] of Object.entries(state.outbox)) {
220
+ // a document on a member key follows the tuple its contact was named through (Recipient.via)
221
+ if (from(it.recipient.contactId) && rkids.has(it.recipient.via ?? it.recipient.rkid))
222
+ (outbox ??= { ...state.outbox })[d] = { ...it, recipient: { ...it.recipient, contactId: to } };
223
+ }
224
+ if (!relationships && !duties && !prompts && !outbox && !joining && !groups && !encounter)
225
+ return state;
226
+ return {
227
+ ...state,
228
+ ...(relationships ? { relationships } : {}), ...(duties ? { duties } : {}), ...(prompts ? { prompts } : {}), ...(outbox ? { outbox } : {}),
229
+ ...(joining ? { joining } : {}), ...(groups ? { groups } : {}), ...(encounter ? { encounter } : {}),
230
+ };
231
+ }
232
+ // ── merge (2140–2180) ──────────────────────────────────────────────────
233
+ /** 2160: per class `off` where any held `off`, else `on` where any held
234
+ * `on`, else `default`; per (group, contact) the same, every per-group
235
+ * `on` reset where the merged sealed group star is `off` (5450). */
236
+ export function mergeSettings(cs) {
237
+ const pick = (xs) => (xs.includes('off') ? 'off' : xs.includes('on') ? 'on' : 'default');
238
+ const settings = {};
239
+ for (const cls of CLASSES)
240
+ settings[cls] = pick(cs.map((c) => c.settings[cls]));
241
+ const groupSettings = {};
242
+ for (const g of unique(cs.flatMap((c) => Object.keys(c.groupSettings))).sort(cmp)) {
243
+ const s = pick(cs.map((c) => c.groupSettings[g] ?? 'default'));
244
+ if (s === 'off' || (s === 'on' && settings.sealedGroupStar !== 'off'))
245
+ groupSettings[g] = s;
246
+ }
247
+ return { settings, groupSettings };
248
+ }
249
+ /**
250
+ * Whether entries can be one (6605, 6560): no relationship of one holds
251
+ * another binding (or another successor) for an anchor a relationship of
252
+ * another holds, and their anchor heads lie on one chain of the links
253
+ * their relationships hold, so the merged entry has exactly one.
254
+ */
255
+ export function mergeable(state, cs) {
256
+ if (cs.length < 2 || new Set(cs.map((c) => c.id)).size !== cs.length)
257
+ return false;
258
+ const rels = cs.map((c) => entryRelationships(state, c));
259
+ for (let i = 0; i < rels.length; i++)
260
+ for (let j = i + 1; j < rels.length; j++)
261
+ if (conflicts(rels[i], rels[j]))
262
+ return false;
263
+ const all = unique(rels.flat());
264
+ const heads = [...cs.map((c) => c.anchorHead), ...all.map((r) => r.heldCounterpartHead)].filter((h) => h !== null);
265
+ return newestOf(heads, all.flatMap((r) => r.links)).length <= 1;
266
+ }
267
+ /** Where the newest active head among the entries' relationships was created (2180). */
268
+ function newestRelationship(state, cs) {
269
+ let best;
270
+ let at = -1;
271
+ for (const c of cs) {
272
+ for (const r of entryRelationships(state, c)) {
273
+ const i = state.entryOrder.heads.indexOf(r.id);
274
+ if (i > at) {
275
+ at = i;
276
+ best = r;
277
+ }
278
+ }
279
+ }
280
+ return best;
281
+ }
282
+ /**
283
+ * The atomic merge of entries (2140, 2150, 2160, 2180): one entry at the
284
+ * earliest position under the survivor's identity, the others its
285
+ * aliases; admitted where any was, so `k` admitted ones free `k−1` slots,
286
+ * which the next pending entries take in position order in the same step;
287
+ * every key of a merged id rewritten to the survivor; the anchor head
288
+ * settled (6560). Per-tuple keys stay with their tuples.
289
+ */
290
+ export function mergeEntries(state, cs, bound) {
291
+ const withPos = cs.filter((c) => c.pos !== null).sort(byPos);
292
+ let survivor;
293
+ let id;
294
+ if (withPos.length) {
295
+ survivor = withPos[0];
296
+ id = survivor.id;
297
+ }
298
+ else {
299
+ const r = newestRelationship(state, cs);
300
+ survivor = (r && cs.find((c) => c.relIds.includes(r.id))) ?? cs[0];
301
+ // the relationship's identity, where it names the survivor (2180)
302
+ id = r && findContact(state, r.contactId)?.id === survivor.id ? r.contactId : survivor.id;
303
+ }
304
+ return mergeInto(state, survivor, id, cs.filter((c) => c !== survivor), bound);
305
+ }
306
+ /** The merge onto a chosen survivor under identity `id` (its id or one of its aliases). */
307
+ export function mergeInto(state, survivor, id, gone, bound) {
308
+ const all = [survivor, ...gone];
309
+ const { settings, groupSettings } = mergeSettings(all);
310
+ const status = all.some((c) => c.status === 'admitted') ? 'admitted' : all.some((c) => c.status === 'pending') ? 'pending' : 'none';
311
+ const positioned = all.filter((c) => c.pos !== null).sort(byPos);
312
+ const merged = all.map((c) => c.id).filter((x) => x !== id);
313
+ const contacts = { ...state.contacts };
314
+ for (const c of all)
315
+ delete contacts[c.id];
316
+ contacts[id] = {
317
+ id, pos: positioned[0]?.pos ?? null, status,
318
+ aliases: unique(all.flatMap((c) => [c.id, ...c.aliases])).filter((x) => x !== id).sort(cmp),
319
+ relIds: unique(all.flatMap((c) => c.relIds)),
320
+ anchorHead: survivor.anchorHead ?? positioned.find((c) => c.anchorHead !== null)?.anchorHead ?? null,
321
+ settings, groupSettings,
322
+ };
323
+ const renames = [];
324
+ let next = rewriteIdentity({ ...state, contacts }, merged, id, { global: true, renames });
325
+ next = { ...next, contacts: { ...next.contacts, [id]: settleHead(next, next.contacts[id]) } };
326
+ // the class duties of the merged settings (2160, 2850, 6410): per
327
+ // relationship against the entry it sent under before (2146)
328
+ const f = followClasses(state, admitPending(next, bound));
329
+ next = f.state;
330
+ return {
331
+ state: next, events: [{ type: 'contact.merged', contactId: id, merged: [...merged].sort(cmp) }],
332
+ ...(f.duties.length ? { duties: f.duties } : {}), renames, survivor: id,
333
+ };
334
+ }
335
+ /**
336
+ * The merge duties a commit on `relId` opens (2230): where the
337
+ * relationship stands behind another entry (2145) that is now mergeable
338
+ * with its own, and, with `net`, the convergence net of rung 2 for an
339
+ * accepted anchor mapping (6520–6605). One duty for every entry that
340
+ * joins, in position order while they stay mergeable together (2150).
341
+ */
342
+ export function mergeDutiesAfter(before, after, relId, opts) {
343
+ const none = { state: after, duties: [] };
344
+ const rel = after.relationships[relId];
345
+ const E = rel && entryOf(after, rel);
346
+ if (!E || E.pos === null || E.anchorHead === null)
347
+ return none;
348
+ const eRels = entryRelationships(after, E);
349
+ const others = Object.values(after.contacts).filter((x) => x.id !== E.id && x.pos !== null && x.anchorHead !== null).sort(byPos);
350
+ const picked = [];
351
+ const take = (x) => { if (!picked.includes(x) && mergeable(after, [E, ...picked, x]))
352
+ picked.push(x); };
353
+ // 2145: the other entry behind this relationship, once comparable and the binding rule admits
354
+ for (const t of rel.tuples) {
355
+ const x = findContact(after, t.entryId);
356
+ if (x && x.id !== E.id && x.pos !== null && x.anchorHead !== null)
357
+ take(x);
358
+ }
359
+ if (opts.net) {
360
+ // the incoming relationship, where it does not lag in its own entry
361
+ // (6550). Head rule (b) against the knowledge the entry held before
362
+ // this commit: its relationships' links with this relationship's taken
363
+ // from before the 6610 union of this verification. What the mapping
364
+ // newly carries counts for (a) only (the 6590 rationale reading)
365
+ if (rel.heldCounterpartHead === E.anchorHead) {
366
+ const relB = Object.hasOwn(before.relationships, relId) ? before.relationships[relId] : rel;
367
+ const own = supersededIn(eRels.map((r) => (r.id === relId ? relB : r)));
368
+ for (const x of others)
369
+ if (joins(rel, x, entryRelationships(after, x), own))
370
+ take(x);
371
+ }
372
+ // 6580: E's anchor head changed — every relationship of another entry against E
373
+ if (findContact(before, E.id)?.anchorHead !== E.anchorHead) {
374
+ for (const x of others) {
375
+ const xs = entryRelationships(after, x);
376
+ if (feeding(after, x).some((r) => r.heldCounterpartHead === x.anchorHead && joins(r, E, eRels, supersededIn(xs))))
377
+ take(x);
378
+ }
379
+ }
380
+ }
381
+ if (!picked.length)
382
+ return none;
383
+ const cs = [E, ...picked];
384
+ const ids = cs.map((c) => c.id).sort(cmp);
385
+ const id = 'merge:' + ids.join('|');
386
+ const duty = { kind: 'merge', v: 1, id, entryIds: ids, relId, witness: witnessOf(after, cs) };
387
+ // a duty open for the same entries takes the new admission's witness
388
+ if (Object.hasOwn(after.duties, id))
389
+ return { state: { ...after, duties: { ...after.duties, [id]: { ...after.duties[id], ...duty } } }, duties: [] };
390
+ return { state: after, duties: [duty] };
391
+ }
392
+ /** The heads and superseded sets a merge is admitted against. */
393
+ function witnessOf(state, cs) {
394
+ const heads = {};
395
+ const superseded = {};
396
+ for (const c of cs)
397
+ heads[c.id] = c.anchorHead;
398
+ for (const y of cs) {
399
+ const sup = supersededIn(entryRelationships(state, y));
400
+ superseded[y.id] = unique(cs.filter((x) => x !== y && sup.has(x.anchorHead)).map((x) => x.anchorHead)).sort(cmp);
401
+ }
402
+ return { heads, superseded };
403
+ }
404
+ /**
405
+ * Whether the merge a duty was opened for still stands (6540, 6550,
406
+ * 6605): every entry still holds the anchor head it was admitted with, no
407
+ * entry holds another's head as superseded beyond what it did then, the
408
+ * relationship that opened it is still one of theirs, and the entries are
409
+ * mergeable (the binding rule, one anchor head).
410
+ */
411
+ function witnessHolds(state, d, cs) {
412
+ const w = d.witness;
413
+ if (cs.some((c) => !Object.hasOwn(w.heads, c.id) || c.anchorHead !== w.heads[c.id]))
414
+ return false;
415
+ if (!cs.some((c) => c.relIds.includes(d.relId)))
416
+ return false;
417
+ for (const y of cs) {
418
+ const sup = supersededIn(entryRelationships(state, y));
419
+ for (const x of cs)
420
+ if (x !== y && sup.has(x.anchorHead) && !(w.superseded[y.id] ?? []).includes(x.anchorHead))
421
+ return false;
422
+ }
423
+ return mergeable(state, cs);
424
+ }
425
+ /** merge/1: the merge as one step after the verification commit (2230),
426
+ * re-checked against its witness; where it no longer holds, the duty is
427
+ * dropped without a trace (a later verification may open another). */
428
+ export const mergeDuty = (bound) => ({
429
+ v: 1,
430
+ apply(state, duty) {
431
+ const d = duty;
432
+ const cs = unique(d.entryIds.map((x) => findContact(state, x)).filter((c) => c !== undefined));
433
+ // the state moved since the commit that opened it: nothing (left) to merge
434
+ if (cs.length < 2 || cs.some((c) => c.pos === null) || !witnessHolds(state, d, cs))
435
+ return { state: state };
436
+ const r = mergeEntries(state, cs, bound);
437
+ return { state: r.state, ...(r.events ? { events: r.events } : {}), ...(r.duties ? { duties: r.duties } : {}), ...(r.renames ? { renames: r.renames } : {}) };
438
+ },
439
+ });
440
+ // ── split (2080–2115) ──────────────────────────────────────────────────
441
+ /** The chain aliases of a relationship (2090): the identities of the
442
+ * husks chained into it. */
443
+ export function chainAliases(state, relId) {
444
+ const out = [];
445
+ for (const r of Object.values(state.relationships)) {
446
+ if (r.chainedInto === relId)
447
+ out.push(r.contactId, ...chainAliases(state, r.id));
448
+ }
449
+ return unique(out).sort(cmp);
450
+ }
451
+ /** The husks chained into `relId`, transitively. */
452
+ const husksOf = (state, relId) => Object.values(state.relationships).filter((r) => r.chainedInto === relId).flatMap((r) => [r.id, ...husksOf(state, r.id)]);
453
+ /**
454
+ * The identity an entry takes when the relationship whose identity it was
455
+ * leaves (2100): its oldest remaining relationship's in entry-commit order
456
+ * (firstVerified, unverified after, then active-head creation), among those
457
+ * whose identity resolves to this entry; the relationship's id where none
458
+ * does (a relationship behind two entries, 2145).
459
+ */
460
+ function oldestIdentity(state, c, remaining, exclude) {
461
+ const heads = state.entryOrder.heads;
462
+ const rels = remaining.filter((id) => Object.hasOwn(state.relationships, id)).map((id) => state.relationships[id]).sort((a, b) => {
463
+ if (a.firstVerified !== null && b.firstVerified !== null) {
464
+ const d = cmpDec(a.firstVerified, b.firstVerified);
465
+ if (d)
466
+ return d;
467
+ }
468
+ if ((a.firstVerified === null) !== (b.firstVerified === null))
469
+ return a.firstVerified === null ? 1 : -1;
470
+ return (heads.indexOf(a.id) - heads.indexOf(b.id)) || cmp(a.id, b.id);
471
+ });
472
+ const own = rels.find((r) => !exclude.has(r.contactId) && (r.contactId === c.id || c.aliases.includes(r.contactId)));
473
+ return own ? own.contactId : rels[0].id;
474
+ }
475
+ /** The keys of the entry's relationships resolve through its new id (2100). */
476
+ const rename = (state, oldId, id, rels, renames) => rewriteIdentity(state, [oldId], id, { rels, global: true, baselines: true, renames });
477
+ /**
478
+ * 2080: the first verified mapping on `relId`, already in `mid`, leaves it
479
+ * holding no anchor the other relationships of its entry hold, or another
480
+ * binding for one they hold: the split, else null. The entry keeps its
481
+ * position, status, the remaining relationships with their aliases and
482
+ * its settings, frees no slot and settles its anchor head; the leaver
483
+ * takes its identity, its chain aliases and the settings of 2115, and
484
+ * holds no position until the caller's entry commit (2090).
485
+ */
486
+ export function splitIfUnlinked(before, mid, relId) {
487
+ if (before.relationships[relId]?.heldCounterpartHead !== null)
488
+ return null;
489
+ const rel = mid.relationships[relId];
490
+ const E = entryOf(mid, rel);
491
+ if (!E)
492
+ return null;
493
+ const others = entryRelationships(mid, E).filter((r) => r.id !== relId);
494
+ const holding = others.filter((r) => r.heldCounterpartHead !== null);
495
+ // 2080 read literally would split every manual merge of unmapped
496
+ // relationships at the first mapping; open point for Trust 0.32. Here
497
+ // the relationship stays where the others hold no anchor to contradict
498
+ if (!holding.length)
499
+ return null;
500
+ const mine = anchorSet(rel);
501
+ const shares = holding.some((o) => [...anchorSet(o)].some((a) => mine.has(a)));
502
+ if (shares && !conflicts([rel], others))
503
+ return null;
504
+ return splitOff(mid, relId);
505
+ }
506
+ /** The split of `relId` from its entry (2080–2115). */
507
+ export function splitOff(state, relId) {
508
+ const rel = state.relationships[relId];
509
+ const E = entryOf(state, rel);
510
+ const identity = rel.contactId;
511
+ const chain = chainAliases(state, relId);
512
+ const leaverIds = new Set([identity, ...chain]);
513
+ const remaining = E.relIds.filter((id) => id !== relId);
514
+ const eId = leaverIds.has(E.id) ? oldestIdentity(state, E, remaining, leaverIds) : E.id;
515
+ // 2115: every class at default, except an `off` of the merged entry, which goes along
516
+ const settings = {};
517
+ for (const cls of CLASSES)
518
+ settings[cls] = E.settings[cls] === 'off' ? 'off' : 'default';
519
+ const groupSettings = {};
520
+ for (const [g, s] of Object.entries(E.groupSettings))
521
+ if (s === 'off')
522
+ groupSettings[g] = 'off';
523
+ const contacts = { ...state.contacts };
524
+ delete contacts[E.id];
525
+ contacts[eId] = { ...E, id: eId, relIds: remaining, aliases: E.aliases.filter((a) => !leaverIds.has(a) && a !== eId) };
526
+ contacts[identity] = {
527
+ id: identity, pos: null, status: 'none', aliases: chain.filter((a) => a !== identity), relIds: [relId], anchorHead: null, settings, groupSettings,
528
+ };
529
+ // the leaver's keys go to its own entry (2145 entryId, its duties and recipients)
530
+ const renames = [];
531
+ let next = rewriteIdentity({ ...state, contacts }, [E.id, ...E.aliases], identity, { rels: (id) => id === relId, renames });
532
+ // the remaining entry's keys that resolved through the old identity (2100)
533
+ if (eId !== E.id)
534
+ next = rename(next, E.id, eId, (id) => id !== relId, renames);
535
+ next = { ...next, contacts: { ...next.contacts, [eId]: settleHead(next, next.contacts[eId]) } };
536
+ // the leaver's classes start at default (2115): the trusted contacts' classes off with the anchor mapping (6410)
537
+ next = dropOff(next).state;
538
+ return { state: next, event: { type: 'contact.split', contactId: eId, leaver: identity, relationshipId: relId }, renames };
539
+ }
540
+ // ── departure (2210, 2250, 2260, 5860) ─────────────────────────────────
541
+ const needInput = (x, name) => {
542
+ if (x === null || typeof x !== 'object' || Array.isArray(x))
543
+ throw new TypeError(`${name} needs an input object`);
544
+ return x;
545
+ };
546
+ /** The matches of every accepted star without the entry ids `ids` (6160). */
547
+ function dropMatches(state, ids) {
548
+ if (!ids.length)
549
+ return state;
550
+ let relationships = null;
551
+ for (const [id, r] of Object.entries(state.relationships)) {
552
+ if (!r.tuples.some((t) => t.starIn.accepted?.matches.some((m) => ids.includes(m))))
553
+ continue;
554
+ const tuples = r.tuples.map((t) => {
555
+ const acc = t.starIn.accepted;
556
+ return acc && acc.matches.some((m) => ids.includes(m)) ? { ...t, starIn: { ...t.starIn, accepted: { ...acc, matches: acc.matches.filter((m) => !ids.includes(m)) } } } : t;
557
+ });
558
+ (relationships ??= { ...state.relationships })[id] = { ...r, tuples };
559
+ }
560
+ return relationships ? { ...state, relationships } : state;
561
+ }
562
+ /** The identities of a relationship: its own and its chain aliases. */
563
+ const identitiesOf = (state, relId) => [state.relationships[relId].contactId, ...chainAliases(state, relId)];
564
+ /**
565
+ * Departure: the relationships leave the state with their husks; an entry
566
+ * left without relationships is removed (pending: 2260; admitted: its
567
+ * slot frees, 2250), one that keeps some drops their aliases, takes a new
568
+ * identity where theirs was its own (2100) and settles its anchor head
569
+ * (2080); pending entries are admitted in position order in the same
570
+ * step, so the set changes once (5860).
571
+ */
572
+ export function depart(state, input, bound) {
573
+ const c = findContact(state, input.contactId);
574
+ if (!c)
575
+ return { ok: false, code: 'unknown-contact' };
576
+ let relIds;
577
+ if (input.relationshipId !== undefined) {
578
+ if (!c.relIds.includes(input.relationshipId))
579
+ return { ok: false, code: 'unknown-contact' };
580
+ relIds = [input.relationshipId];
581
+ }
582
+ else if (input.contactId !== c.id) {
583
+ relIds = c.relIds.filter((rid) => Object.hasOwn(state.relationships, rid) && identitiesOf(state, rid).includes(input.contactId));
584
+ }
585
+ else
586
+ relIds = [...c.relIds];
587
+ const leaving = new Set(relIds.flatMap((rid) => [rid, ...husksOf(state, rid)]));
588
+ const goneIds = new Set([...relIds.flatMap((rid) => (Object.hasOwn(state.relationships, rid) ? identitiesOf(state, rid) : []))]);
589
+ if (input.contactId !== c.id && input.relationshipId === undefined)
590
+ goneIds.add(input.contactId); // an alias without a relationship
591
+ const relationships = {};
592
+ for (const [id, r] of Object.entries(state.relationships))
593
+ if (!leaving.has(id))
594
+ relationships[id] = r;
595
+ const contacts = {};
596
+ const renamed = [];
597
+ let removed = false;
598
+ const removedIds = [];
599
+ for (const x of Object.values(state.contacts)) {
600
+ const rest = x.relIds.filter((id) => !leaving.has(id));
601
+ if (rest.length === x.relIds.length && !x.aliases.some((a) => goneIds.has(a))) {
602
+ contacts[x.id] = x;
603
+ continue;
604
+ }
605
+ if (!rest.length) {
606
+ if (x.id === c.id)
607
+ removed = true;
608
+ removedIds.push(x.id);
609
+ continue;
610
+ }
611
+ const id = goneIds.has(x.id) ? oldestIdentity(state, x, rest, goneIds) : x.id;
612
+ if (id !== x.id)
613
+ renamed.push({ from: x.id, to: id, rels: rest });
614
+ contacts[id] = { ...x, id, relIds: rest, aliases: x.aliases.filter((a) => !goneIds.has(a) && a !== id) };
615
+ }
616
+ const duties = {};
617
+ for (const [id, d] of Object.entries(state.duties)) {
618
+ const relOf = d.relId;
619
+ // a duty of a kind (or version) this library does not know is kept as it is
620
+ if (typeof relOf === 'string' && leaving.has(relOf) && d.kind !== 'merge' && isKnownDuty(d))
621
+ continue;
622
+ duties[id] = d;
623
+ }
624
+ const rotations = {};
625
+ for (const [id, r] of Object.entries(state.rotations)) {
626
+ rotations[id] = Object.keys(r.awaitHead).some((k) => leaving.has(k))
627
+ ? { ...r, awaitHead: Object.fromEntries(Object.entries(r.awaitHead).filter(([k]) => !leaving.has(k))) }
628
+ : r;
629
+ }
630
+ let next = {
631
+ ...state, relationships, contacts, duties, rotations,
632
+ entryOrder: { ...state.entryOrder, heads: state.entryOrder.heads.filter((id) => !leaving.has(id)) },
633
+ };
634
+ // 6160: an accepted star matches the holder's contacts; one that left matches nothing
635
+ next = dropMatches(next, removedIds);
636
+ const renames = [];
637
+ for (const r of renamed)
638
+ next = rename(next, r.from, r.to, (id) => r.rels.includes(id), renames);
639
+ for (const x of Object.values(next.contacts)) {
640
+ const s = settleHead(next, x);
641
+ if (s !== x)
642
+ next = { ...next, contacts: { ...next.contacts, [x.id]: s } };
643
+ }
644
+ // 2850: a relationship that ceased to lag with the departure gets the classes on for it
645
+ const f = followClasses(state, admitPending(next, bound));
646
+ next = f.state;
647
+ return {
648
+ ...(f.duties.length ? { duties: f.duties } : {}),
649
+ state: next, events: [{ type: 'contact.departed', contactId: c.id, relationshipIds: relIds.filter((id) => Object.hasOwn(state.relationships, id)).sort(cmp), removed }],
650
+ ...(renames.length ? { renames } : {}),
651
+ };
652
+ }
653
+ // ── the commands ───────────────────────────────────────────────────────
654
+ export function entryCommands(bound) {
655
+ return {
656
+ // rung 3: the manual merge (6640)
657
+ [ENTRY_COMMANDS.merge]: {
658
+ human: true,
659
+ apply(state, _p, input) {
660
+ const i = needInput(input, ENTRY_COMMANDS.merge);
661
+ if (!Array.isArray(i.contactIds) || i.contactIds.some((x) => typeof x !== 'string' || x === ''))
662
+ throw new TypeError('trust.merge: contactIds must be non-empty strings');
663
+ const cs = [];
664
+ for (const x of i.contactIds) {
665
+ const c = findContact(state, x);
666
+ if (!c)
667
+ return { ok: false, code: 'unknown-contact' };
668
+ if (!cs.includes(c))
669
+ cs.push(c);
670
+ }
671
+ if (cs.length < 2)
672
+ return { state: state };
673
+ if (cs.filter((c) => c.pos !== null).length > 1)
674
+ return { ok: false, code: 'not-mergeable', ref: 'RLTP-TRU-6640' };
675
+ if (!mergeable(state, cs))
676
+ return { ok: false, code: 'not-mergeable', ref: 'RLTP-TRU-6605' };
677
+ const r = mergeEntries(state, cs, bound);
678
+ return { state: r.state, ...(r.events ? { events: r.events } : {}), ...(r.duties ? { duties: r.duties } : {}), ...(r.renames ? { renames: r.renames } : {}) };
679
+ },
680
+ },
681
+ [ENTRY_COMMANDS.depart]: {
682
+ human: true,
683
+ apply(state, _p, input) {
684
+ const i = needInput(input, ENTRY_COMMANDS.depart);
685
+ if (typeof i.contactId !== 'string' || i.contactId === '')
686
+ throw new TypeError('trust.depart: contactId must be a non-empty string');
687
+ if (i.relationshipId !== undefined && (typeof i.relationshipId !== 'string' || i.relationshipId === ''))
688
+ throw new TypeError('trust.depart: relationshipId must be a non-empty string');
689
+ return depart(state, i, bound);
690
+ },
691
+ },
692
+ };
693
+ }
694
+ //# sourceMappingURL=entries.js.map