Spaces:
Running
feat(city): the world model as a place you can walk
Browse files/ui/world already answered what the mesh is — 72 grid nodes, the frequencies on
them, the provenance edges that fed them, and a coverage block saying which of
those facts are observed and which are not modeled at all. All true, and
unwalkable. You asked to explore the city, not inspect its JSON.
grid node -> district (all 72, including the ones carrying nothing)
frequency -> building (its height is its signal count, nothing else)
signal freshness -> its lights (fresh/dim/dark, from world_state's own
age_s / interval_s — not invented here)
provenance edge -> a road in (from the origin that actually fed it)
agent -> a resident
core/city_skin.py adds no facts to the world model. The rule that keeps it a
map instead of a mockup: an aspect marked not_modeled is drawn as unavailable,
never as empty and never as full. residency and lease are not modeled, so the
city has no tenancy and says so on the page, struck through, in the world
model's own words.
Four skins over one projection, not four datasets: Civic, Radio, Organism,
Skinless. Skinless is the control — no metaphor, the model's own vocabulary —
and the plurals are spelled out because "frequencys" and "72 nucleus" read as a
mockup too.
The skyline is scaled once for the whole city rather than per district, or a
lone 2-signal building draws exactly as tall as a 100-signal one and the shape
says nothing. Projection is cached on the world's generated_at: same world,
same city, no rebuild.
Routes: /ui/city (the map), /ui/city.json (the projection), /ui/skins (what the
skins are and what they refuse to draw).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XGo2kuduWZMmEZGwKVNryU
- core/city_skin.py +289 -0
- signalmesh_api.py +248 -0
|
@@ -0,0 +1,289 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
core/city_skin.py — the world model, laid out as a place you can walk.
|
| 3 |
+
|
| 4 |
+
/ui/world already answers what the mesh is: 72 grid nodes, the frequencies
|
| 5 |
+
sitting on them, the provenance edges that fed them, and — the part that
|
| 6 |
+
matters — a coverage block saying which of those facts are *observed* and which
|
| 7 |
+
are *not modeled at all*. That JSON is true and unwalkable.
|
| 8 |
+
|
| 9 |
+
This turns it into a city without adding a single fact to it:
|
| 10 |
+
|
| 11 |
+
grid node → district (all 72, including the empty ones)
|
| 12 |
+
frequency → building (its size is its signal count, nothing else)
|
| 13 |
+
signal freshness → its lights (fresh / dim / dark, from world_state's own
|
| 14 |
+
age_s ÷ interval_s — not invented here)
|
| 15 |
+
provenance edge → a road in (from the origin that actually fed it)
|
| 16 |
+
agent → a resident
|
| 17 |
+
|
| 18 |
+
And the rule that makes it honest: an aspect the world model marks
|
| 19 |
+
`not_modeled` is drawn as unavailable, never as empty and never as full.
|
| 20 |
+
`residency` and `lease` are not modeled, so this city has no residents-per-
|
| 21 |
+
building count and no tenancy — because inventing one is how a map starts lying.
|
| 22 |
+
|
| 23 |
+
Skins are four readings of this one projection, not four datasets.
|
| 24 |
+
"""
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
import time
|
| 28 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 29 |
+
|
| 30 |
+
SCHEMA = "signalmesh.city-skin/v1"
|
| 31 |
+
|
| 32 |
+
ROWS, COLS = 9, 8
|
| 33 |
+
GRID_CELLS = ROWS * COLS # 72, same grid as /api/grid
|
| 34 |
+
ROW_LETTERS = "ALIODQBGU" # the dashboard's own row naming
|
| 35 |
+
|
| 36 |
+
# Plurals are spelled out, not derived: "frequency" + "s" is "frequencys", and
|
| 37 |
+
# more than one nucleus is nuclei. A map that misspells its own legend reads as
|
| 38 |
+
# a mockup.
|
| 39 |
+
SKINS = {
|
| 40 |
+
"civic": {
|
| 41 |
+
"label": "Civic",
|
| 42 |
+
"district": "district", "building": "building",
|
| 43 |
+
"road": "road", "resident": "resident",
|
| 44 |
+
"plural": {"district": "districts", "building": "buildings",
|
| 45 |
+
"road": "roads", "resident": "residents"},
|
| 46 |
+
"blurb": "Districts and buildings. Lit windows are fresh signals.",
|
| 47 |
+
},
|
| 48 |
+
"radio": {
|
| 49 |
+
"label": "Radio",
|
| 50 |
+
"district": "band", "building": "station",
|
| 51 |
+
"road": "feed", "resident": "listener",
|
| 52 |
+
"plural": {"district": "bands", "building": "stations",
|
| 53 |
+
"road": "feeds", "resident": "listeners"},
|
| 54 |
+
"blurb": "Bands and stations. Loud stations are carrying the most.",
|
| 55 |
+
},
|
| 56 |
+
"organism": {
|
| 57 |
+
"label": "Organism",
|
| 58 |
+
"district": "tissue", "building": "cell",
|
| 59 |
+
"road": "vessel", "resident": "nucleus",
|
| 60 |
+
"plural": {"district": "tissues", "building": "cells",
|
| 61 |
+
"road": "vessels", "resident": "nuclei"},
|
| 62 |
+
"blurb": "Tissues and cells. A dark cell has stopped metabolising.",
|
| 63 |
+
},
|
| 64 |
+
"skinless": {
|
| 65 |
+
"label": "Skinless",
|
| 66 |
+
"district": "grid node", "building": "frequency",
|
| 67 |
+
"road": "provenance edge", "resident": "agent",
|
| 68 |
+
"plural": {"district": "grid nodes", "building": "frequencies",
|
| 69 |
+
"road": "provenance edges", "resident": "agents"},
|
| 70 |
+
"blurb": "No metaphor. The world model's own words.",
|
| 71 |
+
},
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def coord_label(index: int) -> str:
|
| 76 |
+
"""Grid index → the dashboard's coordinate, e.g. 52 → 'B5'."""
|
| 77 |
+
row, col = divmod(index, COLS)
|
| 78 |
+
return f"{ROW_LETTERS[row % len(ROW_LETTERS)]}{col + 1}"
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def _health(entry: dict) -> str:
|
| 82 |
+
state = ((entry or {}).get("health") or {}).get("state")
|
| 83 |
+
return state if state in ("fresh", "dim", "dark") else "unknown"
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _district_state(buildings: List[dict]) -> str:
|
| 87 |
+
"""A district is as alive as its liveliest building, and empty if it has none.
|
| 88 |
+
|
| 89 |
+
Deliberately not an average: one fresh frequency on a node means that node
|
| 90 |
+
is receiving, and averaging it against dark neighbours would hide that.
|
| 91 |
+
"""
|
| 92 |
+
if not buildings:
|
| 93 |
+
return "empty"
|
| 94 |
+
states = {b["state"] for b in buildings}
|
| 95 |
+
for s in ("fresh", "dim", "dark"):
|
| 96 |
+
if s in states:
|
| 97 |
+
return s
|
| 98 |
+
return "unknown"
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def build(world: Dict[str, Any]) -> Dict[str, Any]:
|
| 102 |
+
"""Project a world-state document into a walkable city. Adds no facts."""
|
| 103 |
+
frequencies = world.get("frequencies") or []
|
| 104 |
+
nodes = world.get("nodes") or []
|
| 105 |
+
edges = world.get("provenance_edges") or []
|
| 106 |
+
coverage = world.get("coverage") or {}
|
| 107 |
+
|
| 108 |
+
# Roads arrive at a frequency; the frequency knows its district.
|
| 109 |
+
node_of: Dict[str, int] = {f.get("frequency"): f.get("node")
|
| 110 |
+
for f in frequencies if f.get("frequency") is not None}
|
| 111 |
+
|
| 112 |
+
districts: List[Dict[str, Any]] = [{
|
| 113 |
+
"index": i, "coord": coord_label(i),
|
| 114 |
+
"row": i // COLS, "col": i % COLS,
|
| 115 |
+
"buildings": [], "residents": [], "roads": [],
|
| 116 |
+
"signal_count": 0,
|
| 117 |
+
} for i in range(GRID_CELLS)]
|
| 118 |
+
|
| 119 |
+
for f in frequencies:
|
| 120 |
+
idx = f.get("node")
|
| 121 |
+
if not isinstance(idx, int) or not 0 <= idx < GRID_CELLS:
|
| 122 |
+
continue
|
| 123 |
+
count = int(f.get("signal_count") or 0)
|
| 124 |
+
districts[idx]["buildings"].append({
|
| 125 |
+
"name": f.get("frequency"),
|
| 126 |
+
"signals": count,
|
| 127 |
+
"state": _health(f),
|
| 128 |
+
"age_s": f.get("latest_age_s"),
|
| 129 |
+
"kind": f.get("latest_type") or "",
|
| 130 |
+
"source": f.get("source") or "",
|
| 131 |
+
})
|
| 132 |
+
districts[idx]["signal_count"] += count
|
| 133 |
+
|
| 134 |
+
for n in nodes:
|
| 135 |
+
if n.get("role") != "agent":
|
| 136 |
+
continue
|
| 137 |
+
idx = n.get("node")
|
| 138 |
+
if isinstance(idx, int) and 0 <= idx < GRID_CELLS:
|
| 139 |
+
districts[idx]["residents"].append({
|
| 140 |
+
"id": n.get("id"), "state": _health(n),
|
| 141 |
+
"source": n.get("source") or "",
|
| 142 |
+
})
|
| 143 |
+
|
| 144 |
+
inbound = 0
|
| 145 |
+
for e in edges:
|
| 146 |
+
idx = node_of.get(e.get("to"))
|
| 147 |
+
if not isinstance(idx, int) or not 0 <= idx < GRID_CELLS:
|
| 148 |
+
continue
|
| 149 |
+
districts[idx]["roads"].append({
|
| 150 |
+
"from": e.get("from"), "to": e.get("to"), "kind": e.get("kind") or "",
|
| 151 |
+
"evidence": e.get("evidence") or {},
|
| 152 |
+
})
|
| 153 |
+
inbound += 1
|
| 154 |
+
|
| 155 |
+
for d in districts:
|
| 156 |
+
d["buildings"].sort(key=lambda b: (-b["signals"], str(b["name"])))
|
| 157 |
+
d["state"] = _district_state(d["buildings"])
|
| 158 |
+
|
| 159 |
+
# The honesty panel: what this city is allowed to draw, in the world
|
| 160 |
+
# model's own words. `not_modeled` is shown as unavailable, not as zero.
|
| 161 |
+
legend = {
|
| 162 |
+
aspect: {"availability": (info or {}).get("availability", "unknown"),
|
| 163 |
+
"drawable": (info or {}).get("availability") in ("native", "derived"),
|
| 164 |
+
"note": (info or {}).get("caveat") or (info or {}).get("note") or ""}
|
| 165 |
+
for aspect, info in coverage.items()
|
| 166 |
+
}
|
| 167 |
+
undrawable = sorted(a for a, i in legend.items() if not i["drawable"])
|
| 168 |
+
|
| 169 |
+
lit = sum(1 for d in districts if d["state"] in ("fresh", "dim"))
|
| 170 |
+
return {
|
| 171 |
+
"schema": SCHEMA,
|
| 172 |
+
"built_at": time.time(),
|
| 173 |
+
"world_at": world.get("generated_at"),
|
| 174 |
+
"world_schema": world.get("schema"),
|
| 175 |
+
"grid": {"rows": ROWS, "cols": COLS, "cells": GRID_CELLS},
|
| 176 |
+
"districts": districts,
|
| 177 |
+
"skins": SKINS,
|
| 178 |
+
"legend": legend,
|
| 179 |
+
"not_drawable": undrawable,
|
| 180 |
+
"totals": {
|
| 181 |
+
"districts_total": GRID_CELLS,
|
| 182 |
+
"districts_occupied": sum(1 for d in districts if d["buildings"]),
|
| 183 |
+
"districts_lit": lit,
|
| 184 |
+
"buildings": sum(len(d["buildings"]) for d in districts),
|
| 185 |
+
"residents": sum(len(d["residents"]) for d in districts),
|
| 186 |
+
"roads": inbound,
|
| 187 |
+
"roads_unplaced": len(edges) - inbound,
|
| 188 |
+
"signals": sum(d["signal_count"] for d in districts),
|
| 189 |
+
},
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
# ── cache ────────────────────────────────────────────────────────────────────
|
| 194 |
+
# The projection is pure, so it only has to be redone when the world changes.
|
| 195 |
+
# Keyed on the world's own generated_at: a fresh world is a cache miss, the
|
| 196 |
+
# same world served twice is not.
|
| 197 |
+
|
| 198 |
+
_CACHE: Dict[str, Any] = {}
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def build_cached(world: Dict[str, Any]) -> Dict[str, Any]:
|
| 202 |
+
stamp = world.get("generated_at")
|
| 203 |
+
if _CACHE.get("stamp") == stamp and stamp is not None:
|
| 204 |
+
city = dict(_CACHE["city"])
|
| 205 |
+
city["cached"] = True
|
| 206 |
+
return city
|
| 207 |
+
city = build(world)
|
| 208 |
+
city["cached"] = False
|
| 209 |
+
_CACHE.update(stamp=stamp, city=city)
|
| 210 |
+
return city
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
if __name__ == "__main__":
|
| 214 |
+
assert coord_label(0) == "A1" and coord_label(7) == "A8"
|
| 215 |
+
assert coord_label(52) == "B5", coord_label(52) # 52 = row 6, col 4 → 'B','5'
|
| 216 |
+
assert coord_label(71) == "U8"
|
| 217 |
+
|
| 218 |
+
world = {
|
| 219 |
+
"schema": "signalmesh.world-state/v1",
|
| 220 |
+
"generated_at": 1000.0,
|
| 221 |
+
"coverage": {
|
| 222 |
+
"frequency": {"availability": "native"},
|
| 223 |
+
"route": {"availability": "derived"},
|
| 224 |
+
"residency": {"availability": "not_modeled"},
|
| 225 |
+
"lease": {"availability": "not_modeled"},
|
| 226 |
+
},
|
| 227 |
+
"frequencies": [
|
| 228 |
+
{"frequency": "hn_top", "signal_count": 30, "node": 3,
|
| 229 |
+
"latest_age_s": 10.0, "latest_type": "feed_hydrator",
|
| 230 |
+
"health": {"state": "fresh"}, "source": "/ui/frequencies"},
|
| 231 |
+
{"frequency": "old_news", "signal_count": 2, "node": 3,
|
| 232 |
+
"latest_age_s": 99999.0, "health": {"state": "dark"},
|
| 233 |
+
"source": "/ui/frequencies"},
|
| 234 |
+
{"frequency": "offgrid", "signal_count": 5, "node": 999,
|
| 235 |
+
"health": {"state": "fresh"}},
|
| 236 |
+
],
|
| 237 |
+
"nodes": [
|
| 238 |
+
{"id": "hn_top", "role": "frequency", "node": 3},
|
| 239 |
+
{"id": "A1", "role": "agent", "node": 3, "health": {"state": "fresh"}},
|
| 240 |
+
],
|
| 241 |
+
"provenance_edges": [
|
| 242 |
+
{"from": "news.ycombinator.com", "to": "hn_top", "kind": "feed_origin",
|
| 243 |
+
"evidence": {"observed_items": 37}},
|
| 244 |
+
{"from": "nowhere", "to": "not_a_frequency", "kind": "feed_origin"},
|
| 245 |
+
],
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
city = build(world)
|
| 249 |
+
assert len(city["districts"]) == 72, "every grid node is a district, empty or not"
|
| 250 |
+
|
| 251 |
+
d3 = city["districts"][3]
|
| 252 |
+
assert [b["name"] for b in d3["buildings"]] == ["hn_top", "old_news"], d3["buildings"]
|
| 253 |
+
assert d3["signal_count"] == 32 and d3["coord"] == "A4"
|
| 254 |
+
# one fresh building keeps the district alive even beside a dark one
|
| 255 |
+
assert d3["state"] == "fresh", d3["state"]
|
| 256 |
+
assert [r["id"] for r in d3["residents"]] == ["A1"]
|
| 257 |
+
assert len(d3["roads"]) == 1 and d3["roads"][0]["from"] == "news.ycombinator.com"
|
| 258 |
+
|
| 259 |
+
assert city["districts"][0]["state"] == "empty" and not city["districts"][0]["buildings"]
|
| 260 |
+
|
| 261 |
+
t = city["totals"]
|
| 262 |
+
assert t["buildings"] == 2, "a frequency off the grid is not placed on it"
|
| 263 |
+
assert t["roads"] == 1 and t["roads_unplaced"] == 1, t
|
| 264 |
+
assert t["districts_occupied"] == 1 and t["districts_lit"] == 1
|
| 265 |
+
assert t["signals"] == 32 and t["residents"] == 1
|
| 266 |
+
|
| 267 |
+
# the honesty rule: what the world does not model, the city must not draw
|
| 268 |
+
assert city["not_drawable"] == ["lease", "residency"], city["not_drawable"]
|
| 269 |
+
assert city["legend"]["frequency"]["drawable"] is True
|
| 270 |
+
assert city["legend"]["residency"]["drawable"] is False
|
| 271 |
+
|
| 272 |
+
assert build_cached(world)["cached"] is False
|
| 273 |
+
assert build_cached(world)["cached"] is True, "same world must not be rebuilt"
|
| 274 |
+
moved = dict(world, generated_at=2000.0)
|
| 275 |
+
assert build_cached(moved)["cached"] is False, "a new world is a new city"
|
| 276 |
+
|
| 277 |
+
# every skin must name all four things, singular and plural
|
| 278 |
+
for name, skin in SKINS.items():
|
| 279 |
+
for role in ("district", "building", "road", "resident"):
|
| 280 |
+
assert skin.get(role), (name, role)
|
| 281 |
+
assert skin["plural"].get(role), (name, role, "plural")
|
| 282 |
+
assert SKINS["skinless"]["plural"]["building"] == "frequencies"
|
| 283 |
+
assert SKINS["organism"]["plural"]["resident"] == "nuclei"
|
| 284 |
+
|
| 285 |
+
empty = build({})
|
| 286 |
+
assert len(empty["districts"]) == 72 and empty["totals"]["buildings"] == 0
|
| 287 |
+
assert all(d["state"] == "empty" for d in empty["districts"])
|
| 288 |
+
|
| 289 |
+
print("city_skin self-check OK")
|
|
@@ -42,6 +42,7 @@ from core.source_hydrator import source_hydrator, is_fetchable, resolve as resol
|
|
| 42 |
from core import mport_articles
|
| 43 |
from core import binder
|
| 44 |
from core import world_state
|
|
|
|
| 45 |
from core import broadcast_journal
|
| 46 |
from core.antenna import AntennaForge, TARGETS as ANTENNA_TARGETS
|
| 47 |
from core.manifest_parser import manifest_registry, ManifestParser
|
|
@@ -904,6 +905,253 @@ def ui_world_state():
|
|
| 904 |
"""
|
| 905 |
return _do_world_state()
|
| 906 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 907 |
@app.get("/identity", tags=["Core"], summary="Which copy of SignalMesh is canonical")
|
| 908 |
def public_identity():
|
| 909 |
"""Keyless. The centrally adjudicated record for SignalMesh itself.
|
|
|
|
| 42 |
from core import mport_articles
|
| 43 |
from core import binder
|
| 44 |
from core import world_state
|
| 45 |
+
from core import city_skin
|
| 46 |
from core import broadcast_journal
|
| 47 |
from core.antenna import AntennaForge, TARGETS as ANTENNA_TARGETS
|
| 48 |
from core.manifest_parser import manifest_registry, ManifestParser
|
|
|
|
| 905 |
"""
|
| 906 |
return _do_world_state()
|
| 907 |
|
| 908 |
+
|
| 909 |
+
_CITY_HTML = """<!DOCTYPE html><html lang="en" data-skin="civic"><head>
|
| 910 |
+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
| 911 |
+
<title>SignalMesh · The City</title>
|
| 912 |
+
<style>
|
| 913 |
+
*{margin:0;padding:0;box-sizing:border-box}
|
| 914 |
+
:root{
|
| 915 |
+
--bg:#070a10; --panel:#0d1219; --line:#1e2836; --ink:#e8eef7; --dim:#7f8 da0;
|
| 916 |
+
--dim:#7d8ba0; --lit:#ffce6b; --lit2:#ff9d3c; --dark:#243040; --road:#2b3a52;
|
| 917 |
+
--accent:#ffce6b; --glow:rgba(255,206,107,.30); --tile:#101823; --grid-gap:6px;
|
| 918 |
+
--radius:4px;
|
| 919 |
+
}
|
| 920 |
+
html[data-skin=radio]{--lit:#5df2a0;--lit2:#22c98a;--accent:#5df2a0;
|
| 921 |
+
--glow:rgba(93,242,160,.28);--tile:#0a1512;--road:#1d4438;--radius:2px}
|
| 922 |
+
html[data-skin=organism]{--lit:#7ad8ff;--lit2:#3aa8e0;--accent:#7ad8ff;
|
| 923 |
+
--glow:rgba(122,216,255,.28);--tile:#0a1520;--road:#1c3a52;--radius:50%}
|
| 924 |
+
html[data-skin=skinless]{--lit:#cbd5e1;--lit2:#94a3b8;--accent:#cbd5e1;
|
| 925 |
+
--glow:transparent;--tile:#0e131a;--road:#2a3340;--radius:0}
|
| 926 |
+
body{background:var(--bg);color:var(--ink);
|
| 927 |
+
font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
|
| 928 |
+
min-height:100vh;display:flex;flex-direction:column}
|
| 929 |
+
a{color:var(--accent)}
|
| 930 |
+
header{display:flex;gap:14px;align-items:center;flex-wrap:wrap;
|
| 931 |
+
padding:12px 18px;border-bottom:1px solid var(--line);background:var(--panel)}
|
| 932 |
+
h1{font-size:15px;font-weight:650;letter-spacing:.02em}
|
| 933 |
+
h1 small{display:block;font-size:10.5px;color:var(--dim);font-weight:400;letter-spacing:.06em;
|
| 934 |
+
text-transform:uppercase}
|
| 935 |
+
.skins{display:flex;gap:5px;margin-left:auto;flex-wrap:wrap}
|
| 936 |
+
.skins button{background:transparent;border:1px solid var(--line);color:var(--dim);
|
| 937 |
+
padding:6px 13px;border-radius:99px;font-size:12px;cursor:pointer;font-family:inherit}
|
| 938 |
+
.skins button[aria-pressed=true]{border-color:var(--accent);color:var(--accent);
|
| 939 |
+
box-shadow:0 0 14px var(--glow)}
|
| 940 |
+
.blurb{width:100%;font-size:11.5px;color:var(--dim);order:9}
|
| 941 |
+
.totals{display:flex;gap:16px;flex-wrap:wrap;padding:9px 18px;
|
| 942 |
+
border-bottom:1px solid var(--line);font-size:11px;color:var(--dim)}
|
| 943 |
+
.totals b{color:var(--ink);font-weight:600}
|
| 944 |
+
main{flex:1;display:grid;grid-template-columns:minmax(340px,1.15fr) minmax(300px,.85fr);
|
| 945 |
+
gap:0;align-items:start}
|
| 946 |
+
@media(max-width:880px){main{grid-template-columns:1fr}}
|
| 947 |
+
#map{padding:18px}
|
| 948 |
+
.grid{display:grid;grid-template-columns:repeat(8,1fr);gap:var(--grid-gap)}
|
| 949 |
+
.district{background:var(--tile);border:1px solid var(--line);border-radius:var(--radius);
|
| 950 |
+
aspect-ratio:1;padding:5px;cursor:pointer;position:relative;overflow:hidden;
|
| 951 |
+
display:flex;flex-direction:column;justify-content:flex-end;gap:2px;
|
| 952 |
+
transition:transform .12s,border-color .12s,box-shadow .12s}
|
| 953 |
+
.district:hover{transform:translateY(-2px);border-color:var(--accent)}
|
| 954 |
+
.district[aria-current=true]{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent),0 0 18px var(--glow)}
|
| 955 |
+
.district .coord{position:absolute;top:4px;left:5px;font-size:8.5px;color:var(--dim);
|
| 956 |
+
letter-spacing:.05em;font-family:ui-monospace,monospace}
|
| 957 |
+
.district.empty{opacity:.42}
|
| 958 |
+
.bars{display:flex;align-items:flex-end;gap:2px;height:100%;padding-top:12px}
|
| 959 |
+
.bar{flex:1;min-width:2px;background:var(--dark);border-radius:1px 1px 0 0}
|
| 960 |
+
.bar.fresh{background:linear-gradient(180deg,var(--lit),var(--lit2));box-shadow:0 0 7px var(--glow)}
|
| 961 |
+
.bar.dim{background:var(--lit2);opacity:.5}
|
| 962 |
+
.bar.dark{background:var(--dark)}
|
| 963 |
+
html[data-skin=organism] .bar{border-radius:99px}
|
| 964 |
+
#side{padding:18px 18px 18px 0;position:sticky;top:0;max-height:100vh;overflow:auto}
|
| 965 |
+
@media(max-width:880px){#side{padding:0 18px 18px;position:static;max-height:none}}
|
| 966 |
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:8px;
|
| 967 |
+
padding:14px;margin-bottom:12px}
|
| 968 |
+
.card h2{font-size:12px;letter-spacing:.07em;text-transform:uppercase;color:var(--dim);
|
| 969 |
+
font-weight:600;margin-bottom:9px}
|
| 970 |
+
.bldg{display:flex;align-items:baseline;gap:8px;padding:6px 0;border-bottom:1px solid var(--line);
|
| 971 |
+
font-size:12.5px}
|
| 972 |
+
.bldg:last-child{border-bottom:0}
|
| 973 |
+
.bldg .nm{flex:1;font-family:ui-monospace,monospace;overflow:hidden;text-overflow:ellipsis;
|
| 974 |
+
white-space:nowrap}
|
| 975 |
+
.bldg .ct{color:var(--dim);font-size:11px}
|
| 976 |
+
.dot{width:7px;height:7px;border-radius:50%;flex:none;background:var(--dark)}
|
| 977 |
+
.dot.fresh{background:var(--lit);box-shadow:0 0 7px var(--glow)}
|
| 978 |
+
.dot.dim{background:var(--lit2);opacity:.6}
|
| 979 |
+
.road{font-size:11.5px;color:var(--dim);padding:4px 0;font-family:ui-monospace,monospace;
|
| 980 |
+
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
| 981 |
+
.road b{color:var(--ink);font-weight:500}
|
| 982 |
+
.pill{display:inline-block;font-size:10px;padding:1px 7px;border-radius:99px;
|
| 983 |
+
border:1px solid var(--line);color:var(--dim);margin:2px 3px 2px 0}
|
| 984 |
+
.unavail{opacity:.55;text-decoration:line-through}
|
| 985 |
+
.empty-note{color:var(--dim);font-size:12px;font-style:italic}
|
| 986 |
+
.legend-row{display:flex;justify-content:space-between;gap:10px;font-size:11.5px;
|
| 987 |
+
padding:3px 0;border-bottom:1px solid var(--line)}
|
| 988 |
+
.legend-row:last-child{border-bottom:0}
|
| 989 |
+
.legend-row .v{color:var(--dim);font-family:ui-monospace,monospace;font-size:10.5px}
|
| 990 |
+
.key{display:flex;gap:12px;flex-wrap:wrap;font-size:11px;color:var(--dim);margin-top:10px}
|
| 991 |
+
.key span{display:flex;align-items:center;gap:5px}
|
| 992 |
+
</style></head><body>
|
| 993 |
+
<header>
|
| 994 |
+
<h1>SignalMesh<small id="skin-sub">The City</small></h1>
|
| 995 |
+
<nav class="skins" id="skins"></nav>
|
| 996 |
+
<div class="blurb" id="blurb"></div>
|
| 997 |
+
</header>
|
| 998 |
+
<div class="totals" id="totals"></div>
|
| 999 |
+
<main>
|
| 1000 |
+
<div id="map">
|
| 1001 |
+
<div class="grid" id="grid"></div>
|
| 1002 |
+
<div class="key">
|
| 1003 |
+
<span><i class="dot fresh"></i> fresh</span>
|
| 1004 |
+
<span><i class="dot dim"></i> dim</span>
|
| 1005 |
+
<span><i class="dot"></i> dark</span>
|
| 1006 |
+
<span>faded = no <b id="k-bldg">buildings</b></span>
|
| 1007 |
+
<span id="k-src"></span>
|
| 1008 |
+
</div>
|
| 1009 |
+
</div>
|
| 1010 |
+
<aside id="side"></aside>
|
| 1011 |
+
</main>
|
| 1012 |
+
<script>
|
| 1013 |
+
const $=id=>document.getElementById(id);
|
| 1014 |
+
const esc=s=>String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
| 1015 |
+
let CITY=null, SKIN='civic', PICKED=null;
|
| 1016 |
+
|
| 1017 |
+
const age=s=>{ if(s==null)return '';
|
| 1018 |
+
s=Math.round(s); if(s<60)return s+'s'; if(s<3600)return Math.round(s/60)+'m';
|
| 1019 |
+
if(s<86400)return Math.round(s/3600)+'h'; return Math.round(s/86400)+'d'; };
|
| 1020 |
+
const noun=k=>(CITY&&CITY.skins[SKIN]&&CITY.skins[SKIN][k])||k;
|
| 1021 |
+
const plural=k=>((CITY&&CITY.skins[SKIN]&&CITY.skins[SKIN].plural||{})[k])||noun(k);
|
| 1022 |
+
|
| 1023 |
+
function paintSkins(){
|
| 1024 |
+
$('skins').innerHTML=Object.entries(CITY.skins).map(([k,v])=>
|
| 1025 |
+
`<button data-skin="${k}" aria-pressed="${k===SKIN}">${esc(v.label)}</button>`).join('');
|
| 1026 |
+
$('skins').querySelectorAll('button').forEach(b=>b.onclick=()=>{
|
| 1027 |
+
SKIN=b.dataset.skin; document.documentElement.dataset.skin=SKIN;
|
| 1028 |
+
paintSkins(); paintTotals(); paintGrid(); paintSide();
|
| 1029 |
+
});
|
| 1030 |
+
$('blurb').textContent=CITY.skins[SKIN].blurb;
|
| 1031 |
+
$('skin-sub').textContent=CITY.skins[SKIN].label;
|
| 1032 |
+
$('k-bldg').textContent=plural('building');
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
function paintTotals(){
|
| 1036 |
+
const t=CITY.totals;
|
| 1037 |
+
$('totals').innerHTML=[
|
| 1038 |
+
[`<b>${t.districts_lit}</b>/${t.districts_total}`, plural('district')+' lit'],
|
| 1039 |
+
[`<b>${t.buildings}</b>`, plural('building')],
|
| 1040 |
+
[`<b>${t.signals.toLocaleString()}</b>`, 'signals'],
|
| 1041 |
+
[`<b>${t.roads}</b>`, plural('road')],
|
| 1042 |
+
[`<b>${t.residents}</b>`, plural('resident')],
|
| 1043 |
+
].map(([v,l])=>`<span>${v} ${esc(l)}</span>`).join('')
|
| 1044 |
+
+ `<span style="margin-left:auto">world @ ${new Date((CITY.world_at||0)*1000)
|
| 1045 |
+
.toLocaleTimeString()}${CITY.cached?' · cached':''}</span>`;
|
| 1046 |
+
}
|
| 1047 |
+
|
| 1048 |
+
function paintGrid(){
|
| 1049 |
+
// One scale for the whole city, not one per district — otherwise a lone
|
| 1050 |
+
// 2-signal building draws exactly as tall as a 100-signal one and the
|
| 1051 |
+
// skyline says nothing. sqrt so the long tail stays visible next to the peaks.
|
| 1052 |
+
const peak=Math.max(1,...CITY.districts.flatMap(d=>d.buildings.map(b=>b.signals)));
|
| 1053 |
+
const h=v=>Math.max(7,Math.round(Math.sqrt(Math.max(v,0))/Math.sqrt(peak)*100));
|
| 1054 |
+
$('grid').innerHTML=CITY.districts.map(d=>{
|
| 1055 |
+
const bars=d.buildings.slice(0,7);
|
| 1056 |
+
const inner=bars.length
|
| 1057 |
+
? `<div class="bars">`+bars.map(b=>
|
| 1058 |
+
`<i class="bar ${b.state}" style="height:${h(b.signals)}%"
|
| 1059 |
+
title="${esc(b.name)} · ${b.signals}"></i>`).join('')+`</div>`
|
| 1060 |
+
: '';
|
| 1061 |
+
return `<div class="district ${d.buildings.length?'':'empty'}" data-i="${d.index}"
|
| 1062 |
+
aria-current="${PICKED===d.index}" tabindex="0"
|
| 1063 |
+
title="${esc(d.coord)} · ${d.buildings.length} ${esc(plural('building'))}">
|
| 1064 |
+
<span class="coord">${esc(d.coord)}</span>${inner}</div>`;
|
| 1065 |
+
}).join('');
|
| 1066 |
+
$('grid').querySelectorAll('.district').forEach(el=>{
|
| 1067 |
+
const go=()=>{PICKED=+el.dataset.i;paintGrid();paintSide();
|
| 1068 |
+
$('side').scrollIntoView({block:'nearest',behavior:'smooth'})};
|
| 1069 |
+
el.onclick=go;
|
| 1070 |
+
el.onkeydown=e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();go()}};
|
| 1071 |
+
});
|
| 1072 |
+
}
|
| 1073 |
+
|
| 1074 |
+
function paintSide(){
|
| 1075 |
+
const legend=`<div class="card"><h2>What this map may draw</h2>`+
|
| 1076 |
+
Object.entries(CITY.legend).map(([k,v])=>
|
| 1077 |
+
`<div class="legend-row ${v.drawable?'':'unavail'}">
|
| 1078 |
+
<span>${esc(k)}</span><span class="v">${esc(v.availability)}</span></div>`).join('')+
|
| 1079 |
+
(CITY.not_drawable.length
|
| 1080 |
+
? `<p style="margin-top:9px;font-size:11.5px;color:var(--dim)">
|
| 1081 |
+
Struck through = the world model does not carry it, so nothing here
|
| 1082 |
+
invents it: <b>${CITY.not_drawable.map(esc).join(', ')}</b>.</p>` : '')+
|
| 1083 |
+
`</div>`;
|
| 1084 |
+
|
| 1085 |
+
if(PICKED==null){
|
| 1086 |
+
$('side').innerHTML=`<div class="card"><h2>Pick a ${esc(noun('district'))}</h2>
|
| 1087 |
+
<p class="empty-note">All ${CITY.grid.cells} grid nodes are here, including the
|
| 1088 |
+
${CITY.totals.districts_total-CITY.totals.districts_occupied} carrying nothing.</p></div>`+legend;
|
| 1089 |
+
return;
|
| 1090 |
+
}
|
| 1091 |
+
const d=CITY.districts[PICKED];
|
| 1092 |
+
const bl=d.buildings.length?d.buildings.map(b=>
|
| 1093 |
+
`<div class="bldg"><i class="dot ${b.state}"></i>
|
| 1094 |
+
<span class="nm">${esc(b.name)}</span>
|
| 1095 |
+
<span class="ct">${b.signals} · ${esc(age(b.age_s))}</span></div>`).join('')
|
| 1096 |
+
: `<p class="empty-note">No ${esc(plural('building'))} on this node — it is
|
| 1097 |
+
addressable and silent, not missing.</p>`;
|
| 1098 |
+
const rd=d.roads.length?d.roads.map(r=>
|
| 1099 |
+
`<div class="road">${esc(r.from)} <b>→</b> ${esc(r.to)}
|
| 1100 |
+
<span class="pill">${esc(r.kind)}</span></div>`).join('')
|
| 1101 |
+
: `<p class="empty-note">No observed inbound ${esc(plural('road'))}.</p>`;
|
| 1102 |
+
const rs=d.residents.length?d.residents.map(r=>
|
| 1103 |
+
`<span class="pill">${esc(r.id)}</span>`).join('')
|
| 1104 |
+
: `<p class="empty-note">No ${esc(plural('resident'))} recorded here.</p>`;
|
| 1105 |
+
|
| 1106 |
+
$('side').innerHTML=`
|
| 1107 |
+
<div class="card">
|
| 1108 |
+
<h2>${esc(noun('district'))} ${esc(d.coord)} <span style="color:var(--accent)">·
|
| 1109 |
+
${esc(d.state)}</span></h2>
|
| 1110 |
+
<div style="font-size:11.5px;color:var(--dim);margin-bottom:8px">
|
| 1111 |
+
grid node ${d.index} · row ${d.row} col ${d.col} · ${d.signal_count} signals</div>
|
| 1112 |
+
${bl}
|
| 1113 |
+
</div>
|
| 1114 |
+
<div class="card"><h2>${esc(plural('road'))} in</h2>${rd}</div>
|
| 1115 |
+
<div class="card"><h2>${esc(plural('resident'))}</h2>${rs}</div>`+legend;
|
| 1116 |
+
}
|
| 1117 |
+
|
| 1118 |
+
async function load(){
|
| 1119 |
+
try{
|
| 1120 |
+
const r=await fetch('/ui/city.json');
|
| 1121 |
+
CITY=await r.json();
|
| 1122 |
+
}catch(e){
|
| 1123 |
+
$('map').innerHTML='<p class="empty-note">Could not reach the mesh.</p>';return;
|
| 1124 |
+
}
|
| 1125 |
+
$('k-src').innerHTML='every fact from <a href="/ui/world">/ui/world</a>';
|
| 1126 |
+
paintSkins(); paintTotals(); paintGrid(); paintSide();
|
| 1127 |
+
}
|
| 1128 |
+
load();
|
| 1129 |
+
setInterval(load,30000);
|
| 1130 |
+
</script></body></html>"""
|
| 1131 |
+
|
| 1132 |
+
|
| 1133 |
+
# ── The city: the same world model, as a place you can walk ──────────────────
|
| 1134 |
+
|
| 1135 |
+
@app.get("/ui/skins", tags=["Core"], summary="Skins available over the world model")
|
| 1136 |
+
def ui_skins():
|
| 1137 |
+
"""Four readings of one projection — and what every one of them refuses to draw."""
|
| 1138 |
+
city = city_skin.build_cached(_do_world_state())
|
| 1139 |
+
return {"schema": city_skin.SCHEMA, "skins": city_skin.SKINS,
|
| 1140 |
+
"legend": city["legend"], "not_drawable": city["not_drawable"],
|
| 1141 |
+
"totals": city["totals"]}
|
| 1142 |
+
|
| 1143 |
+
|
| 1144 |
+
@app.get("/ui/city.json", tags=["Core"], summary="The world model, laid out as a city")
|
| 1145 |
+
def ui_city_json():
|
| 1146 |
+
"""Districts, buildings, roads, residents — every fact traceable to /ui/world."""
|
| 1147 |
+
return city_skin.build_cached(_do_world_state())
|
| 1148 |
+
|
| 1149 |
+
|
| 1150 |
+
@app.get("/ui/city", response_class=HTMLResponse, include_in_schema=False)
|
| 1151 |
+
def ui_city():
|
| 1152 |
+
"""Walk the mesh instead of reading its JSON."""
|
| 1153 |
+
return HTMLResponse(_CITY_HTML)
|
| 1154 |
+
|
| 1155 |
@app.get("/identity", tags=["Core"], summary="Which copy of SignalMesh is canonical")
|
| 1156 |
def public_identity():
|
| 1157 |
"""Keyless. The centrally adjudicated record for SignalMesh itself.
|