@@ -2,6 +2,7 @@ |
| 2 | 2 | |
| 3 | 3 | from __future__ import annotations |
| 4 | 4 | |
| 5 | +import re |
| 5 | 6 | from collections.abc import Awaitable, Callable |
| 6 | 7 | from dataclasses import dataclass, field |
| 7 | 8 | from datetime import UTC, datetime |
@@ -1148,6 +1149,7 @@ def _build_verification_repair_guidance( |
| 1148 | 1149 | project_root: Path, |
| 1149 | 1150 | ) -> str: |
| 1150 | 1151 | repair_targets = _extract_verification_repair_targets(dod.evidence) |
| 1152 | + quality_targets = _extract_html_quality_repair_targets(dod.evidence) |
| 1151 | 1153 | missing_planned_outputs = _extract_verification_missing_planned_outputs( |
| 1152 | 1154 | dod, |
| 1153 | 1155 | project_root=project_root, |
@@ -1204,6 +1206,8 @@ def _build_verification_repair_guidance( |
| 1204 | 1206 | ] |
| 1205 | 1207 | ) |
| 1206 | 1208 | return "\n".join(lines) |
| 1209 | + if quality_targets and not repair_targets: |
| 1210 | + return _build_html_quality_repair_guidance(quality_targets) |
| 1207 | 1211 | if not fixes and not repair_targets: |
| 1208 | 1212 | return ( |
| 1209 | 1213 | "Use the failed verification evidence directly, avoid rereading unrelated " |
@@ -1292,12 +1296,40 @@ class VerificationRepairTarget: |
| 1292 | 1296 | expected_path: str |
| 1293 | 1297 | |
| 1294 | 1298 | |
| 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 | + |
| 1295 | 1326 | def _build_verification_failure_recovery_nudge( |
| 1296 | 1327 | dod: DefinitionOfDone, |
| 1297 | 1328 | *, |
| 1298 | 1329 | project_root: Path, |
| 1299 | 1330 | ) -> str | None: |
| 1300 | 1331 | repair_targets = _extract_verification_repair_targets(dod.evidence) |
| 1332 | + quality_targets = _extract_html_quality_repair_targets(dod.evidence) |
| 1301 | 1333 | missing_planned_outputs = _extract_verification_missing_planned_outputs( |
| 1302 | 1334 | dod, |
| 1303 | 1335 | project_root=project_root, |
@@ -1353,6 +1385,21 @@ def _build_verification_failure_recovery_nudge( |
| 1353 | 1385 | f"{expected_action[2:] if expected_action.startswith('- ') else expected_action}" |
| 1354 | 1386 | f"{source_hint}" |
| 1355 | 1387 | ) |
| 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 | + ) |
| 1356 | 1403 | |
| 1357 | 1404 | fixes = _extract_verification_repairs(dod.evidence, repair_targets=repair_targets) |
| 1358 | 1405 | if not fixes: |
@@ -1527,6 +1574,24 @@ def _extract_verification_repair_targets( |
| 1527 | 1574 | return targets |
| 1528 | 1575 | |
| 1529 | 1576 | |
| 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 | + |
| 1530 | 1595 | def _extract_verification_missing_planned_outputs( |
| 1531 | 1596 | dod: DefinitionOfDone, |
| 1532 | 1597 | *, |
@@ -1616,6 +1681,39 @@ def _extract_missing_local_html_links(text: str) -> list[str]: |
| 1616 | 1681 | return problems |
| 1617 | 1682 | |
| 1618 | 1683 | |
| 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 | + |
| 1619 | 1717 | def _classify_verification_kind(command: str) -> str: |
| 1620 | 1718 | """Classify the verification command into a summary kind.""" |
| 1621 | 1719 | |