# B 站动态主动刷新采集器 V003 双阻断修订 创建人员:`dev.developer.project.secondary / infodev-2` 文件职责:仅关闭 V002 focused 复审剩余的 `F1_R1_OBSERVATION_PARSED_COUNT_DOUBLE_COUNTS_UNPARSED_NODES` 与 `F2_F4_R1_STALE_FORMAL_LOCK_RECOVERY_SELF_DEADLOCK`。V001+V002 其余合同已通过并冻结;冲突处以 V003 为准。 基线:V001=`24165/DA597448EFD47361779464B44BF92C8CDE9B4AC1769764EE0AF2301075C86045`;V002=`22460/EC7E1D887FF930D7DA75F208975E17812E9D299606CD2A79B3E9BF14DE723A54`;HOLD/2 回执=`msg_20260813130421656_1b883df3`。 ## 1. F1-R1:互斥 observation node schema V002 第 2.2 节的 `parse_complete`、`parsed_card_count` 与 `parser_error_count` 表述由本节完整替代。 每个 observation exact keys 修订为: ```text ordinal,observed_at,cursor_before,cursor_after, visible_node_count,complete_card_count,unparsed_node_count, cards,unparsed_nodes,limit_hit ``` ### 1.1 完整卡与未解析节点互斥 - `cards` **只允许完整卡**,每项 exact keys=`position,identifiers,stable_keys,published_at,content_type,source_url`;不再存在 `parse_complete` 字段。 - `identifiers` exact keys=`dynamic_id,opus_id,bvid`,缺项为 JSON null;CLI 从 identifiers+source_url 重算 stable_keys,至少一个。 - `unparsed_nodes` 只保存无法形成完整卡的可见节点,每项 exact keys=`position,node_fingerprint_sha256,reason_code`。 - `reason_code` exact enum=`IDENTITY_MISSING|IDENTITY_CONFLICT|PUBLISHED_AT_MISSING|PUBLISHED_AT_INVALID|CONTENT_TYPE_UNKNOWN|SOURCE_URL_INVALID|NODE_TRUNCATED|PARSER_REJECTED`;不含 raw DOM/text/error。 - `node_fingerprint_sha256` 是 extractor 对该节点的 bounded sanitized structure(标签名、固定 allowlist 属性名、可见文本长度,不含文本值、URL query、HTML、header或secret)按 contract canonical 算法所得 64 lowercase hex。CLI不能从hash还原内容,但可验证格式、唯一性和 observation canonical hash;extractor/parser身份由已审核hash约束。 互斥算术: ```text complete_card_count = len(cards) unparsed_node_count = len(unparsed_nodes) visible_node_count = complete_card_count + unparsed_node_count ``` cards.position 与 unparsed_nodes.position 各自唯一,二者不相交,并集必须精确等于整数区间 `[0, visible_node_count)`。因此一个 DOM node 恰好进入一个集合,不会双计,也不能从 proof 消失。`visible_node_count<=50`;达到50即 `CARD_PER_OBSERVATION_LIMIT`,禁止 no-new。 同 observation 的 stable-key component 与 unparsed fingerprint 均不得重复;跨 observation 重复允许用于检测 no-progress,但不增加 unique count。全局 `unique_card_count` 只数完整 card components;另记录 `unique_unparsed_node_count`。任意 `unparsed_node_count>0` 或 `unique_unparsed_node_count>0` 均使 `parse_complete=false`、coverage incomplete。 ### 1.2 proof 修订 `coverage_proof` counts exact keys: ```text observation_count,total_visible_node_count,total_complete_card_count, total_unparsed_node_count,unique_card_count,unique_unparsed_node_count ``` 还必须含 ordered observation hashes、ordered complete component hashes 和 ordered unparsed fingerprint hashes。CLI 从 observation 重算所有 counts/hash;输入 evidence 不携带 derived totals。任一 position 缺口/重叠、count drift、完整 card字段缺失、unparsed reason/hash异常均 `E_EVIDENCE_SCHEMA`,且 no-new token=0。 ### 1.3 聚焦反例 测试至少覆盖:一个 incomplete node 只进入 unparsed;同node同时出现在 cards/unparsed;position重叠/缺口;count大/小漂移;未知reason;fingerprint重复;完整card缺字段;unparsed节点从两集合都消失;49 complete+1 unparsed命中50边界。全部从 public commit入口验证正式 manifests不变且不能 no-new。 ## 2. F2/F4-R1:同 run stale formal-lock 安全接管 V002 “stale lock在存在 pending transaction 时不得接管”的绝对禁止被本节替代。只允许下面同 run、同 owner identity、owner 已证明死亡的恢复接管;其他情况仍 safety stop。 ### 2.1 formal lock schema 与 pending 预绑定 formal lock canonical JSON schema 1 exact keys: ```text schema_version,task_id,run_id,owner_nonce,transaction_id, recovery_generation,holder_pid,holder_process_created_at, lock_created_at ``` - `holder_process_created_at` 必须来自 OS 进程 creation time,不是当前wall clock猜测;PID和creation time组成 holder identity。 - `recovery_generation` 初始0,只能每次成功接管加1。 - pending schema2 的 `transaction_identity` 新增 `formal_lock_claim={claim_state,lock_path,lock_bytes,lock_sha256,lock_record}` 和 `takeover`。 - `claim_state=PLANNED|HELD`。在创建 formal lock **之前**,持 StateLock 先把待创建的 exact lock_record/bytes/hash原子写进 pending 为 PLANNED;随后 CreateNew exact lock并回读,再推进 HELD。这样崩溃后的锁永远可与 pending 预绑定。 - PLANNED且主lock不存在:相同仍活 holder返回 E_BUSY;holder PID不存在时走同一 takeover状态机创建新generation,不能假装旧claim已持有。HELD但主lock不存在属于外部删除/歧义,除非 takeover phase 和 quarantine实物能按第2.3节唯一恢复,否则 safety stop。 ### 2.2 owner-dead 判定 为使真实 kill 后恢复可达,V001/V002 所称 existing `StateLock` 必须从“仅靠 CreateNew 文件存在”升级为**稳定 lock file 的内核 advisory exclusive lock**: - `state_dir/.collector.lock` 是预创建/复用的普通非 reparse 稳定文件,内容固定为 1 byte `0x00`;Windows 使用标准库 `msvcrt.locking` 锁定该 1-byte region,测试平台采用等价且进程死亡自动释放的内核锁;不得在持锁时 replace/unlink该文件; - 取得内核锁后,才原子写独立 `state_dir/.collector.lock.owner.json`(pid、process creation time、run_id、acquired_at);退出时删除 exact owner metadata并释放内核锁,稳定 lock file保留; - 进程崩溃时OS自动释放内核锁;下一个进程成功取得后可把已死亡且 exact schema 的旧 owner metadata原子隔离到 owned recovery path,再写自己的 metadata。活owner、PID reuse、metadata第三内容/reparse/查询失败均 safety stop; - 旧 `check/move-completed/handoff` 全部使用同一实现,公开 `E_STATE_LOCKED` 语义不变。不得通过删除 `.collector.lock` 或活 owner metadata“接管”。 测试必须包含活进程互斥、kill 后OS锁释放与旧metadata隔离/恢复,避免 formal lock修好却被原 StateLock 文件永久阻断。 恢复进程必须先独占上述 StateLock,再按顺序验证: 1. pending为同 task/run/owner_nonce/transaction_id,phase为 `TRANSACTION_INTENT|BUSINESS_COMMITTED` 或正在进行的 takeover; 2. formal lock path为配置机械派生路径,lock/pending均普通非reparse,父链无reparse; 3. 主lock exact bytes/hash与 pending claim完全相等; 4. lock holder PID 的 OS 状态: - PID存在且creation time与record相同:owner alive,`E_FORMAL_LOCK_BUSY`; - PID存在但creation time不同:PID reuse,`E_FORMAL_LOCK_PID_REUSE`,不得接管; - PID不存在且OS查询成功:唯一 `OWNER_DEAD_PROVEN`,允许接管; - access denied、查询失败、creation time不可得:`E_FORMAL_LOCK_OWNER_UNPROVEN`,不得接管; 5. pending claim的 lock bytes/hash、run/nonce/transaction/generation 任一不符,主lock第三内容、异run、未来time均 `E_RECOVERY_AMBIGUOUS`。 不能只凭lock年龄、deadline或PID数字不存在缓存结果接管;owner-dead检查必须紧邻takeover,并在同一 StateLock临界区内完成。 ### 2.3 crash-safe takeover 状态机 takeover exact phases: ```text NONE TAKEOVER_PLANNED OLD_LOCK_QUARANTINED NEW_LOCK_HELD ``` owned quarantine path机械派生: ```text state_dir/refresh/lock-recovery/-g-.json ``` 接管顺序(全程持 StateLock): 1. 证明 owner dead;构造 current recovery process identity 和 generation+1 new lock record;冻结 old claim、quarantine path、new claim;pending原子推进 `TAKEOVER_PLANNED`。 2. 再次复核主lock仍等于 old claim;用 no-overwrite atomic rename 把它移到 exact quarantine;fsync目录、回读 bytes/hash;pending推进 `OLD_LOCK_QUARANTINED`。 3. CreateNew 主lock为 new claim;回读 bytes/hash;pending 的 formal_lock_claim替换为 new HELD claim,takeover推进 `NEW_LOCK_HELD`。 4. 执行 V002 双manifest recovery/commit。terminal slot与latest成功后,只有 quarantine仍精确且无任何pending引用时才删除;cleanup失败记 `W_STALE_LOCK_QUARANTINE`,不得回滚业务终态。 StateLock保证步骤2主lock短暂缺失时其他本工具writer不能进入;所有writer固定先取StateLock,禁止只取formal lock。双recovery进程只有StateLock赢家能接管,输家 E_STATE_LOCKED;不得同时rename/create。 ### 2.4 takeover reopen - `TAKEOVER_PLANNED`:主lock=old且quarantine不存在时继续步骤2;主lock缺失且quarantine=old时推进 `OLD_LOCK_QUARANTINED`;其他组合歧义停止。 - `OLD_LOCK_QUARANTINED`:quarantine必须=old,主lock必须不存在;重新证明记录中的旧owner仍dead,再为当前恢复进程生成新的 generation+1 claim并CreateNew;主lock已存在只能在它精确等于 pending已冻结new claim且holder identity可核时推进,否则停止。 - `NEW_LOCK_HELD`:主lock必须精确等于 new claim;若holder是当前进程继续恢复;若holder alive同identity返回busy;若该恢复进程又崩溃,则允许递归执行generation+1 takeover,旧quarantine链逐个保留至terminal cleanup。 - pending不存在而留下formal lock或quarantine:本任务不自动接管/删除,`E_RECOVERY_AMBIGUOUS`。 任何隔离/创建/回读/pending replace失败都保留当前phase与实物,下次只按矩阵继续;不以unlink主lock作为接管步骤,不覆盖quarantine或新lock。 ### 2.5 测试 真实子进程在 PLANNED前后、lock CreateNew后、HELD后、每个transaction phase、TAKEOVER_PLANNED、rename后、OLD_LOCK_QUARANTINED、new CreateNew后、NEW_LOCK_HELD逐点kill。覆盖 alive owner、PID reuse模拟、OS query denied、异run/nonce/transaction、lock bytes/hash第三内容、reparse、双recovery竞争、quarantine已存在不同hash、generation递增和terminal cleanup失败。断言不会永久自闭锁、不会删除活owner锁、不会覆盖第三内容、不会重复formal append。 ## 3. 冻结项与当前状态 V002 F3 mixed-history `22/16/5/1`、F5 UTC-hour unique run/168 slots/durable terminal/latest recovery已关闭,不重开。source-controlled extractor、pending WAL、exact candidates、五终态和V001 preserved contracts继续有效。 状态=`V003_TWO_BLOCKER_CLOSURE_PENDING_ORIGINAL_REVIEWER_IMPLEMENTATION_FROZEN`。 产品 code/test/fixture/formal modification=0;新增Chrome refresh/network retry/download/extension/policy/HKCU/session/formal write=0。V001+V002+V003组合方案只有原reviewer PASS/0后方可实施。