Журнал изменений

GitHub

Каждое значимое изменение, релиз за релизом. Тот же файл публикуется в репозитории и переведён на все 19 языков.

[1.247.0] — 2026-10-09

Хвост дизайн-свипа CAR-49…58 закрыт — все оставшиеся minor/polish-находки плюс тёмная карта, которую прошлый релиз осознанно отложил.

Исправлено

  • Карта рендерит настоящий тёмный режим. Зум/слои/атрибуция потемнели ещё в v1.246.0; теперь и тайлы следуют теме: тёмный режим рисует OSM-слой с CSS-инверсией (без вендорных ключей), а keyed-пресет Carto-dark доступен через MAP_TILE_DARK_URL (его хост добавлен в разрешение CSP img-src). Слой перемонтируется живьём при переключении темы.
  • У жёлтых callout’ов появились тёмные варианты (config, batch, hero-warning) через токен-механику v1.246.0 — и попутно пойман регресс: стили .callout a забивали .btn-primary; теперь скоупятся :not(.btn).
  • URL в инфо-карточке apply больше не обрезается посреди слова (overflow-wrap anywhere).
  • Кнопки заголовка CV — одна иконочная система (текст; микс 📁/📄/💾 убран).
  • Табы статусов трекера в одном регистре — sentence case, с локальными особенностями (CJK/ar/hi/ta без родовых форм).
  • Иконки топбара — единый stroke-SVG набор — микс 🔔/🌙/🩺 убран; кнопка темы рисует sun/moon через CSS, приложение больше не переписывает текст кнопки.
  • Карточка Pipeline на дашборде лишилась розового highlight’а (кольцо только на hover/focus).
  • Delete у сохранённых поисков явно деструктивен (.btn-danger, а не нейтральный ghost).
  • У preview pipeline появилось настоящее пустое состояние (заголовок + подсказка + CTA, паттерн трекера).
  • Мета времени/стоимости — один паттерн: UI.pageMeta рисует ⏱ + стоимость одной строкой под заголовком вью (evaluate/deep/auto/mode-page/orientation).
  • Auto-pipeline лишился ✨ — единственный декорированный h1 вернулся на общий левый край.
  • Batch docs URL — инлайн-ссылка (без голого переноса посреди абзаца).
  • У interview-digest / orientation появились спроектированные пустые состояния (заголовок + подсказка + CTA).
  • Export career-plan неактивен, пока план пуст, действия — в одну строку.
  • stats: дублирующий заголовок «Market report» убран, плейсхолдер региона помещается (короче ×18, фикс-ширина).
  • Config больше не утекает абсолютные пути — серверный displayPath() показывает ~/… или голое имя файла.
  • Хелпер LLM_PROVIDER — три короткие строки (×18 + pre-line) вместо 150-символьной mono-стены.
  • EMAIL-карточка профиля держит адрес в одну строку (ellipsis; перенос только ≤480px).
  • Текст Health больше не ломает слова («Run buttons» остаётся целым; overflow-wrap anywhere).
  • Инлайн-код в help выживает в RTL (LTR-изоляция, nowrap, скролл; career-ops в docs/help/ar.md теперь закодирован как токен).
  • Пилюля «Live evals» в hero больше не сталкивается в ru/ar (flex-wrap, несжимаемые иконки).

Добавлено

  • Пресет тёмных тайлов (MAP_TILE_DARK_URL + attribution-env) задокументирован в коде; добавление в CSP img-src покрывает хост Carto для него.

Примечания

  • Дефолт тёмных тайлов — CSS-инверсия OSM: keyless Carto закрыл базовые карты за API-ключ (проверено запросом), поэтому keyed-пресет — опция, а не дефолт.

[1.246.0] — 2026-10-09

Все десять топ-находок сеньор-дизайн прохода CAR-48 исправлены — включая блокер и все семь мажоров.

Исправлено

  • Apply dark: ссылка в инфо-карточке снова видна. Карточка держала светлый surface в тёмной теме; ссылка «Need Playwright? See …» сидела на ≈1.0:1. Theme-aware .callout--info (токены surface/border/text/link) поднимает тёмную ссылку до ≈7.3:1.
  • RTL больше не переворачивает числовые композиции. Score-пилли («1.5 / 5»), бэнды отчётов («≥ 4.5», «< 3.5») и дашбордное «/ 5.0» получают направленную изоляцию (LTR-span’ы); ar-словарь там, где прозе лучше, переписывает диапазоны словами.
  • CV-markdown остаётся LTR. Textarea и превью рендерятся dir="ltr" в арабском; обвязка вокруг остаётся RTL.
  • Колонка DATE в отчётах больше никогда не переносит ISO-дату (nowrap + табличные цифры).
  • Сайдбар заканчивается выше USAGE HUD (резерв скроллпорта + fade) — последний пункт навигации больше не уезжает под панель; RTL-зеркалирование учтено.
  • Мобильный скан @390: «Save search» полностью виден — ряд контролов переносится и сжимается вместо обрезки у края экрана.
  • Контролы карты следуют тёмной теме (zoom, слои, атрибуция). Сами тайлы намеренно не тронуты: их origin закреплён серверным CSP img-src, а клиентская подмена слоя означала бы правку security-заголовка.
  • Chat FAB больше не перекрывает интерактив — скролл-паддинг страниц очищает его (кнопки Disable в Portals, score-пилль в Reports, инпут two-pager, атрибуция Leaflet).
  • Журнал активности говорит на языке интерфейса: 22 слага действий локализованы ×18 локалей (с fallback на сырой id для неизвестных) и чипы фильтров переводятся; колонка TARGET остаётся — её заполняет auto-pipeline.
  • Формы assessments получают настоящие лейблы (sentence case, связка htmlFor/id) вместо полей только с placeholder’ами.

Примечания

  • Не в этом релизе: тёмные тайлы карты (нужно расширение CSP img-src на тёмный tile-origin — серверная правка, клиентом не делали намеренно); тёмные варианты жёлтых callout’ов (config, batch) — механика токенов callout уже готова для продолжения.

[1.245.0] — 2026-10-09

Тамильский (தமிழ்) присоединяется как 18-я локаль интерфейса — первый новый язык после волны из 17 локалей, с полным паритетом ключей.

Добавлено

  • Тамильский (ta) — 1449/1449 ключей с полным паритетом к en, без потерянных плейсхолдеров, в хинди-стиле Indic-tech-конвенций (провайдеры, модели, пути и команды остаются латиницей). detect() принимает ta, ta-IN, ta-LK; тамильский — LTR (закреплено тестом).
  • Полный docs/help/ta.md (33 H2 / 125 H3, счётчики источников сверены с живым реестром, канареечные якоря Hermes, дословный YAML), плюс зеркала README.ta.md и CHANGELOG.ta.md.
  • Перечисляющие гейты 17 → 18: LANGS, detect-пробы, i18n-аудит, changelog-parity, RTL-страж, CI langs — заодно подхватывает hi, ранее отсутствовавший в CI-списке.

Примечания

  • Лендинг cvstart.org пока остаётся на 17 локалях — перевод лендинга отдельная задача; синк контента сайта уже отдаёт docs/help/ta.md и ta-changelog.
  • report-header.js REPORT_LABELS остаётся на 17 — парсинг тамильских отчётов придёт вместе с генерацией тамильских отчётов в родительском репо.

[1.244.2] — 2026-10-08

Полировка страницы сканирования: лаунчер, строка состояния, панель репостов и фильтры переработаны в один консистентный хром — и таблицу результатов больше не можно растянуть данными бордов.

Исправлено

  • Клетки зарплаты показывают только денежный диапазон. Некоторые борды кладут весь текст льгот в поле зарплаты — видимая клетка теперь показывает только денежную часть, а текст перемещает в подсказку (раньше: 6-строчная клетка, растягивающая каждую строку).
  • Строки с пустым заголовком показывают компанию (затем нейтральное тире) вместо гигантской пустой клетки.
  • Бейдж seniority никогда не переносится в своей колонке фиксированной ширины.
  • Строки без оценки показывают приглушённое «◎ —» с подсказкой, что сравнение с two-pager не нашло совпадений (раньше: ничего, что выглядело сломанным рядом с оценёнными строками).
  • Контракт высоты aux-ячеек теперь измеряет контентный элемент внутри aux-клетки — td растягивается по высоте строки, что маскировало реальные переносы.

Добавлено

  • Редизайн хрому: карточка-лаунчер (выровненный ряд контролов, доминантная primary-кнопка), строка состояния терминала с точкой состояния (простой/выполнение/готово/ошибка) поддерживает reduced-motion, панель репостов имеет потолок высоты со sticky-заголовком (датасет из 1,560 кластеров рендерил панель 155,000 px), и блок фильтров как равномерная адаптивная сетка с выровненным футером.

Примечания

  • Тесты 5045 → 5068 unit; браузерные layout-контракты расширены (предел высоты aux в одну строку).
  • Не в этом релизе: URL-hash состояние фильтров и клавиатурная навигация строк (отложено); live-LLM регресс × 7 локалей заблокирован кредитами апстрима (разблокировано — CAR-44).

[1.244.1] — 2026-10-08

Исправлено

  • Иконки скана объявляют реальные значения. Доступные имена иконок boost/fit/score утекали сырые шаблоны словаря («Соответствие: {band}», «Продвинуто: {by}») — {band}/{by}/{score} теперь подставляются (слово банды, ключевое слово буста, числовой скор). [HIGH a11y, поймано живым регрессом v1.244.0]
  • Пайпы в заголовках режутся правильно. Заголовок из 4 сегментов вида «C++ | Rust | Go Developer | Onsite» больше не выносит «Go Developer» как «страну» — для разделения требуется, чтобы сегмент распознавался как настоящая страна (консервативный Countries.detectCountry).
  • Даты в мета-строке рендерятся для каждой строки. daysSince принимал только дату без времени, а API отдаёт полные таймстампы для ~83% датированных строк — у таких строк сегмент даты не отображался.

Примечания

  • Всё остальное в #/scan идентично v1.244.0 — изменились только три фикса выше.
  • Сознательно не сделано: состояние фильтров в hash URL, навигация строк клавиатурой и парсинг 5 lakhs INR (всё — отложенные хвосты).

[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+.

Добавлено

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

Примечания

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

[1.243.2] — 2026-10-08

Исправлено

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

Notes

  • App code is identical to v1.243.1 — only the deployable tag is fresh (the server’s git fetch --tags refused 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

Исправлено

  • Модалка «Записать итог» теперь действительно закрывается при навигации. Гард 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 показывают ошибку и отключают Save вместо того, чтобы тихо перезаписать файл пустыми значениями.
  • Модалка «Записать итог» больше не переживает навигацию. Открытая на #/tracker, при клике на другой вид она оставалась висеть поверх всей страницы (найдено в двухстендовой регрессии v1.242.1) — теперь закрывается при смене маршрута; её собратья из вида сканирования тоже останавливают свои SSE-потоки и отписывают слушатели при навигации.
  • Разбор зарплат и ключевых слов читает любую письменность. Чипы ключевых слов используют Unicode-границы слов (кириллица, c++, .net фильтруются правильно, а не в ноль строк); один общий парсер зарплат чинит $182.9K - $240K, от 100 000 руб, 100 тыс. руб., 50 000 Kč (было 50 млн долларов) и семантику диапазонов ($100-150K); обязательное «Germany or Netherlands» больше не считается одновременно и совпавшим, и нарушенным.
  • Потоки завершаются ровно один раз. Оборванное SSE-соединение доставляло ошибку дважды и могло навсегда оставить Generate неактивной; каждое терминальное состояние (done/error/close/throw, включая убитый дочерний процесс) закрывается одним путём и возвращает кнопку. Авто-pipeline выгружает свой последний буферизованный кадр вместо того, чтобы терять его.
  • Приватность и корректность в мелких путях. Тело баг-репорта больше не несёт URL оцениваемой вакансии или slug’и отчётов; приватность 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-контрактных тестов (≥ 4.5:1).
  • Всё остальное из раунда: evaluate показывает предупреждение об усечении; memory/config/two-pager отказываются сохранять поверх неудачной загрузки; scan больше не бросает свои SSE-потоки; результаты скана классифицируют доски с API по серверному реестру вместо разъехавшегося регулярного выражения; «Proceed with Caution» в трекере — предупреждение, а не красный бейдж; статистика сворачивает локализованные статусы; столбчатые диаграммы читаются правильно в RTL; docs-FAB больше не накрывает контент на телефонах; GET /app.js отвечает 404 вместо оболочки индекса; GET /api/runners перечисляет свои действия.

Добавлено

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

Примечания

  • Юнит-тестов 4812 → 5036, браузерных 118 + новые навигационные/контрастные контракты. Среднее покрытие 98.15 % строк / 89.34 % ветвей (нижний порог 96/86).
  • Не вошло в релиз: редизайн #/scan (Phase 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 обходит тенантов дальше 100 вакансий (offset-страницы, MAX_PAGES); telegram хранит НОВЕЙШИЕ посты и пагинирует ?before= вместо того, чтобы молча оставлять самые старые; tencent продолжает обход, когда Count отсутствует; taleo разбирает таблицы с французскими заголовками; avature останавливается по сырой длине страницы; штампы дат в UTC (amazon со сдвигом на единицу восточнее UTC, torre, workingnomads, trudvsem); HTML-сущности декодируются (geekjob, getmatch, hh); mycareersfuture выдаёт полную 12-полевую форму вакансии.
  • Детекция удалёнки ужесточена. 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).
  • Не вошло в релиз: клиентские находки того же раунда ревью (Phase 3 — views, libs, CSS/a11y, i18n); у sources/habr.mjs по-прежнему нет отдельного тестового файла (исключение из правила растущего порога).

[1.241.1] — 2026-10-07

Живые оценки работают от начала до конца на любой локали, карта вакансий размещает длинные объявления с несколькими локациями, а деплой больше не убивает идущий скан.

Исправлено

  • Оценки видят метод целиком. Бюджет контекста (144 КБ) всё ещё обрезал конец modes/oferta.md, когда подставлялись резюме и профиль, поэтому каждая живая оценка теряла последние блоки. Теперь бюджет — 192 КБ, а мягкий предел промпта — 256 КБ, заданные один раз в llm-dispatch.mjs.
  • Отчёты на выбранном языке. Языковая инструкция теперь и открывает промпт, и завершает его, а обычные слова (роль, команда, дизайн…) пишутся на языке отчёта; на английском остаются только имена, технологии и дословные цитаты. Исправляет хинди- и японские отчёты, которые были в основном на английском.
  • Медленные оценки больше не заканчиваются 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 запрос/с, back-off и дисковым кэшем. 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 КБ для резюме, а обрезка сообщается. Пустой или обрезанный ответ завершается ошибкой вместо записи пустого отчёта.
  • SSRF: переходы по редиректам повторно проверяются на приватные цели, проверяется каждый DNS-ответ, дедлайн скана охватывает тело ответа, ключи *_BASE_URL уходят только на хост вендора (Hermes/Ollama: loopback или LAN), pandoc запускается с --sandbox.
  • Зависания event loop: два квадратичных регулярных выражения (очистка HTML, санитизация markdown) заменены линейными сканерами.
  • Потеря данных: удаление строки пайплайна сохраняло компенсацию других строк; $& в URL портил pipeline.md; сохранения мок-интервью, нетворкинга и глубокого исследования перезаписывали прежние файлы; история интервью хранила первые 40 реплик вместо последних.
  • Слой LLM: авто-пайплайн и глубокое исследование учитывают LLM_PROVIDER и всех провайдеров через общую диспетчеризацию, фиксируют расход и сообщают об обрезке; оценка/тест Gemini больше не сохраняют отчёт; OpenAI использует max_completion_tokens, а OPENAI_MODEL, подходящий только для Codex, отправляется как gpt-5; описание вакансии обрамляется как недоверенные данные.
  • PDF отчёта, глубокого исследования и встроенные передают --skip-fact-check (проверка фактов резюме блокировала любой отчёт с числами); формат страницы из профиля больше не переопределяется.
  • Сканеры: карантин привязан к 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) — исправления, зеркально перенесённые в восемь существующих провайдеров, и формат чек-листа пайплайна, внесённый @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), опциональные страницы деталей разбираются обходом фигурных скобок с учётом строк.

Исправлено

  • Пайплайн в формате чек-листа родителя — upstream career-ops пишет data/pipeline.md как секции ## Pending / ## Processed со строками - [ ] url | …; UI читал такой файл как 0 в ожидании, а запись выполнял, дописывая ограждённый блок в конец файла, куда родитель никогда не смотрит. parsePipeline, addPipelineUrl и removePipelineUrl теперь читают и пишут такую форму (дедупликация и по обработанным строкам; компенсация — как помеченный сегмент note: comp …); формат с ограждённым блоком не изменился. Вклад @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 — проверочная страница 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.
  • Родитель: форк приведён к upstream main (отставание 144 → 0 коммитов), четыре расхождения из ADR-0002 проверены и остались нетронутыми; откат upstream кириллического 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 (нет хука обогащения даты); рефакторинг sleep в _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 ложных истечений). Help ×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/, а неразбираемый origin проваливается в общий путь вместо выброса исключения. Два собственных теста закодировали баг — они утверждали 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», и именно опережающая проверка это останавливает. Liveness — job expired теперь в мягком уровне, который не может перебить видимую кнопку отклика. 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. В отличие от родителя, хост-регулярка 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 и скаффолдер (web-ui их не зеркалирует и не оборачивает шеллом); TUI Go-дашборда и форма конфигурации web/ (отдельные поверхности); doctor.mjs и analyze-patterns.mjs (ретрансляторы только для чтения, чей fail-soft поглощает изменение). Кириллическое расхождение форка \p{L} в providers/telegram-channel.mjs пережило слияние с апстримом в неизменном виде и было сверено побайтово с копией до слияния.


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