File size: 9,572 Bytes
1184305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48be8c8
1184305
 
 
 
 
 
48be8c8
1184305
 
 
 
 
 
48be8c8
1184305
 
 
 
 
 
48be8c8
1184305
 
 
 
 
 
48be8c8
1184305
 
 
 
 
 
 
 
48be8c8
1184305
 
 
 
 
 
48be8c8
1184305
 
 
 
 
 
48be8c8
1184305
 
 
 
 
 
48be8c8
1184305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48be8c8
1184305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48be8c8
1184305
48be8c8
1184305
 
 
 
 
 
48be8c8
1184305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
/**
 * Stone specialist registry — the auditability contract.
 *
 * Each Stone declares the **complete inventory** of specialists it
 * could fire on a query, along with the FSM step name (used to join
 * against the run trace) and a one-line skip reason for when a
 * specialist is absent from a particular run.
 *
 * v0.4.5 §3: every Stone's expander shows the full intended roster,
 * never a filtered subset. A reader who expands a Stone sees what
 * could have happened *and* what did. Specialists missing from the
 * run output render as `not_invoked` with their registered skip
 * reason.
 *
 * The display name and FSM step name often differ (the trace emits
 * `mta_entrance_exposure`, the FSM action is `step_mta_entrances`,
 * the Findings adapter writes state key `mta_entrances`). The
 * `stepNames` list maps the registry entry to all variants the
 * trace might emit so we don't double-count or miss matches.
 */
import type { StoneKey, StoneMember } from '$lib/types/card';

export type RegistryEntry = {
  /** Stable id used when projecting a not_invoked row into the trace. */
  id: string;
  /** Display name in the provenance row (italic-serif). */
  name: string;
  /** All FSM step names that count as a "fire" for this entry. */
  stepNames: string[];
  /** Default tier when known; not_invoked rows render this color. */
  tier?: StoneMember['tier'];
  /** One-line message rendered when the specialist is not_invoked.
   *  Engineering-honest voice (V0.4.5_SPEC.md §1) — describe the
   *  precondition that wasn't met, not "no data found". */
  skipReason: string;
};

export const STONE_REGISTRY: Record<StoneKey, RegistryEntry[]> = {
  cornerstone: [
    {
      id: 'CORN-001',
      name: 'sandy_inundation.lookup',
      stepNames: ['sandy', 'sandy_inundation', 'sandy_nta'],
      tier: 'empirical',
      skipReason: 'Sandy 2012 inundation: query outside NYC bounds',
    },
    {
      id: 'CORN-002',
      name: 'dep_stormwater.lookup',
      stepNames: ['dep', 'dep_stormwater', 'dep_extreme_2080_nta', 'dep_moderate_2050_nta', 'dep_moderate_current_nta'],
      tier: 'modeled',
      skipReason: 'NYC DEP stormwater scenarios: query outside NYC bounds',
    },
    {
      id: 'CORN-003',
      name: 'usgs_hwm.spatial_join',
      stepNames: ['ida_hwm', 'ida_hwm_2021'],
      tier: 'empirical',
      skipReason: 'USGS Ida HWMs: no marks within 800 m of address',
    },
    {
      id: 'CORN-004',
      name: 'prithvi_water.lookup',
      stepNames: ['prithvi', 'prithvi_eo_v2'],
      tier: 'modeled',
      skipReason: 'Prithvi-EO Ida polygons: no polygons within 500 m',
    },
    {
      id: 'CORN-005',
      name: 'microtopo.dem_hand_twi',
      stepNames: ['microtopo', 'microtopo_lidar', 'microtopo_nta'],
      tier: 'proxy',
      skipReason: 'USGS 3DEP DEM: query outside NYC raster coverage',
    },
  ],
  keystone: [
    {
      id: 'KEY-001',
      name: 'mta_entrance_exposure',
      stepNames: ['mta_entrances', 'mta_entrance_exposure'],
      tier: 'empirical',
      skipReason: 'no entrances within radius',
    },
    {
      id: 'KEY-002',
      name: 'nycha.development_join',
      stepNames: ['nycha', 'nycha_development_exposure'],
      tier: 'empirical',
      skipReason: 'no NYCHA developments within 1.0 mi',
    },
    {
      id: 'KEY-003',
      name: 'doe.school_join',
      stepNames: ['doe_schools', 'doe_school_exposure'],
      tier: 'empirical',
      skipReason: 'no DOE schools within 1.0 mi',
    },
    {
      id: 'KEY-004',
      name: 'doh.facility_join',
      stepNames: ['doh_hospitals', 'doh_hospital_exposure'],
      tier: 'empirical',
      skipReason: 'no acute-care hospitals within 1.0 mi',
    },
    {
      id: 'KEY-005',
      name: 'pluto.lot_lookup',
      stepNames: ['pluto_lookup'],
      tier: 'empirical',
      skipReason: 'PLUTO join skipped: queried address not in NYC PLUTO dataset',
    },
    {
      id: 'KEY-006',
      name: 'terramind.buildings',
      stepNames: ['terramind_buildings', 'terramind_synthesis'],
      tier: 'modeled',
      skipReason: 'TerraMind Buildings: no eo_chip available for this address (recent <30% cloud Sentinel-2 missing) or no high-confidence prediction',
    },
  ],
  touchstone: [
    {
      id: 'TCH-001',
      name: 'floodnet.history',
      stepNames: ['floodnet'],
      tier: 'empirical',
      skipReason: 'FloodNet sensor: no deployments within 600 m',
    },
    {
      id: 'TCH-002',
      name: 'nyc311.flood_complaints',
      stepNames: ['nyc311', 'nyc311_nta'],
      tier: 'proxy',
      skipReason: 'NYC 311: no flood-relevant complaints within 200 m',
    },
    {
      id: 'TCH-003',
      name: 'nws_obs.metar',
      stepNames: ['nws_obs'],
      tier: 'empirical',
      skipReason: 'NWS hourly METAR: nearest ASOS reports no precipitation',
    },
    {
      id: 'TCH-004',
      name: 'noaa_coops.recent',
      stepNames: ['noaa_tides'],
      tier: 'empirical',
      skipReason: 'NOAA tide gauge: nearest station >25 km from address',
    },
    {
      id: 'TCH-005',
      name: 'prithvi_nyc_pluvial',
      stepNames: ['prithvi_live', 'prithvi_eo_live'],
      tier: 'modeled',
      skipReason: 'Prithvi-NYC-Pluvial: no <30% cloud Sentinel-2 chip available in the last 120 d for this address',
    },
    {
      id: 'TCH-006',
      name: 'terramind.lulc',
      stepNames: ['terramind_lulc'],
      tier: 'synthetic',
      skipReason: 'TerraMind LULC: eo_chip fetch returned no Sentinel-2 tile for this address',
    },
  ],
  lodestone: [
    {
      id: 'LOD-001',
      name: 'nws_alerts.flood_relevant',
      stepNames: ['nws_alerts'],
      tier: 'modeled',
      skipReason: 'NWS public alerts: no active flood-relevant alerts at this address',
    },
    {
      id: 'LOD-002',
      name: 'ttm_battery_surge.zero_shot',
      stepNames: ['ttm_forecast'],
      tier: 'modeled',
      skipReason: 'Granite TTM r2 zero-shot: forecast not interesting (peak |residual| < 0.3 ft)',
    },
    {
      id: 'LOD-003',
      name: 'ttm_battery_surge.fine_tune',
      stepNames: ['ttm_battery_surge'],
      tier: 'modeled',
      skipReason: 'Granite TTM Battery fine-tune: forecast not interesting (peak |residual| < 0.3 m)',
    },
    {
      id: 'LOD-004',
      name: 'ttm_311_forecast',
      stepNames: ['ttm_311_forecast'],
      tier: 'modeled',
      skipReason: 'NYC 311 weekly forecast: no per-address history to extrapolate',
    },
    {
      id: 'LOD-005',
      name: 'floodnet_forecast',
      stepNames: ['floodnet_forecast'],
      tier: 'modeled',
      skipReason: 'FloodNet sensor recurrence: sensor has < silent-floor historical events; forecast omitted',
    },
    {
      id: 'LOD-006',
      name: 'npcc4.slr_projection',
      stepNames: ['npcc4_projection'],
      tier: 'modeled',
      skipReason: 'NPCC4 SLR projection: not yet wired into FSM (static reference card on hold)',
    },
  ],
  capstone: [
    {
      id: 'CAP-001',
      name: 'rag.granite_embedding',
      stepNames: ['rag_granite_embedding'],
      tier: 'proxy',
      skipReason: 'Granite Embedding RAG: no policy retrieval (out-of-NYC scope)',
    },
    {
      id: 'CAP-002',
      name: 'gliner.typed_extraction',
      stepNames: ['gliner_extract'],
      tier: 'proxy',
      skipReason: 'GLiNER typed extraction: no RAG hits to extract over',
    },
    {
      id: 'CAP-003',
      name: 'granite41.compose_briefing',
      stepNames: ['reconcile_granite41', 'mellea_reconcile_address', 'reconcile_neighborhood', 'reconcile_development', 'reconcile_live_now'],
      tier: 'modeled',
      skipReason: 'Reconciler did not run (no grounded data available)',
    },
    {
      id: 'CAP-004',
      name: 'mellea.grounding_check',
      stepNames: ['mellea_grounding'],
      tier: 'modeled',
      skipReason: 'Mellea grounding-check: rolled into reconcile step on this run',
    },
  ],
};

/** Project the registry against the run's actual specialist members.
 *  Emits a full-roster member list per Stone — present specialists keep
 *  their live status; absent ones land as `not_invoked` with their
 *  registered skip reason. */
export function fillRosterForStone(
  stone: StoneKey,
  liveMembers: StoneMember[],
): StoneMember[] {
  const registry = STONE_REGISTRY[stone] ?? [];
  // Index live members by every step name they could match.
  const liveByStep = new Map<string, StoneMember>();
  for (const m of liveMembers) {
    liveByStep.set(m.name, m);
  }

  const out: StoneMember[] = [];
  const used = new Set<string>();

  for (const entry of registry) {
    let live: StoneMember | undefined;
    for (const sn of entry.stepNames) {
      const hit = liveByStep.get(sn);
      if (hit) {
        live = hit;
        used.add(sn);
        break;
      }
    }
    if (live) {
      out.push({
        ...live,
        // Override id + name with the registry's display strings so the
        // provenance row reads consistently regardless of trace munging.
        id: entry.id,
        name: entry.name,
        tier: live.tier ?? entry.tier ?? null,
      });
    } else {
      out.push({
        id: entry.id,
        name: entry.name,
        status: 'not_invoked',
        tier: entry.tier ?? null,
        note: entry.skipReason,
      });
    }
  }

  // Append any live members that weren't in the registry — they were
  // emitted by the FSM but we don't know about them. Surface them
  // anyway so we don't silently drop trace rows.
  for (const m of liveMembers) {
    if (!used.has(m.name)) out.push(m);
  }

  return out;
}