背景
#117 のatomic commitで「失敗時にphantom stateを出さない」契約を整える一方、同じ旧画面から送られた複数の編集・import・deleteは現在last-writer-winsになる。StoreSnapshot.generationは内部にあるだけで、APIのread response・mutation request・success responseへ出ていない。
これは非エンジニアが画面を複数開いた場合、または応答喪失後にretryした場合に、意図しない上書き・重複auditを起こす。#120 のundo/redoとは別に、write時点でstale stateを拒否する必要がある。
目的
全てのstate mutationにrevision/operation receiptを導入し、古いrevisionを明示的な409として拒否する。正常retryは同じoperation receiptを返し、event・import history・audit logを重複させない。
対象
- EventStoreのmonotonic revisionとsuccess receipt
- read payload(snapshot、event page、settings、import history、diagnostics)へのrevision露出
- label/review/edit/exclude/split/merge/settings/clear/import/recording finalization/deleteの
expected_revision
- FastAPI、handwritten local API、desktop WebUIの409
stale_revision 表示と再読込導線
- request/operation idempotency keyとSQLite上のdurable operation ledger
- retry、同時writer、response-loss、restart後retryのtest matrix
非対象
Claude Code実装契約
StoreSnapshot.generationを永続revisionとして扱う。process restart後に0へ戻らないようSQLite transaction内で更新する。
- mutation requestは
expected_revisionとclient generated operation_idを必須にする。recording agentの内部retryはsession/sequenceから安定したoperation idを作る。
- EventStoreはlock内でexpected revisionを照合する。異なる場合はDB/memoryを一切変更せず、stable
stale_revision errorを返す。
- operation ledgerはmutation payload fingerprint・revision・success receiptを同じSQLite transactionで保存する。同じoperation idかつ同一payloadのretryは保存済みreceiptを返し、異なるpayloadならstable conflictにする。
- success responseには
revision、operation_id、必要なresource idを含める。409/503ではraw exception、absolute path、PIIを返さない。
- WebUIは409時に「他の変更を検出した。最新状態を読み直してからやり直す」と表示し、入力を勝手に再送しない。
- compatibilityを壊す変更はAPI/schema versionとdocsへ明記し、FastAPI/handwritten serverで同一contractを保つ。
受け入れ条件
- 同じold revisionから送られた2編集は先行1件だけ成功し、後続は409
stale_revision。DB、memory、再起動後stateが一致する。
- import/clear/delete/recording finalizationを含む全mutationで、operation retryはevent/import history/auditを重複させず同じreceiptを返す。
- response-loss後、process restart後、concurrent writer、WAL reader下のproperty testがpassする。
- FastAPI、handwritten local API、desktop WebUIのエラー契約と翻訳が一致する。
./scripts/test.sh、./scripts/lint.sh、./scripts/check_licenses.sh、./scripts/check_no_external_network.sh がpassする。
依存関係
背景
#117 のatomic commitで「失敗時にphantom stateを出さない」契約を整える一方、同じ旧画面から送られた複数の編集・import・deleteは現在last-writer-winsになる。
StoreSnapshot.generationは内部にあるだけで、APIのread response・mutation request・success responseへ出ていない。これは非エンジニアが画面を複数開いた場合、または応答喪失後にretryした場合に、意図しない上書き・重複auditを起こす。#120 のundo/redoとは別に、write時点でstale stateを拒否する必要がある。
目的
全てのstate mutationにrevision/operation receiptを導入し、古いrevisionを明示的な409として拒否する。正常retryは同じoperation receiptを返し、event・import history・audit logを重複させない。
対象
expected_revisionstale_revision表示と再読込導線非対象
Claude Code実装契約
StoreSnapshot.generationを永続revisionとして扱う。process restart後に0へ戻らないようSQLite transaction内で更新する。expected_revisionとclient generatedoperation_idを必須にする。recording agentの内部retryはsession/sequenceから安定したoperation idを作る。stale_revisionerrorを返す。revision、operation_id、必要なresource idを含める。409/503ではraw exception、absolute path、PIIを返さない。受け入れ条件
stale_revision。DB、memory、再起動後stateが一致する。./scripts/test.sh、./scripts/lint.sh、./scripts/check_licenses.sh、./scripts/check_no_external_network.shがpassする。依存関係