diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 858c6fdb..f2aaf372 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "ndf", "source": "./plugins/ndf-claude", - "description": "Claude Code plugin (v7.0.0): 8 specialized agents and 26 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/Gemini), transcript retention guard, and optional Slack notifications." + "description": "Claude Code plugin (v8.0.0): 8 specialized agents and 26 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/Gemini), transcript retention guard, and optional Slack notifications." }, { "name": "playwright-kit", diff --git a/AGENTS.md b/AGENTS.md index e2362ea8..4b4eda73 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,7 +77,7 @@ ai-plugins/ ## NDFプラグインについて -**NDFプラグイン**は、このマーケットプレイスの主要プラグインです(v7.0.0)。plugin 名は全ランタイムで `ndf` を維持し、配布物は `plugins/ndf-claude` / `plugins/ndf-codex` / `plugins/ndf-kiro` に分離しています。 +**NDFプラグイン**は、このマーケットプレイスの主要プラグインです(v8.0.0)。plugin 名は全ランタイムで `ndf` を維持し、配布物は `plugins/ndf-claude` / `plugins/ndf-codex` / `plugins/ndf-kiro` に分離しています。 - 共通編集元は `plugins/ndf-shared/` - Claude Code版は 8個の専門サブエージェント、公開Skills、SessionStart/Stopフックを提供 - Codex版は Codex向け公開Skillsと任意Slack通知hookを提供 diff --git a/CLAUDE.md b/CLAUDE.md index 577ee6ed..6b02857e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,13 +29,15 @@ skills/ → 実行可能なワークフロー 詳細は `docs/specifications/ndf-knowledge-and-kiro.md` を参照。 -## NDF v7.0.0 の Skill 構成 +## NDF v8.0.0 の Skill 構成 Skill は 30 個で、配布は `plugins/ndf-shared/manifests/` が唯一の基準(Claude Code 26 / Codex 24 / Kiro 25)。ブラウザ自動テストの 4 個は `playwright-kit` プラグインへ分離した(`plugins/playwright-kit-shared/`)。frontmatter の書き方は `plugins/ndf-shared/skills/README.md` の規約に従い、`python3 scripts/check-skill-frontmatter.py` で検査する。利用実績と維持・統合・削除の判定は `docs/specifications/ndf-skill-inventory.md` に記録する。 -v6.1.0 で開発方法論レイヤーの 5 個(`development-workflow` / `requirements-design` / `tdd-cycle` / `safe-refactoring` / `quality-gates`)を追加した。モード判定の基準を持つのは `development-workflow` だけで、他の Skill とエージェント定義は判定結果を受け取る側に徹する。 +v6.1.0 で開発方法論レイヤーの 5 個(`development-workflow` / `requirements-design` / `tdd-cycle` / `refactoring`(当時は `safe-refactoring`)/ `quality-gates`)を追加した。モード判定の基準を持つのは `development-workflow` だけで、他の Skill とエージェント定義は判定結果を受け取る側に徹する。 -v7.0.0 で playwright 系 4 個を `playwright-kit` プラグインへ分離した。対応表は `ndf-policies` skill にある(v8.0.0 で削除)。 +v7.0.0 で playwright 系 4 個を `playwright-kit` プラグインへ分離した。対応表は予告どおり v8.0.0 で削除済み。 + +v8.0.0 で `safe-refactoring` を `refactoring` へ改名し、分岐・反復・定数の表現を決める観点を統合した。観点は `references/data-representation.md` に置き、言語固有の手段は `references/lang-<言語>.md` に 1 言語 1 ファイルで置く。SKILL.md が対象言語のファイルだけを読ませるため、他言語の内容はコンテキストに載らない。言語を追加するときも他のファイルは変更しない。対応表は `ndf-policies` にある(v9.0.0 で削除)。 v6.0.0 の対応表(`review` → `pr-review`)は予告どおり削除済み。v6.0.0 以前から移行する場合は v6.1.0 の `ndf-policies` を参照する。 diff --git a/README.md b/README.md index 901797b6..7e2827ba 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,12 @@ Claude Code / Codex / Kiro CLI向けのスキル・MCP設定を共有するた このマーケットプレイスは、チーム全体でAI開発ツール(Claude Code / Codex / Kiro CLI)の導入を加速するための事前設定されたプラグインを提供します。 -**NDFプラグイン v7.0.0** は、同じ `ndf@ai-plugins` という名前で Claude Code / Codex / Kiro CLI へ配布されるランタイム別プラグインです。共通ソースは `plugins/ndf-shared/` に集約し、利用者が install する配布物は `plugins/ndf-claude/` / `plugins/ndf-codex/` / `plugins/ndf-kiro/` に分かれています。 +**NDFプラグイン v8.0.0** は、同じ `ndf@ai-plugins` という名前で Claude Code / Codex / Kiro CLI へ配布されるランタイム別プラグインです。共通ソースは `plugins/ndf-shared/` に集約し、利用者が install する配布物は `plugins/ndf-claude/` / `plugins/ndf-codex/` / `plugins/ndf-kiro/` に分かれています。 - **公開Skills**: Claude Code向け core 26個、Kiro向け core 25個、Codex向け core 24個に分離。 - **元Skills(30個)**: - PR/レビューワークフロー (7): pr, pr-tests, fix, pr-review, cherry-pick-pr, deploy, merged - - 開発方法論 (5): development-workflow, requirements-design, tdd-cycle, safe-refactoring, quality-gates + - 開発方法論 (5): development-workflow, requirements-design, tdd-cycle, refactoring, quality-gates - 原則・ガイドライン (9): ndf-policies, implementation-plan, plan-to-spec, investigation-rules, problem-solving, logging-guidelines, markdown-writing, issue-plan-strategy, ml-model-structure - データ分析・品質・環境 (4): qa-security-scan, docker-container-access, google-auth, official-skills-autoloader - 外部サービス連携 (1): google-drive @@ -102,9 +102,39 @@ kiro-cli chat --agent ndf | プラグイン名 | バージョン | 説明 | 詳細 | |------------|----------|------|------| -| **ndf** | 7.0.0 | Claude Code / Codex / Kiro CLI 向けに runtime 別配布物を提供する NDF プラグイン。8個の専門エージェント(Claude版)、公開Skills(Claude Code向け core 26個、Kiro向け core 25個、Codex向け core 24個)、Claude SessionStart/Stopフック、Codex/Kiro向け通知・実行補助を提供。v4.0.0 で Codex MCP サーバを廃止し、`/ndf:external-ai` skill + `corder` エージェント経由の CLI 直接実行に一本化。 | [Claude](./plugins/ndf-claude/README.md) / [Codex](./plugins/ndf-codex/README.md) / [Kiro](./plugins/ndf-kiro/README.md) | +| **ndf** | 8.0.0 | Claude Code / Codex / Kiro CLI 向けに runtime 別配布物を提供する NDF プラグイン。8個の専門エージェント(Claude版)、公開Skills(Claude Code向け core 26個、Kiro向け core 25個、Codex向け core 24個)、Claude SessionStart/Stopフック、Codex/Kiro向け通知・実行補助を提供。v4.0.0 で Codex MCP サーバを廃止し、`/ndf:external-ai` skill + `corder` エージェント経由の CLI 直接実行に一本化。 | [Claude](./plugins/ndf-claude/README.md) / [Codex](./plugins/ndf-codex/README.md) / [Kiro](./plugins/ndf-kiro/README.md) | | **playwright-kit** | 1.0.0 | Playwright による E2E テストの計画・実装・証跡管理を提供するプラグイン。ページ役割からのテスト計画、動画 / trace 付きスクリプト実装、レポート生成と Drive 保管、playwright_kit ランタイム(init、a11y / CWV スキャン)の 4 Skill。NDF v7.0.0 で分離。 | [Claude](./plugins/playwright-kit-claude/README.md) | +### NDF v8.0.0 の主な変更(非互換) + +**構造改善の Skill を `/ndf:refactoring` へ改名しました。** 引数と手順は変わりません。 + +| 旧コマンド | 移行先 | +|---|---| +| `/ndf:safe-refactoring` | `/ndf:refactoring` | + +`safe-` を外したのは、`/refactoring` で一意に決まり、入力が短くなるためです。対応表は +`ndf-policies` にあり、v9.0.0 で削除します。 + +**あわせて、分岐・反復・定数の表現を決める観点を統合しました。** リファクタリングの起点となる +兆候(コードスメル)に 3 件を追加し、判断材料を参照資料として持たせています。 + +| 追加した観点 | 置き換え先 | +|---|---| +| 業務ルールの埋め込み(料率・区分・しきい値が制御構文に埋まっている) | 対応表への置き換え | +| 一件ずつの反復(往復回数や実行時間が件数に比例する) | 一括処理への置き換え | +| 検証のない外部化(設定・マスタにスキーマ・版・検証がない) | スキーマと版を与え、読み込み境界で検証する | + +判断材料は +[references/data-representation.md](./plugins/ndf-shared/skills/refactoring/references/data-representation.md) +にあります。「分岐が多いから表にする」ではなく**変化するから表にする**、という切り分けを置き、 +ガード節・静的に網羅性を検査できる分岐・逐次依存のループ・閉じた状態集合の列挙型は「そのままで +よい」ものとして明示しています。具体的な手段は言語ごとに 1 ファイルへ分けており(`references/lang-python.md` / +`lang-javascript.md` / `lang-typescript.md` / +[`lang-php.md`](./plugins/ndf-shared/skills/refactoring/references/lang-php.md))、 +SKILL.md が対象言語のファイルだけを読ませます。他言語の内容はコンテキストに載りません。 +記載のない言語でも判断材料はそのまま使えます。 + ### NDF v7.0.0 の主な変更(非互換) **ブラウザ自動テストの 4 Skill を `playwright-kit` プラグインへ分離しました。** Skill 名は @@ -159,7 +189,7 @@ Skill の `name` と `description` は起動時の一覧としてコンテキス | `/ndf:development-workflow` | 変更を 4 モード(`light` / `standard` / `architecture` / `legacy-refactor`)へ分類し、必要な工程だけへ振り分ける | | `/ndf:requirements-design` | 曖昧な要求を、観測可能で検証できる受け入れ条件へ変換する | | `/ndf:tdd-cycle` | 「失敗するテスト → 通す最小実装 → 整理」のサイクルを定義する | -| `/ndf:safe-refactoring` | コードスメル起点の構造改善と、テストが乏しい既存コード向けの現状固定テスト | +| `/ndf:refactoring` | コードスメル起点の構造改善と、テストが乏しい既存コード向けの現状固定テスト(v6.1.0 当時の名称は `/ndf:safe-refactoring`) | | `/ndf:quality-gates` | 完了宣言の前に、実行コマンド・終了コード・実行時刻・対象範囲を証跡として要求する | 全変更にフル工程を課さないことを設計の中心に置いています。文言修正や設定変更は diff --git a/docs/specifications/ndf-skill-inventory.md b/docs/specifications/ndf-skill-inventory.md index d5bfc9bb..c0d21527 100644 --- a/docs/specifications/ndf-skill-inventory.md +++ b/docs/specifications/ndf-skill-inventory.md @@ -425,6 +425,49 @@ superpowers は 28 個で NDF と同規模ながら frontmatter が 1/2.7 だっ `description` が `Use when …` の 1 文だけで、トリガ語の列挙・引用符・追加フィールドを持たない。 v7.0.0 の書式はこの方針に寄せたものである。 +## v8.0.0 での統合と改名(refactoring) + +`safe-refactoring` を **`refactoring`** へ改名し、分岐・反復・定数の表現を決める観点を統合した。 +この観点は独立 Skill `analyzable-coding` として一度追加したもので(v7.1.0 / Skill 31 個)、 +`refactoring` へ吸収したため **Skill 数は 31 個から 30 個へ戻る**。v7.1.0 は同じリリース作業の +途中で作った中間の版で、配布はしていない。したがって配布済みの v7.0.0 から見ると 30 個のままである。 + +統合の根拠は本書「判断基準」節の「機能が他 Skill と重複するものは、統合の対象とし、内容は +統合先へ残す」である。観点を独立 Skill として置くと、発動条件が「コードを書くとき」以外に +書けず、常に該当するトリガは発動判定として働かない。構造改善の起点である +`safe-refactoring` の観点へ寄せることで、発動点が「リファクタリングを始めるとき」に定まる。 + +| Skill | 配布 | 変更 | 判定 | +| --- | --- | --- | --- | +| `refactoring` | CXK | `safe-refactoring` から改名。コードスメルに 3 件追加、参照資料を 2 件追加 | 改名前の実測を引き継ぐ | + +追加した観点と参照資料: + +| 追加物 | 内容 | +| --- | --- | +| コードスメル 3 件 | 業務ルールの埋め込み / 一件ずつの反復 / 検証のない外部化 | +| 手法 2 件 | 対応表への置き換え / 一括処理への置き換え | +| `references/data-representation.md` | 分岐・反復・定数の判定表、外部化してよい条件、判断の記録 | +| `references/lang-python.md` ほか 3 件 | 言語ごとの手段。1 言語 1 ファイルとし、SKILL.md が対象言語のものだけを読ませる | + +改名は公開コマンドの非互換変更にあたるため、対応表を `ndf-policies` へ置いた(v9.0.0 で削除)。 +あわせて v7.0.0 の対応表(playwright 系の分離)を予告どおり削除した。 + +予算への影響(`python3 scripts/check-skill-frontmatter.py --report`): + +Skill 数以外は全 family 合計(ndf + playwright-kit)である。v7.1.0 は配布していない中間の版だが、 +31 個へ増えた地点の実測として残す。 + +| 指標 | v7.0.0 | v7.1.0(未配布) | v8.0.0 | 運用値 | +| --- | ---: | ---: | ---: | ---: | +| Skill 数(ndf 単独) | 30 | 31 | 30 | — | +| Claude Code 初期一覧 | 5,807 | 6,041 | 5,855 | 8,000 | +| Codex 初期一覧 | 5,395 | 5,629 | 5,443 | 8,000 | +| frontmatter 合計 | 10,559 | 10,782 | 10,612 | 11,200 | + +Skill 数は v7.0.0 と同じ 30 個に戻るが、初期一覧は 48 文字(frontmatter 合計は 53 文字)多い。 +`refactoring` の `description` を統合後の役割に合わせて書き足したためで、運用値には余裕がある。 + ## 参照 - 棚卸の計画: [issues/ndf-development-skills/02-skill-inventory.md](../../issues/ndf-development-skills/02-skill-inventory.md) diff --git a/issues/issue-38-chatgpt-response.md b/issues/issue-38-chatgpt-response.md new file mode 100644 index 00000000..2cf77213 --- /dev/null +++ b/issues/issue-38-chatgpt-response.md @@ -0,0 +1,413 @@ +> **注記(本リポジトリによる追記。以下は ChatGPT の回答の生ログであり、本文は改変していない)** +> +> - 生ログとしての記録価値を保つため、誤りが含まれていても本文は書き換えていない。 +> - 脚注 `[1]` のリンクテキスト `"Cat's Cradle"` は誤り。正しくは Eric S. Raymond, +> *The Art of Unix Programming*(URL `https://www.catb.org/esr/writings/taoup/html/ch01s06.html` +> 自体は正しい)。`"Cat's Cradle"` はカート・ヴォネガットの小説であり、Rule of +> Representation の出典ではない。 +> - 検証済みの事実関係は [`issue-38-coding-skill-plan.md`](issue-38-coding-skill-plan.md) の +> 「1.4 事実確認の記録」にまとめてある。本ファイルではなくそちらを参照すること。 + +## 調査結果 + +あります。しかも、草稿の中心にある **「判断や知識をコードではなくデータへ移し、処理系を単純にする」** という発想には、かなり明確な理論的系譜があります。 + +ただし、草稿全体と一対一で一致する単一の開発理論は見当たりませんでした。実態としては、次の理論や設計手法を統合したものです。 + +| 既存の理論・手法 | 草稿との近さ | 対応する主張 | +| -------------------------------------------- | -----: | --------------------------------- | +| **Rule of Representation** | 非常に近い | 知識・判断をコードではなくデータへ移す | +| **Data-Oriented Programming** | 非常に近い | コードとデータを分離し、データ変換として処理を構成する | +| **Table-Driven Design / Decision Tables** | 非常に近い | `if` や `switch` を表・ルール・マッピングで表現する | +| **Policy as Code / Rules Engine / DMN** | 近い | ビジネス判断を独立したポリシーとして管理する | +| **Declarative Programming** | 近い | 「どう処理するか」より「何を実現するか」を記述する | +| **Functional Core, Imperative Shell** | 近い | 中核を純粋なデータ変換にし、副作用を境界へ追い出す | +| **Array Programming / Dataflow / MapReduce** | 近い | 明示的なループを減らし、一括・並列処理可能にする | +| **Observability-Driven Development** | 近い | 判断、結果、失敗、環境を構造化データとして計測する | +| **Event Sourcing** | 部分的に近い | アプリケーションの状態変化や挙動をイベントとして保存する | + +--- + +## 1. もっとも直接的なのは「Rule of Representation」 + +Unix 哲学には、次の原則があります。 + +> “Fold knowledge into data, so program logic can be stupid and robust.” + +つまり、**知識をデータへ畳み込み、プログラムロジックを単純で堅牢にする**という考え方です。草稿の「コードは基本原則のみ」「判断やロジックは極力データとして表現する」は、この原則とほぼ同じ方向を向いています。([Cat's Cradle][1]) + +> 注記: 上のリンクテキスト `Cat's Cradle` は誤り。出典は Eric S. Raymond, *The Art of Unix Programming*(リンク先 URL は正しい)。冒頭の注記を参照。 + +たとえば、 + +```typescript +if (customer.rank === "gold") { + discount = 0.2; +} else if (customer.rank === "silver") { + discount = 0.1; +} else { + discount = 0; +} +``` + +を、 + +```typescript +const discountRates = { + gold: 0.2, + silver: 0.1, + bronze: 0, +} as const; + +const discount = discountRates[customer.rank]; +``` + +とするのは、まさに **プログラムに埋め込まれた知識をデータ構造へ移す**設計です。 + +この原則は、草稿全体の「祖先」と呼んでよいくらい近いです。 + +--- + +## 2. 理論名として最も近いのは Data-Oriented Programming + +Yehonathan Sharvit が整理している **Data-Oriented Programming(DOP)** は、次の原則を掲げています。 + +* コードとデータを分離する +* データを第一級の値として扱う +* 汎用的なデータ構造を利用する +* データを不変として扱う +* データ表現とデータスキーマを分離する + +JavaScript、Pythonなど特定の言語に依存しない考え方として説明されており、今回の language-agnostic な skill と相性がよいです。([Yehonathan Sharvit][2]) + +ただし、DOPでいう「データ」は、単にすべてを辞書やJSONにすることではありません。重要なのは、 + +```text +データ + ↓ +汎用的で予測可能な変換処理 + ↓ +新しいデータ +``` + +という形にシステムを近づけることです。 + +草稿はDOPよりさらに、 + +* 分岐のデータ化 +* ループの排除 +* 並列化可能性 +* 実行時の観測可能性 +* リファクタリング規範 + +まで含んでいます。そのため、**DOPを基礎に、ほかの設計理論を合成したもの**と考えるのが適切です。 + +なお、似た名前の **Data-Oriented Design** は、ゲーム開発や高性能計算の文脈で、メモリ配置、キャッシュ効率、データアクセスパターンを重視する意味でも使われます。今回の思想には重なる部分がありますが、名称としては Data-Oriented Programming の方が近いです。([Data-Oriented Design][3]) + +--- + +## 3. 「分岐をデータとして表現する」は Table-Driven Design + +`if` や `switch` の連鎖を、表、マッピング、決定表として表現する方法は、一般に次の名前で扱われます。 + +* Table-Driven Programming +* Table-Driven Design +* Decision Table +* Dispatch Table +* Rules Engine + +LLVMの **TableGen** も、命令セットなどの大量のドメイン知識を宣言的なレコードとして記述し、そこからコードや各種出力を生成する仕組みです。LLVM自身も、手書きコードでは保守困難になる情報をレコードとして保持することを目的の一つに挙げています。([LLVM][4]) + +ビジネスルールについては、OMGの **Decision Model and Notation(DMN)** がかなり近いです。DMNでは、業務上の判断を決定表として表現し、非エンジニアにも読める形にしながら、検証・実行できることを目指しています。([OMG][5]) + +また、Open Policy Agentは、認可や運用ルールなどの「ポリシー」をアプリケーション本体から分離し、独立して読取り、分析、版管理、配布できるようにします。([Open Policy Agent][6]) + +したがって、草稿の分岐に関する考え方は、次のように整理できます。 + +| 分岐の種類 | 適切な表現 | +| --------------- | ------------------------------- | +| 値から値への単純な対応 | `dict`、`Map`、lookup table | +| 処理方式の切り替え | dispatch table、handler registry | +| 条件の組み合わせが多い業務判断 | decision table、DMN、rules engine | +| 認可、制約、組織ポリシー | policy engine | +| 時系列で状態が変化する処理 | state machine、statechart | +| 閉じた少数の型分岐 | 型付き `switch`、pattern matching | + +特に注文状態、審査状態、ワークフローのような時間的な振る舞いは、単なる関数マップよりも state machine や statechart の方が適しています。Statechartsは、階層、並行状態、イベント通信を含む複雑な状態遷移を表現するために設計されています。([Weizmann Institute of Science][7]) + +--- + +## 4. 「ループを減らす」は Array Programming と Dataflow Programming + +ループを直接書く代わりに、配列や行列全体への演算として問題を表現する思想は、APLなどに代表される **Array Programming** にあります。 + +APLを提唱した Kenneth Iverson の考え方では、ベクトル、行列、高次元配列に対する演算を使い、個々の要素を逐次操作する詳細を表面から消していきます。 + +また、MapReduceでは、利用者は `map` と `reduce` に相当する処理を記述し、分割、スケジューリング、通信、障害処理、並列実行をランタイムへ委ねます。これは草稿の「並列化しない場合も、並列化へ切り替え可能なロジック」という方向に近いです。 + +ただし、草稿の次の記述は修正した方がよいです。 + +> ベクトル、行列の問題として対応する(`.map()`、`.apply()`など) + +`.map()` や `.apply()` は、必ずしもベクトル演算でも並列処理でもありません。 + +JavaScriptの `Array.prototype.map()` は同期的にコールバックを順番に適用するAPIであり、それ自体はCPU並列処理ではありません。([MDN Web Docs][8]) + +また、NumPyの `np.vectorize()` も、公式ドキュメント上「主として利便性のため」であり、実装は本質的にループで、性能目的のベクトル化ではないと説明されています。([NumPy][9]) + +skillでは、少なくとも次を区別する必要があります。 + +| 種類 | 例 | 性質 | +| ----------- | -------------------------- | -------------------- | +| 高階反復 | JS `map`、Python `map` | ループの記述方法を変えただけ | +| ネイティブベクトル演算 | NumPy ufunc、broadcasting | C、SIMD、GPU等で一括実行可能 | +| バッチ処理 | SQL、bulk API | 呼出し回数やI/Oをまとめる | +| 並行処理 | `Promise.all`、async task | 待ち時間を重ねる。CPU並列とは限らない | +| 並列処理 | multiprocessing、worker、JAX | 複数CPU/GPUなどで同時実行 | +| 分散データフロー | Beam、Spark、MapReduce | 分割、再試行、集約を実行基盤が管理 | + +したがって、目指すべきなのは「for文を見つけたらmapへ置き換える」ことではありません。 + +より正確には、 + +> 同種の独立したデータ変換については、逐次制御として書く前に、バッチ演算、ベクトル演算、データフロー、並行・並列実行として表現できないか検討する。 + +となります。 + +--- + +## 5. 「振る舞いをデータとして表現する」は Observability と Event Sourcing + +OpenTelemetryでは、システムから発生する telemetry を、システムの振る舞いを示すデータとして扱い、主に traces、metrics、logs へ整理します。Semantic Conventionsを使うことで、異なるコードやサービスの間でも、属性名や意味をそろえて相関・集約できるようにします。([OpenTelemetry][10]) + +草稿の、 + +> アプリ自体の挙動、周辺環境、ユーザーなどが常に計測可能であること + +という主張は、単なる「ログをたくさん出す」ではなく、次のような **構造化された判断記録** として設計すると強くなります。 + +```json +{ + "event_name": "discount_decided", + "customer_id": "cus_123", + "input": { + "rank": "gold", + "purchase_amount": 120000 + }, + "decision": { + "discount_rate": 0.2, + "rule_id": "customer-rank-discount", + "rule_version": "2026-08-01", + "reason": "customer rank is gold" + }, + "execution": { + "code_version": "a13fd82", + "duration_ms": 4, + "trace_id": "..." + } +} +``` + +OpenTelemetryのイベント設計でも、属性はフィルタ、グループ化、集計、相関に使えるよう設計することが推奨されています。([OpenTelemetry][11]) + +さらに状態変更そのものをイベント列として保存する **Event Sourcing** は、アプリケーションの状態変化をデータ化する考え方です。過去のイベントから状態を再構築したり、過去時点を再現したりできます。([martinfowler.com][12]) + +ただし、Event Sourcingは通常の監査ログよりはるかに大きなアーキテクチャ上の選択です。すべてのアプリへ適用するのではなく、意思決定の再現や履歴が重要な領域に限定する方がよいでしょう。 + +--- + +## 草稿の独自性 + +調査した範囲では、以下を一つにまとめた広く定着した理論名は見当たりませんでした。 + +1. ルールや判断をデータへ移す +2. 処理をデータ変換として構成する +3. 逐次ループよりバッチ・ベクトル・データフローを優先する +4. 並列化可能性を保つ +5. 判断理由、結果、失敗を構造化データとして観測する +6. これを実装だけでなくリファクタリング規範として利用する + +したがって、思想自体の各部品は既存ですが、**それらを言語横断のコード生成・レビュー・リファクタリングskillとして統合する部分には、十分な独自性があります**。 + +名称を付けるなら、次のあたりが内容を比較的正確に表します。 + +* **Observable Data-Oriented Development** +* **Data-First Observable Programming** +* **Policy and Data-Oriented Programming** +* **Observable Declarative Development** + +個人的には **Observable Data-Oriented Development(観測可能なデータ指向開発)** が最もしっくりきます。 + +--- + +## ただし「if・for・enum禁止」にしてはいけない + +ここはかなり重要です。 + +草稿をそのままskillへ入れると、AIエージェントが表面的に、 + +* `if` を辞書アクセスへ置き換える +* `for` を `.map()` へ置き換える +* enumを文字列へ置き換える +* 設定ファイルへ大量のロジックを押し込む + +というリファクタリングをし始める可能性があります。 + +これは、むしろコードを分析しにくくする場合があります。 + +### `if` や `switch` は、それ自体が悪いわけではない + +次のようなガード節は、動的なhandler mapより明確です。 + +```typescript +function withdraw(account: Account, amount: number): Result { + if (amount <= 0) { + return { ok: false, reason: "amount_must_be_positive" }; + } + + if (account.balance < amount) { + return { ok: false, reason: "insufficient_balance" }; + } + + return executeWithdrawal(account, amount); +} +``` + +また、TypeScriptのdiscriminated unionと `never` を使った `switch` は、すべてのケースが処理されているかをコンパイル時に検証できます。これを動的な文字列辞書に置き換えると、静的解析能力が下がることがあります。([TypeScript][13]) + +したがって、 + +> `if`、`switch`を避ける + +ではなく、 + +> 変化頻度の高い業務判断や多数の条件組み合わせを、ネストした制御構文へ埋め込まない。小さく閉じた型分岐、ガード節、不変条件の検査には明示的な分岐を使用してよい。 + +とするのが適切です。 + +### データ化しすぎると、設定ファイルがプログラミング言語になる + +条件、優先順位、参照、式、継承、デフォルト、再試行などを設定データへ追加していくと、やがて独自DSLになります。 + +LLVMのTableGenのドキュメントでも、DSLやカスタムバックエンドが複雑化し、初見の開発者にとって理解困難になる問題が指摘されています。([LLVM][4]) + +判断をデータ化する場合は、少なくとも次が必要です。 + +* スキーマ +* 型またはバリデーション +* バージョン +* 変更履歴 +* 競合・到達不能ルールの検出 +* テスト +* マイグレーション +* 誰が何を変更したかという監査 +* 実行時に適用されたルールIDと理由の記録 + +データへ移動しただけで、複雑性が消えるわけではありません。**複雑性を検査・可視化・変更可能な形式へ移す**ことが目的です。 + +--- + +## 定数・enumについては分類が必要 + +「コード中の定数、enumを極力使用しない」は、やや広すぎます。 + +Pythonのenumは、値へ意味のあるシンボル名を割り当てる仕組みです。適切に使えば、コード値を直接書くより意味を明確にできます。([Python documentation][14]) + +定数は、次のように分類するのがよいです。 + +| 値の性質 | 置き場所 | +| ------------- | ------------------------- | +| 数学的・技術的な不変条件 | コード内の定数 | +| 閉じた状態集合・プロトコル | enum、literal union、schema | +| 頻繁に変わる業務ルール | decision table、policy、設定 | +| 環境ごとに変わる値 | deployment config、環境変数 | +| ユーザーが管理するカテゴリ | DBのマスタ・参照テーブル | +| 表示名・文言 | i18n、コンテンツデータ | +| セキュリティ上の絶対制約 | コード、型、schema、policyの複数層 | + +特に、 + +```typescript +status === 3 +``` + +のような不透明なコード値は避けるべきですが、 + +```typescript +type OrderStatus = + | "pending" + | "confirmed" + | "shipped" + | "cancelled"; +``` + +のような型付きの意味的識別子まで排除する必要はありません。 + +「文字列にすればデータになる」というわけでもなく、型のない文字列は typo や未処理ケースを実行時まで発見できない危険があります。 + +--- + +## 基本方針は、こう直すと理論的に強くなります + +草稿の中核を、次のように再定義することをおすすめします。 + +> **変化する判断や業務知識は、検証可能・版管理可能なデータまたはポリシーとして表現する。安定した機構、不変条件、型、境界はコードとスキーマとして表現する。処理は明示的な入力から出力へのデータ変換として構成し、依存関係、順序、副作用を明らかにする。各判断、結果、失敗は、理由、適用ルール、バージョン、実行コンテキストを伴う構造化データとして観測・再現可能にする。** + +「コードよりもデータ」だけだと、何でも設定ファイルへ追い出す誤解が起きます。 + +より正確には、 + +> **変化する知識はデータへ。安定した機構と不変条件はコードへ。両方をスキーマと計測で接続する。** + +という原則です。 + +これはUnixのRule of Representation、Data-Oriented Programming、宣言的プログラミング、Policy as Code、Observabilityの長所を、かなりきれいに統合しています。 + +--- + +## skillへ落とす際のルール案 + +禁止規則ではなく、`MUST / SHOULD / MAY` に分けると運用しやすくなります。 + +### MUST + +* 外部化したルール、設定、マスタデータにはスキーマとバージョンを持たせる +* 判断結果には `rule_id`、`rule_version`、`reason` を記録できるようにする +* バッチ処理では、成功件数だけでなく全失敗の件数・種類・対象を報告する +* 外部I/O、副作用、時刻、乱数は境界として明示する +* 不変条件やセキュリティ制約を、変更可能な設定だけに依存させない +* 閉じた状態集合は、型またはスキーマによって網羅性を検査する + +### SHOULD + +* 条件の組み合わせが多い業務判断はdecision tableまたはpolicyとして表現する +* 純粋なデータ変換と、副作用を伴う処理を分離する +* 同種の大量処理では、逐次ループよりバッチ、ベクトル、データフローを検討する +* 並行化する処理は、冪等性、順序依存、再試行、タイムアウト、キャンセルを明示する +* ネストした条件分岐は、ガード節、状態機械、決定表、dispatch registryへの変更を検討する + +### MAY + +* 単純なガード節に `if` を使う +* 閉じた型の網羅的処理に `switch` やpattern matchingを使う +* 逐次依存、早期終了、ストリーム、メモリ制約のある処理に明示的なループを使う +* 不変条件、プロトコル、閉じた状態集合に定数やenumを使う + +この形なら、思想を保ちながら、AIが機械的に「ifを辞書へ」「forをmapへ」置換する事故を防げます。 + +全体としては、単なるコードスタイルというより、**「業務知識・実行・観測をすべて構造化データで接続する開発方式」**としてまとめると、かなり筋の通った理論になります。次の段階では、この調査結果をもとに、実際の `skill.md` 用の規範、判定フロー、Python/TypeScriptの良い例・悪い例へ落とすのが自然です。 + +[1]: https://www.catb.org/esr/writings/taoup/html/ch01s06.html?utm_source=chatgpt.com "Basics of the Unix Philosophy" +[2]: https://blog.klipse.tech/dop/2022/06/22/principles-of-dop.html "Principles of Data-Oriented Programming | Yehonathan Sharvit" +[3]: https://www.dataorienteddesign.com/dodbook/node2.html?utm_source=chatgpt.com "It's all about the data - Data-oriented design" +[4]: https://llvm.org/docs/TableGen/ "TableGen Overview - LLVM" +[5]: https://www.omg.org/dmn/ "Decision Model and Notation™ (DMN™) | Object Management Group" +[6]: https://openpolicyagent.org/docs/philosophy "Philosophy | Open Policy Agent" +[7]: https://weizmann.elsevierpure.com/en/publications/statecharts-a-visual-formalism-for-complex-systems/?utm_source=chatgpt.com "Statecharts: a visual formalism for complex systems" +[8]: https://developer.mozilla.org/en-US/docs/Glossary/Callback_function?utm_source=chatgpt.com "Callback function - Glossary - MDN Web Docs - Mozilla" +[9]: https://numpy.org/doc/stable/reference/generated/numpy.vectorize?utm_source=chatgpt.com "numpy.vectorize — NumPy v2.5 Manual" +[10]: https://opentelemetry.io/docs/concepts/observability-primer/?utm_source=chatgpt.com "Observability primer" +[11]: https://opentelemetry.io/docs/specs/semconv/general/events/?utm_source=chatgpt.com "Semantic conventions for events" +[12]: https://martinfowler.com/eaaDev/EventSourcing.html?utm_source=chatgpt.com "Event Sourcing" +[13]: https://www.typescriptlang.org/docs/handbook/2/narrowing.html?utm_source=chatgpt.com "Documentation - Narrowing" +[14]: https://docs.python.org/ja/3/library/enum.html?utm_source=chatgpt.com "enum --- 列挙型のサポート" diff --git a/issues/issue-38-coding-rule.md b/issues/issue-38-coding-rule.md new file mode 100644 index 00000000..e1efa1fd --- /dev/null +++ b/issues/issue-38-coding-rule.md @@ -0,0 +1,31 @@ +# analyzable coding skill作成 + +* python / javascript / typescriptなどでプログラムを作成する際のルール反映するskill +* 分析可能、計測可能なコードスタイルを守るためのルール +* リファクタリング時にも参照すること + +## 基本方針は「コードよりもデータ」 +* コードは基本原則のみを記述。判断やロジックは極力データとして表現する +* アプリの振る舞いや結果を極力データとして表現し、アプリの目的だけでなく、アプリ自体の挙動、周辺環境、ユーザーなどが常に計測可能であることを目指す。 + +## 分岐 +* 分岐はデータとして表現する +* if文 / switch文 / case 文による分岐は極力避ける +* 特にif文、switch文のネストやelseによる多段構成は原則利用しない +* 配列、map、dictなどとポインタ、関数参照などを利用した分岐を優先する + +## ループ +* ループ構文(forやwhile)を極力利用しない +* 特に多次元構造のscanや一括処理をfor文、while文で解決しない +* 一括処理が可能かどうか検討する +* ベクトル、行列の問題として対応する(.map(), .apply()など) +* numpyなど、行列演算モジュールを積極的に活用する +* 常に並列化を意識し、並列化しない場合も並列化に切り替え可能なロジックを組む +* ループ内のエラーなどはその場で処理するだけでなく、ループ終了後にどの程度発生したか、何が問題かを報告可能な状態としておく + +## 定数 +* コード中の定数、enumは極力使用しない +* 代わりにデータとして表現する +* データとして表現しない場合も、configクラス等、外部から操作可能な状態を保つ +* カテゴリ等「コードを見なければ値の意味が分からない」などが発生しないようにする +* カテゴリは極力文字列や(データベースの)enum型、set型等で表現し、置き換えが必要なコード値を利用しない diff --git a/issues/issue-38-coding-skill-plan.md b/issues/issue-38-coding-skill-plan.md new file mode 100644 index 00000000..6965977b --- /dev/null +++ b/issues/issue-38-coding-skill-plan.md @@ -0,0 +1,671 @@ +# issue-38: analyzable coding skill の調査結果と実装プラン + +## 関連リンク + +- 草稿: [issues/issue-38-coding-rule.md](./issue-38-coding-rule.md) +- ChatGPT による調査回答: [issues/issue-38-chatgpt-response.md](./issue-38-chatgpt-response.md) +- Skill 執筆規約: [plugins/ndf-shared/skills/README.md](../plugins/ndf-shared/skills/README.md) +- Skill 棚卸し: [docs/specifications/ndf-skill-inventory.md](../docs/specifications/ndf-skill-inventory.md) + +## モード + +`architecture`。根拠: Skill の追加は公開インタフェース(`/ndf:analyzable-coding` というコマンド)の追加にあたる。既存 Skill の判定ロジックと配布物の互換は壊さない。 + +必須工程: requirements-design(本ファイルの 2.3 / 2.5 / 2.6 に統合)→ implementation-plan(本ファイル)→ tdd-cycle(検査スクリプトを先に落として通す)→ cross-review → quality-gates → plan-to-spec + +--- + +# 第 1 部: 調査結果 + +## 1.1 結論 + +草稿の主張と一対一で対応する単一の開発理論は存在しない。ただし**個々の主張はすべて既存理論に対応物がある**。実態は 5 系統の合成である。Claude / ChatGPT の 2 系統で独立に調査し、この結論は一致した。 + +| 草稿の主張 | 最も近い既存理論 | 出典 | 近さ | +| --- | --- | --- | --- | +| コードよりもデータ | Rule of Representation | Unix 哲学(Eric Raymond) | 非常に近い | +| コードよりもデータ | Data-Oriented Programming (DOP) | Yehonathan Sharvit | 非常に近い | +| 分岐をデータで表現 | Table-Driven Methods | Code Complete 18 章 | 非常に近い | +| 分岐をデータで表現 | Decision Table / DMN / Policy as Code | OMG DMN, Open Policy Agent | 近い | +| ループを避けベクトル化 | Array Programming | Kenneth Iverson (APL), NumPy | 近い | +| 並列化可能性を保つ | MapReduce / Dataflow | Spark, Beam | 近い | +| 定数を外部データへ | Magic Number 排除 / Twelve-Factor Config | 一般的リファクタリング原則 | 近い | +| 常に計測可能 | Observability 2.0 / OpenTelemetry | Charity Majors, OTel semconv | 近い | +| 状態変化をデータ化 | Event Sourcing | Martin Fowler | 部分的に近い | +| ループ後にエラーを集計報告 | Notification パターン | Martin Fowler | 非常に近い | + +## 1.2 系譜ごとの整理 + +### (1) 「コードよりもデータ」— 最も由緒ある系譜 + +草稿の基本方針そのものが 40 年以上前から繰り返されている主張である。 + +- **Fred Brooks**(人月の神話): 「フローチャートを見せてテーブルを隠されたら私は困惑し続ける。テーブルを見せてくれれば、フローチャートはたいてい要らない」 +- **Rob Pike ルール 5**: 「Data dominates. 正しいデータ構造を選び、うまく整理していれば、アルゴリズムはほぼ自明になる」 +- **Linus Torvalds**: 「悪いプログラマはコードを心配する。良いプログラマはデータ構造とその関係を心配する」 +- **Eric Raymond, Rule of Representation**: "Fold knowledge into data, so program logic can be stupid and robust." — 草稿の 1 行目とほぼ同じ主張であり、**草稿の直接の祖先**と言ってよい +- **Alan Perlis のエピグラム**: 「10 種のデータ構造に 10 個の関数より、1 つのデータ構造に 100 個の関数のほうがよい」 + +理論名として最も近いのは Sharvit の **Data-Oriented Programming**。4 原則からなる(下記 1.4 の事実確認を参照)。 + +### (2) 分岐 — Table-Driven Methods + +`if` / `switch` の連鎖を表・マップ・決定表に置き換える手法は **Code Complete 第 18 章**で体系化済み。「論理文で選べるものはほぼすべてテーブルでも選べる」「ロジックの連鎖が複雑になるほどテーブルが有利になる」という記述は、草稿の「特にネストや else の多段構成を避ける」という条件付けと合致する。 + +近縁: data-directed programming(SICP 2.4.3)、Replace Conditional with Polymorphism(Fowler)、Dispatch Table、Rules Engine、LLVM TableGen、OMG DMN、Open Policy Agent の "policy as data"。 + +分岐の種類ごとに適切な表現が異なる点は、skill に落とすうえで重要である。 + +| 分岐の種類 | 適切な表現 | +| --- | --- | +| 値から値への単純な対応 | `dict` / `Map` / lookup table | +| 処理方式の切り替え | dispatch table / handler registry | +| 条件の組み合わせが多い業務判断 | decision table / DMN / rules engine | +| 認可・制約・組織ポリシー | policy engine | +| 時系列で状態が変化する処理 | state machine / statechart | +| 閉じた少数の型分岐 | 型付き `switch` / pattern matching | + +### (3) ループ — Array Programming と Dataflow + +源流は **Kenneth Iverson の APL** とチューリング賞講演 "Notation as a Tool of Thought"(1979)。配列を第一級市民とし、逐次ループを表面から消す。NumPy / pandas / Polars のベクトル化はその直系の子孫。並列化可能性を保つ主張は MapReduce・純粋関数・データ並列の議論に対応する。 + +「ループ内エラーを終了後に集計報告可能にする」は **Martin Fowler の Notification パターン**("Replacing Throwing Exceptions with Notification in Validations")そのもの。最初のエラーで止めず全エラーを収集して返す。Spark の accumulator、pandas の `errors='coerce'`、dead letter queue も同系統。草稿の中で特に固有性が高い部分である。 + +### (4) 定数 — Magic Number 排除と設定の外部化 + +**Magic Number アンチパターン**と **Twelve-Factor App の Config** が対応する。ただし後述のとおり、草稿で最もリスクの高い項目でもある。 + +### (5) 計測可能性 — Observability 2.0 / OpenTelemetry + +Charity Majors の **Observability 2.0** は「任意に幅広い構造化イベント(wide structured events)を単一の真実の源とし、他のデータ型はそこから導出する」「1 アクションにつき 1 イベントを、完了 / エラー直前に発行する」と定める。OpenTelemetry の Semantic Conventions は属性名と意味をそろえ、相関・集約可能にする。草稿の「振る舞いや結果を極力データとして表現する」を実装レベルに落とすとこれになる。 + +状態変化そのものをイベント列として保存する **Event Sourcing** も近いが、通常の監査ログよりはるかに大きなアーキテクチャ選択であり、全アプリに適用すべきものではない。 + +## 1.3 2 つの調査で一致した「危険な点」 + +両調査が独立に、**草稿をそのまま skill にすると害になる**と指摘した。要点は 3 つ。 + +### 危険 1: 「if / switch を避ける」の過剰適用 + +- 「if を消すことで、かえって読みにくく・デバッグしにくく・微妙に間違えやすくなる。単純な if のほうが意図をよく伝える」(Code Complete および table-driven 手法の一般的トレードオフ) +- 「多態への置換自体が、最低 1 つの条件分岐(実装選択)を必要とする」 +- 8th Light の整理: **「条件分岐は悪ではない。重複した条件分岐が悪である」** +- TypeScript の discriminated union + `never` による網羅性チェックを動的な文字列辞書に置き換えると、**静的解析能力が下がる** +- 同じことが PHP にも当てはまる。PHP 8.1 の backed enum に対する `match` 式は、未処理のケースがあれば実行時に `\UnhandledMatchError` を投げ、さらに PHPStan が `match.unhandled`("Match expression does not handle remaining value")として**静的に**検出する。これを連想配列のディスパッチへ置き換えると、この検査が効かなくなる + +→ 「`if` / `switch` を避ける」ではなく「**変化頻度の高い業務判断や多数の条件組み合わせを、ネストした制御構文へ埋め込まない。小さく閉じた型分岐・ガード節・不変条件検査には明示的な分岐を使ってよい**」とすべき。 + +### 危険 2: 定数の外部化しすぎ = Inner-Platform Effect + +Alex Papadimoulis が名付けた **Soft Coding / Inner-Platform Effect**(The Daily WTF, 2006)は、「定数をすべて外部化した結果、プラットフォームの劣化コピーを作ってしまい、顧客どころか熟練プログラマにしか変更できなくなる」現象。条件・優先順位・参照・式・継承・デフォルト・再試行を設定データへ足していくと、やがて独自 DSL になる。LLVM TableGen のドキュメント自身も DSL 複雑化の問題に触れている。 + +データへ移しただけで複雑性は消えない。目的は**複雑性を検査・可視化・変更可能な形式へ移すこと**であり、外部化するなら最低限これらが要る: + +スキーマ / 型・バリデーション / バージョン / 変更履歴 / 競合・到達不能ルールの検出 / テスト / マイグレーション / 監査 / 実行時に適用された rule_id と理由の記録。 + +### 危険 3: 「`.map()` / `.apply()` でベクトル化」は事実として誤り + +草稿の「ベクトル、行列の問題として対応する(`.map()`、`.apply()` など)」は不正確。`.map()` / `.apply()` は必ずしもベクトル演算でも並列処理でもない。混同すると「for を map に置換したから速くなった / 並列化した」という誤った達成感につながる。 + +| 種類 | 例 | 性質 | +| --- | --- | --- | +| 高階反復 | JS `map`、Python `map`、pandas `.apply`、PHP `array_map` | ループの記述方法を変えただけ | +| ネイティブベクトル演算 | NumPy ufunc、broadcasting | C / SIMD / GPU 等で一括実行 | +| バッチ処理 | SQL、bulk API、bulk insert | 呼出し回数や I/O をまとめる | +| 並行処理 | `Promise.all`、async task、PHP Fiber | 待ち時間を重ねる。CPU 並列とは限らない | +| 並列処理 | multiprocessing、worker、JAX | 複数 CPU / GPU で同時実行 | +| 分散データフロー | Beam、Spark、MapReduce | 分割・再試行・集約を実行基盤が管理 | + +→ 「for を見つけたら map へ置き換える」ではなく「**同種の独立したデータ変換は、逐次制御として書く前に、バッチ演算・ベクトル演算・データフロー・並行/並列実行として表現できないか検討する**」。 + +**PHP を対象に加えたことで、この論点はさらに強くなった。** PHP にはネイティブなベクトル演算基盤が標準では存在せず、`array_map` は高階反復に、`array_column` は組み込みの逐次走査にとどまる。つまり草稿の「numpy など行列演算モジュールを積極的に活用する」は Python 固有の手段であり、言語非依存の規範としては成立しない。3 言語に共通して効くのは、次の 2 段だけである。 + +1. **一括処理として表現できないか**(バッチ・データフロー・並行/並列への切替可能性を保つ) +2. **その言語に一括実行基盤があるなら使う**(Python なら NumPy、PHP なら bulk SQL / chunk 取得による N+1 の解消) + +規範は 1 に置き、2 は言語別の参照資料へ落とす。この切り分けが、Skill を言語非依存に保ちながら PHP でも実用にするための設計上の要になる。 + +## 1.4 事実確認の記録 + +調査中に出た主張のうち、一次情報で裏を取ったもの。 + +| 主張 | 検証結果 | +| --- | --- | +| DOP の原則は 3 個 / 5 個という記述が両調査に出た | **正しくは 4 原則**。(1) コードとデータの分離 (2) 汎用データ構造での表現 (3) データの不変性 (4) データスキーマとデータ表現の分離。Sharvit 本人の記事で確認 | +| `np.vectorize` は性能目的ではない | **正しい**。NumPy 公式: "The vectorize function is provided primarily for convenience, not for performance. The implementation is essentially a for loop." | +| Data-Oriented Design と Data-Oriented Programming は別物 | **正しい**。前者(Mike Acton / Unity DOTS)はメモリ配置とキャッシュ効率の話。skill 名に `data-oriented` を使うと混同されるため避ける | + +## 1.5 草稿の独自性 + +以下 6 点を 1 つにまとめた、広く定着した理論名は見つからなかった。 + +1. ルールや判断をデータへ移す +2. 処理をデータ変換として構成する +3. 逐次ループよりバッチ・ベクトル・データフローを優先する +4. 並列化可能性を保つ +5. 判断理由・結果・失敗を構造化データとして観測する +6. これを実装だけでなくリファクタリング規範として使う + +思想の各部品は既存だが、**それらを言語横断のコード生成・レビュー・リファクタリング skill として統合する点には十分な独自性がある**。 + +## 1.6 言語非依存性をどう担保するか(Python / JavaScript / TypeScript / PHP) + +対象言語は Python / JavaScript / TypeScript / PHP とする。ただし **Skill 自体は言語に寄らず発動し、使えること**を要件とする。これは「例を 4 言語分並べる」ことではなく、**規範の階層を分けること**で達成する。 + +| 階層 | 内容 | 言語依存 | 置き場所 | +| --- | --- | --- | --- | +| 原則 | 変化する知識はデータへ、安定した機構はコードへ | なし | SKILL.md | +| 判定 | 分岐/反復/定数の種類 → 適切な表現の対応 | なし | SKILL.md | +| 手段 | その表現を実現する言語機能・ライブラリ | **あり** | `references/language-notes.md` | + +判定層までを言語非依存に保てば、Go や Ruby など対象外の言語でも Skill は成立する。逆に「numpy を使え」「`never` で網羅性を検査せよ」を規範本文に書くと、その言語以外では使えない Skill になる。 + +4 言語での手段の対応は次のとおり。 + +| 判定 | Python | JavaScript | TypeScript | PHP | +| --- | --- | --- | --- | --- | +| 値 → 値の対応 | `dict`、`Mapping` | `Object.freeze` のオブジェクト | `Record`、`as const` | 連想配列、`match` | +| 処理方式の切替 | 関数を値として持つ `dict` | 関数を値として持つオブジェクト | handler map | first-class callable 構文 `foo(...)`(静的解析が追える) | +| 閉じた状態集合 | `Enum`、`Literal` | 凍結した定数オブジェクト | discriminated union、literal union | backed enum(8.1+) | +| 網羅性の静的検査 | mypy の `assert_never` | **手段なし**(実行時に失敗させる) | `never` による網羅性チェック | PHPStan の `match.unhandled` | +| 不変性 | `frozen=True` の dataclass | `Object.freeze` | `readonly`、`as const` | `readonly` プロパティ(8.1+) | +| スキーマ検証 | pydantic、jsonschema | zod、JSON Schema | zod、JSON Schema | JSON Schema、Valinor 等 | +| 一括処理 | NumPy / pandas(ベクトル演算) | バッチ API、`Promise.all` | 同左 | **bulk SQL / chunk 取得**(ベクトル演算基盤はない) | +| 失敗の集計 | 例外を集めて返す / `errors='coerce'` | 失敗を集めて返す | Result 型、集約 | 例外を集めて返す(Notification パターン) | + +JavaScript の列で重要なのは、手段は TypeScript と同じでありながら、**網羅性の静的検査だけが成立しない**こと。したがって JS では MAY の「静的に網羅性を検査できる分岐はそのままでよい」という条件が成立しにくく、静的検査で守れない分をスキーマ検証と実行時の即時失敗で埋める必要性が TypeScript より高い。 + +PHP の列で重要なのは 2 点。**first-class callable 構文と backed enum によって「データ化しても静的解析が効く」範囲が PHP でも成立する**こと。そして **一括処理の主戦場が SIMD ではなく I/O(N+1 の解消、bulk insert)である**こと。後者は 1.3 危険 3 の結論と一致する。 + +## 1.7 中核方針の再定義(採用案) + +草稿の「コードよりもデータ」は、そのままだと「何でも設定ファイルへ追い出す」と誤解される。次の形に再定義する。 + +> **変化する知識はデータへ。安定した機構と不変条件はコードへ。両方をスキーマと計測で接続する。** + +展開形: + +> 変化する判断や業務知識は、検証可能・版管理可能なデータまたはポリシーとして表現する。安定した機構・不変条件・型・境界はコードとスキーマとして表現する。処理は明示的な入力から出力へのデータ変換として構成し、依存関係・順序・副作用を明らかにする。各判断・結果・失敗は、理由・適用ルール・バージョン・実行コンテキストを伴う構造化データとして観測・再現可能にする。 + +--- + +# 第 2 部: 実装プラン + +## 2.1 目的と非目的 + +達成したい状態: + +- Python / JavaScript / TypeScript / **PHP** でコードを書く際に、分析可能・計測可能なコードスタイルを保つための判断基準を、NDF の Skill として 3 ランタイムへ配布する +- **Skill 自体は言語に寄らず発動し、使える。** 対象言語は例示と手段の記載範囲であって、発動条件でも適用範囲の上限でもない +- 新規実装時だけでなくリファクタリング時にも参照される +- AI エージェントが「if を辞書へ」「for を map へ」と機械的置換する事故を、Skill 自身が防ぐ + +やらないこと: + +- 静的解析ツール(linter ルール、AST チェッカ)の実装。今回は判断基準の文書化のみ +- Observability 基盤の導入手順(OpenTelemetry のセットアップなど)。計測すべき内容の規範にとどめる +- Event Sourcing の採用指針。参照として言及するが、規範には含めない +- 既存 Skill(`safe-refactoring` / `tdd-cycle` / `logging-guidelines`)の書き換え。相互参照のリンク追加のみ +- 対象 4 言語以外(Go / Ruby / Java など)の言語別手段の記載。規範は言語非依存に保つため、これらの言語でも Skill は成立するが、`references/language-notes.md` への追記は今回の範囲外とする +- フレームワーク固有の規約(Laravel / Django / NestJS など) + +## 2.2 前提 + +- 前提 1: Skill 名は `analyzable-coding` とする。`data-oriented` 系の名称は Mike Acton 系の Data-Oriented Design と衝突するため採用しない。→ 検証: 名称が `plugins/ndf-shared/skills/analyzable-coding/` に存在し、既知の外部 Skill 名と衝突しないこと(`check-skill-frontmatter.py --strict` の警告で確認) +- 前提 2: 3 ランタイム(Claude Code / Codex / Kiro)すべてに配布する。ランタイム固有の機能に依存しない内容のため。→ 検証: 3 つの manifest すべてに追記され、build 後に各配布物へ生成されること +- 前提 3: 規範は禁止形(MUST NOT の列挙)ではなく MUST / SHOULD / MAY の 3 段で書く。→ 検証: SKILL.md に 3 段構成が存在し、MAY 節に「明示的な `if` / `for` / enum を使ってよい場合」が含まれること +- 前提 4: SKILL.md 本文(原則層・判定層)は言語非依存に保ち、言語固有の手段は `references/language-notes.md` に分離する。→ 検証: AC-9 のとおり、SKILL.md 本文に特定言語の API 名・ライブラリ名が現れないこと +- 前提 5: PHP は 8.1 以降を前提とする(backed enum / `readonly` / first-class callable 構文がこの版から)。8.0 以下では代替手段を注記する。→ 検証: `references/language-notes.md` に必要バージョンが明記されていること + +## 2.3 受け入れ条件 + +- [x] AC-1: `plugins/ndf-shared/skills/analyzable-coding/SKILL.md` が存在し、`python3 scripts/check-skill-frontmatter.py --strict` が成功する +- [x] AC-2: SKILL.md が MUST / SHOULD / MAY の 3 段構成を持ち、MAY 節に「単純なガード節の `if`」「閉じた型の網羅的 `switch`」「逐次依存・早期終了・ストリーム処理の明示的ループ」「不変条件・プロトコル・閉じた状態集合の定数と enum」の 4 つが明記されている +- [x] AC-3: 「分岐の種類 → 適切な表現」「反復の種類(高階反復 / ベクトル演算 / バッチ / 並行 / 並列 / 分散)」「定数の性質 → 置き場所」の 3 つの判定表が含まれる。いずれの表も、行の内容が特定言語の機能名に依存していない +- [x] AC-4: Python / JavaScript / TypeScript / PHP の 4 言語について、良い例・悪い例が最低 1 組ずつ含まれる(`references/language-notes.md` を含めた全体で判定してよい) +- [x] AC-9: **言語非依存性** — (a) `description` と `when_to_use` に言語名が発動条件として現れない (b) SKILL.md 本文(原則層・判定層)に特定言語の API 名・ライブラリ名・バージョン番号が現れず、言語固有の記述はすべて `references/language-notes.md` にある (c) 判定表の各行が、対象 4 言語のいずれにも依存しない語で書かれている +- [x] AC-5: 3 つの manifest(`claude-skills.txt` / `codex-skills.txt` / `kiro-skills.txt`)に `analyzable-coding` が追記され、`bash scripts/build-runtime-plugins.sh` 実行後に `plugins/ndf-{claude,codex,kiro}/skills/analyzable-coding/SKILL.md` が生成される +- [x] AC-6: `bash scripts/validate-runtime-plugins.sh` と `python3 scripts/check-markdown-links.py` が成功する +- [x] AC-7: README.md / CLAUDE.md の Skill 個数と開発方法論グループの記載が更新される(Skill 30 → 31、開発方法論 5 → 6、Claude 26 → 27 / Codex 24 → 25 / Kiro 25 → 26) +- [x] AC-8: 起きてはいけないこと — SKILL.md 内に「`if` を使うな」「`for` を使うな」「enum を使うな」という無条件の禁止表現が存在しない + +## 2.4 代替案と採否 + +| 案 | 内容 | 採否 | 理由 | +| --- | --- | --- | --- | +| A | 独立 Skill `analyzable-coding` を新規追加 | **採用** | 実装時とリファクタリング時の双方から参照される横断規範であり、既存 Skill のどれにも収まらない | +| B | `safe-refactoring` に節を追加 | 不採用 | 新規実装時に発動しない。`safe-refactoring` はテストで守る手順の Skill であり、コードスタイル規範とは責務が違う | +| C | `logging-guidelines` を拡張して計測部分だけ扱う | 不採用 | 草稿の中核(分岐・ループ・定数のデータ化)が落ちる | +| D | 名称を `observable-data-oriented-development` 等にする | 不採用 | 長く、Data-Oriented Design と混同される。`analyzable-coding` のほうがトリガ語として日本語(分析可能・計測可能)に結び付けやすい | +| E | 禁止規則(MUST NOT 列挙)として書く | 不採用 | 1.3 の危険 1〜3 をそのまま踏む。AI エージェントの機械的置換を誘発する | + +## 2.5 ドメイン用語 + +| 用語 | 意味 | +| --- | --- | +| 分析可能(analyzable) | 実行前に、判断の根拠と網羅性を型・スキーマ・テーブルから検査できる状態 | +| 計測可能(measurable / observable) | 実行後に、どの判断がなぜ下されたかを構造化データから再現できる状態 | +| 判断(decision) | 業務ルールに基づく分岐。技術的な不変条件チェック(ガード節)とは区別する | +| 高階反復 | `map` / `apply` など、ループの記述方法を変えただけのもの。ベクトル演算ではない | + +## 2.6 不変条件 + +- Skill は判定基準を提示するだけで、コードを書き換える手順そのものは持たない(書き換え手順は `safe-refactoring` の責務) +- モード判定は `development-workflow` の責務。この Skill は判定結果を受け取る側に徹する +- 3 ランタイムの配布物は `plugins/ndf-shared/` からの生成物であり、直接編集しない +- **SKILL.md 本文の原則層・判定層は、対象言語が増減しても書き換えなくてよい状態を保つ。** 言語の追加は `references/language-notes.md` への追記だけで完結する + +## 2.7 互換性 + +| 対象 | 変更 | 互換性の扱い | +| --- | --- | --- | +| 公開 Skill 一覧 | `analyzable-coding` を追加 | 追加のみ。既存 Skill の名称・挙動は変えない | +| manifest ファイル | 3 ファイルに 1 行追記 | 追加のみ | +| プラグインバージョン | ndf v7.0.0 → v7.1.0 | Skill 追加のため minor | + +## 2.8 修正対象 + +新規: + +- `plugins/ndf-shared/skills/analyzable-coding/SKILL.md` +- `plugins/ndf-shared/skills/analyzable-coding/references/language-notes.md`(言語別の手段。Python / JavaScript / TypeScript / PHP) +- `plugins/ndf-shared/skills/analyzable-coding/references/decision-tables.md`(判定表と例が SKILL.md に収まらない場合) + +変更: + +- `plugins/ndf-shared/manifests/claude-skills.txt` +- `plugins/ndf-shared/manifests/codex-skills.txt` +- `plugins/ndf-shared/manifests/kiro-skills.txt` +- `plugins/ndf-shared/skills/safe-refactoring/SKILL.md`(相互参照リンク) +- `plugins/ndf-shared/skills/development-workflow/references/workflow-modes.md`(工程への組み込み) +- `plugins/ndf-claude/.claude-plugin/plugin.json` ほか各ランタイムの `plugin.json`(バージョン) +- `README.md` / `CLAUDE.md`(Skill 個数・一覧) +- `docs/specifications/ndf-skill-inventory.md`(棚卸し記録) + +生成物(`scripts/build-runtime-plugins.sh` が出力。手で編集しない): + +- `plugins/ndf-{claude,codex,kiro}/skills/analyzable-coding/` + +## 2.9 タスク分解 + +### Task 1: SKILL.md の中核(方針と MUST / SHOULD / MAY)を書く + +- **対象ファイル:** `plugins/ndf-shared/skills/analyzable-coding/SKILL.md` +- **変更内容:** frontmatter(`name` / `description`。トリガ語は `Use when` 文末の全角括弧に `・` 区切り、例: `(分析可能なコード・データ駆動・分岐をデータ化・計測可能な実装)`。**言語名はトリガ語に入れない** — 入れると未記載の言語で発動しなくなる)と、1.7 の中核方針、以下の 3 段規範。 + - **MUST**: 外部化したルール・設定・マスタデータにスキーマとバージョンを持たせる / 判断結果に `rule_id`・`rule_version`・`reason` を記録できるようにする / バッチ処理では成功件数だけでなく全失敗の件数・種類・対象を報告する / 外部 I/O・副作用・時刻・乱数を境界として明示する / 不変条件とセキュリティ制約を変更可能な設定だけに依存させない / 閉じた状態集合は型またはスキーマで網羅性を検査する + - **SHOULD**: 条件の組み合わせが多い業務判断は decision table か policy として表現する / 純粋なデータ変換と副作用を分離する / 同種の大量処理では逐次ループよりバッチ・ベクトル・データフローを検討する / 並行化する処理は冪等性・順序依存・再試行・タイムアウト・キャンセルを明示する / ネストした条件分岐はガード節・状態機械・決定表・dispatch registry への変更を検討する + - **MAY**: 単純なガード節の `if` / 閉じた型の網羅的 `switch`・pattern matching / 逐次依存・早期終了・ストリーム・メモリ制約下の明示的ループ / 不変条件・プロトコル・閉じた状態集合の定数と enum +- **満たす受け入れ条件:** AC-1, AC-2, AC-8, AC-9 +- **進め方:** 先に `python3 scripts/check-skill-frontmatter.py --strict` が落ちることを確認 → frontmatter を書いて通す → 本文を書く。書き上げたら AC-9(b) を検査するため、本文を `grep -inE "numpy|pandas|typescript|phpstan|readonly|dataclass|enum\(|array_map"` にかけ、ヒットしたら `references/` へ移す + +### Task 2: 判定表 3 種を書く + +- **対象ファイル:** `plugins/ndf-shared/skills/analyzable-coding/SKILL.md`(長くなる場合は `references/decision-tables.md` へ分離) +- **変更内容:** 1.2(2) の「分岐の種類 → 適切な表現」、1.3 危険 3 の「反復の種類」、および定数の分類表を収録する。定数の分類表は次のとおり。 + + | 値の性質 | 置き場所 | + | --- | --- | + | 数学的・技術的な不変条件 | コード内の定数 | + | 閉じた状態集合・プロトコル | enum / literal union / schema | + | 頻繁に変わる業務ルール | decision table / policy / 設定 | + | 環境ごとに変わる値 | deployment config / 環境変数 | + | ユーザーが管理するカテゴリ | DB のマスタ・参照テーブル | + | 表示名・文言 | i18n / コンテンツデータ | + | セキュリティ上の絶対制約 | コード・型・schema・policy の複数層 | + +- **満たす受け入れ条件:** AC-3 +- **進め方:** 表を書く → 各行に「この行を選んだ結果どうなるか」を 1 例ずつ添えられるか確認し、添えられない行は削る + +### Task 3: 言語非依存の例(判断の記録)を書く + +- **対象ファイル:** `plugins/ndf-shared/skills/analyzable-coding/SKILL.md` +- **変更内容:** SKILL.md 本文に置くのは、言語に依存しない形で示せる例だけに限る。(a) 分岐のデータ化: rank → 割引率の if 連鎖 vs 対応表(擬似コードまたは表で示し、特定言語の構文に寄せない)(b) 判断の記録: `rule_id` / `rule_version` / `reason` / 実行コンテキストを含む構造化イベントの JSON 例(JSON なので言語非依存)(c) 失敗の集計: バッチ処理で全失敗の件数・種類・対象を返す形の擬似コード +- **満たす受け入れ条件:** AC-4, AC-8, AC-9 +- **進め方:** 各例を「この例から言語固有の語を消せるか」で点検する。消せないものは Task 4 へ移す + +### Task 4: 言語別の手段(Python / JavaScript / TypeScript / PHP)を書く + +- **対象ファイル:** `plugins/ndf-shared/skills/analyzable-coding/references/language-notes.md` +- **変更内容:** 1.6 の対応表を基に、言語ごとに良い例・悪い例を 1 組以上。 + - **共通の MAY の例**(言語を問わず示す): ガード節の `if` と、閉じた型に対する網羅的分岐を「そのままでよい例」として先に提示する + - **Python**: 逐次ループ vs NumPy broadcasting。あわせて `np.vectorize` と `.apply` が高速化にならない反例(公式注記 "provided primarily for convenience, not for performance" を引用)。網羅性は mypy の `assert_never` + - **TypeScript**: discriminated union + `never` による網羅性チェックを、動的な文字列辞書へ置き換えると静的解析が落ちる例 + - **JavaScript**: 手段は TypeScript と同じだが型注釈がなく網羅性の静的検査が効かないこと、その分を実行時の即時失敗とスキーマ検証で埋める例(未知のキーで `undefined` を返す悪い例 / 明示的に失敗させる良い例) + - **PHP(8.1+)**: backed enum + `match` を PHPStan の `match.unhandled` が静的に検出する例。dispatch table は first-class callable 構文 `foo(...)` で書くと静的解析が追えること。**一括処理は N+1 の解消と bulk insert であってベクトル演算ではない**ことを明記し、`array_map` への置換が高速化ではない旨を書く。8.0 以下向けの代替(クラス定数 + `switch`)を注記 +- **満たす受け入れ条件:** AC-4, AC-9 +- **進め方:** 例のコードは実際に動かして確認する。Python の反例は計測して「map / `np.vectorize` への置換では速くならない」ことを数値で示す。PHP は PHPStan を実行して `match.unhandled` が実際に出ることを確認する + +### Task 5: 3 ランタイムへ配布する + +- **対象ファイル:** `plugins/ndf-shared/manifests/*.txt`、各 `plugin.json` +- **変更内容:** manifest 3 ファイルに `analyzable-coding` を追記、プラグインバージョンを v7.1.0 へ更新、`bash scripts/build-runtime-plugins.sh` で配布物を生成 +- **満たす受け入れ条件:** AC-5, AC-6 +- **進め方:** manifest 追記 → build → `bash scripts/validate-runtime-plugins.sh` と `python3 scripts/check-markdown-links.py` で検証 + +### Task 6: 既存 Skill・ドキュメントとの接続 + +- **対象ファイル:** `safe-refactoring/SKILL.md`、`development-workflow/references/workflow-modes.md`、`README.md`、`CLAUDE.md`、`docs/specifications/ndf-skill-inventory.md` +- **変更内容:** `safe-refactoring` から「構造改善の方向性の基準」として参照を追加。`workflow-modes.md` の実装工程に位置づけを追記。README / CLAUDE.md の個数と一覧を更新。inventory に新規 Skill の行を追加 +- **満たす受け入れ条件:** AC-7, AC-6 +- **進め方:** 参照を追加 → `python3 scripts/check-markdown-links.py` でリンク切れを検証 + +## 2.10 影響範囲 + +- 3 ランタイムの NDF 利用者全員に新しい Skill が配布される。既存 Skill の発動判定に干渉しないか、`description` のトリガ語が `safe-refactoring` / `tdd-cycle` と衝突しないかを確認する +- `safe-refactoring` から参照されるため、リファクタリング時の挙動に影響する + +## 2.11 リスクと対処 + +| リスク | 対処 | +| --- | --- | +| AI エージェントが規範を機械的に適用し、`if` / `for` / enum を無条件に置換する | MAY 節を MUST / SHOULD と同じ重みで書き、「そのままでよい例」を先に提示する。AC-8 で禁止表現の不在を確認する | +| 設定外部化を推奨した結果、Inner-Platform Effect を招く | MUST 節に「外部化するならスキーマ・バージョン・テスト・監査が必須」を置き、これを満たせないなら外部化しないと明記する | +| `.map()` = ベクトル化という誤解が Skill 経由で広まる | Task 3(b) で反例を計測付きで示す。NumPy 公式の "provided primarily for convenience, not for performance" を引用する | +| トリガ語が既存 Skill と競合し、発動判定が不安定になる | `check-skill-frontmatter.py --strict` の警告を確認し、`docs/specifications/ndf-skill-inventory.md` の実測手順に沿って発動を確認する | +| Skill が長大化して読まれない | 判定表と例が SKILL.md を圧迫する場合、`references/` へ分離する(Task 2 に分岐を用意済み) | +| 言語別の例が本文へ流れ込み、特定言語向け Skill になる | 原則層・判定層と手段層をファイルで分離し、AC-9(b) の grep 検査で機械的に確認する | +| 対象 4 言語以外で使われたとき、手段が示されず役に立たない | 判定層までで実用に足る粒度にする。`references/language-notes.md` の冒頭に「ここに無い言語では判定表から自分で対応付ける」と明記する | +| PHP で「ベクトル化せよ」と誤解し、無意味な `array_map` 置換が起きる | Task 4 の PHP 節で「一括処理 = N+1 解消と bulk insert」と明示し、`array_map` 置換の反例を置く | + +## 2.12 切り戻し手順 + +- Skill 追加のみで、既存の挙動を変える変更を含まない。manifest から 1 行削除し `build-runtime-plugins.sh` を再実行すれば配布から外れる +- データ移行なし。バージョンを v7.0.0 へ戻せば完全に元へ戻る + +## 2.13 完了の定義 + +- [x] AC-1 〜 AC-9 をすべて満たし、条件ごとに検証コマンドと結果が対応している +- [x] `python3 scripts/check-skill-frontmatter.py --strict` が成功 +- [x] `bash scripts/build-runtime-plugins.sh --check` が成功 +- [x] `bash scripts/validate-runtime-plugins.sh` が成功 +- [x] `python3 scripts/check-markdown-links.py` が成功 +- [x] `claude plugin validate` が成功(`validate-runtime-plugins.sh` に含まれる) +- [ ] 3 ランタイムで実際に Skill が発動することを確認し、結果を `docs/specifications/ndf-skill-inventory.md` へ記録 +- [ ] 発動確認は言語を変えて行う。**PHP のコードを対象にした依頼**と、**対象 4 言語以外(Go など)のコードを対象にした依頼**の双方で発動することを確認する(AC-9(a) の実証) + +--- + +## 参考文献 + +### コードよりデータ + +- [Basics of the Unix Philosophy — Rule of Representation](https://www.catb.org/esr/writings/taoup/html/ch01s06.html) +- [Rob Pike's 5 Rules of Programming](https://www.cs.unc.edu/~stotts/COMP590-059-f24/robsrules.html) +- [Linus Torvalds on data structures](https://groups.google.com/g/mechanical-sympathy/c/CHrUYiwqKIQ/m/vt_qdRi70NoJ) +- [Principles of Data-Oriented Programming — Yehonathan Sharvit](https://blog.klipse.tech/dop/2022/06/22/principles-of-dop.html) +- [Data-Oriented Programming (Manning)](https://www.manning.com/books/data-oriented-programming) +- [Data-Oriented Design(別概念・混同注意)](https://www.dataorienteddesign.com/dodbook/node2.html) + +### 分岐 + +- [Code Complete 2nd Ed. Ch.18 Table-Driven Methods](https://www.oreilly.com/library/view/Code-Complete,-Second-Edition/0735619670/ch18.html) +- [Replace Conditional with Polymorphism — Refactoring.Guru](https://refactoring.guru/replace-conditional-with-polymorphism) +- [Conditionals Aren't Evil, Unless You Duplicate Them — 8th Light](https://8thlight.com/blog/wai-lee-chin-feman/2013/08/11/anti-anti-if.html) +- [Destroy All Ifs — John A. De Goes](https://degoes.net/articles/destroy-all-ifs) +- [TableGen Overview — LLVM](https://llvm.org/docs/TableGen/) +- [Decision Model and Notation (DMN) — OMG](https://www.omg.org/dmn/) +- [Open Policy Agent — Philosophy](https://openpolicyagent.org/docs/philosophy) +- [Statecharts: a visual formalism for complex systems — Harel](https://weizmann.elsevierpure.com/en/publications/statecharts-a-visual-formalism-for-complex-systems/) + +### ループ・並列化 + +- [Notation as a Tool of Thought — Iverson (ACM)](https://dl.acm.org/doi/pdf/10.1145/1283920.1283935) +- [Look Ma, No For Loops: Array Programming With NumPy](https://realpython.com/numpy-array-programming/) +- [numpy.vectorize — 性能目的ではないという公式注記](https://numpy.org/doc/stable/reference/generated/numpy.vectorize.html) +- [Replacing Throwing Exceptions with Notification in Validations — Martin Fowler](https://martinfowler.com/articles/replaceThrowWithNotification.html) + +### 定数・設定・型(言語別) + +- [Inner-platform effect — Wikipedia](https://en.wikipedia.org/wiki/Inner-platform_effect) +- [The Inner-Platform Effect — Exception Not Found](https://exceptionnotfound.net/the-inner-platform-effect-the-daily-software-anti-pattern/) +- [TypeScript Handbook — Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) +- [Python enum](https://docs.python.org/ja/3/library/enum.html) +- [PHP 8.1 リリースアナウンス(enum / readonly / first-class callable)](https://www.php.net/releases/8.1/en.php) +- [PHP 8.1: What's New and Changed — PHP.Watch](https://php.watch/versions/8.1) +- [PHPStan `match.unhandled` エラー識別子](https://phpstan.org/error-identifiers/match.unhandled) +- [First-class Callable Syntax in PHP 8.1](https://lindevs.com/first-class-callable-syntax-in-php-8-1/) + +### 計測 + +- [Live Your Best Life With Structured Events — charity.wtf](https://charity.wtf/2022/08/15/live-your-best-life-with-structured-events/) +- [Observability 2.0: Transforming Logging and Metrics](https://gotopia.tech/articles/336/observability-2-transforming-logging-and-metrics-in-software) +- [OpenTelemetry Observability Primer](https://opentelemetry.io/docs/concepts/observability-primer/) +- [OpenTelemetry Semantic conventions for events](https://opentelemetry.io/docs/specs/semconv/general/events/) +- [Event Sourcing — Martin Fowler](https://martinfowler.com/eaaDev/EventSourcing.html) + + +--- + +# 第 4 部: 独立 Skill から `refactoring` への統合 + +## 4.1 独立 Skill にしなかった理由 + +`analyzable-coding` を単独の Skill として配ると、**発動条件を書けない**。他の Skill は発動する +瞬間を指している(完了報告の直前、バグ修正時、調査レポートを書くとき)のに対し、この内容の +発動条件は「コードを書くとき」以外に書きようがなく、常に該当するトリガは発動判定として +働かない。加えて、読んだエージェントが何を出力し何をもって適用完了とするかも規定できて +いなかった。 + +内容の重複も判定を裏づけた。既存のコードスメル 14 件のうち 5 件(マジックナンバー・文字列 / +設定の散在 / 深いネスト / 例外の飲み込み / 条件分岐の連鎖)と重なっており、棚卸台帳の +判断基準「機能が他 Skill と重複するものは統合の対象とし、内容は統合先へ残す」に該当する。 + +## 4.2 決定 + +`safe-refactoring` を **`refactoring`** へ改名し、観点を統合する。発動点は「リファクタリングを +始めるとき」に定まり、既存の手順(テストで守る・1 手ずつ)に観点が組み込まれる。 + +| 変更 | 内容 | +| --- | --- | +| 改名 | `safe-refactoring` → `refactoring`(公開コマンドの非互換変更) | +| 版 | v7.1.0 → **v8.0.0** | +| Skill 数 | 31 → **30**(Claude 26 / Codex 24 / Kiro 25) | +| 移行対応表 | `ndf-policies` に追加(v9.0.0 で削除)。v7.0.0 の対応表は予告どおり削除 | + +## 4.3 統合先の配置 + +| 追加先 | 内容 | +| --- | --- | +| `references/code-smells.md` | スメル 3 件追加 — 業務ルールの埋め込み / 一件ずつの反復 / 検証のない外部化 | +| `references/refactoring-catalog.md` | 手法 2 件追加 — 対応表への置き換え / 一括処理への置き換え | +| `references/data-representation.md`(新規) | 判定 3 表、手を付けないもの、改善にならない置き換え、外部化してよい条件、判断の記録 | +| `references/language-notes.md`(移設) | Python / JavaScript / TypeScript / PHP での手段 | +| `SKILL.md` | 手順 3 に「何にどう置き換えるかは data-representation.md で決める」を追加 | + +MUST / SHOULD / MAY の 3 段構成は、リファクタリングの文脈では「手を付けないもの」「改善に +ならない置き換え」「外部化してよい条件」として再配置した。規範の宣言ではなく、スメルを +見つけたあとの判断材料として働く。 + +## 4.4 受け入れ条件の読み替え + +第 2 部の受け入れ条件のうち、独立 Skill を前提とするものは次のとおり読み替える。 + +| 条件 | 読み替え | +| --- | --- | +| AC-1 / AC-5(新規 Skill の追加と配布) | `refactoring` の改名と配布で満たす | +| AC-2(MUST / SHOULD / MAY の 3 段) | 4.3 のとおり再配置。3 段の見出しとしては残さない | +| AC-3(判定表 3 種) | `data-representation.md` に収録 | +| AC-9(言語非依存) | `data-representation.md` 本文と `code-smells.md` / `refactoring-catalog.md` に言語固有語を持ち込まない。言語固有は `language-notes.md` のみ | +| AC-7(個数表記) | 31 → 30、Claude 27 → 26 / Codex 25 → 24 / Kiro 26 → 25 | + +## 4.5 言語別ファイルの分割 + +`references/language-notes.md`(316 行 / 14 KB)を言語ごとに 4 ファイルへ分けた。 + +| ファイル | 行数 | +| --- | ---: | +| `lang-python.md` | 85 | +| `lang-javascript.md` | 45 | +| `lang-typescript.md` | 73 | +| `lang-php.md` | 145 | + +参照ファイルは読んだときだけコンテキストへ載る。1 ファイルにまとめていると、PHP の作業でも +Python / JavaScript / TypeScript の内容まで読み込まれる。分割して SKILL.md に「**対象の言語の +ファイルだけを読む**」と書くことで、実際に要る分だけが載る。 + +各ファイルは自己完結させ、冒頭に「判定 → その言語の手段」の対応表を置いた。言語をまたぐ +対応表は持たない(4 言語分を読ませることになるため)。JavaScript は TypeScript と手段が +重なるが、参照ではなく必要な範囲を書き下ろした。 + +--- + +# 第 3 部: 実装と検証の記録 + +## 3.1 実行した検証 + +| 対象 | コマンド | 結果 | +| --- | --- | --- | +| frontmatter 規約 | `python3 scripts/check-skill-frontmatter.py --strict` | Skill 35 個 — エラー 0 / 警告 0 | +| 検査が新 Skill に効いているか | `description` を意図的に壊して再実行 | エラー 1 件を検出(復元後 0 件) | +| 生成物の同期 | `bash scripts/build-runtime-plugins.sh --check` | up to date | +| 配布物の妥当性 | `bash scripts/validate-runtime-plugins.sh` | passed(`claude plugin validate` を含む) | +| リンク | `python3 scripts/check-markdown-links.py` | valid | +| 予算 | `check-skill-frontmatter.py` | claude 6,041 / 8,000、codex 5,629 / 8,000、frontmatter 合計 10,782 / 11,200 | + +## 3.2 例のコードの実行確認 + +| 言語 | 確認方法 | 結果 | +| --- | --- | --- | +| Python 3.12 | 実行 | `assert_never` を使った網羅的分岐、失敗集計の例がいずれも動作 | +| TypeScript 5 | `tsc --strict --noEmit` | 型検査を通過 | +| TypeScript 5(反例) | ケース追加・キー欠落版を型検査 | `TS2322`(`never` への代入不可)と `TS1360`(`satisfies` の欠落検出)が出ることを確認 | +| PHP 8.3 | 実行 | backed enum + `match`、first-class callable の dispatch、`readonly` が動作。`\UnhandledMatchError` の送出も確認 | +| PHP 8.3 + PHPStan 2.2 | `analyse --level 5 / max` | 下表のとおり | + +### PHPStan の実測(本 PR で新たに判明した事実) + +| 書き方 | level 5 | level max | +| --- | --- | --- | +| `match`(`default` なし) | `match.unhandled` で検出 | 同左 | +| その場に書いた連想配列 | 検出なし | `offsetAccess.notFound` で検出 | +| 外部から渡した対応表(`array`) | 検出なし | **検出なし** | + +当初 `references/language-notes.md` には「連想配列へ移すと検査が効かなくなる」と書いていたが、 +その場に書いた連想配列は level max なら検出されるため不正確だった。実測に合わせて表へ差し替えた。 + +**この表が測っているのは「型情報を伴わずに実行時ロードした場合」に限られる**(3 行目は +`array` という幅の広い型で対応表を受け取る形)。外部化一般について +「静的解析の視界から外れる」と言えるわけではない。スキーマから型・定数を生成してビルド時に +取り込む経路をとれば、外部化しても静的検査は維持できる。この限定は round 3 で反映した(3.6)。 + +## 3.3 受け入れ条件の判定における注記 + +- **AC-9(b)**: SKILL.md 本文に残った言語名は、参照節の + 「`references/language-notes.md` — Python / JavaScript / TypeScript / PHP での手段」1 行のみ。規範ではなく + 参照先の内容説明であり、条件を満たすと判断した +- **AC-8**: 検出された唯一の「使わない」表現は「不透明なコード値を使わない」であり、 + `if` / ループ / 列挙型に対する無条件の禁止ではない。本文冒頭に「この Skill は `if` / ループ / + 定数の**禁止規則ではない**」と明記している + +## 3.4 cross-review round 1 での事実訂正 + +- **一括処理の失敗方針(SKILL.md MUST)**: 初版は「最初の失敗で打ち切らない」を全一括処理へ + 無条件に課していた。原子性を持つ処理・安全性検査・不正入力が後続へ波及する処理では継続の + ほうが危険なため、MUST の適用範囲を「項目どうしが独立に処理できる場合」へ限定し、 + fail-fast / rollback を MAY へ追加した(打ち切り位置・理由・巻き戻し範囲の記録が条件) +- **PHP の一括演算(language-notes.md)**: 初版の「PHP に一括演算の基盤はない」は拡張・外部 + ライブラリまで否定する主張になっていた。第 1 部の調査で確認できたのは標準ランタイムに + ネイティブな基盤がないことなので、「PHP 標準に一括演算の基盤はない」へ範囲を限定し、 + 拡張を採用する場合は計測して選ぶ旨を追記した +- **配布物の版数**: `plugins/ndf-codex/.codex-plugin/plugin.json` は + `scripts/build-runtime-plugins.sh` の生成対象外(`write_codex_mcp_manifest` は + `plugins/mcp/codex/*` 専用で、`ndf-codex` は `skills/` のみ同期される)であり、手で維持する + ファイルである。7.0.0 / 24 skills のまま取り残されていたため 7.1.0 / 25 skills へ更新した + +## 3.5 cross-review round 2 での事実訂正 + +- **pandas `.apply()` の記述(language-notes.md)**: 初版は「行ごとの呼び出し」と断定していたが、 + `DataFrame.apply` は既定 `axis=0` で列単位、`axis=1` で行単位であり、ufunc を渡すなど内部で + 一括実行に落ちる経路もある。「Python の関数を要素・行・列のいずれかの単位で呼ぶ経路である + 限りは一括演算ではない」と書き換えた。この節の主題は「置き換えても実行の実体が変わらないこと + がある」点なので、pandas の API 仕様の解説には広げていない +- **ChatGPT 生ログの脚注(`issue-38-chatgpt-response.md`)**: 脚注 `[1]` のリンクテキストが + `"Cat's Cradle"`(カート・ヴォネガットの小説)になっているが、URL は catb.org で、正しい出典は + Eric S. Raymond, *The Art of Unix Programming*。**生ログは記録価値のため改変しない**方針を取り、 + ファイル冒頭に注記ブロックを、該当箇所に短い注記を追加して、検証済みの事実関係は + 「1.4 事実確認の記録」を参照するよう誘導した +- **マーケットプレイス定義の版数**: `.claude-plugin/marketplace.json` の `ndf` エントリの + `description` が `v7.0.0` / `26 focused NDF skills` のまま取り残されていた。このファイルも + `scripts/build-runtime-plugins.sh` の生成対象ではなく手で維持するファイルで、 + `validate-runtime-plugins.sh` は JSON 妥当性と `source` の実在しか見ないため CI をすり抜けていた。 + `plugins/ndf-shared/manifests/claude-skills.txt` の実数 27 に合わせ `v7.1.0` / `27 focused NDF skills` + へ更新した + +## 3.6 cross-review round 3 での事実訂正 + +- **外部化と静的解析の関係(SKILL.md「データ化の前提」/ language-notes.md)**: 初版は + 「**外部化した時点で、その対応表は静的解析の視界から外れる**」と書いていたが、これは + PHPStan の実測(`array` を渡した 3 行目)が支える範囲を超えた一般化だった。 + 実測が示すのは「型情報を伴わずに実行時ロードした場合」に限られる。スキーマから型・定数を + **生成**してビルド時に取り込めば、外部化しても静的検査は維持できる。次のとおり直した。 + - SKILL.md: 記述を「型情報を伴わずに実行時ロードすると〜」へ限定し、選択肢を + **(1) スキーマから型・定数を生成してビルド時に取り込む → (2) 生成できないならスキーマ検証で + 埋める → (3) どちらもできないなら外部化しない** の順に提示する形へ変更。生成が最良の選択肢 + なので先頭に置いた。表現は言語非依存の語(コード生成 / 型生成 / ビルド時)に留めている + - language-notes.md: PHPStan 実測表の直後に「この表が測っているのは型情報を伴わずに実行時 + ロードした場合である」と明記し、生成による静的検査の維持を補記 + - 「先に読む: この Skill が禁じていないこと」の表の括弧書きも同じ限定に揃えた +- **対象言語の不整合(プラン / language-notes.md)**: 「2.1 目的」と AC-9(c) は対象を + **Python / JavaScript / TypeScript / PHP の 4 言語**としていたのに、AC-4 と + `references/language-notes.md` は JavaScript を欠いた 3 言語のままで完了扱いになっていた。 + **スコープを縮めるのではなく JavaScript を追加する方向で揃えた**(Skill の対象読者には JS の + みで書くコードが多く、目的側の記述が本来の意図であるため)。 + - `references/language-notes.md`: 対応表に JavaScript 列を追加し、`## JavaScript` 節を新設。 + 要点は「手段は TypeScript の節と同じだが、**型注釈がないため静的な網羅性検査が効かない**」 + こと。その帰結として (a) MAY の「静的に網羅性を検査できる分岐」の条件が成立せず未知の + ケースで即時失敗させる必要があること (b) スキーマ検証の必要性が TypeScript より高いこと + を、悪い例 / 良い例 1 組とあわせて簡潔に記述した + - プラン: 1.6 の見出し・対応表・AC-4・Task 4・2.8・3.3 の言語表記を 4 言語へ揃えた + +## 3.7 cross-review round 4 での事実訂正 + +- **型生成とスキーマ検証を排他の分岐にしていた(SKILL.md「データ化の前提」/ + language-notes.md)**: round 3 で入れた 3 段の選択肢は「(1) 型・定数を生成する → + (2) **生成できないなら**スキーマ検証で埋める」と書いており、1 と 2 が排他に読めた。しかし + **型定義だけをビルド時に生成し、データ実体は実行時にロードする**構成では、型生成ができて + いてもロード境界のスキーマ検証は依然として必須である。この書き方のままでは「型を生成した + からスキーマ検証は不要」と誤読され、ロード境界で無検証のキャストを書く誘導になりうる。 + **分岐の軸を「生成できるか」から「データを実行時にロードするか」へ組み替えた。** + - SKILL.md: (1) **データごとビルド時に組み込める**なら型・定数を生成して取り込む → + (2) **データを実行時にロードする**ならロード境界をスキーマ検証で守る(型を生成していても + 同じで、**型生成は実行時のスキーマ検証の代わりにならない**)→ (3) どちらも満たせないなら + 外部化しない、へ変更。語は言語非依存(ロード境界 / 実行時ロード / 型生成)に留めた + - language-notes.md: PHP 節の「生成できないときにだけスキーマ検証で埋める」も同じ誤りを + 含んでいたため訂正し、Python / TypeScript / JavaScript / PHP の各節に「型注釈・型生成は + 実体を検査しない」ことを示す 1〜2 行の具体例(`cast` / `as` / JSDoc / `@var`)を追加した + +## 3.8 cross-review round 8 での版数・Skill 数の取り残しの解消 + +第 4 部の決定で版を v7.1.0 から v8.0.0 へ、Skill 数を 31 個から 30 個へ変えたが、 +`scripts/build-runtime-plugins.sh` の生成対象外のファイルに v7.1.0 / 旧 Skill 数が残っていた。 +round 1・round 2 で同じ 2 ファイルを同じ理由で指摘されており、3 度目の再発である。 + +直した箇所(現在値を示すべき記述のみ): + +| ファイル | 内容 | +| --- | --- | +| `.claude-plugin/marketplace.json` | `ndf` の description を `v8.0.0` / `26 focused NDF skills` へ | +| `plugins/ndf-codex/.codex-plugin/plugin.json` | `version` と description を `8.0.0` / `24 focused NDF skills` へ | +| `plugins/ndf-codex/README.md` | プラグインキャッシュのパス例 2 箇所と `codex plugin list` の出力例を `8.0.0` へ | +| `plugins/ndf-kiro/README.md` | `.kiro/agents/ndf.json` の description 例を `v8.0.0` へ | +| `plugins/ndf-shared/skills/ndf-policies/SKILL.md` | 「v7.1.0 の `ndf-policies` を参照」を v7.0.0 へ。**v7.1.0 は配布していない中間の版**であり、参照先として成立しない | +| `plugins/ndf-{claude,codex,kiro}/README.md` | 「移行先の対応表は `ndf-policies` にある」は v8.0.0 での削除により成立しなくなったため、root README の「NDF v7.0.0 の主な変更(非互換)」へ誘導 | +| `docs/specifications/ndf-skill-inventory.md` | 「Skill 数は 30 個で変わらない」を、v7.1.0 の 31 個から 30 個へ戻る旨へ訂正。予算比較表に v7.1.0(未配布)列を追加 | + +据え置いた箇所: 過去の事実を述べる記述(`ndf-policies` の `/ndf:safe-refactoring` 移行対応表、 +root README の「v6.1.0 当時の名称」注記、棚卸台帳の v6.1.0 / v7.0.0 節、`skills/README.md` の +v7.0.0 時点の実測値)と、`issues/` 配下の過去の計画文書。本節より上の round 1 / round 2 の記録も +その時点の事実として残す。`docs/presentations/2026-08-06-ai-plugins-intro.md` は日付を持つ +勉強会資料で、v6.0.0 以前の Skill 名と個数を載せたまま本 PR より前から据え置かれているため触らない。 + +再発防止として `scripts/validate-runtime-plugins.sh` に突き合わせ検査を追加した。Claude 版 +`plugin.json` の `version` を基準に、(a) Codex 版 `plugin.json` の `version`、(b) marketplace と +両 `plugin.json` の description に書かれた `(vX.Y.Z)`、(c) description の Skill 数と +`manifests/-skills.txt` の実数、の 3 つを検査する。plugin family は既存の検出結果を +使い回すため、family を足しても検査対象から漏れない。 + +## 3.9 未了 + +- [ ] 3 ランタイムでの発動実測(`docs/specifications/ndf-skill-inventory.md` への記録)。 + 配布後に利用実績が出てから測定する。台帳には「未測定」として行を追加済み +- [ ] `plan-to-spec` による確定仕様化。cross-review 通過後に実施する diff --git a/plugins/ndf-claude/.claude-plugin/plugin.json b/plugins/ndf-claude/.claude-plugin/plugin.json index 11ea4743..181c4084 100644 --- a/plugins/ndf-claude/.claude-plugin/plugin.json +++ b/plugins/ndf-claude/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", - "version": "7.0.0", - "description": "Claude Code plugin (v7.0.0): 8 specialized agents and 26 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/Gemini), transcript retention guard, and optional Slack notifications.", + "version": "8.0.0", + "description": "Claude Code plugin (v8.0.0): 8 specialized agents and 26 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, statusline, external AI delegation (Codex/Gemini), transcript retention guard, and optional Slack notifications.", "author": { "name": "takemi-ohama", "url": "https://github.com/takemi-ohama" @@ -52,7 +52,7 @@ "./skills/development-workflow", "./skills/requirements-design", "./skills/tdd-cycle", - "./skills/safe-refactoring", + "./skills/refactoring", "./skills/quality-gates" ] } diff --git a/plugins/ndf-claude/README.md b/plugins/ndf-claude/README.md index 3b7be006..1e0c202c 100644 --- a/plugins/ndf-claude/README.md +++ b/plugins/ndf-claude/README.md @@ -22,7 +22,8 @@ Skill 名は変わらないため `/playwright-` まで打てば従来どおり /plugin install playwright-kit@ai-plugins ``` -移行先の対応表は `ndf-policies` skill にあります(v8.0.0 で削除)。 +移行先の対応表は予告どおり v8.0.0 で `ndf-policies` から削除しました。リポジトリ root の +[README.md](../../README.md) の「NDF v7.0.0 の主な変更(非互換)」を参照してください。 ## 同梱内容 diff --git a/plugins/ndf-claude/skills/development-workflow/SKILL.md b/plugins/ndf-claude/skills/development-workflow/SKILL.md index 0409b880..275df320 100644 --- a/plugins/ndf-claude/skills/development-workflow/SKILL.md +++ b/plugins/ndf-claude/skills/development-workflow/SKILL.md @@ -47,7 +47,7 @@ NULL 許容列の追加)は `standard` として扱う。判定に迷う場合 ```text mode: standard 根拠: 注文確定の振る舞いを変更する。公開 API とスキーマは変えない -必須工程: requirements-design → implementation-plan → tdd-cycle → safe-refactoring(必要な場合) +必須工程: requirements-design → implementation-plan → tdd-cycle → refactoring → pr-review → quality-gates → plan-to-spec(仕様が変わった場合) ``` @@ -59,8 +59,8 @@ mode: standard | モード | 対象 | 必須工程 | | --- | --- | --- | | `light` | 文言、ドキュメント、設定、テストの追加など、本番の振る舞いも本番コードの構造も変えない局所変更 | 成功条件の確認、対象範囲の確定、限定的な検証と静的解析 | -| `standard` | 一般的な機能追加・バグ修正、テストが十分にある構造改善 | 仕様、計画、テスト駆動、構造改善(必要な場合)、レビュー、全体検証 | -| `architecture` | 公開インタフェース、移行を伴うスキーマ変更、認証、複数モジュール、重要なドメイン変更 | ドメインモデリング、設計判断の記録、設計レビュー、テスト駆動、契約テストと結合テスト、相互レビュー | +| `standard` | 一般的な機能追加・バグ修正、テストが十分にある構造改善 | 仕様、計画、テスト駆動、構造改善、レビュー、全体検証 | +| `architecture` | 公開インタフェース、移行を伴うスキーマ変更、認証、複数モジュール、重要なドメイン変更 | ドメインモデリング、設計判断の記録、設計レビュー、テスト駆動、構造改善、契約テストと結合テスト、相互レビュー | | `legacy-refactor` | テストが少ない既存コードの振る舞い維持型改善 | 構造分析、計画、現状固定テスト、段階的改善、レビュー、退行検証 | ## モードごとに起動する Skill @@ -70,8 +70,8 @@ mode: standard | 要求と受け入れ条件 | — | `requirements-design` | `requirements-design` | — | | 設計 | — | `implementation-plan` に代替案と採否を記録 | ドメインモデリングと設計レビュー(Release 2 で有効化) | `implementation-plan` に代替案と採否を記録 | | 計画 | — | `implementation-plan` | `implementation-plan` | `implementation-plan` | -| 実装 | 直接編集 | `tdd-cycle` | `tdd-cycle` | `safe-refactoring` | -| 構造改善 | — | `safe-refactoring`(必要な場合) | `safe-refactoring`(必要な場合) | `safe-refactoring` | +| 実装 | 直接編集 | `tdd-cycle` | `tdd-cycle` | `refactoring` | +| 構造改善 | — | `refactoring` | `refactoring` | `refactoring` | | レビュー | — | `pr-review` | `cross-review` | `pr-review` | | 完了判定 | `quality-gates` | `quality-gates` | `quality-gates` | `quality-gates` | | 確定仕様化 | — | `plan-to-spec`(仕様が変わった場合) | `plan-to-spec` | — | @@ -83,6 +83,13 @@ mode: standard レビュー段階は**明示的に呼ぶ**。自然文で「レビューして」と依頼すると、Claude Code では 組み込みの `code-review` が起動して判定の投稿経路が変わる。 +構造改善は**レビューと同じく、通す工程であって任意ではない**。動くコードが出た時点では整理が +済んでいないことを前提に置き、見つけたスメルは直す。対象は書き換えた行だけでなく、**その +呼び出し元・呼び出し先と、同じファイル・同じモジュールの関連箇所まで**を含む(範囲と例外は +`refactoring` の `references/code-smells.md`「手を付ける範囲」)。 + +`light` だけが工程ごと対象外である。本番コードの構造を変えない変更に構造改善の判断は要らない。 + ## 標準フロー この図は**工程の全体像**を表す。どの Skill を起動するかは前節の表が基準であり、図はその @@ -98,7 +105,8 @@ flowchart TD E --> F F --> G[実装計画] G --> H[失敗するテスト → 最小実装 → 整理] - H --> I[仕様適合レビュー] + H --> R[構造改善] + R --> I[仕様適合レビュー] I --> J[コード品質レビュー] J --> K[限定的な検証・静的解析] K --> N[全体テスト → ビルド・結合テスト] @@ -117,7 +125,8 @@ flowchart TD (K は変更箇所を 1 度実行する限定的な検証と静的解析だけを指す。依存パッケージの版更新だけは 例外として既存テスト一式を実行する — [references/workflow-modes.md](references/workflow-modes.md)) - `legacy-refactor` は A から C へ抜けて `standard` と同じ経路をたどり、**B(要求と受け入れ条件)と - M(確定仕様化)は通らない**。H は「現状固定テスト → 段階的改善」、I は「本番の振る舞いが + M(確定仕様化)は通らない**。H は「現状固定テスト」、R は「段階的改善」、I は「本番の振る舞いが + 変わっていないことの確認」として読む ## `architecture` モードの現状 diff --git a/plugins/ndf-claude/skills/development-workflow/references/workflow-modes.md b/plugins/ndf-claude/skills/development-workflow/references/workflow-modes.md index 84a29033..e9f2fc1e 100644 --- a/plugins/ndf-claude/skills/development-workflow/references/workflow-modes.md +++ b/plugins/ndf-claude/skills/development-workflow/references/workflow-modes.md @@ -67,9 +67,10 @@ - 目的: 受け入れ条件を満たし、退行を出さないこと - 受け入れ条件を先に作る。条件が作れない依頼は、質問して止まる -- 実装はテスト駆動で進める。構造改善が必要になったら差分を分ける +- 実装はテスト駆動で進める。**通したあとに構造改善の工程を通す**(手を付けない判断でもよいが、 + 工程は飛ばさない)。手を入れる場合は差分を分ける - 振る舞いを変えない構造変更のみの依頼は、受け入れ条件を「既存テストが通り続けること」とし、 - 実装は `safe-refactoring` の手順で進める + 実装は `refactoring` の手順で進める - 完了判定は全体テストまで通す ### `architecture` diff --git a/plugins/ndf-claude/skills/ndf-policies/SKILL.md b/plugins/ndf-claude/skills/ndf-policies/SKILL.md index 677d4eda..bf6de328 100644 --- a/plugins/ndf-claude/skills/ndf-policies/SKILL.md +++ b/plugins/ndf-claude/skills/ndf-policies/SKILL.md @@ -18,33 +18,18 @@ user-invocable: false 4. **マージ済みブランチには push しない。** 既存 PR の状態を確認し、マージ済みなら新ブランチ + 新 PR を作る(サフィックス `-v2`, `-v3`) 5. **revert を連鎖させない。** 最終的なあるべき状態を直接コミットする方が履歴上の意図が明確になり、後の cherry-pick も簡単になる -## v7.0.0 で移動した Skill(v8.0.0 で削除) +## v8.0.0 で改名した Skill(v9.0.0 で削除) -ブラウザ自動テストの 4 Skill を **`playwright-kit` プラグイン**へ分離した。**Skill 名は -変わらない**ため、`/playwright-` まで打てば従来どおり候補に出る。 +構造改善の Skill を **`/ndf:refactoring`** へ改名し、分岐・反復・定数の表現を決める観点を +統合した。引数と手順は変わらない。 | 旧コマンド | 移行先 | | --- | --- | -| `/ndf:playwright-planning` | `/playwright-kit:playwright-planning` | -| `/ndf:playwright-authoring` | `/playwright-kit:playwright-authoring` | -| `/ndf:playwright-evidence` | `/playwright-kit:playwright-evidence` | -| `/ndf:playwright-kit-ops` | `/playwright-kit:playwright-kit-ops` | - -利用するにはプラグインを別途インストールする。 - -```bash -# Claude Code -/plugin install playwright-kit@ai-plugins -# Codex -codex plugin add playwright-kit@ai-plugins -# Kiro CLI -bash plugins/playwright-kit-kiro/install.sh -``` - -分離したのは、Skill の `name` と `description` が起動時の一覧として常時注入され、その予算が -プラグイン横断で共有されるためである。ブラウザ自動テストは使う場面が限られる一方で MCP -ツールを多用するため frontmatter が大きく(4 個で約 2,400 文字)、全利用者へ常時注入する -取り分に見合わなかった。 - -v6.0.0 の対応表(`/ndf:review` → `/ndf:pr-review`)は、予告どおり本バージョンで削除した。 -v6.0.0 以前から移行する場合は v6.1.0 の `ndf-policies` を参照する。 +| `/ndf:safe-refactoring` | `/ndf:refactoring` | + +`safe-` を外したのは、`/refactoring` で一意に決まり、入力が短くなるためである。統合した観点は +`references/data-representation.md` にあり、スメル一覧からも参照される。 + +v7.0.0 の対応表(playwright 系 4 Skill の `playwright-kit` プラグインへの分離)は、予告どおり +本バージョンで削除した。v7.0.0 より前から移行する場合は、この対応表を持つ最後の配布版である +v7.0.0 の `ndf-policies` を参照する。 diff --git a/plugins/ndf-claude/skills/pr-review/SKILL.md b/plugins/ndf-claude/skills/pr-review/SKILL.md index fe188f1f..04e92849 100644 --- a/plugins/ndf-claude/skills/pr-review/SKILL.md +++ b/plugins/ndf-claude/skills/pr-review/SKILL.md @@ -69,7 +69,7 @@ PR 差分、または `--branch` 指定時は現在のブランチの差分を | 責務・凝集度・結合度 | 1 つの単位が複数の変更理由を持っていないか | | 依存の向き | 業務ロジックが外部の仕組みへ直接依存していないか。循環がないか | | 可読性・単純性 | 分岐の深さ、名前と実態の一致、不要な抽象化 | -| コードスメル | 重複、長すぎる単位、基本型への固執など(`safe-refactoring` の一覧) | +| コードスメル | 重複、長すぎる単位、基本型への固執など(`refactoring` の一覧) | | セキュリティ・性能 | 下の「具体的なチェックポイント」 | | テストが実装詳細に結合していないか | 内部呼び出し回数の検証、private への直接依存(`tdd-cycle` の脆いテスト) | diff --git a/plugins/ndf-claude/skills/problem-solving/SKILL.md b/plugins/ndf-claude/skills/problem-solving/SKILL.md index 80f40e7a..13aa2987 100644 --- a/plugins/ndf-claude/skills/problem-solving/SKILL.md +++ b/plugins/ndf-claude/skills/problem-solving/SKILL.md @@ -57,7 +57,7 @@ description: "Fix bugs and data inconsistencies upstream at the root cause. Use 対象にテストがほとんどない場合、再現テストを書く前に**現状の振る舞いを固定するテスト**を 置く。副作用を分離できず再現テストが書けない状態で修正すると、直したい振る舞い以外を -壊しても気づけない。手順は `safe-refactoring` の現状固定テストに従う。 +壊しても気づけない。手順は `refactoring` の現状固定テストに従う。 ```text 1. 変更対象の入口と副作用を洗い出す diff --git a/plugins/ndf-claude/skills/safe-refactoring/SKILL.md b/plugins/ndf-claude/skills/refactoring/SKILL.md similarity index 79% rename from plugins/ndf-claude/skills/safe-refactoring/SKILL.md rename to plugins/ndf-claude/skills/refactoring/SKILL.md index 55288890..4fff6d27 100644 --- a/plugins/ndf-claude/skills/safe-refactoring/SKILL.md +++ b/plugins/ndf-claude/skills/refactoring/SKILL.md @@ -1,6 +1,6 @@ --- -name: safe-refactoring -description: "Change structure without changing behavior, guarded by tests. Use when cleaning up code or touching legacy code(リファクタリング・コードスメル・現状固定テスト)." +name: refactoring +description: "Change structure without changing behavior, guarded by tests, judging smells and how decisions are represented. Use when cleaning up code or touching legacy code(リファクタリング・コードスメル・分岐をデータ化・現状固定テスト)." --- # 安全な構造改善 @@ -8,6 +8,11 @@ description: "Change structure without changing behavior, guarded by tests. Use **テストがなければ、それは構造改善ではなく単なる編集である。** 振る舞いが変わっていない ことを示す手段がない書き換えは、この Skill の対象外として扱う。 +この工程は**レビューと同じく、実装のあとに必ず通す**。動くコードが出た時点では整理が済んで +いないことを前提に置く。対象は書き換えた行だけでなく、**その呼び出し元・呼び出し先と、同じ +ファイル・同じモジュールの関連箇所まで**を含む(範囲と例外は +[references/code-smells.md](references/code-smells.md) の「手を付ける範囲」)。 + ## 最初に決める 2 つのこと ### 1. 機能変更と構造改善を混ぜない @@ -40,7 +45,9 @@ description: "Change structure without changing behavior, guarded by tests. Use 1. **変更前に既存テストを実行する。** ここで落ちているものがあれば、先に報告する 2. スメルを 1 つ選ぶ(一覧は [references/code-smells.md](references/code-smells.md)) 3. 対応する手法を選ぶ([references/refactoring-catalog.md](references/refactoring-catalog.md)。 - スメル一覧で ★ が付いた手法はカタログに項目がなく、一覧の記述だけで進めてよい) + スメル一覧で ★ が付いた手法はカタログに項目がなく、一覧の記述だけで進めてよい)。 + 分岐・反復・定数を**何にどう置き換えるか**は + [references/data-representation.md](references/data-representation.md) で決める 4. **1 手だけ適用する** 5. テストを実行する。落ちたら直前の 1 手を戻す 6. 通ったらコミットする(1 手 = 1 コミットを既定とする) @@ -109,7 +116,7 @@ flowchart TD - 固定テストが書けない(副作用が分離できない、実行に外部環境が要る) - 1 手で終わらず、テストを通すために本番コードの分岐を足す必要が出た - 改善の途中で仕様の不明点が出た(`requirements-design` へ戻る) -- 差分が依頼範囲を超えて広がった +- 差分が [code-smells.md](references/code-smells.md) の「手を付ける範囲」を超えて広がった 止めたときは「どこまで安全な状態か」を明示する。中途半端な状態を「あとで直す」前提で 残さない。 @@ -130,4 +137,11 @@ flowchart TD - [references/code-smells.md](references/code-smells.md) — 構造改善の起点になる兆候 - [references/refactoring-catalog.md](references/refactoring-catalog.md) — 手法と適用条件 +- [references/data-representation.md](references/data-representation.md) — 分岐・反復・定数を何にどう置き換えるか +- 言語ごとの手段 — **対象の言語のファイルだけを読む** + - [references/lang-python.md](references/lang-python.md) + - [references/lang-javascript.md](references/lang-javascript.md) + - [references/lang-typescript.md](references/lang-typescript.md) + - [references/lang-php.md](references/lang-php.md) + - 一覧にない言語は、`data-representation.md` の判定表から自分で対応付ける - [references/characterization-tests.md](references/characterization-tests.md) — 現状固定テストの作り方 diff --git a/plugins/ndf-claude/skills/safe-refactoring/references/characterization-tests.md b/plugins/ndf-claude/skills/refactoring/references/characterization-tests.md similarity index 100% rename from plugins/ndf-claude/skills/safe-refactoring/references/characterization-tests.md rename to plugins/ndf-claude/skills/refactoring/references/characterization-tests.md diff --git a/plugins/ndf-kiro/skills/safe-refactoring/references/code-smells.md b/plugins/ndf-claude/skills/refactoring/references/code-smells.md similarity index 59% rename from plugins/ndf-kiro/skills/safe-refactoring/references/code-smells.md rename to plugins/ndf-claude/skills/refactoring/references/code-smells.md index 325f04ad..ef77a7e5 100644 --- a/plugins/ndf-kiro/skills/safe-refactoring/references/code-smells.md +++ b/plugins/ndf-claude/skills/refactoring/references/code-smells.md @@ -1,7 +1,7 @@ # コードスメル -スメルは「直すべき欠陥」ではなく **調べる価値がある兆候** である。見つけても、変更の -理由(機能追加・不具合修正・読みにくさ)が伴っていなければ手を付けない。 +スメルは「直すべき欠陥」ではなく **調べる価値がある兆候** である。兆候を見つけたら、下の +「手を付ける範囲」に入っているかを確かめ、入っていれば直す。 行数などの数値規則は使わない。判断は凝集度・結合度・変更理由・認知負荷・テスト容易性で行う。 @@ -26,10 +26,16 @@ | 例外の飲み込み | 捕まえて何もしない、ログだけ出して続行する | 失敗が沈黙し、原因が追えない | ★呼び出し元へ伝える、または明示的に扱う | | 条件分岐の連鎖 | 型・状態ごとの分岐が複数箇所で繰り返される | 種類を増やすたびに全箇所を直す | 多態による分岐の置き換え | | 設定の散在 | 同じ設定値が複数の場所で定義される | どちらが効くか分からない | ★定義を 1 箇所へ寄せる | +| 業務ルールの埋め込み | 料率・区分・しきい値・優先順位が制御構文の中に書かれている | 一覧できない。値を変えるだけの修正にコード変更と再配備が要る | 対応表への置き換え | +| 一件ずつの反復 | 同種で独立した処理を 1 件ずつ繰り返す(1 件ごとの問い合わせを含む) | 往復回数か実行時間が件数に比例して積み上がる | 一括処理への置き換え | +| 検証のない外部化 | 設定・マスタへ出したデータに、スキーマ・版・検証がない | 壊れた値が実行時まで通る。どの版が適用されたか追えない | ★スキーマと版を与え、読み込み境界で検証する | -★ の 4 件をカタログに置いていないのは、手順が「1 箇所へ寄せる/呼び出し元へ返す」で尽きて -おり、適用条件も上の「何が問題か」以外に無いためである。カタログを探さず、この表の記述で -そのまま進めてよい。ただし共通化だけは判断を誤りやすいため、次節の見分け方に従う。 +★ の 5 件をカタログに置いていないのは、手順が「1 箇所へ寄せる/呼び出し元へ返す/検証を足す」 +で尽きており、適用条件も上の「何が問題か」以外に無いためである。カタログを探さず、この表の +記述でそのまま進めてよい。ただし共通化だけは判断を誤りやすいため、次節の見分け方に従う。 + +下 3 件は「どう持つか」の判断を伴う。表現の選び方と、外部化してよい条件は +[data-representation.md](data-representation.md) にある。 ## 共通化してよい重複の見分け方 @@ -61,11 +67,33 @@ 「何を解決したか」と「外し方」を残す。決まった書式は今のところ無い(Release 2 で追加予定の `object-design` Skill で扱う想定だが、現時点では未実装)。 -## スメルに手を付けない場合 +## 手を付ける範囲 + +**今回書き換えた行だけに閉じない。** そこに閉じると、読みにくい領域は読みにくいまま残り続ける。 +一方で無制限に広げると差分がレビューできなくなる。次を境界にする。 + +| 範囲 | 扱い | +| --- | --- | +| 今回変更した関数・クラス | 直す | +| その呼び出し元・呼び出し先 | 直す。変更の影響を読むために通る範囲であり、テストも通っている | +| 同じファイル・同じモジュールの関連箇所 | 直す。差分は分ける | +| そこから遠い領域 | 対象外。後続の作業として記録する | + +範囲に入っていることは、直す理由にはならない。**この文書のスメル一覧のどれにも当たらない箇所は +直さない。**「読みやすくなりそう」だけで手を入れると、レビューできない差分になる。手順(1 手ずつ・ +現状固定テストで守る・差分を分ける)は、範囲を広げても変わらない。 + +広げた分は**別のコミットに切る**(「差分の切り方」)。機能変更と構造改善が同じ差分に入ると、 +レビュアーは意図した変更と構造改善の事故を区別できない。上表の範囲を超えて広がるなら、 +別の変更として出す。 + +## 手を付けない場合 + +範囲に入っていても、次は対象から外す。 -- 変更予定のない領域(読みにくいだけで、今回の変更に関係しない) - 生成物・外部から取り込んだコード(上流で直す) - 削除予定の領域 +- 振る舞いが変わっていないことを示す手段がなく、現状固定テストも書けない箇所 + (`SKILL.md` の「途中で止める条件」) -手を付けないと決めた場合、指摘だけを残すよりも**何もしない**方がよい。残す価値がある -指摘は、後続の作業として記録する。 +外したものは、指摘だけを残さず**後続の作業として記録する**。 diff --git a/plugins/ndf-claude/skills/refactoring/references/data-representation.md b/plugins/ndf-claude/skills/refactoring/references/data-representation.md new file mode 100644 index 00000000..213f15bf --- /dev/null +++ b/plugins/ndf-claude/skills/refactoring/references/data-representation.md @@ -0,0 +1,186 @@ +# 分岐・反復・定数の表現 + +`code-smells.md` の「業務ルールの埋め込み」「一件ずつの反復」「検証のない外部化」を見つけた +ときに、**何にどう置き換えるか**を決めるための判断材料。 + +置き換えの目的は 2 つに尽きる。どちらにも近づかない置き換えは行わない。 + +| 時点 | 近づける状態 | +| --- | --- | +| 実行前 | どの入力がどの結果になるかを実装を読まずに一覧でき、扱っていない入力があれば検査で分かる | +| 実行後 | どの判断がなぜその結果になったかを、記録だけで再現できる | + +## 移す対象を決める + +> **変化する知識はデータへ。安定した機構と不変条件はコードへ。** + +判断の軸は「分岐が多いか」ではなく「**その知識が変化するか**」である。変化しない知識をデータへ +出すと、検査できる場所が減るだけになる。 + +| データへ移す | コードに残す | +| --- | --- | +| 業務ルール、料率、しきい値、優先順位 | 不変条件、安全性の制約 | +| 分類・対応表(値から値への写像) | 機構(どう読み、どう書き、どう失敗を扱うか) | +| 環境ごとに変わる値 | 型・スキーマ・境界の定義 | +| 利用者が管理するカテゴリ | プロトコル、閉じた状態集合 | + +## 手を付けないもの + +次はいずれも**そのままでよい**。スメルとして拾わない。 + +- 単純なガード節の条件分岐(早期リターンによる前提条件の検査) +- 閉じた型に対する網羅的な分岐(静的に網羅性を検査できるもの) +- 逐次依存・早期終了・ストリーム処理・メモリ制約下の明示的なループ +- 不変条件・プロトコル・閉じた状態集合を表す定数と列挙型 +- 原子性・整合性・安全性を守るための即時打ち切りと巻き戻し。全体を 1 単位として成否を決める + 処理、後続に不正な入力が波及する処理、安全性検査の失敗はこれにあたる + +## 改善にならない置き換え + +形だけが変わり、上の 2 つの状態に近づかないもの。着手する前に見分ける。 + +| 置き換え | 何が起きるか | +| --- | --- | +| 条件分岐を機械的に対応表へ移す | 前提条件の検査は、対応表より分岐のほうが意図が明確。網羅性を検査できていた分岐を型情報のない対応表へ移すと、その検査が消える | +| 逐次実行のまま高階反復へ書き換える | 実行の実体は変わらない。一括実行にも並列実行にもなっていない | +| 定数をすべて外部設定へ出す | 条件・参照・既定値が設定側に積み上がり、設定自体が独自の言語になる。変更に必要な知識はむしろ増える | + +## 分岐の種類 → 適切な表現 + +| 分岐の種類 | 適切な表現 | +| --- | --- | +| 値から値への単純な対応 | 対応表(写像) | +| 処理方式の切り替え | 登録表(識別子から処理への対応) | +| 条件の組み合わせが多い業務判断 | 決定表、ルールエンジン | +| 認可・制約・組織ポリシー | ポリシーエンジン | +| 時系列で状態が変化する処理 | 状態機械 | +| 閉じた少数の型分岐 | 型に対する網羅的な分岐(置き換えない) | +| 前提条件の検査 | ガード節(置き換えない) | + +## 反復の実行方式 → 得られるもの + +置き換えても何も得られない場合を見分けるための表である。分類の軸は構文ではなく**実行方式**で +ある。 + +| 実行方式 | 実行の実体 | 得られるもの | +| --- | --- | --- | +| 逐次実行 | 1 件ずつ順に処理する | **なし**(記述の変更のみ) | +| 一括演算 | 基盤側で一括実行 | 実行時間 | +| 一括入出力 | 呼び出し回数の削減 | 往復回数(大量データでは最大の効果) | +| 並行処理 | 待ち時間の重ね合わせ | 待ち時間。計算時間は減らない | +| 並列処理 | 複数の実行資源で同時 | 計算時間 | +| 分散データフロー | 基盤が分割・再試行・集約 | 規模と耐障害性 | + +高階反復は構文であって実行方式ではない。**逐次実行のまま構文だけを高階反復へ置き換えても、この +表のどの行にも移動していない。** 高階反復の形のまま遅延・並行・並列・分散で実行する仕組みを持つ +言語もあるが、得られるものを決めるのはその実行方式であって構文ではない。**移すなら、移動先の行 +を言えなければならない。** 置き換えたら計測し、効果が出ていなければ戻す。 + +一括演算の基盤は言語によって有無が異なる。基盤がない言語では、一括入出力(往復回数の削減)が +主戦場になる。 + +## 定数の性質 → 置き場所 + +| 値の性質 | 置き場所 | +| --- | --- | +| 数学的・技術的な不変条件 | コード内の定数 | +| 閉じた状態集合・プロトコル | 型、列挙型、スキーマ | +| 頻繁に変わる業務ルール | 決定表、ポリシー、設定 | +| 環境ごとに変わる値 | 配備設定、環境変数 | +| 利用者が管理するカテゴリ | 参照テーブル(データベース) | +| 表示名・文言 | 多言語化資源、コンテンツ | +| 安全性の絶対制約 | コード・型・スキーマ・ポリシーの複数層 | + +不透明なコード値(`status == 3`)は避ける。ただし**意味の分かる識別子を持つ列挙型まで置き換え +ない**。型のない文字列にすると、綴り誤りと未処理ケースの発見が実行時まで遅れる。 + +## 外部化してよい条件 + +データへ移すなら、移した先で次を用意できることが条件になる。用意できないなら、コードに残した +ほうが検査できる範囲は広い。 + +- [ ] スキーマがある +- [ ] 型またはバリデーションで検査される +- [ ] 版を持ち、変更履歴が追える +- [ ] 競合するルール・到達不能なルールを検出できる +- [ ] テストがある +- [ ] 変更の適用手順(移行)がある +- [ ] 誰が何を変更したか監査できる +- [ ] 実行時に、適用したルールの識別子と理由を記録する + +外部化したデータをどう守るかは、**そのデータをいつ読むか**で決まる。 + +1. **データごとビルド時に組み込める**なら、スキーマから型・定数を生成して取り込む。外部化しても + 静的な検査が残るため、これが選べるなら最初に選ぶ +2. **データを実行時にロードする**なら、**ロード境界をスキーマ検証で守る**。型を生成していても + 同じで、生成した型は、ロードした値がその形である保証を与えない +3. どちらも満たせないなら、外部化しない + +型情報を伴わずに実行時ロードした対応表は、静的解析の対象から外れる。どの検査がどこまで効くかは +言語によって違う。**対象の言語のファイルだけを読む** — +[lang-python.md](lang-python.md) / [lang-javascript.md](lang-javascript.md) / +[lang-typescript.md](lang-typescript.md) / [lang-php.md](lang-php.md)。 + +## 判断を記録できるようにする + +対応表への置き換えは、**どのルールが適用されたかを記録できる構造を保って完了**である。値を +外へ出した分だけ「なぜこの結果になったか」が追いにくくなるので、表から識別子と版を取り出せる +形を崩さない。 + +記録を出す要件があるなら、1 つの判断につき 1 件、入力・決定・理由・実行文脈をまとめた +構造化イベントを出す。 + +```json +{ + "event_name": "discount_decided", + "customer_id": "cus_123", + "input": { "rank": "gold", "purchase_amount": 120000 }, + "decision": { + "discount_rate": 0.2, + "rule_id": "customer-rank-discount", + "rule_version": "2026-08-01", + "reason": "customer rank is gold" + }, + "execution": { "code_version": "a13fd82", "duration_ms": 4, "trace_id": "..." } +} +``` + +- 属性は、絞り込み・グループ化・集計・相関に使える形にする +- 適用したルールの識別子と版を含めると、結果に至った経緯を後から再現できる +- 個人情報と秘密情報の扱いは `logging-guidelines` に従う + +**記録の追加は振る舞いの変更である。** 構造改善と同じ差分に混ぜず、コミットを分ける。 + +一括処理への置き換えでは、終了時に失敗の集計も出す。 + +```json +{ + "event_name": "import_completed", + "total": 12000, "succeeded": 11940, "failed": 60, + "failures_by_reason": { "invalid_date": 48, "missing_customer": 12 }, + "sample_failed_ids": ["row_88", "row_312", "row_940"] +} +``` + +項目が独立に処理できるのに最初の失敗で全体を止めると、どこまで進んだかが分からなくなる。 +全体を 1 単位として成否を決める処理では、打ち切って巻き戻すのが正しい。その場合は**打ち切った +位置・理由・巻き戻しの範囲**を同じ構造化イベントに含める。 + +## 例: 業務ルールの埋め込みを置き換える + +```text +❌ 変化する料率が制御構文へ埋まっている + if rank == "gold" -> rate = 0.2 + else if rank == "silver" -> rate = 0.1 + else -> rate = 0 + +✅ 対応表として持ち、既定と未知の扱いを明示する + rates = { gold: 0.2, silver: 0.1, bronze: 0.0 } # 版とスキーマを持つ + rate = rates[rank](未知の階級は失敗として扱い、握りつぶさない) +``` + +置き換えた結果、次が言えるようになっていなければ手を付けた意味がない。 + +- 料率の一覧を**コードを読まずに**確認できる +- 新しい階級の追加が、分岐の追加ではなくデータの追加になる +- どの階級にどの版の料率が適用されたか、記録から再現できる diff --git a/plugins/ndf-claude/skills/refactoring/references/lang-javascript.md b/plugins/ndf-claude/skills/refactoring/references/lang-javascript.md new file mode 100644 index 00000000..2c4c0b66 --- /dev/null +++ b/plugins/ndf-claude/skills/refactoring/references/lang-javascript.md @@ -0,0 +1,45 @@ +# JavaScript での手段 + +[data-representation.md](data-representation.md) で選んだ表現を、JavaScript の機能へ対応付ける。 + +| 判定 | 手段 | +| --- | --- | +| 値から値への対応 | `Object.freeze` のオブジェクト | +| 処理方式の切り替え | 関数を値に持つオブジェクト | +| 閉じた状態集合 | 凍結した定数オブジェクト | +| 網羅性の静的検査 | **手段なし**(実行時に失敗させる) | +| 不変性 | `Object.freeze`(凍結はトップレベルのみ)。入れ子まで不変にするなら再帰的に凍結するか、構造共有の不変データ構造を使う | +| スキーマ検証 | zod / JSON Schema | +| 一括処理 | 一括 API / `Promise.all`(並行) | +| 失敗の集計 | 失敗を集めて返す | + +## 静的な網羅性検査が効かないことが判定を変える + +型注釈がないため、**閉じた状態集合に対する分岐の網羅性を実行前に検査できない**。これが +2 つの判定を変える。 + +- 「手を付けないもの」にある「閉じた型に対する網羅的な分岐(静的に網羅性を検査できるもの)」 + の条件が成立しない。分岐をそのまま残しても追加漏れは実行時まで分からないので、**未知の + ケースで必ず失敗させる**書き方を併せて用意する +- 外部化した対応表を守る手段が実行時のスキーマ検証しかない。**静的検査で埋め合わせができない + 分、スキーマ検証の必要性が高い** + +```javascript +// ❌ 未知のケースが undefined として下流へ流れ、どこで壊れたか分からなくなる +const RATES = { gold: 0.2, silver: 0.1 }; +return RATES[rank]; + +// ✅ 未知のケースをその場で失敗させる +const RATES = Object.freeze({ gold: 0.2, silver: 0.1, bronze: 0 }); +if (!Object.hasOwn(RATES, rank)) throw new Error(`unknown rank: ${rank}`); +return RATES[rank]; +``` + +JSDoc の型注釈と `checkJs` を入れられるなら、静的検査が使える状態に戻せる。その場合の書き方は +[lang-typescript.md](lang-typescript.md) を読む。ただし実行時にロードするならロード境界の +検証は同じく要る(JSDoc の型は実体を検査しない)。 + +## 並行と並列を混同しない + +`Promise.all` は待ち時間を重ねるだけで、計算時間は減らない。CPU を使う処理を並列化するには +worker が要る。「反復の実行方式」表の「並行処理」と「並列処理」は別の行である。 diff --git a/plugins/ndf-claude/skills/refactoring/references/lang-php.md b/plugins/ndf-claude/skills/refactoring/references/lang-php.md new file mode 100644 index 00000000..7b5c958f --- /dev/null +++ b/plugins/ndf-claude/skills/refactoring/references/lang-php.md @@ -0,0 +1,146 @@ +# PHP での手段 + +[data-representation.md](data-representation.md) で選んだ表現を、PHP の機能へ対応付ける。 +以下は **8.1 以降**を前提とする(8.0 以下は最終節)。 + +| 判定 | 手段 | +| --- | --- | +| 値から値への対応 | 連想配列 / `match` | +| 処理方式の切り替え | first-class callable 構文 `foo(...)` | +| 閉じた状態集合 | backed enum | +| 網羅性の静的検査 | PHPStan `match.unhandled` | +| 不変性 | `readonly` プロパティ | +| スキーマ検証 | JSON Schema / Valinor | +| 一括処理 | **一括入出力**(標準に一括演算の基盤はない) | +| 失敗の集計 | 失敗を集めて返す | + +8.1 で backed enum・`readonly` プロパティ・first-class callable 構文が入り、**データ化しても +静的解析が効く**範囲が広がった。 + +## 網羅性の静的検査 + +```php +enum Rank: string { + case Gold = 'gold'; + case Silver = 'silver'; + case Bronze = 'bronze'; +} + +function label(Rank $rank): string { + return match ($rank) { + Rank::Gold => 'ゴールド', + Rank::Silver => 'シルバー', + Rank::Bronze => 'ブロンズ', + }; +} +``` + +`match` は一致する腕がなければ実行時に `\UnhandledMatchError` を投げる。さらに **PHPStan が +`match.unhandled`("Match expression does not handle remaining value")として静的に検出する**。 +`Rank` にケースを足すと、`default` を書いていない `match` が解析で落ちる。 + +```php +// ❌ 連想配列へ移すと、検査は弱くなる +$labels = ['gold' => 'ゴールド', 'silver' => 'シルバー']; +return $labels[$rank->value]; +``` + +どこまで弱くなるかは、**対応表が静的に見えているか**で決まる(PHPStan 2.2 で実測)。 + +| 書き方 | level 5 | level max | +| --- | --- | --- | +| `match`(`default` なし) | `match.unhandled` で検出 | 同左 | +| その場に書いた連想配列 | 検出なし | `offsetAccess.notFound` で検出 | +| 外部から渡した対応表(`array`) | 検出なし | **検出なし** | + +**この表が測っているのは、型情報を伴わずに実行時ロードした場合である**(3 行目は +`array` という幅の広い型で対応表を受け取る形)。この形にすると、変化する業務 +ルールは定義ごと静的解析の視界から外れるため、「外部化してよい条件」(スキーマ・ +バリデーション・版・テスト)が必須になる。**静的検査で守れなくなった分を、スキーマ検証で +埋める。** + +一方、スキーマから backed enum や定数クラスを**生成**してビルド時に取り込めば、外部化しても +`match.unhandled` の検査は残る。ただし**データを実行時にロードするなら、型を生成していても +ロード境界の検証は要る**。`json_decode` の戻り値に `@var GeneratedShape` を付けるだけでは、 +静的解析が信じるだけで実体は検査されない。`Valinor` などのマッパか JSON Schema 検証を通す。 + +値が固定で外部化する理由がないなら、`match` のまま置くほうが分析可能性は高い。 + +`default` を書くとケース追加漏れが検出されなくなる。**閉じた列挙に対する `match` に +`default` を置かない**(未知の入力を扱う必要があるなら、それは閉じた列挙ではない)。 + +## 処理方式の切り替え + +```php +// first-class callable 構文。静的解析が参照先を追える +$handlers = [ + 'csv' => $this->importCsv(...), + 'json' => $this->importJson(...), +]; +($handlers[$format] ?? throw new UnsupportedFormat($format))($payload); +``` + +文字列やコールバック配列(`'importCsv'` / `[$this, 'importCsv']`)で書くと解析が追えない。 +**データ化しても分析可能性を落とさない書き方を選ぶ。** + +## 一括処理は入出力の問題である + +**PHP 標準に一括演算の基盤はない。** `array_map` は逐次実行の高階反復であり、「反復の実行方式」 +表のどの行にも移動しない。`array_column` はコールバックを取らない組み込みの列抽出で、走査は逐次 +のままだが PHP レベルのループとは実装が異なるため、**計測して選ぶ**。数値計算向けの拡張や +ライブラリを導入すれば一括演算に移れる場合はあるので、採用するなら**効果を計測してから決める**。 + +```php +// ❌ ループを array_map に変えても、実行の実体は変わらない +$totals = array_map(fn($o) => $o->amount * 1.08, $orders); +``` + +PHP で効くのは**一括入出力**、すなわち往復回数の削減である。 + +```php +// ❌ N+1。1 行ごとに問い合わせる +foreach ($orderIds as $id) { $rows[] = $repo->find($id); } + +// ✅ 一括取得。往復が 1 回になる +$rows = $repo->findByIds($orderIds); + +// ✅ 一括挿入。件数に比例した往復をなくす +$repo->insertMany($rows); // 大量件数は chunk して分割する +``` + +大量データを扱うときは、`yield` による逐次生成でメモリを一定に保つ。これは「逐次依存・ +メモリ制約下の明示的なループ」にあたり、**そのままでよい**。 + +## 不変性 + +```php +final class Discount { + public function __construct( + public readonly string $ruleId, + public readonly string $ruleVersion, + public readonly float $rate, + ) {} +} +``` + +判断の結果を `readonly` の値として持つと、記録に必要なルールの識別子と版を持ち回れる。 + +## 8.0 以下 + +backed enum・`readonly`・first-class callable 構文がいずれも使えない。代替は次のとおりで、 +**いずれも静的検査は弱くなる**。 + +| 8.1+ | 8.0 以下の代替 | +| --- | --- | +| backed enum | クラス定数 + 値オブジェクト | +| `match` の網羅性検査 | `switch` + `default` で例外を投げる(実行時検出のみ) | +| `readonly` プロパティ | `private` + getter のみ | +| `foo(...)` | `Closure::fromCallable('foo')` | + +閉じた状態集合は 8.1 以降と同じくコード側に置く。変わるのは**外部化した業務ルールの守り方**で、 +生成した定数クラスに対する網羅性検査が効かないぶん、実行時のスキーマ検証への依存が高くなる。 + +## 出典 + +- [PHP 8.1 リリースアナウンス](https://www.php.net/releases/8.1/en.php) +- [PHPStan `match.unhandled`](https://phpstan.org/error-identifiers/match.unhandled) diff --git a/plugins/ndf-claude/skills/refactoring/references/lang-python.md b/plugins/ndf-claude/skills/refactoring/references/lang-python.md new file mode 100644 index 00000000..b08108d9 --- /dev/null +++ b/plugins/ndf-claude/skills/refactoring/references/lang-python.md @@ -0,0 +1,85 @@ +# Python での手段 + +[data-representation.md](data-representation.md) で選んだ表現を、Python の機能へ対応付ける。 + +| 判定 | 手段 | +| --- | --- | +| 値から値への対応 | `dict` / `Mapping` | +| 処理方式の切り替え | 関数を値に持つ `dict` | +| 閉じた状態集合 | `Enum` / `Literal` | +| 網羅性の静的検査 | mypy + `assert_never` | +| 不変性 | `@dataclass(frozen=True)` | +| スキーマ検証 | pydantic / jsonschema | +| 一括処理 | NumPy / pandas の**ベクトル化演算**(一括演算) | +| 失敗の集計 | 失敗を集めて返す | + +## 網羅性の静的検査 + +```python +from enum import Enum +from typing import assert_never # 3.11+(それ以前は typing_extensions) + +class Rank(Enum): + GOLD = "gold" + SILVER = "silver" + BRONZE = "bronze" + +def label(rank: Rank) -> str: + match rank: + case Rank.GOLD: return "ゴールド" + case Rank.SILVER: return "シルバー" + case Rank.BRONZE: return "ブロンズ" + case _: assert_never(rank) # ケース追加漏れを mypy が検出 +``` + +`Rank` に階級を足すと mypy が `assert_never` の行で型エラーを出す。**この検査が効いている +分岐は、`dict` へ移さない。** + +外部化した値を実行時にロードするなら、型注釈を用意していてもロード境界の検証は要る。 +`cast(Rates, json.load(f))` は mypy を黙らせるだけで実体を検査しないので、`pydantic` の +`TypeAdapter` や `jsonschema` を通してから使う。 + +## 一括演算とその反例 + +Python は一括演算の基盤(NumPy)を持つ数少ない言語である。ただし**「ループを消したこと」と +「一括演算になったこと」は別**である。 + +```python +# ❌ 逐次。要素ごとに Python のバイトコードを実行する +result = [x * 1.08 for x in prices] + +# ❌ 高階反復。上と実行の実体は変わらない +result = list(map(lambda x: x * 1.08, prices)) + +# ❌ np.vectorize も同じ。公式が明言している +# "provided primarily for convenience, not for performance. +# The implementation is essentially a for loop." +result = np.vectorize(lambda x: x * 1.08)(prices) + +# ✅ 一括演算。C 側で一括実行される +result = prices * 1.08 +``` + +pandas の `.apply()` も、Python の関数を要素・行・列のいずれかの単位で呼ぶ経路である限りは +一括演算ではない(ufunc を渡すなど、内部で一括実行に落ちる経路もある)。**置き換えたら +計測する。** 速くならないなら、「反復の実行方式」表の行を移動できていない。 + +## 失敗の集計 + +```python +def import_rows(rows): + ok, failures = [], [] + for i, row in enumerate(rows): + try: + ok.append(parse(row)) + except ValueError as e: + failures.append({"index": i, "reason": type(e).__name__, "id": row.get("id")}) + return ok, failures # 呼び出し側が件数・種類・対象を報告できる +``` + +例外を握りつぶさず、最初の失敗で打ち切らない。ここは明示的なループでよい(逐次依存では +ないが、失敗の収集がある)。 + +## 出典 + +- [numpy.vectorize — 性能目的ではないという公式注記](https://numpy.org/doc/stable/reference/generated/numpy.vectorize.html) diff --git a/plugins/ndf-claude/skills/refactoring/references/lang-typescript.md b/plugins/ndf-claude/skills/refactoring/references/lang-typescript.md new file mode 100644 index 00000000..6759eaae --- /dev/null +++ b/plugins/ndf-claude/skills/refactoring/references/lang-typescript.md @@ -0,0 +1,73 @@ +# TypeScript での手段 + +[data-representation.md](data-representation.md) で選んだ表現を、TypeScript の機能へ対応付ける。 +型注釈を持たない JavaScript は [lang-javascript.md](lang-javascript.md) を読む。 + +| 判定 | 手段 | +| --- | --- | +| 値から値への対応 | `Record` + `as const` | +| 処理方式の切り替え | ハンドラの `Record` | +| 閉じた状態集合 | 判別可能ユニオン | +| 網羅性の静的検査 | `never` への代入 | +| 不変性 | `readonly` / `as const` | +| スキーマ検証 | zod / JSON Schema | +| 一括処理 | 一括 API / `Promise.all`(並行) | +| 失敗の集計 | 結果型に集約 | + +## 網羅性の静的検査 + +```typescript +type Shape = + | { kind: "circle"; r: number } + | { kind: "rect"; w: number; h: number }; + +function area(s: Shape): number { + switch (s.kind) { + case "circle": return Math.PI * s.r ** 2; + case "rect": return s.w * s.h; + default: { + const _exhaustive: never = s; // ケース追加漏れをコンパイル時に検出 + return _exhaustive; + } + } +} +``` + +```typescript +// ❌ 対応表へ移すと、この検査が消える +const handlers: Record number> = { circle: ..., rect: ... }; +``` + +`Record` は任意の文字列を受けるため、ケースの追加漏れも綴り誤りも実行時まで +分からない。**判別可能ユニオンに対する分岐は、そのまま残す。** + +## 変化する値の対応表 + +一方、**変化する業務ルール**は対応表が適する。キーを閉じた型に固定すれば静的検査も残る。 + +```typescript +const DISCOUNT_RATES = { + gold: 0.2, silver: 0.1, bronze: 0, +} as const satisfies Record; // Rank に追加すると欠落を検出 + +const rate = DISCOUNT_RATES[rank]; +``` + +この表を実行時にロードするなら、型を生成していてもロード境界の検証は要る。 + +```typescript +// ❌ 型生成をスキーマ検証の代わりにしている。実体は何も検査されない +const rates = JSON.parse(raw) as Record; + +// ✅ ロード境界で検証してから使う(zod / JSON Schema) +const rates = RatesSchema.parse(JSON.parse(raw)); +``` + +## 並行と並列を混同しない + +`Promise.all` は待ち時間を重ねるだけで、計算時間は減らない。CPU を使う処理を並列化するには +worker が要る。「反復の実行方式」表の「並行処理」と「並列処理」は別の行である。 + +## 出典 + +- [TypeScript Handbook — Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) diff --git a/plugins/ndf-kiro/skills/safe-refactoring/references/refactoring-catalog.md b/plugins/ndf-claude/skills/refactoring/references/refactoring-catalog.md similarity index 73% rename from plugins/ndf-kiro/skills/safe-refactoring/references/refactoring-catalog.md rename to plugins/ndf-claude/skills/refactoring/references/refactoring-catalog.md index 6c3e5741..412a039b 100644 --- a/plugins/ndf-kiro/skills/safe-refactoring/references/refactoring-catalog.md +++ b/plugins/ndf-claude/skills/refactoring/references/refactoring-catalog.md @@ -57,6 +57,32 @@ 分岐が 1 箇所なら分岐のままが読みやすい。**種類が増えるたびに複数箇所を直している**という 事実が、この手法の適用条件である。 +## 対応表への置き換え + +| 項目 | 内容 | +| --- | --- | +| 適用条件 | **変化する業務ルール**(料率・区分・しきい値・優先順位)が制御構文に埋まっている | +| 手順 | 値の対応を表として外へ出す → 未知の入力を失敗として扱う → 適用したルールの識別子と版を**表から取り出せる形にする** | +| やめる条件 | 値が変化しない。網羅性を静的に検査できている分岐である。表に版・スキーマ・検証を用意できない | + +「分岐が多いから表にする」ではない。**変化するから表にする**。判断の材料は +[data-representation.md](data-representation.md) の 3 表にある。 + +この手順で完了するのは、識別子と版を**記録できるデータ構造**を保つところまでである。実際に +記録を出す実装は振る舞いの変更なので、要件がある場合に別の変更として出す +([data-representation.md](data-representation.md) の「判断を記録できるようにする」)。 + +## 一括処理への置き換え + +| 項目 | 内容 | +| --- | --- | +| 適用条件 | 同種で独立した処理を 1 件ずつ繰り返しており、往復回数か実行時間が積み上がっている | +| 手順 | 一括入出力・一括演算・並行・並列のどれに移すかを決める → 置き換える → **計測して効果を確かめる** | +| やめる条件 | 逐次依存・早期終了・メモリ制約がある。移動先を言えない(高階反復への書き換えだけになる) | + +逐次実行のまま高階反復へ書き換えても実行の実体は変わらない。何が得られるかは +[data-representation.md](data-representation.md) の「反復の実行方式」表で確かめる。 + ## 戦略の切り出し | 項目 | 内容 | diff --git a/plugins/ndf-claude/skills/tdd-cycle/SKILL.md b/plugins/ndf-claude/skills/tdd-cycle/SKILL.md index e16ddd9d..249c224b 100644 --- a/plugins/ndf-claude/skills/tdd-cycle/SKILL.md +++ b/plugins/ndf-claude/skills/tdd-cycle/SKILL.md @@ -16,7 +16,7 @@ description: "Write a failing test first, then the smallest implementation that | Skill | 参照している内容 | 未追加のあいだの代替 | | --- | --- | --- | | `requirements-design` | 受け入れ条件の作り方 | 受け入れ条件を「観測可能・一意・テスト可能」な 1 文へ自分で書き下す | -| `safe-refactoring` | 構造改善と現状固定テスト | サイクル内の整理にとどめ、構造改善は別タスクへ切り出す | +| `refactoring` | 構造改善と現状固定テスト | サイクル内の整理にとどめ、構造改善は別タスクへ切り出す | | `quality-gates` | 全体テストの実行とカバレッジ閾値の判定 | 対象プロジェクトのカバレッジツール設定に従い、設定がなければ測定値の記録だけ行う | ## 適用しない対象 @@ -97,7 +97,7 @@ E ImportError: cannot import name 'validate' ← 期待と違う。先にこ ### 4. 整理する テストを**通ったまま**保って構造を整える。整理中にテストが落ちたら、整理をいったん戻す。 -コードスメル起点の本格的な構造改善は `safe-refactoring`※ に委ねる。 +コードスメル起点の本格的な構造改善は `refactoring`※ に委ねる。 ### 5. 次の条件へ進む @@ -123,7 +123,7 @@ E ImportError: cannot import name 'validate' ← 期待と違う。先にこ ## テストの乏しい既存コードでは順序が変わる 変更対象にテストがほとんどない場合、いきなり新しいテストを足すより、**現状の振る舞いを -固定するテスト**を先に置く。手順は `safe-refactoring`※ の現状固定テストに従う。 +固定するテスト**を先に置く。手順は `refactoring`※ の現状固定テストに従う。 ## テストダブルの優先順 diff --git a/plugins/ndf-codex/.codex-plugin/plugin.json b/plugins/ndf-codex/.codex-plugin/plugin.json index d4b30069..ee785b4a 100644 --- a/plugins/ndf-codex/.codex-plugin/plugin.json +++ b/plugins/ndf-codex/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ndf", - "version": "7.0.0", - "description": "Codex plugin (v7.0.0): 24 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation (Codex/Gemini), and optional Slack completion notifications.", + "version": "8.0.0", + "description": "Codex plugin (v8.0.0): 24 focused NDF skills for PR/review workflows, cross-review, implementation planning, plan-to-spec, Docker container access, external AI delegation (Codex/Gemini), and optional Slack completion notifications.", "skills": "./skills/", "hooks": "./hooks/hooks.json" } diff --git a/plugins/ndf-codex/README.md b/plugins/ndf-codex/README.md index 77f7d824..d6a67662 100644 --- a/plugins/ndf-codex/README.md +++ b/plugins/ndf-codex/README.md @@ -2,7 +2,7 @@ Codex CLI 向けの NDF プラグインです。PR 運用、レビュー、cross-review、実装計画、仕様書化、開発方法論(要求定義・テスト駆動・構造改善・完了判定)、Docker container access、GitHub 操作補助などの Codex 用 skills と、Codex 終了時の任意 Slack 通知 hook を提供します。 -Playwright による E2E テストは v7.0.0 で **`playwright-kit` プラグイン**へ分離しました。`codex plugin add playwright-kit@ai-plugins` で導入してください(移行先は `ndf-policies` の対応表)。 +Playwright による E2E テストは v7.0.0 で **`playwright-kit` プラグイン**へ分離しました。`codex plugin add playwright-kit@ai-plugins` で導入してください。移行先の対応表は予告どおり v8.0.0 で `ndf-policies` から削除したため、リポジトリ root の [README.md](../../README.md) の「NDF v7.0.0 の主な変更(非互換)」を参照してください。 ## インストール @@ -47,7 +47,7 @@ Claude Code 専用の agents、statusline 自動設定、transcript retention ```text # 動く: 実体パスを示して読ませる -~/.codex/plugins/cache/ai-plugins/ndf/7.0.0/skills/deploy/SKILL.md を読んで、その手順どおりに qa/staging へ deploy PR を作成してください。 +~/.codex/plugins/cache/ai-plugins/ndf/8.0.0/skills/deploy/SKILL.md を読んで、その手順どおりに qa/staging へ deploy PR を作成してください。 # 動かない: 明示起動 ($ は展開されない) $deploy qa/staging @@ -69,14 +69,14 @@ marketplace 経由でインストールした場合、Skill の実体は **ワ ```text $CODEX_HOME/plugins/cache////skills//SKILL.md # 既定 ($CODEX_HOME=~/.codex) の例: -# ~/.codex/plugins/cache/ai-plugins/ndf/7.0.0/skills/deploy/SKILL.md +# ~/.codex/plugins/cache/ai-plugins/ndf/8.0.0/skills/deploy/SKILL.md ``` そのため「`deploy` の SKILL.md を探して読んで」のような曖昧な依頼は、Codex のファイル探索がワークスペース内に限られる状況では失敗しえます。**抑止した Skill は `$` が展開されない**ので、`codex plugin list` で実体パスを確認し、絶対パスを渡してください。 ```bash codex plugin list | grep 'ndf@ai-plugins' -# => ndf@ai-plugins installed, enabled 7.0.0 +# => ndf@ai-plugins installed, enabled 8.0.0 ``` 抑止していない Skill(`markdown-writing` など)はキャッシュ配下でも `$` で解決するため、そちらは `$` 起動が使えます。 diff --git a/plugins/ndf-codex/skills/development-workflow/SKILL.md b/plugins/ndf-codex/skills/development-workflow/SKILL.md index 0409b880..275df320 100644 --- a/plugins/ndf-codex/skills/development-workflow/SKILL.md +++ b/plugins/ndf-codex/skills/development-workflow/SKILL.md @@ -47,7 +47,7 @@ NULL 許容列の追加)は `standard` として扱う。判定に迷う場合 ```text mode: standard 根拠: 注文確定の振る舞いを変更する。公開 API とスキーマは変えない -必須工程: requirements-design → implementation-plan → tdd-cycle → safe-refactoring(必要な場合) +必須工程: requirements-design → implementation-plan → tdd-cycle → refactoring → pr-review → quality-gates → plan-to-spec(仕様が変わった場合) ``` @@ -59,8 +59,8 @@ mode: standard | モード | 対象 | 必須工程 | | --- | --- | --- | | `light` | 文言、ドキュメント、設定、テストの追加など、本番の振る舞いも本番コードの構造も変えない局所変更 | 成功条件の確認、対象範囲の確定、限定的な検証と静的解析 | -| `standard` | 一般的な機能追加・バグ修正、テストが十分にある構造改善 | 仕様、計画、テスト駆動、構造改善(必要な場合)、レビュー、全体検証 | -| `architecture` | 公開インタフェース、移行を伴うスキーマ変更、認証、複数モジュール、重要なドメイン変更 | ドメインモデリング、設計判断の記録、設計レビュー、テスト駆動、契約テストと結合テスト、相互レビュー | +| `standard` | 一般的な機能追加・バグ修正、テストが十分にある構造改善 | 仕様、計画、テスト駆動、構造改善、レビュー、全体検証 | +| `architecture` | 公開インタフェース、移行を伴うスキーマ変更、認証、複数モジュール、重要なドメイン変更 | ドメインモデリング、設計判断の記録、設計レビュー、テスト駆動、構造改善、契約テストと結合テスト、相互レビュー | | `legacy-refactor` | テストが少ない既存コードの振る舞い維持型改善 | 構造分析、計画、現状固定テスト、段階的改善、レビュー、退行検証 | ## モードごとに起動する Skill @@ -70,8 +70,8 @@ mode: standard | 要求と受け入れ条件 | — | `requirements-design` | `requirements-design` | — | | 設計 | — | `implementation-plan` に代替案と採否を記録 | ドメインモデリングと設計レビュー(Release 2 で有効化) | `implementation-plan` に代替案と採否を記録 | | 計画 | — | `implementation-plan` | `implementation-plan` | `implementation-plan` | -| 実装 | 直接編集 | `tdd-cycle` | `tdd-cycle` | `safe-refactoring` | -| 構造改善 | — | `safe-refactoring`(必要な場合) | `safe-refactoring`(必要な場合) | `safe-refactoring` | +| 実装 | 直接編集 | `tdd-cycle` | `tdd-cycle` | `refactoring` | +| 構造改善 | — | `refactoring` | `refactoring` | `refactoring` | | レビュー | — | `pr-review` | `cross-review` | `pr-review` | | 完了判定 | `quality-gates` | `quality-gates` | `quality-gates` | `quality-gates` | | 確定仕様化 | — | `plan-to-spec`(仕様が変わった場合) | `plan-to-spec` | — | @@ -83,6 +83,13 @@ mode: standard レビュー段階は**明示的に呼ぶ**。自然文で「レビューして」と依頼すると、Claude Code では 組み込みの `code-review` が起動して判定の投稿経路が変わる。 +構造改善は**レビューと同じく、通す工程であって任意ではない**。動くコードが出た時点では整理が +済んでいないことを前提に置き、見つけたスメルは直す。対象は書き換えた行だけでなく、**その +呼び出し元・呼び出し先と、同じファイル・同じモジュールの関連箇所まで**を含む(範囲と例外は +`refactoring` の `references/code-smells.md`「手を付ける範囲」)。 + +`light` だけが工程ごと対象外である。本番コードの構造を変えない変更に構造改善の判断は要らない。 + ## 標準フロー この図は**工程の全体像**を表す。どの Skill を起動するかは前節の表が基準であり、図はその @@ -98,7 +105,8 @@ flowchart TD E --> F F --> G[実装計画] G --> H[失敗するテスト → 最小実装 → 整理] - H --> I[仕様適合レビュー] + H --> R[構造改善] + R --> I[仕様適合レビュー] I --> J[コード品質レビュー] J --> K[限定的な検証・静的解析] K --> N[全体テスト → ビルド・結合テスト] @@ -117,7 +125,8 @@ flowchart TD (K は変更箇所を 1 度実行する限定的な検証と静的解析だけを指す。依存パッケージの版更新だけは 例外として既存テスト一式を実行する — [references/workflow-modes.md](references/workflow-modes.md)) - `legacy-refactor` は A から C へ抜けて `standard` と同じ経路をたどり、**B(要求と受け入れ条件)と - M(確定仕様化)は通らない**。H は「現状固定テスト → 段階的改善」、I は「本番の振る舞いが + M(確定仕様化)は通らない**。H は「現状固定テスト」、R は「段階的改善」、I は「本番の振る舞いが + 変わっていないことの確認」として読む ## `architecture` モードの現状 diff --git a/plugins/ndf-codex/skills/development-workflow/references/workflow-modes.md b/plugins/ndf-codex/skills/development-workflow/references/workflow-modes.md index 84a29033..e9f2fc1e 100644 --- a/plugins/ndf-codex/skills/development-workflow/references/workflow-modes.md +++ b/plugins/ndf-codex/skills/development-workflow/references/workflow-modes.md @@ -67,9 +67,10 @@ - 目的: 受け入れ条件を満たし、退行を出さないこと - 受け入れ条件を先に作る。条件が作れない依頼は、質問して止まる -- 実装はテスト駆動で進める。構造改善が必要になったら差分を分ける +- 実装はテスト駆動で進める。**通したあとに構造改善の工程を通す**(手を付けない判断でもよいが、 + 工程は飛ばさない)。手を入れる場合は差分を分ける - 振る舞いを変えない構造変更のみの依頼は、受け入れ条件を「既存テストが通り続けること」とし、 - 実装は `safe-refactoring` の手順で進める + 実装は `refactoring` の手順で進める - 完了判定は全体テストまで通す ### `architecture` diff --git a/plugins/ndf-codex/skills/ndf-policies/SKILL.md b/plugins/ndf-codex/skills/ndf-policies/SKILL.md index 677d4eda..bf6de328 100644 --- a/plugins/ndf-codex/skills/ndf-policies/SKILL.md +++ b/plugins/ndf-codex/skills/ndf-policies/SKILL.md @@ -18,33 +18,18 @@ user-invocable: false 4. **マージ済みブランチには push しない。** 既存 PR の状態を確認し、マージ済みなら新ブランチ + 新 PR を作る(サフィックス `-v2`, `-v3`) 5. **revert を連鎖させない。** 最終的なあるべき状態を直接コミットする方が履歴上の意図が明確になり、後の cherry-pick も簡単になる -## v7.0.0 で移動した Skill(v8.0.0 で削除) +## v8.0.0 で改名した Skill(v9.0.0 で削除) -ブラウザ自動テストの 4 Skill を **`playwright-kit` プラグイン**へ分離した。**Skill 名は -変わらない**ため、`/playwright-` まで打てば従来どおり候補に出る。 +構造改善の Skill を **`/ndf:refactoring`** へ改名し、分岐・反復・定数の表現を決める観点を +統合した。引数と手順は変わらない。 | 旧コマンド | 移行先 | | --- | --- | -| `/ndf:playwright-planning` | `/playwright-kit:playwright-planning` | -| `/ndf:playwright-authoring` | `/playwright-kit:playwright-authoring` | -| `/ndf:playwright-evidence` | `/playwright-kit:playwright-evidence` | -| `/ndf:playwright-kit-ops` | `/playwright-kit:playwright-kit-ops` | - -利用するにはプラグインを別途インストールする。 - -```bash -# Claude Code -/plugin install playwright-kit@ai-plugins -# Codex -codex plugin add playwright-kit@ai-plugins -# Kiro CLI -bash plugins/playwright-kit-kiro/install.sh -``` - -分離したのは、Skill の `name` と `description` が起動時の一覧として常時注入され、その予算が -プラグイン横断で共有されるためである。ブラウザ自動テストは使う場面が限られる一方で MCP -ツールを多用するため frontmatter が大きく(4 個で約 2,400 文字)、全利用者へ常時注入する -取り分に見合わなかった。 - -v6.0.0 の対応表(`/ndf:review` → `/ndf:pr-review`)は、予告どおり本バージョンで削除した。 -v6.0.0 以前から移行する場合は v6.1.0 の `ndf-policies` を参照する。 +| `/ndf:safe-refactoring` | `/ndf:refactoring` | + +`safe-` を外したのは、`/refactoring` で一意に決まり、入力が短くなるためである。統合した観点は +`references/data-representation.md` にあり、スメル一覧からも参照される。 + +v7.0.0 の対応表(playwright 系 4 Skill の `playwright-kit` プラグインへの分離)は、予告どおり +本バージョンで削除した。v7.0.0 より前から移行する場合は、この対応表を持つ最後の配布版である +v7.0.0 の `ndf-policies` を参照する。 diff --git a/plugins/ndf-codex/skills/pr-review/SKILL.md b/plugins/ndf-codex/skills/pr-review/SKILL.md index fe188f1f..04e92849 100644 --- a/plugins/ndf-codex/skills/pr-review/SKILL.md +++ b/plugins/ndf-codex/skills/pr-review/SKILL.md @@ -69,7 +69,7 @@ PR 差分、または `--branch` 指定時は現在のブランチの差分を | 責務・凝集度・結合度 | 1 つの単位が複数の変更理由を持っていないか | | 依存の向き | 業務ロジックが外部の仕組みへ直接依存していないか。循環がないか | | 可読性・単純性 | 分岐の深さ、名前と実態の一致、不要な抽象化 | -| コードスメル | 重複、長すぎる単位、基本型への固執など(`safe-refactoring` の一覧) | +| コードスメル | 重複、長すぎる単位、基本型への固執など(`refactoring` の一覧) | | セキュリティ・性能 | 下の「具体的なチェックポイント」 | | テストが実装詳細に結合していないか | 内部呼び出し回数の検証、private への直接依存(`tdd-cycle` の脆いテスト) | diff --git a/plugins/ndf-codex/skills/problem-solving/SKILL.md b/plugins/ndf-codex/skills/problem-solving/SKILL.md index 80f40e7a..13aa2987 100644 --- a/plugins/ndf-codex/skills/problem-solving/SKILL.md +++ b/plugins/ndf-codex/skills/problem-solving/SKILL.md @@ -57,7 +57,7 @@ description: "Fix bugs and data inconsistencies upstream at the root cause. Use 対象にテストがほとんどない場合、再現テストを書く前に**現状の振る舞いを固定するテスト**を 置く。副作用を分離できず再現テストが書けない状態で修正すると、直したい振る舞い以外を -壊しても気づけない。手順は `safe-refactoring` の現状固定テストに従う。 +壊しても気づけない。手順は `refactoring` の現状固定テストに従う。 ```text 1. 変更対象の入口と副作用を洗い出す diff --git a/plugins/ndf-kiro/skills/safe-refactoring/SKILL.md b/plugins/ndf-codex/skills/refactoring/SKILL.md similarity index 79% rename from plugins/ndf-kiro/skills/safe-refactoring/SKILL.md rename to plugins/ndf-codex/skills/refactoring/SKILL.md index 55288890..4fff6d27 100644 --- a/plugins/ndf-kiro/skills/safe-refactoring/SKILL.md +++ b/plugins/ndf-codex/skills/refactoring/SKILL.md @@ -1,6 +1,6 @@ --- -name: safe-refactoring -description: "Change structure without changing behavior, guarded by tests. Use when cleaning up code or touching legacy code(リファクタリング・コードスメル・現状固定テスト)." +name: refactoring +description: "Change structure without changing behavior, guarded by tests, judging smells and how decisions are represented. Use when cleaning up code or touching legacy code(リファクタリング・コードスメル・分岐をデータ化・現状固定テスト)." --- # 安全な構造改善 @@ -8,6 +8,11 @@ description: "Change structure without changing behavior, guarded by tests. Use **テストがなければ、それは構造改善ではなく単なる編集である。** 振る舞いが変わっていない ことを示す手段がない書き換えは、この Skill の対象外として扱う。 +この工程は**レビューと同じく、実装のあとに必ず通す**。動くコードが出た時点では整理が済んで +いないことを前提に置く。対象は書き換えた行だけでなく、**その呼び出し元・呼び出し先と、同じ +ファイル・同じモジュールの関連箇所まで**を含む(範囲と例外は +[references/code-smells.md](references/code-smells.md) の「手を付ける範囲」)。 + ## 最初に決める 2 つのこと ### 1. 機能変更と構造改善を混ぜない @@ -40,7 +45,9 @@ description: "Change structure without changing behavior, guarded by tests. Use 1. **変更前に既存テストを実行する。** ここで落ちているものがあれば、先に報告する 2. スメルを 1 つ選ぶ(一覧は [references/code-smells.md](references/code-smells.md)) 3. 対応する手法を選ぶ([references/refactoring-catalog.md](references/refactoring-catalog.md)。 - スメル一覧で ★ が付いた手法はカタログに項目がなく、一覧の記述だけで進めてよい) + スメル一覧で ★ が付いた手法はカタログに項目がなく、一覧の記述だけで進めてよい)。 + 分岐・反復・定数を**何にどう置き換えるか**は + [references/data-representation.md](references/data-representation.md) で決める 4. **1 手だけ適用する** 5. テストを実行する。落ちたら直前の 1 手を戻す 6. 通ったらコミットする(1 手 = 1 コミットを既定とする) @@ -109,7 +116,7 @@ flowchart TD - 固定テストが書けない(副作用が分離できない、実行に外部環境が要る) - 1 手で終わらず、テストを通すために本番コードの分岐を足す必要が出た - 改善の途中で仕様の不明点が出た(`requirements-design` へ戻る) -- 差分が依頼範囲を超えて広がった +- 差分が [code-smells.md](references/code-smells.md) の「手を付ける範囲」を超えて広がった 止めたときは「どこまで安全な状態か」を明示する。中途半端な状態を「あとで直す」前提で 残さない。 @@ -130,4 +137,11 @@ flowchart TD - [references/code-smells.md](references/code-smells.md) — 構造改善の起点になる兆候 - [references/refactoring-catalog.md](references/refactoring-catalog.md) — 手法と適用条件 +- [references/data-representation.md](references/data-representation.md) — 分岐・反復・定数を何にどう置き換えるか +- 言語ごとの手段 — **対象の言語のファイルだけを読む** + - [references/lang-python.md](references/lang-python.md) + - [references/lang-javascript.md](references/lang-javascript.md) + - [references/lang-typescript.md](references/lang-typescript.md) + - [references/lang-php.md](references/lang-php.md) + - 一覧にない言語は、`data-representation.md` の判定表から自分で対応付ける - [references/characterization-tests.md](references/characterization-tests.md) — 現状固定テストの作り方 diff --git a/plugins/ndf-codex/skills/safe-refactoring/references/characterization-tests.md b/plugins/ndf-codex/skills/refactoring/references/characterization-tests.md similarity index 100% rename from plugins/ndf-codex/skills/safe-refactoring/references/characterization-tests.md rename to plugins/ndf-codex/skills/refactoring/references/characterization-tests.md diff --git a/plugins/ndf-claude/skills/safe-refactoring/references/code-smells.md b/plugins/ndf-codex/skills/refactoring/references/code-smells.md similarity index 59% rename from plugins/ndf-claude/skills/safe-refactoring/references/code-smells.md rename to plugins/ndf-codex/skills/refactoring/references/code-smells.md index 325f04ad..ef77a7e5 100644 --- a/plugins/ndf-claude/skills/safe-refactoring/references/code-smells.md +++ b/plugins/ndf-codex/skills/refactoring/references/code-smells.md @@ -1,7 +1,7 @@ # コードスメル -スメルは「直すべき欠陥」ではなく **調べる価値がある兆候** である。見つけても、変更の -理由(機能追加・不具合修正・読みにくさ)が伴っていなければ手を付けない。 +スメルは「直すべき欠陥」ではなく **調べる価値がある兆候** である。兆候を見つけたら、下の +「手を付ける範囲」に入っているかを確かめ、入っていれば直す。 行数などの数値規則は使わない。判断は凝集度・結合度・変更理由・認知負荷・テスト容易性で行う。 @@ -26,10 +26,16 @@ | 例外の飲み込み | 捕まえて何もしない、ログだけ出して続行する | 失敗が沈黙し、原因が追えない | ★呼び出し元へ伝える、または明示的に扱う | | 条件分岐の連鎖 | 型・状態ごとの分岐が複数箇所で繰り返される | 種類を増やすたびに全箇所を直す | 多態による分岐の置き換え | | 設定の散在 | 同じ設定値が複数の場所で定義される | どちらが効くか分からない | ★定義を 1 箇所へ寄せる | +| 業務ルールの埋め込み | 料率・区分・しきい値・優先順位が制御構文の中に書かれている | 一覧できない。値を変えるだけの修正にコード変更と再配備が要る | 対応表への置き換え | +| 一件ずつの反復 | 同種で独立した処理を 1 件ずつ繰り返す(1 件ごとの問い合わせを含む) | 往復回数か実行時間が件数に比例して積み上がる | 一括処理への置き換え | +| 検証のない外部化 | 設定・マスタへ出したデータに、スキーマ・版・検証がない | 壊れた値が実行時まで通る。どの版が適用されたか追えない | ★スキーマと版を与え、読み込み境界で検証する | -★ の 4 件をカタログに置いていないのは、手順が「1 箇所へ寄せる/呼び出し元へ返す」で尽きて -おり、適用条件も上の「何が問題か」以外に無いためである。カタログを探さず、この表の記述で -そのまま進めてよい。ただし共通化だけは判断を誤りやすいため、次節の見分け方に従う。 +★ の 5 件をカタログに置いていないのは、手順が「1 箇所へ寄せる/呼び出し元へ返す/検証を足す」 +で尽きており、適用条件も上の「何が問題か」以外に無いためである。カタログを探さず、この表の +記述でそのまま進めてよい。ただし共通化だけは判断を誤りやすいため、次節の見分け方に従う。 + +下 3 件は「どう持つか」の判断を伴う。表現の選び方と、外部化してよい条件は +[data-representation.md](data-representation.md) にある。 ## 共通化してよい重複の見分け方 @@ -61,11 +67,33 @@ 「何を解決したか」と「外し方」を残す。決まった書式は今のところ無い(Release 2 で追加予定の `object-design` Skill で扱う想定だが、現時点では未実装)。 -## スメルに手を付けない場合 +## 手を付ける範囲 + +**今回書き換えた行だけに閉じない。** そこに閉じると、読みにくい領域は読みにくいまま残り続ける。 +一方で無制限に広げると差分がレビューできなくなる。次を境界にする。 + +| 範囲 | 扱い | +| --- | --- | +| 今回変更した関数・クラス | 直す | +| その呼び出し元・呼び出し先 | 直す。変更の影響を読むために通る範囲であり、テストも通っている | +| 同じファイル・同じモジュールの関連箇所 | 直す。差分は分ける | +| そこから遠い領域 | 対象外。後続の作業として記録する | + +範囲に入っていることは、直す理由にはならない。**この文書のスメル一覧のどれにも当たらない箇所は +直さない。**「読みやすくなりそう」だけで手を入れると、レビューできない差分になる。手順(1 手ずつ・ +現状固定テストで守る・差分を分ける)は、範囲を広げても変わらない。 + +広げた分は**別のコミットに切る**(「差分の切り方」)。機能変更と構造改善が同じ差分に入ると、 +レビュアーは意図した変更と構造改善の事故を区別できない。上表の範囲を超えて広がるなら、 +別の変更として出す。 + +## 手を付けない場合 + +範囲に入っていても、次は対象から外す。 -- 変更予定のない領域(読みにくいだけで、今回の変更に関係しない) - 生成物・外部から取り込んだコード(上流で直す) - 削除予定の領域 +- 振る舞いが変わっていないことを示す手段がなく、現状固定テストも書けない箇所 + (`SKILL.md` の「途中で止める条件」) -手を付けないと決めた場合、指摘だけを残すよりも**何もしない**方がよい。残す価値がある -指摘は、後続の作業として記録する。 +外したものは、指摘だけを残さず**後続の作業として記録する**。 diff --git a/plugins/ndf-codex/skills/refactoring/references/data-representation.md b/plugins/ndf-codex/skills/refactoring/references/data-representation.md new file mode 100644 index 00000000..213f15bf --- /dev/null +++ b/plugins/ndf-codex/skills/refactoring/references/data-representation.md @@ -0,0 +1,186 @@ +# 分岐・反復・定数の表現 + +`code-smells.md` の「業務ルールの埋め込み」「一件ずつの反復」「検証のない外部化」を見つけた +ときに、**何にどう置き換えるか**を決めるための判断材料。 + +置き換えの目的は 2 つに尽きる。どちらにも近づかない置き換えは行わない。 + +| 時点 | 近づける状態 | +| --- | --- | +| 実行前 | どの入力がどの結果になるかを実装を読まずに一覧でき、扱っていない入力があれば検査で分かる | +| 実行後 | どの判断がなぜその結果になったかを、記録だけで再現できる | + +## 移す対象を決める + +> **変化する知識はデータへ。安定した機構と不変条件はコードへ。** + +判断の軸は「分岐が多いか」ではなく「**その知識が変化するか**」である。変化しない知識をデータへ +出すと、検査できる場所が減るだけになる。 + +| データへ移す | コードに残す | +| --- | --- | +| 業務ルール、料率、しきい値、優先順位 | 不変条件、安全性の制約 | +| 分類・対応表(値から値への写像) | 機構(どう読み、どう書き、どう失敗を扱うか) | +| 環境ごとに変わる値 | 型・スキーマ・境界の定義 | +| 利用者が管理するカテゴリ | プロトコル、閉じた状態集合 | + +## 手を付けないもの + +次はいずれも**そのままでよい**。スメルとして拾わない。 + +- 単純なガード節の条件分岐(早期リターンによる前提条件の検査) +- 閉じた型に対する網羅的な分岐(静的に網羅性を検査できるもの) +- 逐次依存・早期終了・ストリーム処理・メモリ制約下の明示的なループ +- 不変条件・プロトコル・閉じた状態集合を表す定数と列挙型 +- 原子性・整合性・安全性を守るための即時打ち切りと巻き戻し。全体を 1 単位として成否を決める + 処理、後続に不正な入力が波及する処理、安全性検査の失敗はこれにあたる + +## 改善にならない置き換え + +形だけが変わり、上の 2 つの状態に近づかないもの。着手する前に見分ける。 + +| 置き換え | 何が起きるか | +| --- | --- | +| 条件分岐を機械的に対応表へ移す | 前提条件の検査は、対応表より分岐のほうが意図が明確。網羅性を検査できていた分岐を型情報のない対応表へ移すと、その検査が消える | +| 逐次実行のまま高階反復へ書き換える | 実行の実体は変わらない。一括実行にも並列実行にもなっていない | +| 定数をすべて外部設定へ出す | 条件・参照・既定値が設定側に積み上がり、設定自体が独自の言語になる。変更に必要な知識はむしろ増える | + +## 分岐の種類 → 適切な表現 + +| 分岐の種類 | 適切な表現 | +| --- | --- | +| 値から値への単純な対応 | 対応表(写像) | +| 処理方式の切り替え | 登録表(識別子から処理への対応) | +| 条件の組み合わせが多い業務判断 | 決定表、ルールエンジン | +| 認可・制約・組織ポリシー | ポリシーエンジン | +| 時系列で状態が変化する処理 | 状態機械 | +| 閉じた少数の型分岐 | 型に対する網羅的な分岐(置き換えない) | +| 前提条件の検査 | ガード節(置き換えない) | + +## 反復の実行方式 → 得られるもの + +置き換えても何も得られない場合を見分けるための表である。分類の軸は構文ではなく**実行方式**で +ある。 + +| 実行方式 | 実行の実体 | 得られるもの | +| --- | --- | --- | +| 逐次実行 | 1 件ずつ順に処理する | **なし**(記述の変更のみ) | +| 一括演算 | 基盤側で一括実行 | 実行時間 | +| 一括入出力 | 呼び出し回数の削減 | 往復回数(大量データでは最大の効果) | +| 並行処理 | 待ち時間の重ね合わせ | 待ち時間。計算時間は減らない | +| 並列処理 | 複数の実行資源で同時 | 計算時間 | +| 分散データフロー | 基盤が分割・再試行・集約 | 規模と耐障害性 | + +高階反復は構文であって実行方式ではない。**逐次実行のまま構文だけを高階反復へ置き換えても、この +表のどの行にも移動していない。** 高階反復の形のまま遅延・並行・並列・分散で実行する仕組みを持つ +言語もあるが、得られるものを決めるのはその実行方式であって構文ではない。**移すなら、移動先の行 +を言えなければならない。** 置き換えたら計測し、効果が出ていなければ戻す。 + +一括演算の基盤は言語によって有無が異なる。基盤がない言語では、一括入出力(往復回数の削減)が +主戦場になる。 + +## 定数の性質 → 置き場所 + +| 値の性質 | 置き場所 | +| --- | --- | +| 数学的・技術的な不変条件 | コード内の定数 | +| 閉じた状態集合・プロトコル | 型、列挙型、スキーマ | +| 頻繁に変わる業務ルール | 決定表、ポリシー、設定 | +| 環境ごとに変わる値 | 配備設定、環境変数 | +| 利用者が管理するカテゴリ | 参照テーブル(データベース) | +| 表示名・文言 | 多言語化資源、コンテンツ | +| 安全性の絶対制約 | コード・型・スキーマ・ポリシーの複数層 | + +不透明なコード値(`status == 3`)は避ける。ただし**意味の分かる識別子を持つ列挙型まで置き換え +ない**。型のない文字列にすると、綴り誤りと未処理ケースの発見が実行時まで遅れる。 + +## 外部化してよい条件 + +データへ移すなら、移した先で次を用意できることが条件になる。用意できないなら、コードに残した +ほうが検査できる範囲は広い。 + +- [ ] スキーマがある +- [ ] 型またはバリデーションで検査される +- [ ] 版を持ち、変更履歴が追える +- [ ] 競合するルール・到達不能なルールを検出できる +- [ ] テストがある +- [ ] 変更の適用手順(移行)がある +- [ ] 誰が何を変更したか監査できる +- [ ] 実行時に、適用したルールの識別子と理由を記録する + +外部化したデータをどう守るかは、**そのデータをいつ読むか**で決まる。 + +1. **データごとビルド時に組み込める**なら、スキーマから型・定数を生成して取り込む。外部化しても + 静的な検査が残るため、これが選べるなら最初に選ぶ +2. **データを実行時にロードする**なら、**ロード境界をスキーマ検証で守る**。型を生成していても + 同じで、生成した型は、ロードした値がその形である保証を与えない +3. どちらも満たせないなら、外部化しない + +型情報を伴わずに実行時ロードした対応表は、静的解析の対象から外れる。どの検査がどこまで効くかは +言語によって違う。**対象の言語のファイルだけを読む** — +[lang-python.md](lang-python.md) / [lang-javascript.md](lang-javascript.md) / +[lang-typescript.md](lang-typescript.md) / [lang-php.md](lang-php.md)。 + +## 判断を記録できるようにする + +対応表への置き換えは、**どのルールが適用されたかを記録できる構造を保って完了**である。値を +外へ出した分だけ「なぜこの結果になったか」が追いにくくなるので、表から識別子と版を取り出せる +形を崩さない。 + +記録を出す要件があるなら、1 つの判断につき 1 件、入力・決定・理由・実行文脈をまとめた +構造化イベントを出す。 + +```json +{ + "event_name": "discount_decided", + "customer_id": "cus_123", + "input": { "rank": "gold", "purchase_amount": 120000 }, + "decision": { + "discount_rate": 0.2, + "rule_id": "customer-rank-discount", + "rule_version": "2026-08-01", + "reason": "customer rank is gold" + }, + "execution": { "code_version": "a13fd82", "duration_ms": 4, "trace_id": "..." } +} +``` + +- 属性は、絞り込み・グループ化・集計・相関に使える形にする +- 適用したルールの識別子と版を含めると、結果に至った経緯を後から再現できる +- 個人情報と秘密情報の扱いは `logging-guidelines` に従う + +**記録の追加は振る舞いの変更である。** 構造改善と同じ差分に混ぜず、コミットを分ける。 + +一括処理への置き換えでは、終了時に失敗の集計も出す。 + +```json +{ + "event_name": "import_completed", + "total": 12000, "succeeded": 11940, "failed": 60, + "failures_by_reason": { "invalid_date": 48, "missing_customer": 12 }, + "sample_failed_ids": ["row_88", "row_312", "row_940"] +} +``` + +項目が独立に処理できるのに最初の失敗で全体を止めると、どこまで進んだかが分からなくなる。 +全体を 1 単位として成否を決める処理では、打ち切って巻き戻すのが正しい。その場合は**打ち切った +位置・理由・巻き戻しの範囲**を同じ構造化イベントに含める。 + +## 例: 業務ルールの埋め込みを置き換える + +```text +❌ 変化する料率が制御構文へ埋まっている + if rank == "gold" -> rate = 0.2 + else if rank == "silver" -> rate = 0.1 + else -> rate = 0 + +✅ 対応表として持ち、既定と未知の扱いを明示する + rates = { gold: 0.2, silver: 0.1, bronze: 0.0 } # 版とスキーマを持つ + rate = rates[rank](未知の階級は失敗として扱い、握りつぶさない) +``` + +置き換えた結果、次が言えるようになっていなければ手を付けた意味がない。 + +- 料率の一覧を**コードを読まずに**確認できる +- 新しい階級の追加が、分岐の追加ではなくデータの追加になる +- どの階級にどの版の料率が適用されたか、記録から再現できる diff --git a/plugins/ndf-codex/skills/refactoring/references/lang-javascript.md b/plugins/ndf-codex/skills/refactoring/references/lang-javascript.md new file mode 100644 index 00000000..2c4c0b66 --- /dev/null +++ b/plugins/ndf-codex/skills/refactoring/references/lang-javascript.md @@ -0,0 +1,45 @@ +# JavaScript での手段 + +[data-representation.md](data-representation.md) で選んだ表現を、JavaScript の機能へ対応付ける。 + +| 判定 | 手段 | +| --- | --- | +| 値から値への対応 | `Object.freeze` のオブジェクト | +| 処理方式の切り替え | 関数を値に持つオブジェクト | +| 閉じた状態集合 | 凍結した定数オブジェクト | +| 網羅性の静的検査 | **手段なし**(実行時に失敗させる) | +| 不変性 | `Object.freeze`(凍結はトップレベルのみ)。入れ子まで不変にするなら再帰的に凍結するか、構造共有の不変データ構造を使う | +| スキーマ検証 | zod / JSON Schema | +| 一括処理 | 一括 API / `Promise.all`(並行) | +| 失敗の集計 | 失敗を集めて返す | + +## 静的な網羅性検査が効かないことが判定を変える + +型注釈がないため、**閉じた状態集合に対する分岐の網羅性を実行前に検査できない**。これが +2 つの判定を変える。 + +- 「手を付けないもの」にある「閉じた型に対する網羅的な分岐(静的に網羅性を検査できるもの)」 + の条件が成立しない。分岐をそのまま残しても追加漏れは実行時まで分からないので、**未知の + ケースで必ず失敗させる**書き方を併せて用意する +- 外部化した対応表を守る手段が実行時のスキーマ検証しかない。**静的検査で埋め合わせができない + 分、スキーマ検証の必要性が高い** + +```javascript +// ❌ 未知のケースが undefined として下流へ流れ、どこで壊れたか分からなくなる +const RATES = { gold: 0.2, silver: 0.1 }; +return RATES[rank]; + +// ✅ 未知のケースをその場で失敗させる +const RATES = Object.freeze({ gold: 0.2, silver: 0.1, bronze: 0 }); +if (!Object.hasOwn(RATES, rank)) throw new Error(`unknown rank: ${rank}`); +return RATES[rank]; +``` + +JSDoc の型注釈と `checkJs` を入れられるなら、静的検査が使える状態に戻せる。その場合の書き方は +[lang-typescript.md](lang-typescript.md) を読む。ただし実行時にロードするならロード境界の +検証は同じく要る(JSDoc の型は実体を検査しない)。 + +## 並行と並列を混同しない + +`Promise.all` は待ち時間を重ねるだけで、計算時間は減らない。CPU を使う処理を並列化するには +worker が要る。「反復の実行方式」表の「並行処理」と「並列処理」は別の行である。 diff --git a/plugins/ndf-codex/skills/refactoring/references/lang-php.md b/plugins/ndf-codex/skills/refactoring/references/lang-php.md new file mode 100644 index 00000000..7b5c958f --- /dev/null +++ b/plugins/ndf-codex/skills/refactoring/references/lang-php.md @@ -0,0 +1,146 @@ +# PHP での手段 + +[data-representation.md](data-representation.md) で選んだ表現を、PHP の機能へ対応付ける。 +以下は **8.1 以降**を前提とする(8.0 以下は最終節)。 + +| 判定 | 手段 | +| --- | --- | +| 値から値への対応 | 連想配列 / `match` | +| 処理方式の切り替え | first-class callable 構文 `foo(...)` | +| 閉じた状態集合 | backed enum | +| 網羅性の静的検査 | PHPStan `match.unhandled` | +| 不変性 | `readonly` プロパティ | +| スキーマ検証 | JSON Schema / Valinor | +| 一括処理 | **一括入出力**(標準に一括演算の基盤はない) | +| 失敗の集計 | 失敗を集めて返す | + +8.1 で backed enum・`readonly` プロパティ・first-class callable 構文が入り、**データ化しても +静的解析が効く**範囲が広がった。 + +## 網羅性の静的検査 + +```php +enum Rank: string { + case Gold = 'gold'; + case Silver = 'silver'; + case Bronze = 'bronze'; +} + +function label(Rank $rank): string { + return match ($rank) { + Rank::Gold => 'ゴールド', + Rank::Silver => 'シルバー', + Rank::Bronze => 'ブロンズ', + }; +} +``` + +`match` は一致する腕がなければ実行時に `\UnhandledMatchError` を投げる。さらに **PHPStan が +`match.unhandled`("Match expression does not handle remaining value")として静的に検出する**。 +`Rank` にケースを足すと、`default` を書いていない `match` が解析で落ちる。 + +```php +// ❌ 連想配列へ移すと、検査は弱くなる +$labels = ['gold' => 'ゴールド', 'silver' => 'シルバー']; +return $labels[$rank->value]; +``` + +どこまで弱くなるかは、**対応表が静的に見えているか**で決まる(PHPStan 2.2 で実測)。 + +| 書き方 | level 5 | level max | +| --- | --- | --- | +| `match`(`default` なし) | `match.unhandled` で検出 | 同左 | +| その場に書いた連想配列 | 検出なし | `offsetAccess.notFound` で検出 | +| 外部から渡した対応表(`array`) | 検出なし | **検出なし** | + +**この表が測っているのは、型情報を伴わずに実行時ロードした場合である**(3 行目は +`array` という幅の広い型で対応表を受け取る形)。この形にすると、変化する業務 +ルールは定義ごと静的解析の視界から外れるため、「外部化してよい条件」(スキーマ・ +バリデーション・版・テスト)が必須になる。**静的検査で守れなくなった分を、スキーマ検証で +埋める。** + +一方、スキーマから backed enum や定数クラスを**生成**してビルド時に取り込めば、外部化しても +`match.unhandled` の検査は残る。ただし**データを実行時にロードするなら、型を生成していても +ロード境界の検証は要る**。`json_decode` の戻り値に `@var GeneratedShape` を付けるだけでは、 +静的解析が信じるだけで実体は検査されない。`Valinor` などのマッパか JSON Schema 検証を通す。 + +値が固定で外部化する理由がないなら、`match` のまま置くほうが分析可能性は高い。 + +`default` を書くとケース追加漏れが検出されなくなる。**閉じた列挙に対する `match` に +`default` を置かない**(未知の入力を扱う必要があるなら、それは閉じた列挙ではない)。 + +## 処理方式の切り替え + +```php +// first-class callable 構文。静的解析が参照先を追える +$handlers = [ + 'csv' => $this->importCsv(...), + 'json' => $this->importJson(...), +]; +($handlers[$format] ?? throw new UnsupportedFormat($format))($payload); +``` + +文字列やコールバック配列(`'importCsv'` / `[$this, 'importCsv']`)で書くと解析が追えない。 +**データ化しても分析可能性を落とさない書き方を選ぶ。** + +## 一括処理は入出力の問題である + +**PHP 標準に一括演算の基盤はない。** `array_map` は逐次実行の高階反復であり、「反復の実行方式」 +表のどの行にも移動しない。`array_column` はコールバックを取らない組み込みの列抽出で、走査は逐次 +のままだが PHP レベルのループとは実装が異なるため、**計測して選ぶ**。数値計算向けの拡張や +ライブラリを導入すれば一括演算に移れる場合はあるので、採用するなら**効果を計測してから決める**。 + +```php +// ❌ ループを array_map に変えても、実行の実体は変わらない +$totals = array_map(fn($o) => $o->amount * 1.08, $orders); +``` + +PHP で効くのは**一括入出力**、すなわち往復回数の削減である。 + +```php +// ❌ N+1。1 行ごとに問い合わせる +foreach ($orderIds as $id) { $rows[] = $repo->find($id); } + +// ✅ 一括取得。往復が 1 回になる +$rows = $repo->findByIds($orderIds); + +// ✅ 一括挿入。件数に比例した往復をなくす +$repo->insertMany($rows); // 大量件数は chunk して分割する +``` + +大量データを扱うときは、`yield` による逐次生成でメモリを一定に保つ。これは「逐次依存・ +メモリ制約下の明示的なループ」にあたり、**そのままでよい**。 + +## 不変性 + +```php +final class Discount { + public function __construct( + public readonly string $ruleId, + public readonly string $ruleVersion, + public readonly float $rate, + ) {} +} +``` + +判断の結果を `readonly` の値として持つと、記録に必要なルールの識別子と版を持ち回れる。 + +## 8.0 以下 + +backed enum・`readonly`・first-class callable 構文がいずれも使えない。代替は次のとおりで、 +**いずれも静的検査は弱くなる**。 + +| 8.1+ | 8.0 以下の代替 | +| --- | --- | +| backed enum | クラス定数 + 値オブジェクト | +| `match` の網羅性検査 | `switch` + `default` で例外を投げる(実行時検出のみ) | +| `readonly` プロパティ | `private` + getter のみ | +| `foo(...)` | `Closure::fromCallable('foo')` | + +閉じた状態集合は 8.1 以降と同じくコード側に置く。変わるのは**外部化した業務ルールの守り方**で、 +生成した定数クラスに対する網羅性検査が効かないぶん、実行時のスキーマ検証への依存が高くなる。 + +## 出典 + +- [PHP 8.1 リリースアナウンス](https://www.php.net/releases/8.1/en.php) +- [PHPStan `match.unhandled`](https://phpstan.org/error-identifiers/match.unhandled) diff --git a/plugins/ndf-codex/skills/refactoring/references/lang-python.md b/plugins/ndf-codex/skills/refactoring/references/lang-python.md new file mode 100644 index 00000000..b08108d9 --- /dev/null +++ b/plugins/ndf-codex/skills/refactoring/references/lang-python.md @@ -0,0 +1,85 @@ +# Python での手段 + +[data-representation.md](data-representation.md) で選んだ表現を、Python の機能へ対応付ける。 + +| 判定 | 手段 | +| --- | --- | +| 値から値への対応 | `dict` / `Mapping` | +| 処理方式の切り替え | 関数を値に持つ `dict` | +| 閉じた状態集合 | `Enum` / `Literal` | +| 網羅性の静的検査 | mypy + `assert_never` | +| 不変性 | `@dataclass(frozen=True)` | +| スキーマ検証 | pydantic / jsonschema | +| 一括処理 | NumPy / pandas の**ベクトル化演算**(一括演算) | +| 失敗の集計 | 失敗を集めて返す | + +## 網羅性の静的検査 + +```python +from enum import Enum +from typing import assert_never # 3.11+(それ以前は typing_extensions) + +class Rank(Enum): + GOLD = "gold" + SILVER = "silver" + BRONZE = "bronze" + +def label(rank: Rank) -> str: + match rank: + case Rank.GOLD: return "ゴールド" + case Rank.SILVER: return "シルバー" + case Rank.BRONZE: return "ブロンズ" + case _: assert_never(rank) # ケース追加漏れを mypy が検出 +``` + +`Rank` に階級を足すと mypy が `assert_never` の行で型エラーを出す。**この検査が効いている +分岐は、`dict` へ移さない。** + +外部化した値を実行時にロードするなら、型注釈を用意していてもロード境界の検証は要る。 +`cast(Rates, json.load(f))` は mypy を黙らせるだけで実体を検査しないので、`pydantic` の +`TypeAdapter` や `jsonschema` を通してから使う。 + +## 一括演算とその反例 + +Python は一括演算の基盤(NumPy)を持つ数少ない言語である。ただし**「ループを消したこと」と +「一括演算になったこと」は別**である。 + +```python +# ❌ 逐次。要素ごとに Python のバイトコードを実行する +result = [x * 1.08 for x in prices] + +# ❌ 高階反復。上と実行の実体は変わらない +result = list(map(lambda x: x * 1.08, prices)) + +# ❌ np.vectorize も同じ。公式が明言している +# "provided primarily for convenience, not for performance. +# The implementation is essentially a for loop." +result = np.vectorize(lambda x: x * 1.08)(prices) + +# ✅ 一括演算。C 側で一括実行される +result = prices * 1.08 +``` + +pandas の `.apply()` も、Python の関数を要素・行・列のいずれかの単位で呼ぶ経路である限りは +一括演算ではない(ufunc を渡すなど、内部で一括実行に落ちる経路もある)。**置き換えたら +計測する。** 速くならないなら、「反復の実行方式」表の行を移動できていない。 + +## 失敗の集計 + +```python +def import_rows(rows): + ok, failures = [], [] + for i, row in enumerate(rows): + try: + ok.append(parse(row)) + except ValueError as e: + failures.append({"index": i, "reason": type(e).__name__, "id": row.get("id")}) + return ok, failures # 呼び出し側が件数・種類・対象を報告できる +``` + +例外を握りつぶさず、最初の失敗で打ち切らない。ここは明示的なループでよい(逐次依存では +ないが、失敗の収集がある)。 + +## 出典 + +- [numpy.vectorize — 性能目的ではないという公式注記](https://numpy.org/doc/stable/reference/generated/numpy.vectorize.html) diff --git a/plugins/ndf-codex/skills/refactoring/references/lang-typescript.md b/plugins/ndf-codex/skills/refactoring/references/lang-typescript.md new file mode 100644 index 00000000..6759eaae --- /dev/null +++ b/plugins/ndf-codex/skills/refactoring/references/lang-typescript.md @@ -0,0 +1,73 @@ +# TypeScript での手段 + +[data-representation.md](data-representation.md) で選んだ表現を、TypeScript の機能へ対応付ける。 +型注釈を持たない JavaScript は [lang-javascript.md](lang-javascript.md) を読む。 + +| 判定 | 手段 | +| --- | --- | +| 値から値への対応 | `Record` + `as const` | +| 処理方式の切り替え | ハンドラの `Record` | +| 閉じた状態集合 | 判別可能ユニオン | +| 網羅性の静的検査 | `never` への代入 | +| 不変性 | `readonly` / `as const` | +| スキーマ検証 | zod / JSON Schema | +| 一括処理 | 一括 API / `Promise.all`(並行) | +| 失敗の集計 | 結果型に集約 | + +## 網羅性の静的検査 + +```typescript +type Shape = + | { kind: "circle"; r: number } + | { kind: "rect"; w: number; h: number }; + +function area(s: Shape): number { + switch (s.kind) { + case "circle": return Math.PI * s.r ** 2; + case "rect": return s.w * s.h; + default: { + const _exhaustive: never = s; // ケース追加漏れをコンパイル時に検出 + return _exhaustive; + } + } +} +``` + +```typescript +// ❌ 対応表へ移すと、この検査が消える +const handlers: Record number> = { circle: ..., rect: ... }; +``` + +`Record` は任意の文字列を受けるため、ケースの追加漏れも綴り誤りも実行時まで +分からない。**判別可能ユニオンに対する分岐は、そのまま残す。** + +## 変化する値の対応表 + +一方、**変化する業務ルール**は対応表が適する。キーを閉じた型に固定すれば静的検査も残る。 + +```typescript +const DISCOUNT_RATES = { + gold: 0.2, silver: 0.1, bronze: 0, +} as const satisfies Record; // Rank に追加すると欠落を検出 + +const rate = DISCOUNT_RATES[rank]; +``` + +この表を実行時にロードするなら、型を生成していてもロード境界の検証は要る。 + +```typescript +// ❌ 型生成をスキーマ検証の代わりにしている。実体は何も検査されない +const rates = JSON.parse(raw) as Record; + +// ✅ ロード境界で検証してから使う(zod / JSON Schema) +const rates = RatesSchema.parse(JSON.parse(raw)); +``` + +## 並行と並列を混同しない + +`Promise.all` は待ち時間を重ねるだけで、計算時間は減らない。CPU を使う処理を並列化するには +worker が要る。「反復の実行方式」表の「並行処理」と「並列処理」は別の行である。 + +## 出典 + +- [TypeScript Handbook — Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) diff --git a/plugins/ndf-codex/skills/safe-refactoring/references/refactoring-catalog.md b/plugins/ndf-codex/skills/refactoring/references/refactoring-catalog.md similarity index 73% rename from plugins/ndf-codex/skills/safe-refactoring/references/refactoring-catalog.md rename to plugins/ndf-codex/skills/refactoring/references/refactoring-catalog.md index 6c3e5741..412a039b 100644 --- a/plugins/ndf-codex/skills/safe-refactoring/references/refactoring-catalog.md +++ b/plugins/ndf-codex/skills/refactoring/references/refactoring-catalog.md @@ -57,6 +57,32 @@ 分岐が 1 箇所なら分岐のままが読みやすい。**種類が増えるたびに複数箇所を直している**という 事実が、この手法の適用条件である。 +## 対応表への置き換え + +| 項目 | 内容 | +| --- | --- | +| 適用条件 | **変化する業務ルール**(料率・区分・しきい値・優先順位)が制御構文に埋まっている | +| 手順 | 値の対応を表として外へ出す → 未知の入力を失敗として扱う → 適用したルールの識別子と版を**表から取り出せる形にする** | +| やめる条件 | 値が変化しない。網羅性を静的に検査できている分岐である。表に版・スキーマ・検証を用意できない | + +「分岐が多いから表にする」ではない。**変化するから表にする**。判断の材料は +[data-representation.md](data-representation.md) の 3 表にある。 + +この手順で完了するのは、識別子と版を**記録できるデータ構造**を保つところまでである。実際に +記録を出す実装は振る舞いの変更なので、要件がある場合に別の変更として出す +([data-representation.md](data-representation.md) の「判断を記録できるようにする」)。 + +## 一括処理への置き換え + +| 項目 | 内容 | +| --- | --- | +| 適用条件 | 同種で独立した処理を 1 件ずつ繰り返しており、往復回数か実行時間が積み上がっている | +| 手順 | 一括入出力・一括演算・並行・並列のどれに移すかを決める → 置き換える → **計測して効果を確かめる** | +| やめる条件 | 逐次依存・早期終了・メモリ制約がある。移動先を言えない(高階反復への書き換えだけになる) | + +逐次実行のまま高階反復へ書き換えても実行の実体は変わらない。何が得られるかは +[data-representation.md](data-representation.md) の「反復の実行方式」表で確かめる。 + ## 戦略の切り出し | 項目 | 内容 | diff --git a/plugins/ndf-codex/skills/tdd-cycle/SKILL.md b/plugins/ndf-codex/skills/tdd-cycle/SKILL.md index e16ddd9d..249c224b 100644 --- a/plugins/ndf-codex/skills/tdd-cycle/SKILL.md +++ b/plugins/ndf-codex/skills/tdd-cycle/SKILL.md @@ -16,7 +16,7 @@ description: "Write a failing test first, then the smallest implementation that | Skill | 参照している内容 | 未追加のあいだの代替 | | --- | --- | --- | | `requirements-design` | 受け入れ条件の作り方 | 受け入れ条件を「観測可能・一意・テスト可能」な 1 文へ自分で書き下す | -| `safe-refactoring` | 構造改善と現状固定テスト | サイクル内の整理にとどめ、構造改善は別タスクへ切り出す | +| `refactoring` | 構造改善と現状固定テスト | サイクル内の整理にとどめ、構造改善は別タスクへ切り出す | | `quality-gates` | 全体テストの実行とカバレッジ閾値の判定 | 対象プロジェクトのカバレッジツール設定に従い、設定がなければ測定値の記録だけ行う | ## 適用しない対象 @@ -97,7 +97,7 @@ E ImportError: cannot import name 'validate' ← 期待と違う。先にこ ### 4. 整理する テストを**通ったまま**保って構造を整える。整理中にテストが落ちたら、整理をいったん戻す。 -コードスメル起点の本格的な構造改善は `safe-refactoring`※ に委ねる。 +コードスメル起点の本格的な構造改善は `refactoring`※ に委ねる。 ### 5. 次の条件へ進む @@ -123,7 +123,7 @@ E ImportError: cannot import name 'validate' ← 期待と違う。先にこ ## テストの乏しい既存コードでは順序が変わる 変更対象にテストがほとんどない場合、いきなり新しいテストを足すより、**現状の振る舞いを -固定するテスト**を先に置く。手順は `safe-refactoring`※ の現状固定テストに従う。 +固定するテスト**を先に置く。手順は `refactoring`※ の現状固定テストに従う。 ## テストダブルの優先順 diff --git a/plugins/ndf-kiro/README.md b/plugins/ndf-kiro/README.md index f03a1b29..0d033cba 100644 --- a/plugins/ndf-kiro/README.md +++ b/plugins/ndf-kiro/README.md @@ -12,7 +12,7 @@ cat plugins/ndf-kiro/VERSION # 導入済みプロジェクトの版数 python3 -c "import json;print(json.load(open('.kiro/agents/ndf.json'))['description'])" -# => NDF統合開発エージェント(Kiro CLI用 / v7.0.0) +# => NDF統合開発エージェント(Kiro CLI用 / v8.0.0) ``` `install.sh` は実行時にも `NDF バージョン: <版数>` を表示する。 @@ -28,7 +28,9 @@ Skill 名は変わらないため `/playwright-` まで打てば従来どおり bash plugins/playwright-kit-kiro/install.sh ``` -移行先の対応表は `.kiro/steering/ndf-policies.md`(`ndf-policies` skill から生成)にあります。 +移行先の対応表は予告どおり v8.0.0 で `ndf-policies` から削除したため、`.kiro/steering/ndf-policies.md` +にも含まれません。リポジトリ root の [README.md](../../README.md) の +「NDF v7.0.0 の主な変更(非互換)」を参照してください。 ## インストール diff --git a/plugins/ndf-kiro/VERSION b/plugins/ndf-kiro/VERSION index 66ce77b7..ae9a76b9 100644 --- a/plugins/ndf-kiro/VERSION +++ b/plugins/ndf-kiro/VERSION @@ -1 +1 @@ -7.0.0 +8.0.0 diff --git a/plugins/ndf-kiro/skills/development-workflow/SKILL.md b/plugins/ndf-kiro/skills/development-workflow/SKILL.md index 0409b880..275df320 100644 --- a/plugins/ndf-kiro/skills/development-workflow/SKILL.md +++ b/plugins/ndf-kiro/skills/development-workflow/SKILL.md @@ -47,7 +47,7 @@ NULL 許容列の追加)は `standard` として扱う。判定に迷う場合 ```text mode: standard 根拠: 注文確定の振る舞いを変更する。公開 API とスキーマは変えない -必須工程: requirements-design → implementation-plan → tdd-cycle → safe-refactoring(必要な場合) +必須工程: requirements-design → implementation-plan → tdd-cycle → refactoring → pr-review → quality-gates → plan-to-spec(仕様が変わった場合) ``` @@ -59,8 +59,8 @@ mode: standard | モード | 対象 | 必須工程 | | --- | --- | --- | | `light` | 文言、ドキュメント、設定、テストの追加など、本番の振る舞いも本番コードの構造も変えない局所変更 | 成功条件の確認、対象範囲の確定、限定的な検証と静的解析 | -| `standard` | 一般的な機能追加・バグ修正、テストが十分にある構造改善 | 仕様、計画、テスト駆動、構造改善(必要な場合)、レビュー、全体検証 | -| `architecture` | 公開インタフェース、移行を伴うスキーマ変更、認証、複数モジュール、重要なドメイン変更 | ドメインモデリング、設計判断の記録、設計レビュー、テスト駆動、契約テストと結合テスト、相互レビュー | +| `standard` | 一般的な機能追加・バグ修正、テストが十分にある構造改善 | 仕様、計画、テスト駆動、構造改善、レビュー、全体検証 | +| `architecture` | 公開インタフェース、移行を伴うスキーマ変更、認証、複数モジュール、重要なドメイン変更 | ドメインモデリング、設計判断の記録、設計レビュー、テスト駆動、構造改善、契約テストと結合テスト、相互レビュー | | `legacy-refactor` | テストが少ない既存コードの振る舞い維持型改善 | 構造分析、計画、現状固定テスト、段階的改善、レビュー、退行検証 | ## モードごとに起動する Skill @@ -70,8 +70,8 @@ mode: standard | 要求と受け入れ条件 | — | `requirements-design` | `requirements-design` | — | | 設計 | — | `implementation-plan` に代替案と採否を記録 | ドメインモデリングと設計レビュー(Release 2 で有効化) | `implementation-plan` に代替案と採否を記録 | | 計画 | — | `implementation-plan` | `implementation-plan` | `implementation-plan` | -| 実装 | 直接編集 | `tdd-cycle` | `tdd-cycle` | `safe-refactoring` | -| 構造改善 | — | `safe-refactoring`(必要な場合) | `safe-refactoring`(必要な場合) | `safe-refactoring` | +| 実装 | 直接編集 | `tdd-cycle` | `tdd-cycle` | `refactoring` | +| 構造改善 | — | `refactoring` | `refactoring` | `refactoring` | | レビュー | — | `pr-review` | `cross-review` | `pr-review` | | 完了判定 | `quality-gates` | `quality-gates` | `quality-gates` | `quality-gates` | | 確定仕様化 | — | `plan-to-spec`(仕様が変わった場合) | `plan-to-spec` | — | @@ -83,6 +83,13 @@ mode: standard レビュー段階は**明示的に呼ぶ**。自然文で「レビューして」と依頼すると、Claude Code では 組み込みの `code-review` が起動して判定の投稿経路が変わる。 +構造改善は**レビューと同じく、通す工程であって任意ではない**。動くコードが出た時点では整理が +済んでいないことを前提に置き、見つけたスメルは直す。対象は書き換えた行だけでなく、**その +呼び出し元・呼び出し先と、同じファイル・同じモジュールの関連箇所まで**を含む(範囲と例外は +`refactoring` の `references/code-smells.md`「手を付ける範囲」)。 + +`light` だけが工程ごと対象外である。本番コードの構造を変えない変更に構造改善の判断は要らない。 + ## 標準フロー この図は**工程の全体像**を表す。どの Skill を起動するかは前節の表が基準であり、図はその @@ -98,7 +105,8 @@ flowchart TD E --> F F --> G[実装計画] G --> H[失敗するテスト → 最小実装 → 整理] - H --> I[仕様適合レビュー] + H --> R[構造改善] + R --> I[仕様適合レビュー] I --> J[コード品質レビュー] J --> K[限定的な検証・静的解析] K --> N[全体テスト → ビルド・結合テスト] @@ -117,7 +125,8 @@ flowchart TD (K は変更箇所を 1 度実行する限定的な検証と静的解析だけを指す。依存パッケージの版更新だけは 例外として既存テスト一式を実行する — [references/workflow-modes.md](references/workflow-modes.md)) - `legacy-refactor` は A から C へ抜けて `standard` と同じ経路をたどり、**B(要求と受け入れ条件)と - M(確定仕様化)は通らない**。H は「現状固定テスト → 段階的改善」、I は「本番の振る舞いが + M(確定仕様化)は通らない**。H は「現状固定テスト」、R は「段階的改善」、I は「本番の振る舞いが + 変わっていないことの確認」として読む ## `architecture` モードの現状 diff --git a/plugins/ndf-kiro/skills/development-workflow/references/workflow-modes.md b/plugins/ndf-kiro/skills/development-workflow/references/workflow-modes.md index 84a29033..e9f2fc1e 100644 --- a/plugins/ndf-kiro/skills/development-workflow/references/workflow-modes.md +++ b/plugins/ndf-kiro/skills/development-workflow/references/workflow-modes.md @@ -67,9 +67,10 @@ - 目的: 受け入れ条件を満たし、退行を出さないこと - 受け入れ条件を先に作る。条件が作れない依頼は、質問して止まる -- 実装はテスト駆動で進める。構造改善が必要になったら差分を分ける +- 実装はテスト駆動で進める。**通したあとに構造改善の工程を通す**(手を付けない判断でもよいが、 + 工程は飛ばさない)。手を入れる場合は差分を分ける - 振る舞いを変えない構造変更のみの依頼は、受け入れ条件を「既存テストが通り続けること」とし、 - 実装は `safe-refactoring` の手順で進める + 実装は `refactoring` の手順で進める - 完了判定は全体テストまで通す ### `architecture` diff --git a/plugins/ndf-kiro/skills/ndf-policies/SKILL.md b/plugins/ndf-kiro/skills/ndf-policies/SKILL.md index 677d4eda..bf6de328 100644 --- a/plugins/ndf-kiro/skills/ndf-policies/SKILL.md +++ b/plugins/ndf-kiro/skills/ndf-policies/SKILL.md @@ -18,33 +18,18 @@ user-invocable: false 4. **マージ済みブランチには push しない。** 既存 PR の状態を確認し、マージ済みなら新ブランチ + 新 PR を作る(サフィックス `-v2`, `-v3`) 5. **revert を連鎖させない。** 最終的なあるべき状態を直接コミットする方が履歴上の意図が明確になり、後の cherry-pick も簡単になる -## v7.0.0 で移動した Skill(v8.0.0 で削除) +## v8.0.0 で改名した Skill(v9.0.0 で削除) -ブラウザ自動テストの 4 Skill を **`playwright-kit` プラグイン**へ分離した。**Skill 名は -変わらない**ため、`/playwright-` まで打てば従来どおり候補に出る。 +構造改善の Skill を **`/ndf:refactoring`** へ改名し、分岐・反復・定数の表現を決める観点を +統合した。引数と手順は変わらない。 | 旧コマンド | 移行先 | | --- | --- | -| `/ndf:playwright-planning` | `/playwright-kit:playwright-planning` | -| `/ndf:playwright-authoring` | `/playwright-kit:playwright-authoring` | -| `/ndf:playwright-evidence` | `/playwright-kit:playwright-evidence` | -| `/ndf:playwright-kit-ops` | `/playwright-kit:playwright-kit-ops` | - -利用するにはプラグインを別途インストールする。 - -```bash -# Claude Code -/plugin install playwright-kit@ai-plugins -# Codex -codex plugin add playwright-kit@ai-plugins -# Kiro CLI -bash plugins/playwright-kit-kiro/install.sh -``` - -分離したのは、Skill の `name` と `description` が起動時の一覧として常時注入され、その予算が -プラグイン横断で共有されるためである。ブラウザ自動テストは使う場面が限られる一方で MCP -ツールを多用するため frontmatter が大きく(4 個で約 2,400 文字)、全利用者へ常時注入する -取り分に見合わなかった。 - -v6.0.0 の対応表(`/ndf:review` → `/ndf:pr-review`)は、予告どおり本バージョンで削除した。 -v6.0.0 以前から移行する場合は v6.1.0 の `ndf-policies` を参照する。 +| `/ndf:safe-refactoring` | `/ndf:refactoring` | + +`safe-` を外したのは、`/refactoring` で一意に決まり、入力が短くなるためである。統合した観点は +`references/data-representation.md` にあり、スメル一覧からも参照される。 + +v7.0.0 の対応表(playwright 系 4 Skill の `playwright-kit` プラグインへの分離)は、予告どおり +本バージョンで削除した。v7.0.0 より前から移行する場合は、この対応表を持つ最後の配布版である +v7.0.0 の `ndf-policies` を参照する。 diff --git a/plugins/ndf-kiro/skills/pr-review/SKILL.md b/plugins/ndf-kiro/skills/pr-review/SKILL.md index fe188f1f..04e92849 100644 --- a/plugins/ndf-kiro/skills/pr-review/SKILL.md +++ b/plugins/ndf-kiro/skills/pr-review/SKILL.md @@ -69,7 +69,7 @@ PR 差分、または `--branch` 指定時は現在のブランチの差分を | 責務・凝集度・結合度 | 1 つの単位が複数の変更理由を持っていないか | | 依存の向き | 業務ロジックが外部の仕組みへ直接依存していないか。循環がないか | | 可読性・単純性 | 分岐の深さ、名前と実態の一致、不要な抽象化 | -| コードスメル | 重複、長すぎる単位、基本型への固執など(`safe-refactoring` の一覧) | +| コードスメル | 重複、長すぎる単位、基本型への固執など(`refactoring` の一覧) | | セキュリティ・性能 | 下の「具体的なチェックポイント」 | | テストが実装詳細に結合していないか | 内部呼び出し回数の検証、private への直接依存(`tdd-cycle` の脆いテスト) | diff --git a/plugins/ndf-kiro/skills/problem-solving/SKILL.md b/plugins/ndf-kiro/skills/problem-solving/SKILL.md index 80f40e7a..13aa2987 100644 --- a/plugins/ndf-kiro/skills/problem-solving/SKILL.md +++ b/plugins/ndf-kiro/skills/problem-solving/SKILL.md @@ -57,7 +57,7 @@ description: "Fix bugs and data inconsistencies upstream at the root cause. Use 対象にテストがほとんどない場合、再現テストを書く前に**現状の振る舞いを固定するテスト**を 置く。副作用を分離できず再現テストが書けない状態で修正すると、直したい振る舞い以外を -壊しても気づけない。手順は `safe-refactoring` の現状固定テストに従う。 +壊しても気づけない。手順は `refactoring` の現状固定テストに従う。 ```text 1. 変更対象の入口と副作用を洗い出す diff --git a/plugins/ndf-shared/skills/safe-refactoring/SKILL.md b/plugins/ndf-kiro/skills/refactoring/SKILL.md similarity index 79% rename from plugins/ndf-shared/skills/safe-refactoring/SKILL.md rename to plugins/ndf-kiro/skills/refactoring/SKILL.md index 55288890..4fff6d27 100644 --- a/plugins/ndf-shared/skills/safe-refactoring/SKILL.md +++ b/plugins/ndf-kiro/skills/refactoring/SKILL.md @@ -1,6 +1,6 @@ --- -name: safe-refactoring -description: "Change structure without changing behavior, guarded by tests. Use when cleaning up code or touching legacy code(リファクタリング・コードスメル・現状固定テスト)." +name: refactoring +description: "Change structure without changing behavior, guarded by tests, judging smells and how decisions are represented. Use when cleaning up code or touching legacy code(リファクタリング・コードスメル・分岐をデータ化・現状固定テスト)." --- # 安全な構造改善 @@ -8,6 +8,11 @@ description: "Change structure without changing behavior, guarded by tests. Use **テストがなければ、それは構造改善ではなく単なる編集である。** 振る舞いが変わっていない ことを示す手段がない書き換えは、この Skill の対象外として扱う。 +この工程は**レビューと同じく、実装のあとに必ず通す**。動くコードが出た時点では整理が済んで +いないことを前提に置く。対象は書き換えた行だけでなく、**その呼び出し元・呼び出し先と、同じ +ファイル・同じモジュールの関連箇所まで**を含む(範囲と例外は +[references/code-smells.md](references/code-smells.md) の「手を付ける範囲」)。 + ## 最初に決める 2 つのこと ### 1. 機能変更と構造改善を混ぜない @@ -40,7 +45,9 @@ description: "Change structure without changing behavior, guarded by tests. Use 1. **変更前に既存テストを実行する。** ここで落ちているものがあれば、先に報告する 2. スメルを 1 つ選ぶ(一覧は [references/code-smells.md](references/code-smells.md)) 3. 対応する手法を選ぶ([references/refactoring-catalog.md](references/refactoring-catalog.md)。 - スメル一覧で ★ が付いた手法はカタログに項目がなく、一覧の記述だけで進めてよい) + スメル一覧で ★ が付いた手法はカタログに項目がなく、一覧の記述だけで進めてよい)。 + 分岐・反復・定数を**何にどう置き換えるか**は + [references/data-representation.md](references/data-representation.md) で決める 4. **1 手だけ適用する** 5. テストを実行する。落ちたら直前の 1 手を戻す 6. 通ったらコミットする(1 手 = 1 コミットを既定とする) @@ -109,7 +116,7 @@ flowchart TD - 固定テストが書けない(副作用が分離できない、実行に外部環境が要る) - 1 手で終わらず、テストを通すために本番コードの分岐を足す必要が出た - 改善の途中で仕様の不明点が出た(`requirements-design` へ戻る) -- 差分が依頼範囲を超えて広がった +- 差分が [code-smells.md](references/code-smells.md) の「手を付ける範囲」を超えて広がった 止めたときは「どこまで安全な状態か」を明示する。中途半端な状態を「あとで直す」前提で 残さない。 @@ -130,4 +137,11 @@ flowchart TD - [references/code-smells.md](references/code-smells.md) — 構造改善の起点になる兆候 - [references/refactoring-catalog.md](references/refactoring-catalog.md) — 手法と適用条件 +- [references/data-representation.md](references/data-representation.md) — 分岐・反復・定数を何にどう置き換えるか +- 言語ごとの手段 — **対象の言語のファイルだけを読む** + - [references/lang-python.md](references/lang-python.md) + - [references/lang-javascript.md](references/lang-javascript.md) + - [references/lang-typescript.md](references/lang-typescript.md) + - [references/lang-php.md](references/lang-php.md) + - 一覧にない言語は、`data-representation.md` の判定表から自分で対応付ける - [references/characterization-tests.md](references/characterization-tests.md) — 現状固定テストの作り方 diff --git a/plugins/ndf-kiro/skills/safe-refactoring/references/characterization-tests.md b/plugins/ndf-kiro/skills/refactoring/references/characterization-tests.md similarity index 100% rename from plugins/ndf-kiro/skills/safe-refactoring/references/characterization-tests.md rename to plugins/ndf-kiro/skills/refactoring/references/characterization-tests.md diff --git a/plugins/ndf-codex/skills/safe-refactoring/references/code-smells.md b/plugins/ndf-kiro/skills/refactoring/references/code-smells.md similarity index 59% rename from plugins/ndf-codex/skills/safe-refactoring/references/code-smells.md rename to plugins/ndf-kiro/skills/refactoring/references/code-smells.md index 325f04ad..ef77a7e5 100644 --- a/plugins/ndf-codex/skills/safe-refactoring/references/code-smells.md +++ b/plugins/ndf-kiro/skills/refactoring/references/code-smells.md @@ -1,7 +1,7 @@ # コードスメル -スメルは「直すべき欠陥」ではなく **調べる価値がある兆候** である。見つけても、変更の -理由(機能追加・不具合修正・読みにくさ)が伴っていなければ手を付けない。 +スメルは「直すべき欠陥」ではなく **調べる価値がある兆候** である。兆候を見つけたら、下の +「手を付ける範囲」に入っているかを確かめ、入っていれば直す。 行数などの数値規則は使わない。判断は凝集度・結合度・変更理由・認知負荷・テスト容易性で行う。 @@ -26,10 +26,16 @@ | 例外の飲み込み | 捕まえて何もしない、ログだけ出して続行する | 失敗が沈黙し、原因が追えない | ★呼び出し元へ伝える、または明示的に扱う | | 条件分岐の連鎖 | 型・状態ごとの分岐が複数箇所で繰り返される | 種類を増やすたびに全箇所を直す | 多態による分岐の置き換え | | 設定の散在 | 同じ設定値が複数の場所で定義される | どちらが効くか分からない | ★定義を 1 箇所へ寄せる | +| 業務ルールの埋め込み | 料率・区分・しきい値・優先順位が制御構文の中に書かれている | 一覧できない。値を変えるだけの修正にコード変更と再配備が要る | 対応表への置き換え | +| 一件ずつの反復 | 同種で独立した処理を 1 件ずつ繰り返す(1 件ごとの問い合わせを含む) | 往復回数か実行時間が件数に比例して積み上がる | 一括処理への置き換え | +| 検証のない外部化 | 設定・マスタへ出したデータに、スキーマ・版・検証がない | 壊れた値が実行時まで通る。どの版が適用されたか追えない | ★スキーマと版を与え、読み込み境界で検証する | -★ の 4 件をカタログに置いていないのは、手順が「1 箇所へ寄せる/呼び出し元へ返す」で尽きて -おり、適用条件も上の「何が問題か」以外に無いためである。カタログを探さず、この表の記述で -そのまま進めてよい。ただし共通化だけは判断を誤りやすいため、次節の見分け方に従う。 +★ の 5 件をカタログに置いていないのは、手順が「1 箇所へ寄せる/呼び出し元へ返す/検証を足す」 +で尽きており、適用条件も上の「何が問題か」以外に無いためである。カタログを探さず、この表の +記述でそのまま進めてよい。ただし共通化だけは判断を誤りやすいため、次節の見分け方に従う。 + +下 3 件は「どう持つか」の判断を伴う。表現の選び方と、外部化してよい条件は +[data-representation.md](data-representation.md) にある。 ## 共通化してよい重複の見分け方 @@ -61,11 +67,33 @@ 「何を解決したか」と「外し方」を残す。決まった書式は今のところ無い(Release 2 で追加予定の `object-design` Skill で扱う想定だが、現時点では未実装)。 -## スメルに手を付けない場合 +## 手を付ける範囲 + +**今回書き換えた行だけに閉じない。** そこに閉じると、読みにくい領域は読みにくいまま残り続ける。 +一方で無制限に広げると差分がレビューできなくなる。次を境界にする。 + +| 範囲 | 扱い | +| --- | --- | +| 今回変更した関数・クラス | 直す | +| その呼び出し元・呼び出し先 | 直す。変更の影響を読むために通る範囲であり、テストも通っている | +| 同じファイル・同じモジュールの関連箇所 | 直す。差分は分ける | +| そこから遠い領域 | 対象外。後続の作業として記録する | + +範囲に入っていることは、直す理由にはならない。**この文書のスメル一覧のどれにも当たらない箇所は +直さない。**「読みやすくなりそう」だけで手を入れると、レビューできない差分になる。手順(1 手ずつ・ +現状固定テストで守る・差分を分ける)は、範囲を広げても変わらない。 + +広げた分は**別のコミットに切る**(「差分の切り方」)。機能変更と構造改善が同じ差分に入ると、 +レビュアーは意図した変更と構造改善の事故を区別できない。上表の範囲を超えて広がるなら、 +別の変更として出す。 + +## 手を付けない場合 + +範囲に入っていても、次は対象から外す。 -- 変更予定のない領域(読みにくいだけで、今回の変更に関係しない) - 生成物・外部から取り込んだコード(上流で直す) - 削除予定の領域 +- 振る舞いが変わっていないことを示す手段がなく、現状固定テストも書けない箇所 + (`SKILL.md` の「途中で止める条件」) -手を付けないと決めた場合、指摘だけを残すよりも**何もしない**方がよい。残す価値がある -指摘は、後続の作業として記録する。 +外したものは、指摘だけを残さず**後続の作業として記録する**。 diff --git a/plugins/ndf-kiro/skills/refactoring/references/data-representation.md b/plugins/ndf-kiro/skills/refactoring/references/data-representation.md new file mode 100644 index 00000000..213f15bf --- /dev/null +++ b/plugins/ndf-kiro/skills/refactoring/references/data-representation.md @@ -0,0 +1,186 @@ +# 分岐・反復・定数の表現 + +`code-smells.md` の「業務ルールの埋め込み」「一件ずつの反復」「検証のない外部化」を見つけた +ときに、**何にどう置き換えるか**を決めるための判断材料。 + +置き換えの目的は 2 つに尽きる。どちらにも近づかない置き換えは行わない。 + +| 時点 | 近づける状態 | +| --- | --- | +| 実行前 | どの入力がどの結果になるかを実装を読まずに一覧でき、扱っていない入力があれば検査で分かる | +| 実行後 | どの判断がなぜその結果になったかを、記録だけで再現できる | + +## 移す対象を決める + +> **変化する知識はデータへ。安定した機構と不変条件はコードへ。** + +判断の軸は「分岐が多いか」ではなく「**その知識が変化するか**」である。変化しない知識をデータへ +出すと、検査できる場所が減るだけになる。 + +| データへ移す | コードに残す | +| --- | --- | +| 業務ルール、料率、しきい値、優先順位 | 不変条件、安全性の制約 | +| 分類・対応表(値から値への写像) | 機構(どう読み、どう書き、どう失敗を扱うか) | +| 環境ごとに変わる値 | 型・スキーマ・境界の定義 | +| 利用者が管理するカテゴリ | プロトコル、閉じた状態集合 | + +## 手を付けないもの + +次はいずれも**そのままでよい**。スメルとして拾わない。 + +- 単純なガード節の条件分岐(早期リターンによる前提条件の検査) +- 閉じた型に対する網羅的な分岐(静的に網羅性を検査できるもの) +- 逐次依存・早期終了・ストリーム処理・メモリ制約下の明示的なループ +- 不変条件・プロトコル・閉じた状態集合を表す定数と列挙型 +- 原子性・整合性・安全性を守るための即時打ち切りと巻き戻し。全体を 1 単位として成否を決める + 処理、後続に不正な入力が波及する処理、安全性検査の失敗はこれにあたる + +## 改善にならない置き換え + +形だけが変わり、上の 2 つの状態に近づかないもの。着手する前に見分ける。 + +| 置き換え | 何が起きるか | +| --- | --- | +| 条件分岐を機械的に対応表へ移す | 前提条件の検査は、対応表より分岐のほうが意図が明確。網羅性を検査できていた分岐を型情報のない対応表へ移すと、その検査が消える | +| 逐次実行のまま高階反復へ書き換える | 実行の実体は変わらない。一括実行にも並列実行にもなっていない | +| 定数をすべて外部設定へ出す | 条件・参照・既定値が設定側に積み上がり、設定自体が独自の言語になる。変更に必要な知識はむしろ増える | + +## 分岐の種類 → 適切な表現 + +| 分岐の種類 | 適切な表現 | +| --- | --- | +| 値から値への単純な対応 | 対応表(写像) | +| 処理方式の切り替え | 登録表(識別子から処理への対応) | +| 条件の組み合わせが多い業務判断 | 決定表、ルールエンジン | +| 認可・制約・組織ポリシー | ポリシーエンジン | +| 時系列で状態が変化する処理 | 状態機械 | +| 閉じた少数の型分岐 | 型に対する網羅的な分岐(置き換えない) | +| 前提条件の検査 | ガード節(置き換えない) | + +## 反復の実行方式 → 得られるもの + +置き換えても何も得られない場合を見分けるための表である。分類の軸は構文ではなく**実行方式**で +ある。 + +| 実行方式 | 実行の実体 | 得られるもの | +| --- | --- | --- | +| 逐次実行 | 1 件ずつ順に処理する | **なし**(記述の変更のみ) | +| 一括演算 | 基盤側で一括実行 | 実行時間 | +| 一括入出力 | 呼び出し回数の削減 | 往復回数(大量データでは最大の効果) | +| 並行処理 | 待ち時間の重ね合わせ | 待ち時間。計算時間は減らない | +| 並列処理 | 複数の実行資源で同時 | 計算時間 | +| 分散データフロー | 基盤が分割・再試行・集約 | 規模と耐障害性 | + +高階反復は構文であって実行方式ではない。**逐次実行のまま構文だけを高階反復へ置き換えても、この +表のどの行にも移動していない。** 高階反復の形のまま遅延・並行・並列・分散で実行する仕組みを持つ +言語もあるが、得られるものを決めるのはその実行方式であって構文ではない。**移すなら、移動先の行 +を言えなければならない。** 置き換えたら計測し、効果が出ていなければ戻す。 + +一括演算の基盤は言語によって有無が異なる。基盤がない言語では、一括入出力(往復回数の削減)が +主戦場になる。 + +## 定数の性質 → 置き場所 + +| 値の性質 | 置き場所 | +| --- | --- | +| 数学的・技術的な不変条件 | コード内の定数 | +| 閉じた状態集合・プロトコル | 型、列挙型、スキーマ | +| 頻繁に変わる業務ルール | 決定表、ポリシー、設定 | +| 環境ごとに変わる値 | 配備設定、環境変数 | +| 利用者が管理するカテゴリ | 参照テーブル(データベース) | +| 表示名・文言 | 多言語化資源、コンテンツ | +| 安全性の絶対制約 | コード・型・スキーマ・ポリシーの複数層 | + +不透明なコード値(`status == 3`)は避ける。ただし**意味の分かる識別子を持つ列挙型まで置き換え +ない**。型のない文字列にすると、綴り誤りと未処理ケースの発見が実行時まで遅れる。 + +## 外部化してよい条件 + +データへ移すなら、移した先で次を用意できることが条件になる。用意できないなら、コードに残した +ほうが検査できる範囲は広い。 + +- [ ] スキーマがある +- [ ] 型またはバリデーションで検査される +- [ ] 版を持ち、変更履歴が追える +- [ ] 競合するルール・到達不能なルールを検出できる +- [ ] テストがある +- [ ] 変更の適用手順(移行)がある +- [ ] 誰が何を変更したか監査できる +- [ ] 実行時に、適用したルールの識別子と理由を記録する + +外部化したデータをどう守るかは、**そのデータをいつ読むか**で決まる。 + +1. **データごとビルド時に組み込める**なら、スキーマから型・定数を生成して取り込む。外部化しても + 静的な検査が残るため、これが選べるなら最初に選ぶ +2. **データを実行時にロードする**なら、**ロード境界をスキーマ検証で守る**。型を生成していても + 同じで、生成した型は、ロードした値がその形である保証を与えない +3. どちらも満たせないなら、外部化しない + +型情報を伴わずに実行時ロードした対応表は、静的解析の対象から外れる。どの検査がどこまで効くかは +言語によって違う。**対象の言語のファイルだけを読む** — +[lang-python.md](lang-python.md) / [lang-javascript.md](lang-javascript.md) / +[lang-typescript.md](lang-typescript.md) / [lang-php.md](lang-php.md)。 + +## 判断を記録できるようにする + +対応表への置き換えは、**どのルールが適用されたかを記録できる構造を保って完了**である。値を +外へ出した分だけ「なぜこの結果になったか」が追いにくくなるので、表から識別子と版を取り出せる +形を崩さない。 + +記録を出す要件があるなら、1 つの判断につき 1 件、入力・決定・理由・実行文脈をまとめた +構造化イベントを出す。 + +```json +{ + "event_name": "discount_decided", + "customer_id": "cus_123", + "input": { "rank": "gold", "purchase_amount": 120000 }, + "decision": { + "discount_rate": 0.2, + "rule_id": "customer-rank-discount", + "rule_version": "2026-08-01", + "reason": "customer rank is gold" + }, + "execution": { "code_version": "a13fd82", "duration_ms": 4, "trace_id": "..." } +} +``` + +- 属性は、絞り込み・グループ化・集計・相関に使える形にする +- 適用したルールの識別子と版を含めると、結果に至った経緯を後から再現できる +- 個人情報と秘密情報の扱いは `logging-guidelines` に従う + +**記録の追加は振る舞いの変更である。** 構造改善と同じ差分に混ぜず、コミットを分ける。 + +一括処理への置き換えでは、終了時に失敗の集計も出す。 + +```json +{ + "event_name": "import_completed", + "total": 12000, "succeeded": 11940, "failed": 60, + "failures_by_reason": { "invalid_date": 48, "missing_customer": 12 }, + "sample_failed_ids": ["row_88", "row_312", "row_940"] +} +``` + +項目が独立に処理できるのに最初の失敗で全体を止めると、どこまで進んだかが分からなくなる。 +全体を 1 単位として成否を決める処理では、打ち切って巻き戻すのが正しい。その場合は**打ち切った +位置・理由・巻き戻しの範囲**を同じ構造化イベントに含める。 + +## 例: 業務ルールの埋め込みを置き換える + +```text +❌ 変化する料率が制御構文へ埋まっている + if rank == "gold" -> rate = 0.2 + else if rank == "silver" -> rate = 0.1 + else -> rate = 0 + +✅ 対応表として持ち、既定と未知の扱いを明示する + rates = { gold: 0.2, silver: 0.1, bronze: 0.0 } # 版とスキーマを持つ + rate = rates[rank](未知の階級は失敗として扱い、握りつぶさない) +``` + +置き換えた結果、次が言えるようになっていなければ手を付けた意味がない。 + +- 料率の一覧を**コードを読まずに**確認できる +- 新しい階級の追加が、分岐の追加ではなくデータの追加になる +- どの階級にどの版の料率が適用されたか、記録から再現できる diff --git a/plugins/ndf-kiro/skills/refactoring/references/lang-javascript.md b/plugins/ndf-kiro/skills/refactoring/references/lang-javascript.md new file mode 100644 index 00000000..2c4c0b66 --- /dev/null +++ b/plugins/ndf-kiro/skills/refactoring/references/lang-javascript.md @@ -0,0 +1,45 @@ +# JavaScript での手段 + +[data-representation.md](data-representation.md) で選んだ表現を、JavaScript の機能へ対応付ける。 + +| 判定 | 手段 | +| --- | --- | +| 値から値への対応 | `Object.freeze` のオブジェクト | +| 処理方式の切り替え | 関数を値に持つオブジェクト | +| 閉じた状態集合 | 凍結した定数オブジェクト | +| 網羅性の静的検査 | **手段なし**(実行時に失敗させる) | +| 不変性 | `Object.freeze`(凍結はトップレベルのみ)。入れ子まで不変にするなら再帰的に凍結するか、構造共有の不変データ構造を使う | +| スキーマ検証 | zod / JSON Schema | +| 一括処理 | 一括 API / `Promise.all`(並行) | +| 失敗の集計 | 失敗を集めて返す | + +## 静的な網羅性検査が効かないことが判定を変える + +型注釈がないため、**閉じた状態集合に対する分岐の網羅性を実行前に検査できない**。これが +2 つの判定を変える。 + +- 「手を付けないもの」にある「閉じた型に対する網羅的な分岐(静的に網羅性を検査できるもの)」 + の条件が成立しない。分岐をそのまま残しても追加漏れは実行時まで分からないので、**未知の + ケースで必ず失敗させる**書き方を併せて用意する +- 外部化した対応表を守る手段が実行時のスキーマ検証しかない。**静的検査で埋め合わせができない + 分、スキーマ検証の必要性が高い** + +```javascript +// ❌ 未知のケースが undefined として下流へ流れ、どこで壊れたか分からなくなる +const RATES = { gold: 0.2, silver: 0.1 }; +return RATES[rank]; + +// ✅ 未知のケースをその場で失敗させる +const RATES = Object.freeze({ gold: 0.2, silver: 0.1, bronze: 0 }); +if (!Object.hasOwn(RATES, rank)) throw new Error(`unknown rank: ${rank}`); +return RATES[rank]; +``` + +JSDoc の型注釈と `checkJs` を入れられるなら、静的検査が使える状態に戻せる。その場合の書き方は +[lang-typescript.md](lang-typescript.md) を読む。ただし実行時にロードするならロード境界の +検証は同じく要る(JSDoc の型は実体を検査しない)。 + +## 並行と並列を混同しない + +`Promise.all` は待ち時間を重ねるだけで、計算時間は減らない。CPU を使う処理を並列化するには +worker が要る。「反復の実行方式」表の「並行処理」と「並列処理」は別の行である。 diff --git a/plugins/ndf-kiro/skills/refactoring/references/lang-php.md b/plugins/ndf-kiro/skills/refactoring/references/lang-php.md new file mode 100644 index 00000000..7b5c958f --- /dev/null +++ b/plugins/ndf-kiro/skills/refactoring/references/lang-php.md @@ -0,0 +1,146 @@ +# PHP での手段 + +[data-representation.md](data-representation.md) で選んだ表現を、PHP の機能へ対応付ける。 +以下は **8.1 以降**を前提とする(8.0 以下は最終節)。 + +| 判定 | 手段 | +| --- | --- | +| 値から値への対応 | 連想配列 / `match` | +| 処理方式の切り替え | first-class callable 構文 `foo(...)` | +| 閉じた状態集合 | backed enum | +| 網羅性の静的検査 | PHPStan `match.unhandled` | +| 不変性 | `readonly` プロパティ | +| スキーマ検証 | JSON Schema / Valinor | +| 一括処理 | **一括入出力**(標準に一括演算の基盤はない) | +| 失敗の集計 | 失敗を集めて返す | + +8.1 で backed enum・`readonly` プロパティ・first-class callable 構文が入り、**データ化しても +静的解析が効く**範囲が広がった。 + +## 網羅性の静的検査 + +```php +enum Rank: string { + case Gold = 'gold'; + case Silver = 'silver'; + case Bronze = 'bronze'; +} + +function label(Rank $rank): string { + return match ($rank) { + Rank::Gold => 'ゴールド', + Rank::Silver => 'シルバー', + Rank::Bronze => 'ブロンズ', + }; +} +``` + +`match` は一致する腕がなければ実行時に `\UnhandledMatchError` を投げる。さらに **PHPStan が +`match.unhandled`("Match expression does not handle remaining value")として静的に検出する**。 +`Rank` にケースを足すと、`default` を書いていない `match` が解析で落ちる。 + +```php +// ❌ 連想配列へ移すと、検査は弱くなる +$labels = ['gold' => 'ゴールド', 'silver' => 'シルバー']; +return $labels[$rank->value]; +``` + +どこまで弱くなるかは、**対応表が静的に見えているか**で決まる(PHPStan 2.2 で実測)。 + +| 書き方 | level 5 | level max | +| --- | --- | --- | +| `match`(`default` なし) | `match.unhandled` で検出 | 同左 | +| その場に書いた連想配列 | 検出なし | `offsetAccess.notFound` で検出 | +| 外部から渡した対応表(`array`) | 検出なし | **検出なし** | + +**この表が測っているのは、型情報を伴わずに実行時ロードした場合である**(3 行目は +`array` という幅の広い型で対応表を受け取る形)。この形にすると、変化する業務 +ルールは定義ごと静的解析の視界から外れるため、「外部化してよい条件」(スキーマ・ +バリデーション・版・テスト)が必須になる。**静的検査で守れなくなった分を、スキーマ検証で +埋める。** + +一方、スキーマから backed enum や定数クラスを**生成**してビルド時に取り込めば、外部化しても +`match.unhandled` の検査は残る。ただし**データを実行時にロードするなら、型を生成していても +ロード境界の検証は要る**。`json_decode` の戻り値に `@var GeneratedShape` を付けるだけでは、 +静的解析が信じるだけで実体は検査されない。`Valinor` などのマッパか JSON Schema 検証を通す。 + +値が固定で外部化する理由がないなら、`match` のまま置くほうが分析可能性は高い。 + +`default` を書くとケース追加漏れが検出されなくなる。**閉じた列挙に対する `match` に +`default` を置かない**(未知の入力を扱う必要があるなら、それは閉じた列挙ではない)。 + +## 処理方式の切り替え + +```php +// first-class callable 構文。静的解析が参照先を追える +$handlers = [ + 'csv' => $this->importCsv(...), + 'json' => $this->importJson(...), +]; +($handlers[$format] ?? throw new UnsupportedFormat($format))($payload); +``` + +文字列やコールバック配列(`'importCsv'` / `[$this, 'importCsv']`)で書くと解析が追えない。 +**データ化しても分析可能性を落とさない書き方を選ぶ。** + +## 一括処理は入出力の問題である + +**PHP 標準に一括演算の基盤はない。** `array_map` は逐次実行の高階反復であり、「反復の実行方式」 +表のどの行にも移動しない。`array_column` はコールバックを取らない組み込みの列抽出で、走査は逐次 +のままだが PHP レベルのループとは実装が異なるため、**計測して選ぶ**。数値計算向けの拡張や +ライブラリを導入すれば一括演算に移れる場合はあるので、採用するなら**効果を計測してから決める**。 + +```php +// ❌ ループを array_map に変えても、実行の実体は変わらない +$totals = array_map(fn($o) => $o->amount * 1.08, $orders); +``` + +PHP で効くのは**一括入出力**、すなわち往復回数の削減である。 + +```php +// ❌ N+1。1 行ごとに問い合わせる +foreach ($orderIds as $id) { $rows[] = $repo->find($id); } + +// ✅ 一括取得。往復が 1 回になる +$rows = $repo->findByIds($orderIds); + +// ✅ 一括挿入。件数に比例した往復をなくす +$repo->insertMany($rows); // 大量件数は chunk して分割する +``` + +大量データを扱うときは、`yield` による逐次生成でメモリを一定に保つ。これは「逐次依存・ +メモリ制約下の明示的なループ」にあたり、**そのままでよい**。 + +## 不変性 + +```php +final class Discount { + public function __construct( + public readonly string $ruleId, + public readonly string $ruleVersion, + public readonly float $rate, + ) {} +} +``` + +判断の結果を `readonly` の値として持つと、記録に必要なルールの識別子と版を持ち回れる。 + +## 8.0 以下 + +backed enum・`readonly`・first-class callable 構文がいずれも使えない。代替は次のとおりで、 +**いずれも静的検査は弱くなる**。 + +| 8.1+ | 8.0 以下の代替 | +| --- | --- | +| backed enum | クラス定数 + 値オブジェクト | +| `match` の網羅性検査 | `switch` + `default` で例外を投げる(実行時検出のみ) | +| `readonly` プロパティ | `private` + getter のみ | +| `foo(...)` | `Closure::fromCallable('foo')` | + +閉じた状態集合は 8.1 以降と同じくコード側に置く。変わるのは**外部化した業務ルールの守り方**で、 +生成した定数クラスに対する網羅性検査が効かないぶん、実行時のスキーマ検証への依存が高くなる。 + +## 出典 + +- [PHP 8.1 リリースアナウンス](https://www.php.net/releases/8.1/en.php) +- [PHPStan `match.unhandled`](https://phpstan.org/error-identifiers/match.unhandled) diff --git a/plugins/ndf-kiro/skills/refactoring/references/lang-python.md b/plugins/ndf-kiro/skills/refactoring/references/lang-python.md new file mode 100644 index 00000000..b08108d9 --- /dev/null +++ b/plugins/ndf-kiro/skills/refactoring/references/lang-python.md @@ -0,0 +1,85 @@ +# Python での手段 + +[data-representation.md](data-representation.md) で選んだ表現を、Python の機能へ対応付ける。 + +| 判定 | 手段 | +| --- | --- | +| 値から値への対応 | `dict` / `Mapping` | +| 処理方式の切り替え | 関数を値に持つ `dict` | +| 閉じた状態集合 | `Enum` / `Literal` | +| 網羅性の静的検査 | mypy + `assert_never` | +| 不変性 | `@dataclass(frozen=True)` | +| スキーマ検証 | pydantic / jsonschema | +| 一括処理 | NumPy / pandas の**ベクトル化演算**(一括演算) | +| 失敗の集計 | 失敗を集めて返す | + +## 網羅性の静的検査 + +```python +from enum import Enum +from typing import assert_never # 3.11+(それ以前は typing_extensions) + +class Rank(Enum): + GOLD = "gold" + SILVER = "silver" + BRONZE = "bronze" + +def label(rank: Rank) -> str: + match rank: + case Rank.GOLD: return "ゴールド" + case Rank.SILVER: return "シルバー" + case Rank.BRONZE: return "ブロンズ" + case _: assert_never(rank) # ケース追加漏れを mypy が検出 +``` + +`Rank` に階級を足すと mypy が `assert_never` の行で型エラーを出す。**この検査が効いている +分岐は、`dict` へ移さない。** + +外部化した値を実行時にロードするなら、型注釈を用意していてもロード境界の検証は要る。 +`cast(Rates, json.load(f))` は mypy を黙らせるだけで実体を検査しないので、`pydantic` の +`TypeAdapter` や `jsonschema` を通してから使う。 + +## 一括演算とその反例 + +Python は一括演算の基盤(NumPy)を持つ数少ない言語である。ただし**「ループを消したこと」と +「一括演算になったこと」は別**である。 + +```python +# ❌ 逐次。要素ごとに Python のバイトコードを実行する +result = [x * 1.08 for x in prices] + +# ❌ 高階反復。上と実行の実体は変わらない +result = list(map(lambda x: x * 1.08, prices)) + +# ❌ np.vectorize も同じ。公式が明言している +# "provided primarily for convenience, not for performance. +# The implementation is essentially a for loop." +result = np.vectorize(lambda x: x * 1.08)(prices) + +# ✅ 一括演算。C 側で一括実行される +result = prices * 1.08 +``` + +pandas の `.apply()` も、Python の関数を要素・行・列のいずれかの単位で呼ぶ経路である限りは +一括演算ではない(ufunc を渡すなど、内部で一括実行に落ちる経路もある)。**置き換えたら +計測する。** 速くならないなら、「反復の実行方式」表の行を移動できていない。 + +## 失敗の集計 + +```python +def import_rows(rows): + ok, failures = [], [] + for i, row in enumerate(rows): + try: + ok.append(parse(row)) + except ValueError as e: + failures.append({"index": i, "reason": type(e).__name__, "id": row.get("id")}) + return ok, failures # 呼び出し側が件数・種類・対象を報告できる +``` + +例外を握りつぶさず、最初の失敗で打ち切らない。ここは明示的なループでよい(逐次依存では +ないが、失敗の収集がある)。 + +## 出典 + +- [numpy.vectorize — 性能目的ではないという公式注記](https://numpy.org/doc/stable/reference/generated/numpy.vectorize.html) diff --git a/plugins/ndf-kiro/skills/refactoring/references/lang-typescript.md b/plugins/ndf-kiro/skills/refactoring/references/lang-typescript.md new file mode 100644 index 00000000..6759eaae --- /dev/null +++ b/plugins/ndf-kiro/skills/refactoring/references/lang-typescript.md @@ -0,0 +1,73 @@ +# TypeScript での手段 + +[data-representation.md](data-representation.md) で選んだ表現を、TypeScript の機能へ対応付ける。 +型注釈を持たない JavaScript は [lang-javascript.md](lang-javascript.md) を読む。 + +| 判定 | 手段 | +| --- | --- | +| 値から値への対応 | `Record` + `as const` | +| 処理方式の切り替え | ハンドラの `Record` | +| 閉じた状態集合 | 判別可能ユニオン | +| 網羅性の静的検査 | `never` への代入 | +| 不変性 | `readonly` / `as const` | +| スキーマ検証 | zod / JSON Schema | +| 一括処理 | 一括 API / `Promise.all`(並行) | +| 失敗の集計 | 結果型に集約 | + +## 網羅性の静的検査 + +```typescript +type Shape = + | { kind: "circle"; r: number } + | { kind: "rect"; w: number; h: number }; + +function area(s: Shape): number { + switch (s.kind) { + case "circle": return Math.PI * s.r ** 2; + case "rect": return s.w * s.h; + default: { + const _exhaustive: never = s; // ケース追加漏れをコンパイル時に検出 + return _exhaustive; + } + } +} +``` + +```typescript +// ❌ 対応表へ移すと、この検査が消える +const handlers: Record number> = { circle: ..., rect: ... }; +``` + +`Record` は任意の文字列を受けるため、ケースの追加漏れも綴り誤りも実行時まで +分からない。**判別可能ユニオンに対する分岐は、そのまま残す。** + +## 変化する値の対応表 + +一方、**変化する業務ルール**は対応表が適する。キーを閉じた型に固定すれば静的検査も残る。 + +```typescript +const DISCOUNT_RATES = { + gold: 0.2, silver: 0.1, bronze: 0, +} as const satisfies Record; // Rank に追加すると欠落を検出 + +const rate = DISCOUNT_RATES[rank]; +``` + +この表を実行時にロードするなら、型を生成していてもロード境界の検証は要る。 + +```typescript +// ❌ 型生成をスキーマ検証の代わりにしている。実体は何も検査されない +const rates = JSON.parse(raw) as Record; + +// ✅ ロード境界で検証してから使う(zod / JSON Schema) +const rates = RatesSchema.parse(JSON.parse(raw)); +``` + +## 並行と並列を混同しない + +`Promise.all` は待ち時間を重ねるだけで、計算時間は減らない。CPU を使う処理を並列化するには +worker が要る。「反復の実行方式」表の「並行処理」と「並列処理」は別の行である。 + +## 出典 + +- [TypeScript Handbook — Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) diff --git a/plugins/ndf-claude/skills/safe-refactoring/references/refactoring-catalog.md b/plugins/ndf-kiro/skills/refactoring/references/refactoring-catalog.md similarity index 73% rename from plugins/ndf-claude/skills/safe-refactoring/references/refactoring-catalog.md rename to plugins/ndf-kiro/skills/refactoring/references/refactoring-catalog.md index 6c3e5741..412a039b 100644 --- a/plugins/ndf-claude/skills/safe-refactoring/references/refactoring-catalog.md +++ b/plugins/ndf-kiro/skills/refactoring/references/refactoring-catalog.md @@ -57,6 +57,32 @@ 分岐が 1 箇所なら分岐のままが読みやすい。**種類が増えるたびに複数箇所を直している**という 事実が、この手法の適用条件である。 +## 対応表への置き換え + +| 項目 | 内容 | +| --- | --- | +| 適用条件 | **変化する業務ルール**(料率・区分・しきい値・優先順位)が制御構文に埋まっている | +| 手順 | 値の対応を表として外へ出す → 未知の入力を失敗として扱う → 適用したルールの識別子と版を**表から取り出せる形にする** | +| やめる条件 | 値が変化しない。網羅性を静的に検査できている分岐である。表に版・スキーマ・検証を用意できない | + +「分岐が多いから表にする」ではない。**変化するから表にする**。判断の材料は +[data-representation.md](data-representation.md) の 3 表にある。 + +この手順で完了するのは、識別子と版を**記録できるデータ構造**を保つところまでである。実際に +記録を出す実装は振る舞いの変更なので、要件がある場合に別の変更として出す +([data-representation.md](data-representation.md) の「判断を記録できるようにする」)。 + +## 一括処理への置き換え + +| 項目 | 内容 | +| --- | --- | +| 適用条件 | 同種で独立した処理を 1 件ずつ繰り返しており、往復回数か実行時間が積み上がっている | +| 手順 | 一括入出力・一括演算・並行・並列のどれに移すかを決める → 置き換える → **計測して効果を確かめる** | +| やめる条件 | 逐次依存・早期終了・メモリ制約がある。移動先を言えない(高階反復への書き換えだけになる) | + +逐次実行のまま高階反復へ書き換えても実行の実体は変わらない。何が得られるかは +[data-representation.md](data-representation.md) の「反復の実行方式」表で確かめる。 + ## 戦略の切り出し | 項目 | 内容 | diff --git a/plugins/ndf-kiro/skills/tdd-cycle/SKILL.md b/plugins/ndf-kiro/skills/tdd-cycle/SKILL.md index e16ddd9d..249c224b 100644 --- a/plugins/ndf-kiro/skills/tdd-cycle/SKILL.md +++ b/plugins/ndf-kiro/skills/tdd-cycle/SKILL.md @@ -16,7 +16,7 @@ description: "Write a failing test first, then the smallest implementation that | Skill | 参照している内容 | 未追加のあいだの代替 | | --- | --- | --- | | `requirements-design` | 受け入れ条件の作り方 | 受け入れ条件を「観測可能・一意・テスト可能」な 1 文へ自分で書き下す | -| `safe-refactoring` | 構造改善と現状固定テスト | サイクル内の整理にとどめ、構造改善は別タスクへ切り出す | +| `refactoring` | 構造改善と現状固定テスト | サイクル内の整理にとどめ、構造改善は別タスクへ切り出す | | `quality-gates` | 全体テストの実行とカバレッジ閾値の判定 | 対象プロジェクトのカバレッジツール設定に従い、設定がなければ測定値の記録だけ行う | ## 適用しない対象 @@ -97,7 +97,7 @@ E ImportError: cannot import name 'validate' ← 期待と違う。先にこ ### 4. 整理する テストを**通ったまま**保って構造を整える。整理中にテストが落ちたら、整理をいったん戻す。 -コードスメル起点の本格的な構造改善は `safe-refactoring`※ に委ねる。 +コードスメル起点の本格的な構造改善は `refactoring`※ に委ねる。 ### 5. 次の条件へ進む @@ -123,7 +123,7 @@ E ImportError: cannot import name 'validate' ← 期待と違う。先にこ ## テストの乏しい既存コードでは順序が変わる 変更対象にテストがほとんどない場合、いきなり新しいテストを足すより、**現状の振る舞いを -固定するテスト**を先に置く。手順は `safe-refactoring`※ の現状固定テストに従う。 +固定するテスト**を先に置く。手順は `refactoring`※ の現状固定テストに従う。 ## テストダブルの優先順 diff --git a/plugins/ndf-shared/manifests/claude-skills.txt b/plugins/ndf-shared/manifests/claude-skills.txt index dbe13458..05991278 100644 --- a/plugins/ndf-shared/manifests/claude-skills.txt +++ b/plugins/ndf-shared/manifests/claude-skills.txt @@ -22,5 +22,5 @@ official-skills-autoloader development-workflow requirements-design tdd-cycle -safe-refactoring +refactoring quality-gates diff --git a/plugins/ndf-shared/manifests/codex-skills.txt b/plugins/ndf-shared/manifests/codex-skills.txt index cb5da3f6..a00e9860 100644 --- a/plugins/ndf-shared/manifests/codex-skills.txt +++ b/plugins/ndf-shared/manifests/codex-skills.txt @@ -19,6 +19,6 @@ pr-tests problem-solving qa-security-scan quality-gates +refactoring requirements-design -safe-refactoring tdd-cycle diff --git a/plugins/ndf-shared/manifests/kiro-skills.txt b/plugins/ndf-shared/manifests/kiro-skills.txt index 8596b616..4a758fe6 100644 --- a/plugins/ndf-shared/manifests/kiro-skills.txt +++ b/plugins/ndf-shared/manifests/kiro-skills.txt @@ -21,5 +21,5 @@ qa-security-scan development-workflow requirements-design tdd-cycle -safe-refactoring +refactoring quality-gates diff --git a/plugins/ndf-shared/skills/development-workflow/SKILL.md b/plugins/ndf-shared/skills/development-workflow/SKILL.md index 0409b880..275df320 100644 --- a/plugins/ndf-shared/skills/development-workflow/SKILL.md +++ b/plugins/ndf-shared/skills/development-workflow/SKILL.md @@ -47,7 +47,7 @@ NULL 許容列の追加)は `standard` として扱う。判定に迷う場合 ```text mode: standard 根拠: 注文確定の振る舞いを変更する。公開 API とスキーマは変えない -必須工程: requirements-design → implementation-plan → tdd-cycle → safe-refactoring(必要な場合) +必須工程: requirements-design → implementation-plan → tdd-cycle → refactoring → pr-review → quality-gates → plan-to-spec(仕様が変わった場合) ``` @@ -59,8 +59,8 @@ mode: standard | モード | 対象 | 必須工程 | | --- | --- | --- | | `light` | 文言、ドキュメント、設定、テストの追加など、本番の振る舞いも本番コードの構造も変えない局所変更 | 成功条件の確認、対象範囲の確定、限定的な検証と静的解析 | -| `standard` | 一般的な機能追加・バグ修正、テストが十分にある構造改善 | 仕様、計画、テスト駆動、構造改善(必要な場合)、レビュー、全体検証 | -| `architecture` | 公開インタフェース、移行を伴うスキーマ変更、認証、複数モジュール、重要なドメイン変更 | ドメインモデリング、設計判断の記録、設計レビュー、テスト駆動、契約テストと結合テスト、相互レビュー | +| `standard` | 一般的な機能追加・バグ修正、テストが十分にある構造改善 | 仕様、計画、テスト駆動、構造改善、レビュー、全体検証 | +| `architecture` | 公開インタフェース、移行を伴うスキーマ変更、認証、複数モジュール、重要なドメイン変更 | ドメインモデリング、設計判断の記録、設計レビュー、テスト駆動、構造改善、契約テストと結合テスト、相互レビュー | | `legacy-refactor` | テストが少ない既存コードの振る舞い維持型改善 | 構造分析、計画、現状固定テスト、段階的改善、レビュー、退行検証 | ## モードごとに起動する Skill @@ -70,8 +70,8 @@ mode: standard | 要求と受け入れ条件 | — | `requirements-design` | `requirements-design` | — | | 設計 | — | `implementation-plan` に代替案と採否を記録 | ドメインモデリングと設計レビュー(Release 2 で有効化) | `implementation-plan` に代替案と採否を記録 | | 計画 | — | `implementation-plan` | `implementation-plan` | `implementation-plan` | -| 実装 | 直接編集 | `tdd-cycle` | `tdd-cycle` | `safe-refactoring` | -| 構造改善 | — | `safe-refactoring`(必要な場合) | `safe-refactoring`(必要な場合) | `safe-refactoring` | +| 実装 | 直接編集 | `tdd-cycle` | `tdd-cycle` | `refactoring` | +| 構造改善 | — | `refactoring` | `refactoring` | `refactoring` | | レビュー | — | `pr-review` | `cross-review` | `pr-review` | | 完了判定 | `quality-gates` | `quality-gates` | `quality-gates` | `quality-gates` | | 確定仕様化 | — | `plan-to-spec`(仕様が変わった場合) | `plan-to-spec` | — | @@ -83,6 +83,13 @@ mode: standard レビュー段階は**明示的に呼ぶ**。自然文で「レビューして」と依頼すると、Claude Code では 組み込みの `code-review` が起動して判定の投稿経路が変わる。 +構造改善は**レビューと同じく、通す工程であって任意ではない**。動くコードが出た時点では整理が +済んでいないことを前提に置き、見つけたスメルは直す。対象は書き換えた行だけでなく、**その +呼び出し元・呼び出し先と、同じファイル・同じモジュールの関連箇所まで**を含む(範囲と例外は +`refactoring` の `references/code-smells.md`「手を付ける範囲」)。 + +`light` だけが工程ごと対象外である。本番コードの構造を変えない変更に構造改善の判断は要らない。 + ## 標準フロー この図は**工程の全体像**を表す。どの Skill を起動するかは前節の表が基準であり、図はその @@ -98,7 +105,8 @@ flowchart TD E --> F F --> G[実装計画] G --> H[失敗するテスト → 最小実装 → 整理] - H --> I[仕様適合レビュー] + H --> R[構造改善] + R --> I[仕様適合レビュー] I --> J[コード品質レビュー] J --> K[限定的な検証・静的解析] K --> N[全体テスト → ビルド・結合テスト] @@ -117,7 +125,8 @@ flowchart TD (K は変更箇所を 1 度実行する限定的な検証と静的解析だけを指す。依存パッケージの版更新だけは 例外として既存テスト一式を実行する — [references/workflow-modes.md](references/workflow-modes.md)) - `legacy-refactor` は A から C へ抜けて `standard` と同じ経路をたどり、**B(要求と受け入れ条件)と - M(確定仕様化)は通らない**。H は「現状固定テスト → 段階的改善」、I は「本番の振る舞いが + M(確定仕様化)は通らない**。H は「現状固定テスト」、R は「段階的改善」、I は「本番の振る舞いが + 変わっていないことの確認」として読む ## `architecture` モードの現状 diff --git a/plugins/ndf-shared/skills/development-workflow/references/workflow-modes.md b/plugins/ndf-shared/skills/development-workflow/references/workflow-modes.md index 84a29033..e9f2fc1e 100644 --- a/plugins/ndf-shared/skills/development-workflow/references/workflow-modes.md +++ b/plugins/ndf-shared/skills/development-workflow/references/workflow-modes.md @@ -67,9 +67,10 @@ - 目的: 受け入れ条件を満たし、退行を出さないこと - 受け入れ条件を先に作る。条件が作れない依頼は、質問して止まる -- 実装はテスト駆動で進める。構造改善が必要になったら差分を分ける +- 実装はテスト駆動で進める。**通したあとに構造改善の工程を通す**(手を付けない判断でもよいが、 + 工程は飛ばさない)。手を入れる場合は差分を分ける - 振る舞いを変えない構造変更のみの依頼は、受け入れ条件を「既存テストが通り続けること」とし、 - 実装は `safe-refactoring` の手順で進める + 実装は `refactoring` の手順で進める - 完了判定は全体テストまで通す ### `architecture` diff --git a/plugins/ndf-shared/skills/ndf-policies/SKILL.md b/plugins/ndf-shared/skills/ndf-policies/SKILL.md index 677d4eda..bf6de328 100644 --- a/plugins/ndf-shared/skills/ndf-policies/SKILL.md +++ b/plugins/ndf-shared/skills/ndf-policies/SKILL.md @@ -18,33 +18,18 @@ user-invocable: false 4. **マージ済みブランチには push しない。** 既存 PR の状態を確認し、マージ済みなら新ブランチ + 新 PR を作る(サフィックス `-v2`, `-v3`) 5. **revert を連鎖させない。** 最終的なあるべき状態を直接コミットする方が履歴上の意図が明確になり、後の cherry-pick も簡単になる -## v7.0.0 で移動した Skill(v8.0.0 で削除) +## v8.0.0 で改名した Skill(v9.0.0 で削除) -ブラウザ自動テストの 4 Skill を **`playwright-kit` プラグイン**へ分離した。**Skill 名は -変わらない**ため、`/playwright-` まで打てば従来どおり候補に出る。 +構造改善の Skill を **`/ndf:refactoring`** へ改名し、分岐・反復・定数の表現を決める観点を +統合した。引数と手順は変わらない。 | 旧コマンド | 移行先 | | --- | --- | -| `/ndf:playwright-planning` | `/playwright-kit:playwright-planning` | -| `/ndf:playwright-authoring` | `/playwright-kit:playwright-authoring` | -| `/ndf:playwright-evidence` | `/playwright-kit:playwright-evidence` | -| `/ndf:playwright-kit-ops` | `/playwright-kit:playwright-kit-ops` | - -利用するにはプラグインを別途インストールする。 - -```bash -# Claude Code -/plugin install playwright-kit@ai-plugins -# Codex -codex plugin add playwright-kit@ai-plugins -# Kiro CLI -bash plugins/playwright-kit-kiro/install.sh -``` - -分離したのは、Skill の `name` と `description` が起動時の一覧として常時注入され、その予算が -プラグイン横断で共有されるためである。ブラウザ自動テストは使う場面が限られる一方で MCP -ツールを多用するため frontmatter が大きく(4 個で約 2,400 文字)、全利用者へ常時注入する -取り分に見合わなかった。 - -v6.0.0 の対応表(`/ndf:review` → `/ndf:pr-review`)は、予告どおり本バージョンで削除した。 -v6.0.0 以前から移行する場合は v6.1.0 の `ndf-policies` を参照する。 +| `/ndf:safe-refactoring` | `/ndf:refactoring` | + +`safe-` を外したのは、`/refactoring` で一意に決まり、入力が短くなるためである。統合した観点は +`references/data-representation.md` にあり、スメル一覧からも参照される。 + +v7.0.0 の対応表(playwright 系 4 Skill の `playwright-kit` プラグインへの分離)は、予告どおり +本バージョンで削除した。v7.0.0 より前から移行する場合は、この対応表を持つ最後の配布版である +v7.0.0 の `ndf-policies` を参照する。 diff --git a/plugins/ndf-shared/skills/pr-review/SKILL.md b/plugins/ndf-shared/skills/pr-review/SKILL.md index fe188f1f..04e92849 100644 --- a/plugins/ndf-shared/skills/pr-review/SKILL.md +++ b/plugins/ndf-shared/skills/pr-review/SKILL.md @@ -69,7 +69,7 @@ PR 差分、または `--branch` 指定時は現在のブランチの差分を | 責務・凝集度・結合度 | 1 つの単位が複数の変更理由を持っていないか | | 依存の向き | 業務ロジックが外部の仕組みへ直接依存していないか。循環がないか | | 可読性・単純性 | 分岐の深さ、名前と実態の一致、不要な抽象化 | -| コードスメル | 重複、長すぎる単位、基本型への固執など(`safe-refactoring` の一覧) | +| コードスメル | 重複、長すぎる単位、基本型への固執など(`refactoring` の一覧) | | セキュリティ・性能 | 下の「具体的なチェックポイント」 | | テストが実装詳細に結合していないか | 内部呼び出し回数の検証、private への直接依存(`tdd-cycle` の脆いテスト) | diff --git a/plugins/ndf-shared/skills/problem-solving/SKILL.md b/plugins/ndf-shared/skills/problem-solving/SKILL.md index 80f40e7a..13aa2987 100644 --- a/plugins/ndf-shared/skills/problem-solving/SKILL.md +++ b/plugins/ndf-shared/skills/problem-solving/SKILL.md @@ -57,7 +57,7 @@ description: "Fix bugs and data inconsistencies upstream at the root cause. Use 対象にテストがほとんどない場合、再現テストを書く前に**現状の振る舞いを固定するテスト**を 置く。副作用を分離できず再現テストが書けない状態で修正すると、直したい振る舞い以外を -壊しても気づけない。手順は `safe-refactoring` の現状固定テストに従う。 +壊しても気づけない。手順は `refactoring` の現状固定テストに従う。 ```text 1. 変更対象の入口と副作用を洗い出す diff --git a/plugins/ndf-codex/skills/safe-refactoring/SKILL.md b/plugins/ndf-shared/skills/refactoring/SKILL.md similarity index 79% rename from plugins/ndf-codex/skills/safe-refactoring/SKILL.md rename to plugins/ndf-shared/skills/refactoring/SKILL.md index 55288890..4fff6d27 100644 --- a/plugins/ndf-codex/skills/safe-refactoring/SKILL.md +++ b/plugins/ndf-shared/skills/refactoring/SKILL.md @@ -1,6 +1,6 @@ --- -name: safe-refactoring -description: "Change structure without changing behavior, guarded by tests. Use when cleaning up code or touching legacy code(リファクタリング・コードスメル・現状固定テスト)." +name: refactoring +description: "Change structure without changing behavior, guarded by tests, judging smells and how decisions are represented. Use when cleaning up code or touching legacy code(リファクタリング・コードスメル・分岐をデータ化・現状固定テスト)." --- # 安全な構造改善 @@ -8,6 +8,11 @@ description: "Change structure without changing behavior, guarded by tests. Use **テストがなければ、それは構造改善ではなく単なる編集である。** 振る舞いが変わっていない ことを示す手段がない書き換えは、この Skill の対象外として扱う。 +この工程は**レビューと同じく、実装のあとに必ず通す**。動くコードが出た時点では整理が済んで +いないことを前提に置く。対象は書き換えた行だけでなく、**その呼び出し元・呼び出し先と、同じ +ファイル・同じモジュールの関連箇所まで**を含む(範囲と例外は +[references/code-smells.md](references/code-smells.md) の「手を付ける範囲」)。 + ## 最初に決める 2 つのこと ### 1. 機能変更と構造改善を混ぜない @@ -40,7 +45,9 @@ description: "Change structure without changing behavior, guarded by tests. Use 1. **変更前に既存テストを実行する。** ここで落ちているものがあれば、先に報告する 2. スメルを 1 つ選ぶ(一覧は [references/code-smells.md](references/code-smells.md)) 3. 対応する手法を選ぶ([references/refactoring-catalog.md](references/refactoring-catalog.md)。 - スメル一覧で ★ が付いた手法はカタログに項目がなく、一覧の記述だけで進めてよい) + スメル一覧で ★ が付いた手法はカタログに項目がなく、一覧の記述だけで進めてよい)。 + 分岐・反復・定数を**何にどう置き換えるか**は + [references/data-representation.md](references/data-representation.md) で決める 4. **1 手だけ適用する** 5. テストを実行する。落ちたら直前の 1 手を戻す 6. 通ったらコミットする(1 手 = 1 コミットを既定とする) @@ -109,7 +116,7 @@ flowchart TD - 固定テストが書けない(副作用が分離できない、実行に外部環境が要る) - 1 手で終わらず、テストを通すために本番コードの分岐を足す必要が出た - 改善の途中で仕様の不明点が出た(`requirements-design` へ戻る) -- 差分が依頼範囲を超えて広がった +- 差分が [code-smells.md](references/code-smells.md) の「手を付ける範囲」を超えて広がった 止めたときは「どこまで安全な状態か」を明示する。中途半端な状態を「あとで直す」前提で 残さない。 @@ -130,4 +137,11 @@ flowchart TD - [references/code-smells.md](references/code-smells.md) — 構造改善の起点になる兆候 - [references/refactoring-catalog.md](references/refactoring-catalog.md) — 手法と適用条件 +- [references/data-representation.md](references/data-representation.md) — 分岐・反復・定数を何にどう置き換えるか +- 言語ごとの手段 — **対象の言語のファイルだけを読む** + - [references/lang-python.md](references/lang-python.md) + - [references/lang-javascript.md](references/lang-javascript.md) + - [references/lang-typescript.md](references/lang-typescript.md) + - [references/lang-php.md](references/lang-php.md) + - 一覧にない言語は、`data-representation.md` の判定表から自分で対応付ける - [references/characterization-tests.md](references/characterization-tests.md) — 現状固定テストの作り方 diff --git a/plugins/ndf-shared/skills/safe-refactoring/references/characterization-tests.md b/plugins/ndf-shared/skills/refactoring/references/characterization-tests.md similarity index 100% rename from plugins/ndf-shared/skills/safe-refactoring/references/characterization-tests.md rename to plugins/ndf-shared/skills/refactoring/references/characterization-tests.md diff --git a/plugins/ndf-shared/skills/safe-refactoring/references/code-smells.md b/plugins/ndf-shared/skills/refactoring/references/code-smells.md similarity index 59% rename from plugins/ndf-shared/skills/safe-refactoring/references/code-smells.md rename to plugins/ndf-shared/skills/refactoring/references/code-smells.md index 325f04ad..ef77a7e5 100644 --- a/plugins/ndf-shared/skills/safe-refactoring/references/code-smells.md +++ b/plugins/ndf-shared/skills/refactoring/references/code-smells.md @@ -1,7 +1,7 @@ # コードスメル -スメルは「直すべき欠陥」ではなく **調べる価値がある兆候** である。見つけても、変更の -理由(機能追加・不具合修正・読みにくさ)が伴っていなければ手を付けない。 +スメルは「直すべき欠陥」ではなく **調べる価値がある兆候** である。兆候を見つけたら、下の +「手を付ける範囲」に入っているかを確かめ、入っていれば直す。 行数などの数値規則は使わない。判断は凝集度・結合度・変更理由・認知負荷・テスト容易性で行う。 @@ -26,10 +26,16 @@ | 例外の飲み込み | 捕まえて何もしない、ログだけ出して続行する | 失敗が沈黙し、原因が追えない | ★呼び出し元へ伝える、または明示的に扱う | | 条件分岐の連鎖 | 型・状態ごとの分岐が複数箇所で繰り返される | 種類を増やすたびに全箇所を直す | 多態による分岐の置き換え | | 設定の散在 | 同じ設定値が複数の場所で定義される | どちらが効くか分からない | ★定義を 1 箇所へ寄せる | +| 業務ルールの埋め込み | 料率・区分・しきい値・優先順位が制御構文の中に書かれている | 一覧できない。値を変えるだけの修正にコード変更と再配備が要る | 対応表への置き換え | +| 一件ずつの反復 | 同種で独立した処理を 1 件ずつ繰り返す(1 件ごとの問い合わせを含む) | 往復回数か実行時間が件数に比例して積み上がる | 一括処理への置き換え | +| 検証のない外部化 | 設定・マスタへ出したデータに、スキーマ・版・検証がない | 壊れた値が実行時まで通る。どの版が適用されたか追えない | ★スキーマと版を与え、読み込み境界で検証する | -★ の 4 件をカタログに置いていないのは、手順が「1 箇所へ寄せる/呼び出し元へ返す」で尽きて -おり、適用条件も上の「何が問題か」以外に無いためである。カタログを探さず、この表の記述で -そのまま進めてよい。ただし共通化だけは判断を誤りやすいため、次節の見分け方に従う。 +★ の 5 件をカタログに置いていないのは、手順が「1 箇所へ寄せる/呼び出し元へ返す/検証を足す」 +で尽きており、適用条件も上の「何が問題か」以外に無いためである。カタログを探さず、この表の +記述でそのまま進めてよい。ただし共通化だけは判断を誤りやすいため、次節の見分け方に従う。 + +下 3 件は「どう持つか」の判断を伴う。表現の選び方と、外部化してよい条件は +[data-representation.md](data-representation.md) にある。 ## 共通化してよい重複の見分け方 @@ -61,11 +67,33 @@ 「何を解決したか」と「外し方」を残す。決まった書式は今のところ無い(Release 2 で追加予定の `object-design` Skill で扱う想定だが、現時点では未実装)。 -## スメルに手を付けない場合 +## 手を付ける範囲 + +**今回書き換えた行だけに閉じない。** そこに閉じると、読みにくい領域は読みにくいまま残り続ける。 +一方で無制限に広げると差分がレビューできなくなる。次を境界にする。 + +| 範囲 | 扱い | +| --- | --- | +| 今回変更した関数・クラス | 直す | +| その呼び出し元・呼び出し先 | 直す。変更の影響を読むために通る範囲であり、テストも通っている | +| 同じファイル・同じモジュールの関連箇所 | 直す。差分は分ける | +| そこから遠い領域 | 対象外。後続の作業として記録する | + +範囲に入っていることは、直す理由にはならない。**この文書のスメル一覧のどれにも当たらない箇所は +直さない。**「読みやすくなりそう」だけで手を入れると、レビューできない差分になる。手順(1 手ずつ・ +現状固定テストで守る・差分を分ける)は、範囲を広げても変わらない。 + +広げた分は**別のコミットに切る**(「差分の切り方」)。機能変更と構造改善が同じ差分に入ると、 +レビュアーは意図した変更と構造改善の事故を区別できない。上表の範囲を超えて広がるなら、 +別の変更として出す。 + +## 手を付けない場合 + +範囲に入っていても、次は対象から外す。 -- 変更予定のない領域(読みにくいだけで、今回の変更に関係しない) - 生成物・外部から取り込んだコード(上流で直す) - 削除予定の領域 +- 振る舞いが変わっていないことを示す手段がなく、現状固定テストも書けない箇所 + (`SKILL.md` の「途中で止める条件」) -手を付けないと決めた場合、指摘だけを残すよりも**何もしない**方がよい。残す価値がある -指摘は、後続の作業として記録する。 +外したものは、指摘だけを残さず**後続の作業として記録する**。 diff --git a/plugins/ndf-shared/skills/refactoring/references/data-representation.md b/plugins/ndf-shared/skills/refactoring/references/data-representation.md new file mode 100644 index 00000000..213f15bf --- /dev/null +++ b/plugins/ndf-shared/skills/refactoring/references/data-representation.md @@ -0,0 +1,186 @@ +# 分岐・反復・定数の表現 + +`code-smells.md` の「業務ルールの埋め込み」「一件ずつの反復」「検証のない外部化」を見つけた +ときに、**何にどう置き換えるか**を決めるための判断材料。 + +置き換えの目的は 2 つに尽きる。どちらにも近づかない置き換えは行わない。 + +| 時点 | 近づける状態 | +| --- | --- | +| 実行前 | どの入力がどの結果になるかを実装を読まずに一覧でき、扱っていない入力があれば検査で分かる | +| 実行後 | どの判断がなぜその結果になったかを、記録だけで再現できる | + +## 移す対象を決める + +> **変化する知識はデータへ。安定した機構と不変条件はコードへ。** + +判断の軸は「分岐が多いか」ではなく「**その知識が変化するか**」である。変化しない知識をデータへ +出すと、検査できる場所が減るだけになる。 + +| データへ移す | コードに残す | +| --- | --- | +| 業務ルール、料率、しきい値、優先順位 | 不変条件、安全性の制約 | +| 分類・対応表(値から値への写像) | 機構(どう読み、どう書き、どう失敗を扱うか) | +| 環境ごとに変わる値 | 型・スキーマ・境界の定義 | +| 利用者が管理するカテゴリ | プロトコル、閉じた状態集合 | + +## 手を付けないもの + +次はいずれも**そのままでよい**。スメルとして拾わない。 + +- 単純なガード節の条件分岐(早期リターンによる前提条件の検査) +- 閉じた型に対する網羅的な分岐(静的に網羅性を検査できるもの) +- 逐次依存・早期終了・ストリーム処理・メモリ制約下の明示的なループ +- 不変条件・プロトコル・閉じた状態集合を表す定数と列挙型 +- 原子性・整合性・安全性を守るための即時打ち切りと巻き戻し。全体を 1 単位として成否を決める + 処理、後続に不正な入力が波及する処理、安全性検査の失敗はこれにあたる + +## 改善にならない置き換え + +形だけが変わり、上の 2 つの状態に近づかないもの。着手する前に見分ける。 + +| 置き換え | 何が起きるか | +| --- | --- | +| 条件分岐を機械的に対応表へ移す | 前提条件の検査は、対応表より分岐のほうが意図が明確。網羅性を検査できていた分岐を型情報のない対応表へ移すと、その検査が消える | +| 逐次実行のまま高階反復へ書き換える | 実行の実体は変わらない。一括実行にも並列実行にもなっていない | +| 定数をすべて外部設定へ出す | 条件・参照・既定値が設定側に積み上がり、設定自体が独自の言語になる。変更に必要な知識はむしろ増える | + +## 分岐の種類 → 適切な表現 + +| 分岐の種類 | 適切な表現 | +| --- | --- | +| 値から値への単純な対応 | 対応表(写像) | +| 処理方式の切り替え | 登録表(識別子から処理への対応) | +| 条件の組み合わせが多い業務判断 | 決定表、ルールエンジン | +| 認可・制約・組織ポリシー | ポリシーエンジン | +| 時系列で状態が変化する処理 | 状態機械 | +| 閉じた少数の型分岐 | 型に対する網羅的な分岐(置き換えない) | +| 前提条件の検査 | ガード節(置き換えない) | + +## 反復の実行方式 → 得られるもの + +置き換えても何も得られない場合を見分けるための表である。分類の軸は構文ではなく**実行方式**で +ある。 + +| 実行方式 | 実行の実体 | 得られるもの | +| --- | --- | --- | +| 逐次実行 | 1 件ずつ順に処理する | **なし**(記述の変更のみ) | +| 一括演算 | 基盤側で一括実行 | 実行時間 | +| 一括入出力 | 呼び出し回数の削減 | 往復回数(大量データでは最大の効果) | +| 並行処理 | 待ち時間の重ね合わせ | 待ち時間。計算時間は減らない | +| 並列処理 | 複数の実行資源で同時 | 計算時間 | +| 分散データフロー | 基盤が分割・再試行・集約 | 規模と耐障害性 | + +高階反復は構文であって実行方式ではない。**逐次実行のまま構文だけを高階反復へ置き換えても、この +表のどの行にも移動していない。** 高階反復の形のまま遅延・並行・並列・分散で実行する仕組みを持つ +言語もあるが、得られるものを決めるのはその実行方式であって構文ではない。**移すなら、移動先の行 +を言えなければならない。** 置き換えたら計測し、効果が出ていなければ戻す。 + +一括演算の基盤は言語によって有無が異なる。基盤がない言語では、一括入出力(往復回数の削減)が +主戦場になる。 + +## 定数の性質 → 置き場所 + +| 値の性質 | 置き場所 | +| --- | --- | +| 数学的・技術的な不変条件 | コード内の定数 | +| 閉じた状態集合・プロトコル | 型、列挙型、スキーマ | +| 頻繁に変わる業務ルール | 決定表、ポリシー、設定 | +| 環境ごとに変わる値 | 配備設定、環境変数 | +| 利用者が管理するカテゴリ | 参照テーブル(データベース) | +| 表示名・文言 | 多言語化資源、コンテンツ | +| 安全性の絶対制約 | コード・型・スキーマ・ポリシーの複数層 | + +不透明なコード値(`status == 3`)は避ける。ただし**意味の分かる識別子を持つ列挙型まで置き換え +ない**。型のない文字列にすると、綴り誤りと未処理ケースの発見が実行時まで遅れる。 + +## 外部化してよい条件 + +データへ移すなら、移した先で次を用意できることが条件になる。用意できないなら、コードに残した +ほうが検査できる範囲は広い。 + +- [ ] スキーマがある +- [ ] 型またはバリデーションで検査される +- [ ] 版を持ち、変更履歴が追える +- [ ] 競合するルール・到達不能なルールを検出できる +- [ ] テストがある +- [ ] 変更の適用手順(移行)がある +- [ ] 誰が何を変更したか監査できる +- [ ] 実行時に、適用したルールの識別子と理由を記録する + +外部化したデータをどう守るかは、**そのデータをいつ読むか**で決まる。 + +1. **データごとビルド時に組み込める**なら、スキーマから型・定数を生成して取り込む。外部化しても + 静的な検査が残るため、これが選べるなら最初に選ぶ +2. **データを実行時にロードする**なら、**ロード境界をスキーマ検証で守る**。型を生成していても + 同じで、生成した型は、ロードした値がその形である保証を与えない +3. どちらも満たせないなら、外部化しない + +型情報を伴わずに実行時ロードした対応表は、静的解析の対象から外れる。どの検査がどこまで効くかは +言語によって違う。**対象の言語のファイルだけを読む** — +[lang-python.md](lang-python.md) / [lang-javascript.md](lang-javascript.md) / +[lang-typescript.md](lang-typescript.md) / [lang-php.md](lang-php.md)。 + +## 判断を記録できるようにする + +対応表への置き換えは、**どのルールが適用されたかを記録できる構造を保って完了**である。値を +外へ出した分だけ「なぜこの結果になったか」が追いにくくなるので、表から識別子と版を取り出せる +形を崩さない。 + +記録を出す要件があるなら、1 つの判断につき 1 件、入力・決定・理由・実行文脈をまとめた +構造化イベントを出す。 + +```json +{ + "event_name": "discount_decided", + "customer_id": "cus_123", + "input": { "rank": "gold", "purchase_amount": 120000 }, + "decision": { + "discount_rate": 0.2, + "rule_id": "customer-rank-discount", + "rule_version": "2026-08-01", + "reason": "customer rank is gold" + }, + "execution": { "code_version": "a13fd82", "duration_ms": 4, "trace_id": "..." } +} +``` + +- 属性は、絞り込み・グループ化・集計・相関に使える形にする +- 適用したルールの識別子と版を含めると、結果に至った経緯を後から再現できる +- 個人情報と秘密情報の扱いは `logging-guidelines` に従う + +**記録の追加は振る舞いの変更である。** 構造改善と同じ差分に混ぜず、コミットを分ける。 + +一括処理への置き換えでは、終了時に失敗の集計も出す。 + +```json +{ + "event_name": "import_completed", + "total": 12000, "succeeded": 11940, "failed": 60, + "failures_by_reason": { "invalid_date": 48, "missing_customer": 12 }, + "sample_failed_ids": ["row_88", "row_312", "row_940"] +} +``` + +項目が独立に処理できるのに最初の失敗で全体を止めると、どこまで進んだかが分からなくなる。 +全体を 1 単位として成否を決める処理では、打ち切って巻き戻すのが正しい。その場合は**打ち切った +位置・理由・巻き戻しの範囲**を同じ構造化イベントに含める。 + +## 例: 業務ルールの埋め込みを置き換える + +```text +❌ 変化する料率が制御構文へ埋まっている + if rank == "gold" -> rate = 0.2 + else if rank == "silver" -> rate = 0.1 + else -> rate = 0 + +✅ 対応表として持ち、既定と未知の扱いを明示する + rates = { gold: 0.2, silver: 0.1, bronze: 0.0 } # 版とスキーマを持つ + rate = rates[rank](未知の階級は失敗として扱い、握りつぶさない) +``` + +置き換えた結果、次が言えるようになっていなければ手を付けた意味がない。 + +- 料率の一覧を**コードを読まずに**確認できる +- 新しい階級の追加が、分岐の追加ではなくデータの追加になる +- どの階級にどの版の料率が適用されたか、記録から再現できる diff --git a/plugins/ndf-shared/skills/refactoring/references/lang-javascript.md b/plugins/ndf-shared/skills/refactoring/references/lang-javascript.md new file mode 100644 index 00000000..2c4c0b66 --- /dev/null +++ b/plugins/ndf-shared/skills/refactoring/references/lang-javascript.md @@ -0,0 +1,45 @@ +# JavaScript での手段 + +[data-representation.md](data-representation.md) で選んだ表現を、JavaScript の機能へ対応付ける。 + +| 判定 | 手段 | +| --- | --- | +| 値から値への対応 | `Object.freeze` のオブジェクト | +| 処理方式の切り替え | 関数を値に持つオブジェクト | +| 閉じた状態集合 | 凍結した定数オブジェクト | +| 網羅性の静的検査 | **手段なし**(実行時に失敗させる) | +| 不変性 | `Object.freeze`(凍結はトップレベルのみ)。入れ子まで不変にするなら再帰的に凍結するか、構造共有の不変データ構造を使う | +| スキーマ検証 | zod / JSON Schema | +| 一括処理 | 一括 API / `Promise.all`(並行) | +| 失敗の集計 | 失敗を集めて返す | + +## 静的な網羅性検査が効かないことが判定を変える + +型注釈がないため、**閉じた状態集合に対する分岐の網羅性を実行前に検査できない**。これが +2 つの判定を変える。 + +- 「手を付けないもの」にある「閉じた型に対する網羅的な分岐(静的に網羅性を検査できるもの)」 + の条件が成立しない。分岐をそのまま残しても追加漏れは実行時まで分からないので、**未知の + ケースで必ず失敗させる**書き方を併せて用意する +- 外部化した対応表を守る手段が実行時のスキーマ検証しかない。**静的検査で埋め合わせができない + 分、スキーマ検証の必要性が高い** + +```javascript +// ❌ 未知のケースが undefined として下流へ流れ、どこで壊れたか分からなくなる +const RATES = { gold: 0.2, silver: 0.1 }; +return RATES[rank]; + +// ✅ 未知のケースをその場で失敗させる +const RATES = Object.freeze({ gold: 0.2, silver: 0.1, bronze: 0 }); +if (!Object.hasOwn(RATES, rank)) throw new Error(`unknown rank: ${rank}`); +return RATES[rank]; +``` + +JSDoc の型注釈と `checkJs` を入れられるなら、静的検査が使える状態に戻せる。その場合の書き方は +[lang-typescript.md](lang-typescript.md) を読む。ただし実行時にロードするならロード境界の +検証は同じく要る(JSDoc の型は実体を検査しない)。 + +## 並行と並列を混同しない + +`Promise.all` は待ち時間を重ねるだけで、計算時間は減らない。CPU を使う処理を並列化するには +worker が要る。「反復の実行方式」表の「並行処理」と「並列処理」は別の行である。 diff --git a/plugins/ndf-shared/skills/refactoring/references/lang-php.md b/plugins/ndf-shared/skills/refactoring/references/lang-php.md new file mode 100644 index 00000000..7b5c958f --- /dev/null +++ b/plugins/ndf-shared/skills/refactoring/references/lang-php.md @@ -0,0 +1,146 @@ +# PHP での手段 + +[data-representation.md](data-representation.md) で選んだ表現を、PHP の機能へ対応付ける。 +以下は **8.1 以降**を前提とする(8.0 以下は最終節)。 + +| 判定 | 手段 | +| --- | --- | +| 値から値への対応 | 連想配列 / `match` | +| 処理方式の切り替え | first-class callable 構文 `foo(...)` | +| 閉じた状態集合 | backed enum | +| 網羅性の静的検査 | PHPStan `match.unhandled` | +| 不変性 | `readonly` プロパティ | +| スキーマ検証 | JSON Schema / Valinor | +| 一括処理 | **一括入出力**(標準に一括演算の基盤はない) | +| 失敗の集計 | 失敗を集めて返す | + +8.1 で backed enum・`readonly` プロパティ・first-class callable 構文が入り、**データ化しても +静的解析が効く**範囲が広がった。 + +## 網羅性の静的検査 + +```php +enum Rank: string { + case Gold = 'gold'; + case Silver = 'silver'; + case Bronze = 'bronze'; +} + +function label(Rank $rank): string { + return match ($rank) { + Rank::Gold => 'ゴールド', + Rank::Silver => 'シルバー', + Rank::Bronze => 'ブロンズ', + }; +} +``` + +`match` は一致する腕がなければ実行時に `\UnhandledMatchError` を投げる。さらに **PHPStan が +`match.unhandled`("Match expression does not handle remaining value")として静的に検出する**。 +`Rank` にケースを足すと、`default` を書いていない `match` が解析で落ちる。 + +```php +// ❌ 連想配列へ移すと、検査は弱くなる +$labels = ['gold' => 'ゴールド', 'silver' => 'シルバー']; +return $labels[$rank->value]; +``` + +どこまで弱くなるかは、**対応表が静的に見えているか**で決まる(PHPStan 2.2 で実測)。 + +| 書き方 | level 5 | level max | +| --- | --- | --- | +| `match`(`default` なし) | `match.unhandled` で検出 | 同左 | +| その場に書いた連想配列 | 検出なし | `offsetAccess.notFound` で検出 | +| 外部から渡した対応表(`array`) | 検出なし | **検出なし** | + +**この表が測っているのは、型情報を伴わずに実行時ロードした場合である**(3 行目は +`array` という幅の広い型で対応表を受け取る形)。この形にすると、変化する業務 +ルールは定義ごと静的解析の視界から外れるため、「外部化してよい条件」(スキーマ・ +バリデーション・版・テスト)が必須になる。**静的検査で守れなくなった分を、スキーマ検証で +埋める。** + +一方、スキーマから backed enum や定数クラスを**生成**してビルド時に取り込めば、外部化しても +`match.unhandled` の検査は残る。ただし**データを実行時にロードするなら、型を生成していても +ロード境界の検証は要る**。`json_decode` の戻り値に `@var GeneratedShape` を付けるだけでは、 +静的解析が信じるだけで実体は検査されない。`Valinor` などのマッパか JSON Schema 検証を通す。 + +値が固定で外部化する理由がないなら、`match` のまま置くほうが分析可能性は高い。 + +`default` を書くとケース追加漏れが検出されなくなる。**閉じた列挙に対する `match` に +`default` を置かない**(未知の入力を扱う必要があるなら、それは閉じた列挙ではない)。 + +## 処理方式の切り替え + +```php +// first-class callable 構文。静的解析が参照先を追える +$handlers = [ + 'csv' => $this->importCsv(...), + 'json' => $this->importJson(...), +]; +($handlers[$format] ?? throw new UnsupportedFormat($format))($payload); +``` + +文字列やコールバック配列(`'importCsv'` / `[$this, 'importCsv']`)で書くと解析が追えない。 +**データ化しても分析可能性を落とさない書き方を選ぶ。** + +## 一括処理は入出力の問題である + +**PHP 標準に一括演算の基盤はない。** `array_map` は逐次実行の高階反復であり、「反復の実行方式」 +表のどの行にも移動しない。`array_column` はコールバックを取らない組み込みの列抽出で、走査は逐次 +のままだが PHP レベルのループとは実装が異なるため、**計測して選ぶ**。数値計算向けの拡張や +ライブラリを導入すれば一括演算に移れる場合はあるので、採用するなら**効果を計測してから決める**。 + +```php +// ❌ ループを array_map に変えても、実行の実体は変わらない +$totals = array_map(fn($o) => $o->amount * 1.08, $orders); +``` + +PHP で効くのは**一括入出力**、すなわち往復回数の削減である。 + +```php +// ❌ N+1。1 行ごとに問い合わせる +foreach ($orderIds as $id) { $rows[] = $repo->find($id); } + +// ✅ 一括取得。往復が 1 回になる +$rows = $repo->findByIds($orderIds); + +// ✅ 一括挿入。件数に比例した往復をなくす +$repo->insertMany($rows); // 大量件数は chunk して分割する +``` + +大量データを扱うときは、`yield` による逐次生成でメモリを一定に保つ。これは「逐次依存・ +メモリ制約下の明示的なループ」にあたり、**そのままでよい**。 + +## 不変性 + +```php +final class Discount { + public function __construct( + public readonly string $ruleId, + public readonly string $ruleVersion, + public readonly float $rate, + ) {} +} +``` + +判断の結果を `readonly` の値として持つと、記録に必要なルールの識別子と版を持ち回れる。 + +## 8.0 以下 + +backed enum・`readonly`・first-class callable 構文がいずれも使えない。代替は次のとおりで、 +**いずれも静的検査は弱くなる**。 + +| 8.1+ | 8.0 以下の代替 | +| --- | --- | +| backed enum | クラス定数 + 値オブジェクト | +| `match` の網羅性検査 | `switch` + `default` で例外を投げる(実行時検出のみ) | +| `readonly` プロパティ | `private` + getter のみ | +| `foo(...)` | `Closure::fromCallable('foo')` | + +閉じた状態集合は 8.1 以降と同じくコード側に置く。変わるのは**外部化した業務ルールの守り方**で、 +生成した定数クラスに対する網羅性検査が効かないぶん、実行時のスキーマ検証への依存が高くなる。 + +## 出典 + +- [PHP 8.1 リリースアナウンス](https://www.php.net/releases/8.1/en.php) +- [PHPStan `match.unhandled`](https://phpstan.org/error-identifiers/match.unhandled) diff --git a/plugins/ndf-shared/skills/refactoring/references/lang-python.md b/plugins/ndf-shared/skills/refactoring/references/lang-python.md new file mode 100644 index 00000000..b08108d9 --- /dev/null +++ b/plugins/ndf-shared/skills/refactoring/references/lang-python.md @@ -0,0 +1,85 @@ +# Python での手段 + +[data-representation.md](data-representation.md) で選んだ表現を、Python の機能へ対応付ける。 + +| 判定 | 手段 | +| --- | --- | +| 値から値への対応 | `dict` / `Mapping` | +| 処理方式の切り替え | 関数を値に持つ `dict` | +| 閉じた状態集合 | `Enum` / `Literal` | +| 網羅性の静的検査 | mypy + `assert_never` | +| 不変性 | `@dataclass(frozen=True)` | +| スキーマ検証 | pydantic / jsonschema | +| 一括処理 | NumPy / pandas の**ベクトル化演算**(一括演算) | +| 失敗の集計 | 失敗を集めて返す | + +## 網羅性の静的検査 + +```python +from enum import Enum +from typing import assert_never # 3.11+(それ以前は typing_extensions) + +class Rank(Enum): + GOLD = "gold" + SILVER = "silver" + BRONZE = "bronze" + +def label(rank: Rank) -> str: + match rank: + case Rank.GOLD: return "ゴールド" + case Rank.SILVER: return "シルバー" + case Rank.BRONZE: return "ブロンズ" + case _: assert_never(rank) # ケース追加漏れを mypy が検出 +``` + +`Rank` に階級を足すと mypy が `assert_never` の行で型エラーを出す。**この検査が効いている +分岐は、`dict` へ移さない。** + +外部化した値を実行時にロードするなら、型注釈を用意していてもロード境界の検証は要る。 +`cast(Rates, json.load(f))` は mypy を黙らせるだけで実体を検査しないので、`pydantic` の +`TypeAdapter` や `jsonschema` を通してから使う。 + +## 一括演算とその反例 + +Python は一括演算の基盤(NumPy)を持つ数少ない言語である。ただし**「ループを消したこと」と +「一括演算になったこと」は別**である。 + +```python +# ❌ 逐次。要素ごとに Python のバイトコードを実行する +result = [x * 1.08 for x in prices] + +# ❌ 高階反復。上と実行の実体は変わらない +result = list(map(lambda x: x * 1.08, prices)) + +# ❌ np.vectorize も同じ。公式が明言している +# "provided primarily for convenience, not for performance. +# The implementation is essentially a for loop." +result = np.vectorize(lambda x: x * 1.08)(prices) + +# ✅ 一括演算。C 側で一括実行される +result = prices * 1.08 +``` + +pandas の `.apply()` も、Python の関数を要素・行・列のいずれかの単位で呼ぶ経路である限りは +一括演算ではない(ufunc を渡すなど、内部で一括実行に落ちる経路もある)。**置き換えたら +計測する。** 速くならないなら、「反復の実行方式」表の行を移動できていない。 + +## 失敗の集計 + +```python +def import_rows(rows): + ok, failures = [], [] + for i, row in enumerate(rows): + try: + ok.append(parse(row)) + except ValueError as e: + failures.append({"index": i, "reason": type(e).__name__, "id": row.get("id")}) + return ok, failures # 呼び出し側が件数・種類・対象を報告できる +``` + +例外を握りつぶさず、最初の失敗で打ち切らない。ここは明示的なループでよい(逐次依存では +ないが、失敗の収集がある)。 + +## 出典 + +- [numpy.vectorize — 性能目的ではないという公式注記](https://numpy.org/doc/stable/reference/generated/numpy.vectorize.html) diff --git a/plugins/ndf-shared/skills/refactoring/references/lang-typescript.md b/plugins/ndf-shared/skills/refactoring/references/lang-typescript.md new file mode 100644 index 00000000..6759eaae --- /dev/null +++ b/plugins/ndf-shared/skills/refactoring/references/lang-typescript.md @@ -0,0 +1,73 @@ +# TypeScript での手段 + +[data-representation.md](data-representation.md) で選んだ表現を、TypeScript の機能へ対応付ける。 +型注釈を持たない JavaScript は [lang-javascript.md](lang-javascript.md) を読む。 + +| 判定 | 手段 | +| --- | --- | +| 値から値への対応 | `Record` + `as const` | +| 処理方式の切り替え | ハンドラの `Record` | +| 閉じた状態集合 | 判別可能ユニオン | +| 網羅性の静的検査 | `never` への代入 | +| 不変性 | `readonly` / `as const` | +| スキーマ検証 | zod / JSON Schema | +| 一括処理 | 一括 API / `Promise.all`(並行) | +| 失敗の集計 | 結果型に集約 | + +## 網羅性の静的検査 + +```typescript +type Shape = + | { kind: "circle"; r: number } + | { kind: "rect"; w: number; h: number }; + +function area(s: Shape): number { + switch (s.kind) { + case "circle": return Math.PI * s.r ** 2; + case "rect": return s.w * s.h; + default: { + const _exhaustive: never = s; // ケース追加漏れをコンパイル時に検出 + return _exhaustive; + } + } +} +``` + +```typescript +// ❌ 対応表へ移すと、この検査が消える +const handlers: Record number> = { circle: ..., rect: ... }; +``` + +`Record` は任意の文字列を受けるため、ケースの追加漏れも綴り誤りも実行時まで +分からない。**判別可能ユニオンに対する分岐は、そのまま残す。** + +## 変化する値の対応表 + +一方、**変化する業務ルール**は対応表が適する。キーを閉じた型に固定すれば静的検査も残る。 + +```typescript +const DISCOUNT_RATES = { + gold: 0.2, silver: 0.1, bronze: 0, +} as const satisfies Record; // Rank に追加すると欠落を検出 + +const rate = DISCOUNT_RATES[rank]; +``` + +この表を実行時にロードするなら、型を生成していてもロード境界の検証は要る。 + +```typescript +// ❌ 型生成をスキーマ検証の代わりにしている。実体は何も検査されない +const rates = JSON.parse(raw) as Record; + +// ✅ ロード境界で検証してから使う(zod / JSON Schema) +const rates = RatesSchema.parse(JSON.parse(raw)); +``` + +## 並行と並列を混同しない + +`Promise.all` は待ち時間を重ねるだけで、計算時間は減らない。CPU を使う処理を並列化するには +worker が要る。「反復の実行方式」表の「並行処理」と「並列処理」は別の行である。 + +## 出典 + +- [TypeScript Handbook — Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) diff --git a/plugins/ndf-shared/skills/safe-refactoring/references/refactoring-catalog.md b/plugins/ndf-shared/skills/refactoring/references/refactoring-catalog.md similarity index 73% rename from plugins/ndf-shared/skills/safe-refactoring/references/refactoring-catalog.md rename to plugins/ndf-shared/skills/refactoring/references/refactoring-catalog.md index 6c3e5741..412a039b 100644 --- a/plugins/ndf-shared/skills/safe-refactoring/references/refactoring-catalog.md +++ b/plugins/ndf-shared/skills/refactoring/references/refactoring-catalog.md @@ -57,6 +57,32 @@ 分岐が 1 箇所なら分岐のままが読みやすい。**種類が増えるたびに複数箇所を直している**という 事実が、この手法の適用条件である。 +## 対応表への置き換え + +| 項目 | 内容 | +| --- | --- | +| 適用条件 | **変化する業務ルール**(料率・区分・しきい値・優先順位)が制御構文に埋まっている | +| 手順 | 値の対応を表として外へ出す → 未知の入力を失敗として扱う → 適用したルールの識別子と版を**表から取り出せる形にする** | +| やめる条件 | 値が変化しない。網羅性を静的に検査できている分岐である。表に版・スキーマ・検証を用意できない | + +「分岐が多いから表にする」ではない。**変化するから表にする**。判断の材料は +[data-representation.md](data-representation.md) の 3 表にある。 + +この手順で完了するのは、識別子と版を**記録できるデータ構造**を保つところまでである。実際に +記録を出す実装は振る舞いの変更なので、要件がある場合に別の変更として出す +([data-representation.md](data-representation.md) の「判断を記録できるようにする」)。 + +## 一括処理への置き換え + +| 項目 | 内容 | +| --- | --- | +| 適用条件 | 同種で独立した処理を 1 件ずつ繰り返しており、往復回数か実行時間が積み上がっている | +| 手順 | 一括入出力・一括演算・並行・並列のどれに移すかを決める → 置き換える → **計測して効果を確かめる** | +| やめる条件 | 逐次依存・早期終了・メモリ制約がある。移動先を言えない(高階反復への書き換えだけになる) | + +逐次実行のまま高階反復へ書き換えても実行の実体は変わらない。何が得られるかは +[data-representation.md](data-representation.md) の「反復の実行方式」表で確かめる。 + ## 戦略の切り出し | 項目 | 内容 | diff --git a/plugins/ndf-shared/skills/tdd-cycle/SKILL.md b/plugins/ndf-shared/skills/tdd-cycle/SKILL.md index e16ddd9d..249c224b 100644 --- a/plugins/ndf-shared/skills/tdd-cycle/SKILL.md +++ b/plugins/ndf-shared/skills/tdd-cycle/SKILL.md @@ -16,7 +16,7 @@ description: "Write a failing test first, then the smallest implementation that | Skill | 参照している内容 | 未追加のあいだの代替 | | --- | --- | --- | | `requirements-design` | 受け入れ条件の作り方 | 受け入れ条件を「観測可能・一意・テスト可能」な 1 文へ自分で書き下す | -| `safe-refactoring` | 構造改善と現状固定テスト | サイクル内の整理にとどめ、構造改善は別タスクへ切り出す | +| `refactoring` | 構造改善と現状固定テスト | サイクル内の整理にとどめ、構造改善は別タスクへ切り出す | | `quality-gates` | 全体テストの実行とカバレッジ閾値の判定 | 対象プロジェクトのカバレッジツール設定に従い、設定がなければ測定値の記録だけ行う | ## 適用しない対象 @@ -97,7 +97,7 @@ E ImportError: cannot import name 'validate' ← 期待と違う。先にこ ### 4. 整理する テストを**通ったまま**保って構造を整える。整理中にテストが落ちたら、整理をいったん戻す。 -コードスメル起点の本格的な構造改善は `safe-refactoring`※ に委ねる。 +コードスメル起点の本格的な構造改善は `refactoring`※ に委ねる。 ### 5. 次の条件へ進む @@ -123,7 +123,7 @@ E ImportError: cannot import name 'validate' ← 期待と違う。先にこ ## テストの乏しい既存コードでは順序が変わる 変更対象にテストがほとんどない場合、いきなり新しいテストを足すより、**現状の振る舞いを -固定するテスト**を先に置く。手順は `safe-refactoring`※ の現状固定テストに従う。 +固定するテスト**を先に置く。手順は `refactoring`※ の現状固定テストに従う。 ## テストダブルの優先順 diff --git a/scripts/validate-runtime-plugins.sh b/scripts/validate-runtime-plugins.sh index e6073b41..e1efd997 100755 --- a/scripts/validate-runtime-plugins.sh +++ b/scripts/validate-runtime-plugins.sh @@ -38,6 +38,7 @@ done < <(find "$ROOT_DIR/plugins/mcp" -name .mcp.json | sort) run python3 - "$ROOT_DIR" "${FAMILIES[@]}" <<'PY' import json +import re import sys from pathlib import Path @@ -79,6 +80,104 @@ for plugin in codex_marketplace.get("plugins", []): if not (plugin_dir / ".codex-plugin/plugin.json").is_file(): errors.append(f"Codex plugin manifest missing under {source_path}") +# 版数と Skill 数は plugin.json と marketplace の description に重複して書かれている。 +# `.claude-plugin/marketplace.json` と Codex 版 plugin.json は build-runtime-plugins.sh の +# 生成対象ではなく、古い値が残っても JSON としては妥当なため他の検査に掛からない。 +# 実際に版数と Skill 数の取り残しが繰り返し起きたので、Claude 版 plugin.json を基準に突き合わせる。 +VERSION_IN_DESCRIPTION = re.compile(r"\(v(\d+\.\d+\.\d+)\)") +# `<数> ... skills` の形で書く規約。版数(8.0.0)や製品名(E2E)の数字を拾わないよう前後が +# 英数字・ドットでない整数だけを見て、さらに `skills` との間に挟める語を 3 語までに絞る。 +# こうしないと離れた位置にある無関係な数(`8 specialized agents` など)を Skill 数と誤認する。 +DESCRIBED_SKILL_COUNT = re.compile(r"(? None: + if not isinstance(description, str): + return + found = VERSION_IN_DESCRIPTION.search(description) + if not found: + errors.append(f"{label} の description に `(vX.Y.Z)` 形式の版数がない") + elif found.group(1) != version: + errors.append( + f"{label} の description の版数が古い" + f"(description: v{found.group(1)} / {family}-claude の plugin.json: v{version})" + ) + expected = manifest_skill_count(family, runtime) + if expected is None: + return + # 抽出できないこと自体をエラーにする。素通りさせると、Skill 数の記述を消すか書式を変える + # だけでこの検査を無効化できてしまう。 + described = described_skill_count(description) + if described is None: + errors.append( + f"{label} の description から Skill 数を読み取れない" + f"(`<数> ... skills` の形で書く。{runtime}-skills.txt: {expected})" + ) + elif described != expected: + errors.append( + f"{label} の description の Skill 数が manifest と食い違う" + f"(description: {described} / {runtime}-skills.txt: {expected})" + ) + + +for family in families: + claude_plugin_path = root / f"plugins/{family}-claude/.claude-plugin/plugin.json" + if not claude_plugin_path.is_file(): + continue + claude_plugin = read_json(claude_plugin_path) + version = claude_plugin.get("version") + if not isinstance(version, str): + errors.append(f"{family} の claude plugin.json に version がない") + continue + check_description( + f"plugins/{family}-claude/.claude-plugin/plugin.json", + claude_plugin.get("description"), + version, + family, + "claude", + ) + codex_plugin_path = root / f"plugins/{family}-codex/.codex-plugin/plugin.json" + if codex_plugin_path.is_file(): + codex_plugin = read_json(codex_plugin_path) + if codex_plugin.get("version") != version: + errors.append( + f"plugins/{family}-codex/.codex-plugin/plugin.json の version が claude 版と" + f"食い違う(codex: {codex_plugin.get('version')} / claude: {version})" + ) + check_description( + f"plugins/{family}-codex/.codex-plugin/plugin.json", + codex_plugin.get("description"), + version, + family, + "codex", + ) + for plugin in claude_marketplace.get("plugins", []): + if plugin.get("source") != f"./plugins/{family}-claude": + continue + check_description( + f".claude-plugin/marketplace.json の {plugin.get('name')}", + plugin.get("description"), + version, + family, + "claude", + ) + for family in families: shared = root / f"plugins/{family}-shared" for manifest in sorted((shared / "manifests").glob("*-skills.txt")):