建議 model:opus/effort:medium — 要設計兩張新表、寫 RLS、實作狀態機轉換表,是整案的資料地基。

本卡屬 FR-107(母卡 CM-1843)第 .1 棒(子需求卡 CM-1844),子任務 T-1.2:把「暫存批次」與「批次裡的檔」兩張資料表建起來,加上對應的程式物件與狀態機骨架。

這張卡做什麼(白話)

暫存批次=使用者一次上傳的一堆證據檔,還沒分類、還沒掛任何任務,先放在一個「籃子」裡。這張卡把籃子本身的資料結構做出來:一張表記籃子(evidence_batches),一張表記籃子裡有哪些檔(evidence_batch_files)。

同時做狀態機骨架:籃子會在「上傳中 → 就緒 → 分類中 → 審閱 → 已歸檔」之間走,什麼狀態可以做什麼動作要寫死在一張轉換表裡,非法轉換一律擋掉回 409。這張卡先做到「上傳中/就緒」兩個狀態,後面的狀態在 T-3.2 補。

這張卡不做 API(那是 T-1.3),只做表 + 資料物件 + 狀態機的規則。

動哪些檔/表/套件

套件(走 path dependency 開發)
~/Projects/Jedicogy/module/jedi-python-package/jedi-evidence-classification/
  .../infra/model/evidence_batch_model.py        ← 新增
  .../infra/model/evidence_batch_file_model.py   ← 新增
  .../domain/entity/  對應 entity + query entity + repo interface
  .../infra/repository/  repo impl(繼承 BaseRepositoryImpl)
  .../app/service/evidence_batch_service.py      ← 新增,ALLOWED_TRANSITIONS
  .../di_containers/  註冊

主專案
scripts/sql/packages/jedi_evidence_classification/00N-evidence-batches.sql  ← 新增

RLS 範本
scripts/sql/packages/jedi_asset/002-asset-rls-grants.sql:49-75

表結構:compliance.evidence_batches(批次)

欄位                              型別                  說明
id                               serial PK
uid                              varchar(36) UNIQUE NOT NULL   對外定址
tenant_id / org_unit_id          int NOT NULL          RLS 與租戶隔離
                                                        (tenant-scoped 表必含 org_unit_id)
round_id                         int NOT NULL          批次掛稽核輪次。
                                                        🔴 軟參照、不建 FK——project_audit_rounds
                                                        屬另一支插件(jedi-compliance-audit),
                                                        插件互不相依;存在性由宿主
                                                        IClassificationContext.resolve_round 驗
project_id / project_uid         int NOT NULL / varchar(36)   冗餘,列表與專案角色守門用
status                           varchar(16) NOT NULL  uploading/ready/classifying/
                                                        review/archived/failed
model                            varchar(64)           本批實際用的 AI 模型
confidence_threshold             numeric(4,2)          信心門檻
profile_id                       int NULL              本批解析到哪個 AI 分類設定(NULL=內建通用)
current_run_id                   int NULL              最新一次分類結果(重新分類會換)
job_started_at                   timestamptz NULL      D4 心跳(本卡只建欄位,邏輯在 T-3.2)
job_heartbeat_at                 timestamptz NULL      同上
failure_reason                   text NULL             容器失敗/逾時/心跳逾時
file_count                       int DEFAULT 0         統計,各階段回寫
classified_count                 int DEFAULT 0
archived_count                   int DEFAULT 0
no_task_count                    int DEFAULT 0
archived_at / archived_by_user_id       timestamptz / int
purged_at  / purged_by_user_id          timestamptz / int
created_user / updated_user / created_at / updated_at   慣例四欄

🔴 審計欄位回傳規範:API 回傳 created_user/updated_user(帳號)時,必須同時回傳 created_user_name/updated_user_name(暱稱),在 app service 層批次查轉換,不要在 infra 層 JOIN。canonical 是 common/util/audit_nickname.py 的 enrich_audit_nickname_objs。

表結構:compliance.evidence_batch_files(批次檔)

欄位                    型別                     說明
id                     serial PK
batch_id               int NOT NULL FK→evidence_batches.id   同套件內可建 FK
file_id                int NOT NULL             →upload_files.id(🔴 軟參照:另一支套件的表)
                                                 內部用 int、對外 API 一律 file_uid
file_uid               varchar(50) NOT NULL     冗餘存一份,避免每次 join
original_name          varchar(255) NOT NULL    上傳時的檔名
size                   bigint
content_hash           varchar(64) NULL         SHA-256;從 upload_files.sha256 抄
                                                 (D9 內容重複提示用)
status                 varchar(16) NOT NULL     pending/classified/archived/no_task/removed
classification         JSONB NULL               歸檔當下的判定快照
                                                 [{"part_id":..., "confidence":..., "source":"ai|manual"}]
                                                 removed 列靠這欄保留 AI 判定結果(D7)
archived_evidence_uids JSONB NULL               歸檔後寫入的 job_evidences.uid 清單(追溯)
removed_at / removed_by_user_id    timestamptz / int   D7 清理紀錄
tenant_id / org_unit_id            int NOT NULL        RLS
慣例四欄

UNIQUE (batch_id, file_id)
RLS 四條 policy

狀態機轉換表(design.md §6.2,本卡先做前兩列)

動作          允許的來源狀態                      目標狀態      額外前置條件
加檔/刪檔     uploading、ready                    uploading    非 classifying
              (刪檔另允許 review,只能刪                      (否則 409 EC_BATCH_SEALED)
                pending/classified 列)
完成上傳       uploading                           ready        file_count >= 1
------- 以下在 T-3.2/T-4.2 補,本卡先把 dict 的骨架與擋法做好 -------
開始分類       ready、uploading(自動先 ready)、    classifying  儲存設定已設;目標集非空;
              failed、review                                   同專案無其他 classifying 批次
容器回結果     classifying                         review
失敗          classifying                         failed
歸檔          review                              archived     至少一檔有判定
清理          archived                            archived     只動 pending/classified/no_task 的檔

實作方式:套件 EvidenceBatchService 內以一張 ALLOWED_TRANSITIONS dict 實作,非法轉換一律拋 ConflictError。不要散在各個 method 裡用 if 判斷(那樣新增狀態時會漏改)。

怎麼做(逐步)