From c5d26e1c83a9de8927766d0c06d9ca5adb8d9235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=88=E8=89=AF=20=E8=8B=B1=E6=A8=B9?= Date: Tue, 27 Oct 2026 19:48:15 +0900 Subject: [PATCH 1/8] =?UTF-8?q?check-signoff.py:=20=E7=85=A7=E5=90=88?= =?UTF-8?q?=E3=81=AE=E7=AF=84=E5=9B=B2=E3=82=92=202=20=E3=81=A4=E3=81=AE?= =?UTF-8?q?=E6=89=BF=E8=AA=8D=E3=81=8C=E8=A6=8B=E3=81=9F=E4=B8=AD=E8=BA=AB?= =?UTF-8?q?=EF=BC=88shared/=E3=83=BBrequirements/=E3=83=BBspec/=E3=83=BBre?= =?UTF-8?q?moved.csv=EF=BC=89=E3=81=AB=E5=BA=83=E3=81=92=E3=82=8B=EF=BC=88?= =?UTF-8?q?#375=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 見積りの根拠(requirements/・spec/)が承認と違う(#367)・予算側だけの v1 の removed.csv や範囲の外のファイルの行(#372)を 一致と言わない。内部の Q&A 表(spec/qa-dev.csv)と feasibility/・proposal/ は外す。仕様の穴として推奨で決めた(確認は #478)。 Fixes #375 Fixes #367 Fixes #372 Co-Authored-By: Claude Opus 5.5 --- .claude-plugin/marketplace.json | 2 +- CLAUDE.md | 2 +- docs/plans/issue-375.md | 118 ++++++++++++++++++ plugins/spec-loop/.claude-plugin/plugin.json | 2 +- plugins/spec-loop/README.md | 2 +- plugins/spec-loop/references/signoff-tags.md | 16 ++- plugins/spec-loop/references/tree.md | 2 +- plugins/spec-loop/scripts/check-signoff.py | 54 +++++--- plugins/spec-loop/skills/feasibility/SKILL.md | 4 +- plugins/spec-loop/skills/proposal/SKILL.md | 4 +- plugins/spec-loop/skills/req-loop/SKILL.md | 4 +- plugins/spec-loop/skills/scaffold/SKILL.md | 4 +- plugins/spec-loop/skills/spec-review/SKILL.md | 4 +- scripts/test-build-proposal.py | 12 +- scripts/test-check-signoff.py | 108 ++++++++++++---- 15 files changed, 267 insertions(+), 71 deletions(-) create mode 100644 docs/plans/issue-375.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 448012d5..96077ead 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -41,7 +41,7 @@ { "name": "spec-loop", "source": "./plugins/spec-loop", - "version": "0.26.0", + "version": "0.27.0", "author": { "name": "hdknr" }, "description": "要求側のループ(要求確定・仕様レビュー・実現性とコスト・提案書)と、dev-loop への引き渡しを担うプラグイン。現在のスキルは req-loop(Q&A 表への起票(推奨と理由つき)・回答の反映・レビュー会用 PPTX の生成で、利用者の回答待ちで止まる)と spec-review(開発者向けの仕様を書き下し、書き込みツールを持たない 3 つの判定役に別ティアでレビューさせ、利用者に見える結果が変わる指摘は共有の Q&A 表に差し戻し、費用・運用だけが変わる選択は内部の Q&A 表に記録し、範囲の外の場面と実現の手段の指摘は仕様に足さない。抜ける条件を満たさなければ抜けたと報告しない)と feasibility(規模から見積りとスケジュールを更新し、ガントチャートをスクリプトで生成し、費用付きの質問を Q&A 表に起票する。予算責任者の合意待ちで止まる)と proposal(同じ版の 2 つのサインオフのタグが同じ共有の範囲(docs/v/shared/ ほか)を指すかをスクリプトで照合し、一致し、かつ暫定の回答が残っていないときだけ提案書と仕様書(PPTX も選べる)を生成し、利用者にまだ確かめていない暫定の決定を合意と分けて載せる。v2 以降は前の版との比較を載せた変更の提案書になる。決裁者の承認待ちで止まる)と scaffold(確定した仕様を中間表現 spec_ir.json に書き起こし、OpenAPI・型・実装が無いと失敗する Red テストと dev-loop に渡す Issue の本文をスクリプトで生成して、1 機能 = Issue 1 件で登録する。v2 以降は前の版と比べて機能を追加・変更・削除に分け、変わった機能だけを Issue にする。Issue の登録で止まる)の 5 本" } diff --git a/CLAUDE.md b/CLAUDE.md index 321cc582..07f841e5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,7 +67,7 @@ Claude Code のセットアップガイドを mkdocs で構築・公開するプ - `scripts/schedule-to-gantt.py` - `schedule.csv` からガントチャート(SVG)を作る(**同じ表から同じバイト**。 作れなければ**前の出力を消す**。テストは `scripts/test-schedule-to-gantt.py`) - `scripts/check-signoff.py` - 同じ版の 2 つのサインオフのタグ(`signoff/v/req-`)の時点で、 - 共有の範囲(v1〜v の `shared/` と `removed.csv`)が一致するかの照合(#262。**どちらかの時点に比べる中身が + 2 つの承認が見た中身(v1〜v の `shared/`・`requirements/`・`spec/`(`qa-dev.csv` を除く)と `removed.csv`。#375)が一致するかの照合(#262。**どちらかの時点に比べる中身が 無ければ 2**——`git diff` は比べる中身が無くても差分なしを返す。**要求確定のタグの時点に `暫定反映` の Q&A が 残れば 3**(#325。Q&A の行だけを版の順に重ね、重ね方はテストが `merge-versions.py` と突き合わせる。 **要求確定のタグの時点の `removed.csv` の共有の Q&A の行(内部の層の `D-` は除く)について `merge` が拒否する配置は 2——判定は `merge-versions.py` の `check_removed`**(#338)。 diff --git a/docs/plans/issue-375.md b/docs/plans/issue-375.md new file mode 100644 index 00000000..4f6810e4 --- /dev/null +++ b/docs/plans/issue-375.md @@ -0,0 +1,118 @@ +# Issue #375 — check-signoff が比べる範囲の外で、承認と合意の中身が食い違っても一致を返す(#367・#372 のまとめ) + +`check-signoff.py` が比べる範囲を、**2 つの承認が見た中身**に揃える。見積りの根拠(`requirements/`・`spec/`)と、 +v1 の `removed.csv`・`shared/` の外のファイルの行を範囲に入れ、2 つのタグの時点で違えば **1(一致しない)** にする。 + +**不在の周**である——出所は `/dev-loop:cruise 474 --absent`(2026-10-27、利用者が会話で「-absent でまわします」と指示)。 +台帳 `issue-474` の確かめた線: `仕様の穴 = 推奨で決めて起票`・`直す線 = 既定`・`マージ = 関門を通ればマージ`・`周の途中の課題 = 台帳に足して直す` +(出所: cruise の確かめた線(台帳 `issue-474`))。**このリポジトリのマージはサイトを再デプロイするので `マージ` の線は効かない**(#476)——PR まで作って止まる。 + +入場の記録: `find-cycle.py 375` の 1 回目(worktree を作る前)が「当たったものがある」と出したのを確かめずに `prepare-worktree.sh` を走らせた。 +作った後の 2 回目では、当たったのは作ったばかりのブランチ `issue/375-signoff-scope` だけで、計画ファイル・計画を足したコミット・`#375` を含むコミットメッセージは無かった +(`prepare-worktree.sh` は同じ番号の worktree・ブランチがあれば 3 で止まるが、0 で作った)。前の周は無いと判断して新規の周とした。 + +## 変更範囲 + +触る: + +- `plugins/spec-loop/scripts/check-signoff.py` — 比べる範囲(`compare_scope`)・`removed.csv` の比べる行(`removed_scope_rows`)・出力の範囲の言い方・docstring +- `scripts/test-check-signoff.py` — #367・#372 の試料、範囲の外の陽性対照、期待の文言、変異 +- `scripts/test-build-proposal.py` — 「比較は要求のタグの時点から作る」の試料(2 つのタグの間に足すものを範囲の外へ移す) +- `plugins/spec-loop/references/signoff-tags.md` — 「2 つの承認が同じ範囲を見ていたか」の比べる場所 +- `plugins/spec-loop/references/tree.md` — 置き方の決まりの「2 つのサインオフが同じ範囲を見ていたかは `shared/` の中身で確かめる」 +- `plugins/spec-loop/README.md` — `check-signoff.py` の行 +- `CLAUDE.md` — `check-signoff.py` の行 +- `plugins/spec-loop/.claude-plugin/plugin.json`・`.claude-plugin/marketplace.json`・`plugins/spec-loop/skills/*/SKILL.md` — 版(0.26.0 → 0.27.0。0.x なので振る舞いの変更も minor) +- `docs/plans/issue-375.md` — この計画 + +触らない: + +- 暫定の行の判定(`provisional_rows`)と、要求確定のタグの時点の配置の検査(#325・#338・#348) +- `build-proposal.py`(`resolve` の結果をそのまま使う) +- description(JSON 2 箇所と proposal の frontmatter)——「同じ共有の範囲(docs/v/shared/ ほか)」は「ほか」で外れない + +### 同じことを述べている箇所の数 + +「比べる範囲」: 本体の docstring(概要・守らないもの)・`signoff-tags.md`・`tree.md`・`README.md`・`CLAUDE.md` の 5 箇所。 +分けない——同じ決定の写しで、片方だけ直すと範囲の説明が食い違う。 + +## 仕様の穴(不在の線で推奨により決めた) + +Issue の「決めること」A・B・C。**線は `仕様の穴 = 推奨で決めて起票`(cruise の確かめた線(台帳 `issue-474`))**。確認の Issue: **#478**(台帳の `decisions` に記録)。 + +- **A(#367): `requirements/`・`spec/` の差分の扱い** + - 選択肢: (a) 比べる範囲に入れ、違えば 1 / (b) 範囲は今のまま、違えば警告(0 のまま)/ (c) 別の終了コード + - **決めた値: (a)**。`spec/qa-dev.csv`(内部の層の Q&A 表)は外す。 + - 理由: 見積りは `requirements/`・`spec/` から数える(feasibility の手順 2)ので、そこが違えば予算の合意は承認と別の中身を見ている。 + (b) は `build-proposal.py` が終了コードしか見ないので、警告が提案書の手前で消える。(c) は呼ぶ側を全部直すことになる。 + `qa-dev.csv` は feasibility が 2 つのタグの間に正当に書く(費用の行)うえ、#339 で「内部の層はサインオフを割らない」と決めている。 +- **B(#372): 予算側だけにある、`merge` が拒否する配置** + - 選択肢: (a) 比べる範囲に `docs/v1/removed.csv` と、範囲の中のファイル・機能の行を入れる / (b) 予算側にも配置の検査を当てる(2) + - **決めた値: (a)**。`removed.csv` で比べる行は「共有の層の Q&A」「範囲の中(`shared/`・`requirements/`・`spec/`。`spec/qa-dev.csv` を除く)のファイル」「機能」。 + - 理由: A と同じ「範囲」の定義で説明できる(C)。(b) は予算側の時点に `merge` の読み方を写すことになる(#348 で避けた形)。 +- **C**: 範囲は「2 つの承認が見た中身」= v1〜v の `shared/`・`requirements/`・`spec/`(`qa-dev.csv` を除く)と `removed.csv`(v1 を含む)。 + **外すのは `feasibility/`・`proposal/`(予算の合意と決裁の側が書くもの)と内部の層だけ。** + +別の値を選んだら直す場所: `check-signoff.py` の `compare_scope`・`removed_scope_rows`、`signoff-tags.md` の比べる場所、試料の期待。 + +## 乗るデプロイ経路 + +プラグイン(`plugins/spec-loop/`)の版を上げてマージ。マージで main へ push されるとサイトが再デプロイされる(`docs/plans/` を足す)。 + +## 周の在り処 + +- ブランチ: `issue/375-signoff-scope` +- worktree: `/Users/hdknr/Projects/hdknr/claude-code-setup/.claude/worktrees/issue-375` +- 起点: `eab5e13` +- base: 参照 `origin/main`(`refs/remotes/origin/HEAD`)→ `eab5e1387a3fa7587aed615754fcb7bc35cd75e0` + +## verify の受入基準 + +Issue 本文は検査対象であって要件ではない。 + +不変条件: + +- **I1(#367)**: 2 つのタグの時点で `requirements/`・`spec/`(`spec/qa-dev.csv` を除く)の中身が違えば、一致とは言わない(1。違うパスを出す)。前の版のものも同じ。 +- **I2(#372)**: 予算側だけに `docs/v1/removed.csv` がある・`removed.csv` の範囲の中(`requirements/`・`spec/`)のファイルの行や機能の行が違うなら、一致とは言わない(1)。 +- **I3(陽性対照)**: 範囲の外(`feasibility/`・`proposal/`・`spec/qa-dev.csv`・内部の層の Q&A の行・`removed.csv` の範囲の外のファイルの行・理由の列・行の順序・`.` で始まる名前)だけが違えば、今までどおり 0。 +- **I4**: 今までの判定(2 の条件・3 の暫定・`shared/` の比較)は変わらない(既存の試料が緑)。 +- **I5**: 範囲の説明(docstring・`signoff-tags.md`・`tree.md`・`README.md`・`CLAUDE.md`)が本体の範囲と一致する。 + +経路(2 つのタグの時点の中身が照合に入る入口): + +| 経路 | 守り方 | 確かめ方 | +| --- | --- | --- | +| `git diff` の比べる場所(`compare_scope`) | `requirements/`・`spec/`・`docs/v1/removed.csv` を足し、`spec/qa-dev.csv` を除く | 試料: `spec/` だけ違う・`requirements/` だけ違う・前の版の `spec/` だけ違う・v1 の `removed.csv` が予算側だけ(1)/`qa-dev.csv` だけ違う・`feasibility/` だけ違う(0) | +| `removed.csv` の行(`removed_scope_rows`) | 範囲の中のファイル・機能・共有の Q&A の行を比べる | 試料: `ファイル,spec/…`・`ファイル,requirements/…`・`機能,…` の行が予算側だけ(1)/`ファイル,feasibility/…`・`D-` の行だけ(0) | +| `build-proposal.py`(`resolve` を呼ぶ) | 本体を変えない | `test-build-proposal.py` が緑。2 つのタグの間に `requirements/` を足すと作らない | + +各経路の守りを外す変異で赤になる(陽性対照つき)。 + +生成物: `skill-metrics.py` の生成ブロックは dev-loop の `SKILL.md` だけ・図の書き出しも無い——**生成物なし**。 + +未証明: なし(照合は git の出力だけで決まり、試料で全部当てられる)。 + +## 未解決の判断 + +上の「仕様の穴」を確認の Issue で利用者に渡す。 + +## 関門の進捗(再開点) + +- 直す線: `既定`(出所: cruise の確かめた線(台帳 `issue-474`)) +- 実験: 未 +- Verifier 1 パス目: 未 +- `/code-review` 1 パス目: 未 + +## 起票 + +- この周の起票: 1 件(仕様の穴の確認の Issue。起票の承認は不在のため訊いていない) + +## 既知の限界・決着済みの論点 + +- 仕様の穴 A・B・C は上の「仕様の穴」で推奨により決めた(確認の Issue を起票)。**決めた値は受入基準を緩める理由にならない。** +- 暫定の行・要求確定のタグの時点の配置の検査は触らない(#325・#338・#348 で決着)。 +- `test-build-proposal.py` の変異「比較を予算のタグの時点から作る」と試料「比較は要求のタグの時点から作る」を外した—— + この周で照合の範囲(`shared/`・`requirements/`・`spec/`)が重ね合わせの読むもの(`merge-versions.py` の `AREAS`)と同じになり、 + 照合が通る組では 2 つの時点の比較が同じになる(等価変異)。試料を `feasibility/` や内部の Q&A 表に置き換えても、 + 比較(提案書は内部の層を載せない)に出ないので殺せないことを確かめた(どちらの置き換えでも変異が生き残った)。 + 代わりに「2 つのタグの間に要求を足すと照合で止まる」を足した。 diff --git a/plugins/spec-loop/.claude-plugin/plugin.json b/plugins/spec-loop/.claude-plugin/plugin.json index cc8d7cf6..e191a80d 100644 --- a/plugins/spec-loop/.claude-plugin/plugin.json +++ b/plugins/spec-loop/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "spec-loop", "description": "要求側のループ(要求確定・仕様レビュー・実現性とコスト・提案書)と、dev-loop への引き渡しを担うプラグイン。現在のスキルは req-loop(Q&A 表への起票(推奨と理由つき)・回答の反映・レビュー会用 PPTX の生成で、利用者の回答待ちで止まる)と spec-review(開発者向けの仕様を書き下し、書き込みツールを持たない 3 つの判定役に別ティアでレビューさせ、利用者に見える結果が変わる指摘は共有の Q&A 表に差し戻し、費用・運用だけが変わる選択は内部の Q&A 表に記録し、範囲の外の場面と実現の手段の指摘は仕様に足さない。抜ける条件を満たさなければ抜けたと報告しない)と feasibility(規模から見積りとスケジュールを更新し、ガントチャートをスクリプトで生成し、費用付きの質問を Q&A 表に起票する。予算責任者の合意待ちで止まる)と proposal(同じ版の 2 つのサインオフのタグが同じ共有の範囲(docs/v/shared/ ほか)を指すかをスクリプトで照合し、一致し、かつ暫定の回答が残っていないときだけ提案書と仕様書(PPTX も選べる)を生成し、利用者にまだ確かめていない暫定の決定を合意と分けて載せる。v2 以降は前の版との比較を載せた変更の提案書になる。決裁者の承認待ちで止まる)と scaffold(確定した仕様を中間表現 spec_ir.json に書き起こし、OpenAPI・型・実装が無いと失敗する Red テストと dev-loop に渡す Issue の本文をスクリプトで生成して、1 機能 = Issue 1 件で登録する。v2 以降は前の版と比べて機能を追加・変更・削除に分け、変わった機能だけを Issue にする。Issue の登録で止まる)の 5 本", - "version": "0.26.0", + "version": "0.27.0", "author": { "name": "hdknr" } } diff --git a/plugins/spec-loop/README.md b/plugins/spec-loop/README.md index 318c6cd8..760bc61d 100644 --- a/plugins/spec-loop/README.md +++ b/plugins/spec-loop/README.md @@ -24,7 +24,7 @@ | `scripts/check-spec-exit.py` | 仕様レビューを抜ける条件のうち、機械で決まる条件 1 の判定(標準ライブラリのみ) | | `scripts/check-feasibility-csv.py` | スケジュールと見積りの表が決まりどおりかの検査(標準ライブラリのみ) | | `scripts/schedule-to-gantt.py` | スケジュールの表からガントチャート(SVG)を作る(標準ライブラリのみ) | -| `scripts/check-signoff.py` | 同じ版の 2 つのサインオフのタグの時点で、共有の範囲が一致するかの照合(標準ライブラリと `git`) | +| `scripts/check-signoff.py` | 同じ版の 2 つのサインオフのタグの時点で、2 つの承認が見た中身(`shared/`・`requirements/`・`spec/`・`removed.csv`)が一致するかの照合(標準ライブラリと `git`) | | `scripts/build-proposal.py` | 照合が通ったときだけ、承認したタグの時点の中身から提案書と仕様書(`--pptx` で PPTX も)を作る(標準ライブラリと `git`) | | `references/spec-ir.md` | 仕様の中間表現(`spec_ir.json`)の形 | | `references/handoff-issue.md` | dev-loop に渡す Issue の本文の形 | diff --git a/plugins/spec-loop/references/signoff-tags.md b/plugins/spec-loop/references/signoff-tags.md index cff71ea9..1f82c203 100644 --- a/plugins/spec-loop/references/signoff-tags.md +++ b/plugins/spec-loop/references/signoff-tags.md @@ -36,7 +36,7 @@ ## 2 つの承認が同じ範囲を見ていたか -開発に進む前の関門では、**同じ版の最新の 2 つのタグの時点で、その版の共有の範囲が一致するか**を確かめる。 +開発に進む前の関門では、**同じ版の最新の 2 つのタグの時点で、2 つの承認が見たその版の中身が一致するか**を確かめる。 プラグインに同梱の照合を使う(**LLM に判断させない**): ```bash @@ -44,11 +44,17 @@ python3 /scripts/check-signoff.py python3 /scripts/check-signoff.py --version 1 # 版を指定 ``` -- **比べる場所は `docs/v1/shared/`〜`docs/v/shared/` と `docs/v2/removed.csv`〜`docs/v/removed.csv`。** - v の時点の共有の範囲は、前の版の `shared/` と、行を消す `removed.csv` にも依る。 +- **比べる場所は v1〜v の `shared/`・`requirements/`・`spec/` と `removed.csv`(v1 のものも)。** + 利用者は要求と仕様を承認し、予算責任者は `requirements/`・`spec/` から数えた見積りに合意する(feasibility)—— + **そこが 2 つのタグの間で違えば、予算の合意は承認と別の中身を見ている**(#367・#375)。 + v の時点の中身は、前の版と、行を消す `removed.csv` にも依る。 凍結した版(`versions.md`)は動かないので、前の版を含めても誤って食い違うことはない。 - **`removed.csv` は `shared/` に効く行だけを比べる**(種類 `Q&A` で対象が共有の表の番号(`Q-`)の行と、種類 `ファイル` で対象が `shared/` から始まる行)。**内部の Q&A 表(`spec/qa-dev.csv`。番号 `D-`)は比べない**——開発者が決めて記録するもので、サインオフの後に内部で決めても照合は割れない(`qa-csv.md` の「層」)。 - `requirements/` や機能の削除は共有の範囲の外なので、それだけが違っても一致する。 + **v1 の `removed.csv` は在ること自体を比べる**(v1 には置かない決まりなので、予算側にだけあっても一致と言わない。#372)。 + **`removed.csv` は範囲に効く行だけを比べる**(種類 `Q&A` で対象が共有の表の番号(`Q-`)の行・種類 `機能` の行・ + 種類 `ファイル` で対象が `shared/`・`requirements/`・`spec/` から始まる行)。 + **比べないのは `feasibility/`・`proposal/`(予算の合意と決裁の側が書く)と内部の層**——内部の Q&A 表 + (`spec/qa-dev.csv`。番号 `D-`)は開発者が決めて記録するもので、2 つのタグの間に feasibility が費用の行を書く。 + サインオフの後に内部で決めても照合は割れない(`qa-csv.md` の「層」)。 - **版を指定しなければ、作業中の版**(作業ツリーの `docs/` で番号が最大の版)。 - **最新**は、`` を**数として**比べて最大のもの(`-10` は `-9` より新しい)。 **`signoff/v2/req-3-fix` のような、命名に合わないタグは数えない。** diff --git a/plugins/spec-loop/references/tree.md b/plugins/spec-loop/references/tree.md index 0acdb851..1b4ca865 100644 --- a/plugins/spec-loop/references/tree.md +++ b/plugins/spec-loop/references/tree.md @@ -42,7 +42,7 @@ hooks/spec_loop_pages.py # 表示用ページを CSV から作るフック ## 置き方の決まり - **`shared/` には、両方の読者に見せてよいものだけを置く。** 2 つのサインオフが - 同じ範囲を見ていたかは、`shared/` の中身で確かめる(`signoff-tags.md`)。 + 同じ中身を見ていたかは、`shared/`・`requirements/`・`spec/`(`qa-dev.csv` を除く)で確かめる(`signoff-tags.md`)。 - **内部の層の Q&A 表(`spec/qa-dev.csv`)は `shared/` に置かない。** 開発者が決めて記録する質問で、利用者に渡さず、 サインオフも比べない(`qa-csv.md` の「層」)。共有の表(`shared/qa.csv`)に置くのは利用者が決める質問だけ。 - **`requirements/` から `feasibility/` へはリンクしない。** 利用者に見せる費用は、 diff --git a/plugins/spec-loop/scripts/check-signoff.py b/plugins/spec-loop/scripts/check-signoff.py index afc1efba..deb1d9e6 100644 --- a/plugins/spec-loop/scripts/check-signoff.py +++ b/plugins/spec-loop/scripts/check-signoff.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""2 つのサインオフのタグの時点で、版の共有の範囲が一致するかを照合する(提案書の関門)。 +"""2 つのサインオフのタグの時点で、2 つの承認が見た版の中身が一致するかを照合する(提案書の関門)。 usage: python3 check-signoff.py [--repo <正本リポジトリ>] [--version ] [--req <タグ>] [--budget <タグ>] @@ -13,10 +13,15 @@ - タグを指定しなければ、**`` を数として比べて最大のもの**を使う(`-10` は `-9` より新しい)。 **命名に合わないタグ(`signoff/v2/req-3-fix`・`signoff/v2/req-03`・旧い `v2/req-signoff-3` と `req-signoff-v3`)は数えない。** **指定したタグが別の版のものなら照合しない(2)**——別の版の承認を組にしない。 -- **比べる場所は `docs/v1/shared/`〜`docs/v/shared/` と `docs/v2/removed.csv`〜`docs/v/removed.csv`** - (v の時点の共有の範囲は、前の版の `shared/` と、行を消す `removed.csv` にも依る)。 - **`removed.csv` は `shared/` に効く行だけを比べる**——種類 `Q&A` と、種類 `ファイル` で対象が `shared/` から - 始まる行。`requirements/` や機能の削除だけが違っても一致のまま(範囲の外なので)。 +- **比べる場所は v1〜v の `shared/`・`requirements/`・`spec/`(内部の Q&A 表 `spec/qa-dev.csv` を除く)と + `removed.csv`(v1 のものも)**——2 つの承認が見た中身(#375)。利用者は要求と仕様を承認し、予算責任者は + `requirements/`・`spec/` から数えた見積りに合意するので、そこが違えば別の中身を見ていたことになる(#367)。 + v の時点の中身は前の版と、行を消す `removed.csv` にも依る。**v1 の `removed.csv` は在ること自体を比べる** + (`merge` が拒否するものが予算側だけにあっても、一致と言わない。#372)。 + **`removed.csv` は範囲に効く行だけを比べる**——種類 `Q&A` で共有の層の番号(`Q-`)の行・種類 `機能` の行・ + 種類 `ファイル` で対象が `shared/`・`requirements/`・`spec/` から始まる行(`spec/qa-dev.csv` を除く)。 + **外すのは `feasibility/`・`proposal/`(予算の合意と決裁の側が書く)と内部の層**(`spec/qa-dev.csv`・`D-` の行。 + 開発者が決めて記録するもので、2 つのタグの間に feasibility が費用の行を書く。#339)。 - **`.` で始まる名前(`.gitkeep`・`.DS_Store`)は数えない**——重ね合わせ(`merge-versions.py`)が読まないので、 承認が見た範囲に入らない。 - **どちらかの時点で比べる場所にファイルが 1 つも無ければ、一致とは言わない(2)。** @@ -34,7 +39,7 @@ 終了コード: -- 0: 一致する(2 つの承認は同じ共有の範囲を見ていた) +- 0: 一致する(2 つの承認は同じ中身を見ていた) - 1: 一致しない(違うファイルを全部出す) - 3: 要求確定のタグの時点に `暫定反映` の行が残る(行を全部出す。一致しないなら、その違いも出す)—— **利用者に確かめてもらい、`反映済み` にしてから要求確定と予算の合意の両方のサインオフを取り直す** @@ -54,7 +59,7 @@ | --- | --- | | **タグを付け直していないか** | 付け直した後の状態しか見えない。`references/signoff-tags.md` の「付け直さない」は運用で守る | | **誰がタグを付けたか** | 署名を検査しない。タグを付けられる人を絞るのはリポジトリの権限 | -| **共有の範囲の外**(`requirements/`・`feasibility/`) | 2 つの承認が共通に見るのは `shared/` だけ(`references/tree.md`) | +| **比べる範囲の外**(`feasibility/`・`proposal/`・`spec/qa-dev.csv`) | 予算の合意・決裁の側と開発者の内部が書くもの。2 つの承認の間で変わってよい | | **タグの時点の版の並びが決まりに反するか** | 照合するのは比べる場所の中身だけ。版の並びは作業ツリーで `merge-versions.py` が見る | """ @@ -79,6 +84,9 @@ DOCS = "docs" SHARED = "shared/" REMOVED = "removed.csv" +# 2 つの承認が見た中身(版のディレクトリからのパス。#375)。内部の Q&A 表は外す(#339) +SCOPE_DIRS = (SHARED, "requirements/", "spec/") +INTERNAL_QA = "spec/qa-dev.csv" def _load(name: str, filename: str): @@ -125,15 +133,24 @@ def removed_scope(version: int) -> list[str]: return [f"{DOCS}/v{i}/{REMOVED}" for i in range(2, version + 1)] +def compare_scope(version: int) -> list[str]: + """`git diff` で比べる場所(pathspec)。版 1〜n の `shared/`・`requirements/`・`spec/` と v1 の `removed.csv`、 + 内部の Q&A 表を除く。v2 以降の `removed.csv` は行で比べる(`removed_scope_rows`)。""" + paths = [f"{DOCS}/v{i}/{d}" for i in range(1, version + 1) for d in SCOPE_DIRS] + paths.append(f"{DOCS}/v1/{REMOVED}") + return paths + [f":(exclude){DOCS}/v{i}/{INTERNAL_QA}" for i in range(1, version + 1)] + + # `removed.csv` の種類(`references/versions.md` の「削除」。`merge-versions.py` のものを使い、写さない) REMOVED_QA = versions.KIND_QA REMOVED_FILE = versions.KIND_FILE +REMOVED_FEATURE = versions.KIND_FEATURE def read_removed_at(repo: Path, commit: str, tag: str, path: str) -> list[tuple[int, str, str]] | None: """その時点の `removed.csv` の行(行番号・種類・対象)。ファイルが無ければ None。 - **1 つのタグの時点・1 つのパスにつき、ここで 1 回だけ読む**——共有の範囲の比較(`removed_shared_rows`)と + **1 つのタグの時点・1 つのパスにつき、ここで 1 回だけ読む**——範囲の比較(`removed_scope_rows`)と 暫定の行の重ね合わせ(`provisional_rows`)は、`resolve` が読んだものを使う。 """ listed = git(repo, "ls-tree", "--name-only", commit, "--", path) @@ -170,17 +187,18 @@ def refused(tag: str, problems: list[str]) -> str: + " / ".join(problems) + "。照合していない") -def removed_shared_rows(parsed: list[tuple[int, str, str]] | None) -> list[tuple[str, str]]: - """`removed.csv` の行のうち `shared/` に効く行(種類・対象)。 +def removed_scope_rows(parsed: list[tuple[int, str, str]] | None) -> list[tuple[str, str]]: + """`removed.csv` の行のうち、比べる範囲に効く行(種類・対象)。 - 理由の列は比べない——消したものが同じなら、2 つの承認が見た共有の範囲は同じなので。 - 無いことと、`shared/` に効く行が無いことは同じ(どちらも `shared/` から何も消さない)。 + 理由の列は比べない——消したものが同じなら、2 つの承認が見た中身は同じなので。 + 無いことと、範囲に効く行が無いことは同じ(どちらも範囲から何も消さない)。 """ rows = [] for _, kind, target in parsed or []: - # 内部の層の Q&A(`D-`。`spec/qa-dev.csv`)を消した行は共有の範囲の外——開発者が決めるものでサインオフが割れないように(#339) + # 内部の層の Q&A(`D-`。`spec/qa-dev.csv`)を消した行は範囲の外——開発者が決めるものでサインオフが割れないように(#339) if ((kind == REMOVED_QA and not qa.is_internal_number(target)) - or (kind == REMOVED_FILE and target.startswith(SHARED))): + or kind == REMOVED_FEATURE + or (kind == REMOVED_FILE and target.startswith(SCOPE_DIRS) and target != INTERNAL_QA)): rows.append((kind, target)) return sorted(rows) @@ -372,16 +390,16 @@ def resolve(repo: Path, req: str | None = None, budget: str | None = None, "今の予算の合意のタグとは一致しなくなる(references/signoff-tags.md)") pair = f"{info['req']}({info['req_commit'][:7]})と {info['budget']}({info['budget_commit'][:7]})" - where = f"v1〜v{version} の {SHARED}" + (f" と {REMOVED}" if version > 1 else "") - diff = git(repo, "diff", "--name-status", info["req_commit"], info["budget_commit"], "--", *shared_scope(version)) + where = f"v1〜v{version} の {'・'.join(SCOPE_DIRS)}({INTERNAL_QA} を除く)と {REMOVED}" + diff = git(repo, "diff", "--name-status", info["req_commit"], info["budget_commit"], "--", *compare_scope(version)) if diff.returncode != 0: return 2, [f"{where} を比べられない: {diff.stderr.strip()}。照合していない"], {} # 重ね合わせが読まないもの(`.` で始まる名前)だけの違いは、承認が見た範囲の違いではない changed = [line for line in diff.stdout.splitlines() if line and not all(hidden(path) for path in line.split("\t")[1:])] for path, (at_req, at_budget) in removed.items(): - if removed_shared_rows(at_req) != removed_shared_rows(at_budget): - changed.append(f"{REMOVED}\t{path}(shared/ に効く行が違う)") + if removed_scope_rows(at_req) != removed_scope_rows(at_budget): + changed.append(f"{REMOVED}\t{path}(範囲に効く行が違う)") if changed: lines = [f"一致しない: {pair} で {where} が違う({len(changed)} 件)"] lines += [f" {line}" for line in changed] diff --git a/plugins/spec-loop/skills/feasibility/SKILL.md b/plugins/spec-loop/skills/feasibility/SKILL.md index 0b40556a..44a2bfa4 100644 --- a/plugins/spec-loop/skills/feasibility/SKILL.md +++ b/plugins/spec-loop/skills/feasibility/SKILL.md @@ -5,8 +5,8 @@ description: "実現性・コストのループを 1 回分進める。要求・ # feasibility スキル - -> **このスキルの版: 0.26.0**(プラグイン `spec-loop`)。 + +> **このスキルの版: 0.27.0**(プラグイン `spec-loop`)。 > 手元で読まれている版がリポジトリの最新より古いなら、 > `/plugin marketplace update` → `/plugin update spec-loop@claude-code-setup` の順に実行し、 > Claude Code を再起動する([#63](https://github.com/hdknr/claude-code-setup/issues/63))。 diff --git a/plugins/spec-loop/skills/proposal/SKILL.md b/plugins/spec-loop/skills/proposal/SKILL.md index b44b6709..4243e4a2 100644 --- a/plugins/spec-loop/skills/proposal/SKILL.md +++ b/plugins/spec-loop/skills/proposal/SKILL.md @@ -5,8 +5,8 @@ description: "提案書のループを 1 回分進める。利用者の要求確 # proposal スキル - -> **このスキルの版: 0.26.0**(プラグイン `spec-loop`)。 + +> **このスキルの版: 0.27.0**(プラグイン `spec-loop`)。 > 手元で読まれている版がリポジトリの最新より古いなら、 > `/plugin marketplace update` → `/plugin update spec-loop@claude-code-setup` の順に実行し、 > Claude Code を再起動する([#63](https://github.com/hdknr/claude-code-setup/issues/63))。 diff --git a/plugins/spec-loop/skills/req-loop/SKILL.md b/plugins/spec-loop/skills/req-loop/SKILL.md index 8aacf123..424151f8 100644 --- a/plugins/spec-loop/skills/req-loop/SKILL.md +++ b/plugins/spec-loop/skills/req-loop/SKILL.md @@ -5,8 +5,8 @@ description: "要求確定のループを 1 周(= レビュー会 1 回分) # req-loop スキル - -> **このスキルの版: 0.26.0**(プラグイン `spec-loop`)。 + +> **このスキルの版: 0.27.0**(プラグイン `spec-loop`)。 > 手元で読まれている版がリポジトリの最新より古いなら、 > `/plugin marketplace update` → `/plugin update spec-loop@claude-code-setup` の順に実行し、 > Claude Code を再起動する([#63](https://github.com/hdknr/claude-code-setup/issues/63))。 diff --git a/plugins/spec-loop/skills/scaffold/SKILL.md b/plugins/spec-loop/skills/scaffold/SKILL.md index 32e29739..440cfb0d 100644 --- a/plugins/spec-loop/skills/scaffold/SKILL.md +++ b/plugins/spec-loop/skills/scaffold/SKILL.md @@ -5,8 +5,8 @@ description: "確定した仕様を dev-loop の入力(Issue)に変える引 # scaffold スキル - -> **このスキルの版: 0.26.0**(プラグイン `spec-loop`)。 + +> **このスキルの版: 0.27.0**(プラグイン `spec-loop`)。 > 手元で読まれている版がリポジトリの最新より古いなら、 > `/plugin marketplace update` → `/plugin update spec-loop@claude-code-setup` の順に実行し、 > Claude Code を再起動する([#63](https://github.com/hdknr/claude-code-setup/issues/63))。 diff --git a/plugins/spec-loop/skills/spec-review/SKILL.md b/plugins/spec-loop/skills/spec-review/SKILL.md index 4246fd1d..73c9d794 100644 --- a/plugins/spec-loop/skills/spec-review/SKILL.md +++ b/plugins/spec-loop/skills/spec-review/SKILL.md @@ -5,8 +5,8 @@ description: "仕様レビューのループを 1 回分進める。確定した # spec-review スキル - -> **このスキルの版: 0.26.0**(プラグイン `spec-loop`)。 + +> **このスキルの版: 0.27.0**(プラグイン `spec-loop`)。 > 手元で読まれている版がリポジトリの最新より古いなら、 > `/plugin marketplace update` → `/plugin update spec-loop@claude-code-setup` の順に実行し、 > Claude Code を再起動する([#63](https://github.com/hdknr/claude-code-setup/issues/63))。 diff --git a/scripts/test-build-proposal.py b/scripts/test-build-proposal.py index 5d6f0533..dd1d31eb 100644 --- a/scripts/test-build-proposal.py +++ b/scripts/test-build-proposal.py @@ -272,10 +272,10 @@ def pptx_ok(parts: dict) -> bool: # 比較は req のタグの時点から作る——承認の後に作業ツリーとコミットで足した要求は載らない ("承認の後に直した差分は比較に入らない", V2C + [("w", "docs/v2/requirements/late.md", "L\n"), ("c",)], False, lambda r: made(r) and "| requirements/c.md | 追加 |" in r.text and "late.md" not in r.text), - # req のタグと budget のタグの間に足した要求は載らない(比較は req のタグの時点。共有の範囲の外なので照合は通る) - ("比較は要求のタグの時点から作る", SIGNED_R + V2_CHANGES + [("c",), ("t", "signoff/v2/req-1"), - ("w", "docs/v2/requirements/between.md", "B\n"), ("c",), ("t", "signoff/v2/budget-1")], False, - lambda r: made(r) and "| requirements/c.md | 追加 |" in r.text and "between.md" not in r.text), + # req のタグと budget のタグの間に要求を足すと、照合が 1 で止める(#375。見積りの根拠が承認と違う) + ("2 つのタグの間に要求を足すと照合で止まる", SIGNED_R + V2_CHANGES + [("c",), ("t", "signoff/v2/req-1"), + ("w", "docs/v2/requirements/between.md", "B\n"), ("c",), ("t", "signoff/v2/budget-1")], False, + lambda r: not_made(1)(r) and "docs/v2/requirements/between.md" in r.output), # 大文字小文字だけ違うパスは写す前に断る——区別しない FS では上書きし合い、先に置いたリンクを辿って外に書きうる ("大文字小文字だけ違うパスは比較で断る", V2C[:-3] + [("i", "docs/v2/requirements/C.md", "100644", "X\n"), ("c",), ("t", "signoff/v2/req-1"), ("t", "signoff/v2/budget-1")], False, @@ -490,8 +490,8 @@ def pptx_ok(parts: dict) -> bool: ("比較を作業ツリーから作る", " copy_docs(top, commit, tag, Path(tmp))\n", " __import__('shutil').copytree(top / signoff.DOCS, Path(tmp) / signoff.DOCS, symlinks=True)\n", "承認の後に直した差分は比較に入らない"), - ("比較を予算のタグの時点から作る", 'compare_at(top, info["req_commit"], info["req"]', - 'compare_at(top, info["budget_commit"], info["req"]', "比較は要求のタグの時点から作る"), + # 「比較を予算のタグの時点から作る」は #375 から等価変異——照合が比べる範囲(shared/・requirements/・spec/)が + # 重ね合わせの読むもの(merge-versions.py の AREAS)と同じになり、照合が通る組では 2 つの時点の比較が同じになる ("大文字小文字だけ違うパスを断らない", " if clashes:\n", " if False:\n", "大文字小文字だけ違うパスは比較で断る"), ("ファイルとディレクトリの重なりを見ない", " if fold(parent) in seen:\n", " if False:\n", "ファイルとディレクトリが大文字小文字だけ違うと比較で断る"), diff --git a/scripts/test-check-signoff.py b/scripts/test-check-signoff.py index 9b0c0677..7ebca62c 100644 --- a/scripts/test-check-signoff.py +++ b/scripts/test-check-signoff.py @@ -7,11 +7,14 @@ 見るもの: -1. **比べる場所(v1〜v の `shared/` と v2〜v の `removed.csv`)が食い違うタグの組で非ゼロ(1)**—— - 中身の違い・追加・削除・**前の版の `shared/` だけ**・**`removed.csv` の `shared/` に効く行だけ**の違い・ +1. **比べる場所(v1〜v の `shared/`・`requirements/`・`spec/`(`spec/qa-dev.csv` を除く)と `removed.csv`)が + 食い違うタグの組で非ゼロ(1)**——中身の違い・追加・削除・**前の版の `shared/`・`spec/` だけ**・ + **`requirements/`・`spec/` だけ**(#367)・**予算側だけの v1 の `removed.csv`**(#372)・ + **`removed.csv` の範囲に効く行(共有の Q&A・範囲の中のファイル・機能)だけ**の違い・ **片側だけ `.` で始まる名前の rename**(読む側に新しいファイルが現れる)。 -2. **一致する組を通す**(陽性対照。比べる場所の外だけ違う組も一致——**`removed.csv` の `requirements/`・機能の行 - だけ・理由だけ・行の順序だけが違う組も**。**`.` で始まる名前だけの違いも**——途中の `.` で始まるディレクトリの中も含む)。 +2. **一致する組を通す**(陽性対照。比べる場所の外だけ違う組も一致——**`feasibility/`・`proposal/`・`spec/qa-dev.csv` + だけ・`removed.csv` の範囲の外(`feasibility/`・`proposal/`・`spec/qa-dev.csv`)のファイルの行・内部の Q&A の行・ + 理由・行の順序だけが違う組も**。**`.` で始まる名前だけの違いも**——途中の `.` で始まるディレクトリの中も含む)。 3. **「最新」を数で選ぶ**(`-10` > `-9`)・**命名に合わないタグ(旧い `v/req-signoff-`・`req-signoff-v` を含む)を数えない**・ 指定したタグで照合する・**版を省けば作業中の版(番号の最大)**・`--version` で版を選ぶ・ **素の `v1` タグがあっても(先でも後でも)承認のタグを作れて照合が 0 になる**(#270)。 @@ -155,7 +158,7 @@ # 予算側は `merge` が受け付ける配置(v1 の Q-001 を v2 で消す。v2 は Q-001 を置かない——#349) ("removed.csv だけ違う", [("w", SHARED, QA_A), ("w", SHARED2, QA_HEAD + QA_2), ("c",), ("t", R2), ("w", REMOVED2, "種類,対象,理由\nQ&A,Q-001,不要\n"), ("c",), ("t", B2)], - [], 1, "docs/v2/removed.csv(shared/ に効く行が違う)"), + [], 1, "docs/v2/removed.csv(範囲に効く行が違う)"), # `merge` が拒否する配置は要求確定側だけ(予算側は受け付ける)——要求確定側の removed.csv で重ねていれば 2、 # 予算側で重ねると拒否されず、範囲の違いで 1 になる(#349) ("要求確定側だけが拒否する配置", [("w", SHARED, QA_A), ("w", SHARED2, QA_HEAD + QA_2), @@ -164,17 +167,30 @@ [], 2, "docs/v2/removed.csv:2: 前の版までに無いものを消そうとしている: Q&A Q-003"), ("removed.csv の shared のファイルの行が違う", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("c",), ("t", R2), ("w", REMOVED2, "種類,対象,理由\nファイル,shared/nfr.csv,不要\n"), ("c",), ("t", B2)], - [], 1, "docs/v2/removed.csv(shared/ に効く行が違う)"), - # requirements/ や機能の削除は共有の範囲の外——removed.csv のファイルが違っても一致のまま + [], 1, "docs/v2/removed.csv(範囲に効く行が違う)"), + # requirements/・spec/ のファイルと機能の削除は範囲の中——承認が見た中身から消すので(#375) + ("removed.csv の requirements のファイルの行が違う", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("c",), ("t", R2), + ("w", REMOVED2, "種類,対象,理由\nファイル,requirements/r.md,不要\n"), + ("c",), ("t", B2)], + [], 1, "docs/v2/removed.csv(範囲に効く行が違う)"), + ("removed.csv の spec のファイルの行が違う", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("c",), ("t", R2), + ("w", REMOVED2, "種類,対象,理由\nファイル,spec/spec.json,不要\n"), + ("c",), ("t", B2)], + [], 1, "docs/v2/removed.csv(範囲に効く行が違う)"), + ("removed.csv の機能の行だけ違う", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("c",), ("t", R2), + ("w", REMOVED2, "種類,対象,理由\n機能,F-001,不要\n"), ("c",), ("t", B2)], + [], 1, "docs/v2/removed.csv(範囲に効く行が違う)"), + # feasibility/・proposal/・内部の Q&A 表は範囲の外——removed.csv で消しても一致のまま ("removed.csv の範囲の外の行だけ違う", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("c",), ("t", R2), - ("w", REMOVED2, "種類,対象,理由\nファイル,requirements/r.md,不要\n機能,F-001,不要\n"), + ("w", REMOVED2, "種類,対象,理由\nファイル,feasibility/f.md,不要\n" + "ファイル,proposal/p.md,不要\nファイル,spec/qa-dev.csv,不要\n"), ("c",), ("t", B2)], - [], 0, "v1〜v2 の shared/ と removed.csv は同じ"), + [], 0, "v1〜v2 の shared/・requirements/・spec/(spec/qa-dev.csv を除く)と removed.csv は同じ"), # 内部の層の Q&A(spec/qa-dev.csv の D-)は共有の範囲の外——表を変えても、removed.csv で消しても一致のまま(#339) ("removed.csv の内部の Q&A の行だけ違う", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("c",), ("t", R2), ("w", REMOVED2, "種類,対象,理由\nQ&A,D-001,共有へ移した(Q-003)\n"), ("c",), ("t", B2)], - [], 0, "v1〜v2 の shared/ と removed.csv は同じ"), + [], 0, "v1〜v2 の shared/・requirements/・spec/(spec/qa-dev.csv を除く)と removed.csv は同じ"), ("内部の Q&A 表だけ違う", [("w", SHARED, QA_A), ("w", "docs/v1/spec/qa-dev.csv", QA_HEAD), ("c",), ("t", R1), ("w", "docs/v1/spec/qa-dev.csv", QA_HEAD + "D-001,仕様レビュー,分けるか,A: 分ける / B: 分けない,B,費用,未回答,—,確認中,\n"), ("c",), ("t", B1)], @@ -192,7 +208,7 @@ # 重ね合わせ(merge-versions.py)は `.` で始まる名前を読まない——承認が見た範囲に入らない ("shared の . で始まるファイルだけ違う", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("c",), ("t", R2), ("w", "docs/v1/shared/.DS_Store", "x\n"), ("c",), ("t", B2)], - [], 0, "v1〜v2 の shared/ と removed.csv は同じ"), + [], 0, "v1〜v2 の shared/・requirements/・spec/(spec/qa-dev.csv を除く)と removed.csv は同じ"), ("タグの時点の作業中の版が . で始まるファイルだけ", [("w", SHARED, QA_A), ("w", "docs/v2/.gitkeep", ""), ("c",), ("t", R2), ("t", B2), ("w", "docs/v2/spec/s.md", "x\n"), ("c",)], [], 2, f"{R2} の時点に docs/v2/ のファイルが 1 つも無い"), @@ -209,15 +225,31 @@ ("removed.csv の行の順序だけ違う", [("w", SHARED, QA_B), ("w", REMOVED2, "種類,対象,理由\nQ&A,Q-001,不要\nQ&A,Q-002,不要\n"), ("c",), ("t", R2), ("w", REMOVED2, "種類,対象,理由\nQ&A,Q-002,不要\nQ&A,Q-001,不要\n"), ("c",), ("t", B2)], - [], 0, "v1〜v2 の shared/ と removed.csv は同じ"), + [], 0, "v1〜v2 の shared/・requirements/・spec/(spec/qa-dev.csv を除く)と removed.csv は同じ"), # 決まりに反する removed.csv(見出しの順が違う)を、無い removed.csv と同じに読まない ("removed.csv が決まりに反する", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("w", REMOVED2, "対象,種類,理由\nQ-001,Q&A,x\n"), ("c",), ("t", R2), ("rm", REMOVED2), ("c",), ("t", B2)], [], 2, "removed.csv が決まりに反する(references/versions.md): docs/v2/removed.csv:1: 見出し行が決まりと違う"), + # 見積りの根拠(requirements/・spec/)が 2 つのタグの間で変われば、予算の合意は承認と別の中身を見ている(#367) + ("requirements だけ違う", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("c",), ("t", R2), + ("w", "docs/v2/requirements/a.md", "x\n"), ("c",), ("t", B2)], + [], 1, "A\tdocs/v2/requirements/a.md"), + ("spec だけ違う", [("w", SHARED, QA_A), ("w", "docs/v1/spec/s.md", "x\n"), ("c",), ("t", R1), + ("w", "docs/v1/spec/s.md", "y\n"), ("c",), ("t", B1)], + [], 1, "M\tdocs/v1/spec/s.md"), + ("前の版の spec だけ違う", [("w", SHARED, QA_A), ("w", "docs/v1/spec/s.md", "x\n"), ("w", SHARED2, QA_B), ("c",), + ("t", R2), ("w", "docs/v1/spec/s.md", "y\n"), ("c",), ("t", B2)], + [], 1, "M\tdocs/v1/spec/s.md"), + # v1 には removed.csv を置かない(merge が拒否する)——予算側にだけあっても一致と言わない(#372) + ("v1 の removed.csv が予算側だけ", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("c",), ("t", R2), + ("w", "docs/v1/removed.csv", "種類,対象,理由\n"), ("c",), ("t", B2)], + [], 1, "A\tdocs/v1/removed.csv"), + # 範囲の外(予算の合意と決裁の側が書くもの)だけ違うなら一致 ("範囲の外だけ違う", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("c",), ("t", R2), - ("w", "docs/v2/requirements/a.md", "x\n"), ("c",), ("t", B2)], - [], 0, "v1〜v2 の shared/ と removed.csv は同じ"), + ("w", "docs/v2/feasibility/estimate.md", "x\n"), ("w", "docs/v1/proposal/p.md", "x\n"), + ("c",), ("t", B2)], + [], 0, "v1〜v2 の shared/・requirements/・spec/(spec/qa-dev.csv を除く)と removed.csv は同じ"), # 作業中の版は v2 だがタグは v1 にしかない。v1 の範囲に v2 は入らない ("版を指定", [("w", SHARED, QA_A), ("c",), ("t", R1), ("w", SHARED2, QA_A), ("c",), ("t", B1)], ["--version", "1"], 0, f"一致する: {R1}"), @@ -356,21 +388,35 @@ "両方に shared が無い"), ("根で走らせない", " repo = Path(top.stdout.strip())\n", "", "サブディレクトリから"), ("差分を見ない", " if changed:", " if False:", "中身が違う"), - ("比べる場所を絞らない", 'info["budget_commit"], "--", *shared_scope(version))', 'info["budget_commit"])', "一致"), - ("前の版を範囲に含めない", "for i in range(1, version + 1)]", "for i in range(version, version + 1)]", - "前の版の shared だけ違う"), - ("removed.csv を比べない", "if removed_shared_rows(at_req) != removed_shared_rows(at_budget):", "if False:", + ("比べる場所を絞らない", 'info["budget_commit"], "--", *compare_scope(version))', 'info["budget_commit"])', "一致"), + ("前の版を範囲に含めない", "for i in range(1, version + 1) for d in SCOPE_DIRS]", + "for i in range(version, version + 1) for d in SCOPE_DIRS]", "前の版の shared だけ違う"), + ("前の版の spec を範囲に含めない", "for i in range(1, version + 1) for d in SCOPE_DIRS]", + "for i in range(1, version + 1) for d in SCOPE_DIRS if d == SHARED or i == version]", "前の版の spec だけ違う"), + ("requirements を範囲に入れない", 'SCOPE_DIRS = (SHARED, "requirements/", "spec/")', 'SCOPE_DIRS = (SHARED, "spec/")', + "requirements だけ違う"), + ("spec を範囲に入れない", 'SCOPE_DIRS = (SHARED, "requirements/", "spec/")', 'SCOPE_DIRS = (SHARED, "requirements/")', + "spec だけ違う"), + ("内部の Q&A 表を除かない", ' return paths + [f":(exclude){DOCS}/v{i}/{INTERNAL_QA}" for i in range(1, version + 1)]', + " return paths", "内部の Q&A 表だけ違う"), + ("v1 の removed.csv を比べない", ' paths.append(f"{DOCS}/v1/{REMOVED}")\n', "", "v1 の removed.csv が予算側だけ"), + ("removed.csv を比べない", "if removed_scope_rows(at_req) != removed_scope_rows(at_budget):", "if False:", "removed.csv だけ違う"), ("removed.csv の行を絞らない", - "if ((kind == REMOVED_QA and not qa.is_internal_number(target))\n or (kind == REMOVED_FILE and target.startswith(SHARED))):", + "if ((kind == REMOVED_QA and not qa.is_internal_number(target))\n or kind == REMOVED_FEATURE\n" + " or (kind == REMOVED_FILE and target.startswith(SCOPE_DIRS) and target != INTERNAL_QA)):", "if True:", "removed.csv の範囲の外の行だけ違う"), + ("removed.csv の機能の行を見ない", " or kind == REMOVED_FEATURE\n", "", "removed.csv の機能の行だけ違う"), + ("removed.csv の内部の Q&A 表の行を除かない", " and target != INTERNAL_QA)):", ")):", "removed.csv の範囲の外の行だけ違う"), ("内部の Q&A を消した行も比べる", "(kind == REMOVED_QA and not qa.is_internal_number(target))", "kind == REMOVED_QA", "removed.csv の内部の Q&A の行だけ違う"), # 層の判定(`check-qa-csv.py` の `is_internal_number`)を入れ替える(#353) ("比べる行の層を入れ替える", "(kind == REMOVED_QA and not qa.is_internal_number(target))", "(kind == REMOVED_QA and qa.is_internal_number(target))", "removed.csv の内部の Q&A の行だけ違う"), - ("removed.csv の shared のファイルの行を見ない", "and target.startswith(SHARED))):", "and False)):", - "removed.csv の shared のファイルの行が違う"), + ("removed.csv の範囲のファイルの行を見ない", "and target.startswith(SCOPE_DIRS) and target != INTERNAL_QA)):", + "and False)):", "removed.csv の shared のファイルの行が違う"), + ("removed.csv の spec のファイルの行を見ない", "target.startswith(SCOPE_DIRS)", "target.startswith(SCOPE_DIRS[:2])", + "removed.csv の spec のファイルの行が違う"), ("removed.csv の決まりを見ない", " if problems:\n raise Unjudged(refused(tag, problems))\n return parsed", " if False:\n raise Unjudged(refused(tag, problems))\n return parsed", "removed.csv が決まりに反する"), # #351 の前の形(前置きにもパスを書く)に戻す @@ -639,10 +685,16 @@ def reference_mismatches(source: str, markdown: str) -> list[str]: word = re.search(r"`` は (.+?)で、", markdown) if NUMBER_WORDS.get(word.group(1) if word else "") != constant(source, "NUMBER"): problems.append(f"番号の決まり {word and word.group(1)!r} ≠ 本体 {constant(source, 'NUMBER')!r}") - place = re.search(r"比べる場所は `([^`/]+)/v1/([^`]+)`〜`[^`]+` と `[^`/]+/v2/([^`]+)`〜", markdown) - expected = (constant(source, "DOCS"), constant(source, "SHARED"), constant(source, "REMOVED")) - if (place.groups() if place else None) != expected: - problems.append(f"比べる場所 {place and place.groups()!r} ≠ 本体 {expected!r}") + place = re.search(r"比べる場所は v1〜v の ((?:`[^`]+`・?)+) と `([^`]+)`(v1 のものも)", markdown) + dirs = re.search(r"^SCOPE_DIRS = \((.+)\)$", source, flags=re.MULTILINE) + actual = (tuple(constant(source, part) if not part.startswith('"') else part.strip('"') + for part in dirs.group(1).split(", ")) if dirs else None, constant(source, "REMOVED")) + found = (tuple(re.findall(r"`([^`]+)`", place.group(1))), place.group(2)) if place else None + if found != actual: + problems.append(f"比べる場所 {found!r} ≠ 本体 {actual!r}") + internal = constant(source, "INTERNAL_QA") + if not internal or f"(`{internal}`。番号 `D-`)" not in markdown: + problems.append(f"比べない内部の Q&A 表 ≠ 本体 {internal!r}") return problems @@ -650,8 +702,10 @@ def reference_mismatches(source: str, markdown: str) -> list[str]: ("予算のタグの種類", "| `signoff/v/budget-` |", "| `signoff/v/approval-` |"), ("予算のタグの根", "| `signoff/v/budget-` |", "| `approval/v/budget-` |"), ("番号の決まり", "`` は 1 から始まる整数で、", "`` は 0 から始まる整数で、"), - ("比べる場所(shared)", "比べる場所は `docs/v1/shared/`〜", "比べる場所は `docs/v1/common/`〜"), - ("比べる場所(removed.csv)", "と `docs/v2/removed.csv`〜", "と `docs/v2/deleted.csv`〜"), + ("比べる場所(shared)", "比べる場所は v1〜v の `shared/`・", "比べる場所は v1〜v の `common/`・"), + ("比べる場所(spec)", "`requirements/`・`spec/` と `removed.csv`(v1 のものも)", "`requirements/` と `removed.csv`(v1 のものも)"), + ("比べる場所(removed.csv)", "と `removed.csv`(v1 のものも)", "と `deleted.csv`(v1 のものも)"), + ("比べない内部の Q&A 表", "(`spec/qa-dev.csv`。番号 `D-`)", "(`spec/qa-internal.csv`。番号 `D-`)"), ] From 5b93f39a94db9bac7f44481ad811c9210e95787e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=88=E8=89=AF=20=E8=8B=B1=E6=A8=B9?= Date: Tue, 27 Oct 2026 19:48:21 +0900 Subject: [PATCH 2/8] =?UTF-8?q?issue-375:=20=E5=86=8D=E9=96=8B=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/plans/issue-375.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/plans/issue-375.md b/docs/plans/issue-375.md index 4f6810e4..74c266b8 100644 --- a/docs/plans/issue-375.md +++ b/docs/plans/issue-375.md @@ -99,9 +99,11 @@ Issue 本文は検査対象であって要件ではない。 ## 関門の進捗(再開点) - 直す線: `既定`(出所: cruise の確かめた線(台帳 `issue-474`)) -- 実験: 未 -- Verifier 1 パス目: 未 -- `/code-review` 1 パス目: 未 +- 割り目(手順 4 の完了時): 不在のため訊かず続けた +- 実験: 試料(`test-check-signoff.py` 72 件・変異 66 件、`test-build-proposal.py` 81 件・変異 89 件)。`check-all.py` 52/52 +- 1 パス目の起点: このコミットの次(関門に渡す時点の HEAD。下の Verifier に渡した SHA) +- Verifier 1 パス目: 起動(sonnet) +- `/code-review` 1 パス目: 起動(high) ## 起票 From 7025909d880a86b1d4a95808350310525ca0490c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=88=E8=89=AF=20=E8=8B=B1=E6=A8=B9?= Date: Tue, 27 Oct 2026 19:59:25 +0900 Subject: [PATCH 3/8] =?UTF-8?q?check-signoff.py:=20removed.csv=20=E3=81=AF?= =?UTF-8?q?=E5=86=85=E9=83=A8=E3=81=AE=E5=B1=A4=E3=81=AE=20Q&A=20=E4=BB=A5?= =?UTF-8?q?=E5=A4=96=E3=82=92=E5=85=A8=E9=83=A8=E6=AF=94=E3=81=B9=E3=82=8B?= =?UTF-8?q?=E3=83=BB=E6=AF=94=E8=BC=83=E3=81=AE=E8=A9=A6=E6=96=99=E3=82=92?= =?UTF-8?q?=E7=90=86=E7=94=B1=E3=81=AE=E5=88=97=E3=81=A7=E6=88=BB=E3=81=99?= =?UTF-8?q?=E3=83=BB=E5=89=8D=E3=81=AE=E7=89=88=E3=81=AE=20qa-dev.csv=20?= =?UTF-8?q?=E3=81=AE=E8=A9=A6=E6=96=99=EF=BC=88#375=20=E3=81=AE=201=20?= =?UTF-8?q?=E3=83=91=E3=82=B9=E7=9B=AE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/plans/issue-375.md | 35 +++++++++++--- plugins/spec-loop/README.md | 2 +- plugins/spec-loop/references/signoff-tags.md | 4 +- plugins/spec-loop/scripts/check-signoff.py | 22 ++++----- scripts/test-build-proposal.py | 11 ++++- scripts/test-check-signoff.py | 51 +++++++++++--------- 6 files changed, 78 insertions(+), 47 deletions(-) diff --git a/docs/plans/issue-375.md b/docs/plans/issue-375.md index 74c266b8..fc4bb81a 100644 --- a/docs/plans/issue-375.md +++ b/docs/plans/issue-375.md @@ -101,9 +101,12 @@ Issue 本文は検査対象であって要件ではない。 - 直す線: `既定`(出所: cruise の確かめた線(台帳 `issue-474`)) - 割り目(手順 4 の完了時): 不在のため訊かず続けた - 実験: 試料(`test-check-signoff.py` 72 件・変異 66 件、`test-build-proposal.py` 81 件・変異 89 件)。`check-all.py` 52/52 -- 1 パス目の起点: このコミットの次(関門に渡す時点の HEAD。下の Verifier に渡した SHA) -- Verifier 1 パス目: 起動(sonnet) -- `/code-review` 1 パス目: 起動(high) +- 1 パス目の起点: `5b93f39` +- Verifier 1 パス目: 通した——反証 1 件(F1) +- `/code-review` 1 パス目: 通した——指摘 8 件(CR1〜CR8) +- 1 パス目の指摘対応: F1・F1 副次・CR2・CR3・CR6 を直した。差分は 0 行でない → 2 パス目を回す +- 割り目(2 パス目の前): 不在のため訊かず続けた +- 2 パス目の起点: 下の 2 パス目の関門に渡すコミット(このコミットの次に書く) ## 起票 @@ -113,8 +116,24 @@ Issue 本文は検査対象であって要件ではない。 - 仕様の穴 A・B・C は上の「仕様の穴」で推奨により決めた(確認の Issue を起票)。**決めた値は受入基準を緩める理由にならない。** - 暫定の行・要求確定のタグの時点の配置の検査は触らない(#325・#338・#348 で決着)。 -- `test-build-proposal.py` の変異「比較を予算のタグの時点から作る」と試料「比較は要求のタグの時点から作る」を外した—— - この周で照合の範囲(`shared/`・`requirements/`・`spec/`)が重ね合わせの読むもの(`merge-versions.py` の `AREAS`)と同じになり、 - 照合が通る組では 2 つの時点の比較が同じになる(等価変異)。試料を `feasibility/` や内部の Q&A 表に置き換えても、 - 比較(提案書は内部の層を載せない)に出ないので殺せないことを確かめた(どちらの置き換えでも変異が生き残った)。 - 代わりに「2 つのタグの間に要求を足すと照合で止まる」を足した。 +- ~~`test-build-proposal.py` の変異「比較を予算のタグの時点から作る」は等価変異~~——**誤りだった(1 パス目の F1・CR1 で反証)**。 + `removed.csv` の理由の列だけを 2 つのタグの間で変えると、照合は通り、提案書の「削除の理由」が変わる。変異と試料を戻した(試料は理由の列で作り直した)。 +- **I3 の「`removed.csv` の範囲の外のファイルの行だけ違う → 0」は、Issue の受入基準 B(予算側だけにある、`merge` が拒否する配置を 1 か 2 にする)と食い違っていた**—— + 計画を書いたときの写し違い(範囲の外のファイルの行は `merge` が拒否する)。1 パス目の F1 の副次の指摘で分かり、Issue の B に合わせて + **内部の層の Q&A の行を除いて全部比べる**ようにした(基準を緩めたのではなく、Issue の基準に戻した)。確認の Issue #478 にも書き足す。 + +### 1 パス目の指摘の採否 + +Verifier(sonnet): 反証 1 件(F1。副次の指摘つき)。`/code-review`(high): 8 件。どちらも `git status --porcelain` は前後とも空。 + +| # | 指摘 | 実物で失敗を示せるか | 破る受入基準 | 採否 | +| --- | --- | --- | --- | --- | +| F1・CR1 | 「比較を予算のタグの時点から作る」は等価変異ではない(範囲の外の行・理由の列だけ違う組で、照合は通り提案書が変わる) | 示せる(Verifier の試料: 変異で code 1。`/code-review` の `equiv_probe.py`: 理由が入れ替わる) | I4(既存の変異と試料を、偽の理由で外した) | 直す | +| F1 副次 | 予算側だけにある `ファイル,feasibility/x.md` の行(`merge` が拒否する)で照合が 0 | 示せる(Verifier の試料: 照合 0・`merge` が拒否) | I2(Issue の B——予算側だけの `merge` が拒否する配置) | 直す(内部の層の Q&A 以外の行を全部比べる) | +| CR2 | 前の版の `spec/qa-dev.csv` を外すことを試料が見ない(今の版だけ外す変異が生き残る) | 示せる(`qadev_probe.py`: 変異で exit 1、スイートは緑) | I3 | 直す(試料と変異を足した) | +| CR3 | 2 にする「比べる場所が空」の検査(`scope()`)と比べる範囲(`compare_scope()`)が別で、docstring が古い範囲で書いている | 示せる(docstring の文言と `scope()` の中身) | I5 | 直す(docstring を `shared/`(と `removed.csv`)に直した。振る舞いは変えない) | +| CR4 | `INTERNAL_QA`・`SCOPE_DIRS` が `merge-versions.py` の `INTERNAL_QA_PATH`・`AREAS` の写し | 示せない(ずれは起きていない) | 無し | 残課題 | +| CR5 | `qa-csv.md` の「`shared/` は比べる範囲なので内部の表を置かない」の理由が古い | 示せる(文言) | 無し(I5 の列挙の外) | 残課題 | +| CR6 | `README.md` の 93 行目が「共有の範囲(`shared/` ほか)」のままで、同じファイルの 27 行目と食い違う | 示せる(文言) | I5(`README.md` は列挙の中) | 直す | +| CR7 | 定数と `signoff-tags.md` の突き合わせが `DOCS` を見なくなった | 示せない(走らせていない) | 無し | 残課題 | +| CR8 | `requirements/`・`spec/` のモードだけの変更(chmod)も 1 になる | 示せない(走らせていない) | 無し | 残課題 | diff --git a/plugins/spec-loop/README.md b/plugins/spec-loop/README.md index 760bc61d..727b2425 100644 --- a/plugins/spec-loop/README.md +++ b/plugins/spec-loop/README.md @@ -90,7 +90,7 @@ ## `proposal` — 提案書のループ `/spec-loop:proposal` で呼びます。**同じ版の 2 つのサインオフのタグ(`signoff/v/req-`・`signoff/v/budget-`)の時点で -共有の範囲(`docs/v/shared/` ほか。`references/signoff-tags.md`)が一致するかをスクリプトで照合し、一致したときだけ提案書を生成します**(LLM に判断させません)。 +2 つの承認が見た中身(`docs/v/shared/`・`requirements/`・`spec/` ほか。`references/signoff-tags.md`)が一致するかをスクリプトで照合し、一致したときだけ提案書を生成します**(LLM に判断させません)。 提案書は**承認したタグの時点の中身**から作るので、承認の後に直した中身は入りません。 **v2 以降は、前の版との比較(追加・変更・削除)を載せた変更の提案書になります**(`references/versions.md` の「版の差分」)。 **提案書と一緒に仕様書(規模・構成の図・機能・決定表・範囲の外)も作ります。利用者にまだ確かめていない暫定の回答(`暫定反映`)が残るうちは作りません**(`references/tree.md` の「仕様書」・`references/qa-csv.md` の「暫定の行の判別」)。 diff --git a/plugins/spec-loop/references/signoff-tags.md b/plugins/spec-loop/references/signoff-tags.md index 1f82c203..9741ee3a 100644 --- a/plugins/spec-loop/references/signoff-tags.md +++ b/plugins/spec-loop/references/signoff-tags.md @@ -50,8 +50,8 @@ python3 /scripts/check-signoff.py --versi v の時点の中身は、前の版と、行を消す `removed.csv` にも依る。 凍結した版(`versions.md`)は動かないので、前の版を含めても誤って食い違うことはない。 **v1 の `removed.csv` は在ること自体を比べる**(v1 には置かない決まりなので、予算側にだけあっても一致と言わない。#372)。 - **`removed.csv` は範囲に効く行だけを比べる**(種類 `Q&A` で対象が共有の表の番号(`Q-`)の行・種類 `機能` の行・ - 種類 `ファイル` で対象が `shared/`・`requirements/`・`spec/` から始まる行)。 + **`removed.csv` は内部の層の Q&A の行(`D-`)を除いて全部比べる**——`feasibility/` などのファイルの行は + 重ね合わせ(`versions.md`)が拒否するので、予算側だけにあっても一致と言わない(#372)。 **比べないのは `feasibility/`・`proposal/`(予算の合意と決裁の側が書く)と内部の層**——内部の Q&A 表 (`spec/qa-dev.csv`。番号 `D-`)は開発者が決めて記録するもので、2 つのタグの間に feasibility が費用の行を書く。 サインオフの後に内部で決めても照合は割れない(`qa-csv.md` の「層」)。 diff --git a/plugins/spec-loop/scripts/check-signoff.py b/plugins/spec-loop/scripts/check-signoff.py index deb1d9e6..4f42da50 100644 --- a/plugins/spec-loop/scripts/check-signoff.py +++ b/plugins/spec-loop/scripts/check-signoff.py @@ -18,13 +18,14 @@ `requirements/`・`spec/` から数えた見積りに合意するので、そこが違えば別の中身を見ていたことになる(#367)。 v の時点の中身は前の版と、行を消す `removed.csv` にも依る。**v1 の `removed.csv` は在ること自体を比べる** (`merge` が拒否するものが予算側だけにあっても、一致と言わない。#372)。 - **`removed.csv` は範囲に効く行だけを比べる**——種類 `Q&A` で共有の層の番号(`Q-`)の行・種類 `機能` の行・ - 種類 `ファイル` で対象が `shared/`・`requirements/`・`spec/` から始まる行(`spec/qa-dev.csv` を除く)。 + **`removed.csv` は内部の層の Q&A の行(`D-`)を除いて全部比べる**——`feasibility/` などのファイルの行は + 重ね合わせ(`merge`)が拒否するので、予算側だけにあっても一致と言わない(#372 の B)。 **外すのは `feasibility/`・`proposal/`(予算の合意と決裁の側が書く)と内部の層**(`spec/qa-dev.csv`・`D-` の行。 開発者が決めて記録するもので、2 つのタグの間に feasibility が費用の行を書く。#339)。 - **`.` で始まる名前(`.gitkeep`・`.DS_Store`)は数えない**——重ね合わせ(`merge-versions.py`)が読まないので、 承認が見た範囲に入らない。 -- **どちらかの時点で比べる場所にファイルが 1 つも無ければ、一致とは言わない(2)。** +- **どちらかの時点で `shared/`(と `removed.csv`)にファイルが 1 つも無ければ、一致とは言わない(2)**—— + `requirements/`・`spec/` があっても(承認の中心は共有の表なので、`shared/` を空にしたタグを組にしない)。 比べる中身が無くても `git diff --quiet` は 0 を返すので、それを「一致」と読まない。 **どちらかの時点に `docs/v/` のファイルが 1 つも無いときも 2**——前の版の `shared/` があっても、 その時点に版 n はまだ無い。 @@ -144,7 +145,6 @@ def compare_scope(version: int) -> list[str]: # `removed.csv` の種類(`references/versions.md` の「削除」。`merge-versions.py` のものを使い、写さない) REMOVED_QA = versions.KIND_QA REMOVED_FILE = versions.KIND_FILE -REMOVED_FEATURE = versions.KIND_FEATURE def read_removed_at(repo: Path, commit: str, tag: str, path: str) -> list[tuple[int, str, str]] | None: @@ -188,17 +188,17 @@ def refused(tag: str, problems: list[str]) -> str: def removed_scope_rows(parsed: list[tuple[int, str, str]] | None) -> list[tuple[str, str]]: - """`removed.csv` の行のうち、比べる範囲に効く行(種類・対象)。 + """`removed.csv` の行のうち比べる行(種類・対象)——内部の層の Q&A の行を除いた全部。 理由の列は比べない——消したものが同じなら、2 つの承認が見た中身は同じなので。 - 無いことと、範囲に効く行が無いことは同じ(どちらも範囲から何も消さない)。 + 無いことと、比べる行が無いことは同じ(どちらも何も消さない)。 + 範囲の外(`feasibility/` など)のファイルの行も比べる——重ね合わせ(`merge`)が拒否する行が予算側だけにあるのを、 + 一致と言わない(#372 の B)。 """ rows = [] for _, kind, target in parsed or []: - # 内部の層の Q&A(`D-`。`spec/qa-dev.csv`)を消した行は範囲の外——開発者が決めるものでサインオフが割れないように(#339) - if ((kind == REMOVED_QA and not qa.is_internal_number(target)) - or kind == REMOVED_FEATURE - or (kind == REMOVED_FILE and target.startswith(SCOPE_DIRS) and target != INTERNAL_QA)): + # 内部の層の Q&A(`D-`。`spec/qa-dev.csv`)を消した行は比べない——開発者が決めるものでサインオフが割れないように(#339) + if not (kind == REMOVED_QA and qa.is_internal_number(target)): rows.append((kind, target)) return sorted(rows) @@ -399,7 +399,7 @@ def resolve(repo: Path, req: str | None = None, budget: str | None = None, if line and not all(hidden(path) for path in line.split("\t")[1:])] for path, (at_req, at_budget) in removed.items(): if removed_scope_rows(at_req) != removed_scope_rows(at_budget): - changed.append(f"{REMOVED}\t{path}(範囲に効く行が違う)") + changed.append(f"{REMOVED}\t{path}(比べる行が違う)") if changed: lines = [f"一致しない: {pair} で {where} が違う({len(changed)} 件)"] lines += [f" {line}" for line in changed] diff --git a/scripts/test-build-proposal.py b/scripts/test-build-proposal.py index dd1d31eb..d023f3f6 100644 --- a/scripts/test-build-proposal.py +++ b/scripts/test-build-proposal.py @@ -272,6 +272,13 @@ def pptx_ok(parts: dict) -> bool: # 比較は req のタグの時点から作る——承認の後に作業ツリーとコミットで足した要求は載らない ("承認の後に直した差分は比較に入らない", V2C + [("w", "docs/v2/requirements/late.md", "L\n"), ("c",)], False, lambda r: made(r) and "| requirements/c.md | 追加 |" in r.text and "late.md" not in r.text), + # 比較は req のタグの時点から作る——2 つのタグの間で removed.csv の理由の列だけを変える(照合は理由を比べないので通る。 + # 提案書の「削除の理由」は req のタグの時点のもの)。#375 で一度「等価変異」として外し、関門に反証された + ("比較は要求のタグの時点から作る", SIGNED_R + V2_CHANGES + [("c",), ("t", "signoff/v2/req-1"), + ("w", "docs/v2/removed.csv", "種類,対象,理由\nQ&A,Q-005,予算側でだけ書き換えた理由\n" + "ファイル,requirements/b.md,a.md に統合した\n機能,F-001,割引を廃止した\n"), + ("c",), ("t", "signoff/v2/budget-1")], False, + lambda r: made(r) and "夜間の停止を決めた" in r.text and "予算側でだけ書き換えた理由" not in r.text), # req のタグと budget のタグの間に要求を足すと、照合が 1 で止める(#375。見積りの根拠が承認と違う) ("2 つのタグの間に要求を足すと照合で止まる", SIGNED_R + V2_CHANGES + [("c",), ("t", "signoff/v2/req-1"), ("w", "docs/v2/requirements/between.md", "B\n"), ("c",), ("t", "signoff/v2/budget-1")], False, @@ -490,8 +497,8 @@ def pptx_ok(parts: dict) -> bool: ("比較を作業ツリーから作る", " copy_docs(top, commit, tag, Path(tmp))\n", " __import__('shutil').copytree(top / signoff.DOCS, Path(tmp) / signoff.DOCS, symlinks=True)\n", "承認の後に直した差分は比較に入らない"), - # 「比較を予算のタグの時点から作る」は #375 から等価変異——照合が比べる範囲(shared/・requirements/・spec/)が - # 重ね合わせの読むもの(merge-versions.py の AREAS)と同じになり、照合が通る組では 2 つの時点の比較が同じになる + ("比較を予算のタグの時点から作る", 'compare_at(top, info["req_commit"], info["req"]', + 'compare_at(top, info["budget_commit"], info["req"]', "比較は要求のタグの時点から作る"), ("大文字小文字だけ違うパスを断らない", " if clashes:\n", " if False:\n", "大文字小文字だけ違うパスは比較で断る"), ("ファイルとディレクトリの重なりを見ない", " if fold(parent) in seen:\n", " if False:\n", "ファイルとディレクトリが大文字小文字だけ違うと比較で断る"), diff --git a/scripts/test-check-signoff.py b/scripts/test-check-signoff.py index 7ebca62c..34379267 100644 --- a/scripts/test-check-signoff.py +++ b/scripts/test-check-signoff.py @@ -13,8 +13,8 @@ **`removed.csv` の範囲に効く行(共有の Q&A・範囲の中のファイル・機能)だけ**の違い・ **片側だけ `.` で始まる名前の rename**(読む側に新しいファイルが現れる)。 2. **一致する組を通す**(陽性対照。比べる場所の外だけ違う組も一致——**`feasibility/`・`proposal/`・`spec/qa-dev.csv` - だけ・`removed.csv` の範囲の外(`feasibility/`・`proposal/`・`spec/qa-dev.csv`)のファイルの行・内部の Q&A の行・ - 理由・行の順序だけが違う組も**。**`.` で始まる名前だけの違いも**——途中の `.` で始まるディレクトリの中も含む)。 + だけ・`removed.csv` の内部の Q&A の行・理由・行の順序だけが違う組も**(`removed.csv` のファイルの行は範囲の外の対象でも比べる—— + `merge` が拒否する行なので。#372 の B)。**`.` で始まる名前だけの違いも**——途中の `.` で始まるディレクトリの中も含む)。 3. **「最新」を数で選ぶ**(`-10` > `-9`)・**命名に合わないタグ(旧い `v/req-signoff-`・`req-signoff-v` を含む)を数えない**・ 指定したタグで照合する・**版を省けば作業中の版(番号の最大)**・`--version` で版を選ぶ・ **素の `v1` タグがあっても(先でも後でも)承認のタグを作れて照合が 0 になる**(#270)。 @@ -158,7 +158,7 @@ # 予算側は `merge` が受け付ける配置(v1 の Q-001 を v2 で消す。v2 は Q-001 を置かない——#349) ("removed.csv だけ違う", [("w", SHARED, QA_A), ("w", SHARED2, QA_HEAD + QA_2), ("c",), ("t", R2), ("w", REMOVED2, "種類,対象,理由\nQ&A,Q-001,不要\n"), ("c",), ("t", B2)], - [], 1, "docs/v2/removed.csv(範囲に効く行が違う)"), + [], 1, "docs/v2/removed.csv(比べる行が違う)"), # `merge` が拒否する配置は要求確定側だけ(予算側は受け付ける)——要求確定側の removed.csv で重ねていれば 2、 # 予算側で重ねると拒否されず、範囲の違いで 1 になる(#349) ("要求確定側だけが拒否する配置", [("w", SHARED, QA_A), ("w", SHARED2, QA_HEAD + QA_2), @@ -167,24 +167,31 @@ [], 2, "docs/v2/removed.csv:2: 前の版までに無いものを消そうとしている: Q&A Q-003"), ("removed.csv の shared のファイルの行が違う", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("c",), ("t", R2), ("w", REMOVED2, "種類,対象,理由\nファイル,shared/nfr.csv,不要\n"), ("c",), ("t", B2)], - [], 1, "docs/v2/removed.csv(範囲に効く行が違う)"), + [], 1, "docs/v2/removed.csv(比べる行が違う)"), # requirements/・spec/ のファイルと機能の削除は範囲の中——承認が見た中身から消すので(#375) ("removed.csv の requirements のファイルの行が違う", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("c",), ("t", R2), ("w", REMOVED2, "種類,対象,理由\nファイル,requirements/r.md,不要\n"), ("c",), ("t", B2)], - [], 1, "docs/v2/removed.csv(範囲に効く行が違う)"), + [], 1, "docs/v2/removed.csv(比べる行が違う)"), ("removed.csv の spec のファイルの行が違う", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("c",), ("t", R2), ("w", REMOVED2, "種類,対象,理由\nファイル,spec/spec.json,不要\n"), ("c",), ("t", B2)], - [], 1, "docs/v2/removed.csv(範囲に効く行が違う)"), + [], 1, "docs/v2/removed.csv(比べる行が違う)"), ("removed.csv の機能の行だけ違う", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("c",), ("t", R2), ("w", REMOVED2, "種類,対象,理由\n機能,F-001,不要\n"), ("c",), ("t", B2)], - [], 1, "docs/v2/removed.csv(範囲に効く行が違う)"), - # feasibility/・proposal/・内部の Q&A 表は範囲の外——removed.csv で消しても一致のまま - ("removed.csv の範囲の外の行だけ違う", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("c",), ("t", R2), + [], 1, "docs/v2/removed.csv(比べる行が違う)"), + # 範囲の外(feasibility/・proposal/・内部の Q&A 表)のファイルの行も比べる——merge が拒否する行が予算側だけにあるのを + # 一致と言わない(#372 の B。#375 の 1 パス目で反証された) + ("removed.csv の範囲の外のファイルの行も比べる", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("c",), ("t", R2), ("w", REMOVED2, "種類,対象,理由\nファイル,feasibility/f.md,不要\n" - "ファイル,proposal/p.md,不要\nファイル,spec/qa-dev.csv,不要\n"), + "ファイル,proposal/p.md,不要\n"), ("c",), ("t", B2)], + [], 1, "docs/v2/removed.csv(比べる行が違う)"), + # 前の版の内部の Q&A 表も外す(#375 の 1 パス目) + ("前の版の内部の Q&A 表だけ違う", [("w", SHARED, QA_A), ("w", "docs/v1/spec/qa-dev.csv", QA_HEAD), ("w", SHARED2, QA_B), + ("c",), ("t", R2), + ("w", "docs/v1/spec/qa-dev.csv", QA_HEAD + "D-001,仕様レビュー,分けるか,A: 分ける / B: 分けない,B,費用,未回答,—,確認中,\n"), + ("c",), ("t", B2)], [], 0, "v1〜v2 の shared/・requirements/・spec/(spec/qa-dev.csv を除く)と removed.csv は同じ"), # 内部の層の Q&A(spec/qa-dev.csv の D-)は共有の範囲の外——表を変えても、removed.csv で消しても一致のまま(#339) ("removed.csv の内部の Q&A の行だけ違う", [("w", SHARED, QA_A), ("w", SHARED2, QA_B), ("c",), ("t", R2), @@ -402,21 +409,19 @@ ("v1 の removed.csv を比べない", ' paths.append(f"{DOCS}/v1/{REMOVED}")\n', "", "v1 の removed.csv が予算側だけ"), ("removed.csv を比べない", "if removed_scope_rows(at_req) != removed_scope_rows(at_budget):", "if False:", "removed.csv だけ違う"), - ("removed.csv の行を絞らない", - "if ((kind == REMOVED_QA and not qa.is_internal_number(target))\n or kind == REMOVED_FEATURE\n" - " or (kind == REMOVED_FILE and target.startswith(SCOPE_DIRS) and target != INTERNAL_QA)):", - "if True:", "removed.csv の範囲の外の行だけ違う"), - ("removed.csv の機能の行を見ない", " or kind == REMOVED_FEATURE\n", "", "removed.csv の機能の行だけ違う"), - ("removed.csv の内部の Q&A 表の行を除かない", " and target != INTERNAL_QA)):", ")):", "removed.csv の範囲の外の行だけ違う"), - ("内部の Q&A を消した行も比べる", "(kind == REMOVED_QA and not qa.is_internal_number(target))", "kind == REMOVED_QA", + ("内部の Q&A を消した行も比べる", "if not (kind == REMOVED_QA and qa.is_internal_number(target)):", "if True:", "removed.csv の内部の Q&A の行だけ違う"), # 層の判定(`check-qa-csv.py` の `is_internal_number`)を入れ替える(#353) - ("比べる行の層を入れ替える", "(kind == REMOVED_QA and not qa.is_internal_number(target))", - "(kind == REMOVED_QA and qa.is_internal_number(target))", "removed.csv の内部の Q&A の行だけ違う"), - ("removed.csv の範囲のファイルの行を見ない", "and target.startswith(SCOPE_DIRS) and target != INTERNAL_QA)):", - "and False)):", "removed.csv の shared のファイルの行が違う"), - ("removed.csv の spec のファイルの行を見ない", "target.startswith(SCOPE_DIRS)", "target.startswith(SCOPE_DIRS[:2])", - "removed.csv の spec のファイルの行が違う"), + ("比べる行の層を入れ替える", "if not (kind == REMOVED_QA and qa.is_internal_number(target)):", + "if not (kind == REMOVED_QA and not qa.is_internal_number(target)):", "removed.csv の内部の Q&A の行だけ違う"), + ("範囲の外のファイルの行を比べない", "if not (kind == REMOVED_QA and qa.is_internal_number(target)):", + "if not (kind == REMOVED_QA and qa.is_internal_number(target)) and (kind != REMOVED_FILE or target.startswith(SCOPE_DIRS)):", + "removed.csv の範囲の外のファイルの行も比べる"), + ("機能の行を比べない", "if not (kind == REMOVED_QA and qa.is_internal_number(target)):", + "if not (kind == REMOVED_QA and qa.is_internal_number(target)) and kind != versions.KIND_FEATURE:", + "removed.csv の機能の行だけ違う"), + ("前の版の内部の Q&A 表を除かない", ' return paths + [f":(exclude){DOCS}/v{i}/{INTERNAL_QA}" for i in range(1, version + 1)]', + ' return paths + [f":(exclude){DOCS}/v{version}/{INTERNAL_QA}"]', "前の版の内部の Q&A 表だけ違う"), ("removed.csv の決まりを見ない", " if problems:\n raise Unjudged(refused(tag, problems))\n return parsed", " if False:\n raise Unjudged(refused(tag, problems))\n return parsed", "removed.csv が決まりに反する"), # #351 の前の形(前置きにもパスを書く)に戻す From 9caf7f545d008e360415f2b54f4b2927bd21321a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=88=E8=89=AF=20=E8=8B=B1=E6=A8=B9?= Date: Tue, 27 Oct 2026 20:03:47 +0900 Subject: [PATCH 4/8] =?UTF-8?q?issue-375:=20=E5=A4=89=E6=9B=B4=E7=AF=84?= =?UTF-8?q?=E5=9B=B2=E3=82=92=201=20=E8=A1=8C=201=20=E3=83=91=E3=82=BF?= =?UTF-8?q?=E3=83=BC=E3=83=B3=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/plans/issue-375.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/plans/issue-375.md b/docs/plans/issue-375.md index fc4bb81a..7cef2044 100644 --- a/docs/plans/issue-375.md +++ b/docs/plans/issue-375.md @@ -22,7 +22,9 @@ v1 の `removed.csv`・`shared/` の外のファイルの行を範囲に入れ - `plugins/spec-loop/references/tree.md` — 置き方の決まりの「2 つのサインオフが同じ範囲を見ていたかは `shared/` の中身で確かめる」 - `plugins/spec-loop/README.md` — `check-signoff.py` の行 - `CLAUDE.md` — `check-signoff.py` の行 -- `plugins/spec-loop/.claude-plugin/plugin.json`・`.claude-plugin/marketplace.json`・`plugins/spec-loop/skills/*/SKILL.md` — 版(0.26.0 → 0.27.0。0.x なので振る舞いの変更も minor) +- `plugins/spec-loop/.claude-plugin/plugin.json` — 版(0.26.0 → 0.27.0。0.x なので振る舞いの変更も minor) +- `.claude-plugin/marketplace.json` — 同じ版 +- `plugins/spec-loop/skills/*/SKILL.md` — 同じ版の 2 行 - `docs/plans/issue-375.md` — この計画 触らない: From 5ab1f23caac7e21369bbb27127ab0787c0fecd57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=88=E8=89=AF=20=E8=8B=B1=E6=A8=B9?= Date: Tue, 27 Oct 2026 20:04:06 +0900 Subject: [PATCH 5/8] =?UTF-8?q?issue-375:=202=20=E3=83=91=E3=82=B9?= =?UTF-8?q?=E7=9B=AE=E3=81=AE=E5=86=8D=E9=96=8B=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/plans/issue-375.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/plans/issue-375.md b/docs/plans/issue-375.md index 7cef2044..8b25ae35 100644 --- a/docs/plans/issue-375.md +++ b/docs/plans/issue-375.md @@ -108,7 +108,9 @@ Issue 本文は検査対象であって要件ではない。 - `/code-review` 1 パス目: 通した——指摘 8 件(CR1〜CR8) - 1 パス目の指摘対応: F1・F1 副次・CR2・CR3・CR6 を直した。差分は 0 行でない → 2 パス目を回す - 割り目(2 パス目の前): 不在のため訊かず続けた -- 2 パス目の起点: 下の 2 パス目の関門に渡すコミット(このコミットの次に書く) +- 2 パス目の起点: `9caf7f5`(この後のコミットは計画ファイルだけ) +- Verifier 2 パス目: 起動(sonnet) +- `/code-review` 2 パス目: 起動(high) ## 起票 From adaac0ccc066262b6e16fb89065a8f163631a144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=88=E8=89=AF=20=E8=8B=B1=E6=A8=B9?= Date: Tue, 27 Oct 2026 20:13:21 +0900 Subject: [PATCH 6/8] =?UTF-8?q?check-signoff:=20=E5=89=8D=E3=81=AE?= =?UTF-8?q?=E7=89=88=E3=81=AE=20requirements=20=E3=81=AE=E8=A9=A6=E6=96=99?= =?UTF-8?q?=E3=83=BB=E8=AA=AC=E6=98=8E=E3=82=92=E7=A9=BA=E3=81=8B=E3=82=92?= =?UTF-8?q?=E8=A6=8B=E3=82=8B=E5=A0=B4=E6=89=80=E3=81=AB=E6=8F=83=E3=81=88?= =?UTF-8?q?=E3=82=8B=E3=83=BB0.27.0=20=E3=81=AE=E7=A7=BB=E8=A1=8C=E3=81=AE?= =?UTF-8?q?=E7=AF=80=EF=BC=88#375=20=E3=81=AE=202=20=E3=83=91=E3=82=B9?= =?UTF-8?q?=E7=9B=AE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/plans/issue-375.md | 31 ++++++++++++++++---- plugins/spec-loop/references/signoff-tags.md | 15 ++++++++-- plugins/spec-loop/scripts/check-signoff.py | 3 +- scripts/test-check-signoff.py | 6 ++++ 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/docs/plans/issue-375.md b/docs/plans/issue-375.md index 8b25ae35..528254ee 100644 --- a/docs/plans/issue-375.md +++ b/docs/plans/issue-375.md @@ -50,7 +50,7 @@ Issue の「決めること」A・B・C。**線は `仕様の穴 = 推奨で決 `qa-dev.csv` は feasibility が 2 つのタグの間に正当に書く(費用の行)うえ、#339 で「内部の層はサインオフを割らない」と決めている。 - **B(#372): 予算側だけにある、`merge` が拒否する配置** - 選択肢: (a) 比べる範囲に `docs/v1/removed.csv` と、範囲の中のファイル・機能の行を入れる / (b) 予算側にも配置の検査を当てる(2) - - **決めた値: (a)**。`removed.csv` で比べる行は「共有の層の Q&A」「範囲の中(`shared/`・`requirements/`・`spec/`。`spec/qa-dev.csv` を除く)のファイル」「機能」。 + - **決めた値: (a)**。`removed.csv` で比べる行は**内部の層の Q&A(`D-`)以外の全部**(1 パス目の F1 副次で、最初の「範囲の中のファイル・機能・共有の Q&A」からIssue の B に合わせて直した——範囲の外のファイルの行は `merge` が拒否する)。 - 理由: A と同じ「範囲」の定義で説明できる(C)。(b) は予算側の時点に `merge` の読み方を写すことになる(#348 で避けた形)。 - **C**: 範囲は「2 つの承認が見た中身」= v1〜v の `shared/`・`requirements/`・`spec/`(`qa-dev.csv` を除く)と `removed.csv`(v1 を含む)。 **外すのは `feasibility/`・`proposal/`(予算の合意と決裁の側が書くもの)と内部の層だけ。** @@ -76,7 +76,8 @@ Issue 本文は検査対象であって要件ではない。 - **I1(#367)**: 2 つのタグの時点で `requirements/`・`spec/`(`spec/qa-dev.csv` を除く)の中身が違えば、一致とは言わない(1。違うパスを出す)。前の版のものも同じ。 - **I2(#372)**: 予算側だけに `docs/v1/removed.csv` がある・`removed.csv` の範囲の中(`requirements/`・`spec/`)のファイルの行や機能の行が違うなら、一致とは言わない(1)。 -- **I3(陽性対照)**: 範囲の外(`feasibility/`・`proposal/`・`spec/qa-dev.csv`・内部の層の Q&A の行・`removed.csv` の範囲の外のファイルの行・理由の列・行の順序・`.` で始まる名前)だけが違えば、今までどおり 0。 +- **I3(陽性対照)**: 範囲の外(`feasibility/`・`proposal/`・`spec/qa-dev.csv`・内部の層の Q&A の行・理由の列・行の順序・`.` で始まる名前)だけが違えば、今までどおり 0。 + (最初は「`removed.csv` の範囲の外のファイルの行」も 0 に入れていたが、Issue の B と食い違う写し違いだったので外した——下の「既知の限界」。) - **I4**: 今までの判定(2 の条件・3 の暫定・`shared/` の比較)は変わらない(既存の試料が緑)。 - **I5**: 範囲の説明(docstring・`signoff-tags.md`・`tree.md`・`README.md`・`CLAUDE.md`)が本体の範囲と一致する。 @@ -85,7 +86,7 @@ Issue 本文は検査対象であって要件ではない。 | 経路 | 守り方 | 確かめ方 | | --- | --- | --- | | `git diff` の比べる場所(`compare_scope`) | `requirements/`・`spec/`・`docs/v1/removed.csv` を足し、`spec/qa-dev.csv` を除く | 試料: `spec/` だけ違う・`requirements/` だけ違う・前の版の `spec/` だけ違う・v1 の `removed.csv` が予算側だけ(1)/`qa-dev.csv` だけ違う・`feasibility/` だけ違う(0) | -| `removed.csv` の行(`removed_scope_rows`) | 範囲の中のファイル・機能・共有の Q&A の行を比べる | 試料: `ファイル,spec/…`・`ファイル,requirements/…`・`機能,…` の行が予算側だけ(1)/`ファイル,feasibility/…`・`D-` の行だけ(0) | +| `removed.csv` の行(`removed_scope_rows`) | 内部の層の Q&A 以外の行を全部比べる | 試料: `ファイル,spec/…`・`ファイル,requirements/…`・`ファイル,feasibility/…`・`機能,…` の行が予算側だけ(1)/`D-` の行だけ(0) | | `build-proposal.py`(`resolve` を呼ぶ) | 本体を変えない | `test-build-proposal.py` が緑。2 つのタグの間に `requirements/` を足すと作らない | 各経路の守りを外す変異で赤になる(陽性対照つき)。 @@ -109,8 +110,10 @@ Issue 本文は検査対象であって要件ではない。 - 1 パス目の指摘対応: F1・F1 副次・CR2・CR3・CR6 を直した。差分は 0 行でない → 2 パス目を回す - 割り目(2 パス目の前): 不在のため訊かず続けた - 2 パス目の起点: `9caf7f5`(この後のコミットは計画ファイルだけ) -- Verifier 2 パス目: 起動(sonnet) -- `/code-review` 2 パス目: 起動(high) +- Verifier 2 パス目: 通した——反証 1 件(S1) +- `/code-review` 2 パス目: 通した——指摘 10 件(P1〜P10) +- 2 パス目の指摘対応: P1・P2(計画の文言)・P3・P4・P9・P10 を直した。終点は下の狭いパスに渡すコミット +- 狭いパス: 起動(sonnet) ## 起票 @@ -141,3 +144,21 @@ Verifier(sonnet): 反証 1 件(F1。副次の指摘つき)。`/code-revi | CR6 | `README.md` の 93 行目が「共有の範囲(`shared/` ほか)」のままで、同じファイルの 27 行目と食い違う | 示せる(文言) | I5(`README.md` は列挙の中) | 直す | | CR7 | 定数と `signoff-tags.md` の突き合わせが `DOCS` を見なくなった | 示せない(走らせていない) | 無し | 残課題 | | CR8 | `requirements/`・`spec/` のモードだけの変更(chmod)も 1 になる | 示せない(走らせていない) | 無し | 残課題 | + +### 2 パス目の指摘の採否 + +Verifier(sonnet): 反証 1 件(S1)。`/code-review`(high): 10 件。どちらも `git status --porcelain` は前後とも空。 + +| # | 指摘 | 実物で失敗を示せるか | 破る受入基準 | 採否 | +| --- | --- | --- | --- | --- | +| S1 | `removed.csv` の同じ行が片側だけ 2 回あるのを見る試料が無い(`sorted(set(rows))` の変異が生き残る) | 示せる(Verifier の変異) | 無し(この周より前からの振る舞い) | 残課題 | +| P1 | 前の版の `requirements/` だけ違う試料が無い(前の版の `requirements/` を外す変異が生き残る) | 示せる(`/code-review` の試料: 本体 1・変異 0) | I1(前の版も同じ) | 直す(試料と変異を足した) | +| P2 | 計画の I3・経路の表・決めた値 B が、1 パス目の対応の後も古いまま | 示せる(文言) | — (計画ファイル。成果物ではない) | 直す(計画の文言) | +| P3 | `signoff-tags.md` の「比べる場所が無い・空なら 2」が、空かを見る場所(`shared/`)と食い違う | 示せる(文言と `scope()`) | I5 | 直す | +| P4 | `scope()` の docstring が「比べる場所」のまま | 示せる(文言) | I5 | 直す | +| P5 | 大文字小文字だけ違う `Requirements/` が予算側だけにあっても 0(macOS の重ね合わせは読む) | 示せる(`/code-review` の試料: 0) | 無し(`shared/` から同じ穴。#276 が台帳にある) | 残課題(#276) | +| P6 | `spec/spec_ir.json`(scaffold が書く)を 2 つのタグの間で書くと 1 | 示せない(走らせていない) | 無し | 残課題(#478 に書き足す) | +| P7 | 日本語のパスが `\346…` のように 8 進で出る(`core.quotePath`) | 示せる(`/code-review` の試料) | 無し(パスは出ている) | 残課題 | +| P8 | 公開ページ・description の「共有の範囲(shared/ ほか)」 | 示せる(文言) | 無し(I5 の列挙の外。決着済みの「ほか」) | 残課題 | +| P9 | `signoff-tags.md` の「比べないのは `feasibility/`・`proposal/` と内部の層」が、ほかの置き場所(`notes/` など)も比べないことと食い違う | 示せる(`/code-review` の試料: `notes/a.md` だけ違って 0) | I5 | 直す | +| P10 | 0.x の壊れる変更を minor で入れたのに、移行の手順を `references/` に書いていない | 示せる(`CLAUDE.md` の「プラグインの更新」の規定) | 無し(受入基準の外だが、対象リポジトリの `CLAUDE.md` の規約——原則「`CLAUDE.md` のルールを守る」) | 直す(`signoff-tags.md` に移行の節) | diff --git a/plugins/spec-loop/references/signoff-tags.md b/plugins/spec-loop/references/signoff-tags.md index 9741ee3a..13406b52 100644 --- a/plugins/spec-loop/references/signoff-tags.md +++ b/plugins/spec-loop/references/signoff-tags.md @@ -52,14 +52,14 @@ python3 /scripts/check-signoff.py --versi **v1 の `removed.csv` は在ること自体を比べる**(v1 には置かない決まりなので、予算側にだけあっても一致と言わない。#372)。 **`removed.csv` は内部の層の Q&A の行(`D-`)を除いて全部比べる**——`feasibility/` などのファイルの行は 重ね合わせ(`versions.md`)が拒否するので、予算側だけにあっても一致と言わない(#372)。 - **比べないのは `feasibility/`・`proposal/`(予算の合意と決裁の側が書く)と内部の層**——内部の Q&A 表 + **比べないのは、上の範囲の外の全部**(`feasibility/`・`proposal/` など、予算の合意と決裁の側が書くもの。重ね合わせも読まない)**と内部の層**——内部の Q&A 表 (`spec/qa-dev.csv`。番号 `D-`)は開発者が決めて記録するもので、2 つのタグの間に feasibility が費用の行を書く。 サインオフの後に内部で決めても照合は割れない(`qa-csv.md` の「層」)。 - **版を指定しなければ、作業中の版**(作業ツリーの `docs/` で番号が最大の版)。 - **最新**は、`` を**数として**比べて最大のもの(`-10` は `-9` より新しい)。 **`signoff/v2/req-3-fix` のような、命名に合わないタグは数えない。** -- **どちらかの時点で比べる場所が無い・空なら、一致とは言わない**(判定していない)。 - 比べる中身が無いときも `git diff` は「差分なし」を返すので。**どちらかの時点に `docs/v/` が無いときも同じ** +- **どちらかの時点で `shared/`(と `removed.csv`)にファイルが 1 つも無ければ、一致とは言わない**(判定していない)—— + `requirements/`・`spec/` があっても。比べる中身が無いときも `git diff` は「差分なし」を返すので。**どちらかの時点に `docs/v/` が無いときも同じ** (前の版の `shared/` があっても、その時点に版 n はまだ無い)。 - **作業ツリーの版の並びが決まりに反すれば、版を指定しても判定しない。** @@ -71,6 +71,15 @@ CI で走らせるなら、タグを取得できるよう履歴をすべて取 **タグはスキルが付けない。** 承認は人間の判断なので、スキルの仕事は 「どのコミットを承認してもらうか」をそろえて止まるところまでである。 +### 範囲を広げた(spec-loop 0.27.0) + +**spec-loop 0.27.0 で、比べる範囲に `requirements/`・`spec/`(`qa-dev.csv` を除く)と v1 の `removed.csv`、 +`removed.csv` の範囲の外のファイルの行を足した**(#375)。0.26.x までに「一致する」だったタグの組が、 +**2 つのタグの間で `requirements/`・`spec/` を直していれば 1 になる**(提案書も作らない)。 +1 になったら、出力に出るパスを見て、**直したあとの中身で利用者の要求確定(`req-`)を取り直し、予算の合意(`budget-`)も取り直す** +——見積りは直したあとの `requirements/`・`spec/` から数えたものか、数え直してから合意してもらう。 +0 のままの組は何もしなくてよい。 + ## 暫定の回答が残るうちは、要求確定のサインオフを付けない Q&A 表に `暫定反映`(暫定の回答で仕様に反映し、利用者の確認待ち。`qa-csv.md`)の行が残っているうちは、 diff --git a/plugins/spec-loop/scripts/check-signoff.py b/plugins/spec-loop/scripts/check-signoff.py index 4f42da50..db0db4f3 100644 --- a/plugins/spec-loop/scripts/check-signoff.py +++ b/plugins/spec-loop/scripts/check-signoff.py @@ -122,7 +122,8 @@ def tag_form(prefix: str, version: int) -> str: def scope(version: int) -> list[str]: - """比べる場所(版 1〜n の `shared/` と、版 2〜n の `removed.csv`)。""" + """空かを見る場所(版 1〜n の `shared/` と、版 2〜n の `removed.csv`)——どちらかの時点で 1 つも無ければ 2。 + 比べる場所は `compare_scope`(`requirements/`・`spec/` も含む)。""" return shared_scope(version) + removed_scope(version) diff --git a/scripts/test-check-signoff.py b/scripts/test-check-signoff.py index 34379267..625add6c 100644 --- a/scripts/test-check-signoff.py +++ b/scripts/test-check-signoff.py @@ -245,6 +245,9 @@ ("spec だけ違う", [("w", SHARED, QA_A), ("w", "docs/v1/spec/s.md", "x\n"), ("c",), ("t", R1), ("w", "docs/v1/spec/s.md", "y\n"), ("c",), ("t", B1)], [], 1, "M\tdocs/v1/spec/s.md"), + ("前の版の requirements だけ違う", [("w", SHARED, QA_A), ("w", "docs/v1/requirements/r.md", "x\n"), ("w", SHARED2, QA_B), + ("c",), ("t", R2), ("w", "docs/v1/requirements/r.md", "y\n"), ("c",), ("t", B2)], + [], 1, "M\tdocs/v1/requirements/r.md"), ("前の版の spec だけ違う", [("w", SHARED, QA_A), ("w", "docs/v1/spec/s.md", "x\n"), ("w", SHARED2, QA_B), ("c",), ("t", R2), ("w", "docs/v1/spec/s.md", "y\n"), ("c",), ("t", B2)], [], 1, "M\tdocs/v1/spec/s.md"), @@ -400,6 +403,9 @@ "for i in range(version, version + 1) for d in SCOPE_DIRS]", "前の版の shared だけ違う"), ("前の版の spec を範囲に含めない", "for i in range(1, version + 1) for d in SCOPE_DIRS]", "for i in range(1, version + 1) for d in SCOPE_DIRS if d == SHARED or i == version]", "前の版の spec だけ違う"), + ("前の版の requirements を範囲に入れない", "for i in range(1, version + 1) for d in SCOPE_DIRS]", + "for i in range(1, version + 1) for d in SCOPE_DIRS if d != \"requirements/\" or i == version]", + "前の版の requirements だけ違う"), ("requirements を範囲に入れない", 'SCOPE_DIRS = (SHARED, "requirements/", "spec/")', 'SCOPE_DIRS = (SHARED, "spec/")', "requirements だけ違う"), ("spec を範囲に入れない", 'SCOPE_DIRS = (SHARED, "requirements/", "spec/")', 'SCOPE_DIRS = (SHARED, "requirements/")', From 37c587eb8938dae3ef4ebc8c356210bdfe1fee77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=88=E8=89=AF=20=E8=8B=B1=E6=A8=B9?= Date: Tue, 27 Oct 2026 20:20:05 +0900 Subject: [PATCH 7/8] =?UTF-8?q?issue-375:=20=E7=8B=AD=E3=81=84=E3=83=91?= =?UTF-8?q?=E3=82=B9=E3=81=AE=E7=B5=90=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/plans/issue-375.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/plans/issue-375.md b/docs/plans/issue-375.md index 528254ee..29d640bc 100644 --- a/docs/plans/issue-375.md +++ b/docs/plans/issue-375.md @@ -112,8 +112,9 @@ Issue 本文は検査対象であって要件ではない。 - 2 パス目の起点: `9caf7f5`(この後のコミットは計画ファイルだけ) - Verifier 2 パス目: 通した——反証 1 件(S1) - `/code-review` 2 パス目: 通した——指摘 10 件(P1〜P10) -- 2 パス目の指摘対応: P1・P2(計画の文言)・P3・P4・P9・P10 を直した。終点は下の狭いパスに渡すコミット -- 狭いパス: 起動(sonnet) +- 2 パス目の指摘対応: P1・P2(計画の文言)・P3・P4・P9・P10 を直した。起点 `9caf7f5` → 終点 `adaac0c` +- 狭いパス: 通した(sonnet)——反証 0 件(P10 の移行の記述を 0.26.0 と 0.27.0 の本体で当てて一致) +- PR: 作る(マージは止まる——このリポジトリのマージはサイトを再デプロイするので `マージ` の線が効かない。#476) ## 起票 From 40d5480f625404e6fc697acabdd7b38c782ae3b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=88=E8=89=AF=20=E8=8B=B1=E6=A8=B9?= Date: Wed, 28 Oct 2026 16:59:01 +0900 Subject: [PATCH 8/8] =?UTF-8?q?issue-375:=20main=20=E3=81=AE=E5=8F=96?= =?UTF-8?q?=E3=82=8A=E8=BE=BC=E3=81=BF=EF=BC=88=E8=A1=9D=E7=AA=81=E8=A7=A3?= =?UTF-8?q?=E6=B6=88=E3=83=BB=E7=89=88=E3=81=AE=E4=B8=8A=E3=81=92=E7=9B=B4?= =?UTF-8?q?=E3=81=97=EF=BC=89=E3=82=92=E5=86=8D=E9=96=8B=E7=82=B9=E3=81=AB?= =?UTF-8?q?=E8=A8=98=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/plans/issue-375.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plans/issue-375.md b/docs/plans/issue-375.md index 29d640bc..3797cc8a 100644 --- a/docs/plans/issue-375.md +++ b/docs/plans/issue-375.md @@ -115,6 +115,7 @@ Issue 本文は検査対象であって要件ではない。 - 2 パス目の指摘対応: P1・P2(計画の文言)・P3・P4・P9・P10 を直した。起点 `9caf7f5` → 終点 `adaac0c` - 狭いパス: 通した(sonnet)——反証 0 件(P10 の移行の記述を 0.26.0 と 0.27.0 の本体で当てて一致) - PR: 作る(マージは止まる——このリポジトリのマージはサイトを再デプロイするので `マージ` の線が効かない。#476) +- main の取り込み(衝突解消・版の上げ直し): `ccaa49e`(#440・#476 まで)と `f88215c`(#442 まで)。衝突は 2 回とも spec-loop の版の行だけで、周の 0.27.0 を取った(main は 0.26.1 → 0.26.2。0.27.0 はその次の minor のまま)。正味の差分は取り込み前と同じ 15 ファイル ## 起票