coastwise / scripts /seed_expand_statewide.py
Stephen S. Lee
feat: source-derived Q&A with statewide CA ocean species coverage (#1)
c1b067e unverified
Raw
History Blame Contribute Delete
19.3 kB
"""Phase-2 seed expansion: statewide CA ocean species (esp. Southern + inverts).
Every value verified against the cited CDFW page or CCR Title 14 during the
2026-06-15 research pass. Species without a curated rule card are detected via
the fact alias index, so facts alone make them answerable.
"""
import json
from pathlib import Path
FACTS_PATH = Path("data/source-cache/seed/facts.json")
SOUTH = "cdfw_region_southern"
SOUTH_URL = "https://wildlife.ca.gov/Fishing/Ocean/Regulations/Fishing-Map/Southern"
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"
NOW = "2026-06-15T12:00:00+00:00"
GENERAL_BAG = "10"
GENERAL_BAG_UNITS = (
"fish of any one species per day, within the general limit of 20 finfish in "
"combination of all species (no species-specific minimum size limit; CCR T14 27.60)"
)
NO_TAKE = "Take and possession prohibited in California ocean waters; may not be taken or possessed."
def f(id, species, display, fact_type, value, *, units=None, source_id, source_url,
snippet, aliases=(), region_key=None, 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": "", "supporting_snippet": snippet, "validation_set": False,
}
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
def general_limit(key, display, aliases):
return f(f"{key}_bag_limit", key, display, "bag_limit", GENERAL_BAG, units=GENERAL_BAG_UNITS,
source_id=OCEAN, source_url=GENERAL_URL, aliases=aliases,
snippet="No more than 20 finfish in combination of all species, with not more than 10 of any one species, may be taken or possessed (CCR T14 27.60).")
def no_limit(key, display, aliases):
return f(f"{key}_bag_limit", key, display, "bag_limit", "no daily bag limit", units="(CCR T14 27.60(b))",
source_id=OCEAN, source_url=GENERAL_URL, aliases=aliases,
snippet="There is no limit on the following species: anchovy, jacksmelt, topsmelt, Pacific butterfish (pompano), queenfish, sanddabs, skipjack, jack mackerel, Pacific mackerel, ... Pacific sardine ... (CCR T14 27.60(b)).")
def no_take(key, display, aliases, source_url=GENERAL_URL, source_id=OCEAN, snippet=None):
return f(f"{key}_closure", key, display, "closure", NO_TAKE, source_id=source_id, source_url=source_url,
aliases=aliases, snippet=snippet or f"{display}: may not be taken or possessed.")
NEW = [
# --- Southern California bass complex (CCR 28.30 / Southern page) ---
f("kelp_bass_min_size", "kelp_bass", "Kelp bass", "minimum_size", "14", units="inches total length",
source_id=SOUTH, source_url=SOUTH_URL, aliases=["calico bass", "calico", "kelp bass"],
snippet="Kelp bass, barred sand bass and spotted sand bass: the minimum size limit is 14 inches total length."),
f("kelp_bass_bag_limit", "kelp_bass", "Kelp bass", "bag_limit", "5",
units="fish in any combination of kelp, barred sand, and spotted sand bass per day",
source_id=SOUTH, source_url=SOUTH_URL, aliases=["calico bass"],
snippet="Five fish in any combination of species (kelp bass, barred sand bass, spotted sand bass)."),
f("barred_sand_bass_min_size", "barred_sand_bass", "Barred sand bass", "minimum_size", "14", units="inches total length",
source_id=SOUTH, source_url=SOUTH_URL, aliases=["sand bass"],
snippet="Kelp bass, barred sand bass and spotted sand bass: the minimum size limit is 14 inches total length."),
f("barred_sand_bass_bag_limit", "barred_sand_bass", "Barred sand bass", "bag_limit", "5",
units="fish in any combination of kelp/barred sand/spotted sand bass, of which no more than 4 may be barred sand bass",
source_id=SOUTH, source_url=SOUTH_URL, aliases=["sand bass"],
snippet="Five fish in any combination of species, except no more than 4 barred sand bass may be taken. The minimum size limit is 14 inches."),
f("spotted_sand_bass_min_size", "spotted_sand_bass", "Spotted sand bass", "minimum_size", "14", units="inches total length",
source_id=SOUTH, source_url=SOUTH_URL, aliases=["spotted bay bass", "spotty"],
snippet="Kelp bass, barred sand bass and spotted sand bass: the minimum size limit is 14 inches total length."),
f("spotted_sand_bass_bag_limit", "spotted_sand_bass", "Spotted sand bass", "bag_limit", "5",
units="fish in any combination of kelp/barred sand/spotted sand bass per day",
source_id=SOUTH, source_url=SOUTH_URL, aliases=["spotted bay bass"],
snippet="Five fish in any combination of species (kelp bass, barred sand bass, spotted sand bass)."),
# --- California sheephead (Southern page) ---
f("california_sheephead_min_size", "california_sheephead", "California sheephead", "minimum_size", "12", units="inches total length",
source_id=SOUTH, source_url=SOUTH_URL, aliases=["sheephead", "sheepshead"],
snippet="California sheephead: 2 fish, with a minimum size limit of 12 inches total length."),
f("california_sheephead_bag_limit", "california_sheephead", "California sheephead", "bag_limit", "2", units="fish per day",
source_id=SOUTH, source_url=SOUTH_URL, aliases=["sheephead"],
snippet="California sheephead: 2 fish, with a minimum size limit of 12 inches total length."),
# --- California barracuda (CCR 28.25) ---
f("california_barracuda_min_size", "california_barracuda", "California barracuda", "minimum_size", "28",
units="inches total length (or 17 inches alternate length)",
source_id=OCEAN, source_url=GENERAL_URL, aliases=["barracuda", "pacific barracuda"],
snippet="California barracuda minimum size: twenty-eight inches total length or seventeen inches alternate length (CCR T14 28.25)."),
# --- Pacific bonito (CCR 28.32) ---
f("pacific_bonito_min_size", "pacific_bonito", "Pacific bonito", "minimum_size", "24",
units="inches fork length or 5 pounds, except up to 5 smaller fish may be taken",
source_id=OCEAN, source_url=GENERAL_URL, aliases=["bonito"],
snippet="Pacific bonito minimum size: twenty-four inches fork length or five pounds, except that five fish less than twenty-four inches or five pounds may be taken (CCR T14 28.32)."),
f("pacific_bonito_bag_limit", "pacific_bonito", "Pacific bonito", "bag_limit", "10", units="fish per day",
source_id=OCEAN, source_url=GENERAL_URL, aliases=["bonito"],
snippet="Pacific bonito limit: ten (CCR T14 28.32)."),
# --- California yellowtail (Southern page) ---
f("california_yellowtail_min_size", "california_yellowtail", "California yellowtail", "minimum_size", "24",
units="inches fork length, except up to 5 smaller fish may be taken",
source_id=SOUTH, source_url=SOUTH_URL, aliases=["yellowtail"],
snippet="California yellowtail: the minimum size limit is 24 inches fork length, except that up to five fish less than 24 inches fork length may be taken."),
f("california_yellowtail_bag_limit", "california_yellowtail", "California yellowtail", "bag_limit", "10", units="fish per day",
source_id=SOUTH, source_url=SOUTH_URL, aliases=["yellowtail"],
snippet="California yellowtail: ten fish."),
# --- Ocean whitefish (Southern page) ---
f("ocean_whitefish_min_size", "ocean_whitefish", "Ocean whitefish", "minimum_size", "no minimum size limit",
source_id=SOUTH, source_url=SOUTH_URL, aliases=["whitefish"],
snippet="Ocean whitefish: 10 fish within the general daily bag limit of 20 fish total, with no minimum size limit."),
f("ocean_whitefish_bag_limit", "ocean_whitefish", "Ocean whitefish", "bag_limit", "10",
units="fish within the general daily bag limit of 20 finfish total",
source_id=SOUTH, source_url=SOUTH_URL, aliases=["whitefish"],
snippet="Ocean whitefish: 10 fish within the general daily bag limit of 20 fish total."),
# --- California grunion (CCR 28.00) ---
f("california_grunion_season", "california_grunion", "California grunion", "season",
"open to take by hand only, except closed (no take) from April 1 through June 30.",
source_id=OCEAN, source_url=GENERAL_URL, aliases=["grunion"], eff_start="2026-04-01", eff_end="2026-06-30",
snippet="It shall be unlawful to take grunion from April 1 through June 30 (CCR T14 28.00)."),
f("california_grunion_bag_limit", "california_grunion", "California grunion", "bag_limit", "30", units="fish per day",
source_id=OCEAN, source_url=GENERAL_URL, aliases=["grunion"],
snippet="California grunion limit: 30 (CCR T14 28.00)."),
# --- Giant (black) sea bass: no-take off California (CCR 28.10) ---
no_take("giant_sea_bass", "Giant sea bass", ["black sea bass", "giant black sea bass"],
snippet="Giant sea bass may not be taken off California; fish taken incidental to other fishing must be immediately returned to the water (CCR T14 28.10)."),
# --- No-limit baitfish/pelagics (CCR 27.60(b)) ---
no_limit("northern_anchovy", "Northern anchovy", ["anchovy", "anchovies"]),
no_limit("pacific_sardine", "Pacific sardine", ["sardine", "sardines"]),
no_limit("pacific_mackerel", "Pacific mackerel", ["mackerel", "spanish mackerel"]),
no_limit("jack_mackerel", "Jack mackerel", ["jack mackerel"]),
no_limit("queenfish", "Queenfish", ["queenfish", "herring queenfish"]),
no_limit("pacific_butterfish", "Pacific butterfish (pompano)", ["butterfish", "pompano", "pacific pompano"]),
# --- General-limit nearshore SoCal finfish (CCR 27.60) ---
general_limit("white_croaker", "White croaker", ["white croaker", "kingfish", "tomcod"]),
general_limit("yellowfin_croaker", "Yellowfin croaker", ["yellowfin croaker"]),
general_limit("spotfin_croaker", "Spotfin croaker", ["spotfin croaker"]),
general_limit("california_corbina", "California corbina", ["corbina", "corvina"]),
general_limit("opaleye", "Opaleye", ["opaleye", "opal eye"]),
general_limit("halfmoon", "Halfmoon", ["halfmoon", "blue perch", "catalina blue perch"]),
general_limit("sargo", "Sargo", ["sargo", "china croaker"]),
# --- Sharks / rays / skates ---
general_limit("spiny_dogfish", "Spiny dogfish", ["spiny dogfish", "dogfish", "mud shark"]),
general_limit("skates", "Skates", ["skate", "skates", "big skate", "longnose skate"]),
general_limit("bat_ray", "Bat ray", ["bat ray", "bat stingray"]),
general_limit("shovelnose_guitarfish", "Shovelnose guitarfish", ["guitarfish", "shovelnose guitarfish", "shovelnose shark"]),
general_limit("thornback_ray", "Thornback ray", ["thornback", "thornback ray"]),
general_limit("round_stingray", "Round stingray", ["round stingray", "stingray", "round ray"]),
no_take("white_shark", "White shark (great white)", ["great white", "white shark", "great white shark"],
source_id=SOUTH, source_url=SOUTH_URL,
snippet="White sharks may not be taken or possessed at any time (CCR T14 28.06; CDFW Southern region)."),
no_take("gulf_grouper", "Gulf grouper", ["gulf grouper"],
snippet="Gulf grouper may not be taken or possessed (CCR T14 28.12)."),
no_take("broomtail_grouper", "Broomtail grouper", ["broomtail grouper"],
snippet="Broomtail grouper may not be taken or possessed (CCR T14 28.12)."),
no_take("basking_shark", "Basking shark", ["basking shark"],
snippet="Basking sharks are prohibited from general retention in the U.S. West Coast HMS fishery (50 CFR 660.705)."),
no_take("megamouth_shark", "Megamouth shark", ["megamouth", "megamouth shark"],
snippet="Megamouth sharks are prohibited from general retention in the U.S. West Coast HMS fishery (50 CFR 660.705)."),
# --- Invertebrates ---
f("california_spiny_lobster_min_size", "california_spiny_lobster", "California spiny lobster", "minimum_size",
"3¼", units="inches carapace (straight line on the mid-line of the back, rear of eye socket to rear of body shell)",
source_id=OCEAN, source_url=INVERT_URL, aliases=["spiny lobster"],
snippet="California spiny lobster minimum size: 3 and 1/4 inches measured on the mid-line of the back from the rear edge of the eye socket to the rear edge of the body shell (CCR T14 29.90)."),
f("california_spiny_lobster_bag_limit", "california_spiny_lobster", "California spiny lobster", "bag_limit", "7", units="lobster per day",
source_id=OCEAN, source_url=INVERT_URL, aliases=["spiny lobster"],
snippet="California spiny lobster daily bag limit: seven (CCR T14 29.90)."),
f("california_spiny_lobster_season", "california_spiny_lobster", "California spiny lobster", "season",
"open from 6:00 p.m. on the Friday preceding the first Wednesday in October through the first Wednesday after the 15th of March.",
source_id=OCEAN, source_url=INVERT_URL, aliases=["spiny lobster"],
snippet="Open season: from 6:00 p.m. on the Friday before the first Wednesday in October through the first Wednesday after the 15th of March (CCR T14 29.90)."),
f("rock_scallop_bag_limit", "rock_scallop", "Rock scallop", "bag_limit", "10", units="scallops per day",
source_id=OCEAN, source_url=INVERT_URL, aliases=["rock scallop", "scallop", "scallops"],
snippet="Rock scallop limit: ten (CCR T14 29.60)."),
no_take("speckled_scallop", "Speckled (bay) scallop", ["bay scallop", "speckled scallop"],
source_id=OCEAN, source_url=INVERT_URL,
snippet="Speckled (bay) scallops may not be taken or possessed (CCR T14 29.65)."),
f("market_squid_bag_limit", "market_squid", "Market squid", "bag_limit", "no daily bag limit", units="(may be taken with hand-held dip nets)",
source_id=OCEAN, source_url=INVERT_URL, aliases=["squid", "calamari", "market squid"],
snippet="Squid may be taken with hand-held dip nets. There is no limit (CCR T14 29.70)."),
f("kellets_whelk_bag_limit", "kellets_whelk", "Kellet's whelk", "bag_limit", "35", units="whelks per day",
source_id=OCEAN, source_url=INVERT_URL, aliases=["whelk", "kellets whelk", "kellet's whelk"],
snippet="Kellet's whelk: 35 whelks; season July 1 through the first Wednesday after the 15th of March (CCR T14 29.17, 29.05)."),
f("octopus_bag_limit", "octopus", "Octopus", "bag_limit", "35", units="octopus per day",
source_id=OCEAN, source_url=INVERT_URL, aliases=["octopus", "octopuses", "octopi"],
snippet="Octopus: 35 octopus, year round (CCR T14 29.05)."),
f("native_oyster_bag_limit", "native_oyster", "Native oyster", "bag_limit", "35", units="oysters per day",
source_id=OCEAN, source_url=INVERT_URL, aliases=["oyster", "oysters", "native oyster"],
snippet="Oysters, all: 35 oysters, year round (CCR T14 29.05)."),
f("ghost_shrimp_bag_limit", "ghost_shrimp", "Ghost shrimp and blue mud shrimp", "bag_limit", "50", units="in combination per day",
source_id=OCEAN, source_url=INVERT_URL, aliases=["ghost shrimp", "mud shrimp", "blue mud shrimp"],
snippet="Ghost shrimp and blue mud shrimp limit: fifty in combination (CCR T14 29.87)."),
f("coonstripe_shrimp_bag_limit", "coonstripe_shrimp", "Coonstripe shrimp", "bag_limit", "20",
units="pounds in the shell, heads on, per day",
source_id=OCEAN, source_url=INVERT_URL, aliases=["coonstripe shrimp"],
snippet="Coonstripe shrimp: twenty pounds (in the shell, heads on) (CCR T14 29.88)."),
f("sand_crab_bag_limit", "sand_crab", "Sand crab (mole crab)", "bag_limit", "50", units="per day",
source_id=OCEAN, source_url=INVERT_URL, aliases=["sand crab", "mole crab", "sand crabs"],
snippet="Sand crabs (Emerita analoga): fifty (CCR T14 29.85)."),
# --- Distinct named clams (own CCR sections) ---
f("gaper_clam_bag_limit", "gaper_clam", "Gaper clam (horseneck clam)", "bag_limit", "10",
units="of each species per day (Humboldt Bay: 50 in combination, max 25 gaper; Elkhorn Slough: 12 in combination)",
source_id=OCEAN, source_url=INVERT_URL, aliases=["gaper clam", "horseneck clam", "horse clam"],
snippet="Gaper/horseneck clams: limit ten of each species, except in Humboldt Bay fifty in combination (no more than 25 gaper) and in Elkhorn Slough twelve in combination (CCR T14 29.25)."),
f("geoduck_clam_bag_limit", "geoduck_clam", "Geoduck clam", "bag_limit", "3", units="clams per day",
source_id=OCEAN, source_url=INVERT_URL, aliases=["geoduck", "geoduck clam"],
snippet="Geoduck clams: limit three; the first three dug must be retained as the bag limit (CCR T14 29.30)."),
f("pismo_clam_min_size", "pismo_clam", "Pismo clam", "minimum_size", "5", units="inches greatest diameter",
source_id=OCEAN, source_url=INVERT_URL, aliases=["pismo clam"],
snippet="Pismo clams: minimum size five inches in greatest diameter; limit ten (CCR T14 29.40)."),
f("pismo_clam_bag_limit", "pismo_clam", "Pismo clam", "bag_limit", "10", units="clams per day",
source_id=OCEAN, source_url=INVERT_URL, aliases=["pismo clam"],
snippet="Pismo clams: limit ten (CCR T14 29.40)."),
f("razor_clam_bag_limit", "razor_clam", "Razor clam", "bag_limit", "20",
units="clams per day (the first 20 dug must be retained regardless of size or condition)",
source_id=OCEAN, source_url=INVERT_URL, aliases=["razor clam", "razor clams"],
snippet="Razor clams: limit twenty; the first twenty dug must be retained as the bag limit (CCR T14 29.45)."),
f("littleneck_clam_bag_limit", "littleneck_clam", "Littleneck, cockle, and softshell clams", "bag_limit", "50",
units="in combination per day (minimum size 1½ inches, except no size limit for soft-shell clams)",
source_id=OCEAN, source_url=INVERT_URL, aliases=["littleneck clam", "cockle", "softshell clam", "soft-shell clam", "quahog", "chione"],
snippet="Littleneck clams, soft-shell clams, chiones, northern quahogs and cockles: limit fifty in combination; minimum size one and one-half inches, except no size limit for soft-shell clams (CCR T14 29.35)."),
]
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:
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")
species = {f["species_or_category"] for f in data["structured_facts"]}
print(f"added {added}, updated {updated}; total facts {len(data['structured_facts'])}, distinct species {len(species)}")
if __name__ == "__main__":
main()