diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 23627e2b..65ee61f3 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.2", + "version": "0.29.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/docs/plans/issue-465.md b/docs/plans/issue-465.md new file mode 100644 index 00000000..05f9f82d --- /dev/null +++ b/docs/plans/issue-465.md @@ -0,0 +1,137 @@ +# Issue #465 — scaffold.py --check が rules.md(決定表)と spec_ir.json の一致を見ない + +`scaffold.py --check` が見るのは `spec_ir.json` ↔ `scaffold/` の 1 本だけで、仕様(決定表)↔ `spec_ir.json` の一致は機械で確かめていない。 +中間表現のケースに**仕様の中の番号(`ref`)**を持たせ、生成と `--check` がその番号が仕様の原文に在るかを確かめる。 + +**不在の周**である——出所は `/dev-loop:cruise 474 --absent`(2026-10-27、利用者が会話で「-absent でまわします」と指示)。 +台帳 `issue-474` の確かめた線: `仕様の穴 = 推奨で決めて起票`・`直す線 = 既定`・`マージ = 関門を通ればマージ`・`周の途中の課題 = 台帳に足して直す` +(出所: cruise の確かめた線(台帳 `issue-474`))。**このリポジトリのマージはサイトを再デプロイするので `マージ` の線は効かない**(#476)——PR まで作って止まる。 + +## 変更範囲 + +触る: + +- `plugins/spec-loop/scripts/scaffold.py` — ケースの `ref` を許し、`check_ref` で機能の `source` の原文に番号が在るかを見る・docstring の「守らないもの」 +- `plugins/spec-loop/references/spec-ir.md` — ケースの `ref` の行 +- `plugins/spec-loop/skills/scaffold/SKILL.md` — 手順 2 に `ref` を書く指示・版の 2 行 +- `scripts/test-scaffold.py` — 試料と変異 +- `plugins/spec-loop/.claude-plugin/plugin.json` — 版(0.26.0 → 0.29.0。機能の追加で minor。#477・#479・#481 の版と重ならない値) +- `.claude-plugin/marketplace.json` — 同じ版 +- `plugins/spec-loop/skills/*/SKILL.md` — 同じ版の 2 行 +- `docs/plans/issue-465.md` — この計画 + +触らない: + +- 決定表の値(入力・期待)と中間表現のケースの値の突き合わせ(下の「仕様の穴」で見送った) +- `check-spec-exit.py` + +### 同じことを述べている箇所の数 + +「`ref`」: `spec-ir.md` の表・`scaffold.py` の docstring(守らないもの)・scaffold の `SKILL.md` 手順 2 の 3 箇所。分けない——同じ決定の写し。 + +## 仕様の穴(不在の線で推奨により決めた) + +Issue の案は「決定表の行と `spec_ir.json` のケースを突き合わせ、過不足と値の食い違いで赤」と、 +「書式が機械で読めないなら、少なくともケースの出所(R-xx / ケース ID)が仕様に実在するか」の 2 つ。**線は `仕様の穴 = 推奨で決めて起票`**。 + +- 選択肢: (a) ケースに任意の `ref` を足し、その番号が `source` の原文に在るかだけを見る / (b) 仕様の決定表の書式を spec-loop で決め、行と値を突き合わせる / (c) `ref` を必須にする +- **決めた値: (a)** +- 理由: 仕様の決定表・テストケースの書式は spec-loop の決まりに無い(`spec-exit.md` は「番号を振っていれば番号の数、振っていなければ表のデータの行の数」と両方を許す)。(b) は書式を決める大きな変更で、既存の正本リポジトリの仕様を書き直させる。(c) は番号を振らない仕様を書き起こせなくなる。 + (a) は Issue の 2 つ目の案そのもので、#465 の実データ(`R-F10` の値を変えた仕様と古い中間表現)は**番号を残したまま値だけ変えた**ので拾えない——**値のずれは拾わない**ことを、確認の Issue に書く。 + +## 乗るデプロイ経路 + +プラグイン(`plugins/spec-loop/`)の版を上げてマージ。マージで main へ push されるとサイトが再デプロイされる(`docs/plans/` を足す)。 + +## 周の在り処 + +- ブランチ: `issue/465-scaffold-rules-check` +- worktree: `/Users/hdknr/Projects/hdknr/claude-code-setup/.claude/worktrees/issue-465` +- 起点: `eab5e13` +- base: 参照 `origin/main`(`refs/remotes/origin/HEAD`)→ `eab5e1387a3fa7587aed615754fcb7bc35cd75e0` + +## verify の受入基準 + +Issue 本文は検査対象であって要件ではない。 + +不変条件: + +- **I1**: ケースの `ref` が機能の `source` の原文のどれにも無ければ、生成も `--check` も断る(1)。別の番号の一部(`R-F100`・`R-F10a`)は在ることにしない。 +- **I2(陽性対照)**: `ref` が原文に在れば通る。`ref` を書かないケースは今までどおり。 +- **I3**: `ref` を探すのは機能の `source` の原文(`spec/` の中)だけ——`requirements/` などほかのファイルや、全体の根の外は読まない。 +- **I4**: 空・複数行・前後に空白のある `ref` は断る。 +- **I5**: 既存の試料と変異が緑。 + +経路: + +| 経路 | 守り方 | 確かめ方 | +| --- | --- | --- | +| 生成(`scaffold.py`)と `--check`・`--dry-run` が通る `check_ir` | ケースの検査で `check_ref` を呼ぶ | 試料: 在る(0)・無い(1)・別の番号の一部(1)・空(1)・`requirements/` にだけ在る(1)・`--check` でも無いと 1 | +| `check_ref` の原文の選び方 | `source` のうち `spec/` の中のファイルだけ | 試料: `requirements/r.md` にだけ番号がある | + +各経路の守りを外す変異で赤になる。 + +生成物: `skill-metrics.py` の生成ブロックは dev-loop の `SKILL.md` だけ・図の書き出しも無い——**生成物なし**。 + +未証明: なし。 + +## 未解決の判断 + +上の「仕様の穴」を確認の Issue #482 で利用者に渡す(台帳の `decisions` に記録)。 + +## 関門の進捗(再開点) + +- 直す線: `既定`(出所: cruise の確かめた線(台帳 `issue-474`)) +- 割り目(手順 4 の完了時): 不在のため訊かず続けた +- 実験: `test-scaffold.py`(最初 試料 200 件・変異 144 件 → 2 パス目の対応の後 208 件・153 件) +- 1 パス目の起点: `c67289f`(この後のコミットは計画ファイルだけ) +- Verifier 1 パス目: 通した——反証 2 件(V1・V2) +- `/code-review` 1 パス目: 通した——指摘 7 件(CR1〜CR7) +- 1 パス目の指摘対応: CR1・V1/CR3・V2/CR2・CR4 を直した。差分は 0 行でない → 2 パス目を回す +- 割り目(2 パス目の前): 不在のため訊かず続けた +- 2 パス目の起点: `923fb41`(この後のコミットは計画ファイルだけ) +- Verifier 2 パス目: 通した——反証 1 件(W1) +- `/code-review` 2 パス目: 通した——指摘 8 件(P1〜P8) +- 2 パス目の指摘対応: P2・P3・W1・P5・P7・P8 を直した。起点 `923fb41` → 終点 `883cdd6` +- 狭いパス: 通した(sonnet)——指摘 2 件(人間が決める): (1) 全角の区切り・全角の数字(`R-F10.1`・`R-F10.1`)を別の番号とみなさない (2) 境界の文字クラスの要素(`_`・`-`・全角の数字・`:` など)12 件を 1 つずつ欠く変異が ref の試料 14 件では素通りする +- PR: 作る(マージは止まる。#476) +- main の取り込み(PR #483 が CONFLICTING): `origin/main`(`9950e48`)を merge。衝突は spec-loop の版の行だけ(7 ファイル・各 1 塊)で、周の側の 0.29.0 を取った——main は 0.26.2 で、0.27.0・0.28.0 は並行する PR(#479・#481)と重なるので 0.29.0 のまま。main が変えたのは `build-proposal.py` とそのテスト・`tree.md`・dev-loop の `absent-lines` で、周の差分(`scaffold.py`・`test-scaffold.py`・`spec-ir.md`)とは重ならない + +## 起票 + +- この周の起票: 1 件(仕様の穴の確認の Issue。起票の承認は不在のため訊いていない) + +## 既知の限界・決着済みの論点 + +- 値(入力・期待)の突き合わせはしない——番号を残したまま値だけ変えた仕様は拾えない(仕様の穴として推奨で決めた。確認の Issue)。 +- `ref` は任意——番号を振らない仕様の機能には書けない。 +- 版 0.29.0 は、並行する PR(#477 が 0.26.1・#479 が 0.27.0・#481 が 0.28.0)と重ならないための値(CR7 は決着済み)。 + +### 1 パス目の指摘の採否 + +Verifier(sonnet): 反証 2 件。`/code-review`(high): 7 件。どちらも `git status --porcelain` は前後とも空。 + +| # | 指摘 | 実物で失敗を示せるか | 破る受入基準 | 採否 | +| --- | --- | --- | --- | --- | +| CR1 | `R-F10.1`・`R-F10/2` しか無い原文で `ref` = `R-F10` を在ることにする | 示せる(`/code-review` の試料で exit 0) | I1 | 直す(後ろに `.`・`/`・`:` と英数字が続く形を別の番号とみなす。文末の `R-F10.` は在る) | +| V1・CR3 | 前後に空白のある・複数行の `ref` の試料と変異が無い(空の試料だけ) | 示せる(Verifier の変異で素通り) | I4 | 直す(試料 2 件・変異 2 件) | +| V2・CR2 | 「仕様の外のファイルで探さない」の試料が `requirements/r.md` を `source` に入れておらず、守りを外しても緑 | 示せる(Verifier の変異で緑) | I3 | 直す(`source` に入れた試料と、守りを外す変異) | +| CR4 | 計画の「各経路の守りを外す変異で赤」が I3 では偽 | 示せる(V2 と同じ) | — (計画ファイル) | 直す(V2 の対応で真にした) | +| CR5 | `check_ref` の原文の選び方が `check_source` の写しで、`spec/spec_ir.json` を読みうる | 示せない(今は `check_source` が 1 にする) | 無し | 残課題 | +| CR6 | 読めない原文を黙って飛ばし「書き起こしの誤り」と言う | 示せない(走らせていない) | 無し(終了コードは 1 で正しい) | 残課題 | +| CR7 | 版が 0.26.0 → 0.29.0 と飛ぶ | 示せる | 無し | 残課題(並行する PR と重ならない値。上の決着)| + +### 2 パス目の指摘の採否 + +Verifier(sonnet): 反証 1 件(W1)。`/code-review`(high): 8 件。どちらも `git status --porcelain` は前後とも空。 + +| # | 指摘 | 実物で失敗を示せるか | 破る受入基準 | 採否 | +| --- | --- | --- | --- | --- | +| P1 | **v2 で既存のケースに `ref` を足すと、機能の比較(`compare_maps`)と指紋(`fingerprint`)が機能を「変更」とみなす**——Red テストは変わらないのに変更の Issue の本文が出て、登録済みの Issue の指紋も古くなる | 示せない(読解。走らせていない) | 無し(受入基準は `ref` を書かないケースと書いたケースの検査だけ) | **残課題(マージの前に人間が判断)** | +| P2 | 1 パス目の対応で `R-F10/R-F11`・`R-F10.md` の `R-F10` を「無い」と言うようになった | 示せる(`/code-review` の試料) | I2 | 直す(区切りの後ろは数字だけを別の番号とみなす) | +| P3・W1 | 前の境界が非対称——`XR-F10`・`R-F10.1` の `1` を在ることにし、前に付く形の試料・変異が無い | 示せる(Verifier の変異・`/code-review` の試料) | I1 | 直す(前の境界を揃え、数字で始まる `ref` は前の小番号の区切りも見る。試料と変異) | +| P4 | `SKILL.md` の「仕様の後からの修正とのずれを拾う」が言い過ぎ(値は見ない・取り消し線やコメントの番号も在る) | 示せる(文言と試料) | 無し | 残課題 | +| P5 | 境界が ASCII だけ——`R-F10a`(全角)を別の番号とみなさない | 示せる(`/code-review` の試料) | I1 | 直す(全角の英数字も境界に入れる) | +| P6 | ケースごとに原文を読み直す(N×M) | 示せない(性能) | 無し | 残課題 | +| P7 | 計画の件数が古い | 示せる | — (計画ファイル) | 直す | +| P8 | `--dry-run` の試料が無い(v2 の試料も無い) | 示せる(試料の一覧) | I1(経路の表の `--dry-run`) | 直す(`--dry-run` の試料。v2 は P1 と一緒に残課題) | diff --git a/plugins/spec-loop/.claude-plugin/plugin.json b/plugins/spec-loop/.claude-plugin/plugin.json index a3e92ff1..994f7e1c 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.2", + "version": "0.29.0", "author": { "name": "hdknr" } } diff --git a/plugins/spec-loop/references/spec-ir.md b/plugins/spec-loop/references/spec-ir.md index 514a593d..1da33376 100644 --- a/plugins/spec-loop/references/spec-ir.md +++ b/plugins/spec-loop/references/spec-ir.md @@ -126,6 +126,7 @@ python3 /scripts/scaffold.py --repo <正 | `expected` | 正常時に返すもの。**`operation.output` の項目をちょうど全部**、その型(と `enum`)で | | `error` | 失敗するケースなら、`expected` の代わりに `operation.errors` のどれか | | `qa` | このケースの根拠にした Q&A の `No` の配列(省いてよい)。機能の `qa` と同じ決まり | +| `ref` | このケースの元になった、仕様の中の行の番号(決定表の `R-F10`・テストケースの `F1-5b` など。省いてよい)。**仕様が番号を振っていれば書く**——生成と `--check` が、機能の `source` の原文にその番号が在るかを確かめる(#465。値が一致するかまでは見ない) | **`expected` と `error` は、ちょうど片方だけ書く。** **決まりに無いキーは書かない**——書いても生成物に現れないので、検査が落とす。 diff --git a/plugins/spec-loop/scripts/scaffold.py b/plugins/spec-loop/scripts/scaffold.py index e4392e25..e4f3c2f0 100644 --- a/plugins/spec-loop/scripts/scaffold.py +++ b/plugins/spec-loop/scripts/scaffold.py @@ -87,7 +87,7 @@ | 守らないもの | なぜ | | --- | --- | -| **中間表現が仕様と意味で一致するか** | 書き起こしは判断(`scaffold` の仕事)。出所(`source` の実在・Q&A が `反映済み`)までを見る。`暫定反映` の行が表に残れば、指していなくても止める | +| **中間表現が仕様と意味で一致するか** | 書き起こしは判断(`scaffold` の仕事)。出所(`source` の実在・Q&A が `反映済み`・ケースの `ref` の番号が原文に在ること。#465)までを見る。`暫定反映` の行が表に残れば、指していなくても止める | | **Red テストが仕様のケースを漏れなく持つか** | 中間表現に書いたケースだけが生成される。漏れは書き起こしの側で見る | | **Python 以外の言語の型・テスト** | OpenAPI から対象リポジトリの道具で作る | | **Issue を登録したか・登録した本文が同じか** | 登録はスクリプトの外(`gh issue create --body-file` に、生成した本文をそのまま渡す) | @@ -343,6 +343,33 @@ def check_source(where: str, sources, whole: Path) -> list[str]: return problems +def check_ref(where: str, ref, sources, whole: Path) -> list[str]: + """ケースの `ref`——仕様の中でそのケースの元になった行の番号(決定表の `R-F10`・テストケースの `F1-5b` など。#465)。 + 機能の `source` の原文のどれかに、前後に英数字・`_`・`-` が続かない形で現れなければ断る(別の番号の一部は数えない)。 + 英数字には全角の英数字も含める(`R-F10a` は別の番号)。後ろに `.`・`/`・`:` と数字が続く形(`R-F10.1`・`R-F10/2`)も別の番号とみなし、 + 数字で始まる `ref` は前に英数字と `.`・`/`・`:` が付く形(`R-F10.1` の `1`)を別の番号の一部とみなす—— + `R-F10.`(文末)・`R-F10.md`・`R-F10/R-F11` の `R-F11` は在ることにする。 + **値(入力・期待)が仕様と一致するかは見ない**——仕様の表の書式は spec-loop の決まりに無い。""" + if not isinstance(ref, str) or not one_line(ref) or ref != ref.strip(): + return [f"{where}: 仕様の中の番号(1 行・前後に空白なし)でない: {ref!r}"] + sub = r"(? list[str]: return PATH_VAR.findall(path) @@ -463,7 +490,7 @@ def check_ir(ir, whole: Path, answered: dict[str, dict[str, str]], path_rules: b continue for cindex, case in enumerate(cases or []): at = f"{where}.cases[{cindex}]" - found = keys_problem(at, case, {"id", "title", "input"}, {"expected", "error", "qa"}) + found = keys_problem(at, case, {"id", "title", "input"}, {"expected", "error", "qa", "ref"}) if found: problems += found if not isinstance(case, dict): @@ -488,6 +515,8 @@ def check_ir(ir, whole: Path, answered: dict[str, dict[str, str]], path_rules: b problems.append(f"{at}.error: operation.errors に無い: {case['error']!r}") if "qa" in case: problems += check_qa_refs(f"{at}.qa", case["qa"], answered) + if "ref" in case: + problems += check_ref(f"{at}.ref", case["ref"], feature.get("source"), whole) return problems diff --git a/plugins/spec-loop/skills/feasibility/SKILL.md b/plugins/spec-loop/skills/feasibility/SKILL.md index 72390cf0..520730f7 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.2**(プラグイン `spec-loop`)。 + +> **このスキルの版: 0.29.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 0cd9bc0a..1b695339 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.2**(プラグイン `spec-loop`)。 + +> **このスキルの版: 0.29.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 0fc0ca97..706082bb 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.2**(プラグイン `spec-loop`)。 + +> **このスキルの版: 0.29.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 5ac0694c..bda31660 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.2**(プラグイン `spec-loop`)。 + +> **このスキルの版: 0.29.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))。 @@ -84,6 +84,8 @@ python3 <このスキルの base ディレクトリ>/../../scripts/check-spec-ex - **1 機能 = Issue 1 件**になるので、機能の切り方は仕様が書き分けている単位(画面の操作・API・バッチ)に合わせる。 - 各機能の `source` に、その機能を書いた仕様のファイルを**重ね合わせた全体の根からのパス**(`spec/order.md`)で、 `qa` に決定の根拠にした Q&A の No を書く。 +- **仕様が決定表の行やテストケースに番号を振っていれば、各ケースの `ref` にその番号を書く**(`R-F10` など)—— + 生成と `--check` が、その番号が `source` の原文に在るかを確かめる(書き起こしの誤り・仕様の後からの修正とのずれを拾う。#465)。 - **既にあるなら、仕様と食い違うところだけを直す。** 機能の `id` とケースの `id` を振り直さない—— 登録済みの Issue と Red テストの名前がずれる。 diff --git a/plugins/spec-loop/skills/spec-review/SKILL.md b/plugins/spec-loop/skills/spec-review/SKILL.md index a2b066b3..490dccd1 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.2**(プラグイン `spec-loop`)。 + +> **このスキルの版: 0.29.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-scaffold.py b/scripts/test-scaffold.py index 3c17ad01..2b67f062 100644 --- a/scripts/test-scaffold.py +++ b/scripts/test-scaffold.py @@ -1133,6 +1133,41 @@ def remedy(repo: Path) -> None: Case("Q&A が表に無い", 1, "Q-099 は Q&A 表に無い", ir=edit(lambda ir: f1(ir).update(qa=["Q-099"]))), Case("ケースの Q&A が表に無い", 1, "Q-098 は Q&A 表に無い", ir=edit(lambda ir: f1(ir)["cases"][0].update(qa=["Q-098"]))), Case("Q&A の重複", 1, "Q&A の No が重複している", ir=edit(lambda ir: f1(ir).update(qa=["Q-001", "Q-001"]))), + # ケースの ref——仕様の原文にその番号が在るか(#465)。在れば通る(陽性対照)・無い/別の番号の一部/空なら断る + Case("ケースの ref が仕様に在る", 0, ir=edit(lambda ir: f1(ir)["cases"][0].update(ref="R-F10")), + spec={"order.md": SPEC + "| R-F10 | silver | 9999 | 0 |\n"}), + Case("ケースの ref が仕様に無い", 1, "R-F10 が仕様の原文(source)のどこにも無い", + ir=edit(lambda ir: f1(ir)["cases"][0].update(ref="R-F10"))), + Case("ケースの ref が別の番号の一部にしか無い", 1, "R-F10 が仕様の原文(source)のどこにも無い", + ir=edit(lambda ir: f1(ir)["cases"][0].update(ref="R-F10")), + spec={"order.md": SPEC + "| R-F100 | x |\n| R-F10a | y |\n| R-F10.1 | z |\n| R-F10/2 | w |\n| R-F10a | v |\n"}), + # 前に付く形も別の番号の一部(#465 の 2 パス目) + Case("ケースの ref が別の番号の後ろの一部にしか無い", 1, "R-F10 が仕様の原文(source)のどこにも無い", + ir=edit(lambda ir: f1(ir)["cases"][0].update(ref="R-F10")), spec={"order.md": SPEC + "| XR-F10 | x |\n| XR-F10 | y |\n"}), + Case("数字の ref が小番号にしか無い", 1, "1 が仕様の原文(source)のどこにも無い", + ir=edit(lambda ir: f1(ir)["cases"][0].update(ref="1")), spec={"order.md": SPEC + "| R-F10.1 | x |\n"}), + # 区切りの後ろが数字でなければ在ることにする(2 パス目の P2) + Case("拡張子の付いた ref は在ることにする", 0, ir=edit(lambda ir: f1(ir)["cases"][0].update(ref="R-F10")), + spec={"order.md": SPEC + "R-F10.md を見る。\n"}), + Case("区切りの後ろの ref は在ることにする", 0, ir=edit(lambda ir: f1(ir)["cases"][0].update(ref="R-F11")), + spec={"order.md": SPEC + "R-F10/R-F11 を参照。\n"}), + Case("--dry-run もケースの ref を見る", 1, "R-F10 が仕様の原文(source)のどこにも無い", args=("--dry-run",), + ir=edit(lambda ir: f1(ir)["cases"][0].update(ref="R-F10"))), + Case("文末の ref は在ることにする", 0, ir=edit(lambda ir: f1(ir)["cases"][0].update(ref="R-F10")), + spec={"order.md": SPEC + "割引は R-F10.\n"}), + Case("--check もケースの ref を見る", 1, "R-F10 が仕様の原文(source)のどこにも無い", args=("--check",), + ir=edit(lambda ir: f1(ir)["cases"][0].update(ref="R-F10"))), + Case("ケースの ref が空", 1, "仕様の中の番号(1 行・前後に空白なし)でない", ir=edit(lambda ir: f1(ir)["cases"][0].update(ref=""))), + # 原文に「R-F10 |」と在っても、前後に空白のある ref・複数行の ref は断る(#465 の 1 パス目) + Case("ケースの ref の後ろに空白", 1, "仕様の中の番号(1 行・前後に空白なし)でない", + ir=edit(lambda ir: f1(ir)["cases"][0].update(ref="R-F10 ")), spec={"order.md": SPEC + "| R-F10 | x |\n"}), + Case("ケースの ref が複数行", 1, "仕様の中の番号(1 行・前後に空白なし)でない", + ir=edit(lambda ir: f1(ir)["cases"][0].update(ref="R-F10\nR-F11")), spec={"order.md": SPEC + "| R-F10 | x |\n| R-F11 | y |\n"}), + # source に spec/ の外のファイルを書いても(check_source が断る)、ref はそこで探さない(#465 の 1 パス目) + Case("ケースの ref を仕様の外のファイルで探さない", 1, "R-F10 が仕様の原文(source)のどこにも無い", + ir=edit(lambda ir: (f1(ir).update(source=["spec/order.md", "requirements/r.md"]), + f1(ir)["cases"][0].update(ref="R-F10"))), + extra={"docs/v1/requirements/r.md": "R-F10\n"}), Case("仕様のファイルが無い", 1, "spec/missing.md が無い", ir=edit(lambda ir: f1(ir).update(source=["spec/missing.md"]))), Case("仕様の外のファイル", 1, "の中に無い", ir=edit(lambda ir: f1(ir).update(source=["shared/qa.csv"]))), # 版のディレクトリのパスは指さない(重ね合わせた全体の根からのパス) @@ -1465,6 +1500,29 @@ def remedy(repo: Path) -> None: # (名前, 壊す前, 壊した後, 殺すはずの試料の名前) MUTATIONS: list[tuple[str, str, str, str]] = [ + ("ケースの ref を見ない", ' if "ref" in case:\n problems += check_ref', + ' if False:\n problems += check_ref', "ケースの ref が仕様に無い"), + ("ref の部分一致を許す", 'pattern = re.compile(rf"(?