Skip to content

Commit 132dc1e

Browse files
authored
Filter sourced gigs for co-op roles (#436)
* Filter sourced gigs for co-op roles * Preserve relevant HN leads during cleanup * Protect relevant and staged leads during filtering * Reconcile current source roles before cleanup
1 parent e036ec2 commit 132dc1e

9 files changed

Lines changed: 419 additions & 14 deletions

File tree

‎apps/admin_dashboard/src/main.tsx‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ type JobLeadScrapeThread = {
350350
empty?: number
351351
seeking_work?: number
352352
not_contractor_friendly?: number
353+
irrelevant_role?: number
353354
expired?: number
354355
invalid_or_duplicate?: number
355356
}
@@ -366,6 +367,7 @@ type JobLeadScrapeResult = {
366367
created?: number
367368
updated?: number
368369
persisted?: number
370+
rejected_existing?: number
369371
total?: number
370372
incomplete?: boolean
371373
incomplete_reason?: string | null
@@ -5773,6 +5775,7 @@ function JobLeadScrapeStatusPanel({
57735775
["empty", filterReasons?.empty],
57745776
["seeking work", filterReasons?.seeking_work],
57755777
["not contractor-friendly", filterReasons?.not_contractor_friendly],
5778+
["outside co-op roles", filterReasons?.irrelevant_role],
57765779
["expired", filterReasons?.expired],
57775780
["invalid or duplicate", filterReasons?.invalid_or_duplicate],
57785781
].filter(([, count]) => typeof count === "number" && count > 0) as Array<[string, number]>
@@ -5919,6 +5922,12 @@ function JobLeadScrapeStatusPanel({
59195922
{result.created || 0} created · {result.updated || 0} updated
59205923
</p>
59215924
) : null}
5925+
{result?.rejected_existing ? (
5926+
<p className="text-muted-foreground">
5927+
{result.rejected_existing} previously unreviewed, unstaged off-topic leads removed
5928+
from review.
5929+
</p>
5930+
) : null}
59225931
</>
59235932
) : null}
59245933
{normalizedStatus === "succeeded" && !hasDiscoveryCounts ? (

‎apps/api/src/five08/backend/static/dashboard/.vite/manifest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"index.html": {
3-
"file": "assets/index-B-Lkpe-o.js",
3+
"file": "assets/index-BHZP3Pxu.js",
44
"name": "index",
55
"src": "index.html",
66
"isEntry": true,

apps/api/src/five08/backend/static/dashboard/assets/index-B-Lkpe-o.js renamed to apps/api/src/five08/backend/static/dashboard/assets/index-BHZP3Pxu.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎apps/api/src/five08/backend/static/dashboard/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>508 Operations Dashboard</title>
7-
<script type="module" crossorigin src="/dashboard/assets/index-B-Lkpe-o.js"></script>
7+
<script type="module" crossorigin src="/dashboard/assets/index-BHZP3Pxu.js"></script>
88
<link rel="stylesheet" crossorigin href="/dashboard/assets/index-B5zOzk_X.css">
99
</head>
1010
<body>

‎docs/discord-gig-dashboard.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,16 @@ the board's public first page. Titles that describe a contract domain (such as
119119
smart contracts or contract administration) or explicitly say full-time are
120120
excluded from this title-only pilot.
121121

122+
All four sources require a clear engineering, product, data, design, marketing,
123+
or ecommerce role in the job title or HN header before admitting a lead. The
124+
filter excludes recruiting, sales, clinical, language, and data annotation
125+
roles even when their descriptions mention technical skills. A sync also marks
126+
previously unreviewed, unstaged off-topic leads from that source as rejected so
127+
they leave the review queue. Reviewed, posted, and staged leads remain untouched.
128+
The source status reports role mismatches and this cleanup separately. Operators
129+
can restore a rejected lead if the title filter missed a suitable role; later
130+
syncs preserve that manual override.
131+
122132
Operators can run all four sources together or trigger any source alone. Each
123133
source has its own latest-run status and counts, and leads show a source label.
124134
The source filter is applied before the 50-lead page limit.

‎packages/shared/src/five08/job_lead_sources.py‎

Lines changed: 118 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from five08.job_leads import (
2222
JobLeadInput,
2323
existing_job_lead_external_ids,
24+
reject_unreviewed_irrelevant_job_leads,
2425
update_existing_job_lead,
2526
upsert_job_lead,
2627
)
@@ -59,6 +60,52 @@
5960
r"operations|solutions|compliance|officer)\b",
6061
re.I,
6162
)
63+
_COOP_ROLE_RE = re.compile(
64+
r"\b(?:"
65+
r"(?:software|full[ -]?stack|front[ -]?end|back[ -]?end|web|mobile|"
66+
r"platform|infrastructure|cloud|devops|site reliability|security|"
67+
r"machine learning|ml|ai|data|analytics|mechanical|electrical|"
68+
r"systems?)\s+(?:engineer(?:ing)?|developer|architect)|"
69+
r"engineer(?:ing)?|developer|programmer|sre|devops|"
70+
r"full[ -]?stack|front[ -]?end|back[ -]?end|data science|"
71+
r"data\s+(?:scientist|analyst|architect|specialist)|"
72+
r"(?:product|technical product)\s+(?:manager|designer|lead|owner|analyst)|"
73+
r"(?:ux|ui|ux/ui|graphic|graphics|visual|brand|web|creative)\s+designer|"
74+
r"(?:ux|ui|ux/ui)\s+researcher|"
75+
r"(?:digital|performance|growth|product|content|email|social media)?\s*"
76+
r"marketing\s+(?:expert|specialist|manager|strategist|consultant|lead)|"
77+
r"(?:seo|sem|paid media|media buying)\s+"
78+
r"(?:expert|specialist|manager|strategist|consultant|lead)|"
79+
r"media\s+(?:buyer|planner)|"
80+
r"(?:shopify|e[ -]?commerce|amazon marketplace)\s+"
81+
r"(?:expert|specialist|developer|designer|consultant|manager|lead)|"
82+
r"(?:tech|technical|engineering)\s+(?:lead|architect)|"
83+
r"(?:database|db|business intelligence|bi)\s+(?:administrator|analyst|engineer)"
84+
r")\b",
85+
re.I,
86+
)
87+
_NON_COOP_ROLE_RE = re.compile(
88+
r"\b(?:recruit(?:er|ing|ment)|talent acquisition|sourc(?:er|ing)|"
89+
r"sales|account manager|copywriter|transcri(?:ber|ption)|"
90+
r"data (?:entry|rater|annotator)|ai trainer|"
91+
r"nurs(?:e|ing)|physician|therapist|psychologist|counselor)\b",
92+
re.I,
93+
)
94+
95+
96+
def _title_is_coop_role(title: str) -> bool:
97+
"""Keep explicit technical roles; source descriptions often mention unrelated skills."""
98+
return any(
99+
_COOP_ROLE_RE.search(part) and not _NON_COOP_ROLE_RE.search(part)
100+
for part in re.split(r"[|,;]", title)
101+
)
102+
103+
104+
def _lead_is_coop_role(source_key: str, title: str, body: str) -> bool:
105+
"""HN's full first line can carry roles beyond its abbreviated display title."""
106+
if source_key == HN_WHO_IS_HIRING_SOURCE_KEY and body:
107+
return _title_is_coop_role(body.splitlines()[0])
108+
return _title_is_coop_role(title)
62109

63110

64111
def _title_is_gig(title: str) -> bool:
@@ -338,13 +385,15 @@ class HackerNewsThreadScrapeReport:
338385
filtered_empty: int = 0
339386
filtered_seeking_work: int = 0
340387
filtered_not_contractor_friendly: int = 0
388+
filtered_irrelevant_role: int = 0
341389

342390
@property
343391
def filtered_out(self) -> int:
344392
return (
345393
self.filtered_empty
346394
+ self.filtered_seeking_work
347395
+ self.filtered_not_contractor_friendly
396+
+ self.filtered_irrelevant_role
348397
)
349398

350399
def payload(self) -> dict[str, Any]:
@@ -362,6 +411,7 @@ def payload(self) -> dict[str, Any]:
362411
"empty": self.filtered_empty,
363412
"seeking_work": self.filtered_seeking_work,
364413
"not_contractor_friendly": self.filtered_not_contractor_friendly,
414+
"irrelevant_role": self.filtered_irrelevant_role,
365415
},
366416
}
367417

@@ -998,6 +1048,7 @@ def __init__(
9981048
self.include_latest = include_latest
9991049
self.include_non_contractor = include_non_contractor
10001050
self._thread_reports: list[HackerNewsThreadScrapeReport] = []
1051+
self.irrelevant_external_ids: set[str] = set()
10011052

10021053
def discover_threads(self) -> list[HackerNewsThread]:
10031054
if self.story_id is not None:
@@ -1016,6 +1067,7 @@ def discover_threads(self) -> list[HackerNewsThread]:
10161067
def collect(self) -> list[JobLeadInput]:
10171068
leads: list[JobLeadInput] = []
10181069
self._thread_reports = []
1070+
self.irrelevant_external_ids = set()
10191071
for thread in self.discover_threads():
10201072
tree = self.client.get_algolia_item_tree(thread.story_id)
10211073
children = tree.get("children") or []
@@ -1043,6 +1095,12 @@ def collect(self) -> list[JobLeadInput]:
10431095
if lead is None:
10441096
report.filtered_not_contractor_friendly += 1
10451097
continue
1098+
if not _lead_is_coop_role(
1099+
self.source_key, lead.title, lead.body_normalized
1100+
):
1101+
report.filtered_irrelevant_role += 1
1102+
self.irrelevant_external_ids.add(lead.external_id)
1103+
continue
10461104
classification = (lead.metadata or {}).get(
10471105
"contractor_classification", {}
10481106
)
@@ -1065,6 +1123,9 @@ def collection_report(self) -> dict[str, Any]:
10651123
report.filtered_not_contractor_friendly
10661124
for report in self._thread_reports
10671125
),
1126+
"irrelevant_role": sum(
1127+
report.filtered_irrelevant_role for report in self._thread_reports
1128+
),
10681129
}
10691130
return {
10701131
"thread_found": bool(self._thread_reports),
@@ -1090,6 +1151,8 @@ def __init__(
10901151
self.max_pages = max_pages
10911152
self.scanned = 0
10921153
self.expired = 0
1154+
self.irrelevant_role = 0
1155+
self.irrelevant_external_ids: set[str] = set()
10931156
self.pages = 0
10941157
self.included = 0
10951158
self.incomplete = False
@@ -1098,7 +1161,12 @@ def __init__(
10981161
def collect(self) -> list[JobLeadInput]:
10991162
leads: list[JobLeadInput] = []
11001163
seen: set[str] = set()
1101-
self.scanned = self.expired = self.pages = self.included = 0
1164+
self.scanned = 0
1165+
self.expired = 0
1166+
self.irrelevant_role = 0
1167+
self.irrelevant_external_ids = set()
1168+
self.pages = 0
1169+
self.included = 0
11021170
self.incomplete = False
11031171
self.incomplete_reason = None
11041172
now = datetime.now(timezone.utc)
@@ -1162,6 +1230,10 @@ def collect(self) -> list[JobLeadInput]:
11621230
):
11631231
continue
11641232
seen.add(guid)
1233+
if not _title_is_coop_role(title):
1234+
self.irrelevant_role += 1
1235+
self.irrelevant_external_ids.add(guid)
1236+
continue
11651237
body_raw = str(job.get("description") or job.get("excerpt") or "")
11661238
body = html_to_text(body_raw)
11671239
posting_type = JobPostingType.PART_TIME
@@ -1226,7 +1298,11 @@ def collection_report(self) -> dict[str, Any]:
12261298
"filtered_out": self.scanned - self.included,
12271299
"filter_reasons": {
12281300
"expired": self.expired,
1229-
"invalid_or_duplicate": self.scanned - self.included - self.expired,
1301+
"irrelevant_role": self.irrelevant_role,
1302+
"invalid_or_duplicate": self.scanned
1303+
- self.included
1304+
- self.expired
1305+
- self.irrelevant_role,
12301306
},
12311307
"pages_scanned": self.pages,
12321308
"incomplete": self.incomplete,
@@ -1243,11 +1319,14 @@ def __init__(self, *, timeout_seconds: float = 15.0) -> None:
12431319
self.timeout_seconds = timeout_seconds
12441320
self.scanned = 0
12451321
self.included = 0
1322+
self.irrelevant_role = 0
1323+
self.irrelevant_external_ids: set[str] = set()
12461324

12471325
def collect(self) -> list[JobLeadInput]:
12481326
leads: list[JobLeadInput] = []
12491327
seen: set[str] = set()
1250-
self.scanned = self.included = 0
1328+
self.scanned = self.included = self.irrelevant_role = 0
1329+
self.irrelevant_external_ids = set()
12511330
for term in A16Z_SEARCH_TERMS:
12521331
request = Request(
12531332
f"https://jobs.a16z.com/jobs?{urlencode({'q': term})}",
@@ -1271,6 +1350,10 @@ def collect(self) -> list[JobLeadInput]:
12711350
path = title_links[0].get("href", "")
12721351
if not path.startswith("/jobs/") or not _title_is_gig(title):
12731352
continue
1353+
if not _title_is_coop_role(title):
1354+
self.irrelevant_role += 1
1355+
self.irrelevant_external_ids.add(f"https://jobs.a16z.com{path}")
1356+
continue
12741357
source_url = f"https://jobs.a16z.com{path}"
12751358
if source_url in seen:
12761359
continue
@@ -1307,7 +1390,12 @@ def collection_report(self) -> dict[str, Any]:
13071390
"potential_gigs_scraped": self.scanned,
13081391
"included": self.included,
13091392
"filtered_out": self.scanned - self.included,
1310-
"filter_reasons": {"invalid_or_duplicate": self.scanned - self.included},
1393+
"filter_reasons": {
1394+
"irrelevant_role": self.irrelevant_role,
1395+
"invalid_or_duplicate": self.scanned
1396+
- self.included
1397+
- self.irrelevant_role,
1398+
},
13111399
"searches_scanned": len(A16Z_SEARCH_TERMS),
13121400
}
13131401

@@ -1321,10 +1409,13 @@ def __init__(self, *, timeout_seconds: float = 15.0) -> None:
13211409
self.timeout_seconds = timeout_seconds
13221410
self.scanned = 0
13231411
self.included = 0
1412+
self.irrelevant_role = 0
1413+
self.irrelevant_external_ids: set[str] = set()
13241414

13251415
def collect(self) -> list[JobLeadInput]:
13261416
leads: list[JobLeadInput] = []
1327-
self.scanned = self.included = 0
1417+
self.scanned = self.included = self.irrelevant_role = 0
1418+
self.irrelevant_external_ids = set()
13281419
for company, board in SEQUOIA_GREENHOUSE_BOARDS.items():
13291420
request = Request(
13301421
f"https://boards-api.greenhouse.io/v1/boards/{board}/jobs",
@@ -1343,6 +1434,11 @@ def collect(self) -> list[JobLeadInput]:
13431434
source_url = job.get("absolute_url")
13441435
if not isinstance(title, str) or not _title_is_gig(title):
13451436
continue
1437+
if not _title_is_coop_role(title):
1438+
self.irrelevant_role += 1
1439+
if isinstance(source_url, str):
1440+
self.irrelevant_external_ids.add(source_url)
1441+
continue
13461442
if (
13471443
not isinstance(source_url, str)
13481444
or urlsplit(source_url).scheme != "https"
@@ -1375,7 +1471,12 @@ def collection_report(self) -> dict[str, Any]:
13751471
"potential_gigs_scraped": self.scanned,
13761472
"included": self.included,
13771473
"filtered_out": self.scanned - self.included,
1378-
"filter_reasons": {"invalid_or_duplicate": self.scanned - self.included},
1474+
"filter_reasons": {
1475+
"irrelevant_role": self.irrelevant_role,
1476+
"invalid_or_duplicate": self.scanned
1477+
- self.included
1478+
- self.irrelevant_role,
1479+
},
13791480
"portfolio_companies_scanned": len(SEQUOIA_GREENHOUSE_BOARDS),
13801481
}
13811482

@@ -1468,13 +1569,24 @@ def scrape_job_leads(
14681569
created += 1
14691570
else:
14701571
updated += 1
1572+
rejected_existing = reject_unreviewed_irrelevant_job_leads(
1573+
settings,
1574+
source_key=adapter.source_key,
1575+
lead_is_relevant=lambda title, body: _lead_is_coop_role(
1576+
adapter.source_key, title, body
1577+
),
1578+
current_irrelevant_external_ids=getattr(
1579+
adapter, "irrelevant_external_ids", set()
1580+
),
1581+
)
14711582
return {
14721583
"source": adapter.source_key,
14731584
**collection_report,
14741585
"created": created,
14751586
"updated": updated,
14761587
"total": len(lead_ids),
14771588
"persisted": len(lead_ids),
1589+
"rejected_existing": rejected_existing,
14781590
"lead_ids": lead_ids,
14791591
}
14801592

0 commit comments

Comments
 (0)