panatgithub_AnkiHeadingSync/docs/compatibility-touchpoints-report.md
Dusk e2f973dc46 refactor: 切换 manual-sync 主链到 ID noteId / refactor manual-sync mainline to ID noteId
- 将 marker、state、writeback 的 durable identity 改为 noteId
- Switch marker, state, and writeback durable identity to noteId

- 支持满足标题规则的旧 ID 卡片重接进并按当前 renderer 重渲染
- Reconnect legacy ID cards that match heading rules and rerender them with the current renderer

- 补充 schema migration、测试、构建验证与插件同步
- Add schema migration, tests, build verification, and plugin sync
2026-04-19 11:46:37 +08:00

6 KiB
Raw Blame History

兼容性触点报告

分析边界

  • 只评估当前 manual-sync 主链,不把旧 syncRegistry 主链当成最终方案。
  • 证据来源限定为四类真值:仓库代码、用户提供的旧 Markdown 样本、用户提供的新 Markdown 样本、实时 AnkiConnect 返回。
  • 目标问题只有一个:当前主链能否安全接管由 Obsidian to Anki 生成的旧卡,并继续后续同步。

结论先行

当前主链不能直接接管旧卡。核心阻塞点不是 note model也不是 deck而是 identity bootstrap。

  • 旧样本只提供 legacy noteId marker<!--ID: 1760624596099-->
  • 新主链只识别 AHS marker<!-- AHS:card=ahs_xxx note=1776515511477 -->
  • 当前状态层以 cardId -> noteId 为主键,而不是以 noteId 为主键
  • 因此,旧文件在首次进入当前主链时,既拿不到 marker.cardId,也拿不到 pluginState.cards[cardId],最后只能被当成新卡创建

这意味着“直接接管”会造成重复建卡,而不是认领旧卡。

关键触点一览

触点 当前行为 兼容性影响 结论
src/domain/manual-sync/services/CardMarkerService.ts isCandidate()parse() 只认 <!-- AHS:card=... note=... --> 旧样本 <!--ID: ...--> 对主链是不可见 marker 不兼容
src/domain/manual-sync/services/CardIndexingService.tsextractMarker() 只会剥离 AHS marker没识别出的 legacy marker 会被保留在正文里 旧 marker 不会转成 noteId,还会污染 bodyMarkdownrawBlockHash 不兼容
src/domain/manual-sync/services/CardIndexingService.tsresolveIdentity() 解析顺序是 marker.cardId -> pendingWriteBack -> knownCards(filePath, rawBlockHash) -> 生成新 cardId 旧文件首次扫描时三条已有身份路径都不存在,必然生成新 cardId 不兼容
src/domain/manual-sync/services/DiffPlannerService.ts resolvedNoteId 为空就进入 toCreate 旧卡不会被“接管更新”,而会被“新建重复卡” 不兼容
src/domain/manual-sync/entities/PluginState.ts cardscardId 为 key状态里没有 legacy marker 元数据位 当前状态容器无法直接承接“只有 noteId、没有 cardId”的旧卡 不兼容
src/infrastructure/persistence/DataJsonPluginStateRepository.ts 只读取 snapshot.pluginState,不会消费 syncRegistry 即便 data.json 里还有旧 syncRegistry 记录manual-sync 也不会拿来认领旧卡 不兼容
src/application/services/FileIndexerService.ts 只把 pluginState.cardspendingWriteBack 喂给 indexer 旧卡若没先迁入 pluginState,主链没有任何第二身份来源 不兼容
src/application/services/MarkdownWriteBackService.ts 只会写入新的 AHS marker 如果直接跑在旧文件上,会在 legacy marker 后再插入一个 AHS marker而不是替换旧 marker 有风险
src/domain/manual-sync/services/DeckExtractionService.ts + DeckNormalizationService.ts 会提取 TARGET DECK,并保留 deck segment 原文,只做分隔符规范化 旧样本里的 [[anki背诵]]::[[《城市居住区规划设计标准》图解2021年]] 能被原样保留 兼容
src/domain/manual-sync/services/ManualCardRenderer.ts 当前渲染会在背面追加 Open in Obsidian backlink 即使完成接管,后续同步也会把旧卡字段逐步归一到新渲染样式 条件兼容
src/application/services/HeadingSyncMarkerService.ts + src/domain/card/services/CardExtractionService.ts 旧链路支持的是 <!-- AHS:123 --> 这种 noteId-only marker 这和本次真实旧样本的 <!--ID: ...--> 不是同一种格式,也不在 current manual-sync 主链里 不能直接复用

代码触点拆解

1. Marker 语义已经换代

当前 manual-sync 的 marker 不是“把 noteId 嵌进去就行”,而是显式要求:

  • 一个稳定的 cardId
  • 一个可选的 noteId
  • 一个确定的 AHS marker 字面量

旧样本只有 noteId,没有 cardId。这不是缺一个字段那么简单,而是整个主链的身份主键已经从 noteId 切成了 cardId

2. 当前主链不会利用旧 syncRegistry

实时 data.json 里仍然同时存在:

  • syncRegistry.records[].identityMode = "embedded-note-id"
  • pluginState.files / pluginState.cards

DataJsonPluginStateRepository 只加载 pluginStatemanual-sync 完全不看 syncRegistry。所以旧状态即便还在 data.json 里,也不会自动帮助旧卡进入新主链。

3. Deck 不是阻塞点

这是这次分析里最重要的反向结论之一。

  • 旧样本 Markdown 明确声明了 TARGET DECK: [[anki背诵]]::[[《城市居住区规划设计标准》图解2021年]]
  • 旧样本 live card 的 cardsInfo.deckName 也正是这个 wikilink deck 字符串
  • 当前 DeckNormalizationService 不会剥掉 [[...]],只会规范 /\::

因此,旧 deck 的字面值兼容是成立的。真正不兼容的是 identity不是 deck。

4. Note model 与字段映射也不是阻塞点

实时样本显示:

  • 旧样本 live notemodelName = 问答题,字段为 正面 / 背面
  • 新样本 live notemodelName = 问答题,字段为 正面 / 背面
  • 当前插件设置:basic:问答题 -> titleField=正面, bodyField=背面

所以旧卡与新卡在 Anki 侧的数据模型是兼容的,后续接管不需要做 note type 迁移。

最终触点判断

已兼容

  • 旧 deck 字符串可继续沿用
  • 旧 note model / 字段映射与当前设置兼容
  • 旧 noteId 在 Anki 中真实存在,具备可迁移基础

未兼容

  • legacy marker 识别
  • 首次 identity 认领
  • 旧状态向 pluginState 的引导
  • 直接运行时的防重复保障

因此得到的主结论

当前主链不属于“可直接接管”。

它属于“可以接管,但必须先做一次迁移写回”:

  • 先把旧 noteId 身份翻译成新 cardId + noteId 身份
  • 再把 Markdown marker 和 pluginState 一次性对齐到 current manual-sync 语义