diff --git a/docs/A-progress.md b/docs/A-progress.md index 6a7945c..0cccb0a 100644 --- a/docs/A-progress.md +++ b/docs/A-progress.md @@ -1,10 +1,37 @@ # LANE A — CITYGEN · progress (PROCITY-A) -*Status: **all deliverables landed; self-check all-green (14264/14264); rounds 2–14 closed** (see below). -v3.0 RELEASE round: 400-seed district invariant sweep landed + CITY_SPEC §v3 frozen at `v3.0`. Goldens -unchanged (synthetic `0x3fa36874`, gig `0x1f636349`) — no golden moved, no rng draw added this round.* +*Status: **all deliverables landed; self-check all-green (14264/14264); rounds 2–15 closed** (see below). +v3.0 release close-out: frontage-poster +Z flip (ledger #1) + the one sanctioned gig-golden move +(`0x1f636349 → 0x4f4a549d`); synthetic golden `0x3fa36874` frozen.* *Date: 2026-07-16 · owner: PROCITY-A (Opus 4.8)* +## Round 15 (2026-07-16) — the frontage-poster +Z flip (ledger #1) + one sanctioned golden move + +Lane A ran **first** (B's hero re-shoot + F's tour hang off it). The R14 review found every venue's +own-frontage poster had been on the building's **−Z back** since R13 — ~16 m behind the street face +(B measured 15.9 m), invisible from the street, which is why John's `queue_night` framed no poster. + +Delivered (one commit): +1. **Frontage-poster fix** (`gigs.js` buildPosters item 1): flipped to the **+Z street facade** (B's + canon) AND `ry = lot.ry + π` so the printed face points `+Z` at the queue-side camera (else `DoubleSide` + renders a mirrored band name). Proven: pub + rsl frontage posters now sit **0.06 m from B's door**, + printed-face·(+Z street) = **1.000**. Spine posters untouched. +2. **Gig golden re-pinned `0x1f636349 → 0x4f4a549d`** — the round's ONE sanctioned move (posters moved); + synthetic `0x3fa36874` frozen. §v3 FROZEN marker records the amendment (amendment law). +3. **Selfcheck poster check refined**: the brief expected the 400-seed off-carriageway assert to stay + green, but the `+Z` facade is the block-interior side in *plan* coords, so **corner venues** (a band_room/ + rsl fronting one street with another behind — seed 1 lot#481, 1.58 m from a crossing centreline) put the + frontage poster near that street. That's B's building geometry (the door is there too), not a mid-road + defect — so the no-bitumen rule now **exempts building-bound frontage posters** and keeps it for + free-standing **spine** posters. Sweep green again: **14264/14264**. +4. **CITY_SPEC §v3 amended** (poster prose → `+Z` facade + `ry+π`; golden line; frontage/spine carriageway + split; recorded amendment in the FROZEN marker). LANE_A_NOTES §R15 hands B/F the new golden + a + corner-venue eyeball note. + +Convention settled: the visible venue face is `+Z` (`buildings.js` facade `zf=+d/2`; `venue.js` door/queue +`+Z`). The selfcheck's `facing=(−sin,−cos)` "faces frontEdge" is the sim/GLB convention, 180° off the +visual front — the same π-ambiguity D fixed for the fleet in R13. + ## Round 14 (2026-07-16) — v3.0 RELEASE: invariant sweep + CITY_SPEC §v3 freeze Release round, Lane A scope = harden + freeze (no new systems). **No golden moved** (synthetic diff --git a/docs/CITY_SPEC.md b/docs/CITY_SPEC.md index 221af70..6393599 100644 --- a/docs/CITY_SPEC.md +++ b/docs/CITY_SPEC.md @@ -213,6 +213,11 @@ night in v1; interior-mapping shader is a stretch goal). > never silent drift. **One edit is still pending and lands with Lane F's alias-retirement commit** (R13 > debt #1): the R12 single-venue compat alias in the runtime block below retires; everything else here is > final for the `v3.0` tag. +> +> *(**Recorded amendment — ROUND15, ledger #1:** the frontage poster was flipped to the `+Z` street +> facade (it had been on the `−Z` back, invisible from the street); this moved `plan.posters`, so the gig +> golden re-pinned `0x1f636349 → 0x4f4a549d` in the same commit under the amendment law above. The +> synthetic golden `0x3fa36874` did not move — this is the round's one sanctioned gig-golden move.)* > **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 — @@ -279,11 +284,14 @@ gigs: [ { gigId, venueShopId, bandName, genreKey, night, startSeg, endSeg, cover **`plan.posters`** — town-wide gig advertising (Lane B renders): `posters: [ { id, gigId, x, z, ry } ]`. Only **tonight's** (night 0) playing venues are advertised. Each such venue gets one poster on **its own -frontage** (seated on the facade plane, `lot.x + front·(d/2+ε)`), plus a seeded run along the spine — 2 -per main edge, at `t ∈ [0.2,0.8]` of the block, on **either verge**. Different venues' posters share the -same pole runs — that IS the district reading. `ry` follows the one house convention (front/outward -normal = local `−Z` at `ry=0`; `ry = atan2(nx, nz)`), so a poster faces the footpath that reads it. B -places E's poster skin at `(x, z)` rotated `ry`; F wires "poster → that venue's tonight-gig". +frontage** — seated on the **+Z street facade** (B's canon: `buildings.js` facade + `venue.js` door/queue/ +camera are all `+Z`; position `lot + (sin ry, cos ry)·(d/2+ε)`), turned to read *from* the street +(`ry = lot.ry + π` so its printed face points `+Z` at the queue-side camera — **ROUND15 ledger #1**; before +R15 it was on the `−Z` back and invisible from the street). Plus a seeded run along the spine — 2 per main +edge, at `t ∈ [0.2,0.8]` of the block, on **either verge**. Different venues' posters share the same pole +runs — that IS the district reading. `ry` follows the house convention (a poster's printed face = +`(−sin ry, −cos ry)`, so `atan2` of the *away-from-viewer* normal); B places E's poster skin at `(x, z)` +rotated `ry`; F wires "poster → that venue's tonight-gig". **The street-corridor law (ROUND13 debt #5 — published as functions, not plan data).** `edge.width` is the **full corridor** (carriageway + verge/footpath, centreline ± width/2), never the carriageway alone — @@ -292,8 +300,11 @@ pure functions over an edge (no plan fields change ⇒ base goldens stay frozen) barrel: **`roadWidth(edge)`** (full carriageway width — main 10 m, side 6–8, lane = all, arcade 0; the kerb sits at `roadWidth/2` from the centreline), **`vergeBand(edge) → [inner,outer]`** (where footpath furniture lives), **`poleOffset(edge)`** (where a -pole/poster seats — just behind the kerb). Posters, poles, verandah posts belong in the verge band; the -selfcheck asserts no poster stands within `roadWidth/2` of any edge centreline. +pole/poster seats — just behind the kerb). Poles, verandah posts and **spine** posters belong in the verge +band; the selfcheck asserts no **spine** poster stands within `roadWidth/2` of any edge centreline. A +**frontage** poster is exempt — it is bound to its venue building (the +Z facade B draws), so at a corner +venue whose facade abuts a crossing street it may legitimately sit near that street's kerb (B's building +geometry, not a mid-road defect). **Band names + the drop-in.** `bandName(seed)` generates 90s-Aussie pub-rock names from wordlists. `web/assets/custom_bands.json` is an **OPTIONAL** editable drop-in (schema `{ "bands": ["The Feral @@ -306,8 +317,9 @@ by seed, spread **night-major across the whole week's slots town-wide** (not one canonical half-open predicate (`open ≤ hour < close`) for D's gig-night patronage surge and everyone else. **Gig golden** (guards gig-layer drift; pure-generator path, no custom file), seed `20261990` synthetic: -**`0x1f636349`** (ROUND13 re-pin — the district changed gig-plan output; was `0xa6ae5a5e`, the one-pub -alpha). Asserted by `selfcheck.js` alongside the frozen v2 goldens; changing gig output re-pins it. +**`0x4f4a549d`** (**ROUND15 re-pin** — the frontage-poster +Z flip, ledger #1, moved `plan.posters`; was +`0x1f636349` the R13 district, `0xa6ae5a5e` the R12 alpha). Asserted by `selfcheck.js` alongside the frozen +v2 goldens; changing gig output re-pins it. ## Shop types v1 (registry lives in `web/js/core/registry.js`, Lane A writes it, all lanes read) diff --git a/docs/LANES/LANE_A_NOTES.md b/docs/LANES/LANE_A_NOTES.md index 157edf2..f275364 100644 --- a/docs/LANES/LANE_A_NOTES.md +++ b/docs/LANES/LANE_A_NOTES.md @@ -2,6 +2,36 @@ Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`. +## Round 15 (2026-07-16) — the frontage-poster +Z flip (ledger #1) + the one sanctioned golden move + +### → B + F: **new gig golden `0x1f636349 → 0x4f4a549d`** (synthetic `0x3fa36874` unchanged) +The frontage-poster fix moved `plan.posters`, so the gig golden re-pinned — the round's ONE sanctioned +move (amendment law; §v3 marker records it). B: re-shoot `queue_night` against this. F: the smoke's gig +golden is now `0x4f4a549d`. + +### The fix (gigs.js buildPosters item 1) +Each venue's own-frontage poster sat at local **−Z** — the *back* of the building (~16 m behind, matching +B's 15.9 m), invisible from the street, which is why `queue_night` framed no poster. Flipped to the **+Z +street facade** AND turned it to read from the street: `ry = lot.ry + π` so its printed face points `+Z` +at the queue-side camera (else `DoubleSide` shows a mirrored band name). Proven: pub + rsl frontage posters +now sit **0.06 m from B's door**, printed-face·(+Z street) = **1.000**. Only item 1; spine posters untouched. + +### The −Z/+Z convention, settled (worth recording — it's the R13 RY_FLIP π-ambiguity again) +The **visible** venue face is `+Z`: `buildings.js` builds the facade/door at `zf = +d/2` ("front face at +local +Z"), and `venue.js` seats the door-glow/queue/poster-camera on `(sin ry, cos ry)` = `+Z`. The +selfcheck's `facing = (−sin ry,−cos ry)` "every lot faces its frontEdge" is the **sim/GLB** convention, +180° off from the visual front (same ambiguity D fixed at the source for the fleet in R13). A poster faces +the *viewer* when its printed face = `(−sin ry,−cos ry)` points at them, i.e. `ry = atan2(away-from-viewer)`. + +### → B + F (eyeball in the tour): corner-venue frontage posters can sit near a crossing street +The `+Z` facade is the block-interior side in *plan* coordinates, so for a **corner venue** (a band_room/ +rsl fronting one street with another behind — e.g. seed 1 band_room lot#481, 1.58 m from a side-street +centreline) the frontage poster lands near that crossing street's kerb. That's B's building geometry (the +door is there too), not a mid-road defect — so the selfcheck's no-bitumen assert now **exempts frontage +posters** (they're building-bound) and keeps it for free-standing **spine** posters. The pub hero is clean +(29.9 m of open ground on `+Z`). If a corner poster reads as floating over a road in the tour, that's a +v3.1 venue-placement tweak, not a v3.0 blocker. + ## Round 14 (2026-07-16) — v3.0 RELEASE: invariant sweep + CITY_SPEC §v3 frozen Release round — no new systems, no golden moves. **Synthetic `0x3fa36874` and gig `0x1f636349` are diff --git a/web/js/citygen/gigs.js b/web/js/citygen/gigs.js index 900d39d..77d842e 100644 --- a/web/js/citygen/gigs.js +++ b/web/js/citygen/gigs.js @@ -202,9 +202,14 @@ function buildPosters(plan, citySeed, venues, gigs) { const gig = tonight.get(v.shop.id); const lot = gig && lotById.get(v.shop.lot); if (!lot) continue; - const fx = -Math.sin(lot.ry), fz = -Math.cos(lot.ry); // house convention: front = local −Z + // ROUND15 (ledger #1): B's buildings put the street facade on local +Z (frontage/queue seat on + // (sin ry, cos ry)); this once used −Z, so the poster sat ~16 m BEHIND the building, invisible from + // the street — it's why queue_night framed no poster. Seat it on the +Z street face, AND turn it to + // read FROM the street: venue.js renders a poster's printed face along (−sin ry, −cos ry) = A's + // outward normal, so ry = lot.ry + π points that face outward (else DoubleSide shows a mirrored name). + const fx = Math.sin(lot.ry), fz = Math.cos(lot.ry); // +Z: the street facade (B's canon) const off = lot.d / 2 + 0.06; // out onto the facade plane, proud of it - add(lot.x + fx * off, lot.z + fz * off, lot.ry, gig.gigId); + add(lot.x + fx * off, lot.z + fz * off, lot.ry + Math.PI, gig.gigId); } // 2. the spine run — seeded pole positions along main edges, on the verge, facing the footpath. diff --git a/web/js/citygen/selfcheck.js b/web/js/citygen/selfcheck.js index 9765aa0..485d3a7 100644 --- a/web/js/citygen/selfcheck.js +++ b/web/js/citygen/selfcheck.js @@ -194,9 +194,21 @@ function districtInvariants(plan, label) { const gigIds = new Set(plan.gigs.map(g => g.gigId)); ok(Array.isArray(plan.posters) && plan.posters.every(p => gigIds.has(p.gigId) && isFiniteNum(p.x) && isFiniteNum(p.z) && isFiniteNum(p.ry)), `${label}: posters reference a real gig with finite coords`); ok(plan.posters.every(p => plan.gigs.find(g => g.gigId === p.gigId).night === 0), `${label}: posters advertise tonight's gigs`); - // every poster clears its nearest edge's carriageway — the R12 bug was posters mid-road on nodes. + // The no-bitumen rule (the R12 regression: spine posters at node.x+2) is for FREE-STANDING verge + // posters. A venue's own FRONTAGE poster is bound to the building B draws — since R15 it sits on the + // +Z street facade (B's canon: buildings.js facade + venue.js door/queue/camera are all +Z). A corner + // venue whose facade abuts a crossing street legitimately seats its poster near that street's kerb — + // B's building geometry, not a mid-road defect — so frontage posters are exempt here and validated by + // the placement asserts above (they reference tonight's gigs with finite coords). + const lotByIdD = new Map(plan.lots.map(l => [l.id, l])); + const onFacade = p => venues.some(v => { + const lot = lotByIdD.get(v.lot); if (!lot) return false; + const off = lot.d / 2 + 0.06; // gigs.js buildPosters item 1 + return Math.hypot(p.x - (lot.x + Math.sin(lot.ry) * off), p.z - (lot.z + Math.cos(lot.ry) * off)) < 0.5; + }); let onRoad = null; for (const p of plan.posters) { + if (onFacade(p)) continue; // frontage poster: on the venue building, not the verge let best = Infinity, bestEdge = null; for (const e of plan.streets.edges) { const a = nodeById2(plan, e.a), b = nodeById2(plan, e.b); @@ -295,7 +307,7 @@ ok(JSON.stringify(generatePlanFor(42, 'synthetic', { gigs: true })) === JSON.str ok(JSON.stringify(generatePlanFor(20261990, 'synthetic', { gigs: true, customBands: custom })) === JSON.stringify(pc), `custom bands: deterministic given (seed, list)`); } // gig-layer golden (guards gig-layer output drift; pure-generator path, no custom file) -const GIG_GOLDEN = 0x1f636349; // ROUND13 re-pin: multi-venue district changed the gig-plan output (was 0xa6ae5a5e, the one-pub alpha) +const GIG_GOLDEN = 0x4f4a549d; // ROUND15 re-pin: frontage posters flipped to the +Z street face (ledger #1 bug fix). Was 0x1f636349 (R13 district), 0xa6ae5a5e (R12 alpha). { const hash = xmur3(JSON.stringify(generatePlanFor(20261990, 'synthetic', { gigs: true })))() >>> 0; if (GIG_GOLDEN === 0) console.log(` ⚠ gig golden UNPINNED → set GIG_GOLDEN = 0x${hash.toString(16).padStart(8, '0')}`);