-
Notifications
You must be signed in to change notification settings - Fork 2
Tech Debt
已知債務、風險評估與償還建議。原則:考前只還會咬人的債,其餘記帳。
真的咬過人的坑另見 踩過的坑 Gotchas。
純函式測試已補齊(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 沒有。
CI 的 path gate 讓「同時動到 worker 與 frontend 的推送」兩邊都跳過,且以 success 收場。目前靠人記得手動部署。應改成:混合推送時明確 fail,或改為循序部署,不要用綠勾表示「什麼都沒做」。
早期 importer 把 K 型題選項壓平,years/*/batches 內的原始資料受影響。113 已修復,但只有 110/111/113 有 docx ground truth;其餘年份無從機器核對,只能靠成員讀到怪題時回報(challenge 機制承接)。
Google Sheet CSV 的 email 欄位 hardcode 在 column index 3(scripts/sync-access.ts:194)。Sheet 改版會讓每日 cron 同步靜默失效或同步錯欄位。應改成讀 header row 或搬進 config.toml。
PWA(2026-07-20 上線)的三道 Access 防線有 15 個單元測試,但飛航模式、session 過期後開 app、實機加到主畫面都沒有真人測過。SW 是最難收回的前端技術:回滾用的 sw-kill.js 已備妥且已加入 Access bypass,但從未演練過。至少該做一次「故意部署壞版本 → 用 sw-kill 救回」的演習。
FSRS 佇列用凌晨 4 點 rollover(worker/lib/due-window.ts),heatmap 與進度預估用 UTC+8 午夜(worker/lib/activity.ts)。兩套各自都正確——熬夜讀書不該在 session 中途換一批卡;活動日曆該對齊日曆日——但極易誤用。已在檔頭註明,長期應收斂成兩個具名概念(如 studyDay 與 calendarDay)而不是兩段散落的邏輯。
2026-07-20 引入 attempts 事件表時刻意不回填——舊資料只有聚合值,回填等於捏造時間戳並污染中位數。代價:修好的 heatmap 在 0023 之前的日期是空白的,選項分布統計初期票數也偏少(靠 review_progress.last_chosen 補)。這是有意識的取捨,不是遺漏;若日後要補,方案是混合 UNION 而非造假。
早年份詳解多為 AI 產生的種子(seed-explanations.py),品質參差;polish pipeline(polish_batch.py)在批次償還中,但缺一個「哪些題還沒 polish / 沒人審過」的 dashboard。
scripts/ 已有 25+ 支一次性 / 批次工具(enrich、polish、restore、backfill…),彼此約定散落。aggregate-batches.ts、apply-oe-verdicts.py 等含重要 domain 邏輯卻無文件。至少要在每支檔頭寫用途與 danger level(哪些會寫 prod DB)。
自動生成檔已達 ~500KB 並進了 git。考慮 gitignore + 在 setup 流程重生成,或固定 wrangler types 版本減少 churn。
任何省略該欄的 INSERT OR IGNORE 會靜默插入 0 列卻回報成功。已查核三個 insert 點(auth.ts、roster-sync.ts、sync-access.ts)全部正確綁值,所以這不是線上 bug,只是寫臨時 SQL 時的地雷(曾害人 debug 一輪)。SQLite 不能 in-place 加 default,修它要整表重建一張被大量 FK 參照的表——風險大於收益,決定不修。
關鍵詞數量(25–40)、去重上限(50)、輸入視窗(6000 字)都是估的,沒有依實際閱讀體驗校準。調整成本極低(改常數 + bump CLOZE_PROMPT_VERSION 讓快取失效),但需要真人回饋。
欄位與結構已有測試鎖住,但沒有真的拖進 Anki 匯入過;notetype 血專 是否存在於使用者 collection 也未確認。.apkg 與 PDF 明確列為非目標(Worker 內手刻 SQLite + zip 不可行;PDF 需 5–15 MB CJK 字型)。
鎖續期靠前端每 60 秒打一次;筆電闔蓋、網路斷線會留下最長 5 分鐘的殭屍鎖。對 20 人規模可接受;若升 Yjs/DO(見 Roadmap)此債自然消失。
docs/manual.html、frontend/public/manual.html、manual.pdf(4.6MB)等產出物在 repo 根部且部分未追蹤——決定:進 git(小的)或 gitignore + 產生腳本(大的),不要懸著。
@mention 通知靠下次載頁時拉 badge。設計上「夠用就好」,但若成員反映錯過討論,再評估 polling 或 SSE(注意 Workers 免費額度)。
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) |