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
+38
View File
@@ -51,6 +51,44 @@ async def test_deceased_preview_and_apply(client):
assert old not in [r["person_id"] for r in prev2]
async def test_gender_from_spouse_preview_and_apply(client):
h, tid = await _tree(client, "cl-spouse@example.com")
husband = (
await client.post(
f"/api/v1/trees/{tid}/persons",
json={"given": "Otto", "surname": "Frey", "gender": "male"},
headers=h,
)
).json()["id"]
wife = await _person(client, h, tid, "Bea", "Frey") # no sex
loner = await _person(client, h, tid, "Nyx", "Alone") # no sex, no partner
await client.post(
f"/api/v1/trees/{tid}/relationships",
json={"type": "partnership", "person_from_id": husband, "person_to_id": wife},
headers=h,
)
prev = (await client.get(f"/api/v1/trees/{tid}/cleanup/gender/from-spouse", headers=h)).json()
by = {r["person_id"]: r["proposed_gender"] for r in prev}
assert by.get(wife) == "female" # opposite of the confirmed-male husband
assert loner not in by # no known-sex partner → not proposed
assert husband not in by # already has a sex
r = await client.post(
f"/api/v1/trees/{tid}/cleanup/gender",
json={"updates": [{"person_id": wife, "gender": "female"}]},
headers=h,
)
assert r.status_code == 200 and r.json()["updated"] == 1
assert (
await client.get(f"/api/v1/trees/{tid}/persons/{wife}", headers=h)
).json()["gender"] == "female"
# Once set, the wife is no longer proposed.
prev2 = (await client.get(f"/api/v1/trees/{tid}/cleanup/gender/from-spouse", headers=h)).json()
assert wife not in [r["person_id"] for r in prev2]
GED = b"""0 HEAD
0 @I1@ INDI
1 NAME Josias /Moody/