# FR-056.2 任務類型「檢測工具執行」+ 參數 Implementation Plan

> **For agentic workers / runner:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. Steps use `- [ ]` checkbox syntax.
>
> **依賴：FR-056.1（需 detection_tools 目錄 API）。可與 FR-056.3 並行。** 母案設計見 [`design.md`](./design.md)，本階段設計見 design §8 的 FR-056.2 段。

**Goal:** 任務設置頁新增第三種任務類型「檢測工具執行」，選定後可綁定「用哪個檢測工具 + 掃描參數 + 完成模式」，且同步寫回範本 BPMN userTask。此階段只做**設定期**（存得下、讀得出、重開還在），實際「執行」在 FR-056.4 才接。

**Architecture:** 沿用既有問卷（survey）任務的三段式：(1) `GrcJobType` 加 `detection_tool` 值；(2) BPMN userTask 同步——job_type 寫進 `actionType`、工具選擇寫進 `actionInfo`、參數與完成模式各寫一個新 camunda:property；(3) 綁定資料落一張**簡單的單列 side-table** `config.job_execution_detection_tools`（mirror `TaskSurvey` 的形狀，但**不做** survey 的 snapshot + device×department 笛卡兒積 fan-out——一個任務一個工具一組參數，過度工程無必要）。FE 任務設置頁加第三個 job type 選項 + 工具選擇 + 動態參數表單 + 完成模式選擇。

**Tech Stack:** 同 FR-056.1（BE DDD / marshmallow / pytest；FE Vue3 / PrimeVue）。

---

## 前置：關鍵決策與發現（runner 動工前必讀）

| 項目 | 定案 | 來源/理由 |
|------|------|-----------|
| GrcJobType 新值 | `DETECTION_TOOL = "detection_tool"` | 明確語意；不複用 `ActionType.TOOL_EXECUTION`（見下） |
| `ActionType.TOOL_EXECUTION` | **不直接複用**其值 `"tool_execution"`——它是 dormant placeholder（零使用），但 GrcJobType 與 ActionType 是兩個獨立 enum、不自動同步 | Explore：兩 enum 非同步；用 `detection_tool` 語意更準 |
| 綁定資料表 | 新建 `config.job_execution_detection_tools`（單列/任務），**不走** survey 的 snapshot + 笛卡兒積 | survey 機制對「單工具+參數+flag」過重，比例不當 |
| BPMN property | `actionType`=job_type、`actionInfo`=tool_uid、新增 `toolParams`（JSON-encoded）、`completionMode` | mirror survey 的 actionInfo + `sp_properties` JSON-property pattern |
| 完成模式預設 | `manual`（系統掃完不自動按完成，任務留 PROCESSING 等人工），另一值 `auto`（自動 complete_job） | design D8 |
| **完成模式不是狀態** | `completion_mode` 只是存在綁定表的 flag，供 FR-056.4 決定掃完要不要自動呼叫 complete_job。**不新增 JobStatus、不動 jedi_flow_engine**。本階段只存這個值，不做任何狀態邏輯 | user 澄清 2026-07-26 |
| org_unit | 不涉及（此階段純任務綁定） | — |

**兩陷阱同 FR-056.1：** QueryEntity 全 `Optional=None`（幽靈 WHERE）、Entity 可更新欄位預設 `None`（幽靈覆寫）。

**BPMN 同步的既有紀律（務必守）：** `_sync_task_to_template_xml()` 的 `actionInfo` 是 overloaded 欄位——survey 任務存 survey uid，且程式碼註解明言「非 survey 任務的 actionInfo 保留給工具選擇」。detection_tool 正是那個「工具選擇」用途。傳值時只在 `job_type == "detection_tool"` 時帶 `tool_action_info`，避免誤蓋別型任務的 actionInfo。

---

## Task 分佈（對應 design T-2.x）

| Task | 對應 | 產出 | 依賴 |
|------|------|------|------|
| Task 1 | T-2.1 | GrcJobType 加值 + BPMN userTask 同步線擴充 | FR-056.1 完成 |
| Task 2 | T-2.2 | job_execution_detection_tools 綁定表 + param_schemas 讀取 + 完成模式，全 DDD 層 | Task 1 |
| Task 3 | T-2.3 | FE 任務設置頁加 detection_tool 類型 + 工具/參數/完成模式 UI | Task 2 |

---

## Task 1（T-2.1）: GrcJobType 加值 + BPMN userTask 同步線

**Files:**
- Modify: `common/enum/grc_job_type_enum.py`
- Modify: `infra/grc/repository/grc_job_repo_impl.py`（`_sync_task_to_template_xml` + caller `update_job`）
- Modify: `api/grc/serializers/job.py`（兩個 validate.OneOf 自動涵蓋，但要加 detection 專屬欄位）
- Test: `test/test_grc_job_detection_type.py`

- [ ] **Step 1: 加 enum 值**

`common/enum/grc_job_type_enum.py`：
```python
class GrcJobType(StrEnum):
    GENERAL = "general"
    SURVEY = "survey"
    DETECTION_TOOL = "detection_tool"   # FR-056.2
```
> 加值後這些 call site 自動涵蓋（都用 `[e.value for e in GrcJobType]` 或 `_value2member_map_`，非硬編碼二值清單）：`api/grc/serializers/job.py:142,177`（OneOf）、`app/grc/dto/job_dto.py:176,207`（from_view_row）、`infra/grc/repository/grc_job_repo_impl.py:200,236`。**唯一要人工處理**：`infra/grc/mapper/grc_job_mapper.py:24` 的 `to_entity` 用 `SURVEY if has_surveys else GENERAL` 啟發式——這是「從資料反推 job_type」的舊邏輯，detection_tool 的反推放到 Task 2（有綁定表後才能判斷），本 Task 先不動 mapper 啟發式。

- [ ] **Step 2: 寫 failing test（BPMN 同步帶 detection tool 資訊）**

`test/test_grc_job_detection_type.py`：驗證 `_sync_task_to_template_xml` 在 job_type=detection_tool 時，把 tool_uid 寫進 actionInfo、params 寫進 toolParams property。用既有 grc job repo test 的 XML 斷言慣例（runner 先看 `test/` 下既有 `_sync_task_to_template_xml` 相關 test 怎麼組 template xml fixture）。

- [ ] **Step 3: 跑 test 確認失敗** → FAIL。

- [ ] **Step 4: 擴充 `_sync_task_to_template_xml`**

在 `infra/grc/repository/grc_job_repo_impl.py` 的 `field_map` 建構處，加 detection tool 專屬 property（mirror survey_action_info 的條件寫入）：
```python
def _sync_task_to_template_xml(self, job, name, description, guide, job_type,
                               survey_action_info=None,
                               tool_action_info=None,      # detection tool_uid
                               tool_params=None,           # dict → JSON-encode
                               completion_mode=None):
    ...
    if job_type is not None: field_map["actionType"] = job_type or "general"
    if survey_action_info is not None: field_map["actionInfo"] = survey_action_info
    if tool_action_info is not None: field_map["actionInfo"] = tool_action_info   # 工具型任務借用 actionInfo
    if tool_params is not None: field_map["toolParams"] = json.dumps(tool_params, ensure_ascii=False)
    if completion_mode is not None: field_map["completionMode"] = completion_mode
```
> 注意：`actionInfo` 被 survey 與 tool 共用，但同一任務只會是其中一型，不衝突。caller 必須確保只在對應 job_type 時帶對應參數。

- [ ] **Step 5: 擴充 caller `update_job`**

在 `update_job`（~line 487）計算 `survey_action_info` 的相鄰處，加 detection 分支：
```python
tool_action_info = None
tool_params = None
completion_mode = None
if job_type == GrcJobType.DETECTION_TOOL.value:
    tool_action_info = (tool or {}).get("uid", "")   # tool 為新傳入參數
    tool_params = (tool or {}).get("params")
    completion_mode = (tool or {}).get("completion_mode")
self._sync_task_to_template_xml(job, name, description, guide, job_type,
                                survey_action_info=survey_action_info,
                                tool_action_info=tool_action_info,
                                tool_params=tool_params,
                                completion_mode=completion_mode)
```

- [ ] **Step 6: 跑 test 確認通過** → PASS。

- [ ] **Step 7: Commit**
```bash
git add common/enum/grc_job_type_enum.py infra/grc/repository/grc_job_repo_impl.py test/test_grc_job_detection_type.py
git commit -m "feat(fr056): add detection_tool job type + BPMN sync (T-2.1)

GrcJobType 加 detection_tool；_sync_task_to_template_xml 支援 tool_uid(actionInfo)/toolParams/completionMode 寫回 BPMN userTask。"
```

---

## Task 2（T-2.2）: 綁定表 + 參數 schema 讀取 + 完成模式（DDD 全層）

**說明：** 建 `config.job_execution_detection_tools` 單列綁定表（DDD 九層，複製 remote_agent 骨架）+ migration。app service 在 job create/update 時，若 job_type=detection_tool，reconcile 這張綁定表（mirror `_reconcile_task_surveys` 但簡化成單列 upsert）。另加 param_schemas 唯讀讀取（供 FE 拉某工具的參數欄位定義）。

**Files:**
- Create: `scripts/sql/2026-07-26-fr056-2-job-execution-detection-tools.sql`
- Create: infra/domain/app 全層 `job_execution_detection_tool` 模組檔（同 FR-056.1 附錄 A 骨架）
- Create: `domain/detection_tools/service/detection_tool_param_schema_domain_service.py`（param_schemas 唯讀）
- Modify: `app/grc/service/job_service.py`（加 `_replace_detection_tool_binding`）
- Modify: `api/grc/serializers/job.py`（加 tool 欄位）
- Modify: `api/detection_tools/routes/detection_tool_route.py`（加 GET param-schema endpoint）
- Test: `test/test_job_detection_tool_binding.py`

- [ ] **Step 1: migration 建綁定表**

`scripts/sql/2026-07-26-fr056-2-job-execution-detection-tools.sql`（照 sql-migration 慣例，GRANT/sequence/schema_migrations 齊）：
```sql
-- Date: 2026-07-26
-- FR-056.2 任務↔檢測工具綁定（單列/任務，非 survey 的笛卡兒積）
CREATE TABLE config.job_execution_detection_tools (
    id                 BIGSERIAL PRIMARY KEY,
    uid                VARCHAR(36)  NOT NULL UNIQUE,
    tenant_id          BIGINT       NOT NULL,
    job_execution_id   BIGINT       NOT NULL,             -- soft-ref → compliance.job_executions.id
    detection_tool_id  BIGINT       NOT NULL,             -- FK → config.detection_tools.id（同 schema）
    tenant_config_id   BIGINT,                            -- soft-ref → config.tenant_detection_tool_configs.id（用哪份租戶設定）
    tool_params        JSONB        NOT NULL DEFAULT '{}'::jsonb,
    completion_mode    VARCHAR(20)  NOT NULL DEFAULT 'manual',  -- auto / manual
    is_delete          BOOLEAN      NOT NULL DEFAULT FALSE,
    created_user       VARCHAR(255), updated_user VARCHAR(255),
    created_at         TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at         TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT fk_jedt_detection_tool FOREIGN KEY (detection_tool_id) REFERENCES config.detection_tools (id),
    CONSTRAINT uq_jedt_job UNIQUE (job_execution_id)      -- 一任務一綁定
);
COMMENT ON TABLE config.job_execution_detection_tools IS 'FR-056.2 任務↔檢測工具綁定（工具+參數快照+完成模式，單列/任務）';
GRANT SELECT, INSERT, UPDATE, DELETE ON config.job_execution_detection_tools TO cm_app;
GRANT USAGE, SELECT ON SEQUENCE config.job_execution_detection_tools_id_seq TO cm_app;
ALTER TABLE config.job_execution_detection_tools ENABLE ROW LEVEL SECURITY;
CREATE POLICY jedt_tenant_isolation ON config.job_execution_detection_tools
    USING (current_setting('app.is_super_admin', TRUE) = 'true'
           OR tenant_id = ANY (string_to_array(current_setting('app.allowed_tenant_paths', TRUE), ',')::BIGINT[]));
INSERT INTO public.schema_migrations(filename, note) VALUES
  ('2026-07-26-fr056-2-job-execution-detection-tools.sql', 'FR-056.2 任務檢測工具綁定表') ON CONFLICT (filename) DO NOTHING;
```
套進 DEV + 驗證（同 FR-056.1 Task 1 的 psql 驗證方式）。

- [ ] **Step 2: DDD 全層綁定模組**（複製 remote_agent 骨架，tenant-scoped model 繼承 `BaseModel, TenantScopedMixinModel`；entity/query 守兩陷阱）。

- [ ] **Step 3: param_schemas 唯讀 domain service + API**

加 `GET /detection-tools/<tool_uid>/param-schema` 回該工具當前生效版本（`is_current=True`）的 `param_schema`，供 FR-056.2 FE 拉參數欄位定義。

- [ ] **Step 4: 寫 failing test（綁定 upsert + partial update 不清空）**

驗證：(a) job_type=detection_tool + tool payload → 綁定表落一列；(b) 只改別欄位的 partial update 不清空綁定；(c) job_type 改回 general → 綁定標 is_delete。

- [ ] **Step 5: app service `_replace_detection_tool_binding`**

在 `app/grc/service/job_service.py`，mirror `_reconcile_task_surveys` 但簡化：
```python
def _replace_detection_tool_binding(self, job_uid, tool, curr_user, tenant_id):
    """單列 upsert（非笛卡兒積）。tool=None 且非 detection 型 → 不動；job_type 改離 detection → 標 is_delete。"""
    existing = self._jedt_domain.get_one(job_execution_uid=job_uid)
    if not tool:
        if existing: self._jedt_domain.soft_delete(existing.uid, curr_user)
        return
    if existing:
        self._jedt_domain.update_binding(JobExecutionDetectionToolEntity(
            uid=existing.uid, tool_params=tool.get("params"),
            completion_mode=tool.get("completion_mode"), updated_user=curr_user))
    else:
        self._jedt_domain.create_binding(JobExecutionDetectionToolEntity(
            uid=str(uuid.uuid4()), job_execution_id=..., detection_tool_id=...,
            tool_params=tool.get("params") or {}, completion_mode=tool.get("completion_mode") or "manual",
            tenant_id=tenant_id, created_user=curr_user, updated_user=curr_user))
```
在 `create_job` / `update_job` 於處理 survey 綁定的相鄰處，加 `if tool is not None or job_type == "detection_tool":` → 呼叫此方法。

- [ ] **Step 6: serializer 加欄位**

`JobCreateRequestSchema` / `JobUpdateRequestSchema` 加：
```python
tool = fields.Nested(DetectionToolBindingSchema, allow_none=True, load_default=None)
# DetectionToolBindingSchema: uid(工具 uid), params(Dict), completion_mode(OneOf['auto','manual'])
```
route 的 `**data` splat 自動把 `tool` 傳到 app service，app service 簽章加 `tool=None` kwarg。

- [ ] **Step 7: mapper 啟發式補 detection 反推**

`infra/grc/mapper/grc_job_mapper.py` 的 `to_entity`：`has_surveys` 啟發式旁加「有 detection binding → DETECTION_TOOL」判斷（若 mapper 拿得到綁定資訊；拿不到則靠 BPMN actionType 反推，讀路徑見 `ssp_control_implementation_service.py` 的 `props.get("actionType")`）。

- [ ] **Step 8: 跑 test 通過 + 手測** → 建一個 detection_tool 任務、綁 OpenVAS + 參數 + 完成模式 → 重開任務確認資料還在。

- [ ] **Step 9: Commit**
```bash
git commit -m "feat(fr056): job-detection-tool binding + param schema read (T-2.2)"
```

---

## Task 3（T-2.3）: FE 規劃頁任務設定加 detection_tool 類型

> **⚠️ 2026-07-26 落點修正（返工）**：本 Task 原指定 `TaskSetupView.vue`，經查證該頁是**斷頭半成品頁**——選單（DB `ui_routes`）無條目、程式內無任何導覽點、`fetchTree()` 打的 `GET /project/<uid>/task-setup/tree` 在 BE 根本沒註冊（只有 AP-scoped 版本）。使用者實際設定任務的頁面是 **`ProjectPlanningView.vue`**（規劃頁「控制項實作」Tab 點 AO 展開的任務面板，路由 `/project/projects/:id/round/:roundUid/planning`），既有 `jobType: 'general' | 'survey'` 就在此。首次實作（commit `f7d4968`）落在錯頁，需 revert 後在正確頁重做。兩頁讀寫任務用同一組 API（`jobs/list` + `PUT /grc/project/<uid>/job/<id>`），**BE（T-2.2）不受影響**。

> **⚠️ 2026-07-26 追加（plan 遺漏補洞）**：DEV DB 實查發現 `config.detection_tool_param_schemas` **零 seed**——56.1 只 seed 了連線欄位（config_field_schema），漏了掃描參數 schema。而 Agent connector 端 `params.hosts` 是必填（缺了 raise「缺少掃描目標」），T-2.3 的參數 UI 又是動態拉 param-schema 渲染——schema 空的會渲染出零欄位，使用者沒地方填掃描目標，整條鏈每次掃描必炸。故本 Task 增加 **Step 0.5（BE repo）**補 seed migration。

**FE repo 為主，Step 0.5 在 BE repo。** 檔案 `src/views/project/ProjectPlanningView.vue`（規劃頁，任務面板在「控制項實作」Tab）。

- [ ] **Step 0.5: 補 OpenVAS param_schema seed migration（BE repo）**——新檔 `scripts/sql/2026-07-26-fr056-2-openvas-param-schema-seed.sql`，照 migration 鐵則（檔頭 `-- Date:`、檔尾 INSERT schema_migrations），INSERT 一筆 `config.detection_tool_param_schemas`（detection_tool_id 取 code='openvas'、version=1、is_current=true），param_schema 至少含：
```json
[
  {"key":"hosts","label":"掃描目標（IP/主機，逗號分隔）","type":"text","required":true},
  {"key":"timeout_sec","label":"逾時秒數","type":"number","required":false},
  {"key":"scan_config_id","label":"Scan Config UUID（進階，留空用預設）","type":"text","required":false},
  {"key":"scanner_id","label":"Scanner UUID（進階，留空用預設）","type":"text","required":false}
]
```
key 命名必須與 evidence-agent connector 消費端一致（`openvas.py`：`params.hosts` / `params.timeout_sec` / `params.scan_config_id` / `params.scanner_id`）。套 DEV 後實查有進。獨立 commit（BE repo）。

- [ ] **Step 0: revert 錯頁改動**——`git revert f7d4968`（TaskSetupView.vue 的 detection_tool UI），避免死碼裡留第二套真相。menuStore 的 `fetchDetectionToolMenu`、`DetectionConfigField.vue`、i18n key 等可複用資產在後續 step 重新引用（revert 後若這些檔案被一併退掉，從 revert 前版本挑回）。

- [ ] **Step 1: jobTypeOptions 加第三項**（`ProjectPlanningView.vue` 既有 jobTypeOptions 處，約 L223）
```ts
{ label: t('lang.task_setup.job_type_detection'), value: 'detection_tool', icon: 'pi pi-shield' }
```
任務型別（`jobType: 'general' | 'survey'`，約 L46）加 `'detection_tool'`，並加 `toolUid: string|null`、`toolParams: Record<string,any>`、`completionMode: string`。

- [ ] **Step 2: menuStore 工具目錄 fetch**（沿用首次實作的 `fetchDetectionToolMenu`，mirror `fetchSurveyMenu`）——拉 available 工具供選擇。

- [ ] **Step 3: 任務面板加 `v-if="task.jobType === 'detection_tool'"` 區塊**（mirror survey 區塊，約 L2108 survey MultiSelect 同層）：
- 工具選擇：**單選** Dropdown（非 survey 的 MultiSelect），選項來自工具目錄。
- 選定工具後 → 打 `GET /detection-tools/<uid>/param-schema` 拉參數欄位定義 → 用 FR-056.1 建的 `DetectionConfigField.vue`（同一個動態欄位 renderer 複用）渲染參數表單，v-model 綁 `task.toolParams`。
- 完成模式：小 SelectButton / Dropdown（auto / manual），預設 manual。

- [ ] **Step 4: 存檔 payload 加 tool 欄位**（規劃頁既有 saveTask/更新流程，約 L979 `PUT /grc/project/<uid>/job/<id>`）
```ts
tool: task.jobType === 'detection_tool' ? {
  uid: task.toolUid, params: task.toolParams, completion_mode: task.completionMode
} : null,
```
注意規劃頁 L1271 的「刻意不送某些欄位以保留既有關聯」慣例——tool 欄位的送/不送要與該慣例一致（非 detection_tool 任務送 null 不可誤清 binding；比照 surveys 欄位的處理方式）。

- [ ] **Step 5: i18n 補/沿用文案**（job_type_detection、completion_mode_auto/manual、工具選擇/參數相關 key，zh-tw+en；首次實作已加的 key 沿用）。

- [ ] **Step 6: 手測（在真頁面）** → 進 `/project/projects/<id>/round/<roundUid>/planning` 控制項實作 Tab 點 AO → 任務設為檢測工具執行、選 OpenVAS、參數欄位動態出現、選完成模式、存檔、DB 驗證寫入、重整 prefill 回填；general/survey 任務不受影響（讀寫回歸）。

- [ ] **Step 7: Commit（FE repo，revert 與重做分開 commit）**
```bash
git commit -m "revert(fr056): back out detection_tool UI from dead-end TaskSetupView (T-2.3 rework)"
git commit -m "feat(fr056): add detection_tool job type to planning page task panel (T-2.3)"
```

---

## 完成後收尾（等 user 下令）

- [ ] STG/POC 套綁定表 migration。
- [ ] 更新任務設置頁 spec（`docs/specs/current/`）。
- [ ] Notion FR-056.2 子卡標「修正待驗證」+ 回寫。
- [ ] **回填 FR-056.1 Task 4 的引用任務數 stub**——此階段綁定表建好後，`count_referencing_tasks` 可改查 `config.job_execution_detection_tools` 真實引用數（原本回 0 的 TODO(FR-056.2) 到期）。

---

## 附錄：DDD 九層照抄範本

同 FR-056.1 `implementation-plan.md` 附錄 A（來源 `remote_agent` 模組）。綁定表為 tenant-scoped，model 繼承 `BaseModel, TenantScopedMixinModel`。
