← 一覧に戻る

ダッシュボード再設計 v2: Codex指摘3点への修正設計 (j-20260503-006)

2026年5月3日 23:57 更新
MD から自動変換されたページです。内容について質問があれば右下の ? ボタンからどうぞ。

このドキュメントは j-20260503-005 の Codex adversarial review で指摘された3点の構造的欠陥に対し、修正設計を提示し、Codex に再レビューを依頼してLGTM(これで進めてよい)を取得するまで反復した記録です。実装は一切していません。

前提: 元の3指摘(再掲)

  1. 断捨離順序の自爆: refresh-dashboard.sh を1手目で削除すると complete-job.sh が依存しているため壊れる
  2. 案B「強制」が見せかけ: PreToolUse hook での enforcement は3経路で抜けられる(Bash直叩き / 100字未満手書き / allow_long=true)、ということを文書自身が認めている = enforcement theater
  3. 案C2 CHECK制約に NULL 穴: 現案だと next_action IS NULL を許容してしまい、DONE 時の不変条件になっていない

実証で確認した依存関係(指摘1の根拠固め)

refresh-dashboard.sh の呼び出し元を grep で全件抽出した結果:

つまり「完了処理」「dispatch」の二系統が依存している。Codex 指摘の通り、これを最初に消すと両系統が壊れる。

DB の verification_summary 全レコードを集計したところ:

つまり既存運用では DONE ジョブ多数が next_action NULL のまま放置されており、CHECK制約強化や migration が「素直には」通らない。クリーンアップが先行する必要がある。

Phase 2: 3指摘それぞれの修正設計

指摘1の修正案 (断捨離順序)

依存解消を1手目に置くのではなく、置換が完了してから消す という不可逆順序に並べ直す。

具体的な順序:

  1. mini-apps-ui の JobDetail / 進行中ジョブ一覧が稼働していることを確認(j-20260503-004 で完了済み、https://mbp.tail863a2a.ts.net/app/ で確認済み)
  2. complete-job.shdispatch-job.sh から refresh-dashboard.sh 呼び出しを削除する PR を出す。この時点では refresh-dashboard.sh 本体は残す(ロールバック余地)
  3. 1週間程度の運用で問題ないことを確認
  4. dashboards テーブル(pin message_id を保存している)の sweep。SELECT で残骸 chat_id を確認後 DELETE
  5. refresh-dashboard.sh 本体を削除
  6. setMessageReaction で既存 pin を unpin する処理が要るかは別途調査(残しても害は小さいが綺麗ではない)

依存グラフを逆向きに辿って消す形なので、各ステップでロールバック可能。Codex 指摘の「依存中の部品を1手目で消す」自爆は構造的に避けられる。

副次効果: dispatch-job.sh の冒頭に mini-apps-ui に DISPATCH イベントを SSE で push する処理 を入れるなら同タイミングで実装。ただし指摘1の射程外なので別ジョブ。

指摘2の修正案 (enforcement theater 解消)

3抜け道を構造的に塞ぐには、「秘書が直接 Telegram API を叩けないようにする」しかない。文面制御では塞げないというのが Codex の核心指摘なので、経路制御を取る。

修正案B-1: Bot Token を秘書セッションから隔離

現状の問題:

修正案:

副作用と論点:

修正案B-2: 完了判定をキーワード辞書に切り替え(字数依存の廃止)

現状の問題:

修正案:

副作用と論点:

修正案B-3: allow_long=true の完全廃止

現状の問題:

修正案:

「enforcement theater にならない」設計原則

修正設計から抽出した4つの原則を明文化する:

指摘3の修正案 (CHECK制約 NULL 穴)

NULL を許す書き方では DONE 時の不変条件にならない。代わりに「DONE 遷移時に強制検証する仕組み」を二重で持つ。

修正案C2-A: BEFORE UPDATE トリガーで DONE 時の next_action 非空を強制

CREATE TRIGGER enforce_next_action_on_done
BEFORE UPDATE OF status ON jobs
FOR EACH ROW
WHEN NEW.status = 'DONE' AND OLD.status != 'DONE'
BEGIN
  SELECT CASE
    WHEN (SELECT next_action FROM verification_summary WHERE job_id = NEW.id) IS NULL
      OR (SELECT json_array_length(next_action) FROM verification_summary WHERE job_id = NEW.id) = 0
    THEN RAISE(ABORT, 'DONE transition requires non-empty next_action')
  END;
END;

特性:

修正案C2-B: complete-job.sh が next_action を必須引数化

complete-job.sh JOB_ID --next-action='["next step 1", "next step 2"]'

特性:

修正案C2-C: 二重防御 (A + B 同時適用)

これが推奨。秘書がどの経路で UPDATE しようと、トリガーで止まる。スクリプト経由なら引数チェックでも止まる。同じ条件を2層で守ることで、どちらかをすり抜けても他方が捕まえる。

next_action_kind='none' の扱い(指摘の余地への回答)

業務上「本当に次がない」DONE があるか:

既存データのクリーンアップ

migration 適用前に既存 DONE ジョブ(NULL 169件 + 空配列 4件)をクリーンアップする必要がある:

ただし Codex の指摘「制約を通すために偽の next_action を埋める」破壊半径が大きいので、この修正は migration 前に「既存 DONE はトリガーから除外」する条件を入れる手もある:

WHEN NEW.status = 'DONE' AND OLD.status != 'DONE' AND NEW.id NOT IN (SELECT id FROM jobs WHERE created_at < '2026-05-03T00:00:00Z')

→ これも汚いので、クリーンアップを先にやる方を選ぶ。

Phase 3: Codex 再レビュー結果(LGTM 取得まで反復)

反復1回目: Codex(adversarial・フラットモード) 回答全文

1. 指摘1の修正案 — 結論: ギャップあり。

2. 指摘2 B-1 — 結論: ギャップあり。かなり大きい。

3. 指摘2 B-2 — 結論: 破綻している。

4. 指摘2 B-3 — 結論: ギャップあり。

5. 指摘3 C2-C — 結論: まだ閉じていない。

6. 4つの原則 — 不十分。

欠けている原則:

7. 総合判定: 修正必要

LGTM になるために直すべき点:

反復2回目: 修正設計v3 (Codex反復1の8指摘を全て反映)

指摘1 修正v3: dashboards 廃止を「段階的封じ込め」に置き換え

「ロールバック可能」表記を撤廃し、論理 archive + バックアップ + transaction で監査可能化する。

事前準備:

archive 手順(transaction + row-count gate):

BEGIN IMMEDIATE TRANSACTION;
ALTER TABLE dashboards ADD COLUMN archived_at TEXT;
SELECT 'pre', COUNT(*) FROM dashboards WHERE archived_at IS NULL;
UPDATE dashboards SET archived_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE archived_at IS NULL;
SELECT 'post', COUNT(*) FROM dashboards WHERE archived_at IS NULL;  -- 0 を期待
-- 期待値と異なれば ROLLBACK 手動実行
COMMIT;

成功合否条件(期間ではなく数値で):

ロールバック: cp jobs.db.bak-... jobs.db で SQLite ファイル復元 + dispatch-job.sh / complete-job.sh の git revert。pin 自体は archive 段階で touch しないので追跡可能。

指摘2 修正v3: B-1 OS principal 分離

ステップ1(短期):

ステップ2(中期、別ジョブ起票):

「秘書が送信 script を自由実行できない」要求への回答:

/app/api/internal/send-report 認証仕様v3

固定 schema 限定:

type SendReportPayload = {
  job_id: string;
  chat_id: string;
  type: 'done' | 'propose' | 'question';
  timestamp: number;  // Unix epoch sec
  nonce: string;      // UUIDv4
};

raw text フィールドを置かない。schema を満たさないリクエストは 400 で拒否。

認証:

replay 防止:

異常系:

B-2 完全廃止

keyword dictionary 案は削除。代わりに「秘書が mcp__plugin_telegram_telegram__replysend_message を一切使わない」を hook で強制:

B-3 修正v3: send-ack endpoint 新設

/app/api/internal/send-ack:

type SendAckPayload = {
  job_id: string;
  chat_id: string;
  ack_kind: 'received' | 'reading' | 'deferred' | 'noted';
  timestamp: number;
  nonce: string;
};

設計原則7箇条(Codex 指摘の3原則を統合)

  1. 経路を塞げない封鎖は封鎖と呼ばない(文面チェックは封鎖ではない)
  2. バイパス用フラグは1つでも残せばゼロにならない
  3. 共有秘密はプロセス環境変数まで降ろす + ファイルに残す場合は OS principal で分離
  4. block 時メッセージは正規経路へ誘導必須
  5. (新)信頼境界は OS 上の実体で分離する
  6. (新)特権 endpoint は caller identity + request freshness(timestamp/nonce)を検証する
  7. (新)不変条件は全 mutation path で守る

指摘3 修正v3: トリガー三段構え + schema 分離

C2-D: トリガー三段構え
-- (1) jobs.status DONE 遷移時
CREATE TRIGGER enforce_next_action_on_done_jobs
BEFORE UPDATE OF status ON jobs
FOR EACH ROW
WHEN NEW.status = 'DONE' AND OLD.status != 'DONE'
BEGIN
  SELECT CASE
    WHEN NOT EXISTS (
      SELECT 1 FROM verification_summary
      WHERE job_id = NEW.id
        AND legacy_no_next_action != 1
        AND (
          (next_action_kind = 'none'
           AND next_action_explanation IS NOT NULL
           AND TRIM(next_action_explanation) != '')
          OR (next_action IS NOT NULL
              AND json_array_length(next_action) > 0
              AND NOT EXISTS (SELECT 1 FROM json_each(next_action) WHERE TRIM(value) = ''))
        )
    )
    THEN RAISE(ABORT, 'DONE requires non-empty next_action OR next_action_kind=none with explanation')
  END;
END;

-- (2) verification_summary 側 UPDATE で DONE ジョブの next_action を空白化させない
CREATE TRIGGER enforce_next_action_on_vs_update
BEFORE UPDATE ON verification_summary
FOR EACH ROW
WHEN (SELECT status FROM jobs WHERE id = NEW.job_id) = 'DONE'
  AND NEW.legacy_no_next_action != 1
BEGIN
  SELECT CASE
    WHEN (NEW.next_action_kind = 'none'
          AND (NEW.next_action_explanation IS NULL OR TRIM(NEW.next_action_explanation) = ''))
      OR (NEW.next_action_kind != 'none'
          AND (NEW.next_action IS NULL
               OR json_array_length(NEW.next_action) = 0
               OR EXISTS (SELECT 1 FROM json_each(NEW.next_action) WHERE TRIM(value) = '')))
    THEN RAISE(ABORT, 'cannot empty/blank next_action of DONE job')
  END;
END;

-- (3) jobs INSERT 直で status='DONE' を禁止
CREATE TRIGGER forbid_direct_done_insert
BEFORE INSERT ON jobs
FOR EACH ROW
WHEN NEW.status = 'DONE'
BEGIN
  SELECT RAISE(ABORT, 'INSERT with status=DONE forbidden; use PENDING then UPDATE');
END;

これで mutation path 全域が守られる:

INSERT 順序問題は trigger (3) で解決: 新規ジョブは必ず PENDING で先に入れ → verification_summary 記録 → 最後に jobs.status を DONE へ UPDATE する順序が確定。

next_action_kind='none' の schema 分離

説明文1要素配列に潰す案を撤回し、schema で型を分離:

これで「次がない(説明あり)」と「1件以上の次アクションあり」を schema 上で型分離できる。下流処理は next_action_kind で分岐すればよい。

既存データ cleanup を非破壊化

偽 next_action 注入と updated_at 上書きを廃止:

これで「過去データの意味」を保持しつつ、新規 DONE のみに不変条件を適用できる。

CODEX_ITERATION2_PLACEHOLDER

Phase 4: 推奨実装順序(Codex LGTM 後に確定)

PHASE4_PLACEHOLDER

残論点

PHASE_REMAINING_PLACEHOLDER

📝 質問モード — テキストを選択してね
✓ 質問を送信しました