1010 from hyperbase .hyperedge import Atom , Hyperedge
1111
1212
13- def check_correctness (edge : Hyperedge ) -> dict [Hyperedge , list [tuple [str , str ]]]:
14- """Check correctness of a hyperedge, returning errors keyed by subedge."""
13+ def check_correctness (edge : Hyperedge ) -> dict [Hyperedge , list [tuple [str , str , int ]]]:
14+ """Check correctness of a hyperedge, returning errors keyed by subedge.
15+
16+ Each error is ``(code, message, severity)``. Correctness failures are hard
17+ grammar violations, so they all carry severity ``0`` (the most serious).
18+ """
1519 if edge .atom :
1620 return _check_atom (edge ) # type: ignore[arg-type]
1721 return _check_edge (edge )
1822
1923
20- def _check_atom (atom : Atom ) -> dict [Hyperedge , list [tuple [str , str ]]]:
21- output : dict [Hyperedge , list [tuple [str , str ]]] = {}
24+ def _check_atom (atom : Atom ) -> dict [Hyperedge , list [tuple [str , str , int ]]]:
25+ output : dict [Hyperedge , list [tuple [str , str , int ]]] = {}
2226 errors : list [tuple [str , str ]] = []
2327
2428 at = atom .mtype ()
@@ -30,16 +34,16 @@ def _check_atom(atom: Atom) -> dict[Hyperedge, list[tuple[str, str]]]:
3034 EdgeType .TRIGGER ,
3135 EdgeType .CONJUNCTION ,
3236 }:
33- errors .append (("bad-atom-type" , f"{ at } is not a valid atom type" ))
37+ errors .append (("bad-atom-type" , f"atom ' { atom } ' has invalid type { at } " ))
3438
3539 if len (errors ) > 0 :
36- output [atom ] = errors
40+ output [atom ] = [( code , msg , 0 ) for code , msg in errors ]
3741
3842 return output
3943
4044
41- def _check_edge (edge : Hyperedge ) -> dict [Hyperedge , list [tuple [str , str ]]]:
42- output : dict [Hyperedge , list [tuple [str , str ]]] = {}
45+ def _check_edge (edge : Hyperedge ) -> dict [Hyperedge , list [tuple [str , str , int ]]]:
46+ output : dict [Hyperedge , list [tuple [str , str , int ]]] = {}
4347 errors : list [tuple [str , str ]] = []
4448
4549 ct = edge [0 ].mtype ()
@@ -51,40 +55,54 @@ def _check_edge(edge: Hyperedge) -> dict[Hyperedge, list[tuple[str, str]]]:
5155 EdgeType .TRIGGER ,
5256 EdgeType .CONJUNCTION ,
5357 }:
54- errors .append (("conn-bad-type" , f"connector has incorrect type: { ct } " ))
58+ errors .append (
59+ (
60+ "conn-bad-type" ,
61+ f"connector '{ edge [0 ]} ' of '{ edge } ' has incorrect type: { ct } " ,
62+ )
63+ )
5564 # check if modifier structure is correct
5665 if ct == EdgeType .MODIFIER :
5766 if len (edge ) != 2 :
58- errors .append (("mod-1-arg" , "modifiers can only have one argument" ))
67+ errors .append (
68+ ("mod-1-arg" , f"modifier edge '{ edge } ' can only have one argument" )
69+ )
5970 # check if builder structure is correct
6071 elif ct == EdgeType .BUILDER :
6172 if len (edge ) != 3 :
62- errors .append (("build-2-args" , "builders can only have two arguments" ))
73+ errors .append (
74+ ("build-2-args" , f"builder edge '{ edge } ' can only have two arguments" )
75+ )
6376 for arg in edge [1 :]:
6477 at = arg .mtype ()
6578 if at != EdgeType .CONCEPT :
66- e = f"builder argument { arg !s } has incorrect type: { at } "
79+ e = f"builder argument ' { arg } ' of ' { edge } ' has incorrect type: { at } "
6780 errors .append (("build-arg-bad-type" , e ))
6881 # check if trigger structure is correct
6982 elif ct == EdgeType .TRIGGER :
7083 if len (edge ) != 2 :
71- errors .append (("trig-1-arg" , "triggers can only have one arguments" ))
84+ errors .append (
85+ ("trig-1-arg" , f"trigger edge '{ edge } ' can only have one argument" )
86+ )
7287 for arg in edge [1 :]:
7388 at = arg .mtype ()
7489 if at not in {EdgeType .CONCEPT , EdgeType .RELATION }:
75- e = f"trigger argument { arg !s } has incorrect type: { at } "
90+ e = f"trigger argument ' { arg } ' of ' { edge } ' has incorrect type: { at } "
7691 errors .append (("trig-bad-arg-type" , e ))
7792 # check if predicate structure is correct
7893 elif ct == EdgeType .PREDICATE :
7994 for arg in edge [1 :]:
8095 at = arg .mtype ()
8196 if at not in {EdgeType .CONCEPT , EdgeType .RELATION , EdgeType .SPECIFIER }:
82- e = f"predicate argument { arg !s } has incorrect type: { at } "
97+ e = f"predicate argument ' { arg } ' of ' { edge } ' has incorrect type: { at } "
8398 errors .append (("pred-arg-bad-type" , e ))
8499 # check if conjunction structure is correct
85100 elif ct == EdgeType .CONJUNCTION and len (edge ) < 3 :
86101 errors .append (
87- ("conj-2-args-min" , "conjunctions must have at least two arguments" )
102+ (
103+ "conj-2-args-min" ,
104+ f"conjunction edge '{ edge } ' must have at least two arguments" ,
105+ )
88106 )
89107
90108 # check argrole counts
@@ -98,8 +116,8 @@ def _check_edge(edge: Hyperedge) -> dict[Hyperedge, list[tuple[str, str]]]:
98116 errors .append (
99117 (
100118 "pred-bad-arg-role" ,
101- f"{ ar } is not a valid argument role "
102- "for connector of type P " ,
119+ f"' { ar } ' is not a valid argument role for "
120+ f"predicate connector ' { edge [ 0 ] } ' in ' { edge } ' " ,
103121 )
104122 )
105123 elif ct == EdgeType .BUILDER :
@@ -108,16 +126,17 @@ def _check_edge(edge: Hyperedge) -> dict[Hyperedge, list[tuple[str, str]]]:
108126 errors .append (
109127 (
110128 "build-bad-arg-role" ,
111- f"{ ar } is not a valid argument role "
112- "for connector of type B " ,
129+ f"' { ar } ' is not a valid argument role for "
130+ f"builder connector ' { edge [ 0 ] } ' in ' { edge } ' " ,
113131 )
114132 )
115133
116134 if len (ars ) != len (edge ) - 1 :
117135 errors .append (
118136 (
119137 "bad-num-argroles" ,
120- "number of argroles must match number of arguments" ,
138+ f"number of argroles on connector '{ edge [0 ]} ' must match "
139+ f"number of arguments in '{ edge } '" ,
121140 )
122141 )
123142
@@ -127,24 +146,140 @@ def _check_edge(edge: Hyperedge) -> dict[Hyperedge, list[tuple[str, str]]]:
127146 errors .append (
128147 (
129148 f"argrole-{ role } -1-max" ,
130- f"argrole { role } can only be used once" ,
149+ f"argrole ' { role } ' can only be used once in ' { edge } ' " ,
131150 )
132151 )
133152 else :
134153 errors .append (
135154 (
136155 "no-argroles" ,
137- "Connectors of type P or B must have argument roles" ,
156+ f"connector '{ edge [0 ]} ' of '{ edge } ' (type P or B) must have "
157+ "argument roles" ,
138158 )
139159 )
140160 except RuntimeError :
141161 # malformed edges are detected elsewhere
142162 pass
143163
144164 if len (errors ) > 0 :
145- output [edge ] = errors
165+ output [edge ] = [( code , msg , 0 ) for code , msg in errors ]
146166
147167 for subedge in edge :
148168 output .update (check_correctness (subedge ))
149169
150170 return output
171+
172+
173+ def check_structural_quality (
174+ edge : Hyperedge ,
175+ ) -> dict [Hyperedge , list [tuple [str , str , int ]]]:
176+ """Soft, edge-only quality checks on argroles, modifiers and junctions.
177+
178+ Unlike :func:`check_correctness` (hard grammar rules), these flag
179+ structures that are valid but unidiomatic. The third tuple value is a
180+ severity (lower is worse): ``2`` for argrole problems, ``3`` for modifier
181+ and junction issues.
182+ """
183+ errors : dict [Hyperedge , list [tuple [str , str , int ]]] = {}
184+
185+ def _visit (current_edge : Hyperedge ) -> None :
186+ if not current_edge or current_edge .atom :
187+ return
188+
189+ current_errors : list [tuple [str , str , int ]] = []
190+
191+ # Argrole checks
192+ try :
193+ ars = current_edge .argroles ()
194+ ar_counts : Counter [str ] = Counter ()
195+ for ar in ars :
196+ if ar not in "masox" :
197+ current_errors .append (
198+ (
199+ "bad-argrole" ,
200+ f"Bad argument role '{ ar } ' on connector "
201+ f"'{ current_edge [0 ]} ' in '{ current_edge } '. "
202+ "Should be one of 'masox'." ,
203+ 2 ,
204+ )
205+ )
206+ ar_counts [ar ] += 1
207+ except Exception :
208+ pass
209+
210+ # Modifier checks
211+ try :
212+ connector_type = current_edge [0 ].type ()
213+ if len (current_edge ) >= 2 :
214+ target_mt = current_edge [1 ].mt
215+ if connector_type in {"Ma" , "Md" , "Mq" , "Mp" } and target_mt != "C" :
216+ current_errors .append (
217+ (
218+ f"bad-{ connector_type .lower ()} -target" ,
219+ f"Modifier '{ current_edge } ' of type '{ connector_type } ' "
220+ "should only be applied to concepts (type 'C'), but its "
221+ f"target '{ current_edge [1 ]} ' has type '{ target_mt } '." ,
222+ 3 ,
223+ )
224+ )
225+ elif connector_type == "Mm" and target_mt != "P" :
226+ current_errors .append (
227+ (
228+ "bad-mm-target" ,
229+ f"Modifier '{ current_edge } ' of type 'Mm' should only be "
230+ "applied to predicates (type 'P'), but its target "
231+ f"'{ current_edge [1 ]} ' has type '{ target_mt } '." ,
232+ 3 ,
233+ )
234+ )
235+ elif connector_type == "Mn" and target_mt not in {"C" , "P" }:
236+ current_errors .append (
237+ (
238+ "bad-mn-target" ,
239+ f"Modifier '{ current_edge } ' of type 'Mn' should only be "
240+ "applied to concepts (type 'C') or predicates (type 'P'), "
241+ f"but its target '{ current_edge [1 ]} ' has type "
242+ f"'{ target_mt } '." ,
243+ 3 ,
244+ )
245+ )
246+ except Exception :
247+ pass
248+
249+ # Junction checks
250+ try :
251+ if current_edge [0 ].mt == "J" :
252+ types = {child .mt for child in current_edge [1 :]}
253+ if str (current_edge [0 ]) == ":/J/." :
254+ if types != {"R" } and types != {"C" } and types != {"C" , "R" }:
255+ current_errors .append (
256+ (
257+ "bad-colon-junction-types" ,
258+ f"Arguments of junction '{ current_edge } ' should "
259+ "ideally be of either type 'R' or 'C'." ,
260+ 3 ,
261+ )
262+ )
263+ else :
264+ if len (types ) > 1 and types != {"C" , "R" }:
265+ current_errors .append (
266+ (
267+ "bad-junction-types" ,
268+ f"Arguments of junction '{ current_edge } ' should "
269+ "ideally be all of the same type or a combination of "
270+ "'R' and 'C' types." ,
271+ 3 ,
272+ )
273+ )
274+ except Exception :
275+ pass
276+
277+ if current_errors :
278+ errors [current_edge ] = current_errors
279+
280+ for child in current_edge :
281+ _visit (child )
282+
283+ if edge :
284+ _visit (edge )
285+ return errors
0 commit comments