ai: bind the fleet-gather ordering contract where it will be needed

The gather/mint subsystem is not written yet, so this records the requirement at
the emission layer rather than building the subsystem speculatively.

Measured, not guessed: the original's assignment pass walks a container keyed on
fleet->Location, a heap pointer, so its whole observed outcome set on a rich turn
is the two orderings of two heap pointers under LFH randomisation. There is no
original order to match -- it is neither ascending systemId nor ascending minimum
ship id in any process observed -- so we choose one member of its outcome set and
say so. The order must be imposed at the gather, not at assignment: the fleets
already carry their ids when the assignment walk first touches them.
This commit is contained in:
alex 2026-09-09 03:41:32 -04:00
parent 55408a980a
commit 45b16ef626

View file

@ -162,6 +162,45 @@ struct ColonizeOrder {
int trailing = 0; int trailing = 0;
}; };
// ---------------------------------------------------------------------------------------------
// A CONTRACT FOR WHOEVER BUILDS THE FLEET GATHER
//
// These take `fleetId` as a parameter: this layer emits orders and does not form fleets or mint
// ids. The subsystem that does is not written yet, and when it is, it inherits a requirement that
// was measured rather than guessed, so it is recorded here where it cannot be missed.
//
// In the original, two processes running the same turn from the same save with the AI's seeds
// pinned produce different saves -- 35 leaves of 61,147, and all 35 are one transposition: two
// fleets exchange contents. The command blocks differ in exactly three words. The id counter is
// deterministic; the ids are minted in the same order every time. What varies is WHICH GROUP EACH
// ID LANDS ON, because the pass that assigns them walks a container ordered by
// `fleet->Location` -- a HEAP POINTER, with the fleet pointer breaking ties. Under LFH
// randomisation two allocations of the same size land in either relative order, so the original's
// whole observed outcome set on that turn is the two orderings of two heap pointers.
//
// Three consequences bind us:
//
// 1. **Order the groups by a key that is a function of the save, and mint in that order.**
// `(systemId, then minimum ship id)` with a total tie-break. Any deterministic key would do:
// the original's order is NOT ascending systemId and NOT ascending minimum ship id in any
// process observed, so THERE IS NO ORIGINAL ORDER TO MATCH -- we are choosing one member of
// its outcome set, not reproducing a hidden rule. Say so; do not let a later reader mistake
// this for a recovered behaviour.
// 2. **Impose it where the groups are gathered and the ids are minted, not at assignment.**
// The fleets already carry their new ids when the assignment walk first touches them, so
// canonicalising at assignment is one level too low and fixes nothing.
// 3. **A byte-exact replay needs the original's order, and the block already carries it.**
// List 10 records `{systemId, fleetId, ships[]}` per group in visit order, so every captured
// run states its own order for free. An optional override that replays a captured order is
// what turns "equal modulo a relabelling" into "equal" -- three words per new fleet, the same
// cost class as pinning a seed.
//
// Evidence: sots-re `findings/control-flow/fleet-visit-order-mechanism.md` (the container and its
// key, measured across three processes including a branch flip),
// `findings/control-flow/pinned-seed-rich-turn.md` (the residue),
// `findings/resolutions/2026-09-09-fleet-id-order-residue.md` (the ruling).
// ---------------------------------------------------------------------------------------------
// List 8. The route is a counted vector of system ids, so a multi-hop order is longer on the wire // List 8. The route is a counted vector of system ids, so a multi-hop order is longer on the wire
// than a single-hop one -- but it is still ONE element and therefore ONE counter bump. // than a single-hop one -- but it is still ONE element and therefore ONE counter bump.
struct FleetMoveOrder { struct FleetMoveOrder {