@agent-pattern-labs/leads-rig 0.1.5 → 0.1.7
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/batch/README.md +2 -1
- package/bin/create-leads-harness.mjs +1 -1
- package/config/profile.example.yml +13 -0
- 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 +490 -199
- package/lib/leadharness-leads.mjs +47 -1
- package/modes/_shared.md +6 -3
- package/modes/batch.md +4 -3
- package/modes/crawl.md +1 -1
- package/modes/pipeline.md +1 -1
- package/package.json +1 -1
- package/scripts/batch-orchestrator.mjs +10 -2
- package/scripts/crawl.mjs +17 -3
- package/scripts/manifest.mjs +3 -2
- package/scripts/pipeline.mjs +18 -3
- package/scripts/validate-leads.mjs +1 -1
|
@@ -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: {
|
|
@@ -463,7 +471,18 @@ export function loadProfileConfig(root = PROJECT_DIR) {
|
|
|
463
471
|
adminTokenEnv: yamlScalar(text, 'admin_token_env'),
|
|
464
472
|
maxPages: yamlScalar(text, 'max_pages'),
|
|
465
473
|
maxDomainsPerBatch: yamlScalar(text, 'max_domains_per_batch'),
|
|
474
|
+
timeoutMs: yamlScalar(text, 'timeout_ms'),
|
|
466
475
|
concurrency: yamlScalar(text, 'concurrency'),
|
|
476
|
+
pageConcurrency: yamlScalar(text, 'page_concurrency'),
|
|
477
|
+
dnsConcurrency: yamlScalar(text, 'dns_concurrency'),
|
|
478
|
+
cachePath: yamlScalar(text, 'cache_path'),
|
|
479
|
+
robotsCacheTtlMs: yamlScalar(text, 'robots_cache_ttl_ms'),
|
|
480
|
+
dnsCacheTtlMs: yamlScalar(text, 'dns_cache_ttl_ms'),
|
|
481
|
+
pageCache: yamlScalar(text, 'page_cache'),
|
|
482
|
+
pageCacheTtlMs: yamlScalar(text, 'page_cache_ttl_ms'),
|
|
483
|
+
stopAfterGoodLeads: yamlScalar(text, 'stop_after_good_leads'),
|
|
484
|
+
stopAfterContactPath: yamlScalar(text, 'stop_after_contact_path'),
|
|
485
|
+
batchTimeoutMs: yamlScalar(text, 'batch_timeout_ms'),
|
|
467
486
|
userAgent: yamlScalar(text, 'user_agent'),
|
|
468
487
|
minConfidence: yamlScalar(text, 'min_confidence'),
|
|
469
488
|
};
|
|
@@ -648,7 +667,10 @@ function stableLeadKey(lead) {
|
|
|
648
667
|
}
|
|
649
668
|
|
|
650
669
|
function leadQualityScore(lead) {
|
|
651
|
-
|
|
670
|
+
const goodLead = isGoodLead(lead);
|
|
671
|
+
return (goodLead ? 50_000 : 0)
|
|
672
|
+
+ (lead?.email && !goodLead ? -20_000 : 0)
|
|
673
|
+
+ (lead?.confidence || 0) * 1000
|
|
652
674
|
+ verificationRank(lead?.verificationStatus) * 100
|
|
653
675
|
+ pagePriority(lead?.sourceUrl || '') * 10
|
|
654
676
|
+ (Array.isArray(lead?.sources) ? lead.sources.length : 0) * 5
|
|
@@ -738,6 +760,30 @@ export function isRejectedLeadLocalPart(local) {
|
|
|
738
760
|
});
|
|
739
761
|
}
|
|
740
762
|
|
|
763
|
+
export function isGoodLead(lead) {
|
|
764
|
+
if (!lead || typeof lead !== 'object') return false;
|
|
765
|
+
if (stringValue(lead.emailType).toLowerCase() !== 'person') return false;
|
|
766
|
+
if (!lead.email || isRejectedLeadEmail(lead.email)) return false;
|
|
767
|
+
const contactName = stringValue(lead.contactName);
|
|
768
|
+
if (!contactName) return false;
|
|
769
|
+
if (!evidenceIdentifiesContact(lead, contactName)) return false;
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function evidenceIdentifiesContact(lead, contactName) {
|
|
774
|
+
const needle = compactComparable(contactName);
|
|
775
|
+
if (!needle) return false;
|
|
776
|
+
const evidence = [
|
|
777
|
+
lead.evidence,
|
|
778
|
+
...(Array.isArray(lead.sources) ? lead.sources.map((source) => source?.evidence) : []),
|
|
779
|
+
].map(compactComparable).join(' ');
|
|
780
|
+
return evidence.includes(needle);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function compactComparable(value) {
|
|
784
|
+
return stringValue(value).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
785
|
+
}
|
|
786
|
+
|
|
741
787
|
function normalizeRejectedLeadLocalPart(local) {
|
|
742
788
|
let value = String(local || '').trim().toLowerCase();
|
|
743
789
|
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/modes/batch.md
CHANGED
|
@@ -25,9 +25,10 @@ id domain company notes
|
|
|
25
25
|
|
|
26
26
|
1. Run `batch/batch-runner.sh --dry-run` first unless the user explicitly asks to start immediately.
|
|
27
27
|
2. Use `--parallel 2` by default. Do not exceed 2 browser-heavy workers per round.
|
|
28
|
-
3.
|
|
29
|
-
4.
|
|
30
|
-
5.
|
|
28
|
+
3. For broad queues, keep the batch deterministic crawler timeout near the default `--timeout-ms 8000` unless target quality is known to be high.
|
|
29
|
+
4. Each worker writes `batch/lead-results-{id}.json`.
|
|
30
|
+
5. Validate every artifact and update `data/lead-manifest.json`.
|
|
31
|
+
6. Run `npx public-leads verify`.
|
|
31
32
|
|
|
32
33
|
## Output
|
|
33
34
|
|
package/modes/crawl.md
CHANGED
|
@@ -7,7 +7,7 @@ Use this for one or more assigned domains.
|
|
|
7
7
|
1. Normalize the domain to lowercase without protocol or `www.`.
|
|
8
8
|
2. Visit only bounded public pages from the official site.
|
|
9
9
|
3. Prefer the deterministic crawler when no browser-only blocker exists:
|
|
10
|
-
`npx public-leads crawl --input data/domains.tsv --out data/lead-results.json`
|
|
10
|
+
`npx public-leads crawl --input data/domains.tsv --out data/lead-results.json --concurrency 2 --page-concurrency 1`
|
|
11
11
|
4. Extract public email addresses, person names/titles when visible nearby, contact forms, source URLs, page titles, evidence snippets, and warnings.
|
|
12
12
|
5. Emit a JSON payload matching `templates/lead-schema.json`.
|
|
13
13
|
6. Save the payload to `data/lead-results.json` for single-domain runs or `batch/lead-results-{id}.json` for batch workers.
|
package/modes/pipeline.md
CHANGED
|
@@ -15,7 +15,7 @@ Read domains from the first available source:
|
|
|
15
15
|
1. Build a deduped candidate list.
|
|
16
16
|
2. Drop domains already represented in the manifest unless the user asks to retry.
|
|
17
17
|
3. Prefer the deterministic end-to-end command for normal public pages:
|
|
18
|
-
`npx public-leads pipeline --input data/domains.tsv --out data/lead-results.json`
|
|
18
|
+
`npx public-leads pipeline --input data/domains.tsv --out data/lead-results.json --concurrency 2 --page-concurrency 1`
|
|
19
19
|
4. Use browser/MCP workers only when the deterministic crawler cannot reach a public page or the site requires rendering.
|
|
20
20
|
5. For browser-heavy batch work, use `batch/batch-runner.sh --parallel 2`.
|
|
21
21
|
6. Validate every artifact with `npx public-leads validate`.
|
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@ import { mkdir, readFile, rm, writeFile } from 'fs/promises';
|
|
|
7
7
|
import { dirname, join, resolve } from 'path';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import { runWorkflow } from '@agent-pattern-labs/iso-orchestrator';
|
|
10
|
+
import { loadProfileConfig } from '../lib/leadharness-leads.mjs';
|
|
10
11
|
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const PKG_ROOT = resolve(__dirname, '..');
|
|
@@ -37,6 +38,7 @@ Options:
|
|
|
37
38
|
--retry-failed Only retry rows marked failed
|
|
38
39
|
--start-from N Start from numeric id N
|
|
39
40
|
--max-retries N Max failed retries per domain (default: 2)
|
|
41
|
+
--timeout-ms N Deterministic crawler page timeout in worker prompt (default: 8000)
|
|
40
42
|
--workflow-id ID Durable workflow id (default: public-leads-batch)
|
|
41
43
|
-h, --help Show help
|
|
42
44
|
|
|
@@ -113,6 +115,7 @@ async function main(opts) {
|
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
function parseArgs(argv) {
|
|
118
|
+
const profile = loadProfileConfig();
|
|
116
119
|
const opts = {
|
|
117
120
|
runner: process.env.PUBLIC_LEADS_BATCH_RUNNER || process.env.LEAD_HARNESS_BATCH_RUNNER || 'opencode',
|
|
118
121
|
parallel: 1,
|
|
@@ -121,6 +124,7 @@ function parseArgs(argv) {
|
|
|
121
124
|
retryFailed: false,
|
|
122
125
|
startFrom: 0,
|
|
123
126
|
maxRetries: 2,
|
|
127
|
+
timeoutMs: positiveInt(process.env.PUBLIC_LEADS_BATCH_TIMEOUT_MS || process.env.LEAD_HARNESS_BATCH_TIMEOUT_MS || profile.batchTimeoutMs || 8_000, 'batch timeout'),
|
|
124
128
|
workflowId: 'public-leads-batch',
|
|
125
129
|
};
|
|
126
130
|
|
|
@@ -138,6 +142,7 @@ function parseArgs(argv) {
|
|
|
138
142
|
else if (arg === '--retry-failed') opts.retryFailed = true;
|
|
139
143
|
else if (arg === '--start-from') opts.startFrom = nonNegativeInt(next(), '--start-from');
|
|
140
144
|
else if (arg === '--max-retries') opts.maxRetries = positiveInt(next(), '--max-retries');
|
|
145
|
+
else if (arg === '--timeout-ms') opts.timeoutMs = positiveInt(next(), '--timeout-ms');
|
|
141
146
|
else if (arg === '--workflow-id') opts.workflowId = sanitizeWorkflowId(next());
|
|
142
147
|
else if (arg === '-h' || arg === '--help') opts.help = true;
|
|
143
148
|
else throw new Error(`unknown option: ${arg}`);
|
|
@@ -299,7 +304,7 @@ async function processDomain(workflow, item, opts) {
|
|
|
299
304
|
retries,
|
|
300
305
|
});
|
|
301
306
|
|
|
302
|
-
const prompt = buildWorkerPrompt(item, artifact);
|
|
307
|
+
const prompt = buildWorkerPrompt(item, artifact, opts);
|
|
303
308
|
const run = await withWorkerLiveness(workflow, item, logFile, () => runWorker(opts, prompt, logFile));
|
|
304
309
|
const statuses = parseStatusLines(run.output);
|
|
305
310
|
const status = statuses.get(item.id);
|
|
@@ -356,12 +361,15 @@ async function processDomain(workflow, item, opts) {
|
|
|
356
361
|
return { id: item.id, status: 'completed', artifact: status.artifact || artifact, leadCount: status.leadCount ?? 0 };
|
|
357
362
|
}
|
|
358
363
|
|
|
359
|
-
function buildWorkerPrompt(item, artifact) {
|
|
364
|
+
function buildWorkerPrompt(item, artifact, opts) {
|
|
360
365
|
return `Process this assigned public-leads domain.
|
|
361
366
|
|
|
362
367
|
Assignment:
|
|
363
368
|
${JSON.stringify({ ...item, artifact }, null, 2)}
|
|
364
369
|
|
|
370
|
+
Prefer the deterministic crawler first:
|
|
371
|
+
npx public-leads crawl --domain ${item.domain} --out ${artifact} --timeout-ms ${opts.timeoutMs} --stop-after-good-leads 1 --stop-after-contact-path
|
|
372
|
+
|
|
365
373
|
Write the artifact exactly to ${artifact}. Validate it with:
|
|
366
374
|
npx public-leads validate --input ${artifact}
|
|
367
375
|
|
package/scripts/crawl.mjs
CHANGED
|
@@ -17,7 +17,10 @@ const USAGE = `public-leads crawl -- crawl public company pages into a lead arti
|
|
|
17
17
|
Usage:
|
|
18
18
|
public-leads crawl [--domain example.com | --domains example.com,example.org]
|
|
19
19
|
[--input data/domains.tsv] [--out data/lead-results.json]
|
|
20
|
-
[--max-pages 10] [--min-confidence 30]
|
|
20
|
+
[--max-pages 10] [--timeout-ms 14000] [--min-confidence 30]
|
|
21
|
+
[--concurrency 2] [--page-concurrency 1] [--dns-concurrency 8]
|
|
22
|
+
[--stop-after-good-leads 1] [--stop-after-contact-path]
|
|
23
|
+
[--cache-path .leadharness-cache/crawler-cache.json] [--page-cache] [--page-cache-ttl-ms 3600000] [--no-cache]
|
|
21
24
|
[--include-blocked] [--allow-empty] [--json]
|
|
22
25
|
|
|
23
26
|
When no domain or input is supplied, the first existing file is used:
|
|
@@ -44,10 +47,21 @@ async function main() {
|
|
|
44
47
|
const payload = await crawlDomains(domains, {
|
|
45
48
|
maxPages: opts.maxPages || profile.maxPages,
|
|
46
49
|
minConfidence: opts.minConfidence || profile.minConfidence,
|
|
50
|
+
timeoutMs: opts.timeoutMs || profile.timeoutMs,
|
|
51
|
+
concurrency: opts.concurrency || profile.concurrency,
|
|
52
|
+
pageConcurrency: opts.pageConcurrency || profile.pageConcurrency,
|
|
53
|
+
dnsConcurrency: opts.dnsConcurrency || profile.dnsConcurrency,
|
|
47
54
|
userAgent: opts.userAgent || profile.userAgent,
|
|
48
55
|
includeBlocked: Boolean(opts.includeBlocked),
|
|
49
56
|
delayMs: opts.delayMs,
|
|
50
|
-
|
|
57
|
+
cachePath: opts.cachePath || profile.cachePath,
|
|
58
|
+
noCache: Boolean(opts.noCache),
|
|
59
|
+
robotsCacheTtlMs: opts.robotsCacheTtlMs || profile.robotsCacheTtlMs,
|
|
60
|
+
dnsCacheTtlMs: opts.dnsCacheTtlMs || profile.dnsCacheTtlMs,
|
|
61
|
+
pageCache: opts.pageCache || profile.pageCache,
|
|
62
|
+
pageCacheTtlMs: opts.pageCacheTtlMs || profile.pageCacheTtlMs,
|
|
63
|
+
stopAfterGoodLeads: opts.stopAfterGoodLeads || profile.stopAfterGoodLeads,
|
|
64
|
+
stopAfterContactPath: opts.stopAfterContactPath || profile.stopAfterContactPath,
|
|
51
65
|
jobId: opts.jobId,
|
|
52
66
|
});
|
|
53
67
|
writeJson(resolveProjectPath(out), payload);
|
|
@@ -57,7 +71,7 @@ async function main() {
|
|
|
57
71
|
console.log(JSON.stringify({ output: out, validation, summary: validation.summary }, null, 2));
|
|
58
72
|
} else {
|
|
59
73
|
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}`);
|
|
74
|
+
console.log(`domains=${validation.summary.domainCount} leads=${validation.summary.leadCount} goodLeads=${validation.summary.goodLeadCount} results=${validation.summary.resultCount} errors=${payload.errors.length}`);
|
|
61
75
|
}
|
|
62
76
|
|
|
63
77
|
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
|
@@ -21,7 +21,10 @@ Usage:
|
|
|
21
21
|
public-leads pipeline [--domain example.com | --domains example.com,example.org]
|
|
22
22
|
[--input data/domains.tsv] [--out data/lead-results.json]
|
|
23
23
|
[--manifest data/lead-manifest.json]
|
|
24
|
-
[--max-pages 10] [--min-confidence 30]
|
|
24
|
+
[--max-pages 10] [--timeout-ms 14000] [--min-confidence 30]
|
|
25
|
+
[--concurrency 2] [--page-concurrency 1] [--dns-concurrency 8]
|
|
26
|
+
[--stop-after-good-leads 1] [--stop-after-contact-path]
|
|
27
|
+
[--cache-path .leadharness-cache/crawler-cache.json] [--page-cache] [--page-cache-ttl-ms 3600000] [--no-cache]
|
|
25
28
|
[--ingest | --upload] [--dry-run]
|
|
26
29
|
[--api https://cold-agent-leads.example.com]
|
|
27
30
|
|
|
@@ -47,10 +50,21 @@ try {
|
|
|
47
50
|
const payload = await crawlDomains(domains, {
|
|
48
51
|
maxPages: opts.maxPages || profile.maxPages,
|
|
49
52
|
minConfidence: opts.minConfidence || profile.minConfidence,
|
|
53
|
+
timeoutMs: opts.timeoutMs || profile.timeoutMs,
|
|
54
|
+
concurrency: opts.concurrency || profile.concurrency,
|
|
55
|
+
pageConcurrency: opts.pageConcurrency || profile.pageConcurrency,
|
|
56
|
+
dnsConcurrency: opts.dnsConcurrency || profile.dnsConcurrency,
|
|
50
57
|
userAgent: opts.userAgent || profile.userAgent,
|
|
51
58
|
includeBlocked: Boolean(opts.includeBlocked),
|
|
52
59
|
delayMs: opts.delayMs,
|
|
53
|
-
|
|
60
|
+
cachePath: opts.cachePath || profile.cachePath,
|
|
61
|
+
noCache: Boolean(opts.noCache),
|
|
62
|
+
robotsCacheTtlMs: opts.robotsCacheTtlMs || profile.robotsCacheTtlMs,
|
|
63
|
+
dnsCacheTtlMs: opts.dnsCacheTtlMs || profile.dnsCacheTtlMs,
|
|
64
|
+
pageCache: opts.pageCache || profile.pageCache,
|
|
65
|
+
pageCacheTtlMs: opts.pageCacheTtlMs || profile.pageCacheTtlMs,
|
|
66
|
+
stopAfterGoodLeads: opts.stopAfterGoodLeads || profile.stopAfterGoodLeads,
|
|
67
|
+
stopAfterContactPath: opts.stopAfterContactPath || profile.stopAfterContactPath,
|
|
54
68
|
jobId: opts.jobId,
|
|
55
69
|
});
|
|
56
70
|
writeJson(resolveProjectPath(out), payload);
|
|
@@ -66,7 +80,7 @@ try {
|
|
|
66
80
|
const manifest = upsertManifest(manifestPath, out, payload, validation);
|
|
67
81
|
console.log(`pipeline: wrote ${relativeProjectPath(resolveProjectPath(out))}`);
|
|
68
82
|
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}`);
|
|
83
|
+
console.log(`domains=${validation.summary.domainCount} leads=${validation.summary.leadCount} goodLeads=${validation.summary.goodLeadCount} results=${validation.summary.resultCount} errors=${payload.errors.length}`);
|
|
70
84
|
|
|
71
85
|
if (opts.ingest || opts.upload) {
|
|
72
86
|
const ingestResult = await ingestPayload(payload, {
|
|
@@ -110,6 +124,7 @@ function upsertManifest(manifestPath, inputPath, payload, validation) {
|
|
|
110
124
|
batchCount: manifest.batches.length,
|
|
111
125
|
readyForIngest: manifest.batches.filter((item) => item.readyForIngest).length,
|
|
112
126
|
leadCount: manifest.batches.reduce((sum, item) => sum + item.leadCount, 0),
|
|
127
|
+
goodLeadCount: manifest.batches.reduce((sum, item) => sum + (item.goodLeadCount || 0), 0),
|
|
113
128
|
errorCount: manifest.batches.reduce((sum, item) => sum + item.validation.errors, 0),
|
|
114
129
|
warningCount: manifest.batches.reduce((sum, item) => sum + item.validation.warnings, 0),
|
|
115
130
|
};
|
|
@@ -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
|
}
|