"""Baut ``docs/store-notiz.md`` — die öffentlich verwendbaren Versionshinweise.

## Warum es dieses Werkzeug gibt

``CHANGELOG.md`` ist für die Werkbank geschrieben. Dort stehen Dateinamen,
Werkzeugnamen, Testzahlen und Begründungen, die nur versteht, wer den
Quelltext kennt. Nichts davon ist geheim — aber nichts davon gehört in einen
Store-Eintrag, den Lernende lesen.

Gebraucht wird deshalb je Fassung ein zweiter Text: was sich für jemanden
ändert, der die Anwendung **benutzt**. Er geht in das Feld für
Versionshinweise im Partner Center, und er muss bei **jeder** Fassung
entstehen — auch bei einer Unterfassung, die nur einen Fehler behebt. Eine
Fassung ohne öffentlichen Text ist eine Fassung, über die niemand erfährt, was
sie geändert hat.

## Wo der Text steht

Im Changelog selbst, als Abschnitt ``### Für die Öffentlichkeit`` unter der
jeweiligen Fassung — das Gegenstück zu ``### Für die Werkbank``. Die
Überschrift nennt den Leser, und darin liegt ihr Zweck: Wer sie schreibt,
weiß dabei, dass dieser Text hinausgeht.

Dass er im Changelog steht und nicht in einer eigenen Datei, ist ebenfalls
Absicht. So schreibt man denselben Sachverhalt im selben Augenblick zweimal —
einmal für die Werkbank, einmal für die Öffentlichkeit. Läge der öffentliche
Text woanders, würde er beim nächsten Mal vergessen. Genau diese Art von Drift
ist in diesem Projekt schon zweimal vorgekommen.

## Was dieses Werkzeug prüft

1. **Vollständigkeit** — jede Fassung hat genau einen solchen Abschnitt.
   ``[Unveröffentlicht]`` braucht ihn, sobald dort überhaupt etwas steht.
2. **Länge** — der Text passt in das Feld des Partner Centers.
3. **Öffentlichkeit** — er enthält keinen Dateinamen, keinen Pfad, keinen
   Werkzeugnamen, keinen Verweis auf ein anderes Dokument, keine Testzahl,
   keine Git-Kennung und keine personenbezogene Angabe.

Schlägt eine der drei Prüfungen an, bricht das Werkzeug ab und nennt Fassung
und Fundstelle. Es schreibt dann nichts — ein halb geprüfter öffentlicher Text
ist schlimmer als keiner.

``app/tests/dokumentation.test.ts`` hält dieselben drei Zusagen fest, damit sie
auch dann auffallen, wenn niemand dieses Werkzeug aufruft.

## Aufruf

    python tools/store_notiz.py
    python tools/store_notiz.py --pruefen    # meldet nur, ob die Datei aktuell ist
    python tools/store_notiz.py --feld       # gibt nur den Text der aktuellen Fassung aus
"""

from __future__ import annotations

import io
import json
import re
import sys
import unicodedata
from pathlib import Path

WURZEL = Path(__file__).resolve().parent.parent
PAKET = WURZEL / 'app' / 'package.json'
CHANGELOG = WURZEL / 'CHANGELOG.md'
ZIEL = WURZEL / 'docs' / 'store-notiz.md'

#: Überschrift eines Fassungsabschnitts: "## [0.23.0] — 2026-08-29".
FASSUNGSKOPF = re.compile(r'^## \[(?P<nummer>[^\]]+)\](?:\s*—\s*(?P<datum>\S+))?\s*$')

#: Der Abschnitt, der öffentlich werden darf. Genau dieser, kein anderer.
OEFFENTLICH = '### Für die Öffentlichkeit'

#: Grenze des Feldes „What's new in this version" im Partner Center (deutsche
#: Dokumentation: „Was ist neu in dieser Version?"; früher „Versionshinweise",
#: in der Übermittlungs-API bis heute ``releaseNotes``).
#:
#: 1500 Zeichen, am 29.08.2026 auf zwei Seiten von Microsoft Learn unabhängig
#: belegt — der MSIX-Seite und der MSI/EXE-Seite zum Store-Eintrag. Maßgeblich
#: ist die MSIX-Seite, weil dieses Projekt den MSIX-Weg geht.
#:
#: Steht die Zahl einmal falsch hier, ist jeder erzeugte Text falsch bemessen —
#: deshalb steht sie an genau einer Stelle, und ``dokumentation.test.ts`` holt
#: sie von hier, statt sie zu wiederholen.
FELDGRENZE = 1500


def fassung() -> str:
    return json.loads(PAKET.read_text(encoding='utf-8'))['version']


# ── Was in einem öffentlichen Text nie vorkommen darf ───────────────────────
#
# Jedes Muster ist an einer echten Fundstelle im Changelog belegt; keines
# trifft einen gewöhnlichen deutschen Satz. Die Reihenfolge ist die der
# Häufigkeit, damit die erste Meldung meist schon die richtige ist.
VERBOTE: list[tuple[str, str, str]] = [
    (
        'Auszeichnung als Code',
        r'`',
        'Rückwärtsschrägstriche zeichnen Quelltext aus. Im Store steht kein Quelltext.',
    ),
    (
        'Verweis auf ein Dokument',
        r'\]\(',
        'Ein Markdown-Verweis zeigt auf eine Datei, die im Store niemand hat.',
    ),
    (
        'Dateiname',
        r'\b[\w\-]+\.(?:json|jsonl|ts|tsx|js|mjs|cjs|md|py|yml|yaml|html|css|exe|db|sqlite|png|svg|txt|lock)\b',
        'Ein Dateiname sagt einem Lernenden nichts und verrät den inneren Aufbau.',
    ),
    (
        'Pfadangabe',
        r'(?:[A-Za-z]:\\|\b(?:app|docs|content|tools|release|resources|src|e2e|tests?)/)',
        'Ein Pfad gehört in den Quelltext, nicht in einen Store-Eintrag.',
    ),
    (
        'Umgebungsvariable',
        r'%[A-Z][A-Z_]+%',
        'Eine Umgebungsvariable ist eine Angabe für Fachleute.',
    ),
    (
        # Nicht dabei: FSRS. Das ist kein Werkzeug, sondern das benannte
        # Wiedervorlageverfahren — die Produktbeschreibung im Store nennt es
        # mit Absicht („Wiedervorlage nach FSRS-6"), weil man es nachschlagen
        # kann. Diese Liste stand einmal mit FSRS darin und schlug prompt an
        # der eigenen, bereits geprüften Store-Beschreibung an.
        'Werkzeugname',
        r'\b(?:npm|Playwright|Vitest|vitest|ESLint|Prettier|electron-builder|electron-vite|Electron|SQLite|better-sqlite3|axe-core|axe|NSIS|Node\.js|TypeScript|React|Python|Git|GitHub)\b',
        'Womit gebaut wurde, ändert für den Benutzer nichts.',
    ),
    (
        'Git-Kennung',
        r'\b[0-9a-f]{7,40}\b',
        'Eine Commit-Kennung ist ein Zeiger in ein Archiv, das niemand hat.',
    ),
    (
        # Der Binnenversal braucht zwei Buchstaben davor und zwei danach.
        # Ohne diese Schranke traf das Muster „lfB" — die deutsche Bezeichnung
        # der Randfeuerpatrone .22 lfB, die in den Erklärungen dieser
        # Anwendung vierzigmal vorkommt. Ein Muster, das die Fachsprache des
        # eigenen Gegenstands für Quelltext hält, ist unbrauchbar.
        'Bezeichner aus dem Quelltext',
        r'\b\w+_\w+\b|\b[a-z]{2,}[A-Z][a-zA-Z]{2,}\b',
        'Ein Bezeichner mit Unterstrich oder Binnenversal stammt aus dem Quelltext.',
    ),
    (
        'E-Mail-Adresse',
        r'[\w.+-]+@[\w-]+\.[\w.]+',
        'Eine Adresse ist eine personenbezogene Angabe.',
    ),
    (
        # Der Änderungsverlauf führt drei Adressen, die im Store nichts zu
        # suchen haben: die private Webseite mit dem Impressum, den Wirt des
        # Webspace und — nach der eigenen Verbotsliste ausdrücklich — das
        # Zuwendungskonto. Ein Spendenaufruf in einem Store-Eintrag verstößt
        # gegen die Richtlinien.
        'Adresse im Netz',
        r'https?://\S+|\b[\w-]+\.(?:com|net|org|io|dev)\b',
        'Ein Verweis ins Netz gehört nicht in einen Versionshinweis.',
    ),
    (
        'Name oder Anschrift des Herausgebers',
        r'Olaf\s+Willerding|olaf-willerding|ko-fi|kasserver',
        'Wer die Software herausgibt, steht im Copyright-Feld — nicht in jedem Versionshinweis.',
    ),
    (
        # Nicht wegen des Datenschutzes, sondern wegen der Store-Richtlinien:
        # Ein Hinweis auf eine freiwillige Zuwendung bringt die Kaufanmutung
        # ins Listing, die das Produkt gerade fernhalten will. Der Store-Eintrag
        # hat das in Abschnitt 9.3 entschieden — für die Beschreibung. Für den
        # Versionshinweis gilt dasselbe, und dort wäre es beinahe passiert: Der
        # erste Entwurf zu 0.22.0 führte die Bitte als ersten Punkt.
        #
        # Bewusst eng gefasst. „Unterstützung" und „unterstützen" allein sind
        # gewöhnliche Wörter — die Anwendung unterstützt Bildschirmleser, und
        # das darf sie auch sagen.
        'Bitte um eine Zuwendung',
        r'Spende|spenden|Zuwendung|Bitte um Unterstützung|Arbeit unterstützen',
        'Ein Hinweis auf eine Zuwendung lässt den Eintrag nach einem Kauf aussehen (Store-Eintrag 9.3).',
    ),
    (
        'Auftragsname aus dem Bau',
        r'(?<![\w:/])[a-z]{3,}:[a-z]{3,}(?![\w:/])',
        'Ein Bauauftrag wie „dist:win" ist eine Anweisung an die Werkbank.',
    ),
    (
        'Fassungsetikett aus der Versionsverwaltung',
        r'(?<!\w)v\d+\.\d+\.\d+(?!\w)',
        'Das „v" davor ist die Schreibweise des Etiketts im Archiv, nicht die der Fassung.',
    ),
    (
        'Angabe zu Tests oder Testabdeckung',
        r'\bTestabdeckung\b|\bAbdeckung\b[^.\n]{0,60}?(?:%|Prozent)|(?<!\w)\d+\s+(?:von\s+\d+\s+)?Tests?(?!\w)',
        'Wie gut geprüft wurde, gehört in den Stand der Software, nicht in den Store.',
    ),
    (
        # Nicht das Wort „Werkbank" allein — das ist gewöhnliches Deutsch und
        # kommt in den Erklärungen dieser Anwendung vor. Wohl aber die
        # Überschrift, die jemand beim Kopieren mitnimmt.
        #
        # Ebenfalls nicht dabei: „Unveröffentlicht" und „Änderungsverlauf".
        # Beide standen hier und fielen im ersten Probelauf durch — sie sind
        # gewöhnliche Wörter, und ein Satz wie „Der vollständige
        # Änderungsverlauf steht …" wäre in einem Versionshinweis völlig in
        # Ordnung.
        'Überschrift aus dem Änderungsverlauf',
        r'Für die Werkbank',
        'Diese Überschrift gliedert den internen Verlauf, nicht den Store-Text.',
    ),
]

#: Projektinterne Wörter. Sie sind nicht falsch, nur unverständlich.
#:
#: Bewusst kurz. Der erste Entwurf führte auch „Werkbank", „Gegenprobe",
#: „Meisterwerk", „Wache" und „Gate" — und schlug prompt an den Erklärungen
#: der eigenen Anwendung an: Eine „Werkbank im Hobbyraum" ist kein
#: Sicherheitsbehältnis nach § 13 AWaffV, und eine Merkregel zum Repetierer
#: heißt dort „die Gegenprobe". Das sind gewöhnliche deutsche Wörter, die hier
#: zufällig auch Projektsprache sind; eine Maschine kann das nicht
#: unterscheiden. Übrig bleiben nur Zusammensetzungen, die es außerhalb dieses
#: Projekts nicht gibt.
JARGON = [
    'Vollstufe',
    'Schnellstufe',
    'Durchstich',
    'Baukennung',
    'Paketstand',
]


def verstoesse(text: str) -> list[str]:
    """Alle Verletzungen der Öffentlichkeitsregeln in einem Text."""
    gefunden: list[str] = []
    for name, muster, warum in VERBOTE:
        for treffer in re.finditer(muster, text):
            gefunden.append(f'{name}: „{treffer.group(0)}" — {warum}')
    for wort in JARGON:
        # Nur als eigenständiges Wort, sonst trifft „Gate" jedes „Gateway".
        if re.search(rf'\b{re.escape(wort)}\b', text):
            gefunden.append(f'Projektjargon: „{wort}" — versteht nur, wer am Projekt arbeitet.')
    return gefunden


# ── Den Changelog zerlegen ─────────────────────────────────────────────────


def abschnitte() -> list[dict[str, object]]:
    """Alle Fassungen mit ihrem öffentlichen Block, in Dateireihenfolge."""
    zeilen = CHANGELOG.read_text(encoding='utf-8').split('\n')

    koepfe: list[tuple[int, str, str]] = []
    for i, zeile in enumerate(zeilen):
        treffer = FASSUNGSKOPF.match(zeile)
        if treffer is not None:
            koepfe.append((i, treffer.group('nummer'), treffer.group('datum') or ''))

    if not koepfe:
        raise SystemExit('ABBRUCH: CHANGELOG.md hat keine Fassungsüberschrift.')

    # Hinter der ältesten Fassung steht ein Kommentar, der zu keiner gehört.
    schluss = len(zeilen)
    for i in range(koepfe[-1][0] + 1, len(zeilen)):
        if zeilen[i].startswith('<!--'):
            schluss = i
            break

    ergebnis: list[dict[str, object]] = []
    for k, (start, nummer, datum) in enumerate(koepfe):
        ende = koepfe[k + 1][0] if k + 1 < len(koepfe) else schluss
        inhalt = zeilen[start + 1 : ende]
        ergebnis.append(
            {
                'nummer': nummer,
                'datum': datum,
                'zeile': start + 1,
                'unterabschnitte': [z for z in inhalt if z.startswith('### ')],
                'block': block_lesen(inhalt),
            }
        )
    return ergebnis


def block_lesen(inhalt: list[str]) -> list[str] | None:
    """Die Zeilen des öffentlichen Blocks, oder ``None``, wenn er fehlt."""
    treffer = [i for i, z in enumerate(inhalt) if z.strip() == OEFFENTLICH]
    if not treffer:
        return None
    if len(treffer) > 1:
        raise SystemExit(f'ABBRUCH: „{OEFFENTLICH}" steht mehrfach in einer Fassung.')

    start = treffer[0] + 1
    ende = len(inhalt)
    for i in range(start, len(inhalt)):
        if inhalt[i].startswith('### ') or inhalt[i].startswith('## '):
            ende = i
            break

    block = inhalt[start:ende]
    while block and not block[0].strip():
        block.pop(0)
    while block and not block[-1].strip():
        block.pop()
    return block


# ── Aus Markdown reinen Text machen ────────────────────────────────────────
#
# Das Feld im Partner Center nimmt reinen Text. Fettungen blieben dort als
# Sternchen stehen und sähen aus wie ein Versehen.
#
# Der Strich am Zeilenanfang folgt der Hausform der übrigen Store-Texte: Die
# Produktbeschreibung in ``docs/store-eintrag.md`` setzt ihre Aufzählungen
# ebenso, weil das Feld keine Auszeichnung kennt. Ein Aufzählungszeichen wie
# „•" käme je nach Schriftart als Kästchen an.
FETT = re.compile(r'\*\*(?P<inhalt>[^*]+)\*\*')
KURSIV = re.compile(r'(?<![*\w])\*(?P<inhalt>[^*]+)\*(?![*\w])')


def reintext(block: list[str]) -> str:
    """Der öffentliche Block als reiner Text, so wie er ins Feld kommt."""
    absaetze: list[str] = []
    laufend = ''
    for zeile in block:
        blank = zeile.strip()
        if not blank:
            continue
        if blank.startswith('- '):
            if laufend:
                absaetze.append(laufend)
            laufend = '- ' + blank[2:]
        else:
            laufend = (laufend + ' ' + blank).strip() if laufend else blank
    if laufend:
        absaetze.append(laufend)

    text = '\n'.join(absaetze)
    text = FETT.sub(lambda t: t.group('inhalt'), text)
    text = KURSIV.sub(lambda t: t.group('inhalt'), text)
    return text


def zeichenzahl(text: str) -> int:
    """Zeichen so gezählt, wie eine Eingabemaske sie zählt.

    Zusammengesetzte Zeichen (``a`` + Trema) zählen dort als eines. Der
    Unterschied fällt bei „ä" auf, und deutsche Texte sind voll davon.
    """
    return len(unicodedata.normalize('NFC', text))


# ── Prüfen ─────────────────────────────────────────────────────────────────


def pruefen(liste: list[dict[str, object]]) -> list[str]:
    """Alle Beanstandungen über alle Fassungen."""
    fehler: list[str] = []
    for eintrag in liste:
        nummer = str(eintrag['nummer'])
        block = eintrag['block']
        unterabschnitte = eintrag['unterabschnitte']
        assert isinstance(unterabschnitte, list)

        if block is None:
            # Ein leerer [Unveröffentlicht]-Abschnitt braucht keinen Text.
            if nummer == 'Unveröffentlicht' and not unterabschnitte:
                continue
            fehler.append(
                f'Fassung {nummer} (Zeile {eintrag["zeile"]}): '
                f'Der Abschnitt „{OEFFENTLICH}" fehlt. '
                f'Ohne ihn gibt es für diese Fassung keinen Text, der öffentlich werden darf.'
            )
            continue

        assert isinstance(block, list)
        if not block:
            fehler.append(f'Fassung {nummer}: „{OEFFENTLICH}" ist leer.')
            continue

        text = reintext(block)
        laenge = zeichenzahl(text)
        if laenge > FELDGRENZE:
            fehler.append(
                f'Fassung {nummer}: {laenge} Zeichen, erlaubt sind {FELDGRENZE}. '
                f'Um {laenge - FELDGRENZE} zu lang.'
            )

        for verstoss in verstoesse(text):
            fehler.append(f'Fassung {nummer}: {verstoss}')

    return fehler


# ── Schreiben ──────────────────────────────────────────────────────────────

KOPF = """# Öffentliche Versionshinweise

**Stand: Fassung {nummer}.** Was sich in jeder Fassung für jemanden geändert
hat, der die Anwendung benutzt.

> Dieses Dokument ist **erzeugt, nicht von Hand geschrieben**. Quelle ist der
> Abschnitt „Für die Öffentlichkeit" der jeweiligen Fassung im
> Änderungsverlauf. Neu erzeugen mit dem Werkzeug im Verzeichnis der
> Hilfsprogramme.

## Wozu es da ist

Der Änderungsverlauf des Projekts ist für die Entwicklung geschrieben. Er nennt
Dateien, Werkzeuge und Zahlen aus dem Prüflauf — nichts davon ist geheim, aber
nichts davon gehört in einen Store-Eintrag. Hier steht dieselbe Fassung noch
einmal, nur das, was beim Benutzen ankommt, und in einer Sprache ohne
Vorwissen.

Jeder Text ist maschinell daraufhin geprüft, dass er keinen Dateinamen, keinen
Pfad, keinen Werkzeugnamen, keine Zahl aus dem Prüflauf, keine Kennung aus der
Versionsverwaltung und keine personenbezogene Angabe enthält. Diese Prüfung
sieht die Form an, nicht den Sinn: Ob ein Satz inhaltlich hinausgehen soll,
entscheidet, wer ihn schreibt.

**Zwei Dinge, damit niemand mehr hineinliest, als dasteht.** Erstens ist jeder
Eintrag eine **Auswahl**, keine vollständige Liste — das Feld fasst {grenze}
Zeichen, eine große Fassung ändert mehr. Wer alles wissen will, findet es im
Änderungsverlauf. Zweitens nennt jeder Eintrag die **Zahlen seiner Zeit**: Wo
eine ältere Fassung von 87 Glossareinträgen spricht, waren es damals 87. Was
heute gilt, steht in der Produktbeschreibung.

## Was die Fassungsnummern bedeuten

Die Zahl vorn ist eine Null und bleibt es, bis der Funktionsumfang steht.
Alles unterhalb von Fassung {erste_veroeffentlichte} war ein Entwicklungsstand:
hier vollständig aufgeführt, weil zu jeder Änderung eine Angabe gehört, aber
nie öffentlich zu haben. Diese Liste ist eine Änderungsgeschichte, keine
Veröffentlichungsgeschichte.

Was seit der letzten Fassung aufgelaufen und noch nicht freigegeben ist, steht
hier **nicht**. Ein Hinweis auf etwas, das niemand bekommen kann, wäre ein
Versprechen.

**Bei der ersten Einreichung bleibt das Feld leer.** So verlangt es Microsoft
ausdrücklich: Wer eine Anwendung zum ersten Mal übermittelt, lässt „What's new
in this version" frei — es gibt noch nichts, wovon sich etwas unterscheiden
könnte. Der Text unten ist deshalb für die erste **Aktualisierung** gedacht,
nicht für die erste Einreichung.

---

## Zum Einfügen: Fassung {nummer}

Für das Feld „What's new in this version" im Store-Eintrag. Grenze {grenze}
Zeichen, dieser Text hat {zeichen} — {reserve} bleiben frei.

Das Feld nimmt einfachen Text. Ob es Zeilenumbrüche erhält, ist nicht
dokumentiert; deshalb besteht jeder Punkt aus vollständigen Sätzen und bleibt
auch dann lesbar, wenn der Store alles zu einem Absatz zusammenzieht.

```
{feldtext}
```

---

## Alle Fassungen

"""


def bericht() -> str:
    liste = abschnitte()
    fehler = pruefen(liste)
    if fehler:
        raise SystemExit(
            'ABBRUCH: Die öffentlichen Versionshinweise sind nicht in Ordnung.\n  '
            + '\n  '.join(fehler)
        )

    nummer = fassung()
    aktuell = next((e for e in liste if e['nummer'] == nummer), None)
    if aktuell is None:
        raise SystemExit(f'ABBRUCH: CHANGELOG.md hat keinen Abschnitt für Fassung {nummer}.')

    block = aktuell['block']
    assert isinstance(block, list)
    feldtext = reintext(block)

    teile = [
        KOPF.format(
            nummer=nummer,
            grenze=FELDGRENZE,
            zeichen=zeichenzahl(feldtext),
            reserve=FELDGRENZE - zeichenzahl(feldtext),
            feldtext=feldtext,
            erste_veroeffentlichte=ERSTE_VEROEFFENTLICHTE,
        )
    ]

    for eintrag in liste:
        if eintrag['block'] is None:
            continue
        # Was noch keine Fassung ist, wird auch nicht veröffentlicht. Der Text
        # muss trotzdem geschrieben und geprüft sein — sonst entstünde er erst
        # unter Zeitdruck am Tag der Freigabe. Er wartet nur, bis der Abschnitt
        # eine Nummer bekommt.
        if eintrag['nummer'] == 'Unveröffentlicht':
            continue
        kopf = str(eintrag['nummer'])
        if eintrag['datum']:
            kopf += f' — {eintrag["datum"]}'
        zeilen = eintrag['block']
        assert isinstance(zeilen, list)
        teile.append(f'### {kopf}\n\n' + '\n'.join(zeilen) + '\n')

    return '\n'.join(teile)


#: Die erste Fassung, die überhaupt zur Veröffentlichung eingereicht wurde.
#: Alles darunter war ein Entwicklungsstand. Steht hier, damit das Dokument
#: nicht den Eindruck einer langen Veröffentlichungsgeschichte erweckt.
ERSTE_VEROEFFENTLICHTE = '0.22.0'


def main() -> int:
    if '--feld' in sys.argv:
        liste = abschnitte()
        aktuell = next((e for e in liste if e['nummer'] == fassung()), None)
        if aktuell is None or aktuell['block'] is None:
            raise SystemExit(f'ABBRUCH: Fassung {fassung()} hat keinen „{OEFFENTLICH}".')
        block = aktuell['block']
        assert isinstance(block, list)
        text = reintext(block)

        # Geprüft wird auch hier, und zwar bevor etwas ausgegeben wird.
        #
        # Bis Fassung 0.24.1 lief `pruefen()` ausschliesslich aus `bericht()`
        # heraus; dieser Zweig kehrte davor um. Ausgerechnet der Aufruf, dessen
        # einziger Zweck es ist, den Text zum Einfuegen ins Partner Center zu
        # liefern, lief damit ohne Laengengrenze, ohne Verbotsliste und ohne
        # Jargonpruefung — und der Modulkopf sagt das Gegenteil zu. Wer den
        # Text von hier nimmt, nimmt ihn ungeprueft nach draussen.
        beanstandet = [f'Fassung {fassung()}: {grund}' for grund in verstoesse(text)]
        laenge = zeichenzahl(text)
        if laenge > FELDGRENZE:
            beanstandet.append(
                f'Fassung {fassung()}: {laenge} Zeichen, erlaubt sind {FELDGRENZE}.'
            )
        if beanstandet:
            print('ABBRUCH: Der Text darf so nicht hinaus.', file=sys.stderr)
            for grund in beanstandet:
                print(f'  {grund}', file=sys.stderr)
            return 1

        print(text)
        print(f'\n— {laenge} von {FELDGRENZE} Zeichen', file=sys.stderr)
        return 0

    neu = bericht()

    if '--pruefen' in sys.argv:
        alt = ZIEL.read_text(encoding='utf-8') if ZIEL.exists() else ''
        if alt == neu:
            print(f'{ZIEL.relative_to(WURZEL)} ist aktuell.')
            return 0
        print(f'{ZIEL.relative_to(WURZEL)} ist NICHT aktuell — neu erzeugen mit:')
        print('  python tools/store_notiz.py')
        return 1

    io.open(ZIEL, 'w', encoding='utf-8', newline='').write(neu)
    print(f'Geschrieben: {ZIEL.relative_to(WURZEL)} ({len(neu)} Zeichen, Fassung {fassung()}).')
    return 0


if __name__ == '__main__':
    raise SystemExit(main())
