kibi-core 0.6.0 → 0.6.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.6.0",
3
+ "version": "0.6.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
@@ -107,7 +107,7 @@ symbol_coverage_violation(SymbolId, violation(
107
107
  'symbol-coverage',
108
108
  SymbolId,
109
109
  "Production symbol lacks qualifying requirement coverage.",
110
- "Add 'covered_by: TEST-xxx' for production coverage, and ensure that test reaches the requirement through a scenario->test path or direct req->test fallback when no scenario exists.",
110
+ "Add 'covered_by: TEST-xxx' for production coverage. If the requirement has specified_by a scenario, use verified_by(scenario,test) or validates(test,scenario). Direct verified_by(req,test) does not count when a scenario exists.",
111
111
  Source
112
112
  )) :-
113
113
  violation_source(SymbolId, symbol, Source).
@@ -718,7 +718,7 @@ strict_readiness_issue(
718
718
  strict_readiness_issue(
719
719
  ReqId,
720
720
  "Strict readiness: not-ready (strict-ready). Requirement has strict subject and property facts but no contradiction-ready matched pair, so contradiction checks still skip it.",
721
- "Align constrains and requires_property on the same subject_key, and keep the requirement current if it should participate in contradiction checks"
721
+ "Ensure constrains and requires_property use the same subject_key, and keep the requirement current if it should participate in contradiction checks"
722
722
  ) :-
723
723
  kb_entity(ReqId, req, _),
724
724
  strict_readiness_level(ReqId, strict_ready).
@@ -0,0 +1,65 @@
1
+ % Module: derived_chr
2
+ % Internal CHR pilot for bounded derived validation facts.
3
+
4
+ :- module(derived_chr, [
5
+ derive_chr_facts/0,
6
+ clear_chr_facts/0,
7
+ derived_coverage_gap/2,
8
+ derived_symbol_gap/2
9
+ ]).
10
+
11
+ :- use_module(library(chr)).
12
+ :- use_module('kb.pl').
13
+
14
+ :- chr_constraint seen_must_req/1, seen_scenario/1, seen_test/1,
15
+ seen_production_symbol/1, seen_symbol_req_coverage/1.
16
+
17
+ :- dynamic derived_coverage_gap/2.
18
+ :- dynamic derived_symbol_gap/2.
19
+ :- dynamic snapshot_must_req/1.
20
+ :- dynamic snapshot_scenario/1.
21
+ :- dynamic snapshot_test/1.
22
+ :- dynamic snapshot_production_symbol/1.
23
+ :- dynamic snapshot_symbol_req_coverage/1.
24
+
25
+ seen_must_req(Req) <=> assertz(snapshot_must_req(Req)).
26
+ seen_scenario(Req) <=> assertz(snapshot_scenario(Req)).
27
+ seen_test(Req) <=> assertz(snapshot_test(Req)).
28
+ seen_production_symbol(Symbol) <=> assertz(snapshot_production_symbol(Symbol)).
29
+ seen_symbol_req_coverage(Symbol) <=> assertz(snapshot_symbol_req_coverage(Symbol)).
30
+
31
+ clear_chr_facts :-
32
+ retractall(derived_coverage_gap(_, _)),
33
+ retractall(derived_symbol_gap(_, _)),
34
+ retractall(snapshot_must_req(_)),
35
+ retractall(snapshot_scenario(_)),
36
+ retractall(snapshot_test(_)),
37
+ retractall(snapshot_production_symbol(_)),
38
+ retractall(snapshot_symbol_req_coverage(_)).
39
+
40
+ derive_chr_facts :-
41
+ clear_chr_facts,
42
+ forall(kb:must_requirement(Req), seen_must_req(Req)),
43
+ forall(kb:has_scenario(Req), seen_scenario(Req)),
44
+ forall(kb:has_test(Req), seen_test(Req)),
45
+ forall(kb:production_symbol(Symbol), seen_production_symbol(Symbol)),
46
+ forall(kb:production_symbol_covered_for_requirement(Symbol, _), seen_symbol_req_coverage(Symbol)),
47
+ derive_coverage_gaps,
48
+ derive_symbol_gaps.
49
+
50
+ derive_coverage_gaps :-
51
+ forall(
52
+ ( snapshot_must_req(Req),
53
+ \+ snapshot_scenario(Req),
54
+ \+ snapshot_test(Req)
55
+ ),
56
+ assertz(derived_coverage_gap(Req, missing_scenario_and_test))
57
+ ).
58
+
59
+ derive_symbol_gaps :-
60
+ forall(
61
+ ( snapshot_production_symbol(Symbol),
62
+ \+ snapshot_symbol_req_coverage(Symbol)
63
+ ),
64
+ assertz(derived_symbol_gap(Symbol, no_qualifying_production_coverage))
65
+ ).
package/src/discovery.pl CHANGED
@@ -8,6 +8,7 @@
8
8
  ]).
9
9
 
10
10
  :- use_module(library(http/json)).
11
+ :- use_module(library(aggregate)).
11
12
  :- use_module('kb.pl').
12
13
  :- use_module('status.pl', [status_meta_dict/1]).
13
14
  :- use_module('../schema/relationships.pl', [relationship_type/1]).
@@ -90,10 +91,9 @@ relationship_counts(Id, CountsDict) :-
90
91
  dict_pairs(CountsDict, relationship_counts, Pairs).
91
92
 
92
93
  relationship_count(Id, Relationship, Count) :-
93
- findall(1,
94
+ aggregate_all(count,
94
95
  (kb_relationship(Relationship, Id, _); kb_relationship(Relationship, _, Id)),
95
- Matches),
96
- length(Matches, Count).
96
+ Count).
97
97
 
98
98
  coverage_rows(req, Tags, IncludePassing, IncludeTransitive, Rows, Summary) :-
99
99
  !,
package/src/kb.pl CHANGED
@@ -10,6 +10,7 @@
10
10
  kb_log_entity_upsert/3,
11
11
  kb_retract_entity/1,
12
12
  kb_retract_entity/3,
13
+ kb_retract_entity_relationships/1,
13
14
  kb_entity/3,
14
15
  kb_entities_by_source/2,
15
16
  kb_assert_relationship/4,
@@ -280,8 +281,18 @@ kb_assert_entity_no_audit(Type, Props) :-
280
281
  with_kb_mutex((
281
282
  % Create entity URI using prefix notation for namespace expansion
282
283
  format(atom(EntityURI), 'kb:entity/~w', [Id]),
283
- % Upsert semantics: remove any existing triples for this entity first.
284
- rdf_retractall(EntityURI, _, _, Graph),
284
+ % Upsert semantics: remove only property triples, preserving relationships.
285
+ % Relationship triples have entity URI objects (kb:entity/...);
286
+ % property triples have typed literal objects (_^^xsd:...).
287
+ forall(
288
+ ( rdf(EntityURI, Prop, Obj, Graph),
289
+ ( atom(Obj)
290
+ -> \+ atom_concat('kb:entity/', _, Obj)
291
+ ; true
292
+ )
293
+ ),
294
+ rdf_retractall(EntityURI, Prop, Obj, Graph)
295
+ ),
285
296
  % Store type as string literal to prevent URI interpretation
286
297
  atom_string(Type, TypeStr),
287
298
  rdf_assert(EntityURI, kb:type, TypeStr^^'http://www.w3.org/2001/XMLSchema#string', Graph),
@@ -292,6 +303,22 @@ kb_assert_entity_no_audit(Type, Props) :-
292
303
  )
293
304
  )).
294
305
 
306
+ %% kb_retract_entity_relationships(+Id)
307
+ % Remove all relationship triples for an entity, preserving property triples.
308
+ % Used by projectStagedEntities to clear stale relationships before re-asserting.
309
+ kb_retract_entity_relationships(Id) :-
310
+ kb_graph(Graph),
311
+ with_kb_mutex((
312
+ format(atom(EntityURI), 'kb:entity/~w', [Id]),
313
+ forall(
314
+ ( rdf(EntityURI, RelURI, TargetURI, Graph),
315
+ atom(TargetURI),
316
+ atom_concat('kb:entity/', _, TargetURI)
317
+ ),
318
+ rdf_retractall(EntityURI, RelURI, TargetURI, Graph)
319
+ )
320
+ )).
321
+
295
322
  %% kb_log_entity_upsert(+ChangeKind, +Type, +Properties)
296
323
  % Append the audit entry for a successfully committed entity upsert.
297
324
  kb_log_entity_upsert(ChangeKind, Type, Props) :-
@@ -430,12 +457,24 @@ kb_assert_relationship(RelType, FromId, ToId, Metadata) :-
430
457
  % leave partial audit residue.
431
458
  kb_assert_relationship_no_audit(RelType, FromId, ToId, _Metadata) :-
432
459
  kb_graph(Graph),
433
- % Validate entities exist and relationship is valid
434
- % Use once/1 to keep this predicate deterministic even if the store
435
- % contains duplicate type triples from previous versions.
436
- once(kb_entity(FromId, FromType, _)),
437
- once(kb_entity(ToId, ToType, _)),
438
- validate_relationship(RelType, FromType, ToType),
460
+ % Validate source entity exists
461
+ ( once(kb_entity(FromId, FromType, _))
462
+ -> true
463
+ ; throw(error(existence_error(entity, FromId),
464
+ context(kb_assert_relationship, 'Source entity does not exist')))
465
+ ),
466
+ % Validate target entity exists
467
+ ( once(kb_entity(ToId, ToType, _))
468
+ -> true
469
+ ; throw(error(existence_error(entity, ToId),
470
+ context(kb_assert_relationship, 'Target entity does not exist')))
471
+ ),
472
+ % Validate relationship type and direction
473
+ ( validate_relationship(RelType, FromType, ToType)
474
+ -> true
475
+ ; throw(error(type_error(relationship, RelType),
476
+ context(kb_assert_relationship, 'Invalid relationship: ~w from ~w to ~w'-[RelType, FromType, ToType])))
477
+ ),
439
478
  validate_symbol_role_compatibility(RelType, FromId, ToId),
440
479
  % NOTE: Strict-lane fact_kind pairing is validated at the MCP layer
441
480
  % via validateStrictLanePairing() before the transaction begins.
@@ -0,0 +1,150 @@
1
+ % Module: kibi_sparql_client
2
+ % Explicit remote SPARQL client wrapper.
3
+
4
+ :- module(kibi_sparql_client, [
5
+ remote_sparql_select_json/4
6
+ ]).
7
+
8
+ :- use_module(library(error)).
9
+ :- use_module(library(http/json)).
10
+ :- use_module(library(semweb/sparql_client)).
11
+
12
+ remote_sparql_select_json(Endpoint, Query, Options, JsonString) :-
13
+ validate_non_empty_atom(endpoint, Endpoint),
14
+ validate_non_empty_atom(query, Query),
15
+ validate_remote_endpoint(Endpoint),
16
+ validate_select_query(Query),
17
+ validate_sparql_options(Options, SparqlOptions),
18
+ sparql_query(Query, Rows, [endpoint(Endpoint), variable_names(Variables)|SparqlOptions]),
19
+ maplist(row_json(Variables), Rows, JsonRows),
20
+ with_output_to(string(JsonString), json_write_dict(current_output, _{rows: JsonRows}, [])).
21
+
22
+ validate_non_empty_atom(Name, Value) :-
23
+ must_be(atom, Value),
24
+ ( Value == ''
25
+ -> throw(error(domain_error(non_empty_atom, Name), _))
26
+ ; true
27
+ ).
28
+
29
+ validate_remote_endpoint(Endpoint) :-
30
+ ( sub_atom(Endpoint, 0, _, _, 'http://')
31
+ ; sub_atom(Endpoint, 0, _, _, 'https://')
32
+ ),
33
+ endpoint_host(Endpoint, Host),
34
+ \+ local_or_private_host(Host),
35
+ !.
36
+ validate_remote_endpoint(Endpoint) :-
37
+ ( sub_atom(Endpoint, 0, _, _, 'http://')
38
+ ; sub_atom(Endpoint, 0, _, _, 'https://')
39
+ ),
40
+ !,
41
+ throw(error(domain_error(public_remote_endpoint, Endpoint), _)).
42
+ validate_remote_endpoint(Endpoint) :-
43
+ throw(error(domain_error(remote_http_endpoint, Endpoint), _)).
44
+
45
+ validate_select_query(Query) :-
46
+ normalize_space(atom(Normalized), Query),
47
+ downcase_atom(Normalized, Lower),
48
+ sub_atom(Lower, 0, 6, After, 'select'),
49
+ ( After =:= 0
50
+ ; sub_atom(Lower, 6, 1, _, ' ')
51
+ ; sub_atom(Lower, 6, 1, _, '*')
52
+ ),
53
+ !.
54
+ validate_select_query(Query) :-
55
+ throw(error(domain_error(sparql_select_query, Query), _)).
56
+
57
+ endpoint_host(Endpoint, Host) :-
58
+ strip_endpoint_scheme(Endpoint, WithoutScheme),
59
+ segment_before(WithoutScheme, '/', WithoutPath),
60
+ segment_before(WithoutPath, '?', WithoutQuery),
61
+ segment_before(WithoutQuery, '#', Authority),
62
+ strip_userinfo(Authority, HostWithPort),
63
+ strip_port(HostWithPort, Host0),
64
+ downcase_atom(Host0, Host).
65
+
66
+ strip_endpoint_scheme(Endpoint, WithoutScheme) :-
67
+ sub_atom(Endpoint, 0, 7, _, 'http://'),
68
+ !,
69
+ sub_atom(Endpoint, 7, _, 0, WithoutScheme).
70
+ strip_endpoint_scheme(Endpoint, WithoutScheme) :-
71
+ sub_atom(Endpoint, 0, 8, _, 'https://'),
72
+ sub_atom(Endpoint, 8, _, 0, WithoutScheme).
73
+
74
+ segment_before(Atom, Delimiter, Segment) :-
75
+ sub_atom(Atom, Before, _, _, Delimiter),
76
+ !,
77
+ sub_atom(Atom, 0, Before, _, Segment).
78
+ segment_before(Atom, _Delimiter, Atom).
79
+
80
+ strip_userinfo(Authority, HostWithPort) :-
81
+ sub_atom(Authority, _, _, After, '@'),
82
+ !,
83
+ sub_atom(Authority, _, After, 0, HostWithPort).
84
+ strip_userinfo(Authority, Authority).
85
+
86
+ strip_port(HostWithPort, Host) :-
87
+ sub_atom(HostWithPort, 0, 1, _, '['),
88
+ sub_atom(HostWithPort, 1, Length, _, ']'),
89
+ !,
90
+ sub_atom(HostWithPort, 1, Length, _, Host).
91
+ strip_port(HostWithPort, Host) :-
92
+ segment_before(HostWithPort, ':', Host).
93
+
94
+ local_or_private_host(localhost).
95
+ local_or_private_host('0.0.0.0').
96
+ local_or_private_host('::1').
97
+ local_or_private_host(Host) :-
98
+ sub_atom(Host, _, _, 0, '.localhost').
99
+ local_or_private_host(Host) :-
100
+ sub_atom(Host, 0, _, _, '127.').
101
+ local_or_private_host(Host) :-
102
+ sub_atom(Host, 0, _, _, '10.').
103
+ local_or_private_host(Host) :-
104
+ sub_atom(Host, 0, _, _, '192.168.').
105
+ local_or_private_host(Host) :-
106
+ sub_atom(Host, 0, _, _, '169.254.').
107
+ local_or_private_host(Host) :-
108
+ sub_atom(Host, 0, _, _, 'fe80:').
109
+ local_or_private_host(Host) :-
110
+ sub_atom(Host, 0, 2, _, Prefix),
111
+ memberchk(Prefix, ['fc', 'fd']).
112
+ local_or_private_host(Host) :-
113
+ sub_atom(Host, 0, _, _, '172.'),
114
+ atomic_list_concat([_, SecondAtom|_], '.', Host),
115
+ atom_number(SecondAtom, Second),
116
+ Second >= 16,
117
+ Second =< 31.
118
+
119
+ validate_sparql_options([], []).
120
+ validate_sparql_options([result_format(json)|Rest], SparqlOptions) :-
121
+ !,
122
+ validate_sparql_options(Rest, SparqlOptions).
123
+ validate_sparql_options([timeout(Timeout)|Rest], [timeout(Timeout)|SparqlOptions]) :-
124
+ !,
125
+ validate_sparql_options(Rest, SparqlOptions).
126
+ validate_sparql_options([Option|_], _) :-
127
+ throw(error(domain_error(sparql_client_option, Option), _)).
128
+
129
+ row_json(Variables, Row, Dict) :-
130
+ Row =.. [row|Values],
131
+ pairs_json(Variables, Values, Pairs),
132
+ dict_create(Dict, row, Pairs).
133
+
134
+ pairs_json([], [], []).
135
+ pairs_json([Variable|Variables], [Value|Values], [Variable-JsonValue|Pairs]) :-
136
+ sparql_value_json(Value, JsonValue),
137
+ pairs_json(Variables, Values, Pairs).
138
+
139
+ sparql_value_json(Value, JsonValue) :-
140
+ atomic(Value),
141
+ !,
142
+ JsonValue = Value.
143
+ sparql_value_json(literal(Value), JsonValue) :-
144
+ !,
145
+ sparql_value_json(Value, JsonValue).
146
+ sparql_value_json(type(_Type, Value), JsonValue) :-
147
+ !,
148
+ sparql_value_json(Value, JsonValue).
149
+ sparql_value_json(Value, JsonValue) :-
150
+ term_string(Value, JsonValue).