From db07ef0ade6f36754d0d3d5fe45cb55d6a0afd92 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Wed, 28 Oct 2026 23:23:27 +0000 Subject: [PATCH] build: judge stock enquiries by what is left, via @profullstack/form-guard (0.11.1) #74 dropped any idea matching a stock-enquiry regex, so a real buyer who opened with 'I have a few questions about pricing...' and then described an app was silently sent back with no invoice. Now an order is spam only when form-guard's substance() leaves nothing once the stock sentences and pleasantries are removed (or it is selling SEO/backlinks). Co-Authored-By: Claude Opus 5.5 --- bun.lock | 3 +++ package.json | 3 ++- src/build-invoice.ts | 30 +++++++++++++++++++----------- test/build-spam.test.ts | 29 +++++++++++++++++++++++++++++ test/pit.test.ts | 3 +++ 5 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 test/build-spam.test.ts diff --git a/bun.lock b/bun.lock index ed87a03..9547d1d 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ "@profullstack/affiliates": "0.2.0", "@profullstack/bridges": "0.2.0", "@profullstack/footer": "0.4.0", + "@profullstack/form-guard": "0.3.0", "@profullstack/hqtui": "^0.11.0", "@profullstack/openprofile": "0.2.0", "@profullstack/redeem-credits": "0.1.0", @@ -33,6 +34,8 @@ "@profullstack/footer": ["@profullstack/footer@0.4.0", "", { "peerDependencies": { "react": ">=18" }, "optionalPeers": ["react"], "bin": { "profullstack-footer": "bin/profullstack-footer.js" } }, "sha512-EJZnMUQmWDi9Gzx6BAjogloeaPFFSSCHtHJuC9dsXiOz8M87hfn6Z+Zx9AMYWk8s6xemPrPlkXKGB3PZugkcEA=="], + "@profullstack/form-guard": ["@profullstack/form-guard@0.3.0", "", {}, "sha512-/rn5xd4mMG6eXGe85BmPfhz7zz3mmsmNV7U6a1bNhATIpVtKKEUEGF4QPrFx8sg6YDtpFkzmvjNEaFJm3DbFxg=="], + "@profullstack/hqtui": ["@profullstack/hqtui@0.11.0", "", { "bin": { "hqtui": "bin/hqtui.mjs" } }, "sha512-kvpruktJsxwcQ8VYxdxDJn6dtPoWOMuOtr87DZbVV0KIrm7BBF5HRf37gA5Am/ReWd/ayEn9rvmHN6rVFTFALA=="], "@profullstack/keys": ["@profullstack/keys@0.1.0", "", {}, "sha512-Vys47LvS5ivhdZu4SYOxh9Le3DEmWhjP0+CtrU45RhXAbY0ZFG/2Dk9V1bIIUGDp8UWMJqctvUD21JrNjfh04w=="], diff --git a/package.json b/package.json index b8d85de..dcc9c93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pitworld", - "version": "0.11.0", + "version": "0.11.1", "mcpName": "world.pit/pit", "description": "pit.world MCP server: private web search and Moshpit name lookups for AI agents (stdio, no API key). Also a web app, API, CLI and TUI.", "license": "MIT", @@ -34,6 +34,7 @@ "@profullstack/affiliates": "0.2.0", "@profullstack/bridges": "0.2.0", "@profullstack/footer": "0.4.0", + "@profullstack/form-guard": "0.3.0", "@profullstack/hqtui": "^0.11.0", "@profullstack/openprofile": "0.2.0", "@profullstack/redeem-credits": "0.1.0", diff --git a/src/build-invoice.ts b/src/build-invoice.ts index e7da7d2..d740269 100644 --- a/src/build-invoice.ts +++ b/src/build-invoice.ts @@ -6,6 +6,7 @@ * for $2 that CoinPay repeats monthly from day 31. We keep nothing; the order * goes to the builder by email and the buyer goes to their $49 invoice. */ +import { scoreSubmission, substance } from '@profullstack/form-guard' import type { Mailer } from './newsletter.js' export const BUILD_USD = 49 @@ -40,20 +41,27 @@ export function readOrder(form: Record): BuildOrder { return { email, name: str('name', 80), idea } } +/** Selling to us, not buying: no idea that mentions these is an app. */ +const SELLING = /\b(guest post|backlinks?|seo services|rank (your|on) (website|google))\b/i +/** Stock enquiry sentences form-guard does not know (it knows the pricing/availability/process one). */ +const CANNED = [/interested in (scheduling|booking) a (brief|quick|short) (consultation|call)/i] + /** - * Contact-form spam: the stock "questions about pricing" notes that are sent to - * every form on the web. They describe no app, so they get no invoice. + * Contact-form spam: the stock "questions about pricing, availability, and the + * general process of working with you" note sent to every form on the web. + * It is spam only when nothing is left once its stock sentences and + * pleasantries are taken out (form-guard's substance()), so a real buyer who + * opens with the same line and then describes an app still gets an invoice. */ -const SPAM_IDEAS = [ - /questions? about (your )?(pricing|prices|availability)/i, - /general process of working with you/i, - /let me know what (details|information) you need/i, - /interested in (scheduling|booking) a (brief|quick|short) (consultation|call)/i, - /\b(guest post|backlinks?|seo services|rank (your|on) (website|google))\b/i, -] - export function looksLikeSpam(order: BuildOrder): boolean { - return SPAM_IDEAS.some((re) => re.test(order.idea)) + if (SELLING.test(order.idea)) return true + const canned = scoreSubmission({ message: order.idea }).signals.includes('contentless_request') || CANNED.some((re) => re.test(order.idea)) + if (!canned) return false + const rest = substance(order.idea) + .split(/(?<=[.!?])\s+/) + .filter((s) => !CANNED.some((re) => re.test(s))) + .join(' ') + return rest.length < 20 } export async function orderByInvoice(cfg: BuildInvoicing, order: BuildOrder, now = new Date()): Promise<{ build: Invoice; hosting: Invoice }> { diff --git a/test/build-spam.test.ts b/test/build-spam.test.ts new file mode 100644 index 0000000..30cb31f --- /dev/null +++ b/test/build-spam.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from 'bun:test' +import { looksLikeSpam } from '../src/build-invoice.js' + +const order = (idea: string) => ({ email: 'pat@example.com', name: 'Pat', idea }) +const template = + 'Hello, I have a few questions about pricing, availability, and the general process of working with you. Please let me know what details you need from me to proceed. Thanks!' + +describe('looksLikeSpam (build by invoice)', () => { + test('the stock pricing/availability/process note is spam (INV-043/044, 2026-10-28)', () => { + expect(looksLikeSpam(order(template))).toBe(true) + }) + + test('the stock consultation note is spam', () => { + expect(looksLikeSpam(order("Hello, I'm interested in scheduling a brief consultation. Thanks!"))).toBe(true) + }) + + test('selling SEO is spam', () => { + expect(looksLikeSpam(order('We sell backlinks and guest post placements'))).toBe(true) + }) + + test('a real buyer who opens with the stock line still gets an invoice', () => { + expect(looksLikeSpam(order(`${template}\nI want a booking page for my barbershop with SMS reminders.`))).toBe(false) + }) + + test('a short real idea is not spam', () => { + expect(looksLikeSpam(order('A todo app for my team'))).toBe(false) + expect(looksLikeSpam(order('A booking page for my barbershop\nwith reminders'))).toBe(false) + }) +}) diff --git a/test/pit.test.ts b/test/pit.test.ts index dae656f..3d896ae 100644 --- a/test/pit.test.ts +++ b/test/pit.test.ts @@ -612,6 +612,9 @@ describe('/build', () => { expect(spam.headers.get('location')).toBe('/build') expect(sent).toHaveLength(0) expect(mails).toHaveLength(0) + const seo = await post({ idea: 'We offer guest post and backlinks packages for your site', email: 'a@b.example', name: 'S' }, '203.0.113.11') + expect(seo.headers.get('location')).toBe('/build') + expect(sent).toHaveLength(0) const ok = await post({ idea: 'A booking page for my barbershop\nwith reminders', email: 'Pat@Example.com', name: 'Pat' }) expect(ok.status).toBe(302)