From 7b5e625066768ff6556c4ae0ac3d2fd7d4959476 Mon Sep 17 00:00:00 2001 From: Timo Derstappen Date: Thu, 11 Jun 2026 10:49:11 +0200 Subject: [PATCH] fix(cache): require whole --- lines as frontmatter delimiters (#1259) _body_content used substring checks (startswith("---") / find("\n---")), so `----` thematic breaks and `--- text` prose lines were mistaken for frontmatter delimiters and everything above them was silently excluded from the hash -- edits there never invalidated the cache. Both delimiters must now be a whole line of exactly three dashes (optional trailing whitespace). For well-formed frontmatter the stripped body stays byte-identical to the previous implementation, so existing semantic-cache hashes do not churn. Co-authored-by: Cursor --- graphify/cache.py | 23 +++++++++++++---- tests/test_cache.py | 60 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/graphify/cache.py b/graphify/cache.py index 407ae467..dd6235f1 100644 --- a/graphify/cache.py +++ b/graphify/cache.py @@ -5,6 +5,7 @@ import atexit import hashlib import json import os +import re import tempfile from pathlib import Path @@ -14,14 +15,26 @@ from pathlib import Path _GRAPHIFY_OUT = os.environ.get("GRAPHIFY_OUT", "graphify-out") +# A frontmatter delimiter is a whole line of exactly three dashes (optional +# trailing whitespace). Substring checks like startswith("---") / +# find("\n---") also match `----` thematic breaks and `--- text` prose, +# silently dropping everything above them from the hash (#1259). +_FRONTMATTER_DELIM = re.compile(r"^---[ \t]*\r?$", re.MULTILINE) + + def _body_content(content: bytes) -> bytes: """Strip YAML frontmatter from Markdown content, returning only the body.""" text = content.decode(errors="replace") - if text.startswith("---"): - end = text.find("\n---", 3) - if end != -1: - return text[end + 4:].encode() - return content + opener = _FRONTMATTER_DELIM.match(text) + if opener is None: + return content + closer = _FRONTMATTER_DELIM.search(text, opener.end()) + if closer is None: + return content + # Slice right after the closing `---` (not after its line) so the output + # stays byte-identical with the historical implementation for well-formed + # frontmatter -- existing semantic-cache hashes must not churn. + return text[closer.start() + 3:].encode() # Stat-based index: maps absolute path → {size, mtime_ns, hash}. diff --git a/tests/test_cache.py b/tests/test_cache.py index 1c4fbf21..04bc46c4 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -128,6 +128,66 @@ def test_body_content_no_frontmatter(): assert _body_content(content) == content +# --- #1259: frontmatter delimiters must be whole `---` lines ----------------- + +def test_body_content_hr_start_is_not_frontmatter(): + """A document opening with a ``----`` thematic break has no frontmatter; + a later ``---`` hr must not be mistaken for a close delimiter.""" + content = b"----\nIntro paragraph that must be hashed.\n\n---\nbody" + assert _body_content(content) == content + + +def test_body_content_dash_title_start_is_not_frontmatter(): + """``--- title`` on the first line is prose, not an open delimiter.""" + content = b"--- title\nIntro that must be hashed.\n\n---\nbody" + assert _body_content(content) == content + + +def test_body_content_dash_text_line_is_not_close_delimiter(): + """``--- text`` and ``----`` lines inside opened frontmatter are not the + close; without a proper close the content passes through unchanged.""" + content = b"---\ntitle: Test\nbody starts here\n--- not a delimiter\n----\nreal content" + assert _body_content(content) == content + + +def test_body_content_later_proper_close_skips_dash_text_lines(): + """A ``--- text`` line is skipped; the next whole ``---`` line closes.""" + content = b"---\ntitle: Test\nnote: --- inline\n---\nreal body" + assert _body_content(content) == b"\nreal body" + + +def test_body_content_well_formed_output_byte_identical(): + """For well-formed frontmatter the stripped body must stay byte-identical + to the historical substring implementation, so existing semantic-cache + hashes do not churn (re-extraction is billed LLM work).""" + cases = [ + # (input, output of the historical text.find("\n---")+4 algorithm) + (b"---\ntitle: Test\n---\n\nActual body.", b"\n\nActual body."), + (b"---\nreviewed: 2026-01-01\n---\n\n# Title\n\nBody text.", b"\n\n# Title\n\nBody text."), + # close delimiter with trailing whitespace keeps it in the body + (b"---\ntitle: Test\n--- \nbody", b" \nbody"), + # CRLF line endings + (b"---\r\ntitle: Test\r\n---\r\nbody", b"\r\nbody"), + # empty frontmatter block + (b"---\n---\nbody", b"\nbody"), + # close as the very last line, no trailing newline + (b"---\ntitle: Test\n---", b""), + ] + for content, expected in cases: + assert _body_content(content) == expected, content + + +def test_md_edit_above_hr_changes_hash(tmp_path): + """Editing content above a mid-document ``----`` break must change the + hash -- previously that region was silently excluded from hashing.""" + f = tmp_path / "doc.md" + f.write_text("----\nIntro paragraph.\n\n---\nbody") + h1 = file_hash(f) + f.write_text("----\nEdited intro paragraph.\n\n---\nbody") + h2 = file_hash(f) + assert h1 != h2 + + # --- #777: portable cache source_file fields -------------------------------- # ``save_cached`` relativizes ``source_file`` entries inside the cache file # so a committed ``graphify-out/cache/`` is portable across machines and