@agent-pattern-labs/leads-rig 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/general-free.md +3 -1
- package/.cursor/rules/agent-general-free.mdc +3 -1
- package/.cursor/rules/main.mdc +2 -1
- package/.opencode/agents/general-free.md +3 -1
- package/.pi/skills/general-free/SKILL.md +3 -1
- package/AGENTS.md +2 -1
- package/CLAUDE.md +2 -1
- package/README.md +2 -1
- package/bin/create-leads-harness.mjs +1 -1
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CONSTRUCTION.md +1 -1
- package/iso/agents/general-free.md +3 -1
- package/iso/instructions.md +2 -1
- package/lib/leadharness-crawler.mjs +2 -1
- package/lib/leadharness-leads.mjs +36 -1
- package/modes/_shared.md +6 -3
- package/package.json +1 -1
- package/scripts/crawl.mjs +1 -1
- package/scripts/manifest.mjs +3 -2
- package/scripts/pipeline.mjs +2 -1
- package/scripts/validate-leads.mjs +1 -1
|
@@ -27,7 +27,9 @@ Skip this only if the task explicitly says to attach to an existing session.
|
|
|
27
27
|
## Do
|
|
28
28
|
|
|
29
29
|
- Crawl only public, high-signal company pages.
|
|
30
|
-
-
|
|
30
|
+
- Use your judgment on each email candidate: only emit an email lead when public evidence identifies one specific human owner of the address.
|
|
31
|
+
- Extract high-quality named human emails, contact forms, public names/titles, source URL, evidence, and warnings.
|
|
32
|
+
- If an address is shared, role-based, departmental, generic, blocked, or not tied to a visible named human, do not emit it as an email lead; emit a `contact_path` if useful.
|
|
31
33
|
- Emit JSON/JSONL matching `templates/lead-schema.json`.
|
|
32
34
|
- Run `npx public-leads validate --input <file>` before returning a result.
|
|
33
35
|
- Return file paths and terminal status, not a prose-only summary.
|
|
@@ -26,7 +26,9 @@ Skip this only if the task explicitly says to attach to an existing session.
|
|
|
26
26
|
## Do
|
|
27
27
|
|
|
28
28
|
- Crawl only public, high-signal company pages.
|
|
29
|
-
-
|
|
29
|
+
- Use your judgment on each email candidate: only emit an email lead when public evidence identifies one specific human owner of the address.
|
|
30
|
+
- Extract high-quality named human emails, contact forms, public names/titles, source URL, evidence, and warnings.
|
|
31
|
+
- If an address is shared, role-based, departmental, generic, blocked, or not tied to a visible named human, do not emit it as an email lead; emit a `contact_path` if useful.
|
|
30
32
|
- Emit JSON/JSONL matching `templates/lead-schema.json`.
|
|
31
33
|
- Run `npx public-leads validate --input <file>` before returning a result.
|
|
32
34
|
- Return file paths and terminal status, not a prose-only summary.
|
package/.cursor/rules/main.mdc
CHANGED
|
@@ -10,6 +10,7 @@ Agentic public-web lead discovery harness. It crawls company domains, extracts r
|
|
|
10
10
|
## Hard Limits
|
|
11
11
|
|
|
12
12
|
- [H1] Only collect public-source business contact data. Do not infer, guess, brute-force, or enrich private email addresses. Every lead needs a source URL and evidence excerpt. The primary goal is named people, not generic company inboxes.
|
|
13
|
+
- [H8] Apply coding-agent judgment to every email candidate. An email may pass only when public source evidence identifies one specific human owner of that email. Shared, role, departmental, generic, blocked, unknown-owner, or unnamed person-like emails must not be emitted as email leads.
|
|
13
14
|
- [H2] Respect robots.txt, terms, rate limits, and site intent. If a page blocks crawling or is not public, record a warning and move on.
|
|
14
15
|
- [H3] Never send outreach from this harness. The output is reviewable lead records and ingest-ready artifacts, not email campaigns.
|
|
15
16
|
- [H4] Use file-backed artifacts before ingest. A subagent prose summary is not enough; write validated JSON/JSONL first, then ingest.
|
|
@@ -23,7 +24,7 @@ Agentic public-web lead discovery harness. It crawls company domains, extracts r
|
|
|
23
24
|
- [D2] Use deterministic local helpers when they can validate, normalize, manifest, preflight, postflight, or ingest artifacts.
|
|
24
25
|
- [D3] Read the active mode file before executing. Mode files own output shapes and gates.
|
|
25
26
|
- [D4] Prefer official company websites and high-signal pages: home, contact, about, team, leadership, people, press, blog, careers, and legal pages.
|
|
26
|
-
- [D5] Keep artifacts source-backed. Every emitted lead should include `domain`, `sourceUrl`, `evidence`, `emailType`, `extractionMethod`, `verificationStatus`, and `confidence`. Reject generic catch-all inboxes such as `info@`, `hello@`, `contact@`, `support@`, `team@`, and similar aliases; if no named
|
|
27
|
+
- [D5] Keep artifacts source-backed. Every emitted lead should include `domain`, `sourceUrl`, `evidence`, `emailType`, `extractionMethod`, `verificationStatus`, and `confidence`. Reject generic catch-all inboxes such as `info@`, `hello@`, `contact@`, `support@`, `team@`, and similar aliases; if no named human email is available, prefer a `contact_path` record instead.
|
|
27
28
|
|
|
28
29
|
## Procedure
|
|
29
30
|
|
|
@@ -36,7 +36,9 @@ Skip this only if the task explicitly says to attach to an existing session.
|
|
|
36
36
|
## Do
|
|
37
37
|
|
|
38
38
|
- Crawl only public, high-signal company pages.
|
|
39
|
-
-
|
|
39
|
+
- Use your judgment on each email candidate: only emit an email lead when public evidence identifies one specific human owner of the address.
|
|
40
|
+
- Extract high-quality named human emails, contact forms, public names/titles, source URL, evidence, and warnings.
|
|
41
|
+
- If an address is shared, role-based, departmental, generic, blocked, or not tied to a visible named human, do not emit it as an email lead; emit a `contact_path` if useful.
|
|
40
42
|
- Emit JSON/JSONL matching `templates/lead-schema.json`.
|
|
41
43
|
- Run `npx public-leads validate --input <file>` before returning a result.
|
|
42
44
|
- Return file paths and terminal status, not a prose-only summary.
|
|
@@ -26,7 +26,9 @@ Skip this only if the task explicitly says to attach to an existing session.
|
|
|
26
26
|
## Do
|
|
27
27
|
|
|
28
28
|
- Crawl only public, high-signal company pages.
|
|
29
|
-
-
|
|
29
|
+
- Use your judgment on each email candidate: only emit an email lead when public evidence identifies one specific human owner of the address.
|
|
30
|
+
- Extract high-quality named human emails, contact forms, public names/titles, source URL, evidence, and warnings.
|
|
31
|
+
- If an address is shared, role-based, departmental, generic, blocked, or not tied to a visible named human, do not emit it as an email lead; emit a `contact_path` if useful.
|
|
30
32
|
- Emit JSON/JSONL matching `templates/lead-schema.json`.
|
|
31
33
|
- Run `npx public-leads validate --input <file>` before returning a result.
|
|
32
34
|
- Return file paths and terminal status, not a prose-only summary.
|
package/AGENTS.md
CHANGED
|
@@ -5,6 +5,7 @@ Agentic public-web lead discovery harness. It crawls company domains, extracts r
|
|
|
5
5
|
## Hard Limits
|
|
6
6
|
|
|
7
7
|
- [H1] Only collect public-source business contact data. Do not infer, guess, brute-force, or enrich private email addresses. Every lead needs a source URL and evidence excerpt. The primary goal is named people, not generic company inboxes.
|
|
8
|
+
- [H8] Apply coding-agent judgment to every email candidate. An email may pass only when public source evidence identifies one specific human owner of that email. Shared, role, departmental, generic, blocked, unknown-owner, or unnamed person-like emails must not be emitted as email leads.
|
|
8
9
|
- [H2] Respect robots.txt, terms, rate limits, and site intent. If a page blocks crawling or is not public, record a warning and move on.
|
|
9
10
|
- [H3] Never send outreach from this harness. The output is reviewable lead records and ingest-ready artifacts, not email campaigns.
|
|
10
11
|
- [H4] Use file-backed artifacts before ingest. A subagent prose summary is not enough; write validated JSON/JSONL first, then ingest.
|
|
@@ -18,7 +19,7 @@ Agentic public-web lead discovery harness. It crawls company domains, extracts r
|
|
|
18
19
|
- [D2] Use deterministic local helpers when they can validate, normalize, manifest, preflight, postflight, or ingest artifacts.
|
|
19
20
|
- [D3] Read the active mode file before executing. Mode files own output shapes and gates.
|
|
20
21
|
- [D4] Prefer official company websites and high-signal pages: home, contact, about, team, leadership, people, press, blog, careers, and legal pages.
|
|
21
|
-
- [D5] Keep artifacts source-backed. Every emitted lead should include `domain`, `sourceUrl`, `evidence`, `emailType`, `extractionMethod`, `verificationStatus`, and `confidence`. Reject generic catch-all inboxes such as `info@`, `hello@`, `contact@`, `support@`, `team@`, and similar aliases; if no named
|
|
22
|
+
- [D5] Keep artifacts source-backed. Every emitted lead should include `domain`, `sourceUrl`, `evidence`, `emailType`, `extractionMethod`, `verificationStatus`, and `confidence`. Reject generic catch-all inboxes such as `info@`, `hello@`, `contact@`, `support@`, `team@`, and similar aliases; if no named human email is available, prefer a `contact_path` record instead.
|
|
22
23
|
|
|
23
24
|
## Procedure
|
|
24
25
|
|
package/CLAUDE.md
CHANGED
|
@@ -5,6 +5,7 @@ Agentic public-web lead discovery harness. It crawls company domains, extracts r
|
|
|
5
5
|
## Hard Limits
|
|
6
6
|
|
|
7
7
|
- [H1] Only collect public-source business contact data. Do not infer, guess, brute-force, or enrich private email addresses. Every lead needs a source URL and evidence excerpt. The primary goal is named people, not generic company inboxes.
|
|
8
|
+
- [H8] Apply coding-agent judgment to every email candidate. An email may pass only when public source evidence identifies one specific human owner of that email. Shared, role, departmental, generic, blocked, unknown-owner, or unnamed person-like emails must not be emitted as email leads.
|
|
8
9
|
- [H2] Respect robots.txt, terms, rate limits, and site intent. If a page blocks crawling or is not public, record a warning and move on.
|
|
9
10
|
- [H3] Never send outreach from this harness. The output is reviewable lead records and ingest-ready artifacts, not email campaigns.
|
|
10
11
|
- [H4] Use file-backed artifacts before ingest. A subagent prose summary is not enough; write validated JSON/JSONL first, then ingest.
|
|
@@ -18,7 +19,7 @@ Agentic public-web lead discovery harness. It crawls company domains, extracts r
|
|
|
18
19
|
- [D2] Use deterministic local helpers when they can validate, normalize, manifest, preflight, postflight, or ingest artifacts.
|
|
19
20
|
- [D3] Read the active mode file before executing. Mode files own output shapes and gates.
|
|
20
21
|
- [D4] Prefer official company websites and high-signal pages: home, contact, about, team, leadership, people, press, blog, careers, and legal pages.
|
|
21
|
-
- [D5] Keep artifacts source-backed. Every emitted lead should include `domain`, `sourceUrl`, `evidence`, `emailType`, `extractionMethod`, `verificationStatus`, and `confidence`. Reject generic catch-all inboxes such as `info@`, `hello@`, `contact@`, `support@`, `team@`, and similar aliases; if no named
|
|
22
|
+
- [D5] Keep artifacts source-backed. Every emitted lead should include `domain`, `sourceUrl`, `evidence`, `emailType`, `extractionMethod`, `verificationStatus`, and `confidence`. Reject generic catch-all inboxes such as `info@`, `hello@`, `contact@`, `support@`, `team@`, and similar aliases; if no named human email is available, prefer a `contact_path` record instead.
|
|
22
23
|
|
|
23
24
|
## Procedure
|
|
24
25
|
|
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ Agentic public-web lead discovery harness for portable, source-backed lead artif
|
|
|
12
12
|
- Ships OpenCode, Claude Code, Cursor, Codex, and Pi harness config from one `iso/` source.
|
|
13
13
|
|
|
14
14
|
The harness is optimized to find named people who can actually be contacted. When only a general company contact path exists, it should emit a `contact_path` record rather than a generic organizational inbox.
|
|
15
|
+
CLI summaries distinguish total reviewable records from `goodLeads`; only non-generic `person` email records with a named human owner visible in the evidence count as good leads. Role, shared, departmental, unknown-owner, blocked, unsupported, or unnamed person-like emails are rejected by validation.
|
|
15
16
|
|
|
16
17
|
## Core Commands
|
|
17
18
|
|
|
@@ -70,7 +71,7 @@ gh run watch
|
|
|
70
71
|
You can also publish by creating a GitHub release for the package version:
|
|
71
72
|
|
|
72
73
|
```bash
|
|
73
|
-
gh release create v0.1.
|
|
74
|
+
gh release create v0.1.6 --title v0.1.6 --generate-notes
|
|
74
75
|
```
|
|
75
76
|
|
|
76
77
|
The package version must not already exist on npm.
|
|
@@ -51,7 +51,7 @@ write('package.json', JSON.stringify({
|
|
|
51
51
|
'update-harness': 'npm install @agent-pattern-labs/leads-rig@latest && public-leads sync',
|
|
52
52
|
},
|
|
53
53
|
dependencies: {
|
|
54
|
-
'@agent-pattern-labs/leads-rig': '^0.1.
|
|
54
|
+
'@agent-pattern-labs/leads-rig': '^0.1.6',
|
|
55
55
|
},
|
|
56
56
|
engines: {
|
|
57
57
|
node: '>=20.6.0',
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -82,4 +82,4 @@ The package also carries the `@agent-pattern-labs/iso-*` helper ecosystem for tr
|
|
|
82
82
|
- page visits
|
|
83
83
|
- ingest requests
|
|
84
84
|
|
|
85
|
-
The validator intentionally accepts the same defaults the local runtime normalizes, but it fails missing source evidence, invalid URL fields, invalid `emailType`, invalid `verificationStatus`, invalid confidence, person
|
|
85
|
+
The validator intentionally accepts the same defaults the local runtime normalizes, but it fails missing source evidence, invalid URL fields, invalid `emailType`, invalid `verificationStatus`, invalid confidence, person leads without email, and generic catch-all inboxes such as `info@`, `hello@`, or similar organizational aliases. Any email record must be a named `person` lead with a non-generic email and a human owner visible in the evidence; role inboxes, unknown-owner emails, blocked emails, unsupported emails, and unnamed person-like emails fail validation. Summaries expose `goodLeadCount`, which only counts those high-quality named human email records.
|
package/docs/CONSTRUCTION.md
CHANGED
|
@@ -37,4 +37,4 @@
|
|
|
37
37
|
6. Run `npm audit`.
|
|
38
38
|
7. Run `npm pack` and confirm generated runtime surfaces are included.
|
|
39
39
|
8. Scaffold a clean consumer project and test install-time sync from the packed package or source tree.
|
|
40
|
-
9. Publish through GitHub Actions with `gh workflow run publish.yml --ref main`, or create a published release with `gh release create v0.1.
|
|
40
|
+
9. Publish through GitHub Actions with `gh workflow run publish.yml --ref main`, or create a published release with `gh release create v0.1.6 --title v0.1.6 --generate-notes`.
|
|
@@ -38,7 +38,9 @@ Skip this only if the task explicitly says to attach to an existing session.
|
|
|
38
38
|
## Do
|
|
39
39
|
|
|
40
40
|
- Crawl only public, high-signal company pages.
|
|
41
|
-
-
|
|
41
|
+
- Use your judgment on each email candidate: only emit an email lead when public evidence identifies one specific human owner of the address.
|
|
42
|
+
- Extract high-quality named human emails, contact forms, public names/titles, source URL, evidence, and warnings.
|
|
43
|
+
- If an address is shared, role-based, departmental, generic, blocked, or not tied to a visible named human, do not emit it as an email lead; emit a `contact_path` if useful.
|
|
42
44
|
- Emit JSON/JSONL matching `templates/lead-schema.json`.
|
|
43
45
|
- Run `npx public-leads validate --input <file>` before returning a result.
|
|
44
46
|
- Return file paths and terminal status, not a prose-only summary.
|
package/iso/instructions.md
CHANGED
|
@@ -5,6 +5,7 @@ Agentic public-web lead discovery harness. It crawls company domains, extracts r
|
|
|
5
5
|
## Hard Limits
|
|
6
6
|
|
|
7
7
|
- [H1] Only collect public-source business contact data. Do not infer, guess, brute-force, or enrich private email addresses. Every lead needs a source URL and evidence excerpt. The primary goal is named people, not generic company inboxes.
|
|
8
|
+
- [H8] Apply coding-agent judgment to every email candidate. An email may pass only when public source evidence identifies one specific human owner of that email. Shared, role, departmental, generic, blocked, unknown-owner, or unnamed person-like emails must not be emitted as email leads.
|
|
8
9
|
- [H2] Respect robots.txt, terms, rate limits, and site intent. If a page blocks crawling or is not public, record a warning and move on.
|
|
9
10
|
- [H3] Never send outreach from this harness. The output is reviewable lead records and ingest-ready artifacts, not email campaigns.
|
|
10
11
|
- [H4] Use file-backed artifacts before ingest. A subagent prose summary is not enough; write validated JSON/JSONL first, then ingest.
|
|
@@ -18,7 +19,7 @@ Agentic public-web lead discovery harness. It crawls company domains, extracts r
|
|
|
18
19
|
- [D2] Use deterministic local helpers when they can validate, normalize, manifest, preflight, postflight, or ingest artifacts.
|
|
19
20
|
- [D3] Read the active mode file before executing. Mode files own output shapes and gates.
|
|
20
21
|
- [D4] Prefer official company websites and high-signal pages: home, contact, about, team, leadership, people, press, blog, careers, and legal pages.
|
|
21
|
-
- [D5] Keep artifacts source-backed. Every emitted lead should include `domain`, `sourceUrl`, `evidence`, `emailType`, `extractionMethod`, `verificationStatus`, and `confidence`. Reject generic catch-all inboxes such as `info@`, `hello@`, `contact@`, `support@`, `team@`, and similar aliases; if no named
|
|
22
|
+
- [D5] Keep artifacts source-backed. Every emitted lead should include `domain`, `sourceUrl`, `evidence`, `emailType`, `extractionMethod`, `verificationStatus`, and `confidence`. Reject generic catch-all inboxes such as `info@`, `hello@`, `contact@`, `support@`, `team@`, and similar aliases; if no named human email is available, prefer a `contact_path` record instead.
|
|
22
23
|
|
|
23
24
|
## Procedure
|
|
24
25
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { lookup, resolveMx } from 'dns/promises';
|
|
3
3
|
import { setTimeout as sleep } from 'timers/promises';
|
|
4
|
-
import { cleanDomains, dedupeLeadRecords, isRejectedLeadEmail, mergeLeadRecords, normalizeDomain } from './leadharness-leads.mjs';
|
|
4
|
+
import { cleanDomains, dedupeLeadRecords, isGoodLead, isRejectedLeadEmail, mergeLeadRecords, normalizeDomain } from './leadharness-leads.mjs';
|
|
5
5
|
|
|
6
6
|
const DEFAULT_MAX_PAGES = 10;
|
|
7
7
|
const HARD_MAX_PAGES = 25;
|
|
@@ -575,6 +575,7 @@ function classifyEmailLocal(local) {
|
|
|
575
575
|
}
|
|
576
576
|
|
|
577
577
|
function isRelevantLead(lead, options) {
|
|
578
|
+
if (lead.email && !isGoodLead(lead)) return false;
|
|
578
579
|
if (!options.includeBlocked && lead.emailType === 'blocked') return false;
|
|
579
580
|
return lead.confidence >= options.minConfidence;
|
|
580
581
|
}
|
|
@@ -296,6 +296,9 @@ export function validateLead(lead, path, issues) {
|
|
|
296
296
|
if (lead.email && isRejectedLeadEmail(lead.email)) {
|
|
297
297
|
issues.push(issue('error', `${path}.email`, 'generic_inbox_rejected', 'generic company inboxes such as info@, hello@, or support@ are not accepted; submit named people instead'));
|
|
298
298
|
}
|
|
299
|
+
if (lead.email && !isRejectedLeadEmail(lead.email) && ALLOWED_EMAIL_TYPES.has(lead.emailType) && !isGoodLead(lead)) {
|
|
300
|
+
issues.push(issue('error', `${path}.emailType`, 'non_person_email_rejected', 'email records must identify one named human visible in the evidence; role, unknown, blocked, unsupported, or unnamed person emails are not accepted'));
|
|
301
|
+
}
|
|
299
302
|
if (lead.emailType && !ALLOWED_EMAIL_TYPES.has(lead.emailType)) {
|
|
300
303
|
issues.push(issue('error', `${path}.emailType`, 'invalid_email_type', `emailType must be one of ${[...ALLOWED_EMAIL_TYPES].join(', ')}`));
|
|
301
304
|
}
|
|
@@ -381,11 +384,13 @@ export function summarizePayload(payload) {
|
|
|
381
384
|
const byType = {};
|
|
382
385
|
let confidenceTotal = 0;
|
|
383
386
|
let withEmail = 0;
|
|
387
|
+
let goodLeadCount = 0;
|
|
384
388
|
const uniqueEmails = new Set();
|
|
385
389
|
const mxVerifiedEmails = new Set();
|
|
386
390
|
for (const lead of leads) {
|
|
387
391
|
byType[lead.emailType || 'unknown'] = (byType[lead.emailType || 'unknown'] || 0) + 1;
|
|
388
392
|
confidenceTotal += Number.isFinite(lead.confidence) ? lead.confidence : 0;
|
|
393
|
+
if (isGoodLead(lead)) goodLeadCount++;
|
|
389
394
|
if (lead.email) {
|
|
390
395
|
withEmail++;
|
|
391
396
|
uniqueEmails.add(String(lead.email).toLowerCase());
|
|
@@ -399,6 +404,8 @@ export function summarizePayload(payload) {
|
|
|
399
404
|
domainCount: domains.length,
|
|
400
405
|
leadCount: leads.length,
|
|
401
406
|
resultCount: results.length,
|
|
407
|
+
goodLeadCount,
|
|
408
|
+
reviewOnlyLeadCount: Math.max(0, leads.length - goodLeadCount),
|
|
402
409
|
withEmail,
|
|
403
410
|
uniqueEmailCount: uniqueEmails.size,
|
|
404
411
|
mxVerifiedEmailCount: mxVerifiedEmails.size,
|
|
@@ -417,6 +424,7 @@ export function createManifestRecord({ inputPath, payload, validation, generated
|
|
|
417
424
|
generatedAt,
|
|
418
425
|
domains: payload.domains,
|
|
419
426
|
leadCount: payload.leads.length,
|
|
427
|
+
goodLeadCount: validation.summary?.goodLeadCount || 0,
|
|
420
428
|
resultCount: payload.results.length,
|
|
421
429
|
errorCount: payload.errors.length,
|
|
422
430
|
validation: {
|
|
@@ -648,7 +656,10 @@ function stableLeadKey(lead) {
|
|
|
648
656
|
}
|
|
649
657
|
|
|
650
658
|
function leadQualityScore(lead) {
|
|
651
|
-
|
|
659
|
+
const goodLead = isGoodLead(lead);
|
|
660
|
+
return (goodLead ? 50_000 : 0)
|
|
661
|
+
+ (lead?.email && !goodLead ? -20_000 : 0)
|
|
662
|
+
+ (lead?.confidence || 0) * 1000
|
|
652
663
|
+ verificationRank(lead?.verificationStatus) * 100
|
|
653
664
|
+ pagePriority(lead?.sourceUrl || '') * 10
|
|
654
665
|
+ (Array.isArray(lead?.sources) ? lead.sources.length : 0) * 5
|
|
@@ -738,6 +749,30 @@ export function isRejectedLeadLocalPart(local) {
|
|
|
738
749
|
});
|
|
739
750
|
}
|
|
740
751
|
|
|
752
|
+
export function isGoodLead(lead) {
|
|
753
|
+
if (!lead || typeof lead !== 'object') return false;
|
|
754
|
+
if (stringValue(lead.emailType).toLowerCase() !== 'person') return false;
|
|
755
|
+
if (!lead.email || isRejectedLeadEmail(lead.email)) return false;
|
|
756
|
+
const contactName = stringValue(lead.contactName);
|
|
757
|
+
if (!contactName) return false;
|
|
758
|
+
if (!evidenceIdentifiesContact(lead, contactName)) return false;
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function evidenceIdentifiesContact(lead, contactName) {
|
|
763
|
+
const needle = compactComparable(contactName);
|
|
764
|
+
if (!needle) return false;
|
|
765
|
+
const evidence = [
|
|
766
|
+
lead.evidence,
|
|
767
|
+
...(Array.isArray(lead.sources) ? lead.sources.map((source) => source?.evidence) : []),
|
|
768
|
+
].map(compactComparable).join(' ');
|
|
769
|
+
return evidence.includes(needle);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function compactComparable(value) {
|
|
773
|
+
return stringValue(value).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
774
|
+
}
|
|
775
|
+
|
|
741
776
|
function normalizeRejectedLeadLocalPart(local) {
|
|
742
777
|
let value = String(local || '').trim().toLowerCase();
|
|
743
778
|
for (const noise of ['mailto', 'u003e', 'email']) {
|
package/modes/_shared.md
CHANGED
|
@@ -28,18 +28,21 @@ Required fields:
|
|
|
28
28
|
- Prioritize home, contact, about, team, leadership, people, press, blog, careers, and legal pages.
|
|
29
29
|
- Respect robots.txt, paywalls, login walls, and obvious anti-scraping notices.
|
|
30
30
|
- Do not infer email formats. Do not brute-force guessed addresses.
|
|
31
|
+
- Use coding-agent judgment for every email candidate. Only emit an email lead when the source evidence identifies one specific human behind that address.
|
|
31
32
|
- Prefer named people over organizational inboxes. Do not emit generic catch-all emails such as `info@`, `hello@`, `contact@`, `support@`, `team@`, or similar aliases. If only a general contact path exists, emit `contact_path` instead.
|
|
33
|
+
- Reject email records that are role-based, shared, departmental, unknown ownership, blocked operational inboxes, or person-like but missing a visible human name in the source evidence.
|
|
32
34
|
- Keep contact forms as `emailType: "contact_path"` with empty `email`.
|
|
33
35
|
- Mark operational no-contact inboxes such as `noreply`, `abuse`, `security`, and `legal` as `blocked` with confidence `0`.
|
|
34
36
|
|
|
35
37
|
## Quality Heuristics
|
|
36
38
|
|
|
37
|
-
- `
|
|
38
|
-
- `
|
|
39
|
+
- `goodLeadCount`: counts only `person` email records with a non-generic email and named `contactName` visible in the evidence. Generic, role, unknown, blocked, unnamed, unsupported, and `contact_path` records are not good leads.
|
|
40
|
+
- `person`: named human with source context tying that person to the email address.
|
|
41
|
+
- `role`: legacy/review-only classification. Do not emit role email records in new artifacts.
|
|
39
42
|
- `contact_path`: public form or page where no email is available.
|
|
40
43
|
- `blocked`: public address that should not be used for outbound.
|
|
41
44
|
|
|
42
|
-
Confidence should reflect source quality, person specificity, domain match, and verification. Low-confidence
|
|
45
|
+
Confidence should reflect source quality, person specificity, domain match, and verification. Low-confidence contact paths are allowed if they preserve warnings and evidence; email leads should be high-quality named humans only.
|
|
43
46
|
|
|
44
47
|
## Local Commands
|
|
45
48
|
|
package/package.json
CHANGED
package/scripts/crawl.mjs
CHANGED
|
@@ -57,7 +57,7 @@ async function main() {
|
|
|
57
57
|
console.log(JSON.stringify({ output: out, validation, summary: validation.summary }, null, 2));
|
|
58
58
|
} else {
|
|
59
59
|
console.log(`crawl: wrote ${relativeProjectPath(resolveProjectPath(out))}`);
|
|
60
|
-
console.log(`domains=${validation.summary.domainCount} leads=${validation.summary.leadCount} results=${validation.summary.resultCount} errors=${payload.errors.length}`);
|
|
60
|
+
console.log(`domains=${validation.summary.domainCount} leads=${validation.summary.leadCount} goodLeads=${validation.summary.goodLeadCount} results=${validation.summary.resultCount} errors=${payload.errors.length}`);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
if (!validation.ok) {
|
package/scripts/manifest.mjs
CHANGED
|
@@ -50,6 +50,7 @@ try {
|
|
|
50
50
|
batchCount: manifest.batches.length,
|
|
51
51
|
readyForIngest: manifest.batches.filter((item) => item.readyForIngest).length,
|
|
52
52
|
leadCount: manifest.batches.reduce((sum, item) => sum + item.leadCount, 0),
|
|
53
|
+
goodLeadCount: manifest.batches.reduce((sum, item) => sum + (item.goodLeadCount || 0), 0),
|
|
53
54
|
errorCount: manifest.batches.reduce((sum, item) => sum + item.validation.errors, 0),
|
|
54
55
|
warningCount: manifest.batches.reduce((sum, item) => sum + item.validation.warnings, 0),
|
|
55
56
|
};
|
|
@@ -60,8 +61,8 @@ try {
|
|
|
60
61
|
console.log(JSON.stringify({ manifest: relativeProjectPath(manifestPath), record, summary: manifest.summary }, null, 2));
|
|
61
62
|
} else {
|
|
62
63
|
console.log(`manifest: ${relativeProjectPath(manifestPath)}`);
|
|
63
|
-
console.log(`batch: ${record.id} leads=${record.leadCount} ready=${record.readyForIngest}`);
|
|
64
|
-
console.log(`summary: batches=${manifest.summary.batchCount} ready=${manifest.summary.readyForIngest} leads=${manifest.summary.leadCount}`);
|
|
64
|
+
console.log(`batch: ${record.id} leads=${record.leadCount} goodLeads=${record.goodLeadCount} ready=${record.readyForIngest}`);
|
|
65
|
+
console.log(`summary: batches=${manifest.summary.batchCount} ready=${manifest.summary.readyForIngest} leads=${manifest.summary.leadCount} goodLeads=${manifest.summary.goodLeadCount}`);
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
process.exit(validation.ok ? 0 : 1);
|
package/scripts/pipeline.mjs
CHANGED
|
@@ -66,7 +66,7 @@ try {
|
|
|
66
66
|
const manifest = upsertManifest(manifestPath, out, payload, validation);
|
|
67
67
|
console.log(`pipeline: wrote ${relativeProjectPath(resolveProjectPath(out))}`);
|
|
68
68
|
console.log(`manifest: ${relativeProjectPath(resolveProjectPath(manifestPath))}`);
|
|
69
|
-
console.log(`domains=${validation.summary.domainCount} leads=${validation.summary.leadCount} results=${validation.summary.resultCount} errors=${payload.errors.length}`);
|
|
69
|
+
console.log(`domains=${validation.summary.domainCount} leads=${validation.summary.leadCount} goodLeads=${validation.summary.goodLeadCount} results=${validation.summary.resultCount} errors=${payload.errors.length}`);
|
|
70
70
|
|
|
71
71
|
if (opts.ingest || opts.upload) {
|
|
72
72
|
const ingestResult = await ingestPayload(payload, {
|
|
@@ -110,6 +110,7 @@ function upsertManifest(manifestPath, inputPath, payload, validation) {
|
|
|
110
110
|
batchCount: manifest.batches.length,
|
|
111
111
|
readyForIngest: manifest.batches.filter((item) => item.readyForIngest).length,
|
|
112
112
|
leadCount: manifest.batches.reduce((sum, item) => sum + item.leadCount, 0),
|
|
113
|
+
goodLeadCount: manifest.batches.reduce((sum, item) => sum + (item.goodLeadCount || 0), 0),
|
|
113
114
|
errorCount: manifest.batches.reduce((sum, item) => sum + item.validation.errors, 0),
|
|
114
115
|
warningCount: manifest.batches.reduce((sum, item) => sum + item.validation.warnings, 0),
|
|
115
116
|
};
|
|
@@ -58,7 +58,7 @@ try {
|
|
|
58
58
|
function printReport(report) {
|
|
59
59
|
const rel = relativeProjectPath(report.input);
|
|
60
60
|
console.log(`${report.ok ? 'OK' : 'ERROR'} ${rel}`);
|
|
61
|
-
console.log(`domains=${report.summary.domainCount} leads=${report.summary.leadCount} results=${report.summary.resultCount} avgConfidence=${report.summary.averageConfidence}`);
|
|
61
|
+
console.log(`domains=${report.summary.domainCount} leads=${report.summary.leadCount} goodLeads=${report.summary.goodLeadCount} results=${report.summary.resultCount} avgConfidence=${report.summary.averageConfidence}`);
|
|
62
62
|
if (Object.keys(report.summary.byType).length > 0) {
|
|
63
63
|
console.log(`types=${Object.entries(report.summary.byType).map(([type, count]) => `${type}:${count}`).join(', ')}`);
|
|
64
64
|
}
|