Replace the bill article wall of text with a structured brief#198
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The article pipeline emitted one ~900-word markdown blob per item. The prose was careful — nonpartisan framing, sponsor claims treated as intent, removed oversight named rather than called "streamlining" — but structurally it gave the reader one decision: read all of it, or none of it. Nothing was scannable, everything rendered at equal weight, and quotes were just italic text that nothing verified. A brief is the same analysis stored as typed pieces instead of prose: a hook, stat tiles, before/after changes, affected groups, unknowns, a glossary, and optional long-form sections. The reader can stop at any depth and still have learned something true. Editorial guarantees move into the types rather than being trusted to a prompt: - `kind` on a change is mechanical (creates/repeals/waives/funds), never evaluative — a brief can say what a provision does, not whether it's good. - `before`/`after` forces current law to be stated separately from the change. - `direction` on an affected group tops out at mixed/unclear, so the model can decline to score a group instead of manufacturing symmetry. - `unknowns` is non-empty: no brief can claim to have settled everything. - Legal status is derived from the scraped bill status, so "would" vs "does" comes from a string match rather than an inference. Generation is three steps, one of which costs a token. The LLM structures the brief (grounded in the official text plus the existing vetted article), then two deterministic passes run: every quote is checked verbatim against the whole source document and stripped if it doesn't match — the claim survives, the citation doesn't — and loaded political vocabulary in the model's own voice triggers one regeneration. Quotes are exempt from the lint: a sponsor may call their bill "common sense", and quoting that is reporting. Argument-level framing deliberately stays in the existing cited dual-lens. Briefs describe mechanism; the lens carries the debate. Bills only. The schema is written around legislative mechanics, and executive actions and court cases each need their own design pass — `content_brief` already carries a contentType, so adding one is a generator, not a migration. Content without a brief keeps rendering the markdown article. Two adjacent fixes found while verifying this: - `pnpm db:seed` had been failing outright: the fixture bill descriptions exceed the table's 100-char check constraint. Clamped with the same helper the scraper and API use. - `pnpm test` in the scraper globbed `src/**/*.test.ts` unquoted, so the shell expanded it one level deep and silently ran 2 of 5 test files. Quoted, so Node's runner does the globbing: 23 tests now discovered instead of 7. Co-Authored-By: Claude <noreply@anthropic.com>
8ebdb97 to
a3b4370
Compare
Rich article interaction updateA second simulator pass covers the changes from the latest review:
Verified locally on an iPhone 17 Pro simulator (iOS 26.5) against the refreshed seed data. The full official-site exit and Dual-Lens source list remain available, keeping the generated brief a gateway to primary and further reading rather than a replacement for it. |
|
@warrensuca thoughts on the UI @junj-st thoughts on the article design @yohaann196 @icwtyjj review the backend code |
Final article polish + app-wide detail passPushed in
Also verified on the iPhone 17 Pro simulator that View source opens Original text at the matching highlighted passage, the Original text tab no longer repeats the bottom source CTA, and guest Settings no longer triggers an unauthorized request. Validation: Expo typecheck, lint, and formatting; scraper typecheck and 26/26 tests; validators and database typechecks; relevant formatting checks. |
|
Moved Dual Lens below the core article content (after the change, affected-groups, and open-questions sections) and before Keep reading. This keeps the primary explanation uninterrupted while preserving the perspective comparison and follow-on sources. Pushed in |
Cited “Why wasn’t this implemented before?” contextPushed in This adds an expandable historical-context card directly below What this means for you. The collapsed state gives a short answer; expanding it shows documented barriers or earlier attempts, numbered citations beside each claim, and tappable links to the original publishers. The generation pipeline is now explicitly hybrid:
The seeded infrastructure and privacy examples now include cited context from Congressional Research Service material, and the brief schema is bumped to v6 so cached rows regenerate. Validation: local seed refresh; Expo typecheck/lint/format; scraper typecheck and 27/27 tests; validators/database typechecks and formatting. I did not attach a new simulator capture to this update because the local Simulator accessibility bridge timed out repeatedly during the final visual pass; the component itself compiled and the seeded data path was refreshed successfully. |
|
Updated What would change in |
|
Added backwards compatibility in |
|
Fixed the missing second-card details in Root cause: change visuals and source quotes are intentionally optional so the generator does not invent evidence or force irrelevant artwork. The seeded second privacy change omitted both fields even though this example supports them. Updated:
Verified locally: refreshed both seeded briefs; Simulator exposes the second image/caption and |
|
Updated the Who it lands on cards in
Verified in iPhone 17 Pro Simulator. Expo typecheck, lint, and formatting all pass. |
|
Updated generated-article emphasis and fixed the takeaway clipping in Brief-wide emphasis
Clipping fix
Verified locally
|
|
Refined the Who it lands on typography hierarchy in
Verified in iPhone 17 Pro Simulator with refreshed v7 seeds. Full takeaways wrap cleanly and the four text levels are visibly distinct. Expo typecheck/lint/format and database type/format checks pass. |
|
Implemented the Dual-Lens grounding follow-up in What changed:
I also corrected the privacy seed so the relationship is explicit:
Validation: Expo typecheck + lint, scraper typecheck + 30 tests, DB typecheck, and local seed refresh all pass. I did not attach the earlier Simulator capture because its copy predates this semantic correction; the final screenshot should be taken from this commit. |
|
Follow-up pushed in Each newly generated Dual-Lens example must contain:
The synthesis prompt explicitly rejects adjacent facts, generic “this is relevant” language, and repeated claims. The UI renders the relevance explanation as its own WHAT IT SHOWS block. Legacy string examples remain readable, and the lens cache contract is bumped so previously generated lenses refresh. The corrected opponent seed now says:
Validation remains green: Expo typecheck/lint, scraper typecheck and 30 tests, DB typecheck, and local seed refresh. |
|
Proper text-wrapping fix pushed in Root cause addressed: the affected-card prose lived under a column child using The fix:
Validation: Expo typecheck and lint pass. Visual confirmation can now use the original long privacy strings without any hand-inserted line breaks. |
|
The screenshot exposed the remaining root cause; fixed in The first takeaway was the only one whose bold phrase had to continue onto another line. The shared emphasis renderer now keeps the paragraph’s inherited font family and applies Expo typecheck and lint pass. |
|
Correction pushed in The final approach restores This addresses both failures together: long bold phrases can continue onto another line, and the bold styling remains visibly intact. Expo typecheck and lint pass. |



















The problem
The article pipeline emits one ~900-word markdown blob per item — four
##sections rendered as-is. The prose is genuinely careful: nonpartisan framing, sponsor claims treated as intent rather than results, removed oversight named rather than called "streamlining."The problem is structural, not editorial:
A reader who bounces off paragraph two leaves with nothing — not even curiosity. That's the Bradbury Principle failing in the most ordinary way.
The brief
Same analysis, stored as typed pieces instead of prose (
@acme/validators):hookfactschangesbefore→afteraffecteddirectionunknownstermssectionsThe reader can stop after the hook, the tiles, the changes, or go all the way into the source text, and every stopping point is coherent.
Nonpartisan guarantees live in the types
Rather than being trusted to a prompt:
kindis mechanical, never evaluative —creates/repeals/expands/restricts/requires/waives/funds/transfers. A brief can say what a provision does; it has no vocabulary for saying whether that's good.before/aftersplits current law from the change — that blur is where accessible summaries usually go wrong.directiontops out atmixed/unclear— the model can decline to score a group instead of inventing a counterweight to balance the list.unknownsis non-empty — there's no valid brief that claims to have settled everything.deriveLegalStatus()reads the scraped status, so "would" vs "does" comes from a string match rather than an inference.Argument-level framing deliberately stays in the existing cited dual-lens. Briefs describe mechanism; the lens carries the debate. Keeping them separate means the factual layer can't drift into argument.
Citing original sources
Generation is three steps, only one of which costs a token:
common sense,radical,landmark,handout,job-killing,red tape) in the model's own prose triggers one regeneration. Quotes are exempt: a sponsor may call their bill "common sense," and quoting that is reporting.Rendering
BillBrief.tsxrenders each block as the UI element it actually is. Two brand constraints:Scope
Bills only. The schema is written around legislative mechanics.
content_briefalready carries acontentType, so adding executive actions or court cases is a generator and a schema pass, not a migration. Content without a brief keeps rendering the markdown article — this is additive, not a cutover.Verification
verifyBriefQuotesagainst the bill's storedfullText, the framing lint is clean, andcontent.getByIdactually serves the brief through tRPC.Rollout
New and changed bills get briefs automatically. For existing rows:
pnpm db:seedalso writes two example briefs, so the UI is visible locally without an LLM key.Design writeup:
docs/article-generation.md.Rebased onto #199, which landed the two unrelated bugs found while verifying this (broken seed, silent test discovery).
🤖 Generated with Claude Code