Metadata Write Audit Plan(2026-06)
目的
本文档用于审计 Kototoro 当前所有关键 metadata 写入路径,并定义收敛策略。
当前最危险的问题不是 metadata 读取复杂,而是:
metadata 在多个层级被多点写入、互相覆盖,并且都被误认为真相。
因此本计划的目标是:
- 列出当前 metadata 写入入口;
- 区分 authoritative write / override write / cache write;
- 明确哪些写入必须保留、哪些必须降级、哪些必须删除;
- 为 Work 化迁移建立 metadata 单向流动边界。
审计范围
本次重点审计以下 metadata 类别:
- title
- cover
- description
- tags
- content rating
- metadata source selection
- tracking enrichment-derived presentation fields
以及以下承载层:
mangamanga prefsentity_preferencestracking_site_links- tracking details cache
- backup / restore payload
核心原则
1. 允许读取聚合,不允许多点主写
允许:
- resolver 聚合 metadata
- cache 派生 snapshot
不允许:
- manga / entity / tracking cache 同时都能写“canonical truth”
2. 必须区分三种写入
authoritative write
改变主语义的写入。
例如:
- Work 默认 metadata source 选择
- Work override
override write
局部显式覆盖。
例如:
- per-projection metadata source override
- 用户手动标题/封面覆盖
cache write
仅为了读取性能或离线体验的派生缓存。
例如:
- tracking details cache
- resolved metadata snapshot
3. cache 不得反向成为真相
任何 cache write:
- 不得反向覆盖 Work ownership;
- 不得反向提升为 entity/projection 主真相;
- 不得通过 sync 回流重新污染主模型。
当前主要写入路径
以下路径基于当前代码扫描整理。
A. Work / Entity metadata selection 写入
入口 1
方法:
setEntityMetadataSourceSelection(...)
当前行为:
- 写
entity_preferences - 可选写
manga prefs(mirrorLocalMangaIds)
分类:
- entity 写入:authoritative write
- mirror 到 manga prefs:override write / 历史兼容写
当前状态:
- 已经做了止血,不再默认大范围 blind mirror;
- 但 API 仍保留显式 mirror 能力。
治理结论:
- 保留 entity 写入;
- mirror 仅允许窄范围显式 override;
- 不能再作为默认传播机制。
入口 2
方法:
applyMetadataSelection(...)
当前行为:
- 写
entity_preferences
分类:
- authoritative write
治理结论:
- 这条入口已经完成了从“混写 entity + manga prefs”到“只写 Work/entity default”的收口;
- 当前风险不再是它自身混写,而是调用方是否仍把 projection drift 和 Work drift 混在同一 repair 流程里处理;
- 后续审计重点应转向:
- projection drift 的诊断分类是否继续独立;
- repair 动作是否还会通过其它入口重建 projection shadow。
B. Projection metadata selection 写入
入口 3
方法:
setMetadataSourceSelection(mangaId, selection)
当前行为:
- 写
manga prefs.metadata_source_*
分类:
- override write
治理结论:
- 保留,但语义必须明确为:
- projection-local override only
- 不得再被用来承载 Work 默认 metadata authority。
入口 4
方法:
persistMetadataSourceSelection(...)persistMetadataSourceSelectionForCurrentEntity(...)
当前行为:
- 有 entity 时写 entity metadata preference
- 无 entity 时写 current manga prefs
分类:
- 有 entity:authoritative write
- 无 entity:override write / legacy fallback
治理结论:
- 当前方向是对的;
- 后续需要把“无 entity fallback”逐步缩小为纯 projection 场景。
补充说明:
DetailsViewModel当前又新增了一层运行时锚点收口:updateUnifiedReadingStatus()updateUnifiedRating()unregisterScrobbling()bindTrackingMatch()ignoreTrackingSuggestion()removeTrackingMatch()
- 这些入口不再直接复用页面初始
mangaId,而是先解析当前 resolved local projection。
这组改动本身没有新增新的 authoritative write 类型,但它修正了一个高频误用:
- runtime write 的落点仍然可能是 projection anchor;
- 但锚点必须来自当前详情上下文,而不是历史残留参数。
C. 用户 override 写入
入口 5
方法:
setOverride(manga, override)
当前行为:
- 写
manga prefs:title_overridecover_overridecontent_rating_override
分类:
- 当前实现已经是:
- 有 entity 时写
entity_preferences - 无 entity 时回退写
manga prefs
- 有 entity 时写
- 因此主链分类已是 Work override,projection 侧只剩 legacy fallback / local compatibility write
治理结论:
- 这条主写链已经基本完成 ownership 上移;
- 下一步重点不再是改主入口,而是继续清理历史镜像;
- 对于与 entity override 完全重复的 projection override,应通过 repair 剪枝。
当前对应治理入口:
REDUNDANT_PROJECTION_OVERRIDEpruneRedundantProjectionOverrides()
D. Repair / cleanup 写入
入口 6
方法:
clearMangaMetadataSourceIfSuspect(...)clearEntityMetadataSourceIfSuspect(...)
当前行为:
- repair 中回退
tracking -> base
分类:
- repair corrective write
治理结论:
- 必须保留;
- 但必须严格限制只修自己层级:
- projection override 修 projection
- Work default 修 Work
不能跨层顺手覆盖。
D-1. Projection-local tracking suggestion hint 写入
入口 6-1
方法:
setIgnoredTrackingSuggestion(mangaId, suggestion)
当前行为:
- 只写
preferences.ignored_tracking_suggestion_* - 当前 details 页相关入口已经统一先解析 current local projection,再执行该写入
- tracking suggestion 的首次加载与后续 refresh 也已统一跟随 current local projection
分类:
- projection-local hint write
治理结论:
- 这条写链应保留在 projection 层;
- 不应上移成 Work owner state;
- 但所有调用方都应避免继续把“初始打开时的 mangaId”当作当前 hint owner。
E. Tracking link cache 写入
入口 7
方法:
matchLocalContent(...)confirmMatch(...)
当前行为:
- 在 auto-match 或 manual confirm 后删除同 work/projection candidate 范围内的旧 link;
- 回写一条
tracking_site_links记录,作为当前 tracking match cache / audit anchor。
分类:
- cache write
- manual confirm 虽然带有用户意图,但
tracking_site_links本身仍不是 authoritative owner store
当前状态:
- 已完成 owner-aware 收口:
- 写入统一走
attachEntityOwnership(...) - 已有 link 选择不再依赖
first()或数据库返回顺序 - 当前优先级改为:
- requested projection
- anchor projection
- manual
- confidence
- updatedAt
- 写入统一走
治理结论:
- 保留这条写链,但明确只作为 cache / audit;
- 不得再通过公共 helper 反向定义 entity owner;
- 后续如果要进一步演进,应把 “confirmed work binding” 与 “projection cache anchor” 在数据语义上继续拆开。
入口 8
方法:
- source migration 中的 tracking link 搬运逻辑
当前行为:
- 只迁移旧 projection 自己持有的 tracking link anchor;
- 有 entity 时,仅搬
entityId下mangaId == oldMangaId的 link; - 无 entity 时,仅搬旧 projection 自己的 legacy link,并按
manual > confidence > updatedAt选主记录。
分类:
- cache write
- migration compatibility write
治理结论:
- 允许保留,因为 source migration 需要把旧 projection 的可读入口迁到新 projection;
- 但不得再做 work 范围的整组删写;
- entity-only link 与其他 projection anchor 不能被一次 source migration 顺带重写。
入口 9
MergeFavoriteEntitiesUseCase.kt
方法:
buildTrackingBindingGroups(...)isSafeTrackingMergeGroup(...)selectPreferredTrackingSelection(...)
当前行为:
- tracking link 仍可参与 merge 分组与 merge 后默认 tracking metadata 选择;
- 但当前工作树已经把证据门槛收紧为:
entity-owned- 或
manual
分类:
- cache-assisted merge evidence
治理结论:
- 允许把强证据 link 用于 merge 候选与 metadata 选择;
- 纯 legacy auto cache 不得再作为 work merge 的主证据;
- 后续若继续演进,应把 tracking binding truth 与 merge suggestion cache 彻底拆层。
入口 10
BindTrackingToEntitiesUseCase.kt
方法:
preview(...)bindOne(...)
当前行为:
- preview 阶段会先检查 merge group 是否已经解析到单一 entity;
- 如果该 entity 已存在目标 service 的 confirmed binding,则直接复用并产出
EXISTING_BINDINGpreview; - 只有不存在 confirmed binding 时,才继续走 dataset / aggregate api / online search。
- bind 阶段最多只执行一次
confirmMatch(); - 如果 entity 上已经存在对齐的 projection anchor,则直接跳过重复写入。
分类:
- authoritative binding reuse
- cache-avoiding read path
- cache write dedup
治理结论:
- 这是符合 Work-first 的正确短路;
- 已有 confirmed binding 不应被重复外部搜索覆盖;
- 后续 bind 阶段仍可继续复用
confirmMatch(...)维护 projection anchor,但不能把 preview 重新退回 cache-first。
F. Backup / Restore 写入
入口 11
当前行为:
- restore
manga prefs时会恢复:- title override
- cover override
- content rating override
- metadata source raw fields
分类:
- legacy import write
治理结论:
- 必须降级为导入兼容写;
- 不得在 restore 后自动提升为 Work 主真相;
- 后续 Work migration 期间必须与 sync isolation 一起收紧。
入口 12
当前行为:
- restore entity binding / entity prefs
分类:
- legacy import write
治理结论:
- 可保留;
- 但 legacy/sync 数据不得反向触发大范围 metadata propagation。
F. Source-native metadata 写入
入口 9
方法:
storeContent(...)
当前行为:
- 更新
manga表中的 source-native title/cover/authors/tags 等
分类:
- projection source write
治理结论:
- 必须保留;
- 这是 Projection 原始数据更新,不属于 Work 真相写入;
- 但不得反向更新 Work canonical metadata。
问题汇总
问题 1:Work 与 Projection 的 metadata 写入边界未完全切开
当前仍有路径会在一个事务里同时写:
entity_preferencesmanga prefs
这会导致:
- 语义混叠
- repair 复杂
- sync 难隔离
问题 2:override 仍被旧实现当成默认值承载层
manga prefs.metadata_source_* 本应降级为 projection override, 但历史上承担过 entity metadata mirror。
这会导致:
- “局部 override” 和 “作品默认 authority” 混在一起
问题 3:用户 override ownership 仍挂在 manga prefs
以下字段目前仍在 manga prefs:
- title override
- cover override
- content rating override
这与 Work ownership 目标不一致。
补充当前状态:
setOverride(manga, override)在存在 entity/work owner 时, 已经优先写入entity_preferences;- 同一 projection 上遗留的完全冗余 shadow override, 当前也会在写入时顺手清掉;
- 因此这条主链已不再是“纯 projection-owned override”。
但它仍未完全达到最终形态:
- override API 入口仍以
manga/ projection 参数驱动; - backup / restore / source migration 等兼容链路仍需继续防止旧 override 回流。
问题 4:backup/restore 仍能把旧 raw metadata 结构导回主流程
虽然这属于兼容需要,但如果没有 sync isolation 和导入降级策略,会继续造成:
- mirror 回流
- stale raw selection 回流
收敛目标
最终只允许四类 metadata 写入
- Projection source write
- 更新 source-native metadata
- Work authoritative write
- 更新 Work 默认 metadata authority / Work override
- Projection override write
- 更新显式 local override
- Cache write
- 更新 tracking / resolved metadata cache
除此之外的跨层 propagation 写入都应视为异常设计。
治理决策表
| 写入路径 | 当前分类 | 去留决策 | 说明 |
|---|---|---|---|
setEntityMetadataSourceSelection 写 entity prefs | authoritative | 保留 | 继续作为 Work metadata default 入口 |
setEntityMetadataSourceSelection mirror manga prefs | 历史兼容 / override | 收紧 | 仅显式单项 override 允许 |
setMetadataSourceSelection(mangaId) | override | 保留 | 仅 projection-local override |
setOverride(manga, override) | override | 上移 | 过渡期保留,最终迁到 Work override |
applyMetadataSelection 同时写 entity + manga | repair mixed write | 拆分 | 后续拆成 Work default 与 Projection override 两类动作 |
clearMangaMetadataSourceIfSuspect | repair override | 保留 | 仅修 projection override |
clearEntityMetadataSourceIfSuspect | repair authoritative | 保留 | 仅修 Work metadata default |
storeContent 更新 manga metadata | source write | 保留 | Projection source-native 更新 |
| backup/restore 导回 manga prefs metadata | import write | 降级 | 仅导入兼容,不提升为主真相 |
| tracking cache / details cache 更新 | cache write | 保留 | 但不得反写 Work/Projection 真相 |
分阶段执行
Phase 1:冻结传播型写入
目标:
- 停止 metadata 在多个层级无差别传播。
动作:
- 停 blind mirror
- repair 分类拆层
- binding 化 entity metadata preference
状态:
- 该阶段已部分完成
Phase 2:把 override 与 default 彻底拆层
目标:
- Work metadata default 与 projection override 不能再由同一方法长期混写。
动作:
- 拆
applyMetadataSelection(...) - 区分 Work repair 与 Projection repair
- 统一 Details/repair 写入语义
Phase 3:把用户 override 上移到 Work
目标:
titleOverridecoverUrlOverridecontentRatingOverride
从 manga prefs 迁移到 Work ownership。
Phase 4:引入 ResolvedMetadata cache
目标:
- 将 canonical title/cover/tags 收敛为单点 derived cache;
- 不再依赖多点 raw metadata copy。
约束:
- cache 只读或重建;
- 不反写主真相。
Phase 5:与 sync isolation 联动
目标:
- 阻断旧协议把旧 metadata mirror 语义重新写回。
动作:
- sync version 分代
- namespace 切换
- 旧版禁写新语义
验收标准
metadata 写入审计完成并执行收敛后,应满足:
- 新代码中所有 metadata 写入都能被归类为四种写入之一;
- 不再存在默认的 entity -> manga metadata blind mirror;
- projection override 与 Work default 在代码路径上明确分离;
- source-native metadata 更新不再反向定义 Work canonical metadata;
- backup/restore 不再把旧 raw metadata 结构重新扩散为主真相;
- metadata cache 明确只是 derived cache,而不是第三主模型。
一句话结论
你们当前最需要冻结的不是 metadata 读取,而是:
metadata 到底谁能写、写到哪一层、是否允许跨层传播。