Plan Review — bsmart-system
sab-branch transplant onto main
計劃評審 — bsmart-system
從 sab-branch 移植到 main
01Summary摘要
sab-branch is not a branch — it is an alternative timeline. It diverged from main 208 commits ago and rebuilt the commission engine, client model, claim workflow, and added two new modules (CRICOS registry + partner portal). A direct merge would silently delete six months of main-branch work. The plan’s core insight: treat the transplant as a cherry-pick replay, not a merge — and slice destructive changes into atomic backend+frontend co-commits so finance dashboards can’t regress mid-deploy.
sab-branch 不是一條普通分支 — 它是一條平行時間線。從 main 分岔出去後又走了 208 個 commit,重寫了佣金引擎、客戶模型、申領流程,並新增兩個模組(CRICOS 公開資料註冊 + 仲介自助門戶)。直接 merge 會悄悄洗掉 main 過去半年的工作。本計劃的核心洞見:把這次移植當作 cherry-pick replay,而不是 merge — 並把破壞性變更切成「後端 + 前端同 commit」的原子小包,避免財務 dashboard 在部署中途破掉。
Scope (verified against main HEAD): 90 files affected on sab-branch, ~+25,232 / −1,428 lines, spanning 12 product modules. The plan organises these into 18 Multica subtasks (TRA-32 → TRA-50, two new gates added after QA review), with 10 sub-PR splits inside three high-blast tasks and 12+ explicit production-safety gate items in the data-migration and security paths.
範圍(已對 main HEAD 實測):sab-branch 影響 90 個檔案,~+25,232 / −1,428 行,跨 12 個產品模組。本計劃把這些工作組織成 18 個 Multica 子任務(TRA-32 → TRA-50,QA 審查後新增兩個 gate),其中 3 個高風險任務再拆成 10 個 sub-PR,加上 12+ 條明確的生產安全 gate 散佈在資料遷移與安全路徑上。
02Impact dashboard影響評估儀表板
Plan completeness indicators計劃完整度指標
test_commission_semesters.py 10× expansion. Covered.test_commission_semesters.py 10× 擴充。已覆蓋。CLAUDE.md, MULTI_TENANT_SETUP.md, or API docs updates. Port required, not on plan.CLAUDE.md、MULTI_TENANT_SETUP.md 或 API 文件更新。需要移植但不在計劃內。03Current architecture現況架構 (main HEAD)(main HEAD)
A · CURRENT — main branch HEADA · 現況 — main 分支 HEAD
Ctrl/Cmd + wheel to zoom · drag to pan · double-click to fit · ⛶ opens full sizeCtrl/Cmd + 滾輪縮放 · 拖曳平移 · 雙擊回到 fit · ⛶ 開啟全頁
04Planned architecture計劃架構 (after all 18 subtasks)(18 個子任務全部完成後)
B · PLANNED — after sab cherry-pick replayB · 計劃 — sab cherry-pick replay 完成後
Same node names & layout direction as A — visual diff is the new emerald nodes.與 A 相同 node 名稱與佈局方向 — 視覺差異即新增的 emerald node。
05Change-by-change breakdown變更細節
origin/sab-branchis208commits behindorigin/main(verified).- Direct merge would silently delete those 208 commits (621 files diff, large deletions).
- No reconciliation branch, no replay manifest, no sequencing decision.
origin/sab-branch落後origin/main208個 commit(已驗證)。- 直接 merge 會悄悄刪掉那 208 個 commit(diff 621 個檔,大量刪除)。
- 沒有 reconciliation 分支、沒 replay manifest、沒排序決策紀錄。
- Create
reconcile/sab-replayfrom freshorigin/main. - Dry-run cherry-pick of anchor commit
c3ea8e5dfirst. - Build
REPLAY-MANIFEST.mdlabelling every commit asreplay | skip | mediate | drop. - Hold all destructive gates until manifest is signed.
- 從 fresh
origin/main建立reconcile/sab-replay。 - 先做 anchor commit
c3ea8e5d的 dry-run cherry-pick。 - 建立
REPLAY-MANIFEST.md,給每個 commit 標replay | skip | mediate | drop。 - 所有破壞性 gate 在 manifest 簽核前都凍結。
- No
kb_cricos_*,commission_school_receipts,commission_invoice_items, orpartner_portal_userstables. commission_tracking.overall_statusread in11+places incommissions.pyalone (verified at lines 26, 115, 273, 309, 310, 336, 377, 415, 422, 450).schemas/client.py(singular) is 6.5 KB; nois_company;enrolmentstable is thin.- Alembic migrations live in
webapp/migrations/+webapp/backend/migrations/(two paths).
- main 沒有
kb_cricos_*、commission_school_receipts、commission_invoice_items、partner_portal_users等表。 commission_tracking.overall_status在commissions.py就被讀取11+處(第 26、115、273、309、310、336、377、415、422、450 行)。schemas/client.py(單數)共 6.5 KB;沒is_company;enrolments表很薄。- Alembic migrations 散在
webapp/migrations/+webapp/backend/migrations/(兩條路徑)。
- TRA-32: add 9 new tables in one migration set — pure ADD, zero conflict.
- TRA-33 (revised): add columns to 5 existing tables AND keep
overall_statuswith aDeprecationWarning. Drop split into two future sub-gates (NOT yet in Multica). - TRA-34: add
pypdf>=4.0.0+start-dev.sh.
- TRA-32:一次 migration set 加 9 張新表 — 純 ADD,零衝突。
- TRA-33(已修訂):給 5 張現有表加欄位,並保留
overall_status但發DeprecationWarning。Drop 拆成兩個未來 sub-gate(尚未在 Multica 建)。 - TRA-34:加
pypdf>=4.0.0+start-dev.sh。
routers/clients.py1,551 lines (2nd-largest backend file). Singlename, nois_company.VALID_STAGESis a global list — every visa type shares the same enum.- No
routers/cricos.pyon main (verified). - Pydantic schema at
schemas/client.py(singular).
routers/clients.py共 1,551 行(後端第二大檔)。單一name,沒is_company。VALID_STAGES是全域 list — 每種簽證類型共用同一 enum。- main 沒有
routers/cricos.py(已驗證)。 - Pydantic schema 在
schemas/client.py(單數)。
- TRA-35: Pydantic discriminated union on
is_company. Discrepancy plan refers toschemas/clients.py— actual file is singular. - TRA-36: new
core/workflow.pywithvalid_stages_for(...). 9 new stages. Visa enum renames. - TRA-37: brand-new module —
routers/cricos.py+ import script + 2 KB tables. Zero conflict.
- TRA-35:Pydantic 在
is_company上 discriminated union。不一致計劃寫schemas/clients.py— 實際是單數。 - TRA-36:新檔
core/workflow.py含valid_stages_for(...)。9 個新 stage,Visa enum 改名。 - TRA-37:全新模組 —
routers/cricos.py+ 匯入 script + 2 張 KB 表。零衝突。
if app_type == X in 30+ places became unmaintainable. kb_cricos_* shared (no tenant_id) is a deliberate read-only pattern with explicit "promote" copy-on-write.if app_type == X 已不可維護。kb_cricos_* 共用(無 tenant_id)是刻意的 read-only 模式,搭配明確「promote」copy-on-write。commissions.py1,036 lines,commission_semesters.py698 lines — combined ~1,734 lines usingclaimed,approved,overall_status._sync_commission_statsreturnsapproved_amount,invoiced_amount,paid_amount.- Backend consumers: 7 files. Frontend consumers: 3 files (verified).
routers/enrolments.py294 lines — no CoE parser.
commissions.py1,036 行、commission_semesters.py698 行 — 合計 ~1,734 行使用claimed、approved、overall_status。_sync_commission_stats回傳approved_amount、invoiced_amount、paid_amount。- 後端使用者:7 個檔。前端使用者:3 個檔(已驗證)。
routers/enrolments.py共 294 行 — 沒 CoE parser。
- TRA-38 Sub-PR 1: new
commission_engine.py— FIFO,derive_overall_status(). - TRA-38 Sub-PR 2: remove
claimed/approved; new state machine. - TRA-38 Sub-PR 3 (atomic): Stats API rename — all 10 files in one commit.
- TRA-39:
enrolments.py294 → 1,038 lines +coe_parser.py(208 LOC). - TRA-40 split into 4: Steps 4/5/6/7.
- TRA-41: Partner portal behind feature flag.
- TRA-38 Sub-PR 1:新
commission_engine.py— FIFO、derive_overall_status()。 - TRA-38 Sub-PR 2:移除
claimed/approved;新狀態機。 - TRA-38 Sub-PR 3(原子):Stats API 改名 — 10 個檔同 commit。
- TRA-39:
enrolments.py294 → 1,038 行 +coe_parser.py(208 行)。 - TRA-40 拆 4:Step 4/5/6/7。
- TRA-41:仲介門戶藏在 feature flag 後面。
statusConfig.jsas badge-renderer; nogetStagesForType.useFetch30s GET cache; noforce=true.ClientDetailPage.jsx58.6 KB (largest frontend). No Enrolments tab.- i18n: ~7 locales.
- None of EnrolmentFormPage, CricosRegisterPage, UploadCoESheet exist (verified).
statusConfig.js只是 badge renderer;沒getStagesForType。useFetch30s GET cache;沒force=true。ClientDetailPage.jsx58.6 KB(前端最大)。沒 Enrolments tab。- i18n:共 ~7 種 locale。
- EnrolmentFormPage、CricosRegisterPage、UploadCoESheet 都不存在(已驗證)。
- TRA-42: add helpers, force-refresh, dark-mode select fallbacks.
- TRA-43: +509 EN + 507 zh-TW keys (additive).
- TRA-44 i: 8 finance sheets (depends on TRA-38 SubPR3).
- TRA-44 ii: 6 entity-creation sheets.
- TRA-44 iii: 3 new pages + ClientDetail Enrolments tab (lazy).
- TRA-42:加 helpers、force-refresh、dark mode select fallback。
- TRA-43:+509 EN + 507 zh-TW key(純新增)。
- TRA-44 i:8 個財務 sheet(依賴 TRA-38 SubPR3)。
- TRA-44 ii:6 個實體建立 sheet。
- TRA-44 iii:3 個新頁面 + ClientDetail Enrolments tab(lazy)。
applicationstable holds rows whereapplication_type='course_enrollment'— these belong inenrolments.commission_trackingFKs referenceapplication_id.commission_school_receiptstable empty post-Phase-1.
applications表中application_type='course_enrollment'的 row — 應屬於enrolments。commission_trackingFK 指向application_id。- Phase 1 後
commission_school_receipts表為空。
- TRA-45: SELECT/INSERT migration; rewrite FKs; archive old rows for 30 days.
- TRA-46: create receipt per historical paid invoice; rerun recompute.
- 5 gates each: dry-run · backup · staging-validate · rollback rehearse · idempotency proof.
- TRA-45:SELECT/INSERT migration;改寫 FK;舊 row 封存 30 天。
- TRA-46:給每筆歷史已付 invoice 建一筆 receipt;重跑 recompute。
- 各 5 道 gate:dry-run · 備份 · staging 驗證 · 回滾演練 · idempotency 證明。
--commit.--commit 的 DRI。- No
/partner/*routes. No bcrypt JWT module. Nopartner_portal_userstable.
- 沒
/partner/*route。沒 bcrypt JWT 模組。沒partner_portal_users表。
- Independent
auth/partner_portal_auth.pywith bcrypt + separate JWT key. - Static-analysis grep gate on every SELECT in partner routes.
- JWT negative tests: cross-tenant + cross-partner reads return 403.
- Mount behind feature flag, OFF in prod until founder + security agent sign off.
- 獨立
auth/partner_portal_auth.py,bcrypt + 獨立 JWT 金鑰。 - 每個 SELECT 都做靜態分析 grep gate。
- JWT 負面測試:跨 tenant + 跨 partner 讀取回 403。
- 掛 feature flag 後面,prod 預設關,需要 founder + security agent 雙簽。
06Dependency & ripple analysis連動分析
overall_status drop ripple — 11+ reader sitesoverall_status drop 連動 — 11+ reader 點
| File檔案 | Sites點數 | Plan covers計劃覆蓋 |
|---|---|---|
routers/commissions.py | 11 | Yes是 |
routers/claims.py | ≥1 | Implicit隱含 |
routers/finance_dashboard.py | ≥1 | Implicit隱含 |
routers/invoices.py | ≥1 | Implicit隱含 |
schema_manager.py | ≥1 | Yes是 |
frontend reading overall_status前端讀 overall_status | ? | Not surveyed未盤點 |
Stats API rename ripple — 10 files atomicStats API 改名連動 — 10 檔原子
| Layer層級 | File檔案 | Plan covers計劃覆蓋 |
|---|---|---|
| Backend | routers/commissions.py | Yes / 是 |
| Backend | routers/claims.py | Yes / 是 |
| Backend | routers/finance_dashboard.py | Yes / 是 |
| Backend | routers/invoices.py | Yes / 是 |
| Backend | utils/sql_safe.py | Yes / 是 |
| Backend | utils/sql_builder.py | Yes / 是 |
| Backend | schema_manager.py | Yes / 是 |
| Frontend | RevenueWidget.jsx (15.6 KB) | Yes / 是 |
| Frontend | RevenueWidget.test.jsx (13.5 KB) | Yes / 是 |
| Frontend | revenue.schema.js (768 B) | Yes / 是 |
Files NOT yet on main (no merge conflict risk)main 上還沒有的檔案(無 merge 衝突)
| File檔案 | Owner subtask負責子任務 |
|---|---|
backend/utils/coe_parser.py | TRA-39 |
backend/routers/cricos.py | TRA-37 |
backend/routers/partner_portal.py | TRA-41 |
backend/routers/partner_auth.py | TRA-41 |
backend/auth/partner_portal_auth.py | TRA-41 |
backend/core/workflow.py | TRA-36 |
frontend/src/pages/EnrolmentFormPage.jsx | TRA-44 iii |
frontend/src/pages/CricosRegisterPage.jsx | TRA-44 iii |
frontend/src/components/.../UploadCoESheet.jsx | TRA-44 ii |
Likely ripple the plan does NOT mention計劃沒提到但可能波及
| Surface表面 | Likely impact可能影響 | Plan計劃 |
|---|---|---|
export.py (70.9 KB) |
Reads client.name — dual-identity affects CSV exports讀 client.name — 雙身份影響 CSV 匯出 |
Implicit隱含 |
routers/agent.py (51.3 KB) |
AI prompts may break with stage renameAI prompt 可能因 stage 改名破壞 | Not mentioned未提 |
routers/automation.py (45.0 KB) |
Automations match on overall_status自動化以 overall_status 配對 |
Not mentioned未提 |
routers/audit.py (30.0 KB) |
Audit logs quote stage names稽核紀錄引用 stage 名稱 | Implicit隱含 |
| Sentry / observabilitySentry / 可觀察性 | No new fingerprints/dashboards/alerts無新 fingerprint/dashboard/警報 | Not mentioned未提 |
07Risk assessment風險評估
claimed/approved states. If prod rows carry these at deploy, engine throws on read.claimed/approved 狀態。若 prod 還有這些 row,engine 讀取就 throw。SELECT COUNT(*) check; non-zero requires state-translation step.SELECT COUNT(*);非零就必須加 state 轉譯。webapp/migrations/, webapp/backend/migrations/, supabase/migrations/). Plan picks one without explanation.TRA-33-DROP-A/B as blocked issues now.TRA-33-DROP-A/B 建成 blocked issue。08Plan review — Good · Bad · Ugly · Questions計劃審查 — 好 · 壞 · 醜 · 疑問
- Replay over merge. Acknowledging sab is a parallel timeline and refusing direct merge is the highest-leverage decision.
- Atomic 10-file Stats API rename. SubPR3 forces backend + frontend in one commit.
- Three-step deprecation of
overall_status. Avoids "delete-while-still-read" outage. - Five gates on every data migration. Encodes painful past lessons.
- Partner portal feature-flagged OFF in prod by default. Defence-in-depth.
- Quantified done-when on every task. Specific tests, E2E flows, commit SHAs.
- Replay 而非 merge。承認 sab 是平行時間線,拒絕直接 merge — leverage 最高的決定。
- 原子 10 檔 Stats API 改名。SubPR3 強制後端 + 前端同 commit。
- 三步 deprecation
overall_status。避開「邊讀邊刪」outage。 - 每個資料遷移 5 道 gate。編碼了過去的痛苦教訓。
- 仲介門戶 feature flag 預設關。縱深防禦。
- 每個任務 done-when 量化。具體 test、E2E flow、commit SHA。
- TRA-33-DROP sub-gates not in Multica. Risk: column drop never happens.
- schemas/clients vs client path discrepancy. Coder will spend 5 minutes confused.
- Migration directory ambiguity. 3 dirs, plan picks one.
- No mention of
export.py,agent.py,automation.py,audit.pyripple. ~200 KB at risk. - No DRI named for production migration runs.
- i18n scope only EN + zh-TW. Other 5 locales unaccounted.
- TRA-33-DROP sub-gate 沒在 Multica。風險:欄位 drop 永遠不發生。
- schemas/clients vs client 路徑不一致。Coder 會困惑 5 分鐘。
- Migration 目錄歧義。3 個 dir,計劃只挑一個。
- 沒提
export.py、agent.py、automation.py、audit.pyripple。~200 KB 風險。 - 生產 migration 沒指名 DRI。
- i18n 範圍只 EN + zh-TW。其他 5 種 locale 沒交代。
- 10-file atomic SubPR3 will be enormous. Reviewers rubber-stamp due to diff fatigue.
- FIFO recompute not benchmarked. 8 semesters × 10,000 students = seconds wait?
- Two parallel auth surfaces. Doubles attack surface long-term.
- Replay manifest = single Coder judgement. Out-of-order replay = silent corruption.
- No sunset criterion for partner-portal feature flag. Toxic config risk.
- 10 檔原子 SubPR3 龐大。Reviewer 因 diff 疲勞 rubber-stamp。
- FIFO recompute 未 benchmark。8 學期 × 10,000 學生 = 秒級等待?
- 兩套並行 auth。長期雙倍攻擊面。
- Replay manifest = 單一 Coder 判斷。亂序 replay = 靜默損壞。
- 仲介門戶 feature flag 沒退場條件。Toxic config 風險。
- Q1. Big-bang transplant or wave-by-wave deploy?
- Q2. Who is the named DRI for prod
--commit? - Q3. Are the 5 non-EN/non-zh-TW locales abandoned?
- Q4. Lighthouse non-regression: CI baseline or manual?
- Q5. Cleanup criterion for
feature_flag_partner_portal_enabled?
- Q1. 大爆炸移植還是 wave-by-wave 部署?
- Q2. Prod
--commit的 DRI 是誰? - Q3. 5 個非 EN/zh-TW locale 是被放棄?
- Q4. Lighthouse 不退步:CI baseline 還是人工?
- Q5.
feature_flag_partner_portal_enabled的清理條件?
09Understanding gaps認知缺口
Recommendations before implementation動工前的建議
TRA-33-DROP-A/B in Multica today (status = blocked).立刻在 Multica 建 TRA-33-DROP-A/B(status = blocked)。export.py, agent.py, automation.py, audit.py ripple (~200 KB).新增稽核任務:export.py、agent.py、automation.py、audit.py ripple(~200 KB)。