Sins Mesh messaging component (BM kernel, app-local)
Policies that keep the org coherent
name: Sins Mesh Component
overview: Add a bounded-minimum Confederation mesh messaging kernel under Sins (Universe/Mesh/), wired into the campaign hour tick, written so it can later lift out to Novolis.Mesh.Core without redesign.
todos:
- id: mesh-spec-kernel
content: Add Universe/Mesh SPEC.md + immutable MeshState, IDs, Publish/DroneTick/Flood engines, DefaultMeshPipeline, InvariantChecker status: completed
- id: mesh-sins-bridge
content: MeshBridge from AstroEconomyBridge; seed on CampaignWorld.Ids; MeshPulse hour tick in CampaignRunner status: completed
- id: mesh-smoke-report-docs
content: Seed smoke publishes; Spectre Mesh block; docs/mesh-and-communications.md + architecture/places links status: completed
- id: mesh-unit-tests
content: Add SinsOfACapitalismTycoon.Unit with directed/flood/mailbox/bandwidth/loss validation scenarios; wire slnx status: completed isProject: false
Intent
Ship a real mesh simulation (publish/propagate, place-directed vs identity-flood, pulse drones, visibility ≠ delivery) as a Sins component for now — not a new novolis-mesh repo and not inside Novolis.Transports.*.
Shape it like `Novolis.Economy.Core`: SPEC.md, immutable state, ordered step engine, invariants, explicit non-goals — so extraction to Novolis.Mesh.Core later is a move, not a rewrite.
Placement
novolis-apps/src/SinsOfACapitalismTycoon/
Universe/Mesh/ ← BM kernel (no CampaignWorld / Avalonia / EconomySimulation refs)
SPEC.md
MeshState.cs
MeshEngine.cs + IMeshStep
Identifiers.cs / Enums.cs / ModelTypes.cs
PublishEngine.cs ← place-directed + identity-flood entry
DroneTickEngine.cs ← hop / arrive / loss
FloodEngine.cs ← hub fan-out when identity / public
InvariantChecker.cs
DefaultMeshPipeline.cs
Universe/Mesh/Sins/ ← thin product glue only
MeshBridge.cs ← Astro hubs → MeshState
MeshPulse.cs ← hour advance + publish helpers for campaign
docs/mesh-and-communications.mdKernel rule: anything under Universe/Mesh/ (except Sins/) must not reference CampaignWorld, ShipRegistry, Spectre, or Avalonia. IDs are mesh-native (MeshHubId, MeshIdentityId, PacketId); the bridge maps systemId / ship flavor ids.
Bounded minimum (v1)
In boundary
| Concept | Behavior |
|---|---|
| Hubs | One per bridged star system; pulse bandwidth budget per hour |
| Edges | Known hub→hub hops with travel hours (sprint vs cargo) |
| Packets | Pulse / bulk / public; sealed flag; opaque signature blob |
| Address | `Place(HubId)` vs `Identity(IdentityId)` vs `Public` |
| In-flight drones | Aggregate carriers (count + ETA), not named NPCs |
| Hub cache + mailbox | Visibility store; identity mailboxes hold until “connect” |
| Postage priority | Small integer; higher priority wins bandwidth contention |
Contract
- Place address → directed drone path along known edges (scoped propagate).
- Identity / public → flood (one-to-all among hubs) subject to bandwidth.
- Success metric = visible at hub / in mailbox, never “delivered to a human.”
Out of boundary (v1)
Real crypto, ansible channel, per-drone physics, Limbo ceremony as special event type, Transports promotion, money fees charged into Economy ledgers (postage is mesh-internal capacity only).
Timing model
Reuse campaign geography, not cargo cruise speeds:
- Mesh edges seeded from the same hop graph as `AstroEconomyBridge` (
MaxRangeLy = 12). - Pulse travel hours =
ceil(ly / PulseLyPerHour)withPulseLyPerHour≫ tramp cruise (CruiseLyPerHourtoday ≈1/(1.3×24)). Concrete default: ~20× tramp (tiny disposable mass) — documented in SPEC. - Bulk layer uses slower hop hours (closer to cargo) so pulse vs bulk is mechanically distinct.
MeshEngine.Advanceruns once per campaign hour beside existing pulses.
flowchart LR
publish[Publish]
place{AddressKind}
directed[Directed drones along path]
flood[Flood enqueue to neighbor hubs]
tick[DroneTick per hour]
visible[Hub cache / mailbox Visible]
publish --> place
place -->|Place| directed
place -->|Identity_or_Public| flood
directed --> tick
flood --> tick
tick --> visibleCampaign wiring
- Seed in `CampaignWorld.Create`: after
AstroEconomyBridge.Build,MeshBridge.FromBridge(bridge)→MeshState; store onCampaignWorld.IdsasMeshState Mesh(mutable ref holder or replace-with-new each tick — preferIds.Meshupdated immutably like Core). - Identities (minimal): register player hull + tramp ship flavor ids + firm keys as
MeshIdentityIdstrings; optionalLastKnownHubwhen a ship is berthed (from registry/site) as flood bias only — does not upgrade SLA. - Hour tick in `CampaignRunner.LiveSession.PulseDaysAsync` after agents /
Sim.AdvanceAsync:Ids.Mesh = MeshPulse.TickHour(Ids.Mesh). - Smoke publishes: on campaign start, one directed Sol→a known hub pulse and one identity flood for Calypso — proves the system without UI.
- Spectre: short “Mesh” block in `SpectreHeadlessReport` — hubs, in-flight drones, cache hits, flood vs directed counts.
- Docs: `places-and-stations.md` Communications section points at
docs/mesh-and-communications.md; architecture table row for Mesh (Sins-local → futureNovolis.Mesh.Core).
API sketch (kernel)
// Publish — returns next state; never claims delivery
MeshState Publish(MeshState s, MeshPacket packet, MeshAddress to, MeshHubId fromHub);
// Query visibility
bool IsVisibleAt(MeshState s, PacketId id, MeshHubId hub);
bool IsInMailbox(MeshState s, PacketId id, MeshIdentityId identity);
MeshState Advance(MeshState s); // DefaultMeshPipeline foldDirected path: BFS/Dijkstra on mesh edges (ly or hop count); no hard dependency on Novolis.Astro.Routing inside kernel — bridge precomputes edge list with travel hours.
Tests
novolis-apps has no test project today. Add:
- `novolis-apps/tests/SinsOfACapitalismTycoon.Unit/SinsOfACapitalismTycoon.Unit.csproj` — xUnit,
InternalsVisibleTofrom Sins exe (or make Mesh typespublicunder a clear namespace). - Register in `Novolis.Apps.slnx`.
Scenarios (Economy-style validation):
- Directed Sol→Wolf: visible at destination hub; not at unrelated hub before path completes.
- Identity flood: eventually visible at all hubs (or until TTL).
- Offline identity: mailbox holds;
IsDelivereddoes not exist. - Bandwidth: flood displaces lower-priority pulse at a congested hub.
- Multi-hop drone loss + retry still converges to visibility.
Explicit non-goals this PR
- New GitHub Packages package /
novolis-meshrepo - Charging Economy cash for postage
- Captain-bridge compose UI for messaging
- Ansible dual-channel
- Promoting anything into
Novolis.Transports.*
Promotion path (later, not now)
When stable: move Universe/Mesh/* (minus Sins/) → Novolis.Mesh.Core, publish 2026.1.*, Sins keeps MeshBridge / MeshPulse as app glue — same pattern as hull quotes leaving Sins for Logistics.