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.pyOscalProjectStartRequest

改動前(line 30-51)

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,
    )

改動後

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.pyProjectUpdateRequestSchema

改動前(line 77-112)

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)

改動後

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.pystart_oscal_project

改動前(line 456-461)

# 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.devicesstart_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.pyupdate_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

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

After

{
  "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/ 改不了,必須去 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)