依 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>
19 KiB
Reviewer 審查 M8-1 + M8-2 合併結果(2026-04-15)
摘要
- 總結論:✅ 通過(零 Major / 零 Critical;只有若干 Minor 提醒)
- M8-1(砍 yt-dlp):✅ 完整砍除,8 項 deletions.md 條目全達成
- M8-2(砍 Mock):✅ 完整砍除,11 項 deletions.md 條目全達成
- 是否阻擋 M8-4:❌ 不阻擋。M8-4 可以開動(mockMode field 已清、videoIsURL 已清、StartFromURL 已清,M8-4 做 server lifecycle 時無衝突)
- 額外觀察:working tree 同時包含 M8-3(ffmpeg LGPL 切換) 的改動(Makefile / README / installer / scripts / build.yml),該部分依使用者指示不在本次 review 範圍
A. 砍除完整性
A1. M8-1 yt-dlp 砍除清單
| # | 項目 | 預期 | 實測 | 證據 |
|---|---|---|---|---|
| 1 | ResolveWithYTDLP function |
消失 | ✅ | video_source.go diff -91..-140 整段 delete |
| 2 | friendlyYTDLPError function |
消失 | ✅ | 同上 |
| 3 | NewVideoSourceFromURL / ...WithSeek |
消失 | ✅ | video_source.go diff -79..-86 |
| 4 | VideoSource.isURL field + newVideoSource 的 isURL 參數 |
消失 | ✅ | video_source.go L63/L68 簽名已簡化 |
| 5 | ytdlpHosts map |
消失 | ✅ | camera_handler.go -341..-373 |
| 6 | urlKind enum / urlYTDLP / urlDirect / urlBad |
消失 | ✅ | 同上 |
| 7 | classifyVideoURL() function |
消失 | ✅ | 同上 |
| 8 | StartFromURL handler |
消失 | ✅ | -408..-497 整段 delete |
| 9 | CameraHandler.videoIsURL field |
消失 | ✅ | camera_handler.go L33/L303/L536 三處清除 |
| 10 | handleVideoSeek 的 videoIsURL 分支 |
消失 | ✅ | L566 簡化為直接呼叫 NewVideoSourceWithSeek |
| 11 | /media/url route |
消失 | ✅ | router.go:83 delete |
| 12 | deps/checker.go yt-dlp check |
消失 | ✅ | checker.go -30..-32 |
| 13 | main.go 註解改 ffmpeg/ffprobe |
✅ | L88 | 註解已改 |
| 14 | frontend startFromUrl function |
消失 | ✅ | camera-store.ts -167..-196 |
| 15 | frontend videoMode / videoUrl state |
消失 | ✅ | source-selector.tsx -51..-52 |
| 16 | frontend handleUrlSubmit function |
消失 | ✅ | source-selector.tsx -94..-96 |
| 17 | frontend URL tab UI(mode toggle + input + help) | 消失 | ✅ | source-selector.tsx -183..-253 |
| 18 | i18n pasteUrl / urlPlaceholder / urlHelpText / cannotOpenVideoUrl |
消失 | ✅ | types.ts / zh-TW.ts / en.ts 三檔同步 |
| 19 | i18n camera.uploadFile(mode 切換用) |
消失 | ✅ | M8-1 自補:三檔同步刪(本來 deletions.md 沒列,但砍完 mode toggle 就 orphan) |
| 20 | Makefile vendor-ytdlp / vendor-ytdlp-windows / vendor-ytdlp-linux |
消失 | ✅ | Makefile 全檔無 yt-dlp / ytdlp / YTDLP 字樣 |
| 21 | installer/windows visiona-local.iss yt-dlp.exe 行 | 消失 | ✅ | L76 已 delete |
| 22 | installer/linux build-appimage.sh yt-dlp loop | 消失 | ✅ | L61 for tool in ffmpeg ffprobe |
| 23 | scripts/bootstrap-linux.sh vendor-ytdlp-linux |
消失 | ✅ | L57 |
| 24 | scripts/bootstrap-windows.ps1 vendor-ytdlp-windows |
消失 | ✅ | L200 |
| 25 | visiona-local/app.go locateBundleBinDir 的 yt-dlp 字串 |
改為 ffprobe |
✅ | app.go:871 fileExists("ffprobe") |
| 26 | visiona-local/app.go startServer 註解「ffmpeg / yt-dlp」 |
改為 ffmpeg / ffprobe |
✅ | L480 |
| 27 | README.md yt-dlp 三處敘述 | 消失 | ✅ | L18, L74, L128, L131, L179 五處清除(M8-1 自補) |
| 28 | .github/workflows/build.yml vendor-ytdlp-* |
消失 | ✅ | L135/L233 |
A1 結論:M8-1 砍除 100% 達成,且主動自補了 4 個 deletions.md 未明列的位置(README 五處、workflow、app.go bundle dir、i18n uploadFile orphan key)。
A2. M8-2 Mock 砍除清單
| # | 項目 | 預期 | 實測 | 證據 |
|---|---|---|---|---|
| 1 | server/internal/driver/mock/mock_driver.go |
整檔刪 | ✅ | git status: deleted |
| 2 | server/internal/driver/mock/ 目錄 |
刪 | ✅ | find server/internal/driver/mock 無 |
| 3 | server/internal/camera/mock_camera.go |
整檔刪 | ✅ | git status: deleted |
| 4 | VISIONA_MOCK env 偵測 |
消失 | ✅ | grep -r VISIONA_MOCK 0 match |
| 5 | config.MockMode / MockCamera / MockDeviceCount 欄位 |
消失 | ✅ | config.go L19..L26 簡化 |
| 6 | --mock / --mock-camera / --mock-devices flag |
消失 | ✅ | config.go L36..L38 已刪 |
| 7 | device.Manager.mockMode field |
消失 | ✅ | device/manager.go L17 |
| 8 | NewManager(registry, mockMode, mockCount, scriptPath) 簽名簡化 |
NewManager(registry, scriptPath) |
✅ | device/manager.go L22 |
| 9 | device/manager.go 所有 if m.mockMode {} 分支 |
消失 | ✅ | Start() / Rescan() 已簡化 |
| 10 | mockdriver import |
消失 | ✅ | device/manager.go import 區已清 |
| 11 | camera.Manager.mockMode / mockCamera field |
消失 | ✅ | camera/manager.go L16..L19 |
| 12 | camera.NewManager(mockMode) → NewManager() |
簡化 | ✅ | camera/manager.go L23 |
| 13 | camera.Manager.ListCameras/Open/Close/ReadFrame mock 分支 |
消失 | ✅ | camera/manager.go L29-82 |
| 14 | main.go Mock mode: log |
消失 | ✅ | main.go:86 已改為 Dev mode: %v, Python mode: %s |
| 15 | main.go NewManager(registry, cfg.MockMode, ...) 呼叫 |
簡化 | ✅ | main.go:141 |
| 16 | main.go camera.NewManager(cfg.MockCamera) 呼叫 |
簡化 | ✅ | main.go:146 |
| 17 | visiona-local/app.go mockMode field |
消失 | ✅ | app.go:82 已清除 |
| 18 | visiona-local/app.go NewApp() 讀 VISIONA_MOCK env |
消失 | ✅ | app.go:117 替換為 R5-5a 註解 |
| 19 | visiona-local/app.go startServer 所有 a.mockMode 分支 |
消失 | ✅ | L425 / L435 / L455-465 / L488 皆清除 |
| 20 | frontend settings/page.tsx runtimeMode Select + Separator |
消失 | ✅ | page.tsx -140..-158 |
| 21 | frontend i18n runtimeMode / ...Mock / ...Real / ...Hint |
消失 | ✅ | types.ts / zh-TW.ts / en.ts L270..L277 同步 |
| 22 | frontend i18n noDevices 文字去 Mock |
改為 R5-v2-7 empty state | ✅ | 兩語言都改為「請連接 KL520 / KL720 後按掃描」 |
| 23 | Makefile dev-mock target |
消失 | ✅ | Makefile 無 dev-mock |
| 24 | README Mock 段落 | 消失 | ✅ | L73 「Mock 模式」bullet 整行刪、L24 零學習成本 bullet 改為「插上 Kneron 裝置 30 秒」 |
| 25 | manager_test.go TestNewManager_MockMode |
消失 | ✅ | 已刪 |
| 26 | manager_test.go TestManager_ListDevices_Empty 新增 |
新增 | ✅ | L39..L48,R5-5a 紀念註解合法 |
| 27 | api_e2e_test.go 整檔刪 |
刪 | ✅ | git status: deleted |
| 28 | camera-controls.tsx 'mock-cam-0' fallback |
改為 disabled={cameras.length === 0} |
✅ | L22..L28 已改 |
A2 結論:M8-2 砍除 100% 達成,且主動自補了 api_e2e_test.go 整檔刪、manager_test.go 新增空 list 測試、camera-controls.tsx fallback 修正。
B. 誤刪檢查(保留功能 red line)
| 保留項目 | 狀態 | 證據 |
|---|---|---|
POST /api/camera/start |
✅ 保留 | router gin-debug 有 /api/camera/start |
POST /api/camera/stop |
✅ 保留 | 同上 |
POST /api/media/upload/image |
✅ 保留 | 同上 |
POST /api/media/upload/video(本機檔案,非 URL) |
✅ 保留 | 同上 |
POST /api/media/upload/batch-images |
✅ 保留 | 同上 |
GET /api/media/batch-images/:index |
✅ 保留 | 同上 |
POST /api/media/seek(本機影片 seek) |
✅ 保留 | 同上 |
GET /api/camera/stream |
✅ 保留 | 同上 |
GET /api/devices(無硬體回空 list,非 mock) |
✅ 保留 | smoke test:{"devices":[]} |
模型管理 /api/models* |
✅ 保留 | router gin-debug 完整 |
WebSocket /ws/devices/:id/inference |
✅ 保留 | router gin-debug |
VideoSource + NewVideoSource + NewVideoSourceWithSeek |
✅ 保留 | video_source.go L70/L75 |
ProbeVideoInfo |
✅ 保留 | M8-1 未觸碰 |
handleVideoSeek 本地影片分支 |
✅ 保留 | camera_handler.go:566 |
stopActivePipeline |
✅ 保留 | L532 |
ffmpeg 本身 |
✅ 保留 | deps checker + PATH 注入都還在 |
B 結論:零誤刪。所有 R5 要求保留的功能完整存在。
C. 殘留 grep 結果
命令(排除 .autoflow/ / .git/ / vendor/ / node_modules/):
C1. yt-dlp 組
grep -rn 'yt-dlp\|ytdlp\|YTDLP\|ResolveWithYTDLP\|StartFromURL\|ytdlpHosts\|urlYTDLP\|videoIsURL\|startFromUrl\|pasteUrl\|urlPlaceholder\|urlHelpText'
結果:0 match ✅
C2. Mock 組(嚴格:排除合法測試框架用詞)
grep -rn 'VISIONA_MOCK\|MockMode\|MockCamera\|NewMockDriver\|NewMockCamera\|mockdriver\|mock_camera\|runtimeModeMock\|runtimeModeReal\|mock-cam-0'
結果:0 match ✅
C3. 寬鬆 Mock 組(含一般 mock 字樣)
server/internal/ 下只剩:
server/internal/device/manager_test.go:43—// 無硬體時應回傳空 list(R5-5a:沒插硬體就空白,不給 Mock 資料)← 合法紀念註解
frontend/src/tests/ 下的 vi.mock* / mockResolvedValue / clearAllMocks — 合法 Vitest API
visiona-local/ / server/main.go / server/internal/camera/ / server/internal/driver/ 下 — 0 match
C 結論:grep 完全 clean。
D. Build / test 結果
D1. cd server && go build ./...
✅ PASS(無輸出)
D2. cd server && go vet ./...
✅ PASS(無輸出)
D3. cd server && go test ./...
✅ PASS
ok visiona-local/server/internal/device (cached)
ok visiona-local/server/internal/model (cached)
其餘 package 顯示 [no test files],符合預期(本次沒有新增 unit test)。
D4. cd visiona-local && go build .
✅ PASS(無輸出)
D5. cd frontend && pnpm build
✅ PASS — Compiled successfully in 4.2s,TypeScript 通過,12 頁 SSG 生成完成。
D 結論:五項 build / test 全綠。
E. Smoke test 結果
啟動 go run . --port 3722,等 3 秒:
| 測試 | 結果 | 備註 |
|---|---|---|
| Server 啟動成功 | ✅ | 看到 Server listening on 127.0.0.1:3722 |
log 無 Mock mode: |
✅ | log 只剩 Dev mode: false, Python mode: auto |
log 無 yt-dlp: |
✅ | deps check 只有 ffmpeg 和 python3 |
GET /api/devices |
✅ 200 | {"data":{"devices":[]},"success":true} — 空 list,非 mock 資料 |
POST /api/media/url |
✅ 404 | Route 已砍,gin 回 404(符合預期) |
| router gin-debug 印出 | ✅ | 完全沒有 StartFromURL,只有 UploadVideo / UploadImage / UploadBatchImages / SeekVideo / ListCameras / StartPipeline / StopPipeline / StreamMJPEG |
| Kneron 偵測 | ✅ | No Kneron devices detected — 走真實硬體 path(機器上沒插就空 list) |
E 結論:Runtime 行為完全符合 R5-5a / R5-7 預期。
F. Edge case 自補合理性
F1. M8-1 自補
| 自補項 | 合理性 |
|---|---|
| README 清 yt-dlp 五處 + 改「零學習成本」文字 | ✅ 合理,deletions.md 未列但必然要改 |
.github/workflows/build.yml 清 vendor-ytdlp-* 兩處 |
✅ 合理,否則 CI build 會叫不存在的 target |
visiona-local/app.go:locateBundleBinDir 的 fileExists("yt-dlp") → fileExists("ffprobe") |
✅ 合理。ffprobe 會隨 M8-3 進 bundle bin dir,用 ffprobe 當 sentinel 比 ffmpeg 更精準(避免和 system ffmpeg 混淆) |
刪 camera.uploadFile i18n key |
✅ 合理,URL tab 砍後這個 key 變 dead key |
F2. M8-2 自補
| 自補項 | 合理性 |
|---|---|
api_e2e_test.go 整檔刪 |
✅ 合理。該 test 依賴 mock driver + 舊 NewRouter 簽名,修起來成本高於價值。deletions.md §2.7 本來就留給執行者決定 |
camera-controls.tsx 的 'mock-cam-0' fallback 改 disabled={cameras.length === 0} |
✅ 合理。之前是「沒 camera 還是硬塞 mock-cam-0」,現在改為 button disabled + guard if (firstCam),邏輯更嚴謹 |
manager_test.go 新增 TestManager_ListDevices_Empty |
✅ 合理。補足了刪除 TestNewManager_MockMode 後的測試缺口,明確驗證 R5-5a 的空 list 行為 |
visiona-local/app.go 的註解改寫(「M7:預設真實硬體模式」→「R5-5a:沒插硬體就顯示空白狀態」;「M1 預設 mock 模式...」→「R5-5a 之後:python 失敗直接擋啟動」) |
✅ 合理,註解與新行為一致 |
F 結論:兩個 Agent 的自補都是必要且最小化的,沒有過度也沒有不足。
G. 兩個 milestone 交集檔案的正確性
G1. visiona-local/app.go
- M8-1 改:
locateBundleBinDir的fileExists("yt-dlp")→fileExists("ffprobe");startServer註解「ffmpeg / yt-dlp」→「ffmpeg / ffprobe」 - M8-2 改:
App.mockModefield 砍、NewApp()讀VISIONA_MOCK砍、startServer所有a.mockMode分支砍、註解改寫
兩者互不干擾,merge 正確。args 陣列現在無條件帶 --python-mode 和(若 pyBin != "")--python,符合 M8-2 deletions.md §3.1 的改法。ensurePythonRuntime 錯誤不再被 mockMode 遮蔽,直接 return error,符合 R5-5a 精神。
✅ 正確
G2. server/main.go
- M8-1 改:L88 PATH 注入註解「yt-dlp / ffmpeg」→「ffmpeg / ffprobe」
- M8-2 改:L86
logger.Info("Mock mode: ...")→Dev mode: ..., Python mode: ...;L141device.NewManager簽名簡化;L146camera.NewManager簽名簡化
兩者無衝突,merge 正確。
✅ 正確
G3. Makefile
- M8-1 改:
vendor-ytdlp/vendor-ytdlp-windows/vendor-ytdlp-linux三個 target 整段刪、.PHONY移除、vendor-sync/payload-*依賴移除、help 文字更新、YTDLP_URL_*變數刪、payload-*內cp .../yt-dlp ...行刪 - M8-2 改:
.PHONY的dev-mock移除、底部dev-mock:target 整段刪 - M8-3(不審):
vendor-ffmpeg系列從 GPL 改為 LGPL v3 + 新增vendor-ffmpeg-macos-buildtarget + ffmpeg URL 從 evermeet / johnvansickle / BtbN-gpl 改為 BtbN-lgpl
M8-1 + M8-2 在 Makefile 內的改動乾淨、無殘留。M8-3 內容雖然混入但不與 M8-1 / M8-2 衝突(操作的是不同的 line / variable / target)。
✅ M8-1 + M8-2 在 Makefile 的部分正確
G4. README.md
- M8-1 改:L18(zero-dep bullet 去 yt-dlp)、L74(支援 URL / yt-dlp 行去掉)、L128 / L131(vendor-sync / payload-macos 描述去 yt-dlp)、L182(授權表格 yt-dlp row 刪)
- M8-2 改:L24(零學習成本 bullet 去 Mock)、L73(Mock 模式 bullet 整行刪)
- M8-3(不審):L163 GPL TODO 整行刪、L180 ffmpeg GPL row 改 LGPL
M8-1 + M8-2 在 README 的部分正確且完整。
✅ 正確
G5. scripts/installer 類(bootstrap-linux / bootstrap-windows / visiona-local.iss / build-appimage.sh / build.yml)
- M8-1 改:清除
vendor-ytdlp-*/yt-dlp*.exe/ tool loop 中的 yt-dlp - M8-3(不審):LGPL 相關的 LICENSE.txt / COPYING.LGPLv3 複製、GPL warning 去除、ffprobe 複製
M8-1 的 yt-dlp 清除動作完整。
✅ 正確
H. 問題清單
Major(阻擋 M8-4)
無。
Minor(可先往下走,後續 M8-6 / 另開處理)
| # | 檔案 | 問題描述 | 建議處理 |
|---|---|---|---|
| 1 | frontend/src/components/camera/source-selector.tsx:184,189 |
accept=".mp4,.avi,.mov" 和 t('camera.mp4AviMov') 仍為三種副檔名 |
不是 M8-1 的 bug。deletions.md §5.1 的「新增 mpeg/mpg」和 milestone-plan M8-6 任務 2+3 明確把副檔名擴充列為 M8-6 的工作。留給 M8-6 處理即可 |
| 2 | server/internal/api/handlers/camera_handler.go:~251(未檢視) |
後端影片 extension whitelist 仍為 .mp4 / .avi / .mov |
同上,milestone-plan M8-6 任務 4 明確列入 |
| 3 | server/internal/deps/checker.go |
CheckAll() 仍只檢查 ffmpeg,沒加 ffprobe |
不是 M8-1 範圍。M8-3 切 LGPL ffmpeg 後 ffprobe 變必備,屆時應在 deps checker 加一項 |
| 4 | working tree 混入 M8-3 內容 | Makefile / README / installer / scripts / build.yml / .gitignore 都有 M8-3 的改動 |
這是「working tree 目前同時有三個 milestone」的狀態,不是 bug。Orchestrator 決定要不要分 commit 即可 |
| 5 | server/internal/api/api_e2e_test.go 整檔刪 |
本來有 E2E HTTP 層級的煙霧測試,刪除後該層級失去測試 | 不 block M8-4,但 M8-10 前應考慮補回一份不依賴 mock 的 e2e(可改為:無硬體環境下測 /api/devices 回空 list + /api/models 回內建模型等純 read-only 路徑) |
Suggestion(可做可不做)
| # | 檔案 | 建議 |
|---|---|---|
| 1 | visiona-local/app.go:671 的 docblock 「R5-5a 之後:python 失敗直接擋啟動(沒有模擬回退)」 |
文字 OK,可考慮補一句「若要無 Kneron 硬體環境 debug,請直接透過 server binary go run 而不透過 Wails 殼」方便開發者 |
| 2 | server/internal/device/manager_test.go:43 的 R5-5a 紀念註解 |
可愛的紀錄,保留 |
I. 通過 / 不通過結論
✅ 通過
理由:
- M8-1 + M8-2 的 28 + 28 = 56 項砍除清單 100% 達成
- 零誤刪,所有 R5 指定保留功能完整存在
- 殘留 grep 完全 clean
- 五項 build / test 全綠
- Smoke test 驗證 runtime 行為正確(空 device list、/media/url 404、log 無 mock / yt-dlp)
- 兩個 Agent 的自補決定全部合理
- 三個 merge 交集檔案(app.go / main.go / Makefile / README)的 M8-1 和 M8-2 改動相容無衝突
下一步建議給 Orchestrator
- M8-1 + M8-2 可以 commit(建議兩個獨立 commit,或一個合併 commit 標示「M8-1 + M8-2 砍除」)
- M8-3 的 working tree 改動需另外處理 — 建議分開 commit / 分開 review
- M8-4 可以開動 — 無技術阻擋
- M8-6 日後執行時 記得處理 Minor #1 和 #2(副檔名擴充 + 後端 whitelist + i18n 改名)
- M8-3 執行完 順便處理 Minor #3(deps checker 加 ffprobe)
優點
- M8-1 Agent:砍得非常乾淨,連 orphan i18n key(
uploadFile)和 bundle bin dir 的 yt-dlp sentinel 都主動處理 - M8-2 Agent:
TestManager_ListDevices_Empty新增測試體現 R5-5a 精神;camera-controls.tsx的 fallback 改為disabled={cameras.length === 0}是比原本塞'mock-cam-0'更嚴謹的寫法 - 兩個 milestone 的 merge:沒有互相踩腳。
visiona-local/app.go尤其漂亮,mockMode砍掉後args陣列變得非常直觀 - grep 殘留 0 match:連
videoIsURL這種容易忘記的 dead flag 都有徹底清除
砍完這兩個 milestone 後,整個 codebase 瘦了 ~655 行 程式碼(29 修改 + 3 刪除),直接對應 R5-7「砍掉非核心功能、focus 推論核心」的決策。砍得漂亮。