コンテンツにスキップ

横断規約・リリース

本書は AGENTS.md の分冊(正典・AGENTS.md と同格)。優先順位は「実コード → AGENTS.md(本書を含む)→ DESIGN.md → PRODUCT.md」。 節は AGENTS.md から逐語で移設した(PR #nnn・2026-09-16)。対象パス: apps/web/src/scenarios/**` `docs/harness/templates/**` `.github/workflows/**。この領域に触るワーカー / レビュアは本書を読む。

KYO-123 Hardening sweep(不変条件ラチェット・mig127)

T1 監査で、現行の最終定義に残っていた技術的負債は fill_tenant trigger の UPDATE OF 限定 4 件だけだった。DEFINER の role guard(mig106)と代表的な金額 numeric CHECK(mig22/92/95)は既に修正済みであり、mig127 は既存関数の再写経をせず trigger を全列発火へ収束させる。監査の接地列挙は .worker-specs/sweep-audit.md、実証は scripts/proofs/mig127-hardening-sweep.sql で固定する。

  • DEFINER role guard: current_user_role() の拒否判定は NULL を通す <> / != / NOT IN を使わず、IS DISTINCT FROM または明示的な NULL 拒否で fail-closed にする。
  • numeric NaN: 金額・数量・重量等の非負 numeric CHECK は、非負条件だけでなく <> 'NaN'::numeric を同じ CHECK に含める。total_amount のように負値が正規の列も NaN は拒否する。
  • parent-derived fill_tenant: 親から tenant_id 等を再導出する trigger は必ず BEFORE INSERT OR UPDATE(全列)で発火させる。BEFORE INSERT OR UPDATE OF <fk> に戻してはならない(tenant-only PATCH の改ざんを許すため)。
  • 恒久チェック: scripts/hardening-sweep-ratchet.mjs / hardening-sweep-ratchet.test.mjs は mig127 以降の migration に上記 3 パターンが再発していないことを検査し、pnpm turbo run test(従って pnpm verify)で実行する。新しい例外は migration と同じ変更でこの不変条件・テストを更新する。
  • 認可 deny の errcode: guard / RPC の認可拒否(role・tenant・caller profile・サーバ所有列)の raise exception には using errcode = '42501' を付け、proof は sqlstate と文言の両方を照合する(when raise_exception は P0001 専用の条件名で 42501 を受けない)。mig267 が Group A(driver_profiles guard / soft_delete_legal_record / restore_legal_record)に適用した写経元。既存 143 関数・549 箇所の全面 sweep は行わない(proof handler 312 箇所が P0001 に結合しているため別判断)。引数不正・record not found・内部整合の raise は対象外。
  • 全履歴の最終カタログscripts/proofs/qa6-catalog-sweep.sql が RLS・DEFINER grant/search_path・soft-delete guard・fill・denylist列集合を走査する。名前付きの歴史的例外は未修正所見であり、安全認定ではない(QA-6 全スキーマ監査報告)。

業務シナリオテスト S1(不変条件ラチェット・2026-08-21)

設計正典は docs/superpowers/specs/2026-08-21-scenario-test-design.md。導入の根拠: 直近 1 週間で unit/render/proof/mutation の 4 層を全部緑で通過して出荷され、実際に動かして 初めて見つかったバグが 5 件あり、5 件中 4 件はデータ層の合成(画面と画面・状態と状態の 継ぎ目)で壊れていた(解析待ち滞留 v1.76.1 / 目方の便跨ぎ #625 / 再解析が飾り #622 / 破棄ブラックホール #621 / 請求書の会社名 #622)。

  • 業務フローを跨ぐ変更(複数画面・複数状態・driver⇄staff を跨ぐもの)は *.scenario.test.ts(S1・実 DB シナリオテスト)を必須とする。 置き場所は apps/web/src/scenarios/。単一画面内の変更は従来の unit/render で足りる。
  • 仕様(worker spec / 設計書)には業務シナリオをユーザー承認の 9 ステップ形式で必ず含める (2026-08-21 の目方精査でユーザーが書いた形式が原型)。S1 テストはそのシナリオの 各ステップを assert し、レビューは「各ステップが assert されているか」を突合する。
  • S1 では lib/db の内部をモックしない。 差し替えてよいのは @/lib/supabase の client singleton(ローカル PostgREST + ロール別 JWT へ向ける)と、外部 API(Workers の OCR 等)の fetch 境界・Storage の upload だけ。supabase-js → PostgREST → 実 Postgres(shim + 全 migration) → 実 RLS/guard を通す。ハーネス自体の自己検査(越境 read が実際に拒否される)を持つ。
  • seed-lite は scenarioUsers.driver の工場と profile_factories 所属を用意する(工場スコープ検証のためにシナリオ側で所属を補完しない)。 seed-lite は再適用可(冪等)。
  • S1 の PostgREST は max-rows=1000(本番相当)scripts/scenario/docker-compose.ymlPGRST_DB_MAX_ROWS を維持し、大量明細の単発取得による欠落を実 DB シナリオで検出する。
  • : シナリオが守る修正を巻き戻すと赤になることをミューテーションで実証してから完了とする。
  • S1 から Worker の実 handler を import するときの型境界: API の公開 Bindings 型は KVNamespace 等を @cloudflare/workers-types から明示的に type import する。 API の tsconfig にだけある ambient 型へ依存させない(Web の build で TS2304 になる)。
  • S1 の外部 HTTP 境界を差し替える仮想 origin は HTTPS にする。ローカル PostgREST の 接続先はハーネスの環境変数から転送先にだけ使い、仮想 API に HTTP URL を書かない (semgrep の react-insecure-request で検出。テスト用の抑制は追加しない)。
  • per-PR は S1 のみ(目標 5 分以内・rls-proofs の postgres 起動手順を共用)。ブラウザ E2E (S2 golden-path)はリリース PR + 夜間に限定し、per-PR に入れない(速度とフレーク対策)。
  • S1 の起動条件: import / spawn する apps/web 外のファイルは .github/workflows/scenario-tests.yml の push / pull_request の paths に含める。 scripts/scenario-paths-ratchet.test.mjs が到達条件と両集合の一致を固定する。

テスト規約 + 共通ヘルパ(不変条件ラチェット・2026-07-27)

QA スイープのテストパターン監査で確定した規約。基盤は健全(colocated 100%・__tests__ 0 件・ snapshot 乱用 0・assertion ゼロのテスト 0)なので、弱点だけを規約化する。

  • テストは日本語で書く(web 82% が既に日本語)。既存の英語タイトルを一括改名する価値はない。 scripts/**/*.test.mjs も実行器は Vitestscripts/vitest.config.mjs)。node:test を混ぜると 単体の node --test は緑でも verify の Vitest が No test suite found で落ちるため、vitest から test/expect を import する。 ファイル名の infix は .render(react 2 コピー制約下の末端 render)と .freshness (React Query invalidate 契約)と .scenario(S1・実 DB シナリオ)だけを分類として使う。 それ以外の造語 infix は新規で作らない。
  • モックは「実 hook / 実 DB と同じ形」にする。引数を無視して固定値を返すモックは、 配線漏れや遷移バグを緑のまま通す。実例: 一覧の total が未取得ページで 0 に落ちる遷移を 再現していなかったため「2 ページ目へ進めない」バグが通過し、fetchAllPages.range() を 無視するモックは月次集計の未配線を検出できなかった(どちらも 2026-07-26 に修正)。
  • db テストは書込先テーブルも assert するexpect(mocks.from).toHaveBeenCalledWith('<table>'))。 列マッピングの assert だけでは from('別表') への取り違えが緑で通る。
  • 車両の表示識別子は vehicle-labels.tsvehicleIdentity / vehicleIdentityLabel に集約plateNumber ?? chassisNumber ?? shortCode をインラインで書かない(既定値が 4 通りに分裂し、 同じ「車番の無い車両」が画面ごとに違う文字で見えていた)。越境表示には使わないchassis_number は台帳機密。discharger 向けは mig166 の RPC が返すラベル)。 2 段版の文言は 2026-08-04 に 車番未設定 へ統一済み。
  • .dependency-cruiser-known-violations.json は空を保つ。抑制を残すと同じ辺の循環再導入を CI が黙認する(2026-07-27 に stale な 1 件を撤去して空にした)。新しい循環は抑制せず直す。
  • 未 import のコンポーネントは残さない。テストが緑でも production 経路に無いものは 偽のカバレッジになる(SiteAggregationTable を削除済み)。

NaN / 非有限 numeric の全表担保(不変条件ラチェット・mig170)

金額・数量の numeric 列は NaN と ±Infinity を書けてはならない。Postgres は NaN を最大値として 扱い SUM も NaN になるため、精算・単価・数量が壊れる。PostgREST から 'NaN' の文字列で書ける。

  • 権威は scripts/proofs/mig170-nan-guard-sweep.sql の N10(実カタログを走査し、金額・数量の numeric 列に NaN ガードが無いものが 0 件であることを検査する)。scripts/hardening-sweep-ratchet.mjsENFORCED_FROM_MIGRATION=127 は「127 より前は安全」を意味しない — mig004/053/094 の 7 列が 一度も掃かれず無ガードで残っていた(2026-07-28 に実カタログで発見)。テキストを見るラチェットは 速い事前検査で、下限を下げて代わりにはできない(歴史的 migration の旧 DDL を拾って壊れる)。
  • 符号規約を勝手に厳しくしない。 external_partner_settlements.adjustment_amount は符号付き (mig094 §1-2)、net_amount は減算で負になりうる、weighings.total_amount は買取超過で負 (mig111)。これらは非負を課さず NaN / ±Infinity だけを拒否する。非負にすると正当な精算が 入らなくなる(proof N9 が固定)。
  • 精度付き numeric は Infinity を型が弾く。 numeric(12,2) への 'Infinity' は 22003 (numeric field overflow)で CHECK に到達しない。精度なし numeric の列weighings.total_amountweighing_items.*item_unit_settings.kg_per_unit 等 8 列)は型が弾かないので CHECK が必須。
  • N10 は NaN の権威であり Infinity 全列の証明ではない。 QA-6 で見つかった weighing_itemsgross_kg / tare_kg / line_net_kg / kago_biki / dust_mizu_bikimig262 で Infinity 上限を是正済み。既存 Infinity は自動補正せず migration を停止する。精度なし numeric の全列上限は scripts/proofs/qa6-catalog-sweep.sql の Q6-10(検証済み CHECK・例外0)で固定し、5列個別の constraint_name・旧 NaN/負値拒否・有限値 finalize は scripts/proofs/mig262-qa6-hardening.sql が固定する。
  • proof の deny 検査は制約名まで照合する。 不正な列値で別の CHECK が先に落ちると、 当てたい CHECK を検査せずに [OK] を出す(pricing_rules.unit の許容値は per_kg/per_ton/ per_m3/per_run で、'kg' を渡して偽陽性を出した実例がある)。
  • external_partner_settlements は法定記録で物理削除できないblock_physical_delete_legal)。 proof の後片付けで delete すると ON_ERROR_STOP 下で psql が exit 3 になるが、[OK] 文字列だけを 見る全流しでは見逃す。全流しは psql の exit code も判定する([[local-verify-order-gitleaks-and-proof-exitcode]])。

監査列 created_by のサーバー所有(不変条件ラチェット・mig171)

created_by を持つ表は client 供給値を無視してサーバーが決める。INSERT で上書きし、 UPDATE では OLD を保持する(created_at も同様)。実体は supabase/migrations/00000000000171_audit_created_by_guard.sqlscripts/proofs/mig171-audit-created-by.sql

  • 参照先が表ごとに違う。 vehicle_daily_logs / dispatch_ride_alongs / dispatch_stop_ride_alongs / vehicle_cost_entriesprofiles(id) を参照するので fill_audit_created_by()auth.uid() → profile を解決)を使う。jobs.created_by は FK が無く auth.users.id を入れる設計apps/web/src/lib/db/jobs.ts に明記)なので fill_audit_created_by_auth_uid() を使う。取り違えると列の意味が変わり既存データと不整合になる。 新しい表へ足すときは FK の実体を確認してからどちらを当てるか決める。
  • 権威は proof の A6(カタログ全表検査)。守り方は「fill トリガが created_by を代入」か 「INVOKER guard が created_by の変更を raise」のどちらでもよく、両方無い表を 0 件に保つ。 手作業のスイープでは 2 表しか見つからず、カタログ検査が追加で 2 表を出した(jobs / vehicle_cost_entries。他の列を守る guard を持つため手作業の除外条件をすり抜けていた)。
  • トリガの発火順は名前順。 vehicle_daily_logs は既存 4 トリガ(vdl_*)があるので vdl_z_ で 最後に置き、tenant 導出・driver ガードの判断を上書きしない。
  • proof で claims を使うときは begin; ... rollback; で囲む。 set_config('request.jwt.claims', ..., true) は transaction-local なので psql の autocommit では 次の文に残らず current_setting(...)::jsonb が空文字で 22P02 になる(4 回はまった)。