kibi-core 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/kb.pl CHANGED
@@ -7,6 +7,7 @@
7
7
  with_kb_mutex/1,
8
8
  kb_assert_entity/2,
9
9
  kb_assert_entity_no_audit/2,
10
+ kb_commit_upsert/5,
10
11
  kb_log_entity_upsert/3,
11
12
  kb_retract_entity/1,
12
13
  kb_retract_entity/3,
@@ -43,6 +44,7 @@
43
44
  predicate_schema/6,
44
45
  predicate_fact/5,
45
46
  contradicting_reqs/3,
47
+ req_conflict_witness/3,
46
48
  check_req_contradiction/1,
47
49
  normalize_term_atom/2,
48
50
  normalize_term_atom_list/2,
@@ -111,7 +113,10 @@ kb_attach(Directory) :-
111
113
  atom_concat(Directory, '/audit.log', AuditLog),
112
114
  ( db_attached(AuditLog)
113
115
  -> true % Already attached
114
- ; db_attach(AuditLog, [])
116
+ ; % Close the journal stream after each write. The default `flush`
117
+ % mode retains an exclusive write lock for the lifetime of a
118
+ % long-running MCP process and blocks writers in other runtimes.
119
+ db_attach(AuditLog, [sync(close)])
115
120
  ),
116
121
  % Track attachment state
117
122
  assert(kb_attached(Directory)),
@@ -171,12 +176,18 @@ kb_save_locked(Directory) :-
171
176
  catch(
172
177
  (
173
178
  ensure_snapshot_current(DataFile),
179
+ kb_stage(snapshot_save),
174
180
  save_rdf_snapshot(TempFile),
181
+ kb_stage(audit_sync),
182
+ sync_audit_log,
183
+ % Keep the durable RDF rename after audit synchronization. The
184
+ % temporary snapshot can be discarded if an audit-stage failure
185
+ % occurs, so a failed commit cannot publish new RDF state.
186
+ kb_stage(snapshot_save),
175
187
  rename_file(TempFile, DataFile),
176
188
  current_data_stamp(DataFile, UpdatedStamp),
177
189
  retractall(kb_attached_snapshot(_)),
178
- assert(kb_attached_snapshot(UpdatedStamp)),
179
- sync_audit_log
190
+ assert(kb_attached_snapshot(UpdatedStamp))
180
191
  ),
181
192
  Error,
182
193
  (
@@ -304,6 +315,109 @@ kb_assert_entity_no_audit(Type, Props) :-
304
315
  )
305
316
  )).
306
317
 
318
+ %% kb_commit_upsert(+Type, +Properties, +Relationships, +SkipContradiction, -ChangeKind)
319
+ % Commit an entity upsert, its relationships, audit entries, and RDF snapshot
320
+ % as one bounded operation. The branch lock is held before any RDF mutation,
321
+ % so concurrent runtimes cannot observe or publish an intermediate upsert.
322
+ % Relationships use rel(Type, FromId, ToId, Metadata) terms. Metadata is
323
+ % retained for the caller's serialization contract; the audit schema records
324
+ % only the relationship endpoints and type, as before.
325
+ kb_commit_upsert(Type, Props, Relationships, SkipContradiction, ChangeKind) :-
326
+ memberchk(id=Id, Props),
327
+ memberchk(SkipContradiction, [true, false]),
328
+ kb_attached(Directory),
329
+ atom_concat(Directory, '/kb.rdf', DataFile),
330
+ kb_stage(runtime),
331
+ kb_runtime_diagnostic,
332
+ % Emit the lock marker before waiting so a timeout identifies a branch
333
+ % lock held by another runtime instead of reporting only the prior stage.
334
+ kb_stage(lock),
335
+ with_kb_file_lock(Directory, (
336
+ audit_store_writable,
337
+ ensure_snapshot_current(DataFile),
338
+ upsert_change_kind(Id, ChangeKind),
339
+ rdf_transaction((
340
+ kb_stage(rdf_mutation),
341
+ kb_assert_entity_no_audit(Type, Props),
342
+ kb_commit_relationships_no_audit(Relationships),
343
+ kb_maybe_check_req_contradiction(Type, Id, SkipContradiction),
344
+ kb_stage(entity_audit),
345
+ kb_log_entity_upsert(ChangeKind, Type, Props),
346
+ kb_stage(relationship_audit),
347
+ kb_commit_relationship_audits(Relationships),
348
+ kb_save_locked(Directory)
349
+ ))
350
+ )).
351
+
352
+ upsert_change_kind(Id, updated) :-
353
+ once(kb_entity(Id, _, _)),
354
+ !.
355
+ upsert_change_kind(_, created).
356
+
357
+ kb_commit_relationships_no_audit([]).
358
+ kb_commit_relationships_no_audit([
359
+ rel(RelType, FromId, ToId, Metadata)|Rest
360
+ ]) :-
361
+ kb_assert_relationship_no_audit(RelType, FromId, ToId, Metadata),
362
+ kb_commit_relationships_no_audit(Rest).
363
+
364
+ kb_maybe_check_req_contradiction(req, Id, false) :-
365
+ !,
366
+ kb_stage(contradiction_check),
367
+ check_req_contradiction(Id).
368
+ kb_maybe_check_req_contradiction(_, _, _).
369
+
370
+ kb_commit_relationship_audits([]).
371
+ kb_commit_relationship_audits([
372
+ rel(RelType, FromId, ToId, _Metadata)|Rest
373
+ ]) :-
374
+ kb_log_relationship_upsert(RelType, FromId, ToId, []),
375
+ kb_commit_relationship_audits(Rest).
376
+
377
+ % Probe the existing journal without waiting. A process running code from
378
+ % before the sync(close) fix may still retain the journal's write lock; fail
379
+ % before touching RDF so callers receive a safe, retryable diagnostic.
380
+ audit_store_writable :-
381
+ kb_audit_db(AuditLog),
382
+ ( exists_file(AuditLog)
383
+ -> catch(
384
+ setup_call_cleanup(
385
+ open(AuditLog, append, Stream,
386
+ [lock(write), wait(false), encoding(utf8)]),
387
+ true,
388
+ close(Stream)
389
+ ),
390
+ Error,
391
+ throw(error(permission_error(lock, audit_log, AuditLog),
392
+ context(kb_commit_upsert/5, Error)))
393
+ )
394
+ ; true
395
+ ).
396
+
397
+ % Reserved stderr markers are consumed by PrologProcess diagnostics and never
398
+ % form part of the JSON result. They make timeout errors stage-specific even
399
+ % when the child is blocked inside a filesystem primitive.
400
+ kb_stage(Stage) :-
401
+ format(user_error, '__KIBI_STAGE__:~w~n', [Stage]),
402
+ flush_output(user_error).
403
+
404
+ kb_runtime_diagnostic :-
405
+ ( current_prolog_flag(pid, Pid) -> true ; Pid = unknown ),
406
+ ( kb_audit_db(AuditLog) -> true ; AuditLog = unknown ),
407
+ kb_environment_value('KIBI_RUNTIME_NAME', RuntimeName),
408
+ kb_environment_value('KIBI_RUNTIME_VERSION', RuntimeVersion),
409
+ kb_environment_value('KIBI_PACKAGE_VERSIONS', PackageVersions),
410
+ format(user_error,
411
+ '__KIBI_RUNTIME__:pid=~w;audit=~w;runtime=~w@~w;packages=~w~n',
412
+ [Pid, AuditLog, RuntimeName, RuntimeVersion, PackageVersions]),
413
+ flush_output(user_error).
414
+
415
+ kb_environment_value(Name, Value) :-
416
+ ( getenv(Name, Value)
417
+ -> true
418
+ ; Value = unknown
419
+ ).
420
+
307
421
  %% kb_retract_entity_relationships(+Id)
308
422
  % Remove all relationship triples for an entity, preserving property triples.
309
423
  % Used by projectStagedEntities to clear stale relationships before re-asserting.
@@ -358,7 +472,7 @@ kb_retract_entity(Id, Type, AuditProps) :-
358
472
 
359
473
  entity_delete_audit_props(Id, Props, AuditProps) :-
360
474
  findall(Key=Value,
361
- ( member(Key, [title, source, text_ref]),
475
+ ( member(Key, [title, source, text_ref, semantic_text]),
362
476
  memberchk(Key=RawValue, Props),
363
477
  audit_property_value(RawValue, Value)
364
478
  ),
@@ -398,7 +512,7 @@ kb_entity(Id, Type, Props) :-
398
512
  atom_concat(BaseURI, type, TypeURI),
399
513
  PropURI \= TypeURI,
400
514
  uri_to_key(PropURI, Key),
401
- literal_to_value(ValueLiteral, Value)
515
+ literal_to_value(Key, ValueLiteral, Value)
402
516
  ), Props).
403
517
 
404
518
  % Fallback: read from legacy entity/4 facts loaded from .pl files
@@ -625,7 +739,7 @@ store_property(EntityURI, Key, Value, Graph) :-
625
739
  % Key-aware: typed fact fields get specific XSD datatypes.
626
740
  value_to_literal(Key, Value, Literal) :-
627
741
  % Typed fact fields with specific XSD datatypes
628
- ( Key == value_int, integer(Value)
742
+ ( integer_property_key(Key), integer(Value)
629
743
  -> Literal = Value^^'http://www.w3.org/2001/XMLSchema#integer'
630
744
  ; Key == value_number, number(Value)
631
745
  -> Literal = Value^^'http://www.w3.org/2001/XMLSchema#decimal'
@@ -643,9 +757,29 @@ value_to_literal(Key, Value, Literal) :-
643
757
  Literal = Str^^'http://www.w3.org/2001/XMLSchema#string'
644
758
  ).
645
759
 
646
- %% literal_to_value(+Literal, -Value)
647
- % Extract value from RDF literal, parse list syntax back to Prolog lists.
648
- literal_to_value(Literal, Value) :-
760
+ integer_property_key(Key) :-
761
+ memberchk(Key, [
762
+ value_int,
763
+ predicate_arity,
764
+ claim_span_start,
765
+ claim_span_end,
766
+ sourceLine,
767
+ sourceColumn,
768
+ sourceEndLine,
769
+ sourceEndColumn
770
+ ]).
771
+
772
+ %% literal_to_value(+Key, +Literal, -Value)
773
+ % Extract a property value from an RDF literal. JSON-backed fields must remain
774
+ % intact: JSON arrays are also valid Prolog list syntax, but parsing them as
775
+ % Prolog terms corrupts their object entries before the JSON decoder sees them.
776
+ literal_to_value(semantic_inventory, Literal, Value) :-
777
+ !,
778
+ structured_literal_value(Literal, Value).
779
+ literal_to_value(verification_receipts, Literal, Value) :-
780
+ !,
781
+ structured_literal_value(Literal, Value).
782
+ literal_to_value(_Key, Literal, Value) :-
649
783
  ( % Handle ^^/2 functor (RDF typed literal shorthand)
650
784
  Literal = ^^(StrVal, 'http://www.w3.org/2001/XMLSchema#string')
651
785
  -> ( % Preserve RDF typed literal functor for string values so callers
@@ -678,6 +812,12 @@ literal_to_value(Literal, Value) :-
678
812
  ; Value = Literal
679
813
  ).
680
814
 
815
+ structured_literal_value(^^(Value, _), Value) :- !.
816
+ structured_literal_value(literal(type(_, Value)), Value) :- !.
817
+ structured_literal_value(literal(lang(_, Value)), Value) :- !.
818
+ structured_literal_value(literal(Value), Value) :- !.
819
+ structured_literal_value(Value, Value).
820
+
681
821
  %% literal_to_atom(+Literal, -Atom)
682
822
  % Convert RDF literal to atom (for type field).
683
823
  literal_to_atom(Literal, Atom) :-
@@ -979,19 +1119,21 @@ current_req(Id) :-
979
1119
  % semantic conflicts (same subject/property but incompatible values or polarities).
980
1120
  % Checks in order of specificity: polarity conflicts, then value conflicts.
981
1121
  contradicting_reqs(ReqA, ReqB, Reason) :-
982
- current_req(ReqA),
983
- current_req(ReqB),
984
- ReqA @< ReqB,
985
- req_conflict(ReqA, ReqB, Reason).
1122
+ req_conflict_witness(ReqA, ReqB, Witness),
1123
+ Reason = Witness.reason.
986
1124
 
987
1125
  %% ------------------------------------------------------------------
988
1126
  %% Semantic Contradiction Helpers (Task 4)
989
1127
  %% ------------------------------------------------------------------
990
1128
 
991
- %% req_conflict(+ReqA, +ReqB, -Reason)
992
- % Detects conflicts between requirements via shared subject facts and
993
- % incompatible property_value facts linked through requires_property.
994
- req_conflict(ReqA, ReqB, Reason) :-
1129
+ %% req_conflict_witness(+ReqA, +ReqB, -Witness)
1130
+ % Return exact, source-bound evidence for one proven strict or predicate
1131
+ % contradiction. This remains deliberately syntactic: differently shaped
1132
+ % terms are never treated as equivalent merely because their prose is similar.
1133
+ req_conflict_witness(ReqA, ReqB, Witness) :-
1134
+ current_req(ReqA),
1135
+ current_req(ReqB),
1136
+ ReqA @< ReqB,
995
1137
  kb_relationship(constrains, ReqA, SubjectFactA),
996
1138
  kb_relationship(constrains, ReqB, SubjectFactB),
997
1139
  fact_subject_key(SubjectFactA, SubjectKey),
@@ -1005,8 +1147,23 @@ req_conflict(ReqA, ReqB, Reason) :-
1005
1147
  OpB, ValTypeB, ValB, UnitB, ScopeB, PolarityB, Reason)
1006
1148
  ; property_conflict(SubjectKey, PropertyKey, OpA, ValTypeA, ValA, UnitA, PolarityA,
1007
1149
  OpB, ValTypeB, ValB, UnitB, PolarityB, Reason)
1008
- ).
1009
- req_conflict(ReqA, ReqB, Reason) :-
1150
+ ),
1151
+ property_conflict_side(ReqA, FactA, SubjectKey, PropertyKey, OpA, ValTypeA, ValA, UnitA, ScopeA, PolarityA, ValidFromA, ValidToA, Left),
1152
+ property_conflict_side(ReqB, FactB, SubjectKey, PropertyKey, OpB, ValTypeB, ValB, UnitB, ScopeB, PolarityB, ValidFromB, ValidToB, Right),
1153
+ Witness = _{
1154
+ kind: strict_property,
1155
+ status: contradiction,
1156
+ requirements: [ReqA, ReqB],
1157
+ reason: Reason,
1158
+ subjectKey: SubjectKey,
1159
+ propertyKey: PropertyKey,
1160
+ left: Left,
1161
+ right: Right
1162
+ }.
1163
+ req_conflict_witness(ReqA, ReqB, Witness) :-
1164
+ current_req(ReqA),
1165
+ current_req(ReqB),
1166
+ ReqA @< ReqB,
1010
1167
  effective_req_predicate(ReqA, FactA, Namespace, Name, Args, PolarityA),
1011
1168
  effective_req_predicate(ReqB, FactB, Namespace, Name, Args, PolarityB),
1012
1169
  FactA \= FactB,
@@ -1016,6 +1173,86 @@ req_conflict(ReqA, ReqB, Reason) :-
1016
1173
  string(Reason),
1017
1174
  "Predicate conflict on ~w:~w(~w): ~w asserts ~w while ~w asserts ~w",
1018
1175
  [Namespace, Name, ArgsText, ReqA, PolarityA, ReqB, PolarityB]
1176
+ ),
1177
+ predicate_conflict_side(ReqA, FactA, Namespace, Name, Args, PolarityA, Left),
1178
+ predicate_conflict_side(ReqB, FactB, Namespace, Name, Args, PolarityB, Right),
1179
+ length(Args, Arity),
1180
+ findall(SchemaId, predicate_schema(SchemaId, Namespace, Name, Arity, _ArgumentNames, _ArgumentTypes), SchemaIds0),
1181
+ sort(SchemaIds0, SchemaIds),
1182
+ Witness = _{
1183
+ kind: predicate,
1184
+ status: contradiction,
1185
+ requirements: [ReqA, ReqB],
1186
+ reason: Reason,
1187
+ predicateNamespace: Namespace,
1188
+ predicateName: Name,
1189
+ predicateArgs: Args,
1190
+ schemaIds: SchemaIds,
1191
+ left: Left,
1192
+ right: Right
1193
+ }.
1194
+
1195
+ property_conflict_side(ReqId, FactId, SubjectKey, PropertyKey, Operator, ValueType, Value, Unit, Scope, Polarity, ValidFrom, ValidTo, Side) :-
1196
+ entity_evidence_source(ReqId, req, RequirementSource),
1197
+ fact_evidence_fields(FactId, FactSource, ClaimKey, ClaimText, _CanonicalKey),
1198
+ Side = _{
1199
+ requirementId: ReqId,
1200
+ requirementSource: RequirementSource,
1201
+ factId: FactId,
1202
+ factSource: FactSource,
1203
+ claimKey: ClaimKey,
1204
+ claimText: ClaimText,
1205
+ term: _{
1206
+ subjectKey: SubjectKey,
1207
+ propertyKey: PropertyKey,
1208
+ operator: Operator,
1209
+ valueType: ValueType,
1210
+ value: Value,
1211
+ unit: Unit,
1212
+ scope: Scope,
1213
+ polarity: Polarity,
1214
+ validFrom: ValidFrom,
1215
+ validTo: ValidTo
1216
+ }
1217
+ }.
1218
+
1219
+ predicate_conflict_side(ReqId, FactId, Namespace, Name, Args, Polarity, Side) :-
1220
+ entity_evidence_source(ReqId, req, RequirementSource),
1221
+ fact_evidence_fields(FactId, FactSource, ClaimKey, ClaimText, CanonicalKey),
1222
+ Side = _{
1223
+ requirementId: ReqId,
1224
+ requirementSource: RequirementSource,
1225
+ factId: FactId,
1226
+ factSource: FactSource,
1227
+ claimKey: ClaimKey,
1228
+ claimText: ClaimText,
1229
+ term: _{
1230
+ namespace: Namespace,
1231
+ name: Name,
1232
+ args: Args,
1233
+ polarity: Polarity,
1234
+ canonicalKey: CanonicalKey
1235
+ }
1236
+ }.
1237
+
1238
+ entity_evidence_source(EntityId, Type, Source) :-
1239
+ kb_entity(EntityId, Type, Props),
1240
+ (memberchk(source=RawSource, Props) -> evidence_term_atom(RawSource, Source) ; Source = '').
1241
+
1242
+ fact_evidence_fields(FactId, Source, ClaimKey, ClaimText, CanonicalKey) :-
1243
+ kb_entity(FactId, fact, Props),
1244
+ (memberchk(source=RawSource, Props) -> evidence_term_atom(RawSource, Source) ; Source = ''),
1245
+ (memberchk(claim_key=RawClaimKey, Props) -> evidence_term_atom(RawClaimKey, ClaimKey) ; ClaimKey = ''),
1246
+ (memberchk(claim_text=RawClaimText, Props) -> evidence_term_atom(RawClaimText, ClaimText) ; ClaimText = ''),
1247
+ (memberchk(canonical_key=RawCanonicalKey, Props) -> evidence_term_atom(RawCanonicalKey, CanonicalKey) ; CanonicalKey = '').
1248
+
1249
+ evidence_term_atom(Raw, Atom) :-
1250
+ unwrap_rdf_value(Raw, Value),
1251
+ ( atom(Value)
1252
+ -> Atom = Value
1253
+ ; string(Value)
1254
+ -> atom_string(Atom, Value)
1255
+ ; term_string(Value, String), atom_string(Atom, String)
1019
1256
  ).
1020
1257
 
1021
1258
  %% effective_req_predicate(+ReqId, -FactId, -Namespace, -Name, -Args, -Polarity)