kibi-core 0.26.1 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-core",
3
- "version": "0.26.1",
3
+ "version": "0.27.0",
4
4
  "private": false,
5
5
  "description": "Core Prolog modules and RDF graph logic for Kibi",
6
6
  "type": "module",
package/src/checks.pl CHANGED
@@ -6,7 +6,11 @@
6
6
  :- module(checks, [
7
7
  check_all/1, % Returns all violations as a dict
8
8
  check_all_json/1, % Returns all violations as JSON string
9
+ check_all_json_file/2, % Writes all violations as JSON to a file
9
10
  check_selected_json/2, % Returns only selected rule violations as JSON
11
+ check_selected_json_file/2, % Writes selected rule violations as JSON to a file
12
+ check_all_json_transport/4, % File transport with an answer fallback
13
+ check_selected_json_transport/4, % Same, for selected rules
10
14
  check_must_priority_coverage/1, % Returns list of must-priority violations
11
15
  check_symbol_coverage/1, % Returns list of uncovered symbols
12
16
  check_changed_symbol_coverage/2, % symbol-coverage restricted to a staged change set
@@ -470,7 +474,7 @@ parse_json_object(Dict, Dict) :-
470
474
  !.
471
475
  parse_json_object(Raw, Dict) :-
472
476
  (atom(Raw) ; string(Raw)),
473
- catch(atom_json_dict(Raw, Dict, [value_string_as(string)]), _, fail),
477
+ json_text_term(Raw, Dict),
474
478
  is_dict(Dict).
475
479
 
476
480
  parse_json_list(Raw, Entries) :-
@@ -482,9 +486,48 @@ parse_json_list(^^(Value, _), Entries) :-
482
486
  parse_json_list(Value, Entries).
483
487
  parse_json_list(Raw, Entries) :-
484
488
  (atom(Raw) ; string(Raw)),
485
- catch(atom_json_dict(Raw, Entries, [value_string_as(string)]), _, fail),
489
+ json_text_term(Raw, Entries),
486
490
  is_list(Entries).
487
491
 
492
+ % The same JSON text (a rule body, a claim inventory, a receipt history) is
493
+ % parsed by several rules and by every requirement that links the fact, so
494
+ % the parsed term is kept by its text. Parsing is a pure function of the
495
+ % text, so the memo never goes stale; it is bounded so a long-lived engine
496
+ % cannot grow it without limit.
497
+ :- dynamic json_text_memo/2.
498
+ :- dynamic json_text_memo_size/1.
499
+
500
+ % mutalog-disable-next-line literal: the bound is a tuning value; any size gives the same answers
501
+ json_text_memo_limit(16384).
502
+
503
+ json_text_term(Raw, Term) :-
504
+ atom_string(Text, Raw),
505
+ ( json_text_memo(Text, Stored)
506
+ -> Stored = parsed(Term)
507
+ ; ( catch(atom_json_dict(Text, Parsed, [value_string_as(string)]), _, fail)
508
+ -> Stored = parsed(Parsed)
509
+ ; Stored = invalid
510
+ ),
511
+ json_text_remember(Text, Stored),
512
+ Stored = parsed(Term)
513
+ ).
514
+
515
+ json_text_remember(Text, Stored) :-
516
+ ( json_text_memo_size(Size)
517
+ -> true
518
+ ; Size = 0
519
+ ),
520
+ json_text_memo_limit(Limit),
521
+ ( Size >= Limit
522
+ -> retractall(json_text_memo(_, _)),
523
+ retractall(json_text_memo_size(_)),
524
+ Next = 1
525
+ ; retractall(json_text_memo_size(_)),
526
+ Next is Size + 1
527
+ ),
528
+ assertz(json_text_memo(Text, Stored)),
529
+ assertz(json_text_memo_size(Next)).
530
+
488
531
  %% check_no_dangling_refs(-Violations)
489
532
  % Finds all relationships referencing non-existent entities.
490
533
  check_no_dangling_refs(Violations) :-
@@ -1281,7 +1324,28 @@ analysis_outcome(analysis([], [], false, []), feasible).
1281
1324
  % advisory reasons in reporting order; ByException is true when some check is
1282
1325
  % satisfiable only because an approved exception waived a constraint;
1283
1326
  % NotApplicable is non-empty only when no assumption is governed at all.
1284
- scenario_feasibility_analysis(ScenarioId, analysis(Witnesses, Unknowns, ByException, NotApplicable)) :-
1327
+ % The analysis of a scenario is kept per KB generation: the blocking rule,
1328
+ % the unknown-feasibility rule and every requirement's proof ladder ask for
1329
+ % the same scenarios. Inside a transaction (what-if staging) the KB is not
1330
+ % the generation's, so the analysis is computed afresh.
1331
+ scenario_feasibility_analysis(ScenarioId, Analysis) :-
1332
+ ( nonvar(ScenarioId),
1333
+ \+ rdf_db:rdf_active_transaction(_),
1334
+ kb:index_verification_key(scenario_feasibility, Key)
1335
+ -> ( kb:kb_scenario_feasibility_memo(ScenarioId, Key, Analysis0)
1336
+ -> Analysis = Analysis0
1337
+ ; compute_scenario_feasibility_analysis(ScenarioId, Analysis),
1338
+ ( once(kb:kb_scenario_feasibility_memo(_, Stored, _)),
1339
+ Stored \== Key
1340
+ -> retractall(kb:kb_scenario_feasibility_memo(_, _, _))
1341
+ ; true
1342
+ ),
1343
+ assertz(kb:kb_scenario_feasibility_memo(ScenarioId, Key, Analysis))
1344
+ )
1345
+ ; compute_scenario_feasibility_analysis(ScenarioId, Analysis)
1346
+ ).
1347
+
1348
+ compute_scenario_feasibility_analysis(ScenarioId, analysis(Witnesses, Unknowns, ByException, NotApplicable)) :-
1285
1349
  findall(FactId, kb_relationship(assumes, ScenarioId, FactId), Assumed0),
1286
1350
  sort(Assumed0, Assumed),
1287
1351
  ( Assumed == []
@@ -2526,7 +2590,24 @@ check_domain_contradictions_and_witnesses(Violations, Witnesses) :-
2526
2590
  % Exact, JSON-safe evidence for every strict-property, ground-predicate, and
2527
2591
  % safe-rule contradiction result. Rule overlap that cannot be proven or
2528
2592
  % disproven remains status=unresolved rather than becoming consistency.
2593
+ %
2594
+ % kb_check, kb_coverage and the proof context each ask for every witness, so
2595
+ % the result is memoized per KB generation, like the entity property memo.
2596
+ % Inside an RDF transaction (the what-if staging) it is always recomputed.
2529
2597
  check_domain_contradiction_witnesses(Witnesses) :-
2598
+ ( \+ rdf_db:rdf_active_transaction(_),
2599
+ kb:index_verification_key(domain_contradictions, Key)
2600
+ -> ( kb:kb_contradiction_witness_memo(Key, Cached)
2601
+ -> Witnesses = Cached
2602
+ ; compute_domain_contradiction_witnesses(Computed),
2603
+ retractall(kb:kb_contradiction_witness_memo(_, _)),
2604
+ assertz(kb:kb_contradiction_witness_memo(Key, Computed)),
2605
+ Witnesses = Computed
2606
+ )
2607
+ ; compute_domain_contradiction_witnesses(Witnesses)
2608
+ ).
2609
+
2610
+ compute_domain_contradiction_witnesses(Witnesses) :-
2530
2611
  findall(Witness, req_conflict_witness(_ReqA, _ReqB, Witness), GroundWitnesses),
2531
2612
  functional_predicate_declarations(Functional),
2532
2613
  findall(Witness, rule_contradiction_witness(Functional, Witness), RuleWitnesses),
@@ -2701,22 +2782,42 @@ opposing_rule_requirements(ReqA, ReqB, RuleA, RuleB) :-
2701
2782
  % Each unordered pair of (requirement, rule fact) endpoints is compared once,
2702
2783
  % ordered by the requirement-fact pair: ordering requirements and facts
2703
2784
  % independently would never compare REQ-A->FACT-Z with REQ-Z->FACT-A.
2785
+ %
2786
+ % Only current requirements with requires_rule links can pair, so the pairs
2787
+ % are drawn from that list (in current_req/1 order, each with its rule links
2788
+ % in relationship order) rather than from every pair of current
2789
+ % requirements; solutions and their order are unchanged.
2704
2790
  opposing_rule_requirement_facts(ReqA, ReqB, FactA, FactB, RuleA, RuleB) :-
2705
- kb:current_req(ReqA),
2706
- kb:current_req(ReqB),
2791
+ rule_requirement_links(Links),
2792
+ member(ReqA-RulesA, Links),
2793
+ member(ReqB-RulesB, Links),
2707
2794
  ReqA \== ReqB,
2708
- kb_relationship(requires_rule, ReqA, FactA),
2709
- kb_relationship(requires_rule, ReqB, FactB),
2795
+ member(FactA-RuleA, RulesA),
2796
+ member(FactB-RuleB, RulesB),
2710
2797
  FactA \== FactB,
2711
- ReqA-FactA @< ReqB-FactB,
2712
- kb_entity(FactA, fact, PropsA),
2713
- kb_entity(FactB, fact, PropsB),
2714
- memberchk(fact_kind=KindA, PropsA),
2715
- memberchk(fact_kind=KindB, PropsB),
2716
- normalize_term_atom(KindA, rule),
2717
- normalize_term_atom(KindB, rule),
2718
- logic_rule_from_props(PropsA, RuleA),
2719
- logic_rule_from_props(PropsB, RuleB).
2798
+ % mutalog-disable-next-line compare: equal endpoints need ReqA == ReqB, which is excluded above
2799
+ ReqA-FactA @< ReqB-FactB.
2800
+
2801
+ % Each requirement's rule facts with their parsed rules, read once rather
2802
+ % than once per pair: a link to a fact that is not a rule, or whose rule does
2803
+ % not parse, never pairs.
2804
+ rule_requirement_links(Links) :-
2805
+ findall(ReqId-Rules,
2806
+ ( kb:current_req(ReqId),
2807
+ findall(FactId-Rule,
2808
+ ( kb_relationship(requires_rule, ReqId, FactId),
2809
+ rule_fact_rule(FactId, Rule) ),
2810
+ Rules),
2811
+ % mutalog-disable-next-line goal_true,literal: a requirement without rules never pairs; this only prunes
2812
+ Rules \== [] ),
2813
+ Links).
2814
+
2815
+ rule_fact_rule(FactId, Rule) :-
2816
+ kb_entity(FactId, fact, Props),
2817
+ memberchk(fact_kind=Kind, Props),
2818
+ % mutalog-disable-next-line goal_true: only a rule fact carries a rule body to decode
2819
+ normalize_term_atom(Kind, rule),
2820
+ logic_rule_from_props(Props, Rule).
2720
2821
 
2721
2822
  rule_contradiction_witness(Functional, Witness) :-
2722
2823
  opposing_rule_requirement_facts(ReqA, ReqB, FactA, FactB, RuleA, RuleB),
@@ -3472,7 +3573,7 @@ inventory_entries(Raw, Entries) :-
3472
3573
  ( is_list(Raw) -> Entries = Raw
3473
3574
  ; Raw = ^^(Value, _) -> inventory_entries(Value, Entries)
3474
3575
  ; (atom(Raw) ; string(Raw)),
3475
- catch(atom_json_dict(Raw, JsonEntries, [value_string_as(string)]), _, fail),
3576
+ json_text_term(Raw, JsonEntries),
3476
3577
  is_list(JsonEntries)
3477
3578
  -> Entries = JsonEntries
3478
3579
  ; (atom(Raw) ; string(Raw)),
@@ -3748,6 +3849,66 @@ check_all_json_with_options(JsonString, RequireAdr) :-
3748
3849
  JsonString
3749
3850
  ).
3750
3851
 
3852
+ %% check_all_json_file(+Path, +RequireAdr)
3853
+ % Writes the JSON check_all_json_with_options/2 would answer to Path.
3854
+ % The answer itself carries no violations, so a large KB's report is never
3855
+ % bounded by the engine's output capacity; the caller reads the file.
3856
+ check_all_json_file(Path, RequireAdr) :-
3857
+ check_all_with_options(ViolationsDict, RequireAdr),
3858
+ violations_json_to_file(ViolationsDict, Path).
3859
+
3860
+ %% check_selected_json_file(+Rules, +Path)
3861
+ % File-transport form of check_selected_json/2.
3862
+ check_selected_json_file(Rules, Path) :-
3863
+ check_selected(Rules, ViolationsDict),
3864
+ violations_json_to_file(ViolationsDict, Path).
3865
+
3866
+ %% check_all_json_transport(+Path, +RequireAdr, -Transport, -JsonString)
3867
+ % Writes the violations JSON to Path (Transport = file, JsonString = ''), or
3868
+ % answers it as JsonString (Transport = answer) when Path is `none` or the
3869
+ % file cannot be written: a full or read-only temp folder, or an engine that
3870
+ % cannot reach the caller's temp folder, must not fail a check that fits in
3871
+ % an answer.
3872
+ check_all_json_transport(Path, RequireAdr, Transport, JsonString) :-
3873
+ check_all_with_options(ViolationsDict, RequireAdr),
3874
+ violations_json_transport(ViolationsDict, Path, Transport, JsonString).
3875
+
3876
+ %% check_selected_json_transport(+Rules, +Path, -Transport, -JsonString)
3877
+ % check_all_json_transport/4 for the named rules only.
3878
+ check_selected_json_transport(Rules, Path, Transport, JsonString) :-
3879
+ check_selected(Rules, ViolationsDict),
3880
+ violations_json_transport(ViolationsDict, Path, Transport, JsonString).
3881
+
3882
+ violations_json_transport(ViolationsDict, Path, Transport, JsonString) :-
3883
+ ( Path \== none,
3884
+ catch(violations_json_to_file(ViolationsDict, Path), Error,
3885
+ ( file_transport_error(Error)
3886
+ -> fail
3887
+ ; throw(Error)
3888
+ ))
3889
+ -> Transport = file,
3890
+ JsonString = ''
3891
+ ; Transport = answer,
3892
+ violations_dict_to_json(ViolationsDict, JsonDict),
3893
+ with_output_to_string(
3894
+ json_write_dict(current_output, JsonDict, [width(0)]),
3895
+ JsonString
3896
+ )
3897
+ ).
3898
+
3899
+ file_transport_error(error(permission_error(_, _, _), _)).
3900
+ file_transport_error(error(existence_error(_, _), _)).
3901
+ file_transport_error(error(resource_error(_), _)).
3902
+ file_transport_error(error(io_error(_, _), _)).
3903
+
3904
+ violations_json_to_file(ViolationsDict, Path) :-
3905
+ violations_dict_to_json(ViolationsDict, JsonDict),
3906
+ setup_call_cleanup(
3907
+ open(Path, write, Stream, [encoding(utf8)]),
3908
+ json_write_dict(Stream, JsonDict, [width(0)]),
3909
+ close(Stream)
3910
+ ).
3911
+
3751
3912
  %% check_all_with_options(-ViolationsDict, +RequireAdr)
3752
3913
  % Returns a dict with all violations, respecting options.
3753
3914
  check_all_with_options(ViolationsDict, RequireAdr) :-
@@ -35,7 +35,10 @@
35
35
  % has each member as a required (or nullable)
36
36
  % field; with property middleware every
37
37
  % selected route's middleware stands in the
38
- % relation
38
+ % relation; with property precedes and
39
+ % operator includes each member "A -> B"
40
+ % holds in the list read: A and B are in it
41
+ % and every A comes before every B
39
42
  % set fact under state_machine with operator includes, subset_of or
40
43
  % excludes, or with property reachable_pairs or required_waypoints
41
44
  % state_machine the projection stands in the operator's
@@ -230,6 +233,14 @@ source_obligation(FactId, Members, Operator, Result) :-
230
233
  )
231
234
  ; Property == middleware
232
235
  -> Result = obligation(_{kind: "route_middleware", operator: OperatorString, members: Members})
236
+ ; Property == precedes
237
+ -> ( Operator == includes
238
+ -> Result = obligation(_{kind: "sequence_precedes", members: Members})
239
+ ; format(string(Reason),
240
+ "~w is a precedes fact with operator ~w; a source proof decides precedes only with includes (each member \"A -> B\" holds)",
241
+ [FactId, Operator]),
242
+ Result = incompatible(Reason)
243
+ )
233
244
  ; Result = obligation(_{kind: "set_relation", operator: OperatorString, members: Members})
234
245
  ).
235
246
 
@@ -350,6 +361,10 @@ obligation_key(Obligation, relation(Operator, Members)) :-
350
361
  dict_text(Obligation, operator, Operator),
351
362
  obligation_members(Obligation, Members),
352
363
  !.
364
+ obligation_key(Obligation, precedes(Members)) :-
365
+ dict_text(Obligation, kind, "sequence_precedes"),
366
+ obligation_members(Obligation, Members),
367
+ !.
353
368
  obligation_key(Obligation, middleware(Operator, Members)) :-
354
369
  dict_text(Obligation, kind, "route_middleware"),
355
370
  dict_text(Obligation, operator, Operator),
@@ -291,6 +291,10 @@ kind_problem(source, Certificate, Reason) :-
291
291
  ; get_dict(routes, Model, Routes),
292
292
  \+ ( is_list(Routes), maplist(source_route_ok, Routes) )
293
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"
294
298
  ).
295
299
 
296
300
  sorted_distinct_strings(List) :-
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),