Preserve concrete chapter targets
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
c9cf95709717dcd91532d9442f08ecbffd64d479- Parents
-
7302f96 - Tree
cc44051
c9cf957
c9cf95709717dcd91532d9442f08ecbffd64d4797302f96
cc44051| Status | File | + | - |
|---|---|---|---|
| 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: | ||
| 328 | 328 | dod, |
| 329 | 329 | missing_artifact=preferred_missing_artifact, |
| 330 | 330 | ) |
| 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: | |
| 332 | 336 | progress_lines.append(f"Next pending item: {next_pending}") |
| 333 | 337 | todo_refresh = self._todo_refresh_retry_line(dod) |
| 334 | 338 | if todo_refresh: |
@@ -828,6 +832,11 @@ class ResponseRepairer: | ||
| 828 | 832 | lines.append( |
| 829 | 833 | f"Use the existing outline label `{outline_label}` for that file so it matches the current guide structure." |
| 830 | 834 | ) |
| 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 | + ) | |
| 831 | 840 | if not has_confirmed_output_file_progress and not inferred_is_directory: |
| 832 | 841 | lines.append( |
| 833 | 842 | "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 | ||
| 13 | 13 | from .dod import ( |
| 14 | 14 | all_planned_artifacts_exist, |
| 15 | 15 | collect_planned_artifact_targets, |
| 16 | + infer_next_output_file, | |
| 16 | 17 | planned_artifact_target_satisfied, |
| 17 | 18 | slugify, |
| 18 | 19 | ) |
@@ -992,6 +993,34 @@ def infer_pending_todo_output_target( | ||
| 992 | 993 | ): |
| 993 | 994 | return directory |
| 994 | 995 | |
| 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 | + | |
| 995 | 1024 | if not target_label: |
| 996 | 1025 | return None |
| 997 | 1026 | |
tests/test_repair.pymodified@@ -1080,7 +1080,8 @@ def test_empty_response_retry_uses_concrete_file_language_for_aggregate_chapter_ | ||
| 1080 | 1080 | assert decision.retry_message is not None |
| 1081 | 1081 | assert "Next missing planned artifact: `01-introduction.html`" in decision.retry_message |
| 1082 | 1082 | 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`." | |
| 1084 | 1085 | in decision.retry_message |
| 1085 | 1086 | ) |
| 1086 | 1087 | assert ( |
@@ -1092,10 +1093,84 @@ def test_empty_response_retry_uses_concrete_file_language_for_aggregate_chapter_ | ||
| 1092 | 1093 | in decision.retry_message |
| 1093 | 1094 | ) |
| 1094 | 1095 | 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 | |
| 1095 | 1164 | 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 | |
| 1098 | 1172 | ) |
| 1173 | + assert f"`{display_runtime_path(chapter_two)}`" in decision.retry_message | |
| 1099 | 1174 | |
| 1100 | 1175 | |
| 1101 | 1176 | 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( | ||
| 1019 | 1019 | assert target == chapters.resolve(strict=False) |
| 1020 | 1020 | |
| 1021 | 1021 | |
| 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 | + | |
| 1022 | 1071 | def test_preferred_pending_todo_item_keeps_setup_step_when_missing_file_parent_absent( |
| 1023 | 1072 | tmp_path: Path, |
| 1024 | 1073 | ) -> None: |