Lane F R16 (v3.1 THE FLIP) — close: default-boot tour + week soak + docs

The closing half of F's bookend. All gates green on the flipped tree (qa.sh --strict --soak: classic
regression + default-boot gate + smokes + no-giants incl. the seated drummer + the full-week soak).

- tour_shots.py -> docs/shots/release_v3_1/: the money shots on the DEFAULT boot (no per-shot flag juggling
  now the flip covers it) + a ?classic=1 before/after ("the town before v3"). 7 shots, settled, 0 page errors.
- week_soak.py: re-pointed to the default boot; the leak check now treats an at/below-baseline OR non-climbing
  trail as leak-free (a decreasing tex trail is GC/atlas-settle, not a leak — it was false-failing at warn-level).
- docs: README v3.1 flags table (defaults on + ?classic=1), CITY_SPEC v3.1 amendment (the rewritten prime
  law + classic covenant, A's deferred edit), LANE_F_NOTES §16, F-progress R16.

Numbers (seed 20261990): default @ venue block 124 draws / 57k tris (<=300/200k); ?classic 161 draws / 24k
tris (the v2 baseline); seated drummer 1.32 m stature; week soak 42 cycles leak-free (net +0/-6).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-16 13:32:22 +10:00
parent a4946b7ca2
commit 9277238462
14 changed files with 147 additions and 53 deletions

View File

@ -4,6 +4,27 @@
---
## Round 16 (v3.1 THE FLIP) — F bookends; the town shows its best by default
The gig layer soaked through three v3 stages, so v3.1 flips it (and the mature v2 flags) ON by default.
F owns the flip; AE landed their polish against it (seated drummer, town-wide pools, backline, corner-poster).
**Opening commit `a4946b7` (the round's foundation):**
- **The flip**`index.html` `CLASSIC` + `flagOn()`: gigs/weather/tram default-on, `loadPedFleet({sit:!CLASSIC})`,
`window.PROCITY.flags`; `buildings.js` winmap default-on + classic-aware. **`?classic=1`** forces the exact v2 boot.
- **The covenant moved**`flags_off_regression → classic_regression` (byte-identical `0x3fa36874`); a new
`default_boot_gate` (all four flags on, gig layer present, gig golden `0xb1d48ea1`, budget); every gig smoke
re-pointed to the DEFAULT boot; qa.sh gate renamed.
- **Seated-stature gate** (ledger #3) — `interior_scale_check.py` + `flags_check.py` accept `procitySeated` figs
at [0.9,1.5] m; the drummer passes at **1.32 m**.
Verified: `?classic` byte-identical (0x3fa36874, gig absent); default = full town (3 venues / 3 genres, gig golden,
112 draws @ venue block); classic/default/district/continuity/cover all green. **Close: re-baseline, re-shot tour
`release_v3_1/` + a `?classic=1` before/after, docs (CITY_SPEC v3.1 flip+classic law, README flags table,
LANE_F_NOTES §16), tag `v3.1`.**
---
## Round 15 (v3.0 RELEASE CLOSE, `?gigs=1`) — F ran last; the release is closed
R14 landed AE's hardening but **F never ran**, so v3.0 stayed untagged. R15 = the close. All deps were in

View File

@ -48,30 +48,35 @@ Seeded everything, `Math.random` banned in gen code · the game must run with ze
instancing from day one · canvas text, no font files · characters = shared base rigs + one
clip bank, canonical sources live outside this repo · 🟢 web-ok licensing only.
## v3.0 — venues & gigs, THE DISTRICT (`?gigs=1`)
## v3.1 — THE FLIP: the town shows its best by default
The Friday-night layer, shipped behind a flag so the default boot stays byte-identical to v2 (the
synthetic golden `0x3fa36874` never moves). Turn it on and each town seeds **24 venues** — a pub, a
band room, an RSL — with a **full seeded week of gigs** (`plan.gigs`), town-wide **posters**, and a
Friday night that reads: a **4-piece band** (front-line trio + drummer) with real instruments on the
stage, a mixed standing/dancing **crowd**, an outdoor **queue** at the door, a **cover charge** per
venue (paid & free nights), muffled **gig spill** through the wall + **walla and applause** inside,
**streetlamp pools** and lit frontages for the night walk, and **identity continuity** — the ped you
followed in off the street turns up in the crowd. Deterministic from the seed; asset-free
(`?noassets=1`) it all still happens with placeholders and zero fetches.
The Friday-night layer soaked through alpha, beta, and the v3.0 release — so **v3.1 flips it on by
default**, and the mature v2 flags flip with it. Boot the plain URL and each town seeds **24 venues**
(pub, band room, RSL) with a **full seeded week of gigs**, town-wide **posters**, and a Friday night that
reads: a **4-piece band** (trio + a **seated drummer**) with real instruments, a mixed **crowd**, an
outdoor **queue**, a per-venue **cover charge**, muffled **gig spill** + **walla and applause**,
town-wide **streetlamp pools**, and **identity continuity** — the ped you followed in is in the crowd.
Plus **weather**, the **tram**, and the parallax interior-**window shader**, all live by default.
**Flags** (all default-off unless noted; compose freely):
**The covenant moved, it didn't disappear.** `?classic=1` boots the **exact v2 town** — byte-identical
forever (synthetic golden `0x3fa36874`, zero gig layer, zero fetch delta). That is the new frozen
regression target; the default boot is deterministic to the gig golden `0xb1d48ea1`. Asset-free
(`?noassets=1`) composes with the default boot and stays silent-and-fine.
| flag | what |
|---|---|
| `?seed=N` | town seed (default `20261990`) |
| `?gigs=1` | the v3.0 gig layer — venues, week schedule, band/crowd/queue, cover, posters, night pass |
| `?weather=rain` · `?tram=1` · `?winmap=1` | wet strip · tram loop · parallax interior-window shader |
| `?roster=stream` | chunk-streamed citizen density (**on by default**) · `?pop=N` tunes it |
| `?dig=1` · `?stock=real` | crate-riffle on record bins · real GODVERSE sleeves in the stock |
| `?noassets=1` | asset-free — zero network, 100% placeholders, still legible |
| `?plansrc=osm` · `?mute=1` · `?dbg=1` | OSM plan source · silence · the `window.DBG` capture/soak harness |
**Flags** — the four headliners are **ON by default**, each with a `=0` opt-out; `?classic=1` is the
master off:
**Release tour:** [`docs/shots/release_v3/contact.html`](docs/shots/release_v3/) — the six money shots
(`queue_night` is the hero) with settled measurements. **QA:** `tools/qa.sh --strict` (6 gates) +
`tools/qa/week_soak.py` (the full-week soak) + the continuity/gig smokes in `tools/flags_check.py`.
| flag | default | what |
|---|---|---|
| `?classic=1` | off | **the exact v2 boot** — all four below off, byte-identical to the frozen baseline |
| `?gigs=0` | **on** | the gig district — venues, week schedule, band/crowd/queue, cover, posters, night pass |
| `?weather=0` · `?tram=0` · `?winmap=0` | **on** | seeded weather · the tram loop · the parallax interior-window shader |
| `?roster=v1` · `?pop=N` | stream | chunk-streamed citizen density is the baseline; `?roster=v1` restores the fixed roster |
| `?dig=0` · `?stock=real` | dig on | crate-riffle on record bins · real GODVERSE sleeves in the stock |
| `?noassets=1` · `?plansrc=osm` · `?mute=1` · `?dbg=1` | off | asset-free (zero network) · OSM plan source · silence · the `window.DBG` capture/soak harness |
| `?seed=N` | `20261990` | town seed |
**Release tour:** [`docs/shots/release_v3_1/contact.html`](docs/shots/release_v3_1/) — the money shots on
the default boot, plus a `?classic=1` before/after. **QA:** `tools/qa.sh --strict` (classic regression +
default-boot gate + smokes + no-giants) `--soak` (the full-week soak); the seated drummer passes the
no-giants gate at the seated stature band `[0.9,1.5] m`.

View File

@ -226,11 +226,26 @@ night in v1; interior-mapping shader is a stretch goal).
> golden re-pinned `0x4f4a549d → 0xb1d48ea1`; synthetic `0x3fa36874` still frozen. The v3.1 default flip
> + classic law is F's amendment to this section — see the flags table.)*
> **Prime flag law.** The gig layer is a **post-hoc augmentation** applied by the selector only when
> `?gigs=1`. With the flag OFF, `generatePlanFor(seed, src)` returns the byte-identical v2 base plan —
> **all v2 goldens stay frozen**. So this section adds fields that exist *only on gig-on plans*; it does
> not touch the v2 contract above. Deterministic from `(citySeed, customBands)`; works on synthetic AND
> osm towns. Node-pure (no THREE, no runtime fetch — the shell loads the band drop-in and passes it in).
> **Prime flag law.** The gig layer is a **post-hoc augmentation** applied by the selector when the gig
> flag is on. With it off, `generatePlanFor(seed, src)` returns the byte-identical v2 base plan — **all v2
> goldens stay frozen**. So this section adds fields that exist *only on gig-on plans*; it does not touch
> the v2 contract above. Deterministic from `(citySeed, customBands)`; works on synthetic AND osm towns.
> Node-pure (no THREE, no runtime fetch — the shell loads the band drop-in and passes it in).
>
> **Prime flag law — REWRITTEN at `v3.1` (ROUND16, THE FLIP; Lane F amendment).** The covenant MOVED, it
> did not disappear. `gigs`, `weather`, `winmap`, `tram` now default **ON**; the default boot is the full
> experience, deterministic to the **gig golden `0xb1d48ea1`**. Each flag keeps a `=0` opt-out, and
> **`?classic=1`** forces all four off at once:
> - **Classic law (the new frozen regression target):** the `?classic=1` boot is byte-identical to the v2
> baseline **forever** — synthetic fingerprint `0x3fa36874`, zero gig layer, **zero fetch delta** (no
> `custom_bands.json`, no `sit.glb`). The old "flags-off regression" gate is now the **classic-regression** gate.
> - **Default law:** the no-flag boot pins to the gig golden, and every gig / district / continuity / cover
> smoke **and the budget law** (interior ≤350 draws · street ≤300 / 200k tris, at the busiest venue block)
> now run against the **default boot with all four flags live**. `?noassets=1` composes with it and stays
> silent-and-fine.
> - The four flags read through one rule in the shell — `flagOn(name) = !classic && param !== '0'` (winmap
> reads the same rule in `buildings.js`); the shell publishes the intent on `window.PROCITY.flags`, and the
> seated drummer's `sit.glb` rides `!classic` so classic keeps its zero-fetch-delta.
>
> *(v3.0-alpha was one pub, one genre, one night's posters. **ROUND13 → v3.0-beta, the district:** 24
> venues of three kinds, a full week's schedule across town, town-wide posters. Same flag, same laws.)*

View File

@ -594,3 +594,47 @@ pathspec:
- **Drive live-spawned things explicitly**: automation throttles rAF, so the queue's seeded auto-drain may
not fire on schedule — `smoke_continuity`/`week_soak`/`tour_shots` all call `admitOne()` / step the loop
explicitly rather than trust the timer.
---
## 16. Round 16 — v3.1 THE FLIP (`?classic=1` is the new covenant). F bookends; opening below, close after.
The town shows its best by default. After three v3 stages the gig layer flips ON, and the mature v2 flags
(`weather`, `winmap`, `tram`) flip with it. F owns the flip; AE landed their v3.1 polish against it (D's
seated drummer, B's town-wide pools + default re-baseline, C's backline unification, A's corner-poster +
no-spine fixture, E's `sit.glb`).
### The flip (ledger #1) — the covenant MOVED from "flags off" to `?classic=1`
- **Shell** (`index.html`): `const CLASSIC = has('classic') && get('classic')!=='0'`, `const flagOn = name
=> !CLASSIC && get(name)!=='0'`. `gigs`/`weather`/`tram` construct via `flagOn`; `loadPedFleet({sit:!CLASSIC})`
gives the drummer his clip while keeping classic zero-fetch; `window.PROCITY.flags` publishes the intent.
- **`buildings.js`**: `winmap` reads the SAME rule (default-on, classic-off) — the one cross-lane flip line,
because winmap's flag lives in B's file. Marked `[Lane F R16 THE FLIP]`.
- **Two goldens, two gates:** classic → `0x3fa36874` (byte-identical v2 forever, the frozen regression
target); default → the gig golden `0xb1d48ea1`. `flags_off_regression` became **`classic_regression`**
(boot `?classic=1`); a new **`default_boot_gate`** proves the no-flag boot is the full experience (all four
flags on, gig layer present, gig golden green, budget). Every gig smoke re-points to the DEFAULT boot.
### Seated-stature gate (ledger #3) — published in the brief, no mid-round handshake
A seated figure fails the standing no-giants floor by design. Contract (in the round brief so D built to it):
D tags seated figs `fig.userData.procitySeated = true`; F's `interior_scale_check.py` + `flags_check.py`
accept them at **stature ∈ [0.9, 1.5] m** (still < ceiling, feet planted); standing stays [1.4, 2.0]. Landed
in the opening commit. Verified live: the seated drummer passes at **1.32 m stature**.
### Numbers (measured, seed 20261990 → "Boolarra Heads")
| thing | number |
|---|---|
| `?classic=1` | byte-identical v2: fingerprint 0x3fa36874, gig layer absent, streamed roster on, 160 draws / 21k tris |
| default boot | all four flags on · gig golden 0xb1d48ea1 · 14 gigs, 15 posters · **112 draws @ the venue block** (≤300) |
| district (default) | 3 venues, **3 playing** (corner-poster re-pin shifted the schedule) · 3 genres live |
| seated drummer | **1.32 m** stature — passes the seated band [0.9,1.5]; the standing crew 1.681.81 m |
### The gotchas closed this round
- **A flag whose parse lives in another lane's file** (winmap in `buildings.js`) still flips as part of F's
structural change — one marked line, same `!classic && !=='0'` rule as the shell.
- **selfcheck prints the base fingerprint in its summary but asserts the GIG golden silently**, so the
default-boot gate gates on the ALL-GREEN run (which includes the gig-golden assert), not a stdout substring.
- **`P.winmap` isn't exposed** — the default gate verifies winmap *intent* via `window.PROCITY.flags`; the
actual shader is B's own `winmap` smoke.
- **`?classic` must zero the fetch delta**, so `sit.glb` is gated on `!CLASSIC` (not just on the fleet) — a
raw sit fetch under classic would break the covenant even though the drummer never spawns there.

View File

@ -0,0 +1 @@
<!doctype html><meta charset="utf-8"><title>PROCITY v3.1 — release tour</title><style>body{background:#14110c;color:#f4efe6;font:14px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;margin:24px}h1{font-weight:700}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(420px,1fr));gap:18px}figure{margin:0;background:#1d1810;border-radius:10px;overflow:hidden}img{width:100%;display:block}figcaption{padding:8px 12px;color:#cdc4b2}b{color:#ffd9a0}</style><h1>PROCITY v3.1 — release money-shot tour (THE FLIP) <small>· seed 20261990 · settled measurements</small></h1><div class="grid"><figure><img src="venue_night.png" alt="venue_night" loading="lazy"><figcaption><b>venue_night</b><br>124 draws · 57,669 tris · queue 2 · 15 posters</figcaption></figure><figure><img src="district_posters.png" alt="district_posters" loading="lazy"><figcaption><b>district_posters</b><br>189 draws · 139,838 tris · queue 2 · 15 posters</figcaption></figure><figure><img src="queue_night.png" alt="queue_night" loading="lazy"><figcaption><b>queue_night</b><br>122 draws · 62,763 tris · queue 2 · 15 posters</figcaption></figure><figure><img src="tram_stop.png" alt="tram_stop" loading="lazy"><figcaption><b>tram_stop</b><br>111 draws · 178,122 tris · queue 0</figcaption></figure><figure><img src="gig_interior.png" alt="gig_interior" loading="lazy"><figcaption><b>gig_interior</b><br>The Exchange Hotel · Screaming Utes · 4-piece · crowd 8 (2 from roster) · 54 draws · 35,216→182,633 tris (pre-load→settled)</figcaption></figure><figure><img src="rain_verandah.png" alt="rain_verandah" loading="lazy"><figcaption><b>rain_verandah</b><br>137 draws · 77,338 tris · queue 2</figcaption></figure><figure><img src="district_posters_classic.png" alt="district_posters_classic" loading="lazy"><figcaption><b>district_posters_classic</b><br>?classic=1 — the town before v3 (no gig layer, no pools)</figcaption></figure></div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

View File

@ -24,14 +24,18 @@ ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
PORT = 8130
HOST = f'http://127.0.0.1:{PORT}'
SEED = 20261990
OUTDIR = ROOT / 'docs' / 'shots' / 'release_v3'
OUTDIR = ROOT / 'docs' / 'shots' / 'release_v3_1'
if '--seed' in sys.argv:
SEED = int(sys.argv[sys.argv.index('--seed') + 1])
# shot groups — each is one clean page load with the flags that shot needs (deterministic, same seed).
# [R16 THE FLIP] shot groups — the DEFAULT boot now has all four flags on, so most shots come from ONE clean
# load (no per-shot flag juggling). Rain is still forced (the default seed rolls clear); ?classic=1 gives the
# before/after comparison. Deterministic, same seed.
GROUPS = [
{'flags': 'gigs=1', 'street': ['venue_night', 'district_posters', 'queue_night'], 'interior': True},
{'flags': 'weather=rain&tram=1&winmap=1', 'street': ['rain_verandah', 'tram_stop'], 'interior': False},
{'flags': '', 'street': ['venue_night', 'district_posters', 'queue_night', 'tram_stop'], 'interior': True},
{'flags': 'weather=rain', 'street': ['rain_verandah'], 'interior': False},
# the town before v3 — the exact v2 boot; same main-street pose as district_posters for a clean A/B
{'flags': 'classic=1', 'street': ['district_posters'], 'interior': False, 'suffix': '_classic'},
]
# settle + re-pose a street bookmark once its live/async content (queue, instrument GLBs, poster textures,
@ -112,12 +116,12 @@ def contact_html(shots):
f'<figure><img src="{s["file"]}" alt="{s["name"]}" loading="lazy">'
f'<figcaption><b>{s["name"]}</b><br>{cap}</figcaption></figure>')
return (
'<!doctype html><meta charset="utf-8"><title>PROCITY v3.0 — release tour</title>'
'<!doctype html><meta charset="utf-8"><title>PROCITY v3.1 — release tour</title>'
'<style>body{background:#14110c;color:#f4efe6;font:14px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;margin:24px}'
'h1{font-weight:700}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(420px,1fr));gap:18px}'
'figure{margin:0;background:#1d1810;border-radius:10px;overflow:hidden}img{width:100%;display:block}'
'figcaption{padding:8px 12px;color:#cdc4b2}b{color:#ffd9a0}</style>'
f'<h1>PROCITY v3.0 — release money-shot tour <small>· seed {SEED} · settled measurements</small></h1>'
f'<h1>PROCITY v3.1 — release money-shot tour (THE FLIP) <small>· seed {SEED} · settled measurements</small></h1>'
f'<div class="grid">{"".join(cards)}</div>')
@ -127,7 +131,7 @@ def main():
except ImportError:
sys.exit('playwright not installed — tools/.venv/bin/python -m playwright install chromium')
print(f"\033[1mv3.0 RELEASE MONEY-SHOT TOUR\033[0m (seed {SEED} → docs/shots/release_v3/)")
print(f"\033[1mv3.1 RELEASE MONEY-SHOT TOUR\033[0m (seed {SEED} → docs/shots/release_v3_1/)")
OUTDIR.mkdir(parents=True, exist_ok=True)
srv = ensure_server()
shots, errs_total = [], 0
@ -139,15 +143,17 @@ def main():
errs = []
pg.on('pageerror', lambda e: errs.append(str(e)))
boot(pg, grp['flags'])
sfx = grp.get('suffix', '')
for name in grp['street']:
stat = pg.evaluate(STREET_JS, name)
out = OUTDIR / f'{name}.png'
pg.screenshot(path=str(out))
cap = f"{stat['draws']} draws · {stat['tris']:,} tris"
if stat.get('queue') is not None: cap += f" · queue {stat['queue']}"
if name in ('district_posters', 'venue_night', 'queue_night'): cap += f" · {stat['posters']} posters"
shots.append({'name': name, 'file': f'{name}.png', 'caption': cap})
print(f" \033[32m●\033[0m {name}: {cap}")
fname = f'{name}{sfx}.png'
pg.screenshot(path=str(OUTDIR / fname))
cap = (f"?classic=1 — the town before v3 (no gig layer, no pools)" if sfx
else f"{stat['draws']} draws · {stat['tris']:,} tris")
if not sfx and stat.get('queue') is not None: cap += f" · queue {stat['queue']}"
if not sfx and name in ('district_posters', 'venue_night', 'queue_night'): cap += f" · {stat['posters']} posters"
shots.append({'name': name + sfx, 'file': fname, 'caption': cap})
print(f" \033[32m●\033[0m {name}{sfx}: {cap}")
if grp['interior']:
info = pg.evaluate(INTERIOR_JS)
if info.get('ok'):
@ -168,7 +174,7 @@ def main():
if srv: srv.terminate()
(OUTDIR / 'contact.html').write_text(contact_html(shots))
print(f"\n\033[1m{len(shots)} shots\033[0m → docs/shots/release_v3/ (+ contact.html) · {errs_total} page errors")
print(f"\n\033[1m{len(shots)} shots\033[0m → docs/shots/release_v3_1/ (+ contact.html) · {errs_total} page errors")
sys.exit(1 if errs_total or len(shots) < 6 else 0)

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""PROCITY Lane F — the full-week gig soak (round 15, v3.0 release · ?gigs=1). NON-STRICT qa extra.
"""PROCITY Lane F — the full-week gig soak (round 15→16, on the DEFAULT boot (v3.1 flip)). NON-STRICT qa extra.
Rolls the clock through all 7 nights in ONE browser context, entering every playing venue each night,
and proves the shell's per-night gig machinery holds over the week with no leaks:
@ -135,7 +135,7 @@ def main():
except ImportError:
sys.exit('playwright not installed — tools/.venv/bin/python -m playwright install chromium')
print(f"\033[1mFULL-WEEK GIG SOAK\033[0m (seed {SEED} · 7 nights × all venues · leak/cover/roster · ?gigs=1)")
print(f"\033[1mFULL-WEEK GIG SOAK\033[0m (seed {SEED} · 7 nights × all venues · leak/cover/roster · default boot)")
srv = ensure_server()
try:
with sync_playwright() as p:
@ -144,7 +144,7 @@ def main():
errs = []
pg.on('console', lambda m: errs.append(m.text) if m.type == 'error' else None)
pg.on('pageerror', lambda e: errs.append(str(e)))
pg.goto(f'{HOST}/index.html?seed={SEED}&gigs=1&dbg=1')
pg.goto(f'{HOST}/index.html?seed={SEED}&dbg=1') # [R16] the DEFAULT boot now IS the gig boot
pg.wait_for_function('window.DBG && window.DBG.ready === true', timeout=25000)
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
try:
@ -190,15 +190,17 @@ def main():
lk = res['leakAfter']
trail = res['memTrail']
tail = trail[-3:] if len(trail) >= 3 else trail
flat = all(m['geo'] == tail[0]['geo'] and m['tex'] == tail[0]['tex'] for m in tail)
# a real leak CLIMBS every cycle; a flat OR DECREASING back half (GC / atlas settle) is not a leak, and
# ending AT OR BELOW the warmed baseline can't be one either. Only monotone growth into the tail fails.
climbing = (tail[-1]['geo'] > tail[0]['geo']) or (tail[-1]['tex'] > tail[0]['tex'])
traj = "".join(f"{m['geo']}/{m['tex']}" for m in trail)
if lk['geo'] == 0 and lk['tex'] == 0:
OK(f"leak-free: geo/tex back to baseline across {res['cycles']} cycles (night trail {traj})")
elif flat:
OK(f"leak-free: warm-up residue geo {lk['geo']:+d}/tex {lk['tex']:+d}, then FLAT across the back half "
f"— no per-cycle growth (night trail {traj}, {res['cycles']} cycles)")
if lk['geo'] <= 0 and lk['tex'] <= 0:
OK(f"leak-free: geo/tex ended at/below the warmed baseline (net {lk['geo']:+d}/{lk['tex']:+d}) across {res['cycles']} cycles (trail {traj})")
elif not climbing:
OK(f"leak-free: warm-up residue geo {lk['geo']:+d}/tex {lk['tex']:+d}, then flat/settling across the back half "
f"— no per-cycle growth (trail {traj}, {res['cycles']} cycles)")
else:
FAIL(f"LEAK: geo {lk['geo']:+d}/tex {lk['tex']:+d} still climbing (night trail {traj}) across {res['cycles']} cycles")
FAIL(f"LEAK: geo {lk['geo']:+d}/tex {lk['tex']:+d} still climbing (trail {traj}) across {res['cycles']} cycles")
if res['cycles'] < 20:
FAIL(f"cycles: only {res['cycles']} enter/exit (want ≥20)")