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?

仕様駆動開発以前を理解する~ヴァイブコーディングとは何だったのか~

0
Posted at

先日 「ヴァイブコーディング」っていう言葉は2026年なくなるんじゃないか。という記事を書いた。(そして珍しく3桁台の「いいね」をいただいた。ありがとうございますm(__)m )

その記事で私は、AI(LLM)を用いたコーディングについて、

  • LLMは「すべての指示を必ず守る機械」ではない
  • システムプロンプトは思っている以上に重要

という話を書いた。

未完成のまま投げ出してしまったが、あとから考えていて、ふと気づいたことがある。

最初に言い訳:

  • この記事でいうSGDは、最適化・探索プロセスの比喩であり、数学的SGDではない。
  • 名前を付けるなら Human-in-the-loop stochastic search くらい。

あれっ、これってアレですよね?

LLMは「すべての指示を必ず守る機械」ではない

  • LLMは、確率的に「尤もらしい」1出力や動作をする装置

である。(前提1)

一方で、私たちが「ヴァイブコーディング」で欲しいものは、

  • ソースコードとして書かれ、

  • 入出力を通じて何らかのタスクを実行するプログラム

である。

これは抽象的に考えると、

  • 成果として欲しいプログラム=「可変長のパラメータ(=ソースコード)を持つ関数」

であり、さらに言えば、

  • 成果として欲しいプログラム=一種の「モデル」 (と言えなくもない)

である。(前提2)

すると、「ヴァイブコーディング」とは何かというと、

  • LLMによるパラメータ(ソースコード)追加・更新・削除という確率的な手段で

  • モデルのパラメータ(ソースコード)を少しずつ書き換え

  • そのモデル(結果としての「プログラム」)の出力の挙動を、望ましい方向へ寄せていく

というプロセスになる。

これはまさに、

誤差を見ながらパラメータを更新し、
確率的な操作を繰り返して出力を目標に近づけていく

という、ひと昔前に流行った(かつ古典だが現役バリバリな)「アレ」のデジャヴュなのである。

「確率的勾配降下法」 だ。

※ ブラックボックス最適化/探索に分類したほうが正確ではある(厳密には勾配を計算しているわけではなく、離散的な評価に基づく確率的探索に近い)。ただし、テストやLintのエラー/警告を低減するという目的に対し、その評価結果を「確率的な」LLMの次の生成に反映させていく反復構造が、確率的勾配降下法の更新過程と構造的に類似しているため、ここでは比喩的にこの用語を用いる。

確率的勾配降下法とは何か

「確率的勾配降下法」(Stochastic Gradient Descent, SGD)とは、機械学習モデルのパラメータを最適化するための手法のひとつで、例えばLLM自体もこの系統の手法を使って訓練(train)されている。ここでは「機械学習」や「深層学習」に関してなじみのない方向けに、「確率的勾配降下法」の概要のみを記述する。

  • 教師データ(多数の入力と出力のペア)を用意、固定して
  • モデルの出力と教師データ上の「正解出力」の差異を「目的関数」で計測し、
  • 「目的関数」の値がゼロに近づくように、
  • 無数のニューロンを含むモデルの中で、それぞれが持つパラメータ(たとえば y = ax + b の a や b のようなもの)を少しずつ更新していく
  • ことを教師データ全体に対して多数回繰り返す
  • ただし毎回すべての教師データをまとめて用いるのではなく、データの一部(1件または小さなバッチ)ずつを使ってパラメータを更新していく。このとき、どのデータを用いるかはランダムに選ばれることが多く、そのため更新方向にはばらつきが生じる。また実装上は、教師データの順番をシャッフルして用いることも一般的である。ここでの「確率的」は、更新が「データ全体の平均的な勾配」ではなく、サンプル(または小バッチ)から得られるノイジーな推定で行われる、という意味でもある。

ということになる。

なぜ「降下」なのかというと、モデルのパラメータ空間を、ある種の「地形」と見なしたときに、「目的関数」の値が大きい場所(=モデルの出力と教師データの差異が大きい場合)は“ピーク(山頂)や尾根”に対応し、値が小さい場所は“峠や谷、窪地”に対応する、というようなイメージが前提ということである。

SGDにおける学習とは、この山の中で、

  • いま自分が立っている場所(現在のモデルのパラメータの状況)から、

  • どちらの方向へ動けば「目的関数」がより小さくなるか(=より低くなるか)

を計算し、その方向へ少しだけ移動する、という操作を繰り返す過程である。

この「より低い方向へ向かって少しずつ移動する」操作が gradient descent(勾配降下) と呼ばれる所以である。

  • 誤差を測り、
  • 少し動き、
  • また誤差を測り、

という操作を何度も繰り返すことで、最終的には「だいたい低い窪地」にたどり着く。

これが、確率的勾配降下法による学習の基本的なイメージである。

※ 余談: ちなみに、この節の冒頭で、「SGDとは機械学習モデルの・・・」と書いたが、エンジニアがやりたいことは、「ヴァイブコーディング」ではなく、「機械学習」の例に倣えば、「機械コーディング」といったほうが正確なのではないかと思う。(ただしこの言葉はバズらないと思う、そういえば「機械語は奇怪なり」とか言ってる時代もありましたね笑)

ヴァイブコーディングのアナロジーとして

そしてもちろん、この「確率的勾配降下法」そのものが、「ヴァイブコーディング」とまったく一致するという主張を書こうとしているのではない。例えば少なくとも次のような点は「機械学習モデル」を「確率的勾配降下法」で train (訓練) する場合と、「ヴァイブコーディング」的な手法でソースコードを構築する場合では異なると思う。2

  • SGDは教師データ(正解ラベル付きの大量データ)と目的関数を用いて自動的にパラメータを更新するが、ヴァイブコーディングでは、人間等の主体が「これでいい」「ダメだ」という判断を主観的に(もしくは仕様書ベース等に客観的に)行ったり、テストケースや静的検査(コンパイラ・トランスパイラ・lint の結果)をLLMへのフィードバックとして用いる。

  • 汎化性能という観点。機械学習では「未知のデータ」(教師データに含まれていないデータ)に対しても正しく振る舞うこと(汎化)が重要な評価基準になる。一方、コーディングタスクでは、目的とされるソフトウェアの仕様や設計が「未知データ」に対する挙動を包括的に保証するか、あるいは保証しないことを明示するか、という形で扱われることが一般的である。

  • 過学習という観点。機械学習では「目的関数の値がゼロになること」(すなわち教師データとモデルの出力が完全に一致すること)は、前項の「汎化性能」が低下している兆候として忌避されることが多い。一方、コーディングタスクでは「目的関数との完全一致」そのものが目標となるケースが、一般的なユースケースとしては多いであろうと想定される。

ただし、SGDをアナロジーとして、ヴァイブ(雰囲気)コーディングを雰囲気で語ることはできると思う。

結局のところ、ヴァイブコーディングとは、

  • 正解が最初から与えられているわけでもなく、

  • 厳密な目的関数に関しても、
    -「全部を一発で書けない」
    -「目的関数は作れても、多目的で衝突する(lint/可読性/速度/安全性/仕様充足)」
    -「しかも観測できる誤差が離散的(通る/落ちる)で、勾配が取りづらい」
    という中で、

「なんとなく良さそう」
「さっきよりはマシ」
「これは違う気がする」

という人間の感覚を「現時点で観測した誤差」として使いながら、ソースコードというパラメータを少しずつ書き換えていく作業なのだと言える。

それは、

走ってみて、コケて、
方向を変えて、また走って、
そのうち目的地っぽい場所に近づいている

という行為に、どこか似ている。

LLMはこのタスクの地図(仕様の意味)を“内部表現として安定に保持している”保証はない。

ただ「次にそれっぽい一歩」を、確率的に出してくるだけだ。

そして人間は、

「そっちじゃない」
「今のはいい」
「もうちょい右」

と、雰囲気で方向修正を入れる。

この往復運動そのものが、ヴァイブコーディングの正体なのだと思う。

そう考えると、ヴァイブコーディングは「雑なプログラミング」でも「楽をする方法」でもなく、

確率的な存在であるLLMと、曖昧な存在である人間が、
勾配っぽいものを探しながら一緒に降りていく技法

なのかもしれない。

そしてたぶん、2026年に「ヴァイブコーディング」という言葉が消えたとしても、

この 「誤差を見て、ちょっと直して、また試す」 というやり方自体は、別のもっと真面目そうな名前を与えられて、これからも普通に使われ続けるのだろう。

名前は消えても、ヴァイブだけが残る。

――たぶん、そういう未来だ。

image.png

次節では、SDD(仕様駆動開発)に関して考えてみることにする。

SDD(仕様駆動開発)で何が変わったのか

前節のヴァイブなポエムで締めくくろうと思ったが、もう二点ほど追記したい。まずは仕様駆動開発(Spec-Driven Development: SDD)に関して。

私自身は「厳密な」SDDを実施したことはない。しかし、設計書や仕様書をLLMの助けを得て書き、そこからテストケースを作る、という開発は行っている。言ってみれば「手動SDD」くらいの位置づけだろう。

ただ、SDDを先ほどの SGD(確率的勾配降下法)とのアナロジーで言い換えるなら、それは、

「人間が“これが正しいはずだ”と考えるモデルの振る舞いを、あらかじめ仕様という形で固定し、その 仕様に沿ったテストを目的関数として、自動化できるフィードバックは自動化しつつ、実装を更新していく」

という進化だと見るのが自然だと思う。

すなわちSDDとは、ヴァイブコーディングのように人間の感覚に依存していた「正しさ」を、仕様という外部表現に落とし込み、共有可能な形で扱えるようにした開発手法だと捉えられる。

これは、とても良い方向への進化だと思う。ただ一点だけ、いちエンジニアとして気になっていることがある。それは、「仕様駆動開発」という名称が示す印象に比べて、その到達点が過大に評価されてしまわないか、という点である。

人類史とのアナロジーで、かつあえて過激に言えば、これは旧石器時代から新石器時代への進化に相当するような段階であり、「機械コーディング」という長い進化の流れの中では、比較的初期の段階に位置づけられるのではないかと感じている。(繰り返しになりますが、あえて過激に言っています)

SGDとのアナロジーで言えば、仕様の固定化や、目的関数として扱える形での自動化は、SGDが辿ってきた発展の歴史と比べても、まだ出発点に近い変化にとどまる。

SDDを採用した場合の現場の状況を(やや戯画的に)想像すると、例えば次のような構図になる可能性がある。

  • マネージャ層:
    「仕様があり、AIもあるのなら、かなりの部分が自動化できるのではないか」という期待

  • 実際のSDD:
    原理上、「多数のイテレーションが必要な場合が多い」「局所解(目指す方向とは異なる最適化)への収束」「仕様自体が曖昧・矛盾している場合のやり直し」などが避けられない

  • 現場のエンジニア:
    それらの課題への対処(方針の策定)や、未知の状況についての説明・調整(見積もり含む)を担うことになる

このように見ると、SDDは万能な解決策というよりも、「正しさ」を仕様として明示的に扱えるようにした、重要ではあるが初期段階の一歩であり、その限界や前提条件を理解した上で使われるべき手法なのだと思う。

一番誤解されそうなポイントは、「仕様を固めればAI(LLM)もきちんとやってくれるはずだ」という点だ。正しくは「いくら仕様を固めたところでも、AI(LLM)の挙動は『文脈上、確率的にもっとも出やすい出力』に過ぎない。確率的な手段を使って最適解=欲しかったプログラムにたどり着くには、多数のイテレーションが必要である可能性が高い」ということを忘れてはならない、という点かと思う。

そして、「仕様」という目標到達点が固定化された分、コードがそれに忠実に「収束」するまでに必要な繰り返し(イテレーション)の回数が、ヴァイブコーディング時代と比べて数倍になる可能性がある。

なので、「仕様駆動開発」じゃなくて「仕様駆動確率的勾配降下開発法」とか言うのはどうですかね・・・?(笑)

というのは半分冗談だが、本質的にはそれくらい「探索と反復のプロセス」だと考えている。

実践編に続くかも

今回、少しポエミーな内容になってしまったので、最後に「実践編への橋渡し」として、少し TypeScript の実コードと現状を載せておきます。

結論を先に言うと、自然言語の制約は“勾配”になりにくい。勾配になるのは、差分を測ってくれる機械(lint/test)だ、ということ。

※ 畳んであるコード類は Appendix レベルで掲載していますので読み飛ばしていただいて結構です。

前回の記事では「AI(LLM)が書いた」ソースコードの例を挙げましたが、同じリポジトリのソースコードの例をもう一例挙げておきます。これも可読性という面も含めて「ちゃんと書いてるじゃん」感はあるのではないかと思います。

クリックでコードが表示されます
/**
 * @file メタ情報取得ハンドラ群(health/now/version/site)
 * 備考: 公開かつ no-store の情報 API。DB 到達性とスキーマ状態を返す。
 * - 認証不要で到達させ、公開経路と分離する
 * - DB 到達性を簡潔に判定し遅延も併せて返す
 * - now は DB 基準の UTC を返しワーカー時計との差異を避ける
 * - version は環境変数にある git ref をそのまま返却する
 * - site は初期化有無を示し、未初期化でも 200 を返す
 * - レスポンスは envelope なし JSON を統一しエラー詳細を隠す
 * - Cache-Control は全て no-store 固定とし ETag は付与しない
 * - 例外時は HttpError を使わず内部 500 とする
 * @see vibecoding/var/contexts/qualities/docs/context.md
 * @see vibecoding/var/contexts/qualities/core/context.md
 * @see vibecoding/var/MEMO/new-design_read/README.md
 */
import type { Context } from "hono";
import { HttpError } from "../errors";
import { STATUS_INTERNAL_SERVER_ERROR } from "../http-status";
import { logError } from "../logger";
import type { AppEnv } from "../middleware/db";
import { ok } from "../response";

/** health 判定の遅延閾値既定値(設定未取得時のフォールバック)。 */
const DEFAULT_LATENCY_THRESHOLD_MS = 500;

/**
 * 認証済み経路で no-store を設定する。
 * @param c Hono コンテキスト
 */
function setNoStore(c: Context): void {
  c.header("Cache-Control", "no-store");
}

/**
 * 環境変数から遅延閾値を取得する(未設定時は既定値を返す)。
 * @param c Hono コンテキスト
 * @returns 遅延閾値(ミリ秒)
 */
function resolveLatencyThreshold(c: Context<AppEnv>): number {
  const envValue = c.env.HEALTH_LATENCY_THRESHOLD_MS;
  // 環境変数が未設定の場合は既定値をフォールバックとして使う
  if (envValue === undefined) {
    return DEFAULT_LATENCY_THRESHOLD_MS;
  }

  const parsed = Number.parseInt(String(envValue), 10);
  // 数値として解釈できない場合は既定値を使う
  if (Number.isNaN(parsed) || parsed <= 0) {
    return DEFAULT_LATENCY_THRESHOLD_MS;
  }

  return parsed;
}

/**
 * DB 到達性と遅延を返す。
 * @param c Hono コンテキスト
 * @returns health 結果
 */
export async function handleMetaHealth(c: Context<AppEnv>): Promise<Response> {
  const startedAt = Date.now();
  const latencyThreshold = resolveLatencyThreshold(c);
  // DB 到達性を測るために単純なクエリを投げる
  try {
    // データベース接続の到達性を確認する。例外時は degraded として扱う
    await c.var.db`SELECT 1`;
    const latencyMs = Date.now() - startedAt;
    setNoStore(c);
    return ok(c, {
      status: latencyMs > latencyThreshold ? "degraded" : "ok",
      db: "ok",
      latency_ms: latencyMs,
      checked_at: new Date().toISOString(),
    });
  } catch {
    // 到達不可でも 500 を返さず状態を通知する
    setNoStore(c);
    return ok(c, {
      status: "down",
      db: "down",
      latency_ms: null,
      checked_at: new Date().toISOString(),
    });
  }
}

/**
 * DB 基準の現在時刻を返す。
 * @param c Hono コンテキスト
 * @returns 現在時刻情報
 */
export async function handleMetaNow(c: Context<AppEnv>): Promise<Response> {
  // DB 基準の時刻を読み取り、端末時計とずれないようにする
  try {
    // データベース基準の現在時刻を取得する。例外時は500を返す
    const rows = await c.var.db<{ now_utc: string }[]>`
      SELECT (now() AT TIME ZONE 'UTC')::timestamptz AS now_utc
    `;
    const nowUtc = rows[0]?.now_utc;
    // 取得できない場合は内部整合が崩れているため 500 を返す
    if (nowUtc === null || nowUtc === undefined || nowUtc === "") {
      throw new HttpError(STATUS_INTERNAL_SERVER_ERROR, "failed to fetch now");
    }

    setNoStore(c);
    const epochMs = Date.parse(nowUtc);
    return ok(c, { now_utc: nowUtc, now_epoch_ms: epochMs, source: "db" });
  } catch (error) {
    // DB クエリ失敗時は例外を投げて 500 を返す(設計 0400_meta_api.md の規定)
    logError("Failed to fetch now from database", error instanceof Error ? error : new Error(String(error)));
    throw new HttpError(STATUS_INTERNAL_SERVER_ERROR, "failed to fetch now");
  }
}

/**
 * スキーマバージョンを取得する。
 * @param db データベース
 * @returns スキーマバージョン
 */
async function fetchSchemaVersion(db: AppEnv["Variables"]["db"]): Promise<string> {
  // スキーマバージョン取得時にエラーが発生した場合はデフォルト値を返す
  try {
    const rows = await db<{ version: string }[]>`
      SELECT version
      FROM schema_migrations
      ORDER BY version DESC
      LIMIT 1
    `;
    // マイグレーション履歴にレコードがある場合のみ最新バージョンを採用する
    if (rows[0] !== null && rows[0] !== undefined && rows[0].version !== null && rows[0].version !== undefined && rows[0].version !== "") {
      return rows[0].version;
    }
  } catch (error) {
    // DB エラーをログ出力してから内部エラーとして扱う
    logError("Failed to fetch schema version from database", error instanceof Error ? error : new Error(String(error)));
    // マイグレーション情報が取得できない場合は内部エラーとして扱う
    throw new HttpError(STATUS_INTERNAL_SERVER_ERROR, "failed to fetch schema version");
  }

  return "unknown";
}

/**
 * デプロイ済みのバージョン情報を返す。
 * @param c Hono コンテキスト
 * @returns バージョン情報
 */
export async function handleMetaVersion(c: Context<AppEnv>): Promise<Response> {
  const apiVersion = c.env.API_VERSION ?? "unknown";
  const refreshGitRef = c.env.REFRESH_GIT_REF;
  const schemaVersion = await fetchSchemaVersion(c.var.db);

  setNoStore(c);
  return ok(c, {
    api_version: apiVersion,
    schema_version: schemaVersion,
    ...(refreshGitRef !== null && refreshGitRef !== undefined && refreshGitRef !== "" ? { refresh_git_ref: refreshGitRef } : {}),
  });
}

/**
 * site 初期化状態を返す。
 * site_master のみに基づく初期化判定を行う。
 * @param c Hono コンテキスト
 * @returns サイト状態
 */
export async function handleMetaSite(c: Context<AppEnv>): Promise<Response> {
  // site_master のみに基づく初期化判定を行う
  try {
    const rows = await c.var.db<{
      site_name: string;
      board_list_page_size: number;
      created_at: string;
    }[]>`
      SELECT
        sm.site_name,
        sm.board_list_page_size,
        sm.created_at
      FROM site_master sm
      WHERE sm.singleton_id = 1
    `;
    setNoStore(c);
    // site_master が初期化されていない場合も 200 を返し状態だけ通知する
    if (rows[0] === null || rows[0] === undefined) {
      return ok(c, { initialized: false, reason: "site_not_initialized" });
    }

    const site = rows[0];
    return ok(c, {
      initialized: true,
      site_name: site.site_name,
      board_list_page_size: site.board_list_page_size,
      created_at: site.created_at,
    });
  } catch (error) {
    // DB エラーをログ出力してから内部エラーとして扱う
    logError("Failed to fetch site from database", error instanceof Error ? error : new Error(String(error)));
    // 判定に失敗した場合は内部エラーとして扱う
    throw new HttpError(STATUS_INTERNAL_SERVER_ERROR, "failed to fetch site");
  }
}

では、このコードをAIに書いてもらうために、どういう目的関数3を定義しているかという実例として、「どのような静的検査制約のもとでAI(LLM)がこのコードを書いているか」を、AI(LLM)自身に解説してもらうことにしましょう。

LLMへの指示:
src\api\handlers\meta.ts の内容に「効いている」npm check での制約条件を一覧化して、グループ化 (ポリシ、lint等) したうえで tmp 以下に md 形式のファイルとして出力して。

クリックで出力ファイルが開きます

meta.ts に効いている npm check の制約(グループ化)

対象: src/api/handlers/meta.ts(npm run check のゲート対象)

ゲート構成(npm check)

  • scripts/qualities/check.ts が実行される
  • 実行順: policy -> typecheck -> lint -> test(build はゲート対象外でスキップ)
  • preflight の通過マーカーが無いと npm run check は失敗する

Policy(静的ポリシー)

  • Anti-MVP: 禁止語の使用禁止、TODO/FIXME/HACK はチケットID必須
  • eslint-disable 等の抑止コメントは全面禁止
  • ルール緩和の禁止(no_relaxation)
  • 重複JSDocブロックの禁止(jsdoc_no_duplicate)
  • as unknown as T の二重キャスト禁止(no_unknown_double_cast)
  • コメントのロケール制約(ja ロケールではASCIIのみ行を禁止)
  • 依存構造制御(外部から vibecoding/ への依存禁止など)

Lint(ESLint/ドキュメント/構造)

  • モジュール境界: 不適切な依存関係を禁止(import/no-restricted-paths 等)
  • 複雑度: complexity <= 10
  • 関数長: max-lines-per-function <= 80(コメント・空行除外)
  • ファイル長: max-lines <= 300(コメント・空行除外)
  • マジックナンバー禁止(定数化必須)
  • JSDoc 必須: 関数/メソッド/クラス/型/公開exportに説明付きJSDoc
  • JSDoc 整形: アライメント/インデント/空タグ禁止
  • ファイルヘッダ必須: @file + 8行以上の箇条書き + @see 複数
  • 単一ファイルヘッダ強制(ヘッダJSDocは1つのみ)
  • 制御構造コメント必須(if/for/while/do/switch/try/await。jaロケールは非ASCII必須)
  • インラインJSDoc禁止、ラベル風インラインコメント禁止

Boundary-unknown(境界入力の型安全)

  • 境界APIからの値は unknown で受け取り、assert/parseで確定
  • as unknown as T / 境界直後の as T を禁止
  • boundary 由来 unknown の運搬禁止(引数/返却/オブジェクト構築)
  • *Unknown で終わる変数名禁止(本番 src/** は対象)
    • meta.ts では境界API使用が無いが、ゲート上は有効

Typecheck(TypeScript)

  • any 禁止、unsafe assignment/member/call/return 禁止
  • 不要な型アサーション禁止、as スタイル統一
  • オブジェクトリテラルへの型アサーション禁止
  • 厳格な真偽値式(文字列/数値/nullish を直接使わない)
  • テンプレート式は数値のみ許可(真偽値/nullish禁止)
  • export 関数の型は明示必須
  • 型インポートは import type を使用
  • TS コメント制限(ts-ignore/ts-nocheck 禁止、ts-expect-error は説明必須)

Test(副作用)

  • npm run check はテストも実行するため、meta.ts 変更で既存テストが落ちるとゲート失敗
  • vibecoding 側の変更がある場合は内製テストが追加実行される

かなり「厳しい」チェックが行われているのがご理解いただけるのではないかと思います。そして実は、コーディングタスク実施時には、すべての制約を守ってもらうための「制約の要約」も、事前にLLMに渡しています(コンテキストに含めています)。

それぞれクリックで事前入力ファイルが開きます。

core/context.md

core Quality Context

Why(なぜこの品質ゲートが必要か)

品質ゲート未考慮時の問題点

成果物品質への影響

  • 型安全性:any 型の多用や不適切な型キャスト(as unknown as)により、実行時エラーのリスクが増大し、TypeScript の恩恵が失われます。
  • 保守性:関数の肥大化(max-lines-per-function 超過)や複雑なネスト(complexity 超過)により、コードの可読性が低下し、修正時の影響範囲の特定が困難になります。
  • 依存構造の混乱:vibecoding/ 外から内部への不適切な依存が許可されると、責務境界が曖昧になり、システム全体の結合度が不必要に高まります。

品質ゲートでの手戻りコスト

  • トークン消費:実装後に大量の Lint エラーや型エラーが発覚すると、その修正のために LLM のコンテキストを浪費し、再生成コストが嵩みます。
  • 時間損失:CI ゲートでの失敗を繰り返すことで、開発サイクルが停滞し、本来の機能実装に充てるべき時間が削られます。
  • 認知負荷:複雑なロジックやマジックナンバーが散見されるコードは、デバッグやコードレビューの負荷を著しく高めます。

Where(どこに適用されるか)

  • 監視範囲:src/**/*.ts, src/**/*.mts, src/**/*.cts
  • 除外範囲:dist/**, node_modules/**, tmp/**, scripts/tmp/**

What(何をチェックするか)

コマンド 設定ファイル 監視範囲 閾値/禁止事項
npm run lint qualities/eslint/04-complexity-and-magic/core/config.mjs src/**/*.ts complexity: 10, max-lines-per-function: 80
node qualities/policy/dependency_structure_control/core/run.mjs qualities/policy/dependency_structure_control/core/rules.json 全域 !^vibecoding/ -> ^vibecoding/ 禁止
npm run typecheck qualities/tsconfig/tsconfig/types/tsconfig.json src/**/*.ts strict: true, noImplicitAny: true

主要ルールと根拠

  1. 複雑度制限 (complexity: 10):出典 qualities/eslint/04-complexity-and-magic/core/config.mjs:30。認知負荷を抑え、テスト容易性を確保するため。
  2. 関数長制限 (max-lines-per-function: 80):出典 qualities/eslint/04-complexity-and-magic/core/config.mjs:31。単一責務の原則(SRP)を強制し、再利用性を高めるため。
  3. マジックナンバー禁止 (@typescript-eslint/no-magic-numbers):出典 qualities/eslint/04-complexity-and-magic/core/config.mjs:62。数値に意味(名前)を与え、意図を明確にするため(0, 1, 2, -1 は例外)。
  4. 依存構造制御 (no_vibecoding_from_outside):出典 qualities/policy/dependency_structure_control/core/rules.json:2。内部実装(vibecoding)への外部からの直接依存を遮断し、カプセル化を守るため。

How(どうやって守るか)

設定閾値一覧

  • complexity: 10 (出典: qualities/eslint/04-complexity-and-magic/core/config.mjs:30)
  • max-lines-per-function: 80 (出典: qualities/eslint/04-complexity-and-magic/core/config.mjs:31)
  • max-nested-callbacks: 3 (出典: qualities/eslint/04-complexity-and-magic/core/config.mjs:32)
  • @typescript-eslint/no-magic-numbers: 許可 [-1, 0, 1, 2] (出典: qualities/eslint/04-complexity-and-magic/core/config.mjs:62)

成功パターン

/**
 * @file 定数定義の例
 */
export const MAX_RETRY_COUNT = 3; // 意味のある名前を付与

/**
 * SRP に基づいた小さな関数
 */
export function calculateTotal(price: number, taxRate: number): number {
  return price * (1 + taxRate);
}

失敗パターン

// 悪い例: マジックナンバーと複雑な条件分岐
export function process(val: any) {
  if (val > 100) { // ❌ 100 はマジックナンバー
    // ... 複雑なロジック ...
  }
}

エラーメッセージ:'100' is a magic number. (@typescript-eslint/no-magic-numbers)

LLM典型NGパターン(最低5件)

  1. マジックナンバーの直書き: setTimeout(..., 1000) 等。定数化が必要。
  2. any 型の安易な使用: 型定義をサボって any を使う。具体的な interface/type を定義する。
  3. 二重キャスト: as unknown as T。型設計を見直すか、適切な型ガードを用いる。
  4. 巨大な一関数: 条件分岐やループが重なり complexity を超える。プライベート関数やクラスへ切り出す。
  5. JSDoc の欠如: ファイル概要や公開関数の説明を省く。@file や @param を適切に記述する。

事前チェックリスト(コード生成前)

  • 1関数の長さは 80 行以内か?
  • 複雑な条件分岐(ネスト)を早期リターンで解消しているか?
  • 数値(0, 1, 2, -1 以外)はすべて定数化されているか?
  • any や eslint-disable を使おうとしていないか?

修正方針(違反発見時)

  1. Lint エラー: 指示されたルール名を確認し、ルールに適合するようにリファクタリングする(抑止コメントは禁止)。
  2. 型エラー: any を使わず、原因となっている型不整合を根本から直す。
  3. 依存違反: 依存の向きが正しいか確認し、必要なら共通モジュールへ抽出する。
types/context.md

types Quality Context

Why(なぜ必要か)

品質ゲート未考慮時の問題点

成果物品質への影響

  • 型安全性: any 型の使用や unsafe な代入により、型安全性が崩壊し runtime error や null 参照が発生するリスクが増大する。二重キャスト(as unknown as T)により型安全性の保証が失われる。
  • 保守性: 型が不明確なコードにより、IDE の補完やリファクタリングが困難になり、拡張性が低下する。型アサーションの多用により、型の意図が不明確になる。
  • セキュリティ: 型検証を回避した unsafe な代入やアクセスにより、予期しない入力が処理され、セキュリティリスクが発生する。

品質ゲートでの手戻りコスト

  • トークン消費: any 型の削除と適切な型付けで約2000-5000トークン。二重キャストの削除や assert 関数の追加も追加コスト。型注釈の追加は大規模なリファクタリングを要する。
  • 時間損失: 違反修正に約20-40分。特に型注釈の追加や assert 関数の実装が主な作業。
  • 認知負荷: 型の意図を理解し、適切な型付けを設計する必要がある。

Where(適用範囲)

  • 監視範囲: **/*.{ts,tsx,mts,cts}(出典: qualities/eslint/_shared/core/globs.mjs の FILES_TS)
  • ユニット入力:
    • qualities/eslint/02-type-safety/types
    • qualities/eslint/plugins/types
    • qualities/infra/tsconfig/types
    • qualities/policy/no_unknown_double_cast/types

What(何をチェックするか)

  • コマンド: npm run typecheck(出典: package.json 10行目)
  • 主要ルールと閾値:
    • @typescript-eslint/no-explicit-any: error(any 型の使用禁止、出典: qualities/eslint/02-type-safety/types/config.mjs 49行目)
    • @typescript-eslint/no-unsafe-assignment: error(unsafe な代入禁止、出典: 同50行目)
    • @typescript-eslint/no-unsafe-member-access: error(unsafe なメンバーアクセス禁止、出典: 同51行目)
    • @typescript-eslint/no-unsafe-call: error(unsafe な呼び出し禁止、出典: 同52行目)
    • @typescript-eslint/no-unsafe-return: error(unsafe な返却禁止、出典: 同53行目)
    • @typescript-eslint/no-unnecessary-type-assertion: error(不要な型アサーション禁止、出典: 同54行目)
    • @typescript-eslint/consistent-type-assertions: error, { assertionStyle: 'as', objectLiteralTypeAssertions: 'never' }(型アサーションの一貫性、出典: 同55-58行目)
    • @typescript-eslint/strict-boolean-expressions: error, { allowString: false, allowNumber: false, allowNullableObject: false, allowNullableBoolean: false, allowAny: false }(厳格な真偽値式、出典: 同59-68行目)
    • @typescript-eslint/restrict-template-expressions: error, { allowNumber: true, allowBoolean: false, allowNullish: false }(テンプレート式の制限、出典: 同69-72行目)
    • @typescript-eslint/explicit-module-boundary-types: error(モジュール境界の明示的な型、出典: 同73行目)
    • @typescript-eslint/consistent-type-imports: error, { prefer: 'type-imports' }(型インポートの一貫性、出典: 同74行目)
    • @typescript-eslint/ban-ts-comment: error, { 'ts-expect-error': 'allow-with-description', minimumDescriptionLength: 10, 'ts-ignore': true, 'ts-nocheck': true, 'ts-check': false }(TS コメントの制限、出典: 同91-100行目)
    • no_unknown_double_cast ポリシー: as unknown as T の使用禁止(出典: qualities/policy/no_unknown_double_cast/types/run.mjs)

How(どう守るか)

設定閾値一覧

  • @typescript-eslint/no-explicit-any: error(出典: qualities/eslint/02-type-safety/types/config.mjs 49行目)
  • @typescript-eslint/strict-boolean-expressions: error, { allowString: false, allowNumber: false, allowNullableObject: false, allowNullableBoolean: false, allowAny: false }(出典: 同59-68行目)
  • @typescript-eslint/consistent-type-assertions: error, { assertionStyle: 'as', objectLiteralTypeAssertions: 'never' }(出典: 同55-58行目)
  • no_unknown_double_cast: error(出典: qualities/policy/no_unknown_double_cast/types/run.mjs)

基本遵守事項

  • any 型は使用禁止。適切な型を定義する。
  • unsafe な代入・アクセス・呼び出し・返却を避ける。
  • 不要な型アサーションを使用しない。
  • 型アサーションは as スタイルで統一し、オブジェクトリテラルへの型アサーションは禁止。
  • 真偽値式は厳格に評価し、文字列・数値・nullish を直接真偽値として使用しない。
  • テンプレート式では数値のみ許可し、真偽値・nullish は禁止。
  • モジュール境界(export)では明示的な型を付与する。
  • 型インポートは import type を使用する。
  • 二重キャスト(as unknown as T)は全面禁止。

成功パターン

/**
 * @file 型安全な実装
 */
import type { User } from './types';

/**
 * ユーザー情報を取得(型安全)。
 * @param id ユーザーID
 * @returns ユーザー情報
 */
export async function getUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data: unknown = await response.json();
  // assert 関数で型を確定
  assertUser(data);
  return data;
}

/**
 * User 型の型ガード。
 * @param value 検証対象
 */
function assertUser(value: unknown): asserts value is User {
  if (typeof value !== 'object' || value === null) {
    throw new Error('invalid user');
  }
  const u = value as Record<string, unknown>;
  if (typeof u.id !== 'string') {
    throw new Error('id required');
  }
}

/**
 * 真偽値式の厳格な評価。
 * @param value 値
 */
export function isEnabled(value: boolean | null | undefined): boolean {
  // NG: if (value) { ... }
  // OK: 明示的な null/undefined チェック
  return value === true;
}

失敗パターン

// NG: any 型の使用
export function processData(data: any): any {
  return data.value;
}

// NG: unsafe な代入
const value: unknown = getData();
const result = value.someProperty; // no-unsafe-member-access 違反

// NG: 不要な型アサーション
const num = 42;
const str = num as string; // no-unnecessary-type-assertion 違反

// NG: オブジェクトリテラルへの型アサーション
const obj = { a: 1 } as MyType; // consistent-type-assertions 違反

// NG: 文字列を直接真偽値として使用
const str = 'hello';
if (str) { // strict-boolean-expressions 違反
  // ...
}

// NG: 二重キャスト
const value = getData() as unknown as MyType; // no_unknown_double_cast 違反

// NG: 型インポートでない
import { User } from './types'; // consistent-type-imports 違反(型のみの場合)

// NG: モジュール境界で型が不明確
export function process(input) { // explicit-module-boundary-types 違反
  return input.value;
}

LLM典型NGパターン(最低5件)

  1. any 型の使用: 型が不明確な場合に any 型を使用してしまう

    • 回避策: 適切な型を定義し、unknown と型ガードを使用する
  2. unsafe な代入: unknown 型の値を直接代入してしまう

    • 回避策: assert 関数や型ガードで型を確定してから使用する
  3. 二重キャストの使用: as unknown as T を使用して型エラーを回避しようとする

    • 回避策: assert 関数を実装し、asserts x is T 署名で型狭窄を明示する
  4. 真偽値式の緩和: 文字列や数値を直接真偽値として使用する

    • 回避策: 明示的な null/undefined チェックや比較演算子を使用する
  5. 型インポートの混在: 型のみのインポートで import type を使用しない

    • 回避策: 型のみの場合は import type を使用し、値と型の両方の場合は通常の import を使用する
  6. モジュール境界の型不明確: export 関数の型を明示しない

    • 回避策: すべての export 関数に明示的な型注釈を付与する
  7. 不要な型アサーション: 型が既に確定しているのに型アサーションを使用する

    • 回避策: 型アサーションは必要な場合のみ使用し、型推論を活用する

事前チェックリスト(コード生成前)

  • any 型を使用しない
  • unsafe な代入・アクセス・呼び出し・返却を避ける
  • 不要な型アサーションを使用しない
  • 型アサーションは as スタイルで統一
  • 真偽値式は厳格に評価(文字列・数値・nullish を直接使用しない)
  • テンプレート式では数値のみ許可
  • モジュール境界(export)では明示的な型を付与
  • 型インポートは import type を使用
  • 二重キャスト(as unknown as T)を使用しない

修正方針(違反発見時)

  1. any 型を適切な型に置き換え、必要に応じて unknown と型ガードを使用する
  2. unsafe な代入・アクセスを assert 関数や型ガードで型を確定してから使用する
  3. 不要な型アサーションを削除し、型推論を活用する
  4. 型アサーションを as スタイルで統一し、オブジェクトリテラルへの型アサーションを削除する
  5. 真偽値式を明示的な null/undefined チェックや比較演算子に置き換える
  6. テンプレート式から真偽値・nullish を削除し、数値のみを使用する
  7. モジュール境界(export)に明示的な型注釈を付与する
  8. 型のみのインポートを import type に変更する
  9. 二重キャストを削除し、assert 関数で型を確定する
docs/context.md

docs Quality Context

Why(なぜ必要か)

品質ゲート未考慮時の問題点

成果物品質への影響

  • 型安全性: JSDoc の欠如により、型情報がコード上に明示されず、型エラーの検出が困難になる。インターフェースや型エイリアスの説明不足により、使用側で型の誤用が発生するリスクが増大する。
  • 保守性: ファイルヘッダや関数の説明不足により、コードの意図や非目標が不明確になり、拡張性が損なわれる。制御構造コメントの欠如により、分岐やループの意図が理解しにくく、バグ修正やリファクタリングが困難になる。
  • セキュリティ: コメントのロケール不統一(英語のみのJSDocなど)により、非英語話者が仕様を誤解するリスクが増大する。重複したJSDocブロックにより、情報の不整合や古い情報の残存が発生する。

品質ゲートでの手戻りコスト

  • トークン消費: JSDoc 追加で約500-2000トークン。ファイルヘッダ修正で約300-1000トークン。制御構造コメント追加で約200-800トークン。コメントロケール修正で約1000-3000トークン。
  • 時間損失: 違反修正に約20-60分。特にJSDoc追加、ファイルヘッダの箇条書き作成、制御構造コメントの追加、コメントロケールの統一が主な作業。
  • 認知負荷: 関数・型の説明文作成、ファイルヘッダの構造化、制御構造の意図説明、ロケール言語でのコメント統一が必要。

Where(適用範囲)

  • 監視範囲: **/*.{js,cjs,mjs,ts,tsx,mts,cts}(出典: qualities/eslint/_shared/core/globs.mjs の FILES_ALL_CODE)
  • ユニット入力:
    • qualities/eslint/03-documentation/docs
    • qualities/eslint/plugins/docs
    • qualities/policy/jsdoc_no_duplicate/docs
    • qualities/policy/comment_locale/docs

What(何をチェックするか)

  • コマンド: npm run check(出典: package.json 27行目)、npm run lint(出典: package.json 11行目)
  • 主要ルールと閾値:
    • JSDoc 必須: 関数宣言、メソッド定義、クラス宣言、型宣言(interface/type/enum)、エクスポートされた const に JSDoc を要求(出典: qualities/eslint/03-documentation/docs/config.mjs 80-113行目)
    • JSDoc 整形: jsdoc/check-alignment: error、jsdoc/check-indentation: error(出典: 同105-106行目)
    • JSDoc 内容: jsdoc/require-description: error、jsdoc/require-param-description: error、jsdoc/require-returns-description: error、jsdoc/empty-tags: error(出典: 同107-110行目)
    • JSDoc パラメータ: jsdoc/require-param: error、jsdoc/require-returns: error(出典: 同111-112行目)
    • ファイルヘッダ: jsdoc/require-file-overview: error、header/header-bullets-min: error, min: 8(出典: 同113行目、137-144行目)
    • 単一ファイルヘッダ: singleHeader/single-file-header: error(出典: 同146行目)
    • 連続行コメント類似度: cmtSim/consecutive-line-comments-similarity: error, similarityThreshold: 0.25(ja ロケール、出典: 同123-129行目、43-54行目)
    • ブロックコメント整形: blockfmt/block-comment-formatting: error、blockfmt/no-empty-comment: error、blockfmt/prefer-single-line-block-comment: error、blockfmt/no-blank-lines-in-block-comment: error(出典: 同154-157行目)
    • インライン JSDoc 禁止: inlineJSDoc/no-inline-leading-jsdoc: error(出典: 同159行目)
    • インラインコメントラベル禁止: inlineLbl/no-label-style-inline-comment: error(出典: 同161-195行目)
    • 制御構造コメント: control/require-comments-on-control-structures: error, targets: ['if', 'for', 'while', 'do', 'switch', 'try', 'await'], requireSectionComments: 'fullOnly'(ja ロケールでは非ASCII必須、出典: 同225-252行目)
    • ESLint ディレクティブ: eslint-comments/require-description: error、eslint-comments/no-unused-disable: error、eslint-comments/no-unlimited-disable: error(出典: 同260-262行目)
    • describe コメント: blockfmt/require-describe-comment: error(出典: 同269行目)
    • Options typedef: typedef/require-options-typedef: error(出典: 同276行目、284行目)
    • エクスポート JSDoc: exportJsdoc/export-jsdoc-required: error(出典: 同292行目)
    • 浮いているJSDoc検出: noDanglingJsdoc/no-dangling-jsdoc: error(出典: 同300行目)
    • 型ガード直後再代入: noReassign/no-reassign-after-assert-guard: warn(出典: 同214行目)。境界由来 unknown の場合はデフォルトで警告を出さない(ignoreBoundaryUnknown: true、出典: qualities/eslint/plugins/docs/no-reassign-after-assert-guard.js 444行目)。境界APIの検出には PRESET_BOUNDARY_SOURCES を使用し、同一ブロック内で境界API呼び出しから取得した unknown を追跡する(出典: 同17行目、qualities/eslint/plugins/boundary-unknown/index.js 45行目)
    • JSDoc 重複防止: 隣接する重複JSDocブロックを検出(key5一致またはタグ集合の共通、出典: qualities/policy/jsdoc_no_duplicate/docs/run.mjs)
    • コメントロケール: ja 系ロケールではJSDoc内のASCIIのみの行を禁止(出典: qualities/policy/comment_locale/docs/run.mjs)

How(どう守るか)

設定閾値一覧

  • header-bullets-min: 8(出典: qualities/eslint/03-documentation/docs/config.mjs 140行目)
  • consecutive-line-comments-similarity: 0.25(ja ロケール、出典: 同67行目、43-54行目)
  • require-comments-on-control-structures: similarityThreshold: 0.25(ja ロケール、出典: 同239行目、43-54行目)
  • requireTagPattern: '[^\\x00-\\x7F]'(ja ロケールでの制御構造コメント、出典: 同249行目)

基本遵守事項

  • すべての関数宣言、メソッド定義、クラス宣言にJSDocを付与する。
  • interface、type、enum、エクスポートされた const にJSDocを付与する。
  • ファイル先頭に8個以上の箇条書きを含むJSDocヘッダを付与する。
  • JSDoc の整形(アライメント、インデント)を正しく行う。
  • JSDoc に description、@param の説明、@returns の説明を含める。
  • 連続する行コメントの類似度が0.25を超えないようにする(ja ロケール)。
  • ブロックコメントは正しく整形し、空のコメントや改行を含まないようにする。
  • JSDoc を同一行の先頭に書かない(fixerで改行+同一インデントに修正)。
  • インラインコメントにラベル風のメタ記述(「意図」「目的」「理由」など)を含めない。
  • 制御構造(if/for/while/do/switch/try/await)の直前にコメントを付与する(ja ロケールでは非ASCII必須)。
  • switch の case/default 節にもコメントを付与する。
  • ESLint ディレクティブには説明を付与し、未使用の disable を削除する。
  • describe コメントを付与する。
  • Options 型には typedef を付与する。
  • 公開 export に JSDoc を付与する。
  • 浮いているJSDoc(何にも紐づいていないJSDoc)を削除する。
  • 型ガード直後に同じ値を無注釈で再代入しない(型を明示するか直接使用する)。ただし、境界由来 unknown の場合はデフォルトで警告を出さない(boundary-unknown ルールとの整合性のため)。境界API(res.json() など)から取得した unknown は同一ブロック内で追跡され、型ガード後の無注釈再代入でも警告されない。
  • 隣接する重複JSDocブロックを避ける(要約key5一致またはタグ集合の共通を避ける)。
  • ja ロケールではJSDoc内のASCIIのみの行を避け、各行に非ASCII文字を含める。

成功パターン

/**
 * @file ユーザー認証処理(認証フローと権限チェックを担当)
 * - 目的: OAuth 2.0 プロトコルに基づく認証フローを実装し、ユーザーセッションを管理する
 * - 非目標: パスワードリセット機能は含まない(別モジュールで実装)
 * - 入出力: OAuth プロバイダーからの認証コードを受け取り、アクセストークンとリフレッシュトークンを返す
 * - エラー方針: 認証失敗時は HTTP 401 を返し、エラー詳細はログに記録する
 * - 依存: `src/lib/auth/oauth.ts`(OAuth クライアント)、`src/lib/session.ts`(セッション管理)
 * - テスト戦略: モックプロバイダーを使用した統合テストと、リフレッシュトークン更新のユニットテスト
 * - 非同期処理: すべての外部API呼び出しは await で待機し、エラーハンドリングを明示する
 * - 型安全性: レスポンス型は unknown から段階的に絞り込み、as unknown as の直キャストは禁止
 */

/**
 * OAuth 認証フローを実行し、ユーザーセッションを確立する。
 * @param authCode OAuth プロバイダーから受け取った認証コード
 * @param redirectUri リダイレクトURI(プロバイダーに登録済みのものと一致させる)
 * @returns アクセストークンとリフレッシュトークンのペア
 * @throws {OAuthError} 認証コードが無効な場合
 */
export async function authenticateUser(authCode: string, redirectUri: string): Promise<TokenPair> {
  // 認証コードの検証を実行
  if (!authCode || authCode.length === 0) {
    throw new OAuthError('認証コードが空です');
  }
  // トークン交換を実行
  const tokens = await exchangeCodeForTokens(authCode, redirectUri);
  return tokens;
}

失敗パターン

// ❌ ファイルヘッダなし、JSDocなし
export function processData(data: unknown) {
  if (data) {
    return data;
  }
}

// ❌ 制御構造コメントなし(ja ロケール)
function validate(input: string): boolean {
  if (input.length === 0) return false;
  for (const char of input) {
    if (char === ' ') return false;
  }
  return true;
}

// ❌ 境界由来 unknown で型注釈付き再代入(boundary-unknown エラー)
const bodyUnknown: unknown = await res.json();
assertResponseBodyWithStatus(bodyUnknown);
const body: ResponseBodyWithStatus = bodyUnknown; // error

LLM典型NGパターン

  1. JSDoc の重複: 同じ関数に複数のJSDocブロックを付与する(jsdoc_no_duplicate 違反)
  2. 制御構造コメントの欠如: if/for/while の直前にコメントがない(ja ロケールでは非ASCII必須)
  3. ファイルヘッダの箇条書き不足: 8個未満の箇条書き(header-bullets-min 違反)
  4. インラインコメントのラベル使用: 「意図:」「目的:」などのラベルをインラインコメントで使用
  5. 境界 unknown の誤解: 境界由来 unknown の場合に型注釈付き再代入を使用する(boundary-unknown エラー)。正しくは型注釈なし再代入を使用し、no-reassign-after-assert-guard はデフォルトで警告を出さない。

事前チェックリスト

  • すべての関数・クラス・型にJSDocが付与されているか
  • ファイルヘッダに8個以上の箇条書きがあるか
  • 制御構造の直前にコメントがあるか(ja ロケールでは非ASCII必須)
  • JSDoc の整形が正しいか(アライメント、インデント)
  • 重複JSDocブロックがないか
  • 境界由来 unknown の場合は型注釈なし再代入を使用しているか

修正方針

  • JSDoc 欠如: 関数・クラス・型の説明を追加し、@param と @returns を記述する。
  • ファイルヘッダ不足: 目的・非目標・入出力・エラー方針・依存・テスト戦略・非同期処理・型安全性の8項目以上を箇条書きで記述する。
  • 制御構造コメント欠如: if/for/while の直前に、なぜその分岐/ループが必要かを説明するコメントを追加する(ja ロケールでは非ASCII必須)。
  • 境界 unknown の誤用: 境界由来 unknown の場合は型注釈なし再代入を使用し、no-reassign-after-assert-guard の警告はデフォルトで出ないことを理解する。

Quality Context Hash Manifest

unitDigest: ""
files: []
boundary-unknown/context.md

boundary-unknown Quality Context

Why(なぜ必要か)

品質ゲート未考慮時の問題点

成果物品質への影響

  • 型安全性: 境界API(c.req.json, pluginSharedData.get 等)から取得した値を unknown で受け取らずに内側へ持ち込むと、型安全性が崩壊し runtime error が発生するリスクが増大する。二重キャスト(as unknown as T)により型安全性の保証が失われる。
  • 保守性: 境界で unknown を受けずに型アサーションで確定したように扱うパターンが散見されると、型の意図が不明確になり、IDE の補完やリファクタリングが困難になる。変数名が *Unknown で終わる命名パターンは可読性の観点で改善の余地がある。
  • セキュリティ: 境界で型検証を回避した unsafe な代入やアクセスにより、予期しない入力が処理され、セキュリティリスクが発生する。

品質ゲートでの手戻りコスト

  • トークン消費: 境界での unknown 受け取りと assert/parse 関数の追加で約2000-5000トークン。二重キャストの削除や変数名の修正も追加コスト。*Unknown 変数名の修正は大規模なリファクタリングを要する。
  • 時間損失: 違反修正に約30-60分。特に assert 関数の実装や変数名の一括変更が主な作業。
  • 認知負荷: 境界での型の意図を理解し、適切な assert/parse 関数を設計する必要がある。

Where(適用範囲)

  • 監視範囲: **/*.{ts,tsx,mts,cts}(出典: qualities/eslint/_shared/core/globs.mjs の FILES_TS)
  • ユニット入力:
    • qualities/eslint/07-boundary-unknown/boundary-unknown
    • qualities/eslint/plugins/boundary-unknown

What(何をチェックするか)

  • コマンド: npm run lint:boundary-unknown(出典: package.json 12行目)
  • 主要ルールと閾値:
    • boundary-unknown/no-boundary-type-assertion: error(境界API呼び出しでの型引数・型アサーション禁止、as unknown は許容、出典: qualities/eslint/07-boundary-unknown/boundary-unknown/config.mjs 52-57行目)
    • boundary-unknown/no-assert-after-boundary-unknown: error(境界由来 unknown への型アサーション禁止、as unknown は許容、出典: 同59-64行目)
    • boundary-unknown/no-double-assertion: error(境界由来 unknown への二重アサーション禁止、出典: 同66-71行目)
    • boundary-unknown/no-unknown-double-cast: error(二重キャスト禁止、as unknown as T を全域で禁止、出典: 同73-76行目)
    • boundary-unknown/no-inline-dto-cast: error(境界直後の DTO キャスト禁止、出典: 同78-83行目)
    • boundary-unknown/no-dto-redefinition: error(境界 unknown の DTO 再定義禁止、assert/parse 必須、出典: 同85-92行目)
    • boundary-unknown/require-unknown-at-boundary: error(境界API呼び出し結果に : unknown アノテーション必須、出典: 同94-99行目)
    • boundary-unknown/require-assert-before-use: error(境界由来 unknown を使用前に assert/typeGuard 必須、出典: 同101-108行目)
    • boundary-unknown/require-parse-or-assert: error(境界由来 unknown の DTO 再定義禁止、assert/parse 必須、出典: 同110-117行目)
    • boundary-unknown/no-unknown-leak: error(境界 unknown の運搬禁止、関数返却/引数/オブジェクト構築、出典: 同119-127行目)
    • boundary-unknown/prefer-asserts-signature: warn(assert 関数の asserts x is T 署名推奨、出典: 同129-134行目)
    • boundary-unknown/no-unknown-suffix-naming: error(変数名が "Unknown" で終わることを禁止、出典: 同136行目)
  • プリセット設定:
    • boundarySources: c.req.json, request.json, response.json, res.json, c.var.pluginSharedData.get, pluginSharedData.get, JSON.parse, .json(suffix マッチ)(出典: qualities/eslint/plugins/boundary-unknown/index.js 46-56行目)
    • assertFunctions: assertThreadCreateBody, assertThreadCreateInput, parseThreadCreateBody, zThreadCreateBody.parse, isThreadCreateBody, Input, InputType, Body, JwtHeader, JwtPayload, IdTokenPayload, JwksResponse(出典: 同62-77行目)
    • typeGuardFunctions: isThreadCreateBody(出典: 同83行目)

How(どう守るか)

設定閾値一覧

  • boundary-unknown/no-boundary-type-assertion: error(出典: qualities/eslint/07-boundary-unknown/boundary-unknown/config.mjs 52-57行目)
  • boundary-unknown/no-unknown-double-cast: error(出典: 同73-76行目)
  • boundary-unknown/require-unknown-at-boundary: error(出典: 同94-99行目)
  • boundary-unknown/no-unknown-suffix-naming: error(出典: 同136行目)

基本遵守事項

  • 境界API呼び出し結果は : unknown アノテーションを付与する。
  • 境界由来の unknown を使用前に assert/parse 関数で型を確定する。
  • 二重キャスト(as unknown as T)は全面禁止。
  • 境界直後の DTO キャスト(as T / 型注釈)を禁止。
  • 境界 unknown を関数返却/引数/オブジェクト構築で運搬しない。
  • 変数名が *Unknown で終わる命名は禁止。より意図が明確な命名(例: rawBody, requestBody, decodedPayload)を使用する。
  • assert 関数は asserts x is T 署名を持つことを推奨。

成功パターン

/**
 * @file 境界 unknown の適切な扱い
 */
import type { ThreadCreateBody } from './types';

/**
 * スレッド作成リクエストを処理(型安全)。
 * @param c Hono コンテキスト
 */
export async function handleCreateThread(c: Context<AppEnv>): Promise<Response> {
  // 境界API呼び出し結果に : unknown アノテーションを付与
  const pluginRequestBody: unknown = c.var.pluginSharedData?.get("requestBody");
  const requestBody: unknown = pluginRequestBody ?? (await c.req.json());
  
  // assert 関数で型を確定(asserts x is T 署名)
  assertThreadCreateBody(requestBody);
  
  // 型確定後は安全に使用可能
  const title = requestBody.title;
  return ok(c, { title });
}

/**
 * ThreadCreateBody の型ガード。
 * @param body 検証対象
 */
function assertThreadCreateBody(body: unknown): asserts body is ThreadCreateBody {
  if (typeof body !== "object" || body === null) {
    throw new HttpError(STATUS_BAD_REQUEST, "invalid body", { expose: true });
  }
  const b = body as Record<string, unknown>;
  if (typeof b.title !== "string") {
    throw new HttpError(STATUS_BAD_REQUEST, "title required", { expose: true });
  }
}

失敗パターン

// NG: 境界API呼び出し結果に : unknown アノテーションがない
const requestBody = await c.req.json();

// NG: 二重キャスト(as unknown as T)
const body = requestBody as unknown as ThreadCreateBody;

// NG: 境界直後の DTO キャスト
const body: ThreadCreateBody = await c.req.json();

// NG: assert 関数を使わずに使用
const title = requestBody.title; // unknown のまま使用

// NG: 変数名が *Unknown で終わる
const bodyUnknown: unknown = await c.req.json();

// NG: 境界 unknown を関数返却で運搬
function getRequestBody(): unknown {
  return await c.req.json(); // 運搬禁止
}

エラーメッセージ:boundary-unknown/no-boundary-type-assertion, boundary-unknown/require-unknown-at-boundary, boundary-unknown/no-unknown-suffix-naming など

LLM典型NGパターン(最低5件)

  1. 境界API呼び出し結果に : unknown アノテーションがない: 境界API呼び出し結果を unknown で受け取らない

    • 回避策: 境界API呼び出し結果に必ず : unknown アノテーションを付与する
  2. 二重キャスト(as unknown as T)の使用: 型安全性を回避する二重キャストを使用する

    • 回避策: assert/parse 関数で型を確定し、二重キャストは使用しない
  3. 境界直後の DTO キャスト: 境界API呼び出し直後に as T や型注釈でキャストする

    • 回避策: まず : unknown で受け取り、その後 assert/parse 関数で型を確定する
  4. assert 関数を使わずに使用: 境界由来の unknown を assert/parse 関数で型確定せずに使用する

    • 回避策: 使用前に必ず assert/parse 関数で型を確定する
  5. 変数名が *Unknown で終わる: bodyUnknown, requestBodyUnknown などの命名を使用する

    • 回避策: より意図が明確な命名(rawBody, requestBody, decodedPayload)を使用する
  6. 境界 unknown の運搬: 境界 unknown を関数返却/引数/オブジェクト構築で運搬する

    • 回避策: 境界 unknown は同一スコープ内で assert/parse 関数で型確定し、運搬しない
  7. assert 関数の asserts x is T 署名がない: assert 関数が asserts x is T 署名を持たない

    • 回避策: assert 関数は asserts x is T 署名を持つように実装する

事前チェックリスト(コード生成前)

  • 境界API呼び出し結果に : unknown アノテーションを付与
  • 境界由来の unknown を使用前に assert/parse 関数で型を確定
  • 二重キャスト(as unknown as T)を使用しない
  • 境界直後の DTO キャスト(as T / 型注釈)を避ける
  • 変数名が *Unknown で終わる命名を使用しない
  • 境界 unknown を関数返却/引数/オブジェクト構築で運搬しない
  • assert 関数は asserts x is T 署名を持つ

修正方針(違反発見時)

  1. 境界API呼び出し結果に : unknown アノテーションがない場合は、アノテーションを追加する
  2. 二重キャスト(as unknown as T)を使用している場合は、assert/parse 関数で型を確定する
  3. 境界直後の DTO キャストがある場合は、まず : unknown で受け取り、その後 assert/parse 関数で型を確定する
  4. assert 関数を使わずに使用している場合は、使用前に assert/parse 関数で型を確定する
  5. 変数名が *Unknown で終わる場合は、より意図が明確な命名に変更する
  6. 境界 unknown を運搬している場合は、同一スコープ内で assert/parse 関数で型確定し、運搬を避ける
  7. assert 関数が asserts x is T 署名を持たない場合は、署名を追加する

そして、実際に「数か月間AI(LLM)とコーディングをしてわかった」重要な点は、それでも、「コンテキストに制約事項を自然言語で含めただけでは、ほぼ制約事項は守られず、eslintといった『関数として、更新したパラメータ(ソースコード)と制約の差異』を計測するツールがないと、LLM自身も制約を守ることができない」 という確率的モデルの現実なのです。(lint修正でかなりのトークンが消費されるのが現実)

これも「勾配降下」論の実例と言えるのではないでしょうか。

実践編へとつづく、、、かも

  1. 「尤もらしい」であり、「第三者からみてもっともらしく見える」という社会的な意味は含まないことにご注意ください。 ↩

  2. 感覚的にいうと、コーディングに関しては、「勾配を降下する」というイメージ(既に存在する地形を降りるイメージ)よりも、地形を削る、掘るといったニュアンスが近いかとは思うのですが、それは「コーディング」タスクだから、というよりも、「モデルのパラメータが可変」なタスクだから、というところが理由と感じますので、この箇条書きには含めていません。ただしこの観点は後述の「イテレーション回数」に影響を及ぼすのではないかという感覚を持ちます。 ↩

  3. 厳密な機械学習の用語で言えば、lint や型検査は「正則化項」や「目的関数」ではなく、満たすべき制約(feasibility condition)に近いかと思います。しかし、本稿で扱うように、各イテレーション(LLMの推論)で MCP ツールにより自動評価され、その結果をもとに次の生成を更新している運用では、事実上の目的関数として機能しています。そのため、本稿ではこれらをまとめて「目的関数(合否の数を返す評価関数)」と呼びます。 ↩

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?