tenseleyflow/loader / a8a473a

Browse files

Focus HTML quality repairs

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
a8a473a939dff3e4f7cf0b6ed2ba9da6d70a3cb5
Parents
9d7e370
Tree
1b0d02f

2 changed files

StatusFile+-
M src/loader/runtime/finalization.py 98 0
M tests/test_finalization.py 49 0
src/loader/runtime/finalization.pymodified
@@ -2,6 +2,7 @@
22
 
33
 from __future__ import annotations
44
 
5
+import re
56
 from collections.abc import Awaitable, Callable
67
 from dataclasses import dataclass, field
78
 from datetime import UTC, datetime
@@ -1148,6 +1149,7 @@ def _build_verification_repair_guidance(
11481149
     project_root: Path,
11491150
 ) -> str:
11501151
     repair_targets = _extract_verification_repair_targets(dod.evidence)
1152
+    quality_targets = _extract_html_quality_repair_targets(dod.evidence)
11511153
     missing_planned_outputs = _extract_verification_missing_planned_outputs(
11521154
         dod,
11531155
         project_root=project_root,
@@ -1204,6 +1206,8 @@ def _build_verification_repair_guidance(
12041206
             ]
12051207
         )
12061208
         return "\n".join(lines)
1209
+    if quality_targets and not repair_targets:
1210
+        return _build_html_quality_repair_guidance(quality_targets)
12071211
     if not fixes and not repair_targets:
12081212
         return (
12091213
             "Use the failed verification evidence directly, avoid rereading unrelated "
@@ -1292,12 +1296,40 @@ class VerificationRepairTarget:
12921296
     expected_path: str
12931297
 
12941298
 
1299
+@dataclass(frozen=True)
1300
+class VerificationQualityRepairTarget:
1301
+    """Structured content-quality repair target extracted from verification."""
1302
+
1303
+    artifact_path: str
1304
+    issue: str
1305
+
1306
+
1307
+def _build_html_quality_repair_guidance(
1308
+    targets: list[VerificationQualityRepairTarget],
1309
+) -> str:
1310
+    lines = ["Repair focus:"]
1311
+    for target in targets[:4]:
1312
+        lines.append(f"- Improve `{target.artifact_path}`: {target.issue}.")
1313
+    primary = targets[0]
1314
+    lines.extend(
1315
+        [
1316
+            f"- Immediate next step: edit `{primary.artifact_path}`.",
1317
+            "- Update the listed generated artifacts directly; do not recreate the "
1318
+            "artifact set or reread unrelated reference materials.",
1319
+            "- After each content edit, continue with the next listed quality target "
1320
+            "or finish so Loader can re-run verification.",
1321
+        ]
1322
+    )
1323
+    return "\n".join(lines)
1324
+
1325
+
12951326
 def _build_verification_failure_recovery_nudge(
12961327
     dod: DefinitionOfDone,
12971328
     *,
12981329
     project_root: Path,
12991330
 ) -> str | None:
13001331
     repair_targets = _extract_verification_repair_targets(dod.evidence)
1332
+    quality_targets = _extract_html_quality_repair_targets(dod.evidence)
13011333
     missing_planned_outputs = _extract_verification_missing_planned_outputs(
13021334
         dod,
13031335
         project_root=project_root,
@@ -1353,6 +1385,21 @@ def _build_verification_failure_recovery_nudge(
13531385
             f"{expected_action[2:] if expected_action.startswith('- ') else expected_action}"
13541386
             f"{source_hint}"
13551387
         )
1388
+    if quality_targets:
1389
+        primary_target = quality_targets[0]
1390
+        remaining = len(quality_targets) - 1
1391
+        remaining_hint = (
1392
+            f" Then continue with the other {remaining} quality target(s)."
1393
+            if remaining
1394
+            else ""
1395
+        )
1396
+        return (
1397
+            "Verification now identifies generated artifact content quality issues. "
1398
+            "Do not restart discovery or keep auditing unrelated files. "
1399
+            "Your next response should be one concrete `edit` or `write`-style tool "
1400
+            f"call that expands `{primary_target.artifact_path}` to address: "
1401
+            f"{primary_target.issue}.{remaining_hint}"
1402
+        )
13561403
 
13571404
     fixes = _extract_verification_repairs(dod.evidence, repair_targets=repair_targets)
13581405
     if not fixes:
@@ -1527,6 +1574,24 @@ def _extract_verification_repair_targets(
15271574
     return targets
15281575
 
15291576
 
1577
+def _extract_html_quality_repair_targets(
1578
+    evidence_items: list[VerificationEvidence],
1579
+) -> list[VerificationQualityRepairTarget]:
1580
+    targets: list[VerificationQualityRepairTarget] = []
1581
+    seen: set[str] = set()
1582
+    for evidence in evidence_items:
1583
+        for candidate in (evidence.stderr, evidence.output, evidence.stdout):
1584
+            for problem in _extract_html_quality_issues(str(candidate)):
1585
+                parsed = _parse_html_quality_issue(problem)
1586
+                if parsed is None:
1587
+                    continue
1588
+                if parsed.artifact_path in seen:
1589
+                    continue
1590
+                seen.add(parsed.artifact_path)
1591
+                targets.append(parsed)
1592
+    return targets
1593
+
1594
+
15301595
 def _extract_verification_missing_planned_outputs(
15311596
     dod: DefinitionOfDone,
15321597
     *,
@@ -1616,6 +1681,39 @@ def _extract_missing_local_html_links(text: str) -> list[str]:
16161681
     return problems
16171682
 
16181683
 
1684
+def _parse_html_quality_issue(problem: str) -> VerificationQualityRepairTarget | None:
1685
+    match = re.match(r"(?P<path>.+?\.html?):\s*(?P<issue>.+)", problem)
1686
+    if not match:
1687
+        return None
1688
+    artifact_path = match.group("path").strip()
1689
+    issue = match.group("issue").strip()
1690
+    if not artifact_path or not issue:
1691
+        return None
1692
+    return VerificationQualityRepairTarget(artifact_path=artifact_path, issue=issue)
1693
+
1694
+
1695
+def _extract_html_quality_issues(text: str) -> list[str]:
1696
+    if "HTML guide content quality issues:" not in text:
1697
+        return []
1698
+
1699
+    problems: list[str] = []
1700
+    capture = False
1701
+    for raw_line in text.splitlines():
1702
+        line = raw_line.strip()
1703
+        if not line:
1704
+            continue
1705
+        if line == "HTML guide content quality issues:":
1706
+            capture = True
1707
+            continue
1708
+        if not capture:
1709
+            continue
1710
+        if ".html:" not in line and ".htm:" not in line:
1711
+            continue
1712
+        if line not in problems:
1713
+            problems.append(line)
1714
+    return problems
1715
+
1716
+
16191717
 def _classify_verification_kind(command: str) -> str:
16201718
     """Classify the verification command into a summary kind."""
16211719
 
tests/test_finalization.pymodified
@@ -402,6 +402,55 @@ def test_verification_repair_guidance_does_not_create_out_of_scope_link_target(
402402
     assert str(index_path.resolve(strict=False)) in repair.allowed_paths
403403
 
404404
 
405
+def test_verification_repair_guidance_replaces_stale_focus_for_html_quality_issue(
406
+    temp_dir: Path,
407
+) -> None:
408
+    stale_index = temp_dir / "guides" / "nginx" / "index.html"
409
+    stale_index.parent.mkdir(parents=True)
410
+    stale_index.write_text("<h1>Index</h1>\n")
411
+    first_chapter = temp_dir / "guides" / "nginx" / "chapters" / "01-introduction.html"
412
+    third_chapter = temp_dir / "guides" / "nginx" / "chapters" / "03-configuration.html"
413
+    first_chapter.parent.mkdir(parents=True)
414
+    first_chapter.write_text("<h1>Intro</h1>\n")
415
+    third_chapter.write_text("<h1>Config</h1>\n")
416
+    stale_message = Message(
417
+        role=Role.USER,
418
+        content=(
419
+            "Repair focus:\n"
420
+            f"- Fix the broken local reference `../index.html` in `{stale_index}`.\n"
421
+            f"- Immediate next step: edit `{stale_index}`.\n"
422
+        ),
423
+    )
424
+    dod = create_definition_of_done("Create an equally thorough HTML guide.")
425
+    dod.evidence = [
426
+        VerificationEvidence(
427
+            command="quality",
428
+            passed=False,
429
+            output=(
430
+                "HTML guide content quality issues:\n"
431
+                f"{first_chapter}: insufficient structured content (13 blocks, expected at least 18)\n"
432
+                f"{third_chapter}: thin content (1505 text chars, expected at least 1758)\n"
433
+            ),
434
+        )
435
+    ]
436
+
437
+    guidance = _build_verification_repair_guidance(
438
+        dod,
439
+        project_root=temp_dir,
440
+    )
441
+    repair = extract_active_repair_context(
442
+        [stale_message, Message(role=Role.USER, content=guidance)]
443
+    )
444
+
445
+    assert guidance.startswith("Repair focus:")
446
+    assert f"Immediate next step: edit `{first_chapter}`." in guidance
447
+    assert "HTML guide content quality issues" not in guidance
448
+    assert repair is not None
449
+    assert repair.artifact_path == str(first_chapter.resolve(strict=False))
450
+    assert str(stale_index.resolve(strict=False)) not in repair.allowed_paths
451
+    assert str(third_chapter.resolve(strict=False)) in repair.allowed_paths
452
+
453
+
405454
 @pytest.mark.asyncio
406455
 async def test_turn_finalizer_records_skipped_verification_observation(
407456
     temp_dir: Path,