File size: 6,919 Bytes
64d8003
 
c65b8bc
2a534fa
 
 
c65b8bc
 
 
2a534fa
 
a1f52bf
 
 
 
 
 
 
 
 
2a534fa
 
 
 
a1f52bf
 
 
 
2a534fa
 
a1f52bf
2a534fa
a1f52bf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2dfa899
 
 
 
d4c9a3c
 
 
 
 
 
 
 
c5672b8
2dfa899
c5672b8
2dfa899
 
 
 
 
c5672b8
 
2dfa899
 
 
 
c65b8bc
2a534fa
c65b8bc
a1f52bf
 
 
 
2a534fa
 
 
 
a1f52bf
 
 
 
 
2a534fa
 
 
 
 
c65b8bc
2a534fa
c65b8bc
 
 
 
 
 
 
 
 
 
 
 
2a534fa
 
 
4488687
 
 
2a534fa
 
 
 
 
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
# AGENTS.md

Guidance for AI coding agents working in this repo. The active surface is
`app.py` + `ui/` + `wavereader/` + `data/` (the v2 rebuild). `legacy/`
holds the frozen v1 front end (`main.py` + `app/`) β€” don't extend it; it
survives only as the fallback layer in `ui/_compat.py`.

## What is live now

- `app.py` β€” v2 entrypoint: `build()` + `gr.api` tools +
  `demo.launch(css=APP_CSS, mcp_server=True)`. Run `uv run python app.py`.
- `ui/` β€” the single-page front end (no tabs): `app.py` (layout + wiring,
  ≀3 `gr.State`; Bells Beach auto-selected on load so there are no empty
  states), `panels/{lens,story,intel,agent}.py` (map-lens column | forecast
  story | intel rail | full-width agent bar), `charts/{strip,score,seafloor,climate,map,_style}.py`,
  `contracts.py` (`SELECT_KEYS`/`FETCH_KEYS`/`AGENT_KEYS` + `fill()` β€”
  handlers return via `fill(KEYS, …)` so output ordering can't drift),
  `_compat.py` (backend access: v2 core β†’ `legacy.app.*` β†’ stubs; also
  wetsuit hints, engine chips, surf cams), `theme.py` (`APP_CSS`, IBM Plex
  Mono lo-fi identity β€” don't restyle without asking).
- `wavereader/` β€” pure core, typed + tested, no Gradio imports:
  `breaks.py` (catalogue, memoized `load_breaks`), `openmeteo.py`
  (versioned/atomic disk cache, seaward grid-snap), `scoring.py` (deterministic
  0–10 engine β€” the LLM never owns numbers), `seafloor.py` (GEBCO world model),
  `climate.py` (ERA5 5-yr profiles + audit; `break_climate` reads the
  profile embedded on each break record, per-slug file fallback), `tools.py`
  (the typed tool functions shared by agent + MCP), `agent.py`
  (smolagents `ToolCallingAgent`,
  native tool calls, budget: 6 steps / 2 score_week / 700 tokens),
  `narrate.py` + `llm.py` (one InferenceClient factory:
  Nemotron 3 Ultra via deepinfra, `WR_*` env overrides).
- `scripts/warm_caches.py` β€” pre-fill forecast + seafloor caches (demo hot
  start). `scripts/build_climate.py` β€” climatology builder (re-embeds built
  profiles into the catalogue after each run, backup first).
- `scripts/check_coords.py` + `scripts/check_coast.py` β€” break-coordinate
  maintenance: `check_coords` validates/fixes state/region/duplicate
  placement against OSM; `check_coast` audits GEBCO elevation at every
  break point and (`--fix`) snaps non-coastal ones to the nearest 0 m
  shoreline crossing (both write a timestamped backup first).
- `data/` β€” `australia-surf-breaks-enriched.json` (238 breaks; each record
  embeds its ERA5 profile under `climate` β€” 236 of 238, the two duplicate
  Gnaraloo rows have no profile),
  `surf-break-schema.json` (contract), `climate/*.json` (per-slug ERA5
  profiles β€” `build_climate.py` output and `break_climate`'s fallback;
  also `index.json` manifest), `surf-cams.json` (camera links keyed
  `"name | state | region"`), `surf-break-example-bells.json` (prompt worked
  example).

## Deploying to HF Spaces

- The Space `lucashudsn/wavereader` (public) *is* this repo β€” remote
  `space`. Every push to `main` auto-deploys via
  `.github/workflows/deploy-space.yml` (needs the GitHub repo secret
  `HF_TOKEN`) β€” merging to main ships it; no manual step. For any other
  branch, ship manually with `scripts/deploy_space.sh [ref]` (builds a
  throwaway commit that prepends `space-config.yaml` to `README.md` and
  force-pushes it to `space:main` β€” Spaces need YAML frontmatter, GitHub
  doesn't have to see it; the ref defaults to your current branch), then
  `git push origin <branch>` to keep GitHub in sync. The Space builds
  from the pushed commit; watch *Logs β†’ Build* on the Space page or poll
  `HfApi().get_space_runtime("lucashudsn/wavereader")` until `RUNNING`.
- `space-config.yaml` is the Space config. Keep
  `sdk_version` in lockstep with the gradio pin in `requirements.txt`
  (regenerate that file only via `uv export --no-hashes
  --format requirements-txt --no-dev -o requirements.txt` β€” other
  invocations can leave stray progress lines that break pip), and
  `python_version` in lockstep with `pyproject.toml`'s `requires-python`.
  If you change Space settings via the web UI, HF rewrites the Space's
  `README.md` β€” mirror anything you care about back into the yaml.
- `HF_TOKEN` is a Space secret (set once via *Settings β†’ Variables and
  secrets* or `HfApi().add_space_secret`); `WR_*` overrides can be added
  the same way. `.cache/` is ephemeral on the Space and rebuilds lazily β€”
  no persistent storage, no warm step.

## Conventions that matter

- One spot drives everything: `selected` (gr.State) is the single source of
  truth β€” picking a spot re-aims the map, badges, forecast strip, world
  model and climatology together. Keep it that way.
- Staged spin-ups: `ui/panels/story.py::fetch_forecast` is a **generator**
  that yields stage 1 (charts, ~ms) then stage 2 (seafloor world model from
  a parallel thread). Status lines name the engine + latency. Keep that
  pattern for anything slow.
- Charts: theme through `ui/charts/_style.py::style_fig`; weekend shading
  via `add_weekend_shading`. The week strip lives in `charts/strip.py`
  (score/swell/wind rows); window slicing (weekend/mornings/arvos) re-charts
  from state via `filter_hours` β€” no refetch. Seafloor display is bicubically
  smoothed (scipy) and its wave animation is period-driven β€” smoothing and
  animation are display-only; stats stay on the raw grid.
- MCP surface = exactly `score_week` / `rank_region_week` / `explain_score`
  (`gr.api` in `app.py`). All event listeners are registered with
  `api_name=False` β€” keep it that way.
- The LLM never owns numbers. Scores/forecasts/analysis stay deterministic;
  the model narrates and cites tools.

## Data contracts

- Required break fields: `name`, `state`, `region`, `location`,
  `skillLevel`, `breakType`, `peakType`, `idealSwell`, `idealWind`,
  `idealTide`. Optional: `description`, `id`, `bestSeason`, `hazards`,
  `crowdFactor`. Never invent enum values β€” check
  `data/surf-break-schema.json` first.
- Canonical `state`/`region` always come from the input list, never the
  model. `location.country` is `"Australia"`.
- New prompts must load schema + example from `data/` (don't paste copies).

## Rules for edits

1. Everything is tracked in this repo (no nested git β€” don't re-init one).
2. Never commit `.env`, `.venv/`, `__pycache__/`, `.DS_Store`, `.cache/`
   (all ignored).
3. Never read `.env` or print secrets/tokens. Use `.env.example` for var
   names; check presence with `printenv HF_TOKEN | wc -c` or
   `[ -n "$HF_TOKEN" ]` style checks that never echo the value.
4. Keep prompt files in `data/`; keep `app.py` free of embedded schema text.
5. Test cheaply: `uv run pytest -q` (offline fixtures) +
   `uv run python -c "import app"` for import health. Live LLM calls cost
   inference β€” dry-run the agent via the injected fake model
   (`tests/test_agent_dryrun.py`) before any live turn.