创建人员: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 局部合同全部继承且不得回退。
所有模型均为 @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 总映射捕获。
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。| 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;后继严格递增 |
| 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 |
| 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 传递未声明键。
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。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,...]。600000+240000*(quantity-1);其他时间/screens/package/cache 约束继承 V002。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。固定 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.requester;review_owner=TaskSpec.review_owner。source_url='UNKNOWN';source_site='慧博APP';http_status='NOT_APPLICABLE_APP_CACHE';content_type='application/pdf'。不得留空后猜 URL。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。CLI 启动先以同主机 UTC 计算 startup_elapsed_ms,随后只用 monotonic:
batch_close_deadline_ns = start_ns + (TaskSpec.total_budget_ms - startup_elapsed_ms)*1e6;这是整个 CLI 最终 terminal 的硬截止。first_close_deadline_ns=min(batch_close_deadline_ns,start_ns+(600000-startup_elapsed_ms)*1e6)。prev_terminal_ns,item_close_deadline_ns=min(batch_close_deadline_ns,prev_terminal_ns+240000*1e6)。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 截断”的规则。
DeadlineScope=BATCH_CLOSE|ITEM_CLOSE|ITEM_WORK:
ITEM_WORK;ITEM_CLOSE;BATCH_CLOSE,但每个 item 在自己的 item deadline 前已有 item terminal row。begin_first_item() 只能调用一次;begin_next_item() 要求 previous_terminal_ns 等于已关闭 item 的 terminal monotonic。任一 gap/重复调用为 ContractError。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。
前 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
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 个扩展字段。BASELINE_ESTIMATE|CORRECTION_RAISE|APP_RECONCILE|RESERVE|CONSUME_CONFIRMED|CONSUME_UNCERTAIN|RELEASE|ARTIFACT_SUCCESS|ARTIFACT_DUPLICATE_OR_FAILED。ref_event_id=null,但 evidence_ref 必须非空(2026-07-29 固定引用 HIBOR-CHINA-DUTYFREE-20260729-001 manifest/worklog);correction 引用 ledger event 时 ref_event_id 非空,引用外部历史时 evidence_ref 非空。对同一 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。
notenote 写 evidence 摘要;0/0/+1;CONFIRMED=+1/0/-1;UNCERTAIN=0/+1/-1;RELEASE=0/0/-1;| 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。
V003 复审必须确认:
当前状态:PENDING_INDEPENDENT_DESIGN_REREVIEW。V003 PASS 前不创建候选源码/测试、额度账本,不运行 Python/ADB/APP/dry-run,不产生真实 trigger;当前新增 trigger=0。