Журнал змін

GitHub

Кожна значуща зміна, реліз за релізом. Той самий файл постачається в репозиторії та перекладений усіма 19 мовами.

[1.244.0] — 2026-10-08

Результати скану читаються списком, а не стіною: одна вакансія = два рядки, сигнали — іконки, а скан на 700 рядків — це таблиця з посторінковим розбиттям замість сторінки на 37,000 px.

Виправлено

  • Анатомія рядка. Кожна вакансія рендериться як обмежений дворядковий рядок: рядок 1 — назва, рядок 2 — company · location · source · date · work-type. Бейдж буста, смуга відповідності назви та бал відповідності стали іконками з локалізованими accessible-назвами й тултіпами (скрін-рідери читають «Соответствие: сильное» / “Fit: strong”, а не стіну слів); бейджі довіри та релокації згорнулися в ту саму рейку. Сеніорність і зарплата лишаються допоміжними колонками лише для десктопа.
  • Гігієна назв. Хвостовий сегмент | Germany | Remote відділяється від назви в мета-рядок — з урахуванням Unicode, лише коли хвіст є справжнім маркером типу роботи, а середній сегмент схожий на місце (легітимні вертикальні риски на кшталт “C++ | Rust | Go Developer” ніколи не розрізаються).
  • Більше жодного горизонтального розповзання. Фіксоване компонування таблиці, min-width: 0, назва/мета в один рядок із трьокрапкою і повний текст у тултіпі: нульове переповнення сторінки та нуль внутрішнього скролу таблиці на 1440 px і 390 px (стара сторінка переповнювалася на 675 px при ширині телефону).
  • Пагінація. Результати рендеряться по 50 на сторінку (вибір 25/50/100/200) — скан на 700 рядків тепер заввишки ~4,600 px замість ~37,000 px, а DOM тримає ~1,000 вузлів замість 5,500+.

Додано

  • Layout contract-тести: дворядковий рядок, accessible-назви іконок (en + ru), бюджет вузлів пагінації та guard кількості/порядку title-fit виконуються в браузерному наборі — редизайн не може мовчки регреснути.

Примітки

  • Тести 5036 → 5045 юніт (браузерний набір отримав layout contract-тести), середнє покриття 98.14 % рядків / 89.34 % гілок (нижня межа бар’єра 96/86).
  • Те, що знаходить сканер, формули оцінювання та last-scan.json — недоторкані; title-fit лишається анотацією.
  • Не ввійшло в реліз: стан фільтрів в URL-хеші та навігація рядків клавіатурою (follow-up); розбір 5 lakhs INR (було й раніше).

[1.243.2] — 2026-10-08

Виправлено

  • Деплоїбельний реліз вирізано як v1.243.2. Тег v1.243.1 було перевказано після публікації, а серверний git fetch --tags коректно відмовляється затирати наявний тег — тому v1.243.1 виявився недеплоїбельним. Цей тег несе те саме дерево (фікc window-лістенера модалки на базі v1.243.0) плюс це підвищення версії. Більше нічого не змінилося; опублікований npm 1.243.1 тарбол уже містив фікс.

Примітки

  • Код застосунку ідентичний v1.243.1 — новим є лише тег для розгортання (git fetch --tags на сервері відмовив перевказаній v1.243.1).
  • Свідомо не зроблено: примусове оновлення тегу на сервері (опубліковані теги не переміщують).

[1.243.1] — 2026-10-08

Виправлено

  • Модалка «Записати підсумок» тепер справді закривається при навігації. Гард v1.243.0 слухав hashchange на document, де ця подія ніколи не спрацьовує — фікс приїхав мертвим, і модалка досі блокувала наступну вʼюху (піймано живим регресом на проді в раунді v1.243.1). Гард слухає на window; тест тепер диспетчить туди ж і перевірений червоним на зламаному коді.

[1.243.0] — 2026-10-08

Шар браузера береже вашу роботу і говорить вашою мовою: без втрат незбережених редагувань, без мовчазних збоїв, без тихої англійської в перекладеному інтерфейсі, контраст AA всюди.

Виправлено

  • Незбережені редагування переживають повторні рендери. Редактор CV тримає «брудний» буфер і відновлює його під час кожного рендеру — «Скасувати», перемикання мови або перехід у CV Studio тепер запитують підтвердження замість того, щоб викидати правки; згенерований текст кар’єрного плану зберігається так само. Збої завантаження в Config / CV Studio / career-plan / memory / two-pager показують помилку й вимикають «Зберегти» замість того, щоб мовчки перезаписувати файл порожнім.
  • Модалка «Записати результат» більше не переживає навігацію. Якщо відкрити її на #/tracker і клацнути інший вигляд, вона залишалася, блокуючи всю сторінку (знайдено в регресії two-stand v1.242.1) — тепер вона закривається при зміні маршруту; її сусідні scan-вигляди теж зупиняють свої SSE-потоки й відв’язують обробники при навігації.
  • Розбір зарплат і ключових слів розуміє будь-яку писемність. Чипси ключових слів використовують межі слів Unicode (кирилиця, c++, .net тепер фільтруються коректно, а не дають нуль рядків); один спільний парсер зарплат виправляє $182.9K - $240K, от 100 000 руб, 100 тыс. руб., 50 000 Kč (раніше — 50 млн USD) і семантику діапазонів ($100-150K); обов’язкова вимога «Germany or Netherlands» більше не рахується водночас і виконаною, і порушеною.
  • Потоки завершуються рівно один раз. Обірване SSE-з’єднання доставляло помилку двічі і могло назавжди лишити Generate вимкненим; кожен термінальний стан (done/error/close/throw, включно з убитим дочірнім процесом) завершується одним шляхом і знову вмикає кнопку. Авто-pipeline виштовхує свій останній буферизований кадр замість того, щоб його втрачати.
  • Приватність і коректність у дрібних шляхах. Тіло bug-репорту більше не несе URL вакансії, що оцінювалася, або слагів звітів; приватність CV маскує голі посилання linkedin.com/in/… та імена КАПСОМ і перестає псувати європейські дати; визначення країни бере найдовший збіг і відмовляється вгадувати, коли названо дві країни («Cambridge, MA» більше не → UK); завантаження файлів зберігають кириличні імена.
  • Роутер і доступність (a11y). Skip-посилання більше не веде 404-ю в ontent; активний пункт навігації несе aria-current="page"; один <main> / один <nav>; Escape закриває мобільну шторку (тепер вона справді поза порядком табуляції, коли закрита); scroll-padding-top тримає фокус видимим і повз липкі панелі; липкий банер більше не перекриває топбар.
  • Контраст — AA в обох темах — темні тости були білим по білому (1.10:1): тости, кнопки, банери та посилання на --rausch тепер розв’язуються через тема-інваріантні токени, під захистом обчислюваних WCAG contract-тестів (≥ 4.5:1).
  • Все інше з цього раунду: evaluate показує попередження про обрізання; memory / config / two-pager відмовляються «Зберегти» після невдалого завантаження; scan перестає кидати свої SSE-потоки сиротами; результати сканування класифікують API-дошки за серверним реєстром замість сплившого regex; «Proceed with Caution» у трекері — це попередження, а не червоний бейдж; stats згортає локалізовані статуси; стовпчикові діаграми коректно читаються в RTL; docs-FAB більше не перекриває контент на телефонах; GET /app.js відповідає 404 замість index-оболонки; GET /api/runners перелічує свої дії.

Додано

  • Данська приєднується до авто-визначення — браузер із da-DK тепер отримує данський інтерфейс (словник був повний, а визначення його пропускало); zh-Hant* мапиться на традиційну китайську. cv-diagnostics — чекліст резюме — повністю локалізовано (28 ключів diag.* × 17). Додано 26 міток field-spec; видалено 31 мертвий ключ; списки провайдерів більше не стверджують, що Hermes — «останній у порядку авто»; aria-мітки docs-fab і меню локалізовано.

Примітки

  • Тести 4812 → 5036 юніт, браузерні 118 + нові navigation/contrast contract-тести. Середнє покриття 98.15 % рядків / 89.34 % гілок (нижня межа бар’єра — 96 / 86).
  • Не ввійшло в реліз: редизайн #/scan (Фаза 4 — v1.244.0, специфікація вже накидана); окремий ключ словника export.docxFailed (збій перевикористовує common.error); 5 lakhs INR досі розбирається як 5 (було й раніше).

[1.242.1] — 2026-10-08

Виправлено

  • Релізний архів більше не містить симлінків node_modules. Git-тег v1.242.0 ніс симлінк node_modules у корені та site/node_modules (загальний git add -A провів їх повз ignore-патерни із завершальним слешем), через що впала tar-розпаковка серверного розгортання — розгортання безпечно відкотилося, прод залишився на 1.241.1. Обидва симлінки знято з трекінгу, ignore-патерни розширено; npm-тарбол не постраждав (npm виключає node_modules під час пакування).

Примітки

  • Код застосунку ідентичний v1.242.0 — лише пакування. Контент «коректність джерел» їде на прод цим деплоєм (v1.242.0 на прод не доїхав: серверне розгортання відкотилося після збою tar).
  • Свідомо не зроблено: перевипуск тегу v1.242.0 (npm вже опублікував 1.242.0; тарбол у реєстрі чистий — npm виключає node_modules під час пакування).

[1.242.0] — 2026-10-07

Шар даних сканера знову гідний довіри: мертві джерела оживли, некоректна відповідь більше не може видавати себе за порожню дошку, а ворожий URL дошки більше не може перенаправити сканування.

Виправлено

  • Мертві джерела знову живі. justjoin (оболонка API змінилася на {data, meta.next.cursor}) читає весь каталог обходом курсора з новими полями зарплати; nofluffjobs відповідає 400 без salaryCurrency — тепер надсилається обов’язковий запит батьківського проєкту, а результати пагінуються (pageTo). himalayas і jobicy бачили лише ~20 найновіших вакансій зі 100k+ — обидва тепер обходять повні каталоги (пагінація offset / курсором із лімітами сторінок).
  • Відповідь 200 неправильної форми кидає помилку на сторінці 1. ~40 джерел читали пошкоджені або challenge-відповіді як «0 вакансій», і сканування мовчки «завершувалося успішно». Нові бар’єри в server/lib/sources/_shape.mjs (requireArray / requireContainer / requireObject) роблять збій гучним у кожній родині джерел; збій на пізнішій сторінці зберігає вже зібрані сторінки; пагінація зупиняється за сирий розмір сторінки, а не за відфільтровану кількість.
  • SSRF: перевірки вендорів за підрядком замінено на розібрані точні прив’язки хостів. lever (clever.com містить lever.co), greenhouse (5 хостів + застарілі path-slug хости boards[.eu].greenhouse.io), ashby, smartrecruiters, workable, gem REST, ibm, arbeitsagentur, hecklerkoch (лише www), workingnomads (http://169.254.169.254 був досяжним повз DNS-захист), remoteok, remotive, rss, successfactors. Усюди redirect:'error'; URL вакансій мають бути https: на прив’язаному хості (javascript: / data: / з чужого хоста — відкидаються).
  • Один неправильно налаштований запис у portals більше не перериває все сканування. resolveAdapter перехоплює виключення адаптерів, detectApi обгорнуто для кожної компанії, адаптери, що кидають помилку, повертають null; помилки fetch у comeet редагують ?token= до того, як ті потраплять у логи або записи карантину.
  • Пагінація та свіжість. workday обходить tenants далі 100 вакансій (сторінки offset, MAX_PAGES); telegram залишає НАЙНОВІШІ пости і пагінує через ?before= замість того, щоб тихо лишати найстаріші; tencent продовжує обхід, коли Count відсутній; taleo розбирає таблиці з французькими заголовками; avature зупиняється за сирим розміром сторінки; мітки дат у UTC (amazon — зсув на одиницю на схід від UTC, torre, workingnomads, trudvsem); HTML-сутності декодуються (geekjob, getmatch, hh); mycareersfuture видає повну форму вакансії з 12 полів.
  • Виявлення Remote посилено. arbeitsagentur, vdab, rippling, teamtailor: відсоток контракту («Verpleegkundige (100%)», bundesweit) і слово «distributed» («Distributed Systems Engineer») більше не позначають офісні вакансії як Remote.

Додано

  • tests/adapters-pin-coverage.test.mjs доповнює покриття гілок прив’язок хостів, яке запровадили ці прив’язки.

Примітки

  • Тести 4419 → 4812 юніт, браузерні 118. Середнє покриття 98.15 % рядків / 89.36 % гілок (нижня межа бар’єра — 96 / 86).
  • Не ввійшло в реліз: клієнтські знахідки з того самого раунду рев’ю (Фаза 3 — views, libs, CSS/a11y, i18n); sources/habr.mjs досі не має окремого тестового файлу (виняток від бар’єра покриття).

[1.241.1] — 2026-10-07

Живі оцінки працюють наскрізь в усіх локалях, карта вакансій розміщує довгі вакансії з кількома локаціями, а розгортання більше не вбиває запущене сканування.

Виправлено

  • Оцінки бачать увесь метод. Бюджет контексту (144 КБ) і далі обрізав кінець modes/oferta.md, щойно вбудовувалися CV і профіль, тож кожна жива оцінка втрачала останні блоки. Тепер бюджет — 192 КБ, а м’яка межа промпту — 256 КБ; обидва значення задано один раз у llm-dispatch.mjs.
  • Звіти обраною мовою. Мовна інструкція тепер і відкриває промпт, і завершує його, а звичайні слова (role, team, design…) пишуться мовою звіту; англійською лишаються тільки імена, технології та дослівні цитати. Виправляє звіти гінді та японською, які здебільшого були англійською.
  • Повільні оцінки більше не завершуються HTTP 502. Тайм-аут оцінки — 300 с (було типове значення провайдера, 180 с).
  • Літери блоків гінді. Звіт, що записує літери A–G деванагарі (## ए), ## बी) … ## जी)), розпізнається, а SCORE: приймає локальні цифри (३.८, ٣٫٨, 3.8) і десяткову кому.
  • Карта вакансій: довгі локації. Комірку на кшталт «Berlin Office · Berlin · Germany · Bosnia · …» (300+ символів) відхиляли з HTTP 400, і вакансія лишалася без маркера. Тепер береться перше справжнє місце (·, ;, |, /, розриви рядків; сегменти «Remote …», «(HQ)» та «Office» пропускаються); відхиляється лише запит довший за 2000 символів.
  • Розгортання й щогодинне сканування. Розгортання призупиняє таймери сканування й авто-оцінки, чекає завершення запущеного (до 15 хв), перезапускає сервіс і завжди відновлює таймери, зокрема й під час відкату.

Додано

  • Карта вакансій на cvstart.org усіма 17 мовами, а також в англійському й російському README.
  • Знімки мапи всіма мовами — по одному на локаль (scripts/capture-map-screenshots.mjs), у всіх 17 README, на cvstart.org і в новому підрозділі довідки «Як користуватися мапою» (§33, ×17).

Примітки

  • Тести 4404 → 4419, браузерні 118. Віддалений QA (усі маршрути, сайт, посилання та живий LLM × 17 локалей) зелений на продакшні.
  • Не ввійшло в реліз: стримінг оцінок — тайм-аут 300 с лишається жорсткою межею (повільна локальна модель усе ще може в нього впертися), а міст Hermes і далі працює без стримінгу.

[1.241.0] — 2026-10-07

Реліз зі зміцнення за підсумками код-рев’ю від 28 агентів (~190 підтверджених знахідок): закрито класи безпеки, аварій процесу та втрати даних, а також карта вакансій, внесена @bullitt186.

Додано

  • Карта вакансій (#/map) — вакансії з пайплайна, останнього сканування й трекера на карті OpenStreetMap, геокодування через Nominatim з ідентифікаційним User-Agent, чергою 1 запит/с, відкладеними повторами та дисковим кешем. Leaflet роздається з цього сервера (без CDN); CSP лише додає origin тайлів до img-src. Внесено @bullitt186 (#381).
  • Захист запитів — сервер відхиляє Host, який не обслуговує (DNS rebinding), а також міжсайтові записи й GET із побічними ефектами (/api/stream/*, /api/run/*, /api/geocode). Зворотний проксі, що пересилає публічне ім’я, вказує його в ALLOWED_HOSTS; workflow розгортання записує цей systemd drop-in із публічного URL.
  • Бар’єр покриття — npm run test:coverage:gate (scripts/coverage-report.mjs) виводить покриття рядків/гілок по файлах і падає нижче базової лінії, що лише зростає.

Виправлено

  • Некоректний запит більше не зупиняє сервер. Express 4 ігнорував відхилені асинхронні обробники, тож поле неправильного типу в /api/deep, /api/auto-pipeline, /api/career-plan, /api/tracker або NUL-байт у /api/stats завершували процес; тепер це JSON 400, а будь-яке інше відхилення стає 500.
  • Перервене або невдале сканування більше не стирає останні результати. last-scan.json зберігається, коли сканування зупинено або всі джерела впали, а сканування однієї компанії зливається, а не замінює. Один неправильно налаштований запис у portals.yml більше не перериває все сканування.
  • Оцінки бачать увесь метод. Файли режимів обрізалися на 16 КБ, тож oferta.md (92 КБ) втрачав блоки C–G; тепер ліміти на файл — 128 КБ для режимів і 64 КБ для CV, а обрізання повідомляється. Порожні або обірвані відповіді завершуються помилкою, а не записують порожній звіт.
  • SSRF: переходи редиректів повторно перевіряються на приватні цілі, перевіряється кожна DNS-відповідь, дедлайн сканування охоплює тіло відповіді, ключі *_BASE_URL йдуть лише на хост вендора (Hermes/Ollama: loopback або LAN), pandoc працює з --sandbox.
  • Зависання циклу подій: два квадратичні регулярні вирази (вилучення HTML, санітизація markdown) замінено лінійними сканерами.
  • Втрата даних: видалення рядка пайплайна зберігало винагороду інших рядків; $& в URL псував pipeline.md; збереження mock-interview, networking і deep-research перезаписувало раніші файли; історія співбесід зберігала перші 40 реплік замість останніх.
  • Шар LLM: auto-pipeline і deep research дотримуються LLM_PROVIDER і всіх провайдерів через спільний диспетчер, записують використання й повідомляють про обрізання; Gemini evaluate/test більше не зберігає звіт; OpenAI використовує max_completion_tokens, а OPENAI_MODEL, призначений лише для Codex, надсилається як gpt-5; опис вакансії огороджено як недовірені дані.
  • PDF звіту, deep та inline передають --skip-fact-check (перевірка фактів CV блокувала будь-який звіт із числами); формат сторінки з профілю більше не перевизначається.
  • Сканери: карантин прив’язано до URL (виправлення URL дає повторну спробу), дедуплікація в межах запуску — за канонічним URL, порожні елементи списків більше не збігаються з усім, атомарні записи, рядки історії skipped_* не вважаються «баченими».
  • Керування процесами: ескалація до SIGKILL ніколи не спрацьовувала; пакетні запуски вбивають усю свою групу процесів при відключенні.
  • CI і реліз: CI тепер запускає бар’єри паритету changelog, workflow та i18n; workflow з мінімально необхідними правами; продакшн-секрети лише в середовищі production; npm-пакет тепер постачає docs/help/.
  • Тести: жоден тест більше не торкається мережі чи справжнього батьківського проєкту (явний CAREER_OPS_ROOT завжди враховується); скопійовані алгоритми замінено продакшн-кодом.

Примітки

  • Тести 4056 → 4404, браузерні 118. Базова лінія покриття сервера: рядки 96,7 %, гілки 86,3 % (scripts/coverage-baseline.json).
  • Специфікації тепер живуть в одній теці, docs/sdd/specs/; план програми — docs/sdd/PLAN.md, матриця інваріант→бар’єр — docs/sdd/TRACEABILITY.md.
  • Примітка для оператора: за зворотним проксі задайте ALLOWED_HOSTS=<публічний хост>; базовий URL Ollama/Hermes на публічному хості тепер відхиляється.

[1.240.0] — 2026-10-06

Паритет із батьківським проєктом — career-ops main @ 62905981 (VERSION 1.35.0, 177 комітів апстриму від b39931e). Шість нових джерел — 103 → 109 (104 EN + 5 RU) — виправлення, дзеркально перенесені у вісім наявних провайдерів, і формат checklist-пайплайна, внесений @bullitt186.

Додано

  • Шість нових джерел — 103 → 109 (104 EN + 5 RU), 98 → 104 EN-адаптери.
    • ADP Workforce Now (workforcenow.adp.com з cid + ccId) — пагінація $skip за кількістю фактично повернутих рядків; відповідь без jobRequisitions і без meta кидає помилку, а не читається як порожня дошка; опційне збагачення деталями (adpWorkforcenow.fetchDetails, за замовчуванням 25, максимум 100).
    • Gupy (employability-portal.gupy.io) — пошук за ключовими словами по всій дошці, від найновіших, із раннім зупинком за since_days; workplaceType читається як однинний рядок pt-BR, тож гібридні вакансії не губляться; конфіденційні сторінки кар’єри відкидаються.
    • JazzHR (*.applytojob.com) — парсер карток із резервом на голі якорі, тож часткове перероблення дизайну все одно дає вакансії; посилання на вакансії без жодної розібраної картки кидають помилку, а справді порожня дошка повертає [].
    • Startup Jobs (startup.jobs/feeds/jobs) — RSS, один запит; пошкоджене, HTML- або обрізане тіло кидає помилку, а не повертає [] (виправлення з рев’ю батьківського проєкту після злиття, 08fe5d06); CDATA і коментарі маскуються перед зіставленням елементів.
    • Taleo (<tenant>.taleo.net, tre.taleo.net відхиляється) — GET оболонки, потім POST searchjobs; оболонка без id порталу кидає помилку як приватна дошка; колонки зіставляються за заголовком.
    • UKG Pro / UltiPro (recruiting[N].ultipro.{com,ca}) — Top=50 + Skip, totalCount ніколи не береться за умову зупинки, ліміт сторінок 100 (значення обмежується 1500), опційні сторінки деталей розбираються обходом дужок, що враховує рядки.

Виправлено

  • Пайплайн у форматі checklist батьківського проєкту — апстримний career-ops пише data/pipeline.md як розділи ## Pending / ## Processed з рядками - [ ] url | …; UI читав їх як 0 в очікуванні, а записував, дописуючи fence у кінець файлу, куди батьківський проєкт не заглядає. parsePipeline, addPipelineUrl і removePipelineUrl тепер читають і пишуть саме цю форму (дедуплікація й щодо оброблених рядків; винагорода як позначений сегмент note: comp …); fenced-формат без змін. Внесено @bullitt186 (#380).
  • Країна додається до локації — Ashby (основна address.postalAddress), Breezy (country.name) і Recruitee (пласке location) дописують країну, якщо назва ще не містить її як ціле слово, тож віддалена вакансія з основною локацією у Великій Британії та другорядною в США більше не відкидається фільтром локації.
  • MokaHR — регіональний хост hire-r1.mokahr.com приймається (allowlist — рівно app.mokahr.com + hire-r1.mokahr.com), POST до API і посилання на вакансії використовують власний хост дошки, а success:false у тілі відповіді тепер кидає помилку, а не читається як порожня дошка.
  • Avature — картки article--jobs розбираються (назва із якоря в заголовку, локація зі span icon-address).
  • SAP SuccessFactors — URL збереженого пошуку /go/<Category>/<id>[/<offset>]/ резолвиться в корінь орендаря; раніше він потрапляв на …/tile-search-results/, що відповідає 200 без жодної плитки.
  • Deutsche Bahn — db.jobs порожнить будь-яку сторінку результатів, що містить вакансію, яку не може відобразити; обхід тепер сортує за pubExternalDate_tdt по 1000 на сторінку (максимум 5 сторінок), зупиняється за сирою кількістю якорів-збігів і кидає помилку за відсутнього заголовка результатів або нерозібраних посилань на вакансії, а не повертає коротку дошку.
  • HigherEdJobs — challenge-сторінка Incapsula, віддана з HTTP 200, раніше розбиралася як нуль елементів і читалася як порожня дошка; тепер кидає помилку.
  • Тест читав справжній батьківський проєкт на машині розробника — tests/reports-list-cache.test.mjs виставляв CAREER_OPS_ROOT на тимчасову теку без cv.md/portals.yml, яку paths.mjs ігнорує, тож локально він рахував звіти батьківського проєкту (6 замість 3), а CI, де поруч немає батьківського проєкту, проходив.

Примітки

  • Тести 3782 → 4056. Нові набори: 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, а також parsers-pipeline-checklist із #380.
  • Батьківський проєкт: форк доведено до апстримного main (144 → 0 комітів позаду) із перевіреною цілістю чотирьох розбіжностей ADR-0002; апстримне скасування кириличного LOCATIONISH_RE до форка не потрапило.
  • Уже випущено у v1.239.0, не повторюється: AppliTrack, NEOGOV, Red Rover, SchoolSpring, OCC Mundial, вбудовані дошки Greenhouse, Workday myworkdaysite.com.

[1.239.4] — 2026-09-29

Патч за підсумками живої регресії v1.239.3.

Виправлено

  • Корейська та традиційна китайська оцінки повідомляли про відсутність підсумкового блоку. Такі звіти сягають 10–11 тисяч символів, а підсумковий блок іде останнім і опинявся за лімітом 8192 вихідні токени. Оцінювання (і авто-пайплайн) тепер допускає 16384 токени, а якщо провайдер усе ж обрізає відповідь, перше попередження каже саме про це, а не перелічує відсутні блоки.
  • Жива перевірка вважала коротку німецьку відповідь не німецькою (3 німецькі стоп-слова, жодного англійського). У відповіді-списку їх може бути мало; за повної відсутності англійських тепер достатньо трьох.
  • Тести 3778 → 3782.

Примітки

  • Маршрути глибокого дослідження та режимів зберігають ліміт 8192 токени: їхні відповіді в живих прогонах до нього не доходять.

[1.239.3] — 2026-09-29

Патч за підсумками живої регресії v1.239.2.

Виправлено

  • Арабські оцінки й далі не проходили перевірку A–G після того, як v1.239.2 виправив японську та гінді. Замаскований скелет заголовків (без тексту звіту в лозі) показав причину: арабський звіт пише літеру блоку арабською, в порядку абджад, хоч би що просив промпт. Перевірка тепер приймає أ ب ج د هـ و ز як A–G, якщо літера стоїть окремим словом на початку заголовка, — так само, як уже приймала кириличні двійники; ті самі літери всередині арабського слова не зараховуються.
  • Жива перевірка друкує цей замаскований скелет у разі провалу перевірки A–G, щоб наступний випадок можна було розібрати за логом.
  • Тести 3776 → 3778.

Примітки

  • У примітці v1.239.2 сказано, що перевірку не буде послаблено; для арабської її послаблено, бо три живі прогони показали: самим промптом латинську літеру не втримати.

[1.239.2] — 2026-09-29

Патч за підсумками живої регресії v1.239.1.

Виправлено

  • Арабська оцінка переклала літери блоків, а японська загубила підсумковий блок оцінки. У v1.239.1 останній рядок промпту повторював лише мову. Тепер він також каже, що латинська літера A–G у кожному заголовку та блок ---SCORE_SUMMARY--- (маркери й ключі англійською) не перекладаються, і просить власну писемність мови.
  • Тести 3775 → 3776.

Примітки

  • Перевірку звіту не послаблено до прийому перекладених літер чи маркерів: стабільність формату забезпечує промпт.

[1.239.1] — 2026-09-29

Патч за підсумками живої регресії v1.239.0.

Виправлено

  • Оцінки японською та українською приходили майже повністю англійською. Вказівка мови стояла на початку промпту, перед десятками кілобайт англійських файлів проєкту та тексту вакансії. Тепер промпт повторює мову останнім рядком.
  • Жива перевірка вважала сканування невдалим, коли на сервері вже йшло інше (щогодинне сканування, перезапущене одразу після деплою). Відмова сервера запускати друге сканування — правильна поведінка; перевірка тепер чекає й повторює спробу, до 30 хвилин.
  • Тести 3774 → 3775.

Примітки

  • Обмеження «одне сканування за раз» на сервері не послаблено: друге сканування й далі відхиляється з SCAN_BUSY. Змінилася лише перевірка.

[1.239.0] — 2026-09-29

Паритет із career-ops @ b39931e: п’ять нових джерел і три перенесені виправлення.

Додано

  • П’ять нових джерел (98 → 103; англійських адаптерів 93 → 98): AppliTrack (applitrack.com/<округ>), NEOGOV (SchoolJobs / GovernmentJobs), Red Rover (jobs.redroverk12.com/org/<id>) і SchoolSpring (<округ>.schoolspring.com) для вакансій у школах і держсекторі, а також OCC Mundial — найбільший сайт вакансій Мексики (явний provider: occ з пошуковими запитами). Кожне прив’язане до свого хоста, працює лише через HTTPS і відмовляється від редиректів.
  • Тести 3500 → 3774.

Виправлено

  • Вбудовані дошки Greenhouse (job-boards.greenhouse.io/embed/job_board?for=<дошка>) вели на дошку з назвою «embed» і падали; тепер дошка береться з ?for=, і лише на справжньому хості Greenhouse.
  • Дошки Workday на myworkdaysite.com (/recruiting/<tenant>/<site>) не розпізнавалися; тепер вони скануються, а посилання на вакансії ведуть на той самий хост.
  • Назву звіту з дужками (042-acme-(berlin)-….md) у трекері обрізало на першій ); тепер назва зберігається повністю.

Примітки

  • Зміни батьківського проєкту, що стосуються лише CLI, не переносяться — шаблони PDF і CV, scaffolder, пакетний запуск, імпорт career-profile, покриття ключових слів ATS, переклади режимів співбесіди, CLI-хост pi: web-ui їх не запускає.
  • Щоб відкрити такий звіт із трекера, маршрут звітів має приймати дужки в назві файлу; цей маршрут у цьому релізі не змінюється.

[1.238.3] — 2026-09-27

Сторінки відповідають і під час сканування, а логи віддаленого QA більше не розкривають адресу прод-сервера.

Виправлено

  • Дашборд і звіти вантажилися секундами, а під час сканування падали за таймаутом. /api/reports і /api/dashboard читали й розбирали всі звіти на кожному запиті — близько 3,7 с на прод-сервері, блокуючи всі інші запити; під час сканування завантаження сторінок упиралися в 30-секундний таймаут. Тепер розібрані заголовки кешуються для кожного файлу й оновлюються, коли файл змінюється, тож список коштує однієї перевірки файлу на звіт (500 звітів: 430 мс → 3 мс локально).
  • Знахідка віддаленого QA могла вивести частину адреси прод-сервера в публічний лог. GitHub маскує секрет, лише якщо він видний повністю, а повідомлення, обрізане до 180 символів, закінчувалося посеред адреси. Тепер знахідки й звіти повністю редагуються до обрізання.
  • Кожна оцінка через ШІ позначалася як некоректна. Промпт ніколи не просив блок ---SCORE_SUMMARY---, якого вимагає перевірка звіту (скрипти оцінювання батьківського проєкту його просять), а перекладений звіт перекладав слово «Block», і перевірка не знаходила розділи. Тепер промпт просить підсумковий блок і літеру блоку в кожному заголовку, перевірка приймає перекладене слово перед літерою, а підсумковий блок видаляється до показу й збереження звіту.

Додано

  • Тести 3488 → 3500.

Примітки

  • Сканер і далі працює всередині процесу сервера; цей реліз прибирає друге велике джерело блокувань, але в окремий worker сканування не виносить.

[1.238.2] — 2026-09-27

Виправлення 502, які прод-сервер усе ще віддавав під час щогодинного сканування.

Виправлено

  • Сторінки й ресурси віддавали 502 під час сканування. Node закриває неактивне keep-alive-з’єднання через 5 с, а Caddy перевикористовує такі з’єднання до 2 хвилин. Поки сканування всередині процесу завантажувало сервер, Node закривав сокети, у які Caddy щойно надіслав запит, і Caddy відповідав 502 connection reset by peer (62 рази за одне сканування, при цьому сервер був живий). Тепер сервер тримає неактивні з’єднання 125 с — довше за проксі, — тож першим завжди закриває проксі.

Додано

  • deploy.yml → verify показує, чому проксі відповів 502 (види помилок, адреси приховано), кількість 502 за хвилину від останнього перезапуску та підсумок щогодинного сканування.
  • release.yml уміє створити відсутній тег під час ручного запуску — реліз можна випустити там, де теги не пушаться.
  • Тести 3486 → 3488.

Примітки

  • Сканування й далі йде всередині процесу сервера і може займати його на секунди; винесення в окремий worker до цього патча не входить.

[1.238.1] — 2026-09-27

Виправлення стабільності за підсумками першого live-регресу прод-сервера: одне сканування за раз і виправлення сайту.

Виправлено

  • Два сканування одночасно валили сервер. Кожен GET /api/stream/scan запускав повний сканер усередині процесу сервера без жодного захисту; сканування з інтерфейсу збіглося зі щогодинним таймером, і процес упав з JavaScript heap out of memory (двічі, тим часом віддавалися 502). Тепер сканування одне за раз: другий запит отримує SSE-error з кодом SCAN_BUSY, і нічого не запускається. Workflow деплою також піднімає ліміт пам’яті сервера (heap_mb, типово 448 МБ).
  • /help і /changelog на cvstart.org прокручувалися вбік на телефоні (а /changelog ja/zh — і на десктопі): довгий код і зміст тепер переносяться.
  • Гінді не було в мовних альтернативах sitemap; перевірка i18n сайту тепер звіряє sitemap з реєстром мов.
  • Довідка §19 просила додати ключ до «всіх 8» мовних файлів — їх 17.

Додано

  • Віддалений регрес (.github/workflows/remote-qa.yml): кожен маршрут × кожна мова розгорнутого застосунку (лише читання), кожна сторінка × мова × телефон/десктоп на cvstart.org, кожне посилання в README та вікі й, на запит, справжнє сканування та живі виклики ШІ кожною мовою.
  • Workflow деплою сервера (.github/workflows/deploy.yml): recon, deploy (з побайтовим відкатом) і verify (smoke-тест і звіт про стабільність).
  • Тести 3481 → 3486.

Примітки

  • Споживання пам’яті самим сканером у цьому релізі не зменшено: захист не дає йти двом скануванням одночасно, а drop-in піднімає стелю. Профілювання справжнього сканування 98 джерел потребує продової мережі.
  • Щогодинний таймер career-ops-scan не змінювався; recon тепер показує, які команди він запускає.

[1.238.0] — 2026-09-27

Паритет з батьківським проєктом — career-ops main @ 993085ce (VERSION 1.34.0, 182 коміти від de2224a9). Чотири нові джерела, десять дзеркальних виправлень провайдерів, закрита загальносканерна прогалина SSRF і безкоштовна підказка відповідності назви в результатах сканування.

Додано

  • Чотири нові джерела — 94 → 98 (93 EN + 5 RU), 89 → 93 EN-адаптери.
    • Eploy (provider: eploy) — читає sitemap /live-jobs.xml орендаря на будь-якому брендованому хості (лише HTTPS; IP-літерали, localhost, *.local, *.internal та однослівні хости відхиляються). Відповідь, що не є sitemap (сторінка входу), кидає помилку, а не проходить як нуль вакансій. Сторінки деталей — опційні (eploy: { fetchDetails, detailLimit }).
    • HiringRoom (*.hiringroom.com) — спочатку JSON-LD /portal/jobs, застарілі картки /jobs як резерв; URL-адреси вакансій http:// у payload перебудовуються на HTTPS.
    • PeopleSoft Candidate Gateway — визначається за шляхом /psc/{site}/EMPLOYEE/HRMS/c/HRS_HRAM_FL.HRS_CG_SEARCH_FL.GBL на будь-якому брендованому хості. Сам слідує за редиректами сесії PeopleSoft у межах того самого origin (максимум 10 переходів, кожен перехід повторно прив’язується до origin орендаря та перевіряється на приватні адреси), відтворює форму «load more» і позначає частковий прохід (peoplesoftIncomplete), а не звітує про нього як про повний.
    • PrevueAPS (*.prevueaps.ca) — визначає id сайту зі сторінки вакансій, а потім читає JSON API.
  • Підказка відповідності назви в рядках #/scan. Кожен результат несе грубий чіп strong / related / weak fit, що порівнює назву вакансії з цільовими ролями у config/profile.yml (target_roles.primary + назви архетипів). Це безкоштовна оцінка за ключовими словами: вона ніколи не фільтрує, не переупорядковує і не змінює кількості, і підказка про це каже. Обчислюється під час видачі результатів, тож редагування профілю відображається лише при наступному оновленні. Перекладено ×17. Дзеркалить parent #3261.

Виправлено

  • Безпека — захист від DNS rebinding ніколи не спрацьовував під час реального сканування. http-json.mjs перевіряв розв’язані адреси лише тоді, коли отримував голий глобальний fetch, але обидва сканери інжектують makeTimeoutFetch(), тож кожне сканування пропускало цю перевірку. Джерело, що вказувало на ім’я хоста, яке розв’язується в 10.x або 127.0.0.1, отримувалося. Захист тепер працює всередині самого makeTimeoutFetch, у гонці з таймаутом сканування, тож зависла резолюція не може зависнути весь скан. Знайдено під час рев’ю цього релізу.
  • Безпека — IPv6-адреси, що вбудовують IPv4-адресу, не розпізнавалися як приватні. http://[::ffff:127.0.0.1]/ (яку парсер URL переписує на [::ffff:7f00:1]) проходив isValidJobUrl. І крапкова, і шістнадцяткова форми ::ffff:a.b.c.d / ::a.b.c.d тепер класифікуються за вбудованою адресою. Parent _ip-guard.mjs.
  • Rippling перейшов на API дошки v2 (ats.rippling.com/api/v2/board/<slug>/jobs), із пагінацією (за замовчуванням 10 сторінок, максимум 50), блокуванням хоста на посиланнях вакансій та валідацією slug. Вставлений URL API тепер дає справжній slug замість api.
  • Recruitee — пропозиції з кількома локаціями перелічують кожне місце (A · B), демо-вакансії (Sample) та рядки без назви відкидаються, а пропозиція зберігається, якщо хоча б один з її двох URL дійсний.
  • Pinpoint — засіяні демо-вакансії (здвоєне демо-відео) відкидаються.
  • Telegram-канали — перший рядок із кількома | більше не читається як Назва | Компанія, і розпізнається більше слів-ролей. Свідома до кирилиці LOCATIONISH_RE форку залишається незмінною (ADR-0002).
  • Workday — 422/401/403 на першій сторінці перевіряється один раз; сторінка обслуговування Workday або редирект збою позначає дошку як мертву (карантин у строгому режимі). Запити CXS і деталей тепер відмовляються від редиректів.
  • BambooHR — перенаправлений /careers/list тепер тріажиться через публічний embed-фід: порожній означає живу дошку без вакансій, будь-що інше — мертву дошку.
  • Heckler & Koch / Rheinmetall — голий heckler-koch.com / rheinmetall.com переписується на www., щоб транспорт без редиректів все одно до нього дістався. Ashby відмовляється від редиректів. beesite темпує сторінки за допомогою скасовуваного delay().
  • Фільтри назв згортають діакритику з обох боків (сканери EN та RU), тож Développeur виловлюється негативом developpeur.
  • Зіставлення компаній у cooldown розуміє нелатинські назви та корпоративні форми (маркери CJK/корейської, турецька крапкова İ). Назви CJK раніше згорталися в порожній ключ і ніколи не збігалися.
  • location_filter.strict: true тепер відмовляє закрито при порожній локації, якщо встановлено список дозволених або заблокованих.
  • Liveness — нові перевірки публічного API для вбудовувань Greenhouse (gh_jid), Arbeitsagentur і We Work Remotely; SmartRecruiters тепер вирішує за прапорцем active вакансії.
  • PeopleSoft — відносна дія форми «load more» тепер розв’язується відносно сторінки пошуку, а не кореня origin.

Примітки

  • Тести 3210 → 3481. Нові набори: 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.
  • Не перенесено, із причинами: межі слів Unicode у фільтрі локації (фільтр web-ui базується на підрядку, тож немає межі для виправлення — свідомий повний порт фільтра локації батьківського проєкту — робота на майбутнє); дедуплікація заявок з однаковою назвою (web-ui дедуплікує лише за URL); попередження про репости агрегаторів, область домену чорного списку, квитанції неперевіреного нуля та міграція scan-runs.tsv (у web-ui немає жодної з цих поверхонь); розбиття на фасети та теги обрізання Workday (web-ui отримує одну сторінку на дошку); enrichDate BambooHR (немає хука збагачення дати); рефакторинг сну в _http.mjs батьківського проєкту у ~20 провайдерах (web-ui вже використовує спільний скасовуваний delay); updater, doctor, PDF, tracker/merge-tracker, validate-profile, discover-new-companies, застосунок Next.js web/, GitHub-боти — лише CLI або ретрансльовані.
  • Свідомо залишено інакше, ніж у батьківському проєкті: Heckler & Koch / Rheinmetall залишаються лише HTTPS (без апгрейду http→https); мертва дошка Workday у звичайному скані все ще повертає [], а не завалює скан.

[1.237.1] — 2026-09-22

Патч після зовнішнього проходу QA для v1.237.0. Виправлення Jobstreet вперше підтверджено ззовні: на хостах AU, NZ, HK і MY стара форма /id/job/<id> повертає чисту 404, а нова /job/<id> веде на справжній маршрут, захищений Cloudflare; на id.jobstreet.com усе навпаки. Втрачені посилання були справжніми. Знайдено дві вади, обидві — у тому, що додало саме v1.237.0.

Виправлено

  • Мітка посилання на книгу була локалізована, а її ціль — ні. Усі 17 локалізацій SPA та усі 17 локалізацій cvstart.org вказували посилання на футер Agentic Coding Design Patterns на англійське видання, тоді як єдина перекладена мітка — російська, «Паттерны агентного кодинга» — обіцяла російську книгу. Новий аплаєр data-i18n-href (тієї самої форми, що й data-i18n-title, обмежений абсолютними значеннями https://) і ключ footer.patternsUrl спрямовують ru на російське видання, а всі інші локалізації — на англійське. Регресійний тест перевіряє цей інваріант в обох напрямках: перекладена назва означає локалізовану URL.
  • SEEK мігрує хости, а транспорт відмовляється від редиректів. www.seek.com.au → au.seek.com і www.seek.co.nz → nz.seek.com: сторінки деталей уже віддають 301, і обидві пари хостів сьогодні обслуговують пошукове API v5 з кодом 200. Оскільки http-json.mjs встановлює redirect:'error' (захист від SSRF, який варто зберегти), той день, коли шлях API почне редиректити запис AU/NZ на старому хості, призведе до збою — а якщо в allowlist лише хости www., ніхто не зможе мігрувати. au.seek.com і nz.seek.com тепер у ALLOWED_JOBSTREET_HOSTS; старі хости лишаються, тож наявна конфігурація і вже побудовані посилання й далі працюють. Схожі хости (au.seek.com.evil.com, au-seek.com) і далі відхиляються.
  • Вікі: рядок Agentic Practices на карті документації Home був єдиним із дванадцяти, що не екранував | усередині [[…|…]], тож GitHub обрізав комірку — мертве посилання, прихований опис. Один символ.

Примітки

  • Кількість не змінилася: 94 джерела (89 EN + 5 RU), 89 адаптерів. Тести 3201 → 3210 (+5 локалізація посилання у футері, +4 хости SEEK).
  • Не закриється ззовні, зафіксовано, а не заявлено: Oracle Cloud (Akamai заблокував обидва REST-запити), закріплення Personio і appendWorkType (потребують запису в portals.yml, а UI не має видалення для одного з них), і випадки рядків liveness (немає контрольованої сторінки; 40 живих перевірок дали 12 живих, 28 непереконливих, 0 хибно прострочених).
  • Довідка ×17 тепер перелічує обидві форми хостів для AU-Main і NZ-Main. Структура заголовків не змінилася: 32 H2 / 122 H3.

[1.237.0] — 2026-09-21

Паритет із батьківським репозиторієм — career-ops main @ 93c4302b (VERSION 1.33.0, 75 комітів із career-ops-hq/main). Цього разу без нових джерел: чотири дзеркальні виправлення, і два з них тут тихо губили вакансії.

Виправлено

Jobstreet / SEEK — кожне оголошення з Австралії, Нової Зеландії, Сінгапуру, Малайзії та Гонконгу вело на мертву сторінку. parseJobstreetItem безумовно будував URL деталей як ${baseUrl}/id/job/${id}. Сегмент /id/ — це не шлях, а індонезійський локальний префікс, який існує лише на індонезійських хостах. Кожен інший хост платформи SEEK відповідає 404 на /id/job/<id>. web-ui вже додав www.seek.com.au, www.seek.co.nz, sg.jobstreet.com, my.jobstreet.com та hk.jobsdb.com до ALLOWED_JOBSTREET_HOSTS, тож сканування охоче повертало ці оголошення — кожне з яких вело на 404. Тепер шлях прив’язаний до хоста: ID_LOCALE_HOSTS (id.jobstreet.com, www.jobstreet.co.id, jobstreet.co.id) зберігає /id/job/, усе інше отримує /job/, а нерозпізнане джерело падає на спільний шлях, а не кидає виняток. Два наших власних тести закодували цю ваду — вони стверджували hk.jobsdb.com/id/job/… і www.jobstreet.com/id/job/… — і їх виправлено. Oracle Cloud — коротка сторінка посередині списку завершувала обхід і відкидала хвіст дошки. Умовою зупинки було listLen === 0 || listLen < PAGE_SIZE, але ORC віддає менше рядків, ніж ліміт, посередині списку, бо частину рядків фільтрують на боці сервера. Виміряно на батьківському боці на American Express: TotalJobsCount 454, сторінки по 200 / 199 / 54 — 199 було прочитано як кінець, і останні 54 оголошення, 12% дошки, так і не було завантажено. Тепер порожня сторінка завжди завершує обхід; коли орендар повідомляє загальну кількість, пагінація триває, доки offset + PAGE_SIZE >= total; лише орендар без загальної кількості для звірки завершується на короткій сторінці. Це узгоджується зі ширшою конвенцією, за якою API «може повертати менше результатів, ніж запитано… навіть якщо це не кінець колекції» (Google AIP-158). hasMore і далі свідомо ігнорується — деякі орендарі повертають hasMore:false на кожній сторінці дошки з 7000 вакансій. Тепер, коли короткі сторінки більше не завершують обхід, MAX_PAGES лишається єдиним запобіжником проти орендаря, що повідомляє неправдиву загальну кількість, тож це покрито власним тестом. Liveness — фразу «This role is closed» не розпізнавали, а хибне закриття лишається назавжди. Патерн був /this job (listing )?is closed/i, який пропускає так само поширені формулювання з role і position; на одній дошці на батьківському боці виміряно, що 111 зі 111 невизначених оголошень використовували саме «role». Розширено до /this (?:job|role|position)(?: listing)? is closed\b(?!-)/i. Прикінцевий \b(?!-) — це запобіжник складних прикметників: сам по собі \b відкидає closedown, але й далі збігається зі справжнім текстом на кшталт «This role is closed-loop control of the platform», і саме випередження (lookahead) це зупиняє. Liveness — job expired тепер у м’якому рівні, який не може перемогти видиму кнопку Apply. HARD_EXPIRED_PATTERNS перевіряється до тесту на елемент подання заявки, тож усе, що туди потрапляє, перемагає навіть живе оголошення. /\bjob expired\b/i хибно спрацьовував на чотирьох реальних формах живої сторінки — запис каруселі «Similar jobs», чіп фільтра «Hide job expired», футер із FAQ і наведений вище текст про closed-loop — бо класифікатор отримує текст усієї сторінки. Тепер він живе в SOFT_EXPIRED_PATTERNS, перевіряється після елемента подання заявки й до евристики сторінки-списку, повертаючи expired_body_soft; голий банер «JOB EXPIRED» без елемента подання заявки й далі означає закриття. Цей напрямок важливий, бо хибний expired записується в історію сканування як skipped_expired, а потім дедуплікація безстроково вилучає справжню вакансію з кожного наступного сканування, якщо не встановлено scan_history.recheck_after_days.

Додано

Personio — явний personio: <slug> закріплює орендаря. Багато компаній вбудовують свою дошку Personio як iframe на брендовану сторінку кар’єри, тож careers_url вказує на власний домен компанії, а стрічка живе на <slug>.jobs.personio.de; без закріплення такі дошки ніяк не резолвляться. Набір символів слага обмежено прив’язаним allowlist (/^[a-z0-9][a-z0-9-]{0,62}$/i), а отриманий URL і далі проходить через наявний розбір new URL(), перевірку HTTPS і хостовий фільтр PERSONIO_HOST_RE — закріплення ніколи не є способом дістатися іншого хоста. Слаг, що несе ., /, @, :, .., пробіл, провідний - або перевищує ліміт DNS-мітки в 63 символи, падає на сьогоднішню поведінку api/careers_url. На відміну від батьківського репозиторію, хостовий regex web-ui чутливий до регістру, тож закріплення нормалізується через new URL() — AcmeGroup розв’язується в acmegroup.jobs.personio.de, а не будує хост, який власний allowlist потім відхилив би. Jobstreet — опційний appendWorkType дописує до назви типи зайнятості оголошення, напр. Strategy Consultant [Part time], тож фільтри за назвою і тріаж бачать тип зайнятості вже під час сканування. Вимкнено за замовчуванням; відсутній або порожній workTypes не дає жодного ефекту.

Примітки

Кількість джерел і адаптерів не змінилася: 94 джерела (89 EN + 5 RU), 89 EN-адаптерів. Цей реліз змінює лише кількість тестів, 3164 → 3201 (+13 jobstreet, +7 Oracle Cloud, +9 liveness, +8 Personio). Батьківський репозиторій видалив providers/telegram.mjs, консолідувавшись на telegram-channel. web-ui реєструє обидва як джерела, звернені до користувача, тож видалення не повторено: це скоротило б реєстр із 94 до 93 і мовчки зламало б усіх, у кого налаштовано telegram. Повертатися до цього лише як до свідомого, оголошеного видалення. jd-skill-gap.mjs отримав нестягнену форму заголовка «what we are looking for» та зачистку жирних маркерів. web-ui ретранслює цей скрипт, а не дзеркалить його, тож виправлення прийде разом із розгорнутим батьківським репозиторієм і не потребує змін тут. Не портовано, з причинами: lib/latex-escape.mjs, browser-extract.mjs, openai-eval.mjs, update-system.mjs і scaffolder (web-ui їх не дзеркалить і не запускає через shell); TUI-дашборд на Go і конфігураційна форма web/ (окремі поверхні); doctor.mjs і analyze-patterns.mjs (лише читальні ретрансляції, чий м’який відмовний режим поглинає зміну). Кириличне розходження форку \p{L} у providers/telegram-channel.mjs пережило злиття з апстрімом без змін і перевірене побайтово проти копії до злиття.

[1.236.0] — 2026-09-18

Паритет із батьківським репозиторієм — career-ops main @ 6a9c84c (VERSION 1.33.0, 128 комітів). Два нові джерела та три дзеркальні виправлення — два з них тут тихо губили вакансії.

Додано

Два джерела сканера, 92 → 94 (89 EN + 5 RU). Python.org Jobs (pythonorg) читає публічну стрічку дошки; атрибуція роботодавця обов’язкова, тож рядок, чия назва не називає роботодавця, відкидається, а не добудовується назвою запису порталу. Generalist World (generalist-world) скрейпить картки дошки й гучно падає на розмітці, яку не може розпарсити — картка зі зміненою формою кидає виняток, а не читається як порожня дошка, а саме це і є той режим відмови, що робить скрейпнуте джерело ненадійним. Обидва з’являються у фільтрі Source на #/scan, обидва прив’язані до хоста (якірний regex, лише HTTPS), і обидва наскрізь керовані наборами тестів із фейковим транспортом (43 і 36 випадків).

Виправлено

Вакансію Workday, відкриту в кількох містах, відкидав фільтр локації. Список-ендпоінт Workday відповідає на багатолокаційну вакансію лічильником там, де кожна інша вакансія несе місце: "53 Locations". Це не локація, а buildLocationFilter зіставляє за регістронезалежним підрядком — тож вакансія, відкрита в Остіні й ще у 52 інших містах, не збігалася з жодним записом allow: [austin] і відкидалася, тоді як та сама вакансія, вказана поодиноко, проходила. Виміряно на батьківському боці на трьох орендарях: 53 із 291 вакансій були заглушками, тож це звичайний випадок, а не крайовий. Справжні місця вакансії існують лише в детальному документі CXS, тож кожна заглушка тепер коштує один додатковий GET до нього, з обмеженням у 200 на запис — понад ліміт заглушку лишають видимою, а не підробляють, бо невирішена вакансія ніколи не повинна виглядати як вирішена. Невдалий чи безлокаційний деталь лишає вакансію точно такою, якою її повернув список: видиме "53 Locations" чесно повідомляє про те, що не вдалося розв’язати, тоді як порожня локація мовчки пройшла б крізь будь-який фільтр. Той самий GET несе й startDate, тож збагачена вакансія датується точно, а не відносною прозою список-ендпоінта ("Posted 30+ Days Ago" → undefined); усе, що не є голим YYYY-MM-DD, лишається без змін, а не проганяється через Date.parse, чия неISO-заглушка залежить від реалізації. Заявлений рівень більше не згортає дві вакансії в один повторний допис. "Insurance Specialist II" — це власне твердження роботодавця, що ця вакансія не є сестринською до іншої: інша оплатна вилка, обсяг і номер заявки під однією базовою назвою. Токенізатор цього не бачив: w.length > 3 відкидає кожну римську цифру аж до VIII і кожну однозначну цифру, тож "Insurance Specialist" і "Insurance Specialist II" токенізувалися однаково, отримували ідеальний коефіцієнт Жаккара, і детектор повторних дописів позначав другу як переоголошення першої. role-matcher.mjs у web-ui взагалі не мав обробки рівнів, тож це переносить увесь механізм батьківського репозиторію: рівні видобуваються зі згортанням римських і арабських цифр в одне число ("Nurse II" і "Nurse 2" — те саме твердження, і саме ця пара раніше хибно різнилася), два заявлені рівні мають збігатися, а рівень лише з одного боку трактується як вільний переказ, якщо тільки кожна назва не несе й слова, якого немає в іншій, — а це вже дві розбіжності, а не прикраса. Дошка Avature, прив’язана до фільтра, усе одно обходила всю глобальну дошку. UI пошуку Avature генерує непрозорі параметри фасетів (?42386=[812132] — це фільтр за країною), і оператор, який закріплює звужену дошку в portals.yml, очікує, що сканування обійде саме її. resolveSearch перебудовував URL як ${origin}${path} і повністю відкидав рядок запиту — тож кожен запит, включно з першим, обходив увесь борд орендаря: більше сторінок, більше запитів і вакансії, які оператор свідомо відфільтрував. Тепер фасети супроводжують кожен пагінований запит, а власний jobOffset застосовується останнім, щоб випадковий однойменний фасет ніколи не міг перехопити пагінацію.

Безпека

devalue 5.8.1 → 5.9.2 (транзитивно через astro), що закриває єдине відкрите сповіщення Dependabot — відмову в обслуговуванні на пошкодженому вводі. Залежність лише сайту; код рантайму не змінився.

Примітки

Не портовано, з причиною для кожного. Батьківський репозиторій дав lever тайм-аут 30с, а manfred — 25с, проти спільного усталеного 10с — це непорт: транспорт сканування web-ui вже дозволяє 60с (DEFAULT_SCAN_TIMEOUT_MS), тож обидві дошки мають тут більший запас, ніж дає те виправлення там. Подвоєний префікс careers- в iCIMS тут теж неможливий: web-ui бере origin орендаря прямо з api/careers_url і ніколи не будує хост з імені орендаря; новий обхід резервного хоста — це фіча для менш строгої конфігурації батьківського репозиторію, а не виправлення дефекту. deutschebahn і manfred отримали повторну спробу при тимчасовому обриві з’єднання, що при стелі web-ui у 60с значно менш нагально — занотовано, а не портовано, тож майбутній тайм-аут довгого обходу вже має перше, що варто спробувати. Усе інше у 128-комітному дельта-наборі — це CLI, верифікатор, дашборд або web/ (власний фронтенд батьківського репозиторію): liveness-browser, check-jd-archive, verify-ats/verify-portals/verify-cv-facts, update-system, set-status, audit-portals, discover-ats, scaffolder, test-all, маніфести плаґінів. Розбіжності форку повторно перевірено після pull: кириличне виправлення \p{L} у providers/telegram-channel.mjs, власний providers/telegram.mjs форку, hermes у web/src/lib/clis.ts. Апстрім career-ops-hq/main повністю змержено (0 пропущених комітів). 3066 → 3164 тести; довідка тепер вказує 94 адаптери (89 EN + 5 RU) в усіх 17 бандлах, а структура заголовків незмінна — 32 H2 / 122 H3.

[1.235.0] — 2026-09-16

Паритет із батьківським репозиторієм — career-ops main @ 68e6b94 (VERSION 1.33.0, 42 коміти). Два дзеркальні виправлення; одне з них тихо губило вакансії MokaHR тут.

Виправлено

Друга вакансія з тієї самої дошки MokaHR зникала замість того, щоб додаватися. url-key.mjs зводить URL оголошення до ключа порівняння і безумовно відкидав фрагмент — цілком розумно, адже майже кожен фрагмент існує лише для показу (#apply, #section-2). MokaHR — виняток у нашому власному реєстрі: її оголошення мають вигляд …/social-recruitment/{tenant}/{id}#/job/{n}, де всі вакансії одного орендаря поділяють один і той самий шлях, а ідентифікатор оголошення існує лише у фрагменті. Тож усі вони нормалізувалися до одного ключа. Виміряно до виправлення: вставте у конвеєр дві різні вакансії MokaHR — і отримаєте один рядок: без помилки, без попередження, нічого, що впадало б в очі; фільтр свіжості сканера (en-scanner.mjs) читає той самий ключ, тож щойно будь-яка вакансія цього орендаря потрапляла в історію сканування, кожну іншу відкидали як «вже бачену». Тепер normalizeUrl перетворює розпізнаний маршрут #/job/{id} чи #/jobs/{id} на ключ query-рядка, призначений лише для порівняння, перед тим як відкинути фрагмент — MokaHR зберігає своє усталене ім’я mokahr_job_id, щоб раніше записані ключі лишалися порівнюваними, а будь-який інший хост отримує внутрішнє _career_ops_fragment_job_id. Перенесений ідентифікатор додається, а не встановлюється: URL, що вже несе цей параметр, містить ідентифікатор іншого оголошення, і перезапис злив би саме ті дві речі, заради розділення яких і існує ключ. Кожен нерозпізнаний фрагмент і далі згортається — це та половина, яка й раніше була правильною. accept-encoding тепер зафіксовано в кожному запиті до джерела. Без явного значення Node узгоджує все, що пропонує його збірка undici; починаючи з Node ≥ 23 це включає zstd, а zstd-відповідь amazon.jobs повертається обрізаною рівно на 1024 байтах зі статусом 200 — тож збій виглядає як Unterminated string in JSON at position 1024, ніби пошкоджений API, а не проблема транспорту. Тепер кожен запит надсилає gzip, deflate, br. На наших рантаймах це запобіжний захід, а не виправлення: Node 18/20/22 — матриця CI та сервер (22.22.1) — надсилають gzip, deflate, що перевірено напряму, а не припущено, тож тут нічого не обрізалося; сьогодні змінюється те, що пропонується br, який ці версії не запитують за замовчуванням, але коректно розпаковують. Усталене значення застосовується регістронезалежно, і саме цю частину варто перевірити уважніше: імена заголовків регістронезалежні, а fetch об’єднує два однойменні записи в одне значення через кому, тож викликач, який передав би Accept-Encoding: identity, інакше отримав би надіслане gzip, deflate, br, identity.

Примітки

Заголовки лишаються звичайним об’єктом на межі fetchImpl, тоді як батьківський репозиторій передає fetch екземпляр Headers. Це свідома розбіжність, а не недогляд: fetch-хелпери web-ui приймають ін’єктований fetchImpl, і 45 наборів тестів джерел читають opts.headers['User-Agent'] із того, що вони перехоплюють — екземпляр Headers не підтримує доступ через дужки, тож достовірно портовано саме поведінку (регістронезалежне перевизначення, перемагає викликач), а не реалізацію. Об’єкт викликача копіюється, а не мутується, оскільки кілька джерел ділять одну спільну константу заголовків. Супутнє виправлення батьківського репозиторію — застосування усталеного user-agent регістронезалежно — це непортована зміна: web-ui взагалі не впроваджує усталений UA, кожне джерело встановлює той, якого вимагає його дошка, тож немає усталеного значення, з яким міг би зіткнутися User-Agent викликача. Два нові набори тестів і три нові випадки закріплюють цю пару: tests/http-accept-encoding.test.mjs (точне зафіксоване значення, кожен із чотирьох fetch-хелперів і перевизначення викликачем у трьох варіантах регістру — перевіряється, що виживає єдиний ключ accept-encoding, тоді як баг злиття породжував два) та tests/parsers-pipeline-hash-route.test.mjs, який працює зі швом, видимим користувачу, а не з ключем: обидві вакансії MokaHR мають дійти до конвеєра, повторна вставка тієї самої вакансії у чотирьох написаннях має і далі згортатися в один рядок, а косметичний фрагмент не повинен ставати ідентифікатором. Усі підтверджено як провальні до виправлення. Сама фіксація живе на рівні транспорту, а не лише в цих хелперах: 25 із 92 джерел викликають упроваджений fetchImpl напряму й через хелпери взагалі не проходять, тож її застосовує makeTimeoutFetch — обгортка, яку обидва сканери справді впроваджують, і єдина точка проходу, спільна для всіх джерел. (Батьківському проєкту еквівалент не потрібен: providers/_http.mjs — його єдиний транспорт.) Структурна перевірка тепер валить CI, якщо нове джерело викликає fetch напряму замість переданого йому транспорту, а поведінковий кейс проганяє greenhouse — одне з тих 25 — через обгортку й доводить, що заголовок доходить до дроту. Не портовано, із причиною: liveness-browser.mjs (запобіжник повторного використання headed-Playwright — web-ui не керує headed-браузером), check-jd-archive.mjs (у web-ui немає такої ретрансляції), test-all.mjs (харнес батьківського репозиторію; розбіжність форку vpFixtureEnv лишається незмінною), web/ (власний фронтенд батьківського репозиторію, не дзеркалиться), маніфести плаґінів і SIGNATURES.md (існують лише в репозиторії). batch/batch-runner.sh (амперсанди екрановано перед підстановкою в sed) і jd-skill-gap.mjs (резолвить cv.md відносно кореня даних) — ретранслюються лише для читання: web-ui звертається через shell до копій батьківського репозиторію, тож обидва потрапляють сюди без жодної зміни коду, а зміна в jd-skill-gap робить ретрансляцію /api/jds/:id/skill-gap коректнішою: cv.md тепер резолвиться від кореня даних, а не від того каталогу, звідки був запущений процес. Розбіжності форку повторно перевірено після pull: кириличне виправлення \p{L} у providers/telegram-channel.mjs, власний providers/telegram.mjs форку, hermes у web/src/lib/clis.ts. Кількість джерел не змінилася — 92 (87 EN + 5 RU), тож довідка лишилася незмінною. 3042 → 3066 тести.


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