@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,953 @@
1
+ // invariants — predicates over one holder state and over one commit
2
+ // (before, after), for tests and an optional runtime check (lib Stage 2,
3
+ // note §4.7). createHolder(ports, {invariants: true}) runs them on every
4
+ // commit, before the store's CAS, and throws InfraError
5
+ // 'invariant-violated' with the violations as its cause: a violating
6
+ // state never lands. A testing mode, not a production guard; the property
7
+ // generator (test/invariants.test.mjs) checks them after every step.
8
+ //
9
+ // Rule numbers: bare four-digit numbers are RLTP-TRU (trust-layer.md);
10
+ // Identity §n is identity-layer.md, Delivery §n delivery-contract.md.
11
+ //
12
+ // Over one state (stateViolations):
13
+ // register every revocation mark names a held device name; the
14
+ // lineage is chained (each prev the preceding next) and no
15
+ // longer than the head allows; at most one rotation runs,
16
+ // from the head to the next generation, its label persisted
17
+ // (6360), its step-2 duty open; a rotation duty names a
18
+ // running rotation (Identity §5.4, §5.5)
19
+ // entries every tuple's entryId names a contact or an alias (2145);
20
+ // every contact's relIds exist, are distinct, are no husks;
21
+ // every relationship's contactId resolves (2090, 2140);
22
+ // an id or alias names one contact only, never itself as an
23
+ // alias (2140); every live relationship belongs to the entry
24
+ // its identity resolves to and to the entry its active tuple
25
+ // feeds, and otherwise only to entries one of its tuples
26
+ // names: an earlier appended tuple's entry stays behind the
27
+ // chain after a later re-encounter appended another, until a
28
+ // merge duty merges them (2145, 2146); a position exactly
29
+ // where the status is not `none` (2060, 2250); the matches
30
+ // of an accepted star name contacts (6160);
31
+ // entry positions are distinct and below nextPos (2120);
32
+ // entryOrder.heads names each live relationship once (2180)
33
+ // head an entry all of whose relationships feed it holds as its
34
+ // anchor head one of their newest heads, none where they
35
+ // hold none (6560)
36
+ // bindings every binding list is non-empty, sorted and distinct; two
37
+ // bindings for one anchor are the flagged conflict of a
38
+ // chaining's union (6610); two links from one prev to
39
+ // different nexts only beside such a flag
40
+ // salts per tuple and reconciled artifact: every in-flight salt in
41
+ // [1, lastSalt], hwm ≤ lastSalt, snapshot and its digest
42
+ // present together (5630, 5660, 5690); probe sequences
43
+ // likewise (6680)
44
+ // assembly an open assembly's salt lies above both the completed hwm
45
+ // and the lower bound (5150, 5366); an accepted star or group
46
+ // star carries the hwm as its salt
47
+ // duties a class duty (class-on, reissue-mapping, profile) is owed
48
+ // on an existing relationship that is no husk, under the
49
+ // entry its verifications go to, whose class is on (6410,
50
+ // 2790: the drop rule holds); a class-on duty has its
51
+ // deterministic id (trust/duties.ts)
52
+ // encounter no challenge both open and recorded (5220); challenge and
53
+ // record labels in the register (4110); a scanner's record
54
+ // holds its credential (5630), a pending counter document its
55
+ // credential (5770); an encounter-confirm prompt names a record
56
+ // groups the own member anchor on a held roster, its label in the
57
+ // register; no evidence for a member already admitted
58
+ // introductions a mediator completes only with ack, reply forwarded and
59
+ // vouchers sent (8360); completed, consented and vouched hold
60
+ // a reply; forwarded holds the target pin (8270)
61
+ // owed a duty class on for a live relationship (2850, 6595) with
62
+ // an artifact to send has it out or owed there: the anchor
63
+ // mapping issued on the active tuple or a class-on or
64
+ // re-issue duty open on it; the profile (once one exists)
65
+ // issued at the counter's revision on the active tuple
66
+ // (profileOut) or a class-on or profile duty open on it
67
+ //
68
+ // messages a block names a relationship the holder holds (Message
69
+ // Tasks 6010)
70
+ //
71
+ // Over one commit (transitionViolations):
72
+ // commitSeq strictly +1 per commit; lastWhen never falls (4.7)
73
+ // register labels, pair nonces, carriers, device names and revocation
74
+ // marks only grow; the community keeps its genesis and its
75
+ // head never falls; the lineage only extends (Identity §2); a
76
+ // device.added of an act never names a name revoked before
77
+ // (§5.5); names and marks are sets (a state check); the head
78
+ // moves one generation at a time, with exactly one lineage
79
+ // element, onto a label persisted in an earlier commit (6360)
80
+ // profile the own profile's one counter never falls and its content
81
+ // changes only with it (6450)
82
+ // addresses tombstones never shrink; a key leaves only as a tombstone,
83
+ // a member key (group/…, an identity context) excepted
84
+ // (Delivery §5)
85
+ // salts per tuple label: lastSalt, probe seq and the issued
86
+ // revisions never fall, exhausted is sticky, a new in-flight
87
+ // salt is above the old lastSalt (a salt is never issued
88
+ // twice, 5110) with the digest of its view; what a salt or
89
+ // probe committed (view, viewDigest, digests) never changes,
90
+ // only acked grows within its digests (5130; a rename may
91
+ // rewrite the star's members, 2100); an entry leaves only
92
+ // completed at or below hwm, or with no document left in the
93
+ // outbox; hwm and lowerBound never fall (5140, 5366)
94
+ // baseline hwm, snapshot and snapshotDigest change only together,
95
+ // upward, by the delivery-ack that completes the in-flight
96
+ // entry of the new hwm, to that entry's view (5660, 5670,
97
+ // 5680; star and group star); a rename rewrites the star
98
+ // baseline's members only. The probe's done is sticky and
99
+ // set only by the ack completing a probe (6830)
100
+ // tuples a tuple new in this commit carries fresh per-tuple state:
101
+ // every salt, revision and sequence at 0, no baseline,
102
+ // assembly, in-flight entry or verified artifact (6250)
103
+ // positions a position once assigned stays with its entry (2120,
104
+ // 2190; after a split with what remains of it, 2080, 2100):
105
+ // it leaves only with the entry, merged into one at an
106
+ // earlier position or departed, and is never assigned again;
107
+ // nextPos never falls; entryOrder.heads only appends, a
108
+ // departed relationship leaving it and a chaining moving the
109
+ // chain into the appended relationship's place (2180)
110
+ // bindings a second binding for one anchor arises only where a
111
+ // chaining appended a tuple in this commit (6610, the union of
112
+ // 4c); a relationship's held anchor head moves only along the
113
+ // links it holds (6150, 6170), except at such a chaining
114
+ // messages the record of surfaced invitations and the register of
115
+ // declined ones only grow (RLTP-MT-3130, RLTP-MSG-6310); a
116
+ // block is set only by the block act, a merge, or a
117
+ // chaining that absorbs a blocked fresh relationship (6030),
118
+ // cleared only by the block act or with its relationship; a
119
+ // completion-record entry never changes and leaves only by
120
+ // tick (4340); an inbox entry arises only in a unique
121
+ // receive of its document, with its record entry, and leaves
122
+ // only by the take-over (4420)
123
+ // outbox `accepted` appears only through report; a new item starts
124
+ // submitted (Delivery §6.1, note §4.3)
125
+ // effects a new cache entry only through a unique receive, one, the
126
+ // document's digest; entries leave only by tick (Delivery
127
+ // 4.2)
128
+ // duties a duty disappears only where a rule lets it (dutyDrops):
129
+ // (a) fulfilment: resume ran its handler and the handler
130
+ // returned Applied in this commit (CommitOp.fulfilled)
131
+ // (b) the drop rules: a class duty whose class is off toward
132
+ // the entry its relationship's verifications go to (6410,
133
+ // 2790; a split's 2115 included), any known duty whose
134
+ // relationship departed or became a husk (2260, the move
135
+ // at a chaining)
136
+ // (c) the merge into one of its kind owed on the same
137
+ // relationship with its urgency kept: a re-issue into one
138
+ // no later and not waiting where it was due (awaitHead
139
+ // null wins, 6596), a profile duty into one of no lower
140
+ // revision
141
+ // (d) the same delivery: an anchor mapping issued on the
142
+ // relationship in this commit meets every class-on and
143
+ // re-issue of the anchor mapping owed there (6596); a
144
+ // profile issued there (its revision rose on the active
145
+ // tuple, or a profile duty owed there was fulfilled)
146
+ // meets every class-on and profile duty owed there
147
+ // (e) a received document that ends its continuity step: a
148
+ // probe duty by the ack that completes an in-flight probe
149
+ // (done set in this commit, 6830); a mapping duty by its
150
+ // current issuance answered (aligned in this commit) —
151
+ // the ack of one of that issuance's documents, or a
152
+ // continuity mapping arriving on the tuple (6850)
153
+ // (f) a rotation duty, by its rotation's completion in this
154
+ // commit (the act's step 2: the head moved, the rotation
155
+ // gone)
156
+ // expiry: no duty kind expires yet. A duty of a kind (or
157
+ // version) the library does not know leaves only by (a), a
158
+ // module's handler: never where none is registered
159
+ // journal act records leave only by tick (retention)
160
+ // encounter records only grow, a credential held in a direction never
161
+ // changes (5.6 step 8, 5660); a challenge leaves only into a
162
+ // record or aged at lastWhen (5290)
163
+ // introductions an act leaves only past its absolute lifetime (8310)
164
+ // groups a held group and its roster members stay (removal is not built)
165
+ //
166
+ // Choices where the brief and the note leave them open (PR 5):
167
+ // - "effects holds only completed digests" is read as: an entry enters
168
+ // only with the stage-9 commit of a unique receive. A chunk held in an
169
+ // open assembly is such a completed effect (every chunk is acked on
170
+ // its own, Delivery 4.2, 5.2a), so "no effect digest in an open
171
+ // assembly" would contradict the per-chunk ack; it is not checked
172
+ // - "lowerBound ≥ hwm when set" does not hold: a discard raises the lower
173
+ // bound to a salt above hwm, and a later, higher salt completing
174
+ // raises hwm past it (5140, 5366). Checked instead: both never fall,
175
+ // and an open assembly lies above both
176
+ import { MESSAGE_LIFETIME_MS } from './message.js';
177
+ import { jcs } from './core.js';
178
+ import { triasOf } from './state.js';
179
+ import { ADMISSION_BOUND, classOnToward, entryOf, entryRelationships, feeding, findContact, liveRel, newestHeads, newestOf } from './trust/state.js';
180
+ import { classOfDuty, classOnId, hasArtifact, onFor, owedOn } from './trust/duties.js';
181
+ import { CONTINUITY_MAPPING_TYPE, CONTINUITY_PROBE_TYPE } from './trust/continuity.js';
182
+ import { viewDigest } from './trust/star.js';
183
+ import { isKnownDuty } from './codec.js';
184
+ import { communityLabel } from './identity.js';
185
+ import { challengeAged } from './encounter.js';
186
+ import { ACT_EXPIRY, ACT_SKEW } from './introduction.js';
187
+ const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
188
+ const big = (d) => BigInt(d);
189
+ const isHusk = (r) => r.chainedInto !== null;
190
+ // ── one state ──────────────────────────────────────────────────────────
191
+ function seqOutViolations(v, where, o) {
192
+ const last = big(o.lastSalt);
193
+ for (const salt of Object.keys(o.inflight)) {
194
+ if (big(salt) < 1n || big(salt) > last)
195
+ v.push({ invariant: 'salts', detail: `${where}: in-flight salt ${salt} outside [1, lastSalt ${o.lastSalt}]` });
196
+ }
197
+ if (big(o.hwm) > last)
198
+ v.push({ invariant: 'salts', detail: `${where}: hwm ${o.hwm} above lastSalt ${o.lastSalt}` });
199
+ if ((o.snapshot === null) !== (o.snapshotDigest === null))
200
+ v.push({ invariant: 'salts', detail: `${where}: snapshot and snapshotDigest not together` });
201
+ if (o.exhausted && last < ADMISSION_BOUND)
202
+ v.push({ invariant: 'salts', detail: `${where}: exhausted below the bound` });
203
+ }
204
+ function seqInViolations(v, where, i, accepted) {
205
+ if (i.assembly !== null) {
206
+ const s = big(i.assembly.salt);
207
+ if (s <= big(i.hwm) || s <= big(i.lowerBound))
208
+ v.push({ invariant: 'assembly', detail: `${where}: open assembly at salt ${i.assembly.salt} not above hwm ${i.hwm} and lowerBound ${i.lowerBound}` });
209
+ }
210
+ if (accepted && i.accepted !== null && i.accepted.salt !== i.hwm) {
211
+ v.push({ invariant: 'assembly', detail: `${where}: accepted salt ${i.accepted.salt} is not hwm ${i.hwm}` });
212
+ }
213
+ }
214
+ function tupleViolations(v, state, r, t) {
215
+ const where = `${r.id}/${t.ownPairLabel}`;
216
+ if (!findContact(state, t.entryId))
217
+ v.push({ invariant: 'entries', detail: `${where}: entryId ${t.entryId} names no contact` });
218
+ seqOutViolations(v, `${where} starOut`, t.starOut);
219
+ seqOutViolations(v, `${where} groupStarOut`, t.groupStarOut);
220
+ for (const p of Object.keys(t.probeOut.inflight)) {
221
+ if (big(p) < 1n || big(p) > big(t.probeOut.seq))
222
+ v.push({ invariant: 'salts', detail: `${where}: probe ${p} outside [1, seq ${t.probeOut.seq}]` });
223
+ }
224
+ seqInViolations(v, `${where} starIn`, t.starIn, true);
225
+ seqInViolations(v, `${where} groupStarIn`, t.groupStarIn, true);
226
+ seqInViolations(v, `${where} probeIn`, { ...t.probeIn, accepted: null }, false);
227
+ }
228
+ function dutyViolations(v, state, d) {
229
+ const cls = classOfDuty(d);
230
+ if (cls === null)
231
+ return;
232
+ const relId = d.relId;
233
+ if (d.kind === 'class-on' && d.id !== classOnId(cls, relId))
234
+ v.push({ invariant: 'duties', detail: `${d.id}: a class-on duty's id is ${classOnId(cls, relId)}` });
235
+ if (!Object.hasOwn(state.relationships, relId) || isHusk(state.relationships[relId])) {
236
+ v.push({ invariant: 'duties', detail: `${d.id}: owed on ${relId}, which is gone or a husk` });
237
+ return;
238
+ }
239
+ const e = entryOf(state, state.relationships[relId]);
240
+ const named = findContact(state, d.contactId);
241
+ if (!e || !named || named.id !== e.id)
242
+ v.push({ invariant: 'duties', detail: `${d.id}: owed under ${d.contactId}, not the entry ${e?.id} its relationship's verifications go to` });
243
+ else if (!classOnToward(e, cls))
244
+ v.push({ invariant: 'duties', detail: `${d.id}: the class ${cls} is off toward ${e.id} (6410)` });
245
+ }
246
+ /** The register (Identity §2, §5.4, §5.5) and the running rotations. */
247
+ function registerViolations(v, state) {
248
+ const r = state.register;
249
+ for (const n of r.devices.revoked)
250
+ if (!r.devices.names.includes(n))
251
+ v.push({ invariant: 'register', detail: `revocation mark ${n} without its device name` });
252
+ if (new Set(r.devices.names).size !== r.devices.names.length || new Set(r.devices.revoked).size !== r.devices.revoked.length)
253
+ v.push({ invariant: 'register', detail: 'device names or revocation marks repeat: they are sets' });
254
+ const lin = r.lineage;
255
+ for (let i = 1; i < lin.length; i++)
256
+ if (lin[i].body?.prev !== lin[i - 1].body?.next)
257
+ v.push({ invariant: 'register', detail: `lineage element ${i} does not continue element ${i - 1}` });
258
+ if (r.community !== null && big(r.community.head) - 1n < BigInt(lin.length))
259
+ v.push({ invariant: 'register', detail: `${lin.length} lineage elements beyond head ${r.community.head}` });
260
+ for (const rot of Object.values(state.rotations)) {
261
+ if (r.community === null || big(rot.from) !== big(r.community.head) || big(rot.to) !== big(rot.from) + 1n)
262
+ v.push({ invariant: 'register', detail: `rotation ${rot.id} ${rot.from} → ${rot.to} does not start at the head ${r.community?.head}` });
263
+ else if (!r.labels.includes(communityLabel(r.community.genesis, Number(rot.to))))
264
+ v.push({ invariant: 'register', detail: `rotation ${rot.id}: the label of generation ${rot.to} is not persisted (6360)` });
265
+ }
266
+ if (Object.keys(state.rotations).length > 1)
267
+ v.push({ invariant: 'register', detail: 'more than one rotation running' });
268
+ // a running rotation has its step-2 duty open, and a rotation duty names a running rotation
269
+ for (const rot of Object.values(state.rotations)) {
270
+ if (!Object.values(state.duties).some((d) => d.kind === 'rotation' && d.rotationId === rot.id))
271
+ v.push({ invariant: 'register', detail: `rotation ${rot.id} has no rotation duty: it would never complete` });
272
+ }
273
+ for (const d of Object.values(state.duties)) {
274
+ if (d.kind === 'rotation' && isKnownDuty(d) && !Object.hasOwn(state.rotations, d.rotationId))
275
+ v.push({ invariant: 'duties', detail: `${d.id}: no rotation ${d.rotationId} runs` });
276
+ }
277
+ }
278
+ /** The invariants of one state (see the header); empty where all hold. */
279
+ /** The encounter, the groups and the introduction acts (PR 7a). */
280
+ function socialViolations(v, state) {
281
+ const labels = new Set(state.register.labels);
282
+ const e = state.encounter;
283
+ for (const c of Object.values(e.challenges)) {
284
+ if (Object.hasOwn(e.records, c.value))
285
+ v.push({ invariant: 'encounter', detail: `challenge ${c.value} both open and recorded (5220)` });
286
+ if (!labels.has(c.label))
287
+ v.push({ invariant: 'encounter', detail: `challenge ${c.value}: its label is not in the register (4110)` });
288
+ }
289
+ for (const r of Object.values(e.records)) {
290
+ if (!labels.has(r.label))
291
+ v.push({ invariant: 'encounter', detail: `record ${r.own}: its label is not in the register (4110)` });
292
+ if (r.role === 'scanner' && r.out === null)
293
+ v.push({ invariant: 'encounter', detail: `record ${r.own}: a scanner's record without its step credential (5630)` });
294
+ if (r.counterPending && r.out === null)
295
+ v.push({ invariant: 'encounter', detail: `record ${r.own}: a counter document pending without the credential (5770)` });
296
+ }
297
+ for (const p of Object.values(state.prompts)) {
298
+ if (p.kind === 'encounter-confirm' && !Object.hasOwn(e.records, p.id.slice('encounter:'.length)))
299
+ v.push({ invariant: 'encounter', detail: `prompt ${p.id} names no record` });
300
+ }
301
+ for (const g of Object.values(state.groups)) {
302
+ if (g.memberAnchor !== undefined && Object.keys(g.roster).length && !Object.hasOwn(g.roster, g.memberAnchor))
303
+ v.push({ invariant: 'groups', detail: `${g.id}: the own member anchor is not on the roster` });
304
+ if (Object.keys(g.roster).length && !labels.has(g.memberLabel))
305
+ v.push({ invariant: 'groups', detail: `${g.id}: the member label is not in the register` });
306
+ for (const ev of Object.values(g.evidence)) {
307
+ const subject = ev.accept.payload?.accept?.subject;
308
+ if (subject !== undefined && Object.hasOwn(g.roster, subject))
309
+ v.push({ invariant: 'groups', detail: `${g.id}: evidence held for a member already admitted` });
310
+ }
311
+ }
312
+ for (const a of Object.values(e.introductions)) {
313
+ if (a.role === 'mediator' && a.status === 'completed' && !(a.ack !== null && a.replyForwarded && a.vouchersSent))
314
+ v.push({ invariant: 'introductions', detail: `${a.key}: completed without its three duties (8360)` });
315
+ if (a.vouched !== (a.voucher !== null))
316
+ v.push({ invariant: 'introductions', detail: `${a.key}: vouched without its retained voucher, or the reverse (8200)` });
317
+ if ((a.status === 'completed' && a.role === 'requester') || a.status === 'consented' || a.vouched) {
318
+ if (a.reply === null)
319
+ v.push({ invariant: 'introductions', detail: `${a.key}: ${a.status}${a.vouched ? ', vouched' : ''} without a reply` });
320
+ }
321
+ if (a.role === 'mediator' && a.status === 'forwarded' && a.pins.target === undefined)
322
+ v.push({ invariant: 'introductions', detail: `${a.key}: forwarded without a target pin (8270)` });
323
+ }
324
+ }
325
+ export function stateViolations(state) {
326
+ const v = [];
327
+ registerViolations(v, state);
328
+ // a block names a relationship the holder holds (Message Tasks 6010)
329
+ for (const r of state.messages.blocked)
330
+ if (!Object.hasOwn(state.relationships, r))
331
+ v.push({ invariant: 'messages', detail: `blocked ${r} names no relationship` });
332
+ socialViolations(v, state);
333
+ const contacts = Object.values(state.contacts);
334
+ const positions = new Set();
335
+ for (const c of contacts) {
336
+ if (!c.relIds.length || new Set(c.relIds).size !== c.relIds.length)
337
+ v.push({ invariant: 'entries', detail: `${c.id}: relIds ${jcs(c.relIds)} empty or repeated` });
338
+ for (const id of c.relIds) {
339
+ if (!Object.hasOwn(state.relationships, id))
340
+ v.push({ invariant: 'entries', detail: `${c.id}: relationship ${id} does not exist` });
341
+ else if (isHusk(state.relationships[id]))
342
+ v.push({ invariant: 'entries', detail: `${c.id}: relationship ${id} is a husk` });
343
+ }
344
+ if (c.pos !== null) {
345
+ if (positions.has(c.pos))
346
+ v.push({ invariant: 'entries', detail: `${c.id}: position ${c.pos} taken twice` });
347
+ positions.add(c.pos);
348
+ if (big(c.pos) >= big(state.entryOrder.nextPos))
349
+ v.push({ invariant: 'entries', detail: `${c.id}: position ${c.pos} not below nextPos ${state.entryOrder.nextPos}` });
350
+ }
351
+ headViolations(v, state, c);
352
+ }
353
+ const holdersOf = new Map();
354
+ for (const c of contacts)
355
+ for (const id of c.relIds)
356
+ holdersOf.set(id, [...(holdersOf.get(id) ?? []), c.id]);
357
+ const names = new Map();
358
+ for (const c of contacts) {
359
+ if ((c.pos === null) !== (c.status === 'none'))
360
+ v.push({ invariant: 'entries', detail: `${c.id}: position ${c.pos} with status ${c.status}` });
361
+ for (const n of [c.id, ...c.aliases]) {
362
+ if (names.has(n))
363
+ v.push({ invariant: 'entries', detail: `${n} names ${names.get(n)} and ${c.id}` });
364
+ names.set(n, c.id);
365
+ }
366
+ }
367
+ for (const r of Object.values(state.relationships)) {
368
+ if (!findContact(state, r.contactId))
369
+ v.push({ invariant: 'entries', detail: `${r.id}: contactId ${r.contactId} resolves to no contact` });
370
+ if (!isHusk(r)) {
371
+ const holders = holdersOf.get(r.id) ?? [];
372
+ const own = findContact(state, r.contactId)?.id;
373
+ const fed = entryOf(state, r)?.id;
374
+ const named = new Set(r.tuples.map((t) => findContact(state, t.entryId)?.id).filter((x) => x !== undefined));
375
+ const ok = own !== undefined && holders.includes(own) && holders.every((h) => h === own || h === fed || named.has(h)) && (fed === undefined || holders.includes(fed));
376
+ if (!ok)
377
+ v.push({ invariant: 'entries', detail: `${r.id}: held by ${jcs(holders)}, its identity resolves to ${own}, its active tuple feeds ${fed}` });
378
+ }
379
+ for (const t of r.tuples) {
380
+ for (const m of t.starIn.accepted?.matches ?? [])
381
+ if (!findContact(state, m))
382
+ v.push({ invariant: 'entries', detail: `${r.id}/${t.ownPairLabel}: an accepted star matches ${m}, no contact` });
383
+ }
384
+ for (const t of r.tuples)
385
+ tupleViolations(v, state, r, t);
386
+ for (const [anchor, list] of Object.entries(r.bindings)) {
387
+ if (!list.length || jcs(list) !== jcs([...new Set(list)].sort(cmp)))
388
+ v.push({ invariant: 'bindings', detail: `${r.id}: bindings of ${anchor} not a sorted, distinct, non-empty list` });
389
+ }
390
+ for (const l of r.links) {
391
+ if (r.links.some((m) => m.prev === l.prev && m.next !== l.next) && !((r.bindings[l.prev]?.length ?? 0) > 1)) {
392
+ v.push({ invariant: 'bindings', detail: `${r.id}: links from ${l.prev} to different nexts without a flagged binding conflict` });
393
+ break;
394
+ }
395
+ }
396
+ }
397
+ const heads = state.entryOrder.heads;
398
+ if (new Set(heads).size !== heads.length)
399
+ v.push({ invariant: 'entries', detail: 'entryOrder.heads repeats a relationship' });
400
+ for (const id of heads)
401
+ if (!Object.hasOwn(state.relationships, id) || isHusk(state.relationships[id]))
402
+ v.push({ invariant: 'entries', detail: `entryOrder.heads names ${id}, gone or a husk` });
403
+ const headSet = new Set(heads);
404
+ for (const r of Object.values(state.relationships))
405
+ if (liveRel(r) && !headSet.has(r.id))
406
+ v.push({ invariant: 'entries', detail: `${r.id} is live but not in entryOrder.heads` });
407
+ for (const d of Object.values(state.duties))
408
+ dutyViolations(v, state, d);
409
+ // 2850: on for a relationship, out there or owed there
410
+ const duties = Object.values(state.duties);
411
+ for (const r of Object.values(state.relationships)) {
412
+ if (!liveRel(r))
413
+ continue;
414
+ for (const cls of ['anchorMapping', 'profile']) {
415
+ if (!hasArtifact(state, cls) || !onFor(state, r, cls))
416
+ continue;
417
+ const head = r.tuples.find((t) => t.ownPairLabel === r.activeHead);
418
+ const out = cls === 'anchorMapping' ? head.mappingOut.body !== null : head.profileOut.rev === state.profile.revision;
419
+ if (!out && !duties.some((d) => owedOn(d, r.id, cls)))
420
+ v.push({ invariant: 'owed', detail: `${r.id}: the ${cls} class is on for it, nothing is out there and no duty owes it (2850)` });
421
+ }
422
+ }
423
+ return v;
424
+ }
425
+ /** 6560 for an entry all of whose relationships feed it. */
426
+ function headViolations(v, state, c) {
427
+ const all = entryRelationships(state, c);
428
+ const rels = feeding(state, c);
429
+ if (all.length !== rels.length || all.length !== c.relIds.length)
430
+ return;
431
+ const newest = newestHeads(rels);
432
+ if (!newest.length ? c.anchorHead !== null : (c.anchorHead === null || !newest.includes(c.anchorHead))) {
433
+ v.push({ invariant: 'head', detail: `${c.id}: anchor head ${c.anchorHead} is not among the newest heads ${jcs(newest)} of its relationships` });
434
+ }
435
+ }
436
+ // ── one commit ─────────────────────────────────────────────────────────
437
+ const superset = (after, before) => {
438
+ const a = new Set(after);
439
+ return before.filter((x) => !a.has(x));
440
+ };
441
+ /** Every tuple by its own pair label (labels are unique per holder). */
442
+ const tuplesByLabel = (state) => {
443
+ const m = new Map();
444
+ for (const r of Object.values(state.relationships))
445
+ for (const t of r.tuples)
446
+ m.set(t.ownPairLabel, t);
447
+ return m;
448
+ };
449
+ const dueOf = (d) => (Number.isSafeInteger(d.dueAt) ? d.dueAt : 0);
450
+ /** What a tuple new in this commit carries beyond fresh state (6250). */
451
+ function freshViolations(t) {
452
+ const out = [];
453
+ const seqOutFresh = (o) => o.lastSalt === '0' && o.hwm === '0' && !o.exhausted && o.snapshot === null && Object.keys(o.inflight).length === 0;
454
+ const seqInFresh = (i) => i.hwm === '0' && i.lowerBound === '0' && i.assembly === null && (i.accepted ?? null) === null;
455
+ if (!seqOutFresh(t.starOut))
456
+ out.push('starOut');
457
+ if (!seqOutFresh(t.groupStarOut))
458
+ out.push('groupStarOut');
459
+ if (!seqInFresh(t.starIn))
460
+ out.push('starIn');
461
+ if (!seqInFresh(t.groupStarIn))
462
+ out.push('groupStarIn');
463
+ if (!seqInFresh(t.probeIn))
464
+ out.push('probeIn');
465
+ if (t.probeOut.seq !== '0' || Object.keys(t.probeOut.inflight).length || t.probeOut.done)
466
+ out.push('probeOut');
467
+ if (t.mappingOut.rev !== '0' || t.mappingOut.body !== null)
468
+ out.push('mappingOut');
469
+ if (t.mappingIn.rev !== '0' || t.mappingIn.body !== null)
470
+ out.push('mappingIn');
471
+ if (t.continuityOut.rev !== '0' || t.continuityOut.body !== null)
472
+ out.push('continuityOut');
473
+ if (t.continuityIn.rev !== '0' || t.continuityIn.body !== null)
474
+ out.push('continuityIn');
475
+ if (t.profileIn.rev !== '0' || t.profileIn.body !== null)
476
+ out.push('profileIn');
477
+ if (t.profileOut.rev !== '0')
478
+ out.push('profileOut');
479
+ if (t.deactivated)
480
+ out.push('deactivated');
481
+ return out;
482
+ }
483
+ /**
484
+ * 2120, 2190: a position once assigned stays with its entry. Every
485
+ * position held before is held after, unless its entry merged into one at
486
+ * an earlier position or left the state; a position new in this commit
487
+ * lies at or above the old nextPos (never reused); nextPos never falls.
488
+ */
489
+ function positionViolations(v, before, after) {
490
+ if (big(after.entryOrder.nextPos) < big(before.entryOrder.nextPos))
491
+ v.push({ invariant: 'positions', detail: `nextPos fell ${before.entryOrder.nextPos} → ${after.entryOrder.nextPos}` });
492
+ const holder = new Map(Object.values(after.contacts).filter((c) => c.pos !== null).map((c) => [c.pos, c]));
493
+ const had = new Set(Object.values(before.contacts).map((c) => c.pos).filter((p) => p !== null));
494
+ for (const c of Object.values(before.contacts)) {
495
+ if (c.pos === null)
496
+ continue;
497
+ const x = findContact(after, c.id);
498
+ const y = holder.get(c.pos);
499
+ if (y !== undefined) {
500
+ // held by the same entry: the one c.id resolves to, or what remains of
501
+ // c's entry under another identity (2100: a rename; 2080: a split whose
502
+ // leaver took c.id, the remaining relationships keeping the position)
503
+ if (x !== undefined && x !== y && !y.relIds.every((id) => c.relIds.includes(id)))
504
+ v.push({ invariant: 'positions', detail: `${c.id}: position ${c.pos} moved to ${y.id}` });
505
+ continue;
506
+ }
507
+ if (x !== undefined && !(x.pos !== null && big(x.pos) < big(c.pos) && !Object.hasOwn(after.contacts, c.id))) {
508
+ v.push({ invariant: 'positions', detail: `${c.id}: position ${c.pos} changed to ${x.pos}` });
509
+ }
510
+ }
511
+ const held = holder.keys();
512
+ for (const p of held)
513
+ if (!had.has(p) && big(p) < big(before.entryOrder.nextPos))
514
+ v.push({ invariant: 'positions', detail: `position ${p} assigned again (nextPos was ${before.entryOrder.nextPos})` });
515
+ }
516
+ /**
517
+ * 2180: entryOrder.heads only appends, except that a departed relationship
518
+ * leaves it and a chaining puts the chain in the appended relationship's
519
+ * place (the chain's own place leaves).
520
+ */
521
+ function headsViolations(v, before, after) {
522
+ const into = new Map();
523
+ for (const r of Object.values(after.relationships)) {
524
+ if (r.chainedInto !== null && Object.hasOwn(before.relationships, r.id) && before.relationships[r.id].chainedInto === null)
525
+ into.set(r.id, r.chainedInto);
526
+ }
527
+ const chains = new Set(into.values());
528
+ const inAfter = new Set(after.entryOrder.heads);
529
+ const inBefore = new Set(before.entryOrder.heads);
530
+ const kept = before.entryOrder.heads.filter((id) => !chains.has(id)).map((id) => into.get(id) ?? id).filter((id) => inAfter.has(id));
531
+ const a = after.entryOrder.heads;
532
+ const added = a.slice(kept.length);
533
+ if (jcs(a.slice(0, kept.length)) !== jcs(kept) || added.some((id) => inBefore.has(id) && !chains.has(id))) {
534
+ v.push({ invariant: 'positions', detail: `entryOrder.heads ${jcs(before.entryOrder.heads)} → ${jcs(a)} is no append (2180)` });
535
+ }
536
+ }
537
+ /** Whether a rule lets duty `d` of `before` be gone after the commit `op` (see the header). */
538
+ export function dutyDrops(before, after, d, op) {
539
+ // (a) a registered handler ran it (only a kind the engine registers runs)
540
+ if (op.kind === 'resume' && (op.fulfilled ?? []).includes(d.id))
541
+ return true;
542
+ if (!isKnownDuty(d))
543
+ return false; // otherwise never, on any operation
544
+ if (d.kind === 'intro-ack')
545
+ return false; // only its handler's run ends it
546
+ // (f) the rotation's step 2 completed in this commit (the act's own path)
547
+ if (d.kind === 'rotation') {
548
+ const id = d.rotationId;
549
+ return !Object.hasOwn(after.rotations, id) && after.register.community !== null && before.register.community !== null &&
550
+ big(after.register.community.head) === big(before.register.community.head) + 1n;
551
+ }
552
+ const relId = d.relId;
553
+ const relGone = !Object.hasOwn(after.relationships, relId) || isHusk(after.relationships[relId]);
554
+ if (d.kind === 'merge')
555
+ return false;
556
+ if (relGone)
557
+ return true; // (b) departed, moved
558
+ if (d.kind === 'continuity') {
559
+ // (e) on receive, by the end of its own step only
560
+ if (op.kind !== 'receive')
561
+ return false;
562
+ const label = d.tuple;
563
+ const tb = tuplesByLabel(before).get(label), ta = tuplesByLabel(after).get(label);
564
+ if (!tb || !ta)
565
+ return false;
566
+ const ref = op.ref;
567
+ if (d.step === 'probe') {
568
+ // 6830: the ack that completes an in-flight probe, done set in this commit
569
+ if (tb.probeOut.done || !ta.probeOut.done || ref === undefined)
570
+ return false;
571
+ return Object.values(tb.probeOut.inflight).some((e) => e.digests.includes(ref) && e.digests.every((x) => x === ref || e.acked.includes(x)));
572
+ }
573
+ // 6850: the current issuance answered, aligned in this commit — by the
574
+ // ack of one of its own documents, or by a continuity mapping on the tuple
575
+ if (tb.continuityOut.aligned || !ta.continuityOut.aligned)
576
+ return false;
577
+ if (ref !== undefined)
578
+ return tb.continuityOut.sent.includes(ref) && before.outbox[ref]?.type === CONTINUITY_MAPPING_TYPE;
579
+ return op.type === CONTINUITY_MAPPING_TYPE && op.rkid === tb.rkid;
580
+ }
581
+ const cls = classOfDuty(d);
582
+ if (cls === null)
583
+ return false;
584
+ const e = entryOf(after, after.relationships[relId]);
585
+ if (e !== undefined && !classOnToward(e, cls))
586
+ return true; // (b) 6410
587
+ // (c) merged into one of its kind on the same relationship, urgency kept
588
+ const same = Object.values(after.duties).filter((x) => x.kind === d.kind && owedOn(x, relId, cls));
589
+ if (d.kind === 'reissue-mapping') {
590
+ const waits = d.awaitHead;
591
+ if (same.some((x) => dueOf(x) <= dueOf(d) && (waits !== null || x.awaitHead === null)))
592
+ return true;
593
+ }
594
+ if (d.kind === 'profile' && same.some((x) => BigInt(x.rev) >= BigInt(d.rev)))
595
+ return true;
596
+ // (d) the profile went out on the relationship in this commit: its
597
+ // revision rose there, or a profile duty owed there was fulfilled
598
+ if (cls === 'profile') {
599
+ const was = before.relationships[relId], now = after.relationships[relId];
600
+ const head = now.tuples.find((x) => x.ownPairLabel === now.activeHead);
601
+ const prior = was?.tuples.find((x) => x.ownPairLabel === now.activeHead);
602
+ if (head && prior && big(head.profileOut.rev) > big(prior.profileOut.rev))
603
+ return true;
604
+ if (op.kind === 'resume' && (op.fulfilled ?? []).some((id) => Object.hasOwn(before.duties, id) && owedOn(before.duties[id], relId, 'profile')))
605
+ return true;
606
+ }
607
+ // (d) the anchor mapping went out on the relationship in this commit
608
+ if (cls === 'anchorMapping') {
609
+ const was = before.relationships[relId], now = after.relationships[relId];
610
+ const head = now.tuples.find((x) => x.ownPairLabel === now.activeHead);
611
+ const prior = was?.tuples.find((x) => x.ownPairLabel === now.activeHead);
612
+ if (head && prior && big(head.mappingOut.rev) > big(prior.mappingOut.rev))
613
+ return true;
614
+ }
615
+ return false;
616
+ }
617
+ /** A star view renamed only (2100, 2140): the same anchor heads, members renamed. */
618
+ const renamedOnly = (a, b) => {
619
+ const heads = (x) => jcs(x.map((y) => y.head ?? null).sort());
620
+ return a.length === b.length && a.every((y) => y.head !== undefined) && heads(a) === heads(b);
621
+ };
622
+ /**
623
+ * One reconciled sequence's sender side over a commit (5110, 5130, 5660,
624
+ * 5670, 5680): lastSalt, hwm and exhausted never fall back; a salt is
625
+ * issued once, with a view whose digest is its viewDigest; what was
626
+ * committed under a salt (view, viewDigest, digests) stays — only `acked`
627
+ * grows, within the digests — except that a rename rewrites the members
628
+ * of the star's views (`renames`); an entry leaves only once completed at
629
+ * or below hwm or once none of its documents is in the outbox; the
630
+ * baseline (hwm, snapshot, snapshotDigest) changes only together, upward,
631
+ * by the ack (`ack`) that completes the in-flight entry of the new hwm,
632
+ * to that entry's view.
633
+ */
634
+ function seqOutTransition(v, where, b, a, after, ack, renames) {
635
+ if (big(a.lastSalt) < big(b.lastSalt))
636
+ v.push({ invariant: 'salts', detail: `${where}: lastSalt fell ${b.lastSalt} → ${a.lastSalt}` });
637
+ if (b.exhausted && !a.exhausted)
638
+ v.push({ invariant: 'salts', detail: `${where}: exhausted is sticky` });
639
+ if (big(a.hwm) < big(b.hwm))
640
+ v.push({ invariant: 'salts', detail: `${where}: hwm fell ${b.hwm} → ${a.hwm}` });
641
+ for (const [s, e] of Object.entries(a.inflight)) {
642
+ const was = Object.hasOwn(b.inflight, s) ? b.inflight[s] : undefined;
643
+ if (!was) {
644
+ if (big(s) <= big(b.lastSalt))
645
+ v.push({ invariant: 'salts', detail: `${where}: salt ${s} issued again (lastSalt was ${b.lastSalt})` });
646
+ if (e.viewDigest !== viewDigest(e.view))
647
+ v.push({ invariant: 'salts', detail: `${where}: salt ${s} issued with a viewDigest not of its view` });
648
+ if (e.acked.length)
649
+ v.push({ invariant: 'salts', detail: `${where}: salt ${s} issued already acked` });
650
+ continue;
651
+ }
652
+ if (jcs(e.digests) !== jcs(was.digests))
653
+ v.push({ invariant: 'salts', detail: `${where}: the documents of salt ${s} changed` });
654
+ if (jcs(e.view) !== jcs(was.view) || e.viewDigest !== was.viewDigest) {
655
+ if (!(renames && renamedOnly(e.view, was.view) && e.viewDigest === viewDigest(e.view)))
656
+ v.push({ invariant: 'salts', detail: `${where}: the view of salt ${s} changed` });
657
+ }
658
+ if (was.acked.some((x) => !e.acked.includes(x)) || e.acked.some((x) => !e.digests.includes(x)))
659
+ v.push({ invariant: 'salts', detail: `${where}: acked of salt ${s} shrank or left its digests` });
660
+ }
661
+ for (const [s, e] of Object.entries(b.inflight)) {
662
+ if (Object.hasOwn(a.inflight, s))
663
+ continue;
664
+ if (big(s) > big(a.hwm) && e.digests.some((x) => Object.hasOwn(after.outbox, x)))
665
+ v.push({ invariant: 'salts', detail: `${where}: salt ${s} left in flight above hwm ${a.hwm} with documents still in the outbox` });
666
+ }
667
+ const baseline = a.hwm !== b.hwm || jcs(a.snapshot) !== jcs(b.snapshot) || a.snapshotDigest !== b.snapshotDigest;
668
+ if (!baseline)
669
+ return;
670
+ if (a.hwm === b.hwm) {
671
+ // only a rename rewrites the baseline's members, never its heads (2100)
672
+ if (!(renames && a.snapshot !== null && b.snapshot !== null && renamedOnly(a.snapshot, b.snapshot) && a.snapshotDigest === viewDigest(a.snapshot))) {
673
+ v.push({ invariant: 'baseline', detail: `${where}: the baseline changed at hwm ${a.hwm} without a completion (5670, 5680)` });
674
+ }
675
+ return;
676
+ }
677
+ const e = Object.hasOwn(b.inflight, a.hwm) ? b.inflight[a.hwm] : undefined;
678
+ if (!e || ack === undefined || !e.digests.includes(ack) || e.digests.some((x) => x !== ack && !e.acked.includes(x))) {
679
+ v.push({ invariant: 'baseline', detail: `${where}: hwm ${b.hwm} → ${a.hwm} without the ack that completes salt ${a.hwm} (5660)` });
680
+ }
681
+ else if (jcs(a.snapshot) !== jcs(e.view) || a.snapshotDigest !== e.viewDigest) {
682
+ v.push({ invariant: 'baseline', detail: `${where}: the baseline at hwm ${a.hwm} is not the view sent under it (5670)` });
683
+ }
684
+ }
685
+ /** The probe's sender side over a commit (6680, 6830): seq never falls; a
686
+ * probe's documents stay, `acked` grows within them; `done` is sticky and
687
+ * set only by the ack completing an in-flight probe, which empties the
688
+ * in-flight set; otherwise an entry leaves only with its documents gone
689
+ * from the outbox. */
690
+ function probeTransition(v, where, b, a, after, ack) {
691
+ if (big(a.seq) < big(b.seq))
692
+ v.push({ invariant: 'salts', detail: `${where}: probe seq fell` });
693
+ if (b.done && !a.done)
694
+ v.push({ invariant: 'baseline', detail: `${where}: probe done is sticky` });
695
+ const completed = !b.done && a.done;
696
+ if (completed && !Object.values(b.inflight).some((e) => ack !== undefined && e.digests.includes(ack) && e.digests.every((x) => x === ack || e.acked.includes(x)))) {
697
+ v.push({ invariant: 'baseline', detail: `${where}: probe done without the ack completing one (6830)` });
698
+ }
699
+ for (const [s, e] of Object.entries(a.inflight)) {
700
+ const was = Object.hasOwn(b.inflight, s) ? b.inflight[s] : undefined;
701
+ if (!was) {
702
+ if (big(s) <= big(b.seq))
703
+ v.push({ invariant: 'salts', detail: `${where}: probe ${s} issued again` });
704
+ continue;
705
+ }
706
+ if (jcs(e.digests) !== jcs(was.digests))
707
+ v.push({ invariant: 'salts', detail: `${where}: the documents of probe ${s} changed` });
708
+ if (was.acked.some((x) => !e.acked.includes(x)) || e.acked.some((x) => !e.digests.includes(x)))
709
+ v.push({ invariant: 'salts', detail: `${where}: acked of probe ${s} shrank or left its digests` });
710
+ }
711
+ if (completed)
712
+ return;
713
+ for (const [s, e] of Object.entries(b.inflight)) {
714
+ if (!Object.hasOwn(a.inflight, s) && e.digests.some((x) => Object.hasOwn(after.outbox, x)))
715
+ v.push({ invariant: 'salts', detail: `${where}: probe ${s} left in flight with documents still in the outbox` });
716
+ }
717
+ }
718
+ function seqInTransition(v, where, b, a) {
719
+ if (big(a.hwm) < big(b.hwm))
720
+ v.push({ invariant: 'salts', detail: `${where}: received hwm fell ${b.hwm} → ${a.hwm}` });
721
+ if (big(a.lowerBound) < big(b.lowerBound))
722
+ v.push({ invariant: 'salts', detail: `${where}: lowerBound fell ${b.lowerBound} → ${a.lowerBound}` });
723
+ }
724
+ /** The invariants of one commit from `before` to `after` by `op` (see the header). */
725
+ export function transitionViolations(before, after, op) {
726
+ const v = [];
727
+ // the encounter (5.3, 5.5, 5.6): records only grow and a credential once
728
+ // held in a direction stays; a challenge leaves only into a record or aged
729
+ for (const [own, r] of Object.entries(before.encounter.records)) {
730
+ const a = after.encounter.records[own];
731
+ if (!a) {
732
+ v.push({ invariant: 'encounter', detail: `record ${own} removed` });
733
+ continue;
734
+ }
735
+ for (const dir of ['in', 'out']) {
736
+ if (r[dir] !== null && (a[dir] === null || a[dir].digest !== r[dir].digest))
737
+ v.push({ invariant: 'encounter', detail: `record ${own}: the ${dir} credential changed (5.6 step 8, 5660)` });
738
+ }
739
+ }
740
+ for (const [value, c] of Object.entries(before.encounter.challenges)) {
741
+ if (Object.hasOwn(after.encounter.challenges, value) || Object.hasOwn(after.encounter.records, value))
742
+ continue;
743
+ if (!challengeAged(c.tCh, after.lastWhen))
744
+ v.push({ invariant: 'encounter', detail: `challenge ${value} left neither recorded nor aged (5290)` });
745
+ }
746
+ // introduction acts leave only past their absolute lifetime (8310)
747
+ for (const [k, a] of Object.entries(before.encounter.introductions)) {
748
+ if (!Object.hasOwn(after.encounter.introductions, k) && after.lastWhen < a.issuedAt + ACT_EXPIRY + ACT_SKEW)
749
+ v.push({ invariant: 'introductions', detail: `${k} discarded before its lifetime ended` });
750
+ }
751
+ // the record of surfaced invitations and the register of declined ones
752
+ // only grow, for the life of the holder (RLTP-MT-3130, RLTP-MSG-6310)
753
+ for (const [what, b, a] of [['surfaced', before.joining.surfaced, after.joining.surfaced], ['declined', before.joining.declined, after.joining.declined]]) {
754
+ const gone = superset(a, b);
755
+ if (gone.length)
756
+ v.push({ invariant: 'messages', detail: `joining.${what} lost ${jcs(gone)}` });
757
+ }
758
+ // the completion record (Message Tasks 4.4): an entry never changes its
759
+ // body digest and leaves only by tick (4340); the inbox (4.5): an entry
760
+ // arises only in a unique receive, with its record entry, and leaves only
761
+ // by the application's take-over (4420)
762
+ for (const [label, rec] of Object.entries(before.messages.records)) {
763
+ for (const [mid, e] of Object.entries(rec)) {
764
+ const x = after.messages.records[label]?.[mid];
765
+ if (x === undefined) {
766
+ // only tick, after sentAt + H on the mark it writes, or with its tuple deactivated or gone (4340)
767
+ const t = Object.values(after.relationships).flatMap((r) => r.tuples).find((u) => u.ownPairLabel === label);
768
+ if (op.kind !== 'tick')
769
+ v.push({ invariant: 'messages', detail: `record ${label} ${mid} deleted by ${op.kind}` });
770
+ else if (!(e.sentAt + MESSAGE_LIFETIME_MS < after.lastWhen) && t !== undefined && !t.deactivated)
771
+ v.push({ invariant: 'messages', detail: `record ${label} ${mid} deleted before sentAt + H, its tuple live` });
772
+ continue;
773
+ }
774
+ if (x.bodyDigest !== e.bodyDigest || x.sentAt !== e.sentAt)
775
+ v.push({ invariant: 'messages', detail: `record ${label} ${mid} changed` });
776
+ }
777
+ }
778
+ for (const [id, e] of Object.entries(after.messages.inbox)) {
779
+ if (Object.hasOwn(before.messages.inbox, id))
780
+ continue;
781
+ if (op.kind !== 'receive' || op.disposition !== 'unique' || op.digest !== id)
782
+ v.push({ invariant: 'messages', detail: `inbox entry ${id} arose by ${op.kind}` });
783
+ if (after.messages.records[e.tuple]?.[e.mid] === undefined)
784
+ v.push({ invariant: 'messages', detail: `inbox entry ${id} without its record entry` });
785
+ }
786
+ for (const id of Object.keys(before.messages.inbox)) {
787
+ if (!Object.hasOwn(after.messages.inbox, id) && !(op.kind === 'act' && op.command === 'message.take'))
788
+ v.push({ invariant: 'messages', detail: `inbox entry ${id} left by ${op.kind}` });
789
+ }
790
+ // a block changes only by the block act, a merge (6030) or a departure
791
+ // that removes its relationship
792
+ for (const r of before.messages.blocked) {
793
+ if (!after.messages.blocked.includes(r) && Object.hasOwn(after.relationships, r) && !(op.kind === 'act' && op.command === 'message.block'))
794
+ v.push({ invariant: 'messages', detail: `the block of ${r} cleared by ${op.kind}` });
795
+ }
796
+ for (const r of after.messages.blocked) {
797
+ if (before.messages.blocked.includes(r) || (op.kind === 'act' && (op.command === 'message.block' || op.command === 'trust.merge')) || op.kind === 'resume')
798
+ continue;
799
+ // a chaining: the chain takes the block of the fresh relationship it absorbed (6030)
800
+ const absorbed = Object.values(after.relationships).some((h) => h.chainedInto === r && before.relationships[h.id]?.chainedInto === null && before.messages.blocked.includes(h.id));
801
+ if (!absorbed)
802
+ v.push({ invariant: 'messages', detail: `the block of ${r} set by ${op.kind}` });
803
+ }
804
+ // a member stays on the holder's roster (removal is not built)
805
+ for (const [id, g] of Object.entries(before.groups)) {
806
+ const ga = after.groups[id];
807
+ if (!ga) {
808
+ v.push({ invariant: 'groups', detail: `group ${id} left` });
809
+ continue;
810
+ }
811
+ for (const m of Object.keys(g.roster))
812
+ if (!Object.hasOwn(ga.roster, m))
813
+ v.push({ invariant: 'groups', detail: `${id}: member ${m} left the roster` });
814
+ }
815
+ if (big(after.commitSeq) !== big(before.commitSeq) + 1n)
816
+ v.push({ invariant: 'commitSeq', detail: `commitSeq ${before.commitSeq} → ${after.commitSeq}` });
817
+ if (after.lastWhen < before.lastWhen)
818
+ v.push({ invariant: 'commitSeq', detail: `lastWhen fell ${before.lastWhen} → ${after.lastWhen}` });
819
+ // the register (Identity §2): monotone sets
820
+ const rb = before.register, ra = after.register;
821
+ const lost = (what, xs) => { if (xs.length)
822
+ v.push({ invariant: 'register', detail: `${what} lost ${jcs(xs)}` }); };
823
+ lost('labels', superset(ra.labels, rb.labels));
824
+ lost('pairNonces', Object.keys(rb.pairNonces).filter((k) => ra.pairNonces[k] !== rb.pairNonces[k]));
825
+ for (const [k, list] of Object.entries(rb.carriers))
826
+ lost(`carriers ${k}`, superset((ra.carriers[k] ?? []).map((x) => jcs(x)), list.map((x) => jcs(x))));
827
+ lost('device names', superset(ra.devices.names, rb.devices.names));
828
+ lost('device revocations', superset(ra.devices.revoked, rb.devices.revoked));
829
+ if (rb.community !== null && (ra.community === null || ra.community.genesis !== rb.community.genesis || big(ra.community.head) < big(rb.community.head))) {
830
+ v.push({ invariant: 'register', detail: 'the community lost its genesis or its head fell' });
831
+ }
832
+ if (ra.lineage.length < rb.lineage.length || jcs(ra.lineage.slice(0, rb.lineage.length)) !== jcs(rb.lineage))
833
+ v.push({ invariant: 'register', detail: 'the lineage did not only extend' });
834
+ // a revoked name is never given again (Identity §5.5)
835
+ // a revoked name is never given again (§5.5): no device.added names one
836
+ if (op.kind === 'act') {
837
+ for (const e of op.events ?? [])
838
+ if (e.type === 'device.added' && rb.devices.revoked.includes(e.name))
839
+ v.push({ invariant: 'register', detail: `device name ${e.name} given again after its revocation` });
840
+ }
841
+ // the head moves by one generation at a time, with one lineage element,
842
+ // onto a label persisted in an earlier commit (6360, Identity §5.4)
843
+ if (rb.community !== null && ra.community !== null && ra.community.head !== rb.community.head) {
844
+ const g = big(ra.community.head);
845
+ if (g !== big(rb.community.head) + 1n || ra.lineage.length !== rb.lineage.length + 1)
846
+ v.push({ invariant: 'register', detail: `the head moved ${rb.community.head} → ${ra.community.head} with ${ra.lineage.length - rb.lineage.length} lineage elements` });
847
+ if (!rb.labels.includes(communityLabel(ra.community.genesis, Number(g))))
848
+ v.push({ invariant: 'register', detail: `the head moved to generation ${ra.community.head} whose label was not persisted before (6360)` });
849
+ }
850
+ // the own profile's one counter never falls, and its content changes only with it (6450)
851
+ if (big(after.profile.revision) < big(before.profile.revision))
852
+ v.push({ invariant: 'profile', detail: `the profile revision fell ${before.profile.revision} → ${after.profile.revision}` });
853
+ else if (after.profile.revision === before.profile.revision && jcs(after.profile.body) !== jcs(before.profile.body))
854
+ v.push({ invariant: 'profile', detail: `the profile changed under revision ${after.profile.revision}` });
855
+ // addresses (Delivery §5)
856
+ lost('tombstones', superset(after.addresses.tombstones, before.addresses.tombstones));
857
+ for (const k of Object.keys(before.addresses.keys)) {
858
+ if (!Object.hasOwn(after.addresses.keys, k) && !after.addresses.tombstones.includes(k) && !before.addresses.keys[k].label.startsWith('group/'))
859
+ v.push({ invariant: 'addresses', detail: `${k} left without a tombstone` });
860
+ }
861
+ // salts, baselines and revisions, per tuple label; the acked document
862
+ // of a delivery-ack, and whether the operation may rename (2100, 2140)
863
+ const ack = op.kind === 'receive' ? op.ref : undefined;
864
+ const renames = op.kind === 'receive' || op.kind === 'resume' || (op.kind === 'act' && (op.command === 'trust.merge' || op.command === 'trust.depart'));
865
+ const tb = tuplesByLabel(before);
866
+ for (const [label, a] of tuplesByLabel(after)) {
867
+ const b = tb.get(label);
868
+ if (!b)
869
+ continue;
870
+ seqOutTransition(v, `${label} starOut`, b.starOut, a.starOut, after, ack, renames);
871
+ seqOutTransition(v, `${label} groupStarOut`, b.groupStarOut, a.groupStarOut, after, ack, false);
872
+ probeTransition(v, `${label} probeOut`, b.probeOut, a.probeOut, after, ack);
873
+ seqInTransition(v, `${label} starIn`, b.starIn, a.starIn);
874
+ seqInTransition(v, `${label} groupStarIn`, b.groupStarIn, a.groupStarIn);
875
+ seqInTransition(v, `${label} probeIn`, b.probeIn, a.probeIn);
876
+ if (big(a.mappingOut.rev) < big(b.mappingOut.rev))
877
+ v.push({ invariant: 'salts', detail: `${label}: mapping revision fell` });
878
+ if (big(a.continuityOut.rev) < big(b.continuityOut.rev))
879
+ v.push({ invariant: 'salts', detail: `${label}: continuity revision fell` });
880
+ if (big(a.profileOut.rev) < big(b.profileOut.rev))
881
+ v.push({ invariant: 'salts', detail: `${label}: profile revision issued fell` });
882
+ if (big(a.profileOut.rev) > big(after.profile.revision))
883
+ v.push({ invariant: 'salts', detail: `${label}: profile revision ${a.profileOut.rev} issued above the counter ${after.profile.revision} (6450)` });
884
+ if (big(a.profileIn.rev) < big(b.profileIn.rev))
885
+ v.push({ invariant: 'salts', detail: `${label}: the held profile revision fell (6460)` });
886
+ }
887
+ // a new tuple starts with fresh per-tuple state (6250): nothing migrates into it
888
+ for (const r of Object.values(after.relationships)) {
889
+ for (const t of r.tuples) {
890
+ if (tb.has(t.ownPairLabel))
891
+ continue;
892
+ const stale = freshViolations(t);
893
+ if (stale.length)
894
+ v.push({ invariant: 'tuples', detail: `${t.ownPairLabel}: a new tuple carries ${stale.join(', ')} (6250)` });
895
+ }
896
+ }
897
+ // entry positions (2120, 2190) and the creation order of active heads (2180)
898
+ positionViolations(v, before, after);
899
+ headsViolations(v, before, after);
900
+ // bindings and held heads, per relationship (6610, 6150)
901
+ for (const [id, a] of Object.entries(after.relationships)) {
902
+ if (!Object.hasOwn(before.relationships, id) || isHusk(a))
903
+ continue;
904
+ const b = before.relationships[id];
905
+ const chained = a.tuples.length > b.tuples.length;
906
+ if (chained)
907
+ continue;
908
+ for (const [anchor, list] of Object.entries(a.bindings)) {
909
+ if (list.length > 1 && list.length > (b.bindings[anchor]?.length ?? 0))
910
+ v.push({ invariant: 'bindings', detail: `${id}: a second binding for ${anchor} without a chaining` });
911
+ }
912
+ if (b.heldCounterpartHead !== null && a.heldCounterpartHead !== b.heldCounterpartHead &&
913
+ (a.heldCounterpartHead === null || jcs(newestOf([b.heldCounterpartHead, a.heldCounterpartHead], a.links)) !== jcs([a.heldCounterpartHead]))) {
914
+ v.push({ invariant: 'bindings', detail: `${id}: the held head moved ${b.heldCounterpartHead} → ${a.heldCounterpartHead} off its links` });
915
+ }
916
+ }
917
+ // the outbox (Delivery §6.1)
918
+ for (const [digest, it] of Object.entries(after.outbox)) {
919
+ const was = Object.hasOwn(before.outbox, digest) ? before.outbox[digest] : undefined;
920
+ if (!was) {
921
+ if (it.history.length !== 1 || it.history[0].status !== 'submitted')
922
+ v.push({ invariant: 'outbox', detail: `${digest}: a new item starts submitted` });
923
+ continue;
924
+ }
925
+ const n = (x) => x.history.filter((s) => s.status === 'accepted').length;
926
+ if (n(it) > n(was) && op.kind !== 'report')
927
+ v.push({ invariant: 'outbox', detail: `${digest}: accepted without a report (${op.kind})` });
928
+ if (triasOf(was) === 'delivered' && triasOf(it) !== 'delivered')
929
+ v.push({ invariant: 'outbox', detail: `${digest}: stepped back from delivered` });
930
+ }
931
+ // the effect cache (Delivery 4.2)
932
+ const added = Object.keys(after.effects).filter((d) => !Object.hasOwn(before.effects, d));
933
+ if (added.length && !(op.kind === 'receive' && op.disposition === 'unique' && added.length === 1 && (op.digest === undefined || added[0] === op.digest))) {
934
+ v.push({ invariant: 'effects', detail: `${jcs(added)} entered the cache by ${op.kind}${op.kind === 'receive' ? ` (${op.disposition})` : ''}` });
935
+ }
936
+ if (op.kind !== 'tick') {
937
+ const gone = Object.keys(before.effects).filter((d) => !Object.hasOwn(after.effects, d));
938
+ if (gone.length)
939
+ v.push({ invariant: 'effects', detail: `${jcs(gone)} left the cache by ${op.kind}` });
940
+ const acts = Object.keys(before.acts).filter((a) => !Object.hasOwn(after.acts, a));
941
+ if (acts.length)
942
+ v.push({ invariant: 'journal', detail: `${jcs(acts)} left the journal by ${op.kind}` });
943
+ }
944
+ // duties
945
+ for (const d of Object.values(before.duties)) {
946
+ if (!Object.hasOwn(after.duties, d.id) && !dutyDrops(before, after, d, op))
947
+ v.push({ invariant: 'duties', detail: `${d.id} (${d.kind}) disappeared by ${op.kind}${op.kind === 'act' ? ' ' + op.command : ''} without a rule` });
948
+ }
949
+ return v;
950
+ }
951
+ /** Both: the commit's invariants and those of the state it leaves. */
952
+ export const commitViolations = (before, after, op) => [...transitionViolations(before, after, op), ...stateViolations(after)];
953
+ //# sourceMappingURL=invariants.js.map