kibi-core 0.22.5 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,783 @@
1
+ % Module: certificate_check
2
+ % implements REQ-kibi-kb-certificates
3
+ %
4
+ % The independent checker behind `kibi verify`. It states each KB theorem
5
+ % over the facts current requirements name and re-checks a certificate for
6
+ % it. It never searches: a model is checked by evaluating every constraint
7
+ % against the model's value, a contradiction core by re-running the interval
8
+ % decision procedure on the core alone (plus one model per dropped fact for
9
+ % minimality), and a stratification by checking every dependency edge
10
+ % against the stratum numbers. It does not load checks.pl or the emitter
11
+ % (certificates.pl), so a bug in the search that finds certificates cannot
12
+ % make a wrong certificate pass.
13
+ %
14
+ % Theorems and their certificates (kibi.certificates.v1):
15
+ %
16
+ % properties_consistent One property_model per (subject, property, scope
17
+ % context): a value that satisfies every fact in the
18
+ % context. Otherwise a property_contradiction (an
19
+ % irreducible core with one model per dropped fact),
20
+ % or property_unresolved when the core's validity
21
+ % windows never overlap.
22
+ % predicates_consistent One predicate_model per ground atom (true or
23
+ % false), or a predicate_contradiction pair.
24
+ % decisions_consistent One decision_model per decision key (allow or
25
+ % deny), or a decision_contradiction pair.
26
+ % rules_stratified A rule_strata assignment, or a rule_negation_cycle.
27
+ %
28
+ % A context is a set of facts on one subject and property that can be
29
+ % compared: an unscoped fact belongs to every scope context, a scoped fact
30
+ % only to its own. A context the checker cannot compare (mixed value types,
31
+ % mixed units, an unsupported operator or polarity) is unresolved and needs
32
+ % no certificate; it never counts as consistent.
33
+
34
+ :- module(certificate_check, [
35
+ certificate_property_contexts/1,
36
+ certificate_predicate_groups/1,
37
+ certificate_decision_groups/1,
38
+ certificate_rule_edges/1,
39
+ certificate_member_holds/3,
40
+ certificate_members_satisfiable/2,
41
+ set_texts/2,
42
+ verify_certificates/2,
43
+ kb_verify_certificates_file/2
44
+ ]).
45
+
46
+ :- encoding(utf8).
47
+
48
+ :- use_module(library(http/json)).
49
+ :- use_module(library(apply)).
50
+ :- use_module(library(lists)).
51
+ :- use_module(library(pairs)).
52
+ :- use_module('kb.pl', [kb_entity/3, normalize_term_atom/2]).
53
+ :- use_module('intervals.pl', [
54
+ numeric_constraints_satisfiable/2,
55
+ numeric_constraint_holds/3,
56
+ numeric_constraint_negation/2
57
+ ]).
58
+ :- use_module('logic_ir.pl', [logic_rule_from_props/2]).
59
+
60
+ certificates_version('kibi.certificates.v1').
61
+ verification_version('kibi.verification.v1').
62
+
63
+ %% kb_verify_certificates_file(+Path, -JsonString)
64
+ % Read a certificate bundle from Path, verify it against the current KB and
65
+ % return the verification report as a JSON string.
66
+ kb_verify_certificates_file(Path, JsonString) :-
67
+ setup_call_cleanup(
68
+ open(Path, read, Stream, [encoding(utf8)]),
69
+ json_read_dict(Stream, Bundle, [value_string_as(string)]),
70
+ close(Stream)),
71
+ verify_certificates(Bundle, Report),
72
+ with_output_to(string(JsonString), json_write_dict(current_output, Report, [width(0)])).
73
+
74
+ %% verify_certificates(+Bundle, -Report)
75
+ % Bundle is a kibi.certificates.v1 dict as read from JSON. Report has
76
+ % status (verified or rejected), one entry per theorem with its verdict
77
+ % (proved, disproved or unresolved), and the rejected certificates.
78
+ verify_certificates(Bundle, Report) :-
79
+ verification_version(Version),
80
+ ( bundle_certificates(Bundle, Certificates)
81
+ -> numbered(Certificates, Numbered),
82
+ certificate_property_contexts(Contexts),
83
+ certificate_predicate_groups(AtomGroups),
84
+ certificate_decision_groups(DecisionGroups),
85
+ certificate_rule_edges(Edges),
86
+ property_theorem(Contexts, Numbered, PropertyTheorem, _, PropertyRejected),
87
+ group_theorem(predicates_consistent, predicate, AtomGroups, Numbered, PredicateTheorem, _, PredicateRejected),
88
+ group_theorem(decisions_consistent, decision, DecisionGroups, Numbered, DecisionTheorem, _, DecisionRejected),
89
+ rules_theorem(Edges, Numbered, RulesTheorem, _, RulesRejected),
90
+ findall(_{certificate: Index, kind: Kind, reason: "unknown certificate kind"},
91
+ ( member(Index-Certificate, Numbered),
92
+ certificate_kind(Certificate, Kind),
93
+ \+ known_kind(Kind)
94
+ ),
95
+ Unmatched),
96
+ append([PropertyRejected, PredicateRejected, DecisionRejected, RulesRejected, Unmatched], Rejected0),
97
+ sort(certificate, @=<, Rejected0, Rejected),
98
+ Theorems = [PropertyTheorem, PredicateTheorem, DecisionTheorem, RulesTheorem]
99
+ ; Theorems = [],
100
+ Rejected = [_{certificate: 0, kind: bundle, reason: "not a kibi.certificates.v1 bundle"}],
101
+ Numbered = []
102
+ ),
103
+ length(Numbered, Count),
104
+ findall(Index, (member(Failure, Rejected), Index = Failure.certificate, Index > 0), FailedIndexes0),
105
+ sort(FailedIndexes0, FailedIndexes),
106
+ length(FailedIndexes, FailedCount),
107
+ Verified is Count - FailedCount,
108
+ ( Rejected == [] -> Status = verified ; Status = rejected ),
109
+ Report = _{
110
+ version: Version,
111
+ status: Status,
112
+ certificates: Count,
113
+ verified: Verified,
114
+ rejected: Rejected,
115
+ theorems: Theorems,
116
+ notCovered: [rule_conflicts, scenario_feasibility]
117
+ }.
118
+
119
+ bundle_certificates(Bundle, Certificates) :-
120
+ is_dict(Bundle),
121
+ get_dict(version, Bundle, Version),
122
+ certificates_version(Expected),
123
+ text_equal(Version, Expected),
124
+ get_dict(certificates, Bundle, Certificates),
125
+ is_list(Certificates),
126
+ forall(member(Certificate, Certificates), is_dict(Certificate)).
127
+
128
+ numbered(Items, Numbered) :-
129
+ findall(Index-Item, nth1(Index, Items, Item), Numbered).
130
+
131
+ known_kind(Kind) :-
132
+ memberchk(Kind, [property_model, property_contradiction, property_unresolved,
133
+ predicate_model, predicate_contradiction,
134
+ decision_model, decision_contradiction,
135
+ rule_strata, rule_negation_cycle]).
136
+
137
+ certificate_kind(Certificate, Kind) :-
138
+ ( get_dict(kind, Certificate, Raw), text_atom(Raw, Kind) -> true ; Kind = unknown ).
139
+
140
+ kind_is(Certificate, Kind) :-
141
+ certificate_kind(Certificate, Kind0),
142
+ Kind0 == Kind.
143
+
144
+ % ------------------------------------------------------------------
145
+ % Property facts
146
+ % ------------------------------------------------------------------
147
+
148
+ %% certificate_property_contexts(-Contexts)
149
+ % Every comparable context: context(Subject, Property, Scope, Domain, Members)
150
+ % or unresolved(Subject, Property, Scope, Reason). A member is
151
+ % m(FactId, Op, Value, ValidFrom, ValidTo) with the operator already negated
152
+ % for a forbid fact; Domain is int, number, bool, string or set.
153
+ certificate_property_contexts(Contexts) :-
154
+ findall(Fact-tuple(Subject, Property, Op, Type, Value, Unit, Scope, Polarity, From, To),
155
+ ( kb:current_req(Req),
156
+ kb:effective_req_property_fact(Req, Subject, Fact, Property, Op, ValueType, Value0, Unit0, Scope, Polarity, From, To),
157
+ kb:comparison_quantity(ValueType, Value0, Unit0, Type, Value, Unit)
158
+ ),
159
+ Tuples0),
160
+ sort(1, @<, Tuples0, Tuples),
161
+ findall(Subject-Property, member(_-tuple(Subject, Property, _, _, _, _, _, _, _, _), Tuples), Keys0),
162
+ sort(Keys0, Keys),
163
+ findall(Context,
164
+ ( member(Subject-Property, Keys),
165
+ findall(Fact-Tuple, (member(Fact-Tuple, Tuples), Tuple = tuple(Subject, Property, _, _, _, _, _, _, _, _)), Group),
166
+ property_group_context(Subject, Property, Group, Context)
167
+ ),
168
+ Contexts).
169
+
170
+ property_group_context(Subject, Property, Group, Context) :-
171
+ findall(Scope, (member(_-tuple(_, _, _, _, _, _, Scope, _, _, _), Group), Scope \== ''), Scopes0),
172
+ sort(Scopes0, Scopes),
173
+ ( member(_-tuple(_, _, _, _, _, _, '', _, _, _), Group)
174
+ -> ContextScopes = [''|Scopes]
175
+ ; ContextScopes = Scopes
176
+ ),
177
+ member(Scope, ContextScopes),
178
+ findall(Fact-Tuple,
179
+ ( member(Fact-Tuple, Group),
180
+ Tuple = tuple(_, _, _, _, _, _, FactScope, _, _, _),
181
+ ( FactScope == '' -> true ; FactScope == Scope )
182
+ ),
183
+ Members0),
184
+ ( context_domain(Members0, Domain, Reason)
185
+ -> ( Reason == none
186
+ -> maplist(context_member(Domain), Members0, Members),
187
+ Context = context(Subject, Property, Scope, Domain, Members)
188
+ ; Context = unresolved(Subject, Property, Scope, Reason)
189
+ )
190
+ ; Context = unresolved(Subject, Property, Scope, "the facts cannot be compared")
191
+ ).
192
+
193
+ context_domain(Members, Domain, Reason) :-
194
+ findall(Type, member(_-tuple(_, _, _, Type, _, _, _, _, _, _), Members), Types0),
195
+ sort(Types0, Types),
196
+ ( member(_-tuple(_, _, _, _, _, _, _, Polarity, _, _), Members),
197
+ \+ memberchk(Polarity, [require, forbid])
198
+ -> Domain = none, Reason = "a fact has a polarity other than require or forbid"
199
+ ; subtract(Types, [int, number], []), Types \== []
200
+ -> ( Types == [int] -> Domain = int ; Domain = number ),
201
+ context_numeric_reason(Members, Reason)
202
+ ; Types == [bool]
203
+ -> Domain = bool, context_scalar_reason(Members, Reason)
204
+ ; Types == [string]
205
+ -> Domain = string, context_scalar_reason(Members, Reason)
206
+ ; Types == [set]
207
+ -> Domain = set, context_scalar_reason(Members, Reason)
208
+ ; Types = [_, _|_]
209
+ -> Domain = none, Reason = "the facts use different value types"
210
+ ; Domain = none, Reason = "the value type is not supported"
211
+ ).
212
+
213
+ context_numeric_reason(Members, Reason) :-
214
+ findall(Unit, (member(_-tuple(_, _, _, _, _, Unit, _, _, _, _), Members), Unit \== ''), Units0),
215
+ sort(Units0, Units),
216
+ ( Units = [_, _|_]
217
+ -> Reason = "the facts use different units"
218
+ ; member(_-tuple(_, _, Op, _, Value, _, _, _, _, _), Members),
219
+ ( \+ memberchk(Op, [eq, neq, lt, lte, gt, gte]) ; \+ number(Value) )
220
+ -> Reason = "a fact has an unsupported operator or a non-numeric value"
221
+ ; Reason = none
222
+ ).
223
+
224
+ context_scalar_reason(Members, Reason) :-
225
+ ( member(_-tuple(_, _, Op, _, _, _, _, _, _, _), Members),
226
+ \+ memberchk(Op, [eq, neq])
227
+ -> Reason = "only eq and neq compare non-numeric values"
228
+ ; Reason = none
229
+ ).
230
+
231
+ context_member(_Domain, Fact-tuple(_, _, Op0, _, Value, _, _, Polarity, From, To), m(Fact, Op, Value, From, To)) :-
232
+ ( Polarity == forbid
233
+ -> numeric_constraint_negation(c(Op0, _, Value), c(Op, _, _))
234
+ ; Op = Op0
235
+ ).
236
+
237
+ %% certificate_member_holds(+Domain, +Member, +Value)
238
+ % The member's constraint holds for Value.
239
+ certificate_member_holds(Domain, m(_, Op, Bound, _, _), Value) :-
240
+ numeric_domain(Domain),
241
+ !,
242
+ number(Value),
243
+ ( Domain == int -> integer(Value) ; true ),
244
+ numeric_constraint_holds(Op, Value, Bound).
245
+ certificate_member_holds(Domain, m(_, Op, Bound, _, _), Value) :-
246
+ scalar_value(Domain, Value),
247
+ text_of(Value, ValueText),
248
+ text_of(Bound, BoundText),
249
+ ( Op == eq -> ValueText == BoundText
250
+ ; Op == neq -> ValueText \== BoundText
251
+ ).
252
+
253
+ % A set value is the exact set of allowed values: eq holds for the same
254
+ % members in any order, neq for any other set.
255
+ certificate_member_holds(set, m(_, Op, Bound, _, _), Value) :-
256
+ set_texts(Value, ValueTexts),
257
+ set_texts(Bound, BoundTexts),
258
+ ( Op == eq -> ValueTexts == BoundTexts
259
+ ; Op == neq -> ValueTexts \== BoundTexts
260
+ ).
261
+
262
+ %% set_texts(+Value, -Texts)
263
+ % The sorted, unique member texts of a set value.
264
+ set_texts(Value, Texts) :-
265
+ is_list(Value),
266
+ maplist(text_of, Value, Texts0),
267
+ sort(Texts0, Texts).
268
+
269
+ numeric_domain(int).
270
+ numeric_domain(number).
271
+
272
+ scalar_value(bool, Value) :- !, text_of(Value, Text), memberchk(Text, ["true", "false"]).
273
+ scalar_value(string, Value) :- ( atom(Value) ; string(Value) ), !.
274
+
275
+ %% certificate_members_satisfiable(+Domain, +Members)
276
+ % Decision procedure used only on a contradiction core: intervals.pl for
277
+ % numbers, direct evaluation over the finite candidates for bool and string.
278
+ certificate_members_satisfiable(Domain, Members) :-
279
+ numeric_domain(Domain),
280
+ !,
281
+ member_constraints(Members, _Variable, Constraints),
282
+ ( Domain == int -> Integers = all ; Integers = [] ),
283
+ numeric_constraints_satisfiable(Constraints, Integers).
284
+ certificate_members_satisfiable(Domain, Members) :-
285
+ scalar_candidates(Domain, Members, Candidates),
286
+ member(Value, Candidates),
287
+ forall(member(Member, Members), certificate_member_holds(Domain, Member, Value)),
288
+ !.
289
+
290
+ % One constraint per member, all on the same variable.
291
+ member_constraints([], _Variable, []).
292
+ member_constraints([m(_, Op, Bound, _, _)|Members], Variable, [c(Op, Variable, Bound)|Constraints]) :-
293
+ member_constraints(Members, Variable, Constraints).
294
+
295
+ % Any satisfying value is either a bound some eq names or differs from every
296
+ % bound; one fresh text stands for the latter.
297
+ scalar_candidates(bool, _Members, ["true", "false"]).
298
+ scalar_candidates(string, Members, Candidates) :-
299
+ findall(Text, (member(m(_, _, Bound, _, _), Members), text_of(Bound, Text)), Texts0),
300
+ sort(Texts0, Texts),
301
+ atomic_list_concat(["kibi:fresh"|Texts], '|', FreshAtom),
302
+ atom_string(FreshAtom, Fresh),
303
+ append(Texts, [Fresh], Candidates).
304
+ scalar_candidates(set, Members, Candidates) :-
305
+ findall(Texts, (member(m(_, _, Bound, _, _), Members), set_texts(Bound, Texts)), Sets0),
306
+ sort(Sets0, Sets),
307
+ findall(Text, (member(Set, Sets), member(Text, Set)), Texts0),
308
+ sort(Texts0, Texts),
309
+ atomic_list_concat(["kibi:fresh"|Texts], '|', FreshAtom),
310
+ atom_string(FreshAtom, Fresh),
311
+ append(Sets, [[Fresh]], Candidates).
312
+
313
+ property_theorem(Contexts, Numbered, Theorem, Used, Rejected) :-
314
+ findall(Index-Certificate,
315
+ ( member(Index-Certificate, Numbered),
316
+ certificate_kind(Certificate, Kind),
317
+ memberchk(Kind, [property_model, property_contradiction, property_unresolved])
318
+ ),
319
+ Candidates),
320
+ foldl(property_context_result(Candidates), Contexts, [], Results),
321
+ theorem_from_results(properties_consistent, Results, Theorem, Used, Rejected0),
322
+ findall(_{certificate: Index, kind: Kind, reason: "certificate names no comparable property context of this KB"},
323
+ ( member(Index-Certificate, Candidates),
324
+ \+ memberchk(Index, Used),
325
+ certificate_kind(Certificate, Kind)
326
+ ),
327
+ Orphans),
328
+ append(Rejected0, Orphans, Rejected).
329
+
330
+ property_context_result(_Candidates, unresolved(Subject, Property, Scope, Reason), Results,
331
+ [result(unresolved, [], [], _{subjectKey: Subject, propertyKey: Property, scope: Scope, reason: Reason})|Results]) :- !.
332
+ property_context_result(Candidates, context(Subject, Property, Scope, Domain, Members), Results, [Result|Results]) :-
333
+ findall(Index-Certificate,
334
+ ( member(Index-Certificate, Candidates),
335
+ certificate_text(Certificate, subjectKey, Subject),
336
+ certificate_text(Certificate, propertyKey, Property),
337
+ certificate_text(Certificate, scope, Scope)
338
+ ),
339
+ Matches),
340
+ Label = _{subjectKey: Subject, propertyKey: Property, scope: Scope},
341
+ ( Matches == []
342
+ -> Result = result(missing, [], [], Label)
343
+ ; Matches = [Index-Certificate]
344
+ -> property_certificate_check(Certificate, Domain, Members, Verdict, Outcome),
345
+ ( Outcome == ok
346
+ -> Result = result(Verdict, [Index], [], Label)
347
+ ; Outcome = failed(Reason),
348
+ certificate_kind(Certificate, Kind),
349
+ Result = result(missing, [Index], [_{certificate: Index, kind: Kind, reason: Reason}], Label)
350
+ )
351
+ ; findall(Index, member(Index-_, Matches), Indexes),
352
+ findall(_{certificate: Index, kind: property, reason: "more than one certificate for the same context"},
353
+ member(Index, Indexes), Failures),
354
+ Result = result(missing, Indexes, Failures, Label)
355
+ ).
356
+
357
+ certificate_text(Certificate, Key, Expected) :-
358
+ get_dict(Key, Certificate, Value),
359
+ text_equal(Value, Expected).
360
+
361
+ % property_certificate_check(+Certificate, +Domain, +Members, -Verdict, ?Outcome)
362
+ % Outcome is ok when the certificate re-verifies, else failed(Reason) names
363
+ % the first check that failed.
364
+ property_certificate_check(Certificate, Domain, Members, Verdict, Outcome) :-
365
+ certificate_kind(Certificate, Kind),
366
+ ( \+ certificate_text(Certificate, domain, Domain)
367
+ -> Outcome = failed("certificate domain differs from the facts' value domain")
368
+ ; Kind == property_model
369
+ -> Verdict = proved,
370
+ model_check(Certificate, Domain, Members, Outcome)
371
+ ; Kind == property_contradiction
372
+ -> Verdict = disproved,
373
+ core_check(Certificate, Domain, Members, true, Outcome)
374
+ ; Kind == property_unresolved
375
+ -> Verdict = unresolved,
376
+ core_check(Certificate, Domain, Members, false, Outcome)
377
+ ).
378
+
379
+ model_check(Certificate, Domain, Members, Outcome) :-
380
+ findall(Fact, member(m(Fact, _, _, _, _), Members), Facts),
381
+ ( \+ certificate_id_set(Certificate, facts, Facts)
382
+ -> Outcome = failed("model does not list exactly the facts of its context")
383
+ ; \+ get_dict(value, Certificate, _)
384
+ -> Outcome = failed("model has no value")
385
+ ; get_dict(value, Certificate, Value),
386
+ member(Member, Members),
387
+ \+ certificate_member_holds(Domain, Member, Value)
388
+ -> Member = m(Fact, _, _, _, _),
389
+ format(string(Reason), "model value does not satisfy ~w", [Fact]),
390
+ Outcome = failed(Reason)
391
+ ; Outcome = ok
392
+ ).
393
+
394
+ % A contradiction core must be a subset of the context, admit no value, be
395
+ % irreducible (each dropped fact comes with a model of the rest) and, for a
396
+ % contradiction, have validity windows that all overlap. An unresolved
397
+ % certificate is the same core whose windows do not all overlap.
398
+ core_check(Certificate, Domain, Members, WindowsOverlap, Outcome) :-
399
+ ( \+ certificate_ids(Certificate, core, _)
400
+ -> Outcome = failed("core is not a list of fact ids")
401
+ ; certificate_ids(Certificate, core, Core),
402
+ findall(Member, (member(Fact, Core), member(Member, Members), Member = m(Fact, _, _, _, _)), CoreMembers),
403
+ length(Core, CoreCount),
404
+ length(CoreMembers, CoreCount),
405
+ sort(Core, SortedCore),
406
+ length(SortedCore, CoreCount),
407
+ CoreCount > 0
408
+ -> ( certificate_members_satisfiable(Domain, CoreMembers)
409
+ -> Outcome = failed("core admits a value, so it is not a contradiction")
410
+ ; \+ drop_models_valid(Certificate, Domain, CoreMembers)
411
+ -> Outcome = failed("core is not irreducible: a dropped fact has no valid model of the rest")
412
+ ; windows_overlap(CoreMembers, Overlap),
413
+ Overlap \== WindowsOverlap
414
+ -> ( WindowsOverlap == true
415
+ -> Outcome = failed("core facts are never valid at the same time")
416
+ ; Outcome = failed("core facts are valid at the same time, so the core is a contradiction")
417
+ )
418
+ ; Outcome = ok
419
+ )
420
+ ; Outcome = failed("core names a fact outside its context or repeats one")
421
+ ).
422
+
423
+ drop_models_valid(Certificate, Domain, CoreMembers) :-
424
+ get_dict(dropModels, Certificate, DropModels),
425
+ is_list(DropModels),
426
+ forall(member(m(Fact, _, _, _, _), CoreMembers),
427
+ ( member(Drop, DropModels),
428
+ is_dict(Drop),
429
+ certificate_text(Drop, factId, Fact),
430
+ get_dict(value, Drop, Value),
431
+ forall((member(Other, CoreMembers), Other \= m(Fact, _, _, _, _)),
432
+ certificate_member_holds(Domain, Other, Value))
433
+ -> true
434
+ )).
435
+
436
+ windows_overlap(Members, Overlap) :-
437
+ ( forall((member(m(_, _, _, FromA, ToA), Members), member(m(_, _, _, FromB, ToB), Members)),
438
+ kb:intervals_overlap(FromA, ToA, FromB, ToB))
439
+ -> Overlap = true
440
+ ; Overlap = false
441
+ ).
442
+
443
+ certificate_ids(Certificate, Key, Ids) :-
444
+ get_dict(Key, Certificate, Raw),
445
+ is_list(Raw),
446
+ maplist(text_atom, Raw, Ids).
447
+
448
+ certificate_id_set(Certificate, Key, Expected) :-
449
+ certificate_ids(Certificate, Key, Ids),
450
+ msort(Ids, Sorted),
451
+ msort(Expected, Sorted).
452
+
453
+ % ------------------------------------------------------------------
454
+ % Predicate and decision facts
455
+ % ------------------------------------------------------------------
456
+
457
+ %% certificate_predicate_groups(-Groups)
458
+ % group(Label, Members): the facts that assert or deny one ground atom. A
459
+ % member is p(FactId, Value) with Value true for assert and false for deny.
460
+ certificate_predicate_groups(Groups) :-
461
+ findall(Fact-atom(Namespace, Name, Args, Polarity),
462
+ ( kb:current_req(Req),
463
+ kb:effective_req_predicate(Req, Fact, Namespace, Name, Args, Polarity)
464
+ ),
465
+ Facts0),
466
+ sort(1, @<, Facts0, Facts),
467
+ findall(Namespace-Name-Args, member(_-atom(Namespace, Name, Args, _), Facts), Keys0),
468
+ sort(Keys0, Keys),
469
+ findall(Group,
470
+ ( member(Namespace-Name-Args, Keys),
471
+ findall(Fact-Polarity, member(Fact-atom(Namespace, Name, Args, Polarity), Facts), Pairs),
472
+ Label = _{namespace: Namespace, name: Name, args: Args},
473
+ ( member(_-Polarity, Pairs), \+ memberchk(Polarity, [assert, deny])
474
+ -> Group = unresolved(Label, "a fact has a polarity other than assert or deny")
475
+ ; findall(p(Fact, Value), (member(Fact-Polarity, Pairs), polarity_value(Polarity, Value)), Members),
476
+ Group = group(Label, Members)
477
+ )
478
+ ),
479
+ Groups).
480
+
481
+ polarity_value(assert, true).
482
+ polarity_value(deny, false).
483
+
484
+ %% certificate_decision_groups(-Groups)
485
+ % group(Label, Members): the decision-predicate facts that agree on every key
486
+ % argument. A member is p(FactId, Effect) with Effect allow or deny.
487
+ certificate_decision_groups(Groups) :-
488
+ kb:decision_signatures(Signatures),
489
+ findall(Fact-decision(Namespace, Name, Arity, Key, Effect),
490
+ ( kb:current_req(Req),
491
+ kb:effective_req_predicate(Req, Fact, Namespace, Name, Args, Polarity),
492
+ length(Args, Arity),
493
+ memberchk(decision_signature(Namespace, Name, Arity, KeyPositions, DecisionPosition), Signatures),
494
+ findall(Value, (member(Position, KeyPositions), nth1(Position, Args, Value)), Key),
495
+ nth1(DecisionPosition, Args, Decision),
496
+ ( kb:effective_decision(Decision, Polarity, Effect0) -> Effect = Effect0 ; Effect = unknown )
497
+ ),
498
+ Facts0),
499
+ sort(1, @<, Facts0, Facts),
500
+ findall(Namespace-Name-Arity-Key, member(_-decision(Namespace, Name, Arity, Key, _), Facts), Keys0),
501
+ sort(Keys0, Keys),
502
+ findall(Group,
503
+ ( member(Namespace-Name-Arity-Key, Keys),
504
+ findall(p(Fact, Effect), member(Fact-decision(Namespace, Name, Arity, Key, Effect), Facts), Members),
505
+ Label = _{namespace: Namespace, name: Name, arity: Arity, key: Key},
506
+ ( memberchk(p(_, unknown), Members)
507
+ -> Group = unresolved(Label, "a decision is neither allow nor deny")
508
+ ; Group = group(Label, Members)
509
+ )
510
+ ),
511
+ Groups).
512
+
513
+ group_theorem(Theorem, Prefix, Groups, Numbered, Result, Used, Rejected) :-
514
+ atom_concat(Prefix, '_model', ModelKind),
515
+ atom_concat(Prefix, '_contradiction', ContradictionKind),
516
+ findall(Index-Certificate,
517
+ ( member(Index-Certificate, Numbered),
518
+ certificate_kind(Certificate, Kind),
519
+ memberchk(Kind, [ModelKind, ContradictionKind])
520
+ ),
521
+ Candidates),
522
+ foldl(group_result(ModelKind, ContradictionKind, Candidates), Groups, [], Results),
523
+ theorem_from_results(Theorem, Results, Result, Used, Rejected0),
524
+ findall(_{certificate: Index, kind: Kind, reason: "certificate names no group of this KB"},
525
+ ( member(Index-Certificate, Candidates),
526
+ \+ memberchk(Index, Used),
527
+ certificate_kind(Certificate, Kind)
528
+ ),
529
+ Orphans),
530
+ append(Rejected0, Orphans, Rejected).
531
+
532
+ group_result(_ModelKind, _ContradictionKind, _Candidates, unresolved(Label, Reason), Results,
533
+ [result(unresolved, [], [], Label.put(reason, Reason))|Results]) :- !.
534
+ group_result(ModelKind, ContradictionKind, Candidates, group(Label, Members), Results, [Result|Results]) :-
535
+ findall(Index-Certificate,
536
+ ( member(Index-Certificate, Candidates),
537
+ certificate_matches_label(Certificate, Label)
538
+ ),
539
+ Matches),
540
+ ( Matches == []
541
+ -> Result = result(missing, [], [], Label)
542
+ ; Matches = [Index-Certificate]
543
+ -> group_certificate_check(Certificate, ModelKind, ContradictionKind, Members, Verdict, Outcome),
544
+ ( Outcome == ok
545
+ -> Result = result(Verdict, [Index], [], Label)
546
+ ; Outcome = failed(Reason),
547
+ certificate_kind(Certificate, Kind),
548
+ Result = result(missing, [Index], [_{certificate: Index, kind: Kind, reason: Reason}], Label)
549
+ )
550
+ ; findall(Index, member(Index-_, Matches), Indexes),
551
+ findall(_{certificate: Index, kind: group, reason: "more than one certificate for the same group"},
552
+ member(Index, Indexes), Failures),
553
+ Result = result(missing, Indexes, Failures, Label)
554
+ ).
555
+
556
+ certificate_matches_label(Certificate, Label) :-
557
+ forall(get_dict(Key, Label, Expected),
558
+ ( get_dict(Key, Certificate, Actual),
559
+ label_value_equal(Actual, Expected)
560
+ )).
561
+
562
+ label_value_equal(Actual, Expected) :-
563
+ is_list(Expected),
564
+ !,
565
+ is_list(Actual),
566
+ maplist(text_equal, Actual, Expected).
567
+ label_value_equal(Actual, Expected) :-
568
+ integer(Expected),
569
+ !,
570
+ Actual == Expected.
571
+ label_value_equal(Actual, Expected) :-
572
+ text_equal(Actual, Expected).
573
+
574
+ group_certificate_check(Certificate, ModelKind, ContradictionKind, Members, Verdict, Outcome) :-
575
+ certificate_kind(Certificate, Kind),
576
+ findall(Fact, member(p(Fact, _), Members), Facts),
577
+ ( Kind == ModelKind
578
+ -> Verdict = proved,
579
+ ( \+ certificate_id_set(Certificate, facts, Facts)
580
+ -> Outcome = failed("model does not list exactly the facts of its group")
581
+ ; \+ get_dict(value, Certificate, _)
582
+ -> Outcome = failed("model has no value")
583
+ ; get_dict(value, Certificate, Value),
584
+ text_of(Value, ValueText),
585
+ member(p(Fact, Expected), Members),
586
+ text_of(Expected, ExpectedText),
587
+ ValueText \== ExpectedText
588
+ -> format(string(Reason), "model value does not satisfy ~w", [Fact]),
589
+ Outcome = failed(Reason)
590
+ ; Outcome = ok
591
+ )
592
+ ; Kind == ContradictionKind
593
+ -> Verdict = disproved,
594
+ ( certificate_ids(Certificate, core, [FactA, FactB]),
595
+ memberchk(p(FactA, ValueA), Members),
596
+ memberchk(p(FactB, ValueB), Members),
597
+ ValueA \== ValueB
598
+ -> Outcome = ok
599
+ ; Outcome = failed("core is not two facts of the group that disagree")
600
+ )
601
+ ).
602
+
603
+ % ------------------------------------------------------------------
604
+ % Rule stratification
605
+ % ------------------------------------------------------------------
606
+
607
+ %% certificate_rule_edges(-Edges)
608
+ % edge(Head, Body, Sign) for every dependency of every stored rule fact, the
609
+ % same graph logic_rules_stratified/1 checks. Head and Body are
610
+ % predicate(Namespace, Name, Arity).
611
+ certificate_rule_edges(Edges) :-
612
+ findall(edge(Head, Body, Sign),
613
+ ( kb_entity(_FactId, fact, Props),
614
+ memberchk(fact_kind=RawKind, Props),
615
+ normalize_term_atom(RawKind, rule),
616
+ logic_rule_from_props(Props, rule(rule, _Modality, HeadAtom, BodyExpression, _, _, _, _, _, _)),
617
+ atom_signature(HeadAtom, Head),
618
+ body_dependency(BodyExpression, BodyAtom, Sign),
619
+ atom_signature(BodyAtom, Body)
620
+ ),
621
+ Edges0),
622
+ sort(Edges0, Edges).
623
+
624
+ atom_signature(atom(Namespace, Name, Args, _, _), predicate(Namespace, Name, Arity)) :-
625
+ length(Args, Arity).
626
+
627
+ body_dependency(Atom, Atom, positive) :- Atom = atom(_, _, _, _, _).
628
+ body_dependency(not(Atom), Atom, negative).
629
+ body_dependency(all(Items), Atom, Sign) :- member(Item, Items), body_dependency(Item, Atom, Sign).
630
+ body_dependency(any(Items), Atom, Sign) :- member(Item, Items), body_dependency(Item, Atom, Sign).
631
+ body_dependency(count(Atom, _, _), Atom, positive).
632
+
633
+ rules_theorem(Edges, Numbered, Theorem, Used, Rejected) :-
634
+ findall(Index-Certificate,
635
+ ( member(Index-Certificate, Numbered),
636
+ certificate_kind(Certificate, Kind),
637
+ memberchk(Kind, [rule_strata, rule_negation_cycle])
638
+ ),
639
+ Candidates),
640
+ findall(Signature, (member(edge(A, B, _), Edges), member(Signature, [A, B])), Signatures0),
641
+ sort(Signatures0, Signatures),
642
+ length(Signatures, PredicateCount),
643
+ ( Candidates == []
644
+ -> ( Edges == []
645
+ -> Verdict = proved, Used = [], Rejected = []
646
+ ; Verdict = unresolved, Used = [],
647
+ Rejected = [_{certificate: 0, kind: rule_strata, reason: "no stratification certificate for the stored rules"}]
648
+ )
649
+ ; Candidates = [Index-Certificate]
650
+ -> Used = [Index],
651
+ rule_certificate_check(Certificate, Edges, Signatures, Verdict0, Outcome),
652
+ ( Outcome == ok
653
+ -> Verdict = Verdict0, Rejected = []
654
+ ; Outcome = failed(Reason),
655
+ certificate_kind(Certificate, Kind),
656
+ Verdict = unresolved,
657
+ Rejected = [_{certificate: Index, kind: Kind, reason: Reason}]
658
+ )
659
+ ; findall(Index, member(Index-_, Candidates), Used),
660
+ Verdict = unresolved,
661
+ findall(_{certificate: Index, kind: rule_strata, reason: "more than one stratification certificate"},
662
+ member(Index, Used), Rejected)
663
+ ),
664
+ length(Edges, EdgeCount),
665
+ Theorem = _{theorem: rules_stratified, verdict: Verdict, predicates: PredicateCount, edges: EdgeCount}.
666
+
667
+ rule_certificate_check(Certificate, Edges, Signatures, Verdict, Outcome) :-
668
+ certificate_kind(Certificate, Kind),
669
+ ( Kind == rule_strata
670
+ -> Verdict = proved,
671
+ ( get_dict(strata, Certificate, Raw), is_list(Raw),
672
+ maplist(stratum_entry, Raw, Entries)
673
+ -> ( member(Signature, Signatures), \+ memberchk(Signature-_, Entries)
674
+ -> Outcome = failed("strata omit a predicate the rules depend on")
675
+ ; member(edge(Head, Body, Sign), Edges),
676
+ memberchk(Head-HeadStratum, Entries),
677
+ memberchk(Body-BodyStratum, Entries),
678
+ \+ stratum_edge_ok(Sign, HeadStratum, BodyStratum)
679
+ -> Outcome = failed("a rule dependency violates the stratum order")
680
+ ; Outcome = ok
681
+ )
682
+ ; Outcome = failed("strata are not a list of predicates with integer strata")
683
+ )
684
+ ; Kind == rule_negation_cycle
685
+ -> Verdict = disproved,
686
+ ( get_dict(cycle, Certificate, Raw), is_list(Raw), Raw \== [],
687
+ maplist(cycle_edge, Raw, Cycle),
688
+ forall(member(Edge, Cycle), memberchk(Edge, Edges)),
689
+ memberchk(edge(_, _, negative), Cycle),
690
+ cycle_closed(Cycle)
691
+ -> Outcome = ok
692
+ ; Outcome = failed("cycle is not a closed path of stored rule dependencies with a negated edge")
693
+ )
694
+ ).
695
+
696
+ stratum_entry(Entry, predicate(Namespace, Name, Arity)-Stratum) :-
697
+ is_dict(Entry),
698
+ get_dict(namespace, Entry, NamespaceRaw), text_atom(NamespaceRaw, Namespace),
699
+ get_dict(name, Entry, NameRaw), text_atom(NameRaw, Name),
700
+ get_dict(arity, Entry, Arity), integer(Arity),
701
+ get_dict(stratum, Entry, Stratum), integer(Stratum), Stratum >= 0.
702
+
703
+ stratum_edge_ok(positive, Head, Body) :- Head >= Body.
704
+ stratum_edge_ok(negative, Head, Body) :- Head > Body.
705
+
706
+ cycle_edge(Entry, edge(Head, Body, Sign)) :-
707
+ is_dict(Entry),
708
+ get_dict(head, Entry, HeadRaw), signature_from_dict(HeadRaw, Head),
709
+ get_dict(body, Entry, BodyRaw), signature_from_dict(BodyRaw, Body),
710
+ get_dict(sign, Entry, SignRaw), text_atom(SignRaw, Sign).
711
+
712
+ signature_from_dict(Dict, predicate(Namespace, Name, Arity)) :-
713
+ is_dict(Dict),
714
+ get_dict(namespace, Dict, NamespaceRaw), text_atom(NamespaceRaw, Namespace),
715
+ get_dict(name, Dict, NameRaw), text_atom(NameRaw, Name),
716
+ get_dict(arity, Dict, Arity), integer(Arity).
717
+
718
+ % Consecutive edges chain head to body (a head depends on its body) and the
719
+ % last edge's body is the first edge's head.
720
+ cycle_closed([edge(First, Next, _)|Rest]) :-
721
+ cycle_chain(Rest, Next, First).
722
+
723
+ cycle_chain([], Last, First) :- Last == First.
724
+ cycle_chain([edge(Head, Body, _)|Rest], Previous, First) :-
725
+ Head == Previous,
726
+ cycle_chain(Rest, Body, First).
727
+
728
+ % ------------------------------------------------------------------
729
+ % Theorem summaries
730
+ % ------------------------------------------------------------------
731
+
732
+ % result(Verdict, CertificateIndexes, Rejected, Label), Verdict one of
733
+ % proved, disproved, unresolved, missing. A theorem is disproved by any
734
+ % re-verified contradiction, unresolved while any context is unresolved or
735
+ % missing, and proved only when every context has a re-verified model.
736
+ theorem_from_results(Theorem, Results, Summary, Used, Rejected) :-
737
+ findall(Index, (member(result(_, Indexes, _, _), Results), member(Index, Indexes)), Used),
738
+ findall(Failure, (member(result(_, _, Failures, _), Results), member(Failure, Failures)), Rejected1),
739
+ findall(Label, member(result(missing, [], [], Label), Results), MissingLabels),
740
+ missing_reasons(MissingLabels, MissingRejected),
741
+ append(Rejected1, MissingRejected, Rejected),
742
+ count_verdict(proved, Results, Proved),
743
+ count_verdict(disproved, Results, Disproved),
744
+ findall(Label, member(result(unresolved, _, _, Label), Results), Unresolved),
745
+ findall(Label, member(result(disproved, _, _, Label), Results), Contradictions),
746
+ length(Results, Total),
747
+ ( Disproved > 0 -> Verdict = disproved
748
+ ; Proved =:= Total -> Verdict = proved
749
+ ; Verdict = unresolved
750
+ ),
751
+ Summary = _{
752
+ theorem: Theorem,
753
+ verdict: Verdict,
754
+ groups: Total,
755
+ models: Proved,
756
+ contradictions: Contradictions,
757
+ unresolved: Unresolved
758
+ }.
759
+
760
+ missing_reasons(Labels, Rejected) :-
761
+ findall(_{certificate: 0, kind: missing, reason: Reason, group: Label},
762
+ ( member(Label, Labels),
763
+ Reason = "no certificate for this group"
764
+ ),
765
+ Rejected).
766
+
767
+ count_verdict(Verdict, Results, Count) :-
768
+ aggregate_all(count, member(result(Verdict, _, _, _), Results), Count).
769
+
770
+ % ------------------------------------------------------------------
771
+ % Text helpers: JSON gives strings, the KB gives atoms.
772
+ % ------------------------------------------------------------------
773
+
774
+ text_of(Value, Text) :- string(Value), !, Text = Value.
775
+ text_of(Value, Text) :- atom(Value), !, atom_string(Value, Text).
776
+ text_of(Value, Text) :- number(Value), !, number_string(Value, Text).
777
+
778
+ text_atom(Value, Atom) :- atom(Value), !, Atom = Value.
779
+ text_atom(Value, Atom) :- string(Value), atom_string(Atom, Value).
780
+
781
+ text_equal(Left, Right) :-
782
+ text_of(Left, Text),
783
+ text_of(Right, Text).