変更履歴

GitHub

すべての注目すべき変更をリリースごとに記録しています。同じファイルがリポジトリに収録されており、19言語すべてに翻訳されています。

[1.239.0] — 2026-09-29

career-ops @ b39931e との同期: 新しいソース 5 件と、移植した修正 3 件。

追加

  • 新しいソース 5 件(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 ホストでのみ有効です。
  • myworkdaysite.com 上の Workday ボード(/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 秒のタイムアウトに達していました。解析済みのヘッダーをファイルごとにキャッシュし、ファイルが変わったときだけ更新するため、一覧はレポートごとのファイル確認 1 回で済みます(500 件: ローカルで 430 ms → 3 ms)。
  • リモート QA の検出結果が本番ホスト名の一部を公開ログに出力することがありました。 GitHub はシークレットが完全な形で現れたときだけ伏せ字にしますが、180 文字で切ったエラーメッセージがホスト名の途中で終わっていました。検出結果とレポート全体を切る前に伏せ字にするようになりました。
  • AI による評価がすべて形式不正と判定されていました。 プロンプトは、レポート検査が必須とする ---SCORE_SUMMARY--- ブロックを一度も求めていませんでした(親プロジェクトの評価スクリプトは求めています)。また翻訳されたレポートでは “Block” という語も翻訳され、検査が各セクションを見つけられませんでした。プロンプトはサマリーと各見出しのブロック文字を求めるようになり、検査は文字の前の翻訳語を受け付け、サマリーブロックはレポートの表示・保存前に取り除かれます。

追加

  • テスト 3488 → 3500。

備考

  • スキャナーは今も サーバープロセス内 で動いています。このリリースはもう一つの大きなブロック要因を取り除きますが、スキャンを worker に移すことは しません。

[1.238.2] — 2026-09-27

本番サーバーで毎時スキャン中にまだ出ていた 502 の修正。

修正

  • スキャン中にページやアセットが 502 を返していました。 Node はアイドルな keep-alive 接続を 5 秒で閉じ、Caddy はアイドル接続を最大 2 分再利用します。プロセス内スキャンでサーバーが忙しい間、Caddy がリクエストを送った直後のソケットを Node が閉じ、Caddy は 502 connection reset by peer を返していました(1 回のスキャンで 62 回、サーバー自体は正常)。サーバーはアイドル接続を 125 秒(プロキシより長く)保持するようになり、常にプロキシが先に閉じます。

追加

  • deploy.yml → verify がプロキシが 502 を返した理由(エラーの種類、アドレスは伏せ字)、前回の再起動以降の分ごとの 502 件数、毎時スキャンの結果を表示します。
  • release.yml は手動実行時に存在しないタグを作成できるため、タグを push できない環境でもリリースできます。
  • テスト 3486 → 3488。

備考

  • スキャンは今も サーバープロセス内 で実行され、数秒間サーバーを占有することがあります。worker への移行はこのパッチには 含まれません。

[1.238.1] — 2026-09-27

本番サーバーの初のライブ回帰テストを受けた安定性パッチ:スキャンは同時に 1 つだけ、サイトの修正。

修正

  • スキャン 2 つの同時実行でサーバーが落ちていました。 GET /api/stream/scan はいずれも保護なしにサーバープロセス内でスキャナー全体を実行しており、UI から開始したスキャンが毎時タイマーと重なって、プロセスが JavaScript heap out of memory で停止しました(2 回、その間 502 エラー)。現在は同時に 1 つだけで、2 つ目のリクエストはコード SCAN_BUSY の SSE error を受け取り、何も開始されません。デプロイ用ワークフローはサーバーのメモリ上限も引き上げます(heap_mb、既定 448 MB)。
  • cvstart.org の /help と /changelog がスマートフォンで横スクロールしていました(ja/zh の /changelog はデスクトップでも)。長いコードと目次が折り返されるようになりました。
  • サイトマップの言語代替からヒンディー語が抜けていました。サイトの i18n チェックがサイトマップと言語レジストリを照合するようになりました。
  • ヘルプ §19 がキーを「8 つ」の言語ファイルに追加するよう案内していました — 実際は 17 です。

追加

  • リモート回帰テスト(.github/workflows/remote-qa.yml):デプロイ済みアプリの全ルート × 全言語(読み取り専用)、cvstart.org の全ページ × 言語 × スマホ/デスクトップ、README とウィキの全リンク、さらに要求に応じて実際のスキャンと全言語でのライブ AI 呼び出し。
  • サーバーデプロイ用ワークフロー(.github/workflows/deploy.yml):recon、deploy(バイト単位で正確なロールバック付き)、verify(スモークテストと安定性レポート)。
  • テスト 3481 → 3486。

備考

  • このリリースではスキャナー自体のメモリ使用量は削減していません。保護で 2 つの同時スキャンを防ぎ、drop-in で上限を引き上げています。98 ソースの実スキャンのプロファイリングには本番ネットワークが必要です。
  • 毎時タイマー career-ops-scan は変更していません。recon がその実行コマンドを表示するようになりました。

[1.238.0] — 2026-09-27

親パリティ — career-ops main @ 993085ce(VERSION は 1.34.0、de2224a9 から 182 コミット)。新規ソース 4 件、ミラーされたプロバイダー修正 10 件、スキャナー全体にわたる SSRF の穴を 1 件塞ぎ、スキャン結果に無料のタイトル適合ヒントを追加しました。

追加

  • 新しいソース 4 件 — 94 → 98(93 EN + 5 RU)、89 → 93 EN アダプター。
    • Eploy(provider: eploy)— ブランド化された任意のホスト上でテナントの /live-jobs.xml サイトマップを読みます(HTTPS のみ。IP リテラル、localhost、*.local、*.internal、単一ラベルのホストは拒否されます)。サイトマップ以外の応答(ログインページ)はゼロ件として通すのではなく throw します。詳細ページの取得はオプトインです(eploy: { fetchDetails, detailLimit })。
    • HiringRoom(*.hiringroom.com)— まず JSON-LD の /portal/jobs を読み、旧形式の /jobs カードをフォールバックとします。ペイロード内の http:// の求人 URL は HTTPS に組み直します。
    • PeopleSoft Candidate Gateway — ブランド化された任意のホスト上の /psc/{site}/EMPLOYEE/HRMS/c/HRS_HRAM_FL.HRS_CG_SEARCH_FL.GBL パスで検出します。PeopleSoft 自身の同一オリジンセッションリダイレクトを自前で追跡し(最大 10 ホップ、各ホップをテナントのオリジンに再固定してプライベートアドレスと照合)、「さらに読み込む」フォームを再生し、完了として報告する代わりに部分的な走査であることを示します(peoplesoftIncomplete)。
    • PrevueAPS(*.prevueaps.ca)— 求人ページからサイト ID を解決し、その後 JSON API を読みます。
  • #/scan の行にタイトル適合ヒントを追加。 各結果は、求人タイトルを config/profile.yml のターゲットロール(target_roles.primary とアーキタイプ名)と比較した、大まかなstrong / related / weak の適合チップを持ちます。これは無料のキーワード推定であり、結果を絞り込んだり並べ替えたり件数を変えたりすることは一切なく、ツールチップもそう説明します。結果がサーブされる時点で計算されるため、プロフィールを編集すれば次回の更新に反映されます。17 ロケールに翻訳済み。親 #3261 のミラー。

修正

  • セキュリティ — DNS リバインディングガードが実際のスキャンでは一度も走っていませんでした。 http-json.mjs は素の全域 fetch を渡されたときにしか解決済みアドレスをチェックしていませんでしたが、両方のスキャナーは makeTimeoutFetch() を注入しているため、すべてのスキャンでこのチェックが飛ばされていました。10.x や 127.0.0.1 に解決されるホスト名を指したソースはそのまま取得されていました。ガードは今や makeTimeoutFetch 自身の内側で走り、スキャンのタイムアウトと競走させることで、停止したリゾルバーがスキャンをハングさせないようにしています。本リリースのレビューで発見。
  • セキュリティ — IPv4 アドレスを埋め込んだ IPv6 アドレスがプライベートとして扱われていませんでした。 http://[::ffff:127.0.0.1]/(URL パーサーが [::ffff:7f00:1] へ書き換える)は isValidJobUrl を通過していました。::ffff:a.b.c.d / ::a.b.c.d のドット形式と 16 進形式の両方が、今は埋め込まれたアドレスによって分類されます。親の _ip-guard.mjs。
  • Rippling が v2 ボード API へ移行(ats.rippling.com/api/v2/board/<slug>/jobs)。ページネーション(デフォルト 10 ページ、最大 50)、求人リンクへのホスト固定、slug 検証つきです。貼り付けられた API URL からは今や api ではなく実際の slug が得られます。
  • Recruitee — 複数拠点の求人はすべての場所を列挙し(A · B)、(Sample) のデモ求人とタイトルなしの行は落とし、2 つの URL のいずれかが有効であれば求人は残されます。
  • Pinpoint — 種としてのデモ求人(重複したデモ動画)を落とします。
  • Telegram チャンネル — | を複数含む先頭行はもはや タイトル | 会社名 として読まれず、より多くの職種名を認識します。フォークのキリル文字対応 LOCATIONISH_RE には手を加えていません(ADR-0002)。
  • Workday — 1 ページ目の 422/401/403 は 1 回だけ試行し、Workday のメンテナンスページや障害リダイレクトはボードを死んでいると判定します(strict モードでは検疫)。CXS リクエストと詳細リクエストは今やリダイレクトを拒否します。
  • BambooHR — リダイレクトされた /careers/list は公開の埋め込みフィードで判定します。空なら求人のない稼働中ボード、それ以外は死んだボードです。
  • Heckler & Koch / Rheinmetall — 素の heckler-koch.com / rheinmetall.com は www. に書き換えられ、リダイレクトなしのトランスポートでも到達できるようになりました。Ashby はリダイレクトを拒否します。beesite はキャンセル可能な delay() でページ送りの間隔を取ります。
  • タイトルフィルターが両側でアクセント記号を畳み込みます(EN・RU 両スキャナー)。Développeur が developpeur という否定語に捕捉されるようになりました。
  • クールダウンの会社名照合が非ラテン文字の社名と法人格表記を理解します(CJK/韓国語のマーカー、トルコ語のドット付き İ)。CJK の社名は以前は空のキーに潰れて一致しませんでした。
  • location_filter.strict: true は許可リストまたはブロックリストが設定されている場合、所在地が空だとフェイルクローズします。
  • Liveness — Greenhouse の埋め込み(gh_jid)、Arbeitsagentur、We Work Remotely 向けの新しい公開 API チェック。SmartRecruiters は求人の active フラグで判定するようになりました。
  • PeopleSoft — 相対パスの「さらに読み込む」フォームアクションは、オリジンのルートではなく検索ページを基準に解決されます。

備考

  • テスト 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 はボードごとに 1 ページだけ取得します)、BambooHR の enrichDate(日付エンリッチのフックがありません)、~20 個のプロバイダーにまたがる親の _http.mjs の sleep リファクタ(web-ui はすでに共有のキャンセル可能な delay を使っています)、updater、doctor、PDF、tracker/merge-tracker、validate-profile、discover-new-companies、web/ の Next.js アプリ、GitHub ボット — CLI 専用またはリレー対象です。
  • 親と意図的に異なる点:Heckler & Koch / Rheinmetall は HTTPS のみのまま(http→https への昇格なし)。通常のスキャンで死んだ Workday ボードは、スキャンを失敗させるのではなく引き続き [] を返します。

[1.237.1] — 2026-09-22

v1.237.0 に対する外部 QA パスからのパッチ。 Jobstreet の修正は今回初めて外部から確認されました:AU、NZ、HK、MY の各ホストでは、旧形式の /id/job/<id> がきれいな 404 を返し、新形式の /job/<id> は実際に Cloudflare で保護されたルートに到達します。id.jobstreet.com ではその逆が成り立ちます。失われていたリンクは実在するものでした。欠陥が 2 件見つかり、いずれも v1.237.0 自体が追加した部分にありました。

修正

書籍リンクのラベルはローカライズされていましたが、リンク先はそうではありませんでした。 SPA の 17 ロケールすべてと cvstart.org の 17 ロケールすべてが、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 を返しており、両方のホストペアとも今日時点で v5 検索 API を 200 で提供しています。http-json.mjs が redirect:'error' を設定しているため(維持する価値のある SSRF 対策です)、API パスがリダイレクトを始める日には旧ホスト上の AU/NZ エントリが失敗してしまいます——そして www. ホストしか許可リストに入っていなければ、誰も移行できません。au.seek.com と nz.seek.com はいま ALLOWED_JOBSTREET_HOSTS に入っています。旧ホストはそのまま残るため、既存の設定と既に作成済みのリンクは引き続き機能します。なりすまし(au.seek.com.evil.com、au-seek.com)は引き続き拒否されます。 Wiki: Home のドキュメントマップにある Agentic Practices の行は、12 行中唯一 [[…|…]] 内の | をエスケープしていなかったため、GitHub がそのセルを切り詰めていました——リンク切れ、説明も非表示。たった 1 文字の違いでした。

備考

ソースとアダプターの件数は変わりません:94 ソース(89 EN + 5 RU)、89 アダプター。テストは 3201 → 3210(フッターリンクのロケール対応 +5、SEEK ホスト +4)。 外部からは解消を確認できず、主張ではなく記録にとどめたもの:Oracle Cloud(Akamai が REST プローブを 2 件とも遮断)、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、career-ops-hq/main から 75 コミット)。今回新規ソースはありません:ミラー修正 4 件、うち 2 件はここで求人情報を静かに失わせていました。

修正

Jobstreet / SEEK — オーストラリア、ニュージーランド、シンガポール、マレーシア、香港のすべての求人票が無効なページにリンクしていました。 parseJobstreetItem は詳細 URL を無条件に ${baseUrl}/id/job/${id} として組み立てていました。/id/ セグメントはパスではなく、インドネシアロケールのプレフィックスであり、インドネシアのホストにしか存在しません。他のすべての SEEK プラットフォームホストは /id/job/<id> に対して 404 を返します。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/ になり、パースできないオリジンは throw せずに共通パスへフォールスルーします。自前のテスト 2 件はこのバグをそのままコード化していました——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 は引き続き意図的に無視されます——一部のテナントは 7000 件規模のボードのすべてのページで hasMore:false を返すためです。短いページがもはや巡回を終了させなくなったいま、テナントが偽の合計値を報告した場合の唯一の歯止めは MAX_PAGES であり、これは専用のテストでカバーされています。 Liveness — 「This role is closed」が認識されておらず、誤った expired 判定は恒久的に残ります。 パターンは /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 はいまソフトティアに置かれ、可視の Apply ボタンには勝てません。 HARD_EXPIRED_PATTERNS は apply コントロールの判定より前にチェックされるため、そこに置かれたものは何であれ生きている求人票に勝ってしまいます。/\bjob expired\b/i は実在する生きたページの 4 つの形——「Similar jobs」カルーセルのエントリ、「Hide job expired」フィルターチップ、フッターの FAQ、そして上記の closed-loop の文章——で誤発火します。分類器はページ全体のテキストを受け取るためです。いまは SOFT_EXPIRED_PATTERNS に置かれ、apply コントロールの判定の後、一覧ページのヒューリスティックの前にチェックされ、expired_body_soft を返します。apply コントロールを伴わない素の「JOB EXPIRED」バナーは引き続き expired と判定されます。この向きが重要なのは、誤った expired はスキャン履歴に skipped_expired として書き込まれ、scan_history.recheck_after_days が設定されない限り、以降のすべてのスキャンから実在する求人を無期限に重複除外し続けるためです。

追加

Personio — 明示的な personio: <slug> でテナントを固定できます。 多くの企業は自社ブランドの採用ページに Personio のボードを iframe として埋め込んでおり、careers_url は企業自身のドメインを指す一方でフィードは <slug>.jobs.personio.de にあります。固定なしではそれらのボードは何にも解決されません。スラッグはアンカー付きの許可リスト(/^[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 に解決されます。 Jobstreet — オプトインの appendWorkType は求人票の勤務形態をタイトルの末尾に付加します。例:Strategy Consultant [Part time]。これにより、タイトルフィルターとトリアージがスキャン時点で雇用形態を確認できます。既定では無効で、workTypes が欠落または空の場合は no-op です。

備考

ソースとアダプターの件数は変わりません:94 ソース(89 EN + 5 RU)、89 の EN アダプター。今回のリリースで動くのはテスト件数のみです:3164 → 3201 件(jobstreet +13、Oracle Cloud +7、liveness +9、Personio +8)。 親プロジェクトは 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 はこれらをミラーもシェルアウトもしていません)。Go 製ダッシュボード TUI と web/ の設定フォーム(別サーフェスのため)。doctor.mjs と analyze-patterns.mjs(読み取り専用の中継であり、そのフェイルソフトが変更を吸収します)。 フォーク独自の providers/telegram-channel.mjs にあるキリル文字 \p{L} の分岐は、アップストリームのマージを無傷で乗り越えており、マージ前のコピーとバイト単位で照合して検証済みです。

[1.236.0] — 2026-09-18

親パリティ — career-ops main @ 6a9c84c(VERSION は 1.33.0、128 コミット)。新規ソース 2 件と、ミラー修正 3 件——うち 2 件はここで求人情報を静かに失わせていました。

追加

スキャナーソース 2 件、92 → 94(89 EN + 5 RU)。 Python.org Jobs(pythonorg)はそのボードの公開フィードを読み取ります。雇用主の帰属表示は必須のため、タイトルに雇用主名が含まれない行は、ポータル側のエントリ名で埋め合わせるのではなく除外されます。Generalist World(generalist-world)はそのボードのカードをスクレイピングし、パースできないマークアップに対して大きな音を立てて失敗します——カードの形が変わると、空のボードとして読めるのではなく throw します。これはスクレイピング由来のソースを信頼できなくする、まさにその故障モードです。どちらも #/scan の Source フィルターに現れ、どちらもホストに固定されており(アンカー付き正規表現、HTTPS 限定)、どちらもフェイクトランスポートのスイートでエンドツーエンド駆動されます(43 件と 36 件のケース)。

修正

複数都市で募集している Workday の求人が、ロケーションフィルターによって除外されていました。 Workday の一覧エンドポイントは、他のすべての求人が地名を持つのに対し、複数拠点の求人には件数で応答します:"53 Locations"。これは地名ではありませんが、buildLocationFilter は大文字小文字を区別しない部分一致でマッチさせるため、Austin と 他の 52 都市で募集している求人は allow: [austin] のどのエントリにもマッチせず除外され、一方同じ求人が単独で列挙されていれば通過していました。3 テナントにわたって上流で計測したところ、291 件中 53 件の求人がプレースホルダーであり、これは例外ではなく通常のケースです。求人の実際の所在地は CXS の詳細ドキュメントにしか存在しないため、プレースホルダーごとに追加の GET リクエストが 1 回発生するようになり、1 エントリあたり上限 200 件に抑えられています——上限を超えるとプレースホルダーは偽装されずそのまま可視のままにされます。未解決の求人が解決済みの求人として読めてしまうことは、決してあってはならないからです。詳細取得が失敗するか所在地が空の場合、求人は一覧が返したままの状態に留まります:可視の "53 Locations" は解決できなかったことに正直であり、空欄にされたロケーションはすべてのフィルターを静かに通過してしまいます。同じ GET は startDate も運ぶため、拡充された求人は一覧エンドポイントの相対的な文言("Posted 30+ Days Ago" → undefined)ではなく正確に日付が付きます。素の YYYY-MM-DD でないものはそのまま残され、非 ISO のフォールバックが実装依存である Date.parse には通しません。 明記されたレベルが、2 件の求人票を 1 件の再掲載へ折り畳んでしまうことはもうありません。 "Insurance Specialist II" は、この求人票がその「兄弟」ではないという雇用主自身の意思表示です——同じ基本タイトルの下にある、異なる給与帯・職務範囲・求人番号だからです。トークナイザーはそれを見分けられませんでした: w.length > 3 は VIII までのすべてのローマ数字と一桁の数字をすべて捨ててしまうため、"Insurance Specialist" と "Insurance Specialist II" は同一にトークン化され、完璧な Jaccard 比を記録し、再掲載検出器は後者を前者の再出品として検知していました。web-ui の role-matcher.mjs はレベルの扱いをまったく持っていなかったため、今回は親のメカニズム全体が着地します:レベルはローマ数字とアラビア数字を 1 つの数値に畳んで抽出され("Nurse II" と "Nurse 2" は同一の表明です——そしてその組み合わせこそ、以前は誤って別物と判定されていました)、明記された 2 つのレベルは一致していなければならず、片方だけにレベルがある場合は緩い書き直しとして扱われます。ただし双方のタイトルが相手にはない単語を含んでいる場合は例外で、これは装飾ではなく 2 件の不一致とみなされます。 フィルターに固定されているはずの Avature ボードが、それでもグローバルボード全体を巡回していました。 Avature の検索 UI は不透明なファセットパラメータを生成します(?42386=[812132] は国別フィルターです)。オペレーターが portals.yml で絞り込んだボードを固定した場合、スキャンはそのボードを巡回することを期待します。ところが resolveSearch は URL を ${origin}${path} として再構築し、クエリ文字列をまるごと落としていました——そのため、最初のリクエストを含むすべてのリクエストがテナントのボード全体を巡回してしまい、ページ数もリクエスト数も増え、オペレーターが意図的に除外していた求人まで含まれていました。いまはファセットがページ送りのすべてのリクエストに乗り、自前の jobOffset を最後に適用するため、たまたま同名のファセットがページ送りを乗っ取ることは決してありません。

セキュリティ

devalue 5.8.1 → 5.9.2(astro 経由の間接依存)。開いていた Dependabot アラートを 1 件解消しました——不正な入力によるサービス拒否です。サイト限定の依存関係で、ランタイムのコードに変更はありません。

備考

理由付きで未移植のもの。 親プロジェクトは共有の既定 10 秒に対し、lever に 30 秒、manfred に 25 秒のタイムアウトを与えましたが、これは移植なしです:web-ui のスキャントランスポートはすでに60 秒(DEFAULT_SCAN_TIMEOUT_MS)を許容しており、両ボードとも向こうでの修正が与える余裕よりこちらの方が大きいからです。iCIMS の二重 careers- プレフィックスも、ここでは起こり得ません:web-ui はテナントのオリジンを 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、プラグインマニフェスト。 取得後に再確認したフォークの分岐点: providers/telegram-channel.mjs のキリル文字 \p{L} 修正、フォーク独自の providers/telegram.mjs、web/src/lib/clis.ts の hermes。上流の career-ops-hq/main は完全にマージ済みです(欠落コミット 0 件)。3066 → 3164 件。 ヘルプはいま全 17 バンドルで 94 アダプター(89 EN + 5 RU)と記載されており、見出し構造は 32 H2 / 122 H3 のまま変わりません。

[1.235.0] — 2026-09-16

親パリティ — career-ops main @ 68e6b94(VERSION は 1.33.0、42 コミット)。ミラー修正 2 件、うち 1 件はここで MokaHR の求人情報を静かに失わせていました。

修正

同じ MokaHR ボードの 2 件目の求人が、追加されるはずが消えていました。 url-key.mjs は求人 URL を比較用キーへ縮約しますが、フラグメントを無条件に切り捨てていました —— ほとんどのフラグメントは表示専用に過ぎない(#apply、#section-2)ので、それ自体は妥当な判断です。MokaHR はこのレジストリ内での例外で、その求人 URL は …/social-recruitment/{tenant}/{id}#/job/{n} の形をしており、同じテナントの求人はすべて同じパスを共有し、求人 ID はフラグメントの中にしか存在しません。そのため、すべてが同一のキーへ正規化されていました。修正前に計測すると、異なる 2 件の MokaHR 求人をパイプラインに投入しても結果は1 行——エラーもなければ警告もなく、気づく手がかりが一切ありません。スキャナーの鮮度フィルター(en-scanner.mjs)も同じキーを参照するため、そのテナントの求人が一度スキャン履歴に入ると、残り全部が「既知」として捨てられていました。normalizeUrl はいま、フラグメントを落とす前に、認識できる #/job/{id} または #/jobs/{id} というルートを比較専用のクエリキーへ昇格させます —— MokaHR は既存の mokahr_job_id という名前をそのまま使うので過去に書き込まれたキーとの比較可能性を保ち、それ以外のホストには内部用の _career_ops_fragment_job_id が割り当てられます。昇格させた ID は追加であって上書きではありません:すでにそのパラメータを持つ URL は別の求人の ID を保持しており、それを上書きすればキーが分離しようとしている当の 2 つを、まさに統合してしまうからです。認識されないフラグメントは引き続き切り捨てられ、これはすでに正しかった半分です。 accept-encoding はすべてのソースリクエストで固定されるようになりました。 未設定のままだと、Node は自身の undici ビルドが提供するものをそのまま交渉します。Node 23 以降ではそこに zstd が含まれ、amazon.jobs の zstd レスポンスはちょうど 1024 バイトで、しかもステータス 200 のまま切り詰められて返ってきます —— そのため障害は Unterminated string in JSON at position 1024 という、トランスポートの不具合というよりは壊れた API であるかのような形で現れていました。すべてのリクエストはいま gzip, deflate, br を送信します。私たちのランタイム上ではこれは修復ではなく予防です:CI マトリクスとサーバー(22.22.1)である Node 18/20/22 は gzip, deflate を送信しており(推測ではなく直接確認済み)、ここでは何も切り詰められていませんでした。今回変わるのは br を提示するようになった点で、これらのバージョンは既定では要求しないものの正しくデコードできます。この既定値は大文字小文字を区別せずに適用され、これがレビューに値する部分です:ヘッダー名は大文字小文字を区別せず、fetch は同名の 2 エントリを 1 つのカンマ区切り値へ結合するため、呼び出し側が Accept-Encoding: identity を渡した場合、そうしなければ gzip, deflate, br, identity が送られてしまっていたはずです。

備考

fetchImpl の境界では、ヘッダーはプレーンオブジェクトのままです。親プロジェクトはそこで fetch に Headers インスタンスを渡しています。これは見落としではなく意図的な分岐です:web-ui のフェッチヘルパーは注入可能な fetchImpl を受け取り、45 個のソーススイートが捕捉した内容から opts.headers['User-Agent'] を読み取っています —— Headers インスタンスにはブラケットアクセスがないため、移植すべきは実装ではなく挙動(大文字小文字を区別しない上書き、呼び出し側が勝つ)の方だからです。呼び出し側のオブジェクトはコピーされるだけで決して変更されません。複数のソースが 1 つのヘッダー定数を共有しているためです。親の対になる修正 —— 既定のユーザーエージェントを大文字小文字を区別せず適用する件 —— は移植なしです:web-ui は既定の UA を一切注入しておらず、各ソースがそのボードが要求するものを個別に設定するため、呼び出し側の User-Agent と衝突しうる既定値がそもそも存在しません。 新規スイート 2 本と新規ケース 3 件がこの組を固定します: tests/http-accept-encoding.test.mjs(固定された値そのもの、4 つのフェッチヘルパーそれぞれ、そして 3 通りの大文字小文字表記での呼び出し側による上書き —— 単一の accept-encoding キーだけが残ることをアサートしており、これはマージのバグがまさに 2 つ生み出していたものです)と tests/parsers-pipeline-hash-route.test.mjs で、こちらはキーではなく利用者から見える継ぎ目そのものを検証します:2 件の MokaHR 求人が両方ともパイプラインへ到達すること、同じ求人を 4 通りの綴りで貼り直しても 1 行に収束すること、装飾的なフラグメントが ID になってしまわないこと。すべて移植前に失敗することを確認済みです。 このピン留めは、これらのヘルパーだけでなくトランスポートにあります。92 のソースのうち 25 は注入された fetchImpl を直接呼び、ヘルパーを一切通りません。そのため、両スキャナーが実際に注入するラッパーである makeTimeoutFetch が適用します — すべてのソースが共有する唯一の通過点です。(親に相当物は不要です。providers/_http.mjs が唯一のトランスポートだからです。)新しいソースが渡されたトランスポートではなく fetch を直接呼ぶと構造ガードが CI を落とし、振る舞いのケースでは 25 のうちの 1 つである greenhouse をラッパー経由で動かし、ヘッダーが実際に送出されることを確かめます。 理由付きで未移植: liveness-browser.mjs(ヘッド付き Playwright の再利用ガード —— web-ui はヘッド付きブラウザを操作しません)、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 は親のコピーへシェルアウトするだけなので、どちらもここではコード変更なしで着地しますが、jd-skill-gap の方は /api/jds/:id/skill-gap の中継をより正確にします: cv.md はいま、プロセスがどのディレクトリで起動されたかに関わらず、データルートから解決されます。取得後に再確認したフォークの分岐点: providers/telegram-channel.mjs のキリル文字 \p{L} 修正、フォーク独自の providers/telegram.mjs、web/src/lib/clis.ts の hermes。ソース数は 92(EN 87 + RU 5)のまま変わらず、ヘルプも無変更です。3042 → 3066 件。

[1.234.0] — 2026-09-15

親パリティ — career-ops main @ 56cce8f(VERSION はまだ 1.32.0、1.32.0 から 58 コミット先)。15 ソースにまたがるミラー修正 1 件、加えて再発を防ぐガード。

修正

不正な求人 ID がひとつあるだけで、ページ全体が空になることはもうありません。 encodeURIComponent は独りぼっちの UTF-16 サロゲートに対して URIError を投げ、JSON ペイロードはそれを運び得ます — JSON.parse('"\\uD800"') はそれを保持します。15 個のソースは各求人の URL を、パースループの内側でホスト管理下の id またはスラッグから組み立てていたため、不正な求人ひとつがループを中断させ、そのページの求人がすべて失われていました。企業ごとの catch に報告すべき内容が届く前に例外が巻き戻るため、ソース障害として記録されることすらありませんでした。親プロジェクトはこれを providers/_safe-url.mjs で修正し(safeEncodeURIComponent は URIError で null を返し、それ以外は再送出)、各ループの id をそこへ通して null のときはその 1 件だけを落とすようにしました。そのヘルパーはいま server/lib/sources/_safe-url.mjs としてあり、同じ変更がミラーされた 13 ソース — alibaba、arbeitsagentur、bamboohr、feishu-jobs、garena、jibeapply、manfred、meituan、mokahr、phenom、thehub、tkms、vdab — に加え、新しいガードが初回実行で洗い出した web-ui 専用の 2 ソース、jobstreet と trudvsemにも適用されました。どちらも同じループの形と同じ潜在的な throw を持っていました。範囲は意図的なもので、ヘルパーを通すのは API 由来の id だけです。設定由来の値は厳格なエンコードのままとし、失敗時ははっきりと壊れます —— garena の office(portals.yml のセグメント)は throw する urlSegment のままにし、その API の id は null を返す idUrlSegment を得ます。csod の corpName と 4dayweek の SLUG_RE で検証済みのスラッグは、理由付きで名前によって許可リストに入っています。 rheinmetall のタイトル・フォールバックは、不正な href で throw し得ました。 カードに見出しが無いとき、タイトルは decodeURIComponent を介して URL のスラッグから再構成されますが、これはスクレイピングした href 中の不正なパーセントエンコード列に対して throw します — デコード側での、同じ「1 行の不正がページ全体を殺す」形です。いまは防御的にデコードし、失敗時は生のスラッグへフォールバックします。 ソースレジストリは _ 始まりのファイルを飛ばすようになりました。 自動探索は server/lib/sources/ 内のすべての .mjs を自身を除いてインポートし、export const meta を持たないものには警告していました —— そのためこのヘルパーは起動時警告を生んでいました。親プロジェクトの慣行を採用しました: 先頭のアンダースコアはソースではないことを意味し、そうしたファイルはインポートも警告もされません。meta を欠く未宣言のファイルは引き続き警告され、探索テストはいまや両半分をアサートします(旧フィルターに対して赤になることを確認済み)。 サイトビルドの sync-assets.mjs もディスク上のアダプターファイルを同じ規則で数えるため、ヘルパーが無言ドロップ検出ガードに引っかかることはなくなりました。

備考

tests/sources-url-encoding-surrogate.test.mjs は親のスイートを 3 部構成でミラーします: ヘルパーの契約(サロゲートペア —— 普通の絵文字 —— は引き続きエンコードされ、値自身の toString からの throw は呼び出し側のバグとして伝播する)、15 ソース全部の個別挙動(不正な id 1 件 + 正常な id 1 件 → throw なし、正常は残り不正は落ちる、それぞれをソースがエクスポートする純粋なパーサー経由で、thehub についてはフェイクのトランスポートを使うフェッチャー経由で駆動)、そしてソース単位のガード: 変換済みの全ファイルはヘルパーを import し、かつ 呼び出さなければならず、import せずに参照するファイルは許されず、未レビューのファイルが素の encodeURIComponent で求人 URL を組み立てることも許されません —— 旧来の形をした新規ソースは、問題の行番号つきで CI が落とします。15 ソース分のケースはすべて、移植前に失敗することを確認済みです。 親デルタの残りは理由つきで未移植です: batch/batch-runner.sh --cli は CLI 専用フラグ(web-ui はバッチランナーへシェルアウトしません); invite-match.mjs はいま set-status.mjs をコードルートから解決しますが、web-ui はそれを中継していません; company-funded.mjs は内部で同じ safe-url の変更を得ましたが、/api/company-funded がパースするその --json 出力形状は変わっていません; test-all.mjs はタイムアウト予算を動かしただけです(フォーク固有の vpFixtureEnv の分岐はそのまま); web/ は親自身のフロントエンドでありミラーしません; AGENTS.md/SIGNATURES.md はリポジトリ限定です。取得後に再確認したフォークの分岐点: providers/telegram-channel.mjs のキリル文字 \p{L} 修正、フォーク独自の providers/telegram.mjs、web/src/lib/clis.ts の hermes。レジストリは変更なし: 92 ソース(EN 87 + RU 5)。ヘルプも無変更です。3022 → 3042 件。

[1.233.2] — 2026-09-10

修正 — 重大度「高」の CodeQL js/remote-property-injection 2 件を、既にあった不変条件を主張ではなく構造にすることで解消しました。

修正

POST /api/config は再起動なしで効くように保存値を process.env へ写しますが、そのループは要求本文から組み立てた Object.entries(safe) を回り、内部に __proto__ / constructor / prototype への明示的なガードを置いていました。コード自体は安全です——safe は 20 行上で KNOWN_KEYS を辿って該当するものだけを写して作られるため、プロパティ名が利用者の手に落ちることはありません。しかしループ単体では要求本文から作ったオブジェクト由来の名前を持つプロパティへ代入すると読め、CodeQL は 2 つのループにまたがる制約を追えません。いまは定数配列を直接回して safe から値を引くので、プロパティ名はモジュール階層のリテラルであることが証明できます。プロトタイプ・ガードも一緒に消えました——大文字スネークの固定リストにそれが現れることはなく、起こり得ない場合への実行時チェックは雑音だからです。挙動は同一で、警告の日付が 9 月 9 日なのは v1.232/v1.233 が行番号を動かしたためにすぎません。

備考

2 件を誤検知として却下する道もあり、本リポジトリにはその慣行もあります。ここでは修正を選びました——コメント内の主張は、解析器と次の読み手が 20 行の制約を追わずに検証できる「形」より値打ちが低いからです。 旧ガードを固定していた検査は、k === '__proto__' || … という字面をソースから grep するもので、文字列の存在を示すだけで маршрут の安全は示しません。より強い 2 つに置き換えました: process.env[k] へ到達する経路が KNOWN_KEYS の反復であること(かつ要求由来のオブジェクトを回していないこと)を見る構造検査——旧形で赤くなることを確認済み——と、config-endpoint.test.mjs の挙動検査で、__proto__・constructor・prototype・未登録名を生の JSONとして送ります。オブジェクトリテラルでは駄目で、ソース中の { __proto__: … } はプロパティを作らずプロトタイプを設定するため、JSON.stringify は {} を送り、検査は何も証明しなかったでしょう。3021 → 3022 件。

[1.233.1] — 2026-09-10

修正 — v1.233.0 が追加した操作要素が案内に一切書かれておらず、同じ節には 200 リリース前に正しさを失った指示が残っていました。

修正

DOC-1 — 追加された操作要素がどこにも説明されていませんでした。 各ドロップダウンは「既定値を使う(…)」で始まり、これまで UI に表しようのなかった状態、すなわちキーが未設定であることを表します。下の一覧から同じ値を選ぶのとの違いは目に見えず、しかも重大です——「既定値を使う(llama3.2)」は何も書かず、プロジェクトが既定値を動かせば新しい値が届き続けます。一方 llama3.2 を明示的に選ぶと OLLAMA_MODEL=llama3.2 が書かれ、次に既定値が変わったとき黙って古い値に取り残されます。CONFIG-2 と CONFIG-3 が扱ったのはまさにこの静かなずれで、仕組みはコードで直したのに、それを読む場所がありませんでした。既存の ### Behavior に 3 項目を 17 言語すべてで追加しています。見出しゲートには捉えられません——数えるのは H2/H3 であって、網羅は数ではないからです。 DOC-2 — 表の一行がキーを名乗らず、唯一の具体的な指示も誤っていました。 Key 欄には (server uses default UA) という括弧書きの注記が入り、v1.19.0 が KNOWN_KEYS と UI から削除した HH_USER_AGENT を説明していました。さらに dev.hh.ru でアプリを登録し UA 文字列を使えと案内していましたが、v1.65.0 で hh.ru アダプターが自前のブラウザー UA で公開検索サイトを読む方式に変わって以来、それは何の役にも立ちません。同様に古い記述があと 2 か所——v1.28.1 で削除された health 行を説明する「任意チェック」の項目と、dev.hh.ru の助言を繰り返すトラブルシューティング行。3 か所とも 17 言語すべてから除きました。トラブルシューティング行は削除ではなく書き換えです。半分は今も正しいからで、2026 年 7 月以降 hh.ru はロシア国外の IP に HTTP 451 を返すため、ロシアの家庭用 IP は実際に必要です。誤っていたのは登録の手順と、存在しないキーへの言及でした。 キーの表が、自分が何であるかを述べるようになりました。 ### Recognized keys は 45 個のうち 6 個を挙げるだけで、見出しは登録簿を約束しています。プロバイダー系列は同じ節の本文で扱われているので、抜粋であること、および完全な一覧はグループ分けされて #/config にあることを一文で 17 言語に明記しました。キーは増やしていません——表を膨らませるのではなく、曖昧さを解消しています。

備考

tests/help-recognized-keys.test.mjs が、欠けていた門番です。実際にキーを列挙している表の第 1 列を生きた KNOWN_KEYS と突き合わせ、dev.hh.ru に触れる言語があれば落ち、「既定値を使う」の説明を欠く言語があっても落ちます。範囲の絞り込みが肝心で、書き出しの表の PDF のように第 1 列がバッククォート付き大文字語である別の表もあるため、自分の行の少なくとも一つが本物のキーを名乗る表だけを対象にします。修正前の言語ファイルで 3 件とも赤になることを確認済みです。3018 → 3021 件。すべて既存の見出しの内側に収めたので、ヘルプは 17 言語すべてで 32 H2 / 122 H3 のままです。

[1.233.0] — 2026-09-10

修正 — #/config の保存経路で 3 件。CONFIG-3 は v1.232.1 の修正そのものが招いたものです。

修正

CONFIG-3 — 仕込まれた値とたまたま一致した意図的な選択が「触れていない」として捨てられていました。 v1.232.1 の私のコメントは「表示用の既定値と、同じ値をあえて選んだ利用者とを見分けられる」と述べていましたが、実際には見分けていません。initial は 2 つの由来を混同します: .env からの仕込み(一致は未編集を意味し、飛ばすのが正しい)と defaultValue からの仕込み(一致は曖昧——開いていないか、開いて同意したか)。HOST が無いときコントロールは 127.0.0.1 を表示しますが、まさにその値を打って保存すると {} が送られ、キーは現れません。固定する唯一の道は誤った値を入れて保存し、正しい値に戻すこと——誰も見つけない袋小路でした。initial の隣に seededFromFile を記録して解消。ファイル由来なら従来どおり、既定値由来なら dirty が有効な第二の手掛かりになります。 キーの削除が「· 0」と表示されていました。 サーバーは書いたものしか返さないため、削除には数えるものが無く、実際には削除されているのに「何も起きなかった」と読めました。POST /api/config は removed も返すようになり、トーストは両方を数えます。 セレクトは設定できても解除できませんでした。 18 個のどれにも空の選択肢が無く、フォームは '' を送れませんでした。いまは各セレクトが**「既定値を使う(…)」**で始まり、未設定のキーは既定値そのものではなくその項目を選びます。v1.57.1 の意図は残しつつ、選択のふりをやめて明示するようにしました。select-remote は replaceChildren が項目を消すため再生成しています。i18n キー 1 件 ×17。

備考

この機会に確認し、あえてそのままにしたもの: lang は前方一致規則ではなくリテラルの許可リスト(langx・lang_evil・LANG はいずれも 400)。facts.json は設計どおりデプロイ先に無い——22 ファイルがビルド時に取り込み、値は HTML に埋め込まれます。拡張子なしのアップロードをテキスト扱いするのは文書化された意図的な仕様で、書き込みも実行もせず、.exe/.zip は明示的に拒否されます。 ユニットの基準値は 3018 のまま。ゲートはブラウザー側で、playwright-config-save-scope.mjs が 6 件から 11 件へ(ブラウザー計 111 → 116)。新しい 5 件はすべて旧コードで失敗することを先に確認しました。うち 1 件は CONFIG-2 の再発防止専用です。

[1.232.1] — 2026-09-10

修正 — #/config の保存が、利用者の触れていない項目まで書き込んでいました。

修正

CONFIG-2. v1.57.1 は未設定のコントロールに defaultValue を仕込み、サーバーが実際に使う値を項目に表示させます(これは正しく、そのままです)。一方 save() は dirty を秘密項目にしか問い合わせず、それ以外はすべて毎回送っていました。つまり表示のために置かれた値が、利用者の選択として送られていたのです。設定済みキーが 2 つのフィクスチャで測ると、ドロップダウンを 1 つ変えるだけで 28 キーが送られ、誰も開いていない 18 キーが固定されました。何も変えずに保存しても 27 キーです。当日の挙動は変わりません——書かれた値は既定値そのものだからです。変わるのは各設定の状態で、「未設定、プロジェクトの既定に従う」から「.env に固定」へ移ります。curated な一覧はリリース間で動くため、次に既定を変えたとき、一度でも保存した人には黙って届かなくなります。

備考

見どころは比較の基準です。 読み込んだ cfg.values との比較——最初に試した明白な修正——では足りません。未設定の select は defaultValue を表示し、保存値は '' なので、両者は正当に食い違い、17 個のモデル選択はやはり書かれます。送信本文は 27 キーから 19 キーに減っただけで、1 にはなりませんでした。いまは各コントロールが何で初期化されたかと比べます。これだけが「触れていない」を、表示のために置かれた値と、同じ値をあえて選んだ利用者とに分けられる表現です。dirty も基準になりません——値を入力して元に戻した場合にも立つからです。 あわせて修正: トーストのキー数は t() のフォールバック引数の中にあり、config.saved は 17 言語すべてに存在するため、数字は一度も描画されていませんでした。12 キーの保存が 1 キーの保存と同じ見た目だったわけです。 他は動かしていません。v1.57.1 の事前入力はそのまま、秘密項目は触れたときだけ送られ、v1.232.0 のサーバー側の猶予はフォームを通らない直接 API 呼び出しの守りとして残ります。修正はクライアント側なのでテスト基準値は 3018 のまま。門番は tests/playwright-config-save-scope.mjs の 6 件で、test:e2e:browser に登録済み(105 → 111)、6 件すべて旧コードで失敗することを先に確認しています。

[1.232.0] — 2026-09-09

修正 — ブラウザー QA で見つかった 2 件。いずれも v1.231.5 由来ではありません。

修正

PROFILE-1 — #/profile がページ全体を横スクロールさせていました。 値はインラインのフォント指定しか持たない無名の <div> で、算出後の overflow-wrap は normal のままでした。LinkedIn の URL は CSS に正当な改行点を与えないため、要素の min-content 幅は文字列全体になります。グリッド項目は既定で min-width: auto を持ち、それ未満には縮まないので、カードはトラックを、続いて行・主カラム・文書を押し広げました(1280 px で実測 23 px の横スクロール)。共有クラス .card-value に overflow-wrap: anywhere を与え、.card-row > * { min-width: 0 } を控えに置いて解消。break-word ではなく anywhere です——min-content の算出に数えられるのは anywhere だけで、break-word では折り返しても列幅は変わりません。 CONFIG-1 — POST /api/config が select 項目にどんな値でも通していました。 {"LLM_PROVIDER":"not-a-provider"} が 200 を返し、そのまま .env に書かれていました。以後も何も失敗しません——リゾルバーが黙ってフォールバックし、利用者が選んだ設定は効かないまま健全に見えます。その 19 個の値そのものを持つ LLM_PROVIDERS は、ずっと env-config.mjs から公開されていました。検証側が参照していなかっただけです。

備考

検証するのは LLM_PROVIDER だけで、これは報告に対する意図的な絞り込みです。 モデル一覧の実際のドメインはベンダーのカタログにあり、当方のリリースとは無関係に動きます。curated な一覧を強制すれば、提供側が既に配信しているモデルを選べなくなります——実際、本リポジトリの検査自体が curated から外れた gemini-2.0-flash を設定しています。害の性質も同じ方向を示します: 誤ったモデルは呼び出し時に大きな音を立てて失敗し、黙って失敗するのは провайдер名だけです。 既に保存済みの値は大目に見ます。厳格版が使い物にならないことをブラウザー検査が示したからです。 public/js/views/config.js は保存のたびに非秘密の項目をすべて送り直すため、既存の不正値を拒むとページ全体が凍りつき、触れてもいない項目を直すまで何も変更できなくなります。いまは変更のみを拒みます。 あわせて解消: OLLAMA_API_KEY は API 経由では書けるのに #/config に項目がなく、認証プロキシ配下の Ollama を使う人はアプリから設定できませんでした。ディスクリプターを追加(秘密扱い、i18n キー 2 件 ×17)し、サーバーの 45 キーと UI の 45 記述子が一致し続けることを検査で保証します。3013 → 3018 件。

[1.231.5] — 2026-09-09

セキュリティ — 7 件の勧告を解消。うち 1 件は重大な遠隔コード実行です。 ソースファイルの変更はありません。

セキュリティ

Dependabot から取り込み: multer 2.2.0 → 2.3.0(高 — 中断時のファイル記述子リークによるサービス拒否)と、サイトの svgo 4.0.2 → 4.1.0(高および中 — removeScripts が名前空間と内容の扱いを通じて実行可能なリンクを許し、foreignObject 内の実行可能 HTML の無害化も不完全でした)。 続いて、その取り込みが引き起こした再スキャンでさらに 5 件。 影響範囲はいずれも既存の ^ 指定で満たされていたため、マニフェストの編集ではなく更新で足りました: サイトの astro 7.1.0 → 7.3.2 ——重大、AVIF 画像最適化を経由した遠隔コード実行(7.2.8 で修正)、あわせて設定 base を除去する際のパスセグメント境界チェック欠如による認可バイパス(7.2.4);sharp 0.35.3 → 0.35.4 ——高、libheif の脆弱性 2 件;両方のマニフェストで js-yaml 4.3.1 → 4.3.2 ——高、maxTotalMergeKeys が空のマージ元に対して CPU 使用を制限しませんでした。

備考

ここでの「依存関係のみ」は「無リスク」と同義ではないので、監査結果だけを根拠には出していません。js-yaml はアプリが読むすべての設定を解析し、multer はすべてのアップロードを受け取り、Astro はマイナーバージョンを跨ぎました。確認済み: npm run test:ci 3013/3013、ブラウザー検査 4/4、そしてサイトの完全再ビルドで同じ 86 ページ・17 すべてのチェンジログ・ミラー・正しい facts.json。npm audit は両マニフェストで脆弱性 0 件。ソースが変わっていないため、テスト基準値も据え置きです。

[1.231.4] — 2026-09-07

修正 — v1.231.3 は Doctor クリックの「後始末」を直しただけで、その最中は直していませんでした。モバイルとデスクトップの双方から再度の報告。

修正

withSpinner はリクエスト実行中も依然としてボタンを書き換えていました。 v1.231.3 で子ノードを控えて finally で戻すようにし、確かに戻ってはいました——しかし実行中の合図は相変わらず button.replaceChildren(document.createTextNode('⏳ ' + original)) を実行していたのです。そのため doctor.mjs の実行中ずっと、36 px の正方形は ⏳ 🩺Doctor というテキストを抱えた幅広のピルとなり、行を崩していました。デスクトップでは ⏳🩺Doctor と詰まって見えました。子を戻すことは後始末を直しただけで、その瞬間を直してはおらず、終了状態だけを見る検査はこれを通してしまいます。子要素を持つボタンは、もう一切書き換えません。 合図は .is-loading クラスが担い、これがスタイルシート側でアイコンを砂時計に差し替えるので、寸法は変わりようがありません: 320 px において待機 36 px、実行中 36 px、終了後 36 px。テキストのみのボタン——24 か所の呼び出しのうち 23——は従来どおり砂時計を前置します。

備考

教訓はコードではなく検査の側にあります。v1.231.3 のゲートは Doctor をクリックし、aria-busy が消えるのを待ってから測っていました——構造上、finally が既に修復した状態しか見えません。新しいブラウザー検査は自らプロミスを開いたまま保持し、実リクエストと競走せずに飛行中の値を採り、幅が変わらないこと・両方の <span> が残ること・はみ出しがないことを要求します。最初の草案は実クリック後に採り、リクエストが先に終わると検査を飛ばしていました——それは運で通る錠前です。3012 → 3013 件。 新しい 2 つの検査はいずれも、v1.231.3 のコードに対して失敗することを確認済みです。

[1.231.3] — 2026-09-07

修正 — Doctor をタップするとモバイルの上部バーが崩れていました。v1.231.2 公開当日に実機から報告。

修正

UI.withSpinner が、自分の装飾している当のボタンの子要素を潰していました。 実行中の合図を textContent 経由で出していたのです——button.textContent を保存し、'⏳ ' + original を書き、また書き戻す。textContent への代入はすべての子を 1 つのテキストノードで置き換えます。そして v1.231.2 は、まさに上部バーの各操作を .btn-ico と .btn-label に分割し、ラベルを max-width: 900px で隠したばかりでした。そのため Doctor への最初のタップが両方の <span> を永久に破壊し、モバイル用の規則は掛かる相手を失い、ラベルは素のテキストとして戻り、36 px の正方形はテーマ切り替えに覆いかぶさる 🩺Doctor のピルへ膨らみました。自然には直らず、再読み込みだけがマークアップを戻せました。修正では子ノードを控え、replaceChildren で復元します。テキストだけのボタン——24 か所の呼び出しの大半——は従来とまったく同じ挙動です。

  • FIND-3 — 320 px の端末で検索欄が 8 px まで潰れていました。 上部バーを 1 行にまとめた結果、searchbar が唯一の伸縮要素として残り、前回のチェンジログ自身がこう書いていました——「何もなくなるまで縮んでよい」。実際に何もなくなりました: 320 px で入力欄 8 px、340 px で 28 px——21 文字のプレースホルダーのうち 1 文字です。クリックも入力もできますが、もはや欄として読めません。min-width では解決できません。320 px のバーは余白 32 px、メニュー 40 px、操作群 162 px を抱えており、120 px を割く余地がないからです。そこで 420 px 未満——入力欄が自力で 100 px を超える幅——では虫眼鏡の後ろに隠れ、タップでバー全体へ展開します: 320 px で 182 px、360 px で 222 px、390 px で 252 px、どの幅でもはみ出しはありません。開閉ボタンはアクセシブル名を 1 つに保ち、状態は aria-expanded で伝えます。🔍/✕ の切り替えは純粋な CSS です——このボタンに JS は書き込みません。同じリリースで Doctor を壊したのが、まさにその書き込みでした。420 px を超えると何も変わりません。実ビルドに対するブラウザー回帰で発見。

備考

これは v1.231.2 で書き留めた applyI18n() の罠と同じ欠陥種別——子要素を持つようになった要素に textContent を代入するコード——を、別の呼び出し元から踏んだものです。教訓は一般化します: 要素の中身を子 <span> に分割した時点で、その要素に対する既存の textContent 書き込みはすべて潜在的な不具合になります。しかもその書き手は、いま編集しているファイルにいるとは限りません。 二重に固定しました。node:vm 上で本物の withSpinner を偽ボタンに対して動かし、320 px の実ブラウザーで Doctor をクリックします。いずれも旧コードでは失敗することを確認済みです。3009 → 3012 件(ブラウザーの 1 件はオプトインで test:ci には含みません)。

[1.231.2] — 2026-09-06

修正 — モバイルの上部バーが 2 行に折り返していた件、サーバーが古い親で動いていた件、そして cvstart.ru のリダイレクトが既に言語を含むパスすべてで 404 になっていた件。

修正

  • モバイルの上部バーが再び 1 行になり、操作はアイコンのみになりました。 操作ボタンは全幅の 2 行目に強制されており、スマートフォンでは [☰ · 検索] の下に [🔔 🌙 Doctor Scan を開く] という幅広のピル 2 つが独立した行を占めていました。現在はベルとテーマ切り替えの隣に 36 px の正方形(🩺 / ⚡)が並び、320 px でもすべてが 1 行に収まります。目視ではなく実測です: ドキュメント幅は 320・360・390・430 px、および以前ボタンが検索欄に重なっていた 560〜760 px の帯全体でビューポートと一致します。ロケールはモバイルのレイアウトにもはや一切影響しません — ラベルは非表示で、ボタンは固定サイズの正方形だからです。900 px を超えるとラベルはそのまま戻ります。アクセシブル名は aria-label から取ります。display:none のラベルは名前の計算から外れるためです。
  • リリースは親 1.32.0 を前提に作られたのに、サーバーは親 1.31.0 で動いていました。 本番サーバーの /api/health が parentVersion: 1.31.0 を返していました。ランタイム 95 ファイルを同期し、2 ファイルを削除しました。本番面に対するブラウザー QA が発見したものです。
  • cvstart.ru はパスの中身を見ずに /ru/ を前置していました。 /ru/help は /ru/ru/help に、/en/help は /ru/en/help になり、どちらも 404 でした。前者は本サイトから URL をコピーすると起きます。後者はより深刻で、英語話者が英語版にたどり着く手段を完全に塞いでいました。今は 16 個のロケール接頭辞のいずれかを既に含むパスはそのまま通り、en は取り除かれ(cvstart.org は英語をルートで配信し /en/ ディレクトリを持ちません)、それ以外は従来どおり /ru/ を受け取ります。ロケール判定はアンカー付きなので、/enterprise/ が en と誤認されることはありません。

変更

  • 旧版の QA プロンプト 98 件を qa/archive/superseded-prompts/ へ移動しました。 qa/README.md は v1.137.0 以降「最上位に残すのは現行リリースのプロンプトだけ」と定めていましたが、この規則は 95 リリースにわたり適用されていませんでした。
  • さらに 5 つのプロンプトが最上位から外れました — いずれも自らを最新と称していましたが、そうではありませんでした。 REGRESSION-FINAL.md は「唯一の権威ある、バージョン非依存の回帰プロンプト」を名乗っていましたが、その §§11–15 は閉じた 5 つのサイクル台帳(v1.55.x → v1.59.7)であり、掲げるヘルプ基準は 16 ロケールで 28 H2 / 103 H3 ——今日の 32 / 122 と 17 に対して。これに従う QA エージェントは §9 の最初の一歩で落ちていたはずです。QA-REGRESSION-PROMPT.md はバージョンなしの名前に v1.226.0 の中身でした。QA-REGRESSION-PROMPT-v1.76.0-FULL.md は 155 リリース前に固定され、REGRESSION-PROMPT-FINAL.md は v1.58.52 → v1.59.10 のサイクルを閉じるもの、QA-FULL-REGRESSION.md はそれらすべてを v1.131.1 基準で重複させていました。PARENT-SYNC-WORKLIST-v1.26.0.md(親はすでに 1.32.0)も同行し、日付入りの UX-AUDIT-2026-07-06.md は reports/ へ移りました。残るドライバーは 4 つ、その基準値は v1.231.2 の実測に更新済みです。qa/README.md は実在するフォルダーを説明するよう書き直しました。

備考

  • 上部バーの修正で踏んだ詳細度の罠 2 件を app.css にコメントしました: デスクトップ用の規則は max-width: 900px ブロックより下で宣言されているため、詳細度が同じなら後勝ちになります。モバイルの gap は黙って 24 px のままで(これが 320 px をあふれさせていました)、アイコンは非表示のまま(ボタンが空の正方形になっていました)でした。
  • applyI18n() は el.textContent に代入するため、i18n キーは内側の <span> へ移しました。ボタンに置いたままでは言語切り替えのたびにアイコンが消えます。
  • 親の最初の rsync は、ローカルにもう存在しないファイルで転送の途中で中断し、それでも成功を報告しました。親のデプロイは終了コードではなくチェックサムで検証します。

[1.231.1] — 2026-09-06

修正 — v1.231.0 のカウント一括置換が、すべてのリンクのアカウント名を書き換えていました。

修正

  • すべての Fighter90 リンクが Fighter92 になっていました。 v1.231.0 のソース数の一括置換は、数字境界の保護のもとで 90 を 92 に置き換えます。この保護は 290 や 901 を正しく除外しますが、Fighter90 の中の 90 には一致します。直前が英字で、直後はトークンが終わるだけだからです。17 の README すべて、17 のヘルプ一式すべて、サイトのヘルプミラーにわたる 562 のリンクと、ウィキの 138 件が存在しないアカウントを指しており、providers.png のバナーと「GitHub で編集」のリンクはすべて 404 でした。画像が表示されないことに気づいた利用者からの報告です。
  • 過去のリリース参照 5 件がずれました。 同じリリースの 85 → 87 置換が v1.85.0 を v1.87.0 に変え、存在しないリリースを作り出していました。

備考

  • 数字境界だけでは足りません。隣接する数字からは守りますが、識別子やバージョン文字列の一部であることからは守りません。カウント自体は正しく、変わっていません: 92 ソース、EN アダプター 87、テスト 3009。
  • 親リポジトリは無傷です。カウントの一括置換を行っていません。

[1.231.0] — 2026-09-05

追加 — 親 career-ops v1.32.0 からのスキャンソース 2 件(Collage、Telegram 厳格)、Gem の REST モード、そして移植が親自身のコードに見つけた欠陥 1 件。

追加

  • 新しいソース 2 件、90 → 92(EN 87 + RU 5)、ALL_ADAPTERS 85 → 87。どちらもトークン不要。
    • Collage(provider: collage)— Collage HR の公開求人サイト API。パスの区間はテナント自身が選んだサイトのアドレスであって会社名から作った slug ではないため、api: か secure.collage.co/jobs/<アドレス> の careers URL から読み取り、決して推測しません。推測したアドレスは他人のボードを走査することになります。
    • Telegram(厳格)(provider: telegram-channel)— 既存の telegram ソースと同じ t.me/s/<チャンネル> のプレビューを読みますが、取引は正反対です。投稿が行になるのは雇用主を名指しし、かつ求人ページへリンクしているときだけ。ダイジェスト、雇用主を伏せた投稿、連絡先のみの投稿は落とします。網羅率が低いのは設計どおりで、上流の計測では HR 運営チャンネルの 32〜77% の投稿が通過します。そのかわり、どの行も実在の雇用主と実在のリンクを持ちます。親と同じく両方を提供します。
  • Gem に任意の REST モードが加わりました(GraphQL 経路と併存、api: https://api.gem.com/job_board/v0/<board>/job_posts)。ボード ID から導出することは決してなく、GraphQL 経路が既定のままです。親 #3783 から移植。

修正

  • telegram-channel の所在地フィルターはキリル文字を認識できず、都市名を雇用主として返していました。 親の LOCATIONISH_RE は ASCII 限定の \b を使っており、キリル文字の隣では決して発火しません。そのため中のロシア語の候補(москва、спб、офис、удалёнк*)はすべて到達不能で、Senior Engineer | Москва は**「Москва」を雇用主として**返していました。これは帰属ポリシー全体が防ごうとしている、まさにその「誤ったフィールドを事実として書く」動作であり、しかもこのプロバイダーの主要な利用者層を直撃します。ロシア語チャンネルは所在地をキリル文字で書くからです。web-ui の移植では Unicode の判定(\p{L} / \p{N})を使います。これは親が 1 つ下の ROLE_WORD_RE で既に行っていることと同じです。ASCII 形式では失敗するテストで固定しました。上流では未修正です。

備考

  • 親の Workday ファセット復旧(#3851、#3874)は移植不要でした。 web-ui は Workday をページングもファセット分割もしません(offset: 0 の POST が 1 回だけ)。v1.229.0 と同じ結論を、前提とせず再確認しました。
  • providers/_types.js の変更はドキュメントのみです(省略可能な Job.salary)。
  • テスト: 2962 → 3009(+47)。

[1.230.0] — 2026-09-05

変更 — 親 career-ops v1.32.0 からのプロバイダー修正 3 件と、移植そのものが浮かび上がらせた欠陥 1 件。

追加

  • Welcome to the Jungle がサーバー側の Algolia filters 式を受け取ります(wttj: { filters: "offices.country_code:FR AND contract_type:full_time" })。キーワードだけでは全世界規模のボードは絞れません。どの max_hits 件が返るかを決めるのはスキャナーの絞り込みではなく Algolia の関連度順であり、こちらの絞り込みは届いたものに対して後から効くため、上限を超えたものはどれほど適合していても見えません。フィルターは並べ替えではなく結果集合そのものを縮めるので、クエリごとの上限は 200 から 1000 に上がります。フィルターがあれば queries は任意になり、空のクエリは「フィルターを通過するすべて」を意味します。親 #3757 から移植。

修正

  • Radancy は、テナントが自身のバナーの主張より単に少ない求人しか返していないだけの場合に「truncated」と警告しなくなりました。 親プロジェクトは data-total-results を実在の 9 テナントと突き合わせました。5 件は一致しましたが、4 件は — 小規模なものも巨大なものも含めて — 重複行が 1 行もないまま、公称より 10〜56% 少ない一意の求人しか返しませんでした。バナーはテナント自身の検索インデックスが実際に返すものを過大に示すため、蓄積件数と比較する方式は過半数のボードで誤検知になっていました。警告はこちら側の max_jobs/max_pages が走査を止めた場合にのみ出ます。呼び出し側が対処できるのはその場合だけだからです。totalResults は文脈としてメッセージに残りますが、判断には一切関与しません。#3839 から移植。
  • Radancy は JSON フラグメント要求のたびにキャッシュを回避します。 この経路の手前にあるキャッシュ層は一部のテナントで古い応答を返します。同じ求人が数ページ後に再び現れる一方、別の求人は一度も返らない — 上流で 9 回中 9 回再現しました。Cache-Control/Pragma ヘッダーでは何も変わらず、要求ごとのランダムなパラメーターで解決しました。これは呼び出しごとに一意でなければならず、ページ番号から導いてはなりません。決定的なパラメーターではキャッシュに安定した鍵を与えてしまい、同じ安定した誤った対応が残るからです。#3839 から移植。
  • Greenhouse のオフィス一覧は決定的な順序になりました。 Greenhouse は応答間で offices[] の順序を保証せず、/offices ツリーから復元した集合はその走査順に従います。どちらも求人の location フィールドに届くため、ボードがオフィスを並べ替えるとこの文字列が書き換わり、何も変わっていない求人が新規として読まれていました。親は集合を修正しましたが(#3839)、web-ui には親には存在しない同じ欠陥の 2 例目がありました — 求人自身が所在地を持たないときに使う offices[0] のフォールバックです。現在は両方とも並べ替えています。

備考

  • 親の iCIMS 修正(#3728)は移植不要でした。 これは詳細ページの JSON-LD から jobLocation の全項目を読みます。一部のテナントが実際の住所の前にすべて UNAVAILABLE の項目を出すためです。web-ui の iCIMS は一覧ページの HTML から所在地を取り、詳細ページを開くことはありません。
  • 親の _http.mjs の変更も移植不要でした。 isRefusedRedirectError を切り出して 2 番目の利用者と判断を共有するためのものですが、web-ui にその 2 番目の利用者はなく、isRetryableError の挙動は変わりません。
  • 所在地を考慮した重複排除(#3751)は反映していません — 親 CLI の scan.mjs パイプラインにあるものです。
  • レジストリは変更なし: 90 ソース(EN 85 + RU 5)。テスト: 2956 → 2962(+6)。

[1.229.0] — 2026-09-03

追加 — 親 career-ops v1.31.0 から 4 つのスキャンソース: Built In、Feishu Jobs、Garena、MokaHR。

追加

  • 新規ソース 4 件、86 → 90(EN 85 + RU 5)、ALL_ADAPTERS 81 → 85。いずれもトークン不要です。Built In は米国のテック求人ボード 9 市場を横断しますが、既定のクエリを持ちません: queries: も categories: もない設定は何もスキャンせず、その旨を告げます —— 共有ソースが誰か一人の検索語を抱えてはならないからです。許可リストにない市場は拒否され、黙って全国版に差し替えられることはありません。Feishu Jobs が受け付けるホスト形式は 2 つだけで、求人ページのパスが互いに異なります。Garena は単一企業で、office はリンクを形づくり、一覧には影響しません。MokaHR は暗号化された応答(鍵が隣に届く AES-128-CBC のエンベロープ)を返します —— 難読化であって保護ではありませんが、復号しなければ何も読めません。

修正

  • htmlToText が不完全なタグ開始文字をテキストに漏らしうる問題。 safe &lt;img src=x onerror=1 が safe <img src=x onerror=1 として出ていました。今は各デコードのあとに除去が走り、末尾の開始文字は山括弧を失います。親から移植(#3491)。

備考

  • 親の Workday 修正からは何も移植していません。 あれは CXS バックエンドがオフセット 2000 でページングを打ち切るテナントを救うものですが、web-ui はそもそも Workday をページングしません(offset: 0 の POST 1 回、100 行)。
  • verify-cv-facts と merge-tracker の修正は移植不要でした: web-ui はこれらを親のチェックアウトから実行します。
  • テスト: 2909 → 2956(+47)。

[1.228.5] — 2026-08-30

修正 — 報告されるバージョンは、実行中のコードではなくディスク上のファイルを表していました。

修正

  • /api/health が毎リクエストで package.json を読み直していたため、古いプロセスが自分では実行していないバージョンを報告していました。 QA は、version: 1.228.4 と答えながら 1.228.3 で追加された /api/ping に 404 を返すローカルインスタンスを見つけました。デプロイ前に起動し、古いコードを提供しつつ、新しいファイルのバージョンを報告していたのです。ファイルをコピーしても再起動しないデプロイは、まさにこうして成功に見えます —— 運用者が確認する唯一の文字列が、その問題を見られない文字列なのです。v1.228.1 のシンボリックリンクと同じ種類で、あのときも rsync は転送の途中で中断しながら成功を報告していました。

    バージョンはモジュール読み込み時に取得され、実行中のコードを表します。古いプロセスは古いバージョンを報告します —— それが真実であり、最初のリクエストで見えます。parentVersion は意図的にリクエストごとのままです。親チェックアウトを表すもので、このプロセスは読み込まず、稼働中のサーバーの下で正当に変わりうるからです。

[1.228.4] — 2026-08-30

修正 — 認証なしの死活監視が毎リクエストでディスク処理をしていました。

修正

  • GET /api/ping が毎回 package.json を読み込んで解析していました。 認証なしで到達できる唯一のエンドポイントであり、リクエストごとのディスク読み取りと JSON 解析は誰にでも手渡されたサービス拒否のレバーです —— CodeQL はレート制限の欠如として指摘しました。バージョンはルート登録時に一度だけ読み込みます。再起動なしに変わることはないので、ハンドラーは I/O を一切行いません。レート制限は被害に上限をかけますが、処理を取り除けばレバーそのものが消えます。
  • ループバック外でのプロフィール所有者の実名マスクがテストで固定されました。 /api/health はすでに hidden に置き換えていましたが、それはその行がプロジェクトルートと hidden ?? value のガードを共有しているからにすぎません。何もそれを検証しておらず、その行に独自の値を与える変更があれば、実在する人物の名前が誰にも気づかれずに LAN へ漏れていたはずです。

[1.228.3] — 2026-08-30

Fixed — the docs assistant could not answer in English, Telegram company names arrived truncated, and a monitor had no way to see the service was up.

Fixed

  • The docs assistant answered “no matching help sections were found” in English while the same question worked in Russian. Retrieval was fine; the context builder was not. buildAskPrompt did if (ctx.length + sec.body.length > MAX_CONTEXT) break — a break, so one oversized section ended the loop and left the context empty, and the assistant truthfully reported it had nothing. The English §5 had grown to 16 081 bytes against a 14 336-byte budget (partly because v1.228.0 added telegram_channels to it); the Russian §5 was 13 009 and fit. Same ranking in both languages — §5 was first — only the size differed.

    Two changes: a section past 6 KB is now split at its ### boundaries, so telegram_channels is its own retrievable chunk (1 340 bytes) that ranks first in both languages; and an oversized section is skipped or truncated rather than ending the loop. The guide went from 32 chunks to 75.

  • A Telegram company label yielded the sentence after it, not the name. Компания: FinCore Technology (продуктовая разработка, высоконагруженные системы) became FinCore Technology (продуктовая разработка, высоконагруженны — capped at 60 characters, cut mid-word. Three of 141 rows. The name now ends at the first separator that introduces a description — a spaced dash (so Coca-Cola survives), a comma, semicolon, pipe or opening parenthesis — and any remaining cap falls on a word boundary. This source attributes a company as fact; a truncated sentence is as wrong as a guessed name, only less obviously so.

Added

  • GET /api/ping — a liveness probe that leaks nothing. { ok, version } and no more. /api/health reports absolute paths, the profile owner’s real name and which API keys are set: legitimate for an authenticated operator, never for the public internet. A monitor behind a Basic-auth gate previously had no way to see the service was up at all.

[1.228.2] — 2026-08-30

Fixed — a two-word search in /api/scan-results matched the phrase, not the words, and under-reported badly.

Fixed

  • ?q= now matches every term independently instead of the literal phrase. The query was a single includes(q) over title + company + location, so продакт менеджер found only rows carrying those two words in that exact adjacency — 32 of the 162 that carry both. Менеджер продукта and Менеджер по развитию продукта were invisible to it: the same words, reordered or with filler between them.

    This is how a narrow match becomes a wrong answer. The Telegram assistant asked for all Продуктовый менеджер reported 9 — faithfully, because 9 was what the endpoint said. The count it quoted was honest; the endpoint’s was not. Word order and the words between are not something a person typing two words intended to pin.

    Matching is AND across terms, so widening precision costs nothing: a row must still carry every term. менеджер разработчик returns zero rather than the union.

[1.228.1] — 2026-08-29

Fixed — a dangling symlink in the repo that silently truncated deploys.

Fixed

  • .claude/skills/refero-design was a tracked symlink to a path that has never existed here. It pointed at ../../.agents/skills/refero-design, which resolves to .agents/ at the repo root; there is no such directory, and the link arrived in the v1.118.0 parity pack from a machine where the surrounding folders nested differently. Anyone cloning the repo has been getting a broken link ever since.

    It was not inert. rsync aborts on it mid-transfer — a deploy today reported success while the server kept running the previous version, and the mismatch only surfaced because the version was checked afterwards. The parent project’s node test-all.mjs also crashes outright when a web-ui checkout sits inside its tree, because its fixture copy walks the working directory and cannot stat the link. Nothing referenced it: the only refero matches in the repo are the Polish word preferowany.


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