Repository navigation
feat!: 年份支援改為 runtime 探測,並修正兩個統計錯誤 (2.0.0) - #4
Merged
Merged
Conversation
YearService 本意是動態探索上游可用年份以取代硬編碼常數(PRD 10.4),
但它從未被任何生產程式碼呼叫,也未從 src/index.ts 對外匯出 ——
真正生效的年份驗證始終是 SUPPORTED_YEAR_RANGE。
它請求的端點也不存在:data.jsdelivr.net/gh/.../data/ 實測連線失敗,
jsDelivr 的資料 API 在 data.jsdelivr.com/v1/packages/...。
14 個測試全數通過,是因為 vi.mock('ofetch') 餵入手寫的 {files: [...]}
形狀 —— 真實端點從未回傳過那個形狀。測試綠燈保護的是一段連不上線的死碼。
淨刪 386 行。呼叫端數量已用 grep 全 repo 確認為 0。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
年份支援改為 runtime 探測後,上游發布新年度不需要改程式碼, 所以真正需要監測的是「上游結構或內容悄悄變了」—— 那會讓探測 給出看似正常但錯誤的答案,比明確失敗難發現得多。 scripts/upstream-validate.mjs 驗證每年度資料: - 陣列型別、空陣列早退、元素為非 null 物件 - 四個欄位存在且型別正確(不只驗存在性 —— description 變 null 會讓 runtime 的 .includes() 炸,而只驗存在性的哨兵照樣綠燈) - 逐日連續(防「用無效日期湊筆數」) - 星期依日期計算後比對(只驗值域抓不到整體錯位) - 放假天數落在 100-140 合理區間 驗證邏輯抽成獨立模組以便測試(CONTRIBUTING.md 要求所有新程式碼有測試)。 13 條測試涵蓋 10 種畸形資料與台北跨年邊界。 workflow 每月執行,並偵測年度倒退:上游若刪掉已發布的年度, 探測只會安靜地回報較短範圍(對哨兵而言 404 是正常終點)。 以帶 upstream-year label 的 issue 標題當「曾可用」的歷史紀錄。 不使用 --app / --author 過濾:實測 `gh issue list --app <不存在>` 與 `--author app/<不存在>` 都會被靜默忽略(回傳筆數等於不過濾), 只有 --label 確實生效。label 本身即有權限保護。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BREAKING CHANGE: 移除公開匯出 SUPPORTED_YEAR_RANGE; HolidayStats.nationalHolidays 語意變更並新增 weekends 欄位。 ## 年份不再寫死在套件裡 上游早已發布 2027 資料,但 SUPPORTED_YEAR_RANGE.end 寫死 2026 擋住了它, 延誤約半年。資料本來就是 runtime 從 CDN 抓的,把「有哪些年份」打包進 套件是設計錯誤 —— 寫死下限(2017)合理,寫死上限不合理。 改為:收到年份就向上游探測,有資料就能查。上游發布新年度不需更新本套件。 - MIN_SUPPORTED_YEAR + getMaxQueryableYear()(今年+5,僅合理性防護) - HolidayRepository.getAvailableYears() 逐年 HEAD 探測,結果快取 24h - 只有 HTTP 404 代表「上游沒有這一年」;網路故障、超時、5xx 一律往外拋 未來年度 404 與歷史年度 404 分開處理:前者是「尚未發布」(可安全跳過), 後者代表上游把已發布資料弄掉了,靜默跳過會讓使用者拿到不完整的答案卻 以為完整。新增 YearNotPublishedError 型別供呼叫端區分,並在 service 層 保留該型別(原本會被重新包成 ServiceError 而抹掉)。 available_years 這個快取 key 關閉 useCacheOnError:對假期資料而言失敗時 回傳過期內容是合理降級,但對「可查哪些年份」而言那是把網路故障偽裝成 一個看似正常的舊答案。 ## stats 的兩個錯誤 1. holidayTypes 一個 map 混「分類桶」與「具體假日名」兩種語意,當上游的 description 剛好等於分類桶名稱時被加兩次。用真實資料驗證:舊邏輯在 全部 11 個年度(2017-2027)都算錯,碰撞的 key 有兩個 —— 補假(10 個年度)與調整放假(2017-2023)。 2. nationalHolidays 把 description 為空的一般週末也算成國定假日。 2026 年因此回報 114,實際具名國定假日只有 16 個(另 98 個是週末)。 週末移入新欄位 weekends,simple/table 輸出一併顯示。 新增不變式測試(分類桶 === 對應的頂層計數器),它在不指名任何 key 的 情況下就能抓到全部碰撞 —— 先前只針對「補假」的枚舉式測試漏掉了 「調整放假」(2019 年的補假筆數為 0,那年只錨定補假的測試不會紅)。 ## 版本號單一來源 CLI_VERSION 寫死 1.0.1、health 命令回報 1.0.0、package.json 是 1.0.1 —— 三方已經不同步。改為從 package.json 讀取。 ## 工具鏈 - 補上缺失的 eslint devDependency 與設定(npm run lint 原本 exit 127), 清掉 7 個既有錯誤,並移除 ci.yml 中 lint job 的 continue-on-error - 新增 tsconfig.tests.json:主 tsconfig 的 exclude 含 tests,所以 npm run typecheck 從未檢查測試碼,而 vitest 用 esbuild 剝型別也不檢查 (連 satisfies HolidayStats 都只是裝飾品)。用 extends 而非複製設定。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PRD 10.4「年份支援架構建議」原標「✅ 已完成」,但那個結論不成立: YearService 從未被呼叫、端點 URL 不存在、14 個測試靠 mock 假綠。 已改為「⚠️ 原方案未生效」並附「當時記載 vs 實際狀況」對照表。 教訓寫進 PRD:「模組已建立 + 測試通過 + 覆蓋率達標」三者同時成立, 仍不足以證明功能生效 —— 缺的是「有沒有任何生產路徑真的走過它」, 一次 grep 呼叫端就能發現。 其餘: - 支援年份改為「2017 起,上限由上游決定」 - years / stats / next 範例改用實際輸出(原本 stats 範例的數字是編的, 跑真實指令發現 14/100 而非 16/98) - next 的「已達支援年份上限」示例(該行為已不存在)重寫 - 錯誤前綴由「服務錯誤」改為「資料錯誤」(型別保留後的實際輸出) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
為什麼
上游
ruyut/TaiwanCalendar早已發布 2027 資料,但本套件把「支援到哪一年」寫死成 2026,導致查不到,已延誤約半年。資料本來就是 runtime 從 CDN 抓的、不打包進套件 —— 把「有哪些年份」打包進發布物是設計錯誤。寫死下限(2017,上游起點)合理;寫死上限不合理。
順著這條線查出同一病灶的多個變體,以及兩個長期存在的統計錯誤。
破壞性變更
SUPPORTED_YEAR_RANGEMIN_SUPPORTED_YEAR+getMaxQueryableYear()。原本的end是個謊 —— 它宣稱是「支援上限」,實際只是打包當時的寫死值HolidayStats.nationalHolidays語意變更weekends)stats的holidayTypes數值變更版號
1.0.1 → 2.0.0。MAJOR 在 cli / mcp / skills 三個專案間共享,代表同一世代;MINOR/PATCH 各自獨立。破壞性判定不是主觀的 —— 寫一份「舊消費者」檔
import { SUPPORTED_YEAR_RANGE }對打包產物跑tsc:舊 dist exit 0、新 distTS2305 has no exported member。主要變更
年份改為 runtime 探測 — 收到年份就向上游探測,有資料就能查;上游發布新年度不需更新本套件。只有 HTTP 404 代表「上游沒有這一年」;網路故障、超時、5xx 一律往外拋。
未來年度 404 與歷史年度 404 分開處理:前者是「尚未發布」(可安全跳過),後者代表上游把已發布資料弄掉了 —— 靜默跳過會讓使用者拿到不完整答案卻以為完整。
stats兩個錯誤 —holidayTypes一個 map 混「分類桶」與「具體假日名」兩種語意,當上游 description 剛好等於桶名時被加兩次。用真實資料驗證:舊邏輯在全部 11 個年度(2017–2027)都算錯,碰撞的 key 有兩個 ——補假(10 個年度)與調整放假(2017–2023)。另修nationalHolidays把週末算成國定假日。移除 386 行死碼 —
year-service.ts本意是動態探索年份,但零呼叫端、端點 URL 不存在(實測連線失敗),而它的 14 個測試全綠是因為 mock 餵入真實端點從未回傳的形狀。新增上游哨兵 — 每月驗證上游結構與內容(型別、逐日連續、星期比對、合理區間),並偵測年度倒退。13 條測試涵蓋 10 種畸形資料。
工具鏈補洞 —
npm run lint原本 exit 127(eslint 不在 devDependencies);npm run typecheck從未檢查測試碼(主 tsconfig 的 exclude 含 tests,而 vitest 用 esbuild 剝型別不檢查 —— 連satisfies HolidayStats都只是裝飾品)。驗證
依「差異檢查」原則:同一測試在舊碼 FAIL、新碼 PASS。
expected 4 to be 2expected 7 to be 4expected 3 to be 2ValidationError: 年份 2027 超出支援範圍promise resolved "[2017…2026]" instead of rejectingnpm run lint可執行Golden acceptance(真實上游 11 年):舊統計邏輯 11/11 年皆錯、新邏輯 0/11 錯。CLI 輸出與獨立數出的 ground truth 完全一致(2026: 具名 16 / 週末 98 / 補假 6 = 120)。
發佈後行為:
npm pack→ 安裝到乾淨臨時專案 →npx holiday --version回2.0.0、years回2017-2027、stats 2026三個數字全對。離線行為(複製真實 dist、CDN 換成不可解析主機、清快取):
years從「印出 2017-2026 並 exit 0」變成 exit 1 + 明確錯誤。目前狀態:typecheck(含測試)0 錯誤、lint exit 0、333 測試通過、canary 對真實上游 11 年通過。
誠實標示:未驗證的部分
python3 yaml、shellcheck);gh issue create、GITHUB_STEP_SUMMARY渲染、以及排程觸發都要 merge 後workflow_dispatch才算驗證。cron 是否按月觸發無法在合理時間內驗證。explicit-function-return-typewarning 未處理(該規則本身設為 warn;已確認 lint script 無--max-warnings,CI 不會因此紅)。getMaxQueryableYear()= 今年+5 的跨年翻轉無測試。已知待辦(不在本 PR)
holidayTypes一個 map 混兩種語意的結構問題只做防禦性修補,未拆成兩個欄位stats的holidayTypes.補假與compensatoryDays現已一致,但兩者仍是重複資訊~/.agents/孤兒副本需在 npm 上架後更新🤖 Generated with Claude Code