kibi-core 0.14.1 → 0.15.1

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
@@ -26,6 +26,7 @@
26
26
  kb_query_entities/8,
27
27
  kb_search_entities/6,
28
28
  kb_list_search_candidates/5,
29
+ kb_list_search_candidates/6,
29
30
  kb_entity_ids/1,
30
31
  kb_rebuild_indexes/0,
31
32
  kb_entity/3,
@@ -59,6 +60,7 @@
59
60
  deprecated_no_successor/1,
60
61
  symbol_no_req_coverage/2,
61
62
  predicate_schema/6,
63
+ predicate_schema_keys/4,
62
64
  predicate_fact/5,
63
65
  canonical_property_tuple/9,
64
66
  contradicting_reqs/3,
@@ -85,6 +87,7 @@
85
87
  :- use_module('../schema/entities.pl', [entity_type/1, entity_property/3, required_property/2]).
86
88
  :- use_module('../schema/relationships.pl', [relationship_type/1, valid_relationship/3]).
87
89
  :- use_module('units.pl', [canonical_quantity/6]).
90
+ :- use_module('intervals.pl', [numeric_constraints_satisfiable/2]).
88
91
  :- use_module('../schema/validation.pl', [validate_entity/2, validate_relationship/3]).
89
92
 
90
93
  % Constants
@@ -1814,14 +1817,22 @@ kb_search_entities(TypeFilter, Query, Limit, Offset, Rows, Count) :-
1814
1817
  % candidate rows, ordered by id. Semantic discovery uses it when no lexical
1815
1818
  % token can pre-filter the candidate set.
1816
1819
  kb_list_search_candidates(TypeFilter, Limit, Offset, Rows, Count) :-
1820
+ kb_list_search_candidates(TypeFilter, none, Limit, Offset, Rows, Count).
1821
+
1822
+ %% kb_list_search_candidates(+Type, +Source, +Limit, +Offset, -Rows, -Count)
1823
+ % As kb_list_search_candidates/5, optionally restricted to entities whose
1824
+ % indexed source contains Source (the kb_query_entities/8 source filter).
1825
+ % Source-located search uses it so candidates stay projected rows instead of
1826
+ % complete entities.
1827
+ % implements REQ-mcp-search-discovery, REQ-kibi-intent-aware-source-discovery
1828
+ kb_list_search_candidates(TypeFilter, SourceFilter, Limit, Offset, Rows, Count) :-
1817
1829
  integer(Limit),
1818
1830
  integer(Offset),
1819
1831
  Limit >= 0,
1820
1832
  Offset >= 0,
1821
1833
  kb_ensure_indexes,
1822
1834
  findall(Id,
1823
- ( kb_index_entity(Id, _),
1824
- (TypeFilter == none -> true ; kb_index_type(TypeFilter, Id)) ),
1835
+ indexed_entity_match(TypeFilter, none, [], SourceFilter, Id),
1825
1836
  RawIds),
1826
1837
  sort(RawIds, Ids),
1827
1838
  length(Ids, Count),
@@ -2569,8 +2580,8 @@ req_conflict_witness(ReqA, ReqB, Witness) :-
2569
2580
  intervals_overlap(ValidFromA, ValidToA, ValidFromB, ValidToB),
2570
2581
  % Compare canonical quantities (30 min == 1800 s); witnesses keep the
2571
2582
  % 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),
2583
+ comparison_quantity(ValTypeA, ValA, UnitA, CanonTypeA, CanonValA, CanonUnitA),
2584
+ comparison_quantity(ValTypeB, ValB, UnitB, CanonTypeB, CanonValB, CanonUnitB),
2574
2585
  ( polarity_conflict(SubjectKey, PropertyKey, OpA, CanonTypeA, CanonValA, CanonUnitA, ScopeA, PolarityA,
2575
2586
  OpB, CanonTypeB, CanonValB, CanonUnitB, ScopeB, PolarityB, Reason)
2576
2587
  ; property_conflict(SubjectKey, PropertyKey, OpA, CanonTypeA, CanonValA, CanonUnitA, PolarityA,
@@ -2728,6 +2739,21 @@ canonical_property_tuple(FactId, Subject, Property, Op, CanonType, CanonValue, C
2728
2739
  fact_property_tuple(FactId, Subject, Property, Op, ValType, Value, Unit, Scope, Polarity),
2729
2740
  canonical_quantity(ValType, Value, Unit, CanonType, CanonValue, CanonUnit).
2730
2741
 
2742
+ %% comparison_quantity(+ValType, +Value, +Unit, -Type, -CanonValue, -CanonUnit)
2743
+ % implements REQ-kibi-truthful-consistency
2744
+ % canonical_quantity/6 for comparisons that depend on the value domain.
2745
+ % Canonicalization reports any integral value as int, but a value declared
2746
+ % `number` keeps real semantics: number gt 0 and number lt 1 do not conflict
2747
+ % even though 0 and 1 are integral. An int that scales to a fraction
2748
+ % (int 1 ms is 0.001 s) is compared as a number, which only admits more
2749
+ % values.
2750
+ comparison_quantity(ValType, Value, Unit, Type, CanonValue, CanonUnit) :-
2751
+ canonical_quantity(ValType, Value, Unit, CanonType, CanonValue, CanonUnit),
2752
+ ( ValType == number, CanonType == int
2753
+ -> Type = number
2754
+ ; Type = CanonType
2755
+ ).
2756
+
2731
2757
  %% predicate_schema(+FactId, -Namespace, -Name, -Arity, -ArgumentNames, -ArgumentTypes)
2732
2758
  % Read one project-local ontology predicate schema fact.
2733
2759
  predicate_schema(FactId, Namespace, Name, Arity, ArgumentNames, ArgumentTypes) :-
@@ -2744,6 +2770,27 @@ predicate_schema(FactId, Namespace, Name, Arity, ArgumentNames, ArgumentTypes) :
2744
2770
  memberchk(argument_types=ArgumentTypesRaw, Props),
2745
2771
  normalize_term_atom_list(ArgumentTypesRaw, ArgumentTypes).
2746
2772
 
2773
+ %% predicate_schema_keys(?Namespace, ?Name, ?Arity, -KeyPositions)
2774
+ % implements REQ-kibi-truthful-consistency
2775
+ % A predicate_schema that declares key_arguments states a functional
2776
+ % dependency: the remaining arguments are determined by the key arguments.
2777
+ % KeyPositions are the 1-based positions of the named key arguments. A
2778
+ % predicate without this declaration is treated as multivalued, so two atoms
2779
+ % of it are never assumed to describe the same fact.
2780
+ predicate_schema_keys(Namespace, Name, Arity, KeyPositions) :-
2781
+ predicate_schema(FactId, Namespace, Name, Arity, ArgumentNames, _ArgumentTypes),
2782
+ kb_entity(FactId, fact, Props),
2783
+ memberchk(key_arguments=KeysRaw, Props),
2784
+ normalize_term_atom_list(KeysRaw, KeyNames),
2785
+ KeyNames \= [],
2786
+ length(ArgumentNames, Arity),
2787
+ maplist(argument_name_position(ArgumentNames), KeyNames, Positions0),
2788
+ sort(Positions0, KeyPositions).
2789
+
2790
+ argument_name_position(ArgumentNames, KeyName, Position) :-
2791
+ nth1(Position, ArgumentNames, KeyName),
2792
+ !.
2793
+
2747
2794
  %% predicate_fact(+FactId, -Namespace, -Name, -Args, -Polarity)
2748
2795
  % Read one ground ontology predicate fact.
2749
2796
  predicate_fact(FactId, Namespace, Name, Args, Polarity) :-
@@ -2842,9 +2889,23 @@ property_conflict(Subject, Property, OpA, TypeA, ValA, UnitA, Polarity,
2842
2889
  OpB, TypeB, ValB, UnitB, Polarity, Reason) :-
2843
2890
  unit_compatible(UnitA, UnitB),
2844
2891
  compatible_types(TypeA, TypeB),
2845
- values_conflict(OpA, ValA, OpB, ValB, TypeA),
2892
+ conflict_value_type(TypeA, TypeB, Type),
2893
+ values_conflict(OpA, ValA, OpB, ValB, Type),
2846
2894
  format(atom(Reason), 'Value conflict on ~w.~w: ~w ~w vs ~w ~w', [Subject, Property, OpA, ValA, OpB, ValB]).
2847
2895
 
2896
+ %% conflict_value_type(+TypeA, +TypeB, -Type)
2897
+ % The value domain both constraints are compared in. A property is treated as
2898
+ % integer-valued only when both sides declare int; a mixed int/number pair is
2899
+ % compared over the reals so a conflict is never claimed from one side's
2900
+ % narrower declaration.
2901
+ % implements REQ-kibi-truthful-consistency
2902
+ conflict_value_type(int, int, int) :- !.
2903
+ conflict_value_type(TypeA, TypeB, number) :-
2904
+ is_numeric_type(TypeA),
2905
+ is_numeric_type(TypeB),
2906
+ !.
2907
+ conflict_value_type(Type, _, Type).
2908
+
2848
2909
  %% compatible_types(+TypeA, +TypeB)
2849
2910
  % Types are compatible if they are the same or both numeric.
2850
2911
  compatible_types(T, T) :- !.
@@ -2898,33 +2959,28 @@ values_conflict(eq, ValA, neq, ValB, Type) :-
2898
2959
  values_conflict(neq, ValA, eq, ValB, Type) :-
2899
2960
  same_value(Type, ValA, Type, ValB).
2900
2961
 
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) :-
2962
+ % Numeric conflict: the two comparisons admit no common value of the type's
2963
+ % domain. `number` ranges over the reals; `int` ranges over the integers, so
2964
+ % gt 0 and lt 1 conflict for int (no integer lies strictly between them) but
2965
+ % not for number. This covers every operator pair (eq/gt, lte/gt at the same
2966
+ % bound, gte/gt, ...) instead of a hand-written table. neq/neq never
2967
+ % conflicts and eq/neq is handled above.
2968
+ % implements REQ-kibi-truthful-consistency
2969
+ values_conflict(OpA, ValA, OpB, ValB, Type) :-
2926
2970
  is_numeric_type(Type),
2927
- ValA < ValB.
2971
+ numeric_operator(OpA),
2972
+ numeric_operator(OpB),
2973
+ \+ ( OpA == eq, OpB == eq ),
2974
+ \+ ( OpA == eq, OpB == neq ),
2975
+ \+ ( OpA == neq, OpB == eq ),
2976
+ number(ValA), number(ValB),
2977
+ numeric_type_integer_variables(Type, IntegerVariables),
2978
+ \+ numeric_constraints_satisfiable([c(OpA, X, ValA), c(OpB, X, ValB)], IntegerVariables).
2979
+
2980
+ numeric_type_integer_variables(int, all) :- !.
2981
+ numeric_type_integer_variables(_, []).
2982
+
2983
+ numeric_operator(Op) :- memberchk(Op, [eq, neq, lt, lte, gt, gte]).
2928
2984
 
2929
2985
  %% is_numeric_type(+Type)
2930
2986
  % True for numeric value types.