レビュー指摘管理 仕様書

PR レビューの指摘を task で追跡し、GitHub には要約コメント 1 本だけを置く仕組みと運用ルール

ステータス: Draft / 作成日: 2026-08-26 依存: GitHub 連携(github_integrations、1 プロジェクト = 1 リポジトリ)、GitHub App、PAT スコープ


1. 背景と課題

開発フローは「AI がコードを書く → 別の作業者(AI または人間)がレビュー → 修正 → 再レビュー」 を回しているが、次の 2 つが遅さと散らかりの原因になっている。 レビュワーは AI と人間のどちらもあり得る。本仕様の全機能は両者を同格に扱う (AI は PAT + CLI、人間はセッション + Web UI が主経路。モデル・権限規則は共通)。

  1. Low までマージ前に完璧に直している。 優先度の低い指摘の修正までマージの前提に なっており、1 機能のリードタイムが不必要に伸びる。

  2. GitHub が指摘のデータ置き場になっている。 インラインコメントを 1 指摘 = 1 スレッドで 積むと PR ページが重くなり、見通しも悪い。実例が #587 で、スレッド 28 本すべてが 1 コメントのみ(返信ゼロ)、resolved は 16/28 で管理が途中で崩れている。 議論の場としても状態管理としても機能していない。 かといって Low を GitHub Issue に逃すと Issue 一覧が散らかる。

本仕様は、レビュー指摘を task 自身のデータとして管理し(ドッグフーディング)、 GitHub 側には bot の要約コメント 1 本だけを置く形に切り替える。

将来の「AI がレビューを検知して自動修正するループ」は本仕様の範囲外だが、 その土台(機械可読な指摘・状態・集計)になるようにモデルを設計する(§9)。


2. 運用ルール

コードを書く前に決まる規約。task と vrt の両リポジトリに適用する。

ルール 内容
マージ基準 High / Medium はマージ前に必須。Low / Nit は繰り延べ可(deferred → 通常タスク化して後日対応)
インライン禁止 GitHub の PR へインラインレビューコメントを投稿しない。PR に置くのは bot の要約コメント 1 本のみ
権威の所在 指摘の一覧・状態の唯一の権威は task。GitHub のスレッド resolved フラグは使わない・見ない
再レビュー 依頼ごとに 1 ラウンド。判定は verified(解消)/ 据え置き / 未対応 で、指摘の状態として記録する
ゲートの信頼境界 マージ可否はラウンド作成者の誠実性を前提にする。ラウンドは write:review を持つ誰でも、指摘ゼロでも確定できるので、修正した本人が空のラウンドを 1 本出せば「レビュー済み・指摘なし・HEAD 一致」を自力で満たせる。これを task 側では防げない(GitHub の PR 作者と task の利用者を突き合わせる仕組みが無い)。自己レビューの禁止は GitHub の branch protection(必須レビュワー)で担保し、本ゲートはそこに積む二段目として使う
オーナーも境界の内側 テナントオーナーからはゲートを守らない。オーナーは連携の解除も除名も行えるので、取り下げの代行条件(作成者の不在。§3)を自分で作れる——除名 → 代行で棄却 → 再招待、で他人の High を単独で消せる。これを条件の絞り込みで防ごうとしても、オーナーの権限そのものが上位にあるので防ぎきれない。防ぐ代わりに痕跡を残す: 代行での棄却は件数を集計と要約コメントに常設し(§5 / §7)、誰がいつ代行したかは遷移履歴に残る
未連携の PR 空間 連携の無いプロジェクトで扱ってよい PR は1 リポジトリ相当まで。ラウンドの控えが空になり(§3)、PR 番号だけがキーになるため、2 つのリポジトリの PR #10 をどちらもレビューすると同じ PR として続いてしまう

3. 概念モデル

project(GitHub 連携は要約コメントの投稿にだけ必要。無くても起票・管理はできる)
└── reviews(レビューラウンド: 1 PR への 1 回のレビュー)
    └── review_findings(指摘)

reviews(レビューラウンド)

項目 内容
project_id 対象プロジェクト
integration_id / repo_owner / repo_name ラウンドを出した時点の GitHub 連携と、その連携先リポジトリの控え。連携が無ければ integration_id は NULL、repo_owner / repo_name は空文字列(NULL にすると Postgres の UNIQUE が NULL 同士を別物として扱い、採番の防波堤が効かない)
pr_number PR 番号
round PR 内の連番(1 始まり)。表示は R1, R2, …
head_sha レビュー時点の PR head(裏取りした commit の記録)。40 桁の小文字 16 進のみ受け付ける(短縮 SHA は §5 参照)
reviewer_id ラウンドを作成した利用者(PAT の持ち主。AI もこの利用者として動く)
summary 総評(markdown)
pr_title / pr_author 表示用の PR メタ。要約コメント投稿ジョブが GitHub から取得してキャッシュする(連携なし・取得失敗時は空のままで PR 番号だけ表示)。増減行数など鮮度が落ちる数値は持たない

予定(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(指摘)

項目 内容
review_id 属するラウンド
severity high / medium / low / nit
title 1 行の要約
body 詳細(markdown。再現条件・根拠を書く)
file / line 位置情報(任意。インラインコメントの代替はこのテキスト情報で足りる)
state open / fixed / verified / deferred / rejected
deferred_task_id 繰り延べ時に自動起票した通常タスクへのリンク(任意)

状態遷移

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(概要)

パスは既存規約どおりテナント・プロジェクト配下に置く。

メソッドとパス 動作
POST /v1/tenants/{t}/projects/{p}/reviews ラウンド + 指摘の一括作成(1 リクエスト)。成功後に GitHub 要約更新をジョブ投入
GET /v1/tenants/{t}/projects/{p}/reviews?pr=618 ラウンドの一覧(指摘の件数つき)
GET /v1/tenants/{t}/projects/{p}/reviews/{id} ラウンドの詳細(指摘含む)
GET /v1/tenants/{t}/projects/{p}/reviews/pull-requests レビューのある PR の一覧(ラウンド数・未解決数・塞いでいる件数)。可否は返さない——可否には鮮度と連携の有無も要り(§8 の 6 値)、この一覧はその材料を持たない。画面の PR 一覧が使う
GET /v1/tenants/{t}/projects/{p}/reviews/summary?pr=618 PR 単位の集計: 重大度 × 状態の件数、ラウンド数、最新ラウンドの head_sha、集計対象のリポジトリ、オーナー代行での棄却件数、「マージ可否」
GET /v1/tenants/{t}/projects/{p}/review-findings?pr=618&state=&severity= PR の指摘一覧(状態・重大度で絞り込み)。CLI の review list と UI の一覧が使う
PATCH /v1/tenants/{t}/projects/{p}/review-findings/{id} 状態遷移(state と任意のコメント)

上表の 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)。

終了する条件 理由
High / Medium が open か fixed で残っている §2 のマージ基準
ラウンドが 1 件も無い レビューされていない。「指摘なし」とは違う
最新ラウンドの head_sha が照合対象の HEAD と違う レビュー後にコミットが積まれている
照合する HEAD が決まらない 判断できないので通さない(--no-head-check で明示的に外せる)
集計対象のリポジトリが確定しない(連携が無い) どのリポジトリの PR を見た集計か決まらない。連携を外すと視界が空になり、空のラウンド 1 本で「可」を作れてしまう(--allow-unlinked で明示的に外せる)

照合する 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. 画面

項目 値
URL /{tenant}/projects/{key}/reviews(?pr=618 で PR を指定。要約コメントのリンク先)
ページファイル apps/frontend/src/pages/@tenant/projects/@projectKey/reviews/+Page.vue
本体 components/reviews/ReviewFindingsView.vue と ReviewRoundComposer.vue

備えるもの:

  • ラウンドの起票(人間レビュワー向け): 対象 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. 範囲外(今回やらない)

項目 理由・メモ
AI 自動修正ループ(検知 → 修正 → 報告) 別仕様。導入時は ラウンド上限(自動往復 1 ラウンドまで)/ 重大度ゲート(High の自動修正は人間必須)/ 収束判定(同一箇所への再指摘で停止) の 3 ガードレールを必須とする。本モデルのラウンド・状態・集計はその前提を満たす
GitHub インラインコメントの取り込み・同期 resolved 状態の権威が二重になる沼を避ける。#587 型の過去データも移行しない
deferred のプロジェクト横断ビュー §8 のとおり画面はプロジェクト単位。テナント横断の集計エンドポイントを足してから
自己レビューの機械的な禁止 GitHub の PR 作者と task の利用者を同定する仕組み(利用者への GitHub アカウント紐付け)が要る。本仕様ではゲートの信頼境界として明記するに留め(§2)、禁止は branch protection の必須レビュワーで担保する
リポジトリ改名への追従 ラウンドの控えは owner / name の文字列で、github_integrations も数値の repository id を持たない。GitHub 側でリポジトリや org を改名して再連携すると、同じリポジトリなのに別リポジトリ扱いになり、旧ラウンドが一覧・集計の既定の視界から外れる(§5 のリポジトリ絞り込みで読むことはできる)。追従するには id 列の追加と連携フローの改修が要るため、別の変更として扱う
ラウンドの取り消し(void) 誤った PR 番号へ出したラウンドを消す手段は用意しない。ラウンドは「どの head を見た時点の判断か」の記録で、追記不可・削除不可がモデルの根幹。誤投の実害は指摘を rejected へ落とせば消え、ラウンドそのものは「そういうレビューがあった」記録として正しい

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 で行う