Files
hips/scripts/test-index.py
zeekayandHanzo Dev f4684d1b16 One workflow directory per repository, and the shared tier everywhere else
The CI rule needed correcting twice in one pass, and the second correction came
from measuring rather than from the brief. `hanzoai/hips` moved its own
workflows to `.github/workflows` on 2026-09-08 with a sound reason: the forge
reads that directory natively and github.com reads nothing else, so one
directory serves both planes. Measured across the first-party checkouts, twelve
keep their caller in `.hanzo/workflows/` and two forks carry an upstream's
inherited `.github/workflows` — 24 files in datastore, six in kms, none of them
ours.

So the rule is not the directory's name. It is that a repository keeps exactly
ONE, because the forge reads the first that exists rather than the union: a
split silently retires half the pipeline, and the checks that would go red are
the ones no longer running.

The rest of this pass takes the per-app stores out of six more HIPs. The admin
console had Postgres+Prisma, ClickHouse, Redis+BullMQ and MinIO of its own;
serverless had a `hanzo_functions` database; commerce tracked idempotency in
"Redis"; the zero-trust HIP named SPIFFE identities for `postgres` and `redis`,
which are the upstreams we fork rather than the services we run. Payment
processing also had Commerce POSTing balances into IAM through `add-balance` and
`add-transaction`, which contradicts both HIP-0111's forbidden-verb list and the
correction that Commerce owns the position and IAM mirrors it.

Deployment carried the last `/api/` paths and the corpus's only `v2`
(`api.hanzo.ai/v2`, offered as an example of path routing), plus a `resources:`
block that provisioned a per-app Postgres and Redis. It now names which rank of
the shared tier an app is a tenant of.

HIP-0144 replaces the number this HIP was drafted at: 0138 was published as
"Cloud — Unified Hanzo Binary" and retired, and the index generator refused the
reuse — correctly, since every existing reference to HIP-0138 would have
resolved to a different specification.

Two self-test guards were reported as not holding. Both were the harness's own:
the FM011 case appended a second `implementation-go:` key to a subject that
already declared one, so the later key won and the contradiction was never
built; and test-index restored HANZO_CAPABILITIES only when the caller had
exported it, so every case after the capabilities case ran against a missing
input. A check whose test is a no-op has never been shown to work.

The corpus now passes lint, index --check and both self-tests, from 23 errors.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-09 12:50:48 -07:00

237 lines
9.8 KiB
Python

#!/usr/bin/env python3
"""Prove the generator REFUSES. Run: python3 scripts/test-index.py
A generator that can quietly emit a short table is worse than no generator,
because the short table looks authoritative and nobody re-derives it. So every
guard in scripts/index.py is exercised here by breaking a scratch copy of the
corpus on purpose, and each case asserts BOTH halves:
the run exits 1 with REFUSED, and README.md is byte-identical afterwards.
The second half is the one that matters. A guard that reports a problem after
half-writing the file has not protected anything.
The last case asserts the opposite: on a clean corpus the projection is stable,
so running the generator twice changes nothing and --check agrees.
"""
from __future__ import annotations
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
INDEX = os.path.join(ROOT, "scripts", "index.py")
SUBJECT = "hip-0517-branch-naming.md"
STANDARDS_SUBJECT = "hip-0119-hanzo-service-conventions.md"
def run(root: str, *args: str) -> tuple[int, str]:
env = dict(os.environ)
env.setdefault("HANZO_CAPABILITIES",
os.path.join(os.path.dirname(ROOT), "openapi", "capabilities.yaml"))
p = subprocess.run([sys.executable, INDEX, "--root", root, *args],
capture_output=True, text=True, env=env)
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)
def readme(root: str) -> str:
return open(os.path.join(root, "README.md"), encoding="utf-8").read()
# Each case mutates the scratch root; the runner asserts REFUSED + no write.
def m_empty(root):
for f in os.listdir(os.path.join(root, "HIPs")):
os.remove(os.path.join(root, "HIPs", f))
def m_field(root):
write(root, SUBJECT, re.sub(r"^author:.*$", "", read(root, SUBJECT), count=1, flags=re.M))
def m_status(root):
write(root, SUBJECT,
re.sub(r"^status:.*$", "status: Shipped", read(root, SUBJECT), count=1, flags=re.M))
def m_type(root):
write(root, SUBJECT,
re.sub(r"^type:.*$", "type: Freeform", read(root, SUBJECT), count=1, flags=re.M))
def m_category(root):
write(root, STANDARDS_SUBJECT,
re.sub(r"^category:.*$", "category: Vibes",
read(root, STANDARDS_SUBJECT), count=1, flags=re.M))
def m_no_category(root):
write(root, STANDARDS_SUBJECT,
re.sub(r"^category:.*$", "", read(root, STANDARDS_SUBJECT), count=1, flags=re.M))
def m_progress(root):
write(root, SUBJECT,
re.sub(r"^status:(.*)$", r"status:\1\nimplementation-rust: mostly",
read(root, SUBJECT), count=1, flags=re.M))
def m_final_without_go(root):
# STANDARDS_SUBJECT is Final, which is half the contradiction already. Doing
# this to a Draft would prove nothing.
#
# REPLACE any implementation-go: it already declares. Appending a second one
# proved nothing: the front-matter readers here take one line at a time, so
# the later key won, the subject stayed `shipped`, and the contradiction this
# case exists to build was never built. The guard then correctly did not
# fire and the suite reported it as broken -- a check whose test is a no-op
# is a check that has never been shown to work.
text = re.sub(r"^implementation-go:.*\n", "", read(root, STANDARDS_SUBJECT), flags=re.M)
write(root, STANDARDS_SUBJECT,
re.sub(r"^status:(.*)$", r"status:\1\nimplementation-go: none",
text, count=1, flags=re.M))
def m_requires(root):
s = read(root, STANDARDS_SUBJECT)
write(root, STANDARDS_SUBJECT,
re.sub(r"^(requires:.*)$", r"\1, HIP-8888", s, count=1, flags=re.M)
if re.search(r"^requires:", s, re.M)
else s.replace("\nstatus:", "\nrequires: HIP-8888\nstatus:", 1))
def m_duplicate(root):
shutil.copy(os.path.join(root, "HIPs", SUBJECT),
os.path.join(root, "HIPs", "hip-0517-a-second-claim.md"))
def m_shrink(root):
os.remove(os.path.join(root, "HIPs", SUBJECT))
def m_no_section(root):
p = os.path.join(root, "README.md")
s = open(p, encoding="utf-8").read()
open(p, "w", encoding="utf-8").write(s.replace("## HIP Index", "## The Table", 1))
def m_no_capabilities(root):
# The reading order's capability grouping comes from a sibling repo. Losing
# a section because an input was not checked out is the exact silent
# truncation this file exists to prevent, so it must be an error.
os.environ["HANZO_CAPABILITIES"] = os.path.join(root, "nowhere.yaml")
def m_reissue(root):
"""Claim a number that was published under another title and retired.
Deleting the document is right; reissuing its NUMBER is not. Every
reference already in the wild -- in code, in a commit, in someone else's
link -- would silently resolve to a different specification.
"""
spent = os.path.join(root, "spent.json")
json.dump({"9999": "A Retired Proposal"}, open(spent, "w", encoding="utf-8"))
src = os.path.join(root, "HIPs", SUBJECT)
body = open(src, encoding="utf-8").read()
body = body.replace("hip: 0517", "hip: 9999", 1)
open(os.path.join(root, "HIPs", "hip-9999-a-different-thing.md"), "w",
encoding="utf-8").write(body)
CASES = [
("an empty HIPs/", m_empty, None),
("a required front-matter field removed", m_field, None),
("a status outside the vocabulary", m_status, None),
("a type outside the vocabulary", m_type, None),
("a category outside the vocabulary", m_category, None),
("a Standards Track HIP with no category", m_no_category, None),
("an implementation progress outside the vocabulary", m_progress, None),
("a Final HIP declaring implementation-go: none", m_final_without_go, None),
("requires: a HIP that does not exist", m_requires, None),
("two files claiming one number", m_duplicate, None),
("a HIP deleted without saying so", m_shrink, None),
("README's index heading renamed", m_no_section, None),
("capabilities.yaml not checked out", m_no_capabilities, None),
("a retired number reissued for a different proposal", m_reissue, None),
]
def main() -> int:
base = tempfile.mkdtemp(prefix="hips-index-test-")
# Resolve the capabilities path the way run() does, NOT just from the caller's
# environment. m_no_capabilities points HANZO_CAPABILITIES at a file that does
# not exist and the runner restores it afterwards -- but the restore was
# guarded on a value that is None whenever the caller had not exported the
# var, which is the ordinary case. Every case after m_no_capabilities then ran
# against a missing input, and the two that follow it reported failures that
# were the harness's own.
capabilities = os.environ.get("HANZO_CAPABILITIES") or os.path.join(
os.path.dirname(ROOT), "openapi", "capabilities.yaml")
try:
clean = os.path.join(base, "clean")
os.makedirs(clean)
shutil.copytree(os.path.join(ROOT, "HIPs"), os.path.join(clean, "HIPs"))
for f in ("README.md", "vocabulary.json"):
shutil.copy(os.path.join(ROOT, f), clean)
shutil.copytree(os.path.join(ROOT, "scripts"), os.path.join(clean, "scripts"))
rc, out = run(clean, "--check")
if rc != 0:
print("CONTROL FAILED: the committed README does not match HIPs/.")
print(out)
return 1
print("control: README projects the corpus exactly (--check exit 0)\n")
failures = []
for what, mutate, _ in CASES:
root = os.path.join(base, re.sub(r"\W+", "-", what)[:40])
shutil.copytree(clean, root)
mutate(root)
# AFTER the mutation: one case renames a README heading, and what is
# being asserted is that the generator wrote nothing, not that the
# mutation wrote nothing.
before = readme(root)
rc, out = run(root)
if capabilities:
os.environ["HANZO_CAPABILITIES"] = capabilities
refused = rc == 1 and "REFUSED" in out
intact = readme(root) == before
ok = refused and intact
note = "" if ok else (
" <- did not refuse" if not refused else " <- WROTE README ANYWAY")
print(f" {'PASS' if ok else 'FAIL'} refuses: {what}{note}")
if not ok:
failures.append((what, rc, out))
# And the other direction: a clean run is stable and idempotent.
root = os.path.join(base, "stable")
shutil.copytree(clean, root)
rc1, _ = run(root)
once = readme(root)
rc2, _ = run(root)
rc3, _ = run(root, "--check")
stable = rc1 == 0 and rc2 == 0 and rc3 == 0 and readme(root) == once
print(f"\n {'PASS' if stable else 'FAIL'} a clean corpus projects, twice, identically")
if not stable:
failures.append(("idempotent", rc1, f"{rc1} {rc2} {rc3}"))
# Every guard the file can raise should be reachable from a case above.
raised = set(re.findall(r"raise Refused\(", open(INDEX, encoding="utf-8").read()))
if not raised:
print(" FAIL scripts/index.py raises no guard at all")
failures.append(("guards", 0, ""))
if failures:
print(f"\n{len(failures)} guards did not hold:\n")
for what, rc, out in failures:
print(f"--- {what} exit={rc} ---\n{out[-600:]}")
return 1
print(f"\nall {len(CASES)} guards refuse and leave README.md untouched")
return 0
finally:
shutil.rmtree(base, ignore_errors=True)
if __name__ == "__main__":
raise SystemExit(main())