Skip to content

Commit b6046ad

Browse files
nicklaunchesclaude
andcommitted
feat: say when a match came from an adjacent category
`match.widened` was stored and the proposal email disclosed it, but `MatchView` never carried the field, so `list_matches` and the dashboard both showed a bare category. A member paired one step out had no way to tell a deliberate widen from a bad recommendation, which is the opposite of the "we widen and say so" promise on the landing page and in rule §02. Adds `MatchView.widened` and renders it on both surfaces: the MCP tool tags the category "(adjacent to yours)" and adds a `widened:` line, the dashboard tags the Category fact "(adjacent)" and gives the reason as a sentence, because a bare tag is a label and the member needs the why in the same glance. Also fixes a mislabel this made visible. The dashboard fed `MatchRow` from `match.category`, which is `best.candidate.category` and therefore the partner's category only for the side that initiated the pairing. The other member saw their OWN category, so "(adjacent)" would have pointed at itself on one of the two dashboards for every widened match. It now reads `match.partner.category`, the same per-recipient field the email and the MCP tool already use. And words both explanations from the pair rather than "your category". Only the initiating side is measured against `WIDEN_BELOW`, so telling the other member their category was too thin is a claim we never checked and is false whenever they were the well-populated end of the pair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UJrDkXYhYpyp1cV9t4q1GY
1 parent 48319b2 commit b6046ad

4 files changed

Lines changed: 64 additions & 4 deletions

File tree

next-app/src/app/app/match-card.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,16 @@ import { TabList } from "@/components/web/tab-list";
4040
export type MatchRow = {
4141
matchId: string;
4242
state: string;
43+
/**
44+
* The PARTNER's category, which is NOT `MatchView.category`. See the
45+
* mapping in page.tsx for why the two differ on a widened match.
46+
*/
4347
category: string;
48+
/**
49+
* True when the pair came from an adjacent category. Shown rather than left
50+
* to inference: an unlabelled adjacent match just looks like a wrong one.
51+
*/
52+
widened: boolean;
4453
revealed: boolean;
4554
/** Present only when revealed. The type mirrors what the server sent. */
4655
partnerDomain: string | null;
@@ -118,8 +127,23 @@ export function MatchCard({ row }: { row: MatchRow }) {
118127

119128
<p className="mt-4 text-[14.5px] leading-relaxed">{row.partnerDescription}</p>
120129

130+
{/* The "(adjacent)" tag below is a label, not an explanation. Someone
131+
looking at a partner outside their own category needs the reason
132+
in the same glance, or the match reads as a mistake.
133+
134+
Worded from the pair, not from "your category". Only the side
135+
that initiated the pairing was measured against WIDEN_BELOW, so
136+
telling the other member their own category was thin is a claim
137+
we have not checked and is often false. */}
138+
{row.widened ? (
139+
<p className="text-muted mt-3 text-[14px] leading-relaxed">
140+
One of your two categories was too thin to pair inside it, so we widened by a single adjacent step
141+
rather than leave you both unmatched.
142+
</p>
143+
) : null}
144+
121145
<dl className="border-line mt-5 grid gap-px overflow-hidden rounded-sm border sm:grid-cols-3">
122-
<Fact label="Category" value={row.category} />
146+
<Fact label="Category" value={row.widened ? `${row.category} (adjacent)` : row.category} />
123147
<Fact
124148
label="Domain Rating"
125149
value={row.partnerDomainRating == null ? "Not measured" : String(row.partnerDomainRating)}

next-app/src/app/app/page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,13 @@ export default async function DashboardPage() {
7070
return {
7171
matchId: match.matchId,
7272
state: match.state,
73-
category: match.category,
73+
// The PARTNER's category, not the match's. `match.category` is
74+
// `best.candidate.category`, which for the side that did not
75+
// initiate the pairing is their own category — so "(adjacent)"
76+
// would be pointing at itself. The proposal email and the MCP tool
77+
// both read `partner.category` for the same reason.
78+
category: match.partner.category,
79+
widened: match.widened,
7480
revealed,
7581
partnerDomain,
7682
partnerDescription: match.partner.description,

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,13 +331,22 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
331331
return text(
332332
matches
333333
.map((m) => {
334+
// Say when a pair was widened. Unlabelled, an adjacent
335+
// match is indistinguishable from a wrong one, which is
336+
// exactly what house rule §02 promises not to do.
337+
const category = m.widened ? `${m.partner.category} (adjacent to yours)` : m.partner.category;
334338
const who =
335339
"domain" in m.partner
336-
? `${m.partner.domain} (${m.partner.email})`
337-
: `${m.partner.category}, DR ${m.partner.domainRating ?? "unrated"} (hidden until you both accept)`;
340+
? `${m.partner.domain} (${m.partner.email}), ${category}`
341+
: `${category}, DR ${m.partner.domainRating ?? "unrated"} (hidden until you both accept)`;
338342
return [
339343
`${m.matchId} [${m.state}] ${who}`,
340344
` ${m.partner.description}`,
345+
...(m.widened
346+
? [
347+
" widened: one of your two categories was too thin to pair inside, so we went one adjacent step out.",
348+
]
349+
: []),
341350
` wants: ${m.partner.wantedAnchors.join(", ") || "no anchors given"}`,
342351
` next: ${m.nextStep}`,
343352
].join("\n");

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,26 @@ export type MatchView = {
278278
matchId: string;
279279
state: MatchState;
280280
category: Category;
281+
/**
282+
* True when this pair came from an adjacent category, because the exact one
283+
* was too thin to pair inside.
284+
*
285+
* Exposed because the landing page and house rule §02 both promise we widen
286+
* "and say so", and a widened match is indistinguishable from a wrong one
287+
* unless we say which it is. The proposal email has always disclosed this;
288+
* every other surface showed a bare category and left the member to
289+
* conclude the matching was simply bad.
290+
*
291+
* A property of the PAIR, not of the viewer: it is stored as
292+
* `best.candidate.category !== category`, so it is true exactly when the two
293+
* sites sit in different categories, which reads the same from either end.
294+
* Both sides can therefore render it, and each side's "adjacent" means the
295+
* OTHER site's category. What is not symmetric is which category was thin:
296+
* only the side that initiated the pairing was measured against
297+
* `WIDEN_BELOW`, so no surface should tell a member their own category was
298+
* the thin one.
299+
*/
300+
widened: boolean;
281301
/** The viewer's own site in this match. */
282302
mySiteId: string;
283303
/** Masked before mutual accept, revealed after. */
@@ -344,6 +364,7 @@ async function buildMatchView(match: ExchangeMatch, viewerSiteIds: Set<string>):
344364
matchId: match.id,
345365
state,
346366
category: match.category,
367+
widened: match.widened,
347368
mySiteId,
348369
partner,
349370
revealed,

0 commit comments

Comments
 (0)