Skip to content

Commit d91267b

Browse files
committed
A fired page is the work, not the new leads on it: a page of familiar profiles no longer ends the job with the frontier wide open
1 parent 1f8485f commit d91267b

4 files changed

Lines changed: 65 additions & 27 deletions

File tree

openoutreach/core/pipeline/discover.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,19 @@ def _handle_empty(node, offset: int, page) -> str | None:
151151
return verdict
152152

153153

154-
def discover(campaign, qualifier=None) -> int:
155-
"""Fire frontier nodes until one returns leads. Returns the count of new Leads.
156-
157-
``0`` means the frontier is spanned (nothing unfired and nothing left to deepen) or a
158-
fetch was unavailable — both best-effort, since a provider outage must not fail the
159-
enclosing task. A provider *refusal* is the exception and propagates
154+
def discover(campaign, qualifier=None) -> bool:
155+
"""Fire frontier nodes until one returns a page. Returns whether the walk moved.
156+
157+
**The answer is "did a page come back", not "how many leads were new"** — bug 8 again,
158+
one level up. ``_harvest`` counts *newly created* leads, and a page of profiles this
159+
campaign already holds counts 0 while still being a perfectly good page: the node's
160+
offset advanced, its children joined the frontier, and the next pass draws the next
161+
node. Reporting that as nothing left to do stopped a live run dead with 100 rows in
162+
hand and the frontier wide open (``top_up`` reads this return as *was there work*).
163+
164+
``False`` means the frontier is spanned (nothing unfired and nothing left to deepen)
165+
or a fetch was unavailable — both best-effort, since a provider outage must not fail
166+
the enclosing task. A provider *refusal* is the exception and propagates
160167
(``_fetch``): a rejected key must never be read as a query that matches nobody.
161168
``qualifier`` is accepted and ignored: the GP no longer selects
162169
queries (§13), and the parameter stays only so the call sites in ``pools`` read the
@@ -172,9 +179,9 @@ def discover(campaign, qualifier=None) -> int:
172179
from openoutreach.enrichment import bettercontact
173180

174181
if not bettercontact.is_configured():
175-
return 0
182+
return False
176183
if not (campaign.product_docs or campaign.campaign_target):
177-
return 0
184+
return False
178185

179186
logger.info(colored(f"▶ discover · {campaign}", "blue", attrs=["bold"]))
180187

@@ -189,16 +196,16 @@ def discover(campaign, qualifier=None) -> int:
189196
f"■ no more people left to find for {campaign} — every search this "
190197
f"campaign knows how to make is exhausted", "blue"))
191198
logger.debug("frontier spanned · %d node(s) retired this pass", retired)
192-
return 0
199+
return False
193200

194201
offset = node.next_offset
195202
page = _fetch(node, offset)
196203
if page is None:
197-
return 0 # outage: the node keeps its place, the caller carries on
204+
return False # outage: the node keeps its place, the caller carries on
198205

199206
if not page.leads:
200207
if _handle_empty(node, offset, page) is None:
201-
return 0 # transport artifact — re-firing now would just repeat it
208+
return False # transport artifact — re-firing now would just repeat it
202209
retired += 1
203210
continue
204211

@@ -210,4 +217,4 @@ def discover(campaign, qualifier=None) -> int:
210217
glyph="✓", color="green"))
211218
logger.debug("%s", step_line(
212219
"frontier", f"+{grown} node(s) from this page", glyph="+", color="cyan"))
213-
return created
220+
return True

openoutreach/core/pipeline/top_up.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,21 @@ def top_up(campaign, qualifier: BayesianQualifier) -> bool:
8080
consumable = _consumable_candidates(qualifier, candidates)
8181
if consumable:
8282
return run_qualification(campaign, qualifier, candidates=consumable) is not None
83-
return discover(campaign, qualifier) > 0
83+
# A fired page is the work, however much of it we had already seen. Reading
84+
# *new leads* here is what stopped a live run with 100 rows in hand: the page
85+
# was all familiar profiles, so discovery reported nothing and the whole job
86+
# ended `goal_unreached` with the frontier untouched below it.
87+
return discover(campaign, qualifier)
8488

8589
# Explore — label the most informative lead we have. The GP is fitted here, so it
8690
# ranks the pool and there is a best lead to pick; an empty pool is the one case
8791
# with no lead to label, so page one in first.
8892
if not candidates:
89-
if discover(campaign, qualifier) <= 0:
93+
if not discover(campaign, qualifier):
9094
return False
9195
candidates = fetch_qualification_candidates(campaign)
96+
if not candidates:
97+
return True # every profile on that page was already ours — still a move
9298
return run_qualification(campaign, qualifier, candidates=candidates) is not None
9399

94100

tests/test_discovery_wiring.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,13 @@ class TestGates:
6969
def test_no_finder_key_is_a_no_op(self, db):
7070
SiteConfig.objects.update(bettercontact_api_key="")
7171
with patch.object(discover_mod, "_fetch") as fetch:
72-
assert discover(_campaign()) == 0
72+
assert discover(_campaign()) is False
7373
fetch.assert_not_called()
7474

7575
def test_no_icp_text_is_a_no_op(self, db):
7676
c = _campaign(product_docs="", campaign_target="")
7777
with patch.object(discover_mod, "_fetch") as fetch:
78-
assert discover(c) == 0
78+
assert discover(c) is False
7979
fetch.assert_not_called()
8080

8181

@@ -86,9 +86,9 @@ def test_a_productive_page_creates_leads_and_advances_the_node(self, db):
8686
page = Page([_row()], 9027)
8787

8888
with patch.object(discover_mod, "_fetch", return_value=page):
89-
created = discover(c)
89+
assert discover(c) is True
9090

91-
assert created == 1
91+
assert Lead.objects.count() == 1
9292
node.refresh_from_db()
9393
assert node.state == QueryNode.State.FIRED
9494
assert node.next_offset == select.DISCOVERY_PAGE_SIZE
@@ -98,14 +98,16 @@ def test_a_productive_page_creates_leads_and_advances_the_node(self, db):
9898
def test_a_page_of_duplicates_is_not_a_stall(self, db):
9999
# Bug 8: `_harvest` counts *newly created* leads, and a page of already-seen
100100
# profiles used to read as "nothing left to do" and halt the engine with the
101-
# frontier wide open. The node must still advance.
101+
# frontier wide open. The node must still advance, and the walk must report
102+
# that it moved — `top_up` stops the whole job on a False here.
102103
c = _campaign()
103104
node = _node(c, [("lead_job_title", "founder")])
104105
Lead.objects.create(profile_url="https://linkedin.com/in/a", profile_text="x")
105106

106107
with patch.object(discover_mod, "_fetch", return_value=Page([_row()], 10)):
107-
assert discover(c) == 0
108+
assert discover(c) is True
108109

110+
assert Lead.objects.count() == 1 # nothing new created — and that is fine
109111
node.refresh_from_db()
110112
assert node.state == QueryNode.State.FIRED
111113
assert node.next_offset == select.DISCOVERY_PAGE_SIZE
@@ -147,7 +149,7 @@ def test_a_positive_count_with_no_rows_is_a_transport_artifact(self, db):
147149

148150
with patch.object(discover_mod, "_fetch",
149151
return_value=Page([], 71403396)) as fetch:
150-
assert discover(c) == 0
152+
assert discover(c) is False
151153

152154
assert fetch.call_count == 1 # not even retried — the count already answered
153155
node.refresh_from_db()
@@ -172,15 +174,15 @@ def test_the_loop_tries_the_next_node_after_a_dead_one(self, db):
172174

173175
pages = [Page([], 0), Page([_row()], 10)]
174176
with patch.object(discover_mod, "_fetch", side_effect=pages):
175-
assert discover(c) == 1
177+
assert discover(c) is True
176178

177179
assert QueryNode.objects.filter(campaign=c, state=QueryNode.State.DEAD).count() == 1
178180

179-
def test_saturation_returns_zero(self, db):
181+
def test_saturation_is_the_one_false(self, db):
180182
c = _campaign()
181183
_node(c, [("lead_job_title", "a")], state=QueryNode.State.DRAINED)
182184
with patch.object(discover_mod, "_fetch") as fetch:
183-
assert discover(c) == 0
185+
assert discover(c) is False
184186
fetch.assert_not_called()
185187

186188

@@ -192,7 +194,7 @@ def test_an_outage_leaves_the_node_on_the_frontier(self, db):
192194
node = _node(c, [("lead_job_title", "founder")])
193195

194196
with patch.object(discover_mod, "_fetch", return_value=None):
195-
assert discover(c) == 0
197+
assert discover(c) is False
196198

197199
node.refresh_from_db()
198200
assert node.state == QueryNode.State.FRONTIER

tests/test_top_up.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def test_the_cold_phase_names_itself_and_the_anchor_progress(campaign, caplog):
1616
qualifier.set_anchors(np.random.RandomState(0).rand(3, 8))
1717

1818
with (
19-
patch("openoutreach.core.pipeline.top_up.discover", return_value=0),
19+
patch("openoutreach.core.pipeline.top_up.discover", return_value=False),
2020
patch("openoutreach.core.pipeline.top_up.fetch_qualification_candidates",
2121
return_value=[]),
2222
caplog.at_level("INFO"),
@@ -69,9 +69,32 @@ def test_exploit_discovers_rather_than_qualifying_below_the_gate(campaign):
6969
patch("openoutreach.core.pipeline.top_up.fetch_qualification_candidates",
7070
return_value=[_Candidate()]),
7171
patch("openoutreach.core.pipeline.top_up.run_qualification") as qualify,
72-
patch("openoutreach.core.pipeline.top_up.discover", return_value=10) as discover,
72+
patch("openoutreach.core.pipeline.top_up.discover", return_value=True) as discover,
7373
):
7474
assert top_up(campaign, qualifier) is True
7575

7676
assert not qualify.called
7777
assert discover.called
78+
79+
80+
@pytest.mark.django_db
81+
def test_explore_counts_a_page_of_familiar_profiles_as_work(campaign):
82+
"""A fired page is the unit of work, whatever fraction of it was new.
83+
84+
A live run ended `goal_unreached` on this shape: the page came back 100 rows all
85+
already ours, so it left no candidate to label — which is not the same fact as a
86+
spanned frontier, and must not stop the job with the walk one node in."""
87+
qualifier = BayesianQualifier(embedding_dim=8)
88+
rng = np.random.RandomState(0)
89+
qualifier.warm_start(rng.rand(7, 8), np.array([1, 1, 1, 1, 0, 0, 0]))
90+
assert qualifier.acquisition_mode() != "exploit (p)"
91+
92+
with (
93+
patch("openoutreach.core.pipeline.top_up.fetch_qualification_candidates",
94+
return_value=[]),
95+
patch("openoutreach.core.pipeline.top_up.run_qualification") as qualify,
96+
patch("openoutreach.core.pipeline.top_up.discover", return_value=True),
97+
):
98+
assert top_up(campaign, qualifier) is True
99+
100+
assert not qualify.called

0 commit comments

Comments
 (0)