---
title: "週期任務與條件通知 — 第 0 棒探脈盤點 (FR-137)"
brand: "Guidant AI · **FR-137** 週期任務與條件通知"
eyebrow: "FR-137 · 第 0 棒探脈 · 2026-10-12"
h1: "任務能不能自己重開、掃描能不能自己跑、掃到高風險能不能多通知人"
subtitle: "七個面向的現況盤點：任務本體、型別申報、排程、通知、掃描觸發、資源庫頻率欄、問卷任務"
lede: "這份是唯讀盤點，不給設計。每一項都附程式碼行號或 DEV 資料庫查詢結果，並分成「直接用／要改／沒有」三類，讓首腦定方向、寫討論稿骨架。"
chips: [
  {text: "唯讀探脈", kind: accent},
  {text: "七面向 × 接入點表", kind: ok},
  {text: "首腦待裁 12 題（第 5 題作廢）", kind: warn}
]
footer: "FR-137 · 第 0 棒探脈 · 2026-10-12 · Notion CM-2715（母卡 CM-2714）· 依據：BE branch feature/FR-135 程式碼、jedi-python-package 套件源碼、FE 原始碼 grep、DEV DB（localhost:5432 guidant_ai_dev）唯讀查詢"
---

**狀態**：探脈完成，待首腦寫討論稿　｜　**日期**：2026-10-12　｜　**卡片**：Notion CM-2715（母卡 CM-2714）

路徑慣例：BE 路徑相對於 `compliance-manager-be/`；套件路徑以 `pkg/` 代表 `~/Projects/Jedicogy/module/jedi-python-package/`；FE 路徑相對於 `compliance-manager-fe/`。DEV 查詢一律以 `cmmgr` 唯讀執行。

## 一句話結論 {#summary nav="結論"}

- **任務上沒有任何時間軸欄位**（到期日、週期、下次執行），也沒有「複製單筆任務」的方法；最接近的是覆核輪的「整輪重生＋逐對承襲」。
- **型別申報表只有「啟動時」一個掛鉤真的會被呼叫**；`config_schema` 和 `status_machine` 有申報、平台不讀。完成、到期、取消時都沒有掛鉤。
- **排程骨架現成**：APScheduler 已有 12 支 job、寫法統一，有每 15 分鐘的先例；長工作走 `background_jobs` 佇列，也有「排程只丟單、worker 執行」的先例。但多副本之間沒有鎖。
- **寄信能在背景跑**，但沒有寄信紀錄表、沒有使用者語系欄位、也沒有「哪些事件要通知誰」的設定。
- **檢測工具可以排未來時間**（`agent_tasks.scheduled_at` 拉單時會過濾），SBOM 伺服器端掃描不行；SBOM 重新執行沿用上次上傳的源碼包。兩條掃描路「掃完」會匯合在同一處，那裡可以掛門檻判斷，但現在完全不看嚴重度。
- **資源庫 `frequency` 欄沒有任何排程邏輯在讀**；同表還有兩個從沒被寫過的 `last_execute_date`／`next_execute_date`。前端有四處把六個選項寫死。
- **問卷任務建立時會複製快照**，但「填完 → 任務完成」中間沒有自動串接；填完的問卷回覆狀態機規定不能重開。

## ① 任務本體 {#job nav="① 任務本體"}

### 資料表與欄位

- ORM 在套件裡：`pkg/jedi-flow-engine/jedi_flow_engine/infra/models/job_execution.py:15-41`。
- DEV `compliance.job_executions` 實際欄位：`uid, main_workflow_execution_id, workflow_execution_id, template_id, template_job_id, status, type, start_time, end_time, id, created_at, updated_at, created_user, updated_user, name, description, tenant_id, org_unit_id, job_type, guide`。
- **沒有**到期日、週期、下次執行時間、`parent_round_id`。`start_time`／`end_time` 是實際開始／結束時間。
- 「屬於哪一輪」記在 `workflow_execution_control_mapping.round_id`（`app/flow_control/service/prep_job_generation_service.py:257-260`），母輪欄位在輪次表（`pkg/jedi-compliance-audit/.../infra/model/project_audit_round.py:32`）。
- DEV 分布：`job_type` general 64,446／detection_tool 39／survey 3；`status` TODO 64,159／PROCESSING 249／COMPLETED 80；`type` 全部 USER。

### 任務的四條建立路徑（不是同一支依型別分流）

1. **首輪**：`ProjectStartAppService` 呼叫 `generate(reclone_from_props=False)`（`app/project/service/project_start_app_service.py:315`）。核心在 `prep_job_generation_service.py:119`：每個評估目標各複製一份流程範本，建 workflow_execution 與 job（235-241，固定 TODO／USER，**不設 job_type，落成 general**）。
2. **覆核輪**：`pkg/jedi-compliance-audit/.../audit_round_app_service.py:596` `launch_reverify`，在 774-781 以 `reclone_from_props=True, parent_round_id=…` 呼叫同一支 `generate()` **整輪重生**；788 再呼叫 `ReverifyInheritanceService.inherit_from_parent`（`app/flow_control/service/reverify_inheritance_service.py:34`），用 `get_job_pairs(parent_round_id, new_round_id)`（`infra/flow_control/repository/reverify_clone_query.py:31`）把新舊任務配對，搬指派、部門、設備、證據、問卷（69/84/94/104/143）。
3. **問卷與檢測工具任務**：PM 手動建，`JobService.create_job`（`app/flow_control/service/job_service.py:195`）→ repo `create_job`（`infra/flow_control/repository/flow_control_job_repo_impl.py:639`，`job_type` 寫入在 738）；之後型別分支寫死：問卷建快照（`job_service.py:233-237`）、檢測工具 `if … job_type == DETECTION_TOOL_JOB_TYPE`（243）。檔頭 68-71 自註「型別分支待改成 handler 派發」。
4. **流程圖新增節點**：`create_job_for_node`（repo:1015），固定建 general（1034）。

### 狀態機

- `JobStatus` 四態 TODO／PROCESSING／COMPLETED／CANCEL：`pkg/jedi-flow-engine/jedi_flow_engine/common/enum/job_code.py:3-7`。
- TODO → PROCESSING：`activate_jobs`（`infra/flow_control/repository/task_execution_query.py:106`），「開始執行任務」觸發。
- PROCESSING → COMPLETED：`complete_job`（`app/flow_engine/service/workflow_execution_service.py:648`；非 PROCESSING 擋在 667，寫入 672-674）。批次完成 `job_batch_complete_service.py:42`（general 要有附件、survey 要已填完，81-85）。
- 退回：`revert_job`（`workflow_execution_service.py:811`）。
- **CANCEL 沒有任何寫入點**，只出現在「算已結束」的判斷裡。

### 「重開一筆任務」最接近的既有能力

- **沒有單筆任務複製的方法。**
- 覆核輪：輸入是「控制項清單＋round_id」（generate:119），承襲輸入是 new_round（inheritance:34）；`clone_*` 系列吃「舊任務→新任務」配對，前提是新任務已存在。
- `create_job_for_node`（repo:1015）：在既有流程上建一筆空 job，不帶承襲。
- `flow_template_app_service.duplicate`（`app/flow_engine/service/flow_template_app_service.py:264`）：複製範本，不是任務。

| 接入點 | 現況 | 可用度 |
|---|---|---|
| job_executions 到期日／週期／下次執行時間 | 都沒有（job_execution.py:15-41；DEV 欄位清單同） | 沒有 |
| 任務對應輪次 | 在 wecm.round_id，不在任務上（prep_job_generation_service.py:257） | 直接用 |
| 首輪 `generate()` | 依控制項批次生成、一律 general（119／235） | 要改（不支援單筆、不指定型別） |
| 覆核輪整輪重生＋承襲 | launch_reverify:774-788 | 要改（輸入是整輪） |
| 複製單筆任務 | 無 | 沒有 |
| 在既有流程建一筆 job | create_job（repo:639）、create_job_for_node（repo:1015） | 要改（只建空任務） |
| 承襲子方法 `clone_*` | reverify_clone_query.py:69-143 | 要改（綁母輪語意） |
| JobStatus 四態 | job_code.py:3-7；CANCEL 無寫入點 | 直接用（CANCEL 無流程） |
| 完成轉態 | complete_job:648/672 | 直接用 |

## ② 任務型別申報制 {#task-type nav="② 型別申報"}

### TaskTypeSpec 欄位（`pkg/jedi-task-platform/jedi_task_platform/task/domain/task_type_registry.py:31-89`）

| 欄位 | 行 | 用途 | 平台有沒有在用 |
|---|---|---|---|
| `code` | 40 | 型別代號，即 DB 值（對外契約） | 有 |
| `label` | 43 | 顯示名稱 | 有 |
| `required_modules` | 49 | 需要哪些授權模組（任一即可） | 有 |
| `declared_by` | 52 | 誰申報的，診斷用 | 診斷 |
| `status_machine` | 64 | 合法狀態轉換表 | **不用**（58-63 自註；`is_legal_transition`:202 無呼叫者） |
| `config_schema` | 73 | 設定鍵→內建預設值（schema 進套件、值留宿主） | **不用**（70-72 自註；`config_default`:232 無呼叫者） |
| `on_tasks_activated` | 89 | 任務批次啟動後要做的事 `(job_ids, curr_user, tenant_id) -> int` | 有 |

### 誰註冊了哪些型別

- 集中接線 `core/plugins/task.py:98-101`，只申報兩種：
  - `survey_task_type_spec()`：`pkg/jedi-survey/jedi_survey/app/task_type_declaration.py:61-70`（狀態機＋`review_required` 設定，無掛鉤）。
  - `detection_tool_task_type_spec()`：`app/detection_tools/task_type_declaration.py:101-109`（`on_tasks_activated=_auto_dispatch_first_scans`）。
- 登記方式 `assembly.py:33-42` `register_job_type(replace=True)`。
- **general 不在 BE 申報**，是登記表內建（registry:157-158，撤不掉 147-151）。
- DEV `system_menus` 的 `TASK_TYPE` 組有 task／general／survey／file_upload／exec_tool 五個 key，與申報表（general／survey／detection_tool）**對不上**。

### 原則（檔頭）

- registry 1-19：型別真相由插件申報，平台只按代號查表，不寫回平台枚舉。
- registry 80-82：平台不得持有業務編排、不得寫死某型別啟動時做什麼。
- 檢測申報檔 47-51：派掃描是「產品業務規則」，所以從平台搬出。
- 問卷申報檔 1-25：審核要不要做是「設定」不是「客製」，預設在程式、實際值存宿主 `system_configs`。

### config_schema 能不能放「週期」

- 讀取端沒有人走登記表；問卷自己讀 `review_required(config_reader)`（問卷申報檔 73-115），值是**租戶層級** `SURVEY_CONFIG/review_required`（51-53）。
- **任務本身沒有存設定的欄位**。唯一按任務存參數的是 `config.job_execution_detection_tools.tool_params` JSONB（`scripts/sql/2026-07-26-fr056-2-job-execution-detection-tools.sql:11`），只限檢測任務。
- 結論：放 `config_schema` 只能當租戶預設、平台不讀；放任務上則沒地方存。

### 掛鉤

- 唯一呼叫處：`task_execution_service.py:94` → `_run_activation_hooks`（105-140，每支包 SAVEPOINT，失敗只記 warning）。
- `complete_job`（648）、`revert_job`（811）都沒查登記表；**完成、到期、取消、退回皆無掛鉤**。

| 接入點 | 現況 | 可用度 |
|---|---|---|
| `on_tasks_activated` | 有在用（task_execution_service.py:94/122） | 直接用 |
| `status_machine` | 有申報、平台不執法 | 要改 |
| `config_schema` | 有申報、平台不讀、只到租戶層級 | 要改 |
| 任務上存設定 | 無欄位；只有檢測綁定表 tool_params | 沒有 |
| 完成／到期／取消掛鉤 | 無 | 沒有 |
| general 型別申報 | 平台內建，BE 不申報 | 直接用（要掛自訂 spec 需 replace） |
| system_menus TASK_TYPE 與申報表一致性 | 五個 key 與三個申報型別對不上 | 要改（或釐清用途） |

## ③ 排程 {#scheduler nav="③ 排程"}

### 現有 12 支 job（`core/scheduler.py`）

| job | trigger | 頻率 | 行 |
|---|---|---|---|
| drive_sync_worker | interval | 5 秒 | 184-191 |
| webhook_channel_renewer | interval | 6 小時（說明寫 24h，已過期，144） | 222-229 |
| framework_parse_job_cleanup | cron | 每天 01:00 UTC | 258-266 |
| ai_call_log_raw_purge | cron | 每天 03:00 **Asia/Taipei**（唯一指定時區） | 296-304 |
| license_expiry_state_machine | cron | 每天 02:00 UTC | 354-362 |
| agent_cert_expiry_notice | cron | 每天 02:10 UTC | 386-394 |
| tamper_fs_db_sync | date | 開機跑一次 | 440-445 |
| detection_execution_timeout | interval | 15 分鐘 | 485-492 |
| job_binding_orphan_cleanup | cron | 每天 03:10 UTC | 533-541 |
| log_partition_maintenance | cron | 每天 03:30 UTC | 586-594 |
| vulndb_auto_update | cron | 每天 04:10 UTC，只丟單進佇列 | 611-619 |
| integrity_spot_check | date 自排 | 每 4 小時＋0～60 分隨機 | 58-88 |

### 共通寫法

- `@_job_scoped("名稱")`（24-39，log 帶 `job-<名>-<hex>`）→ `try:` → `with app.app_context():` → 函式內 import `system_context` → 取 service → `with system_context("名稱")`（有時加 `session_scope()`）→ 呼叫 → `except` 記 `logger.exception`。全部 `max_instances=1, coalesce=True`。
- 取 service 兩種：DI 容器 `app.extensions.get('di_container').<container>.<service>()`（167-176）；套件 job 用 `app.extensions[HANDLE_KEY]`（374-376、334）。
- 執行緒池預設 4（157；讀 `DRIVE_SYNC_WORKER_POOL_SIZE`，`core/app_factory.py:366-367`）。

### 誰起排程、會不會重複跑

- **只有 `RUN_MODE=api` 起業務 job**（`main.py:153,161-165`；模式表 `main.py:13`）。socketio／worker 只起完整性抽查（`main.py:204-210`、`core/scheduler.py:111-139`）。
- 單一 api 行程只跑一份（只在 gunicorn master，`main.py:155-160`、`core/scheduler.py:558-559`）。
- **多個 api 副本之間沒有分散式鎖**（advisory lock 只在 `infra/setup/setup_provision_lock.py:21`）；防重全靠各 job 冪等，例如 `cert_expiry_notified_at` 條件更新（`core/scheduler.py:365-366`）。

### background_jobs（RUN_MODE=worker）

- 入列 `BackgroundJobAppService.enqueue(job_type, payload, max_attempts=3)`，**需要登入者與租戶**，會存建單者身分（`app/background_job/service/background_job_app_service.py:55-71`）。
- handler 註冊 `@register_job_handler(job_type)`，重名直接 raise（`app/background_job/handler_registry.py:25-32`）；現有 4 支（`core/plugins/_source_scan_vulndb.py:87`、`_source_scan_vulndb_fetch.py:146`、`_evidence_classification_job_queue.py:27`、`source_scan.py:142`）。
- 撿單每 `WORKER_POLL_SECONDS`（預設 2 秒），有租約與心跳（`app/background_job/worker_runner.py:28-61`）；`FOR UPDATE SKIP LOCKED`（`infra/background_job/repository/background_job_repo_impl.py:32`）。
- **無延後執行欄位**（DEV 欄位：`id, uid, tenant_id, org_unit_id, job_type, payload, status, attempts, max_attempts, locked_by, locked_at, progress, result, error, created_user, created_at, updated_at`）。
- DEV 現況：sbom_scan done 32／failed 6、vulndb_apply done 2／failed 7、evidence_classification done 8、vulndb_check_update done 2。

### 分工原則（已成文）

- 長工作（幾分鐘以上、不能被 API 重啟打斷）走 `background_jobs`，禁止自起 Thread 或另開佇列表（`docs/claude/domain-capabilities.md:60`）。
- 先例：APScheduler 只負責定時丟單、worker 執行（`core/scheduler.py:597-598`、`core/plugins/_source_scan_vulndb_fetch.py:127-143`）；排程裡入列要先用 `tenant_context(ROOT)` 補身分。
- 每小時以內的先例：5 秒、15 分鐘；15 分鐘那支有註解說明為何用排程而非查詢時順手處理（447-457）。

### 時區

- scheduler 預設 UTC（158），cron 只有一支個別指定 Asia/Taipei（301）。
- `common/util/common_util.py:36` 寫死 `BUSINESS_TZ = ZoneInfo("Asia/Taipei")`，`business_now()`（39-50）。
- DEV `users` 表沒有語系／時區欄位（只有 `email`）。

| 接入點 | 現況 | 可用度 |
|---|---|---|
| APScheduler cron/interval（api 模式） | 12 支、寫法統一、預設 UTC | 直接用 |
| 每小時級 tick | 有 5 秒、15 分鐘先例 | 直接用 |
| 多副本防重跑 | 無分散式鎖，靠冪等 | 沒有 |
| background_jobs 佇列 | enqueue＋handler、SKIP LOCKED、租約；入列要租戶身分 | 直接用（排程裡要包 tenant_context） |
| 佇列延後執行 | 無欄位 | 沒有 |
| 營運時區 | `BUSINESS_TZ` 寫死；cron 可個別指定 | 直接用（但非租戶層級） |
| 租戶／使用者時區 | 無 | 沒有 |

## ④ 通知 {#notify nav="④ 通知"}

### NotificationService（`app/notification/service/notification_service.py:22`）

- `send_mail_notification(to, subject, content, is_html=False)`（75-84）：依收件人網域挑 ROOT 租戶 SMTP 設定（`smtp_config_picker.py:25-34`），未啟用回 False 不丟例外，**背景可用、不需登入者**。
- `send_discord_notification(message, cfg=None)`（86-99）、`send_telegram_notification(content, cfg=None)`（101-117）。
- 底層都到 jedi-notification `NotificationService(provider, config).send_notification(dto)` 回 bool（`pkg/jedi-notification/jedi_notification/app/service/notification_service.py:7-17`）；SMTP 實作 `infra/smtp_mail/smtp_mail_adapter.py:27-102`，失敗只寫 log。

### 專案啟動信（`app/flow_control/service/project_service.py:642-780`）

- 收件人兩批：專案參與者（角色 manager／reviewer／auditor／viewer，667-708）；任務指派人按人彙總待辦數（711-746）。email 取自 `get_user_by_ids` 的 `user.email`（683）。
- 有收 `notified_emails`（687,707）但第二批沒拿來去重——**同時是參與者與指派人的人會收兩封**。
- Discord／Telegram 設定要在主執行緒先讀（661-662）；每封各開 `threading.Thread` 寄（752-787），無重試、回傳值丟掉。
- 語系：`flask_babel.gettext`（10）走 `select_locale`，有 request 用觸發者 Accept-Language、沒 request 用預設 `zh_Hant_TW`（`core/app_factory.py:121-133`、`config/config.py:176`）——**語系跟觸發者走，不是收件人**；使用者表沒有語系欄位。
- 範本：沒有範本引擎，主旨與內文直接是 `.po` 字串（`config/translations/zh_Hant_TW/LC_MESSAGES/messages.po:106-116`，en 同名檔）；聊天通道另有通用文案（`app/notification/service/chat_notice.py:12-25`）。

### 任務指派／完成現在寄什麼

- 新任務 `notify_user_todo_job`（`workflow_execution_service.py:1240-1300`）寄給該 job 的每位指派人，主旨 `new_task_notification_subject`。觸發點：完成上一步（806）、退回（887）、強制開始（`job_force_start_service.py:95`）、`job_service.py:506`。
- 批次完成（`job_batch_complete_service.py:137-203`；收件人查詢 `infra/readmodel/tasks/job_batch_complete_query.py:186-245`）寄專案 manager／reviewer＋下一步 PROCESSING 任務指派人；單筆通知以 `suppress_notify=True` 關掉（99）。
- `notify_users_batch_assigned`（`workflow_execution_service.py:1193`）無呼叫者，死碼。
- 檢測工具整組完成另有 `_notify_group_result`（見 ⑤）。

### 通知設定

- DEV `system_configs`：`NOTIFY_CONFIG` 只有 `DISCORD`（`{"secret": "", "enabled": false}`）與 `TELEGRAM`（`{"chat_id": …, "enabled": false}`）兩個 key；`SMTP` 組有 `*` 與 `gmail.com`。
- `secret` 加密存放（`infra/system_config/config_secret_crypto.py:28`、`infra/notification/tenant_notify_config_reader.py:26-37`）。
- 兩種讀法：有登入者 `get_channel_config()`（無身分直接 raise，`notification_service.py:33-50`）；背景用 `read_tenant_notify_config_value(tenant_id, channel)`（先例 `core/plugins/detection.py:97-108`）。
- **沒有「哪些事件要通知誰」或「多久提醒一次」的設定**。

### 寄信紀錄

- DEV 搜表名含 notif／mail／email／message／sent／bulletin／inbox：只有 `public.bulletins`、`public.bulletin_org_units`。
- BE 與 jedi-notification grep `notification_log|mail_log|notify_log|notification_history|notified_at`：只有 `remote_agents.cert_expiry_notified_at` 單欄去重戳記（`scripts/sql/packages/jedi_remote_agent/007-remote-agent-cert-expiry.sql:12`）。jedi-notification 沒有任何資料表。**確認：沒有寄信紀錄表。**

### 站內通知

- 沒有收件匣／未讀。最接近的 jedi-bulletin 只有部門層級公告、有上下架時間、無個人已讀（`pkg/jedi-bulletin/jedi_bulletin/infra/models/bulletin.py:39-61`）。socketio 只登記問卷 namespace（`config/socketio_namespaces.py:41-47`）。

| 接入點 | 現況 | 可用度 |
|---|---|---|
| `send_mail_notification` | 背景可用；SMTP 讀 ROOT 設定 | 直接用 |
| Discord／Telegram | 背景走 `read_tenant_notify_config_value` | 直接用 |
| 專案啟動收件人邏輯 | 寫死在 `_notify_project_started`，參與者與指派人重複寄 | 要改 |
| 指派／完成通知 | 新任務寄指派人；批次完成寄 manager／reviewer＋下一步指派人 | 直接用（都在同步動作當下觸發） |
| 信件語系 | 跟觸發者 request；排程裡固定 zh_Hant_TW；無使用者語系欄位 | 要改 |
| 信件範本 | `.po` 字串＋HTML 跳脫 | 直接用（新事件要補 msgid） |
| 通知事件設定（開關、對象、頻率） | NOTIFY_CONFIG 只有管道設定 | 沒有 |
| 寄信／通知紀錄表 | BE 與套件都沒有 | 沒有 |
| 站內通知／個人收件匣 | 無；bulletin 是部門公告 | 沒有 |

## ⑤ 掃描觸發 {#scan nav="⑤ 掃描觸發"}

### 檢測工具：第一次掃描怎麼派

- 時機：「開始執行任務」→ `start_task_execution` 把已指派待辦任務改成執行中，再呼叫 `_run_activation_hooks`（`pkg/jedi-task-platform/.../task_execution_service.py:60-94`）。
- 檢測工具申報 `_auto_dispatch_first_scans`（`app/detection_tools/task_type_declaration.py:43,108`）；每支掛鉤、每筆任務各包 SAVEPOINT（`task_execution_service.py:124-133`、`task_type_declaration.py:85-96`），失敗不擋。
- **只派一次**：只挑「完全沒有執行紀錄」的任務（`get_detection_jobs_without_execution`，`task_type_declaration.py:79`）。
- 派工：`orchestration.start_execution`（87），要求任務在執行中（`detection_orchestration_service.py:212`）；每組指派建一張 `agent_tasks`（pending）＋一筆 `detection_executions`（`_dispatch_one`，515-536）。

### `agent_tasks.scheduled_at`

- 空值＝立即可領；有值＝到時間才可領（`pkg/jedi-remote-agent/.../model/agent_task.py:59-65`）。
- 值從指派列 `job_execution_detection_tool_agents.scheduled_at` 在派工當下複製（`detection_orchestration_service.py:512-522`；欄位 `pkg/jedi-detection/.../job_execution_detection_tool_agent.py:42-44`）。未來時間的執行紀錄狀態為 scheduled（533-534）。單組重跑強制立即（512）。
- **agent 拉單有過濾**：`list_dispatchable_for_agent` 只撈 pending 且 `scheduled_at IS NULL OR scheduled_at <= now()`，用資料庫時鐘（`agent_task_repo_impl.py:18-33`）。「立即開始」端點改成 now（`agent_task_domain_service.py:148-156`）；逾時判斷跳過未到時的單（`detection_orchestration_service.py:1283-1297`）。
- DEV：`agent_tasks` succeeded 49／failed 25／cancelled 5，只有 1 筆用過 `scheduled_at`；指派列 21 筆，0 筆設了 `scheduled_at`。

### SBOM 原始碼掃描（伺服器端）

- 分岔：工具 `execution_target` 是 server 改走伺服器端派工（`detection_orchestration_service.py:220`、`server_side_dispatch.py:65-67`），建執行群組與不掛 agent 工單的執行紀錄，再呼叫登記好的處理函式（`server_side_dispatch.py:70-111`）。
- `dispatch_sbom_scan`（工具代號 `sbom-scan`）把 `execution_uid` 寫進 `source_scans.detection_execution_uid` 並建掃描列（`pkg/jedi-source-scan/.../detection_dispatch_handler.py:14-32`）→ 排進 background_jobs（`source_scan_app_service.py:153`）→ BE `_run_sbom_scan` 執行（`core/plugins/source_scan.py:143-147`）。
- **伺服器端沒有 scheduled_at 可用**（`server_side_dispatch.py:108`），背景佇列也沒有延後機制。
- **重新執行沿用上次的源碼包**：伺服器端派工沒帶新檔時，取綁定 `tool_params` 裡上次那份的 uid（`pkg/jedi-detection/.../server_side_dispatch.py:118-137`），建掃描列後背景工作以該掃描的 `input_file_id` 重掃（`pkg/jedi-source-scan/jedi_source_scan/app/service/source_scan_job_steps.py:102`）。任務抽屜對應顯示「已上傳（沿用上次）」（FE `src/config/locales/i18n/zh-tw/my-tasks.json:138` `source_file_reused`）。**週期自動掃走同一條路：不帶新檔派工，即沿用上次的 `input_file_id`。**
- 從未上傳過源碼包的任務，首次執行回 400（`detection_dispatch_handler.py:24`）；檢測任務啟動掛鉤也會走到這條，被 SAVEPOINT 吞成 warning。
- `list_task_scans` 只列清單，靠 `task_context.resolve` 用 `detection_execution_uid` 反查任務／專案，失敗就全列（`task_scan_app_service.py:28-55`）；BE 實作 `ReadmodelTaskContextResolver`（`core/plugins/source_scan.py:177`）。

### 「掃完」的事件

- 檢測工具：`AgentTaskService.receive_result` → listener `on_task_succeeded`／`on_task_failed`（`agent_task_service.py:155-169`）；port `IAgentTaskLifecycleListener` 可抽換，BE 的 `LazyLifecycleListener` → `DetectionLifecycleListener` → `on_scan_succeeded`／`on_scan_failed`（`core/plugins/remote_agent.py:47-55`、`infra/remote_agent/adapter/detection_lifecycle_listener.py:24-37`）。
- SBOM：`SbomScanJob.run` 跑完對有 `detection_execution_uid` 的掃描呼叫 `DetectionResultForwarder.succeed`／`fail`（`sbom_scan_job.py:111-128`）→ `server_side_result.complete`／`fail`（`server_side_result.py:28-49`）。
- **兩條路匯合在 `_close_group_if_terminal`**（`detection_orchestration_service.py:1626-1675`）：彙總狀態、auto 完成模式下完成任務、`_notify_group_result` 發整組彙總通知（Email／Telegram／Discord，2076）。
- **完全不看嚴重度**，也沒有可外掛的 event／callback；要加門檻只能改這段。

### 嚴重度欄位

- `source_scan_findings.severity`：`String(16)` 可空（`source_scan_models.py:87`），值域 CRITICAL／HIGH／MEDIUM／LOW／NEGLIGIBLE／UNKNOWN，不在清單內存 UNKNOWN（`raw_finding.py:15,76-78`）。DEV 分布 MEDIUM 403／HIGH 371／CRITICAL 96／LOW 93。掃描摘要 `summary.severity` 是各工具小寫計數（`summary.py:14-21,33`）。
- 檢測工具：`detection_executions` **沒有等級欄位**（DEV 欄位只有 `summary` JSONB），鍵依工具而異（`detection_execution.py:38`、`detection_orchestration_service.py:2440-2454`）。有等級的是基準檔控制項 `detection_profile_controls.severity_norm`（none／low／medium／high／critical，無 CHECK，`detection_profile_control.py:44-51`、`inspec.py:286-311`）——那是基準定義，不是單次掃描結果。DEV `detection_executions` succeeded 52／failed 23／cancelled 5。

| 接入點 | 現況 | 可用度 |
|---|---|---|
| 啟動掛鉤 `on_tasks_activated` | 「開始執行任務」後呼叫 | 直接用 |
| `_auto_dispatch_first_scans` | 只派沒有執行紀錄的任務、只派一次 | 要改（週期重派） |
| `agent_tasks.scheduled_at` | 拉單過濾 `<= now()`，可排未來 | 直接用 |
| 指派列 scheduled_at → 工單 | 派工當下複製 | 直接用 |
| 伺服器端（SBOM）排程時間 | 固定空值，佇列無延後 | 沒有 |
| SBOM 週期掃描的源碼來源 | 不帶新檔即沿用上次的 `input_file_id`（server_side_dispatch.py:118-137）；從未上傳過才回 400 | 直接用 |
| agent 掃完 listener port | `IAgentTaskLifecycleListener` 可抽換 | 要改（加門檻判斷） |
| SBOM 掃完轉回檢測 | `DetectionResultForwarder` → `server_side_result` | 要改 |
| 收口與彙總通知 | `_close_group_if_terminal`／`_notify_group_result`，不看嚴重度 | 要改 |
| 嚴重度門檻通知 | 不存在 | 沒有 |
| SBOM 嚴重度 | findings.severity 六級大寫＋summary 計數 | 直接用 |
| 檢測工具單次結果等級 | 只有工具各自 summary JSONB | 沒有 |

## ⑥ 資源庫 frequency 欄 {#frequency nav="⑥ frequency"}

### DEV 值分布（`compliance.module_frames`，174 列，其中 155 列 `is_delete=1`）

| frequency | 全部 | 未刪除 |
|---|---|---|
| annually | 77 | 9 |
| daily | 45 | 2 |
| none | 32 | 5 |
| semi_annually | 8 | 2 |
| QUARTERLY | 4 | 0 |
| weekly | 3 | 0 |
| quarterly | 2 | 1 |
| monthly | 2 | 0 |
| annual | 1 | 0 |

- 大小寫混雜（`QUARTERLY`）與拼法不一（`annual`）**全在已刪除列**；未刪除的 19 列都是小寫標準值＋`none`。
- 同表 `last_execute_date`／`next_execute_date`：**174 列全為 NULL**，ORM 有欄位（`infra/module_frame/models/module_frame.py:28-29`）、DTO／serializer 有回傳，但 grep 不到任何寫入者。
- `frequency` 欄型別 `varchar(255) NOT NULL`（model:26）。

### 寫入點

- `app/module_frame/service/module_frame_item_service.py:106,221`（`payload.get("frequency", "")`，空字串當預設）。
- `app/module_frame/service/module_frame_service.py:105`（逐欄更新）。
- `app/module_frame/service/module_frame_import_service.py:176,200,302,337`（Excel 匯入，欄名 `Frequency`；範本定義 `app/module_frame/excel_template/sheet_definitions.py:94` 必填）。
- `app/oscal/service/ssp_excel_import_app_service.py:573`、`app/oscal/service/resource_library_app_service.py:459-477,551-557`（資源庫建立，缺值寫 `none`）、`api/oscal/routes/resource_library_route.py:62`。

### 讀取點

- 只有回傳：`resource_library_app_service.py:98`、`app/module_frame/dto/module_frame_dto.py:16,50`、`ssp_import_template_app_service.py:1059`、`api/module_frame/serializers/module_frame.py:67,87,112`。
- 套件 `pkg/jedi-compliance-audit/.../flow_control_project_mapper.py:87` 把它帶成專案的 `module_frame_frequency`（`project_dto.py:76,132`），FE grep 不到任何使用者。
- **沒有任何排程、任務生成、提醒邏輯讀它。**

### FE 寫死的選項

- 六個值 daily／weekly／monthly／quarterly／semi_annually／annually（不含 `none`）在四處寫死：`src/stores/menuStore.js:168-174`（`fetchFrequencyMenu` 是假 fetch）、`src/components/grc/ModuleFrameBasicInfoPanel.vue:171-176`（預設 `annually`，51/98）、`src/components/grc/ssp-docx-import-v2/SspDocxImportPage.vue:1133-1138`、`src/components/grc/ssp-excel-import/SheetPreviewBasic.vue:53-58`。
- DEV `system_menus` **沒有** frequency 相關 group。依 CLAUDE.md「下拉選單進 system_menus」鐵則，這四處屬違規寫死。

| 接入點 | 現況 | 可用度 |
|---|---|---|
| `module_frames.frequency` 值 | 未刪除列皆小寫標準值＋none；髒值只在已刪除列 | 直接用（讀值） |
| 頻率選項清單 | FE 四處寫死、system_menus 無對應 group | 要改 |
| `last_execute_date`／`next_execute_date` | 欄位在、全 NULL、無寫入者 | 要改（或棄用） |
| frequency → 專案 | 套件 mapper 帶 `module_frame_frequency`，無人消費 | 要改 |
| frequency 當排程依據 | 無任何讀取端 | 沒有 |

## ⑦ 問卷任務 {#survey nav="⑦ 問卷任務"}

### 怎麼建

- 型別申報：`survey`，有狀態機與 `review_required`，**無啟動掛鉤**（`pkg/jedi-survey/.../task_type_declaration.py:39,62-71`）。
- 路徑一（規劃頁）：`job_service` 建任務時呼叫 `_replace_survey_snapshots`、`_reconcile_task_surveys`（`app/flow_control/service/job_service.py:233-237`；更新 352,380）。
- 路徑二（BPMN）：`actionType=='survey'` → `process_survey_property`＋`link_task_survey`（`app/flow_engine/service/workflow_execution_service.py:394,1306-1334`），**直接引用問卷定義、不建快照**。

### 結構

- 任務掛一筆 `job_evidences`（type SURVEY），`ref_id` 指向快照問卷（`flow_control_job_repo_impl.py:850-874`）。
- `survey.task_surveys`：問卷×設備一筆，即「問卷實例」（`survey_handler.py:82-131`）；DEV 共 5 筆。
- 回覆在 `question_answers`（以 `task_survey_id` 關聯）＋`question_answer_histories`（`question_answer_service.py:164-224`）。
- 規劃頁路徑深度複製：`create_snapshot` 複製 survey、頁、題目、翻譯，`is_snapshot=1`（`survey_snapshot_domain_service.py:36-71`）；BPMN 路徑引用原問卷（`task_survey_service.py:177-217`）。

### 怎麼算完成

- 送答案 `update_task_survey_answer` 寫答案並改 `task_surveys.status`（`question_answer_service.py:114-195`）；狀態 0 未填／1 編輯中／2 待審／3 補充說明／9 完成（`task_survey_code.py:3-8`），合法轉換 `task_survey_status_transition.py:30-36`。
- `next_status_on_answer_complete`（`task_type_declaration.py:116-124`）定義了填完該進 2 或 9，**無呼叫端**，目前由前端直接送 status 值。
- 任務層：批次完成檢查該任務所有 `task_surveys` 最小 status＝9（`infra/readmodel/tasks/job_batch_complete_query.py:158-174`、`job_batch_complete_service.py:85`）才 `complete_job`。**填完問卷不會自動完成任務。**

### 現況下「重開」各要動什麼（只描述結構）

- 重開任務：workflow 退回把任務改回執行中，並以 `batch_set_task_survey_status` 把所有問卷實例設成 1，**答案保留不清**（`workflow_execution_service.py:877-881`、`task_survey_service.py:476-508`）。
- 重開單份回覆：狀態機規定 9 不能轉出（`task_survey_status_transition.py:35`），只有批次系統路徑與 Excel 匯入能繞過。換問卷是軟刪舊實例、另建新的，舊答案保留（`task_survey_service.py:129-153`）；已有答案的快照不能移除，回 409（`survey_handler.py:181-183`）。
- 覆核輪會把 `task_surveys` 與 `question_answers` 一起複製到新任務，標 `inherited_from_round_id`（`infra/flow_control/repository/reverify_clone_query.py:4-41`）——是「帶舊答案的新一份」的現成先例，但綁覆核輪。

| 接入點 | 現況 | 可用度 |
|---|---|---|
| 問卷快照 `create_snapshot` | 規劃頁路徑深度複製；BPMN 路徑引用 | 直接用 |
| 填完問卷 → 任務完成 | 只在批次完成時檢查全 9，無自動轉態 | 沒有 |
| `next_status_on_answer_complete` | 已定義、無呼叫端 | 要改（接線） |
| 重開任務（退回＋問卷改 1） | workflow 退回路徑 | 直接用（答案保留） |
| 重開單份回覆（9 轉出） | 狀態機禁止 | 要改 |
| 複製問卷＋答案到新任務 | reverify_clone_query | 要改（綁覆核輪） |

## 首腦要裁的題目 {#decisions nav="待裁"}

只列問題，不給設計。

1. **「重開」的語意**：週期到了是在同一筆任務上退回重做（保留歷史在同一列），還是生一筆新任務（每期一筆）？前者沿用退回路徑但歷次結果會疊在同一筆；後者沒有單筆複製方法，要從覆核輪承襲邏輯抽出或另建。
2. **新的一期歸屬哪一輪**：任務屬於哪一輪記在 `workflow_execution_control_mapping.round_id`。週期新生的任務算同一輪、還是要有「週期」這個新的歸屬概念？與覆核輪的母輪關係怎麼分？
3. **週期設定存在哪一層**：任務上沒有欄位、`config_schema` 只到租戶層且平台不讀。是任務層新欄位、型別申報層、還是專案層？資源庫 `frequency` 只當建專案時的預設帶入，還是也要能影響既有專案？
4. **完成／到期掛鉤要不要進平台**：申報表目前只有啟動掛鉤。週期判斷、到期提醒、完成後排下一期，要不要比照 `on_tasks_activated` 加申報掛鉤（屬 jedi-task-platform 套件異動），還是在 BE 排程裡直接查？
5. 第 5 題作廢，見 [⑤ 掃描觸發](#scan) 節。
6. **伺服器端掃描怎麼排未來時間**：檢測工具有 `scheduled_at`，SBOM 走的伺服器端派工與 `background_jobs` 都沒有延後欄位。兩種掃描要不要用同一套排程機制？
7. **門檻通知的「門檻」用什麼等級**：SBOM 有標準六級；檢測工具單次結果沒有等級欄，只有各工具不同鍵的 summary。第一版門檻只做 SBOM，還是要先把檢測結果正規化出等級？
8. **門檻通知比對什麼**：「掃到 high 以上就通知」是每次都通知、還是只在「新出現」或「比上次多」時通知？（現有 `_notify_group_result` 每組必發一封、不看內容。）
9. **通知紀錄表的範圍**：要記的是「哪次掃描、什麼等級、何時通知誰」。只記門檻通知，還是把現有指派／完成／專案啟動信也一起納入？表放 BE 還是 jedi-notification 套件（套件目前沒有任何表，屬套件異動）？
10. **收件人語系與時區**：使用者沒有語系／時區欄位，排程裡寄信固定 zh_Hant_TW、時區 Asia/Taipei 寫死。第一版接受現況，還是要先補使用者或租戶層級的語系／時區？
11. **多副本重複觸發**：APScheduler 多個 api 副本之間沒有鎖，現有 job 都靠冪等。週期重開與門檻通知的冪等依據（例如「這一期已開過」「這次掃描已通知過」）要靠什麼欄位？還是第一版只承諾單副本？
12. **頻率選項與正規化**：FE 四處寫死六個頻率值、`system_menus` 無對應 group；同表 `last_execute_date`／`next_execute_date` 全 NULL 無人寫。頻率是否進 `system_menus`、這兩個閒置欄要啟用還是棄用？另外 `system_menus` 的 `TASK_TYPE` 五個 key 與申報表三個型別對不上，是否一併釐清？
13. **問卷週期重開帶不帶舊答案**：退回路徑保留答案、狀態機禁止 9 轉出、覆核輪複製會帶舊答案。週期新一期問卷要空白重填、帶上期答案當預填，還是由設定決定？填完問卷不會自動完成任務這件事要不要一起處理？

