feat(source): return only new companies, in a stable order - #10
Merged
Conversation
Two changes to `prospector source`, both about repeat runs. **Only new companies pass the gate.** Sourcing had no memory: running it twice returned the same businesses both times. Companies already in the vault are now dropped immediately after dedupe and BEFORE the fetch loop, so a repeat sweep costs Places queries and nothing else. Verified against the live vault: replaying the 108 previously sourced companies drops all 108, with no false passes. Matching is by company slug, with the website domain as a second key. Slug is the only key available before the homepage fetch, which is what lets the gate run early; domain catches a business whose Places listing name has drifted since it was first sourced. Email was rejected as a key for two reasons: it is only known after fetching, and 17 of the 62 addresses in the live ledger are on free providers, where the email domain says nothing about the company. The gate is the vault, not the send ledger. The ask was "don't show me companies I already emailed", but on the real data only 51 of the 108 known companies had been emailed -- gating on the ledger alone would have let 57 already-researched, already-drafted companies back through. The vault is a superset, so the stated requirement still holds. Dropped companies are counted and reported, never silently discarded, and a sweep that finds nothing new says so instead of printing an empty table. `--include-known` bypasses the gate; `--vault` points at another vault; a run with no vault yet suppresses nothing. **Output is deterministic.** Places can return the same businesses in a different order on different days, which used to change both the row order and -- via first-seen-wins dedupe -- which of two duplicates survived. Rows are now sorted by company, and dedupe ranks candidates by metro position then place_id before collapsing, so metro precedence is unchanged but the winner no longer depends on API response order. An unchanged result set now produces an unchanged file. Tests shuffle the Places response and assert a byte-identical CSV rather than just running twice -- a run-twice test passed before this change and proved nothing.
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.
Two changes to
prospector source, both about what happens when you run it again.1. Only new companies come back
Sourcing had no memory. Running it twice returned the same businesses both times — you'd spend Places quota and a homepage fetch on every company, to produce rows you'd then throw away by hand.
Companies already in the vault are now dropped immediately after dedupe and before the fetch loop. A repeat sweep costs Places queries and nothing else.
Verified against the live vault: replaying the 108 previously sourced companies drops all 108, with no false passes.
Why the vault, and not the send ledger
The ask was "a company I already emailed shouldn't show up again". Checking the real data first changed the design:
Only 51 of the 108 known companies had actually been emailed. Gating on the ledger alone would have let 57 already-researched, already-drafted companies back through. The vault is a superset of the ledger, so the stated requirement still holds — you just also stop re-finding the ones sitting in your review queue.
Why company slug, and not email or domain
Slug matches as well as email does and is the only strong key available before the homepage fetch — which is what lets the gate run early, and the whole point of the change.
Domain alone is a trap: 17 of the 62 addresses in the live ledger are on free providers, where the email domain says nothing about the business. It's kept as a second key, though, because it catches what slug can't — a business whose Places listing name has drifted since it was first sourced.
Reporting
Dropped companies are counted, never silently discarded — the same lesson as the recent send-skip fix, since a silent drop is exactly what reads as a bug:
A sweep that finds nothing new says so outright rather than printing an empty table.
--include-knownbypasses the gate,--vaultpoints elsewhere, and a first run with no vault suppresses nothing.2. Output is deterministic
Places can return the same businesses in a different order on different days. That used to change two things: the row order in the CSV, and — through first-seen-wins dedupe — which of two duplicates survived.
An unchanged result set now produces an unchanged file, so two candidate CSVs can be diffed meaningfully.
How it was verified
pytest -q→ 607 passed (+10).The determinism tests shuffle the Places response and assert a byte-identical CSV, rather than simply running twice — a run-twice test passed before this change and proved nothing. One test covers the duplicate-domain case specifically, since that's where reordering used to swap the winner.
Gate tests cover: known company dropped; its homepage never fetched (the actual point); renamed company still caught by domain; every-company-known returns a header-only file;
--include-knownbypasses; missing vault suppresses nothing;_Dashboard.mdisn't mistaken for a company.Notes for reviewers
sourcenow reads the vault, which it previously did not touch at all. That's a new (mild) coupling between the two stages — it takes--vaultand defaults tosettings.vault_dir.The gate is a heuristic and can miss. A renamed business with a new domain slips through and appears again. That fails safely: worst case is a duplicate row in a CSV you review before feeding it to
run. It cannot cause a duplicate email — the send ledger still owns that guarantee and is untouched here.🤖 Generated with Claude Code