"""Build Cyberelf's dependency-free public pages from a single product catalog."""
from pathlib import Path
from html import escape as esc
import json
import hashlib
import re

ROOT = Path(__file__).resolve().parents[1]
PRODUCTS = [
    dict(id="whetstone", name="Whetstone", role="Evaluate", status="Public workbench", group="release", headline="Did the change earn its release?", description="Compare your current and candidate AI system on the same checks. Keep every gain and regression visible.", url="https://whetstone.cyberelf.link/benchmark", source="https://github.com/CarlSR9001/whetstone", input="A baseline, a candidate, and the same verifier-backed tasks.", result="Paired outcomes and an explicit PASS, HOLD, or BLOCK decision.", boundary="The public benchmark uses six procedural tasks and self-attested system identities. A public run is not a private evaluation or proof of general model quality.", steps=["Start a shared cohort in the Open Promotion Bench.", "Run each task with your baseline and candidate, keeping their configurations fixed.", "Submit both answer sets. Inspect gains, regressions, scope changes, and the resulting receipt."], secondary="https://whetstone.cyberelf.link/for-agents", secondaryLabel="MCP & API guide"),
    dict(id="saliencelean", name="SalienceLean", role="Prove", status="Working prototype", group="release", headline="Make the contract precise.", description="Turn supported, typed obligations into Lean-checked proof receipts with explicit assumptions.", url="https://salience.cyberelf.link/", source="https://github.com/CarlSR9001/SalienceLean", input="One supported proof obligation, with its parameters and candidate identity.", result="A content-addressed proof receipt tied to an allowlisted Lean template.", boundary="A proof covers the formal statement and its assumptions. It does not establish the behavior, usefulness, or safety of an external model.", steps=["Inspect the supported obligation catalog.", "Select the obligation that matches your controller or package contract.", "Run the local prover and inspect the statement, assumptions, build identity, and receipt."], secondary="https://github.com/CarlSR9001/SalienceLean/blob/main/docs/PRODUCT.md", secondaryLabel="Proof protocol"),
    dict(id="foundry", name="Foundry", role="Adopt", status="Working prototype", group="release", headline="Make evidence agree before adoption.", description="Bind formal, empirical, and runtime receipts to the same candidate. Fail closed when an authority is missing or vetoes the change.", url="https://foundry.cyberelf.link/", source="https://github.com/CarlSR9001/foundry", input="A candidate contract, an admission policy, and independent authority receipts.", result="An admission decision and, when permitted, an explicit baseline adoption record.", boundary="Foundry checks the supplied authorities and identity links. It cannot turn weak evidence into strong evidence or replace the underlying verifiers.", steps=["Describe the candidate and required authorities.", "Supply receipts produced for that exact candidate and artifact.", "Inspect admission. Adopt only after every required check clears."], secondary="https://github.com/CarlSR9001/foundry/tree/main/evidence", secondaryLabel="Reference evidence"),
    dict(id="scopewrit", name="ScopeWrit", role="Authorize", status="Public alpha", group="release", headline="One action. Exact authority.", description="Sign the permitted scope of an agent run. Check each consequential action and consume its authorization once.", url="https://scopewrit.cyberelf.link/", source="https://github.com/CarlSR9001/scopewrit", input="A principal-signed scope manifest and a proposed action through a trusted adapter.", result="A deterministic DENY, PAUSE, or ALLOW, with a short-lived action ticket and audit trail.", boundary="Every effectful path must pass through a guarded adapter. ScopeWrit does not transparently contain a process that retains raw shell or network access.", steps=["Declare resources, methods, limits, run identity, and expiry.", "Sign the manifest using your configured trust root.", "Route proposals through the guarded adapter. Inspect the decision and consume-once ticket."], secondary="https://github.com/CarlSR9001/scopewrit#reproduce-the-evidence", secondaryLabel="Run the isolated benchmark"),
    dict(id="backplane", name="Backplane", role="Execute", status="Working runtime", group="release", headline="Run a capability you can account for.", description="Package and execute signed, scoped runtime capabilities, retaining the decision and execution record.", url="https://backplane.cyberelf.link/", source="https://github.com/CarlSR9001/backplane", input="A signed capability package, runtime policy, and compatible execution backend.", result="An inspectable execution receipt and explicit capability lifecycle.", boundary="The public surface is a control-plane sandbox. Local backend execution and package compatibility require their own checks.", steps=["Inspect a capability manifest and its signature.", "Check its scope and compatibility with your runtime.", "Execute through the selected backend and inspect the resulting receipt."], secondary="https://github.com/CarlSR9001/backplane", secondaryLabel="Runtime source"),
    dict(id="coherence", name="Coherence", role="Reconcile", status="Browser-local alpha", group="operations", headline="One thing. Several versions of the truth.", description="Compare records across independent systems, preserve source evidence, resolve exceptions, and resume a handoff exactly where it stopped.", url="/coherence/", source=None, input="Observed records for the same real-world object, plus the evidence behind them.", result="A field-by-field comparison, correction plan, receipt chain, and a human exception queue.", boundary="Changes affect this browser's workspace. There are no remote connectors or automatic writes to the original systems. Export a backup before clearing browser data.", steps=["Open the sample shipment or add your own object and representations.", "Choose a conflicting field, inspect the proposed corrections, and record your reasoning.", "Preserve read-only evidence, resolve exceptions, and export the workspace for your next handoff."], secondary="/coherence/", secondaryLabel="Open local workspace"),
]

FEATURES = {'whetstone': ['Download drafts and retain an answer archive after grading or expiry.', 'Import answer files and catch malformed patches before spending a cohort.', 'Handle expired server sessions without losing your answers; publication stays off by default.'], 'saliencelean': ['Verify typed obligations, fixed proof templates, axiom policy, and compiler records.', 'Download the full verifier result and inspect its precise claim boundary.', 'Use the pinned Lean toolchain locally to issue a new proof receipt.'], 'foundry': ['Evaluate a candidate against independent evidence in the live admission workbench.', 'Commit the next baseline and its complete known adoption history together.', 'Reject stale adoption, retry safely, and recover an interrupted history export.'], 'scopewrit': ['Inspect unsigned YAML before signing; the public preflight grants no authority.', 'Connect guarded HTTP to the checked IP while verifying the original TLS hostname.', 'Serialize local recorder writers and recover signed run audits after interruption.'], 'backplane': ['Run requests through the Python runtime and download complete execution receipts.', 'Keep tenant stacks separate and retrieve authenticated, paginated receipt history.', 'Bound pending work, settle cancelled requests, and report queue time separately from generation.'], 'coherence': ['Choose exact text, normalized text, decimal, or USD comparison for each field.', 'Preserve identifier case and leading zeros; record every comparison-policy change.', 'Undo edits, recover saved work, and verify evidence and the complete receipt chain.']}

def header(active=""):
    return '''<a class="skip-link" href="#main">Skip to content</a>
<header class="site-header" id="site-header"><a class="brand" href="/" aria-label="Cyberelf Labs home"><img src="/assets/mark.svg" alt="" width="34" height="34"><span>cyberelf<span class="brand-dot">.</span><small>INDEPENDENT SYSTEMS LAB</small></span></a><button class="nav-toggle" aria-expanded="false" aria-controls="site-nav" type="button">Menu <span aria-hidden="true">+</span></button><nav class="site-nav" id="site-nav" aria-label="Main navigation"><a href="/products/" ''' + ('aria-current="page"' if active == "products" else '') + '''>The tools</a><a href="/method/">The method</a><a href="/lab/">The lab</a><a class="nav-contact" href="mailto:justin@cyberelf.link">Let’s talk <span aria-hidden="true">↗</span></a></nav></header>'''

def footer():
    return '''<footer class="site-footer"><div><a class="footer-wordmark" href="/">cyberelf.</a><p>Independent research. Working systems.<br>Waterloo, Iowa.</p></div><nav aria-label="Footer"><a href="/products/">Tools</a><a href="/method/">Method & evidence</a><a href="/lab/">Inside the lab</a><a href="https://cyberelf.link/downloads/">Source releases</a><a href="https://github.com/CarlSR9001">GitHub ↗</a><a href="mailto:justin@cyberelf.link">justin@cyberelf.link ↗</a></nav><div class="footer-end"><span>© 2026 Cyberelf Labs</span><span>Make the change. Test the claim. Keep the receipt.</span></div></footer>'''

def write_page(path, title, description, body, active=""):
    dest = ROOT / path / "index.html"
    dest.parent.mkdir(parents=True, exist_ok=True)
    url = "https://cyberelf.link/" + (path.strip("/") + "/" if path else "")
    schema = {"@context":"https://schema.org", "@type":"Organization", "name":"Cyberelf Labs", "url":"https://cyberelf.link/", "email":"justin@cyberelf.link", "founder":{"@type":"Person","name":"Justin Garringer"}}
    dest.write_text(f'''<!doctype html>
<html lang="en" class="no-js"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>{esc(title)} — Cyberelf Labs</title><meta name="description" content="{esc(description)}"><meta name="theme-color" content="#101213"><meta name="color-scheme" content="dark"><link rel="canonical" href="{url}"><meta property="og:title" content="{esc(title)} — Cyberelf Labs"><meta property="og:description" content="{esc(description)}"><meta property="og:url" content="{url}"><meta property="og:type" content="website"><meta property="og:image" content="https://cyberelf.link/assets/cyberelf-social.png"><meta name="twitter:card" content="summary_large_image"><link rel="icon" href="/assets/mark.svg" type="image/svg+xml"><link rel="stylesheet" href="/assets/site.css"><script src="/assets/site.js" defer></script><script type="application/ld+json">{json.dumps(schema)}</script></head><body>{header(active)}<main id="main">{body}</main>{footer()}</body></html>''', encoding="utf-8")

def product_rows():
    return ''.join(f'''<article class="product-row" data-category="{p['group']}"><span class="row-number">0{i+1}</span><div class="product-name"><span class="eyebrow">{p['role']}</span><h3><a href="/products/{p['id']}/">{p['name']}</a></h3></div><p>{p['description']}</p><div class="row-end"><span class="status">{p['status']}</span><a class="round-link" href="{p['url']}" aria-label="Open {p['name']}">↗</a></div></article>''' for i,p in enumerate(PRODUCTS))

def build():
    # Public source downloads contain the exact reviewed release, not a moving branch.
    for product in PRODUCTS:
        product['upstream'] = product['source']
        product['source'] = f"https://cyberelf.link/downloads/{product['id']}-source.zip"
    home = '''<section class="hero wrap"><div class="hero-copy"><p class="eyebrow"><span class="live-dot"></span> CYBERELF LABS / EST. IN CURIOSITY</p><h1>Strange ideas.<br><em>Serious systems.</em></h1><p class="hero-description">We build tools that make AI changes measurable, actions accountable, and complex work easier to trust.</p><div class="actions"><a class="button button-primary" href="/products/">Find your tool <span>↗</span></a><a class="text-link" href="/method/">See the evidence <span>→</span></a></div><div class="hero-footnote"><span>INDEPENDENT BY DESIGN</span><span>OPEN TO INSPECTION</span></div></div><figure class="hero-art"><img src="/assets/cyberelf-hero.png" width="1122" height="1402" alt="Smoked glass instrument specimen, cut with a luminous lime seam" fetchpriority="high"><figcaption><span>IMAGINATION IS THE STARTING POINT.</span><span>Evidence decides what stays.</span></figcaption></figure></section>
<section class="feature wrap" aria-labelledby="feature-title"><div class="feature-heading"><p class="eyebrow">START HERE / WHETSTONE</p><h2 id="feature-title">Better on average.<br><em>Worse where it matters?</em></h2><p>An AI upgrade can gain six capabilities and lose three you already rely on. Whetstone makes those tradeoffs visible before the release decision.</p><a class="text-link" href="https://whetstone.cyberelf.link/benchmark">Compare two agents <span>↗</span></a></div><div class="receipt-demo"><div class="receipt-top"><span class="eyebrow">PAIRED RELEASE CHECK</span><span class="status status-red" id="demo-verdict">BLOCK</span></div><div class="score-pair"><div><span>Baseline</span><strong id="demo-baseline">7<span>/20</span></strong></div><span class="score-arrow">→</span><div><span>Candidate</span><strong id="demo-candidate">10<span>/20</span></strong></div></div><div class="transition-grid"><div><span class="gain" id="demo-gains">+6</span><small>Gains</small></div><div><span class="loss" id="demo-regressions">3</span><small>Regressions</small></div><div><span id="demo-ties">11</span><small>Ties</small></div></div><p id="demo-explanation">The score improved. Three regressions still block promotion.</p><div class="demo-controls"><label for="demo-case">Inspect an outcome</label><select id="demo-case"><option value="block">A higher score that fails</option><option value="pass">An improvement that passes</option><option value="hold">No evidence of improvement</option></select></div><small class="demo-note">Illustration of the gate. <a href="/method/#evidence">Source & boundaries →</a></small></div></section>
<section class="section wrap" id="products"><div class="section-heading"><div><p class="eyebrow">THE TOOLKIT</p><h2>A tool for each<br><em>hard question.</em></h2></div><p>Start with the problem you have. Whetstone stands on its own; the release tools can also form a governed chain.</p></div>''' + product_rows() + '''<div class="section-tail"><span>Six tools. Explicit responsibilities.</span><a class="text-link" href="/products/">Compare the tools →</a></div></section>
<section class="principle-band"><div class="wrap principle-grid"><p class="eyebrow">THE OPERATING PRINCIPLE</p><h2>Make a claim.<br>Build the test.<br><em>Keep the result.</em></h2><div><p>We like ambitious ideas. We also like the part where they encounter a real constraint.</p><p>Proof, measurement, permission, and execution answer different questions. Keeping them separate makes the whole system easier to inspect.</p><a class="text-link" href="/method/">How the pieces fit →</a></div></div></section>
<section class="section wrap lab-preview"><div><p class="eyebrow">AFTER HOURS / STILL IN THE LAB</p><h2>Leave room<br><em>for the strange.</em></h2><p>Visual instruments. Digital organisms. New ways to organize memory. The lab is where the next useful question starts.</p><a class="text-link" href="/lab/">Explore the experiments →</a></div><a class="experiment-image" href="/lab/#morpho"><img src="/assets/cyberelf-lab.png" alt="Concept illustration of a dark cellular lattice with luminous filaments" loading="lazy" width="640" height="360"><span><strong>Morpho</strong><small>Digital biology / concept illustration ↗</small></span></a></section>
<section class="contact-panel wrap"><p class="eyebrow">WORK WITH THE LAB</p><h2>Bring a problem<br>worth <em>getting right.</em></h2><div><p>Have an AI change to evaluate, a system that needs a clearer boundary, or an experiment worth building? Talk to Justin.</p><a class="button button-primary" href="mailto:justin@cyberelf.link">justin@cyberelf.link <span>↗</span></a></div></section>'''
    write_page("", "Strange ideas. Serious systems.", "Independent tools for evaluating AI changes, proving contracts, authorizing actions, and reconciling complex work.", home)
    directory = '''<section class="page-intro wrap"><p class="eyebrow">THE TOOLKIT / CHOOSE YOUR STARTING POINT</p><h1>What needs<br><em>to work better?</em></h1><p>Evaluate a change. Check a contract. Authorize an action. Resolve a disagreement. Each tool has a specific job and a visible boundary.</p></section><section class="wrap section directory"><div class="filter-bar" role="group" aria-label="Filter tools"><button data-filter="all" aria-pressed="true">All tools <span>06</span></button><button data-filter="release" aria-pressed="false">AI release chain <span>05</span></button><button data-filter="operations" aria-pressed="false">Operations <span>01</span></button></div><p class="sr-only" id="filter-count" aria-live="polite">6 tools</p>''' + product_rows() + '''</section><section class="wrap callout"><p class="eyebrow">HOW TO CHOOSE</p><h2>Start with Whetstone<br>if you’re shipping an AI change.</h2><p>You do not need to install the full chain to compare a baseline and candidate. Add formal proof, admission, authorization, or runtime controls when the release requires those guarantees.</p><a class="text-link" href="/products/whetstone/">Start with evaluation →</a></section>'''
    write_page("products", "The tools", "Find the Cyberelf tool for evaluation, formal proof, admission, authorization, execution, or reconciliation.", directory, "products")
    for p in PRODUCTS:
        source = f'<a href="{p["source"]}" class="text-link">Download this release’s source ↓</a>' if p['source'] else ''
        feature_html = '<section class="wrap release-features"><p class="eyebrow">AVAILABLE IN THIS RELEASE</p><ul>' + ''.join(f'<li>{esc(item)}</li>' for item in FEATURES[p['id']]) + '</ul></section>'
        detail = f'''<section class="page-intro wrap product-intro"><a class="breadcrumb" href="/products/">← All tools</a><p class="eyebrow">{p['role'].upper()} / <span class="status">{p['status']}</span></p><p class="product-title">{p['name']}</p><h1>{p['headline']}</h1><p>{p['description']}</p><div class="actions"><a class="button button-primary" href="{p['url']}">Open {p['name']} ↗</a>{source}</div></section>{feature_html}<section class="wrap io-grid"><article><span class="eyebrow">YOU BRING</span><h2>{p['input']}</h2></article><article><span class="eyebrow">YOU GET</span><h2>{p['result']}</h2></article></section><section class="wrap section guide"><div><p class="eyebrow">YOUR FIRST RUN</p><h2>Get from idea<br><em>to an inspectable result.</em></h2><a class="text-link" href="{p['secondary']}">{p['secondaryLabel']} →</a></div><ol>{''.join(f'<li><span>0{i+1}</span><p>{step}</p></li>' for i,step in enumerate(p['steps']))}</ol></section><section class="wrap boundary"><p class="eyebrow">WHAT THIS DOES — AND WHAT IT DOESN’T</p><h2>A useful tool has a clear boundary.</h2><p>{p['boundary']}</p></section><section class="wrap related"><a href="/products/">← Explore the other tools</a><a href="/method/">Read the method →</a></section>'''
        write_page(f"products/{p['id']}", p['name'], p['description'], detail, "products")
    method = '''<section class="page-intro wrap"><p class="eyebrow">THE METHOD</p><h1>Trust the result.<br><em>Inspect the path.</em></h1><p>A single score cannot tell you whether a system is ready. Cyberelf separates the questions a responsible release needs to answer.</p></section><section class="wrap section method-chain">''' + ''.join(f'<a href="/products/{p["id"]}/"><span class="eyebrow">0{i+1} / {p["role"].upper()}</span><h2>{p["name"]}</h2><p>{p["result"]}</p><span aria-hidden="true">↗</span></a>' for i,p in enumerate([PRODUCTS[1],PRODUCTS[0],PRODUCTS[2],PRODUCTS[3],PRODUCTS[4]])) + '''</section><section class="section wrap" id="evidence"><div class="section-heading"><div><p class="eyebrow">PUBLISHED LOCAL EVIDENCE</p><h2>The gate must<br><em>be able to say no.</em></h2></div><p>These are bounded internal experiments and integration checks. They are not customer results or a benchmark of frontier models.</p></div><div class="evidence-table"><article><span class="status status-red">BLOCK</span><h3>A better aggregate score, three regressions.</h3><p>On a fresh 20-item local code cohort, the baseline scored 7/20 and the candidate 10/20: six gains, three regressions, eleven ties. Three candidate replays produced identical item-level outcomes.</p><p class="muted">Qwen2.5 1.5B → Qwen3 8B · 2,048-token completion budget · exact McNemar p = .5078125.</p><a href="https://github.com/CarlSR9001/whetstone/blob/main/results/local_qwen_code_bakeoff.json">Inspect the published comparison ↗</a></article><article><span class="status status-green">PASS</span><h3>A separate cohort earned promotion.</h3><p>On a fresh 16-item local code cohort, the candidate moved from 6/16 to 13/16: seven gains and zero regressions, with exact p = .015625.</p><p class="muted">A distinct local comparison. The result does not generalize to every model, task, or deployment.</p><a href="https://github.com/CarlSR9001/whetstone/blob/main/results/local_qwen_code_bakeoff.json">Inspect both results ↗</a></article><article><span class="status">INTEGRATION</span><h3>The release chain closes end to end.</h3><p>A separate 38-probe cycle moved from 20/38 to 38/38, with 18 gains and zero regressions. This establishes the reference integration path, not general intelligence improvement.</p><a href="https://github.com/CarlSR9001/foundry/tree/main/evidence/gpu_cycle_20260725">Inspect the reference cycle ↗</a></article></div><p class="fine-print">The homepage’s HOLD example is illustrative: identical paired answers supply no evidence of improvement. Published evidence above is retained with its original scope; a new website does not make an old experiment a new result.</p></section><section class="wrap boundary"><p class="eyebrow">PRIVATE EVALUATION</p><h2>Keep the exam in your environment.</h2><p>The public workbench is for disposable or sanitized inputs. Bring the core into your own environment for sensitive cohorts, answers, and artifacts. Public benchmark publication is opt-in.</p><a class="text-link" href="https://github.com/CarlSR9001/whetstone/blob/main/docs/product.md">Read the evaluation protocol →</a></section>'''
    write_page("method", "Method & evidence", "Independent proof, measurement, admission, authorization, and execution, with published local evidence and explicit limits.", method)
    lab = '''<section class="page-intro wrap"><p class="eyebrow">THE LAB / EXPLORATION IN PUBLIC</p><h1>Follow a<br><em>stranger question.</em></h1><p>These experiments explore different kinds of systems. They stand outside the AI release chain and keep their own evidence boundaries.</p></section><section class="wrap section experiments"><article id="entoptic"><div class="experiment-index">01 / VISUAL SIGNALS</div><div><span class="status">Browser lab</span><h2>Entoptic</h2><p>A visual signal workstation for patching typed device racks across color, depth, motion, masks, normals, confidence, and stereo. Undo project edits, reject incomplete imports without losing your rack, and render shared shaders on the client GPU.</p><p class="muted">Requires WebGL2. Browser operation does not imply feature parity with the desktop Digital Vision Workstation.</p><a class="button button-primary" href="https://entoptic.cyberelf.link/">Open the workstation ↗</a><p><a class="text-link" href="/lab/entoptic/">First patch, recovery, and source →</a></p></div></article><article id="morpho"><div class="experiment-index">02 / DIGITAL BIOLOGY<img src="/assets/cyberelf-lab.png" width="1536" height="1024" loading="lazy" alt="Conceptual cellular lattice, original lab illustration"></div><div><span class="status">Local research prototype</span><h2>Morpho</h2><p>A native PyTorch petri dish with a complete workbench, reproducible sessions, resumable genome evolution, and bitwise archive replay.</p><p class="muted">The image is an original concept illustration. The simulator uses an untrained update rule and a structured growth prior. <a href="/lab/morpho/">See the actual workbench and replay evidence →</a></p></div></article><article id="substrate"><div class="experiment-index">03 / CONTINUAL LEARNING</div><div><span class="status">Research rig</span><h2>Substrate Search</h2><p>A search rig below model architecture: memory, consolidation, context routing, trust gates, and contradiction repair across replayable lifetimes.</p><p class="muted">Training, selection, and final testing are now separated. The corrected 48-trial run did not beat its baseline. <a href="/lab/substrate-search/">Read the result and run the rig →</a></p></div></article></section><section class="wrap contact-panel"><div><p class="eyebrow">JUSTIN GARRINGER / FOUNDER</p><h2>Independent<br>by <em>design.</em></h2></div><div><p>Cyberelf Labs is a founder-led systems lab in Waterloo, Iowa. We work between research and product, building runnable systems and the instruments needed to judge them.</p><div class="actions"><a class="text-link" href="mailto:justin@cyberelf.link">Get in touch ↗</a><a class="text-link" href="https://justin.cyberelf.link/">Research profile ↗</a></div></div></section>'''
    write_page("lab", "Inside the lab", "Entoptic, Morpho, and Substrate Search: visual instruments, digital biology, and continual-learning experiments.", lab)
    from release_pages import build_release_pages
    extra_paths = build_release_pages(write_page, ROOT)
    paths = extra_paths + ["", "products", "method", "lab", "coherence"] + [f"products/{p['id']}" for p in PRODUCTS]
    (ROOT/'sitemap.xml').write_text('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' + ''.join(f'<url><loc>https://cyberelf.link/{p + "/" if p else ""}</loc></url>' for p in paths) + '</urlset>\n', encoding='utf-8')
    (ROOT/'assets'/'products.json').write_text(json.dumps(PRODUCTS, indent=2) + '\n', encoding='utf-8')
    # Version native ES module imports and page assets so returning visitors
    # cannot mix the old application with the new styles or integrity checks.
    app = ROOT/'coherence/app.js'
    js = app.read_text(encoding='utf-8')
    for name in ['core.js','history.js']:
        digest = hashlib.sha256((ROOT/'coherence'/name).read_bytes()).hexdigest()[:12]
        js = re.sub(r'from "\./' + re.escape(name) + r'(?:\?v=[^"]*)?"', f'from "./{name}?v={digest}"', js)
    app.write_text(js,encoding='utf-8')
    for path in paths:
        page = ROOT/path/'index.html'
        html = page.read_text(encoding='utf-8')
        for asset in ['assets/site.css','assets/site.js','coherence/app.js','coherence/coherence.css']:
            digest = hashlib.sha256((ROOT/asset).read_bytes()).hexdigest()[:12]
            html = re.sub(r'"/' + re.escape(asset) + r'(?:\?v=[^"]*)?"', f'"/{asset}?v={digest}"', html)
        page.write_text(html,encoding='utf-8')
    print(f"Built {len(paths)-1} public pages; retained Coherence application.")

if __name__ == '__main__':
    build()
