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 +1 -1
- package/src/checks.pl +178 -17
- package/src/claim_obligations.pl +16 -1
- package/src/code_certificates.pl +4 -0
- package/src/discovery.pl +65 -23
- package/src/intervals.pl +35 -14
- package/src/kb.pl +288 -36
- package/src/kb_consistency.pl +15 -0
- package/src/logic_ir.pl +130 -161
- package/src/model_check.pl +54 -6
- package/src/requirement_proof.pl +91 -10
- package/src/status.pl +16 -0
- package/src/units.pl +1 -3
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2706
|
-
|
|
2791
|
+
rule_requirement_links(Links),
|
|
2792
|
+
member(ReqA-RulesA, Links),
|
|
2793
|
+
member(ReqB-RulesB, Links),
|
|
2707
2794
|
ReqA \== ReqB,
|
|
2708
|
-
|
|
2709
|
-
|
|
2795
|
+
member(FactA-RuleA, RulesA),
|
|
2796
|
+
member(FactB-RuleB, RulesB),
|
|
2710
2797
|
FactA \== FactB,
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
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
|
-
|
|
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) :-
|
package/src/claim_obligations.pl
CHANGED
|
@@ -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),
|
package/src/code_certificates.pl
CHANGED
|
@@ -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
|
|
159
|
-
%
|
|
160
|
-
%
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
553
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
apply_integer_bound(eq-Value, ibox(Lo, Hi, Ex),
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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),
|