@serfab/quereus-plugin-sereus 0.12.0 → 1.0.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.
@@ -1,248 +1,458 @@
1
- import type { NodeOptions } from '@optimystic/db-p2p';
2
-
3
- /**
4
- * Smallest cluster size Optimystic will honour — its own `minAbsoluteClusterSize`,
5
- * and the smallest value that reaches the cluster path at all (a lone node writes to
6
- * local storage without forming a cluster). A validation floor, not a default.
7
- */
8
- export const MIN_CLUSTER_SIZE = 2;
9
-
10
- /**
11
- * How many nodes each block of the **control** (cadre membership) database is replicated
12
- * to. Chosen to exceed any party's node count, so in practice every control block lands
13
- * on every member of the party.
14
- *
15
- * **Why full replication here.** Every control node reads the whole control database —
16
- * membership, peer addresses, the strand list — so a member left out of a block's cohort is
17
- * a member that may never learn the fact, and at a cohort of two the *read repair* that is
18
- * supposed to make that safe cannot converge (it can ask exactly one peer, and that peer may
19
- * be the member that also missed the write). Measured: 4 failures in 10 runs of the
20
- * control-DB replication scenario at size 2, 0 in 20 at size 3 and 0 in 10 at size 8.
21
- *
22
- * **Why this number, and why a constant.** Optimystic caps a cohort at the peers that
23
- * actually serve the network and downsizes a cohort it cannot fill (`allowDownsize: true`,
24
- * which Cadre passes), so any value at or above the party's node count yields the same
25
- * behaviour: cohort = whole party. 16 is roughly twice the largest deployment the product
26
- * documents (`docs/architecture.md` → "Enterprise (Multi-Node Mixed)", 7 nodes) — headroom
27
- * without pretending to support arbitrarily large parties; a party genuinely running more
28
- * than 16 nodes is back to partial replication for its control database and should raise
29
- * this. It is a constant rather than the live member count because Optimystic freezes the
30
- * value at libp2p-node construction, before the `ControlDatabase` holding the `CadrePeer`
31
- * rows exists, and because a per-node derivation lets two members disagree.
32
- *
33
- * A cadre is one party by construction, so the owner-blind cohort-selection question that
34
- * applies to strand networks (see {@link DEFAULT_STRAND_CLUSTER_SIZE} below) is not a question
35
- * here: "replicate to the whole party" already is the whole intent, and there is no party
36
- * diversity for this breadth to buy or fail to buy.
37
- *
38
- * Canonical explanation of all of the above — the read-repair mechanics, what whole-party
39
- * breadth does and does not remove, and what the wider cohort costs in write availability —
40
- * is `docs/architecture.md` → "Replication cluster size". Keep it there, not here.
41
- *
42
- * **Deliberately not `clusterPolicy.assumedClusterSize`.** That option means "the smallest
43
- * cohort this deployment can genuinely field", and it feeds both the membership admission
44
- * gate's low-confidence path *and* the read-repair/reconcile corroboration floor. A Cadre
45
- * party legitimately runs one or two nodes, so Cadre leaves it at Optimystic's default of 2;
46
- * asserting 16 there would make the admission gate demand `ceil(0.75 x 16) = 12` declared
47
- * peers and refuse every real party's writes.
48
- *
49
- * NOTE: cohort selection overfetches proportionally to this number —
50
- * `Libp2pKeyPeerNetwork.membershipOverfetch` asks FRET for
51
- * `max(clusterSize * 4, clusterSize + 16)` candidates and does a peerStore protocol lookup
52
- * per candidate, so 16 requests a 64-peer proximity band. Free today, because the result is
53
- * bounded by the peers FRET actually knows (2 in a 3-node party). If control-network cohort
54
- * selection ever shows up as slow on a large mesh, look here first.
55
- */
56
- export const CONTROL_REPLICATION_BREADTH = 16;
57
-
58
- /**
59
- * Cluster consensus policy every CONTROL-network libp2p node runs — production
60
- * (`CadreNode.buildControlNodeOptions`) and the integration harness alike. One definition so
61
- * the two cannot drift: the object literal used to be hand-copied at both sites, and the copy
62
- * at the harness had picked up a `superMajorityThreshold` override production never had.
63
- *
64
- * `superMajorityThreshold` is deliberately ABSENT. Omitting it makes both the cluster member
65
- * and the coordinator fall back to Optimystic's `DEFAULT_SUPER_MAJORITY_THRESHOLD` (0.75) —
66
- * the bar a real party commits control writes against. Setting it here, or at any one
67
- * consumer, reintroduces exactly the divergence this constant exists to prevent. The absence
68
- * is a researched decision, not an inherited default: no threshold both relaxes the
69
- * three-node unanimity bar and satisfies Optimystic's partition-safety condition at the
70
- * shipped admission fraction — see `docs/architecture.md` → "Replication cluster size".
71
- *
72
- * `sizeTolerance` only makes sense alongside `allowDownsize`: a fixed
73
- * {@link CONTROL_REPLICATION_BREADTH}-wide target is unsatisfiable by any real (2-7 node)
74
- * party, so the cohort must be allowed to shrink to the party that actually exists.
75
- *
76
- * NOT for strand networks: {@link STRAND_CLUSTER_POLICY} is a structurally identical object
77
- * for a different network with different reasoning. The shape match is a coincidence; keep
78
- * them separate.
79
- *
80
- * `assumedClusterSize` is REQUIRED here, and 2 is not a guess — it is the smallest party we
81
- * support. The field feeds two Optimystic consumers with opposite failure modes, and only one
82
- * of them defaults to something workable for us:
83
- *
84
- * - The membership admission gate already defaults to 2 (`minAbsoluteClusterSize`), so naming
85
- * it changes nothing there.
86
- * - The read-repair / reconcile corroboration floor does NOT. Absent this field it falls back
87
- * to `clusterSize` — which for the control network is {@link CONTROL_REPLICATION_BREADTH},
88
- * 16. `corroboratorCapacity(peers, 16)` is then `max(peers, 15)`, so the floor of two
89
- * distinct non-self corroborators binds. A two-node party fields exactly one, can never
90
- * reach two, and so can never repair a block: `cluster-fetch:no-quorum` forever.
91
- *
92
- * Declaring 2 does NOT lower the replication factor — that stays at
93
- * {@link CONTROL_REPLICATION_BREADTH}. It states the cohort size we can genuinely field, which
94
- * is what the corroboration floor is asking. See Optimystic's `cluster/cluster-policy.ts`
95
- * ("Why two size yardsticks, not one") and `corroboratorCapacity` in `cluster/quorum-restore.ts`.
96
- *
97
- * The `satisfies` is load-bearing: without it a mistyped or obsolete key would compile at both
98
- * this definition and every consumer, because TypeScript only excess-property-checks fresh
99
- * object literals, not a shared constant handed to `clusterPolicy`.
100
- */
101
- export const CONTROL_CLUSTER_POLICY = Object.freeze({
102
- allowDownsize: true,
103
- sizeTolerance: 0.5,
104
- assumedClusterSize: 2,
105
- } satisfies NonNullable<NodeOptions['clusterPolicy']>);
106
-
107
- /**
108
- * Default number of nodes a **strand** network is told its replication cluster should have.
109
- *
110
- * Strand data is application data, where partial replication is a legitimate choice: only
111
- * the control database's every-member-reads-all-of-it character forces full replication
112
- * ({@link CONTROL_REPLICATION_BREADTH}). So the question here is not "everyone" but "how
113
- * few is too few", and the answer is four.
114
- *
115
- * **Why four.** A write commits when a super-majority of the cohort approves, and that bar is
116
- * Optimystic's `DEFAULT_SUPER_MAJORITY_THRESHOLD` of 0.75 — which Cadre selects by naming no
117
- * threshold at all (see {@link CONTROL_CLUSTER_POLICY}). Approvals needed is
118
- * `ceil(cohort x 0.75)`, so:
119
- *
120
- * | copies | approvals needed | holders that may be offline |
121
- * |--------|------------------|-----------------------------|
122
- * | 2 | 2 | 0 |
123
- * | 3 | 3 | 0 |
124
- * | 4 | 3 | 1 |
125
- * | 5 | 4 | 1 |
126
- * | 6 | 5 | 1 |
127
- *
128
- * Four is the smallest breadth that lets a write commit while one holder is away. At two or
129
- * three every holder must be awake for every write, which for a workspace shared between
130
- * phones and laptops is the ordinary case, not the rare one. Six buys no more fault tolerance
131
- * than four and costs more overfetch (see the NOTE below).
132
- *
133
- * **It is also a correctness floor, not only a durability one — but the floor is measured in
134
- * machines, not in this number.** A node that has fallen behind and can ask exactly one peer
135
- * whether it is current has Optimystic accept that single answer as the cluster's truth
136
- * (`corroboratorCapacity` in `db-p2p/src/cluster/quorum-restore.ts` lowers the corroboration
137
- * floor to one when the cohort cannot hold a second voter). If that peer is also behind it
138
- * honestly reports the stale revision, the reader concludes it is current, and re-arms its
139
- * repair window forever. Measured on the control-DB replication scenario: 4 failures in 10 runs
140
- * at breadth 2, 0 in 20 at breadth 3, 0 in 10 at breadth 8. What lifts the floor off a single
141
- * voter is a *third machine*, not a wider target: `corroboratorCapacity` takes the cohort peers
142
- * actually present against {@link STRAND_CLUSTER_POLICY}'s declared `assumedClusterSize`, so a
143
- * three-machine strand needs two corroborators whatever this number says, and a two-machine one
144
- * cannot supply them however wide the target is. The underlying Optimystic behaviour is unfixed
145
- * — `backlog/debt-read-repair-single-voter-corroboration` — so a two-machine strand still takes
146
- * the exposure. Raising this number does not buy the way out; adding a machine does.
147
- *
148
- * **Why not derived from the party or member count.** The strand's `Member` rows live *in* the
149
- * strand database, which runs on the strand libp2p node, whose cluster size is frozen at
150
- * construction — reading the list to configure the node needs the node to exist. Open strands
151
- * have no member list at all. The count also changes in the unsafe direction: a node that
152
- * restarted after someone joined derives a wider expected cohort than one that did not, and the
153
- * membership admission gate's confident path is what rejects the *wider* view. And a `Member` is
154
- * a party, not a machine; cohort width consumes machines. Full reasoning in
155
- * `docs/architecture.md` → "Replication cluster size".
156
- *
157
- * **Four machines, not four parties.** This number counts machines, and today that is not the
158
- * same as counting independent holders. Cohort selection upstream is owner-blind — it
159
- * picks nodes by hash proximity with no concept of which peer belongs to which party
160
- * (`optimystic/tickets/backlog/feat-cohort-selection-owner-aware-placement`) — and on the
161
- * cadre-driven path, the one that resolves a seed for itself, a strand's libp2p mesh is seeded
162
- * from exactly one party's own control-database peer rows (`CadreNode.resolveCohortSeed`), so
163
- * the cohort actually formed is that one party's machines. Four copies are, on that path, four
164
- * machines belonging to one party — one failure domain, one witness. A cross-party mesh is not
165
- * forbidden, only undiscoverable: it takes hand-supplied addresses
166
- * (`StrandConnectionOptions.bootstrapNodes`) or a hand-written dial (see the doc). Tracked:
167
- * `backlog/feat-strand-party-identity`. Full reasoning: `docs/architecture.md` →
168
- * "Replication cluster size".
169
- *
170
- * Raising or lowering it per strand is still the embedder's call
171
- * ({@link resolveStrandClusterSize}, `CadreNodeConfig.strandClusterSize`,
172
- * `StrandConnectionOptions.clusterSize`) — but every node on one strand must agree, so a change
173
- * means restarting all of them. There is no smaller-than-{@link MIN_CLUSTER_SIZE} option.
174
- *
175
- * NOTE: cohort selection overfetches proportionally — `Libp2pKeyPeerNetwork.membershipOverfetch`
176
- * asks FRET for `max(clusterSize * 4, clusterSize + 16)` candidates and does one peerStore
177
- * protocol lookup per candidate, so 4 requests a 20-peer band (was 18 at breadth 2). Negligible,
178
- * and bounded by the peers FRET actually knows. If strand cohort selection ever shows up as slow,
179
- * look here first.
180
- */
181
- export const DEFAULT_STRAND_CLUSTER_SIZE = 4;
182
-
183
- /**
184
- * Cluster consensus policy every STRAND-network libp2p node runs — production
185
- * (`StrandInstanceManager.buildStrandRuntime`) and the plugin's own networked e2e mesh alike.
186
- * One definition so the two cannot drift; the literal used to be hand-copied at both sites.
187
- *
188
- * Structurally identical to {@link CONTROL_CLUSTER_POLICY} and deliberately separate from it:
189
- * that one is the control database's, whose breadth is fixed at
190
- * {@link CONTROL_REPLICATION_BREADTH} for reasons that do not apply to application data.
191
- *
192
- * `superMajorityThreshold` is ABSENT for the same reason it is absent there: omitting it is how
193
- * both the coordinator and the cluster member select Optimystic's
194
- * `DEFAULT_SUPER_MAJORITY_THRESHOLD` (0.75), the bar {@link DEFAULT_STRAND_CLUSTER_SIZE}'s
195
- * copies/approvals table is computed against.
196
- *
197
- * `sizeTolerance` only makes sense alongside `allowDownsize`: a {@link
198
- * DEFAULT_STRAND_CLUSTER_SIZE}-wide target is unsatisfiable by a strand of one, two or three
199
- * machines, so the cohort must be allowed to shrink to the mesh that actually exists.
200
- *
201
- * `assumedClusterSize` is REQUIRED, and {@link MIN_CLUSTER_SIZE} is not a guess — it is the
202
- * smallest strand that reaches the cluster path at all. Absent this field the read-repair /
203
- * reconcile corroboration floor falls back to `clusterSize`, so
204
- * `corroboratorCapacity(peers, 4)` is `max(peers, 3)` and two distinct non-self corroborators
205
- * become mandatory. A two-machine strand fields exactly one peer, can never reach two, and so
206
- * can never repair a block — `cluster-fetch:no-quorum` forever, which is a read that never
207
- * catches up and a `create table` that dies with `Missing block` on the joining node. Declaring
208
- * the honest floor does NOT lower the replication factor (that stays at
209
- * {@link DEFAULT_STRAND_CLUSTER_SIZE}) and does not relax anything a strand with three or more
210
- * machines does: `corroboratorCapacity` takes the MAX of the cohort actually present and this
211
- * number, so the relaxed branch is reachable only by a cohort that is genuinely that small. See
212
- * Optimystic's `cluster/cluster-policy.ts` ("Why two size yardsticks, not one").
213
- *
214
- * The `satisfies` is load-bearing for the same reason as {@link CONTROL_CLUSTER_POLICY}'s.
215
- */
216
- export const STRAND_CLUSTER_POLICY = Object.freeze({
217
- allowDownsize: true,
218
- sizeTolerance: 0.5,
219
- assumedClusterSize: MIN_CLUSTER_SIZE,
220
- } satisfies NonNullable<NodeOptions['clusterPolicy']>);
221
-
222
- /**
223
- * Resolve the cluster size to hand `createLibp2pNode` for a **strand** network, applying
224
- * {@link DEFAULT_STRAND_CLUSTER_SIZE} and rejecting values Optimystic cannot honour. The
225
- * control network does not route through here — it uses the fixed
226
- * {@link CONTROL_REPLICATION_BREADTH}.
227
- *
228
- * Leaving the value unset is NOT the same as passing the default: Optimystic's own fallback
229
- * is 10, so every strand node-creating path must route through here.
230
- *
231
- * Every node on the same strand should resolve to the same value: the membership admission
232
- * gate's *confident* path compares a coordinator's declared set against the member's own
233
- * cohort view, and that view is bounded by this number, so a member configured much higher
234
- * can reject a smaller declared set as a downsize. Divergence is a live hazard whenever FRET
235
- * has a confident network-size estimate — not the unconditional refusal earlier revisions of
236
- * this comment described. See `docs/architecture.md` → "Replication cluster size".
237
- */
238
- export function resolveStrandClusterSize(configured?: number): number {
239
- if (configured === undefined) {
240
- return DEFAULT_STRAND_CLUSTER_SIZE;
241
- }
242
- if (!Number.isInteger(configured) || configured < MIN_CLUSTER_SIZE) {
243
- throw new Error(
244
- `clusterSize must be an integer >= ${MIN_CLUSTER_SIZE} (Optimystic's minimum cluster size); got ${configured}`
245
- );
246
- }
247
- return configured;
248
- }
1
+ import type { NodeOptions } from '@optimystic/db-p2p';
2
+
3
+ /**
4
+ * Smallest cluster size Optimystic will honour — its own `minAbsoluteClusterSize`,
5
+ * and the smallest value that reaches the cluster path at all (a lone node writes to
6
+ * local storage without forming a cluster). A validation floor, not a default.
7
+ */
8
+ export const MIN_CLUSTER_SIZE = 2;
9
+
10
+ /**
11
+ * How many nodes each block of the **control** (cadre membership) database is replicated
12
+ * to. Chosen to exceed any party's node count, so in practice every control block lands
13
+ * on every member of the party. Cohort width alone only delivers that for blocks written
14
+ * while the members were serving the network: a block committed while the writer was
15
+ * alone (the founder's genesis above all — collection headers are written once and never
16
+ * revised) has a cohort of one, and it reaches later joiners through cadre-core's
17
+ * peer-join block catch-up (`peer-join-backfill.ts`, membership-gated on the control
18
+ * network), not through this breadth.
19
+ *
20
+ * **Why full replication here.** Every control node reads the whole control database —
21
+ * membership, peer addresses, the strand list — so a member left out of a block's cohort is
22
+ * a member that may never learn the fact, and at a cohort of two the *read repair* that is
23
+ * supposed to make that safe cannot converge (it can ask exactly one peer, and that peer may
24
+ * be the member that also missed the write). Measured: 4 failures in 10 runs of the
25
+ * control-DB replication scenario at size 2, 0 in 20 at size 3 and 0 in 10 at size 8.
26
+ *
27
+ * **Why this number, and why a constant.** Optimystic caps a cohort at the peers that
28
+ * actually serve the network and downsizes a cohort it cannot fill (`allowDownsize: true`,
29
+ * which Cadre passes), so any value at or above the party's node count yields the same
30
+ * behaviour: cohort = whole party. 16 is roughly twice the largest deployment the product
31
+ * documents (`docs/architecture.md` → "Enterprise (Multi-Node Mixed)", 7 nodes) — headroom
32
+ * without pretending to support arbitrarily large parties; a party genuinely running more
33
+ * than 16 nodes is back to partial replication for its control database and should raise
34
+ * this. It is a constant rather than the live member count because Optimystic freezes the
35
+ * value at libp2p-node construction, before the `ControlDatabase` holding the `CadrePeer`
36
+ * rows exists, and because a per-node derivation lets two members disagree.
37
+ *
38
+ * A cadre is one party by construction, so the owner-blind cohort-selection question that
39
+ * applies to strand networks (see {@link DEFAULT_STRAND_CLUSTER_SIZE} below) is not a question
40
+ * here: "replicate to the whole party" already is the whole intent, and there is no party
41
+ * diversity for this breadth to buy or fail to buy.
42
+ *
43
+ * Canonical explanation of all of the above — the read-repair mechanics, what whole-party
44
+ * breadth does and does not remove, and what the wider cohort costs in write availability —
45
+ * is `docs/architecture.md` → "Replication cluster size". Keep it there, not here.
46
+ *
47
+ * **Deliberately not `clusterPolicy.assumedClusterSize`.** That option means "the smallest
48
+ * cohort this deployment can genuinely field", and it feeds both the membership admission
49
+ * gate's low-confidence path *and* the read-repair/reconcile corroboration floor. A Cadre
50
+ * party legitimately runs one or two nodes, so Cadre leaves it at Optimystic's default of 2;
51
+ * asserting 16 there would make the admission gate demand `ceil(0.75 x 16) = 12` declared
52
+ * peers and refuse every real party's writes.
53
+ *
54
+ * NOTE: cohort selection overfetches proportionally to this number —
55
+ * `Libp2pKeyPeerNetwork.membershipOverfetch` asks FRET for
56
+ * `max(clusterSize * 4, clusterSize + 16)` candidates and does a peerStore protocol lookup
57
+ * per candidate, so 16 requests a 64-peer proximity band. Free today, because the result is
58
+ * bounded by the peers FRET actually knows (2 in a 3-node party). If control-network cohort
59
+ * selection ever shows up as slow on a large mesh, look here first.
60
+ */
61
+ export const CONTROL_REPLICATION_BREADTH = 16;
62
+
63
+ /**
64
+ * Cluster consensus policy every CONTROL-network libp2p node runs — production
65
+ * (`CadreNode.buildControlNodeOptions`) and the integration harness alike. One definition so
66
+ * the two cannot drift: the object literal used to be hand-copied at both sites, and the copy
67
+ * at the harness had picked up a `superMajorityThreshold` override production never had.
68
+ *
69
+ * `superMajorityThreshold` is deliberately ABSENT. Omitting it makes both the cluster member
70
+ * and the coordinator fall back to Optimystic's `DEFAULT_SUPER_MAJORITY_THRESHOLD` (0.75) —
71
+ * the bar a real party commits control writes against. Setting it here, or at any one
72
+ * consumer, reintroduces exactly the divergence this constant exists to prevent. The absence
73
+ * is a researched decision, not an inherited default: no threshold both relaxes the
74
+ * three-node unanimity bar and satisfies Optimystic's partition-safety condition at the
75
+ * shipped admission fraction — see `docs/architecture.md` → "Replication cluster size".
76
+ *
77
+ * `sizeTolerance` only makes sense alongside `allowDownsize`: a fixed
78
+ * {@link CONTROL_REPLICATION_BREADTH}-wide target is unsatisfiable by any real (2-7 node)
79
+ * party, so the cohort must be allowed to shrink to the party that actually exists.
80
+ *
81
+ * NOT for strand networks: {@link STRAND_CLUSTER_POLICY} is a structurally identical object
82
+ * for a different network with different reasoning. The shape match is a coincidence; keep
83
+ * them separate.
84
+ *
85
+ * `assumedClusterSize` is REQUIRED here, and 2 is not a guess — it is the smallest party we
86
+ * support. The field feeds two Optimystic consumers with opposite failure modes, and only one
87
+ * of them defaults to something workable for us:
88
+ *
89
+ * - The membership admission gate already defaults to 2 (`minAbsoluteClusterSize`), so naming
90
+ * it changes nothing there.
91
+ * - The read-repair / reconcile corroboration floor does NOT. Absent this field it falls back
92
+ * to `clusterSize` — which for the control network is {@link CONTROL_REPLICATION_BREADTH},
93
+ * 16. `corroboratorCapacity(peers, 16)` is then `max(peers, 15)`, so the floor of two
94
+ * distinct non-self corroborators binds. A two-node party fields exactly one, can never
95
+ * reach two, and so can never repair a block: `cluster-fetch:no-quorum` forever.
96
+ *
97
+ * Declaring 2 does NOT lower the replication factor — that stays at
98
+ * {@link CONTROL_REPLICATION_BREADTH}. It states the cohort size we can genuinely field, which
99
+ * is what the corroboration floor is asking. See Optimystic's `cluster/cluster-policy.ts`
100
+ * ("Why two size yardsticks, not one") and `corroboratorCapacity` in `cluster/quorum-restore.ts`.
101
+ *
102
+ * **`assumedClusterSize` is now the ADMISSION yardstick.** Since Optimystic grew
103
+ * `repairCorroborationClusterSize` (see {@link controlClusterPolicy}), the repair corroboration
104
+ * floor prefers that separate declaration and reads this field only when nothing was declared —
105
+ * the unknown-machine-count path, where the paragraph above still describes what it does. So on
106
+ * every path where the count IS known, the 2 here governs one consumer: the membership admission
107
+ * gate's low-confidence fallback path. Leaving
108
+ * it at 2 is deliberate and must NOT be "fixed" to match the repair yardstick. Raising it would
109
+ * make the gate demand `ceil(0.75 x N)` DECLARED peers before it admits a coordinator's peer
110
+ * set on that path, and a Cadre party is phones and laptops: it cannot promise that three
111
+ * quarters of its enrolled machines are awake at any moment, so a party of eight with three
112
+ * awake would stop transacting entirely. The repair yardstick has no such cost (it gates only
113
+ * whether this node believes a repair answer), which is exactly why the two are now separate
114
+ * numbers rather than one.
115
+ *
116
+ * The `satisfies` is load-bearing: without it a mistyped or obsolete key would compile at both
117
+ * this definition and every consumer, because TypeScript only excess-property-checks fresh
118
+ * object literals, not a shared constant handed to `clusterPolicy`.
119
+ */
120
+ export const CONTROL_CLUSTER_POLICY = Object.freeze({
121
+ allowDownsize: true,
122
+ sizeTolerance: 0.5,
123
+ assumedClusterSize: 2,
124
+ } satisfies NonNullable<NodeOptions['clusterPolicy']>);
125
+
126
+ /**
127
+ * Default number of nodes a **strand** network is told its replication cluster should have.
128
+ *
129
+ * Strand data is application data, where partial replication is a legitimate choice: only
130
+ * the control database's every-member-reads-all-of-it character forces full replication
131
+ * ({@link CONTROL_REPLICATION_BREADTH}). So the question here is not "everyone" but "how
132
+ * few is too few", and the answer is four.
133
+ *
134
+ * **Why four.** A write commits when a super-majority of the cohort approves, and that bar is
135
+ * Optimystic's `DEFAULT_SUPER_MAJORITY_THRESHOLD` of 0.75 — which Cadre selects by naming no
136
+ * threshold at all (see {@link CONTROL_CLUSTER_POLICY}). Approvals needed is
137
+ * `ceil(cohort x 0.75)`, so:
138
+ *
139
+ * | copies | approvals needed | holders that may be offline |
140
+ * |--------|------------------|-----------------------------|
141
+ * | 2 | 2 | 0 |
142
+ * | 3 | 3 | 0 |
143
+ * | 4 | 3 | 1 |
144
+ * | 5 | 4 | 1 |
145
+ * | 6 | 5 | 1 |
146
+ *
147
+ * Four is the smallest breadth that lets a write commit while one holder is away. At two or
148
+ * three every holder must be awake for every write, which for a workspace shared between
149
+ * phones and laptops is the ordinary case, not the rare one. Six buys no more fault tolerance
150
+ * than four and costs more overfetch (see the NOTE below).
151
+ *
152
+ * **Exercised at four machines** by `integration-tests`'
153
+ * `scenarios/strand-two-party-two-machine.integration.ts`, the only scenario that runs a
154
+ * strand at this breadth. It shows a write committing while one of the four machines is off.
155
+ * It does NOT distinguish that from a commit on a cohort downsized to the three live holders
156
+ * (`allowDownsize` in {@link STRAND_CLUSTER_POLICY}), so the table row above remains reasoned
157
+ * arithmetic with an end-to-end existence proof under it, not a measurement of cohort width.
158
+ *
159
+ * **It is also a correctness floor, not only a durability one — but the floor is measured in
160
+ * machines, not in this number.** A node that has fallen behind and can ask exactly one peer
161
+ * whether it is current has Optimystic accept that single answer as the cluster's truth
162
+ * (`corroboratorCapacity` in `db-p2p/src/cluster/quorum-restore.ts` lowers the corroboration
163
+ * floor to one when the cohort cannot hold a second voter). If that peer is also behind it
164
+ * honestly reports the stale revision, the reader concludes it is current, and re-arms its
165
+ * repair window forever. Measured on the control-DB replication scenario: 4 failures in 10 runs
166
+ * at breadth 2, 0 in 20 at breadth 3, 0 in 10 at breadth 8. What lifts the floor off a single
167
+ * voter is a third machine **or a declaration that there are three** — not a wider target.
168
+ * `corroboratorCapacity` takes the max of the cohort peers actually present and the declared
169
+ * repair yardstick, so raising *this* number (the replication breadth) does nothing: the
170
+ * yardstick is a separate declaration, and **a strand node makes no such declaration today**.
171
+ * The control network derives one from the party's enrolled machines
172
+ * ({@link controlClusterPolicy}), which is exact there because every enrolled machine runs the
173
+ * control node. A strand has no equivalent count: it launches only on machines whose embedder
174
+ * registered its sApp config, and reusing the party count over-declares — which is worse than
175
+ * declaring nothing, because at a yardstick of three or more the corroboration floor is pinned
176
+ * at two peers and a strand that can field only one could then never repair at all. So every
177
+ * strand runs {@link STRAND_CLUSTER_POLICY} unchanged and takes the single-voter exposure
178
+ * whenever its cohort view shrinks to one peer. The underlying Optimystic behaviour is unfixed
179
+ * (`backlog/debt-read-repair-single-voter-corroboration`) and the per-strand serving count that
180
+ * would let a strand declare honestly is `backlog/feat-strand-yardstick-from-serving-machines`
181
+ * — its seam ({@link strandClusterPolicy}'s `servingMachines`, threaded through
182
+ * `StartStrandConfig.servingMachines`) is already in place and fed by nothing. Raising this
183
+ * number does not buy the way out; a machine, or an honest declaration of the machines actually
184
+ * serving the strand, does.
185
+ *
186
+ * **Why not derived from the party or member count.** The strand's `Member` rows live *in* the
187
+ * strand database, which runs on the strand libp2p node, whose cluster size is frozen at
188
+ * construction — reading the list to configure the node needs the node to exist. Open strands
189
+ * have no member list at all. The count also changes in the unsafe direction: a node that
190
+ * restarted after someone joined derives a wider expected cohort than one that did not, and the
191
+ * membership admission gate's confident path is what rejects the *wider* view. And a `Member` is
192
+ * a party, not a machine; cohort width consumes machines. Full reasoning in
193
+ * `docs/architecture.md` → "Replication cluster size".
194
+ *
195
+ * **Four machines, not four parties.** This number counts machines, and today that is not the
196
+ * same as counting independent holders. Cohort selection upstream is owner-blind — it
197
+ * picks nodes by hash proximity with no concept of which peer belongs to which party
198
+ * (`optimystic/tickets/backlog/feat-cohort-selection-owner-aware-placement`) — and on the
199
+ * cadre-driven path, the one that resolves a seed for itself, a strand's libp2p mesh is seeded
200
+ * from exactly one party's own control-database peer rows (`CadreNode.resolveCohortSeed`), so
201
+ * the cohort actually formed is that one party's machines. Four copies are, on that path, four
202
+ * machines belonging to one party — one failure domain, one witness. A cross-party mesh has
203
+ * exactly one automatic path — a formation result carries the responder's strand addresses and
204
+ * `CadreNode.resolveCohortSeed` unions them in, seeding only the pair that just redeemed an
205
+ * invitation — and is otherwise hand-supplied addresses
206
+ * (`StrandConnectionOptions.bootstrapNodes`) or a hand-written dial (see the doc). Every party
207
+ * now holds its own membership identity, so the missing piece is placement, not identity:
208
+ * tracked as `backlog/feat-strand-party-diverse-placement`. Full reasoning:
209
+ * `docs/architecture.md` → "Replication cluster size".
210
+ *
211
+ * Raising or lowering it per strand is still the embedder's call
212
+ * ({@link resolveStrandClusterSize}, `CadreNodeConfig.strandClusterSize`,
213
+ * `StrandConnectionOptions.clusterSize`) — but every node on one strand must agree, so a change
214
+ * means restarting all of them. There is no smaller-than-{@link MIN_CLUSTER_SIZE} option.
215
+ *
216
+ * NOTE: cohort selection overfetches proportionally — `Libp2pKeyPeerNetwork.membershipOverfetch`
217
+ * asks FRET for `max(clusterSize * 4, clusterSize + 16)` candidates and does one peerStore
218
+ * protocol lookup per candidate, so 4 requests a 20-peer band (was 18 at breadth 2). Negligible,
219
+ * and bounded by the peers FRET actually knows. If strand cohort selection ever shows up as slow,
220
+ * look here first.
221
+ */
222
+ export const DEFAULT_STRAND_CLUSTER_SIZE = 4;
223
+
224
+ /**
225
+ * Cluster consensus policy every STRAND-network libp2p node runs — production
226
+ * (`StrandInstanceManager.buildStrandRuntime`) and the plugin's own networked e2e mesh alike.
227
+ * One definition so the two cannot drift; the literal used to be hand-copied at both sites.
228
+ *
229
+ * Structurally identical to {@link CONTROL_CLUSTER_POLICY} and deliberately separate from it:
230
+ * that one is the control database's, whose breadth is fixed at
231
+ * {@link CONTROL_REPLICATION_BREADTH} for reasons that do not apply to application data.
232
+ *
233
+ * `superMajorityThreshold` is ABSENT for the same reason it is absent there: omitting it is how
234
+ * both the coordinator and the cluster member select Optimystic's
235
+ * `DEFAULT_SUPER_MAJORITY_THRESHOLD` (0.75), the bar {@link DEFAULT_STRAND_CLUSTER_SIZE}'s
236
+ * copies/approvals table is computed against.
237
+ *
238
+ * `sizeTolerance` only makes sense alongside `allowDownsize`: a {@link
239
+ * DEFAULT_STRAND_CLUSTER_SIZE}-wide target is unsatisfiable by a strand of one, two or three
240
+ * machines, so the cohort must be allowed to shrink to the mesh that actually exists.
241
+ *
242
+ * `assumedClusterSize` is REQUIRED, and {@link MIN_CLUSTER_SIZE} is not a guess — it is the
243
+ * smallest strand that reaches the cluster path at all. Absent this field the read-repair /
244
+ * reconcile corroboration floor falls back to `clusterSize`, so
245
+ * `corroboratorCapacity(peers, 4)` is `max(peers, 3)` and two distinct non-self corroborators
246
+ * become mandatory. A two-machine strand fields exactly one peer, can never reach two, and so
247
+ * can never repair a block — `cluster-fetch:no-quorum` forever, which is a read that never
248
+ * catches up and a `create table` that dies with `Missing block` on the joining node. Declaring
249
+ * the honest floor does NOT lower the replication factor (that stays at
250
+ * {@link DEFAULT_STRAND_CLUSTER_SIZE}) and does not relax anything a strand with three or more
251
+ * machines does: `corroboratorCapacity` takes the MAX of the cohort actually present and this
252
+ * number, so the relaxed branch is reachable only by a cohort that is genuinely that small. See
253
+ * Optimystic's `cluster/cluster-policy.ts` ("Why two size yardsticks, not one").
254
+ *
255
+ * `assumedClusterSize` also carries the ADMISSION yardstick, and on the strand path it carries
256
+ * BOTH jobs today. Optimystic prefers a separately declared
257
+ * `repairCorroborationClusterSize` for the repair floor and falls back to this field only when
258
+ * nothing was declared — which, for a strand, is always: no per-strand serving count exists yet,
259
+ * so {@link strandClusterPolicy} declares nothing and the 2 here governs the repair floor
260
+ * exactly as the paragraph above describes, alongside the membership admission gate's
261
+ * low-confidence fallback. Do not raise it if a strand yardstick ever does get declared
262
+ * (`backlog/feat-strand-yardstick-from-serving-machines`): a strand shared between phones cannot
263
+ * promise that `ceil(0.75 x N)` of its machines are awake, and the admission gate would refuse
264
+ * its writes. Same reason and same warning as {@link CONTROL_CLUSTER_POLICY}'s.
265
+ *
266
+ * The `satisfies` is load-bearing for the same reason as {@link CONTROL_CLUSTER_POLICY}'s.
267
+ */
268
+ export const STRAND_CLUSTER_POLICY = Object.freeze({
269
+ allowDownsize: true,
270
+ sizeTolerance: 0.5,
271
+ assumedClusterSize: MIN_CLUSTER_SIZE,
272
+ } satisfies NonNullable<NodeOptions['clusterPolicy']>);
273
+
274
+ /**
275
+ * Resolve the cluster size to hand `createLibp2pNode` for a **strand** network, applying
276
+ * {@link DEFAULT_STRAND_CLUSTER_SIZE} and rejecting values Optimystic cannot honour. The
277
+ * control network does not route through here — it uses the fixed
278
+ * {@link CONTROL_REPLICATION_BREADTH}.
279
+ *
280
+ * Leaving the value unset is NOT the same as passing the default: Optimystic's own fallback
281
+ * is 10, so every strand node-creating path must route through here.
282
+ *
283
+ * Every node on the same strand should resolve to the same value: the membership admission
284
+ * gate's *confident* path compares a coordinator's declared set against the member's own
285
+ * cohort view, and that view is bounded by this number, so a member configured much higher
286
+ * can reject a smaller declared set as a downsize. Divergence is a live hazard whenever FRET
287
+ * has a confident network-size estimate — not the unconditional refusal earlier revisions of
288
+ * this comment described. See `docs/architecture.md` → "Replication cluster size".
289
+ */
290
+ export function resolveStrandClusterSize(configured?: number): number {
291
+ if (configured === undefined) {
292
+ return DEFAULT_STRAND_CLUSTER_SIZE;
293
+ }
294
+ if (!Number.isInteger(configured) || configured < MIN_CLUSTER_SIZE) {
295
+ throw new Error(
296
+ `clusterSize must be an integer >= ${MIN_CLUSTER_SIZE} (Optimystic's minimum cluster size); got ${configured}`
297
+ );
298
+ }
299
+ return configured;
300
+ }
301
+
302
+ /**
303
+ * The repair yardstick to declare for a network, from the machines that SERVE that network and
304
+ * the breadth it replicates to.
305
+ *
306
+ * ## What this number does
307
+ *
308
+ * Optimystic's block repair (read-repair and reconcile) asks a block's cohort for the newest
309
+ * revision and trusts an answer only when enough independent peers agree on it. "Enough" is
310
+ * measured against a **declared** number rather than the peers currently visible — deliberately,
311
+ * because the visible set comes from unauthenticated routing, and a partition (or an attacker
312
+ * with routing influence) can shrink it. `clusterPolicy.repairCorroborationClusterSize` is that
313
+ * declaration; it moves the repair yardstick ALONE, leaving
314
+ * {@link CONTROL_CLUSTER_POLICY}'s / {@link STRAND_CLUSTER_POLICY}'s `assumedClusterSize` — the
315
+ * membership admission gate's yardstick — untouched. Optimystic resolves
316
+ * `repairCorroborationClusterSize -> assumedClusterSize -> clusterSize`, and a value that is not
317
+ * a positive integer falls THROUGH to the next term rather than being clamped, which is why the
318
+ * builders below refuse to pass a degenerate count at all.
319
+ *
320
+ * ## The arithmetic, and why the formula is what it is
321
+ *
322
+ * Three consumers read the resolved yardstick `N`. With `p` = cohort peers currently visible
323
+ * (self excluded) and Optimystic's `CORROBORATION_FLOOR` = 2:
324
+ *
325
+ * - **Repair corroboration floor** (`corroboratorCapacity` / `quorumSize` in
326
+ * `db-p2p/src/cluster/quorum-restore.ts`): capacity is `max(p, N - 1)` and the floor is
327
+ * `max(1, min(2, capacity))`. So `N = 1` and `N = 2` behave IDENTICALLY at every `p` — both
328
+ * leave the floor at a single voter whenever `p <= 1`. `N >= 3` pins the floor at two
329
+ * corroborators unconditionally, including when the view has shrunk to one peer. That is the
330
+ * entire point of declaring. `N` above 3 raises nothing further (the `min(2, ...)` caps it), so
331
+ * a larger declaration buys only honesty, never more repair.
332
+ * - **Commit freshness window** (`CoordinatorRepo.commitQuorumRulesOutRivals`): `fullCohortSize`
333
+ * is `max(observed cohort, N)`, and a local commit arms the lazy read-repair window only when
334
+ * `approvals > fullCohortSize / 2`. Here a larger `N` is NOT free — see the NOTE below.
335
+ * - The `repair-fault-tolerance` startup advisory — log text only.
336
+ *
337
+ * Hence `max(MIN_CLUSTER_SIZE, min(servingMachines, replicationBreadth))`, two clamps each for
338
+ * a stated reason:
339
+ *
340
+ * - **Capped at the replication breadth**, because a block only ever lives on
341
+ * `min(breadth, machines serving)` machines. A party of eight running strands at
342
+ * {@link DEFAULT_STRAND_CLUSTER_SIZE} has a cohort of four; declaring eight would make
343
+ * `approvals > 4` unsatisfiable (a super-majority of four is three), so the freshness window
344
+ * would never arm for any strand commit, forever — and buy nothing on repair, whose floor is
345
+ * already at its cap at four. Declare the machines that can hold the block, not the machines
346
+ * that exist.
347
+ * - **Floored at {@link MIN_CLUSTER_SIZE}**, so this can only ever RAISE the declared number
348
+ * relative to the un-derived constants. A node that authorizes nobody is a genuine
349
+ * founder-alone party, but it is equally a freshly seeded node whose membership rows have not
350
+ * replicated yet, or one whose trusted-owner anchor is empty — and this node cannot tell those
351
+ * apart. Declaring 1 is safety-neutral for repair (identical to 2 at every `p`, per the table
352
+ * above) but it WOULD let a solo commit arm the freshness window, which is wrong if the "solo"
353
+ * reading came from stale local records. Lifting that floor for a node that can prove it is
354
+ * genuinely alone is separate work: `backlog/feat-solo-node-arms-its-own-freshness-window`.
355
+ *
356
+ * The yardstick is per-node and protects only that node's own reads — no node refuses another
357
+ * anything over it — so two members disagreeing (one has replicated a new `CadrePeer` row, one
358
+ * has not) is harmless by construction, not a race to close.
359
+ *
360
+ * ## The contract: callers pass machines that SERVE this network, or nothing at all
361
+ *
362
+ * Over-declaring is not merely wasteful, it is unsafe in the availability direction: at
363
+ * `N >= 3` the corroboration floor is pinned at two peers, so a cohort that can only ever
364
+ * field one peer can never repair at all (`cluster-fetch:no-quorum`), where a declaration of
365
+ * 2 would have let its single peer answer. So the number a caller passes must be the machines
366
+ * that actually serve the network in question — never a broader population that merely bounds
367
+ * it from above, and never a guess. A caller with no trustworthy count passes nothing, which
368
+ * the builders below turn into "declare no yardstick at all".
369
+ *
370
+ * The CONTROL network satisfies this from the party's enrolled machines, because every enrolled
371
+ * machine runs the control node by construction: there, enrolled IS serving
372
+ * ({@link controlClusterPolicy}, fed by cadre-core's `enrolled-machine-store.ts`). A STRAND does
373
+ * not — it launches only on machines whose embedder registered its sApp config
374
+ * (`CadreNode.addStrand`), so a closed strand shared by two machines of a three-machine party is
375
+ * served by two, and declaring three would be exactly the unsafe over-declaration above. No
376
+ * authenticated per-strand serving count exists yet, so strand nodes declare NOTHING and run the
377
+ * frozen {@link STRAND_CLUSTER_POLICY} — keeping the known, upstream-tracked single-voter
378
+ * exposure (`backlog/debt-read-repair-single-voter-corroboration`) rather than risking the
379
+ * cannot-repair-at-all failure. Building that count:
380
+ * `backlog/feat-strand-yardstick-from-serving-machines`.
381
+ *
382
+ * Both arguments must be positive integers; the builders below sanitize before calling, and
383
+ * a degenerate value here propagates (`NaN` in, `NaN` out) rather than being clamped.
384
+ *
385
+ * NOTE: on a party of three or more machines where fewer than half are awake, a commit reaches a
386
+ * downsized cohort and no longer arms the freshness window, so each such block's next read costs
387
+ * one cohort consult per read-repair window instead of trusting the local commit. That is the
388
+ * correct reading of the honest number — a commit that reached two of five machines genuinely
389
+ * does not rule out a rival quorum, and arming it today is a consequence of under-declaring — and
390
+ * the cost is bounded at one consult per block per window by the read path's solo-self-skip exit.
391
+ * NOT measured under load. If a large, mostly-hibernating party's read path ever shows up as
392
+ * slow, this is the first thing to look at.
393
+ */
394
+ export function resolveRepairYardstick(servingMachines: number, replicationBreadth: number): number {
395
+ return Math.max(MIN_CLUSTER_SIZE, Math.min(servingMachines, replicationBreadth));
396
+ }
397
+
398
+ /**
399
+ * A serving-machine count as a usable number, or `undefined` for "this node does not know".
400
+ * Anything that is not a positive integer is unknown rather than clamped: Optimystic itself
401
+ * treats a degenerate declaration as absent, so silently rounding one here would hide a caller
402
+ * bug behind a number nobody chose.
403
+ */
404
+ function asKnownMachineCount(servingMachines?: number): number | undefined {
405
+ return servingMachines !== undefined
406
+ && Number.isInteger(servingMachines)
407
+ && servingMachines >= 1
408
+ ? servingMachines
409
+ : undefined;
410
+ }
411
+
412
+ /**
413
+ * {@link CONTROL_CLUSTER_POLICY} with the repair yardstick declared from `enrolledMachines`
414
+ * (see {@link resolveRepairYardstick}); the frozen base object ITSELF when the count is unknown,
415
+ * so the cold path is provably today's behaviour and identity assertions keep holding.
416
+ *
417
+ * The parameter keeps the name `enrolledMachines`, unlike {@link strandClusterPolicy}'s, because
418
+ * for the CONTROL network the two quantities are one and the same by construction: every
419
+ * enrolled machine runs the control node, so the machines enrolled in the party ARE the machines
420
+ * serving this network.
421
+ */
422
+ export function controlClusterPolicy(enrolledMachines?: number): NonNullable<NodeOptions['clusterPolicy']> {
423
+ const known = asKnownMachineCount(enrolledMachines);
424
+ if (known === undefined) {
425
+ return CONTROL_CLUSTER_POLICY;
426
+ }
427
+ return Object.freeze({
428
+ ...CONTROL_CLUSTER_POLICY,
429
+ repairCorroborationClusterSize: resolveRepairYardstick(known, CONTROL_REPLICATION_BREADTH)
430
+ } satisfies NonNullable<NodeOptions['clusterPolicy']>);
431
+ }
432
+
433
+ /**
434
+ * {@link STRAND_CLUSTER_POLICY} with the repair yardstick declared from `servingMachines` — the
435
+ * machines that serve THIS strand — and this strand's own `clusterSize` (already resolved by
436
+ * {@link resolveStrandClusterSize}, so it is at least {@link MIN_CLUSTER_SIZE} and the formula's
437
+ * two clamps can never cross); the frozen base object ITSELF when the count is unknown.
438
+ *
439
+ * **The unknown path is the production path today.** No authenticated per-strand serving count
440
+ * exists, and the party's enrolled-machine count is emphatically not one (see
441
+ * {@link resolveRepairYardstick}'s contract section), so cadre-core passes nothing and every
442
+ * strand node runs the frozen constant. The parameter and its threading through
443
+ * `StartStrandConfig.servingMachines` stay in place as the seam that
444
+ * `backlog/feat-strand-yardstick-from-serving-machines` plugs a real count into.
445
+ */
446
+ export function strandClusterPolicy(
447
+ clusterSize: number,
448
+ servingMachines?: number
449
+ ): NonNullable<NodeOptions['clusterPolicy']> {
450
+ const known = asKnownMachineCount(servingMachines);
451
+ if (known === undefined) {
452
+ return STRAND_CLUSTER_POLICY;
453
+ }
454
+ return Object.freeze({
455
+ ...STRAND_CLUSTER_POLICY,
456
+ repairCorroborationClusterSize: resolveRepairYardstick(known, clusterSize)
457
+ } satisfies NonNullable<NodeOptions['clusterPolicy']>);
458
+ }