Initial commit

This commit is contained in:
kfj001 committed 2026-09-06 10:18:26 -07:00
commit 5f2db08b43
100 files changed
+16985

No files matched your search

+190
View File
@@ -0,0 +1,190 @@
# API Contract — Legislative Dashboard UI (Spec §5)
**Audience:** dashboard build lane (`t_b38e9af2`), correlation lane (`t_2736f40d`), LLM lane (`t_950780e0`). One shared contract so no lane diverges.
**Principle:** *auth-agnostic, field-name-stable, degradeable.* The preferences/personalization shape does not depend on which auth method is chosen.
**Base record = `scraper/schema.py::BillAction`** (flat `daily_file_records`) **+** the correlation lane's relational tables (bills, committees, members, bill↔committee joins) **+** the LLM lane's per-bill `tags`.
**Status:** `draft v0.1` — field names are the frozen part; *values* and *endpoints* are ratifiable. Auth method = Open Decision D1; ZIP model = Open Decision D2; scoring weights = D3.
---
## 0. Conventions
- **Dates:** ISO `YYYY-MM-DD` (`date`). Times are strings (`"9:30am"`, `"Upon Call of the Chair"`) — do NOT canonicalize to RFC3339 in the list layer; keep source form, add a derived `hearing_sort` field (see §2) for stable ordering.
- **Enums:** `chamber_origin` ∈ `{ASSEMBLY, SENATE}`. `source` ∈ `{ASSEMBLY_DAILY_FILE, SENATE_DAILY_FILE_PDF, LEGINFO_BILL_RESULTS}`.
- **Nullability:** fields a daily-file event may legitimately lack (`hearing_time`, `location`, `committee` for floor items, `tags` pre-LLM) are nullable. Consumers must render a `TBD`/omitted state, never crash on null.
- **IDs:** `bill_id` = leginfo canonical (`"202520260AB28"`). `legislator_id`, `district_id`, `tag_id`, `user_id` = system-owned opaque IDs (snake_case, stable).
- **Auth:** every `/person`/`/prefs`/`/watch`/`/topics` and the `/bills` personal view requires a bearer token (method per D1). `/bills`, `/tags`, `/legislators`, `/districts/resolve` are public.
- **Errors** (§6): HTTP status + `{ "error": { "code", "message", "field?" } }`. `field` on 400.
- **Versioning:** contract at `/api/v1/*`. Breaking changes → new major; add fields, never rename/delete (the frozen field names).
---
## 1. Shared record: `Bill` (what the UI consumes)
```json
{
"bill_id": "202520260AB28",
"bill_number": "AB 28",
"chamber_origin": "ASSEMBLY",
"author": "Schiavo et al.",
"short_title": "Waste: requires recycling standards…",
"description": "Same as short_title for daily-file; enrichment may expand.",
"committee": "Resources and Water",
"hearing_time": "9:30am",
"hearing_sort": 55800,
"location": "State Capitol, Room 444",
"date": "2026-09-07",
"session": "2025-2026",
"item_number": 14,
"supplement": 2,
"source": "ASSEMBLY_DAILY_FILE",
"tags": ["waste", "environment"],
"relevance": {
"score": 5,
"matched": [
{ "type": "watched-legislator", "value": "Leg. Schiavo (A-14, chair)" },
{ "type": "tag", "value": "waste" }
]
}
}
```
- **`hearing_sort`** (derived, backend-owned): seconds-from-midnight for HH:MMam/pm; `"Upon Call of the Chair"` / `"Call of the Chair"` → **max** (sorts last); null → **max** (with `hearing_time: null`). Enables the within-day sort in DESIGN_DASHBOARD §4.
- **`relevance`** present for **logged-in** requests only; omitted/`score:0` for anon. `matched[]` powers the `Why is this here?` transparency UI. Each entry: `type` ∈ `{watched-legislator, committee-of-watched-legislator, tag}`, `value` = human-readable reason.
- **`tags`** may be `null` (no LLM analysis yet) or `[]` → UI renders no chips, ranking degrades to legislator-match only.
### Bill list envelope
```json
{
"session": "2025-2026",
"as_of": "2026-09-05T18:00:00Z",
"chamber_availability": { "ASSEMBLY": "available", "SENATE": "pending" },
"items": [ /* Bill[] */ ],
"groups": {
"2026-09-07": 14,
"2026-09-08": 31
}
}
```
- `chamber_availability` drives the "Senate unavailable" note — never surface Senate rows when `"pending"`.
- Personal view returns `items` in **floated-first order** with the `PersonalBand` slice (top N) derivable by `relevance.score > 0` then `hearing_sort`.
---
## 2. Endpoint `GET /api/v1/bills`
**Query:** `?upcoming=1&from=2026-09-01&to=2026-09-30&chamber=ASSEMBLY&tags=waste,health&author=…&q=recycling&sort=hearing_limit&personal=1`
- `upcoming=1` → only future-dated events (`date >= as_of` and/or not yet acted on) — see §5 "upcoming" definition.
- `personal=1` (logged in) → apply the float-to-top ranking (§4 of DESIGN_DASHBOARD); response carries `relevance` on items.
- Public without token; token *upgrades* the view (adds `relevance`, enables `personal=1`).
**Response:** the Bill-list envelope (§1). HTTP 200. Empty → `items: []` + `groups: {}` (UI renders `/bills` empty-state, not an error).
---
## 3. Personalization endpoints (logged-in)
### 3.1 `POST /api/v1/districts/resolve` — ZIP/address → districts → legislators
```json
// request (one of the input modes; Open Decision D2 sets which is primary)
{ "zip": "90210" } // fast path (may be ambiguous)
{ "address": "…" } // authoritative (Legislature full-address tool)
{ "mode": "manual", "district": "A-14" } // fallback
// response
{
"resolved_from": "zip", // | "address" | "manual"
"confidence": "single" | "ambiguous" | "zero" | "out-of-state",
"districts": [
{
"district_id": "A-14",
"type": "ASSEMBLY", // | "SENATE"
"number": 14,
"assembly_member": { "legislator_id": "L-asm-14", "name": "M. Schiavo", "party": "D" },
"senate_member": { "legislator_id": "L-sen-08", "name": "R. Riu", "party": "R" }
}
],
"suggested_topics": ["waste", "housing"], // derived from member committees ∩ session tag cloud
"suggested_watch": ["L-asm-14", "L-sen-08"]
}
```
- **Confidence semantics (must match §8 of DESIGN_DASHBOARD):** `single` → UI auto-advances to the candidate/legislator list. `ambiguous`/`zero`/`out-of-state` → UI shows the §6.4 state (candidates to confirm / manual picker / "not in CA"), **never auto-watches.**
- **Source:** `findyourrep.legislature.ca.gov` (by address) + `data.ca.gov` boundary datasets (by zip). Re-verify freshness at redistricting.
- **Resolve does not persist** — it *proposes*. Persisting watches happens in §3.3. (Trusting-resolve = trust requirement; see DESIGN §8.)
### 3.2 `GET /api/v1/legislators?q=…` — browse / find legislators
```json
{ "items": [
{ "legislator_id": "L-asm-14", "name": "M. Schiavo", "party": "D",
"chamber": "ASSEMBLY", "district": 14,
"committees": ["Resources and Water"],
"domains": ["waste", "water"] } ] }
```
### 3.3 `GET / PUT /api/v1/prefs` — the personalization store
```json
{
"user_id": "u_001",
"zip": "90210", // optional (address lookup may leave null)
"address": null, // optional, only if D2 makes address the input
"home_chamber": "ASSEMBLY", // optional hint
"districts": [ { "district_id": "A-14", "type": "ASSEMBLY", "number": 14 } ],
"watched_legislators": [ "L-asm-14", "L-sen-08" ],
"opted_in_tags": [ "waste", "health" ],
"updated_at": "2026-09-05T18:00:00Z"
}
```
- `GET` returns current prefs (or `404` if none → UI shows setup card). `PUT` replaces the whole blob. Granular helpers `POST /prefs/districts/confirm` etc. allowed but not required.
- **This shape is auth-method-independent** — D1 doesn't change it.
### 3.4 `POST /api/v1/prefs/watch` & `DELETE /api/v1/prefs/watch/:legislator_id`
Toggle a single watch (optimistic-update friendly; returns the updated `watched_legislators`). Same for topics:
### 3.5 `POST /api/v1/prefs/topics` & `DELETE /api/v1/prefs/topics/:tag_id`
- 201 on add / 200 on remove; 400 with `field` on unknown id. Idempotent.
---
## 4. Reference data endpoints (public)
### 4.1 `GET /api/v1/tags` — the tag cloud (from the LLM lane)
```json
{
"as_of": "2026-09-05T18:00:00Z",
"tags": [
{ "tag_id": "tag:health", "label": "Health", "count": 214 },
{ "tag_id": "tag:education","label": "Education", "count": 188 },
{ "tag_id": "tag:waste", "label": "Waste", "count": 42 }
]
}
```
- `count` = number of bills carrying the tag (drives the `· 42 bills` UI affordance and the "top topics" teaser). May be sparse early (LLM lane warming) — UI shows the "filling in" hint, never a 0-count dead chip.
- **Ownership note:** the *taxonomy/labels* are produced by the LLM lane (`t_950780e0`); this contract fixes their *shape*, not their values. A future `curator` role (out of scope) would own label normalization.
### 4.2 `GET /api/v1/legislators` & `GET /api/v1/districts` — public browse (from correlation lane)
Same shapes as §3.2 / the `districts[]` items; no token needed.
---
## 5. "Upcoming" — the exact, testable definition
`upcoming` = a bill event satisfying **all**:
1. `event_date = COALESCE(date, hearing date)` and `event_date >= as_of` (future-dated), **and**
2. not in a terminal "acted/closed" state the source marks (a committee that already passed it out of committee, or floor passage) — i.e., the event is an *open* future moment, not a historical action.
3. For a bill with **multiple future events** (committee hearing then floor on different days), each event is its own upcoming row; dedupe only *within* a `date`.
- If the source can't confirm state #2, **default to including** (false-negative risk: dropping a real hearing is worse than showing a closed one) and mark `possibly_closed: true` (future field; not required for v0.1).
- **"This week" / "This month"** filters are just `from`/`to` windows over `event_date`.
---
## 6. Errors (all endpoints)
```json
{ "error": { "code": "invalid_zip" | "ambiguous_district" | "district_not_found"
| "out_of_state" | "tag_unknown" | "legislator_unknown"
| "not_authenticated" | "prefs_not_set" | "chamber_unavailable",
"message": "human-readable", "field": "zip" } }
```
- `400` invalid/ambiguous/out-of-state (with `field`). `401` not authenticated. `404` unknown id / no prefs. `409` race on concurrent watch add (client retries idempotently). `503` chamber unavailable (e.g. Senate antibot) — **not a hard client error**; UI renders "Senate unavailable" and continues on Assembly. `503` also when the LLM tag service is down → UI degrades, no tag chips, ranking by legislator only.
---
## 7. Contract notes for each lane
- **Correlation lane (`t_2736f40d`):** owns §3.1 resolve + §4.2 legislators/districts from `correlation_sources.member_rosters` (findyourrep + data.ca.gov). Must produce the `districts[]` + `assembly_member`/`senate_member` join to members.
- **LLM lane (`t_950780e0`):** owns `tags[]` on `Bill` (§1) and §4.1 `GET /tags`. Must tolerate the **deterministic fallback tagger** (configurable ollama) so `tags` degrades to a known small set when ollama is down — v0.1 `tags` may be `[]`/null until warmed.
- **Dashboard build (`t_b38e9af2`):** implements the float-to-top ranking from `relevance.score` (§1) per DESIGN_DASHBOARD §4; consumes this contract verbatim. Auth method per D1 is chosen *here* but the shapes above are frozen regardless.
- **QA:** assert `hearing_sort` ordering, `upcoming` boundary (a T-1 day is excluded), `chamber_availability` gates Senate rows, and the 6 empty states each return `200 items:[]` (not 4xx/5xx).
---
*Companion docs:* `design/DESIGN_DASHBOARD.md` (IA + flows + visual system), `design/wireframes/*.html` (3 mockups). Open Decisions: **D1 auth method, D2 ZIP vs address primary input + data-source provenance, D3 scoring weights.**