kibi-core 0.23.0 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/schema/entities.pl +6 -0
- package/schema/validation.pl +7 -2
- package/src/certificate_check.pl +62 -1
- package/src/certificates.pl +17 -1
- package/src/checks.pl +4 -1
- package/src/claim_obligations.pl +706 -0
- package/src/code_certificates.pl +532 -0
- package/src/discovery.pl +160 -2
- package/src/intervals.pl +11 -0
- package/src/kb.pl +55 -1
- package/src/model_check.pl +728 -0
- package/src/requirement_proof.pl +561 -42
- package/src/rule_registry.pl +8 -0
- package/src/semantic_quality.pl +1 -1
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
% Module: claim_obligations
|
|
2
|
+
% implements REQ-kibi-claim-bound-proofs, REQ-kibi-non-numeric-proof-kinds
|
|
3
|
+
%
|
|
4
|
+
% What a certificate has to establish for it to prove one requirement claim.
|
|
5
|
+
% A claim is grounded by exactly one typed fact; the obligation is derived
|
|
6
|
+
% from that fact, so a certificate proves the claim's own content instead of
|
|
7
|
+
% a label its test chose:
|
|
8
|
+
%
|
|
9
|
+
% set fact (value_type set, or the legacy allowed_values string "a|b|c")
|
|
10
|
+
% exhaustive, typecheck the domain is exactly the fact's set
|
|
11
|
+
% typecheck with property required_fields: every member
|
|
12
|
+
% is a required, non-nullable field of the type
|
|
13
|
+
% rule every member satisfies the predicate the
|
|
14
|
+
% fact's property names
|
|
15
|
+
% state_machine with property transitions, states,
|
|
16
|
+
% initial_states, terminal_states or
|
|
17
|
+
% reachable_states: that projection of the
|
|
18
|
+
% machine is exactly the set
|
|
19
|
+
% numeric fact (value_type int or number, a comparison operator)
|
|
20
|
+
% arithmetic, smt the certificate's claim is the fact's constraint
|
|
21
|
+
% (negated for polarity forbid)
|
|
22
|
+
% rule fact (kibi.logic.v1)
|
|
23
|
+
% rule, state_machine the rule holds for every assignment of its
|
|
24
|
+
% variables over their domains (model_check.pl)
|
|
25
|
+
% predicate fact (a ground atom asserted or denied)
|
|
26
|
+
% rule, state_machine the code decides the atom as the fact says
|
|
27
|
+
% string fact (value_type string, operator eq, neq or matches)
|
|
28
|
+
% regex the code's string or pattern meets the
|
|
29
|
+
% constraint (decided in the CLI, exactly)
|
|
30
|
+
% set fact under source (any operator: eq, includes, subset_of, excludes)
|
|
31
|
+
% source the members Kibi read from the project source
|
|
32
|
+
% stand in the operator's relation to the set;
|
|
33
|
+
% with property required_fields or
|
|
34
|
+
% nullable_fields every selected table or object
|
|
35
|
+
% has each member as a required (or nullable)
|
|
36
|
+
% field; with property middleware every
|
|
37
|
+
% selected route's middleware stands in the
|
|
38
|
+
% relation
|
|
39
|
+
% set fact under state_machine with operator includes, subset_of or
|
|
40
|
+
% excludes, or with property reachable_pairs or required_waypoints
|
|
41
|
+
% state_machine the projection stands in the operator's
|
|
42
|
+
% relation to the set; a required waypoint
|
|
43
|
+
% "from->to via mid" holds when every path from
|
|
44
|
+
% one state to the other passes through mid
|
|
45
|
+
%
|
|
46
|
+
% Any other pairing of a set, numeric or string fact with a certificate kind
|
|
47
|
+
% is incompatible (a set fact under arithmetic, a numeric fact under
|
|
48
|
+
% exhaustive). A fact kind with no derivation for the proof kind (boolean
|
|
49
|
+
% placeholders, a rule under exhaustive) can only be attested: the test
|
|
50
|
+
% author says the certificate expresses the claim, and Kibi never counts that
|
|
51
|
+
% as proved.
|
|
52
|
+
|
|
53
|
+
:- module(claim_obligations, [
|
|
54
|
+
claim_obligation/3,
|
|
55
|
+
fact_obligation/3,
|
|
56
|
+
obligation_equal/2,
|
|
57
|
+
proof_contract_claim_candidates_json/1,
|
|
58
|
+
proof_contract_claim_issues_json/1,
|
|
59
|
+
test_claim_obligations_json/4,
|
|
60
|
+
claim_model_check_json/4
|
|
61
|
+
]).
|
|
62
|
+
|
|
63
|
+
:- use_module(library(http/json)).
|
|
64
|
+
:- use_module(library(apply)).
|
|
65
|
+
:- use_module(library(lists)).
|
|
66
|
+
:- use_module('kb.pl').
|
|
67
|
+
:- use_module('model_check.pl', [model_check_obligation/4, edge_text/3]).
|
|
68
|
+
|
|
69
|
+
%% fact_obligation(+FactId, +ProofKind, -Result)
|
|
70
|
+
% Result is obligation(Dict), attested, or incompatible(Reason).
|
|
71
|
+
fact_obligation(FactId, ProofKind, Result) :-
|
|
72
|
+
( fact_set(FactId, Members, Operator)
|
|
73
|
+
-> ( ProofKind == source
|
|
74
|
+
-> source_obligation(FactId, Members, Operator, Result)
|
|
75
|
+
; ProofKind == state_machine
|
|
76
|
+
-> machine_obligation(FactId, Members, Operator, Result)
|
|
77
|
+
; Operator == includes
|
|
78
|
+
-> includes_obligation(FactId, Members, ProofKind, Result)
|
|
79
|
+
; Operator == eq
|
|
80
|
+
-> set_obligation(FactId, Members, ProofKind, Result)
|
|
81
|
+
; format(string(Reason),
|
|
82
|
+
"~w is a ~w set fact, which only source and state_machine certificates can derive (a ~w certificate compares the whole set)",
|
|
83
|
+
[FactId, Operator, ProofKind]),
|
|
84
|
+
Result = incompatible(Reason)
|
|
85
|
+
)
|
|
86
|
+
; fact_numeric_constraint(FactId, Variable, ValueType, Operator, Value)
|
|
87
|
+
-> ( memberchk(ProofKind, [arithmetic, smt])
|
|
88
|
+
-> Result = obligation(_{kind: "numeric_constraint", variable: Variable,
|
|
89
|
+
value_type: ValueType, operator: Operator, value: Value})
|
|
90
|
+
; ( memberchk(ProofKind, [typecheck, source]) -> Article = a ; Article = an ),
|
|
91
|
+
format(string(Reason),
|
|
92
|
+
"~w is a numeric fact, which ~w ~w certificate cannot express (use arithmetic or smt)",
|
|
93
|
+
[FactId, Article, ProofKind]),
|
|
94
|
+
Result = incompatible(Reason)
|
|
95
|
+
)
|
|
96
|
+
; memberchk(ProofKind, [rule, state_machine]),
|
|
97
|
+
fact_rule_hash(FactId, Hash)
|
|
98
|
+
-> atom_string(FactId, FactString),
|
|
99
|
+
Result = obligation(_{kind: "rule_holds", fact_id: FactString, rule_hash: Hash})
|
|
100
|
+
; memberchk(ProofKind, [rule, state_machine]),
|
|
101
|
+
predicate_expectation(FactId, Name, Args, Value)
|
|
102
|
+
-> atom_string(Name, NameString),
|
|
103
|
+
maplist(atom_string, Args, ArgStrings),
|
|
104
|
+
Result = obligation(_{kind: "predicate_holds", predicate: NameString,
|
|
105
|
+
args: ArgStrings, value: Value})
|
|
106
|
+
; fact_string_constraint(FactId, Operator, Value, Polarity)
|
|
107
|
+
-> ( ProofKind == regex
|
|
108
|
+
-> Result = obligation(_{kind: "string_constraint", operator: Operator,
|
|
109
|
+
value: Value, polarity: Polarity})
|
|
110
|
+
; memberchk(ProofKind, [exhaustive, typecheck, arithmetic, smt])
|
|
111
|
+
-> Result = attested
|
|
112
|
+
; format(string(Reason),
|
|
113
|
+
"~w is a string fact, which a ~w certificate cannot express (use regex)",
|
|
114
|
+
[FactId, ProofKind]),
|
|
115
|
+
Result = incompatible(Reason)
|
|
116
|
+
)
|
|
117
|
+
; Result = attested
|
|
118
|
+
).
|
|
119
|
+
|
|
120
|
+
% Set facts: what each kind derives from the members, by the property the
|
|
121
|
+
% fact names where the kind reads one.
|
|
122
|
+
set_obligation(_FactId, Members, exhaustive, obligation(_{kind: "set_equals", members: Members})) :- !.
|
|
123
|
+
set_obligation(FactId, Members, typecheck, obligation(Obligation)) :- !,
|
|
124
|
+
( fact_property_key(FactId, required_fields)
|
|
125
|
+
-> Obligation = _{kind: "required_fields", members: Members}
|
|
126
|
+
; Obligation = _{kind: "set_equals", members: Members}
|
|
127
|
+
).
|
|
128
|
+
set_obligation(FactId, Members, rule, obligation(_{kind: "every_member", predicate: Predicate, members: Members})) :-
|
|
129
|
+
fact_property_key(FactId, Property),
|
|
130
|
+
atom_string(Property, Predicate),
|
|
131
|
+
!.
|
|
132
|
+
set_obligation(FactId, Members, state_machine, Result) :- !,
|
|
133
|
+
machine_obligation(FactId, Members, eq, Result).
|
|
134
|
+
set_obligation(FactId, _Members, rule, incompatible(Reason)) :- !,
|
|
135
|
+
format(string(Reason),
|
|
136
|
+
"~w is a set fact with no property to read as the predicate every member satisfies",
|
|
137
|
+
[FactId]).
|
|
138
|
+
set_obligation(FactId, _Members, ProofKind, incompatible(Reason)) :-
|
|
139
|
+
( ProofKind == regex -> Article = a ; Article = an ),
|
|
140
|
+
format(string(Reason),
|
|
141
|
+
"~w is a set fact, which ~w ~w certificate cannot express (use exhaustive, typecheck, rule or state_machine)",
|
|
142
|
+
[FactId, Article, ProofKind]).
|
|
143
|
+
|
|
144
|
+
% includes set facts ("every member of A has P") state containment only, so
|
|
145
|
+
% only the kinds that check each member on its own can derive them: a rule
|
|
146
|
+
% checks the property on every member, and a typecheck checks each field is
|
|
147
|
+
% required. Kinds that compare the whole set would read containment as
|
|
148
|
+
% equality.
|
|
149
|
+
includes_obligation(FactId, Members, rule, Result) :- !,
|
|
150
|
+
set_obligation(FactId, Members, rule, Result).
|
|
151
|
+
includes_obligation(FactId, Members, typecheck, obligation(_{kind: "required_fields", members: Members})) :-
|
|
152
|
+
fact_property_key(FactId, required_fields),
|
|
153
|
+
!.
|
|
154
|
+
includes_obligation(FactId, _Members, ProofKind, incompatible(Reason)) :-
|
|
155
|
+
format(string(Reason),
|
|
156
|
+
"~w is an includes set fact (every member has the property), which a ~w certificate cannot derive: it compares the whole set (use rule, or an eq set fact for exactly these members)",
|
|
157
|
+
[FactId, ProofKind]).
|
|
158
|
+
|
|
159
|
+
% implements REQ-kibi-source-proof-kinds
|
|
160
|
+
% A state machine set fact: the projection its property names stands in the
|
|
161
|
+
% fact operator's relation to the members. eq on a plain projection keeps
|
|
162
|
+
% the machine_set obligation earlier receipts recorded.
|
|
163
|
+
machine_obligation(FactId, Members, Operator, Result) :-
|
|
164
|
+
( fact_property_key(FactId, Projection),
|
|
165
|
+
memberchk(Projection, [transitions, states, initial_states, terminal_states,
|
|
166
|
+
reachable_states, reachable_pairs, required_waypoints])
|
|
167
|
+
-> ( memberchk(Projection, [transitions, reachable_pairs])
|
|
168
|
+
-> maplist(edge_member, Members, Edges0),
|
|
169
|
+
sort(Edges0, Spelled)
|
|
170
|
+
; Projection == required_waypoints
|
|
171
|
+
-> maplist(waypoint_member, Members, Spelled0),
|
|
172
|
+
sort(Spelled0, Spelled)
|
|
173
|
+
; Spelled = Members
|
|
174
|
+
),
|
|
175
|
+
atom_string(Projection, ProjectionString),
|
|
176
|
+
atom_string(Operator, OperatorString),
|
|
177
|
+
( Projection == required_waypoints,
|
|
178
|
+
Operator == subset_of
|
|
179
|
+
-> format(string(Reason),
|
|
180
|
+
"~w names required waypoints with subset_of; state each waypoint with includes (they hold) or excludes (they do not)",
|
|
181
|
+
[FactId]),
|
|
182
|
+
Result = incompatible(Reason)
|
|
183
|
+
; Projection == required_waypoints,
|
|
184
|
+
\+ ( member(M, Spelled), sub_string(M, _, _, _, " via ") )
|
|
185
|
+
-> format(string(Reason),
|
|
186
|
+
"~w names required waypoints that are not spelled \"from->to via state\"",
|
|
187
|
+
[FactId]),
|
|
188
|
+
Result = incompatible(Reason)
|
|
189
|
+
; Operator == eq,
|
|
190
|
+
Projection \== required_waypoints
|
|
191
|
+
-> Result = obligation(_{kind: "machine_set", projection: ProjectionString, members: Spelled})
|
|
192
|
+
; Result = obligation(_{kind: "machine_relation", projection: ProjectionString,
|
|
193
|
+
operator: OperatorString, members: Spelled})
|
|
194
|
+
)
|
|
195
|
+
; format(string(Reason),
|
|
196
|
+
"~w is a set fact whose property is not a machine projection (transitions, states, initial_states, terminal_states, reachable_states, reachable_pairs or required_waypoints)",
|
|
197
|
+
[FactId]),
|
|
198
|
+
Result = incompatible(Reason)
|
|
199
|
+
).
|
|
200
|
+
|
|
201
|
+
% A required waypoint "from -> to via state", spelled "from->to via state".
|
|
202
|
+
waypoint_member(Member, Spelled) :-
|
|
203
|
+
( sub_string(Member, Before, _, After, " via ")
|
|
204
|
+
-> sub_string(Member, 0, Before, _, Edge0),
|
|
205
|
+
sub_string(Member, _, After, 0, Via0),
|
|
206
|
+
edge_member(Edge0, Edge),
|
|
207
|
+
normalize_space(atom(Via), Via0),
|
|
208
|
+
format(string(Spelled), "~w via ~w", [Edge, Via])
|
|
209
|
+
; Spelled = Member
|
|
210
|
+
).
|
|
211
|
+
|
|
212
|
+
% implements REQ-kibi-source-proof-kinds
|
|
213
|
+
% A set fact under the source kind. The property decides what the members
|
|
214
|
+
% are about: fields every selected table or object has as required or
|
|
215
|
+
% nullable, middleware every selected route runs, or (any other property)
|
|
216
|
+
% the members of the source itself.
|
|
217
|
+
% A required_fields or nullable_fields fact says which fields have the
|
|
218
|
+
% property; with subset_of or excludes it would be decided against whatever
|
|
219
|
+
% members the contract selects, so it is refused instead.
|
|
220
|
+
source_obligation(FactId, Members, Operator, Result) :-
|
|
221
|
+
atom_string(Operator, OperatorString),
|
|
222
|
+
( fact_property_key(FactId, Property) -> true ; Property = none ),
|
|
223
|
+
( memberchk(Property-Kind, [required_fields-"required_fields", nullable_fields-"nullable_fields"])
|
|
224
|
+
-> ( memberchk(Operator, [eq, includes])
|
|
225
|
+
-> Result = obligation(_{kind: Kind, members: Members})
|
|
226
|
+
; format(string(Reason),
|
|
227
|
+
"~w is a ~w fact with operator ~w; a source proof decides ~w only with eq or includes (to bound the fields, use a columns or fields fact)",
|
|
228
|
+
[FactId, Property, Operator, Property]),
|
|
229
|
+
Result = incompatible(Reason)
|
|
230
|
+
)
|
|
231
|
+
; Property == middleware
|
|
232
|
+
-> Result = obligation(_{kind: "route_middleware", operator: OperatorString, members: Members})
|
|
233
|
+
; Result = obligation(_{kind: "set_relation", operator: OperatorString, members: Members})
|
|
234
|
+
).
|
|
235
|
+
|
|
236
|
+
% A transition member "from -> to" (or with U+2192 as the arrow) spelled "from->to".
|
|
237
|
+
edge_member(Member, Spelled) :-
|
|
238
|
+
( sub_string(Member, Before, _, After, "->")
|
|
239
|
+
-> sub_string(Member, 0, Before, _, From0),
|
|
240
|
+
sub_string(Member, _, After, 0, To0)
|
|
241
|
+
; sub_string(Member, Before, _, After, "\u2192")
|
|
242
|
+
-> sub_string(Member, 0, Before, _, From0),
|
|
243
|
+
sub_string(Member, _, After, 0, To0)
|
|
244
|
+
; From0 = Member, To0 = ""
|
|
245
|
+
),
|
|
246
|
+
normalize_space(atom(From), From0),
|
|
247
|
+
normalize_space(atom(To), To0),
|
|
248
|
+
edge_text(From, To, Spelled).
|
|
249
|
+
|
|
250
|
+
% What a predicate fact says the code decides. A decision predicate (the
|
|
251
|
+
% built-in permission_rule, or a schema with key_arguments and a decision
|
|
252
|
+
% argument) is about the key arguments: the code's function takes them and
|
|
253
|
+
% returns whether the effective decision is allow. Any other predicate is
|
|
254
|
+
% true when asserted and false when denied.
|
|
255
|
+
predicate_expectation(FactId, Name, KeyArgs, Value) :-
|
|
256
|
+
kb:predicate_fact(FactId, Namespace, Name, Args, Polarity),
|
|
257
|
+
length(Args, Arity),
|
|
258
|
+
( kb:decision_signatures(Signatures),
|
|
259
|
+
memberchk(decision_signature(Namespace, Name, Arity, KeyPositions, DecisionPosition), Signatures)
|
|
260
|
+
-> findall(Arg, ( member(Position, KeyPositions), nth1(Position, Args, Arg) ), KeyArgs),
|
|
261
|
+
nth1(DecisionPosition, Args, Decision),
|
|
262
|
+
kb:effective_decision(Decision, Polarity, Effect),
|
|
263
|
+
( Effect == allow -> Value = true ; Value = false )
|
|
264
|
+
; memberchk(Polarity-Value, [assert-true, deny-false]),
|
|
265
|
+
KeyArgs = Args
|
|
266
|
+
).
|
|
267
|
+
|
|
268
|
+
fact_property_key(FactId, Property) :-
|
|
269
|
+
kb_entity(FactId, fact, Props),
|
|
270
|
+
prop_atom(Props, property_key, Property).
|
|
271
|
+
|
|
272
|
+
fact_rule_hash(FactId, Hash) :-
|
|
273
|
+
kb_entity(FactId, fact, Props),
|
|
274
|
+
prop_atom(Props, fact_kind, rule),
|
|
275
|
+
memberchk(rule_ir=_, Props),
|
|
276
|
+
( prop_atom(Props, rule_hash, HashAtom)
|
|
277
|
+
-> atom_string(HashAtom, Hash)
|
|
278
|
+
; Hash = ""
|
|
279
|
+
).
|
|
280
|
+
|
|
281
|
+
% A string property_value fact: eq, neq or matches on a string value.
|
|
282
|
+
fact_string_constraint(FactId, Operator, Value, Polarity) :-
|
|
283
|
+
kb:fact_property_tuple(FactId, _Subject, Property, Op, string, Raw, _Unit, _Scope, Polarity0),
|
|
284
|
+
Property \== allowed_values,
|
|
285
|
+
memberchk(Op, [eq, neq, matches]),
|
|
286
|
+
text_string(Raw, Value),
|
|
287
|
+
atom_string(Op, Operator),
|
|
288
|
+
atom_string(Polarity0, Polarity).
|
|
289
|
+
|
|
290
|
+
%% claim_obligation(+ReqId, +ClaimKey, +ProofKind, -Result)
|
|
291
|
+
% As fact_obligation/3 for the fact grounding ClaimKey in ReqId, with the
|
|
292
|
+
% fact id: obligation(FactId, Dict), attested(FactId), incompatible(FactId,
|
|
293
|
+
% Reason), or unresolved(Reason) when the claim cannot be named by a proof.
|
|
294
|
+
claim_obligation(ReqId-ClaimKey, ProofKind, Result) :-
|
|
295
|
+
( \+ requirement_claim_status(ReqId, ClaimKey, _)
|
|
296
|
+
-> format(string(Reason), "~w is not a claim of ~w", [ClaimKey, ReqId]),
|
|
297
|
+
Result = unresolved(Reason)
|
|
298
|
+
; requirement_claim_status(ReqId, ClaimKey, nonlogical)
|
|
299
|
+
-> format(string(Reason),
|
|
300
|
+
"claim ~w of ~w is nonlogical: a nonlogical span is context, not a claim to prove",
|
|
301
|
+
[ClaimKey, ReqId]),
|
|
302
|
+
Result = unresolved(Reason)
|
|
303
|
+
; requirement_claim_status(ReqId, ClaimKey, Status),
|
|
304
|
+
Status \== modeled
|
|
305
|
+
-> format(string(Reason), "claim ~w of ~w is ~w; resolve it before proving it",
|
|
306
|
+
[ClaimKey, ReqId, Status]),
|
|
307
|
+
Result = unresolved(Reason)
|
|
308
|
+
; \+ claim_fact(ReqId, ClaimKey, _)
|
|
309
|
+
-> format(string(Reason), "claim ~w of ~w has no grounding fact", [ClaimKey, ReqId]),
|
|
310
|
+
Result = unresolved(Reason)
|
|
311
|
+
; claim_fact(ReqId, ClaimKey, FactId),
|
|
312
|
+
fact_obligation(FactId, ProofKind, FactResult),
|
|
313
|
+
( FactResult = obligation(Dict)
|
|
314
|
+
-> Result = obligation(FactId, Dict)
|
|
315
|
+
; FactResult == attested
|
|
316
|
+
-> Result = attested(FactId)
|
|
317
|
+
; FactResult = incompatible(Reason)
|
|
318
|
+
-> Result = incompatible(FactId, Reason)
|
|
319
|
+
)
|
|
320
|
+
).
|
|
321
|
+
|
|
322
|
+
%% obligation_equal(+Left, +Right)
|
|
323
|
+
% Two obligations (dicts, possibly read back from a receipt) ask the same.
|
|
324
|
+
obligation_equal(Left, Right) :-
|
|
325
|
+
obligation_key(Left, Key),
|
|
326
|
+
obligation_key(Right, Key).
|
|
327
|
+
|
|
328
|
+
obligation_key(Obligation, set(Members)) :-
|
|
329
|
+
dict_text(Obligation, kind, "set_equals"),
|
|
330
|
+
get_dict(members, Obligation, Raw),
|
|
331
|
+
is_list(Raw),
|
|
332
|
+
maplist(text_string, Raw, Strings),
|
|
333
|
+
sort(Strings, Members),
|
|
334
|
+
!.
|
|
335
|
+
obligation_key(Obligation, required_fields(Members)) :-
|
|
336
|
+
dict_text(Obligation, kind, "required_fields"),
|
|
337
|
+
obligation_members(Obligation, Members),
|
|
338
|
+
!.
|
|
339
|
+
obligation_key(Obligation, every(Predicate, Members)) :-
|
|
340
|
+
dict_text(Obligation, kind, "every_member"),
|
|
341
|
+
dict_text(Obligation, predicate, Predicate),
|
|
342
|
+
obligation_members(Obligation, Members),
|
|
343
|
+
!.
|
|
344
|
+
obligation_key(Obligation, nullable_fields(Members)) :-
|
|
345
|
+
dict_text(Obligation, kind, "nullable_fields"),
|
|
346
|
+
obligation_members(Obligation, Members),
|
|
347
|
+
!.
|
|
348
|
+
obligation_key(Obligation, relation(Operator, Members)) :-
|
|
349
|
+
dict_text(Obligation, kind, "set_relation"),
|
|
350
|
+
dict_text(Obligation, operator, Operator),
|
|
351
|
+
obligation_members(Obligation, Members),
|
|
352
|
+
!.
|
|
353
|
+
obligation_key(Obligation, middleware(Operator, Members)) :-
|
|
354
|
+
dict_text(Obligation, kind, "route_middleware"),
|
|
355
|
+
dict_text(Obligation, operator, Operator),
|
|
356
|
+
obligation_members(Obligation, Members),
|
|
357
|
+
!.
|
|
358
|
+
obligation_key(Obligation, machine(Projection, Operator, Members)) :-
|
|
359
|
+
dict_text(Obligation, kind, "machine_relation"),
|
|
360
|
+
dict_text(Obligation, projection, Projection),
|
|
361
|
+
dict_text(Obligation, operator, Operator),
|
|
362
|
+
obligation_members(Obligation, Members),
|
|
363
|
+
!.
|
|
364
|
+
obligation_key(Obligation, machine(Projection, Members)) :-
|
|
365
|
+
dict_text(Obligation, kind, "machine_set"),
|
|
366
|
+
dict_text(Obligation, projection, Projection),
|
|
367
|
+
obligation_members(Obligation, Members),
|
|
368
|
+
!.
|
|
369
|
+
obligation_key(Obligation, rule(FactId, Hash)) :-
|
|
370
|
+
dict_text(Obligation, kind, "rule_holds"),
|
|
371
|
+
dict_text(Obligation, fact_id, FactId),
|
|
372
|
+
dict_text(Obligation, rule_hash, Hash),
|
|
373
|
+
!.
|
|
374
|
+
obligation_key(Obligation, predicate(Name, Args, Value)) :-
|
|
375
|
+
dict_text(Obligation, kind, "predicate_holds"),
|
|
376
|
+
dict_text(Obligation, predicate, Name),
|
|
377
|
+
get_dict(args, Obligation, RawArgs),
|
|
378
|
+
is_list(RawArgs),
|
|
379
|
+
maplist(text_string, RawArgs, Args),
|
|
380
|
+
dict_text(Obligation, value, Value),
|
|
381
|
+
!.
|
|
382
|
+
obligation_key(Obligation, string(Operator, Value, Polarity)) :-
|
|
383
|
+
dict_text(Obligation, kind, "string_constraint"),
|
|
384
|
+
dict_text(Obligation, operator, Operator),
|
|
385
|
+
dict_text(Obligation, value, Value),
|
|
386
|
+
dict_text(Obligation, polarity, Polarity),
|
|
387
|
+
!.
|
|
388
|
+
obligation_key(Obligation, numeric(Variable, Type, Operator, Value)) :-
|
|
389
|
+
dict_text(Obligation, kind, "numeric_constraint"),
|
|
390
|
+
dict_text(Obligation, variable, Variable),
|
|
391
|
+
dict_text(Obligation, value_type, Type),
|
|
392
|
+
dict_text(Obligation, operator, Operator),
|
|
393
|
+
get_dict(value, Obligation, Raw),
|
|
394
|
+
number_value(Raw, Value).
|
|
395
|
+
|
|
396
|
+
obligation_members(Obligation, Members) :-
|
|
397
|
+
get_dict(members, Obligation, Raw),
|
|
398
|
+
is_list(Raw),
|
|
399
|
+
maplist(text_string, Raw, Strings),
|
|
400
|
+
sort(Strings, Members).
|
|
401
|
+
|
|
402
|
+
dict_text(Dict, Key, String) :-
|
|
403
|
+
is_dict(Dict),
|
|
404
|
+
get_dict(Key, Dict, Raw),
|
|
405
|
+
text_string(Raw, String).
|
|
406
|
+
|
|
407
|
+
number_value(Raw, Value) :- number(Raw), !, Value is Raw * 1.0.
|
|
408
|
+
number_value(Raw, Value) :-
|
|
409
|
+
text_string(Raw, String),
|
|
410
|
+
number_string(Number, String),
|
|
411
|
+
Value is Number * 1.0.
|
|
412
|
+
|
|
413
|
+
text_string(Raw, String) :- string(Raw), !, String = Raw.
|
|
414
|
+
text_string(Raw, String) :- atom(Raw), !, atom_string(Raw, String).
|
|
415
|
+
text_string(Raw, String) :- number(Raw), !, number_string(Raw, String).
|
|
416
|
+
text_string(^^(Raw, _), String) :- !, text_string(Raw, String).
|
|
417
|
+
|
|
418
|
+
% The members of a set fact, sorted and unique, and its operator: eq (the
|
|
419
|
+
% set is exactly these members), includes (it contains at least these),
|
|
420
|
+
% subset_of (it contains only these) or excludes (it contains none of them).
|
|
421
|
+
fact_set(FactId, Members, Operator) :-
|
|
422
|
+
kb_entity(FactId, fact, Props),
|
|
423
|
+
prop_atom(Props, fact_kind, property_value),
|
|
424
|
+
( prop_atom(Props, value_type, set)
|
|
425
|
+
-> memberchk(value_set=Raw, Props),
|
|
426
|
+
kb:normalize_literal_atom_list(Raw, Atoms)
|
|
427
|
+
; prop_atom(Props, property_key, allowed_values),
|
|
428
|
+
\+ (memberchk(value_type=RawType, Props),
|
|
429
|
+
kb:normalize_term_atom(RawType, Type),
|
|
430
|
+
Type \== string),
|
|
431
|
+
memberchk(value_string=RawValue, Props),
|
|
432
|
+
kb:unwrap_rdf_value(RawValue, Value),
|
|
433
|
+
text_string(Value, Joined),
|
|
434
|
+
split_string(Joined, "|", " ", Parts),
|
|
435
|
+
exclude(==(""), Parts, NonEmpty),
|
|
436
|
+
maplist([S, A]>>atom_string(A, S), NonEmpty, Atoms)
|
|
437
|
+
),
|
|
438
|
+
( prop_atom(Props, operator, Operator0) -> Operator = Operator0 ; Operator = eq ),
|
|
439
|
+
memberchk(Operator, [eq, includes, subset_of, excludes]),
|
|
440
|
+
\+ prop_atom(Props, polarity, forbid),
|
|
441
|
+
maplist([A, S]>>atom_string(A, S), Atoms, Strings),
|
|
442
|
+
sort(Strings, Members),
|
|
443
|
+
Members \== [].
|
|
444
|
+
|
|
445
|
+
% A numeric property_value fact as one constraint on its property, negated
|
|
446
|
+
% for polarity forbid (forbid gt 5 means the value must be at most 5).
|
|
447
|
+
fact_numeric_constraint(FactId, Variable, ValueType, Operator, Value) :-
|
|
448
|
+
kb:fact_property_tuple(FactId, _Subject, Property, Op0, Type0, Raw, _Unit, _Scope, Polarity),
|
|
449
|
+
memberchk(Type0-ValueType, [int-"int", number-"real"]),
|
|
450
|
+
memberchk(Op0, [eq, neq, lt, lte, gt, gte]),
|
|
451
|
+
number_value(Raw, Float),
|
|
452
|
+
( ValueType == "int", Float =:= truncate(Float)
|
|
453
|
+
-> Value is truncate(Float)
|
|
454
|
+
; Value = Float
|
|
455
|
+
),
|
|
456
|
+
( Polarity == forbid
|
|
457
|
+
-> negated_operator(Op0, Op)
|
|
458
|
+
; Op = Op0
|
|
459
|
+
),
|
|
460
|
+
atom_string(Op, Operator),
|
|
461
|
+
atom_string(Property, Variable).
|
|
462
|
+
|
|
463
|
+
negated_operator(eq, neq).
|
|
464
|
+
negated_operator(neq, eq).
|
|
465
|
+
negated_operator(lt, gte).
|
|
466
|
+
negated_operator(lte, gt).
|
|
467
|
+
negated_operator(gt, lte).
|
|
468
|
+
negated_operator(gte, lt).
|
|
469
|
+
|
|
470
|
+
prop_atom(Props, Key, Atom) :-
|
|
471
|
+
memberchk(Key=Raw, Props),
|
|
472
|
+
kb:normalize_term_atom(Raw, Atom).
|
|
473
|
+
|
|
474
|
+
% The status of a claim in the requirement's semantic inventory.
|
|
475
|
+
% With ClaimKey unbound it enumerates the claims; the first entry for a key
|
|
476
|
+
% wins.
|
|
477
|
+
requirement_claim_status(ReqId, ClaimKey, Status) :-
|
|
478
|
+
nonvar(ClaimKey),
|
|
479
|
+
!,
|
|
480
|
+
once(inventory_claim_status(ReqId, ClaimKey, Status0)),
|
|
481
|
+
Status = Status0.
|
|
482
|
+
requirement_claim_status(ReqId, ClaimKey, Status) :-
|
|
483
|
+
findall(Key-S, inventory_claim_status(ReqId, Key, S), Pairs),
|
|
484
|
+
first_per_key(Pairs, [], Unique),
|
|
485
|
+
member(ClaimKey-Status, Unique).
|
|
486
|
+
|
|
487
|
+
first_per_key([], Acc, Unique) :- reverse(Acc, Unique).
|
|
488
|
+
first_per_key([Key-S|Rest], Acc, Unique) :-
|
|
489
|
+
( memberchk(Key-_, Acc)
|
|
490
|
+
-> first_per_key(Rest, Acc, Unique)
|
|
491
|
+
; first_per_key(Rest, [Key-S|Acc], Unique)
|
|
492
|
+
).
|
|
493
|
+
|
|
494
|
+
inventory_claim_status(ReqId, ClaimKey, Status) :-
|
|
495
|
+
kb_entity(ReqId, req, Props),
|
|
496
|
+
memberchk(semantic_inventory=Raw, Props),
|
|
497
|
+
inventory_list(Raw, Entries),
|
|
498
|
+
member(Entry, Entries),
|
|
499
|
+
entry_field(Entry, claim_key, RawKey),
|
|
500
|
+
kb:normalize_term_atom(RawKey, ClaimKey),
|
|
501
|
+
entry_field(Entry, status, RawStatus),
|
|
502
|
+
kb:normalize_term_atom(RawStatus, Status).
|
|
503
|
+
|
|
504
|
+
inventory_list(Raw, Entries) :- is_list(Raw), !, Entries = Raw.
|
|
505
|
+
inventory_list(^^(Raw, _), Entries) :- !, inventory_list(Raw, Entries).
|
|
506
|
+
inventory_list(Raw, Entries) :-
|
|
507
|
+
( string(Raw) ; atom(Raw) ),
|
|
508
|
+
catch(atom_json_dict(Raw, Parsed, [value_string_as(string)]), _, fail),
|
|
509
|
+
is_list(Parsed),
|
|
510
|
+
!,
|
|
511
|
+
Entries = Parsed.
|
|
512
|
+
inventory_list(Raw, Entries) :-
|
|
513
|
+
( string(Raw) ; atom(Raw) ),
|
|
514
|
+
catch(term_string(Parsed, Raw), _, fail),
|
|
515
|
+
is_list(Parsed),
|
|
516
|
+
!,
|
|
517
|
+
Entries = Parsed.
|
|
518
|
+
inventory_list(_, []).
|
|
519
|
+
|
|
520
|
+
entry_field(Entry, Key, Value) :-
|
|
521
|
+
is_dict(Entry), !,
|
|
522
|
+
get_dict(Key, Entry, Value).
|
|
523
|
+
entry_field(Entry, Key, Value) :-
|
|
524
|
+
is_list(Entry),
|
|
525
|
+
memberchk(Key=Value, Entry).
|
|
526
|
+
|
|
527
|
+
% The fact grounding a claim: the same claim_key, linked from the
|
|
528
|
+
% requirement through requires_property, requires_predicate or requires_rule.
|
|
529
|
+
claim_fact(ReqId, ClaimKey, FactId) :-
|
|
530
|
+
member(Relation, [requires_property, requires_predicate, requires_rule]),
|
|
531
|
+
kb_relationship(Relation, ReqId, FactId),
|
|
532
|
+
kb_entity(FactId, fact, Props),
|
|
533
|
+
prop_atom(Props, claim_key, ClaimKey),
|
|
534
|
+
!.
|
|
535
|
+
|
|
536
|
+
% Current requirements a test reaches through the scenarios it verifies.
|
|
537
|
+
test_requirement(TestId, ReqId) :-
|
|
538
|
+
( kb_relationship(validates, TestId, ScenarioId)
|
|
539
|
+
; kb_relationship(verified_by, ScenarioId, TestId)
|
|
540
|
+
),
|
|
541
|
+
kb_relationship(specified_by, ReqId, ScenarioId),
|
|
542
|
+
kb:current_req(ReqId).
|
|
543
|
+
|
|
544
|
+
%% test_claim_obligations_json(+TestId, +ClaimKeysJson, +ProofKind, -Json)
|
|
545
|
+
% For `kibi prove`: one row per claim key and requirement the test reaches,
|
|
546
|
+
% {claim_key, requirement_id, fact_id?, result, obligation?, reason?}, with
|
|
547
|
+
% result obligation, attested, incompatible or unresolved. A key no reached
|
|
548
|
+
% requirement declares gives one unresolved row without requirement_id.
|
|
549
|
+
test_claim_obligations_json(TestId0, KeysJson, Kind0, Json) :-
|
|
550
|
+
kb:normalize_term_atom(TestId0, TestId),
|
|
551
|
+
kb:normalize_term_atom(Kind0, Kind),
|
|
552
|
+
atom_json_term(KeysJson, Keys0, []),
|
|
553
|
+
maplist([K, A]>>(text_string(K, S), atom_string(A, S)), Keys0, Keys),
|
|
554
|
+
findall(ReqId, test_requirement(TestId, ReqId), ReqIds0),
|
|
555
|
+
sort(ReqIds0, ReqIds),
|
|
556
|
+
foldl(key_rows(Kind, ReqIds), Keys, [], RowsRev),
|
|
557
|
+
reverse(RowsRev, Rows),
|
|
558
|
+
with_output_to(string(Json), json_write_dict(current_output, Rows, [width(0)])).
|
|
559
|
+
|
|
560
|
+
key_rows(Kind, ReqIds, Key, Acc0, Acc) :-
|
|
561
|
+
findall(ReqId,
|
|
562
|
+
(member(ReqId, ReqIds), requirement_claim_status(ReqId, Key, _)),
|
|
563
|
+
Owners),
|
|
564
|
+
atom_string(Key, KeyString),
|
|
565
|
+
( Owners == []
|
|
566
|
+
-> format(string(Reason),
|
|
567
|
+
"claim ~w is not a claim of any current requirement this test reaches through its scenarios",
|
|
568
|
+
[Key]),
|
|
569
|
+
Acc = [_{claim_key: KeyString, result: "unresolved", reason: Reason}|Acc0]
|
|
570
|
+
; foldl(owner_row(Kind, Key, KeyString), Owners, Acc0, Acc)
|
|
571
|
+
).
|
|
572
|
+
|
|
573
|
+
owner_row(Kind, Key, KeyString, ReqId, Acc0, [Row|Acc0]) :-
|
|
574
|
+
atom_string(ReqId, ReqString),
|
|
575
|
+
claim_obligation(ReqId-Key, Kind, Result),
|
|
576
|
+
Base = _{claim_key: KeyString, requirement_id: ReqString},
|
|
577
|
+
( Result = obligation(FactId, Obligation)
|
|
578
|
+
-> atom_string(FactId, FactString),
|
|
579
|
+
Row = Base.put(_{result: "obligation", fact_id: FactString, obligation: Obligation})
|
|
580
|
+
; Result = attested(FactId)
|
|
581
|
+
-> atom_string(FactId, FactString),
|
|
582
|
+
Row = Base.put(_{result: "attested", fact_id: FactString})
|
|
583
|
+
; Result = incompatible(FactId, Reason)
|
|
584
|
+
-> atom_string(FactId, FactString),
|
|
585
|
+
Row = Base.put(_{result: "incompatible", fact_id: FactString, reason: Reason})
|
|
586
|
+
; Result = unresolved(Reason),
|
|
587
|
+
Row = Base.put(_{result: "unresolved", reason: Reason})
|
|
588
|
+
).
|
|
589
|
+
|
|
590
|
+
%% proof_contract_claim_issues_json(-Json)
|
|
591
|
+
% For the proof-contract-claims rule: one row {test_id, claim_key?, reason}
|
|
592
|
+
% per certificate-kind proof contract that names no claims in proves, and
|
|
593
|
+
% per claim key it names that no certificate of its kind could prove
|
|
594
|
+
% (not a modeled claim of a reached requirement, no grounding fact, or a
|
|
595
|
+
% fact the kind cannot express). An attested claim is not an issue here:
|
|
596
|
+
% it is reported as attested, and never counts as proved.
|
|
597
|
+
proof_contract_claim_issues_json(Json) :-
|
|
598
|
+
findall(Row, proof_contract_claim_issue(Row), Rows0),
|
|
599
|
+
sort(Rows0, Rows),
|
|
600
|
+
with_output_to(string(Json), json_write_dict(current_output, Rows, [width(0)])).
|
|
601
|
+
|
|
602
|
+
certificate_kind(exhaustive).
|
|
603
|
+
certificate_kind(typecheck).
|
|
604
|
+
certificate_kind(arithmetic).
|
|
605
|
+
certificate_kind(smt).
|
|
606
|
+
certificate_kind(rule).
|
|
607
|
+
certificate_kind(state_machine).
|
|
608
|
+
certificate_kind(regex).
|
|
609
|
+
certificate_kind(source).
|
|
610
|
+
|
|
611
|
+
test_certificate_contract(TestId, Kind, Contract) :-
|
|
612
|
+
kb_entity(TestId, test, Props),
|
|
613
|
+
memberchk(proof_contract=Raw, Props),
|
|
614
|
+
contract_dict(Raw, Contract),
|
|
615
|
+
get_dict(kind, Contract, RawKind),
|
|
616
|
+
text_string(RawKind, KindString),
|
|
617
|
+
atom_string(Kind, KindString),
|
|
618
|
+
certificate_kind(Kind).
|
|
619
|
+
|
|
620
|
+
contract_dict(^^(Raw, _), Dict) :- !, contract_dict(Raw, Dict).
|
|
621
|
+
contract_dict(Dict, Dict) :- is_dict(Dict), !.
|
|
622
|
+
contract_dict(Raw, Dict) :-
|
|
623
|
+
( atom(Raw) ; string(Raw) ),
|
|
624
|
+
catch(atom_json_dict(Raw, Dict, [value_string_as(string)]), _, fail),
|
|
625
|
+
is_dict(Dict).
|
|
626
|
+
|
|
627
|
+
proof_contract_claim_issue(_{test_id: TestString, reason: Reason}) :-
|
|
628
|
+
test_certificate_contract(TestId, Kind, Contract),
|
|
629
|
+
\+ ( get_dict(proves, Contract, Proves), is_list(Proves), Proves \== [] ),
|
|
630
|
+
atom_string(TestId, TestString),
|
|
631
|
+
format(string(Reason),
|
|
632
|
+
"the ~w proof contract names no claims in proves, so its certificate proves nothing about a requirement",
|
|
633
|
+
[Kind]).
|
|
634
|
+
proof_contract_claim_issue(_{test_id: TestString, claim_key: KeyString, reason: Reason}) :-
|
|
635
|
+
test_certificate_contract(TestId, Kind, Contract),
|
|
636
|
+
get_dict(proves, Contract, Proves),
|
|
637
|
+
is_list(Proves),
|
|
638
|
+
Proves \== [],
|
|
639
|
+
atom_json_term(KeysJson, Proves, []),
|
|
640
|
+
test_claim_obligations_json(TestId, KeysJson, Kind, RowsJson),
|
|
641
|
+
atom_json_dict(RowsJson, Rows, [value_string_as(string)]),
|
|
642
|
+
member(Row, Rows),
|
|
643
|
+
get_dict(result, Row, Result),
|
|
644
|
+
memberchk(Result, ["unresolved", "incompatible"]),
|
|
645
|
+
atom_string(TestId, TestString),
|
|
646
|
+
get_dict(claim_key, Row, KeyString),
|
|
647
|
+
get_dict(reason, Row, Reason).
|
|
648
|
+
|
|
649
|
+
%% proof_contract_claim_candidates_json(-Json)
|
|
650
|
+
% For the schema 12 migration: one row {test_id, kind, candidates} per
|
|
651
|
+
% certificate-kind proof contract without proves. Candidates are the
|
|
652
|
+
% modeled claims {claim_key, requirement_id, fact_id, binding, reason?} of
|
|
653
|
+
% current requirements the test reaches through its scenarios, with the
|
|
654
|
+
% binding a certificate of its kind would get: derived when the grounding
|
|
655
|
+
% fact derives an obligation for the kind, attested when the fact has no
|
|
656
|
+
% derivation, incompatible (with the reason) when the kind cannot express
|
|
657
|
+
% it. The migration pins proves only when exactly one claim is derived;
|
|
658
|
+
% the review lists every candidate so a person can bind the rest.
|
|
659
|
+
proof_contract_claim_candidates_json(Json) :-
|
|
660
|
+
findall(Row, proof_contract_claim_candidates(Row), Rows0),
|
|
661
|
+
sort(Rows0, Rows),
|
|
662
|
+
with_output_to(string(Json), json_write_dict(current_output, Rows, [width(0)])).
|
|
663
|
+
|
|
664
|
+
proof_contract_claim_candidates(_{test_id: TestString, kind: KindString, candidates: Candidates}) :-
|
|
665
|
+
test_certificate_contract(TestId, Kind, Contract),
|
|
666
|
+
\+ ( get_dict(proves, Contract, Proves), is_list(Proves), Proves \== [] ),
|
|
667
|
+
findall(Candidate,
|
|
668
|
+
( test_requirement(TestId, ReqId),
|
|
669
|
+
requirement_claim_status(ReqId, ClaimKey, modeled),
|
|
670
|
+
claim_obligation(ReqId-ClaimKey, Kind, Result),
|
|
671
|
+
claim_candidate(ReqId, ClaimKey, Result, Candidate)
|
|
672
|
+
),
|
|
673
|
+
Candidates0),
|
|
674
|
+
sort(Candidates0, Candidates1),
|
|
675
|
+
sort(claim_key, @=<, Candidates1, Candidates),
|
|
676
|
+
atom_string(TestId, TestString),
|
|
677
|
+
atom_string(Kind, KindString).
|
|
678
|
+
|
|
679
|
+
claim_candidate(ReqId, ClaimKey, Result, Candidate) :-
|
|
680
|
+
( Result = obligation(FactId, _)
|
|
681
|
+
-> Binding = "derived"
|
|
682
|
+
; Result = attested(FactId)
|
|
683
|
+
-> Binding = "attested"
|
|
684
|
+
; Result = incompatible(FactId, Reason)
|
|
685
|
+
-> Binding = "incompatible"
|
|
686
|
+
),
|
|
687
|
+
atom_string(ClaimKey, KeyString),
|
|
688
|
+
atom_string(ReqId, ReqString),
|
|
689
|
+
atom_string(FactId, FactString),
|
|
690
|
+
Base = _{claim_key: KeyString, requirement_id: ReqString, fact_id: FactString, binding: Binding},
|
|
691
|
+
( var(Reason) -> Candidate = Base ; Candidate = Base.put(reason, Reason) ).
|
|
692
|
+
|
|
693
|
+
%% claim_model_check_json(+Kind, +ObligationJson, +CertificateJson, -Json)
|
|
694
|
+
% For `kibi prove`: whether a rule or state_machine certificate's model meets
|
|
695
|
+
% one derived obligation, {"ok": true} or {"ok": false, "reason": R}.
|
|
696
|
+
claim_model_check_json(Kind0, ObligationJson, CertificateJson, Json) :-
|
|
697
|
+
kb:normalize_term_atom(Kind0, Kind),
|
|
698
|
+
atom_json_dict(ObligationJson, Obligation, [value_string_as(string)]),
|
|
699
|
+
atom_json_dict(CertificateJson, Certificate, [value_string_as(string)]),
|
|
700
|
+
model_check_obligation(Kind, Obligation, Certificate, Result),
|
|
701
|
+
( Result == ok
|
|
702
|
+
-> Out = _{ok: true}
|
|
703
|
+
; Result = problem(Reason),
|
|
704
|
+
Out = _{ok: false, reason: Reason}
|
|
705
|
+
),
|
|
706
|
+
with_output_to(string(Json), json_write_dict(current_output, Out, [width(0)])).
|