From 6ba099f6df11409f68259fe8b218bf6d9e133ac1 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 28 Jul 2026 15:47:32 +0000 Subject: [PATCH] fix(leads): three failures that only showed up against real businesses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every one of these was invisible until campaigns ran against companies nobody had picked as a test case. A prospect whose address had to be guessed could not be saved at all. contact_source was constrained to the three ways an address could be found when the table was written; a search fallback and a guessed role address have been added since, and neither was storable. The failure was total rather than cosmetic — the whole row was rejected, so a business was discovered, crawled, searched for and then dropped with a constraint error in the run log. builtinla.com is in that log twice. The grounding guard rejected every draft to 4 Corner Resources for stating "4", which the campaign had indeed never mentioned. The guard checks numbers in the draft against everything the operator wrote, and the operator cannot write the recipient's name — it differs per prospect. Naming who you are writing to is not a claim about them, so the recipient's own host, domain and self-description now ground the check. An invented credential is still caught; that is the whole point of it. And a run that named eleven CTOs was refunded as though it had produced nothing. The produced-something test listed prospects, research, drafts and sends — every kind of output except the one a people-directory actually yields. Rendering, paginating and parsing a directory is the expensive part of the run, and it was being given away. Co-Authored-By: Claude Opus 5 (1M context) --- lib/outreach/pipeline.ts | 10 +++ lib/outreach/runner.ts | 13 +++- ...29050000_prospect_contact_source_guess.sql | 26 +++++++ tests/prod-regressions.test.ts | 70 +++++++++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 supabase/migrations/20260729050000_prospect_contact_source_guess.sql create mode 100644 tests/prod-regressions.test.ts diff --git a/lib/outreach/pipeline.ts b/lib/outreach/pipeline.ts index 5b8ef02b..5157d6a8 100644 --- a/lib/outreach/pipeline.ts +++ b/lib/outreach/pipeline.ts @@ -834,10 +834,20 @@ async function draftCustomEmail(input: { // Facts, plus the intro and ask — everything the operator authored counts // as grounded, or an ask that names a URL makes its own draft invalid. + // + // And the recipient's own identity, which the operator could not have + // written because it differs per prospect. Addressing a company called + // "4 Corner Resources" states the number four, and the guard rejected every + // draft to them for inventing it — the campaign, quite correctly, had never + // mentioned a four. Naming who you are writing to is not a claim about + // them; it is the minimum a personalised email does. const problems = unsupportedCustomClaims(output.body, [ ...facts, input.pitch.intro, input.pitch.ask ?? "", + host, + input.prospect.target_key, + recipient?.selfDescription ?? "", ]); if (problems.length) return { ok: false, problems }; diff --git a/lib/outreach/runner.ts b/lib/outreach/runner.ts index 28da64a5..55c16aba 100644 --- a/lib/outreach/runner.ts +++ b/lib/outreach/runner.ts @@ -329,7 +329,18 @@ export async function runEmailCampaignTick(campaign: CampaignRow): Promise { + const campaign = ["We build websites.", "Reply if useful."]; + + it("lets a draft name 4 Corner Resources", () => { + // The guard rejected every draft to them for stating "4" — which the + // campaign, quite correctly, had never mentioned. You cannot address the + // company without writing its name. + const body = "I came across 4 Corner Resources and had a thought."; + expect(unsupportedCustomClaims(body, [...campaign, "4cornerresources.com"])).toEqual([]); + }); + + it("still catches a number that is genuinely invented", () => { + // The guard has to keep earning its place: a fabricated credential is + // exactly what it exists to stop reaching a stranger. + const problems = unsupportedCustomClaims( + "We have 30 years of experience and 500 clients.", + [...campaign, "4cornerresources.com"], + ); + expect(problems.length).toBeGreaterThan(0); + expect(problems.join(" ")).toMatch(/30|500/); + }); + + it("still catches an invented link", () => { + expect( + unsupportedCustomClaims("See https://example.com/made-up", campaign).join(" "), + ).toMatch(/links to/); + }); +}); + +describe("every contact source the code emits is storable", () => { + // A prospect whose address had to be guessed was discovered, crawled, + // searched for, and then rejected wholesale by a check constraint that + // predated the guess path. builtinla.com is in the run log twice. + const cold = readFileSync(new URL("../lib/outreach/cold.ts", import.meta.url), "utf8"); + const migration = readFileSync( + new URL("../supabase/migrations/20260729050000_prospect_contact_source_guess.sql", import.meta.url), + "utf8", + ); + + it("the constraint covers the declared ContactCandidate sources", () => { + const declared = cold.match(/source:\s*("(?:mailto|text|manual|guess|search)"\s*\|?\s*)+/); + expect(declared).not.toBeNull(); + for (const source of ["mailto", "text", "manual", "guess"]) { + expect(migration).toContain(`'${source}'`); + } + }); +}); + +describe("people are output worth paying for", () => { + // A run against a people-directory names humans without necessarily adding + // a prospect. Leaving them out of the produced-something test refunded + // every such run: eleven CTOs rendered, paginated, parsed — billed as zero. + const runner = readFileSync(new URL("../lib/outreach/runner.ts", import.meta.url), "utf8"); + + it("counts recorded people when deciding whether to refund", () => { + const block = runner.slice(runner.indexOf("const producedSomething")); + expect(block.slice(0, 300)).toContain("result.peopleRecorded"); + }); + + it("still refunds a run that truly did nothing", () => { + expect(runner).toMatch(/if \(billing\.charged\(\) && !producedSomething\)/); + }); +});