Documents the post-droplet L4 topology, the per-call NVML energy
ledger, and the production deploy commands so the repo presents
itself the way an open-source civic tech project should — without
breaking any of the existing app structure or deploy paths.
New files
- docs/EMISSIONS.md full pipeline for real GPU power readings:
what's measured vs. estimated, the NVML
sampler in inference-vllm/proxy.py, the
bracket-sample LLM client path, the
hardware fallback table, and how to verify
with scripts/probe_stones_fire.py.
- docs/DEPLOY.md production topology diagram, env-var
reference for both Spaces, per-Space
deploy commands, local-dev recipes.
- CHANGELOG.md release log starting from the v0.5.0
hackathon submission tag.
- CONTRIBUTING.md quickstart + repo structure + house …
feat(emissions): real GPU power from NVML on the L4 inference Space
Drops the cloud-comparison chip and replaces the data-sheet
energy estimate with a real per-call measurement off the L4 GPU.
inference-vllm/proxy.py
- Background sampler reads nvmlDeviceGetPowerUsage every 100 ms into
a 60 s ring buffer. Each forwarded POST records the upstream
wall-clock window and computes the mean power across the matching
samples; results land on the response as X-GPU-Power-W and
X-GPU-Energy-J (joules).
- New GET /v1/power exposes instantaneous + 1 s avg + 5 s avg for
bracket-sampling clients (used by the LLM path, where LiteLLM
hides response headers).
- pynvml init is best-effort: on failure the proxy still serves
traffic, just without power data.
inference-vllm/Dockerfile
- Adds nvidia-ml-py>=12.560 to the proxy's pip layer.
app/emissions.py
- Tracker.record_{llm,ml} accept optional joules_real / power_w_real.
When present, those values land on the row directly and the row is
flagged measured=True. Otherwise we fall back to the data-sheet
…
feat(emissions): per-call inference energy + token tracker
Adds an additive emissions ledger that records every llm.chat call
and every remote app.inference._post call, surfacing the per-query
total Wh / J + token counts on the briefing.
Backend
- app/emissions.py: thread-local Tracker with hardware power profiles
(MI300X 600 W, T4 50 W, Apple M 20 W, x86 30 W) and a summarize()
rollup with by-kind, by-hardware, and frontier-cloud comparison.
- app/llm.py: chat() times each call, picks hardware via engine
detection, reads prompt/completion tokens from LiteLLM usage for
non-stream calls, estimates from response text for streams.
- app/inference.py: _post() records duration vs the MI300X profile.
- app/fsm.py: captures the parent-thread tracker and re-installs it on
the iter_steps runner thread (mirrors the token_callback pattern);
attaches `emissions` alongside the existing `energy` block in both
run() and the iter_steps final event.
- app/context/eo_chip_cache.py + app/flood_layers/prithvi_live.py:
ThreadPoolExecutor initializ…
build: compiled Svelte with TerraMind Buildings + Prithvi-Live wired
Round-tripped the per-pixel argmax raster from the droplet so the HF
Space can vectorise it onto WGS84 polygons and the MapLibre layer
actually paints the live model output. Was: only terramind_synthesis's
local fallback path generated polygons; everything else returned
class fractions only and the map showed nothing.
Wire format: droplet's /v1/prithvi-pluvial and /v1/terramind responses
gain `pred_b64` (uint8 base64) + `pred_shape` + (terramind only)
`class_labels`. Small payload — 224x224 uint8 round-trips in ~50 KB.
HF side:
- new app/context/_polygonize.py: polygonize_class_raster() and
polygonize_binary_mask() helpers
- app/context/eo_chip_cache.py adds `bounds_4326` to chip output
- app/context/terramind_nyc lulc/buildings accept bounds_4326,
_try_remote polygonises the response and attaches polygons_geojson
- app/context/terramind_synthesis.py polygonises the remote response
- app/flood_layers/prithvi_live.py polygonises the remote water mask
- app/fsm.py threads chip.boun…
fix(ui): match school + hospital adapter field names to backend
adaptSchools read sc.school_name and sc.elev_m, but
doe_schools.summary_for_point emits loc_name and elevation_m. Same
mismatch on adaptHospitals. Pre-existing — register cards have been
quietly showing — for elev and falling through to 'name' for schools
since the rename. Add fallbacks so both old and new shapes render.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
step_nycha hung the FSM for 8+ minutes per query because
summary_for_point did 20 polygon×polygon intersections (5 nearest
developments × 4 layers) every time. Refactor to read pre-computed
exposure flags from data/registers/nycha.json — the bake script
already runs that math citywide once. Per-query work is haversine
+ dict lookup, sub-millisecond.
Same pattern for step_doe_schools (reads data/registers/schools.json,
~1ms). Hospitals have no pre-built catalog (~150 entries; would have
required a new build script + LLM-budget bake) so they read the
30 KB hospitals.geojson directly and sample the Cornerstone baked
rasters per hit (~10ms first call, sub-ms after).
Schema change for NYCHA per-query view: drop pct_inside_sandy_2012
and pct_in_dep_extreme_2080 floats (polygon-overlap percentages
weren't in the prebuilt catalog), replace with binary
inside_sandy_2012 + dep_*_class/dep_*_label to match the schools
and hospitals shape. NYCHA developments are now reported with
"centroid is in…
Cornerstone (DEP stormwater + Sandy 2012) bottleneck on HF Space CPU
is GDB cold-load, not the join itself: dep_extreme_2080 alone is 30s
to read on first call, and worker memory pressure evicts the
lru_cache between queries. Bake DEP scenarios + Sandy extent to
DEFLATE uint8 GeoTIFFs at 10 ft/px (data/baked/, 7 MB total) and
sample with rasterio. ~500x faster cold-load, ~5x faster per-query,
full parity on 4 canonical addresses across 3 DEP scenarios + Sandy.
- app/flood_layers/dep_stormwater.py: add join_raster() with
thread-local DatasetReader cache; legacy GDB join() retained as
fallback when baked rasters absent (local dev) and as the
polygon-overlap path for coverage_for_polygon (neighborhood mode)
- app/flood_layers/sandy_inundation.py: same pattern, inside_raster()
- app/fsm.py: step_dep / step_sandy use the raster path
- scripts/bake_cornerstone_rasters.py: one-shot bake (~50s)
- experiments/22_cornerstone_optim/: bench harness + RESULTS.md
UI honesty: stoneRegistry.ts…
Squash-deploy commit for HF Space (XET-compatible — no binary blobs).
Applies the same 4 bugfixes as origin/main db5743c:
- app/fsm.py: yield error final event when FSM runner dies before any
action completes
- web/main.py: accept "nta" target type in compare; dispatch nta targets
to i_nbhd.run with neighborhood sub-plan
- web/main.py: fix mellea requirements_passed merge to use intersection
- web/sveltekit: fix mellea.attempts → mellea.n_attempts field name;
rebuild adapter-static bundle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(compare): two-column layout with delta summary row, shape test
Phase 1 — tests/test_compare_shape.py: asserts /api/agent/stream for a
compare query emits exactly one final event with intent="compare", two
targets each carrying an address field, and a merged paragraph that
splits at "---" into PLACE A / PLACE B sections each with citations.
6/6 pass against the live server.
Phase 2 — CompareBriefing.svelte: when plan.intent=="compare" and
finalResult lands, renders a two-column layout (1fr 1px 1fr grid,
collapses below 900 px) with:
- Per-column .address-header showing the resolved address
- Existing Briefing component in each column, no changes to Briefing
- compare-delta-bar above both columns: one row per canonical section
where the first compared number differs (e.g. "TWI: 12.4 vs 8.2")
- Stone provenance / FindingsRegion unchanged (full-width below)
During streaming the merged paragraph renders in the existing single
Briefing path; the two-column view switches in only after finalResult.
FinalResult interface gains optional intent + target…
Squash-merge of all commits on origin/main not present on HF Space:
- fix(riprap-models): port v2 test_transform patch (services container)
- feat(map): wire TerraMind LULC + Ida 2021 HWM map layers
- fix(map): MapLegend booleans, FloodNet tier proxy
- fix(slides): deck visual fixes
- docs: live-system screenshots (now as LFS/XET pointers, not raw blobs)
- chore: repo cleanup, session artifacts to docs/sessions/
- chore: LFS tracking rules for screenshot and slide PNGs/PPTX
Binary files (screenshots + deck.pptx) are stored as git-lfs/git-xet
pointers so HF's pre-receive hook accepts them. git-xet handles
upload to XET object store during the push.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(compare): wire compare intent into SSE handler
The compare intent was classified by the planner but silently fell
through to single_address routing in api_agent_stream because (a)
"compare" was not in INTENTS so _validate() defaulted to
single_address, and (b) there was no routing case in the SSE handler.
Changes:
- app/planner.py: add "compare" to INTENTS, SPECIALISTS (compare is
applicable to all single_address specialists), PLAN_SCHEMA_DESC
hard-rules, _required_specialists, _default_specialists, and
_validate fallback. Planner now emits intent=compare with two
address targets for "compare X vs Y" queries.
- web/main.py: add _run_compare() helper that runs the full
single_address specialist suite sequentially for each target via
i_addr.run() and merges the two paragraphs into one Markdown
document (## PLACE A / ## PLACE B sections). Add compare routing
case to both api_agent_stream and api_agent endpoints.
Verified: "Compare 80 Pioneer Street, Brooklyn to 100 Gold Street,
Manhattan" produces a two-target briefi…
Squashed from 5438cc8..6904684. The slides/asce/deck.pptx is tracked
via git-lfs (added *.pptx to .gitattributes), so this commit carries
only the LFS pointer — no binary blob in history.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Noun Project dam mark, recolored to federal blue
Logo: "Dam" by Chintuza via Noun Project (CC-BY 3.0). The original
SVG embedded the attribution as on-canvas Arial text rendered below
the artwork; the cleaned versions in assets/ strip that text from
the image and carry the credit in body copy on every front-facing
surface, per CC-BY's attribution-required-but-not-on-the-artwork
provision.
assets/:
- logo.svg currentColor variant for inline use
- logo-blue.svg #005EA2 (federal blue) baked
- logo-paper.svg #F4F6F9 (paper) baked, for dark surfaces
- logo.png / @2x federal-blue raster, 256/512 px
- logo-paper.png / @2x paper-cream raster, 256/512 px
Color-matched via rsvg-convert against the Civic Hydrology palette
introduced in e84d03c. The mark inherits CSS color in app contexts
(currentColor) and ships its own fill in standalone files.
App wiring:
- new component web/sveltekit/src/lib/components/shell/RipMark.svelte
inlines the SVG with currentColor; size prop, role=img, aria-label
- AppHeader …
Re-theme: Civic Hydrology palette (USWDS federal blue, cool slate)
Wires up the regressions surfaced by a real PS 188 / Lower East Side
run on the AMD MI300X stack. Five fixes + one new probe:
1. Lift the `RIPRAP_HEAVY_SPECIALISTS` gate when remote ML is
reachable. The heavy specialists' GPU work (TerraMind LULC +
Buildings, Prithvi-live, register joins) runs on the droplet, so
the local wall-clock cost drops from ~60 s to ~5 s — gate the
default to ON so the public demo never silently disables them.
2. TTM specialists never crash with the
`ModuleNotFoundError("Could not import module 'PreTrainedModel'")`
that surfaced on local-fallback paths. Force-import
`PreTrainedModel` + `TinyTimeMixerForPrediction` before
`get_model()` so the transformers lazy-registry resolves the
class strings under FSM worker threads.
3. When remote ML is configured but returns non-ok, surface the
remote error rather than fall through to a brittle local model
load (`ttm_forecast`, `ttm_battery_surge`, `prithvi_live`). Local
fallback is r…
ux v0.4.5: live status pill + drop stray dev probes
src/lib/components/shell/StatusPill.svelte (new)
Live phase + active-step + fired-count indicator for the AppHeader.
Reads from the briefingState rune store. Phases the user sees during
a real query, in order:
● planning intent — planner JSON streaming
● gathering evidence · <step name> · X/Y — FSM data Stones firing
● reconciling · granite4.1 + mellea — waiting on the LLM
● writing briefing — first reconcile token landed
(hidden once briefingState.markReady() fires)
Step labels lifted from the legacy agent.js short-name set so the
pill reads "Sandy 2012" / "FloodNet sensors" / "TTM Battery (NYC
fine-tune)" instead of raw FSM ids. Two specifically renamed for
accuracy: prithvi_eo_v2 → "Ida 2021 polygons (baked lookup)" and
prithvi_eo_live → "Prithvi-NYC-Pluvial v2 segmentation" — the
former is a static spatial join, not a model run, and the old label
misled the user about wh…
fix: pre-import heavy ML modules on main thread; bump footer to v0.4.5
Two fixes spotted on the live PS 188 query:
web/main.py
Beef up the startup pre-import block that exists "so the parallel-
fanout workers don't race each other on first import" (per
CLAUDE.md "known sharp edges").
Previously the startup imported terratorch + tsfm_public top-level
only. That left transformers' lazy `PreTrainedModel` getter, the
tsfm_public.toolkit.get_model chain, and every specialist's own
module-level deps probe to be exercised in FSM worker threads —
which races the lazy-import system and surfaces as
ModuleNotFoundError("Could not import module 'PreTrainedModel'")
or "deps unavailable: tsfm_public" even when both ARE installed.
New pre-import block now also touches:
transformers.PreTrainedModel (resolves the lazy chain)
tsfm_public.TinyTimeMixerForPrediction
tsfm_public.toolkit.get_model.get_model
plus force-imports every specialist module that does heavy ML at
runtime so its mo…
ux v0.4.5: replace landing-page mini-map mockup with a real MapLibre tile
The hand-drawn SVG mockup of Red Hook on the landing page's "What
you'll get back" preview was carried over from the design-handoff
prototype. Replace it with a real MapLibre instance pinned to 80
Pioneer Street so visitors see a working map at the actual scale,
with the same stylized FEMA AE polygon / HWM contour / FloodNet pin
/ 311 cluster / address pin overlays.
src/lib/components/landing/LandMiniMap.svelte (new)
- Carto Positron base, no nav controls, no scale, interactive=false
(it's a preview, not a navigable map).
- 6:5 aspect ratio matches the original SVG container so the
3-pane layout doesn't shift.
- z14.5 / centered on -74.0096, 40.6776 (the same Red Hook anchor
the sample fixture uses).
- Legend overlay (empirical / modeled / proxy swatches) painted
on top of the canvas, same place as before.
src/lib/components/landing/LandPreview.svelte
- Map pane swaps the inline <svg> for <LandMiniMap />.
- Updated meta line ("80 Pi…
User feedback: "the marketing landing should be the app page. it's
fine. just get rid of the /app page."
src/routes/app/ — deleted (+page.svelte + +page.ts)
web/main.py — drops the /app GET route
src/routes/+layout.svelte — onResetCold redirects to / again
(instead of /app)
src/lib/components/shell/AppHeader.svelte
— wordmark links to / (was /app)
src/lib/components/landing/LandHeader.svelte
— drops the "Open the app" nav link;
the hero query box IS the app entry
Routing simplifies to:
/ marketing landing + query entry (app entry)
/q/sample prerendered Red Hook briefing
/q/<id> live FSM briefing
/print/<id> print artifact
Build clean. The cold-start ColdStart.svelte component stays on disk
(in case a future iteration wants a separate analyst-mode entry) but
isn't m…
Two related changes that close out v0.4.5 — the landing page that the
README §"Landing page" called for, and the legacy custom-element
fallback routes that were silently masking the bug ("I see the
pre-v0.4.4 UI" was someone's tab on /single or /legacy).
LANDING PAGE — new at /, four sections per the README spec:
src/lib/components/landing/LandHeader.svelte
Wordmark · slash · "Flood Exposure Briefing · NYC" · nav links
(Open the app / Methodology / Sources). Distinct from AppHeader
so the marketing surface reads as its own page.
src/lib/components/landing/LandHero.svelte
Italic-serif headline ("A flood exposure briefing for *any place*
in New York City."), serif deck, big query box ("Brief this place
→"), cycling examples on a fixed dotted-underline rail (six NYC
queries, 2.2 s rotation, click to run). Submit + click both
navigate to /q/<query>.
src/lib/components/landing/LandPreview.svelte
"What you'll get back" three-pane grid: briefing e…
ux v0.4.5: nine polish deltas on the Findings region
Surgical polish on the shipped v0.4.4 Findings region. None of these
are structural; the card grammar, briefing prose, Mellea status strip,
four-tier palette, cold-start, footer, and PDF template all stay.
P1 — Status semantics (V0.4.5_SPEC §1, "actively misrepresenting
system integrity"). Splits the FSM specialist status from the
four-state ok|warn|error|silent into the five-state
fired / silent_by_design / warned / errored / not_invoked.
- lib/types/card.ts gains a documented SpecialistStatus union
- StoneTally / RunHealthStrip / ProvenanceTrace updated to
branch on the new states
- cardAdapter mapStatus() carefully classifies FSM trace-silent
as silent_by_design, not errored — the spec voice
- Top tally now reads "5 Stones · 21 fired · 5 silent · 1
errored · 24.0s · 25 registered" instead of the old "15/18
functions fired · 3 error" lie
P2 — Capstone meta-card field plumbing (§2). Wires the four metrics
to the rec…
ux: stream Findings cards in real time + fade-in instead of typing caret
Two follow-ups from the live Red Hook walkthrough:
1) **Findings stream as specialists fire.** Previously cards only
populated from `finalResult` (one event at end of stream). Now a
`liveResults` map updates on every SSE `step` event and feeds the
adapter; cards pop into each Stone's rail as their specialist
completes.
* lib/client/cardAdapter.ts gains `applyStepEventToLiveState()`,
mapping step names like `sandy_inundation` / `dep_stormwater` /
`mta_entrance_exposure` to the FSM state-keys the card builders
expect (`sandy`, `dep`, `mta_entrances`, ...). The slim `step.result`
summary shapes are translated into the doc-payload shapes the
builders read; one card per fired specialist, partial-data tolerant.
* `adaptFinalToFindings()` accepts `Partial<FinalResult> | null` and
skips the Capstone meta card until at least one of streaming /
finalResult has produced something to summarise.
* src/routes/q/[queryId]/+page.svelte plumbs `…
ux: wire FindingsRegion into both sample + live routes
Replaces the broken EvidenceGrid + RegisterCard + TraceUI bottom
section with the v0.4.4 FindingsRegion in both:
src/routes/q/sample/+page.svelte
Drives off the SAMPLE_FINDINGS fixture (Red Hook). Lifts
linkedKey, density, provenanceMode, showGrammar to the page.
Removes the EvidenceGrid + TraceUI bottom block; FindingsRegion
subsumes both — the Stones own the trace per-stone, and the
Findings cards own the structured-evidence surface.
src/routes/q/[queryId]/+page.svelte
Same swap, plus a $derived findingsData adapter that runs the
new lib/client/cardAdapter.adaptFinalToFindings against the live
FSM final-state + trace tree. Cards re-derive when finalResult
populates; runWallSeconds tracks elapsed time from onMount → onDone.
src/lib/client/cardAdapter.ts (new)
FSM final-state → Findings Card[] mapper. Per-Stone collectors:
Cornerstone: sandy / dep / ida_hwm / prithvi_water / microtopo
Keystone: mta + nycha + doe + doh fo…
ux: build the Findings region (12 card variants + StoneRegion + run-health)
The SSE stone envelope only knew single_address step names. Neighborhood
queries (sandy_nta / dep_*_nta / nyc311_nta / microtopo_nta) flowed
through without opening a Stone, breaking the envelope on those queries.
Spotted live during the Hollis demo; envelope was emitting
stone_start("Touchstone") on prithvi_eo_live before any Cornerstone
boundary because the polygon-aggregated steps before it weren't
mapped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps the FSM step stream in a Stone-boundary envelope so the trace
UI can render the four data-Stones + Capstone as collapsible parent
nodes around their atomic specialist calls.
web/main.py
Adds _STEP_TO_STONE mapping FSM step names to Stone groups, plus
_STONE_META (NAME / TAGLINE / DESCRIPTION) drawn straight from the
app/stones modules so taglines stay single-source.
The /api/agent/stream event_stream() now tracks the current Stone
and emits:
- stone_start when the first step of a new group fires (or when
the first reconcile token arrives, opening Capstone before the
FSM's reconcile step event lands at the end);
- stone_done when the next group opens, the pipeline completes
(final), or it terminates without a final.
Ancillary steps (geocode, rag_granite_embedding, gliner_extract,
nta_resolve) flow through without opening a Stone — they're
orientation / policy infrastructure shared across Stones.
web/static/a…
Wraps msradam/Granite-TTM-r2-Battery-Surge — Apache-2.0 fine-tune of
ibm-granite/granite-timeseries-ttm-r2 trained on AMD MI300X via AMD
Developer Cloud. Test MAE 0.1091 m on held-out 2023-2024 windows
(-41% vs persistence, -25% vs zero-shot TTM r2).
app/live/ttm_battery_surge.py
fetch() pulls the trailing 1024 h of hourly verified water level +
harmonic tide predictions from NOAA station 8518750 (The Battery),
computes surge residual, runs the fine-tuned TTM, and returns the
96 h forecast plus peak-residual / peak-time scalars the reconciler
can paraphrase. Gated by RIPRAP_TTM_BATTERY_SURGE_ENABLE.
app/fsm.py
New @action step_ttm_battery_surge wired into build_app's actions
dict next to the existing TTM specialists. Plumbed through
step_reconcile reads, the snap dict, run() output, and
iter_steps' final-state dict so the SSE stream surfaces the new
state key end-to-end.
app/reconcile.py
build_documents() emits a `ttm_battery` doc in…
Stones C5: upgrade prithvi_live to NYC-Pluvial v2 fine-tune
Switches the live Sentinel-2 segmentation specialist from the
Sen1Floods11 base (`ibm-nasa-geospatial/Prithvi-EO-2.0-300M-TL-
Sen1Floods11`, test flood IoU ~0.10 on NYC chips) to the NYC-specialized
fine-tune (`msradam/Prithvi-EO-2.0-NYC-Pluvial`, test flood IoU
0.5979 — 6x lift). Same input/output contract; doc_id `prithvi_live`
unchanged so reconciler citations and frontend chips stay stable.
Loader is now flexible across two artifact shapes:
v2 (default): Lightning ckpt restored via
SemanticSegmentationTask.load_from_checkpoint. Probes
best_val_loss.ckpt / model.ckpt / last.ckpt then walks for any
*.ckpt in the snapshot.
v1 (fallback): raw .pt + config.yaml via
LightningInferenceModel.from_config — the original code path,
kept so RIPRAP_PRITHVI_LIVE_REPO=ibm-nasa-geospatial/... still
works for A/B comparisons.
Either path resolves the inference helper from BASE_REPO (only the
IBM-NASA repo ships inference.py).
Reconciler doc body now credits the v2 li…
Stones C4: wire TerraMind-NYC LULC + Buildings into FSM
Adds three new Burr actions in app/fsm.py (gated behind
RIPRAP_HEAVY_SPECIALISTS):
step_eo_chip -> writes eo_chip (S2L2A + S1RTC + DEM)
step_terramind_lulc -> writes terramind_lulc
step_terramind_buildings-> writes terramind_buildings
eo_chip is a per-query cache: one ~150 MB Sentinel-2/1/DEM fetch from
Microsoft Planetary Computer, shared by both TerraMind specialists so
they don't each refetch. The cache lives in app/context/eo_chip_cache.py
and gracefully no-ops when terratorch / planetary_computer / pystac_client
/ rioxarray aren't installed.
Both TerraMind specialists call into app/context/terramind_nyc.py (added
in C3), which routes inference through terratorch tiled_inference at
224x224 and ships the per-class fraction summary the reconciler quotes.
Reconciler:
- app/reconcile.py:build_documents() now emits tm_lulc (Touchstone,
after prithvi_live) and tm_buildings (Keystone, after the legacy
terramind_synthetic prior).
- trim_docs_to_pl…
Telescoped from ~25 spine commits into the three UI generations:
Lit (kept on disk for reference, not loaded):
web/static/components/{briefing,trace,signals,sources-footer}.js
Component-per-tag custom elements with reactive props.
Svelte 5 custom-element bundle (legacy primary, served at /legacy):
web/svelte/src/lib/{Briefing,Trace,SourcesFooter}.svelte + main.js
Shared store for cross-component highlight (citeIndex,
highlightedDocId). Built artifact ships at web/static/dist/riprap.js.
SvelteKit design-system v0.4.2 (the new primary):
web/sveltekit/src/* — adapter-static build, IBM Plex, four-tier
glyphs, MapLibre map with custom basemap, server-side render of
/q/sample + /q/<query>. Built artifact ships at web/sveltekit/build/.
web/main.py wired up the new endpoint surface:
/, /q/sample, /q/<query> → SvelteKit build
/legacy, /single, /compare → Svelte custom-element bundle
/api/agent/stream → SSE pipeline (plan/step/toke…
Two more user-facing modes on top of the address endpoint:
/compare — side-by-side briefings for two queries, useful
when picking between candidate parcels or
comparing two neighborhoods.
/register/{class} — pre-computed exposure for an entire asset
register: NYC DOE schools, NYCHA developments,
MTA subway entrances. The JSON ships baked
(built offline by register_builder), so the
UI is just paginated rendering — every row
shows the same paragraph + tier the address
mode would produce.
Asset loaders normalise each register's source GeoJSON into the
common (name, lat, lon, props) shape register_builder expects.
FastAPI primary UI + NYCO design system vendor drop
web/main.py wires the address-mode endpoint at /, dispatches to the
Burr FSM, and returns the reconciled paragraph + per-step trace as
JSON for the front-end to render. /compare and /register/{class}
routes are stubbed for the next slot.
The static UI follows NYC Planning Labs' civic-assessment idiom
(ZoLa, Capital Planning Explorer): clean two-column layout, IBM
Plex Sans throughout, MapLibre for the map, vanilla JS event flow
with no framework. NYCO design-system CSS + fonts ship as a vendor
drop so the build stays Node-free.