Source: scripts/build_public_docs.py · Plain text

scripts/build_public_docs.py

"""Publish tracked research documents and source text without importing research code.

Run with Python-Markdown 3.9 installed. The output is a static GitHub Pages site.
"""

from __future__ import annotations

import argparse
import html
import json
import posixpath
import subprocess
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import quote, unquote, urlsplit

import markdown

ROOT = Path(__file__).resolve().parents[1]
REPOSITORY = "KennyMcSimpson/measurement-scene-state"
GITHUB = f"https://github.com/{REPOSITORY}"
RAW = f"https://raw.githubusercontent.com/{REPOSITORY}/main"
CORE = (
    "STATUS.md",
    "EXPERIMENTS.md",
    "README.md",
    "ROADMAP.md",
    "docs/research_summary_20261003.md",
    "docs/handoff/REPOSITORY_MAP.md",
    "docs/handoff/SHARED_WORKSPACE.md",
    "docs/handoff/3D_REPRODUCTION.md",
    "docs/provenance/CLAIMS.md",
    "docs/provenance/DATA.md",
    "docs/PROTOCOL.md",
    "docs/DATASETS.md",
)
CSS = """
:root{color-scheme:light;--ink:#182b40;--muted:#576879;--accent:#096b73}
*{box-sizing:border-box}body{margin:0;background:#fafaf7;color:var(--ink);
font:17px/1.75 system-ui,-apple-system,"Segoe UI","Noto Sans SC",sans-serif}
header,main,footer{max-width:1080px;margin:auto;padding:28px 32px}
header{border-bottom:1px solid #dae0e2}nav{display:flex;gap:20px;flex-wrap:wrap}
a{color:var(--accent);text-underline-offset:3px}h1,h2,h3{line-height:1.3}
h1{font-size:2.2rem;letter-spacing:-.025em}h2{margin-top:2em}
.eyebrow,.meta,footer{color:var(--muted)}.eyebrow{font-size:.85rem;letter-spacing:.1em}
.lead{font-size:1.12rem;max-width:820px}.meta{font-size:.88rem;overflow-wrap:anywhere}
.links{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:22px}
.links h2{font-size:1.1rem;margin-bottom:4px}.links p{margin-top:4px}
.links section{min-width:0;overflow-wrap:anywhere}
article{overflow-wrap:anywhere}table{display:block;overflow-x:auto;border-collapse:collapse}
td,th{padding:10px 14px;border:1px solid #d8dfe2;text-align:left}
pre{background:#edf1f2;padding:20px;border-radius:5px;overflow:auto;line-height:1.55}
pre code{white-space:pre;font-size:.86rem}code{font-size:.9em}
img{max-width:100%;height:auto}blockquote{margin-left:0;padding-left:20px;
border-left:3px solid #9eb6bc;color:var(--muted)}li{margin:5px 0}
.directory li{font-size:.92rem}footer{border-top:1px solid #dae0e2;margin-top:45px}
@media(max-width:650px){header,main,footer{padding:22px 18px}h1{font-size:1.75rem}}
"""
ALLOWED_TAGS = set(
    "a p br hr h1 h2 h3 h4 h5 h6 pre code ul ol li blockquote strong em b i "
    "table thead tbody tr td th img div span details summary sup sub del dl dt dd".split()
)
VOID_TAGS = {"br", "hr", "img"}


def git(*args: str) -> str:
    return subprocess.check_output(["git", *args], cwd=ROOT, text=True).strip()


def text_path(path: str) -> str:
    return "text/" + (path[:-3] + ".txt" if path.endswith(".md") else path + ".txt")


class DocumentHTML(HTMLParser):
    """Keep passive Markdown HTML and resolve links against the original source path."""

    def __init__(self, source: str, pages: dict[str, str], directories: set[str]) -> None:
        super().__init__(convert_charrefs=True)
        self.source = source
        self.pages = pages
        self.directories = directories
        self.output: list[str] = []
        self.blocked = 0

    def target(self, value: str, image: bool = False) -> str:
        parsed = urlsplit(value)
        if parsed.netloc and not parsed.scheme:
            return "https:" + value
        if parsed.scheme or parsed.netloc:
            return value if parsed.scheme in {"http", "https", "mailto"} else "#"
        if not parsed.path:
            return value
        source = posixpath.normpath(
            posixpath.join(posixpath.dirname(self.source), unquote(parsed.path))
        )
        if source.startswith("outputs/"):
            archived = "docs/experiments/" + source[len("outputs/") :]
            if archived in self.pages:
                source = archived
        if source in self.pages and not image:
            value = posixpath.relpath(
                self.pages[source], posixpath.dirname(self.pages[self.source])
            )
        else:
            prefix = (
                RAW
                if image
                else f"{GITHUB}/{'tree' if source in self.directories else 'blob'}/main"
            )
            value = prefix + "/" + quote(source, safe="/")
        if parsed.query:
            value += "?" + parsed.query
        if parsed.fragment:
            value += "#" + parsed.fragment
        return value

    def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
        if tag in {"script", "style", "iframe"}:
            self.blocked += 1
        if self.blocked or tag not in ALLOWED_TAGS:
            return
        safe = []
        for key, value in attrs:
            if value is None or key not in {"href", "src", "alt", "title", "id", "class", "open"}:
                continue
            if key in {"href", "src"}:
                value = self.target(value, image=key == "src")
            safe.append(f' {key}="{html.escape(value, quote=True)}"')
        self.output.append(f"<{tag}{''.join(safe)}>")

    def handle_endtag(self, tag: str) -> None:
        if tag in {"script", "style", "iframe"} and self.blocked:
            self.blocked -= 1
            return
        if not self.blocked and tag in ALLOWED_TAGS and tag not in VOID_TAGS:
            self.output.append(f"</{tag}>")

    def handle_startendtag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
        self.handle_starttag(tag, attrs)
        if tag not in VOID_TAGS:
            self.handle_endtag(tag)

    def handle_data(self, data: str) -> None:
        if not self.blocked:
            self.output.append(html.escape(data))


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--output", type=Path, default=ROOT / "_site")
    parser.add_argument(
        "--base-url", default="https://kennymcsimpson.github.io/measurement-scene-state/"
    )
    args = parser.parse_args()
    base = args.base_url.rstrip("/") + "/"
    if urlsplit(base).scheme != "https":
        parser.error("--base-url must be an HTTPS URL")
    output = args.output.resolve()
    if output == ROOT or output in ROOT.parents:
        parser.error("--output must not overwrite the repository or its parents")
    output.mkdir(parents=True, exist_ok=True)
    tracked = git("ls-files", "-z").split("\0")
    documents = sorted(
        path
        for path in tracked
        if path.endswith(".md")
        and ("/" not in path or path.startswith(("docs/", "research/ideas/")))
    )
    sources = sorted(
        path
        for path in tracked
        if (
            path.startswith(("src/", "scripts/", "configs/", "docs/", "tests/"))
            and path.endswith((".py", ".yaml", ".yml", ".toml"))
        )
        or path == "pyproject.toml"
    )
    pages = {path: "pages/" + path[:-3] + ".html" for path in documents}
    pages.update({path: "code/" + path + ".html" for path in sources})
    directories = {str(parent) for path in tracked for parent in Path(path).parents}
    commit = git("rev-parse", "HEAD")
    updated = git("show", "-s", "--format=%cI", "HEAD")

    def write(path: str, content: str) -> None:
        target = output / path
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(content, encoding="utf-8", newline="")

    def shell(title: str, body: str, path: str, description: str) -> str:
        return f'''<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{html.escape(title)} | measurement-scene-state</title>
<meta name="description" content="{html.escape(description, quote=True)}">
<meta name="robots" content="index,follow">
<link rel="canonical" href="{base}{quote(path, safe="/")}">
<link rel="stylesheet" href="{base}style.css?v={commit[:12]}">
<link rel="alternate" type="text/plain" href="{base}llms.txt" title="Text index">
</head><body><header><nav>
<a href="{base}">Measurement Scene State</a>
<a href="{base}pages/STATUS.html">Status</a>
<a href="{base}pages/EXPERIMENTS.html">Experiments</a>
<a href="{base}sources.html">Source index</a>
<a href="{GITHUB}">GitHub</a></nav></header><main>{body}</main>
<footer>Public research archive · <a href="{GITHUB}/commit/{commit}">{commit[:12]}</a>
 · <time datetime="{updated}">{updated[:10]}</time>
 · <a href="{base}llms.txt">Text index</a> · <a href="{base}sitemap.xml">Sitemap</a>
</footer></body></html>'''

    entries = []
    for path, page in pages.items():
        text = (ROOT / path).read_bytes().decode("utf-8")
        raw = text_path(path)
        write(raw, text)
        if path in documents:
            rendered = markdown.markdown(text, extensions=["tables", "fenced_code", "toc"])
            resolver = DocumentHTML(path, pages, directories)
            resolver.feed(rendered)
            content = "".join(resolver.output)
        else:
            content = f"<h1>{html.escape(path)}</h1><pre><code>{html.escape(text)}</code></pre>"
        meta = (
            f'<p class="meta">Source: <a href="{GITHUB}/blob/main/{quote(path, safe="/")}">'
            f'{html.escape(path)}</a> · <a href="{base}{quote(raw, safe="/")}">Plain text</a></p>'
        )
        write(page, shell(path, meta + "<article>" + content + "</article>", page, path))
        entries.append(
            {"path": path, "html": page, "text": raw, "raw_url": RAW + "/" + quote(path, safe="/")}
        )

    cards = "".join(
        f'<section><h2><a href="{base}{pages[path]}">{html.escape(path)}</a></h2>'
        f'<p><a href="{base}{text_path(path)}">Plain text / 原文</a></p></section>'
        for path in CORE
        if path in pages
    )
    landing = f'''
<p class="eyebrow">PUBLIC RESEARCH ARCHIVE · KennyMcSimpson</p>
<h1>Measurement Scene State</h1>
<p class="lead">Inverse-JEPA、Dynamic-TTT 与场景状态的研究档案。
本页提供原始文档、实验记录和源代码的公开阅读入口。</p>
<p>Repository: <a href="{GITHUB}">KennyMcSimpson/measurement-scene-state</a></p>
<p>阅读研究结论时,先查看 STATUS.md 和 EXPERIMENTS.md 的证据等级、数据划分与限制。
这些页面同步仓库原文,保留历史记录;具体结论以对应实验协议和报告为准。</p>
<h2>直接读取 / Direct reading</h2>
<p><a href="{base}llms-full.txt">核心文档合并原文 / Core documents in one text file</a> ·
<a href="{base}llms.txt">研究文档与源码的文本索引</a></p>
<p>可把上述链接直接交给具备网页读取功能的 AI。
直接读取公开 URL 不依赖关键词搜索是否已有结果。</p>
<h2>关键文档 / Key documents</h2><div class="links">{cards}</div>
<h2>完整导航 / Browse</h2><p><a href="{base}documents.html">研究文档目录</a> ·
<a href="{base}sources.html">源代码与配置</a></p>'''
    write(
        "index.html",
        shell(
            "Measurement Scene State",
            landing,
            "",
            "KennyMcSimpson/measurement-scene-state: Inverse-JEPA and Dynamic-TTT research "
            "archive, experimental status, protocols, reproduction notes, and source code.",
        ),
    )
    for filename, title, items in (
        ("documents.html", "Research documents / 研究文档", documents),
        ("sources.html", "Source code and configurations / 源码与配置", sources),
    ):
        links = "".join(
            f'<li><a href="{base}{quote(pages[path], safe="/")}">{html.escape(path)}</a>'
            f' · <a href="{base}{quote(text_path(path), safe="/")}">text</a></li>'
            for path in items
        )
        write(
            filename,
            shell(title, f'<h1>{title}</h1><ul class="directory">{links}</ul>', filename, title),
        )
    full = [f"# {REPOSITORY}\n\nRepository: {GITHUB}\nSource commit: {commit}\n"]
    for path in CORE:
        full.append(f"\n\n---\n\nSource file: {path}\nRaw URL: {RAW}/{path}\n\n")
        full.append((ROOT / path).read_bytes().decode("utf-8"))
    write("llms-full.txt", "".join(full))
    index = [
        f"# Measurement Scene State\n\n> Public research archive: {REPOSITORY}.\n",
        f"\nRepository: {GITHUB}\nSource commit: {commit}\n",
        f"\n## Core documents\n\n- [Combined core text]({base}llms-full.txt)\n",
    ]
    for path in CORE:
        index.append(f"- [{path}]({base}{text_path(path)})\n")
    index.append("\n## Published document and source text\n\n")
    for entry in entries:
        index.append(f"- [{entry['path']}]({base}{quote(entry['text'], safe='/')})\n")
    write("llms.txt", "".join(index))
    write(
        "file-index.json",
        json.dumps(
            {"repository": REPOSITORY, "commit": commit, "files": entries},
            ensure_ascii=False,
            indent=2,
        )
        + "\n",
    )
    urls = ["", "documents.html", "sources.html", *pages.values()]
    locations = "".join(
        f"<url><loc>{html.escape(base + quote(path, safe='/'))}</loc>"
        f"<lastmod>{updated[:10]}</lastmod></url>"
        for path in urls
    )
    write(
        "sitemap.xml",
        '<?xml version="1.0" encoding="UTF-8"?>\n'
        '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' + locations + "</urlset>\n",
    )
    write("style.css", CSS)
    write(".nojekyll", "")
    print(f"Built {len(documents)} documents and {len(sources)} source files in {output}")


if __name__ == "__main__":
    main()