kibi-core 0.22.5 → 0.23.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,237 @@
1
+ % Module: certificates
2
+ % implements REQ-kibi-kb-certificates
3
+ %
4
+ % Finds a certificate for every KB theorem that certificate_check.pl states:
5
+ % a model for each consistent property context, predicate atom and decision
6
+ % key, an irreducible core (with a model per dropped fact) for each
7
+ % contradiction, and a stratum assignment (or a negation cycle) for the
8
+ % stored rules. This module may search; certificate_check.pl re-checks
9
+ % everything it emits without trusting it.
10
+
11
+ :- module(certificates, [
12
+ kb_certificates/1,
13
+ kb_write_certificates/2
14
+ ]).
15
+
16
+ :- encoding(utf8).
17
+
18
+ :- use_module(library(http/json)).
19
+ :- use_module(library(apply)).
20
+ :- use_module(library(lists)).
21
+ :- use_module(library(aggregate)).
22
+ :- use_module('kb.pl', []).
23
+ :- use_module('intervals.pl', [numeric_constraint_holds/3]).
24
+ :- use_module('certificate_check.pl', [
25
+ certificate_property_contexts/1,
26
+ certificate_predicate_groups/1,
27
+ certificate_decision_groups/1,
28
+ certificate_rule_edges/1,
29
+ certificate_member_holds/3,
30
+ certificate_members_satisfiable/2
31
+ ]).
32
+
33
+ %% kb_write_certificates(+Path, -JsonString)
34
+ % Write the certificate bundle for the current KB to Path and return a
35
+ % summary (certificate count per kind) as a JSON string.
36
+ kb_write_certificates(Path, JsonString) :-
37
+ kb_certificates(Bundle),
38
+ setup_call_cleanup(
39
+ open(Path, write, Stream, [encoding(utf8)]),
40
+ ( json_write_dict(Stream, Bundle, [width(0)]), nl(Stream) ),
41
+ close(Stream)),
42
+ findall(Kind, (member(Certificate, Bundle.certificates), get_dict(kind, Certificate, Kind)), Kinds0),
43
+ msort(Kinds0, Kinds),
44
+ clumped(Kinds, Counts),
45
+ findall(Kind-Count, member(Kind-Count, Counts), Pairs),
46
+ dict_pairs(ByKind, _, Pairs),
47
+ length(Bundle.certificates, Total),
48
+ with_output_to(string(JsonString),
49
+ json_write_dict(current_output, _{path: Path, certificates: Total, byKind: ByKind}, [width(0)])).
50
+
51
+ %% kb_certificates(-Bundle)
52
+ kb_certificates(_{version: 'kibi.certificates.v1', certificates: Certificates}) :-
53
+ certificate_property_contexts(Contexts),
54
+ convlist(property_certificate, Contexts, PropertyCertificates),
55
+ certificate_predicate_groups(AtomGroups),
56
+ convlist(group_certificate(predicate), AtomGroups, PredicateCertificates),
57
+ certificate_decision_groups(DecisionGroups),
58
+ convlist(group_certificate(decision), DecisionGroups, DecisionCertificates),
59
+ certificate_rule_edges(Edges),
60
+ rule_certificates(Edges, RuleCertificates),
61
+ append([PropertyCertificates, PredicateCertificates, DecisionCertificates, RuleCertificates], Certificates).
62
+
63
+ % ------------------------------------------------------------------
64
+ % Property contexts
65
+ % ------------------------------------------------------------------
66
+
67
+ property_certificate(context(Subject, Property, Scope, Domain, Members), Certificate) :-
68
+ Base = _{subjectKey: Subject, propertyKey: Property, scope: Scope, domain: Domain},
69
+ ( members_model(Domain, Members, Value)
70
+ -> findall(Fact, member(m(Fact, _, _, _, _), Members), Facts),
71
+ Certificate = Base.put(_{kind: property_model, value: Value, facts: Facts})
72
+ ; irreducible_core(Domain, Members, Core),
73
+ findall(_{factId: Fact, value: Value},
74
+ ( member(Dropped, Core),
75
+ Dropped = m(Fact, _, _, _, _),
76
+ exclude(==(Dropped), Core, Rest),
77
+ members_model(Domain, Rest, Value)
78
+ ),
79
+ DropModels),
80
+ findall(Fact, member(m(Fact, _, _, _, _), Core), CoreFacts),
81
+ ( core_windows_overlap(Core)
82
+ -> Kind = property_contradiction
83
+ ; Kind = property_unresolved
84
+ ),
85
+ Certificate = Base.put(_{kind: Kind, core: CoreFacts, dropModels: DropModels})
86
+ ).
87
+
88
+ %% members_model(+Domain, +Members, -Value)
89
+ % A value satisfying every member, found among finitely many candidates that
90
+ % meet every region the members' bounds cut the line into.
91
+ members_model(Domain, Members, Value) :-
92
+ model_candidates(Domain, Members, Candidates),
93
+ member(Value, Candidates),
94
+ forall(member(Member, Members), certificate_member_holds(Domain, Member, Value)),
95
+ !.
96
+
97
+ model_candidates(Domain, Members, Candidates) :-
98
+ memberchk(Domain, [int, number]),
99
+ !,
100
+ findall(Bound, member(m(_, _, Bound, _, _), Members), Bounds0),
101
+ sort(Bounds0, Bounds),
102
+ ( Domain == int
103
+ -> findall(Candidate,
104
+ ( member(Bound, Bounds),
105
+ member(Base, [floor(Bound), ceiling(Bound)]),
106
+ member(Offset, [-1, 0, 1]),
107
+ Candidate is Base + Offset
108
+ ),
109
+ Candidates0)
110
+ ; findall(Candidate,
111
+ ( member(Bound, Bounds), member(Offset, [-1, 0, 1]), Candidate is Bound + Offset
112
+ ; nextto(Low, High, Bounds), Candidate is (Low + High) / 2
113
+ ),
114
+ Candidates0)
115
+ ),
116
+ sort([0|Candidates0], Candidates).
117
+ model_candidates(bool, _Members, [true, false]) :- !.
118
+ model_candidates(string, Members, Candidates) :-
119
+ findall(Bound, member(m(_, _, Bound, _, _), Members), Bounds0),
120
+ sort(Bounds0, Bounds),
121
+ fresh_text(Bounds, 0, Fresh),
122
+ append(Bounds, [Fresh], Candidates).
123
+
124
+ fresh_text(Bounds, Index, Fresh) :-
125
+ format(string(Candidate), "kibi:model:~w", [Index]),
126
+ ( member(Bound, Bounds), text_value(Bound, Candidate)
127
+ -> Next is Index + 1,
128
+ fresh_text(Bounds, Next, Fresh)
129
+ ; Fresh = Candidate
130
+ ).
131
+
132
+ text_value(Value, Text) :- atom(Value), !, atom_string(Value, Text).
133
+ text_value(Value, Text) :- string(Value), !, Text = Value.
134
+
135
+ %% irreducible_core(+Domain, +Members, -Core)
136
+ % Deletion filter: drop each member whose removal keeps the set
137
+ % unsatisfiable. What remains admits no value, and every proper subset
138
+ % does.
139
+ irreducible_core(Domain, Members, Core) :-
140
+ foldl(core_step(Domain), Members, Members, Core).
141
+
142
+ core_step(Domain, Member, Current, Next) :-
143
+ exclude(==(Member), Current, Without),
144
+ ( Without \== [],
145
+ \+ certificate_members_satisfiable(Domain, Without)
146
+ -> Next = Without
147
+ ; Next = Current
148
+ ).
149
+
150
+ core_windows_overlap(Core) :-
151
+ forall((member(m(_, _, _, FromA, ToA), Core), member(m(_, _, _, FromB, ToB), Core)),
152
+ kb:intervals_overlap(FromA, ToA, FromB, ToB)).
153
+
154
+ % ------------------------------------------------------------------
155
+ % Predicate atoms and decision keys
156
+ % ------------------------------------------------------------------
157
+
158
+ group_certificate(Prefix, group(Label, Members), Certificate) :-
159
+ atom_concat(Prefix, '_model', ModelKind),
160
+ atom_concat(Prefix, '_contradiction', ContradictionKind),
161
+ findall(Value, member(p(_, Value), Members), Values0),
162
+ sort(Values0, Values),
163
+ ( Values = [Value]
164
+ -> findall(Fact, member(p(Fact, _), Members), Facts),
165
+ Certificate = Label.put(_{kind: ModelKind, value: Value, facts: Facts})
166
+ ; Members = [p(FactA, ValueA)|_],
167
+ member(p(FactB, ValueB), Members),
168
+ ValueB \== ValueA,
169
+ !,
170
+ Certificate = Label.put(_{kind: ContradictionKind, core: [FactA, FactB]})
171
+ ).
172
+
173
+ % ------------------------------------------------------------------
174
+ % Rule stratification
175
+ % ------------------------------------------------------------------
176
+
177
+ rule_certificates([], []) :- !.
178
+ rule_certificates(Edges, [Certificate]) :-
179
+ findall(Signature, (member(edge(Head, Body, _), Edges), member(Signature, [Head, Body])), Signatures0),
180
+ sort(Signatures0, Signatures),
181
+ findall(Signature-0, member(Signature, Signatures), Initial),
182
+ length(Signatures, Count),
183
+ Rounds is Count + 1,
184
+ ( relax_strata(Rounds, Edges, Initial, Strata)
185
+ -> findall(_{namespace: Namespace, name: Name, arity: Arity, stratum: Stratum},
186
+ member(predicate(Namespace, Name, Arity)-Stratum, Strata),
187
+ Entries),
188
+ Certificate = _{kind: rule_strata, strata: Entries}
189
+ ; negation_cycle(Edges, Cycle),
190
+ findall(_{head: HeadDict, body: BodyDict, sign: Sign},
191
+ ( member(edge(Head, Body, Sign), Cycle),
192
+ signature_dict(Head, HeadDict),
193
+ signature_dict(Body, BodyDict)
194
+ ),
195
+ Entries),
196
+ Certificate = _{kind: rule_negation_cycle, cycle: Entries}
197
+ ).
198
+
199
+ signature_dict(predicate(Namespace, Name, Arity), _{namespace: Namespace, name: Name, arity: Arity}).
200
+
201
+ % Raise each head to at least its body's stratum (one more across a negated
202
+ % edge) until nothing changes. Without a negation cycle that happens within
203
+ % one round per predicate; a cycle keeps raising, and relaxation fails.
204
+ relax_strata(0, _Edges, _Strata, _Final) :- !, fail.
205
+ relax_strata(Rounds, Edges, Strata, Final) :-
206
+ foldl(relax_edge, Edges, Strata, Next),
207
+ ( Next == Strata
208
+ -> Final = Strata
209
+ ; Remaining is Rounds - 1,
210
+ relax_strata(Remaining, Edges, Next, Final)
211
+ ).
212
+
213
+ relax_edge(edge(Head, Body, Sign), Strata, Next) :-
214
+ memberchk(Head-HeadStratum, Strata),
215
+ memberchk(Body-BodyStratum, Strata),
216
+ ( Sign == negative -> Required is BodyStratum + 1 ; Required = BodyStratum ),
217
+ ( HeadStratum < Required
218
+ -> selectchk(Head-HeadStratum, Strata, Head-Required, Next)
219
+ ; Next = Strata
220
+ ).
221
+
222
+ % A negated edge Head -> Body with a dependency path from Body back to Head.
223
+ negation_cycle(Edges, [edge(Head, Body, negative)|Path]) :-
224
+ member(edge(Head, Body, negative), Edges),
225
+ dependency_path(Body, Head, Edges, [Body], Path),
226
+ !.
227
+
228
+ dependency_path(Current, Target, _Edges, _Seen, []) :-
229
+ Current == Target,
230
+ !.
231
+ dependency_path(Current, Target, Edges, Seen, [edge(Current, Next, Sign)|Path]) :-
232
+ member(edge(Current, Next, Sign), Edges),
233
+ ( Next == Target
234
+ -> Path = []
235
+ ; \+ memberchk(Next, Seen),
236
+ dependency_path(Next, Target, Edges, [Next|Seen], Path)
237
+ ).
package/src/checks.pl CHANGED
@@ -50,6 +50,8 @@
50
50
  check_subject_key_identity/1,
51
51
  check_subject_key_shape/1,
52
52
  check_ontology_quality/1,
53
+ check_kb_consistency/1,
54
+ check_kb_consistency_unresolved/1,
53
55
  run_checks_json/0, % Entry point for JSON output
54
56
  violation_id_text/2 % Extract text from entity ID term (exported for testing)
55
57
  ]).
@@ -75,6 +77,7 @@
75
77
  :- use_module('logic_ir.pl', [logic_rule_safety/2, logic_rule_from_props/2, logic_rule_render/2, logic_rule_conflict/3, logic_rule_conflict_witness/4, logic_rules_stratified/1, logic_rule_missing_keys/4, functional_predicate_declarations/1, logic_rule_property_form/2]).
76
78
  % GENERATED registry facts; single source is schema/rule-registry.json.
77
79
  :- use_module('rule_registry.pl', [known_rule/1]).
80
+ :- use_module('kb_consistency.pl', [check_kb_consistency/1, check_kb_consistency_unresolved/1]).
78
81
 
79
82
  % Required fields for all entities
80
83
  required_fields([id, title, status, created_at, updated_at, source]).
@@ -125,6 +128,8 @@ check_all(ViolationsDict) :-
125
128
  check_subject_key_identity(SubjectKeyIdentity),
126
129
  check_subject_key_shape(SubjectKeyShape),
127
130
  check_ontology_quality(OntologyQuality),
131
+ check_kb_consistency(KbConsistency),
132
+ check_kb_consistency_unresolved(KbConsistencyUnresolved),
128
133
  ViolationsDict = _{
129
134
  must_priority_coverage: MustPriority,
130
135
  symbol_coverage: SymbolCoverage,
@@ -159,7 +164,9 @@ check_all(ViolationsDict) :-
159
164
  domain_implication: DomainImplication,
160
165
  subject_key_identity: SubjectKeyIdentity,
161
166
  subject_key_shape: SubjectKeyShape,
162
- ontology_quality: OntologyQuality
167
+ ontology_quality: OntologyQuality,
168
+ kb_consistency: KbConsistency,
169
+ kb_consistency_unresolved: KbConsistencyUnresolved
163
170
  }.
164
171
 
165
172
  %% check_must_priority_coverage(-Violations)
@@ -3663,7 +3670,9 @@ check_selected_dispatch(Rules, _{
3663
3670
  domain_implication: DomainImplication,
3664
3671
  subject_key_identity: SubjectKeyIdentity,
3665
3672
  subject_key_shape: SubjectKeyShape,
3666
- ontology_quality: OntologyQuality
3673
+ ontology_quality: OntologyQuality,
3674
+ kb_consistency: KbConsistency,
3675
+ kb_consistency_unresolved: KbConsistencyUnresolved
3667
3676
  }) :-
3668
3677
  selected_rule(Rules, 'must-priority-coverage', check_must_priority_coverage, MustPriority),
3669
3678
  selected_rule(Rules, 'symbol-coverage', check_symbol_coverage, SymbolCoverage),
@@ -3698,7 +3707,9 @@ check_selected_dispatch(Rules, _{
3698
3707
  selected_rule(Rules, 'domain-implication', check_domain_implication, DomainImplication),
3699
3708
  selected_rule(Rules, 'subject-key-identity', check_subject_key_identity, SubjectKeyIdentity),
3700
3709
  selected_rule(Rules, 'subject-key-shape', check_subject_key_shape, SubjectKeyShape),
3701
- selected_rule(Rules, 'ontology-quality', check_ontology_quality, OntologyQuality).
3710
+ selected_rule(Rules, 'ontology-quality', check_ontology_quality, OntologyQuality),
3711
+ selected_rule(Rules, 'kb-consistency', check_kb_consistency, KbConsistency),
3712
+ selected_rule(Rules, 'kb-consistency-unresolved', check_kb_consistency_unresolved, KbConsistencyUnresolved).
3702
3713
 
3703
3714
  selected_rule(Rules, Name, Goal, Violations) :-
3704
3715
  ( memberchk(Name, Rules)
@@ -3770,6 +3781,8 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
3770
3781
  check_subject_key_identity(SubjectKeyIdentity),
3771
3782
  check_subject_key_shape(SubjectKeyShape),
3772
3783
  check_ontology_quality(OntologyQuality),
3784
+ check_kb_consistency(KbConsistency),
3785
+ check_kb_consistency_unresolved(KbConsistencyUnresolved),
3773
3786
  ViolationsDict = _{
3774
3787
  must_priority_coverage: MustPriority,
3775
3788
  symbol_coverage: SymbolCoverage,
@@ -3803,7 +3816,9 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
3803
3816
  domain_implication: DomainImplication,
3804
3817
  subject_key_identity: SubjectKeyIdentity,
3805
3818
  subject_key_shape: SubjectKeyShape,
3806
- ontology_quality: OntologyQuality
3819
+ ontology_quality: OntologyQuality,
3820
+ kb_consistency: KbConsistency,
3821
+ kb_consistency_unresolved: KbConsistencyUnresolved
3807
3822
  }.
3808
3823
 
3809
3824
  %% violations_dict_to_json(+ViolationsDict, -JsonDict)
package/src/discovery.pl CHANGED
@@ -398,7 +398,7 @@ coverage_report_json(By, Tags, IncludePassing, BindingMode, TestBindings, Includ
398
398
 
399
399
  % implements REQ-kibi-conservative-requirement-proof
400
400
  % Proof-status filtered report. The status filter selects requirement rows by
401
- % proofStatus (proven, missing, unresolved, not_applicable) and replaces the
401
+ % proofStatus (tested, missing, unresolved, not_applicable) and replaces the
402
402
  % default include-passing filtering, so callers can enumerate exactly one
403
403
  % slice — e.g. every not_applicable row together with its typed applicability
404
404
  % reason — without diffing full exports by hand. The summary is always
@@ -1127,7 +1127,7 @@ coverage_summary(Rows, Summary) :-
1127
1127
  include(req_row_missing_scenario, Rows, MissingScenarioRows),
1128
1128
  include(req_row_missing_test, Rows, MissingTestRows),
1129
1129
  include(req_row_missing_both, Rows, MissingBothRows),
1130
- include(req_row_proof_proven, Rows, ProofProvenRows),
1130
+ include(req_row_proof_tested, Rows, ProofTestedRows),
1131
1131
  include(req_row_proof_unresolved, Rows, ProofUnresolvedRows),
1132
1132
  include(req_row_proof_missing, Rows, ProofMissingRows),
1133
1133
  include(req_row_proof_not_applicable, Rows, ProofNotApplicableRows),
@@ -1138,7 +1138,8 @@ coverage_summary(Rows, Summary) :-
1138
1138
  length(MissingScenarioRows, MissingScenario),
1139
1139
  length(MissingTestRows, MissingTest),
1140
1140
  length(MissingBothRows, MissingScenarioAndTest),
1141
- length(ProofProvenRows, ProofProven),
1141
+ length(ProofTestedRows, ProofTested),
1142
+ evidence_level_counts(Rows, EvidenceLevels),
1142
1143
  length(ProofUnresolvedRows, ProofUnresolved),
1143
1144
  length(ProofMissingRows, ProofMissing),
1144
1145
  length(ProofNotApplicableRows, ProofNotApplicable),
@@ -1151,12 +1152,20 @@ coverage_summary(Rows, Summary) :-
1151
1152
  missingScenario: MissingScenario,
1152
1153
  missingTest: MissingTest,
1153
1154
  missingScenarioAndTest: MissingScenarioAndTest,
1154
- proofProven: ProofProven,
1155
+ proofTested: ProofTested,
1155
1156
  proofUnresolved: ProofUnresolved,
1156
1157
  proofMissing: ProofMissing,
1157
- proofNotApplicable: ProofNotApplicable
1158
+ proofNotApplicable: ProofNotApplicable,
1159
+ evidenceLevels: EvidenceLevels
1158
1160
  }.
1159
1161
 
1162
+ % implements REQ-kibi-evidence-levels
1163
+ evidence_level_counts(Rows, _{proved: Proved, tested: Tested, asserted: Asserted, unverified: Unverified}) :-
1164
+ aggregate_all(count, (member(Row, Rows), Row.get(evidenceLevel) == proved), Proved),
1165
+ aggregate_all(count, (member(Row, Rows), Row.get(evidenceLevel) == tested), Tested),
1166
+ aggregate_all(count, (member(Row, Rows), Row.get(evidenceLevel) == asserted), Asserted),
1167
+ aggregate_all(count, (member(Row, Rows), Row.get(evidenceLevel) == unverified), Unverified).
1168
+
1160
1169
  req_row_fully_covered(Row) :-
1161
1170
  CoverageStatus = Row.get(coverageStatus),
1162
1171
  CoverageStatus = fully_covered.
@@ -1175,8 +1184,8 @@ req_row_missing_test(Row) :-
1175
1184
  Row.gaps = [missing_test].
1176
1185
  req_row_missing_both(Row) :-
1177
1186
  Row.gaps = [missing_scenario_and_test].
1178
- req_row_proof_proven(Row) :-
1179
- Row.proofStatus == proven.
1187
+ req_row_proof_tested(Row) :-
1188
+ Row.proofStatus == tested.
1180
1189
  req_row_proof_unresolved(Row) :-
1181
1190
  Row.proofStatus == unresolved.
1182
1191
  req_row_proof_missing(Row) :-
@@ -1377,7 +1386,7 @@ filter_req_coverage_rows(false, Rows, Filtered) :-
1377
1386
 
1378
1387
  req_row_proof_passing(Row) :-
1379
1388
  Status = Row.proofStatus,
1380
- memberchk(Status, [proven, not_applicable]).
1389
+ memberchk(Status, [tested, not_applicable]).
1381
1390
 
1382
1391
  filter_symbol_coverage_rows(true, Rows, Rows).
1383
1392
  filter_symbol_coverage_rows(false, Rows, Filtered) :-
@@ -0,0 +1,213 @@
1
+ % Module: kb_consistency
2
+ % implements REQ-kibi-kb-certificates
3
+ % The kb-consistency check rules. Each run produces the certificate bundle
4
+ % (certificates.pl), round-trips it through JSON exactly as a saved bundle
5
+ % would be read, and re-checks it with the independent checker
6
+ % (certificate_check.pl). Only what the checker accepts becomes a finding:
7
+ %
8
+ % kb-consistency a re-verified contradiction (an irreducible
9
+ % core of property facts, two facts that
10
+ % disagree on an atom or a decision, or a
11
+ % negation cycle among stored rules), or a
12
+ % certificate the checker rejects
13
+ % kb-consistency-unresolved a context the checker cannot compare (mixed
14
+ % types or units, unsupported operators or
15
+ % polarities, windows that never overlap)
16
+ :- module(kb_consistency, [
17
+ check_kb_consistency/1,
18
+ check_kb_consistency_unresolved/1
19
+ ]).
20
+
21
+ :- encoding(utf8).
22
+
23
+ :- use_module(library(apply)).
24
+ :- use_module(library(lists)).
25
+ :- use_module(library(http/json)).
26
+ :- use_module('kb.pl', [kb_entity/3, kb_relationship/3]).
27
+ :- use_module('certificates.pl', [kb_certificates/1]).
28
+ :- use_module('certificate_check.pl', [verify_certificates/2]).
29
+
30
+ %% check_kb_consistency(-Violations)
31
+ check_kb_consistency(Violations) :-
32
+ consistency_verification(Bundle, Report),
33
+ numbered_certificates(Bundle, Numbered),
34
+ findall(Index, (member(Failure, Report.rejected), get_dict(certificate, Failure, Index)), Rejected),
35
+ findall(Violation,
36
+ ( member(Index-Certificate, Numbered),
37
+ \+ memberchk(Index, Rejected),
38
+ contradiction_violation(Certificate, Violation)
39
+ ),
40
+ Contradictions),
41
+ findall(Violation,
42
+ ( member(Failure, Report.rejected),
43
+ rejected_violation(Failure, Violation)
44
+ ),
45
+ RejectedViolations),
46
+ append(Contradictions, RejectedViolations, Unsorted),
47
+ sort(Unsorted, Violations).
48
+
49
+ %% check_kb_consistency_unresolved(-Violations)
50
+ check_kb_consistency_unresolved(Violations) :-
51
+ consistency_verification(_Bundle, Report),
52
+ findall(Violation,
53
+ ( member(Theorem, Report.theorems),
54
+ get_dict(unresolved, Theorem, Entries),
55
+ member(Entry, Entries),
56
+ unresolved_violation(Theorem.theorem, Entry, Violation)
57
+ ),
58
+ Unsorted),
59
+ sort(Unsorted, Violations).
60
+
61
+ consistency_verification(Bundle, Report) :-
62
+ kb_certificates(Produced),
63
+ with_output_to(string(Json), json_write_dict(current_output, Produced, [width(0)])),
64
+ open_string(Json, Stream),
65
+ json_read_dict(Stream, Bundle, [value_string_as(string)]),
66
+ verify_certificates(Bundle, Report).
67
+
68
+ numbered_certificates(Bundle, Numbered) :-
69
+ findall(Index-Certificate, nth1(Index, Bundle.certificates, Certificate), Numbered).
70
+
71
+ % ------------------------------------------------------------------
72
+ % Re-verified contradictions
73
+ % ------------------------------------------------------------------
74
+
75
+ contradiction_violation(Certificate, violation('kb-consistency', EntityId, Description, Suggestion, "", _{certificate: Certificate})) :-
76
+ get_dict(kind, Certificate, KindText),
77
+ atom_string(Kind, KindText),
78
+ contradiction_text(Kind, Certificate, Facts, Description),
79
+ fact_requirements(Facts, Requirements),
80
+ entity_label(Requirements, Facts, EntityId),
81
+ suggestion_text(Kind, Requirements, Suggestion).
82
+
83
+ contradiction_text(property_contradiction, Certificate, Facts, Description) :-
84
+ Facts = Certificate.core,
85
+ context_text(Certificate, Context),
86
+ atomic_list_concat(Facts, ', ', FactText),
87
+ findall(Text,
88
+ ( member(Drop, Certificate.dropModels),
89
+ format(string(Text), "~w: ~w", [Drop.factId, Drop.value])
90
+ ),
91
+ DropTexts),
92
+ atomic_list_concat(DropTexts, ', ', DropText),
93
+ format(string(Description),
94
+ "Facts ~w cannot all hold for ~w: no value satisfies them together, and dropping any one leaves a value (~w)",
95
+ [FactText, Context, DropText]).
96
+ contradiction_text(predicate_contradiction, Certificate, Facts, Description) :-
97
+ Facts = Certificate.core,
98
+ Facts = [FactA, FactB],
99
+ atom_text(Certificate, Atom),
100
+ format(string(Description),
101
+ "Facts ~w and ~w give ~w opposite truth values", [FactA, FactB, Atom]).
102
+ contradiction_text(decision_contradiction, Certificate, Facts, Description) :-
103
+ Facts = Certificate.core,
104
+ Facts = [FactA, FactB],
105
+ decision_text(Certificate, Decision),
106
+ format(string(Description),
107
+ "Facts ~w and ~w decide ~w differently: one allows it and the other denies it", [FactA, FactB, Decision]).
108
+ contradiction_text(rule_negation_cycle, Certificate, [], Description) :-
109
+ findall(Text,
110
+ ( member(Edge, Certificate.cycle),
111
+ signature_text(Edge.head, Head),
112
+ signature_text(Edge.body, Body),
113
+ ( atom_string(negative, Edge.sign) -> Arrow = "depends on not" ; Arrow = "depends on" ),
114
+ format(string(Text), "~w ~w ~w", [Head, Arrow, Body])
115
+ ),
116
+ Texts),
117
+ atomic_list_concat(Texts, "; ", CycleText),
118
+ format(string(Description),
119
+ "Stored rules are not stratified: a predicate depends on its own negation (~w)", [CycleText]).
120
+
121
+ suggestion_text(rule_negation_cycle, _Requirements,
122
+ "Break the cycle: change one rule so no predicate depends on its own negation, or supersede the requirement that stores it") :- !.
123
+ suggestion_text(_Kind, Requirements, Suggestion) :-
124
+ atomic_list_concat(Requirements, ', ', RequirementText),
125
+ format(string(Suggestion),
126
+ "Supersede or change one of ~w so the facts can hold together; kibi check --write-certificates <file> saves the certificate",
127
+ [RequirementText]).
128
+
129
+ % ------------------------------------------------------------------
130
+ % Rejected certificates and unresolved contexts
131
+ % ------------------------------------------------------------------
132
+
133
+ rejected_violation(Failure, violation('kb-consistency', "kb-certificates", Description,
134
+ "Kibi produced a certificate its own checker rejects. Report it with the bundle from kibi check --write-certificates <file>", "")) :-
135
+ ( get_dict(group, Failure, Group)
136
+ -> group_text(Group, Where)
137
+ ; format(string(Where), "certificate ~w (~w)", [Failure.certificate, Failure.kind])
138
+ ),
139
+ format(string(Description),
140
+ "The independent checker rejected ~w: ~w", [Where, Failure.reason]).
141
+
142
+ unresolved_violation(_Theorem, Entry, violation('kb-consistency-unresolved', EntityId, Description, Suggestion, "")) :-
143
+ group_text(Entry, Where),
144
+ ( get_dict(reason, Entry, Reason)
145
+ -> true
146
+ ; Reason = "the contradicting facts' validity windows never overlap"
147
+ ),
148
+ entry_facts(Entry, Facts),
149
+ fact_requirements(Facts, Requirements),
150
+ entity_label(Requirements, Facts, EntityId0),
151
+ ( EntityId0 == "" -> EntityId = Where ; EntityId = EntityId0 ),
152
+ format(string(Description),
153
+ "Kibi cannot decide whether the facts for ~w hold together: ~w. It never counts as consistent", [Where, Reason]),
154
+ Suggestion = "Align the facts' value_type, unit, operator or polarity so they can be compared, or accept that this context stays unchecked".
155
+
156
+ entry_facts(Entry, Facts) :-
157
+ get_dict(subjectKey, Entry, Subject),
158
+ !,
159
+ get_dict(propertyKey, Entry, Property),
160
+ findall(Fact,
161
+ ( kb:current_req(Req),
162
+ kb:effective_req_property_fact(Req, Subject, Fact, Property, _, _, _, _, _, _, _, _)
163
+ ),
164
+ Facts0),
165
+ sort(Facts0, Facts).
166
+ entry_facts(_Entry, []).
167
+
168
+ % ------------------------------------------------------------------
169
+ % Text helpers
170
+ % ------------------------------------------------------------------
171
+
172
+ fact_requirements(Facts, Requirements) :-
173
+ findall(Req,
174
+ ( member(FactText, Facts),
175
+ atom_string(Fact, FactText),
176
+ member(Type, [requires_property, requires_predicate, requires_rule]),
177
+ kb_relationship(Type, Req, Fact),
178
+ kb:current_req(Req)
179
+ ),
180
+ Requirements0),
181
+ sort(Requirements0, Requirements).
182
+
183
+ entity_label([], [], "") :- !.
184
+ entity_label([], Facts, Label) :- !, atomic_list_concat(Facts, '/', Label0), atom_string(Label0, Label).
185
+ entity_label(Requirements, _Facts, Label) :- atomic_list_concat(Requirements, '/', Label0), atom_string(Label0, Label).
186
+
187
+ context_text(Label, Text) :-
188
+ ( get_dict(scope, Label, Scope0) -> atom_string(Scope0, Scope) ; Scope = "" ),
189
+ ( Scope == ""
190
+ -> format(string(Text), "~w.~w", [Label.subjectKey, Label.propertyKey])
191
+ ; format(string(Text), "~w.~w (scope ~w)", [Label.subjectKey, Label.propertyKey, Scope])
192
+ ).
193
+
194
+ group_text(Group, Text) :-
195
+ ( get_dict(subjectKey, Group, _)
196
+ -> context_text(Group, Text)
197
+ ; get_dict(args, Group, _)
198
+ -> atom_text(Group, Text)
199
+ ; get_dict(key, Group, _)
200
+ -> decision_text(Group, Text)
201
+ ; format(string(Text), "~w", [Group])
202
+ ).
203
+
204
+ atom_text(Label, Text) :-
205
+ atomic_list_concat(Label.args, ', ', Args),
206
+ format(string(Text), "~w:~w(~w)", [Label.namespace, Label.name, Args]).
207
+
208
+ decision_text(Label, Text) :-
209
+ atomic_list_concat(Label.key, ', ', Key),
210
+ format(string(Text), "~w:~w/~w for (~w)", [Label.namespace, Label.name, Label.arity, Key]).
211
+
212
+ signature_text(Signature, Text) :-
213
+ format(string(Text), "~w:~w/~w", [Signature.namespace, Signature.name, Signature.arity]).