tenseleyflow/loader / edc01d7

Browse files

Anchor concrete write retries

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
edc01d795f8921397e2785603c9d214f27c50d8d
Parents
b5700b2
Tree
e6b6f7d

2 changed files

StatusFile+-
M src/loader/runtime/repair.py 100 0
M tests/test_repair.py 156 0
src/loader/runtime/repair.pymodified
@@ -429,6 +429,12 @@ class ResponseRepairer:
429429
             lines.append(
430430
                 f"Use the existing outline label `{outline_label}` for that file so it matches the current guide structure."
431431
             )
432
+        reference_line = self._known_reference_structure_line(
433
+            concrete_target,
434
+            require_first_substantive_output=True,
435
+        )
436
+        if reference_line:
437
+            lines.append(reference_line)
432438
         if _should_encourage_initial_version(
433439
             target=concrete_target,
434440
             has_confirmed_output_file_progress=True,
@@ -892,6 +898,15 @@ class ResponseRepairer:
892898
                 lines.append(
893899
                     f"Use the existing outline label `{outline_label}` for that file so it matches the current guide structure."
894900
                 )
901
+            reference_line = self._known_reference_structure_line(
902
+                concrete_target,
903
+                require_first_substantive_output=(
904
+                    has_confirmed_output_file_progress
905
+                    and not has_confirmed_substantive_output_file_progress
906
+                ),
907
+            )
908
+            if reference_line:
909
+                lines.append(reference_line)
895910
             if _should_encourage_initial_version(
896911
                 target=concrete_target,
897912
                 has_confirmed_output_file_progress=has_confirmed_output_file_progress,
@@ -957,6 +972,15 @@ class ResponseRepairer:
957972
                 lines.append(
958973
                     f"Use the existing outline label `{outline_label}` for that file so it matches the current guide structure."
959974
                 )
975
+            reference_line = self._known_reference_structure_line(
976
+                inferred_pending_target,
977
+                require_first_substantive_output=(
978
+                    has_confirmed_output_file_progress
979
+                    and not has_confirmed_substantive_output_file_progress
980
+                ),
981
+            )
982
+            if reference_line:
983
+                lines.append(reference_line)
960984
             if todo_describes_aggregate_mutation(next_pending):
961985
                 lines.insert(
962986
                     1,
@@ -1057,6 +1081,15 @@ class ResponseRepairer:
10571081
                         lines.append(
10581082
                             f"Use the existing outline label `{outline_label}` for that file so it matches the current guide structure."
10591083
                         )
1084
+                    reference_line = self._known_reference_structure_line(
1085
+                        next_output_file,
1086
+                        require_first_substantive_output=(
1087
+                            has_confirmed_output_file_progress
1088
+                            and not has_confirmed_substantive_output_file_progress
1089
+                        ),
1090
+                    )
1091
+                    if reference_line:
1092
+                        lines.append(reference_line)
10601093
                     if _should_encourage_initial_version(
10611094
                         target=next_output_file,
10621095
                         has_confirmed_output_file_progress=has_confirmed_output_file_progress,
@@ -1338,6 +1371,68 @@ class ResponseRepairer:
13381371
                 return first_line or None
13391372
         return None
13401373
 
1374
+    def _known_reference_structure_line(
1375
+        self,
1376
+        target: Path,
1377
+        *,
1378
+        require_first_substantive_output: bool,
1379
+    ) -> str | None:
1380
+        if not require_first_substantive_output:
1381
+            return None
1382
+        reference = self._best_known_reference_path(target)
1383
+        if reference is None:
1384
+            return None
1385
+        return (
1386
+            f"You already read `{display_runtime_path(reference)}`; reuse its overall "
1387
+            "structure as the starting pattern for this new file, then adapt the content "
1388
+            "to the current target."
1389
+        )
1390
+
1391
+    def _best_known_reference_path(self, target: Path) -> Path | None:
1392
+        normalized_target = target.expanduser().resolve(strict=False)
1393
+        target_tokens = {
1394
+            token
1395
+            for token in re.split(r"[^a-z0-9]+", normalized_target.stem.lower())
1396
+            if token
1397
+        }
1398
+        target_number = _leading_numeric_prefix(normalized_target.stem)
1399
+        messages = list(getattr(self.context.session, "messages", []) or [])
1400
+        candidates: list[tuple[int, str, Path]] = []
1401
+
1402
+        for message in messages:
1403
+            for tool_call in getattr(message, "tool_calls", []) or []:
1404
+                if getattr(tool_call, "name", "") != "read":
1405
+                    continue
1406
+                raw_path = str(tool_call.arguments.get("file_path") or "").strip()
1407
+                if not raw_path:
1408
+                    continue
1409
+                candidate = Path(raw_path).expanduser().resolve(strict=False)
1410
+                if candidate == normalized_target or not candidate.suffix:
1411
+                    continue
1412
+                if candidate.suffix.lower() != normalized_target.suffix.lower():
1413
+                    continue
1414
+                score = 0
1415
+                if candidate.name.lower() == normalized_target.name.lower():
1416
+                    score += 8
1417
+                if candidate.parent.name.lower() == normalized_target.parent.name.lower():
1418
+                    score += 2
1419
+                if target_number and _leading_numeric_prefix(candidate.stem) == target_number:
1420
+                    score += 3
1421
+                candidate_tokens = {
1422
+                    token
1423
+                    for token in re.split(r"[^a-z0-9]+", candidate.stem.lower())
1424
+                    if token
1425
+                }
1426
+                score += min(3, len(target_tokens & candidate_tokens))
1427
+                if score <= 0:
1428
+                    continue
1429
+                candidates.append((score, str(candidate), candidate))
1430
+
1431
+        if not candidates:
1432
+            return None
1433
+        candidates.sort(key=lambda item: (item[0], item[1]), reverse=True)
1434
+        return candidates[0][2]
1435
+
13411436
     @staticmethod
13421437
     def _mutation_tool_scaffold(path: Path, *, tool_name: str) -> str:
13431438
         normalized_path = json.dumps(display_runtime_path(path))
@@ -1383,3 +1478,8 @@ def _should_encourage_initial_version(
13831478
     if _is_summary_artifact_path(target):
13841479
         return False
13851480
     return not has_confirmed_substantive_output_file_progress
1481
+
1482
+
1483
+def _leading_numeric_prefix(stem: str) -> str:
1484
+    match = re.match(r"^(\d+)", stem)
1485
+    return match.group(1) if match else ""
tests/test_repair.pymodified
@@ -1177,6 +1177,162 @@ def test_empty_response_retry_keeps_concrete_second_chapter_for_aggregate_chapte
11771177
     assert "Follow the same full-payload one-file-at-a-time write pattern" in decision.retry_message
11781178
 
11791179
 
1180
+def test_empty_response_retry_reuses_known_reference_structure_for_first_substantive_file(
1181
+    temp_dir: Path,
1182
+) -> None:
1183
+    context = build_context(
1184
+        temp_dir=temp_dir,
1185
+        use_react=False,
1186
+    )
1187
+    repairer = ResponseRepairer(context)
1188
+
1189
+    guide_root = temp_dir / "guides" / "nginx"
1190
+    chapters = guide_root / "chapters"
1191
+    chapters.mkdir(parents=True)
1192
+    index_path = guide_root / "index.html"
1193
+    reference_chapter = temp_dir / "guides" / "fortran" / "chapters" / "01-introduction.html"
1194
+    reference_chapter.parent.mkdir(parents=True)
1195
+    reference_chapter.write_text("<h1>Chapter 1: Introduction to Fortran</h1>\n")
1196
+    index_path.write_text(
1197
+        "\n".join(
1198
+            [
1199
+                "<html>",
1200
+                '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1201
+                "</html>",
1202
+            ]
1203
+        )
1204
+        + "\n"
1205
+    )
1206
+    context.session.append(
1207
+        Message(
1208
+            role=Role.ASSISTANT,
1209
+            content="",
1210
+            tool_calls=[
1211
+                ToolCall(
1212
+                    id="call_ref",
1213
+                    name="read",
1214
+                    arguments={"file_path": str(reference_chapter)},
1215
+                )
1216
+            ],
1217
+        )
1218
+    )
1219
+
1220
+    implementation_plan = temp_dir / "implementation.md"
1221
+    implementation_plan.write_text(
1222
+        "\n".join(
1223
+            [
1224
+                "# Implementation Plan",
1225
+                "",
1226
+                "## File Changes",
1227
+                f"- `{guide_root}/`",
1228
+                f"- `{chapters}/`",
1229
+                f"- `{index_path}`",
1230
+                "",
1231
+            ]
1232
+        )
1233
+    )
1234
+
1235
+    dod = create_definition_of_done("Create a multi-file nginx guide.")
1236
+    dod.implementation_plan = str(implementation_plan)
1237
+    dod.touched_files.append(str(index_path))
1238
+    dod.completed_items.append("Develop the main index.html file with proper structure")
1239
+    dod.pending_items.append("Create chapter files following the established pattern")
1240
+
1241
+    decision = repairer.handle_empty_response(
1242
+        task="Create a multi-file nginx guide.",
1243
+        original_task=None,
1244
+        empty_retry_count=1,
1245
+        max_empty_retries=2,
1246
+        dod=dod,
1247
+    )
1248
+
1249
+    assert decision.should_continue is True
1250
+    assert decision.retry_message is not None
1251
+    assert (
1252
+        f"You already read `{display_runtime_path(reference_chapter)}`; reuse its overall structure "
1253
+        "as the starting pattern for this new file, then adapt the content to the current target."
1254
+        in decision.retry_message
1255
+    )
1256
+
1257
+
1258
+def test_compact_first_substantive_retry_reuses_known_reference_structure(
1259
+    temp_dir: Path,
1260
+) -> None:
1261
+    context = build_context(
1262
+        temp_dir=temp_dir,
1263
+        use_react=False,
1264
+    )
1265
+    repairer = ResponseRepairer(context)
1266
+
1267
+    guide_root = temp_dir / "guides" / "nginx"
1268
+    chapters = guide_root / "chapters"
1269
+    chapters.mkdir(parents=True)
1270
+    index_path = guide_root / "index.html"
1271
+    reference_chapter = temp_dir / "guides" / "fortran" / "chapters" / "01-introduction.html"
1272
+    reference_chapter.parent.mkdir(parents=True)
1273
+    reference_chapter.write_text("<h1>Chapter 1: Introduction to Fortran</h1>\n")
1274
+    index_path.write_text(
1275
+        "\n".join(
1276
+            [
1277
+                "<html>",
1278
+                '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1279
+                "</html>",
1280
+            ]
1281
+        )
1282
+        + "\n"
1283
+    )
1284
+    context.session.append(
1285
+        Message(
1286
+            role=Role.ASSISTANT,
1287
+            content="",
1288
+            tool_calls=[
1289
+                ToolCall(
1290
+                    id="call_ref",
1291
+                    name="read",
1292
+                    arguments={"file_path": str(reference_chapter)},
1293
+                )
1294
+            ],
1295
+        )
1296
+    )
1297
+
1298
+    implementation_plan = temp_dir / "implementation.md"
1299
+    implementation_plan.write_text(
1300
+        "\n".join(
1301
+            [
1302
+                "# Implementation Plan",
1303
+                "",
1304
+                "## File Changes",
1305
+                f"- `{guide_root}/`",
1306
+                f"- `{chapters}/`",
1307
+                f"- `{index_path}`",
1308
+                "",
1309
+            ]
1310
+        )
1311
+    )
1312
+
1313
+    dod = create_definition_of_done("Create a multi-file nginx guide.")
1314
+    dod.implementation_plan = str(implementation_plan)
1315
+    dod.touched_files.append(str(index_path))
1316
+    dod.completed_items.append("Develop the main index.html file with proper structure")
1317
+    dod.pending_items.append("Create chapter files following the established pattern")
1318
+
1319
+    decision = repairer.handle_empty_response(
1320
+        task="Create a multi-file nginx guide.",
1321
+        original_task=None,
1322
+        empty_retry_count=3,
1323
+        max_empty_retries=4,
1324
+        dod=dod,
1325
+    )
1326
+
1327
+    assert decision.should_continue is True
1328
+    assert decision.retry_message is not None
1329
+    assert (
1330
+        f"You already read `{display_runtime_path(reference_chapter)}`; reuse its overall structure "
1331
+        "as the starting pattern for this new file, then adapt the content to the current target."
1332
+        in decision.retry_message
1333
+    )
1334
+
1335
+
11801336
 def test_empty_response_retry_prefers_output_index_over_reference_index_with_same_name(
11811337
     temp_dir: Path,
11821338
 ) -> None: