# PM 交叉審閱 TDD v2(2026-04-14) > 審閱者:PM Agent > 被審物:`/Users/jimchen/visionA/local-tool/.autoflow/04-architecture/TDD-v2.md` + `v2/` 全 8 份子檔 > 對照基準:`PRD-v2.md`(484 行)+ R5 五輪決策 + R5-D 補充決策 > 審閱範圍:需求面完整度、R5 / R5-D 落地、PM 5 懸念的技術回答、體驗紅線 --- ## 摘要 - **總結論:需小改後通過**(不是大改、不阻擋整體方向) - **問題數**:Major 4 個 / Minor 5 個 - **是否阻擋進開發**:**部分阻擋**——Major #1 / #2(R5-D1 / R5-D2 / R5-D3 未落地)必須在進 M8-4/M8-5 開發前補上;其餘 Major 可在 M8-10 前修正 - **整體品質**:TDD v2 對 R5 主決策的落地完整度 **90%**;對 R5-D 補充決策的落地完整度 **0%**(這是 PM 的主要不滿);對 R5 主決策以外的技術設計(state machine、LogBuffer、CORS、ffmpeg LGPL)品質很高 --- ## A. 需求完整性檢查(對 PRD v2) | PRD v2 章節 | 要求 | TDD v2 落地位置 | 檢查結果 | |------------|------|---------------|---------| | §4.1 保留功能 (8 項) | device scan / 模型管理 / 批次影像上傳 / camera / 上傳影片 / MJPEG / WebSocket / 系統 API / i18n / Dark Mode / Log 持久化 | `code-reuse-v2.md`(沿用 85-95%)+ `deletions.md` §1-6 明確區分保留 vs 刪除 | ✅ 無誤刪。`deletions.md` §2.6/2.7 小心處理 manager_test / api_e2e_test | | §4.2 砍除 9 項 | URL endpoint / url_handler / yt-dlp / 前端 URL tab / Mock 全部 / webview 載 Next.js / tray / first-run 模式選擇 / US-2 Mock | `deletions.md` §1-6 + `milestone-plan.md` M8-1/M8-2 逐檔逐行 | ✅ 全部對應;`deletions.md` §8 的驗收 grep 是漂亮的防呆 | | §4.3 新增 6 項 (N1-N6) | Wails 控制台 / 自動開瀏覽器 / Offline Overlay / boot-id / CORS / watchServer Error state | `control-panel.md`(N1 / N2)+ `web-ui-offline-overlay.md`(N3)+ `server-lifecycle.md` §9(N4)+ `cors-security.md`(N5)+ `control-panel.md` §4.7(N6) | ✅ 六項皆有專屬子檔 | | §5 US-1 ~ US-7 AC | 每個 AC 應有技術對應 | 見 §C「US AC 追蹤表」 | ⚠️ 有 2 個 AC 沒對應(見 C)| | §6 非功能 | 安裝檔 ≤ 185/165/165 MB、idle RAM ≤ 450 MB、首次推論、瀏覽器相容 | `milestone-plan.md` M8-10 有驗收 installer size;idle RAM 和首次推論時間**完全沒被估算** | ❌ **見 Major #3** | | §7 第三方授權 | ffmpeg LGPL 方案 B(三平台)+ ffprobe + macOS commit + 授權檔 | `ffmpeg-lgpl.md` 完整落地 | ✅ 很完整,甚至連 `spctl` / `codesign` / BUILD.md reproducibility 都想到 | | §8 新風險 N-R1/2/3/4 | 瀏覽器相容性 / macOS ffmpeg 維護 / 關窗誤解 / CI 測試分層 | TDD v2 §3 新增 R-v2-1 ~ R-v2-7 對應(N-R1 吸收進 R-v2-6,N-R2 吸收進 R-v2-2,N-R3 部分吸收進 AC-7.6 要求);N-R4 無明確對應 | ⚠️ **見 Minor #1**(N-R4 要求在 TDD v2 討論階段提出分層方案,TDD 只在 PM 懸念欄說「交 Testing」而沒給方案)| --- ## B. R5 / R5-D 決策落地檢查 ### B.1 R5 主決策(12 條) | 決策 | TDD v2 落地位置 | 落地狀況 | |------|---------------|---------| | R5-1(動機 A+B+G,純 127.0.0.1)| `control-panel.md` §1-2、`server-lifecycle.md` §2.1、`cors-security.md` §3 | ✅ 完整 | | R5-2(視窗關閉 = 結束 server)| `server-lifecycle.md` §2.3、§7 `OnBeforeClose` return false、§8 shutdown 順序 | ✅ 完整,且 main.go 明確寫出 OnBeforeClose hook,預留未來改動空間 | | R5-3(維持砍 tray)| `server-lifecycle.md` §2.3 註解「不 hide-to-tray」、`TDD-v2.md` §0.1 | ✅ 不存在任何 tray code | | R5-4(啟動自動開瀏覽器 + Settings 可關)| `control-panel.md` §4.6 ServerController.Start 末段、`server-lifecycle.md` §2.1 t=2.500 | ⚠️ **語意違反 R5-D3**(見 Major #1)| | R5-5(控制台 scope)| `control-panel.md` §1 明列「不做業務 UI」 | ✅ 完整 | | R5-5a(Mock 完全砍)| `deletions.md` §2 + §3 + §6、`milestone-plan.md` M8-2 | ✅ 涵蓋 Go / Wails app / 前端 / i18n / CLI flag 所有層 | | R5-6(LGPL 方案 B)| `ffmpeg-lgpl.md` §1 表格 | ✅ | | R5-6a(macOS decoder-only ~20 MB)| `ffmpeg-lgpl.md` §2.3 configure flags + §2.2 feature set | ✅ 驗收條件 `< 25 MB` 合理 | | R5-6b(macOS binary commit)| `ffmpeg-lgpl.md` §5 layout + §6 `.gitignore` | ✅ 連 `!/vendor/ffmpeg/macos/**` 的 gitignore 順序陷阱都提到 | | R5-6c(一起包 ffprobe)| `ffmpeg-lgpl.md` §2.2 末段 + §2.3 cp 指令 + §7 payload + §8 installer | ✅ 三平台全覆蓋 | | R5-7(M7 Windows 先不管)| `milestone-plan.md` M8-10 註解「R5-7 同意先不管,這次順帶驗證」 | ✅ 沒偷跑 M7 Windows,但又不漏驗 | | 共識 #14(boot-id)| `server-lifecycle.md` §9 + `web-ui-offline-overlay.md` | ✅ 端到端 | ### B.2 R5-D 補充決策(3 條)— **這是最大問題** | 決策 | 應落地位置 | 落地狀況 | |------|----------|---------| | R5-D1:Server 崩潰時保留 OS 原生通知 | `control-panel.md` §4.7 watchServer Error state / `server-lifecycle.md` §6 watchServer 或 `control-panel.md` §4 ServerController.setState | ❌ **完全缺失**。`grep -ri 'osascript|notify-send|powershell.*notification' v2/` 零匹配。TDD 只 emit `server:error` Wails event 給控制台,沒有發系統原生通知 | | R5-D2:Linux 預設 `openBrowserOnStart=OFF`,mac/Win = ON | `preferences.go`(預設值載入邏輯)或 `control-panel.md` §4.3 Preferences struct 預設 | ❌ **完全缺失**。TDD 只定義 `Preferences.OpenBrowserOnStart bool`,沒有 `NewDefaultPreferences()` 依 `runtime.GOOS` 分平台初值的邏輯。若 `preferences.json` 不存在,使用者第一次啟動在 Linux 上會用 Go zero value(false),剛好誤中,但這是 **碰巧對**,不是設計對 | | R5-D3:每次 Start 成功都自動開瀏覽器(不只首次)| `control-panel.md` §4.6 ServerController.Start 末段 | ❌ **實作與決策相反**。現況是 `if c.app.prefs.OpenBrowserOnStart && !c.app.autoOpenedThisSession` — `autoOpenedThisSession` per-session-once 只會開一次,Restart 後不再開。**這和 R5-D3 「每次啟動都自動開」語意相反**。milestone-plan.md M8-9 step 3 甚至把「防呆:autoOpenedThisSession 確保 Restart 不會二次開瀏覽器」列為驗收條件,直接落地了**錯的語意** | --- ## C. US AC 追蹤表(PRD §5 × TDD v2) | AC | 需求 | TDD 對應 | 狀況 | |----|------|---------|------| | AC-1.1/1.2 | 安裝 ≤ 3 分 / 上限 5 分 | v1 沿用,TDD 未碰 | ✅ | | **AC-1.3** | 首次啟動 Wails + server + 瀏覽器 ≤ 10 秒 | **未估算** | ❌ Major #3 | | AC-1.4 | 無硬體時顯示 empty state | `deletions.md` §6.5 noDevices 文案、R-v2-7 | ✅ | | AC-1.5 | Gatekeeper 引導 | 同 v1 沿用 | ✅ | | AC-1.6 | 可關閉自動開瀏覽器 | Preferences.OpenBrowserOnStart | ✅(但受 R5-D2 / D3 牽連)| | AC-2.1 | 日常啟動 ≤ 5 秒全就緒 | 未估算 | ⚠️ Minor #2 | | AC-2.2 | 保留 i18n / dark mode 偏好 | 前端 localStorage 沿用 | ✅(但沒寫進 TDD) | | AC-2.3 | 關過自動開瀏覽器後只開控制台 | Preferences + autoOpenedThisSession | ✅ | | AC-3.7 | 使用者關瀏覽器後不發 toast / 不發 OS 通知 | TDD 未明確處理,`control-panel.md` 沒有 toast 入口,默認符合 | ⚠️ 和 R5-D1 衝突。AC-3.7 說「不發 OS 通知」,R5-D1 說「server 崩潰時保留 OS 通知」。**兩者 scope 不同**(AC-3.7 是裝置事件,R5-D1 是 server 崩潰),TDD 都沒處理,但兩者需要分開實作 | | AC-4.5 | 拖放 .nef 只在瀏覽器有效 | `control-panel.md` §1 明列「不做業務」隱含 | ✅ | | AC-4.6 | 8 個預設 .nef + 無 Model Zoo | v1 沿用 | ✅ | | AC-5.1-5.6 | 瀏覽器 Workspace / 後端 camera pipeline / 不走 getUserMedia | v1 沿用 | ✅ | | AC-6.1-6.4 | 5 種副檔名上傳影片 / 不支援 URL | `milestone-plan.md` M8-6 + `deletions.md` §5.1 前端 accept + backend handler patch | ✅ diff 清楚 | | **AC-7.1** | 偵測斷線顯示 Offline Overlay | `web-ui-offline-overlay.md` § 全部 | ✅ | | **AC-7.2** | Overlay 文案 + Retry | `web-ui-offline-overlay.md` §4.6 i18n | ✅ | | **AC-7.3** | 控制台狀態變 Error + log panel 顯示錯誤 | `control-panel.md` §4.7 + §3 狀態 badge 紅 | ✅ | | **AC-7.4** | Restart 後 boot-id 變 → reload | `server-lifecycle.md` §2.2 + `web-ui-offline-overlay.md` §4.3 | ✅ | | AC-7.5 | 關 Wails → server 結束 → 相同 Overlay | `server-lifecycle.md` §2.3 | ✅ | | AC-7.6 | Close 按鈕 tooltip / confirm 警語 | **未落地**(Design Agent 任務,TDD 未強制 Wails main.go 加 confirm) | ⚠️ Minor #3 | --- ## D. PM 5 懸念的技術回答檢查 | # | PRD §11 問題 | 歸屬 | TDD 有無回答 | |---|-------------|------|------------| | 1 | 自動開瀏覽器 Settings 與 Web UI Settings 資料是否同一份 | Architect | ✅ `control-panel.md` §4.1 明確說「`visiona-local/preferences.go` 存 `/preferences.json`」— 這是**控制台側**的偏好;Web UI 側的 i18n / dark mode 仍用 browser localStorage(v1 沿用)。兩邊**分離**,PM 接受這個答案 | | 2 | AC-1.3 10 秒預算是否可達 | Architect 估算 | ❌ **完全沒估算**。TDD 沒有任何段落討論「Wails 冷啟動 + Go server waitHealthy + 瀏覽器冷啟動」的時間預算分解。PRD 明確要求 Architect 驗證此預算 | | 3 | idle RAM ≤ 450 MB 可達性 | Architect 實測 | ❌ **完全沒提**。TDD 沒有任何段落討論 idle RAM 估算(v2 砍了 Mock state 和 yt-dlp 子程序後的 baseline) | | 4 | N-R4 CI / E2E 測試分層方案 | Testing Agent(現階段不審) | — 略過 | | 5 | 「由 visionA-local 跑」常駐徽章 | Design Agent(現階段不審) | — 略過 | → **PM 5 懸念中的 2/3(AC-1.3、idle RAM)完全沒被回答。這兩題是 PRD §11 明列「交 Architect」的題目,Architect 沒回等於把球踢回給 PM。** --- ## E. 臆測 / 誤刪 / 誤解清單 ### E.1 臆測超出範圍 - **無**。TDD v2 沒有偷偷新增 LAN mode、tray fallback、daemon、auto-update、telemetry 等 R5 明確否決的功能。這點乾淨 ### E.2 誤刪 - **無誤刪**。批次影像上傳(R5 共識 10 / PRD §4.1)在 `deletions.md` 中未被列入刪除清單,`code-reuse-v2.md` 也把它放在沿用區。`camera_handler.go` 只砍 `StartFromURL`,沒碰 `UploadBatch` 相關 handler - **但有一個 risky 邊界**:`deletions.md` §1.1 說「若 `NewVideoSourceFromURL` **唯一呼叫者**是 StartFromURL → 一起砍」。實際 grep 顯示 `camera_handler.go:731` 還有 `NewVideoSourceFromURLWithSeek` 被本地上傳影片 seek 呼叫。**M8-1 執行者必須先把這個 grep 結果列出**,否則可能會誤砍本地影片 seek 功能。TDD `ffmpeg-lgpl.md` §11 已警示這點,但 `deletions.md` §1.1 的敘述「可從 RTSP stream 呼叫」不精準,實際是「從本地影片 seek 呼叫」 ### E.3 誤解 R5 / R5-D - **R5-4 理解錯誤**(**Major #1**):TDD 把「首次啟動自動開瀏覽器」實作成 per-session-once(`autoOpenedThisSession` flag),但 R5-D3 明確修正語意為「每次 Start Server 成功都開」。這個誤解在 `milestone-plan.md` M8-9 和 `control-panel.md` §4.6 各出現一次,是**系統性誤解** - **R5-D1 / R5-D2 整組遺漏**(**Major #1** 包含) - **R5-2 理解正確**:TDD 明確寫 `OnBeforeClose` return false、不 hide-to-tray、有 `shutdownGracePeriod` 5s → 10s 對齊 server 的討論。這點很到位 --- ## F. 體驗紅線檢查 ### F.1 北極星指標「5 分鐘內瀏覽器跑第一幀推論」可達嗎 PRD §1.4 把「瀏覽器啟動時間」也納入 5 分鐘預算。TDD 沒有做端到端預算分解,但從元件看: - Wails 冷啟動:~0.5-1 s(v1 經驗) - Go server spawn + waitHealthy:1-3 s(依 Python sidecar 啟動) - `OpenInBrowser("")`:系統 call ~0.1 s - 瀏覽器冷啟動(第一次點開 Chrome/Safari/Edge):**3-8 s**(最大不確定性) - Next.js static export 載入:~0.5-1 s → 在已開瀏覽器的情境下 5-6 s 可達 AC-2.1 的「日常啟動 ≤ 5 秒」;在冷啟動情境下 6-13 s **很可能超過 AC-1.3 的 10 秒上限**。 **PM 結論**:AC-1.3 的 10 秒上限在「使用者尚未開過瀏覽器」的情境下可能不可達。建議 Architect 在 TDD v2 增補一段預算分解,若確實超過,PRD 要放寬到 15 秒。這是進開發前必須釐清的事項(見 Major #3)。 ### F.2 race condition:Wails 關閉視窗 → StopServer 過程中瀏覽器 tab TDD v2 `R-v2-4` 已明確提到這個 race condition(0.5-5s 內瀏覽器可能看到 ECONNREFUSED 但 Overlay 還沒觸發),並分析為「實務上使用者關 Wails 通常也會關瀏覽器 tab,race 不構成實際問題;若真的沒關,15 s 後 Overlay 會出現」。**PM 認可這個取捨**,但有補充意見: - **建議**:在 Wails OnBeforeClose 觸發前,先透過 WebSocket broadcaster 發一個 `server:shutdown-imminent` 訊息給所有連著的瀏覽器 tab(利用現有 `/ws/server-logs` 或 `/ws/devices/*` 通道),前端收到後立即顯示 Overlay,不需要等 15 s polling。這個成本低(server 端只 3-5 行、前端 store 新增一個 listener 即 10 行) - 若 Architect 評估成本過高 → 接受現狀。這是 **Minor #4**,不阻擋進開發 ### F.3 「關閉視窗 ≠ 結束 server」的心智負擔(N-R3) PRD §8.2 N-R3 要求三處文案到位: 1. Wails Close 按鈕 tooltip / confirm dialog(AC-7.6)— **TDD 未落地**(main.go 只寫了 `OnBeforeClose: return false`,沒加 confirm 對話框邏輯;是否加是 Design Agent 的 scope,但 TDD 應該**預留 hook**) 2. Offline Overlay 文案引導 — `web-ui-offline-overlay.md` §4.6 ✅ 3. First-Run 歡迎頁一句說明 — TDD 未提(但 v1 有,v2 沿用,推測 OK) → **Minor #3**:TDD 應在 `server-lifecycle.md` §7 的 main.go 範例裡保留 `OnBeforeClose` 可讀 `Preferences.ConfirmOnClose` 的 hook,而不是寫死 `return false`。這樣 Design Agent 後續加 confirm dialog 時不用再重寫 TDD --- ## G. PM 對 Architect Q4 的回答 Architect 在 `server-lifecycle.md` §10 問:「`shutdownGracePeriod` 5s → 10s 的副作用?使用者頻繁開關 app 可能感覺變慢。」 **PM 回答**(基於使用者體驗基準): | 問題 | 答案 | |------|-----| | 總 grace period 建議值 | **7 秒**(而非 10 秒),理由:Nielsen Norman 的研究指出 1 秒內 = 即時反饋、10 秒 = 使用者注意力臨界點;7 秒保留足夠 server 優雅結束餘量,又不會逼近「我是不是當機」的感受閾值 | | 超過多久要顯示「停止中…」進度提示 | **1 秒**。Wails 收到關閉訊號後應立即(< 100 ms)顯示一個不可關閉的 modal「正在停止本機伺服器…」+ spinner,避免使用者看到「空白卡住」。這個 modal 在 7 秒後若 server 仍未退出,換文字為「伺服器未正常結束,正在強制關閉…」,之後 SIGKILL | | 如果 7 秒真的不夠(server 端某些清理需要 > 7 秒)| 兩個選擇:(a) 降低 server 端清理時間(理想)(b) 把 server 端的 `shutdownFn` 10 s timeout 改為 6 s,與 Wails 7 s 留 1 s 緩衝 | **建議**:採用 PM 的 7 秒 + modal 方案,這是比 Architect 提的「5-10 秒區間 emit event」更簡單(不需要 event 機制)且更符合 UX 基準的做法。Architect 若有技術面反對意見,請在使用者確認前提出。 --- ## H. 問題清單 ### Major(阻擋進開發,必須在 M8-4 / M8-5 開發前修) #### Major #1:R5-D1 / R5-D2 / R5-D3 三條補充決策完全沒落地 **問題**:R5-D 三條是使用者在 2026-04-14 Design v2 產出後的補充決策,時間上**晚於 TDD v2 draft**,但 TDD v2 必須被修正以落地這三條。現況: - **R5-D1(崩潰時保留 OS 原生通知)**:TDD 只在 Wails 控制台發 `server:error` event,沒叫 `osascript display notification` / `powershell New-BurntToastNotification` / `notify-send` - **R5-D2(Linux 預設自動開瀏覽器 OFF)**:`preferences.go` 沒有依 `runtime.GOOS` 分平台 default 的邏輯 - **R5-D3(每次 Start 都開,不只首次)**:`autoOpenedThisSession` flag 和 milestone-plan M8-9 驗收條件明確是 per-session-once,**和決策相反** **應加在**: | 決策 | 子檔 | 修正位置 | |------|------|---------| | R5-D1 | `control-panel.md` §4.7(watchServer Error state)+ 新增 §4.8「OS 原生通知 helper」 | 新增 `visiona-local/notify_darwin.go` / `notify_windows.go` / `notify_linux.go` 三檔,在 `ctrl.setState(ServerStateError, ...)` 時呼叫 | | R5-D2 | `control-panel.md` §4.3 Preferences 型別 + `v2/server-lifecycle.md` §2.1 載入流程 | 新增 `NewDefaultPreferences() Preferences`:`runtime.GOOS == "linux"` → `OpenBrowserOnStart: false`,否則 `true`。`preferences.go` 的 load 若檔不存在,用這個預設 | | R5-D3 | `control-panel.md` §4.6 `ServerController.Start` 末段 + `milestone-plan.md` M8-9 step 3/5 驗收條件 | **砍 `autoOpenedThisSession` flag**,改為:只要 `prefs.OpenBrowserOnStart == true` 就每次 Start 成功後 `OpenInBrowser("")`。M8-9 驗收改為「多次按 Restart:每次都開新 tab(或 focus 既有 tab,瀏覽器行為決定)」 | **阻擋等級**:Major,必須在 M8-4/M8-5 開發前修 TDD 子檔。因為這三條會影響 `preferences.go` / `server_control.go` / 新增的 notify_*.go 的介面簽名,若先開發再改會浪費時間 --- #### Major #2:R5-D3 誤解已被寫進 milestone-plan M8-9 驗收條件 **問題**:`milestone-plan.md` 第 335 / 347 / 350 行把「Restart 不會二次開瀏覽器」列為**驗收成功**條件。這會在 Reviewer 審查 M8-9 時被當成「通過」,但其實這**違反 R5-D3**。 **修正**:M8-9 step 3 改寫,驗收條件改為: - 首次啟動 Server:瀏覽器開 1 個 tab - 手動按 Restart:瀏覽器開第 2 個 tab(或在瀏覽器自動重用既有 tab,視瀏覽器行為) - 使用者在 Preferences 關掉 `openBrowserOnStart`:之後 Start/Restart 都不開瀏覽器 **阻擋等級**:與 Major #1 綁在一起修 --- #### Major #3:PM 5 懸念中 AC-1.3(10 秒)與 idle RAM(≤ 450 MB)沒被 Architect 回答 **問題**:PRD §11 明確把這兩題交給 Architect「TDD v2 驗證」,但 TDD 所有 8 個子檔都找不到任何段落在討論這兩個預算。 **應加在**: - **AC-1.3 10 秒可達性**:建議在 `TDD-v2.md` §1 或 `server-lifecycle.md` §2.1 後新增一節「冷啟動預算分解」: ``` Wails 冷啟動 ~0.5-1 s migrateOldDataDirs ~0.01 s startIPCServer ~0.05 s seedUserDataDir ~0.02-2 s(首次安裝時 copy 8 個 .nef,約 73 MB) ensurePythonRuntime ~0.5-3 s(首次啟動 bundled Python extract) exec.Command spawn ~0.01 s waitHealthy (Python sidecar 起) ~1-3 s OpenInBrowser system call ~0.1 s 瀏覽器冷啟動 ~3-8 s(使用者尚未開瀏覽器) Next.js SSG 首次載入 ~0.5-1 s ──────────────────────── 首次安裝冷啟動總計 ~5.5-18 s 回訪情境(瀏覽器已開)~2.5-10 s ``` 結論:**AC-1.3 的 10 秒上限在首次安裝 + 冷啟動瀏覽器情境下不可達**,建議 PRD 放寬到 15 秒,或在 PRD AC-1.3 明確註記「若瀏覽器尚未啟動,可能延長到 15 s」 - **idle RAM ≤ 450 MB**:建議在 `TDD-v2.md` §3 風險清單新增一條,或在 `control-panel.md` 新增一節「資源估算」: ``` Wails 殼 ~80-120 MB(WebView2 / WKWebView 基礎) Go server 子程序 ~40-60 MB(Gin + 8 個預載入 .nef metadata) Python sidecar ~150-220 MB(Python runtime + numpy + opencv + pyusb) logs/ LogBuffer 等 ~5 MB ──────────────────── 總計 ~275-405 MB ``` 結論:**idle RAM ≤ 450 MB 應可達**,上限 550 MB 安全。PM 可以接受此估算作為 TDD 回答 **阻擋等級**:Major,必須在使用者確認 TDD v2 前補上。若 10 秒不可達,要決定是放寬 AC-1.3 還是改架構(例如 Wails 內建 WebView 搭配預先隱藏視窗的 fallback) --- #### Major #4:shutdownGracePeriod 5s → 10s 的使用者體感取捨未解決 **問題**:TDD `server-lifecycle.md` §10-2 把這題列為「待確認」,但這是**體驗面**的問題,不是技術面,Architect 不該把球踢回使用者。 **PM 回答**:見 §G。採用 **7 秒 + 1 秒內顯示「停止中…」modal** 方案。 **應加在**:`server-lifecycle.md` §8 把 `shutdownGracePeriod` 改為 7 s,§5 ServerController.Stop 新增 `emit "server:stopping"` event,前端新增「stopping modal」元件(`control-panel.md` §5 action-bar.js 補),M8-5 驗收條件加「關閉視窗後 1 秒內看到停止中 modal」 **阻擋等級**:Major,但範圍小。可併入 M8-4 / M8-5 開發 --- ### Minor(可先進開發,後續修) #### Minor #1:N-R4(CI / E2E 測試分層)沒有方案 PRD §8.2 明確要求 Architect 在 TDD v2 討論階段提出分層方案。TDD 沒提。但這題現階段交 Testing Agent,暫不阻擋。 **建議**:在 `milestone-plan.md` M8-10 新增一段「測試分層建議」,或在 TDD v2 §3 風險區新增 R-v2-8 占位,標註「待 Testing Agent 審 TDD 時補方案」 --- #### Minor #2:AC-2.1(日常啟動 ≤ 5 秒)沒估算 和 Major #3 同源。若 Major #3 補上冷啟動預算分解,順便也覆蓋 AC-2.1 --- #### Minor #3:AC-7.6(Wails Close 按鈕 tooltip / confirm 警語)沒落地 TDD `server-lifecycle.md` §7 的 main.go 範例寫死 `OnBeforeClose: return false`。這等同「直接關,無警告」,和 PRD AC-7.6 要求的「明確寫『關閉此視窗會結束 Local Server』」衝突。 **建議**:TDD 應預留 hook,例如: ```go OnBeforeClose: func(ctx context.Context) (prevent bool) { if app.prefs.ConfirmOnClose { // TODO: Design Agent 決定 confirm dialog 文案 // 用 wailsRuntime.MessageDialog 跳對話框 } return false }, ``` 這樣 Design Agent 後續加 dialog 時不用再修 main.go。是否顯示 confirm 由 Design Agent 決定,但 hook 要先留 --- #### Minor #4:Wails 關閉時可主動通知瀏覽器 tab 避免 15 s race condition 見 §F.2。建議在 Wails OnBeforeClose 之前透過 WebSocket 廣播 `server:shutdown-imminent` 訊息,前端立即顯示 Overlay。成本低(server 端 ~3 行、前端 ~10 行),體驗提升明顯。 **阻擋等級**:Minor,可在 M8-10 smoke test 發現體驗不佳時補 --- #### Minor #5:`deletions.md` §1.1 「RTSP stream 可能呼叫 NewVideoSourceFromURL」敘述不精準 實際 grep 顯示 `camera_handler.go:731` 用 `NewVideoSourceFromURLWithSeek` 處理**本地上傳影片的 seek**,不是 RTSP。若 M8-1 執行者照字面理解,會把「不是 URL 但共用函式」的本地影片 seek 功能誤砍。 **修正**:`deletions.md` §1.1 文字改為「若唯一呼叫者是 `StartFromURL` → 整個砍;若仍有本地影片 seek 呼叫者(camera_handler.go:731 `NewVideoSourceFromURLWithSeek`)→ **函式保留**,但 rename 去掉 `FromURL` 字眼避免誤解,例如改為 `NewVideoSourceWithSeek`,並把 `--disable-network` 這項 configure flag 保留(因為本地檔案讀取走 `protocol=file` 不需 network)」 --- ## I. 通過 / 不通過 結論 ### 結論:**條件通過** | 條件 | 必須完成 | |------|---------| | Major #1 + Major #2 修完(R5-D1/D2/D3 落地 + milestone-plan 驗收條件修正)| **進 M8-4 / M8-5 前必須** | | Major #3 補上冷啟動預算分解 + idle RAM 估算 | **使用者確認 TDD v2 前必須** | | Major #4 採用 7 秒 + modal 方案 | **進 M8-4 / M8-5 前必須**(介面簽名受影響)| | Minor #1-#5 | 可先進開發,M8-10 smoke test 前補齊即可 | ### 整體評價 **TDD v2 品質:8.5/10**(扣分在 R5-D 整組遺漏 + PM 5 懸念 2 題沒回答)。 - ✅ **值得稱讚**: - `ffmpeg-lgpl.md` 的完整度(configure flags、BUILD.md reproducibility、spctl 驗證、gitignore 陷阱) - `deletions.md` 的「按表操作 + 驗收 grep」設計,防呆到位 - R5-2 / R5-3 的處理乾淨俐落(不存在任何 tray 殘跡) - state machine 五態的思考完整(`txMu` vs `mu` 雙 mutex 對抗 race) - 新風險 R-v2-1 ~ R-v2-7 對應具體且有緩解 - milestone-plan.md 的依賴圖清晰,支援平行開發 - ❌ **需要補強**: - R5-D 三條補充決策**整組遺漏**,這是 PM 的主要不滿。Architect 需要在使用者回答 R5-D 後,立即在 TDD v2 出一個 patch - PM 5 懸念的 AC-1.3 與 idle RAM **完全沒回答**,PRD 明確要求 Architect 在 TDD v2 驗證 - UX 取捨(shutdownGracePeriod)被當成技術問題丟回使用者 ### 下一步建議 1. Orchestrator 啟動 Architect Agent,做 **TDD v2 patch**,聚焦修 Major #1 ~ #4 2. 修完後 Orchestrator 再啟動 PM 做第二輪審閱(只審 patch 部分,~30 分鐘) 3. 同時 Orchestrator 啟動 Design Agent 做 Design 交叉審閱(本輪 PM 審閱發現 AC-7.6 是 Design 的未決項) 4. Design + PM 兩輪都通過後交使用者確認 → 進 M8-1 ~ M8-3(互不依賴的三個砍除/ffmpeg milestone 可立即啟動,不受 Major #1/#2 影響,加速開發) --- ## 附錄:對照資料 - **PRD v2**:`/Users/jimchen/visionA/local-tool/.autoflow/02-prd/PRD-v2.md`(484 行) - **TDD v2 索引**:`/Users/jimchen/visionA/local-tool/.autoflow/04-architecture/TDD-v2.md`(136 行) - **TDD v2 子檔 8 份**:`/Users/jimchen/visionA/local-tool/.autoflow/04-architecture/v2/`(3738 行) - **R5 / R5-D 決策**:`/Users/jimchen/visionA/local-tool/.autoflow/progress.md` §「R5 第五輪」+ §「R5-Design 補充」 --- # PM 第二輪審閱 TDD v2.1(2026-04-14) > 審閱者:PM Agent > 被審物:`TDD-v2.md` v2.1 + 9 份 `v2/*.md` 子檔(新增 `startup-pipeline.md`) > 審閱範圍:只審 v2.0 → v2.1 補丁(Major × 4 + Minor × 5 + R5-E 落地 + Architect 自補),不重審 v2.0 既有內容 ## 摘要 - **總結論:通過(附 1 個 Minor 餘項)** - 第一輪 **Major × 4 全部修好**;Minor × 5 修好 4 個 + 1 個以另一等效方案處理 - **R5-E1~E6 六條全部落地**(`v2/startup-pipeline.md` 新檔 518 行,程式碼級完整) - **不阻擋進 M8 開發**。M8-1/M8-2/M8-3/M8-8 可立即啟動;M8-4/M8-4b/M8-5/M8-9 在 Design v2.1 文案完成後即可啟動 --- ## A. 第一輪 Major 修復檢查 | # | 問題 | 檢查位置 | 結果 | |---|-----|---------|------| | **Major 1 R5-D1 OS 通知** | `server-lifecycle.md` 新增 §10(L690-791)含 `notify.go` + macOS `osascript` / Linux `notify-send -u critical` / Windows PowerShell BurntToast + `msg *` fallback;`control-panel.md` L482-486 在 Start 失敗時 `go sendCrashNotification(...)`;§6 diff(L405-408)在 watchServer 3 次失敗時 fire-and-forget;`startup-pipeline.md` L295-299 pipeline 失敗時一併發通知 | ✅ 完整。觸發點三處(Start 失敗 / watchServer 3 次失敗 / startup pipeline 失敗)都有 non-blocking `go` 呼叫 | | **Major 1 R5-D2 Linux 預設 OFF** | `server-lifecycle.md` §11.3(L824-850)`DefaultPreferences()` 依 `runtime.GOOS != "linux"` 分平台;`control-panel.md` L169 + L498-500 註解對應;`milestone-plan.md` M8-9 驗收 L404-405 分 macOS/Win vs Linux 兩條 | ✅ 完整 | | **Major 1 R5-D3 每次都開** | `control-panel.md` `ServerController.Start` L491-503 直接檢查 `if c.app.prefs.AutoOpenBrowser` + 註解明確「取消 v2.0 的 autoOpenedThisSession flag」;`server-lifecycle.md` L923 明示「已移除」;`milestone-plan.md` L393 / L411 / L439 反覆強調砍除 | ✅ 完整。但 `code-reuse-v2.md:92` 有殘留(見 §G Minor)| | **Major 2 M8-9 驗收條件** | `milestone-plan.md` M8-9 L407「多次按 Restart:若 `AutoOpenBrowser=true`,每次 Restart 都會呼叫 `OpenInBrowser`」L411 Reviewer 檢查重點「不存在 `autoOpenedThisSession` 欄位」 | ✅ 完整。原「Restart 不會二次開」條件已全數砍除 | | **Major 3 §11-1 Preferences 持久化** | `server-lifecycle.md` §11 全段(L795-924):§11.1 `/preferences.json` + §11.2 struct 定義 + §11.3 DefaultPreferences + §11.4 讀寫時機 + L879 atomic write-rename | ✅ 完整 spec | | **Major 3 §11-2 冷啟動預算** | 被 R5-E 取代(索引 L35 明示「原 10 秒可達性已被 R5-E 60 s 上限 + 階段化進度取代」);`server-lifecycle.md` §2.1a(L83-90)補上日常啟動 ~3.8 s 估算 | ✅ 以 R5-E 取代是合理方案 | | **Major 3 §11-3 450 MB 範圍** | `server-lifecycle.md` §11.6(L925-943)明確「450 MB 目標不含瀏覽器 tab 的記憶體消耗」+ 量測條件定義 + 悲觀情境 50 MB 超標時的 fallback | ✅ 完整 | | **Major 4 shutdown 7+1** | `server-lifecycle.md` L125 PM Q4 定案「7 秒 + 1 秒 modal」;L527 `shutdownGracePeriod = 7 秒`;L550 `graceTimer := time.NewTimer(7 * time.Second)`;§8(L499-521)含廣播 + 1 秒 modal + SIGKILL 完整順序;§2.3 時序 t=1.000 `shutdown:modal-show` | ✅ 完整,採 PM 方案 | **結論**:4 個 Major 全部修好。 --- ## B. 第一輪 Minor 修復檢查 | # | 問題 | 檢查 | 結果 | |---|-----|------|------| | Minor 1 N-R4 測試分層 | `control-panel.md` L826 標 `blocked-on-testing-agent` | ✅ 按約定處理 | | Minor 2 AC-2.1 ≤5 s | `server-lifecycle.md` §2.1a(L83-90)日常啟動 ~3.8 s 估算表 + 結論「遠低於 60 s 上限」| ✅ | | Minor 3 OnBeforeClose confirm hook | `server-lifecycle.md` §7(L475-479)寫死 `return false` + 註解「不跳確認對話框」,**未預留 `ConfirmOnClose` hook**;但 Architect 在 L490-493 解釋「Wails v2 default 就是直接關,與 R5-2 語意一致,不加 hook」 | ⚠️ 以等效方案處理。PM 第一輪建議是「預留 hook」方便未來改動,Architect 選擇「保持乾淨不加 dead code」。兩者語意均不彈對話框,符合 R5-2;PM 接受此取捨,**非阻擋** | | Minor 4 WebSocket shutdown-imminent | `server-lifecycle.md` §2.3 t=0.005 / §8 L511 / `web-ui-offline-overlay.md` §3.2a(L78-95)+ L158 新增 `use-shutdown-watcher.ts` + L361 訂閱處理;server 新增 `/ws/system` endpoint;Wails `ctrl.Stop()` SIGTERM 前先廣播 | ✅ 完整(server + 前端 + Wails 三側齊全)| | Minor 5 deletions.md §1.1 精準化 | `deletions.md` L38-94 新增 v2.1 Minor 5 段落:明列 `grep -rn 'NewVideoSourceFromURL'` 命令 + `camera_handler.go:731` 是 `handleVideoSeek` 的 `videoIsURL` dead code + 整個 function 砍 + `videoIsURL` field 砍 + 驗收 grep | ✅ 完整,甚至列出驗收 grep 指令 | **結論**:5 個 Minor 全部處理(1 個以等效方案)。 --- ## C. R5-E 落地檢查(重點) | # | 預期 | 位置 | 結果 | |---|-----|------|------| | **R5-E1** 60 s hard timeout | `startup-pipeline.md` L156 `startupHardTimeout = 60 * time.Second` + L325-330 watcher 檢查 `sinceTotal > startupHardTimeout` → `Fail()` | ✅ | | **R5-E2** 6 階段化 | L154 `startupTotalStages = 6` + L167-177 `StartupPipeline` struct + §3 6 階段表(L118)+ L422 前端 render 6 rows | ✅ | | **R5-E3** 20 s soft timeout 顯示重試提示 | L155 `startupSoftTimeout = 20 * time.Second` + L332-346 watcher 檢查 `sinceStage > startupSoftTimeout` → emit `startup:stage-timeout` + `softTimeoutEmitted` flag 防重複 | ✅ | | **R5-E4** 60 s Error state + 三按鈕 | L325-330 hard timeout 呼叫 `Fail()` + `emitError()` + L292-294 同步叫 `ctrl.setState(ServerStateError, ...)`;三按鈕(重試/檢視 log/回報)在 `startup-panel.js` `showStartupError()`(L469+)和 Design Spec 對齊 | ✅ | | **R5-E5** 階段文字交 Design | `startup-pipeline.md` §2(L87-105)只定義 i18n key(`startup.stage.{1-6}.label`),實際文字留給 `i18n/zh-TW.json` / `en-US.json`(Design 填);L517 明示「R5-E5 Design Spec v2.1 尚未敲定,i18n key 已預留」 | ✅ 零硬編碼文字,完全交 Design | | **R5-E6** WebSocket 連上 = 就緒 | `startup-pipeline.md` L22 R5-E6 明文;§3 L118 階段 6 定義「`OnClientConnected` 第一次觸發」;L120-130 兩種實作方式(long-poll endpoint / sentinel file)留給 M8-4b 執行者選擇;L515 `startup-pipeline.md` 末段留 A-Q1 待 M8-4b 決定 | ✅ 設計完整,實作方式待 M8-4b 決定(PM 接受)| **結論**:R5-E 六條全部落地,且程式碼級細節(event schema、1-indexed 陣列、非阻塞 EventsEmit、softTimeoutEmitted 防重複、watcher 生命週期管理)皆嚴謹。 --- ## D. TDD ↔ PRD v2.1 對齊 | PRD v2.1 AC | TDD v2.1 對應 | 結果 | |------------|--------------|------| | AC-1.3 60 s 硬上限 | `startup-pipeline.md` L156 + `TDD-v2.md` 索引 L18 | ✅ | | AC-1.3a 6 階段進度 | `startup-pipeline.md` §3 + §6 前端 render | ✅ | | AC-1.3b 20 s 重試提示 | `startup-pipeline.md` L332-346 | ✅ | | AC-1.3c 60 s Error state + 三按鈕 | `startup-pipeline.md` L325-330 + `startup-panel.js` showStartupError | ✅ | | AC-1.3d 階段文字 Design | `startup-pipeline.md` L517 | ✅ | | AC-7.7 OS 通知並存 | `server-lifecycle.md` §10 三平台 + L791「兩者互補,不取代」 | ✅ | | AC-2.1 日常啟動 ≤5 s | `server-lifecycle.md` §2.1a 日常啟動 ~3.8 s | ✅ | | AC-2.1a 每次 Start 都自動開 | `control-panel.md` L491-503 | ✅ | --- ## E. Architect 自補項檢查 - **F-2 Restart port 強制保留**:`server-lifecycle.md` §3.2(L179-224)完整,含 `pickPort(preferredPort, forceMatch=true)` + 使用者情境解釋 + port 被佔則進 Error state + 廣播 shutdown-imminent ✅ - **階段 6 WebSocket 實作細節**:`startup-pipeline.md` L120-130 列出 long-poll endpoint / sentinel file 兩方案,L515 明示「M8-4b 執行者決定並回報」。PM 接受此處延後決策,因為兩方案介面簽名相容,不影響上游 M8-4 開發 ✅ - **boot-id crypto/rand 16 bytes**(Q3):索引 L29 v2.1 變更條目記錄,不引額外依賴 ✅ - **navigator.language fallback 強化**(Q5):索引 L30 記錄 ✅ --- ## F. 工時合理性 v2.0 10 人天 → v2.1 12 人天(+2 天),增量分解: - R5-E 階段化啟動 +1 天(M8-4b 新 milestone) - R5-D1 OS 通知 +0.3 天 - PM Q4 7+1 shutdown modal +0.2 天 - Minor 4 WebSocket 廣播 +0.3 天 - 其餘(M8-10 驗收項目擴充)+0.2 天 合計 2.0 天,與 milestone-plan.md L491 總計相符。 **PM 接受 12 人天**;另 L493 建議「加 M8-3 / M8-4 buffer 1 天 → 對使用者回報 ~13 人天」也合理。 **輕微瑕疵**:`milestone-plan.md` L6 文字摘要寫「~11.5 人天」,L491 合計表寫「12.0」。摘要行未同步更新,屬文字不一致。非阻擋。 --- ## G. 第二輪新發現問題 ### Major 無。 ### Minor #### Minor 6(新):`code-reuse-v2.md:92` 殘留 v2.0 字串 **問題**:`v2/code-reuse-v2.md` L92 仍然寫 ``` 第 83 行:砍 `mockMode` 欄位;新增 `ctrl` / `logBuf` / `prefs` / `autoOpenedThisSession` 欄位 ``` **但** `milestone-plan.md:147` / `server-lifecycle.md:923` / `control-panel.md:811` 全部明示「**不存在** `autoOpenedThisSession`」。這是 v2.0 → v2.1 轉版時 `code-reuse-v2.md` 沒同步更新。 **影響**:M8-4 / M8-5 的工程師 Agent 若以此為唯一指引,可能加回 `autoOpenedThisSession` 欄位 → 違反 R5-D3。 **修正**:`code-reuse-v2.md:92` 移除 `autoOpenedThisSession` 字樣。一行 diff。 **阻擋等級**:Minor。不阻擋進開發(因其他三處指引都正確),但進 M8-5 前最好修掉避免混淆。 #### Minor 7(新):`milestone-plan.md:6` 摘要 11.5 vs 合計 12.0 見 §F。非阻擋,下次順手改。 --- ## H. 第二輪通過 / 不通過 結論 ### 結論:**通過**(附 Minor 6 / Minor 7 可隨開發過程修) | 項目 | 第一輪 | 第二輪 | |------|-------|-------| | Major | 4 | **0** | | Minor | 5 | **2**(新 Minor 6 / 7,非阻擋)| | R5-E 落地 | — | **100%** | | 是否阻擋進 M8 開發 | 部分阻擋 | **不阻擋** | | 整體品質 | 8.5/10 | **9.3/10** | ### 稱讚 - R5-D1 OS 通知的三個觸發點(Start 失敗 / watchServer 3 次失敗 / startup pipeline 失敗)全覆蓋 + 非阻塞 goroutine 設計完美 - `startup-pipeline.md` 是一份**程式碼級**的子檔(518 行 Go + JS 全部寫出),Reviewer 可以直接比對 - R5-E5 階段文字零硬編碼,完全以 i18n key 交給 Design,等 Design Spec v2.1 敲定就能直接填 JSON - §11.6 的 450 MB 範圍澄清 + 悲觀情境 fallback 策略很到位 - `deletions.md` Minor 5 修正後的驗收 grep 命令可直接複製執行,防呆 ### 下一步 1. 使用者確認 TDD v2.1 + Design Spec v2.1 → 即可進 M8 開發 2. **M8-1 / M8-2 / M8-3 / M8-8 可立即並行啟動**(互不依賴;Major 修復未觸及此四者) 3. **M8-4 / M8-4b / M8-5 / M8-9** 依賴 `DefaultPreferences()` / `sendCrashNotification()` / `StartupPipeline` / Preferences 持久化,這四者 TDD v2.1 都已 spec 完整可開工 4. Minor 6 / Minor 7 建議 M8-5 啟動前或開發過程中順手修 5. 本輪 PM 審閱完成,不需第三輪 - **審閱耗時**:~1 小時(含所有子檔閱讀 + 交叉對照)