Skip to content

feat: add configurable salary filtering to scanner#677

Open
akashgaikwad28 wants to merge 4 commits into
santifer:mainfrom
akashgaikwad28:feat/salary-filter
Open

feat: add configurable salary filtering to scanner#677
akashgaikwad28 wants to merge 4 commits into
santifer:mainfrom
akashgaikwad28:feat/salary-filter

Conversation

@akashgaikwad28
Copy link
Copy Markdown

@akashgaikwad28 akashgaikwad28 commented May 16, 2026

Summary

This PR introduces optional salary-based filtering to the scanner pipeline.

Currently, the scanner supports title/location filtering, but compensation-sensitive users still receive jobs clearly outside their desired salary range. This change adds a lightweight and conservative salary filtering layer using structured compensation data when available.


Changes

Salary Filtering

  • Added configurable salary_filter support
  • Added min/max compensation filtering
  • Added currency-aware matching
  • Implemented range-overlap filtering logic

Compensation Parsing

  • Added structured salary parsing for Ashby jobs
  • Preserved salary ranges (min, max, currency)
  • Added annualization support for multiple compensation intervals

Filtering Behavior

  • Missing salary data is preserved conservatively
  • Currency mismatches are rejected when both currencies are known
  • Filtering only applies when structured compensation data exists

Example Configuration

salary_filter:
  min: 120000
  max: 250000
  currency: USD

Design Notes

This implementation intentionally stays lightweight and conservative:

  • No FX conversion
  • No equity/bonus valuation
  • No aggressive exclusion of incomplete jobs
  • Structured compensation only

The goal is to reduce downstream noise while keeping behavior predictable and backwards-compatible.


Testing Performed

  • Verified overlapping salary ranges pass correctly
  • Verified jobs completely outside configured ranges are rejected
  • Verified conservative handling of missing salary data
  • Verified currency mismatch rejection logic
  • Ran node scan.mjs --dry-run successfully

Summary by CodeRabbit

  • New Features

    • Job listings from Ashby now include annualized salary ranges with normalized min/max and currency; invalid or missing salary data is handled conservatively.
    • Scans support optional salary-based filtering using annualized min/max (max: 0 = no upper bound), preserving zero bounds, rejecting only when currencies conflict and the job is entirely outside the filter, and reporting a "filtered by salary" count.
  • Documentation

    • Added salary_filter guidance and USD example to the portal template.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1c603624-1d23-43d3-8e5f-a2a9156ee8e3

📥 Commits

Reviewing files that changed from the base of the PR and between 63cbeb8 and 917b238.

📒 Files selected for processing (4)
  • providers/_types.js
  • providers/ashby.mjs
  • providers/greenhouse.mjs
  • templates/portals.example.yml

📝 Walkthrough

Walkthrough

This PR annualizes Ashby provider compensation, exposes structured salary on fetched jobs, adds an optional salary_filter config, and applies salary-based filtering in the scan pipeline with counters and summary reporting.

Changes

Salary filtering for job scans

Layer / File(s) Summary
Compensation annualization helpers
providers/ashby.mjs, scan.mjs
INTERVAL_MULTIPLIERS maps interval labels to annual multipliers. parseCompensation() validates and annualizes Ashby compensation, returning { min, max, currency } or null.
Ashby salary output
providers/ashby.mjs
Ashby provider's job mapping now includes salary: parseCompensation(j) for each fetched job.
Salary filter implementation
scan.mjs
buildSalaryFilter() constructs a null-safe predicate: preserves 0 bounds, disables on malformed bounds, conservatively passes jobs with missing salary, rejects on currency mismatch only when both currencies present, and rejects only when job annualized range is entirely outside configured min/max.
Salary filter integration in scan pipeline
scan.mjs
Wires salaryFilter from config.salary_filter, initializes totalFilteredSalary, applies salary filtering after title/location checks per job, and includes filtered count in final summary.
Configuration, types, and provider JSDoc tweaks
templates/portals.example.yml, providers/_types.js, providers/greenhouse.mjs
Adds optional salary_filter block with semantics and USD example; updates PortalEntry/FetchOptions JSDoc entries; minor Greenhouse JSDoc and cast adjustments.

Sequence Diagram

sequenceDiagram
  participant Config
  participant ScanPipeline
  participant AshbyProvider
  participant Reporter
  Config->>ScanPipeline: salary_filter (min,max,currency)
  ScanPipeline->>ScanPipeline: buildSalaryFilter(salary_filter)
  ScanPipeline->>AshbyProvider: fetch jobs
  AshbyProvider->>AshbyProvider: parseCompensation(job) -> job.salary
  ScanPipeline->>ScanPipeline: apply salaryFilter(job.salary)
  alt salaryFilter rejects
    ScanPipeline->>ScanPipeline: increment totalFilteredSalary
  else accepted
    ScanPipeline->>Reporter: include job in results
  end
  ScanPipeline->>Reporter: emit final summary (includes Filtered by salary)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Suggested labels

🔧 scripts

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add configurable salary filtering to scanner' directly and clearly summarizes the main change: introducing configurable salary-based filtering to the scanner pipeline, which is the primary objective across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@providers/ashby.mjs`:
- Around line 33-51: Validate and sanitize compensation fields before using
them: guard against comp being null, coerce/validate minValue and maxValue with
Number.isFinite (e.g., const minValue = Number(comp?.minValue) and check
Number.isFinite) and multiply only valid numbers by multiplier, and ensure
currency is a string before calling .toUpperCase() (e.g., const currency =
typeof comp?.currency === 'string' ? comp.currency : ''). In the block that
computes min/max (referencing comp, minValue, maxValue, multiplier), treat
non-finite values as null, return null if both are invalid, and use the
sanitized currency when building the returned object.

In `@scan.mjs`:
- Around line 171-177: Validate and normalize salaryFilter values before
building the predicate: in the block using salaryFilter, parse and coerce
salaryFilter.min and salaryFilter.max into numeric values (e.g., strip
non-numeric characters then parseFloat or Number), ensure they are finite and
non-negative, normalize currency via filterCurrency as you already do, and
enforce ordering (if min > max either swap them or treat the filter as invalid).
If validation fails (NaN, negative, or other malformed input) log a warning and
return a no-op predicate (() => true) or otherwise handle it explicitly so
malformed YAML like "100k", negatives, or min > max do not silently produce
incorrect filtering; reference the variables salaryFilter, min, max,
filterCurrency and the early-return predicate construction when making the
change.

In `@templates/portals.example.yml`:
- Around line 61-62: The comment about salary filters is ambiguous; update the
comment around the salary filter keys 'min' and 'max' to state that filtering is
performed against annualized compensation (i.e., inputs are converted to annual
values before filtering) and that 'max: 0' means "no upper limit"; modify the
lines that currently read "# - min/max are yearly compensation filters (before
conversion to annual)" to clearly say they are annualized values used for
filtering (after conversion) and keep the "# - max: 0 means "no upper limit""
note.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dcfb2f56-9191-4691-b736-13a00ed6c08a

📥 Commits

Reviewing files that changed from the base of the PR and between 5d1f3a3 and c3011fe.

📒 Files selected for processing (3)
  • providers/ashby.mjs
  • scan.mjs
  • templates/portals.example.yml

Comment thread providers/ashby.mjs Outdated
Comment thread scan.mjs Outdated
Comment thread templates/portals.example.yml Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
providers/ashby.mjs (1)

34-37: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Treat blank numeric/currency fields as missing before normalization.

Number('') becomes 0, so blank payload fields can be misread as valid salary bounds; and untrimmed currency strings can cause false mismatches later.

Suggested fix
-  const minValue = comp.minValue == null ? null : Number(comp.minValue);
-  const maxValue = comp.maxValue == null ? null : Number(comp.maxValue);
-  const currency = typeof comp.currency === 'string' ? comp.currency : '';
+  const normalizeNum = (v) => {
+    if (v == null) return null;
+    if (typeof v === 'string' && v.trim() === '') return null;
+    const n = Number(v);
+    return Number.isFinite(n) ? n : null;
+  };
+  const minValue = normalizeNum(comp.minValue);
+  const maxValue = normalizeNum(comp.maxValue);
+  const currency = typeof comp.currency === 'string' ? comp.currency.trim() : '';
@@
-  if (
-    (minValue != null && !Number.isFinite(minValue)) ||
-    (maxValue != null && !Number.isFinite(maxValue))
-  ) {
-    return null;
-  }
+  // normalizeNum already rejects non-finite values

As per coding guidelines, **/*.mjs: Ensure scripts handle missing data/ directories gracefully.

Also applies to: 62-63

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@providers/ashby.mjs` around lines 34 - 37, The code currently converts empty
strings to 0 and leaves untrimmed currency values; update the normalization for
comp.minValue and comp.maxValue to treat null/undefined/empty-string (after
trimming) as null and only call Number() when the trimmed value is non-empty and
numeric (e.g., check comp.minValue == null || String(comp.minValue).trim() ===
'' then set minValue = null else set minValue = Number(trimmedValue) and
similarly for maxValue), and for currency trim the string and treat an empty
trimmed value as null or '' consistently (e.g., currency = typeof comp.currency
=== 'string' ? comp.currency.trim() || '' : ''), and apply the same empty-string
handling to the other occurrences referenced around the second location (the
variables at lines 62-63) so blank inputs aren’t misinterpreted as valid numbers
or mismatched currencies.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scan.mjs`:
- Line 174: The currency normalization currently uses (salaryFilter.currency ||
'').toUpperCase() which can produce false mismatches if salaryFilter.currency
contains surrounding whitespace; update the normalization to trim the value
before upper-casing (e.g., call trim() on salaryFilter.currency before
toUpperCase) where filterCurrency is assigned and apply the same
trim+toUpperCase normalization to the other occurrences noted (lines referenced
around the assignments at 199-201) so all comparisons use trimmed, upper-cased
currency strings.

In `@templates/portals.example.yml`:
- Line 64: Update the comment that currently states "If currency mismatch (e.g.,
USD filter, EUR job), it fails" to explicitly state that rejection only occurs
when both the filter currency and the job currency are known and different;
reword to something like "Reject only if both currencies are specified and
differ (e.g., filter=USD and job=EUR) — do not reject when one side is unknown."
This clarifies the conservative behavior and prevents implying unconditional
rejection.

---

Duplicate comments:
In `@providers/ashby.mjs`:
- Around line 34-37: The code currently converts empty strings to 0 and leaves
untrimmed currency values; update the normalization for comp.minValue and
comp.maxValue to treat null/undefined/empty-string (after trimming) as null and
only call Number() when the trimmed value is non-empty and numeric (e.g., check
comp.minValue == null || String(comp.minValue).trim() === '' then set minValue =
null else set minValue = Number(trimmedValue) and similarly for maxValue), and
for currency trim the string and treat an empty trimmed value as null or ''
consistently (e.g., currency = typeof comp.currency === 'string' ?
comp.currency.trim() || '' : ''), and apply the same empty-string handling to
the other occurrences referenced around the second location (the variables at
lines 62-63) so blank inputs aren’t misinterpreted as valid numbers or
mismatched currencies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 21c206ff-ac56-4e99-9e53-61e1a6b12cd6

📥 Commits

Reviewing files that changed from the base of the PR and between c3011fe and 4565649.

📒 Files selected for processing (3)
  • providers/ashby.mjs
  • scan.mjs
  • templates/portals.example.yml

Comment thread scan.mjs Outdated
Comment thread templates/portals.example.yml Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@providers/ashby.mjs`:
- Around line 34-38: The normalizeNum function currently converts negatives to
numbers; update it so any negative numeric result is treated as invalid by
returning null: after computing n = Number(v) (in the normalizeNum function) add
a check for Number.isFinite(n) && n >= 0 and only return n in that case,
otherwise return null; keep the existing checks for null/empty-string and
Number.isFinite but ensure negative values don't propagate.

In `@templates/portals.example.yml`:
- Line 64: Update the ambiguous comment on the salary/currency mismatch (the
line reading “If both currencies are known and mismatch, the salary is
ignored/skipped.”) to explicitly state that the job fails the salary filter
(e.g., “job is rejected/skipped by the salary filter when both currencies are
known and different”) so it is clear the whole job is filtered out rather than
only the salary field being ignored.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 04064c6d-3f2f-44a6-9688-26d51529a3b6

📥 Commits

Reviewing files that changed from the base of the PR and between 4565649 and 63cbeb8.

📒 Files selected for processing (3)
  • providers/ashby.mjs
  • scan.mjs
  • templates/portals.example.yml

Comment thread providers/ashby.mjs Outdated
Comment thread templates/portals.example.yml Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant