kibi-core 0.24.0 → 0.27.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.
@@ -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,174 @@ 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
+ ; get_dict(sequence, Model, Sequence),
295
+ \+ ( is_list(Sequence), forall(member(Item, Sequence), string(Item)),
296
+ sort(Sequence, Members) )
297
+ -> Reason = "the source model's sequence must list the members in the order read"
298
+ ).
299
+
300
+ sorted_distinct_strings(List) :-
301
+ is_list(List),
302
+ forall(member(Item, List), string(Item)),
303
+ sort(List, Sorted),
304
+ Sorted == List.
305
+
306
+ source_entity_ok(Entity) :-
307
+ is_dict(Entity),
308
+ get_dict(name, Entity, Name), nonempty_string(Name),
309
+ get_dict(columns, Entity, Columns), is_list(Columns),
310
+ forall(member(Column, Columns),
311
+ ( is_dict(Column),
312
+ get_dict(name, Column, ColumnName), nonempty_string(ColumnName),
313
+ get_dict(nullable, Column, Nullable), memberchk(Nullable, [true, false]),
314
+ get_dict(optional, Column, Optional), memberchk(Optional, [true, false]) )),
315
+ ( get_dict(foreign_keys, Entity, Keys) -> is_list(Keys), forall(member(K, Keys), string(K)) ; true ).
316
+
317
+ source_route_ok(Route) :-
318
+ is_dict(Route),
319
+ get_dict(method, Route, Method), nonempty_string(Method),
320
+ get_dict(path, Route, Path), nonempty_string(Path),
321
+ get_dict(middleware, Route, Middleware), is_list(Middleware),
322
+ forall(member(M, Middleware), string(M)).
323
+
324
+ % table_problem(+Name, +Table, +Domains, -Reason): the first problem with
325
+ % one predicate table.
326
+ table_problem(Name, Table, Domains, Reason) :-
327
+ field(Table, symbol_id, Symbol),
328
+ field(Table, arg_types, Types),
329
+ field(Table, rows, Rows),
330
+ ( \+ nonempty_string(Symbol)
331
+ -> format(string(Reason), "the table for ~w names no symbol_id", [Name])
332
+ ; \+ (is_list(Types), Types \== [], length(Types, Arity), Arity =< 8,
333
+ forall(member(T, Types), (string(T), atom_string(TA, T), get_dict(TA, Domains, _))))
334
+ -> format(string(Reason), "the table for ~w must name 1 to 8 argument types, each with a domain", [Name])
335
+ ; \+ is_list(Rows)
336
+ -> format(string(Reason), "the table for ~w has no rows", [Name])
337
+ ; member(Row, Rows),
338
+ get_dict(error, Row, Error)
339
+ -> get_dict(args, Row, Args),
340
+ atomic_list_concat(Args, ', ', ArgText),
341
+ format(string(Reason), "~w(~w) failed: ~w", [Name, ArgText, Error])
342
+ ; member(Row, Rows),
343
+ \+ table_row(Row, Types, Domains)
344
+ -> format(string(Reason), "every row of ~w must give one value per argument from its domain and a boolean", [Name])
345
+ ; findall(Args, (member(Row, Rows), get_dict(args, Row, Args)), AllArgs),
346
+ msort(AllArgs, SortedArgs),
347
+ sort(AllArgs, UniqueArgs),
348
+ SortedArgs \== UniqueArgs
349
+ -> format(string(Reason), "the table for ~w lists a combination twice", [Name])
350
+ ; length(Rows, Count),
351
+ foldl(domain_product(Domains), Types, 1, Expected),
352
+ Count =\= Expected
353
+ -> format(string(Reason), "the table for ~w has ~d rows, not one per combination of its domains (~d)", [Name, Count, Expected])
354
+ ).
355
+
356
+ domain_product(Domains, Type, Count0, Count) :-
357
+ atom_string(TypeKey, Type),
358
+ get_dict(TypeKey, Domains, Domain),
359
+ get_dict(members, Domain, Members),
360
+ length(Members, N),
361
+ Count is Count0 * N.
362
+
363
+ table_row(Row, Types, Domains) :-
364
+ is_dict(Row),
365
+ get_dict(args, Row, Args),
366
+ is_list(Args),
367
+ same_length(Args, Types),
368
+ forall(nth1(I, Args, Arg),
369
+ ( nth1(I, Types, T), atom_string(TA, T), get_dict(TA, Domains, D),
370
+ get_dict(members, D, Members), memberchk(Arg, Members) )),
371
+ get_dict(value, Row, Value),
372
+ memberchk(Value, [true, false]).
373
+
374
+ distinct_strings(Values) :-
375
+ is_list(Values),
376
+ Values \== [],
377
+ maplist(string, Values),
378
+ msort(Values, Sorted),
379
+ sort(Values, Unique),
380
+ Sorted == Unique.
381
+
188
382
  % field(+Dict, +Key, -Value): the value, or '$missing' when absent.
189
383
  field(Dict, Key, Value) :-
190
384
  ( is_dict(Dict), get_dict(Key, Dict, Value0)
@@ -206,6 +400,51 @@ arithmetic_constraint(X, Row, c(Op, X, Value)) :-
206
400
  get_dict(value, Row, Value),
207
401
  number(Value).
208
402
 
403
+ % "count = 4", for naming an assumption in a rejection.
404
+ constraint_text(Variable, Row, Text) :-
405
+ get_dict(operator, Row, OpString),
406
+ get_dict(value, Row, Value),
407
+ atom_string(Op, OpString),
408
+ operator_symbol(Op, Symbol),
409
+ format(atom(Text), "~w ~w ~w", [Variable, Symbol, Value]).
410
+
411
+ operator_symbol(eq, '=').
412
+ operator_symbol(neq, '!=').
413
+ operator_symbol(lt, '<').
414
+ operator_symbol(lte, '<=').
415
+ operator_symbol(gt, '>').
416
+ operator_symbol(gte, '>=').
417
+
418
+ % arithmetic_counterexample(+Type, +Assumptions, +Claim, -Value): a value
419
+ % next to one of the constants that satisfies every assumption and breaks the
420
+ % claim, so a rejection can name it (a count of 5 against "exactly 4").
421
+ arithmetic_counterexample(Type, Assumptions, Claim, Value) :-
422
+ findall(C, (member(Row, [Claim|Assumptions]), get_dict(value, Row, C)), Constants),
423
+ ( Type == "int" -> Step = 1 ; Step = 0.5 ),
424
+ findall(V,
425
+ ( member(C, [0|Constants]),
426
+ member(D, [0, -1, 1]),
427
+ V is C + D*Step,
428
+ ( Type == "int" -> integer(V) ; true )
429
+ ),
430
+ Candidates0),
431
+ sort(Candidates0, Candidates),
432
+ member(Value, Candidates),
433
+ forall(member(Row, Assumptions), concrete_holds(Row, Value)),
434
+ \+ concrete_holds(Claim, Value),
435
+ !.
436
+
437
+ concrete_holds(Row, X) :-
438
+ arithmetic_constraint(Y, Row, c(Op, Y, V)),
439
+ concrete_op(Op, X, V).
440
+
441
+ concrete_op(eq, X, V) :- X =:= V.
442
+ concrete_op(neq, X, V) :- X =\= V.
443
+ concrete_op(lt, X, V) :- X < V.
444
+ concrete_op(lte, X, V) :- X =< V.
445
+ concrete_op(gt, X, V) :- X > V.
446
+ concrete_op(gte, X, V) :- X >= V.
447
+
209
448
  integer_variables("int", X, [X]).
210
449
  integer_variables("real", _, []).
211
450
 
package/src/discovery.pl CHANGED
@@ -9,6 +9,7 @@
9
9
  coverage_report_json/12,
10
10
  coverage_report_json/13,
11
11
  coverage_evidence_json/5,
12
+ coverage_evidence_json/7,
12
13
  evidence_floor_json/6,
13
14
  requirement_proof_json/5,
14
15
  requirement_proof_json/7,
@@ -155,12 +156,14 @@ symbol_explain_role(_SymbolId, unknown).
155
156
  % not used: they take seconds on a large KB. The functional-predicate
156
157
  % declarations a rule comparison reads are collected once per call rather
157
158
  % than once per compared pair, which dominated the cost for requirements with
158
- % many rules. A requirement whose checks raise an error reports that error
159
- % instead of failing the answer. Scope names the snapshot the verdicts were
160
- % computed from.
159
+ % many rules; the infeasible success scenarios are likewise analysed once per
160
+ % call rather than once per requirement. A requirement whose checks raise an
161
+ % error reports that error instead of failing the answer. Scope names the
162
+ % snapshot the verdicts were computed from.
161
163
  search_answer_verdicts_json(ReqIds, JsonString) :-
162
164
  search_answer_functional(Functional),
163
- maplist(search_answer_verdict(Functional), ReqIds, Verdicts),
165
+ search_answer_infeasible(Infeasible),
166
+ maplist(search_answer_verdict(Functional, Infeasible), ReqIds, Verdicts),
164
167
  status_scope_dict(Scope),
165
168
  dict_json_string(_{requirements: Verdicts, scope: Scope}, JsonString).
166
169
 
@@ -171,14 +174,27 @@ search_answer_functional(some(Functional)) :-
171
174
  !.
172
175
  search_answer_functional(none).
173
176
 
177
+ % Every infeasible success scenario, analysed once per call: the analysis
178
+ % walks every success scenario, so running it per requirement multiplied a
179
+ % whole-KB pass by the number of requirements in the answer. When it raises,
180
+ % each requirement's forbids check raises the same error itself and reports
181
+ % it as that requirement's error, as before.
182
+ search_answer_infeasible(some(Infeasible)) :-
183
+ catch(findall(infeasible(ScenarioId, ReqIds, AssumedFacts, ReqFacts, Reason),
184
+ infeasible_scenario(ScenarioId, ReqIds, AssumedFacts, ReqFacts, Reason),
185
+ Infeasible),
186
+ _, fail),
187
+ !.
188
+ search_answer_infeasible(none).
189
+
174
190
  search_answer_rule_status(some(Functional), RuleA, RuleB, Status) :-
175
191
  !,
176
192
  logic_rule_conflict(RuleA, RuleB, Functional, Status).
177
193
  search_answer_rule_status(none, RuleA, RuleB, Status) :-
178
194
  logic_rule_conflict(RuleA, RuleB, Status).
179
195
 
180
- search_answer_verdict(Functional, ReqId, Verdict) :-
181
- catch(search_answer_verdict_(Functional, ReqId, Verdict0), Error, true),
196
+ search_answer_verdict(Functional, Infeasible, ReqId, Verdict) :-
197
+ catch(search_answer_verdict_(Functional, Infeasible, ReqId, Verdict0), Error, true),
182
198
  ( var(Error)
183
199
  -> Verdict = Verdict0
184
200
  ; format(string(Text0), "~q", [Error]),
@@ -186,7 +202,7 @@ search_answer_verdict(Functional, ReqId, Verdict) :-
186
202
  Verdict = _{id: ReqId, error: Text}
187
203
  ).
188
204
 
189
- search_answer_verdict_(Functional, ReqId, _{
205
+ search_answer_verdict_(Functional, Infeasible, ReqId, _{
190
206
  id: ReqId,
191
207
  contradictions: Contradictions,
192
208
  scenarios: Scenarios,
@@ -197,7 +213,7 @@ search_answer_verdict_(Functional, ReqId, _{
197
213
  sort(Contradictions0, Contradictions),
198
214
  findall(S, search_answer_scenario_outcome(ReqId, S), Scenarios0),
199
215
  sort(Scenarios0, Scenarios),
200
- findall(F, search_answer_forbidden_scenario(ReqId, F), Forbids0),
216
+ findall(F, search_answer_forbidden_scenario(Infeasible, ReqId, F), Forbids0),
201
217
  sort(Forbids0, Forbids),
202
218
  search_answer_inventory(ReqId, Inventory).
203
219
 
@@ -291,7 +307,18 @@ search_answer_outcome(ScenarioId, Result, _{scenario: ScenarioId, outcome: Outco
291
307
  ; format(atom(Outcome), "~q", [Result])
292
308
  ).
293
309
 
294
- search_answer_forbidden_scenario(ReqId, _{
310
+ search_answer_forbidden_scenario(some(Infeasible), ReqId, _{
311
+ scenario: ScenarioId,
312
+ assumed: AssumedFacts,
313
+ fact: ReqFacts,
314
+ reason: Reason
315
+ }) :-
316
+ member(infeasible(ScenarioId, ReqIds, AssumedFacts, ReqFacts, RawReason), Infeasible),
317
+ memberchk(ReqId, ReqIds),
318
+ \+ kb_relationship(specified_by, ReqId, ScenarioId),
319
+ search_answer_text(RawReason, Reason).
320
+
321
+ search_answer_forbidden_scenario(none, ReqId, _{
295
322
  scenario: ScenarioId,
296
323
  assumed: AssumedFacts,
297
324
  fact: ReqFacts,
@@ -352,6 +379,16 @@ search_answer_clip(Text, Max, Clipped) :-
352
379
  % the full coverage row set, keeping the response small on large KBs.
353
380
  coverage_evidence_json(Tags, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
354
381
  requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, ProofContext),
382
+ coverage_evidence_rows_json(Tags, ProofContext, JsonString).
383
+
384
+ % The same rows under a receipt binding mode, so kb_check judges receipts
385
+ % like kb_coverage and kibi proof explain: per_contract with the TestId ->
386
+ % binding hash dict, or strict_snapshot.
387
+ coverage_evidence_json(Tags, BindingMode, TestBindings, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
388
+ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, ProofContext),
389
+ coverage_evidence_rows_json(Tags, ProofContext, JsonString).
390
+
391
+ coverage_evidence_rows_json(Tags, ProofContext, JsonString) :-
355
392
  findall(Evidence,
356
393
  ( kb_entity(Id, req, Props),
357
394
  matches_tags(Tags, Props),
@@ -479,9 +516,9 @@ coverage_report_json(By, Tags, IncludePassing, BindingMode, TestBindings, Includ
479
516
  binding(By, Tags, IncludePassing, BindingMode, TestBindings, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds),
480
517
  binding_coverage_rows(By, Tags, IncludePassing, BindingMode, TestBindings, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds),
481
518
  SortedRows,
482
- Summary),
519
+ Summary,
520
+ Meta),
483
521
  paginate_rows(SortedRows, Offset, Limit, Rows),
484
- status_meta_dict(Meta),
485
522
  Response = _{summary: Summary, rows: Rows, meta: Meta},
486
523
  dict_json_string(Response, JsonString).
487
524
 
@@ -497,9 +534,9 @@ coverage_report_json(By, Tags, IncludePassing, StatusFilter, IncludeTransitive,
497
534
  status(By, Tags, IncludePassing, StatusFilter, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds),
498
535
  status_filtered_coverage_rows(By, Tags, IncludePassing, StatusFilter, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds),
499
536
  SortedRows,
500
- Summary),
537
+ Summary,
538
+ Meta),
501
539
  paginate_rows(SortedRows, Offset, Limit, Rows),
502
- status_meta_dict(Meta),
503
540
  Response = _{summary: Summary, rows: Rows, meta: Meta},
504
541
  dict_json_string(Response, JsonString).
505
542
 
@@ -512,9 +549,9 @@ coverage_report_json(By, Tags, IncludePassing, StatusFilter, BindingMode, TestBi
512
549
  status_binding(By, Tags, IncludePassing, StatusFilter, BindingMode, TestBindings, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds),
513
550
  status_binding_coverage_rows(By, Tags, IncludePassing, StatusFilter, BindingMode, TestBindings, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds),
514
551
  SortedRows,
515
- Summary),
552
+ Summary,
553
+ Meta),
516
554
  paginate_rows(SortedRows, Offset, Limit, Rows),
517
- status_meta_dict(Meta),
518
555
  Response = _{summary: Summary, rows: Rows, meta: Meta},
519
556
  dict_json_string(Response, JsonString).
520
557
 
@@ -531,26 +568,31 @@ binding_coverage_rows(req, Tags, IncludePassing, BindingMode, TestBindings, Incl
531
568
  binding_coverage_rows(By, Tags, IncludePassing, _BindingMode, _TestBindings, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
532
569
  coverage_rows(By, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary).
533
570
 
534
- :- dynamic coverage_memo/3.
571
+ :- dynamic coverage_memo/4.
535
572
 
536
- %% memo_sorted_coverage(+Args, :Compute, -SortedRows, -Summary)
573
+ %% memo_sorted_coverage(+Args, :Compute, -SortedRows, -Summary, -Meta)
537
574
  % Large coverage reports are fetched in bounded pages so no single answer
538
575
  % approaches the engine's output cap. Every page would otherwise recompute
539
576
  % the whole report; keep the most recent sorted result for the exact
540
577
  % arguments, graph, and RDF generation. Coverage reads only the store and its
541
- % arguments, so the memo is invalidated by any store change.
542
- memo_sorted_coverage(Args, Compute, SortedRows, Summary) :-
578
+ % arguments, so the memo is invalidated by any store change. The status meta
579
+ % is kept with it: its freshness check stats every indexed source, which cost
580
+ % more than the page itself, and the pages of one report share its arguments
581
+ % (including the check time), so they describe the same snapshot.
582
+ memo_sorted_coverage(Args, Compute, SortedRows, Summary, Meta) :-
543
583
  (kb:kb_graph(Graph) -> true ; Graph = none),
544
584
  rdf_generation(Generation),
545
585
  Key = key(Args, Graph, Generation),
546
- ( coverage_memo(Stored, StoredRows, StoredSummary),
586
+ ( coverage_memo(Stored, StoredRows, StoredSummary, StoredMeta),
547
587
  Stored =@= Key
548
588
  -> SortedRows = StoredRows,
549
- Summary = StoredSummary
589
+ Summary = StoredSummary,
590
+ Meta = StoredMeta
550
591
  ; call(Compute, Rows0, Summary),
551
592
  sort_dict_rows(Rows0, SortedRows),
552
- retractall(coverage_memo(_, _, _)),
553
- assertz(coverage_memo(Key, SortedRows, Summary))
593
+ status_meta_dict(Meta),
594
+ retractall(coverage_memo(_, _, _, _)),
595
+ assertz(coverage_memo(Key, SortedRows, Summary, Meta))
554
596
  ).
555
597
 
556
598
  status_filtered_coverage_rows(By, Tags, IncludePassing, [], IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
package/src/intervals.pl CHANGED
@@ -22,6 +22,7 @@
22
22
  %% numeric_constraints_satisfiable(+Constraints)
23
23
  % True when some assignment of reals satisfies every constraint.
24
24
  numeric_constraints_satisfiable(Constraints) :-
25
+ % mutalog-disable-next-line literal: [_] names no variable, so it equals []
25
26
  numeric_constraints_satisfiable(Constraints, []).
26
27
 
27
28
  %% numeric_constraints_satisfiable(+Constraints, +IntegerVariables)
@@ -43,6 +44,7 @@ numeric_constraints_satisfiable(Constraints, IntegerVariables) :-
43
44
  %% numeric_constraint_entailed(+Premises, +Constraint)
44
45
  % True when every assignment that satisfies Premises satisfies Constraint.
45
46
  numeric_constraint_entailed(Premises, Constraint) :-
47
+ % mutalog-disable-next-line literal: [_] names no variable, so it equals []
46
48
  numeric_constraint_entailed(Premises, Constraint, []).
47
49
 
48
50
  %% numeric_constraint_entailed(+Premises, +Constraint, +IntegerVariables)
@@ -60,12 +62,12 @@ finite_constraint(c(_Op, _Variable, Value)) :-
60
62
  ; true
61
63
  ).
62
64
 
63
- integer_variable(_, all) :- !.
65
+ % Only called as an if-then-else condition, which keeps the first answer.
66
+ integer_variable(_, all).
64
67
  integer_variable(Variable, IntegerVariables) :-
65
68
  is_list(IntegerVariables),
66
69
  member(Candidate, IntegerVariables),
67
- Candidate == Variable,
68
- !.
70
+ Candidate == Variable.
69
71
 
70
72
  %% numeric_constraint_negation(+Constraint, -Negated)
71
73
  numeric_constraint_negation(c(Op, Variable, Value), c(Negated, Variable, Value)) :-
@@ -80,10 +82,15 @@ negated_operator(gt, lte).
80
82
 
81
83
  %% numeric_constraint_holds(+Op, +Left, +Right)
82
84
  % Evaluate a comparison between two numbers.
85
+ % mutalog-disable-next-line cut_insert: a comparison has one answer and the operators are distinct
83
86
  numeric_constraint_holds(eq, Left, Right) :- Left =:= Right.
87
+ % mutalog-disable-next-line cut_insert: a comparison has one answer and the operators are distinct
84
88
  numeric_constraint_holds(neq, Left, Right) :- Left =\= Right.
89
+ % mutalog-disable-next-line cut_insert: a comparison has one answer and the operators are distinct
85
90
  numeric_constraint_holds(lt, Left, Right) :- Left < Right.
91
+ % mutalog-disable-next-line cut_insert: a comparison has one answer and the operators are distinct
86
92
  numeric_constraint_holds(lte, Left, Right) :- Left =< Right.
93
+ % mutalog-disable-next-line cut_insert: a comparison has one answer and the operators are distinct
87
94
  numeric_constraint_holds(gt, Left, Right) :- Left > Right.
88
95
  numeric_constraint_holds(gte, Left, Right) :- Left >= Right.
89
96
 
@@ -94,14 +101,18 @@ variable_feasible(Variable, Constraints) :-
94
101
 
95
102
  % interval(Low, LowKind, High, HighKind, Excluded)
96
103
  apply_bound(eq-Value, interval(Lo, LoK, Hi, HiK, Ex), interval(Lo1, LoK1, Hi1, HiK1, Ex)) :-
104
+ % mutalog-disable-next-line cut_insert: raise_low/6 and lower_high/6 are det and the operators are distinct
97
105
  raise_low(Lo, LoK, Value, closed, Lo1, LoK1),
98
106
  lower_high(Hi, HiK, Value, closed, Hi1, HiK1).
99
107
  apply_bound(neq-Value, interval(Lo, LoK, Hi, HiK, Ex), interval(Lo, LoK, Hi, HiK, [Value|Ex])).
100
108
  apply_bound(lt-Value, interval(Lo, LoK, Hi, HiK, Ex), interval(Lo, LoK, Hi1, HiK1, Ex)) :-
109
+ % mutalog-disable-next-line cut_insert: raise_low/6 and lower_high/6 are det and the operators are distinct
101
110
  lower_high(Hi, HiK, Value, open, Hi1, HiK1).
102
111
  apply_bound(lte-Value, interval(Lo, LoK, Hi, HiK, Ex), interval(Lo, LoK, Hi1, HiK1, Ex)) :-
112
+ % mutalog-disable-next-line cut_insert: raise_low/6 and lower_high/6 are det and the operators are distinct
103
113
  lower_high(Hi, HiK, Value, closed, Hi1, HiK1).
104
114
  apply_bound(gt-Value, interval(Lo, LoK, Hi, HiK, Ex), interval(Lo1, LoK1, Hi, HiK, Ex)) :-
115
+ % mutalog-disable-next-line cut_insert: raise_low/6 and lower_high/6 are det and the operators are distinct
105
116
  raise_low(Lo, LoK, Value, open, Lo1, LoK1).
106
117
  apply_bound(gte-Value, interval(Lo, LoK, Hi, HiK, Ex), interval(Lo1, LoK1, Hi, HiK, Ex)) :-
107
118
  raise_low(Lo, LoK, Value, closed, Lo1, LoK1).
@@ -116,13 +127,18 @@ lower_high(Hi, _, Value, Kind, Value, Kind) :- Value < Hi, !.
116
127
  lower_high(Hi, HiK, Value, Kind, Hi, Kind1) :- Value =:= Hi, !, tighter_kind(HiK, Kind, Kind1).
117
128
  lower_high(Hi, HiK, _, _, Hi, HiK).
118
129
 
130
+ % mutalog-disable-next-line cut: the second answer, open, only shrinks the interval
119
131
  tighter_kind(closed, closed, closed) :- !.
120
132
  tighter_kind(_, _, open).
121
133
 
122
134
  % An unbounded or non-degenerate real interval cannot be emptied by finitely
123
135
  % many excluded points. A degenerate interval is a single closed point.
136
+ % The callers keep only the first answer, so the cuts are not observable.
137
+ % mutalog-disable-next-line cut
124
138
  interval_non_empty(interval(ninf, _, _, _, _)) :- !.
139
+ % mutalog-disable-next-line cut
125
140
  interval_non_empty(interval(_, _, pinf, _, _)) :- !.
141
+ % mutalog-disable-next-line cut
126
142
  interval_non_empty(interval(Lo, _, Hi, _, _)) :- Lo < Hi, !.
127
143
  interval_non_empty(interval(Lo, closed, Hi, closed, Excluded)) :-
128
144
  Lo =:= Hi,
@@ -131,34 +147,37 @@ interval_non_empty(interval(Lo, closed, Hi, closed, Excluded)) :-
131
147
  % Integer variables: every strict or fractional bound is tightened to the
132
148
  % nearest admissible integer, so the set is an integer range [Lo, Hi] minus
133
149
  % finitely many excluded integers. An eq bound with a fractional value admits
134
- % no integer at all.
150
+ % no integer at all, so the fold fails. The operators are distinct, so the
151
+ % cuts after the heads are not observable.
135
152
  integer_variable_feasible(Variable, Constraints) :-
136
153
  findall(Op-Value, (member(c(Op, Other, Value), Constraints), Other == Variable), Bounds),
137
154
  foldl(apply_integer_bound, Bounds, ibox(ninf, pinf, []), Box),
138
155
  integer_box_non_empty(Box).
139
156
 
140
- apply_integer_bound(_, empty, empty) :- !.
141
- apply_integer_bound(eq-Value, ibox(Lo, Hi, Ex), Box) :- !,
142
- ( integral_value(Value, Integer)
143
- -> integer_raise_low(Lo, Integer, Lo1),
144
- integer_lower_high(Hi, Integer, Hi1),
145
- Box = ibox(Lo1, Hi1, Ex)
146
- ; Box = empty
147
- ).
157
+ % mutalog-disable-next-line cut
158
+ apply_integer_bound(eq-Value, ibox(Lo, Hi, Ex), ibox(Lo1, Hi1, Ex)) :- !,
159
+ integral_value(Value, Integer),
160
+ integer_raise_low(Lo, Integer, Lo1),
161
+ integer_lower_high(Hi, Integer, Hi1).
162
+ % mutalog-disable-next-line cut
148
163
  apply_integer_bound(neq-Value, ibox(Lo, Hi, Ex), ibox(Lo, Hi, Ex1)) :- !,
149
164
  ( integral_value(Value, Integer)
150
165
  -> Ex1 = [Integer|Ex]
151
166
  ; Ex1 = Ex
152
167
  ).
168
+ % mutalog-disable-next-line cut
153
169
  apply_integer_bound(gt-Value, ibox(Lo, Hi, Ex), ibox(Lo1, Hi, Ex)) :- !,
154
170
  Bound is floor(Value) + 1,
155
171
  integer_raise_low(Lo, Bound, Lo1).
172
+ % mutalog-disable-next-line cut
156
173
  apply_integer_bound(gte-Value, ibox(Lo, Hi, Ex), ibox(Lo1, Hi, Ex)) :- !,
157
174
  Bound is ceiling(Value),
158
175
  integer_raise_low(Lo, Bound, Lo1).
176
+ % mutalog-disable-next-line cut
159
177
  apply_integer_bound(lt-Value, ibox(Lo, Hi, Ex), ibox(Lo, Hi1, Ex)) :- !,
160
178
  Bound is ceiling(Value) - 1,
161
179
  integer_lower_high(Hi, Bound, Hi1).
180
+ % mutalog-disable-next-line cut
162
181
  apply_integer_bound(lte-Value, ibox(Lo, Hi, Ex), ibox(Lo, Hi1, Ex)) :- !,
163
182
  Bound is floor(Value),
164
183
  integer_lower_high(Hi, Bound, Hi1).
@@ -175,11 +194,13 @@ integer_lower_high(pinf, Bound, Bound) :- !.
175
194
  integer_lower_high(Hi, Bound, Hi1) :- Hi1 is min(Hi, Bound).
176
195
 
177
196
  % An unbounded integer range cannot be emptied by finitely many exclusions.
178
- integer_box_non_empty(empty) :- !, fail.
197
+ % A range with Lo above Hi has a non-positive size, so it fails the count.
198
+ % The callers keep only the first answer, so the cuts are not observable.
199
+ % mutalog-disable-next-line cut
179
200
  integer_box_non_empty(ibox(ninf, _, _)) :- !.
201
+ % mutalog-disable-next-line cut
180
202
  integer_box_non_empty(ibox(_, pinf, _)) :- !.
181
203
  integer_box_non_empty(ibox(Lo, Hi, Excluded)) :-
182
- Lo =< Hi,
183
204
  findall(Value, (member(Value, Excluded), Value >= Lo, Value =< Hi), Inside0),
184
205
  sort(Inside0, Inside),
185
206
  length(Inside, ExcludedCount),