#!/usr/bin/env python3 """Per-turn RNG ledger from a lane-Z trace (docs: findings/control-flow/tail-rng-ledger.md). The lane-Z hooks declare the strategic Mars::RNG as a region whose `describe` reports an ABSOLUTE WORD POSITION (see sots-engine/src/shim/hooks/rng_ledger.h). This tool turns those positions into the three numbers a reimplementation needs: * how many 32-bit words the generator consumed inside each hooked call; * the same, attributed by phase, using the fact that the hooks nest; * the bracket total between the pre-turn autosave and the post-turn autosave -- the interval a standalone has to reproduce -- and the RESIDUAL left over after the attributed subtotals. A residual > 0 is a draw site outside every hooked function. A `words: null` is a position the ledger could not place and MUST NOT be read as zero. uv run python3 tools/rng_ledger_report.py """ import json, sys def val(tv): if tv is None: return None return tv.get('v') def sv(struct_tv, key): if struct_tv is None or struct_tv.get('t') != 'struct': return None return val(struct_tv['v'].get(key)) rows = [] for i, line in enumerate(open(sys.argv[1])): d = json.loads(line) if 'meta' in d and 'hook' not in d: continue if 'hook' not in d: continue args = {a.get('n'): val(a) for a in d.get('args', []) if a.get('n')} side = d.get('side', {}) rng = side.get('rng', {}) b, a2 = rng.get('before'), rng.get('after') wb, wa = sv(b, 'words'), sv(a2, 'words') rows.append(dict(cid=d['call_id'], depth=d.get('depth'), hook=d['hook'].split('::')[-1], turn=args.get('turn'), pc=args.get('phase_counter'), enc=args.get('encounters'), end_turn=args.get('end_turn'), paths=args.get('node_paths'), perm=args.get('np_permanent'), imm=args.get('np_immortal'), mortal=args.get('np_mortal'), minlife=args.get('np_min_life'), within5=args.get('np_within5'), res_nb=args.get('res_no_battle'), predict=args.get('predict_words', args.get('predict_nodeline_words')), wb=wb, wa=wa, lb=sv(b,'left'), la=sv(a2,'left'), bb=sv(b,'block'), ba=sv(a2,'block'), words=(wa - wb) if (wa is not None and wb is not None) else None, err=d.get('err'), undecl=d.get('undeclared_total'))) hdr = f"{'cid':>4} {'d':>1} {'hook':<28} {'turn':>4} {'S+8':>4} {'enc':>4} {'paths':>5} {'pred':>4} {'w_in':>8} {'w_out':>8} {'WORDS':>6} {'left':>10}" print(hdr); print('-'*len(hdr)) for r in rows: print(f"{r['cid']:>4} {r['depth']:>1} {r['hook']:<28} {str(r['turn']):>4} {str(r['pc']):>4} " f"{str(r['enc']):>4} {str(r['paths']):>5} {str(r['predict']):>4} {str(r['wb']):>8} " f"{str(r['wa']):>8} {str(r['words']):>6} {str(r['lb'])+'->'+str(r['la']):>10}" + (f" ERR={r['err']}" if r['err'] else '')) # Brackets: Autosave(end_turn=1) .. Autosave(end_turn=0) nl = [r for r in rows if r['hook'] == 'NodeLineDecay'] if nl: print() print("node-line population (phase 11's draw is one word per EXPIRED line):") print(f"{'turn':>5} {'paths':>6} {'permanent':>10} {'immortal':>9} {'mortal':>7} {'min_life':>9} {'<=5':>4} {'expired':>8} {'words':>6}") for r in nl: print(f"{str(r['turn']):>5} {str(r['paths']):>6} {str(r['perm']):>10} {str(r['imm']):>9} " f"{str(r['mortal']):>7} {str(r['minlife']):>9} {str(r['within5']):>4} " f"{str(r['predict']):>8} {str(r['words']):>6}") enc = [r for r in rows if r['hook'] == 'OnAllCombatDone_Tail'] if enc: print() print("tail invocations (P1: does it run on every End Turn?):") for r in enc: print(f" turn {r['turn']}: encounters={r['enc']} words={r['words']}") print() marks = [r for r in rows if r['hook'] == 'Autosave'] for k in range(len(marks) - 1): lo, hi = marks[k], marks[k+1] if not (lo['end_turn'] is True and hi['end_turn'] is False): continue if lo['wb'] is None or hi['wb'] is None: print(f"bracket {k}: INCOMPLETE (pre-turn marker has no ledger position)"); continue total = hi['wb'] - lo['wb'] inner = [r for r in rows if lo['cid'] < r['cid'] < hi['cid'] and r['depth'] == 0 and r['words'] is not None] acc = sum(r['words'] for r in inner) # The autosave hook's `this` is a StrategyHost, so its record carries no turn; take the turn # from the drivers the bracket encloses. turn = next((r['turn'] for r in inner if r['turn'] is not None), None) print(f"BRACKET turn {turn}: total={total} words attributed={acc} residual={total-acc}") for r in inner: print(f" {r['hook']:<30} {r['words']:>6}")