Nhật ký thay đổi
GitHubMọi thay đổi đáng chú ý, theo từng bản phát hành. Cùng một tệp đi kèm repository và được dịch sang cả 19 ngôn ngữ.
[1.255.2] — 2026-10-24
Ba-bản-sửa-hiện-thế-từ-rà-soát-trực-tiếp và-một-báo-cáo-sập-trên-máy-chủ-thật: sập-khi-render-#/scan, kiểm-tra-ngôn-ngữ-tiếng-Việt-mù, và-việc-mất-khối-tóm-tắt-zh-TW lặp-lại.
Đã sửa
- Sập-#/scan (CAR-99): việc-tái-sinh-cổng-đếm từ-v1.253.0 đã-viết literal
FALLBACK_SOURCESvới-dấu-phẩy-kép-thừa ở-cuối-mỗi-mục — 120-lỗ-elision trong-mảng.new Set(FALLBACK_SOURCES.map((s) => s.value))trong-view-quét khi-đó giữ-undefined, và-id.lengthcủa-bộ-phân-loại-công-ty ném-TypeError: Cannot read properties of undefined (reading 'length')ở-mỗi-lượt-render-trang (logbuf bắt-được trên-máy-chủ-thật). Các-lỗ đã-được-loại-bỏ, và-một-cổng-mới (4-spec, xác-minh-bằng-đỏ) chạy-file và-trượt ở-bất-kỳ-lỗ nào, mục-không-dùng-được nào, hay-undefinednào trong-Set-id-adapter — cổng-phát-hiện-lệch chỉ-đọc-văn-bản không-thể-thấy-lớp-này. - Kiểm-tra-ngôn-ngữ-vi-mù (CAR-97): tiếng-Việt là-chữ-Latin và-không-có-mục trong-bảng-stop-word của-QA-trực-tiếp, nên-“vi stop words” đếm-các-trúng-tiếng-Anh và-một-câu-trả-lời-tiếng-Việt bị-đọc là-
report not in vi(rà-soát-trực-tiếp: vi 2, en 2). Tiếng-Việt giờ có-bảng-chức-từ mười-từ riêng-của-mình — một-báo-cáo-thuần-vi đỗ, một-báo-cáo-tiếng-Anh vẫn-trượt. - zh-TW mất-khối-SCORE_SUMMARY (CAR-98): lời-nhắc-ngôn-ngữ-kết chỉ-nói định-dạng-máy không-được-dịch, chưa-bao-giờ nói khối-tóm-tắt phải-tồn-tại — một-báo-cáo-zh-TW ngắn (5,983 ký-tự) mang-các-khối-A–G nhưng-bỏ-hẳn-khối-tóm-tắt (một-sự-lặp-lại-cùng-loại-v1.239.2). Chỉ-đạo-mở-đầu và-lời-nhắc-kết giờ khẳng-định khối là-bắt-buộc và-báo-cáo-thiếu-nó là-không-hợp-lệ.
Ghi chú
- Bản-phát-hành-này cố-ý không-đổi bất-kỳ-nguồn-quét nào, con-số-registry (121 = 116 EN + 5 RU · 116 adapter), hay-cách-nối-provider — chỉ-là-bản-sửa, nên-các-cổng-đếm nguyên-vẹn.
- Kiểm-thử 5695 (+9:
car99-fallback-holes×4,car97-vi-lang-check×4, bất-biến-prompt-CAR-98 ×1). Trình-duyệt 143/143. Lượt-kiểm-tra-trực-tiếp-lại vi/zh-TW đi-cùng-rà-soát-sau-triển-khai.
[1.255.1] — 2026-10-24
Hai-vòng-sửa-sau-rà-soát: vòng-rà-soát-Opus cho-locale-tiếng-Việt (9 phát-hiện) và vòng-rà-soát-lần-hai các-bản-sửa CAR-91/92 (5 bản-sửa-bắt-buộc) — cả-hai được-đóng lại với-các-test-đỏ→xanh.
Đã sửa
- Vòng-rà-soát-locale-vi (tiếp-nối CAR-88): chữ-glyph-Hangul (
신) rò-vàoproject.subtitle/project.roleHintđã-được-sửa lại (độ khớp tín hiệu), kèm-một-cổng trượt khi-bất-kỳ-glyph-CJK/Kana nào xuất-hiện trong một-giá-trị-vi mà từ-điển-EN không mang; sáu-URL-markdown-có-khoảng-trắng trong docs/help/vi.md (what-is career-ops,apply for a job,set up playwrightcùng-ba-mục-khác) được-gạch-nối về đúng-các-slug-hướng-dẫn-thực, kèm-cổng-cấm-khoảng-trắng-trong-URL. - Ranh-giới-tiêu-đề-đánh-giá (CAR-92 r2 H1):
&,.,/,'giờ là ký-tự-từ trong biên-trái của-chữ-khối —## Q&A,## Plan B,## Ph.D. requirement,## E-commercekhông-còn-được-đăng-ký thành khối A/B/D/E; ký-tự-kết-thúc-cuối-dòng-thuần chỉ-được-chấp-nhận sau-một-token-từ-khối (Block/блок/블록/…); ASCII-đã-rời các-lớp-ký-tự-kết-thúc-viết-chữ nên các-tiêu-đềE-commerce/ई-मेलsống-sót. - Báo-cáo-đánh-số-Ukraina/Nga (CAR-92 r2 H2): bộ-dò-dãy А Б В Г Д Є Ж giờ là một-helper-dùng-chung; khi tìm-thấy 3+ chữ-cái-của-dãy tại-một-mức-tiêu-đề, mức-đó trở-thành mức-tham-chiếu của-bộ-cắt — các-mục-con
### A.có-chữ-cái không-còn-cắt-ngắn báo-cáo tại-khối Б. - Vòng-đời-dòng-chuyển-tiếp (CAR-92 r2 M1): các-dòng mà một-nguồn-thất-bại chuyển-tiếp sang-lượt-quét-kế được-đóng-dấu
carriedSince, bị-loại sau 30 ngày hay 5 lần-chuyển-tiếp-liên-tục, và được-nối vào sau-các-dòng-mới thay-vì tích-tụy-lặng-lẽ mãi-mãi. - Mẫu-số-của-gemini-synthesis + legitimacy (CAR-92 r2 M2) và đường-điểm-phần-thân-của-tiến-trình-con (M3): đọc từ tệp-ghi-phát-hiện, sửa với-các-test-đỏ.
- Cổng-phát-hiện-lệch-bundle-tải-lạnh cập-nhật cho từ-điển-vi (90 thẻ-script / 87 có-thể-bundle) và loại-bỏ-biến-gán-test-không-dùng của nó (CodeQL 572).
Ghi chú
- Cách-diễn-đạt-số-adapter trong banner “Mới-nhất” của README.vi mà-rà-soát-chỉ-ra nằm trong banner Bản-trước và hiện-đã-ánh-xạ README-EN byte-for-byte — để-nguyên-tiêu để-giữ-đối-xứng EN↔vi (đã-ghi trong thân-commit).
- Kiểm-thử 5686 (+26:
eval-validate-opus-r2,scanner-carryover-r2, các-assertion cổng-vi). Registry không-đổi ở mức 121 nguồn (116 EN + 5 RU) · 116 adapter.
[1.255.0] — 2026-10-24
Tiếng Việt gia-nhập làm ngôn-ngữ-giao-diện thứ 19 — từ-điển 1525 khóa đối-xứng hoàn-toàn với EN, nhận-diện vi/vi-VN, trọn gói trợ-giúp, bản-phản-ánh README và CHANGELOG, tích-hợp landing cvstart.org, và lá 🇻🇳 trên bộ-chọn-ngôn-ngữ.
Thêm mới
- Tiếng Việt (
vi) — 1525/1525 khóa đối-xứng hoàn-toàn vớien;detect()chấp-nhậnvivàvi-VN; tiếng Việt là LTR (không-bao-giờ thêm vàoRTL_LANGS). ‘Pipeline’ vẫn-là ‘Pipeline’ — từ-mượn-kỹ-thuật-chuẩn, nằm trong danh-sách-trắng. - Đầy-đủ docs/help/vi.md (33 H2 / 125 H3), cộng với bản-phản-ánh README.vi.md và CHANGELOG.vi.md, và các-chuỗi-khung tiếng Việt trong
prompts.mjs(LOCALE_NAMES+SCAFFOLD_STRINGS) — đầu-ra LLM tiếng Việt chạy-ngay-từ-hộp. - Landing cvstart.org bằng tiếng Việt —
site/src/i18n/vi.json, mục-locale trong sitemap và ảnh-chụpdashboard-vi.png; bộ-chọn-ngôn-ngữ có-thêm 🇻🇳 (19 vị-trí). - Các-cổng-liệt-kê 18 → 19 — LANGS, thăm-dò detect, kiểm-toán i18n, đối-xứng changelog, khối-bản-dịch README, quét-locale — cùng
tests/i18n-vi-locale.test.mjsđỏ-trước (139 dòng).
Ghi chú
- Landing cvstart.org nhận-thêm vi — một-lệch-chủ-đích so-với tiền-lệ ta (bản-phát-hành-ấy cố-ý giữ landing ở mức 17); đã-ghi trong đặc-tả-phát-hành.
- vi là chữ-Latin nên cổng no-latin-leaks không-áp-dụng (nó canh các-locale không-Latin); từ-vựng-kỹ-thuật-Latin là-chuẩn-tắc.
- Sàn-kiểm-thử: 5634 → 5642 (1 bị-bỏ-qua = test-trình-duyệt-bắt-ảnh-layout tùy-chọn, không-đổi).
[1.254.0] — 2026-10-24
Một bản phát hành gia-cố từ vòng rà-soát mô-hình-tối: ảnh-chụp-scanner không bao giờ mất dòng nữa, mười-một vi-phạm-bất-biến ở-tầng-LLM được sửa, hai-mươi-ba bản-sửa-bắt-buộc-SPA đổ-bộ, và ATS Paylocity gia-nhập-máy-quét — registry giờ cung cấp 121 nguồn (116 EN + 5 RU) với 116 adapter.
Thêm mới
- Paylocity ATS (parent #5687): các bảng
recruiting.paylocity.comtheo-tenant, bảng-chết 302 → JobNotFound được ánh-xạ-thành-404 nên cách-lý cho-nghỉ-mục-đó, capmax_jobskèm-cờ-báo-cắt-ngắn, định-hình-dòng-an-toàn-ISO-4217. Registry: 121 nguồn = 116 EN + 5 RU · 116 adapter; các-cổng-đếm được-tạo-lại.
Đã sửa
- An-toàn-ảnh-chụp-scanner (R-12): một-lượt-quét-không-thử-gì-cả giờ GIỮ ảnh-chụp-đã-lưu (
nothing scanned) thay vì quét-sạch nó; một-lượt-thay-thế-toàn-phần chuyển-tiếp các-dòng-cũ-của-các-nguồn-thất-bại — đóng-dấu-theo-từng-mục nên một-“Lever”-thất-bại không còn-giữ-các-dòng-nguồn-lever-của-công-ty-khác — chạy-lại qua các-bộ-lọc-HIỆN-TẠI, chỉ-hạ-cánh ởfiltered(một-việc-đã-ứng-tuyển không bao giờ-tái-xuất-như-là-mới). - Mười-một-bất-biến-ở-tầng-LLM: chỉ-dịnh-ngôn-ngữ giờ vừa-mở-vừa-đóng các-prompt-deep/mode; tiến-trình-con-Gemini chuyển-tiếp
--lang, thoát-khác-không→502, ngừng-rò-stderr, tuân-thu-timeout-300 s và-ghi-usage; dự-phòng-thủ-công-của-auto-pipeline mang-theo-URL-tin-đăng; cảnh-báo-cắt-ngắn và-bundle lộ-ra trong-/api/mode; mức-chi-tiền không-còn-bị-client-ghi-đè; ngắt-kết-nối-client hủy-lời-gọi-nhà-cung-cấp-đang-bay (signal xuyên-qua-mọi-nhà-cung-cấp); bộ-cắt-tiêu-đề-lặp không-còn-phá-hủy-các-báo-cáo-hợp-lệ; bộ-trích-điểm và-độ-chính-đáng đồng-bộ-với-bộ-kiểm-thử; dãy-Ukraina và false-accept-“wa”-tiếng-Ả-Rập được-siết-chặt. - Một-vòng-rà-soát Opus 5-5 TRÊN chính-các-bản-sửa (8 bản-sửa-bắt-buộc) cũng-đổ-bộ: đăng-ký-nhiều-chữ-cái-của-bộ-cắt-tiêu-đề (một-tiêu-đề có-thể-đăng-ký-một-chữ-trong-chính-cửa-sổ-của-nó —
## G) Interview Q&Acắt-tại-G), các-ký-tự-kết-thúc-tiêu-đề-rộng-hơn (**Block A** —,[A], fullwidth/、/CRLF), dây-nối-abort-của-tiến-trình-con-Gemini, quy-attribution-chuyển-tiếp-đóng-dấu-theo-mục, các-dòng-chuyển-tiếp-chạy-lại-bộ-lọc-hiện-tại, và-mức-tiêu-đề-tham-chiếu giờ-đếm-các-chữ-khác-nhau. - Hai-mươi-ba-bản-sửa-bắt-buộc-SPA rải-qua-các-view-tìm-nguồn (append-pager-một-trang, xử-lý-bàn-phím-dòng-pipeline, khôi-phục-bộ-lọc-truy-vấn-đã-lưu, đặt-lại-select-nguồn, hoàn-về-mặc-định-config, rò-rỉ-listener, SSE-đóng-khi-kết-thúc, dọn-dẹp-scan-runner, một-lần-fetch-trần, tải-lỗi-trông-như-rỗng, va-chạm-window, giữ-focus) và các-view-hỗ-trợ (deep-link-help-chết, rò-rỉ-listener-help, tái-kích-hoạt-ghi-đè-trắng-hai-trang, single-flight-chạy-trả-phí-batch/deep/mode, luồng-kết-thúc-im-lặng, các-đường-lỗi-spinner-mãi-mãi, các-dòng-ma-bộ-lọc-thất-bại, trùng-docs-fab + ngôn-ngữ).
Ghi chú
- Toàn-bộ-vòng được Opus 5-5 rà-soát ở-mức-nghiêm-ngặt-tối-đa — các-port trước-khi-merge (21 phát-hiện, 13 đã-sửa) và chính-các-bản-sửa sau-đó (8 bản-sửa-bắt-buộc, tất-cả-đổ-bộ-ở-đây với-các-test-đỏ→xanh).
- Sàn-kiểm-thử: 5262 → 5634 (372 spec mới trải-qua-nguồn, máy-quét, tầng-LLM và SPA).
- Chuyển-tiếp-R-12-của-scanner KHÔNG-hồi-sinh-các-tin-đã-ứng-tuyển hay-đã-bỏ (các-dòng-chuyển-tiếp-chạy-lại-bộ-lọc-hiện-tại), và năm-nguồn-RU vẫn-không-có-adapter-thượng-nguồn — chỉ-có-web-ui theo-thiết-kế.
[1.253.1] — 2026-10-24
Two HIGH security findings from the max-model review are fixed: IPv6-literal URLs no longer bypass the scanner’s SSRF guard, and the CV-markdown escape regex no longer backtracks quadratically.
Fixed
- IPv6 literals bypass the SSRF guard (
http-json.mjsguardResolvedHost): a bracketed IPv6 literal ([::1],[::ffff:169.254.169.254],[fd00:ec2::254]— the AWS IPv6 metadata endpoint) madedns.lookupthrow and the guard fail open, letting fetch reach cloud metadata. Bracketed hosts are now unbracketed, classified directly viaisIP+ the private/loopback classifier — no DNS, no fail-open. Found by the Opus 5-5 max-model sweep (CAR-90). - ReDoS in the CV-markdown escape regex (
security.mjsstripDangerousMarkdown): the belt pattern’s\s*/?\s*backtracked quadratically —<+ ~100k spaces cost ~16 s. Replaced with one flat bounded class\s|/run (linear; the same input now ~1 ms); every opener the old pattern escaped still gets escaped. - tests/wiki-banner-check: the JA fixture’s stale version (a leftover from #461) is aligned.
Notes
- The two fixes carry red→green regression tests (3 new IPv6-guard cases + a time-bounded ReDoS case; 5511 tests total, +7).
- The scanner source files are not affected (the guard sits in the shared transport both scanners already route through).
[1.253.0] — 2026-10-24
Ten new job-board sources join the scanner — Atlassian, BWI, Computrabajo, Dover, Google, Kalibrr, Magneto365, Platsbanken, Tech in Asia and Zoho Recruit — the registry now ships 120 sources (115 EN + 5 RU) with 115 adapters, and the role matcher keeps titles apart when they differ only by a short capitalised code.
Added
- 10 scan sources ported from parent 1.38.0, each with its fetch-walk adapter and a CI-isolated suite (fake fetchImpl, zero network): Atlassian (single auth-free JSON feed, iCIMS posting URLs, https+icims.com allowlist on the row URL), BWI (JSON-LD ItemList, JSON.parse-first with an escape-tolerant fallback), Computrabajo (LATAM, entity decode via the shared table, Spanish “remoto” reads remote), Dover (per-tenant DRF pagination,
Remote (International)rendering, UUID-validated boards), Google Careers (per-phrase first-page queries, card employer wins, dedup across queries), Kalibrr (SEA aggregator, feed id as row id — slug churn no longer mints ids), Magneto365 (LATAM, fixed inter-page pacing), Platsbanken (Sweden’s JobTech API, keyword windows past the offset-2000 ceiling), Tech in Asia (Algolia keys discovered fresh from the site build, employer’s own link wins when https and not shared), Zoho Recruit (hidden-input board, opt-in bounded detail enrichment, ISO-4217-only currency parse). - Registry counts move to 120 sources = 115 EN + 5 RU · 115 adapters; the five count gates regenerated from the live registry;
#/scandropdown fallback, cvstart.org source links and help §17 ×18 follow.
Fixed
- role-matcher (#5688 port): titles differing only by a short capitalised code (“HR BP” vs “HR GP”, “AWS” vs “GCP”, even in mixed scripts) are no longer fuzzy-matched as the same opening; codes are read from the raw title with noise words excluded, and the guard sits right before the Jaccard ratio.
- Survived a maximum-rigor Opus 5-5 review of the ports (21 findings): silent-empty-board throws on markup drift (google/bwi), pre-aborted scans throw instead of returning
[], decode-then-strip ordering on the LATAM scrapers (the strip-then-decode XSS shape), https+allowlist on atlassian row URLs, port-rejecting host asserts, later-page parse inside the try (partials survive), and test-honesty fixes (budget counts asserted exactly, coerce contracts pinned truthfully). 13 findings fixed, the [spec] tail tracked separately.
Notes
- The five RU sources remain web-ui-only (no upstream adapters — by design); every EN source has an adapter.
- Coverage ratchet: the six largest port files raised to ≥95 % branch (27 new cases); the defensive-dead-code remainder is documented per file.
- New sources are provider-selected (
provider: <slug>in portals.yml) — existing configs are not affected, and no canonical behavior is changed beyond the dropdown/list counts.
[1.252.1] — 2026-10-17
The wiki’s release banner is now gated by a script instead of a hand-run sweep — the v1.252.0 regression caught it stale only by hand — and the locale directive explicitly overrides the profile’s language.output and the job posting’s language.
Added
scripts/wiki-banner-check.mjs— the release gate the regression asked for: it reads everyHome*.mdin a wiki checkout and compares the banner topackage.jsonand to the LIVE registry counters (scanner adapters from the portals registry — 105, not the banner’s stale 109 — route modules fromserver/lib/routes/, UI locales from the CHANGELOG set). The banner regex is language-agnostic by SHAPE, not by words: the localized Home pages translate the label and the counters («Текущий релиз», «Aktuel udgivelse», «адаптеров сканера», «enhedstests») and their counts had drifted further (5177) than the EN page (5245).--tests Ntakes the count a freshtest:cijust printed;--check-onlyforbids the implicit clone for offline use. Fixture-tested offline intests/wiki-banner-check.test.mjs.
Fixed
- The wiki Home ×18 banner is brought back in sync (pushed with this release): the v1.252.0 sweep replaced badges and links but never the banner line, and nothing mechanical flagged it. The gate above makes that a release-blocking check.
- The locale directive settles the language priority (
buildLocaleDirective): “This UI locale overrides the profile’slanguage.outputsetting and the job posting’s own language.” The parent’s own rule 6 says to write inlanguage.output— with no priority sentence the model resolved the two contradictory instructions inconsistently per locale, which is what the v1.252.0 language-column scatter was. Pinned bytests/locale-directive-language-output.test.mjson the directive text (17 non-EN locales + EN-empty).
Notes
- ta evaluate FORM observation (§3, watch): the first v1.252.0 live run produced a 48 336-character evaluate with no Block G and no SCORE_SUMMARY — looping-shaped, not language-shaped (CAR-70 is language). If it repeats on the next release, it gets its own Linear issue, separate from CAR-70.
- Per the FIX-PROMPT: the parent stays read-only; the override lives entirely in the UI directive.
[1.252.0] — 2026-10-17
The parent fork is synced with career-ops-hq/career-ops — 264 upstream commits, parent 1.36.0 → 1.37.0 — with both fork divergences defended, and the deployment re-pins the running parent to that base.
Added
- Vietnamese (vi) market mode arrives from upstream (
modes/vi/,tuyen-dung/ung-tuyen): BHXH/BHYT/BHTN, thử việc, lương tháng 13, trợ cấp thôi việc — the 21st declared market in the parent’s mode table. - Tech in Asia board provider (upstream): reads the public Algolia
job_postingsindex the techinasia.com jobs UI itself queries — a SEA startup board, ~300 live postings at sampling, 273 of them Indonesian. Keys are discovered fresh per run from the site’s build, the employer’s own application link wins over the board page, and salary stays unattached (the index’s salary fields are flags, not money). - Eight new Block G posting-legitimacy signal docs (
modes/oferta-signals/08–17): benefits/employment terminology country mismatch, third-party platform location tag mismatch, agency licensing, immigration-status overreach, jurisdiction-prohibited content, pay-transparency range width, minimum-wage lawyer question, AI-screening disclosure, fixed-term disclosure, relocation purchasing power.
Changed
- Parent-side hardening carried by the base bump: the story-provenance check now works by unit and sentence and keeps
user-cannot-confirmfigures out of negotiation, ATS and interview output; the CV fact-gate runs on every renderer and reads respelled and structured claims; LaTeX rejects stale extracted slots before writing; the follow-ups launchd sweep is skipped when follow-ups are off; update-system keeps its local-edits scan under the argv limit. - The fork’s divergences survived the merge:
providers/telegram.mjs(fork-only parser, ADR-0001) and the Unicode lookaroundLOCATIONISH_REintelegram-channel.mjs(ADR-0002). The boards doc now lists upstream’s improvedtelegram-channelrow, the fork-onlytelegramrow, Tech in Asia, and Tencent’smax_pagesdefault raised 20 → 100.
Notes
- The SPA ships no behavior change in this release: it is the docs train plus the parent re-pin. The deployment now runs parent 1.37.0; the parent delta does not touch the canonical-states table, so tracker statuses render exactly as before.
- The upstream merge resolved two conflicts:
docs/SUPPORTED_JOB_BOARDS.md(both telegram rows kept) andtest-all.mjs(upstream’s walkTree-basedcopyDirSyncsubsumes the fork’s hand-rolled.gitnested-checkout guard; the import list is the union).
[1.251.1] — 2026-10-12
The map’s specialization filter matches on DISTINCTIVE role words only — seniority and generic job nouns no longer match a posting by themselves — and remote-qa splits shape from language so a drift never red-marks A–G.
Fixed
- The specialization filter drops seniority and generic job nouns from its matcher: «Senior PHP Developer» matched «Senior Java Developer» through the shared Senior/Developer. The matcher keeps only distinctive tokens (PHP, Go, Platform, SRE, Backend…); a role with no distinctive tokens left («Software Engineer») falls back to the whole-phrase match. Word boundaries stay: «Go» never matches «Golang» or «Google».
- remote-qa separates shape from language: the A–G flag is now judged only by block/SCORE_SUMMARY/looping codes; a
report not in <locale>drift moves to the language flag and its own warnings column (the table header gains the missing 4th column; the codes are stored per row). - The map toolbar is a true 50/50 at 390 px (the 58/42 flex split left the search field at 124 px and truncated its placeholder), and the dead
.job-map-job-selectCSS rule is gone. - CodeQL 552 — the unused
OVERLAYSvariable in the design-qa test removed.
Notes
- Drive-time MILES remain deferred by the owner to the next release.
- The ta evaluate language drift stays a DOCUMENTED backend limitation (hermes, prose share 2–26 % across runs; A–G holds, no looping): remote-qa now reports it as an expected finding for ta, not a regression. A two-step flow (English evaluation + translation pass) remains an option if the owner wants it.
- The generic-token list is deliberately minimal (seniority + generic job nouns, en/ru): broadening it is one line per language, and every addition risks over-filtering a legitimate posting.
[1.251.0] — 2026-10-12
The map’s select now filters by SPECIALIZATION from the settings (config/profile.yml → target_roles.primary): pick «Product Manager» or «Backend Engineer (Go / PHP)» and the map keeps only the postings whose title matches that role’s own words. The v1.250.0 job-list locator is gone — it was the wrong lens (the owner’s correction).
Added
- Specialization filter: the map’s select lists the specializations configured in the settings (target_roles.primary), and choosing one keeps only the postings whose title carries one of that role’s distinguishing tokens (word-bounded, case-insensitive — «Go» never matches «Google», Cyrillic works through the same Unicode lookarounds). Filter changes redraw the map and the status chip immediately; Reset returns to all specializations.
Changed
- The v1.250.0 vacancy-list locator is removed: the owner’s correction — a select listing individual vacancies was the wrong lens for the map; the specialization filter replaces it. The peek cards, marker clicks and the text search stay as they are.
Notes
- Drive-time MILES are deliberately NOT part of this release either — the owner already deferred them.
- The specialization match is deliberately TITLE-ONLY (with the settings’ own words as tokens): matching against company names or descriptions would pull unrelated postings into a specialization. A posting outside every configured role stays visible under «All specializations» and disappears only when a specific one is chosen.
[1.250.0] — 2026-10-12
The map page becomes workable: a job locator above the map pans to any visible job and opens its peek (desktop included), and a text search narrows the map by company or title — with both surviving reloads.
Added
- Job locator: a select above the map lists the currently visible (post-filter) jobs sorted by company+title; choosing one flies to its circle — zooming into the spread so clusters separate — and opens its peek card. The peek opens on desktop too: the locator’s whole purpose is «show me this job», which navigating away would defeat. The options mirror the active filters on every render, so the select never offers a job the map isn’t showing.
- Text search: a search input narrows the map by company+title substring (case-insensitive matching; the input keeps the case as typed). The query persists in map-prefs across reloads, Reset clears it, and the locator’s options narrow with the visible set.
Fixed
- The docs FAB no longer covers the map’s attribution: with the new toolbar the map shifted down and the Leaflet attribution’s bottom-right corner landed under the floating button. The FAB is now hidden on
#/mapat every width (on mobile it already was); the docs assistant stays reachable from every other page. - The two mobile-map height specs move their floor 600 → 540 px: the toolbar legally takes ~90 px of the viewport, and the specs assert «the map fills most of the viewport», not an exact pixel value.
Notes
- Drive-time MILES are deliberately NOT part of this release — the owner deferred them to the next one.
- The locator resolves the drawn layer via a
__jobback-reference: the first attempt used_point, which L.Circle already owns — the lookup silently failed and the peek never opened. A property-name collision with a library is invisible until the feature is exercised. - The 4 new i18n keys enter all 18 dictionaries and the assembled-dict snapshot is regenerated.
[1.249.0] — 2026-10-11
The job map is now relevance-first: strong jobs lead with a calm teal-to-grey scale, score and commute filters persist, phones get a full-height map with tap-to-peek, and the tracker shares commute filtering and sorting.
Added
- Upstream commute integration: a read-only, no-store projection of career-ops-plugin-commute v0.2.0 joins jobs by canonical posting URL without exposing router, email or other private plugin settings. A configured home is a neutral reference marker and never enters job counts or clusters.
- Decision filters: All / 3.0+ / 3.8+ / 4.2+ score presets, independent unevaluated and unknown-commute toggles, profile/30/45/60/90-minute commute ceilings, persistent preferences, Reset and explicit Fit results. Filter changes never reframe automatically.
- Usable phone map: the map owns the usable viewport; one bottom sheet serves filters and marker peek. First tap shows company + title, score/fit and commute, while tapping the card opens the job. Desktop keeps hover plus direct click.
- Tracker commute lens: Location and Drive columns appear when commute data exists, with filtering, sorting and an over-profile warning. New UI copy is translated across all 18 locales.
Notes
- Not coupled to the Fighter90 fork: the integration reads the stable upstream plugin files only, so an original-upstream career-ops checkout works once it carries
career-ops-plugin-commutev0.2.0. The UI does not run routing/geocoding, refresh commute data, edit plugin settings or expose private router/email configuration.
[1.248.8] — 2026-10-11
Parent parity: the fork and web-ui sync to the upstream’s 115-commit delta — a new PageUp source, tracker URL cells parsed as markdown links, the Assessment state — plus the ta/ja language work: looping reports are cut and named, the heading skeleton keeps A–G stable, and the script share reads prose only.
Added
- PageUp source (
provider: pageup): scrapes the vendor-hostedcareers.pageuppeople.comlisting tables — one tenant, one entry, paginated with the no-fresh / short-page stop rules and a cappedmax_pages. The registry is 105 EN adapters / 110 sources; PageUp appears in the#/scanSource filter (FALLBACK + live registry). A branded domain is deliberately not claimed — point the entry at the vendor host. - Tracker URL cells parse as markdown links (parent #3516): the parent writes the
URLcell as[ashby](https://…);parseApplicationsnow extracts the href (the dedup key) at parse time. Bare URLs and placeholders pass through verbatim. - The Assessment state (between Responded and Interview) flows through the live states.yml read; the built-in fallback list is updated for parity.
Fixed
- A looping report is cut and named, never filed silently (CAR-74): the 46 587-char ta report repeated Block B and never reached D–G or the score. A repeated block heading now cuts the report at its first occurrence, the rejection reason names the loop (
report looping (block B repeated), …), and non-EN prompts carry a ready## A) … ## G)heading skeleton so the machine format and the language mandate stop competing (the hard mandate had dropped D–G entirely, A–G ✗ 2/3 on prod). - The script share reads prose only (CAR-75): tables, the SCORE_SUMMARY block, code and URLs say nothing about the report’s language. A Japanese report with Latin tables scored 26–27 % own-script under the full-text share and flaked against the 30 % gate; prose-only holds it deterministically.
- /api/evaluate surfaces the language warning (CAR-76): both provider tails push
report not in <locale>when the saved report is off-language, and remote-qa prints the warning codes as a table column (codes only — the log is public).
Notes
- Not ported (documented): the scan-ats-full infrastructure (fetchListing/listingIdentity/_connection-scope — web-ui has no reverse-ATS sweep), the liveness expired-history recorder (web-ui writes no scan-history; the pure
planExpiredHistoryRowsports when a writer appears), coverage-check/discover/story-bank template/pdf –ats/text mode/domain_filter (CLI-only), the dedup-tracker req-id fix (web-ui has no dedup-tracker), README.vi.md (web-ui’s locale set is its own). - The parent fork (Fighter90/career-ops) was synced first: main = db11bee7, the merge defended ADR-0001/0002 — providers/telegram.mjs survives (upstream deleted it on their side) and the Unicode-boundary LOCATIONISH_RE is kept (upstream reverted to ASCII
\b, which never fires next to Cyrillic; the revert merged with zero conflicts, exactly the shape the pipeline warns about).
[1.248.7] — 2026-10-11
The report-header role now reads the parent’s actual H1 convention — # Evaluation: {Company} — {Role} — instead of assuming # <Role> — <Company>, so the tracker no longer carries «Evaluation:
Fixed
- The role is read from the header in the parent’s format: v1.248.7 strips the localized prose label first (
Evaluation:,Оценка вакансии:,Оцінка:, and 16 more locale variants — longest first, same pattern shape as the prose-label regex in parsers.mjs). After a label the format isCompany — Role→ the role is the RIGHT side; without a labelRole — Company→ the left side; a part that names the company decides regardless. The role is never the company and never a bare label word — the cleaned post line is the fallback. (v1.248.6 read the left side and producedEvaluation: Sveakas the role on every auto-pipeline tracker row.) - cleanup-plan reports line counts, not file counts:
— N dirty-URL line(s)lines are summed (the N), not counted as one line each — a file with 5 dirty URLs reported 1, not 5. - A wrong-language evaluate report warns instead of saving silently: the docs assistant answers on-language but the evaluate drifted (ta own-script share 22 % and 2 %, reproduced 2/2 on prod; ja flaked once and passed on the retry). The evaluation prompt now opens AND closes with a mandatory language instruction — the entire report including tables — and the pipeline validates the saved report’s language: a drift surfaces as a
report not in <locale>warning on the done event and in the step detail, while the (otherwise valid) report still saves. - No model chatter before the H1: «I’ll evaluate this job description… Reading files…» preamble is sliced off at save — the report starts at its heading line.
Notes
- Not ported and not touched (verified in the v1.248.6 regression): the embed fetch, the post line structure («Компания : X» with a space before the colon), the «## Rejected» dedup, the domain-root rejection before the fetch, recon/link-check honesty, and the cleanup modes. The language check is a WARNING, not a rejection — a report whose content passed validation (A–G + score) still saves; the operator sees the drift (
report not in <locale>on the done event and in the step detail) and can re-run the evaluation.
[1.248.6] — 2026-10-10
Post-regression polish for the Telegram chain: a real post keeps its line structure (so «Компания : X» and the role line are found where they actually are), the tracker role comes from the report header instead of a raw post line, and the operational surfaces (recon, cleanup-plan, link-check) report what they claim to report.
Fixed
- A t.me post no longer glues into one line:
extractTelegramPostTextturned every tag into a space — including<br>— so the line-anchored patterns (^Компания:) never fired and the role fell back to the first 100 characters of the whole post.<br>(and a closing</blockquote>) now become a newline before tag-stripping, and numeric HTML entities (&#NN;,&#xNN;) decode to their characters. The company self-label pattern widened to^Компания\s*[:—–-]\s*(.+)— the live post said «Компания : Sveak» with a space before the colon, and the tracker got the channel name instead. - The tracker role comes from the report header: the pipeline computed the merged role (header H1 → post line fallback) for its gate but wrote the RAW
guess.roleinto the slug, the tracker row and the done event. The header’s H1 follows the# <Role> — <Company>convention, so the role side is taken (the company doesn’t ride along); the post-line fallback is cleaned — no hashtags (#удаленка), no emoji, ≤80 characters. ## Rejectedno longer duplicates: every rejection appended a fresh## Rejectedheading todata/pipeline.md. The section is created once; each rejection appends its line INTO the existing section (two rejections in a row → one heading, two lines).- A domain root is rejected before the request: the
pathnamecheck ran afterfetchJobDescription, so a junklinkedin.com/cost a full page download (fetch: done) before being declined. The check now fires before the fetch — a junk URL costs nothing. - Recon computes the skip-contract signal instead of asserting it:
honours rejected:is nowyesonly if the deployedauto-pipeline.mjsactually containsmarkPipelineRejected(grep of the fetched source), and the marks count no longer prints0twice (grep -calready prints0on no-match; the|| echo 0fallback duplicated it). - cleanup-plan reports numbers per category: t-role reports to move, dirty-URL pipeline lines, test-row tracker lines — instead of one
grep -cthat counted its own «no unscored -t-role-.md reports» success line (a clean dataset printed a phantom «1»). - link-check treats github.com 5xx as unverified: three runs in a row 503’d on
github.com/…/blob/main/…links from Actions runners while the same URLs returned 200 from other networks — counted behind the bot wall (like 401/403/429), not as broken links.
Notes
- Not ported and not touched (verified working in the v1.248.5 regression): the embed fetch itself, the pipeline skip contract, the offline
_setLookuptests, the uk ordinal acceptance, word-boundary role keywords, and the cleanup-apply workflow mode. Thecleanup-planper-category counting reads the cleanup script’s own output labels (no valid SCORE/dirty-URL line/test-row line) — renaming those labels would silently break the counters.
[1.248.5] — 2026-10-10
Post-regression hardening: Telegram single-post entries now fetch the post text itself (the embed form), a rejected entry leaves the eval timer’s pool server-side, and a Ukrainian report’s native block numbering is accepted — plus one test that secretly needed the live network is offline for good.
Fixed
- t.me entries finally evaluate: a single-post page serves only the channel header in plain HTML (~0.2 KB — «View in Telegram» and nothing else), so the model got an empty description and every Telegram entry died at evaluation. The fetch now rewrites
t.me/<channel>/<id>(and thet.me/s/…feed form) to the embed form (?embed=1&mode=tme) — same host, SSRF validation unchanged — and reads the post from.tgme_widget_message_text(fallback:og:description). No widget text → rejected at the fetch step (telegram post has no text,rejected: true) before any LLM tokens are spent. - The eval timer stops re-paying for rejected entries (server-side contract): a rejected entry is now marked in
data/pipeline.md— its line leaves the URL fence and lands in a## Rejectedsection with a short reason (idempotent, never duplicated). The timer picks bare URLs from the fence, so a marked entry is skipped naturally — no dependence on the timer script’s internals. Recon printspipeline rejected marks: N+honours rejected: yes (server-side)instead of grepping the timer script. - Ukrainian reports pass A–G validation again: a uk model numbers the blocks with its own alphabet order (А Б В Г Д Є Ж), and the lookalike pass only counted А/В/Е — exactly blocks C, D, F, G went missing on prod (2/2 reproductions). The validator now accepts the Ukrainian ordinal sequence when the headings appear in order (the same discipline as abjad for ar and Devanagari for hi), and the evaluation prompt for every locale now demands Latin
A–Gexplicitly. - The role keyword gate fires on words, not substrings:
linkedin.com/andx.com/roots passed the gate becauselead/itlived inside unrelated page chrome. Keywords match on Unicode word boundaries now, and a domain root (a URL with no path) is rejected before the LLM call. - A unit test no longer needs the live network: the no-hints test (
tests/auto-pipeline-jd-gate.test.mjs) stubbed the HTTP transport but still performed a real DNS lookup — green in CI only because the runner had connectivity. safe-fetch gained a_setLookup()hook (the resolver itself is replaceable in tests; production code never overrides it), and the test resolves offline.
Added
cleanup-plan/cleanup-applyworkflow modes in deploy.yml: the prod QA-junk cleanup (post-qa-cleanup) runs server-side without SSH from a laptop. The log carries numbers only — no file names or line previews, because the log is public and file names contain company names. The Verify step is skipped for cleanup modes.
Notes
- Not ported: the parent’s word-boundary location compiler and USPS state table (documented since v1.248.1). The rejected-entry skip contract lives server-side (marking in
data/pipeline.md), not in the timer script — the script is outside the repository and deliberately not edited. - The
telegram post has no textrejection is a validation-driven stop, not a fetch failure: the SSE error payload distinguishes it withrejected: true, and plain fetch failures (404 etc.) keep the bare error shape.
[1.248.4] — 2026-10-10
The role-resolution chain for Telegram entries: a Cyrillic post with a real vacancy now yields a named report instead of a rejected unknown-role file — and the LLM call only fires for entries worth evaluating.
Fixed
- The role chain works for Cyrillic posts: the keyword list gained разработчик/программист/инженер/аналитик/менеджер/руководитель/тимлид/лид/дизайнер/тестировщик/QA/архитектор/специалист/директор/DevOps/SRE (the old list was EN-only — 0 of 40 real posts got a role), the report header is consulted (
parseReportHeader— the model writes the role into the H1), and the first meaningful line of the post is the last resort. - The identity gate moved BEFORE the LLM call: an entry with neither a company nor any role hint now fails at the fetch step (
rejected: true) — no tokens burned on an evaluation that was going to be rejected anyway. t.me/s/<channel>/<id>no longer names the company «S» — the web-preview prefix segment is skipped; both link forms resolve to the channel.- «Компания: X» self-labels on Telegram posts are honoured (and «…в компании X» mid-sentence for messenger hosts).
Added
auto-pipelinefailures carryrejected: true— the server-side eval timer uses the flag to skip the pipeline entry instead of retrying it every 2 hours.
Notes
- Not ported: the parent’s word-boundary location compiler and USPS state table (documented since v1.248.1).
- The README «Translations» blockquote is now generated from one canonical locale list, with a CI parity test (17 links per file, own never linked) — ar/pl/uk gained the block; the
|-vs-·separator drift is gone.
[1.248.3] — 2026-10-10
Follow-up to the regression round: the eval timer can no longer file nameless Telegram-junk reports under any circumstances, the QA-cleanup can no longer delete real Telegram postings, the map attribution is visible again, and the dashboard chips read ≥ 4.5:1.
Fixed
- The auto-pipeline validates BEFORE saving: a report without a 0–5 score or with missing A–G blocks ends the run at the evaluate step (
rejected: truein the SSE payload) — no report file, no tracker row. The t.me entries that cleared the v1.248.2 length gate produced two more junk reports after that release; with this fix the pipeline’s nameless entries are rejected and the run reportscompany/role not identifiable. guessCompanyRoleno longer invents a company from messenger hosts —t.me,telegram.me,vk.com,linkedin.comand friends are EMPTY_DOMAINS: a Telegram post falls back to its channel name, and a nameless entry is rejected instead of filed asunknown-role.post-qa-cleanupreads the header score first: auto-pipeline strips the SCORE_SUMMARY block before saving, so the old summary-only check flagged every saved Telegram posting as junk — on prod 8 of 41 carry a header score and--applywould have deleted them. Header score (viaparseReportHeader) is now the primary signal; the summary block is the fallback. The dry-run prints a per-file reason.- The Leaflet attribution is visible on #/map — the docs FAB covered it (an OSM-licence violation); the corner is lifted 96px.
- Dashboard chips clear 4.5:1 —
dash-chiplabels and the--manualvariant moved to--foggy-strong(were 4.31:1).
Added
auto-pipelinefailures carryrejected: truein the SSE error payload — the server-side eval timer uses the flag to skip the pipeline entry instead of retrying it every 2 hours.
Notes
- The ar/ja evaluation drift did not repeat on the re-run (see v1.248.2).
- The prod cleanup itself still awaits an explicit user yes (CAR-61c); with this fix the cleanup can no longer delete the real Telegram postings.
[1.248.2] — 2026-10-10
Regression round: the eval timer can no longer turn pipeline junk into «Last evaluation», SCORE parsing survives real-world formats, and the tracker finally speaks your language.
Fixed
- Empty-JD reports are never written. The auto-pipeline’s JD gate rose from 50 to 200 characters before any LLM call — placeholder pages (
example.com/qa-v167-…) now end as an SSE error + console warning instead of a…-t-role-<ts>.mdreport that the dashboard showed as «Last evaluation» (42 such files on prod). - SCORE_SUMMARY parsing survives real formats: «4,2/5», «4.2 / 5», «4.2», «SCORE :» all validate now; a comma with exactly 1–2 digits is decimal, with 3 it is thousands (the old code read «4,200» as 4.2 and passed the 0–5 gate). A failed check logs only the
SCORE:line — never report content (CI logs are public). - Tracker statuses are translated: stage tabs and status badges render localized labels in all 18 locales; the canonical value in data, filters and URLs is untouched.
- Honest ETA: «⏱ ~30s» became «~2–4 min» across evaluate/advisor/market-report (the real range is 86–285 s), and the evaluate subtitle reads A–G.
- Accessibility: the config API-keys counter and the pipeline counter now clear 4.5:1 (was 4.31:1); a facet chip with an empty value is no longer rendered without an accessible name.
- UI polish: the saved-search Delete button reads destructive only while a search is selected (disabled otherwise); the profile EMAIL card no longer clips at 1440 px; the live-evals pill gained icon/text spacing including RTL; the docs FAB no longer covers the Leaflet attribution; the help «?» no longer wraps under the stats tabs; the usage view gained air between its subtitle and range tabs; health now says career-ops-ui.
Added
scripts/post-qa-cleanup.mjs: dry-run by default,--applywith automatic backups (*.bak-<ts>, reports moved toqa/cleanup-backup-<ts>/) — ZZ-QA-TEST tracker rows,*-t-role-*reports without a valid SCORE, andexample.compipeline entries.
Notes
- The ar/ja evaluation drift from the regression round did not repeat on the re-run (both: lang ✓ + A–G ✓) — the language-check ta entry and the SCORE parser hardening ship as defense-in-depth.
- The prod cleanup itself awaits an explicit user yes: the dry-run plan precedes any
--apply.
[1.248.1] — 2026-10-09
location_filter gains the parent’s always_allow and block_hard tiers (CAR-60) — a multi-country location string survives a blocked-sibling list, and an explicit hard block can no longer be rescued.
Fixed
always_allowbeatsblock: withblock: [romania, poland, croatia]andalways_allow: [united kingdom], the workable merged cell «London, United Kingdom · Romania · Poland · Croatia» passes — the home region is an option (workable #4806’s consumer case, now in the parent’s own form).block_hardbeatsalways_allow: country-level terms that must never be rescued by an always_allow hit elsewhere in the string.strict: truenow countsblock_hardas a restricting tier; thedocs/helplocation_filter section documents both tiers ×18.
Notes
- Deliberately not ported this time: the parent’s word-boundary location keyword compiler and its USPS state-name table (web-ui keeps case-insensitive substring matching; the table serves US-centric configs — file under CAR-60 if a US config needs it). The workable consumer test’s interim allow-list form stays as a second verdict alongside the new parent-form check.
[1.248.0] — 2026-10-09
Parent parity: five upstream provider/liveness fixes ported — boards that moved their APIs keep working, and a closed-posting banner on its own line is no longer missed.
Fixed
- Gupy follows the API to
portal.gupy.ioand sends the filter names the API actually reads (workplaceType/type) — the old plural names were silently ignored;countryis no longer sent (a configured country now draws a naming warning instead). Confidential employers drop on an exact label match («Confidencial Seguros» survives), andmax_pagesis capped at 100 — the API answers 400 past offset 10,000, and the truncation warning now says to narrow keywords instead of raising the cap. - Workable keeps every country of a multi-country posting: siblings listed under the same job URL fold into one row («London, United Kingdom · Romania · Poland · Croatia») instead of the first country winning.
- Liveness: a banner on its own line is not part of the line above — «Sign in if you want to apply» / «Get notified when this position is filled» under a posting is read as the closing clause it is, while a genuine «until filled» spanning lines still reads active.
- Radancy reads a sibling
job-locationelement in the legacy card markup (Barclays-family layouts put the location after</a>) — rows without their own location stay empty rather than borrowing a neighbour’s. - SmartRecruiters fallback job links keep the configured slug — a posting with a missing/untrusted
refsynthesizes its public URL from the tenant slug, never from the display name.
Added
- No-ports, documented: the upstream
_httpproxyfetchImplfix (web-ui has no proxy branch — porting would defeat the DNS-rebinding guard’s identity check and add an undici dependency the repo deliberately avoids); upstreamverify-ats(+226 lines) andcv-facts(web-ui implements its own ATS scoring and CV parsing); the parent dashboard Go TUI and CLI scripts (web-ui does not shell into them); a Teamtailor import reorder (cosmetic).
Notes
- Not ported (follow-up filed): the parent’s
always_allowlocation-filter tier — workable’s consumer test survives a UK block via it, but web-ui’s location filter does not have that tier; the port expresses the same consumer verdict through an allow-list test instead.
[1.247.0] — 2026-10-09
The tail of the CAR-49…58 design sweep is done — every remaining minor and polish finding, plus the dark map the previous release deliberately deferred.
Fixed
- The map renders a real dark theme. Zoom/layers/attribution went dark in v1.246.0; the tiles themselves now follow: the dark mode renders an OSM tile layer with a CSS inversion (no vendor keys), and a keyed Carto-dark preset is available via
MAP_TILE_DARK_URL(its host is allow-listed in the CSPimg-src). The layer remounts live on theme switches. - Yellow callouts have dark variants (
config,batch, hero warning) via the v1.246.0 callout token mechanics — and a regression caught on the way:.callout astyling overpowered.btn-primary; now scoped:not(.btn). - Apply info-card URL no longer truncates mid-word (overflow-wrap anywhere).
- CV header buttons are one icon system (text-only; the 📁/📄/💾 emoji mix is gone).
- Tracker status tabs are one case — sentence case, folded across locales (CJK/ar/hi/ta handled without gender forms).
- Topbar icons are a single stroke-SVG set — the 🔔/🌙/🩺 emoji mix is gone; the theme button renders sun/moon via CSS, so the app no longer rewrites button text.
- The dashboard Pipeline card lost its stray pink highlight (ring is hover/focus-only now).
- Saved-search Delete is visibly destructive (
.btn-danger, not a neutral ghost). - Pipeline preview has a real empty state (title + hint + CTA, tracker pattern).
- Time/cost meta is one pattern —
UI.pageMetarenders ⏱ + cost as a single line under the view title (evaluate/deep/auto/mode-page/orientation). - Auto-pipeline lost its ✨ — the only decorated h1 in the app is back on the shared left edge.
- Batch docs URL is an inline link (no bare mid-paragraph wrap).
- interview-digest / orientation have designed empty states (title + hint + CTA).
- Career-plan export is disabled until a plan exists, actions on one row.
- stats: the duplicate «Market report» heading is gone and the region placeholder fits (shorter copy ×18, fixed width).
- Config no longer leaks absolute filesystem paths — server-side
displayPath()renders~/…or the bare file name. - The LLM_PROVIDER helper is three short lines (×18 copy +
pre-line), not a 150-char mono wall. - Profile EMAIL card keeps addresses on one line (ellipsis; wrap only ≤480px).
- Health text no longer breaks inside words («Run buttons» stays whole; overflow-wrap anywhere).
- Help inline code survives RTL (LTR-isolated, nowrap, scrollable;
career-opsin docs/help/ar.md is now coded as a token). - The hero «Live evals» pill no longer collides in ru/ar (flex-wrap, non-shrink icons).
Added
- Dark-tile preset (
MAP_TILE_DARK_URL+ attribution env) documented in-code; the CSPimg-srcaddition covers the Carto host for it.
Notes
- The dark-tile default is a CSS inversion of OSM tiles — keyless Carto now gates its basemaps behind an API key (verified by request), so the keyed preset is opt-in, not the default.
[1.246.0] — 2026-10-09
All ten top findings from the CAR-48 senior-design sweep are fixed — the blocker and all seven majors included.
Fixed
- Apply dark: the info-card link is visible again. The card kept its light surface in dark mode; the “Need Playwright? See …” link sat at ≈1.0:1. Theme-aware
.callout--info(surface/border/text/link tokens) puts the dark link at ≈7.3:1. - RTL no longer flips numeric compositions. Score pills (“1.5 / 5”), report bands (“≥ 4.5”, “< 3.5”) and the dashboard “/ 5.0” get directional isolation (LTR-isolated spans); the ar dictionary rephrases numeric ranges in words where prose reads better.
- CV markdown stays LTR. The textarea and the preview render
dir="ltr"in Arabic; the chrome around them stays RTL. - Reports DATE column never wraps an ISO date again (nowrap + tabular numerals).
- The sidebar ends above the USAGE HUD (scrollport reserve + fade) — the last nav item is no longer scrolled under the panel; mirrored for RTL.
- Mobile scan @390: “Save search” is fully visible — the controls row wraps and compresses instead of clipping at the viewport edge.
- Map controls follow the dark theme (zoom, layers, attribution). The tiles themselves stay as-is by design: the tile origin is pinned server-side by the CSP
img-srcdirective, and client-side layer swapping would mean changing a security header. - The chat FAB no longer covers interactive content — page scroll padding clears it (Portals Disable buttons, Reports score pill, the two-pager input, Leaflet attribution).
- Activity log speaks the UI language: 22 action slugs localized ×18 locales (with a raw-id fallback for unknown ones) and the filter chips translate; the TARGET column stays — auto-pipeline fills it.
- Assessments forms get real labels (sentence case,
htmlFor/idbinding) instead of placeholder-only fields.
Notes
- Not in this release: dark map tiles (needs a CSP
img-srcaddition for a dark tile origin — a server-side change, deliberately not done client-side); dark variants of the yellow callouts (config,batch) — the callout token mechanics are in place for a follow-up.
[1.245.0] — 2026-10-09
Tamil (தமிழ்) joins as the 18th UI locale — the first new language since the 17-locale wave, with full key parity.
Added
- Tamil (
ta) — 1449/1449 keys with full parity toen, zero placeholders lost, hi-style Indic-tech conventions (providers, model names, paths and commands stay Latin).detect()acceptsta,ta-IN,ta-LK; Tamil is LTR (locked by test). - Full docs/help/ta.md (33 H2 / 125 H3, source counts verified against the live registry, Hermes canary anchors, verbatim YAML), plus README.ta.md and CHANGELOG.ta.md mirrors.
- Enumeration gates 17 → 18: LANGS, detect probes, i18n audit, changelog parity, RTL guard, CI
langs— which also picks uphi, previously missing from the CI list.
Notes
- Not in this release: the cvstart.org landing translation (the landing stays 17 locales for now — a separate task); the site content sync already ships docs/help/ta.md and the ta changelog.
- Not in this release: Tamil report parsing —
report-header.jsREPORT_LABELS stays 17; it arrives with parent-repo Tamil report generation.
[1.244.2] — 2026-10-08
Scan page polish: the launcher, status bar, reposts panel and filters are redesigned as one consistent chrome — and the results table can no longer be stretched by board-side data.
Fixed
- Salary cells keep only the money range. Some boards put the whole benefits blurb in the salary field (“$243K – $286K • Offers Equity • This role is also eligible for medical benefits, 401(k) plan…”) — the visible cell now shows the money chunk and moves the blurb to the tooltip (was: a 6-line cell stretching every row).
- Rows with an empty title render the company (then a locale-neutral dash) instead of a giant blank cell.
- The seniority badge never wraps in its fixed-width column.
- Unscored rows show a muted «◎ —» with a tooltip explaining the two-pager compare found no matching keywords (was: nothing, which read as broken next to scored rows).
- The aux-height layout contract now measures the content element inside the aux cell — a td stretches to its row’s height, which masked real wraps.
Added
- Scan chrome redesign: launcher card (aligned control row, dominant primary button), terminal status bar (status dot idle/running/done/error, reduced-motion safe), reposts disclosure (styled header, count badge, capped panel with sticky header — a 1,560-cluster dataset rendered a 155,000-px panel before), and the filters block as a uniform responsive grid with an aligned footer.
Notes
- Tests 5045 → 5068 unit; browser layout contracts extended (aux one-line bound).
- Not in this release: URL-hash filter state and keyboard row navigation (follow-ups); live-LLM regression × 7 locales blocked earlier by upstream credits (unblocked — CAR-44).
[1.244.1] — 2026-10-08
Fixed
- Scan icons announce real values. The boost/fit/score icons’ accessible names leaked the raw dict templates (“Fit: {band}”, “Boosted by {by}”) —
{band}/{by}/{score}are now substituted (band word, booster keyword, numeric score). [HIGH a11y, caught live in the v1.244.0 regression round] - Title pipes split correctly. A 4-segment title like «C++ | Rust | Go Developer | Onsite» no longer lifts “Go Developer” out as a “country” — the split demands the segment resolves to a real country (conservative
Countries.detectCountry). - Meta dates render for every row.
daysSinceaccepted date-only ISO, but the API serves full timestamps for ~83% of dated rows — those rows showed no date segment.
Notes
- Everything else in #/scan is identical to v1.244.0 — only the three regression fixes above changed.
- Deliberately not done: URL-hash filter state, keyboard row navigation and
5 lakhs INRparsing (all pre-existing follow-ups).
[1.244.0] — 2026-10-08
The scan results read like a list, not a wall: one posting = two lines, the signals are icons, and a 700-row scan is a paginated table instead of a 37,000-px page.
Fixed
- Row anatomy. Every posting renders as a bounded two-line row: line 1 the title, line 2
company · location · source · date · work-type. The boost badge, the title-fit band and the fit score became icons with localized accessible names and tooltips (screen readers read «Соответствие: сильное» / “Fit: strong”, not a wall of words); trust and relocation badges folded into the same rail. Seniority and salary stay as desktop-only auxiliary columns. - Title hygiene. A trailing
| Germany | Remotesegment is split out of the title into the meta line — Unicode-aware, only when the tail is a real work-type marker and the middle segment is place-shaped (legit pipes like “C++ | Rust | Go Developer” never split). - No more horizontal sprawl. Fixed table layout,
min-width: 0, ellipsized one-line title/meta with the full text in the tooltip: zero page overflow and zero internal table scroll at 1440 px and 390 px (the old page overflowed by 675 px at phone width). - Pagination. Results render 50 per page (25/50/100/200 selectable) — a 700-row scan is now ~4,600 px tall instead of ~37,000 px, and the DOM holds ~1,000 nodes instead of 5,500+.
Added
- Layout contract tests: the two-line row, the icon accessible names (en + ru), the pagination node budget and the title-fit count/order guard run in the browser suite — the redesign cannot silently regress.
Notes
- Tests 5036 → 5045 unit (browser suite gained the layout contracts), coverage mean 98.14 % line / 89.34 % branch (floor 96/86).
- What the scanner finds, the scoring formulas and
last-scan.jsonare untouched — title-fit stays an annotation. - Not in this release: URL-hash filter state and keyboard row navigation (follow-up);
5 lakhs INRparsing (pre-existing).
[1.243.2] — 2026-10-08
Fixed
- Deployable release cut as v1.243.2. The v1.243.1 tag was re-pointed after publishing, and the server’s
git fetch --tagscorrectly refuses to clobber an existing tag — so v1.243.1 was never deployable. This tag carries the identical tree (the tracker outcome-modal window-listener fix on top of v1.243.0) plus this version bump. Nothing else changed; the published npm 1.243.1 tarball already contained the fix.
Notes
- App code is identical to v1.243.1 — only the deployable tag is fresh (the server’s
git fetch --tagsrefused the re-pointed v1.243.1). - Deliberately not done: force-updating the server-side tag (published tags are never moved).
[1.243.1] — 2026-10-08
Fixed
- The «Record outcome» modal actually closes on navigation now. The v1.243.0 guard listened for
hashchangeondocument, where that event never fires — the fix shipped dead and the modal still blocked the next view (caught live on production by the v1.243.1 regression round). The guard listens atwindow; the test now dispatches there too and was verified red against the broken code.
[1.243.0] — 2026-10-08
The browser layer keeps your work safe and speaks your language: no unsaved-edit losses, no silent failures, no silent English in a translated UI, AA contrast everywhere.
Fixed
- Unsaved edits survive re-renders. The CV editor keeps a dirty buffer and restores it on every render — Cancel, a language switch, or navigating to CV Studio now prompts instead of discarding; the career plan keeps its generated text the same way. Load failures in Config/CV Studio/career-plan/memory/two-pager surface an error and disable Save instead of quietly overwriting the file with blanks.
- The “Record outcome” modal no longer survives navigation. Opening it on
#/trackerand clicking another view left it blocking the whole page (found in the v1.242.1 two-stand regression) — it now closes on route change; its scan-view siblings stop their SSE streams and detach listeners on navigation too. - Salary and keyword parsing reads every script. Keyword chips use Unicode word boundaries (Cyrillic,
c++,.netfilter correctly instead of to zero rows); one shared salary parser fixes$182.9K - $240K,от 100 000 руб,100 тыс. руб.,50 000 Kč(was 50M USD) and range semantics ($100-150K); must-have “Germany or Netherlands” no longer counts as both matched and violated. - Streams settle exactly once. A dropped SSE connection delivered the error twice and could leave Generate disabled forever; every terminal state (done/error/close/throw, including a killed child) settles one path and re-enables the button. The auto-pipeline flushes its final buffered frame instead of dropping it.
- Privacy and correctness in the little paths. The bug-report body no longer carries the evaluated job URL or report slugs; CV privacy masks bare
linkedin.com/in/…links, ALL-CAPS names, and stops mangling EU dates; country resolution takes the longest match and refuses to guess when two countries are named (“Cambridge, MA” no longer → UK); filename downloads keep Cyrillic names. - Router and a11y. The skip link no longer 404s into
ontent; the active nav item carriesaria-current="page"; one<main>/one<nav>; Escape closes the mobile drawer (which is now truly out of the tab order when closed);scroll-padding-topkeeps focus visible past sticky bars; the sticky banner no longer covers the topbar. - Contrast is AA in both themes — dark toasts were white-on-white (1.10:1): toasts, buttons, banners and links on
--rauschnow resolve through theme-invariant tokens, guarded by computed WCAG contract tests (≥ 4.5:1). - Everything else from the round: evaluate renders the truncation warning; memory/config/two-pager refuse to Save over a failed load; scan stops orphaning its SSE; scan results classify API-backed boards from the server registry instead of a drifted regex; the tracker’s “Proceed with Caution” is a warning, not a badge-red; stats fold localized statuses; bar charts read correctly in RTL; the docs FAB no longer covers content on phones;
GET /app.jsanswers 404 instead of the index shell;GET /api/runnerslists its actions.
Added
- Danish joins auto-detection — a
da-DKbrowser now gets the Danish UI (the dict was complete, detection was missing it);zh-Hant*maps to Traditional Chinese.cv-diagnostics— the résumé checklist — is fully localized (28diag.*keys × 17). 26 field-spec labels added; 31 dead keys removed; provider lists no longer claim Hermes is “last in the auto order”; the docs-fab/menu aria-labels are localized.
Notes
- Tests 4812 → 5036 unit, browser 118 + new navigation/contrast contracts, coverage mean 98.15 % line / 89.34 % branch (floor 96/86).
- Not in this release: the
#/scanredesign (Phase 4 — v1.244.0, spec drafted); a dedicatedexport.docxFaileddict key (the failure reusescommon.error);5 lakhs INRstill parses as 5 (pre-existing).
[1.242.1] — 2026-10-08
Fixed
- The release archive ships no
node_modulessymlinks again. The v1.242.0 git tag carried a rootnode_modulessymlink andsite/node_modules(a blanketgit add -Aslipped them past the trailing-slash ignore patterns), which failed the server deploy’s tar extraction — the deploy rolled back safely and production stayed on 1.241.1. Both symlinks are untracked and the ignore patterns widened; the npm tarball was never affected (npm excludesnode_moduleswhen packing).
Notes
- App code is identical to v1.242.0 — packaging only. The sources-correctness content ships to production with this deploy (v1.242.0 never reached production; its server deploy rolled back on the tar failure).
- Deliberately not done: re-tagging v1.242.0 (npm already published 1.242.0; the registry tarball is clean — npm excludes
node_moduleswhen packing).
[1.242.0] — 2026-10-07
The scanner’s data layer is trustworthy again: dead sources are alive, a malformed answer can no longer masquerade as an empty board, and a hostile board URL can no longer redirect a scan.
Fixed
- Dead sources live again. justjoin (the API envelope changed to
{data, meta.next.cursor}) reads its full catalog through a cursor walk with the new salary fields; nofluffjobs answers 400 withoutsalaryCurrency— the parent’s required query is sent and results paginate (pageTo). himalayas and jobicy saw only the newest ~20 postings of 100k+ — both now walk their full catalogs (offset / cursor pagination with page caps). - A 200 with the wrong shape throws on page 1. ~40 sources read malformed or challenge answers as “0 postings” and the scan “succeeded” silently. New
server/lib/sources/_shape.mjsguards (requireArray/requireContainer/requireObject) make the failure loud across every family; a later-page failure keeps the pages already collected; pagination stops on the raw page length, not the filtered count. - SSRF: substring vendor checks replaced with parsed exact-host pins. lever (
clever.comcontainslever.co), greenhouse (5 hosts + the legacyboards[.eu].greenhouse.iopath-slug hosts), ashby, smartrecruiters, workable, gem REST, ibm, arbeitsagentur, hecklerkoch (www only), workingnomads (http://169.254.169.254was reachable past the DNS guard), remoteok, remotive, rss, successfactors.redirect:'error'everywhere; job URLs must behttps:on the pinned host (javascript:/data:/ off-host dropped). - One misconfigured portals entry can no longer abort a whole scan.
resolveAdaptercatches adapter throws,detectApiis wrapped per company, throwing adapters returnnull; comeet fetch errors redact?token=before they reach logs or quarantine records. - Pagination and freshness. workday walks tenants past 100 postings (offset pages,
MAX_PAGES); telegram keeps the NEWEST posts and paginates?before=instead of quietly keeping the oldest; tencent walks on whenCountis missing; taleo parses French heading tables; avature stops on raw page length; UTC date stamps (amazon off-by-one east of UTC, torre, workingnomads, trudvsem); HTML entities decoded (geekjob, getmatch, hh); mycareersfuture emits the full 12-field job shape. - Remote detection tightened. arbeitsagentur, vdab, rippling, teamtailor: contract-percentage (“Verpleegkundige (100%)”,
bundesweit) and the word “distributed” (“Distributed Systems Engineer”) no longer tag on-site roles Remote.
Added
tests/adapters-pin-coverage.test.mjscompletes the host-pin branch coverage the pins introduced.
Notes
- Tests 4419 → 4812 unit, browser 118. Coverage mean 98.15 % line / 89.36 % branch (gate floor 96 / 86).
- Not in this release: the client findings from the same review round (Phase 3 — views, libs, CSS/a11y, i18n);
sources/habr.mjsstill lacks a dedicated test file (ratchet exemption).
[1.241.1] — 2026-10-07
Live evaluations work end to end in every locale, the job map places long multi-location postings, and a deploy no longer kills a running scan.
Fixed
- Evaluations see the whole method. The context budget (144 KB) still cut the end of
modes/oferta.mdonce the CV and profile were inlined, so every live evaluation missed its last blocks. The budget is now 192 KB and the prompt soft cap 256 KB, defined once inllm-dispatch.mjs. - Reports in the chosen language. The language instruction now opens the prompt as well as closing it, and ordinary words (role, team, design…) are written in the report’s language; only names, technologies and word-for-word quotes stay English. Fixes Hindi and Japanese reports that were mostly English.
- Slow evaluations no longer end in HTTP 502. The evaluation timeout is 300 s (was the provider default, 180 s).
- Hindi block letters. A report that spells the A–G letters in Devanagari (
## ए),## बी)…## जी)) is recognised, andSCORE:accepts local digits (३.८,٣٫٨,3.8) and a decimal comma. - Job map: long locations. A cell like “Berlin Office · Berlin · Germany · Bosnia · …” (300+ characters) was refused with HTTP 400 and the posting had no marker. The first real place is used now (
·,;,|,/, line breaks; “Remote …” segments, “(HQ)” and “Office” are skipped); only a query over 2000 characters is refused. - Deploy vs. the hourly scan. The deploy holds the scan and auto-evaluation timers, waits for a running one to finish (up to 15 min), restarts, and always resumes the timers — rollback included.
Added
- Job map on cvstart.org in all 17 languages, and in the English and Russian READMEs.
- Job map screenshots in every language — one per locale (
scripts/capture-map-screenshots.mjs), in all 17 READMEs, on cvstart.org and in a new help subsection, “How to use the map” (§33, ×17).
Notes
- Tests 4404 → 4419, browser 118. Remote QA (all routes, site, links and live LLM × 17 locales) green on production.
- Not in this release: streaming evaluations — the 300 s timeout stays a hard limit (a slow local model can still hit it), and the Hermes bridge keeps its non-streaming call.
[1.241.0] — 2026-10-07
Hardening release from a 28-agent code review (~190 verified findings): the security, process-crash and data-loss classes are closed, plus a job map contributed by @bullitt186.
Added
- Job map (
#/map) — postings from the pipeline, the last scan and the tracker on an OpenStreetMap map, geocoded through Nominatim with an identifying User-Agent, a 1 req/s queue, back-off and an on-disk cache. Leaflet is served from this server (no CDN); the CSP only gains the tile origin inimg-src. Contributed by @bullitt186 (#381). - Request guard — the server refuses a
Hostit does not serve (DNS rebinding) and cross-site writes and side-effecting GETs (/api/stream/*,/api/run/*,/api/geocode). A reverse proxy that forwards a public name lists it inALLOWED_HOSTS; the deploy workflow writes that systemd drop-in from the public URL. - Coverage gate —
npm run test:coverage:gate(scripts/coverage-report.mjs) prints per-file line/branch coverage and fails below a ratcheting baseline.
Fixed
- A malformed request no longer stops the server. Express 4 ignored rejected async handlers, so a wrong-typed field in
/api/deep,/api/auto-pipeline,/api/career-plan,/api/trackeror a NUL byte in/api/statsexited the process; they are now JSON 400s, and any other rejection becomes a 500. - An aborted or failed scan no longer wipes the last results.
last-scan.jsonis kept when a scan is stopped or every source failed, and a single-company scan merges instead of replacing. One misconfiguredportals.ymlentry no longer aborts the whole scan. - Evaluations see the whole method. Mode files were cut at 16 KB, so
oferta.md(92 KB) lost blocks C–G; per-file caps are now 128 KB for modes and 64 KB for the CV, and a cut is reported. Empty or cut-off answers fail instead of writing an empty report. - SSRF: redirect hops are re-checked for private targets, every DNS answer is checked, the scan deadline covers the body,
*_BASE_URLkeys only go to the vendor’s host (Hermes/Ollama: loopback or LAN), pandoc runs with--sandbox. - Event-loop stalls: two quadratic regexes (HTML stripping, markdown sanitising) replaced with linear scanners.
- Data loss: deleting a pipeline row kept the other rows’ compensation;
$&in a URL corruptedpipeline.md; mock-interview, networking and deep-research saves overwrote earlier files; interview history kept the first 40 turns instead of the last. - LLM layer: auto-pipeline and deep research honour
LLM_PROVIDERand every provider through the shared dispatch, record usage and report truncation; Gemini evaluate/test no longer save a report; OpenAI usesmax_completion_tokens, and a Codex-onlyOPENAI_MODELis sent asgpt-5; the job description is fenced as untrusted data. - Report, deep and inline PDFs pass
--skip-fact-check(the CV fact check blocked any report with numbers); the profile’s page format is no longer overridden. - Scanners: quarantine is keyed by URL (fixing a URL retries it), within-run dedup by canonical URL, blank list entries no longer match everything, atomic writes,
skipped_*history rows are not “seen”. - Process control: the SIGKILL escalation never fired; batch runs kill their whole process group on disconnect.
- CI and release: CI now runs the changelog-parity, workflow and i18n gates; least-privilege workflow permissions; production secrets only under the
productionenvironment; the npm package now shipsdocs/help/. - Tests: no test touches the network or the real parent project any more (an explicit
CAREER_OPS_ROOTis always honoured); copied algorithms replaced by the production code.
Notes
- Tests 4056 → 4404, browser 118. Server coverage baseline: line 96.7 %, branch 86.3 % (
scripts/coverage-baseline.json). - Specifications now live in one folder,
docs/sdd/specs/; the program plan isdocs/sdd/PLAN.md, the invariant→gate matrixdocs/sdd/TRACEABILITY.md. - Operator note: behind a reverse proxy set
ALLOWED_HOSTS=<public host>; an Ollama/Hermes base URL on a public host is now refused. - Not in this release, deliberately (see
docs/sdd/PLAN.md): the sources-correctness pass (justjoin and nofluffjobs are dead upstream-side, ~40 sources still read a malformed 200 as an empty board) → v1.242.0; client, CSS and a11y review findings → v1.243.0; the#/scanredesign → v1.244.0; the Tamil locale → v1.245.0.
[1.240.0] — 2026-10-06
Parent parity — career-ops main @ 62905981 (VERSION 1.35.0, 177 upstream commits since b39931e). Six new sources — 103 → 109 (104 EN + 5 RU) — fixes mirrored into eight existing providers, and the checklist pipeline format, contributed by @bullitt186.
Added
- Six new sources — 103 → 109 (104 EN + 5 RU), 98 → 104 EN adapters.
- ADP Workforce Now (
workforcenow.adp.comwithcid+ccId) —$skippagination by rows actually returned; a response with neitherjobRequisitionsnormetathrows instead of reading as an empty board; opt-in detail enrichment (adpWorkforcenow.fetchDetails, default 25, cap 100). - Gupy (
employability-portal.gupy.io) — board-wide keyword sweep, newest-first with an early stop onsince_days;workplaceTypeis read as the singular pt-BR string so hybrid postings are not lost; confidential career pages are dropped. - JazzHR (
*.applytojob.com) — card parser with a bare-anchor fallback so a partial redesign still yields postings; posting links with zero parsed cards throw, a truly empty board returns[]. - Startup Jobs (
startup.jobs/feeds/jobs) — RSS, one request; a malformed, HTML or truncated body throws instead of returning[](the parent’s post-merge review fix,08fe5d06); CDATA and comments are masked before item matching. - Taleo (
<tenant>.taleo.net,tre.taleo.netrefused) — shell GET thensearchjobsPOST; a shell with no portal id throws as a private board; columns matched by heading. - UKG Pro / UltiPro (
recruiting[N].ultipro.{com,ca}) —Top=50+Skip,totalCountnever trusted as a stop condition, page cap 100 (clamped at 1500), opt-in detail pages parsed with a string-aware brace walk.
- ADP Workforce Now (
Fixed
- Pipeline in the parent’s checklist format — upstream career-ops writes
data/pipeline.mdas## Pending/## Processedsections of- [ ] url | …rows, which the UI read as 0 pending and wrote to by appending a fence at the end of the file, where the parent never looks.parsePipeline,addPipelineUrlandremovePipelineUrlnow read and write that shape (dedup against processed rows too; compensation as a labelednote: comp …segment); the fenced format is unchanged. Contributed by @bullitt186 (#380). - Country folded into location — Ashby (primary
address.postalAddress), Breezy (country.name) and Recruitee (flatlocation) append the country unless the name already carries it as a whole word, so a UK-primary, US-secondary remote role is no longer dropped by a location filter. - MokaHR — the
hire-r1.mokahr.comregional host is accepted (allowlist is exactlyapp.mokahr.com+hire-r1.mokahr.com), API POST and job links use the board’s own host, and an in-bandsuccess:falsenow throws instead of reading as an empty board. - Avature —
article--jobscards parse (title from the header anchor, location from theicon-addressspan). - SAP SuccessFactors — a
/go/<Category>/<id>[/<offset>]/saved-search URL resolves to the tenant root; it used to hit…/tile-search-results/, which answers 200 with zero tiles. - Deutsche Bahn — db.jobs blanks any results page that holds a posting it cannot render; the walk now sorts by
pubExternalDate_tdtwith 1000 per page (5 pages max), stops on the raw hit-anchor count, and throws on a missing results header or unparsed posting links instead of returning a short board. - HigherEdJobs — an Incapsula challenge answered with HTTP 200 used to parse as zero items and read as an empty board; it now throws.
- A test read the real parent project on a developer machine —
tests/reports-list-cache.test.mjssetCAREER_OPS_ROOTto a temp directory with nocv.md/portals.yml, whichpaths.mjsignores, so locally it counted the parent’s reports (6 instead of 3) while CI, with no parent beside it, passed.
Notes
- Tests 3782 → 4056. New suites:
sources-adp-workforcenow,sources-gupy,sources-jazzhr,sources-startup-jobs,sources-taleo,sources-ultipro,sources-location-country-fold,sources-deutschebahn-blanked-page,sources-higheredjobs-challenge-page, plusparsers-pipeline-checklistfrom #380. - Parent: the fork was brought to upstream
main(144 → 0 commits behind) with the four ADR-0002 divergences verified intact; upstream’s revert of the CyrillicLOCATIONISH_REdid not come through. - Already shipped in v1.239.0, not repeated: AppliTrack, NEOGOV, Red Rover, SchoolSpring, OCC Mundial, Greenhouse embed boards, Workday
myworkdaysite.com. - Not ported, with reasons: trusted-proxy egress (
CAREER_OPS_TRUST_PROXY_EGRESS,_http.mjs/_ip-guard.mjs/_dns-cache.mjs) — web-ui scans through its ownhttp-json.mjstransport; thescan-history.tsvcolumn registry and language-aware requisition dedup — web-ui reads positions 1–12, which did not move, and dedups by URL; SmartRecruitersrequisitionId/language— no consumer in web-ui’s job shape; GemisoCountry→ country name — needs the parent’s 249-entry alpha-3 table, deferred; CLI-only changes (modes, updater, doctor, PDF/ATS payload, tracker scripts, Go dashboard, Singapore mode).
[1.239.4] — 2026-09-29
Patch from the v1.239.3 live regression.
Fixed
- Korean and Traditional Chinese evaluations reported a missing score summary. Those reports run to 10–11 thousand characters, and the summary comes last, so it fell past the 8192 output-token limit. Evaluations (and the auto-pipeline) now allow 16384 tokens, and when a provider still cuts an answer off, the first warning says so instead of listing missing blocks.
- The live check read a short German answer as not German (3 German stop words, no English ones). A list-shaped answer can hold few stop words; with no English ones at all, three are now enough.
- Tests 3778 → 3782.
Notes
- The deep-research and mode routes keep their 8192-token limit: their answers are not long enough to reach it in the live runs.
[1.239.3] — 2026-09-29
Patch from the v1.239.2 live regression.
Fixed
- Arabic evaluations still failed the A–G check after v1.239.2 fixed Japanese and Hindi. A masked heading outline (no report text in the log) showed why: the Arabic report writes the block letter in Arabic, in abjad order, whatever the prompt asks. The check now accepts أ ب ج د هـ و ز as A–G when it stands alone near the start of a heading, as it already accepted Cyrillic look-alikes; the same letters inside an Arabic word do not count.
- The live check prints that masked heading outline when an A–G check fails, so the next case can be diagnosed from the log.
- Tests 3776 → 3778.
Notes
- The v1.239.2 note said the check would not be loosened; for Arabic it now is, because three live runs showed the prompt alone does not hold the Latin letter.
[1.239.2] — 2026-09-29
Patch from the v1.239.1 live regression.
Fixed
- An Arabic evaluation translated its block letters and a Japanese one dropped the score summary. v1.239.1 restated only the language as the prompt’s last line. That line now also says the Latin letter A–G in each heading and the
---SCORE_SUMMARY---block (markers and keys in English) must not be translated, and asks for the language’s own script. - Tests 3775 → 3776.
Notes
- The report check is not loosened to accept translated letters or markers: the prompt keeps the format stable instead.
[1.239.1] — 2026-09-29
Patch from the v1.239.0 live regression.
Fixed
- Japanese and Ukrainian evaluations came back mostly in English. The language instruction sat at the top of the prompt, before tens of kilobytes of English project files and the job description. The prompt now restates the language as its last line.
- The live check reported a failed scan when the server was already running one (the hourly scan, retried right after the deploy). The server refusing a second scan is correct; the check now waits and retries, up to 30 minutes.
- Tests 3774 → 3775.
Notes
- The server’s single-flight scan is not relaxed: a second scan is still refused with
SCAN_BUSY. Only the check changed.
[1.239.0] — 2026-09-29
Parent parity with career-ops @ b39931e: five new sources and three mirrored fixes.
Added
- Five new sources (98 → 103; English adapters 93 → 98): AppliTrack (
applitrack.com/<district>), NEOGOV (SchoolJobs / GovernmentJobs), Red Rover (jobs.redroverk12.com/org/<id>) and SchoolSpring (<district>.schoolspring.com) for school and public-sector hiring, plus OCC Mundial, Mexico’s largest job board (explicitprovider: occwith search queries). Each is pinned to its host, HTTPS-only and refuses redirects. - Tests 3500 → 3774.
Fixed
- Greenhouse embed boards (
job-boards.greenhouse.io/embed/job_board?for=<board>) resolved to a board named “embed” and failed; the board now comes from?for=, and only on a real Greenhouse host. - Workday boards on
myworkdaysite.com(/recruiting/<tenant>/<site>) were not recognised; they now scan, with posting links on the same host. - A report file name with parentheses (
042-acme-(berlin)-….md) was cut at the first)in the tracker; the whole name is kept now.
Notes
- CLI-only parent changes are not ported — PDF and CV templates, the scaffolder, the batch runner, the career-profile import, ATS keyword coverage, interview-mode translations, the
piCLI host: web-ui does not run them. - Opening such a report from the tracker still needs the report route to accept parentheses in a file name; that route is not changed in this release.
[1.238.3] — 2026-09-27
Pages stay responsive while a scan runs, and remote QA logs no longer leak the production host.
Fixed
- The dashboard and reports took seconds to load, and timed out during a scan.
/api/reportsand/api/dashboardread and parsed every report on every request — about 3.7 s on the production server, blocking every other request; with a scan running, page loads hit 30-second timeouts. Parsed report headers are now cached per file and refreshed when the file changes, so a listing costs one file check per report (500 reports: 430 ms → 3 ms locally). - A remote QA finding could print part of the production hostname into a public log. GitHub masks a secret only where its full value appears, and an error message cut to 180 characters ended mid-host. Findings and whole reports are now redacted before they are cut.
- Every AI evaluation was flagged as malformed. The prompt never asked for the
---SCORE_SUMMARY---block the report check requires (the parent’s evaluation scripts do), and a translated report translated the word “Block”, so the check missed its sections. The prompt now asks for the summary and for the block letter in every heading, the check accepts a translated word before the letter, and the summary block is removed before the report reaches the page or is saved.
Added
- Tests 3488 → 3500.
Notes
- The scanner still runs inside the server process; this release removes the other big blocker but does not move the scan to a worker.
[1.238.2] — 2026-09-27
Fix for the 502s the production server still showed while the hourly scan ran.
Fixed
- Pages and assets returned 502 while a scan ran. Node closes an idle keep-alive connection after 5 s; Caddy reuses idle upstream connections for up to 2 minutes. While an in-process scan kept the server busy, Node closed sockets Caddy had just sent a request on, and Caddy answered 502 connection reset by peer (62 times during one scan, with the server itself healthy). The server now keeps idle connections for 125 s — longer than the proxy — so the proxy always closes first.
Added
deploy.yml→verifyshows why the proxy answered 502 (error kinds, addresses masked), 502s per minute since the last restart, and the hourly scan’s outcome.release.ymlcan create a missing tag when started by hand, so a release can be cut where tags cannot be pushed.- Tests 3486 → 3488.
Notes
- The scan still runs inside the server process and can keep it busy for seconds; moving it to a worker is not part of this patch.
[1.238.1] — 2026-09-27
Stability patch from the first live regression of the production server: one scan at a time, and site fixes.
Fixed
- Two scans at once crashed the server. Every
GET /api/stream/scanran the full scanner inside the server process with no guard; a scan started from the UI overlapped the hourly timer and the process died with JavaScript heap out of memory (twice, with 502s meanwhile). Scans are now one at a time: a second request gets an SSEerrorwith codeSCAN_BUSYand nothing starts. The deploy workflow also raises the server’s heap limit (heap_mb, default 448 MB). - cvstart.org
/helpand/changelogscrolled sideways on phones (and ja/zh/changelogon desktop) — long code and table-of-contents lines now wrap. - Hindi was missing from the sitemap’s language alternates; the site’s i18n check now compares the sitemap map with the locale registry.
- Help §19 told translators to add a key to “all 8” language files — there are 17.
Added
- Remote regression (
.github/workflows/remote-qa.yml): every route × every language on the deployed app (read-only), every page × language × phone/desktop on cvstart.org, every link in the READMEs and the wiki, and — on request — one real scan plus live AI calls in every language. - Server deploy workflow (
.github/workflows/deploy.yml):recon,deploy(with a byte-exact rollback) andverify(smoke test plus stability report). - Tests 3481 → 3486.
Notes
- The scanner’s own memory use is not reduced in this release: the guard stops two scans running at once and the heap drop-in raises the ceiling. Profiling a real 98-source scan needs the production network.
- The hourly
career-ops-scantimer is not changed;reconnow shows which commands it runs.
[1.238.0] — 2026-09-27
Parent parity — career-ops main @ 993085ce (VERSION 1.34.0, 182 commits since de2224a9). Four new sources, ten mirrored provider fixes, a scanner-wide SSRF gap closed, and a free title-fit hint on scan results.
Added
- Four new sources — 94 → 98 (93 EN + 5 RU), 89 → 93 EN adapters.
- Eploy (
provider: eploy) — reads the tenant’s/live-jobs.xmlsitemap on any branded host (HTTPS only; IP literals,localhost,*.local,*.internaland single-label hosts refused). A non-sitemap response (a login page) throws instead of passing as zero jobs. Detail pages are opt-in (eploy: { fetchDetails, detailLimit }). - HiringRoom (
*.hiringroom.com) — JSON-LD/portal/jobsfirst, legacy/jobscards as fallback;http://posting URLs in the payload are rebuilt on HTTPS. - PeopleSoft Candidate Gateway — detected by the
/psc/{site}/EMPLOYEE/HRMS/c/HRS_HRAM_FL.HRS_CG_SEARCH_FL.GBLpath on any branded host. It follows PeopleSoft’s same-origin session redirects itself (max 10 hops, every hop re-pinned to the tenant origin and checked against private addresses), replays the “load more” form, and marks a partial walk (peoplesoftIncomplete) rather than reporting it as complete. - PrevueAPS (
*.prevueaps.ca) — resolves the site id from the jobs page, then reads the JSON API.
- Eploy (
- Title-fit hint on
#/scanrows. Each result carries a coarse strong / related / weak fit chip comparing the posting title with the target roles inconfig/profile.yml(target_roles.primary+ archetype names). It is a free keyword estimate: it never filters, reorders or changes counts, and the tooltip says so. Computed when results are served, so a profile edit shows on the next refresh. Translated ×17. Mirrors parent #3261.
Fixed
- Security — the DNS-rebinding guard never ran on a real scan.
http-json.mjsonly checked resolved addresses when handed the bare globalfetch, but both scanners injectmakeTimeoutFetch(), so every scan skipped it. A source pointed at a hostname resolving to10.xor127.0.0.1was fetched. The guard now runs insidemakeTimeoutFetchitself, raced against the scan timeout so a stalled resolver cannot hang a scan. Found in the review of this release. - Security — IPv6 addresses embedding an IPv4 address were not treated as private.
http://[::ffff:127.0.0.1]/(which the URL parser rewrites to[::ffff:7f00:1]) passedisValidJobUrl. Both the dotted and hex forms of::ffff:a.b.c.d/::a.b.c.dare now classified by the embedded address. Parent_ip-guard.mjs. - Rippling moved to the v2 board API (
ats.rippling.com/api/v2/board/<slug>/jobs), with pagination (default 10 pages, max 50), a host lock on job links, and slug validation. A pasted API URL now yields its real slug instead ofapi. - Recruitee — multi-location offers list every place (
A · B),(Sample)demo postings and title-less rows are dropped, and an offer is kept when either of its two URLs is valid. - Pinpoint — seeded demo postings (the doubled demo video) are dropped.
- Telegram channels — a first line with several
|is no longer read asTitle | Company, and more role words are recognised. The fork’s Cyrillic-awareLOCATIONISH_REis untouched (ADR-0002). - Workday — a first-page 422/401/403 is probed once; Workday’s maintenance page or outage redirect marks the board dead (quarantined in strict mode). The CXS and detail requests now refuse redirects.
- BambooHR — a redirected
/careers/listis triaged through the public embed feed: empty means a live board with no openings, anything else is a dead board. - Heckler & Koch / Rheinmetall — a bare
heckler-koch.com/rheinmetall.comis rewritten towww.so the no-redirect transport still reaches it. Ashby refuses redirects. beesite paces pages with the cancellabledelay(). - Title filters fold accents on both sides (EN and RU scanners), so
Développeuris caught by adeveloppeurnegative. - Cooldown company matching understands non-Latin names and corporate forms (CJK/Korean markers, Turkish dotted İ). CJK names used to collapse to an empty key and never match.
location_filter.strict: truefails closed on an empty location when an allow or block list is set.- Liveness — new public-API checks for Greenhouse embeds (
gh_jid), Arbeitsagentur and We Work Remotely; SmartRecruiters now decides by the posting’sactiveflag. - PeopleSoft — a relative “load more” form action resolves beside the search page, not at the origin root.
Notes
- Tests 3210 → 3481. New suites:
sources-eploy,sources-hiringroom,sources-peoplesoft,sources-prevueaps,title-fit,fetch-timeout-dns-guard,cooldown-company-match-unicode,liveness-api-more-rungs,ru-scanner-title-accent-fold. - Not ported, with reasons: location-filter Unicode word edges (web-ui’s filter is substring-based, so there is no edge to fix — a deliberate full port of the parent’s location filter is future work); same-title requisition dedup (web-ui dedups by URL only); aggregator repost warnings, blacklist domain scope, unverified-zero receipts and
scan-runs.tsvmigration (web-ui has none of those surfaces); Workday facet-split and truncation tags (web-ui fetches one page per board); BambooHRenrichDate(no date-enrichment hook); the parent’s_http.mjssleep refactor across ~20 providers (web-ui already uses a shared cancellabledelay); updater, doctor, PDF, tracker/merge-tracker,validate-profile,discover-new-companies,web/Next.js app, GitHub bots — CLI-only or relayed. - Kept deliberately different from the parent: Heckler & Koch / Rheinmetall stay HTTPS-only (no
http→httpsupgrade); a dead Workday board in a normal scan still returns[]rather than failing the scan.
[1.237.1] — 2026-09-22
Patch from an external QA pass on v1.237.0. The Jobstreet fix was confirmed from outside for the first time: on the AU, NZ, HK and MY hosts the old /id/job/<id> form returns a clean 404 and the new /job/<id> reaches a real Cloudflare-guarded route; on id.jobstreet.com the reverse holds. The lost links were real. Two defects were found, both in what v1.237.0 itself added.
Fixed
- The book link’s label was localized but its target was not. All 17 SPA locales and all 17 cvstart.org locales pointed the Agentic Coding Design Patterns footer link at the English edition, while the one translated label — Russian, «Паттерны агентного кодинга» — promised a Russian book. A new
data-i18n-hrefapplier (same shape asdata-i18n-title, restricted to absolutehttps://values) and afooter.patternsUrlkey sendruto the Russian edition and every other locale to the English one. The regression test asserts the invariant in both directions: a translated title implies a localized URL. - SEEK is migrating hosts, and the transport refuses redirects.
www.seek.com.au→au.seek.comandwww.seek.co.nz→nz.seek.com: the detail pages already 301, and both host pairs serve the v5 search API at 200 today. Becausehttp-json.mjssetsredirect:'error'(an SSRF defence worth keeping), the day the API path starts redirecting an AU/NZ entry on the old host would fail — and with only thewww.hosts allowlisted nobody could migrate.au.seek.comandnz.seek.comare now inALLOWED_JOBSTREET_HOSTS; the old hosts stay, so existing config and already-built links keep working. Look-alikes (au.seek.com.evil.com,au-seek.com) are still refused. - Wiki: the Agentic Practices row in the Home documentation map was the only one of twelve not escaping
|inside[[…|…]], so GitHub cut the cell — dead link, description hidden. One character.
Notes
- Counts unchanged at 94 sources (89 EN + 5 RU), 89 adapters. Tests 3201 → 3210 (+5 footer-link locale, +4 SEEK hosts).
- Not closable from outside, recorded rather than claimed: Oracle Cloud (Akamai blocked both REST probes), the Personio pin and
appendWorkType(need aportals.ymlentry, and the UI has no delete for one), and the liveness string cases (no controllable page; 40 live probes gave 12 live, 28 inconclusive, 0 false expired). - Help ×17 now list both host forms for
AU-MainandNZ-Main. Heading structure unchanged at 32 H2 / 122 H3.
career-ops-ui v1.267.1 · page 2/10 · mirror of the project changelog for cvstart.org