# Codex Adversarial Review

- 対象: `~/projects/insta-auto-v3/packages/worker/` (base=96bcaf5 → HEAD=ee99f25, branch=v3)
- 実行日: 2026-04-17
- モデル: Codex (openai-codex plugin 1.0.3) via `/codex:adversarial-review`
- 参照設計書: `20_機能別/子安君_案件共有/04_AIインスタ担当v2_設計書.html` 11章・12章

---

Target: branch diff against 96bcaf5
Verdict: **needs-attention**

no-ship。予約投稿は二重実行防止が成立しておらず、承認フローもsingle-useを原子的に保証していない。提案4/5の設計意図を満たしていない箇所が残っている。

## Findings

### [critical] processing_started_at は実ロックになっておらず、同じ投稿を複数Cronが奪える
`packages/worker/src/services/scheduler.ts:34-61`

`staleThresholdIso` はJSのISO文字列（例: `2026-04-17T23:05:00.000Z`）だが、ロック取得時の `processing_started_at = datetime('now')` はSQLite形式（例: `2026-04-17 23:10:00`）。TEXT比較では空白が `T` より小さいため、取得直後のロックでも `processing_started_at < staleThresholdIso` が真になり得る。つまりAがUPDATEでロック取得してInstagram投稿中でも、Bの同時/再試行CronがSELECT/UPDATEを通過して同じ投稿を再投稿できる。`lockToken` も `last_error` に書くだけで後続UPDATEで検証されず、所有権のない見せかけのロックになっている。

**Recommendation**: 時刻はepoch秒またはSQLite `datetime()` 形式に統一し、`locked_by`/`processing_attempt_id` を明示的に保存する。claim、success、failureの全UPDATEに `WHERE id=? AND status='scheduled' AND locked_by=?` を入れて `changes` を検証し、lease満了後の再取得が外部投稿の二重実行にならないようheartbeatか十分長いlease/外部idempotencyを設計する。

---

### [high] scheduled_at の比較もTEXT形式不一致で、同日予約が期限到来扱いされない
`packages/worker/src/services/scheduler.ts:38-45`

Schedulerは `scheduled_at <= datetime('now')` をTEXT比較している。一方、publish側は `scheduledTime` をISO文字列として検証した後に生値のまま保存しているため、`2026-04-17T10:00:00.000Z <= 2026-04-17 10:01:00` は同じ日付内では偽になり得る。分単位Cronを回してもpredicate自体が壊れているので、予約投稿が翌UTC日まで遅延する、または実行時刻の監査が信用できない。

**Recommendation**: 保存時に `scheduled_at` をUTC epoch秒またはSQLite `YYYY-MM-DD HH:MM:SS` に正規化し、比較も同じ型で行う。少なくとも `datetime(scheduled_at) <= datetime('now')`/`strftime('%s', ...)` を使い、ISO入力の同日・日跨ぎケースをテストに入れる。

---

### [high] 承認トークンはsingle-useではなく、二重POSTで即時投稿が重複する
`packages/worker/src/routes/approval.ts:62-81`

POST `/approval/:token` はpending行をSELECTした後、`UPDATE approval_requests SET status = ? ... WHERE id = ?` を無条件に実行してから副作用に進む。2つの承認リクエストが同時に来ると、両方が同じpending行を読める。UPDATEには `status='pending'` 条件も `changes` チェックもないため、両方が承認済みとしてInstagram投稿処理へ進み、即時投稿では同じ内容が二重投稿される。approve/reject競合ではDB上の承認状態と実際の投稿結果も分裂する。

**Recommendation**: `UPDATE approval_requests SET ... WHERE token=? AND status='pending' AND expires_at > datetime('now')` を最初の原子的claimにし、`changes === 1` の場合だけ副作用へ進む。投稿側も同じトランザクション/claimで保護し、処理済み・期限切れ・存在なしの応答は列挙しにくい形に寄せる。

---

### [medium] プロンプトバージョニングは固定デフォルトで、A/Bテストにも監査にも耐えない
`packages/worker/src/prompts/registry.ts:22-27`

registryは未知/未指定versionを常に `DEFAULT_METHOD_VERSION` に落とすだけで、generate側も固定の `DEFAULT_METHOD_VERSION` を使う。store/tenant/experiment単位でversionを選ぶ入口がなく、後でA/Bや段階 rollout を入れるにはAPI・DB・store設定の横断改修が必要になる。さらにpublishはクライアントから来た `methodVersion` を信じてpostsに保存するため、実際に生成に使ったversionと保存versionがズレても検出できない。設計書12章の「どのプロンプト版で生成されたかを記録」「A/Bテスト可能」を満たしていない。

**Recommendation**: version選択をサーバ側のstore/tenant設定またはexperiment割当テーブルに移し、generate時点で不変のgeneration/artifact IDとmethod_versionを保存する。publishはそのartifact IDを参照し、リクエスト本文の任意version文字列は受け付けないかregistryで厳密検証する。

---

### [medium] Cronが設計書の5分間隔から1分間隔に変わり、壊れたロックとリトライを増幅している
`packages/worker/wrangler.toml:17-18`

wranglerは `* * * * *` で毎分実行だが、設計書10.2/12.5は予約投稿チェックを `*/5 * * * *` とし、5分ズレを許容する前提だった。現在の実装では失敗時に即 `processing_started_at=NULL` へ戻すため、毎分Cronだと一時的なInstagram/API障害で3回リトライを数分で使い切って `failed` になる。さらに上記ロック不成立により、同じUTC分の複数scheduled()やCloudflare側の再試行が重複投稿を発火しやすい設定になっている。

**Recommendation**: 設計通り `*/5 * * * *` に戻すか、毎分運用するなら `next_attempt_at` による明示的なretry backoff、batch checkpoint、lease所有者検証、失敗/重複の監視を先に入れる。

---

## Next steps

- 予約投稿の時刻表現を統一し、lease所有者を検証する回帰テストを追加する。
- 承認POSTを原子的claim方式に変更し、同時approve/rejectのテストを追加する。
- プロンプトversionをstore/tenant設定またはexperiment割当に移し、publish時のクライアント申告を信用しない設計に変える。
