Skip to main content

Feature Specs & Acceptance Criteria

Conventions: FS-x = feature spec, FR-x.y = functional requirement, AC-x.y = acceptance criterion. Every AC maps to a pytest test or an explicit verified-by-inspection entry in docs/traceability.md. Anchor AOI and windows (used throughout):

  • AOI (San Pedro Bay): bbox [-118.29, 33.68, -117.99, 33.79]; LA River outlet (-118.193, 33.755); San Gabriel River outlet (-118.096, 33.740).
  • Pre-storm window: 2024-01-10 → 2024-01-28. Post-storm window: 2024-02-05 → 2024-02-24 (following the Feb 4–7, 2024 atmospheric river).

How the feature specs compose:


FS-1 — Sentinel-2 FDI Ingestion

Detect floating-debris candidates in Sentinel-2 L2A imagery using the Floating Debris Index (Biermann et al. 2020).

Stories: US-05, US-07, US-08

Functional requirements

  • FR-1.1 Search the sentinel-2-l2a collection on the Element84 earth-search STAC API by AOI bbox, datetime window, and max cloud cover (default 40%, configurable).
  • FR-1.2 Read bands B03 (green), B04 (red), B06 (red-edge 2), B08 (NIR), B11 (SWIR1) via windowed COG reads for the AOI only; compute on a 20 m grid.
  • FR-1.3 Mask non-water pixels with NDWI = (B03 − B08)/(B03 + B08); keep pixels with NDWI > 0.
  • FR-1.4 Compute FDI = R_NIR − R′_NIR, where R′_NIR = R_RE2 + (R_SWIR1 − R_RE2) × ((λ_NIR − λ_RED)/(λ_SWIR1 − λ_RED)) × 10, with λ_NIR = 832.8 nm, λ_RED = 664.6 nm, λ_SWIR1 = 1613.7 nm.
  • FR-1.5 Flag anomalies: water pixels with FDI > μ_water + k·σ_water (k default 3.0, scene-adaptive statistics).
  • FR-1.6 Cluster flagged pixels (8-connectivity, minimum 2 pixels) into detections with centroid, footprint, area (m²), mean FDI, and a 0–1 score.
  • FR-1.7 Emit detections as Observation records via the common SourceAdapter interface; retain scene ID, band statistics, and threshold parameters in the raw payload.

Acceptance criteria

IDGiven / When / ThenVerified by
AC-1.1Given the AOI and post-storm window, when the adapter searches the STAC API, then ≥1 L2A scene within the cloud limit is returned.pytest (network-marked)
AC-1.2Given a synthetic 5-band raster with known reflectances, when FDI is computed, then values match the FR-1.4 formula within 1e-6.pytest
AC-1.3Given a synthetic raster containing a land block (NDWI ≤ 0) with debris-like spectra, when the pipeline runs, then no detection falls on land pixels.pytest
AC-1.4Given a completed scene run, when detections are emitted, then every Observation carries scene ID, capture timestamp, geometry, score, and band provenance.pytest
AC-1.5Given the adapter registry, when a new satellite adapter implementing SourceAdapter is registered (e.g., future CDSE), then the pipeline consumes it without changes downstream.pytest

Out of scope: ML classification (MARIDA-trained model is post-MVP), atmospheric re-correction, sun-glint correction.


FS-2 — NOAA MDMAP Ingestion

Parse NOAA Marine Debris Monitoring and Assessment Project shoreline-survey exports.

Stories: US-01, US-03

Functional requirements

  • FR-2.1 Parse the MDMAP survey export CSV format (one row per survey × item category: site, coordinates, survey date, item category, count).
  • FR-2.2 Emit one Observation per survey with per-category item counts aggregated; geometry from site coordinates.
  • FR-2.3 Reject malformed rows (missing coordinates/date, non-numeric counts) into a rejection report with row number and reason.

Acceptance criteria

IDGiven / When / ThenVerified by
AC-2.1Given an MDMAP-format CSV, when ingested, then each survey becomes an Observation with site name, timestamp, geometry, and item counts.pytest
AC-2.2Given the same CSV, when ingested, then the sum of item counts in equals the sum stored (no silent loss).pytest
AC-2.3Given a CSV containing malformed rows, when ingested, then valid rows load, malformed rows appear in the rejection report with reasons, and the counts reconcile.pytest

FS-3 — Debris Tracker Ingestion (unstructured extraction)

Parse Debris Tracker citizen-science exports, including free-text notes, into structured records.

Stories: US-01, US-04

Functional requirements

  • FR-3.1 Parse the Debris Tracker export CSV (list/item/material/quantity/lat/lon/timestamp/description).
  • FR-3.2 Extract structure from free-text descriptions (quantities, materials, items, event context like "after the storm") via a rule-based extractor; an optional LLM-assist path may refine results when Bedrock is available.
  • FR-3.3 Tag every extraction with its method (rules or llm) and set needs_review=True when confidence is low or fields conflict — never silently guess.

Acceptance criteria

IDGiven / When / ThenVerified by
AC-3.1Given a Debris Tracker export CSV, when ingested, then each row becomes an Observation with item, material, quantity, geometry, and timestamp.pytest
AC-3.2Given a note like "about 30 plastic bottles near the jetty after the storm", when extracted, then quantity=30, material=plastic, item=bottles, and storm context are captured with the extraction method recorded.pytest
AC-3.3Given an ambiguous note (e.g., "some stuff washed up"), when extracted, then the observation is flagged needs_review=True with no fabricated quantity.pytest

FS-4 — Normalization Core

One schema, one store, full provenance — the heart of the Helio engine transfer.

Stories: US-01, US-02, US-03

Functional requirements

  • FR-4.1 Define Observation (pydantic): id, source (enum), observed_at, geometry (GeoJSON point/polygon), category (satellite_detection | shoreline_survey | citizen_report), attributes (typed per category: score/area for detections, item counts for surveys, item/material/quantity for reports), needs_review, raw (verbatim source payload), ingested_at.
  • FR-4.2 Persist to SQLite with a deterministic natural key per source record; upserts make re-runs idempotent.
  • FR-4.3 Query interface: filter by bbox, time range, source, category.
  • FR-4.4 Pipeline runner executes all registered adapters and reports per-source counts (loaded / rejected / flagged) plus live-vs-fixture provenance.

Acceptance criteria

IDGiven / When / ThenVerified by
AC-4.1Given output from all three adapters, when validated, then every record conforms to the Observation schema.pytest
AC-4.2Given a completed pipeline run, when the pipeline runs again on the same inputs, then the store contains no duplicates and counts are unchanged.pytest
AC-4.3Given any stored observation, when fetched, then its raw source payload and source identity are intact.pytest
AC-4.4Given stored observations, when queried by bbox + time range + source, then exactly the matching records return.pytest

FS-5 — Accumulation Zone Classification

Cross-reference satellite detections with ground truth to classify zones.

Stories: US-06

Functional requirements

  • FR-5.1 Within a time window, cluster satellite detections spatially (DBSCAN-style: 1 km linkage).
  • FR-5.2 Classify each cluster: confirmed if ≥1 ground observation (survey or citizen report) lies within 2 km and ±14 days of any member detection; otherwise suspected.
  • FR-5.3 Classify ground-only concentrations (≥3 ground observations within 1 km in-window, no satellite cluster) as reported.
  • FR-5.4 Emit zones as a GeoJSON FeatureCollection: convex-hull footprint (buffered for point-degenerate cases), status, evidence (detection/ground counts and IDs), window label.
  • FR-5.5 Zone computation is strictly window-scoped: only in-window observations participate.

Acceptance criteria

IDGiven / When / ThenVerified by
AC-5.1Given a detection cluster and a ground observation within 2 km and ±14 days, when zones are computed, then that zone's status is confirmed and its evidence lists both.pytest
AC-5.2Given a detection cluster with no ground observation in range, then its status is suspected; given ≥3 ground observations within 1 km and no detections, then status is reported.pytest
AC-5.3Given computed zones, when serialized, then the output is valid GeoJSON with status, evidence counts, and window label per feature.pytest
AC-5.4Given observations outside the requested window, when zones are computed, then those observations affect nothing.pytest

FS-6 — Map-First Web App

MapLibre GL map served by FastAPI; the demo surface.

Stories: US-09, US-10, US-11, US-12

Functional requirements

  • FR-6.1 GET / serves the map app; GET /api/observations?window=&source= and GET /api/zones?window= return GeoJSON.
  • FR-6.2 Layers: zones (fill: confirmed #F44336, suspected #FF9800, reported #2196F3), satellite detections (graduated circles by score), ground observations (distinct markers per source); river outlet markers for LA River and San Gabriel River.
  • FR-6.3 Pre/post-storm window toggle re-fetches layers without page reload.
  • FR-6.4 Click popups show observation/zone details incl. source, time, evidence, and needs_review flags.
  • FR-6.5 Legend plus a provenance banner stating which sources are live and which are fixtures in the current dataset (fed by the FR-4.4 run report).

Acceptance criteria

IDGiven / When / ThenVerified by
AC-6.1Given a populated store, when /api/zones?window=post is called, then valid GeoJSON returns and the map renders zone layers colored by status.pytest (API) + inspection (render)
AC-6.2Given the loaded map, when the window toggle is switched, then layers update to the other window without reload.inspection
AC-6.3Given a rendered zone or observation, when clicked, then a popup shows its source, time, and evidence.inspection
AC-6.4Given the loaded map, then the legend and live-vs-fixture provenance banner are visible and accurate per the latest pipeline run report.pytest (API) + inspection

FS-7 — Natural-Language Query

Ask questions over the normalized store; answers must be data-grounded.

Stories: US-13, US-14

Functional requirements

  • FR-7.1 POST /api/query {question}{answer, mode, evidence}.
  • FR-7.2 LLM mode: Claude via AWS Bedrock with tool-use over defined tools — count_observations(filters), zone_summary(window, status), compare_windows(metric), top_items(window). The model must answer only from tool results.
  • FR-7.3 Offline mode: without working Bedrock access, a rule-based handler answers count/summary/comparison questions from the same tools; response is labeled mode: "offline".
  • FR-7.4 Questions outside the data's scope return an explicit insufficient-data answer.

Acceptance criteria

IDGiven / When / ThenVerified by
AC-7.1Given a populated store, when a supported question is posted, then the answer contains figures matching the store and lists the evidence used.pytest (offline mode)
AC-7.2Given Bedrock credentials, when a question is posted, then ≥1 tool call is executed and the answer is built from its results; without credentials, the same endpoint answers in labeled offline mode.pytest (offline) + live check (Bedrock)
AC-7.3Given a question the data cannot answer (e.g., "how many whales were affected?"), then the response says the data is insufficient and fabricates nothing.pytest (offline mode)

FS-8 — Auto-Generated Briefing

One-click post-event situation briefing.

Stories: US-15, US-16

Functional requirements

  • FR-8.1 GET /api/briefing?window=post composes a briefing: scenes analyzed, detections, zone breakdown by status, top ground-reported items, and deltas vs. the pre-storm window.
  • FR-8.2 The briefing includes a data-provenance section (live vs. fixture per source, from the FR-4.4 run report).
  • FR-8.3 Output is markdown; scripts/make_briefing.py renders it to PDF via tools/pdfgen (LLM narrative polish optional when Bedrock is available, clearly attributed).

Acceptance criteria

IDGiven / When / ThenVerified by
AC-8.1Given a populated store, when the post-storm briefing is generated, then it contains detection counts, zone breakdown, top items, and pre-vs-post deltas that match the store.pytest
AC-8.2Given any briefing, then a provenance section states which sources were live vs. fixture.pytest
AC-8.3Given a generated briefing, when rendered via tools/pdfgen, then a PDF is produced.pytest (script exit + file exists)

v2 Addendum — App Shell & Chat Refinement (2026-07-06)

Post-review changes requested after the first working demo: desktop-app shell, map clarity, chat UX.

FS-6b — Desktop shell, basemap, and feature detail

Stories: US-09..US-12 (extends FS-6)

  • FR-6.6 Left icon rail (Figma-style) hosting panels: Layers, Ask, Briefing, Data; one panel open at a time; map stays full-bleed.
  • FR-6.7 Basemap toggle: Chart (Carto light) ↔ Satellite (Esri World Imagery), always visible in a floating top bar beside the window toggle.
  • FR-6.8 Clicking any feature opens a right-side detail panel with the actual metrics: per-item counts for surveys, item/material/quantity + note for citizen reports, FDI score/area/scene for detections, evidence numbers + site names for zones — plus Google Maps satellite and Street View deep links for the location.
  • FR-6.9 First-open explainer card describing the three data layers; dismissible, remembered via localStorage.
  • FR-6.10 Zones carry ground_sites (names) and centroid in their GeoJSON properties; observations expose full attribute metrics (incl. per-item counts).
IDGiven / When / ThenVerified by
AC-6.5Given the map, when the basemap toggle is switched, then tiles change between chart and satellite without reload.inspection
AC-6.6Given a clicked survey/report/detection/zone, then the detail panel shows its real metrics and working Google Maps links.pytest (API metrics) + inspection
AC-6.7Given zones GeoJSON, then features carry ground_sites and centroid.pytest

FS-7b — Chat refinement

Stories: US-13, US-14 (extends FS-7)

  • FR-7.5 Chat thread UI with history, suggested questions, disabled input while pending, and a progress indicator showing elapsed time (LLM queries typically run 10–60 s — waiting must be designed).
  • FR-7.6 site_breakdown tool exposes per-site names/coordinates/totals so answers can reference real places; offline mode routes "which beach/site" questions to it and phrases answers in plain language (no raw JSON).
  • FR-7.7 Bedrock calls run at effort: low with reduced max_tokens to cut interactive latency.
IDGiven / When / ThenVerified by
AC-7.4Given "which beach had the most trash after the storm?", then the answer names the top site with its item total.pytest (offline) + live check
AC-7.5Given a pending query, then the UI shows an animated progress state with elapsed seconds and re-enables on completion or error.inspection