From 87e7dcdd7372c23b5d9fd2984f8b02100bbb83f8 Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Sat, 18 Jul 2026 18:57:59 -0700 Subject: [PATCH 1/2] fix(skill_engine): read block-scalar values in get_frontmatter_field Line-oriented lookup returned the literal "|" for description: | blocks. Delegate to parse_frontmatter so indented body lines are kept. --- .gitignore | 1 + openspace/skill_engine/skill_utils.py | 15 ++++++--------- .../test_get_frontmatter_block_scalar.py | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 tests/skill_engine/test_get_frontmatter_block_scalar.py diff --git a/.gitignore b/.gitignore index 27e8dfb0..5661c60d 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_get_frontmatter_block_scalar.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 555a2cfe..c808248f 100644 --- a/openspace/skill_engine/skill_utils.py +++ b/openspace/skill_engine/skill_utils.py @@ -113,18 +113,15 @@ def get_frontmatter_field(content: str, field_name: str) -> Optional[str]: """Extract a single field value from YAML frontmatter. Returns ``None`` if the field is absent or content has no frontmatter. + Uses :func:`parse_frontmatter` so block scalars (``|`` / ``>``) round-trip. """ - if not content.startswith("---"): + fm = parse_frontmatter(content) + if field_name not in fm: return None - match = _FRONTMATTER_RE.match(content) - if not match: + value = fm[field_name] + if value is None: return None - for line in match.group(1).split("\n"): - if ":" in line: - key, value = line.split(":", 1) - if key.strip() == field_name: - return _yaml_unquote(value.strip()) - return None + return value if isinstance(value, str) else str(value) def set_frontmatter_field(content: str, field_name: str, value: str) -> str: diff --git a/tests/skill_engine/test_get_frontmatter_block_scalar.py b/tests/skill_engine/test_get_frontmatter_block_scalar.py new file mode 100644 index 00000000..9587fd9f --- /dev/null +++ b/tests/skill_engine/test_get_frontmatter_block_scalar.py @@ -0,0 +1,15 @@ +"""Regression: get_frontmatter_field must read YAML block scalars.""" + +from __future__ import annotations + +from openspace.skill_engine.skill_utils import get_frontmatter_field + + +def test_get_frontmatter_field_reads_block_scalar_description(): + src = "---\nname: x\ndescription: |\n line1\n line2\n---\nbody\n" + assert get_frontmatter_field(src, "description") == "line1\nline2" + + +def test_get_frontmatter_field_missing_returns_none(): + src = "---\nname: x\n---\n" + assert get_frontmatter_field(src, "description") is None From 9d32b4db9ca1caf51a95005db340c49608d79f7b Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Sat, 18 Jul 2026 19:05:13 -0700 Subject: [PATCH 2/2] fix(skill_engine): parse block scalars without requiring PyYAML --- openspace/skill_engine/skill_utils.py | 27 ++++++++++++++----- .../test_get_frontmatter_block_scalar.py | 10 +++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/openspace/skill_engine/skill_utils.py b/openspace/skill_engine/skill_utils.py index c808248f..0e938a5b 100644 --- a/openspace/skill_engine/skill_utils.py +++ b/openspace/skill_engine/skill_utils.py @@ -113,15 +113,30 @@ def get_frontmatter_field(content: str, field_name: str) -> Optional[str]: """Extract a single field value from YAML frontmatter. Returns ``None`` if the field is absent or content has no frontmatter. - Uses :func:`parse_frontmatter` so block scalars (``|`` / ``>``) round-trip. + Block scalars (``|`` / ``>``) include following indented lines. """ - fm = parse_frontmatter(content) - if field_name not in fm: + if not content.startswith("---"): return None - value = fm[field_name] - if value is None: + match = _FRONTMATTER_RE.match(content) + if not match: return None - return value if isinstance(value, str) else str(value) + lines = match.group(1).split("\n") + for i, line in enumerate(lines): + if ":" not in line: + continue + key, value = line.split(":", 1) + if key.strip() != field_name: + continue + rest = value.strip() + if re.fullmatch(r"[|>][+-]?", rest): + collected: List[str] = [] + j = i + 1 + while j < len(lines) and lines[j].startswith((" ", "\t")): + collected.append(re.sub(r"^[ \t]+", "", lines[j], count=1)) + j += 1 + return "\n".join(collected) + return _yaml_unquote(rest) + return None def set_frontmatter_field(content: str, field_name: str, value: str) -> str: diff --git a/tests/skill_engine/test_get_frontmatter_block_scalar.py b/tests/skill_engine/test_get_frontmatter_block_scalar.py index 9587fd9f..9aca76cb 100644 --- a/tests/skill_engine/test_get_frontmatter_block_scalar.py +++ b/tests/skill_engine/test_get_frontmatter_block_scalar.py @@ -10,6 +10,16 @@ def test_get_frontmatter_field_reads_block_scalar_description(): assert get_frontmatter_field(src, "description") == "line1\nline2" +def test_get_frontmatter_field_reads_folded_scalar(): + src = "---\nname: x\ndescription: >\n line1\n line2\n---\n" + assert get_frontmatter_field(src, "description") == "line1\nline2" + + def test_get_frontmatter_field_missing_returns_none(): src = "---\nname: x\n---\n" assert get_frontmatter_field(src, "description") is None + + +def test_get_frontmatter_field_keeps_plain_scalar_text(): + src = "---\nenabled: true\n---\n" + assert get_frontmatter_field(src, "enabled") == "true"