[1.236.0] — 2026-09-18
Parent parity — career-ops main @ 6a9c84c (VERSION 1.33.0, 128 commits). Two new sources, and three mirrored fixes — two of which were losing postings here, silently.
Added
Two scanner sources, 92 → 94 (89 EN + 5 RU). Python.org Jobs (pythonorg) reads the board's public feed; employer attribution is mandatory, so a row whose title names no employer is dropped rather than backfilled with the portal entry's name. Generalist World (generalist-world) scrapes the board's cards and fails loud on markup it cannot parse — a card shape that changed throws instead of reading as an empty board, which is the failure mode that makes a scraped source untrustworthy. Both appear in the #/scan Source filter, both are host-pinned (anchored regex, HTTPS-only), and both are driven end-to-end by fake-transport suites (43 and 36 cases).
Fixed
A Workday role open in several cities was dropped by a location filter. Workday's list endpoint answers a multi-location posting with a count where every other posting carries a place: "53 Locations". That is not a location, and buildLocationFilter matches by case-insensitive substring — so a role open in Austin and 52 other cities matched no allow: [austin] entry and was discarded, while the same role listed singly passed. Measured upstream across three tenants: 53 of 291 postings were placeholders, so this is the ordinary case, not an edge one. The posting's real places exist only in the CXS detail document, so each placeholder now costs one extra GET against it, capped at 200 per entry — past the cap the placeholder is left visible rather than faked, because an unresolved posting must never read as a resolved one. A failed or placeless detail leaves the posting exactly as the list returned it: a visible "53 Locations" is honest about what could not be resolved, whereas a blanked location would silently pass every filter. The same GET also carries startDate, so an enriched posting is dated exactly instead of by the list endpoint's relative prose ("Posted 30+ Days Ago" → undefined); anything that is not a bare YYYY-MM-DD is left alone rather than run through Date.parse, whose non-ISO fallback is implementation-defined.
A stated level no longer collapses two requisitions into one repost. "Insurance Specialist II" is the employer's own statement that this requisition is not its sibling — a different pay band, scope and req number under one base title. The tokenizer could not see it: w.length > 3 drops every roman numeral up to VIII and every single digit, so "Insurance Specialist" and "Insurance Specialist II" tokenized identically, scored a perfect Jaccard ratio, and the repost detector flagged the second as a re-listing of the first. web-ui's role-matcher.mjs had no level handling at all, so this lands the parent's whole mechanism: levels are extracted with roman and arabic folded onto one number ("Nurse II" and "Nurse 2" are the same statement — and that pair wrongly differed before), two stated levels must agree, and a level on one side alone is treated as a loose rewrite unless each title also carries a word the other lacks, which is two disagreements rather than a decoration.
An Avature board pinned to a filter walked the whole global board anyway. Avature's search UI generates opaque facet params (?42386=[812132] is a country filter), and an operator who pins a narrowed board in portals.yml expects the scan to walk that. resolveSearch rebuilt the URL as ${origin}${path} and dropped the query string outright — so every request, the first one included, walked the tenant's entire board: more pages, more requests, and postings the operator had deliberately filtered out. Facets now ride every paginated request, with our own jobOffset applied last so a stray same-named facet can never hijack pagination.
Security
devalue 5.8.1 → 5.9.2 (transitively via astro), closing the one open Dependabot alert — a denial of service on malformed input. Site-only dependency; no runtime code changed.
Notes
Not ported, each with the reason. The parent gave lever a 30s and manfred a 25s timeout against its shared 10s default — a no-port: web-ui's scan transport already allows 60s (DEFAULT_SCAN_TIMEOUT_MS), so both boards have more headroom here than the fix grants there. iCIMS' doubled careers- prefix cannot occur here either: web-ui takes the tenant origin straight from api/careers_url and never constructs a host from a tenant name; its new fallback-host walk is a feature for the parent's looser config, not a defect fix. deutschebahn and manfred gained retry-on-transient-abort, which web-ui's 60s ceiling makes far less pressing — noted rather than ported, so a future long-walk timeout has a known first thing to try. Everything else in the 128-commit delta is CLI, verifier, dashboard or web/ (the parent's own frontend): liveness-browser, check-jd-archive, verify-ats/verify-portals/verify-cv-facts, update-system, set-status, audit-portals, discover-ats, scaffolder, test-all, plugin manifests.
Fork divergences re-verified after the pull: the Cyrillic \p{L} fix in providers/telegram-channel.mjs, the fork's own providers/telegram.mjs, hermes in web/src/lib/clis.ts. Upstream career-ops-hq/main is fully merged (0 commits missing). 3066 → 3164 tests; help now states 94 adapters (89 EN + 5 RU) in all 17 bundles, and the heading structure is unchanged at 32 H2 / 122 H3.