Coverage for oc_meta / run / merge / check_utils.py: 100%

23 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-07-25 10:39 +0000

1# SPDX-FileCopyrightText: 2026 Arcangelo Massari <arcangelo.massari@unibo.it> 

2# 

3# SPDX-License-Identifier: ISC 

4 

5from oc_meta.lib.sparql import execute_sparql 

6 

7PRO = "http://purl.org/spar/pro/" 

8OCO = "https://w3id.org/oc/ontology/" 

9 

10 

11def has_next_chain_issues(endpoint: str, br_pattern: str) -> list[str]: 

12 """Return corruption messages for the ``oco:hasNext`` chains of the agent 

13 roles held by the bibliographic resources matched by ``br_pattern``, a 

14 SPARQL group binding ``?br``. A healthy chain is a simple path: no cycles, 

15 at most one successor per role, at most one predecessor per role and one 

16 head per author/editor role type.""" 

17 issues: list[str] = [] 

18 

19 cycle_query = f""" 

20 PREFIX pro: <{PRO}> 

21 PREFIX oco: <{OCO}> 

22 SELECT DISTINCT ?ar WHERE {{ 

23 {br_pattern} 

24 ?br pro:isDocumentContextFor ?ar . 

25 ?ar oco:hasNext+ ?ar . 

26 }} 

27 """ 

28 results = execute_sparql(endpoint, cycle_query, max_retries=3, backoff_factor=1) 

29 for result in results["results"]["bindings"]: 

30 issues.append( 

31 f"Agent role {result['ar']['value']} is part of an oco:hasNext cycle" 

32 ) 

33 

34 fork_query = f""" 

35 PREFIX pro: <{PRO}> 

36 PREFIX oco: <{OCO}> 

37 SELECT ?ar (COUNT(DISTINCT ?next) AS ?count) WHERE {{ 

38 {br_pattern} 

39 ?br pro:isDocumentContextFor ?ar . 

40 ?ar oco:hasNext ?next . 

41 }} 

42 GROUP BY ?ar 

43 HAVING (COUNT(DISTINCT ?next) > 1) 

44 """ 

45 results = execute_sparql(endpoint, fork_query, max_retries=3, backoff_factor=1) 

46 for result in results["results"]["bindings"]: 

47 issues.append( 

48 f"Agent role {result['ar']['value']} has " 

49 f"{result['count']['value']} oco:hasNext successors" 

50 ) 

51 

52 shared_next_query = f""" 

53 PREFIX pro: <{PRO}> 

54 PREFIX oco: <{OCO}> 

55 SELECT ?next (COUNT(DISTINCT ?ar) AS ?count) WHERE {{ 

56 {br_pattern} 

57 ?br pro:isDocumentContextFor ?ar . 

58 ?ar oco:hasNext ?next . 

59 }} 

60 GROUP BY ?next 

61 HAVING (COUNT(DISTINCT ?ar) > 1) 

62 """ 

63 results = execute_sparql( 

64 endpoint, shared_next_query, max_retries=3, backoff_factor=1 

65 ) 

66 for result in results["results"]["bindings"]: 

67 issues.append( 

68 f"Agent role {result['next']['value']} has " 

69 f"{result['count']['value']} oco:hasNext predecessors" 

70 ) 

71 

72 disconnected_chain_query = f""" 

73 PREFIX pro: <{PRO}> 

74 PREFIX oco: <{OCO}> 

75 SELECT ?br ?roleType (COUNT(DISTINCT ?ar) AS ?count) WHERE {{ 

76 {br_pattern} 

77 ?br pro:isDocumentContextFor ?ar . 

78 ?ar pro:withRole ?roleType . 

79 FILTER (?roleType IN (pro:author, pro:editor)) 

80 FILTER NOT EXISTS {{ 

81 ?previous oco:hasNext ?ar . 

82 ?br pro:isDocumentContextFor ?previous . 

83 ?previous pro:withRole ?roleType . 

84 }} 

85 }} 

86 GROUP BY ?br ?roleType 

87 HAVING (COUNT(DISTINCT ?ar) > 1) 

88 """ 

89 results = execute_sparql( 

90 endpoint, disconnected_chain_query, max_retries=3, backoff_factor=1 

91 ) 

92 for result in results["results"]["bindings"]: 

93 role_type = result["roleType"]["value"].split("/")[-1] 

94 issues.append( 

95 f"Bibliographic resource {result['br']['value']} has " 

96 f"{result['count']['value']} disconnected {role_type} chains" 

97 ) 

98 

99 return issues