kibi-core 0.27.0 → 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 +179 -0
- package/src/requirement_proof.pl +444 -4
- package/src/rule_registry.pl +6 -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/requirement_proof.pl
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
:- use_module('checks.pl', [
|
|
21
21
|
check_domain_contradictions_and_witnesses/2,
|
|
22
22
|
infeasible_scenario/5,
|
|
23
|
+
negative_scenario_witness/3,
|
|
24
|
+
opposing_assumption/2,
|
|
23
25
|
scenario_feasibility_outcome/2,
|
|
24
26
|
check_rule_safety/1,
|
|
25
27
|
check_rule_verifiability/1
|
|
@@ -28,6 +30,7 @@
|
|
|
28
30
|
:- use_module('code_certificates.pl', [code_certificate_check/2, code_certificate_digest/2]).
|
|
29
31
|
:- use_module('claim_obligations.pl', [claim_obligation/3, obligation_equal/2]).
|
|
30
32
|
:- use_module('model_check.pl', [model_check_obligation/4]).
|
|
33
|
+
:- use_module('cases.pl', [fact_cases/3, case_covering_scenarios/3, case_dict/2, fact_case_waiver/2]).
|
|
31
34
|
|
|
32
35
|
proof_version('kibi.requirement-proof.v5').
|
|
33
36
|
|
|
@@ -155,12 +158,18 @@ compute_requirement_proof(ReqId, ReqProps, Context, Proof) :-
|
|
|
155
158
|
PassingE2eStage.receiptEvidence, Context,
|
|
156
159
|
ProductionStage, ProductionSymbols),
|
|
157
160
|
source_coordinate_stage(ReqProps, ExecutableSymbols, ProductionSymbols, CoordinateStage),
|
|
161
|
+
negative_path_stage(ReqId, ScenarioStage, ScenarioTestStage, Context, NegativePathStage),
|
|
162
|
+
dimension_stage(ReqId, ScenarioTestStage, Context, DimensionStage),
|
|
163
|
+
cases_stage(ReqId, ScenarioStage, ScenarioTestStage, Context, CasesStage),
|
|
158
164
|
Stages = _{
|
|
159
165
|
semanticInventory: SemanticStage,
|
|
160
166
|
logicGrounding: LogicStage,
|
|
161
167
|
contradictions: ContradictionStage,
|
|
162
168
|
scenarios: ScenarioStage,
|
|
163
169
|
scenarioTests: ScenarioTestStage,
|
|
170
|
+
negativePath: NegativePathStage,
|
|
171
|
+
dimension: DimensionStage,
|
|
172
|
+
cases: CasesStage,
|
|
164
173
|
passingE2e: PassingE2eStage,
|
|
165
174
|
executableSymbols: ExecutableStage,
|
|
166
175
|
productionSymbols: ProductionStage,
|
|
@@ -688,6 +697,12 @@ scenario_stage(ReqId, Stage, ScenarioIds) :-
|
|
|
688
697
|
unknown_feasibility_reason(Reason, ReasonText)
|
|
689
698
|
),
|
|
690
699
|
UnknownFeasibility),
|
|
700
|
+
% implements REQ-kibi-happy-path-gap
|
|
701
|
+
% The linked scenarios that exercise this requirement's failure side: they
|
|
702
|
+
% expect rejection or error and assume a value it forbids.
|
|
703
|
+
findall(ScenarioId,
|
|
704
|
+
(member(ScenarioId, ScenarioIds), once(negative_scenario_witness(ScenarioId, ReqId, _))),
|
|
705
|
+
NegativeScenarios),
|
|
691
706
|
( (ScenarioIds = [] ; InvalidScenarioTargets \= [])
|
|
692
707
|
-> Status = missing
|
|
693
708
|
; InfeasibleScenarios \= []
|
|
@@ -702,6 +717,7 @@ scenario_stage(ReqId, Stage, ScenarioIds) :-
|
|
|
702
717
|
invalidScenarioTargets: InvalidScenarioTargets,
|
|
703
718
|
infeasibleScenarios: InfeasibleScenarios,
|
|
704
719
|
unknownFeasibility: UnknownFeasibility,
|
|
720
|
+
negativeScenarios: NegativeScenarios,
|
|
705
721
|
sources: Sources
|
|
706
722
|
}.
|
|
707
723
|
|
|
@@ -716,6 +732,376 @@ unknown_feasibility_reason(undetermined_validity(_), undetermined_validity).
|
|
|
716
732
|
existing_scenario(ScenarioId) :-
|
|
717
733
|
kb_entity(ScenarioId, scenario, _).
|
|
718
734
|
|
|
735
|
+
%% negative_path_stage(+ReqId, +ScenarioStage, +ScenarioTestStage, +Context, -Stage)
|
|
736
|
+
% implements REQ-kibi-happy-path-gap
|
|
737
|
+
% A requirement is not tested while only its happy path is: when it grounds
|
|
738
|
+
% a claim (a property value, predicate or rule), at least one linked
|
|
739
|
+
% scenario must expect rejection or error and assume a value or situation
|
|
740
|
+
% the requirement forbids. Claims a fresh certificate proves at code scope
|
|
741
|
+
% decide their failure side already and need none. Status is not_required
|
|
742
|
+
% (no such claim, or all certified), passed, waived (an approved
|
|
743
|
+
% failure_side_waiver) or missing.
|
|
744
|
+
negative_path_stage(ReqId, ScenarioStage, ScenarioTestStage, Context, Stage) :-
|
|
745
|
+
findall(Key-FactId,
|
|
746
|
+
( negative_path_link(Link),
|
|
747
|
+
kb_relationship(Link, ReqId, FactId),
|
|
748
|
+
fact_claim_key_or_id(FactId, Key)
|
|
749
|
+
),
|
|
750
|
+
ClaimFacts0),
|
|
751
|
+
sort(ClaimFacts0, ClaimFacts),
|
|
752
|
+
certified_claim_keys(ReqId, ScenarioTestStage, Context, Certified),
|
|
753
|
+
exclude(certified_claim(Certified), ClaimFacts, OpenFacts),
|
|
754
|
+
pairs_keys(OpenFacts, Open0),
|
|
755
|
+
sort(Open0, Open),
|
|
756
|
+
pending_certified_claim_keys(ScenarioTestStage, Context, Open, Pending),
|
|
757
|
+
( get_dict(negativeScenarios, ScenarioStage, Negatives) -> true ; Negatives = [] ),
|
|
758
|
+
failure_side_waiver(ReqId, Waiver),
|
|
759
|
+
( Open == []
|
|
760
|
+
-> Status = not_required
|
|
761
|
+
; Negatives \== []
|
|
762
|
+
-> Status = passed
|
|
763
|
+
; get_dict(approvedBy, Waiver, _)
|
|
764
|
+
-> Status = waived
|
|
765
|
+
; Status = missing
|
|
766
|
+
),
|
|
767
|
+
( Status == missing
|
|
768
|
+
-> findall(_{claimKey: Key, factId: FactId, assumption: Assumption, expects: rejection},
|
|
769
|
+
(member(Key-FactId, OpenFacts), once(opposing_assumption(FactId, Assumption))),
|
|
770
|
+
Suggestions)
|
|
771
|
+
; Suggestions = []
|
|
772
|
+
),
|
|
773
|
+
Stage = _{
|
|
774
|
+
status: Status,
|
|
775
|
+
claims: Open,
|
|
776
|
+
certifiedClaims: Certified,
|
|
777
|
+
negativeScenarios: Negatives,
|
|
778
|
+
suggestedNegatives: Suggestions,
|
|
779
|
+
pendingReproofClaims: Pending,
|
|
780
|
+
waiver: Waiver
|
|
781
|
+
}.
|
|
782
|
+
|
|
783
|
+
% Open claims a certificate test proves (its contract lists them in proves)
|
|
784
|
+
% whose receipt is only older than the code: re-running kibi prove certifies
|
|
785
|
+
% them, so the evidence floor treats them like any other stale receipt.
|
|
786
|
+
pending_certified_claim_keys(ScenarioTestStage, Context, Open, Keys) :-
|
|
787
|
+
( get_dict(tests, ScenarioTestStage, Tests) -> true ; Tests = [] ),
|
|
788
|
+
findall(Key,
|
|
789
|
+
( member(TestId, Tests),
|
|
790
|
+
test_receipt_evidence(Context, [unit, integration, end_to_end], TestId, Evidence),
|
|
791
|
+
get_dict(state, Evidence, stale),
|
|
792
|
+
kb_entity(TestId, test, Props),
|
|
793
|
+
memberchk(proof_contract=RawContract, Props),
|
|
794
|
+
proof_contract_dict(RawContract, Contract),
|
|
795
|
+
get_dict(proves, Contract, Proves),
|
|
796
|
+
is_list(Proves),
|
|
797
|
+
member(RawKey, Proves),
|
|
798
|
+
normalize_atom(RawKey, Key),
|
|
799
|
+
memberchk(Key, Open)
|
|
800
|
+
),
|
|
801
|
+
Keys0),
|
|
802
|
+
sort(Keys0, Keys).
|
|
803
|
+
|
|
804
|
+
%% failure_side_waiver(+ReqId, -Waiver)
|
|
805
|
+
% A requirement whose claims state a capability or an output shape has no
|
|
806
|
+
% input to refuse, so its failure side can be waived: failure_side_waiver
|
|
807
|
+
% gives the reason and failure_side_waiver_approved_by names the human who
|
|
808
|
+
% accepted it. Only an approved waiver satisfies the stage; a reason alone
|
|
809
|
+
% is reported as pending approval. Waiver is _{} when none is recorded.
|
|
810
|
+
failure_side_waiver(ReqId, Waiver) :-
|
|
811
|
+
( kb_entity(ReqId, req, Props),
|
|
812
|
+
memberchk(failure_side_waiver=RawReason, Props),
|
|
813
|
+
normalize_atom(RawReason, Reason),
|
|
814
|
+
Reason \== ''
|
|
815
|
+
-> atom_string(Reason, ReasonText),
|
|
816
|
+
( memberchk(failure_side_waiver_approved_by=RawBy, Props),
|
|
817
|
+
normalize_atom(RawBy, By),
|
|
818
|
+
By \== ''
|
|
819
|
+
-> atom_string(By, ByText),
|
|
820
|
+
Waiver = _{reason: ReasonText, approvedBy: ByText}
|
|
821
|
+
; Waiver = _{reason: ReasonText}
|
|
822
|
+
)
|
|
823
|
+
; Waiver = _{}
|
|
824
|
+
).
|
|
825
|
+
|
|
826
|
+
certified_claim(Certified, Key-_) :- memberchk(Key, Certified).
|
|
827
|
+
|
|
828
|
+
%% cases_stage(+ReqId, +ScenarioStage, +ScenarioTestStage, +Context, -Stage)
|
|
829
|
+
% implements REQ-kibi-case-coverage
|
|
830
|
+
% Edge cases derived from the requirement's typed property claims
|
|
831
|
+
% (cases.pl): the bound and the value past it, zero, one and many for a
|
|
832
|
+
% count, each member of a set and a value outside it. A case is covered
|
|
833
|
+
% when a linked scenario, other than a draft, assumes a value in its class
|
|
834
|
+
% and expects the outcome the claim predicts. Every case whose predicted
|
|
835
|
+
% outcome is rejection must be covered: that is where a test of the one
|
|
836
|
+
% case that works hides the case that breaks. Uncovered success cases and
|
|
837
|
+
% cases no claim decides (unspecified) are reported, not blocking. Claims
|
|
838
|
+
% a fresh certificate proves at code scope are decided already. A grounding
|
|
839
|
+
% fact can carry a case_waiver with case_waiver_approved_by. Status is
|
|
840
|
+
% not_required (no rejection case), passed, waived (every uncovered
|
|
841
|
+
% rejection case is on a fact with an approved waiver) or missing.
|
|
842
|
+
cases_stage(ReqId, ScenarioStage, ScenarioTestStage, Context, Stage) :-
|
|
843
|
+
findall(FactId, kb_relationship(requires_property, ReqId, FactId), Facts0),
|
|
844
|
+
sort(Facts0, Facts),
|
|
845
|
+
certified_claim_keys(ReqId, ScenarioTestStage, Context, Certified),
|
|
846
|
+
( get_dict(scenarios, ScenarioStage, ScenarioIds) -> true ; ScenarioIds = [] ),
|
|
847
|
+
foldl(fact_case_rows(Certified, ScenarioIds), Facts, rows([], false), rows(Rows0, LimitReached)),
|
|
848
|
+
reverse(Rows0, Rows),
|
|
849
|
+
include(case_row_blocking, Rows, Blocking),
|
|
850
|
+
partition(case_row_waived, Blocking, WaivedRows, OpenRows),
|
|
851
|
+
findall(Key, (member(Row, OpenRows), get_dict(claimKey, Row, Key)), OpenKeys0),
|
|
852
|
+
sort(OpenKeys0, OpenKeys),
|
|
853
|
+
pending_certified_claim_keys(ScenarioTestStage, Context, OpenKeys, Pending),
|
|
854
|
+
case_summary(Rows, Summary),
|
|
855
|
+
( \+ (member(Row, Rows), get_dict(predicted, Row, rejection))
|
|
856
|
+
-> Status = not_required
|
|
857
|
+
; OpenRows == [],
|
|
858
|
+
WaivedRows == []
|
|
859
|
+
-> Status = passed
|
|
860
|
+
; OpenRows == []
|
|
861
|
+
-> Status = waived
|
|
862
|
+
; Status = missing
|
|
863
|
+
),
|
|
864
|
+
Stage = _{
|
|
865
|
+
status: Status,
|
|
866
|
+
cases: Rows,
|
|
867
|
+
uncoveredCases: OpenRows,
|
|
868
|
+
waivedCases: WaivedRows,
|
|
869
|
+
summary: Summary,
|
|
870
|
+
limitReached: LimitReached,
|
|
871
|
+
pendingReproofClaims: Pending
|
|
872
|
+
}.
|
|
873
|
+
|
|
874
|
+
fact_case_rows(Certified, ScenarioIds, FactId, rows(Acc0, Limit0), rows(Acc, Limit)) :-
|
|
875
|
+
( fact_cases(FactId, Cases, FactLimit),
|
|
876
|
+
Cases = [case(_, ClaimKey, _, _, _, _, _)|_],
|
|
877
|
+
\+ memberchk(ClaimKey, Certified)
|
|
878
|
+
-> fact_case_waiver(FactId, Waiver),
|
|
879
|
+
foldl(case_row(ScenarioIds, Waiver), Cases, Acc0, Acc),
|
|
880
|
+
( FactLimit == true -> Limit = true ; Limit = Limit0 )
|
|
881
|
+
; Acc = Acc0,
|
|
882
|
+
Limit = Limit0
|
|
883
|
+
).
|
|
884
|
+
|
|
885
|
+
case_row(ScenarioIds, Waiver, Case, Acc, [Row|Acc]) :-
|
|
886
|
+
case_dict(Case, Dict),
|
|
887
|
+
case_covering_scenarios(ScenarioIds, Case, Covering),
|
|
888
|
+
( Covering \== []
|
|
889
|
+
-> Coverage = covered
|
|
890
|
+
; get_dict(predicted, Dict, unspecified)
|
|
891
|
+
-> Coverage = unspecified
|
|
892
|
+
; get_dict(approvedBy, Waiver, _)
|
|
893
|
+
-> Coverage = waived
|
|
894
|
+
; Coverage = uncovered
|
|
895
|
+
),
|
|
896
|
+
Row0 = Dict.put(_{coverage: Coverage, scenarios: Covering}),
|
|
897
|
+
( Waiver == _{} -> Row = Row0 ; Row = Row0.put(waiver, Waiver) ).
|
|
898
|
+
|
|
899
|
+
% A rejection case blocks until a scenario covers it or its fact's waiver
|
|
900
|
+
% is approved.
|
|
901
|
+
case_row_blocking(Row) :-
|
|
902
|
+
get_dict(predicted, Row, rejection),
|
|
903
|
+
get_dict(coverage, Row, Coverage),
|
|
904
|
+
memberchk(Coverage, [uncovered, waived]).
|
|
905
|
+
|
|
906
|
+
case_row_waived(Row) :- get_dict(coverage, Row, waived).
|
|
907
|
+
|
|
908
|
+
case_summary(Rows, _{generated: Generated, covered: Covered, uncovered: Uncovered,
|
|
909
|
+
unspecified: Unspecified, waived: Waived}) :-
|
|
910
|
+
length(Rows, Generated),
|
|
911
|
+
coverage_count(Rows, covered, Covered),
|
|
912
|
+
coverage_count(Rows, uncovered, Uncovered),
|
|
913
|
+
coverage_count(Rows, unspecified, Unspecified),
|
|
914
|
+
coverage_count(Rows, waived, Waived).
|
|
915
|
+
|
|
916
|
+
coverage_count(Rows, Coverage, Count) :-
|
|
917
|
+
aggregate_all(count, (member(Row, Rows), get_dict(coverage, Row, Coverage)), Count).
|
|
918
|
+
|
|
919
|
+
%% dimension_stage(+ReqId, +ScenarioTestStage, +Context, -Stage)
|
|
920
|
+
% implements REQ-kibi-dimension-gap
|
|
921
|
+
% A claim that quantifies over a collection (each, every, all) or bounds a
|
|
922
|
+
% cardinality (at least, at most, exactly, no more than) must be grounded on what
|
|
923
|
+
% varies: a count or bound, a set, a rule over a variable or a state
|
|
924
|
+
% machine. A claim whose grounding facts are all booleans or ground
|
|
925
|
+
% predicates states the verdict, not the dimension, so a scenario with one
|
|
926
|
+
% file can pass where many files break. Claims a fresh certificate proves
|
|
927
|
+
% at code scope are decided already. A grounding fact can carry a
|
|
928
|
+
% dimension_waiver reason and a dimension_waiver_approved_by human for a
|
|
929
|
+
% quantifier word that does not range over anything ("without any network
|
|
930
|
+
% access"). Only, any and some are left out: they mostly scope a
|
|
931
|
+
% condition ("only when", "before any write") rather than range over one. Status is not_required (no such claim), waived (each one has an
|
|
932
|
+
% approved waiver) or missing.
|
|
933
|
+
dimension_stage(ReqId, ScenarioTestStage, Context, Stage) :-
|
|
934
|
+
findall(Key-FactId,
|
|
935
|
+
( negative_path_link(Link),
|
|
936
|
+
kb_relationship(Link, ReqId, FactId),
|
|
937
|
+
fact_claim_key_or_id(FactId, Key)
|
|
938
|
+
),
|
|
939
|
+
ClaimFacts0),
|
|
940
|
+
sort(ClaimFacts0, ClaimFacts),
|
|
941
|
+
pairs_keys(ClaimFacts, Keys0),
|
|
942
|
+
sort(Keys0, Keys),
|
|
943
|
+
certified_claim_keys(ReqId, ScenarioTestStage, Context, Certified),
|
|
944
|
+
findall(Claim,
|
|
945
|
+
( member(Key, Keys),
|
|
946
|
+
\+ memberchk(Key, Certified),
|
|
947
|
+
findall(FactId, member(Key-FactId, ClaimFacts), FactIds),
|
|
948
|
+
unrepresented_quantified_claim(Key, FactIds, Claim)
|
|
949
|
+
),
|
|
950
|
+
Claims),
|
|
951
|
+
partition(approved_dimension_waiver, Claims, Waived, Open),
|
|
952
|
+
findall(Key, (member(Claim, Open), get_dict(claimKey, Claim, Key)), OpenKeys),
|
|
953
|
+
pending_certified_claim_keys(ScenarioTestStage, Context, OpenKeys, Pending),
|
|
954
|
+
( Claims == []
|
|
955
|
+
-> Status = not_required
|
|
956
|
+
; Open == []
|
|
957
|
+
-> Status = waived
|
|
958
|
+
; Status = missing
|
|
959
|
+
),
|
|
960
|
+
Stage = _{
|
|
961
|
+
status: Status,
|
|
962
|
+
claims: Open,
|
|
963
|
+
waivedClaims: Waived,
|
|
964
|
+
pendingReproofClaims: Pending
|
|
965
|
+
}.
|
|
966
|
+
|
|
967
|
+
% A claim whose text quantifies and whose every grounding fact is a boolean
|
|
968
|
+
% property value or a ground predicate.
|
|
969
|
+
unrepresented_quantified_claim(Key, FactIds, Claim) :-
|
|
970
|
+
FactIds = [_|_],
|
|
971
|
+
forall(member(FactId, FactIds), dimensionless_fact(FactId)),
|
|
972
|
+
claim_text_of(FactIds, Text),
|
|
973
|
+
claim_quantifier(Text, Quantifier),
|
|
974
|
+
!,
|
|
975
|
+
quantifier_rewrites(Quantifier, Rewrites),
|
|
976
|
+
dimension_waiver(FactIds, Waiver),
|
|
977
|
+
atom_string(Quantifier, QuantifierText),
|
|
978
|
+
Claim = _{
|
|
979
|
+
claimKey: Key,
|
|
980
|
+
claimText: Text,
|
|
981
|
+
quantifier: QuantifierText,
|
|
982
|
+
factIds: FactIds,
|
|
983
|
+
rewrites: Rewrites,
|
|
984
|
+
waiver: Waiver
|
|
985
|
+
}.
|
|
986
|
+
|
|
987
|
+
dimensionless_fact(FactId) :-
|
|
988
|
+
kb_entity(FactId, fact, Props),
|
|
989
|
+
memberchk(fact_kind=RawKind, Props),
|
|
990
|
+
normalize_atom(RawKind, Kind),
|
|
991
|
+
( Kind == predicate
|
|
992
|
+
-> true
|
|
993
|
+
; Kind == property_value,
|
|
994
|
+
memberchk(value_type=RawType, Props),
|
|
995
|
+
normalize_atom(RawType, bool)
|
|
996
|
+
).
|
|
997
|
+
|
|
998
|
+
claim_text_of(FactIds, Text) :-
|
|
999
|
+
member(FactId, FactIds),
|
|
1000
|
+
kb_entity(FactId, fact, Props),
|
|
1001
|
+
memberchk(claim_text=Raw, Props),
|
|
1002
|
+
normalize_atom(Raw, Atom),
|
|
1003
|
+
Atom \== '',
|
|
1004
|
+
!,
|
|
1005
|
+
atom_string(Atom, Text).
|
|
1006
|
+
claim_text_of(FactIds, Text) :-
|
|
1007
|
+
member(FactId, FactIds),
|
|
1008
|
+
kb_entity(FactId, fact, Props),
|
|
1009
|
+
memberchk(title=Raw, Props),
|
|
1010
|
+
!,
|
|
1011
|
+
normalize_atom(Raw, Atom),
|
|
1012
|
+
atom_string(Atom, Text).
|
|
1013
|
+
|
|
1014
|
+
%% claim_quantifier(+Text, -Quantifier)
|
|
1015
|
+
% The first quantifier over a collection (each, every, all) or cardinality
|
|
1016
|
+
% bound (exactly, at least, at most, no more than) in Text.
|
|
1017
|
+
claim_quantifier(Text, Quantifier) :-
|
|
1018
|
+
string_lower(Text, Lower),
|
|
1019
|
+
split_string(Lower, " \t\n\r.,;:()[]\"'`/!?-", " \t\n\r.,;:()[]\"'`/!?-", Words0),
|
|
1020
|
+
exclude(==(""), Words0, Words),
|
|
1021
|
+
quantifier_in(Words, Quantifier).
|
|
1022
|
+
|
|
1023
|
+
quantifier_in(["no", "more", "than"|_], no_more_than) :- !.
|
|
1024
|
+
quantifier_in(["at", "least"|_], at_least) :- !.
|
|
1025
|
+
quantifier_in(["at", "most"|_], at_most) :- !.
|
|
1026
|
+
quantifier_in([Word|_], Quantifier) :-
|
|
1027
|
+
memberchk(Word, ["each", "every", "all", "exactly"]),
|
|
1028
|
+
!,
|
|
1029
|
+
atom_string(Quantifier, Word).
|
|
1030
|
+
quantifier_in([_|Rest], Quantifier) :-
|
|
1031
|
+
quantifier_in(Rest, Quantifier).
|
|
1032
|
+
|
|
1033
|
+
% The typed groundings that represent what a quantifier ranges over, most
|
|
1034
|
+
% likely first.
|
|
1035
|
+
quantifier_rewrites(Quantifier, ["bound"]) :-
|
|
1036
|
+
memberchk(Quantifier, [at_least, at_most, exactly, no_more_than]),
|
|
1037
|
+
!.
|
|
1038
|
+
quantifier_rewrites(_, ["rule", "count", "set"]).
|
|
1039
|
+
|
|
1040
|
+
%% dimension_waiver(+FactIds, -Waiver)
|
|
1041
|
+
% A grounding fact's dimension_waiver reason and the human who approved it
|
|
1042
|
+
% in dimension_waiver_approved_by. An approved waiver on any grounding fact
|
|
1043
|
+
% wins; otherwise a reason alone is reported as pending approval. Waiver is
|
|
1044
|
+
% _{} when none is recorded.
|
|
1045
|
+
dimension_waiver(FactIds, Waiver) :-
|
|
1046
|
+
findall(W, (member(FactId, FactIds), fact_dimension_waiver(FactId, W)), Waivers),
|
|
1047
|
+
( member(W, Waivers), get_dict(approvedBy, W, _)
|
|
1048
|
+
-> Waiver = W
|
|
1049
|
+
; Waivers = [W|_]
|
|
1050
|
+
-> Waiver = W
|
|
1051
|
+
; Waiver = _{}
|
|
1052
|
+
).
|
|
1053
|
+
|
|
1054
|
+
fact_dimension_waiver(FactId, Waiver) :-
|
|
1055
|
+
kb_entity(FactId, fact, Props),
|
|
1056
|
+
memberchk(dimension_waiver=RawReason, Props),
|
|
1057
|
+
normalize_atom(RawReason, Reason),
|
|
1058
|
+
Reason \== '',
|
|
1059
|
+
atom_string(Reason, ReasonText),
|
|
1060
|
+
( memberchk(dimension_waiver_approved_by=RawBy, Props),
|
|
1061
|
+
normalize_atom(RawBy, By),
|
|
1062
|
+
By \== ''
|
|
1063
|
+
-> atom_string(By, ByText),
|
|
1064
|
+
Waiver = _{reason: ReasonText, approvedBy: ByText}
|
|
1065
|
+
; Waiver = _{reason: ReasonText}
|
|
1066
|
+
).
|
|
1067
|
+
|
|
1068
|
+
approved_dimension_waiver(Claim) :-
|
|
1069
|
+
get_dict(approvedBy, Claim.waiver, _).
|
|
1070
|
+
|
|
1071
|
+
negative_path_link(requires_property).
|
|
1072
|
+
negative_path_link(requires_predicate).
|
|
1073
|
+
negative_path_link(requires_rule).
|
|
1074
|
+
|
|
1075
|
+
fact_claim_key_or_id(FactId, Key) :-
|
|
1076
|
+
( kb_entity(FactId, fact, Props),
|
|
1077
|
+
memberchk(claim_key=Raw, Props),
|
|
1078
|
+
normalize_atom(Raw, Key0),
|
|
1079
|
+
Key0 \== ''
|
|
1080
|
+
-> Key = Key0
|
|
1081
|
+
; Key = FactId
|
|
1082
|
+
).
|
|
1083
|
+
|
|
1084
|
+
% Claims of ReqId a passed receipt binds with a derived obligation outside
|
|
1085
|
+
% intent scope: a certificate decides them, both sides.
|
|
1086
|
+
certified_claim_keys(ReqId, ScenarioTestStage, Context, Keys) :-
|
|
1087
|
+
( get_dict(tests, ScenarioTestStage, Tests) -> true ; Tests = [] ),
|
|
1088
|
+
findall(Key,
|
|
1089
|
+
( member(TestId, Tests),
|
|
1090
|
+
test_receipt_evidence(Context, [unit, integration, end_to_end], TestId, Evidence),
|
|
1091
|
+
get_dict(state, Evidence, passed),
|
|
1092
|
+
get_dict(claimBindings, Evidence, Recorded),
|
|
1093
|
+
member(Entry, Recorded),
|
|
1094
|
+
inventory_entry_field(Entry, requirement_id, RawReq),
|
|
1095
|
+
normalize_atom(RawReq, ReqId),
|
|
1096
|
+
inventory_entry_field(Entry, claim_key, RawKey),
|
|
1097
|
+
normalize_atom(RawKey, Key),
|
|
1098
|
+
test_claim_binding(ReqId, Key, [TestId-Evidence], Binding),
|
|
1099
|
+
get_dict(binding, Binding, derived),
|
|
1100
|
+
\+ get_dict(scope, Binding, intent)
|
|
1101
|
+
),
|
|
1102
|
+
Keys0),
|
|
1103
|
+
sort(Keys0, Keys).
|
|
1104
|
+
|
|
719
1105
|
scenario_test_stage(ScenarioIds, Stage, ScenarioTests) :-
|
|
720
1106
|
maplist(scenario_test_obligation, ScenarioIds, Obligations),
|
|
721
1107
|
findall(TestId,
|
|
@@ -2280,14 +2666,58 @@ freshness_gap(stale_proof_receipt).
|
|
|
2280
2666
|
freshness_gap(proof_contract_mismatch).
|
|
2281
2667
|
|
|
2282
2668
|
freshness_only_gaps(Gaps, Stages) :-
|
|
2283
|
-
|
|
2284
|
-
|
|
2669
|
+
% A stale certificate receipt of a unit-scope test is not a gap of its
|
|
2670
|
+
% own, so a stage that waits only on one counts as the freshness gap.
|
|
2671
|
+
( include(freshness_gap, Gaps, [_|_])
|
|
2672
|
+
-> true
|
|
2673
|
+
; negative_path_freshness_only(Stages)
|
|
2674
|
+
; dimension_freshness_only(Stages)
|
|
2675
|
+
; cases_freshness_only(Stages)
|
|
2676
|
+
),
|
|
2677
|
+
exclude(freshness_gap, Gaps, Rest0),
|
|
2678
|
+
( negative_path_freshness_only(Stages)
|
|
2679
|
+
-> exclude(==(happy_path_only), Rest0, Rest1)
|
|
2680
|
+
; Rest1 = Rest0
|
|
2681
|
+
),
|
|
2682
|
+
( dimension_freshness_only(Stages)
|
|
2683
|
+
-> exclude(==(dimension_unrepresented), Rest1, Rest2)
|
|
2684
|
+
; Rest2 = Rest1
|
|
2685
|
+
),
|
|
2686
|
+
( cases_freshness_only(Stages)
|
|
2687
|
+
-> exclude(==(uncovered_case), Rest2, Rest)
|
|
2688
|
+
; Rest = Rest2
|
|
2689
|
+
),
|
|
2285
2690
|
( Rest == []
|
|
2286
2691
|
-> true
|
|
2287
2692
|
; Rest == [missing_production_symbol_coverage],
|
|
2288
2693
|
production_coverage_freshness_only(Stages)
|
|
2289
2694
|
).
|
|
2290
2695
|
|
|
2696
|
+
% The failure side is open only because certificate receipts that prove
|
|
2697
|
+
% every open claim are stale.
|
|
2698
|
+
negative_path_freshness_only(Stages) :-
|
|
2699
|
+
get_dict(negativePath, Stages, Stage),
|
|
2700
|
+
Stage.status == missing,
|
|
2701
|
+
Stage.claims \== [],
|
|
2702
|
+
subtract(Stage.claims, Stage.pendingReproofClaims, []).
|
|
2703
|
+
|
|
2704
|
+
% Every open quantified claim waits only on a stale certificate receipt.
|
|
2705
|
+
dimension_freshness_only(Stages) :-
|
|
2706
|
+
get_dict(dimension, Stages, Stage),
|
|
2707
|
+
Stage.status == missing,
|
|
2708
|
+
findall(Key, (member(Claim, Stage.claims), get_dict(claimKey, Claim, Key)), Keys),
|
|
2709
|
+
Keys \== [],
|
|
2710
|
+
subtract(Keys, Stage.pendingReproofClaims, []).
|
|
2711
|
+
|
|
2712
|
+
% Every uncovered rejection case waits only on a stale certificate receipt.
|
|
2713
|
+
cases_freshness_only(Stages) :-
|
|
2714
|
+
get_dict(cases, Stages, Stage),
|
|
2715
|
+
Stage.status == missing,
|
|
2716
|
+
findall(Key, (member(Row, Stage.uncoveredCases), get_dict(claimKey, Row, Key)), Keys0),
|
|
2717
|
+
sort(Keys0, Keys),
|
|
2718
|
+
Keys \== [],
|
|
2719
|
+
subtract(Keys, Stage.pendingReproofClaims, []).
|
|
2720
|
+
|
|
2291
2721
|
production_coverage_freshness_only(Stages) :-
|
|
2292
2722
|
Stage = Stages.productionSymbols,
|
|
2293
2723
|
forall(member(SymbolId, Stage.uncoveredSymbols),
|
|
@@ -2351,12 +2781,16 @@ prop_text(Props, Key, Text) :-
|
|
|
2351
2781
|
normalize_atom(Raw, Text),
|
|
2352
2782
|
Text \== ''.
|
|
2353
2783
|
|
|
2784
|
+
% The origin is JSON text, read whole: normalize_atom/2 keeps only what
|
|
2785
|
+
% follows the last "/", which cut an origin.ref quoting a URL down to a
|
|
2786
|
+
% fragment of the JSON.
|
|
2354
2787
|
origin_field(Props, Key, Value) :-
|
|
2355
2788
|
memberchk(origin=Raw, Props),
|
|
2356
|
-
|
|
2789
|
+
kb:source_value_atom(Raw, Json),
|
|
2357
2790
|
catch(atom_json_dict(Json, Origin, [value_string_as(atom)]), _, fail),
|
|
2791
|
+
is_dict(Origin),
|
|
2358
2792
|
get_dict(Key, Origin, Value0),
|
|
2359
|
-
|
|
2793
|
+
kb:source_value_atom(Value0, Value),
|
|
2360
2794
|
Value \== ''.
|
|
2361
2795
|
|
|
2362
2796
|
stage_dict(Stages, Stage) :-
|
|
@@ -2413,6 +2847,9 @@ proof_gap_present(infeasible_scenario, Stages) :- Stages.scenarios.infeasibleSce
|
|
|
2413
2847
|
proof_gap_present(unknown_scenario_feasibility, Stages) :-
|
|
2414
2848
|
get_dict(unknownFeasibility, Stages.scenarios, Unknown),
|
|
2415
2849
|
Unknown \= [].
|
|
2850
|
+
proof_gap_present(happy_path_only, Stages) :- Stages.negativePath.status == missing.
|
|
2851
|
+
proof_gap_present(dimension_unrepresented, Stages) :- Stages.dimension.status == missing.
|
|
2852
|
+
proof_gap_present(uncovered_case, Stages) :- Stages.cases.status == missing.
|
|
2416
2853
|
proof_gap_present(missing_scenario_test, Stages) :- memberchk(Stages.scenarios.status, [passed, blocked]), Stages.scenarioTests.status == missing.
|
|
2417
2854
|
proof_gap_present(missing_passing_e2e, Stages) :- Stages.passingE2e.status == missing.
|
|
2418
2855
|
proof_gap_present(missing_proof_receipt, Stages) :- Stages.passingE2e.missingReceiptTests \= [].
|
|
@@ -2442,6 +2879,9 @@ gap_definition(contradiction_check_incomplete, 41, contradictions, "Complete log
|
|
|
2442
2879
|
gap_definition(unknown_scenario_feasibility, 49, scenarios, "Link the property values the success scenario relies on with assumes facts that a current requirement constrains, so its feasibility can be decided.").
|
|
2443
2880
|
gap_definition(missing_scenario, 50, scenarios, "Add a specified_by scenario for the requirement.").
|
|
2444
2881
|
gap_definition(infeasible_scenario, 49, scenarios, "Set expects: rejection on the scenario, correct its assumes fact, or record a human-approved exception requirement that exempts the forbidding requirement.").
|
|
2882
|
+
gap_definition(dimension_unrepresented, 47, dimension, "Ground the quantified claim on what varies: an integer count or bound, a set of members, a rule over a variable, or a state machine. If the quantifier word ranges over nothing, record a dimension_waiver on the grounding fact for a person to approve, and record their approval in the same pull request.").
|
|
2883
|
+
gap_definition(uncovered_case, 46, cases, "Cover each rejection case: add a scenario that assumes a value in the case's class (the case's assumption), expects rejection or error, is linked with specified_by and is verified by a passing end-to-end test. If a person accepts the cases as covered another way, record a case_waiver on the grounding fact for them to approve, and record their approval in the same pull request.").
|
|
2884
|
+
gap_definition(happy_path_only, 48, negative_path, "Add a scenario that expects rejection or error and assumes a value this requirement forbids, link it with specified_by, and verify it with a passing end-to-end test.").
|
|
2445
2885
|
gap_definition(missing_scenario_test, 51, scenario_tests, "Link the scenario to a test with verified_by or validates.").
|
|
2446
2886
|
gap_definition(missing_passing_e2e, 52, passing_e2e, "Record fresh passing end-to-end evidence on a scenario-backed test.").
|
|
2447
2887
|
gap_definition(missing_proof_receipt, 53, passing_e2e, "Run the configured proof integration through kibi prove and append its kibi.proof-receipt.v1 result for the scenario-backed proof-bearing test.").
|
package/src/rule_registry.pl
CHANGED
|
@@ -28,6 +28,7 @@ known_rule('deprecated-adr-no-successor').
|
|
|
28
28
|
known_rule('superseded-requirement-open').
|
|
29
29
|
known_rule('scenario-feasibility').
|
|
30
30
|
known_rule('scenario-feasibility-unknown').
|
|
31
|
+
known_rule('scenario-outcome-mismatch').
|
|
31
32
|
known_rule('exception-claim-keys').
|
|
32
33
|
known_rule('numeric-string-value').
|
|
33
34
|
known_rule('rule-key-arguments-missing').
|
|
@@ -86,6 +87,7 @@ rule_enforcement_class('deprecated-adr-no-successor', canonical).
|
|
|
86
87
|
rule_enforcement_class('superseded-requirement-open', canonical).
|
|
87
88
|
rule_enforcement_class('scenario-feasibility', canonical).
|
|
88
89
|
rule_enforcement_class('scenario-feasibility-unknown', advisory).
|
|
90
|
+
rule_enforcement_class('scenario-outcome-mismatch', canonical).
|
|
89
91
|
rule_enforcement_class('exception-claim-keys', canonical).
|
|
90
92
|
rule_enforcement_class('numeric-string-value', advisory).
|
|
91
93
|
rule_enforcement_class('rule-key-arguments-missing', advisory).
|
|
@@ -144,6 +146,7 @@ rule_implementation('deprecated-adr-no-successor', prolog).
|
|
|
144
146
|
rule_implementation('superseded-requirement-open', prolog).
|
|
145
147
|
rule_implementation('scenario-feasibility', prolog).
|
|
146
148
|
rule_implementation('scenario-feasibility-unknown', prolog).
|
|
149
|
+
rule_implementation('scenario-outcome-mismatch', prolog).
|
|
147
150
|
rule_implementation('exception-claim-keys', prolog).
|
|
148
151
|
rule_implementation('numeric-string-value', prolog).
|
|
149
152
|
rule_implementation('rule-key-arguments-missing', prolog).
|
|
@@ -198,6 +201,7 @@ rule_predicate('deprecated-adr-no-successor', check_deprecated_adrs).
|
|
|
198
201
|
rule_predicate('superseded-requirement-open', check_superseded_requirement_open).
|
|
199
202
|
rule_predicate('scenario-feasibility', check_scenario_feasibility).
|
|
200
203
|
rule_predicate('scenario-feasibility-unknown', check_scenario_feasibility_unknown).
|
|
204
|
+
rule_predicate('scenario-outcome-mismatch', check_scenario_outcome_mismatch).
|
|
201
205
|
rule_predicate('exception-claim-keys', check_exception_claim_keys).
|
|
202
206
|
rule_predicate('numeric-string-value', check_numeric_string_values).
|
|
203
207
|
rule_predicate('rule-key-arguments-missing', check_rule_key_arguments_missing).
|
|
@@ -235,6 +239,7 @@ rule_predicate_arity('deprecated-adr-no-successor', 1).
|
|
|
235
239
|
rule_predicate_arity('superseded-requirement-open', 1).
|
|
236
240
|
rule_predicate_arity('scenario-feasibility', 1).
|
|
237
241
|
rule_predicate_arity('scenario-feasibility-unknown', 1).
|
|
242
|
+
rule_predicate_arity('scenario-outcome-mismatch', 1).
|
|
238
243
|
rule_predicate_arity('exception-claim-keys', 1).
|
|
239
244
|
rule_predicate_arity('numeric-string-value', 1).
|
|
240
245
|
rule_predicate_arity('rule-key-arguments-missing', 1).
|
|
@@ -276,6 +281,7 @@ rule_description('deprecated-adr-no-successor', 'Deprecated ADRs must have a suc
|
|
|
276
281
|
rule_description('superseded-requirement-open', 'A requirement that another requirement supersedes must have status closed, and supersedes links between requirements must not form a cycle').
|
|
277
282
|
rule_description('scenario-feasibility', 'A scenario that expects success must not assume property values that cannot hold, alone or together, with what current requirements require through typed property values or rules, unless an approved exception requirement waives that requirement or the listed clauses').
|
|
278
283
|
rule_description('scenario-feasibility-unknown', 'Advisory: a scenario that expects success assumes nothing, assumes values that contradict each other, assumes a property value no governing requirement constrains comparably (type, unit or operator mismatch, or conflicting requirements), leaves a governing rule\'s conditions undecided, or conflicts only with constraints whose validity window may not cover it, so its feasibility cannot be decided').
|
|
284
|
+
rule_description('scenario-outcome-mismatch', 'A scenario that expects rejection or error must assume a value or situation a current requirement forbids: when every assumption is governed by current requirements and they permit all of them together, the knowledge base predicts success and the scenario exercises no failure side').
|
|
279
285
|
rule_description('exception-claim-keys', 'An exception requirement\'s exempts_claims must name claim keys of a requirement it exempts, and requires an exempts link').
|
|
280
286
|
rule_description('numeric-string-value', 'Advisory: a property value stores a numeric-looking string where the comparison is numeric (ordering operator, or an int/number fact on the same property); the finding names the fact and the typed value, without converting it').
|
|
281
287
|
rule_description('rule-key-arguments-missing', 'Advisory: an opposing rule pair stays unresolved only because a body predicate declares no key_arguments; the finding names the predicate and the key positions that would decide the pair').
|