Lane A R39 39.1 (close): map.html says the street name out loud; CITY_SPEC + A-progress §39
map.html (Lane A's reference tool) can now load E's REAL town caches — until this round it could only render the three checked-in fixtures, so it could never show a real street. Adds the address line to the panel and `label · side` to the shop tooltip. Verified in-browser (no-store server, both suppliers, ONE line of code, no town-type branch): · ?plansrc=osm&town=castlemaine_real → "address: ways · 841/858 edges named · 22/22 shops · 141 names"; tooltip "XXXX Antiques — Elizabeth Street · south side", "Maxi IGA — Forest Street · north side". Matches the Node measurement shop-for-shop. · default synthetic → "address: district · 493/493 shops labelled by district"; tooltip "Nev's Pawn & Loan — the warehouse fringe · north side". CITY_SPEC: the address layer documented as DERIVED data, with the schema rule that follows from it — no street name, district label or address ever becomes a CityPlan field. `plan_osm` still discards `rd.name` deliberately; the names are recovered downstream because plan nodes ARE snapped projected way points. The only thing the lift publishes is `norm.shift`, into opts.report, never the plan. A-progress §39: the round, the two departures from the synthesis with their numbers, the gate table with the breakage that fires each arm, and the tolerance finding (correctness saturates at 6 m). selfcheck 157,647/157,647 ALL GREEN · fingerprint 0x5f76e76 unchanged · zero re-pins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a569f08b14
commit
34f7dae397
@ -1,11 +1,90 @@
|
||||
# LANE A — CITYGEN · progress (PROCITY-A)
|
||||
|
||||
*Status: **all deliverables landed; self-check ALL GREEN (157,407/157,407); rounds 2–30 + 37 closed**.
|
||||
*Status: **all deliverables landed; self-check ALL GREEN (157,647/157,647); rounds 2–30 + 37 + 39 closed**.
|
||||
R39 (v9 item 39.1): THE ADDRESS LAYER — 17,835 named ways stop being dropped on the floor. New pure
|
||||
module `address.js`; **1,192/1,219 corpus shops (97.8%) resolve to a real street name**, 0 wrong names
|
||||
on 884 shop-bearing edges against an independent resolver, **all goldens frozen, zero re-pins.**
|
||||
R37 (v8 wave 0 §0.1): the poster-clearance gate was VACUOUS — it validated my own placer against my own
|
||||
law and stayed green while all 14 posters on the default boot stood on B's bitumen. Corrected to three
|
||||
arms with a permanent control; proven to fail on the pre-ruling geometry where the old gate cannot.
|
||||
All goldens frozen, zero re-pins.*
|
||||
|
||||
## Round 39 (2026-08-03) — v9 item 39.1: THE ADDRESS LAYER (the approved thing; B and C consume it)
|
||||
|
||||
**The full contract, per-town table and reasoning live in `LANE_A_NOTES.md §39`** — published in the
|
||||
first commit, because B (39.2 the fog) and C (39.3 the op-shop dig) build against it in parallel.
|
||||
|
||||
**Fable's three binding facts, re-derived independently — all three land exactly.** 19,132 road ways,
|
||||
**17,835 named (93.2%)**; median plan-edge-sample → nearest named way **0.03–0.90 m** (worst katoomba);
|
||||
**1,192 / 1,219 corpus shops (97.8%)** resolve. Nothing to contradict.
|
||||
|
||||
**What landed.** `web/js/citygen/address.js` — pure, **zero imports**, no THREE/fetch/DOM/module state,
|
||||
no plan mutation. `createAddresses(plan, cache|null)` → `streetOf` · `localityOf` · `cohort` ·
|
||||
`streets` · `stats`. Two suppliers, one field: a cache town fills `label` from real named ways, the
|
||||
synthetic from `district.kind` + block. Plus `getTownCache(key)` (additive, read-only) because
|
||||
`index.html:129` registers the cache and drops its reference, so `roads[]` was unreachable downstream.
|
||||
`map.html` now loads real caches and prints the address — verified in-browser on castlemaine (*"XXXX
|
||||
Antiques — Elizabeth Street · south side"*) and on the synthetic (*"Nev's Pawn & Loan — the warehouse
|
||||
fringe · north side"*) **through the identical line of code**.
|
||||
|
||||
**Two departures from the synthesis, both measured.**
|
||||
|
||||
1. **The shift is recovered EXACTLY, not voted for.** I implemented the proposed centroid-align +
|
||||
nearest-waypoint vote first: it returns the **WRONG shift on 5 of 23 towns** (braddon, fremantle,
|
||||
hobart, northbridge, westend) — island culling drags the plan centroid off the cache's and the vote
|
||||
converges *confidently* on a wrong offset. A wrong shift renames every street in the town. Replaced
|
||||
with a lattice intersection (node − waypoint candidates, filtered by further nodes): **exact on
|
||||
23/23, nothing to tune, returns null rather than a guess.**
|
||||
2. **Five samples along an edge, not the midpoint.** A midpoint cannot distinguish a street from the
|
||||
street that crosses it — at an intersection both are ~0 m away.
|
||||
|
||||
**The one-line upgrade: TAKEN, and it is genuinely that clean.** `norm.shift = {shx, shz}` goes onto the
|
||||
normalization log, reaching the caller only via the existing `opts.report` sink — never the plan object.
|
||||
It is a **cross-check, not a dependency**: the selfcheck compares plan_osm's published shift against
|
||||
address.js's independent recovery and fires on a **0.01 m** disagreement. Without it the recovery would
|
||||
only be checkable against itself, which is the exact vacuous-gate species R37 was about.
|
||||
|
||||
**The gates (+240 checks), each proven to fire on a broken world.**
|
||||
|
||||
| arm | today | breakage that fires it |
|
||||
|---|---|---|
|
||||
| 0 wrong names on shop-bearing edges, vs an **independent way-membership resolver** | **857/857 agree, 0 wrong** | resolver or shift drift |
|
||||
| corpus resolution ≥ 97% | 1,192/1,219 (97.8%) | names stripped → 0.0% |
|
||||
| shift recovered == plan_osm's published shift | exact, 23/23 | a **0.01 m** error |
|
||||
| `createAddresses` does not mutate the plan | byte-identical per town | any write to `plan` |
|
||||
| CONTROL: `roads[].name` stripped (**the pre-change state**) | 0 streets, supplier `district` | — |
|
||||
| CONTROL: no cache passed | supplier `district`, visible in `stats()` | — |
|
||||
| all-edge disagreements **PINNED at 4** (not `> 0`) | 4/30,986 | a control drifting to zero |
|
||||
|
||||
The 4 corpus disagreements are all 2D-stacked ways where the question has no single answer (geelong's
|
||||
two adjacent malls; a motorway crossing *over* a street on glebe and redhill ×2) and **none carries a
|
||||
shop** — which is why the shop-bearing arm is 0.
|
||||
|
||||
**The tolerance finding, which is the one worth keeping.** Coverage rises with tolerance (2 m → 85.2%,
|
||||
6 m → 97.4%, 8 m → 97.8%, 12 m → 99.1%) but **correctness saturates at 6 m**: 857 agreeing frontage
|
||||
names at 6, 8, 12, 16 *and* 24 m. Every metre past 6 buys exactly one thing — a name attached to a way
|
||||
OSM left unnamed (0 at 6 m, 4 at 8 m, 14 at 24 m). **It can never buy a correction.** Shipped at the
|
||||
briefed 8 m, which keeps four borrowings, all arcade footways inside a named mall.
|
||||
|
||||
**And the resolver degrades to null, not to a wrong name.** Feed it a shift wrong by 30 m and
|
||||
castlemaine/katoomba/adelaide return **zero** street names rather than a town full of wrong ones. Feed
|
||||
it one wrong by 3 m and adelaide's coverage *rises* from 89 to 98 shops while being wrong — which is
|
||||
why the shift arm is exact-or-nothing and **coverage is never used as a proxy for correctness.**
|
||||
|
||||
**The failure modes, named.** All 27 unresolved shops front a way OSM genuinely leaves unnamed:
|
||||
adelaide 21 `arcade` (its arcades are the town's retail spine and OSM leaves the footways unnamed — it
|
||||
is the corpus worst at 80.9% and it is *honestly* worst), fitzroy 1, geelong 1, launceston 1 + 2 `lane`,
|
||||
northbridge 1. **`null` is the honest answer and the module returns it**; a shop with `street: null`
|
||||
carries `label: null` too, asserted per town.
|
||||
|
||||
**The synthetic's honest limit:** 12 label cells over 493 shops, median 41, max 147. Coarse — the same
|
||||
structural fact §8 of the charter records. `block` is the finer cell (32 shop-bearing blocks, median 14).
|
||||
|
||||
**GOLDENS: NOTHING MOVED, ZERO RE-PINS.** synthetic `0x5f76e76`, gig `0xec7a2d39`, 3 osm fixtures, 23
|
||||
`REAL_TOWN_GOLDENS`, 23 `REAL_TOWN_GIG_GOLDENS` — all untouched. Selfcheck **157,407 → 157,647 ALL
|
||||
GREEN**. `createAddresses` costs **≤27 ms** on the worst real town (bendigo), **0.3 ms** on the
|
||||
synthetic, once at plan time; zero draws, zero triangles, zero plan fields.
|
||||
|
||||
## Round 37 (2026-08-03) — v8 WAVE 0 §0.1: THE KERB (ruling-side) — the gate I owned was the vacuous one
|
||||
|
||||
Full detail in LANE_A_NOTES §37. Four jobs, all measured before they were written.
|
||||
|
||||
@ -572,6 +572,34 @@ nothing outside the cache-schema path).
|
||||
> count beats teleport.** The frozen set is unmoved by the entire epoch: synthetic `0x3fa36874`, gig
|
||||
> `0xb1d48ea1`, marched fixtures, classic.
|
||||
|
||||
### The address layer (v9, ROUND39 item 39.1) — DERIVED, never stored on the plan
|
||||
|
||||
`roads[].name` has always ridden the v2 cache (**17,835 of 19,132 ways carry one — 93.2%**) and
|
||||
`plan_osm.buildRealRoads` has always dropped it: `rawWays.push({ kind, pl })` keeps the kind and the
|
||||
geometry and discards the name. **It still does, deliberately.** Street names are recovered
|
||||
*downstream* by `web/js/citygen/address.js` — a pure module (zero imports) that reads a plan plus its
|
||||
cache and returns a query object:
|
||||
|
||||
```js
|
||||
createAddresses(plan, cache|null) → streetOf(edgeId) · localityOf(shopId) · cohort(pred) · streets() · stats()
|
||||
```
|
||||
|
||||
**THE RULE, and it is a schema rule, not a style note: no street name, district label or address ever
|
||||
becomes a CityPlan field.** The plan is the frozen producer contract; addressing is derived data with a
|
||||
97.8% resolution rate and a 2.2% honest-`null` tail, and baking either into `plan.lots`/`plan.shops`
|
||||
would move all 46 pinned goldens to store something a consumer can compute in ≤27 ms. It is recoverable
|
||||
because plan nodes ARE snapped projected way points (`SNAP=3`), so a plan edge lies **on** its way —
|
||||
median sample-to-way distance **0.03–0.90 m** across the 23 caches.
|
||||
|
||||
The one thing the lift now publishes is its own centring translation, `norm.shift = {shx, shz}`, which
|
||||
reaches the caller **only through the existing `opts.report` sink and never through the plan object** —
|
||||
so `JSON.stringify(plan)` is byte-identical and every golden is frozen by construction. It exists as a
|
||||
**cross-check**: `address.js` re-derives the same shift independently, and the selfcheck compares the
|
||||
two. Full contract, per-town rates and failure modes in `LANE_A_NOTES.md §39`.
|
||||
|
||||
**Consumers must not branch on town type.** `localityOf(shopId).label` is a real street name on a cache
|
||||
town and a district phrase (*"the market end"*) on the synthetic — one field, two suppliers, one contract.
|
||||
|
||||
## The stock ladder (v5 — THE REAL SHOP; frozen at `v5.0`, ROUND27 ledger #6)
|
||||
|
||||
> **v4 made the towns real; v5 makes the shops real.** The map-level contract above is unchanged by this
|
||||
|
||||
26
web/map.html
26
web/map.html
@ -86,7 +86,7 @@
|
||||
<!-- No importmap / no THREE: this debug page is pure Canvas 2D (CITY_SPEC allows the map page to
|
||||
use THREE, but Lane A's 2D map doesn't need it). It imports only Lane A's pure-data modules. -->
|
||||
<script type="module">
|
||||
import { generatePlanFor, chunkIndex, CHUNK } from './js/citygen/index.js';
|
||||
import { generatePlanFor, chunkIndex, CHUNK, createAddresses, registerTownCache, getTownCache } from './js/citygen/index.js';
|
||||
// plan source: default 'synthetic'; ?plansrc=osm renders a real-data fixture town, ?town=<key> picks it.
|
||||
const _q = new URLSearchParams(location.search);
|
||||
const PLANSRC = _q.get('plansrc') === 'osm' ? 'osm' : 'synthetic';
|
||||
@ -112,6 +112,7 @@ const cv = document.getElementById('c'), ctx = cv.getContext('2d');
|
||||
let plan = null, idx = null, DPR = Math.min(devicePixelRatio||1, 2);
|
||||
const view = { x:0, z:0, scale:0.6 }; // world→screen: screen = (world - viewcenter)*scale + canvascenter
|
||||
let hover = null;
|
||||
let addr = null; // ROUND39 39.1 — the address layer for the current plan (rebuilt in regen)
|
||||
|
||||
const $ = id => document.getElementById(id);
|
||||
const cssPx = () => ({ w: cv.clientWidth, h: cv.clientHeight });
|
||||
@ -290,8 +291,12 @@ addEventListener('mousemove', e=>{
|
||||
cv.style.cursor='pointer';
|
||||
const l=hover.lot, s=hover.shop;
|
||||
tip.style.display='block'; tip.style.left=sx+'px'; tip.style.top=sy+'px';
|
||||
// ONE consumer contract: print `label`. It is the street name on a real town and the district
|
||||
// phrase on the synthetic, and this line does not know or care which.
|
||||
const loc = s ? addr.localityOf(s.id) : null;
|
||||
const where = loc && loc.label ? `<div class="s" style="color:var(--accent)">${esc(loc.label)}${loc.side?` · ${loc.side} side`:''}</div>` : '';
|
||||
tip.innerHTML = s
|
||||
? `<div class="t">${esc(s.name)}</div><div class="s">${s.type} · ${SHOP_TYPES[s.type]?.label||''} · ${s.storeys}fl · ${pad(s.hours[0])}–${pad(s.hours[1])}</div><div class="s">${s.facadeSkin}</div>`
|
||||
? `<div class="t">${esc(s.name)}</div>${where}<div class="s">${s.type} · ${SHOP_TYPES[s.type]?.label||''} · ${s.storeys}fl · ${pad(s.hours[0])}–${pad(s.hours[1])}</div><div class="s">${s.facadeSkin}</div>`
|
||||
: `<div class="t">${l.use} lot</div><div class="s">${l.w}×${l.d}m · block ${l.block}</div>`;
|
||||
scheduleDraw();
|
||||
} else { hideTip(); }
|
||||
@ -313,6 +318,10 @@ const pad=h=>String(h).padStart(2,'0')+':00';
|
||||
function regen(seed){
|
||||
const s = (seed>>>0);
|
||||
plan = generatePlanFor(s, PLANSRC, { town: TOWN, gigs: GIGS, customBands }); idx = chunkIndex(plan); _sbl = null; // rebuild the cache for the new plan
|
||||
// ROUND39 39.1 — THE ADDRESS LAYER on the reference map. Same two-arg call every consumer makes,
|
||||
// and NO branch on town type: a registered real cache supplies street names, everything else
|
||||
// supplies district labels, and the tooltip prints `label` either way.
|
||||
addr = createAddresses(plan, TOWN ? getTownCache(TOWN) : null);
|
||||
$('seed').value = s;
|
||||
$('name').innerHTML = `<b style="color:var(--accent);font-size:14px">${esc(plan.name)}</b>`;
|
||||
const nShop=plan.shops.length, byType={};
|
||||
@ -320,6 +329,11 @@ function regen(seed){
|
||||
$('stats').innerHTML =
|
||||
`<b>${plan.streets.edges.length}</b> streets · <b>${plan.blocks.length}</b> blocks<br>`+
|
||||
`<b>${plan.lots.length}</b> lots · <b>${nShop}</b> shops · <b>${Object.keys(idx.chunks).length}</b> chunks`;
|
||||
{ const a = addr.stats();
|
||||
$('stats').innerHTML += `<br><span class="muted">address: <b>${a.supplier}</b> · `+
|
||||
(a.supplier === 'ways'
|
||||
? `${a.edgesNamed}/${a.edges} edges named · <b>${a.shopsWithStreet}/${a.shops}</b> shops · ${a.distinctStreets} names`
|
||||
: `${a.shopsLabelled}/${a.shops} shops labelled by district`)+`</span>`; }
|
||||
if (GIGS && plan.gigs && plan.gigs.length){
|
||||
const venue=plan.shops.find(s=>s.venue), g0=plan.gigs[0];
|
||||
$('stats').innerHTML += `<br><b style="color:#ffd23f">♪ ${esc(venue?.name||'venue')}</b><br>`+
|
||||
@ -362,6 +376,14 @@ requestAnimationFrame(async ()=>{
|
||||
resize();
|
||||
// OPTIONAL band drop-in: load John's names for the debug view (graceful if the file is absent).
|
||||
if (GIGS){ try { const r=await fetch('./assets/custom_bands.json'); if(r.ok){ const j=await r.json(); if(Array.isArray(j.bands)) customBands=j.bands; } } catch(e){} }
|
||||
// ROUND39 39.1 — load E's REAL town cache when one exists for ?town=<key>, exactly the way the
|
||||
// shell does (web/index.html:125-129). Until now this page could only render the three checked-in
|
||||
// fixtures, so it could never show a real street. Graceful: a missing cache falls straight through
|
||||
// to the fixture path, which is what melbourne/katoomba/silverton do.
|
||||
if (PLANSRC === 'osm' && TOWN){
|
||||
try { const r = await fetch(`./assets/towns/${TOWN}.json`);
|
||||
if (r.ok) registerTownCache(TOWN, await r.json()); } catch(e){}
|
||||
}
|
||||
regen(bootSeed);
|
||||
});
|
||||
</script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user