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,245 @@
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
+ set_texts/2
32
+ ]).
33
+
34
+ %% kb_write_certificates(+Path, -JsonString)
35
+ % Write the certificate bundle for the current KB to Path and return a
36
+ % summary (certificate count per kind) as a JSON string.
37
+ kb_write_certificates(Path, JsonString) :-
38
+ kb_certificates(Bundle),
39
+ setup_call_cleanup(
40
+ open(Path, write, Stream, [encoding(utf8)]),
41
+ ( json_write_dict(Stream, Bundle, [width(0)]), nl(Stream) ),
42
+ close(Stream)),
43
+ findall(Kind, (member(Certificate, Bundle.certificates), get_dict(kind, Certificate, Kind)), Kinds0),
44
+ msort(Kinds0, Kinds),
45
+ clumped(Kinds, Counts),
46
+ findall(Kind-Count, member(Kind-Count, Counts), Pairs),
47
+ dict_pairs(ByKind, _, Pairs),
48
+ length(Bundle.certificates, Total),
49
+ with_output_to(string(JsonString),
50
+ json_write_dict(current_output, _{path: Path, certificates: Total, byKind: ByKind}, [width(0)])).
51
+
52
+ %% kb_certificates(-Bundle)
53
+ kb_certificates(_{version: 'kibi.certificates.v1', certificates: Certificates}) :-
54
+ certificate_property_contexts(Contexts),
55
+ convlist(property_certificate, Contexts, PropertyCertificates),
56
+ certificate_predicate_groups(AtomGroups),
57
+ convlist(group_certificate(predicate), AtomGroups, PredicateCertificates),
58
+ certificate_decision_groups(DecisionGroups),
59
+ convlist(group_certificate(decision), DecisionGroups, DecisionCertificates),
60
+ certificate_rule_edges(Edges),
61
+ rule_certificates(Edges, RuleCertificates),
62
+ append([PropertyCertificates, PredicateCertificates, DecisionCertificates, RuleCertificates], Certificates).
63
+
64
+ % ------------------------------------------------------------------
65
+ % Property contexts
66
+ % ------------------------------------------------------------------
67
+
68
+ property_certificate(context(Subject, Property, Scope, Domain, Members), Certificate) :-
69
+ Base = _{subjectKey: Subject, propertyKey: Property, scope: Scope, domain: Domain},
70
+ ( members_model(Domain, Members, Value)
71
+ -> findall(Fact, member(m(Fact, _, _, _, _), Members), Facts),
72
+ Certificate = Base.put(_{kind: property_model, value: Value, facts: Facts})
73
+ ; irreducible_core(Domain, Members, Core),
74
+ findall(_{factId: Fact, value: Value},
75
+ ( member(Dropped, Core),
76
+ Dropped = m(Fact, _, _, _, _),
77
+ exclude(==(Dropped), Core, Rest),
78
+ members_model(Domain, Rest, Value)
79
+ ),
80
+ DropModels),
81
+ findall(Fact, member(m(Fact, _, _, _, _), Core), CoreFacts),
82
+ ( core_windows_overlap(Core)
83
+ -> Kind = property_contradiction
84
+ ; Kind = property_unresolved
85
+ ),
86
+ Certificate = Base.put(_{kind: Kind, core: CoreFacts, dropModels: DropModels})
87
+ ).
88
+
89
+ %% members_model(+Domain, +Members, -Value)
90
+ % A value satisfying every member, found among finitely many candidates that
91
+ % meet every region the members' bounds cut the line into.
92
+ members_model(Domain, Members, Value) :-
93
+ model_candidates(Domain, Members, Candidates),
94
+ member(Value, Candidates),
95
+ forall(member(Member, Members), certificate_member_holds(Domain, Member, Value)),
96
+ !.
97
+
98
+ model_candidates(Domain, Members, Candidates) :-
99
+ memberchk(Domain, [int, number]),
100
+ !,
101
+ findall(Bound, member(m(_, _, Bound, _, _), Members), Bounds0),
102
+ sort(Bounds0, Bounds),
103
+ ( Domain == int
104
+ -> findall(Candidate,
105
+ ( member(Bound, Bounds),
106
+ member(Base, [floor(Bound), ceiling(Bound)]),
107
+ member(Offset, [-1, 0, 1]),
108
+ Candidate is Base + Offset
109
+ ),
110
+ Candidates0)
111
+ ; findall(Candidate,
112
+ ( member(Bound, Bounds), member(Offset, [-1, 0, 1]), Candidate is Bound + Offset
113
+ ; nextto(Low, High, Bounds), Candidate is (Low + High) / 2
114
+ ),
115
+ Candidates0)
116
+ ),
117
+ sort([0|Candidates0], Candidates).
118
+ model_candidates(bool, _Members, [true, false]) :- !.
119
+ model_candidates(string, Members, Candidates) :-
120
+ findall(Bound, member(m(_, _, Bound, _, _), Members), Bounds0),
121
+ sort(Bounds0, Bounds),
122
+ fresh_text(Bounds, 0, Fresh),
123
+ append(Bounds, [Fresh], Candidates).
124
+
125
+ model_candidates(set, Members, Candidates) :-
126
+ findall(Set, (member(m(_, _, Bound, _, _), Members), set_texts(Bound, Set)), Sets0),
127
+ sort(Sets0, Sets),
128
+ findall(Text, (member(Set, Sets), member(Text, Set)), Texts),
129
+ fresh_text(Texts, 0, Fresh),
130
+ append(Sets, [[Fresh]], Candidates).
131
+
132
+ fresh_text(Bounds, Index, Fresh) :-
133
+ format(string(Candidate), "kibi:model:~w", [Index]),
134
+ ( member(Bound, Bounds), text_value(Bound, Candidate)
135
+ -> Next is Index + 1,
136
+ fresh_text(Bounds, Next, Fresh)
137
+ ; Fresh = Candidate
138
+ ).
139
+
140
+ text_value(Value, Text) :- atom(Value), !, atom_string(Value, Text).
141
+ text_value(Value, Text) :- string(Value), !, Text = Value.
142
+
143
+ %% irreducible_core(+Domain, +Members, -Core)
144
+ % Deletion filter: drop each member whose removal keeps the set
145
+ % unsatisfiable. What remains admits no value, and every proper subset
146
+ % does.
147
+ irreducible_core(Domain, Members, Core) :-
148
+ foldl(core_step(Domain), Members, Members, Core).
149
+
150
+ core_step(Domain, Member, Current, Next) :-
151
+ exclude(==(Member), Current, Without),
152
+ ( Without \== [],
153
+ \+ certificate_members_satisfiable(Domain, Without)
154
+ -> Next = Without
155
+ ; Next = Current
156
+ ).
157
+
158
+ core_windows_overlap(Core) :-
159
+ forall((member(m(_, _, _, FromA, ToA), Core), member(m(_, _, _, FromB, ToB), Core)),
160
+ kb:intervals_overlap(FromA, ToA, FromB, ToB)).
161
+
162
+ % ------------------------------------------------------------------
163
+ % Predicate atoms and decision keys
164
+ % ------------------------------------------------------------------
165
+
166
+ group_certificate(Prefix, group(Label, Members), Certificate) :-
167
+ atom_concat(Prefix, '_model', ModelKind),
168
+ atom_concat(Prefix, '_contradiction', ContradictionKind),
169
+ findall(Value, member(p(_, Value), Members), Values0),
170
+ sort(Values0, Values),
171
+ ( Values = [Value]
172
+ -> findall(Fact, member(p(Fact, _), Members), Facts),
173
+ Certificate = Label.put(_{kind: ModelKind, value: Value, facts: Facts})
174
+ ; Members = [p(FactA, ValueA)|_],
175
+ member(p(FactB, ValueB), Members),
176
+ ValueB \== ValueA,
177
+ !,
178
+ Certificate = Label.put(_{kind: ContradictionKind, core: [FactA, FactB]})
179
+ ).
180
+
181
+ % ------------------------------------------------------------------
182
+ % Rule stratification
183
+ % ------------------------------------------------------------------
184
+
185
+ rule_certificates([], []) :- !.
186
+ rule_certificates(Edges, [Certificate]) :-
187
+ findall(Signature, (member(edge(Head, Body, _), Edges), member(Signature, [Head, Body])), Signatures0),
188
+ sort(Signatures0, Signatures),
189
+ findall(Signature-0, member(Signature, Signatures), Initial),
190
+ length(Signatures, Count),
191
+ Rounds is Count + 1,
192
+ ( relax_strata(Rounds, Edges, Initial, Strata)
193
+ -> findall(_{namespace: Namespace, name: Name, arity: Arity, stratum: Stratum},
194
+ member(predicate(Namespace, Name, Arity)-Stratum, Strata),
195
+ Entries),
196
+ Certificate = _{kind: rule_strata, strata: Entries}
197
+ ; negation_cycle(Edges, Cycle),
198
+ findall(_{head: HeadDict, body: BodyDict, sign: Sign},
199
+ ( member(edge(Head, Body, Sign), Cycle),
200
+ signature_dict(Head, HeadDict),
201
+ signature_dict(Body, BodyDict)
202
+ ),
203
+ Entries),
204
+ Certificate = _{kind: rule_negation_cycle, cycle: Entries}
205
+ ).
206
+
207
+ signature_dict(predicate(Namespace, Name, Arity), _{namespace: Namespace, name: Name, arity: Arity}).
208
+
209
+ % Raise each head to at least its body's stratum (one more across a negated
210
+ % edge) until nothing changes. Without a negation cycle that happens within
211
+ % one round per predicate; a cycle keeps raising, and relaxation fails.
212
+ relax_strata(0, _Edges, _Strata, _Final) :- !, fail.
213
+ relax_strata(Rounds, Edges, Strata, Final) :-
214
+ foldl(relax_edge, Edges, Strata, Next),
215
+ ( Next == Strata
216
+ -> Final = Strata
217
+ ; Remaining is Rounds - 1,
218
+ relax_strata(Remaining, Edges, Next, Final)
219
+ ).
220
+
221
+ relax_edge(edge(Head, Body, Sign), Strata, Next) :-
222
+ memberchk(Head-HeadStratum, Strata),
223
+ memberchk(Body-BodyStratum, Strata),
224
+ ( Sign == negative -> Required is BodyStratum + 1 ; Required = BodyStratum ),
225
+ ( HeadStratum < Required
226
+ -> selectchk(Head-HeadStratum, Strata, Head-Required, Next)
227
+ ; Next = Strata
228
+ ).
229
+
230
+ % A negated edge Head -> Body with a dependency path from Body back to Head.
231
+ negation_cycle(Edges, [edge(Head, Body, negative)|Path]) :-
232
+ member(edge(Head, Body, negative), Edges),
233
+ dependency_path(Body, Head, Edges, [Body], Path),
234
+ !.
235
+
236
+ dependency_path(Current, Target, _Edges, _Seen, []) :-
237
+ Current == Target,
238
+ !.
239
+ dependency_path(Current, Target, Edges, Seen, [edge(Current, Next, Sign)|Path]) :-
240
+ member(edge(Current, Next, Sign), Edges),
241
+ ( Next == Target
242
+ -> Path = []
243
+ ; \+ memberchk(Next, Seen),
244
+ dependency_path(Next, Target, Edges, [Next|Seen], Path)
245
+ ).
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)
@@ -2370,7 +2377,7 @@ property_value_shape_error(Props, "Property value fact missing required field: o
2370
2377
  property_value_shape_error(Props, "Property value fact missing required field: value_type") :-
2371
2378
  memberchk(operator=_, Props),
2372
2379
  \+ memberchk(value_type=_, Props).
2373
- property_value_shape_error(Props, "Property value fact missing value field (value_string, value_int, value_number, or value_bool)") :-
2380
+ property_value_shape_error(Props, "Property value fact missing value field (value_string, value_int, value_number, value_bool, or value_set)") :-
2374
2381
  memberchk(value_type=_, Props),
2375
2382
  \+ has_value_field(Props).
2376
2383
  property_value_shape_error(Props, "Property value fact has multiple value fields (should have exactly one)") :-
@@ -2387,17 +2394,20 @@ is_value_field(value_string).
2387
2394
  is_value_field(value_int).
2388
2395
  is_value_field(value_number).
2389
2396
  is_value_field(value_bool).
2397
+ is_value_field(value_set).
2390
2398
 
2391
2399
  has_value_field(Props) :- memberchk(value_string=_, Props), !.
2392
2400
  has_value_field(Props) :- memberchk(value_int=_, Props), !.
2393
2401
  has_value_field(Props) :- memberchk(value_number=_, Props), !.
2394
2402
  has_value_field(Props) :- memberchk(value_bool=_, Props), !.
2403
+ has_value_field(Props) :- memberchk(value_set=_, Props), !.
2395
2404
 
2396
2405
  % value_type_matches_field(+ValueType, +Props)
2397
2406
  value_type_matches_field(string, Props) :- memberchk(value_string=_, Props), !.
2398
2407
  value_type_matches_field(int, Props) :- memberchk(value_int=_, Props), !.
2399
2408
  value_type_matches_field(number, Props) :- memberchk(value_number=_, Props), !.
2400
2409
  value_type_matches_field(bool, Props) :- memberchk(value_bool=_, Props), !.
2410
+ value_type_matches_field(set, Props) :- memberchk(value_set=_, Props), !.
2401
2411
 
2402
2412
  predicate_schema_shape_error(Props, "Predicate schema fact missing required field: predicate_name") :-
2403
2413
  \+ memberchk(predicate_name=_, Props).
@@ -3663,7 +3673,9 @@ check_selected_dispatch(Rules, _{
3663
3673
  domain_implication: DomainImplication,
3664
3674
  subject_key_identity: SubjectKeyIdentity,
3665
3675
  subject_key_shape: SubjectKeyShape,
3666
- ontology_quality: OntologyQuality
3676
+ ontology_quality: OntologyQuality,
3677
+ kb_consistency: KbConsistency,
3678
+ kb_consistency_unresolved: KbConsistencyUnresolved
3667
3679
  }) :-
3668
3680
  selected_rule(Rules, 'must-priority-coverage', check_must_priority_coverage, MustPriority),
3669
3681
  selected_rule(Rules, 'symbol-coverage', check_symbol_coverage, SymbolCoverage),
@@ -3698,7 +3710,9 @@ check_selected_dispatch(Rules, _{
3698
3710
  selected_rule(Rules, 'domain-implication', check_domain_implication, DomainImplication),
3699
3711
  selected_rule(Rules, 'subject-key-identity', check_subject_key_identity, SubjectKeyIdentity),
3700
3712
  selected_rule(Rules, 'subject-key-shape', check_subject_key_shape, SubjectKeyShape),
3701
- selected_rule(Rules, 'ontology-quality', check_ontology_quality, OntologyQuality).
3713
+ selected_rule(Rules, 'ontology-quality', check_ontology_quality, OntologyQuality),
3714
+ selected_rule(Rules, 'kb-consistency', check_kb_consistency, KbConsistency),
3715
+ selected_rule(Rules, 'kb-consistency-unresolved', check_kb_consistency_unresolved, KbConsistencyUnresolved).
3702
3716
 
3703
3717
  selected_rule(Rules, Name, Goal, Violations) :-
3704
3718
  ( memberchk(Name, Rules)
@@ -3770,6 +3784,8 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
3770
3784
  check_subject_key_identity(SubjectKeyIdentity),
3771
3785
  check_subject_key_shape(SubjectKeyShape),
3772
3786
  check_ontology_quality(OntologyQuality),
3787
+ check_kb_consistency(KbConsistency),
3788
+ check_kb_consistency_unresolved(KbConsistencyUnresolved),
3773
3789
  ViolationsDict = _{
3774
3790
  must_priority_coverage: MustPriority,
3775
3791
  symbol_coverage: SymbolCoverage,
@@ -3803,7 +3819,9 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
3803
3819
  domain_implication: DomainImplication,
3804
3820
  subject_key_identity: SubjectKeyIdentity,
3805
3821
  subject_key_shape: SubjectKeyShape,
3806
- ontology_quality: OntologyQuality
3822
+ ontology_quality: OntologyQuality,
3823
+ kb_consistency: KbConsistency,
3824
+ kb_consistency_unresolved: KbConsistencyUnresolved
3807
3825
  }.
3808
3826
 
3809
3827
  %% violations_dict_to_json(+ViolationsDict, -JsonDict)