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