edit | blame | history | raw

慧博 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:

# 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()->intutc_now()->datetime;生产实现只调用标准库时钟,测试实现只由测试推进。CanonicalModel 是本文全部 frozen dataclass 的封闭 union,不允许任意 Mapping 混入跨模块边界。

枚举注册表(唯一合法值):

  • DeadlineScope=BATCH_CLOSE|ITEM_CLOSE|ITEM_WORKPhase=preflight|quota|ui_search|ui_scan|detail_and_trigger|cache_wait|copy|validation|manifest|delivery|timing|terminal
  • DeviceState=ONLINE|OFFLINE|UNAUTHORIZED|UNKNOWNObjectType=FILE|DIRECTORY|SYMLINK|OTHER|UNKNOWNPageKind=HOME|SEARCH|RESULT|DETAIL|READER|ACCESS_CONTROL|UNKNOWN
  • ArtifactKind=PDF|SCREENSHOT|DELIVERY|TIMING|TERMINAL|RECOVERYUiAction=START_PACKAGE|FOCUS_SEARCH|CLEAR_SEARCH|INPUT_QUERY|SEARCH|SCROLL|OPEN_DETAIL|OPEN_ORIGINAL|BACKActionOutcome=PASS|NO_EFFECT|DRIFT|ACCESS_CONTROL|FAIL
  • ScanStatus=ENOUGH_CONFIRMED|PAUSED_FOR_DETAIL|EXHAUSTED|STOPPEDTriggerClass=CONFIRMED_TRIGGER|CONFIRMED_NO_TRIGGER|UNCERTAINCacheMatchStatus=UNIQUE_STABLE|NONE_TIMEOUT|AMBIGUOUS|OLD_FILE_CHANGED|UNKNOWN
  • Openability=OPENABLE|NOT_OPENABLE|UNKNOWNEncryptionStatus=NOT_ENCRYPTED|ENCRYPTED|UNKNOWNRecoveryStage=CREATED|COPIED|VALIDATED|LINKED|REOPENEDRecoveryStatus=RECOVERABLE|DUPLICATE|CONFLICT|UNKNOWNEvidenceState=N|V|I|URecoveryAction=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_idreview_owner=source_role_instance_id;不允许调用方另给不同值。
  • 可选字段逐一映射:aliases→aliasesanalysts→analystsinstitutions→institutionsreport_types→report_typesdate_range→date_rangeminimum_pages→minimum_pagesexclude→excludesource_scope→source_scopedestination→destinationpriority→priority、命名要求→naming_requirement;未提供时用 null 或空 tuple,不猜值。
  • mode=DRY_RUN|COLLECT_ONE|COLLECT_BATCH|RESUME_POSTPROCESSdate_range 是固定对象 start:YYYY-MM-DD|None,end:YYYY-MM-DD|Nonepriority 为 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 个内部追溯字段:

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.requesterreview_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_nsitem_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 列为幂等/来源扩展:

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:

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