# Spec 3「AP 套用流程」Phase 6 Follow-up 執行計劃

> **For agentic workers:** REQUIRED SUB-SKILL: 使用 `superpowers:subagent-driven-development`（推薦）或 `superpowers:executing-plans` 逐 task 執行。步驟用 checkbox (`- [ ]`) 追蹤。

**Goal:** 完成 Spec 3 Phase 6 review 揭露的 Tier 1 follow-up（PR1-4）+ M8 跨 repo 測試補強，作為 Spec 3 task arc 的真正收尾，全部完成後一次性 push + 部署 stg。

**Architecture:** 在 `feature/project-flow-engine-integrate` branch 上以 milestone-style commit 逐項 land；每個 Task = 1 commit（含 changelog）= 對應 follow-up tracker 的 1 個 PR。BE service 異動後必重啟。

**Tech Stack:** Python 3.11 / Flask-RESTful / SQLAlchemy / pytest（BE）；Vue 3 + TS + PrimeVue（FE）；Cucumber.js + Playwright（compliance-manager-test）

**範圍對齊**（user 已決策）：
- Tier 1 全做（PR1-4）+ M8
- 全做完才 push + 部署 stg
- 全部 commit 在 `feature/project-flow-engine-integrate`
- 計劃存於：`docs/features/FR-026-2605-project-flow-engine-integrate/03-ap-binding/phase6-followup-plan.md`
- 對應 issue tracker：`docs/issues/pending/2026-05-13-spec3-phase-6-followups.md`

---

## 〇、共通約定

**Commit 規則：**
- 每個 Task 完成 = 1 commit（含 code + test + changelog）
- 顯式檔名 `git add <files>`，**禁用 `-am` / `-A` / `.`**（memory rule `feedback_subagent_explicit_git_add`）
- Commit message 用 HEREDOC 格式，結尾不加 Co-Authored-By（本專案不要求）

**Changelog 規則：**
- 每個 Task 一份 `docs/changelog/2026-05-14-tweak-spec3-followup-prN-*.md`
- frontmatter：`type: tweak`（refactor / test 補強 / housekeeping，都不算讓使用者能做新事情）
- `modules: [project, grc]` 或 `[frontend]`
- `issue: docs/issues/pending/2026-05-13-spec3-phase-6-followups.md`
- 結尾 commit 後回填 `commit: <hash>`

**測試規則：**
- BE：`PYTHONPATH=. pytest test/<file>.py -v`
- 採 TDD：先 RED → 實作 → GREEN
- 改 BE service 後必重啟（memory rule `feedback_be_restart_after_service_change`）：
  ```bash
  pkill -9 -f main_socketio.py && python main_socketio.py > /dev/null 2>&1 &
  ```

**Issue tracker 同步：**
- 每完成 1 PR 在 `docs/issues/pending/2026-05-13-spec3-phase-6-followups.md` 第四節「處理狀態追蹤」表格填入 commit hash
- 全部完成後執行 `git mv pending/...md resolved/...md`，並在 issue 開頭加 `## ⓪ Resolution` 段

---

## 一、Pre-flight verification（開工前必跑）

驗證 follow-up tracker 跟現況一致，避免 plan-vs-reality drift。

- [ ] **驗證 BE 環境 clean**
  ```bash
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
  git status        # 預期：clean，HEAD = b6f4eec
  git log --oneline -3
  ```

- [ ] **驗證 PR1 涉及的 method 行號**（follow-up tracker 寫的行號可能已偏移）
  ```bash
  grep -n "def activate_project\|def confirm_audit\|def terminal_close_ap\|def launch_audit" \
      app/project/service/oscal_audit_service.py
  ```
  預期：
  - `activate_project`：line 77
  - `launch_audit`：line 169
  - `confirm_audit`：line 329
  - `terminal_close_ap`：line 425

- [ ] **驗證 ProjectStatus 來源 + enum 完整列表**
  ```bash
  python -c "from jedi_project.common.code.status_code import ProjectStatus; print([s.name for s in ProjectStatus])"
  ```
  預期至少含：`PENDING / IN_PROGRESS / COMPLETED / SUSPENDED / ARCHIVED`
  - 若 SUSPENDED / ARCHIVED 任一缺，回頭跟 user 對齊（PR1 黑名單 → 白名單修正的「保護目標」可能不存在）

- [ ] **驗證 TerminalCloseHandler.execute 已有 user_id 參數**
  ```bash
  sed -n '149,163p' app/grc/service/oscal_stage_handlers.py
  ```
  預期：`execute(self, ap_uid, project_uid, user_id, curr_user, ctx)` 已有 `user_id`，但 call `terminal_close_ap` 沒傳 — PR1 只需把它接上即可。

- [ ] **驗證 dead code 行號**
  ```bash
  sed -n '216,275p' app/project/service/oscal_audit_service.py
  ```
  預期：line 222 為 `raise PreconditionFailedError(...)`，之後到 `# ── 首次稽核模式 ──`（約 line 273-274）為 dead code（含 return 在 line 270）。**實際刪除範圍以 sed 結果為準，不照 tracker 寫的 "218-271"**。

- [ ] **驗證 M12 completion_rate 實際路徑**
  ```bash
  sed -n '745,760p' infra/grc/repository/grc_project_repo_impl.py
  ```
  預期：line 755-758 有 `if ap_status_str == "closed": completion_rate = 100.0 else: ...`。**tracker 寫的 `app/grc/repo/grc_project_repo_impl.py` 是錯的，實際在 `infra/`**。

- [ ] **驗證 FE 環境 clean**
  ```bash
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
  git status        # 預期：clean，HEAD = 695baad
  git log --oneline -3
  ```

- [ ] **驗證 FE defaultFlowTemplateUid 邏輯**
  ```bash
  sed -n '103,140p' src/views/project/ProjectApListView.vue
  ```

- [ ] **驗證測試 repo 環境**
  ```bash
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-test
  git status; git log --oneline -3
  ```
  - 確認可執行 cucumber：`npm run test:regression` 或對應指令（看 package.json scripts）

---

## 二、Task 1：PR1 — `_sync_project_status` helper + terminal_close_ap 加權限檢查

**Priority**: High（一次清掉 Pattern 1 + Pattern 2 + Silent #1/#3/#6）

**Files:**
- Modify: `app/project/service/oscal_audit_service.py`
  - 新增 helper `_sync_project_status(project_uid, target_status, curr_user, allowed_from)`（約插入 line 75 上方）
  - 替換 line 131-133（activate_project state sync）
  - 替換 line 415-417（confirm_audit state sync）
  - 替換 line 463-465（terminal_close_ap state sync）
  - terminal_close_ap signature line 425 加 `user_id: int` 參數
  - terminal_close_ap method body 加 `self._check_participant_role(project_uid, user_id, ["manager", "auditor"])`
- Modify: `app/grc/service/oscal_stage_handlers.py`
  - TerminalCloseHandler.execute 內呼叫 `terminal_close_ap` 加 `user_id=user_id`（line ~158）
- Test: `test/test_oscal_audit_service.py`（若不存在則新建）
- Test: `test/test_oscal_stage_handlers.py` 更新既有 TerminalCloseHandler test

### 2.1 寫測試（RED）

- [ ] **Step 1: 新增 `_sync_project_status` helper 行為 test**

  新增 `test/test_oscal_audit_service.py`（若已存在則 append）：

  ```python
  # test/test_oscal_audit_service.py
  import pytest
  from unittest.mock import MagicMock
  from app.project.service.oscal_audit_service import OscalAuditService
  from jedi_project.common.code.status_code import ProjectStatus


  class TestSyncProjectStatus:
      def _make_service(self, project_status):
          service = OscalAuditService.__new__(OscalAuditService)
          project = MagicMock(status=project_status, updated_user=None)
          service._project_domain_service = MagicMock()
          service._project_domain_service.get_one.return_value = project
          service._project_domain_service.update = MagicMock()
          return service, project

      def test_allowed_transition_writes_target(self):
          svc, project = self._make_service(ProjectStatus.PENDING)
          svc._sync_project_status("uid", ProjectStatus.IN_PROGRESS, "u", {ProjectStatus.PENDING})
          assert project.status == ProjectStatus.IN_PROGRESS
          assert project.updated_user == "u"
          svc._project_domain_service.update.assert_called_once()

      def test_not_in_allowed_from_skips(self):
          svc, project = self._make_service(ProjectStatus.SUSPENDED)
          svc._sync_project_status("uid", ProjectStatus.COMPLETED, "u",
                                   {ProjectStatus.PENDING, ProjectStatus.IN_PROGRESS})
          assert project.status == ProjectStatus.SUSPENDED   # 不變
          svc._project_domain_service.update.assert_not_called()

      def test_archived_project_protected(self):
          svc, project = self._make_service(ProjectStatus.ARCHIVED)
          svc._sync_project_status("uid", ProjectStatus.COMPLETED, "u",
                                   {ProjectStatus.PENDING, ProjectStatus.IN_PROGRESS})
          assert project.status == ProjectStatus.ARCHIVED
          svc._project_domain_service.update.assert_not_called()

      def test_project_not_found_returns_silently(self):
          svc = OscalAuditService.__new__(OscalAuditService)
          svc._project_domain_service = MagicMock()
          svc._project_domain_service.get_one.return_value = None
          svc._sync_project_status("uid", ProjectStatus.COMPLETED, "u",
                                   {ProjectStatus.IN_PROGRESS})
          svc._project_domain_service.update.assert_not_called()
  ```

- [ ] **Step 2: 跑測試確認 RED**

  ```bash
  PYTHONPATH=. pytest test/test_oscal_audit_service.py::TestSyncProjectStatus -v
  ```
  預期：4 個全 FAIL（`AttributeError: '_sync_project_status' not defined` 或類似）

### 2.2 實作 helper（GREEN）

- [ ] **Step 3: 在 OscalAuditService 加 `_sync_project_status` helper**

  插入位置：`app/project/service/oscal_audit_service.py` 約 line 75（`activate_project` 之前）：

  ```python
      def _sync_project_status(
          self,
          project_uid: str,
          target_status,        # ProjectStatus enum
          curr_user: str,
          allowed_from: set,    # set[ProjectStatus]
      ) -> None:
          """Idempotent project.status transition.

          只在 project.status ∈ allowed_from 時推進；否則 skip + warning log。
          Caller 必須給 allowed_from 白名單 — 防止 SUSPENDED / ARCHIVED 被誤推到 COMPLETED
          破壞 archive 語意（Phase 6 review Pattern 1）。
          """
          from jedi_project.domain.entity.project.project_query_entity import ProjectQueryEntity

          project = self._project_domain_service.get_one(ProjectQueryEntity(uid=project_uid))
          if not project:
              return
          if project.status not in allowed_from:
              logger.warning(
                  f"[sync_project_status] project={project_uid} status={project.status} "
                  f"not in allowed_from={allowed_from}, skip"
              )
              return
          project.status = target_status
          project.updated_user = curr_user
          self._project_domain_service.update(project)
  ```

  **注意**：`ProjectQueryEntity` import path 用實際路徑（pre-flight 可先 grep 確認）；`logger` 用模組頂部既有 logger。

- [ ] **Step 4: 跑 helper test 確認 GREEN**

  ```bash
  PYTHONPATH=. pytest test/test_oscal_audit_service.py::TestSyncProjectStatus -v
  ```
  預期：4 個全 PASS。

### 2.3 替換三處 call site

- [ ] **Step 5: 替換 activate_project (line 131-133)**

  ```python
  # 原：
  if project and project.status == ProjectStatus.PENDING:
      project.status = ProjectStatus.IN_PROGRESS
      self._project_domain_service.update(project)
  # 改：
  self._sync_project_status(
      project_uid, ProjectStatus.IN_PROGRESS, curr_user,
      allowed_from={ProjectStatus.PENDING},
  )
  ```

- [ ] **Step 6: 替換 confirm_audit (line 415-417)**

  ```python
  # 原：
  if project and project.status == ProjectStatus.IN_PROGRESS:
      project.status = ProjectStatus.COMPLETED
      self._project_domain_service.update(project)
  # 改：
  self._sync_project_status(
      project_uid, ProjectStatus.COMPLETED, curr_user,
      allowed_from={ProjectStatus.IN_PROGRESS},
  )
  ```

- [ ] **Step 7: 替換 terminal_close_ap (line 463-465)**

  ```python
  # 原（黑名單，會誤推 SUSPENDED / ARCHIVED）：
  if project and project.status != ProjectStatus.COMPLETED:
      project.status = ProjectStatus.COMPLETED
      self._project_domain_service.update(project)
  # 改（白名單）：
  self._sync_project_status(
      project_uid, ProjectStatus.COMPLETED, curr_user,
      allowed_from={ProjectStatus.PENDING, ProjectStatus.IN_PROGRESS},
  )
  ```

  **註**：替換時，原 block 內可能含其他邏輯（如 fetch project entity 的 line），看 sed 結果決定要不要保留。若 `project = self._project_domain_service.get_one(...)` 只為了 state sync 用途，可一併刪除（helper 內部會自己 fetch）。

- [ ] **Step 8: terminal_close_ap 加 user_id + 權限檢查**

  ```python
  # line 425 簽章改：
  def terminal_close_ap(
      self,
      project_uid: str,
      ap_uid: str,
      user_id: int,           # ← 新增
      curr_user: str,
  ) -> dict:
      self._check_participant_role(project_uid, user_id, ["manager", "auditor"])  # ← 新增
      # ...既有邏輯
  ```

- [ ] **Step 9: 更新 TerminalCloseHandler.execute 把 user_id 傳給 service**

  ```python
  # app/grc/service/oscal_stage_handlers.py:158
  return self._oscal_audit_service.terminal_close_ap(
      project_uid=project_uid,
      ap_uid=ap_uid,
      user_id=user_id,            # ← 新增
      curr_user=curr_user,
  )
  ```

### 2.4 更新 TerminalCloseHandler test

- [ ] **Step 10: 更新 `test/test_oscal_stage_handlers.py` 既有 test**

  搜尋 `TestTerminalCloseHandler`，更新 `terminal_close_ap` mock assertion 加 `user_id` 參數，加一個 assert 確認 service 收到的 user_id 是來自 ctx 的值。

  ```bash
  grep -n "TestTerminalCloseHandler\|terminal_close_ap.assert" test/test_oscal_stage_handlers.py
  ```

### 2.5 跑全套測試 + 重啟 + commit

- [ ] **Step 11: 全套 BE test 確認沒 regression**

  ```bash
  PYTHONPATH=. pytest test/test_oscal_audit_service.py test/test_oscal_stage_handlers.py -v
  ```
  預期：全部 PASS。

- [ ] **Step 12: 重啟 BE**

  ```bash
  pkill -9 -f main_socketio.py && python main_socketio.py > /dev/null 2>&1 &
  sleep 2 && tail -20 log/app.log
  ```
  預期：log 末段顯示 server started，無 traceback。

- [ ] **Step 13: 寫 changelog**

  `docs/changelog/2026-05-14-tweak-spec3-followup-pr1-sync-project-status-helper.md`：

  ```markdown
  ---
  type: tweak
  modules: [project, grc]
  issue: docs/issues/pending/2026-05-13-spec3-phase-6-followups.md
  ---

  # PR1：抽 `_sync_project_status` helper + terminal_close_ap 加權限檢查

  ## 為什麼
  Spec 3 Phase 6 review 揭露 Pattern 1（state sync 3 處不一致：activate 白名單 / confirm 白名單 / terminal_close 黑名單）+ Pattern 2（terminal_close_ap 缺角色檢查）。黑名單會強推 SUSPENDED/ARCHIVED → COMPLETED 破壞 archive 語意。

  ## 變更範圍
  - `app/project/service/oscal_audit_service.py`：新增 `_sync_project_status` helper + 替換 3 處 call site + terminal_close_ap 加 `user_id` 參數 + `_check_participant_role`
  - `app/grc/service/oscal_stage_handlers.py`：TerminalCloseHandler.execute 傳 user_id 給 service
  - `test/test_oscal_audit_service.py`（新增）：4 個 helper test
  - `test/test_oscal_stage_handlers.py`：更新既有 TerminalCloseHandler test

  ## 行為差異
  - SUSPENDED / ARCHIVED project：terminal_close_ap 不再強推 COMPLETED，改 skip + warning log
  - terminal_close_ap：外部 caller 必須帶 user_id + 是 manager / auditor 才能呼叫（API 路由 / handler 已配對）

  ## 測試結果
  `pytest test/test_oscal_audit_service.py test/test_oscal_stage_handlers.py -v` 全 PASS。

  ## 參考
  - Phase 6 review master：`docs/review/2026-05-13-spec3-review.md` Pattern 1 + 2
  - Silent failure：`docs/review/2026-05-13-spec3-review-silent-failures.md` #1 / #3 / #6
  ```

- [ ] **Step 14: Commit**

  ```bash
  git add app/project/service/oscal_audit_service.py \
          app/grc/service/oscal_stage_handlers.py \
          test/test_oscal_audit_service.py \
          test/test_oscal_stage_handlers.py \
          docs/changelog/2026-05-14-tweak-spec3-followup-pr1-sync-project-status-helper.md
  git commit -m "$(cat <<'EOF'
  tweak(spec3 followup PR1): _sync_project_status helper + terminal_close_ap 加權限檢查

  - 抽 _sync_project_status(project_uid, target_status, curr_user, allowed_from) 統一 3 處 state sync
  - terminal_close_ap 從黑名單 (!= COMPLETED) 改白名單 ({PENDING, IN_PROGRESS}) — 修 Pattern 1
  - terminal_close_ap 加 user_id 參數 + _check_participant_role — 修 Pattern 2 + Silent #6
  - TerminalCloseHandler.execute 把 ctx.user_id 傳給 service

  Refs: docs/review/2026-05-13-spec3-review.md (Pattern 1+2, Silent #1/#3/#6)
  EOF
  )"
  ```

- [ ] **Step 15: 更新 issue tracker 狀態表**

  在 `docs/issues/pending/2026-05-13-spec3-phase-6-followups.md` 第四節「處理狀態追蹤」PR1 row 填入 commit hash。

  ```bash
  git log -1 --format=%h
  # 把 hash 填進 tracker，再 commit tracker 變更
  ```

  > **注意**：tracker 更新可以累積，最後一個 Task 完成時一起 commit；不需要每個 PR 都 commit tracker。

---

## 三、Task 2：PR2 — 刪 launch_audit dead code

**Priority**: Medium（housekeeping）

**Files:**
- Modify: `app/project/service/oscal_audit_service.py`（刪 line 222 之後到 line ~273 之前的 unreachable block）

### 3.1 確認刪除範圍

- [ ] **Step 1: 重新 sed 確認**（PR1 後行號可能已偏移）

  ```bash
  grep -n "raise PreconditionFailedError(GrcErrorCode.GRC_AP_REVIEW_VIA_CLOSE_ROUND)\|# ── 首次稽核模式 ──" \
      app/project/service/oscal_audit_service.py
  ```
  記下 `raise` 行（保留）跟 `# ── 首次稽核模式 ──` 行（保留）。中間都刪。

### 3.2 刪除 + 加註解

- [ ] **Step 2: 刪除 unreachable block**

  原 block 約 50 行（從 `# ── 以下覆核模式邏輯已停用（保留 reference）...` 開始，到 `return {"warning": False, "ap_uid": ap_uid, "status": "auditing", "run_no": new_run_no}` 結束，共 ~50 行）。

  保留 `raise PreconditionFailedError(...)` 那行 + 改其上方 comment 為：

  ```python
  if is_review_mode:
      # Spec 2（2026-05-13 review fix H1 + Spec 3 Phase 6 PR2 cleanup）：
      # 「發起覆核」副作用已搬到 close_round handler（Banner「完成改善」推進路徑）。
      # 此分支保留純粹防 dup — 外部 curl / legacy code 若在 remediation 狀態
      # 觸發 launch_audit 會建出第二份 run_no=N+1 ar_data，所以直接 raise 防 data corruption。
      # 對應覆核流程實作見 `oscal_audit_service.close_round()` (line ~427)。
      raise PreconditionFailedError(GrcErrorCode.GRC_AP_REVIEW_VIA_CLOSE_ROUND)
  ```

### 3.3 跑測試 + commit

- [ ] **Step 3: 跑 launch_audit 既有 test 確認沒 regression**

  ```bash
  PYTHONPATH=. pytest test/ -v -k "launch_audit or LaunchAudit"
  ```

- [ ] **Step 4: 重啟 BE**

  ```bash
  pkill -9 -f main_socketio.py && python main_socketio.py > /dev/null 2>&1 &
  ```

- [ ] **Step 5: 寫 changelog**

  `docs/changelog/2026-05-14-tweak-spec3-followup-pr2-launch-audit-dead-code.md`

- [ ] **Step 6: Commit**

  ```bash
  git add app/project/service/oscal_audit_service.py \
          docs/changelog/2026-05-14-tweak-spec3-followup-pr2-launch-audit-dead-code.md
  git commit -m "$(cat <<'EOF'
  tweak(spec3 followup PR2): 刪 launch_audit is_review_mode unreachable block

  Line 222 的 raise 之後是 dead code (Spec 2 H1 fix 後 close_round handler 接手覆核)。
  保留 raise 防 legacy curl / external caller dup，但 unreachable 邏輯刪 ~50 行。

  Refs: docs/review/2026-05-13-spec3-review-be-code.md #2
  EOF
  )"
  ```

---

## 四、Task 3：PR3 — M12 completion_rate 補 unit test

**Priority**: Medium（hotfix `b979a71` 暴露 import smoke 不夠）

**Strategy**：tracker 寫 4 個 case 都是 Python `if ap_status_str == "closed"` 那段，邏輯非常單純（3 行）— 採「extract pure helper + unit test」比 mock session.query 乾淨太多。

**Files:**
- Modify: `infra/grc/repository/grc_project_repo_impl.py`（line 755-758 區域，抽出 `_compute_completion_rate` static method）
- Test: `test/test_grc_project_repo.py`（新建，純 Python unit test）

### 4.1 RED：寫測試

- [ ] **Step 1: 新增 `test/test_grc_project_repo.py`**

  ```python
  # test/test_grc_project_repo.py
  import pytest
  from infra.grc.repository.grc_project_repo_impl import GrcProjectRepoImpl


  class TestComputeCompletionRate:
      @pytest.mark.parametrize("ap_status, completed, total, expected", [
          ("closed", 0,  0,   100.0),
          ("closed", 2,  2,   100.0),
          ("closed", 0,  10,  100.0),
          ("open",   0,  0,     0.0),
          ("open",   1,  2,    50.0),
          ("open",   3,  10,   30.0),
          ("open",   None, 5,   0.0),    # defensive：completed_tasks 為 None
          (None,     1,  2,    50.0),    # ap_status_str 為 None
      ])
      def test_compute(self, ap_status, completed, total, expected):
          result = GrcProjectRepoImpl._compute_completion_rate(ap_status, completed, total)
          assert result == expected
  ```

- [ ] **Step 2: 跑測試確認 RED**

  ```bash
  PYTHONPATH=. pytest test/test_grc_project_repo.py -v
  ```
  預期：FAIL（`_compute_completion_rate` 不存在）

### 4.2 GREEN：抽 helper + 替換 inline 邏輯

- [ ] **Step 3: 在 `GrcProjectRepoImpl` 加 `@staticmethod _compute_completion_rate`**

  ```python
  @staticmethod
  def _compute_completion_rate(ap_status_str, completed_tasks, total_tasks) -> float:
      """AP 已 closed 視為 100% 完成；否則回 completed/total 比例 (四捨五入 1 位)。
      total_tasks=0 且未 closed 回 0.0（不除以零）。"""
      if ap_status_str == "closed":
          return 100.0
      if not total_tasks:
          return 0.0
      return round((completed_tasks or 0) * 100 / total_tasks, 1)
  ```

- [ ] **Step 4: 替換 line 755-758 為 helper call**

  ```python
  # 原：
  if ap_status_str == "closed":
      completion_rate = 100.0
  else:
      completion_rate = round(completed_tasks * 100 / total_tasks, 1) if total_tasks > 0 else 0.0
  # 改：
  completion_rate = self._compute_completion_rate(ap_status_str, completed_tasks, total_tasks)
  ```

- [ ] **Step 5: 跑測試確認 GREEN**

  ```bash
  PYTHONPATH=. pytest test/test_grc_project_repo.py -v
  ```
  預期：8 個 case 全 PASS。

### 4.3 commit

- [ ] **Step 6: 重啟 BE + smoke**

  ```bash
  pkill -9 -f main_socketio.py && python main_socketio.py > /dev/null 2>&1 &
  ```

- [ ] **Step 7: 寫 changelog + commit**

  `docs/changelog/2026-05-14-tweak-spec3-followup-pr3-completion-rate-unit-test.md`

  ```bash
  git add infra/grc/repository/grc_project_repo_impl.py \
          test/test_grc_project_repo.py \
          docs/changelog/2026-05-14-tweak-spec3-followup-pr3-completion-rate-unit-test.md
  git commit -m "$(cat <<'EOF'
  tweak(spec3 followup PR3): M12 completion_rate 抽 helper + 補 unit test

  M12 hotfix b979a71 (String 漏 import) 暴露 import smoke 對 method-body unresolved
  name 不敏感的弱點 — 補純 Python helper + unit test 把 compute path 釘住。

  Refs: docs/review/2026-05-13-spec3-review-be-code.md #1
  EOF
  )"
  ```

---

## 五、Task 4：PR4 — FE default flow_template guard

**Priority**: Medium（UX 問題 — master 軟刪時 inherited hint 仍顯示 + 預設值失效）

**工作目錄切換**：`cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe`

**Files:**
- Modify: `src/views/project/ProjectApListView.vue`（line 111 `defaultFlowTemplateUid` computed 加 guard + line 480-484 inherited hint 同步）

### 5.1 改 computed

- [ ] **Step 1: 改 `defaultFlowTemplateUid` 加可選性 guard**

  **實際現況**（pre-flight 驗證後修正）：computed 來源是 `apList.value?.[0]?.flow_template?.master_uid`，不是 `masterUid.value`。

  ```typescript
  // 原（line 111-115）：
  const defaultFlowTemplateUid = computed<string | null>(() => {
      const latest = apList.value?.[0]
      return latest?.flow_template?.master_uid ?? null
  })
  // 改：
  const defaultFlowTemplateUid = computed<string | null>(() => {
      const latest = apList.value?.[0]
      const uid = latest?.flow_template?.master_uid
      if (!uid) return null
      // master 軟刪 / 不在 active flowTemplates list 內 → 不當 default
      const found = flowTemplates.value.find((t: any) => t.uid === uid)
      if (!found) return null
      // 若 BE 有 is_master_active 旗標再多防一層（沒回則 found 為準）
      if (found.is_master_active === false) return null
      return uid
  })
  ```

  > **驗證**：subagent 開工前先 `sed -n '108,135p' src/views/project/ProjectApListView.vue` 確認現況；若已被 user 動過則對齊實際內容。

- [ ] **Step 2: 同步 inherited hint 顯示條件**

  `src/views/project/ProjectApListView.vue` line 480-484：

  ```vue
  <!-- 原 -->
  <small v-if="defaultFlowTemplateUid && dialogForm.flowTemplateUid === defaultFlowTemplateUid">
      {{ t('lang.project_ap_list.flow_template_inherited_hint') }}
  </small>
  ```

  這個條件本身已經 OK — 因為 `defaultFlowTemplateUid` 為 null 時 hint 自然不顯示。**無需再改**，但要驗證行為（軟刪後 hint 該消失）。

### 5.2 手動 UI 驗證

- [ ] **Step 3: 啟動 FE dev server + DB 模擬軟刪 master**

  ```bash
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
  npm run dev          # 看 package.json scripts 確認 command
  ```

  在 dev DB 找一個 project 的 master flow_template，UPDATE 設 `is_active=FALSE`，FE 重整 ProjectApListView，預期：
  - dropdown 顯示「請選擇」(無 default)
  - hint「預設沿用上一輪」不顯示
  - 仍可手動選任一 active flow_template 送出

  > **註**：UI 驗證為主，無需寫 E2E 測試（FE 沒有對應 unit test 框架慣例給 computed）。

### 5.3 commit

- [ ] **Step 4: 寫 changelog + commit (FE repo)**

  `docs/changelog/2026-05-14-tweak-spec3-followup-pr4-default-flow-template-guard.md`（路徑：FE repo 的 docs/changelog/）

  ```bash
  git add src/views/project/ProjectApListView.vue \
          docs/changelog/2026-05-14-tweak-spec3-followup-pr4-default-flow-template-guard.md
  git commit -m "$(cat <<'EOF'
  tweak(spec3 followup PR4): defaultFlowTemplateUid 加 master 軟刪 guard

  master_uid 不在 flowTemplates list 內 / is_master_active=false 時，預設值改 null。
  inherited hint 因條件已含 defaultFlowTemplateUid && === 對齊，自然不再顯示。

  Refs: docs/review/2026-05-13-spec3-review-fe-code.md #1
  EOF
  )"
  ```

---

## 六、Task 5：M8 — compliance-manager-test repo 4 fixture bug + 3 Banner BDD scenario

**工作目錄切換**：`cd ~/Projects/Billows/Audit-Manager/compliance-manager-test`

**Strategy**：先看 test repo 的 CLAUDE.md / handoff 索引找出實際待修 4 fixture + 3 scenario 的清單；然後逐項修補 + 跑 cucumber 驗證。

### 6.1 盤點實際待辦

- [ ] **Step 1: 找 M8 細節**

  Spec 3 plan / test-plan 應該記了 M8 細項：
  ```bash
  grep -n "M8\|fixture.*bug\|Banner.*BDD\|3.*scenario" \
      ~/Projects/Billows/Audit-Manager/compliance-manager-be/docs/features/FR-026-2605-project-flow-engine-integrate/03-ap-binding/{test-plan.md,implementation-plan.md,design.md} \
      2>/dev/null | head -30
  ```

  - 把 4 個 fixture bug + 3 個 Banner scenario 條列出來
  - 若找不到細節，回頭跟 user 對齊

- [ ] **Step 2: 確認 test repo 跑得起來**

  ```bash
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-test
  cat package.json | grep -A 5 '"scripts"'    # 找 cucumber 跑法
  ```

### 6.2 修 fixture bug

- [ ] **Step 3: 對每個 fixture bug**：定位 → 修正 → 跑單一 scenario 驗
  - 每個 fixture bug 修完跑 `npm run test:<scenario>` 或對應 cucumber tag 命令確認該 scenario 過

### 6.3 寫 3 個 Banner BDD scenario

- [ ] **Step 4: 對每個新 Banner scenario**：
  - 在 `features/` 加 `.feature` 檔
  - 在 `steps/` 補 step 實作
  - 必要時在 `pages/` 加 page object
  - 跑 cucumber 驗證

### 6.4 commit (test repo)

- [ ] **Step 5: 寫 changelog（test repo）**

  test repo 有自己的 changelog 慣例，看 `docs/changelog/` 既有 file 模仿。

- [ ] **Step 6: Commit + push 規劃**

  ```bash
  git add features/... steps/... pages/... fixtures/... docs/changelog/...
  git commit -m "tweak(spec3 followup M8): 修 4 fixture bug + 補 3 Banner BDD scenario"
  ```

  > **注意**：test repo 是否要跟 BE/FE 同步 push 取決於 user 部署慣例。M8 完成後本計劃的「需求收尾」即告達成。

---

## 七、Task 6：收尾（issue tracker / conversation history / push / deploy stg）

**工作目錄切回**：`cd ~/Projects/Billows/Audit-Manager/compliance-manager-be`

### 7.1 Issue tracker 歸檔

- [ ] **Step 1: 在 issue 開頭加 `## ⓪ Resolution` 段**

  `docs/issues/pending/2026-05-13-spec3-phase-6-followups.md` 開頭插入：

  ```markdown
  ## ⓪ Resolution

  | 項目 | 內容 |
  |------|------|
  | 修復日期 | 2026-05-14 |
  | Commits | PR1 `<hash>` / PR2 `<hash>` / PR3 `<hash>` / PR4 `<hash>` (FE repo) / M8 `<hash>` (test repo) |
  | Changelog | docs/changelog/2026-05-14-tweak-spec3-followup-pr1~pr4-*.md |
  | Follow-up | PR5-8 + PR-TD1/2/3 留 backlog（Low priority）；Stage plugin 架構依需求觸發 |
  ```

- [ ] **Step 2: `git mv pending → resolved`**

  ```bash
  git mv docs/issues/pending/2026-05-13-spec3-phase-6-followups.md \
         docs/issues/resolved/2026-05-13-spec3-phase-6-followups.md
  ```

- [ ] **Step 3: 更新所有 changelog frontmatter 的 issue 路徑**

  ```bash
  grep -l "issues/pending/2026-05-13-spec3-phase-6-followups" docs/changelog/2026-05-14-tweak-spec3-followup-*.md \
    | xargs sed -i '' 's|issues/pending/2026-05-13-spec3-phase-6-followups|issues/resolved/2026-05-13-spec3-phase-6-followups|g'
  ```

- [ ] **Step 4: 在 4 個 changelog frontmatter 回填 commit hash**

  ```bash
  git log --oneline -10        # 找出 PR1-4 commit hash
  # 編輯 4 個 changelog 的 frontmatter，加 `commit: <hash>`
  ```

- [ ] **Step 5: Commit tracker + changelog 補回**

  ```bash
  git add docs/issues/resolved/2026-05-13-spec3-phase-6-followups.md \
          docs/changelog/2026-05-14-tweak-spec3-followup-*.md
  git commit -m "docs(spec3 followup): issue tracker 歸檔 resolved + changelog 補 commit hash"
  ```

### 7.2 Conversation history 歸檔（per CLAUDE.md 強制）

- [ ] **Step 6: 從 session JSONL 抽取本次 follow-up 對話**

  follow-up 是接續 Spec 3 task arc，但屬獨立 milestone — 建議建新資料夾：

  ```bash
  mkdir -p docs/conversation-history/2026-05-14-spec3-phase6-followup
  ```

  解析逐輪 Q&A → 拆 part-NN-of-NN-* 檔（每個 PR 或每個 Task 一份）：
  - `part-01-of-06-pre-flight-and-pr1.md`
  - `part-02-of-06-pr2-dead-code.md`
  - `part-03-of-06-pr3-completion-rate-test.md`
  - `part-04-of-06-pr4-fe-guard.md`
  - `part-05-of-06-m8-test-repo.md`
  - `part-06-of-06-wrap-up.md`
  - `SUMMARY.md`

  格式參考 `docs/conversation-history/README.md`。

  > **注意**：整個 `docs/conversation-history/` 在 `.gitignore` 內，不入版控。但仍必做（per CLAUDE.md「『做 summary』指令的含義」）。

### 7.3 Push + deploy

- [ ] **Step 7: 確認三個 repo working tree clean**

  ```bash
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-be && git status
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe && git status
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-test && git status
  ```

- [ ] **Step 8: 確認三個 repo branch + commits 領先 origin**

  ```bash
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-be && git log --oneline origin/feature/project-flow-engine-integrate..HEAD
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe && git log --oneline origin/feature/project-flow-engine-integrate..HEAD
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-test && git log --oneline origin/main..HEAD  # test repo branch 視當下而定
  ```

- [ ] **Step 9: Push 三個 repo（user 決策時機 — 計劃此時暫停等指示）**

  ```bash
  # BE
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-be && \
      git push origin feature/project-flow-engine-integrate
  # FE
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe && \
      git push origin feature/project-flow-engine-integrate
  # test
  cd ~/Projects/Billows/Audit-Manager/compliance-manager-test && \
      git push origin <branch>
  ```

  > **⚠️ Push 屬「risky action」**（CLAUDE.md「Executing actions with care」）— 計劃停在 Step 8 後，待 user 明確 say push 才執行。

- [ ] **Step 10: 部署 stg**

  由 user 主導部署流程（依專案 CI/CD 慣例）。

### 7.4 最終 summary 報告

- [ ] **Step 11: 產出 SUMMARY.md**

  存 `docs/conversation-history/2026-05-14-spec3-phase6-followup/SUMMARY.md`（gitignored）：
  - 本次 5 個 Task 各自 commit hash + 改動範圍 + 行為差異
  - 規範文件清單（4 個 changelog + 1 個 issue resolution）
  - 已知 follow-up（PR5-8 + PR-TD1/2/3 留 backlog）
  - 部署 handover 提醒（M4 是 breaking change，BE/FE 必須一起部署）

---

## 八、執行注意 / 風險點

1. **PR1 是這次最大 surface change** — 替換 3 處 call site 後一定要重啟 + 跑既有所有 oscal_audit_service test。若任一原 test 紅 → 可能 `_check_participant_role` 對 confirm_audit / activate_project 已含 `_check`（被 helper 影響），需要 case-by-case 看。
2. **terminal_close_ap 簽章變化是 breaking** — 但目前只有 TerminalCloseHandler 一個 caller，PR1 內一併改完即可。grep `terminal_close_ap` 全 codebase 應只 1-2 hit（service + handler + test）。
3. **`_sync_project_status` import path** — `ProjectQueryEntity` 在 jedi-project 套件內，pre-flight 要先確認 import path 正確。若 jedi-project 套件版本不對齊，會 ImportError。
4. **PR3 抽 static method** — `GrcProjectRepoImpl._compute_completion_rate` 用 `@staticmethod` 不依賴 `self.session`，避開 RLS / session_scope 問題（純 Python helper）。
5. **PR4 UI 驗證** — 需要 dev DB 有一筆 master_uid 軟刪的測試資料；若沒有，先 SQL UPDATE 製造再驗。
6. **M8 細節未知** — Step 1 找不到 4 fixture bug + 3 scenario 清單時必須回頭跟 user 對齊，不要憑空編造。

---

## 九、Plan vs Reality 偏差 log（pre-flight 修正紀錄）

| Tracker / Plan 初版 | 實際 | 修正位置 |
|---|---|---|
| `app/grc/repo/grc_project_repo_impl.py:746-758` | `infra/grc/repository/grc_project_repo_impl.py:755-758` | PR3 路徑 |
| terminal_close_ap signature 有 user_id | 沒有 user_id（簽章 `(self, project_uid, ap_uid, curr_user)`）| PR1 加參數而非「補上呼叫」|
| TerminalCloseHandler.execute 沒 user_id | 已有 user_id（line 153）— 只是沒傳給 service | PR1 只需把它接到 service call |
| dead code line 218-271 | line 220 raise → line 270 結束（共 50 行）| PR2 用 grep `# ── 首次稽核模式 ──` 找實際邊界 |
| PR4 假設 `defaultFlowTemplateUid` 來源是 `masterUid.value` | 實際是 `apList.value?.[0]?.flow_template?.master_uid` | PR4 Step 1（已修正 v1.1）|
| test repo 預期在 main branch | user 已於 pre-flight 後切到 `feature/project-flow-engine-integrate` | M8 開工 cd 後再次 git status 確認即可 |

---

## 文件版本

| 版本 | 日期 | 變更 |
|---|---|---|
| v1 | 2026-05-14 | 初版（Tier 1 PR1-4 + M8 收尾）|
| v1.1 | 2026-05-14 | pre-flight 後修正：PR4 `defaultFlowTemplateUid` 來源改為 `apList[0].flow_template.master_uid`；Plan vs Reality 表加第 5/6 條 |
