コンテンツにスキップ

計量・按分

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

目方入力の確定保存(不変条件ラチェット・mig222)

  • 確定は原本凍結ではなく、ドライバーが現時点の目方入力を完了したという run 単位の宣言である。
  • 入力ページ表示・save の allowedSiteIds・帰社計量警告は mekataInputStops という同一純関数を共有する。
  • 抑制条件は最終確定時刻より前(同時刻を含む)に完了した stop だけで、確定後に完了した stop は再表示・再警告する。
  • complete_weighing_eventfinalize_run_weighing_confirmed・mig143 ゲート・mig102 一意性は変更しない。
  • 最終処分場除外、claim 済み event 行、按分許容の既存契約も変更しない。
  • 確定 RPC は本人 driver・tenant・run・collecting・未削除をすべて fail-closed に検証し、staff 代行を許可しない。
  • mekata_confirmed_at は上書きされる最終宣言時刻であり、新しい完了 stop の有無だけを比較して導線を導出する。
  • weighings.mekata_confirmed_at は driver の直接 UPDATE 経路を持たない(driver は weighings に書込不可=RLS で 0 行)。導線は confirm_run_mekata(DEFINER)だけを通す。

品目別の按分丸め(不変条件ラチェット・mig223)

  • item_unit_settings.rounding_kg提案値だけを丸める(例 10kg 単位)。NULL は丸めなし=従来挙動で、既存行を backfill しない。
  • 丸めは client の提案生成にのみ適用し、finalize RPC の Σ 検証・保存値の契約は変更しないfinalize_run_weighing_confirmedΣ(round(weight_kg,2)) = round(net_kg,2) 厳密一致は不変)。端数は 1kg 品目が吸収する。
  • CHECK は rounding_kg is null or (> 0 and <> 'NaN' and < 'Infinity')。精度なし numeric なので Infinity ガードを外してはならない(型が弾かない)。
  • テナント別設定なので RLS/列 guard は mig131 の item_unit_settings 契約(SELECT=自テナント全 authenticated / 書込=staff・WITH CHECK 完全修飾・service_role policy なし)をそのまま継承する。

run 按分の総重量/空車系列(不変条件ラチェット・mig224)

  • apportioned_gross_kg / apportioned_tare_kg は finalize RPC が導出する server-derived snapshot である。
  • 系列順の権威は client が供給する p_items の配列順で、同一回収先の全品目行へ同じ gross/tare 対を保存する。
  • 導出は event/run の gross/tare と site 合計 weight から行い、client の系列値は受け取らない。
  • 先頭 gross・隣接 tare/gross・site 差分・最終 tare を検証し、不整合は fail-closed に扱う。
  • zero-net の空 items は従来どおり成功し、系列を保存しない。再按分では既存 snapshot を再導出して上書きする。
  • driver の直書きは mig225 の明示条項で遮断するweighing_items_guard_driver_writewhitelist ではなく denylist(拒否列を名前で列挙する実装)なので、「新列は列挙外だから既定で守られる」は成立しない。mig224 時点の本節はそう書いていたが誤りで、実測(docker・全 migration 適用後)では role=driver が自分の collecting run の行へ update weighing_items set apportioned_gross_kg = 99999 を 1 行成功・commit でき、事務が計量履歴の詳細(WeighingDetailDialogkind='run' で常に「総重量 / 空車」を表示する)でその値を読めた。mig225 が INSERT / UPDATE 双方へ 2 列の拒否条項を追加した。staff の直書きは従来どおり遮断しない(mig129 思想)。
  • weighing_items へサーバ所有列を足すときは必ず mig225 の denylist へ拒否条項を追加する(UPDATE だけでなく INSERT 側も。UPDATE のみだと新規行に値を仕込む迂回が残る)。mig131 の目方 snapshot 3 列は driver 入力なので意図的に列挙外である。回帰は scripts/proofs/mig225-apportioned-driver-guard.sql(G1/G2 UPDATE deny の文言 + 値不変・G3/G4 INSERT deny・G5 許可列の非退行・G6 finalize RPC の DEFINER carve-out 非破壊・G7 staff 非発火)を全流しで固定する。

Resource Weighing Run Optimization(不変条件ラチェット)

資源物(有価物)7品目を運行(1コース)単位で計量する機能(mig100/101/102)の不変条件。現物と一致させること(実体: supabase/migrations/00000000000100_resource_run_weighing.sql..101_complete_run_weighing_rpc.sql..102_run_weighing_single_collecting_per_driver_day.sqlapps/web/src/lib/db/weighings.tsrun-apportionment.tsresource-aggregation.ts)。既存の単一計量(weighing_kind='single')は完全非破壊で温存する。

  • データモデル(mig100): weighings.weighing_kind text not null default 'single' check in ('single','run')weighings.dispatch_stop_id uuid nullweighings.partner_idnullable(run は取引先非紐付け/single 必須は INSERT/UPDATE ポリシーで担保)・statuscollecting 追加(full: collecting/open/weighed/completedcollecting は run 専用=weighings_collecting_requires_run_check)・gross_kg は collecting run のみ null 可(weighings_gross_required_unless_collecting_run_check)。weighing_itemscollection_site_id/partner_id/estimated_kg numeric(10,2)ドライバー目方=原本)/is_remainder boolean not null default falseweight_kg確定重量・default 0)・非負 CHECK weighing_items_estimated_nonneg_check。unique は weighing_items_unique_per_site_item (weighing_id, collection_site_id, item_type_id) NULLS NOT DISTINCT(single=site null でも (weighing_id, item_type_id) 一意を保持しつつ run は回収先別に同一品目を許す)。item_types は資源7品目 activecardboard/magazine/newspaper/pet_bottle/waste_cloth/shredded_paper/aluminum(mig100 追加)・mig16 の code が正典)・styrofoam/mixed_paperactive=false(履歴・FK 温存で行削除しない)。
  • partner_id は単一情報源(mig100 fill トリガ): weighing_items_aaa_fill_partner_from_site(BEFORE INSERT/UPDATE・DEFINER・aaa_ 接頭辞で他 guard より先に発火)が collection_site_id から collection_sites.partner_id を派生し クライアント供給の partner_id を上書き(site null → partner null)。クライアント(weighings.ts)は collection_site_id のみ送出する。月次集計 fetchMonthlyAggregation/aggregateBySiteAndItem/aggregateByPartnerAndItemitem-level の partner_id/collection_site_id を集計軸に優先(run は親 weighings 側が null のため)・single は親値へフォールバック・weight_kg>0 の確定行のみ集計。proof mig101-*.sql の CR14 でピン留め。
  • collecting run の一意性(mig102): 部分ユニーク索引 weighings_one_collecting_run_per_driver_day = (tenant_id, attribution_driver_id, weighed_on) WHERE weighing_kind='run' AND status='collecting' AND deleted_at IS NULL AND attribution_driver_id IS NOT NULL。理由: モバイルの run 解決が (driver, date, weighing_kind='run', collecting)limit(1) で引くため、1 driver+date に collecting run が2つあると誤 run に重量が混ざる。DB 索引(機械強制・23505)+ client 先回りブロック(fetchCollectingRunsForDriverDate/blocksDuplicateRunRunAlreadyCollectingError)の二層防御。恒久解は run→course 紐付け(follow-up)。
  • ライフサイクル/対称ガード: collectingopen(gross)→weighed(tare)→completed。run の gross は recordRunGross(条件付き UPDATE weighing_kind='run' AND status='collecting' AND deleted_at IS NULL・不一致は RunGrossNotCollectingError)、tare は recordRunTare(同 status='open'・不一致は RunTareNotOpenError)で対称にガード(stale パネルが soft-deleted/別状態の法定記録を巻き戻さない)。run の新規作成 UI と web の createRunWeighing は #533/v1.71.0 QA で撤去済み(新規はコース由来・get_or_create_today_run RPC のみ。staff INSERT policy と driver INSERT 不可の DB 契約は不変で、既存 run の閲覧・確定経路も不変)。single は既存 useWeighIn/weighOut/useCompleteWeighing を非破壊で温存。
  • 按分(run-apportionment.ts: net=gross−tare。固定品目 weightKg=estimatedKg・残り品目(is_remainder・運行あたり1つ)weightKg=net−Σ(固定 estimatedKg)applyResidualToItems で充当し Σ(weightKg)=net を厳密成立。許容 = apportionmentToleranceKg(net)=max(1, net*0.005)正側の丸め誤差吸収専用)。negative_net(net<0)/remainder_negative(Σ固定>net・許容でマスクしない)/multiple_remainders/no_positive_items(net>0 で正 item 皆無)/no_remainder_excess(残り無しで正側許容超過)は severity='error'=確定不可。UI 確定ゲートは RPC 契約(net>0 のときのみ空/全ゼロ item 拒否・zero-net は空完了可)と一致。error パスでは固定品目の weightKg を書き換えない(原本保持)。
  • RLS(mig100): driver は「driver inserts/updates own run weighing_items」で 自運行(attribution_driver_id=自分)かつ weighing_kind='run' かつ status='collecting' かつ未削除weighing_items のみ書ける。列 guard weighing_items_guard_driver_writeSECURITY INVOKERcurrent_user in ('authenticated','anon') かつ current_user_role()='driver' のときだけ発火・weight_kg/id/weighing_id/item_type_id/created_at/deleted_at/deleted_by/weighing_event_id/apportioned_gross_kg/apportioned_tare_kg の変更と INSERT 時 weight_kg≠0/deleted_at/deleted_by/weighing_event_id/apportioned_* を raise)で weight_kg 直書きを遮断。これは拒否列を名前で列挙する denylist であって whitelist ではない——列挙しなかった列は driver が直接 PATCH できるので、サーバ所有列を足すときは必ず拒否条項を追加する(実装と食い違う「未知列も既定で保護される」というコメント/記述が mig224 の列挙漏れを生み、mig225 で是正した)。越境 FK 注入は weighing_items_guard_fk_tenant(DEFINER・親 weighing の tenant と collection_site/partner の一致を全経路で検証)で遮断。single は driver 不可(mig99 の driver-own SELECT 二段は温存・再著述しない)。
  • RPC(mig101): complete_run_weighing(p_weighing_id uuid, p_items jsonb)SECURITY INVOKERsearch_path=''・全参照 public. 完全修飾・revoke from public+grant to authenticated。テナントガード(自テナント+weighing_kind='run'+未削除)→ net 未確定/負値 weight_kg/net>0 空完了を raise → Σ検証(max(1,net*0.005))→ 原子(delete weighing_itemsinsert(weight_kg>0 のみ・fill トリガが partner 派生)→update status='completed')→ {weighing, items} を返す。旧 complete_weighing は先頭で run を raise 拒否(single の挙動/署名/INVOKER/search_path は mig75 完全不変)。sentinel mig100_run_weighing_applied()(immutable・true)が dev verifier の preflight。
  • 回帰: docker proof(scripts/proofs/mig100-*.sqlmig101-*.sql(CR14 partner 派生・CR13 空完了拒否・SENT sentinel 含む)/mig102-*.sql)を scripts/proofs/*.sql 全流し(shim.sql + 全 migration に適用・[OK] 検出/[NG]=0)。dev 実機は scripts/verify-run-weighing-rls.mjs(sentinel preflight・infra/seed 起因は softSkip=exit0・漏洩のみ FAIL=exit1)。

Internal-Course Multi-Weighing(不変条件ラチェット・mig108)

社内コース1運行に複数の帰社計量イベントを持たせ、各イベントが「回収完了かつ計量未完了の回収先」を自動 claim して按分する機能(weighing_kind='run' の拡張・mig100/101/102 上に加法)。実体: supabase/migrations/00000000000108_run_weighing_events.sql。既存の single/run は非破壊。

  • データモデル(mig108): 新表 run_weighing_events(帰社計量イベント・run 配下 N 件・gross_kg/tare_kgnet_kg numeric generated always as (gross_kg - tare_kg) storedstatus text check in ('open','weighed','completed')seq int=運行内連番・unique (run_weighing_id, seq)tenant_id は run から fill トリガ補完)。numeric CHECK は net/gross/tare is null or (col >= 0 and col <> 'NaN'::numeric)(NaN 封鎖・[[numeric-money-check-nan-gap]])。weighing_items に加法列: collected boolean not null default false(回収完了・driver 設定)/weighing_event_id uuid(null=計量未完了・非 null=計量完了)。weighing_event_id の FK は on delete restrictset null だと計量済み行が再 claim され二重計上する)。
  • 列 guard(mig108): run_weighing_events_guard_protected_colsSECURITY INVOKERcurrent_user in ('authenticated','anon') かつ coalesce(current_setting('app.bypass_rwe_guard', true), '') <> 'on' のときだけ発火)が client 直 PATCH/INSERT を遮断(UPDATE で status/gross_kg/tare_kg/weighed_at/seq の distinct・INSERT で status<>'open'/weighed_at/gross_kg/tare_kg 非 null を raise=空捏造完了・net 遡及改ざん封鎖)。weighing_items_guard_driver_write(mig100)は許可列に collected を追加・weighing_event_id は driver 直書き禁止・計量済み行(old.weighing_event_id is not null)の driver UPDATE を全面禁止。越 run 紐付けは weighing_items_guard_fk_tenant(DEFINER)が weighing_event_id は同一 weighing_id(run) の event のみ許可で遮断。
  • fill/整合トリガ(mig108): run_weighing_events_fill_tenant(DEFINER・親 run から tenant 補完=越境封鎖・run_weighing_id は immutable・親が weighing_kind='run' かつ未削除を検証)。weighings_sync_weighed_event(DEFINER・AFTER UPDATE OF status,gross_kg,tare_kg・run が weighed へ遷移 or 既 weighed の gross/tare 訂正時に canonical event(seq 最小)を weighed で create-or-sync+active items を collected=true にマーク・completed event を持つ run は不触=確定保護)。
  • RPC(mig108): get_or_create_today_run(p_weighed_on date) returns uuidSECURITY DEFINERsearch_path=''・fail-closed role guard=driver/admin/normal 以外 raise・帰属は role='driver' or can_drive=true 限定=[[epd-attribution-canactasdriver-and-live-partner]]・mig102 部分ユニークで 23505→再 SELECT で冪等)。complete_weighing_event(p_event_id uuid, p_items jsonb) returns jsonbSECURITY INVOKERsearch_path=''・写経 complete_run_weighing): event.status in ('weighed','completed') を許可(再按分)・claim 排他=collected=true AND weighing_event_id IS NULL(初回)or weighing_event_id = p_event_id(再按分)・親 run→event→items の順に FOR UPDATE ロック(TOCTOU/並行 claim 直列化)・p_items と claim 集合の全単射検証・Σ(weight_kg)=event.net_kg 許容 greatest(1, net*0.005)・保護列書込は app.bypass_rwe_guard local GUC で通す。
  • 回帰: docker proof scripts/proofs/mig108-*.sqlscripts/proofs/*.sql 全流し・[[local-docker-proof-must-run-all-proofs]])。dev 実機は scripts/verify-run-weighing-events-rls.mjs(sentinel preflight・infra/seed softSkip=exit0・漏洩のみ exit1)。

Walk-in Weighing(不変条件ラチェット・mig111)

顧客持ち込み(walk-in)計量。weighing_kind='walk_in'(案A)。伝票(session)×品目行の逐次差分/単価/金額を扱う。実体: supabase/migrations/00000000000111_walk_in_weighing.sqlDB 層 + Web UI とも配線済み(mig111 Wave3): apps/web/src/lib/db/walk-in-weighings.tsWalkInWeighingPage/WalkInWeighingsListPagewalkInReceiptData(計量証明書PDF)・App.tsx ルート /collector/walk-in-weighing(一覧/検索/詳細/再発行)。既存の single/run/walk_in 判定は非破壊。綴り: contract_type='walk-in'(ハイフン)/ weighing_kind='walk_in'(アンダースコア)。

  • データモデル(mig111): weighings 加法列 weighing_method(sequential/single/individual・walk_in なら NOT NULL)/payment_method(cash/transfer/card/invoice)/settlement_mode(spot/monthly)/total_amount numeric/receipt_no text/settled_at timestamptzweighing_kind CHECK を ('single','run','walk_in') に拡張・gross-required CHECK に or weighing_kind = 'walk_in' 追加(walk_in は weighing.gross_kg NULL 許容)。新表 walk_in_sessions(伝票セッション・seq/weighed_at/slip_no/vehicle_no・tenant は fill 補完)。weighing_items walk_in 列 walk_in_session_id/line_seq/gross_kg/tare_kg/line_net_kg/kago_biki/dust_mizu_biki/direction(buyback/charge)/unit_price/amount。既存 weight_kg = 課金対象重量(line_net_kg − kago_biki − dust_mizu_biki・控除後)。unique は walk_in 除外の部分索引 + walk_in 専用 (weighing_id, walk_in_session_id, line_seq) + weighings (tenant_id, receipt_no) where receipt_no is not null
  • 符号規約 / NaN 封鎖: total_amount 正=客が支払う / 負=客が受け取る(= Σ(charge amount) − Σ(buyback amount))。total_amount は負値が正規(買取超過)のため >=0 を付けず NaN のみ封鎖total_amount is null or total_amount <> 'NaN'::numeric・[[numeric-money-check-nan-gap]])。walk_in 品目の numeric 列(gross/tare/line_net/kago/dust/unit_price/amount)は >= 0 and <> 'NaN'::numeric の null 許容 CHECK。
  • 列 guard(mig111・walk_in 行のみ): weighings_guard_walk_in_cols(INVOKER・app.bypass_walkin_guard local GUC carve-out)が client の walk_in weighing 直接 INSERT を丸ごと raise(必ず finalize_walk_in_weighing 経由)+ UPDATE で weighing_kind(immutable)/total_amount/receipt_no/settled_at/payment_method/settlement_mode/weighing_method/attribution_driver_id(walk_in は driver 帰属なし=driver 不可視担保)/deleted_by(監査列)の distinct を raise。weighing_items_guard_walk_in_cols(INVOKER・同 GUC)が親 walk_in 配下への client 直接 item INSERT を raise + walk_in item の権威列(walk_in_session_id/line_seq/gross_kg/tare_kg/line_net_kg/kago_biki/dust_mizu_biki/direction/unit_price/amount/weight_kg/deleted_by)の client 変更を raise。越境 session link は weighing_items_guard_fk_tenant(mig108 に walk_in_session の同一 weighing 検証を追記)で遮断。
  • RPC(mig111・いずれも SECURITY INVOKER・search_path=''・staff 限定 fail-closed): finalize_walk_in_weighing(p_partner_id, p_weighing_method, p_weighed_on default null, p_payment_method default null, p_settlement_mode default null, p_sessions default '[]')=1 Tx で walk_in 伝票確定(bypass GUC 下で weighing/session/items INSERT・内部整合Σチェックは永続化後に storage から Σ weight_kg(=v_sum_actual) と Σ round(line_net_kg−kago−dust, 2)(=v_sum_rederive) を独立再導出しループ内 v_sum_expectedis distinct from の厳密一致で照合=許容 greatest(1, expected*0.005) は不採用(設計書より厳格)・不一致は raise で確定中止)。price_walk_in_weighing(p_weighing_id, p_prices)=後付価格(unit_price 更新・amount=round(weight_kg*unit_price)total=Σ(charge)−Σ(buyback) 再算出)。採番は next_walk_in_receipt_seq(p_tenant uuid)(DEFINER・per-tenant/day 行ロック直列採番・p_tenant is distinct from current_tenant_id() で越境採番遮断=fail-closed)。receipt_no = to_char(current_date,'YYYYMMDD')||'-'||lpad(seq,3,'0')日付部は採番日 current_date 基準・weighed_on ではない)。
  • legacy 拒否(mig111): complete_weighing は先頭で run 拒否に加え walk_in 拒否(walk_in は session/逐次差分/単価を持ち legacy の delete→plain reinsert で破壊されるため finalize_walk_in_weighing 経由に限定)。single の挙動/署名/INVOKER/search_path は不変。
  • 回帰: docker proof scripts/proofs/mig111-*.sql(1806/2300/1a04 系 fixture・全流し)。dev 実機は scripts/verify-walk-in-rls.mjs(sentinel mig111_walk_in_weighing_applied() preflight)。

Walk-in 未収(不変条件ラチェット・mig199)

顧客持ち込みの掛売を、確定時の「精算済み」表示から未収として正直に扱う Stage 1。請求書発行・入金消込の本体は別ステージ。

  • settled_at の意味論: settlement_mode='spot' と未指定(NULL)は確定時に now()(従来挙動)。settlement_mode='monthly' を明示した行だけ確定時 NULL のまま=未収。判定は apps/web/src/pages/collector/walkInPageLogic.tswalkInSettlementStatus に一元化する。
  • 入金記録 / 取消: settle_walk_in_weighing(uuid) / unsettle_walk_in_weighing(uuid)(mig199)を経由する。いずれも SECURITY INVOKER・staff(admin/normal)・monthly walk-in・未削除・自テナント限定、role 欠落を含む fail-closed guard、app.bypass_walkin_guard transaction-local GUC、対象行ロック + ROW_COUNT 検査を持つ。client の settled_at 直 PATCH は従来どおり列 guard で拒否する。
  • 過去行の是正: mig199 以前に確定した monthly 行は、実際に入金済みかをデータだけで判別できないため NULL へ backfill しない。事務が実態を確認し、入金済み表示が誤りなら unsettle_walk_in_weighing で人手是正する。
  • 回帰: scripts/proofs/mig199-walk-in-receivables.sql の S1〜S7(monthly/spot/NULL、settle/unsettle、deny 文言・越境・role 欠落、直 PATCH guard)を scripts/proofs/*.sql 全流しで検証する。

社内コース帰社計量 確定保存(不変条件ラチェット・mig118/143)

社内コース(weighing_kind='run')の帰社計量で、この run に目方がある「回収完了かつ確定計量済」回収先×品目のみを対象に、帰社スケール net を確定目方比で按分して確定保存する機能(mig100/101/108 上に加法、②a の目方ゲートは mig143)。実体: supabase/migrations/00000000000118_run_weighing_confirmed_finalize.sqlsupabase/migrations/00000000000143_run_apportion_mekata_gate.sqlapps/web/src/lib/db/run-apportionment.tsapportionRunByRatio)・apps/web/src/lib/weighing/confirmed-apportion-view.tsapps/web/src/components/weighing/ApportionPanelRun.tsx。既存 single/run 完了経路(complete_run_weighing/complete_weighing_event)/ walk_in は完全非破壊。

  • 確定保存 RPC(mig118): finalize_run_weighing_confirmed(p_weighing_id, p_items)SECURITY INVOKERsearch_path=''・完全修飾・revoke public+grant authenticated(mig115 作法・DEFINER 化禁止)。status='weighed' の初回確定のみ親 run を FOR UPDATE(weighing_kind='run'・未削除・tenant_id=current_tenant_id())し、collecting/open は拒否、completed の再按分は既存 complete_run_weighing に限定する。event link 済み行と未 claim collected 行が混在する partial event run は whole-run delete→reinsert を禁止する S1/S2 positive-invariant guard に従い、complete_weighing_event で現在 weighed の event を確定する。通常経路は delete→insert(保存丸め後 weight_kg>0 のみ)→status='completed'→canonical event create-or-complete→reinsert 行へ collected=true + weighing_event_id を原子的に行う。bypass GUC は既存の app.bypass_legal_delete/app.bypass_rwe_guard のみ使用(新規 GUC を作らない)。
  • 許可品目のサーバ強制(mig118・新セキュリティ契約): collection_site_item_types(mig104)をサーバ側で強制する唯一の点。p_items の各 (collection_site_id, item_type_id) は、その site に csit 行があるなら item がその集合に含まれること(未設定 site=0 行は全許可=mig104/driver UI と同 graceful fallback)。違反は raise(fail-closed)。mig104 以前は driver UI のみが whitelist を絞りサーバ強制が皆無だった穴を塞ぐ。client(ApportionPanelRun/confirmed-apportion-view.ts)は表示面でも resolveSiteItemTypes で絞るが、権威はこの RPC。
  • 確定後不変性は event link で担保(staff freeze を作らない): finalize は items を weighing_event_id link + run completed にし、mig108 driver guard(old.weighing_event_id is not null の driver UPDATE 全面禁止)+ FK on delete restrict で driver 側の不変性を担保する。staff の完了後編集は封じない: [[settlement-finalize-snapshot-not-source-freeze]](方針B)で原資料(weighings/weighing_items)は finalize 後も編集可・不変性は精算スナップショットで担保、が採用済み。status='completed' で weight_kg を一律凍結する guard は mig94(settlement source 編集 ALLOW)/mig101(再按分)/mig111(walk-in item 編集)と矛盾するため作らない(この判断は敵対的な全 proof 全流しで検出された回帰から確定・[[local-docker-proof-must-run-all-proofs]])。
  • 確定比按分(client): apportionRunByRatiorun-apportionment.ts)は walk-in computeSingleApportion の整数「銭」(cents) 最大剰余法を移植し net を確定目方比で配分・Σ(weightKg)=net 厳密・全 share≥0・負残差取りこぼし無し。ApportionPanelRunsiteKeys 内の state のみを比率入力・表示合計・canSubmit・payload に使い、検証集合と送信集合を一致させる。既存 apportionRun(固定+残りモデル)は非破壊で温存。サーバ RPC は Σ(round(weight_kg,2)) = round(net_kg,2) の厳密一致を検証し、同じ丸め済み値だけを INSERT する(許容 greatest(1,net*0.005) は使わない)。
  • 回帰: docker proof scripts/proofs/mig118-confirmed-finalize.sql(1807/1a05/1d00-e 系 fixture・F1 許可外品目拒否/F2 fallback/F3 confirmed+Σ=net+event link/F4 driver 改変不可/F5 越境拒否/F6 非 weighed 拒否/F7 sentinel/F9 strict Σ/F10 inactive/F11 sub-cent 保存和乖離拒否)を scripts/proofs/*.sql 全流しし [NG]=0 を確認する。sentinel mig118_confirmed_finalize_applied()新 server-owned 契約(許可品目強制)は prod-critical → codex 敵対的セキュリティ Workflow 必須([[adversarial-workflow-and-new-table-protected-cols]])。

  • 計量ストップの表示範囲(2026-07-20 目方一本化): 計量ボードの運行(計量ストップ)のタイトル(CollectorWeighingPage.runLabelOfrun-board-labels.runSiteLabel)と帰社計量詳細の初回確定対象(confirmed-apportion-view.confirmedSiteIds)は、配車予定(dispatch_stops)そのものではなく run の weighing_itemsestimated_kg > 0 または is_remainder=true のサイト(意味のある目方入力)に scope する。signal は sitesWithEstimate(items)estimated_kg=0/null 非残りは対象外、残り行は estimated null でも対象)。タイトルはボードの run 群に対し useWeighingItemsForRuns(tenantId, runIds) で weighing_items を 1 クエリバッチ取得して N+1 を避ける。再按分パス(reapportionSiteIds=既存内訳)・最終処分場除外・許可品目(mig104)・single は非破壊。通常回収 site(二次搬出の排出元を含む)は最終処分場でなければ対象に残す。回帰は confirmed-apportion-view.test.ts / ApportionPanelRun.test.tsx / run-board-labels.test.ts / weighings.freshness.test.ts で固定。

  • 帰社計量 finalize の目方ゲート(不変条件ラチェット・mig143/mig144): finalize_run_weighing_confirmed の ②a・complete_weighing_event の claim・client confirmedSiteIds/event round の母集合は、この run に estimated_kg > 0 または is_remainder=trueweighing_items がある collection_siteに限る(event round は collected 未計量 claim を 0kg 補完する)。dispatch_stops の有無・日付・帰属 driver・completed 状態を適格性判定へ戻してはならない。collection_sites.partner_id → partners.is_final_disposal=true の最終処分場は、driver 目方行の生成時と両 finalize RPC で fail-closed に除外する。目方無し site、estimated_kg=0 のみの site、null site は対象外。通常回収 site(二次搬出の排出元を含む)は最終処分場でなければ対象に残す。恒久回帰は scripts/proofs/mig143-run-apportion-mekata-gate.sql + scripts/proofs/mig144-final-disposal-mekata-gate.sql + confirmed-apportion-view.test.ts + ApportionPanelRun.test.tsx で固定する。

Driver Mekata Units + 保存後遷移 + PWA鮮度(不変条件ラチェット・mig131)

ドライバー運行目方入力に「品目別 単位/kg換算(テナント別・袋数入力)」「保存後の本日ルート遷移」を加え、PWA のデータ鮮度を改善する加法機能。実体: supabase/migrations/00000000000131_item_unit_settings_and_estimated_count.sqlpackages/shared/src/lib/convert-count-to-kg.tsapps/web/src/lib/db/item-unit-settings.tsapps/web/src/components/weighing/RunWeighingKeypadSheet.tsxapps/web/src/pages/driver/DriverRunWeighingPage.tsxapps/web/src/pages/collector/ItemUnitSettingsPage.tsxapps/web/src/lib/pwa-focus-refetch.ts。既存の single/run/walk_in・按分(run-apportionment/confirmed-finalize mig118/complete_run_weighing mig101/complete_weighing_event mig108)は完全非破壊。

  • テナント別 item_unit_settings(mig131): (tenant_id, item_type_id, unit, kg_per_unit)unique (tenant_id, item_type_id)(通常 unique=PostgREST upsert 可・部分索引 42P10 罠に非該当 [[run-weighing-upsert-42p10-partial-index]])。tenant_idbefore insert or update(全列発火)の INVOKER fill トリガが current_tenant_id() で上書き(越境封鎖・null で fail-closed [[fill-tenant-trigger-scoped-update-gap]])。RLS = SELECT は自テナント全 authenticated(driver 含む・換算読取)/ INSERT/UPDATE/DELETE は staff(admin|normal)自テナントのみ・WITH CHECK は完全修飾 item_unit_settings.tenant_id = current_tenant_id()([[rls-with-check-tenant-id-shadowing]])。service_role policy は作らない(最小権限)。未設定品目は item_types.default_unit='kg'・換算1 に fallback。
  • numeric 契約(mig131): kg_per_unit> 0)・weighing_items.estimated_count>= 0)・estimated_kg_per_unit> 0)の CHECK は非負/正だけでなく <> 'NaN'::numeric かつ < 'Infinity'::numeric をセットで含める(NaN + ±Infinity の全非有限値を封鎖。PG14+ は numeric Infinity を持ち > 0 and <> 'NaN' だけでは Infinity を通すため。[[numeric-money-check-nan-gap]] の Infinity 拡張)。
  • weighing_items スナップショット3列(mig131・additive): estimated_count/estimated_unit/estimated_kg_per_unitestimated_kg は按分原本のまま非改修(= round(count × kg_per_unit, 2) を client 計算で入れる)。weight_kg は driver 書込禁止のまま。mig100 の weighing_items_guard_driver_write(ブロックリスト実装)は再著述しない——新3列は列挙外で driver 書込可、weight_kg は既存 raise で遮断。kg 品目は3列 null(完全後方互換)。この不変性は proof IUS5(driver が3列を書ける)/IUS6(driver の weight_kg 直書きは依然 raise)で固定。 保存 RPC upsert_run_weighing_itemsmig261 で3列を INSERT/UPDATE とも永続化する(mig115 は列を持たず送信値を捨てていた)。キー欠落は NULL。回帰は mig261-mekata-snapshot-persist.sql と S1 12-mekata-units.scenario.test.ts で固定する。dev verifier の sentinel preflight は classifySentinelResponsescripts/lib/postgrest-sentinel.mjs)で分類し、未デプロイ(404/PGRST202)と既知の接続障害(5xx・PGRST000〜003・429)は softSkip、200/false403 等の退行だけ FAIL にする。全 verifier の sentinel preflight は probeSentinel 経由(ラチェット sentinel-preflight-ratchet)。
  • 保存後遷移(web・Part A): DriverRunWeighingPage.handleSavesave() の戻り { saved, fetchError, whitelistDropped, routeDropped } を使い、完全成功(saved===true && !fetchError && whitelistDropped===0 && routeDropped===0)のときだけ navigate('/driver')。部分未保存/各種エラー時は遷移せず入力保持(stale クロージャ回避のため form state でなく save() 戻り値で判定)。
  • PWA 鮮度(web・Part C): queryClient.ts のグローバル既定は不変(refetchOnWindowFocus:false/staleTime:60s)。本日ルート/計量ボード等の閲覧系クエリのみ opt-inrefetchOnWindowFocus:true+refetchOnReconnect:true+staleTime 短縮+refetchInterval(背景停止 refetchIntervalInBackground:false)。目方入力ページはポーリングしない(編集上書き回避・手動更新+save 前 fresh 取得を維持)。iOS standalone 復帰対策に pwa-focus-refetchvisibilitychange/pageshow(persisted)focusManager.setFocused(true)Supabase Realtime は本スライス対象外(follow-up)。
  • 回帰: docker proof scripts/proofs/mig131-item-unit-settings.sqlscripts/proofs/*.sql 全流し([[local-docker-proof-must-run-all-proofs]])。sentinel mig131_item_units_applied()。prod-critical(migration + 新テーブル)は codex 敵対的セキュリティレビュー済([[adversarial-workflow-and-new-table-protected-cols]])。

Scale Serial Auto-Probe(不変条件ラチェット・2026-07-15)

  • 既存の AD-4410(2400/7E1)/AD-4347(9600/7E1)の固定選択は単一 open のまま非破壊で温存し、file モードは対象外とする。
  • auto は SCALE_PROBE_COMBOS を確度順(9600/7E1→2400/7E1→9600/8N1→4800/7E1)に試し、全候補で parseAdScaleLine を使い、isProbeHit(status !== 'unknown') の候補で確定・記憶する。無受信は error とする。
  • 候補ごとの open↔close を厳密に対にし、プローブ中も lifecycleRef の直列化、各 await 後の stale ガード、DTR/RTS assert、XON(shouldSendXon)を維持する。候補切替で二重 open/ポートリークを起こさない。
  • 候補順序・hit 判定・auto 写像・成功候補記憶は純関数/小さな I/O helper として vitest で固定し、hook の I/O 実装へ判断ロジックを戻さない。

計量記録メモの側表(不変条件ラチェット・mig268/269)

weighings.notes は事務が計量記録に付ける現場メモで、書込・閲覧とも staff 限定の設計意図。 PostgreSQL の RLS は列マスクを持たないため、driver と同じ authenticated ロールから 分離できず、側表 weighing_notes(1:1・staff 限定 RLS)へ移した。設計正典は docs/superpowers/specs/2026-09-10-weighing-notes-driver-secrecy-design.md

  • 対象は weighings.notes だけ: driver 所有・driver 宛のメモ列(dispatch_stops.note / vehicle_daily_logs.note / skip 理由類)は秘匿対象にしない(秘匿すると機能が壊れる)。 他 driver の stop の note が読める残存は wl-0t93(P3・別スライス)。
  • 行はメモが空でない計量にだけ存在し(空/NULL は行削除)、notesbtrim <> '' の 名前付き CHECK。tenant_id は DEFINER fill が親 weighings から毎回再導出 (BEFORE INSERT OR UPDATE 全列・UPDATE OF に限定しない)、weighing_id は immutable (fill と INVOKER guard の二重)。updated_by / updated_at は fill がサーバー値で 上書きする(client 直書きを 42501 で拒否するのではなく上書きする。INVOKER guard が 42501 で拒否するのは weighing_id の変更だけ)。
  • RLS: staff(admin|normal)の SELECT・INSERT・UPDATE・DELETE 4 本(個別・ALL ではない)。 いずれも自テナント + 親 weighing が未削除であることを exists で要求する。 driver policy なし・service_role policy なし・anon なし。 trigger 専用の DEFINER 関数(fill・同期)は revoke all ... from public, anon, authenticated, service_role で直接 CALL させない(mig253/196 の作法)。 sentinel mig268_weighing_notes_applied()revoke from public + grant to authenticated だが、関数の default ACL により anon にも EXECUTE が残る (preflight が anon で成立する理由・revoke しない)。
  • expand(mig268・日中可): backfill(冪等 on conflict do nothing)+ 同期トリガ (AFTER INSERT OR UPDATE OF notes・DEFINER・再入防止)で旧列と整合する。 段階 1 の web は旧列(weighings.notes)だけに書き、側表は同期トリガの派生として 読む(fetchWeighingNotes + attachWeighingNotes・未確定時は合流しない)。 親 UPDATE と側表 upsert の dual-write は採らない(別トランザクションの interleave で 新旧不一致になる・PR #772 レビュー F2。保存・CSV はメモ確定まで開始させない)。 driver 経路の weighings 直 select は WEIGHING_COLUMNSnotes 抜き 31 列)の明示列挙にする。 rowToWeighing / rowToWalkInWeighingrow.notes を読まない。
  • contract(後続 migration・採番は着手時に実測・夜間推奨): 同期トリガと関数を drop → weighings.notes の列を 落とす。weighings に staff 限定のメモ列を戻してはならない(RLS は列マスクを持たない)。
  • 回帰: scripts/proofs/mig268-weighing-notes.sql(N1〜N11・変異 3 系統の歯)・ S1 38-weighing-notesscripts/verify-weighing-notes-rls.mjsno-unbounded-query-ratchet の weighings 投影検査(weighing-notes-ok marker)で固定する。