# 慧博 10 分钟快速采集工具开发方案 V003 创建人员:`dev.developer.ana.cai` 文件职责:append-only 修复 V002 复审保留的 3 个 blocker;补全公开能力与跨模块 wire、统一 batch deadline、恢复额度账本消费者字段并消除 APP 总量重复计数。 predecessor:V002=`31916/fb778660e940046d85c9aee7be55743ca4b5d4c84c2c73f31eb533ee5b255313`。 predecessor review:`AUDIT-DEV-ANA-HIBOR-FAST-COLLECTION-DESIGN-V002=HOLD/3/3`。 后继关系:V003 只替换 V002 的第 2.2、3、5.1、7.2 及其直接引用;V002 已关闭的 candidate checkpoint、atomic publish/package closure、6+4 population,以及 Job/lock/chunked I/O 和 quota 原子临界区/replay 局部合同全部继承且不得回退。 ## 1. Public API:所有签名闭合 所有模型均为 `@dataclass(frozen=True, slots=True)`;序列使用 `tuple`,可空用 `T | None`,枚举均为 `str, Enum`。唯一 orchestration owner 仍为 `cli.main`。完整 public API 如下,禁止 `...`、未类型化 `ctx` 或模块自行启动 subprocess: ```python # models.py def load_task_spec(path: Path) -> TaskSpec: ... def validate_task_spec(value: Mapping[str, object]) -> TaskSpec: ... def to_ordered_dict(model: CanonicalModel) -> dict[str, object]: ... # budget.py class Budget: @classmethod def start(cls, observed_at_utc: str, total_ms: int, first_item_ms: int, increment_ms: int, close_reserve_ms: int, clock: Clock) -> Budget: ... def begin_first_item(self, item_id: str) -> ItemBudget: ... def begin_next_item(self, item_id: str, previous_terminal_ns: int) -> ItemBudget: ... def remaining_ms(self, scope: DeadlineScope) -> int: ... def checkpoint(self, phase: Phase, operation: str, scope: DeadlineScope) -> None: ... def slice(self, max_ms: int, teardown_ms: int, scope: DeadlineScope) -> int: ... # process.py class ProcessSupervisor: def run(self, argv: tuple[str, ...], cwd: Path, budget: Budget, phase: Phase, input_bytes: bytes | None, max_stdout: int, max_stderr: int) -> ProcessResult: ... # adb.py class AdbClient: def preflight(self, ctx: RunContext) -> DeviceSnapshot: ... def list_cache(self, ctx: RunContext) -> tuple[RemoteFile, ...]: ... def remote_sha256(self, ctx: RunContext, remote_path: str) -> str: ... def pull(self, ctx: RunContext, remote: RemoteFile, local_staging: Path) -> PullResult: ... def ui_dump(self, ctx: RunContext) -> UiSnapshot: ... def screenshot(self, ctx: RunContext, local_staging: Path) -> ArtifactDraft: ... def tap(self, ctx: RunContext, x: int, y: int, expected_anchor: str) -> ActionReceipt: ... def swipe(self, ctx: RunContext, x1: int, y1: int, x2: int, y2: int, duration_ms: int, expected_anchor: str) -> ActionReceipt: ... def input_text(self, ctx: RunContext, text: str, expected_anchor: str) -> ActionReceipt: ... def back(self, ctx: RunContext, expected_anchor: str) -> ActionReceipt: ... def start_package(self, ctx: RunContext, expected_anchor: str) -> ActionReceipt: ... # ui.py class FastScanner: def scan(self, ctx: RunContext, checkpoint: ScanCheckpoint | None) -> ScanOutcome: ... def confirm_detail(self, ctx: RunContext, candidate: Candidate) -> Candidate: ... def restore(self, ctx: RunContext, checkpoint: ScanCheckpoint) -> ScanCheckpoint: ... def trigger(self, ctx: RunContext, candidate: Candidate, reservation: ReservationResult) -> TriggerReceipt: ... # quota.py class QuotaLedger: def snapshot(self, ctx: RunContext, app_visible_remaining: int | None) -> QuotaSnapshot: ... def reserve(self, ctx: RunContext, report_identity: str, slot_id: str, app_visible_remaining: int | None) -> ReservationResult: ... def confirm(self, ctx: RunContext, reservation: ReservationResult, trigger: TriggerReceipt, app_visible_remaining: int | None) -> QuotaEvent: ... def mark_uncertain(self, ctx: RunContext, reservation: ReservationResult, trigger: TriggerReceipt, reason: ErrorCode, app_visible_remaining: int | None) -> QuotaEvent: ... def release(self, ctx: RunContext, reservation: ReservationResult, reason: ErrorCode, app_visible_remaining: int | None) -> QuotaEvent: ... def raise_external_baseline(self, ctx: RunContext, floor: int, evidence_ref: str, ref_event_id: str | None) -> QuotaEvent: ... def record_artifact_outcome(self, ctx: RunContext, report_identity: str, success_unique: bool, duplicate_or_failed: bool, ref_event_id: str) -> QuotaEvent: ... # cache.py class CacheWatcher: def capture_baseline(self, ctx: RunContext) -> CacheBaseline: ... def wait_unique_stable(self, ctx: RunContext, baseline: CacheBaseline, trigger: TriggerReceipt) -> CacheMatch: ... # archive.py class ArchiveManager: def stage_pull(self, ctx: RunContext, match: CacheMatch) -> ArtifactDraft: ... def validate(self, ctx: RunContext, draft: ArtifactDraft, candidate: Candidate) -> ValidatedArtifact: ... def publish_no_replace(self, ctx: RunContext, artifact: ValidatedArtifact, destination: Path) -> PublishedArtifact: ... def recover(self, ctx: RunContext, record: RecoveryRecord) -> RecoveryResult: ... # manifests.py def append_manifest(ctx: RunContext, row: ManifestRow) -> ManifestReceipt: ... def write_delivery(ctx: RunContext, package: PackageState) -> PublishedArtifact: ... def write_timing(ctx: RunContext, rows: tuple[TimingRow, ...]) -> PublishedArtifact: ... # terminal.py def build_terminal(ctx: RunContext, package: PackageState, stop: StopReason | None) -> TerminalRecord: ... def persist_terminal(ctx: RunContext, record: TerminalRecord) -> TerminalReceipt: ... def render_public_payload(record: TerminalRecord) -> str: ... # cli.py def main(argv: Sequence[str] | None = None) -> int: ... ``` 所有外部进程只可经 `ProcessSupervisor.run(tuple(argv), shell=False)`;locator 来自已验证 TaskSpec。typed errors 只有 `ContractError(code: ErrorCode, field: str|None, detail: str)`, `BudgetExpired(phase,operation,deadline_scope)`, `StateUncertain(code,detail)`;CLI 按 V002 第 8 节 precedence/exit 总映射捕获。 ## 2. 跨模块 canonical models(字段顺序即 wire 顺序) `Clock` 是只读 protocol:`monotonic_ns()->int`、`utc_now()->datetime`;生产实现只调用标准库时钟,测试实现只由测试推进。`CanonicalModel` 是本文全部 frozen dataclass 的封闭 union,不允许任意 Mapping 混入跨模块边界。 枚举注册表(唯一合法值): - `DeadlineScope=BATCH_CLOSE|ITEM_CLOSE|ITEM_WORK`;`Phase=preflight|quota|ui_search|ui_scan|detail_and_trigger|cache_wait|copy|validation|manifest|delivery|timing|terminal`。 - `DeviceState=ONLINE|OFFLINE|UNAUTHORIZED|UNKNOWN`;`ObjectType=FILE|DIRECTORY|SYMLINK|OTHER|UNKNOWN`;`PageKind=HOME|SEARCH|RESULT|DETAIL|READER|ACCESS_CONTROL|UNKNOWN`。 - `ArtifactKind=PDF|SCREENSHOT|DELIVERY|TIMING|TERMINAL|RECOVERY`;`UiAction=START_PACKAGE|FOCUS_SEARCH|CLEAR_SEARCH|INPUT_QUERY|SEARCH|SCROLL|OPEN_DETAIL|OPEN_ORIGINAL|BACK`;`ActionOutcome=PASS|NO_EFFECT|DRIFT|ACCESS_CONTROL|FAIL`。 - `ScanStatus=ENOUGH_CONFIRMED|PAUSED_FOR_DETAIL|EXHAUSTED|STOPPED`;`TriggerClass=CONFIRMED_TRIGGER|CONFIRMED_NO_TRIGGER|UNCERTAIN`;`CacheMatchStatus=UNIQUE_STABLE|NONE_TIMEOUT|AMBIGUOUS|OLD_FILE_CHANGED|UNKNOWN`。 - `Openability=OPENABLE|NOT_OPENABLE|UNKNOWN`;`EncryptionStatus=NOT_ENCRYPTED|ENCRYPTED|UNKNOWN`;`RecoveryStage=CREATED|COPIED|VALIDATED|LINKED|REOPENED`;`RecoveryStatus=RECOVERABLE|DUPLICATE|CONFLICT|UNKNOWN`;`EvidenceState=N|V|I|U`;`RecoveryAction=CONTINUE|UNLINK_STAGING|STOP|NONE`。 - `TerminalStatus=SUCCESS|PARTIAL_SUCCESS|TIME_BUDGET_STOP|PARTIAL_QUOTA_STOP|BLOCKED_INPUT|BLOCKED_ACCESS_CONTROL|BLOCKED_ENVIRONMENT|BLOCKED_AMBIGUOUS_MAPPING|VALIDATION_FAILED|INTERNAL_ERROR|STATE_UNCERTAIN`。 - `ErrorCode=NONE|TASK_SPEC_INVALID|CLOCK_INVALID|DEADLINE_EXPIRED|PROCESS_START_FAILED|PROCESS_TIMEOUT|PROCESS_OUTPUT_LIMIT|PROCESS_LIVENESS_UNKNOWN|DEVICE_NOT_UNIQUE|DEVICE_NOT_ONLINE|PACKAGE_MISSING|CACHE_UNREADABLE|UI_ANCHOR_DRIFT|ACCESS_CONTROL_PRESENT|UI_CURSOR_RESTORE_FAILED|DETAIL_RESULT_MISMATCH|CANDIDATE_REJECTED|TRIGGER_NOT_OCCURRED|TRIGGER_UNCERTAIN|CACHE_TIMEOUT|CACHE_AMBIGUOUS|REMOTE_FILE_CHANGED|LOCK_TIMEOUT|QUOTA_LEDGER_INVALID|QUOTA_REPLAY_CONFLICT|QUOTA_EXHAUSTED|QUOTA_DATE_UNCERTAIN|PULL_FAILED|PDF_MAGIC_INVALID|BYTE_COUNT_MISMATCH|HASH_MISMATCH|PDF_NOT_OPENABLE|PAGE_COUNT_MISMATCH|PDF_ENCRYPTED|FINAL_PATH_CONFLICT|PUBLISH_FAILED|RECOVERY_UNKNOWN|MANIFEST_INVALID|PERSIST_LATE|UNEXPECTED_EXCEPTION`。无错误一律 `NONE`,不允许自由字符串替代 error code。 ### 2.1 共同模型 | model | 有序字段与类型 | 关键约束 | |---|---|---| | `RunContext` | `task:TaskSpec,run_id:Id,budget:Budget,project_root:Path,output_root:Path,destination_root:Path,quota_ledger:Path,process:ProcessSupervisor,clock:Clock,dry_run:bool` | CLI 唯一创建;run_id 在一次进程内不变 | | `StopReason` | `status:TerminalStatus,code:ErrorCode,phase:Phase,operation:str,detail:str,evidence_paths:tuple[Path,...]` | detail 已脱敏;不可空 | | `ProcessResult` | `argv_redacted:tuple[str,...],started:bool,pid:int|None,exit_code:int|None,timed_out:bool,terminate_issued:bool,exited:bool,liveness_unknown:bool,stdout_bytes:bytes,stderr_bytes:bytes,stdout_truncated:bool,stderr_truncated:bool,started_at_utc:UtcTime|None,ended_at_utc:UtcTime|None,elapsed_ms:int|None,error_code:ErrorCode|None` | 与 V002 Job 状态机一致 | | `ItemBudget` | `item_id:Id,item_index:int,item_started_ns:int,item_deadline_ns:int,item_work_deadline_ns:int,previous_terminal_ns:int|None` | first index=0;后继严格递增 | ### 2.2 ADB/UI/cache 模型 | model | 有序字段与类型 | null/枚举约束 | |---|---|---| | `DeviceSnapshot` | `serial:str,state:DeviceState,transport_id:str|None,model:str|None,is_emulator:bool,package_name:str,package_installed:bool,foreground_package:str|None,screen_width:int,screen_height:int,cache_root:str,cache_readable:bool,captured_at_utc:UtcTime` | state=`ONLINE|OFFLINE|UNAUTHORIZED|UNKNOWN`; success 需 ONLINE/installed/readable | | `RemoteFile` | `remote_path:str,file_name:str,object_type:ObjectType,bytes:int|None,mtime_epoch:int|None,remote_sha256:Hash|None` | object=`FILE|DIRECTORY|SYMLINK|UNKNOWN`; 仅 FILE 可匹配 | | `PullResult` | `remote:RemoteFile,local_staging:Path,process:ProcessResult,local_bytes:int|None,local_sha256:Hash|None,complete:bool,error_code:ErrorCode|None` | complete 才允许 validate | | `UiSnapshot` | `package_name:str,page_kind:PageKind,anchor:str|None,visible_text:tuple[str,...],visible_bounds:tuple[str,...],normalized_fingerprint:Hash,screenshot_path:Path|None,captured_at_utc:UtcTime` | page=`HOME|SEARCH|RESULT|DETAIL|READER|ACCESS_CONTROL|UNKNOWN` | | `ArtifactDraft` | `artifact_kind:ArtifactKind,item_id:Id,staging_path:Path,remote_path:str|None,expected_bytes:int|None,expected_sha256:Hash|None,actual_bytes:int|None,actual_sha256:Hash|None,created_at_utc:UtcTime,complete:bool,error_code:ErrorCode|None` | kind=`PDF|SCREENSHOT|DELIVERY|TIMING|TERMINAL|RECOVERY` | | `ActionReceipt` | `action_id:Id,action:UiAction,attempted:bool,pre_package:str|None,pre_anchor:str|None,post_package:str|None,post_anchor:str|None,started_at_utc:UtcTime,ended_at_utc:UtcTime,outcome:ActionOutcome,error_code:ErrorCode|None` | action 为固定白名单;outcome=`PASS|NO_EFFECT|DRIFT|ACCESS_CONTROL|FAIL` | | `ScanOutcome` | `status:ScanStatus,candidates:tuple[Candidate,...],confirmed_ids:tuple[Id,...],checkpoint:ScanCheckpoint,screens_scanned:int,unique_candidates:int,consecutive_no_new:int,hard_limit_reached:bool,error_code:ErrorCode|None` | status=`ENOUGH_CONFIRMED|PAUSED_FOR_DETAIL|EXHAUSTED|STOPPED` | | `TriggerReceipt` | `trigger_id:Id,candidate_id:Id,report_identity:str,reservation_id:Id,action:ActionReceipt,classification:TriggerClass,cache_baseline_id:Id,triggered_at_utc:UtcTime|None,error_code:ErrorCode|None` | class=`CONFIRMED_TRIGGER|CONFIRMED_NO_TRIGGER|UNCERTAIN`; CONFIRMED_TRIGGER 才有 triggered_at | | `CacheBaseline` | `baseline_id:Id,cache_root:str,captured_at_utc:UtcTime,files:tuple[RemoteFile,...],set_fingerprint:Hash` | remote_path 唯一、ordinal 排序 | | `CacheMatch` | `baseline_id:Id,trigger_id:Id,status:CacheMatchStatus,remote:RemoteFile|None,first_seen_at_utc:UtcTime|None,stable_at_utc:UtcTime|None,poll_count:int,ambiguous_paths:tuple[str,...],error_code:ErrorCode|None` | status=`UNIQUE_STABLE|NONE_TIMEOUT|AMBIGUOUS|OLD_FILE_CHANGED|UNKNOWN` | ### 2.3 quota/archive/publish 模型 | model | 有序字段与类型 | null/枚举约束 | |---|---|---| | `QuotaSnapshot` | `ledger_date:str,external_baseline_floor:int,ledger_confirmed_delta:int,ledger_uncertain_delta:int,app_total_consumed:int|None,confirmed_consumed:int,uncertain_consumed:int,active_reservations:int,cumulative_consumed:int,safe_available:int,event_count:int,last_event_id:Id|None` | reducer 见第 6 节 | | `ReservationResult` | `reservation_id:Id,event:QuotaEvent,snapshot:QuotaSnapshot,replayed:bool,allowed:bool,stop_code:ErrorCode|None` | allowed false 时不得 trigger | | `QuotaEvent` | 本文第 6.1 节固定 CSV 33 列对应同名类型 | append 后整行不可变 | | `ValidatedArtifact` | `item_id:Id,staging_path:Path,remote_path:str,remote_bytes:int,remote_sha256:Hash,local_bytes:int,local_sha256:Hash,pdf_magic_valid:bool,openability:Openability,page_count:int,encryption_status:EncryptionStatus,file_id:str,validated_at_utc:UtcTime` | bytes/hash 必须相等;openability=`OPENABLE`; encryption=`NOT_ENCRYPTED` | | `PublishedArtifact` | `artifact_kind:ArtifactKind,item_id:Id,final_path:Path,relative_path:str,bytes:int,sha256:Hash,file_id:str,published_at_utc:UtcTime,deduplicated:bool,source_staging_path:Path|None` | final ordinary non-reparse;dedup 时 source staging null | | `RecoveryRecord` | `schema_version:str,run_id:Id,item_id:Id,remote_path:str,remote_bytes:int,remote_sha256:Hash,staging_path:Path,final_path:Path,stage:RecoveryStage,created_at_utc:UtcTime` | stage=`CREATED|COPIED|VALIDATED|LINKED|REOPENED` | | `RecoveryResult` | `record:RecoveryRecord,status:RecoveryStatus,staging_state:EvidenceState,final_state:EvidenceState,same_file_id:bool|None,bytes_match:bool|None,hash_match:bool|None,action:RecoveryAction,error_code:ErrorCode|None` | status=`RECOVERABLE|DUPLICATE|CONFLICT|UNKNOWN`; action=`CONTINUE|UNLINK_STAGING|STOP|NONE` | | `ManifestReceipt` | `row_id:Id,manifest_path:Path,appended:bool,replayed:bool,bytes_after:int,sha256_after:Hash,row_count_after:int,validated:bool,error_code:ErrorCode|None` | appended xor replayed;validated success 必须 true | | `TerminalReceipt` | `terminal_path:Path|None,persist_attempted:bool,persisted:bool,stdout_emitted:bool,bytes:int|None,sha256:Hash|None,error_code:ErrorCode|None` | close deadline 后 path/bytes/hash null,stdout true | `Candidate`, `ScanCheckpoint`, `TimingRow`, `ManifestRow`, `PackageState`, `TerminalRecord` 由 V003 第 3–5 节及 V002 未冲突字段共同定义。所有跨模块对象现均有唯一字段、类型、null 和枚举来源,模块不得以 dict 传递未声明键。 ## 3. REPORT-COLLECTION-CAPABILITY-V1 无损映射 ### 3.1 `TaskSpec V003`(固定 43 键) `schema_version,contract_version,project_id,handoff_id,task_id,source_role_instance_id,source_thread_id,target_role_instance_id,target_thread_id,reply_thread_id,requester,review_owner,mode,query,quantity,aliases,analysts,institutions,report_types,date_range,minimum_pages,exclude,source_scope,destination,priority,naming_requirement,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,performance_slot_id,performance_plan_id` 约束: - `schema_version='HIBOR_FAST_TASK_SPEC_V003'`;`contract_version='REPORT-COLLECTION-CAPABILITY-V1'`;`project_id='project-info'`;target role 固定 `case_analysis.report_collector`。 - capability 11 个必填字段逐字进入同名字段:`contract_version,handoff_id,project_id,task_id,source_role_instance_id,source_thread_id,target_role_instance_id,target_thread_id,reply_thread_id,query,quantity`,不得重命名或丢失。 - `requester=source_role_instance_id`,`review_owner=source_role_instance_id`;不允许调用方另给不同值。 - 可选字段逐一映射:`aliases→aliases`、`analysts→analysts`、`institutions→institutions`、`report_types→report_types`、`date_range→date_range`、`minimum_pages→minimum_pages`、`exclude→exclude`、`source_scope→source_scope`、`destination→destination`、`priority→priority`、命名要求→`naming_requirement`;未提供时用 null 或空 tuple,不猜值。 - `mode=DRY_RUN|COLLECT_ONE|COLLECT_BATCH|RESUME_POSTPROCESS`。`date_range` 是固定对象 `start:YYYY-MM-DD|None,end:YYYY-MM-DD|None`;`priority` 为 INT|null;其他可选列表为 `tuple[str,...]`。 - collect-one:quantity=1、total=600000;batch:total=`600000+240000*(quantity-1)`;其他时间/screens/package/cache 约束继承 V002。 ### 3.2 `report_collection_terminal V003`(固定 43 键) `schema_version,message_type,contract_version,project_id,task_id,handoff_id,source_role_instance_id,source_thread_id,target_role_instance_id,target_thread_id,reply_thread_id,requester,review_owner,run_id,mode,status,capability_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_cumulative_consumed,quota_safe_available,quota_ledger_path,items,manifest_path,delivery_path,timing_path,prohibited_action_attestation` - `message_type='report_collection_terminal'`;request routing 10 字段原样回显。工具只生成 payload,不发送消息。 - `capability_status` 总映射:SUCCESS→`ACCEPTED`;PARTIAL_SUCCESS→`ACCEPTED`;PARTIAL_QUOTA_STOP→`PARTIAL_QUOTA_STOP`;BLOCKED_INPUT→`BLOCKED_INPUT`;BLOCKED_ACCESS_CONTROL→`BLOCKED_ACCESS_CONTROL`;额度在启动前不足→`ACCEPTED_PARTIAL_QUOTA|QUEUED_NEXT_DAY`(由是否允许部分 quantity 唯一决定);其余内部扩展 status 保持 `status` 原值并在 capability_status=`BLOCKED_INPUT` 或最接近的公开 STOP,不伪装 ACCEPTED。 - items 使用 V002 package row;成功 item 另含正式交付明细:标题、机构、日期、分析师、页数、字节数、SHA、source/status。请求/触发/成功/失败/重复/gap 和额度摘要均为真实计数。 ## 4. Manifest V003:上游字段完整保留 固定 48 列,前 17 列严格保留角色规范最小字段,接着 10 个慧博专用字段,再接 21 个内部追溯字段: ```text task_id,requested_by,review_owner,source_url,source_site,title,publisher, report_date,downloaded_at,http_status,content_type,file_name,relative_path, bytes,sha256,download_status,error_or_note, source_cache_path,source_file_name,android_package,extension_added, pdf_magic_valid,remote_sha256,local_sha256,openability,page_count,encryption_status, schema_version,row_id,handoff_id,run_id,item_id,slot_id,query,candidate_id, report_identity,analysts,selection_reason,remote_bytes,local_bytes, quota_reservation_id,quota_terminal_event_id,quota_artifact_event_id, status,stop_code,reused_without_new_trigger,external_evidence_hash,manifested_at_utc ``` 字段映射: - `requested_by=TaskSpec.requester`;`review_owner=TaskSpec.review_owner`。 - APP 无可靠公开 URL 时 `source_url='UNKNOWN'`;`source_site='慧博APP'`;`http_status='NOT_APPLICABLE_APP_CACHE'`;`content_type='application/pdf'`。不得留空后猜 URL。 - title/publisher/report_date 来自详情确认;不可靠即 `UNKNOWN` 且不能作为 hard-filter success。`analysts` 用 `;` 连接详情值。 - `file_name/relative_path/bytes/sha256` 指正式项目副本;无正式副本时 path/bytes/hash 为空,download_status=`FAILED|STOPPED|DUPLICATE`。 - `downloaded_at` 是 cache trigger 确认时间;`manifested_at_utc` 是 row append 时间。`error_or_note` 必须含 typed stop 或 `NONE`。 - `source_cache_path/source_file_name/android_package/extension_added/pdf_magic_valid/remote_sha256/local_sha256/openability/page_count/encryption_status` 与角色规范同义;extension_added 仅在缓存无扩展且正式名补 `.pdf` 时 true。 - `remote_bytes/local_bytes` 是内部交叉验证字段;成功时两者等于 base `bytes`,remote/local hash 等于 base `sha256` 和 external_evidence_hash。 - row PK、状态/null/重放和锁定规则继承 V002;任何额外消费者字段不得删除或复用为别的语义。 ## 5. 无矛盾 batch deadline ### 5.1 四个 deadline CLI 启动先以同主机 UTC 计算 `startup_elapsed_ms`,随后只用 monotonic: 1. `batch_close_deadline_ns = start_ns + (TaskSpec.total_budget_ms - startup_elapsed_ms)*1e6`;这是整个 CLI 最终 terminal 的硬截止。 2. first item:`first_close_deadline_ns=min(batch_close_deadline_ns,start_ns+(600000-startup_elapsed_ms)*1e6)`。 3. next item i:前一 item terminal 的 monotonic 为 `prev_terminal_ns`,`item_close_deadline_ns=min(batch_close_deadline_ns,prev_terminal_ns+240000*1e6)`。 4. 每个 item 的 `item_work_deadline_ns=item_close_deadline_ns-close_reserve_ms*1e6`;最后 10 秒只做该 item kill/drain/quota/timing/terminal closure。 collect-one 的 batch_close 与 first_close 相同。collect-batch 的 total 为 600000+240000*(quantity-1),所以首份最多 600 秒,I02-I0N 各自最多从前一 terminal 增量 240 秒,最终总 terminal 不超过 batch total。不存在“所有 item 仍被首份 600 秒 close_deadline 截断”的规则。 ### 5.2 API scope `DeadlineScope=BATCH_CLOSE|ITEM_CLOSE|ITEM_WORK`: - UI、trigger、cache、pull、hash、publish、manifest 使用 `ITEM_WORK`; - child kill/drain、quota terminal event、item timing 使用 `ITEM_CLOSE`; - 最后 delivery 汇总、全局 timing 和 public terminal 使用 `BATCH_CLOSE`,但每个 item 在自己的 item deadline 前已有 item terminal row。 - `begin_first_item()` 只能调用一次;`begin_next_item()` 要求 previous_terminal_ns 等于已关闭 item 的 terminal monotonic。任一 gap/重复调用为 ContractError。 ### 5.3 边界测试 fake clock 固定覆盖:启动前耗时 0/599999/600000ms;first 恰在 600000ms 前后;next 在 previous terminal+239999/240000ms;quantity=4 batch total 1320000ms 前后;item work reserve 10000ms 前后;first 未关闭不得启动 next;batch deadline 比 item deadline 更早;terminal 落在 deadline 后不得标 SUCCESS。 ## 6. QuotaLedger V003:兼容 view 与不双计 reducer ### 6.1 固定 33 列 前 20 列严格保留第 4.3.4 节公开账本最小字段,后 13 列为幂等/来源扩展: ```text 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, 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 ``` - 固定常量:timezone=`Asia/Shanghai`、platform/target/hard/buffer=`30/25/27/3`;requester_role 来自 TaskSpec.requester。 - `confirmed_consumed/uncertain_consumed/cumulative_consumed/safe_available_after` 是追加该事件后的 reducer snapshot,不是 delta;delta 只在后 3 个扩展字段。 - event_type 扩展为 `BASELINE_ESTIMATE|CORRECTION_RAISE|APP_RECONCILE|RESERVE|CONSUME_CONFIRMED|CONSUME_UNCERTAIN|RELEASE|ARTIFACT_SUCCESS|ARTIFACT_DUPLICATE_OR_FAILED`。 - artifact 两类事件 quota deltas 均为 0,分别令 success_unique_pdf_delta=1 或 duplicate_or_failed_delta=1;其 ref_event_id 指向 quota terminal event。 - baseline 首次可 `ref_event_id=null`,但 `evidence_ref` 必须非空(2026-07-29 固定引用 `HIBOR-CHINA-DUTYFREE-20260729-001` manifest/worklog);correction 引用 ledger event 时 ref_event_id 非空,引用外部历史时 evidence_ref 非空。 ### 6.2 reducer 无双计公式 对同一 `quota_date` 严格按合法 event_seq fold: ```text external_baseline_floor = max(valid baseline/correction floors, default 0) ledger_confirmed_delta = sum unique CONSUME_CONFIRMED.confirmed_delta ledger_uncertain_delta = sum unique CONSUME_UNCERTAIN.uncertain_delta local_confirmed = external_baseline_floor + ledger_confirmed_delta local_uncertain = ledger_uncertain_delta app_total_floor = max(non-null APP_RECONCILE.app_total_consumed, default 0) app_unattributed_gap = max(0, app_total_floor - local_confirmed - local_uncertain) confirmed_consumed = local_confirmed uncertain_consumed = local_uncertain + app_unattributed_gap cumulative_consumed = confirmed_consumed + uncertain_consumed active_reservations = count(valid RESERVE without one valid terminal event) safe_available_after = max(0, 27 - cumulative_consumed - active_reservations) ``` APP 可见剩余只生成/重放 `APP_RECONCILE(app_total_consumed=30-visible_remaining)`,绝不转写为 `external_baseline_floor`,所以不会与已登记 confirmed events相加两次。随着本地 terminal 对 APP 总量取得可识别覆盖,`app_unattributed_gap` 可以下降,但 cumulative 始终为 `max(local_confirmed+local_uncertain, app_total_floor)`;没有可靠 APP 值则沿用历史 max,不降低。 `external_baseline_floor` 只表示账本启用前、由外部证据确认的历史触发。2026-07-29 初值至少 3;后续本工具所有 trigger 只进入 ledger delta,不再加入 baseline。跨日必须新 ledger;APP 未刷新/身份不确定时不写 reconcile、不 reserve,返回 STOP。 ### 6.3 合法行与 `note` - BASELINE/CORRECTION:delta=0,external floor 非空,`note` 写 evidence 摘要; - APP_RECONCILE:app_total 非空 0..30,其余 delta/floor 为空或 0; - RESERVE=`0/0/+1`;CONFIRMED=`+1/0/-1`;UNCERTAIN=`0/+1/-1`;RELEASE=`0/0/-1`; - ARTIFACT_SUCCESS 的 success delta=1;ARTIFACT_DUPLICATE_OR_FAILED 的 duplicate delta=1;每个 report identity 最多一个 artifact terminal event。 - 每行都写 fold 后完整公开 snapshot;重放同整行不 append。原子 read-validate-fold-check-append-fsync-reopen 与 lock/replay 合同完全继承 V002。 ### 6.4 确定向量 | vector | external | local confirmed | local uncertain | APP total | app gap | effective confirmed/uncertain/cumulative | active | safe | |---|---:|---:|---:|---:|---:|---|---:|---:| | Q0 初始中国中免 | 3 | 0 | 0 | null | 0 | 3/0/3 | 0 | 24 | | Q1 APP 同值 | 3 | 0 | 0 | 3 | 0 | 3/0/3 | 0 | 24 | | Q2 本地部分、APP 覆盖 | 3 | 2 | 0 | 5 | 0 | 5/0/5 | 0 | 22 | | Q3 APP 多出未知 2 | 3 | 2 | 0 | 7 | 2 | 5/2/7 | 0 | 20 | | Q4 本地更保守 | 3 | 2 | 1 | 4 | 0 | 5/1/6 | 0 | 21 | | Q5 APP 先 5、再本地到 6 | 3 | 3 | 0 | 5 | 0 | 6/0/6 | 0 | 21 | | Q6 最后一 safe slot 并发 | 3 | 23 | 0 | 26 | 0 | 26/0/26 | 1 | 0 | 另测 APP remainder=30/27/25/3/0、本地 0/部分/全部 terminal、active before/after trigger、APP 低于历史 floor、同 reconcile replay、baseline ref_event=null+evidence_ref、correction raise、跨日未刷新;断言 `cumulative=max(local_total,app_total_floor)`,绝不 double count/under count/release unknown。 ## 7. 继承验收与当前 gate V003 复审必须确认: 1. 19 个原缺失跨模块模型和所有 public signatures 已闭合,capability request→TaskSpec→terminal 无损; 2. manifest 前 17+慧博 10 字段与角色规范同义且齐全; 3. batch/first/next/item-work/batch-close 公式不冲突并覆盖边界测试; 4. quota 前 20 列兼容,external baseline/local deltas/APP total 分离,7 个向量和扩展负向测试无双计; 5. BLOCK-03/04/06、Job/lock/chunked I/O、atomic quota/replay 和所有禁止边界无回退。 当前状态:`PENDING_INDEPENDENT_DESIGN_REREVIEW`。V003 PASS 前不创建候选源码/测试、额度账本,不运行 Python/ADB/APP/dry-run,不产生真实 trigger;当前新增 trigger=`0`。