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 +1 -1
- package/schema/entities.pl +6 -0
- package/schema/validation.pl +4 -1
- package/src/certificate_check.pl +28 -1
- package/src/certificates.pl +9 -1
- package/src/checks.pl +4 -1
- package/src/claim_obligations.pl +378 -0
- package/src/code_certificates.pl +297 -0
- package/src/discovery.pl +160 -2
- package/src/intervals.pl +11 -0
- package/src/kb.pl +7 -0
- package/src/requirement_proof.pl +554 -42
- package/src/rule_registry.pl +8 -0
- package/src/semantic_quality.pl +1 -1
package/package.json
CHANGED
package/schema/entities.pl
CHANGED
|
@@ -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).
|
package/schema/validation.pl
CHANGED
|
@@ -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
|
package/src/certificate_check.pl
CHANGED
|
@@ -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
|
|
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,
|
package/src/certificates.pl
CHANGED
|
@@ -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
|
|
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).
|