Skip to content

Commit 2f43b54

Browse files
authored
Hold matching until a site passes review, as /terms promises (#1)
autoPair filtered its candidates to active sites but never looked at the status of the site it was pairing FOR. Both submit surfaces call it right after commitSite, while the listing is still pending_review, so an unreviewed site could be proposed to a real member (and both sides emailed) before anyone had looked at it. /terms says exactly the opposite, and so did the comment in setSiteStatus. A non-active site now reports a pending_review reason instead of pairing, and both submit surfaces explain that matching starts on approval. Approval already re-runs autoPair, so nothing changes there.
1 parent 34ce676 commit 2f43b54

4 files changed

Lines changed: 29 additions & 8 deletions

File tree

next-app/src/app/submit/actions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,13 @@ async function describeAutoPair(site: Parameters<typeof autoPair>[0]): Promise<{
268268
outcome: `You already have a match: ${pair.partner.category}, DR ${dr}. It is waiting for you to accept or decline.`,
269269
};
270270
}
271+
if (pair.reason === "pending_review") {
272+
return {
273+
matched: false,
274+
outcome:
275+
"Matching starts the moment a human approves the listing. If a partner is waiting in your category, you will hear by email right then.",
276+
};
277+
}
271278
if (pair.reason === "first_in_category") {
272279
return {
273280
matched: false,

next-app/src/lib/mcp/tools.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -280,9 +280,11 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
280280
const pair = await autoPair(site);
281281
const tail = pair.matched
282282
? `\n\nYou already have a match: ${pair.partner.category}, DR ${pair.partner.domainRating ?? "unrated"}. Say "show my matches" to see it.`
283-
: pair.reason === "first_in_category"
284-
? `\n\nYou are the first site in ${pair.category}. That is a good position: the next member to join it is matched with you immediately.`
285-
: "\n\nNo partner available right now. You will be matched as soon as a suitable one joins.";
283+
: pair.reason === "pending_review"
284+
? "\n\nMatching starts the moment a human approves the listing. If a partner is waiting in your category, you will hear by email right then."
285+
: pair.reason === "first_in_category"
286+
? `\n\nYou are the first site in ${pair.category}. That is a good position: the next member to join it is matched with you immediately.`
287+
: "\n\nNo partner available right now. You will be matched as soon as a suitable one joins.";
286288

287289
return text(`${site.domain} is listed and pending review.${tail}`);
288290
}),

next-app/src/lib/services/matches.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export async function searchPartners(input: {
101101

102102
export type AutoPairResult =
103103
| { matched: true; match: ExchangeMatch; partner: MaskedPartner }
104-
| { matched: false; reason: "first_in_category" | "no_eligible_partner"; category: Category };
104+
| { matched: false; reason: "first_in_category" | "no_eligible_partner" | "pending_review"; category: Category };
105105

106106
/**
107107
* Finds and proposes the best available partner for a site, right now.
@@ -114,6 +114,17 @@ export type AutoPairResult =
114114
export async function autoPair(site: ExchangeSite): Promise<AutoPairResult> {
115115
const category = site.category;
116116

117+
// Only an active site may be proposed to anyone. Every filter below checks
118+
// the status of the CANDIDATES, so without this guard the subject slips
119+
// through: the submit flow calls autoPair the moment a listing is written,
120+
// while it is still `pending_review`, and a match with an unreviewed site
121+
// would go out (with both match-proposed emails) before a human had looked
122+
// at it. /terms says that never happens, so it must not. Approval re-runs
123+
// autoPair (see `setSiteStatus`), which is where a fresh site really pairs.
124+
if (site.status !== "active") {
125+
return { matched: false, reason: "pending_review", category };
126+
}
127+
117128
const [active] = await db()
118129
.select({ n: count() })
119130
.from(exchangeSites)

next-app/src/lib/services/sites.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,10 +226,11 @@ export async function listSitesForReview(status?: SiteStatus): Promise<SiteForRe
226226
* since the first migration and had no reader or writer until now.
227227
*
228228
* Approving MATCHES IMMEDIATELY. `autoPair` already runs on submit, but at that
229-
* point the site is `pending_review` and invisible to the matcher, so this is
230-
* the first moment it can actually pair with anyone. Without the call here an
231-
* approved site would sit idle until the Tuesday cron, which is a week of
232-
* silence at exactly the moment the member has just been told they are live.
229+
* point the site is `pending_review` and autoPair declines to pair it (that is
230+
* the review promise in /terms), so this is the first moment it can actually
231+
* pair with anyone. Without the call here an approved site would sit idle until
232+
* the Tuesday cron, which is a week of silence at exactly the moment the member
233+
* has just been told they are live.
233234
*
234235
* Pairing is awaited rather than fired and forgotten, because its own
235236
* `match-proposed` email should land after the approval email rather than

0 commit comments

Comments
 (0)