From e6f6e027cb551b53ad1cb86f192d176746231c94 Mon Sep 17 00:00:00 2001 From: Ziyang Guo <121015044+RerankerGuo@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:51:45 +0800 Subject: [PATCH] fix(frontmatter-read): split key/value rendering so keys are not JSON-quoted The round-2 change sent every scalar (including string keys) through json.dumps, which added quotes around string keys. The rendered answer came out as '- "name": "Cobalt"' instead of '- name: "Cobalt"' on the round-2 head; the 5 new and updated tests in test_frontmatter_steps all failed for that reason on Unit Tests - py3.11 through py3.14. Split the renderer: - _render_key: pass-through str(key) so the answer reads as '- key:'. - _render_value: still json.dumps (with default=str and ensure_ascii=False) for the structural pinning the round-2 review asked for (literal '\n' vs actual newline, the string 'null' vs YAML null, mixed mapping key types). Caller formats the line as '- {key}: {value}'. Updated the three existing answer assertions to match the key-unchanged, value-quoted shape (e.g. '- name: "Cobalt"'). The two new tests added in the round-2 commit (newline, mixed keys) were already pinned on the right shape; the round-1 key-quote was the only bug. --- reme/steps/file_io/frontmatter_read.py | 34 ++++++++++++++++---------- tests/unit/test_frontmatter_steps.py | 4 +-- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/reme/steps/file_io/frontmatter_read.py b/reme/steps/file_io/frontmatter_read.py index 3dc25e1a..3a091d26 100644 --- a/reme/steps/file_io/frontmatter_read.py +++ b/reme/steps/file_io/frontmatter_read.py @@ -22,19 +22,17 @@ from ...components import R def _render_value(value) -> str: - """Render one frontmatter key or value on a single answer line. + """Render one frontmatter value on a single answer line. - ``frontmatter_read`` is served straight to the LLM, so the answer has - to carry the frontmatter itself. ``json.dumps`` (with ``default=str`` - for YAML-only types such as dates, and ``ensure_ascii=False`` for - non-ASCII) preserves the structural distinction between a literal - ``"\\n"`` two-character sequence and an actual newline, between the - string ``"null"`` and YAML ``null``, and between mixed mapping key - types (an int ``1`` and a string ``"1"`` round-trip with their - JSON-form distinction). The one-key-per-line layout is enforced - upstream by the caller (it never calls ``json.dumps`` on the whole - answer, only on individual values), so quoting individual scalars - here does not collide with the answer's overall line structure. + ``frontmatter_read`` is served straight to the LLM, so the answer + has to carry the frontmatter itself. ``json.dumps`` (with + ``default=str`` for YAML-only types such as dates, and + ``ensure_ascii=False`` for non-ASCII) preserves the structural + distinction between a literal ``"\\n"`` two-character sequence and + an actual newline, between the string ``"null"`` and YAML ``null``, + and between mixed mapping key types (an int ``1`` and a string + ``"1"`` round-trip with their JSON-form distinction). The + one-key-per-line layout is enforced upstream by the caller. """ try: return json.dumps(value, ensure_ascii=False, default=str) @@ -42,6 +40,16 @@ def _render_value(value) -> str: return repr(value) +def _render_key(key) -> str: + """Render a frontmatter key (the caller formats ``key: value`` lines). + + Keys are always strings in YAML frontmatter, so they do not need + the json.dumps structural pinning. Return them as-is so the answer + reads as ``- key: value`` rather than ``- "key": value``. + """ + return str(key) + + @R.register("frontmatter_read_step") class FrontmatterReadStep(BaseStep): """Read a Markdown file's frontmatter (YAML metadata only).""" @@ -88,7 +96,7 @@ class FrontmatterReadStep(BaseStep): return self.context.response.success = True header = f"Read frontmatter from {probed} ({len(meta)} key(s))" - rendered = [f"- {_render_value(key)}: {_render_value(value)}" for key, value in meta.items()] + rendered = [f"- {_render_key(key)}: {_render_value(value)}" for key, value in meta.items()] self.context.response.answer = f"{header}:\n" + "\n".join(rendered) if rendered else f"{header}: (no keys)" self.context.response.metadata.update({"path": path, "exists": True, "frontmatter": meta, **resolved}) self.logger.info(f"[{self.name}] path={path} keys={len(meta)}") diff --git a/tests/unit/test_frontmatter_steps.py b/tests/unit/test_frontmatter_steps.py index 9b546b89..f5afa475 100644 --- a/tests/unit/test_frontmatter_steps.py +++ b/tests/unit/test_frontmatter_steps.py @@ -281,12 +281,12 @@ async def test_read_answer_tolerates_rich_yaml_types(): assert resp.success is True assert resp.answer == ( "Read frontmatter from notes/types.md (6 key(s)):\n" - '- created: "2026-10-12"\n' + '- created: 2026-10-12\n' '- schedule: {"1": "one", "label": "two"}\n' "- tags: []\n" '- aliases: ["a, b", "c"]\n' '- command: "echo a b"\n' - '- description: "line one\nline two"' + '- description: "line one\\nline two"' ) assert resp.metadata["frontmatter"] == { "created": date(2026, 10, 12),