Repository navigation
refactor: one frontmatter splitter, every reader uses it - #744
Merged
Merged
Conversation
markramm
force-pushed
the
refactor/one-frontmatter-splitter
branch
from
October 3, 2026 17:02
1ee7ee5 to
6ca99ef
Compare
markramm
force-pushed
the
refactor/one-frontmatter-splitter
branch
2 times, most recently
from
October 4, 2026 00:28
462f9f7 to
5c85219
Compare
split_frontmatter in pyrite/utils/frontmatter.py is the one rule for where a
file's frontmatter starts and ends, and returns the span so a writer can edit
in place (the B6 write path). Its reference is Hugo and YAML, not the old
loader: the expected column of tests/test_frontmatter_splitter.py is Hugo's
recorded output (re-run live when hugo is installed) and PyYAML/ruamel's safe
loader's, and every place Pyrite departs is a stated row that refuses visibly.
A file the convention refuses is refused by every reader: a first line that
starts with --- and is not a valid opener, or a column-0 ---x / ---- before
the closer, is Malformed; TOML and JSON frontmatter are Unsupported. None is
plain text, so no reader stores its YAML as body. Opening and closing lines
are symmetric apart from a tab (refused on the opener, as YAML parsers do).
Leading blank lines are ignored as Hugo ignores them (a stated limit against
r1030).
A markdown file is one entry: the importer never splits a body into entries
by default. Multi-entry streams are opt-in (pyrite import --stream, the REST
stream parameter, import_markdown(stream=True)) with their limits stated; the
stream rule had truncated 9 of 931 valid entries in kb/ (fenced frontmatter
examples), which a test over kb/ now guards.
Two old splitters disagreed (a BOM and blanks after the closing --- made the
repository load a typed entry as an event) and eleven readers used
find("---", 3), which closes on a --- inside a quoted value. An AST scan fails
on a new fence of most shapes outside the module (its misses are documented).
pyrite index build on kb/ and the fixture KBs gives the same rows as before.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
markramm
force-pushed
the
refactor/one-frontmatter-splitter
branch
from
October 5, 2026 20:49
5c85219 to
c4a0de5
Compare
markramm
marked this pull request as ready for review
October 6, 2026 08:02
This was referenced Oct 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
One function,
split_frontmatterinpyrite/utils/frontmatter.py, now decides where a file's frontmatter starts and ends (design.md, principle 8). Round 1 (cold read, Andon #745) rebased the rule on Hugo and YAML instead of the old loader; see "Oracle" below. Closes the backlog itemone-frontmatter-splitter-...(retro 2026-10-03 quality theme).Shape for B6 P1 (
pyrite/storage/operations.py)split_frontmatter(text)returnsFrontmatter | NoFrontmatter | Unterminated.Frontmattercarriestext(the YAML),body(stripped), and offsets into the text as given (BOM and CRLF included):text[open_start:yaml_start]the opening---line with its line end (open_startis 0, or 1 with a BOM)text[yaml_start:close_start]the YAML (== .text; empty frontmatter hasyaml_start == close_start)text[close_start:close_end]the closing line:---, trailing blanks, line endtext[close_end:]the body, unstrippedload_frontmatter(text) -> (meta, body) | Noneis the old_frontmatter_of.next_delimiter(text, pos)finds a delimiter line for stream readers.Oracle (round 1)
ORACLEintests/test_frontmatter_splitter.pyhas 32 rows: Hugo 0.151.2's recorded behaviour (re-run live whenhugois installed), YAML's, and ours, classedagrees,fixedordeparts. Departures refuse visibly with a typed result:----,---x,---+ NBSP as the closerMalformed(line number), file refused+++, JSON{Unsupported, file refused with the format named--- # cas the closer---+ tab on the opening line---,--- # copeningEvery reader runs on every row (
test_every_reader_follows_the_rule_or_refuses);list_templatesskips a bad template with a warning; the importer no longer invents an entry from a body rule;create --body-fileexits non-zero on frontmatter it cannot close.Round 2
---that is not a valid opener (----,---x,---+ tab or NBSP) isMalformed, never plain text;createjoined the reader matrix. The opener takes spaces only (PyYAML and ruamel raise on---+ tab).---followed directly bytitle:,type:orid:starts the next entry, which must close and parse or the file is refused; any other---is a body rule. An entry's body no longer repeats the entries after it.stream=Falseopts out.import_cmd's docstring now says a markdown file is refused whole.Unsupported(refused, message names the format). I did not edit the ADR.web/src/routes/changes/+page.svelte:72-73,scripts/*appointee*.py.Round 3 (maintainer: one entry per file)
import_markdownnever splits a body by default (the stream rule had truncated 9 of 931 validkb/entries on fenced frontmatter examples). Streams are opt-in:pyrite import --stream, RESTstream=true,import_markdown(stream=True), with the limits in each help text. A test imports everykb/file with valid frontmatter and expects one entry with the file's body. Rebased onto B6 P1:file_operations.pyand its test now callload_frontmatter/split_frontmatterinstead of the removed_frontmatter_of.Riskiest assumption
The groom (the item) assumed the two splitters differ only in edge cases. Ran both over the cases first: they differ on a BOM and on blanks after the closing
---. The repository loader rejected both and fell back toEventEntry.load, so a typed entry loaded as anevent(testtest_repository_keeps_the_type_the_loader_keeps). Kept the loader's answer (from_markdown, whichread_entry_idandids pinalready follow). The item also undercounted readers: eleven, not two.Every splitter (grep of
pyrite/andextensions/)Entry.from_markdown(models/base.py)test_split_table,test_repository_and_from_markdown_agree_frontmatter_of(core_types)load_frontmatterKBRepository._load_entrytest_repository_reads_every_row_as_the_splitter_does,..._keeps_the_type_the_loader_keepsid_pin_service(own_OPENING/_CLOSING, private import)test_id_pin_inserts_before_a_closing_line_with_trailing_blanksand the existing pin testsmarkdown_importertest_markdown_importer_reads_a_file_with_no_body_and_no_final_newline,..._still_reads_a_stream_of_entriesexport_commands(2 copies, now one helper)test_export_does_not_build_an_entry_from_prose_between_two_rulesschema_commands._parse_frontmattertest_schema_check_reads_quoted_dasheskb_service.add_entry_from_file,_extract_frontmattertest_add_entry_from_file_reads_quoted_dashes,test_git_change_summary_reads_quoted_dashestemplate_service._parse_template_filetest_template_file_reads_quoted_dashesentry_commands create --body-file/--stdintest_create_body_file_frontmatter_with_dashes_in_a_value(tests/test_cli_commands.py)hooks._load_aliases_for_actor,migration(3)test_cascade_alias_hook_...,test_cascade_inject_ids_...,test_cascade_migration_keeps_a_bom_and_crlfknown_entities._extract_aliasestest_journalism_known_entities_reads_quoted_dashespyrite/orextensions/*/srctest_no_other_module_splits_frontmatter_with_its_own_patternto_markdown, quartz, export_service, ...)---, never search for itscripts/{cross_reference,conflict_analysis,scrape}_appointee*.pypyrite/; not shippedParity
pyrite index buildonkb/(927 entries),tests/fixtures/roundtrip(10) and the tutorial fixture (4), withorigin/dev's code and with this branch: identical rows (941 of 941). A fourth KB of four hand-written files (plain, CRLF, BOM, trailing blanks after the closing---) differs in exactly the two files expected: BOM and trailing-blanks go fromeventtonote.Changelog fragment added: files with a BOM or blanks after the closing
---now read differently.Not touched:
pyrite/storage/operations.py(B6 P1).🤖 Generated with Claude Code
Round 3 (scoped, maintainer-approved; pushed c4a0de5): one entry per markdown file
import_markdownreads one file as one entry with its whole body. Streams are opt-in throughpyrite import --stream, RESTstream=trueorimport_markdown(stream=True), are refused for non-markdown formats, and their limits are stated.kb/file with valid frontmatter imports as exactly one entry whose body equals the split body (more than 500 files). The cold read's boundary inputs are tests.file_operations.pynow usessplit_frontmatter/load_frontmatter, since P1 had imported the private_frontmatter_ofthis branch removes.Conductor review, round 3 (2026-10-06)
The delta cold read on c4a0de5 found nothing that breaks. All 931
kb/files import as one whole entry each (ADR-0042 keeps its full 68k body).--streamis off by default and refused for non-markdown formats on both the CLI and REST.apply()output on the P1 probe set (p10-p30, an 3765-case sweep) matches dev byte for byte. With the default flipped, 12 targeted tests fail, so the tests guard it. CI on this head: test (3.12), verify-red, kb and gate all pass.Known limits, fail-safe, tracked in #756: blank lines are allowed before the
--streamentry key, which the docs do not say; the test oracle shares the splitter; SHAPES does not cover the newly accepted delimiter shapes; one error message is too generic;.mdis not auto-detected.