tenseleyflow/loader / c9cf957

Browse files

Preserve concrete chapter targets

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
c9cf95709717dcd91532d9442f08ecbffd64d479
Parents
7302f96
Tree
cc44051

4 changed files

StatusFile+-
M src/loader/runtime/repair.py 10 1
M src/loader/runtime/workflow.py 29 0
M tests/test_repair.py 78 3
M tests/test_workflow.py 49 0
src/loader/runtime/repair.pymodified
@@ -328,7 +328,11 @@ class ResponseRepairer:
328328
                 dod,
329329
                 missing_artifact=preferred_missing_artifact,
330330
             )
331
-            if next_pending:
331
+            resume_already_names_pending = bool(
332
+                next_pending
333
+                and any(f"`{next_pending}`" in line for line in progress_lines)
334
+            )
335
+            if next_pending and not resume_already_names_pending:
332336
                 progress_lines.append(f"Next pending item: {next_pending}")
333337
             todo_refresh = self._todo_refresh_retry_line(dod)
334338
             if todo_refresh:
@@ -828,6 +832,11 @@ class ResponseRepairer:
828832
                 lines.append(
829833
                     f"Use the existing outline label `{outline_label}` for that file so it matches the current guide structure."
830834
                 )
835
+            if todo_describes_aggregate_mutation(next_pending):
836
+                lines.insert(
837
+                    1,
838
+                    f"It is the next concrete output needed to continue `{next_pending}`.",
839
+                )
831840
             if not has_confirmed_output_file_progress and not inferred_is_directory:
832841
                 lines.append(
833842
                     "Do not wait to perfect the entire multi-file output before this write. "
src/loader/runtime/workflow.pymodified
@@ -13,6 +13,7 @@ from .clarify_grounding import ClarifyGrounding
1313
 from .dod import (
1414
     all_planned_artifacts_exist,
1515
     collect_planned_artifact_targets,
16
+    infer_next_output_file,
1617
     planned_artifact_target_satisfied,
1718
     slugify,
1819
 )
@@ -992,6 +993,34 @@ def infer_pending_todo_output_target(
992993
             ):
993994
                 return directory
994995
 
996
+    if todo_describes_aggregate_mutation(item) and not todo_describes_broad_setup_step(item):
997
+        aggregate_directories: list[Path] = []
998
+        seen_directories: set[str] = set()
999
+
1000
+        for directory in planned_directories:
1001
+            normalized = directory.expanduser().resolve(strict=False)
1002
+            key = str(normalized)
1003
+            if key in seen_directories:
1004
+                continue
1005
+            seen_directories.add(key)
1006
+            aggregate_directories.append(normalized)
1007
+
1008
+        for target in planned_files:
1009
+            parent = target.expanduser().resolve(strict=False).parent
1010
+            key = str(parent)
1011
+            if key in seen_directories:
1012
+                continue
1013
+            seen_directories.add(key)
1014
+            aggregate_directories.append(parent)
1015
+
1016
+        for directory in aggregate_directories:
1017
+            next_output_file, _ = infer_next_output_file(
1018
+                target=directory,
1019
+                project_root=root,
1020
+            )
1021
+            if next_output_file is not None and not next_output_file.exists():
1022
+                return next_output_file.expanduser().resolve(strict=False)
1023
+
9951024
     if not target_label:
9961025
         return None
9971026
 
tests/test_repair.pymodified
@@ -1080,7 +1080,8 @@ def test_empty_response_retry_uses_concrete_file_language_for_aggregate_chapter_
10801080
     assert decision.retry_message is not None
10811081
     assert "Next missing planned artifact: `01-introduction.html`" in decision.retry_message
10821082
     assert (
1083
-        "Resume with this exact next step: create `01-introduction.html`."
1083
+        "Resume with this exact next step: continue `Create chapter files with content and structure` "
1084
+        "by creating `01-introduction.html`."
10841085
         in decision.retry_message
10851086
     )
10861087
     assert (
@@ -1092,10 +1093,84 @@ def test_empty_response_retry_uses_concrete_file_language_for_aggregate_chapter_
10921093
         in decision.retry_message
10931094
     )
10941095
     assert "Remaining planned artifacts:" not in decision.retry_message
1096
+    assert "Next pending item:" not in decision.retry_message
1097
+
1098
+
1099
+def test_empty_response_retry_keeps_concrete_second_chapter_for_aggregate_chapter_step(
1100
+    temp_dir: Path,
1101
+) -> None:
1102
+    context = build_context(
1103
+        temp_dir=temp_dir,
1104
+        use_react=False,
1105
+    )
1106
+    repairer = ResponseRepairer(context)
1107
+
1108
+    guide_root = temp_dir / "guides" / "nginx"
1109
+    chapters = guide_root / "chapters"
1110
+    chapters.mkdir(parents=True)
1111
+    index_path = guide_root / "index.html"
1112
+    chapter_one = chapters / "01-introduction.html"
1113
+    chapter_two = chapters / "02-installation.html"
1114
+    index_path.write_text(
1115
+        "\n".join(
1116
+            [
1117
+                "<html>",
1118
+                '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1119
+                '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1120
+                "</html>",
1121
+            ]
1122
+        )
1123
+        + "\n"
1124
+    )
1125
+    chapter_one.write_text("<h1>Introduction</h1>\n")
1126
+
1127
+    implementation_plan = temp_dir / "implementation.md"
1128
+    implementation_plan.write_text(
1129
+        "\n".join(
1130
+            [
1131
+                "# Implementation Plan",
1132
+                "",
1133
+                "## File Changes",
1134
+                f"- `{guide_root}/`",
1135
+                f"- `{chapters}/`",
1136
+                f"- `{index_path}`",
1137
+                "",
1138
+            ]
1139
+        )
1140
+    )
1141
+
1142
+    dod = create_definition_of_done("Create a multi-file nginx guide.")
1143
+    dod.implementation_plan = str(implementation_plan)
1144
+    dod.touched_files.extend([str(index_path), str(chapter_one)])
1145
+    dod.completed_items.extend(
1146
+        [
1147
+            "Develop the main index.html file with proper structure",
1148
+            "Create first chapter file (01-introduction.html)",
1149
+        ]
1150
+    )
1151
+    dod.pending_items.append("Create chapter files following the established pattern")
1152
+
1153
+    decision = repairer.handle_empty_response(
1154
+        task="Create a multi-file nginx guide.",
1155
+        original_task=None,
1156
+        empty_retry_count=1,
1157
+        max_empty_retries=2,
1158
+        dod=dod,
1159
+    )
1160
+
1161
+    assert decision.should_continue is True
1162
+    assert decision.retry_message is not None
1163
+    assert "Next pending item:" not in decision.retry_message
10951164
     assert (
1096
-        "continue `Create chapter files with content and structure` by creating `01-introduction.html`."
1097
-        not in decision.retry_message
1165
+        "Resume with this exact next step: continue `Create chapter files following the established pattern` "
1166
+        "by creating `02-installation.html`."
1167
+        in decision.retry_message
1168
+    )
1169
+    assert (
1170
+        "It is the next concrete output needed to continue `Create chapter files following the established pattern`."
1171
+        in decision.retry_message
10981172
     )
1173
+    assert f"`{display_runtime_path(chapter_two)}`" in decision.retry_message
10991174
 
11001175
 
11011176
 def test_empty_response_retry_prefers_output_index_over_reference_index_with_same_name(
tests/test_workflow.pymodified
@@ -1019,6 +1019,55 @@ def test_infer_pending_todo_output_target_maps_broad_setup_to_planned_directory(
10191019
     assert target == chapters.resolve(strict=False)
10201020
 
10211021
 
1022
+def test_infer_pending_todo_output_target_maps_aggregate_chapter_step_to_next_declared_file(
1023
+    tmp_path: Path,
1024
+) -> None:
1025
+    dod = create_definition_of_done("Create a multi-file nginx guide.")
1026
+    nginx_root = tmp_path / "Loader" / "guides" / "nginx"
1027
+    chapters = nginx_root / "chapters"
1028
+    chapters.mkdir(parents=True)
1029
+    index_path = nginx_root / "index.html"
1030
+    chapter_one = chapters / "01-introduction.html"
1031
+    chapter_two = chapters / "02-installation.html"
1032
+    index_path.write_text(
1033
+        "\n".join(
1034
+            [
1035
+                "<html>",
1036
+                '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1037
+                '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1038
+                "</html>",
1039
+            ]
1040
+        )
1041
+        + "\n"
1042
+    )
1043
+    chapter_one.write_text("<h1>Introduction</h1>\n")
1044
+
1045
+    implementation_plan = tmp_path / "implementation.md"
1046
+    implementation_plan.write_text(
1047
+        "\n".join(
1048
+            [
1049
+                "# Implementation Plan",
1050
+                "",
1051
+                "## File Changes",
1052
+                f"- `{nginx_root}/`",
1053
+                f"- `{chapters}/`",
1054
+                f"- `{index_path}`",
1055
+                "",
1056
+            ]
1057
+        )
1058
+    )
1059
+    dod.implementation_plan = str(implementation_plan)
1060
+    dod.touched_files.extend([str(index_path), str(chapter_one)])
1061
+
1062
+    target = infer_pending_todo_output_target(
1063
+        dod,
1064
+        "Create chapter files following the established pattern",
1065
+        project_root=tmp_path,
1066
+    )
1067
+
1068
+    assert target == chapter_two.resolve(strict=False)
1069
+
1070
+
10221071
 def test_preferred_pending_todo_item_keeps_setup_step_when_missing_file_parent_absent(
10231072
     tmp_path: Path,
10241073
 ) -> None: