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>
225 lines
6.9 KiB
JSON
225 lines
6.9 KiB
JSON
{
|
|
"cases": [
|
|
{
|
|
"email": "jane.doe@acme-widgets.example",
|
|
"expected": { "valid": true, "risk": "ok" },
|
|
"note": "plain business address on a reserved .example TLD"
|
|
},
|
|
{
|
|
"email": " JOHN.SMITH@Northwind-Traders.EXAMPLE ",
|
|
"expected": { "valid": true, "risk": "ok" },
|
|
"note": "uppercase + surrounding whitespace is normalized before checks"
|
|
},
|
|
{
|
|
"email": "jane+tag@globex-corp.example",
|
|
"expected": { "valid": true, "risk": "ok" },
|
|
"note": "plus-tagged local part is legal"
|
|
},
|
|
{
|
|
"email": "cto@a.b.co",
|
|
"expected": { "valid": true, "risk": "ok" },
|
|
"note": "subdomain domain with short labels and 2-char TLD"
|
|
},
|
|
{
|
|
"email": "vp.sales@emea.initech-labs.example",
|
|
"expected": { "valid": true, "risk": "ok" },
|
|
"note": "role words inside a longer local part are NOT a role account"
|
|
},
|
|
{
|
|
"email": "user@acme.test",
|
|
"expected": { "valid": true, "risk": "ok" },
|
|
"note": "reserved .test TLD is a fine synthetic business domain"
|
|
},
|
|
{
|
|
"email": "ops@intra-net.example",
|
|
"expected": { "valid": true, "risk": "ok" },
|
|
"note": "hyphen inside a domain label is legal"
|
|
},
|
|
{
|
|
"email": "jane..doe@acme-widgets.example",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "consecutive dots in local part"
|
|
},
|
|
{
|
|
"email": ".jane@acme-widgets.example",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "local part starts with a dot"
|
|
},
|
|
{
|
|
"email": "jane.@acme-widgets.example",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "local part ends with a dot"
|
|
},
|
|
{
|
|
"email": "janedoe.acme-widgets.example",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "missing @"
|
|
},
|
|
{
|
|
"email": "jane@doe@acme-widgets.example",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "two @ signs"
|
|
},
|
|
{
|
|
"email": "@acme-widgets.example",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "empty local part"
|
|
},
|
|
{
|
|
"email": "jane@acme.123",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "numeric TLD"
|
|
},
|
|
{
|
|
"email": "jane@acme.x",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "1-char TLD"
|
|
},
|
|
{
|
|
"email": "jane@localhost",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "domain needs at least two labels"
|
|
},
|
|
{
|
|
"email": "bob@-badstart.example",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "domain label starts with a hyphen"
|
|
},
|
|
{
|
|
"email": "bob@badend-.example",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "domain label ends with a hyphen"
|
|
},
|
|
{
|
|
"email": "jane@acme_widgets.example",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "underscore is not allowed in a domain label"
|
|
},
|
|
{
|
|
"email": "jane doe@acme-widgets.example",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "embedded space"
|
|
},
|
|
{
|
|
"email": "jane,doe@acme-widgets.example",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "embedded comma"
|
|
},
|
|
{
|
|
"email": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@acme-widgets.example",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "local part of 65 chars exceeds the 64-char limit"
|
|
},
|
|
{
|
|
"email": "user@example.com",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "placeholder: exact domain example.com (rule matches example.com/org/net exactly, so acme-widgets.example stays valid)"
|
|
},
|
|
{
|
|
"email": "user@example.org",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "placeholder: exact domain example.org"
|
|
},
|
|
{
|
|
"email": "test@test.example",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "placeholder: contains the test@test pattern"
|
|
},
|
|
{
|
|
"email": "noemail@acme-widgets.example",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "placeholder: contains noemail"
|
|
},
|
|
{
|
|
"email": "no-reply@acme-widgets.example",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "placeholder: no-reply@ is caught before the role check runs"
|
|
},
|
|
{
|
|
"email": "unknown@acme-widgets.example",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "placeholder: contains unknown@"
|
|
},
|
|
{
|
|
"email": "n/a",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "literal placeholder n/a"
|
|
},
|
|
{
|
|
"email": "null",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "literal placeholder null"
|
|
},
|
|
{
|
|
"email": "NONE",
|
|
"expected": { "valid": false, "risk": "invalid" },
|
|
"note": "literal placeholder none, case-insensitive"
|
|
},
|
|
{
|
|
"email": "foo@mailinator.com",
|
|
"expected": { "valid": true, "risk": "disposable" },
|
|
"note": "disposable provider"
|
|
},
|
|
{
|
|
"email": "BAR@YOPMAIL.COM",
|
|
"expected": { "valid": true, "risk": "disposable" },
|
|
"note": "disposable provider matched after lowercasing"
|
|
},
|
|
{
|
|
"email": "baz@temp-mail.org",
|
|
"expected": { "valid": true, "risk": "disposable" },
|
|
"note": "disposable provider with hyphenated domain"
|
|
},
|
|
{
|
|
"email": "qux@guerrillamail.com",
|
|
"expected": { "valid": true, "risk": "disposable" },
|
|
"note": "disposable provider"
|
|
},
|
|
{
|
|
"email": "info@acme-widgets.example",
|
|
"expected": { "valid": true, "risk": "role" },
|
|
"note": "role account: info@"
|
|
},
|
|
{
|
|
"email": "sales@globex-corp.example",
|
|
"expected": { "valid": true, "risk": "role" },
|
|
"note": "role account: sales@"
|
|
},
|
|
{
|
|
"email": "hr@initech-labs.example",
|
|
"expected": { "valid": true, "risk": "role" },
|
|
"note": "role account: hr@"
|
|
},
|
|
{
|
|
"email": "noreply@acme-widgets.example",
|
|
"expected": { "valid": true, "risk": "role" },
|
|
"note": "noreply (no hyphen) misses the no-reply@ placeholder pattern and classifies as a role account"
|
|
},
|
|
{
|
|
"email": "jane.doe@gmail.com",
|
|
"expected": { "valid": true, "risk": "free" },
|
|
"note": "consumer free-mail: gmail"
|
|
},
|
|
{
|
|
"email": "john@yahoo.co.uk",
|
|
"expected": { "valid": true, "risk": "free" },
|
|
"note": "consumer free-mail: regional yahoo"
|
|
},
|
|
{
|
|
"email": "pierre@orange.fr",
|
|
"expected": { "valid": true, "risk": "free" },
|
|
"note": "consumer free-mail: orange.fr"
|
|
},
|
|
{
|
|
"email": "li@qq.com",
|
|
"expected": { "valid": true, "risk": "free" },
|
|
"note": "consumer free-mail: qq.com"
|
|
},
|
|
{
|
|
"email": "sam@protonmail.com",
|
|
"expected": { "valid": true, "risk": "free" },
|
|
"note": "consumer free-mail: protonmail"
|
|
}
|
|
]
|
|
}
|