Files
getcargohq__cargo-skills/cargo-gtm/scripts/fixtures_email_validation.json
Auree Aubert 2792b990ed feat(gtm): executable QA layer — fixture-tested contact-accuracy scripts (#53)
* 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>
2026-07-10 00:16:01 -07:00

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"
}
]
}