kibi-core 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-core",
3
- "version": "0.27.0",
3
+ "version": "0.29.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]).
package/src/checks.pl CHANGED
@@ -26,6 +26,9 @@
26
26
  check_adr_proposed/1,
27
27
  check_scenario_feasibility/1,
28
28
  check_scenario_feasibility_unknown/1,
29
+ check_scenario_outcome_mismatch/1,
30
+ negative_scenario_witness/3,
31
+ opposing_assumption/2,
29
32
  check_exception_claim_keys/1,
30
33
  check_numeric_string_values/1,
31
34
  check_rule_key_arguments_missing/1,
@@ -111,6 +114,7 @@ check_all(ViolationsDict) :-
111
114
  check_adr_proposed(AdrProposed),
112
115
  check_scenario_feasibility(ScenarioFeasibility),
113
116
  check_scenario_feasibility_unknown(ScenarioFeasibilityUnknown),
117
+ check_scenario_outcome_mismatch(ScenarioOutcomeMismatch),
114
118
  check_exception_claim_keys(ExceptionClaimKeys),
115
119
  check_numeric_string_values(NumericStringValues),
116
120
  check_domain_contradictions(Contradictions),
@@ -148,6 +152,7 @@ check_all(ViolationsDict) :-
148
152
  adr_proposed: AdrProposed,
149
153
  scenario_feasibility: ScenarioFeasibility,
150
154
  scenario_feasibility_unknown: ScenarioFeasibilityUnknown,
155
+ scenario_outcome_mismatch: ScenarioOutcomeMismatch,
151
156
  exception_claim_keys: ExceptionClaimKeys,
152
157
  numeric_string_value: NumericStringValues,
153
158
  domain_contradictions: Contradictions,
@@ -2132,6 +2137,176 @@ bool_domain_exhausted(bool, Different) :-
2132
2137
  memberchk(true, Different),
2133
2138
  memberchk(false, Different).
2134
2139
 
2140
+ %% negative_scenario_witness(?ScenarioId, ?ReqId, -Witness)
2141
+ % implements REQ-kibi-happy-path-gap
2142
+ % A scenario that expects rejection or error is a targeted negative for a
2143
+ % requirement when its assumptions break that requirement: the feasibility
2144
+ % analysis finds a witness naming it, or (below) an assumption opposes one of
2145
+ % the requirement's claim facts directly. A rejection scenario whose assumptions
2146
+ % the requirements permit, or that an approved exception waives, exercises no
2147
+ % requirement's failure side.
2148
+ negative_scenario_witness(ScenarioId, ReqId,
2149
+ infeasibility(ReqIds, AssumedFacts, ReqFacts, Reason)) :-
2150
+ negative_scenario(ScenarioId),
2151
+ scenario_feasibility_analysis(ScenarioId, analysis(Witnesses, _, _, _)),
2152
+ member(infeasibility(ReqIds, AssumedFacts, ReqFacts, Reason), Witnesses),
2153
+ member(ReqId, ReqIds).
2154
+ % A scenario the requirement specifies may also target one of its claims
2155
+ % directly: it assumes a fact that opposes the claim's fact (a conflicting
2156
+ % property value or polarity, the opposite polarity of the same predicate),
2157
+ % or, for a rule over predicates, a fact instantiating one of the rule's
2158
+ % atoms, so the scenario is about the situation the rule governs.
2159
+ negative_scenario_witness(ScenarioId, ReqId, opposed(AssumedFact, ReqFact)) :-
2160
+ negative_scenario(ScenarioId),
2161
+ kb_relationship(specified_by, ReqId, ScenarioId),
2162
+ kb:current_req(ReqId),
2163
+ kb_relationship(assumes, ScenarioId, AssumedFact),
2164
+ member(Link, [requires_property, requires_predicate, requires_rule]),
2165
+ kb_relationship(Link, ReqId, ReqFact),
2166
+ facts_oppose(AssumedFact, ReqFact).
2167
+
2168
+ facts_oppose(AssumedFact, ReqFact) :-
2169
+ kb:fact_property_tuple(AssumedFact, Subject, Property, OpA, TypeA, ValueA, UnitA, ScopeA, PolarityA),
2170
+ kb:fact_property_tuple(ReqFact, Subject, Property, OpB, TypeB, ValueB, UnitB, ScopeB, PolarityB),
2171
+ !,
2172
+ kb:scope_intersects(ScopeA, ScopeB),
2173
+ ( kb:polarity_conflict(Subject, Property, OpA, TypeA, ValueA, UnitA, ScopeA, PolarityA,
2174
+ OpB, TypeB, ValueB, UnitB, ScopeB, PolarityB, _)
2175
+ -> true
2176
+ ; PolarityA == PolarityB,
2177
+ kb:property_conflict(Subject, Property, OpA, TypeA, ValueA, UnitA, PolarityA,
2178
+ OpB, TypeB, ValueB, UnitB, PolarityB, _)
2179
+ -> true
2180
+ ).
2181
+ facts_oppose(AssumedFact, ReqFact) :-
2182
+ kb:predicate_fact(AssumedFact, Namespace, Name, Args, PolarityA),
2183
+ kb:predicate_fact(ReqFact, Namespace, Name, Args, PolarityB),
2184
+ !,
2185
+ PolarityA \== PolarityB.
2186
+ facts_oppose(AssumedFact, ReqFact) :-
2187
+ kb:predicate_fact(AssumedFact, Namespace, Name, _, _),
2188
+ stored_rule_fact(ReqFact, _, Rule),
2189
+ sub_term(atom(Namespace, Name, _, _, _), Rule),
2190
+ !.
2191
+
2192
+ %% opposing_assumption(+FactId, -Assumption)
2193
+ % implements REQ-kibi-happy-path-gap
2194
+ % A fact a negative scenario can assume to oppose the claim fact FactId, as
2195
+ % the fields of a new fact: the boundary value just past a numeric bound, the
2196
+ % other boolean, a different string, a set missing a required member or
2197
+ % holding an unlisted or excluded one, the opposite polarity of a predicate,
2198
+ % or, for a rule over predicates, its head instantiated with the variable
2199
+ % names. Fails when no such fact can be derived (a matches pattern).
2200
+ opposing_assumption(FactId, Assumption) :-
2201
+ kb:fact_property_tuple(FactId, Subject, Property, Op, Type, Value, Unit, Scope, Polarity),
2202
+ !,
2203
+ ( Polarity == forbid
2204
+ -> Op1 = Op, Value1 = Value
2205
+ ; opposing_value(Type, Op, Value, Op1, Value1)
2206
+ ),
2207
+ Base = _{fact_kind: property_value, subject_key: Subject, property_key: Property,
2208
+ operator: Op1, value_type: Type, value: Value1},
2209
+ ( Unit == '' -> Assumption0 = Base ; Assumption0 = Base.put(unit, Unit) ),
2210
+ ( Scope == '' -> Assumption = Assumption0 ; Assumption = Assumption0.put(scope, Scope) ).
2211
+ opposing_assumption(FactId, _{fact_kind: predicate, predicate_namespace: Namespace,
2212
+ predicate_name: Name, predicate_args: Args, polarity: Opposite}) :-
2213
+ kb:predicate_fact(FactId, Namespace, Name, Args, Polarity),
2214
+ !,
2215
+ kb:opposite_predicate_polarity(Polarity, Opposite).
2216
+ opposing_assumption(FactId, _{fact_kind: predicate, predicate_namespace: Namespace,
2217
+ predicate_name: Name, predicate_args: Args, polarity: assert}) :-
2218
+ stored_rule_fact(FactId, _, rule(_, _, Head, Body, _, _, _, _, _, _)),
2219
+ ( Head = atom(Namespace, Name, Terms, _, _)
2220
+ -> true
2221
+ ; sub_term(atom(Namespace, Name, Terms, _, _), Body)
2222
+ ),
2223
+ !,
2224
+ maplist(term_constant, Terms, Args).
2225
+
2226
+ term_constant(Term, Constant) :-
2227
+ ( Term = var(Name, _)
2228
+ -> downcase_atom(Name, Constant)
2229
+ ; Term = const(Value, _)
2230
+ -> term_to_atom(Value, Constant)
2231
+ ; term_to_atom(Term, Constant)
2232
+ ).
2233
+
2234
+ opposing_value(bool, eq, Value, eq, Opposite) :-
2235
+ !,
2236
+ ( memberchk(Value, [true, "true", 'True']) -> Opposite = false ; Opposite = true ).
2237
+ opposing_value(Type, Op, Value, Op1, Value1) :-
2238
+ kb:is_numeric_type(Type),
2239
+ number(Value),
2240
+ !,
2241
+ opposing_number(Type, Op, Value, Op1, Value1).
2242
+ opposing_value(set, Op, Members, Op1, Value1) :-
2243
+ !,
2244
+ opposing_set(Op, Members, Op1, Value1).
2245
+ opposing_value(_, eq, Value, neq, Value) :- !.
2246
+ opposing_value(_, neq, Value, eq, Value).
2247
+
2248
+ opposing_number(_, gt, Bound, eq, Bound).
2249
+ opposing_number(_, lt, Bound, eq, Bound).
2250
+ opposing_number(int, gte, Bound, eq, Value) :- !, Value is Bound - 1.
2251
+ opposing_number(_, gte, Bound, lt, Bound).
2252
+ opposing_number(int, lte, Bound, eq, Value) :- !, Value is Bound + 1.
2253
+ opposing_number(_, lte, Bound, gt, Bound).
2254
+ opposing_number(_, eq, Value, neq, Value).
2255
+ opposing_number(_, neq, Value, eq, Value).
2256
+
2257
+ opposing_set(eq, Members, eq, Fewer) :- members_but_last(Members, Fewer).
2258
+ opposing_set(includes, Members, eq, Fewer) :- members_but_last(Members, Fewer).
2259
+ opposing_set(subset_of, _, includes, [unlisted_member]).
2260
+ opposing_set(excludes, [First|_], includes, [First]).
2261
+
2262
+ members_but_last(Members, Fewer) :-
2263
+ ( append(Fewer, [_], Members) -> true ; Fewer = [] ).
2264
+
2265
+ negative_scenario(ScenarioId) :-
2266
+ ( var(ScenarioId)
2267
+ -> findall(Id,
2268
+ (scenario_expects(Id, Outcome), memberchk(Outcome, [rejection, error])),
2269
+ Ids0),
2270
+ sort(Ids0, Ids),
2271
+ member(ScenarioId, Ids)
2272
+ ; scenario_expects(ScenarioId, Outcome),
2273
+ memberchk(Outcome, [rejection, error])
2274
+ ).
2275
+
2276
+ %% check_scenario_outcome_mismatch(-Violations)
2277
+ % implements REQ-kibi-happy-path-gap
2278
+ % A scenario that expects rejection or error but whose assumptions current
2279
+ % requirements govern and permit together aims at no failure: the knowledge
2280
+ % base predicts success for it. Only a decided feasible outcome is reported;
2281
+ % a scenario that assumes nothing, or whose assumptions no requirement
2282
+ % governs, is left alone.
2283
+ check_scenario_outcome_mismatch(Violations) :-
2284
+ findall(Violation, scenario_outcome_mismatch_violation(Violation), Unsorted),
2285
+ sort(Unsorted, Violations).
2286
+
2287
+ scenario_outcome_mismatch_violation(violation(
2288
+ 'scenario-outcome-mismatch',
2289
+ ScenarioId,
2290
+ Description,
2291
+ "Assume a value a current requirement forbids, so the scenario exercises that requirement's failure side, or set expects: success when the scenario describes a permitted situation",
2292
+ Source
2293
+ )) :-
2294
+ negative_scenario(ScenarioId),
2295
+ scenario_expects(ScenarioId, Expected),
2296
+ scenario_feasibility_analysis(ScenarioId, Analysis),
2297
+ analysis_outcome(Analysis, feasible),
2298
+ findall(FactId, kb_relationship(assumes, ScenarioId, FactId), FactIds0),
2299
+ sort(FactIds0, FactIds),
2300
+ atomic_list_concat(FactIds, ', ', FactText),
2301
+ format(string(Description),
2302
+ "Scenario expects ~w, but current requirements permit everything it assumes (~w), so the knowledge base predicts success",
2303
+ [Expected, FactText]),
2304
+ ( kb_entity(ScenarioId, scenario, Props),
2305
+ memberchk(source=Source, Props)
2306
+ -> true
2307
+ ; Source = ""
2308
+ ).
2309
+
2135
2310
  %% check_scenario_feasibility_unknown(-Violations)
2136
2311
  % implements REQ-kibi-scenario-feasibility-v2
2137
2312
  % Advisory: success scenarios whose feasibility the checker cannot decide.
@@ -3754,6 +3929,7 @@ check_selected_dispatch(Rules, _{
3754
3929
  adr_proposed: AdrProposed,
3755
3930
  scenario_feasibility: ScenarioFeasibility,
3756
3931
  scenario_feasibility_unknown: ScenarioFeasibilityUnknown,
3932
+ scenario_outcome_mismatch: ScenarioOutcomeMismatch,
3757
3933
  exception_claim_keys: ExceptionClaimKeys,
3758
3934
  numeric_string_value: NumericStringValues,
3759
3935
  domain_contradictions: Contradictions,
@@ -3791,6 +3967,7 @@ check_selected_dispatch(Rules, _{
3791
3967
  selected_rule(Rules, 'adr-proposed', check_adr_proposed, AdrProposed),
3792
3968
  selected_rule(Rules, 'scenario-feasibility', check_scenario_feasibility, ScenarioFeasibility),
3793
3969
  selected_rule(Rules, 'scenario-feasibility-unknown', check_scenario_feasibility_unknown, ScenarioFeasibilityUnknown),
3970
+ selected_rule(Rules, 'scenario-outcome-mismatch', check_scenario_outcome_mismatch, ScenarioOutcomeMismatch),
3794
3971
  selected_rule(Rules, 'exception-claim-keys', check_exception_claim_keys, ExceptionClaimKeys),
3795
3972
  selected_rule(Rules, 'numeric-string-value', check_numeric_string_values, NumericStringValues),
3796
3973
  selected_rule(Rules, 'domain-contradictions', check_domain_contradictions, Contradictions),
@@ -3925,6 +4102,7 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
3925
4102
  check_adr_proposed(AdrProposed),
3926
4103
  check_scenario_feasibility(ScenarioFeasibility),
3927
4104
  check_scenario_feasibility_unknown(ScenarioFeasibilityUnknown),
4105
+ check_scenario_outcome_mismatch(ScenarioOutcomeMismatch),
3928
4106
  check_exception_claim_keys(ExceptionClaimKeys),
3929
4107
  check_numeric_string_values(NumericStringValues),
3930
4108
  check_domain_contradictions(Contradictions),
@@ -3961,6 +4139,7 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
3961
4139
  adr_proposed: AdrProposed,
3962
4140
  scenario_feasibility: ScenarioFeasibility,
3963
4141
  scenario_feasibility_unknown: ScenarioFeasibilityUnknown,
4142
+ scenario_outcome_mismatch: ScenarioOutcomeMismatch,
3964
4143
  exception_claim_keys: ExceptionClaimKeys,
3965
4144
  numeric_string_value: NumericStringValues,
3966
4145
  domain_contradictions: Contradictions,
package/src/discovery.pl CHANGED
@@ -417,7 +417,7 @@ evidence_floor_json(BindingMode, TestBindings, VerificationSnapshot, CheckedAt,
417
417
  ( member(Id-Proof, Evaluated),
418
418
  Floor = Proof.evidenceFloor,
419
419
  Floor.status \== met,
420
- Row = _{
420
+ Row0 = _{
421
421
  id: Id,
422
422
  evidenceLevel: Proof.evidenceLevel,
423
423
  requiredEvidence: Floor.required,
@@ -427,7 +427,14 @@ evidence_floor_json(BindingMode, TestBindings, VerificationSnapshot, CheckedAt,
427
427
  proofGaps: Proof.proofGaps,
428
428
  gapDetails: Details
429
429
  },
430
- evidence_floor_gap_details(Proof, Details)
430
+ evidence_floor_gap_details(Proof, Details),
431
+ % implements REQ-kibi-claim-bound-proofs
432
+ % A claim its proof refuted: the test and what it read.
433
+ ( get_dict(claimBindingRejections, Proof, Refutations),
434
+ Refutations \== []
435
+ -> Row = Row0.put(refutations, Refutations)
436
+ ; Row = Row0
437
+ )
431
438
  ),
432
439
  Rows0),
433
440
  sort(id, @<, Rows0, Rows),