Lane E R24 (v5.0-alpha): the crate's real street, the honest gate, and Ballarat retired
THE STREET (ledger #3). redhill_real: Red Hill, Brisbane — 36 shops (9 heroes + 27 texture, -29 subsampled), 884 roads, median NN 73 m. **Musgrave Road is in roads[] (41 ways, primary + residential)** — that was the point: Monster Robot Party's real shop now has its real street. One bounded Overpass fetch under the standing authorization; raw snapshots in _raw/, ODbL, C's mapping. This is G's donor for godverse_town.py -> redhill_godverse. TWO HAZARDS HANDED TO G/A, both measured against the real geometry (notes R24 §1): (a) The shop is NOT in OSM — no shop=music node, no name match anywhere in the bbox (checked the raw snapshot, not the emitted cache). It cannot arrive via the fetch; G must inject id 3962749 from the census or the town ships without the shop the release is about. (b) THE SEATING HAZARD: A's lift seats on the NEAREST edge, and from G's geocode three streets are nearer than the addressed one — Zig Zag Street 38.6 m (residential), Craig Street 46.1 m, Musgrave Road 51.6 m. The shop would seat on Zig Zag St and the headline would quietly become false. Measured fix (G's call): the point on the real Musgrave centreline nearest G's geocode is -27.455169, 153.006902 — 0 m to Musgrave, next candidate 43 m. Snaps the street, does NOT verify the house number; I have no address data for where #147 sits along the road. THE HONEST GATE (ledger #4). F's catch was right and there were FOUR bugs: the glob used the published-contract name the runtime can never load (0 matches -> returned 0: it passed identically whether atlases were absent, misnamed, or perfect); the code read provenance top-level while my own docstring said nested (G nested it correctly — the gate would have failed G's real artifact); licence/license; and a stale ceiling. Rebuilt on the "runtime is the authority" ruling: globs stock_godverse/<id>/stock_<type>_index.json, type in {record,book,toy}. Vacuous-gate law implemented literally — 0 matches + stray files FAIL (listing them); 0 matches + empty dir prints "atlas-QA SKIP: no atlases present (0 files matched glob ...)"; plus an id equality (shop.godverse_id == dir) so it proves it touched its subject. Proved on four cases: empty -> loud SKIP, misnamed (F's exact bug) -> FAIL, unparseable -> FAIL, G's real atlas -> PASS (120 items, 2 atlases, licence printed). My own test found a fifth hole — the glob still matched the old stock_shop_* name, so the misnamed case failed only incidentally; added a STOCK_TYPES name check so it fails for the RIGHT reason. A gate that fails for the wrong reason will pass for the wrong reason later. BALLARAT — RETIRED, counted, and a DIFFERENT illness from Toowoomba (notes R24 §3). Toowoomba was dead everywhere. Ballarat is alive exactly where the game isn't: 103 POIs, densest 400 m disc = 47 of them (Toowoomba's was 3) — but that disc is cafes/clothes/bakeries while the op-shops sit 854 m, 1042 m and 1709 m out. Heroes and density don't coincide, so no bbox holds both. Real builds at every span: 1.0 km -> 8 shops/184 m/hub 4-8 but ZERO op-shops (a thrift town with no thrift shop); 2.0 km -> 12/186 m/3-12; 3.0 km as shipped -> 20/255 m/4-20. Every row worse than any shipped town. THE BINDING CONSTRAINT WAS MINE, NOT OSM'S: with 2 heroes the 3x texture cap keeps 6 of 47 texture POIs and the subsample is spatially blind, so survivors scatter at any span — part of the 255 m A's warn caught was my own cap's fingerprint. Seeding texture near heroes fixes the class but moves every town's golden -> filed as a v5.x proposal, not done in a release round. I OVERSTATED MY R23 ADVICE TO A: the hub fraction is NOT a standalone discriminator — glebe (shipped, healthy) reads 13% and redhill 14%; a hub threshold would false-flag both. Median NN is the robust primary; hub is only comparable at equal shop counts (toowoomba 2/12 vs darwin 7/12 was fair because both had 12). The raw-vs-seated calibration point stands. METHODOLOGY, recorded because it nearly shipped: I picked Ballarat's span from an offline simulation that predicted 68 m / hub 5-8, and had already written those numbers into a config comment as fact. The real build returned 184 m / 4-8 — same counts, different sample. Never trust a re-implementation of the pipeline's own selection; run the pipeline. -> A (batched pins): ADD REAL_TOWN_GOLDENS['redhill_real']=0x6046700f and REAL_TOWN_GIG_GOLDENS['redhill_real']=0x673785ee (the headline town currently only WARNS as UNPINNED — ungated until you pin it). REMOVE ballarat_real 0xf0a9e8a1 / 0xf097c331 — dead config by your own R23 note. -> F: tools/qa/tour_shots.py still shoots town=toowoomba_real, which I deleted in R23 — the v4 tour breaks on a missing town. Your file, your call. A retirement isn't done when the cache is deleted; it's done when the consumers are swept. -> C: built to the runtime + G's real artifact per the ruling; LANE_C_PUB §7 line 271 still names the contradictory stock_shop_<id>_index.json. I reconcile when your corrected contract lands. Determinism receipt: the retirement forced a full 21-town rebuild from cached raw (no refetches) and every existing cache came back byte-identical — only ballarat, redhill and index.json moved. Pack: 21 real towns · 1196 shops · 299 heroes (22 rostered with G's newtown_godverse). Selfcheck ALL GREEN 149,696, synthetic golden 0x3fa36874 unchanged. qa.sh --strict --matrix: 7 passed / 0 failed, MATRIX GREEN 10x7. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1eb0b45bd0
commit
5815dfbd58
@ -1,5 +1,146 @@
|
|||||||
# LANE E — cross-lane notes
|
# LANE E — cross-lane notes
|
||||||
|
|
||||||
|
## Round 24 — the alpha completion: redhill, the honest validator, ballarat ⟨v5.0-alpha⟩
|
||||||
|
|
||||||
|
**1. `redhill_real` — the crate's real street (ledger #3). BUILT.** One bounded Overpass fetch under the
|
||||||
|
standing authorization. **Red Hill, Brisbane** · centre `-27.4553, 153.0064` · span 2.4 km · **36 shops
|
||||||
|
(9 heroes + 27 texture, −29 subsampled) · 884 roads** · median NN 73 m, hub 5/36. **Musgrave Road is in
|
||||||
|
`roads[]` — 41 ways, kinds `[primary, residential]`.** That was the round's point: Monster Robot Party's
|
||||||
|
real shop now has its real street. Raw snapshots in `_raw/`, ODbL, C's mapping, same discipline as the
|
||||||
|
pack. **→ Lane G: this is your donor** for `godverse_town.py redhill_real redhill_godverse`.
|
||||||
|
|
||||||
|
**→ G AND A: TWO HAZARDS ON THIS DONOR. Please read before you seat the shop.**
|
||||||
|
|
||||||
|
> **(a) The shop is NOT in OSM.** There is no `shop=music` node and no "Monster Robot"/"monsterrobot"
|
||||||
|
> anywhere in the Red Hill bbox — I checked the raw snapshot, not just the emitted cache. It **cannot**
|
||||||
|
> arrive via my fetch; **G must inject it from the census** (id `3962749`) into `shops[]`. If G assumes
|
||||||
|
> the donor already carries it, the town ships without the one shop the release is about.
|
||||||
|
>
|
||||||
|
> **(b) THE SEATING HAZARD — from G's geocode, the shop seats on the WRONG STREET.** A's lift seats each
|
||||||
|
> shop on its **nearest edge**. Measured against the real `roads[]` geometry, from G's geocode
|
||||||
|
> `-27.4553, 153.0064`:
|
||||||
|
>
|
||||||
|
> | distance | street | kind |
|
||||||
|
> |---|---|---|
|
||||||
|
> | **38.6 m** | **Zig Zag Street** | residential ← **A would seat it HERE** |
|
||||||
|
> | 46.1 m | Craig Street | residential |
|
||||||
|
> | 51.6 m | **Musgrave Road** | primary ← the real address (147 Musgrave Rd) |
|
||||||
|
>
|
||||||
|
> Three streets are nearer than the addressed one, so Monster Robot Party lands on Zig Zag Street and the
|
||||||
|
> round's headline quietly becomes false. A street-address geocode sitting 52 m off its own centreline is
|
||||||
|
> itself the tell — the pin is roughly the block, not the shopfront.
|
||||||
|
>
|
||||||
|
> **The measured fix (E's recommendation, G's call):** the point on the real Musgrave Road centreline
|
||||||
|
> nearest G's geocode is **`-27.455169, 153.006902`**. Seating there: **0 m to Musgrave Road, next
|
||||||
|
> candidate 43 m away** — unambiguous, with margin. It moves the shop 51.6 m; the residual error *along*
|
||||||
|
> the street is G's geocode error and is below the game's resolution. **Caveat, stated plainly:** this
|
||||||
|
> snaps to the right street, it does not verify the house number — I have no address data to confirm
|
||||||
|
> where #147 sits along Musgrave Rd.
|
||||||
|
>
|
||||||
|
> **The robust fix (A's call, likely v5.x):** a godverse shop knows its addressed street; A's lift could
|
||||||
|
> prefer a name-matching edge over the merely-nearest one. That fixes the whole class, not this one shop.
|
||||||
|
> The snap above is enough to ship the alpha honestly.
|
||||||
|
|
||||||
|
**2. `validate_atlas` — made honest (ledger #4). F's catch was right; there were FOUR bugs, not one.**
|
||||||
|
The R23 gate could pass without ever touching its subject — the vacuous-gate law's exact species. Fixed:
|
||||||
|
|
||||||
|
| # | bug | why it mattered |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | globbed the **published-contract** name (`stock_shop_<id>_index.json`) — a name the runtime can *never* load | matched **0 files** and returned 0. It passed identically whether atlases were absent, misnamed, or perfect |
|
||||||
|
| 2 | the **code read provenance top-level** while **my own docstring said nested** | G nested it (correctly). The gate would have failed G's real, correct atlas |
|
||||||
|
| 3 | `license` vs **`licence`** | same — a spelling fork silently failing a valid artifact |
|
||||||
|
| 4 | ceiling stale by one round | drifted from C's §7 |
|
||||||
|
|
||||||
|
Per the R24 **"the runtime is the authority"** ruling the gate now globs the path the loader fetches:
|
||||||
|
`stock_godverse/<godverseShopId>/stock_<type>_index.json`, `type ∈ {record, book, toy}`. **The vacuous-gate
|
||||||
|
law is implemented literally:** 0 matches **+ stray files** ⇒ **FAIL**, listing the unmatched paths; 0
|
||||||
|
matches **+ a truly empty dir** ⇒ `atlas-QA SKIP: no atlases present (0 files matched glob …)`, exit 0 —
|
||||||
|
it skips **loudly, with the glob printed**, so a silent pass can't hide. It also asserts identity
|
||||||
|
(`shop.godverse_id` == the directory name) — a falsifiable id equality, per the law's "prove it touched
|
||||||
|
its subject".
|
||||||
|
|
||||||
|
**Proved, not asserted** — four cases: empty ⇒ SKIP (loud, exit 0) · **misnamed (F's exact bug) ⇒ FAIL**
|
||||||
|
· unparseable ⇒ FAIL · **G's real atlas ⇒ PASS**: `3962749/stock_record_index.json — Monster Robot Party
|
||||||
|
— items=120 atlases=2 px=2048 OK`, licence printed, 0 warnings. **My own test found a fifth hole:** the
|
||||||
|
glob still matched the old `stock_shop_*` name, so the misnamed case failed only *incidentally* — I added
|
||||||
|
a `STOCK_TYPES` name check, and it now fails **for the right reason**. A gate that fails for the wrong
|
||||||
|
reason is a gate that will pass for the wrong reason later.
|
||||||
|
|
||||||
|
**→ Lane C:** built to the runtime + G's real artifact per the ruling. `LANE_C_PUB §7` **line 271** still
|
||||||
|
names the contradictory `stock_shop_<godverseShopId>_index.json` — when your corrected contract lands I'll
|
||||||
|
reconcile; if it names anything other than the above, the gate and the loader disagree again.
|
||||||
|
|
||||||
|
**3. Ballarat — RETIRED (my call, counted). A different illness from Toowoomba.** Full rationale in
|
||||||
|
`SOURCES.md` and the commented config entry. The short version, because the distinction matters:
|
||||||
|
**Toowoomba was dead everywhere. Ballarat is alive exactly where the game isn't.** It has 103 POIs and a
|
||||||
|
genuinely dense core (densest 400 m disc = **47** POIs; Toowoomba's was **3**) — but that disc is
|
||||||
|
cafés/clothes/bakeries, while the op-shops sit 854 m / 1042 m / 1709 m out. Heroes and density don't
|
||||||
|
coincide, so no bbox holds both:
|
||||||
|
|
||||||
|
| span | shops | heroes | median NN | hub ≤160 m | op-shops |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 1.0 km (the live core) | 8 | 2 | 184 m | 4/8 (50%) | **0** ← a thrift town with no thrift shop |
|
||||||
|
| 2.0 km | 12 | 3 | 186 m | 3/12 (25%) | 1 |
|
||||||
|
| 3.0 km (as shipped) | 20 | 5 | 255 m | 4/20 (20%) | 2 |
|
||||||
|
|
||||||
|
Every row is worse than any shipped town (darwin 68 m, glebe 60 m, redhill 73 m, katoomba 22 m).
|
||||||
|
|
||||||
|
**→ A, D: the binding constraint was MINE, not OSM's — and it's a real finding.** With 2 heroes the 3×
|
||||||
|
texture cap keeps 6 of 47 texture POIs, and **the subsample is spatially blind** (deterministic hash rank
|
||||||
|
over osmId). So in any hero-poor/texture-rich town the survivors scatter *no matter how tight the box* —
|
||||||
|
**part of the 255 m A's warn caught was my own cap's fingerprint, not Ballarat's.** Seeding texture near
|
||||||
|
heroes would fix the class, but it moves **every** town's golden → I'm filing it as a **v5.x proposal**,
|
||||||
|
not doing it in a release round.
|
||||||
|
|
||||||
|
**→ A: I OVERSTATED MY R23 ADVICE — please don't encode it as a threshold.** I told you the hub fraction
|
||||||
|
is the discriminator. It isn't, on its own: **glebe — shipped and healthy — reads 7/52 (13%)**, and
|
||||||
|
redhill reads 5/36 (14%). A hub threshold would false-flag both. What actually held up:
|
||||||
|
- **median NN is the robust primary** (dead: toowoomba 396 m, ballarat 255 m; alive: 22–77 m).
|
||||||
|
- **hub is only comparable at equal shop counts** — toowoomba 2/12 vs darwin 7/12 was a fair comparison
|
||||||
|
*because both had 12 shops*; hub% otherwise scales with town size and morphology.
|
||||||
|
- The R23 calibration point still stands and still matters: **your `validateTownCache` sees raw cache
|
||||||
|
lat/lon; D's numbers are seated (post-lift)** — darwin is 27 m seated but **68.3 m raw**, so a threshold
|
||||||
|
read off D's table will false-warn darwin.
|
||||||
|
|
||||||
|
**4. A methodology note I'm recording because it nearly bit me.** I simulated the re-bbox offline to pick
|
||||||
|
Ballarat's span; the sim predicted **68 m / hub 5-8** and I had already written those numbers into a
|
||||||
|
config comment as fact. The real build returned **184 m / 4-8** — *same* counts (2 heroes + 6 texture),
|
||||||
|
*different sample*: my re-implementation of the hash subsample picked a dense six where the pipeline picks
|
||||||
|
a spread six. **Never trust a re-implementation of the pipeline's own selection — run the pipeline.** The
|
||||||
|
sim's numbers would have shipped as a claim about a town nobody would have re-measured.
|
||||||
|
|
||||||
|
**5. Determinism receipt.** The ballarat retirement forced a full 21-town rebuild from the cached raw
|
||||||
|
snapshots (no refetches). **Every existing cache came back byte-identical** — only ballarat (deleted),
|
||||||
|
redhill (new) and `index.json` moved. Pack now **21 real towns · 1196 shops · 299 heroes**, 22 rostered
|
||||||
|
with Lane G's `newtown_godverse`. Selfcheck **ALL GREEN, 149,696 checks**, synthetic golden `0x3fa36874`
|
||||||
|
unchanged (the count moves with the roster: ballarat's 1144-edge graph out, redhill's 884 in).
|
||||||
|
|
||||||
|
**6. → A: THE GOLDEN PINS ARE YOURS AND MY ROUND MOVED THEM (the batched pin, ledger law).** Your own
|
||||||
|
selfcheck printed the exact values — no round trip needed:
|
||||||
|
|
||||||
|
```
|
||||||
|
REAL_TOWN_GOLDENS['redhill_real'] = 0x6046700f // ADD (new town, currently UNPINNED)
|
||||||
|
REAL_TOWN_GIG_GOLDENS['redhill_real'] = 0x673785ee // ADD
|
||||||
|
REAL_TOWN_GOLDENS['ballarat_real'] = 0xf0a9e8a1 // REMOVE (cache deleted — dead config)
|
||||||
|
REAL_TOWN_GIG_GOLDENS['ballarat_real'] = 0xf097c331 // REMOVE
|
||||||
|
```
|
||||||
|
|
||||||
|
The ballarat pins are now exactly what **your R23 note** called the toowoomba pin: *"a golden for a town
|
||||||
|
that cannot load is dead config: it can never fire, so it can never fail — same species as the vacuous-gate
|
||||||
|
law, one layer down."* Your words, my retirement, same fix. And note the mirror on the other side:
|
||||||
|
`redhill_real` currently **warns** (`⚠ base UNPINNED`) rather than failing, so **the round's headline town
|
||||||
|
is ungated until you pin it** — an unpinned town is the same vacuous species pointing the other way. Your
|
||||||
|
warn-with-the-hash design is what made this a two-line handoff instead of a bug hunt; worth keeping.
|
||||||
|
|
||||||
|
**7. → F: an R23 orphan I created and owe you.** `tools/qa/tour_shots.py` (your v4.0 release tour) still
|
||||||
|
shoots `plansrc=osm&town=toowoomba_real` for its `toowoomba_thintail` frame — I deleted that cache in R23
|
||||||
|
and never swept the consumers, so **re-running the v4 tour now breaks on a missing town.** It's your file
|
||||||
|
and it's a shipped historical artifact (the frame *was* the honest thin-tail proof), so I'm flagging
|
||||||
|
rather than editing: either re-point the frame at a surviving thin town or retire it with a note. Same
|
||||||
|
class, lower stakes: `web/js/citygen/gigs.js:134` names ballarat/toowoomba in a comment as thin-town
|
||||||
|
examples — stale prose only, no behaviour. **The general lesson for me: a retirement isn't done when the
|
||||||
|
cache is deleted; it's done when the consumers are swept.**
|
||||||
|
|
||||||
## Round 23 — v5.0-alpha: the atlas validator + the toowoomba call ⟨v5.0-alpha⟩
|
## Round 23 — v5.0-alpha: the atlas validator + the toowoomba call ⟨v5.0-alpha⟩
|
||||||
|
|
||||||
**1. `validate_atlas` — the G2a format gate (ledger #4).** `pipeline/validate_atlas.py`, wired into
|
**1. `validate_atlas` — the G2a format gate (ledger #4).** `pipeline/validate_atlas.py`, wired into
|
||||||
|
|||||||
@ -50,10 +50,30 @@ TOWNS = {
|
|||||||
"bowral_real": {"town": "Bowral", "state": "NSW", "center": (-34.4780, 150.4180), "span_km": 2.2},
|
"bowral_real": {"town": "Bowral", "state": "NSW", "center": (-34.4780, 150.4180), "span_km": 2.2},
|
||||||
"fitzroy_real": {"town": "Fitzroy", "state": "VIC", "center": (-37.7990, 144.9790), "span_km": 2.2},
|
"fitzroy_real": {"town": "Fitzroy", "state": "VIC", "center": (-37.7990, 144.9790), "span_km": 2.2},
|
||||||
"brunswick_real": {"town": "Brunswick", "state": "VIC", "center": (-37.7700, 144.9600), "span_km": 2.6},
|
"brunswick_real": {"town": "Brunswick", "state": "VIC", "center": (-37.7700, 144.9600), "span_km": 2.6},
|
||||||
"ballarat_real": {"town": "Ballarat", "state": "VIC", "center": (-37.5620, 143.8500), "span_km": 3.0},
|
# R24 — RETIRED (counted), and for a DIFFERENT illness than Toowoomba. A's spacing warn caught
|
||||||
|
# it at median NN 255 m. Ballarat is NOT dead: its raw bbox holds 103 POIs and its densest 400 m
|
||||||
|
# disc is 47 of them (Toowoomba's densest was 3). But its retail core is alive exactly where the
|
||||||
|
# GAME isn't — that disc is cafes/clothes/bakeries, while the op-shops sit 854 m, 1042 m and
|
||||||
|
# 1709 m out. The heroes and the density don't coincide, so no bbox holds both. Measured, every
|
||||||
|
# span (real builds, not simulated):
|
||||||
|
# span shops heroes medNN hub160 op-shops
|
||||||
|
# 1.0 km 8 2 184 m 4/8 (50%) 0 <- the live core: a thrift town with NO thrift shop
|
||||||
|
# 2.0 km 12 3 186 m 3/12 (25%) 1
|
||||||
|
# 3.0 km 20 5 255 m 4/20 (20%) 2 <- as shipped
|
||||||
|
# Every row is worse than any shipped town (darwin 68 m/58%, glebe 60 m, redhill 73 m). The
|
||||||
|
# binding constraint is mine, not OSM's: with 2 heroes the 3x cap keeps 6 of 47 texture POIs and
|
||||||
|
# the subsample is spatially blind, so the survivors scatter no matter how tight the box. Fixing
|
||||||
|
# THAT (seeding texture near heroes) is a pipeline change that would move every town's golden —
|
||||||
|
# a v5.x proposal, not a curation move. Cache + raw snapshots removed. See LANE_E_NOTES R24.
|
||||||
|
# "ballarat_real": {"town": "Ballarat", "state": "VIC", "center": (-37.5620, 143.8500), "span_km": 3.0},
|
||||||
"daylesford_real": {"town": "Daylesford", "state": "VIC", "center": (-37.3430, 144.1430), "span_km": 2.0},
|
"daylesford_real": {"town": "Daylesford", "state": "VIC", "center": (-37.3430, 144.1430), "span_km": 2.0},
|
||||||
"geelong_real": {"town": "Geelong", "state": "VIC", "center": (-38.1490, 144.3600), "span_km": 3.0},
|
"geelong_real": {"town": "Geelong", "state": "VIC", "center": (-38.1490, 144.3600), "span_km": 3.0},
|
||||||
"westend_real": {"town": "West End", "state": "QLD", "center": (-27.4830, 153.0100), "span_km": 2.2},
|
"westend_real": {"town": "West End", "state": "QLD", "center": (-27.4830, 153.0100), "span_km": 2.2},
|
||||||
|
# R24 ledger #3 — the v5 crate's real street: **Monster Robot Party, 147 Musgrave Rd, Red Hill,
|
||||||
|
# Brisbane** (John's ruling: the crate ships as the shop it really is, not Newtown). Centre/span
|
||||||
|
# per G's ask (they geocoded the address); westend_real is the only other Brisbane cache and stops
|
||||||
|
# ~2.3 km south. This is the DONOR G runs godverse_town.py against → redhill_godverse.
|
||||||
|
"redhill_real": {"town": "Red Hill", "state": "QLD", "center": (-27.4553, 153.0064), "span_km": 2.4},
|
||||||
# ── RETIRED R23 (E's curation call, counted) — toowoomba_real ────────────────────────────────
|
# ── RETIRED R23 (E's curation call, counted) — toowoomba_real ────────────────────────────────
|
||||||
# D's R22 finding: DEAD — median NN 388 m, 3/12 shops near the hub, **0 patronage finds in 1417
|
# D's R22 finding: DEAD — median NN 388 m, 3/12 shops near the hub, **0 patronage finds in 1417
|
||||||
# checks**. I tried the re-bbox first (3.0 km CBD-wide → 1.6 km on the Ruthven St heart, centre
|
# checks**. I tried the re-bbox first (3.0 km CBD-wide → 1.6 km on the Ruthven St heart, centre
|
||||||
|
|||||||
@ -1,37 +1,49 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Validate per-shop real-stock atlases — the G2a format gate (v5.0-alpha, ROUND23 ledger #4).
|
"""Validate per-shop real-stock atlases — the G2a format gate (v5.0-alpha, R23 #4 · REBUILT R24 #4).
|
||||||
|
|
||||||
Lane G bakes one atlas + index per real shop from dealgod inventory (tier 1 of the charter's stock
|
Judges Lane G's tier-1 atlases **from the committed files alone** — no dealgod DB, no network — so the
|
||||||
ladder). This gate is the FORMAT side: it proves G's output is well-formed, inside C's ceilings, and
|
gate runs on m3. Wired into `validate_manifest` (qa gate 3).
|
||||||
carries its provenance — **on m3, from the files alone, with no dealgod DB and no network**. That's the
|
|
||||||
point: the gate must run anywhere, on the committed artefacts.
|
|
||||||
|
|
||||||
python3 pipeline/validate_atlas.py # exit 0 = every atlas valid (or none yet), 1 = a bad one
|
R24 REBUILD — F held the v5.0-alpha tag partly on this file, and was right. What was wrong (all four
|
||||||
|
real, all mine):
|
||||||
|
1. It globbed `stock_shop_<id>_index.json` — the PUBLISHED contract's name, which **the runtime can
|
||||||
|
never load**. G shipped what the loader reads. So the gate matched 0 files, returned 0, and passed
|
||||||
|
whether the atlas was absent, misnamed or perfect. *A gate that returns 0 on an empty set is not a
|
||||||
|
gate.* → now globs the RUNTIME path, and an empty set SKIPs loudly while a present-but-unmatched
|
||||||
|
file FAILS (the vacuous-gate law).
|
||||||
|
2. Provenance: my code read it TOP-LEVEL while my own docstring said NESTED — G nested it. My doc and
|
||||||
|
my code contradicted each other and G's atlas failed either way. → nested, per the artifact.
|
||||||
|
3. `licence` vs `license` — G uses British; I demanded American. → British (the emitter's, and it's
|
||||||
|
the artifact of record).
|
||||||
|
4. A ceiling stale by one round (I warned on 2 atlases; C's §7.2 allows ≤2 × ≤2048², and G's 120-item
|
||||||
|
crate lands exactly on the cap). → C's current numbers.
|
||||||
|
|
||||||
Contract (published R22 → LANE_E_NOTES E's half · LANE_C_PUB §7 C's ceilings · charter law #3 licence):
|
THE AUTHORITY IS THE RUNTIME (R24 ruling). `stockpack.js` states it: a per-shop pack lives under
|
||||||
index `web/assets/stock_godverse/**/stock_shop_<godverseShopId>_index.json`
|
`assets/stock_godverse/<godverseShopId>/` and carries the SAME `stock_<type>_index.json` name (type picks
|
||||||
{version, atlas_px, cell, atlases[], items:[{id,title,artist,price,price_band,atlas,uv}]}
|
the stock slot); cache identity is `(type, base)`. This gate reads that, not a doc that disagrees with it.
|
||||||
+ provenance: {license, attribution, generator, snapshot}
|
|
||||||
C's ceilings — 1 atlas/shop (hard cap 2); ≤1024² typical, 2048² only for a 60+ item shop.
|
|
||||||
charter law #3 — every atlas carries provenance; no private-individual data, ever.
|
|
||||||
determinism — `snapshot` pins the inventory query result (G snapshots it like E snapshots Overpass raw).
|
|
||||||
|
|
||||||
Fail-soft is NOT this gate's business: a missing atlas is legal at runtime (parody canvas, R7 law). This
|
python3 pipeline/validate_atlas.py # 0 = valid (or an honest SKIP), 1 = a bad/misnamed atlas
|
||||||
gate only judges atlases that EXIST — a malformed one must fail loudly rather than render garbled sleeves.
|
|
||||||
"""
|
"""
|
||||||
import json, os, sys, glob, struct
|
import json, os, sys, glob, struct
|
||||||
|
|
||||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
STOCK_G = os.path.join(ROOT, "web", "assets", "stock_godverse") # Lane G's namespace (charter law #5)
|
STOCK_G = os.path.join(ROOT, "web", "assets", "stock_godverse") # Lane G's namespace (charter law #5)
|
||||||
|
GLOB = os.path.join(STOCK_G, "*", "stock_*_index.json") # the RUNTIME path — what the loader fetches
|
||||||
BANDS = {"bargain", "standard", "collector", "grail"}
|
BANDS = {"bargain", "standard", "collector", "grail"}
|
||||||
|
# The loader fetches `<base>stock_<type>_index.json` where `type` is the shop's registry type — it picks
|
||||||
|
# the stock slot (stockpack SLOT_FOR: record→sleeve, book→spine, toy→box). Any other `<type>` is a name
|
||||||
|
# the runtime can never fetch. The glob alone can't say that (`stock_*_index.json` also matches the old
|
||||||
|
# contract's `stock_shop_<id>_index.json`), so the name is checked, not just matched — presence is not
|
||||||
|
# the-right-thing (the vacuous-gate law's actual lesson).
|
||||||
|
STOCK_TYPES = {"record", "book", "toy"}
|
||||||
REQUIRED_ITEM = ("id", "title", "artist", "price", "price_band", "atlas", "uv")
|
REQUIRED_ITEM = ("id", "title", "artist", "price", "price_band", "atlas", "uv")
|
||||||
REQUIRED_PROV = ("license", "attribution", "generator", "snapshot") # charter law #3 + determinism
|
MAX_ATLASES = 2 # C §7.2: ≤2 atlases per shop
|
||||||
MAX_ATLASES = 2 # C: 1 atlas/shop, hard cap 2 (each extra = +1 draw)
|
PX_MAX = 2048 # C §7.2: ≤2048² each
|
||||||
PX_TYPICAL, PX_MAX = 1024, 2048
|
PX_TYPICAL = 1024 # C §7.2: ≤1024² whenever the crate fits it (≤64 items at 256²)
|
||||||
BIG_SHOP_ITEMS = 60 # C: 2048² only for a 60+ item shop (else spill to the type-pack)
|
FITS_1024 = 64
|
||||||
# charter law #3: "No private-individual data, ever" — a real shop's stock row must never carry a person.
|
# charter law #3: no private-individual data, ever — a stock row must never carry a person.
|
||||||
FORBIDDEN_ITEM_KEYS = {"seller", "seller_name", "owner", "owner_name", "customer", "user", "username",
|
FORBIDDEN_ITEM_KEYS = {"seller", "seller_name", "owner", "owner_name", "customer", "user", "username",
|
||||||
"email", "phone", "mobile", "address", "member", "account", "consignor"}
|
"email", "phone", "mobile", "address", "member", "account", "consignor", "staff"}
|
||||||
|
|
||||||
|
|
||||||
def image_size(path):
|
def image_size(path):
|
||||||
@ -39,17 +51,16 @@ def image_size(path):
|
|||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
head = f.read(32)
|
head = f.read(32)
|
||||||
if head[:8] == b"\x89PNG\r\n\x1a\n":
|
if head[:8] == b"\x89PNG\r\n\x1a\n":
|
||||||
w, h = struct.unpack(">II", head[16:24])
|
return struct.unpack(">II", head[16:24])
|
||||||
return w, h
|
|
||||||
if head[:4] == b"RIFF" and head[8:12] == b"WEBP":
|
if head[:4] == b"RIFF" and head[8:12] == b"WEBP":
|
||||||
fmt = head[12:16]
|
fmt = head[12:16]
|
||||||
if fmt == b"VP8X": # extended: 24-bit minus-one
|
if fmt == b"VP8X":
|
||||||
b = head[24:30]
|
b = head[24:30]
|
||||||
return (b[0] | b[1] << 8 | b[2] << 16) + 1, (b[3] | b[4] << 8 | b[5] << 16) + 1
|
return (b[0] | b[1] << 8 | b[2] << 16) + 1, (b[3] | b[4] << 8 | b[5] << 16) + 1
|
||||||
if fmt == b"VP8 ": # lossy: 14-bit each
|
if fmt == b"VP8 ":
|
||||||
w, h = struct.unpack("<HH", head[26:30])
|
w, h = struct.unpack("<HH", head[26:30])
|
||||||
return w & 0x3FFF, h & 0x3FFF
|
return w & 0x3FFF, h & 0x3FFF
|
||||||
if fmt == b"VP8L": # lossless: packed 14-bit
|
if fmt == b"VP8L":
|
||||||
b = struct.unpack("<I", head[21:25])[0]
|
b = struct.unpack("<I", head[21:25])[0]
|
||||||
return (b & 0x3FFF) + 1, ((b >> 14) & 0x3FFF) + 1
|
return (b & 0x3FFF) + 1, ((b >> 14) & 0x3FFF) + 1
|
||||||
raise ValueError("unrecognised image header (want PNG or WebP)")
|
raise ValueError("unrecognised image header (want PNG or WebP)")
|
||||||
@ -57,57 +68,77 @@ def image_size(path):
|
|||||||
|
|
||||||
def validate_index(path):
|
def validate_index(path):
|
||||||
errs, warns = [], []
|
errs, warns = [], []
|
||||||
name = os.path.basename(path)
|
name = os.path.relpath(path, STOCK_G)
|
||||||
try:
|
try:
|
||||||
idx = json.load(open(path))
|
idx = json.load(open(path))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return [f"{name}: does not parse: {e}"], []
|
return [f"{name}: does not parse: {e}"], [] # present-but-unparseable ⇒ FAIL
|
||||||
d = os.path.dirname(path)
|
d = os.path.dirname(path)
|
||||||
|
|
||||||
# ── provenance (charter law #3) — every atlas carries it, and the gate PRINTS the licence ──
|
# ── the NAME must be one the runtime can fetch: stock_<type>_index.json, type ∈ STOCK_TYPES ──
|
||||||
miss = [f for f in REQUIRED_PROV if not str(idx.get(f, "")).strip()]
|
fname = os.path.basename(path)
|
||||||
if miss:
|
ftype = fname[len("stock_"):-len("_index.json")]
|
||||||
errs.append(f"{name}: missing/empty provenance {miss} (charter law #3 — every atlas carries provenance)")
|
if ftype not in STOCK_TYPES:
|
||||||
|
errs.append(f"{name}: unloadable name — the runtime fetches `stock_<type>_index.json` for "
|
||||||
|
f"type ∈ {sorted(STOCK_TYPES)}; {ftype!r} is never fetched (this is the R23 drift)")
|
||||||
|
elif idx.get("kind") not in (None, ftype):
|
||||||
|
errs.append(f"{name}: kind {idx.get('kind')!r} != the filename's type {ftype!r} — the filename "
|
||||||
|
f"picks the stock slot, so they must agree")
|
||||||
|
|
||||||
# ── C's draw ceiling (LANE_C_PUB §7.1) ──
|
# ── identity: the per-shop base IS the godverse shop id (the loader keys on it) ──
|
||||||
atlases = idx.get("atlases", [])
|
shop_dir = os.path.basename(d)
|
||||||
|
sid = (idx.get("shop") or {}).get("godverse_id")
|
||||||
|
if sid is None:
|
||||||
|
errs.append(f"{name}: shop.godverse_id missing — the loader's base is the shop id; the atlas must say who it is")
|
||||||
|
elif str(sid) != shop_dir:
|
||||||
|
errs.append(f"{name}: shop.godverse_id {sid!r} != its directory {shop_dir!r} "
|
||||||
|
f"(base `stock_godverse/{shop_dir}/` is the cache identity — they must agree)")
|
||||||
|
|
||||||
|
# ── provenance: NESTED, per the artifact + charter law #3 ("every atlas carries provenance") ──
|
||||||
|
prov = idx.get("provenance")
|
||||||
|
if not isinstance(prov, dict):
|
||||||
|
errs.append(f"{name}: `provenance` object missing (charter law #3 — every atlas carries provenance)")
|
||||||
|
prov = {}
|
||||||
|
for f in ("generator", "snapshot"): # snapshot pins determinism (re-bakeable)
|
||||||
|
if not str(prov.get(f, "")).strip():
|
||||||
|
errs.append(f"{name}: provenance.{f} missing/empty")
|
||||||
|
lic = prov.get("licence") or prov.get("license") # British is the artifact's spelling
|
||||||
|
if not isinstance(lic, dict):
|
||||||
|
errs.append(f"{name}: provenance.licence object missing (zone/flag/covers)")
|
||||||
|
else:
|
||||||
|
for f in ("zone", "flag", "covers"):
|
||||||
|
if not str(lic.get(f, "")).strip():
|
||||||
|
errs.append(f"{name}: provenance.licence.{f} missing/empty")
|
||||||
|
|
||||||
|
# ── C §7.2 ceilings ──
|
||||||
|
atlases = idx.get("atlases")
|
||||||
if not isinstance(atlases, list) or not atlases:
|
if not isinstance(atlases, list) or not atlases:
|
||||||
errs.append(f"{name}: no atlases[] listed")
|
errs.append(f"{name}: no atlases[] listed")
|
||||||
atlases = []
|
atlases = []
|
||||||
if len(atlases) > MAX_ATLASES:
|
items = idx.get("items")
|
||||||
errs.append(f"{name}: {len(atlases)} atlases — C's hard cap is {MAX_ATLASES} "
|
|
||||||
f"(1/shop; a 60+ item shop spills to the shared type-pack instead of fragmenting)")
|
|
||||||
elif len(atlases) == 2:
|
|
||||||
warns.append(f"{name}: 2 atlases (+1 draw) — C's rule is 1/shop; 2 is the cap, not the target")
|
|
||||||
|
|
||||||
items = idx.get("items", [])
|
|
||||||
if not isinstance(items, list) or not items:
|
if not isinstance(items, list) or not items:
|
||||||
errs.append(f"{name}: no items")
|
errs.append(f"{name}: no items")
|
||||||
items = []
|
items = []
|
||||||
|
if len(atlases) > MAX_ATLASES:
|
||||||
# ── C's texture/VRAM ceiling (§7.2) + index↔atlas consistency (the declared size must be REAL) ──
|
errs.append(f"{name}: {len(atlases)} atlases — C §7.2 caps a shop at {MAX_ATLASES}")
|
||||||
declared = idx.get("atlas_px")
|
declared = idx.get("atlas_px")
|
||||||
for a in atlases:
|
for a in atlases:
|
||||||
ap = os.path.join(d, a)
|
ap = os.path.join(d, a)
|
||||||
if not os.path.isfile(ap):
|
if not os.path.isfile(ap):
|
||||||
errs.append(f"{name}: atlas {a!r} listed but missing on disk")
|
errs.append(f"{name}: atlas {a!r} listed but missing on disk"); continue
|
||||||
continue
|
|
||||||
try:
|
try:
|
||||||
w, h = image_size(ap)
|
w, h = image_size(ap)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errs.append(f"{name}: atlas {a!r} unreadable: {e}")
|
errs.append(f"{name}: atlas {a!r} unreadable: {e}"); continue
|
||||||
continue
|
|
||||||
if declared and (w != declared or h != declared):
|
if declared and (w != declared or h != declared):
|
||||||
errs.append(f"{name}: atlas {a!r} is {w}×{h} but index declares atlas_px={declared}")
|
errs.append(f"{name}: atlas {a!r} is {w}×{h} but index declares atlas_px={declared}")
|
||||||
big = len(items) >= BIG_SHOP_ITEMS
|
if max(w, h) > PX_MAX:
|
||||||
cap = PX_MAX if big else PX_TYPICAL
|
errs.append(f"{name}: atlas {a!r} {w}×{h} exceeds C's ceiling {PX_MAX}²")
|
||||||
if max(w, h) > cap:
|
elif max(w, h) > PX_TYPICAL and len(items) <= FITS_1024:
|
||||||
errs.append(f"{name}: atlas {a!r} {w}×{h} exceeds C's ceiling {cap}² "
|
warns.append(f"{name}: atlas {a!r} is {w}×{h} for {len(items)} items — C: ≤{PX_TYPICAL}² "
|
||||||
f"({'60+ item shop' if big else f'{len(items)}-item shop → ≤{PX_TYPICAL}²'})")
|
f"whenever the crate fits it (≤{FITS_1024} at 256²)")
|
||||||
if max(w, h) > PX_TYPICAL and not big:
|
|
||||||
warns.append(f"{name}: atlas {a!r} is {w}×{h} for {len(items)} items — prefer ≤{PX_TYPICAL}²")
|
|
||||||
|
|
||||||
# ── the index itself (schema confirmed verbatim in LANE_C_PUB §7.3) ──
|
# ── the index itself (schema per the loader: it reads items[].uv/atlas; `cell`/`cell_w` are informational) ──
|
||||||
ids = set()
|
ids = set()
|
||||||
for i, it in enumerate(items):
|
for i, it in enumerate(items):
|
||||||
if not isinstance(it, dict):
|
if not isinstance(it, dict):
|
||||||
@ -133,30 +164,40 @@ def validate_index(path):
|
|||||||
if not (isinstance(uv, list) and len(uv) == 4 and all(isinstance(v, (int, float)) for v in uv)):
|
if not (isinstance(uv, list) and len(uv) == 4 and all(isinstance(v, (int, float)) for v in uv)):
|
||||||
errs.append(f"{name}[{it['id']}]: uv must be [u0,v0,u1,v1] numbers"); continue
|
errs.append(f"{name}[{it['id']}]: uv must be [u0,v0,u1,v1] numbers"); continue
|
||||||
u0, v0, u1, v1 = uv
|
u0, v0, u1, v1 = uv
|
||||||
if not (0 <= u0 < u1 <= 1 and 0 <= v0 < v1 <= 1): # UV origin top-left; C flips V (R7 law)
|
if not (0 <= u0 < u1 <= 1 and 0 <= v0 < v1 <= 1): # origin top-left; stockpack flips V (R7 law)
|
||||||
errs.append(f"{name}[{it['id']}]: uv out of range / inverted: {uv}")
|
errs.append(f"{name}[{it['id']}]: uv out of range / inverted: {uv}")
|
||||||
return errs, warns
|
return errs, warns
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
idxs = sorted(glob.glob(os.path.join(STOCK_G, "**", "stock_shop_*_index.json"), recursive=True))
|
idxs = sorted(glob.glob(GLOB))
|
||||||
if not idxs:
|
if not idxs:
|
||||||
print("atlas-QA: no per-shop atlases yet (Lane G's G2a) — nothing to validate")
|
# THE VACUOUS-GATE LAW (R23, F): absent subject ⇒ SKIP with a printed reason — and prove the set
|
||||||
|
# really is empty. Files present but unmatched = a misnamed atlas the loader can't fetch ⇒ FAIL.
|
||||||
|
stray = [p for p in glob.glob(os.path.join(STOCK_G, "**", "*"), recursive=True) if os.path.isfile(p)]
|
||||||
|
if stray:
|
||||||
|
print(f"atlas-QA FAIL — {len(stray)} file(s) under stock_godverse/ but 0 matched {GLOB}")
|
||||||
|
print(f" the runtime fetches `<base>stock_<type>_index.json`; these cannot be loaded:")
|
||||||
|
for p in stray[:8]:
|
||||||
|
print(f" unmatched: {os.path.relpath(p, ROOT)}")
|
||||||
|
return 1
|
||||||
|
print(f"atlas-QA SKIP: no atlases present (0 files matched glob {GLOB})")
|
||||||
return 0
|
return 0
|
||||||
all_errs, all_warns = [], []
|
all_errs, all_warns = [], []
|
||||||
for p in idxs:
|
for p in idxs:
|
||||||
e, w = validate_index(p)
|
e, w = validate_index(p)
|
||||||
all_errs += e; all_warns += w
|
all_errs += e; all_warns += w
|
||||||
idx = {}
|
|
||||||
try:
|
try:
|
||||||
idx = json.load(open(p))
|
idx = json.load(open(p))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
idx = {}
|
||||||
print(f" {os.path.basename(p):40} items={len(idx.get('items', [])):>3} "
|
shop = (idx.get("shop") or {}).get("name", "?")
|
||||||
|
print(f" {os.path.relpath(p, STOCK_G):44} {shop[:22]:22} items={len(idx.get('items', [])):>3} "
|
||||||
f"atlases={len(idx.get('atlases', []))} px={idx.get('atlas_px')} "
|
f"atlases={len(idx.get('atlases', []))} px={idx.get('atlas_px')} "
|
||||||
f"snapshot={str(idx.get('snapshot', '—'))[:24]} {'OK' if not e else 'ERR(' + str(len(e)) + ')'}")
|
f"{'OK' if not e else 'ERR(' + str(len(e)) + ')'}")
|
||||||
if idx.get("license"): # licence law: the gate SHOWS it, a human clears it
|
lic = (idx.get("provenance") or {}).get("licence") or {}
|
||||||
print(f" licence: {idx['license']}")
|
if lic: # the licence is PRINTED — clearing it is a human call
|
||||||
|
print(f" licence: {lic.get('zone')} · {lic.get('flag')}")
|
||||||
for w in all_warns:
|
for w in all_warns:
|
||||||
print(f" WARN {w}")
|
print(f" WARN {w}")
|
||||||
for e in all_errs:
|
for e in all_errs:
|
||||||
|
|||||||
@ -20,26 +20,47 @@ the "© OpenStreetMap contributors (ODbL)" credit visible.
|
|||||||
**`index.json` is the machine-readable roster** (key/town/state/shops/roads) — read it rather than
|
**`index.json` is the machine-readable roster** (key/town/state/shops/roads) — read it rather than
|
||||||
hardcoding a list. Every cache carries the same terms; per-town numbers live in each cache's `counts`.
|
hardcoding a list. Every cache carries the same terms; per-town numbers live in each cache's `counts`.
|
||||||
|
|
||||||
**The pack: 21 real towns · 1180 shops · 295 heroes** (+ Lane G's `newtown_godverse` = 22 rostered in
|
**The pack: 21 real towns · 1196 shops · 299 heroes** (+ Lane G's `newtown_godverse` = 22 rostered in
|
||||||
`index.json`), every one fetched from OSM Overpass with a bounded per-town bbox, ODbL 1.0, raw snapshots
|
`index.json`), every one fetched from OSM Overpass with a bounded per-town bbox, ODbL 1.0, raw snapshots
|
||||||
in `_raw/` (shops + roads per town), fetched 2026-07-16.
|
in `_raw/` (shops + roads per town), fetched 2026-07-16 (`redhill_real` 2026-07-17).
|
||||||
|
|
||||||
| state | towns |
|
| state | towns |
|
||||||
|---|---|
|
|---|---|
|
||||||
| NSW (6) | katoomba · newtown · glebe · marrickville · newcastle (Hamilton strip) · bowral |
|
| NSW (6) | katoomba · newtown · glebe · marrickville · newcastle (Hamilton strip) · bowral |
|
||||||
| VIC (7) | fitzroy · brunswick · geelong · bendigo · ballarat · castlemaine · daylesford |
|
| VIC (6) | fitzroy · brunswick · geelong · bendigo · castlemaine · daylesford |
|
||||||
|
| QLD (2) | westend · **redhill** (Monster Robot Party's real street — see below) |
|
||||||
| WA (2) | fremantle · northbridge |
|
| WA (2) | fremantle · northbridge |
|
||||||
| QLD (1) | westend |
|
|
||||||
| TAS (2) | hobart · launceston |
|
| TAS (2) | hobart · launceston |
|
||||||
| SA (1) | adelaide |
|
| SA (1) | adelaide |
|
||||||
| ACT (1) | braddon |
|
| ACT (1) | braddon |
|
||||||
| NT (1) | darwin |
|
| NT (1) | darwin |
|
||||||
|
|
||||||
|
### `redhill_real` — the v5 crate's real street (R24, ledger #3)
|
||||||
|
|
||||||
|
**Red Hill, Brisbane QLD** · centre `-27.4553, 153.0064` · span 2.4 km · 36 shops (9 heroes + 27 texture,
|
||||||
|
−29 subsampled) · 884 roads · median NN 73 m, hub 5/36. Built because v5's 120 real records come from a
|
||||||
|
real shop — **Monster Robot Party, 147 Musgrave Rd** — and the shop had no street. **Musgrave Road is in
|
||||||
|
`roads[]` (41 ways, primary + residential)**, which was the point of the round. This cache is the DONOR
|
||||||
|
Lane G runs `godverse_town.py` against → `redhill_godverse`.
|
||||||
|
|
||||||
|
The shop itself is **not in OSM** (no `shop=music` node in the bbox), so G injects it from the census —
|
||||||
|
it cannot arrive via this fetch. See LANE_E_NOTES R24 for the seating hazard handed to G/A.
|
||||||
|
|
||||||
**Retired (R23, counted):** `toowoomba_real` — D's clustering audit measured it DEAD (0 patronage finds
|
**Retired (R23, counted):** `toowoomba_real` — D's clustering audit measured it DEAD (0 patronage finds
|
||||||
in 1417 checks); a re-bbox onto the Ruthven St heart fixed the spacing metric (median NN 396 → 89 m) but
|
in 1417 checks); a re-bbox onto the Ruthven St heart fixed the spacing metric (median NN 396 → 89 m) but
|
||||||
made the hub worse (2/12 within 160 m, the pack's worst), and its densest 300 m cluster is only 3 shops.
|
made the hub worse (2/12 within 160 m, the pack's worst), and its densest 300 m cluster is only 3 shops.
|
||||||
Cache + raw snapshots removed; the config entry stays commented with the rationale. See LANE_E_NOTES R23.
|
Cache + raw snapshots removed; the config entry stays commented with the rationale. See LANE_E_NOTES R23.
|
||||||
|
|
||||||
|
**Retired (R24, counted):** `ballarat_real` — A's spacing warn caught it at median NN 255 m. Unlike
|
||||||
|
Toowoomba, Ballarat is **not dead**: 103 POIs in the bbox, densest 400 m disc = 47 of them. It fails a
|
||||||
|
different way — **its live core and its heroes don't coincide.** The dense disc is cafés/clothes/bakeries;
|
||||||
|
the op-shops sit 854 m, 1042 m and 1709 m out, so no bbox holds both. Measured at every span (real builds):
|
||||||
|
1.0 km → 8 shops / NN 184 m / hub 4/8 but **zero op-shops** (a thrift town with no thrift shop); 2.0 km →
|
||||||
|
12 / 186 m / 3-12; 3.0 km (as shipped) → 20 / 255 m / 4-20. Every row is worse than any shipped town
|
||||||
|
(darwin 68 m, glebe 60 m, redhill 73 m). The binding constraint is ours, not OSM's: with 2 heroes the 3×
|
||||||
|
texture cap keeps 6 of 47 POIs and the subsample is **spatially blind**, so survivors scatter at any span.
|
||||||
|
Cache + raw snapshots removed; config entry commented with the rationale. See LANE_E_NOTES R24.
|
||||||
|
|
||||||
## The density widening (v4.0-beta, ROUND20 ledger #2)
|
## The density widening (v4.0-beta, ROUND20 ledger #2)
|
||||||
|
|
||||||
The fetch covers the **secondhand heroes** plus the everyday main-street **texture** (bakery/cafe/deli/
|
The fetch covers the **secondhand heroes** plus the everyday main-street **texture** (bakery/cafe/deli/
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1208
web/assets/towns/_raw/redhill_real.overpass.json
Normal file
1208
web/assets/towns/_raw/redhill_real.overpass.json
Normal file
File diff suppressed because it is too large
Load Diff
165975
web/assets/towns/_raw/redhill_real.roads.overpass.json
Normal file
165975
web/assets/towns/_raw/redhill_real.roads.overpass.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema": "procity-towns-index/1",
|
"schema": "procity-towns-index/1",
|
||||||
"generated": "2026-07-16",
|
"generated": "2026-07-17",
|
||||||
"license": "ODbL 1.0",
|
"license": "ODbL 1.0",
|
||||||
"attribution": "\u00a9 OpenStreetMap contributors",
|
"attribution": "\u00a9 OpenStreetMap contributors",
|
||||||
"towns": [
|
"towns": [
|
||||||
@ -12,14 +12,6 @@
|
|||||||
"roads": true,
|
"roads": true,
|
||||||
"source": "osm"
|
"source": "osm"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"key": "ballarat_real",
|
|
||||||
"town": "Ballarat",
|
|
||||||
"state": "VIC",
|
|
||||||
"shops": 20,
|
|
||||||
"roads": true,
|
|
||||||
"source": "osm"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"key": "bendigo_real",
|
"key": "bendigo_real",
|
||||||
"town": "Bendigo",
|
"town": "Bendigo",
|
||||||
@ -172,6 +164,14 @@
|
|||||||
"roads": true,
|
"roads": true,
|
||||||
"source": "osm"
|
"source": "osm"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "redhill_real",
|
||||||
|
"town": "Red Hill",
|
||||||
|
"state": "QLD",
|
||||||
|
"shops": 36,
|
||||||
|
"roads": true,
|
||||||
|
"source": "osm"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "westend_real",
|
"key": "westend_real",
|
||||||
"town": "West End",
|
"town": "West End",
|
||||||
|
|||||||
38554
web/assets/towns/redhill_real.json
Normal file
38554
web/assets/towns/redhill_real.json
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user