Guide/guides

チェックと監査の詳細

check、audit、gate、introspect、doctor の一覧は CLI リファレンスにあります。このページには、その裏にある規則をまとめています。audit の Policy ルールが見るもの、audit の指摘の抑制方法、introspect が実行するもの、エージェント向けのルートとインプロセスエージェントに追加される規則、アーキテクチャ境界、チェックが登録済み(起動はしていない)アプリから読む事実です。

チェックと監査の詳細

check、audit、gate、introspect、doctor の一覧は CLI リファレンスにあります。このページには、その裏にある規則をまとめています。audit の Policy ルールが見るもの、audit の指摘の抑制方法、introspect が実行するもの、エージェント向けのルートとインプロセスエージェントに追加される規則、アーキテクチャ境界、チェックが登録済み(起動はしていない)アプリから読む事実です。

audit の Policy ルール

audit の Policy ルールは、警告(warn)までしか出しません。あるモデルに対する Policy(make:policy が書く app/Policies/<Model>Policy.ts。同じアプリルートの中で対応付けます)が 1 つでもあると、安全でないメソッドのコントローラアクションのうち、本体でそのモデルを参照しているものに policy:<METHOD> <path> の指摘が付きます。アクションが次のどれかに当てはまれば合格(pass)です。

  • this.authorize() か this.can() を呼んでいる
  • ゲートに問い合わせている
  • Policy クラスを参照している
  • authorize()/authorizeResource() ミドルウェアの後ろにある

スキャンで見える範囲で Policy を参照していなければ、アクション名、Policy 名、直し方を添えて警告します。コントローラのソースが読めなかった場合も、合格にはせず警告します。アクションが呼んでいるヘルパーの中までは追いかけません。認可の判定を別の場所で行っているアクションには、その上のコメントに // guren-audit-ignore と書いてください。その指摘は、このコメントを理由に無視済み(ignored)として報告されます。Policy がないアプリでは、この指摘は出ません。

audit の指摘を抑制する

audit の誤検出は、2 つの方法のどちらかで抑制します。どちらを使うかは、指摘にソースの行があるかどうかで決まります(policy:* はどちらも受け付けます)。

ソースの行がある指摘は、その行か直前の行に // guren-audit-ignore を置けば抑制できます。

// guren-audit-ignore -- ドキュメント用のサンプル値
const apiKey = 'example-not-a-real-key'

ルート単位・モデル単位の指摘(authz:*、policy:*、validation:*、agent-annotation:*、mass-assignment:*、hidden-columns:*)には、コメントを付けられる行がありません。ルートレジストラを実行し、モデルを検査して得られる指摘だからです(policy:* だけは、直す場所がアクションなので、その上のコメントに置いたマーカーも受け付けます)。これらを無視するには、config/audit.ts に指摘の key(--json の出力からそのままコピーできます)と、省略できない reason を書きます。force-write-request-data:* と set-spread:* もコントローラーのメソッド単位の指摘で行を持たないので、同じ方法で無視します。

// config/audit.ts
export default {
  ignore: [
    { key: 'authz:POST /webhooks/stripe', reason: 'コントローラでHMAC署名を検証済み' },
  ],
}

無視した指摘もレポートには残り、status: "ignored" と ignoreReason が付きます。何も言わずに握りつぶされる指摘はありません。key や reason が抜けているエントリと、どの指摘にも一致しなかったエントリは、それ自体が警告として報告されます。そのため、使われなくなったルールが気づかれないまま残ることはありません。

config/audit.ts で無視できる指摘は、ソースの行を持たないもの、つまり上のルート単位・モデル単位の指摘に限られます。行に結び付く指摘(ハードコードされた認証情報、生 SQL、無効にされたセキュリティのデフォルト設定)には、すでに // guren-audit-ignore という方法があります。こうした指摘を対象にしたエントリは適用されず、インラインコメントを使うよう促す警告になります。目に付きにくい 2 つ目の抑制方法ができてしまうのを避けるためです。

登録済みアプリのイントロスペクション

guren introspect は、ソースコードの文面ではなく、アプリそのものから答えを得ます。GUREN_INTROSPECT=1 を付けた子プロセスで src/main.ts を import し、すべての provider とルートを登録したところで止まります。ルートはマウントしません。provider の boot()、createApp({ boot }) のコールバック、listen() は実行しないので、ポートは使わず、boot で接続する雛形の defineDatabaseConfig() の定義もデータベースにつなぎません。一方、モジュールスコープや register() で接続するコードは実行されます。子プロセスは guren dev と同じくアプリのルートにある .env を読み込み、シェルで設定した変数はそれより優先されます。--app を使う場合は、カレントディレクトリの .env も CLI が読み込んで同じ経路で子プロセスに渡すので、こちらもアプリの .env より優先されます。

bunx guren introspect                 # provider・ルート・サービス・警告を表で表示
bunx guren introspect --json          # ツール向けのマニフェスト
bunx guren introspect --timeout 60    # 遅い register() を 60 秒まで待つ(既定は 30 秒)
bunx guren introspect --app ../api    # 別のアプリのルートを調べる

provider ごとに、登録の結果が次のどれかで示されます。

  • ran
  • introspect-hook: introspect() が定義されていて、register() の代わりにそれを実行した
  • threw: 例外のメッセージを記録し、ほかの provider の登録は続ける
  • skipped: 遅延登録の provider(deferred provider)

register() で接続を開いたり、実行時のバインディングを読んだりする provider は、introspect() を定義して、マニフェストに必要なものだけをバインドできます。

import { ServiceProvider } from '@guren/core'
import Redis from 'ioredis'

export class RedisProvider extends ServiceProvider {
  register(): void {
    // Connects as soon as it is constructed.
    this.container.instance('redis', new Redis(process.env.REDIS_URL || 'redis://127.0.0.1:6379'))
  }

  introspect(): void {
    // The manifest does not describe 'redis', so nothing needs binding here.
  }
}

provider の外のコードでは、isIntrospecting() で同じ判定ができます。このフラグが立っていると、app.boot() は登録を終えたところで止まり、app.listen() は例外を投げます。そのため、import の途中で listen() を呼ぶエントリは失敗し、bin/serve.ts の形に直すよう案内するメッセージが出ます。失敗したときは、--json が { "status": "failed", "reason", "message" } を出力し、終了コードは 1 になります。reason は no-entry、import、timeout、crashed、old-server のどれかです。old-server は、introspection に対応する前の @guren/core がインストールされている場合で、エントリを import する前に検出します。

エージェントに公開したルート

.agent() メタデータを宣言したルート(ルーティングを参照)は、check の検査対象になり、audit ではより厳しく扱われます。ルールは通常の check スイートで実行され、該当するルートがあるときだけ有効になります。エージェント公開ルートがないアプリでは指摘は出ず、コントローラの走査も行いません。

check が 失敗(fail)にするもの:

指摘のキー ルール
agent-route-name:* エージェントのメタデータを宣言しているのに .name() がない。ツール名はツールを識別する名前そのものなので、名前のないルートはツールになれない。
agent-route-tool-name:* ツール名(agent.toolName またはルート名)が MCP の文法 ^[A-Za-z0-9._-]{1,128}$ に合っていない。クライアントは、そのツールだけでなくツール一覧全体を拒否する。
agent-route-reserved-name:* フレームワークが予約しているツール名を使っている。guren_preflight は、MCP エンドポイントが自分で追加するメタツールの名前。この名前を使ったルートは、まったく公開されない。
agent-route-portable-name:* (警告、参考扱い) MCP としては正しいツール名だが、文法 ^[A-Za-z0-9_-]{1,64}$ に合っていない。この文法は Claude と OpenAI のツール API が課しているもので、Claude Managed Agents は MCP ツールにも適用する。これらのクライアントは、ツールを何も言わずに読み飛ばす。agent.toolName に、どのクライアントでも通る綴り(posts_index)を設定すること。参考扱いなので、check --ci と guren gate はこの指摘では失敗しない。
agent-route-duplicate:* 2 つ以上のルートが同じツール名になる。
agent-route-authorization:* 読み取り専用ではないツールなのに、ミドルウェアチェーンに認可の役割(capability)を持つものがなく、コントローラアクションでも this.authorize(...) を呼んでいない。認証は認可の代わりにならない。this.auth.userOrFail() や API トークンの確認だけの場合は、専用のメッセージで報告される。

check が 警告(warn)にするもの:

指摘のキー ルール
agent-route-output:* ルートに output スキーマも resource ヒントもないので、導出したツールが出力の形を示せない。読み取り系だけでなく、書き込み系のツールにも適用する。
agent-route-inertia:* アクションが this.inertia(...) で応答していて、出力の形も宣言していない。このツールは、ページがコンポーネントに渡した内容をそのまま返すことになる。このルートでは、上の指摘の代わりにこちらが報告される。
agent-route-input:* ボディを持つメソッドのルートに body スキーマがなく、導出される入力スキーマがパスとクエリだけで組み立てられる。インラインハンドラでは、このスキーマがリクエスト時の検証そのものでもあるので、送られてきた内容を検証するものが何もないことになる。
agent-route-annotation:* 読み取り専用のツールなのに、アクションがレコードを削除・更新・force-write している。更新系のメソッドに readOnlyHint: true を明示した場合と、デフォルトで読み取り専用になる GET・QUERY の場合のどちらも対象。読み取り専用であれば認可ルールが適用されなくなるので、アクションの中身と照らし合わせて検査する。
agent-route-authorization:* 判定までたどり着けなかった。ハンドラがインライン関数の場合か、コントローラアクションが check の読み取り対象に入っていない場合に出る。
agent-route-controller-collision:* 同じ名前のコントローラクラスが 2 つあり、その一方をエージェント公開ルートが使っている。コントローラ本体から導いた判定が、もう一方のクラスのものになっているおそれがある。
agent-route-controller-unreadable:* コントローラのファイルを読み取れなかった。そのファイルで定義されたアクションを使うエージェント公開ルートは、本体をまったく見ないまま検査されたことになる。
agent-route-controller-unparsed:* コントローラのファイルを構文解析できなかった。そのファイルで定義されたアクションを使うエージェント公開ルートは、本体を見ないまま検査されている。そのファイルのクラス名を指すルートが、同名のクラスを宣言した別のファイルと照合されることがあり、そのときは名前の衝突が報告されない。
route-graph ルートファイルの読み込みに失敗したので、ルート契約のチェックもエージェントルートのチェックも実行されなかった。

audit は、同じルートに対して次のルールを追加します。

  • 通常のルートでは警告になるボディ検証の指摘が、エージェント公開ルートでは 失敗 になります。キーは validation:* のままなので、既存の config/audit.ts のエントリはそのまま効きます。
  • agent-annotation:* は、レコードを削除・更新・force-write するアクションに destructiveHint: false が宣言されている場合と、アクション本体が読めずにその宣言を検査できなかった場合に警告します。
  • controller-unreadable:* は、コントローラのファイルを読み取れなかった場合に警告します。そのファイルで定義されたアクションについては、上のルールのどれも本体を見られていないからです。
  • controller-unparsed:* は、コントローラのファイルは読み込めたものの構文解析できなかった場合に警告します。そのファイルのアクションの本体は、上のルールからは見えません。さらに、クラス名だけでそのファイルのクラスに対応づけられたルートは、同名のクラスを宣言した別のコントローラファイルの本体で判定され、controller-name-collision:* も出ません。

インプロセスエージェント

@guren/plugin-ai の Agent のサブクラスには、check の専用ルールがあります。これも該当するクラスがあるときだけ有効になり、ないアプリには何も追加されません。対象は、このパッケージの Agent(名前付き import でも namespace import でもかまいません)を継承したクラスと、そのクラスをさらに継承したクラスだけです。親クラスは、識別子の綴りではなく、そのファイルの import を解決して判定します。@guren/plugin-agents の永続エージェントは対象外で、別の場所にある同じ名前のクラスを親と取り違えることもありません。

check が 失敗(fail)にするもの:

指摘のキー ルール
ai-agent-tool-underived:* リテラルで書かれた appTools([...]) の名前を、どの .agent() ルートも導出していない。as() が例外を投げる。
ai-agent-tool-unscoped:* クラスの static scopes(自分で宣言したものか継承したもの)がその名前を許可していない。判定は RFC 0016 の文法(tool:<name>、tools:<prefix>.*、tools:read、tools:*)に従う。scopes のないクラスは何も許可しない。
ai-agent-scope-malformed:* static scopes に文法に合わないエントリ(tickets_show のような名前だけのものなど)がある。そのエントリは何も許可せず、as() が例外を投げる。
ai-agent-audit-duplicate aiPlugin({ audit }) と mcpPlugin({ audit }) の両方で監査ログを設定している。最初のツール呼び出しで例外になる。各プラグインは自分のサーフェスへの呼び出しを自分のキューで承認待ちにするので、承認キューが 2 つあること自体は報告しない。

check が 警告(warn)にするもの:

指摘のキー ルール
ai-agent-plugin-missing Agent のサブクラスがあるのに、プロジェクト内のどのソースファイルも aiPlugin() を呼んでいない。appTools() を呼ぶエージェントは最初の as() で例外になり、queue() と監査ログにもプラグインが必要になる。根拠が「呼び出しが見つからないこと」なので、バインドされていないセッション設定と同じく警告にしている。
ai-agent-app-tools-unreadable:* appTools() の引数が文字列リテラルの配列ではない(スプレッド、変数、計算された要素)。名前を検証できないので、合格にもしない。
ai-agent-scopes-unreadable:* static scopes がリテラルの配列ではないので、名前をスコープと照らし合わせていない。
ai-agent-tools-unverified:* ルートグラフの読み込みに失敗したので、名前を導出済みのツールと照らし合わせていない。

audit は、エージェントの tools() が appTools() のスプレッドと一緒に返すローカルツールを、専用の見出しと --json の aiLocalTools にすべて列挙します。ローカルツールはクロージャの権限で動き、スコープ、ポリシー、承認、監査ログのどれも通りません。ai-local-tool-write:* は、ツールの execute が Model の書き込み(create、update、delete、save)を呼んでいて、そのモデルのテーブルを .agent() ルートのアクションも使っている場合に警告します。その場合は、ルートのほうをエージェントに渡してください。ai-local-tools-unreadable:* は、tools() が返すものが、スキャンで中身をすべて列挙できるオブジェクトリテラルではない場合に警告します。どちらの指摘もソースの行を指すので、// guren-audit-ignore で抑制できます。

アーキテクチャ境界

プロジェクトルートに guren.arch.ts を置けば、フラグを付けなくても guren check が境界を検証するようになります。

// guren.arch.ts
import { defineArchRules } from '@guren/cli/arch'

export default defineArchRules({
  layers: {
    domain: 'app/Domain/**',
    http: 'app/Http/**',
  },
  rules: [
    // ドメインロジックはHTTP層に依存してはいけない
    { from: 'domain', disallow: ['http'] },
    // コントローラはORMを直接使わずModel経由でクエリする
    { from: 'http', disallowPackages: ['drizzle-orm'] },
  ],
})

各ルールの from と disallow には、上で定義したレイヤー名か、インラインの glob を指定します。既存のコードベースに新しい境界を入れるときは、severity: 'warn' から始めて、違反がなくなったら外す(デフォルトの 'fail' に戻す)と安全です。

ルールは実行時の依存を解析します。型だけの import(import type { X } from '...'・export type { X } from '...'・型の位置にある import('...').X)はコンパイルで消えるので、デフォルトでは対象外です。DTO や props の interface をレイヤーをまたいで共有しても、たいていは問題にならないからです。型のレベルでも守りたい境界には、ルール(またはルール全体)に includeTypeImports: true を指定してください。ルール側の指定が優先されます。

rules: [
  // クエリ層への型依存は、実行時依存まであと一歩のリファクタリング距離にある。
  { from: 'frontend', disallow: ['queries'], includeTypeImports: true },
]

includeTypeImports は、guren.arch.ts に宣言したルールにだけ効きます。modules/ ディレクトリがあると自動で有効になる、設定不要のモジュール境界ルールにはこのオプションがなく、常に実行時の import だけを解析します。

AI コーディングエージェントや大きなアプリで使いやすくするためのフラグが 2 つあります。

bunx guren check --arch      # アーキテクチャチェックのみ。いちばん速い
bunx guren check --changed   # main とのマージベースからの変更ファイルのみを検査対象にする

プロジェクト内のファイルに解決できない import は、失敗ではなく警告として報告されるので、解決できないパスのせいでビルドが止まることはありません。

チェックがアプリを読む仕組み

チェックの判定が意外だったときのための節です。check、doctor、audit、gate は、一部の事実を guren introspect でアプリそのものから読み、残りをソースから読みます。

イントロスペクションの結果を読むチェック

guren check と guren doctor は、デプロイ実行環境の判定を、まずイントロスペクションの結果をもとに行います。具体的には、auth manager が持つハッシャー、選ばれているセッションストアとキャッシュストア、登録中に例外を投げた provider があるかどうかを確かめます。guren check は、セッションと添付ファイルの配線もこの結果から判定します。

チェックキー イントロスペクションの結果から読むもの
sessions-binding register() で session をバインドする provider があるか。アプリが読まないセッション設定(バインドがない、または auth.sessionOptions.store が代わりにストアを渡している)は警告になる。バインドしたマネージャと auth.sessionOptions.store を併用すると、アプリが boot を拒否するので失敗になる
sessions-config:* バインドされたセッションマネージャの database ストアが使うテーブルの SQL 名。モジュールも含め、各アプリルートの db/schema.ts が宣言するテーブルと照らし合わせる。スキーマの読み取りで見つからない名前は、参考扱いの警告にとどめる。それも、ソースからスキーマの export をたどれないテーブルに限る。この読み取りでは、drizzle.config が挙げるほかのファイルも、pgTableCreator() の接頭辞も見ない。判定は、そのストアを宣言しているすべての設定に付く。ソースから読めるどの設定もストアを宣言していない場合は、ファイルを持たない sessions-config:<store> のキーで報告する
attachments-model:*、attachments-config:* アプリの登録中に添付ファイルのエンジンが設定されたかと、そのエンジンが書き込むテーブル。テーブルはセッションと同じように照らし合わせ、ファイルを持たないキーは attachments-config になる。どの configureAttachments() もファイルの読み込み時に必ず実行される位置(関数、分岐、クラスフィールドの外)にあり、どのソースもそのファイルを import() で読み込まず、createApp({ boot }) も省略されていないのに、エンジンが設定されていなければ、モデルが失敗になる。この場合、アプリが登録中に読み込むものは、どれもそのファイルを import していない
attachments-delivery エンジンの delivery が指すルートが登録されていて、それが registerAttachmentRoutes() のルートであるか
attachments-route-name:* そのルート名を持つ登録済みルートの数
attachments-serve-redirect:* エンジンがリダイレクトで配信するディスクと、storage manager から読んだ各ディスクのドライバ
attachments-public-disk:* エンジンが書き込むディスク。ディスクの root はソースから読む
route-contract-* provider やプラグインが登録したものも含めた、登録済みのすべてのルート。params スキーマのキーは JSON Schema の properties から読み、失敗か警告かは required で決まる。JSON Schema がスキーマを表しきれない場合(nullable なオブジェクト、z.any() や z.undefined() のキー)は、同じルートをルートファイルの Zod で判定する。アプリだけが登録するルートには Zod がないので、表しきれない場合は読めないものとして報告する。注記なしに落ちたキー(z.undefined())は検出できない
agent-route-* .agent() を宣言したすべてのルート。コントローラはファイルと export で特定する
prototype-* 名前付きのすべてのルート。provider が登録するルートを指すフィクスチャのエントリは、孤立したものとして扱わない。createApp({ prototype }) の配線とフィクスチャ自体は、ソースから読む

イントロスペクションは、それを必要とするチェックがあるときにだけ行います。対象は、デプロイプラグインか Lambda アダプタを宣言したアプリ、セッション設定があるアプリ、configureAttachments() を呼ぶアプリ、Attachable(...) を組み込んだモデルがあるアプリです。ルートファイルで、params スキーマかバインディングを持つルート、.agent() を宣言したルート、prototype ルートのどれかを登録しているアプリと、プロトタイプのフィクスチャがあるアプリも対象です。

各ルールは、自分の対象があるときにだけイントロスペクションを求めます。そのため route-contract は、ルートファイルに params スキーマかバインディングがある場合にだけ、アプリだけが登録するルートを判定します。エージェントルートのルールも、ルートファイルにエージェントルートがある場合にだけ判定します。--routes を付けると、ルートのルールはルートファイルを読みます。マニフェストは、指定したファイルからではなくアプリのエントリから作られるためです。

イントロスペクションは 1 回の実行につき最大 1 回で、ソースファイルを変更していない --changed の実行では行いません。マニフェストは実行環境の .env を読み込んだ状態で作られるので、環境変数で選ぶストアは、ローカルの値で判定されます。また、イントロスペクションは provider の boot() より前で止まります。そのため、関数の中で呼ぶ configureAttachments() については、呼び出しのオプションをソースから、ルートとストレージのドライバをアプリから読みます。

イントロスペクションは、guren check、doctor、audit、gate、plan:verify で行います。上限はどれも 10 秒なので、check --ci とゲートの判定は一致します。編集フック(check --changed)と dev MCP サーバの guren_check はイントロスペクションを行わないので、アプリでしか答えられない判定は -unverified になります。

これらの結果には、--json の出力で evidence が付きます。複数の事実を読む判定では、その中でいちばん弱い根拠を示します。

evidence 意味
manifest イントロスペクションの結果を使って判定した。マニフェストにない事実(OAuth の state ストア、明示的に生成したインメモリストア)はソースの走査で補う
static 事実がソースにあるので、ソースから判定した。provider の自動検出、ディスクの root、関数の中で呼ぶ configureAttachments() のオプション、スキーマが export すべきセッションや添付ファイルのテーブル、アプリが読まないセッション設定がこれにあたる。テーブルの export がないとイントロスペクションそのものが失敗するので、読めるのはソースだけになる。マニフェストを使わなかった理由はメッセージに書かれる
none アプリでしか答えられないのに、それを裏付けるマニフェストがない。アプリをイントロスペクションしなかった場合、失敗した場合、register() で例外を投げた provider がある場合、未設定の環境変数を読む設定がバインドされなかった場合、遅延登録の provider(deferred provider)か、自分を説明できないバインディングがそのセクションを提供している場合がこれにあたる。キーの末尾は -unverified になり、結果は参考扱いの警告で、合格にはならない。対象は、デプロイのハッシャーとストア、セッションのバインド、添付ファイルの配信ルートとリダイレクトのディスク

イントロスペクションが失敗すると、check は理由を書いた参考扱いの introspection-unavailable を 1 行加えます。ソースから読める判定はソースで行い、それ以外は -unverified になります。doctor は理由を JSON の evidenceReason に入れます。--no-introspect を付けると、イントロスペクションを行いません。エントリがまだ import できないアプリで使ってください。

bunx guren check --no-introspect
bunx guren doctor --no-introspect

デプロイ時のビルドも同じ判定を行います。イントロスペクションの上限は 10 秒で、判定ごとに根拠を 1 行で表示します。

guren audit は、ルート単位のルール(validation:*、authz:*、agent-annotation:*)をイントロスペクションの結果で判定します。イントロスペクションは、ルートファイルが更新系のルートかボディを持つルートを登録している場合と、ルートファイル単体では読み込めない場合(アプリとしてなら登録できることがあります)にだけ行います。--routes を付けた場合は、マニフェストがアプリのエントリから作られ、指定したファイルとは一致しないので行いません。マニフェストを使うと、判定は次のように変わります。

  • ミドルウェアの別名(alias)は、アプリのどこで登録したものでも解決された状態で届きます。provider が登録する auth の別名の後ろにあるルートは、authz:* が合格になります。ルートファイルだけを読み込んだ場合は、認識できないガードとして報告されます。
  • 認可はするものの認証をしないチェーンは警告のままで、メッセージに確認すべき内容が入ります。内容は、1 つの ability、複数の ability のどれかまたはすべて、リクエスト時に決まる ability のいずれかです。ゲストのリクエストは null ユーザーのままゲートに届くので、ポリシーが通してしまうこともあります。
  • アプリのどこにも別名や group として登録されていない名前は、未解決として報告します。そのルートは boot 時のマウントで失敗するので、同じチェーンのガードより先に判定し、ガードがあっても合格にはしません。イントロスペクションが実行しない部分(createApp({ boot }) のコールバックや、register() の代わりに introspect() フックを実行した provider)がその名前を登録する可能性がある場合は、メッセージにそのことを書きます。
  • コントローラはファイルと export で特定します。2 つのモジュールがそれぞれ ReportController を宣言していても、各ルートは自分のクラスで判定されます。ルートのクラスがコントローラのファイルのどの export とも一致しない場合(ルートファイルの中で宣言したクラスなど)は、同じ名前で export されたクラスのボディは使いません。ルートファイルかエントリがその名前のクラスを宣言している場合も同じで、そのルートは解析できないものとして報告します。それ以外の場合は、コントローラのファイルが export せずに宣言した同名のクラスか、イントロスペクション中に import が失敗したファイルのクラスを、名前で読みます。controller-name-collision:* は、この名前による読み取りで 2 つのファイルが同じ名前を宣言している場合と、クラスが再 export を通して見つかった場合にだけ報告します。ボディの検査(validateBody()、userOrFail())は、これまでどおりアクションのソースを読みます。

guren check のエージェント公開ルートのルールも、同じ方法でコントローラを特定します。生 SQL、認証情報、mass assignment、CSRF の除外は、どちらの場合もソースから判定します。

ルート単位の指摘には evidence が付きます。マニフェストだけで決まった判定(ガードが担う認証・認可の役割、ルートが強制するボディスキーマ)は manifest、コントローラのボディかルートファイルを読んだ判定は static です。JSON の routeSource には何を読んだかが入り、ルートファイルを読んだ場合はその理由も入ります。イントロスペクションが失敗すると introspection-unavailable の警告を 1 件加えますが、終了コードは変わりません。register() で例外を投げた provider があると、ルールはルートファイルでの判定に戻ります。その provider が、ルートの使う別名を登録するものかもしれないからです。--no-introspect を付けると、ルートファイルだけを読みます。

bunx guren audit --no-introspect

イントロスペクションの結果から読むルート一覧

guren context は、イントロスペクションで得たアプリのルートを一覧にします。provider やプラグインが登録したルートは含まれ、createApp() がマウントしない modules/ 配下のモジュールは含まれません。スキーマの型はこれまでどおりルートファイルから描画するので、アプリだけが登録するルートには型が付きません。--no-introspect か --routes を付けると、ルートファイルのルートを一覧にします。アプリをイントロスペクションできない場合は、Routes の節にその理由を 1 行書き(--json では routesNotIntrospected)、ルートファイルのルートを一覧にします。guren context <Entity> は、2 つのファイルが宣言するコントローラクラスにルートが届く場合にだけイントロスペクションを行い、--routes を付けた場合は行いません。

guren doctor の prototype-routes は、ルートファイルが prototype ハンドラを使っている場合に、イントロスペクションで得たルートを数えます。

guren codegen は、--introspect を付けない限りルートファイルを読みます。Vite プラグインが編集のたびに codegen を実行し、guren check、doctor、guren gate もデフォルトの codegen の出力を基準にしているからです。--introspect を付けると、どのルートがどの順で存在するかはアプリが決め、各ルートはルートファイルの Zod から描画します。すべてのルートがルートファイルとそのモジュールから来るアプリでは、生成物はバイト単位で一致します。例外は、同じ名前のルートが 2 つあり、createApp({ modules }) のモジュールの並びがディレクトリの並びと違う場合です。

アプリだけが登録するルートはスキーマの型なしで追加され、そのルートを挙げた警告が出ます。そのルートのエージェントツールはマニフェストから取ります。2 つのルートが同じツール名を持つ場合は、実行時と同じく、アプリが先に登録したほうを残します。この出力が残るのは、フラグなしで次に codegen が実行されるまで(Vite の監視によるものも含む)です。エージェントツールがすべて provider から来るアプリでは、check と doctor が書き出された .guren/agents.gen.ts を古いものとして報告し、案内に従って guren codegen を実行すると削除されます。ルートファイル自身もツールを導出するアプリでは、check と doctor はファイルがあるかどうかしか見ないので、追加されたツールについては何も報告しません。アプリをイントロスペクションできない場合と、--routes が check の見つけるエントリとは別のファイルを指している場合は、理由を表示したうえでルートファイルから生成します。

bunx guren codegen --introspect

guren spec:generate と check --spec は、常にルートファイルを読みます。ビューはコミットされるもので、guren gate はイントロスペクションせずにプロセス内でビューを生成し直します。そのため、マニフェストから書いたビューは、そこで差分として報告されてしまいます。