kibi-core 0.24.0 → 0.26.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.
@@ -19,6 +19,22 @@
19
19
  % plus the negated claim admit no value.
20
20
  % smt The query asks (check-sat) and the solver answered unsat.
21
21
  % (Kibi re-runs the solver on the query at ingest.)
22
+ % rule Every domain lists distinct values, every predicate table
23
+ % holds exactly one boolean for every combination of its
24
+ % argument domains, and no call failed. (Kibi recomputes the
25
+ % domains and tables from the code at ingest.)
26
+ % state_machine
27
+ % The machine lists distinct states, and its transitions and
28
+ % initial states are pairs and states of that list. (Kibi
29
+ % reads the machine from the code at ingest.)
30
+ % regex The certificate holds the code's pattern (source, flags and
31
+ % flavor) or string constant. (Kibi reads it from the code
32
+ % at ingest and decides the claim's language exactly.)
33
+ % source The certificate names a project source (a reader and its
34
+ % spec) and holds what Kibi read from it: the members as a
35
+ % sorted list of distinct strings, and for schema, validator
36
+ % and route readers the tables, objects or routes. (Kibi
37
+ % reads the source itself at ingest and stores its own read.)
22
38
  %
23
39
  % The digest a receipt records is the sha256 of the certificate's canonical
24
40
  % JSON as canonical_json/2 below spells it. The CLI records the digest this
@@ -35,6 +51,7 @@
35
51
  :- use_module(library(crypto)).
36
52
  :- use_module(library(apply)).
37
53
  :- use_module(library(lists)).
54
+ :- use_module(library(yall)).
38
55
  :- use_module('intervals.pl', [
39
56
  numeric_constraints_satisfiable/2,
40
57
  numeric_constraint_entailed/3
@@ -90,8 +107,8 @@ certificate_problem(Certificate, Reason) :-
90
107
  ),
91
108
  ( kind_problem(Kind, Certificate, Reason0)
92
109
  -> Reason = Reason0
93
- ; \+ memberchk(Kind, [exhaustive, typecheck, arithmetic, smt])
94
- -> Reason = "the certificate kind is not exhaustive, typecheck, arithmetic or smt"
110
+ ; \+ memberchk(Kind, [exhaustive, typecheck, arithmetic, smt, rule, state_machine, regex, source])
111
+ -> Reason = "the certificate kind is not exhaustive, typecheck, arithmetic, smt, rule, state_machine, regex or source"
95
112
  ).
96
113
 
97
114
  kind_problem(exhaustive, Certificate, Reason) :-
@@ -164,9 +181,18 @@ kind_problem(arithmetic, Certificate, Reason) :-
164
181
  arithmetic_constraint(X, Claim, Goal),
165
182
  integer_variables(Type, X, Integers),
166
183
  ( \+ numeric_constraints_satisfiable(Premises, Integers)
167
- -> Reason = "the assumptions admit no value, so the claim holds vacuously"
184
+ -> maplist(constraint_text(Variable), Assumptions, Texts),
185
+ atomic_list_concat(Texts, ' and ', Joined),
186
+ format(string(Reason),
187
+ "the assumptions contradict each other (~w), so no value satisfies them and they prove nothing about the claim",
188
+ [Joined])
168
189
  ; \+ numeric_constraint_entailed(Premises, Goal, Integers)
169
- -> Reason = "the assumptions do not entail the claim: some value satisfies them and breaks it"
190
+ -> ( arithmetic_counterexample(Type, Assumptions, Claim, Value)
191
+ -> format(string(Reason),
192
+ "the assumptions do not entail the claim: ~w = ~w satisfies them and breaks it",
193
+ [Variable, Value])
194
+ ; Reason = "the assumptions do not entail the claim: some value satisfies them and breaks it"
195
+ )
170
196
  )
171
197
  ).
172
198
  kind_problem(smt, Certificate, Reason) :-
@@ -185,6 +211,170 @@ kind_problem(smt, Certificate, Reason) :-
185
211
  -> Reason = "the solver did not answer unsat"
186
212
  ).
187
213
 
214
+ kind_problem(rule, Certificate, Reason) :-
215
+ field(Certificate, domains, Domains),
216
+ field(Certificate, predicates, Predicates),
217
+ ( \+ (is_dict(Domains), dict_pairs(Domains, _, DomainPairs), DomainPairs \== [])
218
+ -> Reason = "the certificate has no domains"
219
+ ; dict_pairs(Domains, _, DomainPairs),
220
+ member(Type-Domain, DomainPairs),
221
+ field(Domain, members, Members),
222
+ \+ distinct_strings(Members)
223
+ -> format(string(Reason), "the domain ~w must list distinct values, at least one", [Type])
224
+ ; \+ (is_dict(Predicates), dict_pairs(Predicates, _, PredicatePairs), PredicatePairs \== [])
225
+ -> Reason = "the certificate has no predicate tables"
226
+ ; dict_pairs(Predicates, _, PredicatePairs),
227
+ member(Name-Table, PredicatePairs),
228
+ table_problem(Name, Table, Domains, Reason0)
229
+ -> Reason = Reason0
230
+ ).
231
+ kind_problem(state_machine, Certificate, Reason) :-
232
+ field(Certificate, machine, Machine),
233
+ field(Machine, symbol_id, Symbol),
234
+ field(Machine, states, States),
235
+ field(Machine, transitions, Transitions),
236
+ ( \+ is_dict(Machine)
237
+ -> Reason = "the certificate has no machine"
238
+ ; \+ nonempty_string(Symbol)
239
+ -> Reason = "the machine names no symbol_id"
240
+ ; \+ distinct_strings(States)
241
+ -> Reason = "the machine must list distinct states, at least one"
242
+ ; \+ is_list(Transitions)
243
+ -> Reason = "the machine has no transitions list"
244
+ ; member(Edge, Transitions),
245
+ \+ ( is_list(Edge), Edge = [From, To], memberchk(From, States), memberchk(To, States) )
246
+ -> Reason = "every transition must be a [from, to] pair of the machine's states"
247
+ ; msort(Transitions, SortedEdges), sort(Transitions, UniqueEdges), SortedEdges \== UniqueEdges
248
+ -> Reason = "the machine lists a transition twice"
249
+ ; get_dict(initial, Machine, Initial),
250
+ \+ ( is_list(Initial), forall(member(S, Initial), memberchk(S, States)) )
251
+ -> Reason = "the machine's initial states must be states of the machine"
252
+ ).
253
+ kind_problem(regex, Certificate, Reason) :-
254
+ field(Certificate, pattern, Pattern),
255
+ field(Pattern, symbol_id, Symbol),
256
+ field(Pattern, type, Type),
257
+ ( \+ is_dict(Pattern)
258
+ -> Reason = "the certificate has no pattern"
259
+ ; \+ nonempty_string(Symbol)
260
+ -> Reason = "the pattern names no symbol_id"
261
+ ; Type == "regexp"
262
+ -> field(Pattern, source, Source),
263
+ field(Pattern, flags, Flags),
264
+ field(Pattern, flavor, Flavor),
265
+ ( \+ (string(Source), string(Flags))
266
+ -> Reason = "a regexp pattern must give its source and flags"
267
+ ; \+ memberchk(Flavor, ["js", "python"])
268
+ -> Reason = "a regexp pattern's flavor must be js or python"
269
+ )
270
+ ; Type == "string"
271
+ -> field(Pattern, value, Value),
272
+ \+ string(Value),
273
+ Reason = "a string pattern must give its value"
274
+ ; Reason = "the pattern's type must be regexp or string"
275
+ ).
276
+
277
+ % implements REQ-kibi-source-proof-kinds
278
+ kind_problem(source, Certificate, Reason) :-
279
+ field(Certificate, source, Source),
280
+ field(Certificate, model, Model),
281
+ field(Model, members, Members),
282
+ ( \+ ( is_dict(Source), get_dict(reader, Source, Reader), nonempty_string(Reader) )
283
+ -> Reason = "the certificate names no source reader"
284
+ ; \+ is_dict(Model)
285
+ -> Reason = "the certificate has no model of what Kibi read"
286
+ ; \+ sorted_distinct_strings(Members)
287
+ -> Reason = "the source model's members must be a sorted list of distinct strings"
288
+ ; get_dict(entities, Model, Entities),
289
+ \+ ( is_list(Entities), maplist(source_entity_ok, Entities) )
290
+ -> Reason = "every source entity must have a name and columns of {name, nullable, optional}"
291
+ ; get_dict(routes, Model, Routes),
292
+ \+ ( is_list(Routes), maplist(source_route_ok, Routes) )
293
+ -> Reason = "every source route must have a method, a path and a middleware list"
294
+ ).
295
+
296
+ sorted_distinct_strings(List) :-
297
+ is_list(List),
298
+ forall(member(Item, List), string(Item)),
299
+ sort(List, Sorted),
300
+ Sorted == List.
301
+
302
+ source_entity_ok(Entity) :-
303
+ is_dict(Entity),
304
+ get_dict(name, Entity, Name), nonempty_string(Name),
305
+ get_dict(columns, Entity, Columns), is_list(Columns),
306
+ forall(member(Column, Columns),
307
+ ( is_dict(Column),
308
+ get_dict(name, Column, ColumnName), nonempty_string(ColumnName),
309
+ get_dict(nullable, Column, Nullable), memberchk(Nullable, [true, false]),
310
+ get_dict(optional, Column, Optional), memberchk(Optional, [true, false]) )),
311
+ ( get_dict(foreign_keys, Entity, Keys) -> is_list(Keys), forall(member(K, Keys), string(K)) ; true ).
312
+
313
+ source_route_ok(Route) :-
314
+ is_dict(Route),
315
+ get_dict(method, Route, Method), nonempty_string(Method),
316
+ get_dict(path, Route, Path), nonempty_string(Path),
317
+ get_dict(middleware, Route, Middleware), is_list(Middleware),
318
+ forall(member(M, Middleware), string(M)).
319
+
320
+ % table_problem(+Name, +Table, +Domains, -Reason): the first problem with
321
+ % one predicate table.
322
+ table_problem(Name, Table, Domains, Reason) :-
323
+ field(Table, symbol_id, Symbol),
324
+ field(Table, arg_types, Types),
325
+ field(Table, rows, Rows),
326
+ ( \+ nonempty_string(Symbol)
327
+ -> format(string(Reason), "the table for ~w names no symbol_id", [Name])
328
+ ; \+ (is_list(Types), Types \== [], length(Types, Arity), Arity =< 8,
329
+ forall(member(T, Types), (string(T), atom_string(TA, T), get_dict(TA, Domains, _))))
330
+ -> format(string(Reason), "the table for ~w must name 1 to 8 argument types, each with a domain", [Name])
331
+ ; \+ is_list(Rows)
332
+ -> format(string(Reason), "the table for ~w has no rows", [Name])
333
+ ; member(Row, Rows),
334
+ get_dict(error, Row, Error)
335
+ -> get_dict(args, Row, Args),
336
+ atomic_list_concat(Args, ', ', ArgText),
337
+ format(string(Reason), "~w(~w) failed: ~w", [Name, ArgText, Error])
338
+ ; member(Row, Rows),
339
+ \+ table_row(Row, Types, Domains)
340
+ -> format(string(Reason), "every row of ~w must give one value per argument from its domain and a boolean", [Name])
341
+ ; findall(Args, (member(Row, Rows), get_dict(args, Row, Args)), AllArgs),
342
+ msort(AllArgs, SortedArgs),
343
+ sort(AllArgs, UniqueArgs),
344
+ SortedArgs \== UniqueArgs
345
+ -> format(string(Reason), "the table for ~w lists a combination twice", [Name])
346
+ ; length(Rows, Count),
347
+ foldl(domain_product(Domains), Types, 1, Expected),
348
+ Count =\= Expected
349
+ -> format(string(Reason), "the table for ~w has ~d rows, not one per combination of its domains (~d)", [Name, Count, Expected])
350
+ ).
351
+
352
+ domain_product(Domains, Type, Count0, Count) :-
353
+ atom_string(TypeKey, Type),
354
+ get_dict(TypeKey, Domains, Domain),
355
+ get_dict(members, Domain, Members),
356
+ length(Members, N),
357
+ Count is Count0 * N.
358
+
359
+ table_row(Row, Types, Domains) :-
360
+ is_dict(Row),
361
+ get_dict(args, Row, Args),
362
+ is_list(Args),
363
+ same_length(Args, Types),
364
+ forall(nth1(I, Args, Arg),
365
+ ( nth1(I, Types, T), atom_string(TA, T), get_dict(TA, Domains, D),
366
+ get_dict(members, D, Members), memberchk(Arg, Members) )),
367
+ get_dict(value, Row, Value),
368
+ memberchk(Value, [true, false]).
369
+
370
+ distinct_strings(Values) :-
371
+ is_list(Values),
372
+ Values \== [],
373
+ maplist(string, Values),
374
+ msort(Values, Sorted),
375
+ sort(Values, Unique),
376
+ Sorted == Unique.
377
+
188
378
  % field(+Dict, +Key, -Value): the value, or '$missing' when absent.
189
379
  field(Dict, Key, Value) :-
190
380
  ( is_dict(Dict), get_dict(Key, Dict, Value0)
@@ -206,6 +396,51 @@ arithmetic_constraint(X, Row, c(Op, X, Value)) :-
206
396
  get_dict(value, Row, Value),
207
397
  number(Value).
208
398
 
399
+ % "count = 4", for naming an assumption in a rejection.
400
+ constraint_text(Variable, Row, Text) :-
401
+ get_dict(operator, Row, OpString),
402
+ get_dict(value, Row, Value),
403
+ atom_string(Op, OpString),
404
+ operator_symbol(Op, Symbol),
405
+ format(atom(Text), "~w ~w ~w", [Variable, Symbol, Value]).
406
+
407
+ operator_symbol(eq, '=').
408
+ operator_symbol(neq, '!=').
409
+ operator_symbol(lt, '<').
410
+ operator_symbol(lte, '<=').
411
+ operator_symbol(gt, '>').
412
+ operator_symbol(gte, '>=').
413
+
414
+ % arithmetic_counterexample(+Type, +Assumptions, +Claim, -Value): a value
415
+ % next to one of the constants that satisfies every assumption and breaks the
416
+ % claim, so a rejection can name it (a count of 5 against "exactly 4").
417
+ arithmetic_counterexample(Type, Assumptions, Claim, Value) :-
418
+ findall(C, (member(Row, [Claim|Assumptions]), get_dict(value, Row, C)), Constants),
419
+ ( Type == "int" -> Step = 1 ; Step = 0.5 ),
420
+ findall(V,
421
+ ( member(C, [0|Constants]),
422
+ member(D, [0, -1, 1]),
423
+ V is C + D*Step,
424
+ ( Type == "int" -> integer(V) ; true )
425
+ ),
426
+ Candidates0),
427
+ sort(Candidates0, Candidates),
428
+ member(Value, Candidates),
429
+ forall(member(Row, Assumptions), concrete_holds(Row, Value)),
430
+ \+ concrete_holds(Claim, Value),
431
+ !.
432
+
433
+ concrete_holds(Row, X) :-
434
+ arithmetic_constraint(Y, Row, c(Op, Y, V)),
435
+ concrete_op(Op, X, V).
436
+
437
+ concrete_op(eq, X, V) :- X =:= V.
438
+ concrete_op(neq, X, V) :- X =\= V.
439
+ concrete_op(lt, X, V) :- X < V.
440
+ concrete_op(lte, X, V) :- X =< V.
441
+ concrete_op(gt, X, V) :- X > V.
442
+ concrete_op(gte, X, V) :- X >= V.
443
+
209
444
  integer_variables("int", X, [X]).
210
445
  integer_variables("real", _, []).
211
446
 
package/src/kb.pl CHANGED
@@ -72,6 +72,7 @@
72
72
  check_req_contradiction/1,
73
73
  normalize_term_atom/2,
74
74
  normalize_term_atom_list/2,
75
+ normalize_literal_atom_list/2,
75
76
  changeset/4, % Export for testing
76
77
  kb_uri/1
77
78
  ]).
@@ -85,7 +86,7 @@
85
86
  :- use_module(library(sha), [sha_hash/3, hash_atom/2]).
86
87
  :- use_module(library(http/json)).
87
88
  :- use_module(library(aggregate), [aggregate_all/3]).
88
- :- use_module(library(lists), [sum_list/2]).
89
+ :- use_module(library(lists), [sum_list/2, subtract/3, intersection/3]).
89
90
  :- use_module(library(ordsets)).
90
91
  :- use_module(library(solution_sequences), [distinct/1, limit/2, offset/2, order_by/2]).
91
92
  :- use_module('../schema/entities.pl', [entity_type/1, entity_property/3, required_property/2]).
@@ -3080,6 +3081,23 @@ normalize_term_atom_list(Raw, Atoms) :-
3080
3081
  !,
3081
3082
  maplist(normalize_term_atom, Value, Atoms).
3082
3083
 
3084
+ %% normalize_literal_atom_list(+Raw, -Atoms)
3085
+ % The members of a stored list as atoms, verbatim. Unlike
3086
+ % normalize_term_atom_list it keeps a member's '/' segments, so set members
3087
+ % such as rxjs/operators, GET /api/users or finance/de stay whole.
3088
+ normalize_literal_atom_list(Raw, Atoms) :-
3089
+ unwrap_rdf_value(Raw, Value),
3090
+ is_list(Value),
3091
+ maplist(normalize_literal_atom, Value, Atoms).
3092
+
3093
+ normalize_literal_atom(Raw, Atom) :-
3094
+ unwrap_rdf_value(Raw, Value),
3095
+ ( atom(Value) -> Atom = Value
3096
+ ; string(Value) -> atom_string(Atom, Value)
3097
+ ; number(Value) -> atom_number(Atom, Value)
3098
+ ; term_string(Value, String), atom_string(Atom, String)
3099
+ ).
3100
+
3083
3101
  normalize_term_integer(Raw, Integer) :-
3084
3102
  unwrap_rdf_value(Raw, Value),
3085
3103
  ( integer(Value)
@@ -3125,7 +3143,7 @@ value_from_props(Props, bool, Value) :- memberchk(value_bool=ValueRaw, Props), !
3125
3143
  % in any order compare equal.
3126
3144
  value_from_props(Props, set, Value) :-
3127
3145
  memberchk(value_set=ValueRaw, Props),
3128
- normalize_term_atom_list(ValueRaw, Atoms),
3146
+ normalize_literal_atom_list(ValueRaw, Atoms),
3129
3147
  !,
3130
3148
  sort(Atoms, Value).
3131
3149
  value_from_props(_, _, '').
@@ -3226,6 +3244,35 @@ same_value(TypeA, ValA, TypeB, ValB) :-
3226
3244
  values_conflict(eq, ValA, eq, ValB, Type) :-
3227
3245
  \+ same_value(Type, ValA, Type, ValB).
3228
3246
 
3247
+ % A set fact with includes says every member of its set has the property;
3248
+ % eq on a set says exactly those members do. Two includes facts always agree
3249
+ % (the members having the property include both sets); includes conflicts
3250
+ % with eq only when it names a member the eq set leaves out.
3251
+ % implements REQ-kibi-non-numeric-proof-kinds
3252
+ values_conflict(includes, Included, eq, Exact, set) :-
3253
+ \+ subtract(Included, Exact, []).
3254
+ values_conflict(eq, Exact, includes, Included, set) :-
3255
+ \+ subtract(Included, Exact, []).
3256
+
3257
+ % subset_of says every member with the property is in its set, excludes that
3258
+ % no member of its set has it. subset_of conflicts with a set that must have
3259
+ % the property (eq or includes) when that set names a member outside it;
3260
+ % excludes conflicts when the two sets share a member. subset_of and excludes
3261
+ % never conflict with each other or themselves: the empty set satisfies them.
3262
+ % implements REQ-kibi-source-proof-kinds
3263
+ values_conflict(subset_of, Allowed, Op, Required, set) :-
3264
+ memberchk(Op, [eq, includes]),
3265
+ \+ subtract(Required, Allowed, []).
3266
+ values_conflict(Op, Required, subset_of, Allowed, set) :-
3267
+ memberchk(Op, [eq, includes]),
3268
+ \+ subtract(Required, Allowed, []).
3269
+ values_conflict(excludes, Excluded, Op, Required, set) :-
3270
+ memberchk(Op, [eq, includes]),
3271
+ intersection(Excluded, Required, [_|_]).
3272
+ values_conflict(Op, Required, excludes, Excluded, set) :-
3273
+ memberchk(Op, [eq, includes]),
3274
+ intersection(Excluded, Required, [_|_]).
3275
+
3229
3276
  % Eq/neq conflict on the same scalar value.
3230
3277
  values_conflict(eq, ValA, neq, ValB, Type) :-
3231
3278
  same_value(Type, ValA, Type, ValB).