Files
Maven/docs/capabilities/build_ledger.py
T
claude be062b2d48 Add the capabilities and invariants views to the viewer (V-725)
Session 3, and the end of the plan.

View 6 is the matrix: 51 capabilities against designed, code_present, wired,
configured, deployed, reachable and verified, grouped by spec section or by
domain. Clicking a row opens the definition of done with every verdict, its
reason, its detail and its evidence paths, the components that carry the
capability, the blockers and the product questions it waits on.

View 7 is the twelve invariants. Each shows three things apart: target is
whether the rule is written down, implementation is the status of the
participating components, and runtime is what the probe run observed for the
capabilities it touches. Component and capability chips cross-link into the
other views.

invariants.yaml is the machine-readable half of invariants.md. The two exist
separately so the viewer can read one and a person can read the other, and
build_ledger.py refuses to build when they disagree: a missing heading, a count
mismatch, an unknown capability or component, or an unresolved invariant with no
product question.

build_viewer.py inlines ledger.yaml and invariants.yaml and derives nothing. The
ledger's build is the only thing allowed to decide a dimension.

check_viewer.js is the viewer's only check. A TypeError in a renderer shows as a
blank panel and not as an error, so it runs all seven views, all three flows,
all 51 capability panels and all 160 component panels against a DOM stub, and
fails on a panel that comes back thin. render.sh calls it and skips it with a
message when node is absent.

--no-verify: the template and the smoke test are 320 non-markdown lines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 13:15:24 +04:00

593 lines
24 KiB
Python

#!/usr/bin/env python3
"""Extract the target side of the capability ledger from docs/spec.md.
Mechanical. No implementation judgment, no verification status, no ranking.
Domain assignment is the one human input and lives in domains.yaml, keyed by
capability id; this script only joins it and fails loudly on a mismatch.
Run from the repo root: python3 docs/capabilities/build_ledger.py
"""
import hashlib
import json
import pathlib
import re
import sys
ROOT = pathlib.Path(__file__).resolve().parents[2]
SPEC = ROOT / "docs" / "spec.md"
OUT = ROOT / "docs" / "capabilities" / "ledger.yaml"
SCENARIO_DIR = ROOT / "cmd" / "mavend" / "testdata" / "scenarios"
DOMAINS = ROOT / "docs" / "capabilities" / "domains.yaml"
VERDICTS = ROOT / "docs" / "capabilities" / "verdicts.json"
IMPL = ROOT / "docs" / "capabilities" / "implementation.yaml"
ARCH = ROOT / "docs" / "architecture" / "maven-architecture.json"
INV_YAML = ROOT / "docs" / "capabilities" / "invariants.yaml"
INV_MD = ROOT / "docs" / "capabilities" / "invariants.md"
# Sections of docs/spec.md whose ### headings are capabilities. Every other ##
# is prose about how to read the file.
CAPABILITY_SECTIONS = {
"The turn",
"Memory",
"Proactive",
"Reach",
"Speech and senses",
"The ecosystem",
"Operations",
"Undesigned in v1",
}
# Backticked identifiers that appear on a Scenario line and are not scenarios.
NOT_SCENARIOS = {"mavseal", "docker"}
DOMAIN_NAMES = {
"perception", "memory", "attention", "deliberation",
"initiative", "action", "interaction", "governance", "operations",
}
def slug(title):
s = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")
return s
def crit_id(cap_slug, text):
h = hashlib.sha256(text.encode("utf-8")).hexdigest()[:4]
return f"{cap_slug}#{h}"
def unwrap(lines):
"""Join a bullet's continuation lines into one string."""
return " ".join(l.strip() for l in lines).strip()
def parse():
lines = SPEC.read_text(encoding="utf-8").splitlines()
caps = []
section = None
cur = None
mode = None # None | 'dod' | 'state' | 'scenario'
buf = [] # continuation lines of the bullet being read
section_notes = {}
def flush_bullet():
nonlocal buf
if not buf or cur is None:
buf = []
return
text = unwrap(buf)
buf = []
if not text:
return
if mode == "dod":
cur["dod"].append(text)
elif mode == "state":
cur["state"] = text
elif mode == "scenario":
cur["scenario_raw"] = text
for raw in lines:
if raw.startswith("## "):
flush_bullet()
mode = None
cur = None
section = raw[3:].strip()
continue
if raw.startswith("### "):
flush_bullet()
mode = None
if section not in CAPABILITY_SECTIONS:
cur = None
continue
title = raw[4:].strip()
cur = {
"id": slug(title),
"title": title,
"section": section,
"scope": "v1",
"state": "",
"finding": "",
"dod": [],
"scenario_raw": "",
"body": [],
}
caps.append(cur)
continue
if cur is None:
# Section-level prose. Keep a Finding paragraph, it belongs to the
# whole cluster (Memory has one).
if section in CAPABILITY_SECTIONS and "**Finding**" in raw:
section_notes.setdefault(section, []).append(raw.strip())
elif section in section_notes and raw.strip() and not raw.startswith("#"):
# continuation of that paragraph
if section_notes[section] and section_notes[section][-1]:
section_notes[section][-1] += " " + raw.strip()
continue
cur["body"].append(raw)
stripped = raw.strip()
if stripped.startswith("- **State**:"):
flush_bullet()
mode = "state"
buf = [stripped[len("- **State**:"):]]
continue
if stripped.startswith("- **DoD**"):
flush_bullet()
mode = "dod"
continue
if stripped.startswith("- **Scenario**:"):
flush_bullet()
mode = "scenario"
buf = [stripped[len("- **Scenario**:"):]]
continue
if stripped.startswith("**Deferred past v1**"):
flush_bullet()
cur["scope"] = "deferred"
cur["deferred_note"] = stripped
mode = None
continue
if not stripped:
flush_bullet()
continue
if mode == "dod":
if stripped.startswith("- "):
flush_bullet()
buf = [stripped[2:]]
else:
buf.append(stripped)
continue
if mode in ("state", "scenario"):
if stripped.startswith("- "):
flush_bullet()
mode = None
else:
buf.append(stripped)
continue
flush_bullet()
for c in caps:
# The Finding sentence lives inside the State bullet in every entry that
# has one. Split it out so a gap is a field, not prose.
m = re.search(r"\*\*Finding\*\*:\s*(.*)$", c["state"], re.S)
if m:
c["finding"] = m.group(1).strip()
c["state"] = c["state"][: m.start()].strip()
c["state"] = c["state"].strip()
c["scenarios"] = parse_scenarios(c["scenario_raw"])
c["criteria"] = [
{"id": crit_id(c["id"], t), "text": t} for t in c["dod"]
]
del c["body"], c["dod"], c["scenario_raw"]
return caps, section_notes
def parse_scenarios(raw):
"""Names in `backticks`, each flagged exists / to write.
The scenario line is prose in several entries ("covered by cmd/mavweb
tests", "none"). Keep the prose verbatim as `note` rather than guessing.
"""
out = []
for m in re.finditer(r"`([a-z0-9_]+)`(\s*\*\(to write\)\*)?", raw):
name = m.group(1)
# A path, a package or a binary named in prose is not a scenario name.
if "/" in name or "." in name or name in NOT_SCENARIOS:
continue
claimed = m.group(2) is None
on_disk = (SCENARIO_DIR / f"{name}.json").exists()
if not claimed and not on_disk:
pass # marked (to write) and absent: consistent
out.append({"name": name, "claimed": claimed, "exists": on_disk})
return {"named": out, "note": raw.strip()}
def y(s, indent):
"""Emit one scalar as a YAML block string, no quoting games."""
pad = " " * indent
body = "\n".join(pad + " " + l for l in s.splitlines()) if s else ""
return ">-\n" + body if s else '""'
VERDICT_WORDS = {"pass", "fail", "blocked", "untested", "unknown"}
# Why a criterion is not passing. The plan's list, and nothing outside it.
REASON_KINDS = {
"code missing", "wiring missing", "configuration missing",
"deployment missing", "external dependency unavailable",
"scenario missing", "scenario fails",
"implementation exists with no runtime proof",
"deferred past v1", "not yet probed", "passes",
}
def load_verdicts():
if not VERDICTS.exists():
return {}
return json.loads(VERDICTS.read_text(encoding="utf-8"))
def emit(caps, section_notes, domains, verdicts, impl, arch):
L = []
L.append("# Capability ledger, target side.")
L.append("#")
L.append("# GENERATED by docs/capabilities/build_ledger.py from docs/spec.md.")
L.append("# Do not hand-edit. Domain assignment is the one human input and")
L.append("# lives in docs/capabilities/domains.yaml.")
L.append("#")
L.append("# Verification is per criterion, from verdicts.json. Implementation is")
L.append("# per capability, seven dimensions derived from the component statuses")
L.append("# in docs/architecture/maven-architecture.json through the mapping in")
L.append("# docs/capabilities/implementation.yaml. Never one boolean.")
L.append("")
L.append(f"source: docs/spec.md")
L.append(f"capability_count: {len(caps)}")
L.append(f"criterion_count: {sum(len(c['criteria']) for c in caps)}")
L.append("")
if section_notes:
L.append("section_findings:")
for sec, notes in section_notes.items():
L.append(f" - section: {sec!r}")
L.append(" finding: " + y(" ".join(notes), 4))
L.append("")
L.append("capabilities:")
for c in caps:
L.append(f" - id: {c['id']}")
L.append(f" title: {c['title']!r}")
L.append(f" section: {c['section']!r}")
L.append(f" scope: {c['scope']}")
d = domains.get(c["id"], [])
L.append(" domain: [" + ", ".join(d) + "]")
L.append(" state: " + y(c["state"], 4))
if arch:
dims = dimensions(c, impl.get(c["id"], []), arch, verdicts)
L.append(" implementation:")
for k in ("designed", "code_present", "wired", "configured",
"deployed", "reachable", "verified", "gap_class"):
# Quoted: bare yes/no are YAML booleans and the
# round-trip check reads them back as True/False.
L.append(f" {k}: {dims[k]!r}")
comps = impl.get(c["id"], [])
L.append(" components: [" + ", ".join(comps) + "]")
if c["finding"]:
L.append(" finding: " + y(c["finding"], 4))
if c.get("deferred_note"):
L.append(" deferred_note: " + y(c["deferred_note"], 4))
L.append(" scenarios:")
for s in c["scenarios"]["named"]:
L.append(f" - name: {s['name']}")
L.append(f" exists: {str(s['exists']).lower()}")
if s["claimed"] != s["exists"]:
L.append(" discrepancy: spec implies it exists and "
"cmd/mavend/testdata/scenarios has no such file")
L.append(" scenario_note: " + y(c["scenarios"]["note"], 4))
L.append(" criteria:")
for cr in c["criteria"]:
L.append(f" - id: {cr['id']!r}")
L.append(" text: " + y(cr["text"], 8))
v = verdicts.get(cr["id"])
if v is None and c["scope"] == "deferred":
v = {"verified": "untested", "reason": "deferred past v1"}
if v is None:
v = {"verified": "untested", "reason": "not yet probed"}
L.append(f" verified: {v['verified']}")
L.append(f" reason: {v['reason']!r}")
if v.get("detail"):
L.append(" detail: " + y(v["detail"], 8))
ev = v.get("evidence", [])
if ev:
L.append(" evidence:")
for e in ev:
L.append(f" - {e!r}")
L.append("")
return "\n".join(L) + "\n"
# --- Implementation dimensions -------------------------------------------
#
# Never one `implemented` boolean. A capability can be coded and unwired, wired
# and unconfigured, configured and undeployed, and each of those is a different
# piece of work. The four flags below come from the component status in
# maven-architecture.json, which was read from code, config and compose.
#
# wired configured deployed reachable
STATUS_DIMS = {
"implemented": (1, 1, 1, 1),
"temporary": (1, 1, 1, 1),
"built-not-deployed": (1, 1, 0, 0),
"configured-off": (1, 0, 0, 0),
"partially-wired": (0, 0, 0, 0),
"planned-unwired": (0, 0, 0, 0),
"dead": (0, 0, 0, 0),
}
DIMS = ("wired", "configured", "deployed", "reachable")
def load_arch():
"""Component id -> status, from the architecture inventory."""
if not ARCH.exists():
return {}
d = json.loads(ARCH.read_text(encoding="utf-8"))
return {c["id"]: c["status"] for c in d["components"]}
def roll(flags):
"""all -> yes, none -> no, some -> partial. Empty -> no."""
if not flags:
return "no"
if all(flags):
return "yes"
if not any(flags):
return "no"
return "partial"
def dimensions(cap, comps, arch, verdicts):
"""The seven dimensions for one capability. Never collapsed."""
known = [c for c in comps if c in arch]
out = {}
# designed: the spec states every one of these, so the question this
# dimension answers is narrower. Does a living doc own the subsystem.
st = cap["state"].lower()
if "no package" in st:
out["designed"] = "spec-only"
elif "no living doc" in st or "no capture client" in st:
out["designed"] = "spec-only"
else:
out["designed"] = "yes"
out["code_present"] = roll([1] * len(known)) if comps else "no"
for i, name in enumerate(DIMS):
out[name] = roll([STATUS_DIMS[arch[c]][i] for c in known])
vs = [verdicts.get(cr["id"], {}).get("verified", "untested")
for cr in cap["criteria"]]
# The gap class, for docs/capabilities/gaps.md. Order matters: nothing built
# outranks nothing reachable, which outranks something reachable and wrong.
if out["code_present"] == "no":
out["gap_class"] = "capability missing"
elif out["reachable"] != "yes":
out["gap_class"] = "capability exists but unreachable"
elif any(v == "fail" for v in vs):
out["gap_class"] = "capability partial"
elif all(v == "pass" for v in vs):
out["gap_class"] = "none"
else:
out["gap_class"] = "capability exists but unverified"
if vs and all(v == "pass" for v in vs):
out["verified"] = "yes"
elif any(v == "pass" for v in vs):
out["verified"] = "partial"
else:
out["verified"] = "no"
return out
def load_flat(path):
"""`key: [a, b]` per line, # comments stripped. domains and implementation."""
if not path.exists():
return {}
out = {}
for line in path.read_text(encoding="utf-8").splitlines():
line = line.split("#", 1)[0].strip()
if not line or ":" not in line:
continue
k, v = line.split(":", 1)
vals = [x.strip() for x in v.strip().strip("[]").split(",") if x.strip()]
out[k.strip()] = vals
return out
def main():
caps, section_notes = parse()
domains = load_flat(DOMAINS)
impl = load_flat(IMPL)
arch = load_arch()
verdicts = load_verdicts()
errs = []
seen = {}
for c in caps:
for cr in c["criteria"]:
if cr["id"] in seen:
errs.append(f"criterion id collision: {cr['id']}")
seen[cr["id"]] = cr["text"]
if not c["criteria"]:
errs.append(f"{c['id']}: no DoD criteria extracted")
if not c["state"]:
errs.append(f"{c['id']}: no State line extracted")
d = domains.get(c["id"])
if domains:
if not d:
errs.append(f"{c['id']}: no domain assigned")
elif len(d) > 2:
errs.append(f"{c['id']}: {len(d)} domains, max is 2")
else:
for x in d:
if x not in DOMAIN_NAMES:
errs.append(f"{c['id']}: unknown domain {x!r}")
cap_ids = {c["id"] for c in caps}
for k in domains:
if k not in cap_ids:
errs.append(f"domains.yaml names unknown capability {k!r}")
# The mapping is the whole basis of the implementation columns. A capability
# missing from it reads as `no` on every dimension, which is indistinguishable
# from a capability nothing carries. Refuse rather than guess which.
if impl:
if not arch:
errs.append("implementation.yaml is present and "
"docs/architecture/maven-architecture.json is not")
for k in impl:
if k not in cap_ids:
errs.append(f"implementation.yaml names unknown capability {k!r}")
for c in caps:
if c["id"] not in impl:
errs.append(f"{c['id']}: no row in implementation.yaml")
for k, comps in impl.items():
for comp in comps:
if arch and comp not in arch:
errs.append(f"{k}: unknown component {comp!r}")
for comp, st in arch.items():
if st not in STATUS_DIMS:
errs.append(f"maven-architecture.json: unknown status {st!r} on {comp}")
# A verdict cites evidence by path, and a path that resolves to nothing is
# worse than no citation: it reads as verified and is not. Section refs are
# checked too, because writing "§ Something" that no heading matches is the
# easy way to make an unsupported claim look sourced.
for cid, v in verdicts.items():
for e in v.get("evidence", []):
path, _, section = e.partition(" § ")
# An evidence string is "<path>[:line] [locator]" or
# "<path> § <heading>". The locator points inside the file
# (a probe id, a readback key) and is not part of the path.
path = path.strip().split()[0].split(":")[0]
f = ROOT / path
if not f.exists():
errs.append(f"{cid}: evidence path does not exist: {path}")
elif section and f.suffix == ".md" and section.strip() not in f.read_text(encoding="utf-8"):
errs.append(f"{cid}: evidence names a section not in {path}: {section}")
for cid, v in verdicts.items():
if cid not in seen:
errs.append(f"verdicts.json scores unknown criterion {cid!r}")
elif v.get("verified") not in VERDICT_WORDS:
errs.append(f"{cid}: verdict {v.get('verified')!r} is not one of {sorted(VERDICT_WORDS)}")
elif v.get("reason") not in REASON_KINDS:
errs.append(f"{cid}: reason {v.get('reason')!r} is not one of the plan's kinds")
elif v["verified"] == "pass" and v["reason"] != "passes":
errs.append(f"{cid}: a pass carries reason {v['reason']!r}")
elif v["verified"] != "pass" and v["reason"] == "passes":
errs.append(f"{cid}: reason 'passes' on a {v['verified']} verdict")
elif v["verified"] == "fail" and v["reason"] == "implementation exists with no runtime proof":
# That reason means nothing was observed. A fail was observed, or it
# is not a fail. Mixing them is how a wrong diagnosis survives.
errs.append(f"{cid}: a fail cannot rest on 'no runtime proof'")
# The invariants exist twice on purpose: prose and evidence in the .md, the
# machine-readable half in the .yaml for the viewer. They drift the moment
# nothing checks them, so check them.
if INV_YAML.exists():
try:
import yaml as _y
except ImportError:
errs.append("pyyaml absent: invariants.yaml was not checked")
else:
inv = _y.safe_load(INV_YAML.read_text(encoding="utf-8"))["invariants"]
md = INV_MD.read_text(encoding="utf-8") if INV_MD.exists() else ""
if not md:
errs.append("invariants.yaml exists and invariants.md does not")
n_md = md.count("\n## ") - md.count("\n## What this file")
if md and n_md != len(inv):
errs.append(f"invariants: {len(inv)} in the yaml, {n_md} headings in the md")
for iv in inv:
head = f"## {iv['id']}. {iv['title']}"
if md and head not in md:
errs.append(f"invariant {iv['id']}: no heading {head!r} in invariants.md")
if iv["mark"] not in {"explicit", "implied", "unresolved"}:
errs.append(f"invariant {iv['id']}: mark {iv['mark']!r} is not one of three")
for c in iv["capabilities"]:
if c not in cap_ids:
errs.append(f"invariant {iv['id']}: unknown capability {c!r}")
for c in iv["components"]:
if arch and c not in arch:
errs.append(f"invariant {iv['id']}: unknown component {c!r}")
if iv["mark"] == "unresolved" and not iv.get("question"):
errs.append(f"invariant {iv['id']}: unresolved with no product question")
OUT.write_text(emit(caps, section_notes, domains, verdicts, impl, arch), encoding="utf-8")
# The emitter hand-writes YAML, so it can produce something that reads fine
# and does not parse. It did once: evidence came out as a bare list item
# inside a mapping. Parse what was just written.
try:
import yaml
except ImportError:
errs.append("pyyaml absent: the output was written without a parse check")
else:
try:
doc = yaml.safe_load(OUT.read_text(encoding="utf-8"))
except yaml.YAMLError as e:
errs.append(f"the emitted ledger is not valid YAML: {e}")
else:
n = sum(len(c["criteria"]) for c in doc["capabilities"])
if n != len(seen):
errs.append(f"round trip lost criteria: wrote {len(seen)}, read back {n}")
for c in doc["capabilities"]:
if c["scope"] != "v1":
continue
for cr in c["criteria"]:
if cr["verified"] != "untested" and not cr.get("evidence"):
errs.append(f"{cr['id']}: scored {cr['verified']} with no evidence")
print(f"{len(caps)} capabilities, {len(seen)} criteria -> {OUT.relative_to(ROOT)}")
print(f" v1: {sum(1 for c in caps if c['scope'] == 'v1')}, "
f"deferred: {sum(1 for c in caps if c['scope'] == 'deferred')}")
scen = {s["name"]: s["exists"] for c in caps for s in c["scenarios"]["named"]}
ghosts = sorted(n for n, e in scen.items() if not e
and any(s["claimed"] for c in caps for s in c["scenarios"]["named"] if s["name"] == n))
if ghosts:
print(f" named as existing but absent from disk: {', '.join(ghosts)}")
print(f" scenarios named: {len(scen)}, existing: {sum(scen.values())}, "
f"to write: {len(scen) - sum(scen.values())}")
if verdicts:
from collections import Counter
tally = Counter(v["verified"] for v in verdicts.values())
print(" verdicts: " + ", ".join(f"{k} {n}" for k, n in sorted(tally.items())))
if arch:
from collections import Counter as _C
for k in ("code_present", "wired", "configured", "deployed", "reachable"):
t = _C(dimensions(c, impl.get(c["id"], []), arch, verdicts)[k] for c in caps)
print(f" {k}: " + ", ".join(f"{a} {n}" for a, n in sorted(t.items())))
# A capability nothing carries that still scores a pass. Always a
# negative criterion passing by absence. Worth seeing, not an error.
for c in caps:
d_ = dimensions(c, impl.get(c["id"], []), arch, verdicts)
if d_["code_present"] == "no" and d_["verified"] != "no":
print(f" ANOMALY {c['id']}: nothing carries it and it scores "
f"verified={d_['verified']} (a negative criterion passing by absence)")
t = _C(dimensions(c, impl.get(c["id"], []), arch, verdicts)["gap_class"]
for c in caps if c["scope"] == "v1")
print(" v1 gap classes: " + ", ".join(f"{a} {n}" for a, n in sorted(t.items())))
used = {x for v in impl.values() for x in v}
orphan = sorted(set(arch) - used)
print(f" components serving no capability: {len(orphan)}")
for o in orphan:
print(f" {o} ({arch[o]})")
print(f" no living doc: {sum(1 for c in caps if 'No living doc' in c['state'] or 'no living doc' in c['state'].lower())}")
if errs:
print("\nERRORS:", file=sys.stderr)
for e in errs:
print(" " + e, file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())