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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-core",
3
- "version": "0.27.0",
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]).
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,
@@ -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
- include(freshness_gap, Gaps, [_|_]),
2284
- exclude(freshness_gap, Gaps, Rest),
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
- normalize_atom(Raw, Json),
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
- normalize_atom(Value0, Value),
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.").
@@ -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').