@@ -749,6 +749,13 @@ class PreActionValidator: |
| 749 | 749 | if not html_declared_target_result.valid: |
| 750 | 750 | return html_declared_target_result |
| 751 | 751 | |
| 752 | + html_root_coverage_result = self._validate_html_root_link_coverage( |
| 753 | + str(file_path), |
| 754 | + str(content), |
| 755 | + ) |
| 756 | + if not html_root_coverage_result.valid: |
| 757 | + return html_root_coverage_result |
| 758 | + |
| 752 | 759 | return ValidationResult(valid=True) |
| 753 | 760 | |
| 754 | 761 | def _validate_edit(self, arguments: dict) -> ValidationResult: |
@@ -1148,7 +1155,15 @@ class PreActionValidator: |
| 1148 | 1155 | |
| 1149 | 1156 | def _relative_html_target(self, root: Path, target: Path) -> str | None: |
| 1150 | 1157 | try: |
| 1151 | | - return str(target.relative_to(root)) |
| 1158 | + normalized_root = root.resolve(strict=False) |
| 1159 | + except OSError: |
| 1160 | + normalized_root = root.expanduser() |
| 1161 | + try: |
| 1162 | + normalized_target = target.resolve(strict=False) |
| 1163 | + except OSError: |
| 1164 | + normalized_target = target.expanduser() |
| 1165 | + try: |
| 1166 | + return str(normalized_target.relative_to(normalized_root)) |
| 1152 | 1167 | except ValueError: |
| 1153 | 1168 | return None |
| 1154 | 1169 | |
@@ -1281,3 +1296,51 @@ class PreActionValidator: |
| 1281 | 1296 | ) |
| 1282 | 1297 | |
| 1283 | 1298 | return ValidationResult(valid=True) |
| 1299 | + |
| 1300 | + def _validate_html_root_link_coverage( |
| 1301 | + self, |
| 1302 | + file_path: str, |
| 1303 | + content: str, |
| 1304 | + ) -> ValidationResult: |
| 1305 | + normalized = Path(file_path).expanduser() |
| 1306 | + if normalized.suffix.lower() != ".html" or normalized.name.lower() != "index.html": |
| 1307 | + return ValidationResult(valid=True) |
| 1308 | + if not normalized.exists(): |
| 1309 | + return ValidationResult(valid=True) |
| 1310 | + |
| 1311 | + root = self._resolve_html_artifact_root(normalized) |
| 1312 | + try: |
| 1313 | + existing_text = normalized.read_text() |
| 1314 | + except OSError: |
| 1315 | + return ValidationResult(valid=True) |
| 1316 | + |
| 1317 | + existing_targets = { |
| 1318 | + relative_target |
| 1319 | + for _href, resolved in self._collect_local_html_targets(normalized, existing_text) |
| 1320 | + if (relative_target := self._relative_html_target(root, resolved)) is not None |
| 1321 | + and resolved.exists() |
| 1322 | + } |
| 1323 | + if not existing_targets: |
| 1324 | + return ValidationResult(valid=True) |
| 1325 | + |
| 1326 | + new_targets = { |
| 1327 | + relative_target |
| 1328 | + for _href, resolved in self._collect_local_html_targets(normalized, content) |
| 1329 | + if (relative_target := self._relative_html_target(root, resolved)) is not None |
| 1330 | + } |
| 1331 | + dropped_targets = sorted(existing_targets - new_targets) |
| 1332 | + if not dropped_targets: |
| 1333 | + return ValidationResult(valid=True) |
| 1334 | + |
| 1335 | + preview = ", ".join(dropped_targets[:3]) |
| 1336 | + if len(dropped_targets) > 3: |
| 1337 | + preview += ", ..." |
| 1338 | + return ValidationResult( |
| 1339 | + valid=False, |
| 1340 | + reason="Edited HTML root page drops links to existing local pages", |
| 1341 | + suggestion=( |
| 1342 | + "Keep the existing local page set linked from the root HTML page " |
| 1343 | + f"unless you are intentionally removing those files, for example restore: {preview}" |
| 1344 | + ), |
| 1345 | + severity="error", |
| 1346 | + ) |