M8-10a smoke test 抓到 M1 就潛藏的 P0 latent bug:server 預設 dataDir = <binary>/data,bundle 內解析成 Contents/Resources/bin/data/(空目錄), 實際 models.json + 8 個 .nef 住在 Contents/Resources/data/(上一層)。 Wails 又傳 --data-dir 成 user home(writable),同樣沒 models.json。 結果任何正式啟動路徑下 /api/models 都回 total: 0,M1-M7 smoke 從沒 跑過這個 endpoint 才漏抓。 修法:把「read-only bundle 內資料」和「writable user 資料」語意拆開。 - 新增 findFirstExisting(candidates, sentinel) helper - 新增 resolveBuiltInDataDir:①VISIONA_BUNDLE_LIB_DIR/data(AppImage) ②<base>/data ③<base>/../data ④<base>/../Resources/data ⑤<base>/../lib/visiona-local/data(Linux FHS)。命中條件是 models.json 存在為 regular file,避開 Wails build artifact 留下的 空 data/ 目錄 - main() 拆 builtInDataDir(modelRepo + flash.Service 用)與 dataDir (custom-models / sentinel / logs 用),職責分明 - flash.NewService 改吃 builtInDataDir — 它要解析 models.json 裡的 相對路徑 "data/nef/kl520/xxx.nef",來源是 bundle 不是 user home - resolveBridgeScript 同步修(同樣的技術債一起清),候選 env var 優先 + FHS fallback,避免 Linux AppImage 上 kneron_bridge.py 也找不到 - fallback 全 filepath.Abs 化,log.Printf 印嘗試過的路徑清單便於除錯 驗證(build / vet / test + smoke)全綠: - macOS bundle:/api/models → 15 models ✅ - dev mode(server/ 下 go run):15 models ✅ - Linux AppImage 模擬 + env var:命中候選 1 ✅ - Linux AppImage 模擬 + 無 env var:命中候選 5(FHS)✅ - 全不命中:log 印完整 tried 清單 + server 不 crash ✅ Reviewer 兩輪通過。第一輪抓到 Linux AppImage 未覆蓋(Major-1)+ 2 Minor + 2 Suggestion;第二輪確認全部處理到位、新發現兩項非阻擋風格建議列為 技術債。報告見 .autoflow/05-implementation/reviews/review-m8-10a-*.md。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
21 KiB
Code Review 報告 — M8-10a P0 Latent Bug 修復(built-in data dir 解析)
審查摘要
- 審查對象:
server/main.go(新增resolveBuiltInDataDirhelper +main()拆分builtInDataDir/dataDir兩個變數) - 關聯檔:
server/internal/flash/service.go(接收參數由dataDir→builtInDataDir,無 body 變動) - 產出 Agent:Backend
- 審查結果:⚠️ 需修 1 個 Major(Linux AppImage 布局未覆蓋)後通過
- 問題統計:Critical: 0 / Major: 1 / Minor: 2 / Suggestion: 2
獨立驗證結果
| 驗證項 | 指令 | 結果 |
|---|---|---|
| Build | cd server && go build -o /tmp/rv-server . |
✅ PASS |
| Vet | cd server && go vet ./... |
✅ PASS |
| Test | cd server && go test -count=1 ./... |
✅ 全綠(api / api/handlers / api/ws / device / model 5 個有測試的 package 全部通過) |
| Smoke (bundle) | 直接跑 .../Contents/Resources/bin/visiona-local-server -port 3801 -data-dir $(mktemp -d) |
✅ Built-in data dir: .../Contents/Resources/data → Loaded 15 built-in models → GET /api/models 回 15 個 model,kl520-yolov5-detection 的 filePath = data/nef/kl520/kl520_20005_yolov5-noupsample_w640h640.nef,bundle 實體 .nef 檔存在(7,506,224 bytes,與 catalog modelSize: 7200000 為人工估值合理吻合) |
| Smoke (dev mode) | 把 binary 放到 server/ 下跑 ./rv-server-tmp -port 3802 -data-dir $(mktemp -d) |
✅ Built-in data dir: /Users/jimchen/visionA/local-tool/server/data → Loaded 15 built-in models → GET /api/models 回 15 個 model |
修復前的 bug 無法在修復後重現:不管 Wails 傳什麼 --data-dir,builtInDataDir 都從 bundle 內解析,/api/models 保證回 > 0 個 model。
所有候選布局覆蓋狀態
| 布局 | binary 位置 | data 位置 | 命中候選 | 覆蓋狀態 |
|---|---|---|---|---|
| macOS bundle | Contents/Resources/bin/visiona-local-server |
Contents/Resources/data/ |
<base>/../data |
✅ 已驗證 |
| Dev mode(go run / 直接在 server/ 下) | server/visiona-local-server(或 cwd) |
server/data/ |
<base>/data |
✅ 已驗證 |
Windows installer(Inno Setup,installer/windows/visiona-local.iss L72/L86) |
{app}\bin\visiona-local-server.exe |
{app}\data\ |
<base>/../data |
✅ 理論命中(未在 Windows 機器實跑,但 Inno 布局 = base = {app}\bin,<base>/../data = {app}\data,路徑對齊) |
Linux AppImage(installer/linux/build-appimage.sh L29-33, L80) |
$APPDIR/usr/bin/visiona-local-server |
$APPDIR/usr/lib/visiona-local/data/ |
無(三個候選算出來分別是 /usr/bin/data、/usr/data、/usr/Resources/data,沒一個對) |
❌ 不覆蓋 — 見 Major-1 |
文件符合性檢查
這次是 P0 bug fix(既有行為修復),依 Autoflow S 級規則不需新增 PRD/TDD 條目。修復與現有 TDD §架構決策(bundle 內唯讀 vs user home 可寫分離)一致——事實上修復才讓程式碼行為與 TDD 的決策真正對齊(修復前是文件對但程式碼錯)。
問題清單
Critical(必須修復,阻擋交付)
無。
Major(必須修復,但不阻擋 macOS 初步交付)
| # | 檔案 | 行數 | 問題描述 | 建議修改方式 |
|---|---|---|---|---|
| M-1 | server/main.go |
95-112 | resolveBuiltInDataDir 的三個候選沒有覆蓋 Linux AppImage 布局。AppImage 把 server binary 放 usr/bin/、data 放 usr/lib/visiona-local/data/(見 installer/linux/build-appimage.sh L29-33, L80),三個現有候選 <base>/data、<base>/../data、<base>/../Resources/data 都不會命中。結果:Linux 版 AppImage 啟動後 built-in catalog 依然載入 0 個 model,等於這個 P0 bug 在 Linux 上沒被修掉。另外 AppRun 已經 export VISIONA_BUNDLE_LIB_DIR=${HERE}/usr/lib/visiona-local(L134),server 目前完全沒讀這個 env var。備註:同樣的缺失其實也存在於先前就有的 resolveBridgeScript(L60-78),不是本次 PR 引入的 regression,但應同時修掉。 |
建議兩層保險同時做:①在 resolveBuiltInDataDir 開頭優先讀 VISIONA_BUNDLE_LIB_DIR env(若非空且 <env>/data/models.json 存在則直接回 <env>/data);②新增第 4 個候選 <base>/../lib/visiona-local/data(對齊 FHS 布局,未來其他 Linux 打包方式也能用)。resolveBridgeScript 同步比照修掉,避免 Linux 版 Kneron bridge 也找不到。 |
Minor(建議修復)
| # | 檔案 | 行數 | 問題描述 | 建議修改方式 |
|---|---|---|---|---|
| m-1 | server/main.go |
110-111 | 三個候選全部沒命中時回的 fallback 是 filepath.Join(base, "data"),不是 filepath.Abs(...) 後的絕對路徑,與成功路徑回的是絕對路徑不一致(成功分支有 filepath.Abs(c))。雖然 model.NewRepository 還是能用相對路徑讀檔,但 log 裡印出來的 Built-in data dir: ... 會是相對路徑,讓除錯變難。 |
fallback 也過一次 filepath.Abs:if abs, err := filepath.Abs(filepath.Join(base, "data")); err == nil { return abs }; return filepath.Join(base, "data")。 |
| m-2 | server/main.go |
95-112 | 函式沒 log 自己試了哪幾條路徑。落到 fallback 時 NewRepository 只會印 Warning: could not load models from <path>,使用者看不到「我還試了另外兩個位置都沒中」。bug reporting 體驗不佳。 |
在 fallback 分支 return 前用 log.Printf(或注入 logger)印出嘗試過的三個候選路徑,或在函式簽名改成接收 logger。 |
Suggestion(非必要,改善建議)
| # | 檔案 | 行數 | 建議內容 |
|---|---|---|---|
| s-1 | server/main.go |
95-112 | resolveBuiltInDataDir 和 resolveBridgeScript(L60-78)兩個函式結構幾乎一模一樣(都是「一組候選 → 找存在的 → fallback 回第一個」)。可以抽一個通用 findFirstExisting(candidates []string, sentinel string) (string, bool),讓兩者各自只定義候選清單和 sentinel 檔案(models.json vs kneron_bridge.py),減少未來「改了一個忘了改另一個」的風險。 |
| s-2 | server/main.go |
146-149 | dataDir 在 cfg.DataDir == "" 時 fallback 到 builtInDataDir(= bundle 內 read-only 目錄),但這只服務 dev mode。實際執行時 sentinel (.first-ws-connected)、custom-models、logs 都可能想寫進去,而 bundle 內目錄在 prod 是 read-only。雖然 dev mode 下通常有寫權限,但把「writable user dir fallback 到 read-only bundle dir」這件事放進註解說清楚比較好,避免未來有人以為這是正常的 prod 行為。 |
邊界情況檢查
| 情境 | 行為 | 評估 |
|---|---|---|
os.Executable() 失敗 |
baseDir 回 ".",resolveBuiltInDataDir 試 ./data、../data、../Resources/data,其中任一有 models.json 就命中 |
✅ 合理,接力自然 |
| 三個候選都沒命中 | 回 <base>/data,model.NewRepository 印 warning,r.models = [],server 繼續跑 |
✅ 容錯策略合理(見 Minor m-2:若能多印「我試過的路徑」更好) |
目錄對但 models.json 損毀(JSON 解析錯) |
os.Stat 判定命中 → NewRepository json.Unmarshal 印 warning → 0 個 model |
✅ 合理,不會誤判成「找別的目錄」 |
目錄對但 models.json 是 0 bytes |
os.Stat 判定命中(non-dir)→ NewRepository json.Unmarshal 失敗印 warning → 0 個 model |
✅ 同上 |
models.json 是個目錄(奇怪但可能發生) |
info.IsDir() 為 true,跳過該候選繼續試下一個 |
✅ 已處理 |
cfg.DataDir 指定為不存在路徑 |
只影響 writable 部分:custom-models 讀取會 warning 但 empty、sentinel 寫入失敗但 WS Hub 會跳過、logs 可能寫不出。built-in catalog 不受影響(依然從 builtInDataDir 讀) |
✅ 修復後行為比修復前好 —— 修復前一個無效 --data-dir 會同時把 built-in catalog 弄掛 |
cfg.DataDir 指定為路徑但 parent 不存在 |
同上 | ✅ |
其他 dataDir 誤用檢查(Grep server/ 全專案)
我 grep 了 dataDir / DataDir / models.json / nef/kl / data/nef 的所有出現位置,確認:
server/internal/api/ws/hub.go:sentinelDataDir(用於.first-ws-connectedsentinel)→ ✅ 語意正確,寫入 writable user dataDirserver/internal/config/config.go:DataDirCLI flag → ✅ 沒改server/internal/flash/service.go:s.dataDir解析data/nef/...相對路徑 → ✅ 本次修復已把flash.NewService改吃builtInDataDir,語意對齊main.go其他位置(sentinel / custom-models / WS Hub)→ ✅ 都正確用 writabledataDir,沒有誤用
沒找到其他地方把「應屬 bundle 內 read-only 的檔案」錯用 writable dataDir 解析。 本次修復清單完備。
優點
- 做法對齊既有範式:
resolveBuiltInDataDir的候選路徑邏輯(1: dev flat / 2: installer / 3: macOS bundle)和既有resolveBridgeScript一致,未來維護者不需學新模式,讀一個就懂兩個。 - 語意清晰拆分:
builtInDataDir(read-only, bundle 內)和dataDir(writable, user home)兩個變數名 + 兩段 comment block 把意圖講得很清楚,修復前後讀 code 一看就懂「為什麼要兩個」。model.NewRepository和flash.NewService傳的是builtInDataDir、custom-models/WS sentinel傳的是dataDir,分工直觀。 - Dev mode 不 regression:
cfg.DataDir == ""時 fallback 回builtInDataDir,讓go run ./server繼續可跑 —— 重要,因為它同時讓 reviewer 這次能在server/目錄下獨立驗證 dev mode。 - 命中條件嚴格:用「
models.json存在且非目錄」當 sentinel(而不是data/目錄存在就行),避免選到 wails build artifact 留下的空data/目錄(這正是修復前誤入的那個坑)。 - 容錯好:三候選都沒命中時 fallback 而非 panic,
NewRepository也只印 warning 不 crash,符合「不 regression、壞的優雅」。 - 增量最小:唯一改動檔就是
server/main.go(+40 −6),沒動flash/service.go的 body,風險面小。 - Build/vet/test 全綠,獨立 smoke(bundle + dev mode 兩種路徑)也全綠。
總結意見(≤400 字)
M8-10a 這個 P0 latent bug 的修復方向正確、做法乾淨,唯一改動檔 server/main.go 把 builtInDataDir(read-only bundle 內)與 dataDir(writable user home)拆成兩個語意明確的變數,並新增 resolveBuiltInDataDir helper 以 models.json 存在為命中條件自動跨 dev / installer / macOS bundle 三種布局。Flash service 同步改吃 builtInDataDir,避免相對路徑被誤解到 user home。Build / vet / test / smoke(bundle + dev mode)四項獨立驗證全綠,/api/models 穩定回 15 個 model,實體 .nef 檔路徑可對到 bundle 內。
但 Linux AppImage 布局(usr/bin/visiona-local-server + usr/lib/visiona-local/data/)三個候選都不命中(Major-1),等於這個 P0 bug 在 Linux 上繼續潛藏——雖然同樣的缺失 resolveBridgeScript 先前就存在、並非本次 PR 引入。修復建議:①讀 VISIONA_BUNDLE_LIB_DIR env(AppRun 已 export)②加第 4 候選 <base>/../lib/visiona-local/data。兩個 Minor(fallback 沒 abs 化、沒 log 試過的路徑)和兩個 Suggestion(抽公用 helper、dev mode writable fallback 註解)屬改善性質,不阻擋交付。
結論:⚠️ 需修 1 個 Major(Linux AppImage 覆蓋)後可交付;如果本次交付明確只限 macOS + Windows 兩平台,Major-1 可降級為「Linux 平台列為 known issue 追蹤到 M9」並直接放行。建議優先採前者,連 resolveBridgeScript 一起修掉,避免兩個函式分開欠債。
第二輪 Review(2026-04-15)
審查對象
第一輪所有 Major / Minor / Suggestion 的修復 —— 唯一改動檔 server/main.go(+119 / −24 行)。
第一輪問題處理狀態
| # | 問題 | 狀態 | 驗證 |
|---|---|---|---|
| Major-1 | Linux AppImage 布局未覆蓋(三候選全不命中) | ✅ 解決 | 候選清單補兩層:①env VISIONA_BUNDLE_LIB_DIR/data(AppRun 已 export)② FHS <base>/../lib/visiona-local/data。情境 2(模擬 AppImage 布局 + env var)→ log Built-in data dir: .../usr/lib/visiona-local/data、Loaded 1、GET /api/models total=1 ✅;情境 3(同布局但不設 env,靠 FHS 候選 5 命中)→ 同樣成功 ✅。resolveBridgeScript 同步修掉,連帶把第一輪備註的技術債清掉。 |
| Minor-1 | fallback 沒 filepath.Abs 化 |
✅ 解決 | L145-149、L103-107:兩個函式 fallback 都過 filepath.Abs,絕對路徑能 err 時才退成 Join 的相對路徑(絕不 crash)。全不命中情境 log 印出 Built-in data dir: /var/.../data(絕對路徑)✅。 |
| Minor-2 | 沒 log 試過的候選路徑 | ✅ 解決 | L142、L100:log.Printf("warn: ... not found. Tried: %v", tried);findFirstExisting 每個候選都先 filepath.Abs 再丟進 tried,log 出來是絕對路徑清單,debug 訊息清楚。全不命中情境實測 log 輸出完整 4/5 個候選絕對路徑 ✅。 |
| Suggestion s-1 | 抽 findFirstExisting helper |
✅ 解決 | L53-73 實作 findFirstExisting(candidates, sentinel) (string, []string),resolveBuiltInDataDir(L139)和 resolveBridgeScript(L97)都改用。未來改動兩個函式的共通邏輯只需改一處。 |
| Suggestion s-2 | dev mode writable fallback 加註解 | ✅ 解決 | L180-190:5 行 comment block 明確寫出「dev mode-only fallback、production 下 Wails 永遠會傳 --data-dir、若 packaged 模式沒傳則 writable 操作 log warning 不 crash」。意圖表達清楚。 |
獨立複驗結果
| 驗證項 | 指令 | 結果 |
|---|---|---|
| Build | cd server && go build -o /tmp/rv2-server . |
✅ PASS |
| Vet | cd server && go vet ./... |
✅ PASS |
| Test | cd server && go test -count=1 ./... |
✅ 全綠(api / handlers / ws / device / model 5 個 test package 全通過) |
| 情境 2:AppImage + env var | 模擬 usr/bin/<server> + usr/lib/visiona-local/data/models.json,VISIONA_BUNDLE_LIB_DIR=<appdir>/usr/lib/visiona-local |
✅ Built-in data dir: .../usr/lib/visiona-local/data → Loaded 1 → GET /api/models total=1 |
| 情境 3:AppImage FHS fallback | 同上布局但不設 env var | ✅ 候選 5 <base>/../lib/visiona-local/data 命中,同樣 Loaded 1 → total=1 |
| 情境:全不命中 | 把 server binary 放到 /tmp/xxx/ 獨立目錄,無 env |
✅ 兩個函式 log 出完整 Tried: 清單(都是絕對路徑),fallback 回絕對路徑,server 繼續跑 0 models(不 crash) |
| AppImage 對齊檢查 | 讀 installer/linux/build-appimage.sh L29-33 / L124-138 |
✅ APPDIR/usr/bin/visiona-local-server + APPDIR/usr/lib/visiona-local/{data,scripts} + AppRun export VISIONA_BUNDLE_LIB_DIR=${HERE}/usr/lib/visiona-local —— 與 server 候選 ①(env 路徑)和 ⑤(FHS 路徑)雙雙對齊 |
| cwd 變動風險檢查 | grep 專案找 os.Chdir / Chdir( |
✅ 零匹配 —— ./scripts 相對候選在執行期不會因 cwd 漂移而解析錯 |
候選優先順序誤命中檢查
逐平台驗證新候選 <base>/../lib/visiona-local/data 不會在非 Linux 場景誤命中:
| 平台 | <base> |
候選 5 解析 | 該路徑實際是否存在 | 風險 |
|---|---|---|---|---|
| macOS bundle | Contents/Resources/bin |
Contents/Resources/lib/visiona-local/data |
❌(macOS bundle 不用 FHS) | 無 |
| Windows installer | {app}\bin |
{app}\lib\visiona-local\data |
❌(Inno Setup 放 {app}\data) |
無 |
| Dev mode | server/ 或 . |
../lib/visiona-local/data |
❌ | 無 |
| Linux AppImage | usr/bin |
usr/lib/visiona-local/data |
✅ | 正是目標 |
候選順序把 <base>/data、<base>/../data、<base>/../Resources/data 排在 FHS 之前,所有既有布局都會在進 FHS 前命中。沒有誤命中風險。
新觀察的邊界情況
| # | 觀察 | 評估 | 建議 |
|---|---|---|---|
| E-1 | findFirstExisting 簽名 (string, []string),呼叫端用 dir != "" 判斷 OK |
Go 慣例通常是 (value, bool) 或 (value, error);現行簽名把「hit/miss」和「嘗試清單」合併一個回傳值,讀起來略非 idiomatic,但因為 tried 在 success path 沒用但無害、miss path 才拿去 log,合併簽名避免多一個回傳值,權衡合理 |
不阻擋通過。若要更 idiomatic 可改 (dir string, tried []string, ok bool),但目前 call site 寫法(if dir, tried := ...; dir != "")已經夠清楚 |
| E-2 | resolveBridgeScript / resolveBuiltInDataDir 的 warn 走 std log.Printf,其他資訊走 pkglogger.logger.Info —— log 格式不一致 |
resolve 函式在 main.go 被呼叫的時序上 logger 已存在(L155 pkglogger.New();resolve 在 L177、L236),其實可以注入 logger;但本次修改保留 std log 有個好處:若將來把 resolve 函式移到更早(在 logger 初始化前),不用再改簽名。不影響正確性。 |
不阻擋通過。建議追蹤到下次重構 logger 時一起統一(加 Minor 備忘) |
| E-3 | VISIONA_BUNDLE_LIB_DIR 只在 Linux AppImage AppRun 設定 —— 其他平台若使用者手動設錯,是否會誤命中? |
findFirstExisting 對每個候選做 os.Stat(.../sentinel),env 指到不存在的路徑會 Stat 失敗自然跳下一個候選;env 指到存在且含正確 sentinel 的路徑,代表使用者有意識地 override,行為合理 |
無風險。Resolve 函式的 os.Stat sentinel check 已是自然保險 |
| E-4 | ./scripts 相對路徑候選(resolveBridgeScript 候選 6)在理論上會隨 cwd 漂移 |
Grep 專案零匹配 os.Chdir,server 啟動流程不改 cwd,且相對候選放最後(env / base 相關的絕對候選都不命中才會走到),實務上只在純 dev mode cwd 對齊時觸發 |
無風險 |
新發現問題
Critical:無。
Major:無。
Minor(非阻擋):
| # | 檔案 | 行 | 問題 | 建議 |
|---|---|---|---|---|
| m2-1 | server/main.go |
100, 142 | 兩個 resolve 函式的 warn log 用 std log.Printf 而非 pkglogger.logger.Warn,與後續 logger.Info("Built-in data dir: ...") 格式不一致 |
下次重構 logger 時把 logger(或至少 pkglogger)注入 resolve 函式,統一格式。不影響本輪通過。 |
Suggestion:
| # | 檔案 | 行 | 建議 |
|---|---|---|---|
| s2-1 | server/main.go |
59-73 | findFirstExisting 若改 (dir string, tried []string, ok bool) 可讓 call site 更 idiomatic(if dir, tried, ok := ...; ok { ... } else { log; fallback })。非必須。 |
優點(第二輪新增)
- 修復完整度超出第一輪要求:第一輪只要求 Major-1(Linux 資料目錄覆蓋),Orchestrator 把所有 Minor + 兩個 Suggestion 一併做掉,且把第一輪備註的技術債
resolveBridgeScript也同步修了,避免「兩個函式結構一樣但一修一未修」的分裂狀態。 - 候選順序保守:FHS 候選放在既有候選之後,零誤命中風險(見上表);既有行為零 regression。
findFirstExisting抽象得當:回傳tried清單給 log 用的設計雖然非完全 idiomatic,但對 debug 訊息清晰度貢獻很大(見全不命中情境 log 輸出)—— 這個權衡合理。- 註解品質高:每個 resolve 函式的候選清單都有編號 + 情境說明;
dataDirfallback 的註解把「dev-only / production Wails 永遠傳 / 壞了不 crash」三件事講清楚,未來維護者不會誤解。 - 情境 2 / 3 雙路命中:env var 和 FHS 兩層保險,即使未來 AppRun 的 env var 被移掉或名稱改了,FHS 還能兜底;反之亦然 —— 防禦性設計好。
- 所有驗證全綠:build / vet / test / 3 個布局實跑(bundle 第一輪已驗 / AppImage-env / AppImage-FHS / 全不命中 fallback)全通過。
總結意見(≤300 字)
Orchestrator 本輪把第一輪的 Major-1 + Minor-1/2 + Suggestion s-1/s-2 全數處理,且連帶修掉備註中 resolveBridgeScript 的同樣欠債,新增 findFirstExisting 共用 helper 讓兩個函式結構一致。候選清單補 env var(VISIONA_BUNDLE_LIB_DIR,AppRun 已 export)+ FHS 雙層保險,對 macOS bundle / Windows installer / Dev mode 零誤命中風險。三個布局實跑(AppImage + env var / AppImage FHS fallback / 全不命中 fallback)全綠,log 明確列出嘗試過的絕對路徑,fallback 也已 filepath.Abs 化。Build / vet / test 全綠。新觀察到的 2 個 Minor / Suggestion(std log vs pkglogger 格式一致性、helper 簽名 idiomatic 度)屬改善性,不阻擋交付。修復完整度超出第一輪最低要求,技術債清理徹底。
結論:✅ 通過(可交付三平台:macOS bundle / Windows installer / Linux AppImage)。 追蹤到下次 logger 重構時統一 std log 與 pkglogger 的格式(Minor m2-1)。