diff --git a/bin/validate-md-links b/bin/validate-md-links
index bd5f275d..4139beea 100755
--- a/bin/validate-md-links
+++ b/bin/validate-md-links
@@ -9,6 +9,8 @@ declare(strict_types=1);
* Checks that internal links (relative paths and anchors) in Markdown files
* point to existing files and valid anchors. External URLs, mailto: and
* images are skipped. Links inside fenced code blocks are ignored.
+ * Valid anchor targets are ATX headings (# ...) and explicit HTML anchors
+ * ( / ), both GitHub-compatible.
*
* Usage:
* php bin/validate-md-links.php [options]
@@ -265,6 +267,21 @@ function generateSlug(string $heading): string
return $slug ?? '';
}
+/**
+ * Extract explicit HTML anchor ids from a line: or .
+ * GitHub honours both forms as link targets, so the validator must too.
+ *
+ * @return list
+ */
+function extractHtmlAnchors(string $line): array
+{
+ if (!preg_match_all('/]*?\b(?:id|name)\s*=\s*(["\'])([^"\'>]+)\1/i', $line, $m)) {
+ return [];
+ }
+
+ return array_values(array_filter($m[2]));
+}
+
/**
* Build an anchor index from file content.
* Returns map: [slug => true] (all anchors including deduplicated ones).
@@ -281,6 +298,11 @@ function buildAnchorIndex(string $content): array
$slugCounts = [];
foreach (explode("\n", $content) as $line) {
+ // Explicit HTML anchors: / , used as-is
+ foreach (extractHtmlAnchors($line) as $id) {
+ $anchors[$id] = true;
+ }
+
// Match ATX headings: # ... ######
if (!preg_match('/^#{1,6}\s+(.+)$/', $line, $m)) {
continue;
diff --git a/docs/conventions/ops/validate-md-links.md b/docs/conventions/ops/validate-md-links.md
index 1535ce87..c64d0d78 100644
--- a/docs/conventions/ops/validate-md-links.md
+++ b/docs/conventions/ops/validate-md-links.md
@@ -15,6 +15,7 @@ description: Настройка и использование валидатор
- Внешние URL (`https://`, `mailto:`), изображения и ссылки внутри `fenced code blocks` **игнорируются**.
- Валидатор поддерживает `inline`-ссылки, `reference-style`-ссылки и якоря (`anchors`).
- Генерация `slug` якоря совместима с GitHub: нижний регистр (`lower-case`), русские символы, дубликаты с суффиксами `-1`, `-2`.
+- Целями якорей считаются заголовки ATX (`#`…`######`) и явные HTML-якоря `` / `` — обе формы работают на GitHub и принимаются как есть, без преобразования `slug`.
## Что проверяется
diff --git a/tests/MdLinksValidator/MdLinksValidatorTest.php b/tests/MdLinksValidator/MdLinksValidatorTest.php
index 2b8dc6b9..ddbf76a0 100644
--- a/tests/MdLinksValidator/MdLinksValidatorTest.php
+++ b/tests/MdLinksValidator/MdLinksValidatorTest.php
@@ -118,9 +118,23 @@ public function testValidReferenceLinks(): void
$this->assertStringNotContainsString('second-id', self::$fixtureOutput);
}
+ public function testRecognizesExplicitHtmlAnchors(): void
+ {
+ // Explicit / anchors are GitHub-valid targets
+ $this->assertStringNotContainsString('#explicit-term', self::$fixtureOutput);
+ $this->assertStringNotContainsString('#shared-term', self::$fixtureOutput);
+ $this->assertStringNotContainsString('#legacy-anchor', self::$fixtureOutput);
+ }
+
+ public function testDetectsBrokenExplicitAnchor(): void
+ {
+ $this->assertStringContainsString('root.md', self::$fixtureOutput);
+ $this->assertStringContainsString('anchor not found: #no-such-explicit', self::$fixtureOutput);
+ }
+
public function testReportsCorrectErrorCount(): void
{
- $this->assertStringContainsString('Found 5 broken link(s)', self::$fixtureOutput);
+ $this->assertStringContainsString('Found 6 broken link(s)', self::$fixtureOutput);
}
public function testNoFailOptionExitsZero(): void
diff --git a/tests/fixtures/md-links/root.md b/tests/fixtures/md-links/root.md
index ea805976..4a9413a1 100644
--- a/tests/fixtures/md-links/root.md
+++ b/tests/fixtures/md-links/root.md
@@ -42,3 +42,17 @@ More content.
A link to [duplicate heading](#duplicate-heading-1).
A link to [russian anchor](subdir/target.md#русский-заголовок).
+
+## Explicit HTML anchors
+
+
+**Explicit term** — definition with an explicit anchor.
+
+A [local explicit anchor](#explicit-term) in this file.
+
+A [cross-file explicit anchor](subdir/target.md#shared-term).
+
+
+A [legacy name anchor](#legacy-anchor).
+
+A [broken explicit anchor](#no-such-explicit).
diff --git a/tests/fixtures/md-links/subdir/target.md b/tests/fixtures/md-links/subdir/target.md
index 4377ec05..4f3f76b5 100644
--- a/tests/fixtures/md-links/subdir/target.md
+++ b/tests/fixtures/md-links/subdir/target.md
@@ -1,5 +1,8 @@
# Target File
+
+**Shared term** — target for a cross-file explicit anchor.
+
## Section One
Target content.
diff --git a/todo/done/TASK-fix-md-links-html-anchors.todo.md b/todo/done/TASK-fix-md-links-html-anchors.todo.md
new file mode 100644
index 00000000..39af9f13
--- /dev/null
+++ b/todo/done/TASK-fix-md-links-html-anchors.todo.md
@@ -0,0 +1,92 @@
+---
+type: fix
+created: 2026-09-21 23:25:00 (1790007900)
+due:
+started: 2026-09-21 23:25:30 (1790007930)
+completed: 2026-09-21 16:28:27 (1790008107)
+cancelled:
+value: V2
+complexity: C1
+priority: P2
+cost_plan:
+cost_fact:
+depends_on:
+epic:
+author: Бэкендер Тони (pi)
+assignee: Бэкендер Тони (pi)
+branch: task/md-links-html-anchors
+pr: https://github.com/prikotov/coding-standard/pull/126
+status: done
+---
+
+# TASK-fix-md-links-html-anchors: validate-md-links — распознавать явные HTML-якоря
+
+## 0. Простое описание (Human Brief)
+
+### Проблема простыми словами (Problem)
+- `validate-md-links` строит индекс якорей только из заголовков ATX (`#`…`######`).
+- Пакет `prikotov/git-workflow` использует в глоссарии явные HTML-якоря `` (валидные на GitHub): валидатор ложно помечает ссылки `#deployment`, `glossary.md#release-publishing` как битые.
+
+### Варианты или путь решения (Solution Sketch)
+- Дополнить `buildAnchorIndex()` сбором явных HTML-якорей `` / `` (принимаются как есть, без slug-преобразования — так же, как их разрешает GitHub).
+- Покрыть фикстурами и тестами, обновить документацию инструмента.
+
+### Ожидаемый результат (Expected Result)
+- Ссылки на явные HTML-якоря валидируются корректно; документация `prikotov/git-workflow` проходит проверку без ложных срабатываний.
+
+## 1. Концепция и Цель (Concept and Goal)
+
+### История (User Story)
+> **User Story:** Как потребитель пакета, я хочу, чтобы `validate-md-links` признавал явные HTML-якоря ``/`` целями ссылок — как это делает GitHub, — чтобы документация с глоссариями не падала в проверках из-за ложных `broken-anchor`.
+
+### Цель по SMART (Goal)
+- В `bin/validate-md-links` индекс якорей собирает и заголовки, и явные HTML-якоря; добавлены фикстуры/тесты (валидные и битые явные якоря); обновлена дока `docs/conventions/ops/validate-md-links.md`; `composer check` зелёный.
+
+## 2. Контекст и Границы (Context and Scope)
+* **Где делаем:** `bin/validate-md-links`, `tests/fixtures/md-links/`, `tests/MdLinksValidator/MdLinksValidatorTest.php`, `docs/conventions/ops/validate-md-links.md`.
+* **Границы (Out of Scope):** не менять slug-генерацию заголовков; не поддерживать прочие HTML-конструкции (кроме ``/``); не трогать остальные инструменты пакета.
+
+## 3. Требования, MoSCoW (Requirements)
+### 🔴 Обязательно (Must Have)
+- [x] `buildAnchorIndex()` собирает явные HTML-якоря `` / `` (регистронезависимо, кавычки обоих видов, якорь принимается как есть).
+- [x] Фикстуры: валидные локальный/межфайловый/legacy `name`-якорь и битый явный якорь.
+- [x] Тесты: распознавание валидных, детект битого, счётчик ошибок обновлён.
+- [x] Документация инструмента описывает поддержку HTML-якорей.
+### ⚫ Won't Have (Не будем делать)
+- Поддержку многострочных HTML-якорей и якорей внутри `inline code` (те же ограничения, что и у заголовков).
+
+## 4. План реализации (Implementation Plan)
+1. [x] Добавить `extractHtmlAnchors()` и включить его в `buildAnchorIndex()`.
+2. [x] Расширить фикстуры и тесты.
+3. [x] Обновить документацию.
+4. [x] Прогнать `composer check` и PHPUnit; сверить на документации `prikotov/git-workflow`.
+
+## 5. Критерии приёмки (Definition of Done)
+- [x] `vendor/bin/phpunit tests/MdLinksValidator/` — зелёный (21 тест).
+- [x] `composer check` — зелёный (289 тестов, PHPStan, phpcs, validate-docs, language).
+- [x] `php bin/validate-md-links /path/to/git-workflow/docs/git-workflow/` — все ссылки валидны (исходный кейс закрыт).
+
+## 6. Самопроверка (Verification)
+```bash
+vendor/bin/phpunit
+composer check
+php bin/validate-md-links /docs/git-workflow/
+```
+
+## 7. Риски и зависимости (Risks и Dependencies)
+- Поведение совместимо: индекс только расширяется (новые цели), ложных «битых» меньше, новые ошибки возможны только там, где ссылки вели на несуществующие явные якоря — это корректное срабатывание.
+
+## 8. Источники (Sources)
+- `bin/validate-md-links` — `buildAnchorIndex()`.
+- Документация `prikotov/git-workflow` (`docs/git-workflow/glossary.md`) — исходный кейс с ``.
+
+## 9. Комментарии (Comments)
+- Обнаружено при обновлении `prikotov/git-workflow` до v0.4.0 в проекте `prikotov/task-orchestrator` (PR #404).
+
+## История изменений (Change History)
+| Дата | Автор (роль) | Изменение |
+| :--- | :--- | :--- |
+| 2026-09-21 23:25:00 (1790007900) | Бэкендер Тони (pi) | Создание задачи |
+| 2026-09-21 23:26:00 (1790007960) | Бэкендер Тони (pi) | Старт задачи, заполнение постановки |
+| 2026-09-21 23:30:00 (1790008200) | Бэкендер Тони (pi) | Реализация, фикстуры, тесты, документация; проверки зелёные |
+| 2026-09-21 23:40:00 (1790008800) | Бэкендер Тони (pi) | PR #126 создан, CI зелёный, задача закрыта |