The Invisible Half
A judge-safe editorial product that audits the coverage debt America owes its Paralympians. Paralympic-first by design — in ordering, in prompts, in lint rules, and on screen.
Tagline: Parity is a verb.
## Inspiration
Every four years, America cheers Team USA. Then the cameras leave — and a second team takes the field. Same flag, same training centers, same medals. A fraction of the coverage.
We talked to fans like Maya in Tulsa, who told us: "I cheered for Team USA all summer. I couldn't name one Paralympian." That isn't apathy. It's a coverage debt — and nobody had ever audited it. So we built the audit.
The five-second differentiator: every competing entry opens with a split-screen Olympic/Paralympic header. The Invisible Half opens with a single Paralympic-first editorial story, and the Olympic comparison appears second — as evidence, not as the default frame.
"You've been seeing the other half your whole life. Here's the half you missed."
## What it does
The Invisible Half is a judge-safe editorial product — not a dashboard — that quantifies how much press America owes its Paralympic medalists.
The headline number on the homepage is the Coverage Debt Counter: the audited total of news articles owed but never written. The lead metric is the Coverage Debt Ratio (CDR):
CDR = news_articles_per_Olympic_medal / news_articles_per_Paralympic_medal
A CDR of 12.4 for Para-snowboard means a Paralympic medal generated one-twelfth the press of an Olympic medal in the same sport. The number is computed deterministically from a curated corpus, cross-checked against a live GDELT 2.0 BigQuery measurement, and every input is inspectable.
What a visitor can do:
- Read the Paralympic-first lead story (Para-snowboard, Beijing 2022) — no Olympic split-screen by design
- Browse the Hometown Twin Story — anonymized pairs of Olympic and Paralympic athletes from the same American town
- Open any of seven sports across two Games, every row rendered Paralympic-on-top by a single
<ParityList>component - Click into the evidence ledger on each sport page — article counts, medal counts, the computed ratio, and clickable source URLs
- Visit
/debug— the live audit surface showing rawParityAgentJSON, trace ID, schema, and citations - Visit
/methodology— the judge-facing page comparing the conservative curated corpus to the live GDELT measurement
Two more metrics are on the documented roadmap: Hometown Echo Index (HEI) — local-paper mentions normalized by US Census ACS town population — and Milestone Lag (ML) — days between an Olympic "first" being mainstream-reported vs. its Paralympic equivalent.
## How we built it
Frontend — Next.js 15 on Cloud Run. Editorial layout: serif type, cream + ink palette, scroll-driven story. The word Dashboard is banned in the codebase via ESLint.
Orchestrator — FastAPI on Cloud Run, IAM-internal. Three ADK agents ship every response through a locked PARITY_SCHEMA:
| Agent | Role |
|---|---|
| ParityAgent | Deterministic CDR — computed before any language model touches the screen |
| MilestoneAgent | Production-safe anonymized twin-pair JSON surface; long-context corpus detection is the next production step |
| NewsAgent | Gemini's Google Search grounding tool by default; Vertex AI Search over a parity-corpus datastore is the roadmap path |
Data — BigQuery (parity.olympic_athletes, paralympic_results, coverage_corpus). Optional Firestore cache for metrics and twin pairs. The deployed
coverage corpus is a hand-curated 34-row seed (17 sport × cycle pairs × 2 movements), cross-checked against a live GDELT 2.0 query for methodology validation.
Imagery — Imagen 3 (imagen-3.0-generate-001), build-time only. Abstract silhouettes, podium geometry, coverage-debt heat blobs. Zero real people.
Auditability — every agent call writes to an in-memory trace store keyed by UUID; the response carries an x-trace-id header so /debug/trace/{id} returns
the audit-friendly step list.
Mechanical compliance guards — every rule that could disqualify the entry is enforced in code, not by convention:
| Rule | Guard |
|---|---|
| No NIL in source data | enforce_column_allowlist() drops name / birthdate / hometown before BigQuery insert; redact_pii() masks athlete-honorific
patterns from /debug |
| No real people in generated images | imagen_review.build_prompt() raises on subjects outside a 3-item allowlist |
| No "former Olympian" / "past Paralympian" | ESLint no-restricted-syntax rule |
| No NGB names, no "Los Angeles 2028", no Olympic IP, no Dashboard | ESLint banlist |
| Winter terminology | gamesName(sport, movement) picks Olympic Winter Games for snowboard / alpine |
| Paralympic-first ordering | <ParityList> is the sole source of truth — tests assert DOM order |
## Challenges we ran into
NIL compliance. One generated face, one stray athlete name, and we're disqualified. We answered it mechanically — Imagen prompt allowlist, column allowlist
before BigQuery insert, PII redaction on every /debug snippet, and ESLint rules that reject "former Olympian" or NGB names anywhere in the codebase.
Avoiding the "obvious dashboard" trap. Competing entries lead with side-by-side charts. We made the word Dashboard fail lint. The first frame is a headline.
Making the metric defensible. GDELT 2.0 doesn't cleanly separate Olympic from Paralympic coverage — the live measurement overcounts Olympic and
undercounts Paralympic. So we ship a curated seed as the conservative number, and put the live measurement next to it on /methodology for judges to
audit independently.
Schema discipline. Locking PARITY_SCHEMA early — and rejecting any agent response that doesn't conform — forced every downstream UI decision to be
deterministic and auditable.
Devil's-advocate gates. Design, backend, and integration audits were BLOCKING — implementation did not advance until each verdict flipped to PASS.
## Accomplishments that we're proud of
- A product whose first frame is a headline, not a chart — and which never uses the word Dashboard
- A single React component (
<ParityList>) that enforces Paralympic-first ordering across the entire site, in one auditable line - A deterministic CDR computed before any LLM touches the screen, so the headline number is reproducible
- A live GDELT 2.0 validation that confirms and amplifies the curated seed gap (median live ratio ~130×, median curated ~7.7×)
- Mechanical NIL / IP / terminology guards in code — not in a style guide
- Every devil's-advocate audit flipped from BLOCK to PASS before submission
- A judge-friendly
/debugsurface where every formula input and evidence URL is one click away
## What we learned
Parity isn't a chart you draw once. It's a discipline you encode — in reading order, in lint rules, in Imagen prompts, in column allowlists, in what the first frame chooses to show.
We also learned that conservative beats dramatic when judges can audit your inputs. The smaller, defensible number — backed by visible source URLs and a live cross-check — is more persuasive than a larger one without provenance.
And: hard compliance rules are easier to keep when a linter enforces them than when a team remembers them.
## What's next for Invisible Half
- Hometown Echo Index (HEI) — local-paper mentions of returning medalists within 30 days, normalized by US Census ACS 5-year town population
- Milestone Lag (ML) — days between an Olympic "first" being mainstream-reported and its Paralympic equivalent, detected with long-context (1M) over the full historical corpus
- Vertex AI Search grounding — promote the
parity-corpusdatastore (teamusa.com + curated news) from documented roadmap to deployed default; switch is a single env-var change - Refreshed BigQuery corpus — replace the seeded article-count aggregates with a refreshed pull, then enable the Firestore counter as a fully live metric
- The LA28 frame — turn the counter from a snapshot into a live ledger every American can watch tick down
## License
Apache-2.0
Log in or sign up for Devpost to join the conversation.