#!/usr/bin/env python3
"""Build a stricter strategic 10-loop EQIX company-deep-dive package."""

from __future__ import annotations

import csv
import html
import json
import urllib.error
import urllib.request
from datetime import datetime, timezone
from pathlib import Path


BASE = Path(__file__).resolve().parent
RUN = BASE.parent
PUBLIC = RUN / "public"
SERVED = Path("/home/dev/public/research-loop/strategic10-eqix-001")
PUBLIC.mkdir(parents=True, exist_ok=True)
SERVED.mkdir(parents=True, exist_ok=True)


def write_csv(name: str, rows: list[dict[str, object]]) -> None:
    with (BASE / name).open("w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=list(rows[0]))
        writer.writeheader()
        writer.writerows(rows)


def fetch_log(rows: list[dict[str, object]]) -> list[dict[str, object]]:
    out = []
    for row in rows:
        url = str(row["source_url"])
        try:
            req = urllib.request.Request(url, headers={"User-Agent": "research-loop/1.0"})
            with urllib.request.urlopen(req, timeout=20) as resp:
                body = resp.read(2048)
                status = getattr(resp, "status", 200)
                final_url = resp.geturl()
                ok = 200 <= status < 400 and bool(body)
                detail = f"fetched {len(body)} bytes"
        except (urllib.error.URLError, TimeoutError, OSError) as exc:
            status = "error"
            final_url = url
            ok = False
            detail = type(exc).__name__
        out.append(
            {
                "source_id": row["source_id"],
                "source_url": url,
                "final_url": final_url,
                "status": status,
                "ok": int(ok),
                "detail": detail,
                "checked_at_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
            }
        )
    return out


source_inventory = [
    {
        "source_id": "eqix_q1_2026_release",
        "company": "Equinix",
        "ticker": "EQIX",
        "source_type": "official_ir_release",
        "source_url": "https://investor.equinix.com/news-events/press-releases/detail/1107/equinix-reports-first-quarter-results-and-raises-full-year",
        "periods_supported": "Q1 2026, Q4 2025 comparison",
        "metrics_supported": "revenue, MRR growth, backlog language, capex guidance, atNorth",
        "caveat": "Backlog is qualitative in the release, not a dollar backlog table.",
    },
    {
        "source_id": "eqix_q4_2025_release",
        "company": "Equinix",
        "ticker": "EQIX",
        "source_type": "official_ir_release",
        "source_url": "https://investor.equinix.com/news-events/press-releases/detail/1096/equinix-provides-robust-2026-outlook-driven-by-strong",
        "periods_supported": "Q4 2025, FY2025, FY2026 guidance",
        "metrics_supported": "annualized gross bookings, MRR, interconnections, capacity delivered, capex guidance",
        "caveat": "Some operational disclosures are floors or qualitative milestones.",
    },
    {
        "source_id": "eqix_q3_2025_release",
        "company": "Equinix",
        "ticker": "EQIX",
        "source_type": "official_ir_release",
        "source_url": "https://investor.equinix.com/news-events/press-releases/detail/1086/equinix-reports-strong-third-quarter-2025-results",
        "periods_supported": "Q3 2025",
        "metrics_supported": "revenue, gross bookings, MRR growth, projects, xScale projects, land capacity",
        "caveat": "Land full-build capacity is not delivered capacity.",
    },
    {
        "source_id": "eqix_q2_2025_release",
        "company": "Equinix",
        "ticker": "EQIX",
        "source_type": "official_ir_release",
        "source_url": "https://investor.equinix.com/news-events/press-releases/detail/1076/equinix-reports-second-quarter-2025-results",
        "periods_supported": "Q2 2025",
        "metrics_supported": "revenue, gross bookings, interconnections, major projects, capex guidance",
        "caveat": "Interconnections are not a direct MW or GPU metric.",
    },
    {
        "source_id": "eqix_q1_2025_release",
        "company": "Equinix",
        "ticker": "EQIX",
        "source_type": "official_ir_release",
        "source_url": "https://investor.equinix.com/news-events/press-releases/detail/1068/equinix-reports-first-quarter-2025-results",
        "periods_supported": "Q1 2025",
        "metrics_supported": "revenue, projects, xScale projects, openings, capex guidance, Japan PPA",
        "caveat": "PPA capacity supports power procurement, not live IT load.",
    },
    {
        "source_id": "eqix_2025_10k_pdf",
        "company": "Equinix",
        "ticker": "EQIX",
        "source_type": "annual_report_pdf",
        "source_url": "https://investor.equinix.com/sec-filings/annual-reports/content/0001101239-26-000075/0001101239-26-000075.pdf",
        "periods_supported": "FY2025",
        "metrics_supported": "data-center count, xScale data centers, annual bookings context",
        "caveat": "PDF extraction should be automated in a later pass.",
    },
    {
        "source_id": "eqix_q4_2025_8k_pdf",
        "company": "Equinix",
        "ticker": "EQIX",
        "source_type": "sec_8k_pdf",
        "source_url": "https://investor.equinix.com/sec-filings/current-reports/content/0001101239-26-000030/0001101239-26-000030.pdf",
        "periods_supported": "Q4 2025",
        "metrics_supported": "official filed copy of Q4 release",
        "caveat": "Duplicate source for audit and parser testing.",
    },
    {
        "source_id": "eqix_press_index",
        "company": "Equinix",
        "ticker": "EQIX",
        "source_type": "official_press_index",
        "source_url": "https://investor.equinix.com/news-events/press-releases",
        "periods_supported": "current press release index",
        "metrics_supported": "canonical release locator",
        "caveat": "Locator source, not direct evidence for values.",
    },
]

source_by_id = {r["source_id"]: r for r in source_inventory}
raw_rows = [
    ["Q1 2025", "revenue", 2.225, "USD_billions", "eqix_q1_2026_release", "Revenues 2,444 2,420 2,225."],
    ["Q2 2025", "revenue", 2.256, "USD_billions", "eqix_q2_2025_release", "Second-Quarter 2025 Results Summary Revenues $2.256 billion."],
    ["Q3 2025", "revenue", 2.316, "USD_billions", "eqix_q3_2025_release", "Third-Quarter 2025 Results Summary Revenues $2.316 billion."],
    ["Q4 2025", "revenue", 2.420, "USD_billions", "eqix_q1_2026_release", "Revenues 2,444 2,420 2,225."],
    ["Q1 2026", "revenue", 2.444, "USD_billions", "eqix_q1_2026_release", "First-Quarter 2026 Results Summary Revenues $2.444 billion."],
    ["FY2025", "revenue", 9.217, "USD_billions", "eqix_q4_2025_release", "2025 Results Summary Revenues $9.217 billion."],
    ["Q1 2025", "recurring_revenue", 2.087, "USD_billions", "eqix_q1_2026_release", "Recurring revenues 2,331 2,294 2,087."],
    ["Q4 2025", "recurring_revenue", 2.294, "USD_billions", "eqix_q1_2026_release", "Recurring revenues 2,331 2,294 2,087."],
    ["Q1 2026", "recurring_revenue", 2.331, "USD_billions", "eqix_q1_2026_release", "Recurring revenues 2,331 2,294 2,087."],
    ["Q2 2025", "annualized_gross_bookings", 345, "USD_millions", "eqix_q2_2025_release", "$345 million in annualized gross bookings."],
    ["Q3 2025", "annualized_gross_bookings", 394, "USD_millions", "eqix_q3_2025_release", "Record annualized gross bookings of $394 million."],
    ["Q4 2025", "annualized_gross_bookings", 474, "USD_millions", "eqix_q4_2025_release", "Delivered record annualized gross bookings of $474 million in Q4."],
    ["FY2025", "annualized_gross_bookings", 1600, "USD_millions", "eqix_q4_2025_release", "delivered $1.6 billion of annualized gross bookings in 2025."],
    ["Q1 2026", "annualized_gross_bookings_record", 1, "flag", "eqix_q1_2026_release", "Delivered largest first-quarter annualized gross bookings in company's history."],
    ["Q1 2025", "major_projects", 56, "projects", "eqix_q1_2025_release", "56 major projects underway in 33 metros across 24 countries."],
    ["Q2 2025", "major_projects", 59, "projects", "eqix_q2_2025_release", "59 major projects underway in 34 metros across 25 countries."],
    ["Q3 2025", "major_projects", 58, "projects", "eqix_q3_2025_release", "58 major projects underway globally, including 12 xScale projects."],
    ["Q4 2025", "major_projects_added", 10, "projects", "eqix_q4_2025_release", "added 10 new expansion projects since October."],
    ["Q1 2025", "xscale_projects", 12, "projects", "eqix_q1_2025_release", "including 12 xScale projects."],
    ["Q2 2025", "xscale_projects", 12, "projects", "eqix_q2_2025_release", "including 12 xScale projects."],
    ["Q3 2025", "xscale_projects", 12, "projects", "eqix_q3_2025_release", "including 12 xScale projects."],
    ["Q3 2025", "land_capacity_full_build", 900, "MW", "eqix_q3_2025_release", "land acquisitions ... support over 900 megawatts of retail and xScale capacity."],
    ["FY2025", "retail_cabinets_delivered", 23250, "cabinets", "eqix_q4_2025_release", "record capacity in 2025, with 23,250 retail cabinets."],
    ["FY2025", "xscale_capacity_delivered", 90, "MW", "eqix_q4_2025_release", "record capacity in 2025, with 23,250 retail cabinets and 90+ MW of xScale capacity."],
    ["FY2025", "projects_opened", 16, "projects", "eqix_q4_2025_release", "Opened 16 projects in 14 metros globally."],
    ["Q2 2025", "total_interconnections", 492000, "connections", "eqix_q2_2025_release", "more than 492,000 total interconnections."],
    ["Q2 2025", "net_interconnections_added", 6200, "connections", "eqix_q2_2025_release", "adding 6,200 interconnections in the second quarter of 2025."],
    ["Q4 2025", "total_interconnections", 500000, "connections", "eqix_q4_2025_release", "Surpassed 500,000 interconnections globally."],
    ["Q1 2026", "mrr_growth_as_reported", 12, "percent", "eqix_q1_2026_release", "Grew monthly recurring revenue 12% on an as-reported basis."],
    ["Q1 2026", "mrr_growth_normalized_cc", 10, "percent", "eqix_q1_2026_release", "10% on a normalized and constant currency basis year over year."],
    ["Q4 2025", "mrr_growth_as_reported", 10, "percent", "eqix_q4_2025_release", "Increased Q4 monthly recurring revenue (MRR) 10% year over year."],
    ["FY2025", "mrr_growth_normalized_cc", 8, "percent", "eqix_q4_2025_release", "increased full-year MRR ... 8% on a normalized and constant currency basis."],
    ["FY2026 guidance", "nonrecurring_capex_low", 3.385, "USD_billions", "eqix_q4_2025_release", "Non-recurring Capital Expenditures (Excludes xScale) $3,385 - 3,865."],
    ["FY2026 guidance", "nonrecurring_capex_high", 3.865, "USD_billions", "eqix_q4_2025_release", "Non-recurring Capital Expenditures (Excludes xScale) $3,385 - 3,865."],
    ["FY2026 guidance revised", "nonrecurring_capex_target", 3.8, "USD_billions", "eqix_q1_2026_release", "Non-recurring Capital Expenditures ... ~$3,800."],
    ["Q1 2026", "atnorth_capacity_to_acquire", 800, "MW", "eqix_q1_2026_release", "atNorth ... expected to add ~800 MW in the Nordics."],
    ["Q1 2025", "japan_ppa_capacity", 30, "MW", "eqix_q1_2025_release", "Japan ... PPA ... securing 30 MW of renewable electricity capacity scheduled to commence in 2028."],
]

core_rows = [
    {
        "company": "Equinix",
        "ticker": "EQIX",
        "period": period,
        "metric": metric,
        "value": value,
        "unit": unit,
        "source_id": sid,
        "source_url": source_by_id[sid]["source_url"],
        "source_snippet": snippet,
        "derived_flag": int("derived" in snippet.lower()),
        "interpretation": {
            "revenue": "Base financial conversion signal for colocation and digital infrastructure demand.",
            "recurring_revenue": "Recurring revenue conversion signal tied to installed customer base.",
            "annualized_gross_bookings": "Forward demand signal for capacity leasing and customer activity.",
            "annualized_gross_bookings_record": "Qualitative booking strength signal.",
            "major_projects": "Supply pipeline breadth indicator.",
            "major_projects_added": "Incremental expansion pipeline activity.",
            "xscale_projects": "Hyperscale supply pipeline indicator.",
            "land_capacity_full_build": "Long-term land-backed capacity option.",
            "retail_cabinets_delivered": "Physical capacity delivery signal.",
            "xscale_capacity_delivered": "Hyperscale capacity delivery signal.",
            "projects_opened": "Physical project completion signal.",
            "total_interconnections": "Network density and enterprise/cloud demand signal.",
            "net_interconnections_added": "Incremental interconnection demand signal.",
            "mrr_growth_as_reported": "Recurring demand and pricing/volume signal.",
            "mrr_growth_normalized_cc": "Recurring demand signal adjusted for currency.",
            "nonrecurring_capex_low": "Forward non-recurring capex guidance.",
            "nonrecurring_capex_high": "Forward non-recurring capex guidance.",
            "nonrecurring_capex_target": "Updated capex target excluding xScale and land acquisitions.",
            "atnorth_capacity_to_acquire": "Potential Nordics AI/data-center capacity addition.",
            "japan_ppa_capacity": "Power procurement support for future operations.",
        }[metric],
        "caveat": "EQIX is a colocation/platform signal; metric bases must not be averaged with GPU cloud backlog or named campus MW.",
    }
    for period, metric, value, unit, sid, snippet in raw_rows
]

diagnostics = [
    {
        "period": "Q2 2025",
        "revenue_usd_b": 2.256,
        "bookings_usd_m": 345,
        "major_projects": 59,
        "xscale_projects": 12,
        "read": "Strong bookings and a broad project pipeline before Q3 acceleration.",
    },
    {
        "period": "Q3 2025",
        "revenue_usd_b": 2.316,
        "bookings_usd_m": 394,
        "major_projects": 58,
        "xscale_projects": 12,
        "read": "Bookings rose 14% sequentially while project pipeline stayed broad.",
    },
    {
        "period": "Q4 2025",
        "revenue_usd_b": 2.420,
        "bookings_usd_m": 474,
        "major_projects": "10 added",
        "xscale_projects": "",
        "read": "Record bookings and delivered capacity show supply-side conversion.",
    },
    {
        "period": "Q1 2026",
        "revenue_usd_b": 2.444,
        "bookings_usd_m": "record Q1",
        "major_projects": "",
        "xscale_projects": "",
        "read": "MRR growth and backlog language indicate continued demand into 2026.",
    },
]

signal_scores = [
    ["colocation_demand", 8, "Bookings rose from $345M in Q2 to $474M in Q4 2025.", "Strong forward demand indicator.", "Quantify Q1 2026 bookings once dollar value is disclosed."],
    ["capacity_delivery", 8, "FY2025 delivered 23,250 retail cabinets and over 90 MW of xScale capacity.", "Physical delivery evidence is better than pure financial backlog.", "Break delivery by metro and customer type."],
    ["xscale_pipeline", 8, "Twelve xScale projects were underway in Q1-Q3 2025.", "Hyperscale supply expansion is visible.", "Extract xScale leased MW and backlog by quarter."],
    ["interconnection_density", 7, "Interconnections surpassed 500,000 globally.", "Network density supports AI/cloud ecosystem demand.", "Separate AI-related interconnection pull-through from normal growth."],
    ["capex_supply_signal", 7, "FY2026 non-recurring capex target is around $3.8B excluding xScale and land.", "Capex is meaningful but excludes some hyperscale and land components.", "Add total capex including xScale where disclosed."],
    ["forecast_actual_quality", 7, "Projects opened and capacity delivered are disclosed, but named forecasts are incomplete.", "Better physical supply signal than ORCL, less campus-specific than APLD.", "Build site-level opening ledger."],
    ["overall_signal_quality", 8, "EQIX adds colocation and xScale supply-side evidence to APLD, CRWV, and ORCL.", "Useful missing lens for the gauge.", "Convert into the common schema next."],
]
score_rows = [
    {"signal_dimension": a, "score_1_10": b, "evidence": c, "why_score": d, "what_to_verify_next": e}
    for a, b, c, d, e in signal_scores
]

comparison_rows = [
    {"company": "Applied Digital", "ticker": "APLD", "best_signal": "named campus MW delivery", "strength": "Forecast-vs-actual physical delivery.", "weakness": "Small revenue scale.", "role_in_research": "Project execution lens."},
    {"company": "CoreWeave", "ticker": "CRWV", "best_signal": "AI cloud backlog/revenue/power", "strength": "Scaled demand conversion.", "weakness": "Less facility-level detail.", "role_in_research": "AI cloud demand lens."},
    {"company": "Oracle", "ticker": "ORCL", "best_signal": "RPO, OCI revenue, capex, leases", "strength": "Hyperscaler demand and financing signal.", "weakness": "Weak named facility ledger.", "role_in_research": "Hyperscaler commitment lens."},
    {"company": "Equinix", "ticker": "EQIX", "best_signal": "colocation/xScale bookings and delivery", "strength": "Supply-side project and interconnection signal.", "weakness": "Backlog dollars and AI split are incomplete.", "role_in_research": "Colocation and xScale supply lens."},
]

decision_rows = [
    {"priority": 1, "next_work": "Build common schema across APLD, CRWV, ORCL, and EQIX.", "why": "Company packs now cover four complementary signal types.", "acceptance_test": "One CSV maps metric basis, forecast/actual class, source, snippet, and confidence."},
    {"priority": 2, "next_work": "Extract site-level EQIX openings and xScale MW.", "why": "EQIX can become a stronger forecast-vs-actual supply ledger.", "acceptance_test": "At least 15 site/project rows with opening or delivery status."},
    {"priority": 3, "next_work": "Add DLR back into the common schema.", "why": "DLR is closest EQIX peer and already has prior artifacts.", "acceptance_test": "DLR rows converted to the same fields as EQIX."},
    {"priority": 4, "next_work": "Add NVDA lag test after common schema.", "why": "Original question is semiconductor-cycle early warning.", "acceptance_test": "Differenced, basis-safe lead/lag test with short-history warning."},
    {"priority": 5, "next_work": "Automate source fetching and snippet verification.", "why": "Manual package building will not scale to 15-20 companies.", "acceptance_test": "Extractor reruns from source inventory and reproduces evidence rows."},
]

loop_rows = [
    [1, "Use EQIX to add colocation and xScale supply evidence.", "Built official EQIX source spine across Q1 2026 and 2025 releases.", 9, "Sources are primary and current.", "Extract dense evidence rows."],
    [2, "Extract revenue, bookings, project, xScale, interconnection, capex, and capacity rows.", "Created 37 source-traced evidence rows with snippets and caveats.", 9, "Rows span eight metric families and six periods.", "Compute signal diagnostics."],
    [3, "Summarize bookings against project pipeline and revenue.", "Built quarterly diagnostics from Q2 2025 through Q1 2026.", 8, "Diagnostics are useful but not yet site-specific.", "Score the signal."],
    [4, "Score EQIX as a gauge input.", "Created seven signal scores with evidence and next verification needs.", 9, "Scores distinguish strong supply evidence from AI attribution gaps.", "Compare all four companies."],
    [5, "Compare APLD, CRWV, ORCL, and EQIX roles.", "Built role comparison showing EQIX as colocation and xScale supply lens.", 9, "Comparison clarifies complementarity without averaging bases.", "Write memo."],
    [6, "Write direct memo against the original research question.", "Wrote answer, evidence, call, watch items, remaining gaps, and falsifiers.", 9, "Memo is clear about strengths and gaps.", "Build dashboard."],
    [7, "Publish an auditable dashboard.", "Rendered source inventory, fetch log, evidence, diagnostics, scores, comparison, and backlog.", 8, "Readable HTML; plotted charts remain future work.", "Validate with company gate."],
    [8, "Validate using stricter company-deep-dive gate.", "Added validator invoking harness production_gate_failures plus EQIX-specific checks.", 9, "Gate catches shallow evidence and missing fetch logs.", "Publish URL."],
    [9, "Publish and verify served page.", "Copied dashboard to public directory and curl-checked served URL.", 9, "The public page is directly auditable.", "Decide next work."],
    [10, "Choose next step toward useful production result.", "Recommended common schema across four company packs before more company loops.", 9, "Schema is now higher leverage than another one-off pack.", "Build the common schema next."],
]
loop_summary = [
    {"iteration": a, "chose_20_words": b, "did_20_words": c, "score_1_10": d, "score_reason_20_words": e, "next_20_words": f}
    for a, b, c, d, e, f in loop_rows
]

memo = """# Equinix Strategic Research Memo

## Original Question

Can company-level forecast-vs-actual evidence help us build an early-warning read on AI data-center buildout and downstream semiconductor demand?

## Answer

Yes. EQIX adds the missing colocation and xScale supply-side lens. It is not a GPU-cloud backlog signal like CRWV and not a hyperscaler RPO signal like Oracle, but it shows whether digital infrastructure operators are actually leasing, opening, connecting, and funding capacity.

## Evidence

- Annualized gross bookings rose from $345M in Q2 2025 to $474M in Q4 2025.
- EQIX delivered record FY2025 capacity: 23,250 retail cabinets and more than 90 MW of xScale capacity.
- It opened 16 projects in 14 metros in FY2025.
- It had 56-59 major projects underway through 2025, including 12 xScale projects in Q1-Q3.
- Interconnections surpassed 500,000 globally, and Q1 2026 MRR grew 12% as reported.

## Call

EQIX is a strong supply-side gauge input. It helps answer whether capacity is being leased and delivered across the colocation ecosystem. It should be used beside APLD, CRWV, and ORCL, not averaged with them.

## Watch Items

1. Does EQIX disclose Q1 2026 annualized gross bookings dollars?
2. Do xScale leased MW and delivered MW continue growing?
3. Are 2026 projects opening on schedule?
4. Does atNorth add the expected Nordics capacity without delays?
5. Do interconnections and MRR keep confirming enterprise/cloud demand?

## Remaining Gaps

The biggest gap is site-level forecast versus actual delivery. This package captures project counts, delivered capacity, bookings, and interconnections, but does not yet map each announced project to opening date, leased MW, customer type, and actual capacity delivered.

## What Would Change The View

- More bullish: bookings stay elevated, xScale delivered MW grows, and projects open on schedule.
- More bearish: bookings fade, project openings slip, or capex guidance is reduced without replacement demand.
"""


def table(title: str, rows: list[dict[str, object]]) -> str:
    headers = list(rows[0])
    out = [f"<h2>{html.escape(title)}</h2>", "<table><thead><tr>"]
    out += [f"<th>{html.escape(h)}</th>" for h in headers]
    out.append("</tr></thead><tbody>")
    for row in rows:
        out.append("<tr>")
        for h in headers:
            value = row[h]
            if h.endswith("url"):
                out.append(f'<td><a href="{html.escape(str(value))}">source</a></td>')
            else:
                out.append(f"<td>{html.escape(str(value))}</td>")
        out.append("</tr>")
    out.append("</tbody></table>")
    return "\n".join(out)


def make_dashboard() -> None:
    cards = [
        ("Q1 2026 Revenue", "$2.444B", "10% YoY"),
        ("Q4 Bookings", "$474M", "record annualized gross bookings"),
        ("FY2025 Delivered", "90+ MW", "xScale capacity"),
        ("Interconnections", "500K+", "global total"),
    ]
    card_html = "\n".join(f"<section><strong>{a}</strong><b>{b}</b><span>{c}</span></section>" for a, b, c in cards)
    page = f"""<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>EQIX Strategic 10-Loop Research</title>
  <style>
    body {{ margin:0; font-family:Arial,sans-serif; background:#f7f8fa; color:#18202a; }}
    header {{ background:#173f35; color:white; padding:28px 34px; }}
    main {{ padding:24px 34px 60px; }}
    .cards {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:12px; margin:18px 0; }}
    section, article {{ background:white; border:1px solid #dfe4ea; border-radius:8px; padding:14px; }}
    .cards strong,.cards span {{ display:block; color:#53606f; font-size:13px; }}
    .cards b {{ display:block; font-size:28px; margin:8px 0; }}
    article {{ max-width:980px; line-height:1.45; }}
    table {{ width:100%; border-collapse:collapse; background:white; font-size:13px; margin-bottom:20px; }}
    th,td {{ border:1px solid #dde3ea; padding:8px; vertical-align:top; }}
    th {{ background:#eef2f6; text-align:left; }}
  </style>
</head>
<body>
  <header><h1>EQIX Strategic 10-Loop Research</h1><p>Equinix as colocation, xScale, interconnection, and supply-side delivery signal.</p></header>
  <main>
    <div class="cards">{card_html}</div>
    <article>{html.escape(memo).replace(chr(10), "<br>")}</article>
    {table("Loop Summary", loop_summary)}
    {table("Source Inventory", source_inventory)}
    {table("Source Fetch Log", fetch_log(source_inventory))}
    {table("Core Evidence Ledger", core_rows)}
    {table("Diagnostics", diagnostics)}
    {table("Signal Scores", score_rows)}
    {table("Company Role Comparison", comparison_rows)}
    {table("Decision Backlog", decision_rows)}
  </main>
</body>
</html>
"""
    (BASE / "strategic_eqix_dashboard.html").write_text(page, encoding="utf-8")
    (PUBLIC / "index.html").write_text(page, encoding="utf-8")
    (SERVED / "index.html").write_text(page, encoding="utf-8")


def main() -> None:
    write_csv("eqix_source_inventory.csv", source_inventory)
    write_csv("eqix_source_fetch_log.csv", fetch_log(source_inventory))
    write_csv("eqix_core_evidence_rows.csv", core_rows)
    write_csv("eqix_diagnostics.csv", diagnostics)
    write_csv("eqix_signal_scores.csv", score_rows)
    write_csv("eqix_company_role_comparison.csv", comparison_rows)
    write_csv("eqix_decision_backlog.csv", decision_rows)
    write_csv("strategic10_loop_summary.csv", loop_summary)
    (BASE / "eqix_one_page_memo.md").write_text(memo, encoding="utf-8")
    make_dashboard()
    state = {
        "run_id": "strategic10-eqix-001",
        "status": "done",
        "built_at_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
        "public_url": "http://204.168.210.83:8130/research-loop/strategic10-eqix-001/index.html",
    }
    (RUN / "state.json").write_text(json.dumps(state, indent=2) + "\n", encoding="utf-8")
    print("built strategic10 EQIX package")


if __name__ == "__main__":
    main()
