From fe5286c4206e19ef2395d9140283f6b0949a62e7 Mon Sep 17 00:00:00 2001 From: Thore Cimbal Date: Fri, 21 Aug 2026 12:00:00 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01F2Q4Ri8NGwyTZzScvKnWFM --- FRAMEWORK-BEFUNDE.md | 2 +- STATUS.md | 7 +- .../2026-08-21-v031-adoption-stage-2.md | 15 +- .../massgeblich-aber-unveraenderlich.md | 76 ++++++++++ .../quittung-bezeugt-aufmerksamkeit.md | 82 +++++++++++ .../pruefe_upstream_drift.cpython-312.pyc | Bin 0 -> 24310 bytes scripts/pruefe_upstream_drift.py | 132 +++++++++++++++++- 7 files changed, 309 insertions(+), 5 deletions(-) create mode 100644 docs/wiki/stolpersteine/massgeblich-aber-unveraenderlich.md create mode 100644 docs/wiki/stolpersteine/quittung-bezeugt-aufmerksamkeit.md create mode 100644 scripts/__pycache__/pruefe_upstream_drift.cpython-312.pyc diff --git a/FRAMEWORK-BEFUNDE.md b/FRAMEWORK-BEFUNDE.md index bf277ac..fbf5006 100644 --- a/FRAMEWORK-BEFUNDE.md +++ b/FRAMEWORK-BEFUNDE.md @@ -7,7 +7,7 @@ als Wiki-Seiten unter [docs/wiki/stolpersteine/](docs/wiki/index.md) — eine Seite je Muster, mit Stand und, wo zutreffend, der Version, die es abdeckt (ADR-0009 des Rahmenwerks). -**Die geclusterte Übersicht steht in [STATUS.md](STATUS.md)**, Abschnitt *Fehlerklassen*: 0 offen, 0 teilweise, 1 geerntet, 0 nicht abgedeckt. +**Die geclusterte Übersicht steht in [STATUS.md](STATUS.md)**, Abschnitt *Fehlerklassen*: 2 offen, 0 teilweise, 1 geerntet, 0 nicht abgedeckt. Diese Datei hält keinen eigenen Bestand mehr. Sie bleibt als Wegweiser bestehen, weil angenommene Entscheidungen auf sie diff --git a/STATUS.md b/STATUS.md index a9c82f5..bdb8fba 100644 --- a/STATUS.md +++ b/STATUS.md @@ -131,7 +131,12 @@ Bedeutung der Meilensteine: siehe [roadmap.md](roadmap.md). - [AAR — Game-Host-Anbindung: Deploy über zwei Übergaben, Ursache war nie die Firewall](docs/aar/2026-08-20-game-host-anbindung.md) - [AAR — rohana lief voll, der ThreadNet-Web-Spiegel riss](docs/aar/2026-08-20-rohana-platte-voll-spiegel-gerissen.md) -## Fehlerklassen (3) +## Fehlerklassen (5) + +### offen (2) + +- [Eine Datei kann nicht maßgeblich und zugleich unveränderlich sein](docs/wiki/stolpersteine/massgeblich-aber-unveraenderlich.md) +- [Eine Quittung bezeugt Aufmerksamkeit, nicht Vollständigkeit](docs/wiki/stolpersteine/quittung-bezeugt-aufmerksamkeit.md) ### geerntet (1) diff --git a/docs/ledger/2026-08-21-v031-adoption-stage-2.md b/docs/ledger/2026-08-21-v031-adoption-stage-2.md index b11449e..474fbf0 100644 --- a/docs/ledger/2026-08-21-v031-adoption-stage-2.md +++ b/docs/ledger/2026-08-21-v031-adoption-stage-2.md @@ -16,7 +16,8 @@ related: | 1 | acbef08 | sorb | DONE | Zuschnitt einmal komplett neu gefasst, weil die Messung ergab, dass Etappe 1 schon lief. Freigabe mit Auflage: erst pruefen, ob v0.3.1 laengst adaptiert ist. | | 2 | afaa74a | sorb | DONE | Auflage erfuellt: Regeln vollstaendig uebernommen, inhaltlicher Rest offen. Option A gewaehlt. | | 3 | b179246 | sorb | DONE | Ausfuehrung von A angepasst (Wegweiser statt Loeschung); Enum auf Einwand um `declined` erweitert. | -| 4.1 | | | DONE | Tracer Bullet: Schema, Generator, eine Seite. Acht Kontrollen gefahren, davon sieben absichtliche Brueche. | +| 4.1 | 0a95437 | sorb | DONE | Tracer Bullet: Schema, Generator, eine Seite. Acht Kontrollen gefahren, davon sieben absichtliche Brueche. | +| 4.2 | | sorb | DONE | Schema-Deckung als Widerspruch zum Handabgleich; zwei Befundseiten fuer die naechste Ernte. 16 Zusicherungen, sechs Brueche. | ## Ladder @@ -31,9 +32,21 @@ related: | `collect()` in `gen_status.py`, bevor ein Leser für die Wiki-Seiten geschrieben wird | liest jedes Artefakt nach Verzeichnis und Typ, inklusive Frontmatter | reused: nur ein Filter auf `area` kommt dazu, kein zweiter Leser | | | Erweiterungsblock in `schema.yaml` und ADR-0024, ob ein vierter Status-Wert überhaupt zulässig ist | zwei Enum-Erweiterungen sind dort bereits erklärt und gedeckt | reused: geübte Praxis statt Ausnahmeantrag — `declined` kommt in denselben Block | | | `apply_rules()` in `validate.py`, für die Beleg-Pflicht bei entschiedenen Ständen | vier Regeln nach demselben Muster, u. a. `waiting_requires_reason` | reused: eine fünfte Regel im selben Schalter, kein eigener Prüfer | | +| `pruefe_upstream_drift.py`, bevor ein eigener Schema-Vergleicher entsteht | prüft bereits Baselines, Zuordnung und Abgleichs-Quittung — nur nicht den Inhalt der erweiterten Dateien | reused: fünfte Prüffunktion im selben Skript samt dessen Selbsttest, kein neues Werkzeug | | +| `FRAMEWORK-BEFUNDE.md`, wohin die zwei neuen Befunde gehören | seit heute erzeugter Wegweiser ohne Bestand; Muster leben als Wiki-Seiten (ADR-0009) | built: zwei `stolpersteine`-Seiten — der erste Einsatz des neuen Mechanismus | | ## Notes +**Die neue Prüfung hat beim Bauen zweimal etwas über sich selbst gelernt.** +Erst brach sie den bestehenden Selbsttest, weil dessen Fixture `schema.yaml` +als Prosa schrieb — genau die unrealistische Fixture, die FB-12 für dieses +Skript schon einmal als Ursache einer wertlosen Zusicherung festhält. Dann +meldete meine Gegenprobe für den entfernten Absturzschutz „nichts", weil ein +Absturz denselben Rückgabewert liefert wie ein Fehlschlag; erst die Prüfung +auf einen Traceback zeigte, dass das Skript ohne den Schutz gar nicht bis zu +seiner Zusammenfassung kommt. Beide Male war nicht die Prüfung stumpf, +sondern ihre Kontrolle. + **Eine unsichere Entscheidung wurde auf Einwand verworfen.** Gate 3 wollte FB-07 und FB-08 mit `open` führen, obwohl das Rahmenwerk sie abgelehnt hat — mit dem Argument, der Enum habe keinen passenden Wert. sorb hat gefragt, diff --git a/docs/wiki/stolpersteine/massgeblich-aber-unveraenderlich.md b/docs/wiki/stolpersteine/massgeblich-aber-unveraenderlich.md new file mode 100644 index 0000000..ef6f749 --- /dev/null +++ b/docs/wiki/stolpersteine/massgeblich-aber-unveraenderlich.md @@ -0,0 +1,76 @@ +--- +type: wiki-page +area: stolpersteine +status: open +related: + - "docs/adr/0024-drei-framework-dateien-erklaert-erweitert.md" + - "docs/wiki/stolpersteine/quittung-bezeugt-aufmerksamkeit.md" +--- + +# Eine Datei kann nicht maßgeblich und zugleich unveränderlich sein + +Zwei Regeln, jede für sich richtig, treffen sich auf derselben Datei und +schließen einander aus. Der Widerspruch fällt nicht beim Beschließen auf, +sondern beim ersten Versuch, die Datei zu benutzen. + +## Der belegte Fall + +[ADR-0024](../../adr/0024-drei-framework-dateien-erklaert-erweitert.md) +erklärt drei Framework-Dateien für projekterweitert und legt fest: + +> *„Maßgeblich ist die Tabelle in `HERKUNFT.md` der jeweils gültigen +> Baseline. Sie und die Paarliste in `pruefe_upstream_drift.py` müssen sich +> decken."* + +`HERKUNFT.md` liegt unter `docs/sources/` — und `docs/sources/` ist +unveränderlich. Gemessen, nicht vermutet: Ein Testcommit auf die Datei +liefert + +``` +pruefe_sperrliste: 1 Verstoss/Verstoesse im Bereich HEAD~1..HEAD +``` + +Damit ist die Tabelle, die deckungsgleich gehalten werden **muss**, die +einzige, die niemand nachziehen **darf**. Aufgefallen ist es am +2026-08-21, als eine Schema-Erweiterung dort einzutragen gewesen wäre; sie +steht jetzt ersatzweise im Erweiterungsblock von `schema.yaml`. + +## Warum es beim Beschließen unsichtbar war + +Beide Regeln stammen von verschiedenen Tagen und aus verschiedenen +Anlässen. Die Unveränderlichkeit von `docs/sources/` ist alt und gut +begründet: Originalquellen werden zitiert, nicht redigiert. Die +Maßgeblichkeit von `HERKUNFT.md` ist einen Tag alt und ebenfalls gut +begründet: Der Handabgleich braucht einen benannten Ort. + +Der Fehler liegt in keiner der beiden Regeln, sondern **dazwischen**: Beim +Beschließen der zweiten wurde nicht gefragt, welchen Regeln der gewählte +Ort bereits unterliegt. Eine Datei erbt die Regeln ihres Verzeichnisses, +und ein Verzeichnis für unveränderliche Originale ist der falsche Platz für +ein gepflegtes Verzeichnis. + +## Woran man es erkennt + +- Eine neue Pflicht benennt einen Ort, ohne dessen bestehende Regeln zu + nennen. +- Zwei Regeln, die je für sich zitiert werden und nie nebeneinander. +- Ein Widerspruch, der erst bei der **zweiten** Benutzung sichtbar wird — + beim Anlegen war die Datei ein erlaubter Zugang, erst beim Fortschreiben + wird sie eine verbotene Änderung. + +## Was dagegen hilft + +Beim Festlegen eines Ortes prüfen, welche Regeln dort schon gelten — eine +Frage, keine Prüfung. Und die Lösung selbst ist eine Entscheidung, keine +Reparatur: Entweder zieht die maßgebliche Tabelle aus `docs/sources/` +heraus, oder die Sperre bekommt eine benannte, begründete Ausnahme. Beides +löst eine angenommene ADR ab und gehört ins Refinement, nicht in einen +Nebensatz. + +## Für die nächste Ernte + +Verallgemeinert: Ein Rahmenwerk, das ein Verzeichnis für unveränderliche +Quellen vorsieht **und** Übernehmer auffordert, dort ihre Baseline samt +Begleitnotizen abzulegen, baut diesen Konflikt für jeden Übernehmer ein. +Die Frage nach oben ist nicht, wie wir ihn hier auflösen, sondern wohin +gepflegte Metadaten zu einer unveränderlichen Quelle gehören. diff --git a/docs/wiki/stolpersteine/quittung-bezeugt-aufmerksamkeit.md b/docs/wiki/stolpersteine/quittung-bezeugt-aufmerksamkeit.md new file mode 100644 index 0000000..dd08342 --- /dev/null +++ b/docs/wiki/stolpersteine/quittung-bezeugt-aufmerksamkeit.md @@ -0,0 +1,82 @@ +--- +type: wiki-page +area: stolpersteine +status: open +related: + - "docs/adr/0024-drei-framework-dateien-erklaert-erweitert.md" + - "docs/wiki/stolpersteine/meldet-erfolg-ist-aber-blind.md" +--- + +# Eine Quittung bezeugt Aufmerksamkeit, nicht Vollständigkeit + +Wo eine Pflicht nicht maschinell prüfbar ist, liegt der Griff nahe, statt +der Sache die **Erinnerung daran** zu prüfen: Wer bestätigt, dass er es +getan hat, hat offenbar daran gedacht. Das ist billig zu bauen, fühlt sich +wie ein Widerspruch an — und deckt nur die Hälfte ab. + +## Der belegte Fall + +Am 2026-08-21 bekam der Abgleich der erklärt-erweiterten Framework-Dateien +(ADR-0024) einen Widerspruch: Hat upstream zwischen zwei Baselines eine +solche Datei angefasst, verlangt `pruefe_upstream_drift.py` eine Zeile in +`ABGLEICH.tsv`. Die Begründung stand ausdrücklich dabei: + +> *„Sie kann nicht entscheiden, ob wir richtig nachgezogen haben — aber sie +> kann sagen, dass es zu tun war. Das genügt: Der Fehler ist Vergessen, +> nicht Falschmachen."* + +**Dieselbe Aufgabe hat den Satz am selben Tag widerlegt.** Die Quittung für +`schema.yaml` wurde geschrieben, sie ist wahrheitsgemäß, und sie nennt +korrekt, was übernommen wurde: + +``` +schema.yaml neckbeard-v0.1.1 vendored, project_section_marker, judged, + Typen ledger und verdict uebernommen +``` + +Verloren gingen trotzdem **zwei Felder** — `wiki-page.status` und +`wiki-page.harvested_in`, also genau der Mechanismus, den ADR-0009 des +Rahmenwerks entscheidet. Aufgefallen ist es erst einen Tag später, als +jemand ihn benutzen wollte und feststellte, dass das Feld nicht existiert. + +Niemand hat etwas vergessen. Es wurde daran gedacht, es wurde gearbeitet, +es wurde quittiert — und es war unvollständig. Die Quittung konnte das +nicht sehen, weil sie nach dem *Ob* fragt und nicht nach dem *Wie weit*. + +## Woran man es erkennt + +- Eine Prüfung, die eine **Bestätigung** verlangt statt eines **Vergleichs**. +- Eine Begründung, die eine Fehlerart ausschließt („der Fehler ist X, nicht + Y") — ohne dass jemand Y gemessen hat. +- Ein Handgriff, dessen Ergebnis niemand gegen die Quelle hält. + +## Was dagegen hilft + +**Vergleichen, wo Vergleichen geht.** Seit dem 2026-08-21 hält +`pruefe_upstream_drift.py` unsere `schema.yaml` strukturell gegen die +Baseline: Jeder Typ, jedes Feld, jeder Enum-Wert und jede Pflichtangabe der +Baseline muss bei uns vorhanden sein. Hinzufügen ist erlaubt — wir +erweitern erklärt —, **verlieren nicht**. Kontrafaktisch gegen den echten +Verlust geprüft: Die Prüfung meldet ihn. + +⚠️ **Für die beiden anderen erweiterten Dateien gibt es das nicht.** +`validate.py` und `gen_status.py` sind Python; ein struktureller Vergleich +wie bei YAML existiert nicht. Dort trägt weiterhin nur die Quittung — und +der Fall oben zeigt, wie weit sie trägt. Das ist die offene Hälfte. + +**Die Ausschluss-Behauptung ist der eigentliche Fehler.** Nicht die +Quittung war falsch, sondern der Satz „der Fehler ist Vergessen, nicht +Falschmachen". Er hat eine Fehlerart per Annahme aus dem Entwurf entfernt, +ohne sie zu messen — dieselbe Bewegung wie bei +[„Meldet Erfolg, ist aber blind"](meldet-erfolg-ist-aber-blind.md), nur +eine Ebene höher: dort behauptet ein Werkzeug ein Ergebnis, hier behauptet +eine Begründung eine Fehlerverteilung. + +## Für die nächste Ernte + +Das Muster ist nicht projektspezifisch: Jedes Rahmenwerk, das Dateien zum +Erweitern freigibt, bekommt dasselbe Problem. Wert für die Übergabe ist +weniger die Quittung als die Frage, die ihr fehlt — **woran misst sich, +dass ein Handabgleich vollständig war?** Für strukturierte Formate ist die +Antwort billig. Für Code ist sie offen, und das gehört gesagt statt +überspielt. diff --git a/scripts/__pycache__/pruefe_upstream_drift.cpython-312.pyc b/scripts/__pycache__/pruefe_upstream_drift.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0379248de479b21fe9ea459889a6ccf4c75fbd4a GIT binary patch literal 24310 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 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)))