Build-Kommando fuer das Glossar: erzeuge_glossar_04.py

Der Erzeuger lag als Wegwerfskript ausserhalb des Repositorys - genau die
Falle, die in diesem Projekt schon zweimal zugeschnappt ist (web_04/,
titeltexte_04.py): eine Quelle, die nicht dort liegt, wo gearbeitet wird.

    python3 Operations_Research_mit_Python_Version_04/erzeuge_glossar_04.py
    python3 .../erzeuge_glossar_04.py --check     # nur pruefen

Die 234 Eintraege stehen in glossar_eintraege_04.py als Viertupel
(Anzeigename, Indexmarke, Definition, Verweisziel). Das Verweisziel ist
eingefroren, nicht berechnet: Die erste Fundstelle eines Begriffs ist oft
nicht die erklaerende - "Schattenpreis" faellt erstmals im LP-Schnellstart,
erklaert wird er im Abschnitt ueber Dualitaet. Ein Automatismus haette diese
Entscheidung jedes Mal neu und jedes Mal gleich falsch getroffen.

Drei Pruefungen, alle gegen das Buch statt gegen eine Annahme:

* Jede Indexmarke muss im Buchtext buchstabengetreu vorkommen - sonst
  entstuende ein zweiter Registereintrag fuer dasselbe Konzept.
* Jedes {ref:}-Ziel muss existieren.
* Kein Verweis darf auf das Glossar selbst zeigen.

Dazu die Handaenderungs-Erkennung nach dem Muster von
spiegle_statische_assets(): Ist 94_Anhang_Glossar.md neuer als ihre Quelle und
weicht ab, bricht der Lauf ab und nennt die Quelle, statt die Arbeit still zu
ueberschreiben. build_version_04.py --check ruft den Erzeuger mit auf, damit
eine veraltete Fassung nicht erst auffaellt, wenn ihn jemand zufaellig startet.

Ein Zwischenfall beim Einbau hat die Pruefung verbessert: Die erste Fassung
meldete 24 angebliche Tippfehler. Es waren keine - es sind die Begriffe, die
im ganzen Buch NUR im Glossar indexiert sind (Rebalancing, Volatilitaet,
Solver-Status ...). Automatisch ist das von einem Tippfehler nicht zu
unterscheiden, also stehen sie jetzt ausdruecklich in NUR_IM_GLOSSAR.

Der Beleg, dass der Erzeuger die Handarbeit exakt trifft: Er schreibt die
vorhandene Datei byte-identisch neu. Beide Waechter gegengeprueft - mit einer
Handaenderung an der .md und mit einem Tippfehler in einer Indexmarke.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dschlueter 2026-09-08 21:28:53 +02:00
commit beff14094c
6 changed files with 1728 additions and 0 deletions

View file

@ -63,6 +63,7 @@ cd Version_04
python3 Operations_Research_mit_Python_Version_04/build_version_04.py --check # nur prüfen python3 Operations_Research_mit_Python_Version_04/build_version_04.py --check # nur prüfen
python3 Operations_Research_mit_Python_Version_04/build_version_04.py --pdf --html python3 Operations_Research_mit_Python_Version_04/build_version_04.py --pdf --html
python3 Operations_Research_mit_Python_Version_04/extract_programme_04.py python3 Operations_Research_mit_Python_Version_04/extract_programme_04.py
python3 Operations_Research_mit_Python_Version_04/erzeuge_glossar_04.py # Anhang E
python3 Operations_Research_mit_Python_Version_04/veroeffentliche_04.py # hochladen python3 Operations_Research_mit_Python_Version_04/veroeffentliche_04.py # hochladen
``` ```
@ -109,6 +110,15 @@ bewacht jede Familie. Konkret:
einer bereits nummerierten Überschrift. Verwiesen wird auf einer bereits nummerierten Überschrift. Verwiesen wird auf
`{ref:sec:<kapitel>-denkfehler}`. `{ref:sec:<kapitel>-denkfehler}`.
* Im Fließtext `{ref:<label>}`, nie eine Literalzahl. * Im Fließtext `{ref:<label>}`, nie eine Literalzahl.
* **Das Glossar (Anhang E) wird erzeugt.** Die 234 Einträge stehen in
`glossar_eintraege_04.py` als Viertupel *(Anzeigename, Indexmarke, Definition,
Verweisziel)*; `erzeuge_glossar_04.py` schreibt daraus `94_Anhang_Glossar.md`. Die `.md`
**nicht** von Hand bearbeiten — `--check` bemerkt es und nennt die Quelle. Der Erzeuger
prüft außerdem gegen das Buch: Jede Indexmarke muss dort buchstabengetreu vorkommen (sonst
entstünde ein zweiter Registereintrag für dasselbe Konzept), jedes `{ref:}`-Ziel muss
existieren, und kein Verweis darf auf das Glossar selbst zeigen. Begriffe, die es nur im
Glossar gibt, stehen ausdrücklich in `NUR_IM_GLOSSAR` — sonst ließe sich ein solcher Fall
nicht von einem Tippfehler unterscheiden.
* **Der Titeltext steht in `Kritik_und_Verbesserungsvorschlaege/NEUER_TITEL.md`** und * **Der Titeltext steht in `Kritik_und_Verbesserungsvorschlaege/NEUER_TITEL.md`** und
nirgends sonst. `titeltexte_04.py` liest ihn; die PDF-Titelseite nirgends sonst. `titeltexte_04.py` liest ihn; die PDF-Titelseite
(`bilder_04/erzeuge_titelseite.py`) und der Hero-Block der Startseite (`bilder_04/erzeuge_titelseite.py`) und der Hero-Block der Startseite

View file

@ -409,9 +409,27 @@ def pruefe_dateien() -> list[str]:
fehlend.extend(pruefe_assets()) fehlend.extend(pruefe_assets())
fehlend.extend(pruefe_titeltexte()) fehlend.extend(pruefe_titeltexte())
fehlend.extend(pruefe_glossar())
return fehlend return fehlend
def pruefe_glossar() -> list[str]:
"""Ruft erzeuge_glossar_04.py --check auf.
Das Glossar wird aus glossar_eintraege_04.py erzeugt. Ohne diese Kopplung
an --check faellt eine veraltete oder von Hand bearbeitete Datei erst auf,
wenn jemand den Erzeuger zufaellig laufen laesst."""
skript = os.path.join(HIER, "erzeuge_glossar_04.py")
if not os.path.exists(skript):
return []
ergebnis = subprocess.run([sys.executable, skript, "--check"],
capture_output=True, text=True)
if ergebnis.returncode == 0:
return []
print(ergebnis.stdout.rstrip() or ergebnis.stderr.rstrip())
return ["94_Anhang_Glossar.md"]
# Die Datei, die den Titeltext ein viertes Mal traegt: als Buchtext, den ein # Die Datei, die den Titeltext ein viertes Mal traegt: als Buchtext, den ein
# Leser im Vorwort sieht. Erzeugt wird er dort NICHT - er steht mit # Leser im Vorwort sieht. Erzeugt wird er dort NICHT - er steht mit
# Hervorhebungen im Fliesstext und gehoert dem Autor. Aber er wird geprueft. # Hervorhebungen im Fliesstext und gehoert dem Autor. Aber er wird geprueft.

View file

@ -0,0 +1,246 @@
#!/usr/bin/env python3
# erzeuge_glossar_04.py
"""
Schreibt 94_Anhang_Glossar.md aus glossar_eintraege_04.py.
Warum erzeugt und nicht von Hand gepflegt: Vier Dinge sollen mechanisch
stimmen, nicht nach Aufmerksamkeit.
1. Die Sortierung nach deutschem Alphabet - Umlaute wie der Grundbuchstabe,
'ss' fuer das scharfe s.
2. Die Gruppierung unter '## <Buchstabe>'-Ueberschriften. Sie werden NICHT
nummeriert, weil ABSCHNITT_RE im Bauskript ein '{#sec:}'-Label verlangt,
das hier fehlt.
3. Genau eine Indexmarke je Eintrag, buchstabengetreu zur Schreibweise im
Buch. Weicht sie ab, entstehen zwei Registereintraege fuer dasselbe
Konzept - der haeufigste stille Fehler bei einem Glossar dieser Groesse.
4. Ein aufgeloester Verweis je Eintrag, keiner davon auf das Glossar selbst.
Geprueft wird ausserdem gegen das Buch: Jede Indexmarke muss dort vorkommen,
jedes {ref:}-Ziel muss existieren. Beides bricht den Lauf ab, statt eine
kaputte Datei zu schreiben.
Aufruf (aus dem Wurzelverzeichnis Version_04):
python3 Operations_Research_mit_Python_Version_04/erzeuge_glossar_04.py
python3 .../erzeuge_glossar_04.py --check # nur pruefen, nichts schreiben
'--check' meldet auch, wenn 94_Anhang_Glossar.md von Hand geaendert wurde:
Diese Aenderung ginge beim naechsten Lauf verloren und gehoert in
glossar_eintraege_04.py.
"""
import argparse
import os
import re
import sys
import textwrap
HIER = os.path.dirname(os.path.abspath(__file__))
BASIS = os.path.dirname(HIER)
sys.path.insert(0, HIER)
from glossar_eintraege_04 import EINTRAEGE # noqa: E402
ZIEL = os.path.join(HIER, "94_Anhang_Glossar.md")
KOPFZEILE = "# Anhang: Glossar {#anhang:glossar}"
EINLEITUNG = (
"Nachschlagewerk zu allen Fachbegriffen, Abkürzungen, Algorithmen und Bibliotheken "
"dieses Buchs. Jeder Eintrag nennt am Ende den Abschnitt, in dem der Begriff eingeführt "
"wird — ein Klick dorthin führt in der Online-Fassung direkt zur Erklärung im "
"Zusammenhang. Wer einen Begriff an *allen* Fundstellen sucht, benutzt das "
"Stichwortverzeichnis.")
SCHLUSS = ("*Weiter mit:* [{ref:anhang:literatur} — Literaturverzeichnis]"
"(95_Anhang_Literatur.md)")
# Deutsche Sortierung: Umlaute zaehlen wie ihr Grundbuchstabe.
UMLAUTE = str.maketrans({"ä": "a", "ö": "o", "ü": "u", "Ä": "A", "Ö": "O",
"Ü": "U", "ß": "ss", "é": "e", "è": "e"})
BREITE = 96 # Zeilenbreite wie in den uebrigen Kapiteldateien
# Begriffe, die im Buchtext NICHT mit {idx:} markiert sind und deren einzige
# Registerfundstelle deshalb das Glossar ist. Das ist zulaessig - der Leser
# findet den Begriff dann eben hier. Die Liste ist trotzdem ausdruecklich
# aufgefuehrt, weil sich ein solcher Fall sonst nicht von einem Tippfehler
# unterscheiden liesse: Beides sieht wie "Marke kommt im Buch nicht vor" aus.
# Wer einen Begriff neu aufnimmt, dessen Marke hier fehlt, bekommt einen
# Abbruch und muss sich entscheiden - Schreibweise korrigieren oder Begriff
# hier eintragen.
NUR_IM_GLOSSAR = frozenset({
"Alternativoptima",
"Binärvariable",
"CP-SAT",
"Calmar Ratio",
"Dualitätstheorie",
"Explainable OR",
"Kanonische Standardform",
"Kohärentes Risikomaß",
"Lagrange-Multiplikator",
"Lineare Programmierung (LP)",
"Maximum Drawdown",
"Nichtlineare Programmierung (NLP)",
"Pivotisierung",
"Positiv (semi-)definit",
"Rebalancing",
"Regime-Shift",
"Relaxation",
"Schätzfehler",
"Solver-Status",
"Transaktionskosten",
"Unsicherheitsmenge",
"Vehicle Routing Problem (VRP)",
"Volatilität",
"Wurzel-Zeit-Regel",
})
def sortierschluessel(name: str) -> tuple:
rein = re.sub(r"[^A-Za-z0-9 ]", "", name.translate(UMLAUTE))
return (rein.upper(), name)
def anfangsbuchstabe(name: str) -> str:
return name.translate(UMLAUTE)[0].upper()
def lies_buch() -> tuple[set[str], set[str]]:
"""Sammelt aus den Kapiteldateien alle Indexbegriffe und alle Labels.
Importiert wird dafuer build_version_04.py - dessen DATEIEN-Liste ist die
einzige Stelle, an der die Reihenfolge und Vollstaendigkeit des Buchs
steht."""
import build_version_04 as b
begriffe: set[str] = set()
labels: set[str] = set()
for name in b.DATEIEN:
pfad = os.path.join(HIER, name)
if not os.path.exists(pfad):
continue
with open(pfad, encoding="utf-8") as f:
text = f.read()
if name == os.path.basename(ZIEL):
# Das Glossar selbst zaehlt nicht als Fundstelle - sonst gaelte
# jeder Begriff als belegt, nur weil er hier steht.
labels |= set(re.findall(r"\{#([\w:-]+)\}", text))
continue
begriffe |= {m.split("!")[0].strip()
for m in re.findall(r"\{idx:([^}]+)\}", text)}
labels |= set(re.findall(r"\{#([\w:-]+)\}", text))
return begriffe, labels
def pruefe(begriffe: set[str], labels: set[str]) -> list[str]:
fehler: list[str] = []
gesehen: set[str] = set()
for name, idx, text, ziel in EINTRAEGE:
if name in gesehen:
fehler.append(f"Doppelter Eintrag: {name!r}")
gesehen.add(name)
if idx:
kern = idx.split("!")[0].strip()
if kern not in begriffe and kern not in NUR_IM_GLOSSAR:
fehler.append(
f"{name!r}: Die Indexmarke {idx!r} kommt im Buchtext nicht vor "
f"und steht nicht in NUR_IM_GLOSSAR.\n"
f" Entweder ist die Schreibweise falsch - dann entstuende "
f"ein zweiter Registereintrag fuer dasselbe Konzept -,\n"
f" oder der Begriff wird bewusst nur hier gefuehrt; dann "
f"gehoert er in NUR_IM_GLOSSAR.")
m = re.fullmatch(r"\{ref:([\w:-]+)\}", ziel)
if m:
if m.group(1) == "anhang:glossar":
fehler.append(f"{name!r}: Der Verweis zeigt auf das Glossar selbst.")
elif m.group(1) not in labels:
fehler.append(f"{name!r}: Unbekanntes Verweisziel {m.group(1)!r}.")
elif not re.fullmatch(r"\[[^\]]+\]\([\w.]+\.md\)", ziel):
fehler.append(f"{name!r}: Verweisziel ist weder eine {{ref:}}-Marke "
f"noch ein Dateilink: {ziel!r}")
saetze = len(re.findall(r"[.!?](?:\s|$)", text))
if saetze < 2:
fehler.append(f"{name!r}: Definition hat nur {saetze} Satz - "
f"zwei bis vier sind vorgesehen.")
return fehler
def baue() -> str:
def absatz(text: str) -> str:
return "\n".join(textwrap.wrap(text, width=BREITE,
break_long_words=False,
break_on_hyphens=False))
zeilen = [KOPFZEILE, "", absatz(EINLEITUNG), ""]
letzter = None
for name, idx, text, ziel in sorted(EINTRAEGE,
key=lambda e: sortierschluessel(e[0])):
buchstabe = anfangsbuchstabe(name)
if buchstabe != letzter:
zeilen += [f"## {buchstabe}", ""]
letzter = buchstabe
marke = "{idx:" + idx + "}" if idx else ""
zeilen += [absatz(f"**{name}**{marke}{text}{ziel}"), ""]
zeilen += ["---", "", SCHLUSS, ""]
return "\n".join(zeilen)
def main() -> int:
zerleger = argparse.ArgumentParser(
description="Erzeugt 94_Anhang_Glossar.md aus glossar_eintraege_04.py.")
zerleger.add_argument("--check", action="store_true",
help="nur pruefen, keine Datei schreiben")
argumente = zerleger.parse_args()
begriffe, labels = lies_buch()
fehler = pruefe(begriffe, labels)
if fehler:
for f in fehler:
print(f"FEHLER: {f}")
return 1
neu = baue()
vorhanden = ""
if os.path.exists(ZIEL):
with open(ZIEL, encoding="utf-8") as f:
vorhanden = f.read()
if vorhanden == neu:
print(f"Glossar unveraendert: {len(EINTRAEGE)} Eintraege.")
return 0
if argumente.check:
# Die Datei weicht ab. Ist sie juenger als ihre Quelle, hat jemand sie
# von Hand bearbeitet - dieselbe Falle wie bei den Website-Assets, und
# deshalb dieselbe Behandlung: benennen statt stillschweigend
# ueberschreiben.
quelle = os.path.join(HIER, "glossar_eintraege_04.py")
if vorhanden and os.path.getmtime(ZIEL) > os.path.getmtime(quelle):
print(f"FEHLER: {os.path.basename(ZIEL)} ist neuer als "
f"{os.path.basename(quelle)} und weicht davon ab - "
f"offenbar von Hand bearbeitet.\n"
f" Die Aenderung gehoert in die Quelle, sonst geht sie "
f"beim naechsten Lauf verloren.")
else:
print(f"FEHLER: {os.path.basename(ZIEL)} ist veraltet - ein Lauf "
f"ohne --check zieht sie nach.")
return 1
with open(ZIEL, "w", encoding="utf-8") as f:
f.write(neu)
buchstaben = sorted({anfangsbuchstabe(n) for n, _, _, _ in EINTRAEGE})
mit_marke = sum(1 for _, i, _, _ in EINTRAEGE if i)
print(f"Geschrieben: {os.path.relpath(ZIEL, BASIS)}")
print(f" {len(EINTRAEGE)} Eintraege, {len(buchstaben)} Buchstabengruppen "
f"({' '.join(buchstaben)})")
print(f" {mit_marke} Indexmarken, alle im Buchtext belegt")
return 0
if __name__ == "__main__":
sys.exit(main())

File diff suppressed because it is too large Load diff

View file

@ -2349,6 +2349,37 @@ einbuchstabige Überschriften; der Index bleibt bei 631 Einträgen.
Kosten: PDF von 759 auf **772 Seiten**. Jede Definition hat zwei bis drei Sätze, alphabetisch Kosten: PDF von 759 auf **772 Seiten**. Jede Definition hat zwei bis drei Sätze, alphabetisch
nach deutschem Alphabet mit Umlauten als Grundbuchstabe. nach deutschem Alphabet mit Umlauten als Grundbuchstabe.
**Nachgereicht: ein Build-Kommando.** Der Erzeuger lag zunächst als Wegwerfskript außerhalb
des Repositorys — genau die Falle, die in diesem Projekt schon zweimal zugeschnappt ist
(`web_04/`, `titeltexte_04.py`). Jetzt gilt:
```bash
python3 Operations_Research_mit_Python_Version_04/erzeuge_glossar_04.py
python3 .../erzeuge_glossar_04.py --check # nur pruefen
```
Die Einträge stehen in `glossar_eintraege_04.py` als Viertupel *(Anzeigename, Indexmarke,
Definition, Verweisziel)*. Das **Verweisziel ist eingefroren, nicht berechnet**: Die erste
Fundstelle eines Begriffs ist oft nicht die erklärende, und ein Automatismus hätte diese
Entscheidung jedes Mal neu und jedes Mal gleich falsch getroffen.
Drei Prüfungen, alle gegen das Buch statt gegen eine Annahme: Jede Indexmarke muss dort
buchstabengetreu vorkommen, jedes `{ref:}`-Ziel muss existieren, kein Verweis darf auf das
Glossar selbst zeigen. Dazu die Handänderungs-Erkennung nach dem Muster von
`spiegle_statische_assets()`: Ist die `.md` neuer als ihre Quelle und weicht ab, bricht der
Lauf ab und nennt die Quelle, statt die Arbeit still zu überschreiben.
**Ein Zwischenfall beim Einbau, der die Prüfung verbessert hat:** Die erste Fassung meldete 24
angebliche Tippfehler. Es waren keine — es sind die Begriffe, die im ganzen Buch *nur* im
Glossar indexiert sind (`Rebalancing`, `Volatilität`, `Solver-Status` …). Automatisch ist das
von einem Tippfehler nicht zu unterscheiden, also stehen sie jetzt ausdrücklich in
`NUR_IM_GLOSSAR`. Wer künftig einen Begriff mit unbekannter Marke aufnimmt, muss sich
entscheiden — Schreibweise korrigieren oder bewusst eintragen.
Der Beleg, dass der Erzeuger die Handarbeit exakt trifft: Er schreibt die vorhandene Datei
**byte-identisch** neu. `build_version_04.py --check` ruft ihn mit auf, damit eine veraltete
Fassung nicht erst auffällt, wenn jemand den Erzeuger zufällig startet.
--- ---
## 8. Commit-Historie des V04-Strangs ## 8. Commit-Historie des V04-Strangs

View file

@ -148,6 +148,9 @@ python3 Operations_Research_mit_Python_Version_04/build_version_04.py --pdf --ht
# Beispielprogramme aus den Kapiteln extrahieren # Beispielprogramme aus den Kapiteln extrahieren
python3 Operations_Research_mit_Python_Version_04/extract_programme_04.py python3 Operations_Research_mit_Python_Version_04/extract_programme_04.py
# Glossar (Anhang E) aus seiner Eintragsliste erzeugen
python3 Operations_Research_mit_Python_Version_04/erzeuge_glossar_04.py
# ein Diagramm neu erzeugen (braucht die Gruppe [figures], teils 'dot') # ein Diagramm neu erzeugen (braucht die Gruppe [figures], teils 'dot')
python3 bilder_04/erzeuge_kondition.py python3 bilder_04/erzeuge_kondition.py
``` ```