Activate NK-WP-0009/0011; add tutorials slice and proposed ADR-0009
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run

- docs/tutorials: template, OpenBao and SSH tutorials (unexercised)
- tools/tutorial-verify + make tutorials-verify (NK-WP-0009-T06)
- ADR-0009 proposed: expanded-mode Keycloak trigger and topology

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 295952@bnt-lap001
Assistant-Session: e93f64ad-516c-46eb-9666-aad8d300c477
This commit is contained in:
tegwick 2026-09-28 23:31:48 +02:00
parent 5c4bc16706
commit 0d460e3c02
11 changed files with 469 additions and 8 deletions

View file

@ -0,0 +1,48 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import tutorial_verify as tv # noqa: E402
ROOT = Path(__file__).resolve().parents[3]
GOOD = (ROOT / "docs/tutorials/TEMPLATE.md").read_text()
def write(tmp_path, text):
p = tmp_path / "t.md"
p.write_text(text)
return p
def test_real_tutorials_pass():
for p in (ROOT / "docs/tutorials").glob("*.md"):
if p.name in ("README.md", "TEMPLATE.md"):
continue
assert tv.check(p, ROOT) == [], p.name
def test_missing_status(tmp_path):
errs = tv.check(write(tmp_path, GOOD.replace("Exercise status: unexercised\n", "")), ROOT)
assert any("Exercise status" in e for e in errs)
def test_missing_rollback(tmp_path):
errs = tv.check(write(tmp_path, GOOD.replace("## Rollback", "## Other")), ROOT)
assert "missing section: Rollback" in errs
def test_step_without_owner(tmp_path):
errs = tv.check(write(tmp_path, GOOD.replace("**[owner: <repo>]** ", "")), ROOT)
assert any("owner tag" in e for e in errs)
def test_secret_marker(tmp_path):
errs = tv.check(write(tmp_path, GOOD + "\nhvs.ABCDEFGHIJKLMNOP\n"), ROOT)
assert "contains secret-looking marker" in errs
def test_retired_endpoint_and_missing_path(tmp_path):
txt = GOOD + "\nOpen https://bao.coulomb.social now. See `docs/nope.md`.\n"
errs = tv.check(write(tmp_path, txt), ROOT)
assert any("bao.coulomb.social" in e for e in errs)
assert "references missing path: docs/nope.md" in errs

View file

@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Structural verifier for docs/tutorials (NK-WP-0009-T06).
Fails a tutorial that is prose-only: missing required sections, missing or
invalid exercise status, no per-step owner tags, retired endpoints, secret
markers, or references to repo paths that do not exist.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
REQUIRED_SECTIONS = [
"Outcome", "Prerequisites", "Architecture context", "Steps",
"Verification", "Rollback", "Threat checks", "Ownership notes",
]
STATUS_RE = re.compile(
r"^Exercise status: (unexercised|exercised \d{4}-\d{2}-\d{2} by \S+)\s*$", re.M)
OWNER_TAG_RE = re.compile(r"\*\*\[owner: [^\]]+\]\*\*")
SECRET_RE = re.compile(
r"(hvs\.[A-Za-z0-9]{8,}|s\.[A-Za-z0-9]{24}|-----BEGIN [A-Z ]*PRIVATE KEY|otpauth://)")
PATH_RE = re.compile(r"`((?:docs|tools|canon|workplans)/[\w./-]+)`")
RETIRED_MENTION_OK = "retired"
def check(path: Path, root: Path) -> list[str]:
text = path.read_text()
errs: list[str] = []
if not STATUS_RE.search(text):
errs.append("missing or invalid 'Exercise status:' header")
headings = set(re.findall(r"^## (.+?)\s*$", text, re.M))
for s in REQUIRED_SECTIONS:
if s not in headings:
errs.append(f"missing section: {s}")
steps = re.search(r"^## Steps\s*$(.*?)(?=^## |\Z)", text, re.M | re.S)
if steps:
items = re.findall(r"^\d+\. .*$", steps.group(1), re.M)
if not items:
errs.append("Steps has no numbered steps")
for i in items:
if not OWNER_TAG_RE.search(i):
errs.append(f"step lacks owner tag: {i[:50]}")
ver = re.search(r"^## Verification\s*$(.*?)(?=^## |\Z)", text, re.M | re.S)
if ver and "Done when" not in ver.group(1):
errs.append("Verification lacks a 'Done when' outcome")
for line in text.splitlines():
if "bao.coulomb.social" in line and RETIRED_MENTION_OK not in line:
errs.append("references bao.coulomb.social without marking it retired")
if SECRET_RE.search(text):
errs.append("contains secret-looking marker")
for ref in PATH_RE.findall(text):
if "<" in ref or "*" in ref:
continue
if not (root / ref).exists():
errs.append(f"references missing path: {ref}")
return errs
def main(argv: list[str]) -> int:
root = Path(__file__).resolve().parents[2]
tdir = Path(argv[1]) if len(argv) > 1 else root / "docs" / "tutorials"
files = sorted(p for p in tdir.glob("*.md") if p.name not in ("README.md", "TEMPLATE.md"))
if not files:
print("no tutorials found", file=sys.stderr)
return 1
failed = 0
for f in files:
errs = check(f, root)
print(f"{'FAIL' if errs else 'ok '} {f.name}")
for e in errs:
print(f" - {e}")
failed += bool(errs)
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main(sys.argv))