code-review について
code-review は、プロジェクトの規約に沿って変更コードを確認するレビュー用スキルです。Python、TypeScript、API設計、ミドルウェア、HTTP通信などの基準を扱います。
主な機能
- 01Python のスタイルと型の確認
- 02Pydantic モデルの要件
- 03TypeScript とモバイルレイアウト規則
- 04API とページネーションの規約
- 05ASGI ミドルウェアの指針
- 06非同期 HTTP クライアント規則
活用シーン
- 01マージ前の変更コードのレビュー
- 02APIエンドポイントの規約確認
- 03非同期 HTTP 通信の監査
- 04コントロールプレーンのスマートフォン表示評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ vectorize-io/ hindsight --skill code-reviewインストール後、Claude Code で/code-reviewを入力して読み込まれたか確認します。
詳細
コードレビュー
プロジェクトの品質基準とコーディング規約に照らして、変更されたコードをすべてレビューします。
コード標準
コードを書く前に、これらの標準を読み、内容を理解してください。以下のレビュー手順では、準拠状況を確認します。
サポート対象のインタープリター
CPython 3.11 が基準です。これは docker/standalone/Dockerfile に含まれ、.python-version の固定バージョンと uv.lock が解決するバージョンです。このパッケージは 3.11 から 3.14 をサポートしています。CI の build-api-python-versions は各バージョンをインストールしてスモークテストを実施するため、これらのインタープリターのいずれかを除外する依存関係の最低バージョンは、後回しにできる問題ではなく、互換性を損なう変更です。
Python のスタイル
- Python 3.11 以降を使用し、型ヒントを必須とします。上記の「サポート対象のインタープリター」も参照してください
- 全体を非同期にします(asyncpg、非同期 FastAPI)
- リクエスト/レスポンスには Pydantic モデルを使用します
- リントには Ruff を使用します(行の長さは 120)
- プロジェクトのルートに Python ファイルを置かず、ディレクトリ構成を整理します
- 複数項目を持つタプルを戻り値に使わないでください。内部関数やプライベート関数であっても同様です。必ずデータクラスまたは Pydantic モデルを使用してください。例外はありません。「値が2つだけだから」といった近道も認められません。関数が複数の値を返す場合は、それらをまとめた名前付きの型を定義してください。
Pydantic モデルによる型安全性
構造化データに生の dict 型を使わないでください。内部ヘルパーやプライベート関数を含む、すべてのコードに適用されます。dict のキーが既知の場合は、データクラスまたは Pydantic モデルにしてください。
- 関数間で受け渡すすべてのデータ構造には、Pydantic の
BaseModelを使用します - Pydantic の検証が不要な軽量な内部データコンテナには、
@dataclassを使用します - 型変換(例:日時がタイムゾーン対応であることの保証)には
@field_validatorを追加します dict.get()を使うパターンは避け、型付きモデルの属性を使用します- 外部データ(JSON、API レスポンス)は境界で Pydantic モデルにパースします
- これにより、ビジネスロジックの深い箇所ではなく、パース時に型エラーを検出できます
dictの使用が許容されるのは、真に動的/未知のキーを扱う場合(例:任意のメタデータ、固定スキーマのない JSON ブロブ)のみです
# BAD - error-prone dict access
def process(data: dict) -> str:
return data.get("name", "") # No validation, silent failures
# GOOD - typed and validated
class UserData(BaseModel):
name: str
created_at: datetime
@field_validator("created_at", mode="before")
@classmethod
def ensure_tz_aware(cls, v):
if isinstance(v, str):
v = datetime.fromisoformat(v.replace("Z", "+00:00"))
if v.tzinfo is None:
return v.replace(tzinfo=timezone.utc)
return v
def process(data: UserData) -> str:
return data.name # Type-safe, validated at construction
TypeScript のスタイル
- コントロールプレーンには Next.js App Router を使用します
- shadcn/ui コンポーネントと Tailwind CSS を使用します
スマートフォン(幅 390px)でのコントロールプレーン
globals.css では、ページの横スクロール防止、タッチ画面でのホバー専用コントロールの表示、iOS でフォームフィールドを拡大表示させない設定がすでに適用されています。以下のチェック項目は、共通ルールでは対応できない部分です。
- ページ内のサブタブには、独自実装のボタン列ではなく
components/ui/underline-tabs.tsxを使用します。 - フィルター、トグル、ボタンを横に並べる場合は、折り返し(
flex-wrap)を使うか、列の内側でスクロールさせます(max-w-full overflow-x-autoを指定し、子要素にはshrink-0 whitespace-nowrapを指定します)。 - スマートフォン向けの代替表示(
w-full sm:w-[420px]、w-[min(420px,calc(100vw-1rem))])なしに、約 340px を超える固定幅を使わないでください。横並びのペインはmd未満で縦に積み重ねます。 - すべてのテーブルで
components/ui/table.tsxを使用します。このコンポーネントはテーブルを専用の枠内でスクロール可能にし、スマートフォンでも列がつぶれないよう、最低幅を 40rem に保ちます。 - グリッドレイアウトのフォームセクションには
min-w-0が必要です(form-layout.tsxの共通Sectionには設定済みです)。横並びの入力欄はsm未満で縦に積み重ねます。 - 全高レイアウトには
h-screen/100vhではなくh-dvh/100dvhを使用します(そうしないと iOS では URL バーの下に画面下部が隠れます)。
コードコメント
- 重要な技術的判断には、その選択の理由を必ずコメントしてください。「なぜこの方法なのか」と尋ねられそうな場合は、説明するコメントが必要です。
- コメントは変更の経緯に合わせて最新の状態に保ってください。実装方針を変更する場合は、以前の方法を試した理由と変更した理由を説明するよう、コメントも更新します。コメントは、問題があった可能性の高い過去の実装を記録する役割も果たします。
- 自明なコードにはコメントを付けないでください。「なぜそうするのか」がコードから明らかでない場合にのみコメントします。
# BAD - no context for future readers
results = await asyncio.gather(*tasks, return_exceptions=True)
# GOOD - explains the non-obvious choice
# Use return_exceptions=True to avoid cancelling sibling tasks on failure.
# Previously we used TaskGroup but it cancelled all tasks when one failed,
# causing partial writes that left orphaned entity links (see #412).
results = await asyncio.gather(*tasks, return_exceptions=True)
API レイヤーとデータアクセス
api/http.py(およびその他の API ルーター)ではデータベースに直接アクセスしません。 HTTP ハンドラーは、SQL を組み立てたり、acquire_with_retry/conn.fetch/conn.fetchrow/conn.executeを呼び出したり、fq_table(...)を参照したりしてはいけません。永続化とクエリはすべてMemoryEngine(エンジンレイヤー)に存在します。ハンドラーはリクエストを解析・検証し、エンジンメソッドを呼び出し、HTTP レスポンスを整形し、ドメインの結果をステータスコードにマッピングします(例:Noneの返却 → 404)。- 認証/テナンシーはハンドラーで前提とされるのではなく、各エンジンメソッドの内部で強制されます。 銀行スコープのデータに触れるすべてのエンジンメソッドは、
request_contextを介して認証を行う必要があります — 通常はawait self._authenticate_tenant(request_context)(多くの場合、get_bank_profile(...)を介して間接的に)— これにより、クエリが実行される前に正しいテナントスキーマが解決されます。ハンドラーはrequest_contextをエンジンメソッドまで引き渡さなければなりません。スキーマがすでに設定されていると仮定して、テナントスコープのテーブルをクエリしないでください。 - エンジンメソッドは生の dict ではなく、型付きモデル(Pydantic/dataclass)を返します(「型安全性」を参照)。
- すべてのリストエンドポイントは、既存のものに倣ってページネーションを行います。 データ量に応じてサイズが増大するコレクション(banks、documents、memories、entities、operations、webhook deliveries、audit logs など)を返す
GETは、limit/offsetを受け取り、結果を上限付きにしなければなりません — 上限のないリストは、上限のないペイロードに加えて、上限のない行ごとの処理(項目ごとのカウント、設定の解決、埋め込みのハイドレーション)を意味します。list_documentsが使っている形をそのまま踏襲し、新しい形を考案しないでください: ハンドラーにlimit: int = Query(default=100, ge=0)とoffset: int = Query(default=0, ge=0)を付け、エンジンメソッドに対応するキーワード引数を用意し、レスポンスにはページに加えてtotal、limit、offsetを含めて、クライアントがいつ停止すべきかを把握できるようにします。コレクションが UI でユーザーが選択する対象である場合は、q検索パラメータを追加してください — クライアント側のフィルタリングでは、読み込まれたページしか見えません。構造的に上限が保証されているエンドポイントは例外であり、原則ではありません: 設計上ツリー全体・エクスポート全体となるもの、あるいは書き込み時に上限が設定されるテーブル(例:observation_history/mental_model_history、挿入時に*_max_entriesに切り詰められる)です。上限がない場合は、ページネーションを行ってください。
HTTP ミドルウェア
BaseHTTPMiddlewareを決して追加しないでください — つまり、@app.middleware("http")(このデコレーターはこれをインストールします)も、add_middleware(SomethingSubclassingBaseHTTPMiddleware)も禁止です。Starlette はそのようなミドルウェアごとに、ダウンストリームのアプリを子タスクで実行し、リクエストとレスポンスを anyio のメモリオブジェクトストリームのペアを通じて流します。これにはリクエストごとにタスクの生成と複数回のスケジューリングホップのコストがかかり、そのコストはスケジューリングであるため、イベントループがすでに逼迫しているまさにそのときに増大します。API からこの 2 つを削除したところ、同じ同時実行数で/health/liveが約 2.4k から約 7.9k rps に、p99 が約 107ms から約 18ms になりました(#4235)。また、その下にあるすべてに対してRequest.is_disconnected()を壊し(http.disconnectイベントがルートに到達しません)(#2122)、BackgroundTask/ ストリーミングのセマンティクスを微妙な形で飲み込んでしまいます。- 代わりにピュア ASGI ミドルウェアを書いてください:
__init__(self, app)とasync def __call__(self, scope, receive, send)を持つクラスで、scope["type"] != "http"の場合はそのまま通過させ、レスポンスを観察する必要がある場合はsendをラップします(http.response.startメッセージからステータスを読み取り、message["headers"]に生のバイトペアとしてヘッダーを追加します)。同じタスク内でのawaitが 1 回だけで、ホップはありません。hindsight_api/api/observability.pyとapi/disconnect.pyがコピーすべきモデルです。 - ルーティングコンテキストが必要な場合は、処理を 1 つ下の層に移すことを優先してください。 どのエンドポイントがヒットしたかを知る必要があるもの — そのパラメーター、シグネチャ、ボディモデル — は、
app.routesをたどってroute.matches()を呼び出してルートを再導出するミドルウェアではなく、APIRouteサブクラス(app.router.route_class = ...)に属します。そこではルートはすでに解決されており、ルートごとの情報はリクエストごとではなく起動時に一度だけ計算できます。api/unknown_params.pyを参照してください。include_routerはアプリのroute_classをルーター自身のルートには適用しないことに注意してください — FastAPI <= 0.140 では各ソースルートのクラスが維持され、>= 0.141 ではソースルーターのroute_classから具体化されます — そのため、インクルードされた/拡張ルーターからのルートには、インクルードの前にuse_unknown_params_routes(router)が必要です。 - クライアント入力から構築されたヘッダー値は、
message["headers"]に到達する前にサニタイズする必要があります: クエリパラメーターとボディフィールドの名前はパーセントデコードされた攻撃者入力であるため、非 latin-1 の名前はsendの途中で例外を発生させ(タイプミスによる 500)、CR/LF を含む名前はレスポンスを分割します。
アウトバウンド HTTP: aiohttp、非同期のみ
- 本番コードでは HTTP 呼び出しに aiohttp を使用し、httpx は決して使用しません。 httpx の非同期クライアントはリクエストあたりのコストが aiohttp よりも明らかに大きく(httpcore プール、anyio レイヤー)、そのオーバーヘッドは私たちが持つ最もホットなパス — retain と recall のたびに実行される埋め込み、リランキング、LLM 呼び出し — にのしかかります。クライアントは
hindsight_api/engine/aiohttp_session.pyで構築してください:LoopLocalSession(イベントループごとに 1 つのClientSessionを遅延生成 —__init__や、別スレッドのasyncio.run配下で実行されうるinitialize()の中でセッションを生成しないでください)、per_phase_timeout(httpx スタイルのフェーズごとのタイムアウト。aiohttp のtotalでは長いストリーミングボディが途中で切られてしまいます)、そしてraise_for_status(UpstreamHTTPErrorを送出し、これはボディと、remote_retryが分類に使用するstatus_codeを保持します)。 - 本番コードでは同期 HTTP は禁止です —
requests、urllib.request、urllib3、http.client、httpx.Client、および SDK に非同期版がある場合の同期 SDK クライアント(openai.OpenAI、cohere.Client、litellm.embeddingなど)は使用しないでください。同期呼び出しはイベントループをブロックするか、リクエストごとにスレッドを必要とします。代わりにループ上で非同期版を使用してください。同期呼び出しをasyncio.to_thread/run_in_executorでラップすることは解決策にはなりません。唯一認められる例外は、非同期 API をまったく提供しない SDK(例:google.authの認証情報リフレッシュ)です — これはスレッドで実行し、その理由をコメントに記述してください。 - ruff によって強制されます(
hindsight-api-slim/pyproject.tomlのTID251、banned-api)。import httpxは、それに依存して構築されたサードパーティ SDK を設定または分類する場合にのみ許可されます(OpenAI/Anthropic SDK 向けのllm_transport.build_sdk_timeout、それらのエラーを分類するremote_retry)。その際は# noqa: TID251と SDK 名を記したコメントを付けます。それ以外のnoqa: TID251は拒否してください。http_probe.pyは唯一のファイル単位の例外です(標準ライブラリのみのレディネスプローブであり、独自のプロセスです)。 - テストは対象外です。 FastAPI の
TestClient/httpx.ASGITransportは問題ありません。aiohttp クライアント向けに上流をスタブするには、セッションをモックするのではなくtests/aiohttp_stub.py::stub_serverから提供してください。
クエリにおけるバンク/テナント分離
- バンク分離はハードなセキュリティ不変条件です。どのクエリも、他のバンクの行を読み取り、カウント、更新、削除してはなりません。 テナント分離はスキーマレベルで強制されます(解決された
search_path/fq_table(...)修飾子であり、_authenticate_tenantによってゲートされます)。バンク分離は、マルチバンクテーブルに触れるすべてのステートメントにおけるbank_id述語によって、スキーマ内で強制されます。 - マルチバンクテーブルに対するすべての SQL ステートメントは、
bank_idによって制約されなければなりません —WHERE内で直接、または推移的に(以下を参照)。マルチバンクテーブルはbank_idカラムを持ちます:memory_units、documents、entities、mental_models、knowledge_pages、memory_links、observation_historyなどです。 - 罠:
bank_idなしで、呼び出し元が指定したグローバルに一意でないキーでフィルタリングすること。document_idやmental_models.idのようなキーはバンクごとにのみ一意です(その PK は複合で、例えば(id, bank_id)です)。そのため、同じ id がすべてのバンクに合法的に存在します。UPDATE memory_units SET tags = $1 WHERE document_id = $2のようなステートメントは —bank_idがなく — その id を共有するすべてのバンクの行を暗黙的に読み取り/書き込みします。これが #3429/#3430 の正確な欠陥です。AND bank_id = $nを追加すると修正されます。 - ステートメントが正当にスコープされる 3 つの方法(これらは受け入れ、どれにも当てはまらないものは指摘してください):
- 明示的
WHERE ... AND bank_id = $n。 - グローバルに一意な単一カラム PK。 グローバル uuid PK(
memory_units.id、entities.id、knowledge_pages.id)またはバンクエンコードされたキー(chunks.chunk_id、engine/chunk_ids.pyによって構築)でフィルタリングすることは、バンク間で衝突しません。チャンク id が単射であるのは、そのヘルパーが各コンポーネント内の区切り文字をエスケープしているからにすぎないことに注意してください — それが置き換えた単純な{bank_id}_{document_id}_{idx}の連結では、('a', 'b_c')と('a_b', 'c')が同じ行を指し示すことができました(#4244)。そのため、それ以前に書き込まれた id はバンクスコープとして扱うのは安全ではありません。複合 PK の id(documents.id/document_id、mental_models.id)と対比してください — これらは危険であり、bank_idを必ず伴わなければなりません。 - 推移的。
bank_idカラムを持たないジャンクションテーブル(unit_entities、entity_cooccurrences、observation_sources)は、同じ呼び出し内でバンクスコープのクエリから選択された、それ自体がグローバルに一意な unit/entity id を通じて到達される場合にのみ安全であり、エッジは構造上バンク内です。id セットに他のバンクの id が含まれる可能性がある場合、それはスコープされていません。
- 明示的
- 2 つの兆候に注意してください: (a)
WHEREで使用される呼び出し元指定の id に隣接するbank_idがなく、一方で同じメソッド内の隣接するステートメントにはbank_idが伴っている場合(非対称性が手がかりです)、(b)bank_id述語がif bank_id:の下でのみ適用され、bank_id: str | None = Noneがデフォルトになっている場合 — 現在のすべての呼び出し元が 1 つを渡していたとしても潜在的な問題です。 - 設計上のクロスバンクでは
bank_idを宛先に書き換える必要があります。 転送/インポート経路は、正当にバンクを越える唯一のものです。すべての書き込みが宛先のbank_idを固定し、ソース行のbank_idを継承しないことを確認してください。
データベースロック
- PostgreSQL アドバイザリロックは決して使用しないでください(
pg_advisory_lock、pg_try_advisory_lock、pg_advisory_xact_lock、pg_advisory_unlockなど)。マイグレーション、エンジンコード、その他どこであっても同様です。Hindsight はコネクションプーラーやマネージド/PG 互換サービスに対して実行され、そこではアドバイザリロックは信頼できないか、サポートされていません。セッションレベルのロックは、プーラーがセッションを別のクライアントに引き渡したときに、黙ってリークしたり消えたりしますし、呼び出し元はサーバーが決して付与しないロックの上で永遠にブロックする可能性があります。「安全」に見えるもの(トランザクションスコープであるため)も含め、新たな使用箇所はすべて却下してください。 hindsight_api/migrations.pyに既存の使用箇所がありますが、これは既得権として容認されているものであり、前例ではありません — 削除するために追跡されています。これをコピーしないでください。- ロックで囲うのではなく、並行性を設計で排除してください。各プロセスに書き込み用の独自のオブジェクトを持たせる(例: 共有の
public.オブジェクトではなくスキーマごとの DDL)、操作を冪等にする、あるいは実際の行/テーブル制約を使用する(INSERT ... ON CONFLICT、固定された順序でのSELECT ... FOR UPDATE)といった方法があります。pg_advisory_xact_lockに手を伸ばし、差し戻さざるを得なかったマイグレーションについては #2690 を参照してください。
並行性: asyncio と threading プリミティブ
Hindsight プロセスはスレッド(エグゼキュータ、asyncio.to_thread)を実行し、またテストやツーリング全体では複数のイベントループを実行します。以下のルールが破られても静かなものです — コードはテストに合格し、負荷がかかると失敗します。
どのロックを使うか。 これはスタイルではなく、所有権の問題です:
asyncio.Lock/Semaphore/Event/Condition/Futureは、それらを最初に待機したイベントループにバインドされます。 これらは単一のループが所有するオブジェクトにのみ使用してください — リクエストごと、コネクションごと、あるいは 1 つのループが作成しそのループだけが触れるオブジェクトにぶら下がる状態です。RuntimeError: ... is bound to a different event loopは、このルールが破られていることを示します。- 複数のループから到達可能なものには
threading.Lockを使用します: モジュールレベルのシングルトン、プロセス全体のキャッシュ、レジストリです。これはループに依存しません。await を含まないクリティカルセクションのみを保護できます —awaitをまたいで保持してはいけません。そうすると、処理を譲るのではなく、ループ全体をブロックしてしまいます。
クリティカルセクションが await する必要があり、かつ オブジェクトがプロセスグローバルである場合、それはロックの問題ではなく設計の問題です: 代わりに状態をループごとにスコープしてください。
インポート時に asyncio プリミティブを作成しないでください。 モジュールレベルの asyncio.Lock()/Semaphore() は、どのループも存在する前に構築され、その後すべてのループで共有されます。また、1 つのプロセス内に 2 つのループが現れた瞬間(テスト、スレッド内の asyncio.run)に、単一 ループのビルドでも失敗します。それらを所有するオブジェクトの内部で遅延的に構築してください。
合流する future はループごとです。 並行するロードを asyncio.Future の背後で重複排除するキャッシュは、その処理中マップを (running loop, key) でキー付けしなければなりません。キャッシュされた データ をループ間で共有するのは問題なく、望ましいことです。future を共有するのはそうではありません。
モジュールレベルの可変状態。 複数のスレッドから到達可能なプロセスグローバルの dict/set/list にはガードが必要であり、別のスレッドが変更する可能性がある間に反復してはいけません(最良でも RuntimeError: dictionary changed size during iteration、サイズ変更中の dict に C 拡張が借用参照を渡された場合はセグフォルトする可能性があります)。ホットパスをロックするよりも、ロックの下で一度だけウォームアップすることを優先してください。
ドキュメントのコード例
hindsight-docs/docs/内で Hindsight を呼び出すすべてのコード例は、テスト済みのサンプルファイルから取得する必要があり、インラインのフェンス付きブロックを使用してはいけません。 つまり、SDK 呼び出し (Python、Node.js、Go)、hindsight ...CLI コマンド、および/v1/...に対するcurlが対象です。コードはhindsight-docs/examples/api/<page>.{py,mjs,sh,go}に# [docs:<section>]/# [/docs:<section>]マーカー (JS/Go では//) で囲んで配置し、<CodeSnippet code={...} section="<section>" language="..." />で表示してください (docs/developer/api/memory-banks.mdxを参照)。CI はすべてのサンプルファイルを稼働中のサーバーに対して実行するため (scripts/test-doc-examples.sh)、メソッドやフラグの名前を変更すると、ドキュメントが黙って壊れるのではなくビルドが失敗します。- その呼び出しを SDK または CLI がサポートするすべての言語についてタブを用意してください。ページ内の類似セクションと同様にします。スニペットが必要な
.mdページは.mdxにします。 - インラインのままで問題ないもの: インストールコマンド、env/設定のスニペット、エンドポイントのシグネチャ行 (
GET /v1/...)、JSON リクエスト/レスポンスの形式、および CI では実行できないインフラを必要とするコマンド (組み込みサーバーの起動、管理用 DB コマンド、FUSE マウント)。
ブランチの衛生管理
- 新しい機能ブランチは常に
origin/mainから開始してください — クリーンなベースを確保するためにリベースします。 - PR/ブランチ/機能に関連するコミットのみを含めてください — 無関係な変更は含めません。ブランチに属さないコミットが含まれている場合は、マージ前に削除する必要があります。
一般原則
- 求められた範囲を超えて機能を追加したり、コードをリファクタリングしたり、「改善」を加えたりしない
- 起こり得ないシナリオに対する不要なエラー処理を追加しない
- 一度きりの操作のためにヘルパーや抽象化を作成しない
- 後方互換性のためのハックは行わない (未使用の変数、再エクスポート、「removed」のコメント)
- 似たようなコードを 3 行書くほうが、早すぎる抽象化よりも良い
レビューの手順
1. ブランチの衛生管理を確認する
git log --oneline main..HEADを実行して、ブランチ上のすべてのコミットを一覧表示します。- すべてのコミットが機能/PR に関連していることを確認します。無関係なコミットがあれば指摘します。
- ブランチが最近の
origin/mainに基づいていることを確認します (古いベースになっていないこと)。
2. 変更されたファイルを特定する
git diff --name-only HEAD (ステージングされていない変更) と git diff --cached --name-only (ステージング済みの変更) を実行して、変更されたすべてのファイルを取得します。ローカルに変更がない場合は、ベースブランチとの差分を git diff main...HEAD --name-only および git diff main...HEAD で取得し、現在のブランチ上のすべてのコミットをレビューします。
3. リンターを実行する
./scripts/hooks/lint.sh
失敗があれば報告してください。自分で修正しないでください — 報告のみです。
4. デッドコードを確認する
変更された各 Python ファイルについて、次を確認します:
- 未使用のインポート (Ruff が検出するはずですが、確認してください)
- 追加されたがどこからも呼び出されていない関数/メソッド/クラス
- 代入されたが一度も読まれていない変数
- 削除すべきコメントアウトされたコードブロック
変更された各 TypeScript ファイルについて、次を確認します:
- 未使用のインポート
- 未使用の変数または関数
- コメントアウトされたコード
5. 型安全性を確認する (Python)
変更された各 Python ファイルについて、違反がないか確認します:
- 構造化データに生の
dictを使わない — 内部/プライベート関数であっても Pydantic モデルまたは dataclass を使用する必要があります (唯一の例外: 真に動的/不明なキー) - 複数項目のタプル戻り値を使わない — 内部/プライベート関数であっても dataclass または Pydantic モデルを使用する必要があります (例外なし)
- 関数のパラメータと戻り値の型における型ヒントの欠落
- タイムゾーン対応であるべき datetime フィールドにおける
@field_validatorの欠落
6. 欠落しているテストを確認する
新規または大幅に変更された関数/エンドポイント/クラスごとに、以下を確認します。
- 対応するテストの追加または更新があるかどうかを確認する
- 新しい API エンドポイントには、統合テストが必須です
- 新しいユーティリティ関数には、単体テストが必須です
- バグ修正には、回帰テストがあることが望ましいです(SHOULD)
テストカバレッジを欠いている新しいロジックがあれば指摘してください。
LLM の挙動の変更には、MockLLM ではなく実際の LLM によるジャッジテストが必要です。 その変更がモデルのプロンプト解釈の仕方を変える場合 — 事実/観察の抽出、fact_type(world/experience)の分類、発話者の帰属、指示追従、プロンプトの文言 — には、実際のパイプラインを実行し、文字列/enum の照合ではなく tests.llm_judge.assert_meets_criteria でアサートする pytest.mark.hs_llm_core を付けたテストが必須です。以下のものを指摘事項として報告してください。
- プロンプト/分類の変更が MockLLM または文字列アサーションのみで検証されている(MockLLM は入力をエコーするため、そのようなテストは誤ってパスします)。修正すべきです。
fact_type == "world"/"experience"(またはその他のモデルが決定する出力)をジャッジせずにハードアサートしているテスト — 非決定的であり、プロバイダーや実行ごとにフレークします。修正すべきです(分類のチェックをジャッジのcriteriaに移し、真に決定的な構造的アサートのみを直接行うようにします)。- 決定的なメカニズム(プロンプトの組み立て、抑制/分岐ロジック)が遅い LLM テストのみでカバーされている — これらには高速な非 LLM の単体テストも必要です。注記。
完全なパターンについては、CLAUDE.md → Key Conventions → Testing を参照してください。
6a. テストが生の SQL ではなくエンジン API を介してメモリ状態をアサートしていることを確認する
テストは、retain / recall / consolidation が何を生成したかを、公開されている MemoryEngine の読み取り API — list_memory_units(ユニットとその metadata / tags、total によるカウント、document_id / fact_type / entity_id フィルター)、list_entities(正規名、言及数)、get_graph_data(ノード/エッジ)、get_bank_stats、recall_async — を呼び出して検証しなければならず、pool.acquire() / conn.fetch* を介した生の SQL でメモリテーブル(memory_units、memory_links、unit_entities)に直接アクセスして検証してはいけません。これらのテーブルに対してアサートすることは、テストをストレージ層の詳細に結合させ、観測可能なプロパティではなくプロキシをチェックすることになります(General Principles → テストはプロパティをアサートする、および 7b のハンドラーのルールを参照してください)。
追加または変更されたテストで、エンジンの読み取りメソッドが同じ事実を返すにもかかわらず、memory_units / memory_links / unit_entities に対して SELECT / COUNT を実行してアサートしているものは、修正すべきとして指摘してください。明白な兆候:テスト本体の中で async with pool.acquire() as conn: の後に SELECT ... FROM memory_units が続くもの、list_memory_units の ["total"] が返すはずの fetchval("SELECT count(*) FROM memory_units ...")、list_entities がカバーする canonical_name クエリ。
これらのテーブルに対する直接 SQL が正当化されるのは、公開 API では表現できない内部状態を強制または検査する場合のみです — 例えば、競合を捏造する UPDATE documents SET updated_at、または重複排除された get_graph_data のエッジリストでは再現できない生の memory_links の行数などです。これらには、直接アクセスが必要な理由を説明するコメントを必ず付けてください。付けていないものは指摘してください。
6b. ユーザー向け機能にシステムテストがあることを確認する
hindsight-system-tests/ には、公開されている Python クライアントを通じて実際の hindsight-api プロセスを駆動するブラックボックスのストーリーが置かれています。エンジンへのアクセスも、SQL も、内部インポートもありません。これらが存在するのは、約 500 ファイルの api-slim スイートがメカニズムごとに 1 つのテストという構成であり、メカニズムのバグは捕捉できてもコンポジションのバグを見逃すためです。すなわち、統合がその下のファクトを消し去ること、差分リフレッシュが遡及ウィンドウを取りこぼすこと、転送がメンタルモデルの証拠を落とすこと、再処理が黙って何もしないことです。これらのいずれも、単一メカニズムのテストではまたがらない複数のステップに及んでいました。追跡 issue: #4214。
変更にシステムストーリーが必要となるのは、それがユーザーが名前を挙げられる機能を追加する場合、または既存の 2 つの機能を初めて組み合わせる場合です。具体的には、次のような PR を should fix としてフラグを立てます:
- 観測可能な挙動を変える API エンドポイント、retain/recall/reflect パラメーター、またはバンク設定フィールドを追加または変更する。
- 新しい派生レイヤーやライフサイクルステップ (観測の種類、リフレッシュトリガー、バックグラウンド操作) を追加する。
- 既存の機能を別の機能と初めて相互作用させる — 新しい組み合わせこそが、ユニットスイートには見えないものです。
- コンポジションのバグを修正する。リグレッションテストは api-slim だけでなくここに置くべきです。なぜなら、そのバグはステップ間の継ぎ目に潜んでいたからです。
次の場合には必要ありません: 観測可能な変化を伴わない内部リファクタリング、パフォーマンス作業、既存のストーリーですでにカバーされているメカニズム、またはその唯一の表面がコントロールプレーンであるもの (このスイートは設計上 API のみです)。
追加または変更されたストーリーをレビューする際は、次を確認してください:
- クライアントを通っていること。
httpxでもMemoryEngineでもなく、client.aretain(...)です。公開クライアントを迂回してアクセスすると、このスイートが明らかにするために存在する SDK の欠陥が隠されます —import_bank_templateはあらゆる SDK から呼び出し不能で (#4232)、クライアント駆動のテストだけがそれを示せました。ストーリーが自らを通すためにクライアントを迂回している場合は Must fix です。 - すべての LLM 呼び出しが宣言されていること。 スクリプト化されていない呼び出しは、貼り付けるべきルールとともにテストを失敗させます。もっともらしいデフォルトで応答するストーリーは何も証明しません。見逃しを黙らせるために catch-all ルールを決して追加しないでください。
- バックグラウンド処理が無効化されず、待機されていること。 Retain は統合をエンキューします。決定論性を保つためにそれをオフにするストーリーは、コンポジションのバグが潜む半分を取り除いてしまっています。
settled(bank_id)を使用してください。 - キーワードの存在ではなく、決定論的なペイロード全体をアサートしていること。 LLM、embedder、reranker がすべてスタブ化されていれば、recall は入力の純粋関数です — ランキング、スコア、レンダリングされたテキストはすべて固定でき、「その単語がどこかに現れる」は、融合が反転したときでも同じように通ってしまいます。
- 既知の欠陥が文書化されるのではなく、失敗すること。 PR が契約を満たさないままにする場合、ストーリーは私たちが望む挙動をアサートし、それが修正されるまでレッドになります — 実行をグリーンに保ち、誰も問題を提起しなくなる
xfailでもなく、今日の誤った答えを固定し、誰かが修正した日に壊れて次の読者にテストを削除するよう教えるテストでもありません。どちらの形でも Must fix です。
スイートの README に規約が記載されています。tests/test_01_retain_and_recall.py が、アサーションがどれほど網羅的であるべきかの参照です。
7. API の一貫性を確認する
hindsight-api-slim/hindsight_api/api/ 内のファイルが変更された場合:
- OpenAPI 仕様は再生成されましたか? (
./scripts/generate-openapi.sh) - クライアント SDK は再生成されましたか? (
./scripts/generate-clients.sh) - コントロールプレーンのプロキシルートは更新されましたか? (
hindsight-control-plane/src/app/api/)
7a. TS/Python ラッパークライアントのパリティを確認する
生成された SDK のうち 2 つは、自動生成された低レベルクライアントの上に手書きで保守されている便利なラッパーを同梱しています。そしてそれはこの 2 つだけです:
- TypeScript:
hindsight-clients/typescript/src/index.ts(HindsightClient) - Python:
hindsight-clients/python/hindsight_client/hindsight_client.py(Hindsight)
(Rust/Go などのクライアントは生成のみで、同期を保つべきラッパーはありません。)
これらのラッパーは、ほとんどのサードパーティ利用者が実際に呼び出すものであり、同じサーフェスを公開する必要があります。**ある変更が一方のラッパーのメソッドに触れる場合 — パラメータの追加/削除、デフォルト値の変更、新しいクエリ/ボディフィールドの転送 — は、もう一方のラッパーの同等のメソッドにも同じ変更を同じ PR (または直後の PR) で加える必要があります。**生成された SDK には存在するパラメータが、一方のラッパーで欠落していると、その言語のすべての利用者に対して黙って取り除かれてしまいます (これはまさに #2975 / #3042 が listMentalModels/getMentalModel の detail/tags_match/limit/offset に対して修正した内容です)。修正すべき — 一方の言語では機能が追加されているのにもう一方では追加されていないラッパーメソッドがあれば指摘し、両側に対応するマッピングのリグレッションテストを追加してください。
注: client-coverage-check CI ツールはリクエストボディのフィールドのみを検証し、GET のクエリパラメータは検証しません。そのため、クエリパラメータのパリティのギャップは自動的には検出されず、ここで手作業で確認する必要があります。
7b. API レイヤーのデータアクセス境界を確認する
hindsight-api-slim/hindsight_api/api/ 内の変更された各ハンドラー (例: http.py、mcp.py) について:
- ハンドラー内での直接的な DB アクセスを指摘してください —
acquire_with_retry、conn.fetch/fetchrow/execute、生の SQL 文字列、またはfq_table(...)。これらは必須の修正です。クエリは型付きモデルを返すMemoryEngineのメソッドに移動し、ハンドラーはそのメソッドを呼び出す必要があります。 - 認証がエンジンで強制されていることを確認してください — ハンドラーは、
request_contextを介して認証するエンジンメソッド (通常はget_bank_profileを通じて_authenticate_tenant) に委譲する必要があります。認証を強制するエンジンメソッドを介さずにテナントスコープのデータを読み書きするハンドラーは必須の修正です (テナントデータがスキーマ間で漏洩する可能性があります)。
7c. バンク/テナントのクエリスコープを確認する
diff で追加または変更されたすべての SQL ステートメントについて (diff を conn.fetch、conn.fetchrow、conn.fetchval、conn.execute、executemany、および生の SELECT/INSERT/UPDATE/DELETE の f-string (複数行のものも含む) で grep してください)、それが別のバンクの行に触れないことを検証してください — 上記のクエリにおけるバンク/テナント分離を参照してください。
マルチバンクテーブル (memory_units、documents、entities、mental_models、knowledge_pages、memory_links、observation_history、…) に対する各ステートメントについて、次の 3 つの正当なメカニズムのいずれかでスコープされていることを確認してください:
- 明示的な
AND bank_id = $n; - グローバルに一意な単一カラム PK (
*.iduuid、またはバンクエンコードされたchunks.chunk_id) —documents.id/document_idやmental_models.idのような複合 PK の id ではありません; - 同じ呼び出し内でそれ自体がバンクスコープのクエリから選択された、グローバルに一意な id の集合を介した推移的なスコープ。
呼び出し元が指定した、グローバルに一意ではないキー (document_id、mental_models.id、エンティティ名など) でマルチバンクテーブルをフィルタリングしつつ、bank_id 述語がないステートメントは必須の修正として指摘してください — 指摘する前に、具体的な 2 バンクのシナリオ (2 つのバンクが id を共有しており、そのステートメントが誤ったバンクの行を読み取り/カウント/更新/削除する、または過剰に報告する) を構築して、それが実在することを確認してください。主な兆候: bank_id を持たないステートメントのすぐ隣にある bank_id を持つ兄弟ステートメント; デフォルトが None の if bank_id: でガードされた WHERE bank_id; 転送先を固定せずに転送元の bank_id を継承するインポート/転送の書き込み。
7d. リストエンドポイントのページネーションを確認する
追加または変更された、コレクションを返すすべての GET ハンドラーについて、limit/offset を受け取り total を返すことを確認します。正確な形については上記の API レイヤーとデータアクセス を参照してください。そのうえで、修正がエンドツーエンドで本当に機能しているかを確認します。誰も強制しないパラメータは、存在しないよりも有害です。
- 制限がレスポンスだけでなく実際の処理にまで届いていること。 ページサイズが本当に高コストな部分を制限しているか確認します。すなわち SQL の
LIMIT/OFFSET、または(list_banksのようにfilter_bank_list拡張フックが任意の bank を除外しうるため、プロセス内フィルターの後にページングを行う必要がある場合)ページ分に対してのみ実行される項目ごとの処理 — ライブストアのカウント、get_bank_configs、再埋め込み — を伴う明示的なスライスです。行を除外しうるフィルターの前に SQL でページングするのは 必ず修正 です。呼び出し元が参照できない行を数えた、不足したページや空のページ、そしてtotalを返してしまうからです。 - リポジトリ内のすべてのコンシューマーがページングすること。 新しいデフォルトの
limitは、以前はすべてを取得していた呼び出し元を黙って切り捨てます。コントロールプレーン(src/lib/api.ts+src/app/api/プロキシルート + リスト全体を保持する任意のコンテキスト/セレクター)、CLI(hindsight-cli/src/api.rs)、MCP ツール、および Zapier の動的ドロップダウンです。それぞれが完了するまでページングするか、UI でページングを公開する必要があります。デフォルトサイズの単一ページのままになっているコンシューマーがあれば指摘してください。 - 検索もそれに伴ってサーバーサイドへ移行すること。 リスト全体に対してクライアントサイドでフィルタリングしていたピッカーは、読み込まれたページだけをフィルタリングするようになります。エンドポイントが
qを取得したなら、UI はそれを送信する必要があります(そしてローカルフィルタリングを無効にします。例えば cmdk のshouldFilter={false})。取得していないなら、そのコレクションがそれを必要としないほど小さい理由を説明してください。 - 自身の行を検索するテストは、ページ 1 に到達することに依存してはなりません — 検索するか、明示的な
limitを渡すべきであり、デフォルトの並び順に頼ってはいけません。
8. コードコメントを確認する
自明でない変更それぞれについて:
- 新しく自明でないロジック — その理由を説明するコメントがありますか?
- 変更されたアプローチ — コメントに、以前何が行われていたか、なぜ変更されたかが含まれていますか?
- 古くなったコメント — 変更されたコードの近くにある既存のコメントは、まだ動作を正確に説明していますか?
9. 統合の完全性を確認する
hindsight-integrations/ 内のファイルが追加または変更された場合は、以下を確認してください。
- テストが存在する — 統合には、外部フレームワークをシミュレート/実行するテストが必要です(ヘルパーの純粋な単体テストだけでは不十分です)。意味のあるテストファイルを含む
tests/ディレクトリがあるか確認してください。 - CI ジョブが存在する —
.github/workflows/test.ymlに対応するtest-<name>-integrationジョブがあるか確認してください。存在しない場合は、指摘してください。 - リリースプロセス — 統合名が
scripts/release-integration.shのVALID_INTEGRATIONS配列に含まれていること、およびhindsight-dev/hindsight_dev/generate_changelog.pyのINTEGRATIONS辞書に含まれていることを確認してください(チェンジログジェネレーターは独自のリストを保持しており、そこに名前がないとチェンジログのステップでリリースが失敗します)。どちらかが欠けている場合は、指摘してください。 - ドキュメントギャラリー + サイドバーエントリ — 統合には
hindsight-docs/src/data/integrations.json内のエントリが必要です。このファイルは、統合ギャラリーとドキュメントサイドバーの両方を動かす唯一の信頼できる情報源です(サイドバーカテゴリは、レンダリング時にすべてのドキュメントバージョンにわたってこのファイルから注入されます)。エントリには内部リンク/sdks/integrations/<slug>のlinkと、それに対応するページhindsight-docs/docs-integrations/<slug>.md(x)が必要です。hindsight-docs/scripts/check-integrations.mjsビルドステップは両方向を強制します — 順方向: すべての内部 JSON エントリにドキュメントページがあること。逆方向: すべてのリリース済みタグ(integrations/<name>/vX.Y.Z)が JSON に含まれていること(cloudflare-oauth-proxyのようなプライベートインフラはスクリプトのEXCLUDEDセットに含まれています)。リリース済み(またはリリース中)であるのにintegrations.jsonに存在しない統合、およびドキュメントページのない JSON エントリを指摘してください。統合リンクを追加するためにversioned_sidebars/*.jsonを手動で編集しないでください — これらは JSON から埋められる位置ベースのプレースホルダーです。 - コード標準 — 統合コードは、すべての Python スタイルルール(型ヒント、生の辞書の禁止、タプルの戻り値の禁止など)に従う必要があります。
9a. 兄弟実装間のパリティを確認する
同じ機能が バリアント ごとに 1 回ずつ実装されている場合 — ハーネスごと、言語ごと、方言ごと、プロバイダーごとに 1 つずつ — 、新規または変更されたバリアントこそが、機能がひっそりと欠落する場所です。その欠陥は diff の中ではバグのようには見えません。間違っているコードとは そこに存在しない コードであり、既存のテストはすべて引き続きパスします。なぜなら、忘れた兄弟とは、定義上、誰もテストを書かなかったものだからです。これが、dsh がデーモンを一度も起動せずにデーモンモードで出荷された経緯です (#3524): ensureDaemon はフック専用ラッパーの中にあり、5 つの永続プラグインハーネスすべてでこれが欠けていました。
このリポジトリで既知の兄弟ファミリー (これは全リストではありません — このルールは 形 に関するものです):
| ファミリー | 場所 |
|---|---|
| コーディングエージェントハーネス | hindsight-integrations/coding-agents/src/ (フックハーネスと永続プラグインハーネス: dsh、opencode、Kilo、Cline、Prime Agent) |
| ラッパー SDK クライアント | TypeScript + Python ラッパー — ステップ 7a を参照 |
| Alembic マイグレーション | すべてのマイグレーションにおける _pg_upgrade / _oracle_upgrade |
| データプレーン ↔ コントロールプレーン | api/http.py のパラメータと hindsight-control-plane/src/app/api/** のプロキシルート + lib/api.ts |
| LLM プロバイダー | engine/llm_wrapper.py のプロバイダーごとの分岐 |
手順 — これは手作業で行ってください。どんなリンターもこれを捕捉しません。 diff が新しい兄弟を追加したとき、またはファミリーのある兄弟を変更したとき:
- ファミリーを列挙する。 既存のすべての兄弟をリストアップします (ディレクトリを
lsする、レジストリを grep する)。 - コードではなく機能リストを diff する。 他の 兄弟が持つ各機能 — 呼び出されるライフサイクルフック、実行されるセットアップ/ティアダウン、尊重される設定フラグ、尊重されるオプトアウト、レジストリ/インストーラー/ドキュメントのエントリ — について、変更された兄弟がそれを持っているか、あるいはその不在が意図的でありコメントされていることを確認します。grep がツールです:
grep -rn ensureDaemon srcは誰がそれを呼び出しているかを証明します。 - コピーよりも巻き上げを優先する。 その機能が N か所に存在するようになった場合、修正は通常、すべての兄弟がすでに共有している 1 つのパス (例:
RuntimeCore、buildHookOutput) にそれを移動することであり、N+1 番目の兄弟がまた忘れるであろう N 番目のコピーを貼り付けることではありません。 - 単体テストだけでなく、構造的なガードを要求する。 忘れた兄弟のテストは構造上存在しないので、ファミリー全体に対して アサートするテストを求めます: ファイルシステム/レジストリから兄弟を列挙し、それぞれがコントラクトを満たすことをアサートし、明示的でコメントされた除外リストを付けます。前例:
registry covers every installable harness(harness/registry.test.ts)、every harness entrypoint reaches a daemon(core/daemon.test.ts)、test_backup_tables_covers_entire_schema、test_migration_shape.py。
1 つを除くすべての兄弟に存在する機能は must fix としてフラグを立てます — どの兄弟がそれを持ち、どれが持っていないか、そしてユーザーに見える症状は何かを述べます (#3524 の場合: すべての hindsight_* ツール呼び出しが ECONNREFUSED で失敗し、デーモンを起動するものが何もない)。ファミリー全体のガードテストなしで新しい兄弟ファミリーメンバーが着地するのは should fix です。
10. MCP ツール登録の完全性を確認する
hindsight-api-slim/hindsight_api/mcp_tools.py で新しい MCP ツールが追加された、または既存のツールの名前が変更された場合:
mcp_tools.pyの_ALL_TOOLSセット — 新しいツール名を含める必要がありますmcp_tools.pyのregister_mcp_tools()にあるtools_to_registerデフォルトセット — 新しいツール名を含める必要がありますhindsight-api-slim/hindsight_api/api/mcp.pyの_SINGLE_BANK_TOOLSセット — 新しいツールがバンクスコープである場合 (list_banks/create_bankのようなバンク管理ツールではない場合)、これを含める必要がありますhindsight-control-plane/src/components/bank-config-view.tsxのMCP_TOOL_GROUPS— UI ツールセレクターの適切なグループに新しいツールを含める必要があります- テスト (例:
test_mcp_tools.py) の ツール数のアサーション — 新しい数を反映するように更新する必要があります
11. バックアップ/リストアのテーブル網羅性を確認する
マイグレーションで新しい PostgreSQL テーブルが追加される場合(hindsight-api-slim/hindsight_api/alembic/versions/ 内の CREATE TABLE / op.create_table を探してください):
hindsight-api-slim/hindsight_api/admin/cli.pyのBACKUP_TABLES— 新しいテーブルを含める必要があり、外部キーで参照するテーブルより後ろに配置します(親の後に子)。エントリの欠落はサイレントなデータ損失を招きます。そのテーブルは決してバックアップされず、リストア時のTRUNCATE banks CASCADEは、たとえ保存されていなくても banks への外部キーを持つ子テーブル(例:mental_models、directives)をリストア時に消去してしまいます。tests/test_admin_backup_restore.pyのガードテストtest_backup_tables_covers_entire_schemaがこれを強制します — 新しいテーブルがBACKUP_TABLESに存在しない場合は must fix として指摘してください。- Oracle 専用のテーブル(例:
observation_sources)は意図的に除外されています — 管理用のバックアップ/リストアは PostgreSQL 専用です。
11b. 新しい設定フラグが env テンプレートを更新しているか確認する
差分が新しい設定フィールドを追加する場合(hindsight-api-slim/hindsight_api/config.py 内の新しい ENV_* / HINDSIGHT_* 環境変数):
.env.example(リポジトリルート) —hindsight-docs/docs/developer/configuration.mdxのドキュメントエントリと並んで、その変数を追加する必要があります(任意の場合はコメントアウト)。config.pyに追加されたフラグが.env.exampleに存在しない場合は should fix です。hindsight-embed/hindsight_embed/env.example— バンドルされたコピーはリポジトリルートの.env.exampleとバイト単位で同一でなければなりません(embed/profile 設定のシードになります)。test_bundled_template_matches_repo_root同期テストは差分があると失敗します。ルートファイルが再コピーされずに変更された場合は、must fix として指摘してください。
11c. アドバイザリロックを確認する
差分で advisory を grep してください(git diff main...HEAD | grep -in advisory)。新しい pg_advisory_lock / pg_try_advisory_lock / pg_advisory_xact_lock / pg_advisory_unlock 呼び出しは must fix です — 上記の「データベースロック」を参照してください。単にロックを外すよう求めるのではなく、代替案(プロセスごとのオブジェクト、冪等な DDL、行レベルの制約)を作者に示してください。
11e. BaseHTTPMiddleware を確認する
git diff main...HEAD -- '*.py' | grep -nE "@app\.middleware\(|BaseHTTPMiddleware"
新しい @app.middleware("http") または BaseHTTPMiddleware のサブクラスは must fix です — 上記の「HTTP ミドルウェア」を参照してください。純粋な ASGI ミドルウェア(またはロジックがルーティングコンテキストを必要とする場合は APIRoute のサブクラス)を求めてください。また、send ラッパーがクライアント入力に由来するヘッダー値をサニタイズしていることを確認してください。
11f. 送信 HTTP が aiohttp かつ非同期であることを確認する
git diff main...HEAD -- '*.py' ':!**/tests/**' | grep -nE "^\+.*(import httpx|from httpx|import requests|urllib\.request|http\.client|noqa: TID251|\.Client\(|asyncio\.to_thread|run_in_executor)"
本番コードでの新しい httpx の使用または同期 HTTP 呼び出しは must fix です — 上記の「送信 HTTP」を参照してください。ruff はインポートを検出しますが、検出できないものはレビューで確認します。非同期版が存在するのに同期 SDK クライアントを使っている、同期呼び出しを to_thread / run_in_executor に押し込んでいる、LoopLocalSession の外で aiohttp.ClientSession を作成している、そしてサードパーティ SDK の設定や分類を行っていない # noqa: TID251 です。
11g. ドキュメントのコード例がテスト済みスニペット由来か確認する
差分が hindsight-docs/docs/** に触れている場合、Hindsight を呼び出す新規または変更されたフェンス付きブロック(```python、```bash、```typescript、```go など)を探してください。SDK クライアントメソッド、hindsight <cmd> CLI コマンド、または /v1/... への curl です。それぞれが hindsight-docs/examples/api/ 内のセクションに裏付けられた <CodeSnippet> でなければなりません(「ドキュメントのコード例」を参照)。また、その例ファイルが呼び出しを実際に実行している(単に出力しているだけではない)こと、および自身の banks をクリーンアップしていることを確認してください。インラインの API 例は must fix として指摘してください。
公開中の /developer/... URL は docs/ ではなく hindsight-docs/versioned_docs/version-<latest>/ 内の最新スナップショットを配信します(docs/ は /developer/next/ です)。リリース時に docs/ がそこへ同期されます。次のリリース前に公開されるべきドキュメント修正は、そのスナップショットにもコピーする必要があります。
11d. 並行処理プリミティブを確認する
上記の「並行処理」を参照してください。diff を grep します:
git diff main...HEAD -- '*.py' | grep -nE "asyncio\.(Lock|Semaphore|Event|Condition|BoundedSemaphore|Future)\(|threading\.(Lock|RLock)\("
必ず修正:
- モジュールスコープで作成された
asyncioプリミティブ、またはプロセス全体のシングルトンに保存されたもの — これは最初にそれを待機したループにバインドされ、他のすべてのループを壊します。 awaitをまたいで保持されるthreading.Lock— これは制御を譲るのではなく、イベントループをブロックします。- 実行中のループをキーに含めずに
asyncio.Futureを保持する、処理中/合流用のマップ。
変更がインタープリターをひそかに脱落させていないかも確認してください:
- 新しい依存関係、またはバージョンアップで、3.11〜3.14 のいずれかにホイールがない —
build-api-python-versionsCI マトリックスはそのすべてをインストールしスモークテストします。 - 3.11 より新しい構文や標準ライブラリの使用 (
uv run ty checkがそのほとんどを検出します)。 - プロセスごとに1つのイベントループを前提としているテストで、それが対象としているのが共有状態であるもの。
修正すべき:
- ロックなしの新しいプロセスグローバルな可変状態 (dict/set/list、可変値に対する
lru_cache)、または並行して変更されうる場所での反復処理。
12. 他のコーディング標準に照らしてレビューする
diff に上記の標準への違反がないか確認してください:
- プロジェクトルートにある Python ファイル (許可されていません)
- 非同期パターンの欠如 (全体を通して非同期であるべきです)
- リクエスト/レスポンス用の Pydantic モデル
- 行長が 120 文字超
- 求められた以上の新機能/コード (過剰なエンジニアリング)
- 不可能なシナリオに対する不要なエラー処理
- 時期尚早な抽象化や投機的なヘルパー
- 後方互換性のためのハック (未使用の変数、再エクスポート、「removed」コメント)
13. 調査結果を報告する
重大度別に整理した明確なサマリーを提示します。
必須修正 — CI を壊す、またはプロジェクトの厳格なルールに違反する問題:
- ブランチ上の無関係なコミット
- Lint の失敗
- パブリック関数の型ヒントの欠如
- 構造化データに対する生の dict の使用 (内部コードを含む)
- 複数項目のタプル返却 (内部コードを含む)
- 新しいエンドポイントに対するテストの欠如
- ユーザー向けの新機能、または既存機能の新たな組み合わせで、
hindsight-system-tests/にストーリーがないもの (ステップ 6b を参照) - 公開されたクライアントをバイパスする、スクリプト化されていない LLM 呼び出しを無効化する、または決定論性を保つためにバックグラウンド処理を無効化するシステムストーリー
MemoryEngineメソッドではなくapi/ハンドラー内での直接 DB アクセス (生の SQL /acquire_with_retry/fq_table)- エンジンで認証が強制されていないテナントスコープのデータアクセス (
_authenticate_tenant/get_bank_profile) - 呼び出し元が指定した、グローバルに一意でないキーでフィルタリングされたマルチバンクテーブルに対する SQL 文で、
bank_id述語がないもの (バンク間の読み書きリーク — ステップ 7c を参照) - 新しい統合にテスト、CI ジョブ、または release-integration.sh のエントリがないもの
- リリース済み/追加済みの統合が
hindsight-docs/src/data/integrations.jsonにない、またはdocs-integrations/<slug>ページのない JSON エントリ (check-integrations.mjsによりドキュメントビルドが失敗する) admin/cli.pyのBACKUP_TABLESにない新しい PostgreSQL テーブル (リストア時のサイレントなデータ損失)- diff 内の実装だけが持たない、他のすべての同種実装が備えている機能 (ステップ 9a を参照) — 他の実装が実行するライフサイクルステップをスキップするハーネス、ダイアレクト、プロバイダー、または言語バリアント
- インポート時に作成される、またはプロセス全体のシングルトンが所有する
asyncioロック/セマフォ/イベント、あるいはawaitをまたいで保持されるthreading.Lock(ステップ 11d を参照) - テスト済みの
hindsight-docs/examples/api/ファイルの<CodeSnippet>ではなく、Hindsight を呼び出す (SDK、CLI、またはcurl /v1/...) インラインドキュメントのコード例 (ステップ 11g を参照) - サポートされているインタープリター 3.11 から 3.14 の一部でのみ動作する変更 (サポートされているインタープリターを参照)
修正推奨 — コード品質を損なう問題:
- リンターが見逃したデッドコード / 未使用のインポート
- 自明でないユーティリティ関数に対するテストの欠如
- タスクスコープを超えた過剰な設計
注記 — 対応が必要かどうか判断が分かれる所見:
- クライアントの再生成が必要になる可能性のある API 変更
- 近隣のコードスタイルから逸脱するパターン
各所見には、ファイルパス、行番号、簡単な説明を含めてください。
問題を自動修正しないでください。すべての所見を報告し、何に対応するかはユーザーに判断を委ねてください。所見がない場合は、コードは問題ないと確認してください。