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 +5 -2
- package/src/checks.pl +2 -2
- package/src/derived_chr.pl +65 -0
- package/src/discovery.pl +3 -3
- package/src/kb.pl +47 -8
- package/src/sparql_client.pl +150 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-core",
|
|
3
|
-
"version": "0.6.
|
|
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": [
|
|
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
|
|
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
|
-
"
|
|
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
|
-
|
|
94
|
+
aggregate_all(count,
|
|
94
95
|
(kb_relationship(Relationship, Id, _); kb_relationship(Relationship, _, Id)),
|
|
95
|
-
|
|
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
|
|
284
|
-
|
|
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
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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).
|