feat(m1): add strict frontmatter parser with documented grammar

This commit is contained in:
Kjell Tore Guttormsen 2026-09-04 19:26:18 +02:00
commit a0874a5e17
2 changed files with 413 additions and 0 deletions

127
tests/test_frontmatter.py Normal file
View file

@ -0,0 +1,127 @@
"""The strict frontmatter grammar for kandidat.md and sak.md (plan Step 4).
Risk H7: this is a hand-rolled parser sitting on the hard-filter path. A YAML
runtime is not allowed, so the grammar has to be small enough to hold in the
head and loud enough that nothing it does not understand slips past as a
default. Every test below is therefore about a refusal or about preservation:
what the parser accepts, and what it must never quietly swallow.
The preservation half matters as much as the refusal half. `geografi.base` is
a Norwegian place name; if a round trip through the parser normalises it from
NFC to NFD or escapes it to an ASCII sequence, the file on disk stops being
the file the operator wrote. paths.slug normalises on purpose -- this module
must not.
Style note: this file follows tests/test_paths.py.
"""
import pytest
from jobbsok_lib import frontmatter
ROUND_TRIP = """---
oppdatert: 2026-09-15
geografi:
base: Tromsø
maks_reisetid_min: 45
absolutte_nei:
- Ærøy-prosjektet
- nattskift på Målselv
---
Kjerne: rådgiver i Ålesund.
"""
def test_nested_mapping_and_scalar_types():
meta, body = frontmatter.parse(
"---\n"
"oppdatert: 2026-09-15\n"
"geografi:\n"
" base: Bergen\n"
" maks_reisetid_min: 45\n"
" vurderer_flytting: nei\n"
"---\n"
"brodtekst\n"
)
assert meta["oppdatert"] == "2026-09-15"
assert meta["geografi"] == {
"base": "Bergen",
"maks_reisetid_min": 45,
"vurderer_flytting": "nei",
}
# An integer arrives as an int, not as the string that spelled it: the
# scoring path compares it numerically.
assert isinstance(meta["geografi"]["maks_reisetid_min"], int)
assert body == "brodtekst\n"
def test_flat_list_of_quoted_and_unquoted_items():
meta, _ = frontmatter.parse(
"---\n"
"absolutte_nei:\n"
" - salg\n"
' - "turnus, med helg"\n'
" - 3\n"
"---\n"
)
assert meta["absolutte_nei"] == ["salg", "turnus, med helg", 3]
def test_empty_document_has_no_frontmatter_and_is_not_an_error():
# Absent frontmatter is not this module's error to raise -- Step 8's
# contract check is where a kandidat.md without frontmatter is rejected.
# Here it is simply an empty mapping and an untouched body.
assert frontmatter.parse("") == ({}, "")
assert frontmatter.parse("bare brodtekst\n") == ({}, "bare brodtekst\n")
def test_hash_is_a_comment_only_at_line_start():
meta, _ = frontmatter.parse(
"---\n"
"# denne linjen er en kommentar\n"
"notat: rolle nr # 3, ikke nr 4\n"
"---\n"
)
assert "#" not in meta
# The hash inside the value is data. Trimming it would silently rewrite
# what the operator wrote, which is the failure mode this test pins.
assert meta["notat"] == "rolle nr # 3, ikke nr 4"
def test_unterminated_fence_raises_with_the_line_number():
with pytest.raises(frontmatter.FrontmatterError) as caught:
frontmatter.parse("---\noppdatert: 2026-09-15\ngeografi:\n base: Bodø\n")
assert caught.value.line == 1
assert "---" in str(caught.value)
def test_tab_indentation_is_refused():
with pytest.raises(frontmatter.FrontmatterError) as caught:
frontmatter.parse("---\ngeografi:\n\tbase: Bodø\n---\n")
assert caught.value.line == 3
assert "tab" in str(caught.value).lower()
def test_duplicate_key_is_refused_rather_than_last_wins():
# Last-wins is the dangerous default: a profile with two salary floors
# would silently score against whichever came last.
with pytest.raises(frontmatter.FrontmatterError) as caught:
frontmatter.parse("---\nlonn:\n gulv_nok: 750000\n gulv_nok: 650000\n---\n")
assert caught.value.line == 4
def test_norwegian_characters_survive_a_round_trip_byte_identically():
meta, body = frontmatter.parse(ROUND_TRIP)
assert meta["geografi"]["base"] == "Tromsø"
assert meta["absolutte_nei"][1] == "nattskift på Målselv"
rendered = frontmatter.render(meta, body)
assert rendered == ROUND_TRIP
assert rendered.encode("utf-8") == ROUND_TRIP.encode("utf-8")
def test_a_line_outside_the_grammar_names_its_line_number():
with pytest.raises(frontmatter.FrontmatterError) as caught:
frontmatter.parse("---\noppdatert: 2026-09-15\ndette er ikke en nokkel\n---\n")
assert caught.value.line == 3
assert "3" in str(caught.value)