215 lines
8.7 KiB
Python
Raw Normal View History

2026-07-30 18:57:10 +02:00
"""One-time migration: split REPORT-tsi-parameter-selection.md into a thematic 4-part set + index.
The single report grew dense and heavily cross-referenced; this slices it into four cohesive parts
(kept in tsi-sim-pernode/ so all report-figures/ links stay valid) plus a short index that reuses the
canonical filename as the entry point. Section NUMBERS (§1-§9, A-C) are preserved as stable identifiers
across files; every §ref is rewritten into a clickable link to a portable `<a id="s6-5">` anchor,
same-file or cross-file as appropriate. Figure embeds and §9's config/script/run paths are untouched.
Run: python scripts/split_report.py (reads REPORT-...md, writes the 4 parts + overwrites the index)
"""
from __future__ import annotations
# ruff: noqa: E501 (one-time migration; index/nav strings are intentionally long prose)
import re
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent.parent
SRC = HERE / "REPORT-tsi-parameter-selection.md"
INDEX = "REPORT-tsi-parameter-selection.md"
P1 = "tsi-report-1-overview-and-recommendations.md"
P2 = "tsi-report-2-accuracy-and-design.md"
P3 = "tsi-report-3-robustness-and-incentives.md"
P4 = "tsi-report-4-reproducibility-and-appendices.md"
# top-level section id -> part filename, and the section order within each part
PART_SECTIONS = {
P1: ["1", "7", "8"],
P2: ["2", "3", "4", "5"],
P3: ["6"],
P4: ["9", "A", "B", "C"],
}
PART_TITLE = {
P1: "Part 1 — Overview and recommendations",
P2: "Part 2 — Accuracy and design",
P3: "Part 3 — Robustness and incentives",
P4: "Part 4 — Reproducibility and appendices",
}
SEC_TO_FILE = {s: f for f, secs in PART_SECTIONS.items() for s in secs}
def top_id(line: str) -> str | None:
m = re.match(r"^##\s+Appendix\s+([A-C])\b", line)
if m:
return m.group(1)
m = re.match(r"^##\s+(\d+)\.", line)
return m.group(1) if m else None
def header_anchor(line: str) -> str | None:
"""Anchor id for a section/subsection header line, e.g. §6.5 -> s6-5, App B.2 -> sB-2."""
m = re.match(r"^##\s+Appendix\s+([A-C])\b", line)
if m:
return "s" + m.group(1)
m = re.match(r"^##\s+(\d+)\.", line)
if m:
return "s" + m.group(1)
m = re.match(r"^###\s+([0-9A-C]+)\.(\d+)", line)
if m:
return f"s{m.group(1)}-{m.group(2)}"
return None
def make_ref_rewriter(current_file: str):
"""Rewrite §N/§N.M and 'Appendix X' refs into links to their anchor (same- or cross-file)."""
def link(top: str, sub: str | None, label: str) -> str:
anchor = "s" + top + (f"-{sub}" if sub else "")
tgt = SEC_TO_FILE.get(top)
if tgt is None:
return label # unknown target: leave as text
dest = f"#{anchor}" if tgt == current_file else f"{tgt}#{anchor}"
return f"[{label}]({dest})"
def sec_sub(m: re.Match) -> str:
top, sub = m.group(1), m.group(2)
return link(top, sub, m.group(0))
def appendix(m: re.Match) -> str:
return link(m.group(1), None, m.group(0))
sec_re = re.compile(r"§\s?(\d+)(?:\.(\d+))?")
app_re = re.compile(r"\bAppendix\s+([A-C])\b")
def rewrite(text: str) -> str:
return app_re.sub(appendix, sec_re.sub(sec_sub, text))
return rewrite
def render_lines(lines: list[str], current_file: str) -> list[str]:
"""Inject anchors before headers and rewrite §refs, skipping fenced code blocks."""
rewrite = make_ref_rewriter(current_file)
out: list[str] = []
in_fence = False
for ln in lines:
if ln.lstrip().startswith("```"):
in_fence = not in_fence
out.append(ln)
continue
if in_fence:
out.append(ln) # never touch code (refs there stay plain text)
continue
aid = header_anchor(ln)
if aid is not None:
out.append(f'<a id="{aid}"></a>')
out.append(ln) # header title kept verbatim (no links inside headers)
continue
out.append(rewrite(ln))
return out
def part_header(fname: str, units_note: str) -> list[str]:
nav = (f"*[Part 1 — Overview & recommendations]({P1}) · [Part 2 — Accuracy & design]({P2}) · "
f"[Part 3 — Robustness & incentives]({P3}) · [Part 4 — Reproducibility & appendices]({P4}) · "
f"[Index]({INDEX})*")
where = ("*Sections live across the set: §1/§7/§8 in Part 1, §2§5 in Part 2, §6 in Part 3, "
"§9 and Appendices AC in Part 4.*")
return [
f"# Total-Stake-Inference parameter selection — {PART_TITLE[fname].split('')[1]}",
"",
units_note,
"",
nav,
"",
where,
"",
"---",
"",
]
def build_index(units_note: str) -> str:
lines = [
"# Total-Stake-Inference parameter selection",
"",
units_note,
"",
"This analysis selects and justifies the TSI parameters for Cryptarchia, from a per-node "
"network simulation (`tsi-sim-pernode`). It is split into four parts:",
"",
f"1. **[Overview and recommendations]({P1})** — the executive summary, the per-knob parameter "
"reference (§7), and the safest selection with residual risks and the recommendation-vs-spec "
"deltas (§8).",
f"2. **[Accuracy and design]({P2})** — the model and counting rule (§2), the seven findings and "
"their evidence (§3), and the design equations / selection algorithm (§4§5).",
f"3. **[Robustness and incentives]({P3})** — jitter, grinding, withholding, selfish mining, the "
"reward design, fork/reorg depth, and organic churn (§6).",
f"4. **[Reproducibility and appendices]({P4})** — how to re-run every study (§9), the residual "
"f-rounding offset (App A), the per-epoch noise floor (App B), and consensus detail (App C).",
"",
f"**Headline recommendation** (Cryptarchia baseline f = 1/30): security `k = 2160`, uncle "
f"window `W = 300` slots, uncle cap `U = ⌈ρ⌉ + 1` (2 at the Blend target), learning rate "
f"`β = 1`, peering degree ≥ 6 at scale, soft uncle rewards with `w_u + w_n < 1`, and operate "
f"at load `ρ = f·D_vis < 1`. The full recommended-configuration table and rationale are in "
f"**[Part 1 →]({P1})**.",
"",
]
return "\n".join(lines) + "\n"
def main() -> None:
text = SRC.read_text()
if "## 6. Robustness" not in text:
sys.exit("Source has already been split (no '## 6. Robustness' found in "
f"{SRC.name}, which is now the index). This one-time migration is complete; "
"re-run against the pre-split backup only.")
raw = text.split("\n")
# frontmatter (title + units note) is everything before the first "## " header
first_h = next(i for i, l in enumerate(raw) if l.startswith("## "))
units_note = raw[2] # the italic "*Per-node network simulation ... 1 slot = 1 s.*" line
# slice into top-level sections
sections: dict[str, list[str]] = {}
cur: str | None = None
for l in raw[first_h:]:
tid = top_id(l)
if tid is not None:
cur = tid
sections[cur] = []
if cur is not None:
sections[cur].append(l)
# update the reading-order note (in §1) to describe the 4-part structure
ro_old_prefix = "The rest of the report, in reading order:"
ro_new = ("This report is split into four parts (see the [index](" + INDEX + ")): "
"**Part 1** — the recommended configuration, the per-knob parameter reference (§7) and "
"the safest selection with residual risks and spec deltas (§8); **Part 2** — the model "
"and counting rule (§2), the evidence behind each finding (§3), and the design equations "
"and selection algorithm (§4§5); **Part 3** — robustness against noise, attacks and the "
"incentive design (§6); **Part 4** — reproducibility (§9) and the appendices (the residual "
"~1 % f-rounding offset, the ±0.9 % per-epoch noise floor, and consensus detail).")
sections["1"] = [
ro_new if l.startswith(ro_old_prefix) else l for l in sections["1"]
]
# assemble each part
for fname, sec_ids in PART_SECTIONS.items():
body: list[str] = list(part_header(fname, units_note))
for sid in sec_ids:
body.extend(sections[sid])
body.append("") # spacer between sections
rendered = render_lines(body, fname)
(HERE / fname).write_text("\n".join(rendered).rstrip() + "\n")
print(f"wrote {fname} ({len(rendered)} lines)")
# index last (overwrites the source-name file)
(HERE / INDEX).write_text(build_index(units_note))
print(f"wrote {INDEX} (index)")
if __name__ == "__main__":
main()