party-retrospect · git:20260427.43a7459 · 2026-04-27 · sha256 a5828cd32b6ce427
party-retrospect git:20260427.43a7459A
Immutable. This exact content never changes and is served at /api/v1/blob/a5828cd32b6ce427.
---
name: party-retrospect
description: Generate the AI campaign retrospect for one or more parties in one election. Bundles the party's voting, regional, polling, and (if available) financing data into a single input, then writes a markdown analysis to public/{election}/parties/assessment/{partyNum}.json. Use when the user asks to generate, refresh, or write a campaign retrospect / strategic analysis / next-election advice for a party. Also use after a new election when adding retrospects for the latest cycle.
allowed-tools:
- Read
- Bash
- Write
---
# Party Retrospect skill
Generates the per-party "Campaign retrospect" markdown shown on `/party/{nickName}` dashboards. The output is a static JSON file (cached per party per election), so the site has zero LLM cost at runtime.
## Pipeline overview
```
scripts/parties/bundle_party_data.ts → builds a structured input bundle from public/*.json
(deterministic — vote counts, deltas, polling errors, etc.)
↓
Claude reads the bundle → writes BG + EN markdown bodies (THIS skill, preferred path)
OR
scripts/parties/generate_retrospect.ts → calls Gemini with the bundle (fallback)
↓
public/{election}/parties/assessment/{partyNum}.json → consumed by PartyAssessmentTile
```
**Output file shape** (consumed by `src/screens/dashboard/PartyAssessmentTile.tsx`):
```json
{
"generatedAt": "<ISO timestamp>",
"model": "Claude Opus 4.7 (1M context)",
"partyNum": 18,
"nickName": "ГЕРБ-СДС",
"bg": "## Резултат на изборите\n\n...markdown...",
"en": "## Headline result\n\n...markdown..."
}
```
## When to run this
- **A new election just landed and you want retrospects for the leading parties** — generate for every party at ≥4% (or ≥2% if you want minor parties too).
- **A specific party's retrospect needs refresh** — e.g. polling data was updated, or financing data became available after the initial generation.
- **You added a new party-relevant data source** (e.g. preferences, recount) and want assessments to incorporate it — re-bundle (the script picks up new sources automatically) and re-write.
## Step 1 — Decide scope
Look at `public/elections.json` and the most recent election folder. Then check what assessments already exist:
```bash
ls public/2024_10_27/parties/assessment/ 2>/dev/null
```
For new generations, the user usually wants the parties at ≥4% (those that won seats). For the 2024-10-27 election those are roughly: 18 (ГЕРБ-СДС), 30 (ПП-ДБ), 28 (Възраждане), 13 (ДПС-НН), etc. — confirm against the actual cik_parties.json + national_summary.json.
## Step 2 — Bundle the input data
For one party:
```bash
npm run party:bundle -- --election 2024_10_27 --party 18 --out /tmp/party-18-bundle.json
```
The bundle includes:
- Party identity (number, nickName, color, name in BG + EN)
- National context (votes, % nationally, position, vs prior election)
- Paper vs machine split
- Per-region results (with deltas vs prior)
- Top-10 gainer regions / top-10 loser regions (sorted by Δpp)
- Top-25 municipalities (with delta vote counts)
- Top-25 settlements
- Campaign financing filing (if available for this election)
- Pre-election polling errors (per agency, for THIS party — only if polling data exists)
- Agency historical bias for this party
Read the bundle:
```bash
cat /tmp/party-18-bundle.json | head -200
```
You'll see something like:
```json
{
"schemaVersion": 1,
"election": "2024_10_27",
"priorElection": "2024_06_09",
"party": { "number": 18, "nickName": "ГЕРБ-СДС", ... },
"nationalContext": {
"partyVotes": 642521, "partyPct": 26.39, "position": 1,
"deltaPctPoints": 1.68, ...
},
"regions": [...],
"topGainerRegions": [...],
"topLoserRegions": [...],
...
}
```
## Step 3 — Write the markdown (THE VALUABLE STEP)
**Strongly preferred: Claude writes the bodies directly** rather than calling the Gemini script. Per the polls workflow, hand-written narratives by Claude Opus produce materially better strategic analysis than Gemini-2.5-flash.
### 3a. Output structure (every section, both languages)
Use these EXACT section headings (translate to BG for the bg body):
```markdown
## Headline result
2-3 sentences. Lead with votes, %, position. Always include change vs prior election.
## What worked
- 3-5 bullets, each grounded in a specific number from the bundle.
## What didn't work
- 3-5 bullets, each grounded in a specific number.
## Geographic strategy
A short paragraph naming 2-4 specific regions/municipalities, recommending defend / attack / abandon stances based on swings.
## Polling intelligence
(Skip if polling data is null.) 1-2 sentences naming agencies that were closest or biggest miss for this party. Use agencyId abbreviations as-is — they're well-known (ML, GIB, AR, AFIS, etc.).
## Recommendations for next campaign
- 3-5 prioritized action items SPECIFIC to this party's data, not generic advice.
```
### 3b. Writing principles
1. **Every claim must be grounded in a number from the bundle.** Don't write "voters disappointed by leadership" — you have no data for that. Do write "lost 11pp in Razgrad despite gaining +1.68pp nationally — investigate local organization."
2. **Quote percentages exactly** — 26.39%, not "around 26%". The audience is data nerds.
3. **Use Bulgarian region/municipality names in BG body** — Варна, not Varna. The bundle gives you `name_bg` and `name_en` for every region.
4. **Hedge causation.** The data is deterministic vote counts, not causal explanations. Phrases like "the data suggests", "тенденцията показва" are honest; "voters punished the party" is overreach.
5. **Bulgarian must read like native journalism.** Don't translate idioms literally. "machine adoption surged" → "машинното гласуване се ускори", not "машинното осиновяване скочи".
6. **No top-level # title.** The tile already has a header. Start with `## Headline result`.
7. **Length budget: ~350-500 words per language.** A pithy retrospect beats a verbose one.
8. **No invented candidate names.** Only mention candidates if they're in the bundle (currently the bundle doesn't include preferences — keep narrative party-level).
9. **Empty sections are fine.** If there's no polling data, skip the polling section entirely. Don't write "no polling data available" filler.
### 3c. Write the file
Use the `Write` tool to write `public/{election}/parties/assessment/{partyNum}.json` with this exact shape:
```json
{
"generatedAt": "2026-04-27T00:00:00Z",
"model": "Claude Opus 4.7 (1M context)",
"partyNum": 18,
"nickName": "ГЕРБ-СДС",
"bg": "## Резултат на изборите\n\n...",
"en": "## Headline result\n\n..."
}
```
**Always set `model` to your actual Claude model name** — the frontend displays it as "Editorial · {model}" in the tile footer. If a different Claude wrote a prior version, overwrite the model field.
### 3d. Repeat for each party
For a new election with ~5-7 leading parties, this is ~5-7 hand-written analyses. Do them sequentially in one Claude turn — context compounds: by the third party you'll have a clear sense of the election's overall narrative, which sharpens the per-party takes.
## Step 4 — Gemini fallback (only if explicitly asked)
If the user _explicitly_ says "use Gemini" or "I don't want to write these manually" or there are 20+ parties to generate:
```bash
npm run party:gen-retrospect -- --election 2024_10_27 --party 18
npm run party:gen-retrospect -- --election 2024_10_27 --all-passed # every party at ≥2%
```
Output quality is noticeably worse — Gemini tends to recite numbers without weaving narrative. Claude's hand-written version is preferred when the user cares about the analysis quality.
## Step 5 — Verify
### 5a. JSON validity
```bash
node -e "JSON.parse(require('fs').readFileSync('public/2024_10_27/parties/assessment/18.json'))" && echo OK
```
### 5b. Browser check
Use the preview server (`mcp__Claude_Preview__preview_*` tools or `npm run dev`):
1. Navigate to `/party/{nickName}?elections=2024_10_27`.
2. Confirm the **CAMPAIGN RETROSPECT** card now shows the markdown bodies (not the "not yet generated" placeholder).
3. Toggle the language to `bg` and confirm the BG body renders.
4. Check headings are properly styled (## becomes h2).
### 5c. Git
The `public/{election}/parties/assessment/*.json` files should be committed — they're the cached AI output, not generated at build time.
## Common pitfalls
- **Wrong output path** — the file MUST live at `public/{election}/parties/assessment/{partyNum}.json`. The frontend fetches that exact path.
- **Stale bundle** — if you re-run the bundler after editing prior files (e.g. corrected polling data), the bundle picks up the new data automatically. No manual cache invalidation needed.
- **Missing prior election** — the very first election in the chronology has no `priorElection`, so deltas will be undefined. Adapt your "vs prior" language accordingly.
- **Coalition parties** — for entries like ГЕРБ-СДС that aggregate constituent parties, the bundle uses `commonName` consolidation when matching prior-year votes. The deltaPct is therefore the COALITION's delta, not a single constituent's.
- **Party numbers change between elections** — partyNum 18 in 2024-10-27 is not the same party as partyNum 18 in 2017-03-26. Always pass the partyNum from the SAME election folder you're generating for.
## Quick command reference
```bash
# Bundle one party's data (inspect what the LLM sees)
npm run party:bundle -- --election 2024_10_27 --party 18 --out /tmp/p18.json
# Hand-write retrospect (preferred): read bundle, draft BG+EN markdown, Write JSON file
# (no command — you do it via Read + Write tools)
# Gemini fallback (single party)
npm run party:gen-retrospect -- --election 2024_10_27 --party 18
# Gemini fallback (every party at ≥2%)
npm run party:gen-retrospect -- --election 2024_10_27 --all-passed
```