tenseleyflow/loader / b210a18

Browse files

Block post-build reference drift

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
b210a18331eb22c802b9e9cbae3401d322d96b79
Parents
3666782
Tree
7de00c7

2 changed files

StatusFile+-
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
 from ..tools.base import ToolResult as RegistryToolResult
14
 from ..tools.base import ToolResult as RegistryToolResult
15
 from .dod import (
15
 from .dod import (
16
     DefinitionOfDoneStore,
16
     DefinitionOfDoneStore,
17
-    all_planned_artifacts_exist,
18
     collect_missing_declared_html_output_files,
17
     collect_missing_declared_html_output_files,
19
     collect_planned_artifact_targets,
18
     collect_planned_artifact_targets,
20
     planned_artifact_target_satisfied,
19
     planned_artifact_target_satisfied,
@@ -667,6 +666,41 @@ def _next_missing_repair_target(repair: Any) -> str:
667
     return ""
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
 class ActiveRepairScopeHook(BaseToolHook):
704
 class ActiveRepairScopeHook(BaseToolHook):
671
     """Keep fix-mode observations anchored to the active artifact set."""
705
     """Keep fix-mode observations anchored to the active artifact set."""
672
 
706
 
@@ -1148,7 +1182,15 @@ class LateReferenceDriftHook(BaseToolHook):
1148
         )
1182
         )
1149
         if not planned_targets:
1183
         if not planned_targets:
1150
             return None
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
             return None
1194
             return None
1153
 
1195
 
1154
         planned_roots: list[str] = []
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
     assert str(temp_dir / "guide") in result.message
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
 @pytest.mark.asyncio
1772
 @pytest.mark.asyncio
1710
 async def test_late_reference_drift_hook_blocks_verification_reference_reads_after_artifacts_exist(
1773
 async def test_late_reference_drift_hook_blocks_verification_reference_reads_after_artifacts_exist(
1711
     temp_dir: Path,
1774
     temp_dir: Path,