@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.
- package/CHANGELOG.md +97 -0
- package/README.md +209 -78
- package/dist/ack.d.ts +84 -0
- package/dist/ack.d.ts.map +1 -0
- package/dist/ack.js +114 -0
- package/dist/ack.js.map +1 -0
- package/dist/adapters/vti-mediator.d.ts +169 -0
- package/dist/adapters/vti-mediator.d.ts.map +1 -0
- package/dist/adapters/vti-mediator.js +659 -0
- package/dist/adapters/vti-mediator.js.map +1 -0
- package/dist/anchor.d.ts +226 -0
- package/dist/anchor.d.ts.map +1 -0
- package/dist/anchor.js +347 -0
- package/dist/anchor.js.map +1 -0
- package/dist/{holder.d.ts → carrier-holder.d.ts} +1 -1
- package/dist/carrier-holder.d.ts.map +1 -0
- package/dist/{holder.js → carrier-holder.js} +5 -4
- package/dist/carrier-holder.js.map +1 -0
- package/dist/carrier-identity.js +4 -4
- package/dist/carrier-identity.js.map +1 -1
- package/dist/carrier.d.ts.map +1 -1
- package/dist/carrier.js +4 -3
- package/dist/carrier.js.map +1 -1
- package/dist/ceremony.d.ts +3 -2
- package/dist/ceremony.d.ts.map +1 -1
- package/dist/ceremony.js +16 -26
- package/dist/ceremony.js.map +1 -1
- package/dist/codec.d.ts +54 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +494 -0
- package/dist/codec.js.map +1 -0
- package/dist/continuity.d.ts +67 -0
- package/dist/continuity.d.ts.map +1 -0
- package/dist/continuity.js +110 -0
- package/dist/continuity.js.map +1 -0
- package/dist/core.d.ts +4 -0
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js.map +1 -1
- package/dist/crypto.d.ts +23 -7
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +54 -20
- package/dist/crypto.js.map +1 -1
- package/dist/delivery.d.ts +31 -3
- package/dist/delivery.d.ts.map +1 -1
- package/dist/delivery.js +75 -28
- package/dist/delivery.js.map +1 -1
- package/dist/encounter/ceremony.d.ts +37 -0
- package/dist/encounter/ceremony.d.ts.map +1 -0
- package/dist/encounter/ceremony.js +662 -0
- package/dist/encounter/ceremony.js.map +1 -0
- package/dist/encounter.d.ts +91 -1
- package/dist/encounter.d.ts.map +1 -1
- package/dist/encounter.js +191 -12
- package/dist/encounter.js.map +1 -1
- package/dist/engine.d.ts +303 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1509 -0
- package/dist/engine.js.map +1 -0
- package/dist/entropy.d.ts +28 -0
- package/dist/entropy.d.ts.map +1 -0
- package/dist/entropy.js +96 -0
- package/dist/entropy.js.map +1 -0
- package/dist/events.d.ts +198 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -0
- package/dist/fixtures.d.ts +30 -0
- package/dist/fixtures.d.ts.map +1 -0
- package/dist/fixtures.js +155 -0
- package/dist/fixtures.js.map +1 -0
- package/dist/forge.d.ts +47 -0
- package/dist/forge.d.ts.map +1 -0
- package/dist/forge.js +20 -0
- package/dist/forge.js.map +1 -0
- package/dist/group-star.d.ts +251 -0
- package/dist/group-star.d.ts.map +1 -0
- package/dist/group-star.js +348 -0
- package/dist/group-star.js.map +1 -0
- package/dist/holder-api.d.ts +165 -0
- package/dist/holder-api.d.ts.map +1 -0
- package/dist/holder-api.js +142 -0
- package/dist/holder-api.js.map +1 -0
- package/dist/holder-state-v1.d.ts +801 -0
- package/dist/holder-state-v1.d.ts.map +1 -0
- package/dist/holder-state-v1.js +2 -0
- package/dist/holder-state-v1.js.map +1 -0
- package/dist/holder-state-v2.d.ts +858 -0
- package/dist/holder-state-v2.d.ts.map +1 -0
- package/dist/holder-state-v2.js +2 -0
- package/dist/holder-state-v2.js.map +1 -0
- package/dist/identity-state.d.ts +43 -0
- package/dist/identity-state.d.ts.map +1 -0
- package/dist/identity-state.js +345 -0
- package/dist/identity-state.js.map +1 -0
- package/dist/identity.d.ts +24 -2
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +70 -11
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +81 -29
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +37 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +22 -0
- package/dist/internal.js.map +1 -0
- package/dist/introduction/holder.d.ts +18 -0
- package/dist/introduction/holder.d.ts.map +1 -0
- package/dist/introduction/holder.js +764 -0
- package/dist/introduction/holder.js.map +1 -0
- package/dist/introduction.d.ts +101 -0
- package/dist/introduction.d.ts.map +1 -0
- package/dist/introduction.js +132 -0
- package/dist/introduction.js.map +1 -0
- package/dist/invariants.d.ts +43 -0
- package/dist/invariants.d.ts.map +1 -0
- package/dist/invariants.js +953 -0
- package/dist/invariants.js.map +1 -0
- package/dist/keys-seed.d.ts +7 -0
- package/dist/keys-seed.d.ts.map +1 -0
- package/dist/keys-seed.js +133 -0
- package/dist/keys-seed.js.map +1 -0
- package/dist/log-memory.d.ts +12 -0
- package/dist/log-memory.d.ts.map +1 -0
- package/dist/log-memory.js +101 -0
- package/dist/log-memory.js.map +1 -0
- package/dist/membership/holder.d.ts +33 -0
- package/dist/membership/holder.d.ts.map +1 -0
- package/dist/membership/holder.js +929 -0
- package/dist/membership/holder.js.map +1 -0
- package/dist/membership.d.ts +123 -0
- package/dist/membership.d.ts.map +1 -0
- package/dist/membership.js +180 -0
- package/dist/membership.js.map +1 -0
- package/dist/message/holder.d.ts +49 -0
- package/dist/message/holder.d.ts.map +1 -0
- package/dist/message/holder.js +290 -0
- package/dist/message/holder.js.map +1 -0
- package/dist/message.d.ts +43 -0
- package/dist/message.d.ts.map +1 -0
- package/dist/message.js +51 -0
- package/dist/message.js.map +1 -0
- package/dist/ports.d.ts +237 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/ports.js +2 -0
- package/dist/ports.js.map +1 -0
- package/dist/profile.d.ts +56 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +193 -0
- package/dist/profile.js.map +1 -0
- package/dist/prompts.d.ts +14 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +69 -0
- package/dist/prompts.js.map +1 -0
- package/dist/receive.d.ts +134 -0
- package/dist/receive.d.ts.map +1 -0
- package/dist/receive.js +317 -0
- package/dist/receive.js.map +1 -0
- package/dist/results.d.ts +173 -0
- package/dist/results.d.ts.map +1 -0
- package/dist/results.js +21 -0
- package/dist/results.js.map +1 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +45 -38
- package/dist/schemas.js.map +1 -1
- package/dist/sha256.d.ts +3 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +69 -0
- package/dist/sha256.js.map +1 -0
- package/dist/star.d.ts +31 -0
- package/dist/star.d.ts.map +1 -0
- package/dist/star.js +56 -0
- package/dist/star.js.map +1 -0
- package/dist/state-builder.d.ts +42 -0
- package/dist/state-builder.d.ts.map +1 -0
- package/dist/state-builder.js +100 -0
- package/dist/state-builder.js.map +1 -0
- package/dist/state.d.ts +914 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +37 -0
- package/dist/state.js.map +1 -0
- package/dist/store-contract.d.ts +11 -0
- package/dist/store-contract.d.ts.map +1 -0
- package/dist/store-contract.js +189 -0
- package/dist/store-contract.js.map +1 -0
- package/dist/store-memory.d.ts +8 -0
- package/dist/store-memory.d.ts.map +1 -0
- package/dist/store-memory.js +17 -0
- package/dist/store-memory.js.map +1 -0
- package/dist/testing.d.ts +22 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +21 -0
- package/dist/testing.js.map +1 -0
- package/dist/trust/continuity.d.ts +71 -0
- package/dist/trust/continuity.d.ts.map +1 -0
- package/dist/trust/continuity.js +1222 -0
- package/dist/trust/continuity.js.map +1 -0
- package/dist/trust/duties.d.ts +67 -0
- package/dist/trust/duties.d.ts.map +1 -0
- package/dist/trust/duties.js +179 -0
- package/dist/trust/duties.js.map +1 -0
- package/dist/trust/entries.d.ts +139 -0
- package/dist/trust/entries.d.ts.map +1 -0
- package/dist/trust/entries.js +795 -0
- package/dist/trust/entries.js.map +1 -0
- package/dist/trust/group-star.d.ts +43 -0
- package/dist/trust/group-star.d.ts.map +1 -0
- package/dist/trust/group-star.js +188 -0
- package/dist/trust/group-star.js.map +1 -0
- package/dist/trust/mapping.d.ts +56 -0
- package/dist/trust/mapping.d.ts.map +1 -0
- package/dist/trust/mapping.js +584 -0
- package/dist/trust/mapping.js.map +1 -0
- package/dist/trust/profile.d.ts +25 -0
- package/dist/trust/profile.d.ts.map +1 -0
- package/dist/trust/profile.js +249 -0
- package/dist/trust/profile.js.map +1 -0
- package/dist/trust/star.d.ts +208 -0
- package/dist/trust/star.d.ts.map +1 -0
- package/dist/trust/star.js +588 -0
- package/dist/trust/star.js.map +1 -0
- package/dist/trust/state.d.ts +83 -0
- package/dist/trust/state.d.ts.map +1 -0
- package/dist/trust/state.js +154 -0
- package/dist/trust/state.js.map +1 -0
- package/dist/util-freeze.d.ts +3 -0
- package/dist/util-freeze.d.ts.map +1 -0
- package/dist/util-freeze.js +11 -0
- package/dist/util-freeze.js.map +1 -0
- package/dist/views.d.ts +179 -0
- package/dist/views.d.ts.map +1 -0
- package/dist/views.js +286 -0
- package/dist/views.js.map +1 -0
- package/dist/visibility.d.ts +5 -5
- package/dist/visibility.d.ts.map +1 -1
- package/dist/visibility.js +22 -28
- package/dist/visibility.js.map +1 -1
- package/dist/wire.d.ts +218 -83
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js.map +1 -1
- package/package.json +60 -10
- package/schemas/holder-state-v1.schema.json +3880 -0
- package/schemas/holder-state-v2.schema.json +4123 -0
- package/dist/holder.d.ts.map +0 -1
- package/dist/holder.js.map +0 -1
- package/dist/probe/acks.d.ts +0 -27
- package/dist/probe/acks.d.ts.map +0 -1
- package/dist/probe/acks.js +0 -152
- package/dist/probe/acks.js.map +0 -1
- package/dist/probe/continuity.d.ts +0 -20
- package/dist/probe/continuity.d.ts.map +0 -1
- package/dist/probe/continuity.js +0 -805
- package/dist/probe/continuity.js.map +0 -1
- package/dist/probe/deps.d.ts +0 -61
- package/dist/probe/deps.d.ts.map +0 -1
- package/dist/probe/deps.js +0 -54
- package/dist/probe/deps.js.map +0 -1
- package/dist/probe/encounter.d.ts +0 -26
- package/dist/probe/encounter.d.ts.map +0 -1
- package/dist/probe/encounter.js +0 -938
- package/dist/probe/encounter.js.map +0 -1
- package/dist/probe/introduce.d.ts +0 -62
- package/dist/probe/introduce.d.ts.map +0 -1
- package/dist/probe/introduce.js +0 -306
- package/dist/probe/introduce.js.map +0 -1
- package/dist/probe/membership.d.ts +0 -96
- package/dist/probe/membership.d.ts.map +0 -1
- package/dist/probe/membership.js +0 -467
- package/dist/probe/membership.js.map +0 -1
- package/dist/probe/trust.d.ts +0 -156
- package/dist/probe/trust.d.ts.map +0 -1
- package/dist/probe/trust.js +0 -1233
- package/dist/probe/trust.js.map +0 -1
- package/dist/probe.d.ts +0 -7
- package/dist/probe.d.ts.map +0 -1
- package/dist/probe.js +0 -42
- 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
|