kibi-core 0.14.1 → 0.15.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.
package/src/discovery.pl CHANGED
@@ -11,7 +11,8 @@
11
11
  requirement_proof_json/5,
12
12
  requirement_proof_json/7,
13
13
  symbol_proof_json/7,
14
- graph_expand_json/8
14
+ graph_expand_json/8,
15
+ search_answer_verdicts_json/2
15
16
  ]).
16
17
 
17
18
  % Source contains non-ASCII text; do not depend on the host locale.
@@ -25,7 +26,9 @@
25
26
  :- use_module('kb.pl').
26
27
  :- use_module(library(semweb/rdf_db), [rdf_generation/1]).
27
28
  :- use_module('requirement_proof.pl', [requirement_proof_context/1, requirement_proof_context/4, requirement_proof_context/6, requirement_proof/4]).
28
- :- use_module('status.pl', [status_meta_dict/1]).
29
+ :- use_module('checks.pl', [scenario_feasibility_outcome/2, infeasible_scenario/5]).
30
+ :- use_module('logic_ir.pl', [logic_rule_conflict/3, logic_rule_conflict/4]).
31
+ :- use_module('status.pl', [status_meta_dict/1, status_scope_dict/1]).
29
32
  :- use_module('../schema/relationships.pl', [relationship_type/1]).
30
33
 
31
34
  find_gaps_json(TypeFilter, MissingRelationships, PresentRelationships, Tags, SourceFilter, Limit, Offset, JsonString) :-
@@ -99,6 +102,214 @@ symbol_explain_role(SymbolId, Role) :-
99
102
  !.
100
103
  symbol_explain_role(_SymbolId, unknown).
101
104
 
105
+ %% search_answer_verdicts_json(+ReqIds, -JsonString)
106
+ % implements REQ-kibi-search-answer-layer-v2
107
+ % What the existing checks report about the requirements a kb_search answer
108
+ % names, in one engine round trip and bounded to those requirements:
109
+ % contradictions strict-property and predicate witnesses from
110
+ % kb:req_conflict_witness/3, and rule witnesses from the
111
+ % pair generator and rule comparison the
112
+ % domain-contradictions check uses (disjoint pairs omitted)
113
+ % scenarios scenario_feasibility_outcome/2 of each scenario the
114
+ % requirement specifies
115
+ % forbids other scenarios infeasible_scenario/5 says it forbids
116
+ % inventory the semantic-inventory stage of the proof ladder, with
117
+ % the propositions still ambiguous or ontology gaps
118
+ % Whole-KB scans (check_domain_contradiction_witnesses/1) are deliberately
119
+ % not used: they take seconds on a large KB. The functional-predicate
120
+ % declarations a rule comparison reads are collected once per call rather
121
+ % than once per compared pair, which dominated the cost for requirements with
122
+ % many rules. A requirement whose checks raise an error reports that error
123
+ % instead of failing the answer. Scope names the snapshot the verdicts were
124
+ % computed from.
125
+ search_answer_verdicts_json(ReqIds, JsonString) :-
126
+ search_answer_functional(Functional),
127
+ maplist(search_answer_verdict(Functional), ReqIds, Verdicts),
128
+ status_scope_dict(Scope),
129
+ dict_json_string(_{requirements: Verdicts, scope: Scope}, JsonString).
130
+
131
+ % The declarations logic_rule_conflict/3 would read for every pair; when they
132
+ % cannot be read here, each comparison reads them itself.
133
+ search_answer_functional(some(Functional)) :-
134
+ catch(logic_ir:functional_predicate_declarations(Functional), _, fail),
135
+ !.
136
+ search_answer_functional(none).
137
+
138
+ search_answer_rule_status(some(Functional), RuleA, RuleB, Status) :-
139
+ !,
140
+ logic_rule_conflict(RuleA, RuleB, Functional, Status).
141
+ search_answer_rule_status(none, RuleA, RuleB, Status) :-
142
+ logic_rule_conflict(RuleA, RuleB, Status).
143
+
144
+ search_answer_verdict(Functional, ReqId, Verdict) :-
145
+ catch(search_answer_verdict_(Functional, ReqId, Verdict0), Error, true),
146
+ ( var(Error)
147
+ -> Verdict = Verdict0
148
+ ; format(string(Text0), "~q", [Error]),
149
+ search_answer_clip(Text0, 300, Text),
150
+ Verdict = _{id: ReqId, error: Text}
151
+ ).
152
+
153
+ search_answer_verdict_(Functional, ReqId, _{
154
+ id: ReqId,
155
+ contradictions: Contradictions,
156
+ scenarios: Scenarios,
157
+ forbids: Forbids,
158
+ inventory: Inventory
159
+ }) :-
160
+ findall(C, search_answer_contradiction(Functional, ReqId, C), Contradictions0),
161
+ sort(Contradictions0, Contradictions),
162
+ findall(S, search_answer_scenario_outcome(ReqId, S), Scenarios0),
163
+ sort(Scenarios0, Scenarios),
164
+ findall(F, search_answer_forbidden_scenario(ReqId, F), Forbids0),
165
+ sort(Forbids0, Forbids),
166
+ search_answer_inventory(ReqId, Inventory).
167
+
168
+ search_answer_contradiction(_Functional, ReqId, _{
169
+ kind: Kind,
170
+ status: Status,
171
+ with: Other,
172
+ facts: Facts,
173
+ reason: Reason
174
+ }) :-
175
+ ( req_conflict_witness(ReqId, Other, Witness)
176
+ ; req_conflict_witness(Other, ReqId, Witness)
177
+ ),
178
+ get_dict(kind, Witness, Kind),
179
+ get_dict(status, Witness, Status),
180
+ get_dict(reason, Witness, RawReason),
181
+ search_answer_text(RawReason, Reason),
182
+ findall(FactId,
183
+ ( member(SideKey, [left, right]),
184
+ get_dict(SideKey, Witness, Side),
185
+ get_dict(factId, Side, FactId)
186
+ ),
187
+ Facts).
188
+ search_answer_contradiction(Functional, ReqId, _{
189
+ kind: rule,
190
+ status: Status,
191
+ with: Other,
192
+ facts: [FactA, FactB],
193
+ reason: Reason
194
+ }) :-
195
+ % The check orders each pair; compare it in that order and only then
196
+ % orient the result towards ReqId.
197
+ ( checks:opposing_rule_requirement_facts(ReqId, Other, FactA, FactB, RuleLeft, RuleRight),
198
+ LeftReq = ReqId, RightReq = Other
199
+ ; checks:opposing_rule_requirement_facts(Other, ReqId, FactB, FactA, RuleLeft, RuleRight),
200
+ LeftReq = Other, RightReq = ReqId
201
+ ),
202
+ search_answer_rule_status(Functional, RuleLeft, RuleRight, Status),
203
+ Status \== disjoint,
204
+ format(string(Reason), "Rule conflict (~w) between ~w and ~w", [Status, LeftReq, RightReq]).
205
+
206
+ search_answer_scenario_outcome(ReqId, Outcome) :-
207
+ kb_relationship(specified_by, ReqId, ScenarioId),
208
+ scenario_feasibility_outcome(ScenarioId, Result),
209
+ search_answer_outcome(ScenarioId, Result, Outcome).
210
+
211
+ search_answer_outcome(ScenarioId, infeasible(witness(ReqIds, AssumedFacts, ReqFacts, RawReason)), _{
212
+ scenario: ScenarioId,
213
+ outcome: infeasible,
214
+ requirement: ReqId,
215
+ requirements: ReqIds,
216
+ assumed: AssumedFacts,
217
+ fact: ReqFacts,
218
+ reason: Reason
219
+ }) :-
220
+ !,
221
+ ( ReqIds = [ReqId|_] -> true ; ReqId = '' ),
222
+ search_answer_text(RawReason, Reason).
223
+ search_answer_outcome(ScenarioId, unknown(Reason), _{
224
+ scenario: ScenarioId,
225
+ outcome: unknown,
226
+ reason: Reason,
227
+ facts: []
228
+ }) :-
229
+ atom(Reason),
230
+ !.
231
+ search_answer_outcome(ScenarioId, unknown(Compound), _{
232
+ scenario: ScenarioId,
233
+ outcome: unknown,
234
+ reason: Reason,
235
+ facts: Ids
236
+ }) :-
237
+ compound(Compound),
238
+ Compound =.. [Reason, Ids0],
239
+ is_list(Ids0),
240
+ !,
241
+ Ids = Ids0.
242
+ search_answer_outcome(ScenarioId, not_applicable(Compound), _{
243
+ scenario: ScenarioId,
244
+ outcome: not_applicable,
245
+ reason: Reason,
246
+ facts: Ids
247
+ }) :-
248
+ compound(Compound),
249
+ Compound =.. [Reason, Ids],
250
+ is_list(Ids),
251
+ !.
252
+ search_answer_outcome(ScenarioId, Result, _{scenario: ScenarioId, outcome: Outcome}) :-
253
+ ( atom(Result)
254
+ -> Outcome = Result
255
+ ; format(atom(Outcome), "~q", [Result])
256
+ ).
257
+
258
+ search_answer_forbidden_scenario(ReqId, _{
259
+ scenario: ScenarioId,
260
+ assumed: AssumedFacts,
261
+ fact: ReqFacts,
262
+ reason: Reason
263
+ }) :-
264
+ infeasible_scenario(ScenarioId, ReqIds, AssumedFacts, ReqFacts, RawReason),
265
+ memberchk(ReqId, ReqIds),
266
+ \+ kb_relationship(specified_by, ReqId, ScenarioId),
267
+ search_answer_text(RawReason, Reason).
268
+
269
+ search_answer_inventory(ReqId, _{
270
+ status: Status,
271
+ propositionCount: Count,
272
+ unresolved: Unresolved
273
+ }) :-
274
+ kb_entity(ReqId, req, Props),
275
+ !,
276
+ requirement_proof:semantic_inventory_stage(Props, Stage, Entries),
277
+ get_dict(status, Stage, Status),
278
+ get_dict(propositionCount, Stage, Count),
279
+ findall(_{claim: Claim, status: EntryStatus},
280
+ ( member(Entry, Entries),
281
+ requirement_proof:inventory_entry_unresolved(Entry),
282
+ requirement_proof:inventory_entry_status(Entry, EntryStatus),
283
+ requirement_proof:inventory_entry_field(Entry, claim_text, RawClaim),
284
+ search_answer_text(RawClaim, Claim0),
285
+ search_answer_clip(Claim0, 300, Claim)
286
+ ),
287
+ Unresolved).
288
+ search_answer_inventory(_ReqId, _{status: unknown, propositionCount: 0, unresolved: []}).
289
+
290
+ search_answer_text(^^(Value, _Type), Text) :-
291
+ !,
292
+ search_answer_text(Value, Text).
293
+ search_answer_text(Value, Text) :-
294
+ string(Value),
295
+ !,
296
+ Text = Value.
297
+ search_answer_text(Value, Text) :-
298
+ atom(Value),
299
+ !,
300
+ atom_string(Value, Text).
301
+ search_answer_text(Value, Text) :-
302
+ format(string(Text), "~w", [Value]).
303
+
304
+ search_answer_clip(Text, Max, Clipped) :-
305
+ string_length(Text, Length),
306
+ ( Length =< Max
307
+ -> Clipped = Text
308
+ ; Keep is Max - 1,
309
+ sub_string(Text, 0, Keep, _, Prefix),
310
+ string_concat(Prefix, "…", Clipped)
311
+ ).
312
+
102
313
  % Slim per-requirement proof evidence for quality diagnostics (W1 push-down).
103
314
  % Projects only the fields the quality diagnostics consume - proof status,
104
315
  % the passing-e2e stage summary, and receipt-related gap codes - instead of
@@ -167,7 +378,7 @@ coverage_report_json(By, Tags, IncludePassing, BindingMode, TestBindings, Includ
167
378
  Response = _{summary: Summary, rows: Rows, meta: Meta},
168
379
  dict_json_string(Response, JsonString).
169
380
 
170
- % implements REQ-kibi-coverage-status-filter
381
+ % implements REQ-kibi-conservative-requirement-proof
171
382
  % Proof-status filtered report. The status filter selects requirement rows by
172
383
  % proofStatus (proven, missing, unresolved, not_applicable) and replaces the
173
384
  % default include-passing filtering, so callers can enumerate exactly one
@@ -0,0 +1,175 @@
1
+ % implements REQ-kibi-truthful-consistency
2
+ %
3
+ % Exact satisfiability for one-variable numeric comparisons.
4
+ %
5
+ % A constraint is c(Op, Variable, Value) where Op is one of eq, neq, lt, lte,
6
+ % gt, gte and Value is a number. Variables are ordinary Prolog variables and
7
+ % are compared by identity, never bound. Each variable ranges over the reals
8
+ % unless the caller names it as integer-valued; the conjunction is
9
+ % satisfiable exactly when every variable's set is non-empty. Strict bounds,
10
+ % non-integer bounds on integer variables and excluded points are handled
11
+ % without scaling or labelling, so the answer is exact and terminates in
12
+ % linear time.
13
+ :- module(intervals, [
14
+ numeric_constraints_satisfiable/1,
15
+ numeric_constraints_satisfiable/2,
16
+ numeric_constraint_entailed/2,
17
+ numeric_constraint_entailed/3,
18
+ numeric_constraint_negation/2,
19
+ numeric_constraint_holds/3
20
+ ]).
21
+
22
+ %% numeric_constraints_satisfiable(+Constraints)
23
+ % True when some assignment of reals satisfies every constraint.
24
+ numeric_constraints_satisfiable(Constraints) :-
25
+ numeric_constraints_satisfiable(Constraints, []).
26
+
27
+ %% numeric_constraints_satisfiable(+Constraints, +IntegerVariables)
28
+ % As numeric_constraints_satisfiable/1, but every variable in
29
+ % IntegerVariables (compared by identity) ranges over the integers: no
30
+ % integer lies strictly between 0 and 1, and eq 0.5 has no integer solution.
31
+ % IntegerVariables may also be the atom `all`, making every variable integral.
32
+ numeric_constraints_satisfiable(Constraints, IntegerVariables) :-
33
+ term_variables(Constraints, Variables),
34
+ forall(
35
+ member(Variable, Variables),
36
+ ( integer_variable(Variable, IntegerVariables)
37
+ -> integer_variable_feasible(Variable, Constraints)
38
+ ; variable_feasible(Variable, Constraints)
39
+ )
40
+ ).
41
+
42
+ %% numeric_constraint_entailed(+Premises, +Constraint)
43
+ % True when every assignment that satisfies Premises satisfies Constraint.
44
+ numeric_constraint_entailed(Premises, Constraint) :-
45
+ numeric_constraint_entailed(Premises, Constraint, []).
46
+
47
+ %% numeric_constraint_entailed(+Premises, +Constraint, +IntegerVariables)
48
+ numeric_constraint_entailed(Premises, Constraint, IntegerVariables) :-
49
+ numeric_constraint_negation(Constraint, Negated),
50
+ \+ numeric_constraints_satisfiable([Negated|Premises], IntegerVariables).
51
+
52
+ integer_variable(_, all) :- !.
53
+ integer_variable(Variable, IntegerVariables) :-
54
+ is_list(IntegerVariables),
55
+ member(Candidate, IntegerVariables),
56
+ Candidate == Variable,
57
+ !.
58
+
59
+ %% numeric_constraint_negation(+Constraint, -Negated)
60
+ numeric_constraint_negation(c(Op, Variable, Value), c(Negated, Variable, Value)) :-
61
+ negated_operator(Op, Negated).
62
+
63
+ negated_operator(eq, neq).
64
+ negated_operator(neq, eq).
65
+ negated_operator(lt, gte).
66
+ negated_operator(gte, lt).
67
+ negated_operator(lte, gt).
68
+ negated_operator(gt, lte).
69
+
70
+ %% numeric_constraint_holds(+Op, +Left, +Right)
71
+ % Evaluate a comparison between two numbers.
72
+ numeric_constraint_holds(eq, Left, Right) :- Left =:= Right.
73
+ numeric_constraint_holds(neq, Left, Right) :- Left =\= Right.
74
+ numeric_constraint_holds(lt, Left, Right) :- Left < Right.
75
+ numeric_constraint_holds(lte, Left, Right) :- Left =< Right.
76
+ numeric_constraint_holds(gt, Left, Right) :- Left > Right.
77
+ numeric_constraint_holds(gte, Left, Right) :- Left >= Right.
78
+
79
+ variable_feasible(Variable, Constraints) :-
80
+ findall(Op-Value, (member(c(Op, Other, Value), Constraints), Other == Variable), Bounds),
81
+ foldl(apply_bound, Bounds, interval(ninf, open, pinf, open, []), Interval),
82
+ interval_non_empty(Interval).
83
+
84
+ % interval(Low, LowKind, High, HighKind, Excluded)
85
+ apply_bound(eq-Value, interval(Lo, LoK, Hi, HiK, Ex), interval(Lo1, LoK1, Hi1, HiK1, Ex)) :-
86
+ raise_low(Lo, LoK, Value, closed, Lo1, LoK1),
87
+ lower_high(Hi, HiK, Value, closed, Hi1, HiK1).
88
+ apply_bound(neq-Value, interval(Lo, LoK, Hi, HiK, Ex), interval(Lo, LoK, Hi, HiK, [Value|Ex])).
89
+ apply_bound(lt-Value, interval(Lo, LoK, Hi, HiK, Ex), interval(Lo, LoK, Hi1, HiK1, Ex)) :-
90
+ lower_high(Hi, HiK, Value, open, Hi1, HiK1).
91
+ apply_bound(lte-Value, interval(Lo, LoK, Hi, HiK, Ex), interval(Lo, LoK, Hi1, HiK1, Ex)) :-
92
+ lower_high(Hi, HiK, Value, closed, Hi1, HiK1).
93
+ apply_bound(gt-Value, interval(Lo, LoK, Hi, HiK, Ex), interval(Lo1, LoK1, Hi, HiK, Ex)) :-
94
+ raise_low(Lo, LoK, Value, open, Lo1, LoK1).
95
+ apply_bound(gte-Value, interval(Lo, LoK, Hi, HiK, Ex), interval(Lo1, LoK1, Hi, HiK, Ex)) :-
96
+ raise_low(Lo, LoK, Value, closed, Lo1, LoK1).
97
+
98
+ raise_low(ninf, _, Value, Kind, Value, Kind) :- !.
99
+ raise_low(Lo, _, Value, Kind, Value, Kind) :- Value > Lo, !.
100
+ raise_low(Lo, LoK, Value, Kind, Lo, Kind1) :- Value =:= Lo, !, tighter_kind(LoK, Kind, Kind1).
101
+ raise_low(Lo, LoK, _, _, Lo, LoK).
102
+
103
+ lower_high(pinf, _, Value, Kind, Value, Kind) :- !.
104
+ lower_high(Hi, _, Value, Kind, Value, Kind) :- Value < Hi, !.
105
+ lower_high(Hi, HiK, Value, Kind, Hi, Kind1) :- Value =:= Hi, !, tighter_kind(HiK, Kind, Kind1).
106
+ lower_high(Hi, HiK, _, _, Hi, HiK).
107
+
108
+ tighter_kind(closed, closed, closed) :- !.
109
+ tighter_kind(_, _, open).
110
+
111
+ % An unbounded or non-degenerate real interval cannot be emptied by finitely
112
+ % many excluded points. A degenerate interval is a single closed point.
113
+ interval_non_empty(interval(ninf, _, _, _, _)) :- !.
114
+ interval_non_empty(interval(_, _, pinf, _, _)) :- !.
115
+ interval_non_empty(interval(Lo, _, Hi, _, _)) :- Lo < Hi, !.
116
+ interval_non_empty(interval(Lo, closed, Hi, closed, Excluded)) :-
117
+ Lo =:= Hi,
118
+ \+ ( member(Value, Excluded), Value =:= Lo ).
119
+
120
+ % Integer variables: every strict or fractional bound is tightened to the
121
+ % nearest admissible integer, so the set is an integer range [Lo, Hi] minus
122
+ % finitely many excluded integers. An eq bound with a fractional value admits
123
+ % no integer at all.
124
+ integer_variable_feasible(Variable, Constraints) :-
125
+ findall(Op-Value, (member(c(Op, Other, Value), Constraints), Other == Variable), Bounds),
126
+ foldl(apply_integer_bound, Bounds, ibox(ninf, pinf, []), Box),
127
+ integer_box_non_empty(Box).
128
+
129
+ apply_integer_bound(_, empty, empty) :- !.
130
+ apply_integer_bound(eq-Value, ibox(Lo, Hi, Ex), Box) :- !,
131
+ ( integral_value(Value, Integer)
132
+ -> integer_raise_low(Lo, Integer, Lo1),
133
+ integer_lower_high(Hi, Integer, Hi1),
134
+ Box = ibox(Lo1, Hi1, Ex)
135
+ ; Box = empty
136
+ ).
137
+ apply_integer_bound(neq-Value, ibox(Lo, Hi, Ex), ibox(Lo, Hi, Ex1)) :- !,
138
+ ( integral_value(Value, Integer)
139
+ -> Ex1 = [Integer|Ex]
140
+ ; Ex1 = Ex
141
+ ).
142
+ apply_integer_bound(gt-Value, ibox(Lo, Hi, Ex), ibox(Lo1, Hi, Ex)) :- !,
143
+ Bound is floor(Value) + 1,
144
+ integer_raise_low(Lo, Bound, Lo1).
145
+ apply_integer_bound(gte-Value, ibox(Lo, Hi, Ex), ibox(Lo1, Hi, Ex)) :- !,
146
+ Bound is ceiling(Value),
147
+ integer_raise_low(Lo, Bound, Lo1).
148
+ apply_integer_bound(lt-Value, ibox(Lo, Hi, Ex), ibox(Lo, Hi1, Ex)) :- !,
149
+ Bound is ceiling(Value) - 1,
150
+ integer_lower_high(Hi, Bound, Hi1).
151
+ apply_integer_bound(lte-Value, ibox(Lo, Hi, Ex), ibox(Lo, Hi1, Ex)) :- !,
152
+ Bound is floor(Value),
153
+ integer_lower_high(Hi, Bound, Hi1).
154
+
155
+ integral_value(Value, Integer) :-
156
+ number(Value),
157
+ Integer is round(Value),
158
+ Integer =:= Value.
159
+
160
+ integer_raise_low(ninf, Bound, Bound) :- !.
161
+ integer_raise_low(Lo, Bound, Lo1) :- Lo1 is max(Lo, Bound).
162
+
163
+ integer_lower_high(pinf, Bound, Bound) :- !.
164
+ integer_lower_high(Hi, Bound, Hi1) :- Hi1 is min(Hi, Bound).
165
+
166
+ % An unbounded integer range cannot be emptied by finitely many exclusions.
167
+ integer_box_non_empty(empty) :- !, fail.
168
+ integer_box_non_empty(ibox(ninf, _, _)) :- !.
169
+ integer_box_non_empty(ibox(_, pinf, _)) :- !.
170
+ integer_box_non_empty(ibox(Lo, Hi, Excluded)) :-
171
+ Lo =< Hi,
172
+ findall(Value, (member(Value, Excluded), Value >= Lo, Value =< Hi), Inside0),
173
+ sort(Inside0, Inside),
174
+ length(Inside, ExcludedCount),
175
+ Hi - Lo + 1 > ExcludedCount.
package/src/kb.pl CHANGED
@@ -59,6 +59,7 @@
59
59
  deprecated_no_successor/1,
60
60
  symbol_no_req_coverage/2,
61
61
  predicate_schema/6,
62
+ predicate_schema_keys/4,
62
63
  predicate_fact/5,
63
64
  canonical_property_tuple/9,
64
65
  contradicting_reqs/3,
@@ -85,6 +86,7 @@
85
86
  :- use_module('../schema/entities.pl', [entity_type/1, entity_property/3, required_property/2]).
86
87
  :- use_module('../schema/relationships.pl', [relationship_type/1, valid_relationship/3]).
87
88
  :- use_module('units.pl', [canonical_quantity/6]).
89
+ :- use_module('intervals.pl', [numeric_constraints_satisfiable/2]).
88
90
  :- use_module('../schema/validation.pl', [validate_entity/2, validate_relationship/3]).
89
91
 
90
92
  % Constants
@@ -2569,8 +2571,8 @@ req_conflict_witness(ReqA, ReqB, Witness) :-
2569
2571
  intervals_overlap(ValidFromA, ValidToA, ValidFromB, ValidToB),
2570
2572
  % Compare canonical quantities (30 min == 1800 s); witnesses keep the
2571
2573
  % authored values so evidence still points at the source text.
2572
- canonical_quantity(ValTypeA, ValA, UnitA, CanonTypeA, CanonValA, CanonUnitA),
2573
- canonical_quantity(ValTypeB, ValB, UnitB, CanonTypeB, CanonValB, CanonUnitB),
2574
+ comparison_quantity(ValTypeA, ValA, UnitA, CanonTypeA, CanonValA, CanonUnitA),
2575
+ comparison_quantity(ValTypeB, ValB, UnitB, CanonTypeB, CanonValB, CanonUnitB),
2574
2576
  ( polarity_conflict(SubjectKey, PropertyKey, OpA, CanonTypeA, CanonValA, CanonUnitA, ScopeA, PolarityA,
2575
2577
  OpB, CanonTypeB, CanonValB, CanonUnitB, ScopeB, PolarityB, Reason)
2576
2578
  ; property_conflict(SubjectKey, PropertyKey, OpA, CanonTypeA, CanonValA, CanonUnitA, PolarityA,
@@ -2728,6 +2730,21 @@ canonical_property_tuple(FactId, Subject, Property, Op, CanonType, CanonValue, C
2728
2730
  fact_property_tuple(FactId, Subject, Property, Op, ValType, Value, Unit, Scope, Polarity),
2729
2731
  canonical_quantity(ValType, Value, Unit, CanonType, CanonValue, CanonUnit).
2730
2732
 
2733
+ %% comparison_quantity(+ValType, +Value, +Unit, -Type, -CanonValue, -CanonUnit)
2734
+ % implements REQ-kibi-truthful-consistency
2735
+ % canonical_quantity/6 for comparisons that depend on the value domain.
2736
+ % Canonicalization reports any integral value as int, but a value declared
2737
+ % `number` keeps real semantics: number gt 0 and number lt 1 do not conflict
2738
+ % even though 0 and 1 are integral. An int that scales to a fraction
2739
+ % (int 1 ms is 0.001 s) is compared as a number, which only admits more
2740
+ % values.
2741
+ comparison_quantity(ValType, Value, Unit, Type, CanonValue, CanonUnit) :-
2742
+ canonical_quantity(ValType, Value, Unit, CanonType, CanonValue, CanonUnit),
2743
+ ( ValType == number, CanonType == int
2744
+ -> Type = number
2745
+ ; Type = CanonType
2746
+ ).
2747
+
2731
2748
  %% predicate_schema(+FactId, -Namespace, -Name, -Arity, -ArgumentNames, -ArgumentTypes)
2732
2749
  % Read one project-local ontology predicate schema fact.
2733
2750
  predicate_schema(FactId, Namespace, Name, Arity, ArgumentNames, ArgumentTypes) :-
@@ -2744,6 +2761,27 @@ predicate_schema(FactId, Namespace, Name, Arity, ArgumentNames, ArgumentTypes) :
2744
2761
  memberchk(argument_types=ArgumentTypesRaw, Props),
2745
2762
  normalize_term_atom_list(ArgumentTypesRaw, ArgumentTypes).
2746
2763
 
2764
+ %% predicate_schema_keys(?Namespace, ?Name, ?Arity, -KeyPositions)
2765
+ % implements REQ-kibi-truthful-consistency
2766
+ % A predicate_schema that declares key_arguments states a functional
2767
+ % dependency: the remaining arguments are determined by the key arguments.
2768
+ % KeyPositions are the 1-based positions of the named key arguments. A
2769
+ % predicate without this declaration is treated as multivalued, so two atoms
2770
+ % of it are never assumed to describe the same fact.
2771
+ predicate_schema_keys(Namespace, Name, Arity, KeyPositions) :-
2772
+ predicate_schema(FactId, Namespace, Name, Arity, ArgumentNames, _ArgumentTypes),
2773
+ kb_entity(FactId, fact, Props),
2774
+ memberchk(key_arguments=KeysRaw, Props),
2775
+ normalize_term_atom_list(KeysRaw, KeyNames),
2776
+ KeyNames \= [],
2777
+ length(ArgumentNames, Arity),
2778
+ maplist(argument_name_position(ArgumentNames), KeyNames, Positions0),
2779
+ sort(Positions0, KeyPositions).
2780
+
2781
+ argument_name_position(ArgumentNames, KeyName, Position) :-
2782
+ nth1(Position, ArgumentNames, KeyName),
2783
+ !.
2784
+
2747
2785
  %% predicate_fact(+FactId, -Namespace, -Name, -Args, -Polarity)
2748
2786
  % Read one ground ontology predicate fact.
2749
2787
  predicate_fact(FactId, Namespace, Name, Args, Polarity) :-
@@ -2842,9 +2880,23 @@ property_conflict(Subject, Property, OpA, TypeA, ValA, UnitA, Polarity,
2842
2880
  OpB, TypeB, ValB, UnitB, Polarity, Reason) :-
2843
2881
  unit_compatible(UnitA, UnitB),
2844
2882
  compatible_types(TypeA, TypeB),
2845
- values_conflict(OpA, ValA, OpB, ValB, TypeA),
2883
+ conflict_value_type(TypeA, TypeB, Type),
2884
+ values_conflict(OpA, ValA, OpB, ValB, Type),
2846
2885
  format(atom(Reason), 'Value conflict on ~w.~w: ~w ~w vs ~w ~w', [Subject, Property, OpA, ValA, OpB, ValB]).
2847
2886
 
2887
+ %% conflict_value_type(+TypeA, +TypeB, -Type)
2888
+ % The value domain both constraints are compared in. A property is treated as
2889
+ % integer-valued only when both sides declare int; a mixed int/number pair is
2890
+ % compared over the reals so a conflict is never claimed from one side's
2891
+ % narrower declaration.
2892
+ % implements REQ-kibi-truthful-consistency
2893
+ conflict_value_type(int, int, int) :- !.
2894
+ conflict_value_type(TypeA, TypeB, number) :-
2895
+ is_numeric_type(TypeA),
2896
+ is_numeric_type(TypeB),
2897
+ !.
2898
+ conflict_value_type(Type, _, Type).
2899
+
2848
2900
  %% compatible_types(+TypeA, +TypeB)
2849
2901
  % Types are compatible if they are the same or both numeric.
2850
2902
  compatible_types(T, T) :- !.
@@ -2898,33 +2950,28 @@ values_conflict(eq, ValA, neq, ValB, Type) :-
2898
2950
  values_conflict(neq, ValA, eq, ValB, Type) :-
2899
2951
  same_value(Type, ValA, Type, ValB).
2900
2952
 
2901
- % Numeric gap conflict: lte X vs gte Y where X < Y
2902
- values_conflict(lte, ValA, gte, ValB, Type) :-
2903
- is_numeric_type(Type),
2904
- ValA < ValB.
2905
- values_conflict(gte, ValB, lte, ValA, Type) :-
2906
- is_numeric_type(Type),
2907
- ValA < ValB.
2908
-
2909
- % Also catch lt/gt variants
2910
- values_conflict(lt, ValA, gt, ValB, Type) :-
2911
- is_numeric_type(Type),
2912
- ValA =< ValB.
2913
- values_conflict(gt, ValB, lt, ValA, Type) :-
2914
- is_numeric_type(Type),
2915
- ValA =< ValB.
2916
- values_conflict(lt, ValA, gte, ValB, Type) :-
2917
- is_numeric_type(Type),
2918
- ValA =< ValB.
2919
- values_conflict(gte, ValB, lt, ValA, Type) :-
2920
- is_numeric_type(Type),
2921
- ValA =< ValB.
2922
- values_conflict(lte, ValA, gt, ValB, Type) :-
2923
- is_numeric_type(Type),
2924
- ValA < ValB.
2925
- values_conflict(gt, ValB, lte, ValA, Type) :-
2953
+ % Numeric conflict: the two comparisons admit no common value of the type's
2954
+ % domain. `number` ranges over the reals; `int` ranges over the integers, so
2955
+ % gt 0 and lt 1 conflict for int (no integer lies strictly between them) but
2956
+ % not for number. This covers every operator pair (eq/gt, lte/gt at the same
2957
+ % bound, gte/gt, ...) instead of a hand-written table. neq/neq never
2958
+ % conflicts and eq/neq is handled above.
2959
+ % implements REQ-kibi-truthful-consistency
2960
+ values_conflict(OpA, ValA, OpB, ValB, Type) :-
2926
2961
  is_numeric_type(Type),
2927
- ValA < ValB.
2962
+ numeric_operator(OpA),
2963
+ numeric_operator(OpB),
2964
+ \+ ( OpA == eq, OpB == eq ),
2965
+ \+ ( OpA == eq, OpB == neq ),
2966
+ \+ ( OpA == neq, OpB == eq ),
2967
+ number(ValA), number(ValB),
2968
+ numeric_type_integer_variables(Type, IntegerVariables),
2969
+ \+ numeric_constraints_satisfiable([c(OpA, X, ValA), c(OpB, X, ValB)], IntegerVariables).
2970
+
2971
+ numeric_type_integer_variables(int, all) :- !.
2972
+ numeric_type_integer_variables(_, []).
2973
+
2974
+ numeric_operator(Op) :- memberchk(Op, [eq, neq, lt, lte, gt, gte]).
2928
2975
 
2929
2976
  %% is_numeric_type(+Type)
2930
2977
  % True for numeric value types.