Block post-build reference drift
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
b210a18331eb22c802b9e9cbae3401d322d96b79- Parents
-
3666782 - Tree
7de00c7
b210a18
b210a18331eb22c802b9e9cbae3401d322d96b793666782
7de00c7| Status | File | + | - |
|---|---|---|---|
| M |
src/loader/runtime/hooks.py
|
44 | 2 |
| M |
tests/test_permissions.py
|
63 | 0 |
src/loader/runtime/hooks.pymodified@@ -14,7 +14,6 @@ from ..tools.base import Tool, ToolRegistry | ||
| 14 | 14 | from ..tools.base import ToolResult as RegistryToolResult |
| 15 | 15 | from .dod import ( |
| 16 | 16 | DefinitionOfDoneStore, |
| 17 | - all_planned_artifacts_exist, | |
| 18 | 17 | collect_missing_declared_html_output_files, |
| 19 | 18 | collect_planned_artifact_targets, |
| 20 | 19 | planned_artifact_target_satisfied, |
@@ -667,6 +666,41 @@ def _next_missing_repair_target(repair: Any) -> str: | ||
| 667 | 666 | return "" |
| 668 | 667 | |
| 669 | 668 | |
| 669 | +def _planned_artifact_targets_satisfied(dod: Any, *, project_root: Path) -> bool: | |
| 670 | + targets = collect_planned_artifact_targets( | |
| 671 | + dod, | |
| 672 | + project_root=project_root, | |
| 673 | + ) | |
| 674 | + if not targets: | |
| 675 | + return False | |
| 676 | + return all( | |
| 677 | + planned_artifact_target_satisfied( | |
| 678 | + dod, | |
| 679 | + target=target, | |
| 680 | + expect_directory=expect_directory, | |
| 681 | + project_root=project_root, | |
| 682 | + ) | |
| 683 | + for target, expect_directory in targets | |
| 684 | + ) | |
| 685 | + | |
| 686 | + | |
| 687 | +def _planned_artifact_targets_declare_missing_html_outputs( | |
| 688 | + dod: Any, | |
| 689 | + *, | |
| 690 | + project_root: Path, | |
| 691 | +) -> bool: | |
| 692 | + for target, _expect_directory in collect_planned_artifact_targets( | |
| 693 | + dod, | |
| 694 | + project_root=project_root, | |
| 695 | + ): | |
| 696 | + if collect_missing_declared_html_output_files( | |
| 697 | + target=target, | |
| 698 | + project_root=project_root, | |
| 699 | + ): | |
| 700 | + return True | |
| 701 | + return False | |
| 702 | + | |
| 703 | + | |
| 670 | 704 | class ActiveRepairScopeHook(BaseToolHook): |
| 671 | 705 | """Keep fix-mode observations anchored to the active artifact set.""" |
| 672 | 706 | |
@@ -1148,7 +1182,15 @@ class LateReferenceDriftHook(BaseToolHook): | ||
| 1148 | 1182 | ) |
| 1149 | 1183 | if not planned_targets: |
| 1150 | 1184 | return None |
| 1151 | - if not all_planned_artifacts_exist(dod, project_root=self.project_root): | |
| 1185 | + if not _planned_artifact_targets_satisfied( | |
| 1186 | + dod, | |
| 1187 | + project_root=self.project_root, | |
| 1188 | + ): | |
| 1189 | + return None | |
| 1190 | + if _planned_artifact_targets_declare_missing_html_outputs( | |
| 1191 | + dod, | |
| 1192 | + project_root=self.project_root, | |
| 1193 | + ): | |
| 1152 | 1194 | return None |
| 1153 | 1195 | |
| 1154 | 1196 | planned_roots: list[str] = [] |
tests/test_permissions.pymodified@@ -1706,6 +1706,69 @@ async def test_late_reference_drift_hook_blocks_reference_reads_after_artifacts_ | ||
| 1706 | 1706 | assert str(temp_dir / "guide") in result.message |
| 1707 | 1707 | |
| 1708 | 1708 | |
| 1709 | +@pytest.mark.asyncio | |
| 1710 | +async def test_late_reference_drift_hook_blocks_reference_reads_when_outputs_exist_but_need_quality( | |
| 1711 | + temp_dir: Path, | |
| 1712 | +) -> None: | |
| 1713 | + registry = create_default_registry(temp_dir) | |
| 1714 | + policy = build_permission_policy( | |
| 1715 | + active_mode=PermissionMode.WORKSPACE_WRITE, | |
| 1716 | + workspace_root=temp_dir, | |
| 1717 | + tool_requirements=registry.get_tool_requirements(), | |
| 1718 | + ) | |
| 1719 | + dod_store = DefinitionOfDoneStore(temp_dir) | |
| 1720 | + dod = create_definition_of_done("Create an equally thorough multi-page HTML guide.") | |
| 1721 | + dod.status = "in_progress" | |
| 1722 | + dod.pending_items.append("Improve generated guide depth and formatting") | |
| 1723 | + plan_path = temp_dir / "implementation.md" | |
| 1724 | + plan_path.write_text( | |
| 1725 | + "\n".join( | |
| 1726 | + [ | |
| 1727 | + "# Implementation Plan", | |
| 1728 | + "", | |
| 1729 | + "## File Changes", | |
| 1730 | + f"- `{temp_dir / 'guide' / 'index.html'}`", | |
| 1731 | + f"- `{temp_dir / 'guide' / 'chapters'}/`", | |
| 1732 | + f"- `{temp_dir / 'guide' / 'chapters' / '01-getting-started.html'}`", | |
| 1733 | + "", | |
| 1734 | + ] | |
| 1735 | + ) | |
| 1736 | + ) | |
| 1737 | + dod.implementation_plan = str(plan_path) | |
| 1738 | + guide_dir = temp_dir / "guide" / "chapters" | |
| 1739 | + guide_dir.mkdir(parents=True, exist_ok=True) | |
| 1740 | + (temp_dir / "guide" / "index.html").write_text( | |
| 1741 | + '<h1>Guide</h1><a href="chapters/01-getting-started.html">One</a>\n' | |
| 1742 | + ) | |
| 1743 | + (guide_dir / "01-getting-started.html").write_text("<h1>One</h1><p>thin</p>\n") | |
| 1744 | + dod_path = dod_store.save(dod) | |
| 1745 | + session = FakeSession(active_dod_path=str(dod_path), messages=[]) | |
| 1746 | + hook = LateReferenceDriftHook( | |
| 1747 | + dod_store=dod_store, | |
| 1748 | + project_root=temp_dir, | |
| 1749 | + session=session, | |
| 1750 | + ) | |
| 1751 | + | |
| 1752 | + result = await hook.pre_tool_use( | |
| 1753 | + HookContext( | |
| 1754 | + tool_call=ToolCall( | |
| 1755 | + id="read-reference", | |
| 1756 | + name="read", | |
| 1757 | + arguments={"file_path": str(temp_dir / "reference" / "index.html")}, | |
| 1758 | + ), | |
| 1759 | + tool=registry.get("read"), | |
| 1760 | + registry=registry, | |
| 1761 | + permission_policy=policy, | |
| 1762 | + source="native", | |
| 1763 | + ) | |
| 1764 | + ) | |
| 1765 | + | |
| 1766 | + assert result.decision == HookDecision.DENY | |
| 1767 | + assert result.message is not None | |
| 1768 | + assert "completed artifact set scope" in result.message | |
| 1769 | + assert str(temp_dir / "guide") in result.message | |
| 1770 | + | |
| 1771 | + | |
| 1709 | 1772 | @pytest.mark.asyncio |
| 1710 | 1773 | async def test_late_reference_drift_hook_blocks_verification_reference_reads_after_artifacts_exist( |
| 1711 | 1774 | temp_dir: Path, |