kibi-core 0.26.1 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/schema/entities.pl +12 -0
- package/src/cases.pl +311 -0
- package/src/checks.pl +357 -17
- package/src/claim_obligations.pl +16 -1
- package/src/code_certificates.pl +4 -0
- package/src/discovery.pl +65 -23
- package/src/intervals.pl +35 -14
- package/src/kb.pl +288 -36
- package/src/kb_consistency.pl +15 -0
- package/src/logic_ir.pl +130 -161
- package/src/model_check.pl +54 -6
- package/src/requirement_proof.pl +535 -14
- package/src/rule_registry.pl +6 -0
- package/src/status.pl +16 -0
- package/src/units.pl +1 -3
package/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]).
|