Files
Holden Salomon a9e8287065
test / test (macos-latest, 3.13) (push) Canceled after 0s
test / test (ubuntu-latest, 3.11) (push) Successful in 17s
test / test (ubuntu-latest, 3.12) (push) Successful in 18s
test / test (ubuntu-latest, 3.13) (push) Successful in 15s
ai-incidents 1.0.0
2026-09-21 17:48:40 +00:00

234 lines
7.9 KiB
Python

"""The ledger: a markdown file of incidents, ranked by what they cost.
``incidents.md`` is the source of truth and stays hand-editable. Each entry is
## <short title> · <YYYY-MM-DD> · <HIGH|MEDIUM|LOW>
- **What:** one sentence: what the agent did
- **Cost:** one sentence: what it actually cost
- **Lesson:** one actionable sentence: how not to repeat it
Everything above the first entry is the header and everything from the first non-entry ``##``
section after the entries (for example ``## Recurring patterns``) is the footer; both are kept
verbatim. Entry bodies are kept verbatim too, so a hand-written entry with extra lines survives a
run untouched. Only the order changes: most severe first, newest first within a severity.
``index.json`` is derived from ``incidents.md`` on every write and is never authored, so a confused
run can garble prose but cannot corrupt the index.
"""
from __future__ import annotations
import datetime as _dt
import hashlib
import json
import re
from dataclasses import dataclass, field
SEVERITIES = ("HIGH", "MEDIUM", "LOW")
SEV_RANK = {s: i for i, s in enumerate(SEVERITIES)}
SEP = " · "
HEAD = re.compile(
r"^##\s+(?P<title>.+?)\s+·\s+(?P<date>[^·]+?)\s+·\s+(?P<sev>HIGH|MEDIUM|LOW)\s*$"
)
ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
_RULE = re.compile(r"^\s*(-{3,}|\*{3,}|_{3,})\s*$")
DEFAULT_HEADER = """# {title}
Times AI-generated work caused a real problem: what it did, what it cost, and the lesson.
Most severe first; newest first within a severity. Written by `ai-incidents`; hand edits and
hand-written entries are kept.
"""
def fingerprint(title: str) -> str:
"""Stable identity for an incident: its title, normalised. Severity gets re-rated and dates get
corrected, but a retitled incident is a different incident."""
norm = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")
return hashlib.sha1(norm.encode()).hexdigest()[:12]
def one_line(s: str, limit: int = 1000) -> str:
"""Collapse whitespace. A newline in model output must never become a new heading."""
s = re.sub(r"\s+", " ", str(s or "")).strip()
return s[:limit]
def clean_title(s: str) -> str:
# The separator is structural; a title containing it would parse as a different entry.
return one_line(s, 160).replace("·", "-").lstrip("#").strip()
@dataclass
class Entry:
title: str
date: str
severity: str
body: list[str] = field(default_factory=list)
@property
def fp(self) -> str:
return fingerprint(self.title)
def heading(self) -> str:
return f"## {self.title}{SEP}{self.date}{SEP}{self.severity}"
def render(self) -> str:
return "\n".join([self.heading(), *self.body])
def sort_date(self) -> str:
m = re.match(r"\d{4}-\d{2}-\d{2}", self.date)
return m.group(0) if m else ""
def what(self) -> str:
for ln in self.body:
m = re.match(r"^\s*-\s*\*\*What:\*\*\s*(.*)$", ln)
if m:
return m.group(1).strip()
return ""
@classmethod
def new(cls, title, date, severity, what, cost, lesson) -> "Entry":
return cls(
clean_title(title),
date,
severity,
[f"- **What:** {one_line(what)}", f"- **Cost:** {one_line(cost)}", f"- **Lesson:** {one_line(lesson)}"],
)
@dataclass
class Ledger:
header: str = ""
entries: list[Entry] = field(default_factory=list)
footer: str = ""
def fps(self) -> set[str]:
return {e.fp for e in self.entries}
def _trim(lines: list[str]) -> list[str]:
"""Drop trailing blank lines and horizontal rules: they are separators, not content."""
while lines and (not lines[-1].strip() or _RULE.match(lines[-1])):
lines = lines[:-1]
return lines
def parse(text: str) -> Ledger:
lines = text.splitlines()
header: list[str] = []
entries: list[Entry] = []
footer: list[str] = []
cur: Entry | None = None
in_footer = False
for ln in lines:
m = HEAD.match(ln)
if in_footer:
if m:
# An entry after the footer started: treat it as an entry, keep the footer intact.
in_footer = False
else:
footer.append(ln)
continue
if m:
if cur:
cur.body = _trim(cur.body)
entries.append(cur)
cur = Entry(m.group("title").strip(), m.group("date").strip(), m.group("sev"))
elif ln.startswith("## ") and entries + ([cur] if cur else []):
if cur:
cur.body = _trim(cur.body)
entries.append(cur)
cur = None
in_footer = True
footer.append(ln)
elif cur is not None:
cur.body.append(ln)
else:
header.append(ln)
if cur:
cur.body = _trim(cur.body)
entries.append(cur)
return Ledger("\n".join(_trim(header)), entries, "\n".join(footer).rstrip())
def ordered(entries: list[Entry], order: str = "severity") -> list[Entry]:
# sorted() is stable, so entries that tie keep their existing relative order.
newest = sorted(entries, key=lambda e: e.sort_date(), reverse=True)
if order == "newest":
return newest
return sorted(newest, key=lambda e: SEV_RANK.get(e.severity, len(SEVERITIES)))
def render(ledger: Ledger, order: str = "severity") -> str:
parts = [ledger.header.rstrip()] if ledger.header.strip() else []
parts += [e.render() for e in ordered(ledger.entries, order)]
if ledger.footer.strip():
parts.append(ledger.footer.rstrip())
return "\n\n".join(parts) + "\n"
def add_entries(ledger: Ledger, new: list[Entry]) -> tuple[list[Entry], list[Entry]]:
"""Add entries whose fingerprint is not already present. Returns (added, duplicates)."""
have = ledger.fps()
added, dupes = [], []
for e in new:
if e.fp in have:
dupes.append(e)
continue
have.add(e.fp)
ledger.entries.append(e)
added.append(e)
return added, dupes
PATTERNS_HEADING = "## Recurring patterns"
def add_patterns(ledger: Ledger, patterns: list[str]) -> list[str]:
"""Append bullets to the Recurring patterns section, creating it if needed."""
pats = [one_line(p, 500) for p in patterns if one_line(p)]
existing = set(re.findall(r"^\s*-\s+(.*)$", ledger.footer, flags=re.M))
pats = [p for p in dict.fromkeys(pats) if p not in existing]
if not pats:
return []
bullets = "\n".join(f"- {p}" for p in pats)
footer = ledger.footer
if PATTERNS_HEADING in footer:
lines = footer.splitlines()
start = next(i for i, ln in enumerate(lines) if ln.strip() == PATTERNS_HEADING)
end = next((i for i in range(start + 1, len(lines)) if lines[i].startswith("## ") or _RULE.match(lines[i])), len(lines))
while end > start + 1 and not lines[end - 1].strip():
end -= 1
lines[end:end] = bullets.splitlines()
ledger.footer = "\n".join(lines)
else:
ledger.footer = (PATTERNS_HEADING + "\n\n" + bullets + ("\n\n" + footer if footer.strip() else "")).rstrip()
return pats
def index(ledger: Ledger, prior: dict | None, today: str) -> dict:
prior_by_fp = {i.get("fp"): i for i in (prior or {}).get("incidents", []) if isinstance(i, dict)}
items = []
for e in ordered(ledger.entries):
items.append({
"fp": e.fp,
"title": e.title,
"date": e.date,
"severity": e.severity,
"first_seen": prior_by_fp.get(e.fp, {}).get("first_seen", today),
})
counts = {"total": len(items)}
for s in SEVERITIES:
counts[s.lower()] = sum(1 for i in items if i["severity"] == s)
return {"generated": today, "counts": counts, "incidents": items}
def dump_index(data: dict) -> str:
return json.dumps(data, indent=2, ensure_ascii=False) + "\n"
def today() -> str:
return _dt.date.today().isoformat()