Keep child artifacts within plan
- SHA
67be201764a94d11e10a67777ce0e4f41722f8d1- Parents
-
18ae40f - Tree
c699b9d
67be201
67be201764a94d11e10a67777ce0e4f41722f8d118ae40f
c699b9d| Status | File | + | - |
|---|---|---|---|
| M |
src/loader/runtime/artifact_invalidation.py
|
25 | 2 |
| M |
tests/test_artifact_invalidation.py
|
33 | 0 |
src/loader/runtime/artifact_invalidation.pymodified@@ -4,6 +4,7 @@ from __future__ import annotations | ||
| 4 | 4 | |
| 5 | 5 | import re |
| 6 | 6 | from enum import StrEnum |
| 7 | +from pathlib import Path | |
| 7 | 8 | |
| 8 | 9 | from .workflow_policy import ( |
| 9 | 10 | ArtifactEvidence, |
@@ -237,10 +238,13 @@ def _text_covers_path_reference(text: str, path: str) -> bool: | ||
| 237 | 238 | return True |
| 238 | 239 | |
| 239 | 240 | canonical_text = _canonical_path_reference(text) |
| 240 | - return any( | |
| 241 | + if any( | |
| 241 | 242 | canonical_candidate and canonical_candidate in canonical_text |
| 242 | 243 | for canonical_candidate in (_canonical_path_reference(candidate) for candidate in candidates) |
| 243 | - ) | |
| 244 | + ): | |
| 245 | + return True | |
| 246 | + | |
| 247 | + return any(anchor in canonical_text for anchor in _directory_reference_anchors(path)) | |
| 244 | 248 | |
| 245 | 249 | |
| 246 | 250 | def _canonical_path_reference(value: str) -> str: |
@@ -249,6 +253,25 @@ def _canonical_path_reference(value: str) -> str: | ||
| 249 | 253 | return " ".join(normalized.split()) |
| 250 | 254 | |
| 251 | 255 | |
| 256 | +def _directory_reference_anchors(path: str) -> tuple[str, ...]: | |
| 257 | + normalized = str(path).strip() | |
| 258 | + if not normalized: | |
| 259 | + return () | |
| 260 | + | |
| 261 | + candidate = Path(normalized) | |
| 262 | + directory = candidate if not candidate.suffix else candidate.parent | |
| 263 | + parts = [part for part in directory.parts if part not in {"", "/", "~"}] | |
| 264 | + if len(parts) < 2: | |
| 265 | + return () | |
| 266 | + | |
| 267 | + anchors: list[str] = [] | |
| 268 | + for width in range(min(4, len(parts)), 1, -1): | |
| 269 | + anchor = _canonical_path_reference("/".join(parts[-width:])) | |
| 270 | + if anchor and anchor not in anchors: | |
| 271 | + anchors.append(anchor) | |
| 272 | + return tuple(anchors) | |
| 273 | + | |
| 274 | + | |
| 252 | 275 | def _text_covers_requirement(text: str, requirement: str) -> bool: |
| 253 | 276 | normalized_text = text.lower() |
| 254 | 277 | normalized_requirement = requirement.lower() |
tests/test_artifact_invalidation.pymodified@@ -138,3 +138,36 @@ def test_artifact_invalidation_allows_supplemental_repair_files_after_failed_ver | ||
| 138 | 138 | and "styles.css" in item.summary |
| 139 | 139 | for item in freshness.evidence |
| 140 | 140 | ) |
| 141 | + | |
| 142 | + | |
| 143 | +def test_artifact_invalidation_treats_child_files_under_planned_directory_as_in_plan() -> None: | |
| 144 | + assessor = ArtifactInvalidationAssessor() | |
| 145 | + | |
| 146 | + freshness = assessor.assess( | |
| 147 | + task_statement="Build a multi-file nginx guide.", | |
| 148 | + clarify_text=None, | |
| 149 | + implementation_text=( | |
| 150 | + "# Implementation Plan\n" | |
| 151 | + "- Create `~/Loader/guides/nginx/index.html`.\n" | |
| 152 | + "- Create `~/Loader/guides/nginx/chapters/`.\n" | |
| 153 | + ), | |
| 154 | + verification_text=( | |
| 155 | + "# Verification Plan\n" | |
| 156 | + "## Acceptance Criteria\n" | |
| 157 | + "- `~/Loader/guides/nginx/index.html` exists.\n" | |
| 158 | + "- Chapter files exist under `~/Loader/guides/nginx/chapters/`.\n" | |
| 159 | + ), | |
| 160 | + acceptance_criteria=[ | |
| 161 | + "~/Loader/guides/nginx/index.html exists.", | |
| 162 | + "Chapter files exist under ~/Loader/guides/nginx/chapters/.", | |
| 163 | + ], | |
| 164 | + touched_files=[ | |
| 165 | + "/private/tmp/session/Loader/guides/nginx/index.html", | |
| 166 | + "/private/tmp/session/Loader/guides/nginx/chapters/03-configuration.html", | |
| 167 | + ], | |
| 168 | + last_verification_result=None, | |
| 169 | + ) | |
| 170 | + | |
| 171 | + assert freshness.stale_plan is False | |
| 172 | + assert freshness.recovery_strategy == WorkflowRecoveryStrategy.NONE.value | |
| 173 | + assert "touched_files_outside_plan" not in freshness.reason_codes | |