feat(triage): convert foreign salaries before they meet the pay floor - #306
Open
MrReasonable wants to merge 1 commit into
Open
feat(triage): convert foreign salaries before they meet the pay floor#306MrReasonable wants to merge 1 commit into
MrReasonable wants to merge 1 commit into
Conversation
The pay floors are denominated in GBP; adverts are denominated in whatever
their market uses. `classify` compared the two as bare numbers, which broke in
two distinct ways the moment a search covered more than one currency zone.
DEFECT 1, the worse one. `_MONEY_RE` recognised money only by `[£$€]` or a `k`
suffix, so Nordic and Polish postings ("SEK 900 000", "900 000 kr",
"zl 250 000") parsed as no money at all. Combined with this module's standing
"no opinion never rejects" rule, every posting in those markets sailed past the
pay floor whatever it paid. Abstaining on what cannot be seen is correct and
stays; not seeing it was the bug.
DEFECT 2. `€` and `$` were compared to a sterling floor at 1:1. A EUR 105,000
role cleared a 100000 GBP floor while being worth about GBP 90k, so the euro
floor read ~15% too generously and the dollar floor ~35%.
- `_salary_ceiling` now returns `(amount, currency)`. A bare "90k" reports
`None` for currency, which is "the advert did not say", not "assume
sterling" -- `_pay_reject` applies that default visibly, exactly as it already
decides what an unmarked pay BASIS means.
- The parser reads a currency before OR after the amount, since the Nordics
write "900 000 kr", and accepts a space as a thousands separator.
- `core/fx.py` converts, from a rate table pinned in the release, overlaid by an
optional cache. An unknown currency or a missing rate returns None and the
caller abstains: a wrong reject bins a lead the user never sees.
- `cfg.refresh_fx_rates` (default OFF) lets a run refresh rates once at its
start, and only when the cache is over a week old. Never per lead.
`kr` is deliberately resolved to SEK. Three countries spell their currency that
way and no regex settles which; SEK is the largest by posting volume and the
three sit within ~15% of each other, so a wrong pick is far cheaper than not
parsing at all. An ISO code always wins over the bare symbol.
Three of this repo's own guards shaped the result and are worth naming, because
each caught a real mistake rather than a bookkeeping omission:
- reading XDG_STATE_HOME directly failed the Homebrew formula sweep and the
tilde roster, so the cache path goes through `paths.resolve` like every other
store;
- refreshing unconditionally in `run()` tripped the DNS guard, which was right:
it had made the orchestrator network-touching for every embedder and every
unit test;
- gating that on a `run()` parameter which `Sluice.triage` passed moved the
fetch onto the production path, so every e2e test reached the internet. The
flag belongs in config, beside `company_resolve_fetch`, which is the same
shape of decision: a real network round trip, off unless asked for.
Per-market floors are the real answer to "is this a good salary here" and are
deliberately not attempted; conversion is their prerequisite.
Closes #305
MrReasonable <4990954+MrReasonable@users.noreply.github.com>
|
Warning Review limit reachedNext included review available in 24 minutes. View limit detailsLimit details: You’ve used the included review currently available. Your 63 included PR review attempts over the past 7 days set your current allowance at 1 review per hour. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Essentials Run ID: 📒 Files selected for processing (11)
Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #305.
What was wrong
The pay floors are denominated in GBP. Adverts are denominated in whatever their market uses.
classifycompared the two as bare numbers, which is invisible in a single-currency search and breaks in two distinct ways the moment a search covers more than one.Nordic and Polish postings were not seen as money at all.
_MONEY_RErecognised money only by£ $ €or aksuffix, soSEK 900 000,900 000 krandzl 250 000parsed as nothing. Combined with this module's standing rule that no opinion never rejects, every posting in those markets passed the pay floor whatever it paid.Euro and dollar figures were compared to a sterling floor at 1:1. A
€105,000role cleared a 100000 GBP floor while being worth roughly £90k. The euro floor read about 15% too generously, the dollar floor about 35%.Both failed permissive, which is the cheaper direction, but the gate was not doing the job its name implies.
What this does
_salary_ceilingreturns(amount, currency). A bare90kreportsNonefor the currency, meaning "the advert did not say" rather than "assume sterling", and_pay_rejectapplies that default visibly. That mirrors how the function already treats an unmarked pay basis as the caller's decision, and it preserves today's verdict for every existing sterling lead.900 000 kr, and accepts a space as a thousands separator.sluice/core/fx.pyconverts, from a rate table pinned in the release and overlaid by an optional cache. Unknown currency or missing rate returnsNone, and the caller abstains.cfg.refresh_fx_rates, default off, lets a run refresh rates once at its start, and only when the cache is more than a week old. Never on the per-lead path.Decisions worth reviewing
krresolves to SEK. Sweden, Norway and Denmark all spell their currency that way and no regex settles which country a barekris in. SEK is the largest of the three by posting volume, and the three sit within about 15% of each other, so a wrong pick is far cheaper than the status quo of not parsing at all. An explicit ISO code always wins over the bare symbol, soNOK 1 100 000is never read as SEK.A pinned table, not a required fetch. A pay floor asks whether the advertised ceiling is under a number the user chose. Rates move a few percent a year, far below the precision that question needs, so a rate a month old changes essentially no verdicts. That is what makes an offline fallback honest rather than a correctness hazard, and why nothing here retries or fails hard.
Every failure path abstains. A malformed cache, an unreachable endpoint, an unknown currency, a non-positive quote: all return
Noneand none raises. The module this feeds exists to remove fails-closed behaviour, and a conversion helper that could manufacture a reject would reintroduce it.Three guards caught real mistakes
Worth naming, because each was a design error rather than a missing roster entry, and the tests were right every time.
XDG_STATE_HOMEdirectly failed both the Homebrew formula sweep and the tilde roster. The cache path now goes throughpaths.resolvelike every other store, which satisfies both by construction.run()tripped the DNS guard. That was correct: it had quietly made the orchestrator network-touching for every embedder and every unit test.run()parameter whichSluice.triagepassed moved the fetch onto the production path, so every e2e test reached the internet. The flag belongs in config besidecompany_resolve_fetch, which is the same shape of decision: a real network round trip, off unless asked for.Deliberately not in scope
Per-market floors. One sterling number across several economies does not really answer "is this a good salary here", since cost of living, tax and equity norms all differ. Conversion is the prerequisite for that work, not a substitute for it, and doing both at once would make neither reviewable.
Testing
Full suite green, 6,049 tests, up from 6,039.
ruff checkclean.New coverage, each written before the change and watched fail:
kfigure reportsNone.€90,000andSEK 900,000reject against a 100000 floor,€150,000survives.kfigure keeps exactly today's behaviour.