Skip to content

Commit b2a4775

Browse files
fix(compiler): preserve frontmatter when closing "---" has no trailing newline (#121)
* fix(compiler): preserve frontmatter when closing "---" has no trailing newline `frontmatter.split` returns the frontmatter block ending in a bare "\n---" (not "\n---\n") for a page that ends right at the closing delimiter with no trailing newline — e.g. a frontmatter-only concept/entity page. Both `_prepend_source_to_frontmatter` and `_remove_source_from_frontmatter` located the closing delimiter with `fm_block.rpartition("\n---\n")`, which finds nothing in that case: `fm_prefix` becomes "" and every existing frontmatter line (the opening "---", `type`, `description`, and the prior `sources:` list) is dropped — silently corrupting the page and losing source provenance. Strip whichever closing form is actually present ("\n---\n" or a bare "\n---") and re-append the same one. Pages that have a body (the common case) are byte-for-byte unaffected. This is reached in practice via `_add_related_link` (run for every related concept/entity during compile) and the `openkb remove` flow (`remove_doc_from_{concept,entity}_pages`). Adds `TestFrontmatterSourceMutation` covering prepend + remove on a no-trailing-newline page, plus a with-body regression guard. * test: fix docstring referencing _prepend (now _prepend_source_to_frontmatter)
1 parent 3fea8cf commit b2a4775

2 files changed

Lines changed: 46 additions & 7 deletions

File tree

openkb/agent/compiler.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -951,12 +951,15 @@ def _prepend_source_to_frontmatter(text: str, source_file: str) -> str:
951951
return text
952952

953953
fm_block, body = parts
954-
# Split the fm_block into lines for per-line manipulation. fm_block ends
955-
# with "\n---\n"; strip the trailing closing delimiter + newline to get
956-
# the prefix lines (opening "---" + content lines), then re-append after.
957-
fm_prefix, _, _ = fm_block.rpartition("\n---\n")
954+
# Strip the trailing closing delimiter to get the prefix lines (opening
955+
# "---" + content lines), then re-append it. `frontmatter.split` leaves the
956+
# closing at the end of fm_block as either "\n---\n" or a bare "\n---" (when
957+
# the page ends at the delimiter with no trailing newline). Assuming only
958+
# "\n---\n" would, for the bare form, make the strip below collapse the
959+
# whole block and drop every existing frontmatter key.
960+
closing = "\n---\n" if fm_block.endswith("\n---\n") else "\n---"
961+
fm_prefix = fm_block[: -len(closing)]
958962
fm_lines = fm_prefix.split("\n")
959-
closing = "\n---\n"
960963

961964
for i, line in enumerate(fm_lines):
962965
if not line.lstrip().startswith("sources:"):
@@ -992,9 +995,12 @@ def _remove_source_from_frontmatter(text: str, source_file: str) -> tuple[str, b
992995
return text, False
993996

994997
fm_block, body = parts
995-
fm_prefix, _, _ = fm_block.rpartition("\n---\n")
998+
# See _prepend_source_to_frontmatter: the closing delimiter may be "\n---\n"
999+
# or a bare "\n---" (no trailing newline); strip whichever is present so the
1000+
# existing frontmatter lines (and the sources: line we need) are preserved.
1001+
closing = "\n---\n" if fm_block.endswith("\n---\n") else "\n---"
1002+
fm_prefix = fm_block[: -len(closing)]
9961003
fm_lines = fm_prefix.split("\n")
997-
closing = "\n---\n"
9981004

9991005
for i, line in enumerate(fm_lines):
10001006
if not line.lstrip().startswith("sources:"):

tests/test_compiler.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,44 @@
2727
_parse_entities_plan,
2828
_filter_entity_items,
2929
_ENTITY_TYPE_LIST,
30+
_prepend_source_to_frontmatter,
31+
_remove_source_from_frontmatter,
3032
remove_doc_from_entity_pages,
3133
)
3234
from openkb.config import resolve_entity_types
3335

3436

37+
class TestFrontmatterSourceMutation:
38+
"""``_prepend_source_to_frontmatter``/``_remove_source_from_frontmatter`` must preserve existing
39+
frontmatter even when the page ends at the closing ``---`` with no trailing
40+
newline — ``frontmatter.split`` then returns a block ending in a bare
41+
``\\n---`` rather than ``\\n---\\n``.
42+
"""
43+
44+
def test_prepend_preserves_keys_without_trailing_newline(self):
45+
text = '---\nsources: ["summaries/p1.md"]\ntype: "Concept"\ndescription: "Focus"\n---'
46+
out = _prepend_source_to_frontmatter(text, "summaries/p2.md")
47+
assert out.startswith("---\n") # opening delimiter kept
48+
assert 'type: "Concept"' in out # other keys kept
49+
assert 'description: "Focus"' in out
50+
assert "summaries/p1.md" in out # existing source kept
51+
assert "summaries/p2.md" in out # new source prepended
52+
53+
def test_remove_preserves_keys_without_trailing_newline(self):
54+
text = '---\ntype: "Organization"\nsources: ["summaries/doc.md"]\n---'
55+
out, now_empty = _remove_source_from_frontmatter(text, "summaries/doc.md")
56+
assert now_empty is True # it was the only source
57+
assert 'type: "Organization"' in out # other key preserved
58+
assert "summaries/doc.md" not in out # source removed
59+
60+
def test_prepend_with_body_is_unchanged(self):
61+
text = '---\nsources: ["a.md"]\ntype: "Concept"\n---\n\nBody.\n'
62+
out = _prepend_source_to_frontmatter(text, "b.md")
63+
assert out.startswith("---\n")
64+
assert "b.md" in out and "a.md" in out
65+
assert out.endswith("\n\nBody.\n") # body + closing untouched
66+
67+
3568
class TestParseJson:
3669
def test_plain_json(self):
3770
assert _parse_json('[{"name": "foo"}]') == [{"name": "foo"}]

0 commit comments

Comments
 (0)