P3.3.3 — Gossip Adaptive Fanout — Audit
Step 1 of the 5-step chain (CLAUDE.md §6). Phase 3 θ Wave 4 — the connectivity-driven fanout helper plus the local exchange tracker that recomputes the connectivity score every N epochs. Companion P3.3.2 (Bloom dedup) and P3.7.1 (MCP tool surface) ship in parallel.
§1. Surface inventory at base SHA 9c7165d5
| Path | Exists? | Role |
|---|---|---|
src/domains/consensus/ |
Yes — θ domain root (P3.1.1 / P3.1.2 / P3.1.3 / P3.3.1 / P3.4.1 / P3.5.1 / P3.6.1 shipped) | θ domain root |
src/domains/consensus/adaptive-fanout.ts |
No — to create | computeFanout, clamp, FanoutTracker class |
src/__tests__/domains/consensus/adaptive-fanout.test.ts |
No — to create | Worked table + clamping + tracker lifecycle |
src/domains/consensus/gossip-wire.ts |
Yes (P3.3.1 — e63a8bcf) |
Consumed peer/event shape reference — NOT imported (slice is pure data, no wire dependency) |
src/domains/rules/builtins.ts |
Yes (κ P1.3.2) | Reference for the clamp ternary pattern (line ~clamp const) — NOT imported (κ’s clamp works on BuiltinValue tuples; this slice uses a bare bigint helper) |
§2. Spec inventory
2.1 Worked table — s08 §Adaptive fanout
docs/spec/s08-gossip.md §Adaptive fanout (lines 75–92):
fanout = max(3, min(10, 15 - connectivity_score))
Where connectivity_score is the number of live peers in the routing table, clamped to [0, 12].
| connectivity_score | fanout |
|---|---|
| 0 (isolated) | 10 |
| 3 | 10 |
| 5 | 10 |
| 7 | 8 |
| 10 | 5 |
| 12 | 3 (minimum) |
Minimum fanout = 3 (always gossip somewhere); maximum = 10 (bandwidth ceiling on isolated nodes). Recomputation period = 5 epochs, driven by successful IHAVE/IWANT exchanges during the window.
2.2 Acceptance criteria (source prompt §P3.3.3 lines 1314–1328)
computeFanout(connectivity_score: bigint): bigintreturnsmax(3n, min(10n, 15n - connectivity_score))- Worked-table fixture passes for score ∈
{0, 3, 5, 7, 10, 12} - Score clamped to
[0n, 12n]— values outside coerced (negative → 0n; >12n → 12n) - Recomputed every 5 epochs based on successful IHAVE/IWANT exchanges
trackExchange(peer_id, success, epoch)records exchange outcomerecomputeIfDue(current_epoch, last_recompute_epoch, period_epochs = 5n)returns updated score iffcurrent - last >= period- Score uses live-peer count: peers with ≥ 1 successful exchange in last
period_epochs
2.3 Determinism guardrails
docs/spec/s08-gossip.md does not list a forbidden-token sweep, but the task prompt’s FORBIDDENS section requires:
- No socket I/O — tracker is pure data
- bigint throughout, no
Number - No
Math.*, noDate.*, noMath.random, no floats - No new npm deps
- Not editing main checkout
These match the existing P3 forbidden-token scanners (messages.test.ts Group 9, gossip-wire.test.ts Group 15, time-anchors.test.ts Group 8).
§3. Reuse map
| Symbol / pattern | Source | Action |
|---|---|---|
| bigint ternary clamp pattern | src/domains/rules/builtins.ts clamp const |
Re-implement narrowly — κ’s clamp works on BuiltinValue tuples plus throws on lo > hi; this slice is a flat bigint helper |
| Header banner conventions | src/domains/consensus/gossip-wire.ts lines 1–57 |
Replicate banner + forbidden-token self-scan note |
| Per-slice forbidden-token scanner | src/__tests__/domains/consensus/time-anchors.test.ts Group 8 |
Replicate, narrowed to adaptive-fanout.ts |
Test fixture style (bufN(seed)) |
src/__tests__/domains/consensus/gossip-wire.test.ts lines 38–44 |
Not needed — this slice uses only string peer_id keys, no Buffers |
§4. Open questions
None — task prompt §P3.3.3 + s08 §Adaptive fanout fully resolve all design points (clamp order, period default, live-peer semantics, retention).
§5. Out of scope
- Wiring
FanoutTrackerinto a live gossip loop (deferred — P3.3.1 / P3.3.2 / P3.7.1 handle wire + dedup + MCP surface) - Persisting the score across process restarts (in-memory only, by design — exchange records are ephemeral over
period_epochs) - Multi-arbiter convergence of fanout scores (no global view; each node computes its own)
- Connectivity-aware peer selection (which peers to gossip to — task prompt scopes this to the count, not the picker)