建議 model:opus/effort:medium — 整案的流程主幹,串起 stage/容器/結果回寫,且背景執行緒租戶脈絡有已知坑。

本卡屬 FR-107(母卡 CM-1843)第 .3 棒(子需求卡 CM-1846),子任務 T-3.2:把「按下開始分類」到「分類跑完進審閱」整條串起來,並讓 BE 重啟後卡住的批次能自動變成失敗。

這張卡做什麼(白話)

這是整案的流程主幹。前面幾張卡把零件做好了(批次表、插座、轉接頭、容器契約),這張卡把它們串成一條線:

按「開始分類」
  → 批次狀態改「分類中」(封口,之後加檔回 409)
  → 解析 AI 分類設定 + 取得檢查點清單
  → 把整批檔拉到 job 工作目錄,寫 catalog.json/manifest.json/prompt.json
  → docker run 起容器
  → 讀回結果 JSON
  → 寫進分類結果表(掛批次、存目標集快照)
  → 批次狀態改「審閱」
  → 刪掉工作目錄裡的 files/(不留第二份實體檔)

心跳機制(D4):分類跑在背景執行緒,如果 BE 中途被砍掉重啟,前端會永遠看到「分類中」永遠不會結束。改成執行緒每 60 秒在批次上蓋一個時間戳,BE 啟動時掃一遍:狀態是「分類中」但心跳超過 30 分鐘沒更新的,自動標成「失敗」,使用者可以按重跑(不必重傳檔案,檔還在批次裡)。

背景執行緒的租戶脈絡陷阱:背景執行緒沒有 HTTP 請求的脈絡,讀「檔案存到哪」的設定時會挑錯租戶。解法是在 HTTP 請求裡先把需要的東西全部解析好,當成參數傳進執行緒,執行緒裡不再讀任何 thread-local。

動哪些檔

套件(走 path dependency 開發)
~/Projects/Jedicogy/module/jedi-python-package/jedi-evidence-classification/
  .../app/service/evidence_batch_service.py
      ← 加 classify(batch_uid, user, model?, threshold?)
      ← ALLOWED_TRANSITIONS 補 classifying/review/failed 三列
  .../app/service/evidence_classification_service.py
      :144 trigger_classify(project_uid, ap_uid, ...)  ← 舊簽名保留給舊 route,不動
      ← 加 run_batch(...)(stage → 寫三個 json → docker run → 讀回 → 寫 run → review)
  .../app/service/job_registry.py:20 JobRegistry
      ← 🔴 只留「進度百分比」這種暫態,持久化狀態全部進批次表
  .../infra/classifier_container_runner.py   ← T-2.3 已改,這裡呼叫
  .../plugin/register  ← 加 BE 啟動 hook:掃心跳逾時
  .../api/routing.py   ← 加 POST /evidence-batches/<batch_uid>/classify

狀態機補齊(design.md §6.2)

動作          允許的來源狀態                       目標狀態      額外前置條件
開始分類       ready、uploading(自動先 ready)、    classifying  ① 儲存設定已設(is_configured)
              failed、review                                    ② 目標集非空(否則 412
                                                                    EC_NO_TARGET_CATALOG)
                                                                 ③ 同專案無其他 classifying 批次
                                                                    (沿用 JobRegistry 單活性)
容器回結果     classifying                          review
失敗          classifying                          failed       failure_reason 要填
加檔/刪檔     🔴 非 classifying                                 classifying 時回 409 EC_BATCH_SEALED

心跳(D4)
  job_started_at   分類開始時寫
  job_heartbeat_at 執行緒每 60 秒 UPDATE(🔴 獨立短 transaction,不要卡在主 transaction 裡)
  BE 啟動 hook:
    UPDATE evidence_batches SET status='failed', failure_reason='heartbeat_timeout'
     WHERE status='classifying' AND job_heartbeat_at < now() - interval '30 min'

🔴 心跳是「執行緒還活著」不是「跑完了」——只要執行緒活著就會一直蓋時間戳,所以超大批次真跑超過 30 分鐘不會被誤標失敗(design.md 風險 11)。30 分鐘這個常數放套件 config 可調。

背景執行緒的租戶脈絡(design.md §6.8,本卡最容易踩的坑)

怎麼做(逐步)