57f2a4a958e5a6432829e33deaaf7b0475971188
braney
  Thu Sep 17 09:52:34 2026 -0700
quickLift: keep the target strand a reverse complemented protein just got

quickLiftPslBackToProtein turns the alignment over when pslTransMap hands back
strand[0] == '-', because a protein psl is only ever "++" or "+-".  pslRc does
that and makes the target strand explicit as it goes, so the psl comes out "+-".
The assignment at the end of the function then set strand[1] to '+' regardless
and undid it, leaving "++" over blocks that are in minus strand target
coordinates.

pslIsProtein wants strand[1] plus the three to one relation between the last
block and the target end, so it answered no.  pslTrack.c then passed a size
multiplier of one to lfFromPslx and every block was drawn at a third of its
length at a mirrored position, which is the symptom the reverse complement was
added to prevent.  The assignment is now the else branch of the same test.

hg/lib/tests/quickLiftTester.c covers this.  It goes through the public
quickLiftPsl rather than the static function, builds its chains in memory in the
shape quickLiftSourceRanges leaves them, and needs no database.  Six cases: a
protein over a same strand chain and over an opposite strand chain, a chain that
gaps only the reference, a chain that drops a source base and so splits a codon,
an mRNA over both chains, and an alignment with no chain under it.  Each one runs
pslCheck2, which is what notices a strand that disagrees with the blocks.  With
this fix backed out, only the opposite strand case changes and it reports three
errors placing the blocks outside the target range.

Found in the v504 code review, refs #38349.

refs #38249

diff --git src/hg/lib/quickLift.c src/hg/lib/quickLift.c
index 0adada5ee80..c2769612503 100644
--- src/hg/lib/quickLift.c
+++ src/hg/lib/quickLift.c
@@ -631,46 +631,51 @@
 {
 int i;
 
 if ((lifted->qStart % 3) || (lifted->qEnd % 3) || (lifted->qSize % 3) ||
     (lifted->qBaseInsert % 3))
     return FALSE;
 for (i = 0; i < lifted->blockCount; i++)
     if ((lifted->blockSizes[i] % 3) || (lifted->qStarts[i] % 3))
         return FALSE;
 
 // A protein psl always has its query on the forward strand, "++" or "+-".  pslTransMap can
 // hand back strand[0] == '-' (it reverse complements the input when the two alignments
 // disagree about the shared sequence's strand), and "-+" would tell pslShow to reverse
 // complement the protein as though it were DNA.  Turn it over so the minus lands on the
 // target side, where the protein display expects it.
+// pslRc makes the target strand explicit as it turns the alignment over, so the
+// assignment below is the other half of this test, not something to do as well.
 if (lifted->strand[0] == '-')
     pslRc(lifted);
+else
+    {
+    // A protein psl carries the target strand explicitly, and pslTransMap normalized the
+    // target onto the forward strand on the way out.
+    lifted->strand[1] = '+';
+    lifted->strand[2] = 0;
+    }
 
 lifted->qStart /= 3;
 lifted->qEnd /= 3;
 lifted->qSize /= 3;
 lifted->qBaseInsert /= 3;
 for (i = 0; i < lifted->blockCount; i++)
     {
     lifted->blockSizes[i] /= 3;
     lifted->qStarts[i] /= 3;
     }
-// A protein psl carries the target strand explicitly, and pslTransMap normalized the
-// target onto the forward strand on the way out.
-lifted->strand[1] = '+';
-lifted->strand[2] = 0;
 return TRUE;
 }
 
 struct psl *quickLiftPsl(struct hash *chainHash, struct hash **pMapPsls, struct psl *psl)
 // Map the target side of an alignment from the other assembly onto our current reference.
 // The query side (the mRNA, EST or protein the alignment is to) is left alone.  Returns
 // NULL if the alignment doesn't map.  pMapPsls points at a hash of mapping alignments the
 // caller keeps across a run of items; point it at a NULL hash to start.
 {
 struct chain *chain = liftOverChainForRange(chainHash, psl->tName, psl->tStart, psl->tEnd);
 if (chain == NULL)
     return NULL;
 
 struct psl *mapPsl = mapPslForChain(pMapPsls, chain);