fix(poller): distinct URLs are distinct leads + source diagnostics (1.7.7)#46
Merged
Merged
Conversation
….7.7)
Ports a code review from the sibling applytrack repo. Two of the five findings
apply to this codebase; the other three are already handled by the .NET/Postgres
architecture (see below).
Finding 1 — dedup dropped distinct postings. score_and_stage deduped scraped
listings by a normalized company+role slug that STRIPPED trailing parentheticals,
so per-city variants of one role (each a distinct application URL) collapsed to
one lead and the rest were silently recorded as seen and dropped — e.g. five
LawnStarter city postings staged one and dropped four. Now:
- _norm_slug keeps the full role title (parentheticals included).
- Seen.has makes the posting URL authoritative: a distinct URL is a distinct
lead; the slug is only the fallback key for URL-less listings.
- score_and_stage records seen keys per outcome (staged / filtered / link-dead)
instead of up front, and no longer marks a listing seen when staging it raises,
so a genuine write failure is retried on a later run.
- PollRepo.add_lead suffixes -N on a slug-name collision (two distinct postings
whose titles normalize to the same name) rather than letting the second drop.
Finding 4 — source failures were swallowed silently, so a dead/renamed job
source looked like an empty market. poll._gather and worker._gather_by_source now
log each failing source at WARNING (and tolerate any exception, not just
HTTP/parse), and the worker logs per-tenant setup/poll failures it had been
swallowing.
Not applicable here (verified against this codebase, no change made):
- Finding 2 (non-atomic file writes): the poller is Postgres-backed; it writes no
state files.
- Finding 3 (optimistic-concurrency clobbers): the draft endpoint writes a
separate cover_letters table, not the versioned application row; the blacklist
batch (PassOpenLeadsAsync) is a single atomic UPDATE of status/version guarded
by `status IN ('lead','ready')`, so it preserves unrelated fields and won't flip
an advanced row — no read-modify-write race to guard.
- Finding 5 (stale docs): the README/SPRINTS already describe poll as
discovery-only.
Tests: ruff + mypy --strict + bandit clean; pytest green (dedup tests updated for
URL-authoritative behavior, plus new distinct-URL, unique-name, url-less-slug, and
source-failure-logging cases). dotnet build of the API clean. Version 1.7.6 ->
1.7.7 (patch) in pyproject.toml + ApplyTrack.Api.csproj.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W84TwzhCdhmoLoxXYV2NuH
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.
What
Ports a code review from the sibling
applytrackrepo. Two of the five findings apply here; the other three are already handled by the .NET/Postgres architecture (verified, no change made).Finding 1 — dedup dropped distinct openings (the core fix)
score_and_stagededuped scraped listings by a normalizedcompany + roleslug that stripped trailing parentheticals, so per-city variants of one role — each a distinct application URL — collapsed to a single lead and the rest were silently recorded as seen and dropped. (In the sibling repo this staged 1 of five LawnStarter city postings and dropped four; the same logic lived here.)_norm_slugnow keeps the full role title (parentheticals included).Seen.hasmakes the posting URL authoritative — a distinct URL is a distinct lead; the slug is only the fallback key for URL-less listings.score_and_stagerecords seen keys per outcome (staged / filtered / link-dead) instead of up front, and no longer marks a listing seen when staging raises, so a genuine write failure is retried next run.PollRepo.add_leadsuffixes-Non a slug-name collision (two distinct postings whose titles normalize to the same name) instead of dropping the second.Verified end-to-end: 5 distinct city URLs → 5 staged; re-poll → 0 (no re-pings).
Finding 4 — source failures were swallowed
A dead/renamed job source looked like an empty market.
poll._gatherandworker._gather_by_sourcenow log each failing source at WARNING (and tolerate any exception, not just HTTP/parse); the worker also logs the per-tenant setup/poll failures it had been swallowing.Not applicable here (verified, no change)
cover_letterstable, not the versioned application row; the blacklist batch (PassOpenLeadsAsync) is a single atomicUPDATEofstatus/version/updated_atguarded bystatus IN ('lead','ready'), so it preserves unrelated fields and won't flip an advanced row — there's no read-modify-write race to guard, and adding one would reduce safety.Tests
ruff+mypy --strict+banditclean;pytestgreen — dedup tests updated for URL-authoritative behavior, plus new distinct-URL, unique-name, url-less-slug, and source-failure-logging cases.dotnet buildof the API clean. Version 1.7.6 → 1.7.7 (patch) inpyproject.toml+ApplyTrack.Api.csproj.🤖 Generated with Claude Code
https://claude.ai/code/session_01W84TwzhCdhmoLoxXYV2NuH