-
-
Notifications
You must be signed in to change notification settings - Fork 13.5k
fix(scan): honor blacklist domain scope #4169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2428,7 +2428,7 @@ const BLACKLIST_PATH = path.join(DATA_ROOT, 'data/blacklist.md'); | |
| * blacklist row "Acme Corp." still catches an ATS feed that says "acme corp". | ||
| * | ||
| * @param {string} text - Raw data/blacklist.md content. | ||
| * @returns {Map<string, {company: string, since: string, scope: string, reason: string}>} | ||
| * @returns {Map<string, {company: string, since: string, scope: 'company'|'domain', reason: string}>} | ||
| * Normalized company key → entry. First row wins on duplicate keys. | ||
| */ | ||
| export function parseBlacklist(text) { | ||
|
|
@@ -2441,16 +2441,53 @@ export function parseBlacklist(text) { | |
| if (company.toLowerCase() === 'company') continue; // header row | ||
| const key = normalizeCompany(company); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Preserve distinct domain blacklist rules.
Use a scope-qualified canonical hostname key for Proposed fix- const key = normalizeCompany(company);
- if (!key || entries.has(key)) continue;
const scope = (cells[3] || 'company').toLowerCase();
+ const key = scope === 'domain'
+ ? `domain:${company.trim().toLowerCase().replace(/\.$/, '')}`
+ : normalizeCompany(company);
+ if (!key || entries.has(key)) continue;🤖 Prompt for AI Agents |
||
| if (!key || entries.has(key)) continue; | ||
| const scope = (cells[3] || 'company').toLowerCase(); | ||
| entries.set(key, { | ||
| company, | ||
| since: cells[2] || '', | ||
| scope: cells[3] || '', | ||
| // A blank or unsupported scope keeps the long-standing company-name | ||
| // behavior. Only the documented `domain` value enables host matching. | ||
| scope: scope === 'domain' ? 'domain' : 'company', | ||
| reason: cells[4] || '', | ||
| }); | ||
| } | ||
| return entries; | ||
| } | ||
|
|
||
| /** | ||
| * Find the blacklist entry that applies to one posting. | ||
| * | ||
| * `company` is the established default: compare the feed's company label with | ||
| * the normalized table value. `domain` is opt-in: the table's Company cell is | ||
| * a hostname suffix, so `ibm.com` matches `jobs.ibm.com` but not `notibm.com`. | ||
| * This deliberately does not infer parent/subsidiary ownership from a URL. | ||
| * | ||
| * @param {Map<string, {company: string, since: string, scope?: string, reason: string}>} blacklist | ||
| * @param {string} company - Feed-provided company label. | ||
| * @param {string} url - Posting URL. | ||
| * @returns {{company: string, since: string, scope?: string, reason: string}|null} | ||
| */ | ||
| export function findBlacklistEntry(blacklist, company, url) { | ||
| if (!blacklist || blacklist.size === 0) return null; | ||
|
|
||
| const companyEntry = blacklist.get(normalizeCompany(company || '')); | ||
| if (companyEntry && companyEntry.scope !== 'domain') return companyEntry; | ||
|
|
||
| let hostname; | ||
| try { | ||
| hostname = new URL(url).hostname.toLowerCase().replace(/\.$/, ''); | ||
| } catch { | ||
| return null; | ||
| } | ||
|
|
||
| for (const entry of blacklist.values()) { | ||
| if (entry.scope !== 'domain') continue; | ||
| const suffix = String(entry.company || '').trim().toLowerCase().replace(/\.$/, ''); | ||
| if (suffix && (hostname === suffix || hostname.endsWith(`.${suffix}`))) return entry; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Load data/blacklist.md if the user opted in. Absent file = empty Map = no | ||
| * filtering anywhere — the scan stays byte-identical to a pre-#1742 run. | ||
|
|
@@ -3099,7 +3136,7 @@ async function main() { | |
| // silent: skips are counted and reported in the run summary, and | ||
| // --include-blacklisted lets the posting through annotated instead. | ||
| if (blacklist.size > 0) { | ||
| const blEntry = blacklist.get(normalizeCompany(job.company || company.name || '')); | ||
| const blEntry = findBlacklistEntry(blacklist, job.company || company.name || '', job.url); | ||
| if (blEntry) { | ||
| if (!includeBlacklisted) { | ||
| totalFilteredBlacklist++; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Pass the posting URL to the blacklist gates or narrow the contract
modes/auto-pipeline.md:36,modes/oferta.md:22, andmodes/apply.md:34instruct the gates to check only the posting's company. They do not apply hostname-suffix matching to the posting URL. AScope: domainentry can therefore be enforced byscan.mjsbut bypassed by these gates. Update the gates to use both the company and posting URL, or narrowDATA_CONTRACT.md:49to company-only matching for these gates.🤖 Prompt for AI Agents