Bronze Age Near East. Generated from ROADMAP.md and data/.
Era rail preview. Segment width is railWeight, derived from
actual band membership in places.geojson — not hand-authored (invariant 10).
Numbers are places attested in each band.
tell repo, push index.html + src/main.jsdocs/DECISIONS.md D1mapView.js: init map centered on Mesopotamia, flat projectionmanifest.json + sw.js with CACHE_VERSIONdocs/DECISIONS.md D5Gate production URL loads a blank basemap in both projections, zero console errors.
Status deployed and passing locally. Verified in headless Chromium against the pinned MapLibre build: map loads, flat ⇄ globe ⇄ flat all correct, service worker registers, place markers render in both projections, and zero console errors, page errors or failed requests. The graticule is rectilinear in flat and visibly curved in globe, confirming the toggle does real work rather than reporting that it did. Still owed by a human: the same pass on the production URL in Safari — the build sandbox has no Safari and cannot reach the deployed site.
docs/CHRONOLOGY.md — middle chronology chosen and documented (resolves SPEC Q2)package.json boundary — "type": "module", zero deps, one gate command (docs/DECISIONS.md D6)docs/LICENSES.md — audited per-source register (47-agent verification). SPEC's table was wrong in 6 rows. Q1 answered: historical-basemaps is GPL-3.0, not CC-BY-SAsrc/graph/schema.js — typedefs + runtime validator, shared by app and ETLetl/fetch-pleiades.js — pull, filter to bbox, cache per-place JSONetl/normalize.js — emit places.geojson; derive railWeight (invariant 10)etl/fetch-periodo.js — periodisation authority, joined to Pleiades via sameAs (falls back to the stale GitHub archive; data.perio.do unreachable from the sandbox)etl/validate.js — schema check, non-empty sources check, exit codes, no --forcedata/places.geojson (target ≥150 records) — 1,043 committed (Phase 7 re-run)Gate node etl/validate.js passes clean. Zero records with empty sources[]. Spot-check five records against Pleiades by hand.
time/bands.js — 7 Bronze Age bands, widths by record densitytime/timeIndex.js — query(year), setBand(), setYear(), onChange; the SOLE temporal authoritytime/eraRail.js — segmented buttons, density-proportional widths (75px Akkadian vs 152px Kassite at desktop width), keyboard ←/→/Home/Endtime/detailTrack.js — in-band scrubbing with fade (P0-2); snapping to attested events deferred — no event data exists yet, and inventing anchor years would fake itplaceGraph to subscribe to timeIndex (injected via bindTime, no import — the dependency is visible in the import graph)#<band>@<year>, cold-load and same-document (hashchange); honest empty-state element wired (invariant 7)scripts/verify-time-authority.mjs — the gate's grep, kept: no year comparison outside src/time/ (documented exemption: the build-time validator in schema.js), and timeIndex importable only by main.js. Three mutations, each caughtGate clicking a band visibly changes map contents in <200ms. Grep the codebase: no year arithmetic outside src/time/.
Status the grep half is mechanical now (verify-time-authority, mutation-tested). Latency measured in headless Chromium under SwiftShader software rendering: first frame with new contents 6–158 ms across six bands, one 229 ms outlier (Kassite, measured mid-sweep with render backlog). Six of seven under the gate on software GL; hardware GPUs should clear it comfortably, but that is expectation, not measurement — worth one check on a real device.
ui/layerPanel.js — 8 toggles with per-band counts; tags with no data yet stay listed as "planned" rather than hidden. geology also governs the historical water, economy the trade routes — the taxonomy means somethinggraph/placeGraph.js — filter by layer tag (untagged records always show: an empty layers[] is legal, invariant 7, and must not become invisible-by-toggle)ui/detailPanel.js for Place records — description prose in-app, clickable typed connections with map lines, references down to the excavation reports; external links demoted to citations (docs/DECISIONS.md D9)ui/detailPanel.js — extend type dispatch to Edge / Text / Artifact as those records landui/emptyState.js — honest sparse-band message (wired in Phase 2, now reachable)Gate all 8 toggles filter without map re-init. Select a deliberately sparse band and confirm the empty state, not a blank canvas.
Watercourse typedef + validator rules in src/graph/schema.js (fifth record type; certainty tier MANDATORY, no default)data/hydrology.geojson — 10 band-snapped states: Euphrates middle course + Terqa reach + ancient eastern course (Kish–Nippur–Uruk–Ur–Eridu) + western Arahtu branch, Iturungal spur to Girsu, Tigris ×2, Gulf shoreline ×2 (contested, marked schematic), Sealand marsh. Ur III canals deferred — no licence-clean canal attestations yetmap/hydrology.js — tier styling: reconstructed dashed, schematic faint/wide/soft; surveyed reserved (solid) and currently EMPTY. Tier paints exported as data so CI can prove they differplaceGraph — not a second timeIndex subscriber (invariants 1, 6). No band selected → no historical water: all-time makes no course claimetl/build-hydrology.js; nothing traces a published figure. Q6 re-arises only if real digitizing starts (would populate the surveyed tier)Gate stepping Ur III → Kassite visibly moves the water, and a reconstructed channel is distinguishable from a surveyed one without clicking. Spot-check every feature's sources.
data/edges.json — 8 edges (target is 20–40; quality over count for the first pass): Ur–Dilmun copper (the Ea-nasir run), Magan direct + transshipped, Meluhha to Sargon's quay, Assur–Kaneš tin caravans, Badakhshan lapis (archaeological tier), Susa tin (inferred tier, and says so). Sources DERIVED from endpoint Pleiades records by etl/build-edges.js; same strict band rule as hydrology — it caught a wrong Susa id and an over-claimed Ur III band on first buildmap/edges.js — animated flow (stepped dash-phase walk, one rAF owner, textual tier only, paused when no edges are on screen, disabled under prefers-reduced-motion). No layer panel exists yet, so edges show per-band rather than behind an economy toggle — Phase 3's layerPanel adds the switchdocs/data-contracts.md — real positions are more honest and make the globe the payoff view)attestation tier visible in the UI — textual/archaeological/inferred drawn differently, explained in the legend, distinctness CI-proven (verify-edges)Gate economy layer on → the trade system reads visually. Show it to someone with no background; if they don't grasp "these places were connected," redesign before moving on.
_redirects for SPA routingcloudflare-deploy checklistdocs/VERIFICATION.md §7etl/fetch-pleiades.js: bbox → 20–60°E / 22–42°N. The planned CSV year pre-filter wasetl/fetch-periodo.js: gap-fill from Pleiades' first-party time_periods.csvetl/bands.source.json: Kassite split at −1200 → kassite (−1595…−1200, labeletl/fetch-basemap.js: Natural Earth clip widened to 14–64°E / 17–47°Ndocs/DATA-COVERAGE.md §8; Phase 1 gate re-judged: metdata/affiliations.json — separate joined file (not a places.geojson mutation), built byscripts/verify-affiliations.mjs — regen-compare, strict band rule against the currentEvent — year (signed int, middle chronology) or range, kindevents (SPEC §4.3 amendment); layerPanel renders it with per-band countsetl/events.source.json + etl/build-events.js: bandIds DERIVED from the year span viascripts/verify-events.mjs — regen-compare, bands-rederived-from-years, strict placesrc/tours/ per OS-6: tourController.js (writes ONLY through timeIndex and the#tour:<slug>:<n>; every step is a history entry so browser back/forwardscripts/verify-tours.mjs: regen-compare, every scene reference resolves (a renamedverify-cache-version now asserts SHELL completeness — every src/**/*.jsGate collapse + events on → the destruction wave reads across the map; Ugarit's destruction opens a sourced account; the detail track snaps.