jim800121chen 1c53253e6e feat(worker): onnx 階段自動移除尾端 Softmax + pre-check regex 收窄
問題:同一顆含 Softmax 的 tflite,正式站能編出 KL520 真 .nef,
本專案 worker 在 nef 階段撞 UnimplementedFeature [Softmax] exit 6。
根因:runtime ktc 的 eliminate_tail 是 no-op(閹割版、只印警告),
尾端 Softmax 未被移除;正式站產物實測為 logits 輸出(Softmax 已砍)。

修法(staging 實測驗證、與正式站產物權重段逐 byte 相同):
- onnx/core.py:新增 remove_tail_softmax(),onnx2onnx_flow 後對所有
  platform 移除 terminal Softmax(cut_nodes、不用 cut_types 避免誤砍
  中間層下游)、多 output rewire 逐項驗證 fail-loud、onnx.checker、
  removed_tail_softmax metadata
- precheck.py:R3 誤擋收窄——刪 hw_not_support_col 欄位名誤命中、
  op capture 改必須、抽不到具體 op 名改放行+warning(真 fail-open)、
  刪自由文字猜 op fallback、加噪音 token 過濾
- tests:+16(29 passed;terminal/no-op/多 output/fail-open 全覆蓋)
- docs:TDD §12、design-doc ADR-012、PRD 輸出語意變更(機率→logits)

行為變更:所有 platform NEF 輸出改 logits(與正式站一致)、
分類後處理需呼叫端自行補 softmax。

Review:兩輪(0C/3M→0C/0M);W3 docker integration 6/6 PASS
(520+Softmax e2e 產真 .nef 799,844 bytes、720 回歸、precheck 放行)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 05:54:13 +08:00

387 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# TDD 索引 — Kneron Model Converter 對外 API
## 作者Architect Agent
## 狀態DraftPhase 0.8b 重寫 + 模組化)
## 最後更新2026-05-16
> **auth 設計演進**:本 TDD 反映 Phase 0.8b 拍板後的「目標狀態」。完整歷史見 visionA repo `docs/autoflow/04-architecture/adr/adr-015-server-to-server-api-key.md` v2.1 + `adr-016-download-via-converter.md` v1.0。
>
> **配套**`design-doc.md`(架構決策)、`../02-prd/PRD.md`(需求)、`../03-design/design-review.md`UX 回饋)。
---
## 變更歷程
| 日期 | 變更 | 作者 |
|------|------|------|
| 2026-04-25 | 初版 Draft 1.0OAuth resource server + promote | Architect Agent |
| 2026-04-25 | Multipart 上傳路徑改 visionA → converter 直傳;移除 FAA GET/HEAD | Architect Agent |
| 2026-05-16 | **Phase 0.8b 重寫**visionA → converter 改 API key新增 `/result` endpointOAuth resource server 章節砍除;模組化拆分為索引 + 子檔案 | Architect Agent |
| 2026-07-06 | **Worker 行為變更M 級)**onnx worker 對**所有 platform** 一律移除尾端 Softmax、輸出 logits與正式站對齊、修 520 batch_compile exit 6pre-check regex 收窄。新增 §12 | Architect Agent |
---
## 1. 文件結構
本 TDD 在 Phase 0.8b 重寫時拆分為模組化結構:
| 檔案 | 內容 | 目標讀者 |
|------|------|---------|
| `TDD.md`(本檔,索引)| 各章節摘要 + 子檔案連結 | 全部 |
| `auth.md` | API key middleware 設計 + 砍除 OAuth resource server 清單 + 保留 OAuth client | Backend |
| `api/api-jobs.md` | `POST/GET /jobs``GET /jobs/:id` 規格 | Backend、Reviewer、Testing |
| `api/api-promote.md` | `POST /jobs/:id/promote` 規格 | Backend、Reviewer、Testing |
| `api/api-result.md` | **新增** `GET /jobs/:id/result` 規格 | Backend、Reviewer、Testing |
| `database.md` | Redis schema + 索引 + Lua script | Backend |
| `infra.md` | Nginx / docker-compose / .env 變動 + 部署順序 | Backend、DevOps |
| `performance.md` | SLO + 延遲預算 + 負載測試 | Backend、Testing |
| `observability.md` | Log 格式 + 敏感資料保護 + 告警 | Backend |
| `security.md` | Trust boundary + Input validation + Auth security | 全部 |
| `design-doc.md` | 架構決策 + ADR | 全部 |
| 本檔 §12 | **Worker 轉檔管線行為變更**(尾端 Softmax 移除 + pre-check regex 收窄2026-07-06 M 級)| Backend、Reviewer、Testing |
---
## 2. Phase 0.8b 改動摘要
### 2.1 對外 auth 改 API key
-`auth/middleware.js`OAuth resource server+ `auth/jwks.js`
-`auth/apiKeyMiddleware.js`
- 4 個既有 endpoint 改掛 `requireApiKey()`
- 新加 `/result` endpoint 也用 `requireApiKey()`
詳見 `auth.md` §1 + §3砍除清單
### 2.2 新增 `/result` endpoint
- `GET /api/v1/jobs/:id/result`
- Streaming proxy NEF from MinIO → caller
- 4 種 4xx + 2 種 5xx 情境
- 雙路徑 NEF key 解析(新格式 + 舊格式向後相容)
- **2026-05-17 補充**rate limit60 req/min獨立 bucket、Range header 防護silently ignore、audit log 8 個 action、Backend `source_filename` 寫入 acceptance criteria§9-§14
詳見 `api/api-result.md`
### 2.3 保留不動
- Promote 流程converter → FAA 仍走 OAuth client_credentials
- Redis schema除確認 `source_filename` 欄位存在)
- Worker、MinIO bucket、Nginx 結構
詳見 `auth.md` §2 + `api/api-promote.md`
### 2.4 Config 變動
- 移除:`MEMBER_CENTER_ISSUER` / `MEMBER_CENTER_JWKS_URL` / `KNERON_CONVERTER_AUDIENCE` / `JWKS_*` / `JWT_CLOCK_TOLERANCE_SEC`
- 新增:`CONVERTER_API_KEY`
- 保留:`MEMBER_CENTER_TOKEN_URL` / `KNERON_CONVERTER_CLIENT_*` / `FILE_ACCESS_AGENT_*` / `OAUTH_*`
詳見 `infra.md` §3。
---
## 3. 系統概述
### 3.1 角色
- **Converter本專案**Node.js Task Scheduler + Python Worker
- **visionA-backend**Go 服務Converter 對外 API 的**唯一** caller
- **Member CenterMC**OAuth authorization server — Phase 0.8b 後**只**給 Converter → FAA promote 用
- **File Access AgentFAA**NAS 邊界檔案閘道single-tenant per instance
### 3.2 API 端點清單Phase 0.8b 後)
| 方法 | 路徑 | Auth | 說明 | 規格 |
|------|------|------|------|------|
| GET | `/health` | — | 健康檢查 | `api/api-jobs.md` §3 |
| POST | `/api/v1/jobs` | API key | 建立 job | `api/api-jobs.md` §4 |
| GET | `/api/v1/jobs` | API key | 列表 / Recovery | `api/api-jobs.md` §6 |
| GET | `/api/v1/jobs/:id` | API key | 單一 job 狀態 | `api/api-jobs.md` §5 |
| POST | `/api/v1/jobs/:id/promote` | API key | 搬檔到 FAA | `api/api-promote.md` |
| GET | `/api/v1/jobs/:id/result` | API key | **NEW** stream NEF | `api/api-result.md` |
| POST | `/api/v1/jobs/:id/download-tokens` | API key | Phase 2回 501 | `api/api-jobs.md` §7 |
| DELETE | `/api/v1/jobs/:id` | API key | Phase 2回 501 | `api/api-jobs.md` §7 |
### 3.3 既有路徑Phase 0.8b 不動)
| 方法 | 路徑 | 用途 |
|------|------|------|
| POST | `/jobs` (multipart) | Web UI 既有上傳 |
| GET | `/jobs/:id` | Web UI 狀態查詢 |
| GET | `/jobs/:id/events` (SSE) | Web UI 進度 push |
| GET | `/jobs/:id/download/:filename` | Web UI 下載 |
| GET | `/queues/stats` | 內部監控 |
這些走 internal vhost不對外、不加 auth。
---
## 4. 技術堆疊(不變)
| 層級 | 選擇 |
|------|------|
| 後端框架 | Node.js 18 + Express 4 |
| 認證(對外)| API key`crypto.timingSafeEqual`Phase 0.8b 新)|
| 認證promote| OAuth client_credentialsjose / 自寫 fetch|
| 資料庫 | Redis 7 |
| 物件儲存 | MinIOConverter Bucket |
| Worker | Python 3.10+ |
| 反向代理 | Nginx |
| 測試 | Jest |
詳見 `design-doc.md` §3.5。
---
## 5. 專案結構
```
apps/task-scheduler/
├── server.js ← Entry
├── src/
│ ├── config.js ← 集中讀 envPhase 0.8b 改)
│ ├── redis.js ← Redis client
│ ├── auth/
│ │ ├── apiKeyMiddleware.js ← 【新】Phase 0.8b
│ │ ├── oauthClient.js ← 【保留】promote 用
│ │ ├── middleware.js ← 【砍】OAuth resource server
│ │ └── jwks.js ← 【砍】
│ ├── fileAccessAgent/
│ │ ├── client.js ← FAA HTTP client保留
│ │ └── errors.js ← 錯誤翻譯(保留)
│ ├── routes/
│ │ ├── legacy.js ← 既有 /jobs/* 路由
│ │ └── v1/
│ │ ├── index.js ← v1 router 組裝(要改 wire result + 換 auth middleware
│ │ ├── jobs.js ← POST/GET要改換 requireApiKey
│ │ ├── promote.js ← POST promote要改換 requireApiKey
│ │ └── result.js ← 【新】Phase 0.8b
│ ├── services/
│ │ ├── jobService.js ← Job CRUD
│ │ └── doneListener.js ← Worker done event
│ ├── middleware/
│ │ ├── errorHandler.js ← 統一錯誤
│ │ └── requestId.js
│ └── utils/
│ └── logger.js
├── docs/openapi.yaml ← 要改 security schemeOAuth → bearer/api_key
├── .env.example ← 要改(見 infra.md §4
├── README.md ← 要改 auth 章節
└── package.json
```
---
## 6. 實作任務拆分(給 Backend
按 Autoflow 增量式開發規範,每個任務 = 一個可獨立 review 的單位。
### Phase A — API key middleware + auth 切換(取代 OAuth
| # | 任務 | 依賴 | 預估 | 驗收標準 |
|---|------|------|------|---------|
| A1 | 新建 `src/auth/apiKeyMiddleware.js` | — | 1d | unit test 全過happy path、missing header、wrong key、constant-time、destroy socket、env 未設定 fail-fast |
| A2 | 修 `src/config.js`:新增 `converter.apiKey`、移除 OAuth resource server 相關 env、保留 promote 相關 | — | 0.5d | config.test.js 過;啟動時 `CONVERTER_API_KEY` 未設只 warn不 throwOAuth resource server env 移除後 server 仍能啟動 |
| A3 | 修 `src/routes/v1/index.js` / `jobs.js` / `promote.js``requireAuth(scope)``requireApiKey()` | A1, A2 | 0.5d | 既有 integration test 全過401 行為改成 API key 模式驗server 啟動正常 |
| A4 | 砍 `src/auth/middleware.js` + `src/auth/jwks.js` + 相關 test | A3 | 0.5d | `git rm` + test runner 沒 broken importsearch code base 沒有 reference 殘留 |
| A5 | 修 `.env.example``docs/openapi.yaml``README.md`:移 OAuth resource server 段、加 `CONVERTER_API_KEY` | A4 | 0.5d | docs lint 過OpenAPI security scheme 改 bearer / api_key |
| A6 | Integration testAPI key 驗證 4 個情境happy / missing / wrong / 503 | A1-A5 | 1d | 全部過;既有 jobs / promote integration test 仍過 |
**Phase A 總工時**~4d
### Phase B — `/result` endpoint
| # | 任務 | 依賴 | 預估 | 驗收標準 |
|---|------|------|------|---------|
| B1 | 確認 `jobService.createJob` 寫入 `source_filename` 欄位(檢查既有 code、補上若缺| — | 0.5d | unit test 過;既有 job record 結構不破壞 |
| B2 | 新建 `src/routes/v1/result.js`(含 `extractNefObjectKey``buildFilename`、stream handler| B1, A1 | 1.5d | unit test 過filename 各情境、雙路徑 key 解析、stream error / client close handling |
| B3 | Wire `/result``src/routes/v1/index.js`(含 `requireApiKey` + per-client rate limiter| B2 | 0.5d | server 啟動 + route table 正確mergeParams 取 :id 通 |
| B4 | Integration test`/result` 8 個情境200 happy / 401 / 404 job / 404 result / 409 / 410 expired / 410 minio miss / 502| B2, B3 | 1d | 全部過 |
**Phase B 總工時**~3.5d
### 任務排程建議
**順序執行 A → B**Backend 單人):
- A1 + A2 可平行
- A3 等 A1 + A2
- A4 等 A3
- A5 等 A4
- A6 等 A5整體 verify
- B1 + B2 可平行B1 簡單B2 是主要工作)
- B3 等 B2
- B4 等 B3
預估總工時:~7.5 工作日單人。若可雙人並行A 和 B 可分工,壓到 ~5d。
### 與 visionA 端的 dependency
| Backend 任務狀態 | visionA 端可以做什麼 |
|---------------|------------------|
| Phase A 完成、deploy stage | visionA 可以打 stage converter 的既有 endpoint 驗 API key 流程 |
| Phase B 完成、deploy stage | visionA 可以打 `/result` endpoint 驗 streaming |
| Phase A + B 都 deploy 完 | e2e 驗證visionA repo commit 9e29ebf 已 ready |
---
## 7. 測試策略
詳見 `performance.md` §7 + 各 `api/*.md` 的 test 章節。
### 7.1 Unit test 覆蓋率目標
- `apiKeyMiddleware`100%(少量 code、必須全 cover
- `result.js`90%
- 既有 OAuth-related 改動:維持 ≥ 85%
### 7.2 Integration test 必跑
- API key 4 情境happy / missing / wrong / 503
- 既有 jobs / promote 在 API key 模式下仍過
- `/result` 8 情境(見 `api/api-result.md` §7.1
### 7.3 Manual stage e2e部署後
- curl 驗:`/health``POST /jobs``GET /jobs/:id``POST /promote``GET /result`
- visionA 端 e2e完整 upload → poll → promote → download
---
## 8. 安全注意事項
詳見 `security.md`。重點:
- `CONVERTER_API_KEY` 不進 git / log / Slack
- `constant-time compare`(防 timing attack
- Sec C1 暫緩(`.env` history rewrite + secret rotation 在 Phase 1 ready 後做、含 CONVERTER_API_KEY
- Trust boundaryvisionA 一旦被 compromise 可冒充任意 user_id接受、與 OAuth 模型一致)
---
## 9. 風險與待確認
| # | 風險 | 影響 | 行動 |
|---|------|------|------|
| R1 | CONVERTER_API_KEY rotation 流程未自動化 | 低 | Phase 1 接受手動 |
| R2 | `/result` 高並發 stream 壓力 | 低 | NEF 通常小、visionA 是唯一 caller、QPS 可控 |
| R3 | Sec C1 暫緩(.env 進 git history | 中 | Phase 1 ready 收尾後 rewrite |
| R4 | NEF 7 天過期後 client 重新轉檔 | 低 | API spec 已定義 410visionA 端處理 |
| R5 | Phase 0.8b 部署期間「OAuth → API key」短暫不可用 | 低 | 既有 stage OAuth 從未跑通、不會有 regression |
---
## 10. 後續步驟
1. 本 TDD 索引 + 子檔案送 PM / Design 三方互審
2. 使用者審核
3. Backend Agent 依 §6 的任務拆分增量開發
4. Reviewer 每個任務把關
5. Testing 整合測試 + e2e
6. DevOps 部署converter 先 + visionA 後)
---
## 11. 變更記錄
| 日期 | 版本 | 變更 | 作者 |
|------|------|------|------|
| 2026-04-25 | Draft 1.0 | 初版Phase 1 完整規格(單檔 1390 行) | Architect Agent |
| 2026-04-25 | Draft 1.1 | Multipart 上傳路徑改 | Architect Agent |
| 2026-05-16 | Draft 2.0 | **Phase 0.8b 重寫**API key + /result + 模組化拆分為索引 + 8 個子檔案 | Architect Agent |
---
## 12. Worker 轉檔管線行為變更2026-07-06M 級)
> **範圍界定**:本 TDD 前 11 章聚焦 Task Scheduler 對外 API / auth 層Phase 0.8b)。本章新增,記錄 **Python Worker 轉檔管線** 的一個行為變更 —— 與 API / auth 無關,但同屬本 repo 的架構決策、故收在同一份 TDD 索引,方便 Backend / Reviewer / Testing 讀。詳細實作插入點見 §12.2。
### 12.1 尾端 Softmax 自動移除(輸出 logits
#### 為什麼(背景)
- **問題**:含尾端 Softmax 的分類模型(如 MobileNet classifier**KL520** 時,`batch_compile -T 520``UnimplementedFeature: undefined CPU op [Softmax]`、**exit 6** 失敗。
- **根因**runtime image`/app/ktc/onnx_optimizer.py`kneronnxopt 版)的 `eliminate_tail` 是**閹割版 no-op**container 內印 `WRANING: eliminate_tail is not available in current conda environment`),所以即使 `onnx2onnx_flow(eliminate_tail=True)` 也砍不掉尾端 Softmax、Softmax 一路帶進 batch_compile 撞 520 不支援的 CPU op。
- **對齊正式站**:正式站 `converter.innovedus.com`(另一套 code base對**同一顆** fixture 能編出真 520 .nef。staging 實測比對正式站 .nefoutput = `(1,3,1,1)` 定點量化 logits、**無 Softmax**,且權重段(`wt` 0xc0530與我們砍掉 Softmax 後產的 .nef **逐 byte 相同**、compiler 版本字串完全相同(`v0.9.1(6d7a863)`)→ 證明正式站就是「模型階段移除 Softmax、logits 落 host 後處理」,這是 Kneron 對 520 的標準做法。
#### 決策(拍板)
**onnx 階段(`onnx2onnx_flow` 之後、`onnx.save` 之前)對所有 platform 一律移除尾端 Softmax、輸出 logits與正式站行為對齊。**
- **不限 520**:雖然只有 520 會因 Softmax exit 6但為了「所有 platform 輸出一致 = logits」、與正式站對齊、避免 platform 分歧造成呼叫端困惑,決定**所有 platform 統一移除**。(若日後有「某 platform 需保留 Softmax」的需求再走 platform 分支,屆時另開 ADR。
- **觸發條件實作對齊2026-07-06 backend 實測修訂)****只移除 terminalgraph 尾端、無下游節點)的 Softmax中間層的 Softmax 不移除**。理由見下方「怎麼做」的 `cut_nodes` 說明——`cut_types` 是 cut-from-node 語意,遇到中間層 Softmax 會把整段下游靜默切掉故改用「terminal-only + `cut_nodes`」精準移除。移除後 logits 自動接成新 graph output。
#### 怎麼做實作方式2026-07-06 backend 實測修訂)
- **插入點**`services/workers/onnx/core.py:36-37` 之間(`onnx2onnx_flow(...)` 之後、`onnx.save(model, output_path)` 之前)。
- **呼叫方式backend 實作 + staging 實測可行)**
```python
# 注意runtime 的 ktc.onnx_optimizer 沒有 editor APIremove_nodes_with_types 等)
# 必須改呼叫底層 tools.other.remove_nodes
import sys
sys.path.insert(0, "libs/ONNX_Convertor/optimizer_scripts") # 依 container 掛載點調整
from tools import other
# 只挑「terminal無下游節點且 op_type == Softmax」的節點名用 cut_nodes 精準移除
terminal_softmax = [
n.name for n in model.graph.node
if n.op_type == "Softmax" and _is_terminal(n, model.graph)
]
if terminal_softmax:
other.remove_nodes(model.graph, cut_nodes=terminal_softmax)
```
`_is_terminal` 判斷該節點的 output 不是任何其他節點的 input即 graph 尾端;實際 helper 命名以 backend 實作為準。)
- **不要用** `cut_types=["Softmax"]`(原 sketch 寫法,已棄用):`cut_types` 是 cut-from-node 語意,遇到**中間層** Softmax 會把其整段下游節點**靜默切掉**(危險)。改用 `cut_nodes`(明確指定要移除的節點名)+ 只挑 terminal Softmax。對已驗證情境fixture、尾端 Softmax兩者行為**逐 byte 等價**,但 `cut_nodes` + terminal-only 在有中間層 Softmax 的模型上才安全。
- **不要用**`ktc.onnx_optimizer.remove_nodes_with_types(...)` —— runtime image 的 `ktc.onnx_optimizer`kneronnxopt 版)**沒有任何 editor API**`ktc/onnx_optimizer_1_7.py` 雖存在但不可 importhardcode `/workspace` 路徑 + 依賴 container 沒有的 `onnx.optimizer`)。
- **不要依賴** `eliminate_tail=True` 做尾端清理runtime 是 no-op
- job metadata 應標注「Softmax 已移除、需 host 端後處理」提醒呼叫端。
#### 行為影響(重要,必須傳達給呼叫端)
- **所有 platform 的 NEF 輸出從「含 Softmax機率」變「logits」**。
- 呼叫端visionA-backend若對轉檔結果做分類後處理取 argmax 通常不受影響、但取「機率值」會受影響),**需自行在 host 端補 softmax**。
- **⚠️ 需通知 visionA 團隊**:這是對外可觀察的輸出語意變更,即使 argmax 分類結果不變、機率數值會變。列為交付前跨團隊溝通項。
#### 證據連結個人層per-branch
- 實驗規劃bug 反推、只讀 code`.autoflow/05-implementation/tflite-520-experiment-plan-2026-07-06.md`
- staging 實測驗證(砍 Softmax → 520 產真 .nef、與正式站逐 byte 比對):`.autoflow/06-testing/reports/tflite-520-softmax-removal-verify-2026-07-06.md`H1/H2/H3 全綠、含正式站 .nef 比對閉環、backend 實作注意事項)
### 12.2 pre-check regex 收窄(設計修訂)
#### 原設計 → 修訂
`services/backends/precheck.py` 的「不支援 op 早期失敗」pre-check原設計是「掃到 marker 字樣regex 命中)就擋」,實測發現會**誤擋**——`hw_not_support_col` 是**欄位名**、被 `hw_not_support` regex 誤命中,導致能編出真 .nef 的乾淨模型也可能被擋。
**修訂為「訊號 + 具體 op 名稱才擋,抽不到 op 名稱時放行 + warningfail-open**
| 項目 | 原設計 | 修訂後 |
|------|--------|--------|
| `hw_not_support_col`(欄位名誤命中) | 會命中、造成誤擋 | **刪除 / 大幅收窄**該 regex |
| `hw_not_support` 的 op capture group | optional抽不到 op 名也擋) | **改必須**capture group 必抽到具體 op 名才算命中) |
| 抽不到具體 op 名稱時 | 擋fail-closed | **放行 + warning**(真 fail-open |
#### 為什麼 fail-open
pre-check 的定位是「早期快速失敗、省 batch_compile 時間」的**優化**,不是**授權邊界**。誤擋(把能轉的擋掉)比漏擋(放行後 batch_compile 自己 fail代價高。抽不到具體 op 名稱 = 訊號不明確 = 寧可放行讓後段真正的 batch_compile 判定。
#### 證據連結
- rootcause 報告 §4apre-check 誤擋分析):見 `.autoflow/06-testing/reports/tflite-520-softmax-removal-verify-2026-07-06.md` §5R3 誤擋風險實測數據點)+ 舊 rootcause 報告 §4a
- **同 PR 收窄**:本 regex 修訂與 §12.1 的 Softmax 移除**同一個 PR** 進避免「Softmax 砍了但 pre-check 還誤擋」的半套狀態。
### 12.3 實作任務(給 Backend
| # | 任務 | 檔案 | 驗收標準 |
|---|------|------|---------|
| W1 | onnx worker 加「移除尾端 Softmax」步驟 | `services/workers/onnx/core.py:36-37` | 用 `tools.other.remove_nodes(graph, cut_nodes=<terminal Softmax 節點名清單>)`**只移除 terminal Softmax、中間層不動**;不可用 `cut_types=["Softmax"]`,會誤砍中間層下游);砍後模型過 `onnx.checker`520 e2e 不再 exit 6、產真 .nefjob metadata 標注 Softmax 已移除 |
| W2 | pre-check regex 收窄 | `services/backends/precheck.py` | 刪 / 收窄 `hw_not_support_col``hw_not_support` op capture group 改必須;抽不到 op 名改放行 + warning既有測試不 broken |
| W3 | 回歸驗證 | — | 720/530/630/730 既有可轉模型仍能轉(輸出改 logits 但 .nef 有效);含 Softmax 的 520 模型能轉成 |
> W1 + W2 **同 PR**。W3 交 Testing 做回歸Prove-It先寫「520+Softmax 應轉成」的 failing test → W1 修 → 轉綠)。
---
**附註**:本 TDD 從 1390 行單檔重組為 ~180 行索引 + 8 個子檔案。每個子檔案 < 500 單一職責可獨立給 Backend / Reviewer / Testing 不同角色讀對應檔案減少 context 負擔。§122026-07-06 新增 worker 轉檔管線行為變更 API/auth 層獨立