Files
hips/scripts/coverage.py
zeekay 499c301006 the capability list has one source, and 21 HIPs take their singular
coverage.py asked which capabilities have no HIP against the UNION of two
separately hand-maintained lists -- hanzoai/openapi's capabilities.yaml and
cloud's manifest/apps.go -- and the two held different words for one thing. The
manifest said bot, campaign, dataset, link, network, node; the taxonomy said the
plurals. A union of two spellings is twelve capabilities where there are six, so
six came back uncovered whichever spelling the HIPs carried, and editing the HIPs
flipped the same six errors to the other spelling and back. The gate could not
reach zero because the question it asked had two answers.

capabilities.yaml now carries a `fleet:` DERIVED from the manifest, gated by
hanzoai/openapi's capabilities.py, so this reads ONE list and cloud decides every
name in it. The union is gone, and with it the branches that had to say "the
manifest cross-check did not run": CV003 and CV006 are decidable now, because a
name absent from `fleet:` is absent because nothing ships it. The list is also
complete where the old one could not be -- an app that serves no HTTP operation
was invisible to this gate by construction, and kafka went that way for months.

`domains:` stays what it was and is read for LABELS only. It groups the PUBLISHED
document, which lags cloud's main by design, so a capability renamed since the
last publish is filed under its old spelling. That is the pin being honest, not a
second opinion about what exists, and reading it as a universe is the bug.

21 HIPs take their singular in front matter, filename and prose. HIP-0139 ยง2.2 is
what decides it: the canonical name is the SINGULAR, the plural is a rewrite the
router performs at the door and nothing publishes, and a capability is never
renamed for number again. The ratchet's note still argued the superseded rule --
that number follows the contract, so a collection takes the plural -- and named
HIP-1107 as declaring `bots`. Both are corrected.

Titles say what the thing IS rather than filing it under an abstract noun. "The
Judgment Plane", "The Compute Primitive", "The Zero Trust Overlay" and "The A/B
Plane" name a category, not a capability, and a reader learns nothing from them;
each now names its type -- a score over model output, a lease on isolated
compute, an org-scoped overlay, arms and an assignment and a verdict.

catalogsync and rollingcap leave the ratchet because they left cloud's manifest:
neither opened a store, neither answered a path, and each claimed a /v1/<name>
with nothing registered behind it. Two lines leave because the capabilities did,
which is the outcome a ratchet is for.

coverage.py: 124 capabilities, 122 covered, 0 errors.
2026-08-21 10:58:40 -07:00

330 lines
14 KiB
Python

#!/usr/bin/env python3
"""Does every capability have a HIP?
`lint-hips.py` checks the corpus against ITSELF -- front matter, structure, the
index. It needs no input but the corpus, which is why it can be total. This asks
the other question, and it cannot be answered from the corpus alone: **is there a
HIP for every `/v1/<capability>` the cloud serves?**
The capability list is therefore an INPUT, and a missing input is an ERROR rather
than a skip. A coverage check that quietly passes when it cannot find the list is
the shape this estate keeps deleting: a gate that has only ever been seen to pass
has not been shown to work.
There is ONE input, and it took a day of churn to get there. It was two: this
gate read hanzoai/openapi's `capabilities.yaml` AND cloud's `manifest/apps.go`,
and took the UNION, because the first could not answer for an app that serves no
HTTP operation. But the two were separately hand-maintained, so they could hold
different words for one thing -- and on 2026-08-21 they did. cloud said `bot`,
`campaign`, `dataset`, `link`, `network`, `node`; the taxonomy said the plurals.
A union of two spellings is TWELVE capabilities where there are six, so six came
back uncovered whichever spelling the HIPs carried. Editing the HIPs flipped the
same six errors to the other spelling and back. The gate could not reach zero
because the question it asked had two answers.
`capabilities.yaml` now carries a `fleet:` list DERIVED from `manifest/apps.go`
by hanzoai/openapi's `capabilities.py`, which gates it. So the union is gone:
this reads one list, cloud decides every name in it, and no second copy is left
to disagree. That list is also complete where the old one could not be -- `kafka`
speaks its protocol on :9092 and emits no operation, and it is IN the manifest,
so it is in `fleet:` and this gate can finally report that it has no spec.
python3 scripts/coverage.py # exits 1 on any ERROR
python3 scripts/coverage.py --list # the checks, by code
python3 scripts/coverage.py --report # measure and print, never fail
**A HIP covers a capability when it SAYS SO, in front matter: `capability: sandbox`.**
Declared, never inferred, and one capability has exactly one HIP.
The two inferring rules were both tried and both overstate. Matching a title reads
"Data Plane" as covering `data` and "On-chain settlement" as covering `chain`.
Matching the ROUTE in the body is tighter and still wrong in the direction that
matters: measured on this corpus it credited 72 of 191, but HIP-0106 earned nine
of those by LISTING the plugin set and HIP-0040 earned `audio` by showing an SDK
call. A reference is not a specification, and a gate that counts references
reports a corpus that covers the surface when it does not.
Route mention survives as a HINT -- `mentions()` names the HIP that already talks
about a capability, so adopting an uncovered one is mechanical rather than a
search. It decides nothing.
**UNCOVERED is a RATCHET, not a list of excuses.** `capability-coverage.txt`
carries the capabilities that have no HIP yet, and it may only SHRINK. A new
capability with no HIP fails, so the gap cannot grow silently while the surface
does. A capability that gains a HIP must leave the file in the same commit, so
the file cannot rot into prose that describes a corpus nobody has. Same shape as
cloud's `openapi/floor.json` and `openapi/unreachable.txt`, for the same reason.
**Nothing here hardcodes a count.** Every number this prints is measured at the
moment it runs, against the capability list of the release being cut. A count
written into prose is stale the next week; a count printed by a gate is not.
"""
from __future__ import annotations
import os
import re
import sys
from collections import Counter
HIP_DIR = "HIPs"
RATCHET = "capability-coverage.txt"
# Where the capability list comes from, most explicit first. It is the PUBLIC
# taxonomy (hanzoai/openapi `capabilities.yaml`), regenerated when a cloud
# version is cut -- so this gate measures the release it is run against rather
# than a snapshot somebody committed here. A second copy inside this repo would
# be exactly the drift the ratchet exists to prevent.
#
# This repo's CI checks out hanzoai/openapi and nothing else, which is why the
# list has to live there rather than in cloud: cloud is private, and a gate that
# needs a credential is a gate that does not run. What makes reading a public
# projection safe is that it is a PROJECTION -- `fleet:` is generated from
# cloud's manifest and `capabilities.py --check` fails when the two part company,
# so a name here is a name cloud decided.
CAPABILITY_SOURCES = (
os.environ.get("HANZO_CAPABILITIES"),
"../openapi/capabilities.yaml",
"../../openapi/capabilities.yaml",
)
CHECKS = [
("CV001", "a capability has no HIP and is not in the ratchet"),
("CV002", "a ratchet entry now HAS a HIP -- delete the line"),
("CV003", "the ratchet names a capability that no longer exists"),
("CV004", "the capability list could not be resolved"),
("CV005", "two HIPs declare one capability"),
("CV006", "a HIP declares a capability nothing ships"),
]
class Report:
def __init__(self) -> None:
self.errors: list[tuple[str, str, str]] = []
self.warns: list[tuple[str, str, str]] = []
def error(self, code: str, where: str, msg: str) -> None:
self.errors.append((code, where, msg))
def warn(self, code: str, where: str, msg: str) -> None:
self.warns.append((code, where, msg))
def capabilities(path: str) -> tuple[list[str], dict[str, str]]:
"""The capabilities that exist, and the domain each one is filed under.
TWO KEYS, TWO QUESTIONS, and only the first one decides who owes a HIP:
`fleet:` every capability cloud ships, DERIVED from its manifest. This is
the universe -- complete, current, and one name per capability.
`domains:` which domain a capability is shown under. Curation over the
PUBLISHED document, which lags cloud's main by design, so it is
read for LABELS and never for existence. A capability renamed
since the last publish is filed here under its old spelling;
that is the pin being honest, not a second opinion about what
exists, and reading it as a universe is what broke this gate.
Parsed without PyYAML: this runs in CI images that carry a bare Python, and
a lint that needs a pip install is a lint that gets skipped. The shape is
fixed and shallow -- flow sequences that wrap across lines -- so a reader can
check this against the file by eye. hanzoai/openapi's test_publish.py holds
this reader and a real YAML load to the same answer.
"""
caps: list[str] = []
domain: dict[str, str] = {}
current = ""
key = ""
buf = "" # an open `[` flow sequence, which wraps across lines
for raw in open(path, encoding="utf-8").read().splitlines():
line = raw.split("#", 1)[0].rstrip() if not buf else raw.rstrip()
if not line.strip():
continue
if buf:
buf += " " + line.strip()
else:
m = re.match(r"^\s*-\s*id:\s*['\"]?([A-Za-z0-9_.-]+)", line)
if m:
current = m.group(1)
continue
m = re.match(r"^\s*(fleet|tags):\s*\[(.*)$", line)
if not m:
continue
key, buf = m.group(1), m.group(2)
if "]" not in buf:
continue
for cap in re.findall(r"[A-Za-z0-9_.-]+", buf[:buf.index("]")]):
if key == "fleet":
caps.append(cap)
else:
domain.setdefault(cap, current)
buf = ""
return sorted(set(caps)), domain
def declares(text: str) -> list[str]:
"""The capabilities a HIP DECLARES it specifies, from its front matter:
capability: sandbox
capability: [sandbox, exec]
Declared, never inferred. Reading a route out of the prose instead credits
every passing mention: HIP-0106 names nine capabilities because it LISTS the
plugin set, and HIP-0040 names `audio` because it shows an SDK call. Those
are references, not specifications, and counting them reports a corpus that
covers the surface when it does not.
"""
fm, _ = split_front_matter(text)
if not fm:
return []
m = re.search(r"^capability:\s*(.+)$", fm, re.M)
if not m:
return []
return re.findall(r"[A-Za-z0-9_.-]+", m.group(1))
def mentions(cap: str, bodies: dict[str, str]) -> list[str]:
"""HIPs whose body names `/v1/<cap>` -- a HINT for which HIP should declare
it, never the rule. Reported to make the migration mechanical."""
route = re.compile(rf"/v1/{re.escape(cap)}(?![A-Za-z0-9_-])")
return sorted(f for f, body in bodies.items() if route.search(body))
def split_front_matter(text: str) -> tuple[str | None, str]:
if not text.startswith("---"):
return None, text
end = text.find("\n---", 3)
if end == -1:
return None, text
return text[3:end].strip(), text[end + 4:]
def ratchet() -> list[str]:
if not os.path.exists(RATCHET):
return []
out = []
for line in open(RATCHET, encoding="utf-8").read().splitlines():
line = line.split("#", 1)[0].strip()
if line:
out.append(line)
return out
def measure() -> tuple[Report, dict]:
rep = Report()
path = next((p for p in CAPABILITY_SOURCES if p and os.path.exists(p)), None)
if not path:
rep.error("CV004", "capability list",
"not found. Set HANZO_CAPABILITIES to hanzoai/openapi's "
"capabilities.yaml, or check that repo out beside this one. "
"Refusing to report coverage against a list I do not have.")
return rep, {}
caps, domain = capabilities(path)
if not caps:
rep.error("CV004", path,
"resolved but carries no `fleet:` -- that list is generated by "
"hanzoai/openapi's capabilities.py from cloud's manifest, and "
"this gate will not report coverage against a universe it does "
"not have")
return rep, {}
bodies = {}
for name in sorted(os.listdir(HIP_DIR)):
if name.endswith(".md"):
bodies[name] = open(os.path.join(HIP_DIR, name),
encoding="utf-8", errors="replace").read()
hits: dict[str, list[str]] = {c: [] for c in caps}
for name, text in bodies.items():
for cap in declares(text):
if cap in hits:
hits[cap].append(name)
else:
# Decidable now, and it was not before. `fleet:` is every app
# cloud ships, so a name absent from it is absent because
# nothing ships it -- not because this gate was looking at a
# list that could not see it.
rep.error("CV006", name,
f"declares capability {cap!r}, which nothing ships")
have = {c for c, v in hits.items() if v}
missing = [c for c in caps if c not in have]
known = ratchet()
for c in missing:
hint = mentions(c, bodies)
where = f" -- {hint[0]} already names the route" if hint else ""
if c not in known:
rep.error("CV001", c,
f"({domain.get(c, 'unfiled')}) no HIP declares it{where}. Write "
f"one, or add {c} to {RATCHET} with the reason.")
for c in known:
if c in have:
rep.error("CV002", c,
f"is in {RATCHET} but {hits[c][0]} declares it -- "
"delete the line in the same commit")
elif c not in caps:
rep.error("CV003", c,
f"is in {RATCHET} but is not a capability any more")
for c in sorted(have):
if len(hits[c]) > 1:
rep.error("CV005", c,
f"{len(hits[c])} HIPs declare it: {', '.join(hits[c])}. "
"One capability, one HIP.")
return rep, {
"source": path, "caps": caps, "domain": domain,
"hits": hits, "have": have, "missing": missing, "known": known,
}
def main() -> int:
if "--list" in sys.argv:
for code, what in CHECKS:
print(f"{code} {what}")
return 0
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if "--root" in sys.argv:
root = sys.argv[sys.argv.index("--root") + 1]
os.chdir(root)
rep, m = measure()
report_only = "--report" in sys.argv
if m:
caps, have, missing = m["caps"], m["have"], m["missing"]
by_domain: dict[str, list[int]] = {}
for c in caps:
# No domain means the PUBLISHED document does not carry this name
# yet -- a capability cloud has added or renamed since the last
# publish. It exists, so it owes a HIP; it is just not filed on a
# page yet, and it files itself when the pin moves.
d = m["domain"].get(c, "unfiled")
row = by_domain.setdefault(d, [0, 0])
row[1] += 1
if c in have:
row[0] += 1
print(f"capabilities {len(caps)} from {m['source']} `fleet:`")
for d, (h, n) in sorted(by_domain.items()):
print(f" {d:16s} {h:3d}/{n:3d}")
print(f"covered {len(have)} uncovered {len(missing)} "
f"ratchet {len(m['known'])}")
for code, where, msg in sorted(rep.warns):
print(f"warn {code} {where}: {msg}")
for code, where, msg in sorted(rep.errors):
print(f"ERROR {code} {where}: {msg}")
if rep.errors and not report_only:
by_code = Counter(c for c, _, _ in rep.errors)
print("\n" + ", ".join(f"{c}x{n}" for c, n in sorted(by_code.items())))
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())