"""One-shot seed expansion: append source-verified structured facts. Every value here was verified against the cited official CDFW/CCR page during the 2026-06-15 research pass. species_or_category uses the curated RULE_CARD key so both the model and deterministic interpreters resolve to it. """ import json from pathlib import Path FACTS_PATH = Path("data/source-cache/seed/facts.json") GROUNDFISH = "cdfw_groundfish_summary" GROUNDFISH_URL = "https://wildlife.ca.gov/Fishing/Ocean/Regulations/Groundfish-Summary" SF = "cdfw_region_san_francisco" SF_URL = "https://wildlife.ca.gov/Fishing/Ocean/Regulations/Fishing-Map/San-Francisco" OCEAN = "cdfw_ocean_regulations" GENERAL_URL = "https://wildlife.ca.gov/Fishing/Ocean/Regulations/Sport-Fishing/General-Ocean-Fishing-Regs" INVERT_URL = "https://wildlife.ca.gov/Fishing/Ocean/Regulations/Sport-Fishing/Invertebrate-Fishing-Regs" ABALONE_URL = "https://wildlife.ca.gov/Conservation/Marine/Invertebrates/Abalone" SALMON_URL = "https://wildlife.ca.gov/Fishing/Ocean/Regulations/Salmon" PHALIBUT_URL = "https://wildlife.ca.gov/Conservation/Marine/Pacific-Halibut" CRABS = "cdfw_crabs" CRABS_URL = "https://wildlife.ca.gov/Conservation/Marine/Invertebrates/Crabs" NOW = "2026-06-15T12:00:00+00:00" RCG_SEASON = ( "open from April 1 through December 31 at all depths, and closed January 1 " "through March 31. These dates apply to the Northern, Mendocino, San Francisco, " "and Central management areas (the Southern area differs). Shore-based anglers " "and spear divers are exempt from seasons and depth limits. Verify current " "in-season depth changes." ) RCG_SEASON_QUOTE = ( "Jan 1 - Mar 31: Closed - unlawful to possess in all waters; Apr 1 - Dec 31: " "Open all depths. Shore-based anglers and spear divers are exempt from seasons " "and depths." ) def fact( id, species, display, fact_type, value, *, units=None, source_id, source_url, snippet, chunk="", region_key=None, aliases=(), eff_start=None, eff_end=None, ): entry = { "id": id, "species_or_category": species, "display_name": display, "aliases": list(aliases), "area_or_scope": "California ocean sport fishing regulations", "fact_type": fact_type, "value": value, "units": units, "source_id": source_id, "source_url": source_url, "retrieved_at": NOW, "supporting_chunk_id": chunk, "supporting_snippet": snippet, "validation_set": True, } if region_key: entry["region_key"] = region_key if eff_start: entry["effective_start"] = eff_start if eff_end: entry["effective_end"] = eff_end return entry NEW_FACTS = [ # --- RCG complex groundfish: rockfish + greenlings (cabezon/lingcod already seeded) --- fact("rockfish_min_size", "rockfish", "Rockfish", "minimum_size", "no minimum size limit", source_id=GROUNDFISH, source_url=GROUNDFISH_URL, aliases=["rcg complex", "rock fish"], snippet="Rockfishes (RCG Complex): No minimum size limit."), fact("rockfish_bag_limit", "rockfish", "Rockfish", "bag_limit", "10", units="fish per person in the Rockfish, Cabezon, and Greenling (RCG) Complex aggregate, with sub-limits: copper rockfish 1; vermilion/sunset rockfish 4 in the Northern Management Area and 2 elsewhere; canary rockfish 2", source_id=GROUNDFISH, source_url=GROUNDFISH_URL, snippet="10 fish in combination per person, except: Copper rockfish: 1 fish per person; Vermilion/sunset rockfish: 4 (Northern) / 2 (other areas); Canary rockfish: 2 fish per person."), fact("greenlings_min_size", "greenlings", "Kelp and rock greenling", "minimum_size", "no minimum size limit", source_id=GROUNDFISH, source_url=GROUNDFISH_URL, aliases=["greenling", "kelp greenling", "rock greenling"], snippet="Greenlings (RCG Complex): No minimum size limit."), fact("greenlings_bag_limit", "greenlings", "Kelp and rock greenling", "bag_limit", "10", units="fish per person in the Rockfish, Cabezon, and Greenling (RCG) Complex aggregate", source_id=GROUNDFISH, source_url=GROUNDFISH_URL, aliases=["greenling"], snippet="Greenlings are part of the RCG Complex aggregate of 10 fish in combination per person."), # --- RCG complex season, scoped to the SF management area --- fact("cabezon_season_sf", "cabezon", "Cabezon", "season", RCG_SEASON, region_key="san_francisco", source_id=GROUNDFISH, source_url=GROUNDFISH_URL, snippet=RCG_SEASON_QUOTE, eff_start="2026-04-01", eff_end="2026-12-31"), fact("lingcod_season_sf", "lingcod", "Lingcod", "season", RCG_SEASON, region_key="san_francisco", source_id=GROUNDFISH, source_url=GROUNDFISH_URL, snippet=RCG_SEASON_QUOTE, eff_start="2026-04-01", eff_end="2026-12-31", aliases=["ling cod"]), fact("rockfish_season_sf", "rockfish", "Rockfish", "season", RCG_SEASON, region_key="san_francisco", source_id=GROUNDFISH, source_url=GROUNDFISH_URL, snippet=RCG_SEASON_QUOTE, eff_start="2026-04-01", eff_end="2026-12-31"), fact("greenlings_season_sf", "greenlings", "Kelp and rock greenling", "season", RCG_SEASON, region_key="san_francisco", source_id=GROUNDFISH, source_url=GROUNDFISH_URL, snippet=RCG_SEASON_QUOTE, eff_start="2026-04-01", eff_end="2026-12-31"), # --- No-retention rockfish exceptions (statewide closures) --- fact("yelloweye_rockfish_no_retention", "yelloweye_rockfish", "Yelloweye rockfish", "closure", "No retention at any time; may not be taken or possessed (zero fish per person).", source_id=GROUNDFISH, source_url=GROUNDFISH_URL, snippet="Bronzespotted Rockfish, Cowcod, Quillback Rockfish, and Yelloweye Rockfish may not be taken or possessed in California - no retention at any time, zero fish per person."), fact("quillback_rockfish_no_retention", "quillback_rockfish", "Quillback rockfish", "closure", "No retention at any time; may not be taken or possessed (zero fish per person).", source_id=GROUNDFISH, source_url=GROUNDFISH_URL, snippet="Bronzespotted Rockfish, Cowcod, Quillback Rockfish, and Yelloweye Rockfish may not be taken or possessed - no retention at any time, zero fish per person."), fact("cowcod_no_retention", "cowcod", "Cowcod", "closure", "No retention at any time; may not be taken or possessed (zero fish per person).", source_id=GROUNDFISH, source_url=GROUNDFISH_URL, snippet="Bronzespotted Rockfish, Cowcod, Quillback Rockfish, and Yelloweye Rockfish may not be taken or possessed - no retention at any time, zero fish per person."), fact("bronzespotted_rockfish_no_retention", "bronzespotted_rockfish", "Bronzespotted rockfish", "closure", "No retention at any time; may not be taken or possessed (zero fish per person).", source_id=GROUNDFISH, source_url=GROUNDFISH_URL, snippet="Bronzespotted Rockfish, Cowcod, Quillback Rockfish, and Yelloweye Rockfish may not be taken or possessed - no retention at any time, zero fish per person."), # --- Flatfish & smelt: no daily bag limit (CCR 27.60(b)) --- fact("flatfish_bag_limit", "flatfish", "Pacific sanddab and other flatfish", "bag_limit", "no daily bag limit", units="(sanddabs, petrale sole, and starry flounder)", source_id=OCEAN, source_url=GENERAL_URL, aliases=["sanddab", "petrale sole", "starry flounder"], snippet="There is no limit on the following species: ... sanddabs ... petrale sole and starry flounder. (CCR T14 27.60(b))"), fact("jacksmelt_topsmelt_bag_limit", "jacksmelt_topsmelt", "Jacksmelt and topsmelt", "bag_limit", "no daily bag limit", units="(jacksmelt and topsmelt)", source_id=OCEAN, source_url=GENERAL_URL, snippet="There is no limit on the following species: ... jacksmelt, topsmelt ... (CCR T14 27.60(b))"), # --- California halibut --- fact("california_halibut_min_size", "california_halibut", "California halibut", "minimum_size", "22", units="inches total length", source_id=SF, source_url=SF_URL, aliases=["halibut", "ca halibut"], snippet="California halibut ... 22 inches total length."), fact("california_halibut_bag_limit", "california_halibut", "California halibut", "bag_limit", "2", units="fish (north of Point Sur, which includes the San Francisco area; the limit differs south of Point Sur)", source_id=SF, source_url=SF_URL, snippet="California halibut ... two fish north of Point Sur ... remains open year-round."), # --- Pacific halibut --- fact("pacific_halibut_min_size", "pacific_halibut", "Pacific halibut", "minimum_size", "no minimum size limit", source_id=SF, source_url=PHALIBUT_URL, snippet="There is no minimum size limit. The daily bag and possession limit for Pacific halibut is one fish."), fact("pacific_halibut_bag_limit", "pacific_halibut", "Pacific halibut", "bag_limit", "1", units="fish per day", source_id=SF, source_url=PHALIBUT_URL, snippet="The daily bag and possession limit for Pacific halibut is one fish."), fact("pacific_halibut_season", "pacific_halibut", "Pacific halibut", "season", "open from April 1 for the 2026 season; it stays open until November 15 north of Point Arena and through December 31 south of Point Arena, or until the quota is reached. Verify current status.", source_id=SF, source_url=PHALIBUT_URL, eff_start="2026-04-01", snippet="The 2026 recreational Pacific halibut fishery opens April 1. North of Point Arena the fishery is open until November 15; south of Point Arena it is open through December 31."), # --- White seabass --- fact("white_seabass_min_size", "white_seabass", "White seabass", "minimum_size", "28", units="inches total length (or 20 inches alternate length)", source_id=SF, source_url=SF_URL, aliases=["seabass", "white sea bass"], snippet="White Seabass ... 28 inches total length or 20 inches alternate length."), fact("white_seabass_bag_limit", "white_seabass", "White seabass", "bag_limit", "3", units="fish (only one fish may be taken south of Point Conception between March 15 and June 15)", source_id=SF, source_url=SF_URL, snippet="White Seabass ... three fish except that only one fish may be taken in waters south of Point Conception between March 15 and June 15."), # --- Surfperch (redtail size + general bag) --- fact("surfperch_min_size", "surfperch", "Surfperch", "minimum_size", "10½", units="inches total length for redtail surfperch (most other surfperch have no minimum size limit)", source_id=SF, source_url=SF_URL, aliases=["surf perch", "redtail surfperch"], snippet="Redtail Surfperch ... Minimum Size: 10½ inches total length."), fact("surfperch_bag_limit", "surfperch", "Surfperch", "bag_limit", "20", units="fish in combination of all species, with no more than 10 of any one species; shiner surfperch have a separate limit of 20", source_id=SF, source_url=SF_URL, snippet="20 fish in combination of all species (except shiner perch), with not more than 10 fish of any one species ... Shiner Perch: separate bag and possession limit of 20 fish."), # --- Sharks --- fact("leopard_shark_min_size", "leopard_shark", "Leopard shark", "minimum_size", "36", units="inches total length", source_id=SF, source_url=SF_URL, snippet="Leopard Shark ... 36 inches total length ... open year-round, at all depths."), fact("leopard_shark_bag_limit", "leopard_shark", "Leopard shark", "bag_limit", "3", units="fish", source_id=SF, source_url=SF_URL, snippet="Leopard Shark ... 3 fish ... open year-round, at all depths."), fact("state_managed_sharks_bag_limit", "state_managed_sharks", "State-managed sharks", "bag_limit", "2", units="fish per day for shortfin mako, common thresher, and blue shark (no size limit); sixgill and broadnose sevengill shark are limited to 1 fish per day", source_id=SF, source_url=SF_URL, aliases=["sharks", "sevengill shark", "mako shark"], snippet="The bag limits for shortfin mako shark, common thresher shark, and blue shark allow take of two fish per day with no size limit. The bag limits for sixgill shark and broadnose sevengill shark allow take of one fish per day with no size limit."), # --- Sturgeon (retention prohibited) --- fact("white_sturgeon_retention", "white_sturgeon", "White sturgeon", "closure", "Catch-and-release only; no retention permitted (zero fish). White sturgeon greater than 60 inches fork length may not be removed from the water.", source_id=SF, source_url=SF_URL, snippet="The White Sturgeon sport fishery is catch-and-release only. No harvest is permitted. White sturgeon greater than 60 inches fork length may not be removed from the water."), fact("green_sturgeon_retention", "green_sturgeon", "Green sturgeon", "closure", "Fully protected; no take. Green sturgeon must be released immediately and may not be removed from the water.", source_id=SF, source_url=SF_URL, snippet="Green Sturgeon ... may not be removed from the water and must be released immediately."), # --- Pacific herring --- fact("pacific_herring_bag_limit", "pacific_herring", "Pacific herring", "bag_limit", "10", units="gallons per day (approximately 100 lb or 520 fish)", source_id=SF, source_url=SF_URL, aliases=["herring"], snippet="Open year-round. Ten gallons of Pacific herring may be taken per day (approximately 100 lb. or 520 fish)."), # --- Tunas (year-round; numeric limits not fabricated) --- fact("tunas_season", "tunas", "Tunas", "season", "open year-round. Bag, possession, and size limits apply - verify current CDFW ocean sport fishing regulations.", source_id=SF, source_url=SF_URL, aliases=["tuna", "albacore"], snippet="The recreational fishery for tunas is open year-round."), # --- Rock crab --- fact("rock_crab_bag_limit", "rock_crab", "Rock crab", "bag_limit", "35", units="crab per day", source_id=CRABS, source_url=SF_URL, aliases=["red rock crab", "yellow rock crab", "brown rock crab"], snippet="The recreational fishery for all rock crab species is open year-round, statewide. The daily bag limit is 35 crab."), fact("rock_crab_min_size", "rock_crab", "Rock crab", "minimum_size", "4", units="inches", source_id=CRABS, source_url=SF_URL, snippet="For all rock crab species ... the minimum size limit is 4 inches."), fact("rock_crab_season", "rock_crab", "Rock crab", "season", "open year-round, statewide.", source_id=CRABS, source_url=SF_URL, snippet="The recreational fishery for all rock crab species is open year-round, statewide."), # --- Dungeness crab bag + size (recreational) --- fact("dungeness_crab_daily_bag", "dungeness_crab", "Dungeness crab", "bag_limit", "10", units="crab per day", source_id=CRABS, source_url=SF_URL, aliases=["dungeness crabs"], snippet="Dungeness crab: the daily bag limit is 10 crab."), fact("dungeness_crab_min_size", "dungeness_crab", "Dungeness crab", "minimum_size", "5¾", units="inches", source_id=CRABS, source_url=SF_URL, aliases=["dungeness crabs"], snippet="Dungeness crab: the minimum size limit is 5¾ inches."), # --- Dungeness crab season, SF / north-central area --- fact("dungeness_crab_season_sf", "dungeness_crab", "Dungeness crab", "season", "open from November 1, 2025 through June 30, 2026 in waters south of the Sonoma/Mendocino county line, which includes the San Francisco area. North of that line the season is open from November 1, 2025 through July 30, 2026. The 2025-26 opener was delayed by domoic-acid and whale-entanglement closures; verify current CDFW openers and health advisories before harvest.", region_key="san_francisco", source_id=CRABS, source_url=SF_URL, eff_start="2025-11-01", eff_end="2026-06-30", aliases=["dungeness crabs"], snippet="Dungeness Crab (south of the Sonoma/Mendocino county line): Open from November 1, 2025 through June 30, 2026."), # --- Ocean salmon, SF zone 2026 --- fact("ocean_salmon_season_sf", "ocean_salmon", "Ocean salmon", "season", "open June 27 - July 22 and August 1 - 31 in the San Francisco zone (Point Arena to Pigeon Point) for the 2026 season; minimum size 20 inches total length (24 inches through May 15). Dates vary by zone and can change in-season; verify current CDFW salmon regulations.", region_key="san_francisco", source_id=OCEAN, source_url=SALMON_URL, aliases=["salmon"], eff_start="2026-06-27", snippet="Point Arena to Pigeon Point (San Francisco): June 27 - July 22, August 1 - 31."), # --- Red abalone (closed) --- fact("red_abalone_closure", "red_abalone", "Red abalone", "closure", "The recreational red abalone fishery is closed; the closure was extended to 2036. All ocean waters are closed to the take of abalone (CCR T14 29.15).", source_id=OCEAN, source_url=ABALONE_URL, aliases=["abalone"], snippet="The poor condition of red abalone populations led the California Fish and Game Commission to close the fishery in 2018 ... the closure was extended to 2036 at the December, 2025 Commission meeting."), # --- Purple sea urchin --- fact("purple_urchin_bag_limit", "purple_urchin", "Purple sea urchin", "bag_limit", "35", units="sea urchins per species per day (40 gallons when skin or SCUBA diving in Humboldt, Mendocino, and Sonoma counties); no possession limit (CCR T14 29.06)", source_id=OCEAN, source_url=INVERT_URL, aliases=["sea urchin", "purple urchin"], snippet="The daily bag limit for sea urchin is 35 individuals for each species. The daily bag limit for purple sea urchin is forty (40) gallons when taken while skin or SCUBA diving in ocean waters of Humboldt, Mendocino, and Sonoma counties."), # --- Kelp / marine plants --- fact("kelp_plants_bag_limit", "kelp_plants", "Kelp and marine aquatic plants", "bag_limit", "10", units="pounds wet weight in the aggregate per day for marine aquatic plants for which take is authorized (CCR T14 30.00)", source_id=OCEAN, source_url=INVERT_URL, aliases=["kelp", "seaweed", "marine plants"], snippet="The daily bag limit on all marine aquatic plants for which the take is authorized ... is 10 pounds wet weight in the aggregate."), # --- Protected plants & fish --- fact("sea_palm_closure", "sea_palm", "Sea palm", "closure", "Sea palm may not be cut or disturbed (protected; CCR T14 30.10).", source_id=OCEAN, source_url=INVERT_URL, snippet="No eel grass (Zostera), surf grass (Phyllospadix) or sea palm (Postelsia) may be cut or disturbed."), fact("eelgrass_surfgrass_closure", "eelgrass_surfgrass", "Eelgrass and surfgrass", "closure", "Eelgrass and surfgrass may not be cut or disturbed (protected habitat; CCR T14 30.10).", source_id=OCEAN, source_url=INVERT_URL, aliases=["eelgrass", "surfgrass"], snippet="No eel grass (Zostera), surf grass (Phyllospadix) or sea palm (Postelsia) may be cut or disturbed."), fact("garibaldi_closure", "garibaldi", "Garibaldi", "closure", "Garibaldi may not be taken or possessed (protected; CCR T14 28.05).", source_id=OCEAN, source_url=GENERAL_URL, snippet="Garibaldi. May not be taken or possessed."), # Note: clams (and mussels) intentionally have no structured bag/size fact - # clam/mussel questions route to the CDPH shellfish advisory, which is the # correct food-safety answer; a bag-limit fact would be unreachable. ] def main(): data = json.loads(FACTS_PATH.read_text()) by_id = {f["id"]: i for i, f in enumerate(data["structured_facts"])} added = updated = 0 for entry in NEW_FACTS: if entry["id"] in by_id: data["structured_facts"][by_id[entry["id"]]] = entry updated += 1 else: data["structured_facts"].append(entry) added += 1 FACTS_PATH.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n") print(f"added {added}, updated {updated}; total now {len(data['structured_facts'])}") if __name__ == "__main__": main()