Files
Maven/docs/capabilities/build_ledger.py
T
claude 8153e5eaa5 Classify every gap and rank the work (V-725)
Session 2 step 3, and the end of the explanation half.

gaps.md compares responsibilities and never package names. Eight classes. The
four capability classes are derived from the ledger's gap_class field and
rebuild with build_ledger.py. The four architecture classes are read from
findings.md and invariants.md, and every entry names the capability or invariant
it affects. An entry naming neither is marked non-blocking cleanup in those
words, which is the whole of class 8 and its eleven rows.

Of 46 v1 capabilities: 5 missing, 21 partial and reachable, 9 built and
unreachable, 11 reachable and unverified.

The nine unreachable ones are seven config blocks and two compose entries. Not
one is a code defect.

The ranked list puts phrasing first: speak-as-herself fails all three criteria,
and everything that asks the resident model to write a Russian sentence inherits
that. His own name not being stored is second. Nine capabilities one config
change from reachable is fourth, and it is the highest ratio of capability to
work in the list.

Items 10, 12 and 13 stall on unresolved invariants and are the owner's call, not
work.

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

560 lines
22 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"
# 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'")
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())