feat(skill-learning): add draft preservation checks
This commit is contained in:
@ -9,6 +9,7 @@ from .missing_skill import (
|
|||||||
MissingSkillSynthesizer,
|
MissingSkillSynthesizer,
|
||||||
)
|
)
|
||||||
from .pipeline import SkillLearningPipelineService
|
from .pipeline import SkillLearningPipelineService
|
||||||
|
from .preservation import check_preservation
|
||||||
from .service import RunReceiptContext, SkillLearningService
|
from .service import RunReceiptContext, SkillLearningService
|
||||||
from .synthesizer import SkillDraftSynthesizer
|
from .synthesizer import SkillDraftSynthesizer
|
||||||
from .worker import SkillLearningWorker, SkillLearningWorkerConfig, SkillLearningWorkerResult
|
from .worker import SkillLearningWorker, SkillLearningWorkerConfig, SkillLearningWorkerResult
|
||||||
@ -23,6 +24,7 @@ __all__ = [
|
|||||||
"MissingSkillSynthesizer",
|
"MissingSkillSynthesizer",
|
||||||
"RunReceiptContext",
|
"RunReceiptContext",
|
||||||
"SkillLearningPipelineService",
|
"SkillLearningPipelineService",
|
||||||
|
"check_preservation",
|
||||||
"SkillDraftSynthesizer",
|
"SkillDraftSynthesizer",
|
||||||
"SkillLearningService",
|
"SkillLearningService",
|
||||||
"SkillLearningWorker",
|
"SkillLearningWorker",
|
||||||
|
|||||||
53
app-instance/backend/beaver/skills/learning/preservation.py
Normal file
53
app-instance/backend/beaver/skills/learning/preservation.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"""Preservation checks for skill revision drafts."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def check_preservation(*, base_content: str, draft_content: str) -> dict[str, Any]:
|
||||||
|
base_sections = _sections(base_content)
|
||||||
|
draft_sections = _sections(draft_content)
|
||||||
|
preserved: list[str] = []
|
||||||
|
changed: list[str] = []
|
||||||
|
dropped: list[str] = []
|
||||||
|
|
||||||
|
for heading, body in base_sections.items():
|
||||||
|
draft_body = draft_sections.get(heading)
|
||||||
|
if draft_body is None:
|
||||||
|
dropped.append(heading)
|
||||||
|
continue
|
||||||
|
preserved.append(heading)
|
||||||
|
if _normalize(body) != _normalize(draft_body):
|
||||||
|
changed.append(heading)
|
||||||
|
|
||||||
|
risk_level = "high" if dropped else "low"
|
||||||
|
return {
|
||||||
|
"passed": not dropped,
|
||||||
|
"risk_level": risk_level,
|
||||||
|
"preserved_sections": preserved,
|
||||||
|
"changed_sections": changed,
|
||||||
|
"dropped_sections": dropped,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _sections(content: str) -> dict[str, str]:
|
||||||
|
current = "body"
|
||||||
|
sections: dict[str, list[str]] = {current: []}
|
||||||
|
for line in (content or "").splitlines():
|
||||||
|
match = re.match(r"^#{1,6}\s+(.+?)\s*$", line)
|
||||||
|
if match:
|
||||||
|
current = match.group(1).strip()
|
||||||
|
sections.setdefault(current, [])
|
||||||
|
continue
|
||||||
|
sections.setdefault(current, []).append(line)
|
||||||
|
return {
|
||||||
|
heading: "\n".join(lines).strip()
|
||||||
|
for heading, lines in sections.items()
|
||||||
|
if "\n".join(lines).strip()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize(value: str) -> str:
|
||||||
|
return re.sub(r"\s+", " ", value or "").strip().lower()
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from beaver.skills.learning.preservation import check_preservation
|
||||||
|
|
||||||
|
|
||||||
|
def test_preservation_passes_when_base_sections_remain() -> None:
|
||||||
|
base = "# Skill\n\n## Workflow\n\n- Read first.\n\n## Safety\n\n- Do not delete files.\n"
|
||||||
|
draft = "# Skill\n\n## Workflow\n\n- Read first.\n- Then write.\n\n## Safety\n\n- Do not delete files.\n"
|
||||||
|
|
||||||
|
report = check_preservation(base_content=base, draft_content=draft)
|
||||||
|
|
||||||
|
assert report["passed"] is True
|
||||||
|
assert report["risk_level"] == "low"
|
||||||
|
assert "Workflow" in report["preserved_sections"]
|
||||||
|
assert "Safety" in report["preserved_sections"]
|
||||||
|
assert report["dropped_sections"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_preservation_flags_dropped_section() -> None:
|
||||||
|
base = "# Skill\n\n## Workflow\n\n- Read first.\n\n## Safety\n\n- Do not delete files.\n"
|
||||||
|
draft = "# Skill\n\n## Workflow\n\n- Read first.\n"
|
||||||
|
|
||||||
|
report = check_preservation(base_content=base, draft_content=draft)
|
||||||
|
|
||||||
|
assert report["passed"] is False
|
||||||
|
assert report["risk_level"] == "high"
|
||||||
|
assert "Safety" in report["dropped_sections"]
|
||||||
Reference in New Issue
Block a user