Skip to content

anchor-integrity/fix.mjs: класс {#id}+декоративное тире помечается ambiguous, хотя чинится детерминированно #208

Description

@stgmt

Суть

anchor-integrity/fix.mjs объявляет «ambiguous» и отказывается чинить целый класс поломок, который на самом деле чинится детерминированно, без LLM. Класс системный: он порождён собственной конвенцией спек-генератора ({#id} в заголовках), поэтому воспроизводится по всему корпусу, а не в одном файле.

Что произошло

Гейт FR-34 на Stop заблокировал сессию:

.specs/anthropic-api-expansion/TASKS.md:46  [Phase 0]   -> #phase-0-refactoring
.specs/anthropic-api-expansion/TASKS.md:47  [Phase 0.5] -> #phase-05-implement-red-phase-stubs
.specs/anthropic-api-expansion/TASKS.md:48  [Phase 1]   -> #phase-1-p0-billing-fixes
.specs/anthropic-api-expansion/TASKS.md:49  [Phase 2]   -> #phase-2-p1-new-endpoints
.specs/anthropic-api-expansion/TASKS.md:50  [Phase 3]   -> #phase-3-p2-message-features
.specs/anthropic-api-expansion/TASKS.md:51  [Phase 4]   -> #phase-4-p3-advanced
Fix: node tools/anchor-integrity/fix.mjs --spec .specs/anthropic-api-expansion --apply --door

Штатный фиксер сдался:

anthropic-api-expansion   fixable=0 ambiguous=6 written=0 via-door
APPLIED via door: 0 deterministic fixes, 6 ambiguous (claude -p), 0 files written

Между тем все шесть ссылок в оглавлении были написаны правильно — сломаны были заголовки. Вот они дословно:

56:  ## Phase 0: Refactoring {#phase-0-refactoring}
112: ## Phase 0.5: Implement Red Phase Stubs {#phase-05-implement-red-phase-stubs}
156: ## Phase 1: P0 — Billing Fixes {#phase-1-p0-billing-fixes}
220: ## Phase 2: P1 — New Endpoints {#phase-2-p1-new-endpoints}
282: ## Phase 3: P2 — Message Features {#phase-3-p2-message-features}
356: ## Phase 4: P3 — Advanced {#phase-4-p3-advanced}

Две независимые причины, обе механические

Причина A — литеральный {#id} уезжает в slug. GLFM явные id не поддерживает: текст {#phase-0-refactoring} считается частью заголовка, поэтому slug получается phase-0-refactoring-phase-0-refactoring, а ссылка ведёт на #phase-0-refactoring. Достаточно снять суффикс {#...} — и slug становится ровно тем, что уже написано в ссылке.

Что {#id} инертны, подтверждается самим spec-сервером: read_spec_doc {section: "phase-05-implement-red-phase-stubs"}SECTION_NOT_FOUND, а {section: "Phase 0: Refactoring {#phase-0-refactoring}"} (полный текст вместе с литералом) → находит. То есть графом эти id не используются, удаление ничего не отвязывает.

Причина B — декоративное длинное тире даёт двойной дефис. Для Phase 1–4 снятия {#id} мало: Phase 1: P0 — Billing Fixes → пунктуация удаляется, два пробела вокруг тире превращаются в два дефиса → phase-1-p0--billing-fixes, а ссылка ведёт на phase-1-p0-billing-fixes. Убрать тире (Phase 1: P0 Billing Fixes) — slug сходится.

Именно на этой развилке (править заголовок или править ссылку) фиксер, судя по поведению, и объявляет ambiguous. Но развилка разрешается правилом, а не рассуждением.

Как это чинится детерминированно

Для каждой битой ссылки #target в том же документе:

  1. Собрать заголовки-кандидаты, чей slug совпал бы с #target после применения нормализаций-удалений, каждая из которых убирает только несемантический мусор:
    • удалить хвостовой {#...};
    • удалить декоративные разделители (, , ·, |) вместе с окружающими пробелами;
    • схлопнуть повторные пробелы.
  2. Если ровно один кандидат — применить, это не ambiguous.
  3. Если ноль или больше одного — только тогда ambiguous / LLM.

Ключевое: правки такого рода — чистые удаления оформительского мусора, они не меняют смысла заголовка и не трогают ссылки. Риск нулевой, а покрывают они, судя по списку гейта (24 спеки в этом репозитории), почти весь класс.

Я применил это правило руками — шесть заголовков, ничего кроме них:

- ## Phase 1: P0 — Billing Fixes {#phase-1-p0-billing-fixes}
+ ## Phase 1: P0 Billing Fixes

Результат вашей же проверялкой:

$ node tools/anchor-integrity/check.mjs --spec .specs/anthropic-api-expansion
0 broken anchors in .specs/anthropic-api-expansion   (exit 0)

Что просится в фикс

  1. Реализовать нормализации выше в fix.mjs как детерминированный слой (до LLM-фоллбэка). Тогда fixable станет 6 вместо 0 и гейт перестанет блокировать сессии из-за оформительского мусора.

  2. Определиться с конвенцией {#id} один раз. Сейчас генератор спек их эмитит (весь корпус ими усыпан: {#task-0-1}, {#phase-*}), а anchor-чекер и read_spec_doc их не признают. Либо чекер честно поддерживает {#id} (как pandoc/kramdown) — тогда эти ссылки не битые вовсе; либо генератор перестаёт их писать и существующие снимаются миграцией. Пока обе стороны не согласованы, гейт будет ловить собственный хвост.

  3. fix.mjs должен объяснять «ambiguous», а не просто считать. Сейчас вывод fixable=0 ambiguous=6 не говорит ни какие кандидаты рассматривались, ни почему выбор не сделан. Агенту приходится реверсить правила слагификации, чтобы понять, что вообще не так.

  4. Смежное, уже заведено отдельно — Дедлок: anchor-гейт требует починки, а MCP-дверь блокирует запись из-за 135 несвязанных pre-existing находок #207: даже когда фикс известен, записать его через MCP-дверь нельзя, если в документе есть несвязанные pre-existing нарушения формы (у меня — 135 находок task: missing Done When block). Мне пришлось воспользоваться логируемым escape [skip-spec-access: ...]. Пока Дедлок: anchor-гейт требует починки, а MCP-дверь блокирует запись из-за 135 несвязанных pre-existing находок #207 не починен, детерминированный фиксер из пункта 1 всё равно будет упираться в written=0 на таких файлах — эти два бага надо чинить парой, иначе фикс не долетит до диска.

Окружение

stgmt/dev-pomogator@2.0.4, Node v24.17.0, Windows 11, SPEC_ACCESS_ENFORCE=true, репозиторий lm-saas.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtoolingDeveloper tooling

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions