@@ -76,12 +76,85 @@ def _not_is_broken(a):
7676 return a in VIEWS or a in NODE_KIND
7777
7878
79+ def _not_composite_is_broken (a , b ):
80+ # `~(A & B)` and `~(A | B)`: negating a *composite* reaches the same wrappers
81+ # through the negation, so `~(A & view)` degenerates to `~A` and loses the
82+ # view entirely. Only a composite of two edge predicates survives.
83+ return _kind (a ) != "edge" or _kind (b ) != "edge"
84+
85+
7986def _ids (collection ):
8087 return frozenset (e .id for e in collection )
8188
8289
90+ def _view_references (graph ):
91+ """Each view atom spelled as the equivalent chained view."""
92+ return {
93+ "layer" : graph .layers (["work" ]),
94+ "layers2" : graph .layers (["work" , "friends" ]),
95+ "before" : graph .before (10 ),
96+ "after" : graph .after (8 ),
97+ "window" : graph .window (3 , 12 ),
98+ "at" : graph .at (10 ),
99+ "latest" : graph .latest (),
100+ "snap_at" : graph .snapshot_at (10 ),
101+ "snap_latest" : graph .snapshot_latest (),
102+ }
103+
104+
105+ # What each non-view atom selects, evaluated directly over the collection. Node
106+ # filters keep the edges whose *both* endpoints pass, which is how a node filter
107+ # reduces onto an edge.
108+ def _node_scores (graph , threshold ):
109+ return {
110+ node .name
111+ for node in graph .nodes
112+ if (node .properties .get ("score" ) or 0 ) > threshold
113+ }
114+
115+
116+ def _both_endpoints (graph , names ):
117+ return {
118+ edge .id
119+ for edge in graph .edges
120+ if edge .src .name in names and edge .dst .name in names
121+ }
122+
123+
124+ def _predicate_references (graph ):
125+ return {
126+ "edge_prop" : {
127+ e .id for e in graph .edges if (e .properties .get ("weight" ) or 0 ) > 5
128+ },
129+ "src" : {e .id for e in graph .edges if e .src .name == "a" },
130+ "dst" : {e .id for e in graph .edges if e .dst .name == "c" },
131+ "node_prop" : _both_endpoints (graph , _node_scores (graph , 15 )),
132+ "node_name" : _both_endpoints (graph , {"b" , "c" }),
133+ "is_valid" : {e .id for e in graph .edges if e .is_valid ()},
134+ "is_deleted" : {e .id for e in graph .edges if e .is_deleted ()},
135+ "is_active" : {e .id for e in graph .edges if e .is_active ()},
136+ "self_loop" : {e .id for e in graph .edges if e .src .name == e .dst .name },
137+ }
138+
139+
83140def _singles (graph ):
84- return {name : _ids (graph .edges [expr ]) for name , expr in _atoms ().items ()}
141+ """What each atom selects, computed *without* the subscript under test.
142+
143+ Reading these back through `graph.edges[atom]` would make the expectations
144+ below agree with the thing they are meant to check: where a single filter
145+ fails open, `EVERYTHING & X == X`, so a combination that dropped a term
146+ matches its expectation and the pins report a live bug as fixed. Views are
147+ referenced through the equivalent chained view instead, and predicates are
148+ evaluated over the collection directly.
149+ """
150+ views = _view_references (graph )
151+ singles = {
152+ name : frozenset (ids ) for name , ids in _predicate_references (graph ).items ()
153+ }
154+ singles .update ({name : _ids (view .edges ) for name , view in views .items ()})
155+ missing = set (_atoms ()) - set (singles )
156+ assert not missing , f"no independent reference for { sorted (missing )} "
157+ return singles
85158
86159
87160def _assert_discriminating (graph , single , names ):
@@ -157,6 +230,21 @@ def check(graph):
157230 cases .append ((f"{ a } & { b } " , atoms [a ] & atoms [b ], single [a ] & single [b ]))
158231 if not _or_is_broken (a , b ):
159232 cases .append ((f"{ a } | { b } " , atoms [a ] | atoms [b ], single [a ] | single [b ]))
233+ if not _not_composite_is_broken (a , b ):
234+ cases .append (
235+ (
236+ f"~({ a } & { b } )" ,
237+ ~ (atoms [a ] & atoms [b ]),
238+ every - (single [a ] & single [b ]),
239+ )
240+ )
241+ cases .append (
242+ (
243+ f"~({ a } | { b } )" ,
244+ ~ (atoms [a ] | atoms [b ]),
245+ every - (single [a ] | single [b ]),
246+ )
247+ )
160248 for a in atoms :
161249 if not _not_is_broken (a ):
162250 cases .append ((f"~{ a } " , ~ atoms [a ], every - single [a ]))
@@ -309,6 +397,17 @@ def check(graph):
309397 ~ atoms ["node_name" ],
310398 every - single ["node_name" ],
311399 ),
400+ # Negating a composite that contains a view: the pairwise rules
401+ # above only ever negate a single atom, so these shapes need their
402+ # own representatives.
403+ "not of an and containing a view loses the view" : (
404+ ~ (atoms ["edge_prop" ] & atoms ["layer" ]),
405+ every - (single ["edge_prop" ] & single ["layer" ]),
406+ ),
407+ "not of an or containing a view returns every edge" : (
408+ ~ (atoms ["edge_prop" ] | atoms ["layer" ]),
409+ every - (single ["edge_prop" ] | single ["layer" ]),
410+ ),
312411 }
313412 fixed = []
314413 for label , (expr , want ) in representatives .items ():
0 commit comments