feat(rich-markdown): expand Obsidian wikilinks in articles - #23
Merged
Conversation
An Obsidian note sent as an article carries its [[wikilinks]] verbatim: message 408926 went out with 19 of them, and the vault holds 554. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three tasks: the pure strip_wikilinks() pass, wiring it first into normalize_rich_markdown() for all surfaces, then the dry-run marker and the CLAUDE.md invariant rewrite it makes necessary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n grew_by Code review fixes for the wikilink-strip branch: - strip_wikilinks now loops to a fixpoint (renamed single-pass body to _strip_once): [[ [[a]] ]], [[a|[[b]]]] and [[[[a]]]] previously left literal [[/]] behind after one pass, contradicting the documented idempotency invariant and shipping raw brackets to the reader. - send_message's post-normalisation over-limit error now names "wikilink expansion" in grew_by (a target with 3+ '#' grows the source via '#' -> ' > '), so an operator is no longer blamed on a spacing/grouping pass that never ran. - Corrected the stale pre-check comment in service.py to match CLAUDE.md's already-accurate "deliberately conservative" wording. - Docs: README/SKILL byte-for-byte recipe now notes wikilinks are always expanded (no knob); SKILL's second dry-run marker list is brought current (wikilinks, spaced, line_breaks, media_grouping, groups); design spec's "shrinks, never grows" claim corrected. - Tests: nested-wikilink fixpoint cases, a service-level test pinning the grew_by wording, and a CLI real-send assertion that wikilinks expand independently of --no-spaced-paragraphs. Full suite: 2237 passed. ruff check src tests: clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fixpoint loop added in the previous fix rescanned the whole document
once per nesting level with no bound, so a pathological run of nested
brackets ("["*16000 + "a" + "]"*16000, well under MAX_RICH_MARKDOWN_CHARS)
resolves one bracket pair per pass and turns one request into ~16 000
document rescans — measured at 3.76s, quadratic in nesting depth. That
scan runs inside normalize_rich_markdown, called from send_message before
the WRITE gate, which is exactly the pre-auth event-loop-blocking hazard
MAX_BLOCK_NESTING exists to prevent for scan_blocks.
Added MAX_WIKILINK_PASSES = 8 (real notes nest wikilinks one or two
levels deep) and bounded the loop by it, same trade-off _scan_nested makes
past MAX_BLOCK_NESTING: nesting past the cap stops and ships the leftover
[[/]] verbatim, like a degenerate [[]]. The three fixpoint repro cases and
test_is_idempotent still fully resolve (well within the cap).
Docs: qualified the "no [[ left" claims in CLAUDE.md, README.md and the
skill to "up to a bounded nesting depth" (one clause each). Skill re-synced
to ~/.claude/skills/telegram-assistant/SKILL.md.
Test: test_nesting_past_the_cap_stops_and_ships_the_remainder_verbatim
proves the cap structurally (pass count via length delta), no wall-clock
assertion.
Full suite: 2238 passed. ruff check src tests: clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Разворачивает Obsidian
[[wikilinks]]в обычный текст перед отправкой rich-статьи в Telegram, на всех трёх поверхностях (CLI / HTTP / MCP).Мотив: сообщение 408926 в Saved Messages уехало с 20 неразвёрнутыми ссылками — читатель видит сырые скобки и каноническое имя ноты вместо слова, которое написал автор.
Правило
Одно правило, без частных случаев: побеждает алиас, иначе цель, где
#→>, а ведущий#отбрасывается.[[Андрей Смирнов]]Андрей Смирнов[[Станислав Попов|Стасу]]Стасу[[#Спорные моменты]]Спорные моменты[[tasks#Настроить statusline]]tasks > Настроить statusline[[note#^blk]]note > ^blkРазделяет только первый
\|([[A\|B\|C]]→B\|C). Пустая половина падает на другую ([[Note\|]]→Note). Обе пустые ([[]],[[\|]]) — не ссылка, уезжает дословно: молча удалить набранные автором символы хуже, чем оставить курьёз.Что пасс не трогает
![[…]]— это медиа, её владелецscan_media. Отсутствие!и есть дискриминатор._CODE_SPAN_RE; маскирование проверяет вложенность, а не пересечение — то же правило, что уiter_line_media_refs.Где он стоит
Первым шагом
normalize_rich_markdown(), доscan_blocks(). Пасс правит текст внутри строки, поэтому счёт блоков и откат по 500 блокам должны видеть тот текст, который реально уедет. Побочно чинится латентный баг:\| [[A\|B]] \|в таблице ломал ячейку о собственный пайп ссылки.Не CLI-only — в отличие от
strip_yaml_frontmatterиscan_media, которые отвечают на вопрос «это файл из vault». Wikilink бессмысленен в Telegram независимо от того, кто прислал текст.Выключателя нет: литеральный
[[…]]в статье — всегда дефект.Инварианты
MAX_WIKILINK_PASSES = 8(по образцуMAX_BLOCK_NESTING). Потолок нужен, потому что цикл без него квадратичен по глубине вложенности и вешает event loop до WRITE-гейта: 8000 уровней давали 3.76 с, сейчас 0.007 с на любой глубине.#→>это +2), поэтомуgrew_byвsend_messageтеперь называет его — иначе переполнение из-за wikilink'ов приписывалось вызывающему.Отчётность
rich_markdown_wikilinksв payload--dry-run(Noneдля обычной отправки, как и все остальныеrich_*маркеры).Проверка
ruff check src testsчисто. Телеграм-трафика в тестах нет, только in-memory фейки.--dry-runпо мотивирующей ноте «Планёрка 29.07.2026»:rich_markdown_wikilinks: 20, отправки не было.🤖 Generated with Claude Code