kibi-core 0.24.0 → 0.27.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.
@@ -1,5 +1,5 @@
1
1
  % Module: claim_obligations
2
- % implements REQ-kibi-claim-bound-proofs
2
+ % implements REQ-kibi-claim-bound-proofs, REQ-kibi-non-numeric-proof-kinds
3
3
  %
4
4
  % What a certificate has to establish for it to prove one requirement claim.
5
5
  % A claim is grounded by exactly one typed fact; the obligation is derived
@@ -8,16 +8,50 @@
8
8
  %
9
9
  % set fact (value_type set, or the legacy allowed_values string "a|b|c")
10
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
11
19
  % numeric fact (value_type int or number, a comparison operator)
12
20
  % arithmetic, smt the certificate's claim is the fact's constraint
13
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; with property precedes and
39
+ % operator includes each member "A -> B"
40
+ % holds in the list read: A and B are in it
41
+ % and every A comes before every B
42
+ % set fact under state_machine with operator includes, subset_of or
43
+ % excludes, or with property reachable_pairs or required_waypoints
44
+ % state_machine the projection stands in the operator's
45
+ % relation to the set; a required waypoint
46
+ % "from->to via mid" holds when every path from
47
+ % one state to the other passes through mid
14
48
  %
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.
49
+ % Any other pairing of a set, numeric or string fact with a certificate kind
50
+ % is incompatible (a set fact under arithmetic, a numeric fact under
51
+ % exhaustive). A fact kind with no derivation for the proof kind (boolean
52
+ % placeholders, a rule under exhaustive) can only be attested: the test
53
+ % author says the certificate expresses the claim, and Kibi never counts that
54
+ % as proved.
21
55
 
22
56
  :- module(claim_obligations, [
23
57
  claim_obligation/3,
@@ -25,38 +59,245 @@
25
59
  obligation_equal/2,
26
60
  proof_contract_claim_candidates_json/1,
27
61
  proof_contract_claim_issues_json/1,
28
- test_claim_obligations_json/4
62
+ test_claim_obligations_json/4,
63
+ claim_model_check_json/4
29
64
  ]).
30
65
 
31
66
  :- use_module(library(http/json)).
32
67
  :- use_module(library(apply)).
33
68
  :- use_module(library(lists)).
34
69
  :- use_module('kb.pl').
70
+ :- use_module('model_check.pl', [model_check_obligation/4, edge_text/3]).
35
71
 
36
72
  %% fact_obligation(+FactId, +ProofKind, -Result)
37
73
  % Result is obligation(Dict), attested, or incompatible(Reason).
38
74
  fact_obligation(FactId, ProofKind, Result) :-
39
- ( fact_set(FactId, Members)
40
- -> ( memberchk(ProofKind, [exhaustive, typecheck])
41
- -> Result = obligation(_{kind: "set_equals", members: Members})
75
+ ( fact_set(FactId, Members, Operator)
76
+ -> ( ProofKind == source
77
+ -> source_obligation(FactId, Members, Operator, Result)
78
+ ; ProofKind == state_machine
79
+ -> machine_obligation(FactId, Members, Operator, Result)
80
+ ; Operator == includes
81
+ -> includes_obligation(FactId, Members, ProofKind, Result)
82
+ ; Operator == eq
83
+ -> set_obligation(FactId, Members, ProofKind, Result)
42
84
  ; format(string(Reason),
43
- "~w is a set fact, which an ~w certificate cannot express (use exhaustive or typecheck)",
44
- [FactId, ProofKind]),
85
+ "~w is a ~w set fact, which only source and state_machine certificates can derive (a ~w certificate compares the whole set)",
86
+ [FactId, Operator, ProofKind]),
45
87
  Result = incompatible(Reason)
46
88
  )
47
89
  ; fact_numeric_constraint(FactId, Variable, ValueType, Operator, Value)
48
90
  -> ( memberchk(ProofKind, [arithmetic, smt])
49
91
  -> Result = obligation(_{kind: "numeric_constraint", variable: Variable,
50
92
  value_type: ValueType, operator: Operator, value: Value})
51
- ; ( ProofKind == typecheck -> Article = a ; Article = an ),
93
+ ; ( memberchk(ProofKind, [typecheck, source]) -> Article = a ; Article = an ),
52
94
  format(string(Reason),
53
95
  "~w is a numeric fact, which ~w ~w certificate cannot express (use arithmetic or smt)",
54
96
  [FactId, Article, ProofKind]),
55
97
  Result = incompatible(Reason)
56
98
  )
99
+ ; memberchk(ProofKind, [rule, state_machine]),
100
+ fact_rule_hash(FactId, Hash)
101
+ -> atom_string(FactId, FactString),
102
+ Result = obligation(_{kind: "rule_holds", fact_id: FactString, rule_hash: Hash})
103
+ ; memberchk(ProofKind, [rule, state_machine]),
104
+ predicate_expectation(FactId, Name, Args, Value)
105
+ -> atom_string(Name, NameString),
106
+ maplist(atom_string, Args, ArgStrings),
107
+ Result = obligation(_{kind: "predicate_holds", predicate: NameString,
108
+ args: ArgStrings, value: Value})
109
+ ; fact_string_constraint(FactId, Operator, Value, Polarity)
110
+ -> ( ProofKind == regex
111
+ -> Result = obligation(_{kind: "string_constraint", operator: Operator,
112
+ value: Value, polarity: Polarity})
113
+ ; memberchk(ProofKind, [exhaustive, typecheck, arithmetic, smt])
114
+ -> Result = attested
115
+ ; format(string(Reason),
116
+ "~w is a string fact, which a ~w certificate cannot express (use regex)",
117
+ [FactId, ProofKind]),
118
+ Result = incompatible(Reason)
119
+ )
57
120
  ; Result = attested
58
121
  ).
59
122
 
123
+ % Set facts: what each kind derives from the members, by the property the
124
+ % fact names where the kind reads one.
125
+ set_obligation(_FactId, Members, exhaustive, obligation(_{kind: "set_equals", members: Members})) :- !.
126
+ set_obligation(FactId, Members, typecheck, obligation(Obligation)) :- !,
127
+ ( fact_property_key(FactId, required_fields)
128
+ -> Obligation = _{kind: "required_fields", members: Members}
129
+ ; Obligation = _{kind: "set_equals", members: Members}
130
+ ).
131
+ set_obligation(FactId, Members, rule, obligation(_{kind: "every_member", predicate: Predicate, members: Members})) :-
132
+ fact_property_key(FactId, Property),
133
+ atom_string(Property, Predicate),
134
+ !.
135
+ set_obligation(FactId, Members, state_machine, Result) :- !,
136
+ machine_obligation(FactId, Members, eq, Result).
137
+ set_obligation(FactId, _Members, rule, incompatible(Reason)) :- !,
138
+ format(string(Reason),
139
+ "~w is a set fact with no property to read as the predicate every member satisfies",
140
+ [FactId]).
141
+ set_obligation(FactId, _Members, ProofKind, incompatible(Reason)) :-
142
+ ( ProofKind == regex -> Article = a ; Article = an ),
143
+ format(string(Reason),
144
+ "~w is a set fact, which ~w ~w certificate cannot express (use exhaustive, typecheck, rule or state_machine)",
145
+ [FactId, Article, ProofKind]).
146
+
147
+ % includes set facts ("every member of A has P") state containment only, so
148
+ % only the kinds that check each member on its own can derive them: a rule
149
+ % checks the property on every member, and a typecheck checks each field is
150
+ % required. Kinds that compare the whole set would read containment as
151
+ % equality.
152
+ includes_obligation(FactId, Members, rule, Result) :- !,
153
+ set_obligation(FactId, Members, rule, Result).
154
+ includes_obligation(FactId, Members, typecheck, obligation(_{kind: "required_fields", members: Members})) :-
155
+ fact_property_key(FactId, required_fields),
156
+ !.
157
+ includes_obligation(FactId, _Members, ProofKind, incompatible(Reason)) :-
158
+ format(string(Reason),
159
+ "~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)",
160
+ [FactId, ProofKind]).
161
+
162
+ % implements REQ-kibi-source-proof-kinds
163
+ % A state machine set fact: the projection its property names stands in the
164
+ % fact operator's relation to the members. eq on a plain projection keeps
165
+ % the machine_set obligation earlier receipts recorded.
166
+ machine_obligation(FactId, Members, Operator, Result) :-
167
+ ( fact_property_key(FactId, Projection),
168
+ memberchk(Projection, [transitions, states, initial_states, terminal_states,
169
+ reachable_states, reachable_pairs, required_waypoints])
170
+ -> ( memberchk(Projection, [transitions, reachable_pairs])
171
+ -> maplist(edge_member, Members, Edges0),
172
+ sort(Edges0, Spelled)
173
+ ; Projection == required_waypoints
174
+ -> maplist(waypoint_member, Members, Spelled0),
175
+ sort(Spelled0, Spelled)
176
+ ; Spelled = Members
177
+ ),
178
+ atom_string(Projection, ProjectionString),
179
+ atom_string(Operator, OperatorString),
180
+ ( Projection == required_waypoints,
181
+ Operator == subset_of
182
+ -> format(string(Reason),
183
+ "~w names required waypoints with subset_of; state each waypoint with includes (they hold) or excludes (they do not)",
184
+ [FactId]),
185
+ Result = incompatible(Reason)
186
+ ; Projection == required_waypoints,
187
+ \+ ( member(M, Spelled), sub_string(M, _, _, _, " via ") )
188
+ -> format(string(Reason),
189
+ "~w names required waypoints that are not spelled \"from->to via state\"",
190
+ [FactId]),
191
+ Result = incompatible(Reason)
192
+ ; Operator == eq,
193
+ Projection \== required_waypoints
194
+ -> Result = obligation(_{kind: "machine_set", projection: ProjectionString, members: Spelled})
195
+ ; Result = obligation(_{kind: "machine_relation", projection: ProjectionString,
196
+ operator: OperatorString, members: Spelled})
197
+ )
198
+ ; format(string(Reason),
199
+ "~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)",
200
+ [FactId]),
201
+ Result = incompatible(Reason)
202
+ ).
203
+
204
+ % A required waypoint "from -> to via state", spelled "from->to via state".
205
+ waypoint_member(Member, Spelled) :-
206
+ ( sub_string(Member, Before, _, After, " via ")
207
+ -> sub_string(Member, 0, Before, _, Edge0),
208
+ sub_string(Member, _, After, 0, Via0),
209
+ edge_member(Edge0, Edge),
210
+ normalize_space(atom(Via), Via0),
211
+ format(string(Spelled), "~w via ~w", [Edge, Via])
212
+ ; Spelled = Member
213
+ ).
214
+
215
+ % implements REQ-kibi-source-proof-kinds
216
+ % A set fact under the source kind. The property decides what the members
217
+ % are about: fields every selected table or object has as required or
218
+ % nullable, middleware every selected route runs, or (any other property)
219
+ % the members of the source itself.
220
+ % A required_fields or nullable_fields fact says which fields have the
221
+ % property; with subset_of or excludes it would be decided against whatever
222
+ % members the contract selects, so it is refused instead.
223
+ source_obligation(FactId, Members, Operator, Result) :-
224
+ atom_string(Operator, OperatorString),
225
+ ( fact_property_key(FactId, Property) -> true ; Property = none ),
226
+ ( memberchk(Property-Kind, [required_fields-"required_fields", nullable_fields-"nullable_fields"])
227
+ -> ( memberchk(Operator, [eq, includes])
228
+ -> Result = obligation(_{kind: Kind, members: Members})
229
+ ; format(string(Reason),
230
+ "~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)",
231
+ [FactId, Property, Operator, Property]),
232
+ Result = incompatible(Reason)
233
+ )
234
+ ; Property == middleware
235
+ -> Result = obligation(_{kind: "route_middleware", operator: OperatorString, members: Members})
236
+ ; Property == precedes
237
+ -> ( Operator == includes
238
+ -> Result = obligation(_{kind: "sequence_precedes", members: Members})
239
+ ; format(string(Reason),
240
+ "~w is a precedes fact with operator ~w; a source proof decides precedes only with includes (each member \"A -> B\" holds)",
241
+ [FactId, Operator]),
242
+ Result = incompatible(Reason)
243
+ )
244
+ ; Result = obligation(_{kind: "set_relation", operator: OperatorString, members: Members})
245
+ ).
246
+
247
+ % A transition member "from -> to" (or with U+2192 as the arrow) spelled "from->to".
248
+ edge_member(Member, Spelled) :-
249
+ ( sub_string(Member, Before, _, After, "->")
250
+ -> sub_string(Member, 0, Before, _, From0),
251
+ sub_string(Member, _, After, 0, To0)
252
+ ; sub_string(Member, Before, _, After, "\u2192")
253
+ -> sub_string(Member, 0, Before, _, From0),
254
+ sub_string(Member, _, After, 0, To0)
255
+ ; From0 = Member, To0 = ""
256
+ ),
257
+ normalize_space(atom(From), From0),
258
+ normalize_space(atom(To), To0),
259
+ edge_text(From, To, Spelled).
260
+
261
+ % What a predicate fact says the code decides. A decision predicate (the
262
+ % built-in permission_rule, or a schema with key_arguments and a decision
263
+ % argument) is about the key arguments: the code's function takes them and
264
+ % returns whether the effective decision is allow. Any other predicate is
265
+ % true when asserted and false when denied.
266
+ predicate_expectation(FactId, Name, KeyArgs, Value) :-
267
+ kb:predicate_fact(FactId, Namespace, Name, Args, Polarity),
268
+ length(Args, Arity),
269
+ ( kb:decision_signatures(Signatures),
270
+ memberchk(decision_signature(Namespace, Name, Arity, KeyPositions, DecisionPosition), Signatures)
271
+ -> findall(Arg, ( member(Position, KeyPositions), nth1(Position, Args, Arg) ), KeyArgs),
272
+ nth1(DecisionPosition, Args, Decision),
273
+ kb:effective_decision(Decision, Polarity, Effect),
274
+ ( Effect == allow -> Value = true ; Value = false )
275
+ ; memberchk(Polarity-Value, [assert-true, deny-false]),
276
+ KeyArgs = Args
277
+ ).
278
+
279
+ fact_property_key(FactId, Property) :-
280
+ kb_entity(FactId, fact, Props),
281
+ prop_atom(Props, property_key, Property).
282
+
283
+ fact_rule_hash(FactId, Hash) :-
284
+ kb_entity(FactId, fact, Props),
285
+ prop_atom(Props, fact_kind, rule),
286
+ memberchk(rule_ir=_, Props),
287
+ ( prop_atom(Props, rule_hash, HashAtom)
288
+ -> atom_string(HashAtom, Hash)
289
+ ; Hash = ""
290
+ ).
291
+
292
+ % A string property_value fact: eq, neq or matches on a string value.
293
+ fact_string_constraint(FactId, Operator, Value, Polarity) :-
294
+ kb:fact_property_tuple(FactId, _Subject, Property, Op, string, Raw, _Unit, _Scope, Polarity0),
295
+ Property \== allowed_values,
296
+ memberchk(Op, [eq, neq, matches]),
297
+ text_string(Raw, Value),
298
+ atom_string(Op, Operator),
299
+ atom_string(Polarity0, Polarity).
300
+
60
301
  %% claim_obligation(+ReqId, +ClaimKey, +ProofKind, -Result)
61
302
  % As fact_obligation/3 for the fact grounding ClaimKey in ReqId, with the
62
303
  % fact id: obligation(FactId, Dict), attested(FactId), incompatible(FactId,
@@ -102,6 +343,63 @@ obligation_key(Obligation, set(Members)) :-
102
343
  maplist(text_string, Raw, Strings),
103
344
  sort(Strings, Members),
104
345
  !.
346
+ obligation_key(Obligation, required_fields(Members)) :-
347
+ dict_text(Obligation, kind, "required_fields"),
348
+ obligation_members(Obligation, Members),
349
+ !.
350
+ obligation_key(Obligation, every(Predicate, Members)) :-
351
+ dict_text(Obligation, kind, "every_member"),
352
+ dict_text(Obligation, predicate, Predicate),
353
+ obligation_members(Obligation, Members),
354
+ !.
355
+ obligation_key(Obligation, nullable_fields(Members)) :-
356
+ dict_text(Obligation, kind, "nullable_fields"),
357
+ obligation_members(Obligation, Members),
358
+ !.
359
+ obligation_key(Obligation, relation(Operator, Members)) :-
360
+ dict_text(Obligation, kind, "set_relation"),
361
+ dict_text(Obligation, operator, Operator),
362
+ obligation_members(Obligation, Members),
363
+ !.
364
+ obligation_key(Obligation, precedes(Members)) :-
365
+ dict_text(Obligation, kind, "sequence_precedes"),
366
+ obligation_members(Obligation, Members),
367
+ !.
368
+ obligation_key(Obligation, middleware(Operator, Members)) :-
369
+ dict_text(Obligation, kind, "route_middleware"),
370
+ dict_text(Obligation, operator, Operator),
371
+ obligation_members(Obligation, Members),
372
+ !.
373
+ obligation_key(Obligation, machine(Projection, Operator, Members)) :-
374
+ dict_text(Obligation, kind, "machine_relation"),
375
+ dict_text(Obligation, projection, Projection),
376
+ dict_text(Obligation, operator, Operator),
377
+ obligation_members(Obligation, Members),
378
+ !.
379
+ obligation_key(Obligation, machine(Projection, Members)) :-
380
+ dict_text(Obligation, kind, "machine_set"),
381
+ dict_text(Obligation, projection, Projection),
382
+ obligation_members(Obligation, Members),
383
+ !.
384
+ obligation_key(Obligation, rule(FactId, Hash)) :-
385
+ dict_text(Obligation, kind, "rule_holds"),
386
+ dict_text(Obligation, fact_id, FactId),
387
+ dict_text(Obligation, rule_hash, Hash),
388
+ !.
389
+ obligation_key(Obligation, predicate(Name, Args, Value)) :-
390
+ dict_text(Obligation, kind, "predicate_holds"),
391
+ dict_text(Obligation, predicate, Name),
392
+ get_dict(args, Obligation, RawArgs),
393
+ is_list(RawArgs),
394
+ maplist(text_string, RawArgs, Args),
395
+ dict_text(Obligation, value, Value),
396
+ !.
397
+ obligation_key(Obligation, string(Operator, Value, Polarity)) :-
398
+ dict_text(Obligation, kind, "string_constraint"),
399
+ dict_text(Obligation, operator, Operator),
400
+ dict_text(Obligation, value, Value),
401
+ dict_text(Obligation, polarity, Polarity),
402
+ !.
105
403
  obligation_key(Obligation, numeric(Variable, Type, Operator, Value)) :-
106
404
  dict_text(Obligation, kind, "numeric_constraint"),
107
405
  dict_text(Obligation, variable, Variable),
@@ -110,6 +408,12 @@ obligation_key(Obligation, numeric(Variable, Type, Operator, Value)) :-
110
408
  get_dict(value, Obligation, Raw),
111
409
  number_value(Raw, Value).
112
410
 
411
+ obligation_members(Obligation, Members) :-
412
+ get_dict(members, Obligation, Raw),
413
+ is_list(Raw),
414
+ maplist(text_string, Raw, Strings),
415
+ sort(Strings, Members).
416
+
113
417
  dict_text(Dict, Key, String) :-
114
418
  is_dict(Dict),
115
419
  get_dict(Key, Dict, Raw),
@@ -126,13 +430,15 @@ text_string(Raw, String) :- atom(Raw), !, atom_string(Raw, String).
126
430
  text_string(Raw, String) :- number(Raw), !, number_string(Raw, String).
127
431
  text_string(^^(Raw, _), String) :- !, text_string(Raw, String).
128
432
 
129
- % The members of a set fact, sorted and unique.
130
- fact_set(FactId, Members) :-
433
+ % The members of a set fact, sorted and unique, and its operator: eq (the
434
+ % set is exactly these members), includes (it contains at least these),
435
+ % subset_of (it contains only these) or excludes (it contains none of them).
436
+ fact_set(FactId, Members, Operator) :-
131
437
  kb_entity(FactId, fact, Props),
132
438
  prop_atom(Props, fact_kind, property_value),
133
439
  ( prop_atom(Props, value_type, set)
134
440
  -> memberchk(value_set=Raw, Props),
135
- kb:normalize_term_atom_list(Raw, Atoms)
441
+ kb:normalize_literal_atom_list(Raw, Atoms)
136
442
  ; prop_atom(Props, property_key, allowed_values),
137
443
  \+ (memberchk(value_type=RawType, Props),
138
444
  kb:normalize_term_atom(RawType, Type),
@@ -144,7 +450,8 @@ fact_set(FactId, Members) :-
144
450
  exclude(==(""), Parts, NonEmpty),
145
451
  maplist([S, A]>>atom_string(A, S), NonEmpty, Atoms)
146
452
  ),
147
- ( prop_atom(Props, operator, Operator) -> Operator == eq ; true ),
453
+ ( prop_atom(Props, operator, Operator0) -> Operator = Operator0 ; Operator = eq ),
454
+ memberchk(Operator, [eq, includes, subset_of, excludes]),
148
455
  \+ prop_atom(Props, polarity, forbid),
149
456
  maplist([A, S]>>atom_string(A, S), Atoms, Strings),
150
457
  sort(Strings, Members),
@@ -311,6 +618,10 @@ certificate_kind(exhaustive).
311
618
  certificate_kind(typecheck).
312
619
  certificate_kind(arithmetic).
313
620
  certificate_kind(smt).
621
+ certificate_kind(rule).
622
+ certificate_kind(state_machine).
623
+ certificate_kind(regex).
624
+ certificate_kind(source).
314
625
 
315
626
  test_certificate_contract(TestId, Kind, Contract) :-
316
627
  kb_entity(TestId, test, Props),
@@ -352,10 +663,14 @@ proof_contract_claim_issue(_{test_id: TestString, claim_key: KeyString, reason:
352
663
 
353
664
  %% proof_contract_claim_candidates_json(-Json)
354
665
  % 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.
666
+ % certificate-kind proof contract without proves. Candidates are the
667
+ % modeled claims {claim_key, requirement_id, fact_id, binding, reason?} of
668
+ % current requirements the test reaches through its scenarios, with the
669
+ % binding a certificate of its kind would get: derived when the grounding
670
+ % fact derives an obligation for the kind, attested when the fact has no
671
+ % derivation, incompatible (with the reason) when the kind cannot express
672
+ % it. The migration pins proves only when exactly one claim is derived;
673
+ % the review lists every candidate so a person can bind the rest.
359
674
  proof_contract_claim_candidates_json(Json) :-
360
675
  findall(Row, proof_contract_claim_candidates(Row), Rows0),
361
676
  sort(Rows0, Rows),
@@ -364,15 +679,43 @@ proof_contract_claim_candidates_json(Json) :-
364
679
  proof_contract_claim_candidates(_{test_id: TestString, kind: KindString, candidates: Candidates}) :-
365
680
  test_certificate_contract(TestId, Kind, Contract),
366
681
  \+ ( get_dict(proves, Contract, Proves), is_list(Proves), Proves \== [] ),
367
- findall(_{claim_key: KeyString, requirement_id: ReqString, fact_id: FactString},
682
+ findall(Candidate,
368
683
  ( test_requirement(TestId, ReqId),
369
684
  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)
685
+ claim_obligation(ReqId-ClaimKey, Kind, Result),
686
+ claim_candidate(ReqId, ClaimKey, Result, Candidate)
374
687
  ),
375
688
  Candidates0),
376
- sort(Candidates0, Candidates),
689
+ sort(Candidates0, Candidates1),
690
+ sort(claim_key, @=<, Candidates1, Candidates),
377
691
  atom_string(TestId, TestString),
378
692
  atom_string(Kind, KindString).
693
+
694
+ claim_candidate(ReqId, ClaimKey, Result, Candidate) :-
695
+ ( Result = obligation(FactId, _)
696
+ -> Binding = "derived"
697
+ ; Result = attested(FactId)
698
+ -> Binding = "attested"
699
+ ; Result = incompatible(FactId, Reason)
700
+ -> Binding = "incompatible"
701
+ ),
702
+ atom_string(ClaimKey, KeyString),
703
+ atom_string(ReqId, ReqString),
704
+ atom_string(FactId, FactString),
705
+ Base = _{claim_key: KeyString, requirement_id: ReqString, fact_id: FactString, binding: Binding},
706
+ ( var(Reason) -> Candidate = Base ; Candidate = Base.put(reason, Reason) ).
707
+
708
+ %% claim_model_check_json(+Kind, +ObligationJson, +CertificateJson, -Json)
709
+ % For `kibi prove`: whether a rule or state_machine certificate's model meets
710
+ % one derived obligation, {"ok": true} or {"ok": false, "reason": R}.
711
+ claim_model_check_json(Kind0, ObligationJson, CertificateJson, Json) :-
712
+ kb:normalize_term_atom(Kind0, Kind),
713
+ atom_json_dict(ObligationJson, Obligation, [value_string_as(string)]),
714
+ atom_json_dict(CertificateJson, Certificate, [value_string_as(string)]),
715
+ model_check_obligation(Kind, Obligation, Certificate, Result),
716
+ ( Result == ok
717
+ -> Out = _{ok: true}
718
+ ; Result = problem(Reason),
719
+ Out = _{ok: false, reason: Reason}
720
+ ),
721
+ with_output_to(string(Json), json_write_dict(current_output, Out, [width(0)])).