feat(footnotes): reuse semantics + import diagnostics (#166) #169

Merged
Ghost merged 2 commits from feat/footnote-reuse-and-warnings into develop 2026-06-24 16:39:00 +03:00

Closes #166 (data-integrity core). Multi-backlink editor UI split to #168.

Проблема

Сноски были строго 1:1: повторная [^a] ссылка считалась коллизией и переименовывалась в a__2, а ссылка без определения синтезировала собственную пустую сноску — поэтому статья агента с переиспользованными ярлыками давала десятки пустых kowiki__N.

Решение — семантика Pandoc REUSE + диагностика импорта

Reuse (ядро).

  • resolveCollisions (footnote-sync): повторные ссылки с одним id — это REUSE (записываются один раз в порядке появления, НИКОГДА не переименовываются) → один номер, одно определение. Только дубль-ОПРЕДЕЛЕНИЕ переименовывается детерминированно (deriveFootnoteId) и, не имея своей ссылки, отбрасывается существующей orphan-политикой (first-wins). CollisionPlan.refReids теперь всегда пуст (безвредный no-op ниже по течению).
  • extractFootnoteDefinitions (marked) и extractFootnotes (MCP): дубль-определения — FIRST-WINS (первое сохраняется, остальные отбрасываются); маркеры ссылок не переписываются. Удалён переписыватель маркеров и ставший мёртвым зеркальный deriveFootnoteId + хелперы в MCP.

Диагностика.

  • Новый analyzeFootnotes() (MCP): fence-aware чистый скан → dangling-ссылки, пустые/дублирующиеся определения, маркеры [^id] в строках таблиц.
  • create_page / update_page / import_page_markdown теперь возвращают footnoteWarnings (только если непусто) — агент видит проблему и чинит разметку; страница всё равно создаётся.

Paste-reuse.

  • footnotePastePlugin ремапит id только если вставляемый слайс ОПРЕДЕЛЯЕТ конфликтующий id; вставка одиночной ссылки на существующий id сохраняет id (reuse).

Тесты

Reuse/first-wins переписаны в footnote.test, footnote-markdown.test, footnote.marked.orphan.test, MCP footnotes.test; новые footnote-paste.test (editor-ext) и footnote-analyze.test (MCP). Удалён derive-id-parity.test.mjs (MCP больше не выводит id; editor-ext deriveFootnoteId сохраняет свой golden-тест). editor-ext 128, MCP 299, server roundtrip 2, client views 3; client+server tsc чисто.

Ревью

Ревью-сабагент: трасса [^d][^d] + два [^d]: сходится за один проход (SYNC_META), детерминизм Yjs сохранён, зеркала marked/MCP идентичны, удаление мёртвого кода безопасно. Вердикт APPROVE with suggestions — обе косметические правки (устаревший комментарий + формулировка предупреждения) применены.

Границы / следующий шаг

Мульти-бэклинки в редакторе (определение возвращает ко ВСЕМ своим ссылкам ↩ a b c) вынесены в #168 — это UI-слой с визуальной проверкой. Forward-ссылки и нумерация уже корректно reuse'ятся; обратная стрелка пока ведёт к первой ссылке.

🤖 Generated with Claude Code

Closes #166 (data-integrity core). Multi-backlink editor UI split to #168. ## Проблема Сноски были строго 1:1: повторная `[^a]` ссылка считалась коллизией и переименовывалась в `a__2`, а ссылка без определения синтезировала собственную пустую сноску — поэтому статья агента с переиспользованными ярлыками давала десятки пустых `kowiki__N`. ## Решение — семантика Pandoc REUSE + диагностика импорта **Reuse (ядро).** - `resolveCollisions` (footnote-sync): повторные ссылки с одним id — это REUSE (записываются один раз в порядке появления, НИКОГДА не переименовываются) → один номер, одно определение. Только дубль-ОПРЕДЕЛЕНИЕ переименовывается детерминированно (`deriveFootnoteId`) и, не имея своей ссылки, отбрасывается существующей orphan-политикой (first-wins). `CollisionPlan.refReids` теперь всегда пуст (безвредный no-op ниже по течению). - `extractFootnoteDefinitions` (marked) и `extractFootnotes` (MCP): дубль-определения — FIRST-WINS (первое сохраняется, остальные отбрасываются); маркеры ссылок не переписываются. Удалён переписыватель маркеров и ставший мёртвым зеркальный `deriveFootnoteId` + хелперы в MCP. **Диагностика.** - Новый `analyzeFootnotes()` (MCP): fence-aware чистый скан → dangling-ссылки, пустые/дублирующиеся определения, маркеры `[^id]` в строках таблиц. - `create_page` / `update_page` / `import_page_markdown` теперь возвращают `footnoteWarnings` (только если непусто) — агент видит проблему и чинит разметку; страница всё равно создаётся. **Paste-reuse.** - `footnotePastePlugin` ремапит id только если вставляемый слайс ОПРЕДЕЛЯЕТ конфликтующий id; вставка одиночной ссылки на существующий id сохраняет id (reuse). ## Тесты Reuse/first-wins переписаны в `footnote.test`, `footnote-markdown.test`, `footnote.marked.orphan.test`, MCP `footnotes.test`; новые `footnote-paste.test` (editor-ext) и `footnote-analyze.test` (MCP). Удалён `derive-id-parity.test.mjs` (MCP больше не выводит id; editor-ext `deriveFootnoteId` сохраняет свой golden-тест). **editor-ext 128, MCP 299, server roundtrip 2, client views 3**; client+server tsc чисто. ## Ревью Ревью-сабагент: трасса `[^d][^d]` + два `[^d]:` сходится за один проход (SYNC_META), детерминизм Yjs сохранён, зеркала marked/MCP идентичны, удаление мёртвого кода безопасно. Вердикт **APPROVE with suggestions** — обе косметические правки (устаревший комментарий + формулировка предупреждения) применены. ## Границы / следующий шаг Мульти-бэклинки в редакторе (определение возвращает ко ВСЕМ своим ссылкам ↩ a b c) вынесены в **#168** — это UI-слой с визуальной проверкой. Forward-ссылки и нумерация уже корректно reuse'ятся; обратная стрелка пока ведёт к первой ссылке. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Ghost added 1 commit 2026-06-24 15:36:03 +03:00
Footnotes were strict 1:1: a repeated `[^a]` reference was treated as a
collision and re-id'd to `a__2`, and a reference with no definition synthesized
its own empty one — so an agent-authored article with reused labels produced
dozens of empty `kowiki__N` footnotes. Move to Pandoc REUSE semantics and add
non-fatal import diagnostics.

Reuse (core):
- resolveCollisions (footnote-sync): repeated references sharing an id are REUSE
  (recorded once in document order, never re-id'd) — one number, one shared
  definition. Only a duplicate DEFINITION is re-id'd deterministically and, with
  no matching reference, dropped by the existing orphan policy (first-wins).
  CollisionPlan.refReids is now always empty (harmless no-op downstream).
- extractFootnoteDefinitions (marked) and extractFootnotes (MCP): duplicate
  definition ids are FIRST-WINS (keep first, drop rest); reference markers are
  never rewritten. Removed the marker-rewriting and the now-dead deriveFootnoteId
  mirror + helpers from the MCP path.

Import diagnostics:
- New analyzeFootnotes() (MCP): fence-aware pure scan reporting dangling
  references, empty/duplicate definitions and `[^id]` markers inside table rows.
- createPage / updatePage / importPageMarkdown now attach `footnoteWarnings`
  (only when non-empty) so an agent can fix its markup; the page is still created.

Paste-reuse:
- footnotePastePlugin remaps only ids the pasted slice DEFINES (a colliding
  definition); a pasted lone reference to an existing id keeps it (reuse).

Tests: reuse/first-wins rewrites of footnote.test, footnote-markdown.test,
footnote.marked.orphan.test and the MCP footnotes.test; new footnote-paste.test
(editor-ext) and footnote-analyze.test (MCP). Deleted derive-id-parity.test.mjs
(the MCP no longer derives ids; editor-ext's deriveFootnoteId keeps its own
golden test). editor-ext 128, MCP 299, server roundtrip 2, client views 3,
client+server tsc clean.

Two review suggestions applied: corrected a stale "duplicated in MCP" comment and
the dangling-reference warning wording.

Note: the multi-backlink editor UI (a reused definition linking back to each of
its references) is deferred to a follow-up — this PR delivers the data-integrity
core (reuse + warnings + paste-reuse). Forward links and numbering already reuse
correctly; the backlink currently targets the first reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ghost added 1 commit 2026-06-24 16:16:43 +03:00
- footnote-sync: remove the now-dead `refReids` (CollisionPlan field, local,
  return, the 6a consumer loop) — references are never re-id'd under reuse, so it
  was dead structure on the hot reconciliation path. Rewrite the stale comments
  (plugin header, step 0, refOccurrences field) that still described the old
  "duplicates re-id'd so both survive" model to the reuse model.
- Shared footnote lexer: new packages/mcp/src/lib/footnote-lex.ts
  (lexFootnoteLines + forEachFootnoteReference). extractFootnotes (collaboration)
  and analyzeFootnotes now consume the SAME fence-aware lexer, so "the analyzer
  sees exactly what the importer keeps/strips" is structural, not comment-kept.
  Removed the duplicated DEF_RE/fence machine from both consumers.
- Tests: new mock test for the footnoteWarnings plumbing on createPage (problems
  -> field present; clean -> omitted); new paste-reuse case for TWO colliding
  pasted definitions (reservation -> distinct ids). Updated the derive-id golden
  test header (no MCP copy / parity test anymore).
- CHANGELOG: [Unreleased] entries for footnote reuse (Changed, supersedes 0.93.0)
  and footnoteWarnings (Added).

editor-ext 129, MCP 301, server roundtrip 2; client+server tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owner

можно мержить после исправления

можно мержить после исправления
Ghost merged commit b9056e2bee into develop 2026-06-24 16:39:00 +03:00
Ghost deleted branch feat/footnote-reuse-and-warnings 2026-06-24 16:39:00 +03:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#169