# B 站博主动态采集辅助工具 HOLD/5 最小修复设计 V002 ## 1. 修订身份与适用范围 - 事项:`DEV-PROJECT-INFO-BILI-DYNAMIC-COLLECTOR-MVP-20260804-001` - 需求:`REQ-BILI-DYNAMIC-COLLECTOR-20260804-001` - 基线:`CODE-DESIGN-PROJECT-INFO-BILI-DYNAMIC-COLLECTOR-MVP-HOLD5-REPAIR-V001.md` - 基线字节数:`11917` - 基线 SHA-256:`16C14FB2751A0EAB63F76371E75D0569FF8EF5B47FDD8724BE861314B6062733` - 前序结论:`HOLD/1`,唯一未闭环项为 `F1_DESIGN_RERUN_PREFLIGHT_ORDER_CONFLICT` - 本修订性质:仅补充 V001 第 4.2、5、8 节之间的执行顺序;V001 已通过的重型分类、精确选择器、manifest 秘密字段检查、规范原生 handoff 及所有范围边界继续冻结。 本修订 PASS 前,代码、示例配置、测试和 fixture 继续保持冻结;不运行网络、浏览器、下载,不接触 `F:\video` 或正式 `ana-data`。 ## 2. 冻结的 move-completed 两阶段预检顺序 `move-completed` 对整个 mapping 批次采用下列固定顺序。任何阶段失败都在复制、目标创建、源删除和 manifest 追加之前结束。 ### 2.1 阶段 A:选择器解析、实体唯一性与终态投影 1. 读取并验证完整 mapping 结构和秘密字段。 2. 对每一行解析所有已提供的 `dynamic_id`、`opus_id`、`bvid`、`source_url`;每个选择器都必须存在、唯一命中,且同一行的全部选择器必须命中同一实体。`source_url` 继续执行 V001 的 registered-host 规范化。 3. 在全批次检查 entity 唯一性;同一实体出现两次即 `E_ENTITY_DUPLICATE`。该检查不依赖 `source_file` 是否仍存在。 4. 读取该实体最新有效 manifest 状态,并将该行分类为: - `TERMINAL_MOVED`:最新状态是 `VIDEO_MOVED` 或 `COMPLETE`; - `TERMINAL_RETAINED`:最新状态是 `VIDEO_MOVED_SOURCE_RETAINED`; - `ACTIVE_MOVE`:最新状态是 `TODO_QUEUED`、`MOVE_FAILED`,或没有移动终态。 5. 终态投影直接使用 manifest 中已经持久化的可信字段,不重新要求源文件存在: - `TERMINAL_MOVED` 返回 `ALREADY_MOVED`、exit 0,并复用已记录的 `local_file` 与 `sha256`; - `TERMINAL_RETAINED` 返回 `VIDEO_MOVED_SOURCE_RETAINED`、exit 4,并复用已记录的 `local_file`、`sha256` 与 `failure_reason`;即使保留源后来被人工删除,告警也保持粘性; - 两类终态均不 stat/resolve `source_file`,不参与物理源重复检查,不创建、删除或改写文件,不追加 manifest 事件。 ### 2.2 阶段 B:仅对 ACTIVE_MOVE 做源与目标预检 只有 `ACTIVE_MOVE` 行进入以下预检: 1. `source_file` 必须存在且为允许下载根目录中的普通文件;逐级拒绝链接、junction/reparse,检查稳定年龄与扩展名。 2. 形成物理 source identity:优先 `(st_dev, st_ino)`;inode 为 0 时使用 `normcase(resolve(strict=True))`。 3. 对全部 `ACTIVE_MOVE` 行检查 source identity 唯一性;重复即 `E_SOURCE_DUPLICATE`。 4. 计算目标路径并检查全批目标唯一、既有目标不覆盖以及根目录安全。 5. 阶段 A 或阶段 B 任一错误均使本次调用正式目标新增数为 0、manifest bytes 不变、所有源文件不变。预检错误不追加 `MOVE_FAILED`;只有完整两阶段预检通过后发生的真实运行时事务失败才可按 V001 记录失败事件。 终态行可以与活动行共存于同一批次。终态行仍参与 entity 唯一性检查,但不参与 source identity/目标写入集合;活动行全部通过阶段 B 后才允许逐项进入 V001 已冻结的 copy/fsync/hash/atomic link/manifest/delete 事务。 ## 3. 精确终态矩阵 | manifest 最新状态 | 当前源文件 | 分类 | 输出状态 | exit | manifest 变化 | | --- | --- | --- | --- | ---: | --- | | `VIDEO_MOVED` / `COMPLETE` | 不存在或存在 | `TERMINAL_MOVED` | `ALREADY_MOVED` | 0 | 无 | | `VIDEO_MOVED_SOURCE_RETAINED` | 存在 | `TERMINAL_RETAINED` | `VIDEO_MOVED_SOURCE_RETAINED` | 4 | 无 | | `VIDEO_MOVED_SOURCE_RETAINED` | 后来被人工删除 | `TERMINAL_RETAINED` | `VIDEO_MOVED_SOURCE_RETAINED` | 4 | 无 | | `TODO_QUEUED` / `MOVE_FAILED` / 无终态 | 存在且通过预检 | `ACTIVE_MOVE` | 按事务结果 | 0/4/3 | 仅运行时事务可追加 | | `TODO_QUEUED` / `MOVE_FAILED` / 无终态 | 不存在或不安全 | `ACTIVE_MOVE` | 输入/安全错误 | 2/3 | 无 | 批次汇总规则保持不变:只要投影或新事务结果中包含一个 retained-source,整体为 `COMPLETE_WITH_RETAINED_SOURCE`、exit 4;否则按既有成功/失败规则汇总。 ## 4. 新增非真空入口测试合同 在 V001 第 8 节基础上必须新增以下实际 CLI 入口回归,全部使用 `TemporaryDirectory`、小型非媒体字节文件和秘密无关的净化 JSON: 1. **正常成功后源已删除重跑**:首次移动成功并确认源不存在;保存目标/hash/manifest bytes;相同 mapping 重跑,断言 exit 0、item=`ALREADY_MOVED`,目标/hash/manifest bytes 完全不变,目录无新增临时文件。 2. **retained 源仍存在重跑**:注入源删除失败得到 exit 4;相同 mapping 重跑,断言 exit 4、item=`VIDEO_MOVED_SOURCE_RETAINED`,复用原 `local_file`/`sha256`/`failure_reason`,源、目标和 manifest bytes 不变。 3. **retained 源被人工删除后重跑**:在第 2 项之后于临时目录人工删除保留源;相同 mapping 重跑,断言仍 exit 4 且复用原可信字段,目标/hash/manifest bytes 不变,无新事件。 4. **活动同源/同实体混合批次**:保持 V001 的 selector 不一致、同一物理源映射两实体、同实体两行反例;断言全批次零业务副作用。终态行不掩盖活动行的选择器错误,也不进入物理同源检查。 测试必须从 CLI/`main()` 入口进入,不得只调用内部分类函数。三种重跑均须比较调用前后 manifest 原始 bytes,而非只比较事件数量。 ## 5. 实施与复审门禁 V001 与 V002 组合为唯一实施合同。V002 限定设计 PASS 后,开发员才可一次性实现五项 HOLD 修复,并运行目标测试与 project 回归;全程不得联网、打开浏览器、触发下载、写 `F:\video` 或正式 `ana-data`。实现完成后只向原 `dev.reviewer.project` 提交一次限定实现复审。