kibi-core 0.7.0 → 0.8.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.
@@ -0,0 +1,629 @@
1
+ % Safe, data-driven interpreter for kibi.logic.v1.
2
+ %
3
+ % This module deliberately parses a small JSON IR into ordinary Prolog terms.
4
+ % It never consults or asserts caller-provided source. The terms are evaluated
5
+ % by a bounded interpreter and are suitable for proof witnesses.
6
+ :- module(logic_ir, [
7
+ logic_rule_from_props/2,
8
+ logic_rule_safety/2,
9
+ logic_rule_render/2,
10
+ logic_rule_semantic_key/2,
11
+ logic_derive/2,
12
+ logic_rule_conflict/3,
13
+ logic_rule_conflict_witness/3,
14
+ logic_atom_signature/2,
15
+ logic_rules_stratified/1
16
+ ]).
17
+
18
+ :- use_module(library(http/json)).
19
+ :- use_module(library(aggregate)).
20
+ :- use_module(library(clpfd)).
21
+ :- use_module('kb.pl', [kb_entity/3, predicate_fact/5]).
22
+
23
+ %% logic_rule_from_props(+Props, -Rule)
24
+ % Props is an entity property list from a fact_kind=rule fact.
25
+ logic_rule_from_props(Props, Rule) :-
26
+ memberchk(rule_ir=Raw, Props),
27
+ raw_json_atom(Raw, Json),
28
+ catch(atom_json_dict(Json, Dict, [value_string_as(atom)]), _, fail),
29
+ json_rule(Dict, Rule).
30
+
31
+ raw_json_atom(^^(Value, _), Atom) :- !, raw_json_atom(Value, Atom).
32
+ raw_json_atom(literal(type(_, Value)), Atom) :- !, raw_json_atom(Value, Atom).
33
+ raw_json_atom(Value, Atom) :- atom(Value), !, Atom = Value.
34
+ raw_json_atom(Value, Atom) :- string(Value), !, atom_string(Atom, Value).
35
+
36
+ json_rule(Dict, rule(Kind, Modality, Head, Body, Exceptions, Scope, From, To, SchemaId, Variables)) :-
37
+ json_rule_keys(Dict),
38
+ get_dict(version, Dict, Version),
39
+ json_text(Version, 'kibi.logic.v1'),
40
+ json_text(Dict.kind, Kind),
41
+ json_modality(Dict.modality, Modality),
42
+ ( get_dict(head, Dict, HeadDict) -> json_atom(HeadDict, Head) ; Head = none ),
43
+ ( get_dict(body, Dict, BodyDict) -> json_expression(BodyDict, Body) ; Body = none ),
44
+ ( get_dict(exceptions, Dict, ExceptionDicts) -> maplist(json_expression, ExceptionDicts, Exceptions) ; Exceptions = [] ),
45
+ ( get_dict(scope, Dict, ScopeDict) -> json_scope(ScopeDict, Scope) ; Scope = scope('', '', []) ),
46
+ ( get_dict(validFrom, Dict, RawFrom) -> json_timestamp(RawFrom, From) ; From = '' ),
47
+ ( get_dict(validTo, Dict, RawTo) -> json_timestamp(RawTo, To) ; To = '' ),
48
+ ( get_dict(ruleSchemaId, Dict, RawSchemaId) -> json_atom_name(RawSchemaId, SchemaId), valid_schema_id(SchemaId) ; SchemaId = '' ),
49
+ ( get_dict(variables, Dict, RawVariables) -> maplist(json_variable, RawVariables, Variables) ; Variables = [] ).
50
+
51
+ json_rule_keys(Dict) :-
52
+ dict_pairs(Dict, _Tag, Pairs),
53
+ forall(member(Key-_, Pairs), memberchk(Key, [version, kind, modality, head, body, variables, exceptions, scope, validFrom, validTo, ruleSchemaId])).
54
+
55
+ json_variable(Dict, variable(Name, Type, Quantifier)) :-
56
+ is_dict(Dict),
57
+ dict_keys_allowed(Dict, [name, type, quantifier]),
58
+ get_dict(name, Dict, RawName), json_atom_name(RawName, Name),
59
+ valid_variable_name(Name),
60
+ get_dict(type, Dict, RawType), json_atom_name(RawType, Type), valid_type_name(Type),
61
+ ( get_dict(quantifier, Dict, RawQuantifier) -> json_quantifier(RawQuantifier, Quantifier) ; Quantifier = forall ).
62
+
63
+ json_quantifier(Value, Value) :- memberchk(Value, [forall, exists]), !.
64
+ json_quantifier(Value, Quantifier) :- string(Value), atom_string(Atom, Value), json_quantifier(Atom, Quantifier).
65
+
66
+ json_modality(Value, Value) :- memberchk(Value, [assert, deny, oblige, permit, forbid]), !.
67
+ json_modality(Value, Value) :- string(Value), atom_string(Atom, Value), json_modality(Atom, Value).
68
+
69
+ json_atom(Dict, atom(Namespace, Name, Args, Polarity, ClosedWorld)) :-
70
+ is_dict(Dict),
71
+ dict_keys_allowed(Dict, [kind, namespace, name, args, polarity, closedWorld]),
72
+ get_dict(kind, Dict, Kind),
73
+ json_text(Kind, atom),
74
+ get_dict(name, Dict, RawName),
75
+ json_atom_name(RawName, Name), valid_predicate_name(Name),
76
+ ( get_dict(namespace, Dict, RawNamespace) -> json_atom_name(RawNamespace, Namespace), valid_predicate_name(Namespace) ; Namespace = default ),
77
+ get_dict(args, Dict, RawArgs),
78
+ is_list(RawArgs),
79
+ length(RawArgs, Arity), Arity =< 8,
80
+ maplist(json_term, RawArgs, Args),
81
+ ( get_dict(polarity, Dict, RawPolarity) -> json_polarity(RawPolarity, Polarity) ; Polarity = positive ),
82
+ ( get_dict(closedWorld, Dict, RawClosedWorld) -> memberchk(RawClosedWorld, [true, false]), ClosedWorld = RawClosedWorld ; ClosedWorld = false ).
83
+
84
+ json_atom_name(Value, Atom) :- atom(Value), !, Atom = Value.
85
+ json_atom_name(Value, Atom) :- string(Value), !, atom_string(Atom, Value).
86
+
87
+ valid_variable_name(Name) :-
88
+ atom(Name),
89
+ atom_chars(Name, [First|Rest]),
90
+ char_type(First, upper),
91
+ forall(member(Char, Rest), (char_type(Char, alnum) ; Char == '_')).
92
+
93
+ valid_type_name(Name) :-
94
+ atom(Name),
95
+ atom_chars(Name, [First|Rest]),
96
+ char_type(First, lower),
97
+ forall(member(Char, Rest), (char_type(Char, alnum) ; memberchk(Char, ['_', '-', ':', '/', '.']))).
98
+
99
+ valid_schema_id(Name) :-
100
+ atom(Name),
101
+ atom_chars(Name, [First|Rest]),
102
+ char_type(First, alnum),
103
+ forall(member(Char, Rest), (char_type(Char, alnum) ; memberchk(Char, ['_', '-', ':', '/', '.']))).
104
+
105
+ json_timestamp(Value, Timestamp) :-
106
+ json_atom_name(Value, Timestamp),
107
+ parse_time(Timestamp, iso_8601, _).
108
+
109
+ valid_predicate_name(Name) :-
110
+ atom(Name),
111
+ atom_chars(Name, [First|Rest]),
112
+ char_type(First, lower),
113
+ forall(member(Char, Rest), allowed_predicate_char(Char)).
114
+
115
+ allowed_predicate_char(Char) :- char_type(Char, alnum), !.
116
+ allowed_predicate_char('_').
117
+ allowed_predicate_char('-').
118
+ allowed_predicate_char('/').
119
+ allowed_predicate_char('.').
120
+
121
+ json_text(Value, Expected) :- atom(Value), !, Value = Expected.
122
+ json_text(Value, Expected) :- string(Value), atom_string(Expected, Value).
123
+
124
+ json_polarity(Value, Value) :- memberchk(Value, [positive, negative]), !.
125
+ json_polarity(Value, Polarity) :- string(Value), atom_string(Atom, Value), json_polarity(Atom, Polarity).
126
+
127
+ json_term(Dict, var(Name, Type)) :-
128
+ is_dict(Dict), dict_keys_allowed(Dict, [kind, name, type]), get_dict(kind, Dict, Kind), json_text(Kind, var),
129
+ get_dict(name, Dict, RawName), json_atom_name(RawName, Name), valid_variable_name(Name),
130
+ get_dict(type, Dict, RawType), json_atom_name(RawType, Type), valid_type_name(Type), !.
131
+ json_term(Dict, const(Value, Type)) :-
132
+ is_dict(Dict), dict_keys_allowed(Dict, [kind, value, type]), get_dict(kind, Dict, Kind), json_text(Kind, const),
133
+ get_dict(value, Dict, RawValue), json_atom_name(RawValue, Value),
134
+ ( get_dict(type, Dict, RawType) -> json_atom_name(RawType, Type) ; Type = string ), !.
135
+ json_term(Dict, number(Value, Unit)) :-
136
+ is_dict(Dict), dict_keys_allowed(Dict, [kind, value, unit]), get_dict(kind, Dict, Kind), json_text(Kind, number),
137
+ get_dict(value, Dict, Value), number(Value), Value =:= Value,
138
+ ( get_dict(unit, Dict, RawUnit) -> json_atom_name(RawUnit, Unit), valid_type_name(Unit) ; Unit = none ), !.
139
+ json_term(Dict, duration(Value, Unit)) :-
140
+ is_dict(Dict), dict_keys_allowed(Dict, [kind, value, unit]), get_dict(kind, Dict, Kind), json_text(Kind, duration),
141
+ get_dict(value, Dict, Value), number(Value), Value =:= Value, Value >= 0,
142
+ get_dict(unit, Dict, RawUnit), json_atom_name(RawUnit, Unit), memberchk(Unit, [ms, s, m, h, d, w]), !.
143
+ json_term(Dict, timestamp(Value)) :-
144
+ is_dict(Dict), dict_keys_allowed(Dict, [kind, value]), get_dict(kind, Dict, Kind), json_text(Kind, timestamp),
145
+ get_dict(value, Dict, RawValue), json_timestamp(RawValue, Value), !.
146
+ json_term(Dict, interval(Start, End)) :-
147
+ is_dict(Dict), dict_keys_allowed(Dict, [kind, start, end]), get_dict(kind, Dict, Kind), json_text(Kind, interval),
148
+ get_dict(start, Dict, RawStart), json_timestamp(RawStart, Start),
149
+ get_dict(end, Dict, RawEnd), json_timestamp(RawEnd, End), Start @=< End, !.
150
+
151
+ json_expression(Dict, Atom) :- is_dict(Dict), dict_keys_allowed(Dict, [kind, namespace, name, args, polarity, closedWorld]), get_dict(kind, Dict, Kind), json_text(Kind, atom), !, json_atom(Dict, Atom).
152
+ json_expression(Dict, all(Items)) :- is_dict(Dict), dict_keys_allowed(Dict, [kind, items]), get_dict(kind, Dict, Kind), json_text(Kind, all), !, get_dict(items, Dict, Raw), is_list(Raw), Raw \= [], length(Raw, Count), Count =< 32, maplist(json_expression, Raw, Items).
153
+ json_expression(Dict, any(Items)) :- is_dict(Dict), dict_keys_allowed(Dict, [kind, items]), get_dict(kind, Dict, Kind), json_text(Kind, any), !, get_dict(items, Dict, Raw), is_list(Raw), Raw \= [], length(Raw, Count), Count =< 32, maplist(json_expression, Raw, Items).
154
+ json_expression(Dict, not(Atom)) :- is_dict(Dict), dict_keys_allowed(Dict, [kind, item]), get_dict(kind, Dict, Kind), json_text(Kind, not), !, get_dict(item, Dict, Raw), json_atom(Raw, Atom).
155
+ json_expression(Dict, compare(Op, Left, Right)) :- is_dict(Dict), dict_keys_allowed(Dict, [kind, operator, left, right]), get_dict(kind, Dict, Kind), json_text(Kind, compare), !, get_dict(operator, Dict, OpRaw), json_atom_name(OpRaw, Op), valid_compare_operator(Op), get_dict(left, Dict, LeftRaw), get_dict(right, Dict, RightRaw), json_term(LeftRaw, Left), json_term(RightRaw, Right).
156
+ json_expression(Dict, count(Atom, Op, Value)) :- is_dict(Dict), dict_keys_allowed(Dict, [kind, atom, operator, value]), get_dict(kind, Dict, Kind), json_text(Kind, count), !, get_dict(atom, Dict, AtomRaw), json_atom(AtomRaw, Atom), get_dict(operator, Dict, OpRaw), json_atom_name(OpRaw, Op), valid_compare_operator(Op), get_dict(value, Dict, Value), integer(Value), Value >= 0.
157
+ json_expression(Dict, temporal(Relation, Left, Right)) :- is_dict(Dict), dict_keys_allowed(Dict, [kind, relation, left, right]), get_dict(kind, Dict, Kind), json_text(Kind, temporal), !, get_dict(relation, Dict, RelationRaw), json_atom_name(RelationRaw, Relation), valid_temporal_relation(Relation), get_dict(left, Dict, LeftRaw), get_dict(right, Dict, RightRaw), json_term(LeftRaw, Left), json_term(RightRaw, Right).
158
+
159
+ valid_compare_operator(Op) :- memberchk(Op, [eq, neq, lt, lte, gt, gte]).
160
+ valid_temporal_relation(Relation) :- memberchk(Relation, [before, after, during, overlaps, starts, finishes]).
161
+
162
+ json_scope(Dict, scope(Authority, Name, Tags)) :-
163
+ dict_keys_allowed(Dict, [authority, name, tags]),
164
+ ( get_dict(authority, Dict, Authority0) -> json_atom_name(Authority0, Authority) ; Authority = '' ),
165
+ ( get_dict(name, Dict, Name0) -> json_atom_name(Name0, Name) ; Name = '' ),
166
+ ( get_dict(tags, Dict, Tags0) -> maplist(json_atom_name, Tags0, Tags) ; Tags = [] ).
167
+
168
+ dict_keys_allowed(Dict, Allowed) :-
169
+ dict_pairs(Dict, _Tag, Pairs),
170
+ forall(member(Key-_, Pairs), memberchk(Key, Allowed)).
171
+
172
+ %% logic_rule_safety(+Props, -Errors)
173
+ % Independent Prolog-side safety check. It intentionally rejects anything
174
+ % that the JSON decoder cannot map into the closed IR vocabulary.
175
+ logic_rule_safety(Props, Errors) :-
176
+ ( logic_rule_from_props(Props, Rule)
177
+ -> rule_safety_errors(Rule, Errors)
178
+ ; Errors = [malformed_rule_ir]
179
+ ).
180
+
181
+ rule_safety_errors(rule(Kind, _Modality, Head, Body, Exceptions, _Scope, _From, _To, _Schema, Variables), Errors) :-
182
+ findall(Error, rule_shape_error(Kind, Head, Body, Error), ShapeErrors),
183
+ findall(Error, expression_safety_error(Body, Error), BodyErrors),
184
+ findall(Error, (member(Exception, Exceptions), expression_safety_error(Exception, Error)), ExceptionErrors),
185
+ findall(Error, variable_safety_error(Head, Body, Variables, Error), VariableErrors),
186
+ findall(Error, undeclared_variable_error(Head, Body, Exceptions, Variables, Error), DeclarationErrors),
187
+ findall(Error, (member(Exception, Exceptions), exception_variable_safety_error(Exception, Body, Error)), ExceptionVariableErrors),
188
+ findall(Error, rule_term_safety_error(Head, Body, Exceptions, Error), TermErrors),
189
+ findall(Error, rule_limit_error(Head, Body, Exceptions, Error), LimitErrors),
190
+ append([ShapeErrors, BodyErrors, ExceptionErrors, VariableErrors, DeclarationErrors, ExceptionVariableErrors, TermErrors, LimitErrors], Errors0),
191
+ sort(Errors0, Errors).
192
+
193
+ rule_term_safety_error(Head, Body, Exceptions, invalid_interval) :-
194
+ member(Term, [Head, Body|Exceptions]),
195
+ expression_term(Term, interval(Start, End)),
196
+ Start @> End.
197
+
198
+ rule_limit_error(Head, Body, Exceptions, too_many_atoms) :-
199
+ expression_atom_count(Head, HeadCount),
200
+ expression_atom_count(Body, BodyCount),
201
+ maplist(expression_atom_count, Exceptions, ExceptionCounts),
202
+ sum_list([HeadCount, BodyCount|ExceptionCounts], Count),
203
+ Count > 64.
204
+ rule_limit_error(Head, Body, Exceptions, expression_depth_exceeded) :-
205
+ member(Term, [Head, Body|Exceptions]),
206
+ expression_depth(Term, Depth),
207
+ Depth > 16.
208
+
209
+ expression_atom_count(none, 0).
210
+ expression_atom_count(atom(_, _, _, _, _), 1).
211
+ expression_atom_count(all(Items), Count) :- maplist(expression_atom_count, Items, Counts), sum_list(Counts, ChildCount), Count is ChildCount.
212
+ expression_atom_count(any(Items), Count) :- maplist(expression_atom_count, Items, Counts), sum_list(Counts, ChildCount), Count is ChildCount.
213
+ expression_atom_count(not(Atom), Count) :- expression_atom_count(Atom, ChildCount), Count is ChildCount.
214
+ expression_atom_count(compare(_, _, _), 0).
215
+ expression_atom_count(count(Atom, _, _), Count) :- expression_atom_count(Atom, Count).
216
+ expression_atom_count(temporal(_, _, _), 0).
217
+
218
+ expression_depth(none, 0).
219
+ expression_depth(atom(_, _, _, _, _), 1).
220
+ expression_depth(all(Items), Depth) :- maplist(expression_depth, Items, Depths), max_list([0|Depths], ChildDepth), Depth is ChildDepth + 1.
221
+ expression_depth(any(Items), Depth) :- maplist(expression_depth, Items, Depths), max_list([0|Depths], ChildDepth), Depth is ChildDepth + 1.
222
+ expression_depth(not(Atom), Depth) :- expression_depth(Atom, ChildDepth), Depth is ChildDepth + 1.
223
+ expression_depth(compare(_, _, _), 1).
224
+ expression_depth(count(Atom, _, _), Depth) :- expression_depth(Atom, ChildDepth), Depth is ChildDepth + 1.
225
+ expression_depth(temporal(_, _, _), 1).
226
+
227
+ expression_term(atom(_, _, Args, _, _), Term) :- member(Term, Args).
228
+ expression_term(all(Items), Term) :- member(Item, Items), expression_term(Item, Term).
229
+ expression_term(any(Items), Term) :- member(Item, Items), expression_term(Item, Term).
230
+ expression_term(not(Atom), Term) :- expression_term(Atom, Term).
231
+ expression_term(compare(_, Left, Right), Term) :- member(Term, [Left, Right]).
232
+ expression_term(count(Atom, _, _), Term) :- expression_term(Atom, Term).
233
+ expression_term(temporal(_, Left, Right), Term) :- member(Term, [Left, Right]).
234
+ expression_term(Term, Term) :- Term = interval(_, _).
235
+
236
+ variable_safety_error(Head, Body, _Variables, head_variable_not_range_restricted(Name)) :-
237
+ head_variable(Name, Head),
238
+ \+ positive_variable(Name, Body).
239
+ variable_safety_error(Head, _Body, Variables, existential_head_variable(Name)) :-
240
+ head_variable(Name, Head),
241
+ memberchk(variable(Name, _Type, exists), Variables).
242
+ variable_safety_error(_Head, Body, _Variables, body_variable_not_range_restricted(Name)) :-
243
+ body_variable(Name, Body),
244
+ \+ positive_variable(Name, Body).
245
+
246
+ variable_safety_error(Head, Body, Variables, existential_variable_not_body_local(Name)) :-
247
+ memberchk(variable(Name, _Type, exists), Variables),
248
+ ( head_variable(Name, Head) ; \+ expression_variable(Body, Name) ).
249
+
250
+ undeclared_variable_error(Head, Body, Exceptions, Variables, undeclared_variable(Name)) :-
251
+ member(Expression, [Head, Body|Exceptions]),
252
+ expression_variable(Expression, Name),
253
+ \+ memberchk(variable(Name, _Type, _Quantifier), Variables).
254
+
255
+ exception_variable_safety_error(Exception, Body, exception_variable_not_range_restricted(Name)) :-
256
+ expression_variable(Exception, Name),
257
+ \+ positive_variable(Name, Body).
258
+
259
+ head_variable(Name, atom(_, _, Args, _, _)) :- member(Arg, Args), term_variable(Arg, Name).
260
+ body_variable(Name, Expression) :- expression_variable(Expression, Name).
261
+ positive_variable(Name, atom(_, _, Args, positive, _)) :- member(Arg, Args), term_variable(Arg, Name).
262
+ positive_variable(Name, all(Items)) :- member(Item, Items), positive_variable(Name, Item).
263
+ positive_variable(Name, any(Items)) :- member(Item, Items), positive_variable(Name, Item).
264
+ positive_variable(Name, count(Atom, _, _)) :- positive_variable(Name, Atom).
265
+ positive_variable(_, _) :- fail.
266
+
267
+ term_variable(var(Name, _), Name).
268
+ expression_variable(atom(_, _, Args, _, _), Name) :- member(Arg, Args), term_variable(Arg, Name).
269
+ expression_variable(all(Items), Name) :- member(Item, Items), expression_variable(Item, Name).
270
+ expression_variable(any(Items), Name) :- member(Item, Items), expression_variable(Item, Name).
271
+ expression_variable(not(Atom), Name) :- expression_variable(Atom, Name).
272
+ expression_variable(compare(_, Left, Right), Name) :- (term_variable(Left, Name) ; term_variable(Right, Name)).
273
+ expression_variable(count(Atom, _, _), Name) :- expression_variable(Atom, Name).
274
+ expression_variable(temporal(_, Left, Right), Name) :- (term_variable(Left, Name) ; term_variable(Right, Name)).
275
+
276
+ rule_shape_error(atom, none, _Body, missing_head).
277
+ rule_shape_error(atom, _Head, Body, body_for_atom) :- Body \= none.
278
+ rule_shape_error(rule, none, _Body, missing_head).
279
+ rule_shape_error(rule, _Head, none, missing_body).
280
+ rule_shape_error(constraint, _Head, none, missing_body).
281
+ rule_shape_error(constraint, Head, _Body, head_for_constraint) :- Head \= none.
282
+ rule_shape_error(_, _, _, _) :- fail.
283
+
284
+ expression_safety_error(none, _) :- fail.
285
+ expression_safety_error(not(atom(_, _, _, _, false)), negation_requires_closed_world).
286
+ expression_safety_error(compare(Op, _Left, _Right), invalid_compare_operator) :- \+ valid_compare_operator(Op).
287
+ expression_safety_error(count(_Atom, Op, _Value), invalid_compare_operator) :- \+ valid_compare_operator(Op).
288
+ expression_safety_error(count(_Atom, _Op, Value), invalid_count_value) :- \+ integer(Value) ; Value < 0.
289
+ expression_safety_error(temporal(Relation, _Left, _Right), invalid_temporal_relation) :- \+ valid_temporal_relation(Relation).
290
+ expression_safety_error(all(Items), Error) :- member(Item, Items), expression_safety_error(Item, Error).
291
+ expression_safety_error(any(Items), Error) :- member(Item, Items), expression_safety_error(Item, Error).
292
+ expression_safety_error(not(Atom), Error) :- expression_safety_error(Atom, Error).
293
+ expression_safety_error(_, _) :- fail.
294
+
295
+ %% logic_rule_render(+Props, -Text)
296
+ logic_rule_render(Props, Text) :-
297
+ logic_rule_from_props(Props, Rule),
298
+ render_rule(Rule, Text).
299
+
300
+ render_rule(rule(_Kind, Modality, Head, none, _Exceptions, _Scope, _From, _To, _Schema, _Variables), Text) :-
301
+ render_head(Modality, Head, HeadText), format(string(Text), '~w.', [HeadText]).
302
+ render_rule(rule(_Kind, Modality, Head, Body, _Exceptions, _Scope, _From, _To, _Schema, _Variables), Text) :-
303
+ render_head(Modality, Head, HeadText), render_expression(Body, BodyText), format(string(Text), '~w :- ~w.', [HeadText, BodyText]).
304
+
305
+ render_head(Modality, none, Text) :- format(string(Text), '~w', [Modality]).
306
+ render_head(Modality, Atom, Text) :- render_atom(Atom, AtomText), format(string(Text), '~w(~w)', [Modality, AtomText]).
307
+ render_atom(atom(Namespace, Name, Args, Polarity, _), Text) :-
308
+ ( Namespace == default -> Full = Name ; format(atom(Full), '~w:~w', [Namespace, Name]) ),
309
+ maplist(render_term, Args, Rendered), atomic_list_concat(Rendered, ',', ArgsText),
310
+ ( Polarity == negative -> format(string(Text), 'negative(~w(~w))', [Full, ArgsText]) ; format(string(Text), '~w(~w)', [Full, ArgsText]) ).
311
+ render_term(var(Name, _), Name).
312
+ render_term(const(Value, _), Value).
313
+ render_term(number(Value, none), Text) :- format(string(Text), '~w', [Value]).
314
+ render_term(number(Value, Unit), Text) :- format(string(Text), '~w~w', [Value, Unit]).
315
+ render_term(duration(Value, Unit), Text) :- format(string(Text), 'duration(~w,~w)', [Value, Unit]).
316
+ render_term(timestamp(Value), Value).
317
+ render_term(interval(Start, End), Text) :- format(string(Text), 'interval(~w,~w)', [Start, End]).
318
+ render_expression(Atom, Text) :- Atom = atom(_, _, _, _, _), !, render_atom(Atom, Text).
319
+ render_expression(all(Items), Text) :- maplist(render_expression, Items, Parts), atomic_list_concat(Parts, ', ', Text).
320
+ render_expression(any(Items), Text) :- maplist(render_expression, Items, Parts), atomic_list_concat(Parts, ' ; ', Inner), format(string(Text), '(~w)', [Inner]).
321
+ render_expression(not(Atom), Text) :- render_atom(Atom, AtomText), format(string(Text), 'not(~w)', [AtomText]).
322
+ render_expression(compare(Op, Left, Right), Text) :- render_term(Left, L), render_term(Right, R), format(string(Text), '~w ~w ~w', [L, Op, R]).
323
+ render_expression(count(Atom, Op, Value), Text) :- render_atom(Atom, AtomText), format(string(Text), 'count(~w,~w,~w)', [AtomText, Op, Value]).
324
+ render_expression(temporal(Relation, Left, Right), Text) :- render_term(Left, L), render_term(Right, R), format(string(Text), 'temporal(~w,~w,~w)', [Relation, L, R]).
325
+
326
+ logic_rule_semantic_key(Props, Key) :-
327
+ memberchk(semantic_key=Key, Props), !.
328
+ logic_rule_semantic_key(Props, Key) :-
329
+ memberchk(rule_hash=Hash, Props), !, Key = Hash.
330
+
331
+ logic_atom_signature(atom(Namespace, Name, Args, Polarity, _), signature(Namespace, Name, Args, Polarity)).
332
+
333
+ %% logic_derive(+Goal, -Proof)
334
+ % Goal is atom(Namespace, Name, Args, Polarity), with ground arguments. Rules
335
+ % are interpreted by a finite, data-driven evaluator; no stored source is
336
+ % consulted and recursive rule chains are bounded.
337
+ logic_derive(Goal, Proof) :-
338
+ ground(Goal),
339
+ derive_goal(Goal, [], 0, Proof).
340
+
341
+ logic_max_depth(16).
342
+
343
+ derive_goal(Goal, _Seen, _Depth, direct(FactId)) :-
344
+ Goal = atom(Namespace, Name, Args, Polarity, _),
345
+ predicate_fact(FactId, Namespace, Name, Args, Polarity), !.
346
+ derive_goal(Goal, Seen, Depth, rule(FactId, BodyProof)) :-
347
+ logic_max_depth(MaxDepth),
348
+ Depth < MaxDepth,
349
+ logic_rule_fact(FactId, StoredRule),
350
+ materialize_rule(StoredRule, Rule),
351
+ Rule = rule(rule, _Modality, Head, Body, Exceptions, _Scope, _From, _To, _Schema, _Variables),
352
+ Goal = Head,
353
+ atom_signature_for_seen(Head, Signature),
354
+ \+ memberchk(Signature, Seen),
355
+ NextDepth is Depth + 1,
356
+ prove_expression(Body, [Signature|Seen], NextDepth, BodyProof),
357
+ exceptions_do_not_hold(Exceptions, [Signature|Seen], NextDepth).
358
+
359
+ logic_rule_fact(FactId, Rule) :-
360
+ kb_entity(FactId, fact, Props),
361
+ memberchk(fact_kind=Kind, Props), normalize_atom(Kind, rule),
362
+ logic_rule_from_props(Props, Rule).
363
+
364
+ % The JSON decoder keeps variable names as data terms for validation and
365
+ % rendering. Evaluation reifies those names into fresh Prolog variables so
366
+ % ordinary unification can be used without ever evaluating caller source.
367
+ materialize_rule(
368
+ rule(Kind, Modality, Head, Body, Exceptions, Scope, From, To, Schema, Variables),
369
+ rule(Kind, Modality, MaterialHead, MaterialBody, MaterialExceptions, Scope, From, To, Schema, Variables)
370
+ ) :-
371
+ variable_environment(Variables, Environment),
372
+ materialize_head(Head, Environment, MaterialHead),
373
+ materialize_expression(Body, Environment, MaterialBody),
374
+ maplist(materialize_expression_with(Environment), Exceptions, MaterialExceptions).
375
+
376
+ variable_environment([], []).
377
+ variable_environment([variable(Name, _Type, _Quantifier)|Rest], [Name-_Variable|Environment]) :-
378
+ variable_environment(Rest, Environment).
379
+
380
+ materialize_head(none, _Variables, none).
381
+ materialize_head(Head, Variables, MaterialHead) :- materialize_atom(Head, Variables, MaterialHead).
382
+
383
+ materialize_expression(none, _Variables, none).
384
+ materialize_expression(Expression, Variables, Materialized) :-
385
+ Expression = atom(_, _, _, _, _), !,
386
+ materialize_atom(Expression, Variables, Materialized).
387
+ materialize_expression(all(Items), Variables, all(MaterialItems)) :- maplist(materialize_expression_with(Variables), Items, MaterialItems).
388
+ materialize_expression(any(Items), Variables, any(MaterialItems)) :- maplist(materialize_expression_with(Variables), Items, MaterialItems).
389
+ materialize_expression(not(Atom), Variables, not(MaterialAtom)) :- materialize_atom(Atom, Variables, MaterialAtom).
390
+ materialize_expression(compare(Op, Left, Right), Variables, compare(Op, MaterialLeft, MaterialRight)) :-
391
+ materialize_term(Left, Variables, MaterialLeft), materialize_term(Right, Variables, MaterialRight).
392
+ materialize_expression(count(Atom, Op, Value), Variables, count(MaterialAtom, Op, Value)) :- materialize_atom(Atom, Variables, MaterialAtom).
393
+ materialize_expression(temporal(Relation, Left, Right), Variables, temporal(Relation, MaterialLeft, MaterialRight)) :-
394
+ materialize_term(Left, Variables, MaterialLeft), materialize_term(Right, Variables, MaterialRight).
395
+ materialize_expression_with(Variables, Expression, Materialized) :- materialize_expression(Expression, Variables, Materialized).
396
+
397
+ materialize_atom(atom(Namespace, Name, Args, Polarity, ClosedWorld), Variables, atom(Namespace, Name, MaterialArgs, Polarity, ClosedWorld)) :-
398
+ maplist(materialize_term_with(Variables), Args, MaterialArgs).
399
+ materialize_term(var(Name, _Type), Variables, Variable) :- memberchk(Name-Variable, Variables), !.
400
+ materialize_term(const(Value, Type), _Variables, const(Value, Type)).
401
+ materialize_term(number(Value, Unit), _Variables, number(Value, Unit)).
402
+ materialize_term(duration(Value, Unit), _Variables, duration(Value, Unit)).
403
+ materialize_term(timestamp(Value), _Variables, timestamp(Value)).
404
+ materialize_term(interval(Start, End), _Variables, interval(Start, End)).
405
+ materialize_term_with(Variables, Term, Materialized) :- materialize_term(Term, Variables, Materialized).
406
+
407
+ prove_expression(Atom, Seen, Depth, Proof) :-
408
+ Atom = atom(_, _, _, _, _),
409
+ derive_goal(Atom, Seen, Depth, Proof).
410
+ prove_expression(all(Items), Seen, Depth, all(Proofs)) :- maplist(prove_expression_seen(Seen, Depth), Items, Proofs).
411
+ prove_expression(any(Items), Seen, Depth, Proof) :- member(Item, Items), prove_expression(Item, Seen, Depth, Proof), !.
412
+ prove_expression(not(Atom), Seen, Depth, negated(Atom)) :-
413
+ Atom = atom(Namespace, Name, Args, Polarity, _),
414
+ \+ derive_goal(atom(Namespace, Name, Args, Polarity, _), Seen, Depth, _).
415
+ prove_expression(compare(Op, Left, Right), _Seen, _Depth, comparison(Op, Left, Right)) :- compare_terms(Op, Left, Right).
416
+ prove_expression(count(Atom, Op, Value), _Seen, _Depth, count(Atom, Op, Value)) :-
417
+ Atom = atom(Namespace, Name, Args, Polarity, _),
418
+ aggregate_all(count, predicate_fact(_, Namespace, Name, Args, Polarity), Count),
419
+ compare_numbers(Op, Count, Value).
420
+ prove_expression(temporal(Relation, Left, Right), _Seen, _Depth, temporal(Relation, Left, Right)) :- temporal_holds(Relation, Left, Right).
421
+ prove_expression_seen(Seen, Depth, Item, Proof) :- prove_expression(Item, Seen, Depth, Proof).
422
+
423
+ exceptions_do_not_hold([], _Seen, _Depth).
424
+ exceptions_do_not_hold([Exception|Rest], Seen, Depth) :-
425
+ \+ prove_expression(Exception, Seen, Depth, _),
426
+ exceptions_do_not_hold(Rest, Seen, Depth).
427
+
428
+ atom_signature_for_seen(atom(Namespace, Name, Args, Polarity, _), signature(Namespace, Name, Args, Polarity)).
429
+
430
+ compare_terms(eq, Left, Right) :- term_value(Left, L), term_value(Right, R), L = R.
431
+ compare_terms(neq, Left, Right) :- term_value(Left, L), term_value(Right, R), L \= R.
432
+ compare_terms(lt, Left, Right) :- term_value(Left, L), term_value(Right, R), L < R.
433
+ compare_terms(lte, Left, Right) :- term_value(Left, L), term_value(Right, R), L =< R.
434
+ compare_terms(gt, Left, Right) :- term_value(Left, L), term_value(Right, R), L > R.
435
+ compare_terms(gte, Left, Right) :- term_value(Left, L), term_value(Right, R), L >= R.
436
+
437
+ term_value(number(Value, Unit), Normalized) :- normalize_numeric_unit(Value, Unit, Normalized).
438
+ term_value(duration(Value, Unit), Normalized) :- duration_milliseconds(Value, Unit, Normalized).
439
+ term_value(const(Value, _), Number) :- atom_number(Value, Number), !.
440
+ term_value(const(Value, _), Value).
441
+ term_value(Value, Value) :- number(Value).
442
+
443
+ normalize_numeric_unit(Value, none, Value) :- !.
444
+ normalize_numeric_unit(Value, Unit, Normalized) :-
445
+ duration_milliseconds(Value, Unit, Normalized).
446
+
447
+ duration_milliseconds(Value, ms, Value) :- !.
448
+ duration_milliseconds(Value, s, Normalized) :- Normalized is Value * 1000, !.
449
+ duration_milliseconds(Value, m, Normalized) :- Normalized is Value * 60000, !.
450
+ duration_milliseconds(Value, h, Normalized) :- Normalized is Value * 3600000, !.
451
+ duration_milliseconds(Value, d, Normalized) :- Normalized is Value * 86400000, !.
452
+ duration_milliseconds(Value, w, Normalized) :- Normalized is Value * 604800000, !.
453
+ duration_milliseconds(Value, _Unit, Value).
454
+
455
+ compare_numbers(eq, L, R) :- L =:= R.
456
+ compare_numbers(neq, L, R) :- L =\= R.
457
+ compare_numbers(lt, L, R) :- L < R.
458
+ compare_numbers(lte, L, R) :- L =< R.
459
+ compare_numbers(gt, L, R) :- L > R.
460
+ compare_numbers(gte, L, R) :- L >= R.
461
+
462
+ temporal_holds(before, Left, Right) :-
463
+ temporal_end(Left, LeftEnd),
464
+ temporal_start(Right, RightStart),
465
+ LeftEnd @< RightStart.
466
+ temporal_holds(after, Left, Right) :-
467
+ temporal_start(Left, LeftStart),
468
+ temporal_end(Right, RightEnd),
469
+ LeftStart @> RightEnd.
470
+ temporal_holds(during, timestamp(Point), interval(Start, End)) :-
471
+ Point @>= Start,
472
+ Point @=< End.
473
+ temporal_holds(during, interval(InnerStart, InnerEnd), interval(Start, End)) :-
474
+ InnerStart @>= Start,
475
+ InnerEnd @=< End.
476
+ temporal_holds(overlaps, interval(LeftStart, LeftEnd), interval(RightStart, RightEnd)) :-
477
+ LeftStart @< RightEnd,
478
+ RightStart @< LeftEnd.
479
+ temporal_holds(starts, interval(LeftStart, _), interval(RightStart, _)) :-
480
+ LeftStart == RightStart.
481
+ temporal_holds(finishes, interval(_, LeftEnd), interval(_, RightEnd)) :-
482
+ LeftEnd == RightEnd.
483
+
484
+ temporal_start(timestamp(Value), Value).
485
+ temporal_start(interval(Start, _), Start).
486
+ temporal_end(timestamp(Value), Value).
487
+ temporal_end(interval(_, End), End).
488
+
489
+ %% logic_rule_conflict(+RuleA, +RuleB, -Status)
490
+ % Conservative symbolic conflict result. Identical opposing heads are a
491
+ % proven conflict; different bodies remain unresolved unless their explicit
492
+ % numeric/temporal constraints are disjoint.
493
+ logic_rule_conflict(RuleA, RuleB, contradiction) :-
494
+ opposing_rule_heads(RuleA, RuleB),
495
+ compatible_rule_context(RuleA, RuleB),
496
+ same_rule_body(RuleA, RuleB), !.
497
+ logic_rule_conflict(RuleA, RuleB, unresolved) :-
498
+ opposing_rule_heads(RuleA, RuleB),
499
+ compatible_rule_context(RuleA, RuleB), !.
500
+ logic_rule_conflict(_, _, disjoint).
501
+
502
+ %% logic_rules_stratified(+Rules)
503
+ % Check the finite dependency graph for a negated edge on a cycle. A single
504
+ % rule can be locally safe while a collection of rules still forms an
505
+ % unstratified negation cycle (p(X) :- not q(X), q(X) :- not p(X)). The
506
+ % interpreter therefore exposes this collection-level check to the KB
507
+ % validation layer instead of relying on Prolog's negation-as-failure.
508
+ logic_rules_stratified(Rules) :-
509
+ is_list(Rules),
510
+ findall(
511
+ edge(HeadPredicate, BodyPredicate, Sign),
512
+ ( member(Rule, Rules), rule_dependency_edge(Rule, HeadPredicate, BodyPredicate, Sign) ),
513
+ Edges
514
+ ),
515
+ \+ (
516
+ member(edge(From, To, negative), Edges),
517
+ dependency_path(To, From, Edges, [])
518
+ ).
519
+
520
+ rule_dependency_edge(
521
+ rule(rule, _Modality, Head, Body, _Exceptions, _Scope, _From, _To, _Schema, _Variables),
522
+ HeadPredicate,
523
+ BodyPredicate,
524
+ Sign
525
+ ) :-
526
+ logic_predicate_signature(Head, HeadPredicate),
527
+ expression_dependency(Body, BodyAtom, Sign),
528
+ logic_predicate_signature(BodyAtom, BodyPredicate).
529
+
530
+ logic_predicate_signature(atom(Namespace, Name, Args, _Polarity, _ClosedWorld), predicate(Namespace, Name, Arity)) :-
531
+ length(Args, Arity).
532
+
533
+ expression_dependency(atom(_Namespace, _Name, _Args, _Polarity, _ClosedWorld), Atom, positive) :-
534
+ Atom = atom(_, _, _, _, _).
535
+ expression_dependency(not(Atom), Atom, negative).
536
+ expression_dependency(all(Items), Atom, Sign) :- member(Item, Items), expression_dependency(Item, Atom, Sign).
537
+ expression_dependency(any(Items), Atom, Sign) :- member(Item, Items), expression_dependency(Item, Atom, Sign).
538
+ expression_dependency(count(Atom, _Operator, _Value), Atom, positive).
539
+
540
+ dependency_path(Current, Target, _Edges, Seen) :-
541
+ Current == Target,
542
+ \+ memberchk(Current, Seen),
543
+ !.
544
+ dependency_path(Current, Target, Edges, Seen) :-
545
+ \+ memberchk(Current, Seen),
546
+ member(edge(Current, Next, _Sign), Edges),
547
+ dependency_path(Next, Target, Edges, [Current|Seen]).
548
+
549
+ %% logic_rule_conflict_witness(+RuleA, +RuleB, -Witness)
550
+ % Return a structured, non-executable witness for an opposing rule result.
551
+ % Consumers can attach requirement IDs and source spans without ever treating
552
+ % the rendered Prolog preview as executable source.
553
+ logic_rule_conflict_witness(RuleA, RuleB, Witness) :-
554
+ logic_rule_conflict(RuleA, RuleB, Status),
555
+ RuleA = rule(_KindA, ModalityA, HeadA, BodyA, ExceptionsA, ScopeA, FromA, ToA, SchemaA, _VariablesA),
556
+ RuleB = rule(_KindB, ModalityB, HeadB, BodyB, ExceptionsB, ScopeB, FromB, ToB, SchemaB, _VariablesB),
557
+ Witness = witness{
558
+ status: Status,
559
+ modality_a: ModalityA,
560
+ modality_b: ModalityB,
561
+ head_a: HeadA,
562
+ head_b: HeadB,
563
+ body_a: BodyA,
564
+ body_b: BodyB,
565
+ exceptions_a: ExceptionsA,
566
+ exceptions_b: ExceptionsB,
567
+ scope_a: ScopeA,
568
+ scope_b: ScopeB,
569
+ valid_from_a: FromA,
570
+ valid_to_a: ToA,
571
+ valid_from_b: FromB,
572
+ valid_to_b: ToB,
573
+ rule_schema_a: SchemaA,
574
+ rule_schema_b: SchemaB
575
+ }.
576
+
577
+ opposing_rule_heads(rule(_, ModalityA, HeadA, _, _, _, _, _, _, _), rule(_, ModalityB, HeadB, _, _, _, _, _, _, _)) :-
578
+ conflicting_modality(ModalityA, ModalityB),
579
+ copy_term((HeadA, HeadB), (CopyA, CopyB)),
580
+ CopyA = CopyB.
581
+
582
+ conflicting_modality(assert, deny).
583
+ conflicting_modality(deny, assert).
584
+ conflicting_modality(oblige, forbid).
585
+ conflicting_modality(forbid, oblige).
586
+ conflicting_modality(permit, forbid).
587
+ conflicting_modality(forbid, permit).
588
+
589
+ compatible_rule_context(rule(_, _, _, BodyA, _, scope(AuthA, ScopeA, _), FromA, ToA, _, _), rule(_, _, _, BodyB, _, scope(AuthB, ScopeB, _), FromB, ToB, _, _)) :-
590
+ (AuthA == '' ; AuthB == '' ; AuthA == AuthB),
591
+ (ScopeA == '' ; ScopeB == '' ; ScopeA == ScopeB),
592
+ (FromA == '' ; ToB == '' ; FromA @=< ToB),
593
+ (FromB == '' ; ToA == '' ; FromB @=< ToA),
594
+ bodies_may_overlap(BodyA, BodyB).
595
+
596
+ bodies_may_overlap(none, _).
597
+ bodies_may_overlap(_, none).
598
+ bodies_may_overlap(BodyA, BodyB) :-
599
+ BodyA \= none,
600
+ BodyB \= none,
601
+ \+ bodies_definitely_disjoint(BodyA, BodyB).
602
+
603
+ bodies_definitely_disjoint(BodyA, _BodyB) :- expression_definitely_false(BodyA), !.
604
+ bodies_definitely_disjoint(_BodyA, BodyB) :- expression_definitely_false(BodyB), !.
605
+
606
+ % Only classify a body as impossible when its finite, ground comparison or
607
+ % temporal constraint can be evaluated to false. Unknown atoms, variables,
608
+ % and unsupported combinations remain unresolved rather than being treated as
609
+ % disjoint by guesswork.
610
+ expression_definitely_false(compare(Op, Left, Right)) :-
611
+ ground(compare(Op, Left, Right)),
612
+ \+ catch(compare_terms(Op, Left, Right), _, fail).
613
+ expression_definitely_false(temporal(Relation, Left, Right)) :-
614
+ ground(temporal(Relation, Left, Right)),
615
+ \+ catch(temporal_holds(Relation, Left, Right), _, fail).
616
+ expression_definitely_false(all(Items)) :-
617
+ member(Item, Items),
618
+ expression_definitely_false(Item), !.
619
+ expression_definitely_false(any(Items)) :-
620
+ Items \= [],
621
+ maplist(expression_definitely_false, Items).
622
+
623
+ same_rule_body(RuleA, RuleB) :-
624
+ RuleA = rule(_, _, _, BodyA, ExceptionsA, ScopeA, FromA, ToA, _, _),
625
+ RuleB = rule(_, _, _, BodyB, ExceptionsB, ScopeB, FromB, ToB, _, _),
626
+ BodyA == BodyB, ExceptionsA == ExceptionsB, ScopeA == ScopeB, FromA == FromB, ToA == ToB.
627
+
628
+ normalize_atom(Value, Atom) :- atom(Value), !, Atom = Value.
629
+ normalize_atom(Value, Atom) :- string(Value), atom_string(Atom, Value).
@@ -44,7 +44,10 @@ validate_remote_endpoint(Endpoint) :-
44
44
 
45
45
  validate_select_query(Query) :-
46
46
  normalize_space(atom(Normalized), Query),
47
- downcase_atom(Normalized, Lower),
47
+ split_string(Normalized, "\n", " \t\r", RawLines),
48
+ strip_sparql_prelude(RawLines, RemainingLines),
49
+ atomic_list_concat(RemainingLines, ' ', Remainder),
50
+ downcase_atom(Remainder, Lower),
48
51
  sub_atom(Lower, 0, 6, After, 'select'),
49
52
  ( After =:= 0
50
53
  ; sub_atom(Lower, 6, 1, _, ' ')
@@ -54,6 +57,25 @@ validate_select_query(Query) :-
54
57
  validate_select_query(Query) :-
55
58
  throw(error(domain_error(sparql_select_query, Query), _)).
56
59
 
60
+ strip_sparql_prelude([], []).
61
+ strip_sparql_prelude([Line | Rest], Result) :-
62
+ normalize_space(atom(Trimmed), Line),
63
+ ( Trimmed == ''
64
+ -> strip_sparql_prelude(Rest, Result)
65
+ ; is_prelude_line(Trimmed)
66
+ -> strip_sparql_prelude(Rest, Result)
67
+ ; Result = [Trimmed | Rest]
68
+ ).
69
+
70
+ is_prelude_line(Line) :-
71
+ downcase_atom(Line, Lower),
72
+ split_string(Lower, " ", " \t", Tokens0),
73
+ exclude(=("") , Tokens0, Tokens),
74
+ Tokens = [FirstToken | _],
75
+ ( FirstToken == "prefix"
76
+ ; FirstToken == "base"
77
+ ).
78
+
57
79
  endpoint_host(Endpoint, Host) :-
58
80
  strip_endpoint_scheme(Endpoint, WithoutScheme),
59
81
  segment_before(WithoutScheme, '/', WithoutPath),