The Simulator

It does not model FrogNet. It runs FrogNet.

A simulator that reimplements a system and then tests the reimplementation proves that two pieces of code agree. They will — the same person wrote both, holding the same misconception. FrogSim simulates the environment and runs the system.

The network you don't have to build to be sure of it

A mesh is hard to test because the interesting failures need a fleet. Fifteen networks behind three access points, an Internet tunnel between them, a station that loses its radio and finds another -- you can describe that in a sentence and it takes a week and a pile of hardware to stand up. So it doesn't get stood up, and the failures wait until someone else finds them.

FrogSim removes the hardware from that sentence. You write the network down, it builds it, converges it, and tells you what happened.

It does not model FrogNet. It runs FrogNet.

This is the whole distinction, and it is worth being exact about.

A simulator that models a system reimplements its behaviour and then tests the reimplementation. What it proves is that two pieces of code agree -- which they will, since the same person wrote both, holding the same misconception.

FrogSim simulates the environment and runs the system. The machine, the network, the tuple space and the origin server are supplied. Discovery, route promotion, service election, FrogNet Memory, the semantic proxy and the daemon are the shipping code, unmodified, doing what they do on a node.

There is exactly one exception, and it is a single line. A node's identity comes from the interfaces of the machine it is running on. Fifty simulated nodes share one machine, so that one function is rebound to answer per node. Everything else -- every routing decision, every election, every tuple write -- is the real call path.

What a run does

You describe the network in one file:

net1:
  hardware: {profile: pi5, memory_gb: 64, disk: {class: class3}}
  projects_ssid: true
  roles: [mediahost, databasehost]
  in_range: [net3, net4]
  tunnel_to: [net5, net6]
  hosts:
    - {name: net1-db, hardware: {profile: db_appliance, memory_gb: 256}}

Or you describe the shape and let it build one:

generate:
  access_points: 3
  attached_per_ap: 15
  ap_mesh: full
  lan_shape: random
  seed: 1234

Either way the run brings every node up, advertises its capability into FrogNet Memory, converges the mesh the way the boxes do it -- merge a node, notify the neighbours it derived, drain the queue -- elects the database host, publishes sensor telemetry over time, reads that telemetry back from a separate process, and takes the access point away to watch a station find another one.

Then it writes a report: conditions, topology diagram, objectives, results, timings with distributions, and what was covered.

Topologies you write this way are not a side channel. They run in the gate beside the twenty built-in ones, over the same real kernel.

Running the real thing on it

A simulator that only converges networks is testing the plumbing. The claim FrogNet actually makes is about shared memory, and the way to test that claim is to write a known value on one machine and read it on another while the network is taken apart underneath.

That needs each node to be a real node. A network namespace gives it its own routing table, which is most of the way there -- but not all: it is a network namespace and not a filesystem one, so without more work every node reads the same /etc/hosts and resolves the database host to the same address, which defeats the entire point. A private mount namespace and one bind mount per node fixes it, and nothing on the real filesystem is touched.

With that, the applications run as themselves. The monitor's data layer is imported directly -- it is a terminal program only at the drawing, and its fetching is three modules with no terminal between them. It performs its ordinary two-step lookup: find the sensor by name, then fetch the payload by id. The value another node wrote comes back byte for byte. The A/V phone runs headless and places a call between two nodes, and the relay on the far side registers the peer.

The election is the more interesting one. Three nodes publish capability; the one with the most memory is not the one with the highest address. The election names the memory -- from inside a namespace, against a hosts file that node wrote for itself, with no part of the decision supplied by the harness.

And the partition test says something a healthy fleet never can. A reader cut off from the data host fails in three seconds with no route to host -- it does not hang, which is the failure everybody actually fears. After the heal the value is still right. The write attempted from the cut-off side never arrived and is simply not there afterwards. No merge, no conflict, no reconciliation pass. That is what "current state, never history" means when something goes wrong, and it is worth being able to demonstrate rather than assert.

Evidence, not assertion

The report's coverage section does not claim that a component ran. It proves it.

Every FrogNet component is registered with the log marker it emits and the source file that emits it. A component counts as exercised only if its own marker appears in the captured output of that run. No marker, no credit -- it is listed as not exercised, with the file named. The registry re-checks itself against the source on every run, so a log line that moves shows up as a stale registry rather than as quiet false coverage.

The same discipline applies to the harness. Its own checks are paired with a negative control that reproduces the older, weaker behaviour and requires each check to fail against it. A test that only ever passes proves nothing; the question is always whether it could have caught the thing it claims to catch.

What it found

A simulator earns its keep by finding what the running fleet cannot show you. The fleet is past the install step; a simulator starts there every time.

The clearest example: the two tables that hold FrogNet's semantic templates had no creator anywhere in the shipped tree. Code read them, a maintenance script truncated them, and the only CREATE for them sat in a file that the release build explicitly excludes and no installer loads. Every running node had the tables -- they were made by hand, once, long ago. Every new node would have failed on its first semantic request, permanently, with no path to recovery. Nothing in the fleet could surface that, because nothing in the fleet was new.

A second one has the same shape. Two services are supposed to start only after discovery converges, ordered behind a systemd target. The gate exists, both consumers name the target, and the target was never packaged into a release -- the step that collects unit files matched services, timers, paths and mounts, and not targets. systemd treats a want on a missing unit as a soft failure: it logs and carries on. So the ordering read as present in every unit file on every node and had never once been enforced, and nothing failed loudly enough for anyone to look.

Both are the same shape, and it is worth naming: a file the system depends on that no release ever ships. Every running node had it, acquired by hand at some point. Every new node would not have. A fleet cannot report that, because a fleet is made of nodes that already got past it.

Alongside them: a convergence validator that gave up after twelve hops and reported correct routes as unreachable; a merge loop that discarded the routing layer's own log evidence before anything could read it; a diagnostic flag read at import time, so enabling it later did nothing; and a sim helper that removed an access point without detaching the stations attached to it.

Where it stops

Stated plainly, because a simulator that overstates its coverage is worse than none.

DEMONSTRATED. Discovery convergence, route promotion and installation, service election from capability, FrogNet Memory reads and writes with freshness, telemetry round-trip, station re-association, and the semantic proxy and daemon serving live requests.

DEMONSTRATED. Packet delivery over a real kernel. Each node is a network namespace with its own routing table; the largest topology carries 220 ordered pairs through it. Topologies you write yourself run there too, not just the built-in ones.

DEMONSTRATED. Real applications over time. The monitor's own fetch code reads a value another node wrote and gets it right. The A/V phone places a call between two nodes. A value survives a partition and a heal; a write made from the cut-off side is simply absent afterwards -- not merged, not in conflict.

DEMONSTRATED. Semantic compression, between two nodes. It takes two: a request aimed at your own node is correctly served locally and never crosses the daemon. With two nodes, every request compressed, and 1,129,267 bytes of payload left as 216,252 bytes on the wire -- five times smaller, measured from the daemon's own log rather than from anything the test reported about itself.

DEMONSTRATED. Media host election, which for a long time elected nobody. The capability record was missing the two fields the handler gates on, so every candidate scored below the floor and the name was removed from /etc/hosts. It was not that no host qualified. It was that the question was never properly asked.

NOT DEMONSTRATED. Concurrency. Merges run one at a time in a single process, so every race is unreachable by construction. The wire has no loss, no latency and no jitter. And no run has yet predicted something a real box then confirmed, which is the difference between a good harness and a qualified simulator.

That last paragraph is the point of the section. A simulator that overstates its coverage is worse than none, because the green run becomes the reason nobody looks.

Measured

Three access-point machines, fifteen attached networks each, forty-eight nodes, full tunnel mesh between the access points, LAN shapes chosen at random from a fixed seed:

Convergence314 merge events, quiesced, 6.5 merges per node
Reachability0 of 2,256 ordered pairs unreachable
Electionone database host, agreed by all forty-eight independently
Telemetry144 sensor readings, verified from a separate process
Coverage17 components, proven from 2,282 captured log lines

Shape has a cost, and it is measurable:

LAN shapeMerge events
star198
snowflake226
snake283
ring283
random (seed 1234)314

Ring matches snake because a ring's closing edge is peer adjacency, not an uplink -- at the association layer the two are the same tree.

The applications, on three nodes each in its own namespace with its own kernel, hosts file and local cache:

Objectives12 of 12, three consecutive runs
Write then cross-node read0.195 s
Read from a cut-off nodefails in 3.2 s, no route to host -- does not hang
Electionnames the 32 GB node over the one with the highest address
After partition and healvalue correct; the cut-off side's write simply absent

Cost is not uniform across a network either. On the forty-eight-node run the merge times are two populations, not one: an access point averages 99 ms, a leaf averages 14 ms. An access point carries fifteen attached networks and the tunnel mesh, and walks all of it. A single total would have shown neither number.

The chapter-length version, including the negative controls and everything the harness has found, is in the build manual.

The Simulator, in Magnum Croakus → Demonstrations → Bring an oracle →