kibi-core 0.22.4 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,237 @@
1
+ % Module: certificates
2
+ % implements REQ-kibi-kb-certificates
3
+ %
4
+ % Finds a certificate for every KB theorem that certificate_check.pl states:
5
+ % a model for each consistent property context, predicate atom and decision
6
+ % key, an irreducible core (with a model per dropped fact) for each
7
+ % contradiction, and a stratum assignment (or a negation cycle) for the
8
+ % stored rules. This module may search; certificate_check.pl re-checks
9
+ % everything it emits without trusting it.
10
+
11
+ :- module(certificates, [
12
+ kb_certificates/1,
13
+ kb_write_certificates/2
14
+ ]).
15
+
16
+ :- encoding(utf8).
17
+
18
+ :- use_module(library(http/json)).
19
+ :- use_module(library(apply)).
20
+ :- use_module(library(lists)).
21
+ :- use_module(library(aggregate)).
22
+ :- use_module('kb.pl', []).
23
+ :- use_module('intervals.pl', [numeric_constraint_holds/3]).
24
+ :- use_module('certificate_check.pl', [
25
+ certificate_property_contexts/1,
26
+ certificate_predicate_groups/1,
27
+ certificate_decision_groups/1,
28
+ certificate_rule_edges/1,
29
+ certificate_member_holds/3,
30
+ certificate_members_satisfiable/2
31
+ ]).
32
+
33
+ %% kb_write_certificates(+Path, -JsonString)
34
+ % Write the certificate bundle for the current KB to Path and return a
35
+ % summary (certificate count per kind) as a JSON string.
36
+ kb_write_certificates(Path, JsonString) :-
37
+ kb_certificates(Bundle),
38
+ setup_call_cleanup(
39
+ open(Path, write, Stream, [encoding(utf8)]),
40
+ ( json_write_dict(Stream, Bundle, [width(0)]), nl(Stream) ),
41
+ close(Stream)),
42
+ findall(Kind, (member(Certificate, Bundle.certificates), get_dict(kind, Certificate, Kind)), Kinds0),
43
+ msort(Kinds0, Kinds),
44
+ clumped(Kinds, Counts),
45
+ findall(Kind-Count, member(Kind-Count, Counts), Pairs),
46
+ dict_pairs(ByKind, _, Pairs),
47
+ length(Bundle.certificates, Total),
48
+ with_output_to(string(JsonString),
49
+ json_write_dict(current_output, _{path: Path, certificates: Total, byKind: ByKind}, [width(0)])).
50
+
51
+ %% kb_certificates(-Bundle)
52
+ kb_certificates(_{version: 'kibi.certificates.v1', certificates: Certificates}) :-
53
+ certificate_property_contexts(Contexts),
54
+ convlist(property_certificate, Contexts, PropertyCertificates),
55
+ certificate_predicate_groups(AtomGroups),
56
+ convlist(group_certificate(predicate), AtomGroups, PredicateCertificates),
57
+ certificate_decision_groups(DecisionGroups),
58
+ convlist(group_certificate(decision), DecisionGroups, DecisionCertificates),
59
+ certificate_rule_edges(Edges),
60
+ rule_certificates(Edges, RuleCertificates),
61
+ append([PropertyCertificates, PredicateCertificates, DecisionCertificates, RuleCertificates], Certificates).
62
+
63
+ % ------------------------------------------------------------------
64
+ % Property contexts
65
+ % ------------------------------------------------------------------
66
+
67
+ property_certificate(context(Subject, Property, Scope, Domain, Members), Certificate) :-
68
+ Base = _{subjectKey: Subject, propertyKey: Property, scope: Scope, domain: Domain},
69
+ ( members_model(Domain, Members, Value)
70
+ -> findall(Fact, member(m(Fact, _, _, _, _), Members), Facts),
71
+ Certificate = Base.put(_{kind: property_model, value: Value, facts: Facts})
72
+ ; irreducible_core(Domain, Members, Core),
73
+ findall(_{factId: Fact, value: Value},
74
+ ( member(Dropped, Core),
75
+ Dropped = m(Fact, _, _, _, _),
76
+ exclude(==(Dropped), Core, Rest),
77
+ members_model(Domain, Rest, Value)
78
+ ),
79
+ DropModels),
80
+ findall(Fact, member(m(Fact, _, _, _, _), Core), CoreFacts),
81
+ ( core_windows_overlap(Core)
82
+ -> Kind = property_contradiction
83
+ ; Kind = property_unresolved
84
+ ),
85
+ Certificate = Base.put(_{kind: Kind, core: CoreFacts, dropModels: DropModels})
86
+ ).
87
+
88
+ %% members_model(+Domain, +Members, -Value)
89
+ % A value satisfying every member, found among finitely many candidates that
90
+ % meet every region the members' bounds cut the line into.
91
+ members_model(Domain, Members, Value) :-
92
+ model_candidates(Domain, Members, Candidates),
93
+ member(Value, Candidates),
94
+ forall(member(Member, Members), certificate_member_holds(Domain, Member, Value)),
95
+ !.
96
+
97
+ model_candidates(Domain, Members, Candidates) :-
98
+ memberchk(Domain, [int, number]),
99
+ !,
100
+ findall(Bound, member(m(_, _, Bound, _, _), Members), Bounds0),
101
+ sort(Bounds0, Bounds),
102
+ ( Domain == int
103
+ -> findall(Candidate,
104
+ ( member(Bound, Bounds),
105
+ member(Base, [floor(Bound), ceiling(Bound)]),
106
+ member(Offset, [-1, 0, 1]),
107
+ Candidate is Base + Offset
108
+ ),
109
+ Candidates0)
110
+ ; findall(Candidate,
111
+ ( member(Bound, Bounds), member(Offset, [-1, 0, 1]), Candidate is Bound + Offset
112
+ ; nextto(Low, High, Bounds), Candidate is (Low + High) / 2
113
+ ),
114
+ Candidates0)
115
+ ),
116
+ sort([0|Candidates0], Candidates).
117
+ model_candidates(bool, _Members, [true, false]) :- !.
118
+ model_candidates(string, Members, Candidates) :-
119
+ findall(Bound, member(m(_, _, Bound, _, _), Members), Bounds0),
120
+ sort(Bounds0, Bounds),
121
+ fresh_text(Bounds, 0, Fresh),
122
+ append(Bounds, [Fresh], Candidates).
123
+
124
+ fresh_text(Bounds, Index, Fresh) :-
125
+ format(string(Candidate), "kibi:model:~w", [Index]),
126
+ ( member(Bound, Bounds), text_value(Bound, Candidate)
127
+ -> Next is Index + 1,
128
+ fresh_text(Bounds, Next, Fresh)
129
+ ; Fresh = Candidate
130
+ ).
131
+
132
+ text_value(Value, Text) :- atom(Value), !, atom_string(Value, Text).
133
+ text_value(Value, Text) :- string(Value), !, Text = Value.
134
+
135
+ %% irreducible_core(+Domain, +Members, -Core)
136
+ % Deletion filter: drop each member whose removal keeps the set
137
+ % unsatisfiable. What remains admits no value, and every proper subset
138
+ % does.
139
+ irreducible_core(Domain, Members, Core) :-
140
+ foldl(core_step(Domain), Members, Members, Core).
141
+
142
+ core_step(Domain, Member, Current, Next) :-
143
+ exclude(==(Member), Current, Without),
144
+ ( Without \== [],
145
+ \+ certificate_members_satisfiable(Domain, Without)
146
+ -> Next = Without
147
+ ; Next = Current
148
+ ).
149
+
150
+ core_windows_overlap(Core) :-
151
+ forall((member(m(_, _, _, FromA, ToA), Core), member(m(_, _, _, FromB, ToB), Core)),
152
+ kb:intervals_overlap(FromA, ToA, FromB, ToB)).
153
+
154
+ % ------------------------------------------------------------------
155
+ % Predicate atoms and decision keys
156
+ % ------------------------------------------------------------------
157
+
158
+ group_certificate(Prefix, group(Label, Members), Certificate) :-
159
+ atom_concat(Prefix, '_model', ModelKind),
160
+ atom_concat(Prefix, '_contradiction', ContradictionKind),
161
+ findall(Value, member(p(_, Value), Members), Values0),
162
+ sort(Values0, Values),
163
+ ( Values = [Value]
164
+ -> findall(Fact, member(p(Fact, _), Members), Facts),
165
+ Certificate = Label.put(_{kind: ModelKind, value: Value, facts: Facts})
166
+ ; Members = [p(FactA, ValueA)|_],
167
+ member(p(FactB, ValueB), Members),
168
+ ValueB \== ValueA,
169
+ !,
170
+ Certificate = Label.put(_{kind: ContradictionKind, core: [FactA, FactB]})
171
+ ).
172
+
173
+ % ------------------------------------------------------------------
174
+ % Rule stratification
175
+ % ------------------------------------------------------------------
176
+
177
+ rule_certificates([], []) :- !.
178
+ rule_certificates(Edges, [Certificate]) :-
179
+ findall(Signature, (member(edge(Head, Body, _), Edges), member(Signature, [Head, Body])), Signatures0),
180
+ sort(Signatures0, Signatures),
181
+ findall(Signature-0, member(Signature, Signatures), Initial),
182
+ length(Signatures, Count),
183
+ Rounds is Count + 1,
184
+ ( relax_strata(Rounds, Edges, Initial, Strata)
185
+ -> findall(_{namespace: Namespace, name: Name, arity: Arity, stratum: Stratum},
186
+ member(predicate(Namespace, Name, Arity)-Stratum, Strata),
187
+ Entries),
188
+ Certificate = _{kind: rule_strata, strata: Entries}
189
+ ; negation_cycle(Edges, Cycle),
190
+ findall(_{head: HeadDict, body: BodyDict, sign: Sign},
191
+ ( member(edge(Head, Body, Sign), Cycle),
192
+ signature_dict(Head, HeadDict),
193
+ signature_dict(Body, BodyDict)
194
+ ),
195
+ Entries),
196
+ Certificate = _{kind: rule_negation_cycle, cycle: Entries}
197
+ ).
198
+
199
+ signature_dict(predicate(Namespace, Name, Arity), _{namespace: Namespace, name: Name, arity: Arity}).
200
+
201
+ % Raise each head to at least its body's stratum (one more across a negated
202
+ % edge) until nothing changes. Without a negation cycle that happens within
203
+ % one round per predicate; a cycle keeps raising, and relaxation fails.
204
+ relax_strata(0, _Edges, _Strata, _Final) :- !, fail.
205
+ relax_strata(Rounds, Edges, Strata, Final) :-
206
+ foldl(relax_edge, Edges, Strata, Next),
207
+ ( Next == Strata
208
+ -> Final = Strata
209
+ ; Remaining is Rounds - 1,
210
+ relax_strata(Remaining, Edges, Next, Final)
211
+ ).
212
+
213
+ relax_edge(edge(Head, Body, Sign), Strata, Next) :-
214
+ memberchk(Head-HeadStratum, Strata),
215
+ memberchk(Body-BodyStratum, Strata),
216
+ ( Sign == negative -> Required is BodyStratum + 1 ; Required = BodyStratum ),
217
+ ( HeadStratum < Required
218
+ -> selectchk(Head-HeadStratum, Strata, Head-Required, Next)
219
+ ; Next = Strata
220
+ ).
221
+
222
+ % A negated edge Head -> Body with a dependency path from Body back to Head.
223
+ negation_cycle(Edges, [edge(Head, Body, negative)|Path]) :-
224
+ member(edge(Head, Body, negative), Edges),
225
+ dependency_path(Body, Head, Edges, [Body], Path),
226
+ !.
227
+
228
+ dependency_path(Current, Target, _Edges, _Seen, []) :-
229
+ Current == Target,
230
+ !.
231
+ dependency_path(Current, Target, Edges, Seen, [edge(Current, Next, Sign)|Path]) :-
232
+ member(edge(Current, Next, Sign), Edges),
233
+ ( Next == Target
234
+ -> Path = []
235
+ ; \+ memberchk(Next, Seen),
236
+ dependency_path(Next, Target, Edges, [Next|Seen], Path)
237
+ ).
package/src/checks.pl CHANGED
@@ -50,6 +50,8 @@
50
50
  check_subject_key_identity/1,
51
51
  check_subject_key_shape/1,
52
52
  check_ontology_quality/1,
53
+ check_kb_consistency/1,
54
+ check_kb_consistency_unresolved/1,
53
55
  run_checks_json/0, % Entry point for JSON output
54
56
  violation_id_text/2 % Extract text from entity ID term (exported for testing)
55
57
  ]).
@@ -75,6 +77,7 @@
75
77
  :- use_module('logic_ir.pl', [logic_rule_safety/2, logic_rule_from_props/2, logic_rule_render/2, logic_rule_conflict/3, logic_rule_conflict_witness/4, logic_rules_stratified/1, logic_rule_missing_keys/4, functional_predicate_declarations/1, logic_rule_property_form/2]).
76
78
  % GENERATED registry facts; single source is schema/rule-registry.json.
77
79
  :- use_module('rule_registry.pl', [known_rule/1]).
80
+ :- use_module('kb_consistency.pl', [check_kb_consistency/1, check_kb_consistency_unresolved/1]).
78
81
 
79
82
  % Required fields for all entities
80
83
  required_fields([id, title, status, created_at, updated_at, source]).
@@ -125,6 +128,8 @@ check_all(ViolationsDict) :-
125
128
  check_subject_key_identity(SubjectKeyIdentity),
126
129
  check_subject_key_shape(SubjectKeyShape),
127
130
  check_ontology_quality(OntologyQuality),
131
+ check_kb_consistency(KbConsistency),
132
+ check_kb_consistency_unresolved(KbConsistencyUnresolved),
128
133
  ViolationsDict = _{
129
134
  must_priority_coverage: MustPriority,
130
135
  symbol_coverage: SymbolCoverage,
@@ -159,7 +164,9 @@ check_all(ViolationsDict) :-
159
164
  domain_implication: DomainImplication,
160
165
  subject_key_identity: SubjectKeyIdentity,
161
166
  subject_key_shape: SubjectKeyShape,
162
- ontology_quality: OntologyQuality
167
+ ontology_quality: OntologyQuality,
168
+ kb_consistency: KbConsistency,
169
+ kb_consistency_unresolved: KbConsistencyUnresolved
163
170
  }.
164
171
 
165
172
  %% check_must_priority_coverage(-Violations)
@@ -3663,7 +3670,9 @@ check_selected_dispatch(Rules, _{
3663
3670
  domain_implication: DomainImplication,
3664
3671
  subject_key_identity: SubjectKeyIdentity,
3665
3672
  subject_key_shape: SubjectKeyShape,
3666
- ontology_quality: OntologyQuality
3673
+ ontology_quality: OntologyQuality,
3674
+ kb_consistency: KbConsistency,
3675
+ kb_consistency_unresolved: KbConsistencyUnresolved
3667
3676
  }) :-
3668
3677
  selected_rule(Rules, 'must-priority-coverage', check_must_priority_coverage, MustPriority),
3669
3678
  selected_rule(Rules, 'symbol-coverage', check_symbol_coverage, SymbolCoverage),
@@ -3698,7 +3707,9 @@ check_selected_dispatch(Rules, _{
3698
3707
  selected_rule(Rules, 'domain-implication', check_domain_implication, DomainImplication),
3699
3708
  selected_rule(Rules, 'subject-key-identity', check_subject_key_identity, SubjectKeyIdentity),
3700
3709
  selected_rule(Rules, 'subject-key-shape', check_subject_key_shape, SubjectKeyShape),
3701
- selected_rule(Rules, 'ontology-quality', check_ontology_quality, OntologyQuality).
3710
+ selected_rule(Rules, 'ontology-quality', check_ontology_quality, OntologyQuality),
3711
+ selected_rule(Rules, 'kb-consistency', check_kb_consistency, KbConsistency),
3712
+ selected_rule(Rules, 'kb-consistency-unresolved', check_kb_consistency_unresolved, KbConsistencyUnresolved).
3702
3713
 
3703
3714
  selected_rule(Rules, Name, Goal, Violations) :-
3704
3715
  ( memberchk(Name, Rules)
@@ -3770,6 +3781,8 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
3770
3781
  check_subject_key_identity(SubjectKeyIdentity),
3771
3782
  check_subject_key_shape(SubjectKeyShape),
3772
3783
  check_ontology_quality(OntologyQuality),
3784
+ check_kb_consistency(KbConsistency),
3785
+ check_kb_consistency_unresolved(KbConsistencyUnresolved),
3773
3786
  ViolationsDict = _{
3774
3787
  must_priority_coverage: MustPriority,
3775
3788
  symbol_coverage: SymbolCoverage,
@@ -3803,7 +3816,9 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
3803
3816
  domain_implication: DomainImplication,
3804
3817
  subject_key_identity: SubjectKeyIdentity,
3805
3818
  subject_key_shape: SubjectKeyShape,
3806
- ontology_quality: OntologyQuality
3819
+ ontology_quality: OntologyQuality,
3820
+ kb_consistency: KbConsistency,
3821
+ kb_consistency_unresolved: KbConsistencyUnresolved
3807
3822
  }.
3808
3823
 
3809
3824
  %% violations_dict_to_json(+ViolationsDict, -JsonDict)
package/src/discovery.pl CHANGED
@@ -398,7 +398,7 @@ coverage_report_json(By, Tags, IncludePassing, BindingMode, TestBindings, Includ
398
398
 
399
399
  % implements REQ-kibi-conservative-requirement-proof
400
400
  % Proof-status filtered report. The status filter selects requirement rows by
401
- % proofStatus (proven, missing, unresolved, not_applicable) and replaces the
401
+ % proofStatus (tested, missing, unresolved, not_applicable) and replaces the
402
402
  % default include-passing filtering, so callers can enumerate exactly one
403
403
  % slice — e.g. every not_applicable row together with its typed applicability
404
404
  % reason — without diffing full exports by hand. The summary is always
@@ -1127,7 +1127,7 @@ coverage_summary(Rows, Summary) :-
1127
1127
  include(req_row_missing_scenario, Rows, MissingScenarioRows),
1128
1128
  include(req_row_missing_test, Rows, MissingTestRows),
1129
1129
  include(req_row_missing_both, Rows, MissingBothRows),
1130
- include(req_row_proof_proven, Rows, ProofProvenRows),
1130
+ include(req_row_proof_tested, Rows, ProofTestedRows),
1131
1131
  include(req_row_proof_unresolved, Rows, ProofUnresolvedRows),
1132
1132
  include(req_row_proof_missing, Rows, ProofMissingRows),
1133
1133
  include(req_row_proof_not_applicable, Rows, ProofNotApplicableRows),
@@ -1138,7 +1138,8 @@ coverage_summary(Rows, Summary) :-
1138
1138
  length(MissingScenarioRows, MissingScenario),
1139
1139
  length(MissingTestRows, MissingTest),
1140
1140
  length(MissingBothRows, MissingScenarioAndTest),
1141
- length(ProofProvenRows, ProofProven),
1141
+ length(ProofTestedRows, ProofTested),
1142
+ evidence_level_counts(Rows, EvidenceLevels),
1142
1143
  length(ProofUnresolvedRows, ProofUnresolved),
1143
1144
  length(ProofMissingRows, ProofMissing),
1144
1145
  length(ProofNotApplicableRows, ProofNotApplicable),
@@ -1151,12 +1152,20 @@ coverage_summary(Rows, Summary) :-
1151
1152
  missingScenario: MissingScenario,
1152
1153
  missingTest: MissingTest,
1153
1154
  missingScenarioAndTest: MissingScenarioAndTest,
1154
- proofProven: ProofProven,
1155
+ proofTested: ProofTested,
1155
1156
  proofUnresolved: ProofUnresolved,
1156
1157
  proofMissing: ProofMissing,
1157
- proofNotApplicable: ProofNotApplicable
1158
+ proofNotApplicable: ProofNotApplicable,
1159
+ evidenceLevels: EvidenceLevels
1158
1160
  }.
1159
1161
 
1162
+ % implements REQ-kibi-evidence-levels
1163
+ evidence_level_counts(Rows, _{proved: Proved, tested: Tested, asserted: Asserted, unverified: Unverified}) :-
1164
+ aggregate_all(count, (member(Row, Rows), Row.get(evidenceLevel) == proved), Proved),
1165
+ aggregate_all(count, (member(Row, Rows), Row.get(evidenceLevel) == tested), Tested),
1166
+ aggregate_all(count, (member(Row, Rows), Row.get(evidenceLevel) == asserted), Asserted),
1167
+ aggregate_all(count, (member(Row, Rows), Row.get(evidenceLevel) == unverified), Unverified).
1168
+
1160
1169
  req_row_fully_covered(Row) :-
1161
1170
  CoverageStatus = Row.get(coverageStatus),
1162
1171
  CoverageStatus = fully_covered.
@@ -1175,8 +1184,8 @@ req_row_missing_test(Row) :-
1175
1184
  Row.gaps = [missing_test].
1176
1185
  req_row_missing_both(Row) :-
1177
1186
  Row.gaps = [missing_scenario_and_test].
1178
- req_row_proof_proven(Row) :-
1179
- Row.proofStatus == proven.
1187
+ req_row_proof_tested(Row) :-
1188
+ Row.proofStatus == tested.
1180
1189
  req_row_proof_unresolved(Row) :-
1181
1190
  Row.proofStatus == unresolved.
1182
1191
  req_row_proof_missing(Row) :-
@@ -1377,7 +1386,7 @@ filter_req_coverage_rows(false, Rows, Filtered) :-
1377
1386
 
1378
1387
  req_row_proof_passing(Row) :-
1379
1388
  Status = Row.proofStatus,
1380
- memberchk(Status, [proven, not_applicable]).
1389
+ memberchk(Status, [tested, not_applicable]).
1381
1390
 
1382
1391
  filter_symbol_coverage_rows(true, Rows, Rows).
1383
1392
  filter_symbol_coverage_rows(false, Rows, Filtered) :-
package/src/kb.pl CHANGED
@@ -16,6 +16,7 @@
16
16
  kb_commit_upsert/5,
17
17
  kb_commit_upsert_batch/2,
18
18
  kb_preview_upsert_contradiction/3,
19
+ kb_preview_upsert_contradiction/4,
19
20
  kb_log_entity_upsert/3,
20
21
  kb_retract_entity/1,
21
22
  kb_retract_entity/3,
@@ -1069,9 +1070,7 @@ kb_commit_upsert(Type, Props, Relationships, SkipContradiction, ChangeKind) :-
1069
1070
  catch(
1070
1071
  rdf_transaction((
1071
1072
  kb_stage(rdf_mutation),
1072
- kb_assert_entity_no_audit(Type, Props),
1073
- kb_commit_relationships_no_audit(Relationships),
1074
- kb_maybe_check_req_contradiction(Type, Id, SkipContradiction),
1073
+ kb_stage_upsert_mutation(Type, Id, Props, Relationships, SkipContradiction),
1075
1074
  kb_stage(entity_audit),
1076
1075
  kb_log_entity_upsert(ChangeKind, Type, Props),
1077
1076
  kb_stage(relationship_audit),
@@ -1101,9 +1100,7 @@ kb_commit_upsert(Type, Props, Relationships, SkipContradiction, ChangeKind) :-
1101
1100
  upsert_change_kind(Id, ChangeKind),
1102
1101
  rdf_transaction((
1103
1102
  kb_stage(rdf_mutation),
1104
- kb_assert_entity_no_audit(Type, Props),
1105
- kb_commit_relationships_no_audit(Relationships),
1106
- kb_maybe_check_req_contradiction(Type, Id, SkipContradiction),
1103
+ kb_stage_upsert_mutation(Type, Id, Props, Relationships, SkipContradiction),
1107
1104
  kb_stage(entity_audit),
1108
1105
  kb_log_entity_upsert(ChangeKind, Type, Props),
1109
1106
  kb_stage(relationship_audit),
@@ -1130,35 +1127,51 @@ kb_maybe_check_req_contradiction(req, Id, false) :-
1130
1127
  check_req_contradiction(Id).
1131
1128
  kb_maybe_check_req_contradiction(_, _, _).
1132
1129
 
1130
+ %% kb_stage_upsert_mutation(+Type, +Id, +Properties, +Relationships, +SkipContradiction)
1131
+ % implements REQ-kibi-operation-interface-parity
1132
+ % The RDF mutation and refusal rules of one upsert, shared by the commit, the
1133
+ % batch commit and the dry-run preview so all three refuse the same payloads:
1134
+ % a relationship whose source or target entity does not exist, an invalid
1135
+ % relationship direction, a symbol mixing executable_for with production
1136
+ % ownership/coverage edges, and (for a requirement) a contradiction.
1137
+ kb_stage_upsert_mutation(Type, Id, Props, Relationships, SkipContradiction) :-
1138
+ kb_assert_entity_no_audit(Type, Props),
1139
+ kb_commit_relationships_no_audit(Relationships),
1140
+ kb_maybe_check_req_contradiction(Type, Id, SkipContradiction).
1141
+
1133
1142
  % Stage one upsert inside an already-open transaction. The caller saves once.
1134
1143
  kb_stage_upsert(Type, Props, Relationships, SkipContradiction, ChangeKind) :-
1135
1144
  memberchk(id=Id, Props),
1136
1145
  memberchk(SkipContradiction, [true, false]),
1137
1146
  upsert_change_kind(Id, ChangeKind),
1138
- kb_assert_entity_no_audit(Type, Props),
1139
- kb_commit_relationships_no_audit(Relationships),
1140
- kb_maybe_check_req_contradiction(Type, Id, SkipContradiction),
1147
+ kb_stage_upsert_mutation(Type, Id, Props, Relationships, SkipContradiction),
1141
1148
  kb_log_entity_upsert(ChangeKind, Type, Props),
1142
1149
  kb_commit_relationship_audits(Relationships).
1143
1150
 
1144
1151
  %% kb_preview_upsert_contradiction(+Type, +Properties, +Relationships)
1152
+ %% kb_preview_upsert_contradiction(+Type, +Properties, +Relationships, +SkipContradiction)
1145
1153
  % implements REQ-kibi-operation-interface-parity
1146
- % Stage one upsert in a transaction that is always rolled back and run the
1147
- % commit-time requirement contradiction check against the staged KB, so a
1148
- % kb_upsert dry run refuses exactly what the commit would refuse. Nothing is
1149
- % written and nothing marks the branch dirty: the no-audit assertions only
1150
- % touch the RDF store, which the transaction rolls back, and no audit entry,
1151
- % snapshot save or journal flush follows. The entity index is not
1152
- % transactional, so the staged id is re-indexed from the restored store.
1154
+ % Stage one upsert in a transaction that is always rolled back and run every
1155
+ % commit-time refusal rule (kb_stage_upsert_mutation/5) against the staged
1156
+ % KB, so a kb_upsert dry run refuses exactly what the commit would refuse:
1157
+ % missing relationship endpoints, invalid directions, mixed symbol roles and,
1158
+ % unless skipped, requirement contradictions. Nothing is written and nothing
1159
+ % marks the branch dirty: the no-audit assertions only touch the RDF store,
1160
+ % which the transaction rolls back, and no audit entry, snapshot save or
1161
+ % journal flush follows. The entity index is not transactional, so the
1162
+ % staged id is re-indexed from the restored store.
1153
1163
  kb_preview_upsert_contradiction(Type, Props, Relationships) :-
1164
+ kb_preview_upsert_contradiction(Type, Props, Relationships, false).
1165
+
1166
+ kb_preview_upsert_contradiction(Type, Props, Relationships, SkipContradiction) :-
1154
1167
  memberchk(id=Id, Props),
1168
+ memberchk(SkipContradiction, [true, false]),
1155
1169
  with_kb_mutex(
1156
1170
  call_cleanup(
1157
1171
  catch(
1158
1172
  rdf_transaction((
1159
- kb_assert_entity_no_audit(Type, Props),
1160
- kb_commit_relationships_no_audit(Relationships),
1161
- kb_maybe_check_req_contradiction(Type, Id, false),
1173
+ kb_stage(rdf_mutation),
1174
+ kb_stage_upsert_mutation(Type, Id, Props, Relationships, SkipContradiction),
1162
1175
  throw(kibi_preview_rolled_back)
1163
1176
  )),
1164
1177
  Caught,