tenseleyflow/loader / 33957e2

Browse files

Tighten directory plan anchors

Authored by espadonne
SHA
33957e202caf5b6f9b5552b0ceaa648e625a02ad
Parents
e5057a7
Tree
cc87bf0

2 changed files

StatusFile+-
M src/loader/runtime/artifact_invalidation.py 24 2
M tests/test_artifact_invalidation.py 25 0
src/loader/runtime/artifact_invalidation.pymodified
@@ -244,7 +244,12 @@ def _text_covers_path_reference(text: str, path: str) -> bool:
244244
     ):
245245
         return True
246246
 
247
-    return any(anchor in canonical_text for anchor in _directory_reference_anchors(path))
247
+    directory_mentions = _extract_directory_mentions(text)
248
+    directory_suffixes = _directory_reference_suffixes(path)
249
+    return any(
250
+        mention and mention in directory_suffixes
251
+        for mention in (_canonical_path_reference(item.rstrip("/")) for item in directory_mentions)
252
+    )
248253
 
249254
 
250255
 def _canonical_path_reference(value: str) -> str:
@@ -253,7 +258,7 @@ def _canonical_path_reference(value: str) -> str:
253258
     return " ".join(normalized.split())
254259
 
255260
 
256
-def _directory_reference_anchors(path: str) -> tuple[str, ...]:
261
+def _directory_reference_suffixes(path: str) -> tuple[str, ...]:
257262
     normalized = str(path).strip()
258263
     if not normalized:
259264
         return ()
@@ -305,6 +310,23 @@ def _extract_path_mentions(*texts: str | None) -> list[str]:
305310
     return mentions
306311
 
307312
 
313
+def _extract_directory_mentions(*texts: str | None) -> list[str]:
314
+    mentions: list[str] = []
315
+    seen: set[str] = set()
316
+    for text in texts:
317
+        if not text:
318
+            continue
319
+        for match in re.finditer(r"(?:~|/)?[\w./-]+/", text):
320
+            if match.end() < len(text) and re.match(r"[\w.-]", text[match.end()]):
321
+                continue
322
+            normalized = match.group(0).strip("`'\",.:;()[]{}")
323
+            if not normalized or normalized in seen:
324
+                continue
325
+            seen.add(normalized)
326
+            mentions.append(normalized)
327
+    return mentions
328
+
329
+
308330
 def _short_requirement(requirement: str, *, limit: int = 72) -> str:
309331
     normalized = " ".join(str(requirement).split()).strip()
310332
     if len(normalized) <= limit:
tests/test_artifact_invalidation.pymodified
@@ -171,3 +171,28 @@ def test_artifact_invalidation_treats_child_files_under_planned_directory_as_in_
171171
     assert freshness.stale_plan is False
172172
     assert freshness.recovery_strategy == WorkflowRecoveryStrategy.NONE.value
173173
     assert "touched_files_outside_plan" not in freshness.reason_codes
174
+
175
+
176
+def test_artifact_invalidation_keeps_root_level_sibling_files_out_of_plan() -> None:
177
+    assessor = ArtifactInvalidationAssessor()
178
+
179
+    freshness = assessor.assess(
180
+        task_statement="Implement the runtime report artifact.",
181
+        clarify_text=None,
182
+        implementation_text=(
183
+            "# Implementation Plan\n"
184
+            "- Create `/tmp/session/planned.txt`.\n"
185
+        ),
186
+        verification_text=(
187
+            "# Verification Plan\n"
188
+            "## Acceptance Criteria\n"
189
+            "- `/tmp/session/planned.txt` exists.\n"
190
+        ),
191
+        acceptance_criteria=["/tmp/session/planned.txt exists."],
192
+        touched_files=["/tmp/session/notes.txt"],
193
+        last_verification_result=None,
194
+    )
195
+
196
+    assert freshness.stale_plan is True
197
+    assert freshness.recovery_strategy == WorkflowRecoveryStrategy.PLAN_REFRESH.value
198
+    assert "touched_files_outside_plan" in freshness.reason_codes