kibi-core 0.22.5 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/schema/entities.pl +6 -0
- package/schema/validation.pl +4 -1
- package/src/certificate_check.pl +783 -0
- package/src/certificates.pl +245 -0
- package/src/checks.pl +23 -5
- package/src/claim_obligations.pl +378 -0
- package/src/code_certificates.pl +297 -0
- package/src/discovery.pl +176 -9
- package/src/intervals.pl +11 -0
- package/src/kb.pl +7 -0
- package/src/kb_consistency.pl +213 -0
- package/src/requirement_proof.pl +628 -33
- package/src/rule_registry.pl +20 -0
- package/src/semantic_quality.pl +1 -1
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
% Module: claim_obligations
|
|
2
|
+
% implements REQ-kibi-claim-bound-proofs
|
|
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
|
+
% numeric fact (value_type int or number, a comparison operator)
|
|
12
|
+
% arithmetic, smt the certificate's claim is the fact's constraint
|
|
13
|
+
% (negated for polarity forbid)
|
|
14
|
+
%
|
|
15
|
+
% Any other pairing that appears in the table with a different proof kind is
|
|
16
|
+
% incompatible (a set fact under arithmetic, a numeric fact under
|
|
17
|
+
% exhaustive). A fact kind with no derivation at all (boolean placeholders,
|
|
18
|
+
% predicates, rules, other strings) can only be attested: the test author
|
|
19
|
+
% says the certificate expresses the claim, and Kibi never counts that as
|
|
20
|
+
% proved.
|
|
21
|
+
|
|
22
|
+
:- module(claim_obligations, [
|
|
23
|
+
claim_obligation/3,
|
|
24
|
+
fact_obligation/3,
|
|
25
|
+
obligation_equal/2,
|
|
26
|
+
proof_contract_claim_candidates_json/1,
|
|
27
|
+
proof_contract_claim_issues_json/1,
|
|
28
|
+
test_claim_obligations_json/4
|
|
29
|
+
]).
|
|
30
|
+
|
|
31
|
+
:- use_module(library(http/json)).
|
|
32
|
+
:- use_module(library(apply)).
|
|
33
|
+
:- use_module(library(lists)).
|
|
34
|
+
:- use_module('kb.pl').
|
|
35
|
+
|
|
36
|
+
%% fact_obligation(+FactId, +ProofKind, -Result)
|
|
37
|
+
% Result is obligation(Dict), attested, or incompatible(Reason).
|
|
38
|
+
fact_obligation(FactId, ProofKind, Result) :-
|
|
39
|
+
( fact_set(FactId, Members)
|
|
40
|
+
-> ( memberchk(ProofKind, [exhaustive, typecheck])
|
|
41
|
+
-> Result = obligation(_{kind: "set_equals", members: Members})
|
|
42
|
+
; format(string(Reason),
|
|
43
|
+
"~w is a set fact, which an ~w certificate cannot express (use exhaustive or typecheck)",
|
|
44
|
+
[FactId, ProofKind]),
|
|
45
|
+
Result = incompatible(Reason)
|
|
46
|
+
)
|
|
47
|
+
; fact_numeric_constraint(FactId, Variable, ValueType, Operator, Value)
|
|
48
|
+
-> ( memberchk(ProofKind, [arithmetic, smt])
|
|
49
|
+
-> Result = obligation(_{kind: "numeric_constraint", variable: Variable,
|
|
50
|
+
value_type: ValueType, operator: Operator, value: Value})
|
|
51
|
+
; ( ProofKind == typecheck -> Article = a ; Article = an ),
|
|
52
|
+
format(string(Reason),
|
|
53
|
+
"~w is a numeric fact, which ~w ~w certificate cannot express (use arithmetic or smt)",
|
|
54
|
+
[FactId, Article, ProofKind]),
|
|
55
|
+
Result = incompatible(Reason)
|
|
56
|
+
)
|
|
57
|
+
; Result = attested
|
|
58
|
+
).
|
|
59
|
+
|
|
60
|
+
%% claim_obligation(+ReqId, +ClaimKey, +ProofKind, -Result)
|
|
61
|
+
% As fact_obligation/3 for the fact grounding ClaimKey in ReqId, with the
|
|
62
|
+
% fact id: obligation(FactId, Dict), attested(FactId), incompatible(FactId,
|
|
63
|
+
% Reason), or unresolved(Reason) when the claim cannot be named by a proof.
|
|
64
|
+
claim_obligation(ReqId-ClaimKey, ProofKind, Result) :-
|
|
65
|
+
( \+ requirement_claim_status(ReqId, ClaimKey, _)
|
|
66
|
+
-> format(string(Reason), "~w is not a claim of ~w", [ClaimKey, ReqId]),
|
|
67
|
+
Result = unresolved(Reason)
|
|
68
|
+
; requirement_claim_status(ReqId, ClaimKey, nonlogical)
|
|
69
|
+
-> format(string(Reason),
|
|
70
|
+
"claim ~w of ~w is nonlogical: a nonlogical span is context, not a claim to prove",
|
|
71
|
+
[ClaimKey, ReqId]),
|
|
72
|
+
Result = unresolved(Reason)
|
|
73
|
+
; requirement_claim_status(ReqId, ClaimKey, Status),
|
|
74
|
+
Status \== modeled
|
|
75
|
+
-> format(string(Reason), "claim ~w of ~w is ~w; resolve it before proving it",
|
|
76
|
+
[ClaimKey, ReqId, Status]),
|
|
77
|
+
Result = unresolved(Reason)
|
|
78
|
+
; \+ claim_fact(ReqId, ClaimKey, _)
|
|
79
|
+
-> format(string(Reason), "claim ~w of ~w has no grounding fact", [ClaimKey, ReqId]),
|
|
80
|
+
Result = unresolved(Reason)
|
|
81
|
+
; claim_fact(ReqId, ClaimKey, FactId),
|
|
82
|
+
fact_obligation(FactId, ProofKind, FactResult),
|
|
83
|
+
( FactResult = obligation(Dict)
|
|
84
|
+
-> Result = obligation(FactId, Dict)
|
|
85
|
+
; FactResult == attested
|
|
86
|
+
-> Result = attested(FactId)
|
|
87
|
+
; FactResult = incompatible(Reason)
|
|
88
|
+
-> Result = incompatible(FactId, Reason)
|
|
89
|
+
)
|
|
90
|
+
).
|
|
91
|
+
|
|
92
|
+
%% obligation_equal(+Left, +Right)
|
|
93
|
+
% Two obligations (dicts, possibly read back from a receipt) ask the same.
|
|
94
|
+
obligation_equal(Left, Right) :-
|
|
95
|
+
obligation_key(Left, Key),
|
|
96
|
+
obligation_key(Right, Key).
|
|
97
|
+
|
|
98
|
+
obligation_key(Obligation, set(Members)) :-
|
|
99
|
+
dict_text(Obligation, kind, "set_equals"),
|
|
100
|
+
get_dict(members, Obligation, Raw),
|
|
101
|
+
is_list(Raw),
|
|
102
|
+
maplist(text_string, Raw, Strings),
|
|
103
|
+
sort(Strings, Members),
|
|
104
|
+
!.
|
|
105
|
+
obligation_key(Obligation, numeric(Variable, Type, Operator, Value)) :-
|
|
106
|
+
dict_text(Obligation, kind, "numeric_constraint"),
|
|
107
|
+
dict_text(Obligation, variable, Variable),
|
|
108
|
+
dict_text(Obligation, value_type, Type),
|
|
109
|
+
dict_text(Obligation, operator, Operator),
|
|
110
|
+
get_dict(value, Obligation, Raw),
|
|
111
|
+
number_value(Raw, Value).
|
|
112
|
+
|
|
113
|
+
dict_text(Dict, Key, String) :-
|
|
114
|
+
is_dict(Dict),
|
|
115
|
+
get_dict(Key, Dict, Raw),
|
|
116
|
+
text_string(Raw, String).
|
|
117
|
+
|
|
118
|
+
number_value(Raw, Value) :- number(Raw), !, Value is Raw * 1.0.
|
|
119
|
+
number_value(Raw, Value) :-
|
|
120
|
+
text_string(Raw, String),
|
|
121
|
+
number_string(Number, String),
|
|
122
|
+
Value is Number * 1.0.
|
|
123
|
+
|
|
124
|
+
text_string(Raw, String) :- string(Raw), !, String = Raw.
|
|
125
|
+
text_string(Raw, String) :- atom(Raw), !, atom_string(Raw, String).
|
|
126
|
+
text_string(Raw, String) :- number(Raw), !, number_string(Raw, String).
|
|
127
|
+
text_string(^^(Raw, _), String) :- !, text_string(Raw, String).
|
|
128
|
+
|
|
129
|
+
% The members of a set fact, sorted and unique.
|
|
130
|
+
fact_set(FactId, Members) :-
|
|
131
|
+
kb_entity(FactId, fact, Props),
|
|
132
|
+
prop_atom(Props, fact_kind, property_value),
|
|
133
|
+
( prop_atom(Props, value_type, set)
|
|
134
|
+
-> memberchk(value_set=Raw, Props),
|
|
135
|
+
kb:normalize_term_atom_list(Raw, Atoms)
|
|
136
|
+
; prop_atom(Props, property_key, allowed_values),
|
|
137
|
+
\+ (memberchk(value_type=RawType, Props),
|
|
138
|
+
kb:normalize_term_atom(RawType, Type),
|
|
139
|
+
Type \== string),
|
|
140
|
+
memberchk(value_string=RawValue, Props),
|
|
141
|
+
kb:unwrap_rdf_value(RawValue, Value),
|
|
142
|
+
text_string(Value, Joined),
|
|
143
|
+
split_string(Joined, "|", " ", Parts),
|
|
144
|
+
exclude(==(""), Parts, NonEmpty),
|
|
145
|
+
maplist([S, A]>>atom_string(A, S), NonEmpty, Atoms)
|
|
146
|
+
),
|
|
147
|
+
( prop_atom(Props, operator, Operator) -> Operator == eq ; true ),
|
|
148
|
+
\+ prop_atom(Props, polarity, forbid),
|
|
149
|
+
maplist([A, S]>>atom_string(A, S), Atoms, Strings),
|
|
150
|
+
sort(Strings, Members),
|
|
151
|
+
Members \== [].
|
|
152
|
+
|
|
153
|
+
% A numeric property_value fact as one constraint on its property, negated
|
|
154
|
+
% for polarity forbid (forbid gt 5 means the value must be at most 5).
|
|
155
|
+
fact_numeric_constraint(FactId, Variable, ValueType, Operator, Value) :-
|
|
156
|
+
kb:fact_property_tuple(FactId, _Subject, Property, Op0, Type0, Raw, _Unit, _Scope, Polarity),
|
|
157
|
+
memberchk(Type0-ValueType, [int-"int", number-"real"]),
|
|
158
|
+
memberchk(Op0, [eq, neq, lt, lte, gt, gte]),
|
|
159
|
+
number_value(Raw, Float),
|
|
160
|
+
( ValueType == "int", Float =:= truncate(Float)
|
|
161
|
+
-> Value is truncate(Float)
|
|
162
|
+
; Value = Float
|
|
163
|
+
),
|
|
164
|
+
( Polarity == forbid
|
|
165
|
+
-> negated_operator(Op0, Op)
|
|
166
|
+
; Op = Op0
|
|
167
|
+
),
|
|
168
|
+
atom_string(Op, Operator),
|
|
169
|
+
atom_string(Property, Variable).
|
|
170
|
+
|
|
171
|
+
negated_operator(eq, neq).
|
|
172
|
+
negated_operator(neq, eq).
|
|
173
|
+
negated_operator(lt, gte).
|
|
174
|
+
negated_operator(lte, gt).
|
|
175
|
+
negated_operator(gt, lte).
|
|
176
|
+
negated_operator(gte, lt).
|
|
177
|
+
|
|
178
|
+
prop_atom(Props, Key, Atom) :-
|
|
179
|
+
memberchk(Key=Raw, Props),
|
|
180
|
+
kb:normalize_term_atom(Raw, Atom).
|
|
181
|
+
|
|
182
|
+
% The status of a claim in the requirement's semantic inventory.
|
|
183
|
+
% With ClaimKey unbound it enumerates the claims; the first entry for a key
|
|
184
|
+
% wins.
|
|
185
|
+
requirement_claim_status(ReqId, ClaimKey, Status) :-
|
|
186
|
+
nonvar(ClaimKey),
|
|
187
|
+
!,
|
|
188
|
+
once(inventory_claim_status(ReqId, ClaimKey, Status0)),
|
|
189
|
+
Status = Status0.
|
|
190
|
+
requirement_claim_status(ReqId, ClaimKey, Status) :-
|
|
191
|
+
findall(Key-S, inventory_claim_status(ReqId, Key, S), Pairs),
|
|
192
|
+
first_per_key(Pairs, [], Unique),
|
|
193
|
+
member(ClaimKey-Status, Unique).
|
|
194
|
+
|
|
195
|
+
first_per_key([], Acc, Unique) :- reverse(Acc, Unique).
|
|
196
|
+
first_per_key([Key-S|Rest], Acc, Unique) :-
|
|
197
|
+
( memberchk(Key-_, Acc)
|
|
198
|
+
-> first_per_key(Rest, Acc, Unique)
|
|
199
|
+
; first_per_key(Rest, [Key-S|Acc], Unique)
|
|
200
|
+
).
|
|
201
|
+
|
|
202
|
+
inventory_claim_status(ReqId, ClaimKey, Status) :-
|
|
203
|
+
kb_entity(ReqId, req, Props),
|
|
204
|
+
memberchk(semantic_inventory=Raw, Props),
|
|
205
|
+
inventory_list(Raw, Entries),
|
|
206
|
+
member(Entry, Entries),
|
|
207
|
+
entry_field(Entry, claim_key, RawKey),
|
|
208
|
+
kb:normalize_term_atom(RawKey, ClaimKey),
|
|
209
|
+
entry_field(Entry, status, RawStatus),
|
|
210
|
+
kb:normalize_term_atom(RawStatus, Status).
|
|
211
|
+
|
|
212
|
+
inventory_list(Raw, Entries) :- is_list(Raw), !, Entries = Raw.
|
|
213
|
+
inventory_list(^^(Raw, _), Entries) :- !, inventory_list(Raw, Entries).
|
|
214
|
+
inventory_list(Raw, Entries) :-
|
|
215
|
+
( string(Raw) ; atom(Raw) ),
|
|
216
|
+
catch(atom_json_dict(Raw, Parsed, [value_string_as(string)]), _, fail),
|
|
217
|
+
is_list(Parsed),
|
|
218
|
+
!,
|
|
219
|
+
Entries = Parsed.
|
|
220
|
+
inventory_list(Raw, Entries) :-
|
|
221
|
+
( string(Raw) ; atom(Raw) ),
|
|
222
|
+
catch(term_string(Parsed, Raw), _, fail),
|
|
223
|
+
is_list(Parsed),
|
|
224
|
+
!,
|
|
225
|
+
Entries = Parsed.
|
|
226
|
+
inventory_list(_, []).
|
|
227
|
+
|
|
228
|
+
entry_field(Entry, Key, Value) :-
|
|
229
|
+
is_dict(Entry), !,
|
|
230
|
+
get_dict(Key, Entry, Value).
|
|
231
|
+
entry_field(Entry, Key, Value) :-
|
|
232
|
+
is_list(Entry),
|
|
233
|
+
memberchk(Key=Value, Entry).
|
|
234
|
+
|
|
235
|
+
% The fact grounding a claim: the same claim_key, linked from the
|
|
236
|
+
% requirement through requires_property, requires_predicate or requires_rule.
|
|
237
|
+
claim_fact(ReqId, ClaimKey, FactId) :-
|
|
238
|
+
member(Relation, [requires_property, requires_predicate, requires_rule]),
|
|
239
|
+
kb_relationship(Relation, ReqId, FactId),
|
|
240
|
+
kb_entity(FactId, fact, Props),
|
|
241
|
+
prop_atom(Props, claim_key, ClaimKey),
|
|
242
|
+
!.
|
|
243
|
+
|
|
244
|
+
% Current requirements a test reaches through the scenarios it verifies.
|
|
245
|
+
test_requirement(TestId, ReqId) :-
|
|
246
|
+
( kb_relationship(validates, TestId, ScenarioId)
|
|
247
|
+
; kb_relationship(verified_by, ScenarioId, TestId)
|
|
248
|
+
),
|
|
249
|
+
kb_relationship(specified_by, ReqId, ScenarioId),
|
|
250
|
+
kb:current_req(ReqId).
|
|
251
|
+
|
|
252
|
+
%% test_claim_obligations_json(+TestId, +ClaimKeysJson, +ProofKind, -Json)
|
|
253
|
+
% For `kibi prove`: one row per claim key and requirement the test reaches,
|
|
254
|
+
% {claim_key, requirement_id, fact_id?, result, obligation?, reason?}, with
|
|
255
|
+
% result obligation, attested, incompatible or unresolved. A key no reached
|
|
256
|
+
% requirement declares gives one unresolved row without requirement_id.
|
|
257
|
+
test_claim_obligations_json(TestId0, KeysJson, Kind0, Json) :-
|
|
258
|
+
kb:normalize_term_atom(TestId0, TestId),
|
|
259
|
+
kb:normalize_term_atom(Kind0, Kind),
|
|
260
|
+
atom_json_term(KeysJson, Keys0, []),
|
|
261
|
+
maplist([K, A]>>(text_string(K, S), atom_string(A, S)), Keys0, Keys),
|
|
262
|
+
findall(ReqId, test_requirement(TestId, ReqId), ReqIds0),
|
|
263
|
+
sort(ReqIds0, ReqIds),
|
|
264
|
+
foldl(key_rows(Kind, ReqIds), Keys, [], RowsRev),
|
|
265
|
+
reverse(RowsRev, Rows),
|
|
266
|
+
with_output_to(string(Json), json_write_dict(current_output, Rows, [width(0)])).
|
|
267
|
+
|
|
268
|
+
key_rows(Kind, ReqIds, Key, Acc0, Acc) :-
|
|
269
|
+
findall(ReqId,
|
|
270
|
+
(member(ReqId, ReqIds), requirement_claim_status(ReqId, Key, _)),
|
|
271
|
+
Owners),
|
|
272
|
+
atom_string(Key, KeyString),
|
|
273
|
+
( Owners == []
|
|
274
|
+
-> format(string(Reason),
|
|
275
|
+
"claim ~w is not a claim of any current requirement this test reaches through its scenarios",
|
|
276
|
+
[Key]),
|
|
277
|
+
Acc = [_{claim_key: KeyString, result: "unresolved", reason: Reason}|Acc0]
|
|
278
|
+
; foldl(owner_row(Kind, Key, KeyString), Owners, Acc0, Acc)
|
|
279
|
+
).
|
|
280
|
+
|
|
281
|
+
owner_row(Kind, Key, KeyString, ReqId, Acc0, [Row|Acc0]) :-
|
|
282
|
+
atom_string(ReqId, ReqString),
|
|
283
|
+
claim_obligation(ReqId-Key, Kind, Result),
|
|
284
|
+
Base = _{claim_key: KeyString, requirement_id: ReqString},
|
|
285
|
+
( Result = obligation(FactId, Obligation)
|
|
286
|
+
-> atom_string(FactId, FactString),
|
|
287
|
+
Row = Base.put(_{result: "obligation", fact_id: FactString, obligation: Obligation})
|
|
288
|
+
; Result = attested(FactId)
|
|
289
|
+
-> atom_string(FactId, FactString),
|
|
290
|
+
Row = Base.put(_{result: "attested", fact_id: FactString})
|
|
291
|
+
; Result = incompatible(FactId, Reason)
|
|
292
|
+
-> atom_string(FactId, FactString),
|
|
293
|
+
Row = Base.put(_{result: "incompatible", fact_id: FactString, reason: Reason})
|
|
294
|
+
; Result = unresolved(Reason),
|
|
295
|
+
Row = Base.put(_{result: "unresolved", reason: Reason})
|
|
296
|
+
).
|
|
297
|
+
|
|
298
|
+
%% proof_contract_claim_issues_json(-Json)
|
|
299
|
+
% For the proof-contract-claims rule: one row {test_id, claim_key?, reason}
|
|
300
|
+
% per certificate-kind proof contract that names no claims in proves, and
|
|
301
|
+
% per claim key it names that no certificate of its kind could prove
|
|
302
|
+
% (not a modeled claim of a reached requirement, no grounding fact, or a
|
|
303
|
+
% fact the kind cannot express). An attested claim is not an issue here:
|
|
304
|
+
% it is reported as attested, and never counts as proved.
|
|
305
|
+
proof_contract_claim_issues_json(Json) :-
|
|
306
|
+
findall(Row, proof_contract_claim_issue(Row), Rows0),
|
|
307
|
+
sort(Rows0, Rows),
|
|
308
|
+
with_output_to(string(Json), json_write_dict(current_output, Rows, [width(0)])).
|
|
309
|
+
|
|
310
|
+
certificate_kind(exhaustive).
|
|
311
|
+
certificate_kind(typecheck).
|
|
312
|
+
certificate_kind(arithmetic).
|
|
313
|
+
certificate_kind(smt).
|
|
314
|
+
|
|
315
|
+
test_certificate_contract(TestId, Kind, Contract) :-
|
|
316
|
+
kb_entity(TestId, test, Props),
|
|
317
|
+
memberchk(proof_contract=Raw, Props),
|
|
318
|
+
contract_dict(Raw, Contract),
|
|
319
|
+
get_dict(kind, Contract, RawKind),
|
|
320
|
+
text_string(RawKind, KindString),
|
|
321
|
+
atom_string(Kind, KindString),
|
|
322
|
+
certificate_kind(Kind).
|
|
323
|
+
|
|
324
|
+
contract_dict(^^(Raw, _), Dict) :- !, contract_dict(Raw, Dict).
|
|
325
|
+
contract_dict(Dict, Dict) :- is_dict(Dict), !.
|
|
326
|
+
contract_dict(Raw, Dict) :-
|
|
327
|
+
( atom(Raw) ; string(Raw) ),
|
|
328
|
+
catch(atom_json_dict(Raw, Dict, [value_string_as(string)]), _, fail),
|
|
329
|
+
is_dict(Dict).
|
|
330
|
+
|
|
331
|
+
proof_contract_claim_issue(_{test_id: TestString, reason: Reason}) :-
|
|
332
|
+
test_certificate_contract(TestId, Kind, Contract),
|
|
333
|
+
\+ ( get_dict(proves, Contract, Proves), is_list(Proves), Proves \== [] ),
|
|
334
|
+
atom_string(TestId, TestString),
|
|
335
|
+
format(string(Reason),
|
|
336
|
+
"the ~w proof contract names no claims in proves, so its certificate proves nothing about a requirement",
|
|
337
|
+
[Kind]).
|
|
338
|
+
proof_contract_claim_issue(_{test_id: TestString, claim_key: KeyString, reason: Reason}) :-
|
|
339
|
+
test_certificate_contract(TestId, Kind, Contract),
|
|
340
|
+
get_dict(proves, Contract, Proves),
|
|
341
|
+
is_list(Proves),
|
|
342
|
+
Proves \== [],
|
|
343
|
+
atom_json_term(KeysJson, Proves, []),
|
|
344
|
+
test_claim_obligations_json(TestId, KeysJson, Kind, RowsJson),
|
|
345
|
+
atom_json_dict(RowsJson, Rows, [value_string_as(string)]),
|
|
346
|
+
member(Row, Rows),
|
|
347
|
+
get_dict(result, Row, Result),
|
|
348
|
+
memberchk(Result, ["unresolved", "incompatible"]),
|
|
349
|
+
atom_string(TestId, TestString),
|
|
350
|
+
get_dict(claim_key, Row, KeyString),
|
|
351
|
+
get_dict(reason, Row, Reason).
|
|
352
|
+
|
|
353
|
+
%% proof_contract_claim_candidates_json(-Json)
|
|
354
|
+
% For the schema 12 migration: one row {test_id, kind, candidates} per
|
|
355
|
+
% certificate-kind proof contract without proves, where candidates are the
|
|
356
|
+
% claims {claim_key, requirement_id, fact_id} of current requirements the
|
|
357
|
+
% test reaches whose grounding fact derives an obligation for its kind. The
|
|
358
|
+
% migration pins proves only when exactly one claim is a candidate.
|
|
359
|
+
proof_contract_claim_candidates_json(Json) :-
|
|
360
|
+
findall(Row, proof_contract_claim_candidates(Row), Rows0),
|
|
361
|
+
sort(Rows0, Rows),
|
|
362
|
+
with_output_to(string(Json), json_write_dict(current_output, Rows, [width(0)])).
|
|
363
|
+
|
|
364
|
+
proof_contract_claim_candidates(_{test_id: TestString, kind: KindString, candidates: Candidates}) :-
|
|
365
|
+
test_certificate_contract(TestId, Kind, Contract),
|
|
366
|
+
\+ ( get_dict(proves, Contract, Proves), is_list(Proves), Proves \== [] ),
|
|
367
|
+
findall(_{claim_key: KeyString, requirement_id: ReqString, fact_id: FactString},
|
|
368
|
+
( test_requirement(TestId, ReqId),
|
|
369
|
+
requirement_claim_status(ReqId, ClaimKey, modeled),
|
|
370
|
+
claim_obligation(ReqId-ClaimKey, Kind, obligation(FactId, _)),
|
|
371
|
+
atom_string(ClaimKey, KeyString),
|
|
372
|
+
atom_string(ReqId, ReqString),
|
|
373
|
+
atom_string(FactId, FactString)
|
|
374
|
+
),
|
|
375
|
+
Candidates0),
|
|
376
|
+
sort(Candidates0, Candidates),
|
|
377
|
+
atom_string(TestId, TestString),
|
|
378
|
+
atom_string(Kind, KindString).
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
% Module: code_certificates
|
|
2
|
+
% implements REQ-kibi-code-proof-kinds
|
|
3
|
+
%
|
|
4
|
+
% Re-checks the kibi.code-certificate.v1 a certificate-kind proof contract
|
|
5
|
+
% leaves in its receipt. A receipt counts as proved only when its
|
|
6
|
+
% certificate re-checks here, so a test that claims to have covered every
|
|
7
|
+
% case cannot earn proved by saying so. The checks never search:
|
|
8
|
+
%
|
|
9
|
+
% exhaustive The domain lists at least one member, no member twice, and
|
|
10
|
+
% the results name every member exactly once, each passed.
|
|
11
|
+
% (Kibi checks at ingest that the proof imported the domain
|
|
12
|
+
% from its source instead of hand-listing it.)
|
|
13
|
+
% typecheck The witness names a file and its sha256 and the compiler
|
|
14
|
+
% that accepted it; the outcome is passed. (Kibi checks at
|
|
15
|
+
% ingest that the witness is the file in the workspace and
|
|
16
|
+
% imports the domain.)
|
|
17
|
+
% arithmetic The assumptions on one variable are satisfiable and entail
|
|
18
|
+
% the claim, decided exactly by intervals.pl: the assumptions
|
|
19
|
+
% plus the negated claim admit no value.
|
|
20
|
+
% smt The query asks (check-sat) and the solver answered unsat.
|
|
21
|
+
% (Kibi re-runs the solver on the query at ingest.)
|
|
22
|
+
%
|
|
23
|
+
% The digest a receipt records is the sha256 of the certificate's canonical
|
|
24
|
+
% JSON as canonical_json/2 below spells it. The CLI records the digest this
|
|
25
|
+
% module returns at ingest, so ingest and every later re-check hash the
|
|
26
|
+
% certificate the same way.
|
|
27
|
+
|
|
28
|
+
:- module(code_certificates, [
|
|
29
|
+
code_certificate_check/2,
|
|
30
|
+
code_certificate_digest/2,
|
|
31
|
+
code_certificate_check_json/2
|
|
32
|
+
]).
|
|
33
|
+
|
|
34
|
+
:- use_module(library(http/json)).
|
|
35
|
+
:- use_module(library(crypto)).
|
|
36
|
+
:- use_module(library(apply)).
|
|
37
|
+
:- use_module(library(lists)).
|
|
38
|
+
:- use_module('intervals.pl', [
|
|
39
|
+
numeric_constraints_satisfiable/2,
|
|
40
|
+
numeric_constraint_entailed/3
|
|
41
|
+
]).
|
|
42
|
+
|
|
43
|
+
code_certificate_version("kibi.code-certificate.v1").
|
|
44
|
+
|
|
45
|
+
%% code_certificate_check(+Certificate, -Result)
|
|
46
|
+
% Result is ok or rejected(Reason) with Reason a string naming what failed.
|
|
47
|
+
code_certificate_check(Certificate, Result) :-
|
|
48
|
+
( catch(certificate_problem(Certificate, Reason), _, (Reason = "the certificate is malformed"))
|
|
49
|
+
-> Result = rejected(Reason)
|
|
50
|
+
; Result = ok
|
|
51
|
+
).
|
|
52
|
+
|
|
53
|
+
%% code_certificate_digest(+Certificate, -Digest)
|
|
54
|
+
code_certificate_digest(Certificate, Digest) :-
|
|
55
|
+
canonical_json(Certificate, Json),
|
|
56
|
+
crypto_data_hash(Json, Digest, [algorithm(sha256), encoding(utf8)]).
|
|
57
|
+
|
|
58
|
+
%% code_certificate_check_json(+CertificateJson, -ResultJson)
|
|
59
|
+
% Entry point for the CLI: {"accepted": true, "digest": D} or
|
|
60
|
+
% {"accepted": false, "reason": R}.
|
|
61
|
+
code_certificate_check_json(CertificateJson, ResultJson) :-
|
|
62
|
+
( catch(atom_json_dict(CertificateJson, Certificate, [value_string_as(string)]), _, fail),
|
|
63
|
+
is_dict(Certificate)
|
|
64
|
+
-> code_certificate_check(Certificate, Result),
|
|
65
|
+
code_certificate_digest(Certificate, Digest),
|
|
66
|
+
( Result == ok
|
|
67
|
+
-> Out = _{accepted: true, digest: Digest}
|
|
68
|
+
; Result = rejected(Reason),
|
|
69
|
+
Out = _{accepted: false, digest: Digest, reason: Reason}
|
|
70
|
+
)
|
|
71
|
+
; Out = _{accepted: false, reason: "the certificate is not a JSON object"}
|
|
72
|
+
),
|
|
73
|
+
with_output_to(string(ResultJson), json_write_dict(current_output, Out, [width(0)])).
|
|
74
|
+
|
|
75
|
+
% certificate_problem(+Certificate, -Reason) succeeds with the first problem.
|
|
76
|
+
certificate_problem(Certificate, Reason) :-
|
|
77
|
+
\+ is_dict(Certificate), !,
|
|
78
|
+
Reason = "the certificate is not an object".
|
|
79
|
+
certificate_problem(Certificate, Reason) :-
|
|
80
|
+
code_certificate_version(Version),
|
|
81
|
+
\+ get_dict(version, Certificate, Version), !,
|
|
82
|
+
format(string(Reason), "the certificate version is not ~w", [Version]).
|
|
83
|
+
certificate_problem(Certificate, Reason) :-
|
|
84
|
+
\+ (get_dict(test_id, Certificate, TestId), nonempty_string(TestId)), !,
|
|
85
|
+
Reason = "the certificate names no test_id".
|
|
86
|
+
certificate_problem(Certificate, Reason) :-
|
|
87
|
+
( get_dict(kind, Certificate, KindString), string(KindString)
|
|
88
|
+
-> atom_string(Kind, KindString)
|
|
89
|
+
; Kind = none
|
|
90
|
+
),
|
|
91
|
+
( kind_problem(Kind, Certificate, Reason0)
|
|
92
|
+
-> Reason = Reason0
|
|
93
|
+
; \+ memberchk(Kind, [exhaustive, typecheck, arithmetic, smt])
|
|
94
|
+
-> Reason = "the certificate kind is not exhaustive, typecheck, arithmetic or smt"
|
|
95
|
+
).
|
|
96
|
+
|
|
97
|
+
kind_problem(exhaustive, Certificate, Reason) :-
|
|
98
|
+
field(Certificate, domain, Domain),
|
|
99
|
+
field(Domain, symbol_id, Symbol),
|
|
100
|
+
field(Domain, members, Members),
|
|
101
|
+
field(Certificate, results, Results),
|
|
102
|
+
( \+ is_dict(Domain)
|
|
103
|
+
-> Reason = "the certificate has no domain"
|
|
104
|
+
; \+ nonempty_string(Symbol)
|
|
105
|
+
-> Reason = "the domain names no symbol_id"
|
|
106
|
+
; \+ (is_list(Members), Members \== [])
|
|
107
|
+
-> Reason = "the domain lists no members"
|
|
108
|
+
; \+ maplist(nonempty_string, Members)
|
|
109
|
+
-> Reason = "every domain member must be a non-empty string"
|
|
110
|
+
; msort(Members, Sorted), sort(Members, Unique), Sorted \== Unique
|
|
111
|
+
-> Reason = "the domain lists a member twice"
|
|
112
|
+
; \+ is_list(Results)
|
|
113
|
+
-> Reason = "the certificate has no results"
|
|
114
|
+
; \+ maplist(result_row, Results)
|
|
115
|
+
-> Reason = "every result must name a member and an outcome"
|
|
116
|
+
; findall(M, (member(Row, Results), get_dict(member, Row, M)), Named),
|
|
117
|
+
msort(Named, NamedSorted),
|
|
118
|
+
msort(Members, MembersSorted),
|
|
119
|
+
NamedSorted \== MembersSorted
|
|
120
|
+
-> Reason = "the results do not name every domain member exactly once"
|
|
121
|
+
; member(Row, Results),
|
|
122
|
+
get_dict(outcome, Row, Outcome),
|
|
123
|
+
Outcome \== "passed"
|
|
124
|
+
-> get_dict(member, Row, Failed),
|
|
125
|
+
format(string(Reason), "member ~w did not pass", [Failed])
|
|
126
|
+
).
|
|
127
|
+
kind_problem(typecheck, Certificate, Reason) :-
|
|
128
|
+
field(Certificate, witness, Witness),
|
|
129
|
+
field(Witness, path, Path),
|
|
130
|
+
field(Witness, sha256, Hash),
|
|
131
|
+
field(Certificate, domain, Domain),
|
|
132
|
+
field(Domain, symbol_id, Symbol),
|
|
133
|
+
field(Certificate, compiler, Compiler),
|
|
134
|
+
field(Compiler, name, Name),
|
|
135
|
+
field(Compiler, version, CompilerVersion),
|
|
136
|
+
field(Certificate, outcome, Outcome),
|
|
137
|
+
( \+ nonempty_string(Path)
|
|
138
|
+
-> Reason = "the certificate names no witness path"
|
|
139
|
+
; \+ sha256_string(Hash)
|
|
140
|
+
-> Reason = "the witness has no sha256"
|
|
141
|
+
; \+ nonempty_string(Symbol)
|
|
142
|
+
-> Reason = "the certificate names no domain symbol_id"
|
|
143
|
+
; \+ (nonempty_string(Name), nonempty_string(CompilerVersion))
|
|
144
|
+
-> Reason = "the certificate names no compiler and version"
|
|
145
|
+
; Outcome \== "passed"
|
|
146
|
+
-> Reason = "the type checker did not accept the witness"
|
|
147
|
+
).
|
|
148
|
+
kind_problem(arithmetic, Certificate, Reason) :-
|
|
149
|
+
field(Certificate, variable, Variable),
|
|
150
|
+
field(Certificate, value_type, Type),
|
|
151
|
+
field(Certificate, assumptions, Assumptions),
|
|
152
|
+
field(Certificate, claim, Claim),
|
|
153
|
+
( \+ nonempty_string(Variable)
|
|
154
|
+
-> Reason = "the certificate names no variable"
|
|
155
|
+
; \+ memberchk(Type, ["int", "real"])
|
|
156
|
+
-> Reason = "value_type must be int or real"
|
|
157
|
+
; \+ (is_list(Assumptions), Assumptions \== [])
|
|
158
|
+
-> Reason = "the certificate states no assumptions"
|
|
159
|
+
; \+ maplist(arithmetic_constraint(_), Assumptions, _)
|
|
160
|
+
-> Reason = "every assumption must be {operator, value} with a comparison operator and a number"
|
|
161
|
+
; \+ arithmetic_constraint(_, Claim, _)
|
|
162
|
+
-> Reason = "the claim must be {operator, value} with a comparison operator and a number"
|
|
163
|
+
; maplist(arithmetic_constraint(X), Assumptions, Premises),
|
|
164
|
+
arithmetic_constraint(X, Claim, Goal),
|
|
165
|
+
integer_variables(Type, X, Integers),
|
|
166
|
+
( \+ numeric_constraints_satisfiable(Premises, Integers)
|
|
167
|
+
-> Reason = "the assumptions admit no value, so the claim holds vacuously"
|
|
168
|
+
; \+ numeric_constraint_entailed(Premises, Goal, Integers)
|
|
169
|
+
-> Reason = "the assumptions do not entail the claim: some value satisfies them and breaks it"
|
|
170
|
+
)
|
|
171
|
+
).
|
|
172
|
+
kind_problem(smt, Certificate, Reason) :-
|
|
173
|
+
field(Certificate, solver, Solver),
|
|
174
|
+
field(Solver, name, Name),
|
|
175
|
+
field(Solver, version, SolverVersion),
|
|
176
|
+
field(Certificate, query, Query),
|
|
177
|
+
field(Certificate, result, Result),
|
|
178
|
+
( \+ (nonempty_string(Name), nonempty_string(SolverVersion))
|
|
179
|
+
-> Reason = "the certificate names no solver and version"
|
|
180
|
+
; \+ nonempty_string(Query)
|
|
181
|
+
-> Reason = "the certificate has no query"
|
|
182
|
+
; \+ sub_string(Query, _, _, _, "(check-sat)")
|
|
183
|
+
-> Reason = "the query never asks (check-sat)"
|
|
184
|
+
; Result \== "unsat"
|
|
185
|
+
-> Reason = "the solver did not answer unsat"
|
|
186
|
+
).
|
|
187
|
+
|
|
188
|
+
% field(+Dict, +Key, -Value): the value, or '$missing' when absent.
|
|
189
|
+
field(Dict, Key, Value) :-
|
|
190
|
+
( is_dict(Dict), get_dict(Key, Dict, Value0)
|
|
191
|
+
-> Value = Value0
|
|
192
|
+
; Value = '$missing'
|
|
193
|
+
).
|
|
194
|
+
|
|
195
|
+
result_row(Row) :-
|
|
196
|
+
is_dict(Row),
|
|
197
|
+
get_dict(member, Row, _),
|
|
198
|
+
get_dict(outcome, Row, _).
|
|
199
|
+
|
|
200
|
+
arithmetic_constraint(X, Row, c(Op, X, Value)) :-
|
|
201
|
+
is_dict(Row),
|
|
202
|
+
get_dict(operator, Row, OpString),
|
|
203
|
+
string(OpString),
|
|
204
|
+
atom_string(Op, OpString),
|
|
205
|
+
memberchk(Op, [eq, neq, lt, lte, gt, gte]),
|
|
206
|
+
get_dict(value, Row, Value),
|
|
207
|
+
number(Value).
|
|
208
|
+
|
|
209
|
+
integer_variables("int", X, [X]).
|
|
210
|
+
integer_variables("real", _, []).
|
|
211
|
+
|
|
212
|
+
nonempty_string(Value) :-
|
|
213
|
+
string(Value),
|
|
214
|
+
Value \== "".
|
|
215
|
+
|
|
216
|
+
sha256_string(Value) :-
|
|
217
|
+
string(Value),
|
|
218
|
+
string_length(Value, 64),
|
|
219
|
+
string_codes(Value, Codes),
|
|
220
|
+
forall(member(C, Codes), (code_type(C, digit) ; between(0'a, 0'f, C))).
|
|
221
|
+
|
|
222
|
+
% The canonical JSON is spelled out here rather than left to json_write, so
|
|
223
|
+
% it does not depend on the stream encoding or on how a key was read (a YAML
|
|
224
|
+
% key and a JSON key for the same text are the same key): object keys sorted
|
|
225
|
+
% by code point, no whitespace, ", \ and the JSON control escapes, other
|
|
226
|
+
% control characters and every non-ASCII character as \u escapes of their
|
|
227
|
+
% UTF-16 code units, integers in decimal.
|
|
228
|
+
canonical_json(Value, Json) :-
|
|
229
|
+
is_dict(Value), !,
|
|
230
|
+
dict_pairs(Value, _, Pairs0),
|
|
231
|
+
maplist(string_keyed, Pairs0, Pairs1),
|
|
232
|
+
keysort(Pairs1, Pairs),
|
|
233
|
+
maplist(canonical_pair, Pairs, Parts),
|
|
234
|
+
atomics_to_string(Parts, ",", Body),
|
|
235
|
+
format(string(Json), "{~s}", [Body]).
|
|
236
|
+
canonical_json(Value, Json) :-
|
|
237
|
+
is_list(Value), !,
|
|
238
|
+
maplist(canonical_json, Value, Parts),
|
|
239
|
+
atomics_to_string(Parts, ",", Body),
|
|
240
|
+
format(string(Json), "[~s]", [Body]).
|
|
241
|
+
canonical_json(Value, Json) :-
|
|
242
|
+
string(Value), !,
|
|
243
|
+
canonical_string(Value, Json).
|
|
244
|
+
canonical_json(Value, Json) :-
|
|
245
|
+
integer(Value), !,
|
|
246
|
+
format(string(Json), "~d", [Value]).
|
|
247
|
+
canonical_json(Value, Json) :-
|
|
248
|
+
memberchk(Value, [true, false, null]), !,
|
|
249
|
+
atom_string(Value, Json).
|
|
250
|
+
canonical_json(Value, Json) :-
|
|
251
|
+
atom(Value), !,
|
|
252
|
+
atom_string(Value, String),
|
|
253
|
+
canonical_string(String, Json).
|
|
254
|
+
canonical_json(Value, Json) :-
|
|
255
|
+
with_output_to(string(Json), json_write(current_output, Value, [width(0)])).
|
|
256
|
+
|
|
257
|
+
% Keys as code lists, so keysort orders them by code point whatever the
|
|
258
|
+
% reader made of them (an atom, or a small integer for a numeric key).
|
|
259
|
+
string_keyed(Key-Value, Codes-Value) :-
|
|
260
|
+
format(string(String), "~w", [Key]),
|
|
261
|
+
string_codes(String, Codes).
|
|
262
|
+
|
|
263
|
+
canonical_pair(Codes-Value, Json) :-
|
|
264
|
+
string_codes(Key, Codes),
|
|
265
|
+
canonical_string(Key, KeyJson),
|
|
266
|
+
canonical_json(Value, ValueJson),
|
|
267
|
+
format(string(Json), "~s:~s", [KeyJson, ValueJson]).
|
|
268
|
+
|
|
269
|
+
canonical_string(String, Json) :-
|
|
270
|
+
string_codes(String, Codes),
|
|
271
|
+
phrase(escaped_codes(Codes), Escaped),
|
|
272
|
+
string_codes(Body, Escaped),
|
|
273
|
+
format(string(Json), "\"~s\"", [Body]).
|
|
274
|
+
|
|
275
|
+
escaped_codes([]) --> [].
|
|
276
|
+
escaped_codes([C|Cs]) --> escaped_code(C), escaped_codes(Cs).
|
|
277
|
+
|
|
278
|
+
escaped_code(0'") --> !, "\\\"".
|
|
279
|
+
escaped_code(0'\\) --> !, "\\\\".
|
|
280
|
+
escaped_code(0'\b) --> !, "\\b".
|
|
281
|
+
escaped_code(0'\f) --> !, "\\f".
|
|
282
|
+
escaped_code(0'\n) --> !, "\\n".
|
|
283
|
+
escaped_code(0'\r) --> !, "\\r".
|
|
284
|
+
escaped_code(0'\t) --> !, "\\t".
|
|
285
|
+
escaped_code(C) --> { C >= 0x20, C =< 0x7e }, !, [C].
|
|
286
|
+
escaped_code(C) -->
|
|
287
|
+
{ C > 0xffff }, !,
|
|
288
|
+
{ V is C - 0x10000,
|
|
289
|
+
High is 0xd800 + (V >> 10),
|
|
290
|
+
Low is 0xdc00 + (V /\ 0x3ff) },
|
|
291
|
+
unit_escape(High),
|
|
292
|
+
unit_escape(Low).
|
|
293
|
+
escaped_code(C) --> unit_escape(C).
|
|
294
|
+
|
|
295
|
+
unit_escape(Unit) -->
|
|
296
|
+
{ format(codes(Codes), "\\u~|~`0t~16r~4+", [Unit]) },
|
|
297
|
+
Codes.
|