Lane A round 23 (v5.0-alpha): the spacing warn — D's thin-tail finding as law (ledger #5)
validateTownCache now warns when medianShopSpacing(cache) > MAX_MEDIAN_SPACING_M (150 m). Metric + threshold exported from the barrel so no lane re-derives them. WARN, NEVER ERROR: a flagged cache still boots a valid district (toowoomba shipped green through the entire v4.0 pack) — E curates, A flags. selfcheck ALL GREEN 161,372/161,372 (+72); synthetic 0x3fa36874 frozen; NO golden moved (only E's toowoomba re-bbox may move one this round, per the round's law). THE THRESHOLD IS MEASURED, NOT PICKED. The brief offered darwin 27 m / toowoomba 388 m as "plenty of daylight for a line" — but that's 2 points. Measuring all 23 towns shows a near-continuum, and the line is FORCED into (119.3, 254.9]: braddon at 119.3 m is ALIVE by D's own hub test (7 shops within 160 m, identical to darwin's 7), ballarat at 254.9 m is not. 150 m sits in the pack's widest gap (x2.14), with 1.26x margin below and 1.70x above. Flags 2 of 23. TWO TOWNS FLAG, NOT ONE. The brief names only toowoomba (D spot-checked the two 12-shop towns). The measurement adds BALLARAT at 255 m — nobody has looked at it, and it's independently one of the three towns my R22 graded cluster fallback fired on (no venue candidate at the >=3-shops/160 m floor). Two signals agree. Filed for E as a curation call, counted either way. THE BRIEF'S ANCHORS WERE IN THE WRONG SPACE, AND IT MATTERS. D's 27 m is PLAN space (post-lift metres); validateTownCache is CACHE space (raw lat/lon — the only thing that exists at ingest, and the layer where a bad bbox must be caught). Darwin is 68.3 m in cache space, not 27. Interpolating a threshold between D's figures would have silently mixed spaces. I verified the hypothesis before trusting it: computing the metric plan-side reproduces D's numbers exactly (darwin 27.2 vs 27, toowoomba 386.8 vs 388, hubs 7 and 3). Cache space is correct HERE and provably safe: the lift marches shops at a regularised cadence, so it COMPRESSES dense towns (darwin plan/cache 0.40) but not sparse ones (toowoomba 0.98, ballarat 1.01) -- the error lives at the dense end, the warn only fires at the sparse end. Verified all 23 towns in both spaces: exactly ONE disagrees, newtown_godverse (94.4 cache passes / 158.3 plan would warn). Filed for G/Fable, NOT acted on: no cache-space line catches it without false-positiving braddon, and godverse is a census subset (18 thriftgod shops over the footprint that carries 72 OSM shops) so its sparsity is coverage, not a bad bbox — re-bboxing, the warn's only remedy, cannot fix it. Worth knowing since R23 stocks the real crate in that town and the beta asks "fitzroy_godverse if the census is dense there". Also fixed en route: the R17 silent-fallback trap bit again in my own scratch harness — an unregistered town key resolves to the melbourne fixture rather than erroring, so five towns silently measured identical. Registered the caches; noting it because it has now cost two rounds. Coverage: sparse cache warns + still boots (structural + district suites on a flagged town), compact cache does NOT warn (no false positive), metric edge cases (< 2 shops -> null, coincident -> 0, no throw on empty), and an exact-value check (a 100 m pair measures 100 m, projection sane). Cost: ~1 ms/town at registration (25.6 ms for all 23 incl. fitzroy's 160 shops) vs a <100 ms plan budget. E -> use the warn as your acceptance test for #4: re-bbox, re-run, warn goes silent = the town has a high street. If your fix moves toowoomba's or ballarat's cache, ping me — goldens are mine to re-pin, and only what you move. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f14e4d1974
commit
292b07aa91
@ -1,11 +1,37 @@
|
||||
# LANE A — CITYGEN · progress (PROCITY-A)
|
||||
|
||||
*Status: **all deliverables landed; self-check all-green (161,300/161,300); rounds 2–22 closed** (see below).
|
||||
R22 (v4.0 epoch close): the town pack absorbed — **23 towns, 1,210 shops, 36 goldens pinned in one commit**;
|
||||
every town 1 connected component + 3 venues; drops 6.4%, counted by cause. classic `0x3fa36874` /
|
||||
gig `0xb1d48ea1` / marched fixtures frozen.*
|
||||
*Status: **all deliverables landed; self-check all-green (161,372/161,372); rounds 2–23 closed** (see below).
|
||||
R23 (v5.0-alpha): D's thin-tail finding is law — `validateTownCache` warns above **150 m median shop
|
||||
spacing**; flags 2 of 23 towns (toowoomba 396 m, **ballarat 255 m — beyond the brief**). Warn, never error.
|
||||
R22: the pack absorbed — 23 towns, 36 goldens in one commit. classic `0x3fa36874` / gig `0xb1d48ea1` frozen.*
|
||||
*Date: 2026-07-16 · owner: PROCITY-A (Opus 4.8)*
|
||||
|
||||
## Round 23 (2026-07-16) — v5.0-alpha: the spacing warn (ledger #5)
|
||||
|
||||
D's R22 finding made law: `MIN_TOWN_SHOPS` counts shops, but what makes a town read alive is **clustering**.
|
||||
`validateTownCache` now warns when `medianShopSpacing(cache) > MAX_MEDIAN_SPACING_M` (**150 m**) — the metric
|
||||
and threshold both exported from the barrel, so no lane re-derives them. **Warn, never error**: a flagged
|
||||
cache still boots a valid district (toowoomba shipped green through the whole v4.0 pack), E curates, the
|
||||
validator flags. selfcheck **ALL GREEN 161,372/161,372** (+72), synthetic `0x3fa36874` frozen, no golden moved.
|
||||
|
||||
- **The threshold is measured, not picked.** Fable's brief offered darwin 27 m / toowoomba 388 m as "plenty
|
||||
of daylight for a line". Measuring all 23 towns showed the pack is a near-continuum, and the line is
|
||||
**forced** into `(119.3, 254.9]` — braddon (119.3 m) is alive by D's own hub test (7 shops within 160 m,
|
||||
identical to darwin's 7), ballarat (254.9 m) is not. 150 m sits in the pack's widest gap (**×2.14**).
|
||||
- **Two towns flag, not one.** The brief names toowoomba; the measurement adds **ballarat (255 m)**, which
|
||||
nobody had looked at — and which is independently one of the three towns my R22 cluster fallback fired on.
|
||||
Filed for E as a curation call, counted either way.
|
||||
- **The brief's anchors were in the wrong space, and it matters.** D's 27 m is **plan** space (post-lift);
|
||||
`validateTownCache` is **cache** space (raw lat/lon — the only thing it has at ingest). Darwin is 68.3 m
|
||||
in cache space, not 27. Coding a threshold interpolated between D's figures would have mixed spaces
|
||||
silently. Cache space is right *here*, and provably safe: the lift compresses dense towns (darwin
|
||||
plan/cache **0.40**) but not sparse ones (toowoomba **0.98**, ballarat **1.01**), so the error lives at the
|
||||
dense end and the warn only fires at the sparse end. Verified all 23 in both spaces: **one town disagrees**
|
||||
(`newtown_godverse`, 94.4 cache → 158.3 plan) — filed for G/Fable, not acted on, since no cache-space line
|
||||
catches it without false-positiving braddon, and its sparsity is census *coverage*, which re-bboxing can't fix.
|
||||
- Cost: ~1 ms/town at registration (25.6 ms for all 23 including fitzroy's 160 shops), against a <100 ms
|
||||
plan budget. Pending: re-pin whatever E's toowoomba re-bbox moves — **only** that.
|
||||
|
||||
## Round 22 (2026-07-16) — v4.0 THE EPOCH CLOSE: the pack absorbed + 36 goldens pinned (ledger #2)
|
||||
|
||||
E's 17 new towns + Lane G's `newtown_godverse` fed through the lift and pinned in ONE golden commit
|
||||
|
||||
@ -394,6 +394,32 @@ it identically to the fixtures. **Hard requirements:** finite `center.{lat,lon}`
|
||||
blank name → type label, unknown `type` → `opshop`, dup ids, and **span > 5 km → "likely more than one
|
||||
town"** (a cache is ONE compact town — an over-broad bbox marches into an unusable multi-km strip).
|
||||
|
||||
**The spacing floor (v5.0-alpha, ROUND23 ledger #5 — D's R22 finding as law).** `MIN_TOWN_SHOPS` counts
|
||||
shops; what makes a town **read alive** is *clustering*. D measured both 12-shop towns in the pack and they
|
||||
disagree — which is the finding: **darwin ALIVE** (one strip; 1,455 patronage checks → 18 visits) vs
|
||||
**toowoomba DEAD** (scattered over a 4.6 km road network; 1,417 checks → **0 visits, 0 finds** — "not a
|
||||
shopping town, a road network with occasional shops"). Both pass every other law. So `validateTownCache`
|
||||
now **warns when `medianShopSpacing(cache) > MAX_MEDIAN_SPACING_M` (150 m)**:
|
||||
- **The metric** (exported from the barrel — import it, don't re-derive it): for each shop, the distance to
|
||||
its **nearest other shop**; take the **median**. Equirectangular metres about `center.lat`, the same
|
||||
convention as the span check. `null` for < 2 usable shops.
|
||||
- **Warn, never error.** A flagged cache still boots a valid district — toowoomba shipped green through the
|
||||
entire v4.0 pack. **E curates** (re-bbox onto the retail strip, or retire the town, counted either way);
|
||||
the validator only flags. The span warn catches a town too **wide**; this catches one too **sparse**, a
|
||||
case span cannot see (toowoomba is *inside* the 5 km law).
|
||||
- **The threshold is measured, not chosen.** Across the 23-town pack the line is **forced** into
|
||||
`(119.3, 254.9]`: braddon at 119.3 m is alive by D's own hub test (7 shops within 160 m — the same as
|
||||
darwin's 7), and ballarat at 254.9 m is not. 150 m sits in that gap — the pack's widest (**×2.14**) —
|
||||
with 1.26× margin below and 1.70× above. It flags **2 of 23**: ballarat 255 m, toowoomba 396 m.
|
||||
- **⚠ CACHE space, not PLAN space — they do not agree.** This runs on raw lat/lon, before any lift; D's
|
||||
notes quote **plan** figures (post-lift metres). The lift marches shops at a regularised cadence, so it
|
||||
**compresses** dense towns (darwin **68.3 m cache → 27.2 m plan**) and leaves sparse ones alone
|
||||
(toowoomba 396.1 → 386.8). Both numbers are right; they measure different spaces. Cache space is correct
|
||||
*here* — the validator's job is to flag a bad bbox **at ingest**, before a lift exists — and it is
|
||||
faithful exactly where the warn lives (ballarat plan/cache **1.01**, toowoomba **0.98**). Known limit:
|
||||
one town flips (`newtown_godverse`, cache 94.4 → plan 158.3); no cache-space line can catch it without
|
||||
false-positiving braddon. See `LANE_A_NOTES` §R23.
|
||||
|
||||
**Wiring:** `generatePlanOSM(seed, key, { cache })` marches a cache; `registerTownCache(key, cache)` adds it
|
||||
to the registry (`osmTownKeys()` + `generatePlanFor(seed,'osm',{town:key})` then see it). `selfcheck.js`
|
||||
auto-loads `web/assets/towns/*.json` — validates, runs the structural + gig suites, pins a per-town golden.
|
||||
|
||||
@ -2,6 +2,56 @@
|
||||
|
||||
Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`.
|
||||
|
||||
## Round 23 (2026-07-16) — v5.0-alpha: the spacing warn (D's thin-tail finding as law)
|
||||
|
||||
### → E: **the warn is live — and it flags TWO towns, not one** (the handshake for your #4)
|
||||
`validateTownCache` now warns when **`medianShopSpacing(cache) > 150 m`**. Import the metric from the
|
||||
barrel (`medianShopSpacing`, `MAX_MEDIAN_SPACING_M`) rather than re-deriving it — and if you re-implement
|
||||
in Python, the definition is: *per shop, distance to the nearest OTHER shop; take the median*, in
|
||||
equirectangular metres about `center.lat`. Warn, never error: **you curate, I only flag.**
|
||||
|
||||
**Use the warn as your acceptance test.** Re-bbox toowoomba, re-run the validator; when the warn goes
|
||||
silent, the town has a high street. That's the whole loop, no judgement call needed.
|
||||
|
||||
| town | median spacing | verdict |
|
||||
|---|---|---|
|
||||
| `toowoomba_real` | **396 m** | your ledger #4 — D proved it dead (1,417 patronage checks → 0 visits) |
|
||||
| `ballarat_real` | **255 m** | **nobody has looked at this one.** Same class, 1.7× inside the line |
|
||||
| braddon (worst passing) | 119 m | alive by D's hub test — 7 shops within 160 m, same as darwin |
|
||||
|
||||
**Ballarat is the finding beyond the brief.** The round doc names only toowoomba, because D only spot-checked
|
||||
the two 12-shop towns. Measuring all 23 says ballarat is the same shape — and it's one of the three towns my
|
||||
R22 graded cluster fallback fired on (no venue candidate at the ≥3-shops/160 m floor), so two independent
|
||||
signals agree. Your call whether it re-bboxes or ships as a known thin-tail; counted either way. **If your
|
||||
fix moves toowoomba's (or ballarat's) cache, ping me — the goldens are mine to re-pin, and only what you move.**
|
||||
|
||||
### ⚠ The number in my validator will NOT match the number in D's notes — both are right
|
||||
D's notes say darwin **27 m**; my validator says darwin **68.3 m**. Neither is wrong: **D measured PLAN
|
||||
space** (post-lift metres), **the validator measures CACHE space** (raw lat/lon, before any lift exists —
|
||||
it has to, that's the layer it runs at). The lift marches shops at a regularised cadence, so it *compresses*
|
||||
dense towns and leaves sparse ones alone:
|
||||
|
||||
| town | cache | plan | plan/cache | |
|
||||
|---|---|---|---|---|
|
||||
| darwin | 68.3 | 27.2 | **0.40** | dense ⇒ the lift compresses hard |
|
||||
| toowoomba | 396.1 | 386.8 | **0.98** | sparse ⇒ the lift barely moves it |
|
||||
| ballarat | 254.9 | 257.7 | **1.01** | sparse ⇒ ditto |
|
||||
|
||||
That asymmetry is *why cache space is safe here*: the error is concentrated at the dense end, and the warn
|
||||
only ever fires at the sparse end, where the two spaces agree to within 2%. I verified all 23 towns in both
|
||||
spaces — **exactly one disagrees.**
|
||||
|
||||
### → G + Fable: `newtown_godverse` is the one town that flips (filed, not acted on)
|
||||
Godverse is the single cache where the two spaces straddle the line: **94.4 m cache (passes) → 158.3 m plan
|
||||
(would warn)**. My warn does **not** flag it, and I think that's correct on both counts: (a) no cache-space
|
||||
threshold can catch it without also flagging braddon, which D's own test says is alive; (b) godverse is a
|
||||
**census subset** — 18 thriftgod shops over the same Newtown footprint that carries 72 OSM shops — so its
|
||||
sparsity is *coverage*, not a bad bbox, and re-bboxing (the warn's whole remedy) cannot fix it.
|
||||
|
||||
**But it's thin by two independent signals** — plan-space 158 m, and it's the third town my R22 cluster
|
||||
fallback fired on. Since R23 stocks a real record shop *in this town*, and the beta asks "fitzroy_godverse
|
||||
if the census is dense there", that's worth knowing now rather than at beta. Not my call — filing it.
|
||||
|
||||
## Round 22 (2026-07-16) — v4.0: the pack absorbed, 36 goldens pinned (the epoch close)
|
||||
|
||||
### → C + D + F: **the pack is in — 23 towns, all pinned, start your verifies** (the handshake)
|
||||
|
||||
@ -9,7 +9,8 @@
|
||||
|
||||
export { generatePlan, chunkIndex, chunkKey, CHUNK, lotCorners, obbOverlap, isOpen } from './plan.js';
|
||||
export { shopName, townName, bandName } from './names.js';
|
||||
export { generatePlanOSM, osmTownKeys, registerTownCache, validateTownCache, MIN_TOWN_SHOPS } from './plan_osm.js';
|
||||
export { generatePlanOSM, osmTownKeys, registerTownCache, validateTownCache, MIN_TOWN_SHOPS,
|
||||
medianShopSpacing, MAX_MEDIAN_SPACING_M } from './plan_osm.js';
|
||||
export { withGigs, gigKeyFor, POSTER_CLEAR } from './gigs.js';
|
||||
// The street-corridor law + venue vocabulary, re-exported so consumers get the whole Lane A contract
|
||||
// from one import (ROUND13). `roadWidth`/`vergeBand`/`poleOffset` are the road-vs-verge split of
|
||||
|
||||
@ -54,6 +54,46 @@ const ACCEPTED_SCHEMAS = new Set(['procity-town-cache/1', 'procity-town-cache/2'
|
||||
export const MIN_TOWN_SHOPS = 6; // functional floor: up to 4 venues + the one openLate landmark + a spare
|
||||
export const MAX_TOWN_SPAN_M = 5000; // a cache should be ONE compact town, not a region (see R17 risk note)
|
||||
|
||||
// ── the spacing floor (ROUND23 ledger #5 — D's R22 thin-tail finding, made law) ──────────────────
|
||||
// MIN_TOWN_SHOPS counts shops; what makes a town READ ALIVE is clustering. D measured both 12-shop
|
||||
// towns in the pack and they disagree — that disagreement is the whole finding:
|
||||
// darwin_real ALIVE — shops sit on one strip; 1455 patronage checks → 18 visits.
|
||||
// toowoomba_real DEAD — shops scattered over a 4.6 km road network; 1417 checks → 0 visits, 0 finds.
|
||||
// "Not a shopping town — a road network with occasional shops."
|
||||
// Both pass MIN_TOWN_SHOPS. One has a high street; the other doesn't. So: WARN, never error — E curates
|
||||
// (re-bbox or retire, counted either way), the validator only flags. A flagged cache still boots a valid
|
||||
// district, which is why this can't be an error: toowoomba shipped green in the whole v4.0 pack.
|
||||
export const MAX_MEDIAN_SPACING_M = 150;
|
||||
|
||||
// D's metric: for each shop, the distance to its NEAREST OTHER shop; take the median. Exported so every
|
||||
// lane measures the same thing (the R13-debt-#5 pattern — publish the law as a function, not a paragraph
|
||||
// each lane re-implements). Returns null for a cache with < 2 usable shops (nothing to space).
|
||||
//
|
||||
// SPACE WARNING — this is CACHE space (raw lat/lon), not PLAN space (post-lift metres), and the two do
|
||||
// NOT agree. The lift marches shops onto real edges at a regularised cadence, so it COMPRESSES dense
|
||||
// towns (darwin: 68.3 m cache → 27.2 m plan) while leaving sparse ones alone (toowoomba: 396.1 → 386.8).
|
||||
// D's notes quote PLAN figures (27 / 388); this function on the same towns returns 68.3 / 396.1. Both are
|
||||
// right — they measure different spaces. Cache space is the correct one HERE: the validator's job is to
|
||||
// flag a bad bbox at ingest, before any lift exists, and it is faithful exactly where the warn lives
|
||||
// (the sparse end: ballarat plan/cache 1.01, toowoomba 0.98). See LANE_A_NOTES §R23 for the full table.
|
||||
export function medianShopSpacing(cache) {
|
||||
const shops = (cache && Array.isArray(cache.shops) ? cache.shops : []).filter(s => s && isNum(s.lat) && isNum(s.lon));
|
||||
if (shops.length < 2) return null;
|
||||
const lat0 = (cache.center && isNum(cache.center.lat)) ? cache.center.lat : shops[0].lat;
|
||||
const kx = 111320 * Math.cos(lat0 * Math.PI / 180), kz = 111320; // same equirectangular convention as the span check
|
||||
const nn = shops.map(s => {
|
||||
let best = Infinity;
|
||||
for (const o of shops) {
|
||||
if (o === s) continue;
|
||||
best = Math.min(best, Math.hypot((s.lon - o.lon) * kx, (s.lat - o.lat) * kz));
|
||||
}
|
||||
return best;
|
||||
});
|
||||
nn.sort((a, b) => a - b);
|
||||
const m = nn.length >> 1;
|
||||
return nn.length % 2 ? nn[m] : (nn[m - 1] + nn[m]) / 2;
|
||||
}
|
||||
|
||||
// ── roads[] (schema v2, ROUND18) ─────────────────────────────────────────────────────────────────
|
||||
// OSM `highway=*` → CityPlan edge kind. The graph lift refines this with shop density (a `side` road
|
||||
// dense with shops becomes the `main` spine). Anything unmapped defaults to 'side'.
|
||||
@ -118,6 +158,13 @@ export function validateTownCache(cache) {
|
||||
const latM = (mxLat - mnLat) * 111320, lonM = (mxLon - mnLon) * 111320 * Math.cos((c && c.lat || 0) * Math.PI / 180);
|
||||
if (Math.max(latM, lonM) > MAX_TOWN_SPAN_M) warnings.push(`shops span ${(Math.max(latM, lonM) / 1000).toFixed(1)} km — likely more than one town (bound the query tighter)`);
|
||||
}
|
||||
// thin-tail warning (ROUND23 ledger #5): the span check catches a town that's too WIDE; this catches
|
||||
// one that's too SPARSE to read as a town at all — D's finding. Metadata only, like the span warn:
|
||||
// plan_osm still boots a valid district, it just has no high street to walk.
|
||||
const spacing = medianShopSpacing(cache);
|
||||
if (spacing !== null && spacing > MAX_MEDIAN_SPACING_M) {
|
||||
warnings.push(`median shop spacing ${Math.round(spacing)} m > ${MAX_MEDIAN_SPACING_M} m — shops are scattered, not a high street (re-bbox onto the retail strip, or retire the town)`);
|
||||
}
|
||||
}
|
||||
// v2: optional roads[] — simplified OSM ways. Each is `{ kind, pts:[[lat,lon],…] }` (id/name optional);
|
||||
// `kind` is the OSM highway=* class. Absent (or a v1 cache) ⇒ the marched-avenue fallback, so nothing
|
||||
|
||||
@ -8,7 +8,8 @@ import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { generatePlan, chunkIndex, lotCorners, obbOverlap, CHUNK } from './plan.js';
|
||||
import { generatePlanOSM, osmTownKeys, validateTownCache, registerTownCache, MIN_TOWN_SHOPS } from './plan_osm.js';
|
||||
import { generatePlanOSM, osmTownKeys, validateTownCache, registerTownCache, MIN_TOWN_SHOPS,
|
||||
medianShopSpacing, MAX_MEDIAN_SPACING_M } from './plan_osm.js';
|
||||
import { generatePlanFor, gigKeyFor, POSTER_CLEAR } from './index.js';
|
||||
import { allFacadeSkins, SHOP_TYPES, VENUE_KINDS, genreForVenueKind, roadWidth } from '../core/registry.js';
|
||||
import { xmur3 } from '../core/prng.js';
|
||||
@ -400,6 +401,40 @@ for (const [label, cache, needle] of [
|
||||
const v = validateTownCache(wide);
|
||||
ok(v.ok && v.warnings.some(w => /span|town/.test(w)), `wide-spread cache: valid but warns (${v.warnings.find(w => /span/.test(w)) || 'NO WARN'})`);
|
||||
}
|
||||
// (c2) the SPACING warn (ROUND23 ledger #5 — D's thin-tail finding as law). The span warn catches a town
|
||||
// that's too WIDE; this catches one too SPARSE to read as a town. Warn, never error — and the town must
|
||||
// still boot green, because that's exactly what toowoomba did through the entire v4.0 pack.
|
||||
{
|
||||
// a scattered town: 12 shops ~400 m apart (toowoomba's shape) — inside the 5 km span law, so the span
|
||||
// warn does NOT fire; this is the gap D found and the reason the metric exists.
|
||||
const scattered = mkCache(Array.from({ length: 12 }, (_, i) => ({
|
||||
id: 600 + i, name: `Scattered ${i}`, type: 'opshop',
|
||||
lat: -33.7 + (i % 4) * 0.0036, lon: 150.3 + Math.floor(i / 4) * 0.0036, suburb: 'Testville',
|
||||
})));
|
||||
const v = validateTownCache(scattered);
|
||||
ok(v.ok, `sparse cache: still VALID (warn, not error) — a flagged town boots a real district`);
|
||||
ok(v.warnings.some(w => /median shop spacing/.test(w)), `sparse cache: warns (${v.warnings.find(w => /spacing/.test(w)) || 'NO WARN'})`);
|
||||
ok(!v.warnings.some(w => /span/.test(w)), `sparse cache: the SPAN warn stays silent — spacing catches what span cannot`);
|
||||
structuralSuite(generatePlanOSM(20261990, 'sparse', { cache: scattered }), `sparse-warn town`);
|
||||
districtInvariants(generatePlanFor(20261990, 'osm', { gigs: true, town: 'sparse', cache: scattered }), `sparse-warn town`);
|
||||
|
||||
// a compact town must NOT warn (no false positive on the shape 20 of 23 pack towns have)
|
||||
const tight = validateTownCache(mkCache(cluster(14)));
|
||||
ok(!tight.warnings.some(w => /median shop spacing/.test(w)), `compact cache: no spacing warn (no false positive)`);
|
||||
|
||||
// the metric itself — D's definition: per shop, distance to the NEAREST OTHER shop, then the median.
|
||||
ok(medianShopSpacing(mkCache(cluster(14))) < MAX_MEDIAN_SPACING_M, `medianShopSpacing: compact town under the floor`);
|
||||
ok(medianShopSpacing(mkCache(cluster(14, { samePoint: true }))) === 0, `medianShopSpacing: coincident shops → 0 m`);
|
||||
ok(medianShopSpacing(mkCache(cluster(1))) === null, `medianShopSpacing: < 2 shops → null (nothing to space)`);
|
||||
ok(medianShopSpacing({ shops: [] }) === null, `medianShopSpacing: no shops → null, no throw`);
|
||||
{
|
||||
// exact-value check: two shops 100 m apart in latitude → both nearest-neighbours are 100 m → median 100.
|
||||
const pair = mkCache([{ id: 1, name: 'A', type: 'opshop', lat: -33.7, lon: 150.3 },
|
||||
{ id: 2, name: 'B', type: 'opshop', lat: -33.7 + 100 / 111320, lon: 150.3 }]);
|
||||
const got = medianShopSpacing(pair);
|
||||
ok(Math.abs(got - 100) < 0.5, `medianShopSpacing: 100 m pair measures ${got.toFixed(1)} m (projection sane)`);
|
||||
}
|
||||
}
|
||||
// (d) the registry seam: register → osmTownKeys() sees it → generatePlanOSM resolves it
|
||||
{
|
||||
registerTownCache('regtest', mkCache(cluster(14)));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user