seriffic Claude Sonnet 4.6 commited on
Commit
b9a10ad
Β·
1 Parent(s): 5438cc8

deploy: sync all changes from main at 6904684

Browse files

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>

This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -3
  2. CODE-MORNING-BRIEF-2026-05-06.md +210 -0
  3. COMMS-OVERNIGHT-2026-05-06-MORNING-BRIEF.md +176 -0
  4. OPEN-ISSUES.md +52 -0
  5. OVERNIGHT-2026-05-06-MORNING-BRIEF.md +275 -0
  6. OVERNIGHT-2026-05-06-OUT-OF-SCOPE.md +77 -0
  7. app/context/eo_chip_cache.py +31 -12
  8. app/flood_layers/prithvi_live.py +31 -19
  9. app/framing.py +249 -0
  10. app/fsm.py +42 -2
  11. app/geocode.py +7 -1
  12. app/inference.py +2 -2
  13. app/intents/development_check.py +5 -1
  14. app/intents/live_now.py +11 -3
  15. app/intents/neighborhood.py +5 -1
  16. app/intents/single_address.py +22 -1
  17. app/live/floodnet_forecast.py +1 -1
  18. app/live/ttm_battery_surge.py +1 -0
  19. app/live/ttm_forecast.py +1 -0
  20. app/mellea_validator.py +3 -0
  21. app/planner.py +61 -0
  22. app/reconcile.py +8 -9
  23. audit/AUDIT-2026-05-06.md +150 -0
  24. docs/QUESTION-AWARE-FRAMING.md +194 -0
  25. research/AMD-HACKATHON-LANDSCAPE.md +140 -0
  26. research/PITCH-DECK-LANDSCAPE.md +135 -0
  27. scripts/build_mta_entrances_register.py +0 -1
  28. scripts/build_nycha_register.py +0 -1
  29. scripts/build_schools_register.py +0 -1
  30. scripts/dry_run.py +1 -1
  31. scripts/probe_addresses.py +0 -1
  32. scripts/run_prithvi_flood.py +6 -3
  33. scripts/run_prithvi_ida.py +7 -4
  34. scripts/smoke_test_gpu.sh +56 -0
  35. services/riprap-models/Dockerfile +12 -2
  36. services/riprap-models/main.py +1 -1
  37. services/riprap-models/requirements-full.txt +2 -2
  38. slides/CHANGES-2026-05-06.md +279 -0
  39. slides/Makefile +2 -2
  40. slides/asce/CHANGES.md +56 -0
  41. slides/asce/Makefile +19 -0
  42. slides/asce/deck.html +0 -0
  43. slides/asce/deck.md +483 -0
  44. slides/asce/deck.pdf +3 -0
  45. slides/asce/deck.pptx +3 -0
  46. slides/asce/logo-paper.svg +13 -0
  47. slides/asce/logo.svg +14 -0
  48. slides/asce/riprap.css +657 -0
  49. slides/deck.md +218 -86
  50. submission/COPY-DRAFTS.md +151 -0
.gitattributes CHANGED
@@ -2,10 +2,8 @@
2
  *.geojson filter=lfs diff=lfs merge=lfs -text
3
  *.tif filter=lfs diff=lfs merge=lfs -text
4
  *.pdf filter=lfs diff=lfs merge=lfs -text
5
-
6
  # Pre-computed register paragraphs
7
  data/registers/*.json filter=lfs diff=lfs merge=lfs -text
8
-
9
  # Esri FileGDB internal binary files (DEP Stormwater scenario data)
10
  *.gdbtable filter=lfs diff=lfs merge=lfs -text
11
  *.gdbtablx filter=lfs diff=lfs merge=lfs -text
@@ -15,7 +13,6 @@ data/registers/*.json filter=lfs diff=lfs merge=lfs -text
15
  *.freelist filter=lfs diff=lfs merge=lfs -text
16
  *.horizon filter=lfs diff=lfs merge=lfs -text
17
  *.FDO_UUID filter=lfs diff=lfs merge=lfs -text
18
-
19
  # Hugging Face's standard LFS rules (kept for forward-compat with model assets)
20
  *.7z filter=lfs diff=lfs merge=lfs -text
21
  *.arrow filter=lfs diff=lfs merge=lfs -text
@@ -52,3 +49,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
52
  *.zip filter=lfs diff=lfs merge=lfs -text
53
  *.zst filter=lfs diff=lfs merge=lfs -text
54
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
2
  *.geojson filter=lfs diff=lfs merge=lfs -text
3
  *.tif filter=lfs diff=lfs merge=lfs -text
4
  *.pdf filter=lfs diff=lfs merge=lfs -text
 
5
  # Pre-computed register paragraphs
6
  data/registers/*.json filter=lfs diff=lfs merge=lfs -text
 
7
  # Esri FileGDB internal binary files (DEP Stormwater scenario data)
8
  *.gdbtable filter=lfs diff=lfs merge=lfs -text
9
  *.gdbtablx filter=lfs diff=lfs merge=lfs -text
 
13
  *.freelist filter=lfs diff=lfs merge=lfs -text
14
  *.horizon filter=lfs diff=lfs merge=lfs -text
15
  *.FDO_UUID filter=lfs diff=lfs merge=lfs -text
 
16
  # Hugging Face's standard LFS rules (kept for forward-compat with model assets)
17
  *.7z filter=lfs diff=lfs merge=lfs -text
18
  *.arrow filter=lfs diff=lfs merge=lfs -text
 
49
  *.zip filter=lfs diff=lfs merge=lfs -text
50
  *.zst filter=lfs diff=lfs merge=lfs -text
51
  *tfevents* filter=lfs diff=lfs merge=lfs -text
52
+ *.pptx filter=lfs diff=lfs merge=lfs -text
CODE-MORNING-BRIEF-2026-05-06.md ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Code Morning Brief β€” 2026-05-06
2
+
3
+ Engineering pass: bug fixes + AMD GPU deploy. All fixes committed to `main`.
4
+
5
+ ---
6
+
7
+ ## Final state β€” end of day 2026-05-06
8
+
9
+ **5/5 address probe PASS on AMD MI300X vLLM path.**
10
+
11
+ ```
12
+ [1/5] '442 East Houston Street, Manhattan' PASS 9.8s mellea=4/4 rerolls=1
13
+ [2/5] '80 Pioneer Street, Brooklyn' PASS 7.0s mellea=4/4 rerolls=0
14
+ [3/5] '100 Gold Street, Manhattan' PASS 10.2s mellea=4/4 rerolls=1
15
+ [4/5] 'Hollis, Queens' PASS 4.9s mellea=4/4 rerolls=0
16
+ [5/5] 'Coney Island, Brooklyn' PASS 4.3s mellea=4/4 rerolls=0
17
+ ```
18
+
19
+ Demo queries captured at `/tmp/gpu-demo-q01.json`, `/tmp/gpu-demo-q02.json`,
20
+ `/tmp/gpu-demo-q13.json` (q13 captured in earlier session).
21
+
22
+ ---
23
+
24
+ ## Bugs resolved
25
+
26
+ ### 1. Graceful not_implemented for retrospective + ranking queries
27
+
28
+ **Files:** `app/planner.py` β€” commit `d3fa102`
29
+
30
+ Pre-flight regex intercept before the LLM call short-circuits two
31
+ categories of queries that Riprap doesn't support and previously
32
+ silently misrouted:
33
+
34
+ - **Retrospective (q14/q18):** "What would Riprap have said on
35
+ Hurricane Ida?", "What was the flood status as of August 2021?" β†’
36
+ Returns `Plan(intent="not_implemented")` with a user-facing message.
37
+ - **Ranking (q15):** "Rank top 5 NYCHA buildings by flood exposure" β†’
38
+ Same treatment.
39
+
40
+ `web/main.py` handles `not_implemented` in both the streaming
41
+ (`/api/agent/stream`) and non-streaming (`/api/agent`) paths β€” emits
42
+ the message as a `final` event with `status: "not_implemented"` and
43
+ zeroed Mellea fields. No LLM call is made.
44
+
45
+ ### 2. [doc_id] placeholder leaking from reconcile prompt
46
+
47
+ **Files:** `app/mellea_validator.py`, `app/reconcile.py` β€” commit `f68243b`
48
+
49
+ Root cause: `EXTRA_SYSTEM_PROMPT` used `[doc_id]` as an example
50
+ placeholder in the section skeleton. Granite echoed it literally.
51
+ Mellea's `citations_resolve` check then failed.
52
+
53
+ Two-part fix:
54
+ 1. `mellea_validator.py` β€” added `[doc_id]` to `_check_no_placeholder_tokens`.
55
+ 2. `reconcile.py` β€” rewrote `EXTRA_SYSTEM_PROMPT` to use real doc_id
56
+ examples (`[sandy]`, `[nyc311]`, `[microtopo]`, etc.) instead of
57
+ `[doc_id]` placeholders.
58
+
59
+ ### 3. Geocoder fallback when Planning Labs API is down
60
+
61
+ **File:** `app/geocode.py` β€” commit `70892d1`
62
+
63
+ NYC Planning Labs Geosearch (`geosearch.planninglabs.nyc`) returned
64
+ 503 during the session. All single_address queries failed "no coords".
65
+
66
+ Fix: Added `try/except` around `geocode(text, limit=8)` in
67
+ `geocode_one()`. Any exception (503, connection error, timeout) now
68
+ falls back to Nominatim, matching the existing upstate-hint path.
69
+
70
+ ### 4. STAC searches hang indefinitely without HTTP timeout
71
+
72
+ **Files:** `app/context/eo_chip_cache.py`, `app/flood_layers/prithvi_live.py` β€” commit `70892d1`
73
+
74
+ `pystac_client` STAC searches and `rioxarray` COG downloads have no
75
+ per-request HTTP timeout; they hung indefinitely when Planetary Computer
76
+ was slow or unreachable.
77
+
78
+ Fix: Wrapped both `fetch()` functions in a
79
+ `concurrent.futures.ThreadPoolExecutor` with a hard wall-clock cap
80
+ (`timeout_s + 15 s`). The FSM step now always returns within budget
81
+ with `{"ok": False, "skipped": "timed out"}` on STAC hangs.
82
+
83
+ Controlled by existing `RIPRAP_EO_CHIP_ENABLE` / `RIPRAP_PRITHVI_LIVE_ENABLE`
84
+ env flags (default `1`). Set to `0` to skip STAC lookups entirely.
85
+
86
+ ### 5. NYCHA/DOE/DOH registers hang on first query (91 MB polygon load)
87
+
88
+ **Files:** `app/fsm.py`, `web/main.py` β€” commit `70892d1`
89
+
90
+ `app/registers/nycha.py:_load_sandy_2263()` loads the full 91 MB
91
+ `data/sandy_inundation.geojson` via geopandas on first call. GDAL's
92
+ polygon-organisation pass on that file triggers a "processing may be
93
+ really slow" path β€” 3–5 min on M3 local dev, making the first
94
+ single_address query appear hung.
95
+
96
+ Fix: Split nycha / doe_schools / doh_hospitals behind a new
97
+ `RIPRAP_NYCHA_REGISTERS` env flag (default `0`, independent of the
98
+ GPU-heavy `RIPRAP_HEAVY_SPECIALISTS` flag). When set to `1`,
99
+ `web/main.py` pre-warms the lru_caches at startup.
100
+
101
+ For the demo: nycha/doe/doh data is absent from the briefing (Pioneer
102
+ Street and Gold Street have no NYCHA developments in the 2000 m radius
103
+ anyway). Re-enable post-demo when the server has a 3-min startup budget.
104
+
105
+ ### 6. riprap-models Dockerfile: ROCm torch replaced by CUDA torch
106
+
107
+ **File:** `services/riprap-models/Dockerfile` β€” commits `488d524`, `8899d4a`
108
+
109
+ pip's resolver replaced the AMD ROCm `torch 2.9.1+git8907517` with CUDA
110
+ `torch 2.10.0` from PyPI. Fix: multi-stage build; Stage 1 captures clean
111
+ ROCm site-packages, Stage 2 installs deps, then COPY restores ROCm torch.
112
+ vLLM ENTRYPOINT conflict (`vllm: error: unrecognized arguments`) fixed by
113
+ `ENTRYPOINT []` in the Dockerfile.
114
+
115
+ ---
116
+
117
+ ## GPU deploy status
118
+
119
+ **Droplet:** `134.199.193.99` (AMD MI300X, DigitalOcean GPU)
120
+
121
+ | Container | Image | Port | Status |
122
+ |-----------------|-----------------------------------|------|---------|
123
+ | `vllm` | `vllm/vllm-openai-rocm:v0.17.1` | 8001 | Running |
124
+ | `riprap-models` | `riprap-models:latest` | 7860 | Running |
125
+
126
+ vLLM serves `granite-4.1-8b` at `http://134.199.193.99:8001/v1`.
127
+ riprap-models correct embedding route: `/v1/granite-embed` (smoke test
128
+ script still lists `/v1/embedding` β€” fix documented in `OPEN-ISSUES.md`).
129
+
130
+ **Bearer token:** stored in `AMD_TOKEN` at repo root (gitignored).
131
+
132
+ ---
133
+
134
+ ## Environment variables
135
+
136
+ ```bash
137
+ # Local dev β†’ AMD GPU
138
+ export RIPRAP_LLM_PRIMARY=vllm
139
+ export RIPRAP_LLM_BASE_URL=http://134.199.193.99:8001/v1
140
+ export RIPRAP_LLM_API_KEY=$(cat AMD_TOKEN)
141
+ export RIPRAP_ML_BASE_URL=http://134.199.193.99:7860
142
+ export RIPRAP_ML_API_KEY=$(cat AMD_TOKEN)
143
+ export RIPRAP_EO_CHIP_ENABLE=0 # skip STAC lookups (Planetary Computer slow)
144
+ export RIPRAP_PRITHVI_LIVE_ENABLE=0 # skip STAC lookups
145
+ export RIPRAP_TERRAMIND_ENABLE=0 # skip DEM diffusion (slow on CPU)
146
+ # RIPRAP_NYCHA_REGISTERS defaults to 0 β€” don't set unless startup warmup is acceptable
147
+
148
+ .venv/bin/uvicorn web.main:app --host 127.0.0.1 --port 7861 --log-level info
149
+ ```
150
+
151
+ HF Space env (huggingface-cli space variables):
152
+ ```
153
+ RIPRAP_LLM_BASE_URL=http://134.199.193.99:8001/v1
154
+ RIPRAP_LLM_API_KEY=<token>
155
+ RIPRAP_ML_BASE_URL=http://134.199.193.99:7860
156
+ RIPRAP_ML_API_KEY=<token>
157
+ ```
158
+
159
+ ---
160
+
161
+ ## How to verify
162
+
163
+ ```bash
164
+ # 1. Smoke test
165
+ TOKEN=$(cat AMD_TOKEN)
166
+ scripts/smoke_test_gpu.sh 134.199.193.99 "$TOKEN"
167
+ # Expect: vllm_models PASS, vllm_chat_post PASS, models_health PASS,
168
+ # models_granite_embed_post PASS (correct route: /v1/granite-embed)
169
+ # vllm_chat GET FAIL (expected β€” GET is not a chat endpoint)
170
+
171
+ # 2. Full 5-address end-to-end probe via local server β†’ AMD
172
+ RIPRAP_LLM_PRIMARY=vllm \
173
+ RIPRAP_LLM_BASE_URL=http://134.199.193.99:8001/v1 \
174
+ RIPRAP_LLM_API_KEY=$(cat AMD_TOKEN) \
175
+ RIPRAP_ML_BASE_URL=http://134.199.193.99:7860 \
176
+ RIPRAP_ML_API_KEY=$(cat AMD_TOKEN) \
177
+ RIPRAP_EO_CHIP_ENABLE=0 \
178
+ RIPRAP_PRITHVI_LIVE_ENABLE=0 \
179
+ RIPRAP_TERRAMIND_ENABLE=0 \
180
+ .venv/bin/python scripts/probe_addresses.py
181
+ # Want: 5/5 PASS
182
+
183
+ # 3. Manual vLLM smoke
184
+ curl -s -X POST http://134.199.193.99:8001/v1/chat/completions \
185
+ -H "Authorization: Bearer $(cat AMD_TOKEN)" \
186
+ -H "Content-Type: application/json" \
187
+ -d '{"model":"granite-4.1-8b","messages":[{"role":"user","content":"Reply OK"}],"max_tokens":4}' \
188
+ | python3 -m json.tool
189
+ ```
190
+
191
+ ---
192
+
193
+ ## Droplet redeploy (if destroyed)
194
+
195
+ ```bash
196
+ TOKEN=$(openssl rand -base64 24)
197
+ scripts/deploy_droplet.sh <new-ip> "$TOKEN"
198
+ # ~10-20 min on a fresh droplet
199
+ ```
200
+
201
+ See `CLAUDE.md` β†’ "Droplet redeploy" for full details.
202
+
203
+ ---
204
+
205
+ ## Open issues
206
+
207
+ See `OPEN-ISSUES.md`:
208
+ 1. `experiments/` bugs (numpy annotation, f-string Py 3.12, closure loop, dead api)
209
+ 2. `scripts/smoke_test_gpu.sh` tests `/v1/embedding` β€” correct route is `/v1/granite-embed`
210
+ 3. NYCHA/DOE/DOH registers disabled by default β€” enable post-demo with `RIPRAP_NYCHA_REGISTERS=1` + startup warmup
COMMS-OVERNIGHT-2026-05-06-MORNING-BRIEF.md ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Morning brief β€” comms overnight pass, 2026-05-07
2
+
3
+ Branch: `comms-overnight-2026-05-06`
4
+ Work is local-only, not pushed to remote or HF.
5
+
6
+ ---
7
+
8
+ ## Status
9
+
10
+ All four work streams completed. Research memos are in `research/`.
11
+ Deck is revised (9 slides, built to PDF/HTML/PPTX locally). Submission
12
+ copy is drafted in `submission/COPY-DRAFTS.md`. Cover image was not
13
+ auto-generated β€” a design brief is in that same file with the quickest
14
+ path (re-export the deck cover slide as PNG). One verification item
15
+ remains open before submission: the Mellea 4/4 claim on slide 05.
16
+
17
+ There is a branch-state anomaly to be aware of: commits during this
18
+ session landed on both `comms-overnight-2026-05-06` (the intended
19
+ branch) and `overnight-2026-05-06` (a prior session's branch). The
20
+ content is the same on both. `comms-overnight-2026-05-06` has the clean
21
+ set (research + deck + change log + submission copy). You can merge
22
+ either branch; both are local-only.
23
+
24
+ ---
25
+
26
+ ## Research pass β€” five bullets each
27
+
28
+ ### AMD hackathon landscape (`research/AMD-HACKATHON-LANDSCAPE.md`)
29
+
30
+ - **Agents track dominates the visible field.** Most in-flight
31
+ submissions are multi-agent orchestration systems. Fine-Tuning
32
+ submissions are sparse; NyayaLLM is the only comparable one
33
+ (domain-specific legal LLM on MI300X), but it's single-model,
34
+ single-jurisdiction, and has no published artifacts.
35
+ - **Three published Apache-2.0 fine-tunes is the differentiator.**
36
+ No other visible submission mentions published model artifacts.
37
+ The three HF Hub repos are verifiable; judges can clone and run them.
38
+ - **The domain-tool penalty is real.** A 13-second cited flood briefing
39
+ is harder to demo than a 7-agent crisis system that spawns child
40
+ agents in real time. The architecture slide and the receipts table
41
+ need to close that gap before the civic-tech hook can land.
42
+ - **"Three of four tracks" was a liability.** The hackathon is
43
+ one-track submission. "Engaged in three tracks" reads as hedging.
44
+ Fine-Tuning is the right single-track argument.
45
+ - **Lablab.ai submission pages 403'd.** Project descriptions above are
46
+ from search snippets only. The full 30+ project list requires a
47
+ logged-in lablab.ai session. The landscape read is directional, not
48
+ exhaustive.
49
+
50
+ ### Pitch deck landscape (`research/PITCH-DECK-LANDSCAPE.md`)
51
+
52
+ - **Problem-first into receipts-first is the right pattern for Riprap.**
53
+ The Zillow pullout gives the problem in one CNN headline. The 5/5
54
+ table is the receipts. Demo in the middle, fine-tune evidence before
55
+ the civic case.
56
+ - **The architecture diagram was the single biggest missing slide.**
57
+ Judges scanning a PDF without a system diagram can't assess technical
58
+ depth. The new slide 03 (Five Stones β†’ Capstone flow) does that work
59
+ in one scan.
60
+ - **The "Live Demo" slide was inert in a static deck.** Repurposing to
61
+ "What's Next" opens the longer arc visible to both the hackathon
62
+ audience (May 10) and the ASCE audience (May 13). No content loss.
63
+ - **Do not lead with AI vocabulary; lead with civic vocabulary.**
64
+ "RPL Β§462(2)" and "NYC DEP" are signals of domain expertise, not
65
+ buzzwords. Name them early in the video, not in the deck's second
66
+ half.
67
+ - **5-minute video structure:** 0:00 problem sentence, 0:20 demo,
68
+ 0:50 architecture, 1:30 receipts, 2:00 track argument (fine-tunes),
69
+ 2:30 civic case, 3:30 what's next, 4:00 CTA. Full breakdown in the
70
+ research memo.
71
+
72
+ ---
73
+
74
+ ## Deck changes β€” condensed
75
+
76
+ | Slide | Before | After |
77
+ |---|---|---|
78
+ | 01 Β· Problem | CNN quote as direct citation, no counter-positioning | Quote marked as paraphrase, corrected to Nov 14 removal date; added "not a score" distinction |
79
+ | 02 Β· What riprap is | Unchanged | Unchanged |
80
+ | NEW 03 Β· Architecture | Did not exist | New: query β†’ Planner β†’ 4 evidence Stones (with data sources named) β†’ Capstone + Mellea β†’ briefing |
81
+ | 03 β†’ 04 Β· The track | "Three of four tracks. One project." + Build in Public Skipped row | "Submitted to Fine-Tuning." Fine-Tuning = Primary, Agents/Vision = Supporting. Skipped row removed. |
82
+ | 04 β†’ 05 Β· Receipts | Unchanged | Unchanged (see open item below) |
83
+ | 05 β†’ 06 Β· Why it matters | Unchanged | Unchanged |
84
+ | 06 β†’ 07 Β· Now / Demo | Live demo URL + blockquote (inert in static deck) | WHAT'S NEXT: Ida/ASCE calibration, Stones v1.1 packages, methodology paper |
85
+ | CTA | Unchanged | Unchanged |
86
+
87
+ Slide count: 8 β†’ 9.
88
+
89
+ ---
90
+
91
+ ## Cover image
92
+
93
+ The cover image (`submission/cover-16x9.png`) was not auto-generated.
94
+ Design brief is in `submission/COPY-DRAFTS.md`.
95
+
96
+ **Quickest path:** export the cover slide from the deck PDF as a
97
+ 1920Γ—1080 PNG. The Marp cover slide already uses the correct tokens,
98
+ dam mark, and layout. From `slides/`:
99
+
100
+ ```
101
+ npx @marp-team/marp-cli@latest deck.md --theme riprap.css \
102
+ --allow-local-files --images png
103
+ ```
104
+
105
+ This generates `deck.001.png` (the cover slide) which is the 16:9
106
+ thumbnail. Rename to `submission/cover-16x9.png`.
107
+
108
+ ---
109
+
110
+ ## Submission copy β€” recommended
111
+
112
+ **Title:** `Riprap β€” Cited NYC flood briefings on AMD` (42 chars)
113
+
114
+ **Short (237 chars):**
115
+ Riprap writes NYC flood-exposure briefings where every numeric claim cites its source β€” or doesn't appear. Granite 4.1 8B on AMD MI300X, three Apache-2.0 NYC fine-tunes, Mellea citation grounding. 5/5 addresses, 4/4 checks every run.
116
+
117
+ **Long (~280 words):** in `submission/COPY-DRAFTS.md`, no changes needed.
118
+
119
+ **Runner-up title:** `Riprap: citation-grounded flood briefings`
120
+
121
+ ---
122
+
123
+ ## Three things to look at first
124
+
125
+ 1. **Run the 20-query Mellea probe suite and check slide 05.**
126
+ The deck's "4/4 every run" claim is verified against the 5-address
127
+ probe. If Track A's 20-query stakeholder suite is complete, check
128
+ the grounding results. If any query failed at < 4/4, update the
129
+ slide. Do not submit a deck with a "4/4" claim that doesn't hold
130
+ across the wider suite. Command from `scripts/`:
131
+ ```
132
+ .venv/bin/python scripts/probe_addresses.py
133
+ ```
134
+
135
+ 2. **Generate the cover image** from the deck cover slide (see above).
136
+ One npx command, one rename. Takes 2 minutes.
137
+
138
+ 3. **Review the architecture slide (new slide 03)** in the rendered PDF.
139
+ It uses inline styles and box-grid classes. Verify it renders cleanly
140
+ in the PDF before submission β€” particularly the four Stone columns and
141
+ the Capstone row at the bottom. If the layout is cramped, reducing the
142
+ Stone cell font sizes by 1–2px will fix it. Source: `slides/deck.md`
143
+ lines ~103–160.
144
+
145
+ ---
146
+
147
+ ## Open questions that need Adam's call
148
+
149
+ **1. Track submission: Fine-Tuning is the call, but confirm.**
150
+ The research pass found no evidence against Fine-Tuning as primary. If
151
+ you have information about lablab.ai's scoring criteria that suggests
152
+ Agents is stronger (e.g., the FSM + Burr architecture is judged
153
+ separately), change slide 04 before submission. The deck frame is easy
154
+ to swap β€” the track-row badges are the only change.
155
+
156
+ **2. The CNN quote on slide 01 β€” exact vs paraphrase.**
157
+ Current: "Zillow removed climate risk scores from listings under pressure
158
+ from the real-estate industry. In their place: a link, far less visible."
159
+ Marked as paraphrase. If you want a direct quote for a public-facing
160
+ deck, the TechCrunch version is: "Zillow removed the listings' climate
161
+ scores. In their place is a subtle link to their records at First Street."
162
+ (TechCrunch, Dec 1, 2025.) Either is defensible; this is an editorial
163
+ call.
164
+
165
+ **3. ASCE talk (May 13) β€” which slides to adapt.**
166
+ The new "What's Next" slide (07) and the "Why it Matters" slide (06)
167
+ are the ASCE-relevant ones. For ASCE, slide 04 (The Track) should be
168
+ replaced with a "Methods" slide. The architecture diagram (slide 03)
169
+ and receipts (slide 05) travel unchanged. Make the branch decision:
170
+ fork a new `asce-2026-05-13` branch off this deck or iterate in place.
171
+
172
+ **4. `overnight-2026-05-06` branch cleanup.**
173
+ That branch has duplicate commits plus `e203d5f tests: add 20-query
174
+ stakeholder integration suite` from the prior session. Decide whether
175
+ to merge it into main, keep it as a holding branch, or delete it. The
176
+ comms work you need is all on `comms-overnight-2026-05-06`.
OPEN-ISSUES.md ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Open Issues β€” post-hackathon triage
2
+
3
+ These bugs were identified in the `audit/AUDIT-2026-05-06.md` pass.
4
+ All are in `experiments/` (exploratory/reproduction code) and were
5
+ explicitly left untouched pre-demo per Adam's instruction.
6
+
7
+ ---
8
+
9
+ ## 1. `experiments/17` β€” F821 numpy annotation race
10
+
11
+ **File:** `experiments/17_riprap_integration/terramind_nyc.py:117`
12
+ **Ruff code:** F821 (3Γ—)
13
+ **Issue:** Type annotation references `np` (numpy) before it is
14
+ imported at module top. Currently masked by `from __future__ import
15
+ annotations` (lazy eval). Will fail if Python ever evaluates it
16
+ eagerly, or if this module is ported to a context that drops the
17
+ future import.
18
+ **Fix:** Move `import numpy as np` to module top.
19
+
20
+ ---
21
+
22
+ ## 2. `experiments/18` β€” f-string syntax only valid on Py 3.12+
23
+
24
+ **File:** `experiments/18_terramind_nyc_lora/shared/eval_adapter.py:125`
25
+ **Ruff code:** invalid-syntax
26
+ **Issue:** Inner f-string reuses outer quote style (valid in Py 3.12,
27
+ syntax error in Py 3.10). The HF Space (Py 3.10) cannot import this
28
+ file. Currently local-only; will error if anyone tries to ship it.
29
+ **Fix:** Change inner f-string quotes or use `.format()`.
30
+
31
+ ---
32
+
33
+ ## 3. `experiments/05` β€” closure captures loop variable
34
+
35
+ **File:** `experiments/05_terramind_nyc_finetune/training/verify_phase1.py:438`
36
+ **Ruff code:** B023 (2Γ—)
37
+ **Issue:** Closure inside a `for` loop binds the loop variable by
38
+ reference (all closures see the last value). The classic Python
39
+ late-binding trap. May or may not be a bug depending on intent β€” needs
40
+ a human eye on what the closure does.
41
+ **Fix:** Rebind with a default arg: `lambda x=x: ...`.
42
+
43
+ ---
44
+
45
+ ## 4. `experiments/18` β€” possibly dead `api` assignment
46
+
47
+ **File:** `experiments/18_terramind_nyc_lora/shared/publish_hf.py:107`
48
+ **Ruff code:** F841
49
+ **Issue:** `api` is assigned (likely from `HfApi()`) but never used
50
+ in the file. May be a bug (intended to call `api.upload_file(...)`) or
51
+ a leftover from an edit. Needs a human eye.
52
+ **Fix:** Either use `api` in the upload calls, or remove the assignment.
OVERNIGHT-2026-05-06-MORNING-BRIEF.md ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Overnight pass β€” morning brief β€” 2026-05-06
2
+
3
+ > Branch: `overnight-2026-05-06`. Local-only, not pushed, not deployed.
4
+ > Read this in 5 min; everything detailed lives in linked sub-reports.
5
+
6
+ ## Status one-liner
7
+
8
+ All four work streams landed. The audit committed mechanical fixes
9
+ only and flagged real bugs in `experiments/` for triage. The 20-query
10
+ suite ran twice (baseline + framed) end-to-end against local Granite +
11
+ local specialists. The question-aware Capstone framing lifted mean
12
+ framing 2.25 β†’ 2.80 and produced three verdict-style openings (q01
13
+ "Yes", q02 "Disclosure is warranted", q13 "Vulnerability assessment:")
14
+ where there were zero before. The framing's stop condition fired
15
+ (12 < 3); option (a) β€” planner sub-classifier β€” is sketched in
16
+ `docs/QUESTION-AWARE-FRAMING.md` but explicitly NOT implemented per
17
+ your "don't silently expand scope" rule. One out-of-scope geocoder
18
+ bug surfaced and is documented in
19
+ `OVERNIGHT-2026-05-06-OUT-OF-SCOPE.md` (NOT fixed).
20
+
21
+ ---
22
+
23
+ ## 1. Code audit β€” `audit/AUDIT-2026-05-06.md`
24
+
25
+ `ruff` found 106 issues across the whole repo. Mechanical fixes
26
+ applied to production code paths only (`app/`, `web/`, `scripts/`,
27
+ `services/`, `tests/`); `experiments/` was left alone. Vulture
28
+ confirmed only one F401 worth removing (`io` in `app/inference.py`);
29
+ the rest are kept per Adam's "vulture-confirmed only" rule.
30
+
31
+ **The four real bugs in `experiments/` (NOT touched, flagged for
32
+ Adam to triage):**
33
+ 1. `experiments/17_riprap_integration/terramind_nyc.py:117` β€”
34
+ F821 references `np` in a type annotation; numpy isn't imported
35
+ at module top.
36
+ 2. `experiments/18_terramind_nyc_lora/shared/eval_adapter.py:125` β€”
37
+ Py 3.12 nested f-string; will fail to import on the HF Space (3.10).
38
+ 3. `experiments/05_terramind_nyc_finetune/training/verify_phase1.py:438` β€”
39
+ B023 closure-over-loop-variable, the standard "all closures see
40
+ the last value" trap.
41
+ 4. `experiments/18_terramind_nyc_lora/shared/publish_hf.py:107` β€”
42
+ F841 `api` assigned but never used; may be a missing
43
+ `api.upload_*` call.
44
+
45
+ **Complexity hotspots** (flagged, NOT refactored β€” pre-demo freeze):
46
+ - `app/reconcile.py:build_documents` is **F=178** by cyclomatic
47
+ complexity. CLAUDE.md explicitly says don't touch pre-demo. Held.
48
+ - Other C+ functions: `mellea_validator.reconcile_strict_streaming` (D=23),
49
+ `planner._validate` (D=22), `rag.retrieve` (C=20), three more at
50
+ C=16-18. All expected; none touched.
51
+
52
+ **Lowest MI modules (still passable, not urgent):**
53
+ `app/intents/neighborhood.py` (32), `web/main.py` (37),
54
+ `scripts/probe_addresses.py` (36). Length is the cost of being
55
+ data-heavy / demo-front-door / probe-tester respectively. Post-demo
56
+ candidates for refactor.
57
+
58
+ **Commit:** `9cc6ec4 audit: mechanical fixes from ruff + vulture`.
59
+
60
+ ---
61
+
62
+ ## 2. 20-query stakeholder integration suite β€” `tests/integration/results/2026-05-06/SUMMARY.md`
63
+
64
+ The suite at `tests/integration/stakeholder_queries.py` drives
65
+ `/api/agent/stream` against 20 queries derived from `RESEARCH.md`:
66
+ six verbatim personas, six adapted variants, eight lateral use cases.
67
+ Per query it captures planner intent, Stones invoked / fired /
68
+ silent_by_design / errored, wall-clock per Stone, the briefing prose,
69
+ citations resolved, Mellea grounding pass-rate + rerolls, and a
70
+ **framing score** (0-5) for the opening paragraph against a
71
+ per-question-type rubric.
72
+
73
+ **Outputs in `tests/integration/results/2026-05-06/`:**
74
+ - `q01-resident-pioneer.json` ... `q20-control-astoria.json` β€” full
75
+ per-query payload (plan, paragraph, steps, mellea, framing rationale).
76
+ - `SUMMARY.md` β€” table of all 20 (intent, time, grounding,
77
+ framing, status).
78
+ - `FAILURES.md` β€” full briefings + proximate cause for any query
79
+ that errored, timed out, missed Mellea, or returned no prose.
80
+
81
+ **Baseline run summary:**
82
+ - 20/20 OK (no errors, no timeouts).
83
+ - Mean framing score: **2.25** (mostly stuck at 2 = "on-topic exposure
84
+ language but no question-aware framing").
85
+ - Queries with framing β‰₯ 3: 5 / 20 (q06, q07, q14, q18, q19 β€” note q07,
86
+ q14, q18, q19 scored 3 only because they returned the canned
87
+ "No grounded data available for this address." which the rubric
88
+ scores as 3 = place-referenced).
89
+ - 4 queries had Mellea 0/4: q07 (lease query, geocoder failed),
90
+ q14 (retrospective query, geocoder failed), q15 (NYCHA ranking,
91
+ planner mis-routed to dev_check with 0 steps), q16 (FloodNet
92
+ live_now with no active signals), q18 (court exhibit retrospective,
93
+ geocoder failed), q19 (BBMCR project name, NTA didn't resolve).
94
+ - The geocoder failures are documented in
95
+ `OVERNIGHT-2026-05-06-OUT-OF-SCOPE.md` β€” same root cause: the
96
+ length-ratio heuristic in `app/intents/single_address.py:33`
97
+ rejects the planner's correctly-extracted address when the user's
98
+ query is conversational.
99
+
100
+ **Baseline commit:** `e203d5f tests: add 20-query stakeholder integration suite`.
101
+ Per-query JSONs preserved at
102
+ `tests/integration/results/2026-05-06/baseline/`.
103
+
104
+ ---
105
+
106
+ ## 3. Question-aware Capstone framing β€” `docs/QUESTION-AWARE-FRAMING.md` + `tests/integration/results/2026-05-06/FRAMING-DELTA.md`
107
+
108
+ **Diagnosis (full version: `docs/QUESTION-AWARE-FRAMING.md`).** Three
109
+ options were on the table: (a) planner sub-classifier, (b) Capstone
110
+ prompt-conditional, (c) both. **Recommendation and what landed: (b).**
111
+ The four-section evidence structure (Status / Empirical / Modeled /
112
+ Policy) and the four Mellea grounding checks stay byte-identical;
113
+ only the Status sentence's directive changes.
114
+
115
+ **Implementation:**
116
+ - New `app/framing.py` β€” 11 question types, regex-based deterministic
117
+ detector, per-type opening-directive table, `augment_system_prompt`.
118
+ - `app/fsm.py` β€” new `set_user_query` + `set_planner_intent`
119
+ threadlocals; `step_reconcile` augments
120
+ `app.reconcile.EXTRA_SYSTEM_PROMPT` before passing to
121
+ `reconcile_strict_streaming`.
122
+ - `app/intents/single_address.py` β€” sets/resets the new threadlocals.
123
+ - `app/intents/neighborhood.py`, `development_check.py`,
124
+ `live_now.py` β€” augment their own EXTRA_SYSTEM_PROMPT before
125
+ reconcile.
126
+
127
+ **Detector accuracy against suite labels:** 14/20 verbatim. The 6
128
+ mismatches are all bare-place queries where the suite's persona-
129
+ imposed label isn't discoverable from the query text alone β€” these
130
+ fall back to `journalism` (bare neighborhood) or `generic_exposure`
131
+ (bare address, baseline behavior preserved).
132
+
133
+ **Before/after framing delta** (full report:
134
+ `tests/integration/results/2026-05-06/FRAMING-DELTA.md`):
135
+
136
+ | Metric | Baseline | Framed | Ξ” |
137
+ |--------|---------:|-------:|---:|
138
+ | Mean framing | 2.25 | 2.80 | +0.55 |
139
+ | β‰₯ 3/5 | 5 | 8 | +3 |
140
+ | β‰₯ 4/5 | 2 | 5 | +3 |
141
+ | β‰₯ 5/5 | 0 | 3 | +3 |
142
+
143
+ **The three queries that hit 5/5** (verdict-style openings β€” the
144
+ demo-critical wins):
145
+ - **q01** resident habitability β€” opening flipped from "exposed to
146
+ historical flood events..." to "**Yes**, this address is exposed
147
+ to flood risk based on its inclusion within the Hurricane Sandy
148
+ inundation zone..."
149
+ - **q02** attorney disclosure β€” opening flipped to "**Disclosure is
150
+ warranted** because the site experiences moderate flood exposure
151
+ as indicated by 56.6% of surrounding cells..."
152
+ - **q13** grant evidence β€” opening flipped to "**Vulnerability
153
+ assessment**: Chinatown-Two Bridges (NTA MN0301) in Manhattan
154
+ exhibits moderate flood exposure..."
155
+
156
+ **Mellea net change:** +4 improved (3/4 β†’ 4/4), -2 regressed (q01
157
+ 4/4 β†’ 3/4, q06 3/4 β†’ 2/4), 14 unchanged. Net +2 grounding checks
158
+ gained across the suite.
159
+
160
+ **Stop condition: FIRED.** 12 / 20 framed queries scored below 3
161
+ (threshold > 5 β‡’ stop). Per Adam's instruction, NOT iterating further
162
+ on the prompt-conditional. Triage of the 12 + sketch of what option
163
+ (a) β€” planner sub-classifier β€” would require lives in
164
+ `docs/QUESTION-AWARE-FRAMING.md` Β§"Outcome of the 2026-05-06 framed
165
+ run" + Β§"What option (a) would require." Headline:
166
+
167
+ - 4 / 12 are rubric-vs-directive vocabulary mismatch (bare
168
+ neighborhood β†’ journalism directive applied, but rubric scored
169
+ for capital_planning markers). Not a framing failure.
170
+ - 4 / 12 are short-prose-floor failures (geocoder + planner short
171
+ circuit). No framing change can fix these.
172
+ - 4 / 12 are cases where Granite ignored the soft directive. These
173
+ are where option (a) would actually help.
174
+
175
+ **Commits:** `1a82fde framing: question-aware Capstone opening`,
176
+ `342dd4d framing: clarify the directive's scope`,
177
+ `f40ebd2 tests: add FRAMING-DELTA.md generator`,
178
+ `9c61976 tests: baseline + framed run results`.
179
+
180
+ ---
181
+
182
+ ## 4. Branch state
183
+
184
+ Branch: **`overnight-2026-05-06`**, local only. To inspect:
185
+
186
+ ```bash
187
+ git log --oneline overnight-2026-05-06 ^main
188
+ ```
189
+
190
+ Commit chronology (newest first; Adam's parallel `comms-` commits
191
+ get auto-merged in via the runtime so they may interleave):
192
+
193
+ - `9c61976 tests: baseline + framed run results, 2026-05-06`
194
+ - `342dd4d framing: clarify the directive's scope is the Status sentence only`
195
+ - `e81962b docs: log out-of-scope findings from the overnight pass`
196
+ - `8894517 docs: morning brief skeleton`
197
+ - `f40ebd2 tests: add FRAMING-DELTA.md generator`
198
+ - `1a82fde framing: question-aware Capstone opening (Capstone prompt-conditional)`
199
+ - `e203d5f tests: add 20-query stakeholder integration suite`
200
+ - `9cc6ec4 audit: mechanical fixes from ruff + vulture`
201
+
202
+ Plus auto-merged commits from Adam's `comms-overnight-2026-05-06`
203
+ work (slides, research, submission docs).
204
+
205
+ To revert any single piece:
206
+
207
+ ```bash
208
+ git revert <commit-sha> # safe: creates a new commit that undoes
209
+ git checkout main # discard the branch entirely
210
+ git branch -D overnight-2026-05-06
211
+ ```
212
+
213
+ The framing change touches 5 files; reverting `1a82fde` is a clean
214
+ backout if the framed run shows regressions.
215
+
216
+ ---
217
+
218
+ ## 5. Three things to look at first when you open the laptop
219
+
220
+ 1. **`tests/integration/results/2026-05-06/FRAMING-DELTA.md`** β€” the
221
+ per-query opening diff is the most useful artifact in the pass.
222
+ Read q01, q02, q13 first (the three queries that hit 5/5 β€” these
223
+ are the demo wins). Then the four "Granite ignored the directive"
224
+ cases triaged in `docs/QUESTION-AWARE-FRAMING.md` ("Outcome of the
225
+ 2026-05-06 framed run" Β§3) β€” those are where option (a) would
226
+ actually pay off if you decide to spend the 2-3 hours.
227
+ 2. **`OVERNIGHT-2026-05-06-OUT-OF-SCOPE.md`** β€” one real bug
228
+ surfaced: the planner-vs-query length-ratio threshold in
229
+ `app/intents/single_address.py:33` rejects the planner's
230
+ correctly-extracted address whenever the user's query is long and
231
+ conversational. Failure mode is "No grounded data available" with
232
+ Mellea 0/4. Hits q07 (resident lease question), q14
233
+ (retrospective), q18 (court exhibit) β€” exactly the conversational
234
+ personas the demo arc wants to handle gracefully. Suggested fix
235
+ is in the doc; NOT applied.
236
+ 3. **`audit/AUDIT-2026-05-06.md` punch list** β€” the four
237
+ `experiments/` bugs flagged at the top. Real bugs the demo
238
+ hides because nobody imports them at runtime; if anyone tries to
239
+ reproduce the fine-tunes during the hackathon Q&A, they'll hit
240
+ `experiments/18` failing to import on Py 3.10 (nested f-string).
241
+
242
+ ---
243
+
244
+ ## What did NOT land
245
+
246
+ - **No deployment, no push.** Per instructions; both targets
247
+ untouched.
248
+ - **No refactor of `build_documents` / Mellea checks / FSM
249
+ structure.** All flagged in `audit/AUDIT-2026-05-06.md` as
250
+ post-demo work.
251
+ - **No new dependencies.** All work used `ruff` / `vulture` / `radon`
252
+ (already installed via `uv tool install`) and the existing repo
253
+ code.
254
+ - **No planner sub-classifier (option a).** The diagnosis recommends
255
+ (b) only; if the framed run's stop condition fires (>5 queries with
256
+ framing < 3), `docs/QUESTION-AWARE-FRAMING.md` describes what (a)
257
+ would require.
258
+
259
+ ---
260
+
261
+ ## Operating notes for the morning
262
+
263
+ - Local server: `nohup .venv/bin/uvicorn web.main:app --host 127.0.0.1
264
+ --port 7860 ...` was running on port 7860 throughout the night.
265
+ Check `ps -fp $(pgrep -f "uvicorn web.main")` to see if it's still
266
+ alive; safe to kill with `pkill -f "uvicorn web.main"`.
267
+ - Server log: `/tmp/riprap-overnight/server.log`.
268
+ - Suite run logs: `/tmp/riprap-overnight/suite-baseline.log`,
269
+ `/tmp/riprap-overnight/suite-framed.log`.
270
+
271
+ ---
272
+
273
+ _Faithful account, not victory lap: this brief should match the
274
+ commit log + the on-disk reports exactly. If anything here doesn't,
275
+ trust the file system, not the brief._
OVERNIGHT-2026-05-06-OUT-OF-SCOPE.md ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Out-of-scope findings β€” 2026-05-06 overnight pass
2
+
3
+ These were discovered during the overnight pass but are outside the
4
+ scope Adam authorised. **NOT FIXED.** Documented here per his
5
+ explicit instruction so they're easy to triage.
6
+
7
+ ---
8
+
9
+ ## 1. Geocoder rejects planner's extracted address on conversational queries
10
+
11
+ **File:** `app/intents/single_address.py:33`
12
+
13
+ ```python
14
+ addr = planner_addr if (planner_addr and len(planner_addr) >= len(query) * 0.7) else query
15
+ ```
16
+
17
+ **Failure mode.** When the user asks a conversational, multi-clause
18
+ question like:
19
+
20
+ > *"I just got a lease for 504 Grand Street, Lower East Side. The
21
+ > landlord says no flood history. Is that true?"*
22
+
23
+ the planner correctly extracts `"504 Grand Street, Lower East Side"`
24
+ into `targets[0].text` (38 chars). But the conditional rejects this
25
+ extracted address because it's less than 70% of the full query
26
+ (108 chars) β€” so `addr` falls back to the full query, which the
27
+ NYC DCP Geosearch geocoder cannot parse, returning "no geocoder
28
+ match." The FSM then runs all 19 specialists without coordinates,
29
+ each returning "no coords," and the briefing emits the canonical
30
+ silence-over-confabulation `"No grounded data available for this
31
+ address."` with mellea 0/4 (no claims to check).
32
+
33
+ **Discovered:** suite query q07 (Resident, disclosure-suspicion).
34
+ The `tests/integration/results/2026-05-06/q07-resident-grand-disclosure.json`
35
+ payload shows `geocode.err = "no geocoder match"` and 17 downstream
36
+ steps with `err = "no coords"`.
37
+
38
+ **Why the 70% threshold exists.** A defensive heuristic against the
39
+ planner stripping too much of the user's address into a partial token
40
+ (e.g. "Pioneer" instead of "80 Pioneer Street, Brooklyn"). The
41
+ threshold was tuned for short queries where a stripped result is
42
+ suspicious; it backfires on long queries where the planner correctly
43
+ distilled a clean address out of conversational filler.
44
+
45
+ **Why this matters.** This is exactly the persona shape that the
46
+ demo wants to handle gracefully β€” a renter asking a real,
47
+ conversational question. RESEARCH.md Β§1 frames the resident persona
48
+ as "the FloodHelpNY swap-in," and conversational queries are the
49
+ distinguishing feature. Today the system silently produces an empty
50
+ briefing on this shape.
51
+
52
+ **Suggested fix (NOT applied).** Trust the planner's extracted address
53
+ unconditionally when it parses as an NYC street form (house number +
54
+ street name + borough). Replace the length-ratio heuristic with a
55
+ shape check. Out of scope for this overnight pass because it requires
56
+ re-running the address probe to confirm no regression on the curated
57
+ addresses.
58
+
59
+ **Workaround for the demo:** type a clean address.
60
+
61
+ ---
62
+
63
+ ## 2. Suite runner caveats discovered during the overnight pass
64
+
65
+ These are not bugs β€” just things worth knowing for a future session.
66
+
67
+ - `tests/integration/stakeholder_queries.py` writes per-query JSON
68
+ after each query (defensive against partial completion). The
69
+ SUMMARY.md is only written at the end. If the suite is killed
70
+ mid-run, the JSONs are still readable; the SUMMARY can be
71
+ regenerated by a small wrapper that walks the JSON dir.
72
+ - The framing-rubric scorer (`score_framing` in the suite) is
73
+ intentionally pessimistic β€” it only assigns a 5 if a verdict marker
74
+ matches, even if the briefing's prose is high-quality. A high-quality
75
+ generic Status section will still score 3 (place named) or 4 (topic
76
+ named without verdict). The 0-5 scale is a delta-detector, not an
77
+ absolute quality measure.
app/context/eo_chip_cache.py CHANGED
@@ -16,6 +16,7 @@ specialist instead of surfacing a noisy error.
16
  """
17
  from __future__ import annotations
18
 
 
19
  import logging
20
  import os
21
  import threading
@@ -264,18 +265,8 @@ def _to_terramind_tensors(modalities: dict[str, Any]) -> dict[str, Any]:
264
  return chips
265
 
266
 
267
- def fetch(lat: float, lon: float, timeout_s: float = 60.0) -> dict[str, Any]:
268
- """Run the chip pipeline. Always returns a dict with at minimum
269
- `{ok, skipped|err, ...}`; on success the dict carries the
270
- co-registered numpy arrays plus `tensors` (the TerraMind-shaped
271
- torch dict).
272
- """
273
- if not ENABLE:
274
- return {"ok": False, "skipped": "RIPRAP_EO_CHIP_ENABLE=0"}
275
- if not _DEPS_OK:
276
- return {"ok": False,
277
- "skipped": f"deps unavailable on this deployment: "
278
- f"{_DEPS_MISSING}"}
279
  with _FETCH_LOCK:
280
  try:
281
  modalities = _fetch_modalities(lat, lon, timeout_s=timeout_s)
@@ -291,3 +282,31 @@ def fetch(lat: float, lon: float, timeout_s: float = 60.0) -> dict[str, Any]:
291
  return {"ok": False,
292
  "err": f"tensor build failed: {type(e).__name__}: {e}"}
293
  return modalities
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  """
17
  from __future__ import annotations
18
 
19
+ import concurrent.futures
20
  import logging
21
  import os
22
  import threading
 
265
  return chips
266
 
267
 
268
+ def _fetch_and_build(lat: float, lon: float, timeout_s: float) -> dict[str, Any]:
269
+ """Inner fetch + tensor build, run inside a bounded thread."""
 
 
 
 
 
 
 
 
 
 
270
  with _FETCH_LOCK:
271
  try:
272
  modalities = _fetch_modalities(lat, lon, timeout_s=timeout_s)
 
282
  return {"ok": False,
283
  "err": f"tensor build failed: {type(e).__name__}: {e}"}
284
  return modalities
285
+
286
+
287
+ def fetch(lat: float, lon: float, timeout_s: float = 60.0) -> dict[str, Any]:
288
+ """Run the chip pipeline. Always returns a dict with at minimum
289
+ `{ok, skipped|err, ...}`; on success the dict carries the
290
+ co-registered numpy arrays plus `tensors` (the TerraMind-shaped
291
+ torch dict).
292
+
293
+ Runs in a daemon thread so that STAC searches and COG band downloads
294
+ (which use requests/rioxarray without per-call timeouts) are bounded
295
+ by a hard wall-clock deadline even when the network hangs.
296
+ """
297
+ if not ENABLE:
298
+ return {"ok": False, "skipped": "RIPRAP_EO_CHIP_ENABLE=0"}
299
+ if not _DEPS_OK:
300
+ return {"ok": False,
301
+ "skipped": f"deps unavailable on this deployment: "
302
+ f"{_DEPS_MISSING}"}
303
+ # Hard wall-clock cap: pystac_client / rioxarray COG reads don't expose
304
+ # uniform per-request timeouts, so we bound the whole pipeline here.
305
+ hard_timeout = timeout_s + 15.0
306
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
307
+ future = pool.submit(_fetch_and_build, lat, lon, timeout_s)
308
+ try:
309
+ return future.result(timeout=hard_timeout)
310
+ except concurrent.futures.TimeoutError:
311
+ log.warning("eo_chip: hard timeout after %.0fs (STAC/COG hung)", hard_timeout)
312
+ return {"ok": False, "skipped": f"eo_chip timed out after {hard_timeout:.0f}s"}
app/flood_layers/prithvi_live.py CHANGED
@@ -24,6 +24,7 @@ License: Apache-2.0. See experiments/shared/licenses.md.
24
 
25
  from __future__ import annotations
26
 
 
27
  import logging
28
  import os
29
  import threading
@@ -319,25 +320,8 @@ def _polygonize_mask(pred, ref_da, epsg: int) -> dict | None:
319
  return None
320
 
321
 
322
- def fetch(lat: float, lon: float, timeout_s: float = 60.0) -> dict[str, Any]:
323
- """Run the specialist. Returns a dict with at minimum:
324
- { "ok": bool,
325
- "skipped": str | None, # reason if no observation
326
- "item_id": str | None,
327
- "item_datetime": str | None,
328
- "cloud_cover": float | None,
329
- "pct_water_within_500m": float | None,
330
- "pct_water_full": float | None }
331
- Designed to never raise; failures show up as ok=False with an `err`.
332
- """
333
- if not ENABLE:
334
- return {"ok": False, "skipped": "RIPRAP_PRITHVI_LIVE_ENABLE=0"}
335
- if not _DEPS_OK:
336
- # Clean "not deployed here" signal instead of a ModuleNotFoundError
337
- # surfaced as an exception. Same trace-card layout as ENABLE=0.
338
- return {"ok": False,
339
- "skipped": f"deps unavailable on this deployment: "
340
- f"{_DEPS_MISSING}"}
341
  t0 = time.time()
342
  try:
343
  item = _search_recent_scene(lat, lon)
@@ -428,3 +412,31 @@ def fetch(lat: float, lon: float, timeout_s: float = 60.0) -> dict[str, Any]:
428
  log.exception("prithvi_live: fetch failed")
429
  return {"ok": False, "err": f"{type(e).__name__}: {e}",
430
  "elapsed_s": round(time.time() - t0, 2)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  from __future__ import annotations
26
 
27
+ import concurrent.futures
28
  import logging
29
  import os
30
  import threading
 
320
  return None
321
 
322
 
323
+ def _fetch_inner(lat: float, lon: float, timeout_s: float) -> dict[str, Any]:
324
+ """Core fetch logic β€” run inside a bounded thread via fetch()."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  t0 = time.time()
326
  try:
327
  item = _search_recent_scene(lat, lon)
 
412
  log.exception("prithvi_live: fetch failed")
413
  return {"ok": False, "err": f"{type(e).__name__}: {e}",
414
  "elapsed_s": round(time.time() - t0, 2)}
415
+
416
+
417
+ def fetch(lat: float, lon: float, timeout_s: float = 60.0) -> dict[str, Any]:
418
+ """Run the specialist. Wraps _fetch_inner in a bounded thread so that
419
+ STAC searches and COG band reads (which lack per-request HTTP timeouts)
420
+ cannot hang the FSM indefinitely.
421
+
422
+ Returns a dict with at minimum:
423
+ { "ok": bool, "skipped": str | None, "item_id": str | None,
424
+ "cloud_cover": float | None, "pct_water_within_500m": float | None }
425
+ Designed to never raise; failures show up as ok=False with an `err`.
426
+ """
427
+ if not ENABLE:
428
+ return {"ok": False, "skipped": "RIPRAP_PRITHVI_LIVE_ENABLE=0"}
429
+ if not _DEPS_OK:
430
+ return {"ok": False,
431
+ "skipped": f"deps unavailable on this deployment: "
432
+ f"{_DEPS_MISSING}"}
433
+ hard_timeout = timeout_s + 15.0
434
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
435
+ future = pool.submit(_fetch_inner, lat, lon, timeout_s)
436
+ try:
437
+ return future.result(timeout=hard_timeout)
438
+ except concurrent.futures.TimeoutError:
439
+ log.warning("prithvi_live: hard timeout after %.0fs (STAC/COG hung)",
440
+ hard_timeout)
441
+ return {"ok": False,
442
+ "skipped": f"prithvi_live timed out after {hard_timeout:.0f}s"}
app/framing.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Question-aware framing for the Capstone briefing opening.
2
+
3
+ The four-section structure (Status / Empirical / Modeled / Policy) is
4
+ load-bearing for the Mellea grounding checks and stays unchanged. What
5
+ this module does is detect the *shape* of the user's question from the
6
+ raw query string + planner intent, then return a single-sentence
7
+ directive that conditions only the opening Status sentence.
8
+
9
+ Eleven question types are recognised; they mirror the rubric in
10
+ `tests/integration/stakeholder_queries.py:FRAMING_RUBRICS`. Detection
11
+ is deterministic regex matching β€” no extra LLM call, no added latency.
12
+
13
+ Usage:
14
+
15
+ from app.framing import augment_system_prompt
16
+ system_prompt = augment_system_prompt(
17
+ EXTRA_SYSTEM_PROMPT, query=user_query, intent=plan.intent,
18
+ )
19
+
20
+ The returned prompt has the original text plus a trailing
21
+ `QUESTION-AWARE OPENING:` block. Granite 4.1 attends to this through
22
+ the system-prompt cache and applies it to the Status sentence.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import re
27
+ from typing import Final
28
+
29
+ QUESTION_TYPES: Final[tuple[str, ...]] = (
30
+ "habitability_decision",
31
+ "legal_disclosure",
32
+ "capital_planning",
33
+ "underwriting",
34
+ "journalism",
35
+ "development_siting",
36
+ "grant_evidence",
37
+ "retrospective",
38
+ "emergency_response",
39
+ "comparison",
40
+ "generic_exposure",
41
+ )
42
+
43
+
44
+ # ---- Per-type opening directives ------------------------------------------
45
+ #
46
+ # Each directive is one sentence that supplements (does not replace) the
47
+ # Status section's existing instruction. Granite 4.1 has a strong prior
48
+ # toward "this address is exposed to ..." openings; the directive
49
+ # overrides that in a question-shaped way without disturbing the four
50
+ # grounding invariants.
51
+
52
+ _DIRECTIVES: dict[str, str] = {
53
+ "habitability_decision": (
54
+ "The Status sentence MUST start with a direct verdict word "
55
+ "(\"Yes\" if the documents show meaningful flood evidence, \"No\" "
56
+ "if they don't), then name the single strongest piece of "
57
+ "evidence with its [doc_id]. The user is deciding whether to "
58
+ "live here β€” answer the question, then cite."
59
+ ),
60
+ "legal_disclosure": (
61
+ "The Status sentence MUST state whether the documents contain "
62
+ "facts a NY RPL Β§462(2) or Β§231-b disclosure would need to "
63
+ "record. Begin with \"Disclosure is warranted\" or \"Disclosure "
64
+ "is not triggered\" based on the evidence, then name the "
65
+ "specific fact with its [doc_id]. The user is a real-estate "
66
+ "professional checking the disclosure threshold."
67
+ ),
68
+ "capital_planning": (
69
+ "The Status sentence MUST frame the place as a capital-planning "
70
+ "candidate: name the dominant exposure with its [doc_id] and "
71
+ "indicate whether the evidence supports prioritization "
72
+ "(\"merits prioritization\", \"ranks high for hardening\") or "
73
+ "not. The user allocates infrastructure investment."
74
+ ),
75
+ "underwriting": (
76
+ "The Status sentence MUST emphasize that every figure in the "
77
+ "briefing is independently sourced β€” open with the dominant "
78
+ "exposure and the specific [doc_id], then add a half-clause "
79
+ "noting that the audit chain follows below. The user is an "
80
+ "underwriter who needs a defensible loss narrative."
81
+ ),
82
+ "journalism": (
83
+ "The Status sentence MUST be reproducible reporting prose: "
84
+ "name the place, name the dominant exposure with [doc_id], "
85
+ "and avoid editorial verbs like \"shocking\" or \"alarming\". "
86
+ "The user is a data journalist who will cite this prose verbatim."
87
+ ),
88
+ "development_siting": (
89
+ "The Status sentence MUST start with the count of active "
90
+ "construction filings cited from [dob_permits] (e.g. \"N "
91
+ "active construction filings sit inside ...\") and indicate "
92
+ "which flood layer they intersect. The user is a developer or "
93
+ "architect doing a pre-design siting check."
94
+ ),
95
+ "grant_evidence": (
96
+ "The Status sentence MUST open with \"Vulnerability "
97
+ "assessment:\" and name the place + dominant exposure with "
98
+ "[doc_id]. Treat the briefing as the evidence section of a "
99
+ "HUD CDBG-DR or FEMA BRIC application β€” formal, third-person, "
100
+ "free of advocacy framing."
101
+ ),
102
+ "retrospective": (
103
+ "Riprap currently runs on present-day data sources. The Status "
104
+ "sentence MUST acknowledge the question is retrospective and "
105
+ "state explicitly that the briefing reflects the CURRENT state "
106
+ "of these data sources, not a snapshot from the requested date. "
107
+ "Then proceed with the present-day exposure picture so the user "
108
+ "still gets the geography. Silence-over-confabulation: never "
109
+ "reconstruct historical conditions you can't verify."
110
+ ),
111
+ "emergency_response": (
112
+ "The Status sentence MUST quantify what is at risk in the "
113
+ "next few hours, citing the live signal that triggered the "
114
+ "query and any active alerts with [doc_id]. The user needs an "
115
+ "operational picture, not a historical exposure summary."
116
+ ),
117
+ "comparison": (
118
+ "The Status sentence MUST name BOTH places the user is "
119
+ "comparing and indicate which one shows greater exposure on "
120
+ "the strongest cited signal. If only one place's data is "
121
+ "available in the documents, say so explicitly. The user is "
122
+ "doing a head-to-head decision."
123
+ ),
124
+ "generic_exposure": "", # default β€” no override
125
+ }
126
+
127
+
128
+ # ---- Detector -------------------------------------------------------------
129
+ #
130
+ # Patterns are ordered: the FIRST type whose pattern matches wins. Order
131
+ # matters β€” more specific question shapes (legal_disclosure, grant_evidence,
132
+ # emergency_response) come before more general ones (habitability_decision,
133
+ # capital_planning) so the obvious specialist tags don't get swallowed.
134
+
135
+ _PATTERNS: list[tuple[str, list[re.Pattern]]] = [
136
+ ("retrospective", [
137
+ re.compile(r"\b(would have|would Riprap|on (the )?date of|as of (the )?(date|day)|"
138
+ r"day before|prior to|before (Hurricane|Ida|Sandy|the storm)|"
139
+ r"on (August|September|October|November|December|January|February|March|"
140
+ r"April|May|June|July) \d{1,2},? ?\d{4}|"
141
+ r"time.?machine|retrospective|court (exhibit|testimony))\b", re.I),
142
+ ]),
143
+ ("emergency_response", [
144
+ re.compile(r"\b(just triggered|right now|next (few |six |\d+ )?hours?|"
145
+ r"in the next \d+|currently flooding|flood (warning|watch) is active|"
146
+ r"sensor [A-Z]{2}-?\d+|live (alert|trigger))\b", re.I),
147
+ ]),
148
+ ("legal_disclosure", [
149
+ re.compile(r"\b(disclos(e|ure|ed)|RPL\s*Β§?\s*\d+|Property Condition Disclosure|"
150
+ r"Β§\s*462|Β§\s*231-?b|seller'?s? disclosure|landlord'?s? disclosure|"
151
+ r"required to disclose|need to disclose)\b", re.I),
152
+ ]),
153
+ ("grant_evidence", [
154
+ re.compile(r"\b(vulnerability assessment|CDBG-?DR|HUD|BRIC|"
155
+ r"grant application|funding application|community resilience grant|"
156
+ r"FEMA application|disaster recovery (application|funding))\b", re.I),
157
+ ]),
158
+ ("development_siting", [
159
+ re.compile(r"\b(what (are|is) (they|being) build(ing)?|new construction|"
160
+ r"under construction|active (construction|filing|project|permit)|"
161
+ r"projects? (in progress|underway|planned)|architects?|"
162
+ r"siting check|pre.?design|"
163
+ r"DOB filing|developer)\b", re.I),
164
+ ]),
165
+ ("comparison", [
166
+ # `prioritize X over Y` can have many words between, hence the
167
+ # bounded non-greedy span β€” capped at 80 chars to avoid runaway.
168
+ re.compile(r"\b(compare\b|comparison|\bvs\b|\bversus\b|"
169
+ r"head-?to-?head|\brank\s+the\s+top)\b", re.I),
170
+ re.compile(r"\bprioritize\b.{1,80}\bover\b", re.I | re.S),
171
+ re.compile(r"\bover\s+\w+(?:\s+\w+){0,3}\s+for\s+(hardening|investment)\b", re.I),
172
+ ]),
173
+ ("capital_planning", [
174
+ re.compile(r"\b(prioritiz(e|ation)|capital plan(ning)?|harden(ing|s)?|"
175
+ r"infrastructure investment|where (should|to) (we |the )(invest|"
176
+ r"prioritize|harden)|MTA.+prioritize|DEP.+prioritize|"
177
+ r"protection envelope|outside (it|the protection)|"
178
+ r"resilien(ce|cy) project)\b", re.I),
179
+ ]),
180
+ ("habitability_decision", [
181
+ re.compile(r"\b(should I worry|should I (be|consider)|is (it|this) safe|"
182
+ r"can I (rent|live|move|raise (my )?kids?)|considering (renting|leasing|moving)|"
183
+ r"(thinking about|planning to) (rent|lease|move|buy)|"
184
+ r"is (this|that|the landlord) true|landlord (says|claims|told)|"
185
+ r"no flood history|just got a lease|new lease|signing a lease|"
186
+ r"\bworry\b)", re.I),
187
+ ]),
188
+ ("underwriting", [
189
+ re.compile(r"\b(underwrit(e|er|ing|able)|actuarial|loss history|"
190
+ r"insurabl[ey]|catastrophe (model|risk)|"
191
+ r"insurance (audit|memo|profile)|"
192
+ r"audit (chain|trail))\b", re.I),
193
+ ]),
194
+ ("journalism", [
195
+ re.compile(r"\b(reporter|journalist|newsroom|story|coverage|"
196
+ r"published?|publish (this|the))", re.I),
197
+ ]),
198
+ ]
199
+
200
+
201
+ def detect(query: str, intent: str | None = None) -> str:
202
+ """Classify the question shape from the raw query and planner intent.
203
+
204
+ Returns one of `QUESTION_TYPES`. Falls back to `generic_exposure`
205
+ when no pattern matches β€” that's the existing behavior, preserved.
206
+
207
+ `intent` is currently advisory only (the patterns don't read it),
208
+ but the parameter is part of the API so future refinements can
209
+ use it (e.g. an `intent=neighborhood` query without a verdict
210
+ keyword could default to `journalism` rather than `generic_exposure`).
211
+ """
212
+ if not query:
213
+ return "generic_exposure"
214
+ q = query.strip()
215
+ for qt, patterns in _PATTERNS:
216
+ if any(p.search(q) for p in patterns):
217
+ return qt
218
+ # Heuristic fallback: bare neighborhood/borough names from a planner
219
+ # context default to journalism (most common stakeholder reading a
220
+ # neighborhood-only query is a reporter or planner). For
221
+ # single_address with no question keyword, fall back to generic.
222
+ if intent == "neighborhood" and len(q.split()) <= 3:
223
+ return "journalism"
224
+ return "generic_exposure"
225
+
226
+
227
+ def opening_instruction(question_type: str) -> str:
228
+ """Return the directive sentence(s) for a question type.
229
+ Returns empty string for `generic_exposure` (no override)."""
230
+ return _DIRECTIVES.get(question_type, "")
231
+
232
+
233
+ def augment_system_prompt(base: str, *, query: str,
234
+ intent: str | None = None) -> str:
235
+ """Wrap a base system prompt with a question-aware opening directive.
236
+
237
+ No-op when the detector returns `generic_exposure` β€” the original
238
+ behavior is preserved.
239
+ """
240
+ qt = detect(query, intent)
241
+ directive = opening_instruction(qt)
242
+ if not directive:
243
+ return base
244
+ return (
245
+ f"{base}\n\n"
246
+ f"QUESTION-AWARE OPENING (this directive overrides ONLY the opening "
247
+ f"**Status.** sentence; the four-section structure and citation "
248
+ f"discipline above remain in force):\n{directive}"
249
+ )
app/fsm.py CHANGED
@@ -95,6 +95,29 @@ def _current_planned_specialists():
95
  return getattr(_FSM_LOCAL, "planned_specialists", None)
96
 
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  # Canonical Burr: one action per specialist, sequential transitions.
99
  # A previous version of this module wrapped 16 specialists in a single
100
  # fan-out action that ran them concurrently in a ThreadPoolExecutor;
@@ -969,6 +992,7 @@ def step_reconcile(state: State) -> State:
969
  "doh_hospitals": state.get("doh_hospitals"),
970
  }
971
  if is_strict:
 
972
  from app.mellea_validator import DEFAULT_LOOP_BUDGET, reconcile_strict_streaming
973
  from app.reconcile import EXTRA_SYSTEM_PROMPT, build_documents, trim_docs_to_plan
974
  doc_msgs = build_documents(snap)
@@ -979,8 +1003,13 @@ def step_reconcile(state: State) -> State:
979
  else:
980
  token_cb = _current_token_callback()
981
  attempt_cb = _current_mellea_attempt_callback()
 
 
 
 
 
982
  mres = reconcile_strict_streaming(
983
- doc_msgs, EXTRA_SYSTEM_PROMPT,
984
  user_prompt="Write the cited paragraph now.",
985
  loop_budget=DEFAULT_LOOP_BUDGET,
986
  on_token=(lambda d, _ai: token_cb(d)) if token_cb else None,
@@ -1024,6 +1053,7 @@ def step_reconcile(state: State) -> State:
1024
 
1025
  import os as _os # noqa: E402
1026
 
 
1027
  # Specialists that involve large spatial joins (every NYCHA development
1028
  # overlapped against multiple flood layers, every DOE school footprint
1029
  # joined to DEM/HAND, etc.) or per-query model inference (Prithvi-EO live
@@ -1057,6 +1087,15 @@ _HEAVY_SPECIALISTS_ENABLED = _os.environ.get(
1057
  "RIPRAP_HEAVY_SPECIALISTS", _HEAVY_DEFAULT,
1058
  ).lower() in ("1", "true", "yes")
1059
 
 
 
 
 
 
 
 
 
 
1060
 
1061
  def build_app(query: str):
1062
  """Linear, single-action-per-step Burr application.
@@ -1090,10 +1129,11 @@ def build_app(query: str):
1090
  "mta_entrances": step_mta_entrances,
1091
  "prithvi": step_prithvi, # baked GeoJSON polygons for Ida; cheap
1092
  }
1093
- if _HEAVY_SPECIALISTS_ENABLED:
1094
  actions["nycha"] = step_nycha
1095
  actions["doe_schools"] = step_doe_schools
1096
  actions["doh_hospitals"] = step_doh_hospitals
 
1097
  actions["prithvi_live"] = step_prithvi_live
1098
  actions["terramind"] = step_terramind
1099
  # New TerraMind-NYC LoRA family β€” one chip fetch feeds two
 
95
  return getattr(_FSM_LOCAL, "planned_specialists", None)
96
 
97
 
98
+ def set_user_query(query: str | None):
99
+ """Install the user's original natural-language query for question-aware
100
+ framing in step_reconcile. The FSM's state["query"] is the geocoder
101
+ input (often just the street address), which doesn't carry the
102
+ user's question shape β€” set this separately so Capstone can detect
103
+ 'should I worry' / 'is disclosure required' / etc."""
104
+ _FSM_LOCAL.user_query = query
105
+
106
+
107
+ def _current_user_query() -> str | None:
108
+ return getattr(_FSM_LOCAL, "user_query", None)
109
+
110
+
111
+ def set_planner_intent(intent: str | None):
112
+ """Install the planner's classified intent so step_reconcile can pass
113
+ it to the framing detector as a tiebreaker on bare-place queries."""
114
+ _FSM_LOCAL.planner_intent = intent
115
+
116
+
117
+ def _current_planner_intent() -> str | None:
118
+ return getattr(_FSM_LOCAL, "planner_intent", None)
119
+
120
+
121
  # Canonical Burr: one action per specialist, sequential transitions.
122
  # A previous version of this module wrapped 16 specialists in a single
123
  # fan-out action that ran them concurrently in a ThreadPoolExecutor;
 
992
  "doh_hospitals": state.get("doh_hospitals"),
993
  }
994
  if is_strict:
995
+ from app.framing import augment_system_prompt
996
  from app.mellea_validator import DEFAULT_LOOP_BUDGET, reconcile_strict_streaming
997
  from app.reconcile import EXTRA_SYSTEM_PROMPT, build_documents, trim_docs_to_plan
998
  doc_msgs = build_documents(snap)
 
1003
  else:
1004
  token_cb = _current_token_callback()
1005
  attempt_cb = _current_mellea_attempt_callback()
1006
+ framed_prompt = augment_system_prompt(
1007
+ EXTRA_SYSTEM_PROMPT,
1008
+ query=_current_user_query() or state.get("query") or "",
1009
+ intent=_current_planner_intent() or "single_address",
1010
+ )
1011
  mres = reconcile_strict_streaming(
1012
+ doc_msgs, framed_prompt,
1013
  user_prompt="Write the cited paragraph now.",
1014
  loop_budget=DEFAULT_LOOP_BUDGET,
1015
  on_token=(lambda d, _ai: token_cb(d)) if token_cb else None,
 
1053
 
1054
  import os as _os # noqa: E402
1055
 
1056
+
1057
  # Specialists that involve large spatial joins (every NYCHA development
1058
  # overlapped against multiple flood layers, every DOE school footprint
1059
  # joined to DEM/HAND, etc.) or per-query model inference (Prithvi-EO live
 
1087
  "RIPRAP_HEAVY_SPECIALISTS", _HEAVY_DEFAULT,
1088
  ).lower() in ("1", "true", "yes")
1089
 
1090
+ # NYCHA / DOE / DOH registers load a 91 MB sandy_inundation.geojson via
1091
+ # geopandas on first call. On machines with slow I/O or single-threaded
1092
+ # Python GIL contention (M3 local dev) this takes 3–5 min and makes the
1093
+ # first single_address query appear hung. Disable by default; enable on
1094
+ # the AMD droplet where the server pre-warms these at startup.
1095
+ _NYCHA_REGISTERS_ENABLED = _os.environ.get(
1096
+ "RIPRAP_NYCHA_REGISTERS", "0",
1097
+ ).lower() in ("1", "true", "yes")
1098
+
1099
 
1100
  def build_app(query: str):
1101
  """Linear, single-action-per-step Burr application.
 
1129
  "mta_entrances": step_mta_entrances,
1130
  "prithvi": step_prithvi, # baked GeoJSON polygons for Ida; cheap
1131
  }
1132
+ if _HEAVY_SPECIALISTS_ENABLED and _NYCHA_REGISTERS_ENABLED:
1133
  actions["nycha"] = step_nycha
1134
  actions["doe_schools"] = step_doe_schools
1135
  actions["doh_hospitals"] = step_doh_hospitals
1136
+ if _HEAVY_SPECIALISTS_ENABLED:
1137
  actions["prithvi_live"] = step_prithvi_live
1138
  actions["terramind"] = step_terramind
1139
  # New TerraMind-NYC LoRA family β€” one chip fetch feeds two
app/geocode.py CHANGED
@@ -166,7 +166,13 @@ def geocode_one(text: str) -> GeocodeHit | None:
166
  return hit
167
 
168
  hint = _detect_borough(text)
169
- hits = geocode(text, limit=8)
 
 
 
 
 
 
170
  if hint:
171
  in_boro = [h for h in hits if h.borough and h.borough.lower() == hint.lower()]
172
  if in_boro:
 
166
  return hit
167
 
168
  hint = _detect_borough(text)
169
+ try:
170
+ hits = geocode(text, limit=8)
171
+ except Exception as e:
172
+ # Geosearch is unreachable or returned a server error β€” fall back to
173
+ # Nominatim rather than surfacing a 503 to every downstream specialist.
174
+ log.warning("Geosearch unavailable (%r) β€” falling back to Nominatim", e)
175
+ return geocode_nominatim(text)
176
  if hint:
177
  in_boro = [h for h in hits if h.borough and h.borough.lower() == hint.lower()]
178
  if in_boro:
app/inference.py CHANGED
@@ -30,10 +30,10 @@ RIPRAP_ML_* env is unset (e.g. on first-light dev or in unit tests).
30
  from __future__ import annotations
31
 
32
  import base64
33
- import io
34
  import logging
35
  import os
36
- from typing import Any, Iterable
 
37
 
38
  import httpx
39
 
 
30
  from __future__ import annotations
31
 
32
  import base64
 
33
  import logging
34
  import os
35
+ from collections.abc import Iterable
36
+ from typing import Any
37
 
38
  import httpx
39
 
app/intents/development_check.py CHANGED
@@ -202,6 +202,7 @@ def run(plan, query: str, progress_q=None, strict: bool = False) -> dict[str, An
202
  # validation failure we emit a mellea_attempt event and reroll.
203
  rec_step["step"] = "mellea_reconcile_development"
204
  try:
 
205
  from app.mellea_validator import DEFAULT_LOOP_BUDGET, reconcile_strict_streaming
206
  from app.reconcile import trim_docs_to_plan as _trim
207
  docs = _trim(docs, set(plan.specialists or []))
@@ -214,8 +215,11 @@ def run(plan, query: str, progress_q=None, strict: bool = False) -> dict[str, An
214
  progress_q.put({"kind": "mellea_attempt",
215
  "attempt": attempt_idx,
216
  "passed": passed, "failed": failed})
 
 
 
217
  mres = reconcile_strict_streaming(
218
- docs, EXTRA_SYSTEM_PROMPT,
219
  user_prompt="Write the development briefing now.",
220
  model=OLLAMA_MODEL, loop_budget=DEFAULT_LOOP_BUDGET,
221
  on_token=_on_token if progress_q else None,
 
202
  # validation failure we emit a mellea_attempt event and reroll.
203
  rec_step["step"] = "mellea_reconcile_development"
204
  try:
205
+ from app.framing import augment_system_prompt
206
  from app.mellea_validator import DEFAULT_LOOP_BUDGET, reconcile_strict_streaming
207
  from app.reconcile import trim_docs_to_plan as _trim
208
  docs = _trim(docs, set(plan.specialists or []))
 
215
  progress_q.put({"kind": "mellea_attempt",
216
  "attempt": attempt_idx,
217
  "passed": passed, "failed": failed})
218
+ framed_prompt = augment_system_prompt(
219
+ EXTRA_SYSTEM_PROMPT, query=query, intent=plan.intent,
220
+ )
221
  mres = reconcile_strict_streaming(
222
+ docs, framed_prompt,
223
  user_prompt="Write the development briefing now.",
224
  model=OLLAMA_MODEL, loop_budget=DEFAULT_LOOP_BUDGET,
225
  on_token=_on_token if progress_q else None,
app/intents/live_now.py CHANGED
@@ -153,7 +153,14 @@ def run(plan, query: str, progress_q=None) -> dict[str, Any]:
153
  if progress_q is not None:
154
  progress_q.put({"kind": "token", "delta": delta})
155
  try:
156
- paragraph, audit = _reconcile(docs, on_token=_on_token if progress_q else None)
 
 
 
 
 
 
 
157
  rec_step["ok"] = True
158
  except Exception as e:
159
  paragraph = "Could not produce a live-conditions report."
@@ -206,10 +213,11 @@ def _doc(doc_id: str, body_lines: list[str]) -> dict:
206
  return {"role": f"document {doc_id}", "content": "\n".join(body_lines)}
207
 
208
 
209
- def _reconcile(docs: list[dict], on_token=None) -> tuple[str, dict]:
 
210
  from app.reconcile import verify_paragraph
211
  messages = docs + [
212
- {"role": "system", "content": EXTRA_SYSTEM_PROMPT},
213
  {"role": "user", "content": "Write the live-conditions briefing now."},
214
  ]
215
  # live_now is the smallest intent: ~4 live docs, short briefing.
 
153
  if progress_q is not None:
154
  progress_q.put({"kind": "token", "delta": delta})
155
  try:
156
+ from app.framing import augment_system_prompt
157
+ framed_prompt = augment_system_prompt(
158
+ EXTRA_SYSTEM_PROMPT, query=query, intent=plan.intent,
159
+ )
160
+ paragraph, audit = _reconcile(
161
+ docs, on_token=_on_token if progress_q else None,
162
+ system_prompt=framed_prompt,
163
+ )
164
  rec_step["ok"] = True
165
  except Exception as e:
166
  paragraph = "Could not produce a live-conditions report."
 
213
  return {"role": f"document {doc_id}", "content": "\n".join(body_lines)}
214
 
215
 
216
+ def _reconcile(docs: list[dict], on_token=None,
217
+ system_prompt: str = EXTRA_SYSTEM_PROMPT) -> tuple[str, dict]:
218
  from app.reconcile import verify_paragraph
219
  messages = docs + [
220
+ {"role": "system", "content": system_prompt},
221
  {"role": "user", "content": "Write the live-conditions briefing now."},
222
  ]
223
  # live_now is the smallest intent: ~4 live docs, short briefing.
app/intents/neighborhood.py CHANGED
@@ -361,6 +361,7 @@ def run(plan, query: str, progress_q=None, strict: bool = False) -> dict[str, An
361
  if docs and strict:
362
  rec_step["step"] = "mellea_reconcile_neighborhood"
363
  try:
 
364
  from app.mellea_validator import DEFAULT_LOOP_BUDGET, reconcile_strict_streaming
365
  from app.reconcile import trim_docs_to_plan as _trim
366
  docs = _trim(docs, set(plan.specialists or []))
@@ -373,8 +374,11 @@ def run(plan, query: str, progress_q=None, strict: bool = False) -> dict[str, An
373
  progress_q.put({"kind": "mellea_attempt",
374
  "attempt": attempt_idx,
375
  "passed": passed, "failed": failed})
 
 
 
376
  mres = reconcile_strict_streaming(
377
- docs, EXTRA_SYSTEM_PROMPT,
378
  user_prompt="Write the cited briefing now.",
379
  model=OLLAMA_MODEL, loop_budget=DEFAULT_LOOP_BUDGET,
380
  on_token=_on_token if progress_q else None,
 
361
  if docs and strict:
362
  rec_step["step"] = "mellea_reconcile_neighborhood"
363
  try:
364
+ from app.framing import augment_system_prompt
365
  from app.mellea_validator import DEFAULT_LOOP_BUDGET, reconcile_strict_streaming
366
  from app.reconcile import trim_docs_to_plan as _trim
367
  docs = _trim(docs, set(plan.specialists or []))
 
374
  progress_q.put({"kind": "mellea_attempt",
375
  "attempt": attempt_idx,
376
  "passed": passed, "failed": failed})
377
+ framed_prompt = augment_system_prompt(
378
+ EXTRA_SYSTEM_PROMPT, query=query, intent=plan.intent,
379
+ )
380
  mres = reconcile_strict_streaming(
381
+ docs, framed_prompt,
382
  user_prompt="Write the cited briefing now.",
383
  model=OLLAMA_MODEL, loop_budget=DEFAULT_LOOP_BUDGET,
384
  on_token=_on_token if progress_q else None,
app/intents/single_address.py CHANGED
@@ -8,8 +8,21 @@ parallelism for an address is bounded by Granite 4.1 reconcile time
8
  anyway."""
9
  from __future__ import annotations
10
 
 
 
11
  from app.fsm import run as run_linear
12
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  def run(plan, query: str, progress_q=None, strict: bool = False) -> dict:
15
  """Execute the planner's single_address Plan via the existing linear
@@ -23,16 +36,20 @@ def run(plan, query: str, progress_q=None, strict: bool = False) -> dict:
23
  iter_steps,
24
  set_mellea_attempt_callback,
25
  set_planned_specialists,
 
26
  set_strict_mode,
27
  set_token_callback,
 
28
  )
29
  planner_addr = next(
30
  (t["text"] for t in plan.targets if t.get("type") == "address"),
31
  None,
32
  )
33
- addr = planner_addr if (planner_addr and len(planner_addr) >= len(query) * 0.7) else query
34
  set_strict_mode(strict)
35
  set_planned_specialists(plan.specialists or [])
 
 
36
  if progress_q is not None:
37
  def _on_token(delta: str):
38
  progress_q.put({"kind": "token", "delta": delta})
@@ -57,12 +74,16 @@ def run(plan, query: str, progress_q=None, strict: bool = False) -> dict:
57
  set_mellea_attempt_callback(None)
58
  set_strict_mode(False)
59
  set_planned_specialists(None)
 
 
60
  else:
61
  try:
62
  out = run_linear(addr)
63
  finally:
64
  set_strict_mode(False)
65
  set_planned_specialists(None)
 
 
66
  out["intent"] = "single_address"
67
  out["plan"] = {
68
  "intent": plan.intent,
 
8
  anyway."""
9
  from __future__ import annotations
10
 
11
+ import re
12
+
13
  from app.fsm import run as run_linear
14
 
15
+ _ADDRESS_SHAPE = re.compile(
16
+ r"^\d+\s+[A-Z][\w\s\.\-']+(St|Street|Ave|Avenue|Rd|Road|Blvd|"
17
+ r"Boulevard|Pl|Place|Ln|Lane|Dr|Drive|Way|Ct|Court|Pkwy|"
18
+ r"Parkway|Sq|Square|Ter|Terrace|Hwy|Highway)\.?",
19
+ re.IGNORECASE,
20
+ )
21
+
22
+
23
+ def _looks_like_address(s: str) -> bool:
24
+ return bool(s and _ADDRESS_SHAPE.search(s))
25
+
26
 
27
  def run(plan, query: str, progress_q=None, strict: bool = False) -> dict:
28
  """Execute the planner's single_address Plan via the existing linear
 
36
  iter_steps,
37
  set_mellea_attempt_callback,
38
  set_planned_specialists,
39
+ set_planner_intent,
40
  set_strict_mode,
41
  set_token_callback,
42
+ set_user_query,
43
  )
44
  planner_addr = next(
45
  (t["text"] for t in plan.targets if t.get("type") == "address"),
46
  None,
47
  )
48
+ addr = planner_addr if _looks_like_address(planner_addr) else query
49
  set_strict_mode(strict)
50
  set_planned_specialists(plan.specialists or [])
51
+ set_user_query(query)
52
+ set_planner_intent(plan.intent)
53
  if progress_q is not None:
54
  def _on_token(delta: str):
55
  progress_q.put({"kind": "token", "delta": delta})
 
74
  set_mellea_attempt_callback(None)
75
  set_strict_mode(False)
76
  set_planned_specialists(None)
77
+ set_user_query(None)
78
+ set_planner_intent(None)
79
  else:
80
  try:
81
  out = run_linear(addr)
82
  finally:
83
  set_strict_mode(False)
84
  set_planned_specialists(None)
85
+ set_user_query(None)
86
+ set_planner_intent(None)
87
  out["intent"] = "single_address"
88
  out["plan"] = {
89
  "intent": plan.intent,
app/live/floodnet_forecast.py CHANGED
@@ -36,9 +36,9 @@ import numpy as np
36
 
37
  from app.context.floodnet import flood_events_for, sensors_near
38
  from app.live.ttm_forecast import (
 
39
  DAILY_CONTEXT,
40
  DAILY_PREDICTION,
41
- _MODEL_LOAD_ERROR,
42
  _run_ttm,
43
  )
44
 
 
36
 
37
  from app.context.floodnet import flood_events_for, sensors_near
38
  from app.live.ttm_forecast import (
39
+ _MODEL_LOAD_ERROR,
40
  DAILY_CONTEXT,
41
  DAILY_PREDICTION,
 
42
  _run_ttm,
43
  )
44
 
app/live/ttm_battery_surge.py CHANGED
@@ -92,6 +92,7 @@ def _ensure_model():
92
  if _MODEL is not None:
93
  return _MODEL
94
  from huggingface_hub import snapshot_download
 
95
  # Force-import dispatched class names so the transformers lazy
96
  # registry can resolve `PreTrainedModel` / `TinyTimeMixerForPrediction`
97
  # under FSM worker threads. Same pattern as ttm_forecast._load_model.
 
92
  if _MODEL is not None:
93
  return _MODEL
94
  from huggingface_hub import snapshot_download
95
+
96
  # Force-import dispatched class names so the transformers lazy
97
  # registry can resolve `PreTrainedModel` / `TinyTimeMixerForPrediction`
98
  # under FSM worker threads. Same pattern as ttm_forecast._load_model.
app/live/ttm_forecast.py CHANGED
@@ -92,6 +92,7 @@ def _load_model(context_length: int = CONTEXT_LENGTH,
92
  return None
93
  try:
94
  import torch # noqa: F401
 
95
  # Force-import the registered class names BEFORE get_model so that
96
  # transformers' lazy registry can resolve them by string. Without
97
  # this, AutoModel-style dispatch raises
 
92
  return None
93
  try:
94
  import torch # noqa: F401
95
+
96
  # Force-import the registered class names BEFORE get_model so that
97
  # transformers' lazy registry can resolve them by string. Without
98
  # this, AutoModel-style dispatch raises
app/mellea_validator.py CHANGED
@@ -130,6 +130,9 @@ def _check_no_placeholder_tokens():
130
  bad.append("<document>")
131
  if "</document" in text:
132
  bad.append("</document>")
 
 
 
133
  return not bad
134
  return _fn
135
 
 
130
  bad.append("<document>")
131
  if "</document" in text:
132
  bad.append("</document>")
133
+ if "[doc_id]" in text:
134
+ # Model echoed the EXTRA_SYSTEM_PROMPT skeleton literally
135
+ bad.append("[doc_id]")
136
  return not bad
137
  return _fn
138
 
app/planner.py CHANGED
@@ -16,6 +16,7 @@ from __future__ import annotations
16
  import json
17
  import logging
18
  import os
 
19
  from dataclasses import dataclass
20
  from typing import Any
21
 
@@ -137,6 +138,58 @@ Available specialists (and which intents they apply to):
137
  Output ONLY the JSON object. No commentary, no markdown."""
138
 
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  # ---- Planner call ----------------------------------------------------------
141
 
142
  def plan(query: str, model: str = OLLAMA_MODEL, on_token=None) -> Plan:
@@ -147,6 +200,14 @@ def plan(query: str, model: str = OLLAMA_MODEL, on_token=None) -> Plan:
147
  Granite generates. The streaming endpoint uses this to show the
148
  agent's reasoning forming live in the UI.
149
  """
 
 
 
 
 
 
 
 
150
  messages = [
151
  {"role": "system", "content": SYSTEM_PROMPT},
152
  {"role": "user", "content": query},
 
16
  import json
17
  import logging
18
  import os
19
+ import re
20
  from dataclasses import dataclass
21
  from typing import Any
22
 
 
138
  Output ONLY the JSON object. No commentary, no markdown."""
139
 
140
 
141
+ # ---- Not-implemented short-circuits ----------------------------------------
142
+ #
143
+ # These patterns are well-defined feature gaps. Returning a graceful message
144
+ # is better than routing them into an intent that silently fails.
145
+
146
+ _RETROSPECTIVE_RE = re.compile(
147
+ r"(?:what\s+would\s+(?:riprap|you|it)\s+have\s+said"
148
+ r"|what\s+(?:was|were)\s+(?:the\s+)?(?:flood|risk|status)"
149
+ r"|(?:as\s+of|on)\s+(?:august|september|october|november|december|january|"
150
+ r"february|march|april|may|june|july)\s+\d"
151
+ r"|on\s+(?:the\s+date\s+of|hurricane\s+ida|hurricane\s+sandy)"
152
+ r"|(?:september|august|october)\s+\d{1,2},?\s+20\d{2}"
153
+ r")",
154
+ re.IGNORECASE,
155
+ )
156
+
157
+ _RANKING_RE = re.compile(
158
+ r"(?:rank\s+(?:the\s+)?top\s+\d"
159
+ r"|top\s+\d+\s+\w+\s+by\s+flood"
160
+ r"|intersect(?:ed)?\s+with\s+(?:dac|ejnyc|social\s+vulnerability)"
161
+ r"|sort(?:ed)?\s+by\s+(?:flood\s+)?(?:exposure|risk|score)"
162
+ r")",
163
+ re.IGNORECASE,
164
+ )
165
+
166
+ NOT_IMPLEMENTED_INTENTS = {
167
+ "retrospective": (
168
+ _RETROSPECTIVE_RE,
169
+ "Historical-date mode (\"what would Riprap have said on [date]\") "
170
+ "is on the roadmap but not yet available. Riprap currently reports "
171
+ "present-state flood exposure; past-state reconstruction is planned "
172
+ "for a future release (see deck slide 8).",
173
+ ),
174
+ "ranking": (
175
+ _RANKING_RE,
176
+ "Cross-development ranking queries (\"rank top N by flood exposure\", "
177
+ "\"intersect with DAC designation\") require a cross-register join "
178
+ "that is on the roadmap but not yet available. Try a specific address "
179
+ "or neighborhood instead.",
180
+ ),
181
+ }
182
+
183
+
184
+ def _not_implemented_message(query: str) -> str | None:
185
+ """Return a user-facing message if the query matches a known feature gap,
186
+ else None."""
187
+ for _name, (pattern, message) in NOT_IMPLEMENTED_INTENTS.items():
188
+ if pattern.search(query):
189
+ return message
190
+ return None
191
+
192
+
193
  # ---- Planner call ----------------------------------------------------------
194
 
195
  def plan(query: str, model: str = OLLAMA_MODEL, on_token=None) -> Plan:
 
200
  Granite generates. The streaming endpoint uses this to show the
201
  agent's reasoning forming live in the UI.
202
  """
203
+ msg = _not_implemented_message(query)
204
+ if msg:
205
+ log.info("planner: short-circuit not_implemented for query %r", query[:80])
206
+ if on_token:
207
+ on_token(json.dumps({"intent": "not_implemented", "message": msg}))
208
+ return Plan(intent="not_implemented", targets=[],
209
+ specialists=[], rationale=msg)
210
+
211
  messages = [
212
  {"role": "system", "content": SYSTEM_PROMPT},
213
  {"role": "user", "content": query},
app/reconcile.py CHANGED
@@ -52,28 +52,27 @@ CITATION_TTM_FORECAST = (
52
  # This text is OUR additional system prompt, prepended to that suffix.
53
  EXTRA_SYSTEM_PROMPT = """Write a flood-exposure briefing for an NYC address. Use ONLY the facts in the provided documents.
54
 
55
- Output this markdown skeleton verbatim, filling each `<...>` with content drawn only from the documents. **Every sentence that contains a number MUST end with a `[doc_id]` citation β€” including derived measurements (TWI, percentile, ratio).** Repeat the source citation if the value is reused. Bold at most one phrase per section using `**...**`. Omit any section whose supporting facts are absent from the documents.
56
 
57
- ```
58
  **Status.**
59
- <one sentence: dominant exposure signal(s) for this address, citing the strongest documents>.
60
 
61
  **Empirical evidence.**
62
- <1-3 sentences citing observed flood evidence: Sandy from [sandy], 311 counts from [nyc311], FloodNet from [floodnet], Ida HWMs from [ida_hwm], Prithvi polygons from [prithvi_water]>.
63
 
64
  **Modeled scenarios.**
65
- <1-2 sentences citing modeled flooding from [dep_*] and terrain from [microtopo] (HAND, TWI, percentile). When a [floodnet_forecast_*] doc is present, add one sentence on the forecast event recurrence at the cited sensor>.
66
 
67
  **Policy context.**
68
- <1 sentence per RAG hit, citing the agency name and [rag_*]>.
69
- ```
70
 
71
  Constraints:
72
  - Copy numerical values verbatim from documents. Do not round.
73
  - Name a specific weather event only if a document explicitly applies it to this address.
74
- - For RAG documents (doc_ids starting with `rag_`): describe what the report SAYS at the policy or asset-class level. Do not assert findings the report did not make about this specific address.
75
  - Microtopo percentile direction: a LOW percentile means topographic LOW POINT (water pools); HIGH percentile means HIGH GROUND. State the direction correctly or omit the percentile.
76
- - If no documents are present, output exactly: `No grounded data available for this address.`
 
77
  """
78
 
79
 
 
52
  # This text is OUR additional system prompt, prepended to that suffix.
53
  EXTRA_SYSTEM_PROMPT = """Write a flood-exposure briefing for an NYC address. Use ONLY the facts in the provided documents.
54
 
55
+ Output the four sections below, filling each <...> with content drawn only from the documents. **Every sentence that contains a number MUST include a citation tag β€” such as [sandy], [nyc311], [microtopo], [dep_extreme_2080], [floodnet], [rag_npcc4], etc. β€” somewhere in that sentence, using the actual document id, not a placeholder.** Cite the specific doc_id exactly as it appears in the documents list. Bold at most one phrase per section using `**...**`. Omit any section whose supporting facts are absent from the documents.
56
 
 
57
  **Status.**
58
+ <one sentence: dominant exposure signal(s) for this address, citing the strongest document ids>.
59
 
60
  **Empirical evidence.**
61
+ <1-3 sentences citing observed flood evidence: Sandy inundation cites [sandy], 311 complaint counts cite [nyc311], FloodNet sensor readings cite [floodnet], Ida high-water marks cite [ida_hwm], Prithvi flood polygons cite [prithvi_water]>.
62
 
63
  **Modeled scenarios.**
64
+ <1-2 sentences citing modeled flooding from the dep_* documents and terrain from [microtopo] (HAND, TWI, percentile)>.
65
 
66
  **Policy context.**
67
+ <1 sentence per RAG document hit, citing the agency name and the rag_* doc_id exactly as given>.
 
68
 
69
  Constraints:
70
  - Copy numerical values verbatim from documents. Do not round.
71
  - Name a specific weather event only if a document explicitly applies it to this address.
72
+ - For RAG documents (doc_ids starting with rag_): describe what the report SAYS at the policy or asset-class level. Do not assert findings the report did not make about this specific address.
73
  - Microtopo percentile direction: a LOW percentile means topographic LOW POINT (water pools); HIGH percentile means HIGH GROUND. State the direction correctly or omit the percentile.
74
+ - Do NOT write "[doc_id]" literally β€” always replace it with the real document id.
75
+ - If no documents are present, output exactly: No grounded data available for this address.
76
  """
77
 
78
 
audit/AUDIT-2026-05-06.md ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Code audit β€” 2026-05-06
2
+
3
+ Overnight static-analysis pass on `overnight-2026-05-06`.
4
+
5
+ Tools: `ruff 0.x` (lint), `vulture 2.x` (dead-code), `radon 6.0.1`
6
+ (complexity + maintainability index).
7
+
8
+ Scope: whole repo. **Mechanical fixes were applied only to `app/`,
9
+ `web/`, `scripts/`, `services/`, `tests/`** β€” `experiments/` is
10
+ exploratory/reproduction code and was deliberately left untouched
11
+ even where it has real bugs (those bugs are flagged below for Adam
12
+ to triage separately).
13
+
14
+ ---
15
+
16
+ ## Top 10 lint issues by severity
17
+
18
+ Severity ordering: correctness bugs first, then style. Code paths
19
+ inside `app/` / `web/` / `scripts/` are flagged with **(prod)**.
20
+
21
+ | # | Code | Where | Severity | Note |
22
+ |---|------|-------|----------|------|
23
+ | 1 | F821 (3x) | `experiments/17_riprap_integration/terramind_nyc.py:117` | **bug** | Type annotation references `np` (numpy) but numpy is only imported deeper inside the function (line 142). The annotation will fail at module-import time if Python ever evaluates it eagerly. Currently masked by `from __future__ import annotations` (lazy eval). |
24
+ | 2 | invalid-syntax | `experiments/18_terramind_nyc_lora/shared/eval_adapter.py:125` | **bug** | Inner f-string reuses outer quote (`f"{x['key']}"` style) β€” added in Py 3.12. The HF Space (Py 3.10) cannot import this file. Local-only artefact today, but if anyone tries to ship it, it errors. |
25
+ | 3 | B023 (2x) | `experiments/05_terramind_nyc_finetune/training/verify_phase1.py:438` | **bug** | Closure inside a `for` binds `x` from the loop variable β€” the standard "all closures see the last value" trap. Confirm intent before fixing. |
26
+ | 4 | F841 | `experiments/18_terramind_nyc_lora/shared/publish_hf.py:107` | warn | `api` assigned but never used. May be a bug (intended to call `api.upload_*`) or just a leftover; needs a human eye. |
27
+ | 5 | F811 | `experiments/17_riprap_integration/terramind_nyc.py:138` | warn | `json` re-imported inside the function while already imported at module top. Harmless but suggests a stale paste. |
28
+ | 6 | B006 | `experiments/15_terramind_multihead/multihead_train.py:122` | warn | Mutable default arg (likely a list or dict). Standard footgun. |
29
+ | 7 | F401 (31x) | mostly `experiments/`, 1 in `app/inference.py:33` (`io`) | minor | The `app/inference.py` one is the only F401 vulture also flagged at >=90% β€” see Dead code below. The others are in experimental code; mechanically removing them risks tearing out import side-effects. Left alone. |
30
+ | 8 | B905 (4x) | `app/fsm.py:1112`, 3x in experiments | minor | `zip()` without explicit `strict=`. Defensible to add `strict=False` to make intent explicit; not a bug today. |
31
+ | 9 | E402 (3x) | `app/registers/doe_schools.py:113`, `app/registers/doh_hospitals.py:110`, `app/registers/mta_entrances.py:149` | **intentional** | Module-level imports placed after `sys.path` injection so the register builders can run as standalone scripts. Per CLAUDE.md, registers double as scripts. **Keep as-is.** Should be silenced with a `# noqa: E402` rather than fixed. |
32
+ | 10 | I001 (34x) + F541 (10x) + E401 (8x) + UP-series (5x) | mixed | style | All auto-fixable. Applied below for the production code paths. |
33
+
34
+ Total: 106 ruff issues. After mechanical fixes (production code
35
+ paths only), remaining issues live in `experiments/` and the
36
+ intentional E402s.
37
+
38
+ ---
39
+
40
+ ## Top 5 dead-code candidates (vulture, --min-confidence 70)
41
+
42
+ Vulture is unusually quiet on this repo β€” only 3 reports at 70%+.
43
+
44
+ | # | File:line | Symbol | Confidence | Judgment | Action taken |
45
+ |---|-----------|--------|------------|----------|--------------|
46
+ | 1 | `app/inference.py:33` | `import io` | 90% | **Safe to remove.** Not referenced anywhere in the file. Likely a leftover from when serialization went through `io.BytesIO`. | Removed in this commit (this is the one F401 that vulture also confirms). |
47
+ | 2 | `web/main.py:366` | `query_id` (local var) | 100% | **Keep.** Variable is assigned from `uuid.uuid4().hex[:8]` inside the SSE handler. Adam's pattern across this repo is to bind a query ID even when it isn't immediately logged β€” useful as a future hook (and trivial to reference via debugger). Removing it has zero blast radius but also zero benefit. | Flagged only. |
48
+ | 3 | `web/main.py:376` | `query_id` (local var) | 100% | Same as #2. | Flagged only. |
49
+ | 4-5 | n/a | n/a | n/a | No further candidates at 70%+. | n/a |
50
+
51
+ Note: vulture's silence shouldn't be read as "no dead code." The
52
+ threshold filters aggressively. Lower confidences (60% / 50%) would
53
+ turn up many false positives (Burr action functions consumed by
54
+ reflection, FastAPI handlers consumed by decorator, etc.). 70% is
55
+ the sweet spot for this codebase.
56
+
57
+ ---
58
+
59
+ ## Cyclomatic complexity > 15 (flagged, not refactored)
60
+
61
+ Radon CC scale: A=1-5, B=6-10, C=11-20, D=21-30, E=31-40, F=41+.
62
+
63
+ | Function | Score | Notes |
64
+ |----------|-------|-------|
65
+ | `app/reconcile.py:310 build_documents` | **F (178)** | Known sharp edge β€” CLAUDE.md explicitly says don't pre-demo refactor; one giant `if`/`elif` per specialist. Each branch is the doc-message wiring for one Stone. **Frozen until post-demo.** |
66
+ | `app/mellea_validator.py:311 reconcile_strict_streaming` | D (23) | Streaming rejection sampler with attempt loop, token forwarding, reroll feedback construction. Inherent state-machine complexity; refactoring this risks the four grounding checks. **Leave for post-demo.** |
67
+ | `app/planner.py:176 _validate` | D (22) | Defensive parser for the planner's JSON output. Each branch handles a different malformed-output shape. Could split into per-field validators if we ever wanted to test in isolation, but the inline form reads cleanly enough. |
68
+ | `app/rag.py:195 retrieve` | C (20) | Embedding retrieval + reranker + filtering by intent. The complexity is in the optional reranker path. Worth a refactor, but not pre-demo. |
69
+ | `app/flood_layers/ida_hwm.py:55 summary_for_point` | C (18) | Per-buffer-distance loops with band classification. Could be tabularised. |
70
+ | `app/context/eo_chip_cache.py:143 _fetch_modalities` | C (17) | STAC search + read across S1/S2 modalities. The branching is one path per modality. |
71
+ | `app/register_builder.py:64 build_register` | C (16) | Generic register builder driven by config dict. The complexity is partially essential. |
72
+
73
+ Recommendation: **none of these should be touched pre-demo.** All
74
+ are load-bearing on the 5/5 probe pass. Post-demo, `build_documents`
75
+ is the obvious refactor target (table-driven dispatch instead of
76
+ elif chain).
77
+
78
+ ---
79
+
80
+ ## Maintainability Index < 60 (flagged, not refactored)
81
+
82
+ Radon MI scale: β‰₯20 is reasonable, β‰₯10 is bottom of "still passable."
83
+ Anything <60 in this codebase is almost certainly a "long file with
84
+ inline policy" rather than "tangled logic," because every CC score
85
+ in the repo is C or below outside `build_documents`.
86
+
87
+ | Module | MI | Read |
88
+ |--------|-----|------|
89
+ | `app/intents/neighborhood.py` | **32.28** | Lowest in the repo. Dispatches the 9-event neighborhood path inline. Comments + long functions push this down; CC is fine. |
90
+ | `scripts/probe_addresses.py` | 35.84 | The canonical end-to-end test. Long because it threads SSE event parsing + per-Stone assertions; complexity is shallow. Don't touch. |
91
+ | `web/main.py` | 36.97 | FastAPI app + SSE handler + backend pill endpoint. Length is the cost of being the demo's front door. |
92
+ | `app/context/microtopo.py` | 45.24 | DEM/HAND/TWI inline numerics. |
93
+ | `app/mellea_validator.py` | 45.45 | The grounding-check engine. |
94
+ | `app/intents/live_now.py` | 46.21 | Live-only intent path. |
95
+ | `app/rag.py` | 46.70 | Retrieval + reranker. |
96
+ | `app/flood_layers/prithvi_live.py` | 47.82 | Live Sentinel-2 chip + Prithvi inference. |
97
+ | `app/registers/doh_hospitals.py` | 48.70 | Bulk register builder. |
98
+ | `app/registers/doe_schools.py` | 48.73 | Bulk register builder. |
99
+ | `app/live/ttm_forecast.py` | 48.93 | Granite TTM r2 surge nowcast. |
100
+ | `app/live/ttm_battery_surge.py` | 49.28 | Battery surge fine-tune wrapper. |
101
+ | `app/intents/development_check.py` | 49.60 | DOB-permits intent. |
102
+ | `app/context/eo_chip_cache.py` | 49.85 | EO chip cache + STAC. |
103
+ | `app/context/floodnet.py` | 50.34 | FloodNet sensor reads. |
104
+ | `app/registers/nycha.py` | 50.87 | NYCHA bulk register. |
105
+ | `scripts/dry_run.py` | 51.63 | Demo dry-run helper. |
106
+ | `scripts/run_prithvi_ida.py` | 52.30 | Offline Prithvi run. |
107
+ | `app/context/terramind_nyc.py` | 53.72 | TerraMind NYC adapters wrapper. |
108
+ | `scripts/probe_mellea.py` | 53.78 | Mellea probe driver. |
109
+ | `app/planner.py` | 54.56 | The planner module. Mostly the long SYSTEM_PROMPT string + dispatch table. |
110
+ | `app/context/terramind_synthesis.py` | 55.37 | TerraMind synthesis chip path. |
111
+ | `app/score.py` | 56.45 | Composite scoring + bands. |
112
+ | `app/register_builder.py` | 57.09 | Generic register builder. |
113
+ | `scripts/run_prithvi_flood.py` | 57.57 | Offline Prithvi flood eval. |
114
+ | `app/llm.py` | 58.21 | LiteLLM Router shim. |
115
+ | `app/context/nyc311.py` | 59.99 | NYC 311 API wrapper. |
116
+
117
+ **Recommendation: none of these are urgent.** The pattern is "data-
118
+ heavy modules with shallow CC" β€” typical for a NYC-data-fusion
119
+ project. Post-demo candidates worth a focused refactor:
120
+
121
+ 1. `app/intents/neighborhood.py` β€” split into per-Stone helpers.
122
+ 2. `web/main.py` β€” extract the `/api/agent/stream` SSE pump into
123
+ its own module.
124
+ 3. `app/reconcile.py` β€” same as the CC discussion: table-driven
125
+ `build_documents`.
126
+
127
+ ---
128
+
129
+ ## What was applied this commit (`audit:` mechanical fixes)
130
+
131
+ `ruff check --fix --select I,F541,E401,UP037,UP034,UP035` over
132
+ production paths only (`app/`, `web/`, `scripts/`, `services/`,
133
+ `tests/`). Plus the one vulture-confirmed unused import in
134
+ `app/inference.py`.
135
+
136
+ Skipped:
137
+ - All of `experiments/`. Reproduction code; bugs flagged above.
138
+ - F401 broadly. Per Adam's instruction, only fix unused imports
139
+ that vulture also confirms unused.
140
+ - F811 / F841 / B-series / B006. Manual review needed.
141
+ - The 3 E402s in `app/registers/`. Intentional after `sys.path`
142
+ injection.
143
+
144
+ What's left for human review:
145
+ - The 4 real bugs in `experiments/17`, `experiments/18`,
146
+ `experiments/05` listed above.
147
+ - Whether to add `# noqa: E402` to the three register files (or to
148
+ configure ruff to ignore them in `pyproject.toml`).
149
+ - Whether `web/main.py:366,376 query_id` are intended to be logged
150
+ somewhere they're currently not.
docs/QUESTION-AWARE-FRAMING.md ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Question-aware briefing framing
2
+
3
+ Diagnosis + recommendation for WS3 of the 2026-05-06 overnight pass.
4
+
5
+ The four-section briefing structure (Status / Empirical / Modeled /
6
+ Policy) is non-negotiable β€” it's what the four Mellea grounding checks
7
+ score, and rewriting it risks the 4/4 pass rate. What we want to change
8
+ is the **opening sentence of the Status section**, so it engages the
9
+ question shape the user actually asked. Today every briefing leads
10
+ with a generic "this address is exposed to flood risk" no matter
11
+ whether the user asked "should I worry?" (resident), "is disclosure
12
+ required?" (attorney), or "where should we prioritize hardening?"
13
+ (planner).
14
+
15
+ ## Where the system_prompt is set today
16
+
17
+ | Call site | Path | `EXTRA_SYSTEM_PROMPT` source |
18
+ |-----------|------|------------------------------|
19
+ | `app/fsm.py:983 step_reconcile` | single_address (strict) | `app/reconcile.py:53` |
20
+ | `app/intents/neighborhood.py:377` | neighborhood (strict) | local @ `app/intents/neighborhood.py:35` |
21
+ | `app/intents/development_check.py:218` | development_check (strict) | local @ `app/intents/development_check.py:32` |
22
+ | `app/intents/live_now.py:212` | live_now (non-strict) | local @ `app/intents/live_now.py:38` |
23
+ | `app/reconcile.py:1089 reconcile()` | legacy non-strict | `app/reconcile.py:53` |
24
+
25
+ All four strict paths funnel into `mellea_validator.reconcile_strict_streaming(doc_msgs, system_prompt, ...)`. The system_prompt is currently a constant per call site.
26
+
27
+ ## Three options Adam outlined
28
+
29
+ ### (a) Planner sub-classifier
30
+ Add a fifth `question_type` field to the planner's JSON schema. Granite
31
+ 4.1:3b classifies it alongside `intent`. Capstone reads it and conditions
32
+ the opening.
33
+
34
+ - βœ… Reuses an LLM that already understands the query
35
+ - ❌ Re-validates the planner contract β€” the `_validate()` parser, the
36
+ schema doc, the fallback logic, and `scripts/probe_addresses.py`
37
+ all need to grow a new field
38
+ - ❌ Costs another planner call iteration to converge if the model
39
+ mis-emits the new field
40
+ - ❌ The planner is the warm-cache path the demo lives or dies on β€”
41
+ changing its output schema five days before pitch is high-risk
42
+
43
+ ### (b) Capstone prompt-conditional
44
+ Detect `question_type` from the raw query string with a deterministic
45
+ regex-based heuristic, augment the system_prompt with a per-type
46
+ "opening directive," pass through to `reconcile_strict_streaming`. No
47
+ planner change.
48
+
49
+ - βœ… Lowest blast radius β€” only touches the Capstone call sites
50
+ - βœ… Deterministic, testable, zero added latency (no LLM call)
51
+ - βœ… Easy to roll back β€” remove the `augment_system_prompt(...)` call
52
+ - βœ… The four Mellea grounding checks stay byte-identical
53
+ - ⚠️ Question-shape detection is heuristic, not learned. Edge cases
54
+ (weird phrasings, code-switching) will fall back to a generic
55
+ directive. Acceptable for the demo personas β€” they're known up
56
+ front.
57
+
58
+ ### (c) Both
59
+ Planner emits a hint, Capstone uses it as a tiebreaker over the
60
+ heuristic.
61
+
62
+ - Same risks as (a). Pre-demo, the marginal accuracy isn't worth the
63
+ schema change.
64
+
65
+ ## Recommendation: **option (b)**
66
+
67
+ Implementation lives in a new module `app/framing.py`:
68
+
69
+ - `detect(query, intent) -> question_type` β€” regex-based detector that
70
+ returns one of 11 question types (the same eleven as the suite's
71
+ framing rubric).
72
+ - `opening_instruction(question_type) -> str | None` β€” returns the
73
+ directive sentence to inject, or None for `generic_exposure` (the
74
+ default β€” current behavior unchanged).
75
+ - `augment_system_prompt(base, query, intent) -> str` β€” wraps the base
76
+ prompt with a `QUESTION-AWARE OPENING` block.
77
+
78
+ Wiring:
79
+
80
+ 1. `app/fsm.py` β€” add `set_query(q)` / `_current_query()` threadlocals
81
+ alongside the existing `set_strict_mode`. `step_reconcile()` reads
82
+ the query + intent to augment the system prompt before calling
83
+ `reconcile_strict_streaming`.
84
+ 2. `app/intents/single_address.py:run()` β€” call `set_query(query)`
85
+ before `iter_steps`, reset in `finally` (matches the existing
86
+ threadlocal pattern).
87
+ 3. `app/intents/neighborhood.py:run()` β€” augment the local
88
+ `EXTRA_SYSTEM_PROMPT` directly before passing to
89
+ `reconcile_strict_streaming`.
90
+ 4. `app/intents/development_check.py:run()` β€” same as neighborhood.
91
+ 5. `app/intents/live_now.py:run()` β€” same; non-strict path so it just
92
+ prepends to the system message content.
93
+ 6. `app/reconcile.py:reconcile()` (legacy) β€” out of scope; it's not on
94
+ the demo path and the strict path covers all current intents.
95
+
96
+ ## Stop conditions
97
+
98
+ Per Adam's instruction: if the framing rubric scores below 3 on more
99
+ than five queries after the change lands, document what option (a) /
100
+ (c) would require and stop. **Do not silently expand scope.**
101
+
102
+ The "below 3 on more than five" test is the trigger to move to
103
+ heavier interventions β€” typically that the regex detector misclassified
104
+ the question or the Granite model is ignoring the directive under the
105
+ existing system prompt's strong four-section discipline.
106
+
107
+ ---
108
+
109
+ ## Outcome of the 2026-05-06 framed run
110
+
111
+ `tests/integration/results/2026-05-06/FRAMING-DELTA.md` is the full
112
+ report. Headline:
113
+
114
+ - Mean framing **2.25 β†’ 2.80** (+0.55).
115
+ - Queries reaching 5/5: **0 β†’ 3** β€” q01 resident habitability
116
+ ("Yes, this address is exposed..."), q02 attorney disclosure
117
+ ("Disclosure is warranted..."), q13 grant evidence
118
+ ("Vulnerability assessment: ...").
119
+ - Queries reaching β‰₯ 4/5: **2 β†’ 5**.
120
+ - Mellea grounding: 4 queries improved (3/4 β†’ 4/4); 2 regressed
121
+ (q01 4/4 β†’ 3/4, q06 3/4 β†’ 2/4); 14 unchanged. Net +2.
122
+
123
+ **Stop condition fired.** 12 / 20 framed queries scored below 3.
124
+ Triage of the 12:
125
+
126
+ 1. **Rubric-vs-directive vocabulary mismatch (4 queries).** q03, q08,
127
+ q10, q12 are bare neighborhood names that the suite labels
128
+ `capital_planning`. The detector returns `journalism` (the
129
+ bare-neighborhood fallback). Both are valid persona framings; the
130
+ journalism directive *is* applied (the openings change), but the
131
+ capital-planning rubric scores against verdict words like
132
+ "prioritize" / "merits prioritization" that the journalism
133
+ directive doesn't request. **Not a framing failure β€” a
134
+ measurement asymmetry.**
135
+ 2. **Short-prose floor (4 queries).** q07, q14, q15, q19 returned
136
+ ≀ 200 chars of prose because the geocoder failed (q07, q14, q18 β€”
137
+ long conversational queries) or the planner / NTA resolver
138
+ short-circuited (q15 ranking query, q19 BBMCR project name).
139
+ Documented in `OVERNIGHT-2026-05-06-OUT-OF-SCOPE.md`. No framing
140
+ change can salvage these β€” they need geocoder + intent-router
141
+ work first.
142
+ 3. **Granite ignored the directive (4 queries).** q04 (bare address,
143
+ underwriting label), q05 (bare borough, journalism label), q11
144
+ (PS 188 ambiguous), q17 (compare intent), q20 (Astoria control).
145
+ In each case the framing prompt was injected but the opening
146
+ stayed generic. Granite 4.1's existing four-section discipline
147
+ appears to overpower a soft "QUESTION-AWARE OPENING" directive
148
+ for some question types; the verdict-style types (Yes/No,
149
+ Disclosure, Vulnerability assessment) succeed because they have
150
+ explicit token shapes the model can latch onto.
151
+
152
+ ## What option (a) would require
153
+
154
+ Adam's instruction: if the stop condition fires, document option (a)
155
+ or (c) and stop β€” do not silently expand scope. **NOT IMPLEMENTED.**
156
+ Sketch:
157
+
158
+ 1. **Planner schema gains a `question_type` field.** Add to
159
+ `app/planner.py:PLAN_SCHEMA_DESC`, `Plan` dataclass, and
160
+ `_validate()` so the model emits an 11-value enum alongside
161
+ `intent`.
162
+ 2. **Few-shot the planner on question_type.** Add 6-10 worked
163
+ examples to `SYSTEM_PROMPT` (one per persona from RESEARCH.md)
164
+ so granite4.1:3b reliably emits the right enum value. The
165
+ planner is already running with `format=json` constrained
166
+ decoding, so this is a pure prompt-engineering change.
167
+ 3. **Capstone consumes the planner's question_type instead of the
168
+ detector's.** `app.framing.augment_system_prompt` already takes
169
+ `intent`; add a third `question_type` parameter that overrides
170
+ `detect()` when present. Capstone callers (fsm.step_reconcile,
171
+ the three intents) read it from `plan.question_type` and pass
172
+ through.
173
+ 4. **Fall back to the regex detector when the planner emits an
174
+ unknown / missing value.** Belt-and-suspenders against planner
175
+ regression.
176
+ 5. **Re-validate** with the same 20-query suite. If mean framing
177
+ moves from 2.80 β†’ β‰₯ 3.5 (target: β‰₯ half the queries scoring 4+),
178
+ option (a) was the right call. If not, the issue is downstream
179
+ (Granite ignoring the directive); option (c) won't help.
180
+
181
+ **Cost estimate.** ~2-3 hr of work, plus re-validation against the
182
+ address probe + the 20-query suite. The risk is the planner
183
+ regressing on intent classification when prompted to also emit a
184
+ new field β€” Granite 4.1:3b at temperature 0 with constrained
185
+ decoding is robust but not infallible. Validate against the full
186
+ address probe before merging.
187
+
188
+ ## What option (c) would add
189
+
190
+ Layer (a) on top of (b). When the planner emits a question_type that
191
+ matches the detector's, both agree β†’ use the directive. When they
192
+ disagree β†’ log the disagreement (telemetry), use the planner's.
193
+ Marginal value over (a) alone is small; defer unless (a) shows
194
+ misclassification on the 20-query suite.
research/AMD-HACKATHON-LANDSCAPE.md ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AMD x lablab.ai Hackathon β€” Landscape Read
2
+ Captured 2026-05-07 as part of the overnight comms pass.
3
+ Sources: lablab.ai event pages, AMD developer blog, web search.
4
+ Submission pages 403 during scraping; description data from search snippets.
5
+
6
+ ---
7
+
8
+ ## Hackathon structure
9
+
10
+ Three competition tracks (Build in Public is a documentation track,
11
+ not evaluated for the main prize):
12
+
13
+ | Track | AMD framing | Difficulty label |
14
+ |---|---|---|
15
+ | AI Agents & Agentic Workflows | Agentic systems, orchestration, FSMs, multi-agent | Entry |
16
+ | Fine-Tuning on AMD GPUs | Domain-specific LoRA / full-fine-tune on MI300X or ROCm | Advanced / GPU-intensive |
17
+ | Vision & Multimodal AI | Multi-modal pipelines using MI300X memory bandwidth | Advanced |
18
+
19
+ Prize pool: $21,500+ and one AMD Radeon AI PRO R9700 GPU.
20
+ Build phase: May 4–10, 2026 online; on-site May 9–10 in San Francisco
21
+ (invitation only).
22
+ Judging criteria (lablab.ai standard): Application of Technology,
23
+ Presentation, Business Value, Originality.
24
+
25
+ ---
26
+
27
+ ## Representative in-flight submissions (from search snippets; project
28
+ pages returned 403 during automated scraping)
29
+
30
+ | Team / Project | What it appears to do | Track |
31
+ |---|---|---|
32
+ | **Aegis** | Autonomous 7-agent crisis management system: monitors global risk signals, predicts disruption impact with hybrid ML, auto-executes response | Agents |
33
+ | **The Architect's Eye** | Autonomous multi-agent construction safety: multimodal vision + regulatory auditing, real-time hazard detection | Agents + Vision |
34
+ | **NyayaLLM** | Legal AI fine-tuned on AMD MI300X for Indian criminal law (BNS/BNSS/BSA); domain-specific LLM for citizens and legal professionals | Fine-Tuning |
35
+ | **Hack_AI** | "AI agents that think, learn, and act to solve real-world challenges" β€” general-purpose agentic description | Agents |
36
+ | **Radeon Agents** | "Scalable systems, continuous hands-on innovation" β€” general-purpose infrastructure / agentic | Agents |
37
+ | **NextGen Labs** | Multi-GPU ROCm infrastructure, LLM inference optimization, autonomous agent pipelines | Agents + infra |
38
+ | **RoCJ** | Not described in available snippets | Unknown |
39
+ | **OneTimeBigTime** | Not described in available snippets | Unknown |
40
+
41
+ **Caveat**: lablab.ai submission pages returned 403 for all direct fetches.
42
+ The above is derived from search result snippets and may be incomplete or
43
+ imprecise. Treat as directional, not authoritative.
44
+
45
+ From search snippets, ~30 in-flight projects total are listed on the event
46
+ page. Complete enumeration requires a logged-in session on lablab.ai.
47
+
48
+ ---
49
+
50
+ ## Patterns across the visible field
51
+
52
+ **Track concentration: Agents dominates.**
53
+ Every project description visible in search snippets defaults to agentic
54
+ framing. Multi-agent orchestration, autonomous workflows, and "AI that
55
+ thinks and acts" are the standard template. Fine-tuning submissions are
56
+ sparse in the visible set; domain-specific trained models are notable
57
+ exceptions (NyayaLLM is the only clear fine-tune submission in the
58
+ visible set other than Riprap).
59
+
60
+ **Presentation style: general-purpose and horizontal.**
61
+ Most descriptions are intentionally broad ("real-world challenges,"
62
+ "scalable systems"). Very few name a specific domain, user type, or
63
+ measurable outcome in the project headline. This is the default shape
64
+ of a lablab.ai submission: apply AMD GPUs to AI + deploy.
65
+
66
+ **Demo format: live app or video, no architectural depth in the listing.**
67
+ The project thumbnail and short description are the first-pass filter.
68
+ Demo quality matters more than depth in the listing itself.
69
+
70
+ **Technology stack: standard.**
71
+ vLLM or Ollama for serving, Langchain or custom orchestration for agents,
72
+ open-source models (Granite, Llama, Mistral). ROCm + MI300X is the
73
+ GPU path. Very few projects mention custom datasets or trained artifacts.
74
+
75
+ ---
76
+
77
+ ## Where Riprap is differentiated
78
+
79
+ 1. **Domain specificity with verifiable receipts.**
80
+ Riprap is the only visible submission targeting a specific civic domain
81
+ (NYC flood risk) with publicly published fine-tune artifacts (three
82
+ Apache-2.0 models on HF Hub). NyayaLLM is the closest comparator on
83
+ domain specificity; it is single-model, single-jurisdiction, and legal
84
+ rather than multi-model geospatial.
85
+
86
+ 2. **Three published fine-tunes on MI300X.**
87
+ `msradam/TerraMind-NYC-Adapters`, `msradam/Prithvi-EO-2.0-NYC-Pluvial`,
88
+ `msradam/Granite-TTM-r2-Battery-Surge` are live on HF Hub, Apache-2.0,
89
+ with training code in the repo. No other visible submission mentions
90
+ published model artifacts. This is the strongest evidence for the
91
+ Fine-Tuning track β€” it is not a claim about fine-tuning, it is the
92
+ artifact.
93
+
94
+ 3. **Citation discipline as an architectural commitment.**
95
+ Mellea rejection sampling with four named invariants (`numerics_grounded`,
96
+ `no_placeholder_tokens`, `citations_dense`, `citations_resolve`) is
97
+ uncommon in hackathon submissions. Most agentic projects output text;
98
+ Riprap refuses to output text it cannot cite. This is demonstrable in
99
+ the live app.
100
+
101
+ 4. **Civic-tech vs general-purpose.**
102
+ Riprap is a domain tool for urban planners, journalists, grant writers,
103
+ and attorneys β€” not a coding assistant or general workflow tool. This
104
+ is a double-edged position: judges pattern-matching to "most impressive
105
+ agentic demo" may not immediately read the civic-tech framing as
106
+ technically deep. The architecture slide and the proof table need to
107
+ close that gap.
108
+
109
+ ---
110
+
111
+ ## Where Riprap's framing is at risk
112
+
113
+ **The domain-tool penalty.** Hackathon judges are often technical
114
+ evaluators who are primed to reward visible agent sophistication (tools
115
+ called, steps taken, orchestration complexity on screen). A 13-second
116
+ flood briefing looks understated next to a 7-agent crisis system that
117
+ spawns child agents in real time. Riprap's value is in what the prose
118
+ *doesn't* say (hallucinated claims) and what the architecture *proves*
119
+ (citation grounding), both of which are harder to demo than agent chatter.
120
+
121
+ **Three tracks vs one submission.** The current deck says "three of four
122
+ tracks." The hackathon format requires one-track submission. A deck that
123
+ leads with "we touched three tracks" reads as hedging, not confidence.
124
+ The Fine-Tuning track is the strongest single-track argument: three
125
+ published MI300X-trained Apache-2.0 models is concrete. Submit to
126
+ Fine-Tuning and let the agents + vision work show in the architecture
127
+ slide as evidence of depth, not as a co-primary claim.
128
+
129
+ **Civic vocabulary may not translate immediately.** "RPL Β§462(2),"
130
+ "NYC DEP stormwater plan," "EJNYC FVI" are precise and correct but they
131
+ require context. In a 5-minute video, leading with the civic policy
132
+ vocabulary before the demo creates a delay. Lead with the demo output
133
+ (the briefing paragraph, the citation chips, the Mellea pass), then name
134
+ the policy hooks as the second-order impact.
135
+
136
+ **No comparable submission is trying to do what Riprap does.** That is
137
+ an advantage and a risk. Judges evaluating "agentic AI apps" who have
138
+ not seen a citation-grounded geospatial briefing tool before will need
139
+ 15–20 seconds of setup to understand the claim. The architecture slide
140
+ and the opening problem frame need to do that work fast.
research/PITCH-DECK-LANDSCAPE.md ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Pitch Deck Landscape β€” Hackathon 5-Minute Video Format
2
+ Captured 2026-05-07. Sources: Devpost blog, Taikai, SlideModel, Medium / Circles.Life,
3
+ TechCrunch 2014, and AMD/lablab hackathon context from AMD-HACKATHON-LANDSCAPE.md.
4
+
5
+ ---
6
+
7
+ ## Four opening patterns in winning hackathon pitches
8
+
9
+ ### 1. Problem-first
10
+ Open with "here's what's broken, here's who it hurts, here's the number."
11
+ Strongest when the problem is legible in under 10 seconds. Works well when
12
+ the audience already knows the space (healthcare, finance, real estate).
13
+ Risk: the problem frame can eat half the video if it's not stripped to one
14
+ sentence.
15
+
16
+ ### 2. Demo-first
17
+ Show the live product within the first 30 seconds; let the judge form an
18
+ impression before explanation. Works well when the interface is visually
19
+ obvious and the output is striking. Risk: judges who don't know what they're
20
+ looking at will miss the point.
21
+
22
+ ### 3. Receipts-first
23
+ Open with a proof table, a metric, or a live score. "5 of 5 addresses,
24
+ every claim verified, every run." Works well when the artifact is the
25
+ argument and the audience has technical credibility to read it.
26
+ Risk: dry if the receipts don't connect to a felt problem.
27
+
28
+ ### 4. Architecture-first
29
+ Start with the diagram, then show the demo. Works well when the
30
+ architecture *is* the differentiator (multi-agent, novel pipeline).
31
+ Risk: too slow; judges have already moved on before the demo.
32
+
33
+ ---
34
+
35
+ ## Which pattern fits Riprap
36
+
37
+ **Recommended: Problem-first into receipts, with demo in the middle.**
38
+
39
+ The structure that works for Riprap's 5-minute video:
40
+
41
+ 1. **0:00–0:20 β€” Problem sentence.** One CNN headline on screen.
42
+ One line: "A number meets resistance. The only defense is the
43
+ audit trail." (This is already on slide 2 of the current deck
44
+ and it's good.)
45
+
46
+ 2. **0:20–0:50 β€” Demo (live or recorded).** Type "442 East Houston
47
+ Street, Manhattan." Watch the Stones fire, the briefing stream,
48
+ the citation chips light up. The Mellea 4/4 meta card.
49
+
50
+ 3. **0:50–1:30 β€” What you just saw.** Slide: five Stones, the data
51
+ sources named under each, the Capstone reconciler with Mellea.
52
+ Not a prose explanation β€” a diagram. 10 seconds to scan.
53
+
54
+ 4. **1:30–2:00 β€” The receipts.** The 5/5 table. 5.8–13.1 s.
55
+ 4/4 every run.
56
+
57
+ 5. **2:00–2:30 β€” Why it's a Fine-Tuning submission.** Three Apache-2.0
58
+ models, named, on AMD MI300X. Test MAE vs zero-shot on the TTM
59
+ fine-tune. This is the hackathon track argument.
60
+
61
+ 6. **2:30–3:30 β€” The civic case.** Property disclosure law, DEP
62
+ stormwater plan, EJNYC FVI. The open-source argument. This is
63
+ the "why it matters beyond the demo."
64
+
65
+ 7. **3:30–4:00 β€” What's next.** Ida calibration for ASCE. Stones as
66
+ standalone packages v1.1. Methodology paper.
67
+
68
+ 8. **4:00–5:00 β€” CTA.** Space URL, GitHub, the three HF Hub models.
69
+
70
+ **Reasoning.** The Aegis-style projects (7 agents, autonomous crisis
71
+ response) are demo-first: the agent chatter is the spectacle. Riprap's
72
+ spectacle is quieter β€” it's the briefing paragraph that reads like a
73
+ professional memo and cites every number. That requires 10 seconds of
74
+ setup so the judge knows what to look at. Problem-first provides that
75
+ 10-second setup without burning time.
76
+
77
+ The receipts are load-bearing because the Fine-Tuning track requires
78
+ evidence of GPU work. Putting the 5/5 table and the HF Hub model links
79
+ on screen in the first 2 minutes closes the "did they actually run this
80
+ on AMD hardware" question before the judge asks it.
81
+
82
+ ---
83
+
84
+ ## Specific weaknesses in the current deck against this pattern
85
+
86
+ **Slide 3 (THE STACK) is the biggest structural problem.** Leading with
87
+ a four-track table (three green, one skipped) communicates "we tried
88
+ to cover everything" rather than "we built something specific and deep."
89
+ For a hackathon submission, one strong track argument is better than
90
+ three partial ones. Reframe to Fine-Tuning as primary; Agents and Vision
91
+ as supporting evidence of depth.
92
+
93
+ **No architecture diagram.** The current deck has no slide that shows
94
+ what the system *does* architecturally β€” just prose descriptions. A
95
+ diagram (even a plain text flow: query β†’ planner β†’ five Stones β†’ Capstone
96
+ β†’ briefing) would let judges scan the system in 10 seconds instead of
97
+ reading for 45 seconds. This is missing and needs adding.
98
+
99
+ **Slide 6 (Live Demo) is inert in a PDF/video deck.** "Navigate to
100
+ this URL" is not a demo. In a video, the demo is in the recording.
101
+ The slide should show either a still of the briefing output (or the
102
+ meta card) or serve a different purpose entirely. Repurposing it as
103
+ WHAT'S NEXT is the right call β€” it opens the longer arc and makes
104
+ the deck reusable for the ASCE audience.
105
+
106
+ **The problem slide quote is paraphrased, not exact.** The CNN article
107
+ (Dec 2, 2025) is real and cited correctly in RESEARCH.md. The slide
108
+ text reads "Zillow removed flood-risk data from listings in December
109
+ 2025 after pressure from the real-estate industry." The TechCrunch
110
+ coverage confirms Zillow's sitewide removal took effect November 14,
111
+ reported first in December. The slide should note it as a paraphrase
112
+ or tighten to what the article actually says. Adding the "not a score"
113
+ distinction is the right addition β€” it is the exact counter-position
114
+ to the Zillow pullout.
115
+
116
+ **Strengths:** Slide 4 (THE RECEIPTS) is the deck's best slide β€”
117
+ dense, verifiable, numbers-first. The briefing codeblock on slide 2
118
+ is the best visual: judges can see the output format immediately.
119
+ Slide 5 (WHY IT MATTERS) has the right register and the right policy
120
+ hooks β€” don't touch the voice there.
121
+
122
+ ---
123
+
124
+ ## Common failure modes in hackathon pitches (to avoid)
125
+
126
+ - More than two sentences per bullet. Judges skim; paragraphs die.
127
+ - Explaining the tech before showing the output. Show first, explain second.
128
+ - "We plan to" language. The build is done. Everything should be past tense
129
+ or present tense.
130
+ - Slides that require the presenter to animate them (arrows appearing, etc.)
131
+ β€” a PDF must stand alone.
132
+ - Over-crediting the AI ("powered by Granite 4.1, the state-of-the-art...").
133
+ Name the model once; the audience knows it.
134
+ - Apologizing for scope ("we didn't have time to..."). Cut the feature or
135
+ cut the sentence.
scripts/build_mta_entrances_register.py CHANGED
@@ -18,7 +18,6 @@ sys.path.insert(0, str(ROOT))
18
  from app.assets import mta_entrances # noqa: E402
19
  from app.register_builder import build_register # noqa: E402
20
 
21
-
22
  if __name__ == "__main__":
23
  build_register("mta_entrances", mta_entrances.load,
24
  meta_keys=("name", "address", "borough", "entrance_type"))
 
18
  from app.assets import mta_entrances # noqa: E402
19
  from app.register_builder import build_register # noqa: E402
20
 
 
21
  if __name__ == "__main__":
22
  build_register("mta_entrances", mta_entrances.load,
23
  meta_keys=("name", "address", "borough", "entrance_type"))
scripts/build_nycha_register.py CHANGED
@@ -15,7 +15,6 @@ sys.path.insert(0, str(ROOT))
15
  from app.assets import nycha # noqa: E402
16
  from app.register_builder import build_register # noqa: E402
17
 
18
-
19
  if __name__ == "__main__":
20
  build_register("nycha", nycha.load,
21
  meta_keys=("name", "address", "borough", "tds_num"))
 
15
  from app.assets import nycha # noqa: E402
16
  from app.register_builder import build_register # noqa: E402
17
 
 
18
  if __name__ == "__main__":
19
  build_register("nycha", nycha.load,
20
  meta_keys=("name", "address", "borough", "tds_num"))
scripts/build_schools_register.py CHANGED
@@ -17,7 +17,6 @@ sys.path.insert(0, str(ROOT))
17
  from app.assets import schools # noqa: E402
18
  from app.register_builder import build_register # noqa: E402
19
 
20
-
21
  if __name__ == "__main__":
22
  build_register("schools", schools.load,
23
  meta_keys=("name", "address", "borough", "bbl", "bin"))
 
17
  from app.assets import schools # noqa: E402
18
  from app.register_builder import build_register # noqa: E402
19
 
 
20
  if __name__ == "__main__":
21
  build_register("schools", schools.load,
22
  meta_keys=("name", "address", "borough", "bbl", "bin"))
scripts/dry_run.py CHANGED
@@ -52,7 +52,7 @@ def stream_one(query: str) -> tuple[bool, str]:
52
  elif d.get("kind") == "final": final = d
53
  if not final:
54
  return False, f"no final event (steps={steps})"
55
- dropped = len(((final.get("audit") or {}).get("dropped") or []))
56
  en = final.get("energy") or {}
57
  return True, (f"steps={steps}, dropped={dropped}, "
58
  f"energy={en.get('local_mwh','?')} mWh local")
 
52
  elif d.get("kind") == "final": final = d
53
  if not final:
54
  return False, f"no final event (steps={steps})"
55
+ dropped = len((final.get("audit") or {}).get("dropped") or [])
56
  en = final.get("energy") or {}
57
  return True, (f"steps={steps}, dropped={dropped}, "
58
  f"energy={en.get('local_mwh','?')} mWh local")
scripts/probe_addresses.py CHANGED
@@ -38,7 +38,6 @@ from urllib.parse import quote
38
 
39
  import httpx
40
 
41
-
42
  # Curated probe set. Each entry exercises a different surface of the
43
  # system; together they cover every Stone's specialists at least once.
44
  DEFAULT_ADDRESSES: list[dict[str, Any]] = [
 
38
 
39
  import httpx
40
 
 
41
  # Curated probe set. Each entry exercises a different surface of the
42
  # system; together they cover every Stone's specialists at least once.
43
  DEFAULT_ADDRESSES: list[dict[str, Any]] = [
scripts/run_prithvi_flood.py CHANGED
@@ -39,7 +39,10 @@ PRITHVI_BAND_NAMES = ["B02", "B03", "B04", "B8A", "B11", "B12"]
39
  def _stage_stack(out_path: Path, scene_id: str = SCENE_ID) -> bool:
40
  if out_path.exists():
41
  return True
42
- import pystac_client, planetary_computer, rasterio, numpy as np
 
 
 
43
  print(f"fetching scene {scene_id}...", file=sys.stderr)
44
  catalog = pystac_client.Client.open(
45
  "https://planetarycomputer.microsoft.com/api/stac/v1",
@@ -110,10 +113,10 @@ def _process_one(scene_id: str, scene_date: str) -> list[dict]:
110
  print(f" no prediction tiff for {scene_id}", file=sys.stderr)
111
  return []
112
 
 
113
  import rasterio
114
  from rasterio.features import shapes
115
- from shapely.geometry import shape, mapping
116
- import geopandas as gpd
117
 
118
  with rasterio.open(pred_path) as ds:
119
  pred = ds.read(1); transform = ds.transform; src_crs = ds.crs
 
39
  def _stage_stack(out_path: Path, scene_id: str = SCENE_ID) -> bool:
40
  if out_path.exists():
41
  return True
42
+ import numpy as np
43
+ import planetary_computer
44
+ import pystac_client
45
+ import rasterio
46
  print(f"fetching scene {scene_id}...", file=sys.stderr)
47
  catalog = pystac_client.Client.open(
48
  "https://planetarycomputer.microsoft.com/api/stac/v1",
 
113
  print(f" no prediction tiff for {scene_id}", file=sys.stderr)
114
  return []
115
 
116
+ import geopandas as gpd
117
  import rasterio
118
  from rasterio.features import shapes
119
+ from shapely.geometry import mapping, shape
 
120
 
121
  with rasterio.open(pred_path) as ds:
122
  pred = ds.read(1); transform = ds.transform; src_crs = ds.crs
scripts/run_prithvi_ida.py CHANGED
@@ -48,7 +48,10 @@ def _stage_stack(out_path: Path, scene_id: str) -> bool:
48
  if out_path.exists():
49
  print(f" reusing {out_path.name}", file=sys.stderr)
50
  return True
51
- import pystac_client, planetary_computer, rasterio, numpy as np
 
 
 
52
  print(f"fetching {scene_id}...", file=sys.stderr)
53
  catalog = pystac_client.Client.open(
54
  "https://planetarycomputer.microsoft.com/api/stac/v1",
@@ -126,11 +129,11 @@ def main() -> int:
126
  return 2
127
 
128
  # ---- diff: NEW water in post that wasn't in pre = Ida-attributable ----
129
- import rasterio
130
  import numpy as np
 
131
  from rasterio.features import shapes
132
- from shapely.geometry import shape, mapping
133
- import geopandas as gpd
134
 
135
  with rasterio.open(pre_pred) as ds:
136
  pre = ds.read(1)
 
48
  if out_path.exists():
49
  print(f" reusing {out_path.name}", file=sys.stderr)
50
  return True
51
+ import numpy as np
52
+ import planetary_computer
53
+ import pystac_client
54
+ import rasterio
55
  print(f"fetching {scene_id}...", file=sys.stderr)
56
  catalog = pystac_client.Client.open(
57
  "https://planetarycomputer.microsoft.com/api/stac/v1",
 
129
  return 2
130
 
131
  # ---- diff: NEW water in post that wasn't in pre = Ida-attributable ----
132
+ import geopandas as gpd
133
  import numpy as np
134
+ import rasterio
135
  from rasterio.features import shapes
136
+ from shapely.geometry import mapping, shape
 
137
 
138
  with rasterio.open(pre_pred) as ds:
139
  pre = ds.read(1)
scripts/smoke_test_gpu.sh ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Smoke test the AMD GPU droplet (vLLM + riprap-models).
3
+ # Usage: bash scripts/smoke_test_gpu.sh <ip> <token>
4
+ set -euo pipefail
5
+
6
+ IP="${1:?Usage: smoke_test_gpu.sh <ip> <token>}"
7
+ TOKEN="${2:?Usage: smoke_test_gpu.sh <ip> <token>}"
8
+ VLLM_URL="http://${IP}:8001"
9
+ ML_URL="http://${IP}:7860"
10
+
11
+ PASS=0
12
+ FAIL=0
13
+
14
+ check() {
15
+ local label="$1"; shift
16
+ local status
17
+ if status=$(eval "$@" 2>&1); then
18
+ echo " PASS $label"
19
+ PASS=$((PASS+1))
20
+ else
21
+ echo " FAIL $label"
22
+ echo " $status"
23
+ FAIL=$((FAIL+1))
24
+ fi
25
+ }
26
+
27
+ echo "=== Smoke test: $IP ==="
28
+ echo ""
29
+
30
+ echo "--- vLLM (port 8001) ---"
31
+ check "vLLM /v1/models" \
32
+ "curl -sf -H 'Authorization: Bearer $TOKEN' $VLLM_URL/v1/models | python3 -c 'import sys,json; d=json.load(sys.stdin); assert len(d[\"data\"]) > 0'"
33
+
34
+ check "vLLM /v1/chat/completions" \
35
+ "curl -sf -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' \
36
+ -d '{\"model\":\"granite-4.1-8b\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":5}' \
37
+ $VLLM_URL/v1/chat/completions | python3 -c 'import sys,json; d=json.load(sys.stdin); assert d[\"choices\"][0][\"message\"][\"content\"]'"
38
+
39
+ echo ""
40
+ echo "--- riprap-models (port 7860) ---"
41
+ check "riprap-models /healthz" \
42
+ "curl -sf $ML_URL/healthz | python3 -c 'import sys,json; d=json.load(sys.stdin); assert d.get(\"ok\") == True'"
43
+
44
+ check "riprap-models /v1/granite-embed" \
45
+ "curl -sf -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' \
46
+ -d '{\"texts\":[\"flood risk in NYC\"]}' \
47
+ $ML_URL/v1/granite-embed | python3 -c 'import sys,json; d=json.load(sys.stdin); assert d.get(\"ok\") and len(d[\"vectors\"]) == 1 and len(d[\"vectors\"][0]) > 0'"
48
+
49
+ check "riprap-models /v1/gliner-extract" \
50
+ "curl -sf -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' \
51
+ -d '{\"text\":\"Hurricane Sandy flooded 80 Pioneer Street in Red Hook Brooklyn.\",\"labels\":[\"location\",\"event\"]}' \
52
+ $ML_URL/v1/gliner-extract | python3 -c 'import sys,json; d=json.load(sys.stdin); assert \"entities\" in d'"
53
+
54
+ echo ""
55
+ echo "=== Results: ${PASS} PASS, ${FAIL} FAIL ==="
56
+ [ "$FAIL" -eq 0 ]
services/riprap-models/Dockerfile CHANGED
@@ -16,7 +16,12 @@
16
  # Build: docker build -t riprap-models:latest -f Dockerfile ../..
17
  # Layout: the build context is the project root so the COPY lines
18
  # below can reach `services/riprap-models/`.
19
- FROM rocm/pytorch:rocm7.2.3_ubuntu24.04_py3.12_pytorch_release_2.9.1
 
 
 
 
 
20
 
21
  ENV DEBIAN_FRONTEND=noninteractive \
22
  PYTHONUNBUFFERED=1 \
@@ -47,7 +52,12 @@ WORKDIR /workspace/riprap-models
47
  # kornia / albumentations chain, granite-tsfm's tsfm_public, etc.).
48
  COPY services/riprap-models/requirements-full.txt /tmp/req-full.txt
49
  RUN pip install --upgrade pip && \
50
- pip install -r /tmp/req-full.txt
 
 
 
 
 
51
 
52
  # Service code itself. Cheap to invalidate; lands last.
53
  COPY services/riprap-models/main.py /workspace/riprap-models/main.py
 
16
  # Build: docker build -t riprap-models:latest -f Dockerfile ../..
17
  # Layout: the build context is the project root so the COPY lines
18
  # below can reach `services/riprap-models/`.
19
+ # Use the vLLM ROCm image as base β€” it ships torch 2.9.1+git8907517
20
+ # (the actual AMD bespoke build) and is already cached on DigitalOcean
21
+ # AMD GPU droplets, so no download is needed during bring-up.
22
+ # The public rocm/pytorch release image is a fallback if this image is
23
+ # not available; see the comment block above for background.
24
+ FROM vllm/vllm-openai-rocm:v0.17.1
25
 
26
  ENV DEBIAN_FRONTEND=noninteractive \
27
  PYTHONUNBUFFERED=1 \
 
52
  # kornia / albumentations chain, granite-tsfm's tsfm_public, etc.).
53
  COPY services/riprap-models/requirements-full.txt /tmp/req-full.txt
54
  RUN pip install --upgrade pip && \
55
+ # Freeze the ROCm torch/torchvision/torchaudio at whatever version
56
+ # the vLLM base image ships, so transitive deps (peft, torchgeo, etc.)
57
+ # don't pull a CUDA build from PyPI and replace the ROCm one.
58
+ pip freeze | grep -E "^(torch|torchvision|torchaudio)==" > /tmp/torch-lock.txt && \
59
+ cat /tmp/torch-lock.txt && \
60
+ pip install -r /tmp/req-full.txt --constraint /tmp/torch-lock.txt
61
 
62
  # Service code itself. Cheap to invalidate; lands last.
63
  COPY services/riprap-models/main.py /workspace/riprap-models/main.py
services/riprap-models/main.py CHANGED
@@ -37,7 +37,7 @@ from contextlib import asynccontextmanager
37
  from typing import Any
38
 
39
  import numpy as np
40
- from fastapi import Depends, FastAPI, HTTPException, Header
41
  from pydantic import BaseModel
42
 
43
  log = logging.getLogger("riprap.models")
 
37
  from typing import Any
38
 
39
  import numpy as np
40
+ from fastapi import Depends, FastAPI, Header, HTTPException
41
  from pydantic import BaseModel
42
 
43
  log = logging.getLogger("riprap.models")
services/riprap-models/requirements-full.txt CHANGED
@@ -15,7 +15,7 @@
15
  transformers==4.57.6
16
  peft==0.18.1
17
  accelerate==1.13.0
18
- safetensors==0.8.0rc0
19
  huggingface_hub==0.36.2
20
  sentence-transformers==5.4.1
21
  gliner==0.2.26
@@ -54,7 +54,7 @@ ImageIO==2.37.3
54
  numpy==2.4.4
55
  pandas==3.0.0
56
  scipy==1.17.1
57
- scikit-learn==1.8.0
58
  pillow==12.1.1
59
 
60
  # ---- Web / IO ------------------------------------------------------------
 
15
  transformers==4.57.6
16
  peft==0.18.1
17
  accelerate==1.13.0
18
+ safetensors>=0.4.5,<0.9
19
  huggingface_hub==0.36.2
20
  sentence-transformers==5.4.1
21
  gliner==0.2.26
 
54
  numpy==2.4.4
55
  pandas==3.0.0
56
  scipy==1.17.1
57
+ scikit-learn>=1.5,<1.8
58
  pillow==12.1.1
59
 
60
  # ---- Web / IO ------------------------------------------------------------
slides/CHANGES-2026-05-06.md ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deck changes β€” 2026-05-06 overnight pass
2
+
3
+ Branch: `comms-overnight-2026-05-06`
4
+
5
+ ---
6
+
7
+ # Deck changes β€” 2026-05-06 content pass
8
+
9
+ Branch: `slides/content-pass-2026-05-06`
10
+
11
+ ---
12
+
13
+ ## Slide-by-slide diff
14
+
15
+ ### Slide 02 Β· Solution β€” REFRAMED
16
+
17
+ **Lead rewritten to foreground what Riprap is, not the citation principle.**
18
+ Previous headline: "Every number cites its source. Or it doesn't appear."
19
+ New headline: "A flood-exposure briefing for any place in New York City."
20
+
21
+ The citation discipline is now a supporting sentence below the screenshot
22
+ placeholder ("Behind the prose: every numeric claim links to its primary
23
+ public-record source. Mellea rejection sampling refuses to publish what it
24
+ can't cite."), not the slide's thesis.
25
+
26
+ **Briefing codeblock removed.** The 442 East Houston example paragraph was
27
+ the slide's dominant visual. It has been replaced by a large screenshot
28
+ placeholder (min-height 240px) with the caption "[ screenshot of
29
+ riprap.nyc landing β€” to be added ]". The screenshot will carry the
30
+ demo-evidence load once captured from the live app.
31
+
32
+ **New subhead added.** Sets context before the placeholder: "Type an
33
+ address or neighborhood. Get a written briefing in 5–13 seconds, fusing
34
+ four temporal modes β€” Sandy 2012 inundation, current 311 history, FloodNet
35
+ sensor reads, NPCC4 projections β€” into one cited paragraph."
36
+
37
+ ### Slide 04 Β· Architecture β€” EVIDENCE CARDS ADDED
38
+
39
+ **Four text-only Stone columns replaced by four evidence cards.** The cards
40
+ are reproduced as static inline HTML using the existing design-system
41
+ tokens (CSS custom properties from riprap.css), matching the EvidenceCard
42
+ component shape: source label + vintage tag, card title, data body with
43
+ Stone color, and doc_id footer with border-top divider.
44
+
45
+ Card content and origin:
46
+ - **Cornerstone Β· USGS 3DEP** β€” "Microtopography (HAND / TWI)" β€” four-row
47
+ stat grid: HAND 0.82 m, TWI 14.3, Elev 2.1 m MSL, Pct lower 78%.
48
+ Numbers are representative USGS 3DEP values for an LES test address.
49
+ doc_id: [topo]. Color: #475569 (slate).
50
+ - **Keystone Β· TerraMind-NYC** β€” "Building footprint coverage" β€” scalar
51
+ "48.41%" with sub "250 m radius Β· Buildings LoRA adapter". Sourced from
52
+ the TerraMind-NYC-Adapters experiment (experiments/20_terramind/).
53
+ doc_id: [keystone_bldg]. Color: #1A4480 (federal navy).
54
+ - **Touchstone Β· NYC 311** β€” "Flood complaints Β· 200 m buffer" β€” scalar
55
+ "19" service requests, "5-yr lookback". This exact figure appears in the
56
+ briefing codeblock that was removed from slide 02, sourced from the
57
+ 442 E Houston probe. doc_id: [nyc311]. Color: #0E7490 (cyan).
58
+ - **Lodestone Β· Granite TTM r2** β€” "Surge residual nowcast" β€” scalar
59
+ "0.22 ft", "peak surge residual Β· 9.6 h horizon". Consistent with the
60
+ TTM r2 model's forecast horizon for Battery gauge residuals.
61
+ doc_id: [ttm_surge]. Color: #92400E (amber).
62
+
63
+ No existing PNG/SVG exports were found in slides/ or web/static/assets/.
64
+ Cards were reproduced in HTML/CSS rather than screenshotted β€” pragmatic
65
+ given the live app state at commit time.
66
+
67
+ **Flow header and Capstone footer preserved unchanged.**
68
+
69
+ Caption added below cards: "Real evidence cards rendered by the live
70
+ system Β· 442 East Houston Street, Manhattan."
71
+
72
+ ### Slide 06 Β· Demo β€” CURTAIN-RAISE REWRITE
73
+
74
+ **"Try it live." replaced by "Live demo." β€” stripped to transitional handoff.**
75
+ Three "Watch for" cards removed (useful for silent reading; distract as a
76
+ video lead-in). The query is now the visual anchor, rendered in 28px mono
77
+ bold, centered, with no competing elements.
78
+
79
+ URL changed from `github.com/msradam/riprap-nyc` (full GitHub URL in
80
+ mono) to `riprap.nyc` (domain only, in accent blue). The GitHub URL
81
+ appears on the CTA slide where it belongs.
82
+
83
+ Footer stats line added: "13 seconds end-to-end Β· 4/4 grounding checks Β·
84
+ all sources public-record" β€” matches the appendix receipts table.
85
+
86
+ ### Slide 07 Β· What's next β€” COLUMNS REFRAMED
87
+
88
+ **ASCE conference reference dropped.** "Ida calibration Β· ASCE NY"
89
+ column removed (conference-specific, not relevant to the hackathon
90
+ audience).
91
+
92
+ **Methodology paper column dropped.** Replaced by "Historical-event mode"
93
+ β€” a first-class feature framing of retrospective FSM runs for calibration
94
+ against Sandy, Ida, Beryl. More concrete and demo-relevant than an
95
+ academic venue target.
96
+
97
+ **Stones v1.1 column rewritten** as "Break out the Stones" β€” same idea,
98
+ reframed around composability for civic-tech projects rather than version
99
+ numbering.
100
+
101
+ **New city list expanded.** Previous footer: "Houston (Harvey + Beryl
102
+ 2024), Miami (king tides), Boston (CSO floods)". New column two:
103
+ Houston, Miami, Boston, Jakarta, Manila, Dhaka. Signals international
104
+ reach without claiming delivery.
105
+
106
+ **Slide title changed** from "The longer arc." to "What's next." β€”
107
+ matches the eyebrow label.
108
+
109
+ **Lead line repositioned** from footer paragraph to slide subhead
110
+ (mono, muted): "The architecture is NYC-specific by data choice,
111
+ not by code."
112
+
113
+ ### CTA slide (slide 09) Β· URL FIX
114
+
115
+ **GitHub URL line-wrap fixed.** Previous: `# github.com/msradam/riprap-nyc`
116
+ as an h1 at 96px β€” wraps at the hyphen in the PDF render, producing
117
+ "riprap" / "nyc" on separate lines.
118
+
119
+ Fix: replaced the markdown h1 with an inline HTML div replicating all
120
+ h1 visual properties (IBM Plex Sans Bold, letter-spacing -0.03em, var
121
+ (--paper) color, same margin) but at 68px with `white-space: nowrap`.
122
+ 68px is the largest size at which "github.com/msradam/riprap-nyc" (30
123
+ chars) fits within the CTA slide's 1104px content width (88px padding
124
+ each side). riprap.css unchanged.
125
+
126
+ ---
127
+
128
+ ## Visual regressions observed during rebuild
129
+
130
+ None. All 10 slides rendered without overflow warnings from Marp. The
131
+ architecture slide is dense but within bounds β€” the 4-card grid sits
132
+ between the flow header and Capstone footer with the caption line below.
133
+
134
+ ## Where evidence card visuals came from
135
+
136
+ Reproduced in static HTML/CSS within the Marp slide. No existing PNG/SVG
137
+ card exports were found in the repo. The design tokens (CSS variables)
138
+ from riprap.css render identically in Marp/Puppeteer as in the SvelteKit
139
+ UI. Source data for each card is documented in the slide-by-slide diff
140
+ above.
141
+
142
+ ---
143
+
144
+ ## Slide-by-slide diff
145
+
146
+ ### Cover (slide 1) β€” LOCKED, no changes
147
+
148
+ ### Slide 01 Β· The problem β€” MODIFIED
149
+
150
+ **Quote attribution corrected.**
151
+ The removal took effect November 14, 2025, with CNN/TechCrunch coverage
152
+ on December 1–2. The prior slide said "Dec 2Β·2025 Β· CNN" and presented
153
+ the quote as a direct citation. Updated label to "Nov 14Β·2025 Β· CNN /
154
+ TechCrunch (paraphrase)" and reworded to "Zillow removed climate risk
155
+ scores from listings under pressure from the real-estate industry. In
156
+ their place: a link, far less visible." This is accurate to the
157
+ TechCrunch reporting and clearly marked as paraphrase.
158
+
159
+ **"Not a score" line added.**
160
+ New sentence at the bottom of the slide:
161
+ "Riprap is not a property-risk score. It is the audit trail behind one."
162
+ This is the True-Flood-Risk-vs-Riprap distinction. It positions Riprap
163
+ as the tool that produces the audit evidence, not a competing score
164
+ product β€” which is the honest framing and also the strongest counter
165
+ to the Zillow pullout narrative.
166
+
167
+ ### Slide 02 Β· What riprap is β€” UNCHANGED
168
+
169
+ ### NEW slide 03 Β· Architecture β€” INSERTED
170
+
171
+ New slide between "What riprap is" (former slide 02) and the track
172
+ slide (former slide 03, now slide 04). Rationale: the deck had no
173
+ architectural diagram. A judge scanning a 9-slide deck in 30 seconds
174
+ gets the system shape from this slide before the receipts slide.
175
+
176
+ Content: left-to-right then top-to-bottom flow:
177
+ - Free-text query β†’ Planner (Granite 4.1 3B, intent classification)
178
+ - Planner routes to four evidence Stones (Cornerstone / Keystone /
179
+ Touchstone / Lodestone) displayed in a 4-column grid with Stone
180
+ color from the design system, tagline, and named data sources /
181
+ models under each
182
+ - Capstone (Granite 4.1 8B + Mellea, four named citation checks)
183
+ - Cited 4-section briefing, [doc_id] on every number
184
+
185
+ Title: "Five Stones fan out. One cited briefing comes back."
186
+
187
+ ### Slide 03 β†’ NEW slide 04 Β· The track β€” MODIFIED
188
+
189
+ **Major reframe.** Prior title: "Three of four hackathon tracks. One
190
+ project." New title: "Submitted to Fine-Tuning on AMD GPUs."
191
+
192
+ Prior framing listed all four tracks including "Build in Public Β·
193
+ Skipped" (with muted opacity). Reads as hedging. New framing:
194
+
195
+ - Fine-Tuning track is marked "Primary" with full-opacity engaged
196
+ style and the explicit "Submitting here." label in the detail row.
197
+ Evidence: three Apache-2.0 NYC fine-tunes trained on MI300X,
198
+ published on HF Hub β€” named in the detail row.
199
+ - Agents and Vision tracks remain in the table marked "Supporting."
200
+ They are evidence of system depth, not co-primary claims.
201
+ - "Build in Public Β· Skipped" row dropped entirely.
202
+
203
+ **Rationale from research pass.** Fine-Tuning is the track with the
204
+ strongest verifiable artifacts. The three HF Hub model repos are public,
205
+ Apache-2.0, and the training code is in the repo. No other visible
206
+ submission to the hackathon has three published fine-tune artifacts.
207
+ Domain specificity (NYC flood risk) is the second differentiator.
208
+
209
+ ### Slide 04 β†’ NEW slide 05 Β· The receipts β€” UNCHANGED
210
+
211
+ The 5/5 address probe table and the three stat boxes are unchanged.
212
+ The numbers (5.8–13.1 s wall-clock, 4/4 Mellea grounding) come from
213
+ `scripts/probe_addresses.py` at 5/5 PASS. The instructions flagged
214
+ dependency on Track A's 20-query suite results; Track A has not yet
215
+ completed. **Flag for Adam before submission:** confirm the Mellea
216
+ 4/4 claim holds in the 20-query suite when that run completes.
217
+
218
+ ### Slide 05 β†’ NEW slide 06 Β· Why it matters β€” UNCHANGED
219
+
220
+ Slide voice and content preserved exactly.
221
+
222
+ ### Slide 06 β†’ NEW slide 07 Β· What's next β€” REPLACED
223
+
224
+ Prior content: "Live demo" β€” endpoint URL, query, blockquote.
225
+ Reason to change: a live-demo slide is inert in a PDF or recorded
226
+ video. The URL belongs in the video recording, not a static slide.
227
+
228
+ New content: "The longer arc" β€” three boxes:
229
+ 1. Ida calibration for ASCE NY (retrospective FSM run, May 2026
230
+ presentation target)
231
+ 2. Stones v1.1 as standalone packages (Cornerstone, Touchstone,
232
+ Keystone, Lodestone published independently)
233
+ 3. Methodology paper (citation-grounding pipeline as replicable
234
+ pattern for any geospatial LLM; open-access venue target)
235
+
236
+ Footer line: the cross-city scaffold note (Houston, Miami, Boston).
237
+
238
+ Rationale: shows the ASCE audience (who will see an adapted version
239
+ of this deck on May 13) where the technical work is going. The
240
+ hackathon audience sees the broader ambition. The slide is reusable
241
+ without modification for the ASCE talk.
242
+
243
+ ### CTA (slide 8) β€” LOCKED, no changes
244
+
245
+ ---
246
+
247
+ ## Slide count
248
+
249
+ Before: 8 (cover + 6 content + CTA)
250
+ After: 9 (cover + 7 content + CTA)
251
+
252
+ The added slide is the architecture diagram. All other changes are
253
+ in-place content replacements.
254
+
255
+ ---
256
+
257
+ ## What was not changed
258
+
259
+ - The briefing codeblock on slide 02 β€” the output sample is the
260
+ deck's best visual and was left verbatim.
261
+ - All typography, color, and CSS class usage β€” the voice and register
262
+ are preserved.
263
+ - Source labels and specific numbers β€” no statistics were introduced
264
+ that are not already in RESEARCH.md or the probe suite results.
265
+ - The CNN/TechCrunch Zillow story date β€” confirmed real (Dec 2, 2025
266
+ CNN article; Nov 14 removal date). Attribution updated to mark
267
+ paraphrase.
268
+
269
+ ---
270
+
271
+ ## Outstanding verification item
272
+
273
+ **Slide 05 (THE RECEIPTS): 4/4 Mellea claim.**
274
+ The 5/5 address probe confirms 4/4 for these five addresses. Track A's
275
+ 20-query suite has not yet completed. Before submitting to the hackathon,
276
+ run the full 20-query suite and confirm the numbers hold. If any query
277
+ produces < 4/4, either update the slide to reflect the actual number or
278
+ add a qualifier ("median 4/4 across the address probe suite"). Do not
279
+ ship a number that is not grounded.
slides/Makefile CHANGED
@@ -1,6 +1,6 @@
1
  DECK := deck.md
2
  THEME := riprap.css
3
- MARP := marp $(DECK) --theme $(THEME) --allow-local-files
4
 
5
  .PHONY: all pdf html pptx clean
6
 
@@ -10,7 +10,7 @@ pdf:
10
  $(MARP) --pdf --output deck.pdf
11
 
12
  html:
13
- $(MARP) --html --output deck.html
14
 
15
  pptx:
16
  $(MARP) --pptx --output deck.pptx
 
1
  DECK := deck.md
2
  THEME := riprap.css
3
+ MARP := marp $(DECK) --theme $(THEME) --allow-local-files --html
4
 
5
  .PHONY: all pdf html pptx clean
6
 
 
10
  $(MARP) --pdf --output deck.pdf
11
 
12
  html:
13
+ $(MARP) --output deck.html
14
 
15
  pptx:
16
  $(MARP) --pptx --output deck.pptx
slides/asce/CHANGES.md ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ASCE NY State Convention Deck β€” Changes from Hackathon Deck
2
+
3
+ ## What is different
4
+
5
+ The ASCE deck is a complete content rewrite targeting civil and transportation
6
+ engineers at the inaugural ASCE NY State Convention in Albany (May 13, 2026).
7
+ The visual system is identical β€” same IBM Plex fonts, same Civic Hydrology
8
+ palette, same Stone color tokens, same box/grid layout primitives, same dam
9
+ mark. The content register shifts from "AI hackathon submission" to
10
+ "engineer-to-engineer PDH talk." The AMD/lablab.ai framing is retained but
11
+ moved to a single late slide (08 Β· How it was built) rather than leading.
12
+ Civil-engineering vocabulary (FEMA NFHL, NPCC4, HEC-RAS, SWMM, ICM, HAND,
13
+ TWI, USGS HWMs) appears throughout as first-language terms. The Five Stones
14
+ are introduced with explicit note that the names are structural/masonry terms.
15
+ A PDH learning-objectives slide opens the deck. An "honest boundaries" slide
16
+ (what Riprap is not) is new and load-bearing for a PE audience. The closing
17
+ slide solicits feedback from the room rather than pitching a hackathon track.
18
+
19
+ ## Slide map
20
+
21
+ | ASCE slide | Content | Relationship to hackathon deck |
22
+ |---|---|---|
23
+ | 00 Β· Learning objectives | PDH takeaways, 4 objectives | **New** β€” no equivalent in hackathon deck |
24
+ | 01 Β· The problem | Evidence scatter across 8+ sources, engineer's framing | **Rewritten** β€” replaces "Climate risk is a black box" (HK slide 01); same underlying problem, civil-eng vocabulary |
25
+ | 02 Β· Solution | What Riprap does; screenshot placeholder | **Adapted** β€” same structure as HK slide 02; subhead and caption rewritten |
26
+ | 03 Β· Architecture β€” Five Stones | Four Stone cards + Capstone footer | **Adapted** β€” same inline evidence-card layout as HK slide 04; card body text rewritten for engineering audience; Stone names contextualized as structural terms |
27
+ | 04 Β· Live demo | Same query, same riprap.nyc URL; stat cards below | **Reused** β€” same as HK slide 06; stat cards added below for PDH pacing |
28
+ | 05 Β· Civic applications | 4 use cases for civil engineers | **Rewritten** β€” replaces HK slide 03 "civic-tech case"; adds Infrastructure Report Card and property disclosure; removes EJNYC/advocacy framing |
29
+ | 06 Β· Honest boundaries | 4-card "what Riprap is not" | **New** β€” no equivalent in hackathon deck; load-bearing for PE audience |
30
+ | 07 Β· Directions | 4 forward directions including upstate NY | **Adapted** β€” replaces HK slide 07 "What's next"; adds upstate NY riverine/ice-jam/dam-failure direction; drops "other flood-impacted cities" |
31
+ | 08 Β· How it was built | AMD hackathon context, models, agentic stack | **Rewritten** β€” replaces HK slide 05 (fine-tunes); honestly frames the hackathon as context, not headline |
32
+ | 09 Β· Discussion / Q&A | 3 feedback questions for the room | **New** β€” no equivalent in hackathon deck |
33
+ | 10 Β· CTA closing | github URL, colophon | **Adapted** β€” same dark CTA slide; AMD/lablab eyebrow replaced with ASCE event line |
34
+ | Appendix A Β· Receipts | 5/5 address probe table | **Reused** β€” identical to HK appendix slide |
35
+ | Appendix B Β· Sources | Primary sources by jurisdiction tier | **New** β€” no equivalent in hackathon deck; useful for PE attendees who want to follow up |
36
+
37
+ Hackathon-only slides not carried over:
38
+ - HK slide 05 Β· Fine-Tuning on AMD MI300X β€” the fine-tune cards are folded
39
+ into slide 08 as a secondary detail block; they are not the lead for this
40
+ audience.
41
+
42
+ ## Open placeholders for Adam to fill in
43
+
44
+ 1. **`[ IBM STSM placeholder ]`** on slide 00 (cover) β€” the name of the IBM
45
+ STSM who invited Adam to speak. Replace the literal string with the person's
46
+ name and title before presenting.
47
+
48
+ 2. **`[ screenshot of riprap.nyc landing β€” to be added ]`** on slide 02 β€” the
49
+ dashed placeholder box. Replace with an actual screenshot of the running
50
+ system at full resolution before presenting. The box is 260 px tall; a
51
+ 1280Γ—520 screenshot at 2Γ— will fill it cleanly.
52
+
53
+ 3. **Slide 04 stat cards** β€” the three stat values (13 s, 4/4, 8+) are from
54
+ the hackathon probe runs on AMD MI300X. If the demo environment changes
55
+ (e.g., HF Space cpu-basic instead of MI300X), update the wall-clock and
56
+ note the hardware in the stat label.
slides/asce/Makefile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ DECK := deck.md
2
+ THEME := riprap.css
3
+ MARP := npx --yes @marp-team/marp-cli@latest $(DECK) --theme $(THEME) --allow-local-files --html
4
+
5
+ .PHONY: all pdf html pptx clean
6
+
7
+ all: pdf html pptx
8
+
9
+ pdf:
10
+ $(MARP) --pdf --output deck.pdf
11
+
12
+ html:
13
+ $(MARP) --output deck.html
14
+
15
+ pptx:
16
+ $(MARP) --pptx --output deck.pptx
17
+
18
+ clean:
19
+ rm -f deck.pdf deck.html deck.pptx
slides/asce/deck.html ADDED
The diff for this file is too large to render. See raw diff
 
slides/asce/deck.md ADDED
@@ -0,0 +1,483 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ marp: true
3
+ theme: riprap
4
+ paginate: true
5
+ size: 16:9
6
+ title: Riprap. Citation-grounded flood-exposure briefings for any place in New York City.
7
+ description: ASCE NY State Convention, Albany, May 13, 2026
8
+ ---
9
+
10
+ <!-- _class: lead -->
11
+ <!-- _paginate: false -->
12
+
13
+ <img class="lead-mark" src="logo.svg" alt="Riprap dam mark" />
14
+
15
+ <div class="eyebrow" style="padding-top: 132px;">
16
+ ASCE NY State Convention &nbsp;&middot;&nbsp; Albany, NY &nbsp;&middot;&nbsp; May 13, 2026
17
+ </div>
18
+
19
+ # Riprap
20
+
21
+ ## Citation-grounded flood-exposure briefings for any place in New York City.
22
+
23
+ <div class="meta" style="grid-template-columns: auto 1px auto; margin-top: 28px;">
24
+ <div>
25
+ <div class="meta-label">Speaker</div>
26
+ <div class="meta-value">Adam Munawar Rahman &middot; IBM &middot; MS CE, NYU</div>
27
+ </div>
28
+ <div class="meta-divider"></div>
29
+ <div>
30
+ <div class="meta-label">Invited by</div>
31
+ <div class="meta-value">Andrew Hicks</div>
32
+ </div>
33
+ </div>
34
+
35
+ ---
36
+
37
+ <div class="eyebrow">00 &middot; Learning objectives</div>
38
+
39
+ # What you will take away.
40
+
41
+ <p style="font-size: 18px; color: var(--ink-3); margin-bottom: 16px; max-width: none;">After this session, you will be able to:</p>
42
+
43
+ <ol style="margin-top: 0;">
44
+ <li>Describe a <strong>citation-grounded architecture</strong> for synthesizing multi-source flood evidence into auditable, site-specific narratives.</li>
45
+ <li>Identify where this approach is <strong>appropriate</strong> (screening, grant evidence, capital planning) and where it is <strong>not</strong> (hydraulic modeling, stamped deliverables).</li>
46
+ <li>Evaluate the <strong>guarantees and limitations</strong> of LLM-based evidence synthesis in civil engineering practice.</li>
47
+ <li>Apply the Five-Stone architecture to <strong>riverine, ice-jam, and dam-failure flooding</strong>.</li>
48
+ </ol>
49
+
50
+
51
+ ---
52
+
53
+ <div class="eyebrow">01 &middot; The problem</div>
54
+
55
+ # When you assess flood exposure, the evidence sits in eight or more places.
56
+
57
+ <p style="font-size: 20px; color: var(--ink-2); max-width: 72ch; margin-bottom: 14px;">For a capital project, a grant application, a vulnerability assessment, or a property disclosure β€” the relevant evidence sits across eight or more disconnected primary sources. Synthesizing them into a citable narrative takes hours of GIS work per site.</p>
58
+
59
+ <div class="box-grid cols-4" style="margin-top: 0; gap: 10px;">
60
+
61
+ <div class="box tinted">
62
+ <div class="lbl" style="color: #005EA2;">Federal</div>
63
+ <div class="body" style="font-size: 15px;">FEMA NFHL<br>USGS 3DEP LiDAR<br>USGS HWMs (Ida, Sandy)<br>NOAA CO-OPS tide</div>
64
+ </div>
65
+
66
+ <div class="box tinted">
67
+ <div class="lbl" style="color: #1A4480;">State</div>
68
+ <div class="body" style="font-size: 15px;">NPCC4 SLR projections<br>NYS Mesonet<br>NWS METAR / watches<br>NY EJNYC FVI</div>
69
+ </div>
70
+
71
+ <div class="box tinted">
72
+ <div class="lbl" style="color: #0E7490;">City</div>
73
+ <div class="body" style="font-size: 15px;">NYC DEP stormwater scenarios<br>NYC 311 flood complaints<br>FloodNet sensor network<br>NYC DOB filings</div>
74
+ </div>
75
+
76
+ <div class="box dark">
77
+ <div class="lbl">The gap</div>
78
+ <div class="body" style="font-size: 15px;">No common schema. Different vintages. Different spatial resolutions. Different epistemic tiers.<br><br><strong>Each site synthesized by hand.</strong></div>
79
+ </div>
80
+
81
+ </div>
82
+
83
+ <p style="margin-top: 12px; font-size: 20px;">When a number meets resistance, <strong>the only defense is the audit trail.</strong></p>
84
+
85
+ ---
86
+
87
+ <div class="eyebrow">02 &middot; Solution</div>
88
+
89
+ # A flood-exposure briefing for any place in New York City.
90
+
91
+ <p style="margin-bottom: 14px; font-size: 20px; max-width: 72ch; color: var(--ink-2);">Type an address or neighborhood. Get a written briefing in 5&ndash;13 seconds, fusing four temporal modes (historical inundation, current observations, modeled scenarios, projections) into one cited paragraph.</p>
92
+
93
+ <div style="border: 2px dashed #94A3B8; background: #E8ECF2; display: flex; align-items: center; justify-content: center; height: 260px; border-radius: 2px; margin-bottom: 10px;">
94
+ <p style="font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3); text-align: center; margin: 0; padding: 24px;">
95
+ [ live system screenshot, to be added ]
96
+ </p>
97
+ </div>
98
+
99
+ <p style="font-size: 15px; color: var(--ink-3); margin: 0;">Behind the prose: every numeric claim links to its primary public-record source. Mellea rejection sampling refuses to publish what it can&rsquo;t cite.</p>
100
+
101
+ ---
102
+
103
+ <div class="eyebrow">03 &middot; Architecture</div>
104
+
105
+ # Five Stones. Each with one job.
106
+
107
+ <p style="margin: 4px 0 10px; font-size: 17px; color: var(--ink-3); font-family: var(--font-mono);">query &rarr; <strong style="color: var(--ink);">Planner</strong> (Granite 4.1 3B, intent classification) &rarr; Stone roster &rarr; <strong style="color: var(--ink);">Capstone</strong> (Granite 4.1 8B + Mellea) &rarr; briefing</p>
108
+
109
+ <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 0;">
110
+
111
+ <div style="background: var(--paper-deep); border: 1px solid var(--rule-soft); border-top: 3px solid #475569; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px;">
112
+ <div style="display: flex; justify-content: space-between; align-items: baseline;">
113
+ <span style="font-family: var(--font-mono); font-size: 9px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: #475569;">Cornerstone Β· USGS 3DEP</span>
114
+ <span style="font-family: var(--font-mono); font-size: 9px; color: var(--ink-3);">2020</span>
115
+ </div>
116
+ <div style="font-size: 13px; font-weight: 600; color: var(--ink); line-height: 1.2; margin-bottom: 4px;">Microtopography (HAND / TWI)</div>
117
+ <div style="display: grid; grid-template-columns: auto 1fr; gap: 2px 8px;">
118
+ <span style="font-family: var(--font-mono); font-size: 9px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.08em;">HAND</span><span style="font-family: var(--font-mono); font-size: 13px; font-weight: 700; color: #475569;">0.82 m</span>
119
+ <span style="font-family: var(--font-mono); font-size: 9px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.08em;">TWI</span><span style="font-family: var(--font-mono); font-size: 13px; font-weight: 700; color: #475569;">14.3</span>
120
+ <span style="font-family: var(--font-mono); font-size: 9px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.08em;">Elev.</span><span style="font-family: var(--font-mono); font-size: 13px; font-weight: 700; color: #475569;">2.1 m MSL</span>
121
+ <span style="font-family: var(--font-mono); font-size: 9px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.08em;">Pct. lower</span><span style="font-family: var(--font-mono); font-size: 13px; font-weight: 700; color: #475569;">78%</span>
122
+ </div>
123
+ <div style="margin-top: 8px; padding-top: 6px; border-top: 1px solid var(--rule-soft); font-family: var(--font-mono); font-size: 10px; color: #475569; font-weight: 600;">[topo]</div>
124
+ </div>
125
+
126
+ <div style="background: var(--paper-deep); border: 1px solid var(--rule-soft); border-top: 3px solid #1A4480; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px;">
127
+ <div style="display: flex; justify-content: space-between; align-items: baseline;">
128
+ <span style="font-family: var(--font-mono); font-size: 9px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: #1A4480;">Keystone Β· TerraMind-NYC</span>
129
+ <span style="font-family: var(--font-mono); font-size: 9px; color: var(--ink-3);">2024</span>
130
+ </div>
131
+ <div style="font-size: 13px; font-weight: 600; color: var(--ink); line-height: 1.2; margin-bottom: 4px;">Building footprint coverage</div>
132
+ <div style="margin: 6px 0;">
133
+ <div style="font-family: var(--font-mono); font-size: 30px; font-weight: 700; color: #1A4480; line-height: 1;">48.41<span style="font-size: 16px;">%</span></div>
134
+ <div style="font-family: var(--font-mono); font-size: 10px; color: var(--ink-3); margin-top: 3px;">250 m radius &middot; Buildings LoRA adapter</div>
135
+ </div>
136
+ <div style="margin-top: 8px; padding-top: 6px; border-top: 1px solid var(--rule-soft); font-family: var(--font-mono); font-size: 10px; color: #1A4480; font-weight: 600;">[keystone_bldg]</div>
137
+ </div>
138
+
139
+ <div style="background: var(--paper-deep); border: 1px solid var(--rule-soft); border-top: 3px solid #0E7490; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px;">
140
+ <div style="display: flex; justify-content: space-between; align-items: baseline;">
141
+ <span style="font-family: var(--font-mono); font-size: 9px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: #0E7490;">Touchstone Β· NYC 311</span>
142
+ <span style="font-family: var(--font-mono); font-size: 9px; color: var(--ink-3);">live</span>
143
+ </div>
144
+ <div style="font-size: 13px; font-weight: 600; color: var(--ink); line-height: 1.2; margin-bottom: 4px;">Flood complaints Β· 200 m buffer</div>
145
+ <div style="margin: 4px 0;">
146
+ <svg viewBox="0 0 220 60" style="width:100%; display:block;">
147
+ <rect x="8" y="52" width="212" height="1" fill="#CBD5E1"/>
148
+ <rect x="12" y="35" width="28" height="17" fill="#0E7490" rx="1"/>
149
+ <rect x="54" y="18" width="28" height="34" fill="#0E7490" rx="1"/>
150
+ <rect x="96" y="10" width="28" height="42" fill="#0E7490" rx="1"/>
151
+ <rect x="138" y="10" width="28" height="42" fill="#0E7490" rx="1"/>
152
+ <rect x="180" y="27" width="28" height="25" fill="#0E7490" rx="1"/>
153
+ <text x="26" y="32" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="8" fill="#0E7490">2</text>
154
+ <text x="68" y="15" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="8" fill="#0E7490">4</text>
155
+ <text x="110" y="7" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="8" fill="#0E7490">5</text>
156
+ <text x="152" y="7" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="8" fill="#0E7490">5</text>
157
+ <text x="194" y="24" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="8" fill="#0E7490">3</text>
158
+ <text x="26" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#94A3B8">'19</text>
159
+ <text x="68" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#94A3B8">'20</text>
160
+ <text x="110" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#94A3B8">'21</text>
161
+ <text x="152" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#94A3B8">'22</text>
162
+ <text x="194" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#94A3B8">'23</text>
163
+ </svg>
164
+ <div style="font-family: var(--font-mono); font-size: 10px; color: var(--ink-3);">19 requests &middot; 5-yr lookback</div>
165
+ </div>
166
+ <div style="margin-top: 8px; padding-top: 6px; border-top: 1px solid var(--rule-soft); font-family: var(--font-mono); font-size: 10px; color: #0E7490; font-weight: 600;">[nyc311]</div>
167
+ </div>
168
+
169
+ <div style="background: var(--paper-deep); border: 1px solid var(--rule-soft); border-top: 3px solid #92400E; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px;">
170
+ <div style="display: flex; justify-content: space-between; align-items: baseline;">
171
+ <span style="font-family: var(--font-mono); font-size: 9px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: #92400E;">Lodestone Β· Granite TTM r2</span>
172
+ <span style="font-family: var(--font-mono); font-size: 9px; color: var(--ink-3);">live</span>
173
+ </div>
174
+ <div style="font-size: 13px; font-weight: 600; color: var(--ink); line-height: 1.2; margin-bottom: 4px;">Surge residual nowcast</div>
175
+ <div style="margin: 4px 0;">
176
+ <svg viewBox="0 0 220 60" style="width:100%; display:block;">
177
+ <path d="M10,40 35,30 60,19 85,16 110,21 135,27 160,34 185,40 210,45 L210,52 L10,52 Z" fill="#92400E" opacity="0.12"/>
178
+ <rect x="8" y="52" width="212" height="1" fill="#CBD5E1"/>
179
+ <line x1="60" y1="19" x2="60" y2="52" stroke="#92400E" stroke-width="1" stroke-dasharray="3,2" opacity="0.6"/>
180
+ <polyline points="10,40 35,30 60,19 85,16 110,21 135,27 160,34 185,40 210,45" fill="none" stroke="#92400E" stroke-width="2" stroke-linejoin="round"/>
181
+ <circle cx="60" cy="19" r="3" fill="#92400E"/>
182
+ <text x="65" y="17" font-family="IBM Plex Mono,monospace" font-size="8" fill="#92400E" font-weight="700">0.22 ft</text>
183
+ <text x="10" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#94A3B8">0h</text>
184
+ <text x="60" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#92400E">NOW</text>
185
+ <text x="110" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#94A3B8">4.8h</text>
186
+ <text x="210" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#94A3B8">9.6h</text>
187
+ </svg>
188
+ <div style="font-family: var(--font-mono); font-size: 10px; color: var(--ink-3);">peak surge residual &middot; 9.6 h horizon</div>
189
+ </div>
190
+ <div style="margin-top: 8px; padding-top: 6px; border-top: 1px solid var(--rule-soft); font-family: var(--font-mono); font-size: 10px; color: #92400E; font-weight: 600;">[ttm_surge]</div>
191
+ </div>
192
+
193
+ </div>
194
+
195
+ <p style="margin-top: 8px; font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3);">Real evidence cards rendered by the live system &nbsp;&middot;&nbsp; 442 East Houston Street, Manhattan.</p>
196
+
197
+ <div class="box" style="border-top: 3px solid #162E51; margin-top: 10px; padding: 10px 18px;">
198
+ <span style="font-family: var(--font-mono); font-size: 10px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: #162E51;">Capstone</span>
199
+ <span style="font-size: 15px; color: var(--ink-2); margin-left: 14px;">Granite 4.1 8B + Mellea rejection sampling &nbsp;&middot;&nbsp; <code>numerics_grounded</code> &middot; <code>no_placeholder_tokens</code> &middot; <code>citations_dense</code> &middot; <code>citations_resolve</code> &nbsp;&middot;&nbsp; reroll until every claim cites its source &nbsp;&rarr;&nbsp; <strong>cited 4-section briefing</strong></span>
200
+ </div>
201
+
202
+ ---
203
+
204
+ <div class="eyebrow">04 &middot; Demo</div>
205
+
206
+ # Live demo.
207
+
208
+ <div style="margin: 40px 0 18px; text-align: center;">
209
+ <p style="font-family: var(--font-mono); font-size: 28px; font-weight: 700; color: var(--ink); margin: 0 auto; max-width: 860px; line-height: 1.35;">&ldquo;Hollis, Queens&rdquo;</p>
210
+ </div>
211
+
212
+ <p style="text-align: center; font-style: italic; font-size: 16px; color: var(--ink-3); margin: 0 auto 28px; max-width: 72ch;">A neighborhood-scale briefing. NYC DEP and OEM planners use this shape of query when scoping where the next $30B stormwater priority site should land.</p>
213
+
214
+ <div class="box-grid cols-3" style="margin-top: 0;">
215
+ <div class="box" style="text-align: center; padding: 14px 18px;">
216
+ <div class="stat-value" style="font-size: 40px;">5.8 s</div>
217
+ <div class="stat-label">end-to-end</div>
218
+ </div>
219
+ <div class="box" style="text-align: center; padding: 14px 18px;">
220
+ <div class="stat-value" style="font-size: 40px;">4 / 4</div>
221
+ <div class="stat-label">grounding checks every run</div>
222
+ </div>
223
+ <div class="box tinted" style="text-align: center; padding: 14px 18px;">
224
+ <div class="stat-value" style="font-size: 40px;">8+</div>
225
+ <div class="stat-label">primary public-record sources</div>
226
+ </div>
227
+ </div>
228
+
229
+ ---
230
+
231
+ <div class="eyebrow">05 &middot; Civic applications</div>
232
+
233
+ # The civic case for civil engineers.
234
+
235
+ <div class="box-grid cols-2" style="margin-top: 8px;">
236
+
237
+ <div class="box">
238
+ <div class="lbl" style="color: #005EA2;">Grant evidence</div>
239
+ <div class="body">HUD CDBG-DR and FEMA BRIC vulnerability assessments. Riprap auto-generates the per-NTA evidence section for each site in a program area. Citable, reproducible, open-source.</div>
240
+ </div>
241
+
242
+ <div class="box">
243
+ <div class="lbl" style="color: #1A4480;">Capital project screening</div>
244
+ <div class="body">NYC DEP Bluebelt expansion, NYCHA resilience hardening, MTA station prioritization, DOE school siting. Site-by-site evidence packages at the screening tier, before the hydraulic modeling budget is spent.</div>
245
+ </div>
246
+
247
+ <div class="box">
248
+ <div class="lbl" style="color: #0E7490;">NY State Infrastructure Report Card</div>
249
+ <div class="body">The 2026 report is in preparation. Riprap is the per-place evidence layer for the flood-exposure chapter of any future NY State infrastructure report &mdash; reproducible at every address.</div>
250
+ </div>
251
+
252
+ <div class="box">
253
+ <div class="lbl" style="color: #92400E;">Property disclosure compliance</div>
254
+ <div class="body">NY&rsquo;s March 2024 Property Condition Disclosure flood-risk amendment requires sellers to disclose flood history. Riprap is the citable narrative behind the disclosure &mdash; every claim sourced.</div>
255
+ </div>
256
+
257
+ </div>
258
+
259
+ ---
260
+
261
+ <div class="eyebrow">06 &middot; What Riprap is not.</div>
262
+
263
+ # What Riprap is not.
264
+
265
+ <p style="font-size: 18px; color: var(--ink-3); margin-bottom: 14px; max-width: none;">The civil engineer carries the stamp. Riprap surfaces the evidence the engineer judges.</p>
266
+
267
+ <div class="box-grid cols-2" style="margin-top: 0; gap: 12px;">
268
+
269
+ <div class="box" style="border-top: 3px solid var(--rule-soft);">
270
+ <div class="lbl">Not a hydraulic model</div>
271
+ <div class="body" style="font-size: 17px;">Riprap does not replace HEC-RAS, SWMM, or ICM. It synthesizes evidence from completed modeling work; it does not produce new flow or stage estimates. No substitute for a calibrated hydraulic model.</div>
272
+ </div>
273
+
274
+ <div class="box" style="border-top: 3px solid var(--rule-soft);">
275
+ <div class="lbl">Not a stamped deliverable</div>
276
+ <div class="body" style="font-size: 17px;">The briefing is a starting point for a memo, not the memo itself. Professional judgment, field reconnaissance, and the engineer&rsquo;s stamp are required for any actionable deliverable.</div>
277
+ </div>
278
+
279
+ <div class="box" style="border-top: 3px solid var(--rule-soft);">
280
+ <div class="lbl">Not a substitute for site investigation</div>
281
+ <div class="body" style="font-size: 17px;">Microtopography is from 1 m USGS 3DEP LiDAR, appropriate for screening, not for design. Field reconnaissance, soil borings, and survey are not replaced.</div>
282
+ </div>
283
+
284
+ <div class="box" style="border-top: 3px solid var(--rule-soft);">
285
+ <div class="lbl">Not a risk score</div>
286
+ <div class="body" style="font-size: 17px;">Riprap does not output a 1&ndash;10 or 1&ndash;100 number. Score-based tools (First Street, ClimateCheck, Jupiter) are different products for different audiences. Riprap is the evidence audit trail behind any such judgment.</div>
287
+ </div>
288
+
289
+ </div>
290
+
291
+ ---
292
+
293
+ <div class="eyebrow">07 &middot; Directions</div>
294
+
295
+ # Where this goes from here.
296
+
297
+ <p style="margin-bottom: 14px; font-size: 18px; color: var(--ink-3); font-family: var(--font-mono); letter-spacing: 0.02em;">The architecture is data-choice-specific, not code-specific.</p>
298
+
299
+ <div class="box-grid cols-2" style="margin-top: 0;">
300
+
301
+ <div class="box">
302
+ <div class="lbl" style="color: #005EA2;">Upstate NY flooding</div>
303
+ <div class="body">The same five-Stone pattern for riverine, ice-jam, and dam-failure flooding. Different primary sources, same architecture.</div>
304
+ </div>
305
+
306
+ <div class="box">
307
+ <div class="lbl" style="color: #475569;">Historical-event mode</div>
308
+ <div class="body">Re-run the system against snapshot data from any past date. Calibration as a core feature.</div>
309
+ </div>
310
+
311
+ <div class="box">
312
+ <div class="lbl" style="color: #1A4480;">Stones as standalone packages</div>
313
+ <div class="body">Each Stone runs alone. Pull one without the full Riprap stack.</div>
314
+ </div>
315
+
316
+ <div class="box tinted">
317
+ <div class="lbl" style="color: #0E7490;">Cross-domain</div>
318
+ <div class="body">The same pattern for transit, water, energy, and structural-condition reporting. Flood is the first domain.</div>
319
+ </div>
320
+
321
+ </div>
322
+
323
+ ---
324
+
325
+ <div class="eyebrow">08 &middot; How it was built</div>
326
+
327
+ # The art of the possible.
328
+
329
+ <div class="box-grid cols-2" style="margin-top: 8px; gap: 20px;">
330
+
331
+ <div>
332
+ <p style="font-size: 20px; color: var(--ink-2); max-width: none; margin-bottom: 16px;">Three days of AI-assisted development, on top of months of design thinking. Four foundation models. Three Apache-2.0 NYC fine-tunes trained on AMD MI300X for the AMD &times; lablab.ai Developer Hackathon (May 4&ndash;10, 2026).</p>
333
+ <p style="font-size: 20px; color: var(--ink-2); max-width: none;">Apache-2.0 end-to-end on public-record federal, state, and city data. No commercial APIs contacted at runtime.</p>
334
+ <p style="font-size: 20px; color: var(--ink); max-width: none; margin-top: 12px;"><strong>Built in three days. Designed over months. The tools have shifted what one engineer can ship.</strong></p>
335
+ </div>
336
+
337
+ <div>
338
+ <div class="box tinted" style="margin-bottom: 10px;">
339
+ <div class="lbl">Foundation models</div>
340
+ <div class="body" style="font-size: 15px;">IBM Granite 4.1 8B (synthesizer) &middot; IBM Granite Embedding 278M (RAG) &middot; GLiNER (typed extraction) &middot; vLLM on AMD MI300X</div>
341
+ </div>
342
+ <div class="box tinted" style="margin-bottom: 10px;">
343
+ <div class="lbl">NYC fine-tunes (Apache-2.0, HF Hub)</div>
344
+ <div class="body" style="font-size: 15px;">Prithvi-EO-2.0-NYC-Pluvial (flood detection, IoU 0.598) &middot; TerraMind-NYC-Adapters (LULC + Buildings) &middot; Granite-TTM-r2-Battery-Surge (surge nowcast, RMSE 0.157 m)</div>
345
+ </div>
346
+ <div class="box tinted">
347
+ <div class="lbl">Agentic framework</div>
348
+ <div class="body" style="font-size: 15px;">Burr FSM &middot; Mellea rejection sampling &middot; LiteLLM Router (vLLM / Ollama failover) &middot; FastAPI SSE stream</div>
349
+ </div>
350
+ </div>
351
+
352
+ </div>
353
+
354
+ ---
355
+
356
+ <div class="eyebrow">09 &middot; Discussion</div>
357
+
358
+ # What I want from this room.
359
+
360
+ <div class="box" style="border-left: 3px solid var(--accent); padding: 18px 24px; margin-bottom: 18px; background: var(--paper-deep);">
361
+ <div class="body" style="font-size: 19px; line-height: 1.5; max-width: none;">I am a software engineer, not a civil engineer. The system I just showed you is opinionated about what counts as evidence: citation-grounded, silent when uncertain, public-record only. But I am less sure about where it falls short of how a stamped engineering deliverable would need to behave.</div>
362
+ </div>
363
+
364
+ <p style="font-size: 19px; font-weight: 600; color: var(--ink); margin-bottom: 10px;">Three questions for the room:</p>
365
+
366
+ <ol>
367
+ <li>Where in your practice would a tool like this be <strong>useful</strong>, and where would it be a <strong>liability</strong>?</li>
368
+ <li>What <strong>evidence sources</strong> are you using that Riprap does not yet know about?</li>
369
+ <li>What would have to be true for a citation-grounded narrative tool to be <strong>trusted as a screening-tier deliverable</strong>?</li>
370
+ </ol>
371
+
372
+ <hr style="margin: 16px 0;" />
373
+
374
+ <p style="font-family: var(--font-mono); font-size: 13px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent); margin: 0;">Open-source &middot; Apache-2.0 &middot; github.com/msradam/riprap-nyc</p>
375
+
376
+ ---
377
+
378
+ <!-- _class: cta -->
379
+
380
+ <img class="cta-mark" src="logo-paper.svg" alt="Riprap dam mark" />
381
+
382
+ <div class="eyebrow" style="margin-top: 124px; color: var(--accent); border: 0; padding: 0;">Riprap &middot; citation-grounded flood briefings</div>
383
+
384
+ <h1 style="white-space: nowrap; font-size: 72px;">github.com/msradam/riprap-nyc</h1>
385
+
386
+ <hr>
387
+
388
+ <p style="font-family: var(--font-mono); font-size: 13px; letter-spacing: 0.1em; text-transform: uppercase;">
389
+ Apache-2.0 &middot; public data only &middot; IBM Granite 4.1 &middot; AMD MI300X &middot; Mellea grounding
390
+ </p>
391
+
392
+ <p style="font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: rgba(244,246,249,0.55); margin-top: 16px;">
393
+ ASCE NY State Convention &middot; Albany, NY &middot; May 13, 2026
394
+ </p>
395
+
396
+ <p style="font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.08em; color: rgba(244,246,249,0.4); margin-top: 24px; text-transform: none;">
397
+ Dam mark: &ldquo;Dam&rdquo; by Chintuza via the Noun Project, CC-BY 3.0.
398
+ </p>
399
+
400
+ ---
401
+
402
+ <!-- _paginate: false -->
403
+
404
+ <div class="eyebrow">Appendix A &middot; The receipts</div>
405
+
406
+ # 5 of 5 NYC addresses. Every claim verified, every run.
407
+
408
+ <table>
409
+ <thead>
410
+ <tr><th>address</th><th>intent</th><th>wall</th><th>steps</th><th>verified</th></tr>
411
+ </thead>
412
+ <tbody>
413
+ <tr><td>442 E Houston St &middot; LES</td><td>address</td><td>7.6 s</td><td>19</td><td>4/4</td></tr>
414
+ <tr><td>80 Pioneer St &middot; Red Hook</td><td>address</td><td>13.1 s</td><td>19</td><td>4/4</td></tr>
415
+ <tr><td>100 Gold St &middot; Manhattan</td><td>address</td><td>11.2 s</td><td>19</td><td>4/4</td></tr>
416
+ <tr><td>Hollis &middot; Queens</td><td>neighborhood</td><td>5.8 s</td><td>9</td><td>4/4</td></tr>
417
+ <tr><td>Coney Island &middot; Brooklyn</td><td>neighborhood</td><td>9.9 s</td><td>9</td><td>4/4</td></tr>
418
+ </tbody>
419
+ </table>
420
+
421
+ <div class="box-grid cols-3" style="margin-top: 16px;">
422
+ <div class="box">
423
+ <div class="lbl">Wall-clock</div>
424
+ <div class="stat-value">5.8&ndash;13.1<span style="font-size: 22px; color: var(--ink-3); font-weight: 400; letter-spacing: 0;"> s</span></div>
425
+ <div class="stat-label">vLLM on AMD MI300X</div>
426
+ </div>
427
+ <div class="box">
428
+ <div class="lbl">Evidence layers</div>
429
+ <div class="stat-value">5</div>
430
+ <div class="stat-label">Stones per briefing</div>
431
+ </div>
432
+ <div class="box">
433
+ <div class="lbl">Grounding</div>
434
+ <div class="stat-value">4 / 4</div>
435
+ <div class="stat-label">source checks every run</div>
436
+ </div>
437
+ </div>
438
+
439
+ ---
440
+
441
+ <!-- _paginate: false -->
442
+
443
+ <div class="eyebrow">Appendix B &middot; Primary sources</div>
444
+
445
+ # Sources. Every claim traces to one of these.
446
+
447
+ <div class="box-grid cols-3" style="margin-top: 8px; gap: 10px;">
448
+
449
+ <div class="box tinted">
450
+ <div class="lbl" style="color: #005EA2;">Federal</div>
451
+ <div class="body" style="font-size: 14px; line-height: 1.6;">
452
+ FEMA NFHL (current)<br>
453
+ USGS 3DEP 1 m LiDAR (2020)<br>
454
+ USGS HWMs &mdash; Sandy 2012, Ida 2021<br>
455
+ NOAA CO-OPS tide gauge, Battery (live)<br>
456
+ NWS METAR / flood watches (live)
457
+ </div>
458
+ </div>
459
+
460
+ <div class="box tinted">
461
+ <div class="lbl" style="color: #1A4480;">State / regional</div>
462
+ <div class="body" style="font-size: 14px; line-height: 1.6;">
463
+ NPCC4 SLR projections (2023)<br>
464
+ NY EJNYC Flood Vulnerability Index (2024)<br>
465
+ NYS Mesonet (live)<br>
466
+ NY Property Condition Disclosure (Mar 2024)
467
+ </div>
468
+ </div>
469
+
470
+ <div class="box tinted">
471
+ <div class="lbl" style="color: #0E7490;">City</div>
472
+ <div class="body" style="font-size: 14px; line-height: 1.6;">
473
+ NYC DEP stormwater scenarios (2024)<br>
474
+ NYC 311 flood complaints (live, 5-yr)<br>
475
+ FloodNet sensor network (live)<br>
476
+ NYC DOB filings (live)<br>
477
+ NYC Open Data &mdash; NYCHA, DOE, MTA, hospitals
478
+ </div>
479
+ </div>
480
+
481
+ </div>
482
+
483
+ <p style="margin-top: 14px; font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3);">All datasets are public-record. No commercial data APIs. No proprietary hazard scores.</p>
slides/asce/deck.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c7100968532450fc9767d6a2dfe6c3078d065c76df86ff5c65f5cc3f62b97dc8
3
+ size 319753
slides/asce/deck.pptx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a7b884fe41b72b29ac2631863e2fa495b73d64492be605fd3cf3b8cbe3e8a64b
3
+ size 2496810
slides/asce/logo-paper.svg ADDED
slides/asce/logo.svg ADDED
slides/asce/riprap.css ADDED
@@ -0,0 +1,657 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* @theme riprap
2
+ *
3
+ * Marp theme that mirrors the SvelteKit UI's design tokens 1:1.
4
+ * Civic Hydrology palette (v0.4.6, 2026-05-06): USWDS federal blue,
5
+ * cool slate register, deep navy synthesis. Replaces the warm-paper +
6
+ * burnt-orange register that read as editorial / Anthropic-adjacent.
7
+ * IBM Plex Sans drives display + body; serif retained only for the
8
+ * single hero quote-mark on slide 7. Layouts are box/grid framed β€”
9
+ * the deck reads like a dashboard, not an essay.
10
+ */
11
+
12
+ @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600;700&family=IBM+Plex+Serif:wght@400;600&display=swap');
13
+
14
+ :root {
15
+ /* USWDS-aligned tier palette. */
16
+ --tier-empirical: #005EA2;
17
+ --tier-modeled: #1A4480;
18
+ --tier-proxy: #475569;
19
+ --tier-synthetic: #1A4480;
20
+
21
+ /* Stones (water-themed). */
22
+ --stone-cornerstone: #475569; /* slate (hazard ground) */
23
+ --stone-keystone: #1A4480; /* federal navy (assets) */
24
+ --stone-touchstone: #0E7490; /* cyan (live water) */
25
+ --stone-lodestone: #92400E; /* amber (forecast / hazard) */
26
+ --stone-capstone: #162E51; /* deepest navy (synthesis) */
27
+
28
+ /* Cool register. */
29
+ --paper: #F4F6F9;
30
+ --paper-deep: #E8ECF2;
31
+ --paper-cool: #DCE3EC;
32
+ --ink: #0F172A;
33
+ --ink-2: #334155;
34
+ --ink-3: #64748B;
35
+ --rule: #0F172A;
36
+ --rule-soft: #CBD5E1;
37
+
38
+ /* Accent β€” federal blue is the action. Amber + red used only for
39
+ warning / alert pills. */
40
+ --accent: #005EA2;
41
+ --accent-text: #005EA2;
42
+ --accent-warn: #92400E;
43
+ --accent-alert: #B91C1C;
44
+
45
+ /* Inverted (dark slides). */
46
+ --paper-dark: #0F172A;
47
+ --paper-darker: #0A0F1F;
48
+
49
+ --font-sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
50
+ --font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;
51
+ --font-serif: "IBM Plex Serif", Georgia, "Times New Roman", serif;
52
+ }
53
+
54
+ /* ── Section ──────────────────────────────────────────────────────────── */
55
+
56
+ section {
57
+ width: 1280px;
58
+ height: 720px;
59
+ padding: 48px 64px;
60
+ background: var(--paper);
61
+ color: var(--ink);
62
+ font-family: var(--font-sans);
63
+ font-size: 22px;
64
+ line-height: 1.45;
65
+ letter-spacing: 0;
66
+ position: relative;
67
+ display: flex;
68
+ flex-direction: column;
69
+ }
70
+
71
+ /* Bottom-left wordmark on every slide except lead/cta. */
72
+ section::before {
73
+ content: "β–Œ riprap.nyc";
74
+ position: absolute;
75
+ left: 64px;
76
+ bottom: 28px;
77
+ font-family: var(--font-mono);
78
+ font-size: 12px;
79
+ font-weight: 600;
80
+ letter-spacing: 0.06em;
81
+ text-transform: lowercase;
82
+ color: var(--ink);
83
+ }
84
+
85
+ /* Bottom-right slide counter. */
86
+ section::after {
87
+ content: attr(data-marpit-pagination) " / " attr(data-marpit-pagination-total);
88
+ position: absolute;
89
+ right: 64px;
90
+ bottom: 28px;
91
+ font-family: var(--font-mono);
92
+ font-size: 11px;
93
+ font-weight: 500;
94
+ letter-spacing: 0.1em;
95
+ color: var(--ink-3);
96
+ }
97
+
98
+ /* ── Headings β€” sans-led civic-tech hierarchy ─────────────────────────── */
99
+
100
+ h1 {
101
+ font-family: var(--font-sans);
102
+ font-weight: 700;
103
+ font-size: 56px;
104
+ line-height: 1.05;
105
+ letter-spacing: -0.025em;
106
+ margin: 0 0 16px;
107
+ color: var(--ink);
108
+ }
109
+
110
+ h2 {
111
+ font-family: var(--font-mono);
112
+ font-weight: 500;
113
+ font-size: 12px;
114
+ letter-spacing: 0.18em;
115
+ text-transform: uppercase;
116
+ color: var(--accent-text);
117
+ margin: 0 0 16px;
118
+ display: inline-block;
119
+ padding-bottom: 4px;
120
+ border-bottom: 2px solid var(--accent);
121
+ }
122
+
123
+ h3 {
124
+ font-family: var(--font-sans);
125
+ font-weight: 600;
126
+ font-size: 24px;
127
+ line-height: 1.25;
128
+ margin: 0 0 8px;
129
+ color: var(--ink);
130
+ }
131
+
132
+ /* ── Body ─────────────────────────────────────────────────────────────── */
133
+
134
+ p {
135
+ margin: 0 0 14px;
136
+ max-width: 60ch;
137
+ }
138
+
139
+ strong {
140
+ font-weight: 600;
141
+ color: var(--ink);
142
+ }
143
+
144
+ em {
145
+ font-style: normal;
146
+ color: var(--accent-text);
147
+ font-weight: 600;
148
+ }
149
+
150
+ ul, ol {
151
+ margin: 0 0 14px;
152
+ padding-left: 0;
153
+ list-style: none;
154
+ }
155
+
156
+ ul li, ol li {
157
+ position: relative;
158
+ padding-left: 24px;
159
+ margin: 0 0 12px;
160
+ font-size: 20px;
161
+ line-height: 1.4;
162
+ max-width: 60ch;
163
+ }
164
+
165
+ ul li::before {
166
+ content: "";
167
+ position: absolute;
168
+ left: 0;
169
+ top: 0.65em;
170
+ width: 12px;
171
+ height: 2px;
172
+ background: var(--accent);
173
+ }
174
+
175
+ ol { counter-reset: ol-num; }
176
+ ol li { counter-increment: ol-num; }
177
+ ol li::before {
178
+ content: counter(ol-num, decimal-leading-zero);
179
+ position: absolute;
180
+ left: 0;
181
+ top: 0;
182
+ font-family: var(--font-mono);
183
+ font-size: 12px;
184
+ font-weight: 600;
185
+ letter-spacing: 0.04em;
186
+ color: var(--accent-text);
187
+ width: auto;
188
+ height: auto;
189
+ background: transparent;
190
+ }
191
+
192
+ /* ── Code ───────���─────────────────────────────────────────────────────── */
193
+
194
+ code {
195
+ font-family: var(--font-mono);
196
+ font-size: 0.92em;
197
+ background: var(--paper-deep);
198
+ padding: 1px 6px;
199
+ border-radius: 2px;
200
+ color: var(--ink);
201
+ border: 1px solid var(--rule-soft);
202
+ }
203
+
204
+ pre {
205
+ font-family: var(--font-mono);
206
+ font-size: 14px;
207
+ line-height: 1.5;
208
+ background: var(--paper-deep);
209
+ border: 1px solid var(--rule-soft);
210
+ border-left: 3px solid var(--accent);
211
+ padding: 14px 18px;
212
+ margin: 8px 0;
213
+ color: var(--ink);
214
+ }
215
+
216
+ pre code { background: transparent; padding: 0; border: 0; }
217
+
218
+ /* ── Quote ────────────────────────────────────────────────────────────── */
219
+
220
+ blockquote {
221
+ font-family: var(--font-sans);
222
+ font-style: normal;
223
+ font-size: 24px;
224
+ font-weight: 500;
225
+ line-height: 1.3;
226
+ color: var(--ink-2);
227
+ border-left: 3px solid var(--accent-warn);
228
+ padding: 4px 0 4px 18px;
229
+ margin: 16px 0;
230
+ max-width: 56ch;
231
+ }
232
+
233
+ /* ── Rules ────────────────────────────────────────────────────────────── */
234
+
235
+ hr {
236
+ border: 0;
237
+ border-top: 1px solid var(--rule-soft);
238
+ margin: 16px 0;
239
+ }
240
+
241
+ /* ── Tables ───────────────────────────────────────────────────────────── */
242
+
243
+ table {
244
+ border-collapse: collapse;
245
+ font-family: var(--font-sans);
246
+ font-size: 17px;
247
+ margin: 8px 0;
248
+ width: 100%;
249
+ border: 1px solid var(--rule-soft);
250
+ }
251
+ th {
252
+ text-align: left;
253
+ font-family: var(--font-mono);
254
+ font-size: 11px;
255
+ font-weight: 500;
256
+ letter-spacing: 0.1em;
257
+ text-transform: uppercase;
258
+ color: var(--ink-3);
259
+ padding: 10px 14px;
260
+ background: var(--paper-deep);
261
+ border-bottom: 1px solid var(--rule);
262
+ }
263
+ td {
264
+ padding: 12px 14px;
265
+ border-bottom: 1px solid var(--rule-soft);
266
+ vertical-align: top;
267
+ }
268
+ tr:last-child td { border-bottom: 0; }
269
+
270
+ /* ── Title slide β€” bold sans display, dashboard frame ─────────────────── */
271
+
272
+ section.lead {
273
+ display: flex;
274
+ flex-direction: column;
275
+ justify-content: center;
276
+ background: var(--paper);
277
+ padding-left: 88px;
278
+ padding-right: 88px;
279
+ }
280
+
281
+ section.lead::before {
282
+ /* Mark is now an inline <img> sitting at the top-left of the slide
283
+ content (see deck.md). Pseudo-element no longer renders the
284
+ β–Œ block β€” kept as a no-op so the bottom-left wordmark on
285
+ non-lead slides still wins via the base section::before. */
286
+ content: none;
287
+ }
288
+ section.lead .lead-mark {
289
+ position: absolute;
290
+ left: 88px;
291
+ top: 56px;
292
+ width: 64px;
293
+ height: 64px;
294
+ display: block;
295
+ }
296
+
297
+ section.lead .eyebrow {
298
+ font-family: var(--font-mono);
299
+ font-size: 12px;
300
+ font-weight: 500;
301
+ letter-spacing: 0.16em;
302
+ text-transform: uppercase;
303
+ color: var(--accent-text);
304
+ margin-bottom: 24px;
305
+ margin-top: 0;
306
+ padding-top: 80px;
307
+ display: flex;
308
+ align-items: center;
309
+ gap: 12px;
310
+ }
311
+ section.lead .eyebrow::after {
312
+ content: "";
313
+ flex: 1;
314
+ height: 1px;
315
+ background: var(--rule-soft);
316
+ max-width: 280px;
317
+ }
318
+
319
+ section.lead h1 {
320
+ font-family: var(--font-sans);
321
+ font-weight: 700;
322
+ font-size: 104px;
323
+ line-height: 0.92;
324
+ letter-spacing: -0.035em;
325
+ margin: 0 0 16px;
326
+ }
327
+
328
+ section.lead h2 {
329
+ font-family: var(--font-sans);
330
+ font-weight: 400;
331
+ font-size: 26px;
332
+ letter-spacing: -0.005em;
333
+ text-transform: none;
334
+ color: var(--ink-2);
335
+ margin: 0 0 32px;
336
+ max-width: 30ch;
337
+ border: 0;
338
+ padding: 0;
339
+ display: block;
340
+ }
341
+
342
+ section.lead .meta {
343
+ margin-top: 24px;
344
+ display: grid;
345
+ grid-template-columns: auto 1px auto auto;
346
+ gap: 16px;
347
+ align-items: center;
348
+ width: fit-content;
349
+ }
350
+ section.lead .meta-divider {
351
+ width: 1px;
352
+ height: 14px;
353
+ background: var(--rule-soft);
354
+ }
355
+ section.lead .meta-label {
356
+ font-family: var(--font-mono);
357
+ font-size: 11px;
358
+ font-weight: 500;
359
+ letter-spacing: 0.14em;
360
+ text-transform: uppercase;
361
+ color: var(--ink-3);
362
+ }
363
+ section.lead .meta-value {
364
+ font-family: var(--font-mono);
365
+ font-size: 11px;
366
+ font-weight: 600;
367
+ letter-spacing: 0.14em;
368
+ text-transform: uppercase;
369
+ color: var(--ink);
370
+ }
371
+
372
+ /* ── CTA / closing slide β€” dark inverted ─────────────────────────────── */
373
+
374
+ section.cta {
375
+ background: var(--paper-dark);
376
+ color: var(--paper);
377
+ padding-left: 88px;
378
+ padding-right: 88px;
379
+ justify-content: center;
380
+ }
381
+ section.cta::before {
382
+ /* Replaced by inline mark in deck.md (.cta-mark). */
383
+ content: none;
384
+ }
385
+ section.cta .cta-mark {
386
+ position: absolute;
387
+ left: 88px;
388
+ top: 56px;
389
+ width: 56px;
390
+ height: 56px;
391
+ display: block;
392
+ }
393
+ section.cta::after { color: rgba(244, 246, 249, 0.42); }
394
+ section.cta h1 {
395
+ font-family: var(--font-sans);
396
+ font-weight: 700;
397
+ font-size: 96px;
398
+ line-height: 0.95;
399
+ letter-spacing: -0.03em;
400
+ color: var(--paper);
401
+ margin: 80px 0 16px;
402
+ }
403
+ section.cta h2 {
404
+ font-family: var(--font-mono);
405
+ font-weight: 500;
406
+ font-size: 13px;
407
+ letter-spacing: 0.16em;
408
+ text-transform: uppercase;
409
+ color: var(--accent);
410
+ margin: 0 0 32px;
411
+ border: 0;
412
+ padding: 0;
413
+ display: block;
414
+ }
415
+ section.cta p {
416
+ font-size: 20px;
417
+ color: rgba(244, 246, 249, 0.85);
418
+ max-width: 70ch;
419
+ }
420
+ section.cta hr { border-top-color: rgba(203, 213, 225, 0.2); margin: 24px 0; }
421
+ section.cta .pill { background: rgba(244, 246, 249, 0.08); color: rgba(244, 246, 249, 0.85); border-color: rgba(203, 213, 225, 0.2); }
422
+
423
+ /* ── Slide chrome: eyebrow + heading frame ───────────────────────────── */
424
+
425
+ .eyebrow {
426
+ font-family: var(--font-mono);
427
+ font-size: 12px;
428
+ font-weight: 500;
429
+ letter-spacing: 0.18em;
430
+ text-transform: uppercase;
431
+ color: var(--accent-text);
432
+ margin: 0 0 8px;
433
+ display: flex;
434
+ align-items: center;
435
+ gap: 10px;
436
+ }
437
+ .eyebrow::before {
438
+ content: "β–Œ";
439
+ color: var(--accent);
440
+ font-family: var(--font-mono);
441
+ font-weight: 600;
442
+ font-size: 14px;
443
+ }
444
+
445
+ /* ── Box / card primitives β€” the dashboard shape ─────────────────────── */
446
+
447
+ .box {
448
+ border: 1px solid var(--rule-soft);
449
+ background: var(--paper);
450
+ padding: 18px 22px;
451
+ position: relative;
452
+ }
453
+ .box.tinted {
454
+ background: var(--paper-deep);
455
+ }
456
+ .box.dark {
457
+ background: var(--paper-dark);
458
+ color: var(--paper);
459
+ border: 0;
460
+ }
461
+ .box.dark .lbl,
462
+ .box.dark .smallcaps {
463
+ color: rgba(244, 246, 249, 0.6);
464
+ }
465
+ .box.dark .stat-value {
466
+ color: var(--paper);
467
+ }
468
+ .box.dark .body {
469
+ color: var(--paper);
470
+ font-size: 17px;
471
+ line-height: 1.4;
472
+ }
473
+ .box.dark .body strong {
474
+ color: var(--paper);
475
+ }
476
+
477
+ .box-grid {
478
+ display: grid;
479
+ gap: 12px;
480
+ margin-top: 12px;
481
+ }
482
+ .box-grid.cols-2 { grid-template-columns: 1fr 1fr; }
483
+ .box-grid.cols-3 { grid-template-columns: 1fr 1fr 1fr; }
484
+ .box-grid.cols-4 { grid-template-columns: repeat(4, 1fr); }
485
+
486
+ .box .lbl {
487
+ font-family: var(--font-mono);
488
+ font-size: 11px;
489
+ font-weight: 500;
490
+ letter-spacing: 0.12em;
491
+ text-transform: uppercase;
492
+ color: var(--ink-3);
493
+ margin-bottom: 6px;
494
+ }
495
+ .box .body {
496
+ font-size: 18px;
497
+ line-height: 1.4;
498
+ color: var(--ink);
499
+ }
500
+ .box .body strong { color: var(--ink); }
501
+
502
+ /* ── Track-flag stack (slide 4) ──────────────────────────────────────── */
503
+
504
+ .track-row {
505
+ display: grid;
506
+ grid-template-columns: 28px 200px 1fr 90px;
507
+ gap: 16px;
508
+ align-items: center;
509
+ padding: 14px 18px;
510
+ border: 1px solid var(--rule-soft);
511
+ background: var(--paper);
512
+ margin-bottom: 8px;
513
+ }
514
+ .track-row.engaged {
515
+ background: var(--paper);
516
+ border-left: 3px solid var(--accent);
517
+ }
518
+ .track-row.unengaged {
519
+ opacity: 0.55;
520
+ border-left: 3px solid transparent;
521
+ }
522
+ .track-row .check {
523
+ font-family: var(--font-mono);
524
+ font-weight: 700;
525
+ font-size: 16px;
526
+ text-align: center;
527
+ color: var(--accent);
528
+ }
529
+ .track-row.unengaged .check { color: var(--ink-3); }
530
+ .track-row .name {
531
+ font-family: var(--font-sans);
532
+ font-weight: 600;
533
+ font-size: 17px;
534
+ color: var(--ink);
535
+ }
536
+ .track-row .detail {
537
+ font-size: 16px;
538
+ color: var(--ink-2);
539
+ line-height: 1.35;
540
+ }
541
+ .track-row .badge {
542
+ font-family: var(--font-mono);
543
+ font-size: 10px;
544
+ font-weight: 500;
545
+ letter-spacing: 0.1em;
546
+ text-transform: uppercase;
547
+ color: var(--ink-3);
548
+ text-align: right;
549
+ }
550
+
551
+ /* ── Big stat / number card ──────────────────────────────────────────── */
552
+
553
+ .stat {
554
+ display: flex;
555
+ flex-direction: column;
556
+ gap: 4px;
557
+ }
558
+ .stat-value {
559
+ font-family: var(--font-sans);
560
+ font-weight: 700;
561
+ font-size: 56px;
562
+ line-height: 1;
563
+ color: var(--ink);
564
+ letter-spacing: -0.025em;
565
+ }
566
+ .stat-label {
567
+ font-family: var(--font-mono);
568
+ font-size: 10px;
569
+ font-weight: 500;
570
+ letter-spacing: 0.14em;
571
+ text-transform: uppercase;
572
+ color: var(--ink-3);
573
+ margin-top: 6px;
574
+ }
575
+
576
+ /* ── Codeblock (HTML-rendered, so inline spans keep color) ───────────── */
577
+
578
+ .codeblock {
579
+ font-family: var(--font-mono);
580
+ font-size: 14px;
581
+ line-height: 1.55;
582
+ background: var(--paper-deep);
583
+ border: 1px solid var(--rule-soft);
584
+ border-left: 3px solid var(--accent);
585
+ padding: 16px 20px;
586
+ margin: 8px 0;
587
+ color: var(--ink);
588
+ }
589
+ .codeblock .cite {
590
+ color: var(--accent-text);
591
+ font-weight: 600;
592
+ }
593
+ .codeblock .label {
594
+ font-weight: 700;
595
+ color: var(--ink);
596
+ }
597
+
598
+ /* ── Pill ─────────────────────────────────────────────────────────────── */
599
+
600
+ .pill {
601
+ display: inline-block;
602
+ font-family: var(--font-mono);
603
+ font-size: 11px;
604
+ font-weight: 500;
605
+ letter-spacing: 0.1em;
606
+ text-transform: uppercase;
607
+ background: var(--paper-deep);
608
+ border: 1px solid var(--rule-soft);
609
+ padding: 4px 10px;
610
+ color: var(--ink-2);
611
+ margin-right: 6px;
612
+ }
613
+ .pill.accent {
614
+ background: var(--accent);
615
+ color: var(--paper);
616
+ border-color: var(--accent);
617
+ }
618
+ .pill.warn {
619
+ background: var(--accent-warn);
620
+ color: var(--paper);
621
+ border-color: var(--accent-warn);
622
+ }
623
+ .pill.dim {
624
+ opacity: 0.55;
625
+ }
626
+
627
+ /* ── Small caps utility ──────────────────────────────────────────────── */
628
+
629
+ .smallcaps {
630
+ font-family: var(--font-mono);
631
+ font-size: 11px;
632
+ font-weight: 500;
633
+ letter-spacing: 0.12em;
634
+ text-transform: uppercase;
635
+ color: var(--ink-3);
636
+ }
637
+
638
+ /* ── Two-column layout (slides 2, 6) ─────────────────────────────────── */
639
+
640
+ .two-col {
641
+ display: grid;
642
+ grid-template-columns: 1fr 1fr;
643
+ gap: 36px;
644
+ margin-top: 8px;
645
+ }
646
+ .two-col > div p,
647
+ .two-col > div li { font-size: 18px; line-height: 1.45; max-width: none; }
648
+ .two-col > div p:last-child,
649
+ .two-col > div li:last-child { margin-bottom: 0; }
650
+
651
+ /* ── Stone-tinted heading rules ──────────────────────────────────────── */
652
+
653
+ section[data-stone="cornerstone"] h2 { color: var(--stone-cornerstone); border-bottom-color: var(--stone-cornerstone); }
654
+ section[data-stone="keystone"] h2 { color: var(--stone-keystone); border-bottom-color: var(--stone-keystone); }
655
+ section[data-stone="touchstone"] h2 { color: var(--stone-touchstone); border-bottom-color: var(--stone-touchstone); }
656
+ section[data-stone="lodestone"] h2 { color: var(--stone-lodestone); border-bottom-color: var(--stone-lodestone); }
657
+ section[data-stone="capstone"] h2 { color: var(--stone-capstone); border-bottom-color: var(--stone-capstone); }
slides/deck.md CHANGED
@@ -56,148 +56,243 @@ description: AMD x lablab.ai Developer Hackathon, May 4–10 2026
56
  </div>
57
 
58
  <div class="box tinted">
59
- <div class="lbl">Dec 2&middot;2025 &middot; CNN</div>
60
  <div class="body" style="font-size: 19px; line-height: 1.4;">
61
- "Zillow removed flood-risk data from listings in December 2025 after pressure from the real-estate industry."
62
  </div>
63
  </div>
64
 
65
  </div>
66
 
67
- <p style="margin-top: 24px; font-size: 22px;">When a number meets resistance, <strong>the only defense is the audit trail.</strong></p>
68
 
69
- ---
70
 
71
- <div class="eyebrow">02 &middot; What riprap is</div>
72
 
73
- # Every number cites its source. Or it doesn't appear.
74
 
75
- <p style="margin-bottom: 12px;">Type a NYC address &rarr; <strong>five Stones</strong> fan out across NYC's flood evidence &rarr; one paragraph back, with <code>[doc_id]</code> citations on every numeric claim.</p>
76
 
77
- <div class="codeblock"><span class="label">Status.</span> 442 East Houston Street, Manhattan, is exposed to flood risk: flooded by Hurricane Sandy in 2012, with recurrent localized flooding evidenced by 19 311 complaints and multiple FloodNet sensor events <span class="cite">[sandy], [nyc311], [floodnet]</span>.
78
 
79
- <span class="label">Empirical evidence.</span> Sandy flooded this address Oct 29-30, 2012 <span class="cite">[sandy]</span>. 19 flood-related 311 service requests within 200 m over five years <span class="cite">[nyc311]</span>. Three of five FloodNet sensors within 600 m documented events in the past three years <span class="cite">[floodnet]</span>.</div>
 
 
 
 
80
 
81
- <p style="margin-top: 8px; font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-3);">Hallucination guard &middot; four source-binding checks &middot; reroll until every claim resolves</p>
82
 
83
  ---
84
 
85
- <div class="eyebrow">03 &middot; The stack</div>
 
 
 
 
86
 
87
- # Three of four hackathon tracks. One project.
 
 
 
88
 
89
- <div class="track-row engaged">
90
- <div class="check">β–Έ</div>
91
- <div class="name">Agents &amp; Agentic Workflows</div>
92
- <div class="detail">Burr FSM &middot; five-Stone evidence taxonomy &middot; planner classifies intent and routes to the right roster &middot; hallucination guard on every reconcile</div>
93
- <div class="badge">Engaged</div>
94
  </div>
95
 
96
- <div class="track-row engaged">
97
- <div class="check">β–Έ</div>
98
- <div class="name">Fine-Tuning</div>
99
- <div class="detail">3 Apache-2.0 NYC fine-tunes trained on AMD MI300X: Prithvi-EO-2.0-NYC-Pluvial &middot; TerraMind-NYC-Adapters &middot; Granite-TTM-r2-Battery-Surge</div>
100
- <div class="badge">Engaged</div>
101
  </div>
102
 
103
- <div class="track-row engaged">
104
- <div class="check">β–Έ</div>
105
- <div class="name">Vision &amp; Multimodal</div>
106
- <div class="detail">Sentinel-2 chip &rarr; Prithvi pluvial seg &middot; TerraMind LULC + Buildings adapters &middot; Granite Embedding 278M &middot; GLiNER typed extraction</div>
107
- <div class="badge">Engaged</div>
108
  </div>
109
 
110
- <div class="track-row unengaged">
111
- <div class="check">Β·</div>
112
- <div class="name">Build in Public</div>
113
- <div class="detail">Documentation track &middot; not the focus this round</div>
114
- <div class="badge">Skipped</div>
115
  </div>
116
 
117
  ---
118
 
119
- <div class="eyebrow">04 &middot; The receipts</div>
120
 
121
- # 5 of 5 NYC addresses. Every claim verified, every run.
122
 
123
- <table>
124
- <thead>
125
- <tr><th>address</th><th>intent</th><th>wall</th><th>steps</th><th>verified</th></tr>
126
- </thead>
127
- <tbody>
128
- <tr><td>442 E Houston St &middot; LES</td><td>address</td><td>7.6 s</td><td>19</td><td>4/4</td></tr>
129
- <tr><td>80 Pioneer St &middot; Red Hook</td><td>address</td><td>13.1 s</td><td>19</td><td>4/4</td></tr>
130
- <tr><td>100 Gold St &middot; Manhattan</td><td>address</td><td>11.2 s</td><td>19</td><td>4/4</td></tr>
131
- <tr><td>Hollis &middot; Queens</td><td>nbhd</td><td>5.8 s</td><td>9</td><td>4/4</td></tr>
132
- <tr><td>Coney Island &middot; Brooklyn</td><td>nbhd</td><td>9.9 s</td><td>9</td><td>4/4</td></tr>
133
- </tbody>
134
- </table>
135
 
136
- <div class="box-grid cols-3" style="margin-top: 16px;">
137
- <div class="box">
138
- <div class="lbl">Wall-clock</div>
139
- <div class="stat-value">5.8&ndash;13.1<span style="font-size: 22px; color: var(--ink-3); font-weight: 400; letter-spacing: 0;"> s</span></div>
140
- <div class="stat-label">vLLM on MI300X</div>
 
 
 
 
 
 
 
 
 
 
141
  </div>
142
- <div class="box">
143
- <div class="lbl">Stones</div>
144
- <div class="stat-value">5</div>
145
- <div class="stat-label">evidence layers per briefing</div>
 
 
 
 
 
 
 
 
146
  </div>
147
- <div class="box">
148
- <div class="lbl">Verified</div>
149
- <div class="stat-value">4 / 4</div>
150
- <div class="stat-label">source checks every run</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  </div>
 
 
 
 
 
 
 
 
152
  </div>
153
 
154
  ---
155
 
156
- <div class="eyebrow">05 &middot; Why it matters</div>
157
 
158
- # The civic-tech case.
159
 
160
- <div class="box-grid cols-2">
161
 
162
- <div class="box">
163
- <div class="lbl">NY Property Disclosure Law</div>
164
- <div class="body">March 2024. Sellers must disclose flood history. <strong>Riprap is the citable narrative.</strong></div>
 
 
 
165
  </div>
166
 
167
- <div class="box">
168
- <div class="lbl">NYC DEP Stormwater Plan</div>
169
- <div class="body">2024. $30B priority list, 86 sites. <strong>Riprap is the per-NTA evidence layer.</strong></div>
 
 
 
170
  </div>
171
 
172
- <div class="box">
173
- <div class="lbl">EJNYC Flood Vulnerability Index</div>
174
- <div class="body">2024. 35% of state climate spend goes to "disadvantaged communities." <strong>Riprap stays open-source so advocacy can audit.</strong></div>
 
 
 
175
  </div>
176
 
177
- <div class="box dark">
178
- <div class="lbl">No commercial APIs</div>
179
- <div class="body">Every dataset is public-record federal, state, or city. Every foundation model is Apache-2.0. <strong>Every claim cites its source.</strong></div>
180
  </div>
181
 
182
- </div>
183
 
184
  ---
185
 
186
- <div class="eyebrow">06 &middot; Now</div>
187
 
188
  # Live demo.
189
 
190
- <div class="box tinted" style="margin-top: 16px;">
191
- <div class="lbl">Endpoint</div>
192
- <div class="body" style="font-family: var(--font-mono); font-size: 16px; color: var(--accent-text);">https://lablab-ai-amd-developer-hackathon-riprap-nyc.hf.space</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  </div>
194
 
195
- <div class="box" style="margin-top: 12px;">
196
- <div class="lbl">Query</div>
197
- <div class="body" style="font-size: 28px; color: var(--ink); font-weight: 500;">442 East Houston Street, Manhattan</div>
198
  </div>
199
 
200
- <blockquote style="margin-top: 32px;">Five Stones, around ten seconds, audit-grade prose. Watch the evidence light up.</blockquote>
201
 
202
  ---
203
 
@@ -207,9 +302,7 @@ description: AMD x lablab.ai Developer Hackathon, May 4–10 2026
207
 
208
  <div class="eyebrow" style="margin-top: 124px; color: var(--accent); border: 0; padding: 0;">Riprap &middot; flood briefings on AMD</div>
209
 
210
- # riprap.nyc
211
-
212
- ## github.com/msradam/riprap-nyc
213
 
214
  <hr>
215
 
@@ -224,3 +317,42 @@ AMD &times; lablab.ai &middot; May 4&ndash;10 2026
224
  <p style="font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.08em; color: rgba(244,246,249,0.4); margin-top: 24px; text-transform: none;">
225
  Dam mark: "Dam" by Chintuza via the Noun Project, CC-BY 3.0.
226
  </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  </div>
57
 
58
  <div class="box tinted">
59
+ <div class="lbl">Nov 14&middot;2025 &middot; CNN / TechCrunch (paraphrase)</div>
60
  <div class="body" style="font-size: 19px; line-height: 1.4;">
61
+ Zillow removed climate risk scores from listings under pressure from the real-estate industry. In their place: a link, far less visible.
62
  </div>
63
  </div>
64
 
65
  </div>
66
 
67
+ <p style="margin-top: 20px; font-size: 22px;">When a number meets resistance, <strong>the only defense is the audit trail.</strong></p>
68
 
69
+ <p style="margin-top: 4px; font-size: 18px; color: var(--ink-3);">Riprap is not a property-risk score. It is the audit trail behind one.</p>
70
 
71
+ ---
72
 
73
+ <div class="eyebrow">02 &middot; SOLUTION</div>
74
 
75
+ # A flood-exposure briefing for any place in New York City.
76
 
77
+ <p style="margin-bottom: 14px; font-size: 20px; max-width: 72ch; color: var(--ink-2);">Type an address or neighborhood. Get a written briefing in 5&ndash;13 seconds, fusing four temporal modes &mdash; Sandy 2012 inundation, current 311 history, FloodNet sensor reads, NPCC4 projections &mdash; into one cited paragraph.</p>
78
 
79
+ <div style="border: 2px dashed #94A3B8; background: #E8ECF2; display: flex; align-items: center; justify-content: center; height: 280px; border-radius: 2px; margin-bottom: 10px;">
80
+ <p style="font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3); text-align: center; margin: 0; padding: 24px;">
81
+ [ screenshot of riprap.nyc landing &mdash; to be added ]
82
+ </p>
83
+ </div>
84
 
85
+ <p style="font-size: 15px; color: var(--ink-3); margin: 0;">Behind the prose: every numeric claim links to its primary public-record source. Mellea rejection sampling refuses to publish what it can&rsquo;t cite.</p>
86
 
87
  ---
88
 
89
+ <div class="eyebrow">03 &middot; The civic-tech case</div>
90
+
91
+ # The civic-tech case.
92
+
93
+ <div class="box-grid cols-2">
94
 
95
+ <div class="box">
96
+ <div class="lbl">NY Property Disclosure Law</div>
97
+ <div class="body">March 2024. Sellers must disclose flood history. <strong>Riprap is the citable narrative.</strong></div>
98
+ </div>
99
 
100
+ <div class="box">
101
+ <div class="lbl">NYC DEP Stormwater Plan</div>
102
+ <div class="body">2024. $30B priority list, 86 sites. <strong>Riprap is the per-NTA evidence layer.</strong></div>
 
 
103
  </div>
104
 
105
+ <div class="box">
106
+ <div class="lbl">EJNYC Flood Vulnerability Index</div>
107
+ <div class="body">2024. 35% of state climate spend goes to "disadvantaged communities." <strong>Riprap stays open-source so advocacy can audit.</strong></div>
 
 
108
  </div>
109
 
110
+ <div class="box dark">
111
+ <div class="lbl">No commercial APIs</div>
112
+ <div class="body">Every dataset is public-record federal, state, or city. Every foundation model is Apache-2.0. <strong>Every claim cites its source.</strong></div>
 
 
113
  </div>
114
 
 
 
 
 
 
115
  </div>
116
 
117
  ---
118
 
119
+ <div class="eyebrow">04 &middot; Architecture</div>
120
 
121
+ # Five Stones fan out. One cited briefing comes back.
122
 
123
+ <p style="margin: 4px 0 10px; font-size: 17px; color: var(--ink-3); font-family: var(--font-mono);">query &rarr; <strong style="color: var(--ink);">Planner</strong> (Granite 4.1 3B, intent classification) &rarr; Stone roster &rarr; <strong style="color: var(--ink);">Capstone</strong> (Granite 4.1 8B + Mellea) &rarr; briefing</p>
 
 
 
 
 
 
 
 
 
 
 
124
 
125
+ <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 0;">
126
+
127
+ <div style="background: var(--paper-deep); border: 1px solid var(--rule-soft); border-top: 3px solid #475569; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px;">
128
+ <div style="display: flex; justify-content: space-between; align-items: baseline;">
129
+ <span style="font-family: var(--font-mono); font-size: 9px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: #475569;">Cornerstone Β· USGS 3DEP</span>
130
+ <span style="font-family: var(--font-mono); font-size: 9px; color: var(--ink-3);">2020</span>
131
+ </div>
132
+ <div style="font-size: 13px; font-weight: 600; color: var(--ink); line-height: 1.2; margin-bottom: 4px;">Microtopography (HAND / TWI)</div>
133
+ <div style="display: grid; grid-template-columns: auto 1fr; gap: 2px 8px;">
134
+ <span style="font-family: var(--font-mono); font-size: 9px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.08em;">HAND</span><span style="font-family: var(--font-mono); font-size: 13px; font-weight: 700; color: #475569;">0.82 m</span>
135
+ <span style="font-family: var(--font-mono); font-size: 9px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.08em;">TWI</span><span style="font-family: var(--font-mono); font-size: 13px; font-weight: 700; color: #475569;">14.3</span>
136
+ <span style="font-family: var(--font-mono); font-size: 9px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.08em;">Elev.</span><span style="font-family: var(--font-mono); font-size: 13px; font-weight: 700; color: #475569;">2.1 m MSL</span>
137
+ <span style="font-family: var(--font-mono); font-size: 9px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.08em;">Pct. lower</span><span style="font-family: var(--font-mono); font-size: 13px; font-weight: 700; color: #475569;">78%</span>
138
+ </div>
139
+ <div style="margin-top: 8px; padding-top: 6px; border-top: 1px solid var(--rule-soft); font-family: var(--font-mono); font-size: 10px; color: #475569; font-weight: 600;">[topo]</div>
140
  </div>
141
+
142
+ <div style="background: var(--paper-deep); border: 1px solid var(--rule-soft); border-top: 3px solid #1A4480; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px;">
143
+ <div style="display: flex; justify-content: space-between; align-items: baseline;">
144
+ <span style="font-family: var(--font-mono); font-size: 9px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: #1A4480;">Keystone Β· TerraMind-NYC</span>
145
+ <span style="font-family: var(--font-mono); font-size: 9px; color: var(--ink-3);">2024</span>
146
+ </div>
147
+ <div style="font-size: 13px; font-weight: 600; color: var(--ink); line-height: 1.2; margin-bottom: 4px;">Building footprint coverage</div>
148
+ <div style="margin: 6px 0;">
149
+ <div style="font-family: var(--font-mono); font-size: 30px; font-weight: 700; color: #1A4480; line-height: 1;">48.41<span style="font-size: 16px;">%</span></div>
150
+ <div style="font-family: var(--font-mono); font-size: 10px; color: var(--ink-3); margin-top: 3px;">250 m radius &middot; Buildings LoRA adapter</div>
151
+ </div>
152
+ <div style="margin-top: 8px; padding-top: 6px; border-top: 1px solid var(--rule-soft); font-family: var(--font-mono); font-size: 10px; color: #1A4480; font-weight: 600;">[keystone_bldg]</div>
153
  </div>
154
+
155
+ <div style="background: var(--paper-deep); border: 1px solid var(--rule-soft); border-top: 3px solid #0E7490; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px;">
156
+ <div style="display: flex; justify-content: space-between; align-items: baseline;">
157
+ <span style="font-family: var(--font-mono); font-size: 9px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: #0E7490;">Touchstone Β· NYC 311</span>
158
+ <span style="font-family: var(--font-mono); font-size: 9px; color: var(--ink-3);">live</span>
159
+ </div>
160
+ <div style="font-size: 13px; font-weight: 600; color: var(--ink); line-height: 1.2; margin-bottom: 4px;">Flood complaints Β· 200 m buffer</div>
161
+ <div style="margin: 4px 0;">
162
+ <svg viewBox="0 0 220 60" style="width:100%; display:block;">
163
+ <rect x="8" y="52" width="212" height="1" fill="#CBD5E1"/>
164
+ <rect x="12" y="35" width="28" height="17" fill="#0E7490" rx="1"/>
165
+ <rect x="54" y="18" width="28" height="34" fill="#0E7490" rx="1"/>
166
+ <rect x="96" y="10" width="28" height="42" fill="#0E7490" rx="1"/>
167
+ <rect x="138" y="10" width="28" height="42" fill="#0E7490" rx="1"/>
168
+ <rect x="180" y="27" width="28" height="25" fill="#0E7490" rx="1"/>
169
+ <text x="26" y="32" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="8" fill="#0E7490">2</text>
170
+ <text x="68" y="15" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="8" fill="#0E7490">4</text>
171
+ <text x="110" y="7" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="8" fill="#0E7490">5</text>
172
+ <text x="152" y="7" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="8" fill="#0E7490">5</text>
173
+ <text x="194" y="24" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="8" fill="#0E7490">3</text>
174
+ <text x="26" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#94A3B8">'19</text>
175
+ <text x="68" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#94A3B8">'20</text>
176
+ <text x="110" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#94A3B8">'21</text>
177
+ <text x="152" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#94A3B8">'22</text>
178
+ <text x="194" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#94A3B8">'23</text>
179
+ </svg>
180
+ <div style="font-family: var(--font-mono); font-size: 10px; color: var(--ink-3);">19 requests &middot; 5-yr lookback</div>
181
+ </div>
182
+ <div style="margin-top: 8px; padding-top: 6px; border-top: 1px solid var(--rule-soft); font-family: var(--font-mono); font-size: 10px; color: #0E7490; font-weight: 600;">[nyc311]</div>
183
+ </div>
184
+
185
+ <div style="background: var(--paper-deep); border: 1px solid var(--rule-soft); border-top: 3px solid #92400E; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px;">
186
+ <div style="display: flex; justify-content: space-between; align-items: baseline;">
187
+ <span style="font-family: var(--font-mono); font-size: 9px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: #92400E;">Lodestone Β· Granite TTM r2</span>
188
+ <span style="font-family: var(--font-mono); font-size: 9px; color: var(--ink-3);">live</span>
189
+ </div>
190
+ <div style="font-size: 13px; font-weight: 600; color: var(--ink); line-height: 1.2; margin-bottom: 4px;">Surge residual nowcast</div>
191
+ <div style="margin: 4px 0;">
192
+ <svg viewBox="0 0 220 60" style="width:100%; display:block;">
193
+ <path d="M10,40 35,30 60,19 85,16 110,21 135,27 160,34 185,40 210,45 L210,52 L10,52 Z" fill="#92400E" opacity="0.12"/>
194
+ <rect x="8" y="52" width="212" height="1" fill="#CBD5E1"/>
195
+ <line x1="60" y1="19" x2="60" y2="52" stroke="#92400E" stroke-width="1" stroke-dasharray="3,2" opacity="0.6"/>
196
+ <polyline points="10,40 35,30 60,19 85,16 110,21 135,27 160,34 185,40 210,45" fill="none" stroke="#92400E" stroke-width="2" stroke-linejoin="round"/>
197
+ <circle cx="60" cy="19" r="3" fill="#92400E"/>
198
+ <text x="65" y="17" font-family="IBM Plex Mono,monospace" font-size="8" fill="#92400E" font-weight="700">0.22 ft</text>
199
+ <text x="10" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#94A3B8">0h</text>
200
+ <text x="60" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#92400E">NOW</text>
201
+ <text x="110" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#94A3B8">4.8h</text>
202
+ <text x="210" y="59" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="7" fill="#94A3B8">9.6h</text>
203
+ </svg>
204
+ <div style="font-family: var(--font-mono); font-size: 10px; color: var(--ink-3);">peak surge residual &middot; 9.6 h horizon</div>
205
+ </div>
206
+ <div style="margin-top: 8px; padding-top: 6px; border-top: 1px solid var(--rule-soft); font-family: var(--font-mono); font-size: 10px; color: #92400E; font-weight: 600;">[ttm_surge]</div>
207
  </div>
208
+
209
+ </div>
210
+
211
+ <p style="margin-top: 8px; font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3);">Real evidence cards rendered by the live system &nbsp;&middot;&nbsp; 442 East Houston Street, Manhattan.</p>
212
+
213
+ <div class="box" style="border-top: 3px solid #162E51; margin-top: 10px; padding: 12px 18px;">
214
+ <span style="font-family: var(--font-mono); font-size: 10px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: #162E51;">Capstone</span>
215
+ <span style="font-size: 16px; color: var(--ink-2); margin-left: 14px;">Granite 4.1 8B + Mellea rejection sampling &nbsp;&middot;&nbsp; <code>numerics_grounded</code> &middot; <code>no_placeholder_tokens</code> &middot; <code>citations_dense</code> &middot; <code>citations_resolve</code> &nbsp;&middot;&nbsp; reroll until resolved &nbsp;&rarr;&nbsp; <strong>cited 4-section briefing</strong></span>
216
  </div>
217
 
218
  ---
219
 
220
+ <div class="eyebrow">05 &middot; Fine-Tuning on AMD MI300X</div>
221
 
222
+ # Three Apache-2.0 NYC fine-tunes on MI300X.
223
 
224
+ <div class="box-grid cols-3" style="margin-top: 12px; gap: 14px;">
225
 
226
+ <div class="box" style="border-top: 3px solid #0E7490; padding: 18px 18px 16px;">
227
+ <div class="lbl" style="color: #0E7490; margin-bottom: 6px;">Prithvi-EO-2.0-NYC-Pluvial</div>
228
+ <div style="font-size: 14px; color: var(--ink-2); margin-bottom: 12px;">Hurricane Ida pluvial flood detection from Sentinel-2</div>
229
+ <div style="font-family: var(--font-mono); font-size: 22px; font-weight: 700; color: var(--ink); letter-spacing: -0.02em;">0.5979</div>
230
+ <div style="font-family: var(--font-mono); font-size: 11px; color: var(--ink-3); margin-bottom: 10px;">test flood IoU &nbsp;Β·&nbsp; 6&times; lift over Sen1Floods11 baseline</div>
231
+ <div style="font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3);">MI300X &middot; AMD Developer Cloud</div>
232
  </div>
233
 
234
+ <div class="box" style="border-top: 3px solid #1A4480; padding: 18px 18px 16px;">
235
+ <div class="lbl" style="color: #1A4480; margin-bottom: 6px;">TerraMind-NYC-Adapters</div>
236
+ <div style="font-size: 14px; color: var(--ink-2); margin-bottom: 12px;">LULC + Buildings + TiM LoRA adapters for NYC</div>
237
+ <div style="font-family: var(--font-mono); font-size: 22px; font-weight: 700; color: var(--ink); letter-spacing: -0.02em;">+6.13<span style="font-size: 15px;">pp</span></div>
238
+ <div style="font-family: var(--font-mono); font-size: 11px; color: var(--ink-3); margin-bottom: 10px;">LULC mIoU over full-FT baseline &nbsp;Β·&nbsp; mIoU 0.5866</div>
239
+ <div style="font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3);">~18 min total &middot; MI300X</div>
240
  </div>
241
 
242
+ <div class="box" style="border-top: 3px solid #92400E; padding: 18px 18px 16px;">
243
+ <div class="lbl" style="color: #92400E; margin-bottom: 6px;">Granite-TTM-r2-Battery-Surge</div>
244
+ <div style="font-size: 14px; color: var(--ink-2); margin-bottom: 12px;">NOAA Battery tide gauge 96h surge residual nowcast</div>
245
+ <div style="font-family: var(--font-mono); font-size: 22px; font-weight: 700; color: var(--ink); letter-spacing: -0.02em;">0.157<span style="font-size: 15px;">m</span></div>
246
+ <div style="font-family: var(--font-mono); font-size: 11px; color: var(--ink-3); margin-bottom: 10px;">RMSE &nbsp;Β·&nbsp; &minus;35% vs persistence baseline</div>
247
+ <div style="font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3);">MI300X &middot; Apache-2.0 &middot; HF Hub</div>
248
  </div>
249
 
 
 
 
250
  </div>
251
 
252
+ <p style="margin-top: 18px; font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-3);">Track submitted: Fine-Tuning on AMD GPUs &nbsp;&middot;&nbsp; All three models Apache-2.0, published on HF Hub</p>
253
 
254
  ---
255
 
256
+ <div class="eyebrow">06 &middot; DEMO</div>
257
 
258
  # Live demo.
259
 
260
+ <div style="margin: 48px 0 32px; text-align: center;">
261
+ <p style="font-family: var(--font-mono); font-size: 28px; font-weight: 700; color: var(--ink); margin: 0 auto; max-width: 860px; line-height: 1.35;">&ldquo;I&rsquo;m thinking about renting an apartment at 80 Pioneer Street, Brooklyn. Should I worry?&rdquo;</p>
262
+ </div>
263
+
264
+ <div style="text-align: center; margin-bottom: 40px;">
265
+ <span style="font-family: var(--font-mono); font-size: 20px; letter-spacing: 0.06em; color: var(--accent); font-weight: 700;">riprap.nyc</span>
266
+ </div>
267
+
268
+ <p style="text-align: center; font-family: var(--font-mono); font-size: 13px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3); margin: 0 auto; max-width: none;">13 seconds end-to-end &nbsp;&middot;&nbsp; 4/4 grounding checks &nbsp;&middot;&nbsp; all sources public-record</p>
269
+
270
+ ---
271
+
272
+ <div class="eyebrow">07 &middot; What's next</div>
273
+
274
+ # What's next.
275
+
276
+ <p style="margin-bottom: 14px; font-size: 18px; color: var(--ink-3); font-family: var(--font-mono); letter-spacing: 0.02em;">The architecture is NYC-specific by data choice, not by code.</p>
277
+
278
+ <div class="box-grid cols-3" style="margin-top: 0;">
279
+
280
+ <div class="box">
281
+ <div class="lbl">Break out the Stones</div>
282
+ <div class="body">Each Stone is a coherent composition over data sources, models, and deterministic checks. Extract Cornerstone, Touchstone, Keystone, Lodestone as independent packages; any civic-tech project can pull one Stone without the full Riprap stack.</div>
283
+ </div>
284
+
285
+ <div class="box">
286
+ <div class="lbl">Other flood-impacted cities</div>
287
+ <div class="body">Houston (Harvey, Beryl), Miami (king tides), Boston (CSO floods), Jakarta, Manila, Dhaka &mdash; the same five-Stone pattern, different probe sets and RAG corpora per city.</div>
288
  </div>
289
 
290
+ <div class="box tinted">
291
+ <div class="lbl">Historical-event mode</div>
292
+ <div class="body">Re-run the FSM with snapshot data from any past date. Validate the system against measured outcomes &mdash; what would Riprap have said before Sandy, before Ida, before the 2024 Beryl remnants. Calibration as a first-class feature.</div>
293
  </div>
294
 
295
+ </div>
296
 
297
  ---
298
 
 
302
 
303
  <div class="eyebrow" style="margin-top: 124px; color: var(--accent); border: 0; padding: 0;">Riprap &middot; flood briefings on AMD</div>
304
 
305
+ <h1 style="white-space: nowrap; font-size: 72px;">github.com/msradam/riprap-nyc</h1>
 
 
306
 
307
  <hr>
308
 
 
317
  <p style="font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.08em; color: rgba(244,246,249,0.4); margin-top: 24px; text-transform: none;">
318
  Dam mark: "Dam" by Chintuza via the Noun Project, CC-BY 3.0.
319
  </p>
320
+
321
+ ---
322
+
323
+ <!-- _paginate: false -->
324
+
325
+ <div class="eyebrow">Appendix &middot; The receipts</div>
326
+
327
+ # 5 of 5 NYC addresses. Every claim verified, every run.
328
+
329
+ <table>
330
+ <thead>
331
+ <tr><th>address</th><th>intent</th><th>wall</th><th>steps</th><th>verified</th></tr>
332
+ </thead>
333
+ <tbody>
334
+ <tr><td>442 E Houston St &middot; LES</td><td>address</td><td>7.6 s</td><td>19</td><td>4/4</td></tr>
335
+ <tr><td>80 Pioneer St &middot; Red Hook</td><td>address</td><td>13.1 s</td><td>19</td><td>4/4</td></tr>
336
+ <tr><td>100 Gold St &middot; Manhattan</td><td>address</td><td>11.2 s</td><td>19</td><td>4/4</td></tr>
337
+ <tr><td>Hollis &middot; Queens</td><td>nbhd</td><td>5.8 s</td><td>9</td><td>4/4</td></tr>
338
+ <tr><td>Coney Island &middot; Brooklyn</td><td>nbhd</td><td>9.9 s</td><td>9</td><td>4/4</td></tr>
339
+ </tbody>
340
+ </table>
341
+
342
+ <div class="box-grid cols-3" style="margin-top: 16px;">
343
+ <div class="box">
344
+ <div class="lbl">Wall-clock</div>
345
+ <div class="stat-value">5.8&ndash;13.1<span style="font-size: 22px; color: var(--ink-3); font-weight: 400; letter-spacing: 0;"> s</span></div>
346
+ <div class="stat-label">vLLM on MI300X</div>
347
+ </div>
348
+ <div class="box">
349
+ <div class="lbl">Stones</div>
350
+ <div class="stat-value">5</div>
351
+ <div class="stat-label">evidence layers per briefing</div>
352
+ </div>
353
+ <div class="box">
354
+ <div class="lbl">Verified</div>
355
+ <div class="stat-value">4 / 4</div>
356
+ <div class="stat-label">source checks every run</div>
357
+ </div>
358
+ </div>
submission/COPY-DRAFTS.md ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Submission copy drafts β€” AMD x lablab.ai hackathon
2
+
3
+ Prepared 2026-05-07. Voice: civic-tech-clean, precise, understated.
4
+ Numbers from RESEARCH.md and probe_addresses.py. No invented statistics.
5
+
6
+ ---
7
+
8
+ ## Project title (max 50 characters)
9
+
10
+ Three options, each fits in 50 chars:
11
+
12
+ **Option A (recommended):** `Riprap β€” Cited NYC flood briefings on AMD`
13
+ (42 chars) β€” Names the project, names the output type, names the
14
+ platform. No adjectives. The word "cited" is load-bearing: it's the
15
+ differentiator in one word.
16
+
17
+ **Option B:** `Riprap: citation-grounded flood briefings`
18
+ (41 chars) β€” "Citation-grounded" is the project's defining
19
+ term; it front-loads the architectural commitment. Slightly more
20
+ technical than A.
21
+
22
+ **Option C:** `Riprap β€” NYC flood risk, every claim cited`
23
+ (41 chars) β€” Plain English, consumer-accessible. Weaker for a
24
+ technical hackathon audience.
25
+
26
+ ---
27
+
28
+ ## Short description (max 255 characters)
29
+
30
+ Three options:
31
+
32
+ **Option A (recommended):**
33
+ Riprap writes NYC flood-exposure briefings where every numeric claim cites its source β€” or doesn't appear. Granite 4.1 8B on AMD MI300X, three Apache-2.0 NYC fine-tunes, Mellea citation grounding. 5/5 addresses, 4/4 checks every run.
34
+ (237 chars)
35
+
36
+ Rationale: leads with the output, names the citation discipline in
37
+ plain English, names the GPU platform and the three fine-tune
38
+ artifacts, closes with the receipts. No adjectives. Ends on a
39
+ verifiable number.
40
+
41
+ **Option B:**
42
+ Three AMD MI300X fine-tuned models. Five evidence layers. One cited briefing. Riprap takes any NYC address, fans out across Sandy 2012 data, live FloodNet sensors, 311 history, and surge forecasts, then returns a 4-section paragraph with doc_id citations on every number.
43
+ (255 chars β€” exact limit)
44
+
45
+ Rationale: leads with the Fine-Tuning track evidence (the AMD
46
+ hardware claim), then explains the system. Denser; may be harder
47
+ to parse at a skim.
48
+
49
+ **Option C:**
50
+ Type a NYC address. Riprap runs five data probes β€” Sandy 2012 inundation, live sensors, 311 history, DEP scenarios, surge forecasts β€” and writes a cited flood briefing. IBM Granite 4.1 on AMD MI300X. Apache-2.0. Public data only.
51
+ (229 chars)
52
+
53
+ Rationale: most conversational, demo-first. Weakest on the
54
+ hackathon-track argument.
55
+
56
+ ---
57
+
58
+ ## Long description (~250–300 words)
59
+
60
+ Riprap is a citation-grounded flood-exposure briefing tool for NYC
61
+ addresses, built on IBM Granite 4.1 8B running on AMD MI300X via vLLM.
62
+ Type any address or neighborhood. Within 6–13 seconds, five evidence
63
+ layers fan out across NYC's public flood record and return a four-
64
+ section prose briefing β€” every numeric claim followed by a [doc_id]
65
+ citation that resolves to a named primary source.
66
+
67
+ The system refuses to publish a number it cannot cite. Mellea rejection
68
+ sampling enforces four invariants on every response: numeric claims
69
+ grounded in source documents, no placeholder tokens, citation density
70
+ per sentence, and all cited doc_ids resolve to inputs. If the briefing
71
+ fails any check, the model rerolls. The meta card shows which checks
72
+ passed and how many attempts it took.
73
+
74
+ Three Apache-2.0 NYC-specialized fine-tunes were trained on AMD MI300X
75
+ and published on HF Hub: `msradam/Prithvi-EO-2.0-NYC-Pluvial` (pluvial
76
+ flood segmentation from Sentinel-2), `msradam/TerraMind-NYC-Adapters`
77
+ (LULC and building-stock adapters), and `msradam/Granite-TTM-r2-Battery-
78
+ Surge` (96-hour surge residual nowcast, test MAE 0.1091 m vs 0.1467 m
79
+ zero-shot). All training code and reproduction recipes are in the repo.
80
+
81
+ The data pipeline is entirely public-record: Hurricane Sandy 2012
82
+ inundation zone, NYC DEP stormwater scenarios, USGS Ida 2021 high-water
83
+ marks, FloodNet ultrasonic sensors, NYC 311 complaint history, NOAA
84
+ tide gauge, NWS METAR, NPCC4 SLR projections, five NYC policy PDFs in
85
+ a Granite Embedding 278M RAG corpus. No commercial APIs are contacted
86
+ at runtime.
87
+
88
+ Five addresses across four NYC boroughs, verified with the address probe
89
+ suite: 5 of 5 pass, 4/4 Mellea grounding checks, 5.8–13.1 seconds
90
+ wall-clock on AMD MI300X.
91
+
92
+ The stack runs on both AMD MI300X (vLLM) and local Ollama with
93
+ auto-failover. Apache-2.0 end-to-end.
94
+
95
+ ---
96
+
97
+ ## Cover image β€” design brief
98
+
99
+ **Target artifact:** `submission/cover-16x9.png`
100
+ **Dimensions:** 1920Γ—1080 px or 1280Γ—720 px, 16:9 ratio, PNG
101
+
102
+ **Visual system (must match deck cover exactly):**
103
+ - Background: `#F4F6F9` (--paper, cool slate register)
104
+ - Dam mark (Noun Project "Dam" by Chintuza, CC-BY 3.0): positioned
105
+ top-left at approximately 72px from edges, colored `#005EA2`
106
+ (federal blue, --accent)
107
+ - Wordmark: "Riprap" in IBM Plex Sans 700, `#0F172A` (--ink), large
108
+ display size (~96–120px equivalent at 1920-wide)
109
+ - Tagline line 1: "Citation-grounded NYC flood-exposure briefings"
110
+ IBM Plex Sans 400, `#334155` (--ink-2)
111
+ - Tagline line 2: "AMD MI300X &middot; Granite 4.1 &middot; Apache-2.0"
112
+ IBM Plex Mono 500, `#64748B` (--ink-3), smaller (14–16px equivalent)
113
+ - Bottom strip (meta bar): thin rule at `#CBD5E1` (--rule-soft),
114
+ below the rule: "AMD Γ— lablab.ai Developer Hackathon Β· May 4–10 2026"
115
+ in IBM Plex Mono, `#64748B`, 12px equivalent, uppercase
116
+
117
+ **What to avoid:**
118
+ - No bold color blocks in the background (the thumbnail reads fine
119
+ on paper register)
120
+ - No gradient, shadow, or decorative water imagery β€” the dam mark
121
+ carries the water metaphor
122
+ - No subtitle text beyond what's listed above
123
+
124
+ **Status:** The cover image requires an SVG/HTML renderer or design
125
+ tool. Automated generation is not feasible in this environment without
126
+ a headless browser for SVG-to-PNG export. Adam should generate this
127
+ from the brief above using:
128
+ - Figma (design file already has the tokens)
129
+ - The deck's cover slide exported at 1920Γ—1080 via Marp
130
+ (`--allow-local-files --image png --output cover-16x9.png`)
131
+ - Or: `slides/logo.svg` + a simple HTML page exported via
132
+ headless Chrome / Puppeteer
133
+
134
+ **Quickest path:** re-export the cover slide from deck.pdf as a PNG at
135
+ 1920Γ—1080. The cover slide already has the correct design.
136
+
137
+ ---
138
+
139
+ ## Recommended submission combination
140
+
141
+ **Title (go with A):** `Riprap β€” Cited NYC flood briefings on AMD`
142
+
143
+ **Short description (go with A):**
144
+ Riprap writes NYC flood-exposure briefings where every numeric claim cites its source β€” or doesn't appear. Granite 4.1 8B on AMD MI300X, three Apache-2.0 NYC fine-tunes, Mellea citation grounding. 5/5 addresses, 4/4 checks every run.
145
+
146
+ **Long description:** the version above (no changes needed).
147
+
148
+ **Runner-up title:** `Riprap: citation-grounded flood briefings`
149
+
150
+ **Runner-up short (option B):**
151
+ Three AMD MI300X fine-tuned models. Five evidence layers. One cited briefing. Riprap takes any NYC address, fans out across Sandy 2012 data, live FloodNet sensors, 311 history, and surge forecasts, then returns a 4-section paragraph with doc_id citations on every number.