Files
HIPs/scripts/test-lint-hips.py
zeekay ad5a010f79 Three words for what a proposal is, and a generator that refuses
The same fact lived in nine hand-kept places and they disagreed about 58
proposals. This makes the file the only authority and everything else a
projection of it.

ONE VOCABULARY, in vocabulary.json, read by the lint, the generator and the
doc site. Three words, each answering "can I rely on this?" and nothing else:

    Draft    nothing implements it yet, or its own text says the work is ahead
    Final    the thing it specifies exists in the code
    Living   governance and process: amended in place, never finished

Eight spellings folded in. `Active` was the one doing real damage: on four Meta
and Process HIPs it meant living, on 48 others it meant shipped, and one word
doing two jobs is why nothing agreed. `Review` and `Last Call` described a
queue rather than a document, and a proposal under review is still one you
cannot build against.

STATUS IS EARNED, not declared. Every Standards Track proposal was checked
against the estate: 235 have their implementation, and eight do not and say so.
HIP-0095 carries a twelve-week plan ending at "mainnet launch" and has no code.
HIP-0525 specifies an all-machine meeting while apps/meet refuses a machine a
seat. HIP-0078 names a prover at a "planned location" that does not exist.
HIP-0114 is the cog.* procedure set, and that procedure set returns nothing.
HIP-0003, HIP-0102, HIP-0305 and HIP-0901 likewise: looked, found nothing.

SIXTEEN TOMBSTONES DELETED rather than statused. A dead document kept alive in
the index is one concept wearing two names. Each successor exists and carries
the text, and the 90 references that pointed at a tombstone now point at it --
whole markdown links at once, so link text cannot outlive its target. Three of
the sixteen were pure compatibility: HIP-0028 specified a migration off a
Bitnami chart, HIP-0029 dual-engine MySQL, HIP-0030 a schema-deprecation
policy. We ship no backwards compatibility, so we do not specify it.

Nine links were already broken and had been since the corpus was renumbered:
short-form ./hip-5.md paths that never existed. Two named the wrong proposal --
one cited "HIP-1: Hanzo Multimodal Models" where multimodal is HIP-0003.

THE GENERATOR REFUSES more often than it writes, because a short table looks
exactly as authoritative as a complete one. Rows must equal files; a shrinking
index must say by how much; a section whose input is missing is an error, not
an omission; nothing is written until all of it validates. Its self-test breaks
the corpus eleven ways and requires each to refuse AND leave README byte-
identical -- a guard that reports after half-writing has protected nothing.
The lint's second implementation of the same comparison is gone.

The site rendered 52 proposals with a fallback grey badge and counted them
toward none of its three stat tiles, because docs/lib/source.ts declared a
status union by hand: it admitted a `Stagnant` no file has ever carried and
omitted the one 52 files did. Five components each kept their own copy of the
colours. All six now read vocabulary.json.
2026-08-28 18:05:41 -07:00

210 lines
8.4 KiB
Python

#!/usr/bin/env python3
"""Prove the lint fails. Run: python3 scripts/test-lint-hips.py
A check that has only ever been seen to pass has not been shown to work -- it is
indistinguishable from a check that returns success unconditionally, and this
repository has already shipped one of those (the GitHub workflow that printed
every missing field and then exited 0, so a HIP with no status went green).
So: copy the corpus, break it on purpose one way at a time, and require the lint
to report the specific code for that break. Every check the lint declares must
appear here, and the last test asserts exactly that -- if someone adds a check
and no test, this file fails.
"""
from __future__ import annotations
import os
import re
import shutil
import subprocess
import sys
import tempfile
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LINT = os.path.join(ROOT, "scripts", "lint-hips.py")
# A file that is structurally clean and small enough to mutate predictably.
SUBJECT = "hip-0517-branch-naming.md"
STANDARDS_SUBJECT = "hip-0119-hanzo-service-conventions.md"
def run(root: str) -> tuple[int, str]:
p = subprocess.run([sys.executable, LINT, "--root", root],
capture_output=True, text=True)
return p.returncode, p.stdout + p.stderr
def read(root: str, name: str) -> str:
return open(os.path.join(root, "HIPs", name), encoding="utf-8").read()
def write(root: str, name: str, text: str) -> None:
open(os.path.join(root, "HIPs", name), "w", encoding="utf-8").write(text)
# Each case: code, description, and a mutation taking the scratch root.
def m_fm001(root):
write(root, SUBJECT, read(root, SUBJECT).split("\n---\n", 1)[1])
def m_fm002(root):
write(root, SUBJECT, re.sub(r"^created:.*$", "", read(root, SUBJECT), count=1, flags=re.M))
def m_fm003(root):
write(root, SUBJECT, re.sub(r"^hip:.*$", "hip: 0999", read(root, SUBJECT), count=1, flags=re.M))
def m_fm004(root):
write(root, SUBJECT, re.sub(r"^status:.*$", "status: Baked", read(root, SUBJECT), count=1, flags=re.M))
def m_fm005(root):
write(root, SUBJECT, re.sub(r"^type:.*$", "type: Freeform", read(root, SUBJECT), count=1, flags=re.M))
def m_fm006(root):
write(root, STANDARDS_SUBJECT,
re.sub(r"^category:.*$", "category: Vibes", read(root, STANDARDS_SUBJECT), count=1, flags=re.M))
def m_fm007(root):
s = read(root, STANDARDS_SUBJECT)
write(root, STANDARDS_SUBJECT, re.sub(r"^(requires:.*)$", r"\1, HIP-8888", s, count=1, flags=re.M))
def m_fm008(root):
write(root, SUBJECT,
re.sub(r"^status:(.*)$", r"status:\1\nsuperseded-by: HIP-0106",
read(root, SUBJECT), count=1, flags=re.M))
def m_fm009(root):
os.rename(os.path.join(root, "HIPs", SUBJECT),
os.path.join(root, "HIPs", "hip-0517-Branch_Naming.md"))
def m_st001(root):
write(root, SUBJECT, re.sub(r"^# HIP-.*$", "", read(root, SUBJECT), count=1, flags=re.M))
def m_st002(root):
write(root, SUBJECT,
re.sub(r"^# HIP-.*$", "# HIP-0517: Something Else Entirely",
read(root, SUBJECT), count=1, flags=re.M))
def m_st003(root):
# Duplicate the heading every Standards Track HIP is REQUIRED to carry. This
# used to rename '## Motivation', which is advisory -- so when a corpus edit
# removed that heading from the subject, the replace matched nothing, the
# lint had no duplicate to report, and the check stopped proving anything
# while still being counted. Anchor a mutation on what the lint requires,
# never on what a document happens to have.
s = read(root, STANDARDS_SUBJECT)
write(root, STANDARDS_SUBJECT, s + "\n## Abstract\n\nA second one, on purpose.\n")
def m_st004(root):
s = read(root, STANDARDS_SUBJECT)
write(root, STANDARDS_SUBJECT, s.replace("### §2 ", "### §1 ", 1))
def m_st005(root):
s = read(root, STANDARDS_SUBJECT)
write(root, STANDARDS_SUBJECT, s.replace("\n## Abstract\n", "\n## Preface\n", 1))
def m_st006(root):
s = read(root, STANDARDS_SUBJECT)
write(root, STANDARDS_SUBJECT, s + "\n\n```go\nfunc dangling() {}\n")
def m_st007(root):
s = read(root, STANDARDS_SUBJECT)
write(root, STANDARDS_SUBJECT,
s.replace("### §2 The entry point", "### §2 The entry point\n\n#### §9.1 Misfiled", 1)
if "### §2 The entry point" in s
else s.replace("## Specification", "## Specification\n\n### 1 One\n\n#### 4.2 Misfiled", 1))
def m_lk001(root):
s = read(root, STANDARDS_SUBJECT)
write(root, STANDARDS_SUBJECT,
s.replace("## Abstract", "## Abstract\n\nSee [HIP-0106](./hip-0106-gone.md).", 1))
def m_ix003(root):
shutil.copy(os.path.join(root, "HIPs", SUBJECT),
os.path.join(root, "HIPs", "hip-0517-duplicate-claim.md"))
def m_pl001(root):
s = read(root, STANDARDS_SUBJECT)
write(root, STANDARDS_SUBJECT,
s.replace("## Abstract", "## Abstract\n\nThis is as good as Terraform.", 1))
def m_pl002(root):
s = read(root, STANDARDS_SUBJECT)
write(root, STANDARDS_SUBJECT,
s.replace("## Abstract", "## Abstract\n\nSee `hanzo-inc/payments` for the rest.", 1))
CASES = [
("FM001", "front matter removed", m_fm001),
("FM002", "created: removed", m_fm002),
("FM003", "hip: renumbered away from the filename", m_fm003),
("FM004", "status: set to a word not in the vocabulary", m_fm004),
("FM005", "type: set to a word not in the vocabulary", m_fm005),
("FM006", "category: set to a word not in the vocabulary", m_fm006),
("FM007", "requires: a HIP that does not exist", m_fm007),
("FM008", "a superseded-by tombstone field added", m_fm008),
("FM009", "filename given capitals and an underscore", m_fm009),
("ST001", "H1 deleted", m_st001),
("ST002", "H1 title made to disagree with front matter", m_st002),
("ST003", "a second '## Abstract' introduced", m_st003),
("ST004", "two sections numbered §1", m_st004),
("ST005", "'## Abstract' renamed to '## Preface'", m_st005),
("ST006", "a code fence left open", m_st006),
("ST007", "a subsection numbered outside its parent", m_st007),
("LK001", "a body link pointed at a HIP file that does not exist", m_lk001),
("IX003", "a second file claiming HIP-0517", m_ix003),
("PL001", "'as good as Terraform' added", m_pl001),
("PL002", "a private-org repository cited", m_pl002),
]
def main() -> int:
base = tempfile.mkdtemp(prefix="hips-lint-test-")
try:
clean_root = os.path.join(base, "clean")
os.makedirs(clean_root)
shutil.copytree(os.path.join(ROOT, "HIPs"), os.path.join(clean_root, "HIPs"))
shutil.copy(os.path.join(ROOT, "README.md"), clean_root)
shutil.copy(os.path.join(ROOT, "vocabulary.json"), clean_root)
rc, out = run(clean_root)
if rc != 0:
print("CONTROL FAILED: the unmutated corpus does not lint clean.")
print(out)
return 1
print(f"control: clean corpus lints clean (exit 0)\n")
failures = []
for code, what, mutate in CASES:
root = os.path.join(base, code)
shutil.copytree(clean_root, root)
mutate(root)
rc, out = run(root)
caught = rc == 1 and re.search(rf"^ERROR {code}\b", out, re.M) is not None
print(f" {'PASS' if caught else 'FAIL'} {code} {what}")
if not caught:
failures.append((code, what, rc, out))
declared = set(re.findall(r'^\s*\("([A-Z]{2}\d{3})",',
open(LINT, encoding="utf-8").read(), re.M))
tested = {c for c, _, _ in CASES}
untested = declared - tested
print(f"\n {'PASS' if not untested else 'FAIL'} every declared check has a test"
+ (f" -- missing {sorted(untested)}" if untested else ""))
if untested:
failures.append(("COVERAGE", "declared but untested", 0, str(sorted(untested))))
if failures:
print(f"\n{len(failures)} of {len(CASES) + 1} checks did not fail as required:\n")
for code, what, rc, out in failures:
print(f"--- {code} ({what}) exit={rc} ---")
print("\n".join(out.splitlines()[-12:]) if isinstance(out, str) else out)
return 1
print(f"\nall {len(CASES)} checks fail on a deliberate break, and the clean "
f"corpus passes")
return 0
finally:
shutil.rmtree(base, ignore_errors=True)
if __name__ == "__main__":
raise SystemExit(main())