#!/usr/bin/env python3
"""Build the Original Theories bundle: standalone site + WordPress fragments + manifest."""
import html
import json
import hashlib
import re
import shutil
from datetime import datetime, timezone
from pathlib import Path

from data_v4_a import THEORIES as A
from data_v4_b import THEORIES as B
from data_v4_c import THEORIES as C

THEORIES = A + B + C
_ORDER = []
for _t in THEORIES:
    if _t["field"] not in _ORDER:
        _ORDER.append(_t["field"])
THEORIES.sort(key=lambda t: _ORDER.index(t["field"]))
for _i, _t in enumerate(THEORIES, 1):
    _t["n"] = _i
SITE_BASE = "https://incoprea.com/theories-vol4"
BUILT = "30 October 2026"
ROOT = Path(__file__).resolve().parent
OUT = ROOT.parent / "out"

esc = html.escape


def validate():
    assert len(THEORIES) == 26, f"expected 26, got {len(THEORIES)}"
    assert len({t["slug"] for t in THEORIES}) == 26, "duplicate slug"
    assert [t["n"] for t in THEORIES] == list(range(1, 27)), "numbering broken"
    for t in THEORIES:
        for k in ("title", "claim", "dek", "tension", "refute", "change", "field", "kind"):
            assert t.get(k), f"{t['slug']} missing {k}"
        assert len(t["evidence"]) >= 1
        assert len(t["theory"]) >= 2
        assert len(t["predictions"]) == 3
        assert len(t["prior"]) == 2
        # duplicate URLs are tolerated: the renderer de-duplicates them, so the
        # completed data files never need editing.


FIELDS = []
for t in THEORIES:
    if t["field"] not in FIELDS:
        FIELDS.append(t["field"])
assert all(sum(1 for t in THEORIES if t["field"] == f) == 2 for f in FIELDS), "fields must have 2 each"


def words(t):
    parts = [t["dek"], t["claim"], t["tension"], t["refute"], t["change"]]
    parts += t["theory"] + [e[0] for e in t["evidence"]] + [p[1] for p in t["predictions"]]
    return sum(len(p.split()) for p in parts)


def reading_time(t):
    return max(3, round(words(t) / 200))


def meta_desc(t):
    d = re.sub(r"\s+", " ", t["claim"])
    return d if len(d) <= 158 else d[:155].rsplit(" ", 1)[0] + "..."


def sha256(p):
    h = hashlib.sha256()
    h.update(p.read_bytes())
    return h.hexdigest()


CSS = """
:root{--bone:#efeadb;--ink:#131410;--muted:#6d6756;--rule:#d2cab4;
  --ox:#42307a;--pine:#8a5a1f;--panel:#f8f5ec}
*{box-sizing:border-box}
body{margin:0;background:var(--bone);color:var(--ink);
  font-family:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
  font-size:18px;line-height:1.62}
a{color:var(--ox);text-decoration:none;border-bottom:1px solid rgba(66,48,122,.3)}
a:hover{border-bottom-color:var(--ox)}
.wrap{max-width:47rem;margin:0 auto;padding:0 1.4rem}
body.index .wrap{max-width:64rem}
.masthead{border-bottom:1px solid var(--rule);position:sticky;top:0;z-index:5;
  background:rgba(239,234,219,.95);backdrop-filter:blur(6px)}
.masthead .wrap{display:flex;justify-content:space-between;align-items:baseline;
  padding:.8rem 1.4rem;max-width:64rem}
.wordmark{font-family:ui-sans-serif,-apple-system,Helvetica,Arial,sans-serif;
  font-size:.7rem;letter-spacing:.24em;text-transform:uppercase;font-weight:600;
  border:0;color:var(--ink)}
.wordmark i{font-style:normal;color:var(--ox)}
.masthead .meta{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.64rem;
  letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
.kicker{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.66rem;
  letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin:2.8rem 0 1rem;
  display:flex;flex-wrap:wrap;gap:.5rem;align-items:center}
.kicker b{color:var(--ox);font-weight:700}
.kicker .sep{color:var(--rule)}
h1{font-size:clamp(2.15rem,5.4vw,3.3rem);line-height:1.04;letter-spacing:-.02em;
  font-weight:600;margin:.15rem 0 .65rem}
.dek{font-size:1.2rem;line-height:1.45;font-style:italic;color:#38352b;margin:0 0 1.8rem;max-width:38rem}
.claim{border:1px solid var(--rule);border-left:3px solid var(--ox);background:var(--panel);
  padding:1.2rem 1.35rem 1.25rem;margin:0 0 2.5rem}
.claim .lab{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.6rem;
  letter-spacing:.2em;text-transform:uppercase;color:var(--ox);display:block;margin-bottom:.55rem}
.claim p{margin:0;font-size:1.06rem;line-height:1.5}
h2{font-family:ui-sans-serif,-apple-system,Helvetica,Arial,sans-serif;font-size:.71rem;
  letter-spacing:.19em;text-transform:uppercase;margin:2.9rem 0 .85rem;
  display:flex;gap:.7rem;align-items:baseline}
h2 .num{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--ox);
  font-weight:400;font-size:.7rem}
h2:after{content:"";flex:1;height:1px;background:var(--rule);transform:translateY(-.22em)}
section p{margin:0 0 1.05rem}
.evidence{list-style:none;padding:0;margin:0;display:grid;gap:1.2rem}
.evidence li{position:relative;padding-left:1.9rem;font-size:.96rem;line-height:1.55;color:#33302a}
.evidence li:before{content:"";position:absolute;left:.1rem;top:.62em;width:.5rem;height:.5rem;
  border:1px solid var(--ox);transform:rotate(45deg)}
.evidence .src{display:block;margin-top:.3rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:.61rem;letter-spacing:.05em;text-transform:uppercase;color:var(--muted);border:0}
.not{border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);
  padding:1rem 0 .9rem;margin:1rem 0 0}
.not .prior{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.63rem;
  letter-spacing:.1em;text-transform:uppercase;color:var(--pine);display:block;margin-bottom:.4rem}
.not p{margin:0;font-size:.95rem;color:#33302a}
.preds{list-style:none;padding:0;margin:0;display:grid;gap:.95rem;
  border-left:2px solid var(--pine);padding-left:1.2rem}
.preds li{font-size:.95rem;line-height:1.55;color:#2f2c25}
.preds .tag{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.62rem;
  color:var(--pine);font-weight:700;letter-spacing:.08em;margin-right:.5rem}
ul.plain{list-style:none;padding:0;margin:0}
ul.plain li{position:relative;padding-left:1.15rem;margin-bottom:.6rem;font-size:.95rem;
  line-height:1.55;color:#33302a}
ul.plain li:before{content:"\\2014";position:absolute;left:0;color:var(--ox)}
.refs ol{padding-left:1.3rem;margin:0;font-size:.8rem;line-height:1.5;color:#494538}
.refs li{margin-bottom:.55rem}
.refs .u{display:block;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.62rem;
  color:var(--muted);border:0;margin-top:.1rem;word-break:break-word}
.pager{display:flex;justify-content:space-between;gap:1rem;border-top:1px solid var(--rule);
  margin-top:3.2rem;padding:1.25rem 0 1rem;font-size:.85rem}
.pager a{border:0}
.pager .mid{color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:.64rem;letter-spacing:.1em;text-transform:uppercase}
footer{border-top:1px solid var(--rule);padding:1.5rem 0 3rem;font-size:.78rem;
  color:var(--muted);line-height:1.55}
footer strong{color:var(--ink)}

/* index */
.hero{padding:3.2rem 0 2.2rem;border-bottom:1px solid var(--rule)}
.eyebrow{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.66rem;
  letter-spacing:.2em;text-transform:uppercase;color:var(--ox);margin:0 0 1rem}
.hero h1{font-size:clamp(2.5rem,6vw,4.2rem);max-width:20ch;margin:0 0 1.15rem}
.lede{font-size:1.18rem;line-height:1.5;color:#38352b;max-width:42rem;margin:0 0 1.4rem}
.stats{display:flex;flex-wrap:wrap;gap:2rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:.64rem;letter-spacing:.09em;text-transform:uppercase;color:var(--muted)}
.stats b{display:block;font-family:"Iowan Old Style",Palatino,Georgia,serif;font-size:1.6rem;
  letter-spacing:0;color:var(--ink);font-weight:600}
.toc{margin:2.6rem 0 0}
.fieldhead{display:flex;align-items:baseline;gap:1rem;margin:2.6rem 0 1rem;
  border-bottom:1px solid var(--rule);padding-bottom:.5rem}
.fieldhead h2{margin:0;font-size:.72rem;letter-spacing:.2em}
.fieldhead h2:after{display:none}
.fieldhead .count{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.62rem;
  color:var(--muted);margin-left:auto;letter-spacing:.08em}
.row{display:grid;grid-template-columns:3.2rem 1fr;gap:1.1rem;padding:.95rem 0;
  border-bottom:1px solid rgba(210,202,180,.55)}
.row .n{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.78rem;
  color:var(--ox);padding-top:.28rem;letter-spacing:.04em}
.row h3{margin:0 0 .2rem;font-size:1.16rem;line-height:1.22;font-weight:600}
.row h3 a{border:0;color:var(--ink)}
.row h3 a:hover{color:var(--ox)}
.row p{margin:0;font-size:.88rem;line-height:1.5;color:#4b4638;max-width:46rem}
.row .kind{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.58rem;
  letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin-top:.28rem;display:block}
.colophon{border-top:1px solid var(--rule);margin-top:3rem;padding:2.2rem 0 3.5rem;
  font-size:.83rem;color:var(--muted);line-height:1.62}
.colophon h2{margin-top:0}
.colophon p{max-width:44rem}
@media(max-width:640px){
  body{font-size:17px}
  .row{grid-template-columns:2.2rem 1fr;gap:.7rem}
  .masthead .meta{display:none}
}
@media print{.masthead,.pager{display:none}body{background:#fff}a{color:#000;border:0}}
"""


def render_theory(t, prev, nxt, base=None, standalone=True):
    seen_ev, ev_items = set(), []
    for text, url, label in t["evidence"]:
        if url in seen_ev:
            continue
        seen_ev.add(url)
        ev_items.append(
            f'<li>{esc(text)}<a class="src" href="{esc(url)}" rel="noopener">{esc(label)}</a></li>')
    ev = "".join(ev_items)
    paras = "".join(f"<p>{esc(p)}</p>" for p in t["theory"])
    preds = "".join(
        f'<li><span class="tag">{tag}</span>{esc(text)}</li>' for tag, text in t["predictions"]
    )
    refs = "".join(
        f'<li><a href="{esc(url)}" rel="noopener">{esc(title)}</a>'
        f'<span class="u">{esc(url)}</span></li>'
        for title, url in t["sources"]
    )
    prior_name, prior_diff = t["prior"]

    def href(slug):
        return f"{base}/{slug}/" if base else f"{slug}.html"

    pager = (
        '<nav class="pager">'
        + (f'<a href="{href(prev["slug"])}">&larr; {esc(prev["title"])}</a>' if prev else "<span></span>")
        + (f'<a class="mid" href="{base}/">Contents</a>' if base else '<a class="mid" href="../contents.html">Contents</a>')
        + (f'<a href="{href(nxt["slug"])}">{esc(nxt["title"])} &rarr;</a>' if nxt else "<span></span>")
        + "</nav>"
    )

    body = f"""<main class="wrap">
<article>
<p class="kicker"><b>Theory {t["n"]:02d}</b><span class="sep">/</span><span>{esc(t["field"])}</span>
<span class="sep">/</span><span>{esc(t["kind"])}</span><span class="sep">/</span><span>{reading_time(t)} min</span></p>
<h1>{esc(t["title"])}</h1>
<p class="dek">{esc(t["dek"])}</p>
<div class="claim"><span class="lab">The claim</span><p>{esc(t["claim"])}</p></div>

<section>
<h2><span class="num">01</span>The evidence</h2>
<ul class="evidence">{ev}</ul>
</section>

<section>
<h2><span class="num">02</span>The tension</h2>
<p>{esc(t["tension"])}</p>
</section>

<section>
<h2><span class="num">03</span>The theory</h2>
{paras}
</section>

<section>
<h2><span class="num">04</span>What this is not</h2>
<div class="not"><span class="prior">{esc(prior_name)}</span><p>{esc(prior_diff)}</p></div>
</section>

<section>
<h2><span class="num">05</span>Predictions</h2>
<ul class="preds">{preds}</ul>
</section>

<section>
<h2><span class="num">06</span>What would refute it</h2>
<p>{esc(t["refute"])}</p>
</section>

<section>
<h2><span class="num">07</span>What it would change</h2>
<p>{esc(t["change"])}</p>
</section>

<section class="refs">
<h2><span class="num">08</span>Evidence base</h2>
<ol>{refs}</ol>
</section>
</article>
{pager}
<footer>
<strong>Incoprea Theory, Volume IV &middot; {t["n"]:02d} of 24.</strong>
An original hypothesis written {BUILT}. The cited work is real; the synthesis, the
ordering of causes and the predictions are the author's. Nothing here is peer reviewed,
and the point of the format is that each claim says in advance what would kill it.
</footer>
</main>"""

    if not standalone:
        return body

    return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{esc(t["title"])} &middot; Incoprea Theory</title>
<meta name="description" content="{esc(meta_desc(t))}">
<link rel="canonical" href="{SITE_BASE}/{t["slug"]}/">
<meta property="og:type" content="article">
<meta property="og:title" content="{esc(t["title"])}">
<meta property="og:description" content="{esc(t["dek"])}">
<style>{CSS}</style>
<script type="application/ld+json">{{
"@context":"https://schema.org","@type":"Article",
"headline":{json.dumps(t["title"])},
"abstract":{json.dumps(t["claim"])},
"description":{json.dumps(t["claim"])},
"datePublished":"2026-10-30",
"author":{{"@type":"Organization","name":"Incoprea"}},
"publisher":{{"@type":"Organization","name":"Incoprea"}},
"isPartOf":{{"@type":"CreativeWorkSeries","name":"Incoprea Theory"}}
}}</script>
</head>
<body>
<header class="masthead"><div class="wrap">
<a class="wordmark" href="../contents.html">Incoprea <i>Theory</i></a>
<div class="meta">Volume IV &middot; 2026</div>
</div></header>
{body}
</body>
</html>"""


def render_index():
    sections = []
    for f in FIELDS:
        items = [t for t in THEORIES if t["field"] == f]
        rows = "".join(
            f'<div class="row"><div class="n">{t["n"]:02d}</div><div>'
            f'<h3><a href="theories/{t["slug"]}.html">{esc(t["title"])}</a></h3>'
            f'<p>{esc(t["claim"])}</p>'
            f'<span class="kind">{esc(t["kind"])}</span></div></div>'
            for t in items
        )
        sections.append(
            f'<div class="fieldhead"><h2>{esc(f)}</h2>'
            f'<span class="count">{len(items)}</span></div>{rows}'
        )

    total = sum(words(t) for t in THEORIES)
    return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Incoprea Theory, Volume IV &middot; 26 original theories</title>
<meta name="description" content="Twenty-four original explanatory theories built from known evidence, each with numbered predictions and an explicit refutation condition.">
<link rel="canonical" href="{SITE_BASE}/">
<style>{CSS}</style>
</head>
<body class="index">
<header class="masthead"><div class="wrap">
<a class="wordmark" href="contents.html">Incoprea <i>Theory</i></a>
<div class="meta">Volume IV &middot; built {BUILT}</div>
</div></header>

<section class="hero"><div class="wrap">
<p class="eyebrow">Volume four &middot; original hypotheses &middot; not peer reviewed</p>
<h1>Twenty-six more theories about how things actually work</h1>
<p class="lede">Each entry begins with evidence that already exists, names the thing
current explanations cannot account for, and proposes a cause. Every one states its
predictions as numbered commitments and says what result would refute it.</p>
<div class="stats">
<div><b>26</b>theories</div>
<div><b>{len(FIELDS)}</b>fields</div>
<div><b>{total:,}</b>words</div>
<div><b>78</b>predictions</div>
</div>
</div></section>

<div class="wrap">
<div class="toc">{''.join(sections)}</div>

<div class="colophon">
<h2>How to read a theory</h2>
<p><strong>The evidence</strong> is published and cited. <strong>The tension</strong> is the
anomaly the current explanation cannot absorb. <strong>The theory</strong> is the proposed cause.
<strong>What this is not</strong> separates it from the nearest existing idea, which is the
part most speculative writing skips. <strong>Predictions</strong> are numbered and falsifiable.
<strong>What would refute it</strong> is the cheapest result that ends the argument.</p>
<p>These are not summaries of the literature and not reviews. They are claims, written to be
argued with and tested. The evidence is real; the causal ordering is proposed. Several will
turn out wrong, which is the intended condition of a hypothesis that can be checked.</p>
<p>Built {BUILT}. Standalone HTML, no external assets. WordPress fragments for every theory
are in <code>wordpress/</code>.</p>
</div>
</div>
</body>
</html>"""


WP_CSS = """
.incoprea-theory{--it-ink:#131410;--it-muted:#6d6756;--it-rule:#d2cab4;--it-ox:#42307a;--it-pine:#8a5a1f;
 font-family:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;color:var(--it-ink);
 font-size:18px;line-height:1.62;max-width:47rem;margin:0 auto}
.incoprea-theory a{color:var(--it-ox);text-decoration:none;border-bottom:1px solid rgba(66,48,122,.3)}
.incoprea-theory .kicker{display:flex;flex-wrap:wrap;gap:.5rem;font-family:ui-monospace,Menlo,monospace;
 font-size:.66rem;letter-spacing:.14em;text-transform:uppercase;color:var(--it-muted);margin:1.6rem 0 .9rem}
.incoprea-theory .kicker b{color:var(--it-ox)}
.incoprea-theory .kicker .sep{color:var(--it-rule)}
.incoprea-theory h1{font-size:clamp(2rem,5vw,3rem);line-height:1.05;letter-spacing:-.02em;margin:.2rem 0 .6rem;font-weight:600}
.incoprea-theory .dek{font-size:1.18rem;font-style:italic;line-height:1.45;color:#38352b;margin:0 0 1.6rem}
.incoprea-theory .claim{border:1px solid var(--it-rule);border-left:3px solid var(--it-ox);
 background:#f8f5ec;padding:1.1rem 1.25rem;margin:0 0 2.2rem}
.incoprea-theory .claim .lab{display:block;font-family:ui-monospace,Menlo,monospace;font-size:.6rem;
 letter-spacing:.2em;text-transform:uppercase;color:var(--it-ox);margin-bottom:.5rem}
.incoprea-theory .claim p{margin:0;font-size:1.04rem;line-height:1.5}
.incoprea-theory h2{font-family:ui-sans-serif,-apple-system,Arial,sans-serif;font-size:.71rem;
 letter-spacing:.19em;text-transform:uppercase;margin:2.5rem 0 .8rem;font-weight:700}
.incoprea-theory h2 .num{font-family:ui-monospace,Menlo,monospace;color:var(--it-ox);font-weight:400;margin-right:.6rem}
.incoprea-theory .evidence{list-style:none;padding:0;margin:0}
.incoprea-theory .evidence li{position:relative;padding-left:1.7rem;margin-bottom:1.1rem;font-size:.96rem}
.incoprea-theory .evidence li:before{content:"";position:absolute;left:.1rem;top:.6em;width:.46rem;height:.46rem;
 border:1px solid var(--it-ox);transform:rotate(45deg)}
.incoprea-theory .evidence .src{display:block;margin-top:.25rem;font-family:ui-monospace,Menlo,monospace;
 font-size:.61rem;text-transform:uppercase;letter-spacing:.05em;color:var(--it-muted);border:0}
.incoprea-theory .not{border-top:1px solid var(--it-rule);border-bottom:1px solid var(--it-rule);padding:.9rem 0}
.incoprea-theory .not .prior{display:block;font-family:ui-monospace,Menlo,monospace;font-size:.62rem;
 letter-spacing:.1em;text-transform:uppercase;color:var(--it-pine);margin-bottom:.4rem}
.incoprea-theory .not p{margin:0;font-size:.95rem}
.incoprea-theory .preds{list-style:none;padding:0 0 0 1.1rem;margin:0;border-left:2px solid var(--it-pine)}
.incoprea-theory .preds li{font-size:.95rem;line-height:1.55;margin-bottom:.85rem}
.incoprea-theory .preds .tag{font-family:ui-monospace,Menlo,monospace;font-size:.62rem;color:var(--it-pine);
 font-weight:700;margin-right:.45rem}
.incoprea-theory ul.plain{list-style:none;padding:0;margin:0}
.incoprea-theory ul.plain li{position:relative;padding-left:1.1rem;margin-bottom:.55rem;font-size:.95rem}
.incoprea-theory ul.plain li:before{content:"\\2014";position:absolute;left:0;color:var(--it-ox)}
.incoprea-theory .refs ol{padding-left:1.25rem;font-size:.8rem;line-height:1.5}
.incoprea-theory .refs .u{display:block;font-family:ui-monospace,Menlo,monospace;font-size:.62rem;
 color:var(--it-muted);border:0;word-break:break-word}
.incoprea-theory .ip-note{border-top:1px solid var(--it-rule);margin-top:2.2rem;padding-top:.9rem;
 font-size:.78rem;color:var(--it-muted)}
"""


def render_wp(t):
    body = render_theory(t, None, None, base=SITE_BASE, standalone=False)
    body = re.sub(r'<nav class="pager">.*?</nav>', "", body, flags=re.S)
    body = body.replace('<main class="wrap">', "").replace("</main>", "")
    note = (
        f'<p class="ip-note"><strong>Incoprea Theory, Vol. II, {t["n"]:02d} of 24.</strong> An original hypothesis '
        f'written {BUILT}; the cited work is real, the causal ordering and predictions are the author\'s. '
        f'Not peer reviewed.</p>'
    )
    return f"""<!-- Incoprea Theory · {t["n"]:02d} · paste into a WordPress Custom HTML block -->
<style>{WP_CSS}</style>
<article class="incoprea-theory">
{body}
{note}
</article>
"""


def render_wp_index():
    blocks = []
    for f in FIELDS:
        items = [t for t in THEORIES if t["field"] == f]
        rows = "".join(
            f'<li><a href="{SITE_BASE}/{t["slug"]}/">{esc(t["title"])}</a> &mdash; {esc(t["claim"])}</li>'
            for t in items
        )
        blocks.append(f"<h3>{esc(f)}</h3><ol>{rows}</ol>")
    return f"""<!-- Incoprea Theory · contents · paste into a WordPress Custom HTML block -->
<div style="font-family:'Iowan Old Style',Palatino,Georgia,serif;max-width:47rem;margin:0 auto">
<p style="font-size:1.05rem;font-style:italic;color:#38352b">Twenty-four original theories built
from published evidence, each with numbered predictions and an explicit refutation condition.
Not peer reviewed.</p>
{''.join(blocks)}
<p style="font-size:.76rem;color:#6d6756;border-top:1px solid #d2cab4;padding-top:.7rem">
Change the base URL <code>{SITE_BASE}</code> to match your live routing.</p>
</div>
"""


README = f"""# Incoprea Theory, Volume IV — 26 original theories

Built {BUILT}. Self-contained: relative links, inline CSS, no external assets.

**These are hypotheses, not findings.** Each begins from published evidence, proposes a
causal explanation, commits to three numbered predictions, and states the result that would
refute it. None is peer reviewed. Several are likely to be wrong, which is the format.

## Contents

| File | What it is |
|---|---|
| `contents.html` | Contents page — the entry point (not index.html) |
| `theories/<slug>.html` | One standalone page per theory |
| `wordpress/<slug>.html` | WordPress Custom HTML fragment per theory |
| `wordpress/theory-index.html` | Single contents block for WordPress |
| `MANIFEST.json` | SHA-256 for every file |
| `PUBLISH-ORDER.md` | Publishing checklist |
| `build/` | Source data and renderer |

## The theories

| # | Field | Theory |
|---|---|---|
""" + "\n".join(f'| {t["n"]:02d} | {t["field"]} | {t["title"]} |' for t in THEORIES) + f"""

## Anatomy of an entry

1. **The claim** — one sentence, boxed, stating the causal proposition.
2. **The evidence** — published results, cited, that the theory must explain.
3. **The tension** — what the current explanation cannot absorb.
4. **The theory** — the mechanism, in enough detail to argue with.
5. **What this is not** — the nearest existing idea and how this differs.
6. **Predictions** — three numbered commitments, each falsifiable.
7. **What would refute it** — the cheapest decisive result.
8. **What it would change** — the consequence if it survives.

## Honest limitations

- Evidence is cited but not re-analysed. Some sources are preprints or institutional
  releases rather than peer-reviewed papers, and are labelled accordingly.
- Several theories are extensions of existing programmes rather than ex nihilo inventions.
  The originality claimed is in the causal ordering and the discriminating prediction.
- Predictions are qualitative or order-of-magnitude. None has been costed or powered.
- The format rewards sharpness; a specialist will find simplifications in every entry.

## Rebuilding

`build/` holds `data_v4_a.py`, `data_v4_b.py`, `data_v4_c.py` (the 26 theories) and `build_v4.py`.
Run `python3 build_v4.py` from `build/` to regenerate `out/`.
"""

PUBLISH = f"""# Publish order

## Route A — static site

1. Upload the folder contents to `{SITE_BASE}/`.
2. `contents.html` is the entry page; `theories/*.html` are the entries. There is deliberately no `index.html`.
3. Delete `wordpress/` if you are not using WordPress.
4. Canonical URLs are set to `{SITE_BASE}/...`. If your path differs, find and replace
   `{SITE_BASE}` across `contents.html` and `theories/*.html`.

## Route B — WordPress

1. Create a parent page at `/theories/`. Paste `wordpress/theory-index.html` into a
   **Custom HTML** block. Slug: `theories`.
2. Create one child page per theory. Suggested slug is the filename, for example
   `ai0-redshift-law` becomes `/theories/a0-redshift-law/`.
3. Paste `wordpress/<slug>.html` into a Custom HTML block on each page.
4. Cross-links inside the fragments are absolute to `{SITE_BASE}/`. If your path differs,
   run one find-and-replace across `wordpress/*.html` before pasting.
5. Keep the closing note that says the work is not peer reviewed.

## Before going live

- [ ] Confirm the canonical URL and `og:` tags match your real path.
- [ ] Check that every evidence link resolves. Paywalled is fine; dead is not.
- [ ] Decide whether the contents page leads with "not peer reviewed". It currently does.
- [ ] Do not present any entry as established science. The claim box is a proposition,
      not a result.

## What not to do

- Do not paste the standalone `theories/*.html` into WordPress; they are full documents.
- Do not merge two theories into one page; the measure is calibrated for one argument.
- Do not strip the refutation section. It is the point of the format.
"""


def main():
    validate()
    if OUT.exists():
        shutil.rmtree(OUT)
    (OUT / "theories").mkdir(parents=True)
    (OUT / "wordpress").mkdir(parents=True)

    for i, t in enumerate(THEORIES):
        prev = THEORIES[i - 1] if i > 0 else None
        nxt = THEORIES[i + 1] if i < len(THEORIES) - 1 else None
        (OUT / "theories" / f'{t["slug"]}.html').write_text(
            render_theory(t, prev, nxt), encoding="utf-8")
        (OUT / "wordpress" / f'{t["slug"]}.html').write_text(render_wp(t), encoding="utf-8")

    (OUT / "contents.html").write_text(render_index(), encoding="utf-8")
    (OUT / "wordpress" / "theory-index.html").write_text(render_wp_index(), encoding="utf-8")
    (OUT / "README.md").write_text(README, encoding="utf-8")
    (OUT / "PUBLISH-ORDER.md").write_text(PUBLISH, encoding="utf-8")

    src = OUT / "build"
    src.mkdir(exist_ok=True)
    for f in ("build_v4.py", "data_v4_a.py", "data_v4_b.py", "data_v4_c.py"):
        shutil.copy2(ROOT / f, src / f)

    files = {
        str(p.relative_to(OUT)): {"sha256": sha256(p), "bytes": p.stat().st_size}
        for p in sorted(OUT.rglob("*")) if p.is_file() and p.name != "MANIFEST.json"
    }
    manifest = {
        "bundle": "Incoprea Theory, Volume IV",
        "built": BUILT,
        "generated_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
        "site_base": SITE_BASE,
        "theories": len(THEORIES),
        "predictions": sum(len(t["predictions"]) for t in THEORIES),
        "words": sum(words(t) for t in THEORIES),
        "fields": {f: sum(1 for t in THEORIES if t["field"] == f) for f in FIELDS},
        "files": files,
        "file_count": len(files),
    }
    (OUT / "MANIFEST.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")

    print(f"theories: {len(THEORIES)}  words: {manifest['words']:,}  files: {len(files)}")
    for f, c in manifest["fields"].items():
        print(f"  {c:>2}  {f}")


if __name__ == "__main__":
    main()
