From c0009ac030ab74cade03107962c990eea255d0fd Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Sat, 18 Jul 2026 18:24:28 -0700 Subject: [PATCH] fix(skill_engine): preserve multiline frontmatter values set_frontmatter_field wrote raw newlines into YAML, so parse_frontmatter kept only the first line. Quote multiline scalars and drop old block continuation lines when replacing a field. --- .gitignore | 1 + openspace/skill_engine/skill_utils.py | 19 ++++++++++++++++--- .../test_set_frontmatter_multiline.py | 18 ++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 tests/skill_engine/test_set_frontmatter_multiline.py diff --git a/.gitignore b/.gitignore index 27e8dfb..58d1400 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ tests/skill_engine/* !tests/skill_engine/test_evolver_length_recovery.py !tests/skill_engine/test_evolution_retry_idempotency.py !tests/skill_engine/test_analyzer_length_recovery.py +!tests/skill_engine/test_set_frontmatter_multiline.py !tests/skill_engine/decision/ tests/skill_engine/decision/* !tests/skill_engine/decision/test_analysis_adapter.py diff --git a/openspace/skill_engine/skill_utils.py b/openspace/skill_engine/skill_utils.py index 555a2cf..718f4de 100644 --- a/openspace/skill_engine/skill_utils.py +++ b/openspace/skill_engine/skill_utils.py @@ -58,10 +58,16 @@ _YAML_NEEDS_QUOTE_RE = re.compile(r"[:\#\[\]{}&*!|>'\"%@`]") def _yaml_quote(value: str) -> str: """Quote a YAML scalar value if it contains special characters.""" - if not value or not _YAML_NEEDS_QUOTE_RE.search(value): + if not value: return value - escaped = value.replace("\\", "\\\\").replace('"', '\\"') - return f'"{escaped}"' + if "\n" in value or _YAML_NEEDS_QUOTE_RE.search(value): + escaped = ( + value.replace("\\", "\\\\") + .replace('"', '\\"') + .replace("\n", "\\n") + ) + return f'"{escaped}"' + return value def _yaml_unquote(value: str) -> str: @@ -147,10 +153,17 @@ def set_frontmatter_field(content: str, field_name: str, value: str) -> str: new_line = f"{field_name}: {quoted}" found = False new_lines = [] + skip_block = False for line in fm_text.split("\n"): + if skip_block: + if line.startswith((" ", "\t")) or line.strip() == "": + continue + skip_block = False if ":" in line and line.split(":", 1)[0].strip() == field_name: new_lines.append(new_line) found = True + # Drop old block-scalar / folded continuation lines under this key. + skip_block = True else: new_lines.append(line) if not found: diff --git a/tests/skill_engine/test_set_frontmatter_multiline.py b/tests/skill_engine/test_set_frontmatter_multiline.py new file mode 100644 index 0000000..137df77 --- /dev/null +++ b/tests/skill_engine/test_set_frontmatter_multiline.py @@ -0,0 +1,18 @@ +"""Regression: multiline frontmatter values must round-trip via parse_frontmatter.""" + +from __future__ import annotations + +from openspace.skill_engine.skill_utils import parse_frontmatter, set_frontmatter_field + + +def test_set_frontmatter_field_preserves_multiline_description(): + out = set_frontmatter_field("---\nname: x\n---\n", "description", "line1\nline2") + assert parse_frontmatter(out)["description"] == "line1\nline2" + + +def test_set_frontmatter_field_replaces_block_scalar_without_orphan_lines(): + src = "---\nname: x\ndescription: |\n old1\n old2\n---\nbody\n" + out = set_frontmatter_field(src, "description", "new1\nnew2") + assert parse_frontmatter(out)["description"] == "new1\nnew2" + assert "old1" not in out + assert "old2" not in out