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