diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 4d971717b..26d059b0b 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "ndf", "source": "./plugins/ndf", - "description": "Claude Code plugin (v10.27.21): 8 specialized agents (plus 1 worker agent for the 3-layer workflow) and 49 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", + "description": "Claude Code plugin (v10.27.24): 8 specialized agents (plus 1 worker agent for the 3-layer workflow) and 49 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/agy), transcript retention guard, and optional Slack notifications.", "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 930b397a9..22d46eb08 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -95,10 +95,24 @@ jobs: # # 前提のコマンドの確認と git の身元の設定は、根の conftest.py が行う。前提を # ワークフローの側にも置くと、増えたときに片方だけが更新される - - run: uv run --frozen --project . --all-extras pytest . -q -n auto + # 統括役の結合テスト(test_relay_hub.py::TestWithTmux)が tmux を使う。runner のイメージは tmux を + # 載せると約束していないため、無いときだけ入れる(ある間は時間を足さない。#2346)。apt の索引が古いと + # 取得で落ちるため、入れる経路では update を install の直前に打つ。版はログに残す + - name: tmux + run: | + command -v tmux || { sudo apt-get update && sudo apt-get install -y tmux; } + tmux -V + # 項目ごとの名前と結果を JUnit XML に残す(ログは -q で件数だけ。#2346) + - run: uv run --frozen --project . --all-extras pytest . -q -n auto --junitxml=pytest-junit-${{ matrix.shard }}.xml env: SHARD_TOTAL: 2 SHARD_INDEX: ${{ matrix.shard }} + - uses: actions/upload-artifact@v7 + if: always() + with: + name: pytest-junit-${{ matrix.shard }} + path: pytest-junit-${{ matrix.shard }}.xml + if-no-files-found: warn # ruleset の必須のチェックは `pytest` という名前の結果を 1 つ待つ。分けたジョブの結果を # ここで 1 つにまとめる。名前を変えると ruleset の変更が要る。 diff --git a/CHANGELOG.md b/CHANGELOG.md index e2a91fea3..ebbe21720 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,68 @@ **開発版(接尾辞の付いた版)は載せない。** `9.8.0` は `9.8.0-dev.1` までしか出ておらず、 その内容は `10.0.0` で届いている。 +## [ndf 10.27.24] - 2026-11-05 + +- 設計の承認に「承認」と答えると、その設計の PR がマージされる(#2542)(#2543) +- 開発ループの周回の境で、承認済みの設計の PR がマージし直される(#2542)(#2543) +- 承認に答えた後、その時点の整理結果が一覧で表示される(#2542)(#2543) +- 承認の答えの `--by` 引数が契約に含まれ、検査の対象になる(#2542)(#2543) +- 差分の棚卸しで見積の無い経路に出る Issue は、起点より後に作られたものだけになる(#2546)(#2547) +- 差分の棚卸しの各行に、プランの進行の記録から写した所要秒数・費用(ドル)・言語モデルが載る(#2546)(#2547) +- `depth compare` で、所要秒数などの中央値を基準と比べられる(#2546)(#2547) +- スプリントのまとめでは、プランの深さを記録するステップが、プランの進行の記録(`--progress`)と開発ループ・周回の番号(`--loop`・`--lap`)を受け取る(#2546)(#2547) +- NDF の `--budget-*` に `default` を渡すと、その単位の予算がリポジトリの予算の既定に戻ります(#2559)(#2561) +- Factory でループの予算の単位を空にすると、次のジョブからその単位にリポジトリの既定が使われます(#2559)(#2561) +- Factory のループの画面では、空の予算の単位が「リポジトリの既定」と表示されます(#2559)(#2561) +- 既定へ戻せない版では、Factory は予算の単位を空にする変更を受け付けず、画面に理由を表示します(#2559)(#2561) +- リポジトリの設定で本番のブランチ(`production_branch`)を宣言していなくても、作業用の worktree をまだ作っていない段階で、リポジトリの既定ブランチを本番のブランチとして実行計画を組む(#2562)(#2565) +- ndf の design で、設計の判断を下す前に外部の調査を行います。これが手順 2 になり、以降の手順は番号が 1 つずつ繰り下がります(#2564)(#2574) +- 設計文書のテンプレートに「外部の調査」の節があります。自前で作ると決めた事項には、比べた既存の候補を書く行があります(#2564)(#2574) +- 自前で作ると決めた事項に比べた候補が書かれていない設計文書は、ndf の設計の検査で指摘されます(#2564)(#2574) +- スプリントの設計の工程では、外部の調査の検査、指摘の修正、修正後の再検査が順に自動で実行されます(#2564)(#2574) +- ndf の pr-review で、レビューの観点に外部の調査の確認が入る(#2572)(#2574) +- ndf の cross-review で、設計 PR と実装 PR を見る担当へ外部の調査の確認の観点が渡る(#2572)(#2574) +- ndf の cross-review で、既定の分類を外すレビューのプロファイルを選んでも、外部の調査の確認の観点は残る(#2572)(#2574) +- ndf プラグインの開発ループは、人が送った停止のときだけ止める指示を置く(#2580)(#2581) + +## [ndf 10.27.23] - 2026-11-05 + +- 設計の承認に「承認」と答えると、その設計の PR がマージされる(#2542)(#2543) +- 開発ループの周回の境で、承認済みの設計の PR がマージし直される(#2542)(#2543) +- 承認に答えた後、その時点の整理結果が一覧で表示される(#2542)(#2543) +- 承認の答えの `--by` 引数が契約に含まれ、検査の対象になる(#2542)(#2543) +- 差分の棚卸しで見積の無い経路に出る Issue は、起点より後に作られたものだけになる(#2546)(#2547) +- 差分の棚卸しの各行に、プランの進行の記録から写した所要秒数・費用(ドル)・言語モデルが載る(#2546)(#2547) +- `depth compare` で、所要秒数などの中央値を基準と比べられる(#2546)(#2547) +- スプリントのまとめでは、プランの深さを記録するステップが、プランの進行の記録(`--progress`)と開発ループ・周回の番号(`--loop`・`--lap`)を受け取る(#2546)(#2547) +- NDF の `--budget-*` に `default` を渡すと、その単位の予算がリポジトリの予算の既定に戻ります(#2559)(#2561) +- Factory でループの予算の単位を空にすると、次のジョブからその単位にリポジトリの既定が使われます(#2559)(#2561) +- Factory のループの画面では、空の予算の単位が「リポジトリの既定」と表示されます(#2559)(#2561) +- 既定へ戻せない版では、Factory は予算の単位を空にする変更を受け付けず、画面に理由を表示します(#2559)(#2561) +- リポジトリの設定で本番のブランチ(`production_branch`)を宣言していなくても、作業用の worktree をまだ作っていない段階で、リポジトリの既定ブランチを本番のブランチとして実行計画を組む(#2562)(#2565) +- ndf の design で、設計の判断を下す前に外部の調査を行います。これが手順 2 になり、以降の手順は番号が 1 つずつ繰り下がります(#2564)(#2574) +- 設計文書のテンプレートに「外部の調査」の節があります。自前で作ると決めた事項には、比べた既存の候補を書く行があります(#2564)(#2574) +- 自前で作ると決めた事項に比べた候補が書かれていない設計文書は、ndf の設計の検査で指摘されます(#2564)(#2574) +- スプリントの設計の工程では、外部の調査の検査、指摘の修正、修正後の再検査が順に自動で実行されます(#2564)(#2574) +- ndf の pr-review で、レビューの観点に外部の調査の確認が入る(#2572)(#2574) +- ndf の cross-review で、設計 PR と実装 PR を見る担当へ外部の調査の確認の観点が渡る(#2572)(#2574) +- ndf の cross-review で、既定の分類を外すレビューのプロファイルを選んでも、外部の調査の確認の観点は残る(#2572)(#2574) + +## [ndf 10.27.22] - 2026-11-05 + +- 設計の承認に「承認」と答えると、その設計の PR がマージされる(#2542)(#2543) +- 開発ループの周回の境で、承認済みの設計の PR がマージし直される(#2542)(#2543) +- 承認に答えた後、その時点の整理結果が一覧で表示される(#2542)(#2543) +- 承認の答えの `--by` 引数が契約に含まれ、検査の対象になる(#2542)(#2543) +- 差分の棚卸しで見積の無い経路に出る Issue は、起点より後に作られたものだけになる(#2546)(#2547) +- 差分の棚卸しの各行に、プランの進行の記録から写した所要秒数・費用(ドル)・言語モデルが載る(#2546)(#2547) +- `depth compare` で、所要秒数などの中央値を基準と比べられる(#2546)(#2547) +- スプリントのまとめでは、プランの深さを記録するステップが、プランの進行の記録(`--progress`)と開発ループ・周回の番号(`--loop`・`--lap`)を受け取る(#2546)(#2547) +- NDF の `--budget-*` に `default` を渡すと、その単位の予算がリポジトリの予算の既定に戻ります(#2559)(#2561) +- Factory でループの予算の単位を空にすると、次のジョブからその単位にリポジトリの既定が使われます(#2559)(#2561) +- Factory のループの画面では、空の予算の単位が「リポジトリの既定」と表示されます(#2559)(#2561) +- 既定へ戻せない版では、Factory は予算の単位を空にする変更を受け付けず、画面に理由を表示します(#2559)(#2561) + ## [ndf 10.27.21] - 2026-11-05 - 設計の承認に「承認」と答えると、その設計の PR がマージされる(#2542)(#2543) diff --git a/README.md b/README.md index faeb5c7c2..8e877d171 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Claude Code では `/ndf:` と打って呼びます。**Skill** は | プラグイン名 | バージョン | できること | 詳細 | | --- | --- | --- | --- | -| **ndf** | 10.27.21 | 開発の作業手順(Skill)と専門のサブエージェント。Claude Code と Codex へ同じ名前で入る | [README](./plugins/ndf/README.md) | +| **ndf** | 10.27.24 | 開発の作業手順(Skill)と専門のサブエージェント。Claude Code と Codex へ同じ名前で入る | [README](./plugins/ndf/README.md) | | **playwright-kit** | 4.0.0 | Playwright による E2E テストの計画・スクリプト作成・証跡のレポート | [README](./plugins/playwright-kit/README.md) | 外部サービスへつなぐ MCP サーバーの設定のプラグインが 10 個あります(Claude Code / Codex 向け)。 @@ -69,7 +69,7 @@ Claude Code では `/ndf:` と打って呼びます。**Skill** は ### NDF プラグインの Skill の一覧 -**NDFプラグイン v10.27.21** に入る Skill の数は、AI ツールごとに違います。 +**NDFプラグイン v10.27.24** に入る Skill の数は、AI ツールごとに違います。 Claude Code向け core 49個、Codex向け core 45個です。 元Skills(49個)の内訳は次のとおりです。 - Pull Request とレビュー (7): pr, pr-tests, fix, pr-review, cherry-pick-pr, deploy, merged diff --git a/conftest.py b/conftest.py index b935ac705..7b3360cfe 100644 --- a/conftest.py +++ b/conftest.py @@ -55,8 +55,9 @@ # 増えた)。**生成物は収集の対象から外す。** collect_ignore_glob = ["plugins/*/dev.agy"] -# 束ごとに要る外部コマンド。**束によって前提が違う**ため、一覧は 1 か所に置きつつ -# 収集した束にだけ適用する。全体へ課すと、必要のないコマンドを求めることになる。 +# 束ごとに要る外部コマンド(前提のコマンドの一覧)。**束によって前提が違う**ため、一覧は +# 1 か所に置きつつ収集した束にだけ適用する。全体へ課すと、必要のないコマンドを求めることになる。 +# キーはリポジトリの根からの相対パスで、ファイルの中のクラスまで絞るときは `::` でつなぐ。 REQUIRED_COMMANDS: dict[str, tuple[str, ...]] = { "scripts/tests": ("bash", "jq", "git"), "plugins/ndf/skills/worktree/tests": ("bash", "jq", "git"), @@ -72,6 +73,9 @@ # スライドの生成スクリプト(pptxgenjs)を node で実行し、依存が無ければ `npm ci` で入れる(#2065) "plugins/ndf/skills/release/tests/test_slide_template.py": ("node", "npm"), "tests/test_presentation_decks.py": ("node", "npm"), + # 名前つきのラッパーを使い捨ての tmux サーバで動かす結合テスト(#2346)。同じファイルの + # ほかのテストは tmux を使わないため、クラスまで絞る。 + "plugins/ndf/scripts/tests/test_relay_hub.py::TestWithTmux": ("tmux",), } # 読み飛ばしを選ぶ指定。**既定は失敗**である。読み飛ばしたい実行環境のために残すが、 @@ -83,16 +87,27 @@ def _allow_missing() -> bool: return os.environ.get(ALLOW_MISSING_ENV, "") not in ("", "0") -def _bundle_of(path: Path) -> str | None: - """その項目が属する束を返す。一覧に無ければ `None`。""" - try: - rel = path.resolve().relative_to(ROOT).as_posix() - except ValueError: +def _item_id(item) -> str | None: + """項目の識別子(根からの相対パス、続けてファイルの中の位置を `::` でつないだもの)を返す。 + + 根の外のファイルは `None`。 + """ + path = Path(str(item.fspath)).resolve() + if not path.is_relative_to(ROOT): return None - for bundle in REQUIRED_COMMANDS: - if rel == bundle or rel.startswith(bundle + "/"): - return bundle - return None + _, sep, inner = item.nodeid.partition("::") + return path.relative_to(ROOT).as_posix() + sep + inner + + +def _bundles_of(item_id: str | None) -> list[str]: + """識別子が当たる一覧のキーを返す。 + + 当たるのは、識別子がキーと一致するか、キーの直後が `/` か `::` のときだけである + (`::TestWithTmux` は `::TestWithTmuxOld` に当たらない)。 + """ + if item_id is None: + return [] + return [k for k in REQUIRED_COMMANDS if item_id == k or item_id.startswith((k + "/", k + "::"))] def _missing(bundles: set[str]) -> dict[str, list[str]]: @@ -213,7 +228,7 @@ def _select_shard(config, items) -> None: def pytest_collection_modifyitems(config, items) -> None: _select_shard(config, items) - bundles = {b for item in items if (b := _bundle_of(Path(str(item.fspath)))) is not None} + bundles = {b for item in items for b in _bundles_of(_item_id(item))} missing = _missing(bundles) if not missing: return @@ -221,7 +236,7 @@ def pytest_collection_modifyitems(config, items) -> None: if _allow_missing(): mark = pytest.mark.skip(reason=f"外部コマンドが無い({ALLOW_MISSING_ENV} の指定により読み飛ばす)") for item in items: - if _bundle_of(Path(str(item.fspath))) in missing: + if any(b in missing for b in _bundles_of(_item_id(item))): item.add_marker(mark) return raise pytest.UsageError( diff --git a/docs/glossary.md b/docs/glossary.md index b1a94f3db..326d32327 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -71,6 +71,7 @@ NDF が提供する工程・承認ゲート・モード・ステップの語。d | フェーズレポート | — | supervisor(またはプラン)が最後に返す報告 | フェーズの報告 | — | `plugins/ndf/skills/development-workflow/references/glossary.md` | | worktree | `worktree` | 開発の変更を行う git worktree。課題ごとに 1 つ切る | 作業ツリー | — | `plugins/ndf/skills/development-workflow/references/glossary.md` | | メインディレクトリ | — | リポジトリを clone したディレクトリ | 主ディレクトリ | — | `plugins/ndf/skills/development-workflow/references/glossary.md` | +| 宣言を探す木 | — | supervise.py new が .ndf/ の宣言を探すディレクトリの並び。--worktree のパス → --repo → --worktree のパスの /.worktrees/ より前 → 今のディレクトリの順。既定ブランチは、この並びで最初に存在する git の木から解く | — | — | — | | ベースブランチ | — | worktree の分岐元と Pull Request の宛先 | 起点のブランチ | — | `plugins/ndf/skills/development-workflow/references/glossary.md` | | 安定版と実験版 | — | NDF の変更の 2 つの経路(stable / experimental)。実験版の置き場は `experimental/` | 安定と試行 | — | `plugins/ndf/skills/development-workflow/references/glossary.md` | | 即時修正 | — | 4 つの条件(`development-workflow` の SKILL.md の「即時修正」)を満たす不具合を、起票せず、その場のプランで直すこと | その場で直す | — | `plugins/ndf/skills/development-workflow/references/glossary.md` | @@ -306,8 +307,8 @@ NDF が提供する工程・承認ゲート・モード・ステップの語。d | 依存グループ | — | lib/deps.py の GROUPS と pyproject.toml の extra に並ぶ、外部パッケージの用途ごとのまとまり(procs・locks など)。deps.require へ名前で渡す | — | — | `docs/specifications/ndf-deps-require-groups-check.md` | | 未宣言のグループ | — | モジュールから import をたどって届く外部パッケージの依存グループのうち、そのモジュールの中で覆う deps.require が無いもの。そのモジュールを読む側が引き受ける | — | — | `docs/specifications/ndf-deps-require-groups-check.md` | | 任意の依存 | — | 無ければ代わりの手段へ落ちる import。except ImportError / ModuleNotFoundError で受けるか、同じ関数の中で find_spec / deps.importable で確かめ、確かめが成り立ったときだけ通る枝で読む | — | — | `docs/specifications/ndf-deps-require-groups-check.md` | -| 作業の報告 | `work_report` | worker が応答の最後に書く「## 作業の報告」の節。「結果:」は 完了 / 判断が要る / できなかった のどれかで、supervise はこれでステップの成否を決める | — | — | `docs/specifications/ndf-supervise-work-step-web-tools-and-report-stop.md` | -| 起こし直し | `resume_prompt` | supervise の full のステップで、作業の報告が無いまま応答が終わったとき、同じ会話(--resume)へ続けるよう送ること。3 回まで | — | — | `docs/specifications/ndf-supervise-work-step-web-tools-and-report-stop.md` | +| 作業の報告 | `work_report` | worker が応答の最後に書く「## 作業の報告」の節。人と conductor が読む。成否は作業の結果ファイルで決め、この節の文からは決めない(#2337) | — | — | `docs/specifications/ndf-supervise-work-step-web-tools-and-report-stop.md` | +| 起こし直し | `resume_prompt` | supervise の full のステップで、作業の結果ファイルが無いまま応答が終わったとき、同じ会話(--resume)へ続けるよう送ること。3 回まで(#2337) | — | — | `docs/specifications/ndf-supervise-work-step-web-tools-and-report-stop.md` | | 所要の記録 | — | `.ndf/project.json` の `test_duration.measured` にある全体テストの所要(秒。w)。出所は ndf-record → ci-junit → ci-steps の順に採る。宣言の解析が書き、プランの作成と test-run.py が読む | — | — | `docs/specifications/ndf-supervise-test-limits-from-whole-runs.md` | | 全体テストの実測 | — | 同じリポジトリで先に走った全体テストの壁時計の秒。所要の記録とは別に残し、後のプランと test-run.py が予算なしのテストの上限を決めるときに読む(#1631) | — | — | `docs/specifications/ndf-supervise-test-limits-from-whole-runs.md` | | テストの上限 | `limits` | test_strategy.limits が返す init_test_timeout・test_timeout・whole_timeout(秒)。ステップの timeout はこれに with_margin の余裕を足した値で、テストの上限には余裕を足さない | — | — | `docs/specifications/ndf-supervise-test-limits-from-whole-runs.md` | @@ -357,6 +358,7 @@ NDF が提供する工程・承認ゲート・モード・ステップの語。d | Skill の対応表 | — | 工程(ステップの stage)から、そのステップの worker へ届ける Skill の一覧を引く表。NDF の scripts/lib/stage-skills.json の 1 か所に置き、プランの worker の起動と ndf:worker の起動指示の両方が読む(#2376) | — | — | `docs/specifications/ndf-worker-stage-skills.md` | | Skill の渡し方 | — | Skill を worker へ届ける形。プランの worker には本文を system プロンプトへ入れ(本文)、ndf:worker には SKILL.md の絶対パスを起動指示の「読む Skill」に書く(パス。#2376) | — | — | `docs/specifications/ndf-worker-stage-skills.md` | | 読む Skill | — | supervisor が ndf:worker の起動指示に書く項目。その作業で読む規則の Skill の名前と SKILL.md の絶対パスを並べ、worker はここに挙がった SKILL.md だけを読む(#2376) | — | — | `docs/specifications/ndf-worker-stage-skills.md` | +| 読む Skill の guard | — | supervisor が ndf:worker を起動する Agent の呼び出しを PreToolUse の hook で見て、「読む Skill」の欄が無い起動と、Skill の対応表を引かずに Skill を挙げた起動を止める判定。止めた理由に正本の「supervisor → worker」の節を添える(#2399) | — | — | — | | 規則の Skill | — | 手順を持たず、書き方・記録の仕方のような規則だけを持つ Skill(document-style・logging-guidelines など)。Skill の対応表に置けるのはこれだけで、工程 Skill は置かない(#2376) | — | — | `docs/specifications/ndf-worker-stage-skills.md` | | 差分の棚卸し | — | 毎スプリントのまとめで行う棚卸しの深さ。そのスプリントの変更で食い違った本文の訂正・直ったのに開いたままの課題を閉じる・閉じた親の子の見直し・そのスプリントで起票した課題(起点のタグの時刻より後に作られた課題)への見積の段階の付与だけを行う(#2315) | — | — | `docs/specifications/ndf-backlog-refinement-depths.md` | | 見直し | — | 数スプリントに 1 回行う棚卸しの深さ。後回しを除く open の課題の枝刈り・順位付け・マイルストーンの再編と、ニーズ・シーズの調査からの起票を行う(#2315) | — | — | `docs/specifications/ndf-backlog-refinement-depths.md` | @@ -402,15 +404,34 @@ NDF が提供する工程・承認ゲート・モード・ステップの語。d | プランのハッシュ | — | プランの型で読んだ中身を、キーを並べ替えた JSON にして取った sha256。完了したプランが変わったかを測る(#2344) | — | — | — | | 呼び手 | — | 関数かエントリポイントを呼ぶ、テストの外のファイル(Skill の本文・雛形・hook・CI・ほかのスクリプト)。テストだけが呼ぶものは呼び手 0 と数える | — | — | — | | 残す関数の一覧 | — | 関数の棚卸し(uvx vulture)が呼び手 0 と見るが、vulture に見えない呼ばれ方などで残す関数を、理由の種類と理由と定義のあるファイルを 1 行ずつ添えて持つ一覧。置き場は scripts/ndf-vulture-keep.txt。構造チェックの例外リストとは別のもの | — | — | — | -| 外部の調査 | — | 設計の判断の前に、業界の事実上の標準・仕様、主要な製品の解き方、使える既存のライブラリとマネージドサービスを一次情報で調べ、設計文書に残す手順(#2519)。棚卸しのシーズの調査とは別のもの | — | — | — | -| 自前で作る決定 | — | 既存のライブラリ・サービス・標準の実装を使わず、プロジェクトの中で作ると決めた決定の記録。決定の作り方の行に「自前で作る」を含むもので見分ける(#2519) | — | — | — | -| 確かめられなかったこと | — | 外部の調査で、公開の資料が無い・取得できなかった・上限で止めた対象と、試した手段(URL か検索の語)と理由を並べた設計文書の欄(#2519) | — | — | — | -| 作り方 | — | 設計文書の決定の記録の中の 1 行。その決定が機能をどう得るか(自前で作る / ライブラリやサービスを使う / 標準に倣う、またはその組み合わせ)を書く(#2519) | — | — | — | -| 比べた候補 | — | 設計文書の決定の記録の中の 1 行。外部の調査の表で比べた行の番号(STD<数> / PRD<数> / LIB<数>)を並べる(#2519) | — | — | — | +| 外部の調査 | — | 設計の判断の前に、業界の事実上の標準・仕様、主要な製品の解き方、使える既存のライブラリとマネージドサービスを一次情報で調べ、設計文書に残す手順(#2519)。棚卸しのシーズの調査とは別のもの | — | — | `plugins/ndf/skills/design/references/external-survey.md` | +| 自前で作る決定 | — | 既存のライブラリ・サービス・標準の実装を使わず、プロジェクトの中で作ると決めた決定の記録。決定の作り方の行に「自前で作る」を含むもので見分ける(#2519) | — | — | `plugins/ndf/skills/design/references/decisions.md` | +| 確かめられなかったこと | — | 外部の調査で、公開の資料が無い・取得できなかった・上限で止めた対象と、試した手段(URL か検索の語)と理由を並べた設計文書の欄(#2519) | — | — | `plugins/ndf/skills/design/references/external-survey.md` | +| 作り方 | — | 設計文書の決定の記録の中の 1 行。その決定が機能をどう得るか(自前で作る / ライブラリやサービスを使う / 標準に倣う、またはその組み合わせ)を書く(#2519) | — | — | `plugins/ndf/skills/design/references/decisions.md` | +| 比べた候補 | — | 設計文書の決定の記録の中の 1 行。外部の調査の表で比べた行の番号(STD<数> / PRD<数> / LIB<数>)を並べる(#2519) | — | — | `plugins/ndf/skills/design/references/decisions.md` | | プランの進行の記録 | — | supervise のプランのステップごとの終了コード・秒・ドルを 1 行ずつ残す記録(`<状態のディレクトリ>/progress.jsonl`)。開発ループのまとめのプランでは `.ndf/loop/<名>/lap-<番号>/close*/` の下にある(#2315) | — | — | — | | 承認した先端 | — | 設計の承認を問うた時点の設計 Pull Request の先頭のコミット。承認した設計のマージはこの先端のときだけ行う | — | — | — | | マージの結果 | — | 承認した設計 Pull Request を宛先へマージする 1 回の試みの結果。マージした・マージ済み・先端が動いた・印が無い・検査が緑でない・衝突・宛先が本番・失敗のどれか | — | — | — | | 答えの後の整理 | — | オーガナイザーが答えを送った後、待つ間に届いた依頼を取り直し、除くものを除いて残りをまとめて問う手順 | — | — | — | +| 本体 | — | エントリポイントの仕事の中身を持つ関数。実行の核が呼び、結果 JSON か終了コードを返す(#2337) | — | — | — | +| 核の対象外 | — | 実行の核を通さないエントリポイントの印。hook の定義から起動されるか、ランタイムが直接起動して標準出力をそのまま使うもの。契約の一覧の core に理由と根拠を書く(#2337) | — | — | — | +| 作業の結果ファイル | — | worker が報告を終える前に書く、結果 JSON の形の 1 行のファイル。supervise と supervisor はこのファイルの status だけで作業の成否を決める(#2337) | — | — | — | +| フェーズの結果 | — | supervise の 1 つのプランの実行の終わり方を結果 JSON の形で書いたファイル(状態ディレクトリの phase-result.json)。supervise.py run の終了コードはこの status から決まる(#2337) | — | — | — | +| 扱い | — | 設計の結果の列。課題ごとに `実装する` / `取り込む` / `閉じる` のどれかを書く | — | — | — | +| 取り込み先 | — | 設計の結果の列。扱いが `取り込む` の課題を、どの課題の実装へ含めるかを課題の番号で書く | — | — | — | +| 触るファイル | — | 設計の結果の列。扱いが `実装する` の課題が変えるファイルの一覧。実装のプランの触る範囲になる | — | — | — | +| 食い違い | — | 同じ課題について、2 本の設計文書が違う設計の結果(扱い・取り込み先・触るファイルのどれか)を書いていること。設計の結果のステージは食い違いを見つけると止まり、人へ戻す | — | — | — | +| 出し直しの指摘 | — | 承認ゲート 1 で人が「直して出し直す」と答えたときに `--note` に渡したファイルの全文。レビューの担当が書く指摘ファイルとは別のもの | — | — | — | +| 出し直しの指摘の履歴 | — | 課題の本文に置く、出し直しの指摘を回ごとに追記する節。人の判断待ちの節を書き直しても消えない | — | — | — | +| 指摘への対応の表 | — | 出し直しの設計 PR の本文に置く表。出し直しの指摘の項目ごとに、直した / 直せない、直した箇所か直せない理由を 1 行で持つ | — | — | — | +| 設計のプラン | — | 開発ループの周回が課題ごとに組む、設計のフェーズのプラン(`sprint/<番号>-design-<課題>.json`)。要求・設計・レビュー・承認ゲート 1 のステップを持つ | — | — | — | +| 答えの種類 | — | 人が承認ゲートや問いに返す答えの区分。承認・直して出し直す・やらない・予算・止めるの 5 つ | — | — | — | +| 自動の出し直し | — | 出し直しの設計が出し直しの指摘の項目を指摘への対応の表に示していないとき、人へ問わずに開発ループが自分で付ける「直して出し直す」の答え。届いていない項目を名指しする。人の出し直しの 1 回につき 1 回まで | — | — | — | +| 出し直しの指摘の入力 | — | 開発ループが周回ごとに、同じリポジトリのすべてのループの記録から、課題の全回の出し直しの指摘を書き出したファイル。回はループの名前と回数の組で識別する。設計のプラン・MVV 判定・指摘への対応の表の突き合わせが読む | — | — | — | +| 設計の承認の依頼 | — | 並列の計画のレーンが、承認ゲート 1 の判断をオーガナイザーへ求めて送る依頼。設計 PR の先頭の SHA を持つ | — | — | — | +| 取り込みのコミット | — | 起点ブランチを設計 PR のブランチへ取り込むマージコミット。これだけで先頭が動いた設計 PR は、承認の依頼を送り直さない | — | — | — | +| 前のループ | — | 同じリポジトリの開発ループのうち、並列の計画のレーンでないもの。その答えがまだの人の判断待ちは、オーガナイザーの問いの一覧に出す | — | — | — | +| 当たらなかった答え | — | オーガナイザーの答えのうち、持ち主のループの記録の行へ当てられなかったもの。答えは耐久の記録に残し、区分 unmatched の問いとしてオーガナイザーの問いの一覧に出す | — | — | — | ## NDF の Slack 通知(`ndf-notification`) @@ -520,7 +541,8 @@ Pull Request のレビューを CLI へ委譲し、新しい指摘が出なく | 最終ゲート修正 | — | 最終ゲートの失敗を直す起動と、そのマージ処理(final-fix / merge-final-fix)。改善項目に属さない | 最終ゲートの修正 | — | `docs/specifications/cross-refactoring-verify-and-final-gate.md` | | 必須トレーラー | — | 担当がコミットメッセージのトレーラーとして書く Item-Id / Impl-Runtime / Impl-Model。最終ゲート修正は Item-Id を除く 2 つ | 必須の記名 | — | `docs/specifications/cross-refactoring-apply-intake.md` | | 帰属トレーラー | — | 実行環境がコミットメッセージの末尾へ足すトレーラー(Co-Authored-By: / Claude-Session:) | 帰属の段落 | — | `docs/specifications/cross-refactoring-apply-intake.md` | -| プロダクションコード | — | コードの拡張子を持ち、テストの置き場所でないファイル(production_code_changes) | 本番コード | — | `docs/specifications/cross-refactoring-round-tests-and-assess.md` | +| プロダクションコード | — | コードの拡張子を持ち、テストの置き場所でないファイル(production_code_changes)。名前がテストの形でもテストの関数を持たない .py は、tests/ などのテスト用ディレクトリの配下になければ含む | 本番コード | — | `docs/specifications/cross-refactoring-round-tests-and-assess.md` | +| テストの関数 | — | Python のファイルで、名前が test で始まる関数かメソッドの定義(async def を含む)。pytest が既定で集める形。名前がテストの形の .py は、これを持つときだけテストのファイルになる | — | — | `docs/specifications/cross-refactoring-scope-template-probe.md` | | 想定最大時間 | — | 利用者が与える所要の上限(分、--budget-minutes)。1 回の実行の子プロセスは、最終ゲートの確かめ・最終ゲート修正・打ち切りの後の取り消しも含めて、すべて想定最大時間の終わりまでに止まる | — | — | `docs/specifications/cross-refactoring-time-budget.md` | | 実装担当 | — | リファクタリング計画・テスト追加・実装・修正・最終ゲート修正を通して担う 1 ランタイム(--implementer) | — | — | `docs/specifications/cross-refactoring-time-budget.md` | | 改善候補 | — | 提案を path + symbol + smell のキーで集約したもの。リファクタリング計画へ渡す(candidates[]) | — | — | `docs/specifications/cross-refactoring-time-budget.md` | @@ -674,6 +696,9 @@ release が走らせるリリースの種別・リリースコマンド・公開 | 配布物の変化なし | — | PR の本文の `## 利用者向けの変化` が「無し」(「なし」も同じ)だけを持つこと。承認資料の「含む PR」の行に印を付け、CHANGELOG の版の節と「配る中身」には載せない。節の無い本文(題名で代える)とは別に扱う | — | — | `docs/specifications/ndf-release-approval-included-prs.md` | | マニフェスト | — | plugins/ndf/manifests/<ランタイム>-skills.txt。ランタイムごとに配る Skill の一覧で、配布の唯一の基準(#2231) | — | — | `docs/specifications/ndf-distribution-runtimes.md` | | 配布先のランタイム | — | マーケットプレイスのプラグインを導入できる形で届けるランタイム。#2231 の後は Claude Code と codex で、相手役のランタイムとは別に決まる(#2231) | — | — | `docs/specifications/ndf-distribution-runtimes.md` | +| 利用者の設定ディレクトリ | — | 利用者の Claude Code の設定ディレクトリ(`CLAUDE_CONFIG_DIR`、無ければ `~/.claude`)と Codex の設定ディレクトリ(`CODEX_HOME`、無ければ `~/.codex`)。リリース後テストはこの中の導入済みのプラグインと取得元の登録を変えない | — | — | — | +| 導入の操作 | — | プラグインの導入・更新と、取得元の登録・更新(`claude plugin install` / `update` / `marketplace add` / `marketplace update`、`codex plugin add` / `marketplace add`)。リリース後テストの worker は利用者の設定ディレクトリでこれを打たない | — | — | — | +| 利用者の設定の控え | — | 利用者の設定ディレクトリの、導入済みのプラグインと取得元の登録を鍵と値の組で書き出したもの。リリース後テストの前に取り、後で今の状態と比べる | — | — | — | ## NDF の指示書チェック(`ndf-instructions`) @@ -956,3 +981,5 @@ NDF を Web UI から操作できるサービスとして提供するときの | 操作の表 | — | Factory の契約から生成する、利用者向けの操作の一覧(operationId・メソッド・経路・操作の扱い・入力の定義)。画面・API・MCP がここから要求を組み、経路を登録する(#2384) | — | — | `docs/specifications/ndf-factory-agent-access.md` | | 要素の結果 | — | Factory の配列で受け渡す操作の応答の 1 要素。入力と同じ順に並び、成功なら値を、失敗なら状態と理由(code)を持つ(#2384) | — | — | `docs/specifications/ndf-factory-agent-access.md` | | 作成の冪等の鍵 | — | Factory で新しい識別子を作る作成の要素に、呼ぶ側が付ける鍵。同じ利用者・同じ操作・同じ鍵の作成を 1 件にする。進捗の事象の冪等の鍵とは別(#2384) | — | — | `docs/specifications/ndf-factory-agent-access.md` | +| 承認待ちの一覧 | — | アカウントのスプリントの実行のうち、答えの無い承認の依頼(ゲート 1・ゲート 2・判断)を、ループの名前と周回の番号とともに 1 つに並べた、読むだけの画面と API の読み出し | — | — | — | +| 取り込みの衝突 | — | ベースブランチの取り込みで解けずにスプリントの実行が止まったこと。進捗の事象では attention の理由 base_conflict で届く | — | — | — | diff --git a/docs/glossary/glossary.json b/docs/glossary/glossary.json index 2a2094058..c3f203339 100644 --- a/docs/glossary/glossary.json +++ b/docs/glossary/glossary.json @@ -598,6 +598,11 @@ ], "source": "plugins/ndf/skills/development-workflow/references/glossary.md" }, + { + "term": "宣言を探す木", + "context": "ndf-workflow", + "meaning": "supervise.py new が .ndf/ の宣言を探すディレクトリの並び。--worktree のパス → --repo → --worktree のパスの /.worktrees/ より前 → 今のディレクトリの順。既定ブランチは、この並びで最初に存在する git の木から解く" + }, { "term": "ベースブランチ", "context": "ndf-workflow", @@ -696,12 +701,18 @@ { "term": "プロダクションコード", "context": "ndf-cross-refactoring", - "meaning": "コードの拡張子を持ち、テストの置き場所でないファイル(production_code_changes)", + "meaning": "コードの拡張子を持ち、テストの置き場所でないファイル(production_code_changes)。名前がテストの形でもテストの関数を持たない .py は、tests/ などのテスト用ディレクトリの配下になければ含む", "source": "docs/specifications/cross-refactoring-round-tests-and-assess.md", "deprecated": [ "本番コード" ] }, + { + "term": "テストの関数", + "context": "ndf-cross-refactoring", + "meaning": "Python のファイルで、名前が test で始まる関数かメソッドの定義(async def を含む)。pytest が既定で集める形。名前がテストの形の .py は、これを持つときだけテストのファイルになる", + "source": "docs/specifications/cross-refactoring-scope-template-probe.md" + }, { "term": "文言固定テスト", "context": "ndf-workflow", @@ -4468,16 +4479,18 @@ { "term": "作業の報告", "context": "ndf-workflow", - "meaning": "worker が応答の最後に書く「## 作業の報告」の節。「結果:」は 完了 / 判断が要る / できなかった のどれかで、supervise はこれでステップの成否を決める", + "meaning": "worker が応答の最後に書く「## 作業の報告」の節。人と conductor が読む。成否は作業の結果ファイルで決め、この節の文からは決めない(#2337)", "code": "work_report", - "source": "docs/specifications/ndf-supervise-work-step-web-tools-and-report-stop.md" + "source": "docs/specifications/ndf-supervise-work-step-web-tools-and-report-stop.md", + "pending_source": "issues/issue-2337-design.md" }, { "term": "起こし直し", "context": "ndf-workflow", - "meaning": "supervise の full のステップで、作業の報告が無いまま応答が終わったとき、同じ会話(--resume)へ続けるよう送ること。3 回まで", + "meaning": "supervise の full のステップで、作業の結果ファイルが無いまま応答が終わったとき、同じ会話(--resume)へ続けるよう送ること。3 回まで(#2337)", "code": "resume_prompt", - "source": "docs/specifications/ndf-supervise-work-step-web-tools-and-report-stop.md" + "source": "docs/specifications/ndf-supervise-work-step-web-tools-and-report-stop.md", + "pending_source": "issues/issue-2337-design.md" }, { "term": "所要の記録", @@ -5022,6 +5035,12 @@ "meaning": "supervisor が ndf:worker の起動指示に書く項目。その作業で読む規則の Skill の名前と SKILL.md の絶対パスを並べ、worker はここに挙がった SKILL.md だけを読む(#2376)", "source": "docs/specifications/ndf-worker-stage-skills.md" }, + { + "term": "読む Skill の guard", + "context": "ndf-workflow", + "meaning": "supervisor が ndf:worker を起動する Agent の呼び出しを PreToolUse の hook で見て、「読む Skill」の欄が無い起動と、Skill の対応表を引かずに Skill を挙げた起動を止める判定。止めた理由に正本の「supervisor → worker」の節を添える(#2399)", + "pending_source": "issues/issue-2399-design.md" + }, { "term": "規則の Skill", "context": "ndf-workflow", @@ -5503,31 +5522,31 @@ "term": "外部の調査", "context": "ndf-workflow", "meaning": "設計の判断の前に、業界の事実上の標準・仕様、主要な製品の解き方、使える既存のライブラリとマネージドサービスを一次情報で調べ、設計文書に残す手順(#2519)。棚卸しのシーズの調査とは別のもの", - "pending_source": "issues/issue-2519-requirements.md" + "source": "plugins/ndf/skills/design/references/external-survey.md" }, { "term": "自前で作る決定", "context": "ndf-workflow", "meaning": "既存のライブラリ・サービス・標準の実装を使わず、プロジェクトの中で作ると決めた決定の記録。決定の作り方の行に「自前で作る」を含むもので見分ける(#2519)", - "pending_source": "issues/issue-2519-design.md" + "source": "plugins/ndf/skills/design/references/decisions.md" }, { "term": "確かめられなかったこと", "context": "ndf-workflow", "meaning": "外部の調査で、公開の資料が無い・取得できなかった・上限で止めた対象と、試した手段(URL か検索の語)と理由を並べた設計文書の欄(#2519)", - "pending_source": "issues/issue-2519-design.md" + "source": "plugins/ndf/skills/design/references/external-survey.md" }, { "term": "作り方", "context": "ndf-workflow", "meaning": "設計文書の決定の記録の中の 1 行。その決定が機能をどう得るか(自前で作る / ライブラリやサービスを使う / 標準に倣う、またはその組み合わせ)を書く(#2519)", - "pending_source": "issues/issue-2519-design.md" + "source": "plugins/ndf/skills/design/references/decisions.md" }, { "term": "比べた候補", "context": "ndf-workflow", "meaning": "設計文書の決定の記録の中の 1 行。外部の調査の表で比べた行の番号(STD<数> / PRD<数> / LIB<数>)を並べる(#2519)", - "pending_source": "issues/issue-2519-design.md" + "source": "plugins/ndf/skills/design/references/decisions.md" }, { "term": "深さの記録", @@ -5565,6 +5584,154 @@ "context": "ndf-workflow", "meaning": "オーガナイザーが答えを送った後、待つ間に届いた依頼を取り直し、除くものを除いて残りをまとめて問う手順", "pending_source": "issues/issue-2507-design.md" + }, + { + "term": "本体", + "context": "ndf-workflow", + "meaning": "エントリポイントの仕事の中身を持つ関数。実行の核が呼び、結果 JSON か終了コードを返す(#2337)", + "source": "", + "pending_source": "issues/issue-2337-design.md" + }, + { + "term": "核の対象外", + "context": "ndf-workflow", + "meaning": "実行の核を通さないエントリポイントの印。hook の定義から起動されるか、ランタイムが直接起動して標準出力をそのまま使うもの。契約の一覧の core に理由と根拠を書く(#2337)", + "source": "", + "pending_source": "issues/issue-2337-design.md" + }, + { + "term": "作業の結果ファイル", + "context": "ndf-workflow", + "meaning": "worker が報告を終える前に書く、結果 JSON の形の 1 行のファイル。supervise と supervisor はこのファイルの status だけで作業の成否を決める(#2337)", + "source": "", + "pending_source": "issues/issue-2337-design.md" + }, + { + "term": "フェーズの結果", + "context": "ndf-workflow", + "meaning": "supervise の 1 つのプランの実行の終わり方を結果 JSON の形で書いたファイル(状態ディレクトリの phase-result.json)。supervise.py run の終了コードはこの status から決まる(#2337)", + "source": "", + "pending_source": "issues/issue-2337-design.md" + }, + { + "term": "利用者の設定ディレクトリ", + "context": "ndf-release", + "meaning": "利用者の Claude Code の設定ディレクトリ(`CLAUDE_CONFIG_DIR`、無ければ `~/.claude`)と Codex の設定ディレクトリ(`CODEX_HOME`、無ければ `~/.codex`)。リリース後テストはこの中の導入済みのプラグインと取得元の登録を変えない", + "pending_source": "issues/issue-2401-requirements.md" + }, + { + "term": "導入の操作", + "context": "ndf-release", + "meaning": "プラグインの導入・更新と、取得元の登録・更新(`claude plugin install` / `update` / `marketplace add` / `marketplace update`、`codex plugin add` / `marketplace add`)。リリース後テストの worker は利用者の設定ディレクトリでこれを打たない", + "pending_source": "issues/issue-2401-requirements.md" + }, + { + "term": "利用者の設定の控え", + "context": "ndf-release", + "meaning": "利用者の設定ディレクトリの、導入済みのプラグインと取得元の登録を鍵と値の組で書き出したもの。リリース後テストの前に取り、後で今の状態と比べる", + "pending_source": "issues/issue-2401-design.md" + }, + { + "term": "扱い", + "context": "ndf-workflow", + "meaning": "設計の結果の列。課題ごとに `実装する` / `取り込む` / `閉じる` のどれかを書く", + "pending_source": "issues/issue-2361-requirements.md" + }, + { + "term": "取り込み先", + "context": "ndf-workflow", + "meaning": "設計の結果の列。扱いが `取り込む` の課題を、どの課題の実装へ含めるかを課題の番号で書く", + "pending_source": "issues/issue-2361-requirements.md" + }, + { + "term": "触るファイル", + "context": "ndf-workflow", + "meaning": "設計の結果の列。扱いが `実装する` の課題が変えるファイルの一覧。実装のプランの触る範囲になる", + "pending_source": "issues/issue-2361-requirements.md" + }, + { + "term": "食い違い", + "context": "ndf-workflow", + "meaning": "同じ課題について、2 本の設計文書が違う設計の結果(扱い・取り込み先・触るファイルのどれか)を書いていること。設計の結果のステージは食い違いを見つけると止まり、人へ戻す", + "pending_source": "issues/issue-2361-design.md" + }, + { + "term": "出し直しの指摘", + "context": "ndf-workflow", + "meaning": "承認ゲート 1 で人が「直して出し直す」と答えたときに `--note` に渡したファイルの全文。レビューの担当が書く指摘ファイルとは別のもの", + "pending_source": "issues/issue-2510-requirements.md" + }, + { + "term": "出し直しの指摘の履歴", + "context": "ndf-workflow", + "meaning": "課題の本文に置く、出し直しの指摘を回ごとに追記する節。人の判断待ちの節を書き直しても消えない", + "pending_source": "issues/issue-2510-requirements.md" + }, + { + "term": "指摘への対応の表", + "context": "ndf-workflow", + "meaning": "出し直しの設計 PR の本文に置く表。出し直しの指摘の項目ごとに、直した / 直せない、直した箇所か直せない理由を 1 行で持つ", + "pending_source": "issues/issue-2510-requirements.md" + }, + { + "term": "設計のプラン", + "context": "ndf-workflow", + "meaning": "開発ループの周回が課題ごとに組む、設計のフェーズのプラン(`sprint/<番号>-design-<課題>.json`)。要求・設計・レビュー・承認ゲート 1 のステップを持つ", + "pending_source": "issues/issue-2510-requirements.md" + }, + { + "term": "答えの種類", + "context": "ndf-workflow", + "meaning": "人が承認ゲートや問いに返す答えの区分。承認・直して出し直す・やらない・予算・止めるの 5 つ", + "pending_source": "issues/issue-2510-requirements.md" + }, + { + "term": "自動の出し直し", + "context": "ndf-workflow", + "meaning": "出し直しの設計が出し直しの指摘の項目を指摘への対応の表に示していないとき、人へ問わずに開発ループが自分で付ける「直して出し直す」の答え。届いていない項目を名指しする。人の出し直しの 1 回につき 1 回まで", + "pending_source": "issues/issue-2510-design.md" + }, + { + "term": "出し直しの指摘の入力", + "context": "ndf-workflow", + "meaning": "開発ループが周回ごとに、同じリポジトリのすべてのループの記録から、課題の全回の出し直しの指摘を書き出したファイル。回はループの名前と回数の組で識別する。設計のプラン・MVV 判定・指摘への対応の表の突き合わせが読む", + "pending_source": "issues/issue-2510-design.md" + }, + { + "term": "設計の承認の依頼", + "context": "ndf-workflow", + "meaning": "並列の計画のレーンが、承認ゲート 1 の判断をオーガナイザーへ求めて送る依頼。設計 PR の先頭の SHA を持つ", + "pending_source": "issues/issue-2510-requirements.md" + }, + { + "term": "取り込みのコミット", + "context": "ndf-workflow", + "meaning": "起点ブランチを設計 PR のブランチへ取り込むマージコミット。これだけで先頭が動いた設計 PR は、承認の依頼を送り直さない", + "pending_source": "issues/issue-2510-requirements.md" + }, + { + "term": "前のループ", + "context": "ndf-workflow", + "meaning": "同じリポジトリの開発ループのうち、並列の計画のレーンでないもの。その答えがまだの人の判断待ちは、オーガナイザーの問いの一覧に出す", + "pending_source": "issues/issue-2510-requirements.md" + }, + { + "term": "当たらなかった答え", + "context": "ndf-workflow", + "meaning": "オーガナイザーの答えのうち、持ち主のループの記録の行へ当てられなかったもの。答えは耐久の記録に残し、区分 unmatched の問いとしてオーガナイザーの問いの一覧に出す", + "pending_source": "issues/issue-2510-design.md" + }, + { + "term": "承認待ちの一覧", + "context": "ndf-factory", + "meaning": "アカウントのスプリントの実行のうち、答えの無い承認の依頼(ゲート 1・ゲート 2・判断)を、ループの名前と周回の番号とともに 1 つに並べた、読むだけの画面と API の読み出し", + "pending_source": "issues/issue-2430-requirements.md" + }, + { + "term": "取り込みの衝突", + "context": "ndf-factory", + "meaning": "ベースブランチの取り込みで解けずにスプリントの実行が止まったこと。進捗の事象では attention の理由 base_conflict で届く", + "pending_source": "issues/issue-2430-requirements.md" } ] } diff --git a/docs/metrics/ndf-token-usage/2026-11-05-4.json b/docs/metrics/ndf-token-usage/2026-11-05-4.json new file mode 100644 index 000000000..ade0c48e4 --- /dev/null +++ b/docs/metrics/ndf-token-usage/2026-11-05-4.json @@ -0,0 +1,812 @@ +{ + "per_pr": [ + { + "version": "10.27.19", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "sessions": 12, + "sessions_with_pr": 0, + "prs": 0 + }, + { + "version": "10.27.19", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "sessions": 25, + "sessions_with_pr": 0, + "prs": 0 + }, + { + "version": "10.27.21", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "sessions": 1, + "sessions_with_pr": 0, + "prs": 0 + }, + { + "version": "10.27.21", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "sessions": 18, + "sessions_with_pr": 0, + "prs": 0 + }, + { + "version": "10.27.21", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "sessions": 18, + "sessions_with_pr": 0, + "prs": 0 + }, + { + "version": "10.27.21", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "claude+codex", + "sessions": 1, + "sessions_with_pr": 0, + "prs": 0 + } + ], + "per_role": [ + { + "version": "10.27.19", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 12, + "cost": 620281.65, + "context": 4266516.833333333, + "out": 25148.5, + "minutes": 31.499813890126017, + "p": 61902.333333333336, + "k": 32.916666666666664, + "w5": 0.0, + "w1h": 144178.5, + "rewrites": 2, + "rewrites_after_5m": 2, + "rewrites_untimed": 0, + "rewrite_tokens": 224461, + "rewrite_tokens_after_5m": 224461, + "read_tokens_after_5m": 1587662, + "rewrite_gap_median": 119.9926833331585 + }, + { + "version": "10.27.19", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "supervisor", + "role": "mvv", + "agent_type": "claude -p", + "count": 1, + "cost": 81701.55, + "context": 38176.0, + "out": 1293.0, + "minutes": 0.25666666666666665, + "k": 1.0, + "w5": 0.0, + "w1h": 37603.0 + }, + { + "version": "10.27.19", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "supervisor", + "role": "pr", + "agent_type": "claude -p", + "count": 5, + "cost": 27659.620000000003, + "context": 14137.6, + "out": 1267.4, + "minutes": 0.21466666666666664, + "k": 1.0, + "w5": 0.0, + "w1h": 10571.2 + }, + { + "version": "10.27.19", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "worker", + "role": "work", + "agent_type": "claude -p", + "count": 11, + "cost": 298297.2545454545, + "context": 2081535.9090909092, + "out": 14507.818181818182, + "minutes": 9.311363636363636, + "k": 23.727272727272727, + "w5": 0.0, + "w1h": 62379.0 + }, + { + "version": "10.27.19", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 25, + "cost": 362717.274, + "context": 2357318.56, + "out": 12851.44, + "minutes": 7.609897333780924, + "p": 61524.68, + "k": 21.96, + "w5": 0.0, + "w1h": 92590.44, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 859526, + "rewrite_gap_median": null + }, + { + "version": "10.27.19", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "supervisor", + "role": "judge", + "agent_type": "claude -p", + "count": 1, + "cost": 28709.0, + "context": 13478.0, + "out": 351.0, + "minutes": 0.08499999999999999, + "k": 1.0, + "w5": 0.0, + "w1h": 13476.0 + }, + { + "version": "10.27.19", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "supervisor", + "role": "mvv", + "agent_type": "claude -p", + "count": 6, + "cost": 46902.38333333333, + "context": 29780.166666666668, + "out": 1507.0, + "minutes": 0.305, + "k": 1.0, + "w5": 0.0, + "w1h": 19423.833333333332 + }, + { + "version": "10.27.19", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "supervisor", + "role": "pr", + "agent_type": "claude -p", + "count": 5, + "cost": 18005.6, + "context": 13672.4, + "out": 915.6, + "minutes": 0.22400000000000003, + "k": 1.0, + "w5": 0.0, + "w1h": 6534.4 + }, + { + "version": "10.27.19", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "supervisor", + "role": "その他", + "agent_type": "ndf:worker", + "count": 14, + "cost": 321881.71785714285, + "context": 2480525.0714285714, + "out": 16455.785714285714, + "minutes": 2.9858321422622316, + "p": 24603.714285714286, + "k": 28.642857142857142, + "w5": 96268.42857142857, + "w1h": 0.0, + "rewrites": 2, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 57188, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": 0.5998999973138174 + }, + { + "version": "10.27.19", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "worker", + "role": "work", + "agent_type": "claude -p", + "count": 17, + "cost": 155429.82352941178, + "context": 774343.5882352941, + "out": 7741.35294117647, + "minutes": 4.139705882352941, + "k": 18.294117647058822, + "w5": 0.0, + "w1h": 39987.94117647059 + }, + { + "version": "10.27.21", + "mode": "light", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 1, + "cost": 278396.6, + "context": 1635254.0, + "out": 5450.0, + "minutes": 21.207466665903727, + "p": 45646.0, + "k": 16.0, + "w5": 0.0, + "w1h": 86846.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 113587, + "rewrite_gap_median": null + }, + { + "version": "10.27.21", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 18, + "cost": 537027.9055555556, + "context": 3757045.388888889, + "out": 20419.055555555555, + "minutes": 32.76583888883944, + "p": 60050.555555555555, + "k": 31.27777777777778, + "w5": 0.0, + "w1h": 126675.72222222222, + "rewrites": 2, + "rewrites_after_5m": 2, + "rewrites_untimed": 0, + "rewrite_tokens": 193001, + "rewrite_tokens_after_5m": 193001, + "read_tokens_after_5m": 2060517, + "rewrite_gap_median": 114.12099166711171 + }, + { + "version": "10.27.21", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "supervisor", + "role": "judge", + "agent_type": "claude -p", + "count": 2, + "cost": 28049.5, + "context": 13477.0, + "out": 219.5, + "minutes": 0.07083333333333333, + "k": 1.0, + "w5": 0.0, + "w1h": 13475.0 + }, + { + "version": "10.27.21", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "supervisor", + "role": "mvv", + "agent_type": "claude -p", + "count": 1, + "cost": 50513.0, + "context": 20180.0, + "out": 2031.0, + "minutes": 0.38333333333333336, + "k": 1.0, + "w5": 0.0, + "w1h": 20178.0 + }, + { + "version": "10.27.21", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "supervisor", + "role": "pr", + "agent_type": "claude -p", + "count": 2, + "cost": 16202.95, + "context": 14054.5, + "out": 1101.5, + "minutes": 0.18666666666666665, + "k": 1.0, + "w5": 0.0, + "w1h": 5123.5 + }, + { + "version": "10.27.21", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "supervisor", + "role": "その他", + "agent_type": "ndf:worker", + "count": 2, + "cost": 642557.7749999999, + "context": 5474422.5, + "out": 37932.0, + "minutes": 8.820508335034052, + "p": 39192.5, + "k": 43.0, + "w5": 149245.0, + "w1h": 0.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null + }, + { + "version": "10.27.21", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "worker", + "role": "work", + "agent_type": "claude -p", + "count": 18, + "cost": 100399.96388888889, + "context": 403989.05555555556, + "out": 4041.8888888888887, + "minutes": 5.20787037037037, + "k": 10.166666666666666, + "w5": 0.0, + "w1h": 30755.555555555555 + }, + { + "version": "10.27.21", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 18, + "cost": 294163.7833333333, + "context": 1694663.0, + "out": 10508.111111111111, + "minutes": 4.2267101855189715, + "p": 61148.944444444445, + "k": 16.944444444444443, + "w5": 0.0, + "w1h": 80439.77777777778, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 106920, + "rewrite_gap_median": null + }, + { + "version": "10.27.21", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "supervisor", + "role": "judge", + "agent_type": "claude -p", + "count": 1, + "cost": 28670.0, + "context": 13471.0, + "out": 346.0, + "minutes": 0.10333333333333333, + "k": 1.0, + "w5": 0.0, + "w1h": 13469.0 + }, + { + "version": "10.27.21", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "supervisor", + "role": "mvv", + "agent_type": "claude -p", + "count": 1, + "cost": 44985.55, + "context": 19543.0, + "out": 1403.0, + "minutes": 0.26999999999999996, + "k": 1.0, + "w5": 0.0, + "w1h": 18970.0 + }, + { + "version": "10.27.21", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "supervisor", + "role": "pr", + "agent_type": "claude -p", + "count": 9, + "cost": 17742.17777777778, + "context": 13894.444444444445, + "out": 1086.4444444444443, + "minutes": 0.1988888888888889, + "k": 1.0, + "w5": 0.0, + "w1h": 5955.555555555556 + }, + { + "version": "10.27.21", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "supervisor", + "role": "その他", + "agent_type": "general-purpose", + "count": 4, + "cost": 93726.1, + "context": 96657.0, + "out": 3363.25, + "minutes": 0.6167708327372868, + "p": 36594.0, + "k": 2.0, + "w5": 60061.0, + "w1h": 0.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null + }, + { + "version": "10.27.21", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "supervisor", + "role": "その他", + "agent_type": "ndf:worker", + "count": 4, + "cost": 73399.2375, + "context": 64504.75, + "out": 4546.0, + "minutes": 0.8831958333651225, + "p": 24969.25, + "k": 2.0, + "w5": 39533.5, + "w1h": 0.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null + }, + { + "version": "10.27.21", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "supervisor", + "role": "仕上げ", + "agent_type": "ndf:worker", + "count": 2, + "cost": 352426.125, + "context": 2606552.5, + "out": 20614.5, + "minutes": 3.953683332602183, + "p": 25633.0, + "k": 34.0, + "w5": 99134.5, + "w1h": 0.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null + }, + { + "version": "10.27.21", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "layer": "worker", + "role": "work", + "agent_type": "claude -p", + "count": 16, + "cost": 146426.4125, + "context": 854988.4375, + "out": 6133.625, + "minutes": 4.634062500000001, + "k": 15.0625, + "w5": 0.0, + "w1h": 37426.4375 + }, + { + "version": "10.27.21", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "claude+codex", + "layer": "conductor", + "role": "-", + "agent_type": "-", + "count": 1, + "cost": 142067.9, + "context": 511422.0, + "out": 1975.0, + "minutes": 2.3665000001589456, + "p": 45638.0, + "k": 6.0, + "w5": 0.0, + "w1h": 54672.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null + }, + { + "version": "10.27.21", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "claude+codex", + "layer": "supervisor", + "role": "judge", + "agent_type": "claude -p", + "count": 1, + "cost": 31103.0, + "context": 14805.0, + "out": 299.0, + "minutes": 0.09833333333333334, + "k": 1.0, + "w5": 0.0, + "w1h": 14803.0 + }, + { + "version": "10.27.21", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "claude+codex", + "layer": "worker", + "role": "work", + "agent_type": "claude -p", + "count": 5, + "cost": 125844.06000000001, + "context": 577738.2, + "out": 6012.2, + "minutes": 9.383000000000001, + "k": 13.0, + "w5": 0.0, + "w1h": 34295.0 + } + ], + "external": [ + { + "version": "10.27.19", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "runtime": "claude", + "skill": "cross-refactoring", + "cli_model": "claude-opus-5-5", + "count": 2, + "context": 421792.0, + "out": 7926.5, + "cost": 143051.52500000002, + "minutes": 5.60952499906222, + "p": 30231.0, + "k": 11.0, + "w5": 0.0, + "w1h": 42209.5, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 39179, + "rewrite_gap_median": null, + "call_seats": 2 + }, + { + "version": "10.27.21", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "runtime": "claude", + "skill": "cross-refactoring", + "cli_model": "claude-opus-5-5", + "count": 8, + "context": 349912.0, + "out": 5484.5, + "cost": 121540.73124999998, + "minutes": 1.6192791665593782, + "p": 24674.25, + "k": 8.5, + "w5": 0.0, + "w1h": 39285.375, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null, + "call_seats": 8 + }, + { + "version": "10.27.21", + "mode": "standard", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "-", + "runtime": "codex", + "skill": "cross-refactoring", + "cli_model": "gpt-6.1-sol", + "count": 1, + "input": 1352397.0, + "cached": 1224832.0, + "out": 7317.0, + "minutes": 4.2646999994913735, + "p": 27034.0, + "k": 15.0, + "rewrites": 1, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 27664, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": 0.11771666606267293, + "call_seats": 1 + }, + { + "version": "10.27.21", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "claude+codex", + "runtime": "claude", + "skill": "cross-review", + "cli_model": "claude-opus-5-5", + "count": 2, + "context": 308244.0, + "out": 4399.5, + "cost": 107451.375, + "minutes": 0.8740500013033549, + "p": 25160.0, + "k": 7.5, + "w5": 0.0, + "w1h": 35911.5, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null, + "call_seats": 2 + }, + { + "version": "10.27.21", + "mode": "不明", + "model": "claude-opus-5-5", + "cc": "2.1.318", + "reviewers": "claude+codex", + "runtime": "codex", + "skill": "cross-review", + "cli_model": "gpt-6.1-sol", + "count": 3, + "input": 266679.0, + "cached": 224256.0, + "out": 1924.0, + "minutes": 1.017683333820767, + "p": 26146.0, + "k": 6.0, + "rewrites": 0, + "rewrites_after_5m": 0, + "rewrites_untimed": 0, + "rewrite_tokens": 0, + "rewrite_tokens_after_5m": 0, + "read_tokens_after_5m": 0, + "rewrite_gap_median": null, + "call_seats": 3 + } + ], + "meta": { + "by": [ + "version", + "mode", + "model", + "cc", + "reviewers" + ], + "min_version": "10.27.19", + "until": "2026-11-05T18:52:00Z", + "sessions": 75, + "sessions_with_pr": 0, + "unlinked_external": 3187, + "unlinked_ledger": 0, + "skipped": { + "ndf の Skill が無い": 932, + "版を判定できない": 22 + }, + "released": "10.27.24", + "previous": "2026-11-05-3.json" + } +} diff --git a/docs/metrics/ndf-token-usage/2026-11-05-4.md b/docs/metrics/ndf-token-usage/2026-11-05-4.md new file mode 100644 index 000000000..ad2b8a509 --- /dev/null +++ b/docs/metrics/ndf-token-usage/2026-11-05-4.md @@ -0,0 +1,117 @@ +# ndf の版ごとのトークン消費と所要時間(2026-11-05 集計・10.27.24 の配布) + +**10.27.24 を正式版として出したときの記録である。** 打ち切りの時刻は `2026-11-05T18:52:00Z`。表の最初の行は 10.27.19(前の記録: 2026-11-05-3.md)。既定では前の記録の最後の版で、記録どうしが 1 行ずつ重なる。生の記録・会話の本文・リポジトリ名・会話の ID は残していない。集計した日時点の記録で、以後の記録の増減には追随しない。 + +## 作り方 + +同じ打ち切りの時刻で走らせ直すと、記録が残っている間は同じ `.json` ができる。ただし kiro の記録はターンに時刻を持たないため、打ち切りをまたいで進行中だった席は credit が変わりうる。 + +```bash +python3 scripts/token-usage-snapshot.py --released 10.27.24 --until 2026-11-05T18:52:00Z --min-version 10.27.19 +python3 scripts/token-usage.py --min-version 10.27.19 --until 2026-11-05T18:52:00Z --by version # 末尾の「集計の出力」 +``` + +## 比べるときの注意 + +- **PR を作った会話が 2 件以下の版:** 無し。作業の中身の違いが版の違いより大きい +- **CHANGELOG.md にあって表に出ない版:** 10.27.19, 10.27.21, 10.27.22, 10.27.23, 10.27.24。その版で始めた PR つきの会話が無い(会話の版は最初に読んだ版で決まる) +- **打ち切りの 30 分前以降にも行がある会話を含む版:** 10.27.21。進行中だった会話は、値が打ち切りの時刻までの分である + +## 版ごとの差(PR 1 本あたり) + +換算の合計は conductor・supervisor・worker の換算の和。前の版との差は、表の 1 つ上の行との比である。±30% を超えた版を下の「読み取り」で扱う。 + +| ndf の版 | 会話 | PR | 換算の合計 | 前の版との差 | conductor | supervisor | worker | 入力 | 所要 | codex 入力 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | + +## 版と層ごとの呼び出しとキャッシュ(1 起動あたり) + +P と k と書き込みは 1 起動あたり、書き直しとうち 5 分超は合計の回数である。 + +| ndf の版 | 層 | 起動 | P | k | 書き込み 5 分 | 書き込み 1 時間 | 書き直し | うち 5 分超 | 1 起動あたりの書き直し | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.27.19 | conductor | 37 | 61.6k | 25.5 | 0.00M | 0.11M | 2 | 2 | 0.05 | +| 10.27.19 | supervisor | 32 | 24.6k | 13.1 | 0.04M | 0.01M | 2 | 0 | 0.14 | +| 10.27.19 | worker | 28 | - | 20.4 | 0.00M | 0.05M | - | - | - | +| 10.27.21 | conductor | 38 | 59.8k | 23.4 | 0.00M | 0.10M | 2 | 2 | 0.05 | +| 10.27.21 | supervisor | 29 | 31.3k | 6.4 | 0.03M | 0.01M | 0 | 0 | 0.00 | +| 10.27.21 | worker | 39 | - | 12.5 | 0.00M | 0.03M | - | - | - | + +## 読み取り + +差の大きい版は無い(すべて ±30% の内)。読み取りは要らない。 + +## 集計の出力 + +対象の会話: 75 件 / PR を作った会話: 0 件 / 寄せ先の無い外部 CLI: 3187 件 / 寄せ先の無い帳簿の行: 0 件 + +換算は input を 1 とした費用(cache read はモデル別: claude-opus-5-5 0.05 / claude-fable-5-1 0.025 / 他 0.1。cache write 5 分 1.25・1 時間 2 / output 5)。入力は input + cache read + cache write。所要は分。 + +### PR 1 本あたり + +| 動いた版 | 会話 | PR | conductor 換算 | supervisor 換算 | worker 換算 | 入力 | 出力 | 所要 | codex 入力 | codex 出力 | kiro credit | claude 席 換算 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | + +### フェーズごと(1 起動あたり) + +| 動いた版 | 層 | フェーズ | 定義 | 起動 | 換算 | 入力 | 出力 | 所要 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.27.19 | conductor | - | - | 37 | 0.45M | 2.98M | 16.8k | 15.4 | +| 10.27.19 | supervisor | judge | claude -p | 1 | 0.03M | 0.01M | 0.4k | 0.1 | +| 10.27.19 | supervisor | mvv | claude -p | 7 | 0.05M | 0.03M | 1.5k | 0.3 | +| 10.27.19 | supervisor | pr | claude -p | 10 | 0.02M | 0.01M | 1.1k | 0.2 | +| 10.27.19 | supervisor | その他 | ndf:worker | 14 | 0.32M | 2.48M | 16.5k | 3.0 | +| 10.27.19 | worker | work | claude -p | 28 | 0.21M | 1.29M | 10.4k | 6.2 | +| 10.27.21 | conductor | - | - | 38 | 0.40M | 2.64M | 14.8k | 18.1 | +| 10.27.21 | supervisor | judge | claude -p | 4 | 0.03M | 0.01M | 0.3k | 0.1 | +| 10.27.21 | supervisor | mvv | claude -p | 2 | 0.05M | 0.02M | 1.7k | 0.3 | +| 10.27.21 | supervisor | pr | claude -p | 11 | 0.02M | 0.01M | 1.1k | 0.2 | +| 10.27.21 | supervisor | その他 | general-purpose | 4 | 0.09M | 0.10M | 3.4k | 0.6 | +| 10.27.21 | supervisor | その他 | ndf:worker | 6 | 0.26M | 1.87M | 15.7k | 3.5 | +| 10.27.21 | supervisor | 仕上げ | ndf:worker | 2 | 0.35M | 2.61M | 20.6k | 4.0 | +| 10.27.21 | worker | work | claude -p | 39 | 0.12M | 0.61M | 5.2k | 5.5 | + +### フェーズごとの呼び出しとキャッシュ + +P は起動ごとの固定費(最初の呼び出しの文脈)、k は呼び出し回数(どちらも 1 起動あたり)。書き直しは 2 回目以降の呼び出しのうち、書き込みが文脈の 50% を超え、かつ 20k を超えたものの回数(合計)。5 分超はそのうち直前の呼び出しとの間隔が 5 分を超えた回数、間隔は直前の間隔の中央値(分)。5 分超の書き直し・読み込みは、直前の間隔が 5 分を超えた呼び出しの書き直しの量と、書き直しでない呼び出しの読み込みの量(どちらも合計)。定義はサブエージェントの定義の名前。 + +| 動いた版 | 層 | フェーズ | 定義 | 起動 | P | k | 書き込み 5 分 | 書き込み 1 時間 | 書き直し | 5 分超 | 5 分超の書き直し | 5 分超の読み込み | 間隔 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.27.19 | conductor | - | - | 37 | 61.6k | 25.5 | 0.00M | 0.11M | 2 | 2 | 0.22M | 2.45M | 120.0 | +| 10.27.19 | supervisor | judge | claude -p | 1 | - | 1.0 | 0.00M | 0.01M | - | - | - | - | - | +| 10.27.19 | supervisor | mvv | claude -p | 7 | - | 1.0 | 0.00M | 0.02M | - | - | - | - | - | +| 10.27.19 | supervisor | pr | claude -p | 10 | - | 1.0 | 0.00M | 0.01M | - | - | - | - | - | +| 10.27.19 | supervisor | その他 | ndf:worker | 14 | 24.6k | 28.6 | 0.10M | 0.00M | 2 | 0 | 0.00M | 0.00M | 0.6 | +| 10.27.19 | worker | work | claude -p | 28 | - | 20.4 | 0.00M | 0.05M | - | - | - | - | - | +| 10.27.21 | conductor | - | - | 38 | 59.8k | 23.4 | 0.00M | 0.10M | 2 | 2 | 0.19M | 2.28M | 114.1 | +| 10.27.21 | supervisor | judge | claude -p | 4 | - | 1.0 | 0.00M | 0.01M | - | - | - | - | - | +| 10.27.21 | supervisor | mvv | claude -p | 2 | - | 1.0 | 0.00M | 0.02M | - | - | - | - | - | +| 10.27.21 | supervisor | pr | claude -p | 11 | - | 1.0 | 0.00M | 0.01M | - | - | - | - | - | +| 10.27.21 | supervisor | その他 | general-purpose | 4 | 36.6k | 2.0 | 0.06M | 0.00M | 0 | 0 | 0.00M | 0.00M | - | +| 10.27.21 | supervisor | その他 | ndf:worker | 6 | 29.7k | 15.7 | 0.08M | 0.00M | 0 | 0 | 0.00M | 0.00M | - | +| 10.27.21 | supervisor | 仕上げ | ndf:worker | 2 | 25.6k | 34.0 | 0.10M | 0.00M | 0 | 0 | 0.00M | 0.00M | - | +| 10.27.21 | worker | work | claude -p | 39 | - | 12.5 | 0.00M | 0.03M | - | - | - | - | - | + +### 外部 CLI(1 起動あたり) + +kiro はトークン数を記録しない(値が 0)ため credit だけを載せる。agy は読まない。 + +| 動いた版 | ランタイム | Skill | モデル | 起動 | 入力 | cache | 出力 | credit | 所要 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.27.19 | claude | cross-refactoring | claude-opus-5-5 | 2 | 0.42M | 0.00M | 7.9k | 0.00 | 5.6 | +| 10.27.21 | claude | cross-review | claude-opus-5-5 | 2 | 0.31M | 0.00M | 4.4k | 0.00 | 0.9 | +| 10.27.21 | claude | cross-refactoring | claude-opus-5-5 | 8 | 0.35M | 0.00M | 5.5k | 0.00 | 1.6 | +| 10.27.21 | codex | cross-review | gpt-6.1-sol | 3 | 0.27M | 0.22M | 1.9k | 0.00 | 1.0 | +| 10.27.21 | codex | cross-refactoring | gpt-6.1-sol | 1 | 1.35M | 1.22M | 7.3k | 0.00 | 4.3 | + +### 外部 CLI の呼び出しとキャッシュ(1 起動あたり) + +codex は input のうち cached に当たらなかった分を書き込みとみなし、呼び出しごとの値(last_token_usage)を持つ席だけで数える。kiro は呼び出し回数を記録しない(利用者のターン数は JSON の turns)。取れない値は -。 + +| 動いた版 | ランタイム | Skill | モデル | 起動 | P | k | 書き直し | 5 分超 | 5 分超の書き直し | 5 分超の読み込み | 間隔 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 10.27.19 | claude | cross-refactoring | claude-opus-5-5 | 2 | 30.2k | 11.0 | 0 | 0 | 0.00M | 0.04M | - | +| 10.27.21 | claude | cross-review | claude-opus-5-5 | 2 | 25.2k | 7.5 | 0 | 0 | 0.00M | 0.00M | - | +| 10.27.21 | claude | cross-refactoring | claude-opus-5-5 | 8 | 24.7k | 8.5 | 0 | 0 | 0.00M | 0.00M | - | +| 10.27.21 | codex | cross-review | gpt-6.1-sol | 3 | 26.1k | 6.0 | 0 | 0 | 0.00M | 0.00M | - | +| 10.27.21 | codex | cross-refactoring | gpt-6.1-sol | 1 | 27.0k | 15.0 | 1 | 0 | 0.00M | 0.00M | 0.1 | diff --git a/docs/specifications/README.md b/docs/specifications/README.md index d2a2f855e..53965f2a7 100644 --- a/docs/specifications/README.md +++ b/docs/specifications/README.md @@ -132,5 +132,6 @@ | [ndf-factory-sprint-views.md](ndf-factory-sprint-views.md) | Neural Development Factory: スプリントの一覧と詳細・工程の中身・バックログの読み出し(事象から導く派生・フェーズの名前の対応づけ・ランナーが送る工程の中身と伏せ字・経路の形・読み出しの口・画面と読み直し・常に成り立つ条件)。#2172 | | [ndf-script-shared-helpers.md](ndf-script-shared-helpers.md) | NDF のスクリプトの共通処理の置き場(使用量の行・時刻の読み取り・git の根・再起動の行・正の整数の判定)と、契約の抽出・パッケージ内の揃い・呼び出し元の数を理由に寄せない重複、長い関数の分け方。手順は `refactoring` の SKILL.md が正 | | [ndf-factory-agent-access.md](ndf-factory-agent-access.md) | Neural Development Factory: エージェントからの操作(委任の資格情報の発行・照合・取り消しと表・人だけが確定できる 10 の操作・要求の検査の順・MCP の受け口とツールの定義・呼び出し元の種類・追記だけの操作の記録と許可した鍵・作成の冪等の鍵の行・環境変数)。#2384 | +| [ndf-factory-shared-code-placement.md](ndf-factory-shared-code-placement.md) | Neural Development Factory: API と画面で同じ役割の処理を 1 か所に置く規則と置き場の一覧(handlers/ を handler の配線だけにする・保存の契約の port.ts・版の競合の再試行・ログインの確認と失敗の表示)。#2199 | Skill の挙動仕様はここに置かない。Skill に関する詳細は対象 Skill の `SKILL.md` を参照する。 diff --git a/docs/specifications/cross-review-review-profile.md b/docs/specifications/cross-review-review-profile.md index 757b0345e..2129a16c3 100644 --- a/docs/specifications/cross-review-review-profile.md +++ b/docs/specifications/cross-review-review-profile.md @@ -136,7 +136,7 @@ graph TD 3 を含め、重点が無いときの 3 は `fix-steps.py` の `_criterion_problems` が別に拒む(重点の検査を 1 か所に残す) - **`--focus` と `--extra-instructions-file` は、プロファイルがあっても自動レビュー観点の後ろに足される。** - **プロファイルの分類の名前が既定の自動カテゴリの名前と同じときは**、`auto_review_categories` では 1 つにまとめ、 - 観点の文は既定の後ろにプロファイルの文を足す(決定 5) + 観点の文は既定の後ろにプロファイルの文を足す(決定 5)。既定の分類が `design` か `code` を持てば、その後ろに外部の調査の確認の観点が付く - **`version: 2`・`3` の宣言を、それを知らない古い NDF が読むと**、宣言全体を読めないとして基準 1・2・4 で続ける(止まらない) - **プロファイルを選ぶ手がかりは PR の変更ファイルのパスだけである。** PR の題・本文・ラベル・ブランチ名では選ばない - **レビューの重点(`focus`)はリポジトリに 1 つのまま**で、どのプロファイルを選んでも効く @@ -176,7 +176,7 @@ PR の作り手が変えられるのは、基準の追加と、基準 1・4・ - `waive_kinds` と `reply` を書いた振り分けで、`finalize` の返信が `{inner}` に種類の名前を入れた文になり、重点があっても重点の句が無いこと。既定の雛形では重点の句が付くこと(I8) - 写しを持つ振り分けで、プロファイルに無い `wording` と `criteria` に無い番号を `finalize` が `invalid` で止め、`criterion: 2` と(重点があるとき)`3` は通すこと(I5) - `works/*/chapters/*/text.md` の分類が `works/novel-a/chapters/011/text.md` に当たり、`works/novel-a/x/chapters/011/text.md` には当たらないこと -- `replace_auto_categories` が真なら観点の列に `common` / `docs_only` が無く文はプロファイルの分だけ、偽か無ければ既定の文の後ろにプロファイルの文が付くこと +- `replace_auto_categories` が真なら観点の列に `common` / `docs_only` が無く文はプロファイルの分だけ、偽か無ければ既定の文の後ろにプロファイルの文が付くこと。どちらでも、既定の分類が `design` か `code` を持てば、最後に外部の調査の確認の観点が付くこと - プロファイルがある根の `fixer` 出力と `context` の文脈のファイルに「コードを変え」が無く「対象のファイルを変えずに」があること - `reply` に `{inner}` が無い宣言・知らない鍵のある宣言で、`review_criteria.py` が終了コード 0・既定の節・標準エラーに理由を出し、`init` の状態ファイルが `profile_status: unreadable` と既定の `reviewer_block` を持ち、`report` が理由を出し、同じ宣言の `focus` は効くこと(I4) - 再開の前にプロファイルを書き換えると `reviewer_block` と `review_instructions` が新しい値になり、書き換えなければ状態ファイルが書き直されないこと(I7) diff --git a/docs/specifications/ndf-backlog-refinement-depths.md b/docs/specifications/ndf-backlog-refinement-depths.md index 7d785593e..7f90a4bf4 100644 --- a/docs/specifications/ndf-backlog-refinement-depths.md +++ b/docs/specifications/ndf-backlog-refinement-depths.md @@ -125,9 +125,10 @@ | 副命令 | 引数 | 成功 | 失敗・否定 | | --- | --- | --- | --- | -| `candidates` | 既存の引数に `--depth diff\|review`(既定 `diff`) | `diff`: 経路 `diff-path`・`diff-identifier`・`sub-issue`・`commit-subject`・`manual`・`unscored`(と `--all`)。`review`: 上の経路に `no-milestone`・`closed-milestone` と、後回しを除く open の全件(`all`)を足す。上限で切らず、`updated_at` の古い順に並べる(`metrics.limit` は `null`、`metrics.depth` に深さ) | 既存 | +| `candidates` | 既存の引数に `--depth diff\|review`(既定 `diff`) | `diff`: 経路 `diff-path`・`diff-identifier`・`sub-issue`・`commit-subject`・`manual`・`unscored`(と `--all`。`unscored` は `--since-ref` のコミットの時刻(`metrics.since`)より後に作られた課題だけ)。`review`: 上の経路に `no-milestone`・`closed-milestone` と、後回しを除く open の全件(`all`)を足す。上限で切らず、`updated_at` の古い順に並べる(`metrics.limit` は `null`、`metrics.depth` に深さ) | 既存 | | `depth due review\|overhaul` | `--loop`・`--lap`・`--no-issue`・`--manual`・`--open N`(省くと GitHub から数える) | 0。`metrics.trigger` と `values`。`due` の行を足す | 3。起動しない(`not_due` の行と判定の値)。記録や設定が読めないときも 3 で、`values.reason: unreadable` の行を残す | -| `depth record` | `--depth diff\|review\|overhaul`(必須)・`--event ran\|not_due\|skipped`(既定 `ran`)・`--trigger a,b`・`--run`・`--report <統一 JSON レポート>`・`--loop`・`--lap`・`--sprint`・`--cost`・`--seconds` | 0。1 行を足す | 2 = `--trigger` の値が条件の一覧に無い | +| `depth record` | `--depth diff\|review\|overhaul`(必須)・`--event ran\|not_due\|skipped`(既定 `ran`)・`--trigger a,b`・`--run`・`--report <統一 JSON レポート>`・`--loop`・`--lap`・`--sprint`・`--cost`・`--seconds`・`--progress <プランの進行の記録>`・`--step <ステップの id>`(既定 `refine`) | 0。1 行を足す。`--progress` を渡すと、そのステップの最後の `kind: step` の行から秒・ドル・言語モデルの名前を写す(`--cost`・`--seconds` を渡せばその値を優先)。読めなければ空のまま 0 | 2 = `--trigger` の値が条件の一覧に無い | +| `depth compare` | `--since-ref `・`--seconds-under`・`--cost-under`(必須)・`--count`(既定 3)・`--model`・`--depth`(既定 `diff`) | 0。`` のコミットの時刻より後の `ran` の行のうち、秒とドルがそろい(外す理由 `no_usage`)、`--model` を渡せば `values.models` がその 1 つだけの(`model`)行を、同じ開発ループ・周回(ループの外では同じスプリント)では最後の 1 行にして(`duplicate`)、古い順に `--count` 件の秒とドルの中央値が両方とも基準を下回る。`metrics` に `count`・`needed`・`median_seconds`・`median_cost`・`baseline`・`delta`・`judged` | 1 = 中央値のどちらかが基準を下回らない / 3 = 件数に満たない(判定の保留)/ 2 = 記録か ref を読めない | | `causes record` | `--classes <[{number, type, sub}]>`(必須)・`--depth review\|overhaul`(既定 `review`) | 0。行を足す | 2 = 配列でない・型が `cause_types` に無い | | `owner claim` | `--what research\|review\|overhaul`(必須)・`--loop`・`--pid`(省くと呼び出し元)・`--keepalive <秒>` | 0(`run`・`taken_from`・`what`、見張りを起こしたら `keepalive_pid`) | 10 = 他の有効な持ち主がいる(`reason: not_owner`・`owner`)/ 3 = 錠を 5 秒で取れない | | `owner keepalive` | `--run`・`--pid`・`--seconds`(すべて必須) | 0(止まった理由 `released` / `owner_gone` / `expired`) | — | @@ -167,7 +168,7 @@ | `id` | 種類 | 中身 | 遷移 | | --- | --- | --- | --- | -| `refine-record` | run | `depth record --depth diff --sprint <名前>` | → `overhaul-due` | +| `refine-record` | run | `depth record --depth diff --sprint <名前> --progress {state_dir}/progress.jsonl`(`new close` に `--loop`・`--lap` を渡したときは、それも足す。開発ループのまとめの組み立てが渡す) | → `overhaul-due` | | `<深さ>-due` | run | `depth due <深さ>` | 3 なら `skip_to` で後へ。0 なら → `<深さ>-claim` | | `<深さ>-claim` | run | `owner claim --what <深さ> --keepalive 5400 --pid "$PPID"` | 10 なら → `<深さ>-skipped`。0 なら → `<深さ>` | | `<深さ>` | work(`full: true`、`stage` は `棚卸し`、`timeout` 5400) | `/ndf:backlog-refinement --depth <深さ>` を無人で通す。持ち主の実行は `owner status`、起動した条件は `depths.jsonl` から読む。最後に `depth record` と `owner release` を打つ | 成否によらず(`on_fail` も)→ `<深さ>-release` | @@ -199,12 +200,12 @@ | `depth` | `diff` / `review` / `overhaul` | | `event` | `ran`(行った)/ `due`(起動を判定した)/ `not_due`(起動しない)/ `skipped`(持ち主になれず飛ばした) | | `trigger` | 起動した条件の配列(`count`・`open_growth`・`carry`・`no_issue`・`manual`・`created_over_closed`・`open_rising`・`cause_share`)。`not_due` は空 | -| `values` | 判定の行は判定に使った値(`count`・`open`・`open_growth`・`carry`・`cause_share: {type, share}`、読めなければ `reason`・`detail`)。`diff` の `ran` の行は `carry`(`candidates.json` の `deferred` の件数) | +| `values` | 判定の行は判定に使った値(`count`・`open`・`open_growth`・`carry`・`cause_share: {type, share}`、読めなければ `reason`・`detail`)。`diff` の `ran` の行は `carry`(`candidates.json` の `deferred` の件数)と、プランの進行の記録から読めたときの `models`(棚卸しのステップで使った言語モデルの名前を名前の順に並べた配列) | | `loop`・`lap`・`sprint` | 空は `--loop` の外 | | `run` | 持ち主の実行。差分の棚卸しと判定の行は空 | | `open_before`・`open_after`・`closed`・`created` | `--report` の `metrics` から写す。`diff` の `ran` は `candidates.json` の `open` と `apply.json` の `closed` から写す | | `skipped_aspects`・`order`・`research` | `--report` の `metrics` にあれば写す | -| `cost`・`seconds`・`report` | 引数の値 | +| `cost`・`seconds`・`report` | 引数の値。`--cost`・`--seconds` が無く `--progress` を渡したときは、プランの進行の記録の棚卸しのステップの値(読めなければ空) | 回数は「最後のその深さの `ran` の行より後にある、`diff` の `ran` の行の数」で数える。`open_growth` は、前の見直しの `ran` の行の `open_after` と比べる。 `carry` は、最後の `diff` の `ran` の行の `values.carry` と、候補の上限 × `carry_factor` を比べる。`created_over_closed` / `open_rising` は、 @@ -343,7 +344,7 @@ classDiagram | まとめのプランで、`refine` の次が `refine-record`、大整理 → 見直しの判定は 3 で飛び、持ち主になれないと `skipped`、本体の成否によらず `release` を通る | `plugins/ndf/scripts/tests/test_supervise_pace.py`(`test_close_runs_spec_retro_and_close_once_each_in_order`)・`test_supervise_pace_verify.py` | | `vet --kind issue` が課題の本文をプロジェクト MVV と照合する | `plugins/ndf/scripts/tests/test_project_mvv.py`(`test_vet_issue_judges_an_issue_body_against_the_project_mvv`) | | `measure` が直接の依存から材料の案を問いで出す | `plugins/ndf/scripts/tests/test_project_decl.py`(`test_measure_proposes_materials_from_direct_dependencies_as_a_question`) | -| 毎スプリントの棚卸しの所要(秒・ドル)が、206〜224 秒・1.41〜1.61 ドルより短い | 手動確認(リリース後テスト)。変更の後の周回の `depths.jsonl` の `diff` の行の `cost`・`seconds` と比べる | +| 毎スプリントの棚卸しの所要(秒・ドル)が、206〜224 秒・1.41〜1.61 ドルより短い | 手動確認(リリース後テスト)。配布の後、`depth compare --since-ref <配布した版のタグ> --seconds-under 206 --cost-under 1.41 --model <言語モデル>` が 0 を返す | | 大整理の分類・柱・付け直しの一覧が #2335 と同じ形になる | 手動確認。固定の入力で大整理の手順 1 をやり直し、件数の差を報告に書く | ## 運用 diff --git a/docs/specifications/ndf-entrypoint-contracts.md b/docs/specifications/ndf-entrypoint-contracts.md index 04f1f4922..ea4586cc6 100644 --- a/docs/specifications/ndf-entrypoint-contracts.md +++ b/docs/specifications/ndf-entrypoint-contracts.md @@ -286,7 +286,8 @@ Pull Request の変更だけで承認ゲート 2 の判定やランタイムの **本番のブランチは `.ndf/worktree.json` の `production_branch`(無ければ既定ブランチ)だけが持つ。** 決め方は `lib/repo.py` の `production_of`・`production_branch` の 1 か所で、`supervise_lib/decl.py` と `lib/delivery.py` は -独自に後退しない(#2097)。`worktree.json` に寄せるのは、ベースブランチと並ぶ値であり、シェル +独自に後退しない。既定ブランチを解く木も `lib/repo.py` の `first_git_tree` 1 つで選び(宣言を探す木の並びのうち、 +最初に存在する git の木)、まだ作っていない worktree のパスを渡しても既定ブランチへ後退する。`worktree.json` に寄せるのは、ベースブランチと並ぶ値であり、シェル (`lib/worktree-branch.sh`)と `project-decl.py write` も既にここを読み書きするためである。 `.ndf/project.json` の `delivery` の行は、本番のブランチ・ベースブランチへのマージで動くことを `channel` で指し、 diff --git a/docs/specifications/ndf-factory-accounts-and-plans.md b/docs/specifications/ndf-factory-accounts-and-plans.md index 4408d71d2..6a5d92a73 100644 --- a/docs/specifications/ndf-factory-accounts-and-plans.md +++ b/docs/specifications/ndf-factory-accounts-and-plans.md @@ -59,8 +59,9 @@ Neural Development Factory の利用者が Google か GitHub のアカウント | アカウントの解決 | `factory/api/src/auth/resolve.ts`・`accounts/service.ts` | 経路の前に token を検証する中間処理と、利用者とアカウントの ID から権限つきの文脈を返す処理。利用者が無ければ作る | | アカウントの操作 | `factory/api/src/accounts/service.ts` | チームの作成・席の追加と種類の変更と削除・料金プランの変更・実行の形態の変更 | | 使える量の表と判定 | `factory/api/src/accounts/limits.ts` | 料金プランと席の種類ごとの上限の表と、実行の形態に合わせた問い合わせと判定 | -| アカウントの保存 | `factory/api/src/accounts/store.ts` | DynamoDB の 1 つの表への読み書き。条件つきの書き込みとトランザクション | -| API の入口 | `factory/api/src/handlers/api.ts` | API Gateway の HTTP API(payload 2.0)から呼ばれる Lambda の handler。ログ・CORS・認証の中間処理・エラーの形・経路の登録 | +| 保存の契約 | `factory/api/src/accounts/port.ts` | `AccountStore`・`AccountPatch`・`AccountUpdate`・`ConditionFailedError`。DynamoDB とメモリの 2 つの実装が満たし、AWS SDK を読まない | +| アカウントの保存 | `factory/api/src/accounts/store.ts` | 保存の契約の DynamoDB の実装。1 つの表への読み書き、条件つきの書き込みとトランザクション | +| API の入口 | `factory/api/src/app.ts`・`auth/deps.ts`・`handlers/api.ts` | `app.ts` の `createApiApp` がログ・CORS・認証の中間処理・エラーの形・経路の登録を持つ。`auth/deps.ts` が環境変数から認証の依存を組む。`handlers/api.ts` は API Gateway の HTTP API(payload 2.0)から呼ばれる Lambda の handler の配線だけを持つ(置き場の規則は [同じ役割の処理の置き場](ndf-factory-shared-code-placement.md)) | | GitHub の仲立ち | `factory/api/src/handlers/github-oidc.ts`・`github-oidc/{app,keys}.ts` | Cognito から OIDC の IdP として呼ばれ、GitHub の OAuth へ取り次ぎ、署名した ID token を返す | | API の契約 | `factory/contracts/openapi/accounts.yaml` | 画面向けの経路の OpenAPI。Spectral で検査する | | CI のジョブ | `.github/workflows/factory-check.yml` の `factory-check` | `factory/` かこのワークフローを変える Pull Request で `npm ci` と `npm run check` を打ち、Chromium を入れて画面の E2E(`npm run e2e -w web`。#2382)を打つ | diff --git a/docs/specifications/ndf-factory-contracts-and-progress.md b/docs/specifications/ndf-factory-contracts-and-progress.md index d2ad50a79..1cb02998e 100644 --- a/docs/specifications/ndf-factory-contracts-and-progress.md +++ b/docs/specifications/ndf-factory-contracts-and-progress.md @@ -208,7 +208,7 @@ graph TD | `EXECUTION_MODE` | 必須。`local` / `byok` / `managed` | | `MODEL_CREDENTIAL_SECRET_ARN` | `EXECUTION_MODE` が `byok` のとき必須、`local`・`managed` のとき持たない。形は `INGEST_TOKEN_SECRET_ARN` と同じ | | `LOOP_NAME`・`LOOP_MILESTONES` | `START` が `loop` のとき必須。ほかの開発ループの入力があれば `LOOP_NAME` も必須 | -| `LOOP_BUDGET` | 任意。宣言した予算(`laps=`・`usd=`・`hours=`)だけをカンマで並べる | +| `LOOP_BUDGET` | 任意。予算の単位(`laps=`・`usd=`・`hours=`)をカンマで並べる。値は数か `default`(その単位をリポジトリの既定へ戻す) | | `EXCLUDE_ISSUES` | 任意。選定から外す課題の番号(ランナーが NDF の `NDF_LOOP_EXCLUDE` へ渡す) | | `LOOP_ANSWERS` | 任意。人の判断待ちの課題への答え | diff --git a/docs/specifications/ndf-factory-dev-loops-data-and-contracts.md b/docs/specifications/ndf-factory-dev-loops-data-and-contracts.md index d153414b6..3085d03ce 100644 --- a/docs/specifications/ndf-factory-dev-loops-data-and-contracts.md +++ b/docs/specifications/ndf-factory-dev-loops-data-and-contracts.md @@ -31,7 +31,7 @@ erDiagram | ループ | `loopId` | 文字列 | 不可 | 32 字(実行と同じ作り方) | | ループ | `repositoryId`・`name` | 数・文字列 | 不可 | 属する登録と、NDF のループ名(`^[0-9A-Za-z][0-9A-Za-z_-]{0,63}$`) | | ループ | `milestones` | 文字列の並び | 不可 | 1〜10 件。題に `,` を含まない(NDF の `--milestones` の区切り) | -| ループ | `budget` | `{laps, usd, hours}` | 各値は可 | 空は「宣言しない(NDF の `.ndf/pace.json` の既定)」 | +| ループ | `budget` | `{laps, usd, hours}` | 各値は可 | 空はリポジトリの予算の既定(NDF の `.ndf/pace.json` の `loop.budget`)に従う | | ループ | `state` | 文字列 | 不可 | `running` / `between_laps` / `awaiting_approval` / `stopped` / `cancelled`。後ろの 2 つが終わりの状態 | | ループ | `waitReason` | 文字列 | 可 | `between_laps` の理由: `slots`(枠)・`legacy_version`・`user_start`(`local`)。空は「起こす途中」 | | ループ | `currentRunId` | 文字列 | 可 | 終わりの状態でない実行(I44)。空は「動く実行が無い」 | @@ -86,6 +86,7 @@ erDiagram | 名前 | 使う側 | 意味 | | --- | --- | --- | | `NDF_PARALLEL_MIN_VERSION` | 制御 | 並行を許す NDF の版の下限。無ければ並行を許さない | +| `NDF_BUDGET_RESET_MIN_VERSION` | 制御 | 予算の単位を既定へ戻せる(`--budget-* default` を受ける)NDF の版の下限。無ければ戻せない | | `LOCAL_LOOP_AUTOSTART` | 制御 | `true` のとき `local` のループも自動で次の周回を起こす。既定 `false` | | `USAGE_LIMIT_RETRY_SECONDS` | 制御 | リセット時刻を読めないときの待ち。既定 900 | | `RUNNER_SECTION_LIMIT` | ランナー | 区間の上限。既定 20 | @@ -103,7 +104,7 @@ erDiagram | `APPROVAL_REQUEST_ID` | 文字列・任意 | 無い `resume` は答えを書かずに続ける(利用上限の後の再開) | | `LOOP_NAME` | `^[0-9A-Za-z][0-9A-Za-z_-]{0,63}$`・`loop` で必須 | NDF のループ名。ループの実行の `resume` にも載せる | | `LOOP_MILESTONES` | 文字列・`loop` で必須 | マイルストーンの題を `,` で結ぶ | -| `LOOP_BUDGET` | `^(laps=\d+(\.\d+)?)?(,?usd=\d+(\.\d+)?)?(,?hours=\d+(\.\d+)?)?$`・任意 | 宣言した予算だけを並べる。無ければ NDF の既定 | +| `LOOP_BUDGET` | `^(laps=(\d+(\.\d+)?\|default))?(,?usd=(\d+(\.\d+)?\|default))?(,?hours=(\d+(\.\d+)?\|default))?$`・任意 | 単位ごとに数か `default`(その単位をリポジトリの既定へ戻す)を並べる。既定へ戻せる版の実行では 3 単位すべてを並べ、戻せない版では値のある単位だけを並べて 1 つも無ければ載せない | | `EXCLUDE_ISSUES` | 番号の並び・任意 | 同じリポジトリの課題の持ち分。無ければ外さない | | `LOOP_ANSWERS` | `<番号>=(approved\|rejected)(#)?` を `,` で結ぶ・任意 | 人の判断待ちへの答え | @@ -157,7 +158,7 @@ erDiagram | `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` | +| `PATCH /accounts/{a}/loops` | 席を持つ人 | `{loop_id, budget}` | ループの 1 行 | 400・403・404・409 `already_finished`・409 `budget_reset_unsupported`(既定へ戻せない版のループで値のある単位を空にした。`details.ndf_version`・`details.units`) | | `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` に無い番号) | diff --git a/docs/specifications/ndf-factory-dev-loops.md b/docs/specifications/ndf-factory-dev-loops.md index 7c889c122..ca8db7130 100644 --- a/docs/specifications/ndf-factory-dev-loops.md +++ b/docs/specifications/ndf-factory-dev-loops.md @@ -191,6 +191,8 @@ stateDiagram-v2 - **止まった・取り消したループは終わりで、戻る遷移を持たない。** 続けたい利用者は同じマイルストーンで新しいループを始める(持ち分は止まった時点で消えている)。戻る遷移を持つと、どの条件が戻れるかの表が要るためである - **ループを止めるときに NDF の `loop stop` を打たない。** 動いている実行は今の取り消し(`cancelSprintRun`)で止まり、ランナーは `SIGTERM` で何も送らずに終わる。取り消したループには次のジョブが起きず、止める印を書いても読む者がいないため、知らせはループの `cancelled` そのものとする - 予算の変更は終わりの状態でないループにだけ効き、次のジョブの指示から NDF の `--budget-*` へ渡る +- **空の予算の単位は、リポジトリの予算の既定(`.ndf/pace.json` の `loop.budget`)に従う。** 実行の版が `NDF_BUDGET_RESET_MIN_VERSION` 以上なら、周回のジョブの指示に空の単位を `<単位>=default`(既定へ戻す指示)で載せ、NDF が反映の時点の既定の値へ戻す。サービスは前の値を覚えず、空の単位を毎回 `default` で載せる(NDF は値が変わらなければ予算の履歴に行を足さない)。既定の値は Factory が持たず、画面は空の単位を「リポジトリの既定」と出す +- **既定へ戻せない版で動くループでは、値のある単位を空にする予算の変更を 409 `budget_reset_unsupported`(`details.ndf_version`・`details.units`)で断る。** 判定に使う版は、ループの今の実行の版、無ければ今のマーケットプレイスの版である。値を入れる・変える・空のままにする変更は、どの版でも受ける。下限の設定が無い・版を読めないときは戻せない側に倒し、指示には値のある単位だけを載せる ### 周回の開始(`startNextLap`) diff --git a/docs/specifications/ndf-factory-shared-code-placement.md b/docs/specifications/ndf-factory-shared-code-placement.md new file mode 100644 index 000000000..62396a043 --- /dev/null +++ b/docs/specifications/ndf-factory-shared-code-placement.md @@ -0,0 +1,192 @@ +# Neural Development Factory: 同じ役割の処理の置き場 + +Neural Development Factory の API(`factory/api/src`)と画面(`factory/web/src`)で、同じ役割の処理を 1 か所に置く規則と、その置き場を定める。HTTP の応答・OpenAPI の契約・DynamoDB の行の形・画面の表示は、この規則で変わらない。 + +## 概要 + +- **Lambda の handler のファイル(`factory/api/src/handlers/*.ts`)は、handler の配線だけを持つ。** Hono のアプリ・環境変数からの依存の組み立てはほかのファイルに置き、`handlers/` の外から `handlers/` を読まない +- **アカウントの保存の契約は、DynamoDB の実装と別のファイルに置く。** 契約は AWS SDK を読まない +- **同じ役割の関数・定数は `factory/` の中で 1 つだけ定義する。** 下の「置き場の一覧」に載る処理を足すときは、その置き場の関数を使い、写しを作らない +- **画面のログインの確認と失敗の表示は `context.tsx` の 2 つに寄せる。** 画面ごとに token の有無を確かめない + +## 用語 + +定義は [用語集](../glossary.md) が正である(文脈 `ndf-factory`)。この文書で使う語は「同じ役割の写し」「保存の契約」である。 + +## コンテキスト + +変更は Neural Development Factory(`ndf-factory`)の中だけに属する。集約(利用者・アカウント)と持ち主は [アカウントと料金プラン](ndf-factory-accounts-and-plans.md) の「集約」のとおりで、この文書は持ち主の内部の関数の置き場だけを定める。 + +## 背景 + +`factory/` の子課題は同じ `factory/api/`・`factory/web/` に足していく。そのため、環境変数の読み取り・再試行・ログインの確認などの処理が、子課題ごとに写されて広がっていた。cross-refactoring は同じ提案を毎回出し、時間の上限で見送っていた。置き場を 1 つに決め、足す側がその関数を使う形にした。 + +## 仕様 + +### 依存の向き + +```mermaid +graph LR + IDX[index.ts] --> APP[app.ts] + IDX --> AD[auth/deps.ts] + IDX --> PD[progress/deps.ts] + RD[runs/deps.ts] --> PD + HA[handlers/api.ts] --> APP + HA --> AD + HI[handlers/ingest.ts] --> PD + HQ[handlers/issue-requests.ts] --> PD +``` + +図の横幅のため、`env.ts` を読む向きは図から省いた。`env.ts` を読むのは `auth/deps.ts`・`progress/deps.ts`・`runs/deps.ts` と、`handlers/` の `ingest.ts`・`github-oidc.ts`・`issue-requests.ts` である。 + +handler の名前と置き場(`handlers/*.ts` の `handler`)は変えない。private リポジトリのインフラがこのファイルをエントリポイントとしてバンドルするためである。 + +### 置き場の一覧 + +| 処理 | 置き場 | 使う側 | +| --- | --- | --- | +| 必須の環境変数の読み取り(`requireEnv`)。無ければ `環境変数 <名前> がありません` を投げる | `factory/api/src/env.ts` | `auth/deps.ts`・`progress/deps.ts`・`runs/deps.ts`・`handlers/` の `ingest.ts`・`github-oidc.ts`・`issue-requests.ts` | +| API の Hono のアプリ(`createApiApp`)と、ログ(`jsonLogger`・`LogEntry`・`Logger`)・画面のオリジン(`webOriginFromEnv`) | `factory/api/src/app.ts` | `handlers/api.ts`・`index.ts`・テストの補助(`testing/app.ts`) | +| 認証の依存の組み立て(`depsFromEnv`。`AuthDeps` を 1 回だけ作る) | `factory/api/src/auth/deps.ts` | `handlers/api.ts`・`index.ts` | +| 進捗の記録の保存の組み立て(`progressStoreFromEnv`) | `factory/api/src/progress/deps.ts` | `handlers/ingest.ts`・`handlers/issue-requests.ts`・`runs/deps.ts`・`index.ts` | +| 保存の契約(`AccountStore`・`AccountPatch`・`AccountUpdate`・`ConditionFailedError`) | `factory/api/src/accounts/port.ts` | `accounts/store.ts`(DynamoDB の実装)・`accounts/memory-store.ts`(メモリの実装)・`accounts/service.ts`・`accounts/seat-service.ts`・`accounts/conflict.ts` | +| 版の競合の再試行(`retryOnConflict`)と、再試行しない書き込みの失敗の読み替え(`writeOr`) | `factory/api/src/accounts/conflict.ts` | `accounts/service.ts`(チームの作成・利用者の作成・料金プランと実行の形態の変更)・`accounts/seat-service.ts`(席の追加・種類の変更・削除) | +| 履歴の行を同じトランザクションで足す書き込み(`transact`)と、アカウントの表の鍵の頭(`USER#`・`ACCOUNT#`)・更新の列の表(`PATCH_COLUMNS`) | `factory/api/src/accounts/store.ts` | 同じファイルの書き込み | +| 席の種類の検証と文言(`validateSeatType`。`席の種類は standard か premium です`) | `factory/api/src/accounts/service.ts` | `routes/accounts.ts`・`routes/teams.ts` | +| アカウントの詳細の本文(`accountDetailBody`。チームなら席を読んで足す) | `factory/api/src/routes/views.ts` | `routes/accounts.ts`(詳細の取得・実行の形態の変更)・`routes/teams.ts`(チームの作成) | +| GitHub の利用者の取得と ID の検証(`githubUser`。取れなければ `null`) | `factory/api/src/github-oidc/app.ts` の `createGithubOidcApp` の中 | `/token`・`/userinfo` | +| IdP の判定(`idpFromUsername`) | `factory/api/src/auth/verify.ts` のモジュールの中 | `verifyAuthorization` | +| ログインの確認(`RequireLogin`) | `factory/web/src/context.tsx` | `App.tsx` のログインの後の画面の枠(`AppShell` の layout route)の `element` | +| 失敗を画面の文へ変える呼び出し(`useAttempt`) | `factory/web/src/context.tsx` | 画面(`pages/*.tsx`)と `pages/parts.tsx` | +| 席の種類の選択肢(`SeatTypeOptions`) | `factory/web/src/pages/parts.tsx`(`SEAT_TYPES` の隣) | `TeamPage.tsx`・`NewTeamPage.tsx` | + +### 同じ役割として寄せない処理 + +次の処理は、名前や形が似ていても判断が違うため、上の置き場へ寄せない。 + +| 処理 | 寄せない理由 | +| --- | --- | +| 実行の表の鍵(`runs/dynamo-table.ts` の `accountPk`) | アカウントの表とは別の表の設計である | +| 実行の枠・ループ・整理役・埋め戻しの再試行(`runs/`・`organizer/` の各定数) | アカウントの版の競合とは別の集約で、読み直しと判定の中身も違う | +| スプリントとループの開始の失敗(`NewSprintPage.tsx`・`LoopsPage.tsx`) | 失敗の種類ごとに文を変える | +| スプリントの手順の詳細の読み込み(`SprintDetailPage.tsx`) | 画面を離れた後の結果を捨てる判断を含む | +| 定期の読み直し(`poll.ts` の `usePolled`)と枠の利用者の読み込み(`ui/AppShell.tsx`) | 読み直しの失敗で前の表示を残し、誤りの文を出さない | +| 画面ごとの「読み込み中」と `role="alert"` の出し分け | 画面ごとに準備の条件が違う | + +### 常に成り立つ条件 + +| # | 条件 | 破れたときの扱い | +| --- | --- | --- | +| S1 | `factory/api/src` の `handlers/` の外のコード(`*.test.ts` を除く。`testing/app.ts` は含む)が `handlers/` を import しない。`handlers/` の中のファイルどうしも互いを import しない | 置き場の一覧の場所へ移してから足す | +| S2 | 置き場の一覧の処理の定義は `factory/` の中に 1 つだけある | 写しを消し、置き場の関数を使う | +| S3 | アカウントの書き込みは `transact` を通り、`TransactItems` を業務の項目 → 履歴の行 → 作成の冪等の鍵の行の順に並べる。鍵・属性・条件式・プレースホルダ(`:plan`・`:mode` など)の名前は [アカウントと料金プラン](ndf-factory-accounts-and-plans.md) の「アカウントの表」のとおりである | 並びか名前が違えば、DynamoDB の行の形が変わる。`store.test.ts` が落とす | +| S4 | `ConditionFailedError` は `accounts/port.ts` にだけ定義し、`store.ts` は再公開しない。2 つの実装・サービス・`conflict.ts` の `instanceof` が同じクラスを見る | 別のクラスになると条件の失敗を読み替えられない | + +### 版の競合の再試行と書き込みの失敗 + +`retryOnConflict(step, onOther)` の `step` は、読み直しと判定をして `{ done: T }`(書かずに終わる)か `{ write, result }`(書いて終わる)を返す。 + +- `step` の中で投げた誤り(権限・競合・料金プラン・席が無い)は、書き込みの前の判定なので、そのまま呼ぶ側へ届く +- 読み替えるのは `write` の失敗だけである +- `onOther` は条件以外の失敗を何へ変えるかを決める。既定は `AccountUnavailableError` で、料金プランの変更は受けた誤りをそのまま返す + +```mermaid +sequenceDiagram + participant S as 呼ぶメソッド + participant R as retryOnConflict + participant St as AccountStore + loop VERSION_RETRIES 回まで + R->>S: step()(読み直しと判定) + alt done + R-->>S: done の値を返す + else write + R->>St: write() + alt 成功 + R-->>S: result を返す + else ConditionFailedError + Note over R: 次の回へ + else ほかの失敗 + R-->>S: onOther(err) を投げる + end + end + end + R-->>S: AccountUnavailableError を投げる +``` + +`writeOr(write, onCondition)` は `write` を呼ぶ。結果は次の 3 つのどれかになる。 + +- 成功なら `undefined` を返す +- 条件の失敗なら `onCondition()` の結果を返す(`onCondition` は投げてもよい) +- それ以外の失敗は `AccountUnavailableError` を投げる + +| 呼ぶメソッド | `onCondition` | +| --- | --- | +| 席の追加 | `ConflictError("seat_exists", …)` を投げる | +| チームの作成 | 冪等の鍵があれば鍵の行を読み直し、先に書いた作成の文脈を返す。無ければ `AccountUnavailableError` を投げる | +| 利用者の作成 | 利用者を読み直し、あれば返す。無ければ `AccountUnavailableError` を投げる | + +### 履歴の行と更新の引数 + +- アカウントの履歴の行は、`accounts/service.ts` の呼ぶ側が `HistoryRecord` の名前つきのオブジェクトで書く。`before` と `after` の取り違えをフィールドの名前で防ぐためである。席の履歴の行は `accounts/seat-service.ts` の `seatHistory` が組み立てる +- `updateAccount` は `AccountUpdate`(`accountId`・`expectedVersion`・`patch`・`updatedAt`・`history`)を 1 つ受ける。`version` が `expectedVersion` と等しいときだけ `patch` を当て、`version` を 1 増やす + +```mermaid +classDiagram + class AccountStore { + <> + +updateAccount(update: AccountUpdate) + +addSeat(seat, history) + } + class AccountUpdate { + +accountId + +expectedVersion + +patch: AccountPatch + +updatedAt + +history: HistoryRecord[] + } + class AccountService + class SeatService + AccountStore ..> AccountUpdate : 受け取る + AccountService --> AccountStore : 使う + SeatService --> AccountStore : 使う + AccountService --> SeatService : 席の操作を任せる + DynamoAccountStore ..|> AccountStore + MemoryAccountStore ..|> AccountStore +``` + +### 画面のログインの確認と失敗の表示 + +- `RequireLogin` は token が無ければ、枠も画面も描かずに `/login` へ移る。ログインの後の画面はすべてこの枠の下にあり、後から足す画面にも確認が掛かる。そのため画面は token の有無を確かめず、`usePolled` は token の有無を引数に取らない +- `useAttempt()` は `attempt(op, show)` を返す。`attempt` は `op` を呼び、失敗したら画面の文を `show` へ渡す(401 ならログインの画面へ移って `show(null)`)。`attempt` は投げない +- `attempt` は描くたびに同じ関数を返す(`useCallback`)。`useEffect` の依存に入れても読み込みを繰り返さない + +## 外部連携 + +インフラ(private リポジトリ)がバンドルする handler は `handlers/*.ts` の `handler` のままである。一覧は [アカウントと料金プラン](ndf-factory-accounts-and-plans.md) の「外部連携」にある。 + +## テスト観点 + +- 次の検索で、置き場の一覧の処理が 1 か所にあることを満たすこと + - `grep -rn "function requireEnv" factory/api/src` が 1 行 + - `factory/web/src/pages` で `Navigate to="/login"` と `hasToken` が 0 行 + - `SEAT_TYPES.map` が `pages/parts.tsx` の 1 行 + - 失敗を画面の文へ変える `onFailure(err)` が `context.tsx`・`poll.ts`・`ui/AppShell.tsx` と、寄せない 3 画面(`NewSprintPage.tsx`・`LoopsPage.tsx`・`SprintDetailPage.tsx`)だけにある +- S1 の 2 つの検索がどちらも 0 行を返すこと + - `grep -rn 'from "[./]*/handlers/' factory/api/src --include=*.ts | grep -v '\.test\.ts:'` + - `grep -n 'from "\./' factory/api/src/handlers/*.ts | grep -v '\.test\.ts:'` +- `npm --prefix factory run check`(vitest・`tsc --noEmit`・spectral)が 0 で終わること。応答・表示・エラーの文言は既存の vitest が縛る +- `store.test.ts` の期待値(鍵・GSI の鍵・条件式・プレースホルダ・履歴の行の位置)が S3 を満たすこと +- `service.test.ts` と `handlers/api.test.ts` で、次の 4 つを満たすこと + - 版の競合が 3 回続けば保存の失敗を返す + - 書き込みの失敗で席も履歴も増えない + - 既に席があれば 409 を返す + - 冪等の鍵の 2 回目が 1 回目のチームを返す +- `verify.test.ts`・`github-oidc/app.test.ts`・`App.test.tsx` で、認証の判定と、token が無いとき・401 のときに `/login` へ移ることが変わらないこと +- `uv run --frozen --project . --all-extras pytest scripts/tests/test_check_markdown_links.py -q` が通ること。スクリプトの読み込みは関数のスコープの fixture で、テストごとに新しいモジュールを作る + +## 関連リンク + +- [アカウントと料金プラン](ndf-factory-accounts-and-plans.md)(集約・構成要素・アカウントの表) +- [画面の部品](ndf-factory-ui-components.md) +- [サービスのディレクトリと境界](ndf-factory-service-boundary.md) +- 課題 #2199( https://github.com/devbasex/ai-plugins/issues/2199 )、実装の Pull Request #2533( https://github.com/devbasex/ai-plugins/pull/2533 ) diff --git a/docs/specifications/ndf-parallel-organizer.md b/docs/specifications/ndf-parallel-organizer.md index d3d37b282..f181f385a 100644 --- a/docs/specifications/ndf-parallel-organizer.md +++ b/docs/specifications/ndf-parallel-organizer.md @@ -81,7 +81,7 @@ lroot2 の変更もまとめて出た。そのため lroot2 が出していた | 承認ゲート 2 は提示のプランが持ち、本番のプランは確定の後にだけ流す | 本番のプランは流し始めるときに worktree と `release/v<版>` を作る。答えの前に作ると、提示 2 の本番のプランが同じ名前のブランチを作り直せない | | 本番チャネルへの Pull Request の head を、承認したコミットから切った `release/v<版>` にする | `develop` を止めずに、承認したコミットと版上げだけを `main` へ出せる。`main` へ出るコミットはすべて `develop` に含まれる。並列の計画の外の配布にも同じ規則が効く | | 並列の計画の下では、区分つきの問いはオーガナイザーだけが出す。レーンの問いは問いの hook が送る | レーンは問いで止まらず別の作業へ進める。conductor の手順だけで送ると、手順に書いていない場所の問いが漏れる。送れなければ今どおり通すので、DBOS が読めないときも今の振る舞いに戻る | -| オーガナイザーは作業を持たない。答えの後の処理は持ち主のレーンが、持ち主のいない共有の作業は一時レーン(同時に 1 本)が行う | オーガナイザーに作業を積むと、その間は問いと配布の取りまとめが止まる。空いているレーンへ移す形は、空きの判定と他のレーンの周回への割り込みが要る | +| オーガナイザーは、設計の承認の答えで設計 PR をマージするほかは作業を持たない。答えの後の処理は持ち主のレーンが、持ち主のいない共有の作業は一時レーン(同時に 1 本)が行う | オーガナイザーに作業を積むと、その間は問いと配布の取りまとめが止まる。空いているレーンへ移す形は、空きの判定と他のレーンの周回への割り込みが要る | | 計画名での停止は `relay.py` ではなく `parallel.py stop` に置く | 統括役はリポジトリをまたぐ層で、並列の計画を知らない。統括役が知る語を役割の印だけに留める | | レーンの数の上限は `NDF_RELAY_MAX_NAMED` から読み、依存のあるレーンも数える | 統括役が実際に断る値と同じにする。同時に動く本数を依存から求めると、起動の前に確かめられない | | プランの本数の合計の上限は `NDF_PARALLEL_MAX_PLANS`(既定 6) | 2026-09-25 の実測(8 コア・30 GB で 6 本)。マシンごとに違うため環境変数で変える(2026-11-03 の承認ゲート 1 で利用者が決めた) | @@ -108,7 +108,7 @@ lroot2 の変更もまとめて出た。そのため lroot2 が出していた | I15 | 範囲に入ったレーンは、依頼していなくても配布の結果を受ける。自分の Pull Request のマージのコミットがすべて `origin/<本番チャネル>` にあれば、依頼を送らずに `released` で進む | 出せた配布ごとに範囲のイベント `range-<連番>` を立てる。`request` は送る前に本番チャネルを照らし、版は `range-<連番>` から読む | | I16 | 本番チャネルへ出すコミットは、すべて `develop` に含まれる | `scripts/check-pr-base.sh`(継続的統合の `pr-base-guard`)が落とす | | I17 | 並列の計画の下で区分つきの問いを出すのはオーガナイザーだけである | レーンと一時レーンの問いは依頼(`kind: gate1` / `gate2` / `ask`)としてオーガナイザーへ届く | -| I18 | オーガナイザーは作業を持たない。一時レーンは同時に 1 本まで | `answer` は答えのメッセージを送るだけで、ラベル・記録・直しは持ち主のレーンが周回の境で行う | +| I18 | オーガナイザーは、設計の承認で設計 PR をマージするほかは作業を持たない。一時レーンは同時に 1 本まで | `answer --approved` は設計の承認の依頼なら、承認した先端のままで必須の検査が緑の設計 PR を待たずにマージしてから答えのメッセージを送り、ほかの答えはメッセージを送るだけである。ラベル・記録・直し・マージし直し・後片付けは持ち主のレーンが周回の境で行う | | I20 | 依頼の `id` は送る側が依頼の中身から決め、受け取った数から採番しない | 打ち直しで同じ `id` になり、冪等の鍵(`:inbox:<依頼>`)が 2 度目を数えない。受け取りの前後で打ち直しても `id` が変わらない | ### 構成要素 @@ -124,6 +124,8 @@ lroot2 の変更もまとめて出た。そのため lroot2 が出していた | `plugins/ndf/scripts/lib/durable.py` | 外のプロセスから記録を開く関数(`open_client`・`send_to`・`event_from`・`status_from`・`output_from`・`wait_from`)と、耐久ワークフローの中の `recv`・`set_event`。DBOS を import するのはこのファイルだけである | | `plugins/ndf/scripts/relay_lib/hub.py`・`hub_client.py` | 役割のファイルの読み書き(`relay.py role`・`spawn --role helper --plan`)と数え方(I13)。`ls --json` の `metrics.hub`・`metrics.tmux` | | `plugins/ndf/scripts/lib/sprint_loop_gate.py` | ループのレーンが設計の承認を「人の判断待ち」へ移すときに、`kind: gate1` の依頼を送る。答えを当てる前に設計 PR の先頭を比べる | +| `plugins/ndf/scripts/lib/design_pr_merge.py` | 承認した設計のマージ(`merge_approved`)。`parallel.py answer --approved`・`loop answer --approved`・周回の境が呼ぶ | +| `plugins/ndf/scripts/lib/parallel_answer.py` | 答えの後に起こす 1 行(`owner_restart`)と、答えの後に残る依頼の一覧と除外の印(`after_items`・`mvv_mark`) | | `plugins/ndf/scripts/hook_lib/ask_guard.py` | 判定が通す問いを、レーンか一時レーンなら `parallel.py request-ask` で送って止める | ```mermaid @@ -250,7 +252,7 @@ stateDiagram-v2 | メッセージ | 話題 `resume` | `confirm`・`settle` | 配布の途中の耐久ワークフロー | `{n, phase: confirm, take}` / `{n, phase: settle, result, version, reason}` | | イベント | `pause` | 耐久ワークフロー | `wait` | `{seq, action, …}`。`action` は `running` / `notice` / `release` / `confirmed` / `held`。`running` では `wait` は返らない。起こす 1 行(`wake`)を添える | | イベント | `questions` | 耐久ワークフロー | `wait` | `{seq, items}`。答えていない問いの依頼の全件 | -| イベント | `answer-<依頼>` | 耐久ワークフロー | `await`・周回の境(`answers_of`) | 配布: `{result, version, release, reason, at}`。問い: `{result, note, choice, stamp, at}` | +| イベント | `answer-<依頼>` | 耐久ワークフロー | `await`・周回の境(`answers_of`) | 配布: `{result, version, release, reason, at}`。問い: `{result, note, choice, stamp, merge, by, at}`(`merge` は設計の承認で試みたマージの結果、`by` は `user` か `mvv`) | | イベント | `defer-<依頼>` | 耐久ワークフロー | `await` | `{seq, reason, at}`。知らせのたびに上書きしてよい | | イベント | `range-<連番>` | 耐久ワークフロー | `request`(`released_version`) | 出せた配布の `{sha, version, prs, lanes}`。差し戻し・失敗の配布の連番は欠ける | | イベント | `lane-<レーン名>` | 耐久ワークフロー | 状態の表示 | `{received}`。依頼の `id` の採番には使わない | diff --git a/docs/specifications/ndf-script-shared-helpers.md b/docs/specifications/ndf-script-shared-helpers.md index 48845b46d..8be3e1962 100644 --- a/docs/specifications/ndf-script-shared-helpers.md +++ b/docs/specifications/ndf-script-shared-helpers.md @@ -49,7 +49,7 @@ NDF のスクリプト(`plugins/ndf/scripts/` と、Skill の `skill-stats`・ | 再起動の行と印の行を組む(セッションの読み取り・境目の行・`cut` と再起動の付与) | `lib/context_decisions.py` の `record_row` | `context-policy.py` の `cmd_record`、同じモジュールの `record_restart`(`sprint-state.py next`) | | 正の整数(`bool` を除く)か判定する | `lib/context_policy.py` の `is_positive_int` | 同じモジュールの方針の解決、`lib/context_decisions.py` の `read_window` | | 試行方式の拒否の句で使う方針と理由の語 | `lib/context_policy.py` の既存の定数 `TRIAL`・`REASON_CAPACITY`・`REASON_COST` | `hook_lib/token_guard.py` の `_trial_cause` | -| 「その深さを実行した」行の判定と、最後に実行した行の位置 | `backlog-refinement/scripts/upkeep_depth.py` の `_is_ran`・`_last_ran` | 同じファイルの `diffs_since`・`_ran`・`_due_overhaul` | +| 「その深さを実行した」行の判定と、最後に実行した行の位置 | `backlog-refinement/scripts/upkeep_depth.py` の `_is_ran`・`_last_ran` | 同じファイルの `diffs_since`・`_ran`・`_due_overhaul`・`compare_rows` | **`clock.parse` は前後の空白と小文字の `z` も読む。** NDF の記録の時刻は `clock.now_iso` が書くため、 この 2 つを含む時刻は記録に現れず、記録を読む結果は変わらない。形の違う時刻の読み取り diff --git a/docs/specifications/ndf-ubiquitous-language.md b/docs/specifications/ndf-ubiquitous-language.md index fbb639236..ae4521fb4 100644 --- a/docs/specifications/ndf-ubiquitous-language.md +++ b/docs/specifications/ndf-ubiquitous-language.md @@ -15,7 +15,7 @@ NDF の開発ワークフローは、要求と設計の前に、そのプロジ | 用語集の設定と用語集の JSON の形・用語チェックの規則・副命令の一覧 | `plugins/ndf/skills/requirements-design/references/glossary-format.md` | | 要求の工程の手順 0(用語集を用意する)・3a(ドメインイベント)・3b(語の突き合わせ)・7(課題の本文とコピー) | `plugins/ndf/skills/requirements-design/SKILL.md` | | ドメインモデルの節の書き方・コンテキストマップの 5 つの関係・集約の 4 つの規則・エンティティと値オブジェクトの区別 | `plugins/ndf/skills/design/references/domain-model.md` | -| 設計の工程の手順 0(入口の検査)・2(語の突き合わせ)・5(コピーの確かめ) | `plugins/ndf/skills/design/SKILL.md` | +| 設計の工程の手順 0(入口の検査)・3(語の突き合わせ)・6(コピーの確かめ) | `plugins/ndf/skills/design/SKILL.md` | | 不変条件のテストを先に書く手順 | `plugins/ndf/skills/tdd-cycle/SKILL.md` の手順 1 | | ai-plugins の語 | [docs/glossary.md](../glossary.md)(`docs/glossary/glossary.json` から作る) | @@ -75,7 +75,7 @@ ai-plugins の設計 Pull Request #742 は、返信を除いたインライン | 語を扱う処理は `glossary.py` にまとめ、既存の検査に足さない | `instructions-check.py` と `doc-lint.py` は読む設定も規則の出所も違う。追加した行の差分の取り方だけを `doc-lint.py` と揃える | | 語の候補は決まった 3 つの形からスクリプトが集め、領域の語かを LLM が選び、利用者が採る | 表の 1 列目・繰り返す短い太字・型の名前は、言語やリポジトリを問わず機械的に取り出せる。リポジトリ全体を LLM に読ませると、同じ入力で違う候補が出て、所要も大きい | | 要求の工程に足した手順 0・3a・3b は `standard` のときだけ通す | 要求の工程は `light` / `operation` / `documentation` も通る。絞らないとこの 3 モードの工程と所要が変わる。`legacy-refactor` は要求の工程を通らないため、`gate` が手順 0 だけを指す | -| ドメインモデルの節は `standard` で必須、ほかのモードで該当時にする | `legacy-refactor` の設計文書は 3 節でできており、振る舞いを変えないため不変条件とドメインイベントは変わらない。`light` / `operation` に求めると、1 節で足りる設計が増える | +| ドメインモデルの節は `standard` で必須、ほかのモードで該当時にする | `legacy-refactor` の設計文書は 4 節でできており、振る舞いを変えないため不変条件とドメインイベントは変わらない。`light` / `operation` に求めると、1 節で足りる設計が増える | | 当たった規則を止める項目を用語集の設定に持たない | 当たった規則はすべて落とす。プロジェクトが変えられるのは、どの文書を見るか(`check.paths`)と、どの表を語とみなすか(`check.term_sections`)と、正本に認める置き場(`check.source_paths`)である | | 設計の工程で足す語は `source` を空にし、`pending_source` に設計文書を書く | 設計文書は確定前で、承認の後も確定仕様になるまで書き換わり、確定の時点で消える。正本にすると、消えた文書を指す | | 語ごとにコードの識別子を用語集に持たせ、基本形(snake_case)を 1 つだけ持ち、ほかの書き方は機械で導く | 日本語のプロジェクトでは語と識別子が別に決まり、同じ概念が場所ごとに別の名前になる(承認ゲートが `gate` / `approval` / `checkpoint`)。書き方ごとに持つと、同じ事実を 4 か所に持ち、ずれる | @@ -144,9 +144,9 @@ stateDiagram-v2 | 要求の手順 3b | `standard` | 受け入れ条件を書く前に、下書きへ `check --file` を打ち、当たった語を採るか言い換えるかに分けて 0 件にする | | 要求の手順 7 | すべて | 要求の全文を課題の本文へ書く。`standard` では `spec-copy.py write` でコピーを作る | | 設計の手順 0 | `standard` / `legacy-refactor` | `gate --mode <モード>` が 0 以外なら作る手順を示して止まる | -| 設計の手順 2 | すべて | ドメインモデルの節(コンテキスト・集約・不変条件・ドメインイベント・用語)を最初に書く。コンテキストが 2 つ以上にまたがるなら、関係を 1 つ宣言する。書いた後に `check --file <設計文書>` を打ち、新しい語・意味の変わった語・廃止する語を同じ変更で用語集へ反映して `render` する | -| 設計の手順 4 | すべて | 7 つ目の突き合わせる対「不変条件 ↔ テスト設計」: 不変条件ごとにテスト設計の行がある | -| 設計の手順 5 | `standard` | `spec-copy.py check` が 0 であることを確かめてから設計 Pull Request を出す。1 なら `write` でコピーを作り直す(本文が正) | +| 設計の手順 3 | すべて | ドメインモデルの節(コンテキスト・集約・不変条件・ドメインイベント・用語)を最初に書く。コンテキストが 2 つ以上にまたがるなら、関係を 1 つ宣言する。書いた後に `check --file <設計文書>` を打ち、新しい語・意味の変わった語・廃止する語を同じ変更で用語集へ反映して `render` する | +| 設計の手順 5 | すべて | 7 つ目の突き合わせる対「不変条件 ↔ テスト設計」: 不変条件ごとにテスト設計の行がある | +| 設計の手順 6 | `standard` | `spec-copy.py check` が 0 であることを確かめてから設計 Pull Request を出す。1 なら `write` でコピーを作り直す(本文が正) | | テスト駆動の手順 1 | すべて | 設計文書に不変条件の表があれば、不変条件 1 つにつきテストを 1 つ、受け入れ条件のテストより先に書く。テストの名前か docstring に不変条件の番号を書く | | 振り返り | すべて | その変更の起点と終点で `diff` を打ち、「用語集の変化」の節にコンテキストごとに 1 行ずつ載せる。0 件なら節を書かない | | `backlog-refinement` | — | 起点(直近の本番のタグ)と終点(`HEAD`)で `diff` を打ち、完了報告の表の「用語集の変化」の行に、足された語の数・廃止された語の数と語の並びを載せる | @@ -233,7 +233,11 @@ stateDiagram-v2 | `design` | work | `/ndf:design #<課題>` | `pr` | | `pr` | pr | 設計 Pull Request を出す | `review` | | `review` | drive | `cross-review`(設計の既定 3 ラウンド) | `sync-review`(失敗は `ready`) | -| `sync-review` | run | `git fetch -q && git merge -q --ff-only '@{u}'`(レビューの直しに追いつく) | `glossary-check` | +| `sync-review` | run | `git fetch -q && git merge -q --ff-only '@{u}'`(レビューの直しに追いつく) | `requirements-recheck` | +| `requirements-recheck` | run | `spec-copy.py check <課題> <コピー>`(レビューの直しの後も、コピーが課題の本文と一致するか) | `survey-check`。0 以外は `on_fail` を置かずに止まる | +| `survey-check` | run | `design-survey.py check --issue <課題>`(自前で作る決定に、外部の調査で比べた候補があるか) | 0 → `glossary-check`。0 以外 → `fix-survey` | +| `fix-survey` | work | 指摘の決定の作り方・比べた候補を直すか、外部の調査の表へ出典つきの候補の行を足してコミットする(push しない)。比べずに直せない指摘は直さずに「判断が要る」と報告する | `survey-recheck` | +| `survey-recheck` | run | `survey-check` と同じ | 0 → `glossary-check`。0 以外は `on_fail` を置かずに止まる | | `glossary-check` | run | `glossary.py check --diff origin/ --root .` | 0 → `push-glossary`。1 → `fix-glossary` | | `fix-glossary` | work | 未登録の語は用語集へ足す(`source` を空にし `pending_source` に設計文書を書く)か言い換え、廃止した語は言い換える。用語集を変えたら `render` してコミットする(push しない)。利用者が採るかを決めるべき語は直さずに「判断が要る」と報告する | `glossary-recheck` | | `glossary-recheck` | run | `glossary-check` と同じ | 0 / 1 とも `push-glossary`(fast は 1 → `push-glossary-gate`) | diff --git a/docs/specifications/ndf-workflow-development-loop.md b/docs/specifications/ndf-workflow-development-loop.md index 1de7b05e5..31f97272c 100644 --- a/docs/specifications/ndf-workflow-development-loop.md +++ b/docs/specifications/ndf-workflow-development-loop.md @@ -7,7 +7,7 @@ 承認ゲート 1・2 は `mvv-gate.py check` の判定だけで通る - **人が手を止めるのはレッドラインと止まる条件のときだけである。** 承認ゲート 1 で「従う」以外になった課題は人の判断待ちへ 外して残りで続け、承認ゲート 2 で人へ戻したときだけ問う。予算・同じ失敗・着手できる課題が無い・MVV の外しか残らない・ - 利用者の止める指示では、問わずに理由と再開のコマンドを残して止まる + 利用者の止める指示では、問わずに理由と再開のコマンドを残して止まる。止める指示は、人が送った停止からだけ置かれる - **どの周回も後から追える。** 何を選び(外した課題と理由を含む)、承認ゲートでどう判定され、何の版を出し、いくら使い、 なぜ止まったかが、ループの記録 1 か所に残る - **`--loop` を付けない起動は今と同じに動く。** ループの記録を作らず、棚卸しの後に次のスプリントを始めない @@ -78,7 +78,7 @@ MVV のレッドラインで人へ戻すという要求がそれを決める。 | [`lib/sprint_loop_gate.py`](../../plugins/ndf/scripts/lib/sprint_loop_gate.py) | 承認ゲートで止まった done の扱い(`gate`。`lap --done` も同じに扱う)・続きの queue・まとめのステージ(`new close --pace auto`)の組み立て・周回の締め | | [`lib/sprint_loop_record.py`](../../plugins/ndf/scripts/lib/sprint_loop_record.py) | 記録の読み書き(排他・原子的な書き込み・終わった周回の書き換えの拒否)・消費・止まる条件の判定・再開のコマンド・引継ぎ文書・`laps.md` の描画。外の世界(gh・`upkeep.py rank`・`supervise.py`)は `Env` の関数を通して呼び、テストで差し替える | | [`lib/pace.py`](../../plugins/ndf/scripts/lib/pace.py) の `read_loop` | `loop` 節を読み、既定(`LOOP_DEFAULTS`)を埋めて形を確かめる。`read_pace` は `loop` 節を検証しない | -| [`supervise_lib/sprint.py`](../../plugins/ndf/scripts/supervise_lib/sprint.py)・[`new_args.py`](../../plugins/ndf/scripts/supervise_lib/new_args.py) | `new close --pace auto` でまとめのステージだけを組む。`--carry`・`--record-pr`、`new sprint --designed` | +| [`supervise_lib/sprint.py`](../../plugins/ndf/scripts/supervise_lib/sprint.py)・[`new_args.py`](../../plugins/ndf/scripts/supervise_lib/new_args.py) | `new close --pace auto` でまとめのステージだけを組む。`--carry`・`--record-pr`・`--loop`・`--lap`、`new sprint --designed` | | [`handoff.py`](../../plugins/ndf/scripts/handoff.py) | 名の形(`NAME_RE`)に `loop-<名>` を持つ | | `backlog-refinement` の [`upkeep_rank.py`](../../plugins/ndf/skills/backlog-refinement/scripts/upkeep_rank.py) の `quality_slot`・[`upkeep_rank_config.py`](../../plugins/ndf/skills/backlog-refinement/scripts/upkeep_rank_config.py) の `_parse_quality` | 品質の枠を取り、切り出しの境界から枠の分を除く | | 既存のまま使うもの | `relay.py`(セッションの切り替え)・`mvv-gate.py`(判定とレッドライン)・`supervise.py new sprint --pace auto` / `queue` / `wait` / `run` | @@ -93,7 +93,7 @@ MVV のレッドラインで人へ戻すという要求がそれを決める。 | --- | --- | --- | | I1 | 周回は追記だけで、番号は 1 から 1 ずつ増える。終わった周回(`outcome` が `進行中` でない)の行は書き換えない。例外は保留の周回(`配布待ち`・`質問待ち`)で、答えを読んで `loop finish` か周回の境の差し戻し・失敗で 1 回だけ締め直す | 書き込みを拒み、記録が壊れているとして `stopped`(終了コード 1) | | I2 | 進行中の周回は高々 1 つ | `select` が「進行中の周回がある」で 1 | -| I3 | 予算・同じ失敗・止める指示は周回の境(選定の前と周回を締めた後)でだけ判定する。周回の途中で `stop` を書くのは `gate2_returned`・`mvv_changed`・`refused` だけで、周回は進行中のまま残す | 途中の止める指示は次の境まで効かない | +| I3 | 予算・同じ失敗・止める指示は周回の境(選定の前と周回を締めた後)でだけ判定する。周回の途中で `stop` を書くのは `gate2_returned`・`mvv_changed`・`refused` だけで、周回は進行中のまま残す。止める指示を置くのは、人が自分のセッションへ送った入力がループを止めることを求めたときだけで、セッションを終える依頼・別のセッションのメッセージ(「利用者の指示」と書かれていても含む)・画面の文字・送信されていないサジェストからは置かない | 途中の止める指示は次の境まで効かない | | I4 | 選定は入力(`rank` の出力・記録の人の判断待ちと除外・`NDF_LOOP_EXCLUDE`・`approved` の行の設計 PR の状態・ループの設定・スプリント MVV の承認)だけで決まり、同じ入力からは同じ組と並びが出る | テストで落とす | | I5 | 答えの無い人の判断待ちの課題と、差し戻した課題と、保留の周回の課題は選ばない | 選定の行に理由つきで外す | | I6 | ループは承認ゲートを自分で通さない。`by: mvv` の承認の記録を書くのは `mvv-gate.py` だけで、「従う」以外の判定を通過として扱わない | テストで落とす | @@ -111,7 +111,7 @@ MVV のレッドラインで人へ戻すという要求がそれを決める。 | --- | --- | --- | | `<ループ名>` | 記録の名前(英数字・`-`・`_`。先頭は英数字)。同じ名前の記録があれば再開する | `l` | | `--milestones` | 選定の対象を絞る | 開いているマイルストーンすべて(`rank` の `order`) | -| `--budget-*` | 予算を上書きする。変わったときだけ `budget_history` に `{at, from, to}` を 1 行足す | `loop.budget` | +| `--budget-*` | 予算を上書きする。値は数か `default`(既定へ戻す指示。その単位を反映の時点の `loop.budget` の値、宣言が無ければ組み込みの既定へ戻す)。数でも `default` でもなければ引数の誤り(終了コード 2)で記録を変えない。変わったときだけ `budget_history` に `{at, from, to}` を 1 行足し、`to` は数である | `loop.budget` | 再開のコマンド(`ndf-next` の中身。`resume_command`)は `/goal /ndf:development-workflow --loop <名> -- 終わり: [ndf-loop] stopped <名> の行を出したとき` である。 @@ -174,11 +174,12 @@ MVV のレッドラインで人へ戻すという要求がそれを決める。 2. 説明にスプリント MVV(`## Mission` / `## Vision` / `## Value`)があり、その sha256 が `sprint_mvv_approvals` に無い マイルストーンは、候補をすべて「スプリント MVV が未承認」で外す 3. 候補ごとに外す理由を順に見る: 「人の判断待ち(棚卸し)」(`waiting_decision`)→「人の判断待ち」(答えの無い行)→ - 「差し戻し」(`rejected`)→「設計 PR がマージ待ち」(`approved` で `merged_at` が無く、`gh pr view` が `MERGED` でない。 - `MERGED` なら `merged_at` を足して以後打たない)→ この周回の `excluded` の理由 → + 「差し戻し」(`rejected`)→「設計 PR がマージ待ち(<マージの結果の理由>)」(`approved` で `merged_at` が無く、`gh pr view` が `MERGED` でない。 + `MERGED` なら `merged_at` を足して以後打たない。宛先が本番の設計 PR は「設計 PR の宛先が本番(承認ゲート 2 で人がマージする)」)→ この周回の `excluded` の理由 → 「周回の中で起票された」(周回の `created`)→「容量(尺度を超える)」 -4. 外れなかった候補に `max_issues` 件の枠を、品質の枠の課題から先に、残りを順位の順に割り当てる。選んだ行の理由は - 「品質の枠」か「順位」、枠からあふれた行は「容量」で外す +4. 外れなかった候補に `max_issues` 件の枠を、先頭に選ぶ課題(直して出し直す課題・配布の直し・承認とマージが済み、その後の周回でまだ + 選ばれていない設計の課題)、品質の枠の課題、順位の順に割り当てる。選んだ行の理由は「承認した設計」「直しの先頭」「品質の枠」「順位」の + どれか、枠からあふれた行は「容量」で外す 5. 選べたら、そのマイルストーンの候補でない行を「順位(切り出しの境界の外)」で外した行として足す。外した行も `selection` に載せ、`rank` の `metrics.digest` を `rank_digest` に書く。スプリント MVV があれば周回のディレクトリの `mvv.md` に写し、`loop sprint` が `sprint-state.py init --mvv` へ渡して承認ゲート `MVV` の記録を状態へ写す @@ -198,7 +199,7 @@ MVV のレッドラインで人へ戻すという要求がそれを決める。 | --- | --- | | `sprint-state.py init --name <名>- --pace auto --issue ... --dev V --prod V --goal <再開のコマンド> [--mvv mvv.md --milestone M]` | `--prod` を省けば開発版の接尾辞を外した版 | | `supervise.py new sprint --pace auto --issue <選んだ課題> --design <承認済みでない課題> [--designed ] ... [<-- の後の引数>]` | `loop answer --approved` で答えた課題は設計のプランを作らず、設計の結果のステージが設計 PR の設計の結果を読む | -| `supervise.py new close --pace auto --issue <選んだ課題> --state [--carry <前の周回の refine.deferred>] [--milestone M]` | まとめのステージ(確定仕様化 → Pull Request → 振り返り → 課題を閉じる → 棚卸し。#2228)だけを組む。最終の検査・開発版・本番は置かず、`fast` 節を読まない | +| `supervise.py new close --pace auto --issue <選んだ課題> --state --loop <名> --lap [--carry <前の周回の refine.deferred>] [--milestone M]` | まとめのステージ(確定仕様化 → Pull Request → 振り返り → 課題を閉じる → 棚卸し。#2228)だけを組む。最終の検査・開発版・本番は置かず、`fast` 節を読まない | `pipeline` は、最初のステージの queue の最後に `--then <まとめのプラン>` をつなぎ、`supervise.py wait ` → `loop lap <名> --done ` を続けた 1 行である。手で行うステージか別の queue のステージが残るときは、それを終えてから @@ -210,6 +211,7 @@ MVV のレッドラインで人へ戻すという要求がそれを決める。 | --- | --- | --- | | `--pace auto` | まとめのステージだけを組む(上の表) | 今の形(`fast` のスプリントの終わり)。出力は変わらない | | `--carry N...` | 棚卸しのステップの指示へ前の周回の持ち越しを `--add` で渡す | 指示は今のまま | +| `--loop <名>`・`--lap ` | 棚卸しの深さの記録のステップ(`depth record --depth diff`)へ開発ループの名前と周回の番号を渡す。開発ループのまとめの組み立て(`build_close`)は常に渡す | 深さの記録の行の `loop`・`lap` は空 | | `--record-pr N` | 課題を閉じるステップ(`sprint-close.py --record-pr`)へ本番の記録の Pull Request を渡す | 経路がすべて届けない経路なら `0`、それ以外は経路にかかわらず `{queue_record}` を残し、queue がまとめを流し始める時点に GitHub の参照からリリース記録の PR を探して埋める(#2230) | ループは `--record-pr` を渡さない。承認ゲート 2 の承認の後にまとめを組み直すときも渡さない(`stop.values.pr` は検査のスプリント PR で、記録の PR ではない。#2230)。 @@ -219,7 +221,7 @@ MVV のレッドラインで人へ戻すという要求がそれを決める。 | 止まった所 | 扱い | | --- | --- | | どこでも、プロジェクト MVV が開始時と違う・未承認 | `mvv_changed` で止まる | -| 設計のプラン(承認ゲート 1)。判定が「従う」以外・`boundary` が空でない・機械のチェック・判定を読めない | 計画の課題を `waiting` へ移し(`{number, reason, verdict, basis, gate: 1, pr, lap, at}`。`pr` は報告の設計 PR、判定は `mvv-gate.jsonl` のその PR の最後の行)、課題の本文の節 `## 人の判断待ち(開発ループ <名>)` を書き直す。設計が完了も関門もしなかった課題は続きから外す。残った課題(`issues_kept`)で、関門 1 の後のステージから外した課題だけの計画を除き、共通の計画の `課題` と `closes` からも外し、まとめを組み直して最後の `--then` につなぐ | +| 設計のプラン(承認ゲート 1)。判定が「従う」以外・`boundary` が空でない・機械のチェック・判定を読めない | 計画の課題を `waiting` へ移し(`{number, reason, verdict, basis, boundary, gate: 1, pr, lap, at, report, state, stamp}`。`pr` は報告の設計 PR、判定は `mvv-gate.jsonl` のその PR の最後の行、`stamp` は問いの時点の設計 PR の先頭のコミット(承認した先端))、課題の本文の節 `## 人の判断待ち(開発ループ <名>)` を書き直す。設計が完了も関門もしなかった課題は続きから外す。残った課題(`issues_kept`)で、関門 1 の後のステージから外した課題だけの計画を除き、共通の計画の `課題` と `closes` からも外し、まとめを組み直して最後の `--then` につなぐ | | 同上で残りが無い | 周回を `人へ戻した`(設計が完了しなかった課題があれば `失敗`)で締め、境の判定へ進む | | 同上で続きを組めない | `refused`(`step: 続きの queue`)で止まる | | 承認ゲート 1 以外(承認ゲート 2) | 判定の写しを周回へ足し、止まった計画を `gate2_plan` に控え、`gate2_returned`(`values: {pr, verdict, reasons}`)で止まる。本番は出さない | @@ -228,8 +230,13 @@ MVV のレッドラインで人へ戻すという要求がそれを決める。 承認ゲート 2 の答えは、止まった時刻より後の、周回の状態の `関門 2` の利用者の記録(`by` が `mvv` でない)と `gate-rejections.jsonl` の差し戻しのうち最新のもので決める。 -`loop answer --approved` は `sprint-state.py gate <その周回の状態> 関門 1 --by user --outcome approved [--pr N]` を内で打つ。 -`--rejected` は `--outcome rejected` で差し戻しの記録を残す。どちらも課題の本文の節を書き直す。 +`loop answer --approved` は、まず行の承認した先端(`stamp`)と設計 PR の今の先端を比べる。違う・行に承認した先端が無いときは、答えを当てずに +`stamp` を今の先端へ書き換え、`status: regated` で返す(新しい先端を見てから答え直す)。同じなら +`sprint-state.py gate <その周回の状態> 関門 1 --by user --outcome approved [--pr N]` を内で打ち、続けて承認した設計のマージ +(`design_pr_merge.merge_approved`。承認した先端のままで必須の検査が緑の設計 PR を、検査の完了を待たずに `--no-cleanup` で宛先へマージする。 +宛先が本番チャネルに当たるか決められない設計 PR はマージしない)を 1 回試み、結果を行の `merge` に、マージできたら `merged_at` を書く。 +終了コードは、マージできたときもできなかったときも 0 で、`summary` に結果を添える。`--rejected` は `--outcome rejected` で差し戻しの記録を残し、マージを呼ばない。 +どちらも課題の本文の節を書き直す。 ### 止まる条件と再開 @@ -247,6 +254,11 @@ MVV のレッドラインで人へ戻すという要求がそれを決める。 やらないなら差し戻し)→ `配布待ち` の `rejected`(`人へ戻した` で締め、`fixes` に課題と理由を足し、次の周回で先頭に選ぶ)・`failed` (`失敗` で締め、鍵は `{issues, kind: 配布, step: 配布の失敗}`)→ 最も古い `released` の `配布待ち` か答えの届いた `質問待ち` (`status: finish` と残りの `pipeline` を返し、新しい周回を始めない)。次の配布へ回した知らせ(`defer-<依頼>`)だけなら `配布待ち` のまま。 +続けて、承認済みで未マージの設計 PR ごとに、前のマージの結果に応じた手を 1 回だけ打つ(未試行・検査が緑でない・取り残し・基盤待ちは承認した設計のマージを呼び直す。 +先端が動いた・承認した先端が無いは新しい先端で問い直す。衝突は設計の worktree で `merged-steps.py base-sync --pr --push` を打ち、 +先端が進めば問い直す。権限の不足など時間で解けない結果・同じ分類の未緑が 3 周回続いた・基盤待ちの再実行が同じ承認した先端で 3 回に達した +ときは、承認した先端のまま理由を添えて利用者へ問い直す。宛先が本番の設計 PR はマージせず、`gh pr view` の状態だけを読む)。 +マージ済みになった設計 PR には `merged-steps.py cleanup ` を打つ。この手は保留の周回が無いときも選定の前に打つ(`settle_approved_designs`)。 その後、答えの無い保留の周回が `release_pending_max` 以上あれば `waiting_only`、3・4 に当たって答えの無い保留の周回があれば 先に `waiting_only` で止まる。選べる課題が無く、保留の周回かオーガナイザーへ送った答えの無い設計の承認があれば、`no_issue` / `outside_mvv` と見直しの代わりに `waiting_only` で止まる(`values: {why, pending, waiting}`)。 @@ -284,7 +296,7 @@ MVV のレッドラインで人へ戻すという要求がそれを決める。 | `.ndf/loop/<名>/loop.json` | 正本(下の表) | | `.ndf/loop/<名>/laps.md` | 人が読む描画(周回の一覧・周回ごとの詳細・人の判断待ち・止まった理由)。記録を書くたびに作り直す | | `.ndf/loop/<名>/lap-/` | 周回のスプリント状態・プランの出力・done・`mvv.md` | -| `.ndf/loop/<名>/stop` | 止める指示(`loop stop` が置き、`user_stop` の再開で消す) | +| `.ndf/loop/<名>/stop` | 止める指示(人が送った停止を受けて `loop stop` が置き、`user_stop` の再開で消す) | | `.ndf/handoff/loop-<名>.md` | 引継ぎ文書。「現在地」「次にやること」「次に実行するコマンド」だけを置き換える | `loop.json`(`version: 1`): @@ -297,7 +309,9 @@ MVV のレッドラインで人へ戻すという要求がそれを決める。 | `same_failure` / `max_issues` / `release_pending_max` / `milestones` | 開始時に設定と引数から写した値 | | `status` / `stop` / `stops` | `running` / `stopped`・今の止まった理由・過去の止まった理由(追記だけ) | | `laps[]` | 周回(追記だけ。I1) | -| `waiting[]` | 人の判断待ち。答えは行を消さずに `answer`(`approved` / `rejected` / `revise`)・`answered_at` を足す。`approved` の行はマージを確かめた時刻 `merged_at` を足す。並列の計画のレーンは、オーガナイザーへ送った設計の承認の依頼の `request` と計画名 `plan` を持つ。`revise` の行は指摘 `note` と回数 `revise_count` を持つ | +| `waiting[]` | 人の判断待ち。答えは行を消さずに `answer`(`approved` / `rejected` / `revise`)・`answered_at` を足す。`approved` の行はマージを確かめた時刻 `merged_at`、だれが承認したか `by`(`user` / `mvv`)、最後の承認した設計のマージの結果 `merge` +(`{result, class, reason, pr, head, at, branch, infra_rerun, lap, streak, infra_reruns, stamp}`)、後片付けの時刻 `cleaned` を足す。 +利用者へ問い直した行は理由 `returned` を持つ(問い直すときは `answer`・`answered_at`・`by`・`merge`・`merged_at` を外す。`infra_rerun` は 1 回のマージで実際に打った再実行の回数、`infra_reruns` はその累計)。マージできずに利用者へ戻した回数 `givebacks` は消さず、レーンの依頼の id の接尾辞 `-m<周回>-<回数>` に使う。並列の計画のレーンは、オーガナイザーへ送った設計の承認の依頼の `request` と計画名 `plan` を持つ。`revise` の行は指摘 `note` と回数 `revise_count` を持つ | | `fixes[]` | 配布を差し戻された周回の直し `{lap, issues, reason, at}`。直しの周回で課題を選ぶと `fixed_by` に周回の番号を書く | | `excluded[]` | `{number, reason, lap}`(`loop exclude`) | | `sprint_mvv_approvals[]` | `{milestone, sha256, at, by}` | diff --git a/docs/specifications/quality-gates-design-match.md b/docs/specifications/quality-gates-design-match.md index 325563729..945a97a99 100644 --- a/docs/specifications/quality-gates-design-match.md +++ b/docs/specifications/quality-gates-design-match.md @@ -82,6 +82,7 @@ LLM を呼ばずに出し、(c) 設計の決定と実装の一致と、(b) で | 要素 | 責務 | | --- | --- | | `plugins/ndf/scripts/design-match.py` | `tests`(確認 (a))と `specs`(確認 (b))。LLM を呼ばず、どの文書も書き換えず、結果を 1 行の JSON(`lib/step_result.py` の形)で返す | +| `plugins/ndf/scripts/lib/design_docs.py` | 設計文書の名前の規則と、課題の番号から HEAD の設計文書を読む処理。`design-match.py` と `design-survey.py`(設計の外部の調査の検査)が読む | | `plugins/ndf/scripts/lib/repo.py`(`SPEC_DIR`) | 確定仕様の置き場の定数。`plan-to-spec-steps.py` の索引のパスと `design-match.py specs` の対象が読む | | `plugins/ndf/scripts/supervise_lib/procedures.py`(`DESIGN_MATCH_MODES`・`design_match_steps`・`design_diff_on_pr`) | 突き合わせの 3 ステップと、`pr` のステップへ持たせる `diff_section`(節のファイル)・`on_section_missing`(節が無いときの行き先) | | `plugins/ndf/scripts/supervise_lib/templates.py`(`plan_to_merge`) | `standard` で課題があるとき、`test-limited` の次と judge の「先へ進む」選択肢を `design-tests` へ向ける | diff --git a/docs/versioning-and-distribution.md b/docs/versioning-and-distribution.md index be1180be4..c17794216 100644 --- a/docs/versioning-and-distribution.md +++ b/docs/versioning-and-distribution.md @@ -57,11 +57,11 @@ semver の順序で除外されるのは、プラグイン間の依存解決(` | 版 | 形 | 意味 | | --- | --- | --- | -| 正式版 | `10.27.21` | 利用者が常用してよい | +| 正式版 | `10.27.24` | 利用者が常用してよい | | 開発版 | `10.28.0-dev.1` | 検証中。入れたくない利用者は取得を控えられる | | 公開前の確認版 | `10.28.0-rc.1` | 正式版の候補。残るのは確認だけ | -- 接尾辞は**次に出す正式版の版数へ付ける**。`10.27.21` の次を開発するなら `10.28.0-dev.1` +- 接尾辞は**次に出す正式版の版数へ付ける**。`10.27.24` の次を開発するなら `10.28.0-dev.1` - 連番は開発版を出すたびに増やす。**同じ版数で中身を差し替えない**。差し替えると、利用者の 手元にある版と `main` の版が同じ番号で別物になり、何を確かめたのかが分からなくなる - **正式版を出すときは接尾辞を外す。** `10.28.0-dev.3` の次は `10.28.0` diff --git a/factory/api/src/runs/control.ts b/factory/api/src/runs/control.ts index 5efedf06b..46737c96f 100644 --- a/factory/api/src/runs/control.ts +++ b/factory/api/src/runs/control.ts @@ -30,7 +30,7 @@ import { } from "./model.ts"; import type { RunnerPlatform } from "./platform.ts"; import type { RunsTable } from "./table.ts"; -import { isLegacyVersion } from "./version.ts"; +import { isLegacyVersion, supportsBudgetReset } from "./version.ts"; import { claimOf, type IdempotencyRef } from "../idempotency.ts"; export const DEFAULT_TIMEOUT_SECONDS = 28_800; @@ -48,6 +48,8 @@ export interface RunControlConfig { ingestUrl: string; // 並行を許す NDF の版の下限(#2347 の決定 12)。無ければ並行を許さない parallelMinVersion?: string | null; + // 予算の単位を既定へ戻せる(default を受ける)NDF の版の下限(#2406 の決定 4)。無ければ戻せない + budgetResetMinVersion?: string | null; // local のループも自動で次の周回を起こす(I52)。既定 false localLoopAutostart?: boolean; usageLimitRetrySeconds?: number; @@ -74,6 +76,7 @@ export function runControlConfig(env: Record): RunCo tokenTtlSeconds, ingestUrl, parallelMinVersion: env.NDF_PARALLEL_MIN_VERSION || null, + budgetResetMinVersion: env.NDF_BUDGET_RESET_MIN_VERSION || null, localLoopAutostart: env.LOCAL_LOOP_AUTOSTART === "true", usageLimitRetrySeconds: num("USAGE_LIMIT_RETRY_SECONDS", DEFAULT_USAGE_LIMIT_RETRY_SECONDS), }; @@ -152,6 +155,22 @@ function runEndOf( return { state, failure: job.stopReason ?? (finished.source === "control" ? reason : "job_failed") }; } +// 実行の終わりの状態を決めるのに、finalize が読み集めたもの +interface RunEndContext { + finished: JobFinishedEvent; + reason: FailureReason; + finishReason: FinishReason | null; + rawReason: JobFinishedEvent["payload"]["reason"] | null; + now: string; + releaseEvents: ReleaseEvents | null; + requests: OrgRequest[]; + lastRequest: string | null; + keepRelease: boolean; +} + +// 実行を終わりへ移し、次の周回と待っているループを起こす必要があるときのループの書き込み。起こさないときは null +type RunMoved = { loopWrite: LoopWrite | undefined } | null; + export class RunControl { private readonly now: () => Date; private readonly newId: () => string; @@ -461,8 +480,8 @@ export class RunControl { instruction.LOOP_NAME = loop.name; if (job.start === "loop") { instruction.LOOP_MILESTONES = loop.milestones.join(","); - const budget = (["laps", "usd", "hours"] as const).filter((k) => loop.budget[k] !== null).map((k) => `${k}=${loop.budget[k]}`); - if (budget.length > 0) instruction.LOOP_BUDGET = budget.join(","); + const budget = loopBudgetArg(loop.budget, supportsBudgetReset(run.ndfVersion, this.deps.config.budgetResetMinVersion)); + if (budget) instruction.LOOP_BUDGET = budget; } if (loop.exclude.length > 0) instruction.EXCLUDE_ISSUES = loop.exclude.join(","); if (loop.answers.length > 0) instruction.LOOP_ANSWERS = loop.answers.map((a) => `${a.number}=${a.decision}${a.pr ? `#${a.pr}` : ""}`).join(","); @@ -470,6 +489,13 @@ export class RunControl { return instruction; } + // ループの次のジョブが使う NDF の版が予算を既定へ戻せるか(#2406 の決定 5)。今の実行があればその版、無ければ今のマーケットプレイスの版 + async budgetResetSupportedFor(loop: DevLoop): Promise<{ supported: boolean; ndfVersion: string }> { + const run = loop.currentRunId ? await this.deps.table.getRun(loop.currentRunId) : null; + const ndfVersion = run ? run.ndfVersion : await this.deps.ndfVersion(); + return { supported: supportsBudgetReset(ndfVersion, this.deps.config.budgetResetMinVersion), ndfVersion }; + } + // ジョブの token を発行する。期限は打ち切りの時間より長い(I41) private async issueToken(run: SprintRun, job: RunJob): Promise { const expiresAt = new Date(this.now().getTime() + this.deps.config.tokenTtlSeconds * 1000); @@ -587,37 +613,8 @@ export class RunControl { // main へマージした後の配布の実行は取り消しを受けても終わらせず、止まった配布として「続きから流す」で仕上げられるようにする(I63) const keepRelease = run.cancelRequested && isReleaseRun(run) && this.organizer !== null && (await this.organizer.releaseMerged(run)); if (run.currentJobId === job.jobId) { - if (run.cancelRequested && !keepRelease) { - const lw = await this.loopWriteFor(run, job.jobId, { runState: "cancelled", failureReason: null, finishReason }); - if (await table.finishRun(run.sprintRunId, "cancelled", null, now, { finishReason, loop: lw?.write, requests })) await this.afterRunMoved(run, lw?.write, true); - } else if (isReleaseRun(run)) { - // 配布の実行は、配布済みで done、承認ゲートか判断で承認待ち、それ以外は止まった配布として承認待ち(I65・決定 11) - if (releaseEvents?.settled?.result === "released") { - await table.finishRun(run.sprintRunId, "done", null, now); - } else if (result === "gate" && !keepRelease) { - await table.awaitApproval(run.sprintRunId, job.jobId, now); - } else { - // 取り消しを受けた実行は取り消しの印を外す(続けたジョブの終わりで取り消しにしない) - await table.awaitApproval(run.sprintRunId, job.jobId, now, { awaitingReason: "release_stalled", ...(keepRelease ? { clearCancel: true } : {}) }); - } - } else if (result === "gate") { - const lw = await this.loopWriteFor(run, job.jobId, { runState: "awaiting_approval", failureReason: null, finishReason }); - const waitsRelease = rawReason === "release_requested" && !run.loopId && lastRequest !== null; - await table.awaitApproval(run.sprintRunId, job.jobId, now, { - loop: lw?.write, - requests, - ...(waitsRelease ? { awaitingReason: "release_requested" as const, releaseRequestId: lastRequest } : {}), - }); - } else if (result === "stopped" && finishReason === "usage_limit" && run.executionMode === "local") { - // local の利用上限はリセット時刻の後に同じ周回を続ける(I54。ほかの形態は failed として扱う) - const resumeAt = this.resumeAtOf(finished); - const lw = await this.loopWriteFor(run, job.jobId, { runState: "awaiting_reset", failureReason: null, finishReason }); - await table.awaitReset(run.sprintRunId, job.jobId, resumeAt, now, { finishReason, loop: lw?.write }); - } else { - const { state, failure } = runEndOf(result, finishReason, job, finished, reason); - const lw = await this.loopWriteFor(run, job.jobId, { runState: state, failureReason: failure, finishReason }); - if (await table.finishRun(run.sprintRunId, state, failure, now, { finishReason, loop: lw?.write, requests })) await this.afterRunMoved(run, lw?.write, true); - } + const moved = await this.resolveRunEnd(run, job, { finished, reason, finishReason, rawReason, now, releaseEvents, requests, lastRequest, keepRelease }); + if (moved) await this.afterRunMoved(run, moved.loopWrite, true); } // 依頼を受けたか配布の実行が動いたら、配布のまとめを進める。落ちてよい(見回りが続ける) if (this.organizer && (isReleaseRun(run) || requests.length > 0)) { @@ -634,6 +631,61 @@ export class RunControl { } } + // 実行の終わりの状態を決めて書く。取り消し・配布の実行・それ以外(ジョブの結果)の順に分ける + private async resolveRunEnd(run: SprintRun, job: RunJob, ctx: RunEndContext): Promise { + const { finishReason, now, requests } = ctx; + if (run.cancelRequested && !ctx.keepRelease) { + const lw = await this.loopWriteFor(run, job.jobId, { runState: "cancelled", failureReason: null, finishReason }); + return (await this.deps.table.finishRun(run.sprintRunId, "cancelled", null, now, { finishReason, loop: lw?.write, requests })) ? { loopWrite: lw?.write } : null; + } + if (isReleaseRun(run)) { + await this.finalizeReleaseRun(run, job, ctx); + return null; + } + return this.finalizeJobResult(run, job, ctx); + } + + // 配布の実行は、配布済みで done、承認ゲートか判断で承認待ち、それ以外は止まった配布として承認待ち(I65・決定 11) + private async finalizeReleaseRun(run: SprintRun, job: RunJob, ctx: RunEndContext): Promise { + const { table } = this.deps; + const { now, keepRelease } = ctx; + if (ctx.releaseEvents?.settled?.result === "released") { + await table.finishRun(run.sprintRunId, "done", null, now); + } else if (ctx.finished.payload.result === "gate" && !keepRelease) { + await table.awaitApproval(run.sprintRunId, job.jobId, now); + } else { + // 取り消しを受けた実行は取り消しの印を外す(続けたジョブの終わりで取り消しにしない) + await table.awaitApproval(run.sprintRunId, job.jobId, now, { awaitingReason: "release_stalled", ...(keepRelease ? { clearCancel: true } : {}) }); + } + } + + // ループの実行と手動の実行の、ジョブの結果に従う終わり(gate・local の利用上限・それ以外) + private async finalizeJobResult(run: SprintRun, job: RunJob, ctx: RunEndContext): Promise { + const { table } = this.deps; + const { finished, finishReason, now, requests, lastRequest } = ctx; + const result = finished.payload.result; + if (result === "gate") { + const lw = await this.loopWriteFor(run, job.jobId, { runState: "awaiting_approval", failureReason: null, finishReason }); + const waitsRelease = ctx.rawReason === "release_requested" && !run.loopId && lastRequest !== null; + await table.awaitApproval(run.sprintRunId, job.jobId, now, { + loop: lw?.write, + requests, + ...(waitsRelease ? { awaitingReason: "release_requested" as const, releaseRequestId: lastRequest } : {}), + }); + return null; + } + if (result === "stopped" && finishReason === "usage_limit" && run.executionMode === "local") { + // local の利用上限はリセット時刻の後に同じ周回を続ける(I54。ほかの形態は failed として扱う) + const resumeAt = this.resumeAtOf(finished); + const lw = await this.loopWriteFor(run, job.jobId, { runState: "awaiting_reset", failureReason: null, finishReason }); + await table.awaitReset(run.sprintRunId, job.jobId, resumeAt, now, { finishReason, loop: lw?.write }); + return null; + } + const { state, failure } = runEndOf(result, finishReason, job, finished, ctx.reason); + const lw = await this.loopWriteFor(run, job.jobId, { runState: state, failureReason: failure, finishReason }); + return (await table.finishRun(run.sprintRunId, state, failure, now, { finishReason, loop: lw?.write, requests })) ? { loopWrite: lw?.write } : null; + } + // job.finished を読む。無ければ control で failed を書き、書けなければ(先に書かれていれば)読み直す private async ensureFinished(run: SprintRun, job: RunJob, reason: FailureReason): Promise { const { reader } = this.deps; @@ -865,6 +917,14 @@ export class RunControl { } } +// 指示の LOOP_BUDGET。既定へ戻せる版では空の単位を default(リポジトリの既定へ戻す)で並べ、戻せない版では値のある単位だけを並べる +export function loopBudgetArg(budget: LoopJobInput["budget"], reset: boolean): string | null { + const parts = (["laps", "usd", "hours"] as const).flatMap((k) => + budget[k] !== null ? [`${k}=${budget[k]}`] : reset ? [`${k}=default`] : [], + ); + return parts.length > 0 ? parts.join(",") : null; +} + // ループの実行のジョブの指示へ載せる値(予算・答えは周回を起こす時点の値)。答えは今の人の判断待ちに残る番号だけ export function loopInput(loop: DevLoop, exclude: number[]): LoopJobInput { const open = new Set(loop.pending.map((p) => p.number)); diff --git a/factory/api/src/runs/loops.test.ts b/factory/api/src/runs/loops.test.ts index 7dbf744da..246d010db 100644 --- a/factory/api/src/runs/loops.test.ts +++ b/factory/api/src/runs/loops.test.ts @@ -10,7 +10,7 @@ import { accountContext, createRunsHarness, TEST_CONFIG, type RunsHarness } from import { backfill } from "./backfill.ts"; import type { RunControlConfig } from "./control.ts"; import { LoopControl, loopStateAfter, type RunOutcome } from "./loops.ts"; -import { compareVersions, isLegacyVersion } from "./version.ts"; +import { compareVersions, isLegacyVersion, supportsBudgetReset } from "./version.ts"; const REPO = 101; const OTHER_REPO = 102; @@ -144,6 +144,15 @@ describe("版の比べ方(決定 12)", () => { expect(isLegacyVersion("10.27.0", null)).toBe(true); expect(isLegacyVersion("10.27.0", "10.27.0-dev.1")).toBe(false); }); + + it("予算を既定へ戻せる版は下限と同じか新しい版。開発版の接尾辞の付いた同じ版・下限が無い・読めない版は戻せない(#2406)", () => { + expect(supportsBudgetReset("10.27.0", "10.27.0")).toBe(true); + expect(supportsBudgetReset("10.27.1@abc", "10.27.0")).toBe(true); + expect(supportsBudgetReset("10.26.9", "10.27.0")).toBe(false); + expect(supportsBudgetReset("10.27.0-dev.1", "10.27.0")).toBe(false); + expect(supportsBudgetReset("10.27.0", null)).toBe(false); + expect(supportsBudgetReset("unknown", "10.27.0")).toBe(false); + }); }); describe("開発ループを始め・止め・予算を変える(受け入れ条件 1・11)", () => { @@ -173,6 +182,40 @@ describe("開発ループを始め・止め・予算を変える(受け入れ expect(row).toMatchObject({ name: "lfactory2", laps_closed: 1, cost_usd: 2, budget_remaining: { laps: 3, usd: 48, hours: null } }); }); + it("既定へ戻せる版では、空にした単位を次の周回の指示へ default として載せる(#2406)", async () => { + await setup({ budgetResetMinVersion: "10.26.1" }); + const { loop_id } = await startLoop("lfactory2", ["25 Factory"], accountContext(), { usd: 10 }); + expect(instructionOf((await runOf((await loopOf(loop_id)).currentRunId!)).currentJobId!).LOOP_BUDGET).toBe("laps=default,usd=10,hours=default"); + await loops.updateBudget(accountContext(), loop_id, { budget: { usd: null } }); + await finishEcs((await loopOf(loop_id)).currentRunId!, { result: "done", summary: "" }, [["loop.lap_closed", lap(1)]]); + const ins = instructionOf((await runOf((await loopOf(loop_id)).currentRunId!)).currentJobId!); + expect(ins.LOOP_BUDGET).toBe("laps=default,usd=default,hours=default"); + expect(validateRunJobInstruction(ins).ok).toBe(true); + }); + + it("既定へ戻せない版では、値のある単位を空にする変更を 409 で断り、値の変更と空のままの単位は受ける(#2406)", async () => { + await setup({ budgetResetMinVersion: "10.27.0" }); + const { loop_id } = await startLoop("lfactory2", ["25 Factory"], accountContext(), { usd: 10 }); + expect(instructionOf((await runOf((await loopOf(loop_id)).currentRunId!)).currentJobId!).LOOP_BUDGET).toBe("usd=10"); + const cleared = loops.updateBudget(accountContext(), loop_id, { budget: { usd: null } }); + expect(await detailsOf(cleared)).toEqual({ ndf_version: "10.26.1", units: ["usd"] }); + expect(await codeOf(loops.updateBudget(accountContext(), loop_id, { budget: {} }))).toBe("budget_reset_unsupported"); + expect((await loopOf(loop_id)).budget).toEqual({ laps: null, usd: 10, hours: null }); + expect(await codeOf(loops.updateBudget(accountContext(), loop_id, { budget: { usd: 20 } }))).toBe("ok"); + expect((await loopOf(loop_id)).budget).toEqual({ laps: null, usd: 20, hours: null }); + }); + + it("判定に使う版は今の実行の版で、今の実行が無ければマーケットプレイスの版。下限が無ければ戻せない(#2406)", async () => { + await setup({ budgetResetMinVersion: "10.27.0" }); + const { loop_id } = await startLoop("lfactory2", ["25 Factory"], accountContext(), { usd: 10 }); + const loop = await loopOf(loop_id); + h.table.runs.get(loop.currentRunId!)!.ndfVersion = "10.27.0"; + expect(await h.control.budgetResetSupportedFor(loop)).toEqual({ supported: true, ndfVersion: "10.27.0" }); + expect(await h.control.budgetResetSupportedFor({ ...loop, currentRunId: null })).toEqual({ supported: false, ndfVersion: "10.26.1" }); + await setup(); + expect((await h.control.budgetResetSupportedFor({ ...loop, currentRunId: null })).supported).toBe(false); + }); + it("形の違う名前・マイルストーン・予算は 400、同じ名前の終わっていないループは 409", async () => { expect(await codeOf(startLoop("-bad", ["25 Factory"]))).toBe("invalid_request"); expect(await codeOf(startLoop("l1", []))).toBe("invalid_request"); @@ -608,3 +651,86 @@ describe("選定の規則を持たない(受け入れ条件 1)", () => { } }); }); + +// 現状固定テスト(cross-refactoring の I-001。RunControl.finalize の分岐ごとの今の振る舞いを固定する) +describe("タスクが止まった後の処理(RunControl.finalize の現状)", () => { + beforeEach(() => setup()); + + const jobOf = async (jobId: string) => (await h.table.getJob(jobId))!; + + it("job.finished の無いまま止まると、control が failed を書き、実行は task_stopped で failed、ジョブは failed で token を消す", async () => { + const run = await manual([4]); + const jobId = (await runOf(run.sprint_run_id)).currentJobId!; + await h.control.onTaskStateChange(jobId, "RUNNING"); + const arn = (await jobOf(jobId)).tokenSecretArn!; + expect(h.platform.tokens.has(arn)).toBe(true); + await h.control.onTaskStateChange(jobId, "STOPPED"); + expect(await runOf(run.sprint_run_id)).toMatchObject({ state: "failed", failureReason: "task_stopped" }); + expect(await jobOf(jobId)).toMatchObject({ state: "failed", result: "failed", finishedAt: h.clock.now.toISOString() }); + expect(h.platform.tokens.has(arn)).toBe(false); + const finished = await memoryProgressReader(h.progress).findJobFinished(run.sprint_run_id, jobId); + expect(finished).toMatchObject({ source: "control", payload: { result: "failed", summary: "実行の制御が終わりを書いた(task_stopped)" } }); + }); + + it("ランナーが failed を書いたジョブは job_failed で failed になる", async () => { + const run = await manual([4]); + await finishEcs(run.sprint_run_id, { result: "failed", summary: "" }); + expect(await runOf(run.sprint_run_id)).toMatchObject({ state: "failed", failureReason: "job_failed" }); + }); + + it("done で終わると実行は done、枠を返し、2 度目の停止の知らせでは何も変えない", async () => { + const run = await manual([4]); + const jobId = await finishEcs(run.sprint_run_id, { result: "done", summary: "" }); + expect(await runOf(run.sprint_run_id)).toMatchObject({ state: "done", failureReason: null }); + expect(await jobOf(jobId)).toMatchObject({ state: "done", result: "done" }); + expect((await h.table.getSlots("acc-1", REPO)).account).toBe(0); + const before = structuredClone(await runOf(run.sprint_run_id)); + await h.control.onTaskStateChange(jobId, "STOPPED"); + expect(await runOf(run.sprint_run_id)).toEqual(before); + }); + + it("起動に失敗したジョブは launch_failed で failed になる", async () => { + h.platform.failRunTask = true; + const run = await manual([4]); + expect(await runOf(run.sprint_run_id)).toMatchObject({ state: "failed", failureReason: "launch_failed" }); + }); + + it("打ち切りの時間を過ぎた local のジョブは見回りが timeout で終える", async () => { + await manual([4], REPO, accountContext({ executionMode: "local" })); + const ins = (await h.control.claimLocalRunJob("acc-1"))!.instruction; + h.clock.now = new Date(h.clock.now.getTime() + (TEST_CONFIG.timeoutSeconds + 1) * 1000); + await h.control.sweep(); + expect(await runOf(ins.SPRINT_RUN_ID)).toMatchObject({ state: "failed", failureReason: "timeout" }); + expect((await jobOf(ins.JOB_ID)).state).toBe("failed"); + }); + + it("手動の実行の gate は承認待ちになり、配布の依頼が無ければ release_requested の理由でも依頼を待たない", async () => { + const run = await manual([4]); + await finishEcs(run.sprint_run_id, { result: "gate", summary: "", reason: "release_requested" }); + const r = await runOf(run.sprint_run_id); + expect(r.state).toBe("awaiting_approval"); + expect(r.awaitingReason ?? null).toBeNull(); + expect(r.releaseRequestId ?? null).toBeNull(); + }); + + it("ループの周回の gate は release_requested の理由を終わりの理由へ写さず、ループも承認待ちになる", async () => { + const { loop_id } = await startLoop("l1", ["25 Factory"]); + const runId = (await loopOf(loop_id)).currentRunId!; + await finishEcs(runId, { result: "gate", summary: "", reason: "release_requested" }); + const r = await runOf(runId); + expect(r.state).toBe("awaiting_approval"); + expect(r.finishReason ?? null).toBeNull(); + expect(r.awaitingReason ?? null).toBeNull(); + expect((await loopOf(loop_id)).state).toBe("awaiting_approval"); + }); + + it("ループの周回が done で終わると、次の周回が起きる", async () => { + const { loop_id } = await startLoop("l1", ["25 Factory"]); + const runId = (await loopOf(loop_id)).currentRunId!; + await finishEcs(runId, { result: "done", summary: "" }, [["loop.lap_closed", lap(1)]]); + expect((await runOf(runId)).state).toBe("done"); + const loop = await loopOf(loop_id); + expect(loop).toMatchObject({ state: "running", lapRuns: 2 }); + expect(loop.currentRunId).not.toBe(runId); + }); +}); diff --git a/factory/api/src/runs/loops.ts b/factory/api/src/runs/loops.ts index fa1cc337d..0986edc86 100644 --- a/factory/api/src/runs/loops.ts +++ b/factory/api/src/runs/loops.ts @@ -438,6 +438,18 @@ export class LoopControl { const budget = parseBudget(body.budget ?? {}); const loop = await this.loopOf(ctx, loopId); if (isTerminalLoop(loop.state)) throw new ConflictError("already_finished", "このループは終わっています"); + // 値のある単位を空にする変更は、次のジョブの NDF が既定へ戻せる版のときだけ受ける(#2406 の I7) + const units = (["laps", "usd", "hours"] as const).filter((k) => loop.budget[k] !== null && budget[k] === null); + if (units.length > 0) { + const { supported, ndfVersion } = await this.deps.control.budgetResetSupportedFor(loop); + if (!supported) { + throw new ConflictError( + "budget_reset_unsupported", + `このループの次のジョブが使う NDF(版 ${ndfVersion})は予算をリポジトリの既定へ戻せません。値を入れるか、次の周回が新しい版で始まってから空にしてください`, + { ndf_version: ndfVersion, units }, + ); + } + } const ok = await this.deps.table.writeLoop({ accountId: loop.accountId, loopId: loop.loopId, diff --git a/factory/api/src/runs/version.ts b/factory/api/src/runs/version.ts index b33ac73e7..9c3610d07 100644 --- a/factory/api/src/runs/version.ts +++ b/factory/api/src/runs/version.ts @@ -42,3 +42,10 @@ export function isLegacyVersion(version: string, minVersion: string | null | und const c = compareVersions(version, minVersion); return c === null || c < 0; } + +// 予算の単位を既定へ戻す指示(default)を NDF が受ける版か(#2406 の決定 4)。下限が無い・読めない版は戻せない側に倒す +export function supportsBudgetReset(version: string, minVersion: string | null | undefined): boolean { + if (!minVersion) return false; + const c = compareVersions(version, minVersion); + return c !== null && c >= 0; +} diff --git a/factory/contracts/openapi/loops.yaml b/factory/contracts/openapi/loops.yaml index 9c6e6760c..280bbb847 100644 --- a/factory/contracts/openapi/loops.yaml +++ b/factory/contracts/openapi/loops.yaml @@ -64,7 +64,10 @@ paths: tags: [loops] operationId: updateLoopBudget summary: 開発ループの予算を変える - description: 終わりの状態でないループにだけ効き、次のジョブの指示から NDF の予算へ渡る。 + description: >- + 終わりの状態でないループにだけ効き、次のジョブの指示から NDF の予算へ渡る。3 つの単位をまとめて置き換え、送らない単位と null の単位は空(リポジトリの既定)として扱う。 + 値のある単位を空にする変更は、次のジョブが使う NDF の版(ループの今の実行の版、無ければ今のマーケットプレイスの版)が予算を既定へ戻せないとき + 409 budget_reset_unsupported(details.ndf_version・details.units)で断る。値を入れる・変える・空のままにする変更は版によらず受ける。 x-ndf-access: write x-ndf-keys: [loop_id] parameters: [{ name: accountId, in: path, required: true, description: アカウントの ID, schema: { type: string } }] @@ -257,6 +260,7 @@ components: LoopName: { type: string, pattern: "^[0-9A-Za-z][0-9A-Za-z_-]{0,63}$" } Budget: type: object + description: 単位ごとに数か null。null の単位はリポジトリの予算の既定(.ndf/pace.json の loop.budget)に従う additionalProperties: false properties: laps: { type: [number, "null"], exclusiveMinimum: 0 } diff --git a/factory/contracts/schemas/run-job-instruction.schema.json b/factory/contracts/schemas/run-job-instruction.schema.json index 435eddaf0..0b3891e59 100644 --- a/factory/contracts/schemas/run-job-instruction.schema.json +++ b/factory/contracts/schemas/run-job-instruction.schema.json @@ -25,7 +25,7 @@ "APPROVAL_REQUEST_ID": { "description": "答えの出た承認の依頼の識別子。答えの本文は載せない。無い resume は答えを書かずに続ける", "type": "string", "minLength": 1 }, "LOOP_NAME": { "description": "NDF の開発ループの名前。loop と、ループの実行の resume に載せる", "type": "string", "pattern": "^[0-9A-Za-z][0-9A-Za-z_-]{0,63}$" }, "LOOP_MILESTONES": { "description": "選定の対象のマイルストーンの題を , で結ぶ", "type": "string", "minLength": 1, "maxLength": 1010 }, - "LOOP_BUDGET": { "description": "宣言した予算だけを並べる。無ければ NDF の既定", "type": "string", "minLength": 1, "pattern": "^(laps=\\d+(\\.\\d+)?)?(,?usd=\\d+(\\.\\d+)?)?(,?hours=\\d+(\\.\\d+)?)?$" }, + "LOOP_BUDGET": { "description": "単位ごとに数か default(その単位をリポジトリの既定へ戻す)を並べる。並べない単位と、LOOP_BUDGET が無いときは NDF の記録の値のまま(新しいループでは NDF の既定)", "type": "string", "minLength": 1, "pattern": "^(laps=(\\d+(\\.\\d+)?|default))?(,?usd=(\\d+(\\.\\d+)?|default))?(,?hours=(\\d+(\\.\\d+)?|default))?$" }, "EXCLUDE_ISSUES": { "description": "同じリポジトリの別の実行が持つ課題の番号(カンマ区切り)。ループの選定から外す", "type": "string", "pattern": "^[1-9][0-9]*(,[1-9][0-9]*)*$" }, "LOOP_ANSWERS": { "description": "人の判断待ちへの答え(<番号>=approved|rejected、承認なら # を続けてよい)を , で結ぶ", "type": "string", "pattern": "^[1-9][0-9]*=(approved|rejected)(#[1-9][0-9]*)?(,[1-9][0-9]*=(approved|rejected)(#[1-9][0-9]*)?)*$" }, "RELEASE_SEQ": { "description": "配布の実行のまとめた配布の連番(#2439)", "type": "string", "pattern": "^[1-9][0-9]*$" }, diff --git a/factory/contracts/src/generated/operations.ts b/factory/contracts/src/generated/operations.ts index 06b5b4cbe..c4d4e5444 100644 --- a/factory/contracts/src/generated/operations.ts +++ b/factory/contracts/src/generated/operations.ts @@ -1030,6 +1030,7 @@ export const OPERATIONS = [ }, "budget": { "type": "object", + "description": "単位ごとに数か null。null の単位はリポジトリの予算の既定(.ndf/pace.json の loop.budget)に従う", "additionalProperties": false, "properties": { "laps": { @@ -1142,7 +1143,7 @@ export const OPERATIONS = [ ], "idField": null, "summary": "開発ループの予算を変える", - "description": "終わりの状態でないループにだけ効き、次のジョブの指示から NDF の予算へ渡る。", + "description": "終わりの状態でないループにだけ効き、次のジョブの指示から NDF の予算へ渡る。3 つの単位をまとめて置き換え、送らない単位と null の単位は空(リポジトリの既定)として扱う。 値のある単位を空にする変更は、次のジョブが使う NDF の版(ループの今の実行の版、無ければ今のマーケットプレイスの版)が予算を既定へ戻せないとき 409 budget_reset_unsupported(details.ndf_version・details.units)で断る。値を入れる・変える・空のままにする変更は版によらず受ける。", "parameters": [], "itemSchema": { "type": "object", @@ -1158,6 +1159,7 @@ export const OPERATIONS = [ }, "budget": { "type": "object", + "description": "単位ごとに数か null。null の単位はリポジトリの予算の既定(.ndf/pace.json の loop.budget)に従う", "additionalProperties": false, "properties": { "laps": { diff --git a/factory/contracts/src/generated/run-job-instruction.ts b/factory/contracts/src/generated/run-job-instruction.ts index 9f3323b82..dcad42727 100644 --- a/factory/contracts/src/generated/run-job-instruction.ts +++ b/factory/contracts/src/generated/run-job-instruction.ts @@ -41,7 +41,7 @@ export type RunJobInstruction = { */ LOOP_MILESTONES?: string; /** - * 宣言した予算だけを並べる。無ければ NDF の既定 + * 単位ごとに数か default(その単位をリポジトリの既定へ戻す)を並べる。並べない単位と、LOOP_BUDGET が無いときは NDF の記録の値のまま(新しいループでは NDF の既定) */ LOOP_BUDGET?: string; /** diff --git a/factory/contracts/tests/definitions.test.ts b/factory/contracts/tests/definitions.test.ts index 646cb2d23..89fd294b4 100644 --- a/factory/contracts/tests/definitions.test.ts +++ b/factory/contracts/tests/definitions.test.ts @@ -191,6 +191,8 @@ describe("実行ジョブの指示の定義(受け入れ条件 1)", () => { instruction({ ...loop, LOOP_BUDGET: "laps=3,usd=60", EXCLUDE_ISSUES: "2171,2186", LOOP_ANSWERS: "12=approved#40,13=rejected" }), ).ok, ).toBe(true); + expect(validateRunJobInstruction(instruction({ ...loop, LOOP_BUDGET: "laps=default,usd=10,hours=default" })).ok).toBe(true); + expect(validateRunJobInstruction(instruction({ ...loop, LOOP_BUDGET: "usd=reset" })).ok).toBe(false); expect(validateRunJobInstruction(instruction({ ...loop, ISSUES: "2171" })).ok).toBe(false); expect(validateRunJobInstruction(instruction({ ...loop, LOOP_MILESTONES: undefined })).ok).toBe(false); expect(validateRunJobInstruction(instruction({ ...loop, LOOP_NAME: "-bad" })).ok).toBe(false); diff --git a/factory/runner/src/loop.test.ts b/factory/runner/src/loop.test.ts index c84f3c8eb..b8f8f9eb2 100644 --- a/factory/runner/src/loop.test.ts +++ b/factory/runner/src/loop.test.ts @@ -274,6 +274,12 @@ describe("START=loop の手順(受け入れ条件 1・12・13・18)", () => expect(firstPrompt(base, null)).toBe('/ndf:development-workflow --loop lfactory2 --milestones "25 Factory,26 根本" --budget-laps 3 --budget-usd 60'); }); + it("既定へ戻す指示 default もそのまま --budget-<単位> default で渡す(#2406)", () => { + expect(firstPrompt({ ...base, LOOP_BUDGET: "laps=default,usd=default,hours=24" }, null)).toBe( + '/ndf:development-workflow --loop lfactory2 --milestones "25 Factory,26 根本" --budget-laps default --budget-usd default --budget-hours 24', + ); + }); + it("job.finished の冪等の鍵は今の形のまま", () => { expect(common({ sprintRunId: "sr1", jobId: "job1", repo: "o/r" }, "finished", "2026-11-02T00:00:00.000Z").idempotency_key).toBe("job1:finished"); }); diff --git a/factory/web/src/i18n/ja.json b/factory/web/src/i18n/ja.json index a28c0907c..aabfbcde4 100644 --- a/factory/web/src/i18n/ja.json +++ b/factory/web/src/i18n/ja.json @@ -313,7 +313,11 @@ "remainingLaps": "{{value}} 周", "remainingUsd": "{{value}}", "remainingHours": "{{value}} 時間", - "noBudget": "掛けない", + "unitLaps": "周回の数", + "unitUsd": "費用", + "unitHours": "時間", + "followsDefault": "{{units}}はリポジトリの既定", + "budgetPlaceholder": "空ならリポジトリの既定", "start": "ループを始める", "badName": "英数字と - _ で 64 字までにしてください", "needMilestone": "マイルストーンを 1 つ以上選んでください", @@ -355,6 +359,7 @@ "rejected": "差し戻し", "budget": "予算を変える", "saveBudget": "予算を保存する", + "budgetResetUnsupported": "このループの次のジョブが使う NDF(版 {{version}})は予算をリポジトリの既定へ戻せません。値を入れるか、次の周回が新しい版で始まってから空にしてください", "cancel": "ループを止める", "nextLap": "次の周回を始める", "minutes": "{{value}} 分" diff --git a/factory/web/src/pages/LoopDetailPage.tsx b/factory/web/src/pages/LoopDetailPage.tsx index 19577df27..682caa96c 100644 --- a/factory/web/src/pages/LoopDetailPage.tsx +++ b/factory/web/src/pages/LoopDetailPage.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from "react"; import { Link, useParams } from "react-router"; import { useTranslation } from "react-i18next"; -import type { LoopDecision, LoopDetail, LoopPending } from "../api.ts"; +import { ApiFailure, type LoopDecision, type LoopDetail, type LoopPending } from "../api.ts"; import { useAttempt, useConnection, useServices } from "../context.tsx"; import { useFormat } from "../i18n/index.ts"; import { Alert } from "../ui/Alert.tsx"; @@ -205,7 +205,16 @@ export function LoopDetailPage() { e.preventDefault(); const parsed = parseBudget(budget); setBudgetError(parsed ? null : t("loops.badBudget")); - if (parsed) void act(() => api.updateLoopBudget(accountId, loopId, parsed)); + if (!parsed) return; + void act(async () => { + try { + await api.updateLoopBudget(accountId, loopId, parsed); + } catch (err) { + // 既定へ戻せない版のループで空にした(#2406)。理由は翻訳の文で予算の欄の誤りとして出す + if (!(err instanceof ApiFailure && err.code === "budget_reset_unsupported")) throw err; + setBudgetError(t("loopDetail.budgetResetUnsupported", { version: String(err.details?.ndf_version ?? "") })); + } + }); }} > diff --git a/factory/web/src/pages/loopParts.tsx b/factory/web/src/pages/loopParts.tsx index 2df9727b5..ae4103315 100644 --- a/factory/web/src/pages/loopParts.tsx +++ b/factory/web/src/pages/loopParts.tsx @@ -51,13 +51,17 @@ export function useLoopView() { return loop.stop_condition.startsWith("budget_") ? `${parts} ${t("loops.budgetContinue")}` : parts; }, usd, + // 値のある単位は残りを、空の単位はリポジトリの既定に従うことをまとめて 1 項目で出す(#2406) remaining: (b: LoopBudget): string => { const items = [ typeof b.laps === "number" ? t("loops.remainingLaps", { value: format.number(b.laps) }) : null, typeof b.usd === "number" ? t("loops.remainingUsd", { value: usd(b.usd) }) : null, typeof b.hours === "number" ? t("loops.remainingHours", { value: format.number(b.hours) }) : null, ].filter((x): x is string => x !== null); - return items.length === 0 ? t("loops.noBudget") : items.join(" / "); + const empty = (["laps", "usd", "hours"] as const).filter((k) => typeof b[k] !== "number"); + const units = { laps: t("loops.unitLaps"), usd: t("loops.unitUsd"), hours: t("loops.unitHours") }; + if (empty.length > 0) items.push(t("loops.followsDefault", { units: empty.map((k) => units[k]).join(t("loops.separator")) })); + return items.join(" / "); }, }; } @@ -69,7 +73,7 @@ export function budgetText(b: LoopBudget = {}): BudgetText { return { laps: s(b.laps), usd: s(b.usd), hours: s(b.hours) }; } -// 空の欄は予算を掛けない(null)。正の数でなければ null を返して入力の誤りにする +// 空の欄は null(リポジトリの既定に従う)。正の数でなければ null を返して入力の誤りにする export function parseBudget(text: BudgetText): LoopBudget | null { const out: LoopBudget = {}; for (const key of ["laps", "usd", "hours"] as const) { @@ -90,11 +94,24 @@ export function BudgetFields({ value, onChange, error }: { value: BudgetText; on const { t } = useTranslation(); return ( <> - onChange({ ...value, laps: e.target.value })} /> - onChange({ ...value, usd: e.target.value })} /> + onChange({ ...value, laps: e.target.value })} + /> + onChange({ ...value, usd: e.target.value })} + /> onChange({ ...value, hours: e.target.value })} error={error} diff --git a/factory/web/src/pages/loops.test.tsx b/factory/web/src/pages/loops.test.tsx index 777305142..3e41d4682 100644 --- a/factory/web/src/pages/loops.test.tsx +++ b/factory/web/src/pages/loops.test.tsx @@ -37,7 +37,9 @@ describe("ループの一覧(受け入れ条件 11)", () => { const table = await screen.findByRole("table", { name: "開発ループ" }); const rows = within(table).getAllByRole("row").slice(1); expect(rows.map((r) => within(r).getAllByRole("cell")[0]!.textContent)).toEqual(["lfactory", "lroot", "lflow"]); - expect(within(rows[0]!).getByText("8 周 / $87.50")).toBeTruthy(); + expect(within(rows[0]!).getByText("8 周 / $87.50 / 時間はリポジトリの既定")).toBeTruthy(); + expect(within(rows[1]!).getByText("周回の数・費用・時間はリポジトリの既定")).toBeTruthy(); + expect(within(table).queryByText(/掛けない/)).toBeNull(); expect(within(rows[0]!).getByText("2026/11/2 8:30:00")).toBeTruthy(); expect(within(rows[1]!).getByText("次の周回を始める操作を待っています")).toBeTruthy(); expect(within(rows[2]!).getByText(/費用の予算に達した.*新しいループで続けられます/)).toBeTruthy(); @@ -131,6 +133,7 @@ describe("ループの詳細(受け入れ条件 12)", () => { await userEvent.type(laps, "12"); await userEvent.click(screen.getByRole("button", { name: "予算を保存する" })); expect(update).toHaveBeenCalledWith(PERSONAL_ID, "lp-0001", { laps: 12, usd: 100, hours: null }); + expect((laps as HTMLInputElement).placeholder).toBe("空ならリポジトリの既定"); cleanup(); show(detail("lp-0003")); await screen.findByRole("heading", { level: 1, name: "lflow" }); @@ -138,6 +141,16 @@ describe("ループの詳細(受け入れ条件 12)", () => { }); }); +describe("予算をリポジトリの既定へ戻す(#2406)", () => { + it("既定へ戻せない版で断られると、版を差し込んだ理由を予算の欄に出す", async () => { + const api = show(detail("lp-0001")); + vi.spyOn(api, "updateLoopBudget").mockRejectedValue(new ApiFailure(409, "budget_reset_unsupported", "", { ndf_version: "10.26.1", units: ["usd"] })); + await userEvent.clear(await screen.findByRole("textbox", { name: "費用の予算(USD)" })); + await userEvent.click(screen.getByRole("button", { name: "予算を保存する" })); + expect(await screen.findByText(/NDF(版 10.26.1)は予算をリポジトリの既定へ戻せません/)).toBeTruthy(); + }); +}); + describe("リポジトリの同時の実行の数(受け入れ条件 8)", () => { it("持ち主は宣言でき、範囲の外は送らない。行からループの画面へ移れる", async () => { const api = show(`/accounts/${PERSONAL_ID}/repositories`); diff --git a/issues/issue-1694-design.md b/issues/issue-1694-design.md new file mode 100644 index 000000000..72bb86762 --- /dev/null +++ b/issues/issue-1694-design.md @@ -0,0 +1,249 @@ +# このリポジトリのテストの宣言: 継続的統合のジョブ 11 件を、手元で走らせる静的解析か理由つきの除外として載せる(#1694) + +## 目的 + +- **今どうなっているか**: このリポジトリの継続的統合のジョブ 24 件のうち 11 件が、テストの宣言(`.ndf/project.json` の `test`)のどこにも載っていない。11 件は生成物の同期・Skill の frontmatter・リンク・文書の行数・指示書・`claude plugin validate` などである。cross-refactoring の改善項目がこれらの検査を壊しても、着手前のテストと最終ゲートは走らせず、Pull Request を出した後の継続的統合で初めて落ちる。`init` は毎回「宣言に無いもの 11 件」を知らせる +- **誰に効くか**: このリポジトリで cross-refactoring・cross-review・テストの実行(`test-run.py`)を回す開発者と AI +- **変えると何が成り立つか**: 手元で判定が決まる 9 件は静的解析の suite として最終ゲートで走り、改善項目が壊せば変更起因として止まる。手元で判定が決まらない 2 件は理由つきの除外になり、`init` は「宣言に無いジョブ」を知らせなくなる + +## 適用範囲 + +- **働く範囲**: このリポジトリだけ。変えるのは宣言の `test` の要素だけで、配布物(`plugins/`)・継続的統合のワークフロー・突き合わせの処理(`ci_coverage.py`)・宣言のスキーマは変えない +- **プロジェクトごとに違うもの**: どのジョブを suite にするか・除外にするかは、各リポジトリの宣言(設定)で受ける。この変更はこのリポジトリの設定の値だけを決める +- **当たるモード**: `standard` + +## あるべき姿の根拠 + +| 根拠 | 種類 | 何を示すか | +| --- | --- | --- | +| 9 本のコマンドを作業ディレクトリの根で走らせると、すべて終了コード 0、所要の合計 19.6 秒(2026-11-05、このブランチの worktree、1 回の実測)。走らせた後の `git status --porcelain` は空 | 実測 | 9 件は手元で判定が決まり、作業ディレクトリを書き換えないこと | +| `claude` を `PATH` から外して `bash scripts/validate-runtime-plugins.sh` を走らせると、`==> claude CLI not found; skipped claude plugin validate` を出して終了コード 0、8.5 秒(2026-11-05) | 実測 | `claude` の CLI が無いホストでも、このジョブの残りの検査は走り、判定が決まること | +| `scripts/validate-runtime-plugins.sh` の `if command -v claude` の分岐 | 実測(コードの読み取り) | CLI の有無で落ちないことが、スクリプト自身の約束であること | +| `plugins/ndf/scripts/lib/test_triage.py` の `lint_verdict`、`plugins/ndf/skills/cross-refactoring/scripts/refactor_lib/gate_lint.py` の `lint_gate` | 実測(コードの読み取り) | 最終ゲートは静的解析の suite を 1 本ずつ走らせ、着手前の suite ごとの成否で既存失敗と変更起因を分けること | +| `plugins/ndf/scripts/project_lib/merge.py` の不変条件(置き換えてよいのは、キーが無い項目と、前の解析の値のままの項目だけ)と、宣言の `analysis.written` に `test` の指紋が無いこと | 実測(コードと宣言の読み取り) | 宣言の再解析が、手で足した `test` の要素を置き換えないこと | + +要求と受け入れ条件は #1694 の本文にある(コピーは [issue-1694-requirements.md](issue-1694-requirements.md) )。この文書は「どう作るか」だけを扱う。 + +## ドメインモデル + +### コンテキスト + +| コンテキスト | 何の語が 1 つの意味に決まるか | +| --- | --- | +| このリポジトリの開発の検査(`repo-dev-checks`) | 継続的統合のジョブ・ジョブの識別子・除外したジョブ | +| cross-refactoring(`ndf-cross-refactoring`) | 宣言に無いジョブ・着手前のテスト・最終ゲート・既存失敗・変更起因 | + +**関係は公開された言語である。** 2 つのコンテキストは、宣言のスキーマの `test.suites[].ci_jobs`・`test.ci_exempt` を介してだけやり取りする。スキーマは `plugins/ndf/skills/development-workflow/schemas/project.schema.json` にある。この変更はスキーマを変えず、このリポジトリの側の値だけを足す。 + +### 集約 + +| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | +| --- | --- | --- | --- | --- | +| テストの宣言(`.ndf/project.json` の `test`) | このリポジトリの開発者(手で書く。宣言の再解析は指紋の無い `test` を置き換えない) | `test` | suite(`name` で識別)・除外したジョブ(`job` で識別) | ジョブの識別子・コマンド・除外の理由 | + +### 不変条件 + +| # | 集約 | 条件 | 破れたときの扱い | +| --- | --- | --- | --- | +| I1 | テストの宣言 | 継続的統合のジョブはどれも、suite の `ci_jobs` か除外したジョブのどちらか一方だけに当たる | 両方に当たる・どちらにも当たらないジョブがあれば、実装の検証で止め、宣言を直す | +| I2 | テストの宣言 | 足した除外したジョブの理由は空でなく、手元で判定が決まらない要因を書く | 理由が空ならスキーマの検査(`project-decl.py check`)が 3 を返す。要因でない理由は実装の検証で直す | +| I3 | テストの宣言 | 足した suite の `command` は、`ci_jobs` に書いたジョブの `run` の行と同じスクリプトを同じ引数で呼ぶ(継続的統合だけで要る準備の手順は除く) | 実装の検証で止め、コマンドを直す | +| I4 | テストの宣言 | 足した suite は、秘密とネットワークを使わずに根で走り、終了コード 0 で終わる。所要の合計は 30 秒以内 | 0 で終わらない suite は、宣言へ載せる前に原因を調べて人へ戻す | +| I5 | テストの宣言 | 既存の suite 5 本と既存の除外 5 件は、名前・コマンド・`ci_jobs`・理由が変わらない | 差分に既存の要素の変更があれば戻す | + +### ドメインイベント + +| # | イベント | 発生元 | 受け手 | +| --- | --- | --- | --- | +| E1 | 宣言に suite と除外を足した | 実装の変更 | cross-refactoring の `init`・着手前のテスト・最終ゲート、cross-review、`test-run.py` | +| E2 | `init` が継続的統合のジョブと宣言を突き合わせた | cross-refactoring の `init`(`compare_jobs`) | `init` の出力(宣言に無いジョブが 0 件なら行を出さない) | +| E3 | 着手前のテストで、足した静的解析の suite を全体で走らせた | cross-refactoring の着手前のテスト(`baseline.py`) | 最終ゲート(suite ごとの成否を既存失敗の判定に使う) | +| E4 | 最終ゲートで、足した静的解析の suite を全体で走らせた | cross-refactoring の最終ゲート(`gate_lint.py`) | 最終ゲートの判定(変更起因なら止まる) | + +### 用語 + +| 用語 | 意味 | 用語集への反映 | +| --- | --- | --- | +| ジョブの識別子 | 継続的統合のジョブを指す `<ワークフローのファイルのパス>#`。`#` を省くとそのファイルのジョブすべてを指す | 無し(要求の用語を引き継ぐ。突き合わせの処理と宣言のスキーマの説明が持つ) | +| 除外したジョブ | 手元の検証で走らせないと宣言したジョブ。宣言の `test.ci_exempt` に理由と組で書く | 無し(同上) | +| 宣言に無いジョブ | 継続的統合のジョブのうち、どの suite の `ci_jobs` にも除外したジョブにも当たらないもの | 無し(同上) | + +## 機能一覧 + +| # | 機能 | 誰が使うか | +| --- | --- | --- | +| F1 | 改善項目が継続的統合の検査 9 件を壊したとき、cross-refactoring の最終ゲートが変更起因として止める | cross-refactoring を回す開発者と AI | +| F2 | cross-review とテストの実行(`test-run.py whole`)で、検査 9 件を全体テストの一部として走らせる | cross-review を回す AI・手元でテストを走らせる開発者 | +| F3 | 手元で判定が決まらない 2 件を、理由つきで手元の検証から外す | 宣言を読む開発者と AI | +| F4 | `init` が「宣言に無いジョブ」を知らせない | cross-refactoring を回す開発者と AI | + +## 構成要素 + +| 要素 | 責務 | +| --- | --- | +| テストの宣言(`.ndf/project.json` の `test`)※変える | suite 9 本と除外したジョブ 2 件を足す。ほかの要素は変えない | +| 突き合わせの処理(`refactor_lib/ci_coverage.py`)※変えない | 宣言とワークフローのジョブを突き合わせ、宣言に無いジョブを挙げる | +| 着手前のテスト(`refactor_lib/baseline.py`)※変えない | 静的解析の suite を全体で走らせ、suite ごとの成否を残す | +| 最終ゲートの静的解析(`refactor_lib/gate_lint.py`)※変えない | 静的解析の suite を 1 本ずつ走らせ、落ちたものを既存失敗か変更起因に分ける | +| テストの実行(`plugins/ndf/scripts/test-run.py`)※変えない | `whole` で宣言の suite の `command` を走らせる | +| 検査スクリプト 9 本(`scripts/` と `plugins/ndf/scripts/instructions-check.py`)※変えない | 継続的統合と同じ検査を、手元で同じ引数で行う | + +```mermaid +graph TD + D[テストの宣言] --> C[突き合わせの処理] + W[継続的統合の
ワークフロー] --> C + D --> B[着手前のテスト] + D --> G[最終ゲートの
静的解析] + D --> T[テストの実行] + B --> S[検査スクリプト 9 本] + G --> S + T --> S +``` + +## データ構造 + +宣言のスキーマは変えない。足す値は次のとおりである。 + +### 足す suite + +9 本とも `kind: lint`・`needs: []` で、`scope_command`・`junit`・`paths` を書かない(決定 3)。`ci_jobs` はジョブの単位の識別子 1 つである。パスは `.github/workflows/runtime-plugin-validate.yml` で、以下の表では `#` 以降だけを書く。並びは既存の suite 5 本の後ろに、ワークフローのジョブの順で置く。 + +| `name` | `runner` | `command` | `ci_jobs` | 手元の所要 | +| --- | --- | --- | --- | --- | +| `runtime-plugin-build-check` | `build-runtime-plugins` | `bash scripts/build-runtime-plugins.sh --check` | `#runtime-plugin-build-check` | 0.2 秒 | +| `skill-frontmatter-check` | `check-skill-frontmatter` | `python3 scripts/check-skill-frontmatter.py` | `#skill-frontmatter-check` | 0.1 秒 | +| `skill-repo-assumptions-check` | `check-skill-repo-assumptions` | `python3 scripts/check-skill-repo-assumptions.py` | `#skill-repo-assumptions-check` | 0.2 秒 | +| `skill-shell-vars-check` | `check-skill-shell-vars` | `python3 scripts/check-skill-shell-vars.py` | `#skill-shell-vars-check` | 0.3 秒 | +| `service-boundary-check` | `check-service-boundary` | `python3 scripts/check-service-boundary.py` | `#service-boundary-check` | 1.3 秒 | +| `runtime-plugin-validate` | `validate-runtime-plugins` | `bash scripts/validate-runtime-plugins.sh` | `#runtime-plugin-validate` | 11.0 秒 | +| `markdown-link-check` | `check-markdown-links` | `python3 scripts/check-markdown-links.py --root .` | `#markdown-link-check` | 6.2 秒 | +| `doc-line-limit-check` | `check-doc-line-limit` | `python3 scripts/check-doc-line-limit.py --root .` | `#doc-line-limit-check` | 0.1 秒 | +| `instruction-files-check` | `check-instruction-files` | `python3 plugins/ndf/scripts/instructions-check.py --root .` | `#instruction-files-check` | 0.1 秒 | + +所要は 2026-11-05 のこのブランチの worktree での 1 回の実測である。合計は 19.6 秒で、非機能の条件の 30 秒以内に収まる(丸める前の値の和。表の値の和は 19.5 秒)。`runtime-plugin-validate` は `claude` の CLI があるときの値で、無いときは 8.5 秒。`name` は継続的統合のジョブ id と同じにし、最終ゲートの記録と継続的統合のチェックの名前を同じ語で読めるようにする。`runner` は呼ぶスクリプトの名前(拡張子を除く)で、判定には使われない。 + +1 本を宣言に書いた形の例: + +```json +{ + "name": "runtime-plugin-build-check", + "runner": "build-runtime-plugins", + "kind": "lint", + "command": "bash scripts/build-runtime-plugins.sh --check", + "needs": [], + "ci_jobs": [ + ".github/workflows/runtime-plugin-validate.yml#runtime-plugin-build-check" + ] +} +``` + +### 足す除外したジョブ + +既存の除外 5 件の後ろに、パスの順で置く。 + +| `job` | `reason` | +| --- | --- | +| `.github/workflows/runtime-plugin-smoke-latest.yml` | 定期と手動でだけ起動し、ネットワークから最新の CLI の版を解決して入れる。項目のコミットだけでは判定が決まらない | +| `.github/workflows/runtime-plugin-validate.yml#required-checks-check` | Pull Request の宛先のブランチと GitHub の API(トークン)を使う。項目の変更で結果が変わらない | + +`runtime-plugin-smoke-latest.yml` はジョブが 1 件だけで扱いが分かれないため、ファイルの単位で書く。`required-checks-check` は同じファイルに suite にするジョブがあるため、ジョブの単位で書く(要求の前提 2)。 + +## 処理の流れ + +宣言を読む処理はどれも変えない。足した値が通る経路は次のとおりである。 + +```mermaid +sequenceDiagram + participant I as init + participant B as 着手前のテスト + participant G as 最終ゲート + participant S as 検査スクリプト + I->>I: compare_jobs(宣言に無いジョブ 0 件、行を出さない) + B->>S: 足した suite を 1 本ずつ全体で走らせる + S-->>B: suite ごとの成否(green / red) + G->>S: 足した suite を 1 本ずつ全体で走らせる + S-->>G: 終了コード + G->>G: 落ちた suite を lint_verdict で判定 +``` + +最終ゲートで落ちた suite の判定は、既存の `lint_verdict` の表のとおりである。足した suite は `scope_command` を持たないため、次の 2 行だけを通る。 + +| 着手前の成否 | 判定 | +| --- | --- | +| 通っていた | 変更起因(最終ゲートが止まる) | +| 落ちていた | 既存失敗(判定から外す) | +| 読めない | 変更起因 | + +`scope_command` を持たない suite は、改善項目ごとの検証(範囲テスト)では走らない。`targets.py` の `lint_runs_for` が、`scope_command` を持つ suite だけを使うためである。改善項目が壊した場合に気づくのは最終ゲートである。 + +## 非機能の実現方式 + +| 大項目 | 実現方式 | +| --- | --- | +| 性能・拡張性 | 足す suite の所要の合計は 19.6 秒(実測)。着手前のテストと最終ゲートは、それぞれこの分だけ長くなる。`runtime-plugin-validate` は別の suite と同じ検査(生成物の同期・リンク・文書の行数)を内部でもう一度行い、重なりは実測で約 6.5 秒(決定 2) | +| システム環境 | 足す suite はリポジトリの中のファイルだけを読む。`scripts/check-markdown-links.py` が使う `urllib` は `urllib.parse`(文字列の分解)だけで、通信をしない。`claude plugin validate` が通信をするかは確かめられていない(「未確認のまま残ること」) | + +## 決定の記録 + +### 決定 1: 落ちた検査を 1 つずつ判定できるよう、ジョブ 1 件に suite を 1 本置く + +最終ゲートは静的解析の suite を 1 本ずつ走らせ、着手前の suite ごとの成否で既存失敗と変更起因を分ける(`gate_lint.py`・`lint_verdict`)。9 件を 1 本の suite にまとめると、どれか 1 つが着手前から落ちていれば suite 全体が既存失敗として判定から外れる。そのとき、ほかの 8 件を改善項目が壊しても止まらない。`&&` でつないだコマンドは最初の失敗で止まり、後ろの検査が走らない。 + +9 本の `command` を 1 本の suite にまとめる形は採らない。suite を増やす費用は宣言の行数だけで、走らせる時間は変わらないためである。 + +根拠: Value 7(根本原因の場所で直す)/ Value 2(人が判断できる量に収める)(MVV 版 5) + +### 決定 2: `claude` の CLI が無いホストでも残りの検査を走らせるため、`runtime-plugin-validate` を suite にする + +このジョブのスクリプトは、CLI が無いと `claude plugin validate` だけを飛ばす。残りの検査(定義ファイルの形・マーケットプレイスの登録・説明文書の数と版・Skill の境界をまたぐ参照)は走らせ、0 で終わる(実測)。後ろの 2 つの検査(`check-doc-staleness.py`・`check-cross-skill-refs.py`)は、継続的統合ではこのジョブでしか走らない。`.github/` と `scripts/*.sh` を検索すると、呼ぶのは `scripts/validate-runtime-plugins.sh` だけである。除外にすると改善項目が壊しても手元で気づけない。継続的統合で先に CLI を入れる手順(`npm install -g`)は準備の手順で、受け入れ条件 4 が除く。Codex のホストで CLI が無いときは、`claude plugin validate` の分だけ継続的統合より検査が少なくなる。 + +除外にする形は採らない。CLI の有無で変わるのは検査の 1 つだけで、残りの検査は手元で判定が決まるためである。スクリプトの中で別の suite と重なる検査(約 6.5 秒)は、重ならないように別の suite を外す形も採らない。外すとその suite の `command` の引数が継続的統合のジョブと変わり(スクリプトの中では `--root` に絶対パスを渡す)、決定 1 の 1 件ずつの判定も失うためである。 + +根拠: Value 6(どのプロジェクトでも使える形を保つ)/ Value 1(小さく出して使って確かめる)(MVV 版 5) + +### 決定 3: 読まれない値を宣言に残さないため、足す suite に `scope_command` と `paths` を書かない + +9 本の検査スクリプトはどれも対象のファイルのパスを引数に受けず、受ける形にする変更はこの課題の範囲の外である(要求の対象範囲)。静的解析の suite の `paths` は、`scope_command` を持つ suite の範囲を決めるときだけ読まれ、`scope_command` の無い suite では読まれない。読むのは `lint_verdict` と、`targets.py` の `suite_lint_files`・`lint_runs_for` である。読まれない値を書くと、検査スクリプトの読む範囲が変わっても誰も気づかず古くなる。 + +`{paths}` を受け取って捨てる `scope_command`(例: 末尾に `# {paths}` を付ける)は採らない。継続的統合と同じ引数で呼ぶ約束(受け入れ条件 4)から外れ、改善項目ごとに全体の検査を走らせて時間が増えるためである。 + +根拠: Value 3(やめる・縮めるを既定にする)/ Value 5(決まった手順はスクリプトに、判断と中身は AI に)(MVV 版 5) + +### 決定 4: 手元で判定が決まらない 2 件を、理由つきの除外にする + +`required-checks-check` は Pull Request の宛先のブランチと GitHub の API を使う。`runtime-plugin-smoke-latest.yml` はネットワークから最新の CLI を入れる。どちらも要求の前提 3 を満たさない。前提 3 は、秘密・ネットワーク・Pull Request の文脈を使わずに手元で判定が決まることである。理由には、手元で判定が決まらない要因を書く(受け入れ条件 2)。 + +`check-required-checks.py` を宛先を固定して手元で走らせる形は採らない。GitHub の API とトークンが要り、継続的統合と同じ引数でも呼べないためである。 + +根拠: 共通原則 C1(秘密に触れる操作を手元の検証に入れない)/ Value 6(MVV 版 5) + +## テスト設計 + +要求の取り決めのとおり、振る舞いを変えないためテストのコードは足さない。受け入れ条件と不変条件は、実装の検証でコマンドを走らせて確かめる。 + +| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | +| --- | --- | --- | +| 受け入れ条件 1・3 / I1 | 根の宣言と作業ディレクトリで `compare_jobs` を打つと、`status` が `compared`、`undeclared` が空 | 足した suite か除外を 1 つ消すと、`undeclared` にそのジョブが出る | +| I1(一方だけ) | 宣言の `ci_jobs` と除外の `job` を並べ、同じ識別子・同じファイルを指すファイルの単位の識別子が両方に無い | `required-checks-check` を suite と除外の両方に書くと、重なりとして見つかる | +| 受け入れ条件 2 / I2 | 足した除外 2 件の `reason` が空でなく、要因(ネットワーク・Pull Request の宛先と GitHub の API)を書いている | `reason` を空にすると `project-decl.py check` が 3 を返す | +| 受け入れ条件 4 / I3 / I4 | 足した suite の `command` を根で 1 本ずつ走らせると 0 で終わり、文字列がワークフローの `run` の行と一致する | 引数を変える(`--root .` を外すなど)と `run` の行との照合で食い違う | +| I4(CLI が無いホスト) | `claude` を `PATH` から外して `runtime-plugin-validate` の `command` を走らせても 0 で終わる | スクリプトが CLI の有無で落ちる形になると 0 以外で終わる | +| 受け入れ条件 5 | `project-decl.py check --root .` が 3 を返さない(今は入力が古いため 2) | スキーマに無いキーを足すと 3 を返す | +| 受け入れ条件 6 / I5 | `git diff` で、既存の suite 5 本と除外 5 件の行が変わっていない | 既存の要素を書き換えると差分に現れる | +| 非機能の条件(性能) | 足した suite の所要の合計が 30 秒以内 | — | +| 全体 | `uv run --frozen --project . --all-extras pytest . -q -n 4` が通る | — | + +## 設計の結果 + +| 課題 | 扱い | 取り込み先 | 触るファイル | +| --- | --- | --- | --- | +| #1694 | 実装する | — | `.ndf/project.json` | + +## 未確認のまま残ること + +| 項目 | 内容 | +| --- | --- | +| `claude plugin validate` の通信 | この環境ではネットワークを切る操作(`unshare -n`)が権限で拒否され、通信の有無を確かめられなかった。通信するなら非機能の条件(システム環境)に触れる。実装の検証で、通信できない状態で走らせられる環境があれば確かめる | +| 所要のばらつき | 所要は 1 回の実測で、ばらつきは測っていない。ファイルが増えるとリンクの検査(6.2 秒)と `runtime-plugin-validate`(11.0 秒)が伸びる | +| ジョブが増えたとき | 継続的統合にジョブを足すと、`init` はまた「宣言に無いジョブ」を知らせる。これは突き合わせの処理の目的どおりの振る舞いで、この課題では自動の検査を足さない | +| 宣言の再解析 | `project-decl.py check` は今「古い」(入力の `.github/workflows/pytest.yml` が変わった)を返す。再解析はこの課題の範囲の外で、再解析は指紋の無い `test` を置き換えない(`merge.py` の不変条件) | diff --git a/issues/issue-1694-requirements.md b/issues/issue-1694-requirements.md new file mode 100644 index 000000000..4e6d8d26d --- /dev/null +++ b/issues/issue-1694-requirements.md @@ -0,0 +1,166 @@ +# #1694: このリポジトリの runtime-plugin-validate.yml の 9 ジョブがテストの宣言に無く、cross-refactoring の項目が壊しても手元で拾えない + +正は課題の本文(#1694)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 + +## 依頼(原文) + +> **このリポジトリの継続的統合の `runtime-plugin-validate.yml` の 9 ジョブ(生成物の同期・frontmatter・リポジトリの前提・手順書の変数・ +> リンク・文書の行数・指示書・必須チェックの設定・`claude plugin validate`)は、テストの宣言(`.ndf/project.json` の `test.suites`)に無い。** +> cross-refactoring の項目がこれらを壊しても、項目の検証と手元の最終ゲートは拾わない。 +> +> #464 の実装(PR #1697)の後の今の宣言で `refactor_lib.ci_coverage.compare_jobs` を打つと、ジョブ 24 件のうち宣言に無いものは次の 11 件である(2026-11-04、develop 252ddf82b)。 +> +> - `runtime-plugin-validate.yml` の 10 件: `doc-line-limit-check`・`instruction-files-check`・`markdown-link-check`・ +> `required-checks-check`・`runtime-plugin-build-check`・`runtime-plugin-validate`・`service-boundary-check`・`skill-frontmatter-check`・ +> `skill-repo-assumptions-check`・`skill-shell-vars-check` +> - `runtime-plugin-smoke-latest.yml` の `runtime-smoke-latest` +> +> 宣言の `ci_jobs`・`ci_exempt` はワークフローのファイル(`.github/workflows/<名前>.yml`)の単位で書く。 +> +> ## 受け入れ条件(案) +> +> - [ ] 上の 11 件のそれぞれが、宣言の suite の `ci_jobs` か `test.ci_exempt`(理由つき)のどちらかに載る +> - [ ] cross-refactoring の `init` がこのリポジトリで宣言に無いジョブを知らせない + +(書き手: 起票者、日付: 2026-10-03 起票・2026-11-04 更新。#464 の設計の途中で範囲外として起票した記録で、利用者の指示の原文は無い) + +> 人へ問わずに進め、決められない点は前提か未決として課題の本文へ書く。 + +(書き手: AI の起動指示、日付: 2026-11-05) + +## 起票者の推定 + +| # | 条件 | 推した出所 | この要求での扱い | +| --- | --- | --- | --- | +| S1 | 宣言に無いジョブ 11 件のそれぞれを、suite の `ci_jobs` か `test.ci_exempt`(理由つき)に載せる | 起票時の受け入れ条件(案)1 | 受け入れ条件 1・2 | +| S2 | cross-refactoring の `init` がこのリポジトリで宣言に無いジョブを知らせない | 起票時の受け入れ条件(案)2 | 受け入れ条件 3 | +| S3 | 宣言の `ci_jobs`・`ci_exempt` はワークフローのファイルの単位で書く | 本文「宣言の `ci_jobs`・`ci_exempt` はワークフローのファイルの単位で書く」 | 採らない。突き合わせ(`refactor_lib/ci_coverage.py`)は `<パス>#<ジョブ id>` の形も受ける。`runtime-plugin-validate.yml` の中には手元で走らせるジョブと走らせないジョブが混ざるため、ジョブの単位で書く(前提 2) | +| S4 | 対象は「9 ジョブ」 | 題と本文の 1 段落目 | 採らない。2026-11-05 の再測定で、`runtime-plugin-validate.yml` のジョブは 10 件(`service-boundary-check` を含む)、ほかに `runtime-plugin-smoke-latest.yml` の 1 件がある。対象は 11 件とする | +| S5 | 各ジョブを suite にするか除外するかは、範囲テストの形(`{paths}` を受けられるか)を確かめて決める | 本文「なぜ範囲外か」 | 採らない。静的解析の suite(`kind: lint`)は `scope_command` を持たなくても全体の実行で判定できる(`plugins/ndf/scripts/lib/test_triage.py` の `lint_verdict`)。suite にする条件は前提 3 で決める | + +## 目的 + +cross-refactoring の項目が、このリポジトリの継続的統合で検査している内容(生成物の同期・Skill の frontmatter・リンク・文書の行数・指示書など)を壊したとき、Pull Request を出す前の手元の検証で気づけるようにする。手元で走らせられない検査は、走らせない理由を宣言に残し、`init` が毎回「宣言に無いジョブ」を知らせる状態を無くす。 + +## 前提 + +- 前提 1: 変更の対象は、このリポジトリのプロジェクトの宣言(`.ndf/project.json` の `test`)だけである。突き合わせの処理(`ci_coverage.py`)・宣言のスキーマ・ワークフローは変えない +- 前提 2: ジョブの識別子は、1 つのファイルのジョブがすべて同じ扱いになるときはファイルの単位(`<パス>`)、扱いが分かれるときはジョブの単位(`<パス>#<ジョブ id>`)で書く +- 前提 3: ジョブを suite にする条件は、次の 3 つをすべて満たすことである。満たさないジョブは除外にする + - 秘密・ネットワーク・Pull Request の文脈(宛先のブランチ・本文)を使わずに、手元で判定が決まる + - 継続的統合のジョブと同じスクリプトを同じ引数で走らせる + - 1 回の実行が 60 秒以内に終わる(2026-11-05 の手元の実測で最長は `validate-runtime-plugins.sh` の 12.2 秒) +- 前提 4: 前提 3 の判定の材料は下の「現状の実測」の表である。どのジョブをどちらに載せるか・suite の分け方(ジョブごとか、まとめるか)・`paths` は設計で決める +- 前提 5: `claude plugin validate` を走らせるジョブ(`runtime-plugin-validate`)は、手元に `claude` の CLI があることを要る。cross-refactoring のホストは Claude Code か Codex であり、Codex のホストでは CLI が無いことがある。suite にするか除外するかは設計で決め、決めた理由を宣言か設計に残す + +### 現状の実測 + +2026-11-05、develop d51159151、このリポジトリの worktree で各ジョブの `run` の行を走らせた結果。 + +| ジョブ | 継続的統合で走らせるもの | 手元の終了コード | 手元の所要 | 手元で判定が決まらない要因 | +| --- | --- | --- | --- | --- | +| `runtime-plugin-build-check` | `bash scripts/build-runtime-plugins.sh --check` | 0 | 0.2 秒 | 無し | +| `skill-frontmatter-check` | `python3 scripts/check-skill-frontmatter.py` | 0 | 0.2 秒 | 無し | +| `skill-repo-assumptions-check` | `python3 scripts/check-skill-repo-assumptions.py` | 0 | 0.2 秒 | 無し | +| `skill-shell-vars-check` | `python3 scripts/check-skill-shell-vars.py` | 0 | 0.4 秒 | 無し | +| `service-boundary-check` | `python3 scripts/check-service-boundary.py` | 0 | 1.6 秒 | 無し | +| `runtime-plugin-validate` | `bash scripts/validate-runtime-plugins.sh`(継続的統合では先に `claude` の CLI を入れる) | 0 | 12.2 秒 | `claude` の CLI が要る(前提 5) | +| `markdown-link-check` | `python3 scripts/check-markdown-links.py --root .` | 0 | 7.3 秒 | 無し | +| `doc-line-limit-check` | `python3 scripts/check-doc-line-limit.py --root .` | 0 | 0.1 秒 | 無し | +| `instruction-files-check` | `python3 plugins/ndf/scripts/instructions-check.py --root .` | 0 | 0.2 秒 | 無し | +| `required-checks-check` | `python3 scripts/check-required-checks.py --repo … --branch <宛先>` | 未実行 | — | Pull Request の宛先のブランチと GitHub の API を使う | +| `runtime-smoke-latest` | 最新の CLI の版を解決して入れ、スモークを走らせる(定期と手動の起動だけ) | 未実行 | — | ネットワークから最新の CLI を入れる | + +手元の所要は 1 回の実測で、ばらつきは測っていない。上の 9 本の検査スクリプトは、どれも対象のファイルのパスを引数に受けない(各スクリプトの `argparse` の定義を確かめた)。 + +## 対象範囲 + +含む: + +- 宣言に無いジョブ 11 件を、suite の `ci_jobs` か `test.ci_exempt` に載せる +- suite にするジョブのために、静的解析の suite(`kind: lint`)を宣言へ足す +- 除外にするジョブの理由を書く + +含まない: + +- 検査スクリプトに、対象のファイルのパスを受ける引数(範囲テストの形)を足すこと +- 突き合わせの処理・宣言のスキーマ・ワークフローの変更 +- このリポジトリ以外のプロジェクトの宣言 +- 宣言の再解析(`project-decl.py check` が古いと判定する件。宣言の `analysis.inputs` の更新を含む) + +## ドメインイベント + +| # | イベント | 引き金 | 失敗したとき | 順序の前提 | +| --- | --- | --- | --- | --- | +| E1 | 宣言に suite と除外を足した | 実装の変更 | 宣言が壊れると、cross-refactoring・cross-review・テストの実行(`test-run.py`)が宣言を読めず止まる(受け入れ条件 5) | — | +| E2 | `init` が継続的統合のジョブと宣言を突き合わせた | cross-refactoring の `init` | 突き合わせられないと「突き合わせられませんでした」の 1 行が出る | E1 | +| E3 | 着手前のテストで、足した静的解析の suite を全体で走らせた | cross-refactoring の着手前のテスト | 着手前から落ちる suite は、最終ゲートで既存失敗として判定から外れる | E1 | +| E4 | 最終ゲートで、足した静的解析の suite を全体で走らせた | cross-refactoring の最終ゲート | 項目が壊していれば変更起因として止まる(目的の振る舞い) | E3 | + +E3・E4 の振る舞いは既存の処理(`lint_verdict`)が持ち、この課題では変えない。 + +## 用語 + +| 用語 | 意味 | +| --- | --- | +| プロジェクトの宣言 | この文書では、このリポジトリの `.ndf/project.json` を指す。本文では「宣言」と略す | +| ジョブの識別子 | 継続的統合のジョブを指す `<ワークフローのファイルのパス>#`。`#` を省くとそのファイルのジョブすべてを指す | +| 除外したジョブ | 手元の検証で走らせないと宣言したジョブ。宣言の `test.ci_exempt` に理由と組で書く | +| 宣言に無いジョブ | 継続的統合のジョブのうち、どの suite の `ci_jobs` にも除外したジョブにも当たらないもの | + +## 受け入れ条件 + +- [ ] 1. 宣言に無いジョブ 11 件(上の「現状の実測」の表の行)のそれぞれが、suite の `ci_jobs` か `test.ci_exempt` の `job` のどちらか一方だけに、ファイルの単位かジョブの単位の識別子で当たる +- [ ] 2. `test.ci_exempt` に足した行は、`reason` が空でなく、手元で判定が決まらない要因(「現状の実測」の表の最後の列に当たるもの)か、走らせない理由を書いている +- [ ] 3. このリポジトリの宣言と作業ディレクトリで `compare_jobs` を打つと、`status` が `compared` で `undeclared` が 0 件になる(`init` が「宣言に無いもの」の行を出さない) +- [ ] 4. 足した suite のそれぞれは、`command` を作業ディレクトリの根で走らせると終了コード 0 で終わり、そのコマンドは `ci_jobs` に書いたジョブの `run` の行と同じスクリプトを同じ引数で呼ぶ(継続的統合だけで要る準備の手順は除く) +- [ ] 5. `project-decl.py check --root .` が 3(壊れている)を返さない +- [ ] 6. 既存の suite(`pytest`・`factory`・`script-structure`・`ndf-contracts`・`lint`)と既存の除外 5 件は、名前・コマンド・`ci_jobs`・理由が変わらない + +## 非機能の条件 + +| 大項目 | 条件 | +| --- | --- | +| 性能・拡張性 | 足した suite の `command` の所要の合計は、手元で 30 秒以内(2026-11-05 の実測では、`claude` の CLI を使うジョブを含めて 22.4 秒、含めないで 10.2 秒)。cross-refactoring の着手前のテストと最終ゲートは、この合計ずつ長くなる | +| システム環境 | 足した suite は、秘密とネットワークを使わずに走る | + +## 影響 + +| 対象 | 影響 | +| --- | --- | +| 公開インタフェース | 変わらない(配布物を変えない) | +| データ | このリポジトリの宣言(`.ndf/project.json`)の `test` に suite と除外が増える。移行は無い | +| 既存の振る舞い | このリポジトリで cross-refactoring を走らせると、着手前のテストと最終ゲートで足した suite が走る。宣言の suite を読む cross-review とテストの実行(`test-run.py`)でも、足した suite が走る対象に入る | + +## 検証手段 + +| 項目 | 手段 | +| --- | --- | +| 突き合わせ(受け入れ条件 1・3) | `cd plugins/ndf/skills/cross-refactoring/scripts && python3 -c "import json,pathlib; from refactor_lib.ci_coverage import compare_jobs; r=compare_jobs(json.load(open('<根>/.ndf/project.json')), pathlib.Path('<根>')); print(r.as_state())"` | +| suite のコマンド(受け入れ条件 4) | 足した suite の `command` を根で 1 本ずつ走らせ、終了コードを見る | +| 宣言の形(受け入れ条件 5) | `python3 plugins/ndf/scripts/project-decl.py check --root .` の終了コード | +| 既存の宣言の不変(受け入れ条件 2・6) | `git diff` で `.ndf/project.json` の既存の要素が変わっていないことと、足した除外の理由を見る | +| テスト | `uv run --frozen --project . --all-extras pytest . -q -n 4` | + +## 前提とする取り決め + +| 項目 | 参照先 / 決めたこと | +| --- | --- | +| プロジェクト構造 | 変更は `.ndf/project.json` だけに置く。プラグイン(`plugins/`)とワークフロー(`.github/workflows/`)は変えない | +| コーディング規約 | 宣言の形は `plugins/ndf/skills/development-workflow/schemas/project.schema.json` に従う | +| テスト戦略 | 振る舞いの変更が無いためテストを足さない。受け入れ条件は検証手段のコマンドの実行結果で確かめる | + +## 境界 + +| 区分 | 内容 | +| --- | --- | +| 常に行う | 足した suite のコマンドを手元で走らせる。突き合わせの結果を確かめる | +| 確認してから行う | 宣言(`.ndf/project.json`)の書き換え。共通原則の C7 に当たり、承認ゲート 2 で人が見る | +| 行わない | 検査スクリプト・ワークフロー・突き合わせの処理の変更。宣言の再解析 | + +## 未決 + +| 項目 | 誰が決めるか | 期限 | +| --- | --- | --- | +| 11 件のそれぞれを suite と除外のどちらに載せるか(前提 3・前提 5) | 設計 | 承認ゲート 1 | +| suite の分け方(ジョブごとに 1 つか、まとめて 1 つか)と各 suite の `paths` | 設計 | 承認ゲート 1 | diff --git a/issues/issue-2097-design.md b/issues/issue-2097-design.md new file mode 100644 index 000000000..5d47d7e52 --- /dev/null +++ b/issues/issue-2097-design.md @@ -0,0 +1,242 @@ +# supervise.py new: production_branch を宣言しないリポジトリでも、既定ブランチを本番にしてリリースプランを組める(#2097) + +## 目的 + +- **今どうなっているか**: `.ndf/worktree.json` に `production_branch` が無いリポジトリで、まだ作っていない worktree のパスを `--worktree` に渡す。すると `supervise.py new release --channel prod` が止まる(終了コード 2)。文面は「本番の配布に要る本番のブランチが無い」である。既定ブランチを、存在しない worktree のパスから解こうとして失敗するためである。`new sprint` も本番のステージを置かない +- **誰に効くか**: `production_branch` を書かず、既定ブランチを本番にしている NDF の利用先のリポジトリと、そこでスプリントとリリースを回す conductor +- **変えると何が成り立つか**: worktree の有無にかかわらず、本番チャネルが既定ブランチに決まり、リリースプランと承認ゲート 2・本番のステージが組まれる。リリースの経路の判定とプランが同じ本番チャネルを使う + +## 適用範囲 + +- **働く範囲**: NDF を使うすべてのリポジトリ(`supervise.py new` の `release`・`sprint`・`close`)。ai-plugins は `production_branch` を宣言しているため、振る舞いは変わらない +- **プロジェクトごとに違うもの**: 本番チャネルは、引数(`--production-branch`)と設定(`.ndf/worktree.json` の `production_branch`)で受ける。どちらも無ければ origin の既定ブランチを使う。受け方は変えない +- **当たるモード**: `standard` + +## あるべき姿の根拠 + +| 根拠 | 種類 | 何を示すか | +| --- | --- | --- | +| 2026-11-05 に一時のリポジトリ(bare の origin と clone、`git remote set-head origin main`、`{"base_branch":"main"}` と `release` の `form: package-plugin`)で `new release --channel prod` を打った結果 | 実測 | worktree を作る前は終了コード 2 で止まり、`git worktree add` の後は同じコマンドが 13 ステップ(`approve` … `judge-record`)のプランを書く。違いは worktree の有無だけである | +| 同じ日に `git -C <存在しないパス> symbolic-ref --short refs/remotes/origin/HEAD` を打った結果 | 実測 | `fatal: cannot change to '…': No such file or directory` と終了コード 128。存在する worktree からは `origin/main` が返る。既定ブランチを解けない原因は、問い合わせる木が存在しないことである | +| `plugins/ndf/scripts/lib/repo.py` の `production_of` の docstring(「本番チャネルの決め方の正本」)と、用語集の「本番チャネル」の行 | 既存の形 | 本番チャネルの決め方は `production_branch` → 既定ブランチの順と決まっている。この変更は順序を変えず、既定ブランチを解く木だけを直す | + +要求と受け入れ条件は #2097 の本文にある(コピーは [issues/issue-2097-requirements.md](issue-2097-requirements.md) )。この文書は「どう作るか」だけを扱う。 + +## ドメインモデル + +### コンテキスト + +| コンテキスト | 何の語が 1 つの意味に決まるか | +| --- | --- | +| NDF の工程(`ndf-workflow`) | 本番チャネル・既定ブランチ・メインディレクトリ・リリースプラン・宣言を探す木 | +| リリース(`ndf-release`) | リリースの経路 | + +リリースが供給者、NDF の工程が顧客の関係(顧客 / 供給者)。プランの雛形(工程の側)は、リリースの経路の判定(`lib/delivery.py`)が使う本番チャネルの決め方(`lib/repo.py` の `production_of`)を、そのまま受けて使う。工程の側に別の決め方を置かない。 + +### 集約 + +| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | +| --- | --- | --- | --- | --- | +| 雛形の引数 | `supervise_lib/decl.py` の `apply_decls`(引数に無い値を宣言から埋める) | `supervise.py new` の引数の名前空間 | — | 本番チャネル(`a.production_branch`)・ベースブランチ(`a.base`) | +| 配布の判定の宣言 | `lib/delivery.py` の `build`(`supervise_lib/decl.py` の `delivery_decl` が入力を渡す) | `DeliveryDecl` | — | 本番チャネル・ベースブランチ・各値の出所 | + +本番チャネルの値を決める規則(`production_of`)は `lib/repo.py` だけが持つ。2 つの集約は、同じ規則に、同じ木から解いた既定ブランチを渡して値を得る。 + +### 不変条件 + +| # | 集約 | 条件 | 破れたときの扱い | +| --- | --- | --- | --- | +| I1 | 雛形の引数 | 本番チャネルは、`--production-branch` → `production_branch` の宣言 → 既定ブランチの順で、最初に決まった値である | 結合のテストで落ちる | +| I2 | 雛形の引数 | 既定ブランチは、宣言を探す木の並び(`decl_roots`)のうち、最初に存在する git の木から解く。worktree のパスが存在しないことで結果が変わらない | 結合のテストで落ちる | +| I3 | 雛形の引数・配布の判定の宣言 | 同じ起動の中で、雛形の引数の本番チャネルと、配布の判定の宣言の本番チャネルは同じ値である | 結合のテストで落ちる | +| I4 | 雛形の引数 | `production_branch` の宣言か引数があるときは、既定ブランチを解く git を起動しない | 単体のテストで落ちる | +| I5 | 雛形の引数 | 3 つの出所のどれでも決まらないとき、止まるのは本番のプランを組む経路(`new release --channel prod`・昇格のプラン・fast / auto の `new sprint`・`new close`)だけで、エラーの文面が 3 つの出所を挙げる(終了コード 2)。開発版のリリース(`--channel dev`)は本番チャネルを使わずにプランを書き、normal の `new sprint` は承認ゲート 2 と本番のステージを置かずに、本番を置かない案内を出してプランを書く(どちらも終了コード 0) | 結合のテストで落ちる | + +### ドメインイベント + +| # | イベント | 発生元 | 受け手 | +| --- | --- | --- | --- | +| E1 | `supervise.py new <種類>` が起動された | 利用者か conductor | `apply_decls` | +| E2 | 宣言を読んだ | `apply_decls`(`read_decl`) | 本番チャネルを決める処理(E3) | +| E3 | 本番チャネルが決まった | `apply_decls`(`repo.production_branch`)と `delivery_decl`(`delivery.build`) | 雛形(`release_templates.py`・`sprint.py`)と、リリースの経路の判定(`sprint_routes.py`) | +| E4 | プランを書いた | 雛形 | 利用者か conductor(結果 JSON) | + +### 用語 + +| 用語 | 意味 | 用語集への反映 | +| --- | --- | --- | +| 宣言を探す木 | `supervise.py new` が `.ndf/` の宣言を探すディレクトリの並び。`--worktree` のパス → `--repo` → `--worktree` のパスの `/.worktrees/` より前 → 今のディレクトリの順 | 追加(`ndf-workflow`) | + +## 機能一覧 + +| # | 機能 | 誰が使うか | +| --- | --- | --- | +| F1 | まだ作っていない worktree を渡しても、`production_branch` の無いリポジトリで本番のリリースプランを組む(`new release --channel prod`) | conductor・利用者 | +| F2 | 同じ条件で、`new sprint`(normal・fast・auto)と `new close` が承認ゲート 2 と本番のステージを置く | conductor | +| F3 | 本番チャネルが決まらないとき、3 つの出所を挙げて止まる | conductor・利用者 | + +## 構成要素 + +パッケージの構成は変えない。新しいファイルを作らない。 + +### 変える要素 + +| 要素 | 責務 | +| --- | --- | +| `lib/repo.py` の `first_git_tree`(新設) | 木の並びから、存在するディレクトリで、かつメインディレクトリ(`decl.main_dir`)を解ける最初の木を返す。無ければ `None`。存在しないパスには git を起動しない | +| `lib/repo.py` の `production_branch`(変更) | 渡された木が `None` のときは既定ブランチを問い合わせず、`production_of(wt, None)` の結果(決まらない理由)を返す | +| `supervise_lib/decl.py` の `apply_decls`(変更) | 本番を使う雛形で本番チャネルが引数にも宣言にも無いときだけ、`first_git_tree(roots)` の木を `repo.production_branch` に渡す | +| `supervise_lib/decl.py` の `delivery_decl`(変更) | `delivery.build` に渡す既定ブランチを、`roots[0]` ではなく `first_git_tree(roots)` の木から解く | +| `supervise_lib/decl.py` の定数 `PRODUCTION_SOURCES`(新設) | 本番チャネルの 3 つの出所(`--production-branch`・`.ndf/worktree.json` の `production_branch`・既定ブランチ)を並べた文。エラーと案内の文面はこれを使う | +| `supervise_lib/release_templates.py` の `_validate_release_decl`(変更) | 本番チャネルが無いときのエラーの文面を `PRODUCTION_SOURCES` で組む。エラーにするのは本番の配布(引数 `dev` が偽)のときだけで、開発版(`dev` が真)は本番チャネルが無くても止めない。チャネルの判定は呼び手の `plan_release_package_plugin` が `a.channel` から `dev` を決めて渡す。この判定は変えない | +| `supervise_lib/delivery_templates.py` の `plan_promote`(変更) | 昇格に要る本番チャネルが無いときのエラーの文面を `PRODUCTION_SOURCES` で組む | +| `supervise_lib/sprint.py` の `has_prod_branch` の docstring と、本番を置かないときの案内(変更) | 案内の出所の並びを `PRODUCTION_SOURCES` で組む | +| `supervise_lib/new_args.py` の `--production-branch` の help(変更) | 既定の決め方を、宣言 → 既定ブランチの順で書く | + +`lib/delivery.py` の `load_delivery` は存在する木(呼び手の `root`)だけを受けるため変えない。`release_lib/names.py` の本番チャネルの文面は、`release-steps.py` が存在するリリースの worktree の中で読むもので、この変更の対象に含めない。 + +### 本番チャネルの値が決まるまでの呼び出し + +```mermaid +graph TD + N[supervise.py new] --> A[decl.apply_decls] + N --> D[decl.delivery_decl] + A --> F[repo.first_git_tree] + D --> F + A --> B[repo.production_branch] + D --> DB[delivery.build] + F --> M[decl.main_dir] + B --> O[repo.production_of] + DB --> O +``` + +図は本番チャネルの値が決まるまでの呼び出しだけを描く。文面の要素は値を決めないため、図に含めない。文面の要素は `PRODUCTION_SOURCES` である。それを使う `_validate_release_decl`・`plan_promote`・`sprint.py` の案内・`new_args.py` の help も文面の要素に当たる。 + +### 変えるファイルの配置 + +``` +plugins/ndf/scripts/ +├── lib/ +│ └── repo.py # first_git_tree を足す。production_branch を変える +└── supervise_lib/ + ├── decl.py # apply_decls・delivery_decl・PRODUCTION_SOURCES + ├── release_templates.py # エラーの文面 + ├── delivery_templates.py # エラーの文面 + ├── sprint.py # 案内の文面 + └── new_args.py # --production-branch の help +``` + +## 入出力の契約 + +コマンドの引数と宣言の形は変えない。変わるのは次の 2 つである。 + +| 場合 | 今 | 変更後 | +| --- | --- | --- | +| `production_branch` も引数も無く、origin の HEAD がある。`--worktree` のパスがまだ無い | `new release --channel prod` は終了コード 2。`new sprint` は承認ゲート 2 と本番を置かない | 既定ブランチを本番チャネルにしてプランを書く(終了コード 0、`status: ok`) | +| 3 つの出所のどれでも本番チャネルが決まらない | 終了コード 2。文面が出所を 2 つ(引数と宣言)挙げる | 終了コード 2 のまま。文面が出所を 3 つ挙げる | + +エラーの文面(`new release --channel prod`): + +```text +supervise.py: error: 本番の配布に要る本番のブランチが無い(--production-branch・.ndf/worktree.json の production_branch・既定ブランチ(origin/HEAD → main / master)) +``` + +## 処理の流れ + +```mermaid +sequenceDiagram + participant N as supervise.py new + participant A as decl.apply_decls + participant R as lib/repo + participant G as git + N->>A: 引数(--worktree・--repo・--production-branch) + A->>A: 宣言を読む(引数 → production_branch の宣言) + alt 本番チャネルが引数か宣言で決まった + A-->>N: 宣言の値(git を起動しない) + else 決まっていない、かつ本番を使う雛形 + A->>R: first_git_tree(宣言を探す木の並び) + loop 並びの順に + R->>R: ディレクトリが無ければ次へ(git を起動しない) + R->>G: main_dir を解く(同じ木の結果は覚えている) + end + R-->>A: 最初に解けた木、または None + A->>R: production_branch(木, 宣言) + alt 木がある + R->>G: origin/HEAD → main → master + G-->>R: 既定ブランチ、または無し + end + R-->>A: 本番チャネル、または None + end + A-->>N: 埋めた引数 + alt 本番チャネルが None で、本番のプランを組む経路(release --channel prod・昇格・fast / auto の sprint・close) + N-->>N: 3 つの出所を挙げたエラーで止まる(終了コード 2) + else 本番チャネルが None で、normal の sprint + N-->>N: 本番のステージを置かない案内を出し、プランを書く + else + N-->>N: プランを書く + end +``` + +`new sprint` と `new close` では、`sprint_routes.apply_routes` が `delivery_decl` を呼ぶ。`delivery_decl` は `first_git_tree` の同じ木から既定ブランチを解いて `delivery.build` に渡す。引数の本番チャネル(`a.production_branch`)は上の流れで既に決まっており、`build` は引数を宣言より先に使うため、2 つの集約の値が一致する(I3)。 + +## 非機能の実現方式 + +| 大項目 | 実現方式 | +| --- | --- | +| 性能・拡張性 | `apply_decls` は、本番チャネルが引数か宣言で決まったときは `first_git_tree` も既定ブランチも呼ばない(I4)。`first_git_tree` は存在しないパスに git を起動せず、存在する木では `decl.main_dir` の覚えた結果を使う(宣言を読む `overlay_keys` が同じ木の `main_dir` を先に解いている)。`delivery_decl` は今も既定ブランチを 1 回問い合わせており、問い合わせる木が変わるだけである | +| 運用・保守性 | 本番チャネルの決め方は `lib/repo.py` の `production_of` だけが持つ。木を選ぶ規則も `lib/repo.py` の `first_git_tree` 1 つに置き、`apply_decls` と `delivery_decl` が同じ関数を呼ぶ。出所の並びの文面は `PRODUCTION_SOURCES` 1 つに置く | + +## 決定の記録 + +### 決定 1: 存在しない worktree でも既定ブランチを解くため、宣言を探す木の並びから最初に存在する git の木を選ぶ + +`decl_roots` の並び(`--worktree` → `--repo` → `/.worktrees/` より前 → 今のディレクトリ)は、宣言を探す順として既に決まっている。同じ並びから最初に git として解ける木を選べば、worktree が存在するときは今と同じ木(`--worktree`)を使い、存在しないときだけメインディレクトリか `--repo` へ進む。origin の HEAD はリポジトリで 1 つのため、どの木から解いても同じ値になる。判定には `decl.main_dir` を使い、git の木であるかの判定を別に作らない。 + +`decl.main_dir(roots[0])` だけを使う形は採らない。存在しないパスではメインディレクトリも解けず、結局は並びの次の木へ進む規則が要る。 + +根拠: Value 7 / Value 6(MVV 版 5) + +### 決定 2: リリースの経路の判定と同じ値にするため、配布の判定の宣言も同じ木から既定ブランチを解く + +`delivery_decl` は今、`roots[0]` から既定ブランチを解く。雛形の側だけを直すと、存在しない worktree ではリリースの経路の判定が本番チャネルを持たないまま残り、受け入れ条件 7 が成り立たない。両方が `first_git_tree` を呼べば、同じ起動の中で同じ木を使う。 + +この結果、昇格のプラン(`plan_promote`)も同じ補完を受ける。`production_branch` を宣言せず `base_branch` が既定ブランチと違うリポジトリでは、存在しない worktree を渡した `new sprint` に昇格のステージが現れる。これは worktree を作った後の今の振る舞いと同じ形であり、退行の確認(受け入れ条件 6)に含める。 + +昇格だけを今の振る舞いに留める形は採らない。worktree の有無で昇格の有無が変わる食い違いが残る。 + +根拠: Value 7(MVV 版 5) + +### 決定 3: 文面を 1 か所で直せるようにするため、本番チャネルの出所の並びを定数 1 つに置く + +出所の並びは、リリースプランのエラー・昇格のエラー・スプリントの案内・引数の help の 4 か所に書かれている。並びを 3 つに直すとき、1 か所でも古いままだと、利用者は既定ブランチへの後退が無いと読む。定数は雛形の側(`supervise_lib/decl.py`)に置く。`lib/repo.py` の `production_of` が返す理由の文は「何が無かったか」を述べ、利用者へ「どこで直すか」を示すのは雛形の側の役割である。 + +根拠: Value 7 / Value 8(MVV 版 5) + +## テスト設計 + +テストは `plugins/ndf/scripts/tests/` に置く。一時の git リポジトリ(bare の origin と clone、origin の HEAD を `main` に設定)を作って `supervise.py new` を通す。 + +| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | +| --- | --- | --- | +| 受け入れ条件 1・I2 | 存在しない worktree のパスで `new release --channel prod` が終了コード 0・`status: ok` で、`approve`・`snapshot`・`release`・`verify` を含むプランを書く | `apply_decls` が `roots[0]` から既定ブランチを解く形に戻すと落ちる | +| 受け入れ条件 2・I1 | 上のプランで、本番の配布の確認が参照する ref が `main` である。origin の HEAD を `main` 以外にした組では、その名前になる | 既定ブランチの代わりにベースブランチか固定の `main` を入れると落ちる | +| 受け入れ条件 3 | worktree を作った後の同じコマンドのステップの並びが、受け入れ条件 1 と一致する | worktree の有無で別の規則を使う(存在しないときだけ固定の名前を入れる)ように壊すと、origin の HEAD を `main` 以外にした組で落ちる | +| 受け入れ条件 4・I5 | origin の HEAD もローカルの `main` / `master` も無いリポジトリで、`new release --channel prod` が終了コード 2 と、文面に 3 つの出所が現れる。同じリポジトリで `new release --channel dev` と `new sprint`(normal)は終了コード 0 でプランを書き、sprint は本番を置かない案内を出す | `PRODUCTION_SOURCES` から既定ブランチを外すと落ちる。本番チャネルが無いことを開発版のリリースと normal の sprint でも止めると落ちる | +| 受け入れ条件 5 | 存在しない worktree のパスで `new sprint` の normal・fast・auto が、承認ゲート 2 と本番のステージを置き、本番を置かない案内を出さない | `delivery_decl` か `apply_decls` の片方だけを `roots[0]` に戻すと落ちる | +| 受け入れ条件 6 | `production_branch` を宣言した組(既定ブランチと違う値を含む)と `--production-branch` を渡した組で、宣言か引数の値が本番チャネルになる。ai-plugins のメインディレクトリで打った `new release --channel prod` のプランが、変更の前後で同じ JSON になる | 既定ブランチを宣言より先に使うと落ちる | +| 受け入れ条件 6(昇格) | `base_branch` が既定ブランチと違い `production_branch` の無い組で、存在しない worktree と存在する worktree の `new sprint` が、同じ昇格のステージを持つ | `delivery_decl` だけを `roots[0]` に戻すと落ちる | +| 受け入れ条件 7・I3 | 同じ起動で、`a.production_branch` と `delivery_decl(a).production` が同じ値である | `delivery_decl` だけを `roots[0]` に戻すと落ちる | +| I4 | `production_branch` を宣言した組で `apply_decls` を通すと、既定ブランチを問い合わせる git が起動しない | `apply_decls` の分岐の条件を外し、常に `first_git_tree` を呼ぶと落ちる | + +## 設計の結果 + +| 課題 | 扱い | 取り込み先 | 触るファイル | +| --- | --- | --- | --- | +| #2097 | 実装する | — | `plugins/ndf/scripts/lib/repo.py`、`plugins/ndf/scripts/supervise_lib/decl.py`、`plugins/ndf/scripts/supervise_lib/release_templates.py`、`plugins/ndf/scripts/supervise_lib/delivery_templates.py`、`plugins/ndf/scripts/supervise_lib/sprint.py`、`plugins/ndf/scripts/supervise_lib/new_args.py`、`plugins/ndf/scripts/tests/` | + +## 未確認のまま残ること + +| 項目 | 内容 | +| --- | --- | +| 今のディレクトリが別のリポジトリのとき | `--repo` も `/.worktrees/` も無く、`--worktree` のパスが存在しないときは、今のディレクトリの木から既定ブランチを解く。今のディレクトリが別のリポジトリなら、そのリポジトリの既定ブランチが入る。並びの順は要求の前提 2 のとおりで、この変更では変えない。conductor は `--worktree` に `.worktrees/` の下のパスを渡すため、この組み合わせは手で打ったときだけ起こる | +| fast / auto の `new sprint` の残りの前提 | fast / auto のプランを組むには、MVV の承認と `.ndf/pace.json` の許可も要る。受け入れ条件 5 のテストの組み立ては実装(`tdd-cycle`)で走らせて決める | diff --git a/issues/issue-2097-requirements.md b/issues/issue-2097-requirements.md new file mode 100644 index 000000000..fa670ddb6 --- /dev/null +++ b/issues/issue-2097-requirements.md @@ -0,0 +1,142 @@ +# #2097: release の雛形が本番のブランチを既定ブランチへ後退させず、worktree.json に production_branch が無いと本番のプランを組めない + +正は課題の本文(#2097)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 + +## 依頼(原文) + +> ## 何を見つけたか +> +> `.ndf/worktree.json` に `production_branch` が無く、`.ndf/supervise.json` に `release.form: package-plugin` があるリポジトリで、`supervise.py new release --version 1.0.0 --prs 1 --channel prod --worktree <根>/.worktrees/release/v1.0.0 --out <パス>` を打つと、`supervise.py: error: 本番の配布に要る本番のブランチが無い(--production-branch か .ndf/worktree.json の production_branch)` で止まる(2026-10-24 に一時ディレクトリで再現)。 +> +> 同じ宣言で、リリースの経路の判定(`lib/delivery.py`)は本番のブランチを既定ブランチ(origin/HEAD)へ後退して決め、経路を雛形(`template`)と判定する。 +> +> ## どこで見つけたか +> +> - `plugins/ndf/scripts/supervise_lib/release_templates.py` の `_validate_release_decl`: `a.production_branch` だけを見る +> - `plugins/ndf/scripts/supervise_lib/decl.py:257`: `a.production_branch` は引数か `worktree.json` の `production_branch` だけから決まる +> - `plugins/ndf/scripts/lib/delivery.py:132-136`・`lib/repo.py:136` の `production_branch`: `production_branch` が無ければ既定ブランチ(origin/HEAD)を使う +> - 用語集の「本番」の行は「`production_branch`(無ければ既定ブランチ)」と書く +> +> #1914 の設計(normal の `new sprint` が本番のプランを書き出す)で、本番のプランを組むときに見つけた。 +> +> ## なぜこの変更の範囲外なのか +> +> #1914 の受け入れ条件は、`pace: normal` の `new sprint` が承認ゲート 2 と本番のステージを書き出すことで、本番のブランチの決め方は含まない。この食い違いは fast / auto と単独の `new release --channel prod` にも同じ形である。#1914 の設計は、本番のブランチ(`a.production_branch`)が無いときは本番のステージを置かない形で、今の振る舞いを変えずに避ける。根拠: Value 6(MVV 版 2。根本原因の層で直すため、本番のブランチの決め方を 1 か所にそろえる変更として分ける) +> +> ## 直さないと何が起きるか +> +> `worktree.json` に `production_branch` を書かず既定ブランチを本番にしているリポジトリでは、経路は雛形と判定されるのに本番のプランを雛形で組めず、`new release --channel prod` も fast / auto の `new sprint` も止まる。#1914 の後は normal の `new sprint` が本番のステージを置かず、本番を手で配る手当てが残る。 +> +> ## 起票者の推定 +> +> - 雛形の本番のブランチは、経路の判定と同じく既定ブランチへ後退するのが正しい(推した出所: `lib/repo.py:137` の docstring と用語集の「本番」の行) +> +> ## 修正レイヤー +> +> `supervise_lib/decl.py` の `apply_decls`(255〜256 行)は `lib/repo.production_branch`(`worktree.json` の `production_branch` → 既定ブランチ)を呼ぶ形になっている。ただし既定ブランチを解く木に `decl_roots` の先頭(`--worktree` のパス)を渡すため、まだ作っていない worktree では既定ブランチ(origin/HEAD)を解けず、同じエラーで止まる。直す場所は、既定ブランチをメインディレクトリ(存在する木)から解く形にすることである。ai-plugins は `production_branch` を宣言しているため、この経路を通らない。 +> +> 2026-10-31 に一時のリポジトリで再現した(`{"base_branch":"main"}` だけで止まり、`production_branch: main` を足すと 12 ステップのプランが ok)。2026-11-04 にも同じ形(origin/HEAD は main、`.ndf/supervise.json` は `release` の `form`・`plugin`・`runtimes`)で同じエラーになることを確かめた。 +> +> ## 由来 +> +> issue #1914 + +(書き手: 起票者(#1914 の設計の途中で範囲外の課題として起票した AI)、日付: 2026-10-24 起票、「修正レイヤー」の節は 2026-10-31・2026-11-04 の追記。利用者の指示の原文は含まない) + +## 起票者の推定 + +| # | 条件 | 推した出所 | この要求での扱い | +| --- | --- | --- | --- | +| S1 | 雛形の本番のブランチは、リリースの経路の判定と同じく既定ブランチへ後退するのが正しい | `plugins/ndf/scripts/lib/repo.py` の `production_of` の docstring と、用語集の「本番」の行 | 受け入れ条件 1〜3・5 | +| S2 | 直す場所は、既定ブランチをメインディレクトリ(存在する木)から解く形にすることである | 本文の「修正レイヤー」の節(2026-10-31・2026-11-04 の再現) | 受け入れ条件 1・4。直し方の形は設計で決める | +| S3 | `new release --channel prod` だけでなく、fast / auto と normal の `new sprint` も同じ原因で本番を置けない | 本文の「なぜこの変更の範囲外なのか」「直さないと何が起きるか」 | 受け入れ条件 5 | + +## 目的 + +- `.ndf/worktree.json` に `production_branch` を書かず、既定ブランチを本番にしているリポジトリで、`supervise.py new` が本番の配布を含むプランを組めるようにする +- 本番のブランチの決め方を、リリースの経路の判定(`lib/delivery.py`)とプランの雛形(`supervise_lib`)で同じ結果にそろえる(Value 6・Value 7) + +## 前提 + +- 前提 1: 本番のブランチの決め方の正本は、今ある `lib/repo.py` の `production_of`(`production_branch` の宣言 → 既定ブランチ)とする。順序は変えない +- 前提 2: 既定ブランチは、まだ作っていない worktree のパスではなく、存在する git の木(`--repo`、`--worktree` のパスの `/.worktrees/` より前、今のディレクトリの順で最初に git として読める木)から解く。メインディレクトリから解いた既定ブランチと、そのリポジトリの worktree から解いた既定ブランチは同じ値になる(origin の HEAD はリポジトリで 1 つ) +- 前提 3: 本番のブランチが既定ブランチに決まっても、リリースプランは承認ゲート 2(`approve` のステップ)を持ったままにする。この変更は承認の要否を変えない +- 前提 4: 2026-11-05 に一時のリポジトリで確かめた今の振る舞いを基準にする。worktree を作る前は上の「依頼(原文)」と同じエラーで止まり、`git worktree add` で worktree を作った後は同じコマンドが 13 ステップのリリースプランを書く + +## 対象範囲 + +含む: +- `supervise.py new` の `release`・`sprint`・`close`(本番を使う雛形)で、本番のブランチを既定ブランチへ後退させる経路が、まだ作っていない worktree でも働くこと +- 本番のブランチが決まらないときのエラーの文面に、3 つの出所(`--production-branch`、`.ndf/worktree.json` の `production_branch`、既定ブランチ)をそろえて書くこと + +含まない: +- 本番のブランチの決め方の順序と出所を変えること(前提 1) +- 昇格のプラン(`delivery_templates.py` の本番チャネルの決め方)の振る舞いを変えること。同じ既定ブランチの補完を受けるかは設計で確かめ、変わる場合は受け入れ条件 6 の退行の確認に含める +- ai-plugins の `.ndf/worktree.json` の `production_branch` を消すこと(C7) +- 承認ゲート 2 の置き方・要否を変えること +- 型・クラスの追加と変更(関数を 1 つ足し、呼び出し元の引数を変えるだけで、クラス図の対象が無い) + +## ドメインイベント + +| # | イベント | 引き金 | 失敗したとき | 順序の前提 | +| --- | --- | --- | --- | --- | +| E1 | `supervise.py new <種類>` が起動された | 利用者か conductor のコマンド | — | — | +| E2 | 宣言(`.ndf/worktree.json`・`.ndf/supervise.json`)を読んだ | E1 | 宣言が壊れていれば宣言のエラーで止まる(今の振る舞い) | E1 | +| E3 | 本番のブランチが決まった(引数 → `production_branch` → 存在する木から解いた既定ブランチ) | E2 で種類が本番を使う雛形のとき | 3 つのどれでも決まらなければ、3 つの出所を挙げたエラーで止まる(受け入れ条件 4) | E2。worktree が作られていることを前提にしない | +| E4 | プランを書いた | E3 | 書けなければ今のエラーの経路 | E3 | + +## 受け入れ条件 + +- [ ] 受け入れ条件 1: origin の HEAD が `main` を指し、`.ndf/worktree.json` が `{"base_branch":"main"}` だけで、`.ndf/supervise.json` の `release` に `form: package-plugin`・`plugin`・`runtimes` があるリポジトリで、まだ作っていない worktree のパス(`<メインディレクトリ>/.worktrees/release/v1.0.0`)を `--worktree` に渡して `supervise.py new release --version 1.0.0 --prs 1 --channel prod --out <パス>` を打つと、終了コード 0・結果 JSON の `status` が `ok` で、`approve`・`snapshot`・`release`・`verify` のステップを持つリリースプランを書く +- [ ] 受け入れ条件 2: 受け入れ条件 1 のプランで、本番の配布の確認に使うブランチ(`verify` などが参照する本番の ref)が `main` である +- [ ] 受け入れ条件 3: 受け入れ条件 1 のリポジトリで worktree を作った後に同じコマンドを打っても、受け入れ条件 1 と同じステップの並びのプランを書く(worktree の有無で結果が変わらない) +- [ ] 受け入れ条件 4: origin の HEAD もローカルの `main` / `master` も無く、`production_branch` も `--production-branch` も無いリポジトリでは、`new release --channel prod` が今と同じく止まり、エラーの文面が `--production-branch`・`.ndf/worktree.json` の `production_branch`・既定ブランチの 3 つを挙げる +- [ ] 受け入れ条件 5: 受け入れ条件 1 のリポジトリで、まだ作っていない worktree のパスを渡した `supervise.py new sprint`(`pace: normal`・`fast`・`auto` のそれぞれ)が、承認ゲート 2 と本番のステージを置いたプランを書き、「本番のブランチ(…)が無いため、承認ゲート 2 と本番は置いていない」の案内を出さない +- [ ] 受け入れ条件 6: 退行しないこと。`production_branch` を宣言したリポジトリ(宣言の値が既定ブランチと違う場合を含む)では宣言の値を、`--production-branch` を渡したときは引数の値を本番のブランチに使う。ai-plugins のメインディレクトリで受け入れ条件 1 と同じ `new release --channel prod` を打った結果のプランが、変更の前後で同じ JSON になる +- [ ] 受け入れ条件 7: 受け入れ条件 1 の宣言で、リリースの経路の判定(`lib/delivery.py`)が使う本番のブランチと、リリースプランが使う本番のブランチが同じ値になる + +## 非機能の条件 + +| 大項目 | 条件 | +| --- | --- | +| 性能・拡張性 | `production_branch` を宣言したリポジトリでは、既定ブランチを解く git の起動を足さない(今の `repo.production_branch` の早期の戻りを保つ) | +| 運用・保守性 | 本番のブランチの決め方を `lib/repo.py` の 1 か所に置いたままにし、雛形の側に別の決め方を足さない(Value 7) | + +## 影響 + +| 対象 | 影響 | +| --- | --- | +| 公開インタフェース | 変わる。`production_branch` を宣言していないリポジトリで、今は止まる `new release --channel prod` と `new sprint` が本番を含むプランを書くようになる。引数と宣言の形は変わらない。エラーの文面が出所を 3 つ挙げる | +| データ | 無し | +| 既存の振る舞い | `production_branch` を宣言したリポジトリ(ai-plugins を含む)は変わらない(受け入れ条件 6) | + +## 検証手段 + +| 項目 | 手段 | +| --- | --- | +| テスト | `uv run --frozen --project . --all-extras pytest plugins/ndf/scripts/tests -q -n 4`。受け入れ条件 1〜7 は一時の git リポジトリ(bare の origin と clone、`git remote set-head origin main`)を作るテストで確かめる | +| 静的解析・型検査 | リポジトリの CI と同じ検査(`python3 scripts/check-skill-frontmatter.py` などは変更範囲に当たるものだけ) | +| 手動確認 | 無し | + +## 前提とする取り決め + +| 項目 | 参照先 / 決めたこと | +| --- | --- | +| プロジェクト構造 | `AGENTS.md` の「マーケットプレイスの構造」。変更は `plugins/ndf/scripts/` の中に置く。安定版の経路(hook・結果 JSON の契約に当たる)で、実験版の置き場は使わない | +| コーディング規約 | `AGENTS.md` の「最小限のコード実装」。外部コマンド(git)の挙動は書く前に実行して確かめる | +| テスト戦略 | 一時の git リポジトリを使う結合の階層のテストで `supervise.py new` を通す。`.md` の文言を照合するテストは書かない | + +## 境界 + +| 区分 | 内容 | +| --- | --- | +| 常に行う | 既存テストの実行、受け入れ条件ごとのテストの追加 | +| 確認してから行う | 本番のブランチの決め方の順序を変えること(承認ゲート 1 で人が見る) | +| 行わない | `.ndf/` の宣言の書き換え(C7)、承認ゲート 2 の置き方の変更、範囲外のリファクタリング | + +## 未決 + +| 項目 | 誰が決めるか | 期限 | +| --- | --- | --- | +| 既定ブランチを解く木の選び方(`decl_roots` の並びから最初の git の木を選ぶか、`lib/decl.py` の `main_dir` を使うか) | 設計(`design`) | 設計 Pull Request | +| 昇格のプランが同じ補完を受けるか(「含まない」の 2 行目) | 設計(`design`) | 設計 Pull Request | diff --git a/issues/issue-2199-design.md b/issues/issue-2199-design.md deleted file mode 100644 index b707e342d..000000000 --- a/issues/issue-2199-design.md +++ /dev/null @@ -1,466 +0,0 @@ -# factory/: 見送ったリファクタリングの提案 20 件を片付け、同じ役割の関数を 1 か所へ寄せる(#2199) - -## 目的 - -- **今どうなっているか**: #2170 の cross-refactoring(PR #2192)は、`factory/` のリファクタリングの提案 20 件(R01〜R20)を時間(`budget`)で見送った。20 件はどの課題にも残っていない。その後に重複が広がった。2026-11-05 の `develop`(86436dace)では、`requireEnv` が 5 か所、ログインの確認が 12 画面、失敗を画面の文へ変える `onFailure(err)` が 23 行にある -- **誰に効くか**: 子課題(#2171・#2172・#2173・#2174・#2189)の実装者と、その cross-refactoring。子課題は同じ `factory/api/`・`factory/web/` に足していく。重複を写して広げ、同じ提案を毎回出し直しては時間で見送る -- **変えると何が成り立つか**: 20 件のそれぞれが「適用した」か「採らない(理由つき)」に決まり、同じ役割の関数の定義が `factory/` の中で 1 か所になる。HTTP の応答・OpenAPI の契約・DynamoDB の行の形・画面の表示は変わらない - -## 適用範囲 - -- **働く範囲**: このリポジトリの `factory/api/src`・`factory/web/src` と `scripts/tests/test_check_markdown_links.py` だけ。配布物(`plugins/`)には届かない -- **プロジェクトごとに違うもの**: 無し(`factory/` の内部の関数の置き場と引数の形だけを変える) -- **当たるモード**: `standard`。要求と受け入れ条件があり、対象には既存のテストがある。vitest は `factory/api/src`・`factory/web/src` の 37 ファイル、pytest は `scripts/tests/test_check_markdown_links.py` の 46 件である - -## あるべき姿の根拠 - -| 根拠 | 種類 | 何を示すか | -| --- | --- | --- | -| PR #2192 のリファクタリング計画のコメント「見送った提案」の 20 行(理由はすべて `budget`)と、提案の結果ファイル(`/tmp/ndf-worktrees/devbasex--ai-plugins/rf2192/work/.cross_refactoring/{claude,codex,kiro}-propose-rf2192-result.json`)の `rationale` と `plan` | 実測(記録の読み取り) | 各件の兆候と、提案した手順。この設計の判定表の出所 | -| 2026-11-05 の `develop`(86436dace)で `grep -rn "function requireEnv" factory/api/src` が 5 行(`handlers/api.ts`・`handlers/github-oidc.ts`・`handlers/ingest.ts`・`handlers/issue-requests.ts`・`runs/deps.ts`) | 実測 | PR #2192 の後に写しが 3 つ増えた(要求の前提 1) | -| 同じ日の `grep -rn 'Navigate to="/login"' factory/web/src/pages` が 12 行、`factory/web/src` の `onFailure(err)` が 13 ファイルで 23 行 | 実測 | 画面の重複が PR #2192 の後に足された画面(スプリント・ループ・委任・バックログなど)へ写っている | -| 同じ日の `factory/web/src/App.tsx` が、ログインの後の画面をすべて 1 つの枠(`AppShell` の layout route)の下に置く | 実測(コードの読み取り) | ログインの確認を 1 か所に置ける場所が既にある(R14) | -| 同じ日の `factory/api/src/github-oidc/app.ts` に `clientCredentials`・`exchangeGithubCode`・`issueIdToken` があり、POST /token の本体は 21 行 | 実測 | R07 の兆候は PR #2192 の I-001 で消えている(要求の前提 2) | -| `factory/api/src/index.ts` が `./handlers/api.ts`(`depsFromEnv`・`createApiApp`)と `./handlers/ingest.ts`(`progressStoreFromEnv`)を読み、どちらも読み込みの時点で `export const handler = ...` を組み立てる。`runs/deps.ts` も `handlers/ingest.ts` を読む | 実測(コードの読み取り) | 公開のエントリポイントが Lambda のエントリポイントを読む向き(R02) | -| 着手前の `uv run --frozen --project . --all-extras pytest scripts/tests/test_check_markdown_links.py -q` が `46 passed`(2026-11-05) | 実測 | 受け入れ条件 5 の「変更の前の件数」 | - -要求と受け入れ条件は #2199 の本文にある(コピーは [issue-2199-requirements.md](issue-2199-requirements.md))。この文書は「どう作るか」だけを扱う。 - -## ドメインモデル - -### コンテキスト - -| コンテキスト | 何の語が 1 つの意味に決まるか | -| --- | --- | -| Neural Development Factory(`ndf-factory`) | アカウント・席・履歴・実行の形態・プラン(`factory/api/src/accounts/`)、GitHub の仲立ち(`github-oidc/`)、画面(`factory/web/`) | -| NDF の cross-refactoring(`ndf-cross-refactoring`) | 見送った提案・リファクタリング計画(R01〜R20 の出所) | - -関係は**順応者(Conformist)**である。この変更は `ndf-cross-refactoring` が残した見送った提案を、その語と兆候の分類(duplication など)のまま受け取り、`ndf-factory` の構造を直す。`ndf-cross-refactoring` の側は何も変えない。 - -### 集約 - -**集約は変えない。** 持ち主と境界をそのまま保ち、持ち主の内部の関数の置き場だけを変える。 - -| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | -| --- | --- | --- | --- | --- | -| 利用者 | `AccountService`(`accounts/service.ts`)。保存は `AccountStore` の契約を通す | 利用者(`UserRecord`) | — | 利用者の ID(Cognito の `sub`)・IdP の種類 | -| アカウント | `AccountService`(`accounts/service.ts`)。保存は `AccountStore` の契約を通す | アカウント(`AccountRecord`) | 席(`SeatRecord`) | プラン・実行の形態・席の種類・履歴の行(`HistoryRecord`)・作成の冪等の鍵の行 | - -利用者とアカウントは別の集約である(確定仕様 `docs/specifications/ndf-factory-accounts-and-plans.md` の「集約」)。2 つを 1 つのトランザクションで書くのは初めてのログインで両方を作るときだけで、それ以外の利用者の読み書きはアカウントの操作に含めない。保存の契約(`AccountStore`)が両方の集約の保存を受け持つのは今のままで、この変更は契約の置き場(`accounts/port.ts`)だけを変える。 - -### 不変条件 - -この変更が守る条件である。I1〜I4 は既存の振る舞いをそのまま保つ条件、I5・I6 はこの変更で新しく成り立たせる構造の条件である。 - -| # | 集約 | 条件 | 破れたときの扱い | -| --- | --- | --- | --- | -| I1 | — | HTTP の応答(状態コード・本文・エラーの文言)と画面の表示が変更の前と同じ | その件を戻し、表の扱いを「採らない(適用すると振る舞いが変わる)」にする | -| I2 | アカウント | DynamoDB へ送る `TransactItems` の項目の並び(業務の項目 → 履歴 → 作成の冪等の鍵の行)・キー・属性・条件式・プレースホルダの名前(`:plan`・`:mode` など)が変更の前と同じ | その件を戻す(C3 に当たる変更を入れない) | -| I3 | アカウント | どの書き込みも履歴の行を同じトランザクションで足す(既存の I20)。条件の失敗は `ConditionFailedError`、それ以外の失敗は今と同じ型で呼ぶ側へ届く | その件を戻す | -| I4 | — | 認証(`auth/verify.ts`・`github-oidc/`)と認可(`auth/resolve.ts`)の判定が変更の前と同じ | その件を戻す(C2 に当たる変更を入れない) | -| I5 | — | 「適用した」件の同じ役割の関数・定数の定義が `factory/` の中で 1 か所にある | 残った写しを寄せるまで実装の Pull Request を出さない | -| I6 | — | `factory/api/src` の `handlers/` の外のコード(`*.test.ts` を除く。テストの補助の `testing/app.ts` は除かない)が `handlers/` を import しない。`handlers/` の中のファイルどうしも互いを import しない | import を移した先へ替えるまで出さない | - -### ドメインイベント - -要求の E1〜E6 を引き継ぐ。どれも実装の工程の中の出来事で、受け手は次の手順と実装の Pull Request の本文である。 - -| # | イベント | 発生元 | 受け手 | -| --- | --- | --- | --- | -| E1 | 20 件のそれぞれが今の `develop` に残っているかを確かめた | 実装担当(この設計の判定表を実装の開始時の `develop` で確かめ直す) | E2・E4 | -| E2 | 同じ役割の写しを PR #2192 の後のファイルから拾った | 実装担当(判定表の「写し」の列) | E4・E6 | -| E3 | 着手前のテストが通った | 実装担当 | E4(基準の件数) | -| E4 | 各件を適用したか、採らないと決めた | 実装担当 | E5・E6 | -| E5 | 全体の検査が通った | 実装担当 | E6 | -| E6 | 20 件と写しの扱いを 1 つの表として Pull Request の本文に残した | 実装担当 | 承認ゲート 2 で見る人 | - -### 用語 - -| 用語 | 意味 | 用語集への反映 | -| --- | --- | --- | -| 見送った提案 | cross-refactoring のリファクタリング計画の「見送った提案」の表の 1 行。ここでは PR #2192 の R01〜R20 | 変えない(`ndf-cross-refactoring` の既存の語のまま) | -| 同じ役割の写し | R01〜R20 の対象と同じ処理・同じ判断を、別の場所に書き写したもの。要求の S3 が対象に含める | 登録済み(`ndf-factory`) | -| 保存の契約 | `AccountStore` の interface と、その引数・失敗の型(`AccountPatch`・`AccountUpdate`・`ConditionFailedError`)。DynamoDB とメモリの 2 つの実装が満たす | 登録済み(`ndf-factory`) | - -## 機能一覧 - -利用者から見た振る舞いは変えない。この変更が作る「使える形」を並べる。 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | 20 件の扱い(適用した・採らない)と写しの扱いを 1 つの表で読む | 承認ゲート 2 で見る人 | -| F2 | 環境変数の読み取り・API のエントリポイントの組み立て・保存の契約・再試行・ログインの確認・失敗の表示を、1 か所の定義から使う | 子課題(#2171・#2172・#2173・#2174・#2189)の実装者 | -| F3 | 後の cross-refactoring が R01〜R20 と同じ提案を出さない | cross-refactoring の提案の担当 | - -## 20 件の判定表 - -**この表が実装の Pull Request の本文の表(受け入れ条件 1)の下書きになる。** 「今の状態」は 2026-11-05 の `develop`(86436dace)で確かめた。実装の開始時に `develop` が進んでいれば、E1 として確かめ直す。 - -| # | 対象 | 今の状態 | 扱い | 直し方(1 か所にまとめる場所) | 同じ役割の写し(S3) | -| --- | --- | --- | --- | --- | --- | -| R01 | `routes/teams.ts#registerTeams` | 席の種類の検証と文言が `teams.ts` に直書き、`accounts.ts` に定数 `SEAT_TYPE_MESSAGE` | 適用する | `accounts/service.ts` の `validateSeatType(value)`(既存の `validateTeamName` の隣)。文言の定数も移す | `routes/accounts.ts` の `addSeat`・`changeSeat` | -| R02 | `index.ts#depsFromEnv` | `index.ts` が `handlers/api.ts`・`handlers/ingest.ts` を読み、`createApiApp` も `handlers/api.ts` から再 export する | 適用する | 決定 1 | `runs/deps.ts` → `handlers/ingest.ts`、`handlers/issue-requests.ts` → `handlers/ingest.ts` | -| R03 | `github-oidc/app.ts#githubJson` | `/token` と `/userinfo` が `/user` の取得と ID の検証を 2 回書く | 適用する | `createGithubOidcApp` の中の `githubUser(accessToken)`(取れなければ `null`) | 無し | -| R04 | `accounts/store.ts#DynamoAccountStore.transact` | 6 つの書き込みが履歴の行の展開を書く。`createTeam` は履歴の後に作成の冪等の鍵の行を足す | 適用する | 決定 3(R09 と同じ直し方) | 無し | -| R05 | `web/pages/TeamPage.tsx#TeamPage` | 席の種類の一覧 `SEAT_TYPES` は `parts.tsx` にあり、そこから `