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
« 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
5from oc_meta.lib.sparql import execute_sparql
7PRO = "http://purl.org/spar/pro/"
8OCO = "https://w3id.org/oc/ontology/"
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] = []
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 )
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 )
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 )
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 )
99 return issues