fix(#244): two HIGH data-loss bugs — lossless markdown export + store-side empty-guard #248
Open
Ghost
wants to merge 5 commits from
fix/244-dataloss-bugs into develop
pull from: fix/244-dataloss-bugs
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:test/244-part-b
vvzvlad:fix/255-ws-redis-adapter-leak
vvzvlad:feat/251-intentional-clear
vvzvlad:fix/252-e2e-open-handles
vvzvlad:feat/184-autonomous-agent-runs
vvzvlad:feat/221-image-captions
vvzvlad:feat/git-sync
vvzvlad:refactor/193-tool-spec-registry
vvzvlad:fix/embeddings-reindex-progress
vvzvlad:develop
vvzvlad:feature/offline-sync
vvzvlad:feat/229-catalog-yaml
vvzvlad:feat/243-blob-sandbox
vvzvlad:feat/228-inline-footnotes
vvzvlad:fix/qa-ui-bugs-216-218
vvzvlad:feature/agent-roles-catalog
vvzvlad:fix/share-alias-rename
vvzvlad:fix/ai-chat-empty-render
vvzvlad:feat/191-chat-doc-binding
vvzvlad:feat/201-temporary-notes
vvzvlad:feat/198-interrupt-agent
vvzvlad:feat/ai-chat-full-history
vvzvlad:feat/199-ai-generate-title
vvzvlad:feat/205-share-aliases
vvzvlad:batch/issues-189-187-170
vvzvlad:feat/170-mcp-test-button
vvzvlad:feat/189-context-badge
vvzvlad:feat/198-interrupt-agent-send-now
vvzvlad:fix/issues-190-159
vvzvlad:fix/ai-chat-new-chat-during-stream
vvzvlad:fix/ai-chat-stream-perf
vvzvlad:batch/issues-2026-06-25
vvzvlad:feat/ai-chat-persistent-history
vvzvlad:fix/ai-chat-copy-chat-wysiwyg
vvzvlad:fix/ai-stream-reset-resilience
vvzvlad:fix/ai-stream-undici-timeout
vvzvlad:fix/footnote-review-1227-followup
vvzvlad:fix/ai-chat-token-counter-realtime
vvzvlad:docs/manual-qa-test-plan
No Reviewers
Labels
Clear labels
bug
documentation
duplicate
enhancement
epic
feature
good first issue
help wanted
idea
invalid
needs-human
question
refactor
review/approved
review/changes-requested
review/needs
security
status/blocked
status/done
status/in-progress
status/ready
test
wontfix
Something isn't working
Improvements or additions to documentation
This issue or pull request already exists
New feature or request
Large multi-phase effort spanning many changes
New functionality request
Good for newcomers
Extra attention is needed
Idea / proposal for discussion
This doesn't seem right
эскалация: нужно решение человека
Further information is requested
Code cleanup / refactoring
в последнем ревью нет открытых blocking-находок
последнее ревью оставило открытые blocking-находки
head не ревьюился (head != reviewed_head)
Security / hardening issue
ждёт зависимость blocked_by
закрыто и проверено
в активной работе (мягкая заявка)
специфицировано, не заблокировано, ждёт исполнителя
Test coverage / test infrastructure
This will not be worked on
Milestone
No items
No Milestone
Projects
Clear projects
No project
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: vvzvlad/gitmost#248
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "fix/244-dataloss-bugs"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Долг #244 Часть A — два HIGH-бага потери данных
Закрывает Часть A issue #244: два подтверждённых бага потери данных, которые PR #230 осознанно оставил незафиксированными (только характеризующие
it.fails/it.failing). Теперь это настоящие фиксы, тесты зелёные.mdrt-2 — экспорт в Markdown молча выкидывал кастом-узлы
turndown.utils.ts: добавлены 4 правила (pageBreak,transclusionReference,mention,status) + предобработка. Две причины: (1) blank-node removal в turndown использует module-levelisVoid(опцией не переопределить) → бездетные atom-<div>(pageBreak/transclusionReference) попадали в blankRule и исчезали → починеноfillEmptyAtomBlocks(zero-width space, как существующийfillEmptyFootnoteRefs); (2) mention/status не пустые, но падали в дефолт-правило, теряяdata-id/data-color. Lossless-представление: каждый узел реэмитится как инертный raw-HTML из ВСЕХ атрибутов (serializeAttrs), inline — с экранированным текстом. Round-trip подтверждён (markdownToHtml пропускает raw-HTML,parseHTMLкаждого узла восстанавливает ноду).persist-6 — пустой live-doc затирал непустой контент
persistence.extension.ts: store-side empty-guard вonStoreDocument(внутри транзакции, после unchanged-short-circuit, до saveHistory/updatePage): еслиisEmptyParagraphDocИpage.contentнепустой И нетcontext.intentionalClear→ warn + skip записи. Зеркалит guard изonLoadDocument. Легитимные очистки не сломаны:intentionalClear===trueпишет; новые/пустые страницы не затронуты (guard требует непустой существующий контент); empty-over-empty отсекается раньше как deep-equal.Тесты
editor-ext vitest 197 pass (turndown.dataloss 8/8, no .fails); server persistence-store 9/9 (no .failing) + intentional-clear escape + empty-over-empty; collaboration 65, import round-trip 59. editor-ext + server tsc чисто.
Часть B (большой тест-бэклог) — НЕ в этом PR, остаётся открытой в #244.
🤖 Generated with Claude Code
Round-1 review F1 (maintainer decision: variant A). The store-side empty-guard's `context?.intentionalClear === true` branch was dead: `intentionalClear` is never set in production (connection context is {user, actor, aiChatId}); it appeared only in the guard and a hand-injected spec, so the guard already blocked empty-over-non-empty unconditionally. - persistence.extension.ts: drop the dead branch; the guard now simply skips empty-over-non-empty, full stop. Reference issue #251 (real intentional-clear UX) in the comment where the branch was. - persistence-store.spec.ts: remove the misleading "persists an intentional clear" escape-hatch test (false coverage — green only because the flag was injected by hand). Real guard tests (empty-over-empty allowed, empty-over-non-empty blocked, etc.) kept. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>The "does not block an empty store over an already-empty page" test set the stored page.content to TiptapTransformer.fromYdoc(document,'default') — exactly the value tiptapJson is computed from — so isDeepStrictEqual(tiptapJson, page.content) was TRUE and onStoreDocument RETURNED at the unchanged short-circuit before ever reaching the empty-guard. It exercised the old short-circuit, not the new guard's `!isEmptyParagraphDoc(page.content)` branch (the only NEW branch protecting empty existing pages from over-blocking); the condition could be removed and the test would still pass (false coverage). Set stored content to an empty paragraph with `content: []` — empty per isEmptyParagraphDoc but NOT deep-equal to the live doc (which normalizes to a paragraph with `attrs: { indent: 0 }` and no content key). Execution now skips the short-circuit and enters the guard; reorient the assertion to "the write is NOT blocked" (updatePage IS called). Verified the test now FAILS if the `!isEmptyParagraphDoc(page.content)` condition is removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Ревью
90a3fa012— переревью ПОЛНЫМИ 8 аспектами (отдельный субагент на каждый: security, stability, regressions, test-coverage, conventions, documentation, simplification, architecture). Вердикт: approved.Все 8 аспектов — LGTM. Подтверждено: XSS/инъекций нет (экранирование атрибутов корректно, импорт через schema/DOMPurify), empty-guard в правильном слое (в транзакции под lock, не глотает легитимное, empty-over-empty доходит до guard и проходит), round-trip lossless и обратно-совместим для документов без новых узлов, оба бага покрыты регрессионными тестами (падали бы на pre-fix), guard-комментарии и ссылка на #251 точны. Открытых находок нет.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.