kibi-core 0.26.1 → 0.28.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/schema/entities.pl +12 -0
- package/src/cases.pl +311 -0
- package/src/checks.pl +357 -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 +535 -14
- package/src/rule_registry.pl +6 -0
- package/src/status.pl +16 -0
- package/src/units.pl +1 -3
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
|
|
@@ -22,6 +26,9 @@
|
|
|
22
26
|
check_adr_proposed/1,
|
|
23
27
|
check_scenario_feasibility/1,
|
|
24
28
|
check_scenario_feasibility_unknown/1,
|
|
29
|
+
check_scenario_outcome_mismatch/1,
|
|
30
|
+
negative_scenario_witness/3,
|
|
31
|
+
opposing_assumption/2,
|
|
25
32
|
check_exception_claim_keys/1,
|
|
26
33
|
check_numeric_string_values/1,
|
|
27
34
|
check_rule_key_arguments_missing/1,
|
|
@@ -107,6 +114,7 @@ check_all(ViolationsDict) :-
|
|
|
107
114
|
check_adr_proposed(AdrProposed),
|
|
108
115
|
check_scenario_feasibility(ScenarioFeasibility),
|
|
109
116
|
check_scenario_feasibility_unknown(ScenarioFeasibilityUnknown),
|
|
117
|
+
check_scenario_outcome_mismatch(ScenarioOutcomeMismatch),
|
|
110
118
|
check_exception_claim_keys(ExceptionClaimKeys),
|
|
111
119
|
check_numeric_string_values(NumericStringValues),
|
|
112
120
|
check_domain_contradictions(Contradictions),
|
|
@@ -144,6 +152,7 @@ check_all(ViolationsDict) :-
|
|
|
144
152
|
adr_proposed: AdrProposed,
|
|
145
153
|
scenario_feasibility: ScenarioFeasibility,
|
|
146
154
|
scenario_feasibility_unknown: ScenarioFeasibilityUnknown,
|
|
155
|
+
scenario_outcome_mismatch: ScenarioOutcomeMismatch,
|
|
147
156
|
exception_claim_keys: ExceptionClaimKeys,
|
|
148
157
|
numeric_string_value: NumericStringValues,
|
|
149
158
|
domain_contradictions: Contradictions,
|
|
@@ -470,7 +479,7 @@ parse_json_object(Dict, Dict) :-
|
|
|
470
479
|
!.
|
|
471
480
|
parse_json_object(Raw, Dict) :-
|
|
472
481
|
(atom(Raw) ; string(Raw)),
|
|
473
|
-
|
|
482
|
+
json_text_term(Raw, Dict),
|
|
474
483
|
is_dict(Dict).
|
|
475
484
|
|
|
476
485
|
parse_json_list(Raw, Entries) :-
|
|
@@ -482,9 +491,48 @@ parse_json_list(^^(Value, _), Entries) :-
|
|
|
482
491
|
parse_json_list(Value, Entries).
|
|
483
492
|
parse_json_list(Raw, Entries) :-
|
|
484
493
|
(atom(Raw) ; string(Raw)),
|
|
485
|
-
|
|
494
|
+
json_text_term(Raw, Entries),
|
|
486
495
|
is_list(Entries).
|
|
487
496
|
|
|
497
|
+
% The same JSON text (a rule body, a claim inventory, a receipt history) is
|
|
498
|
+
% parsed by several rules and by every requirement that links the fact, so
|
|
499
|
+
% the parsed term is kept by its text. Parsing is a pure function of the
|
|
500
|
+
% text, so the memo never goes stale; it is bounded so a long-lived engine
|
|
501
|
+
% cannot grow it without limit.
|
|
502
|
+
:- dynamic json_text_memo/2.
|
|
503
|
+
:- dynamic json_text_memo_size/1.
|
|
504
|
+
|
|
505
|
+
% mutalog-disable-next-line literal: the bound is a tuning value; any size gives the same answers
|
|
506
|
+
json_text_memo_limit(16384).
|
|
507
|
+
|
|
508
|
+
json_text_term(Raw, Term) :-
|
|
509
|
+
atom_string(Text, Raw),
|
|
510
|
+
( json_text_memo(Text, Stored)
|
|
511
|
+
-> Stored = parsed(Term)
|
|
512
|
+
; ( catch(atom_json_dict(Text, Parsed, [value_string_as(string)]), _, fail)
|
|
513
|
+
-> Stored = parsed(Parsed)
|
|
514
|
+
; Stored = invalid
|
|
515
|
+
),
|
|
516
|
+
json_text_remember(Text, Stored),
|
|
517
|
+
Stored = parsed(Term)
|
|
518
|
+
).
|
|
519
|
+
|
|
520
|
+
json_text_remember(Text, Stored) :-
|
|
521
|
+
( json_text_memo_size(Size)
|
|
522
|
+
-> true
|
|
523
|
+
; Size = 0
|
|
524
|
+
),
|
|
525
|
+
json_text_memo_limit(Limit),
|
|
526
|
+
( Size >= Limit
|
|
527
|
+
-> retractall(json_text_memo(_, _)),
|
|
528
|
+
retractall(json_text_memo_size(_)),
|
|
529
|
+
Next = 1
|
|
530
|
+
; retractall(json_text_memo_size(_)),
|
|
531
|
+
Next is Size + 1
|
|
532
|
+
),
|
|
533
|
+
assertz(json_text_memo(Text, Stored)),
|
|
534
|
+
assertz(json_text_memo_size(Next)).
|
|
535
|
+
|
|
488
536
|
%% check_no_dangling_refs(-Violations)
|
|
489
537
|
% Finds all relationships referencing non-existent entities.
|
|
490
538
|
check_no_dangling_refs(Violations) :-
|
|
@@ -1281,7 +1329,28 @@ analysis_outcome(analysis([], [], false, []), feasible).
|
|
|
1281
1329
|
% advisory reasons in reporting order; ByException is true when some check is
|
|
1282
1330
|
% satisfiable only because an approved exception waived a constraint;
|
|
1283
1331
|
% NotApplicable is non-empty only when no assumption is governed at all.
|
|
1284
|
-
|
|
1332
|
+
% The analysis of a scenario is kept per KB generation: the blocking rule,
|
|
1333
|
+
% the unknown-feasibility rule and every requirement's proof ladder ask for
|
|
1334
|
+
% the same scenarios. Inside a transaction (what-if staging) the KB is not
|
|
1335
|
+
% the generation's, so the analysis is computed afresh.
|
|
1336
|
+
scenario_feasibility_analysis(ScenarioId, Analysis) :-
|
|
1337
|
+
( nonvar(ScenarioId),
|
|
1338
|
+
\+ rdf_db:rdf_active_transaction(_),
|
|
1339
|
+
kb:index_verification_key(scenario_feasibility, Key)
|
|
1340
|
+
-> ( kb:kb_scenario_feasibility_memo(ScenarioId, Key, Analysis0)
|
|
1341
|
+
-> Analysis = Analysis0
|
|
1342
|
+
; compute_scenario_feasibility_analysis(ScenarioId, Analysis),
|
|
1343
|
+
( once(kb:kb_scenario_feasibility_memo(_, Stored, _)),
|
|
1344
|
+
Stored \== Key
|
|
1345
|
+
-> retractall(kb:kb_scenario_feasibility_memo(_, _, _))
|
|
1346
|
+
; true
|
|
1347
|
+
),
|
|
1348
|
+
assertz(kb:kb_scenario_feasibility_memo(ScenarioId, Key, Analysis))
|
|
1349
|
+
)
|
|
1350
|
+
; compute_scenario_feasibility_analysis(ScenarioId, Analysis)
|
|
1351
|
+
).
|
|
1352
|
+
|
|
1353
|
+
compute_scenario_feasibility_analysis(ScenarioId, analysis(Witnesses, Unknowns, ByException, NotApplicable)) :-
|
|
1285
1354
|
findall(FactId, kb_relationship(assumes, ScenarioId, FactId), Assumed0),
|
|
1286
1355
|
sort(Assumed0, Assumed),
|
|
1287
1356
|
( Assumed == []
|
|
@@ -2068,6 +2137,176 @@ bool_domain_exhausted(bool, Different) :-
|
|
|
2068
2137
|
memberchk(true, Different),
|
|
2069
2138
|
memberchk(false, Different).
|
|
2070
2139
|
|
|
2140
|
+
%% negative_scenario_witness(?ScenarioId, ?ReqId, -Witness)
|
|
2141
|
+
% implements REQ-kibi-happy-path-gap
|
|
2142
|
+
% A scenario that expects rejection or error is a targeted negative for a
|
|
2143
|
+
% requirement when its assumptions break that requirement: the feasibility
|
|
2144
|
+
% analysis finds a witness naming it, or (below) an assumption opposes one of
|
|
2145
|
+
% the requirement's claim facts directly. A rejection scenario whose assumptions
|
|
2146
|
+
% the requirements permit, or that an approved exception waives, exercises no
|
|
2147
|
+
% requirement's failure side.
|
|
2148
|
+
negative_scenario_witness(ScenarioId, ReqId,
|
|
2149
|
+
infeasibility(ReqIds, AssumedFacts, ReqFacts, Reason)) :-
|
|
2150
|
+
negative_scenario(ScenarioId),
|
|
2151
|
+
scenario_feasibility_analysis(ScenarioId, analysis(Witnesses, _, _, _)),
|
|
2152
|
+
member(infeasibility(ReqIds, AssumedFacts, ReqFacts, Reason), Witnesses),
|
|
2153
|
+
member(ReqId, ReqIds).
|
|
2154
|
+
% A scenario the requirement specifies may also target one of its claims
|
|
2155
|
+
% directly: it assumes a fact that opposes the claim's fact (a conflicting
|
|
2156
|
+
% property value or polarity, the opposite polarity of the same predicate),
|
|
2157
|
+
% or, for a rule over predicates, a fact instantiating one of the rule's
|
|
2158
|
+
% atoms, so the scenario is about the situation the rule governs.
|
|
2159
|
+
negative_scenario_witness(ScenarioId, ReqId, opposed(AssumedFact, ReqFact)) :-
|
|
2160
|
+
negative_scenario(ScenarioId),
|
|
2161
|
+
kb_relationship(specified_by, ReqId, ScenarioId),
|
|
2162
|
+
kb:current_req(ReqId),
|
|
2163
|
+
kb_relationship(assumes, ScenarioId, AssumedFact),
|
|
2164
|
+
member(Link, [requires_property, requires_predicate, requires_rule]),
|
|
2165
|
+
kb_relationship(Link, ReqId, ReqFact),
|
|
2166
|
+
facts_oppose(AssumedFact, ReqFact).
|
|
2167
|
+
|
|
2168
|
+
facts_oppose(AssumedFact, ReqFact) :-
|
|
2169
|
+
kb:fact_property_tuple(AssumedFact, Subject, Property, OpA, TypeA, ValueA, UnitA, ScopeA, PolarityA),
|
|
2170
|
+
kb:fact_property_tuple(ReqFact, Subject, Property, OpB, TypeB, ValueB, UnitB, ScopeB, PolarityB),
|
|
2171
|
+
!,
|
|
2172
|
+
kb:scope_intersects(ScopeA, ScopeB),
|
|
2173
|
+
( kb:polarity_conflict(Subject, Property, OpA, TypeA, ValueA, UnitA, ScopeA, PolarityA,
|
|
2174
|
+
OpB, TypeB, ValueB, UnitB, ScopeB, PolarityB, _)
|
|
2175
|
+
-> true
|
|
2176
|
+
; PolarityA == PolarityB,
|
|
2177
|
+
kb:property_conflict(Subject, Property, OpA, TypeA, ValueA, UnitA, PolarityA,
|
|
2178
|
+
OpB, TypeB, ValueB, UnitB, PolarityB, _)
|
|
2179
|
+
-> true
|
|
2180
|
+
).
|
|
2181
|
+
facts_oppose(AssumedFact, ReqFact) :-
|
|
2182
|
+
kb:predicate_fact(AssumedFact, Namespace, Name, Args, PolarityA),
|
|
2183
|
+
kb:predicate_fact(ReqFact, Namespace, Name, Args, PolarityB),
|
|
2184
|
+
!,
|
|
2185
|
+
PolarityA \== PolarityB.
|
|
2186
|
+
facts_oppose(AssumedFact, ReqFact) :-
|
|
2187
|
+
kb:predicate_fact(AssumedFact, Namespace, Name, _, _),
|
|
2188
|
+
stored_rule_fact(ReqFact, _, Rule),
|
|
2189
|
+
sub_term(atom(Namespace, Name, _, _, _), Rule),
|
|
2190
|
+
!.
|
|
2191
|
+
|
|
2192
|
+
%% opposing_assumption(+FactId, -Assumption)
|
|
2193
|
+
% implements REQ-kibi-happy-path-gap
|
|
2194
|
+
% A fact a negative scenario can assume to oppose the claim fact FactId, as
|
|
2195
|
+
% the fields of a new fact: the boundary value just past a numeric bound, the
|
|
2196
|
+
% other boolean, a different string, a set missing a required member or
|
|
2197
|
+
% holding an unlisted or excluded one, the opposite polarity of a predicate,
|
|
2198
|
+
% or, for a rule over predicates, its head instantiated with the variable
|
|
2199
|
+
% names. Fails when no such fact can be derived (a matches pattern).
|
|
2200
|
+
opposing_assumption(FactId, Assumption) :-
|
|
2201
|
+
kb:fact_property_tuple(FactId, Subject, Property, Op, Type, Value, Unit, Scope, Polarity),
|
|
2202
|
+
!,
|
|
2203
|
+
( Polarity == forbid
|
|
2204
|
+
-> Op1 = Op, Value1 = Value
|
|
2205
|
+
; opposing_value(Type, Op, Value, Op1, Value1)
|
|
2206
|
+
),
|
|
2207
|
+
Base = _{fact_kind: property_value, subject_key: Subject, property_key: Property,
|
|
2208
|
+
operator: Op1, value_type: Type, value: Value1},
|
|
2209
|
+
( Unit == '' -> Assumption0 = Base ; Assumption0 = Base.put(unit, Unit) ),
|
|
2210
|
+
( Scope == '' -> Assumption = Assumption0 ; Assumption = Assumption0.put(scope, Scope) ).
|
|
2211
|
+
opposing_assumption(FactId, _{fact_kind: predicate, predicate_namespace: Namespace,
|
|
2212
|
+
predicate_name: Name, predicate_args: Args, polarity: Opposite}) :-
|
|
2213
|
+
kb:predicate_fact(FactId, Namespace, Name, Args, Polarity),
|
|
2214
|
+
!,
|
|
2215
|
+
kb:opposite_predicate_polarity(Polarity, Opposite).
|
|
2216
|
+
opposing_assumption(FactId, _{fact_kind: predicate, predicate_namespace: Namespace,
|
|
2217
|
+
predicate_name: Name, predicate_args: Args, polarity: assert}) :-
|
|
2218
|
+
stored_rule_fact(FactId, _, rule(_, _, Head, Body, _, _, _, _, _, _)),
|
|
2219
|
+
( Head = atom(Namespace, Name, Terms, _, _)
|
|
2220
|
+
-> true
|
|
2221
|
+
; sub_term(atom(Namespace, Name, Terms, _, _), Body)
|
|
2222
|
+
),
|
|
2223
|
+
!,
|
|
2224
|
+
maplist(term_constant, Terms, Args).
|
|
2225
|
+
|
|
2226
|
+
term_constant(Term, Constant) :-
|
|
2227
|
+
( Term = var(Name, _)
|
|
2228
|
+
-> downcase_atom(Name, Constant)
|
|
2229
|
+
; Term = const(Value, _)
|
|
2230
|
+
-> term_to_atom(Value, Constant)
|
|
2231
|
+
; term_to_atom(Term, Constant)
|
|
2232
|
+
).
|
|
2233
|
+
|
|
2234
|
+
opposing_value(bool, eq, Value, eq, Opposite) :-
|
|
2235
|
+
!,
|
|
2236
|
+
( memberchk(Value, [true, "true", 'True']) -> Opposite = false ; Opposite = true ).
|
|
2237
|
+
opposing_value(Type, Op, Value, Op1, Value1) :-
|
|
2238
|
+
kb:is_numeric_type(Type),
|
|
2239
|
+
number(Value),
|
|
2240
|
+
!,
|
|
2241
|
+
opposing_number(Type, Op, Value, Op1, Value1).
|
|
2242
|
+
opposing_value(set, Op, Members, Op1, Value1) :-
|
|
2243
|
+
!,
|
|
2244
|
+
opposing_set(Op, Members, Op1, Value1).
|
|
2245
|
+
opposing_value(_, eq, Value, neq, Value) :- !.
|
|
2246
|
+
opposing_value(_, neq, Value, eq, Value).
|
|
2247
|
+
|
|
2248
|
+
opposing_number(_, gt, Bound, eq, Bound).
|
|
2249
|
+
opposing_number(_, lt, Bound, eq, Bound).
|
|
2250
|
+
opposing_number(int, gte, Bound, eq, Value) :- !, Value is Bound - 1.
|
|
2251
|
+
opposing_number(_, gte, Bound, lt, Bound).
|
|
2252
|
+
opposing_number(int, lte, Bound, eq, Value) :- !, Value is Bound + 1.
|
|
2253
|
+
opposing_number(_, lte, Bound, gt, Bound).
|
|
2254
|
+
opposing_number(_, eq, Value, neq, Value).
|
|
2255
|
+
opposing_number(_, neq, Value, eq, Value).
|
|
2256
|
+
|
|
2257
|
+
opposing_set(eq, Members, eq, Fewer) :- members_but_last(Members, Fewer).
|
|
2258
|
+
opposing_set(includes, Members, eq, Fewer) :- members_but_last(Members, Fewer).
|
|
2259
|
+
opposing_set(subset_of, _, includes, [unlisted_member]).
|
|
2260
|
+
opposing_set(excludes, [First|_], includes, [First]).
|
|
2261
|
+
|
|
2262
|
+
members_but_last(Members, Fewer) :-
|
|
2263
|
+
( append(Fewer, [_], Members) -> true ; Fewer = [] ).
|
|
2264
|
+
|
|
2265
|
+
negative_scenario(ScenarioId) :-
|
|
2266
|
+
( var(ScenarioId)
|
|
2267
|
+
-> findall(Id,
|
|
2268
|
+
(scenario_expects(Id, Outcome), memberchk(Outcome, [rejection, error])),
|
|
2269
|
+
Ids0),
|
|
2270
|
+
sort(Ids0, Ids),
|
|
2271
|
+
member(ScenarioId, Ids)
|
|
2272
|
+
; scenario_expects(ScenarioId, Outcome),
|
|
2273
|
+
memberchk(Outcome, [rejection, error])
|
|
2274
|
+
).
|
|
2275
|
+
|
|
2276
|
+
%% check_scenario_outcome_mismatch(-Violations)
|
|
2277
|
+
% implements REQ-kibi-happy-path-gap
|
|
2278
|
+
% A scenario that expects rejection or error but whose assumptions current
|
|
2279
|
+
% requirements govern and permit together aims at no failure: the knowledge
|
|
2280
|
+
% base predicts success for it. Only a decided feasible outcome is reported;
|
|
2281
|
+
% a scenario that assumes nothing, or whose assumptions no requirement
|
|
2282
|
+
% governs, is left alone.
|
|
2283
|
+
check_scenario_outcome_mismatch(Violations) :-
|
|
2284
|
+
findall(Violation, scenario_outcome_mismatch_violation(Violation), Unsorted),
|
|
2285
|
+
sort(Unsorted, Violations).
|
|
2286
|
+
|
|
2287
|
+
scenario_outcome_mismatch_violation(violation(
|
|
2288
|
+
'scenario-outcome-mismatch',
|
|
2289
|
+
ScenarioId,
|
|
2290
|
+
Description,
|
|
2291
|
+
"Assume a value a current requirement forbids, so the scenario exercises that requirement's failure side, or set expects: success when the scenario describes a permitted situation",
|
|
2292
|
+
Source
|
|
2293
|
+
)) :-
|
|
2294
|
+
negative_scenario(ScenarioId),
|
|
2295
|
+
scenario_expects(ScenarioId, Expected),
|
|
2296
|
+
scenario_feasibility_analysis(ScenarioId, Analysis),
|
|
2297
|
+
analysis_outcome(Analysis, feasible),
|
|
2298
|
+
findall(FactId, kb_relationship(assumes, ScenarioId, FactId), FactIds0),
|
|
2299
|
+
sort(FactIds0, FactIds),
|
|
2300
|
+
atomic_list_concat(FactIds, ', ', FactText),
|
|
2301
|
+
format(string(Description),
|
|
2302
|
+
"Scenario expects ~w, but current requirements permit everything it assumes (~w), so the knowledge base predicts success",
|
|
2303
|
+
[Expected, FactText]),
|
|
2304
|
+
( kb_entity(ScenarioId, scenario, Props),
|
|
2305
|
+
memberchk(source=Source, Props)
|
|
2306
|
+
-> true
|
|
2307
|
+
; Source = ""
|
|
2308
|
+
).
|
|
2309
|
+
|
|
2071
2310
|
%% check_scenario_feasibility_unknown(-Violations)
|
|
2072
2311
|
% implements REQ-kibi-scenario-feasibility-v2
|
|
2073
2312
|
% Advisory: success scenarios whose feasibility the checker cannot decide.
|
|
@@ -2526,7 +2765,24 @@ check_domain_contradictions_and_witnesses(Violations, Witnesses) :-
|
|
|
2526
2765
|
% Exact, JSON-safe evidence for every strict-property, ground-predicate, and
|
|
2527
2766
|
% safe-rule contradiction result. Rule overlap that cannot be proven or
|
|
2528
2767
|
% disproven remains status=unresolved rather than becoming consistency.
|
|
2768
|
+
%
|
|
2769
|
+
% kb_check, kb_coverage and the proof context each ask for every witness, so
|
|
2770
|
+
% the result is memoized per KB generation, like the entity property memo.
|
|
2771
|
+
% Inside an RDF transaction (the what-if staging) it is always recomputed.
|
|
2529
2772
|
check_domain_contradiction_witnesses(Witnesses) :-
|
|
2773
|
+
( \+ rdf_db:rdf_active_transaction(_),
|
|
2774
|
+
kb:index_verification_key(domain_contradictions, Key)
|
|
2775
|
+
-> ( kb:kb_contradiction_witness_memo(Key, Cached)
|
|
2776
|
+
-> Witnesses = Cached
|
|
2777
|
+
; compute_domain_contradiction_witnesses(Computed),
|
|
2778
|
+
retractall(kb:kb_contradiction_witness_memo(_, _)),
|
|
2779
|
+
assertz(kb:kb_contradiction_witness_memo(Key, Computed)),
|
|
2780
|
+
Witnesses = Computed
|
|
2781
|
+
)
|
|
2782
|
+
; compute_domain_contradiction_witnesses(Witnesses)
|
|
2783
|
+
).
|
|
2784
|
+
|
|
2785
|
+
compute_domain_contradiction_witnesses(Witnesses) :-
|
|
2530
2786
|
findall(Witness, req_conflict_witness(_ReqA, _ReqB, Witness), GroundWitnesses),
|
|
2531
2787
|
functional_predicate_declarations(Functional),
|
|
2532
2788
|
findall(Witness, rule_contradiction_witness(Functional, Witness), RuleWitnesses),
|
|
@@ -2701,22 +2957,42 @@ opposing_rule_requirements(ReqA, ReqB, RuleA, RuleB) :-
|
|
|
2701
2957
|
% Each unordered pair of (requirement, rule fact) endpoints is compared once,
|
|
2702
2958
|
% ordered by the requirement-fact pair: ordering requirements and facts
|
|
2703
2959
|
% independently would never compare REQ-A->FACT-Z with REQ-Z->FACT-A.
|
|
2960
|
+
%
|
|
2961
|
+
% Only current requirements with requires_rule links can pair, so the pairs
|
|
2962
|
+
% are drawn from that list (in current_req/1 order, each with its rule links
|
|
2963
|
+
% in relationship order) rather than from every pair of current
|
|
2964
|
+
% requirements; solutions and their order are unchanged.
|
|
2704
2965
|
opposing_rule_requirement_facts(ReqA, ReqB, FactA, FactB, RuleA, RuleB) :-
|
|
2705
|
-
|
|
2706
|
-
|
|
2966
|
+
rule_requirement_links(Links),
|
|
2967
|
+
member(ReqA-RulesA, Links),
|
|
2968
|
+
member(ReqB-RulesB, Links),
|
|
2707
2969
|
ReqA \== ReqB,
|
|
2708
|
-
|
|
2709
|
-
|
|
2970
|
+
member(FactA-RuleA, RulesA),
|
|
2971
|
+
member(FactB-RuleB, RulesB),
|
|
2710
2972
|
FactA \== FactB,
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2973
|
+
% mutalog-disable-next-line compare: equal endpoints need ReqA == ReqB, which is excluded above
|
|
2974
|
+
ReqA-FactA @< ReqB-FactB.
|
|
2975
|
+
|
|
2976
|
+
% Each requirement's rule facts with their parsed rules, read once rather
|
|
2977
|
+
% than once per pair: a link to a fact that is not a rule, or whose rule does
|
|
2978
|
+
% not parse, never pairs.
|
|
2979
|
+
rule_requirement_links(Links) :-
|
|
2980
|
+
findall(ReqId-Rules,
|
|
2981
|
+
( kb:current_req(ReqId),
|
|
2982
|
+
findall(FactId-Rule,
|
|
2983
|
+
( kb_relationship(requires_rule, ReqId, FactId),
|
|
2984
|
+
rule_fact_rule(FactId, Rule) ),
|
|
2985
|
+
Rules),
|
|
2986
|
+
% mutalog-disable-next-line goal_true,literal: a requirement without rules never pairs; this only prunes
|
|
2987
|
+
Rules \== [] ),
|
|
2988
|
+
Links).
|
|
2989
|
+
|
|
2990
|
+
rule_fact_rule(FactId, Rule) :-
|
|
2991
|
+
kb_entity(FactId, fact, Props),
|
|
2992
|
+
memberchk(fact_kind=Kind, Props),
|
|
2993
|
+
% mutalog-disable-next-line goal_true: only a rule fact carries a rule body to decode
|
|
2994
|
+
normalize_term_atom(Kind, rule),
|
|
2995
|
+
logic_rule_from_props(Props, Rule).
|
|
2720
2996
|
|
|
2721
2997
|
rule_contradiction_witness(Functional, Witness) :-
|
|
2722
2998
|
opposing_rule_requirement_facts(ReqA, ReqB, FactA, FactB, RuleA, RuleB),
|
|
@@ -3472,7 +3748,7 @@ inventory_entries(Raw, Entries) :-
|
|
|
3472
3748
|
( is_list(Raw) -> Entries = Raw
|
|
3473
3749
|
; Raw = ^^(Value, _) -> inventory_entries(Value, Entries)
|
|
3474
3750
|
; (atom(Raw) ; string(Raw)),
|
|
3475
|
-
|
|
3751
|
+
json_text_term(Raw, JsonEntries),
|
|
3476
3752
|
is_list(JsonEntries)
|
|
3477
3753
|
-> Entries = JsonEntries
|
|
3478
3754
|
; (atom(Raw) ; string(Raw)),
|
|
@@ -3653,6 +3929,7 @@ check_selected_dispatch(Rules, _{
|
|
|
3653
3929
|
adr_proposed: AdrProposed,
|
|
3654
3930
|
scenario_feasibility: ScenarioFeasibility,
|
|
3655
3931
|
scenario_feasibility_unknown: ScenarioFeasibilityUnknown,
|
|
3932
|
+
scenario_outcome_mismatch: ScenarioOutcomeMismatch,
|
|
3656
3933
|
exception_claim_keys: ExceptionClaimKeys,
|
|
3657
3934
|
numeric_string_value: NumericStringValues,
|
|
3658
3935
|
domain_contradictions: Contradictions,
|
|
@@ -3690,6 +3967,7 @@ check_selected_dispatch(Rules, _{
|
|
|
3690
3967
|
selected_rule(Rules, 'adr-proposed', check_adr_proposed, AdrProposed),
|
|
3691
3968
|
selected_rule(Rules, 'scenario-feasibility', check_scenario_feasibility, ScenarioFeasibility),
|
|
3692
3969
|
selected_rule(Rules, 'scenario-feasibility-unknown', check_scenario_feasibility_unknown, ScenarioFeasibilityUnknown),
|
|
3970
|
+
selected_rule(Rules, 'scenario-outcome-mismatch', check_scenario_outcome_mismatch, ScenarioOutcomeMismatch),
|
|
3693
3971
|
selected_rule(Rules, 'exception-claim-keys', check_exception_claim_keys, ExceptionClaimKeys),
|
|
3694
3972
|
selected_rule(Rules, 'numeric-string-value', check_numeric_string_values, NumericStringValues),
|
|
3695
3973
|
selected_rule(Rules, 'domain-contradictions', check_domain_contradictions, Contradictions),
|
|
@@ -3748,6 +4026,66 @@ check_all_json_with_options(JsonString, RequireAdr) :-
|
|
|
3748
4026
|
JsonString
|
|
3749
4027
|
).
|
|
3750
4028
|
|
|
4029
|
+
%% check_all_json_file(+Path, +RequireAdr)
|
|
4030
|
+
% Writes the JSON check_all_json_with_options/2 would answer to Path.
|
|
4031
|
+
% The answer itself carries no violations, so a large KB's report is never
|
|
4032
|
+
% bounded by the engine's output capacity; the caller reads the file.
|
|
4033
|
+
check_all_json_file(Path, RequireAdr) :-
|
|
4034
|
+
check_all_with_options(ViolationsDict, RequireAdr),
|
|
4035
|
+
violations_json_to_file(ViolationsDict, Path).
|
|
4036
|
+
|
|
4037
|
+
%% check_selected_json_file(+Rules, +Path)
|
|
4038
|
+
% File-transport form of check_selected_json/2.
|
|
4039
|
+
check_selected_json_file(Rules, Path) :-
|
|
4040
|
+
check_selected(Rules, ViolationsDict),
|
|
4041
|
+
violations_json_to_file(ViolationsDict, Path).
|
|
4042
|
+
|
|
4043
|
+
%% check_all_json_transport(+Path, +RequireAdr, -Transport, -JsonString)
|
|
4044
|
+
% Writes the violations JSON to Path (Transport = file, JsonString = ''), or
|
|
4045
|
+
% answers it as JsonString (Transport = answer) when Path is `none` or the
|
|
4046
|
+
% file cannot be written: a full or read-only temp folder, or an engine that
|
|
4047
|
+
% cannot reach the caller's temp folder, must not fail a check that fits in
|
|
4048
|
+
% an answer.
|
|
4049
|
+
check_all_json_transport(Path, RequireAdr, Transport, JsonString) :-
|
|
4050
|
+
check_all_with_options(ViolationsDict, RequireAdr),
|
|
4051
|
+
violations_json_transport(ViolationsDict, Path, Transport, JsonString).
|
|
4052
|
+
|
|
4053
|
+
%% check_selected_json_transport(+Rules, +Path, -Transport, -JsonString)
|
|
4054
|
+
% check_all_json_transport/4 for the named rules only.
|
|
4055
|
+
check_selected_json_transport(Rules, Path, Transport, JsonString) :-
|
|
4056
|
+
check_selected(Rules, ViolationsDict),
|
|
4057
|
+
violations_json_transport(ViolationsDict, Path, Transport, JsonString).
|
|
4058
|
+
|
|
4059
|
+
violations_json_transport(ViolationsDict, Path, Transport, JsonString) :-
|
|
4060
|
+
( Path \== none,
|
|
4061
|
+
catch(violations_json_to_file(ViolationsDict, Path), Error,
|
|
4062
|
+
( file_transport_error(Error)
|
|
4063
|
+
-> fail
|
|
4064
|
+
; throw(Error)
|
|
4065
|
+
))
|
|
4066
|
+
-> Transport = file,
|
|
4067
|
+
JsonString = ''
|
|
4068
|
+
; Transport = answer,
|
|
4069
|
+
violations_dict_to_json(ViolationsDict, JsonDict),
|
|
4070
|
+
with_output_to_string(
|
|
4071
|
+
json_write_dict(current_output, JsonDict, [width(0)]),
|
|
4072
|
+
JsonString
|
|
4073
|
+
)
|
|
4074
|
+
).
|
|
4075
|
+
|
|
4076
|
+
file_transport_error(error(permission_error(_, _, _), _)).
|
|
4077
|
+
file_transport_error(error(existence_error(_, _), _)).
|
|
4078
|
+
file_transport_error(error(resource_error(_), _)).
|
|
4079
|
+
file_transport_error(error(io_error(_, _), _)).
|
|
4080
|
+
|
|
4081
|
+
violations_json_to_file(ViolationsDict, Path) :-
|
|
4082
|
+
violations_dict_to_json(ViolationsDict, JsonDict),
|
|
4083
|
+
setup_call_cleanup(
|
|
4084
|
+
open(Path, write, Stream, [encoding(utf8)]),
|
|
4085
|
+
json_write_dict(Stream, JsonDict, [width(0)]),
|
|
4086
|
+
close(Stream)
|
|
4087
|
+
).
|
|
4088
|
+
|
|
3751
4089
|
%% check_all_with_options(-ViolationsDict, +RequireAdr)
|
|
3752
4090
|
% Returns a dict with all violations, respecting options.
|
|
3753
4091
|
check_all_with_options(ViolationsDict, RequireAdr) :-
|
|
@@ -3764,6 +4102,7 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
|
|
|
3764
4102
|
check_adr_proposed(AdrProposed),
|
|
3765
4103
|
check_scenario_feasibility(ScenarioFeasibility),
|
|
3766
4104
|
check_scenario_feasibility_unknown(ScenarioFeasibilityUnknown),
|
|
4105
|
+
check_scenario_outcome_mismatch(ScenarioOutcomeMismatch),
|
|
3767
4106
|
check_exception_claim_keys(ExceptionClaimKeys),
|
|
3768
4107
|
check_numeric_string_values(NumericStringValues),
|
|
3769
4108
|
check_domain_contradictions(Contradictions),
|
|
@@ -3800,6 +4139,7 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
|
|
|
3800
4139
|
adr_proposed: AdrProposed,
|
|
3801
4140
|
scenario_feasibility: ScenarioFeasibility,
|
|
3802
4141
|
scenario_feasibility_unknown: ScenarioFeasibilityUnknown,
|
|
4142
|
+
scenario_outcome_mismatch: ScenarioOutcomeMismatch,
|
|
3803
4143
|
exception_claim_keys: ExceptionClaimKeys,
|
|
3804
4144
|
numeric_string_value: NumericStringValues,
|
|
3805
4145
|
domain_contradictions: Contradictions,
|
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) :-
|