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
455 lines
20 KiB
Python
455 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
"""pruefe_upstream_drift.py — Byte-Vergleich gegen die gepinnte Baseline.
|
|
|
|
Schützt die übernommenen Framework-Dateien vor stillem Umschreiben
|
|
(Entscheidung 8 im Design 2026-08-11, Frage von sorb: „wird die
|
|
AGENTS.md ggf. durch Agenten umgeschrieben?"). Die Baseline liegt unter
|
|
docs/sources/upstream/neckbeard-v0.3.1/ (siehe HERKUNFT.md dort); ein
|
|
Framework-Upgrade aktualisiert Baseline und Arbeitskopie im selben,
|
|
bewussten Commit.
|
|
|
|
Prüfungen (Fehler, Exit 1):
|
|
1. byte-identische Paare laut PAARE
|
|
2. AGENTS.md beginnt byte-identisch mit der Baseline-AGENTS.md und
|
|
trägt direkt danach die Marke des Projektabschnitts
|
|
3. **Vollständigkeit:** jede Datei der Baseline ist genau einer Klasse
|
|
zugeordnet — byte-identisch, AGENTS-Präfix, erklärt erweitert
|
|
(ADR-0024) oder bewusst nicht übernommen. Eine vendorierte Datei,
|
|
die in keiner Liste steht, wird von nichts verglichen und fällt
|
|
sonst durchs Raster.
|
|
4. **Abgleich beim Upgrade:** hat upstream zwischen der vorigen und der
|
|
aktuellen Baseline eine erklärt-erweiterte Datei angefasst, muss die
|
|
ABGLEICH.tsv der aktuellen Baseline sie quittieren — eine Zeile
|
|
`<datei>\t<vorige-baseline>\t<was nachgezogen wurde>`.
|
|
|
|
Zu 4, weil es die ungewöhnlichste ist: Die erweiterten Dateien werden
|
|
absichtlich nicht byte-verglichen (ADR-0024), also meldet auch nichts,
|
|
wenn upstream sie ändert. Beim Upgrade v0.1.1 → v0.3.1 waren zwei von
|
|
drei betroffen, und gemeldet hat es nichts — sichtbar wurde es nur, weil
|
|
jemand daran dachte. Diese Prüfung kann nicht beurteilen, ob richtig
|
|
nachgezogen wurde; sie kann sagen, **dass** etwas nachzuziehen war. Der
|
|
Fehler ist Vergessen, nicht Falschmachen (FRAMEWORK-BEFUNDE, FB-12).
|
|
|
|
⚠️ Die Quittung steht bewusst in einer eigenen Datei und nicht in der
|
|
Prosa der HERKUNFT.md: Der erste Entwurf suchte den Dateinamen irgendwo im
|
|
Text — und der steht dort *immer*, in der Inventartabelle. Die Prüfung war
|
|
damit nicht streng, sondern wirkungslos, und ihr eigener Selbsttest hat es
|
|
verdeckt, weil sein Fixture eine leere Notiz war. Eine Zusicherung deckt
|
|
den Fall jetzt ab.
|
|
|
|
Fehlt eine Baseline-Datei, ist das ein Fehler, kein Skip
|
|
(Stillstandsprüfungs-Regel: eine Prüfung ohne Gegenseite ist ungeprüft).
|
|
|
|
Usage: python scripts/pruefe_upstream_drift.py [repo-root]
|
|
python scripts/pruefe_upstream_drift.py --selbsttest
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import shutil
|
|
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 -->"
|
|
HERKUNFT = "HERKUNFT.md"
|
|
ABGLEICH = "ABGLEICH.tsv"
|
|
|
|
# (Arbeitskopie, Baseline-Datei) — byte-identisch
|
|
PAARE = [
|
|
("CLAUDE.md", "CLAUDE.md"),
|
|
("WORKFLOW.md", "WORKFLOW.md"),
|
|
("docs/adr/template.md", "templates/adr-template.md"),
|
|
("docs/design/template.md", "templates/design-template.md"),
|
|
("docs/aar/template.md", "templates/aar-template.md"),
|
|
("docs/issues/template.md", "templates/issue-template.md"),
|
|
("docs/ledger/template.md", "templates/ledger-template.md"),
|
|
("docs/verdict/template.md", "templates/verdict-template.md"),
|
|
("scripts/check_harvest.py", "scripts/check_harvest.py"),
|
|
("scripts/judge.py", "scripts/judge.py"),
|
|
]
|
|
|
|
# Erklärt erweitert (ADR-0024): kein Byte-Vergleich, nur Diff-Referenz.
|
|
ERWEITERT = (
|
|
"schema.yaml",
|
|
"scripts/validate.py",
|
|
"scripts/gen_status.py",
|
|
)
|
|
|
|
# Vendoriert, aber bewusst nicht übernommen — es gibt keine Arbeitskopie.
|
|
# check_locked.py: pruefe_sperrliste.py tut dasselbe seit dem 2026-08-20.
|
|
NICHT_UEBERNOMMEN = (
|
|
"scripts/check_locked.py",
|
|
)
|
|
|
|
# Von der Zuordnung ausgenommen: die Herkunftsnotiz selbst und AGENTS.md,
|
|
# 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())
|
|
|
|
|
|
def version_schluessel(name: str) -> tuple:
|
|
return tuple(int(t) for t in re.findall(r"\d+", name)) or (0,)
|
|
|
|
|
|
def baselines(root: Path) -> list[Path]:
|
|
"""Alle Baselines, aelteste zuerst."""
|
|
verzeichnis = root / UPSTREAM
|
|
if not verzeichnis.is_dir():
|
|
return []
|
|
return sorted((p for p in verzeichnis.iterdir() if p.is_dir()),
|
|
key=lambda p: version_schluessel(p.name))
|
|
|
|
|
|
def pruefe_paare(root: Path, base: Path, fehler: list[str]) -> None:
|
|
for arbeit, original in PAARE:
|
|
a, b = root / arbeit, base / original
|
|
if not b.is_file():
|
|
fehler.append(f"Baseline fehlt: {BASELINE}/{original}")
|
|
elif not a.is_file():
|
|
fehler.append(f"Arbeitskopie fehlt: {arbeit}")
|
|
elif a.read_bytes() != b.read_bytes():
|
|
fehler.append(f"DRIFT: {arbeit} weicht von {BASELINE}/{original} ab")
|
|
|
|
|
|
def pruefe_agents(root: Path, base: Path, fehler: list[str]) -> None:
|
|
agents, agents_base = root / "AGENTS.md", base / "AGENTS.md"
|
|
if not agents_base.is_file():
|
|
fehler.append(f"Baseline fehlt: {BASELINE}/AGENTS.md")
|
|
return
|
|
if not agents.is_file():
|
|
fehler.append("Arbeitskopie fehlt: AGENTS.md")
|
|
return
|
|
upstream = agents_base.read_bytes()
|
|
arbeit = agents.read_bytes()
|
|
if not arbeit.startswith(upstream):
|
|
fehler.append("DRIFT: AGENTS.md — Upstream-Teil ist nicht mehr "
|
|
"byte-identisch mit der Baseline")
|
|
return
|
|
rest = arbeit[len(upstream):].decode("utf-8", "replace")
|
|
if MARKE not in rest.splitlines()[0:3]:
|
|
fehler.append(f"AGENTS.md: Marke '{MARKE}' fehlt direkt nach dem "
|
|
"Upstream-Teil")
|
|
|
|
|
|
def pruefe_vollstaendigkeit(base: Path, fehler: list[str]) -> None:
|
|
"""Jede vendorierte Datei gehoert in genau eine Klasse."""
|
|
zugeordnet = {o for _, o in PAARE} | set(ERWEITERT) | set(NICHT_UEBERNOMMEN) \
|
|
| set(SONDERFALL)
|
|
for datei in baseline_dateien(base):
|
|
if datei not in zugeordnet:
|
|
fehler.append(
|
|
f"UNZUGEORDNET: {BASELINE}/{datei} ist vendoriert, steht aber "
|
|
f"in keiner Liste — es vergleicht sie nichts. Nach PAARE, "
|
|
f"ERWEITERT oder NICHT_UEBERNOMMEN eintragen.")
|
|
for datei in ERWEITERT + NICHT_UEBERNOMMEN:
|
|
if not (base / datei).is_file():
|
|
fehler.append(f"Baseline fehlt: {BASELINE}/{datei} (deklariert, "
|
|
f"aber nicht vendoriert)")
|
|
|
|
|
|
def pruefe_abgleich(root: Path, fehler: list[str]) -> None:
|
|
"""Hat upstream eine erweiterte Datei angefasst, muss HERKUNFT.md sie nennen."""
|
|
stände = baselines(root)
|
|
if len(stände) < 2:
|
|
return # nichts zu vergleichen, kein Befund
|
|
vorher, jetzt = stände[-2], stände[-1]
|
|
if jetzt.name != Path(BASELINE).name:
|
|
fehler.append(f"BASELINE zeigt auf {Path(BASELINE).name}, neuester "
|
|
f"vendorierter Stand ist {jetzt.name} — welcher gilt?")
|
|
return
|
|
geaendert = []
|
|
for datei in ERWEITERT:
|
|
alt, neu = vorher / datei, jetzt / datei
|
|
if not alt.is_file() or not neu.is_file():
|
|
continue # neu hinzugekommen oder entfallen
|
|
if alt.read_bytes() != neu.read_bytes():
|
|
geaendert.append(datei)
|
|
if not geaendert:
|
|
return
|
|
|
|
quittung = jetzt / ABGLEICH
|
|
if not quittung.is_file():
|
|
fehler.append(
|
|
f"{jetzt.name}/{ABGLEICH} fehlt, upstream hat aber "
|
|
f"{len(geaendert)} erweiterte Datei(en) angefasst — ungeprueft "
|
|
f"ist nicht bestanden.")
|
|
return
|
|
quittiert = set()
|
|
for zeile in quittung.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
if not zeile.strip() or zeile.lstrip().startswith("#"):
|
|
continue
|
|
quittiert.add(zeile.split("\t")[0].strip())
|
|
for datei in geaendert:
|
|
if datei not in quittiert:
|
|
fehler.append(
|
|
f"ABGLEICH: upstream hat {datei} zwischen {vorher.name} und "
|
|
f"{jetzt.name} geaendert, {jetzt.name}/{ABGLEICH} quittiert "
|
|
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] = []
|
|
pruefe_paare(root, base, fehler)
|
|
pruefe_agents(root, base, fehler)
|
|
pruefe_vollstaendigkeit(base, fehler)
|
|
pruefe_abgleich(root, fehler)
|
|
pruefe_schema_deckung(root, base, fehler)
|
|
return fehler
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Positivkontrolle. Ein Tor, das nur gruen gesehen wurde, ist eine Vermutung
|
|
# (WORKFLOW.md, Gate 4).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def selbsttest() -> int:
|
|
bestanden, gefallen = 0, []
|
|
|
|
def pruefe_dass(name: str, bedingung: bool) -> None:
|
|
nonlocal bestanden
|
|
if bedingung:
|
|
bestanden += 1
|
|
print(f" ok {bestanden + len(gefallen)} {name}")
|
|
else:
|
|
gefallen.append(name)
|
|
print(f" FEHL {bestanden + len(gefallen)} {name}")
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
alt = root / UPSTREAM / "neckbeard-v0.1.1"
|
|
neu = root / BASELINE
|
|
for b in (alt, neu):
|
|
(b / "scripts").mkdir(parents=True)
|
|
(b / "templates").mkdir(parents=True)
|
|
(b / "AGENTS.md").write_text("upstream\n", encoding="utf-8")
|
|
# Realistisch: die Inventartabelle nennt jede Datei beim Namen.
|
|
# Der erste Entwurf der Abgleichspruefung suchte genau darin und
|
|
# war deshalb wirkungslos - Zusicherung 7 haelt das fest.
|
|
tabelle = "\n".join(f"| `{d}` | erklaert erweitert |"
|
|
for d in ERWEITERT + NICHT_UEBERNOMMEN)
|
|
(b / HERKUNFT).write_text(f"# Herkunft\n\n{tabelle}\n",
|
|
encoding="utf-8")
|
|
for _, original in PAARE:
|
|
(b / original).parent.mkdir(parents=True, exist_ok=True)
|
|
(b / original).write_text("gleich\n", encoding="utf-8")
|
|
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:
|
|
(root / arbeit).parent.mkdir(parents=True, exist_ok=True)
|
|
(root / arbeit).write_text("gleich\n", encoding="utf-8")
|
|
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) == [])
|
|
|
|
(root / "WORKFLOW.md").write_text("veraendert\n", encoding="utf-8")
|
|
pruefe_dass("eine abweichende Arbeitskopie wird gemeldet",
|
|
any(f.startswith("DRIFT: WORKFLOW.md") for f in pruefe(root)))
|
|
(root / "WORKFLOW.md").write_text("gleich\n", encoding="utf-8")
|
|
|
|
(root / "AGENTS.md").write_text("anders\n", encoding="utf-8")
|
|
pruefe_dass("ein veraenderter AGENTS-Praefix wird gemeldet",
|
|
any(f.startswith("DRIFT: AGENTS.md") for f in pruefe(root)))
|
|
(root / "AGENTS.md").write_text(f"upstream\n{MARKE}\neigenes\n",
|
|
encoding="utf-8")
|
|
|
|
(neu / "scripts" / "fremd.py").write_text("x\n", encoding="utf-8")
|
|
pruefe_dass("eine vendorierte Datei ohne Zuordnung wird gemeldet",
|
|
any(f.startswith("UNZUGEORDNET:") for f in pruefe(root)))
|
|
(neu / "scripts" / "fremd.py").unlink()
|
|
|
|
(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)))
|
|
|
|
# Der Fall, an dem der erste Entwurf wirkungslos war: Die Datei steht
|
|
# laengst in der Inventartabelle der HERKUNFT.md. Das ist keine
|
|
# Quittung, und eine leere Quittungsdatei aendert daran nichts.
|
|
(neu / ABGLEICH).write_text("# datei\tvorige\twas\n", encoding="utf-8")
|
|
pruefe_dass("die Zeile in der Inventartabelle quittiert nicht",
|
|
any(f.startswith("ABGLEICH:") for f in pruefe(root)))
|
|
|
|
(neu / ABGLEICH).write_text(f"# datei\tvorige\twas\n"
|
|
f"{ERWEITERT[1]}\t{alt.name}\tandere Datei\n",
|
|
encoding="utf-8")
|
|
pruefe_dass("die Quittung fuer eine andere Datei quittiert diese nicht",
|
|
any(f.startswith("ABGLEICH:") for f in pruefe(root)))
|
|
|
|
(neu / ABGLEICH).write_text(f"# datei\tvorige\twas\n"
|
|
f"{ERWEITERT[0]}\t{alt.name}\tnachgezogen\n",
|
|
encoding="utf-8")
|
|
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(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)))
|
|
|
|
gesamt = bestanden + len(gefallen)
|
|
print()
|
|
if gefallen:
|
|
print(f"selbsttest: {len(gefallen)} von {gesamt} Zusicherungen GEFALLEN")
|
|
for name in gefallen:
|
|
print(f" - {name}")
|
|
return 1
|
|
print(f"selbsttest: {gesamt} Zusicherungen bestanden")
|
|
return 0
|
|
|
|
|
|
def main() -> int:
|
|
if "--selbsttest" in sys.argv[1:]:
|
|
return selbsttest()
|
|
root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
|
|
fehler = pruefe(root)
|
|
for f in fehler:
|
|
print(f"FEHLER {f}")
|
|
print(f"pruefe_upstream_drift: {len(fehler)} Fehler")
|
|
return 1 if fehler else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|