TacReport in particular: TRnc is a count and TRships/TRsats/TRshipsL are ONE interleaved loop body, not three trailing runs -- which is why two lanes could not settle it from a flattened linear recovery with TRnc == 0 everywhere. Also two corrections the conformance binding found: Game::CombatReport's auto and cdst are bools and dur/cdt/cdi are floats (cdt was being read as 1070805848 instead of 1.598), and Game::CombatWeaponReport's damage quartet is flat with float dami/damt -- the nested 'dams' frame the shape modelled does not exist, and modelling it also inflated the coverage denominator by one per weapon report. FTPnts stays carried and labelled: the one item the recovery itself marks unresolved, count 0 everywhere, element framing a property of the helper.
11 KiB
Paying the ratchet debt: the five bodies the new saves exposed
Lane WS, 2026-09-08. Host/static only — no VM. Continues findings/objects/wire-schema-closeout.md
(lane W) and findings/objects/wire-schema-channel.md (lane G).
Headline: 97.6 % → 99.9959 % on the worst save, and the bar did not move
The corpus went from 11 saves to 20 in one evening as lanes manufactured workloads nobody had.
The coverage ratchet in test_save.cpp broke on contact — named coverage fell to 97.6 % against a
99.99 bar — because four new saves carry content no shape named. That is the ratchet working
(rule 27). The reader was sound on every one of them the whole time: round trip: tree identical, typed identical. It carried the bytes; it did not name them.
Named coverage per save, before → after. The honest metric is CoverageArchive's split of items a
field names from items a generic Node merely carries (rule 7), never round-trip success.
| save | before | after | opaque items |
|---|---|---|---|
| human-turn5-traderoutes | 99.0003 % | 99.9951 % | 409 → 2 |
| human-turn8-traderoutes | 98.6349 % | 99.9955 % | 613 → 2 |
| human-turn11-spytechs | 97.6382 % | 99.9959 % | 1166 → 2 |
| human-turn15-spyprogram | 96.9412 % | 99.9964 % | 1706 → 2 |
| the other 16 saves | 99.9940–99.9953 % | unchanged | 2 → 2 |
| corpus | 99.5021 % | 99.9949 % | 3926 → 40 |
All 20 saves are above the ratchet. The 2 items left on every save are the MT19937 blob, which is deliberately opaque. No path stayed opaque that any save exercises.
Conformance: 100 shapes bound, 966 items matched, 0 MISMATCH (was 87 / 856 / 0). Fourteen new
bindings, every one a full match with zero wire-only and zero shape-only items. ctest 58/58 over
all 20 saves; tools/clean_room_check.sh OK; shim cross-build clean (see §6).
1. <rest:prep> — 704 items on turn11, the largest. Closed.
Game::CombatPlayerReport's tail. The shape stopped at mxmsl and swept the rest into ar.rest.
The recovery had the whole thing all along — the corpus simply had no combat report until the
trade saves — and the data confirms the flattened loops item for item:
plr ai ally status mxeng mxcls mxmsl
ncls <- count
cls nshp nsat nshfld nshlst nsatl <- x ncls, one loop body of six
nsec <- count
ssec nshp <- x nsec, one loop body of two
ndam
srep { count, elements } <- Game::CombatShipReport
Game::CombatShipReport in turn: name did cls caps2 nshp nfld nlst dtak dams damp dami damt,
where caps2 is an i64 (the Caps bitfield) and dami/damt are floats.
2. TacReports — 369 items, and why two lanes could not close it
Lanes B5 and J both recorded Game::TacReport as carried-not-typed, with the same reason: the
recovery ends with a computed count TRnc and then three trailing scalar items —
TRships, TRsats, TRshipsL — and there is no way to read a flattened linear pass and tell
- three trailing runs (
TRships× n, thenTRsats× n, thenTRshipsL× n), from - one loop body of three (
TRships TRsats TRshipsL, × n).
Both produce the same three items in the same order in the recovery, because the recovery cannot
see the loop. And TRnc was 0 in every save in the corpus, two levels down under a count that
was itself 0, so the wire could not arbitrate either. Neither lane guessed, which was right.
The trade saves settle it by inspection. TRnc == 3, and the wire reads:
TRnc 3
TRships 0 TRsats 0 TRshipsL 0
TRships 0 TRsats 0 TRshipsL 0
TRships 0 TRsats 0 TRshipsL 0
Interleaved. One loop body of three, run TRnc times. It is a narr, and the whole
20-item class now binds at 20/20. Game::TacReportEvents (TRby / TRto) comes with it: two
ints, four floats, one bool.
This is rule 6 working as intended in both directions — the path was flagged as a hypothesis rather than closed on a guess, and when a save finally exercised it the answer was one line of dump.
3. Lay — 79 items, entirely new. Closed.
Game::FleetLayout, gated by HLay, which is false on every fleet in the old corpus. Three
NULL-named items: a Game::FieldTemplate frame, a count, then that many ship ids. The count word
is on the wire (narr), so nothing here is inferred.
4. <rest:trdmgr> — 9 items — is rt, and it has no count word
Game::ServerTradeManagerImpl writes NumTradeSectors, the sector pairs, SctSize, and then
Game::TradeRoute frames tagged rt. The recovery marks rt a loop body (member: false) and
there is no count item before it in the table and none on the wire — the trdmgr frame holds
1 + 2×6 + 1 + 1 = 15 items on turn11, which accounts for every one. So it is an uncounted run,
read with ar.repeat("rt", …) keyed on the tag, the same shape as DSec in a design.
Both saves that have any route at all carry exactly one, so "uncounted" is the framing the recovery supports; "exactly one" is not a claim the evidence can make.
Game::TradeRoute is eight ints: tro trfow trfr trfrs trtow trto trtos trtc.
5. fwarn — 3 items — and two more the brief did not list
fwarn=Game::ServerTradeSector::FreighterWarning,{pid, ntrns}. The count was 0 in every sector of every save until turn11, where a raided route produced one.spy=Game::SpyCraft(14 items on turn15, the first save withnspy != 0): five ints, two floats, four ints, a float, then anncpcount and itscplelements.Dwg=Game::WeaponGroups(17 items on turn15, the first design withDwgvset):wgngcount, then(wgid, wgb)pairs wherewgbis aGame::GunBankSelectionframe. That frame carries a run of NULL-named ints — three in the only record we have — with no count word, so it too is read as a run rather than as a fixed three.
6. Two corrections found by binding shapes to the table (rule 11)
Neither was visible from the round trip, which is the point of the conformance test.
Game::CombatReport: five items were the wrong primitive. auto and cdst are BOOLs;
dur, cdt and cdi are FLOATS. All five were int32 in the shape. Four of them are 0 or 1 on
every save, where a bool and an int are the same bytes under a four-character tag, so nothing ever
moved. cdt is not: it holds 0x3FCC…, and the shape was reading 1070805848 for what is
1.598. This is the same trap as odet in an earlier round, one level up.
Game::CombatWeaponReport: the damage quartet is flat, and the last two are floats. The old
shape reached dams damp dami damt through an obj_flex sub-struct, i.e. it modelled a nested
dams frame. The binary does not write one — the recovered class lists the four as its own
scalars, grade verified — and no save in the corpus contains one either.
That second one also had a measurement consequence worth recording under rule 12.
CoverageArchive::obj charges one typed item for the frame it is about to descend into; under
obj_flex with framed == false there is no frame, and it charged anyway. So the coverage
denominator was inflated by exactly one per weapon report. The four affected saves have
8, 13, 21 and 32 weapon reports, and their before/after item totals differ by exactly
8, 13, 21 and 32. The pre-fix coverage figures in the table above were therefore slightly
optimistic as well as too low; the after figures are on the corrected count.
7. What stays carried, and the workload that would settle it
One item from this round, deliberately:
FTPnts (Game::FieldTemplate) — the only item in this work that the recovery itself marks
unresolved. It names the element class from the decorated helper name
(Game::FieldTemplate::Point, whose own serializer is fully recovered: FTPShID FTPDesID FTPPosX FTPPosY FTPSqd) but could not type the item, and the count is 0 in every Lay in the corpus.
That is precisely the pair of conditions under which SysMem, mts and nalat were each typed
wrong and stayed wrong invisibly: the element framing is a property of the writer's helper,
not of the element class, and no record has ever shown one. So the elements are carried, and
Game::FieldTemplate::Point is deliberately not bound in the conformance test.
Workload: a save whose fleet has a stored tactical formation — set a fleet's combat layout in
the tactical setup screen, then save. One such save closes it, the same way turn15's single spy
craft closed spy and its single Dwgv design closed Dwg.
Everything else still carried is a container no save has ever filled — nodePaths, Ojvs,
usnc, the four SpyReport sub-lists, the deflay/rtgt pairs, and the polymorphic keys with
no factory entry — plus the RNG blob. None of them costs a single item on any save in the corpus.
8. The honest floor
The ratchet stands at 99.99 % and every save clears it, so there is no new floor to declare.
The tightest save is zuul-turn5-species5 at 99.9940 %, and its whole residual is the RNG blob's
two items against 33,316 typed. A bar of 99.99 is now equivalent to "nothing but the RNG blob",
which is the right thing for it to mean.
The number that will move next is not this one. It is the count of hypotheses in §7: each is worth exactly one manufactured save, and until then each is a labelled guess, not coverage.
9. Gates
Run as separate commands on fresh build directories (rules 13, 24):
tools/clean_room_check.sh— OK- host
ctest— 58/58,SOTS_SAVES_DIRpointed at all 20 saves - shim cross-build (
--preset shim) — clean, 129/129
Caveat on the third, stated rather than assumed (rule 13): CT111 was unreachable from this lane
(Permission denied (publickey,password), and ReVa was down with CONNECTION_CLOSED throughout),
so the cross-build was run locally against i686-w64-mingw32-g++ (GCC) 10-posix 20220113. That is
not necessarily CT111's toolchain, so the integrator should still run the CT111 shim build.
It did catch one thing worth having: Game::TacReport's TRmin field wanted a member named
min, which windows.h defines as a macro — it is mine in the shape for that reason.
Note the win32-threads variant of that compiler cannot build src/mars/vfs/zip_archive.cpp at
all (std::mutex does not exist); the posix variant is required.
Files
sots-engine wip/ws: src/mars/stream/shapes.h, tests/mars_stream/test_wire_schema.cpp,
tests/mars_stream/test_stream.cpp, docs/mars-stream.md. No generated header changed — every
fact used here was already in objects/streams.json and include/generated/sots_stream_schema.h;
what was missing was never the schema, it was a save that put something in the containers.