--- a/rules/convert/comments.rules
+++ b/rules/convert/comments.rules
@@ -25,6 +25,11 @@
 ** 008 CA  20200412 Added incremental conversion support.
 ** 009 CA  20200428 Legacy DEFINE ENUM conversion support.
 ** 010 AOG 20250313 Escape unicode sequence if it has not already been escaped.
+** 011 PMP 20260812 Search past neighbor nodes which earlier conversion rules deleted from the
+**                 tree (e.g. adjacent string literals folded into a single node).  Their IDs are
+**                 still referenced by the shadow node linkage, so a comment surrounded by them
+**                 was reported as orphaned and dropped.  Added the adjacentLive and
+**                 stepOverDeleted functions and made findNonWS hop over the deleted nodes.
 */
  -->
  
@@ -164,7 +169,13 @@
    <variable name="attachidxs"   type="java.util.HashMap" />
    <variable name="count"        type="java.lang.Integer" />
    <variable name="i"            type="java.lang.Integer" />
-  
+
+   <!-- shadow node linkage refers to nodes by ID; conversion rules which run before this one may
+        have deleted some of those nodes from the tree (the most common case is the folding of
+        adjacent string literals into a single node).  A comment whose neighbors were both deleted
+        has to be resolved by searching past them, otherwise it is reported as orphaned and lost.
+        -->
+
    <func-library access="private">
    
       <!-- check the shadow node to the left to see if it is whitespace that
@@ -219,51 +230,147 @@
          
       </function>
       
+      <!-- the shadow node run which continues on the far side of the given node ID, which must be
+           the ID of a node that is no longer part of the tree; the ID still joins the two runs of
+           shadow nodes which surrounded the deleted node, so it can be used to keep searching in
+           the same direction; null is returned if there is nothing on the far side -->
+      <function name="stepOverDeleted">
+
+         <parameter name="ref"        type="com.goldencode.ast.Aast" />
+         <parameter name="deleted"    type="java.lang.Long" />
+         <parameter name="right"      type="java.lang.Boolean" />
+         <variable  name="local"      type="com.goldencode.ast.TreeLocal" />
+         <return    name="next"       type="com.goldencode.ast.ShadowNode" />
+
+         <rule>next = null</rule>
+         <rule>local = ref.getLocal()</rule>
+
+         <rule>local != null
+            <rule>right
+               <action>next = local.getRightShadowNode(deleted)</action>
+               <action on="false">next = local.getLeftShadowNode(deleted)</action>
+            </rule>
+         </rule>
+
+      </function>
+
+      <!-- the ID of the nearest non-shadow node which is still part of the tree, searching in the
+           given direction (true for right, false for left); nodes which earlier conversion rules
+           deleted from the tree are searched past rather than returned, since resolving them would
+           yield nothing usable; null is returned if no live node can be reached -->
+      <function name="adjacentLive">
+
+         <parameter name="shadow"     type="com.goldencode.ast.ShadowNode" />
+         <parameter name="right"      type="java.lang.Boolean" />
+         <variable  name="probe"      type="com.goldencode.ast.ShadowNode" />
+         <variable  name="cand"       type="java.lang.Long" />
+         <variable  name="node"       type="com.goldencode.ast.Aast" />
+         <return    name="liveid"     type="java.lang.Long" />
+
+         <rule>liveid = null</rule>
+         <rule>probe = shadow</rule>
+
+         <while>probe != null
+
+            <action>cand = probe.getAdjacentNonShadow(right)</action>
+
+            <rule>cand == null
+               <break/>
+            </rule>
+
+            <action>node = getAst(cand)</action>
+
+            <rule>node != null
+               <action>liveid = cand</action>
+               <break/>
+            </rule>
+
+            <!-- the neighbor is gone, resume the search on its far side -->
+            <action>
+               probe = execLib("stepOverDeleted", probe.node, cand, right)
+            </action>
+         </while>
+
+      </function>
+
       <!-- find the nearest non-whitespace node (null returned if none exist), 2nd parm is true
            for right, false for left, should return a comment (or other hidden non-ws node like
-           DOT) or a non-shadow node -->
+           DOT) or a non-shadow node which is still part of the tree -->
       <function name="findNonWS">
-      
+
          <parameter name="shadow"     type="com.goldencode.ast.ShadowNode" />
          <parameter name="right"      type="java.lang.Boolean" />
          <variable  name="ref"        type="com.goldencode.ast.Aast" />
+         <variable  name="probe"      type="com.goldencode.ast.ShadowNode" />
          <variable  name="lastid"     type="java.lang.Long" />
          <return    name="nonWSNode"  type="com.goldencode.ast.Aast" />
-         
+
          <rule>nonWSNode = null</rule>
-         
-         <rule>right
-            <action>ref = shadow.node.rightShadowNode</action>
-            <action on="false">ref = shadow.node.leftShadowNode</action>
-         </rule>
-         
-         <!-- walk the shadow nodes -->
-         <while>ref != null
-         
-            <!-- non-whitespace -->
+         <rule>probe = shadow</rule>
+
+         <!-- each pass scans one run of shadow nodes; if the run ends at a node which is no longer
+              part of the tree, the scan resumes with the run on its far side -->
+         <while>probe != null
+
+            <!-- after a hop the run can begin with the very node being searched for, which the
+                 scan below would step over; the node the search started from is never a match -->
             <rule>
-               ref.type != prog.ws         and
-               ref.type != prog.tilde      and
-               ref.type != prog.backslash
-               
-               <!-- done (we found a comment node) -->
-               <action>nonWSNode = ref</action>
+               probe.node.id != shadow.node.id  and
+               probe.node.type != prog.ws       and
+               probe.node.type != prog.tilde    and
+               probe.node.type != prog.backslash
+
+               <action>nonWSNode = probe.node</action>
                <break/>
             </rule>
-            
+
             <rule>right
-               <action>ref = ref.rightShadowNode</action>
-               <action on="false">ref = ref.leftShadowNode</action>
+               <action>ref = probe.node.rightShadowNode</action>
+               <action on="false">ref = probe.node.leftShadowNode</action>
             </rule>
-         </while>
-         
-         <rule>nonWSNode == null
-            <action>lastid = shadow.getAdjacentNonShadow(right)</action>
-            <rule>lastid != null
-               <action>nonWSNode = getAst(lastid)</action>
+
+            <!-- walk the shadow nodes -->
+            <while>ref != null
+
+               <!-- non-whitespace -->
+               <rule>
+                  ref.type != prog.ws         and
+                  ref.type != prog.tilde      and
+                  ref.type != prog.backslash
+
+                  <!-- done (we found a comment node) -->
+                  <action>nonWSNode = ref</action>
+                  <break/>
+               </rule>
+
+               <rule>right
+                  <action>ref = ref.rightShadowNode</action>
+                  <action on="false">ref = ref.leftShadowNode</action>
+               </rule>
+            </while>
+
+            <rule>nonWSNode != null
+               <break/>
             </rule>
-         </rule>
-         
+
+            <action>lastid = probe.getAdjacentNonShadow(right)</action>
+
+            <rule>lastid == null
+               <break/>
+            </rule>
+
+            <action>nonWSNode = getAst(lastid)</action>
+
+            <rule>nonWSNode != null
+               <break/>
+            </rule>
+
+            <!-- the run ended at a deleted node, resume on its far side -->
+            <action>
+               probe = execLib("stepOverDeleted", probe.node, lastid, right)
+            </action>
+         </while>
+
       </function>
                                              
       <!-- similar to findNonWS but this finishes its job when the end of the
@@ -325,7 +432,10 @@
             <action>lastid = shadow.getAdjacentNonShadow(right)</action>
             <rule>lastid != null
                <action>ref = getAst(lastid)</action>
-               <rule>ref.line == line
+               <!-- a node which is no longer part of the tree cannot be resolved, so its line is
+                    unknown; the search is not continued past it because anything on its far side
+                    is by definition not what is adjacent on this line -->
+               <rule>ref != null and ref.line == line
                   <action>nonWS = true</action>
                </rule>
             </rule>
@@ -936,7 +1046,7 @@
                   
                   <!-- non-whitespace to the left on the same line -->
                   <rule>attachId == -1 and leftNonWS != null
-                     <action>lastid = shadow.getAdjacentNonShadow(false)</action>
+                     <action>lastid = execLib("adjacentLive", shadow, false)</action>
                      <rule>lastid != null
                         <action>ref = execLib("getPeer", lastid)</action>
                         <rule>ref != null and shadow.node.line == ref.line
@@ -1043,12 +1153,15 @@
                   <rule>!curDone and attachId == -1
                   
                      <!-- right side first -->
-                     <action>lastid = shadow.getAdjacentNonShadow(true)</action>
-                     
+                     <action>lastid = execLib("adjacentLive", shadow, true)</action>
+
                      <!-- if the lastid does not match then there are non-whitespace shadow nodes
                           on our right side;  non-whitespace shadow nodes on the right tell us
                           that we are really associated with the left side  -->
-                     <rule>lastid != null and rightNonWS.id == lastid
+                     <rule>
+                        lastid != null      and
+                        rightNonWS != null  and
+                        rightNonWS.id == lastid
                      
                         <action>ref = execLib("getPeer", lastid)</action>
                         <rule>ref != null
@@ -1087,9 +1200,9 @@
                      <!-- no right side, try the left side as a last resort -->
                      <rule>attachId == -1
                         <action>
-                           lastid = shadow.getAdjacentNonShadow(false)
+                           lastid = execLib("adjacentLive", shadow, false)
                         </action>
-                        
+
                         <rule>lastid != null
                            <action>ref = execLib("getPeer", lastid)</action>
                            <rule>ref != null
@@ -1129,9 +1242,37 @@
                         </rule>
                         
                      </rule>
-                     
-                     <!-- no right or left side means that we have no way to
-                          determine an attach point, this comment will be
+
+                     <!-- both sides declined the comment, which happens when the neighbors on one
+                          side were deleted from the tree and the other side is a comment that has
+                          not been placed yet; anchor on the nearest live node in either direction
+                          instead of dropping the comment, the emitted position is no worse than
+                          the position of the code the comment was written next to -->
+                     <rule>attachId == -1
+                        <action>lastid = execLib("adjacentLive", shadow, true)</action>
+
+                        <rule>lastid == null
+                           <action>lastid = execLib("adjacentLive", shadow, false)</action>
+                        </rule>
+
+                        <rule>lastid != null
+                           <action>ref = execLib("getPeer", lastid)</action>
+                           <rule>ref != null
+                              <action>parRef = execLib("getPeerParent", ref)</action>
+                              <rule>parRef.parent != null
+                                 <action>attachId = parRef.parent.id</action>
+                                 <action>idx = parRef.indexPos</action>
+                              </rule>
+
+                              <!-- fall back to the in-line position BEFORE the node -->
+                              <action on="false">attachId = ref.parent.id</action>
+                              <action on="false">idx = ref.indexPos</action>
+                           </rule>
+                        </rule>
+                     </rule>
+
+                     <!-- no live node in either direction means that we have no
+                          way to determine an attach point, this comment will be
                           dropped -->
                      <rule>attachId == -1
                         <action>
--- a/src/com/goldencode/p2j/pattern/AstSymbolResolver.java
+++ b/src/com/goldencode/p2j/pattern/AstSymbolResolver.java
@@ -131,6 +131,8 @@
 **     DDF 20250814          Moved getPathToConversionFolder() to ArtifactManager.
 **     DDF 20251022          Renamed methods.
 **     DDF 20251208          Used wildcards for imports.
+** 041 PMP 20260812          Never reload a tree which is already registered.  Doing so replaced the
+**                           live nodes in the registry with stale copies read back from disk.
 */
 
 /*
@@ -292,6 +294,9 @@
    /** Map of ids to ASTs */
    private Map<Long, Aast> astMap = new HashMap<>();
    
+   /** Tree ids of the trees which have been registered in {@link #astMap} */
+   private Set<Long> registeredTrees = new HashSet<>();
+   
    /** Set of filtered results which define a view on the original AST */
    private Set<Aast> view = null;
    
@@ -380,6 +385,7 @@
 
       final Aast ast = AstManager.get().loadTree(artifact);
       resolver.registerTree(ast);
+      resolver.markTreeRegistered(ast);
       
       return ast;
    }
@@ -419,6 +425,15 @@
          return ast;
       }
       
+      // the enclosing tree is already registered, so the node was deleted from that tree during
+      // the current processing (e.g. a literal folded away by expression conversion);  reloading
+      // would register the stale nodes read back from the file in place of the live ones, which
+      // silently discards every annotation added since the tree was last persisted
+      if (resolver.isTreeRegistered(id))
+      {
+         return null;
+      }
+      
       // the AST was not found among those already loaded and registered; load the enclosing
       // tree and try again
       Artifact artifact = AstManager.get().getTreeArtifact(id);
@@ -1275,12 +1290,14 @@
          if (copyAst != null && copyAst.isRoot())
          {
             registerTree(copyAst);
+            markTreeRegistered(copyAst);
          }
          
          // The target nodes need to be "brought along" too.
          if (targetRootAst != null)
          {
             registerTree(targetRootAst);
+            markTreeRegistered(targetRootAst);
          }
       }
    }
@@ -1309,6 +1326,13 @@
     */
    public void deregisterTree(Aast root)
    {
+      Long rootId = root.getId();
+      
+      if (rootId != null)
+      {
+         registeredTrees.remove(AstManager.get().getTreeId(rootId));
+      }
+      
       Iterator<Aast> iter = root.iterator();
       while (iter.hasNext())
       {
@@ -1323,11 +1347,45 @@
    }
    
    /**
+    * Check whether the tree which encloses the node with the given ID has been registered with
+    * this resolver.  A node ID which belongs to a registered tree but which is absent from
+    * {@link #astMap} identifies a node that no longer exists in that tree.
+    *
+    * @param    id
+    *           The project-unique ID of an AST node.
+    *
+    * @return   <code>true</code> if the enclosing tree is registered.
+    */
+   public boolean isTreeRegistered(Long id)
+   {
+      return id != null && registeredTrees.contains(AstManager.get().getTreeId(id));
+   }
+   
+   /**
+    * Record that every node of the tree rooted at the given node has been registered with this
+    * resolver.  Only complete trees may be recorded;  registering a subtree leaves the rest of
+    * the enclosing tree unresolvable, which must still be handled by loading it from its file.
+    *
+    * @param    root
+    *           Root node of a complete tree which has just been registered.
+    */
+   private void markTreeRegistered(Aast root)
+   {
+      Long rootId = (root == null) ? null : root.getId();
+      
+      if (rootId != null)
+      {
+         registeredTrees.add(AstManager.get().getTreeId(rootId));
+      }
+   }
+   
+   /**
     * Clean up internal resources.
     */
    void cleanUp()
    {
       astMap = new HashMap<>();
+      registeredTrees = new HashSet<>();
       setRuleScope(engine.get());
    }
    
