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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-core",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "private": false,
5
5
  "description": "Core Prolog modules and RDF graph logic for Kibi",
6
6
  "type": "module",
@@ -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
 
@@ -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.
@@ -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
- Violations
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 fact via requires_property or a predicate fact via requires_predicate, preserving the same claim_key",
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)
@@ -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).