Files
zeekay c29a2c4a20 A retired number is still an address
Deleting a proposal that is wrong or superseded is right — a document nobody
should follow does not earn a place in the index by being renamed Superseded.
But deletion frees the NUMBER, and the number is the part that outlives the
document: it is cited in code, linked from outside, quoted in commits.

Sixteen numbers were freed here today, and one was spent again within hours.
HIP-0138 was published as "Cloud — Unified Hanzo Binary", deleted, then
reissued for "One Manifest, Five Kinds" — so every existing reference to
HIP-0138 resolved to a specification it never made. The generator could not
see it: it refuses two files claiming one number, but a number claimed once
and then again looks identical to a number claimed once.

spent.json records what each freed number was. validate() refuses a file that
claims one under a different title, and says what the number already means.
The document still goes; only the address is kept, at one line each.

"One Manifest, Five Kinds" moves to HIP-0142, the first genuinely unused
number, with its own H1 corrected — a heading that disagrees with its
front matter is how a stale number survives a renumber.

docs/SCALE_STANDARD.md cited the deleted file for the per-replica capacity
budget. That text lives in HIP-0122, which the lint could not know because the
citation is a backticked path rather than a link. Repointed to the HIP that
actually carries it, without the section number, which did not survive.
2026-08-28 18:49:06 -07:00

422 lines
17 KiB
Python

#!/usr/bin/env python3
"""Project HIPs/ onto README.md. The files are the source; README is derived.
python3 scripts/index.py # write the projection
python3 scripts/index.py --check # fail if README has drifted (CI)
python3 scripts/index.py --delete N # write a projection that is N rows smaller
python3 scripts/index.py --root DIR # project a scratch copy (the self-test)
TWO PROJECTIONS OF ONE CORPUS, neither hand-maintained.
The index is every HIP by number, which is what you want when you already know
the number. The reading order is what you want when you do not: 259 specs in
numeric order teach nothing, because number records when a spec was written and
not what has to be understood first.
The order is DERIVED, never curated. A HIP's `requires:` names what a reader must
already hold, so counting how many HIPs require a given one measures how
foundational the corpus itself treats it. Capability HIPs then group by the
domain `capabilities.yaml` files them under, which is the grouping the doc site
uses. A HIP that becomes foundational rises without anyone noticing it should.
WHY THIS FILE REFUSES MORE OFTEN THAN IT WRITES
A generator that can quietly emit a short table is worse than no generator, because
the short table looks authoritative. The predecessor could emit one four ways: an
unreadable HIPs/ produced an empty table, a `capabilities.yaml` that failed to load
dropped an entire section through a bare `except: return []`, a `replace_section`
that matched nothing appended a second copy, and a crash between the two writes
left README half-projected. So:
* NOTHING IS WRITTEN UNTIL EVERYTHING VALIDATES. Both sections are built and
checked in memory; the file is written once, at the end, or not at all.
* ROWS == FILES, exactly. Not "about right" -- every file on disk appears in
the table exactly once and the table names no file that is not on disk.
* A SHRINKING TABLE MUST SAY SO. Fewer rows than the README being replaced is
refused unless --delete names exactly how many went. This is the check that
catches an empty corpus, which rows==files cannot: zero files project to zero
rows and agree with themselves perfectly.
* A SECTION NEVER VANISHES QUIETLY. If the README being replaced has a section
this run cannot rebuild -- the usual cause is capabilities.yaml not being
checked out beside this repo -- that is an error, not a silent omission.
* LINKS RESOLVE. Every `requires:` and every relative link into HIPs/ must name
a file that exists, or nothing is written.
`--check` regenerates in memory and compares. It writes nothing and exits 1 on any
difference, so CI can hold README to the files without a second implementation of
what the projection is.
"""
from __future__ import annotations
import json
import os
import re
import sys
from pathlib import Path
try:
import yaml
except ImportError:
yaml = None
# --root lets the self-test point this at a mutated copy of the corpus. A guard
# nobody can aim at a broken tree is a guard nobody can prove.
_argv = sys.argv[1:]
ROOT = Path(_argv[_argv.index("--root") + 1] if "--root" in _argv
else Path(__file__).resolve().parent.parent).resolve()
HIP_DIR = ROOT / "HIPs"
README = ROOT / "README.md"
VOCABULARY = ROOT / "vocabulary.json"
SPENT = ROOT / "spent.json"
# The domain grouping is hanzoai/openapi's editorial decision and this script
# reads it rather than restating it -- a second copy of a taxonomy is a second
# answer the day one of them moves. HANZO_CAPABILITIES overrides the path, which
# is how CI points at its own checkout; scripts/coverage.py reads the same var.
CAPABILITIES = Path(
os.environ.get("HANZO_CAPABILITIES", ROOT.parent / "openapi" / "capabilities.yaml")
)
# How many HIPs must require one for it to lead the reading order. Set from the
# measured distribution, which has a wide gap in it: 104, 102, 102, then 23. Any
# threshold between 24 and 102 selects the same three, so this is not a knob
# anybody has to tune.
FOUNDATION = 40
INDEX_HEADING = "## HIP Index"
ORDER_HEADING = "## Reading order"
REQUIRED = ("hip", "title", "author", "type", "status", "created")
TITLE_WIDTH = 55
class Refused(Exception):
"""The projection is not safe to write. Nothing has been written."""
# --------------------------------------------------------------------------- read
def vocabulary() -> dict:
return json.loads(VOCABULARY.read_text(encoding="utf-8"))
def spent() -> dict[int, str]:
"""Numbers that were published and then retired, and what each one was.
Deleting a proposal is right -- a document nobody should follow does not
earn a place in the index by being renamed Superseded. But deletion frees
the NUMBER, and a number is the one part of a proposal that outlives it:
it is cited in code, linked from outside, and quoted in commits. HIP-0138
was deleted and reissued as a different specification within hours, so
every existing reference to it now resolves to something it never said.
The document goes; the address is kept, here, where it costs one line.
"""
if not SPENT.exists():
return {}
return {int(k): v for k, v in
json.loads(SPENT.read_text(encoding="utf-8")).items()}
def front_matter(text: str) -> tuple[dict[str, str], str]:
if not text.startswith("---"):
return {}, text
end = text.find("\n---", 3)
if end == -1:
return {}, text
out: dict[str, str] = {}
for line in text[3:end].strip().splitlines():
m = re.match(r"^([A-Za-z_-]+):(.*)$", line)
if m:
out[m.group(1).strip()] = m.group(2).strip().strip("\"'")
return out, text[end + 4:]
def corpus() -> list[dict]:
"""Every HIP, with its front matter, body and number."""
if not HIP_DIR.is_dir():
raise Refused(f"{HIP_DIR} is not a directory")
hips = []
for path in sorted(HIP_DIR.glob("hip-*.md")):
m = re.match(r"hip-(\d{4})", path.name)
if not m:
raise Refused(f"{path.name} is not hip-NNNN-*.md")
fm, body = front_matter(path.read_text(encoding="utf-8"))
if not fm:
raise Refused(f"{path.name} has no front matter")
hips.append({**fm, "number": int(m.group(1)), "file": path.name, "body": body})
return sorted(hips, key=lambda h: h["number"])
def numbers_in(value: str) -> list[int]:
"""Every HIP number a `requires:` value names.
The field is written several ways across the corpus -- 'HIP-0026', '[26, 27]',
'HIP-0005 (Post-Quantum Security)', bare '26, 27', and 'LP-0010' for a Lux
proposal. Only Hanzo numbers resolve here, so LP-* is dropped rather than
reported as dangling.
"""
return [int(n) for n in re.findall(r"\d+", re.sub(r"\bLP-\d+", "", value or ""))]
# ------------------------------------------------------------------------ refuse
def validate(hips: list[dict]) -> None:
"""Every reason to write nothing at all."""
if not hips:
raise Refused("HIPs/ holds no proposals")
vocab = vocabulary()
known = {h["number"] for h in hips}
if len(known) != len(hips):
seen: dict[int, str] = {}
for h in hips:
if h["number"] in seen:
raise Refused(
f"HIP-{h['number']:04d} is claimed by both "
f"{seen[h['number']]} and {h['file']}"
)
seen[h["number"]] = h["file"]
for number, was in spent().items():
claim = next((h for h in hips if h["number"] == number), None)
if claim and claim["title"] != was:
raise Refused(
f"HIP-{number:04d} was published as {was!r} and retired. "
f"{claim['file']} claims it for {claim['title']!r}, so every "
f"reference to HIP-{number:04d} now resolves to a different "
f"specification. Give it an unused number."
)
files = {h["file"] for h in hips}
for h in hips:
where = h["file"]
for field in REQUIRED:
if not h.get(field):
raise Refused(f"{where} has no {field}:")
if h["status"] not in vocab["status"]:
raise Refused(
f"{where} has status {h['status']!r}; the vocabulary is "
f"{sorted(vocab['status'])}"
)
if h["type"] not in vocab["type"]:
raise Refused(
f"{where} has type {h['type']!r}; the vocabulary is "
f"{sorted(vocab['type'])}"
)
category = h.get("category", "")
if category and category not in vocab["category"]:
raise Refused(f"{where} has category {category!r}, which is not in the vocabulary")
if h["type"] == "Standards Track" and not category:
raise Refused(f"{where} is Standards Track and has no category:")
for target in numbers_in(h.get("requires", "")):
if target not in known:
raise Refused(f"{where} requires HIP-{target:04d}, which does not exist")
# A relative link into the corpus that names a deleted file renders as a
# 404 on hips.hanzo.ai. This is the check that makes deleting a HIP safe:
# the deletion is refused until every document that pointed at it is
# repointed at whatever replaced it.
for link in re.findall(r"\]\(\.{1,2}/(?:HIPs/)?(hip-[0-9a-z-]+\.md)[^)]*\)", h["body"]):
if link not in files:
raise Refused(f"{where} links {link}, which does not exist")
# ------------------------------------------------------------------------- write
def index(hips: list[dict]) -> str:
lines = [
INDEX_HEADING + "\n",
"| Number | Title | Type | Category | Status |",
"|:-------|:------|:-----|:---------|:-------|",
]
for h in hips:
title = h["title"]
if len(title) > TITLE_WIDTH:
title = title[: TITLE_WIDTH - 3] + "..."
lines.append(
f"| [HIP-{h['number']:04d}](./HIPs/{h['file']}) | {title} | "
f"{h['type']} | {h.get('category') or '-'} | {h['status']} |"
)
return "\n".join(lines)
def required_by(hips: list[dict]) -> dict[int, int]:
counts: dict[int, int] = {}
for h in hips:
for n in numbers_in(h.get("requires", "")):
counts[n] = counts.get(n, 0) + 1
return counts
def domains() -> list:
"""The domain grouping, from capabilities.yaml. Raises rather than degrading."""
if yaml is None:
raise Refused("PyYAML is not installed, so the capability grouping cannot be read")
if not CAPABILITIES.exists():
raise Refused(
f"{CAPABILITIES} is not there. It lives in hanzoai/openapi, which this "
"repo expects beside it; set HANZO_CAPABILITIES to point elsewhere."
)
loaded = yaml.safe_load(CAPABILITIES.read_text(encoding="utf-8"))
got = (loaded or {}).get("domains") or []
if not got:
raise Refused(f"{CAPABILITIES} declares no domains")
return got
def order(hips: list[dict], want_capabilities: bool) -> str:
counts = required_by(hips)
by_number = {h["number"]: h for h in hips}
link = lambda h: f"[HIP-{h['number']:04d}](./HIPs/{h['file']})"
lines = [
ORDER_HEADING + "\n",
"Every HIP by number is indexed below. This is the order it is learnable in, "
"derived from the corpus rather than curated: a HIP's `requires:` names what a "
"reader must already hold, so what the most HIPs require is what to read first. "
"Regenerated by `scripts/index.py`.\n",
"### Start here\n",
"| | Required by | |",
"|:--|--:|:--|",
]
if 0 in by_number:
lines.append(f"| {link(by_number[0])} | — | {by_number[0]['title']} — the map |")
for n in sorted((n for n, c in counts.items() if c >= FOUNDATION and n in by_number),
key=lambda n: -counts[n]):
lines.append(f"| {link(by_number[n])} | {counts[n]} | {by_number[n]['title']} |")
lines.append("")
# The next tier: required by enough HIPs to be worth holding before the
# capabilities, but not by nearly all of them.
second = sorted((n for n, c in counts.items() if 5 <= c < FOUNDATION and n in by_number),
key=lambda n: -counts[n])
if second:
lines += ["### Then the invariants\n", "| | Required by | |", "|:--|--:|:--|"]
for n in second:
lines.append(f"| {link(by_number[n])} | {counts[n]} | {by_number[n]['title']} |")
lines.append("")
named: dict[str, dict] = {}
for h in hips:
cap = (h.get("capability") or "").strip().strip("[]")
if cap and "," not in cap:
named.setdefault(cap, h)
if named and want_capabilities:
lines += [
"### The capabilities\n",
f"One capability, one HIP (HIP-0139). {len(named)} of them, grouped as "
"`capabilities.yaml` groups them.\n",
]
filed = set()
for d in domains():
rows = [named[t] for t in (d.get("tags") or []) if t in named]
if not rows:
continue
filed.update(r["number"] for r in rows)
lines.append(f"**{d.get('title', d.get('id', ''))}** — {d.get('role', '')}\n")
lines.append(" · ".join(
f"{link(h)} `{h['capability'].strip()}`"
for h in sorted(rows, key=lambda x: x["number"])))
lines.append("")
rest = [h for h in named.values() if h["number"] not in filed]
if rest:
lines.append("**Not yet grouped**\n")
lines.append(" · ".join(
f"{link(h)} `{h['capability'].strip()}`"
for h in sorted(rest, key=lambda x: x["number"])))
lines.append("")
return "\n".join(lines)
def swap(text: str, heading: str, new: str) -> str:
"""Replace one `## Section` with new text. The section must be there."""
start = text.find(heading)
if start == -1:
raise Refused(f"README has no {heading!r} section to replace")
after = text[start + len(heading):]
nxt = re.search(r"\n## [^#]", after)
if not nxt:
return text[:start] + new
return text[:start] + new + "\n\n" + after[nxt.start() + 1:]
def rows_in(text: str) -> list[str]:
"""The index rows of a README, as filenames."""
start = text.find(INDEX_HEADING)
if start == -1:
return []
after = text[start + len(INDEX_HEADING):]
nxt = re.search(r"\n## [^#]", after)
return re.findall(r"^\|\s*\[HIP-\d{4}\]\(\./HIPs/([^)]+)\)",
after[: nxt.start()] if nxt else after, re.M)
def project(hips: list[dict], before: str, deleting: int) -> str:
"""The whole README, or an exception. Writes nothing."""
validate(hips)
was = rows_in(before)
# capabilities.yaml lives in a sibling repo. It is optional ONLY when the
# README being replaced does not already carry the section it feeds -- losing
# a section because an input was not checked out is the silent truncation
# this file exists to prevent.
want_capabilities = True
if "### The capabilities" not in before and not CAPABILITIES.exists():
want_capabilities = False
text = swap(before, ORDER_HEADING, order(hips, want_capabilities))
text = swap(text, INDEX_HEADING, index(hips))
now = rows_in(text)
if len(now) != len(hips):
raise Refused(f"projected {len(now)} rows for {len(hips)} files; they must be equal")
if set(now) != {h["file"] for h in hips}:
missing = sorted({h["file"] for h in hips} - set(now))
raise Refused(f"the table does not reach {len(missing)} files, e.g. {missing[:3]}")
if len(now) < len(was) and len(was) - len(now) != deleting:
raise Refused(
f"the index would fall from {len(was)} rows to {len(now)}. If that is "
f"deliberate, say so: --delete {len(was) - len(now)}"
)
for heading in (INDEX_HEADING, ORDER_HEADING):
if text.count(heading) != 1:
raise Refused(f"{heading!r} appears {text.count(heading)} times after projecting")
return text
def main() -> int:
argv = sys.argv[1:]
check = "--check" in argv
deleting = int(argv[argv.index("--delete") + 1]) if "--delete" in argv else 0
before = README.read_text(encoding="utf-8")
try:
hips = corpus()
text = project(hips, before, deleting)
except Refused as why:
print(f"REFUSED: {why}", file=sys.stderr)
print("README.md is unchanged.", file=sys.stderr)
return 1
if check:
if text != before:
print("README.md has drifted from HIPs/. Run: python3 scripts/index.py",
file=sys.stderr)
return 1
print(f"README.md projects {len(hips)} HIPs, and matches them")
return 0
README.write_text(text, encoding="utf-8")
caps = sum(1 for h in hips if (h.get("capability") or "").strip())
print(f"README.md: {len(hips)} HIPs indexed, {caps} capabilities in the reading order")
return 0
if __name__ == "__main__":
raise SystemExit(main())