mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-30 01:46:01 +00:00
Normalize YAML frontmatter keys in the shared parser so validation and review report malformed fields instead of failing while sorting mixed key types.
72 lines
1.8 KiB
Python
72 lines
1.8 KiB
Python
"""Shared SKILL.md frontmatter parsing helpers.
|
|
|
|
The runtime parser, install-time validator, and review core all use this module
|
|
as the schema source for DeerFlow SKILL.md metadata.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
import yaml
|
|
|
|
ALLOWED_FRONTMATTER_PROPERTIES = {
|
|
"name",
|
|
"description",
|
|
"license",
|
|
"allowed-tools",
|
|
"required-secrets",
|
|
"secrets-autonomous",
|
|
"metadata",
|
|
"compatibility",
|
|
"version",
|
|
"author",
|
|
}
|
|
|
|
_FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n?", re.DOTALL)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SkillMarkdownParts:
|
|
"""Parsed pieces of a SKILL.md document."""
|
|
|
|
metadata: dict[str, Any]
|
|
frontmatter_text: str
|
|
body: str
|
|
|
|
|
|
def split_skill_markdown(content: str) -> tuple[SkillMarkdownParts | None, str | None]:
|
|
"""Split a SKILL.md document into frontmatter and body.
|
|
|
|
Returns ``(parts, None)`` on success and ``(None, message)`` on failure. The
|
|
message intentionally avoids host paths so callers can reuse it in
|
|
deterministic review output.
|
|
"""
|
|
match = _FRONTMATTER_RE.match(content)
|
|
if not match:
|
|
return None, "No YAML frontmatter found"
|
|
|
|
frontmatter_text = match.group(1)
|
|
try:
|
|
metadata = yaml.safe_load(frontmatter_text)
|
|
except yaml.YAMLError as exc:
|
|
return None, f"Invalid YAML in frontmatter: {exc}"
|
|
|
|
if not isinstance(metadata, dict):
|
|
return None, "Frontmatter must be a YAML dictionary"
|
|
|
|
# YAML permits non-string keys, but downstream validation expects field
|
|
# names to be strings.
|
|
metadata = {str(key): value for key, value in metadata.items()}
|
|
|
|
return (
|
|
SkillMarkdownParts(
|
|
metadata=metadata,
|
|
frontmatter_text=frontmatter_text,
|
|
body=content[match.end() :],
|
|
),
|
|
None,
|
|
)
|