Skip to content

fix(poller): distinct URLs are distinct leads + source diagnostics (1.7.7)#46

Merged
CryptoJones merged 1 commit into
mainfrom
fix/poller-dedup-distinct-urls
Jun 29, 2026
Merged

fix(poller): distinct URLs are distinct leads + source diagnostics (1.7.7)#46
CryptoJones merged 1 commit into
mainfrom
fix/poller-dedup-distinct-urls

Conversation

@CryptoJones

Copy link
Copy Markdown
Owner

What

Ports a code review from the sibling applytrack repo. 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_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 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_slug now 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 raises, so a genuine write failure is retried next run.
  • PollRepo.add_lead suffixes -N on 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._gather and worker._gather_by_source now 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)

  • Finding 2 (non-atomic file writes): the poller is Postgres-backed and 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/updated_at guarded by status 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.
  • Finding 5 (stale docs): 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.

🤖 Generated with Claude Code

https://claude.ai/code/session_01W84TwzhCdhmoLoxXYV2NuH

….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
@CryptoJones CryptoJones merged commit 0ecf47d into main Jun 29, 2026
2 checks passed
@CryptoJones CryptoJones deleted the fix/poller-dedup-distinct-urls branch June 29, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant