- AI ⚙️
- マネジメント
AI生成コードの品質基準|受け入れ基準の表とCIで判定する設定例
AI生成コードの品質基準とは、AIが書いたコードを取り込んでよいかを、誰が判定しても同じ結果になる形で決めた合否の条件です。 人が書いたコードと同じ基準を土台に、AIに特有の起きやすいこと(存在しないパッケージの提案、テストの削除やスキップ、差分の量の増加)に対応する項目を足します。
この記事では、品質ゲートの一覧表、GitHub Actions のワークフロー、しきい値を判定するPythonスクリプト、顧客と合意する受け入れ基準の文例、危険度別の判定表、PRテンプレートを、貼って使える形で載せます。「完成の定義」全般ではなく、AI生成コードに追加する項目に絞っています。
確認環境(確認日 2026年10月7日):Ubuntu 24.04、Python 3.11.15、git 2.43.0、pytest 9.1.1、pytest-cov 7.1.0、coverage 7.16.2、ruff 0.16.10、detect-secrets 1.5.0、pip-licenses 5.5.5。ワークフローの run: に書いたコマンドは、この環境の新しい仮想環境で上から順に実行し、出力を載せています。ワークフローのキーとアクションの版は、GitHub Docs と各アクションのREADMEの記載に合わせています。基準値はすべて例です。
AI生成コードの品質ゲート早見表
| 項目 | 判定方法 | 基準値の例 | 自動か人か |
|---|---|---|---|
| テスト通過 | pytest の終了コード |
失敗0件 | 自動 |
| 全体カバレッジ | coverage.json の合計 |
80%以上 | 自動 |
| 変更ファイルのカバレッジ | 変更したファイルごとの値 | 80%以上(高危険度は95%以上) | 自動 |
| テストのスキップ追加 | 差分の skip・xfail |
追加0件 | 自動 |
| テストの削除 | 差分で消えた def test_ |
0件(理由があれば人が承認) | 自動+人 |
| 静的解析 | ruff check の終了コード |
指摘0件 | 自動 |
| 複雑度 | ruff の C901(循環的複雑度) | 1関数10以下 | 自動 |
| 秘密情報の検出 | detect-secrets-hook |
新規検出0件 | 自動 |
| 依存パッケージの実在 | pip install の成否 |
全パッケージが取得できる | 自動 |
| 依存パッケージのライセンス | pip-licenses と許可リスト |
許可外0件 | 自動 |
| 差分行数 | git diff --numstat |
テスト以外で400行以下 | 自動 |
| 仕様との一致 | 人のレビュー | 受け入れ条件を1つずつ確認 | 人 |
| 設計・既存規約との整合 | 人のレビュー | チェックリストで確認 | 人 |
| AI利用の申告 | PRテンプレートの記入 | 記入あり | 人 |
タブ区切り版です。表計算ソフトのA1セルに貼り付け、「自分たちの基準値」の列を埋めて使います。
項目 判定方法 基準値の例 自動か人か 自分たちの基準値 決めた日
テスト通過 pytest の終了コード 失敗0件 自動
全体カバレッジ coverage.json の合計 80%以上 自動
変更ファイルのカバレッジ 変更したファイルごとの値 80%以上(高危険度は95%以上) 自動
テストのスキップ追加 差分の skip・xfail 追加0件 自動
テストの削除 差分で消えた def test_ 0件 自動+人
静的解析 ruff check の終了コード 指摘0件 自動
複雑度 ruff の C901 1関数10以下 自動
秘密情報の検出 detect-secrets-hook 新規検出0件 自動
依存パッケージの実在 pip install の成否 全パッケージが取得できる 自動
依存パッケージのライセンス pip-licenses と許可リスト 許可外0件 自動
差分行数 git diff --numstat テスト以外で400行以下 自動
仕様との一致 人のレビュー 受け入れ条件を1つずつ確認 人
設計・既存規約との整合 人のレビュー チェックリストで確認 人
AI利用の申告 PRテンプレートの記入 記入あり 人
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「AI生成コードの品質基準|受け入れ基準の表とCIで判定する設定例」
AI生成コードに項目を足す理由
スクラムガイド2020は、完成の定義を「プロダクトに求められる品質基準を満たしたインクリメントの状態を、正式に記述したもの」と説明しています。AIが書いたコードも人が書いたコードも、満たすべき状態は同じです。変わるのは「起きやすいこと」で、そこに対応する判定を足します。
GitHub Docs の「Review AI-generated code」は、AI生成コードのレビューで見る点として次を挙げています。
| GitHub Docs が挙げる点 | この記事の品質ゲートでの対応 |
|---|---|
| まずテストと静的解析を実行する | テスト通過、静的解析 |
| 存在しないAPI・無視された制約・誤ったロジックを探す | 仕様との一致(人) |
| 失敗したテストが直されずに削除・スキップされていないか見る | スキップ追加、テストの削除 |
| 新しいパッケージが実在し、保守されているか確かめる | 依存パッケージの実在 |
| ライセンスが自分たちのものと両立するか確かめる | 依存パッケージのライセンス |
| 複雑・重要な変更はチームでレビューし、チェックリストを使う | 危険度別の判定表、役割分担 |
| スタイル・lint・セキュリティ・カバレッジのCIを用意する | GitHub Actions のワークフロー |
人のレビューで見る観点は、Google の Engineering Practices が挙げる項目(設計、機能、複雑さ、テスト、命名、コメント、スタイル、一貫性、ドキュメント、全行を読む、文脈)が土台になります。AI生成コードでは差分が大きくなりやすいため、「全行を読む」を成り立たせるために差分行数の上限を自動判定に入れています。
なお、生成AIそのものを組み込む機能を開発する場合は、NIST SP 800-218A「Secure Software Development Practices for Generative AI and Dual-Use Foundation Models: An SSDF Community Profile」(2024年7月26日)が、安全なソフトウェア開発の枠組み(SSDF 1.1)にAIモデル開発向けの手順を加えた文書として公開されています。
基準値の決め方
基準値は、外から持ってきた数字ではなく自分たちの直近の値から決めると運用が続きます。
| 項目 | 決め方 | 見直す時期 |
|---|---|---|
| 全体カバレッジ | 現在の値を測り、それを下回らない値にする | 四半期ごと |
| 変更ファイルのカバレッジ | 新しく書くコードは高めに置く。既存の低いファイルは個別に例外を記録 | 四半期ごと |
| 差分行数 | 直近のPRで、レビューに無理がなかった行数の上限を調べる | 指摘漏れが出たとき |
| 複雑度 | 既存コードの最大値を調べ、新規はそれより低く置く | 半年ごと |
| ライセンス | 納品形態と自社の方針で許可リストを決める | 依存を追加するとき |
| スキップ・削除 | 0件を原則とし、例外は理由と期限をPRに書いて人が承認 | 随時 |
基準を初めて入れるときは、まず「落とさずに表示するだけ」の期間を1〜2週間置き、実際の値の分布を見てから合否判定に切り替えると、基準が厳しすぎて進まなくなる事態を避けられます。
CIで判定する設定一式
題材は、会員の有効期限を計算する小さなPythonのリポジトリです。構成は次のとおりです。
sample/
├── .github/
│ ├── pull_request_template.md
│ └── workflows/quality-gate.yml
├── .secrets.baseline
├── gate.toml
├── pyproject.toml
├── requirements.txt
├── requirements-dev.txt
├── src/shop/
│ ├── __init__.py
│ └── member.py
├── tests/test_member.py
└── tools/quality_gate.py
基準値のファイル(gate.toml)
基準値はコードから分け、1つのファイルにまとめます。基準を変えた履歴がGitに残り、誰がいつ変えたかを追えます。
# 品質ゲートの基準値。数字は例です。自分たちの直近の値を見て決めてください。
[normal] # 通常の変更
coverage_total_min = 80 # 全体のカバレッジ(%)
coverage_changed_min = 80 # 変更したファイルごとのカバレッジ(%)
diff_lines_max = 400 # テスト以外の差分行数(追加+削除)
new_skips_max = 0 # 新しく追加された skip / xfail の数
deleted_tests_max = 0 # 削除されたテスト関数の数
[high] # 危険度が高い変更
coverage_total_min = 80
coverage_changed_min = 95
diff_lines_max = 200
new_skips_max = 0
deleted_tests_max = 0
[risk]
# このパスに変更があれば「高」として扱う
high_paths = ["src/shop/auth/", "src/shop/billing/", "src/shop/personal/", "migrations/"]
[diff]
# 差分行数に数えないパス
exclude = ["tests/", "docs/"]
[licenses]
# ライセンス名にこの文字列のどれかを含めば可とする
allow = ["MIT", "BSD", "Apache", "ISC", "PSF"]
ツールの設定(pyproject.toml)
[project]
name = "shop"
version = "0.1.0"
requires-python = ">=3.11"
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
[tool.coverage.run]
branch = true
source = ["src"]
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "W", "B", "C901"]
[tool.ruff.lint.mccabe]
max-complexity = 10
requirements.txt と requirements-dev.txt です。版を固定しておくと、同じ結果を再現できます。
# requirements.txt
python-dateutil==2.9.0.post0
# requirements-dev.txt
pytest==9.1.1
pytest-cov==7.1.0
ruff==0.16.10
detect-secrets==1.5.0
秘密情報検出の基準ファイルは、最初に1回作ってコミットします。
detect-secrets scan > .secrets.baseline
GitHub Actions のワークフロー
.github/workflows/quality-gate.yml として置きます。
name: quality-gate
on:
pull_request:
branches: [main]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
gate:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: '3.11'
cache: 'pip'
- name: Install runtime dependencies
run: pip install -r requirements.txt
- name: List licenses of runtime dependencies
run: |
pip install pip-licenses==5.5.5
pip-licenses --format=json --output-file=licenses.json
- name: Install dev dependencies
run: pip install -r requirements-dev.txt
- name: Lint
run: ruff check .
- name: Detect secrets
run: git ls-files -z | xargs -0 detect-secrets-hook --baseline .secrets.baseline
- name: Test with coverage
run: pytest --cov --cov-report=term --cov-report=json
- name: Quality gate
run: python tools/quality_gate.py --base "origin/${{ github.base_ref }}"
| 設定 | 役割 |
|---|---|
on.pull_request.branches |
main 向けのプルリクエストで実行する |
permissions: contents: read |
トークンの権限を読み取りだけにする |
concurrency |
同じブランチで先に始まった実行を取り消す |
fetch-depth: 0 |
全履歴を取得する。既定値の1では main との差分を計算できない |
github.base_ref |
プルリクエストの取り込み先ブランチ名 |
| 手順の順番 | 本体の依存だけを入れた時点でライセンス一覧を作り、その後に開発用ツールを入れる |
ライセンス一覧を先に作るのは、開発用ツールのライセンスを判定に混ぜないためです。手順が1つでも失敗するとジョブは失敗になります(continue-on-error を付けた手順を除く)。
しきい値を判定するスクリプト(tools/quality_gate.py)
カバレッジ、差分行数、テストのスキップ追加と削除、ライセンスをまとめて判定します。標準ライブラリだけで動きます(tomllib は Python 3.11 以降)。
"""AI生成コードを含む変更の品質ゲート。基準を1つでも満たさなければ終了コード1。
使い方:
python tools/quality_gate.py --base origin/main
"""
import argparse
import json
import re
import subprocess
import sys
import tomllib
SKIP = re.compile(r"pytest\.mark\.(skip|xfail)|pytest\.skip\(|unittest\.skip")
TEST_DEF = re.compile(r"^\s*def (test_\w+)\(")
def git(*args: str) -> str:
return subprocess.run(["git", *args], check=True, capture_output=True,
text=True).stdout
def diff_stat(rng: str, exclude: list[str]) -> tuple[list[str], int]:
"""変更ファイルの一覧と、除外パス以外の差分行数(追加+削除)を返す。"""
changed, lines = [], 0
for row in git("diff", "--numstat", rng).splitlines():
added, deleted, path = row.split("\t")
changed.append(path)
if added != "-" and not path.startswith(tuple(exclude)):
lines += int(added) + int(deleted)
return changed, lines
def coverage_of(path: str, changed: list[str]) -> tuple[float, dict[str, float]]:
"""全体のカバレッジと、変更した src 配下のファイルごとのカバレッジを返す。"""
with open(path, encoding="utf-8") as f:
cov = json.load(f)
per_file = {
p: cov["files"].get(p, {}).get("summary", {}).get("percent_covered", 0.0)
for p in changed
if p.startswith("src/") and p.endswith(".py") and not p.endswith("__init__.py")
}
return cov["totals"]["percent_covered"], per_file
def test_changes(rng: str) -> tuple[int, set[str]]:
"""tests/ で追加された skip/xfail の数と、削除されたテスト関数名を返す。"""
skips, removed, added = 0, set(), set()
for row in git("diff", "-U0", rng, "--", "tests/").splitlines():
if row.startswith(("+++", "---")):
continue
m = TEST_DEF.match(row[1:])
if row.startswith("+"):
skips += bool(SKIP.search(row))
if m:
added.add(m.group(1))
elif row.startswith("-") and m:
removed.add(m.group(1))
return skips, removed - added
def license_issues(path: str, allow: list[str]) -> list[str]:
"""許可リストの文字列を含まないライセンスのパッケージを返す。"""
with open(path, encoding="utf-8") as f:
pkgs = json.load(f)
return [f"{p['Name']}({p['License']})" for p in pkgs
if not any(a in p["License"] for a in allow)]
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--base", default="origin/main")
ap.add_argument("--config", default="gate.toml")
ap.add_argument("--coverage", default="coverage.json")
ap.add_argument("--licenses", default="licenses.json")
args = ap.parse_args()
with open(args.config, "rb") as f:
cfg = tomllib.load(f)
rng = f"{args.base}...HEAD"
changed, diff_lines = diff_stat(rng, cfg["diff"]["exclude"])
hits = [p for p in changed if p.startswith(tuple(cfg["risk"]["high_paths"]))]
level = "high" if hits else "normal"
th = cfg[level]
total, per_file = coverage_of(args.coverage, changed)
worst = min(per_file.values(), default=100.0)
skips, deleted = test_changes(rng)
bad_lic = license_issues(args.licenses, cfg["licenses"]["allow"])
rows = [
("全体カバレッジ", f"{total:.1f}%", f">= {th['coverage_total_min']}%",
total >= th["coverage_total_min"]),
("変更ファイルのカバレッジ", f"{worst:.1f}%",
f">= {th['coverage_changed_min']}%", worst >= th["coverage_changed_min"]),
("差分行数(テスト以外)", str(diff_lines), f"<= {th['diff_lines_max']}",
diff_lines <= th["diff_lines_max"]),
("追加された skip/xfail", str(skips), f"<= {th['new_skips_max']}",
skips <= th["new_skips_max"]),
("削除されたテスト", str(len(deleted)), f"<= {th['deleted_tests_max']}",
len(deleted) <= th["deleted_tests_max"]),
("許可外のライセンス", str(len(bad_lic)), "<= 0", not bad_lic),
]
print(f"危険度: {level}" + (f"(該当: {', '.join(hits)})" if hits else ""))
for name, actual, rule, ok in rows:
print(f"{'PASS' if ok else 'FAIL'} {name}: {actual}(基準 {rule})")
for p, v in sorted(per_file.items()):
print(f" {p}: {v:.1f}%")
for name in sorted(deleted):
print(f" 削除: {name}")
for item in bad_lic:
print(f" ライセンス: {item}")
failed = [name for name, _, _, ok in rows if not ok]
print(f"結果: {'不合格 ' + str(len(failed)) + '件' if failed else '合格'}")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())
判定の中身は次のとおりです。
| 判定 | 取得元 | 補足 |
|---|---|---|
| 危険度 | 変更ファイルのパス | gate.toml の high_paths に当たれば高 |
| 全体カバレッジ | coverage.json の totals |
pytest --cov-report=json が出力 |
| 変更ファイルのカバレッジ | coverage.json の files |
一覧に無いファイルは0%として扱う |
| 差分行数 | git diff --numstat |
tests/ と docs/ は数えない |
| スキップ追加 | git diff -U0 -- tests/ |
追加行の skip・xfail を数える |
| テストの削除 | 同上 | 消えた関数名から、追加された関数名を引く |
| ライセンス | licenses.json |
許可リストの文字列を含まないものを数える |
実行結果:AIが書いた変更が不合格になる例
APIトークンの発行と検証を追加する変更を例にします。次のコードとテストが提案され、そのままコミットされたとします。
# src/shop/auth/token.py(直す前)
"""APIトークンを発行・検証する。"""
import hashlib
import hmac
import os
import time
SIGNING_KEY = "AKIAIOSFODNN7EXAMPLE"
def issue(member_id: int, ttl: int = 3600, now: float | None = None) -> str:
expires = int((now or time.time()) + ttl)
body = f"{member_id}.{expires}"
sig = hmac.new(SIGNING_KEY.encode(), body.encode(), hashlib.sha256).hexdigest()
return f"{body}.{sig}"
def verify(token: str, now: float | None = None) -> int | None:
try:
member_id, expires, sig = token.split(".")
except ValueError:
return None
body = f"{member_id}.{expires}"
expected = hmac.new(SIGNING_KEY.encode(), body.encode(), hashlib.sha256).hexdigest()
if sig != expected:
return None
if int(expires) < (now or time.time()):
return None
return int(member_id)
# tests/test_token.py(直す前)
import pytest
from shop.auth.token import issue, verify
def test_issue_and_verify():
assert verify(issue(42, now=1000.0), now=1001.0) == 42
@pytest.mark.skip(reason="あとで直す")
def test_expired_token_is_rejected():
assert verify(issue(42, ttl=10, now=1000.0), now=2000.0) is None
さらに、既存の tests/test_member.py から test_expires_on_month_end が削除されていました。ワークフローの手順を順に実行した結果です(一部の手順は省略しています)。
=== Lint ===
$ ruff check .
F401 [*] `os` imported but unused
--> src/shop/auth/token.py:4:8
|
2 | import hashlib
3 | import hmac
4 | import os
| ^^
5 | import time
|
help: Remove unused import: `os`
…(省略)…
Found 1 error.
[*] 1 fixable with the `--fix` option.
[exit 1]
=== Detect secrets ===
$ git ls-files -z | xargs -0 detect-secrets-hook --baseline .secrets.baseline
ERROR: Potential secrets about to be committed to git repo!
Secret Type: AWS Access Key
Location: src/shop/auth/token.py:7
…(省略)…
[exit 123]
=== Test with coverage ===
$ pytest --cov --cov-report=term --cov-report=json
…(省略)…
tests/test_member.py .. [ 50%]
tests/test_token.py .s [100%]
Name Stmts Miss Branch BrPart Cover
-------------------------------------------------------------
src/shop/__init__.py 0 0 0 0 100%
src/shop/auth/__init__.py 0 0 0 0 100%
src/shop/auth/token.py 22 4 4 2 77%
src/shop/member.py 6 0 2 0 100%
-------------------------------------------------------------
TOTAL 28 4 6 2 82%
Coverage JSON written to file coverage.json
========================= 3 passed, 1 skipped in 0.06s =========================
[exit 0]
=== Quality gate ===
$ python tools/quality_gate.py --base "origin/main"
危険度: high(該当: src/shop/auth/__init__.py, src/shop/auth/token.py)
PASS 全体カバレッジ: 82.4%(基準 >= 80%)
FAIL 変更ファイルのカバレッジ: 76.9%(基準 >= 95%)
PASS 差分行数(テスト以外): 28(基準 <= 200)
FAIL 追加された skip/xfail: 1(基準 <= 0)
FAIL 削除されたテスト: 1(基準 <= 0)
PASS 許可外のライセンス: 0(基準 <= 0)
src/shop/auth/token.py: 76.9%
削除: test_expires_on_month_end
結果: 不合格 3件
[exit 1]
(確認のため、失敗しても次の手順へ進むようにして全手順を実行しています。GitHub Actions では、手順が失敗した時点でジョブが失敗になります。)
この結果から分かることは次のとおりです。
- テストは「3 passed」で全体カバレッジも82%。テスト通過と全体カバレッジだけを基準にしていると、この変更は合格になる
- 期限切れのテストはスキップされ、既存のテストが1つ消えていた。差分を見る判定を入れて初めて検出できる
src/shop/auth/を触っているため危険度が高になり、変更ファイルのカバレッジ基準が95%に上がった- 文字列として書かれた鍵は、秘密情報検出が行番号つきで示した(例に使ったのは説明用の見本の文字列で、実在の鍵ではありません)
直した後
鍵を環境変数から受け取る形にし、スキップを外し、削除されたテストを戻し、改ざんされたトークンと形式が違うトークンのテストを足しました。
# src/shop/auth/token.py(直した後)
"""APIトークンを発行・検証する。"""
import hashlib
import hmac
import os
import time
def _sign(body: str) -> str:
key = os.environ["SHOP_SIGNING_KEY"].encode() # 鍵は環境変数から受け取る
return hmac.new(key, body.encode(), hashlib.sha256).hexdigest()
def issue(member_id: int, ttl: int = 3600, now: float | None = None) -> str:
current = time.time() if now is None else now
body = f"{member_id}.{int(current + ttl)}"
return f"{body}.{_sign(body)}"
def verify(token: str, now: float | None = None) -> int | None:
try:
member_id, expires, sig = token.split(".")
except ValueError:
return None
if not hmac.compare_digest(sig, _sign(f"{member_id}.{expires}")):
return None
current = time.time() if now is None else now
if int(expires) < current:
return None
return int(member_id)
# tests/test_token.py(直した後)
import pytest
from shop.auth.token import issue, verify
@pytest.fixture(autouse=True)
def signing_key(monkeypatch):
monkeypatch.setenv("SHOP_SIGNING_KEY", "test-only-key")
def test_issue_and_verify():
assert verify(issue(42, now=1000.0), now=1001.0) == 42
def test_expired_token_is_rejected():
assert verify(issue(42, ttl=10, now=1000.0), now=2000.0) is None
def test_tampered_token_is_rejected():
member_id, expires, sig = issue(42, now=1000.0).split(".")
assert verify(f"43.{expires}.{sig}", now=1001.0) is None
def test_malformed_token_is_rejected():
assert verify("not-a-token") is None
def test_default_clock_is_used():
assert verify(issue(42)) == 42
=== Lint ===
$ ruff check .
All checks passed!
[exit 0]
=== Detect secrets ===
$ git ls-files -z | xargs -0 detect-secrets-hook --baseline .secrets.baseline
[exit 0]
=== Test with coverage ===
$ pytest --cov --cov-report=term --cov-report=json
…(省略)…
src/shop/auth/token.py 22 0 4 0 100%
src/shop/member.py 6 0 2 0 100%
-------------------------------------------------------------
TOTAL 28 0 6 0 100%
Coverage JSON written to file coverage.json
============================== 8 passed in 0.07s ===============================
[exit 0]
=== Quality gate ===
$ python tools/quality_gate.py --base "origin/main"
危険度: high(該当: src/shop/auth/__init__.py, src/shop/auth/token.py)
PASS 全体カバレッジ: 100.0%(基準 >= 80%)
PASS 変更ファイルのカバレッジ: 100.0%(基準 >= 95%)
PASS 差分行数(テスト以外): 29(基準 <= 200)
PASS 追加された skip/xfail: 0(基準 <= 0)
PASS 削除されたテスト: 0(基準 <= 0)
PASS 許可外のライセンス: 0(基準 <= 0)
src/shop/auth/token.py: 100.0%
結果: 合格
[exit 0]
自動判定が通っても人が見る点
直す前のコードは、署名を sig != expected で比べていました。lint もテストも通る書き方ですが、Python の公式ドキュメントは、外部から渡されたダイジェストと比べるときは == ではなく hmac.compare_digest() を使うことを勧めています(比較にかかる時間から情報が漏れるのを抑えるため)。直した後のコードではこれに置き換えました。こうした「動くが、その場面にふさわしくない書き方」は自動判定の外にあり、人のレビューの担当です。
存在しないパッケージ名とライセンスの判定
存在しないパッケージ名が requirements.txt に入っていると、依存のインストール手順が次のように止まります。
$ pip install --dry-run sample-nonexistent-helper-0001
ERROR: Could not find a version that satisfies the requirement sample-nonexistent-helper-0001 (from versions: none)
ERROR: No matching distribution found for sample-nonexistent-helper-0001
インストールが通ることは「その名前のパッケージが存在する」ことまでしか示しません。よく似た名前の別パッケージである可能性は残るため、新しい依存が増えたPRでは、配布元・更新状況を人が確認する項目をテンプレートに入れます。
ライセンスは、本体の依存だけを入れた時点の licenses.json で判定します。この例の中身と、許可リストに無いライセンスの行を1件足して判定した結果です。
[{"License": "Apache Software License; BSD License", "Name": "python-dateutil", "Version": "2.9.0.post0"},
{"License": "MIT License", "Name": "six", "Version": "1.17.0"}]
FAIL 許可外のライセンス: 1(基準 <= 0)
ライセンス: sample-gpl-lib(GPL-3.0-or-later)
結果: 不合格 1件
どのライセンスを許可するかは、納品の形と自社の方針で決まります。許可リストは法務・管理部門と相談して決め、gate.toml に書いておきます。
変更の危険度別に基準を重くする判定表
すべての変更に最も重い基準をかけると進みが遅くなり、すべてに軽い基準をかけると重要な箇所の確認が薄くなります。変更の対象で危険度を決め、基準と承認者を変えます。
| 変更の対象 | 危険度 | 追加する基準 | 承認者 |
|---|---|---|---|
| 認証・認可・権限 | 高 | 変更ファイルのカバレッジ95%、差分200行以下、異常系テスト必須 | 人2名(うち1名は責任者) |
| 課金・料金計算 | 高 | 同上+境界値と端数のテスト、計算例を仕様と照合 | 人2名(うち1名は責任者) |
| 個人情報の取り扱い | 高 | 同上+ログ出力に含めない確認、保存先の確認 | 人2名(うち1名は責任者) |
| データ移行・スキーマ変更 | 高 | 戻し方の手順、検証環境での実行記録 | 人2名 |
| CI・権限・基盤の設定 | 高 | 基準値ファイルの変更は理由を記載 | 責任者 |
| 外部連携・公開API | 中 | 契約(入出力)のテスト、互換性の確認 | 人1名+担当者 |
| 通常の機能追加・修正 | 中 | 早見表の基準 | 人1名 |
| 画面の文言・文書・コメント | 低 | テスト通過、静的解析 | 人1名(AI一次レビュー可) |
危険度を上げ下げする条件です。
| 条件 | 扱い |
|---|---|
| 新しい依存パッケージを追加した | 1段階上げる |
| 既存テストを削除・スキップした | 1段階上げ、理由を人が承認 |
| 差分が上限を超えた | PRを分割する。分割できない場合は責任者が承認 |
| 生成部分が大半で、起票者が内容を説明できない | 取り込まず、起票者が理解してから再依頼 |
| 自動判定の基準値を同じPRで変えた | 高として扱う |
スクリプトでは、gate.toml の high_paths に高危険度のディレクトリを書くことで、この表の「高」を自動で適用しています。
顧客と合意する受け入れ基準の文例
受け入れ基準に入れておくと認識がそろう項目です。【 】を書き換えて使います。
■ AIを利用して作成した成果物の受け入れ基準
1. AI利用の申告
受注者は、成果物の作成にAIツールを利用する場合、利用するツール名と利用範囲
(設計書の下書き、ソースコード、テストコード、レビューの一次確認)を
【着手前に書面で】申告する。
2. 入力する情報
受注者は、【本番データ、認証情報、個人情報】をAIツールに入力しない。
発注者の既存ソースコードの入力は、【発注者の事前承諾がある範囲】に限る。
3. 検証責任
AIが生成した部分を含め、成果物の内容は受注者が確認し、受注者の成果物として
納品する。検収および契約不適合の扱いは、AIを利用しない場合と同じとする。
4. 自動判定の基準
納品するソースコードは、次の基準をすべて満たすこと。
(1) 自動テストがすべて成功している
(2) テストのカバレッジが全体で【80】%以上、変更したファイルごとに【80】%以上
(3) 静的解析の指摘が0件
(4) 秘密情報(鍵、パスワード、トークン)の検出が0件
(5) 追加した依存パッケージが、【別紙の許可ライセンス一覧】の範囲内
(6) 既存のテストを削除・スキップした場合、理由が変更記録に記載されている
5. 人による確認
すべての変更は、起票者以外の担当者1名以上がレビューする。
【認証、課金、個人情報】に関わる変更は、2名以上がレビューする。
6. 記録の提出
受注者は、納品時に【自動判定の結果、レビュー記録、AI利用の申告内容】を提出する。
7. 生成物の扱い
AIが生成した部分を含む成果物の権利の帰属は、【契約書第◯条】に従う。
第三者の権利に関する確認の範囲は、【別途協議のうえ定める】。
8. 基準の変更
基準値を変更する場合は、【双方の責任者の合意】を必要とし、変更日と理由を記録する。
権利や責任に関わる条項は、契約書と法務担当の方針に合わせて調整してください。
あいまいな基準を直した例
| 直す前 | 判定しにくい理由 | 直した後 |
|---|---|---|
| AIが生成したコードは十分にレビューすること | 「十分」の程度が人によって違う | 起票者以外の1名以上が全行を確認し、承認の記録を残す |
| 品質に問題がないこと | 何を見るかが決まっていない | 自動テスト成功、静的解析0件、秘密情報の検出0件 |
| テストを適切に書くこと | 量も対象も決まっていない | 変更したファイルごとにカバレッジ80%以上、異常系を1件以上 |
| ライセンスに注意すること | 許可・不許可の線が無い | 追加した依存が別紙の許可ライセンス一覧の範囲内 |
| AIの利用は必要に応じて報告すること | 報告の時期と内容が決まっていない | 着手前にツール名と利用範囲を書面で申告 |
| 重要な箇所は慎重に確認すること | 「重要」の範囲が決まっていない | 認証・課金・個人情報に関わる変更は2名以上がレビュー |
直した後の文は、どれも「はい・いいえ」か数値で答えられます。 判定できる文にしておくと、検収のときに解釈の違いが出にくくなります。
役割分担表とPRテンプレートのAI申告欄
| 役割 | 担当すること | 担当しないこと |
|---|---|---|
| 起票者 | 生成コードを読んで理解する、テストを書く、AI利用を申告する、自動判定を通す | 自分の変更を自分で承認する |
| AIによる一次レビュー | 書式、命名、見落としやすい分岐、テスト不足の候補を挙げる | 合否の判定、承認 |
| 人のレビュアー | 仕様との一致、設計、既存規約との整合、指摘が正しいかの判断 | 自動判定で分かることの手作業での確認 |
| 承認者(責任者) | 高危険度の変更の承認、基準の例外と基準値変更の承認 | 個々の書式の指摘 |
AIによる一次レビューは「候補を挙げる」までとし、取り込むかどうかは人が決めます。 AIが書き、AIがレビューした変更をそのまま通すと、同じ見落としが重なる可能性があるためです。
PRテンプレートは .github/pull_request_template.md に置きます(リポジトリ直下や docs/ に置く方法もあります)。
## 変更の内容
<!-- 何を、なぜ変えたか -->
## AI利用の申告
- [ ] AIを利用していない
- [ ] AIを利用した(下を記入)
| 項目 | 記入 |
|---|---|
| 利用したツール | |
| AIが生成した範囲(ファイル・関数) | |
| 生成後に自分で直した点 | |
| AIに入力した情報(コード、設計書など) | |
## 起票者の確認
- [ ] 生成されたコードを全行読み、処理を自分の言葉で説明できる
- [ ] 存在しない関数・引数・設定項目を呼んでいないことを、実行またはドキュメントで確認した
- [ ] 既存のテストを削除・スキップしていない(した場合は理由と期限を下に書く)
- [ ] 新しい依存パッケージは無い(ある場合は名前・配布元・ライセンスを下に書く)
- [ ] 鍵・パスワード・個人情報をコードやテストデータに書いていない
- [ ] 異常系(入力が空、形式違い、期限切れ、権限なし)のテストを足した
## 危険度
- [ ] 高:認証・認可/課金/個人情報/データ移行/CI・権限の設定
- [ ] 中:外部連携・公開API/通常の機能
- [ ] 低:文言・文書・コメントのみ
## 例外の申請(ある場合のみ)
<!-- 満たせない基準、理由、いつまでに解消するか -->
## レビュアーへの依頼
<!-- 特に見てほしい点 -->
よくあるつまずきと対応
| 起きること | 理由 | 対応 |
|---|---|---|
| 品質ゲートで差分が0行になる | チェックアウトが最新の1コミットだけ | fetch-depth: 0 を指定する |
| カバレッジの基準で以前からあるファイルが落ちる | 既存コードの値が低い | 変更ファイルの基準に例外を記録し、期限を決めて上げる |
| 秘密情報検出が見本の値に反応する | 形式が鍵に似ている | 基準ファイルを更新するか、行に許可コメントを付けて理由を残す |
| 基準値が黙って下げられる | 基準ファイルの変更が見過ごされる | 基準ファイルの変更は高危険度として責任者が承認 |
| 差分行数の上限でPRが止まる | 生成量が多い | 機能のまとまりで分割し、生成物(ロックファイルなど)は除外に入れる |
| ライセンス名の表記がそろわない | パッケージごとに書き方が違う | 許可リストを部分一致にし、判定外になったものを人が確認 |
よくある質問
Q. カバレッジは何%にすればよいですか? A. 決まった正解はありません。現在の値を測り、それを下回らない値から始め、新しく書くファイルには高めの値を置く方法が続けやすいです。この記事の80%と95%は例です。
Q. AIが書いたコードかどうかを、機械的に見分けて基準を変えるべきですか? A. 見分ける仕組みに頼るより、すべての変更に同じ自動判定をかけ、AI利用は申告欄で把握する方法が確実です。基準を重くするかどうかは、誰が書いたかではなく、変更の対象(認証・課金・個人情報など)で決めます。
Q. テストもAIに書かせてよいですか? A. 下書きとして使えます。ただし、期待値がコードの動作をそのまま写したものになっていないかを人が確認します。仕様から期待値を決め、異常系を足すところまでが起票者の担当です。
Q. 完成の定義(DoD)とは別に管理するのですか?
A. 別の文書を増やすより、既存の完成の定義に「AI生成コードの追加項目」として足すのが分かりやすい方法です。自動判定の基準値は gate.toml のようなファイルに、人が確認する項目はPRテンプレートに置くと、文書と運用がずれにくくなります。
まとめ
- 品質基準は「誰が判定しても同じ結果になる」形で書く。数値か、はい・いいえで答えられる文にする
- テスト通過と全体カバレッジだけでは足りない。例では、スキップされたテストと削除されたテストがあっても「3 passed・82%」だった
- AI生成コードに足す項目は、スキップ追加・テスト削除・依存の実在とライセンス・差分行数・AI利用の申告
- 基準値はファイルに分け、変更を責任者が承認する
- 変更の対象で危険度を決め、認証・課金・個人情報は基準と承認者を重くする
- 自動判定は入口で、仕様との一致と書き方の妥当性は人が見る
- 顧客との受け入れ基準には、申告・検証責任・自動判定の基準・記録の提出を入れる
参考・出典
いずれも2026年10月7日に閲覧しました。
- Scrum Guide 2020 https://scrumguides.org/scrum-guide.html
- GitHub Docs「Review AI-generated code」 https://docs.github.com/en/copilot/tutorials/review-ai-generated-code
- GitHub Docs「Workflow syntax for GitHub Actions」 https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax
- GitHub Docs「Contexts reference」 https://docs.github.com/en/actions/reference/workflows-and-actions/contexts
- GitHub Docs「Creating a pull request template for your repository」 https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository
- actions/checkout README https://github.com/actions/checkout
- actions/setup-python README https://github.com/actions/setup-python
- Google Engineering Practices「What to look for in a code review」 https://google.github.io/eng-practices/review/reviewer/looking-for.html
- NIST SP 800-218A https://csrc.nist.gov/pubs/sp/800/218/a/final
- Python ドキュメント「hmac」 https://docs.python.org/3/library/hmac.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「AI生成コードの品質基準|受け入れ基準の表とCIで判定する設定例」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。