Cleanup: infer a missing sex from a known-sex spouse (preview → approve)

Unset sex renders blue (male-colored), which is misleading next to a confirmed
male partner. Add a Cleanup action that proposes the opposite sex for an unset
partner of someone whose sex is set (couples are opposite-sex in practice — a
confirmed-male husband ⇒ a female wife). People whose known partners disagree
are skipped as ambiguous.

It's a preview the user reviews and approves in the Cleanup tool (reusing the
existing gender apply path + audit) — not an autonomous write. Backend:
guess_gender_by_spouse + GET /cleanup/gender/from-spouse. Frontend: an "Infer
from spouse" button feeding the existing proposal list. Test covers
propose-opposite, skip-no-partner, skip-already-set, apply, and re-preview.

Full suite 73 passed; frontend build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
2026-06-09 10:59:08 -04:00
parent 768c68cbe0
commit 05d2773e25
6 changed files with 211 additions and 2 deletions
+16 -2
View File
@@ -109,6 +109,15 @@ export default function CleanupPage() {
setGenSel(new Set((data ?? []).map((g) => g.person_id)));
}
async function guessGenderFromSpouse() {
setGenMsg(null);
const { data } = await api.GET("/api/v1/trees/{tree_id}/cleanup/gender/from-spouse", {
params: { path: { tree_id: treeId } },
});
setGender(data ?? []);
setGenSel(new Set((data ?? []).map((g) => g.person_id)));
}
async function applyGender() {
const updates = (gender ?? [])
.filter((g) => genSel.has(g.person_id))
@@ -246,10 +255,15 @@ export default function CleanupPage() {
<Button variant="outline" onClick={guessGender}>
Guess from first name
</Button>
<Button variant="outline" onClick={guessGenderFromSpouse}>
Infer from spouse
</Button>
</div>
<p className="text-xs text-[var(--muted)]">
Guess from first name uses a built-in name dictionary for people with no sex set;
ambiguous names (Marion, Frances, ) are left for you to decide.
Guess from first name uses a built-in name dictionary for people with no sex set.
Infer from spouse sets the opposite sex for an unset partner of someone whose sex is
known (e.g. a confirmed-male husband a female wife) review before applying, since
it assumes opposite-sex couples.
</p>
{genMsg && <p className="text-sm text-bronze">{genMsg}</p>}
{gender && (
+51
View File
@@ -734,6 +734,26 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/trees/{tree_id}/cleanup/gender/from-spouse": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Guess Gender From Spouse
* @description Infer a missing sex from a partner whose sex is set (opposite-sex couple).
*/
get: operations["guess_gender_from_spouse_api_v1_trees__tree_id__cleanup_gender_from_spouse_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/trees/{tree_id}/cleanup/gender": {
parameters: {
query?: never;
@@ -3687,6 +3707,37 @@ export interface operations {
};
};
};
guess_gender_from_spouse_api_v1_trees__tree_id__cleanup_gender_from_spouse_get: {
parameters: {
query?: never;
header?: never;
path: {
tree_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["GenderProposal"][];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
apply_gender_api_v1_trees__tree_id__cleanup_gender_post: {
parameters: {
query?: never;
+48
View File
@@ -2915,6 +2915,54 @@
}
}
},
"/api/v1/trees/{tree_id}/cleanup/gender/from-spouse": {
"get": {
"tags": [
"cleanup"
],
"summary": "Guess Gender From Spouse",
"description": "Infer a missing sex from a partner whose sex is set (opposite-sex couple).",
"operationId": "guess_gender_from_spouse_api_v1_trees__tree_id__cleanup_gender_from_spouse_get",
"parameters": [
{
"name": "tree_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Tree Id"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GenderProposal"
},
"title": "Response Guess Gender From Spouse Api V1 Trees Tree Id Cleanup Gender From Spouse Get"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/v1/trees/{tree_id}/cleanup/gender": {
"post": {
"tags": [