kibi-core 0.23.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-core",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "private": false,
5
5
  "description": "Core Prolog modules and RDF graph logic for Kibi",
6
6
  "type": "module",
@@ -56,6 +56,9 @@ entity_property(req, exempts_claims, list).
56
56
  % Absent reads as default.
57
57
  entity_property(req, firmness, atom).
58
58
  entity_property(adr, firmness, atom).
59
+ % The weakest evidence level a current requirement accepts: proved, tested,
60
+ % asserted or unverified. Absent: tested when firm, otherwise unverified.
61
+ entity_property(req, required_evidence, atom).
59
62
  % Intended outcome of a scenario: success, rejection or error.
60
63
  entity_property(scenario, expects, atom).
61
64
 
@@ -69,6 +72,8 @@ entity_property(fact, value_string, string).
69
72
  entity_property(fact, value_int, integer).
70
73
  entity_property(fact, value_number, number).
71
74
  entity_property(fact, value_bool, boolean).
75
+ % The exact members of a set fact (value_type set).
76
+ entity_property(fact, value_set, list).
72
77
  entity_property(fact, unit, string).
73
78
  entity_property(fact, scope, string).
74
79
  entity_property(fact, polarity, atom).
@@ -150,6 +155,7 @@ optional_property(req, rationale).
150
155
  optional_property(req, exempts_claims).
151
156
  optional_property(req, firmness).
152
157
  optional_property(adr, firmness).
158
+ optional_property(req, required_evidence).
153
159
  optional_property(test, verification_scope).
154
160
  optional_property(test, verification_perspective).
155
161
  optional_property(test, proof_contract).
@@ -108,6 +108,7 @@ is_fact_only_field(value_string).
108
108
  is_fact_only_field(value_int).
109
109
  is_fact_only_field(value_number).
110
110
  is_fact_only_field(value_bool).
111
+ is_fact_only_field(value_set).
111
112
  is_fact_only_field(unit).
112
113
  is_fact_only_field(scope).
113
114
  is_fact_only_field(polarity).
@@ -227,7 +228,7 @@ text_atom(Value, Atom) :- string(Value), atom_string(Atom, Value).
227
228
  valid_operator(Op) :- memberchk(Op, [eq, neq, lt, lte, gt, gte]), !.
228
229
 
229
230
  % valid_value_type(+VT)
230
- valid_value_type(ValueType) :- memberchk(ValueType, [string, int, number, bool]), !.
231
+ valid_value_type(ValueType) :- memberchk(ValueType, [string, int, number, bool, set]), !.
231
232
 
232
233
  % valid_polarity_in_props(+Props)
233
234
  % Validates polarity if present; succeeds if no polarity in props
@@ -299,6 +300,7 @@ is_value_field(value_string).
299
300
  is_value_field(value_int).
300
301
  is_value_field(value_number).
301
302
  is_value_field(value_bool).
303
+ is_value_field(value_set).
302
304
 
303
305
  % value_type_matches_field(+ValueType, +Props)
304
306
  % Ensures that value_type matches the actual value field present
@@ -306,6 +308,7 @@ value_type_matches_field(string, Props) :- memberchk(value_string=_, Props), !.
306
308
  value_type_matches_field(int, Props) :- memberchk(value_int=_, Props), !.
307
309
  value_type_matches_field(number, Props) :- memberchk(value_number=_, Props), !.
308
310
  value_type_matches_field(bool, Props) :- memberchk(value_bool=_, Props), !.
311
+ value_type_matches_field(set, Props) :- memberchk(value_set=_, Props), !.
309
312
 
310
313
  % valid_origin_in_props(+Props)
311
314
  % Entity provenance travels as a JSON object string (or a dict). When present
@@ -38,6 +38,7 @@
38
38
  certificate_rule_edges/1,
39
39
  certificate_member_holds/3,
40
40
  certificate_members_satisfiable/2,
41
+ set_texts/2,
41
42
  verify_certificates/2,
42
43
  kb_verify_certificates_file/2
43
44
  ]).
@@ -148,7 +149,7 @@ kind_is(Certificate, Kind) :-
148
149
  % Every comparable context: context(Subject, Property, Scope, Domain, Members)
149
150
  % or unresolved(Subject, Property, Scope, Reason). A member is
150
151
  % m(FactId, Op, Value, ValidFrom, ValidTo) with the operator already negated
151
- % for a forbid fact; Domain is int, number, bool or string.
152
+ % for a forbid fact; Domain is int, number, bool, string or set.
152
153
  certificate_property_contexts(Contexts) :-
153
154
  findall(Fact-tuple(Subject, Property, Op, Type, Value, Unit, Scope, Polarity, From, To),
154
155
  ( kb:current_req(Req),
@@ -202,6 +203,8 @@ context_domain(Members, Domain, Reason) :-
202
203
  -> Domain = bool, context_scalar_reason(Members, Reason)
203
204
  ; Types == [string]
204
205
  -> Domain = string, context_scalar_reason(Members, Reason)
206
+ ; Types == [set]
207
+ -> Domain = set, context_scalar_reason(Members, Reason)
205
208
  ; Types = [_, _|_]
206
209
  -> Domain = none, Reason = "the facts use different value types"
207
210
  ; Domain = none, Reason = "the value type is not supported"
@@ -247,6 +250,22 @@ certificate_member_holds(Domain, m(_, Op, Bound, _, _), Value) :-
247
250
  ; Op == neq -> ValueText \== BoundText
248
251
  ).
249
252
 
253
+ % A set value is the exact set of allowed values: eq holds for the same
254
+ % members in any order, neq for any other set.
255
+ certificate_member_holds(set, m(_, Op, Bound, _, _), Value) :-
256
+ set_texts(Value, ValueTexts),
257
+ set_texts(Bound, BoundTexts),
258
+ ( Op == eq -> ValueTexts == BoundTexts
259
+ ; Op == neq -> ValueTexts \== BoundTexts
260
+ ).
261
+
262
+ %% set_texts(+Value, -Texts)
263
+ % The sorted, unique member texts of a set value.
264
+ set_texts(Value, Texts) :-
265
+ is_list(Value),
266
+ maplist(text_of, Value, Texts0),
267
+ sort(Texts0, Texts).
268
+
250
269
  numeric_domain(int).
251
270
  numeric_domain(number).
252
271
 
@@ -282,6 +301,14 @@ scalar_candidates(string, Members, Candidates) :-
282
301
  atomic_list_concat(["kibi:fresh"|Texts], '|', FreshAtom),
283
302
  atom_string(FreshAtom, Fresh),
284
303
  append(Texts, [Fresh], Candidates).
304
+ scalar_candidates(set, Members, Candidates) :-
305
+ findall(Texts, (member(m(_, _, Bound, _, _), Members), set_texts(Bound, Texts)), Sets0),
306
+ sort(Sets0, Sets),
307
+ findall(Text, (member(Set, Sets), member(Text, Set)), Texts0),
308
+ sort(Texts0, Texts),
309
+ atomic_list_concat(["kibi:fresh"|Texts], '|', FreshAtom),
310
+ atom_string(FreshAtom, Fresh),
311
+ append(Sets, [[Fresh]], Candidates).
285
312
 
286
313
  property_theorem(Contexts, Numbered, Theorem, Used, Rejected) :-
287
314
  findall(Index-Certificate,
@@ -27,7 +27,8 @@
27
27
  certificate_decision_groups/1,
28
28
  certificate_rule_edges/1,
29
29
  certificate_member_holds/3,
30
- certificate_members_satisfiable/2
30
+ certificate_members_satisfiable/2,
31
+ set_texts/2
31
32
  ]).
32
33
 
33
34
  %% kb_write_certificates(+Path, -JsonString)
@@ -121,6 +122,13 @@ model_candidates(string, Members, Candidates) :-
121
122
  fresh_text(Bounds, 0, Fresh),
122
123
  append(Bounds, [Fresh], Candidates).
123
124
 
125
+ model_candidates(set, Members, Candidates) :-
126
+ findall(Set, (member(m(_, _, Bound, _, _), Members), set_texts(Bound, Set)), Sets0),
127
+ sort(Sets0, Sets),
128
+ findall(Text, (member(Set, Sets), member(Text, Set)), Texts),
129
+ fresh_text(Texts, 0, Fresh),
130
+ append(Sets, [[Fresh]], Candidates).
131
+
124
132
  fresh_text(Bounds, Index, Fresh) :-
125
133
  format(string(Candidate), "kibi:model:~w", [Index]),
126
134
  ( member(Bound, Bounds), text_value(Bound, Candidate)
package/src/checks.pl CHANGED
@@ -2377,7 +2377,7 @@ property_value_shape_error(Props, "Property value fact missing required field: o
2377
2377
  property_value_shape_error(Props, "Property value fact missing required field: value_type") :-
2378
2378
  memberchk(operator=_, Props),
2379
2379
  \+ memberchk(value_type=_, Props).
2380
- property_value_shape_error(Props, "Property value fact missing value field (value_string, value_int, value_number, or value_bool)") :-
2380
+ property_value_shape_error(Props, "Property value fact missing value field (value_string, value_int, value_number, value_bool, or value_set)") :-
2381
2381
  memberchk(value_type=_, Props),
2382
2382
  \+ has_value_field(Props).
2383
2383
  property_value_shape_error(Props, "Property value fact has multiple value fields (should have exactly one)") :-
@@ -2394,17 +2394,20 @@ is_value_field(value_string).
2394
2394
  is_value_field(value_int).
2395
2395
  is_value_field(value_number).
2396
2396
  is_value_field(value_bool).
2397
+ is_value_field(value_set).
2397
2398
 
2398
2399
  has_value_field(Props) :- memberchk(value_string=_, Props), !.
2399
2400
  has_value_field(Props) :- memberchk(value_int=_, Props), !.
2400
2401
  has_value_field(Props) :- memberchk(value_number=_, Props), !.
2401
2402
  has_value_field(Props) :- memberchk(value_bool=_, Props), !.
2403
+ has_value_field(Props) :- memberchk(value_set=_, Props), !.
2402
2404
 
2403
2405
  % value_type_matches_field(+ValueType, +Props)
2404
2406
  value_type_matches_field(string, Props) :- memberchk(value_string=_, Props), !.
2405
2407
  value_type_matches_field(int, Props) :- memberchk(value_int=_, Props), !.
2406
2408
  value_type_matches_field(number, Props) :- memberchk(value_number=_, Props), !.
2407
2409
  value_type_matches_field(bool, Props) :- memberchk(value_bool=_, Props), !.
2410
+ value_type_matches_field(set, Props) :- memberchk(value_set=_, Props), !.
2408
2411
 
2409
2412
  predicate_schema_shape_error(Props, "Predicate schema fact missing required field: predicate_name") :-
2410
2413
  \+ memberchk(predicate_name=_, Props).
@@ -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).