0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI時代における品質重視型プロジェクト運用モデルの提案(3/3)

Last updated at Posted at 2025-12-23

CIで“ドリフトを検知して弾く”と、互換維持 vs 改善/仕様変更の運用ルール

本トピックは3部構成の最終回となっています。

前編(1/3)では運用モデルの全体像、
中編(2/3)では成果物設計(Spec / BR / SC / I/F定義 / Delta)と参照方向ルールを具体化しました。

後編(3/3)では、その運用を「頑張って維持する」状態から卒業させるために、

  • CIでトレーサビリティを機械検査して“弾く”(ドリフト検知の自動化)
  • 互換維持 vs 改善/仕様変更を分類し、受入テスト・レビュー(特にQA)の判断基準を固定する

…の2点を、現場で採用できる粒度まで落とし込みます。

伝えたいこと

  • AI時代は「仕様(Spec)を書く」だけでは足りません。つながり(Traceability)をCIで検査して“壊れたら弾く” 必要があります。
  • 最小構成でも、次をCIで機械検査できます:
    • BRがSpecに存在する
    • SCが存在し、BRとI/F定義に接続している
    • テスト/実装が変更したBR/SCを参照している(最低限)
  • 品質ゲートを強くするほど、互換維持 vs 改善/仕様変更を曖昧にすると混乱します。
    → だから、分類ラベルをSpec/Deltaに書き、受入テストとQAレビューの判断基準を固定します。

CIで“ドリフトを検知して弾く”(Traceabilityの機械化)

なぜトレーサビリティが必要なのか(現状の課題と、運用の破綻リスク)

ここまで読んで「Spec/SC/I/F定義を用意するのは分かった。しかし なぜ それらを“つなぐ”必要があるのか?」と感じた方もいると思います。
結論から言うと、この運用モデルは “つながりを維持できないと破綻する” からです。

AI時代は、次の理由で 整合性の崩れ(ドリフト)が“必然” になります。

  • 変更量が増える:AIは実装・修正を高速化します。その結果、1チケットあたりの差分が大きくなりやすい
  • 並列化が進む:レビュー待ち中に次工程へ先行着手する運用では、上流と下流が時間差で更新される
  • 「それっぽい」修正が紛れ込む:AIは意図を推測して補完します。推測が当たれば便利ですが、外れると静かにズレます
  • 人間のレビュー帯域は増えない:差分が増えるほど「読み切り」は破綻し、見落としの確率が上がります

現状起きがちな課題(手動運用の限界)

トレーサビリティが無い(または弱い)状態では、典型的に次が起きます。

  1. 上流変更の“追随漏れ”が発生する

    • Specの境界・例外条件が変わったが、SCが古いまま
    • APIのエラー形式が変わったが、受入シナリオやE2Eが古いまま
    • DB制約を変えたが、ユースケース側の期待結果が追随していない
  2. テストが“通っているのに違う”が起きる

    • テストが網羅していない境界条件で挙動がズレている
    • AIがテストを都合よく弱め、CIが緑のまま品質が落ちている
    • 「通ること」が目的化し、意図(Spec)から離れた抜け道実装が残る
  3. 真実のソースが分裂する(どれが正しいか分からない)

    • Spec・SC・I/F定義・実装がそれぞれ別方向に進み、会議で“擦り合わせ”が発生する
    • QAは「これは不具合か?改善か?仕様変更か?」の判断材料が足りず、レビューが詰まる

この運用モデルで“大変な部分”はどこか

率直に言うと、大変なのは 成果物を作ることではなく、成果物間の整合を維持することです。

  • 上流(Spec/SC/I/F定義)は“定義資産”なので、更新すると 下流(テスト/実装)も必ず追随させる必要がある
  • しかもAIがいることで差分が増え、追随漏れが起きやすい
  • 手動で「この変更はどのSCに影響するか」「どのI/F定義を触ったか」を毎回追うのは、スケールしない

つまり、トレーサビリティは「きれいに管理できたら嬉しい」ものではなく、
**運用をスケールさせるための“安全装置”**です。

  • つながりが壊れたら、CIで検知して止める
  • 検知メッセージで、次に直すべき場所を機械的に指し示す

この仕組みが無いと、最終的に「人間が全部読む」しかなくなり、AI導入で加速した分だけ疲弊が増えます。

何を“ドリフト”と呼ぶのか

このモデルにおけるドリフトとは、ざっくり言うと次の状態です。

  • 上流(Spec/SC/I/F定義)と下流(テスト/実装)の整合が壊れている
  • しかも、人が目で追わないと気づけない(=AIがすり抜けやすい)

AIが入ると、差分が増えます。差分が増えるほど、人間の「読み切り品質」は下がります。
だから、“読んで検知する”ではなく、**CIで“弾ける形”**に寄せます。

最小の検査ルール(まずはこれだけで効果が出る)

中編で作った成果物設計を前提に、CIで検査する最小ルールを定義します。

ルールA:IDの存在(定義の一意性)

  • BR-xxx は Spec 内に存在する
  • SC-xxx はシナリオ定義(例:docs/scenarios/)に存在する

ルールB:SCの接続(「シナリオは孤立させない」)

  • SC-xxx少なくとも1つの BR-xxx を参照する
  • SC-xxxI/F定義(UI/API/DB)のいずれか(または複数)に接続する

ルールC:下流の参照(「AIの作業が上流から逸れない」)

  • 変更したBR/SCについて、テストコード・実装コードが参照を持つ(コメントでもOK)

※ポイント:ここで “テストID” や “実装ID” を作り始めると冗長になりやすいので、
 テスト/実装は 参照する側に寄せて、上流IDへのリンクを残すだけにします。

CIで何を実行するか(GitHub Actionsのイメージ)

CIでは、一般的に次の順序にすると壊れにくいです。

  1. API定義の構文検査(例:OpenAPIのlint / validate)
  2. DBマイグレーションの最低限検査(SQL lint、あるいはテスト用DBへ適用)
  3. Traceability検査(下記の check-traceability を実行)
  4. unit / e2e 実行(必要な範囲)

check-traceability はどう作るか(言語に依存しない設計)

ここは技術スタックに依存しますが、考え方は共通です。
「成果物のIDを収集 → 参照を収集 → ルールに照らして欠落を落とす」 だけです。

入力(例)

  • Spec(Markdown)
  • SC(Markdown)
  • I/F定義(YAML/OpenAPI/DDL等。最低限“識別子”が取れればOK)
  • テスト/実装(ソース。コメントでも良い)

収集するもの(例)

  • BR一覧:BR-[A-Z]+-[0-9]+
  • SC一覧:SC-[A-Z]+-[0-9]+
  • I/F一覧:UI-... / API-... / DB-...(プロジェクトの規約でOK)
  • 参照:各ファイル内の BR-... / SC-... / UI-... / API-... / DB-...

実装例(Python 3 + ripgrep)

上の ````text` は「擬似コード(読み物)」で、特定の実在言語ではありません。
現場でそのまま試せる形にするため、ここでは Python 3 の最小スクリプト例で示します。

  • 依存:Python 3 / rg(ripgrep。任意。無い場合は --no-rg で代替)
  • 目的:まず ルールA/B(定義の存在・SC接続) をCIで落とす
  • 追加:必要なら ルールC(変更IDが下流に参照される)--diff-range で有効化

使い方(例)

# 例: ローカル or CI(全体検査:ルールA/B)
python3 scripts/traceability/check_traceability.py \
  --spec-glob "docs/specs/**/spec.md" \
  --sc-glob "docs/scenarios/**/*.md" \
  --if-pattern "UI-[A-Z0-9_-]+" --if-pattern "API-[A-Z0-9_-]+" --if-pattern "DB-[A-Z0-9_-]+" \
  --code-glob "src/**" --code-glob "tests/**"

# 例: PR差分だけ厳しく見る(ルールC:変更IDが下流に参照されているか)
# ※base/headはCI側で適切に渡してください(例:PR base SHA と HEAD SHA)
python3 scripts/traceability/check_traceability.py \
  --spec-glob "docs/specs/**/spec.md" \
  --sc-glob "docs/scenarios/**/*.md" \
  --code-glob "src/**" --code-glob "tests/**" \
  --diff-range "<base_sha>..<head_sha>"

scripts/traceability/check_traceability.py(最小例)

#!/usr/bin/env python3
from __future__ import annotations

import argparse
import glob
import os
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable


BR_RE = re.compile(r"BR-[A-Z0-9]+-[0-9]+")
SC_RE = re.compile(r"SC-[A-Z0-9]+-[0-9]+")


@dataclass(frozen=True)
class ScenarioInfo:
    sc_id: str
    br_refs: set[str]
    if_refs: set[str]
    path: str


def read_text(path: str) -> str:
    return Path(path).read_text(encoding="utf-8", errors="ignore")


def expand_globs(globs: list[str]) -> list[str]:
    files: list[str] = []
    for g in globs:
        files.extend(glob.glob(g, recursive=True))
    # 既存ファイルのみ、重複除去
    uniq = []
    seen = set()
    for f in files:
        if os.path.isfile(f) and f not in seen:
            uniq.append(f)
            seen.add(f)
    return uniq


def collect_ids(paths: Iterable[str], regex: re.Pattern[str]) -> set[str]:
    found: set[str] = set()
    for p in paths:
        found.update(regex.findall(read_text(p)))
    return found


def first_id(text: str, regex: re.Pattern[str]) -> str | None:
    m = regex.search(text)
    return m.group(0) if m else None


def collect_scenarios(sc_files: list[str], if_patterns: list[str]) -> list[ScenarioInfo]:
    if_res = [re.compile(p) for p in if_patterns]
    scenarios: list[ScenarioInfo] = []
    for f in sc_files:
        body = read_text(f)
        sc_id = first_id(body, SC_RE)
        if not sc_id:
            # SCファイルだがIDが取れない場合は、そのままエラーにする方が運用が安定する
            raise ValueError(f"SC id not found in: {f}")

        br_refs = set(BR_RE.findall(body))
        if_refs: set[str] = set()
        for r in if_res:
            if_refs.update(r.findall(body))

        scenarios.append(ScenarioInfo(sc_id=sc_id, br_refs=br_refs, if_refs=if_refs, path=f))
    return scenarios


def collect_code_refs(code_globs: list[str], extra_patterns: list[re.Pattern[str]]) -> set[str]:
    code_files = expand_globs(code_globs)
    all_res = [BR_RE, SC_RE, *extra_patterns]
    refs: set[str] = set()
    for f in code_files:
        body = read_text(f)
        for r in all_res:
            refs.update(r.findall(body))
    return refs


def git_diff_text(diff_range: str) -> str:
    # diff_range 例: "<base>..<head>"
    cmd = ["git", "diff", "--unified=0", diff_range]
    return subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT)


def collect_changed_ids(diff_range: str, if_patterns: list[str]) -> set[str]:
    diff = git_diff_text(diff_range)
    ids = set(BR_RE.findall(diff)) | set(SC_RE.findall(diff))
    for p in if_patterns:
        ids |= set(re.findall(p, diff))
    return ids


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--spec-glob", action="append", required=True)
    ap.add_argument("--sc-glob", action="append", required=True)
    ap.add_argument("--code-glob", action="append", default=[])
    ap.add_argument("--if-pattern", action="append", default=["UI-[A-Z0-9_-]+", "API-[A-Z0-9_-]+", "DB-[A-Z0-9_-]+"])
    ap.add_argument("--diff-range", default=None, help='Optional. e.g. "<base>..<head>"')
    ap.add_argument("--no-rg", action="store_true", help="Not used in this minimal script (reserved).")
    args = ap.parse_args()

    spec_files = expand_globs(args.spec_glob)
    sc_files = expand_globs(args.sc_glob)

    br_defs = collect_ids(spec_files, BR_RE)
    scenarios = collect_scenarios(sc_files, args.if_pattern)

    errors: list[str] = []

    # ルールA:定義の存在
    if not br_defs:
        errors.append("No BR definitions found in spec files (BR-... not detected).")
    if not scenarios:
        errors.append("No SC files found (or SC-... not detected).")

    # ルールB:SCの接続(BR参照+I/F参照)
    for sc in scenarios:
        if not sc.br_refs:
            errors.append(f"{sc.sc_id} has no BR references: {sc.path}")
        else:
            missing = sorted([br for br in sc.br_refs if br not in br_defs])
            if missing:
                errors.append(f"{sc.sc_id} references missing BR defs {missing}: {sc.path}")

        if not sc.if_refs:
            errors.append(f"{sc.sc_id} has no I/F links (UI/API/DB): {sc.path}")

    # ルールC(任意):変更IDが下流(src/tests)に参照される
    if args.diff_range and args.code_glob:
        # 下流参照の集合を作っておき、変更IDが含まれるか確認する
        # ※要件に合わせて「変更した上流IDだけ」や「変更したファイルだけ」に最適化してよい
        extra_res = [re.compile(p) for p in args.if_pattern]
        code_refs = collect_code_refs(args.code_glob, extra_res)
        changed = collect_changed_ids(args.diff_range, args.if_pattern)

        for cid in sorted(changed):
            if cid not in code_refs:
                errors.append(f"Changed id is not referenced in code (src/tests): {cid}")

    if errors:
        print("Traceability check failed:", file=sys.stderr)
        for e in errors:
            print(f"- {e}", file=sys.stderr)
        return 1

    print("Traceability check OK")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

※重要:最後の changed_ids_are_referenced_in_code が効きます。
 「上流を変えたのに下流が追随していない」状態を、レビュー前に弾けるからです。

失敗時の出し方(CIメッセージの設計)

CIが落ちるとき、メッセージが雑だと運用が崩れます。
人間が次の一手を迷わないように、“欠落を列挙”します。

  • SC-ORDER-001 references missing BR: BR-ORDER-999
  • SC-ORDER-002 has no interface links (UI/API/DB)
  • Changed BR-ORDER-003 but no references found in src/ or tests/

互換維持 vs 改善/仕様変更:分類が曖昧だと、QAもAIも迷う

CIとテストで品質ゲートを強くする分、
「何を守るのか(互換)」と「何を変えるのか(改善/仕様変更)」 が曖昧だと混乱します。

特に移行・置換案件では「旧仕様を再現すること」自体が価値になる一方で、旧仕様には暗黙仕様や既知バグが混ざり得ます。
この“整理”をせずにテストを固めると、後から地獄になります。

決めるべき分類(例)

  • 互換維持(Compatibility):旧仕様の挙動をそのまま維持する(期待値は“変えない”)
  • 改善(Improvement):ユーザーにとって望ましい挙動へ直す(期待値を“良い方向へ”変える)
  • 仕様変更(Change):要件上の理由で挙動を変える(期待値を“意図的に”変える)
  • 既知バグ踏襲(Bug-for-bug Compatibility):旧システムの不具合を移行都合で一時的に踏襲する

※この分類は「良し悪し」ではなく、関係者の期待値・受入条件・移行影響を揃えるためのラベルです。

分類を誤ると起きること(典型)

  • 改善なのに互換として扱い、QAが「落とすべき不具合」と「意図した変更」を判別できない
  • 互換なのに改善として扱い、顧客の業務が想定外に変わる(移行トラブル)
  • “テストが正”の運用ゆえに、分類が曖昧な変更はテスト修正の判断が属人化し、リードタイムが伸びる

どう運用に落とすか(このモデルでの位置づけ)

分類は、口頭合意だと必ず崩れます。よって、成果物に埋め込みます。

  • Spec(SDD)側:変更の意図(WHY/WHAT)と、互換/改善/変更の結論を最低限の文章で明記する
  • Delta(delta.md)側:分類と意思決定の経緯を記録する(「なぜ踏襲/なぜ改善するか」)
  • 受入テスト(ATDD)側:分類に沿って期待結果を固定し、QAレビューの判断基準を揃える(特に“互換 vs 改善”)
  • CIゲート:(可能なら)分類タグの欠落や未決事項の残存を検知し、警告または弾く

Spec / Delta の最小テンプレ

Specに入れる(例)

## 変更分類(Compatibility / Improvement / Change / Bug-for-bug)
- 分類:Compatibility
- 影響範囲:<ユーザー影響/移行影響>
- 受入の観点:<QAが見るべきポイント>

Deltaに入れる(例)

# Delta(Decision Log)

- 日付:2025-xx-xx
- 変更対象:BR-ORDER-002 / SC-ORDER-005
- 分類:Improvement
- 変更理由:<ユーザー価値障害運用負債など>
- 代替案:<不採用理由を1行>
- 注意点:<互換で壊れる可能性移行手順など>

受入テスト(SC)とQAレビューの“判断基準”を固定する

分類が決まると、受入テストやQAレビューの迷いが大きく減ります。

Compatibility(互換維持)

  • 期待結果は原則変えない
  • SCの変更が必要なら、その理由(既存の期待結果の誤り/テストの不備)をDeltaに必ず残す
  • “旧仕様の再現”を優先する(改善は別チケットへ)

Improvement(改善)

  • 期待結果を良い方向へ変える(SCの更新が正当)
  • ただし「改善の定義」が曖昧だと揉めるので、SpecにWHY/アウトカムを短く固定する
  • 影響範囲(誰がどう嬉しいか/既存ユーザーへの副作用)をレビュー観点に入れる

Change(仕様変更)

  • 期待結果は意図的に変わる
  • 影響範囲(UI/API/DB、移行、運用手順)をセットでレビュー
  • “移行で困る人”を想定し、注意点をDeltaに残す

Bug-for-bug

  • 互換維持の一種だが、将来必ず返済が必要
  • Deltaに「なぜ今踏襲するか」「いつ返済するか(期限 or 条件)」を残す
  • 受入テストには TODO を残し、踏襲であることが見えるようにする

“壊れたら弾く”運用を、現場に定着させるコツ

最初から完璧に弾こうとしない

最初から「全部CIで落とす」は反発を生みます。おすすめは段階導入です。

  1. まずは 警告(warning) を出す(欠落の見える化)
  2. チームが慣れたら fail に切り替える(強制)
  3. 強制対象は「上流の整合」から始め、次に「分類の欠落」へ広げる

AI運用の観点:CIは“最終命令”として効く

AIにとって最も明確なフィードバックは、CIの赤です。
「落ちた理由」がIDベースで明確なら、AIは修正ループを自走できます。

  • 人間:分類・境界・期待結果(上流)を決める
  • AI:実装・テスト・修正ループを回す
  • CI:ズレを検知し、統合を止める

この形に寄せることで、壁打ち疲れは「人間の会話」ではなく 機械のフィードバックへ移ります。

まとめ

  • TraceabilityをCIで機械検査し、壊れたら弾くことで、AI時代のドリフトを現実的に抑えられます
  • 互換維持 vs 改善/仕様変更を成果物に埋め込み、受入テストとQAレビューの判断基準を固定すると、リードタイムが安定します
  • 最終的に、AIは「テストを通す」だけでなく、“上流に従う”ことをCIで強制されるため、自走しやすくなります

シリーズの補足(次にやるなら)

このシリーズは運用モデルの骨格です。次に磨くなら、以下が効きます。

  • 役割別チェックリスト(PdM / QA / Eng)を固定して、レビューを“迷わない作業”にする
  • check-traceability の出力を、SC一覧・BR一覧として逆引き生成し、Specからの可視性を補完する
  • 「既知バグ踏襲」の返済を、プロダクトロードマップへ接続する(期限が無いと永遠に残る)
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?