|
| 1 | +"""Anti-drift: thinking lenses, engineering prompts, and workflow skills cross-link.""" |
| 2 | + |
| 3 | +from pathlib import Path |
| 4 | + |
| 5 | +import pytest |
| 6 | + |
| 7 | +LIBRARY_ROOT = Path(__file__).parent.parent.parent |
| 8 | +THINKING_DIR = LIBRARY_ROOT / "01-thinking" |
| 9 | +ENGINEERING_DIR = LIBRARY_ROOT / "02-engineering" |
| 10 | +SKILLS_DIR = LIBRARY_ROOT / "skills" |
| 11 | + |
| 12 | +ENGINEERING_THINKING_LINKS: dict[str, tuple[str, ...]] = { |
| 13 | + "task-start.md": ( |
| 14 | + "01-thinking/why-what-how-done.md", |
| 15 | + "01-thinking/falsifiability.md", |
| 16 | + ), |
| 17 | + "spec-writer.md": ("01-thinking/falsifiability.md",), |
| 18 | + "code-reviewer.md": ("01-thinking/critical-thinking-check.md",), |
| 19 | + "implementation-agent.md": ("01-thinking/counterargument.md",), |
| 20 | + "test-designer.md": ("01-thinking/falsifiability.md",), |
| 21 | + "task-slicer.md": ("01-thinking/why-what-how-done.md",), |
| 22 | + "usage-first.md": ("01-thinking/socratic-reviewer.md",), |
| 23 | + "local-feedback.md": ("01-thinking/critical-thinking-check.md",), |
| 24 | +} |
| 25 | + |
| 26 | +SKILL_THINKING_SOURCES: dict[str, tuple[str, ...]] = { |
| 27 | + "reflective-implement": ( |
| 28 | + "01-thinking/counterargument.md", |
| 29 | + "01-thinking/critical-thinking-check.md", |
| 30 | + ), |
| 31 | + "reflective-spec-plan": ( |
| 32 | + "01-thinking/falsifiability.md", |
| 33 | + "01-thinking/why-what-how-done.md", |
| 34 | + ), |
| 35 | + "reflective-handoff-retro": ("01-thinking/socratic-reviewer.md",), |
| 36 | +} |
| 37 | + |
| 38 | +THINKING_PROMPTS = tuple(sorted(THINKING_DIR.glob("*.md"))) |
| 39 | +ENGINEERING_PROMPTS = tuple(sorted(ENGINEERING_DIR.glob("*.md"))) |
| 40 | + |
| 41 | + |
| 42 | +def _preamble(path: Path) -> str: |
| 43 | + return path.read_text(encoding="utf-8").split("```", 1)[0] |
| 44 | + |
| 45 | + |
| 46 | +def _prompt_sources_section(skill_path: Path) -> str: |
| 47 | + text = skill_path.read_text(encoding="utf-8") |
| 48 | + marker = "## Prompt Sources" |
| 49 | + assert marker in text, f"{skill_path.parent.name} missing {marker}" |
| 50 | + return text.split(marker, 1)[1].split("##", 1)[0] |
| 51 | + |
| 52 | + |
| 53 | +@pytest.mark.parametrize("prompt_name,thinking_refs", ENGINEERING_THINKING_LINKS.items()) |
| 54 | +def test_engineering_prompt_links_thinking_lens(prompt_name: str, thinking_refs: tuple[str, ...]): |
| 55 | + path = ENGINEERING_DIR / prompt_name |
| 56 | + preamble = _preamble(path) |
| 57 | + for ref in thinking_refs: |
| 58 | + assert ref in preamble, f"{prompt_name} preamble should reference {ref}" |
| 59 | + |
| 60 | + |
| 61 | +def test_all_engineering_prompts_have_thinking_cross_link(): |
| 62 | + assert set(ENGINEERING_THINKING_LINKS) == {p.name for p in ENGINEERING_PROMPTS} |
| 63 | + |
| 64 | + |
| 65 | +@pytest.mark.parametrize("skill_name,thinking_refs", SKILL_THINKING_SOURCES.items()) |
| 66 | +def test_workflow_skill_lists_thinking_sources(skill_name: str, thinking_refs: tuple[str, ...]): |
| 67 | + skill_path = SKILLS_DIR / skill_name / "SKILL.md" |
| 68 | + section = _prompt_sources_section(skill_path) |
| 69 | + for ref in thinking_refs: |
| 70 | + assert ref in section, f"{skill_name} Prompt Sources should list {ref}" |
| 71 | + |
| 72 | + |
| 73 | +@pytest.mark.parametrize("prompt_path", THINKING_PROMPTS, ids=lambda p: p.name) |
| 74 | +def test_thinking_prompt_maps_to_workflow_skill(prompt_path: Path): |
| 75 | + preamble = _preamble(prompt_path) |
| 76 | + assert "reflective-" in preamble, f"{prompt_path.name} should map to a workflow skill" |
| 77 | + |
| 78 | + |
| 79 | +def test_thinking_lens_files_exist_for_engineering_links(): |
| 80 | + linked = {ref for refs in ENGINEERING_THINKING_LINKS.values() for ref in refs} |
| 81 | + for ref in linked: |
| 82 | + assert (LIBRARY_ROOT / ref).is_file(), f"missing thinking lens file {ref}" |
0 commit comments