# 从观察和提案，走到可信回执

Python 3.11+标准库，离线合成工单。ScriptedSelector是固定选择替身，不是LLM。SQLite提供本地持久化、幂等去重与条件更新；不模拟远程API、真实身份网关或集群。DEMO_KEY和固定教学时钟1000都是公开fixture，绝不能当作生产签名/认证设计。

## 第一次运行

先预测：读两次找到重复依据，缺审批时会提交吗？提交后回执丢失，是否可以直接报告失败？

```bash
cd proposal-receipt
python3 -B flow.py run --scenario missing --out runs/wait
python3 -B flow.py approve --out runs/wait
python3 -B -m unittest discover -s tests -v
```

Windows可用py -3。第一次应为MISSING_APPROVAL、read_calls=2、write_attempts=0、committed_writes=0，proposal.json保存具体工单/version/note/operation_id。你主动执行approve后，可信教学Authority才对这份不可变提案签发信封，得到DONE和一个receipt。18条测试应通过。

`trace.json`是最近运行，`runs.jsonl`保留历次记录，`ledger.sqlite3`保留实际资源/回执。相同目录再次运行会核对已有回执，而不会重建空账本。每个故障使用新的out目录，否则旧成功回执可能让你直接DONE而未经过故障。

## 先读代码中的五个身份

- Request：调用方的tenant/resource/operation；选择器不能改资源或自造操作号。
- Observation：工具按可信tenant读取真实状态；两类工具必须都读到；resource/status/version/duplicate_of任一冲突就HANDOFF。
- Proposal：没有副作用，绑定已读取的version和具体note。
- Approval：operation_id + args_digest + expires_at + HMAC；模型参数approved=true不算审批。
- Receipt：SQLite事务同时记录关闭与幂等回执。DONE来自真实回执，不是模型说“已完成”。

本例get_case/get_registry两次读取同一合成数据源，仅为了教学控制流；真实场景需各自的权威来源与更新时间。审批证明动作获准，不证明现场事实还一致，所以提交事务仍用status=OPEN AND version=observed_version进行条件更新。

## 故障开关

```bash
python3 -B flow.py run --scenario after-commit --out runs/after
python3 -B flow.py run --scenario before-commit --out runs/before
python3 -B flow.py run --scenario changed-args --out runs/changed
python3 -B flow.py run --scenario stale-state --out runs/stale
python3 -B flow.py run --scenario held-state --out runs/held
python3 -B flow.py run --scenario expired --out runs/expired
python3 -B flow.py run --scenario conflict --out runs/conflict
python3 -B flow.py run --scenario forged --out runs/forged
python3 -B flow.py run --scenario loop --out runs/loop
python3 -B flow.py run --scenario cross-tenant --out runs/cross
```

| 场景 | 状态 | local write_attempts / committed_writes |
| --- | --- | --- |
| after-commit | DONE | 1 / 1；超时后lookup原号 |
| before-commit | DONE | 2 / 1；核对无回执，原号有界重投 |
| changed-args | APPROVAL_CHANGED_ARGS | 0 / 0 |
| stale-state | STATE_CHANGED | 1 / 0；OPEN仍在，但version/重复依据变化 |
| held-state | STATE_CHANGED | 1 / 0；资源已HELD |
| expired | APPROVAL_EXPIRED | 0 / 0 |
| conflict | HANDOFF_CONFLICT | 0 / 0 |
| forged | INVALID_ACTION | 0 / 0 |
| loop | NO_PROGRESS | 0 / 0；第三次相同候选前停止 |
| cross-tenant | NO_AUTHORIZED_RESOURCE | 0 / 0；不返回正文 |

write_attempts是本地Ledger.commit调用数，不是实际网络请求。UnknownWrite故意不暴露after_commit事实；调用方只能查同一operation_id。SQLite已持久化回执时，新进程也能lookup到，不需要模型决定重试。

## 新进程恢复与真实边界

```bash
python3 -B flow.py run --scenario after-commit --out runs/after
```

沿用已完成目录，应得到同一receipt、0次新write、committed_writes仍1。相同operation不同payload应冲突；另一项真实意图必须新建操作号。测试验证回执持久化、参数绑定、审批恰到期、状态变化与正常通过。

真实服务lookup可能暂时查不到；本实验lookup同步，不证明最终一致查询契约。无法确定结果时保留unknown并等待/人工核对，不能换号。真实身份必须来自受信网关，审批签名键隔离在可信服务；模型不能自行调用Authority.issue。远程服务端也必须提供去重/条件写入，客户端重复发送本身不能保证只提交一次。

## 红→绿与陌生任务

在自己的副本删去commit中version条件及对应SQL绑定参数，运行stale-state测试，它应红。该场景保留OPEN，只更新version与重复依据，不能被status条件代挡；恢复后再跑18条。只验证最终状态是CLOSED会漏掉覆盖别人修改的错误。

迁移到“预留仓库A的SKU-7三件”：先声明固定工作流是否足够，再定义读取库存/规则、提案数量/版本、人审信封、服务端条件扣减和回执。交四条trace设计：库存足够、库存不足、审批后数量改变、提交后超时；至少实现库存不足/同意图重复投递的失败测试。

参考条件：审批绑定warehouse/SKU/qty/version/operation；提交时available>=qty且version匹配；不同仓库不能复用证据；没有回执不得宣称扣减成功；同意图重投最多一次实际扣减。工单的OPEN/duplicate_of不能直接改名当库存规则。本包未实现库存，只有真正运行的迁移代码才能报告通过。

## 排错

- 文件不存在：进入flow.py所在目录；approve必须使用先前生成proposal的目录。
- 某故障直接DONE：先看目录里是否已有成功回执，为新故障换out目录。
- SQLite错误：核对目录可写，不要删除别人的运行证据。
- 18条绿但接真实模型不稳：这些测试只证明执行边界；模型选择质量应另用真实输入/轨迹评测。

继续原版Ch04有界选择与Ch06工具契约，再读持久化/评测。LangGraph[官方幂等说明](https://github.com/langchain-ai/docs/blob/main/src/oss/langgraph/functional-api.mdx)解释未完成任务恢复可能重执行；checkpoint不会自动替外部服务消除副作用。
