kibi-core 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-core",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "private": false,
5
5
  "description": "Core Prolog modules and RDF graph logic for Kibi",
6
6
  "type": "module",
@@ -9,7 +9,10 @@
9
9
  "bun": ">=1.0"
10
10
  },
11
11
  "main": "./src/kb.pl",
12
- "files": ["src/**/*.pl", "schema/**/*.pl"],
12
+ "files": [
13
+ "src/**/*.pl",
14
+ "schema/**/*.pl"
15
+ ],
13
16
  "license": "AGPL-3.0-or-later",
14
17
  "author": "Piotr Franczyk",
15
18
  "repository": {
package/src/checks.pl CHANGED
@@ -15,7 +15,9 @@
15
15
  check_deprecated_adrs/1, % Returns list of deprecated ADR violations
16
16
  check_domain_contradictions/1, % Returns list of contradiction violations
17
17
  check_strict_fact_shape/1, % Returns list of malformed strict fact violations
18
- run_checks_json/0 % Entry point for JSON output
18
+ check_strict_req_fact_pairing/1,% Returns list of malformed strict req/fact pairing violations
19
+ run_checks_json/0, % Entry point for JSON output
20
+ violation_id_text/2 % Extract text from entity ID term (exported for testing)
19
21
  ]).
20
22
 
21
23
  :- use_module(library(http/json)).
@@ -47,6 +49,7 @@ check_all(ViolationsDict) :-
47
49
  check_deprecated_adrs(DeprecatedADRs),
48
50
  check_domain_contradictions(Contradictions),
49
51
  check_strict_fact_shape(StrictFactShape),
52
+ check_strict_req_fact_pairing(StrictReqFactPairing),
50
53
  ViolationsDict = _{
51
54
  must_priority_coverage: MustPriority,
52
55
  symbol_coverage: SymbolCoverage,
@@ -56,7 +59,8 @@ check_all(ViolationsDict) :-
56
59
  required_fields: RequiredFields,
57
60
  deprecated_adr_no_successor: DeprecatedADRs,
58
61
  domain_contradictions: Contradictions,
59
- strict_fact_shape: StrictFactShape
62
+ strict_fact_shape: StrictFactShape,
63
+ strict_req_fact_pairing: StrictReqFactPairing
60
64
  }.
61
65
 
62
66
  %% check_must_priority_coverage(-Violations)
@@ -412,7 +416,8 @@ property_value_shape_error(Props, "Property value fact has multiple value fields
412
416
  length(Fields, Count),
413
417
  Count > 1.
414
418
  property_value_shape_error(Props, "Property value fact value_type does not match value field") :-
415
- memberchk(value_type=VT, Props),
419
+ memberchk(value_type=RawVT, Props),
420
+ normalize_term_atom(RawVT, VT),
416
421
  \+ value_type_matches_field(VT, Props).
417
422
 
418
423
  % is_value_field(+Field)
@@ -445,6 +450,138 @@ check_domain_contradictions(Violations) :-
445
450
  Violations
446
451
  ).
447
452
 
453
+ %% check_strict_req_fact_pairing(-Violations)
454
+ % Finds current requirements attempting strict-lane modeling with incomplete
455
+ % subject/property pairing or wrong-lane fact targets.
456
+ check_strict_req_fact_pairing(Violations) :-
457
+ findall(
458
+ Violation,
459
+ strict_req_fact_pairing_violation(Violation),
460
+ Violations0
461
+ ),
462
+ sort(Violations0, Violations).
463
+
464
+ strict_req_fact_pairing_violation(violation(
465
+ 'strict-req-fact-pairing',
466
+ ReqId,
467
+ Description,
468
+ Suggestion,
469
+ Source
470
+ )) :-
471
+ strict_req_fact_pairing_issue(ReqId, Description, Suggestion),
472
+ violation_source(ReqId, req, Source).
473
+
474
+ strict_req_fact_pairing_issue(
475
+ ReqId,
476
+ Description,
477
+ "Add a property_value fact via requires_property for the same subject_key"
478
+ ) :-
479
+ kb:current_req(ReqId),
480
+ kb_relationship(constrains, ReqId, SubjectFactId),
481
+ strict_req_fact_pairing_fact_kind(SubjectFactId, subject),
482
+ kb:fact_subject_key(SubjectFactId, SubjectKey),
483
+ \+ kb:effective_req_property_fact(
484
+ ReqId,
485
+ SubjectKey,
486
+ _PropertyFactId,
487
+ _PropertyKey,
488
+ _Operator,
489
+ _ValueType,
490
+ _Value,
491
+ _Unit,
492
+ _Scope,
493
+ _Polarity,
494
+ _ValidFrom,
495
+ _ValidTo
496
+ ),
497
+ format(
498
+ string(Description),
499
+ "Requirement constrains ~w (~w) but has no matching strict requires_property fact",
500
+ [SubjectFactId, SubjectKey]
501
+ ).
502
+
503
+ strict_req_fact_pairing_issue(
504
+ ReqId,
505
+ Description,
506
+ "Add a subject fact via constrains for the same subject_key or remove the mismatched requires_property link"
507
+ ) :-
508
+ kb:current_req(ReqId),
509
+ kb_relationship(requires_property, ReqId, PropertyFactId),
510
+ strict_req_fact_pairing_fact_kind(PropertyFactId, property_value),
511
+ kb:fact_property_tuple(
512
+ PropertyFactId,
513
+ SubjectKey,
514
+ _PropertyKey,
515
+ _Operator,
516
+ _ValueType,
517
+ _Value,
518
+ _Unit,
519
+ _Scope,
520
+ _Polarity
521
+ ),
522
+ \+ kb:effective_req_property_fact(
523
+ ReqId,
524
+ SubjectKey,
525
+ PropertyFactId,
526
+ _MatchedPropertyKey,
527
+ _MatchedOperator,
528
+ _MatchedValueType,
529
+ _MatchedValue,
530
+ _MatchedUnit,
531
+ _MatchedScope,
532
+ _MatchedPolarity,
533
+ _MatchedValidFrom,
534
+ _MatchedValidTo
535
+ ),
536
+ format(
537
+ string(Description),
538
+ "Requirement requires_property ~w (~w) but has no matching strict subject fact via constrains",
539
+ [PropertyFactId, SubjectKey]
540
+ ).
541
+
542
+ strict_req_fact_pairing_issue(
543
+ ReqId,
544
+ Description,
545
+ "Use a subject fact with constrains for contradiction-safe semantics; keep non-subject facts out of strict pairing"
546
+ ) :-
547
+ kb:current_req(ReqId),
548
+ kb_relationship(constrains, ReqId, FactId),
549
+ strict_req_fact_pairing_fact_kind(FactId, Kind),
550
+ Kind \= subject,
551
+ strict_req_fact_pairing_kind_label(Kind, KindLabel),
552
+ format(
553
+ string(Description),
554
+ "Requirement links ~w via constrains using ~w; contradiction-safe constrains links must target subject facts",
555
+ [FactId, KindLabel]
556
+ ).
557
+
558
+ strict_req_fact_pairing_issue(
559
+ ReqId,
560
+ Description,
561
+ "Use a property_value fact with requires_property for contradiction-safe semantics; keep non-property facts out of strict pairing"
562
+ ) :-
563
+ kb:current_req(ReqId),
564
+ kb_relationship(requires_property, ReqId, FactId),
565
+ strict_req_fact_pairing_fact_kind(FactId, Kind),
566
+ Kind \= property_value,
567
+ strict_req_fact_pairing_kind_label(Kind, KindLabel),
568
+ format(
569
+ string(Description),
570
+ "Requirement links ~w via requires_property using ~w; contradiction-safe requires_property links must target property_value facts",
571
+ [FactId, KindLabel]
572
+ ).
573
+
574
+ strict_req_fact_pairing_fact_kind(FactId, Kind) :-
575
+ kb_entity(FactId, fact, Props),
576
+ ( memberchk(fact_kind=RawKind, Props)
577
+ -> normalize_term_atom(RawKind, Kind)
578
+ ; Kind = legacy
579
+ ).
580
+
581
+ strict_req_fact_pairing_kind_label(legacy, "a legacy fact without fact_kind").
582
+ strict_req_fact_pairing_kind_label(Kind, Label) :-
583
+ format(string(Label), "a fact_kind=~w fact", [Kind]).
584
+
448
585
  %% run_checks_json
449
586
  % Entry point for JSON output. Prints all violations as JSON to stdout.
450
587
  run_checks_json :-
@@ -492,6 +629,7 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
492
629
  check_deprecated_adrs(DeprecatedADRs),
493
630
  check_domain_contradictions(Contradictions),
494
631
  check_strict_fact_shape(StrictFactShape),
632
+ check_strict_req_fact_pairing(StrictReqFactPairing),
495
633
  ViolationsDict = _{
496
634
  must_priority_coverage: MustPriority,
497
635
  symbol_coverage: SymbolCoverage,
@@ -501,7 +639,8 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
501
639
  required_fields: RequiredFields,
502
640
  deprecated_adr_no_successor: DeprecatedADRs,
503
641
  domain_contradictions: Contradictions,
504
- strict_fact_shape: StrictFactShape
642
+ strict_fact_shape: StrictFactShape,
643
+ strict_req_fact_pairing: StrictReqFactPairing
505
644
  }.
506
645
 
507
646
  %% violations_dict_to_json(+ViolationsDict, -JsonDict)
@@ -552,8 +691,22 @@ violation_text(Val, Text) :-
552
691
  term_string(Val, Text).
553
692
 
554
693
  violation_id_text(Val, Text) :-
555
- normalize_term_atom(Val, Atom),
556
- atom_string(Atom, Text).
694
+ nonvar(Val),
695
+ Val =.. ['^^', Inner, _Type],
696
+ !,
697
+ violation_id_text(Inner, Text).
698
+ violation_id_text(literal(type(_, Val)), Text) :-
699
+ !,
700
+ violation_id_text(Val, Text).
701
+ violation_id_text(Val, Val) :-
702
+ string(Val),
703
+ !.
704
+ violation_id_text(Val, Text) :-
705
+ atom(Val),
706
+ !,
707
+ atom_string(Val, Text).
708
+ violation_id_text(Val, Text) :-
709
+ term_string(Val, Text).
557
710
 
558
711
  violation_source(EntityId, Type, Source) :-
559
712
  ( kb_entity(EntityId, Type, Props),
package/src/kb.pl CHANGED
@@ -7,8 +7,9 @@
7
7
  with_kb_mutex/1,
8
8
  kb_assert_entity/2,
9
9
  kb_assert_entity_no_audit/2,
10
- kb_log_entity_upsert/2,
10
+ kb_log_entity_upsert/3,
11
11
  kb_retract_entity/1,
12
+ kb_retract_entity/3,
12
13
  kb_entity/3,
13
14
  kb_entities_by_source/2,
14
15
  kb_assert_relationship/4,
@@ -254,8 +255,13 @@ load_kb_pl_files(Directory) :-
254
255
  % Assert an entity into the KB with audit logging.
255
256
  % Properties is a list of Key=Value pairs.
256
257
  kb_assert_entity(Type, Props) :-
258
+ memberchk(id=Id, Props),
259
+ ( once(kb_entity(Id, _, _))
260
+ -> ChangeKind = updated
261
+ ; ChangeKind = created
262
+ ),
257
263
  kb_assert_entity_no_audit(Type, Props),
258
- kb_log_entity_upsert(Type, Props).
264
+ kb_log_entity_upsert(ChangeKind, Type, Props).
259
265
 
260
266
  %% kb_assert_entity_no_audit(+Type, +Properties)
261
267
  % Assert an entity RDF payload without recording audit side effects.
@@ -284,19 +290,30 @@ kb_assert_entity_no_audit(Type, Props) :-
284
290
  )
285
291
  )).
286
292
 
287
- %% kb_log_entity_upsert(+Type, +Properties)
293
+ %% kb_log_entity_upsert(+ChangeKind, +Type, +Properties)
288
294
  % Append the audit entry for a successfully committed entity upsert.
289
- kb_log_entity_upsert(Type, Props) :-
295
+ kb_log_entity_upsert(ChangeKind, Type, Props) :-
290
296
  memberchk(id=Id, Props),
297
+ memberchk(ChangeKind, [created, updated]),
291
298
  with_kb_mutex((
292
299
  get_time(Timestamp),
293
300
  format_time(atom(TS), '%FT%T%:z', Timestamp),
294
- assert_changeset(TS, upsert, Id, Type-Props)
301
+ assert_changeset(TS, upsert, Id, Type-[change_kind=ChangeKind|Props])
295
302
  )).
296
303
 
297
304
  %% kb_retract_entity(+Id)
298
305
  % Remove an entity from the KB with audit logging.
299
306
  kb_retract_entity(Id) :-
307
+ ( once(kb_entity(Id, Type, Props))
308
+ -> entity_delete_audit_props(Id, Props, AuditProps)
309
+ ; Type = unknown,
310
+ AuditProps = [id=Id]
311
+ ),
312
+ kb_retract_entity(Id, Type, AuditProps).
313
+
314
+ %% kb_retract_entity(+Id, +Type, +AuditProps)
315
+ % Remove an entity from the KB and log the provided delete payload.
316
+ kb_retract_entity(Id, Type, AuditProps) :-
300
317
  kb_graph(Graph),
301
318
  with_kb_mutex((
302
319
  % Create entity URI
@@ -306,9 +323,30 @@ kb_retract_entity(Id) :-
306
323
  % Log to audit
307
324
  get_time(Timestamp),
308
325
  format_time(atom(TS), '%FT%T%:z', Timestamp),
309
- assert_changeset(TS, delete, Id, null)
326
+ assert_changeset(TS, delete, Id, Type-AuditProps)
310
327
  )).
311
328
 
329
+ entity_delete_audit_props(Id, Props, AuditProps) :-
330
+ findall(Key=Value,
331
+ ( member(Key, [title, source, text_ref]),
332
+ memberchk(Key=RawValue, Props),
333
+ audit_property_value(RawValue, Value)
334
+ ),
335
+ OptionalProps),
336
+ AuditProps = [id=Id|OptionalProps].
337
+
338
+ audit_property_value(RawValue, Value) :-
339
+ ( RawValue = ^^(Inner, _)
340
+ -> Value = Inner
341
+ ; RawValue = literal(type(_, Inner))
342
+ -> Value = Inner
343
+ ; RawValue = literal(lang(_, Inner))
344
+ -> Value = Inner
345
+ ; RawValue = literal(Inner)
346
+ -> Value = Inner
347
+ ; Value = RawValue
348
+ ).
349
+
312
350
  %% kb_entity(?Id, ?Type, ?Properties)
313
351
  % Query entities from the KB.
314
352
  % Properties is unified with a list of Key=Value pairs.
@@ -356,12 +394,18 @@ convert_legacy_prop(Prop, Prop, true).
356
394
  kb_entities_by_source(SourcePath, Ids) :-
357
395
  findall(Id,
358
396
  (kb_entity(Id, _Type, Props),
359
- memberchk(source=RawSource, Props),
360
- source_value_atom(RawSource, SourceAtom),
397
+ entity_source_atom(Props, SourceAtom),
361
398
  sub_atom(SourceAtom, _, _, _, SourcePath)),
362
399
  RawIds),
363
400
  sort(RawIds, Ids).
364
401
 
402
+ entity_source_atom(Props, SourceAtom) :-
403
+ ( memberchk(sourceFile=RawSourceFile, Props)
404
+ -> source_value_atom(RawSourceFile, SourceAtom)
405
+ ; memberchk(source=RawSource, Props),
406
+ source_value_atom(RawSource, SourceAtom)
407
+ ).
408
+
365
409
  source_value_atom(Value, Atom) :-
366
410
  ( atom(Value)
367
411
  -> Atom = Value
package/src/status.pl CHANGED
@@ -61,7 +61,8 @@ synced_at(DataFile, SyncedAt) :-
61
61
  !,
62
62
  time_file(DataFile, Timestamp),
63
63
  format_time(atom(SyncedAt), '%FT%TZ', Timestamp).
64
- synced_at(_, @(null)).
64
+ % Before the first successful sync there is no kb.rdf, so the public JSON contract must expose syncedAt: null.
65
+ synced_at(_, null).
65
66
 
66
67
  freshness_state(DataFile, true, stale) :-
67
68
  exists_file(DataFile),