diff --git a/server/routes/pedigree.js b/server/routes/pedigree.js index 2dfb29c..92548ef 100644 --- a/server/routes/pedigree.js +++ b/server/routes/pedigree.js @@ -57,7 +57,15 @@ function isDirectRelation(db, sireId, damId) { * calculateCOI(db, sireId, damId) * Wright Path Coefficient method. * Dogs included at gen 0 in their own maps so parent x offspring - * yields 25% COI. Per path: (0.5)^(sireGen + damGen + 1) + * yields ~25% COI. + * + * Fix: do NOT exclude sid/did from commonIds globally. + * - Exclude `did` from sireMap keys (the dam itself can't be a + * common ancestor of the sire's side for THIS pairing's offspring) + * - Exclude `sid` from damMap keys (same logic for sire) + * This preserves the case where the sire IS a common ancestor in the + * dam's ancestry (parent x offspring) while still avoiding reflexive + * self-loops. */ function calculateCOI(db, sireId, damId) { const sid = parseInt(sireId); @@ -65,8 +73,15 @@ function calculateCOI(db, sireId, damId) { const sireMap = getAncestorMap(db, sid); const damMap = getAncestorMap(db, did); + // Common ancestors: in BOTH maps, but: + // - not the dam itself appearing in sireMap (would be a loop) + // - not the sire itself appearing in damMap already handled below + // We collect all IDs present in both, excluding only the direct + // subjects (did from sireMap side, sid excluded already since we + // iterate sireMap keys — but sid IS in sireMap at gen 0, and if + // damMap also has sid, that is the parent×offspring case we WANT). const commonIds = [...sireMap.keys()].filter( - id => damMap.has(id) && id !== sid && id !== did + id => damMap.has(id) && id !== did ); let coi = 0; @@ -103,11 +118,11 @@ function calculateCOI(db, sireId, damId) { }; } -// ============================================================= +// ===================================================================== // IMPORTANT: Specific named routes MUST be registered BEFORE // the /:id wildcard, or Express will match 'relations' and // 'trial-pairing' as dog IDs and return 404/wrong data. -// ============================================================= +// ===================================================================== // POST /api/pedigree/trial-pairing router.post('/trial-pairing', (req, res) => { @@ -122,7 +137,7 @@ router.post('/trial-pairing', (req, res) => { const dam = db.prepare("SELECT * FROM dogs WHERE id = ? AND sex = 'female'").get(dam_id); if (!sire || !dam) { - return res.status(404).json({ error: 'Invalid sire or dam — check sex values in database' }); + return res.status(404).json({ error: 'Invalid sire or dam \u2014 check sex values in database' }); } const relation = isDirectRelation(db, sire_id, dam_id); @@ -153,9 +168,9 @@ router.get('/relations/:sireId/:damId', (req, res) => { } }); -// ============================================================= +// ===================================================================== // Wildcard routes last -// ============================================================= +// ===================================================================== // GET /api/pedigree/:id router.get('/:id', (req, res) => {