@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.
@@ -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
- return (lead?.confidence || 0) * 1000
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
- - `person`: named person or person-like email with source context.
38
- - `role`: public role inbox only when it is clearly role-specific and not a generic catch-all alias.
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 records are allowed if they preserve warnings and evidence.
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. Each worker writes `batch/lead-results-{id}.json`.
29
- 4. Validate every artifact and update `data/lead-manifest.json`.
30
- 5. Run `npx public-leads verify`.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-pattern-labs/leads-rig",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Agentic public-web lead discovery harness with portable ingest artifacts",
5
5
  "type": "module",
6
6
  "author": "Razroo",
@@ -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
- timeoutMs: opts.timeoutMs,
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) {
@@ -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);
@@ -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
- timeoutMs: opts.timeoutMs,
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
  }