创建人员: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。
V002 第 2.2 节的 parse_complete、parsed_card_count 与 parser_error_count 表述由本节完整替代。
每个 observation exact keys 修订为:
ordinal,observed_at,cursor_before,cursor_after,
visible_node_count,complete_card_count,unparsed_node_count,
cards,unparsed_nodes,limit_hit
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约束。互斥算术:
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。
coverage_proof counts exact keys:
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。
测试至少覆盖:一个 incomplete node 只进入 unparsed;同node同时出现在 cards/unparsed;position重叠/缺口;count大/小漂移;未知reason;fingerprint重复;完整card缺字段;unparsed节点从两集合都消失;49 complete+1 unparsed命中50边界。全部从 public commit入口验证正式 manifests不变且不能 no-new。
V002 “stale lock在存在 pending transaction 时不得接管”的绝对禁止被本节替代。只允许下面同 run、同 owner identity、owner 已证明死亡的恢复接管;其他情况仍 safety stop。
formal lock canonical JSON schema 1 exact keys:
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。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 预绑定。为使真实 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保留;check/move-completed/handoff 全部使用同一实现,公开 E_STATE_LOCKED 语义不变。不得通过删除 .collector.lock 或活 owner metadata“接管”。测试必须包含活进程互斥、kill 后OS锁释放与旧metadata隔离/恢复,避免 formal lock修好却被原 StateLock 文件永久阻断。
恢复进程必须先独占上述 StateLock,再按顺序验证:
TRANSACTION_INTENT|BUSINESS_COMMITTED 或正在进行的 takeover;E_FORMAL_LOCK_BUSY;E_FORMAL_LOCK_PID_REUSE,不得接管;OWNER_DEAD_PROVEN,允许接管;E_FORMAL_LOCK_OWNER_UNPROVEN,不得接管;E_RECOVERY_AMBIGUOUS。不能只凭lock年龄、deadline或PID数字不存在缓存结果接管;owner-dead检查必须紧邻takeover,并在同一 StateLock临界区内完成。
takeover exact phases:
NONE
TAKEOVER_PLANNED
OLD_LOCK_QUARANTINED
NEW_LOCK_HELD
owned quarantine path机械派生:
state_dir/refresh/lock-recovery/<run_id>-g<old_generation>-<old_lock_sha256>.json
接管顺序(全程持 StateLock):
TAKEOVER_PLANNED。OLD_LOCK_QUARANTINED。NEW_LOCK_HELD。W_STALE_LOCK_QUARANTINE,不得回滚业务终态。StateLock保证步骤2主lock短暂缺失时其他本工具writer不能进入;所有writer固定先取StateLock,禁止只取formal lock。双recovery进程只有StateLock赢家能接管,输家 E_STATE_LOCKED;不得同时rename/create。
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。E_RECOVERY_AMBIGUOUS。任何隔离/创建/回读/pending replace失败都保留当前phase与实物,下次只按矩阵继续;不以unlink主lock作为接管步骤,不覆盖quarantine或新lock。
真实子进程在 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。
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后方可实施。