Spaces:
Configuration error
Configuration error
| /** | |
| * FSM-state → Findings Card[] adapter. | |
| * | |
| * The Findings region is rendered from a `FindingsData` shape: | |
| * `{ cards, stones, wallSeconds }`. The FSM produces a different | |
| * structure: a stream of step events plus a final payload with each | |
| * specialist's raw output keyed by state name (`sandy`, `dep`, | |
| * `nyc311`, `mta_entrances`, etc.). | |
| * | |
| * This module bridges the two. For each Stone we collect the relevant | |
| * state keys, render a card per non-silent specialist using the most | |
| * legible variant for the data shape, and build a per-Stone trace from | |
| * the TraceNode tree. | |
| * | |
| * Best-effort: a missing specialist drops out (silence over | |
| * confabulation); a specialist that fired with no usable shape becomes | |
| * a `meta` card listing whatever scalars it returned. | |
| */ | |
| import type { | |
| Card, FindingsData, StoneKey, StoneMember, StoneTrace | |
| } from '$lib/types/card'; | |
| import type { TraceNode, TraceStatus } from '$lib/types/trace'; | |
| import type { FinalResult } from '$lib/client/agentStream'; | |
| import { fillRosterForStone } from '$lib/data/stoneRegistry'; | |
| /** Reasonable defaults — when the FSM doesn't supply a vintage, fall | |
| * back to the Riprap publication date. */ | |
| const RIPRAP_VINTAGE = '2026-05'; | |
| /** Map the FSM trace's TraceStatus into the v0.4.5 5-state SpecialistStatus. | |
| * Crucial split: a specialist that "returned no data" is `silent_by_design`, | |
| * not `errored`. The FSM marks both as `silent` in the trace; we | |
| * conservatively classify any successful trace-`silent` as | |
| * silent_by_design (the spec voice). Anything that raised in the FSM | |
| * becomes `errored`. `fan`/`merge` are structural; the few callers | |
| * that look at them treat them as fired. | |
| */ | |
| function mapStatus(s: TraceStatus): StoneMember['status'] { | |
| if (s === 'fan' || s === 'merge') return 'fired'; | |
| if (s === 'silent') return 'silent_by_design'; | |
| if (s === 'error') return 'errored'; | |
| return 'fired'; | |
| } | |
| function flattenTrace(node: TraceNode): TraceNode[] { | |
| return [node, ...(node.children ?? []).flatMap(flattenTrace)]; | |
| } | |
| /** Group leaf specialist nodes by the Stone their state-key belongs to. | |
| * The trace's name often differs from the state key (e.g. | |
| * `mta_entrance_exposure` vs state `mta_entrances`); we map both. */ | |
| function stoneForStep(name: string): StoneKey | null { | |
| const n = name.toLowerCase(); | |
| // Single-address chain | |
| if (n === 'sandy_inundation' || n === 'sandy') return 'cornerstone'; | |
| if (n === 'dep_stormwater' || n === 'dep') return 'cornerstone'; | |
| if (n === 'ida_hwm_2021' || n === 'ida_hwm') return 'cornerstone'; | |
| if (n === 'prithvi_eo_v2' || n === 'prithvi_water') return 'cornerstone'; | |
| if (n === 'microtopo_lidar' || n === 'microtopo') return 'cornerstone'; | |
| if (n === 'mta_entrance_exposure' || n === 'mta_entrances') return 'keystone'; | |
| if (n === 'nycha_development_exposure' || n === 'nycha_developments') return 'keystone'; | |
| if (n === 'doe_school_exposure' || n === 'doe_schools') return 'keystone'; | |
| if (n === 'doh_hospital_exposure' || n === 'doh_hospitals') return 'keystone'; | |
| if (n === 'terramind_synthesis' || n === 'terramind' || n === 'terramind_buildings' || | |
| n === 'eo_chip_fetch') return 'keystone'; | |
| if (n === 'floodnet') return 'touchstone'; | |
| if (n === 'nyc311') return 'touchstone'; | |
| if (n === 'nws_obs') return 'touchstone'; | |
| if (n === 'noaa_tides') return 'touchstone'; | |
| if (n === 'prithvi_eo_live' || n === 'prithvi_live' || n === 'terramind_lulc') | |
| return 'touchstone'; | |
| if (n === 'nws_alerts') return 'lodestone'; | |
| if (n === 'ttm_forecast' || n === 'ttm_311_forecast' || n === 'floodnet_forecast' || | |
| n === 'ttm_battery_surge') return 'lodestone'; | |
| if (n.startsWith('reconcile') || n.startsWith('mellea') || | |
| n === 'rag_granite_embedding' || n === 'gliner_extract') return 'capstone'; | |
| return null; | |
| } | |
| function buildStoneTraces(root: TraceNode | undefined | null): StoneTrace[] { | |
| const buckets: Record<StoneKey, StoneMember[]> = { | |
| cornerstone: [], keystone: [], touchstone: [], lodestone: [], capstone: [], | |
| }; | |
| if (root) { | |
| for (const node of flattenTrace(root)) { | |
| const stone = stoneForStep(node.name); | |
| if (!stone) continue; | |
| buckets[stone].push({ | |
| id: node.id || node.name, | |
| // Preserve the raw FSM step name here — the registry projection | |
| // matches against `stepNames`, not display names. | |
| name: node.name, | |
| status: mapStatus(node.status), | |
| tier: node.tier, | |
| ms: node.ms, | |
| note: node.note ?? node.error ?? undefined, | |
| }); | |
| } | |
| } | |
| // v0.4.5 §3 — project the registry over each Stone so the provenance | |
| // expander shows the full inventory, with absent specialists as | |
| // not_invoked. | |
| return (Object.keys(buckets) as StoneKey[]).map((key) => ({ | |
| key, | |
| members: fillRosterForStone(key, buckets[key]), | |
| })); | |
| } | |
| /* ── Per-specialist card builders. Each returns null if the specialist | |
| didn't fire, returned no usable data, or the shape doesn't exist. ── */ | |
| type Final = Record<string, unknown> & FinalResult; | |
| function num(v: unknown): number | null { | |
| return typeof v === 'number' && Number.isFinite(v) ? v : null; | |
| } | |
| function str(v: unknown): string | null { | |
| return typeof v === 'string' ? v : null; | |
| } | |
| function obj(v: unknown): Record<string, unknown> | null { | |
| return v && typeof v === 'object' && !Array.isArray(v) | |
| ? (v as Record<string, unknown>) : null; | |
| } | |
| function buildSandy(state: Final, geocode: Record<string, unknown> | null): Card | null { | |
| if (state.sandy !== true) return null; | |
| const addr = geocode && str(geocode.address); | |
| return { | |
| id: 'fsm-sandy', | |
| stone: 'cornerstone', tier: 'empirical', variant: 'headline', | |
| source: 'NYC OEM', agency: 'NYC OpenData 5xsi-dfpx · Sandy 2012 inundation', | |
| vintage: '2012-10-29', | |
| title: 'Hurricane Sandy 2012 inundation', | |
| headline: 'Inside zone', | |
| subhead: addr ?? 'address inside the empirical 2012 extent', | |
| body: 'Address sits within the empirical Hurricane Sandy 2012 inundation extent. This is a historical fact, not a model prediction.', | |
| docId: 'sandy', citeId: 'sandy', mapLayer: 'sandy', | |
| }; | |
| } | |
| function buildDep(state: Final): Card | null { | |
| const dep = obj(state.dep); | |
| if (!dep) return null; | |
| const rows: (string | number)[][] = []; | |
| for (const [scen, info] of Object.entries(dep)) { | |
| const i = obj(info); | |
| if (!i) continue; | |
| const cls = num(i.depth_class) ?? 0; | |
| if (cls <= 0) continue; | |
| rows.push([scen.replace('dep_', ''), str(i.depth_label) ?? '—', `class ${cls}`]); | |
| } | |
| if (!rows.length) return null; | |
| return { | |
| id: 'fsm-dep', | |
| stone: 'cornerstone', tier: 'modeled', variant: 'tabular', | |
| source: 'NYC DEP', agency: 'NYC Department of Environmental Protection · Stormwater Flood Maps', | |
| vintage: '2021', | |
| title: 'Stormwater flood scenarios at this address', | |
| columns: ['scenario', 'depth label', 'class'], | |
| rows, | |
| sub: `${rows.length} scenario${rows.length === 1 ? '' : 's'} place this lot in modeled flooding`, | |
| docId: 'dep_stormwater', citeId: 'dep', mapLayer: 'stormwater', | |
| }; | |
| } | |
| function buildIdaHwm(state: Final): Card | null { | |
| const ida = obj(state.ida_hwm); | |
| if (!ida) return null; | |
| const n = num(ida.n_within_radius); | |
| if (!n || n <= 0) return null; | |
| const rows: (string | number)[][] = []; | |
| rows.push(['count', `${n}`, `${num(ida.radius_m) ?? 800} m radius`]); | |
| if (num(ida.max_height_above_gnd_ft) != null) { | |
| rows.push(['max above gnd', `${ida.max_height_above_gnd_ft} ft`, '—']); | |
| } | |
| if (num(ida.nearest_dist_m) != null) { | |
| rows.push(['nearest', str(ida.nearest_site) ?? 'HWM', `${ida.nearest_dist_m} m`]); | |
| } | |
| return { | |
| id: 'fsm-ida-hwm', | |
| stone: 'cornerstone', tier: 'empirical', variant: 'tabular', | |
| source: 'USGS', agency: 'USGS STN Hurricane Ida 2021 high-water marks (Event 312)', | |
| vintage: '2021-09', | |
| title: 'Hurricane Ida 2021 high-water marks nearby', | |
| columns: ['field', 'value', 'context'], | |
| rows, | |
| docId: 'ida_hwm', citeId: 'ida_hwm', mapLayer: 'hwm', | |
| }; | |
| } | |
| function buildPrithviWater(state: Final): Card | null { | |
| const pw = obj(state.prithvi_water); | |
| if (!pw) return null; | |
| const dist = num(pw.nearest_distance_m); | |
| if (dist == null) return null; | |
| return { | |
| id: 'fsm-prithvi-water', | |
| stone: 'cornerstone', tier: 'modeled', variant: 'raster', | |
| source: 'Prithvi-EO 2.0', agency: 'IBM/NASA Prithvi-EO 2.0 · baked Hurricane Ida 2021 polygons', | |
| vintage: '2021-09-02', | |
| title: 'Hurricane Ida 2021 — satellite-attributable inundation', | |
| rasterKind: 'prithvi', | |
| headline: pw.inside_water_polygon ? 'Inside polygon' : `${dist} m away`, | |
| subhead: 'pre/post HLS Sentinel-2 segmentation', | |
| sub: `${num(pw.n_polygons_within_500m) ?? 0} distinct polygons within 500 m`, | |
| docId: 'prithvi_water', citeId: 'prithvi_water', mapLayer: 'prithvi', | |
| }; | |
| } | |
| function buildMicrotopo(state: Final): Card | null { | |
| const mt = obj(state.microtopo); | |
| if (!mt) return null; | |
| const elev = num(mt.point_elev_m); | |
| if (elev == null) return null; | |
| const scalars = [ | |
| { value: `${elev.toFixed(1)} m`, label: 'elevation' }, | |
| ]; | |
| if (num(mt.hand_m) != null) scalars.push({ value: `${(mt.hand_m as number).toFixed(1)} m`, label: 'HAND' }); | |
| if (num(mt.twi) != null) scalars.push({ value: `${(mt.twi as number).toFixed(1)}`, label: 'TWI' }); | |
| if (num(mt.rel_elev_pct_200m) != null) scalars.push({ value: `${mt.rel_elev_pct_200m}%`, label: 'pct lower 200m' }); | |
| return { | |
| id: 'fsm-microtopo', | |
| stone: 'cornerstone', tier: 'proxy', variant: 'scalars', | |
| source: 'USGS 3DEP', agency: 'USGS 3DEP DEM (LiDAR-derived) + whitebox-workflows hydrology', | |
| vintage: '2018', | |
| title: 'Microtopography at this point', | |
| scalars, | |
| sub: 'Lower percentile = topographic low point; runoff routes here.', | |
| docId: 'microtopo', citeId: 'microtopo', | |
| }; | |
| } | |
| /** Per-asset register. Each register-specialist's state has an `available` | |
| * flag + a list of items; we render one card with N rows. */ | |
| function buildRegisters(state: Final): Card | null { | |
| const rows: NonNullable<Card['registers']> = []; | |
| const mta = obj(state.mta_entrances); | |
| if (mta?.available && Array.isArray(mta.entrances)) { | |
| for (const e of (mta.entrances as Record<string, unknown>[]).slice(0, 4)) { | |
| rows.push({ | |
| reg: 'MTA', tier: 'empirical', | |
| label: str(e.station_name) ?? str(e.entrance_id) ?? 'entrance', | |
| detail: `${num(e.distance_m) ?? '—'} m · ${str(e.daytime_routes) ?? ''}`.trim(), | |
| sourceId: str(e.station_id) ?? 'MTA', | |
| note: null, | |
| }); | |
| } | |
| } else if (mta && mta.available === false) { | |
| rows.push({ | |
| reg: 'MTA', tier: 'empirical', label: null, detail: null, sourceId: null, | |
| note: 'no subway entrances within 1.0 mi (silent)', | |
| }); | |
| } | |
| const nycha = obj(state.nycha_developments); | |
| if (nycha?.available && Array.isArray(nycha.developments)) { | |
| for (const d of (nycha.developments as Record<string, unknown>[]).slice(0, 3)) { | |
| rows.push({ | |
| reg: 'NYCHA', tier: 'empirical', | |
| label: str(d.development) ?? 'development', | |
| detail: `${num(d.distance_m) ?? '—'} m · ${str(d.borough) ?? ''}`.trim(), | |
| sourceId: str(d.tds_num) ?? null, | |
| note: null, | |
| }); | |
| } | |
| } else if (nycha && nycha.available === false) { | |
| rows.push({ | |
| reg: 'NYCHA', tier: 'empirical', label: null, detail: null, sourceId: null, | |
| note: 'no NYCHA developments within 1.0 mi (silent)', | |
| }); | |
| } | |
| const doe = obj(state.doe_schools); | |
| if (doe?.available && Array.isArray(doe.schools)) { | |
| for (const s of (doe.schools as Record<string, unknown>[]).slice(0, 3)) { | |
| rows.push({ | |
| reg: 'DOE', tier: 'empirical', | |
| label: str(s.loc_name) ?? 'school', | |
| detail: `${num(s.distance_m) ?? '—'} m · ${str(s.borough) ?? ''}`.trim(), | |
| sourceId: str(s.loc_code) ?? null, | |
| note: null, | |
| }); | |
| } | |
| } else if (doe && doe.available === false) { | |
| rows.push({ | |
| reg: 'DOE', tier: 'empirical', label: null, detail: null, sourceId: null, | |
| note: 'no schools within 1.0 mi (silent)', | |
| }); | |
| } | |
| const doh = obj(state.doh_hospitals); | |
| if (doh?.available && Array.isArray(doh.hospitals)) { | |
| for (const h of (doh.hospitals as Record<string, unknown>[]).slice(0, 3)) { | |
| rows.push({ | |
| reg: 'DOH', tier: 'empirical', | |
| label: str(h.facility_name) ?? 'hospital', | |
| detail: `${num(h.distance_m) ?? '—'} m · ${str(h.borough) ?? ''}`.trim(), | |
| sourceId: str(h.fac_id) ?? null, | |
| note: null, | |
| }); | |
| } | |
| } else if (doh && doh.available === false) { | |
| rows.push({ | |
| reg: 'DOH', tier: 'empirical', label: null, detail: null, sourceId: null, | |
| note: 'no acute-care hospital within 1.0 mi (silent)', | |
| }); | |
| } | |
| if (!rows.length) return null; | |
| return { | |
| id: 'fsm-registers', | |
| stone: 'keystone', tier: 'empirical', variant: 'register', | |
| source: 'NYC OpenData', agency: 'NYC OpenData · multi-agency join', | |
| vintage: RIPRAP_VINTAGE, | |
| title: 'Nearby exposed assets', | |
| registers: rows, | |
| sub: `${rows.filter((r) => r.label).length} of ${rows.length} registers fired · joined within 1.0 mi`, | |
| docId: 'registers', citeId: 'registers', mapLayer: 'registers', | |
| }; | |
| } | |
| function buildTerramindBuildings(state: Final): Card | null { | |
| const tmb = obj(state.terramind_buildings); | |
| if (!tmb?.ok) return null; | |
| return { | |
| id: 'fsm-tm-buildings', | |
| stone: 'keystone', tier: 'modeled', variant: 'raster-pred', | |
| source: 'TerraMind-NYC', agency: 'msradam/TerraMind-NYC-Adapters · Buildings LoRA', | |
| vintage: '2026', | |
| title: 'NYC building footprints — TerraMind LoRA', | |
| rasterKind: 'buildings', | |
| headline: `${num(tmb.pct_buildings) ?? 0}%`, | |
| subhead: 'building-footprint coverage in chip', | |
| sub: `${num(tmb.n_building_components) ?? 0} distinct components · test mIoU 0.5511`, | |
| illustrative: true, | |
| docId: 'tm_buildings', citeId: 'tm_buildings', mapLayer: 'buildings', | |
| }; | |
| } | |
| function buildFloodnet(state: Final): Card | null { | |
| const fn = obj(state.floodnet); | |
| if (!fn || (num(fn.n_sensors) ?? 0) <= 0) return null; | |
| const events = num(fn.n_flood_events_3y) ?? 0; | |
| return { | |
| id: 'fsm-floodnet', | |
| stone: 'touchstone', tier: 'empirical', variant: 'spark', | |
| source: 'FloodNet', agency: 'FloodNet NYC ultrasonic depth sensor network', | |
| vintage: '2026', | |
| title: 'FloodNet sensors near this address', | |
| headline: `${events} events`, | |
| subhead: `${num(fn.n_sensors) ?? 0} sensors · last 3 y`, | |
| spark: Array.from({ length: 24 }, (_, i) => | |
| Math.max(0, Math.round((events / 24) * 1.4 * Math.exp(-Math.pow((i - 14) / 4, 2)) + | |
| (events / 24))) | |
| ), | |
| sparkSub: 'Above-curb depth events ≥ 2 cm. Synthetic monthly distribution; raw deployment-id history is in the audit panel.', | |
| docId: 'floodnet', citeId: 'floodnet', mapLayer: 'floodnet', | |
| }; | |
| } | |
| function buildNyc311(state: Final): Card | null { | |
| const n = obj(state.nyc311); | |
| if (!n) return null; | |
| const total = num(n.n) ?? 0; | |
| if (total <= 0) return null; | |
| // Synthesize a histogram that matches the seasonal pattern when by_year | |
| // / by_descriptor is present; otherwise distribute uniformly. | |
| const byYear = obj(n.by_year); | |
| const byDescriptor = obj(n.by_descriptor); | |
| const hist = byYear | |
| ? Object.values(byYear).map((v) => num(v) ?? 0) | |
| : Array.from({ length: 12 }, () => Math.round(total / 12)); | |
| const top = byDescriptor | |
| ? Object.entries(byDescriptor).sort((a, b) => (num(b[1]) ?? 0) - (num(a[1]) ?? 0))[0]?.[0] | |
| : null; | |
| return { | |
| id: 'fsm-311', | |
| stone: 'touchstone', tier: 'proxy', variant: 'histogram', | |
| source: 'NYC 311', agency: 'NYC 311 service requests (Socrata erm2-nwe9)', | |
| vintage: RIPRAP_VINTAGE, | |
| title: 'Recent 311 flood complaints', | |
| headline: `${total} calls`, | |
| subhead: top ? `top descriptor: ${top}` : 'all flood-related descriptors', | |
| histogram: hist, | |
| sparkSub: `Within ${num(n.radius_m) ?? 200} m · ${num(n.years) ?? 5} y window. Filtered to flood-relevant descriptors.`, | |
| docId: 'nyc311', citeId: 'nyc311', mapLayer: 'complaints', | |
| }; | |
| } | |
| function buildNwsObs(state: Final): Card | null { | |
| const obs = obj(state.nws_obs); | |
| if (!obs || obs.error || obs.station_id == null) return null; | |
| const scalars: NonNullable<Card['scalars']> = []; | |
| if (num(obs.precip_last_hour_mm) != null) scalars.push({ value: `${obs.precip_last_hour_mm} mm`, label: 'precip · 1h' }); | |
| if (num(obs.precip_last_6h_mm) != null) scalars.push({ value: `${obs.precip_last_6h_mm} mm`, label: 'precip · 6h' }); | |
| if (!scalars.length) return null; | |
| return { | |
| id: 'fsm-nws-obs', | |
| stone: 'touchstone', tier: 'empirical', variant: 'scalars', | |
| source: 'NWS', agency: `NWS ASOS station ${str(obs.station_id) ?? '?'}`, | |
| vintage: str(obs.obs_time)?.slice(0, 10) ?? RIPRAP_VINTAGE, | |
| title: 'Recent precipitation', | |
| scalars, | |
| sub: `Nearest hourly METAR: ${str(obs.station_name) ?? '?'} (${num(obs.distance_km) ?? '?'} km).`, | |
| docId: 'nws_obs', citeId: 'nws_obs', mapLayer: 'nws', | |
| }; | |
| } | |
| function buildNoaaTides(state: Final): Card | null { | |
| const t = obj(state.noaa_tides); | |
| if (!t || t.error || num(t.observed_ft_mllw) == null) return null; | |
| const scalars: NonNullable<Card['scalars']> = [ | |
| { value: `${t.observed_ft_mllw} ft`, label: 'observed (MLLW)' }, | |
| ]; | |
| if (num(t.predicted_ft_mllw) != null) scalars.push({ value: `${t.predicted_ft_mllw} ft`, label: 'predicted' }); | |
| if (num(t.residual_ft) != null) scalars.push({ value: `${t.residual_ft} ft`, label: 'residual' }); | |
| return { | |
| id: 'fsm-noaa', | |
| stone: 'touchstone', tier: 'empirical', variant: 'scalars', | |
| source: 'NOAA CO-OPS', agency: `NOAA tide gauge ${str(t.station_name) ?? str(t.station_id) ?? '?'}`, | |
| vintage: str(t.obs_time)?.slice(0, 10) ?? RIPRAP_VINTAGE, | |
| title: 'Live water level (nearest tide gauge)', | |
| scalars, | |
| sub: 'Residual = observed − astronomical tide; positive residual is wind / surge component.', | |
| docId: 'noaa_tides', citeId: 'noaa_tides', mapLayer: 'noaa', | |
| }; | |
| } | |
| function buildPrithviLive(state: Final): Card | null { | |
| const p = obj(state.prithvi_live); | |
| if (!p?.ok) return null; | |
| const sceneDate = str(p.item_datetime)?.slice(0, 10); | |
| return { | |
| id: 'fsm-prithvi-live', | |
| stone: 'touchstone', tier: 'modeled', variant: 'raster-pred', | |
| source: 'Prithvi-NYC-Pluvial', agency: 'NASA-IBM Prithvi v2 · NYC fine-tune', | |
| vintage: sceneDate ? `${sceneDate} · Sentinel-2` : 'Sentinel-2', | |
| title: 'Pluvial flood prediction · Prithvi-NYC-Pluvial', | |
| rasterKind: 'prithvi', | |
| headline: `${num(p.pct_water_within_500m) ?? 0}% flooded`, | |
| subhead: `water within 500 m · cloud ${num(p.cloud_cover) ?? '?'}%`, | |
| sub: 'Test flood IoU 0.5979 on held-out NYC chips. Model interpretation, not a measurement.', | |
| illustrative: true, | |
| docId: 'prithvi_live', citeId: 'prithvi_live', mapLayer: 'prithvi-pluvial', | |
| }; | |
| } | |
| /** Conventional LULC palette — matches the design handoff's LULC card | |
| * visual (urban / water / vegetation / barren / wetland). The colors | |
| * are layer conventions, NOT new tier signals. */ | |
| const LULC_PALETTE: Record<string, string> = { | |
| urban: '#C66', | |
| water: '#5B7FB4', | |
| vegetation: '#5B8A4A', | |
| barren: '#A89A78', | |
| wetland: '#D9C75A', | |
| }; | |
| function buildTerramindLulc(state: Final): Card | null { | |
| const t = obj(state.terramind_lulc); | |
| if (!t?.ok) return null; | |
| // Translate the FSM's class_fractions dict into the design-system's | |
| // expected ordered class-mix (urban / water / vegetation / barren / | |
| // wetland). Unknown class names land in barren as a catch-all. | |
| const fractions = (obj(t.class_fractions) ?? {}) as Record<string, number>; | |
| const buckets: Record<keyof typeof LULC_PALETTE, number> = { | |
| urban: 0, water: 0, vegetation: 0, barren: 0, wetland: 0, | |
| }; | |
| for (const [k, v] of Object.entries(fractions)) { | |
| const lk = k.toLowerCase(); | |
| if (lk.includes('urban') || lk.includes('built') || lk.includes('impervious')) buckets.urban += v; | |
| else if (lk.includes('water')) buckets.water += v; | |
| else if (lk.includes('tree') || lk.includes('vegetation') || lk.includes('crop') || lk.includes('grass')) buckets.vegetation += v; | |
| else if (lk.includes('bare') || lk.includes('barren') || lk.includes('soil')) buckets.barren += v; | |
| else if (lk.includes('wet') || lk.includes('marsh')) buckets.wetland += v; | |
| else buckets.barren += v; | |
| } | |
| const classMix = (Object.entries(buckets) as [keyof typeof LULC_PALETTE, number][]) | |
| .filter(([, v]) => v > 0) | |
| .map(([k, v]) => ({ k, pct: Math.round(v), color: LULC_PALETTE[k] })); | |
| return { | |
| id: 'fsm-tm-lulc', | |
| stone: 'touchstone', tier: 'synthetic', variant: 'lulc', | |
| source: 'TerraMind v1.2', agency: 'IBM TerraMind v1.2 · Sentinel-2 inputs', | |
| vintage: 'Sentinel-2', | |
| title: 'Land use / land cover · TerraMind v1.2', | |
| rasterKind: 'lulc', | |
| classMix: classMix.length ? classMix : undefined, | |
| sub: 'Synthetic prior. LULC palette is a layer convention, not a tier signal.', | |
| illustrative: true, | |
| docId: 'tm_lulc', citeId: 'tm_lulc', mapLayer: 'terramind-lulc', | |
| }; | |
| } | |
| function buildTtmForecast(state: Final): Card | null { | |
| const t = obj(state.ttm_forecast); | |
| if (!t?.available || !t.interesting) return null; | |
| const peak = num(t.forecast_peak_ft); | |
| const ahead = num(t.forecast_peak_minutes_ahead); | |
| if (peak == null || ahead == null) return null; | |
| return { | |
| id: 'fsm-ttm-fc', | |
| stone: 'lodestone', tier: 'modeled', variant: 'timeseries', | |
| source: 'Granite TTM r2 (zero-shot)', agency: 'IBM Granite-TimeSeries · regional', | |
| vintage: RIPRAP_VINTAGE, | |
| title: 'Storm surge nowcast at The Battery — 9.6 h horizon (regional)', | |
| timeseries: { hours: 96, peak: { x: 38, y: 47 }, peakLabel: `${peak} ft @ +${Math.round(ahead/60)}h` }, | |
| headline: `${peak} ft`, | |
| subhead: 'peak surge residual · 9.6h horizon · 6-min cadence', | |
| sub: 'Regional disclosure. Distinct from the fine-tuned Battery surge nowcast.', | |
| spatialNote: 'regional · Battery, not point-of-query', | |
| docId: 'ttm_forecast', citeId: 'ttm_forecast', | |
| }; | |
| } | |
| function buildTtmBatterySurge(state: Final): Card | null { | |
| const t = obj(state.ttm_battery_surge); | |
| if (!t?.available || !t.interesting) return null; | |
| const peak = num(t.forecast_peak_m); | |
| const ahead = num(t.forecast_peak_hours_ahead); | |
| if (peak == null || ahead == null) return null; | |
| return { | |
| id: 'fsm-ttm-batt', | |
| stone: 'lodestone', tier: 'modeled', variant: 'timeseries-ft', | |
| source: 'msradam/Granite-TTM-r2-Battery-Surge', | |
| agency: 'Granite TTM r2 · NYC-specialized fine-tune', | |
| vintage: RIPRAP_VINTAGE, | |
| title: 'Storm surge nowcast at The Battery — 96 h horizon (NYC-specialized fine-tune)', | |
| timeseries: { | |
| hours: 96, | |
| peak: { x: ahead, y: Math.round(peak * 100) }, | |
| peakLabel: `${(peak * 100).toFixed(0)} cm @ +${ahead}h`, | |
| }, | |
| headline: `${(peak * 100).toFixed(0)} cm`, | |
| subhead: `peak surge · 96h horizon · hourly cadence`, | |
| sub: 'Fine-tuned on NYC tide-gauge history. Hourly cadence; applies city-wide via NOAA station 8518750.', | |
| spatialNote: 'regional · The Battery, not point-of-query', | |
| docId: 'ttm_battery', citeId: 'ttm_battery', | |
| // v0.4.5 §5 — fine-tuned-model footer chrome | |
| hfModelCard: 'huggingface.co/msradam/Granite-TTM-r2-Battery-Surge', | |
| rmse: '0.157 m', | |
| skillVsPersistence: '−35% vs persistence', | |
| hardwareBadge: 'MI300X', | |
| }; | |
| } | |
| function buildNwsAlerts(state: Final): Card | null { | |
| const a = obj(state.nws_alerts); | |
| if (!a) return null; | |
| const n = num(a.n_active) ?? 0; | |
| if (n <= 0) return null; | |
| const alerts = Array.isArray(a.alerts) ? (a.alerts as Record<string, unknown>[]) : []; | |
| return { | |
| id: 'fsm-nws-alerts', | |
| stone: 'lodestone', tier: 'modeled', variant: 'tabular', | |
| source: 'NWS', agency: 'NWS Public Alerts API · flood-relevant filter', | |
| vintage: RIPRAP_VINTAGE, | |
| title: `${n} active flood-relevant alert${n === 1 ? '' : 's'}`, | |
| columns: ['event', 'severity', 'expires'], | |
| rows: alerts.slice(0, 4).map((al) => [ | |
| str(al.event) ?? '?', | |
| str(al.severity) ?? '?', | |
| (str(al.expires) ?? '').slice(0, 16), | |
| ]), | |
| sub: 'Live NWS feed. If a FLOOD or FLASH FLOOD WARNING is in this list, foreground it.', | |
| docId: 'nws_alerts', citeId: 'nws_alerts', | |
| }; | |
| } | |
| function buildCapstoneMeta(final: FinalResult, wallSeconds?: number): Card { | |
| // v0.4.5 §2 — wire the four metrics to the reconciler's actual state. | |
| // The FSM emits `mellea` as `{ rerolls, n_attempts, requirements_passed, | |
| // requirements_failed, requirements_total }` (the mellea_validator | |
| // shape). Earlier UI types used `{ passed, failed, attempts }`; we | |
| // accept both so cards keep rendering across backend versions. | |
| const m = (final.mellea ?? {}) as Record<string, unknown>; | |
| const passedArr = Array.isArray(m.requirements_passed) | |
| ? (m.requirements_passed as unknown[]) | |
| : Array.isArray(m.passed) ? (m.passed as unknown[]) : []; | |
| const failedArr = Array.isArray(m.requirements_failed) | |
| ? (m.requirements_failed as unknown[]) | |
| : Array.isArray(m.failed) ? (m.failed as unknown[]) : []; | |
| const passed = passedArr.length; | |
| const failed = failedArr.length; | |
| const totalChecks = (typeof m.requirements_total === 'number' | |
| ? (m.requirements_total as number) | |
| : (passed + failed)) || 4; | |
| const attempts = (typeof m.n_attempts === 'number' | |
| ? (m.n_attempts as number) | |
| : (typeof m.attempts === 'number' ? (m.attempts as number) : 0)); | |
| const rerollsField = typeof m.rerolls === 'number' ? (m.rerolls as number) : null; | |
| const rerolls = rerollsField ?? Math.max(0, attempts - 1); | |
| const cites = final.citations?.length ?? 0; | |
| return { | |
| id: 'fsm-capstone-meta', | |
| stone: 'capstone', tier: 'modeled', variant: 'meta', | |
| source: 'Mellea', agency: 'Capstone synthesis · Granite 4.1 + Mellea grounding check', | |
| vintage: RIPRAP_VINTAGE, | |
| title: 'Briefing reconciliation', | |
| metaRows: [ | |
| { k: 'mellea reroll', v: `${rerolls} reroll${rerolls === 1 ? '' : 's'}` }, | |
| { k: 'grounding checks', v: `${passed}/${totalChecks} passed` }, | |
| { k: 'citations resolved', v: `${cites}` }, | |
| { k: 'wall-clock', v: wallSeconds != null ? `${wallSeconds.toFixed(1)} s` : '—' }, | |
| ], | |
| sub: 'Capstone produces prose, not cards. This meta-card is the integrity-narration UI for the entire pipeline.', | |
| docId: 'capstone', | |
| }; | |
| } | |
| /** Public adapter. Combines per-specialist card builders with the trace | |
| * → StoneTrace mapper into a single FindingsData payload. | |
| * | |
| * Accepts either a real FinalResult (at end-of-stream) or a partial | |
| * one synthesized from in-flight step events (during streaming). Each | |
| * builder returns null when its slice of state is missing — so cards | |
| * pop into the rail as their specialist completes, without waiting | |
| * for the full reconcile. */ | |
| export function adaptFinalToFindings( | |
| final: FinalResult | Partial<FinalResult> | null | undefined, | |
| trace: TraceNode | undefined | null, | |
| wallSeconds?: number, | |
| /** When true, the Capstone meta card renders even with a stub final | |
| * (we always want the run-summary). When false (no final at all), | |
| * the meta card is skipped — there's nothing to summarise yet. */ | |
| hasFinal: boolean = true, | |
| ): FindingsData { | |
| const f = (final ?? {}) as Final; | |
| const geocode = obj(f.geocode); | |
| const cards: (Card | null)[] = [ | |
| // Cornerstone | |
| buildSandy(f, geocode), | |
| buildDep(f), | |
| buildIdaHwm(f), | |
| buildPrithviWater(f), | |
| buildMicrotopo(f), | |
| // Keystone | |
| buildRegisters(f), | |
| buildTerramindBuildings(f), | |
| // Touchstone | |
| buildFloodnet(f), | |
| buildNyc311(f), | |
| buildNwsObs(f), | |
| buildNoaaTides(f), | |
| buildPrithviLive(f), | |
| buildTerramindLulc(f), | |
| // Lodestone | |
| buildNwsAlerts(f), | |
| buildTtmForecast(f), | |
| buildTtmBatterySurge(f), | |
| // Capstone (only once we have something to summarise) | |
| hasFinal ? buildCapstoneMeta((final ?? { paragraph: '' }) as FinalResult, wallSeconds) : null, | |
| ]; | |
| return { | |
| cards: cards.filter((c): c is Card => c != null), | |
| stones: buildStoneTraces(trace), | |
| wallSeconds, | |
| }; | |
| } | |
| /** Per-step-event live-state mapper. The FSM action `step_X` writes to | |
| * state key `X` (sometimes munged — e.g. `step_311` writes `nyc311`, | |
| * `step_terramind` writes `terramind`). The SSE `step.result` payload | |
| * is a slim summary (not the full doc body); cards adapt to whichever | |
| * fields are present. | |
| * | |
| * Mutates `live` in place and returns the keys that changed so callers | |
| * can decide whether to re-render. */ | |
| export function applyStepEventToLiveState( | |
| live: Record<string, unknown>, | |
| stepName: string, | |
| result: unknown, | |
| ok: boolean, | |
| ): string[] { | |
| const STEP_TO_STATE: Record<string, string> = { | |
| sandy_inundation: 'sandy', | |
| dep_stormwater: 'dep', | |
| floodnet: 'floodnet', | |
| nyc311: 'nyc311', | |
| noaa_tides: 'noaa_tides', | |
| nws_alerts: 'nws_alerts', | |
| nws_obs: 'nws_obs', | |
| ttm_forecast: 'ttm_forecast', | |
| ttm_311_forecast: 'ttm_311_forecast', | |
| ttm_battery_surge: 'ttm_battery_surge', | |
| floodnet_forecast: 'floodnet_forecast', | |
| ida_hwm_2021: 'ida_hwm', | |
| prithvi_eo_v2: 'prithvi_water', | |
| prithvi_eo_live: 'prithvi_live', | |
| microtopo_lidar: 'microtopo', | |
| mta_entrance_exposure: 'mta_entrances', | |
| nycha_development_exposure: 'nycha_developments', | |
| doe_school_exposure: 'doe_schools', | |
| doh_hospital_exposure: 'doh_hospitals', | |
| terramind_synthesis: 'terramind', | |
| terramind_lulc: 'terramind_lulc', | |
| terramind_buildings: 'terramind_buildings', | |
| eo_chip_fetch: 'eo_chip', | |
| geocode: 'geocode', | |
| }; | |
| const key = STEP_TO_STATE[stepName]; | |
| if (!key) return []; | |
| // Translate the slim summary shapes the FSM emits into the | |
| // doc-payload shapes the card builders expect. Mostly identity | |
| // (the summaries already nest the relevant fields), with a few | |
| // exceptions documented inline. | |
| if (stepName === 'sandy_inundation') { | |
| // FSM summary: { inside: bool }. Adapter expects state.sandy === true. | |
| const r = result as Record<string, unknown> | null; | |
| live[key] = ok && r?.inside === true ? true : (ok ? false : null); | |
| } else if (stepName === 'dep_stormwater') { | |
| // FSM summary: { dep_extreme_2080: 'label', dep_moderate_2050: 'label', ... }. | |
| // Adapter expects state.dep[scen] = { depth_class, depth_label }. | |
| // Reconstruct depth_class>0 from any non-empty label. | |
| const r = (result as Record<string, unknown>) ?? {}; | |
| const dep: Record<string, unknown> = {}; | |
| for (const [scen, label] of Object.entries(r)) { | |
| const lbl = typeof label === 'string' ? label : ''; | |
| if (!lbl) continue; | |
| dep[scen] = { depth_class: 1, depth_label: lbl }; | |
| } | |
| live[key] = Object.keys(dep).length ? dep : null; | |
| } else if (ok && result != null) { | |
| live[key] = result; | |
| } else { | |
| live[key] = null; | |
| } | |
| return [key]; | |
| } | |