Stephen Lee commited on
Commit
a975979
·
1 Parent(s): a3c85ad

feat: ship source-backed Gradio Q&A

Browse files
README.md CHANGED
@@ -17,10 +17,11 @@ tags:
17
 
18
  # CoastWise
19
 
20
- CoastWise is an offline-first California coastal species and harvest-safety
21
- lookup MVP. It helps users search curated San Francisco coast species and
22
- categories, read conservative rule cards, see cached CDFW/CDPH source links, and
23
- understand when to verify before harvesting.
 
24
 
25
  The MVP is a decision-support field guide. It does not certify species
26
  identification, legality, harvest authorization, or food safety.
@@ -31,13 +32,13 @@ Target track: Backyard AI.
31
 
32
  Target badges and awards, pending final evidence:
33
 
34
- - Best Agent: active-session workflow trace from search/photo candidate to
35
- deterministic rule/advisory result and constrained explanation.
36
  - OpenBMB: MiniCPM5-1B is the planned small-model path for constrained
37
  safety-result explanation.
38
  - Tiny Titan: the submitted model inventory stays at or below 4B parameters.
39
- - Off the Grid: cached name lookup, rule cards, and advisory overlays work
40
- without cloud APIs.
41
  - Off Brand: custom CoastWise Gradio interface.
42
  - Best Demo / Field Notes: public demo video, social post, and write-up links
43
  will be added before submission.
@@ -50,25 +51,35 @@ evidence.
50
 
51
  ## MVP Scope
52
 
53
- - Name search for curated San Francisco coast species, aliases, and grouped
54
- categories.
55
- - Conservative rule cards backed by official CDFW/CDPH source metadata.
56
- - CDPH advisory and annual mussel quarantine overlays.
57
- - Stale-data handling that defaults harvest guidance to verification.
58
- - Candidate-only photo support using approved reference-image categories.
59
- - A constrained generated explanation over deterministic safety results.
60
- - Session-only handling for photos, query text, generated explanations, and
61
- location text.
 
 
 
 
 
 
 
 
 
62
 
63
  ## Model Inventory
64
 
65
  | Model | Provider | Source URL | Role | Parameters | Runtime mode | Required for core safety | Award relevance |
66
  |---|---|---|---|---:|---|---|---|
67
- | MiniCPM5-1B | OpenBMB | https://huggingface.co/openbmb/MiniCPM5-1B | Constrained safety-result explanation | 1.0B | Disabled/mocked fallback in current local MVP | No | OpenBMB, Tiny Titan |
68
 
69
- Optional future path: MiniCPM-V 4.6 may assist or rerank photo candidates only
70
- inside the curated reference-image candidate set. No model fine-tuning is
71
- required for the MVP.
 
72
 
73
  All listed models are under the hackathon 32B cap. Tiny Titan remains eligible
74
  only while every submitted model stays at or below 4B total parameters.
@@ -76,19 +87,31 @@ only while every submitted model stays at or below 4B total parameters.
76
  ## Official Sources And Freshness
77
 
78
  Cached official source metadata is bundled for offline lookup. CoastWise treats
79
- cached rule or advisory data older than 30 days as stale and caps harvest-minded
80
- guidance at `verify_before_harvest` unless a stricter `do_not_harvest` rule
81
- already applies.
82
 
83
- Cache date for the current curated source set: 2026-06-13.
 
84
 
85
  | Source | Use | Cached URL |
86
  |---|---|---|
87
- | CDFW San Francisco Region Ocean Regulations | San Francisco coast species and rule-card baseline | https://wildlife.ca.gov/Fishing/Ocean/Regulations/Fishing-Map/San-Francisco |
88
- | CDFW Ocean Sport Fishing | General ocean sport fishing source for grouped cards | https://wildlife.ca.gov/Fishing/Ocean/Regulations |
 
 
 
 
 
 
 
 
89
  | CDPH Shellfish Advisories | Shellfish advisory overlay source | https://www.cdph.ca.gov/Programs/OPA/Pages/Shellfish-Advisories.aspx |
90
  | CDPH Annual Mussel Quarantine | Annual mussel quarantine overlay source | https://www.cdph.ca.gov/Programs/CEH/DRSEM/Pages/EMB/Shellfish/Annual-Mussel-Quarantine.aspx |
91
 
 
 
 
 
92
  ## Demo And Public Evidence
93
 
94
  - Demo video: pending, add a public link before final submission.
@@ -98,6 +121,19 @@ Cache date for the current curated source set: 2026-06-13.
98
  - Public Space history: pending, push meaningful Codex-attributed commits to the
99
  Hugging Face Space repository before claiming the OpenAI Codex award.
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  ## Local Run
102
 
103
  ```bash
 
17
 
18
  # CoastWise
19
 
20
+ CoastWise is an offline-first California ocean regulation and harvest-safety
21
+ Q&A MVP. It lets users ask practical questions such as "what's the min size of
22
+ lingcod?" or "can I eat mussels from Pacifica today?" and answers from cached
23
+ official CDFW/CDPH source snapshots with source links, retrieval time,
24
+ freshness status, uncertainty, and a next safe action.
25
 
26
  The MVP is a decision-support field guide. It does not certify species
27
  identification, legality, harvest authorization, or food safety.
 
32
 
33
  Target badges and awards, pending final evidence:
34
 
35
+ - Best Agent: source-backed question interpretation, evidence selection, and
36
+ constrained wording over deterministic CDFW/CDPH facts or snippets.
37
  - OpenBMB: MiniCPM5-1B is the planned small-model path for constrained
38
  safety-result explanation.
39
  - Tiny Titan: the submitted model inventory stays at or below 4B parameters.
40
+ - Off the Grid: bundled seed cache and last-good runtime cache can answer
41
+ supported questions without cloud APIs.
42
  - Off Brand: custom CoastWise Gradio interface.
43
  - Best Demo / Field Notes: public demo video, social post, and write-up links
44
  will be added before submission.
 
51
 
52
  ## MVP Scope
53
 
54
+ - Natural-language Q&A for California ocean sport fishing regulations and CDPH
55
+ advisory/quarantine topics.
56
+ - Statewide official source registry covering CDFW ocean regulations,
57
+ in-season changes, Northern, Mendocino, San Francisco, Central, Southern, SF
58
+ Bay, Groundfish Summary, Crabs, CDPH Shellfish Advisories, and CDPH Annual
59
+ Mussel Quarantine sources.
60
+ - Structured answers for the reviewed validation set, including lingcod size,
61
+ lingcod bag limit, Dungeness crab guidance, CDPH shellfish advisories, and
62
+ annual mussel quarantine.
63
+ - Cited official snippet fallback when cached text exists but structured
64
+ extraction is incomplete.
65
+ - Startup/manual refresh with last-good cache preservation and visible
66
+ updated/unchanged/failed source status.
67
+ - 24-hour stale-data handling that still answers from cache when offline while
68
+ making verification prominent.
69
+ - Session-only handling for questions, optional location text, generated
70
+ wording, and refresh UI state.
71
+ - Photo candidate support is not part of the core source-backed MVP flow.
72
 
73
  ## Model Inventory
74
 
75
  | Model | Provider | Source URL | Role | Parameters | Runtime mode | Required for core safety | Award relevance |
76
  |---|---|---|---|---:|---|---|---|
77
+ | MiniCPM5-1B | OpenBMB | https://huggingface.co/openbmb/MiniCPM5-1B | Optional constrained wording over deterministic answers | 1.0B | Disabled/mocked fallback in current local MVP | No | OpenBMB, Tiny Titan |
78
 
79
+ No model fine-tuning is required for the MVP. Model output, when enabled, may
80
+ only help with interpretation or wording and cannot create or override
81
+ regulation facts, advisory facts, source links, dates, freshness, status, or
82
+ next safe action.
83
 
84
  All listed models are under the hackathon 32B cap. Tiny Titan remains eligible
85
  only while every submitted model stays at or below 4B total parameters.
 
87
  ## Official Sources And Freshness
88
 
89
  Cached official source metadata is bundled for offline lookup. CoastWise treats
90
+ cached official source snapshots older than 24 hours as stale, but stale cache
91
+ may still be used when offline with a visible verification warning.
 
92
 
93
+ Seed cache retrieval time for the current reviewed source set:
94
+ 2026-06-14T12:00:00+00:00.
95
 
96
  | Source | Use | Cached URL |
97
  |---|---|---|
98
+ | CDFW Ocean Sport Fishing | Statewide ocean sport fishing source registry entry | https://wildlife.ca.gov/Fishing/Ocean/Regulations |
99
+ | CDFW Ocean Sport Fishing In-Season Regulation Changes | Statewide in-season changes source registry entry | https://wildlife.ca.gov/Fishing/Ocean/Regulations/Inseason |
100
+ | CDFW Northern Ocean Region | Northern California ocean region source registry entry | https://wildlife.ca.gov/Fishing/Ocean/Regulations/Fishing-Map/Northern |
101
+ | CDFW Mendocino Ocean Region | Mendocino ocean region source registry entry | https://wildlife.ca.gov/Fishing/Ocean/Regulations/Fishing-Map/Mendocino |
102
+ | CDFW San Francisco Ocean Region | San Francisco ocean region source registry entry | https://wildlife.ca.gov/Fishing/Ocean/Regulations/Fishing-Map/San-Francisco |
103
+ | CDFW Central Ocean Region | Central California ocean region source registry entry | https://wildlife.ca.gov/Fishing/Ocean/Regulations/Fishing-Map/Central |
104
+ | CDFW Southern Ocean Region | Southern California ocean region source registry entry | https://wildlife.ca.gov/Fishing/Ocean/Regulations/Fishing-Map/Southern |
105
+ | CDFW San Francisco Bay Region | San Francisco Bay region source registry entry | https://wildlife.ca.gov/Fishing/Ocean/Regulations/Fishing-Map/SF-Bay |
106
+ | CDFW Groundfish Summary | Lingcod validation-set size and bag facts | https://wildlife.ca.gov/Fishing/Ocean/Regulations/Groundfish-Summary |
107
+ | CDFW Crabs | Dungeness crab validation-set guidance and crab source snippets | https://wildlife.ca.gov/Conservation/Marine/Invertebrates/Crabs |
108
  | CDPH Shellfish Advisories | Shellfish advisory overlay source | https://www.cdph.ca.gov/Programs/OPA/Pages/Shellfish-Advisories.aspx |
109
  | CDPH Annual Mussel Quarantine | Annual mussel quarantine overlay source | https://www.cdph.ca.gov/Programs/CEH/DRSEM/Pages/EMB/Shellfish/Annual-Mussel-Quarantine.aspx |
110
 
111
+ Only public official source snapshots and refresh metadata are written to the
112
+ runtime cache. Questions and optional beach/coastal-area text are not saved by
113
+ CoastWise.
114
+
115
  ## Demo And Public Evidence
116
 
117
  - Demo video: pending, add a public link before final submission.
 
121
  - Public Space history: pending, push meaningful Codex-attributed commits to the
122
  Hugging Face Space repository before claiming the OpenAI Codex award.
123
 
124
+ ## Release Checklist
125
+
126
+ - README frontmatter includes Build Small hackathon tags.
127
+ - Public Hugging Face Space URL is available and listed above.
128
+ - Demo video and social post links are listed above.
129
+ - Public Space history contains judge-visible Codex commit evidence for the
130
+ OpenAI Codex prize.
131
+ - GitHub may remain private; do not rely on private GitHub commits as the only
132
+ Codex evidence.
133
+ - Source freshness policy, seed/offline behavior, safety limits, privacy
134
+ limits, and model inventory are documented in this README.
135
+ - Full pytest suite passes before final Space push.
136
+
137
  ## Local Run
138
 
139
  ```bash
app.py CHANGED
@@ -2,6 +2,8 @@
2
 
3
  from __future__ import annotations
4
 
 
 
5
  import gradio as gr
6
 
7
  from coastwise.ai import (
@@ -12,23 +14,41 @@ from coastwise.ai import (
12
  )
13
  from coastwise.data import get_rule_card
14
  from coastwise.photo import candidate_from_photo, photo_result_to_dict
 
15
  from coastwise.rules import compose_safety_result, default_context, result_to_dict
16
  from coastwise.schemas import CoastalRegion, UserIntent
17
  from coastwise.search import search_species
 
 
 
18
  from coastwise.submission import default_model_inventory
19
 
20
 
21
  APP_TITLE = "CoastWise"
22
 
23
- INTRO = """
24
- Offline California coastal species lookup.
 
 
 
 
 
25
 
26
- SF coast harvest-safety guidance.
 
27
  """
28
 
29
  APP_CSS = """
 
 
 
 
 
 
30
  .gradio-container {
 
31
  max-width: 100vw !important;
 
32
  overflow-x: hidden !important;
33
  }
34
 
@@ -41,6 +61,7 @@ APP_CSS = """
41
  .gradio-container .block,
42
  .gradio-container .wrap,
43
  .gradio-container fieldset {
 
44
  max-width: 100% !important;
45
  min-width: 0 !important;
46
  }
@@ -51,6 +72,14 @@ APP_CSS = """
51
  .gradio-container h2,
52
  .gradio-container h3 {
53
  overflow-wrap: anywhere;
 
 
 
 
 
 
 
 
54
  }
55
 
56
  @media (max-width: 640px) {
@@ -58,6 +87,11 @@ APP_CSS = """
58
  padding-left: 16px !important;
59
  padding-right: 16px !important;
60
  }
 
 
 
 
 
61
  }
62
  """
63
 
@@ -71,6 +105,15 @@ Does not authorize harvest or food safety.
71
  Verify CDFW/CDPH before harvest.
72
  """
73
 
 
 
 
 
 
 
 
 
 
74
  INTENT_LABELS = {
75
  "Just curious": UserIntent.JUST_CURIOUS,
76
  "Catch and release": UserIntent.CATCH_AND_RELEASE,
@@ -84,6 +127,97 @@ REGION_LABELS = {
84
  }
85
 
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  def lookup_species(
88
  query: str,
89
  intent_label: str,
@@ -195,30 +329,19 @@ def build_app() -> gr.Blocks:
195
  gr.Markdown(f"# {APP_TITLE}")
196
  gr.Markdown(INTRO)
197
  gr.Markdown(LIMITS)
198
- query = gr.Textbox(label="Species or category", placeholder="Try mussel, lingcod, crab")
199
- intent = gr.Radio(
200
- ["Just curious", "Catch and release", "Thinking of harvest"],
201
- value="Just curious",
202
- label="Intent",
203
- )
204
- region = gr.Dropdown(
205
- ["Point Arena to Pigeon Point", "San Francisco Bay", "Unknown"],
206
- value="Point Arena to Pigeon Point",
207
- label="Region",
208
- )
209
- county_or_beach = gr.Textbox(label="Optional county or beach", placeholder="Optional")
210
- lookup = gr.Button("Look up")
211
- output = gr.Markdown("Rule-card results will appear here.")
212
  lookup.click(
213
- render_lookup_result,
214
- inputs=[query, intent, region, county_or_beach],
215
  outputs=output,
216
  )
217
- gr.Markdown("## Photo candidate support")
218
- photo = gr.Image(label="Optional photo", type="pil")
219
- photo_button = gr.Button("Compare photo to curated references")
220
- photo_output = gr.Markdown("Photo candidates will appear here.")
221
- photo_button.click(render_photo_candidate, inputs=photo, outputs=photo_output)
222
  return demo
223
 
224
 
 
2
 
3
  from __future__ import annotations
4
 
5
+ from datetime import datetime, timezone
6
+
7
  import gradio as gr
8
 
9
  from coastwise.ai import (
 
14
  )
15
  from coastwise.data import get_rule_card
16
  from coastwise.photo import candidate_from_photo, photo_result_to_dict
17
+ from coastwise.qna import answer_question, answer_to_dict
18
  from coastwise.rules import compose_safety_result, default_context, result_to_dict
19
  from coastwise.schemas import CoastalRegion, UserIntent
20
  from coastwise.search import search_species
21
+ from coastwise.source_cache import DEFAULT_SEED_CACHE_DIR, load_seed_cache
22
+ from coastwise.source_refresh import refresh_official_sources
23
+ from coastwise.source_registry import load_official_sources
24
  from coastwise.submission import default_model_inventory
25
 
26
 
27
  APP_TITLE = "CoastWise"
28
 
29
+ SOURCE_BACKED_CONTROL_LABELS = (
30
+ "Question",
31
+ "Beach or coastal area",
32
+ "Ask",
33
+ "Refresh official sources",
34
+ "Source status",
35
+ )
36
 
37
+ INTRO = """
38
+ California CDFW/CDPH Q&A.
39
  """
40
 
41
  APP_CSS = """
42
+ html,
43
+ body {
44
+ max-width: 100% !important;
45
+ overflow-x: hidden !important;
46
+ }
47
+
48
  .gradio-container {
49
+ width: 100% !important;
50
  max-width: 100vw !important;
51
+ min-width: 0 !important;
52
  overflow-x: hidden !important;
53
  }
54
 
 
61
  .gradio-container .block,
62
  .gradio-container .wrap,
63
  .gradio-container fieldset {
64
+ width: 100% !important;
65
  max-width: 100% !important;
66
  min-width: 0 !important;
67
  }
 
72
  .gradio-container h2,
73
  .gradio-container h3 {
74
  overflow-wrap: anywhere;
75
+ word-break: break-word;
76
+ white-space: normal !important;
77
+ }
78
+
79
+ .gradio-container input,
80
+ .gradio-container textarea,
81
+ .gradio-container button {
82
+ max-width: 100% !important;
83
  }
84
 
85
  @media (max-width: 640px) {
 
87
  padding-left: 16px !important;
88
  padding-right: 16px !important;
89
  }
90
+
91
+ .gradio-container h1 {
92
+ font-size: 28px !important;
93
+ line-height: 1.2 !important;
94
+ }
95
  }
96
  """
97
 
 
105
  Verify CDFW/CDPH before harvest.
106
  """
107
 
108
+ SOURCE_BACKED_PRODUCT_LIMITS = (
109
+ "CoastWise does not authorize harvest or act as legal/food-safety/species-ID "
110
+ "authority. Verify official CDFW/CDPH guidance before harvest or consumption."
111
+ )
112
+
113
+ SESSION_ONLY_NOTICE = (
114
+ "Session-only: questions and optional locations are not saved."
115
+ )
116
+
117
  INTENT_LABELS = {
118
  "Just curious": UserIntent.JUST_CURIOUS,
119
  "Catch and release": UserIntent.CATCH_AND_RELEASE,
 
127
  }
128
 
129
 
130
+ def _active_cache():
131
+ return load_seed_cache(DEFAULT_SEED_CACHE_DIR)
132
+
133
+
134
+ def ask_source_backed_question(question: str, beach_or_coastal_area: str = "") -> dict[str, object]:
135
+ answer = answer_question(
136
+ question,
137
+ beach_or_coastal_area,
138
+ _active_cache(),
139
+ datetime.now(timezone.utc),
140
+ )
141
+ return answer_to_dict(answer)
142
+
143
+
144
+ def _render_source_backed_answer_payload(result: dict[str, object]) -> str:
145
+ source_lines = "\n".join(
146
+ f"- [{title}]({url})"
147
+ for title, url in zip(result["source_titles"], result["source_links"], strict=False)
148
+ ) or "- No source-backed evidence available."
149
+ snippet_lines = "\n".join(f"> {snippet}" for snippet in result["supporting_snippets"]) or "- No snippet shown."
150
+ uncertainty = "\n".join(f"- {note}" for note in result["uncertainty_notes"]) or "- None."
151
+ location_warning = result["location_warning"] or "Location recognized or not needed for this general answer."
152
+ retrieved = result["retrieved_at"] or "No cached retrieval time available"
153
+ return f"""
154
+ ## Direct answer
155
+ {result["direct_answer"]}
156
+
157
+ **Status:** `{result["status"]}`
158
+ **Evidence type:** `{result["evidence_type"]}`
159
+ **Freshness:** `{result["freshness_status"]}`
160
+ **Retrieved:** {retrieved}
161
+
162
+ ## Source evidence
163
+ {source_lines}
164
+
165
+ ## Location context
166
+ {result["matched_place"] or "General California ocean"}
167
+ {location_warning}
168
+
169
+ ## Supporting snippet
170
+ {snippet_lines}
171
+
172
+ ## Uncertainty
173
+ {uncertainty}
174
+
175
+ ## Next safe action
176
+ {result["next_safe_action"]}
177
+
178
+ ## Product limits
179
+ {SOURCE_BACKED_PRODUCT_LIMITS}
180
+
181
+ {SESSION_ONLY_NOTICE}
182
+ """
183
+
184
+
185
+ def render_source_backed_answer_from_cache(
186
+ question: str,
187
+ beach_or_coastal_area: str,
188
+ cache,
189
+ now: datetime,
190
+ ) -> str:
191
+ answer = answer_question(question, beach_or_coastal_area, cache, now)
192
+ return _render_source_backed_answer_payload(answer_to_dict(answer))
193
+
194
+
195
+ def render_source_backed_answer(question: str, beach_or_coastal_area: str = "") -> str:
196
+ result = ask_source_backed_question(question, beach_or_coastal_area)
197
+ return _render_source_backed_answer_payload(result)
198
+
199
+
200
+ def refresh_official_sources_status(fetcher=None) -> dict[str, object]:
201
+ result, _cache = refresh_official_sources(
202
+ load_official_sources(),
203
+ _active_cache(),
204
+ datetime.now(timezone.utc),
205
+ fetcher,
206
+ )
207
+ return {
208
+ "message": result.user_message,
209
+ "updated": result.updated_source_ids,
210
+ "unchanged": result.unchanged_source_ids,
211
+ "failed": result.failed_source_ids,
212
+ "cache_preserved": result.cache_preserved_source_ids,
213
+ }
214
+
215
+
216
+ def render_refresh_status() -> str:
217
+ status = refresh_official_sources_status()
218
+ return f"**Refresh official sources:** {status['message']}"
219
+
220
+
221
  def lookup_species(
222
  query: str,
223
  intent_label: str,
 
329
  gr.Markdown(f"# {APP_TITLE}")
330
  gr.Markdown(INTRO)
331
  gr.Markdown(LIMITS)
332
+ gr.Markdown(SESSION_ONLY_NOTICE)
333
+ gr.Markdown("**Source status:** seed cache loaded; refresh available.")
334
+ query = gr.Textbox(label="Question", placeholder="what's the min size of lingcod?")
335
+ county_or_beach = gr.Textbox(label="Beach or coastal area", placeholder="Optional, e.g. Pacifica or Monterey")
336
+ lookup = gr.Button("Ask")
337
+ refresh = gr.Button("Refresh official sources")
338
+ output = gr.Markdown("Source-backed answers will appear here.")
 
 
 
 
 
 
 
339
  lookup.click(
340
+ render_source_backed_answer,
341
+ inputs=[query, county_or_beach],
342
  outputs=output,
343
  )
344
+ refresh.click(render_refresh_status, outputs=output)
 
 
 
 
345
  return demo
346
 
347
 
coastwise/advisories.py CHANGED
@@ -5,11 +5,25 @@ from __future__ import annotations
5
  from datetime import date
6
 
7
  from coastwise.data import CDPH_QUARANTINE_URL, CDPH_SHELLFISH_URL, SOURCE_CACHE_DATE
8
- from coastwise.schemas import Advisory, CoastalRegion, HarvestStatus, QueryContext, RuleCard
 
9
 
10
 
11
  SHELLFISH_CARD_KEYS = {"mussels", "clams"}
12
  SHELLFISH_CATEGORIES = {"shellfish"}
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  ANNUAL_MUSSEL_QUARANTINE_WARNING = (
15
  "Annual CDPH mussel quarantine runs May 1 through October 31; do not harvest "
@@ -87,3 +101,38 @@ def matching_advisories(rule_card: RuleCard, context: QueryContext) -> list[Advi
87
 
88
  return matches
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  from datetime import date
6
 
7
  from coastwise.data import CDPH_QUARANTINE_URL, CDPH_SHELLFISH_URL, SOURCE_CACHE_DATE
8
+ from coastwise.schemas import Advisory, AdvisoryFact, CoastalRegion, HarvestStatus, QueryContext, RuleCard
9
+ from coastwise.source_cache import SourceCache
10
 
11
 
12
  SHELLFISH_CARD_KEYS = {"mussels", "clams"}
13
  SHELLFISH_CATEGORIES = {"shellfish"}
14
+ SOURCE_BACKED_SHELLFISH_TERMS = {
15
+ "bivalve",
16
+ "bivalves",
17
+ "clam",
18
+ "clams",
19
+ "crab",
20
+ "crabs",
21
+ "mussel",
22
+ "mussels",
23
+ "oyster",
24
+ "oysters",
25
+ "shellfish",
26
+ }
27
 
28
  ANNUAL_MUSSEL_QUARANTINE_WARNING = (
29
  "Annual CDPH mussel quarantine runs May 1 through October 31; do not harvest "
 
101
 
102
  return matches
103
 
104
+
105
+ def _normalize_source_backed_term(value: str | None) -> str:
106
+ return " ".join((value or "").replace("_", " ").casefold().split())
107
+
108
+
109
+ def _advisory_fact_terms(fact: AdvisoryFact) -> set[str]:
110
+ affected = _normalize_source_backed_term(fact.affected_species_or_category)
111
+ terms = {affected}
112
+ if affected == "mussels":
113
+ terms.update({"mussel", "shellfish"})
114
+ if affected == "shellfish":
115
+ terms.update(SOURCE_BACKED_SHELLFISH_TERMS)
116
+ return {term for term in terms if term}
117
+
118
+
119
+ def source_backed_advisory_facts(cache: SourceCache, species_or_category: str | None) -> list[AdvisoryFact]:
120
+ """Return cached CDPH advisory facts matching a shellfish species/category."""
121
+
122
+ normalized = _normalize_source_backed_term(species_or_category)
123
+ if not normalized:
124
+ return []
125
+
126
+ requested_terms = {normalized}
127
+ if normalized in SOURCE_BACKED_SHELLFISH_TERMS:
128
+ requested_terms.add("shellfish")
129
+ if normalized in {"mussel", "mussels"}:
130
+ requested_terms.update({"mussel", "mussels"})
131
+
132
+ matches = [
133
+ fact
134
+ for fact in cache.advisory_facts
135
+ if requested_terms & _advisory_fact_terms(fact)
136
+ or any(term in fact.supporting_snippet.casefold() for term in requested_terms)
137
+ ]
138
+ return sorted(matches, key=lambda fact: (fact.effective_start is None, fact.id))
coastwise/submission.py CHANGED
@@ -22,7 +22,7 @@ def default_model_inventory() -> ModelInventory:
22
  key="minicpm5_1b",
23
  name="MiniCPM5-1B",
24
  provider="OpenBMB",
25
- source_url="https://huggingface.co/openbmb",
26
  total_parameters_b=1.0,
27
  roles=(ModelRole.SAFETY_EXPLANATION,),
28
  runtime_mode=RuntimeMode.DISABLED_OR_MOCKED,
@@ -70,6 +70,14 @@ def validate_submission_package(package: SubmissionPackage) -> list[ValidationIs
70
  if not value:
71
  issues.append(ValidationIssue(field, "error", f"{field} is required."))
72
 
 
 
 
 
 
 
 
 
73
  if not package.readme_has_frontmatter_tags:
74
  issues.append(ValidationIssue("readme_has_frontmatter_tags", "error", "README frontmatter tags are required."))
75
  if not package.safety_limitations_documented:
@@ -84,6 +92,13 @@ def validate_submission_package(package: SubmissionPackage) -> list[ValidationIs
84
  "OpenAI Codex claim requires public Hugging Face Space commit evidence.",
85
  )
86
  )
 
 
 
 
 
 
 
 
87
  issues.extend(validate_model_inventory(package.model_inventory))
88
  return issues
89
-
 
22
  key="minicpm5_1b",
23
  name="MiniCPM5-1B",
24
  provider="OpenBMB",
25
+ source_url="https://huggingface.co/openbmb/MiniCPM5-1B",
26
  total_parameters_b=1.0,
27
  roles=(ModelRole.SAFETY_EXPLANATION,),
28
  runtime_mode=RuntimeMode.DISABLED_OR_MOCKED,
 
70
  if not value:
71
  issues.append(ValidationIssue(field, "error", f"{field} is required."))
72
 
73
+ if package.space_url and "huggingface.co/spaces/" not in package.space_url:
74
+ issues.append(
75
+ ValidationIssue(
76
+ "space_url",
77
+ "error",
78
+ "Public Space URL must point to the Hugging Face Spaces submission.",
79
+ )
80
+ )
81
  if not package.readme_has_frontmatter_tags:
82
  issues.append(ValidationIssue("readme_has_frontmatter_tags", "error", "README frontmatter tags are required."))
83
  if not package.safety_limitations_documented:
 
92
  "OpenAI Codex claim requires public Hugging Face Space commit evidence.",
93
  )
94
  )
95
+ if package.codex_evidence and "huggingface.co/spaces/" not in package.codex_evidence:
96
+ issues.append(
97
+ ValidationIssue(
98
+ "codex_evidence",
99
+ "error",
100
+ "Codex evidence should be judge-visible in public Hugging Face Space history.",
101
+ )
102
+ )
103
  issues.extend(validate_model_inventory(package.model_inventory))
104
  return issues
 
tests/test_advisories.py CHANGED
@@ -3,9 +3,11 @@
3
  from datetime import date
4
 
5
  from coastwise.advisories import annual_mussel_quarantine_applies, matching_advisories
 
6
  from coastwise.data import get_rule_card
7
  from coastwise.rules import default_context
8
  from coastwise.schemas import CoastalRegion, HarvestStatus, UserIntent
 
9
 
10
 
11
  def test_annual_mussel_quarantine_applies_may_through_october():
@@ -32,3 +34,12 @@ def test_matching_cdph_advisories_include_region_level_shellfish_warning():
32
  and "CDPH" in advisory.source_name
33
  for advisory in advisories
34
  )
 
 
 
 
 
 
 
 
 
 
3
  from datetime import date
4
 
5
  from coastwise.advisories import annual_mussel_quarantine_applies, matching_advisories
6
+ from coastwise.advisories import source_backed_advisory_facts
7
  from coastwise.data import get_rule_card
8
  from coastwise.rules import default_context
9
  from coastwise.schemas import CoastalRegion, HarvestStatus, UserIntent
10
+ from coastwise.source_cache import DEFAULT_SEED_CACHE_DIR, load_seed_cache
11
 
12
 
13
  def test_annual_mussel_quarantine_applies_may_through_october():
 
34
  and "CDPH" in advisory.source_name
35
  for advisory in advisories
36
  )
37
+
38
+
39
+ def test_source_backed_cdph_advisory_facts_include_quarantine_and_cooking_warning():
40
+ facts = source_backed_advisory_facts(load_seed_cache(DEFAULT_SEED_CACHE_DIR), "mussels")
41
+
42
+ assert facts
43
+ assert any("May 1" in fact.advisory_status for fact in facts)
44
+ assert any("Cooking does not reliably destroy" in fact.warning for fact in facts)
45
+ assert all("cdph.ca.gov" in fact.source_url for fact in facts)
tests/test_app.py CHANGED
@@ -1,9 +1,14 @@
1
  """Gradio app integration tests."""
2
 
 
 
 
 
3
  import gradio as gr
4
  from PIL import Image
5
 
6
  import app
 
7
 
8
 
9
  def test_app_import_exposes_coastwise_identity_without_launching():
@@ -18,6 +23,118 @@ def test_build_app_returns_blocks_with_lookup_shell():
18
  assert demo.title == "CoastWise"
19
 
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  def test_lookup_species_returns_source_backed_result_dict():
22
  result = app.lookup_species("lingcod", "Just curious", "Point Arena to Pigeon Point", "")
23
 
 
1
  """Gradio app integration tests."""
2
 
3
+ import time
4
+ from dataclasses import replace
5
+ from datetime import datetime, timedelta, timezone
6
+
7
  import gradio as gr
8
  from PIL import Image
9
 
10
  import app
11
+ from coastwise.source_cache import DEFAULT_SEED_CACHE_DIR, SourceCache, load_seed_cache
12
 
13
 
14
  def test_app_import_exposes_coastwise_identity_without_launching():
 
23
  assert demo.title == "CoastWise"
24
 
25
 
26
+ def test_source_backed_app_shell_exposes_primary_controls_without_legacy_inputs():
27
+ assert "Question" in app.SOURCE_BACKED_CONTROL_LABELS
28
+ assert "Beach or coastal area" in app.SOURCE_BACKED_CONTROL_LABELS
29
+ assert "Refresh official sources" in app.SOURCE_BACKED_CONTROL_LABELS
30
+ assert "Intent" not in app.SOURCE_BACKED_CONTROL_LABELS
31
+ assert "Region" not in app.SOURCE_BACKED_CONTROL_LABELS
32
+ assert "Photo candidate" not in app.SOURCE_BACKED_CONTROL_LABELS
33
+
34
+
35
+ def test_source_backed_question_journey_renders_evidence_and_next_action():
36
+ result = app.ask_source_backed_question("what's the min size of lingcod?", "")
37
+ markdown = app.render_source_backed_answer("what's the min size of lingcod?", "")
38
+
39
+ assert result["direct_answer"]
40
+ assert result["source_links"]
41
+ assert result["freshness_status"] == "current"
42
+ assert "22" in markdown
43
+ assert "Source evidence" in markdown
44
+ assert "Retrieved" in markdown
45
+ assert "Next safe action" in markdown
46
+
47
+
48
+ def test_manual_refresh_status_uses_nontechnical_source_group_language():
49
+ status = app.refresh_official_sources_status(fetcher=lambda _source: "unchanged text")
50
+
51
+ assert "CDFW" in status["message"]
52
+ assert "CDPH" in status["message"]
53
+ assert "checked" in status["message"].lower()
54
+ assert "traceback" not in status["message"].lower()
55
+
56
+
57
+ def test_app_renders_stale_and_no_cache_source_backed_states():
58
+ now = datetime(2026, 6, 14, 13, tzinfo=timezone.utc)
59
+ seed = load_seed_cache(DEFAULT_SEED_CACHE_DIR)
60
+ stale = SourceCache(
61
+ sources=seed.sources,
62
+ snapshots=tuple(replace(snapshot, retrieved_at=now - timedelta(hours=25)) for snapshot in seed.snapshots),
63
+ chunks=seed.chunks,
64
+ facts=seed.facts,
65
+ advisory_facts=seed.advisory_facts,
66
+ )
67
+
68
+ stale_markdown = app.render_source_backed_answer_from_cache(
69
+ "what's the min size of lingcod?",
70
+ "",
71
+ stale,
72
+ now,
73
+ )
74
+ no_cache_markdown = app.render_source_backed_answer_from_cache(
75
+ "what's the min size of lingcod?",
76
+ "",
77
+ SourceCache((), (), (), ()),
78
+ now,
79
+ )
80
+
81
+ assert "stale" in stale_markdown.lower()
82
+ assert "refresh official" in no_cache_markdown.lower()
83
+
84
+
85
+ def test_app_renders_cdph_warning_and_verification_for_mussels():
86
+ markdown = app.render_source_backed_answer("can I eat mussels from Pacifica today?", "")
87
+ lowered = markdown.lower()
88
+
89
+ assert "cdph" in lowered
90
+ assert "cooking does not reliably" in lowered
91
+ assert "verify" in lowered
92
+ assert "safe to eat" not in lowered
93
+
94
+
95
+ def test_app_renders_source_evidence_limits_and_partial_unsupported_states():
96
+ now = datetime(2026, 6, 14, 13, tzinfo=timezone.utc)
97
+ partial = app.render_source_backed_answer("what should I check for ocean rules near Monterey?", "Monterey")
98
+ unsupported = app.render_source_backed_answer("what is the trout limit in Lake Tahoe?", "")
99
+ no_cache = app.render_source_backed_answer_from_cache(
100
+ "what's the min size of lingcod?",
101
+ "",
102
+ SourceCache((), (), (), ()),
103
+ now,
104
+ )
105
+ combined = "\n".join((partial, unsupported, no_cache)).casefold()
106
+
107
+ assert "source evidence" in partial.casefold()
108
+ assert "uncertainty" in partial.casefold()
109
+ assert "partial" in partial.casefold()
110
+ assert "outside this mvp" in unsupported.casefold()
111
+ assert "refresh official" in no_cache.casefold()
112
+ assert "does not authorize harvest" in combined
113
+ assert "session-only" in combined
114
+ assert "source-backed evidence" in combined
115
+
116
+
117
+ def test_cached_answer_app_build_and_refresh_feedback_fit_latency_budgets():
118
+ start = time.perf_counter()
119
+ markdown = app.render_source_backed_answer("what's the min size of lingcod?", "")
120
+ answer_seconds = time.perf_counter() - start
121
+
122
+ start = time.perf_counter()
123
+ demo = app.build_app()
124
+ build_seconds = time.perf_counter() - start
125
+
126
+ start = time.perf_counter()
127
+ status = app.refresh_official_sources_status(fetcher=lambda _source: "unchanged official source text")
128
+ refresh_seconds = time.perf_counter() - start
129
+
130
+ assert "22" in markdown
131
+ assert isinstance(demo, gr.Blocks)
132
+ assert "checked" in status["message"].casefold()
133
+ assert answer_seconds < 3
134
+ assert build_seconds < 3
135
+ assert refresh_seconds < 3
136
+
137
+
138
  def test_lookup_species_returns_source_backed_result_dict():
139
  result = app.lookup_species("lingcod", "Just curious", "Point Arena to Pigeon Point", "")
140
 
tests/test_privacy.py CHANGED
@@ -38,3 +38,22 @@ def test_rendered_ui_copy_does_not_offer_save_export_or_history_actions():
38
  assert "save" not in markdown
39
  assert "export" not in markdown
40
  assert "history" not in markdown
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  assert "save" not in markdown
39
  assert "export" not in markdown
40
  assert "history" not in markdown
41
+
42
+
43
+ def test_source_backed_question_location_and_wording_are_session_only():
44
+ result = app.ask_source_backed_question("what's the min size of lingcod?", "Pacifica")
45
+ markdown = app.render_source_backed_answer("what's the min size of lingcod?", "Pacifica").casefold()
46
+
47
+ assert result["matched_place"] == "Pacifica"
48
+ assert "session-only" in markdown
49
+ assert "questions and optional locations are not saved" in markdown
50
+ assert not hasattr(app, "SOURCE_BACKED_QUESTION_HISTORY")
51
+ assert not hasattr(app, "SOURCE_BACKED_LOCATION_HISTORY")
52
+ assert not hasattr(app, "GENERATED_ANSWER_HISTORY")
53
+
54
+
55
+ def test_source_backed_controls_do_not_offer_account_save_export_history_or_photo_core():
56
+ labels = " ".join(app.SOURCE_BACKED_CONTROL_LABELS).casefold()
57
+
58
+ for forbidden in ("account", "save", "export", "history", "photo", "camera"):
59
+ assert forbidden not in labels
tests/test_safety_copy.py CHANGED
@@ -6,8 +6,11 @@ import app
6
  FORBIDDEN_PHRASES = (
7
  "safe to eat",
8
  "definitely legal",
 
9
  "authorized to harvest",
 
10
  "confirmed identification",
 
11
  )
12
 
13
 
@@ -32,3 +35,16 @@ def test_photo_candidate_copy_never_claims_confirmed_identification():
32
  assert "harvest guidance" not in lowered
33
  for phrase in FORBIDDEN_PHRASES:
34
  assert phrase not in lowered
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  FORBIDDEN_PHRASES = (
7
  "safe to eat",
8
  "definitely legal",
9
+ "legal to harvest",
10
  "authorized to harvest",
11
+ "certifies",
12
  "confirmed identification",
13
+ "you may harvest",
14
  )
15
 
16
 
 
35
  assert "harvest guidance" not in lowered
36
  for phrase in FORBIDDEN_PHRASES:
37
  assert phrase not in lowered
38
+
39
+
40
+ def test_source_backed_answers_show_limits_without_forbidden_claims():
41
+ markdown = app.render_source_backed_answer("can I eat mussels from Pacifica today?", "")
42
+ lowered = markdown.casefold()
43
+
44
+ assert "does not authorize harvest" in lowered
45
+ assert "food-safety" in lowered
46
+ assert "session-only" in lowered
47
+ assert "source evidence" in lowered
48
+ assert "retrieved" in lowered
49
+ for phrase in FORBIDDEN_PHRASES:
50
+ assert phrase not in lowered
tests/test_submission.py CHANGED
@@ -1,6 +1,10 @@
1
  """Submission-readiness tests."""
2
 
 
 
3
  from coastwise.schemas import AwardTag, SubmissionPackage
 
 
4
  from coastwise.submission import (
5
  default_model_inventory,
6
  validate_model_inventory,
@@ -37,3 +41,29 @@ def test_submission_package_validator_flags_missing_public_evidence():
37
  fields = {issue.field for issue in issues}
38
 
39
  assert {"space_url", "demo_video_url", "social_post_url", "codex_evidence"} <= fields
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """Submission-readiness tests."""
2
 
3
+ import json
4
+
5
  from coastwise.schemas import AwardTag, SubmissionPackage
6
+ from coastwise.source_cache import DEFAULT_SEED_CACHE_DIR, DEFAULT_SOURCE_CACHE_ROOT, load_seed_cache
7
+ from coastwise.source_registry import load_official_sources, statewide_source_coverage
8
  from coastwise.submission import (
9
  default_model_inventory,
10
  validate_model_inventory,
 
41
  fields = {issue.field for issue in issues}
42
 
43
  assert {"space_url", "demo_video_url", "social_post_url", "codex_evidence"} <= fields
44
+
45
+
46
+ def test_readme_documents_statewide_source_cache_policy_and_validation_limits():
47
+ readme = open("README.md", encoding="utf-8").read().casefold()
48
+ coverage = statewide_source_coverage()
49
+
50
+ assert "24 hours" in readme
51
+ assert "statewide official source registry" in readme
52
+ assert "structured answers for the reviewed validation set" in readme
53
+ assert "snippet fallback" in readme
54
+ assert "questions and optional beach/coastal-area text are not saved" in readme
55
+ for source in load_official_sources():
56
+ if source.id in coverage.required_source_ids:
57
+ assert source.url.casefold() in readme
58
+
59
+
60
+ def test_seed_manifest_documents_statewide_coverage_and_24_hour_stale_policy():
61
+ manifest = json.loads((DEFAULT_SOURCE_CACHE_ROOT / "seed-manifest.json").read_text(encoding="utf-8"))
62
+ cache = load_seed_cache(DEFAULT_SEED_CACHE_DIR)
63
+ source_ids = set(cache.snapshots_by_source_id)
64
+
65
+ assert manifest["coverage_id"] == statewide_source_coverage().id
66
+ assert manifest["stale_after_hours"] == 24
67
+ assert set(statewide_source_coverage().required_source_ids) <= source_ids
68
+ assert {"lingcod_min_size", "dungeness_crab_bag_limit"} <= set(cache.facts_by_id)
69
+ assert {"cdph_annual_mussel_quarantine", "cdph_shellfish_verify"} <= set(cache.advisory_facts_by_id)