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:
parent
7835cf322d
commit
beff14094c
6 changed files with 1728 additions and 0 deletions
10
CLAUDE.md
10
CLAUDE.md
|
|
@ -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 --pdf --html
|
||||
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
|
||||
```
|
||||
|
||||
|
|
@ -109,6 +110,15 @@ bewacht jede Familie. Konkret:
|
|||
einer bereits nummerierten Überschrift. Verwiesen wird auf
|
||||
`{ref:sec:<kapitel>-denkfehler}`.
|
||||
* 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
|
||||
nirgends sonst. `titeltexte_04.py` liest ihn; die PDF-Titelseite
|
||||
(`bilder_04/erzeuge_titelseite.py`) und der Hero-Block der Startseite
|
||||
|
|
|
|||
|
|
@ -409,9 +409,27 @@ def pruefe_dateien() -> list[str]:
|
|||
|
||||
fehlend.extend(pruefe_assets())
|
||||
fehlend.extend(pruefe_titeltexte())
|
||||
fehlend.extend(pruefe_glossar())
|
||||
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
|
||||
# Leser im Vorwort sieht. Erzeugt wird er dort NICHT - er steht mit
|
||||
# Hervorhebungen im Fliesstext und gehoert dem Autor. Aber er wird geprueft.
|
||||
|
|
|
|||
246
Operations_Research_mit_Python_Version_04/erzeuge_glossar_04.py
Executable file
246
Operations_Research_mit_Python_Version_04/erzeuge_glossar_04.py
Executable 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())
|
||||
1420
Operations_Research_mit_Python_Version_04/glossar_eintraege_04.py
Normal file
1420
Operations_Research_mit_Python_Version_04/glossar_eintraege_04.py
Normal file
File diff suppressed because it is too large
Load diff
31
PROGRESS.md
31
PROGRESS.md
|
|
@ -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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -148,6 +148,9 @@ python3 Operations_Research_mit_Python_Version_04/build_version_04.py --pdf --ht
|
|||
# Beispielprogramme aus den Kapiteln extrahieren
|
||||
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')
|
||||
python3 bilder_04/erzeuge_kondition.py
|
||||
```
|
||||
|
|
|
|||
Loading…
Reference in a new issue