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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-core",
3
- "version": "0.26.1",
3
+ "version": "0.28.0",
4
4
  "private": false,
5
5
  "description": "Core Prolog modules and RDF graph logic for Kibi",
6
6
  "type": "module",
@@ -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]).