@@ -530,14 +530,64 @@ def _add_edge(source: str, target: str, edge_type: str, label: str | None = None
530530 else :
531531 _add_edge (str (disj ), node_iri , "disjointWith" , "disjointWith" )
532532
533+ # Extract seeAlso targets from OWL restrictions on rdfs:seeAlso
534+ def _get_see_also_targets (uri : URIRef ) -> list [URIRef ]:
535+ """Extract seeAlso targets from both direct triples and OWL restrictions.
536+
537+ FOLIO encodes seeAlso as owl:Restriction with owl:someValuesFrom
538+ inside rdfs:subClassOf, not as direct rdfs:seeAlso triples.
539+ """
540+ targets : list [URIRef ] = []
541+ # Direct rdfs:seeAlso triples
542+ for obj in graph .objects (uri , RDFS .seeAlso ):
543+ if isinstance (obj , URIRef ):
544+ targets .append (obj )
545+ # OWL restrictions: subClassOf -> Restriction(onProperty=seeAlso, someValuesFrom=X)
546+ for sc in graph .objects (uri , RDFS .subClassOf ):
547+ if isinstance (sc , URIRef ):
548+ continue # Named superclass, not a restriction
549+ # sc is a blank node (restriction)
550+ on_prop = next (graph .objects (sc , OWL .onProperty ), None )
551+ if on_prop == RDFS .seeAlso :
552+ for val in graph .objects (sc , OWL .someValuesFrom ):
553+ if isinstance (val , URIRef ):
554+ targets .append (val )
555+ for val in graph .objects (sc , OWL .allValuesFrom ):
556+ if isinstance (val , URIRef ):
557+ targets .append (val )
558+ for val in graph .objects (sc , OWL .hasValue ):
559+ if isinstance (val , URIRef ):
560+ targets .append (val )
561+ return targets
562+
563+ def _get_see_also_referrers (uri : URIRef ) -> list [URIRef ]:
564+ """Find classes that have seeAlso restrictions pointing TO this URI."""
565+ referrers : list [URIRef ] = []
566+ # Direct reverse rdfs:seeAlso
567+ for subj in graph .subjects (RDFS .seeAlso , uri ):
568+ if isinstance (subj , URIRef ):
569+ referrers .append (subj )
570+ # Find restrictions that someValuesFrom -> uri
571+ for restriction in graph .subjects (OWL .someValuesFrom , uri ):
572+ on_prop = next (graph .objects (restriction , OWL .onProperty ), None )
573+ if on_prop == RDFS .seeAlso :
574+ for cls in graph .subjects (RDFS .subClassOf , restriction ):
575+ if isinstance (cls , URIRef ) and (cls , RDF .type , OWL .Class ) in graph :
576+ referrers .append (cls )
577+ return referrers
578+
533579 # Collect seeAlso cross-links
580+ # Outgoing seeAlso: checked on all visited nodes (focus + ancestors)
581+ # Incoming seeAlso: only checked on the focus node (intermediates are too noisy)
534582 see_also_nodes : list [URIRef ] = []
535583 if include_see_also :
536584 for node_iri in list (visited .keys ()):
537585 node_uri = URIRef (node_iri )
538586 sa_count = 0
539- for related in graph .objects (node_uri , RDFS .seeAlso ):
540- if not isinstance (related , URIRef ) or sa_count >= max_see_also_per_node :
587+
588+ # Outgoing: this node seeAlso -> related
589+ for related in _get_see_also_targets (node_uri ):
590+ if sa_count >= max_see_also_per_node :
541591 break
542592 related_iri = str (related )
543593 was_new = related_iri not in visited
@@ -546,12 +596,24 @@ def _add_edge(source: str, target: str, edge_type: str, label: str | None = None
546596 if related_node is None :
547597 continue
548598 see_also_nodes .append (related )
549- if node_iri < related_iri :
550- _add_edge (node_iri , related_iri , "seeAlso" , "rdfs:seeAlso" )
551- else :
552- _add_edge (related_iri , node_iri , "seeAlso" , "rdfs:seeAlso" )
599+ _add_edge (node_iri , related_iri , "seeAlso" , "rdfs:seeAlso" )
553600 sa_count += 1
554601
602+ # Incoming: only on the focus node to avoid cascade
603+ if node_uri == class_uri :
604+ for referrer in _get_see_also_referrers (node_uri ):
605+ if sa_count >= max_see_also_per_node :
606+ break
607+ referrer_iri = str (referrer )
608+ was_new = referrer_iri not in visited
609+ if was_new :
610+ referrer_node = _make_node (referrer , 0 )
611+ if referrer_node is None :
612+ continue
613+ see_also_nodes .append (referrer )
614+ _add_edge (referrer_iri , node_iri , "seeAlso" , "rdfs:seeAlso" )
615+ sa_count += 1
616+
555617 # BFS upward from seeAlso nodes to their roots
556618 if see_also_nodes :
557619 sa_queue : list [tuple [URIRef , int ]] = [(u , 0 ) for u in see_also_nodes ]
@@ -572,6 +634,12 @@ def _add_edge(source: str, target: str, edge_type: str, label: str | None = None
572634 sa_visited .add (parent_iri )
573635 sa_queue .append ((parent , current_depth + 1 ))
574636
637+ # Reclassify roots: primary roots (from subClassOf BFS) stay "root",
638+ # roots discovered via seeAlso branches become "secondary_root"
639+ for node in visited .values ():
640+ if node .node_type == "root" and node .iri not in ancestor_visited :
641+ node .node_type = "secondary_root"
642+
575643 truncated = total_discovered [0 ] > len (visited )
576644
577645 return EntityGraphResponse (
0 commit comments