kibi-core 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -3
- package/schema/entities.pl +17 -1
- package/schema/validation.pl +2 -0
- package/src/checks.pl +144 -34
- package/src/discovery.pl +88 -18
- package/src/kb.pl +1339 -42
- package/src/requirement_proof.pl +837 -0
- package/src/status.pl +42 -40
package/src/kb.pl
CHANGED
|
@@ -4,13 +4,27 @@
|
|
|
4
4
|
kb_attach/1,
|
|
5
5
|
kb_detach/0,
|
|
6
6
|
kb_save/0,
|
|
7
|
+
kb_sync_checkpoint/0,
|
|
8
|
+
kb_storage_status/1,
|
|
9
|
+
kb_storage_compact/0,
|
|
10
|
+
kb_storage_compact_if_needed/0,
|
|
11
|
+
kb_storage_export/1,
|
|
12
|
+
kb_migrate_legacy/2,
|
|
7
13
|
with_kb_mutex/1,
|
|
8
14
|
kb_assert_entity/2,
|
|
9
15
|
kb_assert_entity_no_audit/2,
|
|
16
|
+
kb_commit_upsert/5,
|
|
10
17
|
kb_log_entity_upsert/3,
|
|
11
18
|
kb_retract_entity/1,
|
|
12
19
|
kb_retract_entity/3,
|
|
13
20
|
kb_retract_entity_relationships/1,
|
|
21
|
+
kb_retract_relationship/3,
|
|
22
|
+
kb_retract_all_relationships/0,
|
|
23
|
+
kb_entities_by_tag/2,
|
|
24
|
+
kb_indexed_sources/1,
|
|
25
|
+
kb_query_entities/8,
|
|
26
|
+
kb_search_entities/6,
|
|
27
|
+
kb_rebuild_indexes/0,
|
|
14
28
|
kb_entity/3,
|
|
15
29
|
kb_entities_by_source/2,
|
|
16
30
|
kb_assert_relationship/4,
|
|
@@ -43,6 +57,7 @@
|
|
|
43
57
|
predicate_schema/6,
|
|
44
58
|
predicate_fact/5,
|
|
45
59
|
contradicting_reqs/3,
|
|
60
|
+
req_conflict_witness/3,
|
|
46
61
|
check_req_contradiction/1,
|
|
47
62
|
normalize_term_atom/2,
|
|
48
63
|
normalize_term_atom_list/2,
|
|
@@ -51,9 +66,13 @@
|
|
|
51
66
|
]).
|
|
52
67
|
|
|
53
68
|
:- use_module(library(semweb/rdf11)).
|
|
69
|
+
:- use_module(library(semweb/rdf_persistency)).
|
|
54
70
|
:- use_module(library(persistency)).
|
|
55
71
|
:- use_module(library(thread)).
|
|
56
72
|
:- use_module(library(filesex)).
|
|
73
|
+
:- use_module(library(readutil)).
|
|
74
|
+
:- use_module(library(aggregate), [aggregate_all/3]).
|
|
75
|
+
:- use_module(library(lists), [sum_list/2]).
|
|
57
76
|
:- use_module(library(ordsets)).
|
|
58
77
|
:- use_module('../schema/entities.pl', [entity_type/1, entity_property/3, required_property/2]).
|
|
59
78
|
:- use_module('../schema/relationships.pl', [relationship_type/1, valid_relationship/3]).
|
|
@@ -78,10 +97,31 @@ kb_uri('urn-kibi:').
|
|
|
78
97
|
:- dynamic kb_audit_db/1.
|
|
79
98
|
:- dynamic kb_graph/1.
|
|
80
99
|
:- dynamic kb_attached_snapshot/1.
|
|
100
|
+
:- dynamic kb_storage_mode/1. % legacy | journaled
|
|
101
|
+
:- dynamic kb_persistency_directory/1.
|
|
102
|
+
:- dynamic kb_commit_sequence/1.
|
|
103
|
+
:- dynamic kb_generation/1.
|
|
104
|
+
:- dynamic kb_legacy_sentinel_stamp/1.
|
|
105
|
+
:- dynamic kb_audit_sequence/1.
|
|
106
|
+
:- dynamic kb_dirty/0.
|
|
107
|
+
% Rebuildable read indexes. RDF remains authoritative; a dirty marker causes
|
|
108
|
+
% the next unbound discovery query to rebuild these daemon-lifetime indexes.
|
|
109
|
+
:- dynamic kb_index_ready/0.
|
|
110
|
+
:- dynamic kb_index_dirty/0.
|
|
111
|
+
:- dynamic kb_index_entity/2.
|
|
112
|
+
:- dynamic kb_index_type/2.
|
|
113
|
+
:- dynamic kb_index_tag/2.
|
|
114
|
+
:- dynamic kb_index_source/2.
|
|
115
|
+
:- dynamic kb_index_token/2.
|
|
116
|
+
:- dynamic kb_index_symbol_coordinate/5.
|
|
117
|
+
:- dynamic kb_index_entity_count/1.
|
|
118
|
+
:- dynamic kb_index_triple_count/1.
|
|
81
119
|
:- dynamic entity/4. % Support legacy .pl file format (Type, Id, Title, Props)
|
|
82
120
|
|
|
83
121
|
%% kb_attach(+Directory)
|
|
84
|
-
% Attach to a KB directory
|
|
122
|
+
% Attach to a KB directory. New stores use SWI's journaled RDF persistence;
|
|
123
|
+
% unmarked directories retain the legacy RDF/XML path for migration and
|
|
124
|
+
% backwards-compatible temporary KBs.
|
|
85
125
|
% Creates directory if it doesn't exist.
|
|
86
126
|
kb_attach(Directory) :-
|
|
87
127
|
( kb_attached(_)
|
|
@@ -93,6 +133,67 @@ kb_attach(Directory) :-
|
|
|
93
133
|
-> true
|
|
94
134
|
; make_directory_path(Directory)
|
|
95
135
|
),
|
|
136
|
+
( journaled_store(Directory)
|
|
137
|
+
-> kb_attach_journaled(Directory)
|
|
138
|
+
; kb_attach_legacy(Directory)
|
|
139
|
+
).
|
|
140
|
+
|
|
141
|
+
journaled_store(Directory) :-
|
|
142
|
+
atom_concat(Directory, '/storage.json', Marker),
|
|
143
|
+
exists_file(Marker),
|
|
144
|
+
catch(
|
|
145
|
+
read_file_to_string(Marker, Contents, []),
|
|
146
|
+
_,
|
|
147
|
+
fail
|
|
148
|
+
),
|
|
149
|
+
sub_string(Contents, _, _, _, "kibi.rdf-journal.v1").
|
|
150
|
+
|
|
151
|
+
% implements REQ-core-journaled-engine-persistence
|
|
152
|
+
kb_attach_journaled(Directory) :-
|
|
153
|
+
atom_concat(Directory, '/rdf', PersistencyDirectory),
|
|
154
|
+
( exists_directory(PersistencyDirectory)
|
|
155
|
+
-> true
|
|
156
|
+
; make_directory_path(PersistencyDirectory)
|
|
157
|
+
),
|
|
158
|
+
% rdf_persistency owns the database lock for the lifetime of the engine.
|
|
159
|
+
rdf_attach_db(PersistencyDirectory,
|
|
160
|
+
[access(read_write), silent(true), concurrency(4)]),
|
|
161
|
+
% Create RDF graph name from directory. Do not unload a graph here:
|
|
162
|
+
% rdf_attach_db has already restored it from its snapshot/journal. A
|
|
163
|
+
% staged migration can have been atomically moved after the graph was
|
|
164
|
+
% first persisted, so accept the single restored graph URI as well.
|
|
165
|
+
journal_graph_uri(Directory, GraphURI),
|
|
166
|
+
read_journal_metadata(Directory, Generation, Sequence),
|
|
167
|
+
assert(kb_attached(Directory)),
|
|
168
|
+
assert(kb_graph(GraphURI)),
|
|
169
|
+
assert(kb_storage_mode(journaled)),
|
|
170
|
+
assert(kb_persistency_directory(PersistencyDirectory)),
|
|
171
|
+
assert(kb_generation(Generation)),
|
|
172
|
+
assert(kb_commit_sequence(Sequence)),
|
|
173
|
+
assert(kb_attached_snapshot(journal_snapshot(Generation, Sequence))),
|
|
174
|
+
retractall(kb_dirty),
|
|
175
|
+
initialize_journal_audit_sequence(GraphURI),
|
|
176
|
+
atom_concat(Directory, '/kb.rdf', SentinelFile),
|
|
177
|
+
current_data_stamp(SentinelFile, SentinelStamp),
|
|
178
|
+
assert(kb_legacy_sentinel_stamp(SentinelStamp)),
|
|
179
|
+
|
|
180
|
+
load_kb_pl_files(Directory),
|
|
181
|
+
kb_rebuild_indexes.
|
|
182
|
+
|
|
183
|
+
journal_graph_uri(Directory, GraphURI) :-
|
|
184
|
+
atom_concat('file://', Directory, Expected),
|
|
185
|
+
( rdf_graph(Expected)
|
|
186
|
+
-> GraphURI = Expected
|
|
187
|
+
; findall(G,
|
|
188
|
+
(rdf_graph(G), rdf(_, _, _, G)),
|
|
189
|
+
Graphs),
|
|
190
|
+
( Graphs = [GraphURI|_]
|
|
191
|
+
-> true
|
|
192
|
+
; GraphURI = Expected
|
|
193
|
+
)
|
|
194
|
+
).
|
|
195
|
+
|
|
196
|
+
kb_attach_legacy(Directory) :-
|
|
96
197
|
% Create RDF graph name from directory
|
|
97
198
|
atom_concat('file://', Directory, GraphURI),
|
|
98
199
|
% If a graph with this URI is already present, unload it to avoid duplicates.
|
|
@@ -111,16 +212,21 @@ kb_attach(Directory) :-
|
|
|
111
212
|
atom_concat(Directory, '/audit.log', AuditLog),
|
|
112
213
|
( db_attached(AuditLog)
|
|
113
214
|
-> true % Already attached
|
|
114
|
-
;
|
|
215
|
+
; % Close the journal stream after each write. The default `flush`
|
|
216
|
+
% mode retains an exclusive write lock for the lifetime of a
|
|
217
|
+
% long-running MCP process and blocks writers in other runtimes.
|
|
218
|
+
db_attach(AuditLog, [sync(close)])
|
|
115
219
|
),
|
|
116
220
|
% Track attachment state
|
|
117
221
|
assert(kb_attached(Directory)),
|
|
118
222
|
assert(kb_audit_db(AuditLog)),
|
|
119
223
|
assert(kb_graph(GraphURI)),
|
|
224
|
+
assert(kb_storage_mode(legacy)),
|
|
120
225
|
assert(kb_attached_snapshot(SnapshotStamp)),
|
|
121
226
|
|
|
122
227
|
% Load legacy .pl entity files if present
|
|
123
|
-
load_kb_pl_files(Directory)
|
|
228
|
+
load_kb_pl_files(Directory),
|
|
229
|
+
kb_rebuild_indexes.
|
|
124
230
|
|
|
125
231
|
|
|
126
232
|
%% kb_detach
|
|
@@ -130,6 +236,11 @@ kb_attach(Directory) :-
|
|
|
130
236
|
kb_detach :-
|
|
131
237
|
( kb_attached(_Directory)
|
|
132
238
|
-> (
|
|
239
|
+
( kb_storage_mode(journaled)
|
|
240
|
+
-> catch(rdf_flush_journals([min_size(16384)]), _, true),
|
|
241
|
+
catch(rdf_detach_db, _, true)
|
|
242
|
+
; true
|
|
243
|
+
),
|
|
133
244
|
% Unload RDF graph from memory to prevent duplication on reattach
|
|
134
245
|
( kb_graph(GraphURI)
|
|
135
246
|
-> rdf_unload_graph(GraphURI)
|
|
@@ -143,7 +254,24 @@ kb_detach :-
|
|
|
143
254
|
retractall(kb_attached(_)),
|
|
144
255
|
retractall(kb_audit_db(_)),
|
|
145
256
|
retractall(kb_graph(_)),
|
|
146
|
-
retractall(kb_attached_snapshot(_))
|
|
257
|
+
retractall(kb_attached_snapshot(_)),
|
|
258
|
+
retractall(kb_storage_mode(_)),
|
|
259
|
+
retractall(kb_persistency_directory(_)),
|
|
260
|
+
retractall(kb_commit_sequence(_)),
|
|
261
|
+
retractall(kb_generation(_)),
|
|
262
|
+
retractall(kb_legacy_sentinel_stamp(_)),
|
|
263
|
+
retractall(kb_audit_sequence(_)),
|
|
264
|
+
retractall(kb_dirty),
|
|
265
|
+
retractall(kb_index_ready),
|
|
266
|
+
retractall(kb_index_dirty),
|
|
267
|
+
retractall(kb_index_entity(_, _)),
|
|
268
|
+
retractall(kb_index_type(_, _)),
|
|
269
|
+
retractall(kb_index_tag(_, _)),
|
|
270
|
+
retractall(kb_index_source(_, _)),
|
|
271
|
+
retractall(kb_index_token(_, _)),
|
|
272
|
+
retractall(kb_index_symbol_coordinate(_, _, _, _, _)),
|
|
273
|
+
retractall(kb_index_entity_count(_)),
|
|
274
|
+
retractall(kb_index_triple_count(_))
|
|
147
275
|
)
|
|
148
276
|
; true
|
|
149
277
|
).
|
|
@@ -152,7 +280,9 @@ kb_detach :-
|
|
|
152
280
|
% Save RDF graph and sync audit log to disk
|
|
153
281
|
% implements REQ-009
|
|
154
282
|
kb_save :-
|
|
155
|
-
(
|
|
283
|
+
( kb_storage_mode(journaled)
|
|
284
|
+
-> kb_save_journaled
|
|
285
|
+
; kb_attached(Directory)
|
|
156
286
|
-> with_kb_mutex(with_kb_file_lock(Directory, kb_save_locked(Directory)))
|
|
157
287
|
; true
|
|
158
288
|
).
|
|
@@ -166,17 +296,29 @@ with_kb_file_lock(Directory, Goal) :-
|
|
|
166
296
|
).
|
|
167
297
|
|
|
168
298
|
kb_save_locked(Directory) :-
|
|
299
|
+
( kb_storage_mode(journaled)
|
|
300
|
+
-> kb_save_journaled_locked(Directory)
|
|
301
|
+
; kb_save_legacy_locked(Directory)
|
|
302
|
+
).
|
|
303
|
+
|
|
304
|
+
kb_save_legacy_locked(Directory) :-
|
|
169
305
|
atom_concat(Directory, '/kb.rdf', DataFile),
|
|
170
306
|
temp_rdf_file(Directory, TempFile),
|
|
171
307
|
catch(
|
|
172
308
|
(
|
|
173
309
|
ensure_snapshot_current(DataFile),
|
|
310
|
+
kb_stage(snapshot_save),
|
|
174
311
|
save_rdf_snapshot(TempFile),
|
|
312
|
+
kb_stage(audit_sync),
|
|
313
|
+
sync_audit_log,
|
|
314
|
+
% Keep the durable RDF rename after audit synchronization. The
|
|
315
|
+
% temporary snapshot can be discarded if an audit-stage failure
|
|
316
|
+
% occurs, so a failed commit cannot publish new RDF state.
|
|
317
|
+
kb_stage(snapshot_save),
|
|
175
318
|
rename_file(TempFile, DataFile),
|
|
176
319
|
current_data_stamp(DataFile, UpdatedStamp),
|
|
177
320
|
retractall(kb_attached_snapshot(_)),
|
|
178
|
-
assert(kb_attached_snapshot(UpdatedStamp))
|
|
179
|
-
sync_audit_log
|
|
321
|
+
assert(kb_attached_snapshot(UpdatedStamp))
|
|
180
322
|
),
|
|
181
323
|
Error,
|
|
182
324
|
(
|
|
@@ -185,6 +327,486 @@ kb_save_locked(Directory) :-
|
|
|
185
327
|
)
|
|
186
328
|
).
|
|
187
329
|
|
|
330
|
+
% Journaled stores never rewrite kb.rdf on the hot path. SWI's monitor
|
|
331
|
+
% appends RDF edits to a per-source journal; flushing with min_size(16384)
|
|
332
|
+
% makes a compact binary snapshot only after a journal reaches 16 MiB.
|
|
333
|
+
kb_save_journaled :-
|
|
334
|
+
( kb_attached(Directory)
|
|
335
|
+
-> with_kb_mutex(kb_save_journaled_locked(Directory))
|
|
336
|
+
; true
|
|
337
|
+
).
|
|
338
|
+
|
|
339
|
+
%% kb_sync_checkpoint
|
|
340
|
+
% Publish compiler freshness even when a successful sync has no RDF delta.
|
|
341
|
+
% The commit sequence remains the durable ordering boundary used by status;
|
|
342
|
+
% shutdown still uses kb_save/0 and therefore does not mint phantom commits.
|
|
343
|
+
kb_sync_checkpoint :-
|
|
344
|
+
kb_mark_dirty,
|
|
345
|
+
kb_save_journaled.
|
|
346
|
+
|
|
347
|
+
% implements REQ-core-journaled-engine-persistence
|
|
348
|
+
kb_save_journaled_locked(Directory) :-
|
|
349
|
+
ensure_journal_sentinel_current(Directory),
|
|
350
|
+
( kb_dirty
|
|
351
|
+
-> kb_stage(journal_flush),
|
|
352
|
+
rdf_flush_journals([min_size(16384)]),
|
|
353
|
+
next_journal_commit(Generation, Sequence),
|
|
354
|
+
write_journal_head(Directory, Generation, Sequence),
|
|
355
|
+
retractall(kb_attached_snapshot(_)),
|
|
356
|
+
assert(kb_attached_snapshot(journal_snapshot(Generation, Sequence))),
|
|
357
|
+
retractall(kb_dirty)
|
|
358
|
+
; true
|
|
359
|
+
).
|
|
360
|
+
|
|
361
|
+
kb_mark_dirty :-
|
|
362
|
+
(kb_dirty -> true ; assertz(kb_dirty)).
|
|
363
|
+
|
|
364
|
+
kb_mark_index_dirty :-
|
|
365
|
+
(kb_index_dirty -> true ; assertz(kb_index_dirty)).
|
|
366
|
+
|
|
367
|
+
drop_entity_index_rows(Id) :-
|
|
368
|
+
retractall(kb_index_entity(Id, _)),
|
|
369
|
+
retractall(kb_index_type(_, Id)),
|
|
370
|
+
retractall(kb_index_tag(_, Id)),
|
|
371
|
+
retractall(kb_index_source(_, Id)),
|
|
372
|
+
retractall(kb_index_token(_, Id)),
|
|
373
|
+
retractall(kb_index_symbol_coordinate(Id, _, _, _, _)).
|
|
374
|
+
|
|
375
|
+
kb_refresh_entity_index(Id) :-
|
|
376
|
+
( kb_index_ready,
|
|
377
|
+
\+ kb_index_dirty,
|
|
378
|
+
kb_index_entity_count(Count)
|
|
379
|
+
-> (kb_index_entity(Id, _) -> WasIndexed = true ; WasIndexed = false),
|
|
380
|
+
drop_entity_index_rows(Id),
|
|
381
|
+
( once(kb_entity_raw(Id, Type, Props))
|
|
382
|
+
-> index_entity_row(Id, Type, Props),
|
|
383
|
+
(WasIndexed == true -> NextCount = Count ; NextCount is Count + 1)
|
|
384
|
+
; (WasIndexed == true -> NextCount is max(0, Count - 1) ; NextCount = Count)
|
|
385
|
+
),
|
|
386
|
+
retractall(kb_index_entity_count(_)),
|
|
387
|
+
assertz(kb_index_entity_count(NextCount))
|
|
388
|
+
; kb_mark_index_dirty
|
|
389
|
+
).
|
|
390
|
+
|
|
391
|
+
kb_remove_entity_index(Id) :-
|
|
392
|
+
( kb_index_ready,
|
|
393
|
+
\+ kb_index_dirty,
|
|
394
|
+
kb_index_entity_count(Count)
|
|
395
|
+
-> (kb_index_entity(Id, _) -> WasIndexed = true ; WasIndexed = false),
|
|
396
|
+
drop_entity_index_rows(Id),
|
|
397
|
+
(WasIndexed == true -> NextCount is max(0, Count - 1) ; NextCount = Count),
|
|
398
|
+
retractall(kb_index_entity_count(_)),
|
|
399
|
+
assertz(kb_index_entity_count(NextCount))
|
|
400
|
+
; kb_mark_index_dirty
|
|
401
|
+
).
|
|
402
|
+
|
|
403
|
+
ensure_journal_sentinel_current(Directory) :-
|
|
404
|
+
atom_concat(Directory, '/kb.rdf', SentinelFile),
|
|
405
|
+
current_data_stamp(SentinelFile, CurrentStamp),
|
|
406
|
+
( kb_legacy_sentinel_stamp(ExpectedStamp)
|
|
407
|
+
-> ( CurrentStamp == ExpectedStamp
|
|
408
|
+
-> true
|
|
409
|
+
; throw(error(permission_error(save, kb, stale_snapshot), kb_save/0))
|
|
410
|
+
)
|
|
411
|
+
; true
|
|
412
|
+
).
|
|
413
|
+
|
|
414
|
+
next_journal_commit(Generation, Sequence) :-
|
|
415
|
+
( kb_generation(CurrentGeneration)
|
|
416
|
+
-> Generation = CurrentGeneration
|
|
417
|
+
; Generation = 'generation-1'
|
|
418
|
+
),
|
|
419
|
+
( kb_commit_sequence(CurrentSequence)
|
|
420
|
+
-> Sequence is CurrentSequence + 1
|
|
421
|
+
; Sequence = 1
|
|
422
|
+
),
|
|
423
|
+
retractall(kb_generation(_)),
|
|
424
|
+
assert(kb_generation(Generation)),
|
|
425
|
+
retractall(kb_commit_sequence(_)),
|
|
426
|
+
assert(kb_commit_sequence(Sequence)).
|
|
427
|
+
|
|
428
|
+
read_journal_metadata(Directory, Generation, Sequence) :-
|
|
429
|
+
atom_concat(Directory, '/CURRENT', HeadFile),
|
|
430
|
+
( exists_file(HeadFile),
|
|
431
|
+
catch(read_file_to_string(HeadFile, Contents, []), _, fail),
|
|
432
|
+
split_string(Contents, ':\n\r', ' \t', Parts),
|
|
433
|
+
Parts = [GenerationString, SequenceString|_],
|
|
434
|
+
atom_string(Generation, GenerationString),
|
|
435
|
+
catch(number_string(Sequence, SequenceString), _, fail)
|
|
436
|
+
-> true
|
|
437
|
+
; Generation = 'generation-1',
|
|
438
|
+
Sequence = 0
|
|
439
|
+
).
|
|
440
|
+
|
|
441
|
+
write_journal_head(Directory, Generation, Sequence) :-
|
|
442
|
+
atom_concat(Directory, '/CURRENT.tmp', TempFile),
|
|
443
|
+
atom_concat(Directory, '/CURRENT', HeadFile),
|
|
444
|
+
setup_call_cleanup(
|
|
445
|
+
open(TempFile, write, Stream, [encoding(utf8)]),
|
|
446
|
+
format(Stream, '~w:~w~n', [Generation, Sequence]),
|
|
447
|
+
close(Stream)
|
|
448
|
+
),
|
|
449
|
+
rename_file(TempFile, HeadFile).
|
|
450
|
+
|
|
451
|
+
%% kb_storage_status(-Status)
|
|
452
|
+
% Return a compact operational view without scanning the RDF graph.
|
|
453
|
+
kb_storage_status(Status) :-
|
|
454
|
+
kb_storage_mode(Mode),
|
|
455
|
+
kb_attached(Directory),
|
|
456
|
+
(kb_generation(Generation) -> true ; Generation = unknown),
|
|
457
|
+
(kb_commit_sequence(Sequence) -> true ; Sequence = 0),
|
|
458
|
+
journal_bytes(Directory, JournalBytes),
|
|
459
|
+
Status = _{
|
|
460
|
+
mode: Mode,
|
|
461
|
+
directory: Directory,
|
|
462
|
+
generation: Generation,
|
|
463
|
+
sequence: Sequence,
|
|
464
|
+
journalBytes: JournalBytes
|
|
465
|
+
}.
|
|
466
|
+
|
|
467
|
+
journal_bytes(Directory, Bytes) :-
|
|
468
|
+
( kb_persistency_directory(_PersistencyDirectory)
|
|
469
|
+
-> ( findall(Size,
|
|
470
|
+
( rdf_journal_file(_Graph, JournalFile),
|
|
471
|
+
size_file(JournalFile, Size) ),
|
|
472
|
+
Sizes),
|
|
473
|
+
sum_list(Sizes, Bytes)
|
|
474
|
+
)
|
|
475
|
+
; atom_concat(Directory, '/audit.log', AuditLog),
|
|
476
|
+
(exists_file(AuditLog) -> size_file(AuditLog, Bytes) ; Bytes = 0)
|
|
477
|
+
).
|
|
478
|
+
|
|
479
|
+
directory_size(Directory, Bytes) :-
|
|
480
|
+
( exists_directory(Directory)
|
|
481
|
+
-> directory_files(Directory, Files),
|
|
482
|
+
directory_size_files(Directory, Files, 0, Bytes)
|
|
483
|
+
; Bytes = 0
|
|
484
|
+
).
|
|
485
|
+
|
|
486
|
+
directory_size_files(_, [], Total, Total).
|
|
487
|
+
directory_size_files(Directory, [File|Rest], Acc, Total) :-
|
|
488
|
+
( memberchk(File, ['.', '..'])
|
|
489
|
+
-> Next = Acc
|
|
490
|
+
; atom_concat(Directory, '/', Prefix),
|
|
491
|
+
atom_concat(Prefix, File, FullPath),
|
|
492
|
+
( exists_directory(FullPath)
|
|
493
|
+
-> directory_size(FullPath, ChildSize), Next is Acc + ChildSize
|
|
494
|
+
; exists_file(FullPath), size_file(FullPath, Size)
|
|
495
|
+
-> Next is Acc + Size
|
|
496
|
+
; Next = Acc
|
|
497
|
+
)
|
|
498
|
+
),
|
|
499
|
+
directory_size_files(Directory, Rest, Next, Total).
|
|
500
|
+
|
|
501
|
+
%% kb_storage_compact
|
|
502
|
+
kb_storage_compact :-
|
|
503
|
+
kb_storage_mode(journaled),
|
|
504
|
+
kb_attached(Directory),
|
|
505
|
+
with_kb_mutex(kb_storage_compact_locked(Directory)).
|
|
506
|
+
|
|
507
|
+
%% kb_storage_compact_if_needed
|
|
508
|
+
% Compact only when the live journal has crossed the 16 MiB idle-compaction
|
|
509
|
+
% threshold. The engine invokes this predicate after the client queue drains;
|
|
510
|
+
% ordinary writes only append durable journal records and never rewrite a
|
|
511
|
+
% snapshot on the hot path.
|
|
512
|
+
kb_storage_compact_if_needed :-
|
|
513
|
+
kb_storage_mode(journaled),
|
|
514
|
+
kb_attached(Directory),
|
|
515
|
+
with_kb_mutex((
|
|
516
|
+
journal_bytes(Directory, JournalBytes),
|
|
517
|
+
( JournalBytes >= 16777216
|
|
518
|
+
-> kb_storage_compact_locked(Directory)
|
|
519
|
+
; true
|
|
520
|
+
)
|
|
521
|
+
)).
|
|
522
|
+
|
|
523
|
+
kb_storage_compact_locked(Directory) :-
|
|
524
|
+
% A caller may explicitly compact immediately after a low-level mutation
|
|
525
|
+
% that has not gone through kb_save/0. Publish that dirty transaction
|
|
526
|
+
% first so CURRENT never points at an older commit sequence.
|
|
527
|
+
kb_save_journaled_locked(Directory),
|
|
528
|
+
rdf_flush_journals([min_size(0)]),
|
|
529
|
+
( kb_generation(Generation) -> true ; Generation = 'generation-1' ),
|
|
530
|
+
( kb_commit_sequence(Sequence) -> true ; Sequence = 0 ),
|
|
531
|
+
write_journal_head(Directory, Generation, Sequence).
|
|
532
|
+
|
|
533
|
+
%% kb_storage_export(+TargetDirectory)
|
|
534
|
+
% Export a journaled branch to the legacy RDF/XML shape without changing the
|
|
535
|
+
% active store. The human-readable audit export is intentionally derived.
|
|
536
|
+
kb_storage_export(TargetDirectory) :-
|
|
537
|
+
kb_graph(GraphURI),
|
|
538
|
+
( exists_directory(TargetDirectory)
|
|
539
|
+
-> true
|
|
540
|
+
; make_directory_path(TargetDirectory)
|
|
541
|
+
),
|
|
542
|
+
atom_concat(TargetDirectory, '/kb.rdf', TargetFile),
|
|
543
|
+
rdf_save(TargetFile,
|
|
544
|
+
[graph(GraphURI), base_uri('urn-kibi:'), namespaces([kb, xsd])]),
|
|
545
|
+
atom_concat(TargetDirectory, '/audit.log', AuditFile),
|
|
546
|
+
setup_call_cleanup(
|
|
547
|
+
open(AuditFile, write, Stream, [encoding(utf8)]),
|
|
548
|
+
export_audit_events(Stream, GraphURI),
|
|
549
|
+
close(Stream)
|
|
550
|
+
).
|
|
551
|
+
|
|
552
|
+
%% kb_migrate_legacy(+SourceDirectory, +TargetDirectory)
|
|
553
|
+
% Copy a legacy RDF/audit graph into a fresh journaled directory. The caller
|
|
554
|
+
% publishes the staged directory atomically; this predicate deliberately
|
|
555
|
+
% leaves the source untouched on failure.
|
|
556
|
+
% implements REQ-core-journaled-engine-persistence
|
|
557
|
+
kb_migrate_legacy(SourceDirectory, TargetDirectory) :-
|
|
558
|
+
kb_attached(SourceDirectory),
|
|
559
|
+
kb_storage_mode(legacy),
|
|
560
|
+
% Hold the legacy branch lock for the complete import and audit copy so a
|
|
561
|
+
% pre-cutover writer cannot change the source between validation and
|
|
562
|
+
% publication.
|
|
563
|
+
with_kb_file_lock_nowait(
|
|
564
|
+
SourceDirectory,
|
|
565
|
+
kb_migrate_legacy_locked(SourceDirectory, TargetDirectory)
|
|
566
|
+
).
|
|
567
|
+
|
|
568
|
+
% Migration is a cutover operation, not a background writer. Never leave a
|
|
569
|
+
% CLI/engine request blocked indefinitely behind an old MCP process that still
|
|
570
|
+
% owns the legacy lock; surface the lock owner to the caller so it can stop the
|
|
571
|
+
% old client and retry. Ordinary legacy saves retain their blocking lock
|
|
572
|
+
% semantics for compatibility.
|
|
573
|
+
with_kb_file_lock_nowait(Directory, Goal) :-
|
|
574
|
+
atom_concat(Directory, '/kb.lock', LockFile),
|
|
575
|
+
setup_call_cleanup(
|
|
576
|
+
open(LockFile, append, LockStream, [lock(write), wait(false)]),
|
|
577
|
+
call(Goal),
|
|
578
|
+
close(LockStream)
|
|
579
|
+
).
|
|
580
|
+
|
|
581
|
+
kb_migrate_legacy_locked(_SourceDirectory, TargetDirectory) :-
|
|
582
|
+
kb_graph(SourceGraph),
|
|
583
|
+
findall(
|
|
584
|
+
t(S, P, O),
|
|
585
|
+
( rdf(S, P, O, SourceGraph),
|
|
586
|
+
\+ audit_resource(S)
|
|
587
|
+
),
|
|
588
|
+
Triples
|
|
589
|
+
),
|
|
590
|
+
findall(c(TS, Op, Entity, Data),
|
|
591
|
+
changeset(TS, Op, Entity, Data),
|
|
592
|
+
Audits),
|
|
593
|
+
canonical_triple_digest(Triples, SourceDigest),
|
|
594
|
+
length(Triples, TripleCount),
|
|
595
|
+
length(Audits, AuditCount),
|
|
596
|
+
kb_detach,
|
|
597
|
+
( exists_directory(TargetDirectory)
|
|
598
|
+
-> true
|
|
599
|
+
; make_directory_path(TargetDirectory)
|
|
600
|
+
),
|
|
601
|
+
write_journal_marker(TargetDirectory),
|
|
602
|
+
kb_attach(TargetDirectory),
|
|
603
|
+
kb_graph(TargetGraph),
|
|
604
|
+
forall(member(t(S, P, O), Triples), rdf_assert(S, P, O, TargetGraph)),
|
|
605
|
+
assert_migration_audits(Audits, 1),
|
|
606
|
+
kb_mark_dirty,
|
|
607
|
+
kb_mark_index_dirty,
|
|
608
|
+
validate_migrated_graph(
|
|
609
|
+
TargetGraph,
|
|
610
|
+
SourceDigest,
|
|
611
|
+
TripleCount,
|
|
612
|
+
AuditCount
|
|
613
|
+
),
|
|
614
|
+
kb_save_journaled,
|
|
615
|
+
kb_detach.
|
|
616
|
+
|
|
617
|
+
canonical_triple_digest(Triples, Digest) :-
|
|
618
|
+
sort(Triples, Canonical),
|
|
619
|
+
term_hash(Canonical, Digest).
|
|
620
|
+
|
|
621
|
+
validate_migrated_graph(Graph, ExpectedDigest, ExpectedTriples, ExpectedAudits) :-
|
|
622
|
+
findall(
|
|
623
|
+
t(S, P, O),
|
|
624
|
+
( rdf(S, P, O, Graph),
|
|
625
|
+
\+ audit_resource(S)
|
|
626
|
+
),
|
|
627
|
+
Triples
|
|
628
|
+
),
|
|
629
|
+
length(Triples, ActualTriples),
|
|
630
|
+
( ActualTriples =:= ExpectedTriples
|
|
631
|
+
-> true
|
|
632
|
+
; throw(error(migration_validation(triple_count, ExpectedTriples, ActualTriples),
|
|
633
|
+
kb_migrate_legacy/2))
|
|
634
|
+
),
|
|
635
|
+
canonical_triple_digest(Triples, ActualDigest),
|
|
636
|
+
( ActualDigest == ExpectedDigest
|
|
637
|
+
-> true
|
|
638
|
+
; throw(error(migration_validation(triple_digest, ExpectedDigest, ActualDigest),
|
|
639
|
+
kb_migrate_legacy/2))
|
|
640
|
+
),
|
|
641
|
+
findall(Audit,
|
|
642
|
+
rdf(Audit, 'urn:kibi:audit/operation', _, Graph),
|
|
643
|
+
AuditResources),
|
|
644
|
+
length(AuditResources, ActualAudits),
|
|
645
|
+
( ActualAudits =:= ExpectedAudits
|
|
646
|
+
-> true
|
|
647
|
+
; throw(error(migration_validation(audit_count, ExpectedAudits, ActualAudits),
|
|
648
|
+
kb_migrate_legacy/2))
|
|
649
|
+
),
|
|
650
|
+
% Re-run the schema and relationship invariants before publication. This
|
|
651
|
+
% catches missing required fields and dangling edges while the staged
|
|
652
|
+
% generation is still disposable.
|
|
653
|
+
findall(
|
|
654
|
+
problem(Id, Type),
|
|
655
|
+
( kb_entity(Id, Type, Props),
|
|
656
|
+
normalize_migration_props(Type, Props, ValidationProps),
|
|
657
|
+
\+ catch(validate_entity(Type, ValidationProps), _, fail)
|
|
658
|
+
),
|
|
659
|
+
EntityProblems
|
|
660
|
+
),
|
|
661
|
+
( EntityProblems == []
|
|
662
|
+
-> true
|
|
663
|
+
; throw(error(migration_validation(entity_schema, EntityProblems),
|
|
664
|
+
kb_migrate_legacy/2))
|
|
665
|
+
),
|
|
666
|
+
findall(
|
|
667
|
+
problem(RelType, FromId, ToId),
|
|
668
|
+
( relationship_type(RelType),
|
|
669
|
+
kb_relationship(RelType, FromId, ToId),
|
|
670
|
+
( \+ once(kb_entity(FromId, _, _))
|
|
671
|
+
; \+ once(kb_entity(ToId, _, _))
|
|
672
|
+
; once(kb_entity(FromId, FromType, _)),
|
|
673
|
+
once(kb_entity(ToId, ToType, _)),
|
|
674
|
+
\+ catch(validate_relationship(RelType, FromType, ToType), _, fail)
|
|
675
|
+
)
|
|
676
|
+
),
|
|
677
|
+
RelationshipProblems
|
|
678
|
+
),
|
|
679
|
+
( RelationshipProblems == []
|
|
680
|
+
-> true
|
|
681
|
+
; throw(error(migration_validation(relationships, RelationshipProblems),
|
|
682
|
+
kb_migrate_legacy/2))
|
|
683
|
+
).
|
|
684
|
+
|
|
685
|
+
assert_migration_audits([], _).
|
|
686
|
+
assert_migration_audits([c(TS, Op, Entity, Data)|Rest], Index) :-
|
|
687
|
+
kb_assert_audit_event_with_timestamp(TS, Op, Entity, Data, Index),
|
|
688
|
+
NextIndex is Index + 1,
|
|
689
|
+
assert_migration_audits(Rest, NextIndex).
|
|
690
|
+
|
|
691
|
+
normalize_migration_props(_, [], []).
|
|
692
|
+
normalize_migration_props(Type, [Key=Value|Rest], NormalizedProps) :-
|
|
693
|
+
normalize_migration_props(Type, Rest, RestProps),
|
|
694
|
+
( entity_property(Type, Key, _)
|
|
695
|
+
-> normalize_migration_value(Type, Key, Value, Normalized),
|
|
696
|
+
NormalizedProps = [Key=Normalized|RestProps]
|
|
697
|
+
; % Preserve unknown legacy RDF fields in the copied graph, but do not
|
|
698
|
+
% reject an otherwise valid entity merely because an older schema
|
|
699
|
+
% version stored an extension that the current validator no longer
|
|
700
|
+
% models. Required/currently typed fields are still validated below.
|
|
701
|
+
NormalizedProps = RestProps
|
|
702
|
+
).
|
|
703
|
+
|
|
704
|
+
% Legacy RDF reads intentionally preserve typed literal wrappers for the
|
|
705
|
+
% public query API. Validation, however, needs the schema-level value (atom,
|
|
706
|
+
% string, number, boolean, or list) that an upsert would have received. Use
|
|
707
|
+
% the entity schema as the decoder instead of maintaining another list of
|
|
708
|
+
% fields that can drift as the schema evolves.
|
|
709
|
+
normalize_migration_value(Type, Key, Raw, Normalized) :-
|
|
710
|
+
migration_unwrap_literal(Raw, Value),
|
|
711
|
+
( entity_property(Type, Key, Kind)
|
|
712
|
+
-> normalize_migration_kind(Kind, Value, Normalized)
|
|
713
|
+
; Normalized = Value
|
|
714
|
+
).
|
|
715
|
+
|
|
716
|
+
migration_unwrap_literal(^^(Value, _Type), Value) :- !.
|
|
717
|
+
migration_unwrap_literal(literal(type(_, Value)), Value) :- !.
|
|
718
|
+
migration_unwrap_literal(literal(lang(_, Value)), Value) :- !.
|
|
719
|
+
migration_unwrap_literal(literal(Value), Value) :- !.
|
|
720
|
+
migration_unwrap_literal(Value, Value).
|
|
721
|
+
|
|
722
|
+
normalize_migration_kind(atom, Value, Normalized) :-
|
|
723
|
+
!,
|
|
724
|
+
normalize_term_atom(Value, Normalized).
|
|
725
|
+
normalize_migration_kind(atom_or_string, Value, Normalized) :-
|
|
726
|
+
!,
|
|
727
|
+
(atom(Value) ; string(Value)),
|
|
728
|
+
Normalized = Value.
|
|
729
|
+
normalize_migration_kind(string, Value, Normalized) :-
|
|
730
|
+
!,
|
|
731
|
+
migration_string(Value, Normalized).
|
|
732
|
+
normalize_migration_kind(uri, Value, Normalized) :-
|
|
733
|
+
!,
|
|
734
|
+
migration_string(Value, Normalized).
|
|
735
|
+
normalize_migration_kind(datetime, Value, Normalized) :-
|
|
736
|
+
!,
|
|
737
|
+
migration_string(Value, Normalized).
|
|
738
|
+
normalize_migration_kind(list, Value, Normalized) :-
|
|
739
|
+
!,
|
|
740
|
+
( is_list(Value)
|
|
741
|
+
-> maplist(normalize_migration_list_value, Value, Normalized)
|
|
742
|
+
; Normalized = Value
|
|
743
|
+
).
|
|
744
|
+
normalize_migration_kind(list_or_json, Value, Value) :- !.
|
|
745
|
+
normalize_migration_kind(integer, Value, Normalized) :-
|
|
746
|
+
!,
|
|
747
|
+
(integer(Value) -> Normalized = Value ; atom_number(Value, Normalized)).
|
|
748
|
+
normalize_migration_kind(number, Value, Normalized) :-
|
|
749
|
+
!,
|
|
750
|
+
(number(Value) -> Normalized = Value ; atom_number(Value, Normalized)).
|
|
751
|
+
normalize_migration_kind(boolean, Value, Value) :- !.
|
|
752
|
+
normalize_migration_kind(_, Value, Value).
|
|
753
|
+
|
|
754
|
+
migration_string(Value, Value) :- string(Value), !.
|
|
755
|
+
migration_string(Value, String) :- atom(Value), !, atom_string(Value, String).
|
|
756
|
+
migration_string(Value, String) :- number(Value), !, number_string(Value, String).
|
|
757
|
+
migration_string(Value, String) :- term_string(Value, String).
|
|
758
|
+
|
|
759
|
+
normalize_migration_list_value(Value, Normalized) :-
|
|
760
|
+
(atom(Value) -> Normalized = Value ; string(Value) -> Normalized = Value ; Normalized = Value).
|
|
761
|
+
|
|
762
|
+
migration_atom_key(id).
|
|
763
|
+
migration_atom_key(status).
|
|
764
|
+
migration_atom_key(owner).
|
|
765
|
+
migration_atom_key(priority).
|
|
766
|
+
migration_atom_key(severity).
|
|
767
|
+
migration_atom_key(symbol_role).
|
|
768
|
+
migration_atom_key(granularity_reason).
|
|
769
|
+
migration_atom_key(fact_kind).
|
|
770
|
+
migration_atom_key(operator).
|
|
771
|
+
migration_atom_key(value_type).
|
|
772
|
+
migration_atom_key(polarity).
|
|
773
|
+
|
|
774
|
+
audit_resource(Resource) :-
|
|
775
|
+
atom(Resource),
|
|
776
|
+
sub_atom(Resource, 0, _, _, 'urn:kibi:audit/').
|
|
777
|
+
|
|
778
|
+
write_journal_marker(Directory) :-
|
|
779
|
+
atom_concat(Directory, '/storage.json', Marker),
|
|
780
|
+
setup_call_cleanup(
|
|
781
|
+
open(Marker, write, Stream, [encoding(utf8)]),
|
|
782
|
+
format(Stream,
|
|
783
|
+
'{"format":"kibi.rdf-journal.v1","schemaVersion":1}~n',
|
|
784
|
+
[]),
|
|
785
|
+
close(Stream)
|
|
786
|
+
),
|
|
787
|
+
get_time(Now),
|
|
788
|
+
Millis is floor(Now * 1000),
|
|
789
|
+
(current_prolog_flag(pid, Pid) -> true ; Pid = 0),
|
|
790
|
+
format(atom(Generation), 'generation-~w-~w', [Pid, Millis]),
|
|
791
|
+
atom_concat(Directory, '/CURRENT', HeadFile),
|
|
792
|
+
setup_call_cleanup(
|
|
793
|
+
open(HeadFile, write, HeadStream, [encoding(utf8)]),
|
|
794
|
+
format(HeadStream, '~w:0~n', [Generation]),
|
|
795
|
+
close(HeadStream)
|
|
796
|
+
).
|
|
797
|
+
|
|
798
|
+
export_audit_events(Stream, GraphURI) :-
|
|
799
|
+
forall(
|
|
800
|
+
rdf(Audit, 'urn:kibi:audit/operation', Operation, GraphURI),
|
|
801
|
+
(
|
|
802
|
+
rdf(Audit, 'urn:kibi:audit/timestamp', Timestamp, GraphURI),
|
|
803
|
+
rdf(Audit, 'urn:kibi:audit/entity', Entity, GraphURI),
|
|
804
|
+
rdf(Audit, 'urn:kibi:audit/data', Data, GraphURI),
|
|
805
|
+
format(Stream, 'changeset(~q, ~q, ~q, ~q).~n',
|
|
806
|
+
[Timestamp, Operation, Entity, Data])
|
|
807
|
+
)
|
|
808
|
+
).
|
|
809
|
+
|
|
188
810
|
temp_rdf_file(Directory, TempFile) :-
|
|
189
811
|
get_time(Timestamp),
|
|
190
812
|
Millis is floor(Timestamp * 1000),
|
|
@@ -234,6 +856,22 @@ ensure_snapshot_current(DataFile) :-
|
|
|
234
856
|
with_kb_mutex(Goal) :-
|
|
235
857
|
with_mutex(kb_lock, Goal).
|
|
236
858
|
|
|
859
|
+
% Run a journaled RDF mutation atomically and only mark the branch dirty after
|
|
860
|
+
% the transaction commits. A failed validation/contradiction check therefore
|
|
861
|
+
% cannot make a later shutdown publish a phantom commit sequence. Preserve a
|
|
862
|
+
% pre-existing dirty bit for callers that intentionally batch several writes
|
|
863
|
+
% before an explicit kb_save/0.
|
|
864
|
+
kb_run_journaled_mutation(Goal) :-
|
|
865
|
+
( kb_dirty -> WasDirty = true ; WasDirty = false ),
|
|
866
|
+
catch(
|
|
867
|
+
rdf_transaction(Goal),
|
|
868
|
+
Error,
|
|
869
|
+
( (WasDirty == false -> retractall(kb_dirty) ; true),
|
|
870
|
+
throw(Error)
|
|
871
|
+
)
|
|
872
|
+
),
|
|
873
|
+
kb_mark_dirty.
|
|
874
|
+
|
|
237
875
|
%% load_kb_pl_files(+Directory)
|
|
238
876
|
% Load legacy .pl entity files from the KB directory.
|
|
239
877
|
% These files use entity/4 format: entity(Type, Id, Title, Props).
|
|
@@ -264,8 +902,15 @@ kb_assert_entity(Type, Props) :-
|
|
|
264
902
|
-> ChangeKind = updated
|
|
265
903
|
; ChangeKind = created
|
|
266
904
|
),
|
|
267
|
-
|
|
268
|
-
|
|
905
|
+
( kb_storage_mode(journaled)
|
|
906
|
+
-> kb_run_journaled_mutation((
|
|
907
|
+
kb_assert_entity_no_audit(Type, Props),
|
|
908
|
+
kb_log_entity_upsert(ChangeKind, Type, Props)
|
|
909
|
+
))
|
|
910
|
+
; kb_mark_dirty,
|
|
911
|
+
kb_assert_entity_no_audit(Type, Props),
|
|
912
|
+
kb_log_entity_upsert(ChangeKind, Type, Props)
|
|
913
|
+
).
|
|
269
914
|
|
|
270
915
|
%% kb_assert_entity_no_audit(+Type, +Properties)
|
|
271
916
|
% Assert an entity RDF payload without recording audit side effects.
|
|
@@ -302,26 +947,229 @@ kb_assert_entity_no_audit(Type, Props) :-
|
|
|
302
947
|
member(Key=Value, Props),
|
|
303
948
|
store_property(EntityURI, Key, Value, Graph)
|
|
304
949
|
)
|
|
950
|
+
)),
|
|
951
|
+
kb_refresh_entity_index(Id).
|
|
952
|
+
|
|
953
|
+
%% kb_commit_upsert(+Type, +Properties, +Relationships, +SkipContradiction, -ChangeKind)
|
|
954
|
+
% Commit an entity upsert, its relationships, audit entries, and RDF snapshot
|
|
955
|
+
% as one bounded operation. The branch lock is held before any RDF mutation,
|
|
956
|
+
% so concurrent runtimes cannot observe or publish an intermediate upsert.
|
|
957
|
+
% Relationships use rel(Type, FromId, ToId, Metadata) terms. Metadata is
|
|
958
|
+
% retained for the caller's serialization contract; the audit schema records
|
|
959
|
+
% only the relationship endpoints and type, as before.
|
|
960
|
+
kb_commit_upsert(Type, Props, Relationships, SkipContradiction, ChangeKind) :-
|
|
961
|
+
kb_storage_mode(journaled),
|
|
962
|
+
!,
|
|
963
|
+
memberchk(id=Id, Props),
|
|
964
|
+
memberchk(SkipContradiction, [true, false]),
|
|
965
|
+
upsert_change_kind(Id, ChangeKind),
|
|
966
|
+
with_kb_mutex((
|
|
967
|
+
( kb_dirty -> WasDirty = true ; WasDirty = false ),
|
|
968
|
+
catch(
|
|
969
|
+
rdf_transaction((
|
|
970
|
+
kb_stage(rdf_mutation),
|
|
971
|
+
kb_assert_entity_no_audit(Type, Props),
|
|
972
|
+
kb_commit_relationships_no_audit(Relationships),
|
|
973
|
+
kb_maybe_check_req_contradiction(Type, Id, SkipContradiction),
|
|
974
|
+
kb_stage(entity_audit),
|
|
975
|
+
kb_log_entity_upsert(ChangeKind, Type, Props),
|
|
976
|
+
kb_stage(relationship_audit),
|
|
977
|
+
kb_commit_relationship_audits(Relationships)
|
|
978
|
+
)),
|
|
979
|
+
Error,
|
|
980
|
+
( (WasDirty == false -> retractall(kb_dirty) ; true),
|
|
981
|
+
throw(Error)
|
|
982
|
+
)
|
|
983
|
+
),
|
|
984
|
+
kb_mark_dirty,
|
|
985
|
+
kb_save_journaled
|
|
986
|
+
)).
|
|
987
|
+
kb_commit_upsert(Type, Props, Relationships, SkipContradiction, ChangeKind) :-
|
|
988
|
+
memberchk(id=Id, Props),
|
|
989
|
+
memberchk(SkipContradiction, [true, false]),
|
|
990
|
+
kb_attached(Directory),
|
|
991
|
+
atom_concat(Directory, '/kb.rdf', DataFile),
|
|
992
|
+
kb_stage(runtime),
|
|
993
|
+
kb_runtime_diagnostic,
|
|
994
|
+
% Emit the lock marker before waiting so a timeout identifies a branch
|
|
995
|
+
% lock held by another runtime instead of reporting only the prior stage.
|
|
996
|
+
kb_stage(lock),
|
|
997
|
+
with_kb_file_lock(Directory, (
|
|
998
|
+
audit_store_writable,
|
|
999
|
+
ensure_snapshot_current(DataFile),
|
|
1000
|
+
upsert_change_kind(Id, ChangeKind),
|
|
1001
|
+
rdf_transaction((
|
|
1002
|
+
kb_stage(rdf_mutation),
|
|
1003
|
+
kb_assert_entity_no_audit(Type, Props),
|
|
1004
|
+
kb_commit_relationships_no_audit(Relationships),
|
|
1005
|
+
kb_maybe_check_req_contradiction(Type, Id, SkipContradiction),
|
|
1006
|
+
kb_stage(entity_audit),
|
|
1007
|
+
kb_log_entity_upsert(ChangeKind, Type, Props),
|
|
1008
|
+
kb_stage(relationship_audit),
|
|
1009
|
+
kb_commit_relationship_audits(Relationships),
|
|
1010
|
+
kb_save_locked(Directory)
|
|
1011
|
+
))
|
|
305
1012
|
)).
|
|
306
1013
|
|
|
1014
|
+
upsert_change_kind(Id, updated) :-
|
|
1015
|
+
once(kb_entity(Id, _, _)),
|
|
1016
|
+
!.
|
|
1017
|
+
upsert_change_kind(_, created).
|
|
1018
|
+
|
|
1019
|
+
kb_commit_relationships_no_audit([]).
|
|
1020
|
+
kb_commit_relationships_no_audit([
|
|
1021
|
+
rel(RelType, FromId, ToId, Metadata)|Rest
|
|
1022
|
+
]) :-
|
|
1023
|
+
kb_assert_relationship_no_audit(RelType, FromId, ToId, Metadata),
|
|
1024
|
+
kb_commit_relationships_no_audit(Rest).
|
|
1025
|
+
|
|
1026
|
+
kb_maybe_check_req_contradiction(req, Id, false) :-
|
|
1027
|
+
!,
|
|
1028
|
+
kb_stage(contradiction_check),
|
|
1029
|
+
check_req_contradiction(Id).
|
|
1030
|
+
kb_maybe_check_req_contradiction(_, _, _).
|
|
1031
|
+
|
|
1032
|
+
kb_commit_relationship_audits([]).
|
|
1033
|
+
kb_commit_relationship_audits([
|
|
1034
|
+
rel(RelType, FromId, ToId, _Metadata)|Rest
|
|
1035
|
+
]) :-
|
|
1036
|
+
kb_log_relationship_upsert(RelType, FromId, ToId, []),
|
|
1037
|
+
kb_commit_relationship_audits(Rest).
|
|
1038
|
+
|
|
1039
|
+
% Probe the existing journal without waiting. A process running code from
|
|
1040
|
+
% before the sync(close) fix may still retain the journal's write lock; fail
|
|
1041
|
+
% before touching RDF so callers receive a safe, retryable diagnostic.
|
|
1042
|
+
audit_store_writable :-
|
|
1043
|
+
kb_audit_db(AuditLog),
|
|
1044
|
+
( exists_file(AuditLog)
|
|
1045
|
+
-> catch(
|
|
1046
|
+
setup_call_cleanup(
|
|
1047
|
+
open(AuditLog, append, Stream,
|
|
1048
|
+
[lock(write), wait(false), encoding(utf8)]),
|
|
1049
|
+
true,
|
|
1050
|
+
close(Stream)
|
|
1051
|
+
),
|
|
1052
|
+
Error,
|
|
1053
|
+
throw(error(permission_error(lock, audit_log, AuditLog),
|
|
1054
|
+
context(kb_commit_upsert/5, Error)))
|
|
1055
|
+
)
|
|
1056
|
+
; true
|
|
1057
|
+
).
|
|
1058
|
+
|
|
1059
|
+
% Reserved stderr markers are consumed by PrologProcess diagnostics and never
|
|
1060
|
+
% form part of the JSON result. They make timeout errors stage-specific even
|
|
1061
|
+
% when the child is blocked inside a filesystem primitive.
|
|
1062
|
+
kb_stage(Stage) :-
|
|
1063
|
+
format(user_error, '__KIBI_STAGE__:~w~n', [Stage]),
|
|
1064
|
+
flush_output(user_error).
|
|
1065
|
+
|
|
1066
|
+
kb_runtime_diagnostic :-
|
|
1067
|
+
( current_prolog_flag(pid, Pid) -> true ; Pid = unknown ),
|
|
1068
|
+
( kb_audit_db(AuditLog) -> true ; AuditLog = unknown ),
|
|
1069
|
+
kb_environment_value('KIBI_RUNTIME_NAME', RuntimeName),
|
|
1070
|
+
kb_environment_value('KIBI_RUNTIME_VERSION', RuntimeVersion),
|
|
1071
|
+
kb_environment_value('KIBI_PACKAGE_VERSIONS', PackageVersions),
|
|
1072
|
+
format(user_error,
|
|
1073
|
+
'__KIBI_RUNTIME__:pid=~w;audit=~w;runtime=~w@~w;packages=~w~n',
|
|
1074
|
+
[Pid, AuditLog, RuntimeName, RuntimeVersion, PackageVersions]),
|
|
1075
|
+
flush_output(user_error).
|
|
1076
|
+
|
|
1077
|
+
kb_environment_value(Name, Value) :-
|
|
1078
|
+
( getenv(Name, Value)
|
|
1079
|
+
-> true
|
|
1080
|
+
; Value = unknown
|
|
1081
|
+
).
|
|
1082
|
+
|
|
307
1083
|
%% kb_retract_entity_relationships(+Id)
|
|
308
1084
|
% Remove all relationship triples for an entity, preserving property triples.
|
|
309
1085
|
% Used by projectStagedEntities to clear stale relationships before re-asserting.
|
|
310
1086
|
kb_retract_entity_relationships(Id) :-
|
|
311
1087
|
kb_graph(Graph),
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
1088
|
+
( kb_storage_mode(journaled)
|
|
1089
|
+
-> kb_run_journaled_mutation(with_kb_mutex((
|
|
1090
|
+
entity_id_to_uri(Id, EntityURI),
|
|
1091
|
+
forall(
|
|
1092
|
+
( rdf(EntityURI, RelURI, TargetURI, Graph),
|
|
1093
|
+
atom(TargetURI),
|
|
1094
|
+
entity_uri_to_id(TargetURI, _)
|
|
1095
|
+
),
|
|
1096
|
+
rdf_retractall(EntityURI, RelURI, TargetURI, Graph)
|
|
1097
|
+
)
|
|
1098
|
+
)))
|
|
1099
|
+
; kb_mark_dirty,
|
|
1100
|
+
with_kb_mutex((
|
|
1101
|
+
entity_id_to_uri(Id, EntityURI),
|
|
1102
|
+
forall(
|
|
1103
|
+
( rdf(EntityURI, RelURI, TargetURI, Graph),
|
|
1104
|
+
atom(TargetURI),
|
|
1105
|
+
entity_uri_to_id(TargetURI, _)
|
|
1106
|
+
),
|
|
1107
|
+
rdf_retractall(EntityURI, RelURI, TargetURI, Graph)
|
|
1108
|
+
)
|
|
1109
|
+
))
|
|
1110
|
+
).
|
|
1111
|
+
|
|
1112
|
+
%% kb_retract_relationship(+Type, +From, +To)
|
|
1113
|
+
% Retract one compiled edge and its audit record atomically. This is used by
|
|
1114
|
+
% relationship-shard deltas so a small shard edit never requires clearing and
|
|
1115
|
+
% rebuilding every edge in the graph.
|
|
1116
|
+
kb_retract_relationship(RelType, FromId, ToId) :-
|
|
1117
|
+
kb_graph(Graph),
|
|
1118
|
+
entity_id_to_uri(FromId, FromURI),
|
|
1119
|
+
entity_id_to_uri(ToId, ToURI),
|
|
1120
|
+
kb_uri(BaseURI),
|
|
1121
|
+
atom_concat(BaseURI, RelType, RelURI),
|
|
1122
|
+
( kb_storage_mode(journaled)
|
|
1123
|
+
-> kb_run_journaled_mutation(with_kb_mutex((
|
|
1124
|
+
rdf_retractall(FromURI, RelURI, ToURI, Graph),
|
|
1125
|
+
format(atom(RelId), '~w->~w', [FromId, ToId]),
|
|
1126
|
+
kb_assert_audit_event(delete_rel, RelId,
|
|
1127
|
+
RelType-[from=FromId, to=ToId])
|
|
1128
|
+
)))
|
|
1129
|
+
; kb_mark_dirty,
|
|
1130
|
+
with_kb_mutex(rdf_retractall(FromURI, RelURI, ToURI, Graph))
|
|
1131
|
+
).
|
|
1132
|
+
|
|
1133
|
+
%% kb_retract_all_relationships/0
|
|
1134
|
+
% Remove only entity-to-entity edges. This is used when a relationship shard
|
|
1135
|
+
% changes: RDF property literals and audit resources remain untouched, while
|
|
1136
|
+
% the current shard contents can be asserted again in one transaction.
|
|
1137
|
+
% implements REQ-core-journaled-engine-persistence
|
|
1138
|
+
kb_retract_all_relationships :-
|
|
1139
|
+
kb_graph(Graph),
|
|
1140
|
+
( kb_storage_mode(journaled)
|
|
1141
|
+
-> kb_run_journaled_mutation(with_kb_mutex(
|
|
1142
|
+
forall(
|
|
1143
|
+
( rdf(SubjectURI, RelURI, ObjectURI, Graph),
|
|
1144
|
+
atom(SubjectURI),
|
|
1145
|
+
atom(ObjectURI),
|
|
1146
|
+
entity_uri_to_id(SubjectURI, _),
|
|
1147
|
+
entity_uri_to_id(ObjectURI, _)
|
|
1148
|
+
),
|
|
1149
|
+
rdf_retractall(SubjectURI, RelURI, ObjectURI, Graph)
|
|
1150
|
+
)
|
|
1151
|
+
))
|
|
1152
|
+
; kb_mark_dirty,
|
|
1153
|
+
with_kb_mutex(
|
|
1154
|
+
forall(
|
|
1155
|
+
( rdf(SubjectURI, RelURI, ObjectURI, Graph),
|
|
1156
|
+
atom(SubjectURI),
|
|
1157
|
+
atom(ObjectURI),
|
|
1158
|
+
entity_uri_to_id(SubjectURI, _),
|
|
1159
|
+
entity_uri_to_id(ObjectURI, _)
|
|
1160
|
+
),
|
|
1161
|
+
rdf_retractall(SubjectURI, RelURI, ObjectURI, Graph)
|
|
1162
|
+
)
|
|
320
1163
|
)
|
|
321
|
-
)
|
|
1164
|
+
).
|
|
322
1165
|
|
|
323
1166
|
%% kb_log_entity_upsert(+ChangeKind, +Type, +Properties)
|
|
324
1167
|
% Append the audit entry for a successfully committed entity upsert.
|
|
1168
|
+
kb_log_entity_upsert(ChangeKind, Type, Props) :-
|
|
1169
|
+
kb_storage_mode(journaled),
|
|
1170
|
+
!,
|
|
1171
|
+
memberchk(id=Id, Props),
|
|
1172
|
+
kb_assert_audit_event(upsert, Id, Type-[change_kind=ChangeKind|Props]).
|
|
325
1173
|
kb_log_entity_upsert(ChangeKind, Type, Props) :-
|
|
326
1174
|
memberchk(id=Id, Props),
|
|
327
1175
|
memberchk(ChangeKind, [created, updated]),
|
|
@@ -343,6 +1191,26 @@ kb_retract_entity(Id) :-
|
|
|
343
1191
|
|
|
344
1192
|
%% kb_retract_entity(+Id, +Type, +AuditProps)
|
|
345
1193
|
% Remove an entity from the KB and log the provided delete payload.
|
|
1194
|
+
kb_retract_entity(Id, Type, AuditProps) :-
|
|
1195
|
+
kb_storage_mode(journaled),
|
|
1196
|
+
!,
|
|
1197
|
+
kb_graph(Graph),
|
|
1198
|
+
kb_run_journaled_mutation(with_kb_mutex((
|
|
1199
|
+
entity_id_to_uri(Id, EntityURI),
|
|
1200
|
+
% A source delta may replace an entity that is still referenced by
|
|
1201
|
+
% another source. Remove both directions so the transaction cannot
|
|
1202
|
+
% leave a dangling relationship behind. The compiler re-adds valid
|
|
1203
|
+
% edges from the current source shards in the same sync.
|
|
1204
|
+
forall(
|
|
1205
|
+
( rdf(SubjectURI, RelURI, EntityURI, Graph),
|
|
1206
|
+
entity_uri_to_id(SubjectURI, _)
|
|
1207
|
+
),
|
|
1208
|
+
rdf_retractall(SubjectURI, RelURI, EntityURI, Graph)
|
|
1209
|
+
),
|
|
1210
|
+
rdf_retractall(EntityURI, _, _, Graph),
|
|
1211
|
+
kb_assert_audit_event(delete, Id, Type-AuditProps),
|
|
1212
|
+
kb_remove_entity_index(Id)
|
|
1213
|
+
))).
|
|
346
1214
|
kb_retract_entity(Id, Type, AuditProps) :-
|
|
347
1215
|
kb_graph(Graph),
|
|
348
1216
|
with_kb_mutex((
|
|
@@ -353,12 +1221,13 @@ kb_retract_entity(Id, Type, AuditProps) :-
|
|
|
353
1221
|
% Log to audit
|
|
354
1222
|
get_time(Timestamp),
|
|
355
1223
|
format_time(atom(TS), '%FT%T%:z', Timestamp),
|
|
356
|
-
assert_changeset(TS, delete, Id, Type-AuditProps)
|
|
1224
|
+
assert_changeset(TS, delete, Id, Type-AuditProps),
|
|
1225
|
+
kb_remove_entity_index(Id)
|
|
357
1226
|
)).
|
|
358
1227
|
|
|
359
1228
|
entity_delete_audit_props(Id, Props, AuditProps) :-
|
|
360
1229
|
findall(Key=Value,
|
|
361
|
-
( member(Key, [title, source, text_ref]),
|
|
1230
|
+
( member(Key, [title, source, text_ref, semantic_text]),
|
|
362
1231
|
memberchk(Key=RawValue, Props),
|
|
363
1232
|
audit_property_value(RawValue, Value)
|
|
364
1233
|
),
|
|
@@ -381,6 +1250,20 @@ audit_property_value(RawValue, Value) :-
|
|
|
381
1250
|
% Query entities from the KB.
|
|
382
1251
|
% Properties is unified with a list of Key=Value pairs.
|
|
383
1252
|
kb_entity(Id, Type, Props) :-
|
|
1253
|
+
( nonvar(Id)
|
|
1254
|
+
-> kb_entity_raw(Id, Type, Props)
|
|
1255
|
+
; kb_ensure_indexes,
|
|
1256
|
+
( nonvar(Type)
|
|
1257
|
+
-> kb_index_type(Type, IndexedId),
|
|
1258
|
+
kb_entity_raw(IndexedId, Type, Props),
|
|
1259
|
+
Id = IndexedId
|
|
1260
|
+
; kb_entity_raw(Id, Type, Props)
|
|
1261
|
+
)
|
|
1262
|
+
).
|
|
1263
|
+
|
|
1264
|
+
% Raw RDF/entity facts are kept separate from the index wrapper. Rebuilding
|
|
1265
|
+
% an index must never recurse through the indexed query path.
|
|
1266
|
+
kb_entity_raw(Id, Type, Props) :-
|
|
384
1267
|
kb_graph(Graph),
|
|
385
1268
|
% Find entity by pattern - use unquoted namespace term kb:type
|
|
386
1269
|
( var(Id)
|
|
@@ -398,14 +1281,115 @@ kb_entity(Id, Type, Props) :-
|
|
|
398
1281
|
atom_concat(BaseURI, type, TypeURI),
|
|
399
1282
|
PropURI \= TypeURI,
|
|
400
1283
|
uri_to_key(PropURI, Key),
|
|
401
|
-
literal_to_value(ValueLiteral, Value)
|
|
1284
|
+
literal_to_value(Key, ValueLiteral, Value)
|
|
402
1285
|
), Props).
|
|
403
1286
|
|
|
404
1287
|
% Fallback: read from legacy entity/4 facts loaded from .pl files
|
|
405
|
-
|
|
1288
|
+
kb_entity_raw(Id, Type, Props) :-
|
|
406
1289
|
entity(Type, Id, _Title, PropList),
|
|
407
1290
|
convert_legacy_props(PropList, Props).
|
|
408
1291
|
|
|
1292
|
+
kb_ensure_indexes :-
|
|
1293
|
+
( kb_index_ready,
|
|
1294
|
+
\+ kb_index_dirty,
|
|
1295
|
+
kb_index_entity_count(Expected),
|
|
1296
|
+
current_entity_index_count(Expected)
|
|
1297
|
+
-> true
|
|
1298
|
+
; kb_rebuild_indexes
|
|
1299
|
+
).
|
|
1300
|
+
|
|
1301
|
+
% Rebuild all acceleration structures from RDF/entity facts. This is
|
|
1302
|
+
% intentionally disposable: no query result is written back to RDF when an
|
|
1303
|
+
% index is stale or externally invalidated.
|
|
1304
|
+
kb_rebuild_indexes :-
|
|
1305
|
+
kb_attached(_),
|
|
1306
|
+
with_kb_mutex((
|
|
1307
|
+
retractall(kb_index_entity(_, _)),
|
|
1308
|
+
retractall(kb_index_type(_, _)),
|
|
1309
|
+
retractall(kb_index_tag(_, _)),
|
|
1310
|
+
retractall(kb_index_source(_, _)),
|
|
1311
|
+
retractall(kb_index_token(_, _)),
|
|
1312
|
+
retractall(kb_index_symbol_coordinate(_, _, _, _, _)),
|
|
1313
|
+
findall(Id-Type-Props, kb_entity_raw(Id, Type, Props), Rows),
|
|
1314
|
+
forall(member(Id-Type-Props, Rows), index_entity_row(Id, Type, Props)),
|
|
1315
|
+
length(Rows, EntityCount),
|
|
1316
|
+
retractall(kb_index_entity_count(_)),
|
|
1317
|
+
assertz(kb_index_entity_count(EntityCount)),
|
|
1318
|
+
rdf_statistics(triples(TripleCount)),
|
|
1319
|
+
retractall(kb_index_triple_count(_)),
|
|
1320
|
+
assertz(kb_index_triple_count(TripleCount)),
|
|
1321
|
+
retractall(kb_index_dirty),
|
|
1322
|
+
retractall(kb_index_ready),
|
|
1323
|
+
assertz(kb_index_ready)
|
|
1324
|
+
)).
|
|
1325
|
+
|
|
1326
|
+
current_entity_index_count(Count) :-
|
|
1327
|
+
aggregate_all(count,
|
|
1328
|
+
rdf(_, kb:type, _, _),
|
|
1329
|
+
RdfCount),
|
|
1330
|
+
aggregate_all(count,
|
|
1331
|
+
entity(_, _, _, _),
|
|
1332
|
+
LegacyCount),
|
|
1333
|
+
Count is RdfCount + LegacyCount.
|
|
1334
|
+
|
|
1335
|
+
index_entity_row(Id, Type, Props) :-
|
|
1336
|
+
assertz(kb_index_entity(Id, Type)),
|
|
1337
|
+
assertz(kb_index_type(Type, Id)),
|
|
1338
|
+
( entity_source_atom(Props, Source)
|
|
1339
|
+
-> assertz(kb_index_source(Source, Id)),
|
|
1340
|
+
index_text_tokens(Source, SourceTokens),
|
|
1341
|
+
forall(member(Token, SourceTokens), assertz(kb_index_token(Token, Id)))
|
|
1342
|
+
; true
|
|
1343
|
+
),
|
|
1344
|
+
( memberchk(title=TitleValue, Props)
|
|
1345
|
+
-> normalize_term_atom(TitleValue, Title),
|
|
1346
|
+
index_text_tokens(Title, TitleTokens),
|
|
1347
|
+
forall(member(Token, TitleTokens), assertz(kb_index_token(Token, Id)))
|
|
1348
|
+
; true
|
|
1349
|
+
),
|
|
1350
|
+
index_text_tokens(Id, IdTokens),
|
|
1351
|
+
forall(member(Token, IdTokens), assertz(kb_index_token(Token, Id))),
|
|
1352
|
+
index_text_tokens(Type, TypeTokens),
|
|
1353
|
+
forall(member(Token, TypeTokens), assertz(kb_index_token(Token, Id))),
|
|
1354
|
+
forall(
|
|
1355
|
+
( member(SearchKey, [owner, priority, severity, text_ref, semantic_text]),
|
|
1356
|
+
memberchk(SearchKey=SearchValue, Props) ),
|
|
1357
|
+
( index_text_tokens(SearchValue, SearchTokens),
|
|
1358
|
+
forall(member(Token, SearchTokens), assertz(kb_index_token(Token, Id))) )
|
|
1359
|
+
),
|
|
1360
|
+
( memberchk(tags=RawTags, Props), is_list(RawTags)
|
|
1361
|
+
-> forall(member(RawTag, RawTags),
|
|
1362
|
+
( normalize_term_atom(RawTag, Tag),
|
|
1363
|
+
assertz(kb_index_tag(Tag, Id)),
|
|
1364
|
+
index_text_tokens(Tag, TagTokens),
|
|
1365
|
+
forall(member(Token, TagTokens), assertz(kb_index_token(Token, Id)))
|
|
1366
|
+
))
|
|
1367
|
+
; true
|
|
1368
|
+
),
|
|
1369
|
+
( Type == symbol,
|
|
1370
|
+
memberchk(sourceLine=Line, Props),
|
|
1371
|
+
memberchk(sourceColumn=Column, Props),
|
|
1372
|
+
memberchk(sourceEndLine=EndLine, Props),
|
|
1373
|
+
memberchk(sourceEndColumn=EndColumn, Props)
|
|
1374
|
+
-> assertz(kb_index_symbol_coordinate(Id, Line, Column, EndLine, EndColumn))
|
|
1375
|
+
; true
|
|
1376
|
+
).
|
|
1377
|
+
|
|
1378
|
+
index_text_tokens(Value, Tokens) :-
|
|
1379
|
+
normalize_term_atom(Value, Atom),
|
|
1380
|
+
downcase_atom(Atom, Lower),
|
|
1381
|
+
atomic_list_concat(Parts, ' ', Lower),
|
|
1382
|
+
findall(Token,
|
|
1383
|
+
( member(Part, Parts),
|
|
1384
|
+
atom_string(Part, PartString),
|
|
1385
|
+
split_string(PartString, "-_./:#", "-_./:#", Strings),
|
|
1386
|
+
member(String, Strings),
|
|
1387
|
+
String \= '',
|
|
1388
|
+
atom_string(Token, String)
|
|
1389
|
+
),
|
|
1390
|
+
RawTokens),
|
|
1391
|
+
sort(RawTokens, Tokens).
|
|
1392
|
+
|
|
409
1393
|
% Convert legacy property list format to Key=Value pairs
|
|
410
1394
|
convert_legacy_props([], []).
|
|
411
1395
|
convert_legacy_props([Prop|Rest], [Key=Value|OutRest]) :-
|
|
@@ -422,13 +1406,130 @@ convert_legacy_prop(Prop, Prop, true).
|
|
|
422
1406
|
%% kb_entities_by_source(+SourcePath, -Ids)
|
|
423
1407
|
% Returns all entity IDs whose source property matches SourcePath (substring match).
|
|
424
1408
|
kb_entities_by_source(SourcePath, Ids) :-
|
|
1409
|
+
kb_ensure_indexes,
|
|
1410
|
+
source_value_atom(SourcePath, SourceQuery),
|
|
425
1411
|
findall(Id,
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
1412
|
+
( kb_index_source(SourceAtom, Id),
|
|
1413
|
+
sub_atom(SourceAtom, _, _, _, SourceQuery) ),
|
|
1414
|
+
RawIds),
|
|
1415
|
+
sort(RawIds, Ids).
|
|
1416
|
+
|
|
1417
|
+
%% kb_entities_by_tag(+Tag, -Ids)
|
|
1418
|
+
% Index-backed tag lookup used by exact/paginated discovery queries.
|
|
1419
|
+
kb_entities_by_tag(Tag, Ids) :-
|
|
1420
|
+
kb_ensure_indexes,
|
|
1421
|
+
normalize_term_atom(Tag, NormalizedTag),
|
|
1422
|
+
findall(Id, kb_index_tag(NormalizedTag, Id), RawIds),
|
|
430
1423
|
sort(RawIds, Ids).
|
|
431
1424
|
|
|
1425
|
+
%% kb_indexed_sources(-Sources)
|
|
1426
|
+
% Return unique normalized source paths for freshness checks without
|
|
1427
|
+
% materializing every entity property list.
|
|
1428
|
+
kb_indexed_sources(Sources) :-
|
|
1429
|
+
kb_ensure_indexes,
|
|
1430
|
+
( setof(Source, Id^kb_index_source(Source, Id), Sources)
|
|
1431
|
+
-> true
|
|
1432
|
+
; Sources = []
|
|
1433
|
+
).
|
|
1434
|
+
|
|
1435
|
+
%% kb_query_entities(+Type, +Id, +Tags, +Source, +Limit, +Offset, -Rows, -Count)
|
|
1436
|
+
% Index-backed exact/paginated discovery. Only the requested page is
|
|
1437
|
+
% materialized into full property lists; RDF remains authoritative for the
|
|
1438
|
+
% final rows and the index only supplies candidate IDs.
|
|
1439
|
+
kb_query_entities(TypeFilter, IdFilter, Tags, SourceFilter, Limit, Offset, Rows, Count) :-
|
|
1440
|
+
integer(Limit),
|
|
1441
|
+
integer(Offset),
|
|
1442
|
+
Limit >= 0,
|
|
1443
|
+
Offset >= 0,
|
|
1444
|
+
is_list(Tags),
|
|
1445
|
+
kb_ensure_indexes,
|
|
1446
|
+
findall(Id,
|
|
1447
|
+
indexed_entity_match(TypeFilter, IdFilter, Tags, SourceFilter, Id),
|
|
1448
|
+
RawIds),
|
|
1449
|
+
sort(RawIds, Ids),
|
|
1450
|
+
length(Ids, Count),
|
|
1451
|
+
drop_index_ids(Offset, Ids, Remaining),
|
|
1452
|
+
take_index_ids(Limit, Remaining, PageIds),
|
|
1453
|
+
findall([Id, Type, Props],
|
|
1454
|
+
( member(Id, PageIds),
|
|
1455
|
+
kb_entity(Id, Type, Props) ),
|
|
1456
|
+
Rows).
|
|
1457
|
+
|
|
1458
|
+
indexed_entity_match(TypeFilter, IdFilter, Tags, SourceFilter, Id) :-
|
|
1459
|
+
( IdFilter == none
|
|
1460
|
+
-> kb_index_entity(Id, _)
|
|
1461
|
+
; Id = IdFilter,
|
|
1462
|
+
kb_index_entity(Id, _)
|
|
1463
|
+
),
|
|
1464
|
+
( TypeFilter == none
|
|
1465
|
+
-> true
|
|
1466
|
+
; kb_index_type(TypeFilter, Id)
|
|
1467
|
+
),
|
|
1468
|
+
( Tags == []
|
|
1469
|
+
-> true
|
|
1470
|
+
; member(Tag, Tags),
|
|
1471
|
+
kb_index_tag(Tag, Id)
|
|
1472
|
+
),
|
|
1473
|
+
( SourceFilter == none
|
|
1474
|
+
-> true
|
|
1475
|
+
; source_value_atom(SourceFilter, SourceQuery),
|
|
1476
|
+
kb_index_source(Source, Id),
|
|
1477
|
+
sub_atom(Source, _, _, _, SourceQuery)
|
|
1478
|
+
).
|
|
1479
|
+
|
|
1480
|
+
drop_index_ids(0, Ids, Ids) :- !.
|
|
1481
|
+
drop_index_ids(_, [], []) :- !.
|
|
1482
|
+
drop_index_ids(Offset, [_|Rest], Remaining) :-
|
|
1483
|
+
Next is Offset - 1,
|
|
1484
|
+
drop_index_ids(Next, Rest, Remaining).
|
|
1485
|
+
|
|
1486
|
+
take_index_ids(0, _, []) :- !.
|
|
1487
|
+
take_index_ids(_, [], []) :- !.
|
|
1488
|
+
take_index_ids(Limit, [Id|Rest], [Id|Page]) :-
|
|
1489
|
+
Next is Limit - 1,
|
|
1490
|
+
take_index_ids(Next, Rest, Page).
|
|
1491
|
+
|
|
1492
|
+
%% kb_search_entities(+Type, +Query, +Limit, +Offset, -Rows, -Count)
|
|
1493
|
+
% Return an index-filtered page for the higher-level deterministic ranking
|
|
1494
|
+
% contract. Every normalized query token must match at least one indexed token
|
|
1495
|
+
% for the entity. The TypeScript ranker remains responsible for scores,
|
|
1496
|
+
% reasons, snippets, and final ordering.
|
|
1497
|
+
kb_search_entities(TypeFilter, Query, Limit, Offset, Rows, Count) :-
|
|
1498
|
+
integer(Limit),
|
|
1499
|
+
integer(Offset),
|
|
1500
|
+
Limit >= 0,
|
|
1501
|
+
Offset >= 0,
|
|
1502
|
+
kb_ensure_indexes,
|
|
1503
|
+
index_text_tokens(Query, QueryTokens),
|
|
1504
|
+
QueryTokens \= [],
|
|
1505
|
+
findall(Id,
|
|
1506
|
+
( kb_index_entity(Id, _),
|
|
1507
|
+
(TypeFilter == none -> true ; kb_index_type(TypeFilter, Id)),
|
|
1508
|
+
( indexed_markdown_source(Id)
|
|
1509
|
+
; forall(member(QueryToken, QueryTokens),
|
|
1510
|
+
indexed_token_match(QueryToken, Id)) ) ),
|
|
1511
|
+
RawIds),
|
|
1512
|
+
sort(RawIds, Ids),
|
|
1513
|
+
length(Ids, Count),
|
|
1514
|
+
drop_index_ids(Offset, Ids, Remaining),
|
|
1515
|
+
take_index_ids(Limit, Remaining, PageIds),
|
|
1516
|
+
findall([Id, Type, Props],
|
|
1517
|
+
( member(Id, PageIds), kb_entity(Id, Type, Props) ),
|
|
1518
|
+
Rows).
|
|
1519
|
+
|
|
1520
|
+
indexed_token_match(QueryToken, Id) :-
|
|
1521
|
+
kb_index_token(IndexedToken, Id),
|
|
1522
|
+
sub_atom(IndexedToken, _, _, _, QueryToken),
|
|
1523
|
+
!.
|
|
1524
|
+
|
|
1525
|
+
% Markdown bodies remain outside RDF and are ranked by the Node discovery
|
|
1526
|
+
% layer. Keep those usually-small document entities in the candidate set so
|
|
1527
|
+
% indexed search does not change the public body-search contract. Large symbol
|
|
1528
|
+
% manifests still take the fast token-index path.
|
|
1529
|
+
indexed_markdown_source(Id) :-
|
|
1530
|
+
kb_index_source(Source, Id),
|
|
1531
|
+
sub_atom(Source, _, _, _, '.md').
|
|
1532
|
+
|
|
432
1533
|
entity_source_atom(Props, SourceAtom) :-
|
|
433
1534
|
( memberchk(sourceFile=RawSourceFile, Props)
|
|
434
1535
|
-> source_value_atom(RawSourceFile, SourceAtom)
|
|
@@ -449,8 +1550,15 @@ source_value_atom(Value, Atom) :-
|
|
|
449
1550
|
%% kb_assert_relationship(+Type, +From, +To, +Metadata)
|
|
450
1551
|
% Assert a relationship between two entities with validation.
|
|
451
1552
|
kb_assert_relationship(RelType, FromId, ToId, Metadata) :-
|
|
452
|
-
|
|
453
|
-
|
|
1553
|
+
( kb_storage_mode(journaled)
|
|
1554
|
+
-> kb_run_journaled_mutation((
|
|
1555
|
+
kb_assert_relationship_no_audit(RelType, FromId, ToId, Metadata),
|
|
1556
|
+
kb_log_relationship_upsert(RelType, FromId, ToId, Metadata)
|
|
1557
|
+
))
|
|
1558
|
+
; kb_mark_dirty,
|
|
1559
|
+
kb_assert_relationship_no_audit(RelType, FromId, ToId, Metadata),
|
|
1560
|
+
kb_log_relationship_upsert(RelType, FromId, ToId, Metadata)
|
|
1561
|
+
).
|
|
454
1562
|
|
|
455
1563
|
%% kb_assert_relationship_no_audit(+Type, +From, +To, +Metadata)
|
|
456
1564
|
% Assert a relationship RDF payload without recording audit side effects.
|
|
@@ -516,6 +1624,12 @@ mixed_symbol_role(covered_by, SymbolId) :-
|
|
|
516
1624
|
|
|
517
1625
|
%% kb_log_relationship_upsert(+Type, +From, +To, +Metadata)
|
|
518
1626
|
% Append the audit entry for a successfully committed relationship upsert.
|
|
1627
|
+
kb_log_relationship_upsert(RelType, FromId, ToId, _Metadata) :-
|
|
1628
|
+
kb_storage_mode(journaled),
|
|
1629
|
+
!,
|
|
1630
|
+
format(atom(RelId), '~w->~w', [FromId, ToId]),
|
|
1631
|
+
kb_assert_audit_event(upsert_rel, RelId,
|
|
1632
|
+
RelType-[from=FromId, to=ToId]).
|
|
519
1633
|
kb_log_relationship_upsert(RelType, FromId, ToId, _Metadata) :-
|
|
520
1634
|
with_kb_mutex((
|
|
521
1635
|
get_time(Timestamp),
|
|
@@ -524,6 +1638,65 @@ kb_log_relationship_upsert(RelType, FromId, ToId, _Metadata) :-
|
|
|
524
1638
|
assert_changeset(TS, upsert_rel, RelId, RelType-[from=FromId, to=ToId])
|
|
525
1639
|
)).
|
|
526
1640
|
|
|
1641
|
+
% Journaled audit records live in the same RDF graph and transaction as the
|
|
1642
|
+
% domain mutation. This keeps audit durability atomic without a second
|
|
1643
|
+
% persistency lock or a second authoritative database.
|
|
1644
|
+
kb_assert_audit_event(Operation, EntityId, Data) :-
|
|
1645
|
+
kb_graph(Graph),
|
|
1646
|
+
get_time(Timestamp),
|
|
1647
|
+
format_time(atom(TS), '%FT%T%:z', Timestamp),
|
|
1648
|
+
term_to_atom(Data, DataAtom),
|
|
1649
|
+
term_to_atom(EntityId, EntityAtom),
|
|
1650
|
+
term_to_atom(Operation, OperationAtom),
|
|
1651
|
+
next_audit_sequence(Sequence),
|
|
1652
|
+
format(atom(AuditURI), 'urn:kibi:audit/~16f-~w-~w-~w',
|
|
1653
|
+
[Timestamp, EntityId, Operation, Sequence]),
|
|
1654
|
+
with_kb_mutex((
|
|
1655
|
+
rdf_assert(AuditURI, 'urn:kibi:audit/operation',
|
|
1656
|
+
OperationAtom^^'http://www.w3.org/2001/XMLSchema#string', Graph),
|
|
1657
|
+
rdf_assert(AuditURI, 'urn:kibi:audit/timestamp',
|
|
1658
|
+
TS^^'http://www.w3.org/2001/XMLSchema#string', Graph),
|
|
1659
|
+
rdf_assert(AuditURI, 'urn:kibi:audit/entity',
|
|
1660
|
+
EntityAtom^^'http://www.w3.org/2001/XMLSchema#string', Graph),
|
|
1661
|
+
rdf_assert(AuditURI, 'urn:kibi:audit/data',
|
|
1662
|
+
DataAtom^^'http://www.w3.org/2001/XMLSchema#string', Graph)
|
|
1663
|
+
)).
|
|
1664
|
+
|
|
1665
|
+
initialize_journal_audit_sequence(GraphURI) :-
|
|
1666
|
+
aggregate_all(count,
|
|
1667
|
+
rdf(_, 'urn:kibi:audit/operation', _, GraphURI),
|
|
1668
|
+
Count),
|
|
1669
|
+
retractall(kb_audit_sequence(_)),
|
|
1670
|
+
assert(kb_audit_sequence(Count)).
|
|
1671
|
+
|
|
1672
|
+
next_audit_sequence(Sequence) :-
|
|
1673
|
+
( retract(kb_audit_sequence(Previous))
|
|
1674
|
+
-> Sequence is Previous + 1
|
|
1675
|
+
; Sequence = 1
|
|
1676
|
+
),
|
|
1677
|
+
assertz(kb_audit_sequence(Sequence)).
|
|
1678
|
+
|
|
1679
|
+
kb_assert_audit_event_with_timestamp(TS, Operation, EntityId, Data) :-
|
|
1680
|
+
kb_assert_audit_event_with_timestamp(TS, Operation, EntityId, Data, 0).
|
|
1681
|
+
|
|
1682
|
+
kb_assert_audit_event_with_timestamp(TS, Operation, EntityId, Data, Index) :-
|
|
1683
|
+
kb_graph(Graph),
|
|
1684
|
+
term_to_atom(Data, DataAtom),
|
|
1685
|
+
term_to_atom(EntityId, EntityAtom),
|
|
1686
|
+
term_to_atom(Operation, OperationAtom),
|
|
1687
|
+
format(atom(AuditURI), 'urn:kibi:audit/~w-~w-~w-~w',
|
|
1688
|
+
[TS, EntityId, Operation, Index]),
|
|
1689
|
+
with_kb_mutex((
|
|
1690
|
+
rdf_assert(AuditURI, 'urn:kibi:audit/operation',
|
|
1691
|
+
OperationAtom^^'http://www.w3.org/2001/XMLSchema#string', Graph),
|
|
1692
|
+
rdf_assert(AuditURI, 'urn:kibi:audit/timestamp',
|
|
1693
|
+
TS^^'http://www.w3.org/2001/XMLSchema#string', Graph),
|
|
1694
|
+
rdf_assert(AuditURI, 'urn:kibi:audit/entity',
|
|
1695
|
+
EntityAtom^^'http://www.w3.org/2001/XMLSchema#string', Graph),
|
|
1696
|
+
rdf_assert(AuditURI, 'urn:kibi:audit/data',
|
|
1697
|
+
DataAtom^^'http://www.w3.org/2001/XMLSchema#string', Graph)
|
|
1698
|
+
)).
|
|
1699
|
+
|
|
527
1700
|
%% validate_strict_lane_pairing(+RelType, +FromId, +ToId)
|
|
528
1701
|
% Validate that constrains/requires_property relationships target facts
|
|
529
1702
|
% with the correct fact_kind for strict-lane semantics.
|
|
@@ -600,6 +1773,7 @@ entity_id_to_uri(Id, URI) :-
|
|
|
600
1773
|
%% entity_uri_to_id(+URI, -Id)
|
|
601
1774
|
% Extract entity ID from canonical or legacy entity URIs.
|
|
602
1775
|
entity_uri_to_id(URI, Id) :-
|
|
1776
|
+
atom(URI),
|
|
603
1777
|
( kb_uri(BaseURI),
|
|
604
1778
|
atom_concat(BaseURI, 'entity/', Prefix),
|
|
605
1779
|
atom_concat(Prefix, Id, URI)
|
|
@@ -625,7 +1799,7 @@ store_property(EntityURI, Key, Value, Graph) :-
|
|
|
625
1799
|
% Key-aware: typed fact fields get specific XSD datatypes.
|
|
626
1800
|
value_to_literal(Key, Value, Literal) :-
|
|
627
1801
|
% Typed fact fields with specific XSD datatypes
|
|
628
|
-
( Key
|
|
1802
|
+
( integer_property_key(Key), integer(Value)
|
|
629
1803
|
-> Literal = Value^^'http://www.w3.org/2001/XMLSchema#integer'
|
|
630
1804
|
; Key == value_number, number(Value)
|
|
631
1805
|
-> Literal = Value^^'http://www.w3.org/2001/XMLSchema#decimal'
|
|
@@ -643,9 +1817,29 @@ value_to_literal(Key, Value, Literal) :-
|
|
|
643
1817
|
Literal = Str^^'http://www.w3.org/2001/XMLSchema#string'
|
|
644
1818
|
).
|
|
645
1819
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
1820
|
+
integer_property_key(Key) :-
|
|
1821
|
+
memberchk(Key, [
|
|
1822
|
+
value_int,
|
|
1823
|
+
predicate_arity,
|
|
1824
|
+
claim_span_start,
|
|
1825
|
+
claim_span_end,
|
|
1826
|
+
sourceLine,
|
|
1827
|
+
sourceColumn,
|
|
1828
|
+
sourceEndLine,
|
|
1829
|
+
sourceEndColumn
|
|
1830
|
+
]).
|
|
1831
|
+
|
|
1832
|
+
%% literal_to_value(+Key, +Literal, -Value)
|
|
1833
|
+
% Extract a property value from an RDF literal. JSON-backed fields must remain
|
|
1834
|
+
% intact: JSON arrays are also valid Prolog list syntax, but parsing them as
|
|
1835
|
+
% Prolog terms corrupts their object entries before the JSON decoder sees them.
|
|
1836
|
+
literal_to_value(semantic_inventory, Literal, Value) :-
|
|
1837
|
+
!,
|
|
1838
|
+
structured_literal_value(Literal, Value).
|
|
1839
|
+
literal_to_value(verification_receipts, Literal, Value) :-
|
|
1840
|
+
!,
|
|
1841
|
+
structured_literal_value(Literal, Value).
|
|
1842
|
+
literal_to_value(_Key, Literal, Value) :-
|
|
649
1843
|
( % Handle ^^/2 functor (RDF typed literal shorthand)
|
|
650
1844
|
Literal = ^^(StrVal, 'http://www.w3.org/2001/XMLSchema#string')
|
|
651
1845
|
-> ( % Preserve RDF typed literal functor for string values so callers
|
|
@@ -678,6 +1872,12 @@ literal_to_value(Literal, Value) :-
|
|
|
678
1872
|
; Value = Literal
|
|
679
1873
|
).
|
|
680
1874
|
|
|
1875
|
+
structured_literal_value(^^(Value, _), Value) :- !.
|
|
1876
|
+
structured_literal_value(literal(type(_, Value)), Value) :- !.
|
|
1877
|
+
structured_literal_value(literal(lang(_, Value)), Value) :- !.
|
|
1878
|
+
structured_literal_value(literal(Value), Value) :- !.
|
|
1879
|
+
structured_literal_value(Value, Value).
|
|
1880
|
+
|
|
681
1881
|
%% literal_to_atom(+Literal, -Atom)
|
|
682
1882
|
% Convert RDF literal to atom (for type field).
|
|
683
1883
|
literal_to_atom(Literal, Atom) :-
|
|
@@ -979,19 +2179,21 @@ current_req(Id) :-
|
|
|
979
2179
|
% semantic conflicts (same subject/property but incompatible values or polarities).
|
|
980
2180
|
% Checks in order of specificity: polarity conflicts, then value conflicts.
|
|
981
2181
|
contradicting_reqs(ReqA, ReqB, Reason) :-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
ReqA @< ReqB,
|
|
985
|
-
req_conflict(ReqA, ReqB, Reason).
|
|
2182
|
+
req_conflict_witness(ReqA, ReqB, Witness),
|
|
2183
|
+
Reason = Witness.reason.
|
|
986
2184
|
|
|
987
2185
|
%% ------------------------------------------------------------------
|
|
988
2186
|
%% Semantic Contradiction Helpers (Task 4)
|
|
989
2187
|
%% ------------------------------------------------------------------
|
|
990
2188
|
|
|
991
|
-
%%
|
|
992
|
-
%
|
|
993
|
-
%
|
|
994
|
-
|
|
2189
|
+
%% req_conflict_witness(+ReqA, +ReqB, -Witness)
|
|
2190
|
+
% Return exact, source-bound evidence for one proven strict or predicate
|
|
2191
|
+
% contradiction. This remains deliberately syntactic: differently shaped
|
|
2192
|
+
% terms are never treated as equivalent merely because their prose is similar.
|
|
2193
|
+
req_conflict_witness(ReqA, ReqB, Witness) :-
|
|
2194
|
+
current_req(ReqA),
|
|
2195
|
+
current_req(ReqB),
|
|
2196
|
+
ReqA @< ReqB,
|
|
995
2197
|
kb_relationship(constrains, ReqA, SubjectFactA),
|
|
996
2198
|
kb_relationship(constrains, ReqB, SubjectFactB),
|
|
997
2199
|
fact_subject_key(SubjectFactA, SubjectKey),
|
|
@@ -1005,8 +2207,23 @@ req_conflict(ReqA, ReqB, Reason) :-
|
|
|
1005
2207
|
OpB, ValTypeB, ValB, UnitB, ScopeB, PolarityB, Reason)
|
|
1006
2208
|
; property_conflict(SubjectKey, PropertyKey, OpA, ValTypeA, ValA, UnitA, PolarityA,
|
|
1007
2209
|
OpB, ValTypeB, ValB, UnitB, PolarityB, Reason)
|
|
1008
|
-
)
|
|
1009
|
-
|
|
2210
|
+
),
|
|
2211
|
+
property_conflict_side(ReqA, FactA, SubjectKey, PropertyKey, OpA, ValTypeA, ValA, UnitA, ScopeA, PolarityA, ValidFromA, ValidToA, Left),
|
|
2212
|
+
property_conflict_side(ReqB, FactB, SubjectKey, PropertyKey, OpB, ValTypeB, ValB, UnitB, ScopeB, PolarityB, ValidFromB, ValidToB, Right),
|
|
2213
|
+
Witness = _{
|
|
2214
|
+
kind: strict_property,
|
|
2215
|
+
status: contradiction,
|
|
2216
|
+
requirements: [ReqA, ReqB],
|
|
2217
|
+
reason: Reason,
|
|
2218
|
+
subjectKey: SubjectKey,
|
|
2219
|
+
propertyKey: PropertyKey,
|
|
2220
|
+
left: Left,
|
|
2221
|
+
right: Right
|
|
2222
|
+
}.
|
|
2223
|
+
req_conflict_witness(ReqA, ReqB, Witness) :-
|
|
2224
|
+
current_req(ReqA),
|
|
2225
|
+
current_req(ReqB),
|
|
2226
|
+
ReqA @< ReqB,
|
|
1010
2227
|
effective_req_predicate(ReqA, FactA, Namespace, Name, Args, PolarityA),
|
|
1011
2228
|
effective_req_predicate(ReqB, FactB, Namespace, Name, Args, PolarityB),
|
|
1012
2229
|
FactA \= FactB,
|
|
@@ -1016,6 +2233,86 @@ req_conflict(ReqA, ReqB, Reason) :-
|
|
|
1016
2233
|
string(Reason),
|
|
1017
2234
|
"Predicate conflict on ~w:~w(~w): ~w asserts ~w while ~w asserts ~w",
|
|
1018
2235
|
[Namespace, Name, ArgsText, ReqA, PolarityA, ReqB, PolarityB]
|
|
2236
|
+
),
|
|
2237
|
+
predicate_conflict_side(ReqA, FactA, Namespace, Name, Args, PolarityA, Left),
|
|
2238
|
+
predicate_conflict_side(ReqB, FactB, Namespace, Name, Args, PolarityB, Right),
|
|
2239
|
+
length(Args, Arity),
|
|
2240
|
+
findall(SchemaId, predicate_schema(SchemaId, Namespace, Name, Arity, _ArgumentNames, _ArgumentTypes), SchemaIds0),
|
|
2241
|
+
sort(SchemaIds0, SchemaIds),
|
|
2242
|
+
Witness = _{
|
|
2243
|
+
kind: predicate,
|
|
2244
|
+
status: contradiction,
|
|
2245
|
+
requirements: [ReqA, ReqB],
|
|
2246
|
+
reason: Reason,
|
|
2247
|
+
predicateNamespace: Namespace,
|
|
2248
|
+
predicateName: Name,
|
|
2249
|
+
predicateArgs: Args,
|
|
2250
|
+
schemaIds: SchemaIds,
|
|
2251
|
+
left: Left,
|
|
2252
|
+
right: Right
|
|
2253
|
+
}.
|
|
2254
|
+
|
|
2255
|
+
property_conflict_side(ReqId, FactId, SubjectKey, PropertyKey, Operator, ValueType, Value, Unit, Scope, Polarity, ValidFrom, ValidTo, Side) :-
|
|
2256
|
+
entity_evidence_source(ReqId, req, RequirementSource),
|
|
2257
|
+
fact_evidence_fields(FactId, FactSource, ClaimKey, ClaimText, _CanonicalKey),
|
|
2258
|
+
Side = _{
|
|
2259
|
+
requirementId: ReqId,
|
|
2260
|
+
requirementSource: RequirementSource,
|
|
2261
|
+
factId: FactId,
|
|
2262
|
+
factSource: FactSource,
|
|
2263
|
+
claimKey: ClaimKey,
|
|
2264
|
+
claimText: ClaimText,
|
|
2265
|
+
term: _{
|
|
2266
|
+
subjectKey: SubjectKey,
|
|
2267
|
+
propertyKey: PropertyKey,
|
|
2268
|
+
operator: Operator,
|
|
2269
|
+
valueType: ValueType,
|
|
2270
|
+
value: Value,
|
|
2271
|
+
unit: Unit,
|
|
2272
|
+
scope: Scope,
|
|
2273
|
+
polarity: Polarity,
|
|
2274
|
+
validFrom: ValidFrom,
|
|
2275
|
+
validTo: ValidTo
|
|
2276
|
+
}
|
|
2277
|
+
}.
|
|
2278
|
+
|
|
2279
|
+
predicate_conflict_side(ReqId, FactId, Namespace, Name, Args, Polarity, Side) :-
|
|
2280
|
+
entity_evidence_source(ReqId, req, RequirementSource),
|
|
2281
|
+
fact_evidence_fields(FactId, FactSource, ClaimKey, ClaimText, CanonicalKey),
|
|
2282
|
+
Side = _{
|
|
2283
|
+
requirementId: ReqId,
|
|
2284
|
+
requirementSource: RequirementSource,
|
|
2285
|
+
factId: FactId,
|
|
2286
|
+
factSource: FactSource,
|
|
2287
|
+
claimKey: ClaimKey,
|
|
2288
|
+
claimText: ClaimText,
|
|
2289
|
+
term: _{
|
|
2290
|
+
namespace: Namespace,
|
|
2291
|
+
name: Name,
|
|
2292
|
+
args: Args,
|
|
2293
|
+
polarity: Polarity,
|
|
2294
|
+
canonicalKey: CanonicalKey
|
|
2295
|
+
}
|
|
2296
|
+
}.
|
|
2297
|
+
|
|
2298
|
+
entity_evidence_source(EntityId, Type, Source) :-
|
|
2299
|
+
kb_entity(EntityId, Type, Props),
|
|
2300
|
+
(memberchk(source=RawSource, Props) -> evidence_term_atom(RawSource, Source) ; Source = '').
|
|
2301
|
+
|
|
2302
|
+
fact_evidence_fields(FactId, Source, ClaimKey, ClaimText, CanonicalKey) :-
|
|
2303
|
+
kb_entity(FactId, fact, Props),
|
|
2304
|
+
(memberchk(source=RawSource, Props) -> evidence_term_atom(RawSource, Source) ; Source = ''),
|
|
2305
|
+
(memberchk(claim_key=RawClaimKey, Props) -> evidence_term_atom(RawClaimKey, ClaimKey) ; ClaimKey = ''),
|
|
2306
|
+
(memberchk(claim_text=RawClaimText, Props) -> evidence_term_atom(RawClaimText, ClaimText) ; ClaimText = ''),
|
|
2307
|
+
(memberchk(canonical_key=RawCanonicalKey, Props) -> evidence_term_atom(RawCanonicalKey, CanonicalKey) ; CanonicalKey = '').
|
|
2308
|
+
|
|
2309
|
+
evidence_term_atom(Raw, Atom) :-
|
|
2310
|
+
unwrap_rdf_value(Raw, Value),
|
|
2311
|
+
( atom(Value)
|
|
2312
|
+
-> Atom = Value
|
|
2313
|
+
; string(Value)
|
|
2314
|
+
-> atom_string(Atom, Value)
|
|
2315
|
+
; term_string(Value, String), atom_string(Atom, String)
|
|
1019
2316
|
).
|
|
1020
2317
|
|
|
1021
2318
|
%% effective_req_predicate(+ReqId, -FactId, -Namespace, -Name, -Args, -Polarity)
|