kibi-core 0.24.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-core",
3
- "version": "0.24.0",
3
+ "version": "0.26.1",
4
4
  "private": false,
5
5
  "description": "Core Prolog modules and RDF graph logic for Kibi",
6
6
  "type": "module",
@@ -157,6 +157,8 @@ validate_fact_shape(property_value, Props) :-
157
157
  valid_operator(Op),
158
158
  memberchk(value_type=VT, Props),
159
159
  valid_value_type(VT),
160
+ ( Op == matches -> VT == string ; true ),
161
+ ( memberchk(Op, [includes, subset_of, excludes]) -> VT == set ; true ),
160
162
  exactly_one_value_field(Props),
161
163
  value_type_matches_field(VT, Props),
162
164
  valid_optional_fact_enums(Props),
@@ -225,7 +227,7 @@ text_atom(Value, Atom) :- atom(Value), !, Atom = Value.
225
227
  text_atom(Value, Atom) :- string(Value), atom_string(Atom, Value).
226
228
 
227
229
  % valid_operator(+Op)
228
- valid_operator(Op) :- memberchk(Op, [eq, neq, lt, lte, gt, gte]), !.
230
+ valid_operator(Op) :- memberchk(Op, [eq, neq, lt, lte, gt, gte, matches, includes, subset_of, excludes]), !.
229
231
 
230
232
  % valid_value_type(+VT)
231
233
  valid_value_type(ValueType) :- memberchk(ValueType, [string, int, number, bool, set]), !.
@@ -204,7 +204,7 @@ context_domain(Members, Domain, Reason) :-
204
204
  ; Types == [string]
205
205
  -> Domain = string, context_scalar_reason(Members, Reason)
206
206
  ; Types == [set]
207
- -> Domain = set, context_scalar_reason(Members, Reason)
207
+ -> Domain = set, context_set_reason(Members, Reason)
208
208
  ; Types = [_, _|_]
209
209
  -> Domain = none, Reason = "the facts use different value types"
210
210
  ; Domain = none, Reason = "the value type is not supported"
@@ -228,6 +228,25 @@ context_scalar_reason(Members, Reason) :-
228
228
  ; Reason = none
229
229
  ).
230
230
 
231
+ % Sets also compare with includes (every member of the bound has the
232
+ % property), subset_of (every member with the property is in the bound) and
233
+ % excludes (no member of the bound has it), but only as requirements: a
234
+ % forbidden one has no single negation among the set operators. neq beside
235
+ % subset_of or excludes is left unresolved: the finite candidates below decide
236
+ % those only without neq.
237
+ context_set_reason(Members, Reason) :-
238
+ ( member(_-tuple(_, _, Op, _, _, _, _, Polarity, _, _), Members),
239
+ ( \+ memberchk(Op, [eq, neq, includes, subset_of, excludes])
240
+ ; memberchk(Op, [includes, subset_of, excludes]), Polarity == forbid
241
+ )
242
+ -> Reason = "only eq, neq and a required includes, subset_of or excludes compare set values"
243
+ ; member(_-tuple(_, _, neq, _, _, _, _, _, _, _), Members),
244
+ member(_-tuple(_, _, Bounding, _, _, _, _, _, _, _), Members),
245
+ memberchk(Bounding, [subset_of, excludes])
246
+ -> Reason = "neq beside subset_of or excludes on one set property is not compared"
247
+ ; Reason = none
248
+ ).
249
+
231
250
  context_member(_Domain, Fact-tuple(_, _, Op0, _, Value, _, _, Polarity, From, To), m(Fact, Op, Value, From, To)) :-
232
251
  ( Polarity == forbid
233
252
  -> numeric_constraint_negation(c(Op0, _, Value), c(Op, _, _))
@@ -250,13 +269,17 @@ certificate_member_holds(Domain, m(_, Op, Bound, _, _), Value) :-
250
269
  ; Op == neq -> ValueText \== BoundText
251
270
  ).
252
271
 
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.
272
+ % A set value is the exact set of members with the property: eq holds for
273
+ % the same members in any order, neq for any other set, includes for any set
274
+ % that contains every member of the bound.
255
275
  certificate_member_holds(set, m(_, Op, Bound, _, _), Value) :-
256
276
  set_texts(Value, ValueTexts),
257
277
  set_texts(Bound, BoundTexts),
258
278
  ( Op == eq -> ValueTexts == BoundTexts
259
279
  ; Op == neq -> ValueTexts \== BoundTexts
280
+ ; Op == includes -> subtract(BoundTexts, ValueTexts, [])
281
+ ; Op == subset_of -> subtract(ValueTexts, BoundTexts, [])
282
+ ; Op == excludes -> \+ ( member(Text, ValueTexts), memberchk(Text, BoundTexts) )
260
283
  ).
261
284
 
262
285
  %% set_texts(+Value, -Texts)
@@ -308,7 +331,18 @@ scalar_candidates(set, Members, Candidates) :-
308
331
  sort(Texts0, Texts),
309
332
  atomic_list_concat(["kibi:fresh"|Texts], '|', FreshAtom),
310
333
  atom_string(FreshAtom, Fresh),
311
- append(Sets, [[Fresh]], Candidates).
334
+ % The union of every bound satisfies any set of includes facts alone; the
335
+ % union of the includes bounds is the least set every includes fact
336
+ % allows, so with subset_of and excludes (and no eq or neq) it is
337
+ % satisfiable exactly when that union is.
338
+ includes_union(Members, Included),
339
+ append(Sets, [Texts, Included, [Fresh]], Candidates).
340
+
341
+ includes_union(Members, Union) :-
342
+ findall(Text,
343
+ (member(m(_, includes, Bound, _, _), Members), set_texts(Bound, Set), member(Text, Set)),
344
+ Texts),
345
+ sort(Texts, Union).
312
346
 
313
347
  property_theorem(Contexts, Numbered, Theorem, Used, Rejected) :-
314
348
  findall(Index-Certificate,
@@ -125,9 +125,17 @@ model_candidates(string, Members, Candidates) :-
125
125
  model_candidates(set, Members, Candidates) :-
126
126
  findall(Set, (member(m(_, _, Bound, _, _), Members), set_texts(Bound, Set)), Sets0),
127
127
  sort(Sets0, Sets),
128
- findall(Text, (member(Set, Sets), member(Text, Set)), Texts),
128
+ findall(Text, (member(Set, Sets), member(Text, Set)), Texts0),
129
+ sort(Texts0, Texts),
129
130
  fresh_text(Texts, 0, Fresh),
130
- append(Sets, [[Fresh]], Candidates).
131
+ % The union of every bound satisfies any set of includes facts alone;
132
+ % the union of the includes bounds alone is the least set they allow,
133
+ % which subset_of and excludes facts accept when anything does.
134
+ findall(Text,
135
+ (member(m(_, includes, Bound, _, _), Members), set_texts(Bound, Set), member(Text, Set)),
136
+ Included0),
137
+ sort(Included0, Included),
138
+ append(Sets, [Texts, Included, [Fresh]], Candidates).
131
139
 
132
140
  fresh_text(Bounds, Index, Fresh) :-
133
141
  format(string(Candidate), "kibi:model:~w", [Index]),
@@ -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,47 @@
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
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
14
45
  %
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.
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.
21
52
 
22
53
  :- module(claim_obligations, [
23
54
  claim_obligation/3,
@@ -25,38 +56,237 @@
25
56
  obligation_equal/2,
26
57
  proof_contract_claim_candidates_json/1,
27
58
  proof_contract_claim_issues_json/1,
28
- test_claim_obligations_json/4
59
+ test_claim_obligations_json/4,
60
+ claim_model_check_json/4
29
61
  ]).
30
62
 
31
63
  :- use_module(library(http/json)).
32
64
  :- use_module(library(apply)).
33
65
  :- use_module(library(lists)).
34
66
  :- use_module('kb.pl').
67
+ :- use_module('model_check.pl', [model_check_obligation/4, edge_text/3]).
35
68
 
36
69
  %% fact_obligation(+FactId, +ProofKind, -Result)
37
70
  % Result is obligation(Dict), attested, or incompatible(Reason).
38
71
  fact_obligation(FactId, ProofKind, Result) :-
39
- ( fact_set(FactId, Members)
40
- -> ( memberchk(ProofKind, [exhaustive, typecheck])
41
- -> Result = obligation(_{kind: "set_equals", members: Members})
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)
42
81
  ; format(string(Reason),
43
- "~w is a set fact, which an ~w certificate cannot express (use exhaustive or typecheck)",
44
- [FactId, ProofKind]),
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]),
45
84
  Result = incompatible(Reason)
46
85
  )
47
86
  ; fact_numeric_constraint(FactId, Variable, ValueType, Operator, Value)
48
87
  -> ( memberchk(ProofKind, [arithmetic, smt])
49
88
  -> Result = obligation(_{kind: "numeric_constraint", variable: Variable,
50
89
  value_type: ValueType, operator: Operator, value: Value})
51
- ; ( ProofKind == typecheck -> Article = a ; Article = an ),
90
+ ; ( memberchk(ProofKind, [typecheck, source]) -> Article = a ; Article = an ),
52
91
  format(string(Reason),
53
92
  "~w is a numeric fact, which ~w ~w certificate cannot express (use arithmetic or smt)",
54
93
  [FactId, Article, ProofKind]),
55
94
  Result = incompatible(Reason)
56
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
+ )
57
117
  ; Result = attested
58
118
  ).
59
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
+
60
290
  %% claim_obligation(+ReqId, +ClaimKey, +ProofKind, -Result)
61
291
  % As fact_obligation/3 for the fact grounding ClaimKey in ReqId, with the
62
292
  % fact id: obligation(FactId, Dict), attested(FactId), incompatible(FactId,
@@ -102,6 +332,59 @@ obligation_key(Obligation, set(Members)) :-
102
332
  maplist(text_string, Raw, Strings),
103
333
  sort(Strings, Members),
104
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
+ !.
105
388
  obligation_key(Obligation, numeric(Variable, Type, Operator, Value)) :-
106
389
  dict_text(Obligation, kind, "numeric_constraint"),
107
390
  dict_text(Obligation, variable, Variable),
@@ -110,6 +393,12 @@ obligation_key(Obligation, numeric(Variable, Type, Operator, Value)) :-
110
393
  get_dict(value, Obligation, Raw),
111
394
  number_value(Raw, Value).
112
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
+
113
402
  dict_text(Dict, Key, String) :-
114
403
  is_dict(Dict),
115
404
  get_dict(Key, Dict, Raw),
@@ -126,13 +415,15 @@ text_string(Raw, String) :- atom(Raw), !, atom_string(Raw, String).
126
415
  text_string(Raw, String) :- number(Raw), !, number_string(Raw, String).
127
416
  text_string(^^(Raw, _), String) :- !, text_string(Raw, String).
128
417
 
129
- % The members of a set fact, sorted and unique.
130
- fact_set(FactId, Members) :-
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) :-
131
422
  kb_entity(FactId, fact, Props),
132
423
  prop_atom(Props, fact_kind, property_value),
133
424
  ( prop_atom(Props, value_type, set)
134
425
  -> memberchk(value_set=Raw, Props),
135
- kb:normalize_term_atom_list(Raw, Atoms)
426
+ kb:normalize_literal_atom_list(Raw, Atoms)
136
427
  ; prop_atom(Props, property_key, allowed_values),
137
428
  \+ (memberchk(value_type=RawType, Props),
138
429
  kb:normalize_term_atom(RawType, Type),
@@ -144,7 +435,8 @@ fact_set(FactId, Members) :-
144
435
  exclude(==(""), Parts, NonEmpty),
145
436
  maplist([S, A]>>atom_string(A, S), NonEmpty, Atoms)
146
437
  ),
147
- ( prop_atom(Props, operator, Operator) -> Operator == eq ; true ),
438
+ ( prop_atom(Props, operator, Operator0) -> Operator = Operator0 ; Operator = eq ),
439
+ memberchk(Operator, [eq, includes, subset_of, excludes]),
148
440
  \+ prop_atom(Props, polarity, forbid),
149
441
  maplist([A, S]>>atom_string(A, S), Atoms, Strings),
150
442
  sort(Strings, Members),
@@ -311,6 +603,10 @@ certificate_kind(exhaustive).
311
603
  certificate_kind(typecheck).
312
604
  certificate_kind(arithmetic).
313
605
  certificate_kind(smt).
606
+ certificate_kind(rule).
607
+ certificate_kind(state_machine).
608
+ certificate_kind(regex).
609
+ certificate_kind(source).
314
610
 
315
611
  test_certificate_contract(TestId, Kind, Contract) :-
316
612
  kb_entity(TestId, test, Props),
@@ -352,10 +648,14 @@ proof_contract_claim_issue(_{test_id: TestString, claim_key: KeyString, reason:
352
648
 
353
649
  %% proof_contract_claim_candidates_json(-Json)
354
650
  % 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.
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.
359
659
  proof_contract_claim_candidates_json(Json) :-
360
660
  findall(Row, proof_contract_claim_candidates(Row), Rows0),
361
661
  sort(Rows0, Rows),
@@ -364,15 +664,43 @@ proof_contract_claim_candidates_json(Json) :-
364
664
  proof_contract_claim_candidates(_{test_id: TestString, kind: KindString, candidates: Candidates}) :-
365
665
  test_certificate_contract(TestId, Kind, Contract),
366
666
  \+ ( get_dict(proves, Contract, Proves), is_list(Proves), Proves \== [] ),
367
- findall(_{claim_key: KeyString, requirement_id: ReqString, fact_id: FactString},
667
+ findall(Candidate,
368
668
  ( test_requirement(TestId, ReqId),
369
669
  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)
670
+ claim_obligation(ReqId-ClaimKey, Kind, Result),
671
+ claim_candidate(ReqId, ClaimKey, Result, Candidate)
374
672
  ),
375
673
  Candidates0),
376
- sort(Candidates0, Candidates),
674
+ sort(Candidates0, Candidates1),
675
+ sort(claim_key, @=<, Candidates1, Candidates),
377
676
  atom_string(TestId, TestString),
378
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)])).