diff --git a/docs/glossary.md b/docs/glossary.md index 2fa50f4a5..d1dad3722 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -856,15 +856,15 @@ NDF を Web UI から操作できるサービスとして提供するときの | 進捗の受け口 | — | ランナー・GitHub の Webhook・承認の API から進捗の事象を受け、出所ごとに認証して冪等に追記する、factory/api/ の HTTP の経路 | — | — | `docs/specifications/ndf-factory-contracts-and-progress.md` | | 実行ジョブの token | — | 実行の制御が実行ジョブ 1 つごとに発行する、期限つきの token。そのジョブのスプリントの実行にだけ進捗の事象を書け、Pull Request の参照はそのジョブの取り込むリポジトリのものだけを残せる。承認の依頼の答えを読める | — | — | `docs/specifications/ndf-factory-contracts-and-progress.md` | | 冪等の鍵 | — | 進捗の事象の重複を判定する鍵(idempotency_key)。形は出所ごとに決まり、同じ鍵の事象は 1 つだけ残る | — | — | `docs/specifications/ndf-factory-contracts-and-progress.md` | -| アカウント | — | Neural Development Factory で、プランと使える量を持ち、リポジトリの登録とスプリントの実行を持つ単位。個人とチームの 2 種類。`account_id` で指す | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | +| アカウント | — | Neural Development Factory で、料金プランと使える量を持ち、リポジトリの登録とスプリントの実行を持つ単位。個人とチームの 2 種類。`account_id` で指す | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | | 席 | — | チームのアカウントの中で、1 人の利用者に割り当てる枠。種類は standard と premium | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | -| プラン | — | アカウントの契約の種類。Free・個人 Pro・個人 Max・Team の 4 つ。Team の使える量は席の種類で決まる | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | -| 使える量 | — | プランか席の種類ごとに決まる上限。同時に動かせるスプリントの実行の数と、月の token の上限の 2 つ | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | +| 料金プラン | — | アカウントの契約の種類。Free・個人 Pro・個人 Max・Team の 4 つ。Team の使える量は席の種類で決まる | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | +| 使える量 | — | 料金プランか席の種類ごとに決まる上限。同時に動かせるスプリントの実行の数と、月の token の上限の 2 つ | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | | 利用者 | — | Google か GitHub の 1 つのアカウントで Neural Development Factory にログインした人。Cognito の `sub` を利用者の ID として持つ。同じ人でも IdP が違えば別の利用者である | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | | チームの持ち主 | — | チームのアカウントを作った利用者。席を足す・種類を変える・外すことができる唯一の利用者 | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | | 使える量の判定 | — | 呼び出す側が渡した今の値(動いているスプリントの実行の数・その月の token の数)を使える量の上限と照らし、許すか断るかと理由を返すこと | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | | GitHub の仲立ち | — | GitHub の OAuth を Cognito が受け取れる OIDC の形へ変える、`factory/api/` のプログラム | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | -| 実行の形態 | — | アカウントのランナーが Claude をどう呼ぶか。`local`(手元のランナー)・`byok`(キーの持ち込み)・`managed`(サービスの Bedrock)の 3 つで、アカウントごとに 1 つ持つ。Free は `managed` を選べず、月の token の上限は `managed` だけに掛かる | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | +| 実行の形態 | — | アカウントのランナーが Claude をどう呼ぶか。`local`(手元のマシン)・`byok`(キーの持ち込み)・`managed`(サービスの Bedrock)の 3 つで、アカウントごとに 1 つ持つ。Free は `managed` を選べず、月の token の上限は `managed` だけに掛かる | — | — | `docs/specifications/ndf-factory-accounts-and-plans.md` | | 起票の依頼 | — | 実行ジョブの中で NDF が別のリポジトリへの起票を決めたとき、ランナーが進捗の事象 issue.requested でサービスへ送る依頼。起票先・題・本文を持ち、ランナーは自分では起票しない | — | — | — | | 起票の依頼の結果 | — | 起票の依頼 1 件ごとに 1 件残る進捗の事象 issue.request_resolved。サービスが起票した Issue の参照か、下書きへ回した理由を持つ | — | — | — | | 起票の代行 | — | 記録された起票の依頼を読み、起票してよい先へはサービスが 1 リポジトリと Issues の書き込みだけに絞った導入の token で起票し、そうでない先は下書きを人へ回す、サービスの処理 | — | — | — | @@ -890,10 +890,13 @@ NDF を Web UI から操作できるサービスとして提供するときの | スプリント全体の欄 | — | スプリントの詳細で、課題の番号を持たない工程の記録を並べる欄(#2172) | — | — | — | | 区間 | — | 1 つの実行ジョブの中でランナーが起動する claude -p の 1 回分。conductor の ndf-next で次の区間へ移る | — | — | `docs/specifications/ndf-factory-dev-loops.md` | | 区間の上限 | — | 1 つの実行ジョブで起動してよい区間の数の上限(既定 20)。超えたジョブは section_limit で終わる | — | — | `docs/specifications/ndf-factory-dev-loops.md` | -| 同時のジョブの数 | `max_jobs` | 手元のランナーが同時に動かす実行ジョブの数の上限。ランナーが登録と心拍で申告し、サービスはそれを超えて指示を渡さない | — | — | `docs/specifications/ndf-factory-dev-loops.md` | +| 同時のジョブの数 | `max_jobs` | 手元のマシンが同時に動かす実行ジョブの数の上限。手元のマシンが登録と心拍で申告し、サービスはそれを超えて指示を渡さない | — | — | `docs/specifications/ndf-factory-dev-loops.md` | | マイルストーンの持ち分 | — | 終わりの状態でない開発ループが、リポジトリのマイルストーン 1 つを選定の対象として占めていることを表す実行の枠の行。同じリポジトリのループどうしで重ならない | — | — | `docs/specifications/ndf-factory-dev-loops.md` | | 課題の持ち分 | — | 終わりの状態でない手動のスプリントの実行が、リポジトリの課題 1 つを持っていることを表す実行の枠の行。同じ課題を 2 つの実行に入れない | — | — | `docs/specifications/ndf-factory-dev-loops.md` | -| 手元のランナーの枠 | — | 1 つの手元のランナーが running で持つ local の実行ジョブの数を数えた実行の枠の行。同時のジョブの数を超えて受け取らせない | — | — | `docs/specifications/ndf-factory-dev-loops.md` | +| 手元のマシンの枠 | — | 1 つの手元のマシンが running で持つ local の実行ジョブの数を数えた実行の枠の行。同時のジョブの数を超えて受け取らせない | 手元のランナーの枠 | — | `docs/specifications/ndf-factory-dev-loops.md` | +| 手元のマシン | — | 利用者の環境に登録し、同時のジョブの数まで実行ジョブを受け取るマシン。受け取った実行ジョブごとに別のランナーが動く | 手元のランナー | — | — | +| Factory のオーガナイザー | — | 同じリポジトリで並行する Factory の実行をまとめる役割。1 台の中のオーガナイザーの仕事すべて(配布を 1 本にまとめる・問いを集める・答えの後の作業を持ち主へ返す・共有の作業を一時のレーンへ渡す)に、Factory 固有の仕事(実行ジョブの起動・停止・答えでの再開、実行の枠と手元のマシンの枠、ジョブをまたぐ状態の置き場)を足したもの。API の handler と見回りの中で動き、常駐しない。1 台の中のオーガナイザーと同じ層の別の実装で、互いに排他 | — | — | — | +| 配布の実行 | — | Factory のオーガナイザーが、まとめた配布 1 つのために起こすスプリントの実行。範囲のどの実行でもなく、承認資料を作り、本番の配布を流し、結果を返して終わる。同じリポジトリで同時に 1 本まで | — | — | — | | 画面の地図 | — | Factory の全画面の経路・役割ごとの主な動線・確定仕様の機能との対応を 1 つにまとめた表 | — | — | `docs/specifications/ndf-factory-ui-components.md` | | デザインのトークン | — | 色・文字・余白の値に名前を付け、画面の部品がその名前で参照する値の集合 | — | — | `docs/specifications/ndf-factory-ui-components.md` | | ダミーの API クライアント | — | 画面が呼ぶ API クライアントのインタフェースを満たし、決まったデータを返す実装。本物の API クライアントと差し替えられる | — | — | `docs/specifications/ndf-factory-ui-components.md` | diff --git a/docs/glossary/glossary.json b/docs/glossary/glossary.json index 822a5b16a..51a0150a7 100644 --- a/docs/glossary/glossary.json +++ b/docs/glossary/glossary.json @@ -4390,7 +4390,7 @@ { "term": "アカウント", "context": "ndf-factory", - "meaning": "Neural Development Factory で、プランと使える量を持ち、リポジトリの登録とスプリントの実行を持つ単位。個人とチームの 2 種類。`account_id` で指す", + "meaning": "Neural Development Factory で、料金プランと使える量を持ち、リポジトリの登録とスプリントの実行を持つ単位。個人とチームの 2 種類。`account_id` で指す", "source": "docs/specifications/ndf-factory-accounts-and-plans.md" }, { @@ -4400,7 +4400,7 @@ "source": "docs/specifications/ndf-factory-accounts-and-plans.md" }, { - "term": "プラン", + "term": "料金プラン", "context": "ndf-factory", "meaning": "アカウントの契約の種類。Free・個人 Pro・個人 Max・Team の 4 つ。Team の使える量は席の種類で決まる", "source": "docs/specifications/ndf-factory-accounts-and-plans.md" @@ -4408,7 +4408,7 @@ { "term": "使える量", "context": "ndf-factory", - "meaning": "プランか席の種類ごとに決まる上限。同時に動かせるスプリントの実行の数と、月の token の上限の 2 つ", + "meaning": "料金プランか席の種類ごとに決まる上限。同時に動かせるスプリントの実行の数と、月の token の上限の 2 つ", "source": "docs/specifications/ndf-factory-accounts-and-plans.md" }, { @@ -4438,7 +4438,7 @@ { "term": "実行の形態", "context": "ndf-factory", - "meaning": "アカウントのランナーが Claude をどう呼ぶか。`local`(手元のランナー)・`byok`(キーの持ち込み)・`managed`(サービスの Bedrock)の 3 つで、アカウントごとに 1 つ持つ。Free は `managed` を選べず、月の token の上限は `managed` だけに掛かる", + "meaning": "アカウントのランナーが Claude をどう呼ぶか。`local`(手元のマシン)・`byok`(キーの持ち込み)・`managed`(サービスの Bedrock)の 3 つで、アカウントごとに 1 つ持つ。Free は `managed` を選べず、月の token の上限は `managed` だけに掛かる", "source": "docs/specifications/ndf-factory-accounts-and-plans.md" }, { @@ -4968,7 +4968,7 @@ "term": "同時のジョブの数", "context": "ndf-factory", "code": "max_jobs", - "meaning": "手元のランナーが同時に動かす実行ジョブの数の上限。ランナーが登録と心拍で申告し、サービスはそれを超えて指示を渡さない", + "meaning": "手元のマシンが同時に動かす実行ジョブの数の上限。手元のマシンが登録と心拍で申告し、サービスはそれを超えて指示を渡さない", "source": "docs/specifications/ndf-factory-dev-loops.md" }, { @@ -4984,10 +4984,37 @@ "source": "docs/specifications/ndf-factory-dev-loops.md" }, { - "term": "手元のランナーの枠", + "term": "手元のマシンの枠", "context": "ndf-factory", - "meaning": "1 つの手元のランナーが running で持つ local の実行ジョブの数を数えた実行の枠の行。同時のジョブの数を超えて受け取らせない", - "source": "docs/specifications/ndf-factory-dev-loops.md" + "meaning": "1 つの手元のマシンが running で持つ local の実行ジョブの数を数えた実行の枠の行。同時のジョブの数を超えて受け取らせない", + "source": "docs/specifications/ndf-factory-dev-loops.md", + "deprecated": [ + "手元のランナーの枠" + ] + }, + { + "term": "手元のマシン", + "context": "ndf-factory", + "meaning": "利用者の環境に登録し、同時のジョブの数まで実行ジョブを受け取るマシン。受け取った実行ジョブごとに別のランナーが動く", + "deprecated": [ + "手元のランナー" + ], + "source": "", + "pending_source": "issues/issue-2439-design.md" + }, + { + "term": "Factory のオーガナイザー", + "context": "ndf-factory", + "meaning": "同じリポジトリで並行する Factory の実行をまとめる役割。1 台の中のオーガナイザーの仕事すべて(配布を 1 本にまとめる・問いを集める・答えの後の作業を持ち主へ返す・共有の作業を一時のレーンへ渡す)に、Factory 固有の仕事(実行ジョブの起動・停止・答えでの再開、実行の枠と手元のマシンの枠、ジョブをまたぐ状態の置き場)を足したもの。API の handler と見回りの中で動き、常駐しない。1 台の中のオーガナイザーと同じ層の別の実装で、互いに排他", + "source": "", + "pending_source": "issues/issue-2439-design.md" + }, + { + "term": "配布の実行", + "context": "ndf-factory", + "meaning": "Factory のオーガナイザーが、まとめた配布 1 つのために起こすスプリントの実行。範囲のどの実行でもなく、承認資料を作り、本番の配布を流し、結果を返して終わる。同じリポジトリで同時に 1 本まで", + "source": "", + "pending_source": "issues/issue-2439-design.md" }, { "term": "画面の地図", diff --git a/issues/issue-2439-design-data.md b/issues/issue-2439-design-data.md new file mode 100644 index 000000000..edb9e15ac --- /dev/null +++ b/issues/issue-2439-design-data.md @@ -0,0 +1,199 @@ +# Factory のオーガナイザーの設計: データ構造と入出力の契約(#2439) + +設計文書 [issue-2439-design.md](issue-2439-design.md) から分けたファイルである。DynamoDB の行と、API・進捗の事象・ランナーの指示の契約を扱う。不変条件の番号(I56〜I68)は設計文書のものを指す。 + +## データ構造 + +### 保存する形 + +オーガナイザーの進行の行は、今の実行の表(`pk=ACCOUNT#`)に置く。まとめた配布と配布の実行を 1 回のトランザクションで書く(I57)ために、同じ表に置く。 + +```mermaid +erDiagram + ORG ||--o{ ORGREQ : "受けた" + ORG ||--o{ ORGBATCH : "組んだ" + ORG ||--o{ ORGLOG : "積む" + ORGBATCH ||--|| SPRINT_RUN : "配布の実行" + ORGREQ }o--|| SPRINT_RUN : "依頼した実行" + ORGREQ }o--o| ORGBATCH : "答えた配布" + ORG { + string sk PK "ORG#repositoryId" + number version + number currentSeq + number nextSeq + string waitReason + } + ORGREQ { + string sk PK "ORGREQ#repositoryId#requestId" + string sprintRunId + string lane + object answer + } + ORGBATCH { + string sk PK "ORGBATCH#repositoryId#seq6桁" + string state + string releaseRunId + list presentations + } +``` + +### テーブル定義 + +**根(`sk=ORG#`)**: 1 リポジトリに 1 行。最初の依頼を受けた `tick` が作る。 + +| 列 | 型 | 空を許すか | 意味 | +| --- | --- | --- | --- | +| `repositoryId` | 文字列 | 許さない | リポジトリの登録の識別子 | +| `version` | 数値 | 許さない | 楽観的な排他の版(I67)。書くたびに 1 増やす | +| `currentSeq` | 数値 | 許す | 流れているまとめた配布の連番。空は「流れていない」(I56) | +| `nextSeq` | 数値 | 許さない | 次に組むまとめた配布の連番(1 から) | +| `waitReason` | 文字列 | 許す | `external_release_open` / `legacy_run`。空は「待っていない」 | +| `waitDetail` | 文字列 | 許す | 待ちの相手(外の Pull Request の URL・古い版の数)。`waitReason` が空なら空 | +| `gsi1pk`・`gsi1sk` | 文字列 | 許す | 実行の表の既存の GSI `gsi1` の鍵 `ORGANIZER_ACTIVE` / 行の `sk`。`currentSeq` があるときだけ持つ(見回りが流れている根を引く) | + +**受けた依頼(`sk=ORGREQ##`)**: 依頼 1 件に 1 行(I59)。依頼の中身は書き換えない。 + +| 列 | 型 | 空を許すか | 意味 | +| --- | --- | --- | --- | +| `requestId` | 文字列 | 許さない | NDF が作る依頼の id(#2436 の `<レーン名>-r<乱数 12 桁>`) | +| `sprintRunId` | 文字列 | 許さない | 依頼を出した実行 | +| `loopId` | 文字列 | 許す | 依頼を出した実行のループ。空は手動の実行 | +| `lane` | 文字列 | 許さない | `NDF_ORGANIZER_LANE` の値(`loop-` / `run-`) | +| `prs`・`issues` | 数値の配列 | 許さない(空の配列は可) | 依頼の Pull Request と課題 | +| `ndfVersion` | 文字列 | 許さない | 依頼した実行の NDF の版 | +| `requestedAt` | 文字列 | 許さない | 依頼のファイルの時刻 | +| `answer` | オブジェクト | 許す | `{seq, result, version, prodPrUrl, reason, at}`。空は「未回答」。1 回だけ書く(I58) | +| `deliveredAt` | 文字列 | 許す | 答えを依頼元へ届け終えた時刻。依頼元の実行の進捗の記録へ `release.answered` を追記し、手動の実行なら `requestResume` が通った(か、実行がもうこの依頼を待っていない)後に書く。「この依頼を待っている」は、実行が `awaiting_approval` で `awaitingReason` が `release_requested`、`releaseRequestId` がこの依頼の id と一致することを指し、それ以外(取り消し・終わり・別の理由や別の依頼での `awaiting_approval`)は待っていないとする。空は「届け終えていない」 | +| `gsi1pk`・`gsi1sk` | 文字列 | 許す | 実行の表の既存の GSI `gsi1` の鍵 `ORGANIZER_ACTIVE` / 行の `sk`。`deliveredAt` が空の間だけ持つ(見回りが未回答の依頼と、答えを届け終えていない依頼を引く)。`deliveredAt` を書く書き込みで外す | + +**まとめた配布(`sk=ORGBATCH##`)** + +| 列 | 型 | 空を許すか | 意味 | +| --- | --- | --- | --- | +| `seq` | 数値 | 許さない | 連番 | +| `state` | 文字列 | 許さない | `composed` / `presented` / `represented` / `confirmed` / `stalled` / `released` / `rejected` | +| `releaseRunId` | 文字列 | 許さない | 配布の実行(I57) | +| `lanes` | 配列 | 許さない | 組んだ時点のレーンの一覧 `{lane, sprintRunId, loopId?, requestIds, issues}`。依頼の無いレーンは `requestIds` が空 | +| `presentations` | 配列 | 許さない(空の配列は可) | 提示 `{k, sha, version, prs, lanes:[{lane, requested}], addedPrs, at}`。2 つまで(I60)。上書きせず足す | +| `confirmedK` | 数値 | 許す | 確定した提示の番号。空は「確定していない」 | +| `mainMerged` | 真偽 | 許さない | `main` へのマージを記録したか(I63)。`release.progress` の `main_merged` か、GitHub で head が `release/v<提示の版>` の `main` 宛の Pull Request がマージ済みなら真。既定は偽 | +| `result` | オブジェクト | 許す | `{version, prodPrUrl, sha, prs}`。空は「配布済みでない」 | +| `stallReason` | 文字列 | 許す | `stalled` の理由(ジョブの失敗の理由)。`stalled` 以外は空 | +| `stalledFrom` | 文字列 | 許す | 止まる前の状態(`composed` / `represented` / `confirmed`)。「続きから流す」でこの状態へ戻し、再開の手(ジョブの `releaseStep`)をこの値で決める。`stalled` 以外は空 | + +**記録(`sk=ORGLOG###<種類>#<乱数 8 桁>`)**: 追記だけで書き換えない。末尾の乱数は、同じ時刻に同じ種類の記録を 2 件書いても上書きしないために付け、書き込みは `attribute_not_exists(sk)` を条件にする。並びは時刻で決まる。種類は `waited` / `composed` / `presented` / `represented` / `confirmed` / `stalled` / `resolved` / `released` / `rejected`。中身は種類ごとの値(待ちの相手・提示のコミット・確定の番号・結果)。 + +**スプリントの実行・ジョブ・リポジトリの枠(変更)**: 列を足すだけで既存の列は変えない。 + +| 行 | 列 | 型 | 空を許すか | 意味 | +| --- | --- | --- | --- | --- | +| リポジトリの枠 | `legacySeq` | 数値 | 許す | 古い版の実行を `legacyCount` へ数えに入れた回数。`legacyCount` を増やす書き込み(開始・移行の埋め戻し)で同じトランザクションで 1 増やし、減らす書き込みでは変えない。まとめた配布を組む書き込みの条件に使う(I62)。空は「まだ数えに入れていない」 | +| 実行 | `purpose` | 文字列 | 許す | `release`。空は今のスプリントの実行(`sprint`) | +| 実行 | `releaseSeq` | 数値 | 許す | 配布の実行のまとめた配布。`purpose` が空なら空 | +| 実行 | `awaitingReason` | 文字列 | 許す | `awaiting_approval` の理由 `release_requested` / `release_stalled`。空は今の承認の依頼の待ち | +| 実行 | `releaseRequestId` | 文字列 | 許す | 手動の実行が待つ依頼の id。`awaitingReason` が `release_requested` のときだけ | +| ジョブ | `start` | 文字列 | 許さない | 今の値に `release` を足す | +| ジョブ | `releaseStep` | 文字列 | 許す | `present` / `represent` / `confirm:1` / `confirm:2` / `continue`。配布の実行のジョブだけ。「続きから流す」では `stalledFrom` が `composed` なら `present`、`represented` なら `represent`、`confirmed` なら `release.progress` があるか、`mainMerged` が真か、GitHub に `release/v<提示の版>` の枝か本番の Pull Request があれば `continue`・どれも無ければ `confirm:`。判断の答えの再開は `continue` | + +### 時系列の扱い + +**事象の記録と、追記だけの提示の列を採る。** まとめた配布の `state` は上書きするが、遷移は記録の行に積み、提示は配列に足すだけにする。承認する人が何のコミットを見て答えたかを、出し直しの後も辿れる必要があるためである(非機能の「運用・保守性」)。根の `waitReason` は上書きで、待ちの履歴は記録の行が持つ。 + +### 機能 × データ + +| 機能 | 根 | 受けた依頼 | まとめた配布 | 記録 | 実行・ジョブ | +| --- | --- | --- | --- | --- | --- | +| F1 依頼を出す | — | C(`finalize`) | — | — | U | +| F2 組む | C・U | R | C | C | C(配布の実行) | +| F3 提示する | — | — | U | C | U | +| F4 答えと出し直し | R | — | U | C | U(再開) | +| F5 結果を返す | U | U(答え) | U | C | U(手動の実行の再開) | +| F6 一覧 | R | R | R | R | R | +| F7 止まった配布を解く | U | U(差し戻しの答え) | U | C | U | +| F8 見回り | R | R・U(届け終わり) | R | — | R・U(手動の実行の再開) | + +### 移行 + +既存のデータの移行は無い。根は最初の依頼で作り、実行とジョブの足した列は空を今の意味として読む。 + +### 検査の手段 + +`cd factory && npm ci && npm run check`(型検査と vitest。`MemoryRunsTable` に同じ行と条件つきの書き込みを足し、配布のまとめの規則はメモリの表で確かめる)。 + +## 入出力の契約 + +### 画面向けの経路(`factory/contracts/openapi/organizer.yaml`。新規) + +| 経路 | 入力 | 成功 | 失敗の形 | +| --- | --- | --- | --- | +| `GET /accounts/{accountId}/repositories/{repositoryId}/approval-requests` | — | 200 `{organizer: {currentSeq, waitReason, waitDetail}, items: [...]}`。`items` の 1 件は `{kind, sprintRunId?, loopId?, approvalRequestId?, prUrl?, createdAt, release?}`。`kind` は `gate1` / `gate2` / `decision` / `loop_pending` / `release_stalled`。`release` は配布の実行のゲート 2 と止まった配布だけが持ち `{seq, k, sha, lanes:[{lane, requested, prs}], addedPrs, choices}` | 401・403(アカウントのリポジトリでない)・404 | +| `GET /accounts/{accountId}/repositories/{repositoryId}/releases` | `?limit`(既定 20) | 200 `{items: [{seq, state, releaseRunId, presentations, confirmedK, result, log}]}`(新しい順) | 401・403・404 | +| `POST /accounts/{accountId}/repositories/{repositoryId}/releases/{seq}/resolve` | `{choice: "continue" \| "reject", body?}`。Web のセッションの認証だけ | 202 `{seq, state}` | 401・403(セッションでない呼び出し。I66)・404・409 `not_stalled`・409 `release_after_main`(I63) | + +- 一覧の並びは作った時刻の古い順である。承認の依頼の中身(承認資料・答え)は #2173 の経路が持ち、一覧は識別子と要約だけを返す +- 一覧の承認の依頼は「同じリポジトリの `awaiting_approval` の実行」から #2173 の読み出し(未確認のまま残ること)で引く。ループの人の判断待ちはループの行から引く +- 一覧の経路と履歴の経路は読むだけで、答えを確定する経路はこのファイルに無い(I66)。MCP は同じ GET を呼ぶ + +### ランナー向けの経路(`factory/contracts/openapi/runs.yaml`。足す) + +| 経路 | 入力 | 成功 | 失敗の形 | +| --- | --- | --- | --- | +| `GET /run-jobs/organizer/answers` | 実行ジョブの token | 200 `{answers: [{request_id, result, version, release, reason, at}], ranges: [{seq, sha, version, prs, lanes}]}`。そのジョブの実行のレーン(ループならループのすべての実行)の依頼の答えと、リポジトリの配布済みの範囲 | 401・403 | +| `GET /run-jobs/organizer/batch` | 実行ジョブの token(配布の実行のジョブだけ) | 200 `{seq, step, lanes, presentations, choices}` | 401・403(配布の実行でない)・404 | + +答えの形は #2436 の `parallel-request.json` の答えと範囲にそろえる。 + +| 形 | #2436 のファイル | 項目 | +| --- | --- | --- | +| 答え | `answer-<依頼>` | `{result, version, release, reason, at}` | +| 範囲 | `range-<連番>` | `{sha, version, prs, lanes}` | + +答えの項目は、受けた依頼の行の `answer` から次のように写す。#2436 の答えの `release` は、この設計では本番の Pull Request の URL を入れる。 + +| 受けた依頼の行(`answer`) | ランナーへ返す答え・置き場の `answers/` | +| --- | --- | +| `result`・`version`・`reason`・`at` | 同じ名前 | +| `prodPrUrl` | `release` | +| `seq` | 返さない(範囲の `ranges` が連番を持つ) | +| 行の `requestId` | `request_id`(経路の応答だけ。ファイルでは名前が依頼の id) | + +ランナーはこれを置き場のファイルへそのまま書く([issue-2439-design-ndf.md](issue-2439-design-ndf.md))。 + +### 進捗の事象(`factory/contracts/schemas/progress-event.schema.json`。種類を足す) + +| 種類 | 出所 | payload | 冪等の鍵 | +| --- | --- | --- | --- | +| `release.requested` | `runner` | `{request_id, lane, prs, issues, ndf_version, requested_at}` | `` | +| `release.presented` | `runner` | `{seq, k, sha, version, prs, lanes:[{lane, requested}], added_prs, auto}`。`auto` は MVV 判定が通したとき真 | `:presented:` | +| `release.progress` | `runner` | `{seq, stage}`。`stage` は `release_branch` / `develop_merged` / `prod_pr_opened` / `main_merged` / `tagged` | `:progress:` | +| `release.settled` | `runner` | `{seq, result, version?, prod_pr_url?, sha?, prs, lanes, reason?}`。`result` は `released` / `failed` | `:settled` | +| `release.answered` | `control` | `{seq, request_id?, result, version?, prod_pr_url?}`。範囲の実行の進捗の記録へ書く(AC 1) | `:` | + +`gate.reached` の payload に任意の `choices`(文字列の配列)を足す。配布の実行が `main` のマージの後に判断で止まったときは `["continue"]` だけを載せる(I63)。既存の項目と既存の種類は変えない。 + +### ジョブの指示(`factory/contracts/schemas/run-job-instruction.schema.json`。足す) + +| 項目 | 型 | 意味 | +| --- | --- | --- | +| `START` | 文字列 | 今の値に `release` を足す | +| `RELEASE_SEQ` | 数値 | 配布の実行のまとめた配布の連番 | +| `RELEASE_STEP` | 文字列 | `present` / `represent` / `confirm:1` / `confirm:2` / `continue` | +| `RELEASE_REQUEST_ID` | 文字列 | 手動の実行の再開で、答えの出た依頼の id | + +レーンの中身(レーンの一覧と依頼)は指示に載せず、ランナーが `GET /run-jobs/organizer/batch` で読む。RunTask の `overrides` の上限(8192 字)を避けるためである。 + +### 実行の制御の関数(変更) + +| 関数 | 変えること | +| --- | --- | +| `requestResume({sprintRunId, approvalRequestId?, releaseRequestId?})` | 配布の実行なら `organizer.decide` が `RELEASE_STEP` を決めてから今の書き込みで再開する。`releaseRequestId` は手動の実行の配布の答えの再開に使い、承認の依頼の答えを読まない。`releaseRequestId` を渡したときは、再開のジョブを作る条件つきの書き込み(`addResumeJob`)の条件に「状態が `awaiting_approval`・`awaitingReason` が `release_requested`・実行の `releaseRequestId` が渡した値と一致する」を足す。条件に当たらなければ再開せず、実行がこの依頼を待っていないことを返す(見回りは `deliveredAt` を書く)。別の承認ゲートの待ちを、その承認の依頼の答え無しに再開させないためである | +| `finalize` | 上の「止まったときと再開」と I65 の表のとおり。`release.requested` を受けた依頼の行に、`release.settled` を `settle` へ渡す | +| `sweep` | 最後に `organizer.sweep()` を呼ぶ | + +### 互換性 + +- 足すのは経路・事象の種類・payload の任意の項目・指示の項目・行の列だけで、既存の項目の意味は変えない +- 依頼を出さない NDF の版の実行は、今どおり自分で本番の配布を流す。Factory はその実行を、並行を許す版の下限(設定 `NDF_PARALLEL_MIN_VERSION`)より古い版の実行(`legacy` が真)として見分ける。下限が切り替え口(`parallel.py request --external`)の入った版より低いと、下限以上で依頼を出さない版の実行が I62 の待ちをすり抜けて自分で配布し、まとめた配布とぶつかる。そのため実装の配布の手順で、Factory の本番へこの課題を出す前に、下限を切り替え口の入った NDF の版以上へ上げる。古い版かどうか(`legacy`)は実行の開始で決まり、下限を変えても既に始まった実行の値は変わらない。古い下限以上として始まった切り替え口の無い版の実行は `legacy` が偽のまま自分で配布するため、下限を上げた後、上げる前に始まって終わりの状態でない実行が無くなるまで待ってから、この課題を本番へ出す +- 古い版の実行はリポジトリで唯一になり([ndf-factory-dev-loops.md](../docs/specifications/ndf-factory-dev-loops.md) の I48)、依頼を出していない古い版の実行がある間は配布のまとめは組まない(I62 の `legacy_run`)。まとめた配布が流れている間は、古い版の実行を始めない(I62)。下限が無い間はすべての版が古い版として数えられるが、依頼を出して待つ実行は I62 で数えないため、まとめた配布は組まれる +- `requestResume` を呼ぶ側(#2173)の引数は変わらない。足した `releaseRequestId` は配布のまとめだけが渡す +- 検査は 2 つである。`npm run check` の契約の lint(`contracts`)と、構造チェック(`scripts/contracts_lib/model.py`)が、ランナーの翻訳と NDF の契約の例の組を照らす diff --git a/issues/issue-2439-design-decisions.md b/issues/issue-2439-design-decisions.md new file mode 100644 index 000000000..3c240c62f --- /dev/null +++ b/issues/issue-2439-design-decisions.md @@ -0,0 +1,91 @@ +# Factory のオーガナイザーの設計: 決定の記録(#2439) + +設計文書 [issue-2439-design.md](issue-2439-design.md) の決定を分けたファイル。要求の「起票者の推定」(S1〜S11)と「未決」(1〜4)への答えを含む。 + +## 決定の記録 + +### 決定 1: 実行基盤の違う 2 つの実装を互いに独立して変えられるよう、Factory のオーガナイザーを `factory/` に置き、#2436 の契約だけに依る + +1 台の中のオーガナイザーは SQLite(DBOS)の耐久ワークフローで、実行ジョブの間をまたげない。Factory の実行はジョブごとに別のコンテナと clone を持ち、共有できるのは DynamoDB と GitHub だけである。そのため同じ層の別の実装として `factory/` に置く。共有するのは、依頼と答えの形(#2436 の `parallel-request.json`)と配布の規則(#2436 の決定 13〜15)だけである。2 つは排他で、Factory の実行ジョブの中では 1 台の中のオーガナイザーを起動しない(I68。#2436 が `NDF_FACTORY_RUN` で断る)。 + +Factory のオーガナイザーの仕事は、1 台の中のオーガナイザーの仕事すべてに Factory 固有の仕事(ランナーの制御・枠・状態の置き場)を足したものである。固有の仕事とレーンの起動・停止は、既存の実行の制御とループの制御がすでに持つ。そのため作り直さずに役割の一部として数える。足すのは、足りない「配布を 1 本にまとめる」「問いを集める」「共有の作業を渡す」だけで、配布のまとめと配布の実行として足す。 + +#2436 の設計へ組み込む案は採らない。`plugins/` から `factory/` を参照できない(`scripts/check-service-boundary.py`)。#2436 の設計はすでに 6 ファイル 1,445 行ある。 + +根拠: Value 5 / Value 6(MVV 版 4) + +### 決定 2: 新しい常駐とインフラを足さずに済ませるため、オーガナイザーの進行を実行の表の DynamoDB の行で持ち、API の handler と見回りが進める + +要求の未決 3 への答えである。配布のまとめの 1 手は「根を読み、条件つきで書き、必要なら実行を起こす」で、待ちは人の答えか次の事象である。今の実行の制御(`startNextLap`・`finalize`)が同じ形で動いている。同じ表に置けば、まとめた配布と配布の実行を 1 回のトランザクションで書ける(I57)。見回りが引く索引も、実行の表の既存の GSI `gsi1` に鍵 `ORGANIZER_ACTIVE` の行を載せて使い、インフラの定義を変えない。費用は行の読み書きに比例し、依頼の無いリポジトリには掛からない。 + +| 案 | 採否 | 理由 | +| --- | --- | --- | +| DynamoDB の行(実行の表)と既存の handler・見回り | **採る** | 部品が既にあり、実行の行と同じトランザクションに入る。常駐が無い | +| AWS Step Functions の Standard ワークフロー(人の答えをタスクの token で待つ) | 採らない | ステートマシン・IAM・起動の経路を private リポジトリに足す。実行の行と同じトランザクションに入らず、ワークフローと表の二重の記録の整合が要る | +| 配布のまとめ用の別の表 | 採らない | 配布の実行との原子的な作成ができない | + +根拠: Value 4 / Value 10 / Mission(MVV 版 4) + +### 決定 3: 配布の実行を 2 本作らないため、配布の実行をまとめた配布と同じトランザクションで作り、範囲のどの実行でもない専用の実行にする + +要求の S5 への答えである。範囲の実行のどれかに配布を流させると、その実行のループの周回と配布が同じジョブに混ざる。ループの予算と打ち切りの時間が配布に掛かる。配布の実行を専用にすれば、範囲の実行は答えを待たずに次の周回へ進める(AC 7)。作成をまとめた配布と別のトランザクションにすると、間で止まったときに見回りが 2 本目を起こす。依頼が 1 件でも同じ経路を通す(S8)。1 本のときだけ今の経路に残すと、1 本から 2 本へ増えた瞬間の扱いが要るためである。 + +根拠: Value 6 / Value 2(MVV 版 4) + +### 決定 4: 承認済みの開発の配布を枠で止めないため、配布の実行は実行の枠を持たず使える量の判定にかけず、費用は登録したアカウントの利用量へ数える + +要求の未決 1 への答えで、S11 を採る。配布の実行は範囲の実行がすでに入った枠の続きの仕事である。枠を持たせると、ループが枠を埋めたリポジトリで配布が組めず、`develop` の変更が `main` へ届かない。同時の数はリポジトリに 1 本まで(I56・I57)で、枠が無くても増えない。トークンと費用は今の `usage.recorded` で配布の実行の識別子に付く。アカウントの利用量と `managed` の月のトークンに数える。範囲の実行の数で割る案は、割り方と表示の規則が増えるため採らない。 + +承認ゲート 1 でこの数え方を利用者が確かめる(要求の未決 1 の「誰が決めるか」)。 + +根拠: Value 2 / Value 10(MVV 版 4) + +### 決定 5: NDF を Factory に依らずに外のオーガナイザーへつなぐため、切り替え口をこの課題で作り、置き場のファイルの契約にする + +要求の未決 2 への答えで、S7 を採る。#2436 は 1 台の中に閉じ、依頼をメッセージで送る。Factory のランナーはメッセージを受けられず、NDF の書くファイルを読んで翻訳する形(腐敗防止層)がすでにある。そのため、環境変数 `NDF_ORGANIZER_DIR` があるときだけ、依頼と答えを置き場のファイルで受け渡す。切り替え口の唯一の使い手は Factory なので、この課題で作る。#2436 の契約がマージされてから実装する(前提 1)。 + +環境変数の名前を `NDF_FACTORY_RUN` 1 つにまとめる案は採らない。`plugins/` の切り替え口に Factory を名指しせず、手元の別のまとめ役からも同じ切り替え口を使えるようにするためである。 + +根拠: Value 5(MVV 版 4) + +### 決定 6: Factory の今の承認の流れを保つため、置き場で依頼にするのは本番の配布だけにし、承認ゲート 1 と問いは一覧への並べ方だけをまとめる + +要求の S9 を採る。Factory の実行は承認の依頼でジョブを終えて費用を止め、答えで再開する。承認ゲート 1 と問いを依頼にしてレーンを進め続けると(#2436 の I17)、待つ間もジョブが動く。今の形と違う費用の掛かり方になる。設計 Pull Request は互いに独立で、まとめて 1 つの承認にできる単位も無い。そのため 3 種類の承認の依頼とループの人の判断待ちを、リポジトリごとの 1 つの一覧に並べるだけにする(AC 9)。 + +根拠: Value 2(MVV 版 4) + +### 決定 7: 承認したコミット以外を出さないため、答えの時点の `develop` の比べを API の側で GitHub の口から 1 回だけ行う + +要求の S4 を採る(#2436 の決定 14 と同じ規則)。比べる時点は答えが保存された直後で、配布の実行のジョブはまだ起きていない。API で比べれば、出し直し(`represent`)か確定(`confirm:1`)かを再開の指示に載せられる。ジョブを 1 回起こして比べる費用が要らない。出し直しへの答えでは比べないため、出し直しは連鎖しない(I60)。 + +根拠: Value 4 / Value 10(MVV 版 4) + +### 決定 8: 2 つのオーガナイザーが互いの記録を読めなくても衝突させないため、GitHub の上の開いている本番の Pull Request と古い版の実行で待つ + +要求の S6 を採る。両方が読めるのは GitHub の状態だけである。待つ相手は `main` 宛の Pull Request で、head が `release/v` で始まるものと、head が `develop` のものの両方である。#2436 の配布の規則より前の NDF の版は head が `develop` の Pull Request を作るためである。同じ理由で、リポジトリに依頼を出していない古い版の実行がある間も組まず、まとめた配布が流れている間は古い版の実行を始めない(I62)。その実行は自分で本番を配布する。依頼を出して待つ実行は自分で配布しないため、古い版として数えない。禁止の案は、利用者の手元の作業を Factory が止めるため採らない。 + +根拠: Value 6 / C4(MVV 版 4) + +### 決定 9: ループの周回と手動の実行の今の待ち方を変えないため、依頼の後のループの実行は `done`、手動の実行は理由つきの `awaiting_approval` にする + +ループは周回を配布待ちで締めると次の周回を起こせる(#2436 のレーンのループ)。手動の実行は今もゲート 2 の待ちで枠を持ったまま `awaiting_approval` になっている。同じ状態に理由(`release_requested`)を足せば、状態の遷移を増やさずに済む。答えは `requestResume` で届け、NDF は残りの工程を流す。新しい状態 `awaiting_release` を足す案は、状態の遷移の表・一覧の絞り込み・ループの写し方の 3 つを変えるため採らない。 + +根拠: Value 2 / Value 6(MVV 版 4) + +### 決定 10: 承認の依頼の持ち主を 1 つに保つため、一覧は既存の承認の依頼と人の判断待ちを読んで並べ、新しい保存を作らない + +承認の依頼の保存と答えの経路は #2173 が持つ(前提 3)。配布のまとめがコピーを持つと、答えたかどうかが 2 か所に分かれる。一覧は、同じリポジトリの `awaiting_approval` の実行から #2173 の読み出しで承認の依頼を引き、ループの行から人の判断待ちを引いて並べる。答えは今の経路(#2173 の答え → `requestResume`、ループの答えの経路)で持ち主へ届く。 + +根拠: Value 7 / Value 6(MVV 版 4) + +### 決定 11: `main` へのマージの後の配布を差し戻さないため、配布の実行は失敗しても終わりの状態にせず、止まった配布を「続きから流す」か「差し戻す」で解く + +配布の実行を `failed` で終えると、続きを流すには別の実行を作るしかない。1 つのまとめた配布に 1 本の守り(I57)が崩れる。理由 `release_stalled` の `awaiting_approval` で止めれば、今の再開の書き込みで続けられる。`main` のマージの後は選択肢を「続きから流す」だけにし(AC 13)、マージを `release.progress` と GitHub の両方から照らす。ジョブが段を書く前に落ちても、GitHub でマージ済みと分かるためである。解く操作は承認ゲートではなく失敗の扱いで、承認の工程を足さない。 + +根拠: Value 1 / C4(MVV 版 4) + +### 決定 12: 語の衝突を読み手の側で解くため、「手元のマシン」と「料金プラン」へ改め、識別子は変えない + +要求の未決 4 への答えで、S10 を採る。マシン単位の受け手は、利用者が自分の環境に登録するマシンそのものとして読めればよく、要求と #2189 がすでに「手元のマシン」と書いている。「手元の受け手」は業界で流通していない語である。「エージェント」「ホスト」はこのリポジトリの別の意味(AI のエージェント、Claude Code などのホスト)と重なる。アカウントの契約の種類は、料金の契約であることを語に含めて `ndf-workflow` の「プラン」(`plan.json`)と分ける。識別子(`runnerId`・`plan`)を変えると、API の互換と保存済みのデータの移行が要る。衝突しているのは説明文の語だけである。 + +根拠: Value 8(MVV 版 4) diff --git a/issues/issue-2439-design-ndf.md b/issues/issue-2439-design-ndf.md new file mode 100644 index 000000000..d2637a3b3 --- /dev/null +++ b/issues/issue-2439-design-ndf.md @@ -0,0 +1,115 @@ +# Factory のオーガナイザーの設計: NDF の側の切り替え口と語の改名(#2439) + +設計文書 [issue-2439-design.md](issue-2439-design.md) から分けたファイルである。`plugins/ndf/` に置く切り替え口と、語の改名(F9)を扱う。どちらも Factory を名指ししない(`plugins/` は `factory/` を参照しない。`scripts/check-service-boundary.py`)。 + +## 切り替え口: 外のオーガナイザーとファイルでやり取りする + +### 具体例 + +Factory のループ `l20261102` の 3 周目と 4 周目の実行ジョブで、次の順に進む。 + +1. ランナーは `NDF_ORGANIZER_DIR=<状態の置き場>/organizer`・`NDF_ORGANIZER_LANE=loop-l20261102` を渡す +2. NDF は本番のステージを組む時点で、外のオーガナイザーのレーンと判定する。`requests/loop-l20261102-r3f9a1c2e4b7d.json` を書いて周回を配布待ちで締める +3. ランナーはそれを `release.requested` へ写す +4. 4 周目のジョブの開始で、ランナーは `answers/loop-l20261102-r3f9a1c2e4b7d.json`(`released`・`10.28.0`)を書く +5. NDF は周回の境でそれを読んで残りの工程を流す + +### 置き場の契約(`plugins/ndf/contracts/forms/parallel-external.json`。新規) + +```text +/ +├── requests/<依頼の id>.json # NDF が書く(レーン) +├── answers/<依頼の id>.json # 外のオーガナイザーが書く +├── ranges/<連番>.json # 外のオーガナイザーが書く(配布済みの範囲) +├── batch.json # 外のオーガナイザーが書く(配布の実行だけ) +├── presented/<連番>-<提示>.json # NDF が書く(配布の実行) +├── progress/<連番>.json # NDF が書く(配布の実行) +└── results/<連番>.json # NDF が書く(配布の実行) +``` + +| ファイル | 形 | +| --- | --- | +| `requests/` | #2436 の `parallel-request.json` の依頼(`kind: release`)。`{kind, id, lane, prs, issues, sprint_state, at}` に `ndf_version` を足す | +| `answers/`・`ranges/` | #2436 の `parallel-request.json` の答え `{result, version, release, reason, at}` と範囲 `{sha, version, prs, lanes}` | +| `batch.json` | `{seq, step, lanes:[{lane, requests:[{id, prs}], issues}], presentations, choices}`。`step` は `present` / `represent` / `confirm:1` / `confirm:2` / `continue` | +| `presented/` | `{seq, k, sha, version, prs, lanes:[{lane, requested}], added_prs, auto}` | +| `progress/` | `{seq, stages}`。届いた段(`release_branch` / `develop_merged` / `prod_pr_opened` / `main_merged` / `tagged`) | +| `results/` | `{seq, result, version, prod_pr_url, sha, prs, lanes, reason}`。`result` は `released` / `failed` | + +- **どちらの書き手も一時ファイルへ書いて名前を付け替える。** 読み手は名前の付いたファイルだけを読む。書きかけのファイルを読まないためである +- 契約の例の組(依頼・答え・範囲・`batch.json`・提示・結果の 1 組ずつ)を置く。構造チェック(`scripts/contracts_lib/model.py`)とランナーのテストが同じ例を読む(`ndf-next-block.json` と同じ扱い) +- 環境変数の宣言 `plugins/ndf/contracts/env/NDF_ORGANIZER_DIR.json`・`NDF_ORGANIZER_LANE.json` を足す。`NDF_FACTORY_RUN` の宣言は #2436 が足す + +### レーンの側 + +| 点 | 決まり | +| --- | --- | +| 判定 | `parallel_lane.lane_of()` は、`NDF_ORGANIZER_DIR` があれば外のオーガナイザーのレーン(名前は `NDF_ORGANIZER_LANE`)を返す。無ければ #2436 の判定のまま | +| 依頼 | `parallel.py request --external` は `requests/.json` を書いて 10 で終わる。自分の Pull Request がすべて `origin/main` にあれば、`ranges/` から版を読んで書かずに 0 で終わる(#2436 の I15)。依頼の `id` の決め方は #2436 のまま | +| 答え | `parallel.py await --external --timeout 0` は `answers/.json` を読む。無ければ 3 | +| 問い | 外のオーガナイザーのレーンは、設計の承認と承認ゲートの外の問いを依頼にしない(#2436 の I17 を当てない)。今どおり止まって人へ戻す。問いの hook は `NDF_ORGANIZER_DIR` があれば送らない | +| ループ | 周回の結果「配布待ち」と周回の境の読み方は #2436 のレーンのループのまま。答えを運ぶのは外のオーガナイザー(ランナー)である | + +### 配布の実行の側 + +`/goal /ndf:development-workflow --parallel-helper --external` で始める。conductor は `batch.json` の `step` ごとに次の 1 手を打つ。 + +| `step` | すること | 終わり方 | +| --- | --- | --- | +| `present` / `represent` | `parallel.py compose --external` が提示の番号 `k`(`present` は 1、`represent` は 2)の提示のプランと本番のプランを `<置き場>/releases/<連番>//` に作る(#2436 の「配布を組む」と同じ関数。版は `origin/develop` の `plugin.json` の開発版から接尾辞を外す)。`supervise.py queue present.json --then release.json` を流す | MVV 判定が通せば本番まで流して `settle`。承認ゲート 2 で止まれば `parallel.py present --external` が `presented/` を書き、「判断が要る」で終える | +| `confirm:` | 確定した提示 `k` の本番のプランだけを流す(#2436 の確定の後と同じ)。本番のプランは `batch.json` の `presentations` の k 番目の `sha` から `release/v<版>` を切り、その後に進んだ `develop` を出さない | `settle` | +| `continue` | 止まったプランを今の再開の手順で続ける | `settle` か、判断で止まる | + +`parallel.py settle --external <連番> --done ` は、本番のプランの done とリリース記録を読む。届いた段を `progress/` に、結果を `results/` に書く。配布の実行の失敗の扱い(CI の失敗・版数の衝突の直し)は #2436 の一時レーンと同じで、直せなければ問いとして止まる。 + +`<置き場>` は、ランナーが S3 へ保存する NDF の状態の中に置く。配布の実行の再開(`confirm:k`・`continue`)は、保存した提示のプランと本番のプランから続ける。 + +### 構成要素と触るファイル + +| ファイル | 変えること | +| --- | --- | +| `plugins/ndf/scripts/lib/parallel_external.py` | 置き場の読み書き(一時ファイルと付け替え)・`batch.json` の読み・`compose`・`present`・`settle` の外の版 | +| `plugins/ndf/scripts/lib/parallel_lane.py` | `lane_of()` の外のオーガナイザーの判定、`request`・`await` の外の版 | +| `plugins/ndf/scripts/parallel.py` | 副命令に `--external` を足す(`request`・`await`・`compose`・`present`・`settle`) | +| 問いの hook(`plugins/ndf/scripts/hook_lib/ask_guard.py`) | `NDF_ORGANIZER_DIR` があれば依頼を送らない | +| `plugins/ndf/contracts/forms/parallel-external.json`・`plugins/ndf/contracts/env/NDF_ORGANIZER_{DIR,LANE}.json` | 置き場の契約と例の組、環境変数の宣言 | +| `plugins/ndf/skills/development-workflow/references/parallel.md` | `--parallel-helper --external` の手順の節 | +| `plugins/ndf/scripts/tests/test_parallel_external.py` | 下のテスト設計 | + +### テスト設計 + +| 条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | +| --- | --- | --- | +| 判定 | `NDF_ORGANIZER_DIR` があると本番のステージが配布の依頼のプランになり、無いと #2436 の判定のまま | 環境変数を読まないと落ちる | +| 依頼と答え | 依頼のファイルが契約の例と同じ形で書かれ、答えのファイルを `await --timeout 0` が読む。打ち直しても同じ `id` | 形を変える・`id` を採番し直すと落ちる | +| 問い | 外のレーンの設計の承認と問いは依頼にならず、今どおり止まる | 問いの hook が送ると落ちる | +| 提示と確定 | `represent` で提示 2 のプランを作り、`confirm:1` は提示 1 の本番のプランだけを流す | 確定しない提示の本番のプランを流すと落ちる | +| 結果 | `settle` が `main` のマージの後に止まった本番のプランで `main_merged` を `progress/` に書く | 段を書かないと落ちる | + +## 語の改名(F9) + +### 用語 + +| 用語 | 改める前 | 対象 | +| --- | --- | --- | +| 手元のマシン | `手元のランナー` | 用語集の `ndf-factory`・Factory の確定仕様・`factory/` の説明文・`ja.json` | +| 手元のマシンの枠 | `手元のランナーの枠` | 同じ | +| 料金プラン | `ndf-factory` の文脈の裸の「プラン」 | 同じ。`ndf-workflow` の「プラン」(`plan.json`)と、Factory の文書の中の NDF のプラン(本番のプランなど)は変えない | + +### 対象と件数(2026-11-02 の検索) + +| 対象 | 改める前の語の件数 | +| --- | --- | +| Factory の確定仕様 `docs/specifications/ndf-factory-*.md` | `手元のランナー`38 件(11 ファイル中 6 ファイル)。「プラン」57 件(7 ファイル。NDF のプランを指す行を除いて改める) | +| `factory/README.md` | 「プラン」2 件 | +| `factory/web/src/i18n/ja.json` | `screens.runners``手元のランナー`・`mode.local``local(手元のランナー)`・`account.plan`「プラン: {{plan}}」・`repositories.maxConcurrent`「同時の実行の数(空ならプランの値)」の 4 件 | +| `factory/` のコメントとテストの期待値 | `手元のランナー`16 ファイル・「プラン」18 ファイル(`ja.json` を含む)。文言を照合するテスト(`App.test.tsx` など)は期待値を改めた文言へ合わせる | +| 用語集 | `手元のランナーの枠`の語、「同時のジョブの数」「実行の形態」などの意味の中の語、`ndf-factory` の「プラン」 | + +件数は実装の時点で同じ検索をし直して照らす。対象の外は、発表資料(`docs/presentations/`)・記事(`docs/articles/`)・#2436 の設計文書・`issues/` の過去の文書である。 + +### 守ること + +- コードの識別子(`LocalRunner`・`runnerId`・`Plan`・`plan`)・API の項目名・データの項目名・i18n のキーは変えない(AC 16) +- 用語集は、新しい語に `pending_source`(この設計文書)を置き、`plan-to-spec` が確定仕様を書いたときに `source` へ移す。`手元のランナー`は廃止した語として残し、`glossary.py check` が新しく足した行で拾う +- `ndf-factory` の「プラン」は廃止した語にしない。同じ語が `ndf-workflow` で生きており、用語チェックは生きた語と同じ廃止した語を照合から外すためである。Factory の文書に裸の「プラン」が残らないことは、実装の検索で確かめる diff --git a/issues/issue-2439-design.md b/issues/issue-2439-design.md new file mode 100644 index 000000000..969caa73e --- /dev/null +++ b/issues/issue-2439-design.md @@ -0,0 +1,364 @@ +# Factory のオーガナイザー: 同じリポジトリで並行する実行の本番の配布を 1 本にまとめ、承認と問いをリポジトリごとに 1 か所へ集める(#2439) + +## 目的 + +- **今どうなっているか**: Neural Development Factory の実行は、承認ゲート 2 と本番の配布を実行ごとに出す。2026-11-02 に 1 台の tmux で衝突が起きた。ndf v10.27.14 が他の 2 本の変更もまとめて出し、Pull Request https://github.com/devbasex/ai-plugins/pull/2420 を閉じた。同じ衝突が、同じリポジトリの実行ジョブの間でも起きる。#2436 のオーガナイザーは 1 台の SQLite に閉じ、実行ジョブの間をまたげない +- **誰に効くか**: 同じリポジトリで 2 本以上の開発ループかスプリントを Factory で回す利用者 +- **変えると何が成り立つか**: 本番の配布は Factory のオーガナイザーが 1 本にまとめ、配布専用の実行ジョブが流す。承認ゲート 2 は 1 回で済み、承認と問いはリポジトリごとの 1 つの一覧に並ぶ。語の衝突 2 つ(手元の受け手・アカウントの契約の種類)が解ける +- **なぜ衝突が無くなるか**: 衝突の原因は、実行ごとの本番の配布が `develop` の先頭を出すため、先に出した実行が他の実行の変更も出すことにある。この設計では、同じリポジトリで本番の配布を流すのは配布の実行 1 本だけになる(I56・I57)。出すのは承認したコミットまでの範囲で、範囲に Pull Request が入った実行すべてへ同じ版と本番の Pull Request を返す(I61)。範囲の外の変更は次のまとめた配布で出る。この仕組みの外で本番を出しうるもの(Factory の外の本番の Pull Request と、依頼を出さない古い NDF の版の実行)がある間は組まない(I62) + +## 適用範囲 + +- **働く範囲**: `factory/`(API・ランナー・契約・画面の文言)と、NDF の側の切り替え口(`plugins/ndf/`。Factory を名指ししない)。NDF の切り替え口は、外のオーガナイザーとファイルで依頼と答えをやり取りしたいどのリポジトリでも使える +- **プロジェクトごとに違うもの**: 本番の配布の手段は各リポジトリのリリースのテンプレート(`.ndf/release.json`)のまま。承認ゲート 2 を MVV 判定に任せるかは各リポジトリの `.ndf/pace.json` のまま +- **当たるモード**: `standard`。オーガナイザーはモードを持たない。実行の中のスプリントは今どおりモードを判定する + +## あるべき姿の根拠 + +| 根拠 | 種類 | 何を示すか | +| --- | --- | --- | +| #2439 の「依頼(原文)」(2026-11-02) | 利用者の指示の原文 | Factory にもまとめ役が要る。「上位のオーガナイザー」ではなく、同じ役割を Factory の側で担う実装として定義する | +| Pull Request 2420 を閉じた件 | 実測 | 本番の配布が `develop` の先頭を出すため、先に出した実行が他の実行の変更も出す | +| #2436 の設計(`design/issue-2436` の `issues/issue-2436-design*.md`。決定 13〜15・19・21) | 設計の正本 | 配布の中身を `develop` のコミットの範囲で決める。本番の Pull Request の head は `release/v<版>`。依頼と答えの形は NDF の契約 | +| Factory の確定仕様 [ndf-factory-dev-loops.md](../docs/specifications/ndf-factory-dev-loops.md) の概要 | 設計の正本 | 常駐のプロセスを持たない。実行をまたぐ制御は API の handler と 5 分ごとの見回りに置く | +| `factory/` の調査(2026-11-02) | 実測 | 承認の依頼の保存と一覧(#2173)、手元の受け手の登録の経路(#2189)はまだ無い。本番の配布はランナーの中の NDF が流し、Factory は関わらない | + +要求と受け入れ条件は #2439 の本文にある(コピーは [issue-2439-requirements.md](issue-2439-requirements.md))。この文書は「どう作るか」だけを扱う。部分の設計は次の 3 つに分けた。 + +| 文書 | 扱う部分 | +| --- | --- | +| [issue-2439-design-data.md](issue-2439-design-data.md) | データ構造と入出力の契約(DynamoDB の行・API・進捗の事象・ランナーの指示) | +| [issue-2439-design-ndf.md](issue-2439-design-ndf.md) | NDF の側の切り替え口(外のオーガナイザーとの置き場の契約)と、語の改名 | +| [issue-2439-design-decisions.md](issue-2439-design-decisions.md) | 決定の記録 | + +## ドメインモデル + +### コンテキスト + +| コンテキスト | 何の語が 1 つの意味に決まるか | +| --- | --- | +| Neural Development Factory(`ndf-factory`) | Factory のオーガナイザー・配布の実行・受けた依頼・まとめた配布・提示・待ちの理由・手元のマシン・料金プラン | +| NDF の開発ワークフロー(`ndf-workflow`) | オーガナイザー・レーン・配布の依頼・承認ゲート 2・承認したコミット | + +**Factory のオーガナイザーは、既存の実行の制御・ループの制御と、この課題で足す配布のまとめでできる役割である。** 1 台の中のオーガナイザーの仕事と Factory の担い手は次のとおり対応する。 + +| 1 台の中のオーガナイザーの仕事(#2436) | Factory の担い手 | この課題で | +| --- | --- | --- | +| レーンを起動・監視・停止する | 実行の制御・ループの制御(周回の開始・停止の後の処理・見回り・取り消し) | 変えない | +| 本番の配布を 1 本にまとめる | 配布のまとめ(`OrganizerControl`) | 新規 | +| 問いを集める | 配布のまとめの一覧(リポジトリごと) | 新規 | +| 答えの後の作業を持ち主のレーンへ返す | `requestResume`・ループの答え・ランナー向けの答えの経路 | 変更 | +| 共有の作業を一時レーンへ渡す | 配布の実行 | 新規 | +| (Factory 固有)実行ジョブの起動・停止・答えでの再開、実行の枠と手元のマシンの枠、ジョブをまたぐ状態の置き場 | 実行の制御・ランナー向けの経路 | 変えない | + +`ndf-workflow` は依頼と答えの形を**公開された言語**として持つ。形のファイルは 2 つある。#2436 の `plugins/ndf/contracts/forms/parallel-request.json` と、この課題が足す置き場の契約 `parallel-external.json` である。`ndf-factory` はランナーの翻訳(`factory/runner/src/organizer.ts`)を**腐敗防止層**として置き、ファイルの形を進捗の事象へ写す。API は NDF のファイルを読まない。今の `ndf-next` のブロックと `loop.json` の扱いと同じである。 + +### 集約 + +| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | +| --- | --- | --- | --- | --- | +| オーガナイザーの進行 | 配布のまとめ(`OrganizerControl`) | リポジトリのオーガナイザー(`repositoryId`) | 受けた依頼・まとめた配布 | 待ちの理由・提示(コミット・範囲の実行・加わった範囲)・答え・配布の結果・本番の Pull Request | +| スプリントの実行(変更) | 実行の制御(`RunControl`) | スプリントの実行 | 実行ジョブ | 目的(`sprint` / `release`)・属するまとめた配布・待っている依頼 | +| 開発ループ | ループの制御(変えない) | ループ | — | 人の判断待ち・答え | +| 承認の依頼 | 承認と通知(#2173。変えない) | 承認の依頼 | — | 種類・答え | + +- オーガナイザーの進行は、ほかの集約を識別子でだけ参照する(実行は `sprintRunId`、ループは `loopId`、承認の依頼は `approvalRequestId`) +- **1 つの処理が書き換える集約は 1 つ**で、例外は 2 つである。まとめた配布を組む処理は、オーガナイザーの進行と配布の実行(実行とジョブ)を 1 つのトランザクションで書く(I57)。停止の後の処理は、ジョブの `release.requested` を受けた依頼の行として同じトランザクションへ入れる(I59)。どちらも、間で止まったときに見回りが 2 本目を起こすか依頼を取りこぼすためである。扱いは既存の 3 集約のトランザクションと同じにする([ndf-factory-dev-loops.md](../docs/specifications/ndf-factory-dev-loops.md) の「集約」) +- 承認の依頼と開発ループの人の判断待ちは書き換えない。一覧(F6)はそれらを読んで並べるだけである + +### 不変条件 + +番号は Factory の確定仕様の続き(I55 の次)から振る。 + +| # | 集約 | 条件 | 破れたときの扱い | +| --- | --- | --- | --- | +| I56 | オーガナイザーの進行 | 同じリポジトリで終わりの状態でないまとめた配布は 1 つまで | 組む書き込みは根の `currentSeq` が空で `version` が読んだ値のときだけ通す。落ちたら読み直し、組まない | +| I57 | オーガナイザーの進行 | まとめた配布 1 つに配布の実行は 1 本で、まとめた配布と同じトランザクションで作る | 片方だけを書く経路を持たない。トランザクションが落ちればどちらも無い | +| I58 | オーガナイザーの進行 | 受けた依頼 1 件に答えは 1 回だけ書く | 答えの書き込みは `attribute_not_exists(answer)` を条件にし、落ちたら書かない | +| I59 | オーガナイザーの進行 | 同じ依頼の id は 1 行(ランナーが再送しても増えない) | 停止の後の処理は在る行を読んでから無い行だけを書く。進捗の事象は冪等の鍵 `<依頼の id>` で 1 件になる | +| I60 | オーガナイザーの進行 | 提示は 1 つのまとめた配布に 2 つまで。`develop` の先頭を比べるのは提示 1 への承認のときだけ | 提示 2 への答えでは GitHub を読まない。3 つ目の提示を組む経路を持たない | +| I61 | オーガナイザーの進行 | 答えを返す依頼は、まとめた配布の終わり方で決まる。配布済みでは、確定した範囲に Pull Request が入る依頼へ `released` を返す。差し戻しでは、最後の提示の範囲に Pull Request が入る依頼へ `rejected` を返す。提示の前に止まった配布の差し戻しでは、まとめた配布が受けた依頼すべてへ `rejected` を返す。どの範囲にも入らない依頼は次のまとめた配布へ残る | 配布済みの範囲は、配布の実行が返す `release.settled` の `prs` で照らす。差し戻しはジョブを起こさず `release.settled` も来ないため、まとめた配布に記録した最後の提示の `prs` で照らす。照らせない依頼は答えずに残す | +| I62 | オーガナイザーの進行 | Factory の外の本番の Pull Request(宛先 `main`、head が `release/v` で始まるか `develop`)が開いている間、またはリポジトリに依頼を出していない古い NDF の版の実行がある間は、まとめた配布を組まない。依頼を出していない古い版の実行の数は、リポジトリの枠の `legacyCount` から、受けた依頼を待つ `awaiting_approval`(`release_requested`)の実行のうち `legacy` が真のものの数を引いた値である(依頼を出した実行は自分で配布しないため数えない)。逆に、まとめた配布が終わりの状態でない間は、古い版の実行を始めない | 組まずに待ちの理由(`external_release_open` / `legacy_run`)を根と記録の行に書く。次の見回りで照らし直す。組む書き込み(I57)は、リポジトリの枠の `legacySeq`(古い版の実行を数えに入れるたびに 1 増える版)が照らした時に読んだ値と同じであることを条件に入れる(読んだ時に無ければ無いことを条件にする)。照らした後に古い版の実行が 1 本でも数えに入れば、`legacyCount` が同じ値へ戻っていても落ち、次の見回りで照らし直す。古い版の実行の開始([ndf-factory-dev-loops.md](../docs/specifications/ndf-factory-dev-loops.md) の I48 が求める、リポジトリの枠の `count` が 0 の条件の書き込み)は、オーガナイザーの根の `currentSeq` が空であることを条件に入れ、落ちれば 409 `parallel_unsupported_version`(ループは待つ)。どちらも相手の集約を条件として読むだけで書き換えない。古い版の見分け方は [issue-2439-design-data.md](issue-2439-design-data.md) の「互換性」にある | +| I63 | オーガナイザーの進行 | `main` へのマージを記録したまとめた配布は差し戻せない | 止まった配布の選択肢は「続きから流す」だけを返し、差し戻しの操作と答えを 409 `release_after_main` で断る | +| I64 | スプリントの実行 | 配布の実行は実行の枠(アカウント・リポジトリ)を持たず、使える量の判定にかけない。トークンと費用は登録したアカウントの利用量へ数える | 配布の実行の作成は枠の行を書かない。`judgeLimits` を呼ばない | +| I65 | スプリントの実行 | 配布の依頼を出したループの実行は `done` で終わる(周回は配布待ちで締まる)。手動の実行は `awaiting_approval`(理由 `release_requested`)で枠を持ったまま答えを待つ。配布の実行は、ジョブが失敗しても差し戻しか配布済みまで終わりの状態にならない(理由 `release_stalled` の `awaiting_approval`) | 停止の後の処理が実行の目的と `job.finished` の結果と理由から決める。表に無い組は今の扱いのまま | +| I66 | 承認の依頼 | 保存した承認の依頼へ答えを書けるのは人だけ。一覧の API と MCP は読むだけで、答えを書く経路を持たない。MVV 判定が承認ゲート 2 を通すときは承認の依頼を作らないため、この条件の対象にならない(E12) | 一覧の経路は GET だけを持つ。止まった配布を解く操作は Web のセッションの認証でだけ受ける | +| I67 | オーガナイザーの進行 | 根の状態を変える書き込みは `version` を条件にする(API の handler と見回りが同時に進めても 1 回だけ通る) | 落ちたら 3 回まで読み直す。状態が変わっていれば何もしない | +| I68 | スプリントの実行 | Factory の実行ジョブでは 1 台の中のオーガナイザーを起動せず、本番の配布は依頼として出す | ランナーが `NDF_FACTORY_RUN=1`・`NDF_ORGANIZER_DIR`・`NDF_ORGANIZER_LANE` を必ず渡す。#2436 の `parallel.py init`・`run` は `NDF_FACTORY_RUN` で断る | + +### ドメインイベント + +| # | イベント | 発生元 | 受け手 | +| --- | --- | --- | --- | +| E1 | 実行が配布の依頼を出した(`release.requested`) | ランナー(NDF の依頼のファイルの翻訳) | 停止の後の処理(受けた依頼の行)→ 配布のまとめ | +| E2 | 配布の範囲を決めるまとめた配布を組んだ | 配布のまとめ(`tick`) | 実行の制御(配布の実行を起こす) | +| E3 | 承認資料をそろえ、承認ゲート 2 で止まった(`release.presented`・`gate.reached`) | ランナー(配布の実行) | 配布のまとめ(提示の記録)・承認と通知(承認の依頼) | +| E4 | 人が承認ゲート 2 の承認の依頼に答えた | 承認と通知(`requestResume`) | 配布のまとめ(`decide`) | +| E5 | 答えの時点の `develop` が進んでいたので出し直した | 配布のまとめ(`decide`) | 配布の実行(`represent` で再開) | +| E6 | 配布の実行を起こした | 配布のまとめ(`tick`・`decide`) | 実行の制御(`launch`) | +| E7 | 本番の Pull Request を 1 本作ってマージし、版を出した(`release.progress`・`release.settled`) | ランナー(配布の実行) | 配布のまとめ(`settle`) | +| E8 | 範囲のすべての実行へ配布の結果を返した | 配布のまとめ(`settle`) | 実行の進捗の記録(`release.answered`)・次のジョブ(答えの経路)・手動の実行(`requestResume`) | +| E9 | 実行が問いの依頼を出した(承認ゲートの外) | ランナー(今の `gate.reached` の判断・ループの `pending`) | 承認と通知・ループの制御(今のまま) | +| E10 | 問いの答えを持ち主の実行へ返した | 承認と通知(`requestResume`)・ループの制御(答えの保存) | 持ち主の実行か、ループの次の周回(今のまま) | +| E11 | Factory の外の本番の Pull Request が開いていたので待った | 配布のまとめ(`tick`) | 一覧(待ちの理由)・記録の行 | +| E12 | MVV 判定が承認ゲート 2 を通した(`release.presented` の `auto` が真。承認の依頼を作らない) | ランナー(配布の実行) | 配布のまとめ(提示 1 の記録と確定。`decide` を通さない) | + +### 用語 + +| 用語 | 意味 | 用語集への反映 | +| --- | --- | --- | +| Factory のオーガナイザー | 同じリポジトリで並行する Factory の実行をまとめる役割。1 台の中のオーガナイザーの仕事すべて(配布を 1 本にまとめる・問いを集める・答えの後の作業を持ち主へ返す・共有の作業を一時のレーンへ渡す)に、Factory 固有の仕事(実行ジョブの起動・停止・答えでの再開、実行の枠と手元のマシンの枠、ジョブをまたぐ状態の置き場)を足したもの。API の handler と見回りの中で動き、常駐しない。1 台の中のオーガナイザーと同じ層の別の実装で、互いに排他 | 追加(`ndf-factory`) | +| 配布の実行 | Factory のオーガナイザーが、まとめた配布 1 つのために起こすスプリントの実行。範囲のどの実行でもなく、承認資料を作り、本番の配布を流し、結果を返して終わる。同じリポジトリで同時に 1 本まで | 追加(`ndf-factory`) | +| 手元のマシン | 利用者の環境に登録し、同時のジョブの数まで実行ジョブを受け取るマシン。受け取った実行ジョブごとに別のランナーが動く | 追加(`ndf-factory`。廃止した語`手元のランナー`) | +| 手元のマシンの枠 | 1 つの手元のマシンが `running` で持つ `local` の実行ジョブの数を数えた実行の枠の行 | 意味の変更(語を改める。廃止した語`手元のランナーの枠`) | +| 料金プラン | アカウントの契約の種類。Free・個人 Pro・個人 Max・Team の 4 つ | 意味の変更(`ndf-factory` の「プラン」を改める。`ndf-workflow` の「プラン」は変えない) | + +配布のまとめ・受けた依頼・まとめた配布・提示・待ちの理由は、この設計の中だけで使う構成要素の名前で、用語集へは足さない。 + +### 既存の語と記号 + +この設計が定義し直さずに使う既存の語と記号は、次の正本の意味で読む。 + +| 語・記号 | 意味 | 正本 | +| --- | --- | --- | +| 切り替え口 | NDF が本番の配布を自分で組まずに、外のオーガナイザーへ配布の依頼をファイルで出す経路。環境変数 `NDF_ORGANIZER_DIR` があるときだけ働く | この設計([issue-2439-design-ndf.md](issue-2439-design-ndf.md))。要求の「前提 2」 | +| レーン | 並列の計画の下で 1 本のセッションが進める開発の流れ。Factory では、ループ 1 つか手動の実行 1 つが 1 つのレーンになる(名前は `loop-` / `run-`) | #2436 の設計の「用語」 | +| 一時レーン | #2436 で、持ち主のいない共有の作業(まとめた配布のプランの実行と、その失敗の対応)を受け持つ短命のレーン。この設計では配布の実行がその役を受け持つ | #2436 の `issue-2436-design-approvals.md` | +| 1 台の中のオーガナイザー | #2436 の、1 台のマシンの中でレーンを起動・監視し、配布の依頼をまとめる conductor の役割 | #2436 の設計の「用語」(語は「オーガナイザー」) | +| 承認ゲート 1・承認ゲート 2 | 人の承認を求める 2 つの点。1 は設計 Pull Request のマージ、2 は本番系へ届く操作(本番の配布) | 用語集 `ndf-workflow` の「承認ゲート」「ゲート 1」「ゲート 2」 | +| MVV 判定 | 承認ゲートの資料がプロジェクトの MVV(Mission / Vision / Value)に従うかの判定。`.ndf/pace.json` が許すときは、従うと判定した承認ゲートを人の答えなしで通す | 用語集 `ndf-workflow` の「MVV 判定」 | +| conductor | 人と対話しているセッション。承認ゲートで人へ問えるのはこの層だけ | 用語集 `ndf-workflow` の「conductor」 | +| `/goal` | Claude Code の、目標の条件を満たすまでセッションを止めずに進めさせるコマンド。NDF は工程 Skill をこの条件の先頭に置いて始める | `development-workflow` の `references/relay.md` | +| 見回り | 実行の制御が 5 分ごとに動かす処理 | 用語集 `ndf-factory` の「見回り」 | +| 古い版の実行・`legacy`・`legacyCount`・`NDF_PARALLEL_MIN_VERSION` | 並行を許す NDF の版の下限(設定 `NDF_PARALLEL_MIN_VERSION`)より古い版で始まった実行を古い版の実行と呼び、実行の行の `legacy` を真にする。リポジトリの枠の `legacyCount` は、終わりの状態でない古い版の実行の数 | [ndf-factory-dev-loops.md](../docs/specifications/ndf-factory-dev-loops.md) の I48 と「並行の開始」、[ndf-factory-dev-loops-data-and-contracts.md](../docs/specifications/ndf-factory-dev-loops-data-and-contracts.md) | +| I1〜I55 | Factory の確定仕様の不変条件。I48 は [ndf-factory-dev-loops.md](../docs/specifications/ndf-factory-dev-loops.md) の「古い版の実行がリポジトリにあれば、その実行は唯一である」を指す(`ndf-factory-issue-requests.md` の同じ番号とは別) | 各確定仕様の「不変条件」 | +| S1〜S11・前提 1〜5・未決 1〜4・AC と受け入れ条件の番号 | 要求の「起票者の推定」・「前提」・「未決」・「受け入れ条件」の番号。AC は受け入れ条件(acceptance criteria) | [issue-2439-requirements.md](issue-2439-requirements.md) | +| Value 1〜10・Mission・C4(MVV 版 4) | 決定の根拠に挙げた、プロジェクト MVV の第 4 版の項目。C4 は NDF の共通原則の「必ず承認が要る操作」の 4 番目(戻せない操作) | `.ndf/mvv.md`、`issues/ndf-common-principles.md` | +| `pending_source` | 用語集の語が、まだ確定仕様になっていない設計文書を出典とすることを示す項目。確定仕様化で `source` へ移す | `plugins/ndf/scripts/glossary.py`、[ndf-glossary-merge.md](../docs/specifications/ndf-glossary-merge.md) | + +## 機能一覧 + +| # | 機能 | 誰が使うか | +| --- | --- | --- | +| F1 | 実行が本番の配布の代わりに配布の依頼を出し、答えを待たずに周回を締める | 実行の中の NDF・ランナー | +| F2 | 配布の依頼を受け、流れている配布が無ければ直ちにまとめた配布を組み、配布の実行を起こす | 配布のまとめ | +| F3 | 承認資料を作って承認ゲート 2 で止まるか、MVV 判定が通せば続けて本番の配布を流す | 配布の実行 | +| F4 | 承認の答えの時点で `develop` が進んでいれば 1 回だけ出し直し、確定した提示の本番の配布を流させる | 配布のまとめ | +| F5 | 配布の結果を範囲の実行へ返し、範囲に入らない依頼で次のまとめた配布を組む | 配布のまとめ | +| F6 | リポジトリごとの承認と問いの一覧と、まとめた配布の履歴を読む | 利用者(Web・API・MCP) | +| F7 | 止まった配布を「続きから流す」か「差し戻す」で解く(`main` へのマージの後は前者だけ) | 利用者(Web) | +| F8 | 途中で止まった配布のまとめの処理を、見回りが記録から続ける | 実行の制御(見回り) | +| F9 | 手元のマシンと料金プランの語へ説明文・用語集・画面の文言を改める | 利用者(読み手) | + +## 構成要素 + +| 要素 | 責務 | 新規 / 変更 | +| --- | --- | --- | +| `factory/api/src/organizer/control.ts`(配布のまとめ。`OrganizerControl`) | `accept`(受けた依頼)・`tick`(待ちの判定・組む・`settle` の続き)・`decide`(承認の答えと出し直し)・`settle`(結果と答え)・`resolve`(止まった配布)・`list`(一覧)・`sweep` | 新規 | +| `factory/api/src/organizer/model.ts` | 根・受けた依頼・まとめた配布・記録の行の型と、状態の遷移の純粋な関数 | 新規 | +| `factory/api/src/runs/table.ts`・`dynamo-table.ts`・`memory-table.ts` | オーガナイザーの進行の行の読み書きと、まとめた配布と配布の実行を 1 回で書くトランザクション(`composeRelease`。リポジトリの枠の `legacySeq` を条件にする)。古い版の実行の開始で `legacyCount` と同じトランザクションで `legacySeq` を 1 増やす。`addResumeJob` に、待っている理由と依頼の id を照らす条件を足す | 変更 | +| `factory/api/src/runs/backfill.ts` | 実行を古い版として `legacyCount` へ写す書き込みで `legacySeq` を 1 増やす | 変更 | +| `factory/api/src/runs/control.ts`(`RunControl`) | `finalize` が `release.*` を読んで受けた依頼の行を同じトランザクションへ入れ、後で `organizer.tick` を呼ぶ。`requestResume` が配布の実行を `organizer.decide` へ回す。`sweep` が `organizer.sweep` を呼ぶ。配布の実行を枠の外で作る | 変更 | +| `factory/api/src/runs/model.ts` | 実行の `purpose`・`releaseSeq`・`releaseRequestId`、ジョブの `start: release`・`releaseStep` | 変更 | +| `factory/api/src/github/port.ts`・`app.ts` | `branchHead(repo, "develop")`・宛先 `main` の開いている Pull Request の head の一覧・head を指定した Pull Request(開いている・マージ済み)・`release/v<版>` の枝の有無 | 変更 | +| `factory/api/src/routes/factory.ts` | 一覧・まとめた配布の履歴・止まった配布を解く操作の 3 経路 | 変更 | +| `factory/api/src/runs/runner-api.ts` | ランナー向けの 2 経路(答えと範囲・まとめた配布の中身) | 変更 | +| `factory/api/src/progress/authorize.ts` | 出所 `runner` が `release.*` の 4 種類を書け、出所 `control` が `release.answered` を書ける | 変更 | +| `factory/runner/src/organizer.ts` | 置き場の翻訳: 依頼・提示・進み・結果のファイル → 進捗の事象。答え・範囲・まとめた配布 → ファイル。未回答の依頼の再送 | 新規 | +| `factory/runner/src/main.ts`・`runtime.ts`・`translate.ts` | `START=release` の手順。環境変数 `NDF_FACTORY_RUN`・`NDF_ORGANIZER_DIR`・`NDF_ORGANIZER_LANE`。配布の依頼で締めた手動の実行の終わり方 | 変更 | +| `factory/contracts/`(`schemas/progress-event.schema.json`・`schemas/run-job-instruction.schema.json`・`openapi/organizer.yaml`・`openapi/runs.yaml`) | 契約([issue-2439-design-data.md](issue-2439-design-data.md)) | 変更・新規 | +| NDF の切り替え口(`plugins/ndf/`) | 外のオーガナイザーとの置き場の契約と、依頼の書き出し・答えの読み込み・配布の実行の手順([issue-2439-design-ndf.md](issue-2439-design-ndf.md)) | 変更・新規 | +| 語の改名(用語集・Factory の確定仕様・`factory/` の説明文・`factory/web/src/i18n/ja.json`) | F9([issue-2439-design-ndf.md](issue-2439-design-ndf.md) の「語の改名」) | 変更 | + +```mermaid +graph TD + subgraph sg1["API(factory/api)"] + RT[画面向けの経路] + RAPI[ランナー向けの経路] + RC[実行の制御] + OC[配布のまとめ] + TBL[実行の表] + GH[GitHub の口] + end + subgraph sg2["実行ジョブ(ECS か手元のマシン)"] + TR[置き場の翻訳] + MAIN[ランナーの手順] + end + NDF["NDF(切り替え口)"] + AP["承認と通知(#2173)"] + ING[進捗の受け口] + RT --> OC + RAPI --> OC + RC -->|finalize・sweep・requestResume| OC + OC -->|composeRelease・行| TBL + OC -->|launch| RC + OC --> GH + OC -->|開いている承認の依頼| AP + AP -->|requestResume| RC + MAIN --> TR + TR -->|置き場のファイル| NDF + TR -->|release.*| ING + MAIN --> RAPI +``` + +画面(`factory/web`)は文言の改名だけで、一覧の画面は #2430 が持つため図に含めない。型の定義(2 つの `model.ts`)・出所の許可(`authorize.ts`)・契約のファイル・語の改名は、上の要素が使う定義と文書で、図に含めない。 + +### 置き場(パッケージ構成) + +```text +factory/ +├── api/src/ +│ ├── organizer/{control,model}.ts # 新規 +│ ├── organizer/control.test.ts # 新規(メモリの表) +│ ├── runs/{backfill,control,model,table,dynamo-table,memory-table,runner-api}.ts # 変更 +│ ├── github/{port,app}.ts # 変更 +│ ├── progress/authorize.ts # 変更 +│ └── routes/factory.ts # 変更 +├── runner/src/ +│ ├── organizer.ts # 新規 +│ └── {main,runtime,translate}.ts # 変更 +├── contracts/ +│ ├── openapi/{organizer.yaml,runs.yaml} # 新規・変更 +│ └── schemas/{progress-event,run-job-instruction}.schema.json # 変更 +└── web/src/i18n/ja.json # 変更(文言) +``` + +### システムの文脈と配置 + +```mermaid +graph TD + U[利用者の Web] --> API[API の handler] + EB[EventBridge 5 分] --> SW[見回りの Lambda] + API --> DB[(DynamoDB の実行の表)] + SW --> DB + API --> GHB[GitHub] + SW --> GHB + SW -->|RunTask| L1[実行ジョブ: レーン] + SW -->|RunTask| R1[実行ジョブ: 配布の実行] + L1 -->|release.requested| ING[進捗の受け口] + R1 -->|release.presented 等| ING + R1 -->|本番の Pull Request| GHB +``` + +配布のまとめは新しいプロセスもインフラの部品も持たない。既存の API の Lambda と見回りの Lambda の中で、実行の制御から呼ばれる。見回りが引く索引は、実行の表の既存の GSI `gsi1`(待っているループ `WAITING_LOOPS` などを引く疎な索引)に鍵 `ORGANIZER_ACTIVE` の行を載せて使い、インフラの定義は変えない。 + +## 処理の流れ + +### まとめた配布の状態 + +```mermaid +stateDiagram-v2 + [*] --> composed: tick が組んだ(配布の実行を起こした) + composed --> presented: 承認ゲート 2 で止まった + composed --> confirmed: MVV 判定が通した + presented --> confirmed: 答え(develop が同じ / 提示 2 への答え) + presented --> represented: 提示 1 への承認で develop が進んでいた + represented --> presented: 提示 2 の承認資料で止まった + presented --> rejected: 差し戻し + confirmed --> released: release.settled(released) + composed --> stalled: 配布の実行のジョブが失敗した + represented --> stalled: 配布の実行のジョブが失敗した + confirmed --> stalled: 配布の実行のジョブが失敗した + stalled --> composed: 続きから流す(提示の前に止まった) + stalled --> represented: 続きから流す(出し直しの途中で止まった) + stalled --> confirmed: 続きから流す(確定の後に止まった) + stalled --> rejected: 差し戻す(main のマージの前だけ) + released --> [*] + rejected --> [*] +``` + +### 依頼から結果まで + +```mermaid +sequenceDiagram + participant L as レーンの実行ジョブ + participant C as 実行の制御 + participant O as 配布のまとめ + participant R as 配布の実行ジョブ + participant A as 承認と通知 + L->>C: release.requested・job.finished + C->>O: finalize で依頼の行 → tick + O->>O: 外の Pull Request と古い版を照らす + O->>C: composeRelease → launch(START=release) + R->>O: release.presented・gate.reached(gate2) + A->>C: requestResume(人の答え) + C->>O: decide(develop の先頭を比べる) + O->>C: 再開(confirm:k か represent) + R->>O: release.settled(finalize → settle) + O->>L: 答えと範囲(次のジョブ)・release.answered +``` + +1. **依頼(F1)**: NDF は本番のステージの代わりに依頼のファイルを置き場へ書く([issue-2439-design-ndf.md](issue-2439-design-ndf.md))。ランナーは 2 秒ごとの読みで `release.requested` を送る。ループの実行は周回を配布待ちで締めて `done` で、手動の実行は `job.finished`(`gate`・理由 `release_requested`)で終わる(I65) +2. **受ける**: `finalize` は `release.requested` をジョブの冪等の鍵の範囲で読む。受けた依頼の行は、実行の移りと同じトランザクションで書く(I59)。通った後に `organizer.tick(repositoryId)` を呼ぶ(落ちてよい。見回りが続ける) +3. **組む(F2)**: `tick` は根を読み、まとめた配布が流れていなければ、未回答の依頼があるときだけ組む。先に I62 を照らし、当たれば待ちの理由を書いて終える。当たらなければ `composeRelease` で 5 つの行を 1 回で書き、`launch` する。5 つは根(`currentSeq`・`nextSeq`・`version`)・まとめた配布(`composed`)・配布の実行・ジョブ(`START=release`、`releaseStep=present`)・記録の行である。依頼が 1 件でも同じである(S8) +4. **提示(F3)**: 配布の実行は、ランナー向けの経路でまとめた配布の中身(レーンの一覧と依頼)を読み、NDF の配布の手順を流す。MVV 判定が通せば、`release.presented`(`auto` が真)を送って本番の配布まで続ける。このとき承認の依頼は作らず、配布のまとめは `decide` を通さずにまとめた配布を `confirmed`(`confirmedK=1`)にする(E12)。止まれば `release.presented`(提示の番号・コミット・範囲・加わった範囲)と `gate.reached`(`gate2`)を送って終わる。配布の実行は `awaiting_approval` になる。承認の依頼は #2173 が今の経路で作る +5. **答え(F4)**: #2173 が答えを保存して `requestResume` を呼ぶと、実行の制御は配布の実行を `organizer.decide` へ回す。提示 1 への承認では GitHub の口で `develop` の先頭を読み、提示したコミットと比べる。同じなら `confirm:1`、進んでいれば `represent` で再開する。提示 2 への答えは比べずに `confirm:2`(新しい範囲)か `confirm:1`(最初の範囲)で再開する(I60)。`confirm:` で出すのは提示 k のコミット(まとめた配布の `presentations` の k 番目の `sha`)で、本番のプランはそのコミットから `release/v<版>` を切る。答えの後に `develop` が進んでも、出すコミットは変わらない(#2436 の設計の「まとめた配布」と `issue-2436-design-release-rule.md` の「`release/v<版>` を切る起点」)。差し戻しはジョブを起こさない。まとめた配布を `rejected`、配布の実行を `stopped`(`release_rejected`)にし、最後の提示の範囲の依頼へ `rejected` を返す(I61)。範囲の実行ごとに `release.answered` を書き、手動の実行には `requestResume`(依頼の識別子つき)を呼ぶ。届け方は 6 の配布済みと同じである。続けて `tick` を呼び、範囲に入らない依頼で次のまとめた配布を組む +6. **結果(F5)**: 配布の実行の `finalize` が `release.settled` を読み、`settle` へ渡す。`settle` は根を空け、まとめた配布を `released` にし、範囲に Pull Request が入る依頼へ答えを書く(I58・I61)。範囲の実行ごとに `appendControlEvent` で `release.answered`(冪等の鍵 `:`)を書く。手動の実行には `requestResume`(依頼の識別子つき)を呼ぶ。依頼ごとに、この 2 つが通った後に `deliveredAt` を書いて見回りの索引から外す(答えを書いただけでは外さない。途中で止まっても見回りが届け直す)。続けて `tick` を呼び、残った依頼で次のまとめた配布を組む +7. **答えを届ける**: レーンの次のジョブは、ループの次の周回か手動の実行の再開である。その開始で、ランナーは答えと範囲を読んで置き場へ書き、答えの無い依頼のファイルを再送する(I59)。NDF は周回の境で答えを読み、`released` なら残りの工程を流す(#2436 のレーンのループ) + +### 止まったときと再開(F7・F8) + +| 場面 | 扱い | +| --- | --- | +| `tick` が組む前・組んだ後に止まった | トランザクションの単位で残るか消えるかが決まる。次の `tick`(見回りか次の `finalize`)が根から続ける | +| `composed` のまま `launch` の前に止まった | 見回りは、`composed` のまとめた配布の配布の実行のジョブが `pending` のまま起動の記録が無ければ `launch` し直す(ジョブの冪等は今の `startedBy`) | +| 配布の実行のジョブが失敗した・打ち切られた | `finalize` は配布の実行を終わりの状態にせず `awaiting_approval`(理由 `release_stalled`)へ移す。続く `tick` がまとめた配布を `stalled` にし、`release.progress` に `main_merged` があるか、GitHub で head が `release/v<提示の版>` の `main` 宛の Pull Request がマージ済みなら選択肢を「続きから流す」だけにする(I63)。一覧に止まった配布として並ぶ | +| 止まった配布を「続きから流す」 | `resolve` が同じ配布の実行を `requestResume` と同じ書き込みで再開する。配布の実行を作り直さない(I57)。再開の手(`releaseStep`)は `stalledFrom` で決める。`composed` は `present`(提示 1 のプランから作り直す)、`represented` は `represent`(提示 2 のプランから作り直す)、`confirmed` は、配布の実行の進捗の記録に `release.progress` が 1 件でもあるか、`mainMerged` が真か、GitHub の口で読んで `release/v<提示の版>` の枝か head がその枝の `main` 宛の Pull Request があれば `continue`(NDF が保存した本番のプランの状態から続ける)、どれも無ければ `confirm:`(本番のプランを初めから流す)である。ジョブは段の事象を送る前に落ちうるため、GitHub の状態でも照らす(決定 11)。まとめた配布の `state` は `stalledFrom` の値へ戻す | +| 止まった配布を「差し戻す」 | `resolve` が、答え(5)の差し戻しと同じ処理をする。ジョブを起こさず、範囲の依頼へ `rejected` を返す(I61)。提示の前に止まった配布では、まとめた配布が受けた依頼すべてが範囲になる | +| 配布の実行の中の NDF が判断を求めた | 今の経路(`gate.reached` の判断 → 承認の依頼 → `requestResume`)。`decide` は `continue` で再開する。`main` のマージの後は `gate.reached` の `choices` を `continue` だけにする | +| `settle` の後、答えを書き終える前に止まった | 見回りが `released` のまとめた配布で、範囲の依頼のうち答えの無いものを書き直す(I58 で 2 回目は書かれない) | +| 答えを書いた後、届け終える前に止まった | 答えがあり `deliveredAt` が空の依頼は見回りの索引に残る。見回りが `release.answered` を追記し直し(冪等の鍵で 1 件)、手動の実行には `requestResume`(`releaseRequestId` つき)を呼び直してから `deliveredAt` を書く。呼び直しが再開するのは、実行が `awaiting_approval` で、`awaitingReason` が `release_requested`、`releaseRequestId` がこの依頼の id と一致するときだけである。この照らしは再開のジョブを作る条件つきの書き込み(`addResumeJob`)の条件に入れ、読んでから書くまでの間に実行の待ちが変わっても通さない。`settle` の `requestResume` が通った後で止まり、再開した実行が別の判断・承認ゲートで `awaiting_approval` に戻っていれば、条件に当たらず再開しない。このときは「実行がもうこの依頼を待っていない」として `deliveredAt` を書く | +| 手動の実行の `requestResume` が `sprint_run_busy` | `deliveredAt` を書かずに残す。見回りが、答えのある依頼を待つ実行(`awaiting_approval`・`awaitingReason` が `release_requested`・`releaseRequestId` がこの依頼の id)へ、上の行と同じ条件つきの書き込みで呼び直す。実行がもうこの依頼を待っていなければ(取り消し・終わり・別の理由や別の依頼での `awaiting_approval`)呼ばずに `deliveredAt` を書く | + +見回りは実行の表の既存の GSI `gsi1` を鍵 `ORGANIZER_ACTIVE` で引き(流れているまとめた配布を持つ根と、答えを届け終えていない依頼の行だけが載る)、リポジトリごとに `tick` を呼ぶ。`tick` は根の続きに加えて、答えがあって届け終えていない依頼へ届け直す。 + +## 非機能の実現方式 + +| 大項目 | 条件 | 実現方式 | +| --- | --- | --- | +| 可用性 | 常駐のプロセスを持たない。止まっても次の見回りで続きから再開する | 状態はすべて DynamoDB の行。`tick` は根の状態だけで次の 1 手が決まり、何度呼んでも同じ結果になる(I56・I58・I67) | +| 性能・拡張性 | 依頼から配布の実行の起動まで見回り 1 回分(5 分)以内。承認資料を作る時間は含めない(2026-11-03 の承認ゲート 1 で利用者が決めた読み方) | `finalize` の直後に `tick` を呼ぶ。落ちても見回りが 5 分以内に呼ぶ。見回りが引く索引は既存の GSI `gsi1` で、インフラを足さずに働く。承認の依頼は配布の実行が承認資料を作り終えた時点で作られ、その所要は今の本番のプランの承認資料の生成と同じ | +| 運用・保守性 | 範囲・出し直し・待ちの理由・配布の実行の結果が後から辿れる | まとめた配布の行が提示を 2 つまで上書きせずに持ち、記録の行(`ORGLOG`)が待ち・組む・出し直し・確定・結果を積む。まとめた配布の履歴の経路で読める | +| セキュリティ | 承認の確定は人だけ。配布の実行は範囲の実行と同じリポジトリの権限だけを持つ | I66。配布の実行のジョブの token と導入の token は、今の実行と同じく実行のリポジトリ 1 つに絞る | + +## 決定の記録 + +[issue-2439-design-decisions.md](issue-2439-design-decisions.md) に分けた。 + +## テスト設計 + +| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | +| --- | --- | --- | +| 1・I61 | 同じリポジトリの 2 本の実行が依頼を出すと、まとめた配布 1 つ・配布の実行 1 本になり、`settle` の後に 2 本とも同じ版と本番の Pull Request の `release.answered` を持つ | 依頼ごとに組む・範囲に入らない依頼へ答えると落ちる | +| 2 | 依頼 1 件で、他を待たずに `tick` が組む。1 本だけの実行でも承認の依頼 1 件・配布の実行 1 本 | 依頼の数か待ち時間を条件にすると落ちる | +| 3 | まとめた配布の中身の経路が、依頼のあるレーンと無いレーンを印つきで返す。`release.presented` のコミットが提示の行に残る | 依頼の集合で範囲を決めると落ちる | +| 4・I60 | 提示 1 への承認で `develop` が進んでいれば `represent`、同じなら `confirm:1`。提示 2 への答えでは GitHub を読まない | 提示 2 でも比べる・3 つ目の提示を組むと落ちる | +| 5・I56 | 流れている間に届いた依頼は組まずに残り、`settle` の直後の `tick` で次が組まれる。同時の 2 つの `tick` で組まれるのは 1 つ | 根の条件を外すと 2 つ組まれて落ちる | +| 6・I57 | 配布の実行は範囲のどの実行とも別の実行で、まとめた配布と同じトランザクションで作られる。片方の書き込みを失敗させると両方が無い | 2 回の書き込みに分けると落ちる | +| 7・I65 | ループの実行は依頼の後に `done` で終わり、ループは次の周回へ進む。手動の実行は `awaiting_approval`(`release_requested`)で枠を持つ | ループの実行を承認待ちにすると落ちる | +| 8・I62 | 外の `release/v*` か `develop` の head の `main` 宛の Pull Request が開いている間、または依頼を出していない古い版の実行がある間は組まず、待ちの理由を書く。閉じた後の最初の `tick` で組む。下限の設定が無く、依頼を出して `release_requested` で待つ手動の実行 1 本だけが `legacy` 真のときは組む。まとめた配布が流れている間(枠の `count` が 0 でも)は古い版の実行の開始が 409 `parallel_unsupported_version` になる。照らした後に、依頼を待つ古い版の実行の取り消しと別の古い版の実行の開始で `legacyCount` が 1→0→1 と戻ると、組む書き込みが `legacySeq` の条件で落ちる | 照らしを外す・依頼を出した実行を数える・開始の条件から根を外すと落ちる | +| 9 | 一覧が、同じリポジトリの承認待ちの実行の承認の依頼(ゲート 1・判断・配布の実行のゲート 2)と、ループの人の判断待ちを 1 つの並びで返す。別のリポジトリのものを含まない | 実行やループごとの一覧に戻すと落ちる | +| 10・I68 | ランナーの環境に 3 つの環境変数があり、依頼のファイルが `release.requested` へ写る。契約の例の組をランナーのテストと NDF の構造チェックが読む | 環境変数を落とす・形を変えると落ちる | +| 11・I66 | 一覧とまとめた配布の履歴の経路は GET だけで、答えを書く経路が増えない。止まった配布を解く操作は Web のセッションでない呼び出しを 403 で断る | 書く経路を足すと落ちる | +| 12・I58・I59・I67 | `tick`・`settle` を途中で止めて呼び直しても、答えは 1 回、依頼の行は 1 行、本番の Pull Request を作る配布の実行は 1 本。答えを書いた後に止めた依頼と、`requestResume` が `sprint_run_busy` だった依頼は、見回りが `gsi1` の `ORGANIZER_ACTIVE` から引いて届け直し、`release.answered` は 1 件、届け終えた後に索引から外れる。`settle` の `requestResume` が通った後・`deliveredAt` を書く前に止め、再開した手動の実行を別の承認ゲートで `awaiting_approval`(理由が空)に戻してから見回りを流すと、再開のジョブは作られず、その承認の依頼は人の答えを待ったまま残り、依頼の `deliveredAt` が書かれる | 冪等の条件を外す・答えを書いた時点で索引から外す・`addResumeJob` の条件から理由と依頼の id の照らしを外すと落ちる | +| 13・I63 | `main_merged` を記録した止まった配布の選択肢は `continue` だけで、差し戻しは 409 `release_after_main`。`confirmed` から止まった配布は、`release.progress` が無くても、`mainMerged` が真か、GitHub に `release/v<提示の版>` の枝か本番の Pull Request があれば `continue` で再開し、どれも無いときだけ `confirm:` で再開する | 選択肢を固定にすると落ちる | +| 14 | 用語集に「手元のマシン」があり、廃止した語`手元のランナー`を `glossary.py check` が拾う。Factory の確定仕様・`factory/` の説明文・`ja.json` に残らない(検索 0 件) | 廃止した語の登録を外すと落ちる | +| 15 | 用語集の `ndf-factory` に「料金プラン」があり、`ndf-workflow` の「プラン」と並ぶ。Factory の確定仕様・`factory/` の説明文・`ja.json` の「プラン」の検索の当たりが、NDF のプランを指す行だけになる | 契約の種類を指す行を 1 つ残すと、検索の当たりを NDF のプランの行と照らす確認で落ちる | +| 16 | 改名の差分にコードの識別子・API の項目名・データの項目名が無く、`npm run check` が変更前と同じく通る | 識別子を変えると型検査かテストが落ちる | +| I64 | 配布の実行の作成は枠の行を増やさず、`judgeLimits` を呼ばない。`usage.recorded` はアカウントの利用量に数えられる | 枠を書くと、枠が埋まったリポジトリで配布が組めず落ちる | + +## 設計の結果 + +| 課題 | 扱い | 取り込み先 | 触るファイル | +| --- | --- | --- | --- | +| #2439 | 実装する | — | `factory/api/src/organizer/`、`factory/api/src/runs/`、`factory/api/src/github/`、`factory/api/src/progress/authorize.ts`、`factory/api/src/routes/factory.ts`、`factory/runner/src/`、`factory/contracts/`、`factory/web/src/i18n/ja.json`、`factory/README.md`、`plugins/ndf/scripts/lib/parallel_lane.py`、`plugins/ndf/scripts/lib/parallel_external.py`、`plugins/ndf/scripts/parallel.py`、`plugins/ndf/contracts/forms/parallel-external.json`、`plugins/ndf/contracts/env/`、`plugins/ndf/skills/development-workflow/references/parallel.md`、`plugins/ndf/scripts/tests/`、`docs/specifications/`、`docs/glossary/glossary.json`、`docs/glossary.md` | + +## 未確認のまま残ること + +| 項目 | 内容 | +| --- | --- | +| #2173 の承認の依頼の読み方 | 一覧は「実行の識別子から開いている承認の依頼を読む」関数を #2173 に求める。#2173 の保存はまだ無く、関数の形は #2173 の設計で決まる | +| `gate.reached` の `choices` | 承認の依頼が選択肢を持ち、選択肢の外の答えを断るのは #2173 の実装に依る(I63 の二重の守り)。配布のまとめの `decide` と `resolve` は単独でも断る | +| #2436 の側の待ち | 1 台の中のオーガナイザーが、Factory の本番の Pull Request が開いている間に待つかは #2436 の設計に無い。S6 の両側の排他は片側だけになる。範囲外の課題 https://github.com/devbasex/ai-plugins/issues/2441 で扱う | +| レーンの名前の長さ | `NDF_ORGANIZER_LANE` に `loop-` / `run-` を渡す。識別子の長さが #2436 の名前の形に収まるかは、実装で識別子の生成を読んで確かめる | +| 配布の実行の NDF の版 | 範囲の依頼の NDF の版のうち最も新しいものを入れ、下限(`NDF_PARALLEL_MIN_VERSION`)とは比べない。依頼を出せるのは切り替え口の入った版だけで、下限が無い間に `legacy` 真と数えられた実行の依頼でも、版は切り替え口を持つ。実装で依頼の `ndf_version` を読んで確かめる | diff --git a/issues/issue-2439-requirements.md b/issues/issue-2439-requirements.md new file mode 100644 index 000000000..4df83681b --- /dev/null +++ b/issues/issue-2439-requirements.md @@ -0,0 +1,193 @@ +# #2439: NDF Factory: オーガナイザーを Factory の側で実装し、同じリポジトリで並行する実行の本番の配布を 1 本にまとめ、承認と問いを 1 か所に集める + +正は課題の本文(#2439)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 + +## 具体例 + +2026-11-02、3 本の開発ループ(lroot2・lflow・lfactory2)が 1 台のコンテナの tmux で動き、本番の配布がぶつかった。lflow が ndf v10.27.14 に他の 2 本の変更もまとめて出し、lroot2 の本番の Pull Request https://github.com/devbasex/ai-plugins/pull/2420 を閉じた。#2436(tmux の並列開発を計画から始める)は、これを 1 台の中のオーガナイザーで防ぐ。 + +Neural Development Factory で同じ 3 本を Web から始めると、周回ごとに別の実行ジョブで動く。ジョブはそれぞれ別のコンテナと別の clone を持つ(ECS の `managed`・`byok`、または利用者の手元のマシン #2189)。#2347(開発ループと並行する実行の調整)は、実行を起こす時点で課題・マイルストーン・NDF の版・同時の数の取り合いを守る。一方、承認ゲート 2 と本番の配布は実行ごとに出る。そのため 11-02 と同じ衝突が、**同じリポジトリの実行ジョブの間で**起きる。#2436 のオーガナイザーは 1 台の SQLite(DBOS)に閉じるので、この衝突を防げない。 + +Factory では、実行ジョブ 1 本が #2436 のレーン 1 本に当たる(ジョブの中で動くのはスプリント 1 本の conductor で、tmux のレーンは並ばない)。したがって Factory に要るのは #2436 の上位のまとめ役ではない。**#2436 と同じオーガナイザーの役割を、レーン = 実行ジョブとして Factory の側で担う実装**である(2026-11-02 の利用者の指摘で定義を改めた)。 + +この課題が終わると、同じリポジトリで並行する Factory の実行の本番の配布は、Factory のオーガナイザーが 1 本にまとめる。承認ゲート 2 は 1 回で済む。承認ゲートの外の問いも、リポジトリごとに 1 か所に並ぶ。 + +## 依頼(原文) + +> 今回設計したオーガナイザーの挙動は同一コンテナ内の並列についての挙動ですが、Neural Development Factoryではさらにその上位にランナー間単位のオーガナイザーが必要になりそうです。検討して今の設計に組み込むか、別の設計PRを起こしてください + +(書き手: 利用者、日付: 2026-11-02。#2436 の設計 PR https://github.com/devbasex/ai-plugins/pull/2437 の承認ゲート 1 の前) + +> 用語が混乱してきた。1コンテナ=1ランナーっていう考え方でいいんだっけ? +> それとも複数コンテナ起動できるマシン単位で1ランナー? + +> 多分、今回のオーガナイザーの方が範囲が広くなっちゃってるね。「上位のオーガナイザー」の定義を変えないとダメかも + +> というより、全体のヒエラルキーを定義しないとダメかも。 + +> Factory のオーガナイザーと 1 台の中のオーガナイザー は、対等という以上に排他ですね。 +> Factoryで動くときには1台の中のオーガナイザーは動かない。 + +> Factoryのオーガナイザーは「1台の中のオーガナイザーの仕事全て+ランナー制御等固有の仕事」がその役割になるはず + +(書き手: 利用者、日付: 2026-11-02。続けて受けた 2 つの指摘) + +## 起票者の推定 + +利用者の原文に無く、起票者(conductor)と要求の工程が推して足した条件。S1〜S5 は起票者の判断の記録 09a63a07a4a5(`ask.py decide`)による。S6〜S11 は要求の工程が、人へ問わずに最も筋の通る案として選んだ。承認ゲート 1 で利用者が確かめるまで、利用者の決定として扱わない。 + +| # | 条件 | 推した出所 | この要求での扱い | +| --- | --- | --- | --- | +| S1 | #2436 へ組み込まず、別の課題と別の設計 PR にする | `plugins/` から `factory/` を参照できない(`scripts/check-service-boundary.py`)。実行の基盤(DynamoDB・見回り)が違う。#2436 の設計は 6 本で 1,445 行ある | 含む(この課題の範囲そのもの) | +| S2 | Factory のオーガナイザーは常駐のプロセスにせず、Factory の API と 5 分ごとの見回りの中に置く | [docs/specifications/ndf-factory-dev-loops.md](https://github.com/devbasex/ai-plugins/blob/develop/docs/specifications/ndf-factory-dev-loops.md) の概要「常駐のプロセスを持たない」 | 非機能の条件(可用性・運用) | +| S3 | レーンからオーガナイザーへの依頼と答えの形(配布の依頼・承認ゲート 1/2 の依頼・問いの依頼)は #2436 が NDF の契約として公開し、この課題はランナーの翻訳で同じ形を受ける | #2436 の決定 21。下流の腐敗防止層の形(`ndf-factory-dev-loops.md` のコンテキスト) | 前提 1・受け入れ条件 10 | +| S4 | 本番の配布の中身は #2436 と同じく、承認資料を作る時点の `develop` のコミットの範囲で決める。本番の Pull Request の規則(head が `release/v<版>`、`develop` に含まれるコミットだけ)を共有する | #2436 の決定 13〜15 | 受け入れ条件 1・3・4 | +| S5 | 本番の配布は、#2436 の一時レーンに当たる専用の実行ジョブ(配布の実行ジョブ)で流す | #2436 の決定 19。Factory では実行ジョブがレーンの単位になる | 受け入れ条件 6 | +| S6 | Factory の実行と Factory の外の 1 台の並列(#2436)が同じリポジトリを同時に回すときは、禁止せず、GitHub の上の本番の Pull Request で排他にする。どちらかの本番の Pull Request(head が `release/v` で始まり、宛先が `main`)が開いている間は、もう一方は配布を組まずに待つ | 2 つのオーガナイザーは互いの記録を読めない(DBOS と DynamoDB)。両方が読めるのは GitHub の状態だけである。禁止すると、利用者の手元の作業を Factory が止めることになる | 受け入れ条件 8 | +| S7 | Factory の実行の中では、NDF は本番の配布を自分で組まず、配布の依頼を出して周回を締める。切り替えはランナーが NDF へ渡す設定で行う | #2436 の決定 11 は「ラッパーの外では今の振る舞いのまま」とする。このままでは Factory の実行の中の NDF が自分で配布する | 前提 2・受け入れ条件 10 | +| S8 | 同じリポジトリの実行が 1 本だけでも、配布は Factory のオーガナイザーを通す(1 本のときだけ今の経路に残さない) | 経路を 2 つ持つと、1 本から 2 本へ増えた瞬間の扱いが要る。1 本でも結果は今と同じく 1 回の承認と 1 本の本番の Pull Request になる | 受け入れ条件 2 | +| S9 | 承認ゲート 1 の依頼は設計 PR ごとに別の承認の依頼のままにし、まとめるのは一覧への並べ方だけにする | #2436 の決定 16。設計 PR は互いに独立で、コミットの範囲との結び付きが無い | 受け入れ条件 9 | +| S10 | `手元のランナー` の改名とアカウントの「プラン」の改名は、説明文・用語集・画面の文言を対象にし、コードの識別子・API の項目名・データの項目名は変えない | 識別子を変えると、API の互換と保存済みのデータの移行が要る。衝突は読み手の語の問題で、識別子は衝突していない(`runnerId`・`plan`) | 前提 4・受け入れ条件 14〜16 | +| S11 | 配布の実行ジョブの費用は、リポジトリを登録したアカウントの利用量に数える | 同じリポジトリの実行は同じアカウントに属する。範囲に入った実行の数で割ると、計算と表示の規則が増える | 前提 5・未決 1 | + +## 目的 + +同じリポジトリで並行する Factory の実行どうしで、本番の配布をぶつけずに 1 本へまとめる。承認と問いを、リポジトリごとに 1 か所で答えられるようにする。あわせて、#2436 の階層の定義(`issues/issue-2436-design-hierarchy.md`)が Factory の側へ割り当てた語の衝突 2 つを解く。 + +## 前提 + +- 前提 1: #2436 が、レーンとオーガナイザーの間の依頼と答えの形を NDF の契約(`plugins/ndf/contracts/forms/parallel-request.json`)として公開し、`develop` へマージしている。この課題の実装は、その契約がマージされるまで始めない +- 前提 2: NDF の側に、本番の配布を自分で組まずに配布の依頼を出す切り替え口がある。切り替え口をどちらの課題が作るかは未決 2 で決める +- 前提 3: 承認の依頼の保存・通知・答えの経路は #2173 のものを使う。この課題は、まとめた配布を 1 件の承認の依頼(種類 ゲート 2)として載せる +- 前提 4: 衝突を解く改名は、`ndf-factory` の文脈の語だけを対象にする。`ndf-workflow` の「プラン」(`plan.json`)は変えない +- 前提 5: 配布の実行ジョブは、リポジトリを登録したアカウントの実行として数える(S11)。利用量の表示と料金の上限での扱いは未決 1 で決める +- 前提 6: 同じリポジトリで同時に動く実行の数は、今の上限(`concurrent_repo_runs`)のまま変えない + +## 対象範囲 + +含む: +- Factory のオーガナイザー: 同じリポジトリの実行が出す配布の依頼・承認ゲート 1/2 の依頼・問いの依頼を集め、答えを持ち主の実行へ返す +- ランナーの翻訳: #2436 の契約の依頼を、Factory の進捗の事象へ写す +- 配布の実行ジョブ: まとめた本番の配布を流し、結果を返して終わる専用の実行ジョブ +- 承認の依頼の一覧への載せ方(データと API。リポジトリごとに 1 つ) +- 語の衝突のうち Factory の側の 2 つの改名: マシン単位の `手元のランナー` と、アカウントの契約の種類の「プラン」 + +含まない: +- 1 台の中の並列のオーガナイザー(#2436) +- 実行を起こす時点の課題・マイルストーンの取り合い(#2347) +- 承認の依頼の保存と通知の経路(#2173) +- 複数のループの承認の依頼の一覧の画面(#2430) +- 手元のマシンの登録(#2189) +- 発表資料(`docs/presentations/`)と記事(`docs/articles/`)の語。どちらも書いた日時点の記録である +- ホットフィックス(並列の外の配布の経路) + +## ドメインイベント + +| # | イベント | 引き金 | 失敗したとき | 順序の前提 | +| --- | --- | --- | --- | --- | +| E1 | 実行が配布の依頼を出した | 実行の中の conductor が承認ゲート 2 の手前に達した | ランナーが依頼を送れずにジョブが終わった → 実行は終わりの理由つきで終わり、見回りが依頼の欠けを検出する(受け入れ条件 12) | 実行の変更が `develop` へマージされている | +| E2 | オーガナイザーが配布の範囲を決めた | E1 が届き、流れている配布が無い | `develop` の先頭を読めない → 次の見回りで読み直す | E1 | +| E3 | 承認資料をそろえた承認の依頼(ゲート 2)を作った | E2 | 資料の生成が失敗した → 問いの依頼にして人へ戻す | E2 | +| E4 | 人(または `pace: auto` の MVV 判定)が承認の依頼に答えた | E3 | 答えが届かない → 承認待ちのまま、実行は次の周回へ進む | E3 | +| E5 | 答えの時点の `develop` の先頭を比べ、進んでいたので承認資料を作り直して出し直した | E4 で、提示したコミットから `develop` が進んでいた | 出し直しへの答えでは比べない(1 回だけ) | E4 | +| E6 | 配布の実行ジョブを起こした | E4 または E5 の答えが「出す」 | ジョブが起動できない → 見回りが起こし直す | E4・E5 | +| E7 | 本番の Pull Request を 1 本作り、`main` へマージし、版を出した | E6 | 途中で止まった → `main` へのマージの前なら差し戻せる。後なら「続きから流す」だけを出す | E6 | +| E8 | 範囲に入ったすべての実行へ配布の結果を返した | E7 | 返し損ねた → 見回りが記録から返し直す。1 件の依頼に答えは 1 回 | E7 | +| E9 | 実行が問いの依頼を出した(承認ゲートの外) | conductor が人の判断を求めた | E1 と同じ | — | +| E10 | 問いの答えを持ち主の実行へ返した | 人が答えた | 持ち主の実行が終わっていた → 持ち主のループの次の周回へ渡す | E9 | +| E11 | Factory の外の本番の Pull Request が開いていたので、配布を組まずに待った | E2 の時点で、他のオーガナイザーの本番の Pull Request が開いている | 開いたまま動かない → 待ちの理由を一覧へ出す | E1 | + +## 用語 + +| 用語 | 意味 | +| --- | --- | +| 実行ジョブ | 実行基盤が 1 回起動するコンテナ 1 つ分の実行(用語集の `ndf-factory`)。Factory では実行ジョブ 1 本がレーン 1 本に当たる | +| ランナー | 1 つの実行ジョブの中で動くプログラム。1 コンテナ = 1 実行ジョブ = 1 ランナー(用語集の `ndf-factory`) | +| 承認の依頼 | 実行ジョブが人の答えを待って終わったときに作る依頼。種類はゲート 1・ゲート 2・判断の 3 つ(用語集の `ndf-factory`) | +| 見回り | 実行の制御が 5 分ごとに動かす処理(用語集の `ndf-factory`) | + +**オーガナイザー**は役割の名前で、並行するレーンの本番の配布を 1 本にまとめ、問いを集め、答えの後の作業を持ち主のレーンへ返す。実装は 1 台の中(#2436。レーン = tmux のセッション)と Factory(この課題。レーン = 同じリポジトリの実行ジョブ)の 2 つで、上下ではなく**排他**である。Factory の実行ジョブの中では 1 台の中のオーガナイザーは動かない(2026-11-02 の利用者の指摘)。Factory のオーガナイザーの仕事は、**1 台の中のオーガナイザーの仕事すべて**(配布を 1 本にまとめる・問いを集める・答えの後の作業を持ち主へ返す・共有の作業を一時レーンへ渡す)に、**Factory 固有の仕事**(ランナーの制御=実行ジョブの起動・停止・答えでの再開、実行の枠と手元のマシンの枠、ジョブをまたぐ状態の置き場)を足したものである(同日の利用者の指摘)。語の定義は #2436 の変更が用語集へ足す。「上位のオーガナイザー」という呼び方は使わず、「Factory のオーガナイザー」と呼ぶ。 + +この課題で新しく決める語(Factory のオーガナイザーの部品の名前、マシン単位の手元の受け手の新しい名前、アカウントの契約の種類の新しい名前)は設計で決め、同じ変更で用語集へ足す。 + +## 受け入れ条件 + +配布のまとめ: + +- [ ] 1. 同じリポジトリで並行する 2 本以上の実行が承認ゲート 2 に達したとき、本番の Pull Request は 1 本だけ作られる。範囲に入った実行は、どれも同じ本番の Pull Request と版を配布の結果として記録に持つ +- [ ] 2. 配布の依頼が届いた時点で流れている配布が無ければ、他の実行の依頼を待たずに配布の範囲を決め、承認の依頼を作る。同じリポジトリの実行が 1 本だけのときも同じ経路を通り、承認の依頼 1 件と本番の Pull Request 1 本になる +- [ ] 3. 配布の範囲は、承認資料を作る時点の `develop` の先頭のコミット(`main..`)で決まる。範囲に入った実行は、依頼を出したかどうかの印つきで承認資料に並ぶ +- [ ] 4. 承認の答えの時点で `develop` が提示したコミットから進んでいれば、承認資料を作り直し、加わった範囲を添えて承認の依頼を 1 回だけ出し直す。出し直しへの答えでは比べず、出し直しで承認したコミットを出す +- [ ] 5. 配布が流れている間に届いた依頼と `develop` へ入った変更は、今の配布の後に組む次の配布になる。同じリポジトリで同時に流れる配布は 1 本までである +- [ ] 6. 本番の配布は、範囲に入った実行のどれでもない、配布専用の実行ジョブで流れる。配布専用の実行ジョブは結果を返して終わり、同じリポジトリで同時に 1 本までである +- [ ] 7. 承認待ちの間も、範囲に入った実行は答えを待たずに次の周回へ進める(ジョブを終えて費用を止める今の形のまま) +- [ ] 8. 同じリポジトリに、Factory の外で作られた本番の Pull Request(head が `release/v` で始まり、宛先が `main`)が開いている間は、Factory のオーガナイザーは配布を組まずに待ち、待ちの理由を承認の依頼の一覧の記録に残す。閉じた後の最初の見回りで配布を組む + +承認と問いの集約: + +- [ ] 9. 承認ゲート 1 の依頼と承認ゲートの外の問い(種類 判断)は、リポジトリごとに 1 つの一覧として API から取れる。答えは持ち主の実行(持ち主の実行が終わっていれば、そのループの次の周回)へ届く +- [ ] 10. Factory の実行の中では #2436 のオーガナイザーを起動しない。ランナーは実行ジョブの環境に `NDF_FACTORY_RUN=1` を立て、`parallel.py init`・`run` はそれを見て断る(#2436 の設計の階層の文書)。実行の中の NDF は本番の配布を自分で組まず、配布の依頼を #2436 の契約の形で出し、ランナーがその依頼を進捗の事象へ写す +- [ ] 11. 承認ゲートの承認は、今と同じく人だけが確定できる。API と MCP からは、まとめた配布の承認の依頼と材料を読めるが、承認を確定できない(退行しない) + +障害と再開: + +- [ ] 12. Factory のオーガナイザーの処理が途中で止まっても、次の見回りが記録から続きを再開する。1 件の依頼に答えは 1 回だけ返り、同じ範囲で本番の Pull Request が 2 本作られることは無い +- [ ] 13. `main` へのマージの後に止まった配布は、差し戻しを選べず「続きから流す」だけを出す + +語の衝突: + +- [ ] 14. 用語集の `ndf-factory` の文脈で、マシン単位で複数の実行ジョブを受け取る手元の受け手に、「ランナー」と別の語が付く。もとの `手元のランナー` は廃止した語として用語集に残り、`glossary.py check` が拾う。Factory の確定仕様・`factory/` の説明文・画面の文言(`factory/web/src/i18n/`)に、廃止した語が残らない +- [ ] 15. 用語集の `ndf-factory` の文脈で、アカウントの契約の種類が「料金プラン」になる。`ndf-workflow` の「プラン」(`plan.json`)とは別の語として並ぶ。Factory の確定仕様・`factory/` の説明文・画面の文言に、契約の種類を指す裸の「プラン」が残らない +- [ ] 16. 改名で、コードの識別子・API の項目名・保存済みのデータの項目名は変わらない(`factory/` の型検査とテストが変更前と同じく通る) + +## 非機能の条件 + +| 大項目 | 条件 | +| --- | --- | +| 可用性 | 常駐のプロセスを持たない。オーガナイザーの処理は API の handler と 5 分ごとの見回りの中で動き、止まっても次の見回りで続きから再開する(受け入れ条件 12) | +| 性能・拡張性 | 配布の依頼が届いてから配布の実行が起動されるまで、見回り 1 回分(5 分)以内。承認資料を作る時間は含めない(2026-11-03 の承認ゲート 1 で利用者が決めた読み方) | +| 運用・保守性 | 配布の範囲・出し直し・待ちの理由・配布専用の実行ジョブの結果が、リポジトリごとの記録に残り、後から辿れる | +| セキュリティ | 承認の確定は人だけ(受け入れ条件 11)。配布専用の実行ジョブは、範囲に入った実行と同じリポジトリの権限だけを持つ | + +## 影響 + +| 対象 | 影響 | +| --- | --- | +| 公開インタフェース | 変わる。承認の依頼の一覧の API が、リポジトリごとの一覧を返す。進捗の事象に、配布の依頼と結果の種類が加わる(加えるだけで既存の項目は変えない)。画面の文言の 2 語が変わる | +| データ | 加わる。リポジトリごとのオーガナイザーの進み具合(受けた依頼・配布の範囲・答え)を持つ行。既存のデータの移行は無い | +| 既存の振る舞い | 変わる。Factory の実行は本番の配布を自分で流さず、配布専用の実行ジョブが流す。実行が 1 本だけのときも、承認の依頼 1 件と本番の Pull Request 1 本の結果は今と同じ | + +## 検証手段 + +| 項目 | 手段 | +| --- | --- | +| テストと型検査 | `cd factory && npm ci && npm run check`(契約の lint と、`contracts`・`web`・`api`・`runner` の型検査と vitest。オーガナイザーの集約・出し直し・待ち・再開は `api` の単体テストでメモリの表を使って確かめる) | +| 契約 | 構造チェック(リポジトリの根の `scripts/contracts_lib/model.py`)が、ランナーの翻訳と #2436 の契約の例の組を照らす | +| 境界 | `python3 scripts/check-service-boundary.py` が通る | +| 語 | `python3 plugins/ndf/scripts/glossary.py check` が当たり 0 件 | +| 手動確認 | 同じリポジトリで 2 本の開発ループを Factory で回し、承認ゲート 2 が 1 件だけ一覧に出て、本番の Pull Request が 1 本だけ作られることを、利用者がリリース後テストで見る | + +## 前提とする取り決め + +| 項目 | 参照先 / 決めたこと | +| --- | --- | +| プロジェクト構造 | Factory のオーガナイザーとランナーの翻訳は `factory/` に置く。`plugins/` と `.claude-plugin/marketplace.json` は `factory/` を参照しない(`AGENTS.md` の「マーケットプレイスの構造」)。NDF の側の切り替え口は `plugins/ndf/` に置き、Factory を名指ししない | +| コーディング規約 | `factory/` の TypeScript の既存の書き方と、確定仕様 [ndf-factory-dev-loops.md](https://github.com/devbasex/ai-plugins/blob/develop/docs/specifications/ndf-factory-dev-loops.md) の集約・トランザクションの規則(1 つの処理が書き換える集約は 1 つ) | +| テスト戦略 | オーガナイザーの規則は API の単体テスト(メモリの表)で、ランナーの翻訳は契約の例の組で、2 本の実行の衝突はリリース後テストで確かめる | + +## 境界 + +| 区分 | 内容 | +| --- | --- | +| 常に行う | `factory/` のテストと型検査の実行、境界のチェック、用語集のチェック | +| 確認してから行う | 公開インタフェース(API・進捗の事象・画面の文言)の変更。承認ゲート 1・2 で人が見る | +| 行わない | #2436 の 1 台の中のオーガナイザーの変更、`main` への出し方の規則(#2436 の決定 15)の変更、コードの識別子の改名、発表資料と記事の書き換え | + +## 未決 + +| 項目 | 誰が決めるか | 期限 | +| --- | --- | --- | +| 1. 配布専用の実行ジョブの費用を、利用量の表示と料金の上限(同時の数を含む)でどう数えるか。前提 5 はアカウントの利用量に数えるとだけ置いた | 利用者(承認ゲート 1) | 設計の承認まで | +| 2. NDF の側の切り替え口(配布を自分で組まずに依頼を出す)を、#2436 の契約と同じ変更で作るか、この課題で作るか | #2436 と #2439 の設計 | 設計の承認まで | +| 3. オーガナイザーの進行の状態を DynamoDB の行で持つか、AWS の耐久ワークフローの部品(Step Functions など)を使うか。#2436 と同じく既存の部品を先に比べ、費用が増えるなら理由と代わりの案を示す | 設計 | 設計の承認まで | +| 4. マシン単位の手元の受け手の新しい名前 | 設計(用語集の語の選び方に従う) | 設計の承認まで |