@@ -1214,12 +1214,15 @@ def _build_verification_repair_guidance( |
| 1214 | 1214 | lines.extend(f"- {item}" for item in fixes) |
| 1215 | 1215 | primary_target = repair_targets[0] if repair_targets else None |
| 1216 | 1216 | if primary_target is not None: |
| 1217 | + expected_action = _repair_expected_action_line( |
| 1218 | + dod, |
| 1219 | + primary_target, |
| 1220 | + project_root=project_root, |
| 1221 | + ) |
| 1217 | 1222 | lines.extend( |
| 1218 | 1223 | [ |
| 1219 | 1224 | f"- Immediate next step: edit `{primary_target.artifact_path}`.", |
| 1220 | | - "- If the broken reference should remain, create " |
| 1221 | | - f"`{primary_target.expected_path}`; otherwise remove or replace " |
| 1222 | | - f"`{primary_target.failing_reference}`.", |
| 1225 | + expected_action, |
| 1223 | 1226 | *( |
| 1224 | 1227 | [ |
| 1225 | 1228 | "- Use the existing artifact files as the source of truth while " |
@@ -1327,6 +1330,11 @@ def _build_verification_failure_recovery_nudge( |
| 1327 | 1330 | ) |
| 1328 | 1331 | if repair_targets: |
| 1329 | 1332 | primary_target = repair_targets[0] |
| 1333 | + expected_action = _repair_expected_action_line( |
| 1334 | + dod, |
| 1335 | + primary_target, |
| 1336 | + project_root=project_root, |
| 1337 | + ) |
| 1330 | 1338 | source_hint = "" |
| 1331 | 1339 | if repair_source_paths: |
| 1332 | 1340 | preview = ", ".join(f"`{path}`" for path in repair_source_paths[:4]) |
@@ -1342,8 +1350,7 @@ def _build_verification_failure_recovery_nudge( |
| 1342 | 1350 | "Your next response should be one concrete `edit` or `write`-style tool " |
| 1343 | 1351 | f"call that updates `{primary_target.artifact_path}` to repair " |
| 1344 | 1352 | f"`{primary_target.failing_reference}`. " |
| 1345 | | - f"If that reference should stay, create `{primary_target.expected_path}`; " |
| 1346 | | - "otherwise remove or replace the broken local reference." |
| 1353 | + f"{expected_action[2:] if expected_action.startswith('- ') else expected_action}" |
| 1347 | 1354 | f"{source_hint}" |
| 1348 | 1355 | ) |
| 1349 | 1356 | |
@@ -1397,6 +1404,106 @@ def _existing_repair_source_paths( |
| 1397 | 1404 | return paths |
| 1398 | 1405 | |
| 1399 | 1406 | |
| 1407 | +def _repair_expected_action_line( |
| 1408 | + dod: DefinitionOfDone, |
| 1409 | + target: VerificationRepairTarget, |
| 1410 | + *, |
| 1411 | + project_root: Path, |
| 1412 | +) -> str: |
| 1413 | + if _repair_expected_path_is_within_artifact_scope( |
| 1414 | + dod, |
| 1415 | + target, |
| 1416 | + project_root=project_root, |
| 1417 | + ): |
| 1418 | + return ( |
| 1419 | + "- If the broken reference should remain, create " |
| 1420 | + f"`{target.expected_path}`; otherwise remove or replace " |
| 1421 | + f"`{target.failing_reference}`." |
| 1422 | + ) |
| 1423 | + return ( |
| 1424 | + "- The missing target resolves outside the requested artifact scope; " |
| 1425 | + "do not create that outside file just to satisfy the link. " |
| 1426 | + f"Remove or replace `{target.failing_reference}` in " |
| 1427 | + f"`{target.artifact_path}` instead." |
| 1428 | + ) |
| 1429 | + |
| 1430 | + |
| 1431 | +def _repair_expected_path_is_within_artifact_scope( |
| 1432 | + dod: DefinitionOfDone, |
| 1433 | + target: VerificationRepairTarget, |
| 1434 | + *, |
| 1435 | + project_root: Path, |
| 1436 | +) -> bool: |
| 1437 | + try: |
| 1438 | + expected_path = Path(target.expected_path).expanduser().resolve(strict=False) |
| 1439 | + artifact_path = Path(target.artifact_path).expanduser().resolve(strict=False) |
| 1440 | + except (OSError, RuntimeError, ValueError): |
| 1441 | + return False |
| 1442 | + |
| 1443 | + scope_roots = _repair_artifact_scope_roots(dod, project_root=project_root) |
| 1444 | + if not scope_roots: |
| 1445 | + scope_roots = (artifact_path.parent,) |
| 1446 | + |
| 1447 | + for root in scope_roots: |
| 1448 | + if not _path_is_relative_to(artifact_path, root): |
| 1449 | + continue |
| 1450 | + if _path_is_relative_to(expected_path, root): |
| 1451 | + return True |
| 1452 | + if artifact_path.name.lower() != "index.html": |
| 1453 | + nested_artifact_root = artifact_path.parent.parent |
| 1454 | + if _path_is_relative_to(expected_path, nested_artifact_root): |
| 1455 | + return True |
| 1456 | + return False |
| 1457 | + |
| 1458 | + |
| 1459 | +def _repair_artifact_scope_roots( |
| 1460 | + dod: DefinitionOfDone, |
| 1461 | + *, |
| 1462 | + project_root: Path, |
| 1463 | +) -> tuple[Path, ...]: |
| 1464 | + roots: list[Path] = [] |
| 1465 | + seen: set[str] = set() |
| 1466 | + |
| 1467 | + def add_root(path: Path) -> None: |
| 1468 | + try: |
| 1469 | + resolved = path.expanduser().resolve(strict=False) |
| 1470 | + except (OSError, RuntimeError, ValueError): |
| 1471 | + return |
| 1472 | + key = str(resolved) |
| 1473 | + if key in seen: |
| 1474 | + return |
| 1475 | + seen.add(key) |
| 1476 | + roots.append(resolved) |
| 1477 | + |
| 1478 | + for target, expect_directory in collect_planned_artifact_targets( |
| 1479 | + dod, |
| 1480 | + project_root=project_root, |
| 1481 | + max_paths=24, |
| 1482 | + ): |
| 1483 | + if expect_directory: |
| 1484 | + add_root(target) |
| 1485 | + elif target.suffix: |
| 1486 | + add_root(target.parent) |
| 1487 | + |
| 1488 | + for path_str in dod.touched_files: |
| 1489 | + if not str(path_str).strip(): |
| 1490 | + continue |
| 1491 | + path = Path(path_str) |
| 1492 | + effective_path = path if path.is_absolute() else project_root / path |
| 1493 | + if effective_path.suffix: |
| 1494 | + add_root(effective_path.parent) |
| 1495 | + |
| 1496 | + return tuple(roots) |
| 1497 | + |
| 1498 | + |
| 1499 | +def _path_is_relative_to(path: Path, root: Path) -> bool: |
| 1500 | + try: |
| 1501 | + path.relative_to(root) |
| 1502 | + except ValueError: |
| 1503 | + return False |
| 1504 | + return True |
| 1505 | + |
| 1506 | + |
| 1400 | 1507 | def _extract_verification_repair_targets( |
| 1401 | 1508 | evidence_items: list[VerificationEvidence], |
| 1402 | 1509 | ) -> list[VerificationRepairTarget]: |