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

+485
View File
@@ -0,0 +1,485 @@
# California Legislature Daily File — Parse & Ingestion Spec
**Research task:** `t_a171f4d1`
**Date:** 2026-09-04 (PDT, current 2025-2026 regular session)
**Verified against live pages** and **archived to `./samples/`**.
This document is the single source of truth for the ingestion
task (`t_f595542f`), the parser task (`t_fb8081e7`), and the RDB schema
task (`t_8e5a3821`). A developer should be able to implement a parser from
this document without revisiting the live pages, using the samples in
`./samples/`.
---
## TL;DR — the most important finding
- **Assembly Daily File = fully server-rendered HTML.** Three data tables with
semantic CSS classes and `data-sort` attributes hold pre-cleaned canonical
values (dates without the `-###` suffix, trimmed committee/author names).
Parse offline with BeautifulSoup + the CSS selectors below; no JS required.
- **Senate Daily File = PDF only.** `senate.ca.gov/dailyfile` is a Vue pdf-viewer
wrapper with **no parseable data table**. The publication exists only as a
PDF, and `senate.ca.gov` wraps the download path in an **antibot challenge**
(HTTP 403 with a 67-byte HTML body on programmatic fetch, verified 2026-09-04).
The Senate daily file must be ingested via a PDF path (pdfplumber / PyMuPDF)
with antibot mitigation, **or** enriched from leginfo's parallel
`billResultsClient` / `billHistoryClient` views (see alt sources).
- **Neither `?date=` nor `?dailyfile=…` is addressable.** The Assembly page
always renders the **current live** daily file — there is no historical-date
query parameter. "Date" is a data value (read from the hearing table), not a
URL parameter.
- **Cross-chamber bills:** an Assembly bill (AB…) can appear on a Senate daily
file and vice-versa. The "chamber" field means **origin**, not publishing house.
---
## 1. Canonical URLs
### 1.1 Assembly Daily File
| Item | URL |
|---|---|
| Canonical (HTML, always current) | `<https://www.assembly.ca.gov/schedules-publications/daily-file>` |
| Alias (redirects to canonical) | `https://assembly.ca.gov/schedules-publications/daily-file` |
| Alias | `https://assembly.ca.gov/dailyfile/pdfpublications` |
| Legacy help page | `https://leginfo.ca.gov/help/dayfileh.html` |
| Live sample | `samples/assembly_dailyfile_latest.html` (2026-09-04, 401 192 bytes) |
Properties:
- `requires_js=false` — data lives in static `<table>` markup.
- `antibot=false` — plain `requests` GET with a sensible `User-Agent` works.
- `content-type: text/html; charset=UTF-8`.
- No date parameter. `?date=2025-08-31` and `?dailyfile=2026-08-31` were
probed and **do not select a historical file** — the page always returns the
current live daily file.
### 1.2 Senate Daily File
| Item | URL |
|---|---|
| HTML index (human page, JS-rendered pdf viewer) | `https://www.senate.ca.gov/dailyfile` |
| Canonical (per `<link rel=canonical>`) | `https://www.senate.ca.gov/publications/senate-daily-file` |
| PDF download base | `https://senate.ca.gov/dailyfile/publications/download/` |
| Live index sample | `samples/senate_dailyfile_index.html` (34 137 bytes) |
| Verified PDF fragment | `SEN_COM_2025_R_20260902.PDF` (200 when served; 403 under antibot for this scanner) |
PDF filename pattern:
```
SEN_{TYPE}_{year}_R_{YYYYMMDD}.PDF
```
- `{TYPE}` = `R` (regular) or `COM` (committee daily file). Historical variants:
`SEN_ESS_*` for extraordinary/special sessions.
- `{year}` = the legislative session start year (e.g., `2025` for 2025-2026).
- `{YYYYMMDD}` = the publication date.
**Critical:** Not every calendar day has a published file (weekends,
non-legislative days). 404 on the PDF is **expected**, NOT an error —
the ingestion layer must distinguish "404 no-file" from "5xx transient".
---
## 2. Parameterisation
| Source | Parameter | Scheme | Notes |
|---|---|---|---|
| Assembly Daily File | `date` | — | No URL param. As-of date is read from the hearing table. |
| Senate Daily File | `date` + `session_year` + `type` | `SEN_{TYPE}_{year}_R_{YYYYMMDD}.PDF` | Filename-encoded. |
| Bill detail (enrichment) | — | `bill_id` | Leginfo canonical bill page; see §3. |
| Committee rosters | — | Static pages | No per-date param; refreshed periodically. |
---
## 3. Assembly Daily File HTML structure
Three `<table>` elements, in document order. Each represents a distinct record
type. Every data cell is a `td` with a **specific class** and, where
applicable, a **`data-sort`** attribute carrying the clean canonical value.
Prefer `data-sort` over `textContent` in extraction — it strips the display
suffix (`'08/31/26-001'` → `08/31/26`), leading zeros, and the like.
### 3.1 Committee Hearings
- **Table selector:** `table.committee-hearing-table`
- **Row selector:** `tr[class*='committee-hearing-details']`
Verified on 2026-09-04: **4 hearing rows** in the current file.
| Field | Selector | Best value source |
|---|---|---|
| `date` | `td.committee_hearing-date` | `attr:data-sort` (e.g. `"08/31/26"`; raw `"08/31/26-001"`) |
| `time` | `td.committee_hearing-time` | `text` (e.g. `"9:30am"`, `"Upon Call of the Chair"`) |
| `committee` | `td.committee_hearing-name` | `attr:data-sort` |
| `location` | `td.committee_hearing-location` | `attr:data-sort` |
| `agenda_id` | `td.committee_hearing-menu a[href^='/api/dailyfile/agenda/']` | `attr:href` — Drupal AJAX id |
| `ics_url` | `td.committee_hearing-menu a[href^='/api/ics/generate']` | `attr:href` |
Example row (verbatim):
```html
<tr class=" committee-hearing-details">
<td class="committee_hearing-date" data-sort="08/31/26-001">08/31/26</td>
<td class="committee_hearing-time">Upon Call of the Chair</td>
<td class="committee_hearing-name" data-sort="Budget">Budget</td>
<td class="committee_hearing-location" data-sort="1021 O Street, Room 1100">1021 O Street, Room 1100</td>
<td class="committee_hearing-menu">
<div class="was-dropdown" data-once="wasDropdown">
<button class="was-dropdown-toggle hearing-options" aria-haspopup="true" aria-expanded="false">…</button>
<div class="was-dropdown-menu" role="menu" aria-hidden="true">
<ul>
<li role="none"><a href="/api/ics/generate?name=Budget&amp;start=20260831T000000&amp;end=20260901T000000&amp;location=1021%20O%20Street%2C%20Room%201100" role="menuitem">Add to Calendar</a></li>
<li role="none"><a href="/api/dailyfile/agenda/20361" role="menuitem" class="use-ajax">View Agenda</a></li>
</ul>
</div>
</div>
</td>
</tr>
```
XPath equivalents:
```
//tr[contains(@class,'committee-hearing-details')]//td[contains(@class,'committee_hearing-date')]/@data-sort
//tr[contains(@class,'committee-hearing-details')]//td[contains(@class,'committee_hearing-name')]/@data-sort
//tr[contains(@class,'committee-hearing-details')]//td[contains(@class,'committee_hearing-menu')]//a[starts-with(@href,'/api/dailyfile/agenda/')]/@href
```
### 3.2 Bills on the Floor (`floor-bills-table`)
- **Table selector:** `table.floor-bills-table`
- **Row selector:** `tr.class='floor-bill-details'`
Verified on 2026-09-04: **156 rows**.
| Field | Selector | Best value source |
|---|---|---|
| `bill_number` | `td.floor_bill-measure a` | `attr:href` (parse bill_id; §4) |
| `bill_display` | `td.floor_bill-measure a` | `text` (e.g. `"AB 28"`) |
| `supplement` | `td.floor_bill-measure` | `text` after `<br>` (e.g. `"Supplemental 2"`) |
| `author` | `td.floor_bill-author` | `attr:data-sort` |
| `title` | `td.floor_bill-subject` | `text` (short title / description) |
| `location` | `td.floor_bill-location` | `attr:data-sort` |
| `item_number` | `td.floor_bill-item` | `text` (zero-padded; strip to int) |
| `chamber` | derived | from `bill_id` prefix |
| `date` | **derived** | Not on the row; context is the hearing/date section |
Example row (verbatim, first row):
```html
<tr class="floor-bill-details">
<td class="floor_bill-measure" data-sort="AB 00028">
<a href="https://leginfo.legislature.ca.gov/faces/billNavClient.xhtml?bill_id=202520260AB28" target="_blank">AB 28</a><br>Supplemental 2</td>
<td class="floor_bill-author" data-sort="Schiavo et al.">Schiavo et al.</td>
<td class="floor_bill-subject">An act relating to solid waste, and making an appropriation therefor.</td>
<td class="floor_bill-location" data-sort="Unfinished Business – Concurrence In Senate Amendments">Unfinished Business – Concurrence In Senate Amendments</td>
<td class="floor_bill-item" data-sort="0000032">32</td>
<td class="floor_bill-view-details">
<!-- Modal Dialog --><dialog class="was-dialog floor_bill__modal" data-once="wasDialog">…</dialog>
</td>
</tr>
```
**Note on `td.floor_bill-view-details`:** contains a `<dialog>` modal DOM
widget (close button, SVG, "Details" panel), NOT a data field. **Ignore it.**
XPath equivalents:
```
//tr[contains(@class,'floor-bill-details')]//td[contains(@class,'floor_bill-measure')]/a/@href
//tr[contains(@class,'floor-bill-details')]//td[contains(@class,'floor_bill-author')]/@data-sort
//tr[contains(@class,'floor-bill-details')]//td[contains(@class,'floor_bill-subject')]/text()
//tr[contains(@class,'floor-bill-details')]//td[contains(@class,'floor_bill-item')]/text()
```
### 3.3 Bills in Committee (`committee-bills-table`)
- **Table selector:** `table.committee-bills-table`
- **Row selector:** `tr[class*='committee-bills-details']`
Verified on 2026-09-04: **1 row** (the current live file has few committee
bills — the count varies by day).
| Field | Selector | Best value source |
|---|---|---|
| `bill_number` | `td.committee_bill-number a` | `attr:href` (parse bill_id; §4) |
| `author` | `td.committee_bill-author` | `attr:data-sort` |
| `title` | `td.committee_bill-subject` | `text` |
| `committee` | `td.committee_bill-committee` | `attr:data-sort` |
Example row (verbatim, only row):
```html
<tr class="committee-bills-details">
<td class="committee_bill-number" data-sort="SB 00795">
<a href="https://leginfo.legislature.ca.gov/faces/billNavClient.xhtml?bill_id=202520260SB795" target="_blank">SB 795</a></td>
<td class="committee_bill-author" data-sort="Archuleta">Archuleta</td>
<td class="committee_bill-subject">Air pollution: gasoline vapor control systems: ethanol blends.</td>
<td class="committee_bill-committee" data-sort="Emergency Management">Emergency Management</td>
</tr>
```
---
## 4. Bill ID encoding (universal across all daily-file links)
Every bill link on the Assembly (and Senate) daily file points to a single
canonical pattern:
```
https://leginfo.legislature.ca.gov/faces/billNavClient.xhtml?bill_id={bill_id}
```
`bill_id` decomposes with the regex:
```regex
^(?P<session>\d{8})\d(?P<chamber>AB|SB|ACR|SCR|HR|SJR|ABR|SCR\d+)(?P<number>\d+)$
```
| Component | Example | Meaning |
|---|---|---|
| `session` | `20252026` | `YYYYYYYY` = `YYYY-YYYY` regular session (e.g. `2025-2026`) |
| middle `0` | `0` | constant pad between session and chamber |
| `chamber` | `AB` | `AB`/`ACR`/`HR`/`ABR` = Assembly-origin; `SB`/`SCR`/`SJR` = Senate-origin |
| `number` | `28` | 1–4 digits; **zero-pad to 4** when writing back (`AB 28` ↔ `AB 0028`) |
Worked example:
```
bill_id = 202520260AB28
session = 2025-2026
chamber = AB
number = 28
display = "AB 28" # or "AB 0028" with 4-digit zero-pad
```
This bill_id is the join key to:
- `billNavClient.xhtml?bill_id=…` → status, current version, author(s), short title
- `billHistoryClient.xhtml?bill_id=…` → full action-level history, committee referrals
- `billResultsClient.xhtml?…` → floor / committee agenda views (JS-rendered; use JSON variants)
---
## 5. Senate Daily File — PDF parsing
The Senate Daily File is a **PDF**. There is no HTML table equivalent with the
same structure. Two ingestion options:
### 5a. PDF path (primary)
1. Resolve the current filename from the index HTML
(`samples/senate_dailyfile_index.html`):
```
# Find the most recent PDF link on the index page
grep -oE '/dailyfile/publications/download/SEN[A-Z_]*_[0-9]{4}_R_[0-9]{8}\.PDF' …
```
On 2026-09-04 the only referenced PDF is `SEN_COM_2025_R_20260902.PDF`.
2. Fetch the PDF:
```
https://www.senate.ca.gov/dailyfile/publications/download/SEN_COM_2025_R_20260902.PDF
```
**Antibot mitigation required.** Without a browser challenge solver, this
returns `HTTP 403` with a 67-byte HTML body. See `./skills/blocked-page-recovery`
and `t_f595542f` for the mitigation plan.
3. Extract text (pdfplumber / PyMuPDF), then apply layout heuristics:
- Section headers: `BILLS ON THE FLOOR`, `COMMITTEE HEARINGS`,
`BILLS IN COMMITTEE`, `HELD AT DESK`, `INACTIVE FILE`,
`OFFICERS OF THE SENATE`, `SENATE ORDER OF BUSINESS`.
- Bill-number regex at start of line:
```regex
^(AB|SB|ACR|SCR|HR|SJR|ABR)\s+(\d+)
```
- Following lines carry author, subject, and (for committeee sections)
committee name (`COMMITTEE OF <NAME>`).
### 5b. Leginfo alternative (recommended as backup / enrichment)
`billResultsClient/xhtml` and `billHistoryClient/xhtml` (see §6) give a
structured representation of Senate floor items and committee actions. They
are JS-rendered; use the underlying JSON endpoints (documented by leginfo;
exposed by `billResultsClient.xhtml?…`) for programmatic fetch. Sample:
`samples/leginfo_assembly_floor_2026-04-16.html` (title: "California
Legislative Information"; results injected by JS).
---
## 6. Correlation sources (for §2.5 of the parent — bills ↔ committees ↔ members)
| Concern | URL | Sample |
|---|---|---|
| Assembly committee rosters | `https://www.assembly.ca.gov/committees` | — |
| Senate committee rosters | `https://www.senate.ca.gov/committees` | `samples/senate_committees_index.html` |
| Assembly members | `https://www.assembly.ca.gov/assemblymembers` | — |
| Find-my-rep by address | `https://www.assembly.ca.gov/assemblymembers/find-my-rep` and `https://findyourrep.legislature.ca.gov/` | — |
| Bill detail (author / title / status / committee history) | `https://leginfo.legislature.ca.gov/faces/billNavClient.xhtml?bill_id={bill_id}` | `samples/leginfo_assembly_floor_2026-04-16.html` |
| Bill committee history (action-level) | `https://leginfo.legislature.ca.gov/faces/billHistoryClient.xhtml?bill_id={bill_id}` | — |
### 6.1 Committee rosters — key observation
Senate committee slugs are **URL-encoded names** (no slugs or IDs; the slug IS
the committee name). Example from 2026-09-04:
```
/committees/selectcommitteeonhydrogenenergy
/committees/selectcommitteeoneconomicdevelopmentandtechnologicalinnovation
/committees/senateselectcommitteeonolderlgbtqcalifornians
/committees/offices-caucuses
```
Normalize by title-casing and stripping non-alphanumerics; cross-reference
against the `data-sort` value in `td.committee_hearing-name` and
`td.committee_bill-committee`.
### 6.2 Bill detail — structure observed
From `samples/leginfo_assembly_floor_2026-04-16.html` (a bill-detail page for
`202520260AB28`):
- `<h1 id="bill_nav_title">Bill Text</h1>` — page title
- `.bill_title_yr` — short title + session year (e.g. "Solid waste landfills…")
- Author and committee fields live in the bill-info block (exact selectors to
re-verify at parse-time; this page is enrichment, NOT the daily file source).
---
## 7. Normalised output schema (for `t_fb8081e7`)
One JSON-Lines record per daily-file entry:
```json
{
"id": "sha1(house|bill_number|date|committee)",
"date": "2026-09-02",
"house": "ASSEMBLY", // or SENATE
"source": "ASSEMBLY_DAILY_FILE", // | SENATE_DAILY_FILE_PDF | LEGINFO_BILL_RESULTS
"bill_number": "AB 28",
"bill_id": "202520260AB28",
"session": "2025-2026",
"chamber_origin": "ASSEMBLY", // AB-origin vs SB-origin
"author": "Schiavo et al.",
"short_title": "An act relating to solid waste, and making an appropriation therefor.",
"description": "An act relating to solid waste, and making an appropriation therefor.",
"committee": null, // null for floor items; e.g. "Emergency Management" for committee items
"hearing_time": null, // "9:30am" / "Upon Call of the Chair" for committee-hearings rows
"location": "State Capitol, Room 444",
"item_number": 32, // int, un-padded
"supplement": 2, // optional, from "Supplemental 2"
"ics_url": "https://www.assembly.ca.gov/api/ics/generate?name=Budget&start=…", // where applicable
"agenda_id": "20361" // where applicable
}
```
Field-to-row mapping:
- Floor bill rows: `bill_number` ← `floor_bill-measure a href`, `author` ← `floor_bill-author data-sort`, `title` ← `floor_bill-subject text`, `location` ← `floor_bill-location data-sort`, `item_number` ← `floor_bill-item text` (un-pad). `committee` = null, `hearing_time` = null (unless the bill also appears as a committee item on the same file).
- Committee bill rows: same fields plus `committee` ← `committee_bill-committee data-sort`, `item_number` = null.
- Committee hearing rows: no bill — record the hearing metadata itself (date, time, committee, location, `agenda_id`, `ics_url`).
- Senate (from PDF or leginfo alt): same schema, `source=SENATE_DAILY_FILE_PDF` or `LEGINFO_BILL_RESULTS`.
---
## 8. Pitfalls & risk register
1. **Antibot on Senate PDFs.** `senate.ca.gov/dailyfile/publications/download/…`
behind a `form.antibot` challenge. Programmatic `requests`/browser JS fetch
yields `HTTP 403` with a 67-byte HTML body (verified 2026-09-04). Mitigation:
headless browser with a cookie/session pre-warm, or route through the
`blocked-page-recovery` skill.
2. **No historical date param.** The Assembly page is always current-live.
"Date" is read from data (hearing table), not written into the URL.
3. **404 ≠ error.** Non-legislative days legitimately return 404 on the Senate
PDF index. The ingestion layer must classify 404 as "no file today" and
5xx / retry-able as a transient error.
4. **Session boundary.** When the session changes in January, the URL pattern
(`bill_id` = `YYYYYYYY` + …) stays the same but bills renumber. No code
change required — the `session` field in the output reflects the current
value read from `bill_id`.
5. **Data-sort > textContent.** Always prefer the `data-sort` attribute; it
is the clean canonical value. `textContent` carries display suffixes and
leading zeros.
6. **Modal widget in floor-bill rows.** `td.floor_bill-view-details` holds a
`<dialog>`; do NOT extract it as a field.
7. **Cross-chamber bills.** `AB`-prefilled rows can appear on a Senate daily
file and vice-versa. `chamber_origin` means *origin*, not publishing house.
8. **Supplement tracking.** A bill can have multiple supplements
(`Supplemental 2`); the `supplement` field is the edition.
9. **JS-rendered leginfo pages.** `billResultsClient` and `billHistoryClient`
need a headless browser or JSON endpoint; the static `<table>` they render
is NOT in the initial HTML.
---
## 9. Sample archive
```
samples/
assembly_dailyfile_latest.html # 401 192 bytes, 2026-09-04
# 4 hearing rows, 156 floor-bill rows, 1 committee-bills row
senate_dailyfile_index.html # 34 137 bytes, pdf-viewer index
# links to SEN_COM_2025_R_20260902.PDF
senate_committees_index.html # 58 642 bytes, Senate committee slugs
leginfo_assembly_floor_2026-04-16.html # 163 825 bytes, leginfo results page
# billResultsClient, JS-rendered
```
These are the offline test fixtures for `t_fb8081e7`. Every sample was
fetched via `curl` (assembly index, senate index, committee index, leginfo
floor) or `browser_exec` fetch (leginfo floor). The Senate PDF sample is NOT
archived because it is behind antibot — that is itself a documented finding.
See §10 for the acceptance test harness the parser task should run against
these files.
---
## 10. Acceptance test harness (for `t_fb8081e7`)
```yaml
- id: asm_hearings
input: samples/assembly_dailyfile_latest.html
selector: tr[class*='committee-hearing-details']
expect_min_rows: 1
verified: 4
- id: asm_floor_bills
input: samples/assembly_dailyfile_latest.html
selector: tr.class='floor-bill-details'
expect_min_rows: 100
expect_fields: [bill_number, author, short_title, item_number]
verified: 156
- id: asm_committee_bills
input: samples/assembly_dailyfile_latest.html
selector: tr[class*='committee-bills-details']
expect_min_rows: 1
verified: 1
- id: bill_id_parse
regex: "^(?P<session>\d{8})\d(?P<chamber>AB|SB|ACR|SCR|HR|SJR|ABR)(?P<number>\d+)$"
example_url: "https://leginfo.legislature.ca.gov/faces/billNavClient.xhtml?bill_id=202520260AB28"
expect: {session: "2025-2026", chamber: "AB", number: 28, display: "AB 28"}
verified: pass
- id: senate_pdf_index
input: samples/senate_dailyfile_index.html
expect: "at least one /dailyfile/publications/download/*.PDF reference"
verified: 1 (SEN_COM_2025_R_20260902.PDF)
```
---
**Downstream handoff:**
- `t_f595542f` (ingestion): read `daily_file_sources.yaml` for URL patterns +
parameters + antibot flags. Add a Senate PDF download step with a
`blocked-page-recovery` fallback for 403s.
- `t_fb8081e7` (parser): the §3 and §5 tables are the exact selector / XPath
/ regex contracts. Use `data-sort` over `textContent`. Skip
`td.floor_bill-view-details`.
- `t_8e5a3821` (RDB schema): the §7 per-record schema is the target. Foreign
keys to `committee_roster`, `bill_detail`, `bill_history` tables (see §6).