Files
Amateurfunk-Anki/EXPLANATIONS.md
T

619 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Explanations — agent-authored answer rationales
This document is the working contract for any AI agent (or human)
asked to **add or improve explanations** for questions in the BNetzA
amateur-radio catalog. The decks built by `amateurfunk_anki.py`
optionally append an English explanation block to the back of each
card; the text of those explanations lives in `explanations.json` at
the repo root and is edited by hand (or by an agent following this
file).
The build is non-blocking on this data: a missing file or missing
entry just produces a card without an explanation block. Adding an
entry is purely additive — no regenerate ceremony beyond
`make anki`.
---
## 1. File location and shape
- **Path:** `explanations.json` at the repo root. Tracked in git.
- **Encoding:** UTF-8, no BOM, two-space indent for readability.
- **Top level:** a JSON object keyed by **question number** exactly
as it appears in the catalog (e.g. `"NA101"`, `"BA205"`,
`"EH410"`). Keys are case-sensitive.
- **Sort order:** keep keys in alphabetical order. Diffs stay clean
and merge conflicts get easier; the build does not care.
### Per-entry schema
Every entry MUST have these four required fields, and MAY carry the one
optional field below:
| Field | Type | Constraint |
|---------------|---------|--------------------------------------------|
| `revision` | integer | `>= 1`. Starts at `1`, bumps on improvement |
| `explanation` | string | Non-empty. **English.** Correct & helpful, WHY-focused |
| `source` | string | Non-empty. URL or citation like `AFuV §16(2)` |
| `confidence` | integer | `1..10` inclusive. See scale in §5 |
| `provenance` | string | *Optional.* Only allowed value: `"50ohm-loesungsweg"` |
`provenance` records **how the text was produced**, which `source` (a
citation) does not. Set it to `"50ohm-loesungsweg"` only on entries
that are genuinely a translation/condensation of a 50ohm.de worked
solution (`contents/solutions/<ID>.md` in `DARC-e-V/50ohm-contents-dl`).
The build uses it — and *not* the `source` domain — to decide whether
to show the CC BY 4.0 derivative-work credit on the card (CC BY
requires naming the author team and indicating modification). Do **not**
add it just because an entry cites a 50ohm.de study page; merely citing
a page is not a derivative work. Any other value, or any other extra
field, is rejected by `load_explanations()` with
`unknown fields [...]` / `provenance must be one of [...]`. The loader
is also strict about types: a JSON `true` will not satisfy the integer
contract for `revision` or `confidence`. To track other editorial
metadata, propose a schema change rather than smuggling fields in.
Top-level keys (the question numbers) must also match the catalog
exactly. An entry keyed on a number that no live question carries
(typo, stale ID after a catalog revision) fails the build with
`explanation keys not present in the catalog: ...`. Fix the key, or
remove the entry.
### Minimal example
```json
{
"NA101": {
"revision": 1,
"explanation": "100 m weighs 210 g, so 55 g is 55/210 of 100 m ≈ 26.2 m. Mass scales linearly with length for the same wire.",
"source": "https://50ohm.de/lernen/wissen/elektrotechnik-mathematik",
"confidence": 8
}
}
```
The build appends this on the back card as a styled block headed
**Explanation**, with the source rendered as a clickable link when
it looks like an HTTP(S) URL.
---
## 2. How the explanation reaches the card
`amateurfunk_anki.py` does the wiring; the agent does not need to
modify Python code to ship a new explanation. End-to-end:
1. `load_explanations(./explanations.json)` parses the file and
validates each entry against the schema above. A malformed entry
is a hard build error.
2. While rendering a card, `render_question()` looks up the
question's `number` in the dict. On a hit, `render_explanation()`
produces an HTML block: header "Explanation", the body (with
inline `$...$` LaTeX rewritten to MathJax `\(...\)`, same as the
question text), and a "Source: ..." line.
3. The block lands inside `.af-back` at the very end, styled by the
`.af-explanation*` CSS rules — serif body, sans-serif
metadata, separated from the answer by a top border. When the
entry's `confidence` is **below 7**, a small "low confidence"
badge appears next to the **Explanation** header — a hint to the
learner that the reasoning may be incomplete or weakly sourced.
`revision` is never shown on the card.
4. The note GUID is keyed on `category.slug:number`, NOT on field
content, so adding/changing an explanation does **not** create
duplicate cards on re-import — Anki updates the existing note in
place.
Run `make anki` (or `python3 amateurfunk_anki.py`) after editing the
file. The CLI summary line will show `... N with explanations` per
deck.
---
## 3. Locating a question to explain
Questions live in the JSON catalog under the per-edition data
directory:
```
data/<slug>/fragenkatalog<edition>.json
```
To find one by number, e.g. `EH410`:
```sh
python3 -c '
import json, glob
for path in glob.glob("data/*/fragenkatalog*.json"):
cat = json.load(open(path))
def walk(node, prefix=()):
for q in node.get("questions", []) or []:
if q["number"] == "EH410":
print(json.dumps(q, ensure_ascii=False, indent=2))
print(" / ".join(prefix))
for s in node.get("sections", []) or []:
walk(s, prefix + (s.get("title", "?"),))
walk(cat)
'
```
You get the question stem, the four answers (`answer_a` is the
**correct** one upstream; the deck shuffles before display), the
class digit (`"1"` = N, `"2"` = E, `"3"` = A), and the section path.
That path tells you the topic context — useful when choosing a
source citation.
---
## 4. Writing the explanation
### Style
- **Language: English.** The questions and answers stay in German
on the card; the explanation is the one English element. Don't
switch.
- **Correct and helpful first; length follows the topic.** There is
no sentence cap. Use as much room as the question genuinely needs —
a one-line arithmetic restatement for a simple lookup, a full
worked derivation or a multi-step rule walkthrough where that is
what makes the answer *click*. Don't pad: every sentence should
earn its place by adding correctness, context, or a usable hook.
The card already shows the question, the correct answer text, and
the breadcrumb — the explanation adds the missing *why* and the
understanding needed to get there, not a restatement of those.
- **Lead with the reasoning.** Don't open with "The correct answer
is X because..." — the card already declares the correct answer
one line above. Go straight to the principle, the formula, the
rule of thumb.
- **Math.** Use inline `$...$`; it's rewritten to MathJax on the
card, same as for question text. Don't use display math `$$...$$`
(the deck has no MathJax support for those). Getting the *inside* of
`$...$` right is its own discipline — see **§4a Math typesetting**
below. This is the single most error-prone part of this file.
- **Inline emphasis.** `<u>...</u>` is the only HTML you should
type by hand. Everything else gets HTML-escaped.
- **No spoilers about other answers.** If a distractor is a common
trap, naming the trap is fine; quoting the distractor's text is
noise.
### 4a. Math typesetting (MathJax) — get this right
Everything between `$...$` is TeX. MathJax renders **bare multi-letter
runs as a product of italic variables**, so `ohm` shows as *o·h·m*,
`log10` as *l·o·g·10*, `P_{PEP}` as *P·(P·E·P)*. A whole multi-round
review on this repo was spent fixing exactly these. The rules below are
the checklist; wrong → right.
**Units** — never leave a unit as bare letters; wrap in `\text{}` (or use
the symbol) and put a thin space `\,` after the number:
| Wrong (in `$...$`) | Right |
|---|---|
| `0.01 ohm`, `10 kOhm`, `5 MOhm` | `0.01\,\Omega`, `10\,\text{k}\Omega`, `5\,\text{M}\Omega` |
| `3 microhenry`, `3 uH` | `3\,\mu\text{H}` |
| `1 uF`, `100 nF`, `47 pF` | `1\,\mu\text{F}`, `100\,\text{nF}`, `47\,\text{pF}` |
| `5 mA`, `2 mV`, `1 kHz`, `28 MHz` | `5\,\text{mA}`, `2\,\text{mV}`, `1\,\text{kHz}`, `28\,\text{MHz}` |
| `12 dB`, `8 dBi`, `46.8 bit/s` | `12\,\text{dB}`, `8\,\text{dBi}`, `46.8\,\text{bit/s}` |
| `5 mm2`, `2.5 A/mm2` | `5\,\text{mm}^2`, `2.5\,\text{A/mm}^2` |
| `360 degrees`, `45°` | `360^\circ`, `45^\circ` |
| `20 A`, `12 V`, `100 W`, `5 m`, `50 s` | `20\,\text{A}`, `12\,\text{V}`, `100\,\text{W}`, `5\,\text{m}`, `50\,\text{s}` |
**Operators, functions, Greek, powers:**
| Wrong | Right | Why |
|---|---|---|
| `log10(x)` | `\log_{10}(x)` | bare `log` is *l·o·g* |
| `10^(20/10)` | `10^{20/10}` | `^(` superscripts only the `(`; braces group |
| `lambda`, `pi`, `tau`, `omega`, `rho`, `mu` | `\lambda`, `\pi`, `\tau`, … | bare = italic letters |
| `sin`, `cos`, `ln` | `\sin`, `\cos`, `\ln` | |
| `2 x 3`, `R1 || R2` | `2 \cdot 3`, `R_1 \parallel R_2` | `x`/`||` are not operators |
| `62.5%` | `62.5\%` | **bare `%` is a TeX comment — it silently eats the rest of the math** |
| `4,200,000` | `4{,}200{,}000` | bare `,` gets TeX punctuation spacing |
**Subscripts.** Descriptive multi-letter labels/acronyms are roman; a
single-letter subscript stays italic:
| Wrong | Right |
|---|---|
| `P_{EIRP}`, `U_{peak}`, `f_{mod}`, `V_{BE}`, `f_{sum}` | `P_\mathrm{EIRP}`, `U_\mathrm{peak}`, `f_\mathrm{mod}`, `V_\mathrm{BE}`, `f_\mathrm{sum}` |
| `R1/R2 = R3/R4` | `R_1/R_2 = R_3/R_4` |
| keep italic: `U_F`, `f_c`, `X_C`, `R_g` | (single letter = variable, leave as-is) |
A standalone acronym used as a variable (e.g. `MUF` on a formula's LHS)
also goes roman: `\mathrm{MUF}`.
**Dimensional constants keep their unit.** The field-strength constant is
`30\,\Omega`, so write `\sqrt{30\,\Omega \cdot P_\mathrm{EIRP}}` and
`(E\cdot d)^2/(30\,\Omega)`, not a bare `30`. Same idea for the dB-level
exponent: `10^{g/(10\,\text{dB})}` (the `(...)` groups the denominator —
`10^{g/10\,\text{dB}}` parses as `(g/10)·dB`).
**Unit vs. variable — don't blindly unit-ize a single letter.** `A` is
amperes in `20 A` but *area* in `N^2 A/l`; `s`/`m` are seconds/metres in
`50 s` / `5 m` but could be variables elsewhere. The tell: a *free-standing
number* immediately before the letter (`20 A`) means a unit; an exponent
base (`N^2 A`) or a symbolic factor (`S \cdot A`) means a variable. When
unsure, read the sentence.
**What stays prose — do NOT force into `$...$`:**
- Band/wavelength designations: `5/8-wave`, `20/15/10 m trap dipole`,
`the 80 m band`.
- Conceptual word-equations: `1st overtone = 2nd harmonic`,
`Region 1 = Europe/Africa`.
- Units mentioned adjectivally in a sentence: "a 50 ohm antenna",
"the 28 V/m limit", "about 11.7 V/m".
But a **worked computation with an `=`** (e.g. `230 V / 20 = 11.5 V`)
belongs in `$...$`, fully typeset.
### Verifying it — the process lessons
These review rounds kept finding *peers* of an already-fixed defect.
Avoid the repeat:
1. **Fix the whole record, not a substring.** The same formula usually
appears in *both* the explanation body *and* the `Hilfsmittel:` note.
Search the entire entry and fix every occurrence.
2. **Fix the whole class, file-wide — not just the IDs someone listed.**
If `P_{EIRP}` is wrong in one card it is wrong in twenty; sweep the
whole file for the pattern.
3. **Verify with a positive sweep, not a blacklist.** A list of
known-bad tokens always misses a new category (it missed `degrees`,
`bit`, `||`, bare seconds…). Instead: strip `\text{}`/`\mathrm{}` and
the known macros, then flag *every remaining* ≥2-letter run inside
`$...$` — each survivor must be a deliberate variable product
(`LC`, `jX`, `RC`, `di/dt`) or it's a bug.
4. **Don't claim "0 / exhaustive" unless the check actually covers that
category.** Report what you checked, not a blanket adjective.
A ready-made sweep:
```sh
python3 -c '
import json, re
d = json.load(open("explanations.json"))
span = re.compile(r"\$[^$]*\$")
def strip(s):
s = re.sub(r"\\(?:text|mathrm|operatorname)\{[^{}]*\}", " ", s)
return re.sub(r"\\[A-Za-z]+", " ", s) # drop macros (\sqrt, \pi, \cdot, ...)
for k, e in d.items():
ex = e["explanation"] if isinstance(e, dict) else ""
if ex.count("$") % 2 or ex.count("{") != ex.count("}"):
print("BALANCE", k)
for m in span.finditer(ex):
s = m.group(0)
if re.search(r"(?<!\\)%", s) or "||" in s or "°" in s \
or re.search(r"(?<!\\)\blog10\b|10\^\(", s) \
or re.search(r"_\{[A-Za-z][A-Za-z,]*\}", s) \
or re.search(r"(?<=\d),(?=\d\d\d)", s):
print("ISSUE", k, m.group(0)[:60]); break
for tok in re.findall(r"[A-Za-z]{2,}", strip(s)):
# whitelist genuine products / kept tokens; investigate the rest
if tok not in {"LC","RC","RA","UI","CU","PR","jX","fL","fC","di","dt"}:
print("TOKEN?", k, tok, "|", m.group(0)[:60])
'
```
Anything it prints is either a real defect or a new legitimate product to
add to the whitelist — look, don't assume.
### What "WHY-focused" means in practice
| Less useful | More useful |
|----------------------------------------------|-----------------------------------------------|
| "The correct answer is 26.2 m." | "55 g is 55/210 of 100 m by mass scaling." |
| "Because regulations require it." | "AFuV §16(2): only class A may operate ≤10 m." |
| "It's how the formula works." | "Q is reactance over resistance, so Q rises as R falls." |
If the *only* honest explanation is "memorize the table" (e.g. a
band-plan lookup), say that plainly and cite the band plan in
`source` — confidence stays low (34) until someone finds a deeper
hook.
### The Hilfsmittel note
Candidates may use the official exam aid (`Hilfsmittel_12062024.pdf`)
during the exam. Its complete contents — every formula and table, with
**printed page numbers** — are catalogued in
[`references/Hilfsmittel.md`](references/Hilfsmittel.md). When a
question's answer is a lookup or a direct application of something in
that sheet, append a short note to the end of the `explanation` body:
```
... <u>Hilfsmittel:</u> <pointer>
```
Rules — this is the part that was historically done badly (generic
boilerplate stamped on everything), so be strict:
- **Only cite what is actually in the sheet.** Verify against
`references/Hilfsmittel.md`. If the fact the question turns on is a
memory item — diode forward voltages (~0.6 V / ~0.3 V), `tan δ = 1/Q`,
S-meter step = 6 dB, harmonics = n × fundamental, ppm/percent
arithmetic, semiconductor behaviour, definitions, antenna
length/shortening factors — **do not add a Hilfsmittel note at all.**
- **Name the specific formula(s) or table, and the page.** Not "the
formula is in the Formelsammlung" but e.g. `P = U²/R (Leistung,
S.12)` or `the Widerstands-Farbcode table (S.11)`.
- **For multi-step calculations, give the order:** "first
`U_eff = Û/√2` (Wechselspannung, S.12), then `P = U_eff²/R`
(Leistung, S.12)". State plainly when a value comes from outside the
sheet (e.g. a diode drop) versus from a sheet formula.
- **For a pure table lookup**, say it is a lookup and cite the table +
page (e.g. "a table lookup, not a memory item — the band limits …
are in the Frequenzbereichszuweisung (Anlage 1, Tabellarische
Übersicht, S. 23)").
- **Page numbering:** the printed page number = PDF page 2 (the cover
and the "Hinweis" page are unnumbered). Always cite the printed
number, as `references/Hilfsmittel.md` does.
---
## 5. Confidence scale
| Score | Meaning |
|------:|------------------------------------------------------------------------|
| 10 | Direct quote / arithmetic restatement from a primary legal source. |
| 89 | Derivation from a well-known formula or law text; no ambiguity. |
| 67 | Reasoning sound but condensed; a reviewer might want one more line. |
| 45 | Educated guess based on context; corroborating source is weak. |
| 13 | Best-effort placeholder. Flag for re-explanation. |
When in doubt, score lower. The query "explain everything below
confidence 7" is a real workflow — a too-generous score hides work
that should be redone.
The score is also capped by the source tier — see §6 "Source ↔
confidence linkage". A tier-8 (general web) source caps confidence
at 5 regardless of how persuasive the prose is.
**Card surface.** Confidence below 7 also surfaces a "low
confidence" badge on the card itself (see §2). This is deliberate:
the learner sees that the explanation is provisional rather than
trusting it as definitive. Scoring 7 turns the badge off, so do
not nudge a 6 to a 7 just to clear the badge — fix the explanation
or the source first.
---
## 6. Sources
Use the **strictest tier that actually answers the question**. Drop
down a tier only when the higher one doesn't cover the topic — never
because a lower-tier source is easier to find. The exam tests
knowledge of German law and BNetzA-curated material; a primary-law
question backed by a random tutorial site is a worse explanation
than no explanation at all.
The full priority order, top (most preferred) to bottom:
1. **Primary German law.**
- AFuG (Amateurfunkgesetz), AFuV (Amateurfunkverordnung), BEMFV,
TKG when applicable, plus frequency-allocation ordinances
(vfg / VVnömL).
- Canonical URL pattern: `https://www.gesetze-im-internet.de/<gesetz>/__<§>.html`.
Cite as `AFuV §16(2)` style. Use the URL form when possible so
the card link is clickable.
- If a question's answer is fixed by German law, **no lower tier
is acceptable** as the sole source.
2. **EU / international regulation binding on Germany.**
- CEPT recommendations (T/R 61-01, T/R 61-02), ITU Radio
Regulations, ECC decisions, EU EMC / RED directives.
- Use `docdb.cept.org/...` or the ITU document portal as the
canonical URL.
- Reach for this tier when the question references CEPT
licensing, foreign operation, or harmonised band plans that
AFuV doesn't restate.
3. **BNetzA official publications.**
- The question catalog's own `README.txt`, BNetzA "Vfg"
announcements, the German band plan as published by BNetzA,
official explanatory notes.
- Useful when primary law is too terse to convey the *why* on
its own — these are the regulator's own gloss on the law.
- Do not cite the question catalog PDF/ZIP itself as an
explanation source. It proves what the official answer is, but
it usually does not explain why that answer is right. Use a
law, regulation, BNetzA notice, DARC/50ohm study page, standard,
or other source that actually supports the explanation.
4. **German amateur-radio organisations.**
- DARC publications and band plan (`darc.de`), Runder Tisch
Amateurfunk (RTA) papers, **50ohm.de** (the community study
site linked from `README.md`).
- Exam-aligned by community convention; well-suited to the WHY
when law text alone isn't pedagogically clear.
5. **International amateur-radio organisations.**
- IARU Region 1 documents (Germany is in R1) and DARC's IARU R1
liaison material; ARRL publications when they explain the
underlying physics or an internationally harmonised band plan.
- ARRL is **not** a source for German licensing rules — don't
use it that way.
6. **Standards bodies and primary technical references.**
- NIST, IEEE, IEC, ITU-R recommendations, ETSI standards (when
not already binding via tier 2).
- Appropriate for purely technical questions — units, defining
equations, definitions — where no amateur-radio-specific
source applies.
7. **Established engineering references.**
- University lecture notes hosted by the university, recognised
EE textbook publishers, well-known authors' personal sites.
- Use only when tiers 16 don't cover the topic.
8. **General web technical sites — last resort.**
- Wikipedia, tutorial sites (`allaboutcircuits.com`,
`amateur-radio-wiki.net`, `biopac.com`, etc.), random
manufacturer pages, blog posts.
- **Treat citations at this tier as temporary.** Score
`confidence` ≤ 5 and consider the entry a §8.3 candidate for
re-sourcing the moment a higher-tier source is identified.
Never use a tier-8 source as the sole citation for a question
that has a clear answer in tiers 14.
### Source ↔ confidence linkage
The §5 confidence scale isn't independent of the tier:
- Tier 12 + sound derivation → 910 is appropriate.
- Tier 34 → cap at 8 unless the source quotes a primary citation.
- Tier 56 → cap at 7.
- Tier 7 → cap at 6.
- Tier 8 (sole source) → cap at 5. Flag for re-sourcing.
This isn't a hard limit the validator enforces — it's editorial
discipline. If you find yourself wanting confidence 9 with a
tier-7 source, you almost always either (a) need to find the
tier-14 source that backs the same claim, or (b) are about to
overstate certainty.
### Form
If the source is a URL, store it as one — `_source_html()` makes
plain `http://` / `https://` strings clickable. Mixed text+URL
sources are stored verbatim and rendered as escaped text (no auto
link), so prefer one or the other. A short citation like
`AFuV §16(2), gesetze-im-internet.de/afuv_2005/__16.html` is
acceptable but loses click-through. When possible, narrow the
source to a single canonical URL.
---
## 7. Revisions
`revision` is editorial bookkeeping; it never appears on the card.
- A brand-new entry starts at `revision: 1`.
- **Bump** when you materially improve the explanation, broaden the
source, or fix an error. A typo fix is not a revision bump.
- Don't lower the revision. If a previous revision was wrong, fix
it forward and explain the fix in the commit message.
- A revision bump usually pairs with a confidence bump (or, more
rarely, with a deliberate confidence reset to indicate the
reviewer is less sure than the prior author was).
---
## 8. Workflows
These are the common ways the file gets edited. The agent should
read the request and pick one.
### 8.1 "Explain question X"
1. Look up X in the catalog (§3) — read the stem, the correct
answer (`answer_a` upstream), and the section path.
2. Identify *why* the correct answer is correct. Reach for a
primary source before paraphrasing 50ohm.de.
3. Draft the body per §4, pick a source per §6, score honestly
per §5.
4. Open `explanations.json`, insert the new entry in **alphabetical
key order**, save.
5. Run `python3 -m unittest test_amateurfunk_anki` — the schema
validator runs as part of the build path used by tests, so a
typo'd entry fails fast.
6. Rebuild decks (`make anki`) and spot-check the new entry on
the back of the card.
### 8.2 "Explain every question that doesn't have an entry yet"
This is bulk work — handle it in chunks, not as one giant batch.
1. Compute the unexplained set:
```sh
python3 -c '
import json, glob
catalog = next(iter(glob.glob("data/*/fragenkatalog*.json")))
exp = json.load(open("explanations.json"))
def walk(n):
for q in n.get("questions", []) or []: yield q["number"]
for s in n.get("sections", []) or []: yield from walk(s)
missing = sorted(set(walk(json.load(open(catalog)))) - exp.keys())
print(len(missing), "questions unexplained")
print("\n".join(missing[:20]))
'
```
2. Process in groups of ~10 within one topic. Same section path
often shares one source citation, so context stays warm and
confidence scores stay consistent.
3. After each group, save and run the tests. Don't accumulate
hundreds of unsaved entries.
### 8.3 "Improve all entries with confidence below N"
1. List low-confidence entries:
```sh
python3 -c '
import json
exp = json.load(open("explanations.json"))
for k, v in sorted(exp.items()):
if v["confidence"] < 7: print(k, v["confidence"], "—", v["source"])
'
```
2. For each, look up the question (§3), seek a stronger source
(§6), rewrite if the new source changes the framing.
3. **Bump `revision`** and update `confidence` to reflect the new
evidence. Do not just bump confidence without changing anything
else — that's how bad explanations entrench themselves.
### 8.4 "This explanation is wrong"
1. Read the current entry. Confirm the error against the catalog
stem and the cited source.
2. Rewrite the body, replace the source if needed, bump
`revision`, and re-score `confidence` from scratch (don't carry
over the prior score).
3. Commit message should name the question number and summarize
what was wrong — future agents need to know what kind of
mistake to look for elsewhere.
---
## 9. Validation checklist
Before saving, confirm:
- [ ] Key is the exact catalog `number` (case-sensitive).
- [ ] All four fields present, correct types, in range.
- [ ] `explanation` is English, correct, helpful, WHY-focused — as
long as the topic needs, no longer.
- [ ] `source` is non-empty and as primary as possible.
- [ ] `revision` is correct (1 for new, bumped for improvement).
- [ ] `confidence` honestly reflects how well the source supports
the explanation.
- [ ] Keys remain in alphabetical order in the file.
- [ ] **Math typesetting (§4a):** every formula is in `$...$`; units are
`\text{}`/symbols with a leading `\,`; no bare `log10`, `10^(`,
Greek words, `%`, `||`, multi-letter `_{...}` subscripts, or raw
thousands commas; dimensional constants keep their unit. Fix every
occurrence in **both** the body and the `Hilfsmittel:` note.
- [ ] The §4a verification sweep prints nothing unexpected (run it after
any batch of math edits — don't trust a per-ID fix to have caught
the peers).
- [ ] `python3 -m unittest test_amateurfunk_anki` passes. (The full deck
suite is `test_amateurfunk_fetch test_amateurfunk_anki
test_amateurfunk_shorthand test_amateurfunk_technical` — 95 tests;
the two-module subset that validates explanation schema is 65.)
---
## 10. What's out of scope here
- **Translating questions into English.** The cards are bilingual
by design: German question + English explanation. Don't
paraphrase the German.
- **Editing the catalog itself.** `data/` is a build artifact —
refreshed by `amateurfunk_fetch.py` from BNetzA. Mistakes in the
upstream questions are upstream's to fix.
- **Removing explanations.** If an entry is genuinely useless, fix
it forward (§8.4). Don't delete keys — that loses the audit
trail. (The schema doesn't currently encode "retracted", so if
you ever need it, propose a schema change.)