diff --git a/docs/glossary.md b/docs/glossary.md index cddb8f0fc..332d429c9 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -328,9 +328,9 @@ NDF が提供する工程・承認ゲート・モード・ステップの語。d | 判断の項目 | `decisions` | 結果 JSON の decisions。スクリプトが決めたことと、その根拠を並べる | — | — | `docs/specifications/ndf-step-result-gaps-and-reading.md` | | 最終工程の記録 | — | 最終工程を終えたことを示す、GitHub に残った記録。配布は配布の記録の `段階:`、リリース後テストはその課題の行、振り返りは課題の本文の `振り返り: <コメントの URL>` の行のうち、指すコメントの識別子が記録の PR の最後の配布の記録のコメントの識別子以上のもの(その実行の振り返り)。sprint-close.py が閉じる前に確かめる | — | — | `plugins/ndf/skills/progress-tracking/SKILL.md` | | 閉じる予定の参照 | — | Pull Request の本文に `最終工程の後に閉じる: #<番号>` と書く形。GitHub は閉じず、sprint-close.py がスプリント課題として集める。宛先が既定ブランチの Pull Request では、NDF の手順が closing keywords をこの形へ置き換える | — | — | `plugins/ndf/skills/progress-tracking/SKILL.md` | -| 版の差し込み | — | 配布のプランが具体的な版の代わりに持つ `{release_version}`(出す版)と `{release_base}`(その接尾辞を外した版)。キューがステージを流す直前に版の記録の値で埋める | — | — | — | -| 版の記録 | — | 配布のプランの状態ディレクトリの `version.json`。キューが決めた、または確かめた版と、決めた根拠と、決め直しの履歴を持つ。流し直しはこれを読む | — | — | — | -| 版の決め直し | — | 開発版の流し直しで、版の記録の版が自分の配布のマージより前に別のスプリントに出ていたとき、キューが次に空いている版を決め、差し込み前のプランの写しから埋め直すこと。明示の版と、自分の配布のマージの後には行わない | — | — | — | +| 版の差し込み | — | 前の版の NDF が組んだ配布のプランが、具体的な版の代わりに持つ `{release_version}`(出す版)と `{release_base}`(その接尾辞を外した版)。プランの型の移行関数が版の参照へ読み替える(#2344) | — | — | — | +| 版の記録 | — | 解決の控えのうち、版の参照の行。決めた・確かめた版と、決めた根拠と、決め直しの履歴を持ち、流し直しはこれを読む。前の版の NDF が書いた `version.json` は移行関数でこの行へ読み替える(#2344) | — | — | — | +| 版の決め直し | — | 開発版の流し直しで、版の記録の版が自分の配布のマージより前に別のスプリントに出ていたとき、次に空いている版を決め、解決の控えの版の行をその版へ上書きし、前の版と理由を控えの履歴に残すこと。プランのファイルは書き換えない。明示の版と、自分の配布のマージの後には行わない(#2344) | — | — | — | | 明示の版 | — | `new sprint` / `new close` の `--version`・`--prod`、`new release` の `--version` で渡した版。プランを作る時点で決まり、キューはその版がまだ出ていないかだけを確かめる | — | — | — | | 実行の核 | `run_core` | 入口・ステップが必ず通る共通の実行の仕組み。入力の解決・記録・lease・型の確認・再試行と上限・結果の変換を引き受け、ステップは仕事の中身だけを書く(#2335) | — | — | — | | 契約の一覧 | `contract_registry` | エントリポイントごとの入力(引数・環境変数・.ndf/ の設定のキー・標準入力)・出力・書く状態・終了コードと、受け渡しのファイル・設定・環境変数の書き手と読み手と置き場を持つ、リポジトリで 1 つの宣言 | — | — | `docs/specifications/ndf-entrypoint-contracts.md` | @@ -393,6 +393,11 @@ NDF が提供する工程・承認ゲート・モード・ステップの語。d | 作業の識別子 | — | 複数のセッションを 1 つの作業として結ぶ鍵。起動の引数かスプリントに含まれる課題番号の組(#829 #830) | — | — | `docs/specifications/ndf-context-restart-policy.md` | | 費用の見込み | — | 判断の帯で、続けた場合と再起動した場合の次の工程までの費用を、記録から較正した中央値で比べた値 | — | — | `docs/specifications/ndf-context-restart-policy.md` | | 引継ぎの区間 | — | conductor が引継ぎ文書を書き始める前に記録した「引継ぎ開始」の時点から、そのセッションの最後の行まで | — | — | `docs/specifications/ndf-context-restart-policy.md` | +| プランの参照 | — | プランが値の代わりに持つ、値の取り先の名前(次に空いている開発版・課題のリリース記録を置いた PR・前のプランが出した PR・承認したコミットと照らすブランチの先端)。流す直前に実行の核が値へ解決する(#2344) | — | — | — | +| 解決の記録 | — | 1 回の実行がプランの参照ごとに解決した値と出所。実行の記録の行として残り、プランのファイルへは書き戻さない(#2344) | — | — | — | +| 承認時の値 | — | 承認ゲートで人か MVV 判定が承認資料で見た値(承認したコミット・版)。流す直前に解決した値と照らし、食い違えば流さずに人へ戻す(#2344) | — | — | — | +| 解決の控え | — | 流し直しでも同じ値を使う参照(版)の解決した値と決め直しの履歴を、プランの状態ディレクトリに残すファイル(`refs.json`)(#2344) | — | — | — | +| プランのハッシュ | — | プランの型で読んだ中身を、キーを並べ替えた JSON にして取った sha256。完了したプランが変わったかを測る(#2344) | — | — | — | | 呼び手 | — | 関数かエントリポイントを呼ぶ、テストの外のファイル(Skill の本文・雛形・hook・CI・ほかのスクリプト)。テストだけが呼ぶものは呼び手 0 と数える | — | — | — | | 残す関数の一覧 | — | 関数の棚卸し(uvx vulture)が呼び手 0 と見るが、vulture に見えない呼ばれ方などで残す関数を、理由の種類と理由と定義のあるファイルを 1 行ずつ添えて持つ一覧。置き場は scripts/ndf-vulture-keep.txt。構造チェックの例外リストとは別のもの | — | — | — | diff --git a/docs/glossary/glossary.json b/docs/glossary/glossary.json index bdd8b8390..620f61027 100644 --- a/docs/glossary/glossary.json +++ b/docs/glossary/glossary.json @@ -4814,19 +4814,19 @@ { "term": "版の差し込み", "context": "ndf-workflow", - "meaning": "配布のプランが具体的な版の代わりに持つ `{release_version}`(出す版)と `{release_base}`(その接尾辞を外した版)。キューがステージを流す直前に版の記録の値で埋める", + "meaning": "前の版の NDF が組んだ配布のプランが、具体的な版の代わりに持つ `{release_version}`(出す版)と `{release_base}`(その接尾辞を外した版)。プランの型の移行関数が版の参照へ読み替える(#2344)", "source": "" }, { "term": "版の記録", "context": "ndf-workflow", - "meaning": "配布のプランの状態ディレクトリの `version.json`。キューが決めた、または確かめた版と、決めた根拠と、決め直しの履歴を持つ。流し直しはこれを読む", + "meaning": "解決の控えのうち、版の参照の行。決めた・確かめた版と、決めた根拠と、決め直しの履歴を持ち、流し直しはこれを読む。前の版の NDF が書いた `version.json` は移行関数でこの行へ読み替える(#2344)", "source": "" }, { "term": "版の決め直し", "context": "ndf-workflow", - "meaning": "開発版の流し直しで、版の記録の版が自分の配布のマージより前に別のスプリントに出ていたとき、キューが次に空いている版を決め、差し込み前のプランの写しから埋め直すこと。明示の版と、自分の配布のマージの後には行わない", + "meaning": "開発版の流し直しで、版の記録の版が自分の配布のマージより前に別のスプリントに出ていたとき、次に空いている版を決め、解決の控えの版の行をその版へ上書きし、前の版と理由を控えの履歴に残すこと。プランのファイルは書き換えない。明示の版と、自分の配布のマージの後には行わない(#2344)", "source": "" }, { @@ -5431,6 +5431,36 @@ "meaning": "conductor が引継ぎ文書を書き始める前に記録した「引継ぎ開始」の時点から、そのセッションの最後の行まで", "source": "docs/specifications/ndf-context-restart-policy.md" }, + { + "term": "プランの参照", + "context": "ndf-workflow", + "meaning": "プランが値の代わりに持つ、値の取り先の名前(次に空いている開発版・課題のリリース記録を置いた PR・前のプランが出した PR・承認したコミットと照らすブランチの先端)。流す直前に実行の核が値へ解決する(#2344)", + "source": "" + }, + { + "term": "解決の記録", + "context": "ndf-workflow", + "meaning": "1 回の実行がプランの参照ごとに解決した値と出所。実行の記録の行として残り、プランのファイルへは書き戻さない(#2344)", + "source": "" + }, + { + "term": "承認時の値", + "context": "ndf-workflow", + "meaning": "承認ゲートで人か MVV 判定が承認資料で見た値(承認したコミット・版)。流す直前に解決した値と照らし、食い違えば流さずに人へ戻す(#2344)", + "source": "" + }, + { + "term": "解決の控え", + "context": "ndf-workflow", + "meaning": "流し直しでも同じ値を使う参照(版)の解決した値と決め直しの履歴を、プランの状態ディレクトリに残すファイル(`refs.json`)(#2344)", + "source": "" + }, + { + "term": "プランのハッシュ", + "context": "ndf-workflow", + "meaning": "プランの型で読んだ中身を、キーを並べ替えた JSON にして取った sha256。完了したプランが変わったかを測る(#2344)", + "source": "" + }, { "term": "持ち主の契約", "context": "ndf-worktree", diff --git a/issues/issue-2344-design-decisions.md b/issues/issue-2344-design-decisions.md new file mode 100644 index 000000000..b0ec3ad0d --- /dev/null +++ b/issues/issue-2344-design-decisions.md @@ -0,0 +1,142 @@ +# #2344 の設計: 決定の記録 + +設計文書の本体は [issue-2344-design.md](issue-2344-design.md) にある。要求の未決のうち、参照の書き方は決定 1、流し直しの判定は決定 7、 +版の記録の置き場は決定 3 が受ける。参照へ移す設定のキーの一覧は、設定を参照にしない(決定 4)ため要らない。 + +## 決定の記録 + +### 決定 1: 新しい種類の値を同じ規則で解決できるよう、参照はプランの型の `refs` 欄に名前つきで宣言し、ステップからは `{ref:<名前>}` で指す + +今の差し込みは、種類ごとに次の 3 つを持つ。 + +- 書き方(`{release_version}`・`{queue_record}`・`{queue_pr:<名>}`・`QUEUE_PRS`) +- それを探す正規表現 +- 埋める関数 + +種類を足すたびに 3 つを足すことになる。解決できないときの扱いも関数ごとに違った(`{queue_pr:<名>}` は PR が無ければ `0`、`{queue_prs:<名>}` は止める)。 +宣言を 1 つの欄に集めれば、プランの型が宣言の形を確かめ、参照の部品は宣言の `kind` で種類のモジュールを引くだけになる。ステップの中の書き方は +文字列の中の `{ref:<名前>}` と数の欄の `{"ref": "<名前>"}` の 2 つだけにする。 + +文字列の差し込みを種類ごとに続ける形は採らない。上の 3 つが種類ごとに増え、新しい種類が既存の規則に乗らないためである。 + +根拠: Value 7 / Value 5(MVV 版 5) + +### 決定 2: プランが変わったのが人の編集か解決かを区別できるよう、解決した値はプランのファイルへ書き戻さず、メモリ上のステップにだけ入れる + +今のキューは差し込みを埋めたプランでファイルを上書きする。そのため #2349 では、完了の後にプランが変わったのか差し込みが埋まったのかを判定できなかった。 +その結果、完了したプランを流し直した。書き戻さなければ、プランのファイルは組んだエントリポイントと人だけが書く。変わったことがそのまま人の編集を意味する。 +解決した値は実行の記録の `ref` 行に残り、どの実行がどの値で流したかは記録から追える。 + +埋めたプランを書き戻し、別に差し込み前のコピー(`version-plan.json`)を持つ形は採らない。コピーとプランの 2 つが正になるためである。 +決め直しの途中で落ちたときの戻し方(今の `ensure_filled`)も要る。 + +根拠: Value 7 / Value 8(MVV 版 5) + +### 決定 3: 流し直しでも同じ版を使えるよう、版の解決は状態ディレクトリの解決の控え(`refs.json`)に残し、`version.json` は移行関数で読む + +実行の記録は 1 回の起動ごとのファイルで、30 日で消える(#2335 の決定 6)。版はプランの寿命の間ずっと同じ値である必要がある(受け入れ条件 5)。 +実行の記録だけでは、流し直しで前の版を引けない。そこでプランの状態ディレクトリに、流し直しでも同じ値を使う参照の控えを 1 つのファイルで置く。 +今その性質を持つのは版だけである。控えは参照の名前ごとの行で持ち、種類を足しても置き場を増やさない。決め直しは前の値を `history` へ追記し、過去を失わない。 + +`version.json` をそのまま残す形は採らない。同じ性質の参照を足したときに種類ごとのファイルが増えるためである。 +控えを持たず実行の記録だけに置く形は、上の寿命の理由で採らない。 + +根拠: Value 7 / Value 8(MVV 版 5) + +### 決定 4: 範囲を起きた不具合に根拠のあるものに収めるよう、設定の値と予算は参照にせず組んだ時点の値で持ち、組んだ時点の設定とも照らさない + +この柱が参照にするのは、版数・記録の PR・前のプランの結果・承認したコミットと照らす先端の 4 種類である。版数と記録の PR は、プランを組んだ後に値が変わって +止まった事例がある(#2164・#2230・#2354)。前のプランの結果と先端の照合は、今も同じキューの中で流す直前に解決している手当てで、解決の規則を 1 か所にするために同じ形へ載せる。 +子の課題 4 件(#2230・#2164・#2349・#2354。要求の「子の課題」の表)に、`.ndf/` の設定から読んだ値か予算の秒が組んだ後に変わったことを原因とするものは無い。 +そのため設定の値と予算は今と同じく、プランを組むエントリポイントが組んだ時点の値で書く。設定を変えたときにプランへ反映するには、今と同じくプランを組み直す。 + +設定(導入の確認のコマンド・配布のテンプレート・本番のブランチなど 11 キー)と予算を参照の仕組みに載せる形は採らない。 +組んだ時点の設定の値をプランへ控え、流す前に今の値と照らして違えば止める形も採らない。どちらも起きた不具合に根拠が無いためである。 +また、設定の契約・プランの型・プランの実行の順序に欄と判定を足すことになり、この柱の承認資料の量が増える(利用者の範囲の指示、2026-11-05)。 + +根拠: Value 2 / Value 3 / Value 4(MVV 版 5) + +### 決定 5: 利用者が明示した版を AI が置き換えないよう、明示した版は版の参照の `explicit` に値のまま入れる + +`--version` で渡した版は、利用者か conductor がその版で流すと決めたものである。参照にして流す時点の版へ置き換えると、明示した決定を黙って上書きする。 +一方で明示の版にも確かめることがある。#2164 でも明示の版は決め直さず、既出なら止める。承認ゲート 2 の後は承認した版とも照らす(決定 6)。 +そこで `--version` の値は版の参照の宣言の `params.explicit` に入れる。参照の部品は値を置き換えずに、既出の判定と承認した版との照合を通す。解決の記録の出所は `plan:explicit` である。 + +明示の版を参照にせずリテラルで持つ形は採らない。既出の判定と承認した版との照合を通る経路が無くなるためである。 + +根拠: Vision / Value 2(MVV 版 5) + +### 決定 6: 承認ゲート 2 で人が見た版と流す版を照らせるよう、承認資料に「承認した版」の行を足し、本番のエントリポイントはエントリポイントの契約の `refs` で参照の部品を通る + +今の承認資料は承認したコミットの行だけを持つ。版は、承認ゲート 2 の通過の後に系列の版の記録が変わったか(`series_changed`)でしか照らせない。 +承認資料に「承認した版」を書けば、人が見た版と解決した版をそのまま比べられる。照合は承認したコミットと同じく `approved: gate-2` を付けた参照で行う。 +食い違えば `gate` で人へ戻す。今の `series_changed` は `stopped` である。承認した値との食い違いは人が判断することなので `gate` にする(受け入れ条件 4)。 +本番のエントリポイントはプランからも手動でも呼ばれる。そのため照合をエントリポイントの契約の `refs` に置く。核はエントリポイントごとに同じ参照の部品で照らす。照らす関数は `lib/approved_commit.py` の 1 か所にする。 + +照合をプランのステップの側だけに置く形は採らない。手動で本番のエントリポイントを呼んだときに照合が抜けるためである。 +「承認した版」の行が無い旧い承認資料は、`series_changed` の照らし方で読む(次の正式版まで)。 + +根拠: Value 2 / Value 7(MVV 版 5) + +### 決定 7: 完了したプランの流し直しを中身の変更だけで判定できるよう、プランの同一性はプランの型で読んだ中身を正規化した JSON の sha256 で測る + +決定 2 でプランのファイルは解決で変わらなくなる。ただし `jq` などで整形し直すと、キーの並びと空白だけが変わる。 +ファイルのバイト列の sha256 では、中身の同じプランを「変わった」と判定して人へ戻すことになる。型で読んだ値をキーを並べ替えた JSON にしてから取る。そうすれば、意味の変わった編集だけを拾える。 +ハッシュは実行の記録の `start` 行とキューの done の項目に持ち、再開とキューの `--resume` が今のハッシュと照らす。 +前のハッシュが無い旧い done では、完了したプランを流さない(受け入れ条件 7 の前半)。 + +ファイルの sha256 は上の理由で採らない。流し直しを完了の結果だけで止め、変わっても知らせない形も採らない。#2349 の「流し直すかを知らせて止まる」に反するためである。 + +根拠: Value 3 / Value 8(MVV 版 5) + +### 決定 8: 飛ばすはずのプランが開始のステップで流れないよう、実行条件は参照の解決より先に、開始のステップによらず 1 回だけ評価する + +実行条件は「このプランを流すか」の判定で、どのステップから始めるかと関係しない。今は開始のステップを渡すと評価を飛ばす(`engine.py:219`)。 +そのため #2349 で取り込み済みのプランが流れた。参照より先に評価すれば、飛ぶプランは版を照会しない。キューが条件を先に打つ手当て(`_release_skipped_by_condition`)も要らなくなる。 + +開始のステップが実行条件の後ろにあるときだけ評価する形は採らない。実行条件はステップではなくプランに付くため、前後の判定に意味が無い。 + +根拠: Value 7 / Value 5(MVV 版 5) + +### 決定 9: 前のプランの結果をキューの外からも同じに解決できるよう、前のプランの結果の参照は状態ディレクトリから読む + +今の `{queue_pr:<名>}` などは、次の 2 つの経路を順に見る。 + +1. キューが持つ前のステージの結果 +2. 無ければ、同じディレクトリのプランの状態ディレクトリ + +2 つの経路は同じ報告(`report.md` と `work/`)を読むため、後者だけにしても値は変わらない。経路を 1 つにすると、参照の部品はキューに依らない。 +単独で流すとき・再開するとき・キューで流すときに同じ値になる。 +キューの前のステージ全体の PR(今の `QUEUE_PRS`)は、キューが done の隣に書く一覧から前のステージのプランを引く。 + +キューが結果を参照の部品へ渡す形は採らない。キューの外から流したときに別の経路が要り、2 つの経路が食い違いうるためである。 + +根拠: Value 7 / Value 6(MVV 版 5) + +### 決定 10: 結果を読む側を壊さないよう、参照で止まったプランのキューの項目は `unfilled: true` を保つ + +次の読む側は、`unfilled` で「差し込みを埋められずに流さなかった」プランを数える。 + +- 開発ループ(`lib/sprint_loop.py:481`・`lib/sprint_loop_gate2.py:243`) +- キューの要約(`queue.py:340`) + +参照で止まったプランは結果が「止まった」になるが、`unfilled` を外すと読む側の数え方が変わる。結果 JSON のキーは足すだけで消さない(#2335 の移行の表)。 +そのため、参照で止まった項目にも `unfilled: true` を付け、旧い版の `流さなかった` の項目も読む。 + +`unfilled` を外し、読む側を同じ Pull Request で `止まった` の判定に寄せる形は採らない。旧い版のキューが書いた done を新しい版の開発ループが読むと、数え方が食い違うためである。 + +根拠: Value 6 / Value 1(MVV 版 5) + +### 決定 11: 開発ループのスプリントが版の取り合いで止まらないよう、`loop sprint` の `--dev` を任意にし、省いたら版を渡さない + +#2164 で `new sprint` は `--version` を省けば配布の時点に版を決める。開発ループは `--dev` を必須にして `--version` を必ず渡すため、この経路を選べない(#2354)。 +`--dev` を省いたときと渡したときの振る舞いは次のとおりである。 + +| `--dev` | 振る舞い | +| --- | --- | +| 省く | `new sprint` に `--version` を、`sprint-state.py init` に `--dev` / `--prod` を渡さない。周回の版は、配布の答えを書く `settle_release` が今と同じく `versions.prod` に入れる | +| 渡す | 今と同じ明示の版のプランになり、呼び方を変えた利用者の手を止めない | + +`--dev` を廃止する形は採らない。明示の版で回したい利用者(決定 5)の手段が無くなるためである。 + +根拠: Vision / Value 2(MVV 版 5) diff --git a/issues/issue-2344-design.md b/issues/issue-2344-design.md new file mode 100644 index 000000000..150cde6a6 --- /dev/null +++ b/issues/issue-2344-design.md @@ -0,0 +1,467 @@ +# プランの参照: プランを作り直さずに版・記録の PR を流す直前の値で使い、承認時の値と食い違えば止まる(#2344) + +## 目的 + +- **今どうなっているか**: プランは版と記録の PR を、組んだ時点の値か差し込みで持つ。差し込みはキューが流す前に埋めるが、埋めた値でプランのファイルを上書きし、差し込みの種類ごとに別の関数が解決する。`--from` で始めると実行条件を見ずに流れ(#2349)、開発ループのスプリントは明示の版しか持てない(#2354) +- **誰に効くか**: NDF でスプリントを回す conductor と利用者。並行するスプリントの配布の後にプランを作り直す手当て(#2164 の再発 2 回)と、完了したプランの流し直し(#2349 の 24 分・$0.89)が無くなる +- **変えると何が成り立つか**: プランは値の取り先(参照)を持ち、参照の部品がステップの直前に 1 つの規則で解決・記録・照合する。解決できない・承認時の値と食い違うときは、推定の値で進まずに `stopped` か `gate` で返る + +## 適用範囲 + +- **働く範囲**: NDF の配布物のうち次の 3 つ。配布先のリポジトリでも同じに働く。`factory/` のプランは含まない + - プランを組むエントリポイント(`supervise.py new sprint` / `new close`、`sprint-state.py loop sprint`) + - プランを流す処理(キュー・プランの実行・再開のエントリポイント) + - 配布の本番のエントリポイント +- **プロジェクトごとに違うもの**: 参照の値は各リポジトリの origin の版とタグ・GitHub の Pull Request・プランの状態ディレクトリから読む。版の規則に要る値(プラグインの名前・チャネル)は参照の宣言の `params` が持ち、ai-plugins の値を部品に置かない +- **参照にしないもの**: `.ndf/` の設定から読んだ値と予算の秒は、今と同じく組んだ時点の値でプランに書く。組んだ時点の設定と流す時点の設定も照らさない(決定 4) +- **当たるモード**: `standard`。#2335 の柱 P5 で、柱 P6(#2339 データの契約)・P8(#2341 入力の契約)・P3(#2342 持ち主の記録)のマージの後に実装する + +## あるべき姿の根拠 + +| 根拠 | 種類 | 何を示すか | +| --- | --- | --- | +| 利用者の指示(2026-11-01)「異常系処理とログ出力、異常時の回復処理が統一されていないから起こる不具合・課題が多い。1 件ずつ対処してもきりがない」「出力だけでなく入力もそろえるべき」 | 利用者の指示の原文 | 値の解決を種類ごとの手当てでなく 1 つの仕組みに寄せること | +| 差し込みを解決する関数が `supervise_lib/queue_fill.py` に 5 つ(`_fill_prs`・`_fill_states`・`_fill_work`・`_fill_record`・`fill_release_version`)と `supervise_lib/queue.py` に 1 つ(`fill_queue_prs`)ある。埋めた値は `fill_queue_pr`・`fill_queue_prs`・`_Fill.ensure_filled` の 3 か所でプランのファイルへ上書きされる(`write_text_atomic(Path(plan), …)` と `write_text_atomic(self.path, …)`) | 実測(2026-11-04 の develop のコード) | 解決の規則が 6 か所に分かれ、プランが変わったのが人の編集か解決かを区別できないこと | +| `supervise_lib/engine.py:219` の `if self.plan.get("実行の条件") and not start:` | 実測(同上) | 開始のステップを渡すと実行条件が評価されないこと(#2349 の 1 つ目の原因) | +| `lib/sprint_loop_cli.py:97` の `s.add_argument("--dev", required=True)` と `lib/sprint_loop.py:294` の `"--version", dev` | 実測(同上) | 開発ループが版を参照にできないこと(#2354 の原因) | + +要求と受け入れ条件は #2344 の本文にある。コピーは [issue-2344-requirements.md](issue-2344-requirements.md) にある。 +この文書は「どう作るか」だけを扱う。決定の記録は [issue-2344-design-decisions.md](issue-2344-design-decisions.md) にある。 +上位の設計は [issue-2335-design.md](issue-2335-design.md) である。実行の核・参照の部品(`lib/refs.py`)・不変条件 I14・ドメインイベント E5 はそれに従う。 + +## ドメインモデル + +### コンテキスト + +| コンテキスト | 何の語が 1 つの意味に決まるか | +| --- | --- | +| NDF の開発ワークフロー(`ndf-workflow`) | プランの参照・解決の記録・解決の控え・承認時の値・プランのハッシュ・版の記録 | +| リリース(`ndf-release`) | 承認したコミット・承認資料(語の意味は変えない。承認資料に「承認した版」の行を足す) | + +**リリースが供給者、開発ワークフローが顧客の関係(顧客 / 供給者)。** 参照の部品は承認資料の形(承認したコミットと承認した版の行)と +`lib/approved_commit.py` の比較を、リリースが決めたとおりに読む。参照の部品は承認資料を書かない。 + +### 集約 + +| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | +| --- | --- | --- | --- | --- | +| プラン | プランを組むエントリポイントと人(実行の間は誰も書かない) | プランのファイル | ステップ | 参照の宣言・実行条件 | +| 解決の控え | 参照の部品(`lib/refs.py`) | プランの状態ディレクトリ | 控えの行(参照の名前ごと) | 解決した値・出所・決め直しの履歴 | +| 実行 | 実行の核(#2335) | 実行の識別子 | 実行の記録の行 | 解決の記録の行・プランのハッシュ | + +**集約の間は識別子で参照する。** 解決の控えはプランを状態ディレクトリのパスで、実行の記録の `ref` 行は控えを参照の名前で指す。 +1 回の解決が書き換える集約は「解決の控え」だけで、実行の記録へは追記だけを行う。プランは解決で書き換えない。 + +### 不変条件 + +| # | 集約 | 条件 | 破れたときの扱い | +| --- | --- | --- | --- | +| I1 | プラン | プランのファイルは参照の解決で書き換わらない。解決した値はメモリ上のステップにだけ入る | 書き込みの経路が無い(解決の部品はプランのパスを書き口として受け取らない) | +| I2 | 実行 | 参照からステップに入る値は、解決した参照・控えの値・明示の版・旧い形の差し込みのどれかの出所を持ち、1 値 1 行で解決の記録に残る。参照を通らない値(設定・予算・引数で明示した上限とブランチ)はプランの値そのもので、解決の記録に行を持たない | 記録へ書けなければ `stopped`(#2335 の I3) | +| I3 | 実行 | 参照の解決は、持ち主の記録を取った後・実行条件の評価の後に行う。プランの上位の欄(`作業場所`・`branch`)の参照は、worktree を作る前に 1 回だけ解決する。ステップの参照は、ステップを流す直前に解決する。開始のステップ(`--from`)によらず実行条件を先に評価する。版の決め直しの事前判定も実行条件の評価の後に行い、飛ばすプランでは版を照会しない | 順序はプランの実行の 1 つの関数が持つ(下の処理の流れ) | +| I4 | 実行 | 解決できない参照を使うステップは流れない。推定の値・空・`0`・`unknown` で埋めない | `stopped`・終了コード 6、`gaps` に `ref:<名前>` と理由 | +| I5 | 実行 | 承認時の値を持つ参照は、解決した値と承認時の値が食い違えば流れない。承認したコミットの後に許したもの(この版の配布の Pull Request)だけが入ったときは食い違いとみなさない | `gate`・終了コード 10(`EXIT_GATE`)、`items` に承認時の値と実行時の値 | +| I6 | 解決の控え | 版の参照は控えがあれば控えの版を使う。決め直すのは #2164 の規則に当たるときだけで、前の版と理由を控えの `history` と解決の記録に残す | 規則に当たらない決め直しの経路が無い(決め直しは `release_version` の判定だけが起こす) | +| I7 | 実行 | 完了したプランを `--from` なしに流し直さない。完了した実行の記録のプランのハッシュと今のハッシュが違えば流さない | `gate`・終了コード 10(`EXIT_GATE`)、`next` に `--from` で流し直すコマンド | + +### ドメインイベント + +要求の E1〜E9 を引き継ぐ。右端は #2335 の番号である。 + +| # | イベント | 発生元 | 受け手 | #2335 | +| --- | --- | --- | --- | --- | +| E1 | プランを参照つきで組んだ | プランを組むエントリポイント | プランのファイル | — | +| E2 | 承認時の値(承認したコミット・承認した版)を承認資料に記録した | 承認ゲート 1・2 の承認(`approved_commit.record_approval`) | 参照の部品(E6) | — | +| E3 | 持ち主の記録を取った | 実行の核(柱 P3) | プランの実行 | E4 | +| E4 | 実行条件を評価した | プランの実行 | プランの実行(飛ばすなら E5〜E8 を行わずに完了) | E5 | +| E5 | 参照を解決した | 参照の部品 | プランの実行・解決の控え | E5 | +| E6 | 解決した値を承認時の値と照らした | 参照の部品 | プランの実行(食い違えば `gate`) | E5 | +| E7 | 解決の記録を書いた | 参照の部品(`ctx.log("ref", …)`) | 実行の記録 | E5 | +| E8 | ステップを流した | プランの実行 | 実行の核 | E7 | +| E9 | 同じプランを流し直し・再開した | 再開のエントリポイント(柱 P3)・キュー | プランの実行(ハッシュの照合) | E13 | + +### 用語 + +| 用語 | 意味 | 用語集への反映 | +| --- | --- | --- | +| プランの参照 | 要求の用語のとおり | 追加済み(要求) | +| 解決の記録 | 要求の用語のとおり。実行の記録の `ref` 行である | 追加済み(要求) | +| 承認時の値 | 要求の用語のとおり | 追加済み(要求) | +| 解決の控え | 流し直しでも同じ値を使う参照(版)の解決した値と決め直しの履歴を、プランの状態ディレクトリに残すファイル(`refs.json`) | 追加(`ndf-workflow`) | +| プランのハッシュ | プランの型で読んだ中身を、キーを並べ替えた JSON にして取った sha256。完了したプランが変わったかを測る | 追加(`ndf-workflow`) | +| 版の記録 | 解決の控えのうち、版の参照の行 | 意味の変更(`ndf-workflow`) | +| 版の差し込み | 前の版の NDF が組んだプランの `{release_version}`・`{release_base}`。移行関数が版の参照へ読み替える | 意味の変更(`ndf-workflow`) | + +## 機能一覧 + +| # | 機能 | 誰が使うか | +| --- | --- | --- | +| F1 | プランを組むエントリポイントが、版・記録の PR・前のプランの結果を参照で書く | `new sprint` / `new close`・開発ループ | +| F2 | ステップの直前に参照を解決し、解決の記録を書いてメモリ上のステップへ値を入れる | プランの実行 | +| F3 | 版の参照の解決を控えに残し、流し直しで同じ版を使い、#2164 の規則のときだけ決め直す | 配布のプラン | +| F4 | 承認時の値と照らし、食い違えば `gate` で返す | 本番の配布(プランからも手動の起動からも) | +| F5 | 実行条件を開始のステップによらず先に評価する | プランの実行・再開 | +| F6 | 完了したプランの流し直しをプランのハッシュで判定する | 再開のエントリポイント・キュー | +| F7 | 旧い形のプラン(差し込み・値を書き込んだもの)を移行関数で読み、値をそのまま流す | 前の版の NDF が組んだプラン | + +## 構成要素 + +| 要素 | 責務 | 新設 / 変更 | +| --- | --- | --- | +| 参照の部品(`lib/refs.py`) | 参照の宣言の型・種類の表・`resolve(ctx, plan, step)`。種類ごとの解決を遅れて読み、解決の記録と解決の控えを書く。外へ書くのは控えと記録だけ | 新設 | +| 参照の種類(`lib/ref_kinds/`) | 種類ごとに 1 モジュール(`version`・`record_pr`・`plan_result`・`tip`)。値か失敗の理由を返し、終了コードと出力を持たない | 新設(`queue_fill.py` の 5 つの関数と `queue.fill_queue_prs` を移す) | +| 版の判定(`lib/release_version.py`) | 次に空いている版・既出の判定・自分の配布のマージの有無(変えない) | 変えない | +| 承認したコミット(`lib/approved_commit.py`) | 「承認した版」の行の読み書きを足す。`release_lib/approval.py` の `check_approved`・`release_pr_allowed` を移し、`tip` の種類と配布の本番のエントリポイントが同じ関数を呼ぶ | 変更 | +| 承認資料を書くエントリポイント(`release-steps.py approval-facts`) | 承認資料に「承認した版」の行を書く | 変更 | +| プランの型(柱 P6 の `lib/model/` のプラン) | `refs`(参照の宣言)の欄を持つ。旧い差し込みを参照へ読み替える移行関数を持つ | 変更(柱 P6 の型に欄を足す) | +| 解決の控えの型(`lib/model/` の `refs-store`) | 控えの形と、`version.json` を控えへ読み替える移行関数 | 新設 | +| プランの実行(`supervise_lib/engine.py`) | ステップの直前に実行条件 → 参照の解決の順に呼ぶ。開始のステップで実行条件を飛ばさない | 変更 | +| キュー(`supervise_lib/queue.py`・`supervise_lib/flow.py`) | 差し込みを埋めない。done の項目にプランのハッシュを書き、参照で止まったプランに `unfilled: true` を付ける。ステージの前に worktree を作る耐久ステップ(`flow.worktrees_step`)は、上位の欄(`作業場所`・`branch`)に参照を持つプランの worktree を作らない。そのプランの worktree は、プランの実行が `resolve_head` で解決した値で作る(実行条件を持つプランと同じ扱い) | 変更(`fill_stage` の埋める処理と `_release_skipped_by_condition`・`fill_queue_prs` を消す。`worktrees_step` の対象の条件を変える) | +| 差し込みの解決(`supervise_lib/queue_fill.py`) | — | 削除(`FINISHED`・`named`・`results_named` は `ref_kinds/plan_result.py` へ移す) | +| 再開(`supervise_lib/queue_resume.py`・柱 P3 の `supervise.py resume`) | 完了したプランをプランのハッシュで照らし、違えば `gate` で返す | 変更 | +| プランを組むエントリポイント(`supervise_lib/release_templates.py`・`delivery_templates.py`・`sprint_routes.py`・`sprint.py`・`sprint_waves.py`・`design_merge.py`、`lib/parallel_release.py`。差し込みを書く 7 ファイル) | 差し込みの代わりに参照を書く。明示した版は版の参照の `explicit` に書く。設定・予算・引数で明示した上限とブランチは今と同じく値で書く | 変更 | +| 開発ループ(`lib/sprint_loop_cli.py`・`lib/sprint_loop.py`) | `--dev` を任意にし、省いたら `new sprint` に `--version` を、`sprint-state.py init` に `--dev` / `--prod` を渡さない | 変更 | +| エントリポイントの契約(`plugins/ndf/contracts/entrypoints/`) | 配布の本番のエントリポイントの行に `refs` を足す。`tip` の参照(承認したコミットと照らす)と、引数の `--version` を `params.explicit` に入れ `approved: gate-2` を付けた `version` の参照(承認した版と照らす)の 2 つである | 変更 | + +### 要素の関係 + +```mermaid +graph TD + NEW[プランを組むエントリポイント] -->|参照と形を書く| PL[プランのファイル] + LOOP[開発ループ] --> NEW + Q[キュー] --> EN[プランの実行] + RS[再開] --> EN + EN -->|読む| PL + EN --> RF[参照の部品] + RF --> RK[参照の種類] + RK --> RV[版の判定] + RK --> AC[承認したコミット] + RF -->|書く| ST[解決の控え] + RF -->|追記| LG[実行の記録] + AF[承認資料を書くエントリポイント] --> AC + PROD[配布の本番のエントリポイント] --> RF +``` + +図に描かない要素は次のとおりである。 + +| 要素 | 図に描かない理由 | +| --- | --- | +| プランの型と解決の控えの型 | 参照の部品とプランの実行が読み書きに使う | +| エントリポイントの契約 | 核が配布の本番のエントリポイントの `refs` を読む | +| 差し込みの解決 | 消す | + +### 置き場所 + +```text +plugins/ndf/ +├── contracts/ +│ └── entrypoints/*.json # 変更: 配布の本番のエントリポイントに refs +└── scripts/ + ├── lib/ + │ ├── refs.py # 新設 + │ ├── ref_kinds/ # 新設: version・record_pr・plan_result・tip + │ ├── model/ # 変更(柱 P6): プランの refs、refs-store + │ ├── approved_commit.py # 変更 + │ ├── sprint_loop.py / sprint_loop_cli.py # 変更 + │ ├── parallel_release.py # 変更: 差し込みの代わりに参照 + │ └── release_version.py # 変えない + ├── release_lib/approval.py # 変更: check_approved を approved_commit へ移す + └── supervise_lib/ + ├── engine.py / flow.py / queue.py / queue_resume.py # 変更 + ├── queue_fill.py # 削除 + └── release_templates.py など supervise_lib のプランを組む 6 ファイル # 変更 +``` + +## 構造 + +```mermaid +classDiagram + class RefSpec { + +name + +kind + +params + +approved + +sticky + } + class Resolution { + +name + +value + +source + +approved_value + } + class RefFailure { + +name + +reason + +status + } + class StoreRow { + +name + +value + +how + +observed + +history + } + RefSpec ..> Resolution : 解決する + RefSpec ..> RefFailure : 解決できない・食い違う + RefSpec ..> StoreRow : sticky なら読み書き +``` + +- **`resolve_head(ctx, plan) -> (plan, list[Resolution]) | RefFailure`**: プランの上位の欄(`作業場所`・`branch`)が使う参照を解決する。値を入れたプランのコピーをメモリ上で返す。実行条件の評価の後、worktree を作る前(`ensure_worktree` の前)に 1 回だけ呼ぶ。worktree の準備とその後のステップは、このコピーを使う。キューがステージの前に worktree を作る `worktrees_step` は `resolve_head` を呼ばないため、上位の欄に `{ref:` を含むプランを対象から外し、生の値(`release/v{ref:version}` など)で worktree とブランチを作らない。控えの無い版の最初の決定はこの解決で行い、決め直しはこの解決では起こさない(決め直しは耐久ワークフローを始める前に判定する。下の「耐久ワークフローの世代」) +- **`resolve(ctx, plan, step) -> (step, list[Resolution]) | RefFailure`**: ステップが使う参照だけを解決する。値を入れたステップのコピーを返す。`plan` は `resolve_head` が返したコピーである。`RefFailure.status` は `stopped` か `gate` +- **`plan_digest(plan) -> str`**: プランのハッシュ。プランの型で読んだ値を `json.dumps(sort_keys=True, ensure_ascii=False, separators=(",", ":"))` にして sha256 を取る +- **種類のモジュールの形**: `resolve(ctx, spec, plan) -> Resolution | RefFailure`。GitHub と origin を読む種類は、その種類を使うステップでだけ読み込まれる + +## データ構造 + +### 参照の宣言(プランの `refs`) + +プランの型の欄 `refs` は、参照の名前 → 宣言の対応である。ステップからは次の形で指す。 + +| 指す場所 | 書き方 | +| --- | --- | +| ステップの文字列の中 | `{ref:<名前>}`(版の起点は `{ref:<名前>.base}`) | +| プランの上位の欄(`作業場所`・`branch`) | `{ref:<名前>}`。版を省いた配布のプランは `branch` に `release/v{ref:version}` を、`作業場所` に版を含むパスを持つ | + +| 種類 | `params` | 解決した値 | `sticky` | 解決できないとき | +| --- | --- | --- | --- | --- | +| `version` | `plugin`・`channel`(`dev` / `prod`)・`level`・`series`(本番が版を読む開発版のプランの名前)・`explicit`(明示の版。任意) | `{version, base}` | 真 | origin の版かタグを読めない・明示の版が既出・系列の版の記録が無い(#2164 の `unreadable` / `taken` / `no_series`) | +| `record_pr` | `issues`(課題の番号の並び) | リリース記録を置いた PR の番号 | 偽 | 記録の PR が見つからない(`record_find` の理由) | +| `plan_result` | `plan`(名前)か `plans: "earlier"`(キューの前のステージ)・`field`(`pr` / `state` / `prs` / `approved`)・`optional` | PR の番号・状態ディレクトリのパス・PR の並び・承認したコミット | 偽 | 名前の一致するプランの完了した結果が無い(`optional` の `state` だけ空を返す) | +| `tip` | `branch`・`approved_from`(承認したコミットを返す参照の名前)・`allowed_pr`(許すものの参照の名前) | ブランチの先端の SHA | 偽 | 先端を読めない | + +`approved`(任意)は承認時の値を持つ参照に付け、`gate-2` を書く。`version` では承認資料の「承認した版」と、`tip` では承認したコミットと +`compare_approved` で照らす。承認資料に「承認した版」の行が無い旧い資料では、#2164 の `series_changed`(承認ゲート 2 の通過の後に系列の版の記録が変わった)で照らす。 + +**設定の値・予算の秒・引数で明示した上限とブランチは参照にしない。** プランを組むエントリポイントが今と同じく値で書く(決定 4)。 + +**`--version` で明示した版は、版の参照の `params.explicit` に値のまま入れる。** 参照の部品は明示の版を置き換えず、版の参照の解決で既出の判定(#2164 の `taken`。既出なら `stopped`・終了コード 6)と、`approved` を付けた参照では承認した版との照合を必ず通す。解決の記録の出所は `plan:explicit`、控えの `how` は `explicit` になる(決定 5)。 + +### 解決の控え(`<状態ディレクトリ>/refs.json`) + +| キー | 型 | 空を許すか | 意味 | +| --- | --- | --- | --- | +| `kind` / `version` | str / int | 許さない | `refs-store` / 1(柱 P6 の型の版) | +| `rows.<名前>.value` | object | 許さない | 解決した値(版なら `{version, base}`) | +| `rows.<名前>.how` | str | 許さない | `decided`(次に空いている版)/ `explicit` / `series` | +| `rows.<名前>.observed` | object | 許さない | 決めたときに読んだ origin の版とタグ(`Observed.as_dict()`) | +| `rows.<名前>.at` | str | 許さない | 決めた時刻(UTC の ISO 8601) | +| `rows.<名前>.history` | list | 空のリストを許す | 決め直しごとに `{from, to, reason, existing, observed, left, at}` を追記する。消さない | + +**時系列は追記で持つ。** 決め直しは `value` を上書きするが、前の値は `history` に残る。`version.json` は移行関数で `rows.version` へ読み替え、 +書くのは `refs.json` だけにする。`version-plan.json`(差し込み前のプランのコピー)は、プランを書き換えないため作らない。 + +### 実行の記録の `ref` 行 + +#2335 の実行の記録の行(`event: "ref"`)の `fields` は次の形である。1 つの参照の解決ごとに 1 行。 + +| キー | 型 | 空を許すか | 意味 | +| --- | --- | --- | --- | +| `step` | str | 許さない | ステップの id。上位の欄の解決は `head` | +| `name` / `kind` | str | 許さない | 参照の名前と種類。プランの値は `kind: "literal"` | +| `source` | str | 許さない | `ref`(解決した)/ `store`(控えの値)/ `plan:explicit` / `plan:legacy` | +| `value` | any | 許す | 解決した値 | +| `outcome` | str | 許さない | `resolved` / `redecided` / `unresolved` / `mismatch` / `match` | +| `approved` | object | 許す | 承認時の値(照らしたときだけ) | +| `reason` | str | 許す | 解決できない・食い違う・決め直した理由 | + +実行の記録の `start` 行の `fields` に `plan` と `plan_digest` を足す。キューの done の項目にも `plan_digest` を足す(キーは足すだけ)。 + +### CRUD + +| 機能 | プランのファイル | 解決の控え | 実行の記録 | 承認資料 | done | +| --- | --- | --- | --- | --- | --- | +| F1 組む | C | — | — | — | — | +| F2 解決 | R | R | C(`ref`) | — | — | +| F3 版 | R | C・U | C | — | — | +| F4 承認時の値 | R | R | C | R | — | +| F5 実行条件 | R | — | C | — | — | +| F6 流し直し | R | — | R(`start`) | — | R・C | +| F7 旧い形 | R | R(`version.json`) | C | — | — | + +書く相手が 2 つ以上ある機能は F3(控えと記録)である。記録は追記だけのため、控えを書いてから記録を書く順にする。 +落ちたら次の解決が控えを読み、同じ版の `ref` 行を書き直す。 + +## 入出力の契約 + +| 項目 | 約束 | +| --- | --- | +| `sprint-state.py loop sprint` | `--dev` を任意にする。渡せば今と同じく `new sprint --version <開発版>` を組む。省けば `--version` を渡さず、周回の `versions` は配布の答え(`settle_release`)で埋まる | +| 解決できないときの結果 | ステップを流さず、プランの結果は「止まった」、結果 JSON は `status: "stopped"`・終了コード 6、`gaps` に `ref:<名前>`、`summary` に参照の名前と理由 | +| 承認時の値と食い違うときの結果 | `status: "gate"`・終了コード 10(`EXIT_GATE`)、`items` に `{kind: "ref-mismatch", name, approved, actual, reason}` | +| 完了したプランが変わったときの結果 | 再開とキューの `--resume` は `status: "gate"`・終了コード 10(`EXIT_GATE`)、`items` に `{kind: "plan-changed", plan, digest_done, digest_now}`、`next` に `supervise.py resume <実行の識別子> --from <プラン>=<ステップ>` | +| キューの done | 参照で止まったプランの項目は `result: "止まった"`・`unfilled: true`・`reason`。旧い版の `result: "流さなかった"`・`unfilled: true` の項目も読む | +| 承認資料 | 「2. 承認の判断に使うもの」の表に `| 承認した版 | <版> |` の行を足す。承認の記録(`record_approval`)は今と同じ | +| 旧い形のプラン | `{release_version}`・`{release_base}`・`{queue_record}`・`{queue_pr:<名>}`・`{queue_state(?):<名>}`・`{queue_prs:<名>}`・`{queue_approved:<名>}`・`QUEUE_PRS` は、プランの型の移行関数が参照へ読み替える。受けるのは次の正式版まで | + +## 処理の流れ + +### プランを流す順 + +```mermaid +sequenceDiagram + participant C as キュー・再開 + participant E as プランの実行 + participant F as 参照の部品 + C->>E: プランを流す(開始のステップ) + E->>E: 持ち主の記録を取る(E3、柱 P3) + E->>E: 実行条件を評価(E4) + alt 飛ばす + E-->>C: 完了(実行条件に当たらない) + end + E->>F: resolve_head(作業場所・branch) + alt 解決できない・食い違う + F-->>E: RefFailure + E-->>C: stopped・6 か gate・10 + end + F-->>E: 値を入れたプランのコピー + E->>E: worktree を作る(コピーの作業場所・branch) + loop ステップごと + E->>F: resolve(ステップ) + alt 解決できない + F-->>E: RefFailure(stopped) + E-->>C: stopped・6 + else 食い違う + F-->>E: RefFailure(gate) + E-->>C: gate・10 + end + F-->>E: 値を入れたステップ + E->>E: ステップを流す(E8) + end + E-->>C: 結果 +``` + +### 参照を解決する中身 + +`resolve` の中(E5〜E7)は次のとおりである。 + +```mermaid +sequenceDiagram + participant F as 参照の部品 + participant K as 参照の種類 + participant S as 解決の控え + participant R as 実行の記録 + F->>S: sticky の参照の控えを読む + F->>K: 種類ごとに解決(E5) + K-->>F: 値か理由 + F->>F: 承認時の値と照らす(E6) + F->>S: 決めた・決め直した版を書く + F->>R: ref 行を書く(E7) +``` + +図に描かない要素は次のとおりである。 + +| 要素 | 図に描かない理由 | +| --- | --- | +| 版の判定と承認したコミット | 参照の種類の中で呼ぶ | +| プランの型と解決の控えの型 | 読み書きの型 | +| プランを組むエントリポイント・開発ループ | 流す前の E1 で働く | +| 承認資料を書くエントリポイント | E2 で働く | +| エントリポイントの契約と配布の本番のエントリポイント | 下の「流れの決まり」の 2 つ目の項目 | +| 差し込みの解決 | 消す | + +### 流れの決まり + +- **実行条件を評価する関数は 1 つにする。** 呼ぶのは E4 と、版の決め直しの事前判定(下の「耐久ワークフローの世代」の 2)だけである。今の `engine.py` の `and not start` を外す。`queue._release_skipped_by_condition`(版を照会する前に条件を打つ手当て)を消す。実行条件が版の参照より先に評価されるため、配布不要のプランは版を照会しない +- **配布の本番のエントリポイントは、エントリポイントの契約の `refs` で同じ参照の部品を通る。** プランから呼ばれても手動で呼ばれても、核の E5 で `tip` の参照を解決して承認したコミットと照らし、引数の版を `params.explicit` に入れた `version` の参照を解決して承認資料の「承認した版」と照らす。手動で `release --version <版B> --approval <版A の承認資料>` と呼んだときも、コミットの照合とは別に版の照合で `gate` になる。`check_approved` を呼ぶ箇所はこの 1 つになる。関数は `release_lib/approval.py` から `lib/approved_commit.py` へ移す +- **前のプランの結果は、キューが渡す結果でなく状態ディレクトリから読む。** 今の `results_named` が承認ゲートの後に単独で流すときに使う経路を、唯一の経路にする。その経路は、同じディレクトリのプランの `report.md` と `work/` を読む。`plans: "earlier"` はキューが done の隣に書く一覧(`.plans.json` の `stages`)から前のステージのプランを引く +- **版の参照の解決は #2164 の判定を順に呼ぶ。** 控えが無ければ `release_version.observe` → `decide_dev` か `taken` で決めて控えを書く。控えがあれば `taken` → `own_released` で確かめ、当たれば `decide_dev` で決め直す(明示の版は止める)。本番は明示の版か系列の開発版の控えの `base` を使う +- **版の決め直しを確かめるのは、1 回の起動で耐久ワークフローの接頭辞を決める前の判定だけである。** 判定の中身は下の「耐久ワークフローの世代」の 2 にある。その後の解決とステップは、その起動で決めた版をそのまま使う。1 回の起動の中で旧版と新版が混ざらない + +### 版を決め直したときの退避 + +**決め直したら、旧版の成果物を退避して新版の先頭から流す。** 次の順で行い、プランのファイルは書き換えない。 + +1. 状態ディレクトリのファイルのうち、ステップの記録・結果・`work/` を `superseded/v<旧版>/` へ移す。移さないのは、解決の控え(`refs.json`)・退避先(`superseded/`)・状態の実体のコピー(`plan.json`)である +2. 解決の控えの `rows.version` を新版で上書きする。`history` には `{from, to, reason, existing, observed, left, at}` を追記する。`left` は旧版の `作業場所`・`branch`・退避先である。旧版の worktree とブランチは消さずに残す(消すのは戻せない操作のため) +3. `ref` 行を `outcome: "redecided"` で書く +4. 上位の欄を新版で解決し直し、新しい作業場所と `branch` で worktree を作り、最初のステップから流す。旧版の済んだステップの記録は 1 で退避したため、再利用されない + +1 の後で落ちても、済んだステップの記録が無いため、次の起動は最初のステップから流れる。控えを書く前に落ちれば控えの判定で同じ決め直しへ戻り、書いた後に落ちれば新版で流れる。 + +### 耐久ワークフローの世代 + +**決め直しは、耐久ワークフローの回復より先に判定し、決め直した世代は新しい耐久ワークフローとして先頭から始める。** 耐久ステップの記録(`prepare`・`run_step` の出力)は状態ディレクトリの外の耐久の記録にあり、上の「版を決め直したときの退避」の 1 では消えない。プランのファイルを書き換えないため、プランの中身から作る耐久ワークフローの接頭辞(`flow.prefix_of`)も変わらない。そのため次のとおり決める。 + +1. 世代は、解決の控えの `rows.version.history` の件数である(控えが無ければ 0)。`flow.prefix_of` は、プランの鍵・中身の鍵・開始のステップに世代を加えた `plan-<パス鍵>-<中身鍵>-<世代>-<開始>` を返す。中身鍵は、プランのファイルのバイト列でなく、プランのハッシュ(`plan_digest`)から取る。`run` の `flow.plan_prefix` とキューの `admit_step` は、どちらもプランをプランの型で読んで `plan_digest` を取り、それを `prefix_of` へ渡す(今の `key_hash(body, 8)` を使わない)。キーの並びと空白だけを変えたプランは接頭辞が変わらず、E9 の判定(I7)と耐久の記録の引き当てが同じ結論になる。世代も、2 つとも状態ディレクトリの控えから読む。読んだ世代は `prefix_of` へ渡す。決め直すと世代が 1 つ増え、接頭辞が変わるため、旧版の耐久ステップの記録は引かれない +2. 控えに版の行があるプランは、接頭辞を決める前に、決め直しの事前判定を次の順で行う。`run` は耐久の記録を開く(`durable.launch`)前、キューは耐久の記録を開く(`launch_queue`)前に、その起動で流すプランへ行う。前の起動の判定は根拠にせず、その起動の実行条件で確かめる + 1. E9 の照合(下の「流し直しの判定」)を先に行う。前の結果が完了で、ハッシュが同じか前のハッシュが無く、`--from` も無いプランは流さないため、判定の対象から外す。外したプランでは origin も PR も照会しない。完了でハッシュが違うプランも `gate` で人へ戻すため外す + 2. 実行条件を評価する(E4 と同じ関数)。飛ばすなら、版を照会せず、判定も退避もしない + 3. 「流れの決まり」の版の参照の判定(`taken` → `own_released`)を流す。当たれば「版を決め直したときの退避」の 1〜3 を行ってから接頭辞を決める + + 飛ばすプランは、控えの版のまま耐久ワークフローへ渡し、耐久ワークフローの中の E4 が同じ結果(完了)を返す。控えが無いプランの最初の版は、これまでどおり `resolve_head` の中で決め、世代は 0 のままである +3. 世代が変わったら、前の世代の接頭辞の途中の耐久ワークフローを、新しい世代を始める前に止める。`run` は `durable.launch` の `keep` へ新しい接頭辞を渡し、回復の前に止める。キューは接頭辞を耐久ステップ `admit_step` の中で決める。キューの再開(`launch_queue` の `keep=None`)では、その出力が再生されて旧世代の接頭辞が使われる。そのため、2 の事前判定で 1 つでも決め直したプランがあれば、`launch_queue` を `fresh` で開く。途中の耐久ワークフローは子ごと止まり、`admit_step` が世代の増えた接頭辞を計算し直す。完了したプランは、完了の記録が接頭辞で引けるため流し直されない。決め直しが無ければ、これまでどおり続きから再開する +4. 耐久ワークフローの中の `resolve_head` は、決め直しを起こさない。控えの版をそのまま使う(控えが無いときの最初の決定を除く)。決め直しの経路は 2 の判定の 1 つだけになる + +2 の判定の後・退避の 1〜3 の途中で落ちたときの次の起動は 2 通りになる。控えを書く前なら、2 の判定が同じ結果を返す。控えを書いた後なら、世代の増えた控えから新しい接頭辞で始まる。どちらでも旧版の耐久ステップの記録は再利用されない。 + +開始のステップ(`--from`)を渡した起動で決め直しが要るときは流さない。旧版の済んだステップを前提にできないためである。結果は `stopped`・終了コード 6 で、`gaps` に `ref:version`、`next` に `--from` を外したコマンドを出す。 + +### 流し直しの判定(E9) + +```mermaid +stateDiagram-v2 + [*] --> 照合: 再開・--resume + 照合 --> 流す: 前の結果が完了でない + 照合 --> 流す: --from がある + 照合 --> 流さない: 完了でハッシュが同じ + 照合 --> 流さない: 完了で前のハッシュが無い(旧い done) + 照合 --> 人へ戻す: 完了でハッシュが違う + 流す --> [*] + 流さない --> [*] + 人へ戻す --> [*] +``` + +`--from` は柱 P3 の決まりどおり常に勝つ(#2342 の I9)。流し直す手段は `--from` の 1 つで、「人へ戻す」の結果はそのコマンドを `next` に出す。 + +## 非機能の実現方式 + +| 大項目 | 要求の条件 | 実現方式 | 確かめ方 | +| --- | --- | --- | --- | +| 性能・拡張性 | 参照を持たないエントリポイントの起動に足す時間は #2335 の上限 150 ms の内側。GitHub と origin はその参照を持つステップだけが読む | 核はエントリポイントの契約に `refs` が無ければ参照の部品を読み込まない。種類のモジュールは使う種類だけを読み込む | 参照を持たないエントリポイントの `--help` を前後で 10 回ずつ測り、差の中央値を実装の Pull Request に書く。GitHub を呼ぶ関数を差し替え、参照を持たないプランで呼ばれないことをテストで見る | +| 可用性 | 解決できない・食い違うときは待ち続けずに返る | 種類のモジュールは待ちを持たず、外部の呼び出しは上限の部品(柱 P4)の 1 回の上限で打ち切る | 解決できない・食い違うテストが `stopped` / `gate` で返る | +| 運用・保守性 | 解決した・解決できなかった・照合した・食い違ったを実行の識別子つきで書く | `ref` 行の `outcome` に 5 つの値 | 種類ごとのテストで `ref` 行の `outcome` を見る | +| 移行性 | 旧い形のプランは値のまま流す。`version.json` は解決の記録として読む。2 つの Pull Request をそれぞれ revert すれば戻る | 移行関数(プランと控え)。Pull Request は、参照の仕組み(参照の部品・種類・プランの実行・キュー)と、エントリポイントの移し(プランを組むエントリポイント・開発ループ)の 2 つに分ける | 前の版が組んだプランと `version.json` の実物で流すテスト | +| セキュリティ | 参照が解決する値は秘密を含まない。C1・C2 を変えない | 種類は `version`・`record_pr`・`plan_result`・`tip` の 4 つで、版・PR の番号・状態ディレクトリのパス・コミットの SHA だけを返す。設定と秘密を読む種類を持たない | 種類の表(データ構造の節)を設計レビューで見る | +| システム環境 | 版の比較は `versions`(extras)。無い環境では `stopped`・69 | `version` の種類だけが `versions` を読み込み、読めなければ `deps` の判定で返す | extras を外した環境で版の参照を持つプランを流すテスト | + +## 決定の記録 + +決定の記録は [issue-2344-design-decisions.md](issue-2344-design-decisions.md) にある。 + +## テスト設計 + +| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | +| --- | --- | --- | +| 1 | 版を明示しない `new sprint`・`new close`・`loop sprint` が組むプランのファイルに、版数と記録の PR の番号が無く `refs` がある。設定の値と予算の秒は今と同じ値で入る | 組む時点に版を書く/記録の PR の番号を書く/旧い差し込み(`{release_version}`・`{queue_record}`)を書く | +| 2・I1・I2 | 流す前と後でプランのファイルの sha256 が同じ。ステップに入った値ごとに `ref` 行がある | 解決した値をプランへ書き戻す/`ref` 行を書かずに流す | +| 3・I4 | 記録の PR が無い・origin を読めない・前のプランの完了した結果が無い、のそれぞれでステップが流れず、`stopped` と `gaps` の `ref:<名前>` と理由が出る | 見つからない PR を `0` で埋める/読めない版を前の版で埋める | +| 4・I5 | 承認したコミットの後に許していないコミットがある・承認した版と解決した版が違う、で本番のステップが流れず `gate` と両方の値。許した配布の PR だけのときは流れる。本番のエントリポイントを手動で、承認資料と違う `--version` で呼ぶと、コミットが承認どおりでも `gate` | 照合を飛ばして流す/許した PR まで食い違いとする/エントリポイントの契約の `refs` を `tip` だけにする | +| 5・I6 | 同じプランを流し直すと控えの版を使う。#2164 の決め直しに当たるときだけ版が変わり、`history` と `ref` 行に前の版と理由が残る。決め直すと旧版のステップの記録が `superseded/v<旧版>/` へ移り、新版の作業場所で、世代の増えた新しい耐久ワークフローとして最初のステップから流れる。決め直した後に落ちて打ち直しても、旧版の耐久ステップの記録を引かない。キューを `--resume` で再開して決め直しが起きても、`admit_step` が世代の増えた接頭辞で流す | 流し直しのたびに次に空いている版を決める/決め直しで `history` を上書きする/旧版の済んだステップの記録を新版で再利用する/耐久ワークフローの接頭辞に世代を入れない/キューの再開で `admit_step` の旧い出力を再生する | +| 6・I3 | #2349 の再現の手順: 実行条件で飛ぶプランを `--from <プラン>=<ステップ>` で始めても飛ばされ、結果に飛ばした旨。実行条件は参照より先に評価され、飛ぶプランは版を照会しない。控えに版の行があるプランも、実行条件で飛ぶ起動では、決め直しの事前判定で版を照会せず、状態ディレクトリを退避しない。版を省いた配布のプランは、`run` でもキューでも、worktree を版の入った `branch` と作業場所で作る。キューで流したとき、`{ref:` を含む名前の worktree とブランチが作られない | `and not start` を戻す/参照を実行条件より先に解決する/事前判定を実行条件より先に流す/上位の欄を解決せずに worktree を作る/`worktrees_step` が上位の欄に参照を持つプランも生の値で作る | +| 7・I7 | #2349 の再現の手順: 完了したプランを含む done を `--resume` しても流れない。完了の後にプランを書き足すと `gate` と `--from` のコマンド。キーの並びだけを変えたプランは同じハッシュで、同じ耐久ワークフローの接頭辞になり流し直されない。`--resume` で流さない完了したプランでは、決め直しの事前判定が origin も PR も照会しない | ハッシュを見ずに流し直す/ファイルのバイト列でハッシュか接頭辞の中身鍵を取る/完了したプランにも事前判定を流す | +| 8 | #2354 の再現の手順: `loop sprint` を `--dev` なしで起動でき、組んだスプリントのプランが版の参照を持つ。`--dev` を渡せば版の参照の `explicit` に入り、既出の版なら `stopped`・6 | `--dev` を省いたときに既定の版を入れる/明示の版をリテラルで書いて既出の判定を飛ばす | +| 9 | #2164 の受け入れ条件 1〜10 と #2230 の受け入れ条件の既存のテストが通る。`supervise_lib/queue_fill.py` が無く、`queue.py` に `fill_` で始まる関数が無い | 差し込みの関数をキューに残して二重に解決する | +| 10・F7 | 前の版が組んだプラン(差し込みのもの・値を書き込んだもの)と `version.json` を持つ状態ディレクトリで流れ、`ref` 行の `source` が `plan:legacy` / `store` | 移行関数を外す/旧い差し込みを文字のまま流す | +| 11 | (手動)子の課題 4 件の再現の手順を本番へ出した版で通し、課題の本文の表に結果を書く | — | +| 12 | 参照を持たないエントリポイントとプランの振る舞いと結果が前と同じ。全体テスト `uv run --frozen --project . --all-extras pytest . -q -n 4` が前後で通る | 参照を持たないプランでも参照の部品が GitHub を読む | + +## 設計の結果 + +| 課題 | 扱い | 取り込み先 | 触るファイル | +| --- | --- | --- | --- | +| #2344 | 実装する | — | `plugins/ndf/scripts/lib/refs.py`、`plugins/ndf/scripts/lib/ref_kinds/`、`plugins/ndf/scripts/lib/model/`、`plugins/ndf/scripts/lib/approved_commit.py`、`plugins/ndf/scripts/lib/sprint_loop.py`、`plugins/ndf/scripts/lib/sprint_loop_cli.py`、`plugins/ndf/scripts/release_lib/approval.py`、`plugins/ndf/scripts/release-steps.py`、`plugins/ndf/scripts/supervise_lib/`、`plugins/ndf/scripts/tests/`、`plugins/ndf/scripts/lib/parallel_release.py`、`plugins/ndf/contracts/entrypoints/`、`docs/glossary/glossary.json`、`docs/glossary.md` | + +子の課題 4 件(#2230・#2164・#2349・#2354)は表に載せない。表の行はスプリントの Pull Request で閉じる。 +子の課題は、本番へ出した版で再現の手順を通してから、閉じるか残すかを決める(受け入れ条件 11)。 +#2349 の 3 つ目の事象は、差分の無い実装のプランで `pr` が終了コード 1 になることである。 +これは柱 P1(#2337)の結果の契約の問題で、この柱では直さない(要求の「含まない」)。 + +## 未確認のまま残ること + +| 項目 | 内容 | +| --- | --- | +| 柱 P3・P6・P8 の設計 | 2026-11-04 の時点で P6(#2339)と P8(#2341)の設計文書は無く、P3(#2342)は設計 Pull Request 2480 が開いた状態である。この設計はそれらの語(`ctx.resolve`・`ctx.log`・プランの型・入力の宣言・`supervise.py resume`)にだけ依る。形が変わったら実装の開始時に読み直す(要求の未決 5) | +| プランのキーの名前 | #2335 の決定 4 でプランの日本語のキーは英語へそろう。この文書の `実行の条件`・`版` などは今の名前で書いた。英語の名前は柱 P6 が決める | +| 承認した版の行の書き手 | 承認資料を書く `approval-facts` が、配布の本番のプランの版の参照をどの時点で読むか(承認ゲート 2 の提示の前に版が決まっていること)は、提示のプランの流れで確かめる | +| 差し込みを書くエントリポイントの網羅 | 構成要素の 7 ファイルは、`plugins/ndf/scripts/supervise_lib/` と `lib/` を差し込みの書き方(`{release_version}`・`{release_base}`・`{queue_record}`・`{queue_pr`・`{queue_state`・`{queue_approved`・`QUEUE_PRS`)で検索して当たったもののうち、プランを組むものである(2026-11-05 の develop)。`plugins/ndf/scripts/` 直下のエントリポイントがプランを組んで差し込みを書いていないかは、実装の開始時に同じ検索で確かめる | +| 並行するスプリントの版の取り合い | 2 つのスプリントが同時に次に空いている版を決めたときは、#2164 の `own_released` の確かめで後から決め直す。版の参照を持ち主の記録で排他にするかは測っていない(#2354 の起票者の推定 2 点目) | diff --git a/issues/issue-2344-requirements.md b/issues/issue-2344-requirements.md new file mode 100644 index 000000000..fdf5c17dd --- /dev/null +++ b/issues/issue-2344-requirements.md @@ -0,0 +1,208 @@ +# #2344: プランは値でなく参照を持ち、実行の直前に解決して照合する + +正は課題の本文(#2344)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 + +大方針 #2335(異常系・ログ・回復とデータ・入出力の形を 1 つの実行の核へ集める)の柱の 1 本(柱の記号 P5「実行時点の解決」)。子の課題は、この柱の仕組みがそろえば解消する可能性があるものである。 + +## 依頼(原文) + +この課題の題と本文は、起票者(課題を書いた AI)が 2026-11-01 に大方針 #2335 の組み直しの記録として書いた文で、利用者の指示の原文ではない。この柱の元になった利用者の指示は、#2335 の「依頼(原文)」が引用した次の文である。 + +> 異常系処理とログ出力、異常時の回復処理が統一されていないから起こる不具合・課題が多い。1 件ずつ対処してもきりがない + +> 内部データ構造と出力のフォーマットの不統一の影響も大きそう + +> NDF は Skill の Script 化から発展してきたため、内部データ構造や入出力契約がほぼ整理されていない + +> 出力だけでなく入力もそろえるべき + +(書き手: 利用者、日付: 2026-11-01。#2335 の本文 https://github.com/devbasex/ai-plugins/issues/2335 から引用した) + +起票時の本文(書き手: 起票者、日付: 2026-11-01): + +> プランは版・記録の PR・宣言・予算・触るファイル・承認時の先端などを参照で持ち、実行の核がステップの直前に解決して記録する。解決できない・承認した値と食い違うときは流さずに止まる。m26-3 の #2230(記録の PR)と #2164(版数)を最初の事例とし、その形を全体へ広げる。 + +> 1. プランは版・記録の PR・宣言の値を参照で持ち、核が実行の直前に解決する。解決できなければ流さずに止まる +> 2. 承認ゲートで見た値(先端・版)と実行時の値の食い違いを核が照合し、食い違えば止まって人へ戻す +> 3. 子の課題それぞれを、閉じた(解消を確かめた)か残した(理由つき)かに分けて本文に記録する + +> 子の課題は 1 件ずつ直さない。柱の仕組みを入れた後に、子の課題の再現の手順を 1 回ずつ通し、解消したものを理由を添えて閉じる。設計は #2335 の設計(マイルストーン「28 根本解決 1: 設計と契約の棚卸し」)の後に、この柱ごとに行う。 + +子の課題の結び付け(書き手: 起票者、日付: 2026-11-01。課題のコメント): + +> 子として #2349・#2354 を結び付けました(スプリント lroot-2 の棚卸し)。 + +この要求の整理の指示(書き手: AI の起動指示、日付: 2026-11-04): + +> 人へ問わずに進め、決められない点は前提か未決として課題の本文へ書く。 + +範囲の指示(書き手: 利用者、日付: 2026-11-05。設計の承認ゲート 1 の答え): + +> 範囲を、起きた不具合に根拠のあるものに絞る: 版数・リリース記録の PR の直前の解決(#2164)・実行条件を先に評価(#2349)・流し直しの判定(#2349)・開発ループの版(#2354)。 +> 外すもの: 設定 11 キーと予算を同じ参照の仕組みに載せる部分と、形を決めた設定のハッシュ照合(受け入れ条件 1・11 も合わせて縮める)。 + +**解釈**: 利用者が求めているのは、プランを作った時点の値が流す時点で古くなって止まる型の不具合(大方針の原因の型 E「解決の時点」)のうち、起きた不具合に根拠のあるもの(版数・記録の PR・実行条件の評価・完了したプランの流し直し・開発ループの版)を、1 件ずつではなく仕組みで無くすことである。今は #2164 の版の差し込み(`{release_version}`)と #2230 の記録の PR の差し込み(`{queue_record}`)が、キューの中でそれぞれ別の手当てとして解決している。この柱は、その 2 つを実行の核の参照の部品(#2335 の設計の `lib/refs.py`)へ寄せ、同じキューの中で解決している前のプランの結果の差し込みと承認したコミットの照合も同じ形に載せる。設定の値と予算は参照にしない(利用者の範囲の指示)。 + +## 起票者の推定 + +題・本文のうち、上の利用者の原文に無く、起票者が推して足した条件。承認ゲート 1 で利用者が確かめるまで、利用者の決定として扱わない。S6〜S8 はこの要求の整理で足した。 + +| # | 条件 | 推した出所 | この要求での扱い | +| --- | --- | --- | --- | +| S1 | プランは版・記録の PR・宣言の値を、値でなく参照で持つ | 起票時の本文の受け入れ条件 1・#2335 の受け入れ条件 6 | 受け入れ条件 1。版と記録の PR だけを参照にし、宣言の値は含まない(利用者の範囲の指示、2026-11-05) | +| S2 | 実行の核がステップの直前(持ち主の記録を取った後)に参照を解決し、解決した値を記録する。解決できなければ流さずに止まる | 起票時の本文・#2335 の設計の不変条件 I14・ドメインイベント E5 | 受け入れ条件 2・3 | +| S3 | 承認ゲートで見た値(先端・版)と実行時の値の食い違いを核が照合し、食い違えば止まって人へ戻す | 起票時の本文の受け入れ条件 2 | 受け入れ条件 4 | +| S4 | 予算も参照で持つ | 起票時の本文「何をするか」・#2335 の原因の型 E の説明 | 含まない(利用者の範囲の指示、2026-11-05)。予算は今と同じく、プランを組む時点の秒で持つ | +| S5 | 触るファイルも参照で持つ | 起票時の本文「何をするか」 | 含まない。触るファイルは、プランを書いた側が宣言する「触る予定の範囲」で、プランの外の状態が変わって古くなる値ではない。触るファイルが絡む課題(#1811・#2037・#2165)は重なりの判定の粒度の問題で、柱 P3(#2342)が残す見込みとしている | +| S6 | 解決した値はプランのファイルへ書き戻さず、実行の記録にだけ残す(今のキューは差し込みを埋めたプランでファイルを上書きする) | #2349 の「完了の後にプランの JSON が変わったら流し直す」事象。プランを上書きすると、プランが変わったのが人の編集か解決かを区別できない | 受け入れ条件 2。誤ると、解決の記録と流し直しの判定(受け入れ条件 7)を作り直すことになるため、問わずにこの案を選んだ | +| S7 | 実行条件(プランの `実行の条件`)も、ステップの直前に評価する値として参照と同じ段で扱い、`--from` で始めても先に評価する | #2349 の「直したいこと」1 | 受け入れ条件 6 | +| S8 | 開発ループ(`sprint-state.py loop sprint`)が組むスプリントも、版を参照で持つ | #2354 の「起票者の推定」1 点目 | 受け入れ条件 8 | + +## 目的 + +- プランを作ってから流すまでの間に版・記録の PR が変わっても、プランを作り直さずに流せる状態にする(今は #2164 の 2 回の再発のように、conductor がプランを作り直すか版を書き換えた) +- 参照を解決できないとき・承認ゲートで人が見た値と食い違うときに、推定の値や古い値で黙って進まず、理由を添えて止まる状態にする +- 参照を解決する手当てを 1 か所にまとめ、新しい種類の値を足しても同じ規則で解決・記録・照合される状態にする(今は版・記録の PR・前のプランの結果が、キューの中で別々の関数として解決している) + +## 前提 + +- 前提 1: この柱の設計と実装は、#2335 の設計(`issues/issue-2335-design.md`・`issues/issue-2335-design-decisions.md`・`issues/issue-2335-design-pillars.md`)を正とする。参照の部品(`lib/refs.py`)・不変条件 I14・ドメインイベント E5 はそれに従う +- 前提 2: この柱の実装は、柱 P6(#2339 データの契約。プランの型)・P8(#2341 入力の契約)・P3(#2342 持ち主の記録)のマージの後に始める(`issues/issue-2335-design-pillars.md` の順序)。2026-11-04 の時点で 3 本とも未完了で、`plugins/ndf/scripts/lib/` に `run_core.py`・`refs.py`・`lease.py` は無い(`ls plugins/ndf/scripts/lib/` の結果に 3 つの名前が出ないことで確かめた)。設計は 3 本の設計文書を正として先に書いてよい +- 前提 3: 寄せる元の実装は次の 4 つである。どれも 2026-11-04 の develop にあり、キューがステージを流す直前に解決している + - 版の差し込み(`{release_version}`・`{release_base}`)と版の記録(`supervise_lib/queue_fill.py` の `fill_release_version`、`lib/release_version.py`) + - 記録の PR の差し込み(`{queue_record}`。`supervise_lib/queue_fill.py` の `_fill_record`) + - 前のプランの結果の差し込み(`{queue_pr:<名>}`・`{queue_state:<名>}`・`{queue_prs:<名>}`・`{queue_approved:<名>}`・`QUEUE_PRS`) + - 承認したコミットと先端の比較(`lib/approved_commit.py` の `compare_approved`。#815) +- 前提 4: 承認したコミットとの照合は、今の `compare_approved` と同じく「承認したコミットの後に、許したもの(この版の配布の Pull Request)だけが入っている」なら食い違いとみなさない +- 前提 5: 参照を持たない旧い形のプラン(この柱より前の版の NDF が作り、値を書き込んだプラン)は、書き込まれた値をそのまま使って流す。実行の記録にはその値の出所を「プランの値」と書く(#2335 の移行性。旧い版が書いた状態ファイルを読む受け入れ条件 10b と同じ扱い) + +## 対象範囲 + +含む: +- 参照の部品と、実行の核の参照の段(E4〜E7) +- 前提 3 の 4 つの実装を参照の部品へ寄せること(キューに残る差し込みの関数を消す) +- 実行条件を、`--from` で始めるときも含めてステップの直前に評価すること(#2349 の 1 つ目) +- 完了したプランの流し直しの判定を、解決の記録とプランの中身の照合で行うこと(#2349 の 2 つ目) +- 開発ループが組むスプリントの版を参照にすること(#2354) +- 子の課題 4 件の再現の手順を通し、閉じるか残すかを本文に記録すること + +含まない: +- 子の課題を 1 件ずつ直す Pull Request(即時修正の 4 条件を満たすものと本番を止めているものは除く) +- 触るファイルを参照にすること(起票者の推定 S5) +- 宣言の値(`.ndf/` の設定)と予算を参照にすること、プランを組んだ時点の設定の値と流す時点の値を照らすこと(利用者の範囲の指示)。どちらも今と同じく、プランを組む時点の値で持つ +- 差分の無い実装のプランで `pr` が終了コード 1 になること(#2349 の 3 つ目)。値の解決の時点の問題でなく、結果の契約(柱 P1 #2337)の問題である(受け入れ条件 11 で確かめて残す) +- 上限と再試行の値そのもの(柱 P4 #2343)と、持ち主の記録の取り方(柱 P3 #2342) +- 版の規則(次に空いている版の決め方・決め直しの条件)を変えること。#2164 の規則をそのまま使う +- `factory/` のプラン + +## ドメインイベント + +1 回のプランの実行のうち、参照の解決と照合に関わる部分を時間の順に並べる。右端の列は #2335 のドメインイベントの番号である。 + +| # | イベント | 引き金 | 失敗したとき | 順序の前提 | #2335 | +| --- | --- | --- | --- | --- | --- | +| E1 | プランを参照つきで作った | `supervise.py new sprint` / `new close`・`sprint-state.py loop sprint` | 参照の形がプランの型に合わない → 作らずに `stopped`(入力違い、終了コード 2) | — | — | +| E2 | 承認ゲートで人が見た値(承認したコミット・版)を承認の証跡に記録した | 承認ゲート 1・2 の承認 | 記録できない → 承認を成立させない(今の `approved_commit.record` と同じ) | E1 | — | +| E3 | ステップの起動で持ち主の記録を取った | キュー・`supervise.py run`・再開の入口 | 柱 P3 の扱い | E2(承認ゲートの後のステップ) | E4 | +| E4 | 実行条件を評価した | E3 | 条件のコマンドが判定できない → `stopped`。飛ばす条件に当たる → 参照を解決せずに完了 | E3。`--from` の有無によらない | E5 | +| E5 | 参照を解決した | E4 で流すと決まった | 解決できない(記録の PR が無い・版を読めない・前のプランの結果が無い)→ ステップを流さず `stopped` | E3(並行する実行と同じ版を取り合わない) | E5 | +| E6 | 解決した値を承認ゲートで見た値と照らした | E5 で、E2 の記録がある参照 | 食い違う → `gate`(人へ戻す) | E2・E5 | E5 | +| E7 | 解決した値と出所を実行の記録へ書いた | E5・E6 | 書けない → `stopped`(#2335 の I3) | E6 | E5 | +| E8 | ステップを流した | E7 | 柱 P1 の扱い | E7 | E7 | +| E9 | 同じプランを流し直し・再開した | 再開の入口・キューの `--resume` | プランの中身が前の実行の記録と違う、かつ前の実行が完了 → 流さずに `gate` | 前の実行の記録がある | E13 | + +洗い出しで移したもの: E1 で参照の形を決める型はプランの型(柱 P6)に置いた(前提 2)。E5 の「流し直しで前に解決した値を使い続けるか」は受け入れ条件 5 に置いた。E6 の「食い違いとみなさない差」は前提 4 に置いた。E9 の「プランの中身の同一性を何で測るか」は未決に置いた。 + +## 用語 + +| 用語 | 意味 | +| --- | --- | +| プランの参照 | プランが値の代わりに持つ、値の取り先の名前(例: 次に空いている開発版・この課題のリリース記録を置いた PR・前のプランが出した PR・承認したコミットと照らすブランチの先端)。流す直前に実行の核が値へ解決する。この文書で単に「参照」と書くときはこの意味である | +| 解決の記録 | 1 回の実行が解決した参照ごとの値と出所。実行の記録(#2335 の P2)の行として残る | +| 承認時の値 | 承認ゲートで人(か MVV 判定)が承認資料で見た値。承認したコミットと版を含む | +| 版の差し込み | 配布のプランが具体的な版の代わりに持つ `{release_version}`・`{release_base}`。この柱で参照の 1 種類になる | +| 版の記録 | 配布のプランの状態ディレクトリの `version.json`。この柱で解決の記録の 1 種類になる | +| 記録の PR | リリース記録(`## 配布の記録`)を置いた Pull Request | +| 承認したコミット | 承認ゲート 2 の承認資料が対象にした、ベースブランチの先端のコミットの SHA | +| 実行条件 | プランを流す前に打つコマンド(プランの `実行の条件`)。`skip_code` を返せば完了とする | +| 実行の核 | 入口・ステップが必ず通る共通の実行の仕組み(#2335 の `lib/run_core.py`) | + +## 受け入れ条件 + +- [ ] 1. `supervise.py new sprint`(版を明示しない)・`new close`・`sprint-state.py loop sprint` が組むプランのファイルは、版数と記録の PR の番号を値として持たず、参照で持つ。設定から読んだ値と予算の秒は、今と同じく組んだ時点の値で持つ。組んだプランのファイルを読むテストで確かめる +- [ ] 2. 参照は、ステップの直前(持ち主の記録を取った後)に実行の核が解決し、参照ごとの値と出所が実行の記録に残る。解決でプランのファイルは書き換わらない(流す前と後でファイルの sha256 が同じ) +- [ ] 3. 参照を解決できないとき(記録の PR が見つからない・origin の版かタグを読めない・前のプランの完了した結果が無い)は、ステップを流さず `stopped` で返り、結果 JSON に参照の名前と解決できなかった理由が出る。推定の値・空・`0`・`unknown` で埋めて流さない +- [ ] 4. 承認ゲート 2 の後に、承認したコミットの後へ許したもの以外のコミットが入った、または承認資料の版と解決した版が違うとき、本番のステップを流さず `gate` で返り、結果 JSON に承認時の値と実行時の値が並ぶ。承認したコミットの後に許したものだけが入ったときは流れる(前提 4) +- [ ] 5. 同じプランの同じステップを流し直すと、前の実行で解決した版を使う。ただし #2164 の版の決め直し(自分の配布のマージより前に別のスプリントがその版を出した)に当たるときは、#2164 の規則で決め直し、決め直した前の版と理由が解決の記録に残る +- [ ] 6. 実行条件を満たして飛ばすはずのプランを `--from <プラン>=<ステップ>` で始めても、そのプランは飛ばされ、結果に飛ばした旨が出る(#2349 の再現の手順で確かめる) +- [ ] 7. 完了したプランを含む done のファイルを `--resume` しても、完了したプランは流れない。完了の後にプランのファイルが変わっていたら、流さずに `gate` で返り、変わったことと流し直す手段を結果に出す(#2349 の再現の手順で確かめる) +- [ ] 8. `sprint-state.py loop sprint` は `--dev` を省いて起動でき、省いたとき組むスプリントのプランは版を参照で持つ。`--dev` を渡したときは今と同じく明示の版のプランになる(#2354 の再現の手順で確かめる) +- [ ] 9. #2164 の受け入れ条件 1〜10 と #2230 の受け入れ条件のテストが、参照の部品へ寄せた後も通る。前提 3 の差し込みを解決する関数が、キュー(`supervise_lib/queue_fill.py`・`supervise_lib/queue.py`)に残らない +- [ ] 10. 前提 5 の旧い形のプラン(値を書き込んだプラン)を流すと、書き込まれた値で流れ、実行の記録に出所「プランの値」が残る +- [ ] 11. 子の課題 4 件(下の「子の課題」の表)それぞれについて、再現の手順を 1 回通した結果(コマンドと出力の要点)と、閉じた(解消を確かめた)か残した(理由つき)かを、この課題の本文の表に記録する。閉じた課題には、確かめた結果へのリンクを添える +- [ ] 12. 参照を持たない入口とプランの振る舞いが変わらない。全体テストが柱のマージの前後で通る + +## 子の課題 + +受け入れ条件 11 で、柱の仕組みを入れた後に 1 件ずつ再現の手順を通す。「見込み」は要求の整理(2026-11-04)での見立てで、結論ではない。 + +| 課題 | 何が起きるか(要約) | 当たる受け入れ条件 | 見込み | +| --- | --- | --- | --- | +| #2230 | まとめのプランを組む時点でリリース記録の PR が決まらない | 2・3・9 | 解消(実装は 10.27.6 で入っている。寄せた後に受け入れ条件を通して閉じる) | +| #2164 | 作った時点の版数を持つプランが、並行するスプリントの配布の後に使えなくなる | 1・4・5・9 | 解消(同上) | +| #2349 | `--from` が実行条件を見ず、`--resume` が完了したプランを流し直し、差分の無い PR で止まる | 6・7 | 1 つ目と 2 つ目は解消。3 つ目(差分なしの `pr`)は残す見込み(対象範囲の「含まない」) | +| #2354 | 開発ループのスプリントが版を明示の版で持ち、並行するスプリントと版がぶつかると止まる | 8 | 解消 | + +## 非機能の条件 + +| 大項目 | 条件 | +| --- | --- | +| 性能・拡張性 | 参照を持たない入口の起動に足す時間は、#2335 の上限 150 ms(核全体)の内側に収まる。GitHub と origin を読む参照は、その参照を持つステップだけが読む(今と同じ) | +| 可用性 | 解決できないときと食い違うときは、待ち続けずに `stopped` か `gate` で返る(受け入れ条件 3・4) | +| 運用・保守性 | 解決した・解決できなかった・照合した・食い違ったを、実行の記録の行に実行の識別子つきで書く(受け入れ条件 2) | +| 移行性 | 移す対象: 前提 3 の差し込みと、プランを組む入口が書き込む版と記録の PR の番号。並走: 旧い形のプランは値のまま流す(受け入れ条件 10)。版の記録(`version.json`)は解決の記録として読む。切り戻し: 参照の段の Pull Request と入口を移す Pull Request を、それぞれ revert すれば前の形へ戻る | +| セキュリティ | 参照が解決する値は版・PR の番号・コミットの SHA・状態ディレクトリのパスで、秘密を含まない。認証・認可(C2)と秘密の扱い(C1)は変えない | +| システム環境 | 版の比較は今と同じ `versions`(optional extras)を使う。extras が無い環境では、核が `stopped`(終了コード 69)と足りない extras の名前を返す(#2335 の受け入れ条件 15) | + +## 影響 + +| 対象 | 影響 | +| --- | --- | +| 公開インタフェース | 変わる。`sprint-state.py loop sprint` の `--dev` が任意になる(渡せば今と同じ)。承認ゲートの後の食い違いは `gate` で返る。差し込みの書き方(`{queue_record}` など)は旧い形のプランを読むために次の正式版まで受ける | +| データ | 変わる。プランのファイルは解決で書き換わらなくなり、解決した値は実行の記録に残る。版の記録(`version.json`)の置き場と形は設計で決める(未決)。利用者の `.ndf/` は変えない | +| 既存の振る舞い | 変わる。完了の後に変わったプランを `--resume` すると流さずに止まる。`--from` でも実行条件を先に評価する | + +## 検証手段 + +| 項目 | 手段 | +| --- | --- | +| テスト | `uv run --frozen --project . --all-extras pytest . -q -n 4`(全体テスト)。参照の種類ごとの解決・解決できない・承認時の値との食い違い・流し直し・旧い形のプランのテスト。#2164・#2230 の既存のテスト | +| 静的解析・構造の検査 | `python3 scripts/check-script-structure.py` | +| 配布物の検証 | `claude plugin validate .` | +| 手動確認(マージ前) | #2349 の再現の手順(実行条件を持つプランを `--from` で始める・完了したプランを含む done を `--resume` する)を、直した版のスクリプトで 1 回ずつ通し、結果を Pull Request に書く | +| 手動確認 | 子の課題 4 件の再現の手順を、本番へ出した版で通し、受け入れ条件 11 の表を埋める(リリース後テスト) | + +## 前提とする取り決め + +| 項目 | 参照先 / 決めたこと | +| --- | --- | +| プロジェクト構造 | 参照の部品は `plugins/ndf/scripts/lib/` に置く。既定で働くため `experimental/` に置かない(`AGENTS.md` の「安定版と実験版」)。`factory/` を参照しない | +| コーディング規約 | `AGENTS.md` の「最小限のコード実装」と、外部コマンド(`git fetch`・`gh`)の挙動を書く前に実行して確かめる決まり | +| テスト戦略 | 参照の部品は単体テストで解決・失敗・照合を担保し、プランを組む入口はプランのファイルを読むテストで担保する。子の課題は再現の手順で確かめる。`.md` の文言を照合するテストは書かない | + +## 境界 + +| 区分 | 内容 | +| --- | --- | +| 常に行う | #2335 の設計を正として変更する。全体テストと構造の検査を通す。参照の部品へ寄せた差し込みの関数をキューから消す | +| 確認してから行う(承認ゲート 1・2 で人が見る) | `loop sprint` の `--dev` を任意にする変更(公開インタフェース)。承認ゲート 2 の後の食い違いを `gate` で返す変更 | +| 行わない | 子の課題を 1 件ずつ直す Pull Request(「含まない」の例外を除く)。版の規則を変える。利用者の `.ndf/` を書き換える。承認時の値との食い違いを AI の判断で見逃す(共通原則の承認の扱い) | + +## 未決 + +| 項目 | 誰が決めるか | 期限 | +| --- | --- | --- | +| 参照の書き方(今の `{queue_record}` のような文字列の差し込みを続けるか、プランの型の欄にするか) | この柱の設計(柱 P6 のプランの型に従う) | 設計 Pull Request | +| 流し直しの判定(受け入れ条件 7)で、プランの中身の同一性を何で測るか(ファイルの sha256・型で正規化した中身) | この柱の設計 | 設計 Pull Request | +| 版の記録(`version.json`)を実行の記録へ移すか、状態ディレクトリに残して解決の記録から指すか | この柱の設計 | 設計 Pull Request | +| 柱 P3・P6・P8 のマージの前にこの柱の設計を承認したとき、それらの設計が変わった場合の読み直し | この柱の設計の担当(前提 2) | 実装の開始時 |