Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions docs/working/TASK-0896/pbi-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# PBI INPUT PACKAGE — TASK-0896

> Issue: [#896](https://github.com/s977043/plangate/issues/896)(P1 / enhancement / area:workflow)
> 関連 EPIC: [#870](https://github.com/s977043/plangate/issues/870)(#873/#874 実装前の基盤整理として位置づけ)
> 作成: 2026-07-22(重複を実測裏取り済み・main 3ec2e24)

## Context / Why

c3-prime 契約(#872 で確定・PR #885〜#889/#895 で実装完了)の検証規則が `scripts/ai-loop/` の 3 ファイルに分散実装されている。#873(delivery.py)/#874(run_evidence.py)で同規則の消費者が 4〜5 に増える前に共通層へ集約する(「3 回目に初めて共通化」基準は充足済み)。分散のままだと契約改版時の実装間 drift で**承認境界の検証強度が silent に割れる**。

実測(2026-07-22・main 3ec2e24):

| 重複 | 箇所 | 多重度 |
|------|------|--------|
| 定数(`VALID_DECISIONS`/`VALID_VERDICTS`/`ARTIFACTS`)| plan_package.py L27,82-83 + c3prime_verify.py L28,32-33 | 2 重 |
| snapshot キー定義 | arbiter.py `SNAPSHOT_REQUIRED_KEYS`(L486) + c3prime_verify.py `SNAPSHOT_KEYS`(L34) + plan_package.py 組み立て側(L294-303) | **3 重** |
| file sha256 ヘルパー | plan_package.py `_sha256_of`(L137) + c3prime_verify.py `_sha256`(L46) | 2 重 |
| canonical JSON hash 式(`json.dumps(sort_keys)` の sha256)| plan_package.py L148-153 + c3prime_verify.py L150-152 | 2 重 |
| **snapshot 三つ組照合(#872 契約 AC-5「reviewer が同一 hash を観た」の核心規則)** | arbiter.py `plan_package_check`(L495) + c3prime_verify.py(L155-171) | 2 重(**別実装**)|

前例: `check_evidence` は既に plan_package.py へ集約済み(c3prime_verify.py L26 が import)— その延長。

## What(Scope)

### In scope

1. **`scripts/ai-loop/c3_contract.py` 新設**(非 HO): 契約定数(ARTIFACTS / VALID_DECISIONS / VALID_VERDICTS / SNAPSHOT_KEYS / REQUIRED_KEYS 系)+ **I/O なし純関数**(`canonical_hash(dict)` / `check_snapshot_trio(container, reviewers)`)+ **I/O あり**(`sha256_of_file`)を層分離して定義。`test_c3_contract.py` 新設
2. **3 消費者の import 置換**(1 コミット 1 種類・refactoring-guidance 準拠): (a) 定数 → (b) hash → (c) 三つ組照合 の順で arbiter.py / plan_package.py / c3prime_verify.py を置換
3. **sync 配布整合**: `sync-plugin-plangate.sh` の copy + delete 保護の両列挙へ `c3_contract.py` / `test_c3_contract.py` 追加(R-008 教訓)。ta-30 bundled 展開先の自立 PASS
4. **受理側の敵対レビュー**: c3prime_verify(承認境界の受理器)touch のため複数エージェント敵対レビュー 1 ラウンド以上(#889 教訓: 受理側は 1 ラウンドでは表層しか出ない)

### Out of scope(issue Non-goals verbatim)

- arbiter `plan_package_check`(入力ブロック検証)と c3prime_verify(record 検証)の**全統合**(対象が異なる — premature abstraction 回避。共通化は定数・hash・三つ組照合の 3 点に限定)
- `c3-prime-contract.md` の規則変更(実装集約のみ・契約は不変)
- 機能追加・挙動変更

## 受入基準(issue #896 verbatim・8 項目)

- AC-1: 契約定数(ARTIFACTS / VALID_DECISIONS / VALID_VERDICTS / SNAPSHOT_KEYS / REQUIRED_KEYS 系)が単一モジュール定義で、3 消費者が import 参照する
- AC-2: sha256 / canonical JSON hash が単一実装になる
- AC-3: snapshot 三つ組照合が I/O なし共通純関数になり、arbiter / c3prime_verify の両経路が同一実装を使う
- AC-4: 既存テスト全 green(振る舞い不変: test_arbiter 247 / test_plan_package 30 / test_c3prime_verify 12 / run-tests 411)
- AC-5: sync 列挙(copy + delete 保護)に新モジュールが追加され、sync 2 回目 no-op + ta-30 bundled 自立 PASS
- AC-6: arbiter は `c3_contract` の **I/O あり関数(`sha256_of_file`)を import / call しない**(arbiter は既存どおり ho-paths.md 読取・stdin 読取の自前 I/O は持つが、共通層からは I/O なし関数〔定数・`canonical_hash`・`check_snapshot_trio`〕のみを取り込む)
- AC-7: 偽造 record 群(test_c3prime_verify の手 mutate 系)の reject が不変
- AC-8: 敵対レビュー 1 ラウンド以上の disposition 記録

## 変更順序(refactoring-guidance 準拠・各ステップでグリーン維持)

| Step | 変更 | テスト確認項目 |
|------|------|---------------|
| 0 | ベースライン確立(4 系全 green の記録)+ **共通関数の返り値契約を先に確定**(下記 Unknowns 解決)| test_arbiter 247 / test_plan_package 30 / test_c3prime_verify 12 / run-tests 411 |
| 1 | 定数集約(c3_contract.py 新設・import 置換)| 定数値 byte 同一 assert + 全テスト不変 pass |
| 2 | hash ヘルパー統合(file sha256 + canonical JSON hash)| TC-09 系(1 byte 改変検出)+ 冪等テスト不変 |
| 3 | 三つ組照合の**5 キー整合ロジックのみ**共通純関数化(呼び出し側の分岐制御 = arbiter の tuple 部分成功 / c3prime_verify の即時 reject は各自に残す)| snapshot 不一致→BLOCKED(arbiter)/ reject(c3prime)の**判定結果**が両経路で不変 + 偽造 14 パターン不変。**メッセージ既存テストは decision/exit code のみ検証(実測: test_c3prime_verify のメッセージ assert 0 件)→ 判定結果ベースで振る舞い不変を確認** |
| 4 | sync 列挙 + ta-30 確認 | sync 2 回目 no-op / `git diff --quiet plugin/` / ta-30 pass |
| 5 | 敵対レビュー + disposition | 偽造 record 群 reject 不変の実測 |

## Notes from Refinement

- **非 HO・AI 実装可**: `scripts/ai-loop/**` は HO 対象外(PoC 隔離)。ただし **exec は EH-1 の plan+TASK 文脈要求により `PLANGATE_HOOK_TASK=TASK-0896` 専用セッションでメイン直実装が正規**(worker 委譲不可)
- **arbiter の設計原則維持**: arbiter は自前で ho-paths.md(L209 `read_text`)と stdin(L1179)を読むが、**裁定ロジックは入力 dict のみに依存**する純関数構成。c3_contract は I/O なし関数(定数・canonical_hash・check_snapshot_trio)と I/O あり(sha256_of_file)を層分離し、**arbiter は共通層から I/O なし関数のみ import**(arbiter に新たなファイル読取依存を持ち込まない)
- **bundled 自立の制約**: ta-30 が plugin 展開先での自立実行を検証 → import は同一ディレクトリ前提(`sys.path.insert` の既存パターン踏襲)
- **EPIC #870 への位置づけ(要正式化)**: #896 は EPIC #870 の Child Issues / 実装順序(#871→#872→#873→#874)に**含まれない P1 割込み**。#873(P0)着手前に #896 を挟むと優先度が逆転する。**位置づけの正式化を Step 0 前提**とする — (a) EPIC #870 本体の実装順に「#873 前提整理として #896(P1)」を追記する issue コメント、または (b) #896 を #873 と**並行**(別セッション・別ファイル集合)にして P0 を待たせない、のいずれかを **C-3 で確定**(決定権 = 人間)
- **タイミング**: #873/#874 の exec **前**に共通層を merge できれば delivery.py / run_evidence.py が最初から import できて最効果。ただし #873 は P0 のため、**#896 を P0 の前段ブロッカーにしない**(下記 Risks で遅延を定量化)

## Estimation Evidence

### Risks

| リスク | 検証手段 | Fallback |
|--------|---------|----------|
| 共通化で検証強度が意図せず変わる(受理器の挙動変化)| Step 0 ベースライン + 偽造 14 パターン + 411 テスト不変を各ステップで実測 | 差分検出時は該当ステップを revert(1 コミット 1 種類のため局所 revert 可)|
| **#896(P1)を #873(P0)の前に置くと P0 着手が遅延**(#896 は high-risk + 敵対レビュー 1 ラウンド以上を含み、完了まで数日規模)| 実装順は **C-3 で人間が確定**(#896 先行 or #873 と並行)。並行を選ぶ場合 c3_contract を先行 merge → 他が rebase | **P0 を待たせない場合は #873 を先行着手し、#896 は #873 exec と別セッションで並行**(触るファイルが重複しないため両立可能。#873=delivery.py 新設 / #896=既存 3 ファイルの import 置換)。逐次固定で P0 を待たせる判断は C-3 で明示承認した場合のみ |
| #873/#874 exec と #896 が同一ファイルを触りコンフリクト | 触る集合を確認: #873=delivery.py 新設(既存非改変)/ #896=arbiter/plan_package/c3prime_verify の import 行 → **重複は c3prime_verify のみ**(#873 が import 再利用する箇所)| c3_contract を先行 merge し #873 が rebase。または #896 完了後に #873 着手 |
| ta-30 bundled 自立が import 追加で壊れる | Step 4 で ta-30 実測 | import fallback(同 dir 探索)を c3_contract 側でなく消費者側の既存 sys.path パターンで担保 |
| **既存テストが decision/exit code のみ検証でメッセージ非検証**(実測: test_c3prime_verify のメッセージ assert 0 件・test_arbiter は decision/priority のみ)→ 共通化で理由文言が silent に変わっても検出されない | 共通関数は**理由文字列リスト**を返し呼び出し側で既存文言を組む契約に確定(Step 0)。判定結果(BLOCKED/reject/exit code)は既存テストが検証済みで不変を担保 | 理由コード enum + 呼び出し側マッピングで文言も固定。メッセージ回帰テストを Step 3 で追加 |

### Unknowns

- ~~`check_snapshot_trio` の返り値契約~~ → **Step 0 で確定(本 pbi で方針固定)**: 共通関数は**理由文字列リスト(空=OK)を返し、判定・終端制御は呼び出し側**(arbiter は tuple 部分成功へ変換 = リスト非空なら integrity_ok=False + reason=先頭 / c3prime_verify はリスト非空なら `_fail(先頭)`)。両呼び出し側の変換コード(各 5〜10 行)を plan の Work Breakdown に Step として計上
- test_c3_contract.py の粒度: **偽造 14 パターンは test_c3prime_verify.py に残置**(受理器の統合テスト)、test_c3_contract.py は**純関数の境界値**(キー欠落・型不一致・空値・三つ組不一致)のみを対象 → plan で最終確認

### Assumptions

- touch 見込み 9 ファイル: 新設 2(c3_contract + test)+ 変更 3(arbiter/plan_package/c3prime_verify)+ test 変更最大 3 + sync 1
- **Mode: high-risk**(定量: 変更ファイル数 9 = 6-15「高」・受入基準数 8 = 6-10「高」。定性: 受理器 touch = セキュリティ関連で「最低中」を上回る)。**人間 C-3 レビュー必須**(mode-classification フェーズ適用マトリクスで high-risk の C-3 は ○=実行)。**autonomous APPROVE 不可**の根拠は working-context.md「C-3 Autonomous APPROVE 判定マトリクス」の `Mode = high-risk → ❌ 不可(人間 C-3 必須)`(mode-classification 側でなくこちらが autonomous 可否の正本)
- 機能変更ゼロ・`refactor:` プレフィックス PR・振る舞い不変