BALLOT BRIEF

Teaching 7,515 candidate case files to tell the truth about severity — without inventing a scale the law never wrote.

Client

Self

DELIVERABLES

Two-axis severity taxonomy Full-stack React build Civic data UX UX case study

Role

Solo product designer & full-stack build

Case file — civic tech / legal data UX
GraveSerious

Ballot Brief

Teaching 7,515 candidate case files to tell the truth about severity — without inventing a scale the law never wrote.

Role
Solo product designer & full-stack build
Domain
Civic tech · legal data · India
Stack
React · Cloudflare D1 · Pages Functions
Data
7,515 candidates · myneta.info / ADR

A raw number — "12 pending cases" — tells a voter nothing.

Since 2003, Indian election law has required every candidate to swear an affidavit disclosing criminal cases, assets, and liabilities — a real transparency win, made public through ADR and myneta.info. But the data is published as raw legal text and aggregate counts, never classified. An affidavit lists IPC section numbers — "323, 506, 149" — not what they mean; a voter without legal training has no way to know that one of those is a minor offense and another is a serious one, so the actual severity of an alleged crime gets lost in translation before it ever reaches the person it's meant to inform.

Existing tools, including the Election Commission's own "Know Your Candidate," surface the disclosure; none of them interpret it. The gap voters actually face isn't access — it's translation. And translation done carelessly, by inventing an arbitrary "danger score," would just replace one kind of opacity with another. Ballot Brief's working thesis: grade every case against the law's own structure — the CrPC's bail and cognizance rules, the IPC's chapter taxonomy — not a number made up to sound authoritative.

Exhibit A

Discover

Before building anything: does this already exist, what data is actually available, and what's the legal terrain to build inside?

Comparator research

Five domestic and international projects were reviewed against the same question: does any of them combine structured legal case data with a plain-language severity explainer, grounded in the law's own taxonomy rather than an invented one?

Project
What it does
What's missing
ECI Know Your Candidate
Official government disclosure lookup — the legitimacy anchor; EC already mandates this disclosure.
Raw affidavit data only. No classification, no severity context.
Brazil — DivulgaCand / Ficha Limpa
Closest international parallel; "clean record" law with an 8-year ineligibility on conviction, even pending appeal.
Candidate documents are scanned PDFs — not structured, not analyzable.
Philippines — VERA Files
Rigorous promise-tracking methodology (verbatim quote + sourced evidence per claim).
Different domain — tracks promises, not criminal disclosure.
US — Ballotpedia / Vote Smart
Strong UI/UX reference for candidate-profile information design.
Different disclosure norms — doesn't track criminal cases at all.
UK — Democracy Club / mySociety
Open civic-tech nonprofit operating model.
Same — no criminal-case disclosure equivalent in UK electoral law.
White space confirmed

Data-source diligence

Before committing to any feature, every prospective data source was checked for an actual API, license terms, and rate-limit risk at scale.

Source
Use case
Finding
myneta.info / ADR
Core candidate, case & asset data
Primary source. Already integrated — 7,515 candidates scraped & verified.
NJDG (eCourts)
Court-pendency context
No public self-serve API — restricted to government credentials. Deprioritized.
Google News RSS
Related news coverage
Free, no key — but a ToS grey zone with no rate-limit guarantee at 8,000+ candidates.
PRS Legislative Research
MP performance data
No API, but CC-BY-4.0 licensed — scraping is legally clear. Worth a direct data request.

Stress-testing the idea before scaling it

Three candidates were selected — by a fixed, neutral method (next page of myneta's own case-sorted list, first matching row), never by searching for a specific allegation — to deliberately exercise edge cases before any classification logic touched real scale.

Volume stress-test

K. Surendran

243 filed cases — mostly protest-cluster charges, with a real attempt-murder/dacoity tail. Tests whether the taxonomy holds up under sheer case volume.

Density stress-test

Athram Suguna

49 cases, smaller count but higher serious-tier density — surfaced the need for a Special Legislation tier sitting outside the ordinary IPC scale (a UAPA case).

Edge-case exercise

Sukanta Majumdar

Added specifically to exercise the Crime Against Women / Sexual sub-tag (a case citing IPC §509) — found via the same neutral method, not targeted.

Exhibit B

Define

The first design mistake, caught in early critique: collapsing severity into one score. A case needed two independent answers, not one number.

Axis 1 / Severity tier
  • Grounded in CrPC bail & cognizance rules — not an invented scale
  • Special Legislation (UAPA, etc.) sits in its own tier, outside the ordinary IPC scale entirely
  • Liability modifiers (149, 34, 120B, 511) are stripped before classification — they attach to a charge, they aren't one
  • Tiered by the single highest-severity substantive section in a multi-section case row, never an average
Axis 2 / Offense-type bucket
  • Fixed lists of IPC sections per bucket, grounded in the IPC's own chapter structure
  • Murder & Attempted Murder, Crime Against Women, Robbery & Dacoity, Hurt & Assault, and 12 more
  • Crime Against Women further splits Sexual / Non-sexual — sourced from affidavit text, never assumed
  • Independent of severity tier — a bucket says what kind, a tier says how grave
"243 cases" can't tell you if that's mostly protest charges or mostly violent crime. Both axes are needed before either number means anything.

The edge case that defined the display ethic

A pending criminal case is an allegation, not a finding of fact — and that distinction has to hold regardless of how serious the alleged conduct is. So the system was built to apply one rule everywhere, without exception: classify strictly from what the source document says, and let severity and offense-type do all of the signaling — never let the interface itself add a second, unstated layer of judgment on top. IPC §366 — kidnapping or abducting a woman to compel marriage — is the case that put this rule to a real test. The section's text alone doesn't specify whether sexual intent is involved, so a tagging rule was needed: a case is only marked Sexual if the source affidavit explicitly states "illicit intercourse" — tagged from the document's own language, never inferred. And once tagged, that case is still rendered through the same severity and offense-type system as every other Serious-tier case — accurately labeled, fully visible in its own right, with no separate visual emphasis bolted on beyond what the facts in the record already establish. The system's job is to classify precisely and disclose fully; deciding how alarmed a reader should feel about an unproven allegation isn't a judgment the interface gets to make.

House rules (apply to every feature built after this point)

  1. Naming avoids editorializing. "Criminal politician" became "Daily Case Spotlight." Names describe the mechanism, never render a verdict.
  2. Charges ≠ convictions. Every surface touching criminal cases carries an explicit, visible disclaimer — pending cases are allegations, not adjudicated guilt.
  3. Verbatim over paraphrase, for legal safety. Exact quotes with context, not summaries — accuracy is the strongest defense against defamation exposure.
  4. Bias-blindness is designed in, not assumed. Sourcing and selection logic apply uniformly regardless of party — flagged repeatedly as the single biggest credibility risk in the whole project.
  5. Unverified ≠ guessed. Any legal fact not yet checked against a primary source is marked verified:false and surfaced in the UI — never silently defaulted to a "safe" assumption.
Exhibit C

Develop

Three real builds, not one — a hardcoded prototype, a scale-up to a real backend, and a full visual redesign, each correcting something the last one got wrong.

Stage 1 — Prototype
Three candidates, hardcoded, dark ledger aesthetic
Vite + React + Tailwind, no backend — three candidates inside a flat array, used purely to stress-test the taxonomy (Exhibit A). A 3-view state machine (home / party / candidate), deliberately built without a router library — overkill for three candidates, but structured to be router-ready the moment it needed to be.
Stage 2 — Scale-up
7,515 real candidates, Cloudflare D1
A Python scraper against myneta.info, feeding a 7-table Cloudflare D1 schema, served through four Pages Functions endpoints (candidate profile, search, by-party, party-stats). The 3-candidate demo's classification logic carried over unchanged — the taxonomy didn't need to change, only its data source did.
Stage 3 — Redesign
Light-theme rebuild, severity ramp under real scrutiny
A full visual redesign (Claude Design → React) reconciling new components against real backend field names — and the moment the classification logic met real long-tail data, it broke in instructive ways (below).

The severity ramp couldn't survive contact with real data

The redesign launched with four tiers — Minor, Moderate, Serious, Grave. Reviewing real case records immediately surfaced a problem: armed rioting and weapon-involved hurt were landing in "Moderate." That reads as far too lenient for anyone actually holding a weapon during an offense.

Rioting, armed with a deadly weapon — moved to Grave
IPC §148 · was Moderate
Causing hurt by a dangerous weapon — moved to Grave
IPC §324 · was Serious
Plain rioting, simple hurt — stayed Serious
IPC §147 / §323 · deliberately not escalated

The fix wasn't a fourth tier — it was collapsing to two (Serious / Grave, plus an honest Not Determinable) and defining an explicit rule: a case is Grave if it's life/death-eligible, or involves a weapon, or involves grievous injury — regardless of what a formal sentencing-years field happens to say. The years field measures an offence's ceiling, not how grave the act actually was; an attempted-murder charge shouldn't read as lesser than a completed one just because the attempt's maximum sentence is shorter on paper.

Open risk, disclosed

Closing that gap surfaced a sharper one: IPC §303 — punishment for murder by a person already under a life sentence — was struck down as unconstitutional in 1983 (Mithu v. State of Punjab). Its replacement, BNS §104, uses an entirely different numbering scheme that happens to share section numbers with unrelated, still-active IPC provisions. §303 → §104 is safe to map today because old IPC §104 is a private-defence clause nobody would cite as a charge — but that's a narrow escape, not a structural fix. Logged as an explicit open risk rather than quietly patched and forgotten, in keeping with the project's own 'unverified ≠ guessed' rule — applied here to itself.

Visual system, rebuilt around legibility

Color
A "Grave" that actually reads as danger
The redesign's first pass at the Grave color was a muted brownish-red — critiqued as not carrying enough alarm for what it represents. Deepened to a true, saturated danger red, while Serious stayed a calmer amber — the contrast between the two now does real signaling work.
Identity system
Real party symbols, honest fallback for the rest
16 parties get their real, sourced symbol. The other ~52 — including every independent and minor party — get a deterministic, hashed-color identity tile instead of a generic placeholder, so coverage gaps read as a design choice, not a bug.
Information density
Bringing legibility back after the redesign over-simplified it
The new visual language initially dropped detail the original case-ledger view had — raw cited sections, "other acts," and a classification transparency line. Restored deliberately: a severity badge is only trustworthy if you can see the reasoning behind it.
Engineering hardening
Mobile responsiveness, debugged systematically
A multi-round mobile audit (header overflow, a flex input's intrinsic min-width, finally an implicit CSS Grid track-sizing default present in every grid across the app) — resolved by auditing and fixing all of them in one pass rather than patching each report individually.

Two earlier lessons that changed how the project is run

Lesson — silent failure
A build can succeed and still ship a blank page
An undefined constant reference crashed the app on render — Vite's build only catches syntax errors, not undefined-variable runtime errors, so npm run build reported success while the live page was blank. A static "every identifier used has a matching declaration" check is now run before every handoff, not just a build pass.
Lesson — process, not just code
Two unsynced editing surfaces will eventually diverge
Editing both locally (via git) and directly through GitHub's web editor, without ever syncing between them, produced a real branch divergence and a merge conflict. Resolved, and the workflow itself was changed afterward to close the gap rather than just patching that one conflict.
Exhibit D

Deliver

What's actually live, and — in the same spirit as the project's own disclosure ethic — what isn't finished yet.

Shipped
Four real screens
Overview, candidate search, candidate profile with full case-record breakdown, and a party leaderboard — all reading from live Cloudflare D1, not demo data.
Shipped
Full dataset, real scale
7,515 candidates, 543 seats, 68 parties — the entire Lok Sabha 2024 field, not a curated sample.
Shipped
Deployed, mobile-hardened
Live on Cloudflare Pages with GitHub auto-deploy, audited and fixed for mobile after a genuine multi-round debugging pass.

Disclosed, not hidden

Limitation
What it actually means
Partial IPC verification
Most sections are well-established legal knowledge but individually marked verified:false until checked against the CrPC First Schedule directly — surfaced live in the UI, not hidden.
Party leaderboard excludes small parties
The live endpoint filters to parties with 10+ candidates so percentages stay statistically meaningful — meaning a true "all 68 parties" view needs a deliberate backend change, not a frontend patch.
Search results lack per-case severity
The search endpoint returns a case count, not classified case data — so the rich severity bar shown on a candidate profile can't yet render in the search grid.
IPC/BNS numbering collision
The single resolved case (§303 → §104) worked because of a narrow coincidence, not a structural safeguard — logged as an open risk for any future BNS-era case.
Case assessment

SWOT

A reflexive read on the project as it stands — not a sales pitch.

Strengths
  • A legally-grounded, two-axis taxonomy — not an invented severity scale
  • Real, verified dataset at full national scale (7,515 candidates)
  • An honesty ethic that's structural, not aspirational — unverified facts are marked, not hidden
  • Confirmed white space against every researched domestic and international comparator
Weaknesses
  • IPC reference table only partially checked against the primary legal source
  • Severity-tier logic is a documented placeholder heuristic, not yet a verified legal mapping
  • No automated test coverage — regressions have been caught by manual review
  • Solo-maintained — every fix this far has gone through one person
Opportunities
  • A genuine differentiator nothing else researched combines: structured legal data + plain-language severity + a neutral taxonomy
  • PRS Legislative Research data is CC-BY-4.0 — legally clear path to an MP Performance Card
  • Multi-cycle myneta data already supports an Asset Growth Tracker with low added engineering cost
  • Multilingual rollout staged sensibly (UI shell → labeled machine translation → full localization by proven traffic)
Threats
  • Public Statement Tracker concept carries real defamation/hate-speech exposure — flagged for legal review before being built, not after
  • Google News RSS sits in a ToS grey zone at this scale; no production SLA
  • Severity-weighted feature selection (e.g. Daily Case Spotlight) reopens an editorial-targeting risk the rest of the project works hard to avoid
  • An election-cycle-specific tool needs a real re-scraping commitment to stay relevant past 2024
Appendix

Roadmap & future filings

A full feature-brainstorm session, mapped on Miro, evaluated against the same house rules established in Exhibit B. View the live board →

Daily Case Spotlight
  • Severity-weighted daily rotation, plain-language summary, mandatory 'not a conviction' disclaimer
  • Dispute/correction flagging built in from day one
Editorial-targeting risk
Related News Coverage
  • Case-specific matching (name + section + year — disambiguation matters with common names)
  • Source-tier labeling, status-override banner if a case is later disposed
Public Statement Tracker
  • Verbatim quote + full context, primary-source priority over paraphrase
  • Confirmed / Reported / Disputed verification tiering
Legal review recommended
Promise Tracker (Central, ruling party only)
  • Status taxonomy borrowed from VERA Files, including an honest 'Not Yet Rated' category
  • State-level and opposition tracking explicitly deferred — not rejected, just scoped out of v1
No automation shortcut
MP Performance Card
  • Attendance, bills, MPLADS utilization via PRS Legislative Research (CC-BY-4.0)
  • Plain-language synthesis + percentile ranking vs. all 543 MPs
Most build-ready
Asset Growth Tracker
  • Net-worth trend across 2014 → 2019 → 2024 using existing multi-cycle myneta data
  • Explicit non-accusatory framing — growth ≠ proof of wrongdoing
Low engineering cost
Defection History
  • Factual party-change timeline, sourced from ECI records + news confirmation
Low difficulty — facts only
Civic Engagement
  • Share candidate card, polling reminders, watchlist, contact-my-MP, local issue reporting
  • Moderated discussion threads flagged as hardest to build well at any scale
Civic Awareness
  • First-time voter guide, 'how Parliament works,' myth-busting, election glossary
  • Distinct from Engagement — this branch is about understanding, not acting

Every feature above is evaluated against the same rules set in Exhibit B: names describe the mechanism, not a verdict; charges are never displayed without a conviction disclaimer; sourcing pipelines apply uniformly regardless of party; and dispute-correction flagging isn't a one-off feature, it's a standing safeguard attached to anything that could misrepresent a real person.