`ServerSystem::ComputeOutput` 0x00751fb0 and the whole of `ComputeOutputFromRates`
0x00751bb0, read from the instruction stream to the next function start. This is the
function lane E1 correctly identified as the real blocker on P01/P02/P03/P05/P06 after
its own chain came out 25/25 on BnkEl and did not unblock them.
The shape, and the fact everything turns on: construction points the build queue and
the ship-repair pass do not spend are redistributed over trade / terraform /
infrastructure, and the TRADE share is added to the money channel. Unspent
infrastructure points cascade into the terraform pool and unspent terraform points
cascade into money -- two hops, not one. So a colony with an empty build queue earns
the same money whichever way its sliders point, which is why the turn path's money
sits within one trade point of the projected path on this corpus.
Eleven addresses, five of them dropped as agreeing duplicates. New: the repair pass
0x00751590 (the B1 double-run side effect, and its round robin is provably a `min`),
the construction-point helper 0x00746830, the queue-demand walker 0x008251e0, the
ideal-suitability resolver 0x00745d60, and two helpers with non-standard conventions
that would be silently wrong read as thiscall -- 0x0074c6f0 takes `this` in ESI and
0x007460b0 takes the system in EBX.
Corrections in place: `income-term.md`'s "leftover science points" are leftover SHIP
CONSTRUCTION points (there is no science channel in this function), its
science-cascade bullet moves from inferred to instruction-verified, and
`output-term.md`'s 0x00751fa8 boundary is padding -- the `ret 8` is at 0x00751fa0.
Measured, with the engine change (sots-engine wip/output2 d3ee453):
turn1-state -> turn2-state 209 -> 157 closed 52 regressed 0 (was 51 / 0)
turn2-state -> turn3-state 108 -> 86 closed 22 regressed 0 (was 21 / 0)
Closed 1 per pair, regressed 0. Every prediction in the doc's section 4 held; the one
that was off is named and explained. The strongest check is not the leaf count but
the eleven-save self-check in section 5.3, where every difference between the turn
path and the projected path decomposes to the unit into the build queue's points.
24 KiB
The population → base-output term (lane N, 2026-09-08)
The formula that blocks P01 P02 P03 P05 P06 in src/app. Read from the instruction stream,
then hooked live on VM140.
Evidence: own objdump -d pass over Sword of the Stars.exe, every range disassembled to the
next function start and the real boundary found (rule 17). Field names per
findings/objects/struct-recovery.md.
0. Correction to strategic-turn-internals.md §3.3
That section says:
total = round( BaseOutput × mods ) // 0x00750480: BaseOutput from pop (0x007483b0: (pbon+Pop)/… resource-availability factor, capped by species +0x4c)
0x007483b0 is not the population→output term. It is the strip-mining / over-harvest resource
demand, its P is Res (+0x68) and not Pop, and species +0x4c is added, not a cap — the
cap is the available resource stock. (pbon+Pop) enters it only as a clamp01((pbon+Pop)×1e-5)
scale factor, and only when the over-harvest rate SRoh > 0. The line is corrected in place below
and in strategic-turn-internals.md.
The real population→output term is PopOutput(0) + PopOutput(1) + SlaveOutput() inside
0x00750480, and it is far simpler than the note implied: output points are linear in population,
pop × typeMod × 1.8 / 500000.
1. The call chain
| addr | name | conv / boundary | note |
|---|---|---|---|
| 0x00751bb0 | ServerSystem::ComputeOutputFromRates(int out[12], OutputRates* rates) |
thiscall ret 8, ends 0x00751fa0 |
repairs ships in orbit — not compare-safe |
| 0x00750480 | ServerSystem::ComputeTotalOutput(double SRoh) |
thiscall ret 8, ends 0x007505a5 |
side-effect free — the compare target |
| 0x00747d30 | ServerSystem::StripMineFraction() |
thiscall, returns float, ends 0x00747ddd | |
| 0x007483b0 | ServerSystem::OverHarvestDemand(double SRoh) |
thiscall ret 8, ends 0x007484c4 (Ghidra size 276 = correct here) |
|
| 0x0074d8f0 | ServerSystem::PopOutput(int groupType) |
thiscall ret 4, ends 0x0074da01 |
|
| 0x0074b880 | ServerSystem::SlaveOutput() |
thiscall, ends 0x0074b901 | |
| 0x0074b7a0 | ServerSystem::GroupOutput(int groupType, int species, double count) |
thiscall ret 0x10, ends 0x0074b871 |
the per-capita term |
| 0x00747ba0 | ServerSystem::GroupPopulation(int groupType, int species) → int64 |
thiscall ret 8 |
|
| 0x00746910 | ServerSystem::MoraleOutputMod(int species) → double |
thiscall ret 4 |
|
| 0x00535e00 | PopTypeRow(int t) |
cdecl | table at .bss 0x00b104e8, stride 0x30, 3 rows |
| 0x00535ca0 | InitPopTypeTable() |
— | the table is built in code, see §3 |
| 0x00545cc0 | SpeciesDef(int sp) |
cdecl | table at .bss 0x00b10a00, stride 0x184, 7 rows |
| 0x008e5680 | signed_cbrt(double) |
cdecl | x>=0 ? pow(x,1/3) : -pow(-x,1/3); exponent is the double 1/3 at 0x00a3a7c8 |
(Boundary corrected by lane C3: the ret 8 is at 0x00751fa0 and 0x00751fa3..0x00751faf is
int3 padding, so the 0x00751fa8 above was padding, not code.)
ComputeOutputFromRates is reached from ComputeOutput 0x00751fb0 (projected) and
ComputeMaxIncome 0x007521c0 (max), which are what ComputeBudget 0x00863030 and
UpdateBankruptcyLimits 0x00818600 call.
2. The formula
2.1 ComputeTotalOutput(SRoh) — 0x00750480
SRoh is the fifth float of the normalised OutputRates ({SRt, SRsc, SRtf, SRi, SRoh, SRs, int SRnr}, 0x1c), read at [ebp-0x4c] after NormaliseOutputRates. It is the over-harvest rate.
if (sys->PID == 0) return 0.0 // +0x100
if (sys->rbfl != 0) return 0.0 // +0x1dc, any rebelling species
resAvail = sys->Res // +0x68
if (PID && PID->[0x138]) resAvail += sys->MRes + sys->ARes2 // +0x70 + +0x6c (the AMine flag)
resTerm = (double)(sys->TRes + resAvail) // +0x74
* (double)StripMineFraction() // float, narrowed
* 0.9 // .rdata 0x00a05d48
baseTerm = OverHarvestDemand(SRoh) * (double)(float)SpeciesDef(PID->Species)->f50 // +0x50
popTerm = PopOutput(0) + 0.0 // .rdata 0x009e1e68 == 0.0
+ PopOutput(1)
+ SlaveOutput()
sum = baseTerm + resTerm + popTerm // in that association:
// ((y + x1 + (x0 + 0.0)) + (baseTerm + resTerm))
mod = (AddictionPhase(sys, sp) >= 3) ? ADDICTION_OUTPUT_MOD : 1.0
sp = PID ? (sys->indi ? sys->indi->[4] : PID->Species) : -1
sum *= PID->[0x124] // p.OutMod
sum *= sys->OutMod // +0x7c
sum *= PID->[0x224] // the game-setup handicap output multiplier
sum *= PID->[0x128] // p.RebOutMod
sum *= PID->[0x12c] // p.ScOutMod
return mod * sum
Every multiply is 80-bit x87 with no intermediate store, so the whole tail is one long-double
chain; only the two (double)(float) narrowings above are real roundings.
2.2 The population term — PopOutput 0x0074d8f0 and GroupOutput 0x0074b7a0
PopOutput(t): // t = 0 imperial, 1 civilian
sum = 0
for sp in 0..6:
n = GroupPopulation(t, sp) // int64
surplus = 0
if (PID && sys->indi == 0 && t == 1 && sp == PID->Species):
MaxPop(sys, PID, sp, &A, 0, 0) // 0x0074a6d0, out slot 4
MaxPop(sys, PID, sp, 0, 0, &B) // out slot 6
surplus = (B > A) ? (B - A) : 0
total = surplus + n
if (total > 0) sum += GroupOutput(t, sp, (double)total)
return sum
SlaveOutput(): // t = 2, no station factor, no morale
sum = 0
for sp in 0..6:
s = SlaveCount(sp) // 0x0074b610, int64
if (s > 0) sum += max(0.0, POPTYPE[2].out * 1.8 * ((double)s / 500000.0))
return sum
GroupOutput(t, sp, count):
if (!(count > 0.0)) return 0.0
q = count / 500000.0 // .rdata 0x00a1fb00 = 500000.0
sf = 1.0
if (PID != 0 && t == 0):
k = StationCount(sys, PID, 0) // 0x00815c10 -> 0x00815b10(.., 1)
b = (STATION_BONUS_IMPERIAL_OUTPUT > 0.0) ? STATION_BONUS_IMPERIAL_OUTPUT : 0.0
sf = 1.0 + (double)k * b
mo = (t == 1) ? MoraleOutputMod(sp) : 1.0
v = (double)POPTYPE[t].out * (sf * 1.8) * mo * q // 1.8 = .rdata 0x00a1faf8
return max(v, 0.0)
GroupPopulation(t, sp) (0x00747ba0):
t == 0 → (sp == (sys->indi ? sys->indi->[4] : PID->Species)) ? (sys->pbon + sys->Pop) : 0,
sign-extended to int64. Otherwise → Population::Count(sys->pbon2 /*+0x1b4*/, t, sp) + Population::Count(sys->Pop2 /*+0x1a0*/, t, sp).
MoraleOutputMod(sp) (0x00746910): 1.0 if no owner, if sys->indi != 0, or if
sys->cm.value[sp] == 0 (the Morale int[7] at +0x120). Else, with m = cm.value[sp]:
m >= MORALE_INCREASE_OUTPUT → MORALE_INCREASE_OUTPUT_MOD if that is > 0, else 1.0;
m <= MORALE_DECREASE_OUTPUT → MORALE_DECREASE_OUTPUT_MOD if that is > 0, else 1.0;
otherwise 1.0.
So the per-capita output rate is typeMod × 1.8 / 500000 — for an imperial population with no
stations that is exactly 3.6e-6 output points per head.
2.3 The two resource terms
StripMineFraction() 0x00747d30, returns float:
Fi = (double)float32(sys->ibon + sys->Infra) // +0x198 + +0x190, narrowed
V = signed_cbrt((sys->pbon + sys->Pop) / 100.0) * 0.01 // 100.0 @0x009e20e8, 0.01 @0x00a1a448
R = clamp01(V)
if (0.0001 + R >= 1.0) R = Fi // 0.0001 @0x00a1fa18
return float32( (R < Fi) ? R : Fi )
OverHarvestDemand(SRoh) 0x007483b0, ret 8, returns double — the corrected §3.3 row:
resAvail = sys->Res; if (PID && PID->[0x138]) resAvail += sys->MRes + sys->ARes2
P = (double)resAvail
B = 0.0
if (SRoh > 0.0):
S = sys->pbon + sys->Pop // int add, may wrap
R = clamp01((double)S * 1e-5) // 1e-5 @0x009faa28
B = max(SRoh * P * R, 1.0) // floor of 1.0, not 0.0
D = PID ? (double)(int)SpeciesDef(PID->Species)->f4c : 0.0 // +0x4c, an int
return min(P, max(D + B, 0.0))
ComputeOutputFromRates calls it a second time for the resource ledger:
need = ftol(OverHarvestDemand(SRoh)) + CivilianConsumption() (0x0074c6f0);
out[2] = min(resAvail, max(need, 0)); out[1] = min(out[2], ftol(OverHarvestDemand(SRoh)))
— which confirms B4's correction that out[1] is min(out[2], stripMineDemand) and names the
demand.
3. Where each input comes from — and this is the useful part
InitPopTypeTable 0x00535ca0 builds the 3-row table in code, from x87 literals, with only
three fields taken from GlobalConst slots. Read out of the initialiser (the function keeps six
values on the x87 stack and rotates them with fxch, so the rows are not adjacent in the
listing):
| row | +0 | +4 | +8 (max pop) | +0xc | +0x10 out |
+0x14 income |
+0x18 | +0x1c | +0x20 |
|---|---|---|---|---|---|---|---|---|---|
| 0 imperial | 0 | 1.0 | 50 000 000 | 0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 1 civilian | 1 | 0.25 | 20 000 000 | 0 | 0.33 | 0.33 | 1.0 | 0.5 | 2.0 |
| 2 slaves | 2 | 0.0 | 0 | 0 | SLAVES_OUTPUT_MOD |
SLAVES_INCOME_MOD |
SLAVES_REPAIR_MOD |
0.0 | — |
(the 50 000 000 confirms B4's "the 50,000,000 cap"; the GlobalConst slot pointers are 0x00ae2e88 → 0x00b0e9b0 out, 0x00ae2e84 → 0x00b0e9ac income, 0x00ae2e8c → 0x00b0e9b4 repair.)
So the whole population→output term is hard-coded in the executable for imperial and civilian population. The data-file dependency of §2 reduces to this closed list:
| # | value | where | needed when |
|---|---|---|---|
| 1 | STATION_BONUS_IMPERIAL_OUTPUT |
slot 0x00af08f4 → 0x00af08f0, in the file image as 0.1f |
any imperial pop with ≥1 station |
| 2 | MORALE_INCREASE_OUTPUT (int) |
slot 0x00aec784 | civilian pop with non-zero morale |
| 3 | MORALE_INCREASE_OUTPUT_MOD (float) |
slot 0x00aec78c | ditto |
| 4 | MORALE_DECREASE_OUTPUT (int) |
slot 0x00aec794 | ditto |
| 5 | MORALE_DECREASE_OUTPUT_MOD (float) |
slot 0x00aec79c | ditto |
| 6 | SLAVES_OUTPUT_MOD (float) |
0x00b0e9b0 | any slaves |
| 7 | ADDICTION_OUTPUT_MOD (float) |
slot 0x00aeca50 → 0x00aeca4c | addiction phase ≥ 3 |
| 8 | SpeciesDef[sp]->f4c (int) |
0x00b10a00 + sp·0x184 + 0x4c | always (the base resource demand) |
| 9 | SpeciesDef[sp]->f50 (float) |
+0x50 | always |
Everything else — 1.8, 500000, 0.9, 1.0, 0.33, 1e-5, 0.01, 100.0, 1/3, 0.0001
— is a .rdata literal, i.e. a fact about the algorithm, not about the shipped data.
Nine numbers. That is the whole tuning-table surface of this term.
4. PREDICTION — written before the run
Build output-<sha>, hook Game::ServerSystem::ComputeTotalOutput (0x00750480) in compare
mode. Chosen over ComputeOutputFromRates deliberately: that one repairs ships in orbit and is
not compare-safe. ComputeTotalOutput and every one of its callees were checked for stores to
the game state and are read-only (0x0074a6d0's only writes are through its int64 out-parameters).
4.1 What I expect
- Call count.
ComputeOutput/ComputeMaxIncomeare called from the UI as well as the turn, so the count will be dominated by UI polling, exactly asComputeBudget's 4,437 were. I expect hundreds to thousands of calls, and only a few tens of distinct system states — and I will report distinct states, not the call count (rule 15). - Zero divergences on the returned double, compared as exact IEEE-754 bits.
SRoh == 0.0on every call from all 11 saves (all 28 systems inturn1-state.savcarrySRoh = 0.0), so theSRoh > 0branch ofOverHarvestDemandwill be unexercised. I will flag it as a hypothesis (rule 6) rather than claim it.POPTYPE[0].out == 1.0f,POPTYPE[1].out == 0.33f,POPTYPE[1].maxpop == 20000000,POPTYPE[0].maxpop == 50000000read live out of 0x00b104e8. If any of these differ from the initialiser I read, my reading of thefxchrotation is wrong and everything in §3 is suspect.STATION_BONUS_IMPERIAL_OUTPUT == 0.1flive (the file image value survives the data load).
4.2 Numbers, from turn1-state.sav, before the run
Gamma Cephei (Idx 4, human homeworld): Pop = pbon = 1e9, Pop2 = {type 1, species 0, 5e8},
Res 5000, MRes 155, ARes2 0, TRes 0, Infra = ibon = 1.0, OutMod 1.0, cm[0] = 75,
SRoh 0, rbfl 0.
GroupPopulation(0, 0) = 2e9→GroupOutput(0,0,2e9) = 1.0 × sf × 1.8 × (2e9/5e5)= 7200 × sf,sf = 1 + 0.1kforkstations.GroupOutput(1, 0, 5e8 + surplus) = 0.33 × 1.8 × mo × ((5e8+surplus)/5e5)= 594 × mo plus the surplus term.StripMineFraction:cbrt(2e9/100) × 0.01 = 2.7144…→ clamps to1.0;Fi = 2.0;min(1.0, 2.0) = 1.0. SoresTerm = (0 + resAvail) × 1.0 × 0.9= 4500 without the AMine flag, 4639.5 with it. This is a live discriminator forPID->[0x138].baseTerm = min(resAvail, max(f4c, 0)) × f50(theSRoh = 0form).
So the returned total for Gamma Cephei should be
(baseTerm + resTerm + 7200·sf + 594·mo) × p.OutMod × 1.0 × p.f224 × p.RebOutMod × p.ScOutMod,
and with the four player multipliers at 1.0 and no stations it should sit in the low five figures.
The single strongest check: total must be linear in Pop. Koa'Vo has Pop = 1.2e8,
pbon = 0 → imperial term 1.2e8/5e5 × 1.8 = 432, exactly 2000/7200 of Gamma Cephei's, with
Res 3394 giving resTerm = 3054.6. Two colonies, one ratio, no fitted parameter.
4.3 Falsification — how this model could be wrong, and the symptom of each
| way it could be wrong | symptom in the run |
|---|---|
[ecx+0x68] in 0x007483b0 is not Res — I matched the offsets to struct-recovery.md's memory column, which is 8 more than the wire column |
the AMine-gated +0x70 + +0x6c sum would not match MRes + ARes2; ours diverges on every call by a fixed additive amount, and the traced resAvail will not equal any save field |
my fxch-rotation reading of InitPopTypeTable is off by one register |
POPTYPE[1].out reads live as 0.25 or 0.5 or 2.0 rather than 0.33; the civilian term is then wrong by a constant ratio and only civilian-bearing systems diverge |
the max(v, 1.0) I read at 0x74844c–0x748451 is really min |
invisible here, because SRoh = 0 on every save — so this is explicitly unverified and stays a hypothesis |
GroupOutput's station factor applies to civilians too (I read the t == 0 test at 0x74b7e0 as excluding them) |
systems with stations diverge; systems without do not — so a divergence that correlates with NumSnF > 0 names this |
the MoraleOutputMod thresholds are inclusive the other way (> vs >=) |
a divergence only on systems whose cm[sp] sits exactly on a threshold; likely unexercised, so it will be reported as a hypothesis |
| the association of the additive chain differs (x87 is not associative) | a divergence in the last 1–2 ulps only, on the largest colonies |
p.f224 / RebOutMod / ScOutMod are not all 1.0 in these saves |
ours diverges by a clean multiplicative factor on every call of one player — which identifies the slot rather than refuting the model |
A run with 0 divergences and only 2–3 distinct colony states would be a weak result and will be
reported as one. The strong form is the turn1 → turn2 → turn3 and Zuul turn15 → turn23
progressions, where Pop, Res and Infra all move.
5. The live result
Two builds, two workloads, both on VM140. Hook set: Game::ServerSystem::GroupOutput and
Game::ServerSystem::ComputeTotalOutput, both in compare mode, every other hook off.
Traces verify/traces/output-*.jsonl.gz, reports verify/results/compare/output-*.md.
| run | build | workload | calls | compared | diverged | undeclared writes |
|---|---|---|---|---|---|---|
| 1 | output-b48d860-20260908T1527Z |
ref-turn2 load + one End Turn (Human, turn 2 -> 3) |
14,853 | 14,853 | 4,957 | 0 |
| 2 | output2-b48d860-20260908T1553Z |
zuul-turn5 load + one End Turn (Zuul, turn 5 -> 6) |
11,966 | 11,966 | 0 | 0 |
| 2 (cont.) | same | + ref-turn2 reloaded |
24,357 | 24,357 | 1 | 0 |
Run 1's 4,957 divergences were every one of them exactly one ulp, and every one of them on
the civilian population row. The cause was not the model: it was that a 32-bit x87 build of
game::sim is allowed to leave an intermediate in a register at the register's own precision,
while the original's x87 rounds every multiply to double (its control word is 0x127f). Run 2
carries sim::Narrow, which forces the round the original performs anyway, and the population
term then matches bit for bit on 13,105 calls across two species.
5.1 Every prediction in §4.1, checked
| predicted | measured | verdict |
|---|---|---|
POPTYPE[0].out == 1.0f |
1.0 |
HELD |
POPTYPE[1].out == 0.33f |
0.330000013 |
HELD |
POPTYPE[0].maxpop == 50 000 000 |
50000000 |
HELD |
POPTYPE[1].maxpop == 20 000 000 |
20000000 |
HELD |
POPTYPE[2].maxpop == 0 |
0 |
HELD |
STATION_BONUS_IMPERIAL_OUTPUT == 0.1f |
0.100000001 |
HELD |
SRoh == 0 on every call |
0 on all 24,357 | HELD (and the over-harvest branch is therefore unexercised -- see §5.3) |
| zero divergences on the returned double | 0 of 13,105 on GroupOutput, 1 of 11,252 on ComputeTotalOutput |
HELD, with one residual |
| hundreds-to-thousands of calls, few distinct states | 24,357 calls, 13 distinct system states | HELD, and the coverage is thin exactly as forecast |
So the reading of InitPopTypeTable's x87 rotation was right: the imperial and civilian output
modifiers really are literals in the executable, and the shipped data changes neither.
Values the data files DO supply, read out of the running process (these were unknown before this run):
| key | value |
|---|---|
STATION_BONUS_IMPERIAL_OUTPUT |
0.1f |
MORALE_INCREASE_OUTPUT / _MOD |
85 / 1.5f |
MORALE_DECREASE_OUTPUT / _MOD |
20 / 0.5f |
SLAVES_OUTPUT_MOD |
3.0f |
SpeciesDef +0x4c (base resource demand) |
0 for Human and Tarkas, 10 for Zuul |
SpeciesDef +0x50 (resource output factor) |
10 for Human and Tarkas, 40 for Zuul |
The last two are the reason a species table is not optional: they differ by species and they
scale a whole term. Every colony in the corpus carries cm = 75, which sits strictly between
20 and 85, so the morale multiplier is 1.0 on every call the corpus makes -- both morale
branches are unexercised and stay hypotheses.
5.2 The prediction that was wrong, and it was mine, not the model's
§4.2 said Gamma Cephei's strip-mine fraction would be min(1.0, 2.0) = 1.0 and its resource
term 4500. It is 2.0 and 9000. The branch at 0x00747db4 is a substitution, not a
clamp: once the population term reaches 1 - 1e-4 the infrastructure term replaces it outright
rather than capping it, so a colony with a pending infrastructure bonus extracts at better than
unity. The disassembly said so and I mis-evaluated my own reading of it by hand. The model in
the code was right from the start; the arithmetic in the prediction was not. Caught by the very
first host test, before any VM time was spent.
5.3 What this run did NOT cover -- read this before quoting the zero
24,357 calls sounds like a lot. It is 13 distinct system states and 12 distinct
GroupOutput rows:
- 5 colonies in total across both workloads (Gamma Cephei, Ke'Dolarra, Koa'Vo, Gallandro and three more Zuul systems), and 4,832 of run 1's calls are the UI polling one of them.
SRohis 0 on every single call, soOverHarvestDemand's wholerate > 0branch -- including themax(v, 1.0)floor, which is the one place I could most easily have read aminas amax-- is untested. It is a hypothesis, not a finding.- The station factor is untested. No system in either workload has a station, so
stationFactorwas 1.0 on all 13,105 rows and the hook's ownoursassumed zero stations. A save with an imperial station would test it; none exists. - The slave term is untested.
slaveGroupPopwas 0 on every call, including the Zuul workload, soSLAVES_OUTPUT_MOD = 3.0was read but never used. - The morale multiplier is untested (see above): every colony sits at 75.
- The addiction multiplier is untested: no colony reached phase 3.
- The civilian capacity surplus is untested: no colony was at its cap.
- Zuul have no civilian population at all, so run 2 exercises only the imperial row -- which is why its 11,966 calls found zero divergences even before you account for the fix.
The honest summary is: the imperial and civilian population laws are verified hard, on two species and two workloads; every conditional hanging off them is unexercised and labelled.
5.4 The one residual
ComputeTotalOutput diverges on exactly one of 11,252 calls in run 2: Koa'Vo, the independent
colony, 9248.450318530804 against 9248.450318530802. One ulp, in the five-multiplier tail
(OutMod there is 2.18500018, the only non-trivial one in the corpus). It is immaterial to
every consumer: the caller rounds this value half-to-even to an integer before splitting it
across the output channels, and 9248.4503 rounds to 9248 either way. It is recorded rather than
papered over, and it is not fixed.
6. What this unblocks, and what it does not
The brief's premise was that this term blocks P01 P02 P03 P05 P06. It is now verified -- and
that turns out not to be enough to unblock them, for a reason worth stating plainly.
ComputeBudget's missing input is not a system's output; it is a system's money, which is
ComputeMaxIncome(s) = max(ComputeOutputRates(s, maxMods)[3], 0). That runs the verified output
total through a second chain -- TradePointsToMoney -- which has its own population law
(typeIncomeModifier / 14000, no 1.8 factor, a different table column) and its own multipliers.
Lane Y's tools/max_income_oracle.py inverts the stored BnkEl to state the true
Sum max(ComputeMaxIncome(s), 0) for 25 player-records across the corpus. tools/max_income_predict.py
(new, this lane) computes the same number from colony state using the verified output term plus
that income chain and compares. Result:
6 of 25 records MATCH exactly; 19 differ.
The six that match are every record whose owner is the human player or the independent colony --
including turn1/turn2/turn3-state for both -- and they match to the unit on a five-figure
number, three of them on a moving workload. The nineteen that differ are all AI-owned, and
the single-system ones differ by a clean factor of exactly 1.1 (246992 x 1.1 = 271691).
That factor is not on the wire: it is DifficultyMods(owner)->+4, the AI trade/income
difficulty multiplier that TradePointsToMoney applies and that formula-gaps.md Q3 already
names as unresolved. The multi-system AI empires differ by more, so there is at least one
further term there (candidates, in order: trade routes, stations, the suitability money cost on
non-ideal planets).
So the blocker moves rather than clears: the population -> base-output term is closed, and
what now stands between src/app and systemIncome is the income tail, not the output term.
That is a smaller and much better-specified target, and the 25-record oracle makes it falsifiable
without a VM.