feat: contradict an incomplete hand-reconciliation, not just a forgotten one

Slice 4.2. The receipt duty from ADR-0024 was built yesterday on the
premise that "the error is forgetting, not doing it wrong". The same
upgrade falsified that on the same day: the receipt for schema.yaml was
written, it is truthful, it names correctly what was carried over - and two
fields were lost anyway. Nobody forgot anything.

pruefe_upstream_drift.py now holds our schema.yaml structurally against the
baseline: every type, field, enum value and required entry the baseline
carries must exist here. Adding is allowed, losing is not. Checked
counterfactually against the real loss, which it reports.

For the two other extended files there is no equivalent - they are Python,
and no structural comparison exists. That half stays open and is written
down rather than glossed over.

Building it taught the check two things about itself. It broke the existing
selftest because that fixture wrote schema.yaml as prose - the same
unrealistic fixture FB-12 already records as having made an assertion
worthless for this very script. And my counter-control reported "nothing"
for the removed crash guard, because a crash returns the same exit code as
a failure; only checking for a traceback showed the script never reaches
its summary without it. Both times the control was blunt, not the check.

Sixteen assertions, six deliberate breaks, none uncovered.

The occasion is recorded for the next harvest as two stolpersteine pages -
the first use of the mechanism ADR-0009 decided: a receipt certifies
attention rather than completeness, and a file cannot be authoritative and
immutable at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2Q4Ri8NGwyTZzScvKnWFM
This commit is contained in:
Thore Cimbal
2026-08-21 12:00:00 +00:00
co-authored by Claude Opus 5
parent 0a9543703d
commit fe5286c420
7 changed files with 309 additions and 5 deletions
+130 -2
View File
@@ -51,6 +51,11 @@ import sys
import tempfile
from pathlib import Path
try:
import yaml
except ImportError: # pragma: no cover
sys.exit("pruefe_upstream_drift.py braucht PyYAML: pip install pyyaml")
UPSTREAM = "docs/sources/upstream"
BASELINE = f"{UPSTREAM}/neckbeard-v0.3.1"
MARKE = "<!-- projektabschnitt -->"
@@ -88,6 +93,23 @@ NICHT_UEBERNOMMEN = (
# das über den Präfix-Vergleich läuft.
SONDERFALL = (HERKUNFT, ABGLEICH, "AGENTS.md")
# Fixture fuer den Selbsttest: echtes, minimales YAML in der Form, die die
# Deckungspruefung erwartet.
SCHEMA_FIXTURE = """link_fields: [related, judged]
types:
wiki-page:
required: [type, area]
fields:
status: { enum: [open, harvested] }
harvested_in: { kind: str }
"""
# Unsere Fassung: alles aus der Baseline plus eigene Zusaetze - ein vierter
# Enum-Wert und ein eigener Typ. Beides darf NICHT gemeldet werden.
SCHEMA_FIXTURE_UNSER = SCHEMA_FIXTURE.replace(
"{ enum: [open, harvested] }", "{ enum: [open, harvested, declined] }"
) + " eigener_typ:\n required: [type]\n"
def baseline_dateien(base: Path) -> list[str]:
return sorted(str(p.relative_to(base)) for p in base.rglob("*") if p.is_file())
@@ -193,6 +215,68 @@ def pruefe_abgleich(root: Path, fehler: list[str]) -> None:
f"sie nicht. Nachziehen und dort eintragen.")
def pruefe_schema_deckung(root: Path, base: Path, fehler: list[str]) -> None:
"""Deckt unsere schema.yaml noch alles ab, was die Baseline fuehrt?
⚠️ Anlass: Beim Upgrade auf v0.3.1 wurde der Handabgleich quittiert —
wahrheitsgemaess — und verlor trotzdem zwei Felder (wiki-page.status,
wiki-page.harvested_in). Die Quittungspflicht aus ADR-0024 fragt, OB
nachgezogen wurde, nicht OB VOLLSTAENDIG. Sie wurde in der Annahme
gebaut, der Fehler sei Vergessen und nicht Falschmachen; dieselbe
Aufgabe hat das an ihrem ersten Tag widerlegt.
Diese Pruefung schliesst die Luecke nur fuer schema.yaml, weil YAML
strukturell vergleichbar ist. Fuer die beiden Python-Dateien gibt es
keine Entsprechung — das bleibt offen und ist als Muster notiert.
Erlaubt ist Hinzufuegen (wir erweitern erklaert, ADR-0024), verboten
ist Verlieren.
"""
eigen_pfad, basis_pfad = root / "schema.yaml", base / "schema.yaml"
if not (eigen_pfad.is_file() and basis_pfad.is_file()):
fehler.append("schema.yaml fehlt in Arbeitskopie oder Baseline")
return
try:
eigen = yaml.safe_load(eigen_pfad.read_text(encoding="utf-8")) or {}
basis = yaml.safe_load(basis_pfad.read_text(encoding="utf-8")) or {}
except yaml.YAMLError as ex:
fehler.append(f"schema.yaml nicht lesbar: {str(ex)[:80]}")
return
# Ein Schema, das keine Abbildung ist, ist ein Fehler - kein Absturz und
# erst recht kein stilles Bestanden.
if not isinstance(eigen, dict) or not isinstance(basis, dict):
fehler.append("schema.yaml ist keine YAML-Abbildung "
"(Arbeitskopie oder Baseline)")
return
for schluessel in ("link_fields", "vendored"):
fehlt = set(basis.get(schluessel) or []) - set(eigen.get(schluessel) or [])
if fehlt:
fehler.append(f"schema.yaml: {schluessel} verliert {sorted(fehlt)} "
f"gegenueber {base.name}")
for typ, spec in (basis.get("types") or {}).items():
eigener = (eigen.get("types") or {}).get(typ)
if eigener is None:
fehler.append(f"schema.yaml: Typ '{typ}' aus {base.name} fehlt")
continue
for teil in ("fields", "required", "rules"):
hier = eigener.get(teil) or ([] if teil != "fields" else {})
dort = spec.get(teil) or ([] if teil != "fields" else {})
fehlt = set(dort) - set(hier)
if fehlt:
fehler.append(f"schema.yaml: {typ}.{teil} verliert "
f"{sorted(fehlt)} gegenueber {base.name}")
for feld, regel in (spec.get("fields") or {}).items():
werte = (regel or {}).get("enum")
unser = ((eigener.get("fields") or {}).get(feld) or {}).get("enum")
if werte and unser is not None:
fehlt = set(werte) - set(unser)
if fehlt:
fehler.append(f"schema.yaml: {typ}.{feld} Enum verliert "
f"{sorted(fehlt)} gegenueber {base.name}")
def pruefe(root: Path) -> list[str]:
base = root / BASELINE
fehler: list[str] = []
@@ -200,6 +284,7 @@ def pruefe(root: Path) -> list[str]:
pruefe_agents(root, base, fehler)
pruefe_vollstaendigkeit(base, fehler)
pruefe_abgleich(root, fehler)
pruefe_schema_deckung(root, base, fehler)
return fehler
@@ -241,6 +326,11 @@ def selbsttest() -> int:
for datei in ERWEITERT + NICHT_UEBERNOMMEN:
(b / datei).parent.mkdir(parents=True, exist_ok=True)
(b / datei).write_text("upstream-stand\n", encoding="utf-8")
# ⚠️ schema.yaml MUSS echtes YAML sein. Eine Prosa-Fixture laesst
# die Deckungspruefung abstuerzen statt pruefen - dieselbe Klasse,
# die bei diesem Skript schon einmal eine Zusicherung wertlos
# gemacht hat (FB-12, Punkt 3).
(b / "schema.yaml").write_text(SCHEMA_FIXTURE, encoding="utf-8")
(root / "AGENTS.md").write_text(f"upstream\n{MARKE}\neigenes\n",
encoding="utf-8")
for arbeit, _ in PAARE:
@@ -249,6 +339,8 @@ def selbsttest() -> int:
for datei in ERWEITERT:
(root / datei).parent.mkdir(parents=True, exist_ok=True)
(root / datei).write_text("unser erweiterter stand\n", encoding="utf-8")
(root / "schema.yaml").write_text(SCHEMA_FIXTURE_UNSER,
encoding="utf-8")
pruefe_dass("ein sauberer Stand meldet nichts", pruefe(root) == [])
@@ -268,7 +360,8 @@ def selbsttest() -> int:
any(f.startswith("UNZUGEORDNET:") for f in pruefe(root)))
(neu / "scripts" / "fremd.py").unlink()
(neu / ERWEITERT[0]).write_text("upstream hat angefasst\n", encoding="utf-8")
(neu / ERWEITERT[0]).write_text(SCHEMA_FIXTURE + "# oben angefasst\n",
encoding="utf-8")
pruefe_dass("ohne Quittungsdatei ist eine oben geaenderte erweiterte "
"Datei ein Fehler, kein Skip",
any(ABGLEICH in f and "fehlt" in f for f in pruefe(root)))
@@ -292,11 +385,46 @@ def selbsttest() -> int:
pruefe_dass("dieselbe Aenderung ist still, sobald ABGLEICH.tsv quittiert",
not any(f.startswith("ABGLEICH:") for f in pruefe(root)))
(neu / ERWEITERT[0]).write_text("upstream-stand\n", encoding="utf-8")
(neu / ERWEITERT[0]).write_text(SCHEMA_FIXTURE, encoding="utf-8")
(neu / ABGLEICH).unlink()
pruefe_dass("ohne Aenderung oben wird ueberhaupt keine Quittung verlangt",
not any(ABGLEICH in f for f in pruefe(root)))
shutil.rmtree(alt)
# --- Deckung der erklaert-erweiterten schema.yaml -------------------
# Anlass: Der Handabgleich beim v0.3.1-Upgrade wurde quittiert und
# verlor trotzdem zwei Felder. Die Quittungspflicht fragt OB, nicht
# OB VOLLSTAENDIG.
eigen = root / "schema.yaml"
eigen.write_text(SCHEMA_FIXTURE_UNSER.replace(
" harvested_in: { kind: str }\n", ""), encoding="utf-8")
pruefe_dass("ein verlorenes Feld der Baseline wird gemeldet",
any("wiki-page.fields verliert" in f for f in pruefe(root)))
eigen.write_text(SCHEMA_FIXTURE_UNSER.replace(
"[open, harvested, declined]", "[open, declined]"), encoding="utf-8")
pruefe_dass("ein verlorener Enum-Wert wird gemeldet",
any("Enum verliert" in f for f in pruefe(root)))
eigen.write_text(SCHEMA_FIXTURE_UNSER.replace(
" wiki-page:", " andere-seite:"), encoding="utf-8")
pruefe_dass("ein verlorener Typ wird gemeldet",
any("Typ 'wiki-page'" in f for f in pruefe(root)))
eigen.write_text(SCHEMA_FIXTURE_UNSER.replace(
"link_fields: [related, judged]", "link_fields: [related]"),
encoding="utf-8")
pruefe_dass("ein verlorenes link_fields wird gemeldet",
any("link_fields verliert" in f for f in pruefe(root)))
eigen.write_text("nur prosa, kein schema\n", encoding="utf-8")
befunde = pruefe(root)
pruefe_dass("ein Schema ohne Abbildung ist ein Fehler, kein Absturz",
any("keine YAML-Abbildung" in f for f in befunde))
eigen.write_text(SCHEMA_FIXTURE_UNSER, encoding="utf-8")
pruefe_dass("eigene Zusaetze - vierter Enum-Wert, eigener Typ - sind still",
pruefe(root) == [])
pruefe_dass("mit nur einer Baseline gibt es nichts zu vergleichen",
not any(f.startswith("ABGLEICH:") for f in pruefe(root)))