# FR-063 Nuitka 落地版打包 — Arc SUMMARY

> 2026-08-15 收案。本文由 `FR-063-LOG.md` 全 16 blocks 濃縮而成（事實以 LOG/STATE 為準；LOG Block 14 作廢之 http2 誤診相關敘述已排除）。
> 母案 Notion CM-1187（Done）；設計文件 `design.md`（D1–D11＋§5）；部署環境變數 `deployment-env.md`。

## 1. 一段話總結

產品進入落地版（on-premise）交付階段，客戶主機上不可再出現一行可讀業務原始碼。本案把 BE 整包用 **Nuitka standalone 編成機器碼 binary**（業務 `.py`＝0），統一入口單 binary 雙模式（`RUN_MODE=api/socketio`），建立可重複的三段 build 管線（rebuild wheel → 編譯 → 打 image ＋七項探針），最終產出並推上 Harbor：**`guidant-ai/guidant-ai-be:1.14.0`＝`latest`（digest `sha256:bba0449d…`，壓縮 0.55GB），產物 commit bake `cc7714d2`**。與 FR-062 license 執法構成雙保險；為三階段戰線第一棒（後續 FR-064 防竄改 → FR-065 Installer）。全程 13 工作卡＋2 子需求卡＋母案全 Done，場外衍生 CM-1206（socket 共編存檔 bug）一併修復收 Done。

## 2. Commits 清單（逐 block 收齊）

### BE（compliance-manager-be）

| Commit | 內容（對應卡） |
|---|---|
| `b053c765` | design.md 定稿（設計棒） |
| `18ef3142` | .1a DI 靜態模組清單（CM-1194） |
| `78937428` | .1b 資源路徑 resource_path（CM-1195） |
| `5acedd9b` | .1g config 三層重整＋runtime_config seed（CM-1200） |
| `b40a5710` | .1c＋.1d 統一入口 main.py＋.env 平鋪（CM-1196/1197） |
| `cb830d09` | config-migration-mapping.md §6 STG/POC 上版 checklist |
| `aa760c50`、`8a83fa98` | .1e version bake＋fail-fast（CM-1198） |
| `d510ed79` | .2a＋.2b build 機環境＋私房 wheel rebuild（CM-1201/1202） |
| `8a697d85` | .2c Nuitka build script（CM-1203） |
| `02146e9d` | jedi 發版收官後 BE push 齊點（pin 還原、poetry.lock 入版控、TURNSTILE 平鋪、ENV 四值新慣例） |
| `381acf76` → `f449f939`（共 5 筆） | .2d 編譯加速：公開套件排除＋dev group 搬遷＋三道自動把關（CM-1205） |
| `78debb6c`、`e8104ef6` | .4 socketio namespace 集中註冊＋Flask-SocketIO 5.6.1 升版（CM-1193） |
| `6de7ffd8` | CM-1206 socket auth context 修復（BE 側） |
| `29c4013f` → `cc7714d2`（共 3 筆） | .3 production image：Dockerfile.production／entrypoint／探針（CM-1192）；`cc7714d2` 即最終產物 bake |
| `0dcabc8d` | build_image 產出（合流輪） |
| `f921ed39` | build script i18n 加 `--statistics`＋.mo 現場重編語意註明 |
| `2834cf4c` | i18n 死條目 `non_existent_key` 清除，兩 locale 回 100% |

### FE（compliance-manager-fe）

| Commit | 內容 |
|---|---|
| `967e67b` | CM-1206 socket 修復 FE 側配合 |

### jedi-*（jedi-python-package）

| Commit / 版本 | 內容 |
|---|---|
| `57e8a41` | .1f LibreOffice 路徑解析共用 helper（`LIBREOFFICE_CMD` env override＋PATH fallback，CM-1199） |
| file-upload `0.0.21`、captcha `0.0.14` | 發版推 Nexus（收官日，pin 還原一併完成） |

## 3. 各子需求摘要

**.1「程式整理」（7 卡 CM-1194~1200）**——讓 codebase 具備可編譯性：
- **.1a DI 靜態模組清單**（CM-1194）：`gen_di_modules_static.py` 產 138 模組靜態清單取代 runtime 目錄掃描（Nuitka 下無檔案系統可掃）；`--check` 走 AST parse 防清單漂移，static 模式缺檔 fail-fast 不靜默退回動態。
- **.1b 資源路徑**（CM-1195）：四類資源讀取點（翻譯目錄／SSP docx 範本／canon／下載範本）統一走 resource_path 解析，打包後可定位。
- **.1c＋.1d 統一入口＋.env 平鋪**（CM-1196/1197）：三支舊入口退役 → 單一 `main.py` 以 `RUN_MODE` 切模式；scheduler 只在 api 模式起（解排程雙跑老問題）；.env 由 JSON 包裝改平鋪（向後相容），逐 key 審計 49 項清 6 死項。
- **.1e version bake＋fail-fast**（CM-1198）：build 期 bake version＋commit 進產物，version 端點成為「哪個版本在跑」的唯一身分證；REGISTERED_APPS 缺模組改 RuntimeError fail-fast（上線即抓到 `resource` 死模組 19 次靜默 warning 的陳年漏拔）。
- **.1f LibreOffice 路徑**（CM-1199，jedi 側）：清除兩 adapter 的平台硬編路徑，共用 helper＋env override。
- **.1g config 三層重整**（CM-1200，源自 D11）：六套環境 class 塌一套；Secret 拆 JSON 包裝＋`config_loader` 集中驗證（缺項一次列全）；營運參數遷 DB `system_config`（缺列 fail-open 回內建預設）；清 SQLALCHEMY_*/APISPEC_* 等死定義。

**.2 Build 管線（4 卡 CM-1201/1202/1203/1205）**：
- **.2a＋.2b build 機＋私房 wheel**：24core build 機環境建置（Nuitka 4.1.3／ccache）；**D2 炸點親驗排除**——dependency-injector 官方 abi3 wheel 在 Nuitka 下 import 炸 SystemError，私房 cp311 重編 wheel PASS，全案最底層風險解除；rebuild script 帶版本感知快取（命中 0.047s）。
- **.2c Nuitka build script**：整包編譯一次過（首編 56m42s／二次 18m36s），產物 832MB、`.py`＝0、資源在位、binary 親起驗證 .1 全部機制在真產物內活著；修兩個「開發環境看不出、正式承載才死」的陰險 bug（gunicorn 動態載入模組顯式 include＋升健檢項）。
- **.2d 編譯加速**（CM-1205）：user 拍板「**公開套件不需編譯保護**」——pymupdf（31 分編譯地板）等公開大套件 `--nofollow` 排除原樣附帶、測試依賴搬 dev group；冷編 56m42s → 穩態 **12m11s（省 78%）**；三個靜默複製 bug 全修＋三道自動把關（完整性斷言／功能探針 9 項／dry-run 預檢）。jedi-*＋業務碼照舊全編譯。

**.3 Production image**（CM-1192）：`Dockerfile.production`（glibc 對齊、非 root uid1000、entrypoint chown-then-setpriv、apt 清單＋fc-match 斷言確保 NotoSansCJK）＋volume 佈局（design §5.5）＋`probe_container.sh` 七項探針（§5.6，末輪 PASS=11 FAIL=0）；合流輪 rebuild bake `cc7714d2`（含 CM-1206 修復）→ build_image → push Harbor 並 API 端複驗。

**.4 eventlet × Nuitka 驗證**（CM-1193）：本卡目的＝驗證 eventlet monkey_patch 在編譯產物下是否存活。過程中揪出 **.1c 模式裁剪誤刪 socketio namespace 註冊**的真回歸（healthz 級探針測不到的層）→ 修法＝namespace 抽離至集中清單 `config/socketio_namespaces.py`（比照 REGISTERED_APPS）統一註冊；Flask-SocketIO 5.3.6→5.6.1 升版根治 session 問題（留「不可退版」警語）。產物探針 4/4 PASS——**eventlet × Nuitka 命題閉環，D1 保底方案（換 gevent）不需啟用**。

**場外 CM-1206**：user 實測 .4 時現形的既存獨立 bug（2026-07-07 起，先前被「連不上」蓋住）——socket 路徑無 auth context 導致共編自動存檔 AttributeError。修法：connect 驗 JWT 建 auth context、FR-048 守門不降級、錯誤不再靜默（handler emit 錯誤事件＋log）、fail-closed 全路徑。BE `6de7ffd8`＋FE `967e67b`。

## 4. 行為差異對照（Before → After)

| 面向 | 舊 | 新 |
|---|---|---|
| 啟動方式 | `main_app.py`（8000）／`main_socketio.py`（8002）兩支入口 | **單一 `python main.py`**＝API（8000，日常唯一指令）；`RUN_MODE=socketio python main.py`＝通知服務（8002）。api 模式 `DEBUG=true` 走 Flask dev server、否則內嵌 gunicorn |
| Scheduler | 兩入口各起一份（雙跑） | 只有 api 模式起（4 jobs）；socketio 模式 0 份，log 明示 disabled |
| Config | 六套環境 class（Development/Staging/…×2 雲別） | **一套 class＋`ENV` 降級為顯示標籤**（新慣例四值 DEV/STG/POC/PRD；變數本身不退，保護既有部署腳本）；`config_loader` 啟動期集中驗證，缺項一次列全 |
| Secret env | `DB_SECRET`/`JWT_SECRET`/`REDIS_SECRET` JSON 包裝 | **平鋪 key**（向後相容舊 JSON 填法）；TURNSTILE 同步平鋪化 |
| 營運參數 | 散在 config class 常數 | 遷 DB `system_config`（migration seed 7 列、冪等）；JWT 效期改 DB 後需重啟、登入政策/MFA 即時生效 |
| socketio namespace | 藏在各 module `create_module()` 內註冊 | 集中清單 `config/socketio_namespaces.py`（封閉集合 2 個：notification／fill-survey），app_factory 統一註冊 |
| 交付形態 | 原始碼部署 | Nuitka standalone binary in Docker image（業務 `.py`＝0；公開第三方套件原樣附帶） |
| 退役物 | — | `main_app.py`／`main_socketio.py` 刪除；六套環境 class 刪除；死 env 項清 6 支（REDIS_DB、通知四項、DRIVE_WEBHOOK_PUBLIC_BASE_URL）＋SQLALCHEMY_*/APISPEC_*/TOKEN_TTL/WEBSITE_URL 死定義 |
| 依賴管理 | poetry.lock 不入版控（歷史誤排除） | **poetry.lock 入版控**（build 機 lock 不同步事故後拍板） |

## 5. 關鍵決策軌跡

1. **Nuitka 取代 .pyc**：Python 3.11 反編譯工具鏈已斷、純 .pyc 已有基礎保護，但 user 拍板最高保護等級 → standalone 機器碼，對齊 Go binary 交付形態。
2. **D2 私房 wheel**：dependency-injector 官方 wheel 走 Limited API（abi3）強制 PEP 489 多相初始化，與 Nuitka 靜態嵌入不相容（import 期 SystemError）→ 私房 cp311 rebuild 納管線前置（版本感知快取＋sdist 留檔）；abi3 觀察名單（cryptography/gevent/nacl）整包編譯未觸發，續觀察。
3. **D9 統一入口（user 提議）**：兩支入口各編一次 → 單 binary `RUN_MODE` 切模式，build ×2 變 ×1，順帶解 scheduler 雙跑。
4. **D11 config 三層重整（user 提問觸發）**：binary 化後「改 config＝重編譯交付」，設定放錯層代價放大 → class 常數／env／DB system_config 三層各歸其位。
5. **.2d 公開套件不需編譯保護（user 拍板）**：保護目標是業務碼與 jedi-*，pypi 公開套件編了只換到編譯時間 → 排除後冷 build 省 78%，且 Nuitka 常數表連坐圈縮小。
6. **.4 eventlet 命題閉環**：編譯產物下 monkey_patch＋socketio.run 正常，D1 保底（換 gevent）封存不啟用。
7. **Flask-SocketIO 5.6.1 不可退版**：5.3.6 session 問題以升版根治（取代 manage_session workaround），app_factory 留警語＋症狀特徵防回退。
8. **CM-1206 socket auth**：socket 路徑補 JWT auth context、FR-048 守門不降級、fail-closed——即時協作路徑首次納入正規授權體系。

## 6. 教訓彙整

- **「服務有回應 ≠ 你要的版本在跑」**：commit bake 的 version 端點是唯一身分證（同日兩例：runner 撞 root 舊進程佔 8002；首腦在 188 跑未 push 的 `--dry-run` 被舊版無視、誤觸真編譯）。跑新功能前先驗目標機 code 版本。
- **靜默失效是本案主敵**：三個複製 bug 全部「回報成功但成品是壞的」；防線＝把人工清點做進腳本斷言＋功能級探針（不只 import 級／healthz 級——.4 namespace 回歸正是 healthz 測不到的層）。
- **驗證順序金字塔**：`bash -n` 0.01s → dry-run 0.3s → 小探針 10s → 全量 19m，先快後慢（runner 名言：「拿最慢的迴圈去測最快能測的東西」）。
- **Nuitka 常數表連坐**：模組集合一變全量重編；排除清單越大連坐圈越小（12m11s 的由來）。
- **env 管理三坑同日爆**：sample 歷史欠帳 18 支／188 設定藏 .bashrc 隱形／LICENSE_ACTIVATION 隱式預設剛好能動——解法＝單一真相來源＋顯式化。審 env 死活必須連 jedi 套件一起 grep（曾差點誤殺 4 支 jedi 在讀的 key）。
- **http2 誤診（Block 14 更正）**：runner 曾記「nginx http2 擋 WebSocket」，實為 namespace 未註冊（.1c 回歸）與 http2 假設兩問題疊置的誤診；修好前者後 user 實證 h2 200 正常。**教訓：runner 的「獨立問題發現」也要驗因果——修好主因後回頭重測再下結論**；nginx 不需任何 http2 特殊處置。
- **起遠端長任務前先 `pgrep` 查同類進程**：兩份 build 互撞的 linker crash（undefined reference）像 code 問題，實為共用 nuitka-out 汙染。
- **runner 的「環境不可行」結論也要親驗**：一條 `ssh -T` 戳破「188 無 gitlab 金鑰」誤判，誤信就多養一條 rsync 歧路（user 同日立 memory：部署機程式碼一律 git pull）。
- **覆蓋率統計是便宜守門**：i18n `--statistics` 一行參數，首輪即抓到潛伏死條目。
- **stash 驗既存性禁整樹還原**：.1g runner `checkout-index -f -a` 抹掉跨棒未 commit 檔案（STATE/LOG＋pyproject override）——恢復只准 `stash pop`；交接檔寫完盡快 commit。

## 7. 已知 follow-up

| 項目 | 說明 |
|---|---|
| **FR-064 防竄改（CM-1188）** | 決策者親做中，非派工項 |
| **CM-1207（FR-065.0 DB 初始化基線）** | 卡已開、盤點棒 prompt 已交 user（唯讀可先行）；實作棒等 FR-064 完成後派 |
| **FR-065 Installer（CM-1189）** | 前置＝FR-064＋CM-1207；FE image／產品 compose 明確劃入此案（compose 形狀由 installer 決定，不提早做） |
| CM-1204（手冊下載死碼端點） | 場外待排程，不掛任何 FR |
| STG 188 舊產物去留 | 8000/8002 兩服務跑的是舊產物 `36708b35`（非最終版），去留／換新 image 由決策者定 |
| Swagger 待補 | `docs.init_app` 從未啟用（本案盤查發現，APISPEC_* 死項已清）；復原時掛 DEBUG 開關 |
| analysis 文件 | 「公開套件不需編譯保護」推理（已允諾 user）＋常數表連坐與驗證金字塔可併寫 |

## 8. 部署 Handover

- **Image 拉法**：Harbor `192.168.50.171`（＝`harbor.jedicogy.com.tw:8081`，188 `/etc/hosts` 有映射），專案 `guidant-ai`：`docker pull …/guidant-ai/guidant-ai-be:1.14.0`（＝`latest`，digest `sha256:bba0449d…`）。起服務：`docker run … guidant-ai-be:1.14.0`，`RUN_MODE=api`→8000／`RUN_MODE=socketio`→8002，單 image 雙模式。
- **環境變數清單**：`docs/features/FR-063-2608-nuitka-packaging/deployment-env.md`（必填/一般/功能模組＋jedi-* 16 項＋volume 掛載表＋交付前 checklist）。STG/POC 上版另見 `config-migration-mapping.md` §6 的 13 項 checklist（含 CORS 補實際站台網址）。
- **Build 管線三支腳本**（build 機 188 `/opt/guidant_ai`，程式碼同步一律 git pull）：
  1. `build_release.sh` — wheel rebuild（含 dependency-injector 私房 wheel）＋Nuitka 編譯，穩態 ~12m；dry-run 預檢＋完整性斷言＋i18n `--statistics`
  2. `build_image.sh` — 產 production image（Dockerfile.production）
  3. `probe_container.sh` — 七項探針健檢（design §5.6）
- **主機層依賴**：image 內建 fc-match 斷言確保 `Noto Sans CJK TC`；非 root uid1000 執行。
