#!/usr/bin/env python3
"""Build the additive SUBSTELLAR Edition 06 reader.

Run with Python 3.10+ from any directory. Only the Python standard library and
the preserved local parse_substellar_blocks.py module are required. The exact
Edition 05 reader is the input, never the previous output of this script.

This program edits documentation outputs only. It performs no networking,
image generation, game development, publication or repository operations.
"""
from __future__ import annotations

import copy
import hashlib
import html
from html.parser import HTMLParser
import json
from pathlib import Path
import re
from collections import Counter, defaultdict
from urllib.parse import unquote, urlsplit

from parse_substellar_blocks import blocks_from_text

HERE = Path(__file__).resolve().parent
ROOT = HERE.parent
EDITION = "Selected completion 06"
DATE = "2026-10-07"

SOURCES = [
    ("c06-reconciliation", "reconciliation_06.txt", "P00", "W0"),
    ("c06-industry", "industry_completion_06.txt", "P06", "W1"),
    ("c06-advanced-industry", "advanced_industry_routes_06.txt", "P06", "W1"),
    ("c06-materials", "materials_completion_06.txt", "P07", "W1"),
    ("c06-earth", "earth_completion_06.txt", "P01", "W2"),
    ("c06-old-valley", "old_valley_completion_06.txt", "P02", "W2"),
    ("c06-ecology", "ecology_completion_06.txt", "P10", "W2"),
    ("c06-moon", "moon_completion_06.txt", "P03", "W3"),
    ("c06-tier1", "tier1_completion_06.txt", "P11", "W3"),
    ("c06-support-vehicles", "support_vehicles_06.txt", "P12-A", "W3"),
    ("c06-orbital", "orbital_places_06.txt", "P12-B", "W3"),
]
FIGURE_INPUTS = [
    "core_figures_a_06.json", "core_figures_b_06.json", "moon_figures_06.json",
    "tier1_figures_06.json", "vehicles_orbit_figures_06.json", "raster_figures_06.json",
]
WAVES = {
    "W0": ("Establish the current direction", "Source authority, confirmed choices and the manual Website report round trip."),
    "W1": ("Make a discovery useful", "All ten industrial capability bands, 66 machine responsibilities, 37 material families and ten manufactured-product families."),
    "W2": ("Give places a working life", "Seven Earth realms and 35 experiences, Old Valley’s grain mill and first market, and ten selected Europa, Titan and Morrow ecology families."),
    "W3": ("Prepare, travel and return", "The remaining Moon journeys, Tier 1 documentation and fit study, four local support vehicles, and two orbital station families with seven shared modules."),
}
PACKAGE_SUMMARIES = {
    "P00": ("Reconciliation and decisions", "Revision-bound game and Website evidence, retained history, confirmed choices, selected scope and the report round trip."),
    "P01": ("Earth’s regional handbook", "Seven realms and all 35 named creative experiences; generation grammar, lateral routes, construction relationships and review criteria."),
    "P02": ("Old Valley’s everyday life", "Grain milling, a useful workshop, public circulation, the first market, partial progress, recovery and completion criteria."),
    "P03": ("The remaining Moon", "Five regional contexts, the credited first expedition, two additional complete expeditions, a signature mission and a working gallery."),
    "P06": ("Industry and progression", "IP00–IP09, all 66 machine responsibilities, connections and operating states, plus six proposed advanced input-to-product routes."),
    "P07": ("Materials and useful products", "Nineteen Earth and eighteen planetary material families, ten product families, chart reconciliation and explicit recipe limits."),
    "P10": ("Europa, Titan and Morrow ecology", "Ten established families: form, habitat, response, recovery, sampling, bounded cultivation and return value. CEC01 remains conditional."),
    "P11": ("Tier 1 closure and fit study", "Twenty canonical assets, confirmed reference identities, compact-first Explorer arithmetic, separate station functions and forty future verification scenarios."),
    "P12-A": ("Four local support vehicles", "Survey Rover, Cargo Crawler, Ice Submersible and Atmospheric Survey Skiff: deployment, occupied use, services, complete outings and recovery."),
    "P12-B": ("Orbital homes and stations", "Home Orbit Anchorage, Transfer Yard and seven shared module subjects; staged building, pressure boundaries, personal rooms, arrivals and returns."),
}


def read_json(name):
    return json.loads((HERE / name).read_text(encoding="utf-8"))


def json_text(value):
    return json.dumps(value, ensure_ascii=False, indent=2) + "\n"


def embedded_json(value):
    return json.dumps(value, ensure_ascii=False, separators=(",", ":")).replace("<", "\\u003c").replace("\u2028", "\\u2028").replace("\u2029", "\\u2029")


def sha(data):
    if isinstance(data, str):
        data = data.encode("utf-8")
    return hashlib.sha256(data).hexdigest()


def words(value):
    return len(re.findall(r"\b[\w]+(?:[’'-][\w]+)*\b", re.sub(r"[#*`|]", " ", value)))


def esc(value):
    return html.escape(str(value), quote=True)


def slug(value):
    return re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")


class Inventory(HTMLParser):
    """Capture exact original element spans without reserializing any old HTML."""
    VOID = {"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"}

    def __init__(self, text):
        super().__init__(convert_charrefs=True)
        self.text = text
        self.line_starts = [0]
        self.line_starts.extend(m.end() for m in re.finditer("\n", text))
        self.stack = []
        self.ids = Counter()
        self.elements = {}
        self.source_blocks = {}
        self.links = []
        self.tags = []
        self.feed(text)
        self.close()

    def pos(self):
        line, col = self.getpos()
        return self.line_starts[line - 1] + col

    def start(self, tag, attrs, closed=False):
        d = dict(attrs)
        begin = self.pos()
        finish = begin + len(self.get_starttag_text())
        item = {"tag": tag, "attrs": d, "start": begin, "open_end": finish}
        self.tags.append(item)
        if d.get("id"):
            self.ids[d["id"]] += 1
        for attr in ("href", "src", "data-image", "data-original"):
            if d.get(attr):
                self.links.append((tag, attr, d[attr]))
        if closed or tag in self.VOID:
            self.finish(item, finish)
        else:
            self.stack.append(item)

    def handle_starttag(self, tag, attrs):
        self.start(tag, attrs)

    def handle_startendtag(self, tag, attrs):
        self.start(tag, attrs, True)

    def finish(self, item, end):
        item["end"] = end
        d = item["attrs"]
        if d.get("id"):
            self.elements[d["id"]] = item
        if "source-block" in d.get("class", "").split():
            key = (d.get("data-source"), d.get("data-block"))
            if key in self.source_blocks:
                raise AssertionError(f"Duplicate source block: {key}")
            self.source_blocks[key] = self.text[item["start"]:end]

    def handle_endtag(self, tag):
        end = self.text.find(">", self.pos()) + 1
        for i in range(len(self.stack) - 1, -1, -1):
            if self.stack[i]["tag"] == tag:
                popped = self.stack[i:]
                del self.stack[i:]
                for item in reversed(popped):
                    self.finish(item, end)
                break


def replace_json_script(text, identifier, data):
    pattern = re.compile(r'(<script\b[^>]*\bid="' + re.escape(identifier) + r'"[^>]*>)(.*?)(</script>)', re.S)
    result, count = pattern.subn(lambda m: m.group(1) + embedded_json(data) + m.group(3), text)
    assert count == 1, identifier
    return result


def original_json_script(text, identifier):
    pattern = r'<script\b[^>]*\bid="' + re.escape(identifier) + r'"[^>]*>(.*?)</script>'
    return json.loads(re.search(pattern, text, re.S).group(1))


def insert_after_open(text, identifier, addition):
    pattern = re.compile(r'<(?:section|aside|div|nav|details)\b[^>]*\bid="' + re.escape(identifier) + r'"[^>]*>')
    matches = list(pattern.finditer(text))
    assert len(matches) == 1, (identifier, len(matches))
    end = matches[0].end()
    return text[:end] + addition + text[end:]


def section_transform(text, identifier, fn):
    inventory = Inventory(text)
    span = inventory.elements[identifier]
    old = text[span["start"]:span["end"]]
    return text[:span["start"]] + fn(old) + text[span["end"]:]


def block_text(block):
    if block[0] == "heading":
        return block[2]
    if block[0] == "table":
        return " | ".join(" | ".join(row) for row in block[1])
    return block[1]


def write_outputs(outputs):
    for relative, value in outputs.items():
        target = ROOT / relative
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(value, encoding="utf-8", newline="\n")


def main():
    old = (HERE / "reader_05_preserved.html").read_text(encoding="utf-8")
    baseline = Inventory(old)
    old_manifest = read_json("edition05_manifest_preserved.json")
    old_map = read_json("source_anchor_map_05.json")
    old_visuals = original_json_script(old, "visual-data")
    old_search = original_json_script(old, "search-data")
    scope = read_json("scope_06.json")
    coverage = read_json("coverage_06.json")
    decisions = read_json("decisions_06.json")
    generation = read_json("image_generation_06.json")
    assert old_manifest["total_source_words"] == 111411
    assert len(old_map["chapters"]) == 49
    assert len(baseline.source_blocks) == 2000
    assert len(old_visuals["records"]) == 59
    assert not [key for key, count in baseline.ids.items() if count != 1]

    inputs = {}
    chapters = []
    source_entries = []
    block_targets = {}
    section_titles = {}
    new_ids = set()
    source_by_package = defaultdict(list)
    for key, filename, package, wave in SOURCES:
        text = (HERE / filename).read_text(encoding="utf-8")
        inputs[filename] = text
        blocks = blocks_from_text(text)
        chapter = None
        local_count = 0
        for number, block in enumerate(blocks):
            block_id = f"{key}-{number:04d}"
            if block[0] == "heading" and block[1] == 1:
                local_count += 1
                chapter_id = f"{key}-{local_count:02d}"
                chapter = {"id": chapter_id, "title": block[2], "source_key": key,
                           "source_file": f"sources/{filename}", "package": package, "wave": wave,
                           "href": f"#{chapter_id}", "new_in_edition06": True, "sections": [], "blocks": []}
                chapters.append(chapter)
                new_ids.add(chapter_id)
                source_by_package[package].append(chapter)
            assert chapter is not None, f"Source must begin with a chapter: {filename}"
            target = block_id
            if block[0] == "heading" and block[1] > 1:
                match = re.match(r"^(C06-[A-Za-z0-9-]+)(?:\s|$)", block[2])
                target = match.group(1) if match else f"{chapter['id']}--{slug(block[2])}"
                assert target not in new_ids and target not in baseline.ids, f"Duplicate heading {target}"
                chapter["sections"].append({"id": target, "title": block[2], "level": block[1],
                                            "href": f"#{target}", "data_block": block_id})
                section_titles[target] = block[2]
            assert target not in new_ids and target not in baseline.ids, f"Duplicate source target {target}"
            new_ids.add(target)
            block_targets[block_id] = target
            chapter["blocks"].append({"id": block_id, "target": target, "value": block})
        source_entries.append({"key": key, "file": filename, "words": words(text),
                               "blocks": len(blocks), "chapters": local_count,
                               "tables": sum(b[0] == "table" for b in blocks),
                               "sha256": sha((HERE / filename).read_bytes()),
                               "package": package, "wave": wave, "new_in_edition06": True})

    assert len(chapters) == 18, f"Review chapter-count change: {len(chapters)}"
    current_symbols = dict(old_map["canonical_symbol_anchors"])
    current_symbols.update({key: key for key in section_titles if key.startswith("C06-")})

    def collect_symbols(value):
        if isinstance(value, dict):
            identifier = value.get("id")
            section = value.get("section_id")
            if identifier and section in new_ids:
                current_symbols[identifier] = section
            for nested in value.values():
                collect_symbols(nested)
        elif isinstance(value, list):
            for nested in value:
                collect_symbols(nested)

    register_names = sorted({filename for package in coverage["packages"] for filename in package["register_files"]})
    for filename in register_names:
        collect_symbols(read_json(filename))
    for n in range(10):
        current_symbols[f"IP{n:02d}"] = "C06-P06-02"
    for n in range(1, 5):
        current_symbols[f"SUP{n:02d}"] = f"C06-P12A-{2*n+1:02d}"
    current_symbols.update({"STA01": "C06-P12B-04", "STA02": "C06-P12B-06", "STA03": "C06-P12B-07",
                            "T1-DEC01": "edition06-decisions", "T1-DEC02": "edition06-decisions",
                            "CEC01": "edition06-decisions"})
    for chapter in chapters:
        for section in chapter["sections"]:
            match = re.match(r"(EAR\d{2})\b", section["title"])
            if match:
                current_symbols[match.group(1)] = section["id"]
    valid_link_targets = set(baseline.ids) | new_ids | {"edition06-decisions"}
    current_symbols = {key: value for key, value in current_symbols.items() if value in valid_link_targets}

    symbol_pattern = re.compile(r"\b(?:C06-[A-Z0-9-]+|IND-M\d{2}|MAT\d{2}|PM\d{2}|IP\d{2}|SUP\d{2}|STA\d{2}|CEC\d{2}|REF\d{2}|EAR\d{2}|MO-[A-Z]\d|T1-DEC\d{2})\b")

    def plain_inline(value, link_symbols=True):
        if not link_symbols:
            return esc(value)
        chunks = []
        position = 0
        for match in symbol_pattern.finditer(value):
            chunks.append(esc(value[position:match.start()]))
            symbol = match.group(0)
            if symbol in current_symbols:
                chunks.append(f'<a class="source-id-link" href="#{esc(current_symbols[symbol])}">{esc(symbol)}</a>')
            else:
                chunks.append(esc(symbol))
            position = match.end()
        chunks.append(esc(value[position:]))
        return "".join(chunks)

    inline_pattern = re.compile(r"(\*\*[^*]+\*\*|`[^`]+`|\[[^\]]+\]\([^\s)]+\)|https?://[^\s<>]+)")

    def inline(value, link_symbols=True):
        chunks = []
        position = 0
        for match in inline_pattern.finditer(value):
            chunks.append(plain_inline(value[position:match.start()], link_symbols))
            token = match.group(0)
            if token.startswith("**"):
                chunks.append("<strong>" + plain_inline(token[2:-2], link_symbols) + "</strong>")
            elif token.startswith("`"):
                chunks.append("<code>" + esc(token[1:-1]) + "</code>")
            elif token.startswith("["):
                m = re.fullmatch(r"\[([^\]]+)\]\(([^)]+)\)", token)
                target = m.group(2)
                if urlsplit(target).scheme not in ("", "http", "https", "mailto"):
                    chunks.append(esc(token))
                else:
                    chunks.append(f'<a href="{esc(target)}">{esc(m.group(1))}</a>')
            else:
                stripped = token.rstrip(".,;")
                chunks.append(f'<a href="{esc(stripped)}">{esc(stripped)}</a>' + esc(token[len(stripped):]))
            position = match.end()
        chunks.append(plain_inline(value[position:], link_symbols))
        return "".join(chunks)

    figures = []
    figures_at = defaultdict(list)
    for filename in FIGURE_INPUTS:
        data = read_json(filename)
        records = data if isinstance(data, list) else data["figures"]
        for original in records:
            record = copy.deepcopy(original)
            assert record["id"] not in {f["id"] for f in figures}
            assert (ROOT / record["file"]).is_file(), record["file"]
            if record.get("original"):
                assert (ROOT / record["original"]).is_file(), record["original"]
            assert record.get("section_ids"), record["id"]
            missing = [identifier for identifier in record["section_ids"] if identifier not in new_ids]
            assert not missing, (record["id"], missing)
            record["kind"] = "Edition 06 editable diagram" if record["file"].endswith(".svg") else "Edition 06 concept board"
            record["target"] = "figure-" + record["id"].lower()
            record["metadata_source"] = "sources/" + filename
            record["placed_after_section"] = record["section_ids"][0]
            record["sha256_file"] = sha((ROOT / record["file"]).read_bytes())
            if record.get("original"):
                record["sha256_original"] = sha((ROOT / record["original"]).read_bytes())
            figures.append(record)
            figures_at[record["placed_after_section"]].append(record)
    assert len(figures) == 25
    assert sum(f["file"].endswith(".svg") for f in figures) == 13
    assert sum(bool(f.get("original")) for f in figures) == 12
    figures.sort(key=lambda f: f["id"])
    figure_by_package = defaultdict(list)
    for record in figures:
        figure_by_package[record["package"]].append(record)

    def source_link(identifier):
        target = current_symbols.get(identifier)
        if identifier in new_ids:
            target = identifier
        return f'<a href="#{esc(target)}"><code>{esc(identifier)}</code></a>' if target else f'<code>{esc(identifier)}</code>'

    rendered_figures = []

    def figure_markup(record):
        rendered_figures.append(record["id"])
        diagram = record["file"].endswith(".svg")
        original = record.get("original", record["file"])
        kind = "Editable explanatory SVG" if diagram else "Minecraft concept illustration"
        scope_note = "; ".join(record.get("not_established", []))
        context = " · ".join(f'<a href="#{esc(identifier)}">{esc(identifier)}</a>' for identifier in record["section_ids"])
        sources = " · ".join(source_link(identifier) for identifier in record.get("source_ids", []))
        return (f'<figure class="plate companion-figure edition06-figure{" edition06-diagram" if diagram else ""}" id="{esc(record["target"])}" data-figure-id="{esc(record["id"])}">'
                f'<button type="button" data-image="{esc(record["file"])}" data-original="{esc(original)}" data-caption="{esc(record["title"])}" aria-label="Enlarge {esc(record["title"])}">'
                f'<img src="{esc(record["file"])}" alt="{esc(record["alt"])}" loading="lazy" decoding="async"></button>'
                f'<figcaption><div class="plate-meta"><strong>{esc(record["title"])}</strong><span>{esc(record["id"])} · {esc(kind)}</span></div>'
                f'<p>{esc(record["caption"])}</p><p class="figure-status"><strong>Status:</strong> {esc(record["status"])}</p>'
                f'<details class="figure-limits"><summary>Scope and source context</summary><p><strong>Not established:</strong> {esc(scope_note)}</p>'
                f'<p>Source identities: {sources}</p><p>Related sections: {context}</p>'
                f'<p><a href="{esc(record["metadata_source"])}">Figure provenance and metadata</a></p></details>'
                f'<p class="figure-download"><a href="{esc(original)}" target="_blank" rel="noopener">{"Open the full editable SVG" if diagram else "Open the unchanged selected PNG"} ↗</a>'
                f' · <a href="{esc(original)}" download>Download {"SVG" if diagram else "original PNG"}</a></p></figcaption></figure>')

    new_search = []
    chapter_html = {}
    for chapter_index, chapter in enumerate(chapters):
        head = chapter["blocks"][0]
        attrs = lambda item: f'class="source-block" data-source="{esc(chapter["source_key"])}" data-block="{esc(item["id"])}" id="{esc(item["target"])}"'
        rendered = [f'<section class="chapter edition06-chapter" id="{esc(chapter["id"])}" data-source-section="{esc(chapter["source_key"])}" data-wave="{esc(chapter["wave"])}" data-package="{esc(chapter["package"])}">',
                    f'<header class="chapter-header"><span class="eyebrow">Edition 06 · {esc(chapter["wave"])} · {esc(chapter["package"])} · Chapter {chapter_index+1:02d}</span>',
                    f'<h2 {attrs(head)}>{inline(chapter["title"], False)}</h2>',
                    f'<div class="chapter-actions"><span class="pill overview">Creative documentation · implementation separate</span>',
                    f'<button type="button" data-copy-anchor="{esc(chapter["id"])}">Copy section link</button>',
                    f'<a href="{esc(chapter["source_file"])}" download>Exact text source</a>',
                    f'<a href="#edition06-package-{slug(chapter["package"])}">Package context</a></div></header><div class="chapter-body">']
        active_list = None
        current_section = None

        def close_list():
            nonlocal active_list
            if active_list:
                rendered.append(f'</{active_list}>')
                active_list = None

        def flush_figures():
            if current_section:
                for figure in figures_at.get(current_section, []):
                    rendered.append(figure_markup(figure))

        for item_index, item in enumerate(chapter["blocks"]):
            value = item["value"]
            target = chapter["id"] if item_index == 0 else item["target"]
            new_search.append({"id": target, "chapter": chapter["title"],
                               "title": value[2] if value[0] == "heading" else section_titles.get(current_section, chapter["title"]),
                               "text": block_text(value), "source": chapter["source_key"],
                               "source_file": chapter["source_file"], "block_id": item["id"],
                               "package": chapter["package"], "wave": chapter["wave"]})
            if item_index == 0:
                continue
            kind = value[0]
            if kind not in ("bullet", "number"):
                close_list()
            if kind == "heading":
                flush_figures()
                current_section = item["target"]
                level = min(value[1] + 1, 5)
                rendered.append(f'<h{level} {attrs(item)}>{inline(value[2], False)}<a class="anchor-mark" aria-hidden="true" tabindex="-1" href="#{esc(item["target"])}">#</a></h{level}>')
            elif kind == "paragraph":
                rendered.append(f'<p {attrs(item)}>{inline(value[1])}</p>')
            elif kind in ("bullet", "number"):
                desired_list = "ul" if kind == "bullet" else "ol"
                if active_list != desired_list:
                    close_list()
                    rendered.append(f'<{desired_list}>')
                    active_list = desired_list
                rendered.append(f'<li {attrs(item)}>{inline(value[1])}</li>')
            elif kind == "table":
                rows = value[1]
                rendered.append(f'<div class="table-wrap" tabindex="0" aria-label="Table in {esc(chapter["title"])}"><table {attrs(item)}>')
                rendered.append('<thead><tr>' + "".join(f'<th scope="col">{inline(cell)}</th>' for cell in rows[0]) + '</tr></thead><tbody>')
                for row in rows[1:]:
                    rendered.append('<tr>' + "".join(f'<td>{inline(cell)}</td>' for cell in row) + '</tr>')
                rendered.append('</tbody></table></div>')
            else:
                raise AssertionError(f"Unsupported source block {kind}")
        close_list()
        flush_figures()
        rendered.append('</div>')
        rendered.append(f'<details class="review-box" data-chapter="{esc(chapter["id"])}" data-title="{esc(chapter["title"])}"><summary>Your review notes <span class="review-state"></span></summary><div class="review-fields">'
                        f'<label>Direction for this chapter<select aria-label="Review direction for {esc(chapter["title"])}"><option value="">No review note</option><option value="Keep this direction">Keep this direction</option><option value="Question to resolve">Question to resolve</option><option value="Change requested">Change requested</option></select></label>'
                        f'<label>Notes<textarea placeholder="What should stay, change, or be explored further?" aria-label="Review notes for {esc(chapter["title"])}"></textarea></label><span class="review-help">Notes stay in this browser. Export them to keep a portable copy.</span><button type="button" data-export-notes>Export all review notes</button></div></details>')
        prev = chapters[chapter_index - 1] if chapter_index else {"id": "edition06", "title": "Edition 06 overview"}
        nxt = chapters[chapter_index + 1] if chapter_index + 1 < len(chapters) else {"id": "current-edition", "title": "Preserved Edition 05 and the complete earlier source"}
        rendered.append(f'<nav class="chapter-link-nav" aria-label="Adjacent chapters"><a href="#{esc(prev["id"])}">← {esc(prev["title"])}</a><a href="#{esc(nxt["id"])}">{esc(nxt["title"])} →</a></nav></section>')
        chapter_html[chapter["id"]] = "\n".join(rendered)
    assert Counter(rendered_figures) == Counter({f["id"]: 1 for f in figures})

    added_words = sum(entry["words"] for entry in source_entries)
    total_words = old_manifest["total_source_words"] + added_words
    all_chapter_count = len(old_map["chapters"]) + len(chapters)
    wave_nav = '<nav class="edition06-wave-nav" aria-label="Edition 06 waves">' + "".join(f'<a href="#edition06-{wave.lower()}"><span>{wave}</span>{esc(title)}<small>{esc(desc)}</small></a>' for wave, (title, desc) in WAVES.items()) + '</nav>'
    package_cards = []
    for package in scope["selected_packages"]:
        title, summary = PACKAGE_SUMMARIES[package]
        chapter = source_by_package[package][0]
        links = " · ".join(f'<a href="#{esc(f["target"])}">{esc(f["id"])}</a>' for f in figure_by_package[package])
        package_cards.append(f'<article class="card edition06-package" id="edition06-package-{slug(package)}"><span class="card-id">{esc(package)} · {esc(chapter["wave"])}</span><h3><a href="#{esc(chapter["id"])}">{esc(title)}</a></h3><p>{esc(summary)}</p><p class="edition06-package-figures">Figures: {links}</p><a class="button" href="#{esc(chapter["id"])}">Read this package →</a></article>')
        new_search.append({"id": f"edition06-package-{slug(package)}", "chapter": "Edition 06 overview", "title": f"{package} · {title}", "text": summary, "source": "edition06-editorial", "package": package})

    hub = f'''<section class="section-wrap edition06-hub" id="edition06">
<div class="section-head"><div><span class="eyebrow">Selected completion · Edition 06 · {DATE}</span><h2>A complete source.<br>A clearer next layer of detail.</h2></div><p>Read the selected additions in four waves, or enter the package you want to review. Every earlier chapter, supplied reference and existing illustration remains available below.</p></div>
<div class="edition06-stats" aria-label="Edition counts"><div><strong>{total_words:,}</strong><span>Source words across 22 narrative files</span></div><div><strong>{all_chapter_count}</strong><span>Complete chapters · 49 preserved</span></div><div><strong>84</strong><span>Visual records · 59 preserved</span></div><div><strong>10</strong><span>Selected packages · four waves</span></div></div>
<p class="overview-intro">This edition adds {added_words:,} words, 18 chapters, 12 Minecraft concept boards and 13 editable diagrams. The images establish broad forms and activities with coarse game-scale materials. Exact recipes, final model fit and runtime behaviour retain their explicit evidence limits.</p>
<div class="edition06-actions"><a class="button primary" href="#edition06-w0">Start with the current evidence</a><a class="button" href="#edition06-decisions">Confirmed choices and open evidence</a><a class="button" href="#edition06-figures">Browse the new artwork</a><a class="button" href="#edition06-sources">Sources and handoff</a><a class="button" href="#current-edition">Read the preserved editions</a></div>
<div class="intro-note"><strong>Current facts have a source and a revision.</strong> The inspected game record <code>a0a702c2b578</code> is accessible and pushed, with its development work unmerged. The working column is 672 levels and the current guide has 15 implemented depth biomes. Seven realms and 35 creative experiences describe the creative atlas; they are not a one-to-one biome mapping. Smelt remains the next unbuilt gameplay slice. Website receipt <code>adc088109436</code> is recorded separately. <a href="#C06-P00-02">Read the evidence and limitations</a>.</div>
{wave_nav}
<div class="section-head edition06-small-head"><div><span class="eyebrow">Choose your subject</span><h2>The ten selected packages.</h2></div><p>These cards describe delivered documentation scope. New game implementation and public Website publication remain separate work.</p></div>
<div class="edition06-package-grid">{''.join(package_cards)}</div>
<div class="intro-note"><strong>Programme boundaries.</strong> The selected ecology, support craft and orbital subjects stand on their own. This edition does not activate the full Mars or farther-world campaigns, a broad equipment expansion, additional Earth ecology, later rocket hulls, new worlds or the full P15 reader redesign. Their earlier writing and artwork remain preserved. <a href="sources/scope_06.json">Exact selected and deferred scope</a>.</div>
</section>'''

    decision_hub = '''<section class="section-wrap edition06-decisions" id="edition06-decisions"><div class="section-head"><div><span class="eyebrow">Decisions and evidence</span><h2>Three choices are confirmed.</h2></div><p>The design directions below are settled for this programme. Physical fit, numeric tuning and implementation evidence remain separate questions.</p></div><div class="card-grid">
<article class="card"><span class="card-id">T1-DEC01 · Confirmed direction</span><h3>Compact-first Explorer</h3><p>The preferred candidate is nine blocks high, with a 3 × 3 body and a 5 × 5 fin envelope. Preserve the confirmed appearance and a usable airlocked cabin. The section study tests proposed occupied allocations; it does not verify actual cabin fit or approve final dimensions.</p><a href="#C06-P11-03">Inspect the fit arithmetic and remaining evidence →</a></article>
<article class="card"><span class="card-id">T1-DEC02 · Confirmed direction</span><h3>A dedicated station pair</h3><p>REF10 supplies the fabrication identity; REF09 supplies the assembly-station identity. Assembly controls a separate, explicitly linked vehicle berth. There are two mandatory functions, with no third mandatory assembly table. Final placement, dimensions and access need model evidence.</p><a href="#C06-P11-06">Read the station responsibilities →</a></article>
<article class="card"><span class="card-id">CEC01 · Conditional and unselected</span><h3>Leave Mars mats open</h3><p>The selected ecology work covers the established Europa, Titan and Morrow families. A Mars mat direction has not been selected and receives no new concept board in this programme.</p><a href="#C06-P10-01">Read the selected ecology scope →</a></article>
</div><div class="edition06-evidence-grid"><div class="intro-note"><strong>Future approval follows concrete evidence.</strong> If a measured Explorer fit study needs a larger envelope or a reference identity must change, bring back the smallest concrete alternatives. This documentation build does not require another decision to begin. <a href="sources/decisions_06.json">Decision record</a> · <a href="sources/tier1_closure_06.json">Tier 1 evidence and future scenarios</a>.</div><div class="intro-note"><strong>Recipes are recommendations, not tuned data.</strong> Material relationships and six advanced routes are documented. Unassigned numeric yields are <code>null</code>; energy, capacity, timing, gameplay balance and runtime recovery need later evidence. <a href="#C06-P07-01">Material record policy</a> · <a href="#C06-P06-ADV01">Advanced route status</a>.</div></div></section>'''

    figure_groups = []
    for wave in WAVES:
        group = [figure for figure in figures if source_by_package[figure["package"]][0]["wave"] == wave]
        figure_groups.append(f'<div class="edition06-figure-group"><h3>{wave} · {esc(WAVES[wave][0])}</h3><ul>' + "".join(f'<li><a href="#{esc(f["target"])}"><code>{esc(f["id"])}</code> {esc(f["title"])}</a><span>{"Editable diagram" if f["file"].endswith(".svg") else "Minecraft concept board"}</span></li>' for f in group) + '</ul></div>')
    figure_hub = f'''<section class="section-wrap" id="edition06-figures"><div class="section-head"><div><span class="eyebrow">Balanced illustration pass</span><h2>Twenty-five new views, in context.</h2></div><p>Twelve selected concept boards show the high-level forms and experiences. Thirteen exact SVG diagrams explain relationships, flows and boundaries. Every figure is placed beside its source section, with a full-size view and provenance.</p></div><p class="overview-intro">The generation ledger records 15 calls of the agreed maximum 16, including two user-rejected outputs and one failed reference-load attempt. Only the twelve selected boards are presented as the new edition’s artwork. The corrected direction keeps block-built shapes and coarse pixel materials. Lighting in concept art is not a promise of shader availability.</p><div class="edition06-figure-index">{''.join(figure_groups)}</div><p><a href="#visual-directory">Browse all 84 visual records</a> · <a href="sources/visual_catalogue_06.json" download>Download the complete catalogue</a> · <a href="sources/image_generation_06.json">Read generation provenance and style corrections</a></p></section>'''

    source_rows = []
    for entry in source_entries:
        chapter = next(c for c in chapters if c["source_key"] == entry["key"])
        source_rows.append(f'<tr><td><a href="sources/{esc(entry["file"])}" download>{esc(entry["file"])}</a></td><td>{esc(entry["wave"])} · {esc(entry["package"])}<br><a href="#{esc(chapter["id"])}">Read in the document</a></td><td>{entry["words"]:,} words<br>{entry["chapters"]} chapter{"s" if entry["chapters"] != 1 else ""}</td><td><code>{entry["sha256"][:16]}</code></td></tr>')
    registry_links = "".join(f'<li><a href="sources/{esc(filename)}" download>{esc(filename)}</a></li>' for filename in register_names)
    handoff_hub = f'''<section class="section-wrap" id="edition06-sources"><div class="section-head"><div><span class="eyebrow">Portable source package</span><h2>Read it, review it, derive from it.</h2></div><p>The ordinary text files and JSON registers carry the complete source. This HTML provides the reading experience, contextual artwork, direct links, search and local review notes.</p></div><div class="edition06-actions"><a class="button primary" href="sources/complete_direction_06.txt" download>Download the complete text · 22 sources</a><a class="button" href="sources/edition-manifest.json" download>Edition manifest</a><a class="button" href="sources/source_anchor_map_06.json" download>Chapter, section and symbol map</a><a class="button" href="sources/visual_catalogue_06.json" download>All 84 visuals</a><a class="button" href="README.txt">Open the package guide</a></div>
<div class="table-wrap" tabindex="0" aria-label="Eleven new narrative source files"><table><thead><tr><th>Exact narrative source</th><th>Reading route</th><th>Scope</th><th>SHA-256 prefix</th></tr></thead><tbody>{''.join(source_rows)}</tbody></table></div>
<details class="current-source-group" open><summary>Selected scope, evidence and subject registers</summary><ul class="edition06-file-list">{registry_links}</ul><p><a href="sources/figures_06.json" download>25 new figure records</a> · <a href="sources/image_generation_06.json" download>Generation and selection ledger</a> · <a href="sources/reader_build_report_06.json" download>Reader build and preservation checks</a> · <a href="sources/verification_06.json" download>Independent verification receipt</a> · <a href="sources/verify_edition06.py" download>Independent verifier</a> · <a href="sources/build_edition06.py" download>Portable additive builder</a></p></details>
<div class="edition06-handoff" id="edition06-handoff"><span class="eyebrow">Maintainer ↔ Website report</span><h3>One manual reconciliation round trip.</h3><p>Give the Website agent the pinned source edition and handoff. It should credit the Edition 05 work already integrated, preserve newer work, map these additions into its derived summaries and return a report with exact revisions, coverage, source corrections, artwork derivatives and actual verification. The documentation maintainer then reconciles that report into a later source record.</p><div class="edition06-actions"><a class="button primary" href="sources/website_handoff_06.md" download>Website agent handoff</a><a class="button" href="sources/website_return_template_06.md" download>Return report and prompt template</a></div><p class="edition06-fine">The immutable source prompt is accompanied by a ready-to-send WEBSITE_AGENT_PROMPT.txt in the delivery envelope. That prompt and the repository delivery receipt identify the exact source commit; use their pin instead of assuming the branch has not moved. The return template keeps unknown and NOT RUN evidence explicit. Public Website implementation and publication belong to the Website agent; this reader build makes no deployment claim.</p></div>
<div class="intro-note"><strong>Historical sources stay intact.</strong> <a href="sources/reader_05_preserved.html">Open the exact Edition 05 reader</a>, <a href="sources/edition05_manifest_preserved.json">its preserved manifest</a> or <a href="sources/baseline_05_inventory.json">the 191-file baseline inventory</a>. The earlier eleven source files remain available in the <a href="#source-package">retained source directory</a>. Edition 06 adds to their content instead of rewriting their history.</div></section>'''

    sections = [hub, decision_hub, figure_hub, handoff_hub]
    for wave, (title, desc) in WAVES.items():
        wave_chapters = [chapter for chapter in chapters if chapter["wave"] == wave]
        links = "".join(f'<li><a href="#{esc(chapter["id"])}">{esc(chapter["title"])}</a><span>{esc(chapter["package"])}</span></li>' for chapter in wave_chapters)
        sections.append(f'<section class="reader-band edition06-wave" id="edition06-{wave.lower()}"><span class="eyebrow">Selected completion · {wave}</span><h2>{esc(title)}</h2><p>{esc(desc)}</p><ul class="edition06-wave-chapters">{links}</ul><a href="#edition06">Back to the Edition 06 overview ↑</a></section>')
        sections.extend(chapter_html[chapter["id"]] for chapter in wave_chapters)
    new_content = '\n<!-- Edition 06 additive source layer; Edition 05 follows unchanged below. -->\n' + "\n".join(sections) + '\n<!-- End Edition 06 additive source layer. -->\n'

    css = '''
/* Edition 06 additive presentation. Existing fixed-reader scripts and storage keys remain intact. */
.edition06-hub .section-head h2{max-width:700px}.edition06-stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:1px;background:var(--line);border:1px solid var(--line);margin:28px 0}.edition06-stats>div{background:var(--panel);padding:22px}.edition06-stats strong{display:block;color:var(--ink);font-family:var(--serif);font-weight:400;font-size:36px;line-height:1.15}.edition06-stats span{display:block;color:var(--muted);font-size:11px;line-height:1.55;margin-top:8px}.edition06-actions{display:flex;flex-wrap:wrap;gap:9px;margin:20px 0}.edition06-wave-nav{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:28px 0 38px}.edition06-wave-nav>a{display:block;border:1px solid var(--line);border-left:3px solid var(--accent);padding:20px 22px;text-decoration:none;color:var(--ink);font-size:17px;line-height:1.5;background:var(--panel)}.edition06-wave-nav>a:hover{border-color:var(--sea)}.edition06-wave-nav span{display:block;font-size:11px;font-family:monospace;letter-spacing:.08em;color:var(--accent);margin-bottom:7px}.edition06-wave-nav small{display:block;font-size:12px;line-height:1.65;color:var(--muted);margin-top:8px}.edition06-small-head{margin-top:38px}.edition06-package-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.edition06-package{display:flex;flex-direction:column;scroll-margin-top:var(--reader-offset,160px)}.edition06-package h3 a{font:inherit;text-decoration:none;color:var(--ink)}.edition06-package h3 a:hover{text-decoration:underline}.edition06-package>.button{align-self:flex-start;margin-top:auto}.edition06-package-figures{padding-top:12px;border-top:1px solid var(--line)}.edition06-package-figures a{white-space:nowrap}.edition06-evidence-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}.edition06-evidence-grid .intro-note{margin-bottom:0}.edition06-decisions .card p{font-size:13px}.edition06-figure-index{display:grid;grid-template-columns:1fr 1fr;gap:22px 28px;margin:25px 0}.edition06-figure-group h3{font-size:17px}.edition06-figure-group ul,.edition06-wave-chapters{padding:0;list-style:none}.edition06-figure-group li{border-top:1px solid var(--line);padding:12px 0;font-size:13px;line-height:1.65}.edition06-figure-group li span{display:block;font-size:11px;color:var(--muted);margin-top:3px}.edition06-figure-group code{font-size:11px;margin-right:5px}.edition06-wave-chapters{max-width:820px;margin:20px 0}.edition06-wave-chapters li{display:flex;gap:15px;justify-content:space-between;border-top:1px solid var(--line);padding:10px 0;font-size:13px}.edition06-wave-chapters span{font-family:monospace;color:var(--accent);white-space:nowrap}.edition06-file-list{columns:2;column-gap:28px;padding-left:22px;overflow-wrap:anywhere}.edition06-file-list li{padding:4px 0;font-size:12px;break-inside:avoid}.edition06-handoff{border:1px solid var(--line);background:var(--panel);padding:25px;margin:28px 0}.edition06-handoff h3{font-family:var(--serif);font-size:28px;font-weight:400;margin-top:10px}.edition06-handoff p{font-size:13px;color:var(--muted)}.edition06-handoff .edition06-fine{font-size:11px}.edition06-historical-note{border-left:3px solid var(--accent);background:var(--panel2);color:var(--muted);padding:16px 20px;margin:0 0 24px;font-family:var(--sans);font-size:12px;line-height:1.75}.edition06-historical-note strong{color:var(--ink)}.edition06-chapter .chapter-actions>a{font:11px var(--sans);line-height:1.5}.edition06-figure{font-family:var(--sans);max-width:100%;margin:30px 0 38px}.edition06-figure figcaption{background:var(--panel);color:var(--muted);font-size:12px}.edition06-figure figcaption p{margin:9px 0;font:inherit;line-height:1.8}.edition06-figure .plate-meta{flex-wrap:wrap;gap:6px}.edition06-figure .plate-meta span{white-space:normal}.edition06-figure .figure-status{border-top:1px solid var(--line);padding-top:11px}.edition06-figure .figure-limits{border-top:1px solid var(--line);padding-top:10px;margin-top:12px}.edition06-figure .figure-limits summary{color:var(--sea)}.edition06-figure .figure-download{margin-top:13px}.edition06-diagram>button{background:#111c27}.edition06-diagram img{width:100%;height:auto;max-height:none}.source-id-link{font-size:.9em;text-decoration-style:dotted;text-decoration-thickness:1px}.edition06-nav-group>summary{color:var(--accent)}.edition06-chapter .chapter-link-nav a{max-width:48%;overflow-wrap:break-word}.edition06-catalogue-note{font-size:12px;color:var(--muted);margin:18px 0}.edition06-wave{margin-top:0}.edition06-wave>a{font-size:12px}
@media(max-width:1100px){.edition06-decisions .card-grid{grid-template-columns:1fr}.edition06-stats strong{font-size:30px}.edition06-stats>div{padding:17px}}
@media(max-width:700px){.edition06-stats{grid-template-columns:1fr 1fr}.edition06-wave-nav,.edition06-package-grid,.edition06-figure-index,.edition06-evidence-grid{grid-template-columns:1fr}.edition06-file-list{columns:1}.edition06-stats strong{font-size:30px}.edition06-wave-nav>a{padding:18px}.edition06-figure figcaption{padding:17px}.edition06-figure .plate-meta{display:block}.edition06-figure .plate-meta span{display:block;margin-top:7px}.edition06-wave-chapters li{font-size:12px}.edition06-handoff{padding:20px}.edition06-actions .button{font-size:11px}.edition06-chapter .chapter-actions{align-items:flex-start}.edition06-package{padding:21px}}
@media print{.edition06-wave-nav,.edition06-actions,.edition06-package>.button,.edition06-figure .figure-download,.edition06-chapter .chapter-actions{display:none}.edition06-stats,.edition06-package,.edition06-handoff,.edition06-historical-note{background:#fff;color:#222;border-color:#aaa}.edition06-stats>div{background:#fff}.edition06-figure figcaption{background:#fff;color:#333}.edition06-figure .figure-limits{display:block}.edition06-figure .figure-limits:not([open])>:not(summary){display:block}.edition06-figure{break-inside:avoid}.edition06-figure .plate-meta strong{color:#111}.edition06-historical-note a,.edition06-figure a{color:#333}}
'''
    result = old.replace('</style></head>', css + '</style></head>', 1)
    hero = baseline.elements["overview"]
    old_hero = old[hero["start"]:hero["end"]]
    new_hero = old_hero.replace("Current edition 05", "Selected completion edition 06", 1)
    new_hero = new_hero.replace('href="#current-edition">Open the current direction', 'href="#edition06">Open the current direction', 1)
    old_strip = re.search(r'<div class="edition-strip">.*?</div></div>\s*</section>$', new_hero, re.S)
    assert old_strip
    new_strip = f'<div class="edition-strip"><div><b>111,411</b><span>Earlier source words preserved</span></div><div><b>{added_words:,}</b><span>Words of selected completion</span></div><div><b>15</b><span>Confirmed reference sheets</span></div><div><b>84</b><span>Concepts, references and diagrams</span></div></div></section>'
    new_hero = new_hero[:old_strip.start()] + new_strip
    assert old_hero in result
    result = result.replace(old_hero, new_hero + new_content, 1)
    result = result.replace('<title>SUBSTELLAR — Creative &amp; Product Direction</title>', '<title>SUBSTELLAR — Creative &amp; Product Direction · Edition 06</title>', 1)
    result = result.replace('<a class="skip" href="#complete-text">Skip to the complete text</a>', '<a class="skip" href="#edition06-w0">Skip to the current complete text</a>', 1)
    result = result.replace('Current edition 05 · Reader navigation update 04 retained</span><a href="#overview">', 'Selected completion 06 · Reader navigation update 04 retained</span><a href="#overview">', 1)

    nav = []
    for wave, (title, _) in WAVES.items():
        nav.append(f'<details class="nav-group edition06-nav-group" open><summary>{esc(wave)} · {esc(title)}</summary><div class="nav-chapter"><a href="#edition06-{wave.lower()}">{esc(wave)} reading route</a></div>')
        for chapter in [c for c in chapters if c["wave"] == wave]:
            num = chapters.index(chapter) + 1
            nav.append(f'<div class="nav-chapter"><a href="#{esc(chapter["id"])}"><span class="nav-num">{num:02d}</span>{esc(chapter["title"])}</a><details><summary>Sections · {len(chapter["sections"])}</summary><div class="nav-sub">')
            nav.extend(f'<a href="#{esc(section["id"])}">{esc(section["title"])}</a>' for section in chapter["sections"])
            nav.append('</div></details></div>')
        nav.append('</details>')

    def update_sidebar(sidebar):
        prime_addition = '<a href="#edition06">Current direction · Edition 06</a><a href="#edition06-decisions">Confirmed choices and evidence</a><a href="#edition06-figures">25 new figures</a><a href="#edition06-sources">Current sources and handoff</a>'
        sidebar = sidebar.replace('<nav class="nav-prime">', '<nav class="nav-prime">' + prime_addition, 1)
        sidebar = sidebar.replace('href="#current-edition">Current direction · Edition 05', 'href="#current-edition">Preserved Edition 05 overview', 1)
        sidebar = sidebar.replace('href="#decision-review-05">Decisions and recommendations', 'href="#decision-review-05">Historical decisions · Edition 05', 1)
        sidebar = sidebar.replace('href="#remaining-detail-05">', 'href="#remaining-detail-05">', 1)
        sidebar = sidebar.replace('</nav>', '</nav>' + "\n".join(nav), 1)
        return sidebar

    result = section_transform(result, "sidebar", update_sidebar)

    historical = {
        "current-edition": "This is the preserved Edition 05 overview. Its current/pending wording records that earlier edition. The selected Edition 06 work and three confirmed choices are above.",
        "decision-review-05": "Historical decision review. Compact-first Explorer and the dedicated REF10/REF09 pair are now confirmed directions; their physical fit remains unverified. CEC01 is conditional and unselected.",
        "source-map-05": "This topic map records the Edition 05 synthesis and its earlier sources. The Edition 06 package, chapter and symbol map adds the selected completion sources without changing this record.",
        "reconciliation-05": "This is retained reconciliation history. Current game evidence is the accessible pushed a0a702 development record; the 512-level proposal is historical and Smelt remains unbuilt. Read Edition 06 for the current evidence boundary.",
        "remaining-detail-05": "This is the earlier remaining-detail register. Several selected subjects have since received Edition 06 documentation and artwork. Its old gap/status labels are historical; use the current coverage and completion backlog for remaining work.",
        "illustrated-coverage": "This coverage table records Illustrated Overview 03. Its ten overview slices remain credited. Edition 06 adds selected detailed coverage and 25 figures; earlier dedicated-view gaps should be reconciled with the current coverage record.",
        "planning-slice": "This is retained Planning Edition 02 material. Its source text remains complete. Subsequent worked journeys, confirmed choices, current game evidence and selected completion work are recorded in Editions 05 and 06.",
        "planning-decisions": "Historical Planning Edition 02 decisions. Apply the current confirmed choices above before treating a former pending option as unresolved. Exact model dimensions and game balance still require their own evidence.",
        "tier1-model-matrix": "This original Tier 1 matrix preserves the earlier reference/model proposal. The Edition 06 closure applies the confirmed compact-first and dedicated-pair directions; the old numbers are not newly verified fit evidence.",
        "planning-build-backlog": "This is an earlier proposed game-build backlog, retained as source history. Edition 06 is documentation work, not a game-development authorization. The inspected game roadmap still identifies Smelt as the next unbuilt slice.",
        "next-image-slice": "This was the next image plan at an earlier edition. Later illustrations and the Edition 06 generation ledger now record selected outputs. Its old future-tense artwork statuses are preserved history.",
        "artwork-backlog": "This is the preserved original artwork backlog. It remains useful for individual future views, but old missing-image labels do not describe all later editions. Check the current 84-record catalogue and Edition 06 coverage before commissioning more art.",
        "source-package": "This directory preserves the earlier edition’s files and descriptions. For the complete current text across all 22 narrative sources, use complete_direction_06.txt and the Edition 06 manifest above.",
    }
    for chapter in old_map["chapters"]:
        if chapter.get("new_in_edition05"):
            historical[chapter["id"]] = "Preserved Edition 05 narrative. Its proposals, recorded pending choices and evidence limits remain readable in their original wording. Edition 06 applies the later confirmed directions and current revision-bound reconciliation above."
    for identifier, message in historical.items():
        note = f'<aside class="edition06-historical-note" aria-label="Historical edition context"><strong>Retained source history.</strong> {esc(message)} <a href="#edition06">Edition 06 overview</a> · <a href="#edition06-decisions">Current decisions</a> · <a href="sources/coverage_06.json">Current coverage</a>.</aside>'
        result = insert_after_open(result, identifier, note)

    visual_records = copy.deepcopy(old_visuals["records"]) + copy.deepcopy(figures)
    visual_catalogue = {"edition": EDITION, "records": visual_records,
                        "unique_raster_concepts_and_references": 46, "editable_figures": 38,
                        "total_distinct_visuals": 84, "new_in_edition06": 25,
                        "notes": ["All 59 earlier visual records and original image files are retained.",
                                  "Edition 06 adds 12 selected Minecraft concept boards and 13 exact explanatory SVGs.",
                                  "Concept images do not establish game implementation, final model fit or shader availability.",
                                  "Source metadata, captions, status, original-file links and limitations remain attached to each new figure."]}
    new_visual_rows = []
    for record in figures:
        row_id = "visual-row-" + record["id"].lower()
        new_visual_rows.append(f'<tr id="{row_id}" data-visual-row data-kind="{esc(record["kind"])}"><td><code>{esc(record["id"])}</code><br><a href="#{esc(record["target"])}">{esc(record["title"])}</a></td><td>{esc(record["kind"])}<br><span class="pill overview">{esc(record["status"])}</span></td><td>{esc(record["caption"])}</td><td>{" · ".join(source_link(s) for s in record.get("source_ids", []))}</td></tr>')
        new_search.append({"id": record["target"], "chapter": "Edition 06 visual catalogue", "title": f'{record["id"]} · {record["title"]}',
                           "text": " ".join([record["caption"], record["status"], *record.get("source_ids", []), *record.get("not_established", [])]),
                           "source": "edition06-figure", "package": record["package"]})

    def update_visual_directory(section):
        section = section.replace('</select>', '<option value="Edition 06 concept board">Edition 06 concept board</option><option value="Edition 06 editable diagram">Edition 06 editable diagram</option></select>', 1)
        section = section.replace('</tbody>', "".join(new_visual_rows) + '</tbody>', 1)
        marker = '<div class="backlog-controls">'
        return section.replace(marker, '<p class="edition06-catalogue-note">Current catalogue: 84 records — 46 raster concepts/references and 38 editable diagrams or interface sheets. Earlier statuses retain their edition context. <a href="#edition06-figures">View the 25 Edition 06 additions</a>.</p>' + marker, 1)

    result = section_transform(result, "visual-directory", update_visual_directory)

    def update_chapter_directory(section):
        cards = []
        for chapter in chapters:
            description = PACKAGE_SUMMARIES[chapter["package"]][1]
            cards.append(f'<article class="card chapter-guide-card" data-chapter-guide data-slice="{esc(chapter["wave"])}"><span class="card-id">Edition 06 · {esc(chapter["wave"])} · {esc(chapter["package"])}</span><h3><a href="#{esc(chapter["id"])}">{esc(chapter["title"])}</a></h3><p>{esc(description)}</p><a class="guide-context" href="#edition06-{chapter["wave"].lower()}">Current wave context →</a></article>')
        # The retained directory has 45 earlier cards. Add the four Edition 05
        # narrative chapters as well, so this directory now covers all 67.
        for chapter in old_map["chapters"]:
            if f'href="#{chapter["id"]}"' not in section:
                cards.append(f'<article class="card chapter-guide-card" data-chapter-guide data-slice="05"><span class="card-id">Preserved Edition 05 narrative</span><h3><a href="#{esc(chapter["id"])}">{esc(chapter["title"])}</a></h3><p>Complete earlier source, preserved with its historical proposal and evidence context.</p><a class="guide-context" href="#current-edition">Edition 05 context →</a></article>')
        section = section.replace('<div class="chapter-guide-grid">', '<div class="chapter-guide-grid">' + "".join(cards), 1)
        options = ''.join(f'<option value="{wave}">Edition 06 · {wave} · {esc(title)}</option>' for wave, (title, _) in WAVES.items()) + '<option value="05">Preserved Edition 05 additions</option>'
        section = section.replace('</select>', options + '</select>', 1)
        return section.replace('<div class="backlog-controls">', f'<p class="edition06-catalogue-note">All {all_chapter_count} complete chapters are indexed here: 18 Edition 06 additions, four preserved Edition 05 additions and the 45 earlier chapters.</p><div class="backlog-controls">', 1)

    result = section_transform(result, "chapter-directory", update_chapter_directory)
    editorial_entries = [
        ("edition06", "Selected completion Edition 06", "Ten selected packages across W0 W1 W2 W3; full source preservation; Minecraft concept artwork."),
        ("edition06-decisions", "Confirmed choices and open evidence", "T1-DEC01 compact first nine blocks 3 by 3 5 by 5 candidate fit unverified. T1-DEC02 dedicated REF10 fabrication REF09 assembly separate berth. CEC01 Mars mats conditional unselected."),
        ("edition06-figures", "25 new figures", "12 selected Minecraft concept boards, 13 editable SVG diagrams, generation ledger 15 calls maximum 16."),
        ("edition06-sources", "Current source downloads", "22 complete narrative sources, JSON registers, manifest, stable anchor map, reproducible Python builder and preservation report."),
        ("edition06-handoff", "Website reconciliation handoff", "Website agent prompt, return report, source corrections, current revisions, manual reconciliation, no automatic publication."),
    ]
    editorial_entries.extend((f"edition06-{wave.lower()}", f"{wave} · {title}", desc) for wave, (title, desc) in WAVES.items())
    for identifier, title, text in editorial_entries:
        new_search.append({"id": identifier, "chapter": "Edition 06 overview", "title": title, "text": text, "source": "edition06-editorial"})
    search = old_search + new_search
    result = replace_json_script(result, "search-data", search)
    result = replace_json_script(result, "visual-data", visual_catalogue)

    # Check source identity before writing. No HTML parsing/serialization is
    # allowed to change any retained source block or original inline script.
    final = Inventory(result)
    duplicate_ids = [key for key, count in final.ids.items() if count != 1]
    assert not duplicate_ids, duplicate_ids[:20]
    assert set(baseline.ids).issubset(final.ids), "An earlier anchor was removed"
    altered_blocks = [key for key, value in baseline.source_blocks.items() if final.source_blocks.get(key) != value]
    assert not altered_blocks, altered_blocks[:10]
    assert len(final.source_blocks) == 2000 + sum(s["blocks"] for s in source_entries)
    for entry in source_entries:
        assert sum(key[0] == entry["key"] for key in final.source_blocks) == entry["blocks"]
    inline_scripts = lambda text: [m.group(0) for m in re.finditer(r'<script>(.*?)</script>', text, re.S)]
    assert inline_scripts(old) == inline_scripts(result), "Existing application scripts changed"
    old_art = re.search(r'<figure class="hero-art">.*?</figure>', old_hero, re.S).group(0)
    assert old_art in result, "Hero artwork markup changed"
    assert all(record["id"] in final.ids for record in search), "Unresolved search target"
    assert len({row["id"] for row in visual_records}) == 84
    assert all(record["target"] in final.ids for record in visual_records), "Unresolved visual target"
    sidebar_span = final.elements["sidebar"]
    sidebar_html = result[sidebar_span["start"]:sidebar_span["end"]]
    missing_nav = [item["id"] for chapter in chapters for item in [chapter, *chapter["sections"]] if f'href="#{item["id"]}"' not in sidebar_html]
    assert not missing_nav, missing_nav
    guide_count = sum("data-chapter-guide" in tag["attrs"] for tag in final.tags)
    assert guide_count == all_chapter_count, (guide_count, all_chapter_count)
    visual_row_count = sum("data-visual-row" in tag["attrs"] for tag in final.tags)
    assert visual_row_count == 84

    preserved_inventory = read_json("baseline_05_inventory.json")
    preserved_overrides = {"index.html": "sources/reader_05_preserved.html", "README.txt": "sources/README_05.txt", "sources/edition-manifest.json": "sources/edition05_manifest_preserved.json"}
    preserved_mismatches = []
    for record in preserved_inventory:
        path = ROOT / preserved_overrides.get(record["path"], record["path"])
        if not path.is_file() or sha(path.read_bytes()) != record["sha256"]:
            preserved_mismatches.append(record["path"])
    assert not preserved_mismatches, preserved_mismatches
    assert sum(record["path"].startswith("images/") for record in preserved_inventory) == 93
    for source in old_manifest["source_content"]:
        assert sha((HERE / source["file"]).read_bytes()) == source["sha256"]

    all_sources = copy.deepcopy(old_manifest["source_content"]) + source_entries
    complete_parts = ["SUBSTELLAR — Creative and Product Direction\nSelected completion 06\n\nComplete narrative source collection. Each source below is preserved verbatim; source-boundary labels are editorial separators. Current evidence and decisions are in reconciliation_06.txt and decisions_06.json. Earlier wording retains its edition context.\n"]
    for source in all_sources:
        filename = source["file"]
        value = (HERE / filename).read_text(encoding="utf-8")
        complete_parts.append(f'\n\n===== BEGIN EXACT SOURCE: {filename} | SHA-256 {source["sha256"]} =====\n\n' + value + f'\n\n===== END EXACT SOURCE: {filename} =====\n')
    complete = "".join(complete_parts)
    assert all((HERE / source["file"]).read_text(encoding="utf-8") in complete for source in all_sources)

    exported_chapters = [{k: v for k, v in chapter.items() if k != "blocks"} for chapter in chapters]
    source_map = {"schema_version": 2, "edition": EDITION, "reader_base": "index.html", "path_base": "Complete substellar folder",
                  "purpose": "Exact retained and additive chapter/section anchors, source-block targets and current symbol relationships.",
                  "chapter_count": all_chapter_count, "chapters": copy.deepcopy(old_map["chapters"]) + exported_chapters,
                  "canonical_symbol_anchors": current_symbols,
                  "historical_symbol_anchors_05": copy.deepcopy(old_map["canonical_symbol_anchors"]),
                  "edition06_source_block_targets": block_targets,
                  "new_section_count": sum(len(chapter["sections"]) for chapter in chapters),
                  "symbol_anchor_note": "Current symbols point to the selected documentation where defined; preserved Edition 05 symbol relationships remain in historical_symbol_anchors_05. Anchors are documentation references, not implementation claims.",
                  "figure_anchors": {figure["id"]: figure["target"] for figure in figures},
                  "reader_sha256": sha(result)}
    figure_manifest = {"schema_version": 1, "edition": EDITION, "figures": figures, "count": 25,
                       "concept_boards": 12, "editable_diagrams": 13, "input_registers": FIGURE_INPUTS,
                       "placement_policy": "Each figure appears once after its first declared source section; all other declared sections remain linked in its caption."}
    baseline_summary = {"edition": old_manifest["edition"], "manifest": "sources/edition05_manifest_preserved.json",
                        "reader": "sources/reader_05_preserved.html", "inventory": "sources/baseline_05_inventory.json",
                        "source_words": 111411, "chapters": 49, "source_blocks": 2000,
                        "distinct_visuals": 59, "art_files_preserved": 93, "baseline_files_preserved": 191}
    manifest = {"title": old_manifest["title"], "edition": EDITION, "edition_id": "selected-completion-06", "date": DATE, "date_timezone": "Etc/UTC",
                "previous_edition": baseline_summary, "site": "index.html", "reader_sha256": sha(result),
                "original_text_approved": True, "current_scope": "sources/scope_06.json", "current_decisions": "sources/decisions_06.json",
                "source_content": all_sources, "total_source_words": total_words, "new_source_words": added_words,
                "prior_source_words_preserved": 111411, "total_chapters": all_chapter_count, "new_chapters": len(chapters),
                "source_file_count": len(all_sources), "source_block_count": len(final.source_blocks),
                "references": 15, "raster_visuals": 46, "diagrams": 38, "total_distinct_visuals": 84,
                "new_concept_boards": 12, "new_editable_diagrams": 13,
                "new_generation_call_cap": generation["budget"]["maximum_total_calls"],
                "new_generation_calls_used": generation["budget"]["calls_used_in_this_programme"],
                "generation_ledger": "sources/image_generation_06.json", "figure_register": "sources/figures_06.json", "visual_catalogue": "sources/visual_catalogue_06.json",
                "source_anchor_map": "sources/source_anchor_map_06.json", "complete_text": "sources/complete_direction_06.txt",
                "complete_text_sha256": sha(complete), "selected_packages": scope["selected_packages"], "waves": scope["waves"],
                "confirmed_decisions": decisions["decisions"], "image_records": copy.deepcopy(old_manifest["image_records"]) + [copy.deepcopy(figure) for figure in figures if figure.get("original")],
                "reader_navigation_revision": {"revision": "04 retained", "features": old_manifest["reader_navigation_revision"]["features"],
                                               "all_new_chapters_and_sections_registered": True, "storage_keys_changed": False, "browser_interaction_verified": False},
                "verification": {"builder_report": "sources/reader_build_report_06.json", "static_checks_passed": True,
                                 "browser_checks": "NOT RUN by this builder", "mod_checks": "NOT RUN; documentation scope", "publication": "No public Website or game publication performed by this builder"},
                "actual_mod_development_authorized": False,
                "notes": ["All 191 baseline files are preserved either in place or in the three explicitly named preserved copies.",
                          "All 2000 earlier source-block element byte strings, earlier anchors and inline application scripts remain intact.",
                          "Historical status views are labelled; current evidence and confirmed choices are additive.",
                          "All 22 narrative sources are reproduced in full in complete_direction_06.txt.",
                          "New concepts follow the corrected Minecraft block and coarse-pixel material direction.",
                          "The 15-call ledger includes rejected and failed attempts; only 12 selected concepts are current artwork.",
                          "Exact recipes, fit, capacities, travel physics and runtime behaviour remain proposed or unverified where documented.",
                          "This manifest supersedes current presentation fields only; previous edition manifests remain unchanged."]}

    readme = f'''SUBSTELLAR — CREATIVE & PRODUCT DIRECTION
SELECTED COMPLETION 06 · {DATE}

OPEN THE READER
Open index.html in a modern browser. Keep the complete substellar folder together;
images and source downloads use relative paths. The reader has no server or
network dependency. A local HTTP server may also serve this folder.

WHAT THIS EDITION CONTAINS
{total_words:,} source words across 22 complete narrative sources and {all_chapter_count} chapters.
All 111,411 earlier source words, 49 earlier chapters, 2,000 old source blocks,
59 earlier visual records and 93 earlier artwork files are preserved.
The additions are {added_words:,} words, 18 chapters, 12 selected Minecraft concept boards
and 13 editable explanatory SVGs. The complete catalogue has 84 records:
46 raster concepts/references and 38 editable diagrams or interface sheets.

The ten selected packages are P00, P01, P02, P03, P06, P07, P10, P11, P12-A and
P12-B, repacked into W0–W3. Start at index.html#edition06 or use any package card.
W0: evidence, scope and confirmed decisions.
W1: industry, all 66 machine responsibilities, advanced route recommendations,
    all 37 material families and ten manufactured-product families.
W2: Earth's seven realms/35 experiences, Old Valley and ten selected ecology families.
W3: remaining Moon journeys, Tier 1 closure/fit study, four support vehicles,
    two orbital station families and seven shared module subjects.

READING TOOLS
The fixed current-reading bar and following contents sidebar are retained.
Every new chapter and subsection is registered in navigation and search.
Search the whole reader with the Search button or /. Expand images with their
buttons and use original PNG/full SVG links for detailed review. Chapter notes
remain local to the current browser; export them to keep a portable copy.
The existing theme, note and follow-preference storage keys are unchanged.
Print controls remain available. Browser interaction, accessibility automation,
and printed-page layout were NOT RUN by this static builder; do not infer runtime
verification from source checks. Read later independent verification receipts
when included in the final delivery.

CURRENT AUTHORITY AND LIMITS
Read sources/reconciliation_06.txt and sources/evidence_06.json. Accessible game
revision a0a702c2b5788d099645ab55ad23d8a5bf45c137 is pushed development work and
unmerged. The working column is 672 levels; the guide has 15 implemented depth
biomes. Seven creative realms/35 experiences are a different design vocabulary.
Smelt remains the next unbuilt gameplay slice. Website receipt adc088109436 is
recorded separately. This package is documentation, not new mod implementation.

Compact-first Explorer and the dedicated REF10 fabrication / REF09 assembly pair
are confirmed directions. Actual fit and final dimensions are unverified. The
separate linked vehicle berth remains required. CEC01 Mars mats stay conditional
and unselected. Proposed recipes and quantities require later evidence; a null
numeric yield does not mean zero cost. Attractive concept lighting does not
establish shader availability or a final in-game appearance.

SOURCE FILES
sources/complete_direction_06.txt — all 22 exact narrative sources with separators.
sources/edition-manifest.json — current content counts, source hashes and limits.
sources/source_anchor_map_06.json — complete chapter, section, block and symbol map.
sources/visual_catalogue_06.json — all 84 visual records.
sources/figures_06.json — 25 new figure records with exact context and file hashes.
sources/image_generation_06.json — 15-call ledger, selection and style corrections.
sources/scope_06.json, decisions_06.json, evidence_06.json — current authority.
sources/coverage_06.json and completion_backlog_06.json — scope and remaining work.
sources/reader_build_report_06.json — static build and preservation receipt.
sources/verification_06.json — independent verification and its explicit limits.
sources/verify_edition06.py — independent source, preservation and reader verifier.

PRESERVED HISTORY
sources/reader_05_preserved.html — exact Edition 05 index.html.
sources/edition05_manifest_preserved.json — exact Edition 05 manifest.
sources/README_05.txt — exact Edition 05 root README.
sources/baseline_05_inventory.json — hashes of all 191 baseline files.
All other baseline files remain unchanged in their original relative locations.
Historical decisions and backlog views have visible Edition 06 context notices.
Old sources and old status records are evidence of their edition, not silently
updated current facts. The preserved DOCX and all supplied artwork remain included.

WEBSITE AGENT HANDOFF
sources/website_handoff_06.md — immutable source prompt. The ready-to-send
WEBSITE_AGENT_PROMPT.txt in the delivery envelope and the repository receipt
handoffs/2026-10-07-edition-06-documentation-delivery.md supply the exact source
commit. Use that pin instead of assuming a branch has not moved. The envelope
is outside this preserved substellar source folder; its prompt is distinct
from the earlier Website prompt retained in this folder as baseline history.
sources/website_return_template_06.md — report and return-prompt template.
The Website agent should preserve current work, credit Edition 05 integration,
derive current summaries from this source and return exact evidence/corrections.
The documentation maintainer then reconciles that report. No automatic source
synchronization, public Website publication or game BUILD is performed here.

REBUILD THE READER
Python 3.10+; Python standard library only, with the included preserved parser.
Run from this folder: python sources/build_edition06.py
The script resolves paths relative to itself and can run from any directory.
It always starts from sources/reader_05_preserved.html and reads the exact 11
additional narrative sources plus figure/register metadata. It never rebuilds
from the previous output. All required final sources and 25 figure files must
exist; missing inputs fail the build. Generated output files are index.html,
the current manifest, current figure/catalogue/anchor JSON, complete current text,
this README and the static reader-build report. Old sources are never rewritten.
The unchanged Edition 05 inline scripts supply interactive behaviour.

ARTWORK BUDGET AND STYLE
15 calls used of the agreed maximum 16, counting all retries and failed attempts.
12 selected boards are included. Two excessively realistic outputs were rejected
and one reference-load attempt failed; the ledger preserves that provenance.
Do not treat rejected boards as current creative reference. The corrected art
uses chunky Minecraft forms and coarse pixel textures for ores, ingots, powders,
equipment and buildings. No further image generation is part of this builder.
'''

    report = {"schema_version": 1, "edition": EDITION, "date": DATE, "method": "Python standard-library additive builder and independent inventory of generated HTML",
              "result": "PASS", "baseline_files_preserved": 191, "baseline_art_files_preserved": 93,
              "earlier_source_words_preserved": 111411, "earlier_source_blocks_byte_preserved": 2000,
              "earlier_anchor_count_preserved": len(baseline.ids), "earlier_chapters_preserved": 49,
              "new_source_files": 11, "new_source_words": added_words, "new_source_blocks": sum(s["blocks"] for s in source_entries),
              "new_chapters": len(chapters), "new_sections": sum(len(c["sections"]) for c in chapters),
              "total_source_words": total_words, "total_chapters": all_chapter_count,
              "total_source_blocks": len(final.source_blocks), "total_html_ids": len(final.ids),
              "duplicate_ids": duplicate_ids, "missing_new_navigation_targets": missing_nav,
              "search_records": len(search), "new_search_records": len(new_search),
              "visual_catalogue_records": 84, "contextual_new_figures": len(rendered_figures),
              "new_raster_concepts": 12, "new_editable_diagrams": 13,
              "chapter_directory_records": guide_count, "visual_directory_records": visual_row_count,
              "existing_inline_scripts_byte_preserved": True, "hero_artwork_markup_byte_preserved": True,
              "existing_storage_keys_unchanged": True, "all_new_chapters_and_sections_in_sidebar": True,
              "all_search_targets_resolve": True, "all_new_source_blocks_in_search": True,
              "all_22_complete_source_texts_present": True, "reader_sha256": sha(result),
              "static_local_links": "checked after outputs written", "browser_interaction": "NOT RUN",
              "axe_lighthouse": "NOT RUN", "print_layout": "NOT RUN", "game_runtime": "NOT RUN",
              "public_website_publication": "Not performed", "deterministic_rebuild": "Same inputs produce the same generated bytes; independently compare a repeated build before claiming the repeat was run."}

    outputs = {"index.html": result, "README.txt": readme,
               "sources/figures_06.json": json_text(figure_manifest),
               "sources/visual_catalogue_06.json": json_text(visual_catalogue),
               "sources/source_anchor_map_06.json": json_text(source_map),
               "sources/edition-manifest.json": json_text(manifest),
               "sources/complete_direction_06.txt": complete,
               "sources/reader_build_report_06.json": json_text(report)}
    write_outputs(outputs)
    broken_fragments, missing_files = [], []
    for tag, attr, link in final.links:
        parsed = urlsplit(link)
        if parsed.scheme or parsed.netloc or link.startswith("//"):
            continue
        if parsed.path:
            if not (ROOT / unquote(parsed.path)).is_file():
                missing_files.append(link)
        elif parsed.fragment and unquote(parsed.fragment) not in final.ids:
            broken_fragments.append(link)
    assert not broken_fragments, sorted(set(broken_fragments))[:30]
    assert not missing_files, sorted(set(missing_files))[:30]
    report["static_local_links"] = "PASS: every local file reference and same-document fragment resolves"
    report["broken_fragments"] = []
    report["missing_local_files"] = []
    (HERE / "reader_build_report_06.json").write_text(json_text(report), encoding="utf-8")
    print(json.dumps({"result": "PASS", "edition": EDITION, "chapters": all_chapter_count,
                      "words": total_words, "new_words": added_words, "figures": 84,
                      "new_figures": len(figures), "search_records": len(search),
                      "reader_sha256": sha(result), "output": str(ROOT / "index.html")}, indent=2))


if __name__ == "__main__":
    main()
