kibi-core 0.9.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/src/kb.pl +1084 -24
- package/src/status.pl +42 -40
package/src/kb.pl
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
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,
|
|
@@ -12,6 +18,13 @@
|
|
|
12
18
|
kb_retract_entity/1,
|
|
13
19
|
kb_retract_entity/3,
|
|
14
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,
|
|
15
28
|
kb_entity/3,
|
|
16
29
|
kb_entities_by_source/2,
|
|
17
30
|
kb_assert_relationship/4,
|
|
@@ -53,9 +66,13 @@
|
|
|
53
66
|
]).
|
|
54
67
|
|
|
55
68
|
:- use_module(library(semweb/rdf11)).
|
|
69
|
+
:- use_module(library(semweb/rdf_persistency)).
|
|
56
70
|
:- use_module(library(persistency)).
|
|
57
71
|
:- use_module(library(thread)).
|
|
58
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]).
|
|
59
76
|
:- use_module(library(ordsets)).
|
|
60
77
|
:- use_module('../schema/entities.pl', [entity_type/1, entity_property/3, required_property/2]).
|
|
61
78
|
:- use_module('../schema/relationships.pl', [relationship_type/1, valid_relationship/3]).
|
|
@@ -80,10 +97,31 @@ kb_uri('urn-kibi:').
|
|
|
80
97
|
:- dynamic kb_audit_db/1.
|
|
81
98
|
:- dynamic kb_graph/1.
|
|
82
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.
|
|
83
119
|
:- dynamic entity/4. % Support legacy .pl file format (Type, Id, Title, Props)
|
|
84
120
|
|
|
85
121
|
%% kb_attach(+Directory)
|
|
86
|
-
% 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.
|
|
87
125
|
% Creates directory if it doesn't exist.
|
|
88
126
|
kb_attach(Directory) :-
|
|
89
127
|
( kb_attached(_)
|
|
@@ -95,6 +133,67 @@ kb_attach(Directory) :-
|
|
|
95
133
|
-> true
|
|
96
134
|
; make_directory_path(Directory)
|
|
97
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) :-
|
|
98
197
|
% Create RDF graph name from directory
|
|
99
198
|
atom_concat('file://', Directory, GraphURI),
|
|
100
199
|
% If a graph with this URI is already present, unload it to avoid duplicates.
|
|
@@ -122,10 +221,12 @@ kb_attach(Directory) :-
|
|
|
122
221
|
assert(kb_attached(Directory)),
|
|
123
222
|
assert(kb_audit_db(AuditLog)),
|
|
124
223
|
assert(kb_graph(GraphURI)),
|
|
224
|
+
assert(kb_storage_mode(legacy)),
|
|
125
225
|
assert(kb_attached_snapshot(SnapshotStamp)),
|
|
126
226
|
|
|
127
227
|
% Load legacy .pl entity files if present
|
|
128
|
-
load_kb_pl_files(Directory)
|
|
228
|
+
load_kb_pl_files(Directory),
|
|
229
|
+
kb_rebuild_indexes.
|
|
129
230
|
|
|
130
231
|
|
|
131
232
|
%% kb_detach
|
|
@@ -135,6 +236,11 @@ kb_attach(Directory) :-
|
|
|
135
236
|
kb_detach :-
|
|
136
237
|
( kb_attached(_Directory)
|
|
137
238
|
-> (
|
|
239
|
+
( kb_storage_mode(journaled)
|
|
240
|
+
-> catch(rdf_flush_journals([min_size(16384)]), _, true),
|
|
241
|
+
catch(rdf_detach_db, _, true)
|
|
242
|
+
; true
|
|
243
|
+
),
|
|
138
244
|
% Unload RDF graph from memory to prevent duplication on reattach
|
|
139
245
|
( kb_graph(GraphURI)
|
|
140
246
|
-> rdf_unload_graph(GraphURI)
|
|
@@ -148,7 +254,24 @@ kb_detach :-
|
|
|
148
254
|
retractall(kb_attached(_)),
|
|
149
255
|
retractall(kb_audit_db(_)),
|
|
150
256
|
retractall(kb_graph(_)),
|
|
151
|
-
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(_))
|
|
152
275
|
)
|
|
153
276
|
; true
|
|
154
277
|
).
|
|
@@ -157,7 +280,9 @@ kb_detach :-
|
|
|
157
280
|
% Save RDF graph and sync audit log to disk
|
|
158
281
|
% implements REQ-009
|
|
159
282
|
kb_save :-
|
|
160
|
-
(
|
|
283
|
+
( kb_storage_mode(journaled)
|
|
284
|
+
-> kb_save_journaled
|
|
285
|
+
; kb_attached(Directory)
|
|
161
286
|
-> with_kb_mutex(with_kb_file_lock(Directory, kb_save_locked(Directory)))
|
|
162
287
|
; true
|
|
163
288
|
).
|
|
@@ -171,6 +296,12 @@ with_kb_file_lock(Directory, Goal) :-
|
|
|
171
296
|
).
|
|
172
297
|
|
|
173
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) :-
|
|
174
305
|
atom_concat(Directory, '/kb.rdf', DataFile),
|
|
175
306
|
temp_rdf_file(Directory, TempFile),
|
|
176
307
|
catch(
|
|
@@ -196,6 +327,486 @@ kb_save_locked(Directory) :-
|
|
|
196
327
|
)
|
|
197
328
|
).
|
|
198
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
|
+
|
|
199
810
|
temp_rdf_file(Directory, TempFile) :-
|
|
200
811
|
get_time(Timestamp),
|
|
201
812
|
Millis is floor(Timestamp * 1000),
|
|
@@ -245,6 +856,22 @@ ensure_snapshot_current(DataFile) :-
|
|
|
245
856
|
with_kb_mutex(Goal) :-
|
|
246
857
|
with_mutex(kb_lock, Goal).
|
|
247
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
|
+
|
|
248
875
|
%% load_kb_pl_files(+Directory)
|
|
249
876
|
% Load legacy .pl entity files from the KB directory.
|
|
250
877
|
% These files use entity/4 format: entity(Type, Id, Title, Props).
|
|
@@ -275,8 +902,15 @@ kb_assert_entity(Type, Props) :-
|
|
|
275
902
|
-> ChangeKind = updated
|
|
276
903
|
; ChangeKind = created
|
|
277
904
|
),
|
|
278
|
-
|
|
279
|
-
|
|
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
|
+
).
|
|
280
914
|
|
|
281
915
|
%% kb_assert_entity_no_audit(+Type, +Properties)
|
|
282
916
|
% Assert an entity RDF payload without recording audit side effects.
|
|
@@ -313,7 +947,8 @@ kb_assert_entity_no_audit(Type, Props) :-
|
|
|
313
947
|
member(Key=Value, Props),
|
|
314
948
|
store_property(EntityURI, Key, Value, Graph)
|
|
315
949
|
)
|
|
316
|
-
))
|
|
950
|
+
)),
|
|
951
|
+
kb_refresh_entity_index(Id).
|
|
317
952
|
|
|
318
953
|
%% kb_commit_upsert(+Type, +Properties, +Relationships, +SkipContradiction, -ChangeKind)
|
|
319
954
|
% Commit an entity upsert, its relationships, audit entries, and RDF snapshot
|
|
@@ -322,6 +957,33 @@ kb_assert_entity_no_audit(Type, Props) :-
|
|
|
322
957
|
% Relationships use rel(Type, FromId, ToId, Metadata) terms. Metadata is
|
|
323
958
|
% retained for the caller's serialization contract; the audit schema records
|
|
324
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
|
+
)).
|
|
325
987
|
kb_commit_upsert(Type, Props, Relationships, SkipContradiction, ChangeKind) :-
|
|
326
988
|
memberchk(id=Id, Props),
|
|
327
989
|
memberchk(SkipContradiction, [true, false]),
|
|
@@ -423,19 +1085,91 @@ kb_environment_value(Name, Value) :-
|
|
|
423
1085
|
% Used by projectStagedEntities to clear stale relationships before re-asserting.
|
|
424
1086
|
kb_retract_entity_relationships(Id) :-
|
|
425
1087
|
kb_graph(Graph),
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
(
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
+
)
|
|
434
1163
|
)
|
|
435
|
-
)
|
|
1164
|
+
).
|
|
436
1165
|
|
|
437
1166
|
%% kb_log_entity_upsert(+ChangeKind, +Type, +Properties)
|
|
438
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]).
|
|
439
1173
|
kb_log_entity_upsert(ChangeKind, Type, Props) :-
|
|
440
1174
|
memberchk(id=Id, Props),
|
|
441
1175
|
memberchk(ChangeKind, [created, updated]),
|
|
@@ -457,6 +1191,26 @@ kb_retract_entity(Id) :-
|
|
|
457
1191
|
|
|
458
1192
|
%% kb_retract_entity(+Id, +Type, +AuditProps)
|
|
459
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
|
+
))).
|
|
460
1214
|
kb_retract_entity(Id, Type, AuditProps) :-
|
|
461
1215
|
kb_graph(Graph),
|
|
462
1216
|
with_kb_mutex((
|
|
@@ -467,7 +1221,8 @@ kb_retract_entity(Id, Type, AuditProps) :-
|
|
|
467
1221
|
% Log to audit
|
|
468
1222
|
get_time(Timestamp),
|
|
469
1223
|
format_time(atom(TS), '%FT%T%:z', Timestamp),
|
|
470
|
-
assert_changeset(TS, delete, Id, Type-AuditProps)
|
|
1224
|
+
assert_changeset(TS, delete, Id, Type-AuditProps),
|
|
1225
|
+
kb_remove_entity_index(Id)
|
|
471
1226
|
)).
|
|
472
1227
|
|
|
473
1228
|
entity_delete_audit_props(Id, Props, AuditProps) :-
|
|
@@ -495,6 +1250,20 @@ audit_property_value(RawValue, Value) :-
|
|
|
495
1250
|
% Query entities from the KB.
|
|
496
1251
|
% Properties is unified with a list of Key=Value pairs.
|
|
497
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) :-
|
|
498
1267
|
kb_graph(Graph),
|
|
499
1268
|
% Find entity by pattern - use unquoted namespace term kb:type
|
|
500
1269
|
( var(Id)
|
|
@@ -516,10 +1285,111 @@ kb_entity(Id, Type, Props) :-
|
|
|
516
1285
|
), Props).
|
|
517
1286
|
|
|
518
1287
|
% Fallback: read from legacy entity/4 facts loaded from .pl files
|
|
519
|
-
|
|
1288
|
+
kb_entity_raw(Id, Type, Props) :-
|
|
520
1289
|
entity(Type, Id, _Title, PropList),
|
|
521
1290
|
convert_legacy_props(PropList, Props).
|
|
522
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
|
+
|
|
523
1393
|
% Convert legacy property list format to Key=Value pairs
|
|
524
1394
|
convert_legacy_props([], []).
|
|
525
1395
|
convert_legacy_props([Prop|Rest], [Key=Value|OutRest]) :-
|
|
@@ -536,13 +1406,130 @@ convert_legacy_prop(Prop, Prop, true).
|
|
|
536
1406
|
%% kb_entities_by_source(+SourcePath, -Ids)
|
|
537
1407
|
% Returns all entity IDs whose source property matches SourcePath (substring match).
|
|
538
1408
|
kb_entities_by_source(SourcePath, Ids) :-
|
|
1409
|
+
kb_ensure_indexes,
|
|
1410
|
+
source_value_atom(SourcePath, SourceQuery),
|
|
539
1411
|
findall(Id,
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
RawIds),
|
|
1412
|
+
( kb_index_source(SourceAtom, Id),
|
|
1413
|
+
sub_atom(SourceAtom, _, _, _, SourceQuery) ),
|
|
1414
|
+
RawIds),
|
|
544
1415
|
sort(RawIds, Ids).
|
|
545
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),
|
|
1423
|
+
sort(RawIds, Ids).
|
|
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
|
+
|
|
546
1533
|
entity_source_atom(Props, SourceAtom) :-
|
|
547
1534
|
( memberchk(sourceFile=RawSourceFile, Props)
|
|
548
1535
|
-> source_value_atom(RawSourceFile, SourceAtom)
|
|
@@ -563,8 +1550,15 @@ source_value_atom(Value, Atom) :-
|
|
|
563
1550
|
%% kb_assert_relationship(+Type, +From, +To, +Metadata)
|
|
564
1551
|
% Assert a relationship between two entities with validation.
|
|
565
1552
|
kb_assert_relationship(RelType, FromId, ToId, Metadata) :-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
+
).
|
|
568
1562
|
|
|
569
1563
|
%% kb_assert_relationship_no_audit(+Type, +From, +To, +Metadata)
|
|
570
1564
|
% Assert a relationship RDF payload without recording audit side effects.
|
|
@@ -630,6 +1624,12 @@ mixed_symbol_role(covered_by, SymbolId) :-
|
|
|
630
1624
|
|
|
631
1625
|
%% kb_log_relationship_upsert(+Type, +From, +To, +Metadata)
|
|
632
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]).
|
|
633
1633
|
kb_log_relationship_upsert(RelType, FromId, ToId, _Metadata) :-
|
|
634
1634
|
with_kb_mutex((
|
|
635
1635
|
get_time(Timestamp),
|
|
@@ -638,6 +1638,65 @@ kb_log_relationship_upsert(RelType, FromId, ToId, _Metadata) :-
|
|
|
638
1638
|
assert_changeset(TS, upsert_rel, RelId, RelType-[from=FromId, to=ToId])
|
|
639
1639
|
)).
|
|
640
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
|
+
|
|
641
1700
|
%% validate_strict_lane_pairing(+RelType, +FromId, +ToId)
|
|
642
1701
|
% Validate that constrains/requires_property relationships target facts
|
|
643
1702
|
% with the correct fact_kind for strict-lane semantics.
|
|
@@ -714,6 +1773,7 @@ entity_id_to_uri(Id, URI) :-
|
|
|
714
1773
|
%% entity_uri_to_id(+URI, -Id)
|
|
715
1774
|
% Extract entity ID from canonical or legacy entity URIs.
|
|
716
1775
|
entity_uri_to_id(URI, Id) :-
|
|
1776
|
+
atom(URI),
|
|
717
1777
|
( kb_uri(BaseURI),
|
|
718
1778
|
atom_concat(BaseURI, 'entity/', Prefix),
|
|
719
1779
|
atom_concat(Prefix, Id, URI)
|