panatgithub_AnkiHeadingSync/docs/id-noteid-mainline-gap-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

262 lines
8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ID NoteId Mainline Gap Report
## Scope
本报告只针对当前 manual-sync 主链,审查它与本轮契约之间的偏差:
- 主链 durable identity 切换为 `<!--ID: noteId-->`
- 满足当前标题级别规则的旧卡支持按 `ID` marker 重接进
- 认领成功后按当前 renderer 重渲染更新
- plugin state / writeback / execution 全部切到 noteId 主身份
## Executive Summary
当前实现与本轮契约的偏差是系统性的,不是局部修补能解决的小问题。
当前 manual-sync 主链仍然围绕以下假设建立:
- Markdown marker 协议是 `<!-- AHS:card=... note=... -->`
- `cardId` 是 durable identity
- `PluginState.cards``cardId` 作为 key
- `pendingWriteBack``cardId` 追踪
- 一次运行内与持久化状态共用同一身份键
而本轮契约要求:
- `<!--ID: noteId-->` 成为唯一主链 marker 协议
- `noteId` 成为 durable identity
- `syncKey` 只作为单次运行内的相关键
-`ID` marker 卡片可以在满足当前标题规则时被认领并重渲染
结论:当前主链必须做一次贯穿 marker、index、plan、execute、state、writeback 的统一重构。
## Gap Inventory
### 1. Marker protocol is wrong
文件:`src/domain/manual-sync/services/CardMarkerService.ts`
现状:
- 候选 marker 只看 `<!-- AHS:`
- 合法 marker 只接受 `<!-- AHS:card=... note=... -->`
- create/writeback 也只会生成 AHS marker
- service 内还负责生成新的 `cardId`
与契约冲突:
- 主链 marker 必须切为 `<!--ID: noteId-->`
- 合法 marker 只能接受正整数 noteId
- 非法 trailing `<!--ID: ...-->` 候选也要剥离并占用 marker 槽位
- 主链不再以 marker 生成 durable `cardId`
影响:
- 当前主链无法识别旧卡 marker
- 当前 writeback 永远不会写出正确协议
### 2. IndexedCard schema still treats cardId as durable identity
文件:`src/domain/manual-sync/entities/IndexedCard.ts`
现状:
- `IndexedCard` 仍持有 `cardId`
- 仍使用 `markerState = "missing" | "card-only" | "card-and-note"`
- 没有 `syncKey`
- 没有 `idMarkerState`
- 没有 `noteIdSource`
与契约冲突:
- durable `cardId` 应从主链 index 结果中移除
- 需要新增运行内 `syncKey`
- 需要区分 marker 是否存在且是否合法
- 需要显式记录 noteId 来源是 marker / state recovery / pending writeback
影响:
- 后续 planning / execution 无法按契约判断“认领”“恢复”“重写 marker”
### 3. Persisted state schema is still cardId-centric
文件:`src/domain/manual-sync/entities/PluginState.ts`
现状:
- `FileState.cardIds` 仍然存在
- `CardState.cardId` 仍然存在
- `CardState.noteId` 仍是可选
- `PendingWriteBackState``cardId` 追踪
- `PluginState.cards` 的 key 仍隐含为 `cardId`
与契约冲突:
- `PluginState.cards` 必须改为 `String(noteId)` 键控
- `FileState.cardIds` 必须改为 `noteIds`
- `CardState.noteId` 必须是必填
- `pendingWriteBack` 必须改成按块身份追踪
影响:
- 当前 persisted state 不能作为 noteId 主身份的真相源
### 4. State repository has no schema migration
文件:`src/infrastructure/persistence/DataJsonPluginStateRepository.ts`
现状:
- `load()` 只是直接返回 `snapshot.pluginState`
- 不迁移旧 `cards[cardId]`
- 不迁移 `files[].cardIds`
- 不丢弃旧 pending writeback
与契约冲突:
- 需要在 load 阶段显式升级旧 schema
-`pendingWriteBack` 要直接丢弃
影响:
- 主链切协议后,旧状态无法安全进入新主链
### 5. Indexing still resolves identity by cardId
文件:`src/domain/manual-sync/services/CardIndexingService.ts`
现状:
- marker extraction 只识别 AHS marker
- `resolveIdentity()` 顺序是marker cardId -> pending by cardId -> knownCards by rawBlockHash -> 生成新 cardId
- `usedCardIds` 用于去重
- `rawBlockHash` 当前只会剥除 AHS marker不会剥除旧 `ID` marker
与契约冲突:
- 先识别“当前标题规则命中的块”,再处理 ID marker
- identity resolution 必须变成marker noteId -> unique same-file rawBlockHash state recovery -> pending writeback -> undefined
- 去重必须基于 noteId而不是 cardId
- `rawBlockHash` 必须基于剥除合法/非法 trailing ID marker 后的内容计算
影响:
-`<!--ID: ...-->` 卡会被当成正文内容
- 会错误走新建分支
### 6. Planner still assumes cardId + markerNoteId semantics
文件:`src/domain/manual-sync/services/DiffPlannerService.ts`
现状:
- 使用 `cardsById``card.cardId` 去重
- 读取 `state.cards[card.cardId]`
- `pendingWriteBack``cardId` 命中
- `toRewriteMarker` 依赖 `markerState = missing/card-only`
- orphan 识别依赖 `card.cardId`
与契约冲突:
- 规划必须以最终 noteId 和运行内 syncKey 为中心
- recovered noteId、invalid marker、新创建 note 都要触发 rewrite marker
- blocks with existing noteId but no state 也能正常进入 update path
影响:
- 当前 planner 无法正确表达“旧卡重接进 + 重写 ID marker”
### 7. Executor still uses cardId as the main run-time correlation key
文件:`src/application/services/AnkiBatchExecutor.ts`
现状:
- `resolvedNoteIds``card.cardId` 保存
- `touchedCardIds``card.cardId` 保存
- `markerWriteMap``card.cardId` 保存
- rendered cards map 也依赖 `card.cardId`
与契约冲突:
- durable identity 不能再依赖 cardId
- 运行内相关键应切为 `syncKey`
- note existence check 失败时应降级到 create flow并最终写回真实 `<!--ID: newNoteId-->`
影响:
- 即便 planner 修好executor 仍会把旧身份流重新扭回 cardId 模式
### 8. Markdown writeback still writes by cardId
文件:`src/application/services/MarkdownWriteBackService.ts`
现状:
- 结果结构返回 `writtenCardIds`
- 写回请求包含 `cardId`
- pending entry 保存 `cardId`
- target marker 仍通过 AHS marker service 生成
与契约冲突:
- 写回必须统一输出 `<!--ID: noteId-->`
- pending writeback 要按 `(filePath, blockStartLine, rawBlockHash)` 追踪
- 结果结构也应转向 noteId/syncKey 语义
影响:
- 当前 writeback 不能支撑新 marker 协议和新 pending shape
### 9. Tests are still asserting the old contract
主要文件:
- `src/domain/manual-sync/services/CardMarkerService.test.ts`
- `src/domain/manual-sync/services/CardIndexingService.test.ts`
- `src/domain/manual-sync/services/DiffPlannerService.test.ts`
现状:
- 测试名称和断言大量依赖 `cardId`
- 大量断言使用 AHS marker 字面值
- 缺少“invalid ID marker but valid block”“not matching heading rules -> ignore marker”“schema migration”这类契约要求
影响:
- 即使代码完成重构,现有测试也会大面积误报失败
- 当前测试对本轮目标没有形成足够保护
## Architecture Notes
### Good existing structure worth preserving
- `ManualSyncService -> FileIndexerService -> DiffPlannerService -> AnkiBatchExecutor -> MarkdownWriteBackService` 这条主链分层是合理的
- 当前 rendering boundary 仍然干净,可以直接复用在旧卡重接进后的重渲染更新
- `DeckResolution` 和 deck migration 相关逻辑与本轮冲突较少,应尽量少动
### Mainline drift that must be removed
- main sync durable identity 仍与 module 3/4 的 AHS assumptions 紧耦合
- `cardId` 同时承担 persisted identity 和 in-run correlation key两类职责未分离
## Required Repair Direction
必须完成的主修复方向:
1. marker 协议切换到 `<!--ID: noteId-->`
2. `noteId` 成为持久主身份,`syncKey` 成为运行内相关键
3. state schema 与 repository load 做显式迁移
4. index / plan / execute / writeback 全链去掉 durable `cardId` 依赖
5. 增加对“满足当前标题规则且带旧 `ID` marker”的重接进与重渲染支持
6. 重写对应测试,补足 integration 覆盖
## Out Of Scope
根据契约,本轮明确不做:
- 继续支持 `<!-- AHS:card=... note=... -->` 文件
- 双 marker 并读
- 旧 HTML 保留模式
- 不匹配标题规则的 `ID` marker 清理工具
- legacy heading compatibility mode