sots-engine/docs/L3-predictions.md
alex 6699f543bc watch.mode=cont: both containers, both ends, re-armed every turn; and hooks=off no longer disarms the watchpoints
Lane L3 needed to watch the trade-route and spy-program vectors across a whole game
played forward, not one turn of one save. `modcount` and `tshn` arm once on purpose --
their targets are picked from one turn's state and re-picking them would move the
measurement -- so this is a third mode rather than a change to either.

`cont` puts all four debug slots on the two containers, `_Myfirst` as well as `_Mylast`.
Both vectors are default-constructed with all three pointers zero, so the first element
writes all three: watching only `_Mylast` cannot separate "allocated for the first time"
from "appended to an existing buffer", and those are different events in the model this
lane set out to falsify. It re-arms and re-logs on every End Turn, and the canary
self-test's counter is therefore read as a delta -- on the arm-once modes the delta is
the old value, so their log lines are byte-identical.

`ReportContainer` is factored out of `ArmTshnSlots` so both modes emit the same container
line. Lane W3's published count=0 is the control every later count is compared against,
and a reformatted line would have made that comparison a judgement call.

The defect: `Shim_Init` returned before `install_watchpoints` whenever the trace mode was
`off`, so `hooks=off watch=on` printed `watch=on` in the banner and armed absolutely
nothing -- a config that reports a confident zero, which is the failure method rule 1
exists to catch. The watchpoints are an independent instrument with their own arming
detour and no trace records, and a long play session wants them without paying 30-45 s
per End Turn for template hooks that measure nothing it is asking about. MinHook is now
initialised and the module installed on the `hooks=off` path when `watch=on`.

Configs: shim.cfg.l3cont / .l3control differ in exactly one key for rule 19;
.l3probe is lane H's hp11 verbatim plus the three watch keys, so the entry counts stay
comparable to lane H's empty-container baseline line for line.

Gates: clean_room_check OK; host ctest 54/54; CT111 shim cross-build OK, exports 66
names identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
2026-09-08 19:10:10 -04:00

134 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Lane L3 — predictions, written before the build (method rule 2)
Lane L3 owns VM144 (`sots-re-win10-144`, `re@192.168.10.144`, a ZFS clone of VM140).
Target: manufacture a workload in which the trade-route vector and/or the spy-program
vector is **non-empty**, then measure whether the four RNG-bearing tail callees draw.
Instrument: `watch.mode=cont`, a third mode on lane W3's watchpoint module. Same single
arming detour (`StrategyServer::ApplyAllTurnCommands`, RVA `0x0038f6a0`), same VEH, same
canary, same hit format. Two differences from `tshn`:
1. it arms **all four** debug slots on the two containers — `_Myfirst` and `_Mylast` of
each — so an allocation (`_Myfirst` moves from 0) and a `push_back` (`_Mylast` moves)
are separable;
2. it **re-arms and re-logs every End Turn** instead of only the first. Lane W3 printed
one count from one turn of one save; this workload is a game played across many turns,
and the question "did the container grow on turn N" needs a line per turn.
---
## What the static reading says the workload is (before any run)
Read out of `sots.exe`, `TechTree/MasterTechList.tech`, `Data/Strategy/StrategyVars.txt`,
`Species/*/sections/*.shipsection` and the repo's own catalogs. This is the part three
previous lanes did not have, and it changes the cost estimate by an order of magnitude.
### Trade routes
* The gate is the tech **`CCC_FTLEcon`** (id 10026) — it sets `ServerPlayer+0xff` (`CnTrd`)
unless the player is `RebAI` (`ghidra/addresses.json:1510`, `tech-effects.md:64`).
* Cost: `CCC_ROOT → CCC_FTLBrdB` (RP 4,000) `→ CCC_FTLEcon` (RP 18,000). **Zuul cannot
research it** (`Zuul:0` in the availability line) — so the workload must not be Zuul.
* `StrategyServer_RegisterTradeSystems 0x007adc80` registers, for every owned system whose
owner has `CnTrd`, `ServerSystem::NumTradeRoutesSupported 0x00819d20` route slots, and
that function is `ceil(civ/REQ_CIV) + ceil(imp/REQ_IMP)` with a **floor of 1**.
* **Therefore the population thresholds in `StrategyVars.txt`
(`TRADE_ROUTE_REQ_CIVPOPULATION 200000000`, `TRADE_ROUTE_REQ_IMPPOPULATION 500000000`)
are not a hard gate — they scale the count, and the minimum is 1 per owned system.**
* `TRADE_SECTOR_SIZE 10.0` — routes live inside a trade sector, so the endpoints have to be
near each other.
* A **trade station** (`DNStationTrade`, station type 4) adds `STATION_TRADE_NUMROUTES 2`
and `STATION_BONUS_TRADE_INCOME 0.25`. It is a *bonus*, not a prerequisite.
> **Prediction T1.** The premise carried in `tail-probes.md` §3.2 and repeated in the L3
> brief — "trade income needs trade-station construction plus its tech" — is **wrong about
> the station**. `CCC_FTLEcon` plus two owned systems inside one trade sector is sufficient
> to make the trade-route vector non-empty. No trade station is needed.
>
> *Falsifier:* a game with `CCC_FTLEcon` researched, ≥2 owned systems within 10 units, and
> `trade routes … count=0` still logged after several End Turns.
> **Prediction T2.** The vector fills during turn processing, not at the moment the tech
> completes, so the first non-zero count appears on the End Turn **after** the one that
> finished `CCC_FTLEcon`.
### Spy programs
Nobody had a click path. The static reading gives one.
* `Species/<race>/sections/_Spy.shipsection` — `entity_class "SpyShip"`, `section_type
mission`, `nodesign 1`, `spy 1`, `cost 20000`, and
`requires CCC_SpyBm` + `requires IND_SlvgTech`.
* The **spy tender** — the "ship a spy can dock with" in `Can't build spy: Fleet does not
have a ship spy can dock with.` — is the section carrying `spytender`. In this image that
is exactly one section per race: **`CRRepairandSalvage`** (Cruiser Repair & Salvage
mission section, `requires IND_SlvgTech`, cost 60,000). Nothing in the UI says "tender".
* Tech cost: `IND_OrbFound → IND_CruisCon` (16,000) `→ IND_SlvgTech` (12,000) for the
tender and half the spy; `CCC_ROOT → CCC_FTLBrdB` (4,000) `→ CCC_SpyBm` (12,000) for the
other half. `CCC_FTLBrdB` is shared with the trade chain.
* The action path is the **`Special` menu on the fleet panel** (`(229,464)` at 1024×768):
`SHIPACTION_BUILDSPY` / `DEPLOYSPY` / `PICKUPSPY`, backed by `Game::BuildSpyDialog`,
`Game::DeploySpyDialog`, `Game::PickupSpyDialog` and the `Game::CanBuildSpy` functor.
* The engine's own refusal strings enumerate the preconditions exactly:
build — own system, player has ability, fleet has a tender, tender not already carrying;
deploy — **not your own system**, no enemy fleet present, and
`Can't deploy spy: Spy requires an asteroid belt to hide.`
(`SPYSHIP_NEARBY_ASTEROID_RANGE 500.0`; `STARMAP_ASTEROIDBELT_ODDS 25` %).
> **Prediction S1.** `ServerSpyManager`'s vector is the spy-**craft** list, and it grows at
> **Build Spy**, not at Deploy — the `Game::SpyCraft` record has `atto` (attached-to) and
> `tdep` (turn deployed) fields, which only make sense if the object exists while still
> docked. If S1 holds, the spy half of this workload needs **no second empire at all**.
>
> *Falsifier:* Build Spy succeeds (the fleet shows a docked spy) and the next End Turn still
> logs `spy programs … count=0`. That would move the growth point to Deploy and make an
> enemy colony with an asteroid belt mandatory.
> **Prediction S2.** The trapped write to the spy vector's `_Mylast` will have an `eip`
> inside `ServerSpyManager` and an `ebpret` inside the turn-command applier, not inside a UI
> class — because the debug registers are armed on the **turn thread only**
> (`ArmCurrentThread(GetCurrentThread())`), and SOTS is lockstep: the UI issues a command
> and the server applies it. If the write instead happens on the UI thread, **no trap will
> be taken at all** and the count line is the only evidence. This is the rule-20 trap in
> this instrument: a zero trap count with a rising count line means *wrong thread*, not
> *no write*.
### Getting there without twenty turns of play
The `Custom` game-setup screen (`GAMESETUPFE_BTN_CUSTOM`) exposes
`GAMESETUP_INITCOLONIES`, `GAMESETUP_INITSAVINGS`, `GAMESETUP_INITTECHS`,
`GAMESETUP_MONEY_RESEARCHMOD`, `GAMESETUP_MONEY_INCOMEMOD`, `GAMESETUP_SYSTEM_DISTANCE`,
`GAMESETUP_SYSTEM_SIZE`, `GAMESETUP_SYSTEM_SUITABILITY`. All of it is **stock**: no data
file is modified, no save is edited, so the resulting save is a legitimate corpus member.
> **Prediction W1.** With initial colonies, initial savings, initial techs and the research
> modifier at maximum on a small close-packed map, both tech chains are reachable inside
> ~10 End Turns rather than the "tens of 30–60 s End Turns" the brief budgets.
---
## Instrument predictions
> **Prediction I1 (control, rule 19).** `watch=on watch.mode=cont` is byte-neutral: one End
> Turn from `ref-turn2.sav` reproduces the determinism oracle
> — `(Autosave EndTurn).sav` `bb4fd9ac89f41e3b…`, `(Autosave).sav` `978041acd168b56e…` —
> exactly as `watch=off` does. The two configs differ in one key.
> **Prediction I2.** On `ref-turn2.sav` the new mode reproduces lane W3's numbers:
> both managers non-null, all six pointers zero, both counts 0, zero traps. If it does not,
> the mode is wrong and nothing measured with it later counts.
> **Prediction I3.** The canary self-test passes on **every** turn, not just the first —
> the delta form of the counter is new in this mode and is itself a thing that can be wrong.
---
## What would make this lane's result worthless
* Counting a container from the wrong base. `S` is the `ApplyAllTurnCommands` `this`; every
`StrategyServer_off_*` is in the **`S+4`** frame (`lane-t.json:172`). A wrong base yields
a plausible-looking zero.
* Reporting "0 traps" when the writer ran on another thread (S2).
* Producing a save whose non-empty container came from a modified data file. Nothing in this
lane modifies `sots.gob` or any loose data file; if that ever changes it must be recorded
in the save's provenance line.