@agent-pattern-labs/leads-rig 0.1.3

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.
Files changed (87) hide show
  1. package/.claude/agents/general-free.md +39 -0
  2. package/.claude/agents/general-paid.md +20 -0
  3. package/.claude/agents/glm-minimal.md +9 -0
  4. package/.claude/iso-route.resolved.json +21 -0
  5. package/.claude/settings.json +3 -0
  6. package/.codex/config.toml +24 -0
  7. package/.cursor/iso-route.md +17 -0
  8. package/.cursor/mcp.json +19 -0
  9. package/.cursor/rules/agent-general-free.mdc +38 -0
  10. package/.cursor/rules/agent-general-paid.mdc +19 -0
  11. package/.cursor/rules/agent-glm-minimal.mdc +8 -0
  12. package/.cursor/rules/main.mdc +61 -0
  13. package/.mcp.json +19 -0
  14. package/.opencode/agents/general-free.md +48 -0
  15. package/.opencode/agents/general-paid.md +29 -0
  16. package/.opencode/agents/glm-minimal.md +19 -0
  17. package/.opencode/instructions.md +3 -0
  18. package/.opencode/skills/lead-harness.md +56 -0
  19. package/.opencode/skills/public-leads.md +56 -0
  20. package/.pi/prompts/lead-harness.md +54 -0
  21. package/.pi/prompts/public-leads.md +54 -0
  22. package/.pi/skills/general-free/SKILL.md +38 -0
  23. package/.pi/skills/general-paid/SKILL.md +19 -0
  24. package/.pi/skills/glm-minimal/SKILL.md +8 -0
  25. package/AGENTS.md +56 -0
  26. package/CLAUDE.md +56 -0
  27. package/LICENSE +21 -0
  28. package/README.md +61 -0
  29. package/batch/README.md +37 -0
  30. package/batch/batch-prompt.md +19 -0
  31. package/batch/batch-runner.sh +18 -0
  32. package/bin/create-leads-harness.mjs +178 -0
  33. package/bin/lead-harness.mjs +201 -0
  34. package/bin/sync.mjs +150 -0
  35. package/config/profile.example.yml +34 -0
  36. package/docs/ARCHITECTURE.md +85 -0
  37. package/docs/CONSTRUCTION.md +40 -0
  38. package/docs/README.md +5 -0
  39. package/docs/SETUP.md +89 -0
  40. package/examples/README.md +9 -0
  41. package/examples/sample-leads.json +57 -0
  42. package/iso/agents/general-free.md +50 -0
  43. package/iso/agents/general-paid.md +31 -0
  44. package/iso/agents/glm-minimal.md +21 -0
  45. package/iso/commands/lead-harness.md +59 -0
  46. package/iso/commands/public-leads.md +59 -0
  47. package/iso/config.json +11 -0
  48. package/iso/instructions.md +56 -0
  49. package/iso/instructions.opencode.md +3 -0
  50. package/iso/mcp.json +16 -0
  51. package/lib/leadharness-crawler.mjs +911 -0
  52. package/lib/leadharness-ingest.mjs +157 -0
  53. package/lib/leadharness-leads.mjs +574 -0
  54. package/models.yaml +32 -0
  55. package/modes/_shared.md +50 -0
  56. package/modes/batch.md +34 -0
  57. package/modes/crawl.md +25 -0
  58. package/modes/ingest.md +33 -0
  59. package/modes/pipeline.md +27 -0
  60. package/modes/reference-local-helpers.md +13 -0
  61. package/modes/review.md +15 -0
  62. package/modes/setup.md +25 -0
  63. package/opencode.json +43 -0
  64. package/package.json +186 -0
  65. package/scripts/batch-orchestrator.mjs +558 -0
  66. package/scripts/crawl.mjs +129 -0
  67. package/scripts/ingest.mjs +48 -0
  68. package/scripts/manifest.mjs +71 -0
  69. package/scripts/pipeline.mjs +118 -0
  70. package/scripts/validate-leads.mjs +69 -0
  71. package/templates/canon.json +26 -0
  72. package/templates/capabilities.json +23 -0
  73. package/templates/context.json +20 -0
  74. package/templates/contracts.json +72 -0
  75. package/templates/facts.json +13 -0
  76. package/templates/index.json +13 -0
  77. package/templates/lead-schema.json +143 -0
  78. package/templates/lineage.json +10 -0
  79. package/templates/migrations.json +4 -0
  80. package/templates/postflight.json +11 -0
  81. package/templates/preflight.json +19 -0
  82. package/templates/prioritize.json +10 -0
  83. package/templates/redact.json +15 -0
  84. package/templates/score.json +18 -0
  85. package/templates/states.yml +25 -0
  86. package/templates/timeline.json +10 -0
  87. package/verify-pipeline.mjs +123 -0
@@ -0,0 +1,574 @@
1
+ import { createHash } from 'crypto';
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ readdirSync,
7
+ statSync,
8
+ writeFileSync,
9
+ } from 'fs';
10
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'path';
11
+
12
+ export const PROJECT_DIR = process.env.PUBLIC_LEADS_PROJECT || process.env.LEAD_HARNESS_PROJECT || process.cwd();
13
+ export const HARNESS_ROOT = process.env.PUBLIC_LEADS_ROOT || process.env.LEAD_HARNESS_ROOT || resolve(dirname(new URL(import.meta.url).pathname), '..');
14
+
15
+ export const DEFAULT_ARTIFACT_GLOBS = [
16
+ 'data/lead-results.json',
17
+ 'data/lead-results.jsonl',
18
+ 'batch/lead-results.json',
19
+ 'batch/lead-results.jsonl',
20
+ ];
21
+
22
+ export const ALLOWED_EMAIL_TYPES = new Set(['person', 'role', 'blocked', 'contact_path', 'unknown']);
23
+ export const ALLOWED_VERIFICATION_STATUSES = new Set([
24
+ 'verified',
25
+ 'mx_verified',
26
+ 'unverified',
27
+ 'not_applicable',
28
+ 'blocked',
29
+ 'unknown',
30
+ ]);
31
+
32
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
33
+
34
+ export function resolveProjectPath(input, root = PROJECT_DIR) {
35
+ return isAbsolute(input) ? input : resolve(root, input);
36
+ }
37
+
38
+ export function relativeProjectPath(input, root = PROJECT_DIR) {
39
+ return relative(root, input) || '.';
40
+ }
41
+
42
+ export function readJson(path) {
43
+ return JSON.parse(readFileSync(path, 'utf8'));
44
+ }
45
+
46
+ export function writeJson(path, value) {
47
+ const parent = dirname(path);
48
+ if (!existsSync(parent)) mkdirSync(parent, { recursive: true });
49
+ writeFileSync(path, JSON.stringify(value, null, 2) + '\n', 'utf8');
50
+ }
51
+
52
+ export function parseArgs(args) {
53
+ const opts = { _: [] };
54
+ for (let i = 0; i < args.length; i++) {
55
+ const arg = args[i];
56
+ if (arg === '--help' || arg === '-h') {
57
+ opts.help = true;
58
+ } else if (arg.startsWith('--')) {
59
+ const [flag, inline] = arg.split('=', 2);
60
+ const key = flag.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
61
+ if (inline !== undefined) {
62
+ opts[key] = inline;
63
+ } else if (args[i + 1] && !args[i + 1].startsWith('--')) {
64
+ opts[key] = args[++i];
65
+ } else {
66
+ opts[key] = true;
67
+ }
68
+ } else {
69
+ opts._.push(arg);
70
+ }
71
+ }
72
+ return opts;
73
+ }
74
+
75
+ export function readLeadArtifact(path, { now = new Date().toISOString() } = {}) {
76
+ const abs = resolveProjectPath(path);
77
+ if (!existsSync(abs)) throw new Error(`input not found: ${path}`);
78
+ const raw = readFileSync(abs, 'utf8');
79
+ const value = extname(abs).toLowerCase() === '.jsonl'
80
+ ? parseJsonl(raw, abs)
81
+ : JSON.parse(raw);
82
+ return normalizePayload(value, { now });
83
+ }
84
+
85
+ export function normalizePayload(value, { now = new Date().toISOString(), sourcePath = '' } = {}) {
86
+ let payload;
87
+ if (Array.isArray(value)) {
88
+ payload = { leads: value };
89
+ } else if (value && typeof value === 'object' && Array.isArray(value.leads)) {
90
+ payload = { ...value };
91
+ } else if (value && typeof value === 'object' && value.lead && typeof value.lead === 'object') {
92
+ payload = { leads: [value.lead] };
93
+ } else {
94
+ throw new Error('lead artifact must be a JSON array, JSONL records, or an object with a leads array');
95
+ }
96
+
97
+ const leads = payload.leads.map((lead) => normalizeLead(lead, { now }));
98
+ const results = Array.isArray(payload.results)
99
+ ? payload.results.map((result) => normalizeResult(result, { now }))
100
+ : [];
101
+ const domains = cleanDomains([
102
+ ...(Array.isArray(payload.domains) ? payload.domains : []),
103
+ ...results.map((result) => result.domain),
104
+ ...leads.map((lead) => lead.domain),
105
+ ]);
106
+ const errors = Array.isArray(payload.errors)
107
+ ? payload.errors.map((error) => String(error)).filter(Boolean)
108
+ : [];
109
+
110
+ return {
111
+ ...(payload.jobId ? { jobId: String(payload.jobId).trim() } : {}),
112
+ domains,
113
+ leads,
114
+ results,
115
+ errors,
116
+ ...(sourcePath ? { sourcePath } : {}),
117
+ };
118
+ }
119
+
120
+ export function normalizeLead(input, { now = new Date().toISOString() } = {}) {
121
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
122
+ throw new Error('lead must be an object');
123
+ }
124
+
125
+ const domain = normalizeDomain(input.domain || domainFromUrl(input.websiteUrl || input.sourceUrl || '') || '');
126
+ const email = stringValue(input.email).toLowerCase();
127
+ const sourceUrl = stringValue(input.sourceUrl);
128
+ const contactName = stringValue(input.contactName);
129
+ const title = stringValue(input.title);
130
+ const emailType = normalizeEmailType(input.emailType, email);
131
+ const verificationStatus = normalizeVerificationStatus(input.verificationStatus, emailType);
132
+ const confidence = clampInteger(input.confidence, 0, 100, 0);
133
+ const foundAt = normalizeDateTime(input.foundAt, now);
134
+ const phone = stringValue(input.phone);
135
+ const socialUrls = normalizeUrlList(input.socialUrls);
136
+ const contactUrls = normalizeUrlList(input.contactUrls);
137
+
138
+ const lead = {
139
+ id: stringValue(input.id),
140
+ company: stringValue(input.company) || displayCompany(domain),
141
+ domain,
142
+ websiteUrl: stringValue(input.websiteUrl) || (domain ? `https://${domain}/` : ''),
143
+ contactName,
144
+ title,
145
+ email,
146
+ emailType,
147
+ sourceUrl,
148
+ sourceLabel: stringValue(input.sourceLabel) || sourceUrl,
149
+ evidence: compactWhitespace(stringValue(input.evidence)),
150
+ extractionMethod: stringValue(input.extractionMethod) || 'agentic_harness',
151
+ verificationStatus,
152
+ confidence,
153
+ warnings: normalizeWarnings(input.warnings),
154
+ phone,
155
+ socialUrls,
156
+ contactUrls,
157
+ foundAt,
158
+ };
159
+
160
+ if (!lead.id) lead.id = stableLeadID(lead);
161
+ return lead;
162
+ }
163
+
164
+ export function normalizeResult(input, { now = new Date().toISOString() } = {}) {
165
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
166
+ throw new Error('result must be an object');
167
+ }
168
+ const domain = normalizeDomain(input.domain || domainFromUrl(input.websiteUrl || ''));
169
+ return {
170
+ domain,
171
+ websiteUrl: stringValue(input.websiteUrl) || (domain ? `https://${domain}/` : ''),
172
+ leads: Array.isArray(input.leads) ? input.leads.map((lead) => normalizeLead(lead, { now })) : [],
173
+ pages: Array.isArray(input.pages) ? input.pages.map(normalizePageVisit) : [],
174
+ warnings: normalizeWarnings(input.warnings),
175
+ completedAt: normalizeDateTime(input.completedAt, now),
176
+ };
177
+ }
178
+
179
+ export function normalizePageVisit(input) {
180
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
181
+ return { url: '', title: '', statusCode: 0, emailsFound: 0 };
182
+ }
183
+ const page = {
184
+ url: stringValue(input.url),
185
+ title: stringValue(input.title),
186
+ statusCode: clampInteger(input.statusCode, 0, 999, 0),
187
+ emailsFound: clampInteger(input.emailsFound, 0, 100000, 0),
188
+ phonesFound: clampInteger(input.phonesFound, 0, 100000, 0),
189
+ socialUrlsFound: clampInteger(input.socialUrlsFound, 0, 100000, 0),
190
+ contactUrlsFound: clampInteger(input.contactUrlsFound, 0, 100000, 0),
191
+ };
192
+ if (input.error) page.error = stringValue(input.error);
193
+ return page;
194
+ }
195
+
196
+ export function validatePayload(payload, { allowEmpty = false } = {}) {
197
+ const issues = [];
198
+ const leads = Array.isArray(payload?.leads) ? payload.leads : [];
199
+ const results = Array.isArray(payload?.results) ? payload.results : [];
200
+
201
+ if (!allowEmpty && leads.length === 0) {
202
+ issues.push(issue('error', 'leads', 'missing_leads', 'at least one lead is required'));
203
+ }
204
+
205
+ if (!Array.isArray(payload?.domains)) {
206
+ issues.push(issue('error', 'domains', 'invalid_domains', 'domains must be an array'));
207
+ }
208
+
209
+ const ids = new Set();
210
+ const leadKeys = new Set();
211
+ leads.forEach((lead, index) => {
212
+ validateLead(lead, `leads[${index}]`, issues);
213
+ if (lead.id) {
214
+ if (ids.has(lead.id)) {
215
+ issues.push(issue('warning', `leads[${index}].id`, 'duplicate_id', `duplicate lead id ${lead.id}`));
216
+ }
217
+ ids.add(lead.id);
218
+ }
219
+ const key = stableLeadKey(lead);
220
+ if (leadKeys.has(key)) {
221
+ issues.push(issue('warning', `leads[${index}]`, 'duplicate_lead_key', 'duplicate lead identity fields'));
222
+ }
223
+ leadKeys.add(key);
224
+ });
225
+
226
+ results.forEach((result, index) => {
227
+ validateResult(result, `results[${index}]`, issues);
228
+ });
229
+
230
+ const errors = issues.filter((item) => item.severity === 'error').length;
231
+ const warnings = issues.filter((item) => item.severity === 'warning').length;
232
+ return {
233
+ ok: errors === 0,
234
+ errors,
235
+ warnings,
236
+ issues,
237
+ summary: summarizePayload(payload),
238
+ };
239
+ }
240
+
241
+ export function validateLead(lead, path, issues) {
242
+ requiredString(lead.domain, `${path}.domain`, 'domain is required', issues);
243
+ requiredString(lead.emailType, `${path}.emailType`, 'emailType is required', issues);
244
+ requiredString(lead.sourceUrl, `${path}.sourceUrl`, 'sourceUrl is required', issues);
245
+ requiredString(lead.evidence, `${path}.evidence`, 'evidence is required', issues);
246
+ requiredString(lead.extractionMethod, `${path}.extractionMethod`, 'extractionMethod is required', issues);
247
+ requiredString(lead.verificationStatus, `${path}.verificationStatus`, 'verificationStatus is required', issues);
248
+
249
+ if (lead.domain && normalizeDomain(lead.domain) !== lead.domain) {
250
+ issues.push(issue('warning', `${path}.domain`, 'domain_not_normalized', 'domain should be lowercase without protocol or www'));
251
+ }
252
+ if (lead.email && !EMAIL_RE.test(lead.email)) {
253
+ issues.push(issue('error', `${path}.email`, 'invalid_email', 'email must be a valid email address when present'));
254
+ }
255
+ if (lead.emailType && !ALLOWED_EMAIL_TYPES.has(lead.emailType)) {
256
+ issues.push(issue('error', `${path}.emailType`, 'invalid_email_type', `emailType must be one of ${[...ALLOWED_EMAIL_TYPES].join(', ')}`));
257
+ }
258
+ if (lead.verificationStatus && !ALLOWED_VERIFICATION_STATUSES.has(lead.verificationStatus)) {
259
+ issues.push(issue('error', `${path}.verificationStatus`, 'invalid_verification_status', `verificationStatus must be one of ${[...ALLOWED_VERIFICATION_STATUSES].join(', ')}`));
260
+ }
261
+ if (!Number.isInteger(lead.confidence) || lead.confidence < 0 || lead.confidence > 100) {
262
+ issues.push(issue('error', `${path}.confidence`, 'invalid_confidence', 'confidence must be an integer from 0 to 100'));
263
+ }
264
+ if (lead.sourceUrl && !isHttpUrl(lead.sourceUrl)) {
265
+ issues.push(issue('error', `${path}.sourceUrl`, 'invalid_source_url', 'sourceUrl must be an http(s) URL'));
266
+ }
267
+ if (lead.websiteUrl && !isHttpUrl(lead.websiteUrl)) {
268
+ issues.push(issue('warning', `${path}.websiteUrl`, 'invalid_website_url', 'websiteUrl should be an http(s) URL'));
269
+ }
270
+ if (lead.emailType === 'contact_path' && lead.email) {
271
+ issues.push(issue('warning', `${path}.email`, 'contact_path_with_email', 'contact_path leads should normally leave email empty'));
272
+ }
273
+ if ((lead.emailType === 'person' || lead.emailType === 'role') && !lead.email) {
274
+ issues.push(issue('error', `${path}.email`, 'email_required', `${lead.emailType} leads require email`));
275
+ }
276
+ if (lead.emailType === 'blocked' && lead.confidence > 0) {
277
+ issues.push(issue('warning', `${path}.confidence`, 'blocked_confidence', 'blocked operational inboxes should use confidence 0'));
278
+ }
279
+ if (lead.evidence && lead.evidence.length < 8) {
280
+ issues.push(issue('warning', `${path}.evidence`, 'thin_evidence', 'evidence is very short'));
281
+ }
282
+ if (lead.phone && !/^\+\d{10,15}$/.test(lead.phone)) {
283
+ issues.push(issue('warning', `${path}.phone`, 'phone_not_normalized', 'phone should be normalized E.164-style when present'));
284
+ }
285
+ if (!Array.isArray(lead.socialUrls)) {
286
+ issues.push(issue('error', `${path}.socialUrls`, 'invalid_social_urls', 'socialUrls must be an array'));
287
+ } else {
288
+ lead.socialUrls.forEach((url, index) => {
289
+ if (!isHttpUrl(url)) issues.push(issue('warning', `${path}.socialUrls[${index}]`, 'invalid_social_url', 'socialUrls entries should be http(s) URLs'));
290
+ });
291
+ }
292
+ if (!Array.isArray(lead.contactUrls)) {
293
+ issues.push(issue('error', `${path}.contactUrls`, 'invalid_contact_urls', 'contactUrls must be an array'));
294
+ } else {
295
+ lead.contactUrls.forEach((url, index) => {
296
+ if (!isHttpUrl(url)) issues.push(issue('warning', `${path}.contactUrls[${index}]`, 'invalid_contact_url', 'contactUrls entries should be http(s) URLs'));
297
+ });
298
+ }
299
+ if (!Array.isArray(lead.warnings)) {
300
+ issues.push(issue('error', `${path}.warnings`, 'invalid_warnings', 'warnings must be an array'));
301
+ }
302
+ if (lead.foundAt && Number.isNaN(Date.parse(lead.foundAt))) {
303
+ issues.push(issue('error', `${path}.foundAt`, 'invalid_found_at', 'foundAt must be ISO datetime'));
304
+ }
305
+ }
306
+
307
+ export function validateResult(result, path, issues) {
308
+ requiredString(result.domain, `${path}.domain`, 'result domain is required', issues);
309
+ if (result.websiteUrl && !isHttpUrl(result.websiteUrl)) {
310
+ issues.push(issue('warning', `${path}.websiteUrl`, 'invalid_website_url', 'websiteUrl should be an http(s) URL'));
311
+ }
312
+ if (!Array.isArray(result.pages)) {
313
+ issues.push(issue('error', `${path}.pages`, 'invalid_pages', 'pages must be an array'));
314
+ }
315
+ if (!Array.isArray(result.leads)) {
316
+ issues.push(issue('error', `${path}.leads`, 'invalid_result_leads', 'result leads must be an array'));
317
+ }
318
+ }
319
+
320
+ export function summarizePayload(payload) {
321
+ const leads = Array.isArray(payload?.leads) ? payload.leads : [];
322
+ const results = Array.isArray(payload?.results) ? payload.results : [];
323
+ const domains = cleanDomains(payload?.domains || leads.map((lead) => lead.domain));
324
+ const byType = {};
325
+ let confidenceTotal = 0;
326
+ let withEmail = 0;
327
+ const uniqueEmails = new Set();
328
+ const mxVerifiedEmails = new Set();
329
+ for (const lead of leads) {
330
+ byType[lead.emailType || 'unknown'] = (byType[lead.emailType || 'unknown'] || 0) + 1;
331
+ confidenceTotal += Number.isFinite(lead.confidence) ? lead.confidence : 0;
332
+ if (lead.email) {
333
+ withEmail++;
334
+ uniqueEmails.add(String(lead.email).toLowerCase());
335
+ if (lead.verificationStatus === 'mx_verified' || lead.verificationStatus === 'verified') {
336
+ mxVerifiedEmails.add(String(lead.email).toLowerCase());
337
+ }
338
+ }
339
+ }
340
+ return {
341
+ domains,
342
+ domainCount: domains.length,
343
+ leadCount: leads.length,
344
+ resultCount: results.length,
345
+ withEmail,
346
+ uniqueEmailCount: uniqueEmails.size,
347
+ mxVerifiedEmailCount: mxVerifiedEmails.size,
348
+ byType,
349
+ averageConfidence: leads.length ? Math.round(confidenceTotal / leads.length) : 0,
350
+ };
351
+ }
352
+
353
+ export function createManifestRecord({ inputPath, payload, validation, generatedAt = new Date().toISOString() }) {
354
+ const input = resolveProjectPath(inputPath);
355
+ const hash = fileHash(input);
356
+ return {
357
+ id: payload.jobId || `lead-batch-${hash.slice(0, 12)}`,
358
+ input: relativeProjectPath(input),
359
+ inputSha256: hash,
360
+ generatedAt,
361
+ domains: payload.domains,
362
+ leadCount: payload.leads.length,
363
+ resultCount: payload.results.length,
364
+ errorCount: payload.errors.length,
365
+ validation: {
366
+ ok: validation.ok,
367
+ errors: validation.errors,
368
+ warnings: validation.warnings,
369
+ },
370
+ readyForIngest: validation.ok && payload.leads.length > 0,
371
+ };
372
+ }
373
+
374
+ export function discoverLeadArtifacts(root = PROJECT_DIR) {
375
+ const files = new Set();
376
+ for (const rel of DEFAULT_ARTIFACT_GLOBS) {
377
+ const abs = resolve(root, rel);
378
+ if (existsSync(abs) && statSync(abs).isFile()) files.add(abs);
379
+ }
380
+ for (const dirRel of ['data', 'batch']) {
381
+ const dir = resolve(root, dirRel);
382
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) continue;
383
+ for (const file of readdirSync(dir)) {
384
+ if (!/^lead-results.*\.jsonl?$/.test(file) && !/^ingest-payload.*\.jsonl?$/.test(file)) continue;
385
+ files.add(join(dir, file));
386
+ }
387
+ }
388
+ return [...files].sort();
389
+ }
390
+
391
+ export function loadProfileConfig(root = PROJECT_DIR) {
392
+ const path = resolve(root, 'config/profile.yml');
393
+ if (!existsSync(path)) return {};
394
+ const text = readFileSync(path, 'utf8');
395
+ return {
396
+ baseUrl: yamlScalar(text, 'base_url'),
397
+ ingestPath: yamlScalar(text, 'ingest_path'),
398
+ targetProject: yamlScalar(text, 'target_project'),
399
+ operatorEmail: yamlScalar(text, 'operator_email'),
400
+ operatorEmailHeader: yamlScalar(text, 'operator_email_header'),
401
+ authHeader: yamlScalar(text, 'auth_header'),
402
+ authScheme: yamlScalar(text, 'auth_scheme'),
403
+ authTokenEnv: yamlScalar(text, 'auth_token_env'),
404
+ adminEmail: yamlScalar(text, 'admin_email'),
405
+ adminEmailHeader: yamlScalar(text, 'admin_email_header'),
406
+ adminTokenEnv: yamlScalar(text, 'admin_token_env'),
407
+ maxPages: yamlScalar(text, 'max_pages'),
408
+ maxDomainsPerBatch: yamlScalar(text, 'max_domains_per_batch'),
409
+ concurrency: yamlScalar(text, 'concurrency'),
410
+ userAgent: yamlScalar(text, 'user_agent'),
411
+ minConfidence: yamlScalar(text, 'min_confidence'),
412
+ };
413
+ }
414
+
415
+ export function cleanDomains(values) {
416
+ const out = [];
417
+ const seen = new Set();
418
+ for (const value of values || []) {
419
+ const domain = normalizeDomain(value);
420
+ if (!domain || seen.has(domain)) continue;
421
+ seen.add(domain);
422
+ out.push(domain);
423
+ }
424
+ return out;
425
+ }
426
+
427
+ export function normalizeDomain(value) {
428
+ let raw = stringValue(value).toLowerCase();
429
+ if (!raw) return '';
430
+ if (!raw.includes('://')) raw = `https://${raw}`;
431
+ try {
432
+ const url = new URL(raw);
433
+ raw = url.hostname;
434
+ } catch {
435
+ raw = raw.replace(/^https?:\/\//, '').split('/')[0];
436
+ }
437
+ return raw.replace(/^www\./, '').replace(/\.$/, '').trim();
438
+ }
439
+
440
+ export function domainFromUrl(value) {
441
+ const raw = stringValue(value);
442
+ if (!raw) return '';
443
+ try {
444
+ return normalizeDomain(new URL(raw).hostname);
445
+ } catch {
446
+ return '';
447
+ }
448
+ }
449
+
450
+ export function stableLeadID(lead) {
451
+ const key = stableLeadKey(lead);
452
+ if (!key.replace(/\|/g, '').trim()) return `agentic-${Date.now()}`;
453
+ return createHash('sha1').update(key).digest('hex');
454
+ }
455
+
456
+ function stableLeadKey(lead) {
457
+ if (lead?.email) {
458
+ return ['email', lead.domain || '', lead.email || ''].join('|').toLowerCase();
459
+ }
460
+ if (lead?.emailType === 'contact_path') {
461
+ return ['contact_path', lead.domain || ''].join('|').toLowerCase();
462
+ }
463
+ return [
464
+ lead?.email || '',
465
+ lead?.sourceUrl || '',
466
+ lead?.domain || '',
467
+ lead?.contactName || '',
468
+ lead?.title || '',
469
+ ].join('|').toLowerCase();
470
+ }
471
+
472
+ export function fileHash(path) {
473
+ return createHash('sha256').update(readFileSync(path)).digest('hex');
474
+ }
475
+
476
+ export function issue(severity, path, code, message) {
477
+ return { severity, path, code, message };
478
+ }
479
+
480
+ function parseJsonl(raw, path) {
481
+ const records = [];
482
+ for (const [index, line] of raw.split(/\r?\n/).entries()) {
483
+ const trimmed = line.trim();
484
+ if (!trimmed) continue;
485
+ try {
486
+ records.push(JSON.parse(trimmed));
487
+ } catch (error) {
488
+ throw new Error(`${basename(path)}:${index + 1}: invalid JSONL: ${error.message}`);
489
+ }
490
+ }
491
+ return records;
492
+ }
493
+
494
+ function normalizeEmailType(value, email) {
495
+ const raw = stringValue(value).toLowerCase();
496
+ if (ALLOWED_EMAIL_TYPES.has(raw)) return raw;
497
+ return email ? 'unknown' : 'contact_path';
498
+ }
499
+
500
+ function normalizeVerificationStatus(value, emailType) {
501
+ const raw = stringValue(value).toLowerCase();
502
+ if (ALLOWED_VERIFICATION_STATUSES.has(raw)) return raw;
503
+ if (emailType === 'contact_path') return 'not_applicable';
504
+ if (emailType === 'blocked') return 'blocked';
505
+ return 'unverified';
506
+ }
507
+
508
+ function normalizeWarnings(value) {
509
+ if (!Array.isArray(value)) return [];
510
+ return value.map((item) => stringValue(item)).filter(Boolean);
511
+ }
512
+
513
+ function normalizeUrlList(value) {
514
+ if (!Array.isArray(value)) return [];
515
+ const urls = [];
516
+ const seen = new Set();
517
+ for (const item of value) {
518
+ const url = stringValue(item);
519
+ if (!url || seen.has(url)) continue;
520
+ seen.add(url);
521
+ urls.push(url);
522
+ }
523
+ return urls;
524
+ }
525
+
526
+ function normalizeDateTime(value, fallback) {
527
+ const raw = stringValue(value);
528
+ if (!raw) return fallback;
529
+ const date = new Date(raw);
530
+ return Number.isNaN(date.getTime()) ? fallback : date.toISOString();
531
+ }
532
+
533
+ function requiredString(value, path, message, issues) {
534
+ if (!stringValue(value)) issues.push(issue('error', path, 'required', message));
535
+ }
536
+
537
+ function isHttpUrl(value) {
538
+ try {
539
+ const url = new URL(value);
540
+ return url.protocol === 'http:' || url.protocol === 'https:';
541
+ } catch {
542
+ return false;
543
+ }
544
+ }
545
+
546
+ function clampInteger(value, min, max, fallback) {
547
+ const number = Number(value);
548
+ if (!Number.isFinite(number)) return fallback;
549
+ return Math.max(min, Math.min(max, Math.round(number)));
550
+ }
551
+
552
+ function stringValue(value) {
553
+ return value === null || value === undefined ? '' : String(value).trim();
554
+ }
555
+
556
+ function compactWhitespace(value) {
557
+ return stringValue(value).replace(/\s+/g, ' ');
558
+ }
559
+
560
+ function displayCompany(domain) {
561
+ const clean = normalizeDomain(domain);
562
+ if (!clean) return '';
563
+ const root = clean.split('.')[0] || clean;
564
+ return root
565
+ .split(/[-_]/)
566
+ .filter(Boolean)
567
+ .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
568
+ .join(' ');
569
+ }
570
+
571
+ function yamlScalar(text, key) {
572
+ const match = text.match(new RegExp(`^\\s*${key}:\\s*["']?([^"'\\n#]+)["']?`, 'm'));
573
+ return match ? match[1].trim() : '';
574
+ }
package/models.yaml ADDED
@@ -0,0 +1,32 @@
1
+ # Public Leads Harness model policy.
2
+ #
3
+ # Keeps OpenCode on the reliable DeepSeek V4 Flash route. Agents bind to these roles through
4
+ # iso/agents/*.md:
5
+ # @general-free -> role: fast
6
+ # @general-paid -> role: quality
7
+ # @glm-minimal -> role: minimal
8
+
9
+ extends: standard
10
+
11
+ default:
12
+ targets:
13
+ opencode:
14
+ provider: opencode
15
+ model: opencode-go/deepseek-v4-flash
16
+
17
+ roles:
18
+ fast:
19
+ targets:
20
+ opencode:
21
+ provider: opencode
22
+ model: opencode-go/deepseek-v4-flash
23
+ quality:
24
+ targets:
25
+ opencode:
26
+ provider: opencode
27
+ model: opencode-go/deepseek-v4-flash
28
+ minimal:
29
+ targets:
30
+ opencode:
31
+ provider: opencode
32
+ model: opencode-go/deepseek-v4-flash
@@ -0,0 +1,50 @@
1
+ # Shared Lead Discovery Policy
2
+
3
+ This harness creates reviewable, public-source lead records and ingest-ready artifacts.
4
+
5
+ ## Lead Contract
6
+
7
+ Every persisted lead must validate with:
8
+
9
+ ```bash
10
+ npx public-leads validate --input <artifact>
11
+ ```
12
+
13
+ Required fields:
14
+
15
+ | Field | Rule |
16
+ |---|---|
17
+ | `domain` | Lowercase company domain, no protocol or `www.` |
18
+ | `emailType` | `person`, `role`, `blocked`, `contact_path`, or `unknown` |
19
+ | `sourceUrl` | Public HTTP(S) page where the evidence was found |
20
+ | `evidence` | Short public-source excerpt supporting the lead |
21
+ | `extractionMethod` | Method label such as `agentic_harness_public_page` |
22
+ | `verificationStatus` | `verified`, `mx_verified`, `unverified`, `not_applicable`, `blocked`, or `unknown` |
23
+ | `confidence` | Integer 0-100 |
24
+
25
+ ## Source Rules
26
+
27
+ - Use official company websites first.
28
+ - Prioritize home, contact, about, team, leadership, people, press, blog, careers, and legal pages.
29
+ - Respect robots.txt, paywalls, login walls, and obvious anti-scraping notices.
30
+ - Do not infer email formats. Do not brute-force guessed addresses.
31
+ - Keep contact forms as `emailType: "contact_path"` with empty `email`.
32
+ - Mark operational no-contact inboxes such as `noreply`, `abuse`, `security`, and `legal` as `blocked` with confidence `0`.
33
+
34
+ ## Quality Heuristics
35
+
36
+ - `person`: named person or person-like email with source context.
37
+ - `role`: public role inbox such as `sales@`, `partnerships@`, or `press@`.
38
+ - `contact_path`: public form or page where no email is available.
39
+ - `blocked`: public address that should not be used for outbound.
40
+
41
+ Confidence should reflect source quality, person specificity, domain match, and verification. Low-confidence records are allowed if they preserve warnings and evidence.
42
+
43
+ ## Local Commands
44
+
45
+ ```bash
46
+ npx public-leads validate --input data/lead-results.json
47
+ npx public-leads manifest --input data/lead-results.json
48
+ npx public-leads ingest --input data/lead-results.json --dry-run
49
+ npx public-leads verify
50
+ ```
package/modes/batch.md ADDED
@@ -0,0 +1,34 @@
1
+ # Batch Mode
2
+
3
+ Use this for queued multi-domain lead discovery.
4
+
5
+ ## Runner
6
+
7
+ Prefer:
8
+
9
+ ```bash
10
+ batch/batch-runner.sh --parallel 2
11
+ ```
12
+
13
+ The runner delegates to `scripts/batch-orchestrator.mjs`, which uses `@razroo/iso-orchestrator` for durable workflow records in `.public-leads-runs/`, bounded fan-out, and state updates.
14
+
15
+ ## Input
16
+
17
+ `batch/batch-input.tsv`:
18
+
19
+ ```text
20
+ id domain company notes
21
+ 1 example.com Example Seed target
22
+ ```
23
+
24
+ ## Procedure
25
+
26
+ 1. Run `batch/batch-runner.sh --dry-run` first unless the user explicitly asks to start immediately.
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`.
31
+
32
+ ## Output
33
+
34
+ Report completed, failed, skipped, artifact paths, and manifest status.