mirror of
https://github.com/getcargohq/cargo-skills.git
synced 2026-09-14 19:17:15 +08:00
2792b990ed
* feat(gtm): executable QA layer — fixture-tested contact-accuracy scripts
Four deterministic TypeScript scripts under cargo-gtm/scripts/ (Node >= 22.18
native type-stripping, zero deps in file mode) replace in-context row checking:
- validate-emails.ts free syntax/risk/duplicate cull before paid verifyEmail
- select-current-role.ts current role from an experiences array (job changers)
- validate-linkedin-names.ts name<->profile match (same-name decoys); P>=0.95/R>=0.85 gate
- contact-accuracy-audit.ts final per-row SEND/VERIFY/REVIEW/REMOVE stamp
Each reads --input CSV/JSON or a finished run directly (--workflow-uuid via
@cargo-ai/api, reusing the CLI's stored login) and self-tests with --fixtures
(152 synthetic cases total). New qa-script-fixtures CI job runs every suite on
push/PR. Doctrine in references/contact-accuracy.md ("run the script, don't
re-derive"); wired into the router (new section 8, spine step 8) and the
prospecting / outreach-activation verify steps. cargo-gtm -> 1.2.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(gtm): provider playbooks 6 → 20 + read-first gate (#54)
Fourteen new per-provider playbooks grounded in live `connection integration
get` schemas: sourcing/company-data (linkedin, proxycurl, apolloio, oceanio,
datagma), email/contact (hunter, prospeo, icypeas, findyMail, leadMagic,
contactOut, zeroBounce), research (firecrawl, serper). Each carries the action
table with credit costs, real config-shape examples, input quirks, cost traps,
and its recipe-spine position.
Router §11 opens with a hard read-first stop before any paid call to a covered
provider; the linter now errors when a playbook on disk is missing from the
SKILL.md catalog.
Grounding also surfaced and fixed a recipe bug: linkedin-url-lookup.md
documented findProfileUrl inputs that don't exist — the live schema is
fullName (required) + companyName. Stale cost-table rows (prospeo.verifyEmail,
serper pricing) flagged in the CHANGELOG for the next regeneration.
cargo-gtm -> 1.3.0.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(gtm): QA-chain review feedback — six wiring bugs in scripts and recipes
- contact-accuracy-audit.ts: detect a bare "status" header (raw waterfall
verifyEmail rows) as the verification column, lowest priority so specific
headers still win; two new fixture cases.
- lib/common.ts: accept raw execute-batch output ({"results": [...]} /
{"records": [...]}) in JSON inputs — no jq unwrap needed.
- validate-emails.ts + contact-accuracy-audit.ts: new --json flag emits rows
as a JSON array for jq chaining; duplicates now get recommendation "skip"
so they never enter the paid verify batch (savings count includes them).
- prospecting.md step 6/6.5: the paid verify batch is built from the culled
output (recommendation != "skip"), an explicit merge step joins statuses
back onto ALL rows before the audit, and only audit_action == "SEND" rows
proceed to write-back.
- outreach-activation.md step 4: same cull -> verify survivors -> merge ->
audit-all -> SEND-only flow; steps 5-6 now read the audited SEND set
(deliverable.json), with the step-5/6 jq paths fixed for array input.
- contact-accuracy.md: documents the never-pre-filter-to-valid rule, the
execute-batch input support, and --json.
All four fixture suites pass; the full recipe chain was exercised end-to-end
on simulated batch outputs (disposable + duplicate culled before payment,
catch-all single-source -> VERIFY, only SEND rows in the handoff set).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(gtm): case-insensitive status merge; strict identity coverage; dedupe verdict
- Recipes: the verification-status merge now joins on the lowercased email —
verifyEmail results may re-case the address, and a missed join left
emailStatus empty, silently degrading deliverable rows to VERIFY.
- contact-accuracy-audit.ts: identityColumnsPresent now requires BOTH
name_match and role_confidence columns — one upstream script alone no
longer masks partial identity coverage (partial-signals flag stays on).
- contact-accuracy-audit.ts: new duplicate-row REMOVE rule reading the
is_duplicate column — a later occurrence of an address never reaches the
sequencer even when all other signals are green (found while re-testing:
the case-insensitive join let the dupe inherit "valid" and SEND).
23 fixture cases pass; chain re-run with re-cased verify results yields
exactly one SEND.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(gtm): merge verifyEmail via email_status, not status
waterfall.verifyEmail batch rows expose the verdict on email_status per the
connector output schema. The recipe merge jq read .status, so merged rows got
an empty emailStatus and contact-accuracy-audit stamped VERIFY instead of SEND.
Co-authored-by: Auree Aubert <aureeaubert@users.noreply.github.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Auree Aubert <aureeaubert@users.noreply.github.com>
377 lines
9.8 KiB
JSON
377 lines
9.8 KiB
JSON
{
|
|
"cases": [
|
|
{
|
|
"sourceName": "John Carter",
|
|
"profileName": "John Carter",
|
|
"expected": true,
|
|
"note": "exact match"
|
|
},
|
|
{
|
|
"sourceName": "MARIA LOPES",
|
|
"profileName": "maria lopes",
|
|
"expected": true,
|
|
"note": "case-insensitive"
|
|
},
|
|
{
|
|
"sourceName": "José Álvarez",
|
|
"profileName": "Jose Alvarez",
|
|
"expected": true,
|
|
"note": "accents normalized (NFD strip)"
|
|
},
|
|
{
|
|
"sourceName": "Renee Dupont",
|
|
"profileName": "Renée Dupont",
|
|
"expected": true,
|
|
"note": "accents normalized, profile side"
|
|
},
|
|
{
|
|
"sourceName": "Mike Turner",
|
|
"profileName": "Michael Turner",
|
|
"expected": true,
|
|
"note": "nickname source→formal"
|
|
},
|
|
{
|
|
"sourceName": "Michael Turner",
|
|
"profileName": "Mike Turner",
|
|
"expected": true,
|
|
"note": "nickname formal→source"
|
|
},
|
|
{
|
|
"sourceName": "Bob Whitfield",
|
|
"profileName": "Robert Whitfield",
|
|
"expected": true,
|
|
"note": "nickname Bob/Robert"
|
|
},
|
|
{
|
|
"sourceName": "Elizabeth Kraus",
|
|
"profileName": "Liz Kraus",
|
|
"expected": true,
|
|
"note": "nickname Liz/Elizabeth"
|
|
},
|
|
{
|
|
"sourceName": "Beth Nolan",
|
|
"profileName": "Elizabeth Nolan",
|
|
"expected": true,
|
|
"note": "nickname Beth/Elizabeth"
|
|
},
|
|
{
|
|
"sourceName": "Rick Osborne",
|
|
"profileName": "Richard Osborne",
|
|
"expected": true,
|
|
"note": "nickname Rick/Richard"
|
|
},
|
|
{
|
|
"sourceName": "Jim Falk",
|
|
"profileName": "James Falk",
|
|
"expected": true,
|
|
"note": "nickname Jim/James"
|
|
},
|
|
{
|
|
"sourceName": "Katie Merton",
|
|
"profileName": "Katherine Merton",
|
|
"expected": true,
|
|
"note": "nickname Katie/Katherine"
|
|
},
|
|
{
|
|
"sourceName": "Tony Marek",
|
|
"profileName": "Anthony Marek",
|
|
"expected": true,
|
|
"note": "nickname Tony/Anthony"
|
|
},
|
|
{
|
|
"sourceName": "Chris Devlin",
|
|
"profileName": "Christopher Devlin",
|
|
"expected": true,
|
|
"note": "nickname Chris/Christopher"
|
|
},
|
|
{
|
|
"sourceName": "Chris Halloran",
|
|
"profileName": "Christine Halloran",
|
|
"expected": true,
|
|
"note": "nickname Chris/Christine (same nickname, second formal)"
|
|
},
|
|
{
|
|
"sourceName": "Dave Pemberton",
|
|
"profileName": "David Pemberton",
|
|
"expected": true,
|
|
"note": "nickname Dave/David"
|
|
},
|
|
{
|
|
"sourceName": "Ted Grayson",
|
|
"profileName": "Edward Grayson",
|
|
"expected": true,
|
|
"note": "nickname Ted/Edward"
|
|
},
|
|
{
|
|
"sourceName": "Alex Riven",
|
|
"profileName": "Alexandra Riven",
|
|
"expected": true,
|
|
"note": "nickname Alex/Alexandra"
|
|
},
|
|
{
|
|
"sourceName": "Chuck Ambrose",
|
|
"profileName": "Charles Ambrose",
|
|
"expected": true,
|
|
"note": "nickname Chuck/Charles"
|
|
},
|
|
{
|
|
"sourceName": "Peggy Lindqvist",
|
|
"profileName": "Margaret Lindqvist",
|
|
"expected": true,
|
|
"note": "nickname Peggy/Margaret"
|
|
},
|
|
{
|
|
"sourceName": "Jack Tremaine",
|
|
"profileName": "John Tremaine",
|
|
"expected": true,
|
|
"note": "nickname Jack/John"
|
|
},
|
|
{
|
|
"sourceName": "Sam Ellery",
|
|
"profileName": "Samantha Ellery",
|
|
"expected": true,
|
|
"note": "nickname Sam/Samantha"
|
|
},
|
|
{
|
|
"sourceName": "Steve Calloway",
|
|
"profileName": "Stephen Calloway",
|
|
"expected": true,
|
|
"note": "nickname Steve/Stephen"
|
|
},
|
|
{
|
|
"sourceName": "Zack Templeton",
|
|
"profileName": "Zachary Templeton",
|
|
"expected": true,
|
|
"note": "nickname Zack/Zachary"
|
|
},
|
|
{
|
|
"sourceName": "Meg Ostrander",
|
|
"profileName": "Margaret Ostrander",
|
|
"expected": true,
|
|
"note": "nickname Meg/Margaret"
|
|
},
|
|
{
|
|
"sourceName": "J. Rutherford",
|
|
"profileName": "James Rutherford",
|
|
"expected": true,
|
|
"note": "source first name is an initial"
|
|
},
|
|
{
|
|
"sourceName": "Nathaniel Brooks",
|
|
"profileName": "N. Brooks",
|
|
"expected": true,
|
|
"note": "profile first name is an initial"
|
|
},
|
|
{
|
|
"sourceName": "Ana García",
|
|
"profileName": "Ana García Fernández",
|
|
"expected": true,
|
|
"note": "maiden/married double surname on profile"
|
|
},
|
|
{
|
|
"sourceName": "Laura Jensen",
|
|
"profileName": "Laura Jensen Holt",
|
|
"expected": true,
|
|
"note": "married surname appended on profile"
|
|
},
|
|
{
|
|
"sourceName": "Claire Beaumont",
|
|
"profileName": "Claire Beaumont-Ridley",
|
|
"expected": true,
|
|
"note": "hyphenated profile surname contains source surname"
|
|
},
|
|
{
|
|
"sourceName": "Priya Raghavan-Iyer",
|
|
"profileName": "Priya Raghavan",
|
|
"expected": true,
|
|
"note": "hyphenated source surname, profile has one half"
|
|
},
|
|
{
|
|
"sourceName": "Bob Callahan",
|
|
"profileName": "Robert \"Bob\" Callahan",
|
|
"expected": true,
|
|
"note": "quoted nickname in profile"
|
|
},
|
|
{
|
|
"sourceName": "Kate Winslow",
|
|
"profileName": "Katherine (Kate) Winslow",
|
|
"expected": true,
|
|
"note": "parenthesized nickname in profile"
|
|
},
|
|
{
|
|
"sourceName": "Sunny Deshpande",
|
|
"profileName": "Suniti (Sunny) Deshpande",
|
|
"expected": true,
|
|
"note": "parenthesized nickname not in the nickname table"
|
|
},
|
|
{
|
|
"sourceName": "Daniel Okafor",
|
|
"profileName": "Daniel Okafor, PhD",
|
|
"expected": true,
|
|
"note": "credentials after comma stripped"
|
|
},
|
|
{
|
|
"sourceName": "Marcus Bell Jr",
|
|
"profileName": "Marcus Bell",
|
|
"expected": true,
|
|
"note": "generational suffix stripped from source"
|
|
},
|
|
{
|
|
"sourceName": "Dr Helena Voss",
|
|
"profileName": "Helena Voss",
|
|
"expected": true,
|
|
"note": "title stripped from source"
|
|
},
|
|
{
|
|
"sourceName": "Tara Whitcombe",
|
|
"profileName": "Tara Whitcombe 🚀",
|
|
"expected": true,
|
|
"note": "emoji stripped from profile"
|
|
},
|
|
{
|
|
"sourceName": "Sofia Marchetti",
|
|
"profileName": "Sofia Elena Marchetti",
|
|
"expected": true,
|
|
"note": "middle name on profile ignored"
|
|
},
|
|
{
|
|
"sourceName": "Omar Haddad",
|
|
"profileName": "Omar K. Haddad",
|
|
"expected": true,
|
|
"note": "middle initial on profile ignored"
|
|
},
|
|
{
|
|
"sourceName": "Peter James Quill",
|
|
"profileName": "Peter Quill",
|
|
"expected": true,
|
|
"note": "middle name on source ignored"
|
|
},
|
|
{
|
|
"sourceName": "田中 太郎",
|
|
"profileName": "田中 太郎",
|
|
"expected": true,
|
|
"note": "non-Latin (CJK) exact match"
|
|
},
|
|
{
|
|
"sourceName": "Иван Смирнов",
|
|
"profileName": "Иван Смирнов",
|
|
"expected": true,
|
|
"note": "non-Latin (Cyrillic) exact match"
|
|
},
|
|
{
|
|
"sourceName": "Jen Falco",
|
|
"profileName": "Jennifer Falco Marsh",
|
|
"expected": true,
|
|
"note": "nickname plus married surname combined"
|
|
},
|
|
{
|
|
"sourceName": "WILLIAM HART",
|
|
"profileName": "Will Hart, MBA",
|
|
"expected": true,
|
|
"note": "case + nickname + credential suffix combined"
|
|
},
|
|
{
|
|
"sourceName": "John Carter",
|
|
"profileName": "John Mercer",
|
|
"expected": false,
|
|
"note": "same first name, different surname — decoy"
|
|
},
|
|
{
|
|
"sourceName": "Alice Fontaine",
|
|
"profileName": "Marie Fontaine",
|
|
"expected": false,
|
|
"note": "same surname, different first name"
|
|
},
|
|
{
|
|
"sourceName": "David Kowalski",
|
|
"profileName": "",
|
|
"expected": false,
|
|
"note": "empty profile name"
|
|
},
|
|
{
|
|
"sourceName": "Sarah Linden",
|
|
"profileName": "Tobias Grunwald",
|
|
"expected": false,
|
|
"note": "completely different names"
|
|
},
|
|
{
|
|
"sourceName": "Mike Turner",
|
|
"profileName": "Nick Turner",
|
|
"expected": false,
|
|
"note": "different nicknames of different formal names"
|
|
},
|
|
{
|
|
"sourceName": "Robert Callahan",
|
|
"profileName": "Robert",
|
|
"expected": false,
|
|
"note": "single-token profile cannot confirm surname"
|
|
},
|
|
{
|
|
"sourceName": "Иван Смирнов",
|
|
"profileName": "Пётр Волков",
|
|
"expected": false,
|
|
"note": "non-Latin (Cyrillic) mismatch"
|
|
},
|
|
{
|
|
"sourceName": "李 伟",
|
|
"profileName": "王 芳",
|
|
"expected": false,
|
|
"note": "non-Latin (CJK) mismatch"
|
|
},
|
|
{
|
|
"sourceName": "Jonathan Pierce",
|
|
"profileName": "John Pierson",
|
|
"expected": false,
|
|
"note": "near-miss decoy on both name parts"
|
|
},
|
|
{
|
|
"sourceName": "Emily Watkins",
|
|
"profileName": "Emma Watkins",
|
|
"expected": false,
|
|
"note": "similar first names are not nicknames"
|
|
},
|
|
{
|
|
"sourceName": "T. Vasquez",
|
|
"profileName": "Ramon Vasquez",
|
|
"expected": false,
|
|
"note": "initial does not match first letter"
|
|
},
|
|
{
|
|
"sourceName": "Karen Doyle",
|
|
"profileName": "LinkedIn Member",
|
|
"expected": false,
|
|
"note": "scraper placeholder garbage"
|
|
},
|
|
{
|
|
"sourceName": "Hannah Brightwater",
|
|
"profileName": "H",
|
|
"expected": false,
|
|
"note": "single-letter profile name"
|
|
},
|
|
{
|
|
"sourceName": "Noah Feldstein",
|
|
"profileName": " ",
|
|
"expected": false,
|
|
"note": "whitespace-only profile name"
|
|
},
|
|
{
|
|
"sourceName": "Greg Santos",
|
|
"profileName": "Theodore \"Teddy\" Santos",
|
|
"expected": false,
|
|
"note": "quoted nickname does not rescue a first-name mismatch"
|
|
},
|
|
{
|
|
"sourceName": "Kate Holm",
|
|
"profileName": "Katie Holm",
|
|
"expected": true,
|
|
"note": "two nicknames sharing a formal name (Kate/Katie via Katherine)"
|
|
},
|
|
{
|
|
"sourceName": "Rick Sanders",
|
|
"profileName": "Dick Sanders",
|
|
"expected": true,
|
|
"note": "shared formal via Richard"
|
|
}
|
|
]
|
|
}
|