Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
8759ee6
Tidying up with Claude
sesquideus Apr 23, 2026
e52ce52
Fixed one more bug
sesquideus Apr 23, 2026
0d3cf21
Vibecoding frenzy
sesquideus Apr 23, 2026
3ba896f
Removed property from mag
sesquideus Apr 23, 2026
1cc59f8
Tests now passing too
sesquideus Apr 23, 2026
11f3b49
Quantity operations added
sesquideus Apr 27, 2026
1de55e1
More Claude work
sesquideus Apr 27, 2026
3860321
More tests
sesquideus Apr 27, 2026
d3654f5
Fixed Náboj template list, newline after preamble
sesquideus Apr 28, 2026
0fefbb8
Math objects upgraded
sesquideus Apr 28, 2026
2f32787
Better display and bug hunting with Claude
sesquideus Apr 28, 2026
e1e0b8f
Converted to new format
sesquideus Apr 28, 2026
e649e0f
Fixed resurfacing bugs
sesquideus Apr 28, 2026
611d49f
Microfixes on train
sesquideus Apr 28, 2026
d23b30d
Added only_unit, reverted unit from property
sesquideus May 1, 2026
6bc4353
Docstrings
sesquideus May 1, 2026
b31e339
Claude's tests
sesquideus May 1, 2026
bcd4091
Restored properties, added .minted to .gitignore
sesquideus May 1, 2026
4c3c282
Switched to uv
sesquideus Jul 14, 2026
f9e3de1
Claude and constants
sesquideus Jul 26, 2026
b088732
Fixed faulty context schemas
sesquideus Jul 26, 2026
3589459
Added euro
sesquideus Jul 26, 2026
bce94f3
Ruffed
sesquideus Jul 26, 2026
dca8bbc
Update python-app.yml
sesquideus Jul 26, 2026
58825fb
Claude converted the workflow file
sesquideus Jul 26, 2026
4baf80d
Fixed uv action
sesquideus Jul 26, 2026
d60cc0f
Removed v9
sesquideus Jul 26, 2026
3f705ec
Maybe like this
sesquideus Jul 26, 2026
a82ab2a
Fixed Ohm
sesquideus Jul 26, 2026
39ceec8
Removed flake8
sesquideus Jul 26, 2026
11f4e0e
Removed ruff too
sesquideus Jul 26, 2026
de8f5f7
Ruffed completely
sesquideus Jul 26, 2026
576679e
Hotfixes
sesquideus Jul 27, 2026
2c93601
Claude Quantity Range and Product
sesquideus Jul 27, 2026
52b01ab
Tan and shortened symbol to s
sesquideus Jul 28, 2026
9b39dde
troy-ounce exact
MarekSliva Jul 28, 2026
77cf973
Added columns to 200
sesquideus Jul 28, 2026
d8b5ca4
Added C_V
sesquideus Jul 28, 2026
a98f533
refraction_air added
MarekSliva Jul 28, 2026
2d0583d
Updated constants and wrap mode
sesquideus Jul 28, 2026
8aa5c86
Expanded 'to' to other quantity
sesquideus Jul 28, 2026
6532b19
Approx filters + tests
sesquideus Jul 30, 2026
14aa09f
More precise constant
sesquideus Aug 1, 2026
10057b5
Completed math punctuation and approx filter families
sesquideus Aug 2, 2026
9d71ffb
Documented the pipeline, filters and quantities for contributors
sesquideus Aug 2, 2026
ddc200a
Removed minted
sesquideus Aug 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions .claude/skills/naboj-authoring/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
name: naboj-authoring
description: Author, edit, and debug Náboj competition problem sources (source/naboj/**) using DGS's Markdown+Jinja+LaTeX pipeline. Use whenever the user is creating a new problem, editing problem.md / solution.md / answer.md / preamble.md / meta.yaml under source/naboj/phys/*/problems/** or source/naboj/chem/*/problems/**, debugging mdcheck violations, Jinja MissingVariablesError, pandoc/XeLaTeX errors, or extending core/ or modules/naboj (new filters, new physical constants in core/data/constants.yaml, new LaTeX macros in core/latex/*.tex, new templates in modules/naboj/templates). Applies to all Náboj volumes (phys/26, phys/27, phys/28, chem/*, ...) — the format is stable across volumes with only minor differences.
---

# Náboj problem authoring (DGS)

DGS renders each problem through this pipeline:

```
source/naboj/<comp>/<vol>/problems/<pid>/<lang>/{problem,solution}.md (Markdown + Jinja)
+ source/naboj/<comp>/<vol>/problems/<pid>/{preamble,answer,answer-also,answer-interval}.md
+ source/naboj/<comp>/<vol>/problems/<pid>/meta.yaml
→ render/naboj/<comp>/<vol>/problems/<pid>/<lang>/*.md (pure Markdown, no Jinja)
→ build/naboj/<comp>/<vol>/problems/<pid>/<lang>/*.tex (pandoc → XeLaTeX)
→ PDF (booklet, tearoff, answers, solutions, ...)
```

Preamble + meta.yaml are prepended into the Jinja context; the Jinja renderer runs **twice** so
substituted equations get their inner tags expanded on the second pass. Then the DGS Markdown
style linter (`core/mdcheck`) runs, and pandoc converts to TeX using DGS's custom class
(`core/latex/dgs.cls`, plus `math.tex`, `symbols.tex`, `siunitx.tex`, `hacks.tex`).

## Where to start

**Route by task:**

- Authoring / editing a problem (`problem.md`, `solution.md`, `meta.yaml`, `preamble.md`,
`answer*.md`) → read `references/layout.md`, then `references/markdown-extensions.md`.
- Using `(§ … §)` templating, `@J set …`, math filters, `Q(…)`, `const.g`, etc. →
`references/jinja-templating.md`.
- Defining `values:` in meta.yaml, using `PhysicsQuantity`, `.eq`, `.approx`, `.widen`, ranges,
formatting filters (`|f2`, `|g3`, `|ef2`, `|af2`, `|w(0.05)`) →
`references/quantities-and-constants.md`.
- Using / adding custom LaTeX macros (`\Int`, `\Sum`, `\Ceil`, `\Nuclide`, `\Implies`, …) →
`references/latex-macros.md`.
- Style linter failures (missing spaces around `=`, `\SI` vs `\qty`, label conventions,
`\frac` in answers, …) → `references/markdown-extensions.md` §Style checker.

## Ground rules

1. **Do not invent macros.** Every math command not in vanilla amsmath comes from
`core/latex/*.tex`. Grep `core/latex/symbols.tex` and `core/latex/math.tex` before
introducing a new one.
2. **Use `\qty` / `\num` / `\ang` — never `\SI`.** `mdcheck` fails the build on `\SI`.
3. **Use `\Implies`, not `\implies` or `\Rightarrow`.** Same rule for `\Int`/`\Sum` over
`\int`/`\sum`, `\Ceil{…}` over `\lceil…\rceil`, `\ang{…}` over `^\circ`.
4. **Preserve spaces around binary operators** in math: `= \approx \doteq \geq \leq \gg \ll +
\cdot`. `mdcheck` flags each violation with a caret pointing at the column.
5. **Labels start with the problem id.** In `solution.md` you must add a
sublabel (`{#eq:archery:hd}`). In `problem.md` you can go bare
(`{#eq:archery}`) or sub-labelled (`{#fig:archery:diagram}`) — most
`problem.md` equations get no label at all.
6. **Answers use `\dfrac`, not `\frac`.** `mdcheck` enforces this on `answer.md` /
`answer-also.md` / `answer-interval.md`.
7. **Two-pass rendering matters.** If a value expands into a `\qty{…}` that itself contains
Jinja tags (e.g. from a MathObject), it still gets rendered on the second pass. Don't
try to escape or defer — just write the natural thing.

## Rendering pipeline entry points

- Convertor for problems: `modules.naboj.builder.renderer` (subclass of
`core.builder.renderer.CLIInterface`). Meta.yaml + preamble.md become the context.
- Jinja setup: `core/builder/jinja.py` — see `MarkdownJinjaRenderer` for the exact filter /
global table.
- Style linter: `core/markdown-check.py` runs `core/mdcheck/check.py` rules per line.
- Templates that assemble PDFs: `modules/naboj/templates/*.jtex` (these use `(* … *)` for
variables, unlike `.md` files which use `(§ … §)`).
- Build orchestration: root `Makefile` + `modules/naboj/module.mk` (rules
`NABOJ_TRANSLATABLE` for problem/solution, `NABOJ_NONTRANSLATABLE` for
answer/answer-also/answer-interval).

## Volume differences to be aware of

- `phys/28` uses `authors:` (plural, list). `phys/27` mostly uses `author:` (singular).
Both are tolerated; new problems should use `authors: [...]`.
- Older volumes often lack `values:` entirely; numbers are hard-coded in the Markdown.
- `chem/*` volumes use a flatter layout (`source/naboj/chem/<vol>/problems/<pid>/…`)
and use chemistry macros (`\ce{…}`, `\Nuclide{…}`, `\chemfig{…}`). Their `sk/` may be
the only language directory.

## Environment / build

The project uses **uv** (see recent commit "Switched to uv"). To build one problem:

```
uv run python -m modules.naboj.builder.renderer \
-C source/naboj/phys/28/problems/archery/meta.yaml \
-P source/naboj/phys/28/problems/archery/preamble.md \
source/naboj/phys/28/problems/archery/en/solution.md \
/tmp/out.md
```

Or via Make targets defined in `modules/naboj/module.mk`. Do not commit the `_minted-output`
directory (`.gitignore`d).
261 changes: 261 additions & 0 deletions .claude/skills/naboj-authoring/references/jinja-templating.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
# Jinja2 templating in DGS

DGS uses **two** Jinja environments with different delimiters. Get the file type
right and everything follows.

## Delimiters by file type

### Markdown files (`.md` under `source/`)

Custom delimiters chosen so as not to clash with Markdown / TeX syntax:

| Purpose | Delimiter | Example |
| ----------------------- | ---------------- | ----------------------------- |
| Variable expression | `(§ … §)` | `(§ v0|f2 §)` |
| Block statement | `(@ … @)` | `(@ if fig @)…(@ endif @)` |
| Comment | `(# … #)` | `(# note: dead code #)` |
| Line statement prefix | `@J ` (with space)| `@J set result = v0 + v1` |
| Line comment prefix | `%#` | `%# throwaway comment` |

`trim_blocks=True`, `autoescape=False`. Missing variables are **collected and
raised together** as `MissingVariablesError` after render — you'll see them all at
once. Filters may still catch undefined via `|default(...)` without triggering the
error.

### Static TeX templates (`.jtex` under `modules/naboj/templates/` and `core/templates/`)

Different delimiters — variables use `(* … *)`:

| Purpose | Delimiter | Example |
| ----------------------- | ---------------- | ----------------------------- |
| Variable expression | `(* … *)` | `(* problem.number *)` |
| Block statement | `(@ … @)` | `(@ if target=='booklet' @)…` |
| Comment | `(# … #)` | |
| Line statement prefix | `@J ` | |

Everything else about the environments is identical.

## Rendering flow for `.md` files

`core/builder/renderer.py::JinjaConvertor.run` does **two passes**:

1. Prepend `preamble.md` (if any).
2. First render pass: expands values, equations, `@J set …`.
3. Second render pass: re-renders the intermediate so that any Jinja tags that
appear **inside expanded content** (e.g. inside a MathObject) get expanded too.

This is why you can write things like `(§ result|f0 §)` in `answer.md` and have
`result` come from `preamble.md` — the preamble is prepended before the first pass.

The second pass matters most for `eq:` fragments: `(§ eq.foo|disp §)` expands
on pass 1 to a display equation whose body still contains raw `(§ … §)` tags
(the ones defined in the `eq:` YAML value); pass 2 evaluates those inner tags.
Without the second pass, `eq:` would be useless.

## Context available in Markdown Jinja

Provided by `core/builder/renderer.py::CLIInterface.build_context`:

- `id` — the problem id (from directory name).
- **Every key under `values:` in `meta.yaml`** becomes a variable in the local
scope. If the value is a dict with `magnitude:` and `unit:`, it becomes a
`PhysicsQuantity`; if a bare string/number, it's used as-is.
- **Every key under `eq:` in `meta.yaml`** becomes a `MathObject` accessible as
`eq.<name>`. The `MathObject`'s `.id` is set to `<problem-id>:<name>` so
`|disp` and `|align` emit a label automatically. See
`source/naboj/chem/04/problems/maliari/` for the canonical example — the
content of each `eq:` entry can itself contain `(§ … §)` tags, which get
expanded on the **second** render pass (see below).
- `const` — the physics constants dict from `core/data/constants.yaml`. Access as
`const.g`, `const.G`, `const.c`, `const.gforce`, ... aliases are also mapped.

## Filters (registered in `core/builder/jinja.py::MarkdownJinjaRenderer`)

Number formatting — precision-parameterised versions are pre-generated for 0–9
digits. Apply to `PhysicsQuantity`, `QuantityRange`, `QuantityList`,
`QuantityProduct`, or a raw number:

| Filter | What it does |
| -------------------- | --------------------------------------------------------- |
| `|f`, `|f0`, ..., `|f9` | Fixed-decimal formatting. `q|f2` → `\qty{50.00}{...}` |
| `|g`, `|g0`, ..., `|g9` | General formatting (may use scientific). `q|g3` |
| `|n` | Wrap raw value in `\num{…}` (no unit). |
| `|nf`, `|nf0..9` | `\num{…}` with fixed precision. |
| `|ng`, `|ng0..9` | `\num{…}` with general precision. |
| `|ef`, `|ef0..9` | `symbol = \qty{…}` fixed. Uses `PhysicsQuantity.symbol`. |
| `|eg`, `|eg0..9` | Same, general precision. |
| `|af`, `|af0..9` | `symbol \approx \qty{…}` fixed — like `|ef` but `\approx`. |
| `|ag`, `|ag0..9` | Same, general precision. |
| `|mag` | Extract raw magnitude (pint magnitude, not a string). |
| `|unit` | Just the unit, formatted as `\unit{…}`. |
| `|sim` | `.simplify()` — convert to base SI units. |
| `|w(value)`, `|widen(value)` | Construct a tolerance range: `x|w(0.05)` → ±5%. |

Every family exists both bare and suffixed `0`–`9`. Bare means "no precision in
the format spec", i.e. Python's default for that kind: `f` gives six decimals
(`\qty{96.700000}{…}`), `g` gives six significant digits with trailing zeros
dropped (`\qty{96.7}{…}`). In practice the bare `f` forms are rarely what you
want — reach for `|g`, `|eg`, `|ag` or an explicit precision.

`|ef*` vs `|af*` is purely the relation symbol (`=` vs `\approx`); use `|af*`
for rounded numeric results. Both read `q.symbol`, so a quantity without a
symbol renders the literal `None` — set `symbol:` in `values:` or use
`.alias('x')`.

For a `MathObject`:

| Filter | What it does |
| ------------- | ---------------------------------------------------------------- |
| `|inline` | Renders `$…$`. Write sentence punctuation **outside** the tag. |
| `|disp` | Renders `$$\n …\n$$ {#eq:<id>}`. |
| `|disp('.')` | Same with trailing punctuation inside the math. |
| `|align` | `$${\n …\n}$$ {#eq:<id>}` (aligned environment). |
| `|align(',')` | Same with punctuation. |
| `|dispd`, `|dispc`, `|disps`, `|dispq`, `|dispe` | Shorthands for `|disp` with `.` `,` `;` `?` `!`. |
| `|alignd`, `|alignc`, `|aligns`, `|alignq`, `|aligne` | Ditto for `|align`. |

Mnemonic: **d**ot, **c**omma, **s**emicolon, **q**uestion mark, **e**xclamation
mark — one suffix per member of `MathObject._INTERPUNCTION`, so the shorthands
cover every punctuation mark the formatter accepts.

The suffixed forms are `functools.partial`s with the punctuation already bound,
so they take **no** argument: `(§ eq|dispd('.') §)` raises `TypeError`.
They exist because `|dispc` reads better than `|disp(',')` in the middle of a
derivation, where nearly every equation ends in a comma or a full stop.

Punctuation rules, enforced in `MathObject.__format__`:

- Accepted trailing punctuation is exactly `. , ; ? !`.
- A bad trailing character after a valid base spec raises `ValueError` naming
the character; an unknown base spec raises `NotImplementedError`. The two are
deliberately distinct — the first is the common author typo.
- `|inline` accepts no punctuation; write it after the closing `$`.
- `disp` and `align` indent each content line by four spaces and append
`{#eq:<MathObject.id>}` — this is why `eq:` keys double as label names.

## Globals

Math functions available in Jinja expressions:

```
sin cos tan asin acos atan atan2
sqrt cbrt log log10 log2 exp pow
ceil floor
rad deg # radians ↔ degrees
gamma beta # Γ and B(x,y) = Γ(x)Γ(y)/Γ(x+y)
pi tau euler # constants
```

All of them work on raw numbers. On a `PhysicsQuantity` most are numpy ufuncs,
which dispatch to a same-named method on the object — so **only the subset that
`PhysicsQuantity` implements works**. Verified behaviour:

| Works on a `PhysicsQuantity` | Raises `TypeError` / `AttributeError` |
| ------------------------------------------------ | ------------------------------------- |
| `sin cos tan asin acos atan log deg ceil floor` | `cbrt log10 log2 exp rad atan2` |
| `sqrt` (implemented as `x ** 0.5`), `pow` | |

The failure message is opaque (`loop of ufunc does not support argument 0 of
type PhysicsQuantity which has no callable log10 method`). Fix by taking the
magnitude first: `log10(x|mag)` / `log10(x.mag)`, or add the missing method to
`core/builder/context/quantities/physics_quantity.py`. Note `deg(x)` works but
`rad(x)` does not — asymmetric because only `.degrees()` is implemented.

`sqrt` on a quantity whose unit is not a perfect square yields a fractional
exponent: `sqrt(Q(4, 'metre'))` → `\qty{2}{\meter\tothe{0.500}}`. That is almost
always an authoring bug; take `.mag` or fix the expression's dimensions.

Constructors (short aliases in parentheses):

```
Q(magnitude, unit) # ad-hoc PhysicsQuantity, e.g. Q(100, '%')
QuantityList(q1, q2, q3) # (QL) combine several commensurate quantities into a list
QuantityProduct(q1, q2, q3) # (QP) combine several commensurate quantities into a product (e.g. box dimensions)
QuantityRange(lo, hi) # (QR) build a range directly; equivalent to `lo % hi`
```

Both the long names and the short aliases are registered, so `QR(a, b)` and
`QuantityRange(a, b)` are the same global.

`q1 % q2` and `q.widen(v)` remain the idiomatic ways to build a `QuantityRange` (see
quantities-and-constants.md); `QR(lo, hi)` is there for when the operands aren't
bare variables and `%` would need extra parens anyway.

## Units beyond SI

The pint registry lives in `core/builder/jinja.py` and is installed as pint's
*application* registry, so every module shares it. Two local extensions:

- **Currency.** `eur` is defined as its own dimension `[currency]`, with `€` and
`EUR` as symbols; a preprocessor rewrites a literal `€` in unit strings to
`EUR`. So `Q(3, 'eur')|f2` → `\qty{3.00}{\eur}` (`\eur` is declared in
`core/latex/siunitx.tex`). Currency is not commensurate with anything else, as
intended.
- **Temperature.** `PhysicsQuantity.format_struct` rewrites pint's
`\degree_Celsius` → `\celsius` and `\delta_degree_Celsius` → `\dcelsius`
(both declared in `siunitx.tex`). Use `'degC'` for absolute temperatures and
`'delta_degC'` for differences. `absolute + delta` is fine
(`20 °C + 5 Δ°C = 25 °C`), but multiplying or dividing an absolute Celsius
value raises pint's `OffsetUnitCalculusError` — convert with `.to('kelvin')`
before doing arithmetic that scales it.

## `@J set` — the everyday case

Used almost exclusively inside `preamble.md`:

```
@J set result = v0**2 / const.g.approx - sqrt((v0**4 / const.g.approx**2) - D**2)
@J set result_exact = v0**2 / const.g - sqrt((v0**4 / const.g**2) - D**2)
```

The `@J` (line-statement prefix) form is preferred over `(@ set … @)` for
readability. Idiomatic pattern: compute a rounded-constant version for `answer.md`
and an exact-constant version for `solution.md`.

## Control flow (`.jtex` templates)

Standard Jinja `if / for` blocks with the DGS delimiters:

```
(@ for vid, venue in volume.venues.items() @)%
(* venue.head *) ((* i18n[language.id].venues[vid] *))%
(@ endfor @)%
```

Whitespace control follows Jinja: `(@- … -@)` trims surrounding whitespace.

Path helpers available in static templates:

- `path_exists('some/path')` — check whether a file exists (used to conditionally
include `answer-extra.md`, etc.).
- `file_size('some/path')` — file size in bytes.

## MissingVariablesError

If any Jinja variable used at render time is not found, the collector aggregates
them and raises `MissingVariablesError`. The message lists every missing name in
insertion order. Typical causes:

- Typo in `(§ v_0 §)` when the value is defined as `v0`.
- Value defined in preamble but preamble file not prepended (check that
`preamble.md` exists at the problem level — the Makefile falls back to a
no-preamble rule if it's missing).
- Cross-problem reference. Values are scoped to a single problem.

## Common pitfalls

- **Space around `@J`.** It's a line statement *prefix*, not a delimiter. Write
`@J set …` (with a space). No leading whitespace either — must be column 0.
- **`(§ … §)` in `.jtex` won't work.** The `.jtex` environment uses `(* … *)` for
variables. Conversely, `(* … *)` in `.md` is a literal `(*` `*)`, not a
variable.
- **Numeric arithmetic on `PhysicsQuantity` returns `PhysicsQuantity`.** So
`(§ v0 * 2 §)` becomes `\qty{100}{\metre\per\second}`. If you want a bare number,
use `.mag` or `|mag`.
- **Ranges (`QuantityRange`) via `%`.** `q1 % q2` constructs a range with `q1` as
minimum and `q2` as maximum. If `q1 > q2`, pint raises. Prefer the
`.widen(fraction)` method when you want a symmetric tolerance band.
- **Constants inside math.** `const.g.approx` gives a rounded-magnitude
`PhysicsQuantity`; use it in expressions. `const.g.symbol` (or `const.g.sym`)
gives the TeX symbol string. `const.g.full` gives the printable full-precision
form.
Loading