# C4 — 專案建立/編輯流程改造

> **級別**：中
> **依賴**：C3（受評範圍 derived from SSP；schema 變動才能拿掉 request fields）
> **被依賴**：C6（FE 表單拿掉）

---

## 1. 目標

把專案建立/編輯 API 內的 `audit_systems` / `devices` 欄位拿掉（這兩個現在屬 SSP 範疇），保留基本資訊 + 參與人員。Response Schema **對外不變**（後端內部已在 C3 改 derived from SSP），FE 表單調整由 C6 處理。

## 2. 範圍

### In scope（**全 BE**）

- `OscalProjectStartRequest`（建立專案 request）：拿掉 `audit_systems` + `devices` 欄位
- `ProjectUpdateRequestSchema`（編輯專案 request）：拿掉 `audit_systems` + `devices` 欄位
- `oscal_project_service.start_oscal_project`：移除 Step C（devices INSERT）+ audit_systems INSERT 邏輯
- `GrcProjectService.update_project`：移除 Section 3（audit_systems 替換）+ Section 5（devices 替換）邏輯
- DI 解依賴：兩個 service 不再需要 `project_device_mapping_service` / `project_information_system_domain_service`（已在 C3 廢，C4 確認 DI 清理）

### Out of scope

- Response Schema `audit_systems` / `devices`（**保留**，由 C3 改 derive from SSP，API 對外格式不變）
- `/project-devices` API（**保留 GET**，由 C3 改 derive；C3 同步處理 write endpoints 退 410）
- FE 表單調整（C6）
- 「拿掉受評範圍後，user 要怎麼建立範圍？」答：建專案 → 進專案規劃頁 → SSP tab → 在 SSP 編輯頁加範圍（C5）

## 3. 改動詳情

### 3.1 `api/project/serializers/project.py` — `OscalProjectStartRequest`

**改動前（line 30-51）**：
```python
class OscalProjectStartRequest(Schema):
    profile_uid = fields.String(required=True)
    ssp_uid = fields.String(required=False, allow_none=True)
    name = fields.String(required=True)
    description = fields.String(required=False)
    start_date = fields.Date('%Y-%m-%d', allow_none=True)
    end_date = fields.Date('%Y-%m-%d', allow_none=True)
    owner_uid = fields.String(required=False, allow_none=True, load_default=None)
    flow_template_uid = fields.UUID(required=True)
    audit_systems = fields.List(
        fields.Nested(AuditSystemInputSchema), load_default=[], allow_none=True,
    )
    participants = fields.List(
        fields.Nested(ParticipantInputSchema), load_default=[], allow_none=True,
    )
    devices = fields.List(
        fields.Nested(DeviceInputSchema), load_default=[], allow_none=True,
    )
```

**改動後**：
```python
class OscalProjectStartRequest(Schema):
    profile_uid = fields.String(required=True)
    ssp_uid = fields.String(required=False, allow_none=True)
    name = fields.String(required=True)
    description = fields.String(required=False)
    start_date = fields.Date('%Y-%m-%d', allow_none=True)
    end_date = fields.Date('%Y-%m-%d', allow_none=True)
    owner_uid = fields.String(required=False, allow_none=True, load_default=None)
    flow_template_uid = fields.UUID(required=True)
    participants = fields.List(
        fields.Nested(ParticipantInputSchema), load_default=[], allow_none=True,
    )
    # audit_systems + devices 已移除（C4, 2026-MM-DD）
    # 受評範圍改在 SSP 編輯頁維護，見 docs/features/FR-011.3-2605-ssp-edit-in-project/
```

`AuditSystemInputSchema` / `DeviceInputSchema`：
- 此 Schema 仍可能被其他地方 import（如測試）→ 保留 class 定義，僅從 `OscalProjectStartRequest` 拿掉欄位
- 若 C 階段尾聲確認無 caller，連同 schema class 一起刪

### 3.2 `api/grc/serializers/project.py` — `ProjectUpdateRequestSchema`

**改動前（line 77-112）**：
```python
class ProjectUpdateRequestSchema(Schema):
    name = fields.String(allow_none=True, load_default=None)
    description = fields.String(allow_none=True, load_default=None)
    start_date = fields.Date(format="%Y-%m-%d", allow_none=True, load_default=None)
    end_date = fields.Date(format="%Y-%m-%d", allow_none=True, load_default=None)
    status = fields.String(allow_none=True, load_default=None, validate=...)
    owner_uid = fields.String(allow_none=True, load_default=None)
    audit_systems = fields.List(fields.Nested(_AuditSystemInputSchema), allow_none=True, load_default=None)
    participants = fields.List(fields.Nested(_ParticipantInputSchema), allow_none=True, load_default=None)
    devices = fields.List(fields.Nested(_DeviceInputSchema), allow_none=True, load_default=None)
```

**改動後**：
```python
class ProjectUpdateRequestSchema(Schema):
    """PUT /grc/project/<uid> request body.

    All fields are optional; only provided fields are updated.
    Providing participants as a list (including []) replaces participants entirely.
    Omitting (null/missing) leaves it unchanged.

    audit_systems + devices 從 C4 (2026-MM-DD) 起移除 — 受評範圍改在 SSP 編輯頁
    維護（見 docs/features/FR-011.3-2605-ssp-edit-in-project/）。
    """
    name = fields.String(allow_none=True, load_default=None)
    description = fields.String(allow_none=True, load_default=None)
    start_date = fields.Date(format="%Y-%m-%d", allow_none=True, load_default=None)
    end_date = fields.Date(format="%Y-%m-%d", allow_none=True, load_default=None)
    status = fields.String(allow_none=True, load_default=None, validate=...)
    owner_uid = fields.String(allow_none=True, load_default=None)
    participants = fields.List(fields.Nested(_ParticipantInputSchema), allow_none=True, load_default=None)
```

### 3.3 `app/project/service/oscal_project_service.py` — `start_oscal_project`

**改動前（line 456-461）**：
```python
# Step C — 加入設備
for device in (start_project_dto.devices or []):
    self.project_device_mapping_service.add(
        project_uid=str(project.uid),
        device_uid=device.device_uid,
    )
```

**改動後**：
- **整段 Step C 移除**
- 移除上方 audit_systems INSERT（如果存在類似邏輯，pre-flight 確認 line 號）
- `start_project_dto.devices` 和 `start_project_dto.audit_systems` 屬性已不存在（request schema 拿掉）

**DI 改動**：
- `__init__` 拿掉 `project_device_mapping_service` 參數
- DI container `di_containers/project/project_containers.py` 對應 wiring 拿掉
- `self.project_device_mapping_service` 屬性移除

### 3.4 `app/grc/service/project_service.py` — `update_project`

**改動前**：
- Section 3 (line 367+)：audit_systems 替換
- Section 5 (line 432-444)：devices 替換

**改動後**：兩段整段移除

**DI 改動**：
- `__init__` 拿掉 `project_device_mapping_service` + `project_information_system_domain_service` 參數
- DI container `di_containers/grc/grc_containers.py` 對應 wiring 拿掉
- `self._device_mapping_service` + `self._pis_domain_service` 屬性移除

## 4. API 行為變化

### 4.1 POST `/oscal-project/start`

**Before**：
```json
{
  "profile_uid": "...",
  "name": "稽核 2026",
  "flow_template_uid": "...",
  "audit_systems": [{"information_system_uid": "..."}],
  "devices": [{"device_uid": "..."}],
  "participants": [{"user_uid": "...", "role": "auditor"}]
}
```

**After**：
```json
{
  "profile_uid": "...",
  "name": "稽核 2026",
  "flow_template_uid": "...",
  "participants": [{"user_uid": "...", "role": "auditor"}]
}
```

**送舊 schema 含 audit_systems / devices 會發生什麼？**
- Marshmallow `Meta.unknown = EXCLUDE` 預設 → 忽略未知欄位，建專案成功但 audit_systems / devices 不會被處理
- **若不設 EXCLUDE** → 拋 ValidationError
- **建議**：保持 EXCLUDE（向後相容容忍舊 FE，但不寫入資料）+ log warn

**Response**：原樣（保留 `audit_systems` + `devices` 在 ProjectResponseSchema，由 C3 改 derived from SSP）

### 4.2 PUT `/grc/project/<uid>`

同樣行為：移除 request 內 `audit_systems` / `devices`，Response 不變。

## 5. 邊界條件

| 情境 | 行為 |
|------|------|
| FE 還沒同步 deploy，POST 含 audit_systems / devices | `unknown=EXCLUDE` 忽略，建專案成功，但這些 list 不會寫入 |
| 建專案後 user 直接想看 devices | Response 回 `[]`（C3-Q5 決策），FE 顯示「請至 SSP tab 設定範圍」|
| 想改舊有專案的 devices | 透過 PUT /grc/project/<uid> 改不了，必須去 SSP tab |

## 6. 待 user 拍板的小決策

| 編號 | 問題 | 我建議 |
|------|------|--------|
| C4-D1 | request 內舊欄位處理：`unknown=EXCLUDE` 忽略 vs 直接 reject (ValidationError)？ | **EXCLUDE + log warn**（向後相容，避免硬撞 FE）|
| C4-D2 | `AuditSystemInputSchema` / `DeviceInputSchema` schema class 留還是刪？ | **C 階段尾聲確認無 caller 後刪**（C4 本期保留，等 C 全綠再清）|
| C4-D3 | `ParticipantInputSchema.role` 是否限制 manager/reviewer/auditor/viewer？目前用 fields.String 沒 validate | **加 validate.OneOf 比較嚴謹**（順手修） |

---

## 7. 開發後狀態

- `OscalProjectStartRequest` 內無 `audit_systems` / `devices`
- `ProjectUpdateRequestSchema` 內無 `audit_systems` / `devices`
- `oscal_project_service.start_oscal_project` 不再寫 device / audit_systems mapping
- `GrcProjectService.update_project` 不再處理 device / audit_systems 替換
- DI dependency 縮減（兩個服務不再需要 device/info_system mapping service）
- Response Schema 對外不變（依賴 C3 內部改 derive）
