#!/usr/bin/env python3
"""Pre-written Four Cs rubric (our own reconstruction; the creator's real rubric is not public). 20 items x 5 points.
Deterministic scorer + item descriptions (the LLM auditor is given only the descriptions). Usage: rubric.py <workspace> [--json]"""
import os, re, sys, json, glob
def rd(root, p):
try: return open(os.path.join(root, p), encoding="utf-8").read()
except Exception: return None
def first(root, names):
for n in names:
t = rd(root, n)
if t is not None: return n, t
return None, None
def words(t): return len(re.findall(r"\w+", t or ""))
def mdfiles(root, d): return sorted(glob.glob(os.path.join(root, d, "**", "*.md"), recursive=True))
LINK = re.compile(r"\[\[[^\]]+\]\]|\]\((?!https?:)[^)]+\.md[^)]*\)")
def items(root):
agents = rd(root, "AGENTS.md") or ""
notes = mdfiles(root, "notes")
proj = mdfiles(root, "projects")
def routes():
return re.findall(r"`([A-Za-z0-9_\-./]+)`", agents)
def routes_exist():
r = [x for x in routes() if "/" in x or x.endswith(".md")]
return len(r) > 0 and all(os.path.exists(os.path.join(root, x.rstrip("/"))) for x in r)
idx_n, idx = first(root, ["index.md", "notes/index.md"])
def idx_lists_all():
if not idx or not notes: return False
return all(os.path.basename(n)[:-3] in idx for n in notes if os.path.basename(n) != "index.md")
def no_orphans():
files = [n for n in notes if os.path.basename(n) != "index.md"]
if not files: return False
corpus = "\n".join(open(f).read() for f in glob.glob(os.path.join(root, "**", "*.md"), recursive=True))
return all(os.path.basename(n)[:-3] in corpus.replace(open(n).read(), "") for n in files)
cap_n, cap = first(root, ["capabilities.md"])
skills = mdfiles(root, "skills")
cad_n, cad = first(root, ["cadence.md"])
caps_listed = len(re.findall(r"^\s*(?:[-*]|\d+\.|#{2,3})\s+\S", cap or "", re.M))
audits = [f for f in glob.glob(os.path.join(root, "audits", "audit-*.md"))]
log = rd(root, "log.md") or ""
P = [
("C1","about-me.md exists with at least 120 words", lambda: words(rd(root,"about-me.md"))>=120),
("C2","about-me.md states role, location and an hourly rate", lambda: all(re.search(p,rd(root,"about-me.md") or "",re.I) for p in ["research","g[oö]teborg|gothenburg|sweden","rate|SEK|hour"])),
("C3","priorities.md exists listing at least 3 bullet items", lambda: len(re.findall(r"^\s*[-*]\s+\S",rd(root,"priorities.md") or "",re.M))>=3),
("C4","about-business.md exists with at least 80 words", lambda: words(rd(root,"about-business.md"))>=80),
("C5","AGENTS.md exists and contains a routing map with at least 4 routes", lambda: len(routes())>=4 and re.search(r"rout",agents,re.I) is not None),
("N1","every path in the AGENTS.md routing map exists on disk", routes_exist),
("N2","at least half of the files in notes/ contain a link to another markdown file or a [[wikilink]]", lambda: len(notes)>0 and sum(1 for n in notes if LINK.search(open(n).read()))*2>=len(notes)),
("N3","every file in projects/ links to at least one note, person or client file", lambda: len(proj)>0 and all(LINK.search(open(p).read()) for p in proj)),
("N4","an index.md exists that lists every note in notes/ by name", idx_lists_all),
("N5","no orphan notes: every file in notes/ is mentioned by some other markdown file", no_orphans),
("P1","capabilities.md exists listing at least 3 repeatable tasks", lambda: caps_listed>=3),
("P2","each capability states a 'Trigger:' and an 'Output:'", lambda: caps_listed>0 and len(re.findall(r"Trigger:",cap or ""))>=3 and len(re.findall(r"Output:",cap or ""))>=3),
("P3","at least one file in skills/ contains a numbered procedure of 3+ steps", lambda: any(len(re.findall(r"^\s*\d+\.\s+\S",open(s).read(),re.M))>=3 for s in skills)),
("P4","AGENTS.md mentions capabilities.md or skills/", lambda: re.search(r"capabilities\.md|skills/",agents) is not None),
("P5","a templates/ directory contains at least one markdown template", lambda: len(mdfiles(root,"templates"))>=1),
("D1","cadence.md exists with at least two of daily/weekly/monthly sections", lambda: sum(1 for k in ["daily","weekly","monthly"] if re.search(k,cad or "",re.I))>=2),
("D2","cadence.md or a checklist file has a weekly review checklist with at least 4 checkbox items", lambda: len(re.findall(r"- \[ \]",cad or ""))>=4),
("D3","audits/ contains a dated audit report named audit-YYYY-MM-DD.md", lambda: any(re.match(r"audit-\d{4}-\d{2}-\d{2}\.md",os.path.basename(a)) for a in audits)),
("D4","log.md exists with at least one dated entry (YYYY-MM-DD)", lambda: re.search(r"\d{4}-\d{2}-\d{2}",log) is not None),
("D5","cadence.md states a next review date (YYYY-MM-DD)", lambda: re.search(r"next review[^\n]*\d{4}-\d{2}-\d{2}",cad or "",re.I) is not None),
]
return P
def score(root):
res = {}
for iid, desc, fn in items(root):
try: ok = bool(fn())
except Exception: ok = False
res[iid] = ok
return res
if __name__ == "__main__":
r = score(sys.argv[1])
tot = sum(5 for v in r.values() if v)
if "--json" in sys.argv: print(json.dumps({"score": tot, "items": r}))
else:
for iid, desc, _ in items(sys.argv[1]): print(("PASS " if r[iid] else "FAIL ")+iid+" "+desc)
print("SCORE", tot, "/100")