Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
651b9cd
契約を機能から起こした配列の形へ書き換え、操作の表を生成する(#2384)
takemi-ohama Nov 4, 2026
8890d54
API と画面の API クライアントを操作の表から組み、配列で受け渡す(#2384)
takemi-ohama Nov 4, 2026
5fce25d
委任の資格情報を発行・照合・取り消し、人だけが確定できる操作を委任から断る(#2384)
takemi-ohama Nov 4, 2026
088db45
委任の検査のテストの型の誤りを直す(#2384)
takemi-ohama Nov 4, 2026
c3dd76d
利用者向けの操作を 1 回ごとに操作の記録へ追記する(#2384)
takemi-ohama Nov 4, 2026
b965108
MCP の受け口を足し、ツールの呼び出しを同じアプリの経路へ送る(#2384)
takemi-ohama Nov 4, 2026
5bbce77
確定仕様の画面向けの経路を、要素の配列で受け渡す操作の形へ合わせる(#2384)
takemi-ohama Nov 4, 2026
0091b3a
利用者向けの操作の形を契約の定義の確定仕様へ移し、全体の契約を行数の上限に収める(#2384)
takemi-ohama Nov 4, 2026
ff65626
確定仕様のループの認可を、要素ごとに照らす結果の形へ合わせる(#2384)
takemi-ohama Nov 4, 2026
1dfad18
docs: 関連リンクの行から課題番号の由来を外す
takemi-ohama Nov 4, 2026
3a1061d
Merge pull request #2512 from devbasex/feat/issue-2384-lfactory3-4
takemi-ohama Nov 5, 2026
d5dfbfe
画面の地図の予定の画面に、その画面を作る課題を書く(#2431)
takemi-ohama Nov 5, 2026
27b2dff
Merge pull request #2517 from devbasex/feat/issue-2431-lfactory3-4
takemi-ohama Nov 5, 2026
8203279
Refactor: centralize_configuration — factory/api/src/audit/middleware…
takemi-ohama Nov 5, 2026
510ee22
Refactor: fix_dependency_direction — factory/api/src/routes/factory.t…
takemi-ohama Nov 5, 2026
5052408
Refactor: extract_method — factory/api/src/runs/memory-table.ts#Memor…
takemi-ohama Nov 5, 2026
b31183d
fix(factory): 冪等の再送のアカウント照合・競合時の再確認・要素ごとの 503・画面の鍵の持ち越し
takemi-ohama Nov 5, 2026
644ce77
fix(factory): 委任の送り直しで値が返らないときに知らせる(PR #2518 round 2)
takemi-ohama Nov 5, 2026
52effd5
Merge remote-tracking branch 'origin/develop' into sprint/lfactory3-4
takemi-ohama Nov 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions docs/specifications/ndf-factory-accounts-and-plans.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ graph TD

あわせて `LIMITS_TABLE`・`AccountService`・エラーの型(`ApiError`・`AuthError`・`ForbiddenError`・`PlanError`)と、`AccountContext`・`Caller`・`ExecutionMode`・`Plan`・`SeatType`・`Limits`・`Judgment`・`CurrentUsage`・`LimitReason`・`AuthEnv`・`ApiApp` の型を export する。

**使える量は HTTP の経路にしない。** #2186・#2174 の処理は同じ `factory/api/` の中で上の関数を呼ぶ。画面は `GET /accounts/{account_id}` の `limits` で上限を出す。
**使える量は HTTP の経路にしない。** #2186・#2174 の処理は同じ `factory/api/` の中で上の関数を呼ぶ。画面は `GET /accounts?account_id=` の `limits` で上限を出す。

### token の検証とアカウントの解決

Expand Down Expand Up @@ -214,22 +214,22 @@ sequenceDiagram

### API の経路

経路はすべて `Authorization: Bearer <access token>` を要る。エラーの本文は `{"error": {"code": "<機械の値>", "message": "<利用者に出す文>"}}` に揃える。機械可読な正は `factory/contracts/openapi/accounts.yaml` である。
経路はすべて `Authorization: Bearer <access token>`(または委任の資格情報)を要る。エラーの本文は `{"error": {"code": "<機械の値>", "message": "<利用者に出す文>"}}` に揃える。機械可読な正は `factory/contracts/openapi/accounts.yaml` である。入力と成功の形は [利用者向けの操作の形](ndf-factory-contracts-and-progress.md#利用者向けの操作の形) に従う。「要素・条件」は要素 1 個の項目、「値」は成功の要素の `value`(一覧は配列の 1 件)、「失敗」は要素の結果の状態である(401 は要求全体)。チームの作成と席の追加・変更・削除は人だけが確定でき、委任の資格情報では要求全体を 403 `human_only` で断る。

| 経路 | 入力 | 成功 | 失敗 |
| 経路 | 要素・条件 | 値 | 失敗 |
| --- | --- | --- | --- |
| `GET /me` | — | 200 `{user_id, idp, personal_account_id, accounts: [{account_id, kind, name, plan, role, seat_type, execution_mode}]}`。先頭は個人のアカウント。初回は個人のアカウントを作る | 401・503 `account_unavailable` |
| `GET /accounts/{account_id}` | — | 200 `{account_id, kind, name, plan, role, seat_type, execution_mode, limits: {concurrent_sprint_runs, monthly_tokens}, seats}`。`monthly_tokens` は `managed` のときだけ数で、ほかは `null`。`seats` はチームだけで `[{user_id, idp, seat_type, added_at}]`、個人では `null` | 401・403 `forbidden` |
| `POST /teams` | `{name: 1〜64 字(空白だけは不可), seat_type}` | 201 アカウントの本文(上と同じ形)。作った利用者が持ち主で、その席が 1 つある | 400 `invalid_request` |
| `PUT /accounts/{account_id}/execution-mode` | `{execution_mode: "local" \| "byok" \| "managed"}` | 200 アカウントの本文。今と同じ値なら履歴の行を足さずに 200 | 400 `invalid_request`・403 `forbidden`(I22・I18)・409 `managed_not_allowed_on_free`(`message` は「Free の料金プランでは managed(サービスの Bedrock)を選べません」)・503 `account_unavailable` |
| `POST /accounts/{account_id}/seats` | `{user_id: 1〜128 字, seat_type}` | 201 席 `{user_id, idp, seat_type, added_at}` | 400・403・409 `not_a_team`・422 `user_not_found`・409 `seat_exists` |
| `PATCH /accounts/{account_id}/seats/{user_id}` | `{seat_type}` | 200 席 | 400・403・409 `not_a_team`・404 `seat_not_found`・503 |
| `DELETE /accounts/{account_id}/seats/{user_id}` | — | 204 | 403・409 `not_a_team`・409 `owner_seat_protected`・404 `seat_not_found`・503 |
| `GET /me` | — | `{user_id, idp, personal_account_id, accounts: [{account_id, kind, name, plan, role, seat_type, execution_mode}]}`。先頭は個人のアカウント。初回は個人のアカウントを作る | 401・503 `account_unavailable` |
| `GET /accounts?account_id=` | 鍵 `account_id`(要素ごとにアカウントを解決する) | `{account_id, kind, name, plan, role, seat_type, execution_mode, limits: {concurrent_sprint_runs, monthly_tokens}, seats}`。`monthly_tokens` は `managed` のときだけ数で、ほかは `null`。`seats` はチームだけで `[{user_id, idp, seat_type, added_at}]`、個人では `null` | 401・403 `forbidden` |
| `POST /teams` | `{name: 1〜64 字(空白だけは不可), seat_type, idempotency_key}` | アカウントの本文(上と同じ形)。作った利用者が持ち主で、その席が 1 つある | 400 `invalid_request` |
| `PATCH /accounts` | `{account_id, execution_mode: "local" \| "byok" \| "managed"}`(要素ごとにアカウントを解決する) | アカウントの本文。今と同じ値なら履歴の行を足さずに成功 | 400 `invalid_request`・403 `forbidden`(I22・I18)・409 `managed_not_allowed_on_free`(`message` は「Free の料金プランでは managed(サービスの Bedrock)を選べません」)・503 `account_unavailable` |
| `POST /accounts/{account_id}/seats` | `{user_id: 1〜128 字, seat_type}` | 席 `{user_id, idp, seat_type, added_at}` | 400・403・409 `not_a_team`・422 `user_not_found`・409 `seat_exists` |
| `PATCH /accounts/{account_id}/seats` | `{user_id, seat_type}` | 席 | 400・403・409 `not_a_team`・404 `seat_not_found`・503 |
| `DELETE /accounts/{account_id}/seats?user_id=` | 鍵 `user_id` | `{removed: true}` | 403・409 `not_a_team`・409 `owner_seat_protected`・404 `seat_not_found`・503 |

- **持ち主の確かめは、席の有無と料金プランの照合より先に行う。** 404 `seat_not_found` と 409 `managed_not_allowed_on_free` は、要求した者が持ち主と確かめた後だけ返す
- **`byok` を選んでもキーの登録はこの経路で求めない。** キーの保存は #2186 が持つ
- **`GET /me` のチームの一覧は GSI を候補として引き、元の席を強い整合で確かめる。** 外した席が GSI に残る間に、そのチームの名前や料金プランを返さないためである
- 本文が JSON のオブジェクトでなければ 400 `invalid_request`。経路の中で起きた想定外の失敗は 503 `account_unavailable` に揃える
- 本文が配列でなければ要求全体を、要素がオブジェクトでなければその要素を 400 `invalid_request` にする。経路の中で起きた想定外の失敗は 503 `account_unavailable` に揃える

### GitHub の仲立ち

Expand Down Expand Up @@ -434,7 +434,7 @@ graph TD
| 他人のアカウント(I18) | 利用者 A の token で、他人の個人のアカウント・入っていないチーム・存在しない ID の 3 つに読む・書くを送ると、すべて 403 で本文が同じであること |
| 料金プランの組(I13) | 個人のアカウントへの席の操作は 409、`setPlan` で個人に `team`・チームに `pro` は失敗で、行が変わらないこと |
| 履歴(I20) | 席・料金プラン・実行の形態の変更のたびに履歴の行が 1 つ増え、変更が失敗したときは増えないこと |
| 実行の形態の読み出し | `GET /me`・`GET /accounts/{account_id}` の応答と `resolveAccount` の文脈に保存した形態が出ること |
| 実行の形態の読み出し | `GET /me`・`GET /accounts?account_id=` の応答と `resolveAccount` の文脈に保存した形態が出ること |
| 実行の形態の変更(I21・I22) | 持ち主の変更は 200 で形態が変わり、持ち主でない者は 403、Free で `managed` は 409 `managed_not_allowed_on_free` で、どちらも形態と履歴が変わらないこと |
| Free へ下げる(I21) | `managed` のプロのアカウントへ `setPlan(…, "free")` を呼ぶと、料金プランが `free`・形態が `local` になり、2 つの履歴の行が同じトランザクションで増えること。形態の書き込みが失敗すれば料金プランも変わらないこと |
| 版の競合(I21) | `setPlan` の書き込みの前に別の要求が `managed` を保存して版を進めても、`setPlan` が読み直して、終わったとき料金プランが `free`・形態が `local` であること |
Expand Down
16 changes: 16 additions & 0 deletions docs/specifications/ndf-factory-contracts-and-progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,22 @@ sequenceDiagram
- **`SavedAnswerReader` は `(approvalRequestId) => Promise<SavedAnswer | null>` である。** `SavedAnswer` は `{ approvalRequestId; sprintRunId; kind; decision?; answeredByAccountId }` で、`decision` は判断(`kind` が `decision`)では持たない。`null` は未保存、読めないときは投げる(受け口が 503 にする)
- **handler は `savedAnswers` に `saved-answers.ts` の `savedAnswersFromEnv()` を渡す。** 今は常に `SavedAnswersUnavailableError` を投げる既定で、`api` の経路は 503 を返す。#2173 が同じファイルの中身を本物の読み出しに替え、受け口のほかのファイルは変えずに済む

### 利用者向けの操作の形

画面・委任を受けたエージェント・MCP のクライアントが呼ぶ利用者向けの操作(契約の操作に `x-ndf-access` を持つもの)は、どれも次の形で受け渡す。形の正は `factory/contracts/openapi/*.yaml` の拡張(`x-ndf-access`・`x-ndf-keys`・`x-ndf-id`)と、そこから生成する操作の表 `factory/contracts/src/generated/operations.ts`(`npm run generate -w contracts -- --check` で食い違いを落とす)である。ランナー向けの経路(`/run-jobs/*`)と進捗の受け口(`/ingest/*`)はこの形に入らない。

| 種類 | メソッド | 入力 | 成功 |
| --- | --- | --- | --- |
| 一覧 | `GET` | 条件の query | 200 と値の配列(要素の結果で包まない) |
| 指定の取得・削除 | `GET`・`DELETE` | 鍵の query を繰り返す(`?sprint_run_id=a&sprint_run_id=b`)。鍵を持たない操作は query 無しで要素 1 個 | 200 と要素の結果の配列 |
| 作成・更新・操作 | `POST`・`PATCH`・`PUT` | 本文の要素の配列(1 件でも要素 1 個の配列) | 200 と要素の結果の配列 |

- **経路の変数はアカウントの ID だけ**である。実行・ループ・リポジトリなどの識別子は鍵の query か要素の項目で渡し、要素ごとにそのアカウントに属するかを照らす
- **要素の結果**は入力と同じ順・同じ数で、成功は `{ok: true, value}`、失敗は `{ok: false, status, error: {code, message, details?}}` である。失敗の `status` と `code` は、各操作の確定仕様の表にある誤りのまま(400・403・404・409・422・503)。要素がすべて失敗しても応答は 200 である
- **要求全体を断るのは次だけ**である。本文が配列でない(400 `invalid_request`)・要素が 0 個(400 `empty_items`)・101 個以上(400 `too_many_items`)・同じ鍵の要素が 2 つ(400 `duplicate_key`)・資格情報が無効(401)・経路のアカウントを解決できない(403 `forbidden`)・委任の資格情報で人だけが確定できる操作を呼んだ(403 `human_only`)・読み取りだけの委任で書き込みの操作を呼んだ(403 `scope_insufficient`)・操作の記録を追記できない(503 `audit_unavailable`)
- **新しい識別子を作る作成**(スプリントの実行・開発ループ・チーム・委任の資格情報)は、要素ごとに冪等の鍵 `idempotency_key`(1〜64 字の英数字とハイフン)を受ける。同じ利用者・同じ操作・同じ鍵の 24 時間以内の 2 回目は、1 回目に作ったものを返す
- 画面の API クライアント(`factory/web/src/api.ts`)は要求を操作の表から組み、要素の結果をほどいて 1 件の値か `ApiFailure` を返す

### 常に成り立つ条件

I10 は全体の契約のもの。#2170 の I24 に続けて番号を振る。I37 は #2186 が #2216 の照合として足し、I44・I45・I50 は #2216 が足した(I32〜I36・I38〜I43 は #2186 のほかの集約の条件、I46〜I49 は [起票の依頼と起票の代行](ndf-factory-issue-requests.md) の台帳の条件)。
Expand Down
22 changes: 11 additions & 11 deletions docs/specifications/ndf-factory-dev-loops-data-and-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,19 +150,19 @@ erDiagram

### 画面向けの経路(`openapi/loops.yaml`・`openapi/runs.yaml`)

どの経路も Cognito の access token を要り、`resolveFromRequest` でアカウントを解決する。ループの経路はループの `accountId` が一致しなければ 403、無ければ 404 `loop_not_found`。エラーの本文は `{"error": {"code", "message", "details"?}}`。
どの経路も Cognito の access token か委任の資格情報を要り、`resolveFromRequest` でアカウントを解決する。ループの要素はループの `accountId` が一致しなければ 403、無ければ 404 `loop_not_found` の要素の結果になる。エラーの本文は `{"error": {"code", "message", "details"?}}`。入力と成功の形は [利用者向けの操作の形](ndf-factory-contracts-and-progress.md#利用者向けの操作の形) に従う。「要素・条件」は要素 1 個の項目か一覧の条件、「値」は成功の要素の `value`(一覧は配列の 1 件)、「失敗」は要素の結果の状態である。

| 経路 | 誰が | 入力 | 成功 | 失敗 |
| 経路 | 誰が | 要素・条件 | 値 | 失敗 |
| --- | --- | --- | --- | --- |
| `POST /accounts/{a}/repositories/{r}/loops` | 席を持つ人 | `{name, milestones, budget?: {laps?, usd?, hours?}}` | 201 `{loop_id, state}`(`running` か `between_laps`) | 400 `invalid_request`(名前・題の形、1〜10 件、予算が正の数でない)・403・404 `repository_not_found`・409 `milestone_in_use`(`details.milestones`・`details.loop_name`)・409 `loop_name_in_use`(同じ登録の終わりの状態でないループ) |
| `GET /accounts/{a}/repositories/{r}/loops` | 席を持つ人 | — | 200 `{loops: [{loop_id, name, milestones, state, wait_reason, laps_closed, cost_usd, budget, budget_remaining, last_run_at, stop_condition, stop_detail}]}`(新しい順 50 件) | 400・403 |
| `GET /accounts/{a}/loops/{id}` | 席を持つ人 | — | 200 `{loop, laps: [{lap, issues, result, version, cost_usd, seconds}], runs: [{sprint_run_id, lap, state, failure_reason, finish_reason, resume_at, cost_usd, sections}], pending: [{number, reason, lap, answer}]}` | 403・404 |
| `PATCH /accounts/{a}/loops/{id}` | 席を持つ人 | `{budget}` | 200 ループの 1 行 | 400・403・404・409 `already_finished` |
| `POST /accounts/{a}/loops/{id}/cancel` | 席を持つ人 | — | 200 `{state}`(`cancelled` か、実行の停止を待つ `running`) | 403・404・409 `already_finished` |
| `POST /accounts/{a}/loops/{id}/next-lap` | 席を持つ人 | — | 202 `{state}` | 403・404・409 `not_awaiting_user_start` |
| `POST /accounts/{a}/loops/{id}/answers` | 席を持つ人 | `{issue, decision: approved \| rejected, pr?}` | 200 `{pending}` | 400・403・404・409 `not_pending`(`pending` に無い番号) |
| `PATCH /accounts/{a}/repositories/{r}`(`runs.yaml`) | 持ち主 | `{max_concurrent_runs: 1〜100 の整数か null}` | 200 登録の 1 行 | 400・403・404 |
| `POST /accounts/{a}/sprint-runs`(`runs.yaml`) | 席を持つ人 | 既存のまま | 既存のまま | 既存の失敗に加えて 409 `issue_in_use`(`details.numbers`)・409 `issue_in_loop`(`details.numbers`・`details.loop_name`)・409 `parallel_unsupported_version`、`limits_exceeded` の `reasons` に `concurrent_repo_runs` |
| `POST /accounts/{a}/loops` | 席を持つ人 | `{repository_id, name, milestones, budget?: {laps?, usd?, hours?}, idempotency_key}` | `{loop_id, state}`(`running` か `between_laps`) | 400 `invalid_request`(名前・題の形、1〜10 件、予算が正の数でない)・403・404 `repository_not_found`・409 `milestone_in_use`(`details.milestones`・`details.loop_name`)・409 `loop_name_in_use`(同じ登録の終わりの状態でないループ) |
| `GET /accounts/{a}/loops?repository_id=` | 席を持つ人 | 条件 `repository_id` | `{loop_id, name, milestones, state, wait_reason, laps_closed, cost_usd, budget, budget_remaining, last_run_at, stop_condition, stop_detail}`(新しい順 50 件) | 要求全体の 400・403 |
| `GET /accounts/{a}/loops/detail?loop_id=` | 席を持つ人 | 鍵 `loop_id` | `{loop, laps: [{lap, issues, result, version, cost_usd, seconds}], runs: [{sprint_run_id, lap, state, failure_reason, finish_reason, resume_at, cost_usd, sections}], pending: [{number, reason, lap, answer}]}` | 403・404 |
| `PATCH /accounts/{a}/loops` | 席を持つ人 | `{loop_id, budget}` | ループの 1 行 | 400・403・404・409 `already_finished` |
| `POST /accounts/{a}/loops/cancel` | 席を持つ人 | `{loop_id}` | `{state}`(`cancelled` か、実行の停止を待つ `running`) | 403・404・409 `already_finished` |
| `POST /accounts/{a}/loops/next-lap` | 席を持つ人 | `{loop_id}` | `{state}` | 403・404・409 `not_awaiting_user_start` |
| `POST /accounts/{a}/loops/answers` | 席を持つ人(委任の資格情報では断る) | `{loop_id, issue, decision: approved \| rejected, pr?}` | `{loop_id, pending}` | 400・403・404・409 `not_pending`(`pending` に無い番号) |
| `PATCH /accounts/{a}/repositories`(`runs.yaml`) | 持ち主 | `{repository_id, max_concurrent_runs: 1〜100 の整数か null}` | 登録の 1 行 | 400・403・404 |
| `POST /accounts/{a}/sprint-runs`(`runs.yaml`) | 席を持つ人 | [スプリントの実行の経路](ndf-factory-runs-and-runner.md#画面向けの経路) のまま | 同じ | 既存の失敗に加えて 409 `issue_in_use`(`details.numbers`)・409 `issue_in_loop`(`details.numbers`・`details.loop_name`)・409 `parallel_unsupported_version`、`limits_exceeded` の `reasons` に `concurrent_repo_runs` |

## 関連リンク

Expand Down
2 changes: 1 addition & 1 deletion docs/specifications/ndf-factory-dev-loops.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ sequenceDiagram
## セキュリティ

- サービスは `local` のアカウントと認証を知らない。利用上限の事象はリセット時刻だけを持つ
- ループの操作は、そのリポジトリの登録を持つアカウントだけが行える。ループの経路は `resolveFromRequest` の後にループの `accountId` と登録を照らし、他人は 403、無ければ 404 になる
- ループの操作は、そのリポジトリの登録を持つアカウントだけが行える。ループの経路は `resolveFromRequest` の後に要素ごとにループの `accountId` と登録を照らし、他人のループの要素は 403、無ければ 404 の要素の結果になる
- 同時の実行の数の宣言は持ち主だけが変えられる

## テスト観点
Expand Down
Loading
Loading