kibi-core 0.27.0 → 0.29.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 +179 -0
- package/src/discovery.pl +9 -2
- package/src/model_check.pl +209 -6
- package/src/requirement_proof.pl +467 -19
- package/src/rule_registry.pl +10 -0
package/package.json
CHANGED
package/schema/entities.pl
CHANGED
|
@@ -47,6 +47,10 @@ entity_property(req, proof_exempt_reason, string).
|
|
|
47
47
|
% Only an approved exception makes a success scenario feasible.
|
|
48
48
|
entity_property(req, approved_by, string).
|
|
49
49
|
entity_property(req, approval_ref, string).
|
|
50
|
+
% Why a requirement has no failure side to test (its claims state a
|
|
51
|
+
% capability or output shape), and the human who accepted that.
|
|
52
|
+
entity_property(req, failure_side_waiver, string).
|
|
53
|
+
entity_property(req, failure_side_waiver_approved_by, string).
|
|
50
54
|
% Why the requirement exists, in the words of whoever stated the intent.
|
|
51
55
|
% Explanation only: it is never part of the checked meaning.
|
|
52
56
|
entity_property(req, rationale, string).
|
|
@@ -83,6 +87,12 @@ entity_property(fact, valid_to, datetime).
|
|
|
83
87
|
entity_property(fact, canonical_key, string).
|
|
84
88
|
entity_property(fact, claim_key, string).
|
|
85
89
|
entity_property(fact, claim_text, string).
|
|
90
|
+
% Why a quantifier word in claim_text ranges over nothing, so the claim
|
|
91
|
+
% needs no dimension, and the human who accepted that.
|
|
92
|
+
entity_property(fact, dimension_waiver, string).
|
|
93
|
+
entity_property(fact, dimension_waiver_approved_by, string).
|
|
94
|
+
entity_property(fact, case_waiver, string).
|
|
95
|
+
entity_property(fact, case_waiver_approved_by, string).
|
|
86
96
|
entity_property(fact, predicate_name, string).
|
|
87
97
|
entity_property(fact, predicate_namespace, string).
|
|
88
98
|
entity_property(fact, predicate_arity, integer).
|
|
@@ -149,6 +159,8 @@ optional_property(req, semantic_source_hash).
|
|
|
149
159
|
optional_property(req, semantic_inventory).
|
|
150
160
|
optional_property(req, proof_exempt).
|
|
151
161
|
optional_property(req, proof_exempt_reason).
|
|
162
|
+
optional_property(req, failure_side_waiver).
|
|
163
|
+
optional_property(req, failure_side_waiver_approved_by).
|
|
152
164
|
optional_property(req, approved_by).
|
|
153
165
|
optional_property(req, approval_ref).
|
|
154
166
|
optional_property(req, rationale).
|
package/src/cases.pl
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
% implements REQ-kibi-case-coverage
|
|
2
|
+
%
|
|
3
|
+
% Edge cases derived from a requirement's typed claims. A case is never
|
|
4
|
+
% authored: it is computed from a property fact the requirement grounds a
|
|
5
|
+
% claim on, so it cannot drift from the claim. Each case names a value
|
|
6
|
+
% class of the fact's subject and property (a boundary, a count of zero,
|
|
7
|
+
% one or many, a member of a set, a value outside it), the outcome the
|
|
8
|
+
% claim predicts for it, and a stable id hashed from the claim key, the
|
|
9
|
+
% generator and the constraint.
|
|
10
|
+
%
|
|
11
|
+
% A scenario covers a case when the requirement specifies it, its expects
|
|
12
|
+
% matches the predicted outcome (rejection and error both refuse), and one
|
|
13
|
+
% of its assumes facts on the same subject and property entails the case
|
|
14
|
+
% constraint. Cases are generated per fact, never across facts, in a
|
|
15
|
+
% deterministic order and capped per fact.
|
|
16
|
+
:- module(cases, [
|
|
17
|
+
requirement_cases/2,
|
|
18
|
+
fact_cases/3,
|
|
19
|
+
case_covering_scenarios/3,
|
|
20
|
+
case_dict/2,
|
|
21
|
+
fact_case_waiver/2,
|
|
22
|
+
case_limit/1
|
|
23
|
+
]).
|
|
24
|
+
|
|
25
|
+
:- use_module(library(crypto)).
|
|
26
|
+
:- use_module(library(lists)).
|
|
27
|
+
:- use_module(library(apply)).
|
|
28
|
+
:- use_module('kb.pl').
|
|
29
|
+
:- use_module('intervals.pl', [numeric_constraint_entailed/3]).
|
|
30
|
+
|
|
31
|
+
%% case_limit(-Limit)
|
|
32
|
+
% Most cases one fact yields. Beyond it the requirement reports
|
|
33
|
+
% caseLimitReached and the rest are left out, in their deterministic order.
|
|
34
|
+
case_limit(12).
|
|
35
|
+
|
|
36
|
+
% case(Id, ClaimKey, FactId, Subject-Property, Generator, Constraint, Predicted)
|
|
37
|
+
% Constraint is num(Type, Op, Value) on the property, member(Value),
|
|
38
|
+
% non_member(Set) or excluded(Value) for a set property.
|
|
39
|
+
% Predicted is success, rejection or unspecified.
|
|
40
|
+
|
|
41
|
+
%% requirement_cases(+ReqId, -Cases)
|
|
42
|
+
% Every case of every property fact a current requirement grounds a claim
|
|
43
|
+
% on with requires_property, sorted by claim key, fact and generation order.
|
|
44
|
+
requirement_cases(ReqId, Cases) :-
|
|
45
|
+
findall(FactId, kb_relationship(requires_property, ReqId, FactId), Facts0),
|
|
46
|
+
sort(Facts0, Facts),
|
|
47
|
+
findall(Case,
|
|
48
|
+
( member(FactId, Facts),
|
|
49
|
+
fact_cases(FactId, FactCases, _),
|
|
50
|
+
member(Case, FactCases)
|
|
51
|
+
),
|
|
52
|
+
Cases).
|
|
53
|
+
|
|
54
|
+
%% fact_cases(+FactId, -Cases, -LimitReached)
|
|
55
|
+
fact_cases(FactId, Cases, LimitReached) :-
|
|
56
|
+
kb:fact_property_tuple(FactId, Subject, Property, AuthoredOp, ValueType, Value, _Unit, _Scope, Polarity),
|
|
57
|
+
claim_key_of(FactId, ClaimKey),
|
|
58
|
+
effective_operator(Polarity, AuthoredOp, Op),
|
|
59
|
+
findall(Generator-Constraint-Predicted,
|
|
60
|
+
fact_case(ValueType, Op, Value, Generator, Constraint, Predicted),
|
|
61
|
+
Raw0),
|
|
62
|
+
distinct_constraints(Raw0, Raw),
|
|
63
|
+
case_limit(Limit),
|
|
64
|
+
length(Raw, Count),
|
|
65
|
+
( Count > Limit
|
|
66
|
+
-> length(Kept, Limit),
|
|
67
|
+
append(Kept, _, Raw),
|
|
68
|
+
LimitReached = true
|
|
69
|
+
; Kept = Raw,
|
|
70
|
+
LimitReached = false
|
|
71
|
+
),
|
|
72
|
+
findall(case(Id, ClaimKey, FactId, Subject-Property, Generator, Constraint, Predicted),
|
|
73
|
+
( member(Generator-Constraint-Predicted, Kept),
|
|
74
|
+
case_id(ClaimKey, Generator, Constraint, Id)
|
|
75
|
+
),
|
|
76
|
+
Cases).
|
|
77
|
+
|
|
78
|
+
claim_key_of(FactId, Key) :-
|
|
79
|
+
kb_entity(FactId, fact, Props),
|
|
80
|
+
( memberchk(claim_key=Raw, Props)
|
|
81
|
+
-> normalize_term_atom(Raw, Key)
|
|
82
|
+
; Key = FactId
|
|
83
|
+
).
|
|
84
|
+
|
|
85
|
+
effective_operator(require, Op, Op) :- !.
|
|
86
|
+
effective_operator(forbid, Op, Negated) :- negated_operator(Op, Negated), !.
|
|
87
|
+
effective_operator(_, Op, Op).
|
|
88
|
+
|
|
89
|
+
negated_operator(eq, neq).
|
|
90
|
+
negated_operator(neq, eq).
|
|
91
|
+
negated_operator(lt, gte).
|
|
92
|
+
negated_operator(gte, lt).
|
|
93
|
+
negated_operator(gt, lte).
|
|
94
|
+
negated_operator(lte, gt).
|
|
95
|
+
negated_operator(includes, excludes).
|
|
96
|
+
negated_operator(excludes, includes).
|
|
97
|
+
|
|
98
|
+
% Two generators can name the same value class (eq 0 is both the boundary
|
|
99
|
+
% and the zero count), or one class can sit inside another with the same
|
|
100
|
+
% outcome (eq 0 and eq 1 inside lt 8): the first one in generation order
|
|
101
|
+
% keeps it, so each case is a class a scenario must reach on its own.
|
|
102
|
+
distinct_constraints(Raw, Distinct) :-
|
|
103
|
+
foldl(keep_new_case, Raw, [], Reversed),
|
|
104
|
+
reverse(Reversed, Distinct).
|
|
105
|
+
|
|
106
|
+
keep_new_case(_-C-P, Kept, Kept) :-
|
|
107
|
+
member(_-Earlier-P, Kept),
|
|
108
|
+
subsumed_constraint(C, Earlier),
|
|
109
|
+
!.
|
|
110
|
+
keep_new_case(Case, Kept, [Case|Kept]).
|
|
111
|
+
|
|
112
|
+
subsumed_constraint(C, C) :- !.
|
|
113
|
+
subsumed_constraint(num(Type, Op, Value), num(Type, EarlierOp, EarlierValue)) :-
|
|
114
|
+
integer_variables(Type, Var, IntVars),
|
|
115
|
+
numeric_constraint_entailed([c(Op, Var, Value)], c(EarlierOp, Var, EarlierValue), IntVars).
|
|
116
|
+
|
|
117
|
+
%% fact_case(+ValueType, +Op, +Value, -Generator, -Constraint, -Predicted)
|
|
118
|
+
% Numeric facts: the bound itself and the nearest value past it, then the
|
|
119
|
+
% zero, one and many classes when the property is an integer count.
|
|
120
|
+
fact_case(Type, Op, Value, Generator, num(Type, CaseOp, CaseValue), Predicted) :-
|
|
121
|
+
numeric_type(Type),
|
|
122
|
+
number(Value),
|
|
123
|
+
numeric_case(Type, Op, Value, Generator, CaseOp, CaseValue),
|
|
124
|
+
numeric_prediction(Type, Op, Value, CaseOp, CaseValue, Predicted).
|
|
125
|
+
% Set facts: each member, then the value class outside the set.
|
|
126
|
+
fact_case(set, Op, Members, member, member(Member), success) :-
|
|
127
|
+
memberchk(Op, [eq, includes, subset_of]),
|
|
128
|
+
is_list(Members),
|
|
129
|
+
member(Member, Members).
|
|
130
|
+
fact_case(set, Op, Members, non_member, non_member(Members), Predicted) :-
|
|
131
|
+
is_list(Members),
|
|
132
|
+
non_member_prediction(Op, Predicted).
|
|
133
|
+
fact_case(set, excludes, Members, excluded_member, excluded(Member), rejection) :-
|
|
134
|
+
is_list(Members),
|
|
135
|
+
member(Member, Members).
|
|
136
|
+
|
|
137
|
+
upper_past(eq, gt).
|
|
138
|
+
upper_past(neq, gt).
|
|
139
|
+
upper_past(lte, gt).
|
|
140
|
+
upper_past(lt, gte).
|
|
141
|
+
lower_past(eq, lt).
|
|
142
|
+
lower_past(neq, lt).
|
|
143
|
+
lower_past(gte, lt).
|
|
144
|
+
lower_past(gt, lte).
|
|
145
|
+
|
|
146
|
+
numeric_type(int).
|
|
147
|
+
numeric_type(number).
|
|
148
|
+
|
|
149
|
+
non_member_prediction(eq, rejection).
|
|
150
|
+
non_member_prediction(subset_of, rejection).
|
|
151
|
+
non_member_prediction(includes, unspecified).
|
|
152
|
+
|
|
153
|
+
% The boundary rows of x Op V: the bound itself, and each open side past
|
|
154
|
+
% it (x > V for an upper bound, x < V for a lower one), so any value that
|
|
155
|
+
% breaks the bound lands in the past-the-bound class.
|
|
156
|
+
numeric_case(_, Op, V, boundary, eq, V) :-
|
|
157
|
+
memberchk(Op, [eq, neq, lte, gte]).
|
|
158
|
+
numeric_case(int, lt, V, boundary, eq, Inside) :- Inside is V - 1.
|
|
159
|
+
numeric_case(int, gt, V, boundary, eq, Inside) :- Inside is V + 1.
|
|
160
|
+
numeric_case(_, Op, V, boundary, PastOp, V) :-
|
|
161
|
+
upper_past(Op, PastOp).
|
|
162
|
+
numeric_case(Type, Op, V, boundary, PastOp, V) :-
|
|
163
|
+
lower_past(Op, PastOp),
|
|
164
|
+
\+ (Type == int, PastOp == lt, V =< 0),
|
|
165
|
+
\+ (Type == int, PastOp == lte, V < 0).
|
|
166
|
+
% Zero, one and many: the partition a one-item test hides. It applies to
|
|
167
|
+
% an integer property bounded at one or more; a bound of zero is already
|
|
168
|
+
% split by its boundary rows.
|
|
169
|
+
numeric_case(int, _, V, cardinality, eq, 0) :- V >= 1.
|
|
170
|
+
numeric_case(int, _, V, cardinality, eq, 1) :- V >= 1.
|
|
171
|
+
numeric_case(int, _, V, cardinality, gte, 2) :- V >= 1.
|
|
172
|
+
|
|
173
|
+
% A case is predicted success when every value it admits satisfies the
|
|
174
|
+
% claim and rejection when none does. A class that straddles the bound
|
|
175
|
+
% (x >= 2 against x <= 5) is not a case: its values on each side are the
|
|
176
|
+
% boundary rows already.
|
|
177
|
+
numeric_prediction(Type, Op, Value, CaseOp, CaseValue, Predicted) :-
|
|
178
|
+
integer_variables(Type, Var, IntVars),
|
|
179
|
+
( numeric_constraint_entailed([c(CaseOp, Var, CaseValue)], c(Op, Var, Value), IntVars)
|
|
180
|
+
-> Predicted = success
|
|
181
|
+
; negated_operator(Op, Negated),
|
|
182
|
+
numeric_constraint_entailed([c(CaseOp, Var, CaseValue)], c(Negated, Var, Value), IntVars)
|
|
183
|
+
-> Predicted = rejection
|
|
184
|
+
).
|
|
185
|
+
|
|
186
|
+
integer_variables(int, Var, [Var]) :- !.
|
|
187
|
+
integer_variables(_, _, []).
|
|
188
|
+
|
|
189
|
+
case_id(ClaimKey, Generator, Constraint, Id) :-
|
|
190
|
+
format(string(Text), '~w|~w|~q', [ClaimKey, Generator, Constraint]),
|
|
191
|
+
crypto_data_hash(Text, Hash, [algorithm(sha256), encoding(utf8)]),
|
|
192
|
+
sub_atom(Hash, 0, 12, _, Prefix),
|
|
193
|
+
upcase_atom(Prefix, Upper),
|
|
194
|
+
atom_concat('CASE-', Upper, Id).
|
|
195
|
+
|
|
196
|
+
%% case_covering_scenarios(+ScenarioIds, +Case, -Covering)
|
|
197
|
+
% The requirement's scenarios, other than drafts, whose expects matches the
|
|
198
|
+
% case's predicted outcome and whose assumptions on the case's subject and
|
|
199
|
+
% property entail the case constraint.
|
|
200
|
+
case_covering_scenarios(ScenarioIds, case(_, _, _, Key, _, Constraint, Predicted), Covering) :-
|
|
201
|
+
findall(ScenarioId,
|
|
202
|
+
( member(ScenarioId, ScenarioIds),
|
|
203
|
+
\+ draft_scenario(ScenarioId),
|
|
204
|
+
scenario_expects(ScenarioId, Expects),
|
|
205
|
+
outcome_matches(Predicted, Expects),
|
|
206
|
+
kb_relationship(assumes, ScenarioId, AssumedId),
|
|
207
|
+
assumption_entails(AssumedId, Key, Constraint)
|
|
208
|
+
),
|
|
209
|
+
Covering0),
|
|
210
|
+
sort(Covering0, Covering).
|
|
211
|
+
|
|
212
|
+
draft_scenario(ScenarioId) :-
|
|
213
|
+
kb_entity(ScenarioId, scenario, Props),
|
|
214
|
+
memberchk(status=Raw, Props),
|
|
215
|
+
normalize_term_atom(Raw, draft).
|
|
216
|
+
|
|
217
|
+
scenario_expects(ScenarioId, Expects) :-
|
|
218
|
+
kb_entity(ScenarioId, scenario, Props),
|
|
219
|
+
( memberchk(expects=Raw, Props)
|
|
220
|
+
-> normalize_term_atom(Raw, Expects)
|
|
221
|
+
; Expects = success
|
|
222
|
+
).
|
|
223
|
+
|
|
224
|
+
outcome_matches(success, success).
|
|
225
|
+
outcome_matches(rejection, rejection).
|
|
226
|
+
outcome_matches(rejection, error).
|
|
227
|
+
|
|
228
|
+
assumption_entails(FactId, Subject-Property, Constraint) :-
|
|
229
|
+
kb:fact_property_tuple(FactId, Subject, Property, AuthoredOp, ValueType, Value, _, _, Polarity),
|
|
230
|
+
effective_operator(Polarity, AuthoredOp, Op),
|
|
231
|
+
constraint_entailed(ValueType, Op, Value, Constraint).
|
|
232
|
+
|
|
233
|
+
constraint_entailed(Type, Op, Value, num(_, CaseOp, CaseValue)) :-
|
|
234
|
+
numeric_type(Type),
|
|
235
|
+
number(Value),
|
|
236
|
+
integer_variables(Type, Var, IntVars),
|
|
237
|
+
numeric_constraint_entailed([c(Op, Var, Value)], c(CaseOp, Var, CaseValue), IntVars).
|
|
238
|
+
constraint_entailed(set, Op, Members, member(Member)) :-
|
|
239
|
+
memberchk(Op, [eq, includes]),
|
|
240
|
+
memberchk(Member, Members).
|
|
241
|
+
constraint_entailed(set, Op, Members, non_member(Allowed)) :-
|
|
242
|
+
memberchk(Op, [eq, includes]),
|
|
243
|
+
member(Member, Members),
|
|
244
|
+
\+ memberchk(Member, Allowed),
|
|
245
|
+
!.
|
|
246
|
+
constraint_entailed(set, Op, Members, excluded(Member)) :-
|
|
247
|
+
memberchk(Op, [eq, includes]),
|
|
248
|
+
memberchk(Member, Members).
|
|
249
|
+
|
|
250
|
+
%% case_dict(+Case, -Dict)
|
|
251
|
+
% The case as reported, with the assumption a scenario would state to land
|
|
252
|
+
% in it and the expects it would declare.
|
|
253
|
+
case_dict(case(Id, ClaimKey, FactId, Subject-Property, Generator, Constraint, Predicted), Dict) :-
|
|
254
|
+
constraint_text(Constraint, Text),
|
|
255
|
+
constraint_assumption(Subject, Property, Constraint, Assumption),
|
|
256
|
+
Dict = _{
|
|
257
|
+
caseId: Id,
|
|
258
|
+
claimKey: ClaimKey,
|
|
259
|
+
factId: FactId,
|
|
260
|
+
subjectKey: Subject,
|
|
261
|
+
propertyKey: Property,
|
|
262
|
+
generator: Generator,
|
|
263
|
+
constraint: Text,
|
|
264
|
+
predicted: Predicted,
|
|
265
|
+
assumption: Assumption
|
|
266
|
+
}.
|
|
267
|
+
|
|
268
|
+
constraint_assumption(Subject, Property, num(Type, Op, Value), Assumption) :-
|
|
269
|
+
Assumption = _{fact_kind: property_value, subject_key: Subject, property_key: Property,
|
|
270
|
+
operator: Op, value_type: Type, value: Value}.
|
|
271
|
+
constraint_assumption(Subject, Property, member(Member), Assumption) :-
|
|
272
|
+
Assumption = _{fact_kind: property_value, subject_key: Subject, property_key: Property,
|
|
273
|
+
operator: includes, value_type: set, value: [Member]}.
|
|
274
|
+
constraint_assumption(Subject, Property, non_member(_), Assumption) :-
|
|
275
|
+
Assumption = _{fact_kind: property_value, subject_key: Subject, property_key: Property,
|
|
276
|
+
operator: includes, value_type: set, value: ['<a value outside the set>']}.
|
|
277
|
+
constraint_assumption(Subject, Property, excluded(Member), Assumption) :-
|
|
278
|
+
Assumption = _{fact_kind: property_value, subject_key: Subject, property_key: Property,
|
|
279
|
+
operator: includes, value_type: set, value: [Member]}.
|
|
280
|
+
|
|
281
|
+
%% fact_case_waiver(+FactId, -Waiver)
|
|
282
|
+
% A grounding fact whose cases a person accepted as covered some other way
|
|
283
|
+
% (a unit suite that walks every member, a value class the system cannot
|
|
284
|
+
% produce) records why in case_waiver and who accepted it in
|
|
285
|
+
% case_waiver_approved_by. Only an approved waiver counts. Waiver is _{}
|
|
286
|
+
% when none is recorded.
|
|
287
|
+
fact_case_waiver(FactId, Waiver) :-
|
|
288
|
+
( kb_entity(FactId, fact, Props),
|
|
289
|
+
memberchk(case_waiver=RawReason, Props),
|
|
290
|
+
normalize_term_atom(RawReason, Reason),
|
|
291
|
+
Reason \== ''
|
|
292
|
+
-> atom_string(Reason, ReasonText),
|
|
293
|
+
( memberchk(case_waiver_approved_by=RawBy, Props),
|
|
294
|
+
normalize_term_atom(RawBy, By),
|
|
295
|
+
By \== ''
|
|
296
|
+
-> atom_string(By, ByText),
|
|
297
|
+
Waiver = _{reason: ReasonText, approvedBy: ByText}
|
|
298
|
+
; Waiver = _{reason: ReasonText}
|
|
299
|
+
)
|
|
300
|
+
; Waiver = _{}
|
|
301
|
+
).
|
|
302
|
+
|
|
303
|
+
constraint_text(num(_, Op, Value), Text) :-
|
|
304
|
+
format(string(Text), '~w ~w', [Op, Value]).
|
|
305
|
+
constraint_text(member(Member), Text) :-
|
|
306
|
+
format(string(Text), 'includes ~w', [Member]).
|
|
307
|
+
constraint_text(non_member(Allowed), Text) :-
|
|
308
|
+
atomic_list_concat(Allowed, ', ', Joined),
|
|
309
|
+
format(string(Text), 'includes a value outside [~w]', [Joined]).
|
|
310
|
+
constraint_text(excluded(Member), Text) :-
|
|
311
|
+
format(string(Text), 'includes ~w', [Member]).
|
package/src/checks.pl
CHANGED
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
check_adr_proposed/1,
|
|
27
27
|
check_scenario_feasibility/1,
|
|
28
28
|
check_scenario_feasibility_unknown/1,
|
|
29
|
+
check_scenario_outcome_mismatch/1,
|
|
30
|
+
negative_scenario_witness/3,
|
|
31
|
+
opposing_assumption/2,
|
|
29
32
|
check_exception_claim_keys/1,
|
|
30
33
|
check_numeric_string_values/1,
|
|
31
34
|
check_rule_key_arguments_missing/1,
|
|
@@ -111,6 +114,7 @@ check_all(ViolationsDict) :-
|
|
|
111
114
|
check_adr_proposed(AdrProposed),
|
|
112
115
|
check_scenario_feasibility(ScenarioFeasibility),
|
|
113
116
|
check_scenario_feasibility_unknown(ScenarioFeasibilityUnknown),
|
|
117
|
+
check_scenario_outcome_mismatch(ScenarioOutcomeMismatch),
|
|
114
118
|
check_exception_claim_keys(ExceptionClaimKeys),
|
|
115
119
|
check_numeric_string_values(NumericStringValues),
|
|
116
120
|
check_domain_contradictions(Contradictions),
|
|
@@ -148,6 +152,7 @@ check_all(ViolationsDict) :-
|
|
|
148
152
|
adr_proposed: AdrProposed,
|
|
149
153
|
scenario_feasibility: ScenarioFeasibility,
|
|
150
154
|
scenario_feasibility_unknown: ScenarioFeasibilityUnknown,
|
|
155
|
+
scenario_outcome_mismatch: ScenarioOutcomeMismatch,
|
|
151
156
|
exception_claim_keys: ExceptionClaimKeys,
|
|
152
157
|
numeric_string_value: NumericStringValues,
|
|
153
158
|
domain_contradictions: Contradictions,
|
|
@@ -2132,6 +2137,176 @@ bool_domain_exhausted(bool, Different) :-
|
|
|
2132
2137
|
memberchk(true, Different),
|
|
2133
2138
|
memberchk(false, Different).
|
|
2134
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
|
+
|
|
2135
2310
|
%% check_scenario_feasibility_unknown(-Violations)
|
|
2136
2311
|
% implements REQ-kibi-scenario-feasibility-v2
|
|
2137
2312
|
% Advisory: success scenarios whose feasibility the checker cannot decide.
|
|
@@ -3754,6 +3929,7 @@ check_selected_dispatch(Rules, _{
|
|
|
3754
3929
|
adr_proposed: AdrProposed,
|
|
3755
3930
|
scenario_feasibility: ScenarioFeasibility,
|
|
3756
3931
|
scenario_feasibility_unknown: ScenarioFeasibilityUnknown,
|
|
3932
|
+
scenario_outcome_mismatch: ScenarioOutcomeMismatch,
|
|
3757
3933
|
exception_claim_keys: ExceptionClaimKeys,
|
|
3758
3934
|
numeric_string_value: NumericStringValues,
|
|
3759
3935
|
domain_contradictions: Contradictions,
|
|
@@ -3791,6 +3967,7 @@ check_selected_dispatch(Rules, _{
|
|
|
3791
3967
|
selected_rule(Rules, 'adr-proposed', check_adr_proposed, AdrProposed),
|
|
3792
3968
|
selected_rule(Rules, 'scenario-feasibility', check_scenario_feasibility, ScenarioFeasibility),
|
|
3793
3969
|
selected_rule(Rules, 'scenario-feasibility-unknown', check_scenario_feasibility_unknown, ScenarioFeasibilityUnknown),
|
|
3970
|
+
selected_rule(Rules, 'scenario-outcome-mismatch', check_scenario_outcome_mismatch, ScenarioOutcomeMismatch),
|
|
3794
3971
|
selected_rule(Rules, 'exception-claim-keys', check_exception_claim_keys, ExceptionClaimKeys),
|
|
3795
3972
|
selected_rule(Rules, 'numeric-string-value', check_numeric_string_values, NumericStringValues),
|
|
3796
3973
|
selected_rule(Rules, 'domain-contradictions', check_domain_contradictions, Contradictions),
|
|
@@ -3925,6 +4102,7 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
|
|
|
3925
4102
|
check_adr_proposed(AdrProposed),
|
|
3926
4103
|
check_scenario_feasibility(ScenarioFeasibility),
|
|
3927
4104
|
check_scenario_feasibility_unknown(ScenarioFeasibilityUnknown),
|
|
4105
|
+
check_scenario_outcome_mismatch(ScenarioOutcomeMismatch),
|
|
3928
4106
|
check_exception_claim_keys(ExceptionClaimKeys),
|
|
3929
4107
|
check_numeric_string_values(NumericStringValues),
|
|
3930
4108
|
check_domain_contradictions(Contradictions),
|
|
@@ -3961,6 +4139,7 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
|
|
|
3961
4139
|
adr_proposed: AdrProposed,
|
|
3962
4140
|
scenario_feasibility: ScenarioFeasibility,
|
|
3963
4141
|
scenario_feasibility_unknown: ScenarioFeasibilityUnknown,
|
|
4142
|
+
scenario_outcome_mismatch: ScenarioOutcomeMismatch,
|
|
3964
4143
|
exception_claim_keys: ExceptionClaimKeys,
|
|
3965
4144
|
numeric_string_value: NumericStringValues,
|
|
3966
4145
|
domain_contradictions: Contradictions,
|
package/src/discovery.pl
CHANGED
|
@@ -417,7 +417,7 @@ evidence_floor_json(BindingMode, TestBindings, VerificationSnapshot, CheckedAt,
|
|
|
417
417
|
( member(Id-Proof, Evaluated),
|
|
418
418
|
Floor = Proof.evidenceFloor,
|
|
419
419
|
Floor.status \== met,
|
|
420
|
-
|
|
420
|
+
Row0 = _{
|
|
421
421
|
id: Id,
|
|
422
422
|
evidenceLevel: Proof.evidenceLevel,
|
|
423
423
|
requiredEvidence: Floor.required,
|
|
@@ -427,7 +427,14 @@ evidence_floor_json(BindingMode, TestBindings, VerificationSnapshot, CheckedAt,
|
|
|
427
427
|
proofGaps: Proof.proofGaps,
|
|
428
428
|
gapDetails: Details
|
|
429
429
|
},
|
|
430
|
-
evidence_floor_gap_details(Proof, Details)
|
|
430
|
+
evidence_floor_gap_details(Proof, Details),
|
|
431
|
+
% implements REQ-kibi-claim-bound-proofs
|
|
432
|
+
% A claim its proof refuted: the test and what it read.
|
|
433
|
+
( get_dict(claimBindingRejections, Proof, Refutations),
|
|
434
|
+
Refutations \== []
|
|
435
|
+
-> Row = Row0.put(refutations, Refutations)
|
|
436
|
+
; Row = Row0
|
|
437
|
+
)
|
|
431
438
|
),
|
|
432
439
|
Rows0),
|
|
433
440
|
sort(id, @<, Rows0, Rows),
|