@real-life/trust-protocol 0.3.1 → 0.4.0

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