# 慧博 10 分钟快速采集工具开发方案 V004 创建人员:`dev.developer.ana.cai` 文件职责:append-only 关闭 V003 复审仅余的公开 terminal/manifest wire 和 quota event-family/transaction 两项。 predecessor:V003=`28781/a6d46d9ffc8081a94ea76436a6201c9cf67b1f9db5d1f70692e2d294bfba6187`。 predecessor review:`AUDIT-DEV-ANA-HIBOR-FAST-COLLECTION-DESIGN-V003=HOLD/2/2`。 继承:V003 已关闭 BLOCK-02;V002 已关闭 BLOCK-03/04/06;V001/V002/V003 的 Job、deadline、candidate、atomic publish、package、performance、quota lock/read-fold/replay 和全部禁止边界继续有效。V004 对 terminal nested item、manifest literal/content type 和 quota event identity/调用顺序拥有后继优先级。 ## 1. 公开 terminal 唯一映射 ### 1.1 `CapabilityStatus` 封闭枚举 `ACCEPTED|ACCEPTED_PARTIAL_QUOTA|QUEUED_NEXT_DAY|PARTIAL_QUOTA_STOP|TIME_BUDGET_STOP|BLOCKED_INPUT|BLOCKED_ACCESS_CONTROL|BLOCKED_ENVIRONMENT|BLOCKED_AMBIGUOUS_MAPPING|VALIDATION_FAILED|INTERNAL_ERROR|STATE_UNCERTAIN` 接单阶段的 `ACCEPTED_PARTIAL_QUOTA/QUEUED_NEXT_DAY` 由采集角色在 CLI 运行前/外部安排时使用;CLI 的 `report_collection_terminal` 不反向伪造接单消息。CLI 内部 `TerminalStatus` 到公开 `capability_status` 的唯一总映射如下,不允许“最接近”、default 分支或运行时选择: | precedence | internal `TerminalStatus` | public `capability_status` | exit | |---:|---|---|---:| | 1 | `STATE_UNCERTAIN` | `STATE_UNCERTAIN` | 27 | | 2 | `BLOCKED_ACCESS_CONTROL` | `BLOCKED_ACCESS_CONTROL` | 13 | | 3 | `TIME_BUDGET_STOP` | `TIME_BUDGET_STOP` | 10 | | 4 | `PARTIAL_QUOTA_STOP` | `PARTIAL_QUOTA_STOP` | 11 | | 5 | `BLOCKED_INPUT` | `BLOCKED_INPUT` | 12 | | 6 | `BLOCKED_ENVIRONMENT` | `BLOCKED_ENVIRONMENT` | 14 | | 7 | `BLOCKED_AMBIGUOUS_MAPPING` | `BLOCKED_AMBIGUOUS_MAPPING` | 15 | | 8 | `VALIDATION_FAILED` | `VALIDATION_FAILED` | 16 | | 9 | `INTERNAL_ERROR` | `INTERNAL_ERROR` | 20 | | 10 | `PARTIAL_SUCCESS` | `ACCEPTED` | 2 | | 11 | `SUCCESS` | `ACCEPTED` | 0 | `PARTIAL_SUCCESS` 与 `SUCCESS` 虽共享公开既有值 `ACCEPTED`,消费者仍由 top-level `status` 和 exit 2/0 唯一识别;同一 internal status 永远只有一个公开值。若多个 stop 同时出现,先按 precedence 选唯一 internal status,再按本表映射一次。 ### 1.2 `TerminalItem V004`(固定 26 键) `item_id,slot_id,candidate_id,report_identity,state,status,stop_code,trigger_attempted,triggered,quota_state,quota_reservation_id,quota_terminal_event_id,quota_artifact_event_id,title,institution,report_date,analysts,page_count,bytes,sha256,source_cache_path,source_file_name,final_path,manifest_row_id,reused_without_new_trigger,error_or_note` 类型:前 13 个 identity/state 字段为 `str|None`(`trigger_attempted/triggered/reused_without_new_trigger` 为 BOOL);`analysts=tuple[str,...>|null`;`page_count/bytes=int|null`;`sha256=Hash|null`;路径为 `str|null`。`status=SUCCESS|DUPLICATE|FAILED|STOPPED`;`state=P00_INIT..P10_CLOSED`;`quota_state=NONE|RESERVED|CONFIRMED|UNCERTAIN|RELEASED`。 变体约束: 1. SUCCESS:candidate/report/title/institution/date/analysts/page/bytes/hash/cache/final/manifest 全非空;triggered=true(或 resume 时 reused=true、triggered=false);quota terminal/artifact event 非空;stop_code=null;error=`NONE`。 2. DUPLICATE:report identity、bytes/hash/final/manifest 非空;`quota_state=RELEASED|CONFIRMED` 取决于是否已触发,必须有对应 terminal event;stop_code=`DUPLICATE_EXISTING_ARTIFACT`,error 说明已有证据。V004 将 `DUPLICATE_EXISTING_ARTIFACT` 追加到 V003 `ErrorCode` 封闭枚举。 3. FAILED:只记录实际可知值;status/stop_code/error 非空,不得填造 title/PDF/hash。若 trigger 已 confirmed/uncertain,quota terminal 与 artifact failure event 非空。 4. STOPPED:由高优先级 terminal 导致,未进入步骤的字段 null;`state` 和 trigger/quota 交叉字段必须反映实际 package row。 `TerminalRecord.items` 的类型固定为 `tuple[TerminalItem,...]`,按 item_id ordinal 排序;不得“另含”未声明字段或按成功/失败追加额外键。公开 PDF 明细完全由 TerminalItem 固定字段承载。 ## 2. Manifest literal 与 evidence-based content type V003 48 列顺序不变,仅冻结三个值域: - `source_site` 固定精确字面量:`慧博 APP(安卓模拟器本地缓存)`。 - `http_status`:APP/cache 来源固定 `NOT_APPLICABLE_APP_CACHE`;若尚未证明来源是该 cache,填 `UNKNOWN`。 - `content_type` 只按已取得证据赋值:`pdf_magic_valid=true` 时为 `application/pdf`;存在普通本地文件且 magic 已验证 false 时为 `application/octet-stream`;文件未出现、不可读、magic 未执行或事实未知时为 `UNKNOWN`。失败/STOP 行不得无条件声称 PDF。 `download_status` 的固定值为 `SUCCESS|DUPLICATE|FAILED|STOPPED`;`status` 必须相同。无正式副本时 base `file_name,relative_path,bytes,sha256` 为空字段,`error_or_note` 非空;规范允许未知的 title/publisher/report_date/source_url 使用 `UNKNOWN`,不得用 null 破坏 CSV 列宽。 ## 3. Quota event-family identity V004 ### 3.1 `QuotaEvent CSV V004`(固定 34 列) V003 33 列在公开最小字段 `note` 后新增 `event_family`,其余列顺序/语义不变;前 20 列继续与上游完全一致: `quota_date,timezone,platform_limit,automation_target,automation_hard_stop,reserved_buffer,task_id,requester_role,handoff_id,event_at,report_identity,event_type,confirmed_consumed,uncertain_consumed,active_reservation_delta,success_unique_pdf_delta,duplicate_or_failed_delta,cumulative_consumed,safe_available_after,note` 后 14 列为: `event_family,schema_version,event_id,idempotency_key,event_seq,run_id,slot_id,reservation_id,ref_event_id,evidence_ref,external_baseline_floor,confirmed_delta,uncertain_delta,app_total_consumed` `event_family=BASELINE|APP_OBSERVATION|RESERVATION|QUOTA_TERMINAL|ARTIFACT_TERMINAL`。每个事件类型到 family 唯一: - BASELINE_ESTIMATE/CORRECTION_RAISE→BASELINE; - APP_RECONCILE→APP_OBSERVATION; - RESERVE→RESERVATION; - CONSUME_CONFIRMED/CONSUME_UNCERTAIN/RELEASE→QUOTA_TERMINAL; - ARTIFACT_SUCCESS/ARTIFACT_DUPLICATE_OR_FAILED→ARTIFACT_TERMINAL。 ### 3.2 family-scoped idempotency preimage 所有 preimage 用 UTF-8、`|` 分隔、字段不得含 `|`;`idempotency_key=sha256(preimage)`,`event_id=sha256('HIBOR-QUOTA-EVENT-V004|'+idempotency_key+'|'+event_type)`。event_seq 只用于 ledger ordinal,不进入 event_id;replay 先按 family+idempotency_key 查找,再直接返回既有整行,绝不以新 event_at 构造冲突行。 | family | idempotency preimage | |---|---| | BASELINE estimate | `quota_date|BASELINE|BASELINE_ESTIMATE|evidence_ref|external_baseline_floor` | | BASELINE correction | `quota_date|BASELINE|CORRECTION_RAISE|ref_event_id_or_NONE|evidence_ref|external_baseline_floor` | | APP_OBSERVATION | `quota_date|APP_OBSERVATION|observation_id` | | RESERVATION | `quota_date|RESERVATION|task_id|handoff_id|slot_id|report_identity` | | QUOTA_TERMINAL | `quota_date|QUOTA_TERMINAL|reservation_id` | | ARTIFACT_TERMINAL | `quota_date|ARTIFACT_TERMINAL|task_id|handoff_id|run_id|slot_id|report_identity` | `observation_id=sha256(device_serial|quota_date|captured_at_utc|visible_remaining|ui_snapshot_fingerprint)`,这些字段来自一次已完成只读 UI snapshot;APP row 的 `report_identity='__APP_TOTAL__'`、slot_id 为空、evidence_ref=`APP-OBS:`。同 reservation 发生 confirm/uncertain/release 竞争时共享 QUOTA_TERMINAL key,首个合法 committed row 胜出;后续同整行 replay,其他 event_type 为冲突 STOP。artifact 的 run_id 允许失败后在新 run 做 postprocess,但 reducer 的 success_unique 以 report_identity 去重。 `reservation_id` 固定等于 RESERVE 行的 `event_id`;QUOTA_TERMINAL.ref_event_id 必须等于该 reservation_id;ARTIFACT_TERMINAL.ref_event_id 必须等于该 report 的 QUOTA_TERMINAL event_id。不同 family 的同 report 事件因此既可串联又不会共享 idempotency key。 ### 3.3 Quota API 后继签名 V004 替换 V003 中带 `app_visible_remaining` 的 quota API: ```python def snapshot(ctx: RunContext) -> QuotaSnapshot def observe_app_remaining(ctx: RunContext, observation: AppQuotaObservation) -> QuotaEvent def reserve(ctx: RunContext, report_identity: str, slot_id: str, observation_event_id: str | None) -> ReservationResult def confirm(ctx: RunContext, reservation: ReservationResult, trigger: TriggerReceipt) -> QuotaEvent def mark_uncertain(ctx: RunContext, reservation: ReservationResult, trigger: TriggerReceipt, reason: ErrorCode) -> QuotaEvent def release(ctx: RunContext, reservation: ReservationResult, reason: ErrorCode) -> QuotaEvent ``` `AppQuotaObservation` 固定 6 键:`observation_id,device_serial,captured_at_utc,visible_remaining,app_total_consumed,ui_snapshot_fingerprint`;visible 0..30,app_total=30-visible,observation_id 必须按上式复算。 ## 4. APP observation→reserve:只用独立已提交事务 每个 quota API 调用在锁内最多追加一行;V004 明确选择“先独立提交 reconcile,再独立 reserve”,禁止一锁两行和隐式多行事务: 1. UI 未取得可靠剩余量:不调用 observe,直接 `reserve(...,observation_event_id=None)`,只按已有 ledger 保守 fold。 2. UI 取得可靠剩余量:调用 `observe_app_remaining`,在一个临界区完成 read→validate→fold→append APP_RECONCILE→flush/fsync→reopen/fold;返回已 committed event。 3. CLI 只有在步骤 2 返回 committed/replayed event_id 后,才调用 reserve 并传该 id。reserve 在新临界区重读全 ledger,验证 observation event 存在、family/日期/device evidence 正确,fold 后再 check safe/append 一行。 4. crash 在 reconcile 前:无行、无 reservation;crash 在 reconcile 后/reserve 前:只有更保守 APP 总量,无 active reservation,安全可重跑;crash 在 reserve append 前:无 reservation;append 后:active reservation 保守占用。不存在“第一行未 durable 却写第二行”。 5. 任一 append/fsync/reopen mismatch 返回 STATE_UNCERTAIN/27;不做下一 API、不 rollback/删行。已 durable reconcile 可重放;已 durable reservation 由后续同 reservation terminal 收口。 并发时两个进程可先各提交不同 observation;reducer 取历史 APP max。随后 reserve 各自重新 fold,最后 safe slot 仍只有一个成功。reserve 引用较早但同日合法 observation 仍使用全 ledger 最新更保守 max,不使用该行的旧余额。 ## 5. 强制测试与当前 gate 新增测试: 1. 11 个 TerminalStatus 全映射、所有 precedence pair、未知 enum 拒绝、SUCCESS/PARTIAL 同公开值但 top-level 可区分; 2. TerminalItem SUCCESS/DUPLICATE/FAILED/STOPPED 的 26 键、null/enum/交叉字段与 extra-key 拒绝; 3. manifest 精确 source_site、PDF/octet-stream/UNKNOWN 三种 content type、失败/unknown 行不制造 PDF 断言; 4. 五个 event family 的 preimage/event_id/replay/conflict;同 report reservation/quota-terminal/artifact-terminal 三个 key 必须互不相同;APP observation 无 slot/report 仍可唯一重放; 5. reconcile 前后 crash、reconcile committed 后 reserve、reserve 前后 crash、两进程最后 safe slot、同 reservation confirm/uncertain race、artifact fail→新 run success、同 run replay; 6. V003 七个 reducer 向量保持,且每个公开 quota snapshot 字段与 34 列行一致。 当前状态:`PENDING_INDEPENDENT_DESIGN_REREVIEW`。V004 PASS 前仍不创建源码/测试/额度账本,不运行 Python/ADB/APP/dry-run,不触发下载;新增 trigger=`0`。