更新日誌

GitHub

每一項重大變更,逐版記錄。同一份檔案隨儲存庫發布,並已翻譯為全部 19 種語言。

[1.56.3] — 2026-05-19

fix(reliability):提供者金鑰偵測拒絕佔位符 / 過短值,而不僅是空字串。 父 .env 中的佔位符 GEMINI_API_KEY 被報告為「✓ set」,並被選為作用中提供者而非有效的 ANTHROPIC_API_KEY。effectiveEnv() 僅拒絕 undefined/'',故 10 字元垃圾被當作真實金鑰:導覽橫幅顯示 GEMINI ✓ set,GET /api/status/providers 回傳 activeProvider: "gemini",所有即時 ⚡ 評估會對著死金鑰靜默失敗,而忽略有效的 108 字元 Anthropic 金鑰。新純函式 isUsableKey()(env-config.mjs)僅當金鑰 ≥ 20 字元(受支援提供者金鑰無更短者 — Gemini AIza… ≈ 39、Anthropic sk-ant-… ≈ 100+、OpenAI ≥ 40、Qwen ≈ 35)且非已知佔位符(your_*_here、changeme、placeholder、<…>、單字元重複…)時才視為已設定。統一套用於 hasAnthropicKey()/hasGeminiKey()(anthropic.mjs)、hasOpenAIKey()/hasQwenKey()(openai.mjs)及 GET /api/health 的 GEMINI_API_KEY/ANTHROPIC_API_KEY 列(從原始 process.env 遷移至同一 effective+plausible 視圖)——健康頁、提供者端點與 OR 路由現始終一致。selectActiveProvider() 不變(僅接收正確的 keysConfigured)。測試: 新增 CI 隔離套件 tests/key-detection-rejects-placeholder.test.mjs(5):isUsableKey 單元 + in-process createApp() 重現所報場景(暫存 .env 含 10 字元 GEMINI_API_KEY + 真實 ANTHROPIC_API_KEY)——gemini 不在 keysConfigured,activeProvider === "anthropic",/api/health 列一致。四個既有 effective-env 分層測試將過短樁值加長(契約不變)。821 → 826。fix(reliability) · test: tests/key-detection-rejects-placeholder.test.mjs。詳情見 CHANGELOG.md。


[1.56.2] — 2026-05-19

feat(a11y):UX-N1 — 依路由的在地化 document.title(多分頁辨識 + 螢幕閱讀器頁面變更播報)。 修正前 24 個路由都維持 index.html 的靜態 <title>(「career-ops — command center」)——分頁同名、書籤通用、每次「頁面已變更」播報相同。public/js/router.js 的 focusNewView() 現從檢視自身在地化的 <h1 class="page-title"> 衍生標題——「檢視 — career-ops」——因此標題自動翻譯(無需新 i18n 鍵)且每路由唯一。在首次繪製 guard 之前設定,使初始分頁也有標題(與 v1.56.0 UX-12 的 tabindex 設定順序一致)。檢視無標題時回退為 career-ops — command center。測試: 新增 CI 隔離的原始碼靜態套件 tests/document-title-per-route.test.mjs(4):focusNewView 指派 document.title;標題源自 <h1>(依路由 + 在地化,非單一字面值);指派先於 !firstPaintDone;存在產品預設值。817 → 821。feat(a11y) · test: tests/document-title-per-route.test.mjs。詳情見 CHANGELOG.md。


[1.56.1] — 2026-05-19

fix(a11y):消除路由託管的 tabindex="-1" 標題聚焦時出現的虛假品牌聚焦環。 public/js/router.js 在每次用戶端導覽時為目標檢視標題加上 tabindex="-1" 並 .focus()(讓螢幕閱讀器播報新頁面)。tabindex="-1" 元素無法以鍵盤抵達,但 Chromium 的 :focus-visible 啟發式仍繪製全域品牌環(*:focus-visible { outline: 2px solid var(--rausch) })——每次導覽在頁面標題周圍出現紅色矩形(如 #/dashboard 的「Command Center」),且已烘焙進 images/dashboard-*.png 主視覺截圖。修正為一條限定範圍的規則 [tabindex="-1"]:focus, [tabindex="-1"]:focus-visible { outline: none }(WAI-ARIA APG 託管聚焦模式)。互動控制項上真正的鍵盤聚焦保留全域 *:focus-visible 環(WCAG 2.4.7 不變);skip-link 的環不受影響(它是 <a>,非 tabindex="-1",特異度更高)。8 個 images/dashboard-*.png 已重新產生——紅框消失。測試: 新增 CI 隔離的原始碼靜態套件 tests/managed-focus-no-ring.test.mjs(4):全域 *:focus-visible 環仍定義(WCAG 2.4.7 無回歸);[tabindex="-1"]:focus,:focus-visible ⇒ outline:none;抑制規則位於全域規則之後(層疊安全);修正具範圍(無全域 *:focus{outline:none})。與 tests/dashboard-initial-focus.test.mjs 配對。813 → 817。fix(a11y) · test: tests/managed-focus-no-ring.test.mjs。詳情見 CHANGELOG.md。


[1.56.0] — 2026-05-19

feat(ux):LOW 打磨合集 —— UX-9 / UX-10 / UX-11 / UX-12(一個分組的次要發佈)。 UX-9 #/cv:頁面標題降級為安靜的 .cv-breadcrumb 麵包屑晶片,吵鬧的副標題移入 <h1> 的 title 提示 —— 讓使用者的履歷(預覽中的姓名)佔據視覺層級。F-V54-A 不變量保持 —— 仍是恰好一個 <h1>,仍為 .page-title。UX-10 新增共享輔助 UI.providerCostHint(t),置於 #/auto、#/evaluate、#/deep 及每個 #/<mode> 的 ⚡ 即時執行旁;復用 GET /api/status/providers(v1.55.3):有金鑰時顯示 「預估費用:OpenAI gpt-5-codex · ~$0.04/eval」(數量級,“~”);無金鑰時說明 ⚡ 複製手動提示(無 API 費用);fail-soft。UX-11 #/help:當 TOC 過濾縮小到恰好一個區段時,300ms 閒置後捲動至該處(防抖;0 或 >1 不觸發)。UX-12 #/dashboard:首次繪製時將 <h1> 設為可聚焦(tabindex="-1"),#content 保持 aria-live="polite"(啟動時朗讀)—— 不搶佔焦點(避免與跳過連結衝突,v1.41.0 決定)。新增 i18n 鍵 cost.estimate、cost.manual ×8;新增 .cv-breadcrumb/.cost-hint CSS。**測試:**4 個新源靜態 CI 隔離套件(cv-breadcrumb 3、run-cost-line 4、help-toc-autoscroll 4、dashboard-initial-focus 3);更新既有 cv-single-h1/help-nav-a11y 鎖(不變量保留)。800 → 813。4 項即時 Playwright 探針,0 主控台錯誤。feat(ux) · 4 test suites。詳見 CHANGELOG.md。


[1.55.8] — 2026-05-19

feat(tracker):伺服器端分頁 + 可點擊的漏斗晶片(UX-8)。 伺服器:GET /api/tracker 新增可選 ?page / ?pageSize / ?status 查詢參數。不帶參數時,回應與舊的 { rows: [...] } 逐位元組一致(所有現有呼叫方/測試不受影響)。帶參數時回傳 { rows: slice, total, page, pageSize, funnel } —— pageSize 箝制到 [1,500],page 箝制到 ≥1,status 過濾 rows+total,funnel 是整個歷史的狀態→計數細分(與頁/過濾無關,故晶片始終準確)。#/tracker:頂部新增可點擊漏斗晶片列 —— 「所有狀態 · N · Applied · N · Interview · N …」(順序 Applied → Responded → Interview → Offer → Rejected → Discarded → Evaluated → SKIP)。點擊晶片設定 Status 過濾(再次點擊作用中晶片則清除);作用中晶片為 aria-pressed 且高亮。8 個語言新增 i18n 鍵 track.funnelAria;新增 .tracker-funnel/.tracker-chip/.tracker-chip--active CSS。test: tests/tracker-server-paged.test.mjs(新增,7 個案例,CI 隔離,臨時埠行程內 Express + 臨時 CAREER_OPS_ROOT applications.md —— CLAUDE.md #2/#8):back-compat(無參數 ⇒ 恰為 {rows});?page&pageSize 切片 + total/page/pageSize/funnel 合計 N;最後部分頁無重疊;越界頁 ⇒ 空 rows + 有效 total;?status= 過濾 total/rows 而 funnel 為整個歷史;pageSize 上限;+ 晶片列源靜態鎖定。793 → 800。feat(tracker) · test: tests/tracker-server-paged.test.mjs。詳見 CHANGELOG.md。


[1.55.7] — 2026-05-19

feat(pipeline):>1000 列時的 vanilla-JS 列虛擬化(UX-7)。 #/pipeline 此前渲染每一列(filtered.forEach(list.appendChild(urlRow)))—— 一次掃描會用數千個 URL 填滿佇列,於是數千個列節點(每個是 flex div + <a> + 兩個按鈕)在每次篩選按鍵時同步建構,淹沒 DOM 與無障礙樹。新增 vanilla-JS 虛擬化(react-window 等價,無相依):超過 VIRTUALIZE_THRESHOLD = 1000 時 #/pipeline 變為固定高度(70vh)捲動視口,配一個不可壓縮的佔位墊(flex:0 0 auto,height = 列數 × 56px)以保留整個清單的真實捲軸,rAF 節流的捲動監聽只渲染視口 ± 5 列緩衝(一次約 16–19 個節點而非 N 個)。閾值及以下保持原始簡單渲染逐位元組不變,故典型管道與所有現有測試/e2e 不受影響。每個虛擬化列保留按 URL 區分的 ▶/✕ aria-label(F-V54-B 回歸鎖定)。視窗計算為純函式 computeWindow()。test: tests/pipeline-virtualize.test.mjs(新增,5 個案例,CI 隔離,源靜態):~1000 數值閾值;≤閾值分支保持 forEach→appendChild;>閾值分支以 rAF 捲動監聽 + 佔位墊渲染 slice(start,end);computeWindow() 在 [0,total] ± 緩衝內箝制;列保留 ▶/✕ aria-label。788 → 793。即時 Playwright 探針(1200-URL 夾具):scrollHeight≈67248,DOM 中僅約 16–19 個節點,視窗端到端跟隨捲動,0 主控台錯誤。feat(pipeline) · test: tests/pipeline-virtualize.test.mjs。詳見 CHANGELOG.md。


[1.55.6] — 2026-05-19

feat(scan):將次要篩選收納進「進階篩選」摺疊區(UX-4)。 #/scan 此前把所有篩選 —— 自由文字、遠端/混合/現場、範圍、來源,以及掃描後的 stack/level/dynamic facet 晶片 —— 等權堆疊,形成控制項之牆。現在日常篩選保持可見(自由文字 + 遠端/混合/現場;🌐 掃描按鈕已在控制項卡中單列),次要篩選摺疊進 <details class="scan-advanced"><summary>進階篩選</summary>:範圍 + 來源下拉,以及單獨的 facet 晶片簇(現在新結果集以表格而非晶片牆開頭,且僅在至少有一列晶片時渲染)。8 個語言新增 i18n 鍵 scan.advancedFilters;新增 .scan-advanced 摘要樣式(安靜的 ⚙ 提示、無標記、展開時加粗)。test: tests/scan-advanced-disclosure.test.mjs(新增,6 個案例,CI 隔離,源靜態):帶 .scan-advanced 鉤子與 scan.advancedFilters 標籤的 <details>/<summary> 存在;自由文字 + 遠端保持可見;範圍 + 來源在摺疊區內;chipsContainer 為 <details>;.scan-advanced summary 有樣式;scan.advancedFilters ×8。782 → 788。feat(scan) · test: tests/scan-advanced-disclosure.test.mjs。詳見 CHANGELOG.md。


[1.55.5] — 2026-05-19

feat(dashboard):2 個 P0 CTA + 聚焦近期活動提示的 hero(UX-3)。 #/dashboard 此前以約 30 個等權重節點開啟 —— 沒有清晰的「下一步做什麼」。新增 .dash-hero 區塊現位於頁首正下方:兩個 P0 旅程 —— ✨ URL 自動管道 與 🌐 立即掃描 —— 提升為大號 .btn-hero 按鈕;單一聚焦近期活動提示(「最近評估: <分數> — <標題>」,連結至報告;冷啟動時經 dash.heroNoEval 顯示引導空狀態)告訴回訪使用者停在何處、告訴新使用者唯一重要的動作。兩個主按鈕已從頁首移除(僅保留次要的「📋 開啟管道」)以避免動作重複。狀態計數從醒目的 .badge 降級為安靜的 .dash-chip 膠囊。8 個語言新增 i18n 鍵 dash.lastEval、dash.heroNoEval;新增 .dash-hero/.btn-hero/.dash-chip CSS。test: tests/dashboard-hero.test.mjs(新增,5 個案例,CI 隔離,源靜態):.dash-hero 存在且先於 Quick-actions 網格;兩個 P0 CTA 為帶 /auto+/scan 路由的 .btn-hero;聚焦 dash.lastEval + 空狀態 dash.heroNoEval;桶使用 .dash-chip;CSS 存在;dash.lastEval+dash.heroNoEval ×8。777 → 782。feat(dashboard) · test: tests/dashboard-hero.test.mjs。詳見 CHANGELOG.md。


[1.55.4] — 2026-05-19

feat(ux):Run 旁的誠實 auto-pipeline ETA + 掃描時醒目的 Stop(UX-6)。 #/auto:新增 .auto-eta 提示 —— “⏱ 約 1–2 分鐘”(鍵 auto.eta,title 經 auto.etaTitle)—— 現位於 Run 按鈕旁,使一鍵承諾在使用者決定之前就對耗時誠實;文案與 career-ops.org/docs(「貼上 URL → 1–2 分鐘內完整報告」)一致。#/scan:在數分鐘爬取執行中(aria-busy)時,Stop 從低對比幽靈按鈕提升為醒目的破壞性按鈕(新增 .btn-danger —— 填充,高對比白字配珊瑚色,字重 600)。setScanRunning(running) 在 btn-danger(執行中)與 btn-ghost(閒置,反正隱藏)之間切換 scan-stop-btn,使使用者在負載下也能找到並信任 Stop。8 個語言新增 i18n 鍵 auto.eta、auto.etaTitle;新增 .btn-danger/.auto-eta CSS。test: tests/auto-eta-stop.test.mjs(新增,4 個案例,CI 隔離,源靜態):#/auto 在 runBtn 旁以 .auto-eta 類渲染 t('auto.eta');auto.eta ×8;setScanRunning(running) 將 Stop 提升為 btn-danger;.btn-danger 存在且為高對比白字。773 → 777。feat(ux) · test: tests/auto-eta-stop.test.mjs。詳見 CHANGELOG.md。


[1.55.3] — 2026-05-19

feat(onboarding):畫面上的 4 提供方 OR 狀態 —— 冷啟動橫幅 + 活躍提供方徽章(UX-2,HIGH)。 新增唯讀端點 GET /api/status/providers → { activeProvider, activeModel, keysConfigured }。keysConfigured 使用與 llm.mjs 閘門相同的有效 env 視圖(process.env ∨ 父 .env);activeProvider 是 OR 路由器實際會選的 —— env-config.mjs 中的新純函式 selectActiveProvider() 走訪 providerOrder()(無對應金鑰的 LLM_PROVIDER 鎖定回傳 null)。不回傳任何機密 —— 僅提供方名稱 + 模型 id。SPA 外殼現在渲染全域引導區域(#onboarding-banner,由 app.js 填充,僅 CSP 安全 DOM):0 金鑰 → 紅色橫幅 + 指向 #/config?tab=api-keys 的 CTA;≥1 金鑰 → 低調徽章 顯示活躍提供方+模型。讓招牌差異點(「Anthropic / Gemini / OpenAI / Qwen 之一,自動排序」)在畫面上可發現,而非靠試錯。8 個語言新增 onboarding.* i18n 鍵;新增 .onboarding-warn/.onboarding-ok CSS。test: tests/onboarding-key-banner.test.mjs(新增,9 個案例,CI 隔離):selectActiveProvider 語意;GET /api/status/providers 行程內(臨時埠 + 臨時 CAREER_OPS_ROOT .env,絕不讀取真實父金鑰 —— CLAUDE.md #2/#8);靜態 SPA 接線 + onboarding.* ×8 覆蓋。764 → 773。feat(onboarding) · test: tests/onboarding-key-banner.test.mjs。詳見 CHANGELOG.md。


[1.55.2] — 2026-05-18

fix(cv):為 #/cv markdown 編輯器賦予描述性、自含的可存取名稱(F-V55-H / UX-5)。 #/cv 主編輯器 <textarea id="cv-editor"> 現在透過新鍵 cv.editorAria 帶有描述性 aria-label —— “CV Markdown 編輯器 —— 你的 markdown 格式專業履歷” —— 取代它從可見的「Markdown」區段標題繼承的簡略名稱。註:與 F-V55-H 症狀(僅檢查 aria-label/labels)相反,該欄位並非無名 —— v1.47.0(WS2 #16)早已透過 aria-labelledby → <h3 id="cv-md-heading">Markdown</h3> 綁定,故螢幕閱讀器播報「Markdown,編輯,多行」。v1.55.2 將該簡略「Markdown」升級為自含標籤。冗餘的 aria-labelledby 被移除(否則即死標記 —— 按 ARIA 優先級 aria-label 勝出);可見的 <h3>Markdown</h3> 為視力正常使用者保留。WCAG 1.3.1 + 4.1.2;與 v1.54.5 batch-tsv 修復(F-V54-C)平行。test: tests/cv-editor-a11y.test.mjs(新增,3 個案例,CI 隔離,如 auto-stepper-prerender.test.mjs 的源靜態):#cv-editor 透過 t('cv.editorAria', …) 自命名且回退非空;cv.editorAria 在全部 8 個語言存在且非空;元素上無冗餘 aria-labelledby。761 → 764。fix(cv) · test: tests/cv-editor-a11y.test.mjs。詳見 CHANGELOG.md。


[1.55.1] — 2026-05-18

fix(auto):在 #/auto 掛載時預先渲染 5 階段流程步進器(F-V55-E / UX-1,資深觀察 S-4 重新開啟)。 #/auto 現在在畫面掛載的那一刻就顯示文件化的五階段概要 —— 驗證 → 擷取 → 評估 → 儲存報告 → 加入追蹤器 —— 而不再在首個 SSE 事件前保持空白。此前 <ol class="auto-stepper"> 以 display:none 建立,且 renderStepper() 僅從 setStep() / run() 到達,因此冷啟動使用者在點擊 Run 之前從未看過文件承諾的流程。步進器現在在掛載時即可見,五個階段皆為 pending 狀態,並帶有 aria-label(auto.stepperAria)以便輔助技術朗讀該區域。關閉 F-V55-E(a11y/靜態保證視角)與 UX-1(承諾保真視角)—— 同一修復,兩個視角。test: tests/auto-stepper-prerender.test.mjs(新增,4 個案例,CI 隔離,如 router.test.mjs 的源靜態):STEPS 陣列恰好是 5 個規範階段且按序;stepperEl 掛載時非 display:none 且帶 auto.stepperAria;掛載作用域的 renderStepper() 呼叫先於 function setStep(;auto.stepperAria 存在於全部 8 個語言。757 → 761。fix(auto) · test: tests/auto-stepper-prerender.test.mjs。詳見 CHANGELOG.md。


[1.55.0] — 2026-05-18

feat(llm):無頭即時評估透過 “OR” 運作 —— Anthropic | Gemini | OpenAI | Qwen,依設定了哪個金鑰自動選擇。 應使用者請求,web-ui 的 ⚡ 即時評估現在使用任何已設定的 API 金鑰運作,而不僅是 Anthropic/Gemini。LLM_PROVIDER 新增 openai 與 qwen;auto(預設)使用第一個存在金鑰的提供方,優先順序為 Anthropic → Gemini → OpenAI → Qwen。明確值固定為一個;強制指定但無金鑰的提供方仍回退到手動提示路徑。新增 server/lib/openai.mjs —— 一個零相依的 OpenAI 相容 Chat Completions 用戶端(與 anthropic.mjs 相同的安全直連 HTTPS 模式:AbortController 逾時、金鑰從不記錄、effectiveEnv() 金鑰解析使父 .env 的金鑰無需重啟即生效)。單一核心(runOpenAICompatible)支撐 runOpenAI(api.openai.com)與 runQwen(阿里雲 DashScope 的 OpenAI 相容模式;中國大陸主機在 raw .env 中用 QWEN_BASE_URL 覆寫端點)。無 SDK、無任意 CLI 執行 —— 父專案保持 CLI 無關(Claude Code · Codex · Gemini · OpenCode · Qwen · Copilot · Kimi);這僅擴充無頭 API 金鑰路徑。OpenAI/Qwen 尾部已接入所有評估面:/api/evaluate、/api/deep、/api/mode/:slug 以及 /api/auto-pipeline SSE —— 在 Anthropic(內聯)+ Gemini(子行程)分支之後被查詢以保留 auto 偏好,並使用與 Anthropic 相同的打包上下文內聯。env-config.mjs:QWEN_API_KEY(機密)+ QWEN_MODEL(非機密)加入 KNOWN_KEYS/KEY_GROUPS.core;LLM_PROVIDERS 與 providerOrder() 擴充;OPENAI_API_KEY 現為一級無頭提供方金鑰(此前僅儲存)。#/config API 金鑰標籤頁:LLM_PROVIDER 選擇器新增 openai/qwen;新增 QWEN_API_KEY + QWEN_MODEL 欄位(精選 qwen-max/qwen-plus/qwen-turbo/qwen2.5-* 清單);標籤頁頂部的新說明解釋 CLI 無關的父專案 vs web-ui 無頭評估及 OR 順序。8 個語言全部新增 i18n 鍵。test: tests/openai.test.mjs(新增,9 個案例,CI 隔離):OpenAI/Qwen 成功 + 區塊陣列內容、Bearer 認證、預設及 QWEN_BASE_URL 覆寫端點、4xx/5xx/格式錯誤、max_tokens 鉗制、逾時、effectiveEnv 金鑰偵測、金鑰無洩漏金絲雀。tests/provider-selector.test.mjs 已更新以涵蓋 v1.55.0 的 providerOrder/LLM_PROVIDERS/SECRET 面 + OpenAI/Qwen 尾部接線。748 → 757。feat(llm) · test: tests/openai.test.mjs · test: tests/provider-selector.test.mjs。詳見 CHANGELOG.md。


[1.54.10] — 2026-05-18

fix(auto-pipeline):SSE 用戶端斷線衛生 —— 消除不穩定的 Playwright e2e 作業。 Playwright e2e 作業間歇性變紅(32/32 個單項測試通過,但 not ok 2 - tests/playwright-smoke.mjs):在 #/auto SSE 串流進行中關閉頁面,會使伺服器下一次 res.write() 以 EPIPE/"aborted" 被拒絕,而 —— 由於回應上沒有 'error' 監聽器 —— Node 將其升級為 uncaughtException,node:test 報告為 “asynchronous activity after the test ended”。auto-pipeline.mjs 中的 openSse() 現在註冊一個 no-op 的 res.on('error'),並以 res.writableEnded || res.destroyed 保護 send()(以 try/catch 包裹)—— 消失的用戶端是預期的,而非異常。這是正確的生產 SSE 衛生,不只是測試修復。tests/playwright-smoke.mjs:Cmd+K 測試使用了真實的外送 URL(https://example.com/jobs/123),但只等待模態出現,因此 closePage() 在測試結束後中止了伺服器進行中的 safeGet()。現在它等待管線到達終態(以便 fetch 在關閉前正常解析)。共用的 closePage() 輔助函式(window.stop() 然後關閉)和 after 鉤子的 server.closeAllConnections() 作為縱深防禦保留。已驗證:連續 8/8 綠色執行(6× node --test + 2× browser-smoke),此前約每 2 次有 1 次紅。tests/auto-pipeline.test.mjs +1 個靜態案例,鎖定 openSse 斷線衛生契約(res.on('error') 監聽器 + writableEnded||destroyed 守衛 + try 包裹的寫入)。747 → 748。fix(auto-pipeline) · test: tests/auto-pipeline.test.mjs。詳見 CHANGELOG.md。


[1.54.9] — 2026-05-18

fix(llm):在請求時尊重父專案 .env 的 LLM 金鑰 —— 停止錯誤路由到過期/無效的提供方。 即便 ANTHROPIC_API_KEY 是已設定的提供方,即時評估也可能以 “Gemini API error: API key not valid” 失敗。根本原因:hasAnthropicKey() / hasGeminiKey()(以及 runAnthropic 的金鑰/模型查找)只讀取啟動時的 process.env 快照。如果在伺服器啟動後才把 Anthropic 金鑰加入父 .env,執行中的行程永遠看不到它 → Anthropic 偵測為 false,評估隨後回退到 process.env 中確實存在的任何過期金鑰(通常是舊的、無效的 GEMINI_API_KEY)。Gemini 執行路徑(父 Node 子行程)已經讀取父專案的即時 .env,因此兩個提供方解析金鑰的方式不一致。env-config.mjs 新增 effectiveEnv(key, envFilePath):非空的 process.env 值優先(涵蓋 shell export 與 POST /api/config 的即時套用),否則查閱目前父 .env 檔案。anthropic.mjs 現在透過它解析 ANTHROPIC_API_KEY、ANTHROPIC_MODEL 和 Gemini 金鑰檢查,因此設定在父 .env 的金鑰無需重啟伺服器即被尊重,且金鑰偵測始終與請求實際送出的金鑰一致。提供方順序不變(auto → Anthropic-然後-Gemini);這只修復偵測。金鑰從不被記錄或回傳(REVIEW-B4 無洩漏測試仍通過)。tests/anthropic.test.mjs 重寫為 CI 隔離(temp CAREER_OPS_ROOT、動態 import),含 2 個重現確切 bug 的新案例(金鑰僅在父 .env → 被偵測到;process.env 未設定時 runAnthropic 送出父 .env 的金鑰 + 模型)。tests/env-config.test.mjs +3 個 effectiveEnv 案例(process.env 優先、含空字串視為未設定的 .env 回退、檔案缺失 / 金鑰缺失 / 無路徑 → undefined)—— 新分支 100%。742 → 747。fix(llm) · test: tests/anthropic.test.mjs · test: tests/env-config.test.mjs。詳見 CHANGELOG.md。


[1.54.8] — 2026-05-18

feat(config):Modes 欄位表單始終渲染規範結構描述(即便在空/樁檔案上),並帶 career-ops.org 欄位指引。 v1.54.3 的 Modes 欄位表單只為已存在的 ## 小節渲染欄位 —— 因此在全新、空或非結構描述的 modes/_profile.md(例如常見的 1 行樁)上,它會回退到 “No ## sections found — use the raw editor below.”,使用者拿不到任何欄位。應使用者請求(“разбей по полям … описание полей возьми из career-ops.org/docs”),表單現在始終按文件化順序渲染 5 個規範欄位(Target Roles、Adaptive Framing、Exit Narrative、Comp Targets、Location Policy),存在時從檔案預填,不存在時為空但可編輯 —— 因此全新的 profile 可完全透過表單填寫。每個欄位顯示一段來自規範 career-ops.org Quick Start §Step-5 的描述(在 Target Roles / Adaptive Framing / Exit Narrative / Comp Targets / Location Policy 中分別填什麼),透過 aria-describedby 接入供螢幕閱讀器使用。容忍標題變體:範本的 ## Your Target Roles(等)對應到與 ## Target Roles 相同的規範欄位,因此範本與伺服端鷹架慣例都不會破壞表單。collect() 現在是帶標籤的酬載:當渲染的標題與檔案既有標題完全一致時進行非破壞性的 { sections } 合併(前言 + 未觸碰 + 自訂小節按位元組穩定保留),或當檔案缺少結構描述時進行 { markdown } 全檔案重建,引導/正規化一份符合結構描述的文件。重建路徑在 config.js 中經確認閘控(它替換父檔案 —— WS2 #4 破壞性儲存不變量),保留既有前言(或文件化的預設值),並按 verbatim 保留非規範小節。8 個語言環境新增 6 個 i18n 鍵(config.modesDescTargetRoles … config.modesDescLocationPolicy + config.modesFormRebuildBody)。tests/modes-form.test.mjs 按 v1.54.8 契約重寫:結構描述 + 規範順序、config.js 酬載/確認接線、8 個語言環境中每個欄位來自文件的描述存在、canonicalKey “Your X” 容忍、清單往返穩定性、引導始終渲染保證,以及帶資料安全的帶標籤 sections-vs-markdown collect()。已針對真實父樁檔案線上驗證(5 個欄位 + 描述出現,0 主控台錯誤)和隔離樁夾具(填寫 → 經確認閘控儲存 → 5 個規範小節全部持久化)。feat(config) · test: tests/modes-form.test.mjs。詳見 CHANGELOG.md。


[1.54.7] — 2026-05-18

fix: W-001 — 程式碼/樣式資源 + SPA 外殼以 Cache-Control: no-store 提供(部署衛生)。 SPA 透過不帶版本查詢字串的純 <script src> 載入 api.js / router.js / 每個視圖,且沒有建置步驟(無內容雜湊),因此部署後瀏覽器可能繼續提供快取的舊 bundle 達數小時 —— 在查詢字串路由上出現 stale-cache 404(於 v1.29.2 回歸期間線上觀察到;回歸執行 W-001)。server/index.mjs 現在透過 express.static 的 setHeaders 掛鉤在 .js / .mjs / .css / .html 上設定 Cache-Control: no-store,並在 SPA 外殼 catch-all(它使用 sendFile 並繞過 setHeaders)上明確設定,使瀏覽器始終重新驗證驅動路由的程式碼。非程式碼靜態資源保留 express.static 的預設快取。安全標頭(CSP / nosniff / frame-deny / referrer-policy)不變 —— 由既有的 security-headers 套件(8 個案例)與新測試並行跑綠驗證。新增 1 個測試檔 tests/asset-cache-control.test.mjs —— 4 個案例(JS 資源 no-store、CSS no-store、靜態 index.html no-store、SPA catch-all 深層路由外殼 no-store),針對隔離的 CAREER_OPS_ROOT 啟動真實應用。另加 tests/playwright-smoke.mjs 中的 flaky teardown 修復(單獨的 test(e2e) 提交):auto-pipeline 的 SSE 冒煙測試現在於 finally 中取消 reader + 中止 fetch,且 after 掛鉤強制關閉殘留 socket,消除了使 v1.54.6 Playwright e2e 作業變紅的 teardown 後 “Error: aborted”。738 → 742。fix · test: tests/asset-cache-control.test.mjs。詳見 CHANGELOG.md。


[1.54.6] — 2026-05-18

fix(a11y): S-7 — #/help 的 back-to-top 按鈕攜帶規範選擇器類別 back-to-top。 #/help 的浮動 back-to-top 按鈕運作正常(已線上驗證),但其類別清單(btn btn-primary help-back-top)位於 spec §2 #28 測試所瞄準的 .back-to-top 選擇器慣例之外 —— 收緊後的選擇器本會出現 flaky(回歸執行 S-7,「輕鬆取勝」)。該按鈕現在也攜帶規範的 back-to-top 類別。純粹增量且為 CSS no-op:help-back-top(既有的 CSS 掛鉤)未變,而 back-to-top 沒有 CSS 規則 —— 它只是一個穩定的測試/自動化控柄。已線上驗證:document.querySelector('.back-to-top') 解析到該按鈕,aria-label 完整,0 主控台錯誤。於 tests/help-nav-a11y.test.mjs 中擴展了既有的 #12 案例,新增一條斷言:back-to-top 按鈕的類別清單包含規範的 back-to-top 選擇器(無新檔)。fix(a11y) · test: tests/help-nav-a11y.test.mjs。詳見 CHANGELOG.md。


[1.54.5] — 2026-05-18

fix(a11y): F-V54-C — #/batch TSV 編輯器擁有可存取名稱。 #/batch 的 TSV <textarea> 此前有一個透過 aria-describedby 接線的提示,但沒有可存取名稱 —— 無 <label htmlFor>,無 aria-label/aria-labelledby(回歸執行 F-V54-C;WCAG 1.3.1 Info & Relationships / 4.1.2 Name, Role, Value)。aria-describedby 提供的是描述而非名稱,因此螢幕閱讀器讀出的是無標籤的「edit text」。該 textarea 現在透過新增 i18n 鍵 batch.tsvAria 攜帶 aria-label,與已使用 *Aria 鍵的同級執行控制輸入保持一致;既有的 describedby 提示得以保留。已線上驗證:aria-label 存在且已本地化,aria-describedby 完整,0 主控台錯誤。新增 i18n 鍵 batch.tsvAria 於全部 8 個語言區。新增 1 個測試檔 tests/batch-tsv-accessible-name.test.mjs(2 個案例:batch-tsv 區塊在保留其 describedby 提示的同時透過 t(batch.tsvAria) 擁有 aria-label;batch.tsvAria 在全部 8 個語言區中定義);736 → 738。fix(a11y) · test: tests/batch-tsv-accessible-name.test.mjs。詳見 CHANGELOG.md。


[1.54.4] — 2026-05-18

fix(a11y): F-V54-B — #/pipeline 列操作按鈕擁有可存取名稱。 #/pipeline 上每列的 ▶(評估)和 ✕(刪除)按鈕此前是僅含 title 屬性的純圖示按鈕(回歸執行 F-V54-B;WCAG 4.1.2 Name, Role, Value)。title 不是可靠的可存取名稱,因此螢幕閱讀器使用者聽到的是一長串無法區分的「button」,無法判斷刪除會命中哪一列。兩個按鈕現在都帶有明確的 aria-label,透過新增的 shortUrl() 輔助函式以精簡 URL 消歧(host + …/ + 最後 2 個路徑區段;不可剖析輸入回退為尾端切片),因此 a11y 樹讀出如 「Delete: hh.ru/…/vacancy/12345」。無新增 i18n 鍵 —— 重用 common.delete / pipe.evaluateBtn + URL。已線上驗證:1385 列,每個按鈕名稱按列唯一,0 主控台錯誤。新增 1 個測試檔 tests/pipeline-row-action-names.test.mjs(4 個案例:兩個按鈕均以 shortUrl(url) 接線 + 恰好兩個此類標籤,shortUrl 在使用前宣告,同主機不同職缺的 URL 不會合併,裸主機 / 不可剖析 / 空回退);732 → 736。fix(a11y) · test: tests/pipeline-row-action-names.test.mjs。詳見 CHANGELOG.md。


[1.54.3] — 2026-05-18

feat(config): #/config “Modes” 分頁的結構化欄位表單(不再是原始 markdown)。 “Modes” 分頁此前將 modes/_profile.md 按每個 ## 區塊編輯為單個原始 <textarea>(v1.36.0 的區塊級粒度)。應使用者要求,現在改為算繪從文件化結構描述衍生的結構化欄位表單(career-ops.org Quick Start §Step-5):Target Roles / Adaptive Framing / Comp Targets → 可增刪的可重複帶標籤行輸入(每個欄位一行 role/angle/comp,+ Add line / 每行帶 aria-label 的 ✕);Exit Narrative / Location Policy → 單個帶標籤的散文 <textarea>。每個欄位都是透過 <label htmlFor> 繫結、帶 i18n 區塊名的真實控制項。新增 public/js/lib/modes-form.js(window.ModesForm)持有 parse → render → collect() 邏輯;它饋入既有的 PUT /api/modes/_profile { sections } 合併路徑,因此前導文字、順序以及表單未觸及的任何區塊都保持位元組穩定(合併而非取代,由伺服端強制)。資料安全: 正文不是純項目符號清單的正規清單區塊(使用者在此放入了散文)以及任何非正規 ## 區塊,會回退為帶說明註記的帶標籤原樣 <textarea> —— 任意內容原樣 round-trip,絕不會被靜默重構或遺失。Round-trip 穩定性已驗證:serialise(parse(body)) 重新剖析完全一致。整檔原始 markdown 編輯器仍作為帶確認門的 Advanced 摺疊區保留,用於增刪區塊及編輯前導文字(WS2 #4 破壞性儲存門不變)。8 個語言區新增 10 個 i18n 鍵(config.modesTargetRoles … config.modesUnknownNote)。新增 1 個測試檔 tests/modes-form.test.mjs(7 個案例);725 → 732。已針對隔離的 CAREER_OPS_ROOT fixture 線上驗證:5 個正規區塊算繪為欄位 + 1 個自訂區塊作為帶標籤回退,編輯並儲存的 round-trip 保留了前導文字 + 自訂區塊,0 主控台錯誤。feat(config) · test: tests/modes-form.test.mjs。詳見 CHANGELOG.md。


[1.54.2] — 2026-05-18

feat(config): #/config 中的 OpenAI / Codex 模型選擇器。 #/config 此前無法選擇 OpenAI / Codex 模型 —— 儘管 OPENAI_API_KEY 已為父專案多 CLI(Codex / OpenCode)流程公開,卻只有 ANTHROPIC_MODEL 與 GEMINI_MODEL 有下拉選單。現在 OPENAI_MODEL 成為一等環境變數鍵:已加入 env-config.mjs 的 KNOWN_KEYS(排在 OPENAI_API_KEY 之後)及 core 鍵群組,並刻意不納入 SECRET_KEYS —— 它是模型 id 而非憑證,故永不遮罩。config.js 新增一份精選 OPENAI_MODELS 清單(預設 gpt-5-codex,其後為 gpt-5 / gpt-5-mini / gpt-4.1 / o4-mini / o3),以及在 OpenAI 鍵之後算繪的 OPENAI_MODEL <select> 欄位,完全鏡像 Anthropic/Gemini 模型欄位。8 個語言區新增 i18n 鍵 config.openaiModel + config.openaiModelHint。新增 1 個測試檔 tests/openai-model-selector.test.mjs(4 個案例);721 → 725。已線上驗證:#/config → 含 6 個選項的 OPENAI_MODEL select,預設 gpt-5-codex,已繫結標籤,0 主控台錯誤。feat(config) · test: tests/openai-model-selector.test.mjs。詳見 CHANGELOG.md。


[1.54.1] — 2026-05-18

fix(a11y): F-V54-A —— #/cv 單一 <h1>。 CV markdown 自身的 # Name 算繪成了頁面標題 <h1>CV</h1> 旁的第二個頂層 <h1>(回歸執行 F-V54-A;WCAG 1.3.1 資訊與關係 / 2.4.6 標題)。cv.js 現將 CV 預覽的每個注入點(初次算繪、檔案匯入時重新整理、編輯器即時同步)經由作用域受限的 cvMd() 統一處理,將標題下移一級(h1→h2 … h6→role="heading" aria-level="7"),使頁面恰好保留一個 <h1>。刻意將作用域限定於 cv.js —— UI.md 由 help/reports/deep/evaluate 共用,各自以自有方式管理標題。新增 1 個測試檔 tests/cv-single-h1.test.mjs(4 個案例);717 → 721。已線上驗證:#/cv → 1 個 <h1>,使用者的 # Name 現為 <h2>,0 主控台錯誤。fix(a11y): F-V54-A · test: tests/cv-single-h1.test.mjs。詳見 CHANGELOG.md。


[1.54.0] — 2026-05-18

WS10 —— canonical-docs 再驗證 + help 套件 H3 對等(最終收斂版本)。 CHANGELOG/結構 CI 閘門只檢查 H2,因此 docs/help/en.md 已悄然漂移至 70 個 H3 子節,而 7 個本地化套件仍停在 68 —— 差距在 §17(「Reference adapters」表 + 「Common pitfalls」清單,僅英文)。兩者現已譯入全部 7 種語言(轉接器檔名 / 連結 / 識別碼保持逐位元組一致);8 個套件現均為 17 H2 / 70 H3。help-ru-config-section.test.mjs 中新的 H3 對等閘門鎖定之(716 → 717)。canonical-docs-coverage.test.mjs 7/7 確認 help 仍鏡像 career-ops.org/docs 的全部 5 篇指南;WS2 的 UX 稽核(v1.41→v1.52 的 40 項)對每個畫面與 docs 進行校驗 —— 無背離。docs/sdd/CONVENTIONS.md 更新至 v1.54.0(測試合計、H3 對等閘門、檔案尺寸離群項、新增無障礙約定章節)。WS0–WS10 完成;僅餘 WS11。fix(docs): WS10 canonical re-validation + H3 parity · test(help): H3-parity gate。詳見 CHANGELOG.md。


[1.53.0] — 2026-05-18

WS9 —— shell 表面測試金字塔(最後一個未測層)。 4 個 bin/*.sh 指令稿與 .githooks/pre-commit 掛鉤此前覆蓋率為零;新增的 tests/sh-files.test.mjs 加入 10 個案例,鎖定 bash -n/sh -n 語法、shebang + 可執行位,以及其他 workstream 所依賴的行為契約:career-ops-ui.sh —— help 以 0 結束且無 shell-source 洩漏(v1.40.0 回歸守衛),未知 verb 以 2 結束,usage() 為 heredoc;start.sh —— 尊重 NO_OPEN、要求 Node ≥ 18,並將瀏覽器前置委派給 scripts/open-dashboard.mjs(v1.43.0 守衛);setup.sh —— 嚴格模式、SKIP_START、克隆兩個儲存庫;run_all.sh —— --quick/--no-e2e 解析與 4 個套件;.githooks/pre-commit exec WS7 評審器,且沒有任何 shell 檔案呼叫 git --no-verify(CLAUDE.md 硬規則 #7 守衛);install-hooks.mjs 接線 core.hooksPath。docs/architecture/TESTING.md —— 在金字塔圖中加入 shell 表面基礎層 + v1.53.0 合計註記(716 個 node --test 案例 / 90 個檔案 + 4 個 E2E 表面)。706 → 716。詳見 CHANGELOG.md。


[1.52.0] — 2026-05-18

WS2 LOWs #33–#40 —— 批次打磨清掃(收尾 UX 稽核佇列)。 八項低嚴重度發現。fix(a11y/i18n): WS2 LOW batch —— #33:#/dashboard —— 頁首的 3 個 CTA 不一致(僅 2 個有前導圖示);「Open Pipeline」現帶 📋,三者齊整。#34:#/profile —— 原型的 fit/level 算繪為兩個含糊的 chip;現加前綴(Fit: / Level:)並配對應的 aria-label。#35:#/health —— Run-doctor / verify 的 toast 顯示 doctor.mjs 的原始字串;現已 i18n 鍵化。#36:#/health —— 檢查結果原是扁平的 <div> 串;現為 role=list 的 <ul>/<li>,狀態徽章帶 aria-label="<check>: <status>"。#37:#/reports —— 報告卡原是僅滑鼠的 <div onClick>;現為 role=link + tabindex + Enter/Space 處理器 + aria-label。#38:#/activity —— 分頁器註解寫「200」而程式碼請求 500;已對齊到 CAP 常數,且當 500 上限截斷舊歷史時浮現 role=note 通知。#39:#/batch —— prose 佔位符為英文硬編碼而其 aria-label 已 localized;四個現已 i18n 鍵化。#40:模式頁在非同步探測後靜默重新命名主按鈕;現由禮貌的 role=status 區域播報。新增 10 個 i18n 鍵 × 8 個語系({n} 保留);測試 +9:test: tests/low-sweep.test.mjs。697 → 706。收尾 WS2 的 UX 稽核佇列(v1.41→v1.52 的 #1–#40);接下來 WS9 → WS10 → WS11。詳見 CHANGELOG.md。


[1.51.0] — 2026-05-18

WS2 #13 + #14 + #18 + #19 + #20 —— #/auto 與 #/evaluate 的 feedback/i18n 清掃。 UX 稽核的五項發現。fix(a11y/ux): auto+evaluate — busy state, actionable HTTP errors, clipboard fallback, aria-live result, spinner-guarded submit —— #13:#/auto 的 Run 按鈕現在顯示忙碌狀態(is-loading + aria-busy +「Running…」),而非僅停用。#14:失敗的 HTTP 請求現在於步驟上浮現可操作的 i18n 訊息並附帶 toast(帶 {n} 的 auto.httpFail),不再是乾巴巴的「HTTP 500」。#18:手動模式的「Copy prompt」現在使用非同步 Clipboard API 並帶 execCommand 回退,真正失敗時 toast 提示,而非虛假的「Copied」。#19:evaluate 結果容器現為 role=status aria-live=polite,使漫長的 LLM 呼叫向螢幕閱讀器播報。#20:Evaluate 按鈕以 UI.withSpinner 包裹(原先為樸素的 onClick: run,允許重複送出)。新增 3 個 i18n 鍵 × 8 個語系;測試 +6:691 → 697。另有一處僅測試的修正(提交 7f8e250):e2e pipeline-delete 的拆卸位於 v1.48 之前的原生 confirm 路徑上;改為 API DELETE(fix(test): … —— CI 的 Playwright-e2e 為紅;並非產品回歸)。詳見 CHANGELOG.md。


[1.50.0] — 2026-05-18

WS2 #12 + #27 + #28 —— help 導覽無障礙。 一份 17 個章節、90+ 個標題的指南中,#/help 上 UX 稽核的三項發現,於 help.js 中修正。fix(a11y): help — single h1, labelled+filterable TOC, focus-on-anchor, back-to-top —— #28:文件 markdown 以自帶的 # Title 開頭,在頁首已提供規範 h1 的頁面上又產生了第二個 <h1>;現已剝除文章的所有 <h1>,使全頁恰有一個 h1,層級從 <h2> 章節乾淨起始。#27:TOC 的 <nav> 是無名地標(頁面上有兩個無標籤 <nav>);現帶 aria-label(help.toc),點擊 TOC 項目時焦點移至章節標題(tabindex=-1 + focus()),而非僅捲動視口。#12:長文件中無從查找;TOC 上方的 type=search 篩選器依標題文字即時收窄項目,捲動後出現帶 aria-label 的浮動「Back to top」按鈕,返回頂部並把焦點移回頁面 <h1>;其 scroll 監聽器在離開 #/help 的 hashchange 時移除。新增 2 個 i18n 鍵 × 8 個語系 —— help.tocFilter、help.backToTop;測試 +6:test: tests/help-nav-a11y.test.mjs。685 → 691。詳見 CHANGELOG.md。


[1.49.0] — 2026-05-18

WS2 #10 + #11 + #25 + #26 —— tracker 表格無障礙與排序。 #/tracker 上 UX 稽核的四項發現,於 tracker.js 中修正。fix(a11y): tracker headers, sortable table, localized fix labels, empty state —— #10:動作欄表頭是空字串,每列的 Report 按鈕缺少脈絡;現每個 <th> 皆帶 scope=col,動作表頭與 Score/PDF 表頭改為 i18n 鍵(原先為空或硬編碼英文),Report 按鈕取得帶公司名的 aria-label(<report> — <company>)。#11:tracker 沒有排序方式;Date / Score / Status 表頭現為 <th> 內可鍵盤操作的排序按鈕,帶 aria-sort(none/ascending/descending);sorted() 比較器(score 按數值,date/status 按 locale 比較)在分頁前執行,點擊切換方向並重置分頁器。#25:track.normalize/dedup/merge 是風險最高的破壞性控制項,卻在全部 8 個語系為同一英文(原地重寫 data/applications.md)—— 現已正確在地化,並新增 title 提示。#26:零列首次執行顯示與過度篩選清單相同的「no match」訊息;rows.length === 0 現渲染獨立的空狀態(標題 + 內文 +「Open pipeline」CTA)。新增 7 個 i18n 鍵 × 8 個語系 + 3 個重新在地化;測試 +6:test: tests/tracker-a11y-sort.test.mjs。677 → 683。詳見 CHANGELOG.md。


[1.48.0] — 2026-05-18

WS2 #8 + #22 —— pipeline:焦點陷阱確認 + 預覽無障礙。 #/pipeline 上 UX 稽核的兩項發現,於 pipeline.js 中修正。fix(a11y): pipeline UI.confirm() + live preview region —— #8:#/pipeline 的三個動作皆使用原生 confirm()(未做焦點陷阱):預覽面板的 Delete、每列的 ✕ 刪除、以及「Evaluate first」;現全部改走帶焦點陷阱的 UI.confirm()(v1.44.0 基礎設施)—— 兩個刪除 danger:true(Cancel 為預設),「Evaluate first」danger:false;pipeline.js 中已無任何原生 confirm()。#22:previewPane 沒有 live 角色,且 fetch 失敗被塞進 previewBody,渲染成誤導性的 <pre>「preview」;現為帶 aria-label 的 role=region aria-live=polite,失敗時另設 previewError 並渲染為獨立的 role=alert 區塊((重新)選擇時及刪除當前列時清除)。新增 4 個 i18n 鍵 × 8 個語系;測試 +5:test: tests/pipeline-confirm-preview.test.mjs。672 → 677。詳見 CHANGELOG.md。


[1.47.0] — 2026-05-18

WS2 #7 + #30 + #31 + #16 —— 未綁定標籤無障礙清掃。 UX 稽核的四項發現:表單控制項缺少程式化標籤(WCAG 1.3.1 / 3.3.2 / 4.1.2),現已全部綁定。fix(a11y): bind every swept form control to an accessible name —— #7 scan.js:dry-run 核取方塊與 company-select 下拉選單的標籤缺少 for;依既有 id 加上 htmlFor。#30 deep.js:company / role 輸入框存在未綁定標籤;加上 id + htmlFor(deep-company、deep-role)。#31 apply.js:url / jd 存在未綁定標籤;加上 id + htmlFor(apply-url、apply-jd)。#16 cv.js:主 markdown <textarea> 無可存取名稱;透過 aria-labelledby 綁定到可見的「Markdown」標題 —— 螢幕閱讀器名稱與螢幕標題一致,無新增 i18n 鍵。沿用 batch.js / mode-page.js 中已為標準的明確 label[for]↔control[id] 模式;無新增 i18n 鍵;行為零變更。測試 +5:test: 667 → 672。詳見 CHANGELOG.md。


[1.46.0] — 2026-05-18

WS2 #5 + #6 + #21 + #24 —— scan SSE 無障礙。 #/scan 上 UX 稽核的四項發現,於 scan.js 中修正。fix(a11y): scan SSE — live-log region, Stop, run-state, error banner —— #5:串流主控台現為 role=log aria-live=polite(+ aria-label、tabindex=0、可鍵盤捲動),並有一個獨立的視覺隱藏 assertive role=status 區域播報終態事件(完成 / 失敗 / 已停止)。#6:Stop 按鈕關閉進行中的 EventSource(es.close()),取消結果輪詢並重設狀態;僅在 scan 執行時顯示。#21:scan 執行時 Scan 按鈕被停用 + 置 aria-busy 並顯示 Stop,兩條串流路徑皆如此(單階段 streamTo 與多階段 runScanAll —— 後者僅在終態 done、final !== false 時結束本次執行)。#24:SSE 失敗不再只是 3.5 秒提示列;現由持久的 role=alert 橫幅顯示錯誤並附帶重試操作(重新呼叫上次的執行函式),下次執行時清除。新增 8 個 i18n 鍵 × 8 個語系;測試 +7:test: tests/scan-sse-a11y.test.mjs。660 → 667。詳見 CHANGELOG.md。


[1.45.0] — 2026-05-18

WS2 #3 —— #/config 分頁:完整的 WAI-ARIA Tabs 模式。 #/config 的三個分頁(API keys / Profile / Modes)曾是僅靠點擊啟用的樸素 <button class="tab-btn">:無 role、無 aria-selected、無鍵盤模型(UX 稽核 HIGH #3,WCAG 4.1.2 / 2.1.1)。fix(a11y): config.js tabs implement role=tablist/tab/tabpanel —— 現為帶 aria-label 的 role=tablist 容器;每個分頁 role=tab + id + aria-controls + aria-selected(於 activate() 中同步)+ 漫遊 tabindex(啟用 0 / 其餘 -1);面板 role=tabpanel + tabindex=0 + 跟隨啟用分頁的 aria-labelledby。完整鍵盤導覽:←/→/↑/↓(環繞)+ Home/End 既移動焦點又啟用。遺留 .tab-btn.is-active CSS 掛勾予以保留。新增 1 個 i18n 鍵 × 8 個語系(config.tablistLabel);測試 +7:test: tests/config-tabs-aria.test.mjs。另有一處僅測試的修正:fix(test): retarget 2 stale auto-pipeline smoke tests —— 兩個 v1.34 之前的 Playwright-e2e smoke 測試斷言一個儀表板「Auto-pipeline」按鈕在 v1.34.0 起不再開啟的瞬態模態(→ Router.go('/auto'));它們在獨立的 Playwright-e2e CI 作業中一直為紅。重新指向 #/auto 畫面。653 → 660。詳見 CHANGELOG.md。


[1.44.0] — 2026-05-18

WS2 #4 + #9 —— 父專案檔案破壞性覆寫前的焦點陷阱確認。 UX 稽核兩項 HIGH,皆為資料遺失:(#4)config.js 的 saveProfileRaw/saveModesRaw 未經確認即整體取代父層 config/profile.yml / _profile.md;(#9)tracker.js 的 Normalize/Dedup/Merge 未經確認即原地重寫父層 data/applications.md。fix(a11y/safety): UI.confirm() gate before whole-file parent overwrites —— 在 public/js/api.js 新增 UI.confirm(),一個重用既有 WAI-ARIA 模態基建的焦點陷阱對話框(_onClose 掛勾使 Esc / backdrop / × / Cancel 所有關閉路徑皆 resolve false;焦點預設落在 Cancel;回傳 Promise<boolean>;非原生 confirm())。三處破壞性呼叫現已在寫入前全部加上閘門。新增 8 個 i18n 鍵 × 8 個語系({op} 佔位符逐字保留);測試 +8:test: tests/confirm-gate.test.mjs,644 → 652。詳見 CHANGELOG.md。


[1.43.0] — 2026-05-18

使用者要求 —— career-ops-ui open + autostart 將瀏覽器帶到前景。 在 setup/run 之後,當瀏覽器已在執行時,裸 open/xdg-open 會讓儀表板分頁停留在背景,使用者得自行尋找。feat(cli): career-ops-ui open — open AND raise the dashboard tab —— 新的 scripts/open-dashboard.mjs 從 HOST/PORT 建構 URL(將 0.0.0.0 綁定改寫為 loopback),可選地等待 /api/health,開啟預設瀏覽器,然後強制將其帶到前景 —— macOS 以 osascript 啟用 Chrome/Brave/Edge/Safari/Arc/Firefox 中正在執行的那個,Linux 以 xdg-open+wmctrl,Windows 以 start。作為 career-ops-ui open 動詞公開(別名 dash、focus)。bin/start.sh 的 autostart 現委派給它,因此分頁會自動帶到前景;NO_OPEN=1 在 headless/CI 啟動時停用 auto-open。README ×8 + help §1 ×8 已更新;測試 +8:test: tests/open-dashboard.test.mjs,636 → 644。詳見 CHANGELOG.md。


[1.42.0] — 2026-05-18

WS2 修正 #2 —— 死路由 #/portals → config 深層連結。 #/portals 是一條未註冊路由,會渲染 404 檢視,儘管它是管理入口來源時合理的書籤/手動輸入 URL(UX 稽核 HIGH 第 2 項)。fix(router): #/portals 404 → alias to config + Regional-sources deep-link —— 在 router.js 的 ALIASES 中新增 portals: 'config'(與 settings→profile 相同的書籤穩定性模式),現在它解析為 config 檢視且 config 導覽項處於啟用狀態。當存在 Regional-sources 群組時,檢視(config.js)偵測 #/portals 雜湊,強制展開該 <details> 群組、捲動至可見區並將焦點移至其 summary(覆寫預設的 h1 焦點),使使用者恰好落在入口來源控制項上;絕不會僅憑別名渲染空的地區群組。help-bundle §5 × 8 新增一條快捷鍵提示;router 測試 +1:test(router): portals→config alias guarantee 加入 router.test.mjs,635 → 636。詳見 CHANGELOG.md。


[1.41.0] — 2026-05-18

WS2 —— 資深 UX/可用性稽核 + 橫切焦點管理修正。 一次 10 年以上經驗的啟發式稽核(Nielsen × WCAG 2.2 AA × 專案慣例)檢視全部 17 條路由,產出一份依嚴重度排序的 40 項發現佇列(.planning/.../UX-AUDIT.md);HIGH→MEDIUM→LOW 現按每次發行一項修正逐一交付。本次發行落地橫切 HIGH 第 1 名。修正:fix(a11y): move focus to the new view on every route change —— router.js render() 在每次 hashchange 替換 #content 卻從不移動焦點,因此鍵盤/螢幕閱讀器使用者停留在被銷毀的節點上而失去位置(WCAG 2.4.3 Focus Order / 4.1.3 Status Messages —— 橫切,影響全部 17 個畫面);新的 focusNewView(content) 聚焦新檢視首個 h1/.page-title(簡潔的 SR 播報 + 正確的焦點順序),必要時令標題可聚焦(tabindex=-1)並回退到 #content;跳過最初一次繪製以免與 skip-link 衝突;在成功與錯誤兩條渲染路徑皆接線;已即時驗證:導覽後 document.activeElement 為新檢視的 H1.page-title。測試:test(router): focus-management static guarantees —— router.test.mjs 新增 4 個案例(輔助函式已定義、標題目標 + content 回退、首繪跳過守衛、≥2 個呼叫點);631 → 635。詳見 CHANGELOG.md。


[1.40.0] — 2026-05-18

WS8.3 —— docs 實態化掃描 + career-ops-ui help 修正 + askSecret 強化。 修正:fix(cli): career-ops-ui help no longer leaks shell source —— 調度器以 sed -n '2,12p' 印出其標頭註解,但第 12 行(set -euo pipefail)是程式碼而非註解,因此 career-ops-ui help(以及未知動詞的用法文字)以一行多餘的 set -euo pipefail 結尾;在 help 與 *) 兩種情形下收窄為 2,11p(註解區塊);help 以 exit 0 結束,未知動詞以 exit 2 結束 —— 已驗證。fix(cli): scripts/init.mjs key entry never echoes —— v1.39.0 的後續將裝飾性的 readline 覆寫遮罩替換為真實的 raw 模式讀取器:setRawMode(true) + 帶緩衝的行,使輸入/貼上的金鑰位元組根本不會到達終端機(無 scrollback / tmux / 螢幕分享外洩);完整的 VT 跳脫 FSM 消化每個 CSI/SS3/OSC/DCS/SOS/PM/APC 序列,使方向鍵與功能鍵無法破壞密鑰;stdin 透過相依注入,因此非 TTY 退路在不碰觸全域的情況下做單元測試;反覆迭代至 AI 審查乾淨 LGTM。文件:README ×8 —— 舊的「一條命令安裝」章節替換為醒目的 「一條命令啟動並初始化」 章節(curl 單行加上顯式的 career-ops-ui CLI 鏈:clone → npm link → setup → init → doctor → run → help,供應方精靈說明,CI 形式 --provider --anthropic-key --yes,以及 LLM_PROVIDER 註記);8 個 README 徽章從陳舊的 v1.22–v1.24 / tests-461–474 實態化為 v1.40.0 / tests-631(e2e 徽章改為非數字以避免杜撰計數);help-bundle ×8 §1 —— 在快速上手手冊頂部(「A. Setup」之前)向全部 8 個語言新增「一條命令啟動 & init」標註;H2 章節配平保持(各 17 —— CI 閘門綠)。測試:test(init): non-TTY askSecret fallback —— provider-selector.test.mjs 新增一個 DI-stdin 案例,斷言 askSecret 在非 TTY 下委派給普通 ask()(trim 配平)且不更動共享全域;629 → 631。詳見 CHANGELOG.md。


[1.39.0] — 2026-05-18

WS8.2 —— LLM 供應方選擇器 + OpenAI/Codex 金鑰 + 互動式 init 精靈。 env-config 新增 LLM_PROVIDER(auto|claude|gemini)+OPENAI_API_KEY(密鑰)。llm.mjs 全部 6 個 gate-site 經 _provGate() 用 providerOrder();auto 行為不變。#/config 新增 select+欄位。scripts/init.mjs 現為真實精靈(經驗證路徑寫 parent .env)。7 測試。622 → 629。README ×8/規範文件 fold = WS8.3/WS10。詳見 CHANGELOG.md。


[1.38.0] — 2026-05-17

WS8.1 —— 統一 CLI 調度器 + doctor 動詞。 bin/career-ops-ui.sh 路由 setup/run/doctor/init/help。scripts/doctor.mjs 重用確切的 /api/health 引擎(createApp 程序內 → 終端報告);僅當所有必需檢查通過才 exit 0。docs/sdd + help §1 ×8。6 測試。616 → 622。README ×8 = WS8.3。詳見 CHANGELOG.md。


[1.37.0] — 2026-05-17

WS7 —— git 工作流 pre-commit AI 審查。 確定性底線(fail-HARD):攔截 staged .env/金鑰、diff 中金鑰模式、staged 視圖中的 .also(、node --check 失敗。AI 層(fail-SOFT):CLI 可用且 AI_REVIEW != off 時跑 claude -p。.githooks/pre-commit + prepare 接 core.hooksPath。禁用 --no-verify。docs/sdd。6 測試。610 → 616。詳見 CHANGELOG.md。


[1.36.0] — 2026-05-17

WS6.3 —— Modes 分頁:原始區塊 → 分區編輯器。WS6 完成。 modes/_profile.md 按 ## 區塊編輯(每標題一個可折疊 textarea)。伺服端 splitProfileSections 位元組精確;PUT { sections } 僅合併指定區塊 —— 前言+其他區塊+順序按位元組保留。未知標題 → 400。raw 路徑不變。i18n 5 鍵 ×8。help §2 ×8。新增 6 測試。604 → 610。WS6 收尾。詳見 CHANGELOG.md。


[1.35.0] — 2026-05-17

WS6.4 —— Profile 陣列編輯器 + WS6.2 API-keys 稽核。 PUT /api/profile 接受 { arrays }(可與 { fields } 組合):Target roles/Superpowers(清單)、Archetypes(name/level/fit)、Proof points(name/url/hero-metric)。相同 merge-not-replace;空列丟棄;空清單刪除鍵。#/config 新增 4 個增刪編輯器。i18n 6 鍵 ×8。稽核:KNOWN_KEYS ≡ FIELDS,無 gap。新增 7 測試。597 → 604。詳見 CHANGELOG.md。


[1.34.0] — 2026-05-17

WS5 —— 一鍵 Auto-pipeline 頁面(#/auto)。 模態升級為獨立可連結頁面。一鍵執行 驗證→擷取→評估→儲存報告→追蹤器(SSE)。無障礙 stepper、深連、無 key 手動模式、可連結 #/auto?url=…&go=1。側欄入口;dashboard ✨ 按鈕改到此處。i18n 14 鍵 ×8。help §1 ×8 + README ×8。新增 8 測試。589 → 597。詳見 CHANGELOG.md。


[1.33.0] — 2026-05-17

WS4 —— career-ops 1.8.0 對等稽核 + location_filter。 父 scan.mjs 新增 location_filter(#570);web-ui 的程序內 scanner 不委派給它,故未流通。新增 server/lib/location-filter.mjs 逐字複製語義,接入兩個 scanner。help §5 ×8。新增 8 測試。581 → 589。詳見 CHANGELOG.md。


[1.32.0] — 2026-05-17

#/config Profile 分頁 —— 原始 YAML 區塊 → 逐欄位表單(WS1)。 3 個可折疊分區(候選人/敘述/薪酬),14 個純量路徑。逐欄位儲存合併進 config/profile.yml:archetype、proof point 與自訂鍵原樣保留。Advanced 保留 raw-YAML 退路(保留註解)。23 個 i18n 鍵 ×8。新增 7 測試。574 → 581。詳見 CHANGELOG.md。


[1.31.0] — 2026-05-17

與 career-ops 1.8.0 同步 — #/batch 暴露 --model + --start-from。 父專案 1.7.1 → 1.8.0;batch-runner.sh 新增 --model NAME(#504)與 --start-from N。web-ui 在 #/batch 暴露(模型、起始 # 輸入)並在伺服端做 defense-in-depth 驗證。i18n ×8。新增 7 個測試。567 → 574。完整詳情見 CHANGELOG.md。


[1.30.0] — 2026-05-14

#/scan 結果分頁器 — 取代 v1.12 的「顯示前 200(共 N)」截斷。

v1.30 之前,掃描結果表會被硬截斷為前 200 列過濾後的資料,底部一行「Showing first 200 of N」提示,201..N 列無法從 UI 存取。v1.30.0 將上限替換為 UI.paginate(與 #/tracker / #/reports / #/activity 同一 helper)。PAGE_SIZE = 200 保持原有視覺密度;boost-to-top 排序在跨頁時仍穩定(先對完整集合排序,再分頁);任意篩選變化時自動重置為第 1 頁。已棄用的 i18n key scan.shownTop 被移除(8 個語系)。tests/scan-paginator.test.mjs 新增 9 個案例(7 個靜態 canary + 含 6 個邊界條件的純邏輯表 1 個 + 摘要計算 1 個)。558 → 567 單元 + 驗收測試(+9)。完整細節見 CHANGELOG.md。


[1.29.2] — 2026-05-14

熱修復:🌐 Scan 在 source=both 模式下只跑了 EN 階段,RU 階段被靜默丟棄。

SSE 客戶端(public/js/api.js:156)在第一個 done 事件就關閉了 EventSource,但伺服器在 source=both 模式下每階段各發一個 done。RU 階段剛啟動就被取消。修復:伺服器在每個 done 上標記 final: true|false,客戶端僅在 final !== false 時關閉。向後相容 — 不設定 final 的單階段生產者繼續保持原行為。547 → 558 單元 + 驗收測試(+11 新增)。完整細節見 CHANGELOG.md。


[1.29.1] — 2026-05-14

為 help-bundle §5 的 8 個語系全部加入面向使用者的 5 個 RU 入口網站設定詳盡指南。

在 §5(Portals & sources)內新增 ### 子節「設定俄文入口網站 — 詳細設定指南」:5 個來源的清單表(含驗證與地理限制)、定位與編輯 portals.yml 的分步說明、完整的 5 來源 YAML 範例、與 negative 清單的衝突及其修復範例、臨時停用某個來源的方法、透過 🌐 Scan 與 SSE 記錄驗證設定的方法。§17(v1.29.0 上線)涵蓋開發者流程,§5 v1.29.1 涵蓋最終使用者流程。540 → 547 單元 + 驗收測試(+7 新增)。完整細節見 CHANGELOG.md。


[1.29.0] — 2026-05-14

俄文職位入口網站掃描器從 2 個來源擴展到 5 個;registry + 動態下拉框;新增 §17「如何新增新入口網站」。

  • 3 個新 RU adapter: Trudvsem(政府 open-data API,無認證、無地理閘),GetMatch 與 GeekJob(HTML 抓取,防禦式解析器 — 解析失敗回傳 [],健康 200 絕不 throw)。
  • Source registry 位於 server/lib/sources/registry.mjs — 由 dispatcher + endpoint + dropdown 共同消費的單一事實來源。v1.29 之前列表硬編碼在三處。
  • 新增 endpoint GET /api/scan/sources(Cache-Control: max-age=60)— SPA 在掛載 #/scan 時動態重繪來源篩選下拉。
  • 新增 §17 覆蓋 8 個語系:「如何新增職位入口網站來源」(adapter 範本、registry 條目、dispatcher、mock 測試、portals.yml)。
  • russian_portals.sources 預設值從 ["hh", "habr"] 改為 5 個來源;如果你的 portals.yml 已明示列出 sources:,需手動加入 3 個新條目。
  • 測試:520 → 540(+20)。完整細節見 CHANGELOG.md。

[1.28.1] — 2026-05-14

熱修復:?query 雜湊導致 router 404;從 health 移除 HH_USER_AGENT 列。

v1.28.1 之前,Router.go('/evaluate?url=…') 產生的 hash 經 split('/') 後第一段是字面量 "evaluate?url=…",永遠不會比對到已註冊路由 → __not_found__(404)。一行修復:在依名稱拆分前先 hash.split('?')[0]。覆蓋兩個已回報的點擊:#/pipeline → ▶ 與「App settings → Modes」。/api/health 中可選的 HH_USER_AGENT 列被移除(俄羅斯境外 403 提示仍保留在 help-bundle §16 中,掃描時 stderr 也仍會提示)。515 → 520 單元 + 驗收測試(+5 新增)。完整細節見 CHANGELOG.md。


[1.28.0] — 2026-05-14

**文件對齊 + #/batch 新增 --max-retries N 控制項。**關閉 qa/QA-PROMPT-docs-vs-app.md 中提出的兩個未決 issue。

  • Issue #2 — #/batch 現在提供「Max retries」數字輸入(1–10),僅在勾選「Retry failed」時啟用。伺服器端使用 parseInt 並驗證 1≤N≤10,超出範圍的值會靜默丟棄;未啟用 --retry-failed 時 --max-retries 旗標被忽略。tests/batch-max-retries.test.mjs 中 7 個測試案例。新增 2 個 i18n key × 8 語系。
  • Issue #1 — 8 個 help-bundle 與 8 個 README 的 AI CLI 列表與 career-ops.org/docs 正典(Claude Code · Codex · OpenCode · Qwen CLI)對齊,並附本地化一句:「其他 Claude 相容 CLI 也透過相同的斜線指令介面運作」。README 中關於 web-ui 自身 shim 檔案的「Multi-CLI」項目保持不變(那是另一種 surface)。tests/canonical-docs-coverage.test.mjs 中新增 2 個回歸 canary。
  • 506 → 515 單元 + 驗收測試(+9 新增)。Playwright 32/32 無變化。完整細節見 CHANGELOG.md。

[1.27.0] — 2026-05-14

外觀 + 無障礙打磨:去重側邊欄 #/dashboard 入口。

側邊欄中,品牌標誌(<a class="logo" href="#/dashboard">)與第一個導覽項目指向同一路由。螢幕閱讀器重複念出「Dashboard」兩次,鍵盤使用者多出一個無意義的 tab 焦點。標誌區塊現在改為普通的 <div class="logo">,唯有導覽項目保留為 #/dashboard 的唯一連結。506 / 506 單元 + 32 / 32 Playwright — 無變化。完整細節見 CHANGELOG.md。


[1.26.1] — 2026-05-14

WCAG 2.5.5 熱修復 — 恢復 .btn 最小高度 44 px.

v1.26.0 中 .btn 的 min-height: 44px 宣告遺失,標頭按鈕渲染為 39-41 px(違反 WCAG 2.5.5)。v1.26.1 恢復 44 px 下限 + flex-shrink: 0 + line-height: 1.2。502 → 506 unit,Playwright 32/32 不變。詳細見 CHANGELOG.md。


[1.26.0] — 2026-05-14

測試金字塔 + 行覆蓋率 ≥ 93 %.

依 v1.25 待辦事項採用四級測試金字塔(unit → functional → acceptance → e2e)。新增 22 個測試,涵蓋 v1.25 的最大空白(jds.mjs 61.64 % → 100 %,auto-pipeline 拒絕路徑)。新增 tests/acceptance/ 目錄用於跨端點使用者旅程測試。480 → 502 unit + acceptance,Playwright 32/32 不變。完整細節見 CHANGELOG.md 和 docs/architecture/TESTING.md。


[1.25.0] — 2026-05-14

Auto-pipeline 手動模式短路 + 儀表板外觀微修 + CHANGELOG 對齊回填。 一次解決 G-014(auto-pipeline 忽略 mode: 'manual')、G-012(CHANGELOG 對齊漂移 — 6 個 locale 落後兩個版本),以及儀表板 ✨ ✨ 雙字符外觀問題。G-003(README.cn.md 重新命名)其實已自然關閉 — 此版本庫只存在 README.zh-CN.md。G-005(A-G → A-F 報表區塊重排)需要父專案協同提交,本版本持續延後。

🛡️ G-014 — Auto-pipeline mode: 'manual' 短路

  • fix(auto-pipeline): G-014 — honour mode:'manual' short-circuit (server/lib/routes/auto-pipeline.mjs:158-195) — v1.25 之前,該路由不論如何都會呼叫 LLM。傳入 mode: 'manual'(自 v1.10.2 起 /api/evaluate 已支援的旗標)被靜默忽略,請求會在 Anthropic 端卡住 1–3 分鐘。新版 handler:
    • 同時接受 mode 與 evalMode 以維持向後相容。任一參數值為 'manual' 即觸發短路。
    • 仍會發出全部 5 個 SSE 階段,但 status 為 'done' 或 'skipped'。不發 fetch、不呼叫 LLM、每次請求 0.05 美元的開銷也省下。
    • done 事件 payload 攜帶 { mode: 'manual', prompt: <buildEvaluationPrompt scaffold>, message } — SPA 可比照既有 /api/evaluate 的手動提示卡片渲染。
  • 同步關閉 HOST=0.0.0.0 下的 DoS 風險:過去即使有 llmRateLimit 限制每 IP 每 60 秒 10 次,10 名攻擊者 × 10 次 = 每分鐘 50 美元的 Anthropic 燒帳。短路在進入速率限制計數之前就先生效。
  • 測試 — tests/auto-pipeline-manual-mode.test.mjs:3 個測試確認(1)mode: 'manual' 在 2 秒內回傳並包含全部 5 個 step key、(2)即使設定了 ANTHROPIC_API_KEY 短路依然生效(這正是原始症狀)、(3)傳統 evalMode: 'manual' 呼叫端持續運作。

📝 G-012 — CHANGELOG 對齊回填(6 個 locale × 2 個遺漏版本)

  • docs(changelog): backfill v1.23.0, v1.24.0, v1.24.1, v1.25.0 in 6 lagging locales — v1.25 之前只有 EN 包含 v1.23–v1.24;RU 落後 1 個版本,其餘 6 個 locale 落後 2 個版本。v1.25 派發平行翻譯 agent(沿用 v1.23 的模式)將四個條目同時落入 CHANGELOG.{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.md。RU 補上 v1.24.0 + v1.24.1 + v1.25.0(它在 v1.23 週期已取得 v1.23.0)。
  • feat(ci): scripts/check-changelog-parity.mjs gate — 若任何 locale CHANGELOG 的最新條目舊於 EN 規範版,即視為建置失敗。已串接至 npm run test:ci。此前的 G-012 漂移在跨越 EN 邊界的當下就會自我攔截。

✨ 外觀微修 — 儀表板雙字符去重

  • fix(dashboard): dedup ✨ glyph in auto-pipeline button label (public/js/lib/i18n-dict.js:219) — dash.autoPipeline 在每個 locale 字串裡都帶了開頭的 ✨,而 public/js/views/dashboard.js:58 又額外加了一個 ✨。結果:按鈕渲染成 ✨ ✨ Auto-pipeline …。v1.25 移除每個 locale DICT entry 的開頭字符;view 端的前綴成為唯一來源。同次稽核掃過剩餘的 i18n 資料,未發現其他雙字符模式。

🚫 延後至未來版本

  • G-005 — 報表區塊 A-G → A-F 對齊規範 career-ops.org/docs — 需要在父專案 santifer/career-ops 協同提交(改寫 modes/oferta.md,輸出 A=Role、B=CV-match、C=Strategy、D=Comp、E=Personalization、F=STAR — 將 C-Risks/G-Legitimacy 從獨立區塊移除)。v1.25.0 已讓 web-ui 端準備好接收新 schema(reports.js 自 v1.13 起即接受任意區塊字母)。等待父專案 + 子專案能同步出貨的下個發版視窗。
  • G-003 — README.cn.md → README.zh-CN.md 重新命名 — v1.25 整備期已驗證:此版本庫實際只有 README.zh-CN.md(worktree 任何位置都不存在孤兒 README.cn.md)。G-003 的觀察已過時。

🧪 測試

  • 477 → 480 個單元測試(+3 來自 PR-B 的 auto-pipeline-manual-mode.test.mjs)。
  • 32/32 Playwright 不變。
  • npm run test:ci 現在執行 npm test + check-no-also-leftovers.mjs + check-changelog-parity.mjs。

驗證

$ npm run test:ci
# 480 / 480
# ✓ no .also( leftovers in views/
# ✓ CHANGELOG parity: all 8 locales at v1.25.0

# G-014 — 即使設定 ANTHROPIC_API_KEY,manual 模式仍於 2 秒內回傳:
$ ANTHROPIC_API_KEY=sk-ant-test PORT=4317 npm start &
$ sleep 3
$ time curl -sS -X POST -H 'Content-Type: application/json' \
    -d '{"url":"https://job-boards.greenhouse.io/anthropic/jobs/x","mode":"manual"}' \
    http://127.0.0.1:4317/api/auto-pipeline | head -20
# real  0m0.1xx s  (此前為 1-3 分鐘)
# event: start … event: step (×5) … event: done {"mode":"manual","prompt":"…"}

# G-012 — 每個 locale 的 CHANGELOG 都帶有 v1.25.0 條目:
$ grep -c '^## \[1.25.0\]' CHANGELOG*.md
# 8 個檔案,每個 → 1

# 外觀微修 — 儀表板字符:
$ grep "dash.autoPipeline" public/js/lib/i18n-dict.js
# 任何 locale 的值皆已無開頭 ✨(view 提供唯一字符)

破壞性變更

無。mode: 'manual' 為 opt-in;傳統 evalMode: 'manual' 呼叫端維持原狀運作。

範圍外(v1.26+)

項目 備註
G-005 — A-F 報表區塊重排 需父專案協同提交(santifer/career-ops 改寫 modes/oferta.md)。
QA 場景 31 視覺 子測試的實機執行 需瀏覽器驅動 agent(Claude Cowork)。Playwright smoke 已部分涵蓋。
i18n-dict.js 超過 400-LOC 目標 屬翻譯 fixture — 依政策豁免。在無 bundler 的情境下拆分只會新增 HTTP 請求。

[1.24.1] — 2026-05-14

Hot-fix:8 個 locale 的 #/config 全數崩潰(G-015)。

🚑 緊急 hot-fix

  • fix(config): G-015 — replace removed Element.prototype.also call in config.js (public/js/views/config.js:371) — v1.22.0 的 N-2 移除了 Element.prototype.also 全域 monkey-patch 並將 cv.js 改為 free-statement 寫法,但漏掉了 config.js。後果:每個 locale 首次進入 #/config 時都會以 c(...).also is not a function 崩潰。v1.24.1 套用與 cv.js:188-201 相同的遷移模式 — 將樹根抽至 const root = c(...),獨立執行 activation 區塊,最後 return root;。

🛡️ CI 守門

  • feat(ci): scripts/check-no-also-leftovers.mjs sweep — 走訪 public/js/views/ 下的每個檔案,只要還存在 .also( 呼叫點(允許註解中的引用),即視為建置失敗。已串接至新增的 npm run test:ci script。日後若有人意外撤回 monkey-patch 移除動作,也無法靜默重現同一個迴歸。

🧪 測試

  • test: tests/config-view-syntax.test.mjs — 三道防線:
    • 透過 node:vm.Script 解析 config.js(不需 Playwright 即可捕捉語法層級迴歸)
    • 斷言註解之外無 .also( 殘留
    • 斷言 const root = c(...) / return root; 的遷移錨點存在
  • 474 → 477 個單元測試(+3)+ 32/32 Playwright 不變。

驗證

$ npm run test:ci
# 477 / 477
# ✓ no .also( leftovers in views/

# 瀏覽器 smoke:
$ open http://127.0.0.1:4317/#/config
# → 正常渲染,無 "is not a function" 卡片。每個 locale 等同。

範圍外(延至 v1.25)

  • G-014、G-012、G-005、G-003 — 詳見下方 v1.25.0 條目的批次處理。

[1.24.0] — 2026-05-14

Help-bundle 內容深度刷新 + QA 場景 31 實機執行 + RU CHANGELOG 全文翻譯。 一次關閉 v1.23.0「範圍外」表格延後至 v1.24 的兩項:依 5 個規範 career-ops.org/docs URL 對全部 8 個 help bundle 做完整內容深度刷新(自 v1.11.x 以來只覆蓋了 URL 出現位置),以及在實機伺服器上執行 QA 場景 31(原列為「需瀏覽器 agent + LLM 憑證」 — 實測發現 6/6 子測試可用 curl + grep 達成,只有視覺類子測試需要瀏覽器)。

📖 Help-bundle 內容深度刷新

  • docs(help): refresh en.md from 5 canonical career-ops.org/docs URLs (docs/help/en.md) — v1.24 之前 EN bundle 共 1113 行,在 front-matter 列出 5 個規範 URL,卻未於主文展開。v1.24 透過 WebFetch 取得全部 5 個 URL,並深化對應的 H2 區段:
    • About career-ops(front-matter) — 補上原則(data sovereignty、AI-agnostic、human-controlled)、「What career-ops is NOT」區塊;概念清單從 6 列擴增至 10 列(新增 Proof points、JD store、Interview-prep、Batch additions)。
    • §5 Portals — 補入規範 bootstrap cp templates/portals.example.yml portals.yml,釐清 tracked_companies 各筆的必填 vs 選填欄位。
    • §7 Scan — 為 Option A 加上「無 AI token 消耗」標註,後續指令清單(apply / contacto / deep / tracker)。
    • §14 Apply checklist — 拆分為 SPA checklist 模式 vs Manual-vs-Playwright-assisted vs Full CLI flow(規範的 8 步驟,從 /career-ops apply <company> 到 Submitted. 並含 Evaluated → Applied 自動轉移);batch evaluate 子節新增 TSV schema 表格 + 4 個旗標完整說明 + merge-tracker.mjs --dry-run;Playwright Setup 子節列出安裝指令、MCP 註冊、替代 .claude/settings.local.json、預設 headless 註記。
  • 保留 16-H2 區段對等(CI 測試 help-ui.test.mjs::section-parity 斷言全部 8 個 locale 皆精確有 16 個 H2 區段)。
  • 5 個規範 URL 各自至少出現 2 次(由 CI 測試 canonical-docs-coverage.test.mjs 強制執行)。v1.24 後逐 URL 計數:what-is-career-ops × 4、scan-job-portals × 5、apply-for-a-job × 3、batch-evaluate-offers × 5、set-up-playwright × 3。
  • docs(help): translate the v1.24 deepening to 7 non-EN locales — 派發 7 個平行翻譯 agent。每個目標 locale(es / pt-BR / ko-KR / ja / ru / zh-CN / zh-TW)取得與 EN 逐節對齊的刷新版 bundle,逐字保留程式碼區塊 / URL / 檔案路徑 / 按鈕標籤(📁 Upload CV / 🌐 Scan now / ▶ Evaluate / 📄 Generate PDF / 💾 Save)與英文縮寫(CSP、SSRF、TOCTOU、WCAG、ATS、JD、SSE、REST、API),並以目標語言的出版品級技術風格翻譯深化內容。

🧪 QA 場景 31 — 實機執行(6/6 PASS)

  • docs(qa): append last-verified live-execution log to qa/claude-cowork-browser-test-prompt.md — v1.24 之前,場景 31 已寫成文件但從未對實機伺服器跑過(列為「需瀏覽器 agent + LLM 憑證」)。v1.24 對 http://127.0.0.1:4317 執行全部 6 個子測試:

    子測試 描述 狀態
    31.1 help bundle 中的 score thresholds ✅ PASS(docs/help/en.md 中 4.5 × 3、4.0 × 9、3.5 × 6 次)
    31.2 Scan workflow 端點 ✅ PASS(/api/stream/scan-{en,ru} + /api/scan-ru/config → 404;/api/scan/regional/config → 200)
    31.3 /api/apply-helper checklist ✅ PASS(回應含 career-ops apply + auto-submit 警示)
    31.4 /api/batch 端點 ✅ PASS(keys 為 [exists, runnerExists, raw, rows, additions])
    31.5 Playwright 可用性 ✅ PASS(/api/health 回報 Playwright (parent node_modules) ok: true, value: installed)
    31.6 help bundle URL 覆蓋率(5 個 URL × 8 個 locale) ✅ PASS(40 / 40 ✓)

    視覺類子測試(需瀏覽器)在 QA prompt 中另行標註 — 仍可透過 Claude Cowork 或 npm run test:e2e:browser 執行。

🌐 RU CHANGELOG 全文翻譯(M-9 後續)

  • docs(translate): CHANGELOG.ru.md retry agent — full body translation (CHANGELOG.ru.md) — v1.23.0 出貨時,RU CHANGELOG 的重試 agent 仍在進行中(它曾因 socket 錯誤崩潰一次並被重新派發)。v1.24 收下 agent 產出的 1542 行全文翻譯:每個條目 v1.23.0 → v1.6.0 皆有出版品級的俄文正文,不再保留 EN 過渡填補。風格規範比照 v1.22.0 README 品質刷新:以 “функциональность” / “возможности” / “поведение” 取代生硬的 “функционал”;以 “через” / “с помощью” 取代 “при помощи”;主動語態優於被動;以 “эндпоинт”、“лимит запросов”、“состояние гонки”、“санитайзинг” 作為規範用詞;英文縮寫(TOCTOU、CSP、SSRF、WCAG、ATS、JD、SSE、REST、API)保留原文。

🧪 測試

  • 474 / 474 單元 + 20 / 20 smoke E2E + 32 / 32 Playwright。零行為差異;每一項 help-bundle 的 CI 斷言(16 個 H2 區段 × 8 個 locale、5 個 URL × ≥ 2 次提及、內容下限)持續綠燈。

驗證

$ npm test                            # 474 / 474

# Help-bundle 深化:
$ wc -l docs/help/en.md
# ~1270 行(原 1113 — 深化,不膨脹)

$ for url in what-is-career-ops scan-job-portals apply-for-a-job \
             batch-evaluate-offers set-up-playwright; do
    echo -n "$url: "
    grep -c "$url" docs/help/en.md
  done
# what-is-career-ops: 4
# scan-job-portals: 5
# apply-for-a-job: 3
# batch-evaluate-offers: 5
# set-up-playwright: 3

# 場景 31.6 — 40/40 URL 覆蓋率:
$ for lang in en es pt-BR ko ja ru zh-CN zh-TW; do
    echo -n "$lang: "
    for url in what-is-career-ops scan-job-portals apply-for-a-job \
               batch-evaluate-offers set-up-playwright; do
      curl -sS "http://127.0.0.1:4317/api/help/$lang" \
        | python3 -c "import sys,json; print(json.load(sys.stdin).get('markdown',''))" \
        | grep -q "$url" && echo -n "✓ " || echo -n "✗ "
    done
    echo
  done

破壞性變更

無。

範圍外(v1.25+)

項目 備註
場景 31 視覺 子測試的實機執行 需瀏覽器驅動 agent(Claude Cowork 或 npm run test:e2e:browser)。curl 單一執行模式不在範圍內;Playwright smoke 已涵蓋。
RU CHANGELOG 舊條目(v1.5.x 及之前)的正文翻譯 重試 agent 僅覆蓋 v1.6.0 起。pre-v1.6 條目(v1.5.x 等)— 若曾經存在 — 仍維持既有內容。
未來 SPA 變更後對儀表板截圖的視覺迴歸 scripts/capture-dashboard-screenshots.mjs 可重新產生各 locale 的 PNG;目前尚未有自動 diff。

[1.23.0] — 2026-05-14

i18n 拆分 + 連線橫幅 CI hot-fix + 各 locale 儀表板截圖 + 既有 backlog 完整收尾。 出貨 v1.22.0「範圍外」表格列為 v1.23 的三項(M-9 各 locale CHANGELOG 正文、N-1 i18n.js LOC 拆分、help-bundle 內容稽核),並包含修補 v1.22.0 之後讓 main 分支 CI 變紅的 smoke E2E hot-fix。

🚑 CI hot-fix — 連線橫幅恢復

  • fix(client): reset health-poll cadence + visibilitychange eager re-check (public/js/api.js:21-91) — v1.22.0 的 M-6 指數退避方向正確(3 秒 → 6 秒 → 12 秒 → 上限 15 秒,原始上限為 60 秒),但飛行中的 setTimeout 被鎖定在上一次設定的 delay。當伺服器於 t=0.1 被殺、第一次 ping 在 t=3 失敗,delay 會加倍為 6,下一次恢復探測要等到 t=9 才會發出。smoke E2E 的「Flow 2a: connection banner appears on server down, hides on recovery」只等 4 秒,於 main 上轉紅。

    v1.23.0 重塑輪詢迴圈:

    • 追蹤 _healthHandle,讓 setConnectionState(lost=true) 能 clearTimeout 並以 _HEALTH_MIN 重新排程。第一次恢復探測現在保證在離線 3 秒內發出,不受先前排入的 delay 影響。
    • _HEALTH_MAX 從 60 秒降至 15 秒。後臺分頁面對已死伺服器,使用者切回時仍可在一個輪詢週期內恢復;頻寬節省依然顯著。
    • document.addEventListener('visibilitychange') 於分頁重獲焦點且 connectionLost === true 時積極重檢 — Cmd-Tab 回來不必等下個退避 tick。

🧹 N-1 — i18n.js 拆分(超過 400-LOC 目標)

  • refactor(client): split DICT into i18n-dict.js (data) + i18n.js (logic) — v1.23 之前 public/js/lib/i18n.js 共 639 LOC。大宗(23-586 行)是 DICT 翻譯表 — 純結構化資料。v1.23.0 將其抽至 public/js/lib/i18n-dict.js(578 LOC,依 CLAUDE.md「豁免於上述限制:generated files、migrations、test fixtures、lock files、vendored code」— 翻譯表屬 fixture 性質,符合豁免),留下 public/js/lib/i18n.js 為 86 LOC 的純模組邏輯(遠低於 400-LOC 目標)。
  • 載入契約:i18n-dict.js 寫入 window.__I18N_DICT = { … },接著 i18n.js 在既有 IIFE 內讀取。public/index.html 依序載入 — 先 i18n-dict.js 再 i18n.js — 確保 IIFE 在建構時即看到填妥的 DICT。Missing-dict fallback:每一次 t() 呼叫都會回傳 inline fallback 或裸 key,讓設定錯誤大聲浮現但不至於讓 SPA 崩潰。
  • 測試管線更新:tests/i18n-coverage.test.mjs、tests/help-ui.test.mjs、tests/canonical-docs-coverage.test.mjs 現皆於測試 VM context 中執行兩個檔案(或在正則掃描時串接兩者原始碼),保留所有既有斷言。

🌐 M-9 — 各 locale CHANGELOG 正文翻譯

  • docs(translate): 7 non-EN CHANGELOG files end-to-end — v1.23 之前,CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md 自 v1.13.0 起的每個條目都帶著 EN 正文的過渡填補與引導讀者去看 EN 規範版的 footer。v1.23.0 派發 7 個平行翻譯 agent — 每個 locale 一個 — 將每個正文改寫為目標語言的出版品級技術風格。移除過渡填補。各 locale 一律逐字保留程式碼區塊、檔案路徑、URL、commit-message 字串(fix(security): B-1 — …)、環境變數與連結標籤。

🖼️ 各 locale README 對接專屬儀表板截圖

  • docs(readme): wire each locale README at its locale-specific PNG — v1.23 之前只有 README.pt-BR.md 指向 dashboard-pt-BR.png,其餘 6 個非 EN README 仍指向 dashboard-en.png。v1.22.0 週期由 scripts/capture-dashboard-screenshots.mjs 產出的截圖已置於 images/ 但未被引用。v1.23.0 將每個 README.{es,ja,ko-KR,ru,zh-CN,zh-TW}.md 第 14 行更新為對應的 dashboard-<locale>.png。

🧪 測試

  • 與 v1.22.0 同為 474 / 474 單元 + 32 / 32 Playwright。Smoke E2E 現為 20 / 20(v1.22.0 後在 main 上曾因橫幅恢復迴歸成 19/1 fail;v1.23.0 的重排程修復收尾)。
  • 為了 i18n 拆分重接三個既有測試。零新增測試檔案;零刪除斷言。

驗證

$ npm test
# 474 / 474

$ npm run test:e2e
# passed: 20    failed: 0    (v1.22.0 main 時為 19/1)

$ wc -l public/js/lib/i18n.js public/js/lib/i18n-dict.js
#       86 public/js/lib/i18n.js          ← 邏輯,低於目標
#      578 public/js/lib/i18n-dict.js     ← 資料 fixture,豁免

$ grep -h 'dashboard-' README*.md | sed -E 's/.*(dashboard-[^)]+).*/\1/' | sort -u
# dashboard-en.png    (僅 README.md)
# dashboard-es.png    dashboard-ja.png
# dashboard-ko-KR.png dashboard-pt-BR.png
# dashboard-ru.png    dashboard-zh-CN.png  dashboard-zh-TW.png

# CHANGELOG 翻譯 sanity check:每個 locale 檔案均 > 200 行原生語言內容
$ wc -l CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md | grep -v total

破壞性變更

無。public/index.html 現在載入兩個 script(原本一個)— 任何透過 CDN 提供 SPA 的部署需同步取得 i18n-dict.js;載入順序由 index.html 的 <script src> 標籤順序強制。執行階段 fallback(空 DICT → t() 回傳 inline EN fallback)可避免新檔案缺失時硬崩潰。

範圍外(v1.24+)

項目 備註
依 career-ops.org/docs 對 help-bundle 做內容深度刷新(相對於僅 URL 覆蓋) 5 個規範 URL 自 v1.11.x 起已在每個 locale 的 help bundle 中出現,QA prompt 場景 31.6 驗證了覆蓋率。內容深度刷新為 v1.24+ 候選。
對實機伺服器執行 QA 場景 31 需瀏覽器 agent + 實機 LLM 憑證。v1.24 候選。
新增 mode-page hint 段落的逐元件觸控目標掃描 v1.22.0 M-1 新增的 <p class="field-hint"> 元素尚未在全部 8 個 locale 對 WCAG 2.5.5 最小高度做驗證。

[1.22.0] — 2026-05-14

M / L / N 級待辦清空 + 文件對齊 + 翻譯品質 pass。 整個 v1.20.1-BACKLOG.md 中的 medium 及以下層級全部於一次發布中交付:九項 M、五項 L、兩項 nits。另對五份規範 career-ops.org/docs 指南進行了文件對齊稽核,刷新 .claude/ 與 .github/ 下的系統提示,並對 7 個非英文 locale 的 README 進行了品質刷新。

🛡️ 安全性強化(深度防禦)

  • fix(security): M-4 — 具實體感知能力的 stripDangerousMarkdown (server/lib/security.mjs) — v1.22 之前的正規表達式僅將 <script>、javascript:、on*= 視為字面子字串匹配。&lt;script&gt;、java&#115;cript: 與 <img src="data:image/svg+xml,<svg onload=…>"> 都能繞過。新版淨化流程會在執行 strip 正規表達式之前先解碼 &lt;、&gt;、&amp;、&quot;、數值實體(&#NN;)與十六進位實體(&#xHH;)。由 tests/cv-xss-bypasses.test.mjs 中 11 個測試驗證。真正的防禦仍仰賴客戶端 UI.md escape-first 流程;此處強化的是靜態儲存的檔案。

  • fix(security): L-2 — batch runner 使用 bash --noprofile --norc (server/lib/routes/batch.mjs:108) — spawn('bash', [PATHS.batchRunner, ...]) 過去會繼承使用者 ~/.bashrc。惡意 rc 檔案可能影響執行結果。現改為 spawn('bash', ['--noprofile', '--norc', PATHS.batchRunner, ...])。

🔒 韌性

  • fix(client): M-6 — 健康檢查 ping 採用指數退避 (public/js/api.js:22-48) — 離線狀態下的 poller 過去一夜會對失效伺服器發出 28,800 次 fetch。現改為 3 秒 → 6 秒 → 12 秒 → 24 秒 → 60 秒;首次 2xx 恢復後重置為 3 秒。設定採用 setTimeout 鏈(非 setInterval),確保每一步都能套用新的延遲。

  • fix(client): M-5 — Safari 私密模式 localStorage 防護 (public/js/lib/i18n.js:572-583) — Safari 私密模式對任何 localStorage.getItem/setItem 呼叫都會拋出 SecurityError。載入期間的 IIFE 過去會讓整個 i18n 模組失敗,導致 SPA 渲染出原始鍵名。已將兩處呼叫包進 try/catch,並以 detect() 之瀏覽器語系作為後備。

  • fix(server): M-2 — preview 外連 fetch 的回應大小上限(測試 + 驗證) — v1.21.0 的 safeGet 已實作分塊串流並依 opts.maxBytes 設限。v1.22 在 tests/ssrf-redirect-rebind.test.mjs 中新增明確的迴歸測試以鎖定契約:上游 100 KB + 4 KB 上限 → 回應 ≤ 4 KB。

  • fix(client): L-5 — 在 scan.js 的 hashchange 時清除 setTimeout (public/js/views/scan.js:6-22, :113-120) — 掃描完成後 300 ms 的 refreshResults() 計時器,過去在使用者於該時間窗離開 #/scan 時會洩漏。現於 __cancelActiveScanPoll 中捕捉並清除 handle。

  • fix(client): L-4 — SSE 多行 data: 串接 (public/js/lib/auto-pipeline.js:158-176) — SSE 解析器原先使用 match()(單行)。依據規範,一個事件可帶多行 data:,消費端應以 \n 串接。伺服器目前送的是單行 JSON,所以舊程式碼可運作 — 但對未來任何多行 payload 都很脆弱。

♿ 無障礙

  • feat(a11y): M-3 — WCAG 1.4.1 score pill 與連線橫幅的冗餘提示 (public/css/app.css:602-625, :812-822) — score-high / score-mid / score-low 過去僅以色相(紅 / 琥珀 / 綠)傳達狀態。無法辨識色相的使用者沒有任何後備。各層級現在透過 ::before 取得冗餘字符(✓ / ◐ / ○)。連線橫幅在離線狀態取得前置 ⚠ 字符。渲染端不動 — 純 CSS 強化。

  • feat(a11y): M-1 — 每個 mode-page 欄位的行內提示段落 (public/js/views/mode-page.js、public/js/lib/i18n.js) — v1.20.0 為每個 mode-page 欄位接好了 htmlFor → id,但未帶入行內提示文案;僅 README walkthroughs 文件說明了各欄位用途。v1.22.0 新增 19 個提示 i18n 鍵 × 8 locale = 152 條新翻譯,並讓 field() builder 為每個欄位渲染一個 <p id="…-hint"> 並串接 aria-describedby。螢幕閱讀器使用者在 input 取得焦點時即可聽見提示。

  • fix(a11y): M-7 — UI.el() htmlFor 別名的 null 防護 (public/js/api.js:194-198) — htmlFor: null 過去會渲染出字面 for="null"。一行式地對齊 fallthrough 分支的 v != null && v !== false 防護。

🧹 品質 / 可攜性

  • fix(server): L-1 — health.mjs + bin/start.sh + bin/setup.sh 的 parseInt 加上 radix — parseInt(process.versions.node) 沒帶 radix 會觸發 lint 警告,並在 Node 若改以十六進位版本號發布時變脆。各處統一補上 10。

  • fix(server): L-3 — Windows 友善的進入點檢查 (server/index.mjs:159-163) — import.meta.url === \file://${process.argv[1]}`在 Windows 上會錯誤處理磁碟機代號與反斜線。改為fileURLToPath(import.meta.url) === path.resolve(process.argv[1])`。

  • refactor(client): N-2 — 移除 Element.prototype.also monkey-patch (public/js/views/cv.js:188-201) — 屬於全域 DOM 原型污染。改以區域變數承接樹根。

  • test(canary): M-8 — 退役 /api/scan-ru/config 的 404 迴歸測試 (tests/scan-consolidated.test.mjs) — v1.20.0 退役該別名但未加 canary。三行式新增,鏡像 v1.18 的退役測試。

📚 文件 + 系統提示

  • docs(architecture): 為 v1.21+ 介面刷新 OVERVIEW + DATA-FLOWS — 於 OVERVIEW.md 中加入 safe-fetch.mjs(DNS-pinned GET)、file-lock.mjs(per-path mutex)、rate-limit.mjs(LLM 節流)與 sanitizePathName。DATA-FLOWS.md 新增兩節:「Outbound URL fetches (DNS-rebind-safe)」與「LLM endpoint rate-limiting」。

  • docs(readme): 安全範圍說明刷新 — README.md 的「Security notes」現在記載 v1.21+ 安全範圍中的每個 helper(sanitizePathName、safeGet、withFileLock、llmRateLimit、entity-aware stripDangerousMarkdown)。

  • docs(qa): 場景 31 — career-ops.org/docs 對齊 (qa/claude-cowork-browser-test-prompt.md) — 六個新子測試(31.1–31.6)用以驗證 UI 行為與五份規範 career-ops.org/docs 指南一致:score thresholds、scan workflow(單一按鈕)、apply workflow(checklist,而非自動送出)、batch workflow(TSV 編輯器)、Playwright 設定(優雅失敗)、help-bundle 覆蓋率(5 個 URL × 8 個 locale)。

  • docs(translate): README 品質刷新 × 7 個非英文 locale — 每個非英文 README 都以該語言之出版品級技術風格重寫。替換常見生硬計算式翻譯;補入 v1.21 / v1.22 安全範圍說明;發布與測試徽章同步更新。

  • docs(system): .claude/PROJECT-CONTEXT.md + .github/copilot-instructions.md — 給加入工作階段的 agent 的單檔定位資料。壓縮的 CLAUDE.md,點名 v1.21+ helper,列出常見陷阱。

  • docs(bin): 更新 start.sh / setup.sh / run_all.sh 註解 — 「two deps」→「three deps」(express + js-yaml + multer);「298 tests」→「474+ tests」;parseInt 補上 radix。

🧪 測試

  • 461 → 474 個單元測試(+13)+ 32/32 Playwright 不變。
  • 新測試檔案:cv-xss-bypasses.test.mjs(M-4,11 個測試)。
  • 延伸:ssrf-redirect-rebind.test.mjs(+1 對應 M-2 body cap)、scan-consolidated.test.mjs(+1 對應 M-8 alias canary)。
  • 既有套件零行為差異 — 每個修復都是 additive 或由新 canary 覆蓋。

驗證

npm test                          # 474 / 474
npm run test:e2e:browser          # 32 / 32

# 實體編碼 XSS strip:
node -e "import('./server/lib/security.mjs').then(({stripDangerousMarkdown}) => console.log(stripDangerousMarkdown('&lt;script&gt;alert(1)&lt;/script&gt;')))"
# → '' (no <script> survives)

# 健康檢查退避(打開 devtools,kill 伺服器,觀察 network panel):
#   3 秒 → 6 秒 → 12 秒 → 24 秒 → 60 秒,首次成功 ping 後重置

# Score-pill 字符(在淺色 + 深色主題下開啟 #/reports):
#   .score-high 顯示 ✓ + 數值分數
#   .score-mid  顯示 ◐ + 數值分數
#   .score-low  顯示 ○ + 數值分數

# Mode-page 提示(#/contacto 等):
#   <input aria-describedby="mode-contacto-recipient-hint">  ← 指向 <p id="…">

# 退役別名:
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4317/api/scan-ru/config
# → 404

破壞性變更

無。所有修復皆為 additive 或保留既有端點契約。

範圍外(v1.23+)

項目 說明
M-9 — locale CHANGELOG 內文翻譯 所有 CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md 自 v1.13+ 的條目皆為英文內文的權宜方案。在發布節奏放緩後可作為批次翻譯候選。
N-1 — public/js/lib/i18n.js 超過 400 LOC 目標 依 locale 拆分會在沒有 bundler 的情況下增加 HTTP 成本。延後到 build-step 決策落定為止。
由 career-ops.org/docs 刷新 help-bundle 內容 五個規範 URL 自 v1.11.x 起已出現在每個 locale 的 help bundle。QA prompt 的場景 31.6 會驗證覆蓋率。內容深度刷新為 v1.23 候選。

[1.21.0] — 2026-05-14

安全性 + 並行性 + 無障礙拋光,來自兩次獨立的程式碼審查。 docs/sdd/specs/V1.20.1-BACKLOG.md 中的七項發現於一次發布中交付:一個 blocker(DNS-rebind TOCTOU)、六個 high-severity 缺陷(路徑遍歷淨化散落、LAN 部署的速率限制缺口、並行寫入競爭條件、i18n 覆蓋率破口、懸空的 aria-describedby、標籤關聯缺失)。34 個新測試;基準從 427 → 461 個單元測試 + 32/32 Playwright。每項修復都搭配命名迴歸測試。

🛡️ 安全性

  • fix(security): B-1 — 透過 safe-fetch.mjs 關閉 DNS-rebind TOCTOU (server/lib/safe-fetch.mjs) — 先前模式是先做一次明確的 dnsLookup 驗證,再讓 fetch() 自行做獨立查詢。具有 TTL=0 的 DNS rebind 攻擊者可在查詢 1 回傳公開 IP,在查詢 2 回傳 127.0.0.1 / 169.254.169.254 / LAN 位址,藉此繞過 isPrivateOrLoopbackHost。新的 safeGet 只解析一次,透過 node:http(s) 將 TCP 連線釘到該確切 IP,並設定 SNI/Host 讓憑證驗證仍對準原主機名。由 /api/pipeline/preview 與 /api/auto-pipeline 使用。查詢失敗時 fail-CLOSED(反轉先前的 try { … } catch { /* fall through */ })。由 tests/ssrf-redirect-rebind.test.mjs 中 8 個新測試驗證。

  • fix(security): H-4 — 跨 10 條路由整合 sanitizePathName (server/lib/security.mjs) — 裸的 replace(/[^\w\-.]/g, '') 正規表達式重複出現在 jds.mjs、content.mjs、reports.mjs、llm.mjs、runners.mjs 中,而且保留了 . 字元,所以 ..pdf、....md、開頭點號的名稱都能存活。只有 reports.mjs::sanitizeSlug 做對。v1.21.0 將正確版本(sanitizePathName)提升至 security.mjs,刪除 10 處損壞副本,並對空結果以 400 拒絕。由 tests/path-traversal.test.mjs 中 12 個測試驗證。

  • fix(security): H-5 — 公開繫結時對 LLM 端點施加速率限制 (server/lib/rate-limit.mjs) — /api/evaluate、/api/deep、/api/mode/:slug、/api/auto-pipeline 之前沒有 per-IP 節流。Loopback 使用者不受影響;LAN 暴露的部署(HOST=0.0.0.0)得到 10 req/min/IP,過載時帶 Retry-After 與 X-RateLimit-* 標頭。可透過 LLM_RATE_LIMIT="N/Ws" 設定。在 v2.0 P-12 認證閘到位之前的廉價過渡防禦。由 tests/rate-limit.test.mjs 中 6 個測試驗證。

🔒 並行性

  • fix(data): H-6 — applications.md / pipeline.md 採用 per-file mutex (server/lib/file-lock.mjs) — 並行的 POST /api/tracker(或 auto-pipeline 與手動新增競賽)過去會同時讀到 num=42,雙方都寫入 num=43,並無聲丟失較早那筆。withFileLock(path, fn) 依路徑序列化 read-modify-write;不同路徑仍可並行。已接入 tracker.mjs、pipeline.mjs(POST + DELETE),以及 auto-pipeline.mjs 的 tracker 步驟。由 tests/concurrent-tracker-write.test.mjs 中 5 個測試驗證,包含一個 20-concurrent-POST 整合檢查,斷言 001..020 列依序落地。

♿ 無障礙

  • fix(a11y): H-1 — batch.js 的提示段落補上 id="batch-tsv-hint" (public/js/views/batch.js) — v1.20.0 為 TSV textarea 加上了 aria-describedby="batch-tsv-hint",但提示 <p> 從未獲得對應的 id。螢幕閱讀器沒有任何內容可朗讀。已修正。

  • fix(a11y): H-2 — batch-parallel / batch-min-score 標籤的 htmlFor (public/js/views/batch.js) — v1.20.0 的四個 input 得到新 id,但其 label 未以程式方式關聯。WCAG 3.3.2 現已滿足。

  • 於 tests/a11y-form-wires.test.mjs 中新增靜態分析 canary — 走訪每個 view 檔案,斷言每個 aria-describedby / htmlFor IDREF 都指向同層級的 id: 宣告。可在 CI 階段攔截錯字型迴歸。

🌐 i18n

  • fix(i18n): H-3 — v1.20.0 的 13 個鍵在 7 個 locale 中無聲 fallback 至英文 (public/js/lib/i18n.js) — pipe.filter、pipe.count、pipe.preview*、pipe.openTab、pipe.evaluateAll*、eval.jdHint、batch.parallelAria、batch.minScoreAria,以及 common.delete、config.group{Core,Runtime,Regional}、config.profileEmpty、config.viewProfile、scan.atsBadge、scan.regionalBadge 是透過 t('key', 'EN fallback') 引用,卻從未加入 DICT。俄文、日文、中文螢幕閱讀器使用者聽到的是英文 aria-label — 直接抵銷了 v1.20.0 宣稱的 WCAG 3.3.2 勝利。v1.21.0 補齊全部 19 個鍵 × 8 個 locale(約 150 條新翻譯),並延伸 tests/i18n-coverage.test.mjs:新增的靜態分析 pass 會掃描 public/js/**/*.js 內每個 t('key', …) 呼叫,斷言每個鍵都存在於 DICT。未來的漂移將於 CI 階段攔截。

🧪 測試

  • 427 → 461 個單元測試(+34)+ 32/32 Playwright 不變。
  • 新測試檔案:ssrf-redirect-rebind、path-traversal、concurrent-tracker-write、rate-limit、a11y-form-wires。
  • 既有 pipeline-preview.test.mjs 從 globalThis.fetch mock 改接 safe-fetch.mjs 中新的 _setTransport 注入點 — SSRF 路徑不再走 fetch,所以舊 mock 早就被無聲繞過。

驗證

npm test                              # 461 / 461
npm run test:e2e:browser              # 32 / 32
node --test tests/ssrf-redirect-rebind.test.mjs tests/path-traversal.test.mjs \
  tests/concurrent-tracker-write.test.mjs tests/rate-limit.test.mjs \
  tests/a11y-form-wires.test.mjs      # 34 個新測試全綠

# 路徑遍歷:所有 traversal 形式的 :name 都回傳 400 / 404
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4317/api/jds/..pdf
# → 400

# 公開繫結下的速率限制:
HOST=0.0.0.0 LLM_RATE_LIMIT=3/60s npm start &
for i in 1 2 3 4; do
  curl -sS -o /dev/null -w '%{http_code} ' -X POST -H 'Content-Type: application/json' \
    -d '{"jd":"…"}' http://0.0.0.0:4317/api/evaluate
done
# → 200 200 200 429

# 並行 tracker 寫入:20 個 parallel POST,20 列落地:
node tests/concurrent-tracker-write.test.mjs
# 20 個依序排列的 001..020 列

# Aria wires 健全性:
grep -r 'aria-describedby' public/js/views/ | wc -l
# 對應的 `id:` 查找全部解析(a11y-form-wires.test.mjs canary)

範圍外(v1.22+)

項目 說明
pipeline-preview body-size streaming 上限 (M-2) await upstream.text() 在 8 KB 切片前讀入整個 body;惡意 1 GB 串流可能耗盡記憶體。需 stream-read 加 byte counter + abort。
WCAG 1.4.1 — .connection-banner 與 score pill 的純色狀態 (M-3) 僅以色相傳達狀態;新增圖示前綴(✓ / ◐ / ○)或文字後綴。
stripDangerousMarkdown 透過 HTML 實體繞過 (M-4) &lt;script&gt;、java&#115;cript:、<img src="data:image/svg+xml,<svg onload=…>"> 仍可通過正規表達式。透過 UI.md 的深度防禦仍成立;以測試掃描方式記錄並鎖定繞過。
Safari 私密模式 localStorage 存取未加 try/catch (M-5) i18n.js:544/571 拋例外 → SPA 渲染原始鍵名。以 try/catch 包覆並以 'en' 為預設。
setInterval(checkHealth, 3000) 永遠輪詢且無退避 (M-6) 指數 3 秒 → 6 秒 → 12 秒 → 上限 60 秒。
htmlFor 別名缺少 null 防護 (M-7) 一行 if (v != null && v !== false) 防禦。
退役 /api/scan-ru/config 的 404 canary (M-8) 三行測試,鏡像 v1.18 先例。
Locale CHANGELOG 內文翻譯 (M-9) 在發布節奏放緩後可作為批次翻譯候選。
每個 mode-page 欄位的行內提示段落 (M-1) 約 168 個 i18n 鍵 × 8 個 locale;暫列為 polish 項目。
L-1 至 L-5 nits parseInt radix、bash –noprofile、Windows-safe fileURLToPath、多行 SSE、scan.js 計時器清理。


career-ops-ui v1.267.1 · page 7/9 · mirror of the project changelog for cvstart.org