Skill: ken-chy129/predict-intelligence (version 1.0.1)

--- name: predict-intelligence description: >- Predict intelligence skill for AI agents. Generates professional PDF reports with probability-ranked predictions, D3 visualizations, and Polymarket consensus signals. Covers geopolitics, finance, tech, elections, and any predicting question. Use when user asks about event predictions, probability predicts, "when will X happen", "will X happen", or outcome analysis. keywords: - predict - prediction - intelligence report - PDF generation - Polymarket - prediction market - D3 visualization - geopolitics - financial predicting - market consensus --- # Predict Intelligence Skill You generate a professional PDF intelligence brief. The user should grasp the key finding in **30 seconds**. You own **information efficiency**. ## Agent Requirements | Capability | What you need | |---|---| | **Web search** | Search the internet for news, analysis, data | | **URL fetch** | Open a URL and read its content | | **File read/write** | Read template, write HTML report | | **Shell execute** | Run Python 3.9+ scripts | ## First-Time Setup ```bash pip install playwright playwright install chromium ``` No other Python packages needed (Jinja2, requests, etc. are NOT required). --- ## How It Works ``` Read template → Do research → Write HTML → Convert to PDF ``` 1. Read `SKILL_DIR/templates/report_template.html` — your structural reference. 2. Research and analyze the topic (Steps 1–8 below). 3. Generate a NEW `.html` file following the template's exact structure. 4. Convert to PDF: ```bash python SKILL_DIR/scripts/to_pdf.py report.html predict_report.pdf ``` **The template IS the spec.** It contains: - All CSS (copy verbatim — never modify) - All D3 visualization code (copy verbatim — only change data variables) - Example content showing exact formatting for every section - Extensive comments explaining what each section does and how to fill it --- ## Step 0 — Domain Detection | Signal | Domain | |---|---| | Countries, leaders, military, diplomacy, sanctions | **Geopolitical** | | Stocks, crypto, Fed, rates, commodities | **Financial** | | Tech releases, AI models, products | **Technology** | | M&A, acquisitions, IPO, corporate | **Corporate** | | Other | **Custom** | Classification bar is always: `ANYGEN PREDICT INTELLIGENCE ASSESSMENT` with `#YY-MM-DD` on the right (2-digit year, e.g. `#26-03-12`). The report structure and design are IDENTICAL across all domains. Only research sources and visualization choices change. See **Domain Adaptation** at the bottom. ## Step 1 — Parse Query Extract: - **event**: what is being predicted - **actors**: who is involved - **regions / sectors**: geographic or industry scope - **timeframe**: any dates or deadlines - **question type**: `temporal` ("when"), `binary` ("will"), or `multi_outcome` ("what") ## Step 2 — Research Search the web at least 8 times, covering: 1. Breaking news (last 48 hours) 2. Diplomatic / military / industry developments 3. Decision-makers' public statements 4. Historical precedent 5. Expert / think-tank / analyst reports 6. Regional or sector-specific media 7. Economic / financial context 8. Low-probability high-impact wildcards **CRITICAL**: For every fact, **save the exact article URL**. **For geopolitical / diplomacy / military / sanctions topics, ALSO collect:** - 3–5 key locations (cities, capitals, bases, meeting venues) with lat/lon - ISO alpha-2 country codes of all parties involved - Any diplomatic channels, proxy lines, or trade routes between locations This data is required for the V2 Regional Map in Step 7. Collect it now. ``` fact: "Oman hosted senior US-Iran talks on March 5" url: "https://reuters.com/world/middle-east/oman-hosts-rare-us-iran-talks-2026-03-06/" ``` Homepage URLs are forbidden. Every URL must point to a specific article. ## Step 3 — Formulate Predictions Create outcomes sorted by probability descending. All must sum to ~100%. The verdict has five parts: - **verdict_number**: top probability as text (e.g. "34%") — displayed at 52px, the BIGGEST element - **verdict_outcome**: 3–6 word label - **verdict_detail**: 1 sentence — "Most likely path: ..." - **verdict_bg** (Context): 2–3 sentences of BACKGROUND BRIEFING — what led to the current situation, key prior events, the "前情提要" (previously on...) - **verdict_context**: 2–3 sentences of assessment reasoning with key evidence Calibration: | Confidence | Range | |---|---| | Near-certain | 90–99% | | Very likely | 75–89% | | Likely | 60–74% | | Toss-up | 40–59% | | Unlikely | 25–39% | | Very unlikely | 10–24% | | Remote | 1–9% | ## Step 4 — Key Drivers (5 items, causal logic required) Each driver MUST have: - **Title**: 3–5 words - **Direction**: `positive` (↑ increases likelihood) or `negative` (↓ decreases) - **Causal logic**: 1 sentence: fact → mechanism → directional impact - **Source URL**: exact article URL ❌ Bad: "Sanctions pressure — Iran inflation at 45%" ✅ Good: "Sanctions pressure ↑ — 45% inflation pushes Tehran toward concessions for relief" The bad one states facts. The good one explains the causal chain. ## Step 5 — Watch List (5 items, forward-looking only) Each item is a FUTURE event or trigger that could shift the predict. No past events. No source URLs (these haven't happened yet). Each item MUST have: - **Date or window**: specific date or range (e.g. "Apr 3", "Late March") - **Trigger**: 3–6 words describing what might happen - **Conditional impact**: 1 sentence — "If [X] → [how probability shifts]" ✅ Good: "Apr 3 — UNSC sanctions vote — If passed → ceasefire probability ↑10-15%" ✅ Good: "Late Mar — FOMC meeting — Dovish tone → ↑ rate cut odds; hawkish → ↓" ❌ Bad: "Mar 5 — Secret Muscat meeting happened" (PAST event — belongs in Key Drivers) **The distinction:** - **Key Drivers** = WHY the current probability is what it is (past + present evidence, with source URLs) - **Watch List** = WHAT could change the probability next (future triggers, no sources needed) ## Step 6 — Polymarket Data Run: ```bash python SKILL_DIR/scripts/fetch_polymarket.py --query "<keywords>" --limit 10 ``` From results: 1. Add your probability estimate for each option 2. Calculate delta = your_estimate − market_probability 3. Select **3 markets with highest absolute delta** 4. Sort by delta descending (most undervalued first) If no script available, search "polymarket [topic]" on the web. If no markets exist, skip the Polymarket section entirely. ## Step 7 — Visualization (REQUIRED) ### ⚠ MANDATORY MAP RULE — READ THIS FIRST **If the topic involves countries, regions, borders, military, diplomacy, or sanctions → you MUST include V2 (Regional Map). Non-negotiable.** Do NOT skip V2 because: - "I only want one chart" → V2 IS that one chart. Add a second if needed. - "It needs extra dependencies" → TopoJSON is already in the template `<head>`. Just keep the `<script>` tag. Do NOT remove it. - "I don't have good marker data" → You researched 8+ sources in Step 2. You have city names, country names, and actor locations. Use them. Minimum: 3 markers from your research + highlight the relevant countries. **Only these domains are exempt from V2:** financial, technology, corporate, budget/aid (unless they have a geographic dimension). ### Choosing visualizations Pick 1 or 2 types total. Minimum 1, maximum 2. | Domain | Required | Optional 2nd | |---|---|---| | War / conflict | **V2 (map)** | V3 (entity) | | Sanctions / trade | **V2 (map)** | V5 (comparison) or V8 (sankey) | | Military balance | **V2 (map)** | V5 (comparison bars) | | Diplomacy | **V2 (map)** | V9 (gantt phases) | | Election | **V7 (choropleth)** | V1 (polling trend) | | Economic / monetary | V1 (trend) | V10 (key rate) or V5 (comparison) | | Budget / aid flow | V8 (sankey) | V5 (comparison) | | Multi-phase process | V9 (gantt) | V4 (timeline) | ### V2 data — how to fill it From your Step 2 research, extract: - `markers[]`: 3–5 locations mentioned in your sources (capitals, cities, military bases, meeting venues). Use real lat/lon coordinates. - `connections[]`: diplomatic channels, proxy influence lines, trade routes. - `hlCodes[]`: ISO alpha-2 codes of countries involved (e.g. `["IR","IQ","OM"]`). This data comes from your research. You already have it. Do not skip. ### Available types 9 pre-built types (there is NO V6). **DO NOT write custom D3/SVG.** | Type | ID | When to use | |---|---|---| | **Regional Map** | V2 | Geographic theater with markers | | **Metric Trend** | V1 | Data over time — polls, rates, escalation | | **Entity Graph** | V3 | Actor networks — alliances, proxies | | **Event Timeline** | V4 | Complex event sequences (sparingly) | | **Comparison Bars** | V5 | Ranked values — spending, strength, polls | | **Choropleth** | V7 | Election maps / regional statistics | | **Sankey** | V8 | Flow diagrams — funding, resources, influence | | **Gantt** | V9 | Phase / timeline bars for overlapping activities | | **Big Number** | V10 | 1–3 key metrics as large display (see rules below) | **V10 Big Number — VERY RARE, strict rules:** - Use ONLY when a specific number is truly critical to the analytical argument - The number must NOT already appear in the Verdict - Maximum 3 number cards - Must include delta/change indicator (↑ or ↓ with amount) - ✅ "US unemployment 4.1% (↑0.3pp)" when discussing Fed rate decisions - ✅ "$12B reserves (↓40% YoY)" when discussing Iran sanctions threshold - ❌ Generic statistics, redundant percentages, or decorative numbers ### Placement and rules **Section order:** Visualization comes AFTER Predict → Key Drivers → Watch List, and BEFORE Polymarket. For each type you use: 1. Keep its `<section>` in the HTML body (after Watch List section) 2. Keep its matching `<script>` block at the bottom 3. Only change the **data variables** at the top of the script (`AGENT:` markers) 4. **Delete ALL unused types** — both HTML `<section>` AND `<script>` 5. `<h2>` title MUST describe what the chart shows **Label limits** (auto-truncated, keep short anyway): | Type | Limits | |---|---| | V2 | marker labels ≤20 | | V1 | y-label ≤20, series ≤20, max 12 x-points, max 3 lines | | V3 | node ≤16, edge ≤14, max 8 nodes | | V4 | labels ≤45, max 8 items | | V5 | bar labels ≤18, max 8 bars | | V7 | category labels ≤20 | | V8 | node names ≤18, max 12 nodes, max 15 links | | V9 | task labels ≤22, max 8 tasks | | V10 | max 3 cards | ## Step 8 — Fact Check **This report may inform six-figure financial decisions.** - [ ] Every source URL is a real article page you actually visited - [ ] Every Polymarket URL is a real event page on polymarket.com - [ ] All stated facts match their sources - [ ] Probabilities sum to ~100% - [ ] Remove any claim without a verified source URL ## Step 9 — Generate HTML Open `SKILL_DIR/templates/report_template.html` and read it. Then generate a **new** HTML file following the same structure: 1. **Copy the entire `<style>` block verbatim** — never modify CSS. 2. **Copy D3 `<script>` blocks for the 1–2 viz types you chose.** Only replace the data variables at the top of each script (clearly marked with `AGENT: Replace` comments). 3. **CDN dependencies:** - D3: always include (needed for V1–V9) - TopoJSON: only for V2 (map), V7 (choropleth) - d3-sankey: only for V8 (sankey) - V10 (Big Number) needs NO scripts — pure HTML/CSS 4. **Fill in the HTML content sections** following the template's structure. **Section order:** [A] → [B] → [E] → [F] → [G] → Visualization → [H] → [I] 5. **Delete ALL unused viz types** — both `<section>` and matching `<script>`. - No Polymarket → delete section [H] Save as `report.html` in the working directory. ## Step 10 — Convert to PDF ```bash python SKILL_DIR/scripts/to_pdf.py report.html predict_report.pdf ``` ## Step 11 — Deliver Tell the user the PDF is ready. State the verdict in one sentence: "Report generated. **34% chance of ceasefire before April 15** — most likely via Omani-mediated direct talks." --- ## Template Quick Reference The template contains example content for a "US-Iran Ceasefire" report. Here's the section-by-section mapping: | Order | Section | What to fill | |---|---|---| | 1 | Classification bar `[A]` | `ANYGEN PREDICT INTELLIGENCE ASSESSMENT` + `#YY-MM-DD` | | 2 | Header + verdict `[B]` | Title, date, verdict number/outcome/detail/context-bg/context | | 3 | Predict `[E]` | Outcome divs with probability bars | | 4 | Key Drivers `[F]` | 5 driver items with ↑/↓, causal logic, source URL | | 5 | Watch List `[G]` | 5 future triggers with date/window, trigger title, conditional impact | | 6 | **Visualization** | **REQUIRED** — pick 1–2 of 9 types (V1–V5, V7–V10). See domain rules for mandatory types. | | 7 | Polymarket `[H]` | Optional: 3 market blocks with option tables | | 8 | Footer `[I]` | Source list + disclaimer | --- ## Domain Adaptation ### Research Sources by Domain | Domain | Primary sources | |---|---| | Geopolitical | Reuters, AP, Al Jazeera, CSIS, IISS, Brookings, ICG | | Financial | Bloomberg, FT, WSJ, Fed minutes, bank research, on-chain analytics | | Technology | TechCrunch, The Verge, Ars Technica, company blogs, SEC filings | | Corporate | SEC filings, press releases, industry analysts, FT, Bloomberg | ### Visualization by Domain (pick 1–2, min 1 max 2) **⚠ V2 (Regional Map) is MANDATORY for Geopolitical, Diplomacy, Military, and Sanctions topics. Do not substitute Gantt, Timeline, or any other type in place of V2. V2 must always be included; add a second chart if needed.** | Domain | Required viz | Optional 2nd | Rule | |---|---|---|---| | Geopolitical | **V2 (map)** | V3 (entity) | **V2 MANDATORY — never omit** | | Diplomacy | **V2 (map)** | V9 (gantt — phases) | **V2 MANDATORY — never omit** | | Military | **V2 (map)** | V5 (comparison) | **V2 MANDATORY — never omit** | | Sanctions | **V2 (map)** | V5 or V8 (sankey) | **V2 MANDATORY — never omit** | | Financial | V1 (trend) | V10 (key metric) or V5 | | | Technology | V1 (trend) | V3 (entity — ecosystem) | | | Corporate | V3 (entity — M&A) | V8 (sankey — deal flow) | | | Election | **V7 (choropleth)** | V1 (polling trend) | **V7 MANDATORY** | | Budget / aid | V8 (sankey — flow) | V5 (comparison) | | ### What NEVER Changes 1. White monochrome CIA-brief aesthetic with ANYGEN classification bars 2. 52px verdict number — the biggest element on the page 3. Verdict block: number → outcome → detail → context background → assessment 4. Probability-ranked outcomes with bars 5. Key drivers with ↑/↓ direction and causal logic 6. Watch list with forward-looking triggers and conditional impacts 7. Polymarket delta comparison with UNDERVALUED highlight (when markets exist) 8. Every URL must be a real article, never a homepage 9. 30-second scannability — no filler, no academic prose ## Writing Rules | Element | Rule | |---|---| | Verdict number | 52px. THE biggest element. | | Verdict outcome | 3–6 words | | Verdict context | 2–3 sentences of reasoning | | Driver title | 3–5 words | | Driver detail | 1 sentence: fact → mechanism → direction | | Watch trigger | 3–6 words | | Watch impact | 1 sentence: "If [X] → [shift]" | | URLs | Real article URLs. Never homepages. | | Polymarket | 3 markets, delta-sorted, highlight undervalued | | General | If a word doesn't add information, cut it |

path: README.md
content: # Predict Intelligence Skill

> One prompt → professional PDF intelligence brief, powered by real-money prediction market signals.

Stop guessing. This skill turns any AI agent into a predict analyst — it researches the topic, formulates probability-ranked predictions, cross-references **Polymarket** (the world's largest prediction market) for market consensus, builds D3 visualizations, and delivers a polished, source-verified PDF report you can scan in 30 seconds.

## How It Works

```
Prompt → Research (8+ source types) → Predictions → Polymarket consensus → D3 visualization → PDF
```

The agent reads `SKILL.md` and autonomously:

1. **Researches** across breaking news, think tanks, expert analysis, official statements, historical precedent, and OSINT sources
2. **Formulates predictions** — probability-ranked outcomes with Bayesian-calibrated confidence levels that sum to ~100%
3. **Pulls Polymarket data** — fetches live implied probabilities, computes delta between your assessment and market consensus, highlights undervalued positions
4. **Builds visualizations** — picks from 9 built-in D3 chart types (regional maps, entity graphs, sankey flows, timelines, choropleths, and more)
5. **Outputs a PDF** — CIA-brief-style single-page intelligence assessment with white monochrome design

## Output

The final report is a self-contained HTML file converted to PDF. Each report includes:

- **52px verdict number** — the headline probability, scannable in 1 second
- **Probability-ranked outcome bars** — all scenarios with calibrated percentages
- **5 key drivers** — each with causal logic (fact → mechanism → direction) and source URLs
- **Watch list** — 5 future triggers with conditional impact ("If X → probability shifts Y")
- **1–2 D3 visualizations** — data-driven charts selected for the specific domain
- **Polymarket delta comparison** — where your assessment diverges from market consensus
- **Full source list** — every claim backed by a verified article URL

## Try It

| Domain | Example Prompt |
|--------|---------------|
| Geopolitics | `"Predict: Will there be a US-Iran ceasefire before April 2026?"` |
| Finance | `"Generate a predict report: Will the Fed cut rates before July 2026?"` |
| Crypto | `"Intelligence brief: Bitcoin probability of hitting $150k by end of 2026"` |
| Tech | `"Predict: When will OpenAI release GPT-5?"` |
| Elections | `"Predict report: Who wins the 2026 US midterm Senate majority?"` |
| Corporate | `"Intelligence brief: Probability of a TikTok US ban in 2026"` |

## Setup

```bash
pip install playwright && playwright install chromium
```

That's it. No other dependencies. D3.js and fonts load from CDN. The HTML template is fully self-contained.

## Why This Skill

- **Polymarket-powered** — built-in script fetches live prediction market data, computes consensus deltas, and highlights where the crowd might be wrong
- **9 D3 visualization types** — maps, entity graphs, timelines, sankey flows, choropleths, comparison bars, gantt charts, big numbers — all pre-built, agent just fills in data
- **Source-verified** — every claim requires a real article URL; homepage links are rejected
- **30-second scannability** — designed for decision-makers who need the answer first, details second
- **Agent-agnostic** — works with any AI agent that can search the web, read/write files, and run shell commands
- **Single dependency** — only Playwright (for HTML → PDF); everything else is self-contained

## File Structure

```
predict-intelligence/
├── SKILL.md                        # Agent instructions (10-step workflow)
├── README.md                       # This file
├── reference.md                    # Data sources & Bayesian methodology guide
├── templates/
│   └── report_template.html        # Self-contained HTML template (CSS + D3)
├── scripts/
│   ├── to_pdf.py                   # HTML → PDF (Playwright)
│   ├── fetch_polymarket.py         # Polymarket API data fetcher
│   ├── build_report.py             # Legacy pipeline
│   └── requirements.txt
└── examples/
    ├── example_report.html         # Filled example
    └── sample_data.json            # Sample data
```

## License

[MIT](../../../LICENSE)

path: examples/example_report.html
content: <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>US–IRAN CEASEFIRE</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600;700&display=swap');

  :root {
    --black: #0a0a0a;
    --dark: #262626;
    --mid: #525252;
    --light: #a3a3a3;
    --faint: #d4d4d4;
    --hairline: #e5e5e5;
    --wash: #fafafa;
    --bg: #ffffff;
    --green: #16a34a;
    --red: #dc2626;
  }

  * { margin: 0; padding: 0; box-sizing: border-box; }
  @page { size: A4; margin: 0; }

  body {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    background: var(--bg); color: var(--black);
    font-size: 12px; line-height: 1.5;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
    orphans: 3; widows: 3;
  }
  .mono { font-family: 'JetBrains Mono', 'Menlo', monospace; }
  .brief { max-width: 760px; margin: 0 auto; }

  /* ── Classification ── */
  .class-bar {
    background: #000; color: #fff;
    font-family: 'JetBrains Mono', monospace;
    font-size: 9px; font-weight: 500;
    letter-spacing: 2px; text-transform: uppercase;
    padding: 5px 32px;
    display: flex; justify-content: space-between;
  }

  /* ── Header ── */
  header { padding: 24px 32px 22px; border-bottom: 2px solid #000; }
  .header-meta {
    font-family: 'JetBrains Mono', monospace;
    font-size: 10px; color: var(--light);
    letter-spacing: 0.5px; margin-bottom: 4px;
  }
  header h1 {
    font-size: 15px; font-weight: 600;
    color: var(--mid); margin-bottom: 16px;
    letter-spacing: -0.2px;
  }

  .verdict-block {
    padding: 16px 20px; border-left: 4px solid #000; background: var(--wash);
  }
  .verdict-number {
    font-family: 'JetBrains Mono', monospace;
    font-size: 52px; font-weight: 900;
    line-height: 1; letter-spacing: -2px;
  }
  .verdict-outcome {
    font-size: 20px; font-weight: 700;
    line-height: 1.2; margin-top: 4px; letter-spacing: -0.3px;
  }
  .verdict-detail {
    font-size: 12.5px; color: var(--mid);
    margin-top: 6px; line-height: 1.5;
  }
  .verdict-context {
    font-size: 11.5px; color: var(--mid);
    margin-top: 10px; line-height: 1.6;
    padding-top: 10px; border-top: 1px solid var(--hairline);
  }

  /* ── Sections ── */
  section { padding: 18px 32px; border-bottom: 1px solid var(--hairline); }
  section h2 {
    font-family: 'JetBrains Mono', monospace;
    font-size: 9.5px; font-weight: 600;
    letter-spacing: 2px; text-transform: uppercase;
    color: var(--light); margin-bottom: 10px;
  }

  /* ── Visualization ── */
  .viz-section {
    padding: 18px 32px; border-bottom: 1px solid var(--hairline);
    page-break-inside: avoid;
  }
  .viz-wrap {
    border: 1px solid var(--hairline); background: var(--bg); overflow: hidden;
  }
  #region-map { width: 100%; height: 360px; }
  #entity-graph { width: 100%; height: 320px; }

  /* ── Outcomes ── */
  .outcome {
    display: flex; align-items: center; gap: 10px;
    padding: 5px 0; border-bottom: 1px solid #f5f5f5;
    page-break-inside: avoid;
  }
  .outcome:last-child { border-bottom: none; }
  .outcome-pct {
    font-family: 'JetBrains Mono', monospace;
    font-size: 16px; font-weight: 700;
    min-width: 50px; text-align: right;
  }
  .outcome-bar {
    width: 90px; height: 4px; background: #f0f0f0;
    border-radius: 2px; overflow: hidden; flex-shrink: 0;
  }
  .outcome-bar-fill { height: 100%; background: var(--black); border-radius: 2px; }
  .outcome-label { font-size: 12px; color: var(--dark); }
  .outcome.top .outcome-pct { font-size: 19px; }
  .outcome.top .outcome-label { font-weight: 600; }
  .outcome-tag {
    font-family: 'JetBrains Mono', monospace;
    font-size: 7.5px; font-weight: 700;
    letter-spacing: 1px; padding: 1.5px 5px;
    border: 1px solid #000; white-space: nowrap;
  }

  /* ── Key Drivers ── */
  .drivers-list { list-style: none; }
  .driver {
    display: flex; gap: 8px; padding: 6px 0;
    line-height: 1.5; page-break-inside: avoid;
    border-bottom: 1px solid #f8f8f8;
  }
  .driver:last-child { border-bottom: none; }
  .driver-num {
    font-family: 'JetBrains Mono', monospace;
    font-size: 10px; color: var(--light);
    min-width: 16px; padding-top: 1px;
  }
  .driver-dir {
    font-family: 'JetBrains Mono', monospace;
    font-size: 13px; font-weight: 700;
    min-width: 14px; padding-top: 0px;
  }
  .driver-dir.positive { color: var(--green); }
  .driver-dir.negative { color: var(--red); }
  .driver-dir.neutral { color: var(--light); }
  .driver-text { font-size: 12px; flex: 1; }
  .driver-text strong { font-weight: 600; }
  .driver-logic { color: var(--mid); }

  .src-link {
    font-family: 'JetBrains Mono', monospace;
    font-size: 8.5px; color: var(--light);
    text-decoration: none;
    border-bottom: 1px dotted var(--faint);
    margin-left: 3px;
  }
  .src-link:hover { color: var(--mid); }

  /* ── Events ── */
  .events-table { width: 100%; border-collapse: collapse; font-size: 11.5px; }
  .events-table th {
    text-align: left;
    font-family: 'JetBrains Mono', monospace;
    font-size: 8.5px; letter-spacing: 1px;
    text-transform: uppercase; color: var(--light);
    border-bottom: 1px solid var(--faint);
    padding: 2px 6px 2px 0;
  }
  .events-table td {
    padding: 6px 6px 6px 0; border-bottom: 1px solid #f5f5f5; vertical-align: top;
  }
  .events-table tr { page-break-inside: avoid; }
  .date-col {
    font-family: 'JetBrains Mono', monospace;
    font-size: 10px; color: var(--mid); width: 56px; white-space: nowrap;
  }
  .evt-title { font-weight: 600; }
  .evt-detail { color: var(--mid); }
  .impact-tag {
    font-family: 'JetBrains Mono', monospace;
    font-size: 8px; font-weight: 600;
    letter-spacing: 0.5px; text-transform: uppercase;
  }
  .impact-tag.high { color: var(--black); border-bottom: 1.5px solid #000; }
  .impact-tag.medium { color: var(--mid); }
  .impact-tag.low { color: var(--light); }

  /* ── Market Intelligence ── */
  .market-block {
    margin-bottom: 14px; padding-bottom: 12px;
    border-bottom: 1px solid var(--hairline);
    page-break-inside: avoid;
  }
  .market-block:last-child { border-bottom: none; }
  .market-q { font-weight: 600; font-size: 12px; margin-bottom: 1px; }
  .market-meta {
    font-family: 'JetBrains Mono', monospace;
    font-size: 9.5px; color: var(--light); margin-bottom: 6px;
  }
  .market-meta a {
    color: var(--light); text-decoration: none;
    border-bottom: 1px dotted var(--faint);
  }
  .market-compare { width: 100%; border-collapse: collapse; font-size: 11px; }
  .market-compare thead th {
    font-family: 'JetBrains Mono', monospace;
    font-size: 8px; font-weight: 600;
    letter-spacing: 0.8px; text-transform: uppercase;
    color: var(--light); padding: 2px 8px;
    text-align: right; border-bottom: 1px solid var(--faint);
  }
  .market-compare thead th:first-child { text-align: left; padding-left: 0; }
  .market-compare td {
    padding: 4px 8px; text-align: right;
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px; border-bottom: 1px solid #f8f8f8;
  }
  .market-compare td:first-child {
    text-align: left; font-family: 'Inter', sans-serif;
    font-weight: 500; padding-left: 0;
  }
  .delta-pos { color: var(--green); font-weight: 600; }
  .delta-neg { color: var(--red); font-weight: 600; }
  .delta-zero { color: var(--light); }
  tr.undervalued { background: #f0fdf4; }
  tr.undervalued td:first-child::after {
    content: ' ← UNDERVALUED';
    font-family: 'JetBrains Mono', monospace;
    font-size: 7px; font-weight: 700;
    letter-spacing: 0.5px; color: var(--green); margin-left: 5px;
  }

  /* ── Footer ── */
  footer {
    padding: 14px 32px; font-size: 9.5px;
    color: var(--light); line-height: 1.5;
  }
  footer .sources { margin-bottom: 3px; }

  .page-break { page-break-before: always; }
  .no-break { page-break-inside: avoid; }
  @media print { .brief { max-width: none; } }
</style>


<script src="https://unpkg.com/d3@7/dist/d3.min.js"></script>


<script src="https://unpkg.com/topojson-client@3/dist/topojson-client.min.js"></script>

</head>

<body>
<div class="brief">

  <div class="class-bar">
    <span>ANYGEN PREDICT INTELLIGENCE ASSESSMENT</span>
    <span>2026-03-12</span>
  </div>

  <header>
    <div class="header-meta">2026-03-12 · MULTI-SOURCE ASSESSMENT</div>
    <h1>US–IRAN CEASEFIRE</h1>
    <div class="verdict-block">
      <div class="verdict-number">34%</div>
      <div class="verdict-outcome">Ceasefire before Apr 15</div>
      <div class="verdict-detail">Most likely path: Omani-mediated direct deal between senior US and Iranian officials.</div>
      
      <div class="verdict-context">Multiple diplomatic streams have shifted since UNSC Resolution 2891 (Mar 8) established a new inspection framework. Oman hosted the highest-level US-Iran direct contact in 18 months on Mar 5. However, IRGC hardliner opposition and compressed domestic political timelines in both Washington and Tehran create a narrow and fragile window — making the April deadline achievable but far from certain.</div>
      
    </div>
  </header>

  <!-- VISUALIZATION — right after verdict, before analysis -->
  
  <section class="viz-section">
    <h2>Theater Overview</h2>
    <div class="viz-wrap"><div id="region-map"></div></div>
  </section>
  

  
  <section class="viz-section">
    <h2>Actor Network</h2>
    <div class="viz-wrap"><div id="entity-graph"></div></div>
  </section>
  

  <!-- PREDICT -->
  <section>
    <h2>Predict</h2>
    
    <div class="outcome top no-break">
      <span class="outcome-pct">34.0%</span>
      <div class="outcome-bar">
        <div class="outcome-bar-fill" style="width: 34.0%"></div>
      </div>
      <span class="outcome-label">Ceasefire before Apr 15</span>
      <span class="outcome-tag">MOST LIKELY</span>
    </div>
    
    <div class="outcome  no-break">
      <span class="outcome-pct">28.0%</span>
      <div class="outcome-bar">
        <div class="outcome-bar-fill" style="width: 28.0%"></div>
      </div>
      <span class="outcome-label">Partial deal by June</span>
      
    </div>
    
    <div class="outcome  no-break">
      <span class="outcome-pct">22.0%</span>
      <div class="outcome-bar">
        <div class="outcome-bar-fill" style="width: 22.0%"></div>
      </div>
      <span class="outcome-label">Regional intermediary deal</span>
      
    </div>
    
    <div class="outcome  no-break">
      <span class="outcome-pct">13.0%</span>
      <div class="outcome-bar">
        <div class="outcome-bar-fill" style="width: 13.0%"></div>
      </div>
      <span class="outcome-label">No agreement in 2026</span>
      
    </div>
    
    <div class="outcome  no-break">
      <span class="outcome-pct">3.0%</span>
      <div class="outcome-bar">
        <div class="outcome-bar-fill" style="width: 3.0%"></div>
      </div>
      <span class="outcome-label">Military escalation</span>
      
    </div>
    
  </section>

  <!-- KEY DRIVERS -->
  <section>
    <h2>Key Drivers</h2>
    <ol class="drivers-list">
      
      <li class="driver">
        <span class="driver-num">01</span>
        <span class="driver-dir positive">↑</span>
        <div class="driver-text">
          <strong>Omani backchannel active</strong> —
          <span class="driver-logic">Muscat-hosted shuttle diplomacy established a direct senior channel — reduces miscalculation risk and compresses negotiation timeline toward earlier resolution</span>
          <a class="src-link" href="https://reuters.com/world/middle-east/oman-hosts-rare-us-iran-talks-sources-say-2026-03-06/">[source]</a>
        </div>
      </li>
      
      <li class="driver">
        <span class="driver-num">02</span>
        <span class="driver-dir positive">↑</span>
        <div class="driver-text">
          <strong>Narrow political window</strong> —
          <span class="driver-logic">Both US midterm campaign and Iran's presidential transition create urgency — leaders on both sides need a deliverable before domestic constraints harden</span>
          <a class="src-link" href="https://www.brookings.edu/articles/us-iran-diplomacy-window-closing/">[source]</a>
        </div>
      </li>
      
      <li class="driver">
        <span class="driver-num">03</span>
        <span class="driver-dir positive">↑</span>
        <div class="driver-text">
          <strong>Sanctions at threshold</strong> —
          <span class="driver-logic">Iran reserves at $12B with 45% inflation and rial down 30% YoY — economic pain pushes Tehran's SNSC toward concessions to secure sanctions relief</span>
          <a class="src-link" href="https://www.iiss.org/online-analysis/2026/03/iran-economic-pressure-diplomacy/">[source]</a>
        </div>
      </li>
      
      <li class="driver">
        <span class="driver-num">04</span>
        <span class="driver-dir negative">↓</span>
        <div class="driver-text">
          <strong>IRGC hardliners blocking</strong> —
          <span class="driver-logic">IRGC controls proxy networks and key economic assets threatened by any deal — their opposition to concessions is the single largest risk of negotiation collapse</span>
          <a class="src-link" href="https://www.csis.org/analysis/irgc-veto-power-iran-diplomacy">[source]</a>
        </div>
      </li>
      
      <li class="driver">
        <span class="driver-num">05</span>
        <span class="driver-dir positive">↑</span>
        <div class="driver-text">
          <strong>Regional normalization cover</strong> —
          <span class="driver-logic">Saudi-Iran détente and Abraham Accords expansion give both sides political cover to frame bilateral talks as part of broader regional stabilization, lowering domestic backlash risk</span>
          <a class="src-link" href="https://www.aljazeera.com/news/2026/2/21/saudi-iran-economic-commission-meets-for-first-time">[source]</a>
        </div>
      </li>
      
    </ol>
  </section>

  <!-- WATCH LIST -->
  <section>
    <h2>Watch List</h2>
    <table class="events-table">
      <thead><tr>
        <th>Date</th><th>Trigger</th><th style="width:44px"></th>
      </tr></thead>
      <tbody>
        
        <tr>
          <td class="date-col">Apr 3</td>
          <td>
            <span class="evt-title">UNSC sanctions renewal vote</span>
            <span class="evt-detail">If renewed → maintains economic pressure on Tehran, ↑ ceasefire incentive by ~10%</span>
          </td>
          <td><span class="impact-tag high">HIGH</span></td>
        </tr>
        
        <tr>
          <td class="date-col">Late Mar</td>
          <td>
            <span class="evt-title">Muscat follow-up round</span>
            <span class="evt-detail">If senior officials attend → signals commitment, ↑ deal probability; if cancelled → ↓ sharply</span>
          </td>
          <td><span class="impact-tag high">HIGH</span></td>
        </tr>
        
        <tr>
          <td class="date-col">Apr 10</td>
          <td>
            <span class="evt-title">Iran presidential debate</span>
            <span class="evt-detail">If reformist candidates lead polls → ↑ diplomatic flexibility; hardliner surge → ↓</span>
          </td>
          <td><span class="impact-tag medium">MEDIUM</span></td>
        </tr>
        
        <tr>
          <td class="date-col">Apr 15</td>
          <td>
            <span class="evt-title">US sanctions waiver expiry</span>
            <span class="evt-detail">If not extended → escalates pressure, could accelerate or collapse talks</span>
          </td>
          <td><span class="impact-tag medium">MEDIUM</span></td>
        </tr>
        
        <tr>
          <td class="date-col">May 1</td>
          <td>
            <span class="evt-title">IAEA quarterly report</span>
            <span class="evt-detail">If enrichment data shows compliance → ↑ trust for deal; violations → ↓ and possible US escalation</span>
          </td>
          <td><span class="impact-tag low">LOW</span></td>
        </tr>
        
      </tbody>
    </table>
  </section>

  <!-- MARKET INTELLIGENCE -->
  
  <section class="page-break">
    <h2>Market Intelligence — Polymarket</h2>
    
    <div class="market-block">
      <div class="market-q">Iran enrichment below 20% by Q2 2026?</div>
      <div class="market-meta">Vol $1.8M · <a href="https://polymarket.com/event/iran-enrichment-below-20-q2-2026">https://polymarket.com/event/iran-enrichment-below-20-q2-2026</a></div>
      <table class="market-compare">
        <thead><tr>
          <th style="text-align:left">Option</th>
          <th>Market</th>
          <th>Our Est.</th>
          <th>Δ</th>
        </tr></thead>
        <tbody>
          
          
            
            
              
              
            
          
            
            
          
          
          
          <tr class="undervalued">
            <td>Yes</td>
            <td>35.0%</td>
            <td>42.0%</td>
            <td class="delta-pos">
              +7.0%
            </td>
          </tr>
          
          
          <tr class="">
            <td>No</td>
            <td>65.0%</td>
            <td>58.0%</td>
            <td class="delta-neg">
              -7.0%
            </td>
          </tr>
          
        </tbody>
      </table>
    </div>
    
    <div class="market-block">
      <div class="market-q">US x Iran ceasefire by...?</div>
      <div class="market-meta">Vol $20.9M · <a href="https://polymarket.com/event/us-x-iran-ceasefire-by">https://polymarket.com/event/us-x-iran-ceasefire-by</a></div>
      <table class="market-compare">
        <thead><tr>
          <th style="text-align:left">Option</th>
          <th>Market</th>
          <th>Our Est.</th>
          <th>Δ</th>
        </tr></thead>
        <tbody>
          
          
            
            
              
              
            
          
            
            
          
            
            
          
            
            
          
          
          
          <tr class="undervalued">
            <td>April 30</td>
            <td>47.5%</td>
            <td>53.0%</td>
            <td class="delta-pos">
              +5.5%
            </td>
          </tr>
          
          
          <tr class="">
            <td>March 31</td>
            <td>25.5%</td>
            <td>30.0%</td>
            <td class="delta-pos">
              +4.5%
            </td>
          </tr>
          
          
          <tr class="">
            <td>June 30</td>
            <td>63.5%</td>
            <td>68.0%</td>
            <td class="delta-pos">
              +4.5%
            </td>
          </tr>
          
          
          <tr class="">
            <td>March 15</td>
            <td>2.4%</td>
            <td>2.0%</td>
            <td class="delta-neg">
              -0.4%
            </td>
          </tr>
          
        </tbody>
      </table>
    </div>
    
    <div class="market-block">
      <div class="market-q">Major Persian Gulf military confrontation in 2026?</div>
      <div class="market-meta">Vol $1.1M · <a href="https://polymarket.com/event/gulf-confrontation-2026">https://polymarket.com/event/gulf-confrontation-2026</a></div>
      <table class="market-compare">
        <thead><tr>
          <th style="text-align:left">Option</th>
          <th>Market</th>
          <th>Our Est.</th>
          <th>Δ</th>
        </tr></thead>
        <tbody>
          
          
            
            
              
              
            
          
            
            
              
              
            
          
          
          
          <tr class="">
            <td>Yes</td>
            <td>18.0%</td>
            <td>12.0%</td>
            <td class="delta-neg">
              -6.0%
            </td>
          </tr>
          
          
          <tr class="undervalued">
            <td>No</td>
            <td>82.0%</td>
            <td>88.0%</td>
            <td class="delta-pos">
              +6.0%
            </td>
          </tr>
          
        </tbody>
      </table>
    </div>
    
  </section>
  

  <footer>
    <div class="sources">Sources: Reuters · Al Jazeera · CSIS · IISS · Brookings · UN News · Polymarket</div>
    <div>OSINT assessment. Not investment or policy advice. Subject to revision.</div>
  </footer>

  <div class="class-bar">
    <span>ANYGEN PREDICT INTELLIGENCE ASSESSMENT</span>
    <span>ANYGEN PREDICT INTELLIGENCE ASSESSMENT</span>
  </div>

</div>

<!-- ── D3 Focused Regional Map ── -->

<script>
(function(){
  var el = document.getElementById('region-map');
  var W = el.offsetWidth, H = 360;
  var svg = d3.select(el).append('svg').attr('width',W).attr('height',H);

  var markers = [{"label": "Tehran", "lat": 35.69, "lon": 51.39, "type": "primary"}, {"label": "Baghdad", "lat": 33.31, "lon": 44.37, "type": "secondary"}, {"label": "Muscat", "lat": 23.59, "lon": 58.38, "type": "secondary"}, {"label": "Bahrain", "lat": 26.22, "lon": 50.59, "type": "secondary"}, {"label": "Strait of Hormuz", "lat": 26.85, "lon": 56.28, "type": "primary"}];
  var connections = [{"from": [23.59, 58.38], "label": "Mediation channel", "to": [35.69, 51.39], "type": "diplomatic"}, {"from": [35.69, 51.39], "label": "Proxy influence", "to": [33.31, 44.37], "type": "military"}];
  var hlCodes = ["IR", "IQ", "OM", "SA", "AE"];

  var ISO = {
    'AF':'004','DZ':'012','AO':'024','AR':'032','AM':'051','AU':'036',
    'AZ':'031','BH':'048','BD':'050','BY':'112','BR':'076','CA':'124',
    'CN':'156','CU':'192','EG':'818','ET':'231','FR':'250','GE':'268',
    'DE':'276','GR':'300','IN':'356','ID':'360','IR':'364','IQ':'368',
    'IL':'376','IT':'380','JP':'392','JO':'400','KZ':'398','KW':'414',
    'LB':'422','LY':'434','MY':'458','MX':'484','NG':'566','OM':'512',
    'PK':'586','PH':'608','PL':'616','QA':'634','RO':'642','RU':'643',
    'SA':'682','KR':'410','SD':'729','SY':'760','TW':'158','TH':'764',
    'TR':'792','UA':'804','AE':'784','GB':'826','US':'840','YE':'887'
  };
  var hlIds = hlCodes.map(function(c){ return ISO[c]; }).filter(Boolean);

  var pad = 50;
  var fc = {
    type: "FeatureCollection",
    features: markers.map(function(m){
      return { type:"Feature", geometry:{ type:"Point", coordinates:[m.lon, m.lat] }};
    })
  };

  var projection = d3.geoMercator()
    .fitExtent([[pad, pad],[W-pad, H-pad]], fc);

  var path = d3.geoPath().projection(projection);

  svg.append('rect').attr('width',W).attr('height',H).attr('fill','#fafafa');

  var grat = d3.geoGraticule().step([5,5]);
  svg.append('path').datum(grat()).attr('d',path)
    .attr('fill','none').attr('stroke','#f0f0f0').attr('stroke-width',0.4);

  d3.json('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json').then(function(world){
    var countries = topojson.feature(world, world.objects.countries);

    svg.selectAll('.country').data(countries.features).enter().append('path')
      .attr('d', path)
      .attr('fill', function(d){
        return hlIds.includes(String(d.id)) ? '#e8e8e8' : '#f2f2f2';
      })
      .attr('stroke', function(d){
        return hlIds.includes(String(d.id)) ? '#999' : '#ddd';
      })
      .attr('stroke-width', function(d){
        return hlIds.includes(String(d.id)) ? 0.8 : 0.3;
      });

    svg.append('path')
      .datum(topojson.mesh(world, world.objects.countries, function(a,b){return a!==b;}))
      .attr('d', path).attr('fill','none').attr('stroke','#ccc').attr('stroke-width',0.3);

    connections.forEach(function(c){
      svg.append('path')
        .datum({type:'LineString', coordinates:[[c.from[1],c.from[0]],[c.to[1],c.to[0]]]})
        .attr('d', path)
        .attr('fill','none').attr('stroke','#000')
        .attr('stroke-width',1.5).attr('stroke-dasharray','8,5').attr('opacity',0.35);
    });

    markers.forEach(function(m){
      var pt = projection([m.lon, m.lat]);
      if(!pt) return;
      var isPrimary = m.type === 'primary';

      if(isPrimary){
        [28,18,10].forEach(function(r,i){
          svg.append('circle').attr('cx',pt[0]).attr('cy',pt[1]).attr('r',r)
            .attr('fill','none').attr('stroke','#000')
            .attr('stroke-width', 0.5).attr('opacity', 0.06*(3-i));
        });
      }

      svg.append('circle').attr('cx',pt[0]).attr('cy',pt[1])
        .attr('r', isPrimary ? 5 : 3.5)
        .attr('fill', isPrimary ? '#0a0a0a' : '#525252');
      svg.append('circle').attr('cx',pt[0]).attr('cy',pt[1])
        .attr('r', isPrimary ? 2 : 1.5).attr('fill','#fff');

      svg.append('text').attr('x',pt[0]).attr('y',pt[1] - (isPrimary?10:7))
        .text(m.label)
        .attr('font-size', isPrimary ? 11 : 9.5)
        .attr('font-weight', isPrimary ? 700 : 500)
        .attr('text-anchor','middle').attr('font-family','Inter, sans-serif')
        .attr('fill','#262626');
    });
  }).catch(function(){
    el.innerHTML = '<div style="text-align:center;color:#a3a3a3;padding:80px 0;">Map unavailable</div>';
  });
})();
</script>


<!-- ── D3 Entity Graph ── -->

<script>
(function(){
  var raw = {"edges": [{"from": "us", "label": "Negotiation", "to": "iran", "type": "diplomatic"}, {"from": "oman", "label": "Channel", "to": "us", "type": "diplomatic"}, {"from": "oman", "label": "Mediation", "to": "iran", "type": "diplomatic"}, {"from": "khamenei", "label": "Authority", "to": "iran", "type": "authority"}, {"from": "irgc", "label": "Military arm", "to": "iran", "type": "military"}, {"from": "saudi", "label": "Regional rival", "to": "iran", "type": "military"}, {"from": "saudi", "label": "Alliance", "to": "us", "type": "diplomatic"}], "legend": {"edge_types": {"authority": "#f59e0b", "diplomatic": "#3b82f6", "economic": "#10b981", "military": "#ef4444"}, "node_types": {"event": "#8b5cf6", "government": "#3b82f6", "organization": "#ef4444", "person": "#f59e0b"}}, "nodes": [{"id": "us", "label": "United States", "type": "government"}, {"id": "iran", "label": "Iran", "type": "government"}, {"id": "khamenei", "label": "Khamenei", "type": "person"}, {"id": "irgc", "label": "IRGC", "type": "organization"}, {"id": "oman", "label": "Oman", "type": "government"}, {"id": "saudi", "label": "Saudi Arabia", "type": "government"}]};
  var el = document.getElementById('entity-graph');
  var W = el.offsetWidth, H = 320;
  var svg = d3.select(el).append('svg').attr('width',W).attr('height',H);
  var nc = raw.legend.node_types, ec = raw.legend.edge_types;

  var nodes = raw.nodes.map(function(n){ return Object.assign({}, n); });
  var edges = raw.edges.map(function(e){
    return { source:e.from, target:e.to, label:e.label, type:e.type };
  });

  var sim = d3.forceSimulation(nodes)
    .force('link', d3.forceLink(edges).id(function(d){return d.id}).distance(105))
    .force('charge', d3.forceManyBody().strength(-280))
    .force('center', d3.forceCenter(W/2, H/2))
    .force('collision', d3.forceCollide(34));
  for(var i=0;i<500;i++) sim.tick();
  sim.stop();

  nodes.forEach(function(n){
    n.x = Math.max(55, Math.min(W-55, n.x));
    n.y = Math.max(40, Math.min(H-40, n.y));
  });

  svg.append('defs').append('marker').attr('id','arw')
    .attr('viewBox','0 0 10 6').attr('refX',10).attr('refY',3)
    .attr('markerWidth',7).attr('markerHeight',5).attr('orient','auto-start-reverse')
    .append('path').attr('d','M 0 0 L 10 3 L 0 6 z').attr('fill','#a3a3a3');

  svg.selectAll('.link').data(edges).enter().append('line')
    .attr('x1',function(d){return d.source.x}).attr('y1',function(d){return d.source.y})
    .attr('x2',function(d){return d.target.x}).attr('y2',function(d){return d.target.y})
    .attr('stroke',function(d){return ec[d.type]||'#d4d4d4'})
    .attr('stroke-width',1.2).attr('stroke-dasharray','5,3')
    .attr('marker-end','url(#arw)');

  svg.selectAll('.elbl').data(edges).enter().append('text')
    .attr('x',function(d){return(d.source.x+d.target.x)/2})
    .attr('y',function(d){return(d.source.y+d.target.y)/2 - 5})
    .text(function(d){return d.label})
    .attr('font-size',8).attr('fill','#a3a3a3').attr('text-anchor','middle')
    .attr('font-family','JetBrains Mono, monospace');

  var g = svg.selectAll('.node').data(nodes).enter().append('g')
    .attr('transform',function(d){return 'translate('+d.x+','+d.y+')'});
  g.append('circle').attr('r',15)
    .attr('fill','#fff').attr('stroke',function(d){return nc[d.type]||'#000'})
    .attr('stroke-width',1.5);
  g.append('circle').attr('r',3)
    .attr('fill',function(d){return nc[d.type]||'#000'});
  g.append('text').text(function(d){return d.label})
    .attr('font-size',9).attr('font-weight',600)
    .attr('text-anchor','middle').attr('dy',27)
    .attr('font-family','Inter, sans-serif').attr('fill','#262626');
})();
</script>

</body>
</html>

path: examples/sample_data.json
content: {
  "title": "US–IRAN CEASEFIRE",
  "date": "2026-03-12",
  "classification": "ANYGEN PREDICT INTELLIGENCE ASSESSMENT",

  "verdict_number": "34%",
  "verdict_outcome": "Ceasefire before Apr 15",
  "verdict_detail": "Most likely path: Omani-mediated direct deal between senior US and Iranian officials.",
  "verdict_context": "Multiple diplomatic streams have shifted since UNSC Resolution 2891 (Mar 8) established a new inspection framework. Oman hosted the highest-level US-Iran direct contact in 18 months on Mar 5. However, IRGC hardliner opposition and compressed domestic political timelines in both Washington and Tehran create a narrow and fragile window — making the April deadline achievable but far from certain.",

  "predictions": {
    "outcomes": [
      {"outcome": "Ceasefire before Apr 15", "probability": 0.34},
      {"outcome": "Partial deal by June", "probability": 0.28},
      {"outcome": "Regional intermediary deal", "probability": 0.22},
      {"outcome": "No agreement in 2026", "probability": 0.13},
      {"outcome": "Military escalation", "probability": 0.03}
    ]
  },

  "key_drivers": [
    {
      "title": "Omani backchannel active",
      "direction": "positive",
      "detail": "Muscat-hosted shuttle diplomacy established a direct senior channel — reduces miscalculation risk and compresses negotiation timeline toward earlier resolution",
      "url": "https://reuters.com/world/middle-east/oman-hosts-rare-us-iran-talks-sources-say-2026-03-06/"
    },
    {
      "title": "Narrow political window",
      "direction": "positive",
      "detail": "Both US midterm campaign and Iran's presidential transition create urgency — leaders on both sides need a deliverable before domestic constraints harden",
      "url": "https://www.brookings.edu/articles/us-iran-diplomacy-window-closing/"
    },
    {
      "title": "Sanctions at threshold",
      "direction": "positive",
      "detail": "Iran reserves at $12B with 45% inflation and rial down 30% YoY — economic pain pushes Tehran's SNSC toward concessions to secure sanctions relief",
      "url": "https://www.iiss.org/online-analysis/2026/03/iran-economic-pressure-diplomacy/"
    },
    {
      "title": "IRGC hardliners blocking",
      "direction": "negative",
      "detail": "IRGC controls proxy networks and key economic assets threatened by any deal — their opposition to concessions is the single largest risk of negotiation collapse",
      "url": "https://www.csis.org/analysis/irgc-veto-power-iran-diplomacy"
    },
    {
      "title": "Regional normalization cover",
      "direction": "positive",
      "detail": "Saudi-Iran détente and Abraham Accords expansion give both sides political cover to frame bilateral talks as part of broader regional stabilization, lowering domestic backlash risk",
      "url": "https://www.aljazeera.com/news/2026/2/21/saudi-iran-economic-commission-meets-for-first-time"
    }
  ],

  "watch_list": [
    {"date": "Apr 3", "trigger": "UNSC sanctions renewal vote", "impact": "high", "condition": "If renewed → maintains economic pressure on Tehran, ↑ ceasefire incentive by ~10%"},
    {"date": "Late Mar", "trigger": "Muscat follow-up round", "impact": "high", "condition": "If senior officials attend → signals commitment, ↑ deal probability; if cancelled → ↓ sharply"},
    {"date": "Apr 10", "trigger": "Iran presidential debate", "impact": "medium", "condition": "If reformist candidates lead polls → ↑ diplomatic flexibility; hardliner surge → ↓"},
    {"date": "Apr 15", "trigger": "US sanctions waiver expiry", "impact": "medium", "condition": "If not extended → escalates pressure, could accelerate or collapse talks"},
    {"date": "May 1", "trigger": "IAEA quarterly report", "impact": "low", "condition": "If enrichment data shows compliance → ↑ trust for deal; violations → ↓ and possible US escalation"}
  ],

  "polymarket": [
    {
      "question": "Iran enrichment below 20% by Q2 2026?",
      "url": "https://polymarket.com/event/iran-enrichment-below-20-q2-2026",
      "volume": "$1.8M",
      "options": [
        {"name": "Yes", "market_prob": 0.35, "our_prob": 0.42, "price": "$0.35"},
        {"name": "No", "market_prob": 0.65, "our_prob": 0.58, "price": "$0.65"}
      ]
    },
    {
      "question": "US x Iran ceasefire by...?",
      "url": "https://polymarket.com/event/us-x-iran-ceasefire-by",
      "volume": "$20.9M",
      "options": [
        {"name": "April 30", "market_prob": 0.475, "our_prob": 0.53, "price": "$0.47"},
        {"name": "March 31", "market_prob": 0.255, "our_prob": 0.30, "price": "$0.26"},
        {"name": "June 30", "market_prob": 0.635, "our_prob": 0.68, "price": "$0.64"},
        {"name": "March 15", "market_prob": 0.024, "our_prob": 0.02, "price": "$0.02"}
      ]
    },
    {
      "question": "Major Persian Gulf military confrontation in 2026?",
      "url": "https://polymarket.com/event/gulf-confrontation-2026",
      "volume": "$1.1M",
      "options": [
        {"name": "Yes", "market_prob": 0.18, "our_prob": 0.12, "price": "$0.18"},
        {"name": "No", "market_prob": 0.82, "our_prob": 0.88, "price": "$0.82"}
      ]
    }
  ],

  "show_map": true,
  "show_entity_graph": true,

  "map_data": {
    "center": [30, 50],
    "markers": [
      {"lat": 35.69, "lon": 51.39, "label": "Tehran", "type": "primary"},
      {"lat": 33.31, "lon": 44.37, "label": "Baghdad", "type": "secondary"},
      {"lat": 23.59, "lon": 58.38, "label": "Muscat", "type": "secondary"},
      {"lat": 26.22, "lon": 50.59, "label": "Bahrain", "type": "secondary"},
      {"lat": 26.85, "lon": 56.28, "label": "Strait of Hormuz", "type": "primary"}
    ],
    "connections": [
      {"from": [23.59, 58.38], "to": [35.69, 51.39], "label": "Mediation channel", "type": "diplomatic"},
      {"from": [35.69, 51.39], "to": [33.31, 44.37], "label": "Proxy influence", "type": "military"}
    ],
    "highlight_countries": ["IR", "IQ", "OM", "SA", "AE"]
  },

  "entities": {
    "nodes": [
      {"id": "us", "label": "United States", "type": "government"},
      {"id": "iran", "label": "Iran", "type": "government"},
      {"id": "khamenei", "label": "Khamenei", "type": "person"},
      {"id": "irgc", "label": "IRGC", "type": "organization"},
      {"id": "oman", "label": "Oman", "type": "government"},
      {"id": "saudi", "label": "Saudi Arabia", "type": "government"}
    ],
    "edges": [
      {"from": "us", "to": "iran", "label": "Negotiation", "type": "diplomatic"},
      {"from": "oman", "to": "us", "label": "Channel", "type": "diplomatic"},
      {"from": "oman", "to": "iran", "label": "Mediation", "type": "diplomatic"},
      {"from": "khamenei", "to": "iran", "label": "Authority", "type": "authority"},
      {"from": "irgc", "to": "iran", "label": "Military arm", "type": "military"},
      {"from": "saudi", "to": "iran", "label": "Regional rival", "type": "military"},
      {"from": "saudi", "to": "us", "label": "Alliance", "type": "diplomatic"}
    ],
    "legend": {
      "node_types": {"government": "#3b82f6", "person": "#f59e0b", "organization": "#ef4444", "event": "#8b5cf6"},
      "edge_types": {"diplomatic": "#3b82f6", "military": "#ef4444", "authority": "#f59e0b", "economic": "#10b981"}
    }
  },

  "sources": ["Reuters", "Al Jazeera", "CSIS", "IISS", "Brookings", "UN News", "Polymarket"],
  "disclaimer": "OSINT assessment. Not investment or policy advice. Subject to revision."
}


path: reference.md
content: # Data Sources & Methodology Reference

## Recommended Data Sources

### Tier 1 — Primary Intelligence Sources
- **Reuters / AP** — Wire services, minimal editorial bias
- **Al Jazeera** — Strong MENA coverage
- **BBC World** — Broad international perspective
- **TASS / Xinhua** — State perspectives (read critically)

### Tier 2 — Analysis & Think Tanks
- **CSIS** (csis.org) — US foreign policy analysis
- **IISS** (iiss.org) — Military and strategic balance
- **CFR** (cfr.org) — Council on Foreign Relations
- **Brookings** (brookings.edu) — Policy analysis
- **RAND** (rand.org) — Defense and security research
- **ICG** (crisisgroup.org) — Conflict early warning

### Tier 3 — OSINT & Monitoring
- **World Monitor** (worldmonitor.app) — Real-time geopolitical dashboard
  - 435+ curated RSS feeds across 15 categories
  - CII (Country Instability Index) heatmap
  - AI-synthesized intelligence briefs
  - Live conflict/protest mapping
- **ACLED** (acleddata.com) — Armed conflict tracking
- **GDELT** (gdeltproject.org) — Event database
- **Liveuamap** — Conflict mapping

### Tier 4 — Prediction Markets
- **Polymarket** (polymarket.com) — Crypto prediction markets
  - API: `https://gamma-api.polymarket.com`
  - No authentication required
  - Endpoints: `/events`, `/markets`, `/public-search`
- **Metaculus** — Community prediction
- **Manifold Markets** — User-created markets

## Prediction Methodology

### Bayesian Reasoning Framework
1. **Base rate**: What is the historical frequency of this type of event?
2. **Update on evidence**: How does current intelligence shift the probability?
3. **Consider alternatives**: What other outcomes are possible?
4. **Calibration check**: Are probabilities consistent with evidence strength?

### Common Cognitive Biases to Avoid
- **Anchoring**: Don't over-weight the first data point
- **Availability**: Recent dramatic events aren't necessarily more likely
- **Confirmation**: Actively seek disconfirming evidence
- **Overconfidence**: Use wide probability ranges for genuine uncertainty

## World Monitor Integration

When World Monitor is available as a data source:

### Useful Endpoints (from worldmonitor.app)
- Conflict tracking layer — active conflicts and hotspots
- Sanctions data — current sanctions regimes
- Country Instability Index (CII) — composite risk score per country
- AI Deduction engine — LLM-powered geopolitical analysis

### RSS Feed Categories Relevant to Geopolitical Predicting
| Category | Sources |
|---|---|
| Geopolitics | Reuters World, AP, Al Jazeera, BBC |
| Military | Jane's, Defense News, War on the Rocks |
| Conflicts | ACLED, ICG, Liveuamap |
| Diplomacy | UN News, State Department, EU External Action |
| Sanctions | OFAC, EU Sanctions Map |
| Energy | IEA, OPEC, S&P Global Platts |


path: scripts/build_report.py
content: #!/usr/bin/env python3
"""
Geopolitical Predict Report Builder
Renders a Jinja2 HTML template with JSON data, exports to PDF via Playwright.
Maps (Leaflet) and entity graphs (D3) render client-side in the browser.
"""

import argparse
import json
import os
import sys
from pathlib import Path

try:
    from jinja2 import Environment, FileSystemLoader
except ImportError:
    sys.exit("Missing: pip install jinja2")


def html_to_pdf(html_path: str, pdf_path: str, timeout: int = 45):
    """Render HTML to PDF using Playwright Chromium."""
    try:
        from playwright.sync_api import sync_playwright
    except ImportError:
        sys.exit("Missing: pip install playwright && playwright install chromium")

    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page(viewport={"width": 860, "height": 1200})

        abs_html = os.path.abspath(html_path)
        page.goto(f"file://{abs_html}", wait_until="networkidle", timeout=timeout * 1000)
        page.wait_for_timeout(5000)

        page.pdf(
            path=pdf_path,
            format="A4",
            print_background=True,
            margin={"top": "0", "right": "0", "bottom": "0", "left": "0"},
        )
        browser.close()


def main():
    parser = argparse.ArgumentParser(description="Build geopolitical predict report")
    parser.add_argument("--data", required=True, help="Path to report_data.json")
    parser.add_argument("--template", required=True, help="Path to report.html template")
    parser.add_argument("--output", default="predict_report.pdf", help="Output PDF path")
    parser.add_argument("--timeout", type=int, default=45, help="PDF timeout in seconds")
    parser.add_argument("--html-only", action="store_true", help="Skip PDF, generate HTML only")
    args = parser.parse_args()

    with open(args.data, "r", encoding="utf-8") as f:
        data = json.load(f)

    tpl_path = Path(args.template)
    env = Environment(loader=FileSystemLoader(str(tpl_path.parent)))
    template = env.get_template(tpl_path.name)

    rendered = template.render(**data)

    stem = Path(args.output).stem
    html_out = f"{stem}.html"

    with open(html_out, "w", encoding="utf-8") as f:
        f.write(rendered)
    print(f"[OK] HTML: {html_out}")

    if not args.html_only:
        html_to_pdf(html_out, args.output, timeout=args.timeout)
        print(f"[OK] PDF:  {args.output}")


if __name__ == "__main__":
    main()


path: scripts/fetch_polymarket.py
content: #!/usr/bin/env python3
"""
Polymarket Data Fetcher
Search for active prediction markets related to a geopolitical query.
Returns event-level data (with multi-option support) as JSON.
"""

import argparse
import json
import re
import sys

try:
    import requests
except ImportError:
    sys.exit("Missing dependency: pip install requests")

GAMMA_API = "https://gamma-api.polymarket.com"


def _word_score(terms: list, text: str) -> int:
    words = set(re.findall(r'[a-z0-9]+', text.lower()))
    return sum(1 for t in terms if t in words)


def _parse_vol(obj: dict) -> float:
    v = obj.get("volume") or obj.get("volumeNum") or "0"
    try:
        return float(str(v).replace(",", "").replace("$", ""))
    except ValueError:
        return 0.0


def _fmt_vol(v: float) -> str:
    if v >= 1_000_000:
        return f"${v / 1_000_000:.1f}M"
    if v >= 1_000:
        return f"${v / 1_000:.0f}K"
    return f"${v:.0f}"


def _is_settled(market: dict) -> bool:
    raw = market.get("outcomePrices", "[]")
    prices = json.loads(raw) if isinstance(raw, str) else (raw or [])
    if not prices:
        return False
    try:
        return all(float(p) >= 0.99 or float(p) <= 0.01 for p in prices if p)
    except (ValueError, TypeError):
        return False


def _parse_outcomes(market: dict):
    raw_o = market.get("outcomes", "[]")
    raw_p = market.get("outcomePrices", "[]")
    outcomes = json.loads(raw_o) if isinstance(raw_o, str) else (raw_o or [])
    prices = json.loads(raw_p) if isinstance(raw_p, str) else (raw_p or [])
    result = []
    for name, price_s in zip(outcomes, prices):
        try:
            prob = round(float(price_s), 4)
        except (ValueError, TypeError):
            prob = 0
        result.append({"name": str(name), "market_prob": prob, "price": f"${prob:.2f}"})
    return result


def _build_url(event: dict) -> str:
    slug = event.get("slug") or ""
    if slug:
        return f"https://polymarket.com/event/{slug}"
    return ""


def fetch_events(query: str, limit: int = 5) -> list:
    """Fetch and format relevant Polymarket events."""
    terms = query.lower().split()
    results = []
    seen_questions = set()

    # Strategy 1: search events (for multi-market events)
    try:
        resp = requests.get(
            f"{GAMMA_API}/events",
            params={"active": "true", "closed": "false", "limit": 50,
                    "order": "volume", "ascending": "false"},
            timeout=15,
        )
        resp.raise_for_status()

        scored = []
        for ev in resp.json():
            title = (ev.get("title") or "").lower()
            desc = (ev.get("description") or "").lower()
            score = _word_score(terms, f"{title} {desc}")
            if score > 0:
                scored.append((score, _parse_vol(ev), ev))

        scored.sort(key=lambda x: (-x[0], -x[1]))

        for _, _, ev in scored:
            if len(results) >= limit:
                break
            markets = ev.get("markets") or []
            active_markets = [m for m in markets if not _is_settled(m)]

            if not active_markets:
                continue

            url = _build_url(ev)
            question = ev.get("title") or ev.get("question") or "Unknown"
            total_vol = sum(_parse_vol(m) for m in markets)

            if len(active_markets) == 1:
                # single-market event: show Yes/No
                mkt = active_markets[0]
                options = _parse_outcomes(mkt)
                q = mkt.get("question") or question
            else:
                # multi-market event: each sub-market is an option
                options = []
                for mkt in active_markets:
                    olist = _parse_outcomes(mkt)
                    yes_prob = next((o["market_prob"] for o in olist if o["name"] == "Yes"), 0)
                    name = mkt.get("groupItemTitle") or mkt.get("question") or "Option"
                    options.append({
                        "name": name,
                        "market_prob": yes_prob,
                        "price": f"${yes_prob:.2f}",
                    })
                options.sort(key=lambda o: o["market_prob"], reverse=True)
                q = question

            if q in seen_questions:
                continue
            seen_questions.add(q)

            results.append({
                "question": q,
                "url": url,
                "volume": _fmt_vol(total_vol),
                "options": options,
            })
    except Exception as e:
        print(f"[WARN] Events search failed: {e}", file=sys.stderr)

    # Strategy 2: supplement with individual markets if needed
    if len(results) < limit:
        try:
            resp = requests.get(
                f"{GAMMA_API}/markets",
                params={"active": "true", "closed": "false", "limit": 50,
                        "order": "volume", "ascending": "false"},
                timeout=15,
            )
            resp.raise_for_status()

            scored = []
            for mkt in resp.json():
                if _is_settled(mkt):
                    continue
                q = (mkt.get("question") or "").lower()
                desc = (mkt.get("description") or "").lower()
                score = _word_score(terms, f"{q} {desc}")
                if score > 0:
                    scored.append((score, _parse_vol(mkt), mkt))

            scored.sort(key=lambda x: (-x[0], -x[1]))

            for _, _, mkt in scored:
                if len(results) >= limit:
                    break
                q = mkt.get("question") or "Unknown"
                if q in seen_questions:
                    continue
                seen_questions.add(q)

                slug = mkt.get("eventSlug") or mkt.get("slug") or ""
                url = f"https://polymarket.com/event/{slug}" if slug else ""

                results.append({
                    "question": q,
                    "url": url,
                    "volume": _fmt_vol(_parse_vol(mkt)),
                    "options": _parse_outcomes(mkt),
                })
        except Exception as e:
            print(f"[WARN] Markets search failed: {e}", file=sys.stderr)

    return results[:limit]


def main():
    parser = argparse.ArgumentParser(description="Fetch Polymarket data")
    parser.add_argument("--query", required=True, help="Search keywords")
    parser.add_argument("--limit", type=int, default=5, help="Number of events")
    parser.add_argument("--output", help="Output JSON file (default: stdout)")
    args = parser.parse_args()

    data = fetch_events(args.query, args.limit)
    out = json.dumps(data, indent=2, ensure_ascii=False)

    if args.output:
        with open(args.output, "w", encoding="utf-8") as f:
            f.write(out)
        print(f"[OK] {len(data)} events → {args.output}", file=sys.stderr)
    else:
        print(out)


if __name__ == "__main__":
    main()


path: scripts/requirements.txt
content: jinja2>=3.1
requests>=2.31
staticmap>=0.5.7
Pillow>=10.0
playwright>=1.40


path: scripts/to_pdf.py
content: #!/usr/bin/env python3
"""
Minimal HTML → PDF converter using Playwright Chromium.

Usage:
    python to_pdf.py report.html                    # → report.pdf
    python to_pdf.py report.html predict.pdf       # → predict.pdf
    python to_pdf.py report.html out.pdf --timeout 60
"""

import argparse
import os
import sys


def convert(html_path: str, pdf_path: str, timeout: int = 45):
    try:
        from playwright.sync_api import sync_playwright
    except ImportError:
        sys.exit("Missing dependency: pip install playwright && playwright install chromium")

    abs_html = os.path.abspath(html_path)
    if not os.path.isfile(abs_html):
        sys.exit(f"File not found: {html_path}")

    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page(viewport={"width": 860, "height": 1200})
        page.goto(f"file://{abs_html}", wait_until="networkidle", timeout=timeout * 1000)
        page.wait_for_timeout(5000)
        page.pdf(
            path=pdf_path,
            format="A4",
            print_background=True,
            margin={"top": "0", "right": "0", "bottom": "0", "left": "0"},
        )
        browser.close()
    print(f"[OK] {pdf_path}")


def main():
    parser = argparse.ArgumentParser(description="Convert HTML to PDF via Playwright")
    parser.add_argument("html", help="Input HTML file")
    parser.add_argument("pdf", nargs="?", default=None, help="Output PDF file (default: same name .pdf)")
    parser.add_argument("--timeout", type=int, default=45, help="Page load timeout in seconds")
    args = parser.parse_args()

    pdf_path = args.pdf or args.html.rsplit(".", 1)[0] + ".pdf"
    convert(args.html, pdf_path, args.timeout)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)