A template entry may now narrow its pattern and carry a note.
TemplateEntry gains requires (per segment kind,
the tokens a name must carry), excludes (segment kinds a name
must not fill) and note (one sentence). The accessor exposes
the note on AppliedTemplate.note; the consumer renders it
after the entry's documentation opening.
requires is satisfied when the name carries at least one of
the listed tokens for that segment kind. The population, aggregation and
state segments are closed single-token segments, so the intersection test
reduces to membership; stated as an intersection it also holds for a
segment that admits several tokens. excludes is satisfied when
the name fills none of the listed segment kinds. The name's tokens per
segment kind come from the parse: the accessor groups them once in
_ir_segment_tokens, and _ir_tokens is now that
grouping's union rather than a second walk of the IR.
Precedence rule. Selection runs the subset test, then the conditions, then
precedence: among matching entries that share a base and the same
required segment kinds, a conditioned entry supersedes its unconditioned
siblings — the conditioned entry is selected and the unconditioned sibling
is dropped for that name. A conditioned entry and an unconditioned
sibling therefore never both render for one name, so a note placed on the
conditioned entry reaches only the names it admits. An entry with no
conditions selects exactly as before, and entries that share a base but
differ in their required segment kinds (the several
temperature openings) are not siblings and all still apply.
Reversing the rule — the unconditioned sibling wins when both match — makes
test_conditioned_entry_supersedes_its_unconditioned_sibling
fail.
The note rides the template, not the applied name: it is emitted on every name that selects the entry and on no other. Because a name that selects two openings of the same base (a species and an aggregation opening) may carry a note from each, a note is written on the single entry whose pattern it separates, so it still renders once per name.
Every shipped template selects as before. The shipped
templates.yml carries no condition and no note, so the new
branch is a no-op.
test_every_existing_template_selects_as_before asserts all
shipped entries are unconditioned and, for the names the existing
convention tests render, that the selected
(base, segments) tuples equal an independent reimplementation
of the plain subset test.
Follow-on folded in: _entry_definitions in
vocab_loaders.py no longer opens and parses its YAML itself;
it reads through _load_yaml like the other loaders.
Measure. tests/grammar/test_template_conditions.py (new)
exercises a required token, an excluded kind, a note on a selecting and a
non-selecting name, a name selecting two openings, the loader refusals for
an unknown required token and an unknown excluded kind, the precedence
rule, and the shipped-registry no-op.
Gate: PYTHONPATH=$PWD uv run --no-sync pytest -p no:cacheprovider
tests/grammar -q ran 1212 passed, 32 skipped (EXIT=0) at head
versus 1204 passed, 32 skipped (EXIT=0) at base — eight added tests, zero
added failures, judged by delta.
Negative control, declared as dropping the condition filter from
_select_templates and holding everything else fixed. With that
filter removed, tests/grammar/test_template_conditions.py ran
3 failed, 5 passed (EXIT=1), failing the required-token test, the
excluded-kind test and the precedence test — all passing at head. Two
additional mutations were run and are recorded in the run directory: drop
note=entry.note from the applied template (3 failed, EXIT=1,
including both note tests), and reverse the precedence rule (1 failed,
EXIT=1, the precedence test). Log:
negative-control.log in the run directory.
No figure accompanies this fragment: the change is a selection rule and a field, and the tests above carry it. The figure directory given to this node is left empty.