Files
Amateurfunk-Anki/CLAUDE.md
T
2026-06-17 11:48:27 +02:00

175 lines
9.0 KiB
Markdown

# Amateurfunk — BNetzA Question Catalog → Anki Decks
A small two-stage Python pipeline that downloads the German amateur-radio
exam question catalog ("Fragenkatalog") published by the Bundesnetzagentur
(BNetzA) and turns it into Anki decks.
The full source-discovery notes, JSON schema, exam-structure details, and
per-stage design decisions live in `DESIGN.md`. This file is a short
orientation for anyone (human or agent) opening the project.
## What the catalog is
- Official German amateur-radio exam questions for classes **N, E, A**
(German license tiers).
- Published by the Bundesnetzagentur under the **DL-DE→BY-2.0** open
data license (free reuse, attribution required).
- Distributed as a single ZIP containing one JSON file with the full
question tree, plus a `svgs/` folder with figures referenced by
individual questions.
- Current edition at time of writing: **3. Auflage, März 2024**
(issued 2024-03-20, valid from 2024-06-24, ~1750 questions).
## Canonical source
- ZIP (machine-readable): `https://www.bundesnetzagentur.de/SharedDocs/Downloads/DE/Sachgebiete/Telekommunikation/Unternehmen_Institutionen/Frequenzen/Amateurfunk/Fragenkatalog/PruefungsfragenZIP.zip?__blob=publicationFile`
- Landing page (short link): `https://www.bnetza.de/amateurfunk-fragenkatalog`
- PDF (human-readable, not used by this pipeline): same path with
`Pruefungsfragen.pdf` instead of `PruefungsfragenZIP.zip`.
The ZIP URL is stable across editions — BNetzA replaces the file
in-place. The `Last-Modified` HTTP header is reliable for change
detection. The filename inside the ZIP (`fragenkatalog3b.json`) encodes
the edition (`3b` = 3rd edition, revision b) and will change on new
editions, so we discover it from the archive rather than hard-coding.
## Pipeline overview
```
BNetzA ZIP ──[Stage 1: amateurfunk_fetch.py]──► data/<slug>/
├── fragenkatalog*.json
├── svgs/
├── README.txt
└── manifest.json
data/ ──[Stage 2: amateurfunk_anki.py]──► anki/
├── amateurfunk-technische-kenntnisse-n.apkg
├── amateurfunk-technische-kenntnisse-e.apkg
├── amateurfunk-technische-kenntnisse-a.apkg
├── amateurfunk-betriebliche-kenntnisse.apkg
└── amateurfunk-kenntnisse-von-vorschriften.apkg
shorthand.json ──[Stage 2b: amateurfunk_shorthand.py]──► anki/
└── amateurfunk-abkuerzungen-q-gruppen.apkg
technical.json ──[Stage 2c: amateurfunk_technical.py]──► anki/
└── amateurfunk-technische-abkuerzungen.apkg
```
### Stage 1 — `amateurfunk_fetch.py`
1. Download the ZIP from the canonical URL.
2. Verify it is a valid ZIP and contains the expected JSON + SVG files.
3. Extract to a target directory (default: `./data/<edition>/`).
4. Emit a small `manifest.json` next to the data: source URL,
fetched-at timestamp, `Last-Modified` from the server, JSON edition
metadata, sha256 of the ZIP.
5. Be idempotent — re-running without an upstream change is a no-op.
The skip key is the HTTP `Last-Modified` header recorded on the
previous manifest; the ZIP is deleted by default after extraction,
so the recorded sha256 is provenance, not a re-verification target.
See `DESIGN.md` §4 for the full idempotency contract.
### Stage 2 — `amateurfunk_anki.py`
1. Read the latest edition from `data/` (following
`manifest-latest.json` to a per-edition directory).
2. Split the catalog into five categories. Betriebliche and
Vorschriften get one deck each (shared across every candidate).
Technische is additionally fanned out per license class into three
decks (N / E / A) using a strict equality split on the question's
`class` field. The `klasse-N|E|A` tag is still emitted on every
note for inside-Anki filtering.
3. Render every question as an Anki note: shuffled A/B/C/D choices on
the front, the displayed position of the correct answer on the
back. Inline `$...$` LaTeX is converted to MathJax `\(...\)`
delimiters; the catalog's safe inline markup (`<u>...</u>`) is
preserved. If the question's number has an entry in
`explanations.json` (see EXPLANATIONS.md), an English explanation
block is appended to the back; a "low confidence" badge shows for
entries with `confidence < 7`.
4. Hand-roll the v11 Anki collection (SQLite + JSON config) and
package it as a `.apkg` ZIP with deterministic timestamps. By
default each build mints a fresh shuffle seed, so answers are
reshuffled every run. Pass `--seed` (and `--epoch`) for the
reproducible-build contract: same catalog + same seed + same
timestamp → byte-identical output across runs.
The Anki design decisions (shuffle seeding, deterministic build epoch,
SVG dark-mode handling, schema choices) live in `DESIGN.md` §7.
### Stage 2b — `amateurfunk_shorthand.py`
A sibling builder for a standalone reference deck of Q-groups and
operating abbreviations — the ones in the exam plus the most common
on-air shorthand the exam never covers (real operating knowledge, not
just the test). Content lives in the hand-curated `shorthand.json`
(editorial, tracked in git, like `explanations.json`); the
`references/Q-Codes.md` reference is where the exam-present codes were
catalogued.
Each code is one Anki *note* with two card templates — forward
(code → meaning) and reverse (meaning → code) — so a single record
drives both directions. A Q-group means one thing as a statement
(`QSO`) and another as a question (`QSO?`), so each Q-group yields two
notes; plain abbreviations yield one. All IDs/GUIDs are hashed from the
displayed code form (stable re-import). The deck is catalog-independent
and fully deterministic; it only consults `data/` to borrow the
manifest build epoch when present. Low-level apkg/SQLite machinery is
imported from `amateurfunk_anki.py` so the two stay in lockstep. The
glossary machinery shared with Stage 2c (two-template note type,
two-cards-per-note writer, packager, build-epoch resolver, entry
validator) also lives here.
### Stage 2c — `amateurfunk_technical.py`
A third glossary deck, same card mechanics as Stage 2b (one note,
forward+reverse templates), for the *technical* vocabulary rather than
operating shorthand: modulation/modes (SSB, FM, CW), signal domains
(NF, HF, ZF), building blocks (VFO, PLL, AGC), components, measurements
(dB, SWR, PEP), propagation, digital modes, and the
organisations/regulations (ITU, CEPT, EMV) — exam terms plus common HAM
abbreviations beyond the exam. Content lives in the curated
`technical.json`; each entry carries a German `category` (Betriebsart,
Bauteil, …) shown on the card and used as a `kategorie-*` tag. The
shared glossary machinery is imported from `amateurfunk_shorthand.py`;
this script only adds the data shape, the deck/model names, and the tag
scheme. IDs live in their own `technical` namespace so the two glossary
decks never collide on import.
## Repo conventions
- Python 3.11+, standard library only. No third-party dependencies
in either stage.
- Single-file scripts: `amateurfunk_fetch.py`, `amateurfunk_anki.py`.
No frameworks, no CLI library beyond `argparse`.
- Style: section banners, commented constants, docstrings on every
function, inline comments at decision points. The two scripts
intentionally read the same way.
- Outputs are build artifacts: kept under `data/` and `anki/`, both
gitignored.
- License attribution string (required by DL-DE→BY-2.0) is preserved
verbatim from the upstream `README.txt` whenever we redistribute the
data.
## Working on this repo
- `EXPLANATIONS.md` — the editorial contract for agents asked to add
or improve per-question explanations. The schema, the workflows
("explain everything unexplained", "improve everything below
confidence 7"), and the source/confidence guidance live there.
`explanations.json` is an empty `{}` until agents populate it.
- Start from `DESIGN.md` — it has the JSON schema, the question/answer
conventions (answer A is always correct upstream → consumers shuffle
before display), the LaTeX-in-questions caveat, the exam-structure
rationale for the Anki package layout (Betriebliche, Vorschriften,
and Technische split per license class), and per-stage design notes.
- Do not invent new download URLs; the ones in `DESIGN.md` were
verified against the live BNetzA site.
- When BNetzA publishes a new edition, expect a new
`fragenkatalog<N><rev>.json` filename inside the ZIP. The fetcher
must not hard-code the current name.
- Both stages have a fixture-driven test suite. Run with
`python3 -m unittest test_amateurfunk_fetch test_amateurfunk_anki`.
Network access is only needed for the manual smoke test of Stage 1.