NumericalOS

site/build.py

back to source

"""Build the numericalos.pages.dev static site.

Produces, into public/:
  - the content pages, wrapped in a common shell
  - numericalos.git/   a real dumb-HTTP git remote (git clone works)
  - browse/            generated file tree, blobs, and commit log
  - skills/            the agentic build skills, served as plain text
  - index.json         machine-readable skill index
  - llms.txt           agent discovery entry point
  - AGENTS.md          the multi-agent protocol
  - skills.zip         the whole skill bundle

Python 3 stdlib only. Run from the repo root:  py site/build.py
"""

import html
import io
import json
import os
import shutil
import subprocess
import zipfile

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SITE = os.path.join(ROOT, "site")
# Overridable: on Windows another process can hold a handle on the git pack
# files under the output tree, which makes the pre-build clean fail. Building
# to a fresh directory sidesteps that without killing anyone else's process.
OUT = os.environ.get("NUMOS_OUT") or os.path.join(ROOT, "public")

SITE_URL = "https://numericalos.com"

NAV = [
    ("/", "home"),
    ("/docs/", "docs"),
    ("/whitepaper/", "whitepaper"),
    ("/skills/", "skills"),
    ("/browse/", "source"),
]


def _force_remove(func, path, _exc):
    """rmtree onerror hook: git packs are read-only, and Windows honors that."""
    os.chmod(path, 0o700)
    func(path)


def rmtree(path):
    if os.path.isdir(path):
        shutil.rmtree(path, onerror=_force_remove)


def git(*args):
    """Run a git command in the repo and return stripped stdout."""
    return subprocess.run(
        ["git", "-C", ROOT] + list(args),
        capture_output=True, text=True, check=True,
    ).stdout.strip()


def write(relpath, text):
    path = os.path.join(OUT, relpath)
    parent = os.path.dirname(path)
    if parent and not os.path.isdir(parent):
        os.makedirs(parent)
    with open(path, "w", encoding="utf-8", newline="\n") as handle:
        handle.write(text)


def shell(title, body, active="", description=""):
    """Wrap a body fragment in the common page shell."""
    nav = "".join(
        '<a href="%s"%s>%s</a>' % (href, ' class="on"' if label == active else "", label)
        for href, label in NAV
    )
    return PAGE % {
        "title": html.escape(title),
        "description": html.escape(description or title),
        "nav": nav,
        "body": body,
    }


PAGE = """<!doctype html>
<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>%(title)s</title>
<meta name="description" content="%(description)s">
<link rel="stylesheet" href="/assets/site.css">
</head><body>
<header><a class="brand" href="/">NumericalOS</a><nav>%(nav)s</nav></header>
<main>%(body)s</main>
<footer>
<p>NumericalOS &mdash; a Linux userspace whose init is a graph.
<strong>Status: bootstrap logic tested; boot unverified.</strong></p>
<p><a href="/numericalos.git">git</a> &middot;
<a href="/llms.txt">llms.txt</a> &middot;
<a href="/AGENTS.md">AGENTS.md</a> &middot;
<a href="/index.json">index.json</a> &middot;
<a href="/skills.zip">skills.zip</a></p>
</footer>
</body></html>
"""


# ---------------------------------------------------------------- skills

def parse_frontmatter(text):
    """Return (meta, body) for a SKILL.md with YAML-ish frontmatter."""
    if not text.startswith("---"):
        return {}, text
    end = text.find("\n---", 3)
    if end == -1:
        return {}, text
    raw = text[3:end].strip()
    body = text[end + 4:].lstrip("\n")
    meta = {}
    key = None
    for line in raw.split("\n"):
        if line and not line[0].isspace() and ":" in line:
            key, _, value = line.partition(":")
            key = key.strip()
            meta[key] = value.strip()
        elif key and line.strip():
            meta[key] = (meta[key] + " " + line.strip()).strip()
    return meta, body


def collect_skills():
    """Read every skills/*/SKILL.md into a sorted list of dicts."""
    skills = []
    base = os.path.join(ROOT, "skills")
    for name in sorted(os.listdir(base)):
        path = os.path.join(base, name, "SKILL.md")
        if not os.path.isfile(path):
            continue
        with open(path, encoding="utf-8") as handle:
            text = handle.read()
        meta, _ = parse_frontmatter(text)
        skills.append({
            "name": meta.get("name", name),
            "description": meta.get("description", ""),
            "path": "skills/%s/SKILL.md" % name,
            "url": "%s/skills/%s/SKILL.md" % (SITE_URL, name),
            "bytes": len(text.encode("utf-8")),
            "text": text,
        })
    return skills


def build_skills(skills):
    """Serve each SKILL.md verbatim, plus an index page."""
    for skill in skills:
        rel = skill["path"].replace("skills/", "skills/", 1)
        write(rel, skill["text"])

    rows = "".join(
        '<tr><td><a href="/%s">%s</a></td><td>%s</td></tr>'
        % (skill["path"], html.escape(skill["name"]),
           html.escape(skill["description"]))
        for skill in skills
    )
    body = """
<h1>Agentic build skills</h1>
<p class="lead">These skills support <em>you</em> building NumericalOS targets on
your own machine. Nothing here generates images and nothing here hosts them.</p>
<p>Point your agent at <a href="/AGENTS.md">AGENTS.md</a> for the protocol, or
take the whole bundle: <a href="/skills.zip">skills.zip</a>.</p>
<table class="skills"><thead><tr><th>skill</th><th>when it fires</th></tr></thead>
<tbody>%s</tbody></table>
<h2>The honesty contract</h2>
<p>Only <code>numericalos-verify-boot</code> may claim a boot, and only for the
artifact and architecture it observed. Build skills produce artifacts and say
exactly that. This project has never been booted by its authors &mdash; if you
build a target, you may be producing its first real boot.</p>
""" % rows
    write("skills/index.html", shell("Skills - NumericalOS", body, "skills",
                                    "Agentic skills for building NumericalOS targets locally."))


def build_bundle(skills):
    """skills.zip - the whole bundle including AGENTS.md."""
    buffer = io.BytesIO()
    with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
        for skill in skills:
            archive.writestr(skill["path"], skill["text"])
        with open(os.path.join(ROOT, "skills", "AGENTS.md"), encoding="utf-8") as handle:
            archive.writestr("skills/AGENTS.md", handle.read())
    with open(os.path.join(OUT, "skills.zip"), "wb") as handle:
        handle.write(buffer.getvalue())


def build_discovery(skills, head):
    """index.json + llms.txt + AGENTS.md at the root."""
    index = {
        "name": "numericalos",
        "description": "A Linux userspace whose init is a graph export.",
        "site": SITE_URL,
        "status": "bootstrap logic tested; boot unverified",
        "commit": head,
        "clone": "%s/numericalos.git" % SITE_URL,
        "agents": "%s/AGENTS.md" % SITE_URL,
        "bundle": "%s/skills.zip" % SITE_URL,
        "skills": [
            {k: s[k] for k in ("name", "description", "path", "url", "bytes")}
            for s in skills
        ],
    }
    write("index.json", json.dumps(index, indent=2, sort_keys=True) + "\n")

    lines = [
        "# NumericalOS",
        "",
        "> A Linux userspace whose init is a graph export. No unit files: every",
        "> supervisable thing, boot ordering constraint, health rule, and",
        "> remediation path is a node in an M/G/S/MGS graph quartet.",
        "",
        "Status: bootstrap logic tested; boot unverified. The authors have never",
        "booted it, on hardware or in emulation. See /docs/status.",
        "",
        "## Build it yourself",
        "",
        "These skills drive YOUR agent to build targets on YOUR machine. This site",
        "does not generate images and does not host them.",
        "",
    ]
    for skill in skills:
        lines.append("- [%s](%s): %s" % (skill["name"], skill["url"], skill["description"]))
    lines += [
        "",
        "## Protocol",
        "",
        "- [AGENTS.md](%s/AGENTS.md): multi-agent build protocol and honesty contract" % SITE_URL,
        "- [index.json](%s/index.json): machine-readable index" % SITE_URL,
        "- [skills.zip](%s/skills.zip): the whole bundle" % SITE_URL,
        "",
        "## Source",
        "",
        "    git clone %s/numericalos.git" % SITE_URL,
        "",
        "Served as static files over dumb-HTTP. No forge, no account, no",
        "server-side code. Verify what you got before you run it.",
        "",
    ]
    write("llms.txt", "\n".join(lines))

    shutil.copyfile(os.path.join(ROOT, "skills", "AGENTS.md"),
                    os.path.join(OUT, "AGENTS.md"))


# ---------------------------------------------------------------- git

def build_git_remote():
    """A real dumb-HTTP git remote: git clone <site>/numericalos.git works.

    Packed into a single packfile so the object count stays small, then
    update-server-info writes the indexes the dumb protocol needs.
    """
    dest = os.path.join(OUT, "numericalos.git")
    rmtree(dest)
    subprocess.run(["git", "clone", "--bare", "--quiet", ROOT, dest], check=True)

    # Explode the pack into loose objects.
    #
    # The dumb protocol probes objects/<sha> directly. With everything packed
    # that probe 404s, and git inflates the 404 body before checking status --
    # printing "inflate: data stream error ... corrupt" and only THEN falling
    # back to the pack. The clone succeeds, but a documented clone command
    # that prints "corrupt" is not shippable. Loose objects make the probe hit.
    packdir = os.path.join(dest, "objects", "pack")
    packs = [p for p in os.listdir(packdir) if p.endswith(".pack")]
    for pack in packs:
        with open(os.path.join(packdir, pack), "rb") as handle:
            subprocess.run(["git", "-C", dest, "unpack-objects", "-q"],
                           stdin=handle, check=True)
    for name in os.listdir(packdir):
        os.chmod(os.path.join(packdir, name), 0o700)
        os.unlink(os.path.join(packdir, name))

    subprocess.run(["git", "-C", dest, "update-server-info"], check=True)

    # A bare clone carries hooks samples and a local-path origin; neither
    # belongs on a public remote.
    rmtree(os.path.join(dest, "hooks"))
    subprocess.run(["git", "-C", dest, "remote", "remove", "origin"],
                   capture_output=True)
    return dest


# ---------------------------------------------------------------- browse

TEXT_EXT = {".py", ".sh", ".md", ".json", ".txt", ".toml", ".cfg", ".yml", ".yaml"}


def build_browse(head):
    """Generated source browser: file tree, blobs, and commit log."""
    listing = git("ls-tree", "-r", "--name-only", "HEAD").split("\n")
    listing = [p for p in listing if p]

    rows = []
    for path in listing:
        size = git("cat-file", "-s", "HEAD:%s" % path)
        rows.append(
            '<tr><td><a href="/browse/%s.html">%s</a></td><td class="num">%s</td></tr>'
            % (html.escape(path), html.escape(path), size)
        )
        _build_blob(path)

    log = git("log", "--pretty=format:%h\x1f%an\x1f%ad\x1f%s", "--date=short").split("\n")
    entries = []
    for line in log:
        parts = line.split("\x1f")
        if len(parts) != 4:
            continue
        sha, _author, date, subject = parts
        entries.append(
            '<tr><td class="mono">%s</td><td class="num">%s</td><td>%s</td></tr>'
            % (html.escape(sha), html.escape(date), html.escape(subject))
        )

    body = """
<h1>Source</h1>
<p class="lead">Generated from commit <code>%s</code>. This is a rendering; the
repository itself is the authority:</p>
<pre class="cmd">git clone %s/numericalos.git</pre>
<h2>Files (%d)</h2>
<table class="files"><thead><tr><th>path</th><th class="num">bytes</th></tr></thead>
<tbody>%s</tbody></table>
<h2>History (%d commits)</h2>
<table class="files"><thead><tr><th>commit</th><th class="num">date</th><th>subject</th></tr></thead>
<tbody>%s</tbody></table>
""" % (head, SITE_URL, len(listing), "".join(rows), len(entries), "".join(entries))
    write("browse/index.html", shell("Source - NumericalOS", body, "source",
                                     "Browse the NumericalOS source and history."))


def _build_blob(path):
    ext = os.path.splitext(path)[1]
    if ext not in TEXT_EXT:
        return
    try:
        content = git("show", "HEAD:%s" % path)
    except subprocess.CalledProcessError:
        return
    body = """
<h1 class="mono">%s</h1>
<p><a href="/browse/">back to source</a></p>
<pre class="code">%s</pre>
""" % (html.escape(path), html.escape(content))
    write("browse/%s.html" % path, shell(path + " - NumericalOS", body, "source"))


# ---------------------------------------------------------------- content

def build_content(head):
    """Wrap the hand-written content fragments in the site shell."""
    content_dir = os.path.join(SITE, "content")
    for name in sorted(os.listdir(content_dir)):
        if not name.endswith(".html"):
            continue
        with open(os.path.join(content_dir, name), encoding="utf-8") as handle:
            raw = handle.read()
        title, _, fragment = raw.partition("\n")
        title = title.replace("<!--", "").replace("-->", "").strip()
        fragment = fragment.replace("{{COMMIT}}", head)

        stem = name[:-5]
        if stem == "index":
            rel, active = "index.html", "home"
        elif stem == "whitepaper":
            rel, active = "whitepaper/index.html", "whitepaper"
        else:
            rel, active = "docs/%s/index.html" % stem, "docs"
            if stem == "docs":
                rel = "docs/index.html"
        write(rel, shell(title, fragment, active))


def build_404():
    """A real 404 page - load-bearing, not decoration.

    Without 404.html, Cloudflare Pages answers unmatched paths with the root
    index.html and status 200. Git's dumb-HTTP protocol probes for loose
    objects that legitimately do not exist (everything is packed); receiving
    HTML with a 200 instead of a 404, it reports 'inflate: data stream error'
    and the clone breaks. A real 404 lets git fall through to the packfile.
    """
    body = """
<h1>404</h1>
<p class="lead">No such page.</p>
<p><a href="/">home</a> &middot; <a href="/docs/">docs</a> &middot;
<a href="/skills/">skills</a> &middot; <a href="/browse/">source</a></p>
"""
    write("404.html", shell("404 - NumericalOS", body))


def build_headers():
    write("_headers", """/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin

/numericalos.git/*
  Content-Type: application/octet-stream
  Cache-Control: no-cache

/skills/*
  Content-Type: text/markdown; charset=utf-8

/llms.txt
  Content-Type: text/plain; charset=utf-8

/AGENTS.md
  Content-Type: text/markdown; charset=utf-8
""")
    write("robots.txt", "User-agent: *\nAllow: /\nSitemap: %s/sitemap.xml\n" % SITE_URL)


def main():
    rmtree(OUT)
    os.makedirs(OUT)

    head = git("rev-parse", "--short", "HEAD")
    skills = collect_skills()

    shutil.copytree(os.path.join(SITE, "assets"), os.path.join(OUT, "assets"))
    build_content(head)
    build_skills(skills)
    build_bundle(skills)
    build_discovery(skills, head)
    build_browse(head)
    build_git_remote()
    build_404()
    build_headers()

    count = sum(len(files) for _, _, files in os.walk(OUT))
    print("built %s at commit %s: %d files, %d skills" % (OUT, head, count, len(skills)))


if __name__ == "__main__":
    main()