レビュー指摘管理 仕様書
ステータス: Draft / 作成日: 2026-08-26 依存: GitHub 連携(
github_integrations、1 プロジェクト = 1 リポジトリ)、GitHub App、PAT スコープ
1. 背景と課題
開発フローは「AI がコードを書く → 別の作業者(AI または人間)がレビュー → 修正 → 再レビュー」 を回しているが、次の 2 つが遅さと散らかりの原因になっている。 レビュワーは AI と人間のどちらもあり得る。本仕様の全機能は両者を同格に扱う (AI は PAT + CLI、人間はセッション + Web UI が主経路。モデル・権限規則は共通)。
-
Low までマージ前に完璧に直している。 優先度の低い指摘の修正までマージの前提に なっており、1 機能のリードタイムが不必要に伸びる。
-
GitHub が指摘のデータ置き場になっている。 インラインコメントを 1 指摘 = 1 スレッドで 積むと PR ページが重くなり、見通しも悪い。実例が #587 で、スレッド 28 本すべてが 1 コメントのみ(返信ゼロ)、resolved は 16/28 で管理が途中で崩れている。 議論の場としても状態管理としても機能していない。 かといって Low を GitHub Issue に逃すと Issue 一覧が散らかる。
本仕様は、レビュー指摘を task 自身のデータとして管理し(ドッグフーディング)、 GitHub 側には bot の要約コメント 1 本だけを置く形に切り替える。
将来の「AI がレビューを検知して自動修正するループ」は本仕様の範囲外だが、 その土台(機械可読な指摘・状態・集計)になるようにモデルを設計する(§9)。
2. 運用ルール
コードを書く前に決まる規約。task と vrt の両リポジトリに適用する。
3. 概念モデル
project(GitHub 連携は要約コメントの投稿にだけ必要。無くても起票・管理はできる)
└── reviews(レビューラウンド: 1 PR への 1 回のレビュー)
└── review_findings(指摘)
reviews(レビューラウンド)
予定(Git ホスティング↔タスク連携 §2):
integration_id/repo_owner/repo_name/pr_number/pr_title/pr_authorは PR の実体表forge_pull_requestsへ移り、reviewsはpull_request_idで参照する。PR の自然キーはhost+host_url+repo_owner+repo_name+number(プロジェクト境界を含む)で、採番の UNIQUE は(pull_request_id, round)になる。API の読み取り指定は下記の host-aware な規則に従う。
同一 PR への再レビューは新しいラウンドとして作る(更新しない)。round はサーバーが
PR 内で採番し、「どのラウンド(R1, R2, …)で出た指摘か」「どの head を見たか」が
履歴として残る。移行後の採番には UNIQUE (pull_request_id, round) を張り、同じ PR へ
ほぼ同時にラウンドが確定しても番号が重ならないようにする(採番から
挿入までをプロジェクト行のロックで直列化し、制約は最後の防波堤として残す)。番号が
重なると R1, R2, … の表示と「どの head を見た判断か」の対応が崩れる。
PR を指すキーにホストとリポジトリを含める。 プロジェクトの連携先は解除・再連携で
差し替えられるので、project_id + pr_number だけだと、旧リポジトリの PR #10 と
新リポジトリの PR #10 が同じ PR として続き、旧リポジトリ向けの指摘を新リポジトリへ
投稿してしまう。採番・一覧・集計・要約更新ジョブの合流キーは、いずれもラウンドに
控えた host / host_url / リポジトリを含めた単位で扱う。一覧と集計が見るのは現在の連携先
(連携が無ければリポジトリ無し)のラウンドで、旧リポジトリのラウンドは
履歴として残るが混ざらない。
連携を解除しても指摘は消さない(integration_id は NULL になるだけ)。指摘の一覧と
状態の権威は task 側にあり、GitHub 連携は要約コメントの投稿にだけ必要だから
(取り込んだ Issue のリンクが解除で消えるのとは扱いが違う)。
ラウンドは確定時に指摘ごと一括作成し、確定後の指摘の追記はできない。 人間のレビュワーは UI の下書きに指摘を貯めて最後に確定する(確定 = 一括作成 API を 1 回呼ぶ。下書きはクライアント側の関心事でサーバーは持たない)。出し忘れは 新しいラウンドとして出す——1 件だけのラウンドも正当。追記を許すと 「どの head を見た時点の判断か」というラウンドの意味が濁る。
review_findings(指摘)
状態遷移
open ──→ fixed ──→ verified (修正宣言 → レビュー側の確認)
↑ │
│ └─→ open (差し戻し: 再確認で未修正と判断。レビュー側のみ)
├─⇄ deferred (Low/Nit のみ繰り延べ可。同プロジェクトに通常タスクを自動起票しリンク。
│ open へ戻すとき自動起票タスクはシステムが自動クローズ)
└─⇄ rejected (指摘自体が誤り。遷移・再オープンとも、その指摘を出した
ラウンドの作成者だけ)
-
fixedへはwrite:reviewを持つ誰でも遷移できる(修正側の宣言) -
verifiedへの遷移とfixed → openの差し戻しはレビュー側だけ: その指摘を含む ラウンドの作成者、または同じ PR のより新しいラウンドの作成者。fixedを宣言した 本人は不可(自分の修正を自分で検証済みにできない)。fixed → openの差し戻しが 再レビューの「未対応」判定に相当する。後から出したラウンドの作成者にも認めるのは、 再レビューの判定(解消/未対応)がまさにこの遷移だから -
rejectedへの遷移とrejected → openは、その指摘を出したラウンドの作成者だけ。 ただし作成者がテナントの利用者でなくなっている場合に限り、テナントオーナーが 代行できる(誰が代行したかは遷移履歴に残り、代行での棄却は件数を集計と要約コメントに 常設する。§5 / §7。数えるのは代行で棄却された指摘の件数で、rejected → openが 通る以上、遷移の回数で数えると指摘 1 件の痕跡が何件にも見える)。除名・退会で作成者が消えると、誤った High を取り下げる主体が 永久に居なくなり、直していないものをfixed → verifiedと記録するしかなくなる—— 監査記録に嘘を書かせないための例外。この条件はオーナー自身が作れる(除名 → 代行 → 再招待)が、それは防がない(§2「オーナーも境界の内側」)。 「より新しいラウンドの作成者」まで広げない。ラウンドは指摘ゼロでも作れる(§6)ので、 広げると修正する側が空のラウンドを 1 本確定するだけで「レビュー側」を自称でき、 他人が出した High をrejectedにしてマージ基準(§2)を 1 人で迂回できてしまう。 指摘の取り下げは出した本人の判断に閉じる。他人の指摘が誤りだと思ったら、 自分のラウンドで反論を出すか、修正側としてfixedを宣言する -
deferredへ遷移できるのはlow/nitの指摘だけ。high/mediumの繰り延べは 拒否する(409)。マージ可否は集計側(§5)が open / fixed の High・Medium を数えて出すので、 繰り延べを重大度で縛らないと、High を 1 回deferredにするだけで §2 のマージ基準を 迂回できてしまう。マージ可否の集計から外れる遷移(deferred/rejected/verified)は、 どれも「修正する側が 1 人で通せない」ことを揃えて満たす -
open → deferredはwrite:reviewの誰でも可(修正側が「これは今回やらない」と 判断する場面が主)。deferred → openも同じく誰でも可(「やはり今直す」も正当) -
deferredにした時点で、同じプロジェクトへ通常タスク(優先度 Low、本文に指摘への参照)を 自動起票してdeferred_task_idにリンクする。以降の追跡は普段のタスク運用に乗る。openへ戻す際、システムが自動起票したタスクを自動でクローズする(二重管理を作らない) -
繰り延べを繰り返しても、有効な自動起票タスクは指摘 1 件につき同時に 1 つ。
deferred → openでクローズしたあと再びdeferredにしたら、deferred_task_idの タスクが残っていれば(同じプロジェクトにあり、削除されていなければ)再オープンして 使い回す。削除済み・リンク無しなら代替を 1 件起票してリンクを付け替える。 毎回起票すると、往復のたびにseq_idと通知を消費してタスク一覧が同じ内容で埋まる。 「常に同じ物理タスク」ではなく「同時に存在する有効なタスクは 1 件」が不変条件 -
タスクの削除はソフトデリート(
deleted_at)で、deferred_task_idの外部キーは 外れない。生死はリンク先のdeleted_atで判定する(リンクが残っているかで 判定すると、削除済みのタスクを再オープンして黙って復活させてしまう) -
再オープンは完了・畳みを解いて既定ステータスへ戻し、
completed_atを消す。 既定ステータスが無ければ起票時と同じく拒否する(409。指摘の状態は変えない) -
完了ステータスの無いプロジェクトでは、クローズ時にタスクをソフト削除で畳むため、 再繰り延べは毎回「削除済み → 代替起票」になる。これは許容する(畳み方を 可逆にするより、この稀な構成でタスクが作り直される方を選ぶ)
-
verifiedは終端(戻さない。誤りだったと分かったら新しいラウンドで指摘を出し直す) -
各遷移は誰がいつ行ったかを記録する(監査ログと同じ流儀)
-
状態遷移は指摘の行をロックして直列化する(
SELECT … FOR UPDATE相当)。 状態の確認、副作用(繰り延べタスクの起票・再オープン・クローズ)、deferred_task_idと状態の更新までを同一トランザクションで行う。 直列化しないと、同じ open の指摘へdeferredが同時に届いたとき双方がタスクを 起票し、後勝ちのリンクだけが残ってもう 1 件が参照されない孤児になる—— 「有効なタスクは同時に 1 件」の不変条件が並行実行で破れる。 ラウンドの採番(プロジェクト行のロック)・要約ジョブ(Redis のロック)と同じく、 書き込みの単位ごとに直列化の道具を明記しておく
4. 認可
-
新スコープ
read:review/write:reviewを追加する。レビュー専用の AI に タスク書き換え権限(write:task等)を渡さずに済ませるため -
write:reviewはread:reviewを包含する(起票した本人が自分の結果を読めないと 運用にならない)。既存スコープは task 系が包含・project 系が非包含で前例が割れているため、 ここで明示する(project 系の非包含はのちに欠落と判じ、全対を包含へ揃えた。 apps/backend/docs/personal-access-tokens-authz.md の「含意の規則」) -
ただし**
write:reviewは繰り延べ経由で通常タスクの作成・クローズ・再オープンを伴う** (§3)。write:taskを渡さない分離の例外として、この副作用だけは含むと理解する。 作られるのは指摘に紐づく 1 件だけで、本文も指摘の写しに限られる -
admin:tenantは既存規約どおり全スコープを包含する -
リソース束縛は既存の PAT 規則(テナント束縛 +
allowed_project_ids)にそのまま乗る -
セッションは既存どおり全スコープ相当。閲覧はプロジェクトに入れる人全員、 作成・遷移は §3 の役割規則に従う
5. API(概要)
パスは既存規約どおりテナント・プロジェクト配下に置く。
上表の PR 単位の読み取り(ラウンド一覧・集計・指摘一覧)は共通して pr と、任意の
repo=owner/name、host、host_url、pull_request_id を受ける。pull_request_id を
指定した場合は自然キーの候補探索を行わず、その PR 行だけを対象にする。ただし repo / host /
host_url も指定された場合は対象行との一致を検証し、1 つでも一致しなければ 400 とする。
-
PR 番号は1 以上の整数として検証する。実在確認まではしない(それは要約コメントの 投稿時に判明する。投稿失敗は起票を巻き戻さない)
-
状態遷移後も要約更新ジョブを投入する
-
絞り込みクエリに未知の値が混ざっていたら 400。黙って無視すると「絞り込みが 効いていない」ことに気づけない
-
ラウンドの採番はプロジェクト行のロックで直列化する (移行後の
UNIQUE (pull_request_id, round)が最終的な防波堤) -
409 は理由を本文(
message)に入れる(High / Medium の繰り延べ・規則にない遷移・ 既定ステータスが無い)。共通のconflictだけでは、CLI から使うレビュワーが 「直すのか、取り下げるのか」を選べない -
読み取り(ラウンド一覧・指摘一覧・集計)は既定で現在の連携先のラウンドだけを見る (§3)。過去の連携先や、連携を張る前に溜めたラウンドを読むために、 リポジトリを明示する絞り込みを用意する。これが無いと「履歴として残る」と言いながら 読む手段が無く、旧リポジトリの指摘を作成者が整理することもできない
-
読み取りの PR 選択は
repo=owner/name+pr=Nに加えてhost/host_urlまたはpull_request_idを受け付ける。pull_request_idは候補探索をせず対象を確定するが、併記したrepo/host/host_urlは対象行との一致を検証し、不一致なら 400 とする。repo + pr だけの 指定は同じプロジェクト内の候補が 1 件のときだけ許可する。複数候補は 409 とし、host またはpull_request_idの再指定を要求する。候補が無い場合も現在の連携先へ暗黙にフォールバックしない -
マージ可否は「ラウンドが 1 件以上ある」かつ「open / fixed の High・Medium が 0」。 件数だけで判定すると、一度もレビューされていない PR が 0 件として「可」で通る。 これはマージ前ゲートとして最も危ない誤りなので、レビューの不在と「指摘なし」を 区別する
-
集計は最新ラウンドの
head_sha(レビューした commit)も返す。現在の HEAD と 突き合わせるのは呼び出し側(§6)。読み取り API から GitHub を呼ばないのは、 ゲートの応答時間と可用性を GitHub に握らせないため -
集計はキャッシュ済みの現在 head とその確認時刻(要約ジョブが §7 で控えたもの。 無ければ空)も返す。画面(§8)の「レビューが古い / 鮮度不明」の出し分けに使う。 これはジョブが最後に走った時点の値であり、push では更新されない——だから ゲートの条件には使わず、表示の降格にだけ使う
-
集計はどのリポジトリを見た結果か(
repository。連携が無ければ空)も返す。 集計の視界は現在の連携先で決まる(§3)ので、連携を外すと視界が空になり、 そこで空のラウンドを 1 本作れば「レビュー済み・指摘なし」を満たせてしまう。 リポジトリが確定しない集計をゲートとして通さないのは呼び出し側の役目。 呼び出し側は CLI(§6)だけでなく画面(§8)も含む——人間は Web UI を主経路に するので(§1)、UI だけ素通しにするとゲートの半分が無いのと同じになる -
集計はオーナー代行で棄却された件数も返す。代行の条件(作成者の不在)はオーナー自身が 作れるので(§2)、マージ可否を読むその場所に痕跡を出す
6. CLI
AI レビュワーの主経路は JSON 一括投入(生成しやすく、検証もしやすい)。 人間のレビュワーは Web UI から起票する(§8。下書きに貯めて確定した時点で CLI と同じ一括作成 API を 1 回呼ぶ)。
CLI(apps/backend/crates/cli)は Rust の単一バイナリで、実行にランタイムの用意が要らない。
CI では置くだけで使える(v* タグの push で GitHub Release に添付される。導入手順は
apps/backend/crates/cli/README.md)。投入 JSON の検証と絞り込みの綴りは backend の型
(payload / entity / common::validation)をそのまま使うので、CLI と
サーバーで規則が二重にならない。
# レビュー 1 ラウンドぶんを一括起票(ファイル or `-` で標準入力)
task review submit findings.json --project TASK
task review submit findings.json --project TASK --pr 618 # JSON の pr を上書き
# 指摘一覧(フィルタつき)
task review list --project TASK --pr 618 --state open --severity high,medium
# ラウンド一覧(R1, R2, … と head SHA・件数)
task review rounds --project TASK --pr 618
# 状態遷移
task review resolve <finding-id> --project TASK --state fixed
task review resolve <finding-id> --project TASK --state deferred --note "後で直す"
# 集計。マージできない状態なら非 0 で終了する(CI や手元のマージ前確認に使える)
task review summary --project TASK --pr 618
# 照合する HEAD を明示する(CI では PR head の SHA を渡す)
# GitHub Actions の pull_request では GITHUB_SHA が合成マージコミットを指すので使わない
task review summary --project TASK --pr 618 --head "${{ ERROR }}"
task review summary --project TASK --pr 618 --no-head-check # 鮮度を見ない
task review summary --project TASK --pr 618 --allow-unlinked # 連携なしプロジェクトで使う
# 読み取りの視界を明示する(既定は現在の連携先)。連携を差し替えたあとに旧リポジトリの
# ラウンドを読む、連携を張る前のラウンド(空文字)を読む、のどちらにも使う
task review list --project TASK --pr 618 --repo acme/old
task review rounds --project TASK --pr 618 --repo ""
task review list --project TASK --pr 618 --repo org/app --host-url https://forgejo.example.com
task review rounds --project TASK --pull-request-id 00000000-0000-0000-0000-000000000000
投入 JSON と絞り込みの値は送信前に CLI 側でも検証する。綴り違い
(severity: "critical"、--state closed)や必須項目の欠落は、どの指摘の
どの項目かを添えて終了コード 2 で弾く。サーバー側の検証に任せきりにすると、
AI が生成した JSON の取り違えを直す手がかりが薄くなる。
読み取りの 3 コマンド(list / rounds / summary)は --repo に加えて
--host / --host-url / --pull-request-id を受ける。API のリポジトリ絞り込み(§5)を
CLI からも使えるようにするためで、これが無いと AI レビュワーの主経路から過去の連携先の
ラウンドへ到達できない。--repo owner/name --pr N だけの指定は候補が一意なときだけ許可し、
複数候補なら終了コード 2 以外の非 0 で失敗して host または PR ID の再指定を要求する。
値は owner/name、空文字は連携を張る前のラウンドを指す。形式が違えば終了コード 2 で弾く
(黙って現在の連携先へ落とすと、読めていないことに気づけない)。
summary は次のいずれかで非 0 終了する。ゲートとして使う以上、判断できない
ときは通さない(fail-closed)。
照合する HEAD は --head があればそれ、無ければ実行ディレクトリの
git rev-parse HEAD。GitHub へ取りに行かないのは、CI でも手元でも
「いま検査している木」の SHA がそこにあるからで、余計な依存と権限を増やさない。
submit の JSON スキーマ(例)
{
"pr": 618,
"head_sha": "60cdd7795f94fa4e4148ce996c2efb4c363e3f5e",
"summary": "総評。実装は整合。ストーリーのセレクタに 1 件。",
"findings": [
{
"severity": "medium",
"title": "LastAdminGuard の findByRole が複数一致で必ず失敗する",
"body": "説明文にもマッチするため…(再現条件・根拠)",
"file": "apps/frontend/stories/components/MembersSection.stories.ts",
"line": 257
}
]
}
指摘ゼロ(findings: [])のラウンドも正当(「指摘なし」の記録として意味を持つ)。
head_sha は 40 桁の小文字 16 進で書く。git log --oneline が見せる短縮 SHA を入れると、
鮮度の照合が厳密一致のため、そのラウンドは指摘を全部解消しても「レビュー後に更新あり」の
まま抜けられなくなる。CLI(終了コード 2)と API(400)の両方で弾く。
7. GitHub 要約コメント
-
投稿主体は GitHub App(既存連携の installation token)。個人の PAT に依存しない
-
投稿先はラウンドに控えたリポジトリ(
repo_owner/repo_name)。現在の連携先を 使うと、連携を差し替えたあとに旧リポジトリ向けの指摘を新リポジトリへ書き込む。 控えたリポジトリが現在の連携先と違う、または連携が無いラウンドは投稿しない -
集計の範囲・合流の鍵・投稿先・installation token は、連携行を 1 回だけ引いて そこから作る。同じジョブの中で連携を引き直すと、その間に差し替えが入ったときに 「範囲は旧リポジトリ、投稿先は新リポジトリ」になり、上の規則をすり抜けて旧リポジトリの 指摘が新リポジトリの同番号 PR へ載る
-
1 PR × 1 プロジェクトに 1 本。HTML マーカーにプロジェクト ID を含める (例:
<!-- koyori-review-summary:{project_id} -->)。 マーカーを固定文字列にすると、同じリポジトリへ 2 つのプロジェクトが連携したときに 互いのコメントを自分のものと誤認して交互に上書きし、一方に未解決の High があるのに 他方が「マージ可」を出し続ける(連携は 1 プロジェクト = 1 リポジトリだが、逆向きは 制約が無く、同じリポジトリを複数プロジェクトが見られる)。 合流とロックの鍵も project を含む(後述)ので、系統ごとに 1 本にすると単位が揃う -
コメントの特定はマーカーだけに頼らない。探索時は「マーカー一致」かつ 「作成者が自分(この GitHub App の bot)」の両方を必須にする。マーカーは PR の 参加者なら誰でも本文に書けるので、マーカーだけで特定すると第三者が先取りできる—— App は他人のコメントを編集できないため更新は失敗し続け、失敗はベストエフォートで 握り潰されるので、正式な要約が永久に作られない。第三者の同一マーカーは無視して 新規作成する
-
一度投稿できたら
comment_idを控え、以後は探索せずに直接更新する。 探索はあくまで初回と、控えを失ったときの復旧手段 -
控えを捨ててよいのは、編集が 404(コメントが存在しない)を返したときだけ。 権限エラー・レート制限・5xx・通信断では控えを保持し、次回の更新に任せる。 「更新に失敗したら作り直す」と実装すると、一時障害のたびに新しいコメントが増える—— 古い方は誰も更新しなくなって「マージ可」と書かれたまま PR に残り、 この節の「1 PR × 1 プロジェクトに 1 本」が崩れる。恒久的な不在(404)と 一時的な失敗を分けるのは、投稿失敗をベストエフォートで握り潰す設計 (後述)と対になる規則
-
内容: ラウンド数(R1, R2, …)と最新ラウンドの総評、重大度 × 状態の件数表、マージ可否、 オーナー代行での棄却件数(あれば)、task の指摘一覧へのリンク、最終更新時刻
-
投稿時に HEAD の鮮度を見る。ジョブは PR メタ(既に取得している
GET /pulls/{number}の応答)から現在のhead.shaを読み、最新ラウンドのhead_shaと比較する。不一致なら「レビュー後に更新あり」としてマージ可を出さない。 PR メタが取れないときもマージ可を出さない(件数表は出す)。取得したhead.shaは 確認時刻とセットでラウンドへキャッシュし(pr_title/pr_authorと同じ流儀)、 画面が使う(§8) -
コメントは最終更新時刻の時点のスナップショット。push だけでは更新されない (更新の契機は task 側のラウンド作成と状態遷移だけ)。push で追従させるには webhook の push イベント処理が要る——受信基盤は既存(issues の取り込みで使用中) なので、その追補として別仕様で扱う
-
更新契機: ラウンドの作成時と指摘の状態遷移時(apalis ジョブで非同期。ペイロードに 機微情報を載せない既存規約に従う)
-
ジョブは投入時に見ていたリポジトリをペイロードに持つ(リポジトリ名は機微情報では ないので載せてよい)。印とロックの鍵は投入側がラウンドの控えたリポジトリで作るので、 実行側が現在の連携先で引き直すと別のキーを消しに行き、元の印が TTL のあいだ残る。 その間そのリポジトリの遷移は合流で捨てられ、コメントが古いまま止まる (A で遷移 → 実行前に A→B→A と戻す、で踏める)。実行時に連携先が変わっていたら、 投入時のリポジトリの印だけを落として投稿せずに降りる。新しい連携先ぶんの更新は、 そちらの遷移が自前で積む
-
同一 (project, リポジトリ, pr) の更新要求は1 本に合流させる。要求ごとにジョブを積むと、20 件の 指摘を順に
fixedにしただけで同じコメントへ 20 回書き込みに行き、GitHub の secondary rate limit に当たる。投入前に「更新待ち」の印を立て、既に立っていれば積まない。 印を落とすのはロックを取れたジョブ、取れずに積み直すジョブ、投入時から連携先が 変わっていて降りるジョブだけで、最初のものは最新状態を読む前に落とす。 合流された更新も次の 1 回に必ず含まれる。印にはTTL を置く。ジョブが再試行を使い切って死ぬと 印だけが残り、「立っていれば積まない」規則で以後の遷移が 1 件も積まれなくなるため (TTL が切れれば次の遷移で自然に再開する) -
印の TTL は投入からジョブがそれを落とすまで(キューでの滞留 + ロック待ち)の 最悪ケースより長く取る。ロックの TTL とは基準が違うので、同じ値を流用しない。 短いとジョブが落とす前に印が切れ、以後の遷移がそれぞれ別のジョブを積む。 合流が効かなくなって連続書き込みが戻るうえ、外からは「合流しているつもりで 効いていない」状態が見えない。長いと、ジョブが終端失敗したあとの凍結が その分だけ延びる。なお、ロックを握ったまま死んだジョブがいる間は、印が切れて 再投入されてもロックで弾かれるだけなので、印をロックの TTL より長くする必要はない
-
合流に加えて、同一 (project, リポジトリ, pr) のジョブ実行そのものを直列化する。合流は ジョブの本数を減らすだけで、同時に走ることは止められない。並行して走ると、 先に古い状態を読んだジョブの書き込みが後から着き、コメントが巻き戻ったまま 次の遷移まで直らない。ロックを取れなかったジョブは、印を落として少し後ろへ 積み直し、自分は正常終了する(積み直した側が最新状態を読み直す)。 「自分の番ではない」をジョブの失敗にしてはいけない。apalis の既定の再試行には バックオフが無く、数ミリ秒で試行回数を使い切って終端する。そのとき印だけが残り、 生きたジョブが 1 本も無いまま、印の TTL のあいだ以降の遷移が合流で捨てられる。 積み直しは回数で打ち切らない。持ち主が落ちてロックが TTL で失効するまで待てるのが 目的で、打ち切ると要約が古いまま止まる。待ち時間はキューを見に行く刻みより長く 取る(待たずに積み直すと空振りを繰り返す)
-
ロックはランダムなトークンを値に置き、TTL 付きで取る。解放はそのトークンが 現在値と一致するときだけ行う(既存の
service::github::import_lockと同じ形)。 TTL はワーカーが投稿中に落ちた場合の保険で、無いとその PR の要約を誰も更新 できなくなる。トークンの照合が無いと、TTL 超過後に別のジョブが取り直した ロックを、遅れて完走した古いジョブが解放してしまう。 TTL はジョブ 1 回ぶんの GitHub API 往復(トークン取得・PR メタ・コメント探索・ 投稿)が最悪ケースで収まる長さにする。短すぎると保持中に期限が切れて、 塞いだはずの並行書き込みが戻る -
GitHub 連携の無いプロジェクトでは投稿をスキップする(起票・管理は可能)
-
投稿・編集の失敗はベストエフォート(ログに残すが API は成功させる)
-
マーカー探索は先頭 5 ページ(100 件 × 5)まで。要約は初回に作られるので実際には 1 ページ目で見つかる。見つからなければ新規投稿になり、次回以降はその新しい方が 更新され続ける
-
PR メタの取得に失敗しても要約は投稿する(PR 番号だけで用は足りる)
8. 画面
備えるもの:
- ラウンドの起票(人間レビュワー向け): 対象 PR と head SHA を指定し、指摘
(重大度 / title / body / file:line)を下書きに 1 件ずつ追加して、まとめて確定する。
確定までサーバーには何も作られない。指摘ゼロでの確定(指摘なしの記録)も可。
head SHA は 40 桁の小文字 16 進かを送信前に見る(CLI と同じ。
git log --onelineの 短縮 SHA を貼るのは人間のほうが起こりやすく、サーバーの 400 では何桁必要か伝わらない) - PR 単位の指摘一覧: 重大度・状態・ラウンドでフィルタ。各指摘は title / file:line / 本文 / 遷移履歴を持つ
- 状態遷移の操作: fixed / verified / deferred / rejected に加えて、戻り遷移
(fixed → open の差し戻し / deferred → open / rejected → open)。役割制約あり
(自分の修正を自分で verified にできない。High / Medium には deferred を出さない。
rejected は指摘を出した本人にだけ出す。ただし作成者がテナントの利用者でなくなった
ラウンド(
reviewer_left_tenant)では、テナントオーナーにも出す(§3 の代行)。 fixed → verified / open はレビュー側——その指摘のラウンド以降のラウンドを出した人に だけ出す)。§3 の表と 1:1 で対応させる -
マージ可否の即答: リポジトリ未確定 / 未レビュー / マージ不可(残数つき)/ レビューが古い / 鮮度不明 / マージ可 の 6 つを出し分ける(§5 と同じ規則 + キャッシュ済みの現在 head との比較)。判定は片道降格——「可」以外へ落とす方向にだけ 使い、「可」を保証には使わない
-
「リポジトリ未確定」: GitHub 連携が無い。ゲートとして扱ってよい表示ではない—— 連携を外すと集計の視界が空になり、空のラウンド 1 本で「可」を作れるため。 CLI だけで塞いでも、人間の主経路である画面が素通しならゲートの半分が無いのと同じ
-
「レビューが古い」: キャッシュ済みの現在 head(§7 でジョブが控えたもの)と 最新ラウンドの
head_shaが不一致。可を出さない -
「鮮度不明」: キャッシュが無い(ジョブが一度も走っていない・メタ取得に失敗した)。 可を出さない
-
「マージ可」には GitHub を最後に確認した時刻を必ず併記する。キャッシュは push では更新されない(§7)ので、「一致 = いま新鮮」の保証にはならない。 権威のゲートはあくまで CLI の
--head照合と branch protection
-
- 判断の材料を並べる: レビューした commit(手元の HEAD と見比べれば、レビュー後に 積まれたコミットに気づける)、集計対象のリポジトリ(空を含む)、 オーナー代行での棄却件数(あれば)、GitHub の確認時刻
- ラウンドの履歴: 同じ PR に何ラウンド(R1, R2, …)レビューが走り、どの head を見たか
- 繰り延べの行き先: deferred → リンクされた通常タスクへジャンプ
- プロジェクト横断の集計: 溜まっている deferred(Low/Nit)の件数と一覧 — 未実装 (プロジェクト単位の一覧のみ。テナント横断のエンドポイントが要る)
遷移規則(どの操作を出すか・押せるか)は lib/review-findings.ts に置き、backend の
service::reviews と同じ表を持つ。押しても 409 / 403 になるボタンを出さないためで、
片方だけ変えると「押せるのに失敗する」ボタンができるので両方直すこと。
canTransition / canDefer / requiresFindingAuthor / requiresReviewerSide は
4 つとも findingActions から使う。定義しただけで呼び忘れると、その規則ぶんだけ
「押すと 403」のボタンが戻る。
ページは <ReviewFindingsView :key="projectId"> で作り直す。vike-vue はクライアント遷移で
同じ +Page.vue に解決される URL 間ではコンポーネントを patch するため、これが無いと
クエリの引数も選択中の PR も前のプロジェクトのまま残る(設定画面と同じ扱い)。
9. 範囲外(今回やらない)
10. 受け入れ条件(テスト観点)
-
正常系: 一括起票 → 一覧 → fixed → verified が通り、GitHub に要約コメントが 1 本だけ作られ、状態遷移で同じコメントが更新される
-
指摘ゼロのラウンド、同一 PR への 2 ラウンド目(R2 として新しいラウンドになる)、
findingsが境界を越える件数 -
ラウンドの採番: 同じ PR へ同時にラウンドを確定しても
roundが重複しない -
リポジトリの同定: 連携を別リポジトリへ差し替えると、同じ PR 番号でも R1 から始まり、 旧リポジトリのラウンドは一覧・集計に混ざらない。要約コメントは控えたリポジトリが 現在の連携先と違えば投稿しない
-
ホスト移行時の履歴選択: GitHub と Forgejo に同じ
org/appの PR #10 があるとき、--repo org/app --pr 10またはrepo=org/app&pr=10だけの指定は 409 / CLI 非 0 になり、host_url(必要ならhost)またはpull_request_idを指定した場合だけ対象が確定する。 候補が 1 件の repo-only 指定は成功する。--pull-request-id X/pull_request_id=Xと併記したrepo/host/host_urlのいずれかが X の行と一致しなければ API は 400、CLI は非 0 になる -
マージ可否: レビューが 1 件も無い PR は「可」にならない(対照: 指摘ゼロの ラウンドが 1 件あれば「可」)。集計は最新ラウンドの
head_shaを返す -
CLI の鮮度検査: 最新ラウンドの
head_shaと照合対象の HEAD が違えば非 0 終了。 HEAD が決まらないときも非 0(--no-head-checkを付けたときだけ 0) -
CLI のリポジトリ検査: 連携の無いプロジェクトでは非 0 終了(
--allow-unlinkedを 付けたときだけ 0)。連携を外して視界を空にし、空のラウンド 1 本で「可」を作る手が通らない -
取り下げの代行: 作成者がテナントの利用者でなくなった指摘は、テナントオーナーが
rejectedにできる(対照: 作成者が在籍しているうちは、オーナーでも代行できない) -
代行の痕跡: 除名 → 代行で棄却 → 再招待を通しても、集計と要約コメントに 「オーナー代行での棄却 n 件」が残り、遷移履歴に代行者と時刻が残る。 同じ指摘を
rejected → open → rejectedと往復させても件数は 1 のまま (数えるのは指摘であって遷移ではない) -
拒否系: スコープ不足 403 / 他テナント・他プロジェクト 404 / fixed 宣言者本人による verified の拒否(対照: 別レビュワーなら成功)/
rejectedからopen以外への遷移 (rejected → openは作成者なら通る)/ PR 番号が 0 以下 -
取り下げの主体: 他人が出した指摘は、空のラウンドを確定して「レビュー側」を 名乗っても
rejectedにできない(対照: 出した本人は取り下げられる。 同じ空ラウンドの作成者でもverifiedは行える) -
繰り延べ: deferred で通常タスクが同プロジェクトに起票されリンクされる。 タスク起票に失敗したとき deferred へ遷移しない(不整合を作らない)。 同じ指摘へ同時に
deferredを送っても、有効なタスクは 1 件だけ作られる (負けた側は 409。指摘の行ロックで直列化されるため)。 deferred → open で自動起票タスクがクローズされる。既にクローズ済み・削除済みなら 冪等成功として遷移を通す(自動起票タスクは普段のタスク運用に乗るので、人が先に 片付けていることがある。ここで失敗にすると指摘が deferred から戻せなくなる) -
繰り延べの重大度制約: High / Medium を deferred にしようとすると拒否され、 マージ可否が「可」に変わらない(対照: Low / Nit は成功し、通常タスクが起票される)
-
繰り延べの往復: deferred → open → deferred を繰り返しても自動起票タスクは 1 件のまま (同じ
deferred_task_idが再オープンされ、新しいタスクを作らない。 対照: リンク先のタスクを削除してから再繰り延べすると、代替が 1 件だけ起票され リンクが差し替わる——削除済みタスクが復活しない) -
戻り遷移: fixed → open の差し戻しはレビュー側のみ(fixed 宣言者本人は 403、 対照: 別レビュワーは成功)。verified からの遷移はすべて拒否(終端)
-
要約コメント: 連携なしプロジェクトでは投稿だけスキップして起票は成功。 投稿失敗は起票を巻き戻さずログに残る
-
要約コメントの鮮度: 投稿時点の PR head が最新ラウンドの
head_shaと違えば 「マージ可」と書かれない(対照: 一致すれば可)。PR メタの取得に失敗したときも 「マージ可」と書かれない(件数表は出る) -
要約コメントの持ち主: 同じリポジトリへ 2 つのプロジェクトが連携している PR では、 それぞれのコメントを自分のマーカーで見分け、互いを上書きしない
-
マーカーの先取り: 第三者が同じマーカーを含むコメントを先に投稿していても、 それを要約コメントと誤認せず(作成者が自分でないため)、自分のコメントを 新規に作成して以後はそれを更新する
-
控えの寿命: 一時的な失敗(レート制限・5xx・通信断)のあと再実行しても 要約コメントは 1 本のまま(新しいコメントを作らない)。 対照: コメントが手で削除されていれば(編集が 404)作り直す
-
画面のゲート表示: 連携の無いプロジェクトでは「マージ可」を出さず、 リポジトリ未確定として示す(対照: 連携があってラウンド 1 件・未解決 0 なら「可」)
-
画面の鮮度表示: キャッシュ済みの現在 head と最新ラウンドの
head_shaが不一致なら 「レビューが古い」、キャッシュが無ければ「鮮度不明」で、どちらも「可」を出さない。 一致なら「可」に GitHub の確認時刻が併記される -
要約コメントの合流: 連続した状態遷移でも更新ジョブは 1 本に合流し (印の TTL が短すぎればここで落ちる)、コメントは 1 本のまま最新の件数・ マージ可否になる
-
要約コメントの直列化: 同じ PR の更新ジョブが走っている間、後続のジョブは 投稿せずに再試行へ回る(古い状態での上書きを起こさない)
-
担い手が消えたときの復帰: ジョブが再試行を使い切っても、印の TTL 経過後の 状態遷移で更新が再開する(コメントが恒久的に凍結しない)。 期限切れのロックは別のジョブが取り直せて、古いジョブはそれを解放しない
11. 決定事項ログ
- 2026-08-26: 範囲は「運用ルール + 指摘のタスク管理 + GitHub 要約投稿」。自動修正ループは範囲外
- 2026-08-26: データモデルは専用エンティティ(reviews / review_findings)。将来の自動化が 機械可読な状態・集計を要求するため、既存タスク + カスタムフィールドの相乗りは不採用
- 2026-08-26: 重大度は High / Medium / Low / Nit の 4 段階。マージ必須は High / Medium
- 2026-08-26: 状態は open / fixed / verified / deferred / rejected。verified / rejected は レビュー側のみ、fixed 宣言者本人は不可
- 2026-08-26: 起票はレビュワーが CLI から JSON 一括投入。GitHub 経由の取り込みはしない
- 2026-08-26: 置き場所はプロジェクト。GitHub 連携は要約コメントの投稿にだけ必要で、 連携が無くても起票・管理はできる(投稿だけスキップする。§7 / §10)。 deferred は通常タスクへ自動変換
- 2026-08-26: GitHub へは App(bot)名義の要約コメント 1 本のみ。インライン投稿は禁止
- 2026-08-26: スコープは read:review / write:review を新設
- 2026-08-26: 指摘一覧は
review-findingsを独立したパスに置く(CLI のreview listと UI の一覧が「ラウンド単位」ではなく「PR 単位」で引くため) - 2026-08-26: 差し戻し後も
fixed_byを残す(差し戻した本人が同じ指摘を verified に 進めるのを防ぐ) - 2026-08-26: 指摘一覧への導線 URL は既存の
email_verification_app_url(アプリの公開 URL)を 流用する。要約コメント専用の設定は増やさない - 2026-08-26: CLI は投入 JSON と絞り込みの値を送信前に検証する(終了コード 2)。
review summaryは未解決が残ると終了コード 1 - 2026-08-26: 画面は
/{tenant}/projects/{key}/reviews。PR 一覧のためにGET .../reviews/pull-requests(集計つき)を追加した - 2026-09-01: PR 一覧は可否を断定しない(
mergeableを返さず、バッジは 「未解決なし / N 件が未解決」)。可否には鮮度と連携の有無も要り、一覧にmergeableを置くと、詳細パネルが「リポジトリ未確定」等へ降格させた横で 一覧だけ「マージ可」と出る矛盾が起きた - 2026-09-01: ラウンドは作成者の不在(
reviewer_left_tenant)を返す。§3 の オーナー代行は画面(人間の主経路)から使えないと意味が半分になるが、在籍状態を 返さないと画面が代行の条件を組めなかった。オーナーはtenant_members行を 持たないので、この判定では不在に数えない - 2026-08-26: 絞り込みは画面側で適用する(API は PR 単位で全件返す)。指摘は 1 PR あたり 高々数十件で、往復を増やす価値がない
- 2026-08-31: 読み取りの 3 コマンドに
--repoを置く(owner/name、空文字は連携前)。 API 側の絞り込み(§5)だけでは、CLI を主経路にする AI レビュワーが過去の連携先の ラウンドを読めない - 2026-09-07: レビュー履歴の PR 選択に
host/host_url/pull_request_idを追加し、repo + prだけは候補が一意な場合に限定する。GitHub と Forgejo の同名リポジトリ・同番 PR は API 409 / CLI 非 0 で再指定を要求する - 2026-09-09:
pull_request_idは候補探索を省略するが、併記されたrepo/host/host_urlは 対象行との一致を検証し、不一致を API 400 / CLI 非 0 で拒否する(レビュー指摘: Git ホスティング↔ タスク連携仕様では不一致を拒否する一方、本仕様は他の指定を無視するように読めた) - 2026-08-26: レビューの反復の呼称は「ラウンド」(表示は R1, R2, …)。「巡」表記は使わない
- 2026-08-26: レビュワーは AI(PAT + CLI)と人間(セッション + Web UI)を同格に扱う。 ラウンドは確定時一括作成・追記不可で、人間の下書きは UI 側の関心事(サーバーは持たない)
- 2026-08-26: モック評価を受けて戻り遷移(fixed→open 差し戻し / deferred→open / rejected→open)を追加。deferred から戻すとき自動起票タスクはシステムが自動クローズ。verified は終端
- 2026-08-26: ラウンドは出した時点の連携とリポジトリ(
integration_id/repo_owner/repo_name)を控える。連携先は差し替えられるので、project_id + pr_numberだけでは 別リポジトリの同番 PR を同じ PR として続けてしまうため。採番・一覧・集計・要約の 合流はリポジトリを含めた単位。連携解除では指摘を消さない(integration_idが NULL に なるだけ。取り込み Issue のリンクとは扱いが違う) - 2026-08-26: マージ可否は「ラウンドが 1 件以上」かつ「open / fixed の High・Medium が 0」。
件数だけだと未レビューの PR が 0 件で「可」になる。HEAD の鮮度は集計が返す
head_shaを CLI が手元の HEAD と突き合わせて見る(読み取り API から GitHub を 呼ばない。ゲートの可用性を GitHub に握らせないため) - 2026-08-26: マージ可否のゲートは「ラウンド作成者の誠実性」を信頼境界とする。自己レビューの
機械的な禁止は PR 作者と利用者の同定が要るため範囲外にし、branch protection で担保する。
一方、連携を外して集計の視界を空にする手は塞ぐ——集計はどのリポジトリを見たかを返し、
CLI は確定しないときに通さない(
--allow-unlinkedで明示的に外せる) - 2026-08-26: 取り下げは、作成者がテナントの利用者でなくなった場合に限り テナントオーナーが代行できる。除名・退会で取り下げる主体が消えると、監査記録に 嘘(直していないものを verified)を書かせることになるため
- 2026-08-28: テナントオーナーからはゲートを守らないと決めた。代行の条件(作成者の 不在)はオーナー自身が除名で作れるが、条件を絞っても防ぎきれない(アカウント削除に 限ると元の問題が解決せず、「不在が N 日」は除名時刻の記録という新しい仕組みを要求して 攻撃を遅らせるだけ)。オーナーは連携解除もメンバー管理もできる上位の主体なので、 そこだけ守れるふりをしない。代わりに痕跡を残す——代行での棄却件数を集計と 要約コメントに常設する
- 2026-08-28: 連携なしのゲート拒否を画面にも要求する。CLI にだけ実装すると、 人間の主経路(§1)が素通しになる。マージ可否の表示は「リポジトリ未確定」 「レビューが古い」「鮮度不明」を含む 6 値(§8)
- 2026-08-28: 要約コメントのマーカーにプロジェクト ID を含め、原則を 「1 PR × 1 プロジェクトに 1 本」へ改める。同一リポジトリへの複数連携は禁止しない (UNIQUE の置き場が無く、グローバルに張るとテナントを跨いだ連携妨害になる)
- 2026-08-28: 読み取り API にリポジトリの絞り込みを用意する。「履歴として残る」と 書きながら読む手段が無いと、旧リポジトリ・未連携時代の指摘へ到達できない
- 2026-08-28: 再繰り延べは既存の自動起票タスクを再オープンして使い回す(
deferred_task_idを保持)。毎回起票すると往復のたびにタスクが増える。不変条件は「常に同じ物理タスク」 ではなく**「同時に存在する有効なタスクは 1 件」**——リンク先が削除済みなら代替を 1 件起票する(削除はソフトデリートで FK が外れないため、生死はリンク先のdeleted_atで判定する。「同じ物理タスク」に固定すると、削除済みタスクとの往復が 成立しなくなる) - 2026-08-28: 要約コメントの特定は「マーカー一致 + 作成者が自分の bot」の両方を必須にし、
投稿後は
comment_idを控えて直接更新する(控えを捨てるのは編集が 404 のときだけ。 一時障害で捨てるとコメントが増える)。マーカーだけだと PR の参加者が先取りでき、 App は他人のコメントを編集できないため要約が永久に作られなくなる - 2026-08-28: 状態遷移は指摘の行ロック(
SELECT … FOR UPDATE相当)で直列化し、 副作用(繰り延べタスクの起票・クローズ・再オープン)とリンク・状態の更新を 同一トランザクションで行う。並行のdeferredで孤児タスクができるのを防ぐ - 2026-08-28: GitHub 要約コメントも投稿時に HEAD の鮮度を見る(PR メタの
head.shaと 最新ラウンドのhead_shaの比較。追加の API 呼び出しは無い)。不一致・メタ取得失敗では 「マージ可」と書かない。画面は要約ジョブが控えたキャッシュで「レビューが古い/鮮度不明」 へ片道降格し、「可」には確認時刻を併記する——キャッシュは push で更新されないため、 一致を保証に使わない。push での追従(webhook の push イベント処理。受信基盤は既存)は 別仕様 - 2026-08-26:
rejectedの主体は「その指摘を出したラウンドの作成者」に限る。 「より新しいラウンドの作成者」まで広げると、指摘ゼロのラウンドを 1 本作るだけで レビュー側を自称でき、他人の High を棄却してマージ基準を単独で迂回できるため。verifiedと差し戻しは再レビューの判定そのものなので、後続ラウンドの作成者にも認める - 2026-08-26: 繰り延べは Low / Nit に限る(High / Medium の deferred は 409)。マージ可否の 集計から外れる状態への遷移をレビュー側以外にも開くと、マージ基準そのものを迂回できるため
- 2026-08-26: ラウンドの採番は
UNIQUE (project_id, repo_owner, repo_name, pr_number, round)- 行ロックで直列化する(連携が無いラウンドの
repo_owner/repo_nameは空文字列)
- 行ロックで直列化する(連携が無いラウンドの
- 2026-08-26: 要約更新ジョブは同一 (project, リポジトリ, pr) で合流させる(GitHub の secondary rate limit 対策)。 あわせて実行区間も同じ単位で直列化する(並行実行だと古い状態の書き込みが後から着き、 コメントが巻き戻る)。直列化は Redis のロックで行う——advisory lock だと GitHub API 呼び出しのあいだ Postgres のトランザクションを開いたままにする必要があるため。 ロック・印とも TTL 付きにし、ロックの解放はトークン照合つき(担い手が落ちても詰まらせない)
- 2026-08-26: PR メタはタイトル・作者のみキャッシュ(行数は鮮度が落ちるため持たない)。マージ基準のゲートは設定化せず固定(High+Medium 必須・fixed は未解決扱い)。snippet 専用フィールドは作らず body の markdown コードブロックで表現
- 2026-08-31:
head_shaは 40 桁の小文字 16 進に限る(CLI は終了コード 2、API は 400)。 鮮度の照合は厳密一致なので、短縮 SHA を受け取るとそのラウンドは永久にマージ可へ届かず、 しかも「同じ commit に見えるのに再レビューを要求される」形で出て原因を辿れないため - 2026-09-02: PR を実体表(
forge_pull_requests)へ切り出し、reviewsは FK で参照する方針を Git ホスティング↔タスク連携 側に置いた。レビュー指摘 → PR → タスクと辿るため。実装は同仕様の分割 1 で行う