kibi-core 0.7.1 → 0.8.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 +1 -1
- package/schema/entities.pl +12 -0
- package/schema/relationships.pl +2 -0
- package/schema/validation.pl +39 -32
- package/src/checks.pl +288 -7
- package/src/logic_ir.pl +629 -0
package/package.json
CHANGED
package/schema/entities.pl
CHANGED
|
@@ -30,6 +30,7 @@ entity_property(_, links, list).
|
|
|
30
30
|
entity_property(_, text_ref, uri).
|
|
31
31
|
entity_property(_, sourceFile, uri).
|
|
32
32
|
entity_property(req, logic_claims, list).
|
|
33
|
+
entity_property(req, semantic_inventory, list).
|
|
33
34
|
|
|
34
35
|
% Typed fact fields - only valid for fact entities
|
|
35
36
|
entity_property(fact, fact_kind, atom).
|
|
@@ -59,6 +60,16 @@ entity_property(fact, argument_descriptions, list).
|
|
|
59
60
|
entity_property(fact, aliases, list).
|
|
60
61
|
entity_property(fact, examples, list).
|
|
61
62
|
entity_property(fact, predicate_args, list).
|
|
63
|
+
entity_property(fact, rule_ir, string).
|
|
64
|
+
entity_property(fact, rule_hash, string).
|
|
65
|
+
% Rule schema references are JSON strings at the public boundary, but the
|
|
66
|
+
% Prolog peer represents entity identifiers as atoms. Accept both forms so
|
|
67
|
+
% decoded rule facts validate without losing the typed reference.
|
|
68
|
+
entity_property(fact, rule_schema_id, atom_or_string).
|
|
69
|
+
entity_property(fact, rule_name, string).
|
|
70
|
+
entity_property(fact, semantic_key, string).
|
|
71
|
+
entity_property(fact, claim_span_start, integer).
|
|
72
|
+
entity_property(fact, claim_span_end, integer).
|
|
62
73
|
|
|
63
74
|
% Typed symbol metadata fields - only valid for symbol entities
|
|
64
75
|
entity_property(symbol, symbol_role, atom).
|
|
@@ -84,6 +95,7 @@ optional_property(Type, severity) :- entity_type(Type).
|
|
|
84
95
|
optional_property(Type, links) :- entity_type(Type).
|
|
85
96
|
optional_property(Type, text_ref) :- entity_type(Type).
|
|
86
97
|
optional_property(req, logic_claims).
|
|
98
|
+
optional_property(req, semantic_inventory).
|
|
87
99
|
optional_property(test, verification_scope).
|
|
88
100
|
optional_property(test, verification_perspective).
|
|
89
101
|
|
package/schema/relationships.pl
CHANGED
|
@@ -19,6 +19,7 @@ relationship_type(supersedes).
|
|
|
19
19
|
relationship_type(constrains).
|
|
20
20
|
relationship_type(requires_property).
|
|
21
21
|
relationship_type(requires_predicate).
|
|
22
|
+
relationship_type(requires_rule).
|
|
22
23
|
|
|
23
24
|
% valid_relationship(RelType, FromType, ToType).
|
|
24
25
|
valid_relationship(depends_on, req, req).
|
|
@@ -40,6 +41,7 @@ valid_relationship(consumes, symbol, event).
|
|
|
40
41
|
valid_relationship(constrains, req, fact).
|
|
41
42
|
valid_relationship(requires_property, req, fact).
|
|
42
43
|
valid_relationship(requires_predicate, req, fact).
|
|
44
|
+
valid_relationship(requires_rule, req, fact).
|
|
43
45
|
|
|
44
46
|
%% supersedes(+NewAdrId, +OldAdrId)
|
|
45
47
|
%% NewAdrId is the decision that replaces OldAdrId.
|
package/schema/validation.pl
CHANGED
|
@@ -42,6 +42,7 @@ validate_property_type(Type, Prop, Value) :-
|
|
|
42
42
|
% check_kind(Kind, Value) succeeds if Value matches Kind
|
|
43
43
|
check_kind(atom, V) :- atom(V).
|
|
44
44
|
check_kind(string, V) :- string(V).
|
|
45
|
+
check_kind(atom_or_string, V) :- atom(V) ; string(V).
|
|
45
46
|
check_kind(datetime, V) :- string(V). % accept ISO strings for now
|
|
46
47
|
check_kind(list, V) :- is_list(V).
|
|
47
48
|
check_kind(uri, V) :- string(V).
|
|
@@ -106,6 +107,13 @@ is_fact_only_field(argument_descriptions).
|
|
|
106
107
|
is_fact_only_field(aliases).
|
|
107
108
|
is_fact_only_field(examples).
|
|
108
109
|
is_fact_only_field(predicate_args).
|
|
110
|
+
is_fact_only_field(rule_ir).
|
|
111
|
+
is_fact_only_field(rule_hash).
|
|
112
|
+
is_fact_only_field(rule_schema_id).
|
|
113
|
+
is_fact_only_field(rule_name).
|
|
114
|
+
is_fact_only_field(semantic_key).
|
|
115
|
+
is_fact_only_field(claim_span_start).
|
|
116
|
+
is_fact_only_field(claim_span_end).
|
|
109
117
|
|
|
110
118
|
% is_test_only_field(+Key) - true if Key is a test-specific field
|
|
111
119
|
is_test_only_field(verification_scope).
|
|
@@ -115,7 +123,7 @@ is_test_only_field(verification_perspective).
|
|
|
115
123
|
validate_fact_shape(subject, Props) :-
|
|
116
124
|
memberchk(subject_key=_Val, Props),
|
|
117
125
|
valid_optional_fact_enums(Props),
|
|
118
|
-
valid_strict_polarity_in_props(Props)
|
|
126
|
+
valid_strict_polarity_in_props(Props), !.
|
|
119
127
|
validate_fact_shape(property_value, Props) :-
|
|
120
128
|
memberchk(subject_key=_Subject, Props),
|
|
121
129
|
memberchk(property_key=_Property, Props),
|
|
@@ -126,14 +134,14 @@ validate_fact_shape(property_value, Props) :-
|
|
|
126
134
|
exactly_one_value_field(Props),
|
|
127
135
|
value_type_matches_field(VT, Props),
|
|
128
136
|
valid_optional_fact_enums(Props),
|
|
129
|
-
valid_strict_polarity_in_props(Props)
|
|
137
|
+
valid_strict_polarity_in_props(Props), !.
|
|
130
138
|
validate_fact_shape(observation, Props) :-
|
|
131
139
|
valid_optional_fact_enums(Props),
|
|
132
|
-
valid_polarity_in_props(Props)
|
|
140
|
+
valid_polarity_in_props(Props), !.
|
|
133
141
|
validate_fact_shape(meta, Props) :-
|
|
134
142
|
% Meta facts are allowed but don't require full strict property tuple yet
|
|
135
143
|
valid_optional_fact_enums(Props),
|
|
136
|
-
valid_polarity_in_props(Props)
|
|
144
|
+
valid_polarity_in_props(Props), !.
|
|
137
145
|
validate_fact_shape(predicate_schema, Props) :-
|
|
138
146
|
memberchk(predicate_name=_Name, Props),
|
|
139
147
|
memberchk(predicate_arity=Arity, Props),
|
|
@@ -143,32 +151,39 @@ validate_fact_shape(predicate_schema, Props) :-
|
|
|
143
151
|
same_length(ArgumentNames, ArgumentTypes),
|
|
144
152
|
length(ArgumentNames, Arity),
|
|
145
153
|
valid_optional_fact_enums(Props),
|
|
146
|
-
valid_polarity_in_props(Props)
|
|
154
|
+
valid_polarity_in_props(Props), !.
|
|
147
155
|
validate_fact_shape(predicate, Props) :-
|
|
148
156
|
memberchk(predicate_name=_Name, Props),
|
|
149
157
|
memberchk(predicate_args=Args, Props),
|
|
150
158
|
Args \= [],
|
|
151
159
|
memberchk(canonical_key=_CanonicalKey, Props),
|
|
152
160
|
valid_optional_fact_enums(Props),
|
|
153
|
-
valid_predicate_polarity_in_props(Props)
|
|
161
|
+
valid_predicate_polarity_in_props(Props), !.
|
|
162
|
+
validate_fact_shape(rule_schema, Props) :-
|
|
163
|
+
memberchk(rule_name=_Name, Props),
|
|
164
|
+
memberchk(argument_names=ArgumentNames, Props),
|
|
165
|
+
memberchk(argument_types=ArgumentTypes, Props),
|
|
166
|
+
same_length(ArgumentNames, ArgumentTypes),
|
|
167
|
+
valid_optional_fact_enums(Props),
|
|
168
|
+
valid_polarity_in_props(Props), !.
|
|
169
|
+
validate_fact_shape(rule, Props) :-
|
|
170
|
+
memberchk(rule_ir=RuleIr, Props),
|
|
171
|
+
nonempty_claim_value(RuleIr),
|
|
172
|
+
memberchk(rule_hash=_Hash, Props),
|
|
173
|
+
memberchk(rule_schema_id=_SchemaId, Props),
|
|
174
|
+
memberchk(semantic_key=_SemanticKey, Props),
|
|
175
|
+
valid_optional_fact_enums(Props),
|
|
176
|
+
valid_polarity_in_props(Props), !.
|
|
154
177
|
validate_fact_shape(Kind, _Props) :-
|
|
155
178
|
% Unknown fact_kind values fail validation
|
|
156
|
-
\+ memberchk(Kind, [subject, property_value, observation, meta, predicate_schema, predicate]),
|
|
179
|
+
\+ memberchk(Kind, [subject, property_value, observation, meta, predicate_schema, predicate, rule_schema, rule]),
|
|
157
180
|
fail.
|
|
158
181
|
|
|
159
182
|
% valid_operator(+Op)
|
|
160
|
-
valid_operator(eq)
|
|
161
|
-
valid_operator(neq).
|
|
162
|
-
valid_operator(lt).
|
|
163
|
-
valid_operator(lte).
|
|
164
|
-
valid_operator(gt).
|
|
165
|
-
valid_operator(gte).
|
|
183
|
+
valid_operator(Op) :- memberchk(Op, [eq, neq, lt, lte, gt, gte]), !.
|
|
166
184
|
|
|
167
185
|
% valid_value_type(+VT)
|
|
168
|
-
valid_value_type(string)
|
|
169
|
-
valid_value_type(int).
|
|
170
|
-
valid_value_type(number).
|
|
171
|
-
valid_value_type(bool).
|
|
186
|
+
valid_value_type(ValueType) :- memberchk(ValueType, [string, int, number, bool]), !.
|
|
172
187
|
|
|
173
188
|
% valid_polarity_in_props(+Props)
|
|
174
189
|
% Validates polarity if present; succeeds if no polarity in props
|
|
@@ -190,8 +205,8 @@ valid_claim_provenance(Props) :-
|
|
|
190
205
|
; \+ memberchk(claim_text=_, Props)
|
|
191
206
|
).
|
|
192
207
|
|
|
193
|
-
nonempty_claim_value(Value) :- string(Value), Value \= ""
|
|
194
|
-
nonempty_claim_value(Value) :- atom(Value), Value \= ''
|
|
208
|
+
nonempty_claim_value(Value) :- string(Value), Value \= "", !.
|
|
209
|
+
nonempty_claim_value(Value) :- atom(Value), Value \= '', !.
|
|
195
210
|
|
|
196
211
|
% valid_optional_fact_enums(+Props)
|
|
197
212
|
% Validates enum-typed fact fields whenever they are present
|
|
@@ -206,25 +221,17 @@ valid_optional_test_enums(Props) :-
|
|
|
206
221
|
( memberchk(verification_perspective=Perspective, Props) -> valid_verification_perspective(Perspective) ; true ).
|
|
207
222
|
|
|
208
223
|
% valid_polarity(+P)
|
|
209
|
-
valid_polarity(require)
|
|
210
|
-
valid_polarity(forbid).
|
|
211
|
-
valid_polarity(assert).
|
|
212
|
-
valid_polarity(deny).
|
|
224
|
+
valid_polarity(Polarity) :- memberchk(Polarity, [require, forbid, assert, deny]), !.
|
|
213
225
|
|
|
214
|
-
valid_strict_polarity(require)
|
|
215
|
-
valid_strict_polarity(forbid).
|
|
226
|
+
valid_strict_polarity(Polarity) :- memberchk(Polarity, [require, forbid]), !.
|
|
216
227
|
|
|
217
|
-
valid_predicate_polarity(assert)
|
|
218
|
-
valid_predicate_polarity(deny).
|
|
228
|
+
valid_predicate_polarity(Polarity) :- memberchk(Polarity, [assert, deny]), !.
|
|
219
229
|
|
|
220
230
|
% valid_verification_scope(+Scope)
|
|
221
|
-
valid_verification_scope(unit)
|
|
222
|
-
valid_verification_scope(integration).
|
|
223
|
-
valid_verification_scope(end_to_end).
|
|
231
|
+
valid_verification_scope(Scope) :- memberchk(Scope, [unit, integration, end_to_end]), !.
|
|
224
232
|
|
|
225
233
|
% valid_verification_perspective(+Perspective)
|
|
226
|
-
valid_verification_perspective(internal)
|
|
227
|
-
valid_verification_perspective(consumer).
|
|
234
|
+
valid_verification_perspective(Perspective) :- memberchk(Perspective, [internal, consumer]), !.
|
|
228
235
|
|
|
229
236
|
% exactly_one_value_field(+Props)
|
|
230
237
|
exactly_one_value_field(Props) :-
|
package/src/checks.pl
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
check_strict_readiness/1, % Returns list of strict readiness audit violations
|
|
20
20
|
check_predicate_verifiability/1,% Returns list of unverifiable predicate-lane requirement links
|
|
21
21
|
check_logic_coverage/1, % Returns list of incomplete declared logical claim manifests
|
|
22
|
+
check_rule_safety/1,
|
|
23
|
+
check_rule_verifiability/1,
|
|
24
|
+
check_semantic_completeness/1,
|
|
22
25
|
run_checks_json/0, % Entry point for JSON output
|
|
23
26
|
violation_id_text/2 % Extract text from entity ID term (exported for testing)
|
|
24
27
|
]).
|
|
@@ -28,6 +31,7 @@
|
|
|
28
31
|
:- use_module('kb.pl').
|
|
29
32
|
:- use_module('../schema/entities.pl', [entity_type/1, required_property/2]).
|
|
30
33
|
:- use_module('../schema/relationships.pl', [relationship_type/1]).
|
|
34
|
+
:- use_module('logic_ir.pl', [logic_rule_safety/2, logic_rule_from_props/2, logic_rule_conflict/3, logic_rules_stratified/1]).
|
|
31
35
|
|
|
32
36
|
% Required fields for all entities
|
|
33
37
|
required_fields([id, title, status, created_at, updated_at, source]).
|
|
@@ -35,7 +39,7 @@ required_fields([id, title, status, created_at, updated_at, source]).
|
|
|
35
39
|
% Relationship types to check for dangling references
|
|
36
40
|
all_relationship_types([
|
|
37
41
|
depends_on, verified_by, validates, specified_by,
|
|
38
|
-
constrains, requires_property, requires_predicate, supersedes, relates_to
|
|
42
|
+
constrains, requires_property, requires_predicate, requires_rule, supersedes, relates_to
|
|
39
43
|
]).
|
|
40
44
|
|
|
41
45
|
%% check_all(-ViolationsDict)
|
|
@@ -56,6 +60,9 @@ check_all(ViolationsDict) :-
|
|
|
56
60
|
check_strict_readiness(StrictReadiness),
|
|
57
61
|
check_predicate_verifiability(PredicateVerifiability),
|
|
58
62
|
check_logic_coverage(LogicCoverage),
|
|
63
|
+
check_rule_safety(RuleSafety),
|
|
64
|
+
check_rule_verifiability(RuleVerifiability),
|
|
65
|
+
check_semantic_completeness(SemanticCompleteness),
|
|
59
66
|
ViolationsDict = _{
|
|
60
67
|
must_priority_coverage: MustPriority,
|
|
61
68
|
symbol_coverage: SymbolCoverage,
|
|
@@ -69,7 +76,10 @@ check_all(ViolationsDict) :-
|
|
|
69
76
|
strict_req_fact_pairing: StrictReqFactPairing,
|
|
70
77
|
strict_readiness: StrictReadiness,
|
|
71
78
|
predicate_verifiability: PredicateVerifiability,
|
|
72
|
-
logic_coverage: LogicCoverage
|
|
79
|
+
logic_coverage: LogicCoverage,
|
|
80
|
+
rule_safety: RuleSafety,
|
|
81
|
+
rule_verifiability: RuleVerifiability,
|
|
82
|
+
semantic_completeness: SemanticCompleteness
|
|
73
83
|
}.
|
|
74
84
|
|
|
75
85
|
%% check_must_priority_coverage(-Violations)
|
|
@@ -408,9 +418,25 @@ strict_fact_shape_violation(violation(
|
|
|
408
418
|
Description = First,
|
|
409
419
|
Suggestion = "Ensure predicate facts have predicate_name, non-empty predicate_args, canonical_key, and assert/deny polarity when present"
|
|
410
420
|
)
|
|
421
|
+
; Kind = rule_schema
|
|
422
|
+
-> findall(Msg, rule_schema_shape_error(Props, Msg), Errors),
|
|
423
|
+
( Errors = []
|
|
424
|
+
-> fail
|
|
425
|
+
; Errors = [First|_],
|
|
426
|
+
Description = First,
|
|
427
|
+
Suggestion = "Ensure rule_schema facts declare rule_name plus same-length argument_names and argument_types"
|
|
428
|
+
)
|
|
429
|
+
; Kind = rule
|
|
430
|
+
-> findall(Msg, rule_shape_error(Props, Msg), Errors),
|
|
431
|
+
( Errors = []
|
|
432
|
+
-> fail
|
|
433
|
+
; Errors = [First|_],
|
|
434
|
+
Description = First,
|
|
435
|
+
Suggestion = "Ensure rule facts contain a safe kibi.logic.v1 rule_ir, full rule_hash, rule_schema_id, rule_name, and semantic_key"
|
|
436
|
+
)
|
|
411
437
|
; % Unknown fact_kind - report as malformed
|
|
412
438
|
format(string(Description), "Unknown fact_kind: ~w", [Kind]),
|
|
413
|
-
Suggestion = "Use one of: subject, property_value, observation, meta, predicate_schema, predicate"
|
|
439
|
+
Suggestion = "Use one of: subject, property_value, observation, meta, predicate_schema, predicate, rule_schema, rule"
|
|
414
440
|
),
|
|
415
441
|
|
|
416
442
|
( memberchk(source=Source0, Props)
|
|
@@ -503,6 +529,42 @@ predicate_shape_error(Props, "Predicate fact polarity must be assert or deny") :
|
|
|
503
529
|
normalize_term_atom(RawPolarity, Polarity),
|
|
504
530
|
\+ memberchk(Polarity, [assert, deny]).
|
|
505
531
|
|
|
532
|
+
rule_schema_shape_error(Props, "Rule schema fact missing required field: rule_name") :-
|
|
533
|
+
\+ memberchk(rule_name=_, Props).
|
|
534
|
+
rule_schema_shape_error(Props, "Rule schema fact missing required field: argument_names") :-
|
|
535
|
+
memberchk(rule_name=_, Props),
|
|
536
|
+
\+ memberchk(argument_names=_, Props).
|
|
537
|
+
rule_schema_shape_error(Props, "Rule schema fact missing required field: argument_types") :-
|
|
538
|
+
memberchk(argument_names=_, Props),
|
|
539
|
+
\+ memberchk(argument_types=_, Props).
|
|
540
|
+
rule_schema_shape_error(Props, "Rule schema fact argument_names and argument_types must have equal lengths") :-
|
|
541
|
+
memberchk(argument_names=RawNames, Props),
|
|
542
|
+
memberchk(argument_types=RawTypes, Props),
|
|
543
|
+
checks_normalize_atom_list(RawNames, Names),
|
|
544
|
+
checks_normalize_atom_list(RawTypes, Types),
|
|
545
|
+
length(Names, NameCount),
|
|
546
|
+
length(Types, TypeCount),
|
|
547
|
+
NameCount =\= TypeCount.
|
|
548
|
+
|
|
549
|
+
rule_shape_error(Props, "Rule fact missing required field: rule_ir") :-
|
|
550
|
+
\+ memberchk(rule_ir=_, Props).
|
|
551
|
+
rule_shape_error(Props, "Rule fact missing required field: rule_hash") :-
|
|
552
|
+
memberchk(rule_ir=_, Props),
|
|
553
|
+
\+ memberchk(rule_hash=_, Props).
|
|
554
|
+
rule_shape_error(Props, "Rule fact missing required field: rule_schema_id") :-
|
|
555
|
+
memberchk(rule_hash=_, Props),
|
|
556
|
+
\+ memberchk(rule_schema_id=_, Props).
|
|
557
|
+
rule_shape_error(Props, "Rule fact missing required field: rule_name") :-
|
|
558
|
+
memberchk(rule_schema_id=_, Props),
|
|
559
|
+
\+ memberchk(rule_name=_, Props).
|
|
560
|
+
rule_shape_error(Props, "Rule fact missing required field: semantic_key") :-
|
|
561
|
+
memberchk(rule_name=_, Props),
|
|
562
|
+
\+ memberchk(semantic_key=_, Props).
|
|
563
|
+
rule_shape_error(Props, "Rule fact rule_ir failed kibi.logic.v1 safety validation") :-
|
|
564
|
+
memberchk(rule_ir=_, Props),
|
|
565
|
+
logic_rule_safety(Props, Errors),
|
|
566
|
+
Errors \= [].
|
|
567
|
+
|
|
506
568
|
checks_normalize_integer(Raw, Integer) :-
|
|
507
569
|
( Raw = ^^(Value, _Type)
|
|
508
570
|
-> checks_normalize_integer(Value, Integer)
|
|
@@ -537,8 +599,41 @@ check_domain_contradictions(Violations) :-
|
|
|
537
599
|
format(string(EntityId), "~w/~w", [ReqA, ReqB]),
|
|
538
600
|
format(string(Description), "~w [strict-readiness: contradiction-ready]", [Reason])
|
|
539
601
|
),
|
|
540
|
-
|
|
541
|
-
)
|
|
602
|
+
StrictViolations
|
|
603
|
+
),
|
|
604
|
+
findall(
|
|
605
|
+
violation(
|
|
606
|
+
'domain-contradictions',
|
|
607
|
+
EntityId,
|
|
608
|
+
Description,
|
|
609
|
+
"Align the opposing rule heads, separate their scopes, or add an explicit supersedes relationship",
|
|
610
|
+
""
|
|
611
|
+
),
|
|
612
|
+
( opposing_rule_requirements(ReqA, ReqB, RuleA, RuleB),
|
|
613
|
+
logic_rule_conflict(RuleA, RuleB, Conflict),
|
|
614
|
+
Conflict \= disjoint,
|
|
615
|
+
format(string(EntityId), "~w/~w", [ReqA, ReqB]),
|
|
616
|
+
format(string(Description), "Rule conflict (~w) between ~w and ~w", [Conflict, ReqA, ReqB])
|
|
617
|
+
),
|
|
618
|
+
RuleViolations
|
|
619
|
+
),
|
|
620
|
+
append(StrictViolations, RuleViolations, Violations).
|
|
621
|
+
|
|
622
|
+
opposing_rule_requirements(ReqA, ReqB, RuleA, RuleB) :-
|
|
623
|
+
kb:current_req(ReqA),
|
|
624
|
+
kb:current_req(ReqB),
|
|
625
|
+
ReqA @< ReqB,
|
|
626
|
+
kb_relationship(requires_rule, ReqA, FactA),
|
|
627
|
+
kb_relationship(requires_rule, ReqB, FactB),
|
|
628
|
+
FactA @< FactB,
|
|
629
|
+
kb_entity(FactA, fact, PropsA),
|
|
630
|
+
kb_entity(FactB, fact, PropsB),
|
|
631
|
+
memberchk(fact_kind=KindA, PropsA),
|
|
632
|
+
memberchk(fact_kind=KindB, PropsB),
|
|
633
|
+
normalize_term_atom(KindA, rule),
|
|
634
|
+
normalize_term_atom(KindB, rule),
|
|
635
|
+
logic_rule_from_props(PropsA, RuleA),
|
|
636
|
+
logic_rule_from_props(PropsB, RuleB).
|
|
542
637
|
|
|
543
638
|
%% check_strict_req_fact_pairing(-Violations)
|
|
544
639
|
% Finds current requirements attempting strict-lane modeling with incomplete
|
|
@@ -816,7 +911,7 @@ logic_coverage_violation(violation(
|
|
|
816
911
|
'logic-coverage',
|
|
817
912
|
ReqId,
|
|
818
913
|
Description,
|
|
819
|
-
"Ground every declared claim with a property_value
|
|
914
|
+
"Ground every declared claim with a property_value, predicate, or rule fact via its typed relationship, preserving the same claim_key",
|
|
820
915
|
Source
|
|
821
916
|
)) :-
|
|
822
917
|
kb:current_req(ReqId),
|
|
@@ -901,6 +996,9 @@ grounded_requirement_claim_fact(ReqId, ClaimKey, FactId) :-
|
|
|
901
996
|
grounded_requirement_claim_fact(ReqId, ClaimKey, FactId) :-
|
|
902
997
|
kb_relationship(requires_predicate, ReqId, FactId),
|
|
903
998
|
ground_fact_claim_key(FactId, predicate, ClaimKey).
|
|
999
|
+
grounded_requirement_claim_fact(ReqId, ClaimKey, FactId) :-
|
|
1000
|
+
kb_relationship(requires_rule, ReqId, FactId),
|
|
1001
|
+
ground_fact_claim_key(FactId, rule, ClaimKey).
|
|
904
1002
|
|
|
905
1003
|
logical_ground_signature(FactId, predicate(Namespace, Name, Args, Polarity)) :-
|
|
906
1004
|
kb:predicate_fact(FactId, Namespace, Name, Args, Polarity).
|
|
@@ -919,6 +1017,14 @@ logical_ground_signature(
|
|
|
919
1017
|
Scope,
|
|
920
1018
|
Polarity
|
|
921
1019
|
).
|
|
1020
|
+
logical_ground_signature(FactId, rule(SemanticKey)) :-
|
|
1021
|
+
kb_entity(FactId, fact, Props),
|
|
1022
|
+
memberchk(fact_kind=RawKind, Props),
|
|
1023
|
+
normalize_term_atom(RawKind, rule),
|
|
1024
|
+
( memberchk(semantic_key=RawKey, Props)
|
|
1025
|
+
-> normalize_term_atom(RawKey, SemanticKey)
|
|
1026
|
+
; memberchk(rule_hash=RawKey, Props), normalize_term_atom(RawKey, SemanticKey)
|
|
1027
|
+
).
|
|
922
1028
|
|
|
923
1029
|
ground_fact_claim_key(FactId, ExpectedKind, ClaimKey) :-
|
|
924
1030
|
kb_entity(FactId, fact, Props),
|
|
@@ -930,6 +1036,175 @@ ground_fact_claim_key(FactId, ExpectedKind, ClaimKey) :-
|
|
|
930
1036
|
normalize_term_atom(RawClaimText, ClaimText),
|
|
931
1037
|
ClaimText \= ''.
|
|
932
1038
|
|
|
1039
|
+
%% check_rule_safety(-Violations)
|
|
1040
|
+
% Every stored rule must decode through the closed Logic IR vocabulary and
|
|
1041
|
+
% pass the independent Prolog-side safety checks.
|
|
1042
|
+
check_rule_safety(Violations) :-
|
|
1043
|
+
findall(Violation, rule_safety_violation(Violation), Raw),
|
|
1044
|
+
sort(Raw, Violations).
|
|
1045
|
+
|
|
1046
|
+
rule_safety_violation(violation(
|
|
1047
|
+
'rule-safety',
|
|
1048
|
+
FactId,
|
|
1049
|
+
Description,
|
|
1050
|
+
"Use kibi.logic.v1 typed IR; remove raw Prolog, unsafe variables, or unstratified negation",
|
|
1051
|
+
Source
|
|
1052
|
+
)) :-
|
|
1053
|
+
kb_entity(FactId, fact, Props),
|
|
1054
|
+
memberchk(fact_kind=RawKind, Props),
|
|
1055
|
+
normalize_term_atom(RawKind, rule),
|
|
1056
|
+
logic_rule_safety(Props, Errors),
|
|
1057
|
+
Errors \= [],
|
|
1058
|
+
format(string(Description), "Rule fact ~w failed safety validation: ~w", [FactId, Errors]),
|
|
1059
|
+
violation_source(FactId, fact, Source).
|
|
1060
|
+
|
|
1061
|
+
% Local rule checks cannot detect a negation cycle spanning multiple facts.
|
|
1062
|
+
% Run the finite dependency-graph check over the complete stored rule set and
|
|
1063
|
+
% report each participating negated rule as infrastructure-grade unsafe IR.
|
|
1064
|
+
rule_safety_violation(violation(
|
|
1065
|
+
'rule-safety',
|
|
1066
|
+
FactId,
|
|
1067
|
+
"Stored rules contain an unstratified negation dependency cycle",
|
|
1068
|
+
"Separate the negated dependency into strata or replace it with an explicit closed-world fact",
|
|
1069
|
+
Source
|
|
1070
|
+
)) :-
|
|
1071
|
+
findall(Rule, stored_rule_term(Rule), Rules),
|
|
1072
|
+
Rules \= [],
|
|
1073
|
+
\+ logic_rules_stratified(Rules),
|
|
1074
|
+
stored_rule_fact(FactId, _Props, Rule),
|
|
1075
|
+
rule_contains_negation(Rule),
|
|
1076
|
+
violation_source(FactId, fact, Source).
|
|
1077
|
+
|
|
1078
|
+
stored_rule_term(Rule) :-
|
|
1079
|
+
kb_entity(_FactId, fact, Props),
|
|
1080
|
+
memberchk(fact_kind=RawKind, Props),
|
|
1081
|
+
normalize_term_atom(RawKind, rule),
|
|
1082
|
+
logic_rule_from_props(Props, Rule).
|
|
1083
|
+
|
|
1084
|
+
stored_rule_fact(FactId, Props, Rule) :-
|
|
1085
|
+
kb_entity(FactId, fact, Props),
|
|
1086
|
+
memberchk(fact_kind=RawKind, Props),
|
|
1087
|
+
normalize_term_atom(RawKind, rule),
|
|
1088
|
+
logic_rule_from_props(Props, Rule).
|
|
1089
|
+
|
|
1090
|
+
rule_contains_negation(rule(_, _, _Head, Body, Exceptions, _Scope, _From, _To, _Schema, _Variables)) :-
|
|
1091
|
+
( expression_contains_negation(Body)
|
|
1092
|
+
; member(Exception, Exceptions), expression_contains_negation(Exception)
|
|
1093
|
+
),
|
|
1094
|
+
!.
|
|
1095
|
+
|
|
1096
|
+
expression_contains_negation(not(_)).
|
|
1097
|
+
expression_contains_negation(all(Items)) :- member(Item, Items), expression_contains_negation(Item).
|
|
1098
|
+
expression_contains_negation(any(Items)) :- member(Item, Items), expression_contains_negation(Item).
|
|
1099
|
+
expression_contains_negation(count(Atom, _Operator, _Value)) :- expression_contains_negation(Atom).
|
|
1100
|
+
|
|
1101
|
+
%% check_rule_verifiability(-Violations)
|
|
1102
|
+
% A requires_rule edge is meaningful only when it reaches a rule fact whose
|
|
1103
|
+
% schema endpoint exists and whose rule IR is safe.
|
|
1104
|
+
check_rule_verifiability(Violations) :-
|
|
1105
|
+
findall(Violation, rule_verifiability_violation(Violation), Raw),
|
|
1106
|
+
sort(Raw, Violations).
|
|
1107
|
+
|
|
1108
|
+
rule_verifiability_violation(violation(
|
|
1109
|
+
'rule-verifiability',
|
|
1110
|
+
ReqId,
|
|
1111
|
+
Description,
|
|
1112
|
+
"Link requires_rule to fact_kind=rule with a valid rule_schema_id and safe kibi.logic.v1 IR",
|
|
1113
|
+
Source
|
|
1114
|
+
)) :-
|
|
1115
|
+
kb:current_req(ReqId),
|
|
1116
|
+
kb_relationship(requires_rule, ReqId, FactId),
|
|
1117
|
+
kb_entity(FactId, fact, Props),
|
|
1118
|
+
rule_verifiability_description(Props, FactId, Description),
|
|
1119
|
+
violation_source(ReqId, req, Source).
|
|
1120
|
+
|
|
1121
|
+
rule_verifiability_description(Props, FactId, Description) :-
|
|
1122
|
+
( memberchk(fact_kind=RawKind, Props)
|
|
1123
|
+
-> normalize_term_atom(RawKind, Kind)
|
|
1124
|
+
; Kind = legacy
|
|
1125
|
+
),
|
|
1126
|
+
Kind \= rule,
|
|
1127
|
+
format(string(Description), "Requirement uses requires_rule ~w but target is fact_kind=~w", [FactId, Kind]).
|
|
1128
|
+
rule_verifiability_description(Props, FactId, Description) :-
|
|
1129
|
+
memberchk(fact_kind=RawKind, Props),
|
|
1130
|
+
normalize_term_atom(RawKind, rule),
|
|
1131
|
+
\+ memberchk(rule_schema_id=_, Props),
|
|
1132
|
+
format(string(Description), "Rule fact ~w has no rule_schema_id", [FactId]).
|
|
1133
|
+
rule_verifiability_description(Props, FactId, Description) :-
|
|
1134
|
+
memberchk(fact_kind=RawKind, Props),
|
|
1135
|
+
normalize_term_atom(RawKind, rule),
|
|
1136
|
+
memberchk(rule_schema_id=SchemaId, Props),
|
|
1137
|
+
\+ kb_entity(SchemaId, fact, _),
|
|
1138
|
+
format(string(Description), "Rule fact ~w references missing rule schema ~w", [FactId, SchemaId]).
|
|
1139
|
+
rule_verifiability_description(Props, FactId, Description) :-
|
|
1140
|
+
memberchk(fact_kind=RawKind, Props),
|
|
1141
|
+
normalize_term_atom(RawKind, rule),
|
|
1142
|
+
memberchk(rule_schema_id=SchemaId, Props),
|
|
1143
|
+
kb_entity(SchemaId, fact, SchemaProps),
|
|
1144
|
+
\+ valid_rule_schema_props(SchemaProps),
|
|
1145
|
+
format(string(Description), "Rule fact ~w references ~w, which is not a rule_schema fact", [FactId, SchemaId]).
|
|
1146
|
+
rule_verifiability_description(Props, FactId, Description) :-
|
|
1147
|
+
memberchk(fact_kind=RawKind, Props),
|
|
1148
|
+
normalize_term_atom(RawKind, rule),
|
|
1149
|
+
logic_rule_safety(Props, Errors),
|
|
1150
|
+
Errors \= [],
|
|
1151
|
+
format(string(Description), "Rule fact ~w is not safe: ~w", [FactId, Errors]).
|
|
1152
|
+
|
|
1153
|
+
valid_rule_schema_props(Props) :-
|
|
1154
|
+
memberchk(fact_kind=RawKind, Props),
|
|
1155
|
+
normalize_term_atom(RawKind, rule_schema),
|
|
1156
|
+
memberchk(rule_name=Name, Props),
|
|
1157
|
+
nonempty_normalized(Name),
|
|
1158
|
+
memberchk(argument_names=RawNames, Props),
|
|
1159
|
+
memberchk(argument_types=RawTypes, Props),
|
|
1160
|
+
checks_normalize_atom_list(RawNames, Names),
|
|
1161
|
+
checks_normalize_atom_list(RawTypes, Types),
|
|
1162
|
+
same_length(Names, Types).
|
|
1163
|
+
|
|
1164
|
+
nonempty_normalized(Value) :-
|
|
1165
|
+
normalize_term_atom(Value, Atom),
|
|
1166
|
+
Atom \= ''.
|
|
1167
|
+
|
|
1168
|
+
%% check_semantic_completeness(-Violations)
|
|
1169
|
+
% The proposition ledger is additive and optional during migration. When it is
|
|
1170
|
+
% present, every assertive proposition must either be modeled or explicitly
|
|
1171
|
+
% recorded as an unresolved review state; silently missing entries are errors.
|
|
1172
|
+
check_semantic_completeness(Violations) :-
|
|
1173
|
+
findall(Violation, semantic_completeness_violation(Violation), Raw),
|
|
1174
|
+
sort(Raw, Violations).
|
|
1175
|
+
|
|
1176
|
+
semantic_completeness_violation(violation(
|
|
1177
|
+
'semantic-completeness',
|
|
1178
|
+
ReqId,
|
|
1179
|
+
Description,
|
|
1180
|
+
"Run kb_semantic_advisor again, preserve every proposition span, and model or explicitly classify each assertive proposition",
|
|
1181
|
+
Source
|
|
1182
|
+
)) :-
|
|
1183
|
+
kb:current_req(ReqId),
|
|
1184
|
+
kb_entity(ReqId, req, Props),
|
|
1185
|
+
memberchk(semantic_inventory=RawInventory, Props),
|
|
1186
|
+
inventory_entries(RawInventory, Entries),
|
|
1187
|
+
member(Entry, Entries),
|
|
1188
|
+
inventory_entry_status(Entry, Status),
|
|
1189
|
+
memberchk(Status, [missing]),
|
|
1190
|
+
format(string(Description), "Requirement proposition ledger contains an unclassified assertive span (~w)", [Entry]),
|
|
1191
|
+
violation_source(ReqId, req, Source).
|
|
1192
|
+
|
|
1193
|
+
inventory_entries(Raw, Entries) :-
|
|
1194
|
+
( is_list(Raw) -> Entries = Raw
|
|
1195
|
+
; Raw = ^^(Value, _) -> inventory_entries(Value, Entries)
|
|
1196
|
+
; (atom(Raw) ; string(Raw)),
|
|
1197
|
+
catch(atom_json_term(Raw, JsonEntries, [value_string_as(string)]), _, fail),
|
|
1198
|
+
is_list(JsonEntries)
|
|
1199
|
+
-> Entries = JsonEntries
|
|
1200
|
+
; Entries = []
|
|
1201
|
+
).
|
|
1202
|
+
|
|
1203
|
+
inventory_entry_status(Entry, Status) :-
|
|
1204
|
+
is_dict(Entry), get_dict(status, Entry, Raw), normalize_term_atom(Raw, Status).
|
|
1205
|
+
inventory_entry_status(Entry, Status) :-
|
|
1206
|
+
is_list(Entry), memberchk(status=Raw, Entry), normalize_term_atom(Raw, Status).
|
|
1207
|
+
|
|
933
1208
|
strict_readiness_has_fact_link(ReqId) :-
|
|
934
1209
|
kb_relationship(constrains, ReqId, FactId),
|
|
935
1210
|
kb_entity(FactId, fact, _),
|
|
@@ -1004,6 +1279,9 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
|
|
|
1004
1279
|
check_strict_readiness(StrictReadiness),
|
|
1005
1280
|
check_predicate_verifiability(PredicateVerifiability),
|
|
1006
1281
|
check_logic_coverage(LogicCoverage),
|
|
1282
|
+
check_rule_safety(RuleSafety),
|
|
1283
|
+
check_rule_verifiability(RuleVerifiability),
|
|
1284
|
+
check_semantic_completeness(SemanticCompleteness),
|
|
1007
1285
|
ViolationsDict = _{
|
|
1008
1286
|
must_priority_coverage: MustPriority,
|
|
1009
1287
|
symbol_coverage: SymbolCoverage,
|
|
@@ -1017,7 +1295,10 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
|
|
|
1017
1295
|
strict_req_fact_pairing: StrictReqFactPairing,
|
|
1018
1296
|
strict_readiness: StrictReadiness,
|
|
1019
1297
|
predicate_verifiability: PredicateVerifiability,
|
|
1020
|
-
logic_coverage: LogicCoverage
|
|
1298
|
+
logic_coverage: LogicCoverage,
|
|
1299
|
+
rule_safety: RuleSafety,
|
|
1300
|
+
rule_verifiability: RuleVerifiability,
|
|
1301
|
+
semantic_completeness: SemanticCompleteness
|
|
1021
1302
|
}.
|
|
1022
1303
|
|
|
1023
1304
|
%% violations_dict_to_json(+ViolationsDict, -JsonDict)
|
package/src/logic_ir.pl
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
% Safe, data-driven interpreter for kibi.logic.v1.
|
|
2
|
+
%
|
|
3
|
+
% This module deliberately parses a small JSON IR into ordinary Prolog terms.
|
|
4
|
+
% It never consults or asserts caller-provided source. The terms are evaluated
|
|
5
|
+
% by a bounded interpreter and are suitable for proof witnesses.
|
|
6
|
+
:- module(logic_ir, [
|
|
7
|
+
logic_rule_from_props/2,
|
|
8
|
+
logic_rule_safety/2,
|
|
9
|
+
logic_rule_render/2,
|
|
10
|
+
logic_rule_semantic_key/2,
|
|
11
|
+
logic_derive/2,
|
|
12
|
+
logic_rule_conflict/3,
|
|
13
|
+
logic_rule_conflict_witness/3,
|
|
14
|
+
logic_atom_signature/2,
|
|
15
|
+
logic_rules_stratified/1
|
|
16
|
+
]).
|
|
17
|
+
|
|
18
|
+
:- use_module(library(http/json)).
|
|
19
|
+
:- use_module(library(aggregate)).
|
|
20
|
+
:- use_module(library(clpfd)).
|
|
21
|
+
:- use_module('kb.pl', [kb_entity/3, predicate_fact/5]).
|
|
22
|
+
|
|
23
|
+
%% logic_rule_from_props(+Props, -Rule)
|
|
24
|
+
% Props is an entity property list from a fact_kind=rule fact.
|
|
25
|
+
logic_rule_from_props(Props, Rule) :-
|
|
26
|
+
memberchk(rule_ir=Raw, Props),
|
|
27
|
+
raw_json_atom(Raw, Json),
|
|
28
|
+
catch(atom_json_dict(Json, Dict, [value_string_as(atom)]), _, fail),
|
|
29
|
+
json_rule(Dict, Rule).
|
|
30
|
+
|
|
31
|
+
raw_json_atom(^^(Value, _), Atom) :- !, raw_json_atom(Value, Atom).
|
|
32
|
+
raw_json_atom(literal(type(_, Value)), Atom) :- !, raw_json_atom(Value, Atom).
|
|
33
|
+
raw_json_atom(Value, Atom) :- atom(Value), !, Atom = Value.
|
|
34
|
+
raw_json_atom(Value, Atom) :- string(Value), !, atom_string(Atom, Value).
|
|
35
|
+
|
|
36
|
+
json_rule(Dict, rule(Kind, Modality, Head, Body, Exceptions, Scope, From, To, SchemaId, Variables)) :-
|
|
37
|
+
json_rule_keys(Dict),
|
|
38
|
+
get_dict(version, Dict, Version),
|
|
39
|
+
json_text(Version, 'kibi.logic.v1'),
|
|
40
|
+
json_text(Dict.kind, Kind),
|
|
41
|
+
json_modality(Dict.modality, Modality),
|
|
42
|
+
( get_dict(head, Dict, HeadDict) -> json_atom(HeadDict, Head) ; Head = none ),
|
|
43
|
+
( get_dict(body, Dict, BodyDict) -> json_expression(BodyDict, Body) ; Body = none ),
|
|
44
|
+
( get_dict(exceptions, Dict, ExceptionDicts) -> maplist(json_expression, ExceptionDicts, Exceptions) ; Exceptions = [] ),
|
|
45
|
+
( get_dict(scope, Dict, ScopeDict) -> json_scope(ScopeDict, Scope) ; Scope = scope('', '', []) ),
|
|
46
|
+
( get_dict(validFrom, Dict, RawFrom) -> json_timestamp(RawFrom, From) ; From = '' ),
|
|
47
|
+
( get_dict(validTo, Dict, RawTo) -> json_timestamp(RawTo, To) ; To = '' ),
|
|
48
|
+
( get_dict(ruleSchemaId, Dict, RawSchemaId) -> json_atom_name(RawSchemaId, SchemaId), valid_schema_id(SchemaId) ; SchemaId = '' ),
|
|
49
|
+
( get_dict(variables, Dict, RawVariables) -> maplist(json_variable, RawVariables, Variables) ; Variables = [] ).
|
|
50
|
+
|
|
51
|
+
json_rule_keys(Dict) :-
|
|
52
|
+
dict_pairs(Dict, _Tag, Pairs),
|
|
53
|
+
forall(member(Key-_, Pairs), memberchk(Key, [version, kind, modality, head, body, variables, exceptions, scope, validFrom, validTo, ruleSchemaId])).
|
|
54
|
+
|
|
55
|
+
json_variable(Dict, variable(Name, Type, Quantifier)) :-
|
|
56
|
+
is_dict(Dict),
|
|
57
|
+
dict_keys_allowed(Dict, [name, type, quantifier]),
|
|
58
|
+
get_dict(name, Dict, RawName), json_atom_name(RawName, Name),
|
|
59
|
+
valid_variable_name(Name),
|
|
60
|
+
get_dict(type, Dict, RawType), json_atom_name(RawType, Type), valid_type_name(Type),
|
|
61
|
+
( get_dict(quantifier, Dict, RawQuantifier) -> json_quantifier(RawQuantifier, Quantifier) ; Quantifier = forall ).
|
|
62
|
+
|
|
63
|
+
json_quantifier(Value, Value) :- memberchk(Value, [forall, exists]), !.
|
|
64
|
+
json_quantifier(Value, Quantifier) :- string(Value), atom_string(Atom, Value), json_quantifier(Atom, Quantifier).
|
|
65
|
+
|
|
66
|
+
json_modality(Value, Value) :- memberchk(Value, [assert, deny, oblige, permit, forbid]), !.
|
|
67
|
+
json_modality(Value, Value) :- string(Value), atom_string(Atom, Value), json_modality(Atom, Value).
|
|
68
|
+
|
|
69
|
+
json_atom(Dict, atom(Namespace, Name, Args, Polarity, ClosedWorld)) :-
|
|
70
|
+
is_dict(Dict),
|
|
71
|
+
dict_keys_allowed(Dict, [kind, namespace, name, args, polarity, closedWorld]),
|
|
72
|
+
get_dict(kind, Dict, Kind),
|
|
73
|
+
json_text(Kind, atom),
|
|
74
|
+
get_dict(name, Dict, RawName),
|
|
75
|
+
json_atom_name(RawName, Name), valid_predicate_name(Name),
|
|
76
|
+
( get_dict(namespace, Dict, RawNamespace) -> json_atom_name(RawNamespace, Namespace), valid_predicate_name(Namespace) ; Namespace = default ),
|
|
77
|
+
get_dict(args, Dict, RawArgs),
|
|
78
|
+
is_list(RawArgs),
|
|
79
|
+
length(RawArgs, Arity), Arity =< 8,
|
|
80
|
+
maplist(json_term, RawArgs, Args),
|
|
81
|
+
( get_dict(polarity, Dict, RawPolarity) -> json_polarity(RawPolarity, Polarity) ; Polarity = positive ),
|
|
82
|
+
( get_dict(closedWorld, Dict, RawClosedWorld) -> memberchk(RawClosedWorld, [true, false]), ClosedWorld = RawClosedWorld ; ClosedWorld = false ).
|
|
83
|
+
|
|
84
|
+
json_atom_name(Value, Atom) :- atom(Value), !, Atom = Value.
|
|
85
|
+
json_atom_name(Value, Atom) :- string(Value), !, atom_string(Atom, Value).
|
|
86
|
+
|
|
87
|
+
valid_variable_name(Name) :-
|
|
88
|
+
atom(Name),
|
|
89
|
+
atom_chars(Name, [First|Rest]),
|
|
90
|
+
char_type(First, upper),
|
|
91
|
+
forall(member(Char, Rest), (char_type(Char, alnum) ; Char == '_')).
|
|
92
|
+
|
|
93
|
+
valid_type_name(Name) :-
|
|
94
|
+
atom(Name),
|
|
95
|
+
atom_chars(Name, [First|Rest]),
|
|
96
|
+
char_type(First, lower),
|
|
97
|
+
forall(member(Char, Rest), (char_type(Char, alnum) ; memberchk(Char, ['_', '-', ':', '/', '.']))).
|
|
98
|
+
|
|
99
|
+
valid_schema_id(Name) :-
|
|
100
|
+
atom(Name),
|
|
101
|
+
atom_chars(Name, [First|Rest]),
|
|
102
|
+
char_type(First, alnum),
|
|
103
|
+
forall(member(Char, Rest), (char_type(Char, alnum) ; memberchk(Char, ['_', '-', ':', '/', '.']))).
|
|
104
|
+
|
|
105
|
+
json_timestamp(Value, Timestamp) :-
|
|
106
|
+
json_atom_name(Value, Timestamp),
|
|
107
|
+
parse_time(Timestamp, iso_8601, _).
|
|
108
|
+
|
|
109
|
+
valid_predicate_name(Name) :-
|
|
110
|
+
atom(Name),
|
|
111
|
+
atom_chars(Name, [First|Rest]),
|
|
112
|
+
char_type(First, lower),
|
|
113
|
+
forall(member(Char, Rest), allowed_predicate_char(Char)).
|
|
114
|
+
|
|
115
|
+
allowed_predicate_char(Char) :- char_type(Char, alnum), !.
|
|
116
|
+
allowed_predicate_char('_').
|
|
117
|
+
allowed_predicate_char('-').
|
|
118
|
+
allowed_predicate_char('/').
|
|
119
|
+
allowed_predicate_char('.').
|
|
120
|
+
|
|
121
|
+
json_text(Value, Expected) :- atom(Value), !, Value = Expected.
|
|
122
|
+
json_text(Value, Expected) :- string(Value), atom_string(Expected, Value).
|
|
123
|
+
|
|
124
|
+
json_polarity(Value, Value) :- memberchk(Value, [positive, negative]), !.
|
|
125
|
+
json_polarity(Value, Polarity) :- string(Value), atom_string(Atom, Value), json_polarity(Atom, Polarity).
|
|
126
|
+
|
|
127
|
+
json_term(Dict, var(Name, Type)) :-
|
|
128
|
+
is_dict(Dict), dict_keys_allowed(Dict, [kind, name, type]), get_dict(kind, Dict, Kind), json_text(Kind, var),
|
|
129
|
+
get_dict(name, Dict, RawName), json_atom_name(RawName, Name), valid_variable_name(Name),
|
|
130
|
+
get_dict(type, Dict, RawType), json_atom_name(RawType, Type), valid_type_name(Type), !.
|
|
131
|
+
json_term(Dict, const(Value, Type)) :-
|
|
132
|
+
is_dict(Dict), dict_keys_allowed(Dict, [kind, value, type]), get_dict(kind, Dict, Kind), json_text(Kind, const),
|
|
133
|
+
get_dict(value, Dict, RawValue), json_atom_name(RawValue, Value),
|
|
134
|
+
( get_dict(type, Dict, RawType) -> json_atom_name(RawType, Type) ; Type = string ), !.
|
|
135
|
+
json_term(Dict, number(Value, Unit)) :-
|
|
136
|
+
is_dict(Dict), dict_keys_allowed(Dict, [kind, value, unit]), get_dict(kind, Dict, Kind), json_text(Kind, number),
|
|
137
|
+
get_dict(value, Dict, Value), number(Value), Value =:= Value,
|
|
138
|
+
( get_dict(unit, Dict, RawUnit) -> json_atom_name(RawUnit, Unit), valid_type_name(Unit) ; Unit = none ), !.
|
|
139
|
+
json_term(Dict, duration(Value, Unit)) :-
|
|
140
|
+
is_dict(Dict), dict_keys_allowed(Dict, [kind, value, unit]), get_dict(kind, Dict, Kind), json_text(Kind, duration),
|
|
141
|
+
get_dict(value, Dict, Value), number(Value), Value =:= Value, Value >= 0,
|
|
142
|
+
get_dict(unit, Dict, RawUnit), json_atom_name(RawUnit, Unit), memberchk(Unit, [ms, s, m, h, d, w]), !.
|
|
143
|
+
json_term(Dict, timestamp(Value)) :-
|
|
144
|
+
is_dict(Dict), dict_keys_allowed(Dict, [kind, value]), get_dict(kind, Dict, Kind), json_text(Kind, timestamp),
|
|
145
|
+
get_dict(value, Dict, RawValue), json_timestamp(RawValue, Value), !.
|
|
146
|
+
json_term(Dict, interval(Start, End)) :-
|
|
147
|
+
is_dict(Dict), dict_keys_allowed(Dict, [kind, start, end]), get_dict(kind, Dict, Kind), json_text(Kind, interval),
|
|
148
|
+
get_dict(start, Dict, RawStart), json_timestamp(RawStart, Start),
|
|
149
|
+
get_dict(end, Dict, RawEnd), json_timestamp(RawEnd, End), Start @=< End, !.
|
|
150
|
+
|
|
151
|
+
json_expression(Dict, Atom) :- is_dict(Dict), dict_keys_allowed(Dict, [kind, namespace, name, args, polarity, closedWorld]), get_dict(kind, Dict, Kind), json_text(Kind, atom), !, json_atom(Dict, Atom).
|
|
152
|
+
json_expression(Dict, all(Items)) :- is_dict(Dict), dict_keys_allowed(Dict, [kind, items]), get_dict(kind, Dict, Kind), json_text(Kind, all), !, get_dict(items, Dict, Raw), is_list(Raw), Raw \= [], length(Raw, Count), Count =< 32, maplist(json_expression, Raw, Items).
|
|
153
|
+
json_expression(Dict, any(Items)) :- is_dict(Dict), dict_keys_allowed(Dict, [kind, items]), get_dict(kind, Dict, Kind), json_text(Kind, any), !, get_dict(items, Dict, Raw), is_list(Raw), Raw \= [], length(Raw, Count), Count =< 32, maplist(json_expression, Raw, Items).
|
|
154
|
+
json_expression(Dict, not(Atom)) :- is_dict(Dict), dict_keys_allowed(Dict, [kind, item]), get_dict(kind, Dict, Kind), json_text(Kind, not), !, get_dict(item, Dict, Raw), json_atom(Raw, Atom).
|
|
155
|
+
json_expression(Dict, compare(Op, Left, Right)) :- is_dict(Dict), dict_keys_allowed(Dict, [kind, operator, left, right]), get_dict(kind, Dict, Kind), json_text(Kind, compare), !, get_dict(operator, Dict, OpRaw), json_atom_name(OpRaw, Op), valid_compare_operator(Op), get_dict(left, Dict, LeftRaw), get_dict(right, Dict, RightRaw), json_term(LeftRaw, Left), json_term(RightRaw, Right).
|
|
156
|
+
json_expression(Dict, count(Atom, Op, Value)) :- is_dict(Dict), dict_keys_allowed(Dict, [kind, atom, operator, value]), get_dict(kind, Dict, Kind), json_text(Kind, count), !, get_dict(atom, Dict, AtomRaw), json_atom(AtomRaw, Atom), get_dict(operator, Dict, OpRaw), json_atom_name(OpRaw, Op), valid_compare_operator(Op), get_dict(value, Dict, Value), integer(Value), Value >= 0.
|
|
157
|
+
json_expression(Dict, temporal(Relation, Left, Right)) :- is_dict(Dict), dict_keys_allowed(Dict, [kind, relation, left, right]), get_dict(kind, Dict, Kind), json_text(Kind, temporal), !, get_dict(relation, Dict, RelationRaw), json_atom_name(RelationRaw, Relation), valid_temporal_relation(Relation), get_dict(left, Dict, LeftRaw), get_dict(right, Dict, RightRaw), json_term(LeftRaw, Left), json_term(RightRaw, Right).
|
|
158
|
+
|
|
159
|
+
valid_compare_operator(Op) :- memberchk(Op, [eq, neq, lt, lte, gt, gte]).
|
|
160
|
+
valid_temporal_relation(Relation) :- memberchk(Relation, [before, after, during, overlaps, starts, finishes]).
|
|
161
|
+
|
|
162
|
+
json_scope(Dict, scope(Authority, Name, Tags)) :-
|
|
163
|
+
dict_keys_allowed(Dict, [authority, name, tags]),
|
|
164
|
+
( get_dict(authority, Dict, Authority0) -> json_atom_name(Authority0, Authority) ; Authority = '' ),
|
|
165
|
+
( get_dict(name, Dict, Name0) -> json_atom_name(Name0, Name) ; Name = '' ),
|
|
166
|
+
( get_dict(tags, Dict, Tags0) -> maplist(json_atom_name, Tags0, Tags) ; Tags = [] ).
|
|
167
|
+
|
|
168
|
+
dict_keys_allowed(Dict, Allowed) :-
|
|
169
|
+
dict_pairs(Dict, _Tag, Pairs),
|
|
170
|
+
forall(member(Key-_, Pairs), memberchk(Key, Allowed)).
|
|
171
|
+
|
|
172
|
+
%% logic_rule_safety(+Props, -Errors)
|
|
173
|
+
% Independent Prolog-side safety check. It intentionally rejects anything
|
|
174
|
+
% that the JSON decoder cannot map into the closed IR vocabulary.
|
|
175
|
+
logic_rule_safety(Props, Errors) :-
|
|
176
|
+
( logic_rule_from_props(Props, Rule)
|
|
177
|
+
-> rule_safety_errors(Rule, Errors)
|
|
178
|
+
; Errors = [malformed_rule_ir]
|
|
179
|
+
).
|
|
180
|
+
|
|
181
|
+
rule_safety_errors(rule(Kind, _Modality, Head, Body, Exceptions, _Scope, _From, _To, _Schema, Variables), Errors) :-
|
|
182
|
+
findall(Error, rule_shape_error(Kind, Head, Body, Error), ShapeErrors),
|
|
183
|
+
findall(Error, expression_safety_error(Body, Error), BodyErrors),
|
|
184
|
+
findall(Error, (member(Exception, Exceptions), expression_safety_error(Exception, Error)), ExceptionErrors),
|
|
185
|
+
findall(Error, variable_safety_error(Head, Body, Variables, Error), VariableErrors),
|
|
186
|
+
findall(Error, undeclared_variable_error(Head, Body, Exceptions, Variables, Error), DeclarationErrors),
|
|
187
|
+
findall(Error, (member(Exception, Exceptions), exception_variable_safety_error(Exception, Body, Error)), ExceptionVariableErrors),
|
|
188
|
+
findall(Error, rule_term_safety_error(Head, Body, Exceptions, Error), TermErrors),
|
|
189
|
+
findall(Error, rule_limit_error(Head, Body, Exceptions, Error), LimitErrors),
|
|
190
|
+
append([ShapeErrors, BodyErrors, ExceptionErrors, VariableErrors, DeclarationErrors, ExceptionVariableErrors, TermErrors, LimitErrors], Errors0),
|
|
191
|
+
sort(Errors0, Errors).
|
|
192
|
+
|
|
193
|
+
rule_term_safety_error(Head, Body, Exceptions, invalid_interval) :-
|
|
194
|
+
member(Term, [Head, Body|Exceptions]),
|
|
195
|
+
expression_term(Term, interval(Start, End)),
|
|
196
|
+
Start @> End.
|
|
197
|
+
|
|
198
|
+
rule_limit_error(Head, Body, Exceptions, too_many_atoms) :-
|
|
199
|
+
expression_atom_count(Head, HeadCount),
|
|
200
|
+
expression_atom_count(Body, BodyCount),
|
|
201
|
+
maplist(expression_atom_count, Exceptions, ExceptionCounts),
|
|
202
|
+
sum_list([HeadCount, BodyCount|ExceptionCounts], Count),
|
|
203
|
+
Count > 64.
|
|
204
|
+
rule_limit_error(Head, Body, Exceptions, expression_depth_exceeded) :-
|
|
205
|
+
member(Term, [Head, Body|Exceptions]),
|
|
206
|
+
expression_depth(Term, Depth),
|
|
207
|
+
Depth > 16.
|
|
208
|
+
|
|
209
|
+
expression_atom_count(none, 0).
|
|
210
|
+
expression_atom_count(atom(_, _, _, _, _), 1).
|
|
211
|
+
expression_atom_count(all(Items), Count) :- maplist(expression_atom_count, Items, Counts), sum_list(Counts, ChildCount), Count is ChildCount.
|
|
212
|
+
expression_atom_count(any(Items), Count) :- maplist(expression_atom_count, Items, Counts), sum_list(Counts, ChildCount), Count is ChildCount.
|
|
213
|
+
expression_atom_count(not(Atom), Count) :- expression_atom_count(Atom, ChildCount), Count is ChildCount.
|
|
214
|
+
expression_atom_count(compare(_, _, _), 0).
|
|
215
|
+
expression_atom_count(count(Atom, _, _), Count) :- expression_atom_count(Atom, Count).
|
|
216
|
+
expression_atom_count(temporal(_, _, _), 0).
|
|
217
|
+
|
|
218
|
+
expression_depth(none, 0).
|
|
219
|
+
expression_depth(atom(_, _, _, _, _), 1).
|
|
220
|
+
expression_depth(all(Items), Depth) :- maplist(expression_depth, Items, Depths), max_list([0|Depths], ChildDepth), Depth is ChildDepth + 1.
|
|
221
|
+
expression_depth(any(Items), Depth) :- maplist(expression_depth, Items, Depths), max_list([0|Depths], ChildDepth), Depth is ChildDepth + 1.
|
|
222
|
+
expression_depth(not(Atom), Depth) :- expression_depth(Atom, ChildDepth), Depth is ChildDepth + 1.
|
|
223
|
+
expression_depth(compare(_, _, _), 1).
|
|
224
|
+
expression_depth(count(Atom, _, _), Depth) :- expression_depth(Atom, ChildDepth), Depth is ChildDepth + 1.
|
|
225
|
+
expression_depth(temporal(_, _, _), 1).
|
|
226
|
+
|
|
227
|
+
expression_term(atom(_, _, Args, _, _), Term) :- member(Term, Args).
|
|
228
|
+
expression_term(all(Items), Term) :- member(Item, Items), expression_term(Item, Term).
|
|
229
|
+
expression_term(any(Items), Term) :- member(Item, Items), expression_term(Item, Term).
|
|
230
|
+
expression_term(not(Atom), Term) :- expression_term(Atom, Term).
|
|
231
|
+
expression_term(compare(_, Left, Right), Term) :- member(Term, [Left, Right]).
|
|
232
|
+
expression_term(count(Atom, _, _), Term) :- expression_term(Atom, Term).
|
|
233
|
+
expression_term(temporal(_, Left, Right), Term) :- member(Term, [Left, Right]).
|
|
234
|
+
expression_term(Term, Term) :- Term = interval(_, _).
|
|
235
|
+
|
|
236
|
+
variable_safety_error(Head, Body, _Variables, head_variable_not_range_restricted(Name)) :-
|
|
237
|
+
head_variable(Name, Head),
|
|
238
|
+
\+ positive_variable(Name, Body).
|
|
239
|
+
variable_safety_error(Head, _Body, Variables, existential_head_variable(Name)) :-
|
|
240
|
+
head_variable(Name, Head),
|
|
241
|
+
memberchk(variable(Name, _Type, exists), Variables).
|
|
242
|
+
variable_safety_error(_Head, Body, _Variables, body_variable_not_range_restricted(Name)) :-
|
|
243
|
+
body_variable(Name, Body),
|
|
244
|
+
\+ positive_variable(Name, Body).
|
|
245
|
+
|
|
246
|
+
variable_safety_error(Head, Body, Variables, existential_variable_not_body_local(Name)) :-
|
|
247
|
+
memberchk(variable(Name, _Type, exists), Variables),
|
|
248
|
+
( head_variable(Name, Head) ; \+ expression_variable(Body, Name) ).
|
|
249
|
+
|
|
250
|
+
undeclared_variable_error(Head, Body, Exceptions, Variables, undeclared_variable(Name)) :-
|
|
251
|
+
member(Expression, [Head, Body|Exceptions]),
|
|
252
|
+
expression_variable(Expression, Name),
|
|
253
|
+
\+ memberchk(variable(Name, _Type, _Quantifier), Variables).
|
|
254
|
+
|
|
255
|
+
exception_variable_safety_error(Exception, Body, exception_variable_not_range_restricted(Name)) :-
|
|
256
|
+
expression_variable(Exception, Name),
|
|
257
|
+
\+ positive_variable(Name, Body).
|
|
258
|
+
|
|
259
|
+
head_variable(Name, atom(_, _, Args, _, _)) :- member(Arg, Args), term_variable(Arg, Name).
|
|
260
|
+
body_variable(Name, Expression) :- expression_variable(Expression, Name).
|
|
261
|
+
positive_variable(Name, atom(_, _, Args, positive, _)) :- member(Arg, Args), term_variable(Arg, Name).
|
|
262
|
+
positive_variable(Name, all(Items)) :- member(Item, Items), positive_variable(Name, Item).
|
|
263
|
+
positive_variable(Name, any(Items)) :- member(Item, Items), positive_variable(Name, Item).
|
|
264
|
+
positive_variable(Name, count(Atom, _, _)) :- positive_variable(Name, Atom).
|
|
265
|
+
positive_variable(_, _) :- fail.
|
|
266
|
+
|
|
267
|
+
term_variable(var(Name, _), Name).
|
|
268
|
+
expression_variable(atom(_, _, Args, _, _), Name) :- member(Arg, Args), term_variable(Arg, Name).
|
|
269
|
+
expression_variable(all(Items), Name) :- member(Item, Items), expression_variable(Item, Name).
|
|
270
|
+
expression_variable(any(Items), Name) :- member(Item, Items), expression_variable(Item, Name).
|
|
271
|
+
expression_variable(not(Atom), Name) :- expression_variable(Atom, Name).
|
|
272
|
+
expression_variable(compare(_, Left, Right), Name) :- (term_variable(Left, Name) ; term_variable(Right, Name)).
|
|
273
|
+
expression_variable(count(Atom, _, _), Name) :- expression_variable(Atom, Name).
|
|
274
|
+
expression_variable(temporal(_, Left, Right), Name) :- (term_variable(Left, Name) ; term_variable(Right, Name)).
|
|
275
|
+
|
|
276
|
+
rule_shape_error(atom, none, _Body, missing_head).
|
|
277
|
+
rule_shape_error(atom, _Head, Body, body_for_atom) :- Body \= none.
|
|
278
|
+
rule_shape_error(rule, none, _Body, missing_head).
|
|
279
|
+
rule_shape_error(rule, _Head, none, missing_body).
|
|
280
|
+
rule_shape_error(constraint, _Head, none, missing_body).
|
|
281
|
+
rule_shape_error(constraint, Head, _Body, head_for_constraint) :- Head \= none.
|
|
282
|
+
rule_shape_error(_, _, _, _) :- fail.
|
|
283
|
+
|
|
284
|
+
expression_safety_error(none, _) :- fail.
|
|
285
|
+
expression_safety_error(not(atom(_, _, _, _, false)), negation_requires_closed_world).
|
|
286
|
+
expression_safety_error(compare(Op, _Left, _Right), invalid_compare_operator) :- \+ valid_compare_operator(Op).
|
|
287
|
+
expression_safety_error(count(_Atom, Op, _Value), invalid_compare_operator) :- \+ valid_compare_operator(Op).
|
|
288
|
+
expression_safety_error(count(_Atom, _Op, Value), invalid_count_value) :- \+ integer(Value) ; Value < 0.
|
|
289
|
+
expression_safety_error(temporal(Relation, _Left, _Right), invalid_temporal_relation) :- \+ valid_temporal_relation(Relation).
|
|
290
|
+
expression_safety_error(all(Items), Error) :- member(Item, Items), expression_safety_error(Item, Error).
|
|
291
|
+
expression_safety_error(any(Items), Error) :- member(Item, Items), expression_safety_error(Item, Error).
|
|
292
|
+
expression_safety_error(not(Atom), Error) :- expression_safety_error(Atom, Error).
|
|
293
|
+
expression_safety_error(_, _) :- fail.
|
|
294
|
+
|
|
295
|
+
%% logic_rule_render(+Props, -Text)
|
|
296
|
+
logic_rule_render(Props, Text) :-
|
|
297
|
+
logic_rule_from_props(Props, Rule),
|
|
298
|
+
render_rule(Rule, Text).
|
|
299
|
+
|
|
300
|
+
render_rule(rule(_Kind, Modality, Head, none, _Exceptions, _Scope, _From, _To, _Schema, _Variables), Text) :-
|
|
301
|
+
render_head(Modality, Head, HeadText), format(string(Text), '~w.', [HeadText]).
|
|
302
|
+
render_rule(rule(_Kind, Modality, Head, Body, _Exceptions, _Scope, _From, _To, _Schema, _Variables), Text) :-
|
|
303
|
+
render_head(Modality, Head, HeadText), render_expression(Body, BodyText), format(string(Text), '~w :- ~w.', [HeadText, BodyText]).
|
|
304
|
+
|
|
305
|
+
render_head(Modality, none, Text) :- format(string(Text), '~w', [Modality]).
|
|
306
|
+
render_head(Modality, Atom, Text) :- render_atom(Atom, AtomText), format(string(Text), '~w(~w)', [Modality, AtomText]).
|
|
307
|
+
render_atom(atom(Namespace, Name, Args, Polarity, _), Text) :-
|
|
308
|
+
( Namespace == default -> Full = Name ; format(atom(Full), '~w:~w', [Namespace, Name]) ),
|
|
309
|
+
maplist(render_term, Args, Rendered), atomic_list_concat(Rendered, ',', ArgsText),
|
|
310
|
+
( Polarity == negative -> format(string(Text), 'negative(~w(~w))', [Full, ArgsText]) ; format(string(Text), '~w(~w)', [Full, ArgsText]) ).
|
|
311
|
+
render_term(var(Name, _), Name).
|
|
312
|
+
render_term(const(Value, _), Value).
|
|
313
|
+
render_term(number(Value, none), Text) :- format(string(Text), '~w', [Value]).
|
|
314
|
+
render_term(number(Value, Unit), Text) :- format(string(Text), '~w~w', [Value, Unit]).
|
|
315
|
+
render_term(duration(Value, Unit), Text) :- format(string(Text), 'duration(~w,~w)', [Value, Unit]).
|
|
316
|
+
render_term(timestamp(Value), Value).
|
|
317
|
+
render_term(interval(Start, End), Text) :- format(string(Text), 'interval(~w,~w)', [Start, End]).
|
|
318
|
+
render_expression(Atom, Text) :- Atom = atom(_, _, _, _, _), !, render_atom(Atom, Text).
|
|
319
|
+
render_expression(all(Items), Text) :- maplist(render_expression, Items, Parts), atomic_list_concat(Parts, ', ', Text).
|
|
320
|
+
render_expression(any(Items), Text) :- maplist(render_expression, Items, Parts), atomic_list_concat(Parts, ' ; ', Inner), format(string(Text), '(~w)', [Inner]).
|
|
321
|
+
render_expression(not(Atom), Text) :- render_atom(Atom, AtomText), format(string(Text), 'not(~w)', [AtomText]).
|
|
322
|
+
render_expression(compare(Op, Left, Right), Text) :- render_term(Left, L), render_term(Right, R), format(string(Text), '~w ~w ~w', [L, Op, R]).
|
|
323
|
+
render_expression(count(Atom, Op, Value), Text) :- render_atom(Atom, AtomText), format(string(Text), 'count(~w,~w,~w)', [AtomText, Op, Value]).
|
|
324
|
+
render_expression(temporal(Relation, Left, Right), Text) :- render_term(Left, L), render_term(Right, R), format(string(Text), 'temporal(~w,~w,~w)', [Relation, L, R]).
|
|
325
|
+
|
|
326
|
+
logic_rule_semantic_key(Props, Key) :-
|
|
327
|
+
memberchk(semantic_key=Key, Props), !.
|
|
328
|
+
logic_rule_semantic_key(Props, Key) :-
|
|
329
|
+
memberchk(rule_hash=Hash, Props), !, Key = Hash.
|
|
330
|
+
|
|
331
|
+
logic_atom_signature(atom(Namespace, Name, Args, Polarity, _), signature(Namespace, Name, Args, Polarity)).
|
|
332
|
+
|
|
333
|
+
%% logic_derive(+Goal, -Proof)
|
|
334
|
+
% Goal is atom(Namespace, Name, Args, Polarity), with ground arguments. Rules
|
|
335
|
+
% are interpreted by a finite, data-driven evaluator; no stored source is
|
|
336
|
+
% consulted and recursive rule chains are bounded.
|
|
337
|
+
logic_derive(Goal, Proof) :-
|
|
338
|
+
ground(Goal),
|
|
339
|
+
derive_goal(Goal, [], 0, Proof).
|
|
340
|
+
|
|
341
|
+
logic_max_depth(16).
|
|
342
|
+
|
|
343
|
+
derive_goal(Goal, _Seen, _Depth, direct(FactId)) :-
|
|
344
|
+
Goal = atom(Namespace, Name, Args, Polarity, _),
|
|
345
|
+
predicate_fact(FactId, Namespace, Name, Args, Polarity), !.
|
|
346
|
+
derive_goal(Goal, Seen, Depth, rule(FactId, BodyProof)) :-
|
|
347
|
+
logic_max_depth(MaxDepth),
|
|
348
|
+
Depth < MaxDepth,
|
|
349
|
+
logic_rule_fact(FactId, StoredRule),
|
|
350
|
+
materialize_rule(StoredRule, Rule),
|
|
351
|
+
Rule = rule(rule, _Modality, Head, Body, Exceptions, _Scope, _From, _To, _Schema, _Variables),
|
|
352
|
+
Goal = Head,
|
|
353
|
+
atom_signature_for_seen(Head, Signature),
|
|
354
|
+
\+ memberchk(Signature, Seen),
|
|
355
|
+
NextDepth is Depth + 1,
|
|
356
|
+
prove_expression(Body, [Signature|Seen], NextDepth, BodyProof),
|
|
357
|
+
exceptions_do_not_hold(Exceptions, [Signature|Seen], NextDepth).
|
|
358
|
+
|
|
359
|
+
logic_rule_fact(FactId, Rule) :-
|
|
360
|
+
kb_entity(FactId, fact, Props),
|
|
361
|
+
memberchk(fact_kind=Kind, Props), normalize_atom(Kind, rule),
|
|
362
|
+
logic_rule_from_props(Props, Rule).
|
|
363
|
+
|
|
364
|
+
% The JSON decoder keeps variable names as data terms for validation and
|
|
365
|
+
% rendering. Evaluation reifies those names into fresh Prolog variables so
|
|
366
|
+
% ordinary unification can be used without ever evaluating caller source.
|
|
367
|
+
materialize_rule(
|
|
368
|
+
rule(Kind, Modality, Head, Body, Exceptions, Scope, From, To, Schema, Variables),
|
|
369
|
+
rule(Kind, Modality, MaterialHead, MaterialBody, MaterialExceptions, Scope, From, To, Schema, Variables)
|
|
370
|
+
) :-
|
|
371
|
+
variable_environment(Variables, Environment),
|
|
372
|
+
materialize_head(Head, Environment, MaterialHead),
|
|
373
|
+
materialize_expression(Body, Environment, MaterialBody),
|
|
374
|
+
maplist(materialize_expression_with(Environment), Exceptions, MaterialExceptions).
|
|
375
|
+
|
|
376
|
+
variable_environment([], []).
|
|
377
|
+
variable_environment([variable(Name, _Type, _Quantifier)|Rest], [Name-_Variable|Environment]) :-
|
|
378
|
+
variable_environment(Rest, Environment).
|
|
379
|
+
|
|
380
|
+
materialize_head(none, _Variables, none).
|
|
381
|
+
materialize_head(Head, Variables, MaterialHead) :- materialize_atom(Head, Variables, MaterialHead).
|
|
382
|
+
|
|
383
|
+
materialize_expression(none, _Variables, none).
|
|
384
|
+
materialize_expression(Expression, Variables, Materialized) :-
|
|
385
|
+
Expression = atom(_, _, _, _, _), !,
|
|
386
|
+
materialize_atom(Expression, Variables, Materialized).
|
|
387
|
+
materialize_expression(all(Items), Variables, all(MaterialItems)) :- maplist(materialize_expression_with(Variables), Items, MaterialItems).
|
|
388
|
+
materialize_expression(any(Items), Variables, any(MaterialItems)) :- maplist(materialize_expression_with(Variables), Items, MaterialItems).
|
|
389
|
+
materialize_expression(not(Atom), Variables, not(MaterialAtom)) :- materialize_atom(Atom, Variables, MaterialAtom).
|
|
390
|
+
materialize_expression(compare(Op, Left, Right), Variables, compare(Op, MaterialLeft, MaterialRight)) :-
|
|
391
|
+
materialize_term(Left, Variables, MaterialLeft), materialize_term(Right, Variables, MaterialRight).
|
|
392
|
+
materialize_expression(count(Atom, Op, Value), Variables, count(MaterialAtom, Op, Value)) :- materialize_atom(Atom, Variables, MaterialAtom).
|
|
393
|
+
materialize_expression(temporal(Relation, Left, Right), Variables, temporal(Relation, MaterialLeft, MaterialRight)) :-
|
|
394
|
+
materialize_term(Left, Variables, MaterialLeft), materialize_term(Right, Variables, MaterialRight).
|
|
395
|
+
materialize_expression_with(Variables, Expression, Materialized) :- materialize_expression(Expression, Variables, Materialized).
|
|
396
|
+
|
|
397
|
+
materialize_atom(atom(Namespace, Name, Args, Polarity, ClosedWorld), Variables, atom(Namespace, Name, MaterialArgs, Polarity, ClosedWorld)) :-
|
|
398
|
+
maplist(materialize_term_with(Variables), Args, MaterialArgs).
|
|
399
|
+
materialize_term(var(Name, _Type), Variables, Variable) :- memberchk(Name-Variable, Variables), !.
|
|
400
|
+
materialize_term(const(Value, Type), _Variables, const(Value, Type)).
|
|
401
|
+
materialize_term(number(Value, Unit), _Variables, number(Value, Unit)).
|
|
402
|
+
materialize_term(duration(Value, Unit), _Variables, duration(Value, Unit)).
|
|
403
|
+
materialize_term(timestamp(Value), _Variables, timestamp(Value)).
|
|
404
|
+
materialize_term(interval(Start, End), _Variables, interval(Start, End)).
|
|
405
|
+
materialize_term_with(Variables, Term, Materialized) :- materialize_term(Term, Variables, Materialized).
|
|
406
|
+
|
|
407
|
+
prove_expression(Atom, Seen, Depth, Proof) :-
|
|
408
|
+
Atom = atom(_, _, _, _, _),
|
|
409
|
+
derive_goal(Atom, Seen, Depth, Proof).
|
|
410
|
+
prove_expression(all(Items), Seen, Depth, all(Proofs)) :- maplist(prove_expression_seen(Seen, Depth), Items, Proofs).
|
|
411
|
+
prove_expression(any(Items), Seen, Depth, Proof) :- member(Item, Items), prove_expression(Item, Seen, Depth, Proof), !.
|
|
412
|
+
prove_expression(not(Atom), Seen, Depth, negated(Atom)) :-
|
|
413
|
+
Atom = atom(Namespace, Name, Args, Polarity, _),
|
|
414
|
+
\+ derive_goal(atom(Namespace, Name, Args, Polarity, _), Seen, Depth, _).
|
|
415
|
+
prove_expression(compare(Op, Left, Right), _Seen, _Depth, comparison(Op, Left, Right)) :- compare_terms(Op, Left, Right).
|
|
416
|
+
prove_expression(count(Atom, Op, Value), _Seen, _Depth, count(Atom, Op, Value)) :-
|
|
417
|
+
Atom = atom(Namespace, Name, Args, Polarity, _),
|
|
418
|
+
aggregate_all(count, predicate_fact(_, Namespace, Name, Args, Polarity), Count),
|
|
419
|
+
compare_numbers(Op, Count, Value).
|
|
420
|
+
prove_expression(temporal(Relation, Left, Right), _Seen, _Depth, temporal(Relation, Left, Right)) :- temporal_holds(Relation, Left, Right).
|
|
421
|
+
prove_expression_seen(Seen, Depth, Item, Proof) :- prove_expression(Item, Seen, Depth, Proof).
|
|
422
|
+
|
|
423
|
+
exceptions_do_not_hold([], _Seen, _Depth).
|
|
424
|
+
exceptions_do_not_hold([Exception|Rest], Seen, Depth) :-
|
|
425
|
+
\+ prove_expression(Exception, Seen, Depth, _),
|
|
426
|
+
exceptions_do_not_hold(Rest, Seen, Depth).
|
|
427
|
+
|
|
428
|
+
atom_signature_for_seen(atom(Namespace, Name, Args, Polarity, _), signature(Namespace, Name, Args, Polarity)).
|
|
429
|
+
|
|
430
|
+
compare_terms(eq, Left, Right) :- term_value(Left, L), term_value(Right, R), L = R.
|
|
431
|
+
compare_terms(neq, Left, Right) :- term_value(Left, L), term_value(Right, R), L \= R.
|
|
432
|
+
compare_terms(lt, Left, Right) :- term_value(Left, L), term_value(Right, R), L < R.
|
|
433
|
+
compare_terms(lte, Left, Right) :- term_value(Left, L), term_value(Right, R), L =< R.
|
|
434
|
+
compare_terms(gt, Left, Right) :- term_value(Left, L), term_value(Right, R), L > R.
|
|
435
|
+
compare_terms(gte, Left, Right) :- term_value(Left, L), term_value(Right, R), L >= R.
|
|
436
|
+
|
|
437
|
+
term_value(number(Value, Unit), Normalized) :- normalize_numeric_unit(Value, Unit, Normalized).
|
|
438
|
+
term_value(duration(Value, Unit), Normalized) :- duration_milliseconds(Value, Unit, Normalized).
|
|
439
|
+
term_value(const(Value, _), Number) :- atom_number(Value, Number), !.
|
|
440
|
+
term_value(const(Value, _), Value).
|
|
441
|
+
term_value(Value, Value) :- number(Value).
|
|
442
|
+
|
|
443
|
+
normalize_numeric_unit(Value, none, Value) :- !.
|
|
444
|
+
normalize_numeric_unit(Value, Unit, Normalized) :-
|
|
445
|
+
duration_milliseconds(Value, Unit, Normalized).
|
|
446
|
+
|
|
447
|
+
duration_milliseconds(Value, ms, Value) :- !.
|
|
448
|
+
duration_milliseconds(Value, s, Normalized) :- Normalized is Value * 1000, !.
|
|
449
|
+
duration_milliseconds(Value, m, Normalized) :- Normalized is Value * 60000, !.
|
|
450
|
+
duration_milliseconds(Value, h, Normalized) :- Normalized is Value * 3600000, !.
|
|
451
|
+
duration_milliseconds(Value, d, Normalized) :- Normalized is Value * 86400000, !.
|
|
452
|
+
duration_milliseconds(Value, w, Normalized) :- Normalized is Value * 604800000, !.
|
|
453
|
+
duration_milliseconds(Value, _Unit, Value).
|
|
454
|
+
|
|
455
|
+
compare_numbers(eq, L, R) :- L =:= R.
|
|
456
|
+
compare_numbers(neq, L, R) :- L =\= R.
|
|
457
|
+
compare_numbers(lt, L, R) :- L < R.
|
|
458
|
+
compare_numbers(lte, L, R) :- L =< R.
|
|
459
|
+
compare_numbers(gt, L, R) :- L > R.
|
|
460
|
+
compare_numbers(gte, L, R) :- L >= R.
|
|
461
|
+
|
|
462
|
+
temporal_holds(before, Left, Right) :-
|
|
463
|
+
temporal_end(Left, LeftEnd),
|
|
464
|
+
temporal_start(Right, RightStart),
|
|
465
|
+
LeftEnd @< RightStart.
|
|
466
|
+
temporal_holds(after, Left, Right) :-
|
|
467
|
+
temporal_start(Left, LeftStart),
|
|
468
|
+
temporal_end(Right, RightEnd),
|
|
469
|
+
LeftStart @> RightEnd.
|
|
470
|
+
temporal_holds(during, timestamp(Point), interval(Start, End)) :-
|
|
471
|
+
Point @>= Start,
|
|
472
|
+
Point @=< End.
|
|
473
|
+
temporal_holds(during, interval(InnerStart, InnerEnd), interval(Start, End)) :-
|
|
474
|
+
InnerStart @>= Start,
|
|
475
|
+
InnerEnd @=< End.
|
|
476
|
+
temporal_holds(overlaps, interval(LeftStart, LeftEnd), interval(RightStart, RightEnd)) :-
|
|
477
|
+
LeftStart @< RightEnd,
|
|
478
|
+
RightStart @< LeftEnd.
|
|
479
|
+
temporal_holds(starts, interval(LeftStart, _), interval(RightStart, _)) :-
|
|
480
|
+
LeftStart == RightStart.
|
|
481
|
+
temporal_holds(finishes, interval(_, LeftEnd), interval(_, RightEnd)) :-
|
|
482
|
+
LeftEnd == RightEnd.
|
|
483
|
+
|
|
484
|
+
temporal_start(timestamp(Value), Value).
|
|
485
|
+
temporal_start(interval(Start, _), Start).
|
|
486
|
+
temporal_end(timestamp(Value), Value).
|
|
487
|
+
temporal_end(interval(_, End), End).
|
|
488
|
+
|
|
489
|
+
%% logic_rule_conflict(+RuleA, +RuleB, -Status)
|
|
490
|
+
% Conservative symbolic conflict result. Identical opposing heads are a
|
|
491
|
+
% proven conflict; different bodies remain unresolved unless their explicit
|
|
492
|
+
% numeric/temporal constraints are disjoint.
|
|
493
|
+
logic_rule_conflict(RuleA, RuleB, contradiction) :-
|
|
494
|
+
opposing_rule_heads(RuleA, RuleB),
|
|
495
|
+
compatible_rule_context(RuleA, RuleB),
|
|
496
|
+
same_rule_body(RuleA, RuleB), !.
|
|
497
|
+
logic_rule_conflict(RuleA, RuleB, unresolved) :-
|
|
498
|
+
opposing_rule_heads(RuleA, RuleB),
|
|
499
|
+
compatible_rule_context(RuleA, RuleB), !.
|
|
500
|
+
logic_rule_conflict(_, _, disjoint).
|
|
501
|
+
|
|
502
|
+
%% logic_rules_stratified(+Rules)
|
|
503
|
+
% Check the finite dependency graph for a negated edge on a cycle. A single
|
|
504
|
+
% rule can be locally safe while a collection of rules still forms an
|
|
505
|
+
% unstratified negation cycle (p(X) :- not q(X), q(X) :- not p(X)). The
|
|
506
|
+
% interpreter therefore exposes this collection-level check to the KB
|
|
507
|
+
% validation layer instead of relying on Prolog's negation-as-failure.
|
|
508
|
+
logic_rules_stratified(Rules) :-
|
|
509
|
+
is_list(Rules),
|
|
510
|
+
findall(
|
|
511
|
+
edge(HeadPredicate, BodyPredicate, Sign),
|
|
512
|
+
( member(Rule, Rules), rule_dependency_edge(Rule, HeadPredicate, BodyPredicate, Sign) ),
|
|
513
|
+
Edges
|
|
514
|
+
),
|
|
515
|
+
\+ (
|
|
516
|
+
member(edge(From, To, negative), Edges),
|
|
517
|
+
dependency_path(To, From, Edges, [])
|
|
518
|
+
).
|
|
519
|
+
|
|
520
|
+
rule_dependency_edge(
|
|
521
|
+
rule(rule, _Modality, Head, Body, _Exceptions, _Scope, _From, _To, _Schema, _Variables),
|
|
522
|
+
HeadPredicate,
|
|
523
|
+
BodyPredicate,
|
|
524
|
+
Sign
|
|
525
|
+
) :-
|
|
526
|
+
logic_predicate_signature(Head, HeadPredicate),
|
|
527
|
+
expression_dependency(Body, BodyAtom, Sign),
|
|
528
|
+
logic_predicate_signature(BodyAtom, BodyPredicate).
|
|
529
|
+
|
|
530
|
+
logic_predicate_signature(atom(Namespace, Name, Args, _Polarity, _ClosedWorld), predicate(Namespace, Name, Arity)) :-
|
|
531
|
+
length(Args, Arity).
|
|
532
|
+
|
|
533
|
+
expression_dependency(atom(_Namespace, _Name, _Args, _Polarity, _ClosedWorld), Atom, positive) :-
|
|
534
|
+
Atom = atom(_, _, _, _, _).
|
|
535
|
+
expression_dependency(not(Atom), Atom, negative).
|
|
536
|
+
expression_dependency(all(Items), Atom, Sign) :- member(Item, Items), expression_dependency(Item, Atom, Sign).
|
|
537
|
+
expression_dependency(any(Items), Atom, Sign) :- member(Item, Items), expression_dependency(Item, Atom, Sign).
|
|
538
|
+
expression_dependency(count(Atom, _Operator, _Value), Atom, positive).
|
|
539
|
+
|
|
540
|
+
dependency_path(Current, Target, _Edges, Seen) :-
|
|
541
|
+
Current == Target,
|
|
542
|
+
\+ memberchk(Current, Seen),
|
|
543
|
+
!.
|
|
544
|
+
dependency_path(Current, Target, Edges, Seen) :-
|
|
545
|
+
\+ memberchk(Current, Seen),
|
|
546
|
+
member(edge(Current, Next, _Sign), Edges),
|
|
547
|
+
dependency_path(Next, Target, Edges, [Current|Seen]).
|
|
548
|
+
|
|
549
|
+
%% logic_rule_conflict_witness(+RuleA, +RuleB, -Witness)
|
|
550
|
+
% Return a structured, non-executable witness for an opposing rule result.
|
|
551
|
+
% Consumers can attach requirement IDs and source spans without ever treating
|
|
552
|
+
% the rendered Prolog preview as executable source.
|
|
553
|
+
logic_rule_conflict_witness(RuleA, RuleB, Witness) :-
|
|
554
|
+
logic_rule_conflict(RuleA, RuleB, Status),
|
|
555
|
+
RuleA = rule(_KindA, ModalityA, HeadA, BodyA, ExceptionsA, ScopeA, FromA, ToA, SchemaA, _VariablesA),
|
|
556
|
+
RuleB = rule(_KindB, ModalityB, HeadB, BodyB, ExceptionsB, ScopeB, FromB, ToB, SchemaB, _VariablesB),
|
|
557
|
+
Witness = witness{
|
|
558
|
+
status: Status,
|
|
559
|
+
modality_a: ModalityA,
|
|
560
|
+
modality_b: ModalityB,
|
|
561
|
+
head_a: HeadA,
|
|
562
|
+
head_b: HeadB,
|
|
563
|
+
body_a: BodyA,
|
|
564
|
+
body_b: BodyB,
|
|
565
|
+
exceptions_a: ExceptionsA,
|
|
566
|
+
exceptions_b: ExceptionsB,
|
|
567
|
+
scope_a: ScopeA,
|
|
568
|
+
scope_b: ScopeB,
|
|
569
|
+
valid_from_a: FromA,
|
|
570
|
+
valid_to_a: ToA,
|
|
571
|
+
valid_from_b: FromB,
|
|
572
|
+
valid_to_b: ToB,
|
|
573
|
+
rule_schema_a: SchemaA,
|
|
574
|
+
rule_schema_b: SchemaB
|
|
575
|
+
}.
|
|
576
|
+
|
|
577
|
+
opposing_rule_heads(rule(_, ModalityA, HeadA, _, _, _, _, _, _, _), rule(_, ModalityB, HeadB, _, _, _, _, _, _, _)) :-
|
|
578
|
+
conflicting_modality(ModalityA, ModalityB),
|
|
579
|
+
copy_term((HeadA, HeadB), (CopyA, CopyB)),
|
|
580
|
+
CopyA = CopyB.
|
|
581
|
+
|
|
582
|
+
conflicting_modality(assert, deny).
|
|
583
|
+
conflicting_modality(deny, assert).
|
|
584
|
+
conflicting_modality(oblige, forbid).
|
|
585
|
+
conflicting_modality(forbid, oblige).
|
|
586
|
+
conflicting_modality(permit, forbid).
|
|
587
|
+
conflicting_modality(forbid, permit).
|
|
588
|
+
|
|
589
|
+
compatible_rule_context(rule(_, _, _, BodyA, _, scope(AuthA, ScopeA, _), FromA, ToA, _, _), rule(_, _, _, BodyB, _, scope(AuthB, ScopeB, _), FromB, ToB, _, _)) :-
|
|
590
|
+
(AuthA == '' ; AuthB == '' ; AuthA == AuthB),
|
|
591
|
+
(ScopeA == '' ; ScopeB == '' ; ScopeA == ScopeB),
|
|
592
|
+
(FromA == '' ; ToB == '' ; FromA @=< ToB),
|
|
593
|
+
(FromB == '' ; ToA == '' ; FromB @=< ToA),
|
|
594
|
+
bodies_may_overlap(BodyA, BodyB).
|
|
595
|
+
|
|
596
|
+
bodies_may_overlap(none, _).
|
|
597
|
+
bodies_may_overlap(_, none).
|
|
598
|
+
bodies_may_overlap(BodyA, BodyB) :-
|
|
599
|
+
BodyA \= none,
|
|
600
|
+
BodyB \= none,
|
|
601
|
+
\+ bodies_definitely_disjoint(BodyA, BodyB).
|
|
602
|
+
|
|
603
|
+
bodies_definitely_disjoint(BodyA, _BodyB) :- expression_definitely_false(BodyA), !.
|
|
604
|
+
bodies_definitely_disjoint(_BodyA, BodyB) :- expression_definitely_false(BodyB), !.
|
|
605
|
+
|
|
606
|
+
% Only classify a body as impossible when its finite, ground comparison or
|
|
607
|
+
% temporal constraint can be evaluated to false. Unknown atoms, variables,
|
|
608
|
+
% and unsupported combinations remain unresolved rather than being treated as
|
|
609
|
+
% disjoint by guesswork.
|
|
610
|
+
expression_definitely_false(compare(Op, Left, Right)) :-
|
|
611
|
+
ground(compare(Op, Left, Right)),
|
|
612
|
+
\+ catch(compare_terms(Op, Left, Right), _, fail).
|
|
613
|
+
expression_definitely_false(temporal(Relation, Left, Right)) :-
|
|
614
|
+
ground(temporal(Relation, Left, Right)),
|
|
615
|
+
\+ catch(temporal_holds(Relation, Left, Right), _, fail).
|
|
616
|
+
expression_definitely_false(all(Items)) :-
|
|
617
|
+
member(Item, Items),
|
|
618
|
+
expression_definitely_false(Item), !.
|
|
619
|
+
expression_definitely_false(any(Items)) :-
|
|
620
|
+
Items \= [],
|
|
621
|
+
maplist(expression_definitely_false, Items).
|
|
622
|
+
|
|
623
|
+
same_rule_body(RuleA, RuleB) :-
|
|
624
|
+
RuleA = rule(_, _, _, BodyA, ExceptionsA, ScopeA, FromA, ToA, _, _),
|
|
625
|
+
RuleB = rule(_, _, _, BodyB, ExceptionsB, ScopeB, FromB, ToB, _, _),
|
|
626
|
+
BodyA == BodyB, ExceptionsA == ExceptionsB, ScopeA == ScopeB, FromA == FromB, ToA == ToB.
|
|
627
|
+
|
|
628
|
+
normalize_atom(Value, Atom) :- atom(Value), !, Atom = Value.
|
|
629
|
+
normalize_atom(Value, Atom) :- string(Value), atom_string(Atom, Value).
|