Skip to content

Tech Debt

Hsiehting Lin edited this page Aug 9, 2026 · 5 revisions

技術債

已知債務、風險評估與償還建議。原則:考前只還會咬人的債,其餘記帳。

真的咬過人的坑另見 踩過的坑 Gotchas

高優先(可能咬人)

1. 整合層完全沒有驗證:元件、生命週期、瀏覽器引擎

純函式測試已補齊(2026-07-20:287 個 worker + 79 個前端測試),但只涵蓋純函式。React 元件、effect 生命週期、TipTap 整合完全沒有測試,而最貴的三個 bug 全在這一層,發生時測試都是綠的

Bug 日期 影響
個人筆記無限 render 迴圈 2026-07-20 畫記 popup 失效、裝置發燙
自動挖空 AI 呼叫失敗被吞 2026-07-20 功能靜默壞掉
Safari 題目頁整頁白屏 2026-07-29 發現 所有 iOS 使用者的題目頁完全不能用,而且不知道壞了多久

第三個揭露了一個更大的缺口:驗證從來只跑 Chromium。那個 bug 在 WebKit 上 100% 命中、在 Chromium 上 0%(成因見 Gotchas 十四),而 iOS 強制所有瀏覽器使用 WebKit——等於整個 iPhone 族群的題目頁長期是白畫面,卻沒有任何一道防線會發現。

建議的最小償還(成本低、涵蓋率高,優先於元件測試):一支 Playwright WebKit 冒煙測試,對數條關鍵路徑斷言「有內容且無 pageerror」,接進 pnpm test

const b = await webkit.launch()
const page = await (await b.newContext(devices['iPhone 13'])).newPage()
page.on('pageerror', e => errs.push(e.message))
// 逐條路徑 goto → 斷言 document.body.innerText.length > 0 且 errs 為空

必須打正式建置產物(dev server 的 StrictMode 雙掛載會產生不同訊號),且要打 deploy.sh 實際產出的那份 dist(worktree 建出來的雜湊可能不同,見 Gotchas 二十六)。

其後才是元件測試,優先順序:AnnotatableContent(effect 依賴與 setContent 時機)、Exam.tsx(計時器 × tutor mode × 標記三組狀態交錯)。需要引入元件測試框架,目前 repo 沒有。

2. 部署正確性仰賴人工判斷

CI 的 path gate 讓「同時動到 worker 與 frontend 的推送」兩邊都跳過,且以 success 收場。目前靠人記得手動部署。應改成:混合推送時明確 fail,或改為循序部署,不要用綠勾表示「什麼都沒做」。

3. K-type(組合題)資料品質

早期 importer 把 K 型題選項壓平,years/*/batches 內的原始資料受影響。113 已修復,但只有 110/111/113 有 docx ground truth;其餘年份無從機器核對,只能靠成員讀到怪題時回報(challenge 機制承接)。

4. Roster 欄位寫死

Google Sheet CSV 的 email 欄位 hardcode 在 column index 3(scripts/sync-access.ts:194)。Sheet 改版會讓每日 cron 同步靜默失效或同步錯欄位。應改成讀 header row 或搬進 config.toml

中優先

5. Service worker 已上線但未經真人驗證

PWA(2026-07-20 上線)的三道 Access 防線有 15 個單元測試,但飛航模式、session 過期後開 app、實機加到主畫面都沒有真人測過。SW 是最難收回的前端技術:回滾用的 sw-kill.js 已備妥且已加入 Access bypass,但從未演練過。至少該做一次「故意部署壞版本 → 用 sw-kill 救回」的演習。

6. 系統裡有兩套「今天」

FSRS 佇列用凌晨 4 點 rollover(worker/lib/due-window.ts),heatmap 與進度預估用 UTC+8 午夜(worker/lib/activity.ts)。兩套各自都正確——熬夜讀書不該在 session 中途換一批卡;活動日曆該對齊日曆日——但極易誤用。已在檔頭註明,長期應收斂成兩個具名概念(如 studyDaycalendarDay)而不是兩段散落的邏輯。

7. attempts 沒有回填歷史

2026-07-20 引入 attempts 事件表時刻意不回填——舊資料只有聚合值,回填等於捏造時間戳並污染中位數。代價:修好的 heatmap 在 0023 之前的日期是空白的,選項分布統計初期票數也偏少(靠 review_progress.last_chosen 補)。這是有意識的取捨,不是遺漏;若日後要補,方案是混合 UNION 而非造假。

8. 詳解覆蓋不均 + 種子詳解品質

早年份詳解多為 AI 產生的種子(seed-explanations.py),品質參差;polish pipeline(polish_batch.py)在批次償還中,但缺一個「哪些題還沒 polish / 沒人審過」的 dashboard。

9. Scripts 目錄野蠻生長

scripts/ 已有 25+ 支一次性 / 批次工具(enrich、polish、restore、backfill…),彼此約定散落。aggregate-batches.tsapply-oe-verdicts.py 等含重要 domain 邏輯卻無文件。至少要在每支檔頭寫用途與 danger level(哪些會寫 prod DB)。

10. worker-configuration.d.ts 肥大

自動生成檔已達 ~500KB 並進了 git。考慮 gitignore + 在 setup 流程重生成,或固定 wrangler types 版本減少 churn。

低優先(記帳即可)

11. users.updated_atNOT NULL 且無 DEFAULT

任何省略該欄的 INSERT OR IGNORE靜默插入 0 列卻回報成功。已查核三個 insert 點(auth.tsroster-sync.tssync-access.ts)全部正確綁值,所以這不是線上 bug,只是寫臨時 SQL 時的地雷(曾害人 debug 一輪)。SQLite 不能 in-place 加 default,修它要整表重建一張被大量 FK 參照的表——風險大於收益,決定不修

12. 自動挖空的密度是猜的

關鍵詞數量(25–40)、去重上限(50)、輸入視窗(6000 字)都是估的,沒有依實際閱讀體驗校準。調整成本極低(改常數 + bump CLOZE_PROMPT_VERSION 讓快取失效),但需要真人回饋。

13. 匯出的 CSV 未實測匯入 Anki

欄位與結構已有測試鎖住,但沒有真的拖進 Anki 匯入過;notetype 血專 是否存在於使用者 collection 也未確認。.apkg 與 PDF 明確列為非目標(Worker 內手刻 SQLite + zip 不可行;PDF 需 5–15 MB CJK 字型)。

14. Pessimistic lock 的極端情況

鎖續期靠前端每 60 秒打一次;筆電闔蓋、網路斷線會留下最長 5 分鐘的殭屍鎖。對 20 人規模可接受;若升 Yjs/DO(見 Roadmap)此債自然消失。

15. 未追蹤的產出物

docs/manual.htmlfrontend/public/manual.htmlmanual.pdf(4.6MB)等產出物在 repo 根部且部分未追蹤——決定:進 git(小的)或 gitignore + 產生腳本(大的),不要懸著。

16. 通知無即時推播

@mention 通知靠下次載頁時拉 badge。設計上「夠用就好」,但若成員反映錯過討論,再評估 polling 或 SSE(注意 Workers 免費額度)。

17. 電子紙底色用的是症狀修法,根因未查明

BOOX 的 NeoBrowser 會在沒有不透明背景的 <button> 底下畫一塊底色。四個假設(tap-highlight、UA focus ring、原生控制項繪製、accent-color / color-scheme)都在實機上被逐一否證,最後採用的是複製已知有效的條件 —— 給每顆按鈕不透明白底(詳見 Gotchas 十八)。

風險是有界的:修法不依賴任何關於成因的假設,所以就算根因是別的,它照樣有效;einkIsolation.test.ts 與「亮模式不受 e-ink 干擾」兩支測試守著它不外漏。但它也可能在別的元素上重演 —— 今天只有 <button> 被觀察到,<summary><input><select> 沒有逐一驗過。

償還:拿到那台裝置(或同款 WebView)能連遠端偵錯時,用 DevTools 的 Layers/Paint profiler 直接看那塊底色屬於哪一層。在那之前不值得再燒回合猜 —— 四輪已經證明,隔著一層轉述查不出來。

償還紀錄

日期 修法
2026-07-20 完全沒有自動化測試 九功能開發全程 TDD;純函式一律先抽出再測。287 worker + 79 frontend
2026-07-20 使用者可控的 IN (?) 無上限 worker/lib/sql-params.ts 集中 chunkParams / parseTagList;成因見 Gotchas
2026-07-20 /heatmap 嚴重低估活動量 原本數 review_progress.last_seen_at(每題一列、會被覆寫),一天做 10 題只算 1 次;改數 attempts
2026-07-20 FSRS 新卡無每日上限 逐年 deck 與 /due 共用同一套日界與 remainingNewToday,避免 Anki 新手雪崩
2026-07-20 未作答即可讀到全體正確率 與選項分布走同一道 gate;無方向性的人數仍開放
2026-07-20 AI 錯誤被吞成空結果 記 log、區分 ai_error / ai_empty、失敗降級重試
2026-07-20 自動挖空重整就消失 關鍵詞本來就存在 D1,缺的是開關狀態;補 cached_only=1 還原且不重複計費
2026-07-20 個人筆記畫記 popup 失效 effect 依賴從物件識別改成內容雜湊,解掉無限 render 迴圈
2026-08-09 作答紀錄來回切換就遺失 覆蓋來自離開時鄰居頁的閒置預抓,不是回來時的重抓;preserveLocalAnswer() 掛在 questionCache 的 fetcher(三條重抓路徑的唯一交會點)。成因見 Gotchas 二十一
2026-08-09 亮/暗主題沒有東西擋住 e-ink 覆寫外漏 兩層守門:靜態掃 styles.css 每條選擇器都帶 .eink;瀏覽器裡斷言含 eink 的規則一條都沒命中,且走切換路徑(繞一圈回亮模式)。兩層都確認過會紅
2026-08-09 e2e 以牆上時鐘為門檻,在 CI 上紅 改成斷言「有沒有再打一次網路」與「POST 有沒有 Resource Timing 條目」。見 Gotchas 二十二
2026-08-08 電子紙下 BOOX 的按鈕底色 複製「已知有效的條件」給每顆按鈕不透明白底。根因未查明,見上方第 17 項
2026-08-07 弱點地圖在索引未回填時整頁空白 question_tags → tag_topics 的確定性保底分群,語意分群仍優先
2026-08-06 deploy.sh 每次都誤報 R2/Vectorize 失敗 改問存在性(r2 bucket info / vectorize get 回 0/1),不解析 CLI 的人類可讀輸出
2026-07-29 Safari 題目頁整頁白屏 useEditor 會先交出已銷毀的 Editor;守衛從 !editor 改成 live(editor)(同時擋 null 與 isDestroyed)。債本身(無跨引擎驗證)尚未償還,見上方第 1 項
2026-07-29 策展快取 995 MB _ytdlp() 出口投影成 11 個會用到的欄位,3.4 MB
2026-06 importer 覆蓋社群升級答案 fix(import): never clobber community-revised answers;加 db:pull 鏡像工具
2026-06 error detail 外洩 + CORS 過鬆 fix(security): stop error-detail leakage and tighten CORS/headers
2026-05/06 113 K-type 壓平 修 importer + 依 docx ground truth 修資料
2026-05 /mcq 共享金鑰 per-user HMAC key + 自助 .skill(05-26 design)

Clone this wiki locally