diff --git a/F-progress.md b/F-progress.md index 729c6f4..e720d85 100644 --- a/F-progress.md +++ b/F-progress.md @@ -947,3 +947,81 @@ F drives/inspects via `window.PROCITY` + JS for programmatic checks. msg `Lane F: integration — interiors + keepers + street peds + QA harness + barrel + skins fix`. - A local `python3 -m http.server 8130` is running for the smoke test (harmless; kill when done). - Lane prompts hard-code `/Users/jing/Documents/PROCITY`; real path is `/Users/m3ultra/Documents/procity`. + +--- + +## Round 38 (v8 WAVE 1 — INHABITATION) — the budget reconciliation · four new gates, four controls · the round close + +**THE BUDGET OF RECORD DOES NOT MOVE: 292 / 300 draws, margin still EIGHT.** The brief's naive sum +(292 + washing 1 + magpie 1 = 294) is wrong in our favour. **The pinned worst-draw frame is a NIGHT +frame and both draw-spending layers are structurally zero at night** — the washing comes in +(`count = 0`), the magpie roosts (`count = 0`). Wave 1's one draw is spent in daylight, where the +walked peak sits ten draws lower. Two pinned trees (`6bc7926` control, byte-identical under `web/` to +R37's `7bd96e5`; `4514cb9` treatment, byte-identical under `web/js` to the final `74d6257`), assets +symlinked to the live tree on both sides, 802 stations @ 2 m at **yaw 0**, sim + tram **LIVE**: +**night 292 → 292 (+0)** / 120,145 → 122,879 tris; **noon 282 → 283 (+1)** / 99,872 → 103,118; +`?classic=1` **269 / 90,580 → unchanged**; `fitzroy_real` **172 / 130,727 → unchanged**; +`bendigo_real` **204 / 131,492 → unchanged even with `?magpie=1` forced**. The control reproduced +R37's pin **to the byte, twice**. Every revert is exact: `?yards=0` at night gives the control's three +numbers; `?yards=0` at noon gives 282 / 99,872, isolating the noon +1 to the washing alone. + +**Lane B's "control 346" — answered rather than dismissed.** As a *delta* it is the wall-clock ped +spawn artifact R37 named (114 draws of swing between two browser instances measures the sim, not the +build), and B was right to hide the sim. But 346 > 300 is a claim about the world, and neither +`stepwalk` (walks the chunks in, photographs a half-dressed street) nor `dwell` (fills the street, +arrives clean at 27 chunks) can adjudicate it. **`tools/qa/budget_walk.py` gains `--method +stepsettle`**: the audit's fixed-yaw stations, each **drained then held N real rAF frames** — R+1 +dispose residue AND a fully-acquired crowd in the same frame. **Like for like — the same 101 stations at 8 m, yaw 0 — the settle is worth +3 draws at noon (277→280 control, 278→281 R38) and −9 at night (287→278 both trees), and the sign flip is the finding: at night the settled street RETIRES near rigs to impostors (tris 120,145→100,939), so the crowd goes home and a sampler that never waits does not see it. **The highest draw count anywhere in the reconciliation — 4 methods × 2 trees × 2 segments × 5 towns — is 292, the pin. Nothing read over 300.** The mechanism that could legitimately have breached it (full crowd inside a full R+1 dispose window) now reads 280/281. B's 346 was the artifact, and the refusal is now backed by the measurement that could have falsified it. + +**A correction to my own R37 pin:** the corpus TRI worst is **130,727** on `fitzroy_real`, not +124,793 — and it reads 130,727 on the **control** tree, so it is my R37 sampling density (12-bearing +8 m sweep) versus this round's 2 m fixed-yaw stations, not a wave-1 cost. Re-pinned. *A pin is only +as good as its station spacing, and mine had not said so.* + +**FOUR NEW GATES, FOUR CONTROLS DEMONSTRATED** (`smoke_ambient` · `smoke_character` · `smoke_magpie` +· `smoke_yards`, all wired into `flags_check.main()`): +- **§1.1 audio** — six sources anchored (63/8/81/32/39/57, B's numbers exactly); dog **audible at + 5.9 m with 1 play scheduled** (for a one-shot `audible` is only published once `plays > 0`, so one + word covers fetch→decode→gain→schedule→counter); **hours enforced** (sprinkler audible 06:30 → idle + 12:30, same pose); **day-of-week enforced** (mower idle Mon → audible Sat, same pose); **pan is a + bearing** (yaw 0/90/180/270 → +0.00 +0.85 −0.00 −0.85); **zero draws byte-identical to + `?ambient=0`**. **THE CONTROL:** a second server, root symlinked file-for-file except one patched + `manifest.json` with `sfx['dog-bark-fence']` renamed → that source reports **`no-entry`, 0 plays, + never audible**, other five unchanged, **0 console errors**. Stated on every run: audible OUTPUT is + unverifiable headlessly — **nobody in this loop has ears**. +- **§1.2 character** — classic is the frozen v1 literals with all tints null and 5 ground meshes; + **all 23 shipped keys matched per-town including the two carrying `state: ''`**; **the control + fires** (an unknown key returns `matched:false`); **21 distinct palettes across 23 towns** (a single + shared palette FAILS by name); and the two orphans are **on the GPU** — `ground-gravel.jpg` bound on + Castlemaine, `ground-reddust.jpg` on Darwin, read off the live material. +- **§1.3 magpie** — the latch arm verbatim with its **positive control first** (TRUE before the roll); + day-roll clears it; **localStorage gained NO new key** (baseline taken *after* a `game.save()` — + the first cut blamed the magpie for v7's own write); **the season is 23 distinct positions across + 24 towns, run UNFORCED**, against B's first cut's 24-of-24-at-214, with `newtown_real` in season on + day 1 and 129/1000 seeds hitting the 11.5% design ratio; **territory 6 → 157** synthetic → bendigo; + **+1 draw / +182 tris** at one pose reused across both boots. +- **§1.4 yards** — 145 house + 36 yard = 181 lots, 181 hoists → 394 items, 256 active, **0 at night**; + the cost decomposition at one pose with the sim quiet: **box props +0 draws** (and **−2**, because + the 36 `yard` lots used to fall through to `buildShopfront()`) with **+1,936 tris**, **washing + exactly +1 draw / +512 tris**; and on real towns it **SKIPS LOUDLY** — a `⊘ SCOPE` line printing + 0 house/yard lots / 0 hoists / mesh never created, next to the synthetic 181 that makes the zero + attributable. + +**Also caught, in my own gates:** the pan arm's first draft used two bearings and read `+0.00/−0.00` +because that anchor sat on the forward axis — it would have called a working panner inconclusive. +Four bearings. *The gate's first draft failed the standard the gate exists to enforce.* + +**SUITE: `tools/qa.sh --strict --matrix` GREEN on the true final tree** (`74d6257` — E's session-2 +publish landed mid-round, `web/` diff vs B's `4514cb9` is manifest + one thumb, **no code**, and both +were already in the tree both budget servers read): **7 passed · 0 failed · 0 warn · 0 skipped**; +**flags_check GREEN 0/0** with all four new gates inside it; **MATRIX GREEN 10 towns × 7 gates**, +0 fetches under `?noassets` on all ten, tram verdicts unchanged. Wave 1 moved nothing in the matrix. + +**Held: nothing.** Recorded, not fixed (other people's files): `magpie.js` says **154 triangles** at +`:9`/`:123` and **182** at `:42` — measured 182, so two stale comments in B's file; E's three +published GLBs are manifest-and-depot but not under `web/assets/models/` (staged in +`pipeline/_normalized/`, nothing loads them, validator's contract covers it — **not a defect**). +Carried debt unchanged: A's D1–D4 · the matrix's budget column is bookmark numbers · `?r=3` breaches +at 307 · the `?classic=1` town-selector question. + +**No tag** — wave 1 is content; the epoch tags when it is whole. diff --git a/docs/LANES/LANE_F_NOTES.md b/docs/LANES/LANE_F_NOTES.md index 6af6f7a..5e9d1fd 100644 --- a/docs/LANES/LANE_F_NOTES.md +++ b/docs/LANES/LANE_F_NOTES.md @@ -2438,3 +2438,273 @@ measures it. Making the matrix walk-and-turn every town is a real improvement an **filed to the v8 debt ledger, not smuggled into a floor round.** **No tag.** Wave 0 levels the floor; it does not ship a version. + +--- + +## §38 — R38: v8 WAVE 1, INHABITATION. F: the budget reconciliation · the four gates · the round close + +**Verdict: WAVE 1 SHIPS. The budget of record does NOT move — still 292 / 300 draws, and the margin +is still EIGHT, not six.** The brief's naive sum (292 + washing 1 + magpie 1 = 294) is wrong, and it +is wrong in our favour. **The pinned worst-draw frame is a NIGHT frame, and both of wave 1's +draw-spending layers are structurally zero at night**: the washing comes in (`mesh.count = 0`) and +the magpie roosts (`mesh.count = 0`). Wave 1's one draw is spent in daylight, where the walked peak +sits **ten draws lower**. Measured on two pinned trees, one box, one afternoon — not reasoned. + +### §38.1 THE BUDGET RECONCILIATION — **292 / 300 stands. +0 draws at the pinned worst frame.** + +**Method: R37 §37.1's, unchanged, so the two rounds are comparable.** Two **port-isolated no-store +servers** over `git archive`'d trees. `f38_ctrl` = **`6bc7926`** (pre-B, pre-E) — byte-identical under +`web/` to R37's pinned `7bd96e5` (`git diff --stat 7bd96e5 6bc7926 -- web/` is empty, so the control +IS the tree the pin was taken on). `f38_treat` = **`4514cb9`** (B on top of E) — byte-identical under +`web/js` + `web/index.html` to the round's true final `74d6257` (E's session-2 publish is assets only). +**`web/assets` is a SYMLINK to the live tree on BOTH sides**, so Lane E's asset wave and its mid-round +follow-up are the same bytes in both browsers and cannot be the difference. Default boot, seed +20261990, synthetic golden town, **1280×720**, `--method stepwalk --stepm 2 --fixed-yaw 0 --laps 2` = +**802 stations** down the main spine at **yaw 0, the worst bearing** (R37: 110 draws of spread from +the camera direction alone, peak at the *default argument* of `DBG.teleport`). **The citizen sim and +the tram are LIVE**, because the default boot is what the pin measures. + +**The control reproduces the R37 pin to the byte, twice, on a fresh checkout: 292 draws / 120,145 +tris, 31 live chunks, at x −4.1 z 319.2.** The method is repeatable; that is what makes the rest of +this table an argument rather than an anecdote. + +| synthetic, default boot, 802 stations @ 2 m, yaw 0, **sim LIVE** | control `6bc7926` | R38 `4514cb9` | delta | +|---|---|---|---| +| **NIGHT (seg 5) — worst draws** | **292** | **292** | **+0** | +| NIGHT — worst tris | 120,145 | 122,879 | +2,734 | +| **NOON (seg 2) — worst draws** | **282** | **283** | **+1** | +| NOON — worst tris | 99,872 | 103,118 | +3,246 | +| night p50 draws | 177 | 176 | −1 | +| `?classic=1`, night | **269 / 90,580** | **269 / 90,580** | **0 / 0** | +| `fitzroy_real`, night (corpus tri worst) | **172 / 130,727** | **172 / 130,727** | **0 / 0** | +| `bendigo_real`, noon | **204 / 131,492** | **204 / 131,492** | **0 / 0** | +| `bendigo_real`, noon, **`?magpie=1` forced** | — | **204 / 131,492** | **0 / 0** | + +**The reverts are EXACT, not approximate.** On the treatment tree: `?yards=0` at night → +**292 / 120,145**, all three of the control's numbers; all four flags off +(`yards=0&washing=0&magpie=0&ambient=0`) → **292 / 120,145**; and `?yards=0` **at noon** → +**282 / 99,872**, which isolates the noon **+1 to the washing and to nothing else**. `?magpie=1` +forced at noon still reads **283** — the bird is +1 *where he is*, and where he is is never the +worst frame. + +**Settled at the pinned worst point** (`dwell`, 700 frames at x −4.1 z 318.3 yaw 0, sim live, crowd +fully acquired): night control **270 / 60,476** vs R38 **270 / 61,276** — **+0 draws**; noon control +**265 / 55,734** vs R38 **266 / 57,046** — **+1**, the washing again, in daylight again. + +**Lane B's isolated costs reproduce independently in my gates** (`smoke_yards`, `smoke_magpie`, at a +fixed pose with the citizen sim quiet on both sides via `?roster=v1&pop=0`): washing **+1 draw / ++512 tris**, magpie **+1 draw / +182 tris**, every yard box prop **+0 draws**. B's numbers are B's +numbers and they are also mine. + +**One finding B did not claim and I will:** the yard build is **2 draws CHEAPER** than what it +replaced at a residential pose (`?washing=0` 191 draws vs `?yards=0` 193). The 36 `yard` lots that +fell through to `buildShopfront()` since v1 carried facade, signage and awning materials; they are now +`boxMats`-only fenced blocks. That is why B's 102-frame sum went **down** 13 draws while the worst +frame went up 1. + +### §38.2 LANE B's "control 346": **the artifact is real AND the mechanism is real. Both, separately.** + +B reported that with the sim live the same step read **control 346 vs treatment 232**, and correctly +refused to use it. As a *delta* it is the artifact R37 §37.1 named: `CitizenSim` acquires near-tier +rigs on a per-frame budget (`sim.js NEAR_MAX 24`) and peds spawn on **wall-clock** time, so how many +rigs are dressed at sample time is a function of how long that browser has been sitting there. A +114-draw swing between two browser instances measures the sim, not the build. B was right to hide it. + +**But 346 is also above 300, and "it is an artifact" is not an answer to that.** R37 named *two* +under-reporting mechanisms and each of my existing methods defeats exactly one: + +| method | R+1 dispose residue | crowd acquisition | reads at the pinned point | +|---|---|---|---| +| `stepwalk` (the pin) | **yes** — 31 live chunks | **no** — 1 frame/station | **292** | +| `dwell` | **no** — clean arrival, 27 chunks | **yes** — 700 frames | **270** | +| **`stepsettle` (new this round)** | **yes** | **yes** | *below* | + +`tools/qa/budget_walk.py` gains **`--method stepsettle`**: the audit's fixed-yaw stations, each one +**drained (`chunks.pending → 0`) and then held `--holdframes` real rAF frames** so the shell's own +`frame()` runs the sim to steady state before the sample is taken. It is Lane B's method — drain, +then read through the composer — with R37's bearing discipline on top. It is the only one of the four +that can adjudicate a >300 claim, and it exists because B's 346 deserved an answer rather than a +dismissal. + +**THE ANSWER: a >300 transient is NOT reproducible, on either tree, by any of the four methods.** +Like for like — the **same 101 stations at 8 m, yaw 0, one lap**, so the ONLY difference between the +two rows is the settle: + +| 101 stations @ 8 m, yaw 0, sim live | `stepwalk` (1 frame/station) | `stepsettle` (drained + 25 frames) | what the settle is worth | +|---|---|---|---| +| **NIGHT** control | **287** / 120,145 | **278** / 100,939 | **−9 draws** | +| **NIGHT** R38 | **287** / 122,879 | **278** / 104,178 | −9 | +| **NOON** control | **277** / 99,872 | **280** / 111,308 | **+3 draws** | +| **NOON** R38 | **278** / 103,118 | **281** / 115,169 | +3 | + +**Settling is worth +3 draws at noon and −9 at night, and the sign flip is the finding.** At noon +the crowd fills in — R37's mechanism, in the direction R37 predicted, and smaller than R37's +standing-still +9. **At night it goes the other way**: the single-frame teleport photographs rigs +still dressed from the previous station and the settled street *retires them to impostors* +(tris fall 120,145 → 100,939, and a near rig is ~2.4k tris). The night crowd goes home; a sampler +that never waits does not see it happen. + +**So the highest draw count anywhere in this reconciliation — four methods × two trees × two +segments × five towns — is 292, and it is the pin.** Nothing read over 300. The mechanism that +*could* legitimately have breached it (a fully-acquired crowd standing inside a full R+1 dispose +window) is now measured and it reads **280 / 281**: nineteen draws under the cap, eleven under the +pin. **Lane B's 346 was the artifact, and B was right to refuse it.** What I will add is that the +refusal is now backed by a measurement rather than by a citation — the method that could have +falsified it exists, was run, and did not. + +*(One caution the same table teaches: `stepsettle`'s 278 is below `stepwalk`'s 292 mostly because it +walked 101 stations and the pin walks 802. **Station spacing, not settle policy, is the single +biggest lever on a walked worst-frame number** — see §38.3, where it moved a corpus pin by 6k tris. +Any future budget claim must state its station spacing as well as its settle policy and its bearing.)* + +### §38.3 A CORRECTION TO MY OWN R37 PIN: the corpus TRI worst is **130,727**, not 124,793. + +R37 pinned `fitzroy_real` at **124,793 tris** off a 12-bearing × 8 m *sweep*. Walked at **2 m stations +at yaw 0**, the same tree reads **130,727** — and it reads 130,727 on the **control**, so this is a +sampling-density correction to my own number, not a wave-1 cost. A finer station grid finds a worse +frame; that is what finer grids are for. **Re-pin: corpus tri worst 130,727 / 200,000** (35% headroom). +The draw pin is untouched. *A pin is only as good as its station spacing, and mine had not said so.* + +### §38.4 THE FOUR GATES — each with its falsifiability control DEMONSTRATED, in the same run + +`tools/flags_check.py` gains **`smoke_ambient` · `smoke_character` · `smoke_magpie` · `smoke_yards`**, +wired into `main()` before `smoke_tier2`. The house rule they all obey, in one line: **a gate that +cannot fail is not a gate.** This round earned that sentence twice over — R37 killed two vacuous +gates, and R38's own §1.3 season mechanism produced a constant (24 of 24 towns at 214 days) that +*would have gated green* because `?magpie=1` forces the state. Lane B caught its own bug. Every arm +below either flips a subject between two boots that differ in exactly one thing, or asserts a +negative that a broken build would satisfy — never "the object exists". + +#### (a) `smoke_ambient` — §1.1, the audio inhabitation layer. **GREEN, 0 warns.** + +| arm | result | +|---|---| +| six sources published, table == `character.js`'s `AMBIENCE_SOURCES` | ✓ | +| all six resolve in the **shipped** manifest; none reads `no-entry` unpatched | ✓ (so the control below means something) | +| all six anchored on the default boot | **63 dog · 8 mower · 81 radio · 32 sprinkler · 39 roller-door · 57 magpie-carol** — B's numbers exactly | +| **positive control** — one source reaches `audible` | dog-bark-fence **audible at 5.9 m, gain 0.25, 1 play scheduled**. For a one-shot, `audible` is only published once `plays > 0`, so this single word covers fetch → decode → layer gain → scheduling → counter | +| **HOURS is enforced** | sprinkler ([6,10)) **audible at 06:30 → idle at 12:30, same pose** | +| **DAY-OF-WEEK is enforced** | mower (weekend-only) **idle on day 1 (Mon) → audible on day 6 (Sat), same pose, same segment** | +| **PAN is a bearing, not a constant** | yaw 0/90/180/270° → **+0.00 +0.85 −0.00 −0.85** | +| **ZERO DRAWS is structural** | default **191 draws / 30,342 tris == `?ambient=0`, byte for byte** | +| `?classic=1` | layer OFF, no sources constructed | +| **THE FALSIFIABILITY CONTROL (kept verbatim)** | a second server on its own port whose root is `web/` **symlink-for-symlink** except one patched `manifest.json` — `audio.sfx['dog-bark-fence']` renamed. That source reports **`no-entry`, 0 plays, never `audible`**, anchors still 63; the other five are **unchanged**; **0 console errors** (a missing key is silence by house law, not an error) | + +**The pan arm's first cut was itself nearly vacuous and I am recording it.** Two bearings (0°/180°) +read `+0.00 / −0.00`, because that anchor happened to sit on the forward axis — a working panner +would have been reported "inconclusive". Four bearings cannot be dodged by geometry. *The gate's own +first draft failed the standard the gate exists to enforce.* + +**What this gate cannot do, said plainly:** it cannot confirm a human would HEAR anything. The +autoplay policy needs a trusted gesture and headless output goes nowhere. Everything downstream of +the unlock is measured; the acoustics are not. **Nobody in this loop has ears.** The gate prints that +sentence on every run rather than leaving it in a notes file nobody opens. + +#### (b) `smoke_character` — §1.2, the per-town character vector. **GREEN, 0 warns.** + +- **Classic BY CONSTRUCTION**: the default boot's character is the frozen v1 literals + (`grass`/`djsim-road`/`djsim-footpath`/`brickpave`), **all four tints null**, kerb `0x9a948c`, sky + null; all five ground meshes untinted `#ffffff` plus the flat kerb; **still exactly 5 town-wide + meshes**. No flag test is involved — a boot with no town key has no character, which is what + `?classic=1` and the synthetic default both get. +- **The per-town arm, against the SHIPPED index**: all **23** keys carry their own entry, including + the two that carry `state: ''` — `newtown_godverse` and `redhill_godverse`. A `state`-keyed + implementation would have dropped **exactly those two**, silently. Asserted against + `assets/towns/index.json`, not against a list in the gate. +- **THE CONTROL**: `townCharacter('__no_such_town_r38')` returns **`matched:false`**, label + `unmatched`, and so does `townCharacter(null)`. Without this arm, "matched is true for all 23" is a + claim a `matched: true` constant would satisfy. +- **Not vacuous as a table**: **21 distinct palettes across 23 towns** (the 2 repeats are the + deliberate godverse↔real sibling pairs). B's count, reproduced by a harness B did not write. A + single shared palette would FAIL this arm by name. +- **The two orphans are on the GPU, not just in a table**: booted live, `castlemaine_real` binds + **`ground-gravel.jpg`**, `darwin_real` binds **`ground-reddust.jpg`**, `hobart_real` binds + **`ground-grass-temperate.jpg`** — read off the live material's texture URL. Both orphans had been + selected by **nothing** since v1. + +#### (c) `smoke_magpie` — §1.3, the magpie. **GREEN, 0 warns.** + +- **THE LATCH, kept verbatim, with its positive control first**: after 1 swoop `gotYouToday` is + **TRUE** (without that, "false afterwards" is worthless); `game.sleep()` → day 1→2, `gotYouToday` + **false**, `swoopsToday` **0**, `dayRolls` 1, mode back to **`perched`**. +- **THE DELTA LAW**: **localStorage gained NO new key** across the swoop and the roll, and the save + blob mentions none of magpie/swoop/washing/yard (keys: cash, collection, day, savedAt, schema, + town, wants). *The baseline is taken **after** a `game.save()` on purpose:* the first run of this + arm reported `procity-save` as "new" and blamed the magpie for v7's own write. A delta law needs a + correctly-drawn baseline or it fabricates violations. +- **THE SEASON IS NOT A CONSTANT — the arm B's own bug demands, run UNFORCED** (`forced:false` + asserted first, or the arm is vacuous): **23 distinct positions in the year across 24 towns**, + min 0 / max 275 days out, against the first cut's **24 of 24 at 214**. **`newtown_real` is in + season on day 1** on the default seed, and **129/1000 `?seed=` values (12.9%)** put the synthetic + default in season against a design ratio of 42/364 = **11.5%** — the distribution, not one lucky + boot. Every one of B's season numbers reproduces. +- **TERRITORY SCALES WITH THE ROAD GRAPH**: synthetic **6** over 6.78 km → `bendigo_real` **157** over + 188.6 km (1 per ~1.2 km both times). **A fixed-capacity magpie passes every other arm in this gate** + — which is why this one is here. +- **+1 DRAW, and the 0 is by absence**: same pose on both boots (taken once off the `?magpie=1` boot + and reused verbatim — `?magpie=0` has no `PROCITY.magpie` to ask, and two different poses is not an + A/B), sim quiet: **165 → 166 draws (+1), 22,378 → 22,560 tris (+182)** at instance count 1. + `?magpie=0` constructs nothing at all. + +#### (d) `smoke_yards` — §1.4, backyard Australia. **GREEN, 0 warns, and it SKIPS LOUDLY.** + +- **The subject is real**: **145 house + 36 yard = 181 lots**; **181 hoists → 394 items, cap 256, 256 + active** (the cap BINDS here, so proximity-first spending is doing real work); at **night the count + is 0** — the washing comes in, and that zero is the vendored early-return, not a small number. +- **The cost decomposition, both halves falsifiable at one pose, sim quiet on all three boots** + (residential centroid `[9.04, 0.87]`): `?yards=0` **193 d / 30,582 t** · `?washing=0` **191 d / + 32,518 t** · default **192 d / 33,030 t**. So: **the box props add 0 draws** (and in fact save 2 — + see §38.1) while moving **+1,936 tris**, and **the washing costs exactly +1 draw / +512 tris**. A + per-chunk yards module would have read +1 × up to 31 live chunks here. If `?yards=0` were + decoration the triangles would not move, and the gate FAILS on that explicitly. +- **THE LOUD SKIP**: on `newtown_real` (67 lots) and `bendigo_real` (35 lots) the gate prints a + `⊘ SCOPE` line — **0 house/yard lots, 0 hoists, 0 washing items, mesh never created, flag still + ON** — and says in the same breath that the synthetic arm found 181, *which is what makes the zero + a scope statement and not an empty run*. §1.4 is synthetic-only until Ruling 2's civic fetch lands + districts, and the suite now says so out loud every time instead of passing quietly. + +### §38.5 THE SUITE — **`tools/qa.sh --strict --matrix` GREEN on the true final tree** + +Run on the live working tree, which at the time of the run equalled **`74d6257`** (Lane E's session-2 +publish landed mid-round; its `web/` diff against B's `4514cb9` is **manifest + one thumb, no code**, +and both were already present in the tree both budget servers read — so the reconciliation above +measured the final tree, not a stale one). + +``` +SUMMARY 7 passed · 0 failed · 0 warn · 0 skipped +● QA GREEN — every landed gate passed. +``` + +- **`flags_check` GREEN — 0 fails, 0 warns**, with all four new R38 gates inside it (they print their + own `GATE R38 §1.x` banners in the suite log, and `smoke_yards` prints its two `⊘ SCOPE` lines). +- **MATRIX GREEN — 10 towns × 7 gates** (boot · determinism · budget · district · roads · tram · + noassets): 2 synthetic + 3 fixtures + 5 real caches, every town byte-equal on re-boot, **0 fetches + under `?noassets` across all ten**, tram verdicts unchanged (fenced on bendigo / fremantle / + katoomba_real, runs elsewhere). Wave 1 moved nothing in the matrix. +- scaffold_check · consistency · manifest validator (`--depot`) · interior figure-scale: all green. + 29 interior figs across 6 shops still human-sized; the R36 DJ bodies still spawn as rigs. +- Total: **flags harness ~18 min**, whole suite ~19 min, on this box with two budget harnesses + running beside it. + +### §38.6 HELD, AND HANDED ON + +Nothing is held. Wave 1 gates green and the budget did not move. The following are **recorded, not +fixed**, because they are other people's files or other rounds' scope: + +1. **`magpie.js`'s own header contradicts itself on the triangle count** — `:9` and `:123` say + **154 triangles**, `:42` says **182**, and the measured number (my gate, an independent harness) is + **182**. Two stale comments in Lane B's file; flagged, not edited. It is the kind of drift R38's own + §1.3 season bug was made of. +2. **E's three published GLBs are in the manifest but not under `web/assets/models/`** — they live in + `pipeline/_normalized/` and on the public depot, and `validate_manifest.py --depot` passes because + its contract is "local `_normalized` **OR** HEAD 200 on the depot". Nothing loads them (B declined + all three on measured budget). **Not a defect** — recorded so nobody reads the manifest as a + shipping list. +3. **The corpus TRI pin needs its station spacing quoted with it** (§38.3). Filed as a method note on + `budget_walk.py`, which now carries four methods with four different biases; the tool's docstring + says which is which. +4. Carried from R37, unchanged: **A's D1–D4** · **the matrix's budget column is bookmark numbers** + (still a regression signal, not the budget of record) · **`?r=3` breaches at 307** and is still an + ungated diagnostic override · **should `?classic=1` show the town selector at all**. + +**No tag.** Wave 1 is content; the epoch tags when it is whole. diff --git a/tools/flags_check.py b/tools/flags_check.py index 00716fc..5931a6c 100644 --- a/tools/flags_check.py +++ b/tools/flags_check.py @@ -3397,6 +3397,838 @@ def smoke_rotation(p): b.close() +# ══════════════════════════════════════════════════════════════════════════════════════════════════ +# [R38 — v8 WAVE 1, INHABITATION] THE FOUR GATES (Lane F) +# +# Four content systems landed in one round (Lane B `4514cb9`, Lane E `1568c9b`). Each gate below +# ships its FALSIFIABILITY CONTROL demonstrated in the same run, because this house has now twice +# shipped a mechanism that produced a constant and gated green on it: +# · R37 killed two vacuous gates (`smoke_tram`'s single assertion that was TRUE on the fenced path; +# the classic covenant's total absence of a network assertion); +# · R38's own §1.3 season mechanism was keyed on `citySeed` alone, made **24 of 24 towns a constant +# 214 days from swooping season**, and would have gated green because `?magpie=1` forces the +# state. Lane B caught it in its own sweep. Every arm here therefore either (a) flips a subject +# between two boots that differ in exactly one thing, or (b) asserts a NEGATIVE that a broken +# build would satisfy — never "the object exists". +# +# The house rule these four obey, in one line: **a gate that cannot fail is not a gate.** +# ══════════════════════════════════════════════════════════════════════════════════════════════════ + +R38_AMB_KEYS = ['dog-bark-fence', 'mower-distant', 'radio-through-window', 'sprinkler', + 'roller-door', 'magpie-carol'] +R38_V1_GROUND = {'road': 'djsim-road', 'foot': 'djsim-footpath', 'base': 'grass', 'plaza': 'brickpave'} +R38_V1_KERB = 0x9a948c +R38_ALT_PORT = PORT + 41 # the patched-manifest control server (its own port, its own root) + + +def _r38_patched_root(mutate): + """A second web root that is `web/` symlink-for-symlink EXCEPT `assets/manifest.json`, which is a + patched copy. Everything else — index.html, every module, every byte of every asset — is the same + inode, so the only difference between the two servers is the one manifest key `mutate` changes. + That is what makes the control's verdict attributable.""" + import tempfile + d = pathlib.Path(tempfile.mkdtemp(prefix='procity-r38-ctl-')) + src = ROOT / 'web' + for e in src.iterdir(): + if e.name != 'assets': + (d / e.name).symlink_to(e) + a = d / 'assets'; a.mkdir() + for e in (src / 'assets').iterdir(): + if e.name != 'manifest.json': + (a / e.name).symlink_to(e) + man = json.loads((src / 'assets' / 'manifest.json').read_text()) + mutate(man) + (a / 'manifest.json').write_text(json.dumps(man, indent=1)) + return d + + +def _r38_serve(root, port): + proc = subprocess.Popen(['python3', '-m', 'http.server', str(port)], cwd=str(root), + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + for _ in range(60): + if port_up(port): return proc + time.sleep(0.1) + proc.terminate() + return None + + +def _r38_boot_at(pg, host, query): + pg.goto(f'{host}/index.html?seed={SEED}&dbg=1' + (('&' + query) if query else '')) + 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: + pg.wait_for_function("() => window.PROCITY && (!window.PROCITY.fleet || window.PROCITY.fleet.ready)", timeout=12000) + except Exception: + pass + + +# The five ground meshes, hashed over VERTEX + UV bytes (FNV-1a, the JS_VERTEX_FP shape) plus the +# material identity. §1.2's whole claim is "+0 draws / +0 tris and classic is byte-identical BY +# CONSTRUCTION" — a hash of the geometry and a read of the shared material is exactly that claim. +JS_R38_GROUND = """() => { + const P = window.PROCITY, g = P.scene.getObjectByName('ground'); + const c = P.character || null; + const fnv = (arr) => { let h = 0x811c9dc5 >>> 0; + const u = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength); + for (let i = 0; i < u.length; i++) { h = (h ^ u[i]) >>> 0; h = Math.imul(h, 0x01000193) >>> 0; } + return h >>> 0; }; + const meshes = []; + if (g) g.traverse((o) => { + if (!o.isMesh || !o.geometry) return; + const pos = o.geometry.getAttribute('position'), uv = o.geometry.getAttribute('uv'); + let h = 0x811c9dc5 >>> 0; + if (pos) h = (h ^ fnv(pos.array)) >>> 0; + if (uv) h = (h ^ fnv(uv.array)) >>> 0; + const m = o.material; + const src = (m && m.map && m.map.image && (m.map.image.src || m.map.image.currentSrc)) || null; + meshes.push({ fp: h >>> 0, color: m && m.color ? '#' + m.color.getHexString() : null, + map: src ? String(src).split('/').pop() : null, + tris: pos ? (o.geometry.index ? o.geometry.index.count : pos.count) / 3 : 0 }); + }); + return { + townKey: P.townKey || null, + character: c ? { key: c.key, label: c.label, matched: !!c.matched, + ground: Object.assign({}, c.ground), tint: Object.assign({}, c.tint), + kerb: c.kerb, sky: c.sky } : null, + meshes, meshCount: meshes.length, + }; +}""" + +JS_R38_AMB = """() => { + const A = window.PROCITY.audio; + const a = A && A.state && A.state.ambient; + if (!a) return null; + const out = { panning: !!a.panning, off: !!a.off, ready: !!(A.state && A.state.ready), sources: {} }; + for (const k of Object.keys(a.sources || {})) { + const s = a.sources[k]; + out.sources[k] = { anchors: s.anchors, status: s.status, dist: s.dist, gain: s.gain, + pan: s.pan, plays: s.plays }; + } + return out; +}""" + + +def _r38_amb(pg): + return pg.evaluate(JS_R38_AMB) + + +def _r38_home_lots(pg, n=34): + """A deterministic spread of house/yard lot centres — the anchor pool §1.1 and §1.4 both derive + from. Returned in plan order, evenly sampled, so both the treatment and the control walk the + SAME list.""" + return pg.evaluate("""(n) => { + const L = (window.PROCITY.plan.lots || []).filter((l) => l.use === 'house' || l.use === 'yard'); + if (!L.length) return []; + const out = []; const step = Math.max(1, Math.floor(L.length / n)); + for (let i = 0; i < L.length && out.length < n; i += step) out.push([L[i].x, L[i].z]); + return out; + }""", n) + + +def _r38_probe_poses(pg, poses): + """Walk the pose list once, recording the closest approach to every §1.1 source. One pass (the + engine re-scans nearest-anchor every 0.33 s) that serves the whole gate, so the per-source + assertions below all stand at a position where the subject is genuinely in range.""" + best = {k: (1e9, None) for k in R38_AMB_KEYS} + for (x, z) in poses: + pg.evaluate("([x, z]) => window.DBG.teleport(x, z, 0)", [x, z]) + pg.wait_for_timeout(420) + st = _r38_amb(pg) + if not st: continue + for k, s in st['sources'].items(): + d = s.get('dist') + if d is not None and d < best.get(k, (1e9, None))[0]: + best[k] = (d, (x, z)) + return best + + +def _r38_wait_status(pg, key, want, timeout_s=22, poll=0.5): + """Poll one source's status until it is in `want`. Returns the final reading.""" + t0 = time.time() + last = None + while time.time() - t0 < timeout_s: + st = _r38_amb(pg) + last = st['sources'].get(key) if st else None + if last and last['status'] in want: + return last + pg.wait_for_timeout(int(poll * 1000)) + return last + + +def smoke_ambient(p): + """[R38 §1.1] THE AUDIO INHABITATION LAYER — six new mixer layers, zero draws. + + Arms, each with its control: + 1. six sources resolve against the SHIPPED manifest (not a table of hopes) and all six anchor + on the default boot; + 2. THE FALSIFIABILITY CONTROL, kept verbatim from Lane B: a second server whose ONLY difference + is `audio.sfx['dog-bark-fence']` renamed → that source must report `no-entry`, 0 plays, and + must NEVER reach `audible`, while the other five read exactly what they read unpatched; + 3. HOURS is a real gate — the sprinkler ([6,10)) flips audible→idle between seg 0 (06:30) and + seg 2 (12:30) at the SAME position; + 4. DAY-OF-WEEK is a real gate — the mower (weekend-only) flips idle→audible between day 1 + (Monday) and day 6 (Saturday) at the same position and the same segment; + 5. PANNING is a real bearing, not a constant — the pan SIGN FLIPS when the camera turns 180°; + 6. ZERO DRAWS is structural — `?ambient=0` and the default boot read the same draw count at a + fixed pose with the citizen sim quiet. + + WHAT THIS GATE CANNOT DO, said plainly: it cannot confirm that a human would HEAR anything. + The autoplay policy needs a trusted gesture and headless output goes nowhere; everything + downstream of the unlock (manifest resolve → fetch → decode → layer gain → pan → one-shot + scheduling → play counters) is measured, and the acoustics are not. Nobody in this loop has ears. + """ + head('GATE R38 §1.1: the audio inhabitation layer (6 point sources · hours · day · pan · the patched-manifest control)') + b, pg, errs = new_page(p) + ref = None; poses = None; best = None + try: + boot(pg, '') + # A real Input-domain KEY press: the autoplay unlock (audio.js listens for pointerdown OR + # keydown). Deliberately not a click — a click on the canvas also requests Pointer Lock, which + # the automation browser refuses, and the resulting console error is an artifact of the probe + # rather than of the build (R30/R37 recorded the same four). + pg.keyboard.press('Shift') + pg.wait_for_timeout(500) + pg.evaluate("() => window.DBG.setSegment(2)") + st = _r38_amb(pg) + if not st: + FAIL('§1.1: no PROCITY.audio.state.ambient on the default boot — the layer did not construct') + return + if st.get('off'): + FAIL('§1.1: the ambient layer reports off on the DEFAULT boot (it is default-on)') + return + got = sorted(st['sources'].keys()) + if got == sorted(R38_AMB_KEYS): + OK(f"six point sources published: {' '.join(R38_AMB_KEYS)}") + else: + FAIL(f"§1.1: source table is {got}, want {sorted(R38_AMB_KEYS)}") + return + OK(f"StereoPannerNode available: panning={st['panning']} (mono fail-soft says so in the same field)") + + # ── arm 1: every key resolves against the manifest that actually shipped ── + man = json.loads((ROOT / 'web' / 'assets' / 'manifest.json').read_text()) + banks = man.get('audio', {}) + missing = [k for k in R38_AMB_KEYS + if not any(k in banks.get(bk, {}) for bk in ('sfx', 'ambience', 'music'))] + if missing: FAIL(f"§1.1: manifest has no entry for {missing} — the table names sounds nobody shipped") + else: OK('all six keys resolve in the shipped manifest (Lane E `1568c9b`)') + noent = [k for k, s in st['sources'].items() if s['status'] == 'no-entry'] + if noent: FAIL(f"§1.1: {noent} report no-entry on the UNPATCHED boot") + else: OK('no source reports no-entry unpatched — the control below therefore means something') + zero = [k for k, s in st['sources'].items() if not s['anchors']] + if zero: FAIL(f"§1.1: {zero} anchored NOWHERE on the default boot (the magpie-carol running-counter bug's shape)") + else: OK('all six anchored on the default boot: ' + + ' · '.join(f"{k} {st['sources'][k]['anchors']}" for k in R38_AMB_KEYS)) + + # ── position once, use everywhere ── + poses = _r38_home_lots(pg) + if len(poses) < 8: + FAIL(f"§1.1: only {len(poses)} house/yard lots on the default boot — the pose search is vacuous") + return + best = _r38_probe_poses(pg, poses) + OK('closest approach per source over %d residential poses: %s' % ( + len(poses), ' · '.join(f"{k} {best[k][0]:.0f}m" for k in R38_AMB_KEYS))) + + # ── arm 2 (positive control for the whole gate): ONE source reaches `audible` ── + # For a one-shot, `audible` is only published once `plays > 0` (audio.js), so this single + # assertion covers fetch, decode, gain, scheduling and the play counter in one. + dpos = best['dog-bark-fence'][1] + if not dpos: + FAIL('§1.1: no pose ever came within range of a dog anchor') + else: + pg.evaluate("([x, z]) => window.DBG.teleport(x, z, 0)", list(dpos)) + r = _r38_wait_status(pg, 'dog-bark-fence', {'audible'}, timeout_s=26) + if r and r['status'] == 'audible' and r['plays'] > 0: + OK(f"positive control: dog-bark-fence AUDIBLE at {r['dist']} m — gain {r['gain']}, " + f"pan {r['pan']}, {r['plays']} play(s) scheduled") + ref = _r38_amb(pg) + else: + FAIL(f"§1.1: dog-bark-fence never reached audible in 26 s at {best['dog-bark-fence'][0]:.0f} m " + f"(status {r and r['status']}, plays {r and r['plays']}) — the whole layer is unproven") + + # ── arm 5: the pan is a BEARING, not a constant ── + if dpos: + # FOUR bearings, not two. An anchor that happens to sit on the forward axis reads ~0 at + # yaw 0 AND at yaw 180 — a two-bearing arm would call a working panner inconclusive (it + # did, on the first run of this gate). Turning through all four quadrants means the + # anchor is hard right at one of them and hard left at another, whatever its bearing. + pans = [] + for k in range(4): + pg.evaluate("([x, z, y]) => window.DBG.teleport(x, z, y)", [dpos[0], dpos[1], k * 1.5707963]) + pg.wait_for_timeout(600) + pans.append(_r38_amb(pg)['sources']['dog-bark-fence']['pan']) + if max(pans) > 0.25 and min(pans) < -0.25: + OK('pan is a real bearing, not a constant: yaw 0/90/180/270° → ' + + ' '.join(f'{v:+.2f}' for v in pans) + + ' (hard right at one bearing, hard left at another)') + else: + FAIL('pan does not track the camera: yaw 0/90/180/270° → ' + + ' '.join(f'{v:+.2f}' for v in pans) + + ' — the StereoPanner is not driven by the bearing-to-anchor') + + # ── arm 3: HOURS is a real gate (sprinkler [6,10)) ── + spos = best['sprinkler'][1] + if spos and best['sprinkler'][0] < 15: + pg.evaluate("([x, z]) => { window.DBG.setSegment(0); window.DBG.teleport(x, z, 0); }", list(spos)) + on = _r38_wait_status(pg, 'sprinkler', {'audible', 'loading'}, timeout_s=10) + pg.evaluate("([x, z]) => { window.DBG.setSegment(2); window.DBG.teleport(x, z, 0); }", list(spos)) + off = _r38_wait_status(pg, 'sprinkler', {'idle'}, timeout_s=10) + if on and on['status'] in ('audible', 'loading') and off and off['status'] == 'idle': + OK(f"HOURS arm: sprinkler {on['status']} at 06:30 → idle at 12:30, same pose ({on['dist']} m) " + '— the [6,10) window is enforced, not decorative') + else: + FAIL(f"§1.1 hours arm: sprinkler 06:30 {on and on['status']} / 12:30 {off and off['status']} " + '— the hours rule did not change the state') + else: + FAIL(f"§1.1 hours arm: no pose within the sprinkler's 15 m radius " + f"(closest {best['sprinkler'][0]:.0f} m) — arm SKIPPED, and a skipped arm is not a pass") + pg.evaluate("() => window.DBG.setSegment(2)") + + # ── arm 4: DAY OF WEEK is a real gate (mower = weekend only; dow = (day-1)%7) ── + mpos = best['mower-distant'][1] + if mpos and best['mower-distant'][0] < 95: + pg.evaluate("([x, z]) => window.DBG.teleport(x, z, 0)", list(mpos)) + wk = _r38_wait_status(pg, 'mower-distant', {'idle'}, timeout_s=8) + day = pg.evaluate("() => window.PROCITY.game && window.PROCITY.game.day") + for _ in range(8): + d = pg.evaluate("() => { window.PROCITY.game.sleep(); return window.PROCITY.game.day; }") + pg.wait_for_timeout(120) + if (d - 1) % 7 == 5: break # Saturday + d2 = pg.evaluate("() => window.PROCITY.game.day") + pg.evaluate("([x, z]) => { window.DBG.setSegment(2); window.DBG.teleport(x, z, 0); }", list(mpos)) + we = _r38_wait_status(pg, 'mower-distant', {'audible', 'loading'}, timeout_s=14) + if wk and wk['status'] == 'idle' and we and we['status'] in ('audible', 'loading'): + OK(f"DAY arm: mower idle on day {day} (dow {(day-1)%7}, a weekday) → {we['status']} on day {d2} " + f"(dow {(d2-1)%7}, Saturday) at the same pose — the local dowOf() rule is enforced") + else: + FAIL(f"§1.1 day arm: mower day {day} {wk and wk['status']} → day {d2} {we and we['status']} " + '— weekend-only did not change the state') + else: + FAIL(f"§1.1 day arm: no pose within the mower's 95 m radius (closest {best['mower-distant'][0]:.0f} m) — arm SKIPPED") + + real = [e for e in errs if 'PointerLock' not in e and 'Pointer Lock' not in e] + if real: FAIL(f"§1.1: {len(real)} console error(s) on the default boot; first: {real[0][:140]}") + else: OK('0 console errors on the default boot (Pointer Lock refusals excluded — the automation ' + 'browser, recorded R30/R37)') + finally: + b.close() + + # ══ arm 6: ZERO DRAWS, and it is structural. Sim quiet on BOTH sides (?roster=v1&pop=0) so the + # comparison is of the audio layer, not of how many peds happened to spawn on wall-clock time — + # the trap R37 named and R38 reproduced. + draws = {} + for label, q in (('default', 'roster=v1&pop=0'), ('?ambient=0', 'ambient=0&roster=v1&pop=0')): + b, pg, _ = new_page(p) + try: + boot(pg, q) + r = pg.evaluate("() => { window.DBG.setSegment(2); return window.DBG.teleport(0, 0, 0); }") + draws[label] = (r['drawCalls'], r['tris']) + finally: + b.close() + if draws['default'][0] == draws['?ambient=0'][0] and draws['default'][1] == draws['?ambient=0'][1]: + OK(f"ZERO DRAWS confirmed: default {draws['default'][0]} draws / {draws['default'][1]:,} tris " + '== ?ambient=0, byte for byte (nothing in §1.1 reaches the scene graph)') + else: + FAIL(f"§1.1 claims zero draws but default {draws['default']} != ?ambient=0 {draws['?ambient=0']}") + + # ══ arm 2b: THE FALSIFIABILITY CONTROL — kept verbatim ═══════════════════════════════════════ + # One manifest key renamed. Nothing else on the disk differs (symlinked root). That source must + # report `no-entry`, must never reach `audible`, must have 0 plays; the other five must read + # exactly what they read unpatched. A missing key is silence by house law, so `status` is the + # ONLY thing that can tell a missing key from a quiet street — which is why the field exists. + if ref is None or not poses: + FAIL('§1.1: the unpatched reference reading was never taken — the control cannot be attributed') + return + def _rename_dog(m): + m['audio']['sfx']['dog-bark-fence-MISSING'] = m['audio']['sfx'].pop('dog-bark-fence') + root = _r38_patched_root(_rename_dog) + proc = _r38_serve(root, R38_ALT_PORT) + if not proc: + FAIL(f"§1.1 control: could not start the patched-manifest server on :{R38_ALT_PORT}") + return + b, pg, errs = new_page(p) + try: + _r38_boot_at(pg, f'http://127.0.0.1:{R38_ALT_PORT}', '') + pg.keyboard.press('Shift') + pg.wait_for_timeout(400) + pg.evaluate("() => window.DBG.setSegment(2)") + dpos = best['dog-bark-fence'][1] + pg.evaluate("([x, z]) => window.DBG.teleport(x, z, 0)", list(dpos)) + r = _r38_wait_status(pg, 'dog-bark-fence', {'no-entry'}, timeout_s=12) + st2 = _r38_amb(pg) + d = st2['sources']['dog-bark-fence'] + if d['status'] == 'no-entry' and d['plays'] == 0: + OK(f"CONTROL FIRES: one manifest key renamed → dog-bark-fence reports no-entry, {d['plays']} plays, " + f"anchors still {d['anchors']} (it is the SOUND that is missing, not the street)") + else: + FAIL(f"§1.1 CONTROL DID NOT FIRE: with the key renamed dog-bark-fence reads status " + f"{d['status']}, plays {d['plays']} — this gate cannot distinguish a shipped sound from a missing one") + # the other five must be untouched + drift = [k for k in R38_AMB_KEYS if k != 'dog-bark-fence' + and st2['sources'][k]['anchors'] != ref['sources'][k]['anchors']] + if drift: FAIL(f"§1.1 control: renaming one key moved the anchor counts of {drift} — the sources are coupled") + else: OK('the other five sources are unchanged by the patch (anchor counts identical) — the control is attributable') + if any(s['status'] == 'audible' for k, s in st2['sources'].items() if k == 'dog-bark-fence'): + FAIL('§1.1 control: the renamed source reached audible') + real = [e for e in errs if 'PointerLock' not in e and 'Pointer Lock' not in e] + if real: FAIL(f"§1.1 control: {len(real)} console error(s) — a missing key must be SILENT, not an " + f"error; first: {real[0][:140]}") + else: OK('a missing key is silence, not an error: 0 console errors on the patched boot (house audio law 1)') + finally: + b.close() + proc.terminate() + import shutil; shutil.rmtree(root, ignore_errors=True) + + # ── scope + classic, stated rather than assumed ── + b, pg, _ = new_page(p) + try: + boot(pg, 'classic=1') + st = _r38_amb(pg) + if st is None or st.get('off'): + OK('?classic=1: the ambient layer is OFF (no sources constructed) — the covenant boot never hears it') + else: + FAIL(f"§1.1: ?classic=1 constructed the ambient layer ({st})") + finally: + b.close() + print(' \033[36m[record]\033[0m audible OUTPUT is NOT verified here and cannot be headlessly: the autoplay ' + 'policy needs a trusted gesture and nothing plays to a speaker. Everything downstream of the unlock ' + 'is measured above. Nobody in this loop has ears.') + + +def smoke_character(p): + """[R38 §1.2] THE PER-TOWN CHARACTER VECTOR — +0 draws, +0 tris, classic byte-identical. + + Arms: + 1. CLASSIC BY CONSTRUCTION — the default boot's character is the frozen v1 literals, every + ground material is untinted (#ffffff), the kerb is 0x9a948c, and the ground is still exactly + 5 meshes. No `if (classic)` is needed because a boot with no town key HAS no character. + 2. THE VACUOUS ARM IS PER-TOWN, not per-corpus — every one of the 23 shipped town keys has its + own entry, INCLUDING the two carrying `state: ''` (`newtown_godverse`, `redhill_godverse`), + which a `state`-keyed implementation would have dropped silently. Measured against the + shipped index, not asserted. + 3. THE CONTROL: an unknown key must return `matched:false`. Without this arm, "matched is true + for all 23" is a claim that a `matched: true` constant would satisfy. + 4. NOT VACUOUS AS A TABLE — the 23 towns must not all carry the SAME palette (a character + vector that gives every town identical ground is the §1.3 season bug wearing a hat). + 5. THE TWO ORPHANS ARE ON THE GPU — `gravel` (Castlemaine) and `reddust` (Darwin, both Red + Hills) were selected by NOTHING since v1. Asserted at the bound material's texture URL on a + live boot, not at the table. + """ + head('GATE R38 §1.2: the per-town character vector (classic by construction · per-town arm · the two orphans on the GPU)') + b, pg, errs = new_page(p) + try: + boot(pg, '') + g = pg.evaluate(JS_R38_GROUND) + c = g['character'] + if not c: + FAIL('§1.2: PROCITY.character is absent on the default boot'); return + if c['matched'] is False and c['key'] is None and c['ground'] == R38_V1_GROUND \ + and all(v is None for v in c['tint'].values()) and c['kerb'] == R38_V1_KERB and c['sky'] is None: + OK('classic BY CONSTRUCTION: default boot → frozen v1 literals ' + f"({c['ground']['base']}/{c['ground']['road']}/{c['ground']['foot']}/{c['ground']['plaza']}), " + 'all four tints null, kerb 0x9a948c, sky null') + else: + FAIL(f"§1.2: the default boot's character is NOT the frozen literals: {c}") + tinted = [m for m in g['meshes'] if m['color'] and m['color'].lower() not in ('#ffffff',)] + # the kerb mesh is a flat colour by design (0x9a948c) — it is the only non-white one + tinted = [m for m in tinted if m['color'].lower() != '#9a948c'] + if not tinted: + OK(f"the default boot's {g['meshCount']} ground meshes are all untinted (#ffffff) + the 0x9a948c kerb " + '— the shared material is the same object v1 built') + else: + FAIL(f"§1.2: default boot ground carries tints {[m['color'] for m in tinted]} — classic is not byte-identical") + if g['meshCount'] == 5: + OK('ground is still exactly 5 town-wide meshes (the +0-draw mechanism, re-measured)') + else: + FAIL(f"§1.2: ground has {g['meshCount']} meshes, want 5 — the palette swap added draws") + print(f" \033[36m[record]\033[0m default-boot ground fp: " + + ' / '.join(f"{m['fp']:08x}" for m in g['meshes'])) + + # ── arms 2/3/4: the table itself, over the SHIPPED index ── + idx = json.loads((ROOT / 'web' / 'assets' / 'towns' / 'index.json').read_text()) + towns = idx['towns'] if isinstance(idx, dict) else idx + keys = [t['key'] for t in towns] + blank = [t['key'] for t in towns if not (t.get('state') or '').strip()] + r = pg.evaluate("""async (keys) => { + const m = await import('./js/world/character.js'); + const out = {}; + for (const k of keys) { + const c = m.townCharacter(k); + out[k] = { matched: !!c.matched, label: c.label, sig: JSON.stringify([c.ground, c.tint, c.sky]) }; + } + const bad = m.townCharacter('__no_such_town_r38'); + out.__control = { matched: !!bad.matched, label: bad.label, sig: JSON.stringify([bad.ground, bad.tint, bad.sky]) }; + out.__null = { matched: !!m.townCharacter(null).matched }; + return out; + }""", keys) + unmatched = [k for k in keys if not r[k]['matched']] + if unmatched: FAIL(f"§1.2: {len(unmatched)} shipped town key(s) have NO entry: {unmatched}") + else: OK(f"all {len(keys)} shipped town keys carry their own entry (per-town, not per-corpus)") + if all(r[k]['matched'] for k in blank): + OK(f"the per-corpus trap is closed: {blank} carry state:'' in the index and are matched BY KEY " + '— a state-keyed arm would have dropped exactly these two, silently') + else: + FAIL(f"§1.2: the two state:'' towns {blank} are not matched by key") + if r['__control']['matched'] is False and r['__null']['matched'] is False: + OK("CONTROL FIRES: an unknown key returns matched:false (label '%s') and townCharacter(null) too " + '— "matched" is a real bit, not a constant' % r['__control']['label']) + else: + FAIL('§1.2 CONTROL DID NOT FIRE: an unknown key reports matched:%s — the 23 greens above prove nothing' + % r['__control']['matched']) + sigs = {r[k]['sig'] for k in keys} + if len(sigs) == 1: + FAIL(f"§1.2 VACUOUS: all {len(keys)} towns share ONE palette — the character vector produces a constant") + elif len(sigs) >= 20: + OK(f"{len(sigs)} distinct palettes across {len(keys)} towns " + f"({len(keys) - len(sigs)} deliberate godverse↔real sibling repeats)") + else: + WARN(f"§1.2: only {len(sigs)} distinct palettes across {len(keys)} towns") + # the orphans, at the table + orph = {'castlemaine_real': 'gravel', 'darwin_real': 'reddust', + 'redhill_real': 'reddust', 'redhill_godverse': 'reddust'} + bad = {k: json.loads(r[k]['sig'])[0]['base'] for k, want in orph.items() + if json.loads(r[k]['sig'])[0]['base'] != want} + if bad: FAIL(f"§1.2: the formerly-orphaned skins are not selected where claimed: {bad}") + else: OK('gravel → Castlemaine · reddust → Darwin + both Red Hills (selected by NOTHING since v1)') + if errs: FAIL(f"§1.2: {len(errs)} console error(s); first: {errs[0][:140]}") + finally: + b.close() + + # ── arm 5: the orphans reach the GPU. The table is a claim; the bound texture is the fact. ── + for town, want, skin in (('castlemaine_real', 'ground-gravel.jpg', 'gravel'), + ('darwin_real', 'ground-reddust.jpg', 'reddust'), + ('hobart_real', 'ground-grass-temperate.jpg', 'grass-temperate')): + b, pg, errs = new_page(p) + try: + boot(pg, f'plansrc=osm&town={town}') + g = pg.evaluate(JS_R38_GROUND) + maps = [m['map'] for m in g['meshes'] if m['map']] + c = g['character'] + if not c or not c['matched']: + FAIL(f"§1.2: {town} booted with character matched={c and c['matched']}") + elif want in maps: + OK(f"{town}: character '{c['label']}' → {skin} is BOUND ({want} on a live ground mesh), " + f"{g['meshCount']} meshes, sky {c['sky']}") + else: + FAIL(f"§1.2: {town} wants {skin} but the bound ground maps are {maps}") + if errs: WARN(f"§1.2 {town}: {len(errs)} console error(s); first: {errs[0][:120]}") + finally: + b.close() + + +def smoke_magpie(p): + """[R38 §1.3] THE MAGPIE — +1 draw on screen, exactly 0 otherwise. + + Arms: + 1. THE LATCH, kept verbatim from the packet: the day-roll clears `gotYouToday` AND + **localStorage gained no new key**. Both halves matter — a positive control first (the latch + must be TRUE before the roll, or "it is false afterwards" is worthless), then the clear, then + the delta-law assertion on storage and on the save blob itself. + 2. THE SEASON IS NOT A CONSTANT — Lane B's first cut keyed the year offset on `citySeed` alone + and made **24 of 24 towns 214 days from swooping season**; it would have gated green because + `?magpie=1` forces the state. So this arm runs UNFORCED and asserts the season varies across + the shipped corpus, and that at least one town is in season on day 1. + 3. TERRITORY SCALES WITH THE ROAD GRAPH — synthetic (6.8 km) and bendigo_real (188 km) must not + land on the same count. A fixed-capacity magpie passes every other arm here. + 4. +1 DRAW, and the 0 is real — same pose, `?magpie=1` vs `?magpie=0`, citizen sim quiet. + """ + head('GATE R38 §1.3: the magpie (the latch + no new storage key · the season is not a constant · territory scales · +1 draw)') + b, pg, errs = new_page(p) + try: + boot(pg, 'magpie=1') + pg.evaluate("() => window.DBG.setSegment(2)") + # Baseline the storage AFTER a save exists. `game.sleep()` writes `procity-save` — that is v7 + # behaviour this item must not be blamed for; the arm is "did the MAGPIE add a key", so the + # pre-existing key has to be in the baseline or the gate reports someone else's write. + pg.evaluate("() => { try { window.PROCITY.game.save(); } catch {} }") + st = pg.evaluate("() => { const m = window.PROCITY.magpie; return m ? JSON.parse(JSON.stringify({" + "territories: m.state.territories, roadKm: m.state.roadKm, forced: m.state.forced," + "inSeason: m.state.inSeason, active: m.state.active, mode: m.state.mode," + "seasonOffset: m.state.seasonOffset })) : null; }") + if not st: + FAIL('§1.3: PROCITY.magpie is null on ?magpie=1'); return + syn_terr = st['territories'] + OK(f"synthetic default: {syn_terr} territories over {st['roadKm']} km of road " + f"(1 per {1000 * st['roadKm'] / max(1, syn_terr):.0f} m), forced={st['forced']}") + + # ── arm 1: the latch, and the storage delta law ── + keys0 = pg.evaluate("() => Object.keys(localStorage).sort()") + act = pg.evaluate("() => window.PROCITY.magpie.state.active") + if not act: + FAIL('§1.3: no active territory picked — the latch arm has no subject'); return + pg.evaluate("(a) => window.DBG.teleport(a.x + 6, a.z + 6, 0)", act) + got = None + for _ in range(40): + pg.wait_for_timeout(400) + s = pg.evaluate("() => ({ got: window.PROCITY.magpie.state.gotYouToday," + " swoops: window.PROCITY.magpie.state.swoopsToday," + " launches: window.PROCITY.magpie.state.launches," + " mode: window.PROCITY.magpie.state.mode })") + if s['got']: + got = s; break + if not got: + FAIL(f"§1.3: he never swooped in 16 s at his own pole (last {s}) — the latch arm is vacuous without it") + else: + OK(f"positive control: after {got['swoops']} swoop(s) — gotYouToday TRUE, launches {got['launches']}") + day0 = pg.evaluate("() => window.PROCITY.game.day") + pg.evaluate("() => window.PROCITY.game.sleep()") + pg.wait_for_timeout(700) + after = pg.evaluate("() => ({ got: window.PROCITY.magpie.state.gotYouToday," + " swoops: window.PROCITY.magpie.state.swoopsToday," + " rolls: window.PROCITY.magpie.state.dayRolls," + " day: window.PROCITY.game.day, mode: window.PROCITY.magpie.state.mode })") + if after['got'] is False and after['swoops'] == 0 and after['day'] == day0 + 1: + OK(f"THE DAY ROLL CLEARS THE LATCH: day {day0}→{after['day']}, gotYouToday false, swoopsToday 0, " + f"dayRolls {after['rolls']}, mode '{after['mode']}' (he is back on his pole — a swoop cannot " + 'be in progress across a night)') + else: + FAIL(f"§1.3: the day roll did not clear the latch: {after}") + keys1 = pg.evaluate("() => Object.keys(localStorage).sort()") + new = [k for k in keys1 if k not in keys0] + if not new: + OK(f"DELTA LAW: localStorage gained NO new key across the swoop and the roll (keys: {keys1})") + else: + FAIL(f"§1.3: localStorage gained {new} — a v8 content layer must not grow the save file") + blob = pg.evaluate("() => { try { return window.PROCITY.game.export(); } catch { return ''; } }") + leaked = [w for w in ('magpie', 'swoop', 'washing', 'yard') if w in blob.lower()] + if leaked: FAIL(f"§1.3: the save blob mentions {leaked}") + else: OK('the save blob mentions none of magpie/swoop/washing/yard — ' + + 'keys: ' + ','.join(sorted(json.loads(blob).keys()) if blob else [])) + if errs: FAIL(f"§1.3: {len(errs)} console error(s); first: {errs[0][:140]}") + finally: + b.close() + + # ── arm 2: THE SEASON IS NOT A CONSTANT (Lane B's own bug, gated) — UNFORCED ── + b, pg, _ = new_page(p) + try: + boot(pg, '') + forced = pg.evaluate("() => window.PROCITY.magpie ? window.PROCITY.magpie.state.forced : null") + if forced is not False: + FAIL(f"§1.3: the default boot reports forced={forced} — the season arm below would be vacuous") + idx = json.loads((ROOT / 'web' / 'assets' / 'towns' / 'index.json').read_text()) + towns = idx['towns'] if isinstance(idx, dict) else idx + keys = [t['key'] for t in towns] + r = pg.evaluate("""async (keys) => { + const m = await import('./js/world/magpie.js'); + const seed = (window.PROCITY.plan.citySeed >>> 0); + const out = { synthetic: m.daysToSeason(1, seed, null) }; + for (const k of keys) out[k] = m.daysToSeason(1, seed, k); + return out; + }""", keys) + vals = sorted(r.values()) + distinct = len(set(vals)) + inseason = [k for k, v in r.items() if v == 0] + if distinct <= 1: + FAIL(f"§1.3 VACUOUS SEASON: all {len(r)} towns are {vals[0]} days from swooping season — " + 'this is exactly the constant Lane B shipped first (24/24 at 214 days)') + else: + OK(f"the season is keyed per town: {distinct} distinct positions in the year across {len(r)} towns " + f"(min {vals[0]}, max {vals[-1]} days out) — NOT the 24-of-24 constant the first cut produced") + if inseason: + OK(f"unforced, on the default seed, {len(inseason)} town(s) are IN swooping season on day 1: {inseason}") + else: + WARN(f"no town is in season on day 1 of the default seed (nearest {vals[0]} days) — " + 'the mechanism varies but nobody arrives to a swoop; design ratio is 42/364 = 11.5%') + # the design ratio itself, over 1000 seeds — the distribution, not one lucky boot + hit = pg.evaluate("""async () => { + const m = await import('./js/world/magpie.js'); + let n = 0; for (let s = 1; s <= 1000; s++) if (m.inSwoopSeason(1, s >>> 0, null)) n++; + return n; + }""") + if 60 <= hit <= 200: + OK(f"{hit}/1000 ?seed= values put the synthetic default in season on day 1 " + f"({hit/10:.1f}% against a design ratio of 42/364 = 11.5%)") + else: + FAIL(f"§1.3: {hit}/1000 seeds in season on day 1 — the distribution is not the designed 11.5%") + finally: + b.close() + + # ── arm 3: territory scales with the road graph ── + b, pg, _ = new_page(p) + try: + boot(pg, 'plansrc=osm&town=bendigo_real&magpie=1') + st = pg.evaluate("() => { const m = window.PROCITY.magpie; return m ? { t: m.state.territories," + " km: m.state.roadKm } : null; }") + if not st: + FAIL('§1.3: no magpie on bendigo_real') + elif st['t'] > syn_terr * 3: + OK(f"territory SCALES with the road graph: synthetic {syn_terr} over {'%.1f' % 6.78} km → " + f"bendigo_real {st['t']} over {st['km']} km (1 per {1000*st['km']/st['t']:.0f} m). A fixed-capacity " + 'magpie would read the same number in both towns and pass every other arm here.') + else: + FAIL(f"§1.3: bendigo_real ({st['km']} km) has {st['t']} territories against synthetic's {syn_terr} " + '— the count is not scaling with the graph') + finally: + b.close() + + # ── arm 4: +1 draw on screen, exactly 0 off. Sim quiet on both sides. ── + res = {}; mpose = None + for label, q in (('magpie=1', 'magpie=1&roster=v1&pop=0'), ('magpie=0', 'magpie=0&roster=v1&pop=0')): + b, pg, _ = new_page(p) + try: + boot(pg, q) + pg.evaluate("() => window.DBG.setSegment(2)") + if mpose is None: + # the pose is taken ONCE, off the ?magpie=1 boot, and reused verbatim on the ?magpie=0 + # boot — where PROCITY.magpie is null and there is nothing to ask. Two different poses + # is not an A/B, it is two numbers. + a = pg.evaluate("() => { const t = window.PROCITY.magpie.state.active; return [t.x + 10, t.z + 10]; }") + mpose = [round(a[0], 2), round(a[1], 2)] + pg.evaluate("(m) => window.DBG.teleport(m[0], m[1], 0)", mpose) + pg.wait_for_timeout(900) + i = pg.evaluate("(m) => window.DBG.teleport(m[0], m[1], 0)", mpose) + cnt = pg.evaluate("() => window.PROCITY.magpie ? window.PROCITY.magpie.count : null") + res[label] = (i['drawCalls'], i['tris'], cnt) + finally: + b.close() + d1, t1, c1 = res['magpie=1']; d0, t0, c0 = res['magpie=0'] + if c0 is not None: + FAIL(f"§1.3: ?magpie=0 still constructed the layer (count {c0})") + else: + OK('?magpie=0 constructs nothing (PROCITY.magpie is null) — the 0 is by absence, not by a count') + print(f" \033[36m[record]\033[0m the cost pose is {mpose} (his territory + 10 m), sim quiet on both boots") + if c1 == 1 and d1 - d0 == 1: + OK(f"+1 DRAW while he is on screen, measured: {d0} → {d1} draws (+1), {t0:,} → {t1:,} tris " + f"(+{t1-t0}) with the instance count at {c1}") + elif c1 == 1: + WARN(f"§1.3: bird on screen (count 1) and draws moved {d0} → {d1} (want +1); tris +{t1-t0}") + else: + FAIL(f"§1.3: could not put the bird on screen for the cost arm (count {c1}) — the +1 claim is unmeasured") + + +def smoke_yards(p): + """[R38 §1.4] BACKYARD AUSTRALIA — the box props ride buildings.js's existing boxMats at +0 draws; + only the washing pays a draw. + + Arms: + 1. THE SUBJECT IS REAL — 181 house/yard lots on the default boot, 181 hoists, N washing items, + the cap spent at 256 by day and **0 at night** (the washing comes in). + 2. THE COST DECOMPOSITION, with both halves falsifiable at one pose, sim quiet: + `?yards=0` vs `?washing=0` → **draws EQUAL, tris DIFFERENT** (the box props are free) + `?washing=0` vs default → **draws +1** (the cloth's own mesh) + If the props were paying a draw the first comparison fails; if `?yards=0` were decoration the + tris would not move. + 3. IT SKIPS LOUDLY ON REAL TOWNS — §1.4 is synthetic-only until the civic fetch lands. On a real + town the gate asserts the zero is REAL (0 house/yard lots ⇒ 0 hoists ⇒ the mesh is never + created) and says so as a SCOPE line. A pass there would be vacuous, and the synthetic arm + above is what makes the zero attributable rather than an empty run. + """ + head('GATE R38 §1.4: backyard Australia (the subject · the cost decomposition · the LOUD SKIP on real towns)') + b, pg, errs = new_page(p) + syn_lots = None + try: + boot(pg, '') + r = pg.evaluate("""() => { + const P = window.PROCITY, L = P.plan.lots || []; + const c = {}; for (const l of L) c[l.use] = (c[l.use] || 0) + 1; + const w = P.washing; + return { uses: c, house: c.house || 0, yard: c.yard || 0, + washing: w ? { hoists: w.state.hoists, items: w.state.items, active: w.state.active, + cap: w.state.cap, count: w.count, night: w.state.night } : null, + flags: { yards: P.flags.yards, washing: P.flags.washing } }; + }""") + syn_lots = r['house'] + r['yard'] + if syn_lots < 100: + FAIL(f"§1.4: only {syn_lots} house/yard lots on the default boot — the whole item has no subject") + return + OK(f"subject: {r['house']} house + {r['yard']} yard = {syn_lots} lots (the 36 `yard` lots had " + 'rendered as blank shopfront sheds since v1)') + w = r['washing'] + if not w: + FAIL('§1.4: PROCITY.washing is null on the default boot') + elif w['hoists'] == syn_lots and w['items'] > w['cap']: + OK(f"washing: {w['hoists']} hoists → {w['items']} items, cap {w['cap']}, {w['active']} active " + '(the cap BINDS here — proximity-first spending is doing real work)') + else: + WARN(f"§1.4: washing {w} against {syn_lots} lots") + # night: the washing comes in, and an empty count is a REAL zero + pg.evaluate("() => window.DBG.setSegment(5)") + pg.wait_for_timeout(300) + pg.evaluate("() => window.DBG.teleport(60, 60, 0)") + pg.wait_for_timeout(500) + n = pg.evaluate("() => ({ c: window.PROCITY.washing.count, night: window.PROCITY.washing.state.night })") + if n['c'] == 0 and n['night']: + OK('at NIGHT the washing comes in: count 0 (the vendored early-return ⇒ the layer costs exactly 0 draws)') + else: + FAIL(f"§1.4: at night the washing layer reads {n} — the night zero is not real") + if errs: FAIL(f"§1.4: {len(errs)} console error(s); first: {errs[0][:140]}") + finally: + b.close() + + # ── arm 2: the cost decomposition at ONE pose, three boots, citizen sim quiet on all three ── + pose = None + b, pg, _ = new_page(p) + try: + boot(pg, 'roster=v1&pop=0') + pose = pg.evaluate("""() => { + const L = (window.PROCITY.plan.lots || []).filter((l) => l.use === 'house' || l.use === 'yard'); + let x = 0, z = 0; for (const l of L) { x += l.x; z += l.z; } + return [ +(x / L.length).toFixed(2), +(z / L.length).toFixed(2) ]; + }""") + finally: + b.close() + cost = {} + for label, q in (('default', 'roster=v1&pop=0'), ('washing=0', 'washing=0&roster=v1&pop=0'), + ('yards=0', 'yards=0&roster=v1&pop=0')): + b, pg, _ = new_page(p) + try: + boot(pg, q) + pg.evaluate("() => window.DBG.setSegment(2)") + pg.evaluate("(p) => window.DBG.teleport(p[0], p[1], 0)", pose) + pg.wait_for_timeout(700) + i = pg.evaluate("(p) => window.DBG.teleport(p[0], p[1], 0)", pose) + cost[label] = (i['drawCalls'], i['tris']) + finally: + b.close() + dY, tY = cost['yards=0']; dW, tW = cost['washing=0']; dD, tD = cost['default'] + print(f" \033[36m[record]\033[0m residential pose {pose}, sim quiet — " + f"?yards=0 {dY}d/{tY:,}t · ?washing=0 {dW}d/{tW:,}t · default {dD}d/{tD:,}t") + if tW == tY: + FAIL(f"§1.4 VACUOUS: ?yards=0 and ?washing=0 render identical triangles ({tW:,}) — ?yards=0 reverts NOTHING") + elif dW <= dY: + note = ('' if dW == dY else + f" — and it is {dY - dW} CHEAPER, because 36 `yard` lots that fell through to buildShopfront()" + ' since v1 (facade + signage + awning materials) are now boxMats-only fenced blocks') + OK(f"+0 DRAWS for every box prop: ?washing=0 {dW} draws vs ?yards=0 {dY} draws while triangles move " + f"{tY:,} → {tW:,} ({tW-tY:+,}){note}. The fences, hoists, aerials and tanks ride an InstancedMesh " + 'that was already being submitted; a per-chunk yards module would have read +1 × up to 31 here.') + else: + FAIL(f"§1.4: the box props ADD {dW - dY} draw(s) (?washing=0 {dW} vs ?yards=0 {dY}) — not +0") + if dD - dW == 1: + OK(f"the washing costs exactly +1 draw: {dW} → {dD} ({tW:,} → {tD:,} tris, {tD-tW:+,}) — ONE town-wide " + 'InstancedMesh at any N, not +1 × up to 31 live chunks') + else: + WARN(f"§1.4: the washing moved draws {dW} → {dD} ({dD-dW:+d}) at this pose (want +1); " + f"tris {tW:,} → {tD:,}") + + # ── arm 3: THE LOUD SKIP. Real towns are 100% use:'shop' until the civic fetch lands. ── + for town in ('newtown_real', 'bendigo_real'): + b, pg, _ = new_page(p) + try: + boot(pg, f'plansrc=osm&town={town}') + r = pg.evaluate("""() => { + const P = window.PROCITY, L = P.plan.lots || []; + let h = 0; for (const l of L) if (l.use === 'house' || l.use === 'yard') h++; + const w = P.washing; + return { lots: L.length, homes: h, washing: w ? { hoists: w.state.hoists, items: w.state.items, + count: w.count } : null, flag: P.flags.yards }; + }""") + if r['homes'] == 0 and r['washing'] and r['washing']['hoists'] == 0 and r['washing']['count'] == 0: + print(f" \033[35m⊘ SCOPE\033[0m §1.4 DOES NOT APPLY on {town}: {r['lots']} lots, " + f"**0 house/yard**, 0 hoists, 0 washing items, mesh never created. The flag is ON " + f"({r['flag']}) and the item still costs exactly nothing — measured, not assumed. " + f"The synthetic arm above found {syn_lots} lots, which is what makes this zero a SCOPE " + 'and not an empty gate. Extends to real towns when A\'s civic fetch lands districts.') + elif r['homes'] == 0: + FAIL(f"§1.4 on {town}: 0 house/yard lots but the washing layer reads {r['washing']} — " + 'the "free on real towns" claim is wrong') + else: + OK(f"§1.4 APPLIES on {town} after all: {r['homes']} house/yard lots, washing {r['washing']} " + '— the civic fetch has landed; retire the synthetic-only scope note') + finally: + b.close() + + + def main(): # [R37] `--only name[,name…]` runs a subset of the smokes by function name (e.g. `--only smoke_tram`). # Development ergonomics only: the suite's contract is still "run them all"; --only prints a banner @@ -3456,6 +4288,11 @@ def main(): smoke_wantlist_travel(p) # R33 — wantlist · keeper rumor · travel-costs-a-day (+ controls) smoke_godbay(p) # R34 — GODBAY offline auction (one hammer · bounded · morning paper) smoke_first_five(p) # R35 — the first five minutes (seeded want · splash · beats + controls) + # ── [R38 v8 WAVE 1] the four inhabitation gates, each with its control demonstrated ── + smoke_ambient(p) # R38 §1.1 — six point sources · hours · day-of-week · pan · THE PATCHED MANIFEST + smoke_character(p) # R38 §1.2 — classic by construction · the per-town arm · the two orphans on the GPU + smoke_magpie(p) # R38 §1.3 — the latch + no new storage key · the season is NOT a constant · +1 draw + smoke_yards(p) # R38 §1.4 — the cost decomposition · the LOUD SKIP on real towns smoke_tier2(p) # R27 — the kill-the-server gate (charter law #1) smoke_real_crate(p) # R24 — THE REAL CRATE (per-shop atlas identity · soft fall · no-op off the path) finally: diff --git a/tools/qa/budget_walk.py b/tools/qa/budget_walk.py index 5a98dbd..b61242b 100644 --- a/tools/qa/budget_walk.py +++ b/tools/qa/budget_walk.py @@ -20,6 +20,11 @@ is a measurement rather than an argument. The three methods: frames, and a real walker gets ~830 frames per chunk). Without --settle you measure a walker who outruns his own streamer, which UNDER-reports. stepwalk the audit's method: `DBG.teleport()` in 8 m steps (teleport does warmup+render). + stepsettle [R38] the audit's stations at a FIXED yaw, but DRAINED and then SETTLED for + `--holdframes` real rAF frames before sampling — so the frame carries the R+1 dispose + residue AND a fully-acquired citizen near-tier at the same time. `stepwalk` under-reports + the crowd (one frame per station); `dwell` under-reports the streamer (clean arrival). + This is the only method that defeats both, and it is what a >300 claim must be measured with. bookmark the pre-audit method: `DBG.shot(name)` at a named bookmark, clean load. Every method reports draws AND tris, the worst frame, and where it stood. @@ -293,6 +298,61 @@ STEPWALK_JS = r""" """ +# ── [R38] the STEPSETTLE method: the audit's fixed-yaw stations, but SETTLED at each one ───────── +# R37 named two under-reporting mechanisms and this round's budget question is what happens when you +# defeat BOTH at once. `stepwalk` walks a station in (so it carries the R+1 dispose residue — up to +# 31 live chunks) but samples ONE frame, so `CitizenSim`'s per-frame near-tier budget (sim.js +# NEAR_MAX 24) has photographed a half-dressed street. `dwell` settles completely but arrives clean +# (27 live chunks). Neither is the worst case. This method is: teleport to the station at a FIXED +# yaw, then hold `hold` real rAF frames so the shell's own frame() runs the sim to steady state, and +# sample the LAST of them. Walked-in chunk residue AND a fully-acquired crowd, in the same frame. +# It is the method Lane B's R38 A/B used (drain, then render through the composer), and it is the +# only one of the four that can answer "is the control's 346 a real transient or an artifact". +STEPSETTLE_JS = r""" +(cfg) => new Promise((resolve) => { + const P = window.PROCITY, D = window.DBG; + const { poly, cum, total } = cfg.path; + const posAt = (s) => { + s = Math.max(0, Math.min(total, s)); + let i = 0; while (i < cum.length - 2 && cum[i + 1] < s) i++; + const seg = cum[i + 1] - cum[i] || 1, f = (s - cum[i]) / seg; + const x = poly[i][0] + (poly[i + 1][0] - poly[i][0]) * f; + const z = poly[i][1] + (poly[i + 1][1] - poly[i][1]) * f; + let dx = poly[i + 1][0] - poly[i][0], dz = poly[i + 1][1] - poly[i][1]; + const L = Math.hypot(dx, dz) || 1; + return { x, z, dx: dx / L, dz: dz / L }; + }; + const stations = []; + for (const dir of [1, -1]) { + for (let k = 0; k <= Math.floor(total / cfg.stepm); k++) + stations.push([dir > 0 ? k * cfg.stepm : total - k * cfg.stepm, dir]); + if (cfg.laps < 2) break; + } + const out = []; + let si = 0, held = 0, drained = 0; + const q0 = posAt(stations[0][0]); + D.teleport(q0.x, q0.z, cfg.fixedYaw === null ? Math.atan2(-q0.dx, -q0.dz) : cfg.fixedYaw); + function step() { + const [s, dir] = stations[si]; + const pend = P.chunks ? P.chunks.pending : 0; + // drain first (a fully-streamed frame is what a real 4.6 m/s walker is permanently in), then + // settle: the crowd is what the hold is actually for. + if (pend > 0 && drained < cfg.maxDrain) { drained++; return requestAnimationFrame(step); } + if (held < cfg.hold) { held++; return requestAnimationFrame(step); } + const r = P.renderer.info.render, pos = P.player.position; + out.push([r.calls, r.triangles, P.chunks ? P.chunks.count : 0, pend, + +pos.x.toFixed(1), +pos.z.toFixed(1), +s.toFixed(1), dir]); + si++; held = 0; drained = 0; + if (si >= stations.length) return resolve(out); + const [s2, d2] = stations[si], q = posAt(s2); + P.player.teleport(q.x, q.z, cfg.fixedYaw === null ? Math.atan2(-q.dx * d2, -q.dz * d2) : cfg.fixedYaw); + requestAnimationFrame(step); + } + requestAnimationFrame(step); +}) +""" + + def boot(pg, host, query, seg): pg.goto(f'{host}/index.html?seed={SEED}&dbg=1' + (('&' + query) if query else '')) pg.wait_for_function('window.DBG && window.DBG.ready === true', timeout=45000) @@ -361,6 +421,11 @@ def run_one(p, host, town, bootflag, method, seg, stepm, laps, maxm, settle, max raw = pg.evaluate(STEPWALK_JS, {'path': path, 'stepm': stepm, 'laps': laps, 'fixedYaw': fixed_yaw}) extra = {} + elif method == 'stepsettle': + raw = pg.evaluate(STEPSETTLE_JS, {'path': path, 'stepm': stepm, 'laps': laps, + 'fixedYaw': fixed_yaw, + 'hold': max(1, holdframes), 'maxDrain': 240}) + extra = {'hold_frames_per_station': max(1, holdframes)} else: if method == 'dwell': x, z, yaw = dwell_at @@ -403,7 +468,8 @@ def main(): ap.add_argument('--port', type=int, default=int(os.environ.get('PROCITY_QA_PORT', '8951'))) ap.add_argument('--town', default='synthetic') ap.add_argument('--boot', default='default', choices=['default', 'classic']) - ap.add_argument('--method', default='walk', choices=['walk', 'stepwalk', 'dwell', 'sweep']) + ap.add_argument('--method', default='walk', + choices=['walk', 'stepwalk', 'stepsettle', 'dwell', 'sweep']) ap.add_argument('--yaws', type=int, default=12, help='bearings sampled per station (--method sweep)') ap.add_argument('--extra', default='', help='extra query string appended to every boot (e.g. r=3&shadows=1)') ap.add_argument('--fixed-yaw', type=float, default=None,