fix(converter): warnings вместо тихой потери незнакомых нод/марок
Незнакомый тип ноды (default-ветка switch) молча схлопывался в свои дети, а незнакомая марка молча выбрасывалась — тихая потеря данных. Теперь сериализатор РЕПОРТИТ потерю: - по умолчанию поведение байт-в-байт прежнее (graceful degrade), но при переданном options.warnings в сток кладётся по одному сообщению на незамапленный тип (дедуп по типу) — потеря наблюдаема; - options.strict бросает ConverterLossError на ПЕРВОМ незнакомом типе (warning = ошибка). git-sync (lossless-путь) включает strict в stabilizePageBody: тип без серизализующей ветки падает громко на записи, а не пишет lossy .md. Валидный контент не затронут — у всех текущих типов схемы есть ветка. Покрытие: converter-loss-warnings.test.ts (нода/марка × strict/non-strict, дедуп, чистый контент) и strict-пин в git-sync stabilize.test.ts — всё через реальный конвертер. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,7 +72,13 @@ export async function stabilizePageFile(
|
||||
* keeps re-pulls of an unchanged page byte-identical (no churn, loop-guard).
|
||||
*/
|
||||
export async function stabilizePageBody(content: unknown): Promise<string> {
|
||||
const md1 = convertProseMirrorToMarkdown(content);
|
||||
// git-sync is the LOSSLESS mirror path, so run the serializer in `strict`
|
||||
// mode: a node/mark type the converter has no case for (e.g. one added to the
|
||||
// schema without a matching serializer arm) throws a ConverterLossError here
|
||||
// rather than silently degrading — surfacing the loss loudly at write time
|
||||
// instead of committing a lossy file. Valid content (every current schema type
|
||||
// has a case) is unaffected.
|
||||
const md1 = convertProseMirrorToMarkdown(content, { strict: true });
|
||||
const doc2 = await markdownToProseMirror(md1);
|
||||
return convertProseMirrorToMarkdown(doc2);
|
||||
return convertProseMirrorToMarkdown(doc2, { strict: true });
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { stabilizePageFile, type PageMeta } from '../src/engine/stabilize.js';
|
||||
// global DOM via jsdom at module load time (required for @tiptap/html under Node).
|
||||
import { markdownToProseMirror } from '@docmost/prosemirror-markdown';
|
||||
import { parseDocmostMarkdown } from '@docmost/prosemirror-markdown';
|
||||
import { ConverterLossError } from '@docmost/prosemirror-markdown';
|
||||
|
||||
// stabilize.ts (SPEC §11 normalize-on-write) was 0% covered (only the gated e2e
|
||||
// touched it). stabilizePageFile is import-testable: build a small ProseMirror
|
||||
@@ -66,6 +67,23 @@ describe('stabilizePageFile — normalize-on-write fixpoint (SPEC §11)', () =>
|
||||
expect(body1).toContain('data-src="/d.drawio"');
|
||||
});
|
||||
|
||||
it('runs the serializer in STRICT mode — an unmappable node throws, not a lossy write (#493)', async () => {
|
||||
// git-sync is the lossless mirror path: a node type the converter has no
|
||||
// case for (here a fabricated one, standing in for a schema type added
|
||||
// without a matching serializer arm) must surface loudly at write time
|
||||
// rather than being silently flattened into a lossy .md file.
|
||||
const content = {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{ type: 'paragraph', content: [{ type: 'text', text: 'ok' }] },
|
||||
{ type: 'quantumWidget', content: [{ type: 'text', text: 'lost?' }] },
|
||||
],
|
||||
};
|
||||
await expect(stabilizePageFile(content, meta)).rejects.toBeInstanceOf(
|
||||
ConverterLossError,
|
||||
);
|
||||
});
|
||||
|
||||
it('already-stable content is unchanged by the pass (idempotent)', async () => {
|
||||
// Plain prose is already a fixpoint; stabilizing it once and twice agree.
|
||||
const content = {
|
||||
|
||||
Reference in New Issue
Block a user