Files
jakman85andClaude Opus 5 6e77150ae1 fix(ci): kill the false-green base resolution; cover R-SEC-01/02 + R-TEST-06 (L0)
Unblocks the enforced Gitea gate, which correctly BLOCKED this branch on
['R-SEC-01','R-SEC-02','R-TEST-06'] being touched but uncovered. Tests were authored;
the coverage rule was NOT narrowed.

=== THE FALSE GREEN (root cause) ===
`make -C ci gate` locally reported PASS having checked NOTHING. resolve_base() validated
nothing, and EVERY way of failing to find a base degraded silently to an empty touched
set + exit 0: an `HEAD~1` last resort, a two-dot diff fallback masking a failing
three-dot diff, and no check that HEAD is even AHEAD of the base. Run while sitting on
`main` (main == origin/main), `origin/main...HEAD` is empty -> "no changed files vs
base" -> the empty set means run.sh is invoked with NO --requirements -> coverage.py's
--require gate never runs at all. Green by vacuum.

Now: candidates are BASE_REF > GITHUB_BASE_REF > origin/main > main, each verified to
resolve. HEAD~1 and the two-dot fallback are gone. Unresolvable base / no merge base /
failed diff / HEAD-not-ahead -> loud banner, EMPTY stdout, exit 3, and a machine-readable
reason (on-base | no-base | diff-failed). gate.sh turns exit 3 into an UNDECIDED
decision: `on-base` -> "NOT APPLICABLE — no branch under gate" (exit 0 so push-to-main
runs don't break, but it never reads as PASS); `no-base`/`diff-failed` -> BLOCKED. It
also now prints the diff base it used. A stale local main warns (widens, never falsely
greens). Also fixed `make -C ci test`, which was SWALLOWING failing suites.

Regression: ci/tests/touched-requirements-base.test.sh — 31 assertions over 8
throwaway-repo scenarios including the exact false-green case, an already-merged branch,
no base ref, a single-commit repo, bogus BASE_REF, a shallow GITHUB_BASE_REF, and
origin/main-preferred-over-stale-main.

=== NEW L0 STATIC LAYER (conformance/tests/l0-static/) ===
Where docs/14 actually puts these invariants. Each check.sh really executes offline via
`run.sh --layer l0` and is wired into ci/l0.sh, so it runs on every PR. No scaffolds.

R-SEC-01 no-plaintext-secrets (165 assertions / 445 tracked files): .sops.yaml holds only
PUBLIC age recipients; every committed Secret is SOPS-wrapped with a mac and ENC[...]
values; ansible vault.yml files are $ANSIBLE_VAULT; no secret-valued ansible key carries
a literal outside a vault; no PEM key / age secret key / kubeconfig client-key-data is
tracked; Flux declares sops decryption; the disallow-plaintext-secrets policy Enforces
and has a proving `result: fail` row. Exemptions are SELF-POLICING, not path allowlists —
a .example must be referenced by no kustomization and hold only placeholders, and the
Kyverno fixture is exempt ONLY while a test asserts the policy rejects it. Proven against
7 injected violations, including deleting the negative row and weakening Enforce->Audit.

R-SEC-02 least-privilege-rbac (41 assertions): no wildcard apiGroup/resource/verb and no
escalate|bind|impersonate in any in-repo Role/ClusterRole; no ServiceAccount bound to
cluster-admin or a wildcard role; the conformance runner's own test identity is scoped
and holds no rights over RBAC objects; the default-deny/host-namespace/egress-class
policies Enforce and each carry a negative row. Proven against 6 injected violations.

R-TEST-06 spec-immutability-enforced (16 assertions): drives the REAL
ci/spec-immutability.sh against HERMETIC throwaway spec repos and asserts its exit code
in every state — clean-at-pin 0, edits under docs/ or decisions/ 1, untracked addition 1,
unadopted drift 1, explicit pin bump 0, ABSENT spec repo 3 (honest skip, never green),
unpinned SPEC_PIN 1 — plus that gate.sh sets BLOCK=1 on its failure, so the mechanism is
load-bearing. The real spec repo is never written to.

Each check documents what it does NOT cover (runtime decryption, git history, runtime
least-privilege, branch protection) rather than overclaiming. status.py now reads
check.sh too, so a placeholder L0 body still outranks an `executable: true` annotation.

Coverage 37/51 -> 40/51 (11 gaps); executable tests 32 -> 35; scaffold-only stays 5 —
no sixth scaffold was added.

Serves: R-SEC-01, R-SEC-02, R-TEST-01..06.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-31 23:26:49 -05:00

322 lines
13 KiB
Python
Executable File

#!/usr/bin/env python3
"""Requirement-coverage report generator (R-TEST-04).
Maps EVERY R-* requirement in the authoritative registry to the Chainsaw test(s) that
verify it, and flags requirements with no verifying test as visible, blocking gaps.
Single source of truth for coverage is the per-requirement label on each Chainsaw Test:
metadata:
labels:
conformance.home.k3s/R-NET-01: "true"
Reads:
* requirements/registry.yaml -- the mirror of every spec R-* ID (see that file).
* tests/**/chainsaw-test.yaml -- discovers labels + the requirements annotation.
Writes (under --out, default _artifacts/coverage/):
* coverage.json -- machine-readable {requirement: [tests...]}, gaps, safety gaps.
* coverage.md -- human-readable table for PRs / CI logs.
Exit status / gating (R-TEST-03 gates are *scoped*, not "all 51 always"):
0 always in plain report mode.
--strict HYGIENE gate (always-on, WS-TEST owns it): fails on tag/annotation
drift or tags absent from the registry. Does NOT fail merely because
a future-phase requirement has no test yet — those are visible gaps,
not harness defects.
--require IDs SCOPED completeness gate (merge gate): fails if any listed requirement
is uncovered, or a listed safety-critical requirement lacks an L3 test.
CI passes the requirement IDs the change touches.
--require-all FULL completeness gate (phase-exit / final): fails if ANY registry
requirement is uncovered or any safety-critical one lacks an L3 test.
--allow-gap IDs exempt specific IDs from the completeness gates (documented, out-of-
scope gaps such as R-ENV-02/R-GITOPS-01 owned by another workstream).
Pure standard library (no PyYAML) so the harness has no pip dependency: the registry and
the test label/annotation lines we care about are parsed with line-oriented regexes, and
the file formats are constrained (documented in registry.yaml / common.sh) to keep that
parse deterministic.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from pathlib import Path
LABEL_PREFIX = "conformance.home.k3s"
RID_RE = re.compile(r"R-[A-Z0-9]+-[0-9]+")
LABEL_LINE_RE = re.compile(
r"^\s*" + re.escape(LABEL_PREFIX) + r"/(R-[A-Z0-9]+-[0-9]+)\s*:\s*[\"']?true", re.I
)
# --------------------------------------------------------------------------- registry
def parse_registry(path: Path) -> list[dict]:
"""Parse the constrained registry.yaml (`- id:` blocks, single-line scalars)."""
reqs: list[dict] = []
cur: dict | None = None
for raw in path.read_text().splitlines():
line = raw.split("#", 1)[0] if raw.lstrip().startswith("#") else raw
m = re.match(r"^\s*-\s*id:\s*(\S+)", line)
if m:
if cur:
reqs.append(cur)
cur = {"id": m.group(1).strip(), "doc": "", "safety_critical": False,
"summary": ""}
continue
if cur is None:
continue
m = re.match(r"^\s+doc:\s*(.+?)\s*$", line)
if m:
cur["doc"] = m.group(1).strip().strip('"')
continue
m = re.match(r"^\s+safety_critical:\s*(true|false)\s*$", line)
if m:
cur["safety_critical"] = (m.group(1) == "true")
continue
m = re.match(r"^\s+summary:\s*(.+?)\s*$", line)
if m:
cur["summary"] = m.group(1).strip().strip('"')
if cur:
reqs.append(cur)
return reqs
# ------------------------------------------------------------------------------ tests
def parse_test(path: Path) -> dict:
"""Extract name, verified R-IDs (from labels), annotation list, and layer."""
text = path.read_text()
labels: set[str] = set()
for line in text.splitlines():
m = LABEL_LINE_RE.match(line)
if m:
labels.add(m.group(1).upper())
ann_reqs: set[str] = set()
m = re.search(r"^\s*" + re.escape(LABEL_PREFIX) + r"/requirements:\s*(.+)$",
text, re.M)
if m:
ann_reqs = {r.upper() for r in RID_RE.findall(m.group(1))}
layer = ""
m = re.search(r"^\s*" + re.escape(LABEL_PREFIX) + r"/layer:\s*[\"']?([LlAaMm0-9]+)",
text, re.M)
if m:
layer = m.group(1).upper()
name = ""
m = re.search(r"^\s*name:\s*(\S+)", text, re.M)
if m:
name = m.group(1)
# Layer inferred from path as a fallback / cross-check.
rel = str(path)
if not layer:
if "l0-static" in rel:
layer = "L0"
elif "l1-rendering" in rel:
layer = "L1"
elif "l2-behavioral" in rel:
layer = "L2"
elif "l3-fault-injection" in rel:
layer = "L3"
elif "/meta/" in rel:
layer = "META"
return {
"name": name,
"path": rel,
"labels": sorted(labels),
"annotation_requirements": sorted(ann_reqs),
"layer": layer,
}
def discover_tests(tests_dir: Path) -> list[dict]:
return [parse_test(p) for p in sorted(tests_dir.rglob("chainsaw-test.yaml"))]
# ----------------------------------------------------------------------------- report
def build(registry: list[dict], tests: list[dict]) -> dict:
by_req: dict[str, list[dict]] = {r["id"]: [] for r in registry}
unknown: list[tuple[str, str]] = [] # (rid, test path) not in registry
mismatches: list[dict] = [] # label set != annotation set
for t in tests:
lbl = set(t["labels"])
ann = set(t["annotation_requirements"])
if lbl != ann:
mismatches.append({"test": t["path"], "labels": sorted(lbl),
"annotation": sorted(ann)})
for rid in lbl:
if rid in by_req:
by_req[rid].append(t)
else:
unknown.append((rid, t["path"]))
reg_index = {r["id"]: r for r in registry}
covered, uncovered, safety_gaps = [], [], []
for rid, req in reg_index.items():
verifiers = by_req[rid]
if verifiers:
covered.append(rid)
else:
uncovered.append(rid)
if req["safety_critical"]:
has_l3 = any(v["layer"] == "L3" for v in verifiers)
if not has_l3:
safety_gaps.append(rid)
return {
"summary": {
"total_requirements": len(registry),
"covered": len(covered),
"uncovered": len(uncovered),
"coverage_pct": round(100.0 * len(covered) / max(1, len(registry)), 1),
"safety_critical_gaps": len(safety_gaps),
"annotation_label_mismatches": len(mismatches),
"unknown_requirement_tags": len(unknown),
"total_tests": len(tests),
},
"by_requirement": {
r["id"]: {
"doc": r["doc"],
"safety_critical": r["safety_critical"],
"summary": r["summary"],
"verified_by": [
{"name": v["name"], "path": v["path"], "layer": v["layer"]}
for v in by_req[r["id"]]
],
}
for r in registry
},
"uncovered": sorted(uncovered),
"safety_critical_gaps": sorted(safety_gaps),
"annotation_label_mismatches": mismatches,
"unknown_requirement_tags": [{"requirement": u[0], "test": u[1]} for u in unknown],
}
def render_md(rep: dict) -> str:
s = rep["summary"]
out: list[str] = []
out.append("# Requirement coverage report\n")
out.append(
f"- Requirements: **{s['total_requirements']}** | "
f"Covered: **{s['covered']}** ({s['coverage_pct']}%) | "
f"Uncovered: **{s['uncovered']}**\n"
f"- Safety-critical requirements missing an L3 fault-injection test: "
f"**{s['safety_critical_gaps']}**\n"
f"- Scaffolded tests: **{s['total_tests']}** | "
f"Annotation/label mismatches: **{s['annotation_label_mismatches']}** | "
f"Unknown tags: **{s['unknown_requirement_tags']}**\n"
)
out.append("\n> Phase 0: tests are scaffolds. A requirement is **satisfied** only "
"when its tagged test passes green against a cluster (R-TEST-03) — this "
"report tracks *scaffold* coverage, i.e. that a verifying test exists.\n")
out.append("\n## Coverage by requirement\n")
out.append("| Requirement | Safety | Verified by (layer) | Status |")
out.append("|---|---|---|---|")
for rid, info in rep["by_requirement"].items():
vs = info["verified_by"]
if vs:
cell = "<br>".join(f"`{v['name']}` ({v['layer']})" for v in vs)
status = "covered"
else:
cell = "—"
status = "**GAP**"
sc = "yes" if info["safety_critical"] else ""
out.append(f"| {rid} | {sc} | {cell} | {status} |")
if rep["uncovered"]:
out.append("\n## Uncovered requirements (gaps)\n")
for rid in rep["uncovered"]:
out.append(f"- {rid} — {rep['by_requirement'][rid]['summary']}")
if rep["safety_critical_gaps"]:
out.append("\n## Safety-critical requirements without an L3 fault-injection test "
"(R-TEST-05)\n")
for rid in rep["safety_critical_gaps"]:
out.append(f"- {rid}")
if rep["annotation_label_mismatches"]:
out.append("\n## Tests whose labels disagree with their `requirements` annotation\n")
for m in rep["annotation_label_mismatches"]:
out.append(f"- `{m['test']}` labels={m['labels']} annotation={m['annotation']}")
if rep["unknown_requirement_tags"]:
out.append("\n## Tags not present in the registry (typo or spec drift)\n")
for u in rep["unknown_requirement_tags"]:
out.append(f"- {u['requirement']} in `{u['test']}`")
return "\n".join(out) + "\n"
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
here = Path(__file__).resolve().parent.parent # conformance/
ap.add_argument("--registry", type=Path,
default=here / "requirements" / "registry.yaml")
ap.add_argument("--tests", type=Path, default=here / "tests")
ap.add_argument("--out", type=Path, default=here / "_artifacts" / "coverage")
ap.add_argument("--strict", action="store_true",
help="hygiene gate: fail on tag/annotation drift or unknown tags")
ap.add_argument("--require", default="",
help="comma/space list of R-IDs that MUST be covered (merge gate)")
ap.add_argument("--require-all", action="store_true",
help="every registry requirement MUST be covered (phase-exit gate)")
ap.add_argument("--allow-gap", default="",
help="comma/space list of R-IDs exempt from completeness gates")
args = ap.parse_args()
registry = parse_registry(args.registry)
tests = discover_tests(args.tests)
rep = build(registry, tests)
args.out.mkdir(parents=True, exist_ok=True)
(args.out / "coverage.json").write_text(json.dumps(rep, indent=2) + "\n")
(args.out / "coverage.md").write_text(render_md(rep))
s = rep["summary"]
print(f"[coverage] {s['covered']}/{s['total_requirements']} requirements have a "
f"scaffolded test ({s['coverage_pct']}%); {s['uncovered']} gap(s), "
f"{s['safety_critical_gaps']} safety-critical gap(s).")
print(f"[coverage] wrote {args.out/'coverage.md'} and {args.out/'coverage.json'}")
def split(v):
return {t.upper() for t in re.split(r"[,\s]+", v) if t}
exempt = split(args.allow_gap)
uncovered = set(rep["uncovered"])
safety_gaps = set(rep["safety_critical_gaps"])
rc = 0
# HYGIENE (always-on): things WS-TEST controls and must keep green.
if args.strict:
if rep["annotation_label_mismatches"] or rep["unknown_requirement_tags"]:
print("[coverage] STRICT(hygiene): FAILED — label/annotation drift or "
"tags not in the registry.", file=sys.stderr)
rc = 1
# SCOPED completeness (merge gate): only the requirements the change touches.
required = split(args.require)
if args.require_all:
required |= {r["id"] for r in registry}
required -= exempt
if required:
miss = sorted(required & uncovered)
sgap = sorted(required & safety_gaps)
if miss:
print(f"[coverage] REQUIRE: FAILED — required requirement(s) uncovered: "
f"{miss}", file=sys.stderr)
rc = 1
if sgap:
print(f"[coverage] REQUIRE: FAILED — required safety-critical requirement(s) "
f"lack an L3 fault-injection test: {sgap}", file=sys.stderr)
rc = 1
return rc
if __name__ == "__main__":
raise SystemExit(main())