依 R5 五輪決策把 visionA-local 從「Wails 內嵌 Next.js」重構為「Wails
本機伺服器控制台 + 瀏覽器 Web UI」模式(類比 Docker Desktop / Ollama)。
程式碼變動
- M8-1 砍 yt-dlp 全套(後端 resolver / URL handler / 前端 URL tab /
Makefile vendor / installer / bootstrap / CI workflow,-555 行)
- M8-2 砍 Mock 模式全套(driver/mock、mock_camera、Settings runtimeMode、
VISIONA_MOCK 環境變數,-528 行)
- M8-3 ffmpeg 從 GPL 切換到 LGPL 混合方案:Windows/Linux 用 BtbN 現成
LGPL binary,macOS 自 build minimal decoder-only 進 git
(vendor/ffmpeg/macos/ffmpeg 5.7MB + ffprobe 5.6MB,比 GPL 版省 85% 空間)
- M8-4 Wails Server Controller:state machine、log ring buffer 2000 行、
preferences.json atomic write、boot-id、Gin SkipPaths、shutdown 7+1 秒、
notify_*.go 三平台 OS 通知、watchServer 改 Error state 不 os.Exit
- M8-4b 啟動階段管線 R5-E:6 階段進度 event、20s soft / 60s hard timeout、
stage 5/6 skip 規則、sentinel file、RestartStartupSequence 5 步驟
- M8-5 Wails 控制台 vanilla HTML/JS/CSS(9 檔 ~2012 行)取代 M7-B splash:
state 視覺、log panel、startup progress panel、Stage 6 manual CTA
pulse、shutdown modal、Settings、Dark Mode、i18n 中英雙語
- M8-6 上傳影片副檔名擴充(mp4/avi/mov/mpeg/mpg)
- M8-7 Web UI Server Offline Overlay(role=alertdialog + focus trap +
wsEverConnected 容錯 + Page Visibility)
- M8-8 CORS middleware(127.0.0.1/localhost only + suffix attack 防護)+
ws/origin.go 獨立 WebSocket CheckOrigin 避 package cycle
- MAJ-4 server:shutdown-imminent WebSocket broadcast 機制
(/ws/system endpoint + notifyShutdownImminent helper)
- M8-9 Boot-ID + 瀏覽器 tab 自動重連(sessionStorage loop guard)
品質
- ~105+ 新 unit test + race detector (-count=2) 全綠
- 10 個 milestone 全部通過 Reviewer 審查
- 三方 v2 + v2.1 文件(PRD / Design Spec / TDD)+ 交叉互審紀錄
收錄在 .autoflow/
交付前待處理(M8-10)
- 重跑 make payload-macos 把舊 GPL 77MB binary 換成新 LGPL
- 三平台 end-to-end build 驗證
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
552 lines
35 KiB
Markdown
552 lines
35 KiB
Markdown
# Architect 交叉審閱 Design Spec v2(2026-04-14)
|
||
|
||
> 審閱者:Architect Agent
|
||
> 對象:`03-design/design-spec-v2.md` + `03-design/v2/*.md`
|
||
> 基準:`04-architecture/TDD-v2.md` + `04-architecture/v2/*.md`
|
||
|
||
---
|
||
|
||
## 摘要
|
||
|
||
- **結論**:**有條件通過**(需小改)
|
||
- **Major**:**2**(A-4 Settings 儲存 spec + A-5 Linux 預設值)
|
||
- **Minor**:**12**
|
||
- **是否阻擋進開發**:**不阻擋**,Major 都是 < 10 行 spec 改動,可在 M8-4/M8-5 前補
|
||
|
||
---
|
||
|
||
## A. Design ↔ TDD 實作一致性
|
||
|
||
| Design v2 | TDD v2 | 一致性 | 備註 |
|
||
|---|---|---|---|
|
||
| `control-panel.md`(5 態 + 元件 + i18n) | `control-panel.md` + `server-lifecycle.md §5` | ⚠️ 部分 | A-1~A-5 |
|
||
| `server-offline-overlay.md`(`role=alertdialog` + focus trap + 3s active polling) | `web-ui-offline-overlay.md` | ⚠️ 間隔差 | A-6 |
|
||
| `source-selector-update.md`(砍 URL + `.mpeg/.mpg` + i18n) | `deletions.md §1 §5` | ✅ 大致 | A-7 |
|
||
| `first-run-update.md`(2 步 + 硬體可略過) | 未描述(屬 Next.js onboarding) | ✅ 無衝突 | — |
|
||
| `settings-update.md`(auto-open toggle) | `control-panel.md §4.1` + `server-lifecycle.md §2.1` | ⚠️ 命名差 | A-4 |
|
||
|
||
**A-1**(Minor)狀態名大小寫 Design 用首大寫 / TDD 用全小寫 — 語意同,i18n key 對齊即可。不需改。
|
||
|
||
**A-2**(Minor)Design 3 顆 Primary(Stop/Restart 收 Manage dropdown)vs TDD 6 顆扁平。binding 層一致,Design 是 UI 組合方式。**M8-5 實作時 Manage dropdown item 呼叫 TDD 定義的 `StopServer()`/`RestartServer()` bindings**。我會在 M8-5 前補到 TDD `control-panel.md §3` 註記。
|
||
|
||
**A-3**(Minor)Log 行數上限:Design 1000 / TDD 2000。**決案採 2000**(Go ring buffer 容量常數,~400KB 記憶體可忽略)。**Design Spec §4.4 需改 1000 → 2000**(含 Footer 的 `Lines: {current} / 1000`)。
|
||
|
||
**A-4(Major)**Settings 儲存 spec 不一致
|
||
- Design Spec `settings-update.md §2.2`:`settings.json` + 「走 Wails 既有 settings store」
|
||
- TDD v2 `control-panel.md §4.1`:`preferences.go` + `<dataDir>/preferences.json`
|
||
- **問題**:Wails v2 **沒有**內建 settings store(Design 誤解);現況無任何 settings 存檔機制,需新建
|
||
- **決案**:採用 TDD 的 `preferences.json` + `<dataDir>/` 路徑 + 新建 `visiona-local/preferences.go`
|
||
- **Design 需改**:§2.2 檔名改 `preferences.json`,刪掉「走 Wails 既有 settings store」一句
|
||
|
||
**A-5(Major)**R5-D2 Linux 預設 OFF 兩份 spec 都未落地
|
||
- Design 寫「預設值:ON」三平台一致
|
||
- TDD `Preferences{OpenBrowserOnStart bool}` 預設填 `true`
|
||
- **決案**:新增 `DefaultPreferences()` 依 `runtime.GOOS` 回傳:
|
||
```go
|
||
func DefaultPreferences() Preferences {
|
||
return Preferences{OpenBrowserOnStart: runtime.GOOS != "linux"}
|
||
}
|
||
```
|
||
- **Design 需改**:§2.2「預設值」那格加註「macOS/Windows=ON,Linux=OFF(xdg-open 極簡 WM 可能失敗)」
|
||
- **TDD 需改**:`control-panel.md §4.1` 補 `DefaultPreferences()`(我負責 M8-4 前補)
|
||
|
||
**A-6**(Minor)Offline Overlay 間隔不同
|
||
- Design:10s 正常 / 失敗 2 次 / overlay 期間 3s
|
||
- TDD:5s 正常 / 失敗 3 次(無 active 間隔切換)
|
||
- **決案採 Design**(10s 省 CPU,3s active 更貼合 R5-D3 重啟救援體驗)
|
||
- **TDD 需改**:`web-ui-offline-overlay.md §3.1` `POLL_INTERVAL_MS=10000` / `FAILURE_THRESHOLD=2` + overlay active 切換至 `3000`(我負責 M8-7 前補)
|
||
|
||
**A-7**(Minor)i18n 刪除清單微差
|
||
- Design 砍 `camera.uploadFile`(TDD 漏列)
|
||
- TDD 砍 `cannotOpenVideoUrl`(Design 漏列)
|
||
- **M8-1 執行者取聯集即可**
|
||
|
||
---
|
||
|
||
## B. R5-D 補充決策技術落地
|
||
|
||
**B-1 R5-D1 Server 崩潰保留 OS 原生通知**
|
||
- 現況 `visiona-local/app.go:240-272` `showNativeError()` 已有三平台實作(osascript display dialog / PowerShell MessageBox / zenity / kdialog),但這是**給 `reportFatal()` 用的 modal dialog**
|
||
- TDD v2 `control-panel.md §4.7` 把 watchServer 3 次失敗從 `reportFatal` 改為 `setState(Error)`,**同時等於砍掉 OS 通知 → 違反 R5-D1**
|
||
- **決案**:Error state 時並存兩件事:(1) 控制台 banner(Design §6),(2) 新的 **non-blocking toast notification**(不是 modal)
|
||
- macOS:`osascript -e 'display notification "..." with title "..."'`(toast,非 dialog)
|
||
- Windows:優先 `wailsRuntime.SendNotification`(見 `frontend/wailsjs/runtime/runtime.d.ts:275-310` 已自動生成),fallback `msg *` 命令列
|
||
- Linux:`notify-send`,fallback `zenity --notification`
|
||
- 新增 `visiona-local/notify.go` 放 `sendCrashNotification(title, body)`
|
||
- **保留** `showNativeError()` / `reportFatal()` 僅給 startup 致命錯誤用(data dir 建不起來等)
|
||
- **TDD 需改**:`control-panel.md §4.7` 補 `sendCrashNotification` 呼叫(我負責 M8-4 前補)
|
||
- **Design 建議加**:§6.2/§6.3 加一句「Error state 時另發 non-blocking OS notification(R5-D1)」
|
||
|
||
**B-2 R5-D2 Linux 預設 OFF** — 見 A-5。
|
||
|
||
**B-3 R5-D3 每次啟動都自動開**
|
||
- TDD `control-panel.md §4.6`:`if prefs.OpenBrowserOnStart && !autoOpenedThisSession`
|
||
- `autoOpenedThisSession` 是 per-App-process flag,每次 Wails 新啟動會重建 → 每次啟動都會開 ✅
|
||
- Restart Server(同一 App process 內)不會重開瀏覽器 ✅
|
||
- **技術落地正確**
|
||
- **Design 建議改**:`control-panel.md §7.1` 第 5 步「[首次 / Settings 為 ON]」→「[每次 / Settings 為 ON]」
|
||
|
||
---
|
||
|
||
## C. 體驗 ↔ 技術紅線
|
||
|
||
**C-1 OnBeforeClose**:Design Footer 持久提示取代 modal 確認 ↔ TDD `server-lifecycle.md §7` `OnBeforeClose return false` → ✅ 一致。但 Linux AppImage(i3/xmonad)flaky 的 edge case TDD §10 已記錄,M8-10 驗收時實測。
|
||
|
||
**C-2 Offline Overlay focus trap**:Design 要求 Tab 只能在「重試 / 了解更多」間循環,TDD `web-ui-offline-overlay.md §4.4` **沒 focus trap 實作**。**決案**:M8-7 Frontend Agent 手動實作(`useEffect` 裡 onKeyDown 擋 Tab + 強制 focus,~15 行,不引入 `react-focus-lock`)。**SSR 相容性**:`'use client'` + Zustand + effect 只在 client 跑,✅ 無問題。
|
||
|
||
**C-3 3s active polling 對 log pump 的壓力**:0.33 行/秒 vs R-v2-3 的推論 30-100 行/秒差兩個量級,✅ 無壓力。**額外建議**:server Gin logger 加入 `SkipPaths: []string{"/api/system/boot-id"}` 避免 boot-id probe 洗版控制台 log panel。我負責 M8-9 前補到 TDD。
|
||
|
||
**C-4 Wails 控制台 i18n**:
|
||
- Design Spec §9.1 列 30+ key,namespace `control.*`
|
||
- TDD §6.1 範例只 ~15 key,namespace `statusCard.*`/`actions.*`
|
||
- **決案**:以 Design 為準(`control.*` 更語意化,對齊 Web UI 風格)。M8-5 執行者依 Design Spec §9.1 填 `visiona-local/frontend/i18n/{zh-TW,en-US}.json`
|
||
|
||
**C-5 Dark Mode**:Design 要求跟隨系統 + 與 Web UI 共用 token,TDD §2 用 CSS vars + `prefers-color-scheme` → ✅ 方向一致。**建議做法**:M8-5 執行者**直接從 `frontend/src/app/globals.css` 複製 `:root` + `[data-theme='dark']` token block 到 `visiona-local/frontend/style.css`**,不重造一份。
|
||
|
||
---
|
||
|
||
## D. Architect 7 懸念決策
|
||
|
||
**Q1 NewVideoSourceFromURL 呼叫者**(grep 驗證):
|
||
- `camera_handler.go:435` 在 `StartFromURL` 內(即將砍)
|
||
- `camera_handler.go:731` 在 seek handler 的 `if h.videoIsURL` 分支;砍 `StartFromURL` 後 `h.videoIsURL` 永遠 false → dead branch
|
||
- **決案**:`NewVideoSourceFromURL` / `NewVideoSourceFromURLWithSeek` / `h.videoIsURL` field 一起砍。M8-1 執行時套用。我負責補到 TDD `deletions.md §1.2` 末段。
|
||
|
||
**Q3 uuid vs crypto/rand**:**採 `crypto/rand`**(~10 行 hex encode,不引入 `google/uuid` 依賴)。TDD `server-lifecycle.md §9.1/§10 Q3` 以此為準。
|
||
|
||
**Q4 shutdownGracePeriod 5s vs 10s**:**blocked-on-pm-review**(我的意見:建議 10s 對齊 server `shutdownFn` timeout,使用者等 10s 是罕見 edge case)
|
||
|
||
**Q5 navigator.language fallback**:
|
||
```javascript
|
||
const raw = (navigator.language || navigator.userLanguage || 'en').toLowerCase();
|
||
if (raw.startsWith('zh')) return 'zh-TW';
|
||
if (raw.startsWith('en')) return 'en-US';
|
||
return 'zh-TW'; // 預設繁中(visionA 團隊內部工具)
|
||
```
|
||
TDD §6.2/§8 Q4 以此為準。
|
||
|
||
**Q6 window.close() 限制**:**blocked-on-design-review**(我的意見:Chrome/Safari/Edge 禁止非 `window.open()` 開啟的 tab 呼叫 `window.close()`。建議 Design 把「關閉這個頁面」按鈕改為「我知道了」純視覺收斂,或改為提示「請手動關閉此分頁」)
|
||
|
||
**Q7 Preferences JSON atomic write**:**write-rename pattern**
|
||
```go
|
||
os.WriteFile(path+".tmp", data, 0o644); os.Rename(path+".tmp", path)
|
||
```
|
||
POSIX/Windows 都原子。TDD §8 Q3 以此為準。
|
||
|
||
---
|
||
|
||
## E. PM 3 技術懸念回答
|
||
|
||
**§11-1 Settings 資料落地位置**:
|
||
- 檔名 `preferences.json`,路徑 `<dataDir>/preferences.json`
|
||
- macOS `~/Library/Application Support/visiona-local/preferences.json`
|
||
- Windows `%APPDATA%\visiona-local\preferences.json`
|
||
- Linux `~/.local/share/visiona-local/preferences.json`
|
||
- 以 `migrateOldDataDirs()/ensureDataDir()` 算出的路徑(`visiona-local/app.go:103-155`)
|
||
- JSON 格式:`{"openBrowserOnStart": true}`
|
||
- 原子寫入:write-rename(D-Q7)
|
||
- 讀取失敗 fallback:`DefaultPreferences()`(平台差異,見 A-5)
|
||
|
||
**§11-2 US-1 AC-1.3 10 秒預算**:
|
||
| 步驟 | 估時 |
|
||
|---|---|
|
||
| Wails 載入 + OnStartup | 0.3-0.5s |
|
||
| migrate/lock/ipc/seed | 0.1-0.2s |
|
||
| ensurePythonRuntime | 0.2-0.5s |
|
||
| pickPort + spawn server | 0.05-0.1s |
|
||
| Server cold start | 1.5-2.5s |
|
||
| waitHealthy 首次成功 | 0.5-1.5s |
|
||
| OpenInBrowser + OS open | 0.3-0.8s |
|
||
| Next.js SPA 載入 | 0.8-1.5s |
|
||
| boot-id + WS ready | 0.3-0.5s |
|
||
| **樂觀** | **~4.1s** ✅ |
|
||
| **悲觀**(Intel Mac + system Python) | **~8.1s** ✅ |
|
||
| **Linux AppImage 首次解壓** | **+1-2s**,仍達標 |
|
||
| **Windows + Defender 掃描** | **+2-3s**,最壞 ~11s**可能超時** |
|
||
- **風險**:Windows 首次啟動可能超過 10s。**建議 M8-10 驗收實測,超時則 PRD AC-1.3 放寬到 12s**
|
||
|
||
**§11-3 idle RAM ≤ 450 MB**:
|
||
| 程序 | v1 | v2 |
|
||
|---|---|---|
|
||
| Wails shell | 150-200 MB | **120-160 MB**(無 Next.js bundle) |
|
||
| Go server | 80-120 MB | **70-100 MB**(砍 Mock + yt-dlp) |
|
||
| Python sidecar | 150-250 MB | **0 MB 若 on-demand;150-250 MB 若 always spawn** |
|
||
| Browser tab (Next.js Web UI) | — | **180-240 MB**(新項目) |
|
||
| **樂觀** | — | **~370 MB** ✅ |
|
||
| **悲觀** | — | **~500 MB** ❌ 超 50 MB |
|
||
|
||
- **建議**:PRD v2 §NFR 明示「**450 MB 不含 browser tab**,只含 Wails + Go server + Python sidecar;browser tab 視為使用者瀏覽器自己的資源」—— 這個 clarify 符合常識且達標
|
||
- 或:放寬到 **≤ 550 MB**
|
||
- **另需確認**:v1 `visiona-local/app.go:441` 的 `ensureDriverInstalled` 是否在無硬體時仍 spawn python。若是,v2 建議改為 on-demand 節省 idle RAM
|
||
|
||
---
|
||
|
||
## F. 技術衝突
|
||
|
||
**F-1 CORS ↔ 瀏覽器 tab origin**:
|
||
- Wails「Open in Browser」→ `http://127.0.0.1:<port>` → 白名單內 ✅
|
||
- 使用者手改 `localhost` → 白名單也接受 ✅
|
||
- Same-origin fetch(不送 Origin)→ `CORSMiddleware()` 第一個 if 放行 ✅
|
||
- **無衝突**
|
||
|
||
**F-2 Restart Server port 保留**(Major 隱含議題):
|
||
- TDD `v2/server-lifecycle.md §3`:Restart 後重新 `pickPort(3721)`,可能 fallback 到 3722
|
||
- Boot-ID 機制假設 tab URL 的 port 不變 → Restart 後 port 變 → 舊 tab `window.location.reload()` 仍連舊 port → overlay 永久卡死
|
||
- **決案**:`ServerController.Restart()` **強制保留舊 port**,若 old port 不可用 → 進 Error state 並發 OS notification「port 被占用」,**不 fallback**。cold start 則仍允許 3721→3722 fallback
|
||
- **TDD 需改**:`server-lifecycle.md §3` 新增 Restart 同 port 規則(我負責 M8-4 前補)
|
||
- Design Spec `control-panel.md §7.3` 的 fallback 情境只適用 cold start,不影響 Design,但 Frontend Agent 需知
|
||
|
||
**F-3 Boot-ID 機制在「關 Wails = 結束 server」場景**:
|
||
- server 沒了 → boot-id 不會變 → 由 overlay 接手(連續失敗 → 顯示)
|
||
- 使用者重開 app 若走 cold start port fallback 到新 port → 舊 tab 的 URL(3721)連不上新 port(3722)→ overlay 永卡;使用者需從新 Wails 控制台 Open in Browser 開新 tab
|
||
- **設計正確**,TDD §2.3 時序已暗示,不需改
|
||
|
||
---
|
||
|
||
## G. 臆測 / 超範圍
|
||
|
||
**G-1 Export log 按鈕**(Design §4.5):TDD 沒對應 binding。技術可行(`wailsRuntime.SaveFileDialog`),**M8-5 補 `ExportLog(path) error` binding**。我負責補到 TDD §4.2。
|
||
|
||
**G-2 Copy 按鈕**(Design §4.5):直接用 `navigator.clipboard.writeText()` 瀏覽器 API,Wails WebView 支援。**不需新 binding**。
|
||
|
||
**G-3 Report 按鈕**(Design §6.2 Error banner):需要 GitHub Issue URL,**目前沒公開 repo**。**降級**:M8-5 若 PM 沒提供 URL → 不實作 Report 按鈕,Error banner 只留 Restart + View details。**Design 需加**:§6.2 hold 註記「Report 按鈕待 PM 提供 repo URL,暫不實作」。
|
||
|
||
**G-4 Log rotate 7 天/10MB**(Design §4.4):TDD 沒 rotate 實作,v1 也無。**降級為 append 模式,不做 rotate**(rotate 需要 `lumberjack.v2` 或自刻定時掃描 + size 比較,非 M8 scope)。**Design 需改**:§4.4 「rotate 7 天/10MB」改為「append 模式;使用者可 Clear Logs 清畫面、Open Log Folder 手動刪檔」。未來 M9+ 再加。
|
||
|
||
---
|
||
|
||
## H. 問題清單
|
||
|
||
**Major(阻擋,需修正)**:
|
||
|
||
| # | 位置 | 問題 | 修正 |
|
||
|---|------|------|------|
|
||
| M-1 | Design `settings-update.md §2.2` | 檔名 `settings.json` + 「走 Wails settings store」誤解 | 改 `preferences.json`,刪掉 Wails settings store 那句 |
|
||
| M-2 | Design `settings-update.md §2.2` + TDD `control-panel.md §4.1` | R5-D2 Linux 預設 OFF 未落地 | Design 加「macOS/Windows=ON,Linux=OFF」;TDD 加 `DefaultPreferences()` 依 GOOS |
|
||
|
||
**Minor(不阻擋)**:
|
||
|
||
| # | 位置 | 處理者 |
|
||
|---|------|------|
|
||
| m-1 | Design `control-panel.md §4.4` + Footer | 1000 → 2000 |
|
||
| m-2 | TDD `web-ui-offline-overlay.md §3.1` | 10s/2 次/3s active(我 M8-7 前補) |
|
||
| m-3 | M8-1 執行者 | i18n 刪除清單取聯集 |
|
||
| m-4 | TDD `control-panel.md §4.7` + Design `control-panel.md §6` | Error state 補 OS notification(我 M8-4 前補 TDD) |
|
||
| m-5 | Design `control-panel.md §7.1 第 5 步` | 「首次」→「每次」對齊 R5-D3 |
|
||
| m-6 | M8-7 Frontend Agent | Offline Overlay focus trap 實作 |
|
||
| m-7 | TDD `server-lifecycle.md §9` | Gin SkipPaths + crypto/rand(我 M8-9 前補) |
|
||
| m-8 | M8-5 執行者 | i18n 以 Design §9.1 為準 |
|
||
| m-9 | TDD `server-lifecycle.md §3` | Restart 強制同 port(我 M8-4 前補) |
|
||
| m-10 | TDD `control-panel.md §4.2` | 補 `ExportLog` binding(我 M8-5 前補) |
|
||
| m-11 | Design `control-panel.md §6.2` | Report 按鈕 hold 註記 |
|
||
| m-12 | Design `control-panel.md §4.4` | Log rotate 降級為無 rotate |
|
||
|
||
---
|
||
|
||
## I. 結論
|
||
|
||
**✅ 有條件通過** — Design Spec v2 方向正確,與 TDD v2 無不可修復衝突。
|
||
|
||
**Design Agent 必改**(Major + 關鍵 Minor):
|
||
- `settings-update.md §2.2`(M-1 + M-2)
|
||
- `control-panel.md §4.4`(m-1 + m-12)
|
||
- `control-panel.md §6.2`(m-4 + m-11)
|
||
- `control-panel.md §7.1`(m-5)
|
||
|
||
**TDD 必改(我負責,M8-* 執行前補)**:
|
||
- `control-panel.md §4.1`(`DefaultPreferences()` 平台差異)
|
||
- `control-panel.md §4.2`(`ExportLog` binding)
|
||
- `control-panel.md §4.7`(`sendCrashNotification` + 新增 `notify.go`)
|
||
- `control-panel.md §3`(Manage dropdown 註記)
|
||
- `server-lifecycle.md §3`(Restart 同 port)
|
||
- `server-lifecycle.md §9`(Gin SkipPaths + `crypto/rand`)
|
||
- `web-ui-offline-overlay.md §3.1`(10s/2 次/3s active)
|
||
- `deletions.md §1.2`(`videoIsURL` field 直接砍)
|
||
|
||
**Q 決案狀態**:Q1 ✅ Q3 ✅ Q4 ⏳ PM Q5 ✅ Q6 ⏳ Design Q7 ✅
|
||
|
||
**PM 3 技術懸念**:
|
||
- §11-1 ✅ `preferences.json` @ `<dataDir>/`,write-rename
|
||
- §11-2 ✅ 10 秒樂觀 ~4s / 悲觀 ~8s / Windows 邊界 ~11s(M8-10 驗收時實測,可能需放寬到 12s)
|
||
- §11-3 ⚠️ idle RAM 需 clarify「不含 browser tab」或放寬到 550 MB;另需確認 Python sidecar 是否 on-demand
|
||
|
||
**不阻擋進開發**:所有修正都是 < 10 行 spec 改動,可在 M8-4 / M8-5 執行前 1-2 小時內完成。
|
||
|
||
---
|
||
|
||
**審閱日期**:2026-04-14
|
||
**下一步**:交 Orchestrator 彙整三方 + R5-D 對齊 + Q4/Q6 收斂 → 使用者最終確認 → M8-1
|
||
|
||
---
|
||
|
||
# Architect 第二輪審閱 Design Spec v2.1(2026-04-14)
|
||
|
||
> 對象:`design-spec-v2.md` v2.1 索引 + `v2/settings-update.md` v2.1 + `v2/control-panel.md` v2.1 + **新檔** `v2/startup-progress.md` v2.1
|
||
> 基準:TDD v2.1 `v2/startup-pipeline.md`(518 行)+ 第一輪審閱結論
|
||
|
||
## 摘要
|
||
|
||
- **總結論**:**通過,需小改**(3 個 Minor,都在 TDD 側補齊)
|
||
- **第一輪 Major × 2 / Minor × 12 修復**:**Major 2/2 修好**;**Minor 12/12 修好或已落到 TDD/執行者待辦**
|
||
- **R5-E startup-progress.md 技術可行性**:**可行**,event schema 7 成對齊,有 3 處 TDD 側需補小改
|
||
- **是否阻擋進 M8 開發**:**不阻擋**,TDD 小改可在 M8-4b 執行前 30 分鐘內補完
|
||
|
||
---
|
||
|
||
## A. 第一輪 Major 修復檢查
|
||
|
||
| # | 第一輪意見 | v2.1 現況 | 結果 |
|
||
|---|-----------|---------|-----|
|
||
| **M-1** Settings 落地檔案 | 改 `preferences.json` + 刪 Wails settings store | `settings-update.md §2.2`(L48-56)已明文「`preferences.json` @ `<dataDir>/`」+「Go server 端負責讀寫(非 Wails 內建機制 — Wails v2 沒有 settings store)」+「write-rename pattern」;全檔 grep 無 `settings.json` 殘留,無「Wails settings store」殘留 | ✅ **修好** |
|
||
| **M-2** R5-D2 Linux 預設 OFF | 加「macOS/Windows=ON,Linux=OFF」依 GOOS | `settings-update.md §2.2`(L51)「macOS/Windows = ON;Linux = OFF」+ L55「`DefaultPreferences()` 依 `runtime.GOOS`」+ §2.2 Linux 首次使用說明(L59-63)+ §7 遷移策略(L180-190)全部落地 | ✅ **修好** |
|
||
|
||
---
|
||
|
||
## B. 第一輪 Minor 修復檢查
|
||
|
||
| # | 第一輪意見 | v2.1 現況 | 結果 |
|
||
|---|-----------|---------|-----|
|
||
| m-1 | Log 上限 1000→2000 | `control-panel.md §4.4`(L152)「最大行數 2000」、§4.6 Footer(L186)「`Lines: {current} / 2000`」| ✅ |
|
||
| m-2 | Offline Overlay 10s/2 次/3s active → TDD 側 | 非 Design 修,我負責 M8-7 前補 TDD | ⏳ 我的 TODO(未阻擋) |
|
||
| m-3 | i18n 刪除清單聯集 → 執行者 | M8-1 執行者責任 | ⏳ 執行者 TODO |
|
||
| m-4 | Error state OS notification | `control-panel.md §6.3`(L269-275)「R5-D1 OS 原生通知並存」完整三平台實作表;Design Diff §12-6(L459)| ✅ |
|
||
| m-5 | §7.1 「首次」→「每次」 | `control-panel.md §7.1`(L310)「5. 【每次 / Settings 為 ON(macOS/Windows 預設;Linux 預設 OFF)】」;L331「每次啟動(每次 Wails App process 新啟動)都會跑完整 6 階段流程」| ✅ |
|
||
| m-6 | Offline Overlay focus trap | M8-7 Frontend 責任 | ⏳ 執行者 TODO |
|
||
| m-7 | Gin SkipPaths + crypto/rand → TDD 側 | 我負責 M8-9 前補 | ⏳ 我的 TODO |
|
||
| m-8 | i18n 以 Design §9.1 為準 | Design `control-panel.md §9`(L387-408)完整 `control.*` 清單 | ✅ |
|
||
| m-9 | Restart 強制同 port → TDD 側 | 我負責 M8-4 前補 | ⏳ 我的 TODO |
|
||
| m-10 | `ExportLog` binding → TDD 側 | 我負責 M8-5 前補 | ⏳ 我的 TODO |
|
||
| m-11 | Report 按鈕 hold | `control-panel.md §6.2`(L265)「**【hold】** 現階段**先不實作**」;`startup-progress.md §3.7`(L221)「Report Issue **【hold】**」| ✅ |
|
||
| m-12 | Log rotate 降級 | `control-panel.md §4.4`(L152-162)「無落地寫檔(in-memory ring buffer)」+「為什麼取消落地寫檔與 rotate」整段說明 | ✅ |
|
||
|
||
**修復統計**:Design 側 8/8 已修(m-1、m-4、m-5、m-8、m-11、m-12 + M-1、M-2);TDD 側 4 個由我自己負責(m-2、m-7、m-9、m-10);執行者側 2 個(m-3、m-6)。**無 Design 側遺漏**。
|
||
|
||
---
|
||
|
||
## C. R5-E `startup-progress.md` 技術可行性(重中之重)
|
||
|
||
### C-1 Wireframe DOM/CSS 難度
|
||
|
||
`startup-progress.md §2`(L25-101)三個 wireframe 全部 text-based,StageItem 結構(§3.3 L141-154)明確示範 vanilla HTML + CSS class data-state,可直接對齊 TDD `startup-pipeline.md §6`(L407-491)的 vanilla JS 實作。**無 framework-specific 要求**。✅
|
||
|
||
### C-2 6 階段定義對應 TDD §3 階段表
|
||
|
||
| # | Design label | TDD `§3` Go 實作點 | 對應 |
|
||
|---|-----|-----|-----|
|
||
| 1 | 初始化控制台 | `app.go:startup` 首行 → `seedUserDataDir()` 返回 | ✅ |
|
||
| 2 | 檢查 Python 執行環境 | `startServerV2` → `ensurePythonRuntime()` 返回 | ✅ |
|
||
| 3 | 啟動本機伺服器 | `waitHealthy(port, 30s)` 返回 | ✅ |
|
||
| 4 | 偵測 Kneron 裝置 | `GET /api/devices` 第一次 response | ✅ |
|
||
| 5 | 開啟瀏覽器 | `OpenInBrowser("")` 命令 return | ✅ |
|
||
| 6 | 等待 Web UI 連線 | WebSocket hub `OnClientConnected` 首次 | ✅ |
|
||
|
||
**完全對齊**。Design §8 的訊號來源(L361-367)和 TDD §3 表格用詞一致。✅
|
||
|
||
### C-3 Event schema 一致性(發現 3 個小衝突)
|
||
|
||
Design `startup-progress.md §9`(L386-393)預設的 event 名:
|
||
|
||
```
|
||
startup:stage {id, state, errorMessage?}
|
||
startup:complete / startup:error
|
||
```
|
||
|
||
TDD `startup-pipeline.md §1`(L28-84)定義的 event 名:
|
||
|
||
```
|
||
startup:progress (StartupProgressEvent: stage, totalStages, labelKey, status, startedAt)
|
||
startup:stage-timeout (StartupStageTimeoutEvent: stage, softTimeoutSeconds)
|
||
startup:error (StartupErrorEvent: stage, error, cause)
|
||
startup:ready (無 payload)
|
||
```
|
||
|
||
**衝突 1**:事件名 `startup:stage` vs `startup:progress` 不一致。**決案:採 TDD 的 `startup:progress`**(更語義化;`stage` 單字太模糊)。Design Agent 不必改 startup-progress.md §9(備忘性質),由 M8-4b / M8-5 執行者以 TDD 為準。
|
||
|
||
**衝突 2**:Design `StageState` 列舉(§8 L335-341)有 `pending | running | running-slow | done | failed | skipped`,TDD `StartupProgressEvent.Status`(§1.1 L41)只定義 `pending | running | completed | failed`。
|
||
|
||
差異分析:
|
||
- `running-slow` — Design 明確標註「UI 派生狀態」(L338 註解),由前端根據 `now - startedAt > 20s` 計算,Go 端不需 emit → **無衝突**
|
||
- `done` vs `completed` — 語義同,用詞差。**決案:Go 端 emit `completed`**,前端 Design class name 用 `done` 即可(CSS 層面對應)
|
||
- **`skipped` Go 端完全沒有** — 這是 **TDD 側 Minor 問題**,要補
|
||
|
||
**衝突 3**:`soft timeout` 事件名不同。Design §9 預設「前端自己 timer 檢查 20 秒 → 派生 `running-slow`」,TDD §1.2 定義「Go 端 watcher goroutine emit `startup:stage-timeout`」。兩種實作都可行,**決案以 TDD 為準**(後端 source of truth,避免兩邊各自計時導致時間漂移),Design 前端僅負責接 `startup:stage-timeout` event 更新 UI。
|
||
|
||
**小結**:事件協議 Design 預設是草稿,TDD 是 ground truth;M8-4b/M8-5 執行者以 TDD 為準即可,**Design Agent 無需修 startup-progress.md §9**。但 **TDD 需補 `skipped` status**(見 E 節)。
|
||
|
||
### C-4 Timeout UI 觸發時機對齊
|
||
|
||
- **20 秒 soft timeout**:Design §3.6(L189-205)「`stage.state === 'running' && (now - stage.startedAt) > 20_000ms`」;TDD §4 watcher goroutine(L332-346)`sinceStage > startupSoftTimeout` + `softTimeoutEmitted` flag 確保只 emit 一次。✅ **觸發時機一致**(Go emit event → 前端收到顯示 hint)
|
||
- **60 秒 hard timeout**:Design §3.7(L207-224)「任一階段 failed 或總計 > 60 秒 → Error mode」;TDD §4 watcher(L325-330)`sinceTotal > startupHardTimeout` → `Fail(cur, ...)` + `emitError(cur, ..., "total-timeout")`。✅ **一致**
|
||
|
||
### C-5 無障礙可行性
|
||
|
||
- `role="progressbar"` + `aria-valuemin="0"` + `aria-valuemax="6"` + `aria-valuenow` — vanilla HTML `<section>` 直接標即可 ✅
|
||
- `aria-live="polite"` 搭配 `.sr-only` div — Design §6(L281)已設計,vanilla JS `textContent` 更新即可觸發 SR 讀出 ✅
|
||
- `⌘0` / `Ctrl+0` focus + `Esc` — Design §6(L283):vanilla JS `addEventListener('keydown')` 攔截即可 ✅
|
||
- `prefers-reduced-motion` — CSS media query,vanilla CSS 支援 ✅
|
||
|
||
**可行性**:**100%**。不需引入任何 third-party library。
|
||
|
||
### C-6 Linux / Toggle OFF 分支(**重要**)
|
||
|
||
Design 規格(§4.1 L244-256):
|
||
- 階段 5:`pending → skipped`(不經過 `running`),顯示 ⏭ icon
|
||
- 階段 6:`pending → running`,description 改 manualHint,**不套 20 秒 retry hint**
|
||
- 當使用者手動點 Open in Browser 並建立 WebSocket → `done`
|
||
|
||
TDD §3 表(L117)階段 5:「若 `AutoOpenBrowser=false` 則立即 complete 跳過」— **TDD 是 `completed`,Design 是 `skipped`,語義不同**。Design 要顯示「跳過(依偏好設定)」⏭ icon,不是綠勾 ✅。
|
||
|
||
TDD 也**沒有處理階段 6 不套 soft timeout 的情境**:§4 watcher 對所有階段一視同仁,Toggle OFF 時階段 6 會在 20 秒後觸發 `startup:stage-timeout`,前端會顯示「正在重試...」—— 這是錯的,因為在等人為動作。
|
||
|
||
**兩個問題都是 TDD 側需補**(見 E 節 E-2 / E-3)。
|
||
|
||
### C-7 i18n key 30+ 條
|
||
|
||
Design §7(L291-329)列出 27 個 i18n key,namespace 一致用 `startup.*`。TDD §2(L87-103)只列 11 個 key 作為骨架,註明「文案由 Design Spec v2.1 敲定」。
|
||
|
||
**key 名稱微差**:
|
||
- Design:`startup.panel.title` / TDD:`startup.title` — 統一採 **Design 的 `startup.panel.title`**(更完整的 namespace 樹狀結構)
|
||
- Design:`startup.timeout.message` / TDD:`startup.retrying` — 統一採 **TDD 的 `startup.retrying`**(對應 TDD event `startup:stage-timeout` 的 copy,更精準)
|
||
- Design:`startup.error.description.timeout` / TDD:`startup.error.totalTimeout` — 統一採 **Design 的樹狀版**
|
||
|
||
這些微差不影響可行性,M8-4b/M8-5 執行者統一用 Design §7 清單 + TDD 補上 labelKey path 即可。載入時機:Wails 控制台前端 `app.js` 在 `main()` 最前呼叫 `i18n/loader.js` 載入 JSON,早於 `initStartupPanel()`,順序正確。✅
|
||
|
||
**可行性**:**可行**。
|
||
|
||
### 7 子項總結:**全部可實作**。發現的 3 個技術衝突都在 TDD 側(不在 Design 側)— skipped status、階段 6 不套 soft timeout、`RestartStartupSequence` binding — 由我在 M8-4b 執行前補 TDD 修復。
|
||
|
||
---
|
||
|
||
## D. Design v2.1 新增 3 個懸念的技術回答
|
||
|
||
### D-Q1「正在重試」vs「正在處理中」文案
|
||
|
||
**技術面**:**不影響實作**。Go 端只 emit `startup:stage-timeout` event,文案在前端 i18n JSON,改字面 0 行程式碼成本。
|
||
|
||
**Architect 決案**:交由 Design 最終定稿即可,我不干涉文案選擇。若要技術建議:採 Design 原提案「正在重試...」—— event 名本身是 `stage-timeout`,副文字用「重試」與 icon ⚠ 搭配更有「系統知道有狀況且在努力」的語感,對齊 Nielsen Norman perceived-performance 原則。**不阻擋**。
|
||
|
||
### D-Q2 WebSocket 被安全軟體攔截的偵測
|
||
|
||
**技術面評估**:**不可行**。
|
||
- Windows Defender SmartScreen 攔截瀏覽器連線時,Go server 端 `OnClientConnected` 根本不會被呼叫,server 無法區分「使用者還沒打開瀏覽器」、「瀏覽器正在載入 Next.js」、「瀏覽器被擋」這三種情境
|
||
- 唯一能辨識的訊號是「階段 5 已 complete 但階段 6 > 30 秒未 complete」,但這個時間閾值和正常冷啟動(悲觀 ~8 秒 Next.js + WS)無法可靠區分
|
||
- 若硬要辨識,需要辨識多種安全軟體的特徵(Defender SmartScreen / McAfee / Kaspersky / 企業 MDM),過度設計
|
||
- **接受 Design 建議**:不做特殊偵測,Error 說明用通用文案「階段 6 超時,請檢查瀏覽器是否能開啟 http://127.0.0.1:{port}」+ 引導看 log details 即可
|
||
|
||
**Architect 決案**:**同意 Design 建議,不做特殊偵測**。M8-5 前端 i18n 的 `startup.error.description.timeout` 文案建議補上「請確認瀏覽器可存取 127.0.0.1:{port}」作為通用提示。
|
||
|
||
### D-Q3 Retry 按鈕語意:重置整個啟動 vs 重試當前階段(**關鍵技術決策**)
|
||
|
||
**技術面評估**:**「重置整個啟動流程」可行且建議採用**。
|
||
|
||
**實作細節**(`RestartStartupSequence()` 新 function,位於 `startup_pipeline.go`):
|
||
|
||
```go
|
||
// RestartStartupSequence 停掉當前 pipeline,清理 server 狀態,重新跑 pipeline 從階段 1 開始。
|
||
// 只能在 Error state 時呼叫(Starting/Running 時 noop)。
|
||
func (a *App) RestartStartupSequence() error {
|
||
if a.ctrl.State() != ServerStateError {
|
||
return errors.New("RestartStartupSequence only allowed in Error state")
|
||
}
|
||
|
||
// 1. 停掉舊 watcher(若還活著)
|
||
if a.pipeline != nil {
|
||
a.pipeline.stopWatcher()
|
||
}
|
||
|
||
// 2. 強制 kill 舊 server process(若還存在)
|
||
// ctrl.Stop() 會走 graceful → 這裡用 ForceKill 因為 server 很可能已處於壞狀態
|
||
a.ctrl.ForceKill()
|
||
|
||
// 3. 重置 ServerController state machine 回 Stopped
|
||
a.ctrl.setState(ServerStateStopped, "")
|
||
|
||
// 4. 重建 StartupPipeline(清掉所有 stage state)
|
||
a.pipeline = NewStartupPipeline(a)
|
||
a.pipeline.Start(a.ctx)
|
||
|
||
// 5. 重跑 ctrl.Start() — 會依序觸發階段 2-6
|
||
return a.ctrl.Start()
|
||
}
|
||
```
|
||
|
||
**關鍵考量**:
|
||
|
||
1. **階段 1 不需重跑**:Wails `OnStartup` 已經跑過,`seedUserDataDir` / `migrateOldDataDirs` 等只需跑一次。**建議:Retry 時 `pipeline.Start(ctx)` 後直接 `Complete(1)`**(0.0 秒完成,視覺上瞬過)
|
||
2. **階段 2 `ensurePythonRuntime()` 重跑成本**:若 Python venv 已在 `<dataDir>/python/` 解壓完成,`ensurePythonRuntime()` 內部 check 應該 < 100ms return;若 Python extract 到一半失敗需重跑,成本較高但仍有邊界(Architect §11-2 估悲觀 0.5s)。**可接受**
|
||
3. **階段 3 server 全砍重起**:必須全砍。舊 server process 若處於半掛狀態(port 卡著、goroutine leak),保留只會讓新 pipeline 再卡一次。`ForceKill` → `pickPort` 重新選 port — 但這和第一輪 F-2(Restart 強制同 port)有衝突,需要微調:**Retry 情境下 port 可以 fallback**(反正 Error state 下舊 Web UI tab 已顯示 overlay,使用者會從新控制台 Open in Browser 開新 tab)
|
||
4. **階段 6 WebSocket sentinel file 清理**:若 TDD §3 採 sentinel file `<dataDir>/.first-ws-connected`,Retry 時必須 **先 `os.Remove()` sentinel file**,否則新階段 6 會瞬間 `completed` 錯誤
|
||
5. **Wails binding**:需新增 `RestartStartupSequence() error` 到 `app.go` bindings,前端 Design §3.7 Retry 按鈕 `onclick` 呼叫 `window.go.main.App.RestartStartupSequence()`
|
||
|
||
**Architect 決案**:**可行,採用「重置整個啟動流程」語意**。我會在 TDD `v2/startup-pipeline.md` 新增 §9「Retry 機制」小節落地上述實作細節(M8-4b 執行前補)。Design Agent **無需**修 startup-progress.md,維持 §3.7 Retry 按鈕「重置進度面板,重新跑階段 1」的 wireframe 文字即可。
|
||
|
||
---
|
||
|
||
## E. 新發現的 Design ↔ TDD 衝突(TDD 側 Minor)
|
||
|
||
### E-1 WebSocket `OnClientConnected` → Wails event 路徑未敲定
|
||
|
||
- Design `startup-progress.md §9-4`(L391)明確要求「Go WebSocket hub `OnConnect` 事件中 emit `startup:stage {id: 6, state: 'done'}`」
|
||
- TDD `startup-pipeline.md §3`(L122-132)提出兩個選項:long-poll endpoint 或 sentinel file,**尚未敲定**
|
||
- **Architect 補決案**:**採 sentinel file 方案**(long-poll endpoint 要改 server 路由、rg ctx 維護成本較高)。server WebSocket hub 的 `OnClientConnected` callback 首次觸發時 `os.WriteFile(<dataDir>/.first-ws-connected, []byte{}, 0o644)`;Wails 階段 5 complete 後 goroutine `for { if exists { pipeline.Complete(6); return }; time.Sleep(200ms) }`,30 秒逾時 `pipeline.Fail(6, ...)`
|
||
- **TDD 需改**:`startup-pipeline.md §3` 表格階段 6 列敲定 sentinel 方案(我負責 M8-4b 前補)
|
||
|
||
### E-2 `skipped` status Go 端未定義
|
||
|
||
- Design §3.4(L165)、§4.1(L244-248)明確需要 `skipped` status(階段 5 Toggle OFF 時)
|
||
- TDD §1.1 `StartupProgressEvent.Status` 只有 `pending | running | completed | failed`,**缺 `skipped`**
|
||
- **Architect 補決案**:TDD `startup-pipeline.md §1.1` 新增 `"skipped"` 到 Status 枚舉;§4 `StartupPipeline` 新增 `Skip(stage int)` method,內部 `p.stages[stage].status = "skipped"` + `emitProgress` + 自動進入下一階段(不 `Fail`)
|
||
- **TDD 需改**:§1.1 補 status、§4 補 `Skip()` method(我負責 M8-4b 前補)
|
||
|
||
### E-3 階段 6 `AutoOpenBrowser=false` 時不觸發 soft timeout
|
||
|
||
- Design §4.1(L255)「不套 20 秒 retry hint(因為是等待人為動作,不是系統卡住)」
|
||
- TDD §4 watcher goroutine 對所有階段一視同仁,**未排除階段 6 Toggle OFF 情境**
|
||
- **Architect 補決案**:TDD §4 watcher 新增條件 `if cur == 6 && !p.app.prefs.OpenBrowserOnStart { continue }`(跳過 soft timeout 檢查);hard timeout 仍照常檢查(使用者 60 秒內不點 Open in Browser 仍應進 Error state,合理)
|
||
- **TDD 需改**:§4 watcher goroutine 條件補上(我負責 M8-4b 前補)
|
||
|
||
---
|
||
|
||
## F. 第二輪新發現問題
|
||
|
||
### Major(阻擋)
|
||
|
||
**無**。
|
||
|
||
### Minor(不阻擋)
|
||
|
||
| # | 位置 | 問題 | 處理 |
|
||
|---|-----|-----|------|
|
||
| 2-m1 | TDD `startup-pipeline.md §1.1` | `StartupProgressEvent.Status` 缺 `"skipped"` | 我 M8-4b 前補 |
|
||
| 2-m2 | TDD `startup-pipeline.md §3` | 階段 6 WebSocket sentinel 方案未敲定 | 我 M8-4b 前補(採 sentinel file) |
|
||
| 2-m3 | TDD `startup-pipeline.md §4` + 新增 §9 | watcher 階段 6 Toggle OFF 跳過 soft timeout;新增 `RestartStartupSequence()` Retry 小節 | 我 M8-4b 前補 |
|
||
|
||
---
|
||
|
||
## G. 第二輪結論
|
||
|
||
**✅ 通過**(需小改,TDD 側 3 個 Minor)
|
||
|
||
**Design Spec v2.1 側**:
|
||
- **Major 2/2 修好** — `settings-update.md §2.2` 檔名 + Linux 預設 OFF 全部落地
|
||
- **Design 側 Minor 8/8 修好**(m-1/m-4/m-5/m-8/m-11/m-12 + M-1/M-2)
|
||
- **無新發現 Design 側問題**,Design Agent **無需再動 v2.1 任何檔案**
|
||
|
||
**TDD v2.1 側**(我自己負責,M8-4b 執行前補):
|
||
- 第一輪遺留 4 項(m-2/m-7/m-9/m-10)
|
||
- 第二輪新增 3 項(2-m1/2-m2/2-m3)
|
||
- 合計 7 項小改,估計 1-2 小時完成
|
||
|
||
**R5-E startup-progress.md 通過**:Design 定義完整且技術可行,與 TDD startup-pipeline.md 7 成對齊,3 處差異全部是 TDD 側可小改。
|
||
|
||
**3 個 Design v2.1 懸念已技術回答**:
|
||
- D-Q1 文案:不影響技術,Design 自由定稿
|
||
- D-Q2 安全軟體偵測:不做(過度設計),採通用文案
|
||
- **D-Q3 Retry 按鈕語意:採「重置整個啟動流程」**,`RestartStartupSequence()` 可行,實作細節見 D-Q3 區塊
|
||
|
||
**不阻擋進 M8 開發**。三方 v2.1 已收斂。
|
||
|
||
**下一步**:Orchestrator 可將此第二輪結論 + Design v2.1 現況 + TDD 7 項待補 → 呈報使用者最終確認 → 我補 TDD 小改 → 進 M8-1。
|
||
|
||
**審閱日期**:2026-04-14(第二輪)
|