# 慧博 10 分钟快速采集工具开发方案 V002 创建人员:`dev.developer.ana.cai` 文件职责:append-only 修复 V001 独立审核的 6 个 blocking findings,并冻结可直接实现、测试和复核的模块 API、状态模型、时间边界、原子归档、额度并发与性能样本合同。 predecessor:`CODE-DESIGN-ANA-HIBOR-FAST-COLLECTION-V001`=`15549/ecacaaea740454ae56e928ef58af4a5d5e706334b1ac917c8647db8a8eb3edf9`。 predecessor review:`AUDIT-DEV-ANA-HIBOR-FAST-COLLECTION-DESIGN-V001=HOLD/6/6`,审计入口 `ana-doc/案例审计报告.md`。 记录方式:V001 保持不可变;V002 对冲突处拥有后继优先级,V001 已通过的包名、缓存、访问控制、原件、额度上限、角色和零实现关口全部继续有效。普通源码/内部测试按语义验收,PDF、正式 manifest、额度账本和终态证据按真实消费者要求做哈希与不可变验证,不恢复普遍逐字节预冻结。 ## 1. 本轮 closure 与不变边界 V002 必须同时关闭: 1. `BLOCK-HIBOR-DESIGN-01`:逐模块 API、调用图、canonical model/schema、主键、错误与 exit 总映射; 2. `BLOCK-HIBOR-DESIGN-02`:任务观察时点传递、单一 monotonic deadline、子进程/锁/I/O/terminal 的有界生命周期; 3. `BLOCK-HIBOR-DESIGN-03`:`PROVISIONAL_VISIBLE` 与 `DETAIL_CONFIRMED` 分离,早停失效后的 cursor 恢复与补扫; 4. `BLOCK-HIBOR-DESIGN-04`:同目录 staging、完整验证后原子 no-replace 发布、崩溃恢复与 package closure; 5. `BLOCK-HIBOR-DESIGN-05`:额度 read-fold-reserve 原子临界区、事件主键/幂等、并发和 replay; 6. `BLOCK-HIBOR-DESIGN-06`:10 个 trigger 的预登记总体、批量计数、SLA failure 与 external exclusion 口径。 方案复审 PASS 前仍禁止创建候选源码/测试、运行 Python/ADB/APP/dry-run、创建额度账本或产生真实下载 trigger。 ## 2. 唯一 owner、模块 API 与调用图 ### 2.1 唯一 owner `cli.main()` 是唯一 orchestration owner;其余模块不得互相创建 run、写 terminal、追加额度或自行启动外部命令。`RunContext` 由 CLI 创建一次并只读传递。外部命令全部通过 `ProcessSupervisor.run(argv: Sequence[str], ...)`,必须是 argv list、`shell=False`;模块不得调用 `os.system`、shell 字符串、隐式 PATH 脚本或自行 `subprocess.*`。 ### 2.2 公共 API 下列为包内唯一 public API;未列函数/类必须以下划线开头,测试不得依赖其实现细节。 | 模块 | 唯一公共 API | 返回/异常 | owner | |---|---|---|---| | `models.py` | `load_task_spec(path: Path) -> TaskSpec`;`validate_task_spec(x: Mapping) -> TaskSpec`;`to_ordered_dict(model) -> dict` | 仅抛 `ContractError(code, field, detail)` | CLI | | `budget.py` | `Budget.start(observed_at_utc: str, total_ms: int, close_reserve_ms: int, clock: Clock) -> Budget`;`remaining_ms(kind: Literal['work','close']) -> int`;`checkpoint(phase: Phase, operation: str, kind='work') -> None`;`slice(max_ms: int, teardown_ms: int) -> int` | 到期抛 `BudgetExpired`; wall-clock 回退/未来时间/启动前已耗尽抛 `ContractError` | CLI | | `process.py` | `ProcessSupervisor.run(argv: Sequence[str], cwd: Path, budget: Budget, phase: Phase, input_bytes: bytes|None, max_stdout: int, max_stderr: int) -> ProcessResult` | 不抛裸异常;所有可观测结果封装;liveness unknown 置 `LIVENESS_UNKNOWN` | ADB/PDF provider only | | `adb.py` | `AdbClient.preflight(ctx) -> DeviceSnapshot`;`list_cache(ctx) -> tuple[RemoteFile,...]`;`remote_sha256(ctx,path)->str`;`pull(ctx,remote,local_staging)->PullResult`;`ui_dump(ctx)->UiSnapshot`;`screenshot(ctx,dst_staging)->ArtifactDraft`;`tap/swipe/input_text/back/start_package(ctx,...) -> ActionReceipt` | 只经 ProcessSupervisor;包/设备/cache 身份漂移为 typed stop | UI/cache | | `ui.py` | `FastScanner.scan(ctx, checkpoint: ScanCheckpoint|None) -> ScanOutcome`;`confirm_detail(ctx,candidate)->Candidate`;`restore(ctx,checkpoint)->ScanCheckpoint`;`trigger(ctx,candidate,reservation)->TriggerReceipt` | 不写 quota/PDF/terminal;每次动作前后验证 anchor/package | CLI | | `quota.py` | `QuotaLedger.snapshot(ctx)->QuotaSnapshot`;`reserve(ctx, report_key, slot_id)->ReservationResult`;`confirm(ctx,reservation,trigger)->QuotaEvent`;`mark_uncertain(...)`;`release(...)`;`raise_baseline(ctx,floor,evidence_ref)->QuotaEvent` | 每次 API 内部独占一次完整 read-validate-fold-check-append-fsync-reopen 临界区 | CLI | | `cache.py` | `CacheWatcher.capture_baseline(ctx)->CacheBaseline`;`wait_unique_stable(ctx,baseline,trigger)->CacheMatch` | 只读远端;歧义/超时返回 typed stop | CLI | | `archive.py` | `ArchiveManager.stage_pull(ctx,match)->ArtifactDraft`;`validate(ctx,draft,detail)->ValidatedArtifact`;`publish_no_replace(ctx,validated,destination)->PublishedArtifact`;`recover(ctx,recovery_record)->RecoveryResult` | 不写 manifest/quota/terminal;正式路径只通过原子 publish 出现 | CLI | | `manifests.py` | `append_manifest(ctx,row)->ManifestReceipt`;`write_delivery(ctx,package)->PublishedArtifact`;`write_timing(ctx,rows)->PublishedArtifact` | 同目录 staging+验证+no-replace;manifest 单锁 append 并 fsync | CLI | | `terminal.py` | `build_terminal(ctx,package,stop)->TerminalRecord`;`persist_terminal(ctx,record)->TerminalReceipt`;`render_public_payload(record)->str` | 不发送消息;console payload 永远可生成,持久化失败如实记录 | CLI | | `cli.py` | `main(argv: Sequence[str]|None=None)->int` | 捕获所有 typed/untyped 错误,按第 5 节唯一映射返回 | public | ### 2.3 固定调用图 ```text cli.main -> models.load_task_spec -> Budget.start -> AdbClient.preflight + QuotaLedger.snapshot -> FastScanner.scan -> AdbClient.ui_dump/screenshot/action -> FastScanner.confirm_detail -> CacheWatcher.capture_baseline -> QuotaLedger.reserve -> FastScanner.trigger -> QuotaLedger.confirm|mark_uncertain|release -> CacheWatcher.wait_unique_stable -> ArchiveManager.stage_pull -> validate -> publish_no_replace -> append_manifest -> write_delivery -> write_timing -> build_terminal -> persist_terminal -> render_public_payload ``` `collect-batch` 仅在 CLI 层循环 item;每个 item 使用独立 `report_key`、reservation、artifact 和 manifest row,共享 search/scan checkpoint。任何模块不得反向调用 CLI 或 terminal。 ## 3. Canonical model 与 schema ### 3.1 基本类型与序列化 - `Id`:非空 ASCII `[A-Za-z0-9._:-]{1,160}`;`PathText`:绝对 Windows 路径字符串;`RemotePath`:以固定 cache root 开头的 POSIX 路径。 - `UtcTime`:UTC RFC3339,毫秒 3 位,后缀 `Z`;`Hash`:小写 64 位十六进制;`Bytes`/`Millis`:非负 JSON integer;BOOL 只允许 JSON true/false。 - JSON 使用 UTF-8、无 BOM;模型输出按本文字段顺序,null 必须显式出现,不允许额外键。CSV 列顺序固定,null 编码为空字段,BOOL 编码 `true|false`。 - canonical acceptance 是“严格 parse + 字段/类型/枚举/交叉约束 + 重序列化语义一致”;只有 quota/正式 manifest/terminal 发布后记录实际 bytes/hash,不预冻结未来运行字节。 ### 3.2 `TaskSpec V002`(有序 27 键) `schema_version,task_id,handoff_id,requester,review_owner,mode,query,quantity,destination_root,output_root,quota_ledger,adb_executable,pdfinfo_executable,package_name,cache_root,device_serial,observed_at_utc,total_budget_ms,close_reserve_ms,batch_increment_budget_ms,min_screens,normal_max_screens,hard_max_screens,hard_max_candidates,selection_filters,performance_slot_id,performance_plan_id` - `schema_version='HIBOR_FAST_TASK_SPEC_V002'`;`mode=DRY_RUN|COLLECT_ONE|COLLECT_BATCH|RESUME_POSTPROCESS`。 - `package_name='cn.com.hibor'`;`cache_root='/sdcard/Android/data/cn.com.hibor/files/myfile/'`。 - `quantity` INT 1..10;`collect-one` 的 `total_budget_ms=600000`;`collect-batch` 的 `total_budget_ms=600000+240000*(quantity-1)`;`close_reserve_ms=10000`;`batch_increment_budget_ms=240000`;screens=`3/5/20`;candidates=`100`。 - `device_serial` STRING|null;dry-run 必须 null,真实模式必须非空并与唯一设备一致。 - `performance_slot_id/performance_plan_id` 均 STRING|null;真实性能运行必须非空,普通生产采集均为 null。 - `selection_filters` 为有序对象:`subject:string,allowed_institutions:array|null,report_types:array,date_from:YYYY-MM-DD,date_to:YYYY-MM-DD,min_pages:int,max_pages:int,analyst_required:bool,max_per_institution:int`;不得含评分之外的主观字段。 ### 3.3 `Candidate V002`(有序 22 键) `candidate_id,report_key,state,query,screen_index,rank_on_screen,result_title,result_institution,result_date,result_pages,result_analysts,detail_title,detail_institution,detail_date,detail_pages,detail_analysts,detail_fingerprint,hard_filter_pass,score,duplicate_of,reject_code,checkpoint_id` - `state=DISCOVERED|PROVISIONAL_VISIBLE|DETAIL_OPENED|DETAIL_CONFIRMED|REJECTED|TRIGGER_RESERVED|TRIGGERED|CACHE_MATCHED|ARCHIVED|FAILED`。 - result 字段在 `PROVISIONAL_VISIBLE` 后非空;detail 字段在 `DETAIL_CONFIRMED` 后非空;此前为 null。 - `hard_filter_pass` 为 BOOL|null,仅 `DETAIL_CONFIRMED/REJECTED` 可非空;`score` 为 INT|null,仅 result 字段完整时可非空。 - `duplicate_of/reject_code` 为 STRING|null;被拒/重复时必须提供其一。稳定 top-K 只能使用 `PROVISIONAL_VISIBLE` 的 provisional ranking 作为“暂停扫描”依据,最终 trigger 集只允许 `DETAIL_CONFIRMED && hard_filter_pass=true && duplicate_of=null`。 ### 3.4 `ScanCheckpoint V002`(有序 10 键) `checkpoint_id,query,screen_index,scroll_count,page_fingerprint,first_visible_key,last_visible_key,seen_candidate_ids,provisional_top_ids,created_at_utc` 数组顺序为实际扫描顺序;`page_fingerprint` 是 UI dump 规范化可见节点的 SHA-256,不是截图内容推断。restore 后 query、screen/scroll、first/last key 和 fingerprint 必须全部一致,否则 `UI_CURSOR_RESTORE_FAILED`。 ### 3.5 `QuotaEvent CSV V002`(固定 18 列) `schema_version,event_id,idempotency_key,ledger_date,event_seq,event_type,task_id,handoff_id,run_id,slot_id,report_key,reservation_id,ref_event_id,confirmed_delta,uncertain_delta,reservation_delta,baseline_floor,created_at_utc` - `event_id=sha256(ledger_date+'|'+idempotency_key+'|'+event_type+'|'+event_seq)`;`idempotency_key=sha256(task_id+'|'+handoff_id+'|'+slot_id+'|'+report_key)`。 - `event_type=BASELINE_ESTIMATE|CORRECTION_RAISE|RESERVE|CONSUME_CONFIRMED|CONSUME_UNCERTAIN|RELEASE`。 - baseline/correction:三个 delta 为 0,`baseline_floor` 为 INT;baseline 首次至少 3,correction 只能提高 floor,`ref_event_id` 指向证据事件。 - reserve:`0/0/+1`;confirmed:`+1/0/-1`;uncertain:`0/+1/-1`;release:`0/0/-1`;其余组合非法。 - 一个 idempotency key 最多一个 active reservation 和一个终结事件;同 event_id/同整行是 replay,返回原 receipt;同 event_id 或同 idempotency key 但字段不同为 `QUOTA_REPLAY_CONFLICT`。 ### 3.6 `TimingRow CSV V002`(固定 12 列) `schema_version,run_id,item_id,phase,operation,monotonic_start_ns,monotonic_end_ns,elapsed_ms,wall_start_utc,wall_end_utc,outcome,detail_code` `phase=preflight|quota|ui_search|ui_scan|detail_and_trigger|cache_wait|copy|validation|manifest|delivery|timing|terminal`;`outcome=PASS|STOP|FAIL|SKIP`。monotonic 只允许进程内差值,wall 仅审计。 ### 3.7 `ManifestRow CSV V002`(固定 34 列) `schema_version,row_id,task_id,handoff_id,run_id,item_id,slot_id,requester,review_owner,query,candidate_id,report_key,title,institution,report_date,analysts,selection_reason,remote_path,remote_bytes,remote_sha256,local_path,local_bytes,local_sha256,pdf_magic_ok,pdf_openable,page_count,encrypted,quota_reservation_id,quota_terminal_event_id,status,stop_code,created_at_utc,reused_without_new_trigger,external_evidence_hash` - `row_id=sha256(task_id|run_id|item_id|report_key|status)`;成功必须 34 键全部有值或合法 null,且 remote/local bytes/hash 相等、magic/openable true、encrypted false、page_count 与详情一致。 - `status=SUCCESS|DUPLICATE|FAILED|STOPPED`;非 SUCCESS 的未知证据保持 null,禁止填造。 - `external_evidence_hash` 是该行对应正式 PDF 的 SHA-256;无正式 PDF 时为 null。 ### 3.8 `PackageState V002` 与终态(有序字段) 每个 item 的 package row 为:`item_id,report_key,state,trigger_attempted,quota_state,staging_path,final_path,manifest_row_id,delivery_present,timing_present,terminal_present,stop_code`。 `TerminalRecord V002` 固定 30 键: `schema_version,task_id,handoff_id,run_id,mode,status,exit_code,stop_code,blocker,observed_at_utc,started_at_utc,ended_at_utc,total_elapsed_ms,work_deadline_reached,close_deadline_reached,requested,triggered,succeeded,failed,duplicates,gaps,quota_confirmed,quota_uncertain,quota_active,quota_safe_available,items,manifest_path,delivery_path,timing_path,prohibited_action_attestation` `items` 按 item_id 排序,元素即上述 package row。任何路径不存在时 null;`prohibited_action_attestation` 固定对象:`remote_original_modified:false,credential_persisted:false,access_control_bypassed:false,body_parsed:false,research_conclusion_generated:false,external_message_sent:false`。 ## 4. 候选、早停、详情确认与补扫状态机 ### 4.1 两层集合 - `provisional_set`:结果页可见字段通过“可先判断”的过滤规则;只用于决定是否暂停扫描、安排详情复核,不等于合格候选。 - `confirmed_set`:详情页字段与结果页一致、所有硬过滤通过、去重完成;只有该集合可 reservation/trigger。 - `stable_top_k`:连续两屏 provisional top `quantity+2` 的 id+score 不变,只产生 `PAUSE_FOR_DETAIL`,不产生最终早停。 ### 4.2 固定流程 1. 从 checkpoint 或第 1 屏开始,至少扫描 3 屏;每屏落内存 candidate/checkpoint,不写正式结果。 2. 达到 provisional `quantity+2` 且两屏稳定时保存 checkpoint,暂停扫描并按 score/既有并列规则逐个 `confirm_detail`。 3. 详情字段必须与结果页 title/institution/date/pages/analysts 可比字段一致;不一致为 `DETAIL_RESULT_MISMATCH` 并 REJECTED。 4. 同机构上限、报告 identity 去重、日期/页数/类型/分析师过滤在详情确认后再次执行。 5. `confirmed_set>=quantity` 才允许最终结束扫描;否则调用 `restore(checkpoint)`,从该 cursor 的下一未处理屏继续。 6. 详情打开失败、详情不一致、duplicate、硬过滤失败、trigger 前页面漂移、trigger 后确认未触发,都不会把 provisional 当成功;恢复后继续扫描。 7. 扩展扫描终止只允许:confirmed 数量满足;连续 3 屏无新增 provisional 且所有 provisional 已详情判定;或达到 20 屏/100 unique 上限。数量不足时返回 gap,不伪装成功。 ### 4.3 checkpoint 恢复 恢复路径固定为:回到搜索入口→重新输入同 query→按 `scroll_count` 重放纵向翻页→比较 first/last visible key 与 page_fingerprint→从已记录 seen ids 后继续。每步都受 Budget;任一 anchor/fingerprint 不一致停止,不猜坐标。测试必须覆盖详情失败、详情/结果不一致、重复候选、trigger 未发生、早停后补扫成功、恢复 fingerprint 漂移和硬上限不足。 ## 5. 单一时间预算与子进程/阻塞 I/O 生命周期 ### 5.1 从“请求已观察”开始计时 调用方在观察请求且环境可用时写入 `TaskSpec.observed_at_utc`;CLI 启动立即读取同主机 UTC,并计算 `startup_elapsed_ms=now_utc-observed_at_utc`。允许时钟前进,若 observed 在未来超过 2 秒、无法 parse、时差计算异常或 startup elapsed 已达到 600000ms,返回 `BLOCKED_INPUT`/`TIME_BUDGET_STOP`,不触发 UI。 CLI 随后记录 `start_monotonic_ns`,并创建: - `close_deadline=start_monotonic + max(0,600000-startup_elapsed_ms)`; - `work_deadline=close_deadline-10000ms`;最后 10 秒只用于 kill/drain、quota 收口、timing/terminal,不得启动新 UI、pull、hash、publish 或 manifest 行。 - 批量首 item 继承上述 deadline;每个后续 item 在前一 item terminal 时创建 `min(close_deadline, item_start+240000ms)` 的 item deadline。总体任务不得超过首份 600 秒;如任务明确允许批量持续,batch task spec 必须把 `total_budget_ms` 写成 `600000+240000*(quantity-1)`,首份仍独立 600 秒。V002 默认按该公式验证,不接受其他值。 所有 SLA 总耗时使用 `ended_at_utc-observed_at_utc` 与进程 monotonic elapsed 交叉核对;偏差超过 2 秒为 timing invalid,样本计失败。 ### 5.2 `ProcessSupervisor` 总状态机 固定状态:`NOT_STARTED -> STARTED -> EXITED`;异常分支 `START_FAILED`;deadline 分支 `TIMEOUT -> TERMINATE_ONCE -> WAIT_BOUNDED -> EXITED_AFTER_TERMINATE|LIVENESS_UNKNOWN`。 1. start 前 `budget.slice(max_ms, teardown_ms=3000)` 必须返回正值;argv 非空字符串数组,`shell=False`,cwd 固定,stdin/stdout/stderr 为 binary pipes 或 DEVNULL。 2. Windows 使用一个 per-child Job Object,`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`,先 suspended create、成功 assign 后 resume;任一步失败时不运行未受管 child。非 Windows 测试用新 process group 等价模拟。 3. `communicate(timeout=slice)`;stdout/stderr 各有上限,超限立即进入 terminate,不截断后冒充成功。 4. timeout/异常只允许一次 `TerminateJobObject`,随后 `wait/communicate` 最多 `min(3000ms,close_remaining)`;确认 exit 后才返回。不能确认时关闭 Job handle并返回 `LIVENESS_UNKNOWN`,CLI 只做内存 terminal,不再读/写可能被 child 改动的 artifact。 5. `ProcessResult` 固定字段:`argv_redacted,started,pid,exit_code,timed_out,terminate_issued,exited,liveness_unknown,stdout_bytes,stderr_bytes,stdout_truncated,stderr_truncated,started_at_utc,ended_at_utc,elapsed_ms,error_code`。凭据永不进入 argv/stdin/output。 ### 5.3 锁与文件 I/O - 锁:每 50ms 非阻塞尝试,最长 `min(2000ms,work_remaining)`;失败 `LOCK_TIMEOUT`,不得阻塞到 deadline。 - pull 输出仅到 staging;复制/hash 每 1MiB 前后 checkpoint;截图/小 JSON 每 64KiB。到期立即关闭句柄,保留 staging,不登记 success。 - `flush/fsync`、`pdfinfo` 等不可中断边界仅在 `work_remaining>=5000ms` 时进入;进入前后均 checkpoint。调用返回晚于 work deadline 时结果一律 `TIME_BUDGET_STOP`,即使 bytes/hash 正确也不得发布/登记 SUCCESS。 - manifest/delivery/timing/terminal 进入各自步骤前保留 2000/2000/2000/3000ms;不足则跳过下游、在 console terminal 中把对应 present=false。close deadline 到达后不得再做文件写,直接返回内存 terminal。 - hang/kill/wait、stdout overflow、锁阻塞、copy/hash 分块到期、fsync late、pdfinfo late、manifest late、terminal persist late 都必须有 deterministic fake-clock/subprocess tests。 ## 6. 原子归档、崩溃恢复与 package closure ### 6.1 staging 与 no-replace 发布 1. staging 必须在最终目标同一 NTFS volume、同一 destination 目录下的 `.hibor-staging/`,名称 `...part`,以 `os.open(O_CREAT|O_EXCL|O_WRONLY)` 创建;staging 永不被当正式 PDF。 2. pull/copy 完成后 flush+`os.fsync`,关闭;独占只读 reopen,验证 `%PDF-`、remote/local bytes/hash、pdfinfo open/page/encryption 和详情页一致。 3. 发布使用 Win32 `CreateHardLinkW(final,staging)`:同卷、原子创建新目录项、final 已存在则失败且绝不替换;随后只读 reopen final 再校验 bytes/hash,最后 unlink staging。若 hard-link capability/reparse/volume 不符合,预检 STOP,不降级为覆盖 rename/copy。 4. crash before link:只有 `.part`;crash after link before unlink:final 与 `.part` 指向同一已验证文件;恢复以 file-id+bytes/hash 证明同一后清理 staging。不同 hash、unknown file-id 或 final reparse 一律冲突 STOP。 5. 任何最终路径若已存在:ordinary file 且 hash 等于期望→`DUPLICATE`,复用原文件但不重复 consume;其他 object/hash→`FINAL_PATH_CONFLICT`。不删除、截断、覆盖或改名模拟器原件和既有正式文件。 ### 6.2 package state/expected-presence(总序) | state | PDF staging | final PDF | quota terminal | manifest | delivery | timing | terminal | 允许后继 | |---|---:|---:|---:|---:|---:|---:|---:|---| | P00_INIT | N | N | N | N | N | N | N | preflight | | P01_RESERVED | N | N | N | N | N | N | N | trigger | | P02_TRIGGER_UNKNOWN | N | N | U | N | N | N | N | close only | | P03_CACHE_MATCHED | N | N | V | N | N | N | N | stage | | P04_STAGING_PARTIAL | I | N | V | N | N | N | N | recover/stop | | P05_STAGING_VALID | V | N | V | N | N | N | N | publish | | P06_PUBLISHED | N|V | V | V | N | N | N | N | manifest | | P07_MANIFESTED | N|V | V | V | V | N | N | N | delivery | | P08_DELIVERY | N|V | V | V | V | V | N | N | timing | | P09_TIMING | N|V | V | V | V | V | V | N | terminal | | P10_CLOSED | N|V | V|N | V|U|N | V|N | V|N | V|N | V|N | none | `N=absent,V=known valid,I=known partial/invalid,U=unknown`。顺序固定:quota trigger 终结→PDF publish→manifest→delivery→timing→terminal。上游失败不补造下游;P10 terminal 根据实际 presence 记录,不要求为了“闭包好看”写假文件。 ### 6.3 恢复 每个 staging 创建后同步写一个同目录 recovery JSON(同样 O_EXCL),字段为 `schema_version,run_id,item_id,remote_path,remote_bytes,remote_sha256,staging_path,final_path,stage,created_at_utc`。`resume-postprocess` 仅在:无 active UI/process、quota 已是 confirmed/uncertain、remote identity 不变、staging/final 可安全复核时继续;否则 STOP。故障注入点固定为 create 前、每 1MiB 后、fsync 前后、hardlink 前后、final reopen 后、manifest/delivery/timing/terminal 前后,断言正式路径不会指向 partial。 ## 7. 额度并发、reducer 与 replay ### 7.1 唯一临界区 账本旁固定 `daily_quota_.lock`。第一次以 O_CREAT 建立普通 lock file,之后所有进程打开同一文件并用 `msvcrt.locking(LK_NBLCK,1)` 锁第 0 字节;lock file 不删除。每个 quota public API 在同一个锁持有期内严格执行: `read all bytes -> strict CSV/header/date/row validation -> fold -> idempotency lookup -> APP visible remainder conservative reconcile(if provided) -> safe_available check -> construct one row -> append -> flush -> fsync -> reopen/read/fold verify -> unlock`。 锁前后都 checkpoint;锁 timeout/ledger unreadable/invalid/reopen mismatch 为 STOP,不点击原文。不得缓存旧 fold 后另开写事务。 ### 7.2 reducer - `baseline_floor=max(all BASELINE_ESTIMATE/CORRECTION_RAISE.baseline_floor)`;2026-07-29 首次必须 `>=3`,证据 ref 为中国中免任务。 - `confirmed=baseline_floor + count(unique idempotency keys terminal CONSUME_CONFIRMED)`;`uncertain=count(...CONSUME_UNCERTAIN)`;`active=count(RESERVE without exactly one terminal)`。 - `safe_available=max(0,27-confirmed-uncertain-active)`;APP 可见剩余若可信,则 `app_consumed=30-visible_remaining`,有效 confirmed floor 取本地与 APP 更保守者;APP 不可信不降低本地值。 - `RESERVE` 只在 safe_available>=1、同 key 无 terminal/active 时写入。写入并 reopen 验证前不得点击。 - `CONSUME_CONFIRMED/UNCERTAIN/RELEASE` 必须引用 active reservation 并使其恰好终结一次;重复相同请求返回原 event,不追加。 - correction 只提高 baseline 或以明确 ref 把残留 reservation终结;禁止负 confirmed/uncertain、降低 baseline 或删除历史。 ### 7.3 崩溃/竞争测试 必须用两个真实测试进程竞争最后一个 safe slot,唯一一个 RESERVE 成功;覆盖锁 timeout、append 前崩溃(无行)、append 后 fsync 前/后、reopen mismatch、同 CLI 重放、同 handoff 不同 payload 冲突、残留 reservation、release/confirmed/uncertain replay、baseline>=3、correction raise、跨日 ledger、APP 值更保守。所有测试用临时账本,不接 APP、不消耗额度。 ## 8. 错误、终态、exit code 与 precedence precedence 从高到低:`LIVENESS_UNKNOWN/STATE_UNCERTAIN -> ACCESS_CONTROL -> TIME_BUDGET -> QUOTA -> INPUT -> ENVIRONMENT -> AMBIGUOUS_MAPPING -> VALIDATION -> INTERNAL -> PARTIAL/SUCCESS`。高优先级证据不得被后续低优先级覆盖。 | terminal status | stop/error family | exit | |---|---|---:| | `SUCCESS` | 全部 requested item P10 success | 0 | | `PARTIAL_SUCCESS` | 至少一个 success,存在非 quota/time 的已知 gap | 2 | | `TIME_BUDGET_STOP` | deadline/late I/O/subprocess timeout | 10 | | `PARTIAL_QUOTA_STOP` | safe_available 不足且有有效子集 | 11 | | `BLOCKED_INPUT` | task/schema/replay contract | 12 | | `BLOCKED_ACCESS_CONTROL` | login/CAPTCHA/paywall/subscription/permission | 13 | | `BLOCKED_ENVIRONMENT` | device/package/cache/tool/lock/liveness known failure | 14 | | `BLOCKED_AMBIGUOUS_MAPPING` | cache/candidate/final identity 多义 | 15 | | `VALIDATION_FAILED` | PDF/hash/page/manifest/package validation | 16 | | `INTERNAL_ERROR` | 未分类异常,证据保持 null | 20 | | `STATE_UNCERTAIN` | child/file/quota liveness 或事实未知 | 27 | 成功子集存在时,TIME/QUOTA/ACCESS/STATE_UNCERTAIN 仍保留该高优先级 terminal,不降为 partial。CLI 捕获裸异常为 `INTERNAL_ERROR/20`;任何 terminal 中 `status/exit_code/stop_code/blocker` 必须互相符合本表。 ## 9. Manifest/delivery/timing/terminal 发布规则 - PDF/manifest/quota 是正式不可变证据;manifest 使用固定 34 列,单锁 read/validate/idempotency→append/fsync/reopen,`row_id` replay 同整行返回已有;冲突 STOP。 - `delivery.md` 只列元数据、文件路径、选择理由、状态、缺口与额度摘要,不含正文/结论;其源数据必须来自已验证 model,不从文件名猜。 - `timing_manifest.csv` 固定 12 列,所有已进入阶段均有一行,未进入阶段不造行。 - terminal JSON 是 package 最后一个可选持久化对象;无剩余 close budget 时只向 stdout 输出严格 JSON,并标 `terminal_path=null,terminal_present=false`。工具不发送 Codex/网络消息。 - 所有正式输出记录实际 bytes/hash 到运行 receipt;不以 hash 匹配替代语义验证。 ## 10. 测试与性能总体 ### 10.1 L0/L1/L2(零真实 trigger) 1. L0:`py_compile`、import、禁止 API/shell/credential 静态扫描。 2. L1:标准库 unittest 覆盖全部 schema/交叉约束、candidate/cursor、Budget/ProcessSupervisor、quota reducer/并发、atomic publish/package matrix、manifest/terminal 与错误映射。 3. L2:fake ADB + 合成 PDF + 临时 NTFS 目录,真实启动可控 helper child 测 hang/kill/wait/stdout overflow;dry-run 必须 `real_adb_action_count=0,trigger_count=0,real_quota_write_count=0`。 4. 实现复审前必须提供 test case→requirement→result trace matrix;失败可在 L0/L1 范围修复重跑,保留失败记录,不需逐次管理 A001。 ### 10.2 真实性能计划(最多 10 个 distinct trigger) 在第一次真实 reserve 前生成并锁定 `performance_plan.json`:10 个 primary item + 最多 3 个 no-trigger alternate;字段固定 `plan_id,created_at_utc,slot_id,mode,batch_id,item_order,query,expected_report_identity,external_exclusion_allowed,selected_from_readonly_discovery`。该计划的实际 bytes/hash 写入执行日志,之后不替换已触发 slot。 固定总体: | slots | mode | 计入单份样本 | trigger 上限 | 批量角色 | |---|---|---:|---:|---| | `PERF-S01..PERF-S06` | 6 次 `collect-one` | 6 | 6 | 无 | | `PERF-B01-I01..I04` | 1 次同 query `collect-batch` 的 4 个 item | 4 | 4 | I01=首份,I02-I04=增量 | | `PERF-A01..A03` | 预登记 alternate | 仅替代 trigger 前 external-excluded primary | 合计仍不得超过 10 | 保持被替代 slot mode | 因此批量每个 item 同时是一个真实单份样本,正式 PASS 分母固定为 10,distinct trigger 最大也是 10;不得为了凑 PASS 触发第 11 份。alternate 仅在 reserve/点击前出现有证据的 external blocker 时替换,且被替代 primary 永久标 `EXCLUDED_PRETRIGGER_EXTERNAL`。 ### 10.3 纳入、排除与统计 - slot 在 preflight 后第一次成功 RESERVE 时进入 SLA population;从 task observed 到该 item terminal 是单份总耗时。任何 `TIME_BUDGET_STOP`、internal、validation、mapping、quota-state failure 都保留在分母并令 performance acceptance 失败,不能删样本。 - 可排除项只有在 RESERVE/点击前确认的:登录/验证码/付费/权限、模拟器离线、APP 服务不可用或报告已不可访问;必须有截图/UI dump/process receipt,且使用预登记 alternate。触发后才出现的外部问题仍计 SLA failure。 - collect-batch I01 从 batch request observed 到 I01 terminal;I02-I04 增量从前一 item terminal monotonic 到本 item terminal。每个 item 也记录自身 start-to-terminal。 - median:10 个总耗时排序后第 5/6 平均;nearest-rank P90:排序第 `ceil(0.9*10)=9` 个。任何计入样本非成功、任何验证项失败或样本少于 10,整体只能 `PERFORMANCE_FAILED` 或 `INSUFFICIENT_PERFORMANCE_SAMPLES`。 - PASS 必须同时:10/10 item success;每份 magic、remote/local bytes/hash、openability、page_count、manifest、quota 全通过;median<=480000ms;P90<=600000ms;B01-I01<=600000ms;I02-I04 每份 incremental<=240000ms;trigger_total<=10;confirmed+uncertain+active 永不超过 27。 - 当日安全额度不足以做完时保留已完成样本并 `INSUFFICIENT_PERFORMANCE_SAMPLES` 顺延到次日新 ledger;不同日期样本可合并,但每个 slot 仍唯一、每日日限独立,统计不可重置失败 slot。 ## 11. 实施顺序与验收矩阵 设计 PASS 后按以下顺序实施: 1. models/budget/process + schema/child tests; 2. quota 原子 reducer + 双进程测试; 3. candidate/scan checkpoint + fake UI 测试; 4. archive staging/publish/recovery + crash injection; 5. manifest/delivery/timing/terminal + package matrix; 6. CLI 集成与 fake ADB dry-run; 7. 开发自检与独立实现复审前置检查; 8. 真实 10-trigger 性能计划、执行、证据与最终独立复审。 | blocker | V002 closure | 强制测试 | |---|---|---| | 01 | 第 2–3、8 节 API/schema/call graph/exit | schema/order/null/extra-key/typed error | | 02 | 第 5 节 observed→monotonic、Job child、bounded I/O | hang/kill/lock/late copy/hash/fsync/terminal | | 03 | 第 4 节 provisional/confirmed/checkpoint | mismatch/duplicate/detail fail/补扫/漂移 | | 04 | 第 6、9 节 staging+hardlink+closure | 每边界 crash、冲突、duplicate、恢复 | | 05 | 第 3.5、7 节 atomic quota/replay | 双进程、崩溃、重复、残留、跨日 | | 06 | 第 10.2–10.3 节固定 6+4 population | external exclusion、failure inclusion、median/P90 | 实现不得以跳过 hash、页数、manifest、quota、access-control 或 terminal closure 换取 SLA。开发员只做实现与自检,最终由 `dev.reviewer.ana.cai` 独立审核。 ## 12. 当前状态 `PENDING_INDEPENDENT_DESIGN_REREVIEW`。 V002 复审 PASS 前,候选源码/测试/额度账本仍不存在;不得运行 Python、ADB、慧博 APP、真实或合成采集。当前真实 trigger 增量=`0`,2026-07-29 已知外部基线仍按至少 3 保守处理。