557629d2bf
gates / gates (push) Successful in 2m3s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
133 lines
6.1 KiB
Python
133 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""check_stands.py — the gate on documentation/architecture/where-felhom-stands.yaml.
|
|
|
|
The page that dataset renders is the operator's picture of where the product stands. Its whole
|
|
value is that every claim on it is traceable, so this checks the traceability rather than the
|
|
claims: a claim nobody can trace is not a weaker claim, it is not a claim at all.
|
|
|
|
WHAT IT CONVICTS ON (each is a FAIL, exit 1):
|
|
|
|
1. an entry with NO source — §4 rule 1: that is a defect, not a claim
|
|
2. an `evidence:` path that does not resolve — a citation nobody opened
|
|
3. a `register:` id in neither OPEN-ITEMS.md nor CLOSED-ITEMS.md — a dangling register
|
|
reference. A CLOSED row is a valid citation (R-819): a stand that is walked or built is often
|
|
walked BECAUSE its row closed, and the 2026-10-03 triage moved most cited rows there. Reading
|
|
only the open register convicted every finished row the page leaned on.
|
|
4. a `capability-map:` anchor not found — the row it derives from has moved or gone
|
|
5. status `walked` with no `evidence:` source — THE LOAD-BEARING ONE. "Walked" means done end
|
|
to end on real hardware with evidence on file. If no evidence document is cited, the page is
|
|
drawing a green dot from an opinion. This is the rule the positive control exercises: take a
|
|
claim the page marks `missing`, mark it `walked`, and this fires.
|
|
|
|
WHAT IT DOES NOT DO, said plainly so the green is not over-read: it does not read the evidence and
|
|
judge whether it supports the claim, and it cannot. A human verdict lives in each entry's
|
|
`verified:` block; this gate checks that the paperwork exists, not that the paperwork is right.
|
|
|
|
Run: python3 scripts/check_stands.py [path-to-yaml]
|
|
"""
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
DEFAULT = os.path.join(ROOT, "documentation", "architecture", "where-felhom-stands.yaml")
|
|
REGISTER = os.path.join(ROOT, "documentation", "backlog", "OPEN-ITEMS.md")
|
|
CLOSED = os.path.join(ROOT, "documentation", "backlog", "CLOSED-ITEMS.md")
|
|
CAPMAP = os.path.join(ROOT, "documentation", "architecture", "00-capability-map.md")
|
|
DOCS = os.path.join(ROOT, "documentation")
|
|
|
|
|
|
def load(path):
|
|
"""Minimal parser for the shape this file is written in.
|
|
|
|
Deliberately not PyYAML: the runner image and the gate hosts carry python3 and nothing else
|
|
(the same constraint that keeps `uses:` out of the CI workflow), and a gate that needs a pip
|
|
install is a gate that silently stops running.
|
|
"""
|
|
claims, cur, in_sources = [], None, False
|
|
for raw in open(path, encoding="utf-8"):
|
|
line = raw.rstrip("\n")
|
|
if line.startswith(" - id:"):
|
|
cur = {"id": line.split(":", 1)[1].strip(), "sources": []}
|
|
claims.append(cur)
|
|
in_sources = False
|
|
continue
|
|
if cur is None:
|
|
continue
|
|
if line.strip() == "sources:":
|
|
in_sources = True
|
|
continue
|
|
m = re.match(r'\s+- (capability-map|evidence|register): (.*)$', line)
|
|
if in_sources and m:
|
|
cur["sources"].append((m.group(1), m.group(2).strip().strip('"')))
|
|
continue
|
|
m = re.match(r'\s+(status|title|verdict|depth): (.*)$', line)
|
|
if m:
|
|
in_sources = False
|
|
cur[m.group(1)] = m.group(2).strip().strip('"')
|
|
return claims
|
|
|
|
|
|
def main():
|
|
path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT
|
|
claims = load(path)
|
|
register = open(REGISTER, encoding="utf-8").read()
|
|
closed = open(CLOSED, encoding="utf-8").read()
|
|
# Normalise the map before matching: its row labels carry ** and ` markup, so a literal
|
|
# substring probe against the raw file fails on text that is plainly there. Matching the
|
|
# rendered words is what the citation means.
|
|
capmap = open(CAPMAP, encoding="utf-8").read()
|
|
capmap = re.sub(r"[`*]", "", capmap)
|
|
capmap = re.sub(r"\s+", " ", capmap).lower()
|
|
|
|
print("check_stands — %d claim(s) in %s" % (len(claims), os.path.relpath(path, ROOT)))
|
|
fails = []
|
|
closed_cites = []
|
|
|
|
for c in claims:
|
|
cid = c["id"]
|
|
if not c["sources"]:
|
|
fails.append("%s: NO SOURCE — an entry with no source is a defect, not a claim" % cid)
|
|
for kind, ref in c["sources"]:
|
|
if kind == "evidence":
|
|
if not os.path.exists(os.path.join(DOCS, ref)):
|
|
fails.append("%s: evidence path does not resolve: documentation/%s" % (cid, ref))
|
|
elif kind == "register":
|
|
# A row's id cell is `| **R-NNN** |`; the closing ** keeps R-27 from matching R-273.
|
|
row = r"\*\*%s\*\*" % re.escape(ref)
|
|
if re.search(row, register):
|
|
pass
|
|
elif re.search(row, closed):
|
|
closed_cites.append("%s → %s" % (cid, ref))
|
|
else:
|
|
fails.append("%s: register id %s is in neither OPEN-ITEMS.md nor CLOSED-ITEMS.md"
|
|
% (cid, ref))
|
|
elif kind == "capability-map":
|
|
probe = " ".join(ref.split()[:4])
|
|
probe = re.sub(r"[`*]", "", probe)
|
|
if probe and probe.lower() not in capmap:
|
|
fails.append("%s: capability-map anchor not found: %r" % (cid, probe))
|
|
if c.get("status") == "walked" and not any(k == "evidence" for k, _ in c["sources"]):
|
|
fails.append("%s: status 'walked' but NO evidence document cited — a green dot "
|
|
"drawn from an opinion" % cid)
|
|
|
|
counts = {}
|
|
for c in claims:
|
|
counts[c.get("status")] = counts.get(c.get("status"), 0) + 1
|
|
print(" statuses: " + ", ".join("%s=%d" % kv for kv in sorted(counts.items())))
|
|
print(" register citations resolved in CLOSED-ITEMS.md: %d" % len(closed_cites))
|
|
|
|
if fails:
|
|
print("\nCONVICTED — %d problem(s):" % len(fails))
|
|
for f in fails:
|
|
print(" " + f)
|
|
return 1
|
|
print("\ncheck_stands: OK — every claim cites a source, every citation resolves, and every "
|
|
"'walked' cites a walk.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|