fix: preserve implementation step boundaries
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import tomllib
|
||||
|
||||
|
||||
@@ -86,6 +88,55 @@ LIVE_CONTRACT_FILES = (
|
||||
ROOT / "docs" / "SOLVER_SKILL_DESIGN.md",
|
||||
)
|
||||
|
||||
AGENT_STAGE_GUIDANCE_FILES = (
|
||||
ROOT / "AGENTS.md",
|
||||
ROOT / "docs" / "SOLVER_AGENT_DESIGN.md",
|
||||
)
|
||||
|
||||
EXPECTED_AGENT_STAGE_MAPPING = (
|
||||
("1. 요구조건", "requirement-agent", "requirements.md"),
|
||||
("2. 연구", "research-agent", "research.md"),
|
||||
("3. 정식화", "formulation-agent", "formulation.md"),
|
||||
(
|
||||
"4. 수치 검토 + reference model 계약",
|
||||
"numerical-review-agent",
|
||||
"numerical-review.md, reference-model.md",
|
||||
),
|
||||
("5. I/O 정의", "io-definition-agent", "io.md"),
|
||||
(
|
||||
"6. 구현 계획 + C++ 구현 + build/test + reference comparison",
|
||||
"implementation-planning-agent, implementation-agent",
|
||||
"implementation-plan.md, implementation-report.md, build-test.md, reference-comparison.md",
|
||||
),
|
||||
("7. 물리 검토", "physics-evaluation-agent", "physics-evaluation.md"),
|
||||
("8. 배포 준비", "release-agent", "release.md"),
|
||||
)
|
||||
|
||||
CANONICAL_FEATURE_DIRS = (
|
||||
ROOT / "docs" / "linear-static-3d-euler-beam",
|
||||
ROOT / "docs" / "linear-static-mitc4-shell",
|
||||
)
|
||||
|
||||
# Plans and specs intentionally retain deleted source paths as migration history.
|
||||
HISTORICAL_PATH_EXEMPT_DIRS = (
|
||||
ROOT / "docs" / "superpowers" / "plans",
|
||||
ROOT / "docs" / "superpowers" / "specs",
|
||||
)
|
||||
|
||||
LEGACY_README_PATTERN = re.compile(
|
||||
r"docs/(?:coordination|requirements|research|formulations|numerical-reviews|"
|
||||
r"io-definitions|reference-models|implementation-plans|build-test-reports|"
|
||||
r"corrections|reference-verifications|physics-evaluations|releases)/README\.md",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
EXPECTED_SKILL_FRONTMATTER_KEYS = ("name", "description")
|
||||
EXPECTED_SKILL_INTERFACE_KEYS = (
|
||||
"display_name",
|
||||
"short_description",
|
||||
"default_prompt",
|
||||
)
|
||||
|
||||
RETIRED_TOKENS = (
|
||||
"reference-model-agent",
|
||||
"reference model agent",
|
||||
@@ -122,6 +173,65 @@ def live_contract_text() -> str:
|
||||
return "\n".join(read(path) for path in paths)
|
||||
|
||||
|
||||
def markdown_stage_mapping(path: Path) -> tuple[tuple[str, str, str], ...]:
|
||||
rows = []
|
||||
for line in read(path).splitlines():
|
||||
columns = [column.strip().replace("`", "") for column in line.strip().split("|")[1:-1]]
|
||||
if len(columns) >= 4 and re.fullmatch(r"[1-8]\. .+", columns[0]):
|
||||
rows.append((columns[0], columns[1], columns[3]))
|
||||
return tuple(rows)
|
||||
|
||||
|
||||
def operational_markdown_paths() -> tuple[Path, ...]:
|
||||
paths = [
|
||||
path
|
||||
for feature_dir in CANONICAL_FEATURE_DIRS
|
||||
for path in sorted(feature_dir.glob("*.md"))
|
||||
]
|
||||
paths.extend(sorted(ROOT.glob("phases/**/step*.md")))
|
||||
return tuple(paths)
|
||||
|
||||
|
||||
def parse_skill_frontmatter(path: Path) -> dict[str, str]:
|
||||
lines = read(path).splitlines()
|
||||
assert lines and lines[0] == "---", path
|
||||
assert "---" in lines[1:], path
|
||||
closing_index = lines.index("---", 1)
|
||||
field_lines = lines[1:closing_index]
|
||||
assert len(field_lines) == len(EXPECTED_SKILL_FRONTMATTER_KEYS), path
|
||||
|
||||
parsed = {}
|
||||
for line in field_lines:
|
||||
match = re.fullmatch(r"([a-z_]+): ([^\s].*)", line)
|
||||
assert match is not None, (path, line)
|
||||
key, value = match.groups()
|
||||
assert key not in parsed, (path, key)
|
||||
parsed[key] = value
|
||||
|
||||
assert tuple(parsed) == EXPECTED_SKILL_FRONTMATTER_KEYS, path
|
||||
assert re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", parsed["name"]), path
|
||||
assert parsed["description"].startswith("Use when "), path
|
||||
return parsed
|
||||
|
||||
|
||||
def parse_skill_interface(path: Path) -> dict[str, str]:
|
||||
lines = [line for line in read(path).splitlines() if line]
|
||||
assert lines and lines[0] == "interface:", path
|
||||
assert len(lines) == len(EXPECTED_SKILL_INTERFACE_KEYS) + 1, path
|
||||
|
||||
parsed = {}
|
||||
for line in lines[1:]:
|
||||
match = re.fullmatch(r' ([a-z_]+): ("(?:[^"\\]|\\.)*")', line)
|
||||
assert match is not None, (path, line)
|
||||
key, quoted_value = match.groups()
|
||||
assert key not in parsed, (path, key)
|
||||
parsed[key] = json.loads(quoted_value)
|
||||
|
||||
assert tuple(parsed) == EXPECTED_SKILL_INTERFACE_KEYS, path
|
||||
assert all(parsed.values()), path
|
||||
return parsed
|
||||
|
||||
|
||||
def test_agent_inventory_is_consolidated():
|
||||
actual = {path.stem for path in AGENT_DIR.glob("*.toml")}
|
||||
assert actual == EXPECTED_AGENTS
|
||||
@@ -178,6 +288,20 @@ def test_implementation_absorbs_build_test_and_reference_comparison():
|
||||
assert marker in text
|
||||
|
||||
|
||||
def test_implementation_steps_keep_final_comparison_in_a_final_gate():
|
||||
paths = (
|
||||
AGENT_DIR / "implementation-agent.toml",
|
||||
SKILL_DIR / "fesa-cpp-msvc-tdd" / "SKILL.md",
|
||||
)
|
||||
for path in paths:
|
||||
text = read(path).lower()
|
||||
assert "final implementation-owned verification step/gate" in text, path
|
||||
assert "non-final step" in text, path
|
||||
assert "artifact check -> compare -> classify -> report" in text, path
|
||||
assert "results.h5" in text, path
|
||||
assert "reference-comparison.md" in text, path
|
||||
|
||||
|
||||
def test_agent_hierarchy_is_explicit():
|
||||
coordinator = read(AGENT_DIR / "coordinator-agent.toml").lower()
|
||||
assert "main agent" in coordinator
|
||||
@@ -200,12 +324,24 @@ def test_live_contracts_have_no_retired_names_or_paths():
|
||||
assert token not in text, token
|
||||
|
||||
|
||||
def test_live_guidance_declares_eight_stage_workflow():
|
||||
def test_live_guidance_declares_exact_ordered_eight_stage_workflow():
|
||||
for path in LIVE_CONTRACT_FILES:
|
||||
text = read(path)
|
||||
assert "8단계" in text, path
|
||||
assert "docs/<feature-id>/" in text, path
|
||||
|
||||
for path in AGENT_STAGE_GUIDANCE_FILES:
|
||||
assert markdown_stage_mapping(path) == EXPECTED_AGENT_STAGE_MAPPING, path
|
||||
|
||||
|
||||
def test_operational_documents_do_not_use_deleted_stage_readmes():
|
||||
paths = operational_markdown_paths()
|
||||
assert paths
|
||||
for path in paths:
|
||||
assert not any(path.is_relative_to(directory) for directory in HISTORICAL_PATH_EXEMPT_DIRS)
|
||||
match = LEGACY_README_PATTERN.search(read(path))
|
||||
assert match is None, (path, match.group(0) if match else None)
|
||||
|
||||
|
||||
def test_mitc4_live_authority_links_use_feature_bundle():
|
||||
text = read(ROOT / "docs" / "MITC4_SUPP.md")
|
||||
@@ -232,7 +368,7 @@ def test_mitc4_live_authority_links_use_feature_bundle():
|
||||
assert legacy_path not in authority_section
|
||||
|
||||
|
||||
def test_agent_toml_and_skill_ui_metadata_are_parseable():
|
||||
def test_agent_toml_and_limited_skill_yaml_schemas_are_parseable():
|
||||
for path in sorted(AGENT_DIR.glob("*.toml")):
|
||||
parsed = tomllib.loads(read(path))
|
||||
assert parsed["name"] == path.stem
|
||||
@@ -240,7 +376,7 @@ def test_agent_toml_and_skill_ui_metadata_are_parseable():
|
||||
|
||||
for skill_name in EXPECTED_SKILLS:
|
||||
skill_dir = SKILL_DIR / skill_name
|
||||
skill_text = read(skill_dir / "SKILL.md")
|
||||
ui_text = read(skill_dir / "agents" / "openai.yaml")
|
||||
assert f"name: {skill_name}" in skill_text
|
||||
assert f"${skill_name}" in ui_text
|
||||
frontmatter = parse_skill_frontmatter(skill_dir / "SKILL.md")
|
||||
interface = parse_skill_interface(skill_dir / "agents" / "openai.yaml")
|
||||
assert frontmatter["name"] == skill_name
|
||||
assert f"${skill_name}" in interface["default_prompt"]
|
||||
|
||||
Reference in New Issue
Block a user