kibi-core 0.1.4
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 +19 -0
- package/schema/entities.pl +50 -0
- package/schema/relationships.pl +47 -0
- package/schema/validation.pl +49 -0
- package/src/kb.pl +616 -0
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kibi-core",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Core Prolog modules and RDF graph logic for Kibi",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=18",
|
|
9
|
+
"bun": ">=1.0"
|
|
10
|
+
},
|
|
11
|
+
"main": "./src/kb.pl",
|
|
12
|
+
"files": ["src/**/*.pl", "schema/**/*.pl"],
|
|
13
|
+
"license": "AGPL-3.0-or-later",
|
|
14
|
+
"author": "Piotr Franczyk",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/Looted/kibi.git"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
% Module: kibi_entities
|
|
2
|
+
% Entity type and property definitions for Kibi knowledge base
|
|
3
|
+
:- module(kibi_entities, [entity_type/1, entity_property/3, required_property/2, optional_property/2]).
|
|
4
|
+
|
|
5
|
+
% Entity types
|
|
6
|
+
entity_type(req).
|
|
7
|
+
entity_type(scenario).
|
|
8
|
+
entity_type(test).
|
|
9
|
+
entity_type(adr).
|
|
10
|
+
entity_type(flag).
|
|
11
|
+
entity_type(event).
|
|
12
|
+
entity_type(symbol).
|
|
13
|
+
entity_type(fact).
|
|
14
|
+
|
|
15
|
+
% entity_property(EntityType, Property, Type).
|
|
16
|
+
% Basic typing hints (atom, string, datetime, list, uri)
|
|
17
|
+
entity_property(_, id, atom).
|
|
18
|
+
entity_property(_, title, string).
|
|
19
|
+
entity_property(_, status, atom).
|
|
20
|
+
entity_property(_, created_at, datetime).
|
|
21
|
+
entity_property(_, updated_at, datetime).
|
|
22
|
+
entity_property(_, source, uri).
|
|
23
|
+
|
|
24
|
+
% Optional properties
|
|
25
|
+
entity_property(_, tags, list).
|
|
26
|
+
entity_property(_, owner, atom).
|
|
27
|
+
entity_property(_, priority, atom).
|
|
28
|
+
entity_property(_, severity, atom).
|
|
29
|
+
entity_property(_, links, list).
|
|
30
|
+
entity_property(_, text_ref, uri).
|
|
31
|
+
|
|
32
|
+
% Required properties for all entity types
|
|
33
|
+
required_property(Type, id) :- entity_type(Type).
|
|
34
|
+
required_property(Type, title) :- entity_type(Type).
|
|
35
|
+
required_property(Type, status) :- entity_type(Type).
|
|
36
|
+
required_property(Type, created_at) :- entity_type(Type).
|
|
37
|
+
required_property(Type, updated_at) :- entity_type(Type).
|
|
38
|
+
required_property(Type, source) :- entity_type(Type).
|
|
39
|
+
|
|
40
|
+
% Optional properties for all entity types
|
|
41
|
+
optional_property(Type, tags) :- entity_type(Type).
|
|
42
|
+
optional_property(Type, owner) :- entity_type(Type).
|
|
43
|
+
optional_property(Type, priority) :- entity_type(Type).
|
|
44
|
+
optional_property(Type, severity) :- entity_type(Type).
|
|
45
|
+
optional_property(Type, links) :- entity_type(Type).
|
|
46
|
+
optional_property(Type, text_ref) :- entity_type(Type).
|
|
47
|
+
|
|
48
|
+
% Documentation helpers
|
|
49
|
+
% list all entity types
|
|
50
|
+
all_entity_types(Ts) :- findall(T, entity_type(T), Ts).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
% Module: kibi_relationships
|
|
2
|
+
% Relationship type definitions and valid entity combinations
|
|
3
|
+
:- module(kibi_relationships, [relationship_type/1, valid_relationship/3, relationship_metadata/1]).
|
|
4
|
+
|
|
5
|
+
% Relationship types
|
|
6
|
+
relationship_type(depends_on).
|
|
7
|
+
relationship_type(specified_by).
|
|
8
|
+
relationship_type(verified_by).
|
|
9
|
+
relationship_type(validates).
|
|
10
|
+
relationship_type(implements).
|
|
11
|
+
relationship_type(covered_by).
|
|
12
|
+
relationship_type(constrained_by).
|
|
13
|
+
relationship_type(guards).
|
|
14
|
+
relationship_type(publishes).
|
|
15
|
+
relationship_type(consumes).
|
|
16
|
+
relationship_type(relates_to).
|
|
17
|
+
relationship_type(supersedes).
|
|
18
|
+
relationship_type(constrains).
|
|
19
|
+
relationship_type(requires_property).
|
|
20
|
+
|
|
21
|
+
% valid_relationship(RelType, FromType, ToType).
|
|
22
|
+
valid_relationship(depends_on, req, req).
|
|
23
|
+
valid_relationship(specified_by, req, scenario).
|
|
24
|
+
valid_relationship(verified_by, req, test).
|
|
25
|
+
valid_relationship(validates, test, req).
|
|
26
|
+
valid_relationship(implements, symbol, req).
|
|
27
|
+
valid_relationship(covered_by, symbol, test).
|
|
28
|
+
valid_relationship(constrained_by, symbol, adr).
|
|
29
|
+
% guards can target symbol, event, or req
|
|
30
|
+
valid_relationship(guards, flag, symbol).
|
|
31
|
+
valid_relationship(guards, flag, event).
|
|
32
|
+
valid_relationship(guards, flag, req).
|
|
33
|
+
valid_relationship(publishes, symbol, event).
|
|
34
|
+
valid_relationship(consumes, symbol, event).
|
|
35
|
+
valid_relationship(constrains, req, fact).
|
|
36
|
+
valid_relationship(requires_property, req, fact).
|
|
37
|
+
|
|
38
|
+
%% supersedes(+NewAdrId, +OldAdrId)
|
|
39
|
+
%% NewAdrId is the decision that replaces OldAdrId.
|
|
40
|
+
%% OldAdrId's status should be archived or deprecated as a consequence.
|
|
41
|
+
valid_relationship(supersedes, adr, adr).
|
|
42
|
+
valid_relationship(supersedes, req, req).
|
|
43
|
+
% escape hatch - allow any to any
|
|
44
|
+
valid_relationship(relates_to, _, _).
|
|
45
|
+
|
|
46
|
+
% Relationship metadata fields (some optional)
|
|
47
|
+
relationship_metadata([created_at, created_by, source, confidence]).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
% Module: kibi_validation
|
|
2
|
+
% Validation rules for entities and relationships in Kibi
|
|
3
|
+
:- module(kibi_validation,
|
|
4
|
+
[ validate_entity/2, % +Type, +Props
|
|
5
|
+
validate_relationship/3, % +RelType, +FromEntity, +ToEntity
|
|
6
|
+
validate_property_type/3 % +Type, +Prop, +Value
|
|
7
|
+
]).
|
|
8
|
+
|
|
9
|
+
:- use_module('entities.pl').
|
|
10
|
+
:- use_module('relationships.pl').
|
|
11
|
+
|
|
12
|
+
% validate_entity(+Type, +Props:list)
|
|
13
|
+
% Props is a list of Property=Value pairs (e.g. id=ID, title=Title)
|
|
14
|
+
validate_entity(Type, Props) :-
|
|
15
|
+
% check entity type exists
|
|
16
|
+
entity_type(Type),
|
|
17
|
+
% required properties present
|
|
18
|
+
forall(required_property(Type, P), memberchk(P=_Val, Props)),
|
|
19
|
+
% all properties have correct types
|
|
20
|
+
forall(member(Key=Val, Props), validate_property_type(Type, Key, Val)).
|
|
21
|
+
|
|
22
|
+
% validate_relationship(+RelType, +From, +To)
|
|
23
|
+
% From and To are pairs Type=Id or structures type(Type) - allow Type or Type=Id
|
|
24
|
+
validate_relationship(RelType, From, To) :-
|
|
25
|
+
relationship_type(RelType),
|
|
26
|
+
% extract types
|
|
27
|
+
type_of(From, FromType),
|
|
28
|
+
type_of(To, ToType),
|
|
29
|
+
% valid combination
|
|
30
|
+
valid_relationship(RelType, FromType, ToType).
|
|
31
|
+
|
|
32
|
+
type_of(Type, Type) :- atom(Type), entity_type(Type), !.
|
|
33
|
+
type_of(Type=_Id, Type) :- atom(Type), entity_type(Type), !.
|
|
34
|
+
|
|
35
|
+
% validate_property_type(+EntityType, +Prop, +Value)
|
|
36
|
+
validate_property_type(_Type, Prop, Value) :-
|
|
37
|
+
% find declared property type, default to atom
|
|
38
|
+
( entity_property(_Any, Prop, Kind) -> true ; Kind = atom ),
|
|
39
|
+
check_kind(Kind, Value), !.
|
|
40
|
+
|
|
41
|
+
% check_kind(Kind, Value) succeeds if Value matches Kind
|
|
42
|
+
check_kind(atom, V) :- atom(V).
|
|
43
|
+
check_kind(string, V) :- string(V).
|
|
44
|
+
check_kind(datetime, V) :- string(V). % accept ISO strings for now
|
|
45
|
+
check_kind(list, V) :- is_list(V).
|
|
46
|
+
check_kind(uri, V) :- string(V).
|
|
47
|
+
|
|
48
|
+
% Fallback false
|
|
49
|
+
check_kind(_, _) :- fail.
|
package/src/kb.pl
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
% Module: kb
|
|
2
|
+
% Core Knowledge Base module with RDF persistence and audit logging
|
|
3
|
+
:- module(kb, [
|
|
4
|
+
kb_attach/1,
|
|
5
|
+
kb_detach/0,
|
|
6
|
+
kb_save/0,
|
|
7
|
+
with_kb_mutex/1,
|
|
8
|
+
kb_assert_entity/2,
|
|
9
|
+
kb_retract_entity/1,
|
|
10
|
+
kb_entity/3,
|
|
11
|
+
kb_entities_by_source/2,
|
|
12
|
+
kb_assert_relationship/4,
|
|
13
|
+
kb_relationship/3,
|
|
14
|
+
transitively_implements/2,
|
|
15
|
+
transitively_depends/2,
|
|
16
|
+
impacted_by_change/2,
|
|
17
|
+
affected_symbols/2,
|
|
18
|
+
coverage_gap/2,
|
|
19
|
+
untested_symbols/1,
|
|
20
|
+
stale/2,
|
|
21
|
+
orphaned/1,
|
|
22
|
+
conflicting/2,
|
|
23
|
+
deprecated_still_used/2,
|
|
24
|
+
current_adr/1,
|
|
25
|
+
superseded_by/2,
|
|
26
|
+
adr_chain/2,
|
|
27
|
+
deprecated_no_successor/1,
|
|
28
|
+
symbol_no_req_coverage/2,
|
|
29
|
+
contradicting_reqs/3,
|
|
30
|
+
normalize_term_atom/2,
|
|
31
|
+
changeset/4, % Export for testing
|
|
32
|
+
kb_uri/1
|
|
33
|
+
]).
|
|
34
|
+
|
|
35
|
+
:- use_module(library(semweb/rdf11)).
|
|
36
|
+
:- use_module(library(persistency)).
|
|
37
|
+
:- use_module(library(thread)).
|
|
38
|
+
:- use_module(library(filesex)).
|
|
39
|
+
:- use_module(library(ordsets)).
|
|
40
|
+
:- use_module('../schema/entities.pl', [entity_type/1, entity_property/3, required_property/2]).
|
|
41
|
+
:- use_module('../schema/relationships.pl', [relationship_type/1, valid_relationship/3]).
|
|
42
|
+
:- use_module('../schema/validation.pl', [validate_entity/2, validate_relationship/3]).
|
|
43
|
+
|
|
44
|
+
% Constants
|
|
45
|
+
kb_uri('urn-kibi:').
|
|
46
|
+
|
|
47
|
+
% RDF namespace for KB entities and relationships
|
|
48
|
+
:- kb_uri(URI), rdf_register_prefix(kb, URI).
|
|
49
|
+
:- rdf_register_prefix(xsd, 'http://www.w3.org/2001/XMLSchema#').
|
|
50
|
+
:- rdf_meta
|
|
51
|
+
kb_entity(?, ?, ?),
|
|
52
|
+
kb_relationship(?, ?, ?).
|
|
53
|
+
|
|
54
|
+
% Persistent audit log declaration
|
|
55
|
+
:- persistent
|
|
56
|
+
changeset(timestamp:atom, operation:atom, entity_id:atom, data:any).
|
|
57
|
+
|
|
58
|
+
% Dynamic facts to track KB state
|
|
59
|
+
:- dynamic kb_attached/1.
|
|
60
|
+
:- dynamic kb_audit_db/1.
|
|
61
|
+
:- dynamic kb_graph/1.
|
|
62
|
+
|
|
63
|
+
%% kb_attach(+Directory)
|
|
64
|
+
% Attach to a KB directory with RDF persistence and file locking.
|
|
65
|
+
% Creates directory if it doesn't exist.
|
|
66
|
+
kb_attach(Directory) :-
|
|
67
|
+
% If we were already attached in this process, detach first.
|
|
68
|
+
% This prevents accidentally loading the same RDF snapshot multiple times.
|
|
69
|
+
( kb_attached(_)
|
|
70
|
+
-> kb_detach
|
|
71
|
+
; true
|
|
72
|
+
),
|
|
73
|
+
% Ensure directory exists
|
|
74
|
+
( exists_directory(Directory)
|
|
75
|
+
-> true
|
|
76
|
+
; make_directory_path(Directory)
|
|
77
|
+
),
|
|
78
|
+
% Create RDF graph name from directory
|
|
79
|
+
atom_concat('file://', Directory, GraphURI),
|
|
80
|
+
% If a graph with this URI is already present, unload it to avoid duplicates.
|
|
81
|
+
( rdf_graph(GraphURI)
|
|
82
|
+
-> rdf_unload_graph(GraphURI)
|
|
83
|
+
; true
|
|
84
|
+
),
|
|
85
|
+
% Load existing RDF data if present
|
|
86
|
+
atom_concat(Directory, '/kb.rdf', DataFile),
|
|
87
|
+
( exists_file(DataFile)
|
|
88
|
+
-> rdf_load(DataFile, [graph(GraphURI), silent(true)])
|
|
89
|
+
; true
|
|
90
|
+
),
|
|
91
|
+
% Set up audit log - only attach if not already attached
|
|
92
|
+
atom_concat(Directory, '/audit.log', AuditLog),
|
|
93
|
+
( db_attached(AuditLog)
|
|
94
|
+
-> true % Already attached
|
|
95
|
+
; db_attach(AuditLog, [])
|
|
96
|
+
),
|
|
97
|
+
% Track attachment state
|
|
98
|
+
assert(kb_attached(Directory)),
|
|
99
|
+
assert(kb_audit_db(AuditLog)),
|
|
100
|
+
assert(kb_graph(GraphURI)).
|
|
101
|
+
|
|
102
|
+
%% kb_detach
|
|
103
|
+
% Safely detach from KB, flushing journals and closing audit log.
|
|
104
|
+
kb_detach :-
|
|
105
|
+
( kb_attached(_Directory)
|
|
106
|
+
-> (
|
|
107
|
+
kb_save,
|
|
108
|
+
% Clear state
|
|
109
|
+
retractall(kb_attached(_)),
|
|
110
|
+
retractall(kb_audit_db(_)),
|
|
111
|
+
retractall(kb_graph(_))
|
|
112
|
+
)
|
|
113
|
+
; true
|
|
114
|
+
).
|
|
115
|
+
|
|
116
|
+
%% kb_save
|
|
117
|
+
% Save RDF graph and sync audit log to disk
|
|
118
|
+
kb_save :-
|
|
119
|
+
( kb_attached(Directory)
|
|
120
|
+
-> (
|
|
121
|
+
% Save RDF graph to file with namespace declarations
|
|
122
|
+
atom_concat(Directory, '/kb.rdf', DataFile),
|
|
123
|
+
% Get current graph URI
|
|
124
|
+
kb_graph(GraphURI),
|
|
125
|
+
% If we have a graph URI, save that graph. Otherwise save all data
|
|
126
|
+
% (fallback) so a kb.rdf is always produced. Report errors if save fails.
|
|
127
|
+
( kb_graph(GraphURI)
|
|
128
|
+
-> catch(rdf_save(DataFile, [graph(GraphURI), base_uri('urn-kibi:'), namespaces([kb, xsd])]), E, print_message(error, E))
|
|
129
|
+
; catch(rdf_save(DataFile, [base_uri('urn-kibi:'), namespaces([kb, xsd])]), E2, print_message(error, E2))
|
|
130
|
+
),
|
|
131
|
+
% Sync audit log
|
|
132
|
+
( kb_audit_db(AuditLog)
|
|
133
|
+
-> db_sync(AuditLog)
|
|
134
|
+
; true
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
; true
|
|
138
|
+
).
|
|
139
|
+
|
|
140
|
+
%% with_kb_mutex(+Goal)
|
|
141
|
+
% Execute Goal with KB mutex protection for thread safety.
|
|
142
|
+
with_kb_mutex(Goal) :-
|
|
143
|
+
with_mutex(kb_lock, Goal).
|
|
144
|
+
|
|
145
|
+
%% kb_assert_entity(+Type, +Properties)
|
|
146
|
+
% Assert an entity into the KB with validation and audit logging.
|
|
147
|
+
% Properties is a list of Key=Value pairs.
|
|
148
|
+
kb_assert_entity(Type, Props) :-
|
|
149
|
+
% Validate entity
|
|
150
|
+
validate_entity(Type, Props),
|
|
151
|
+
% Extract ID
|
|
152
|
+
memberchk(id=Id, Props),
|
|
153
|
+
% Get current graph
|
|
154
|
+
kb_graph(Graph),
|
|
155
|
+
% Execute with mutex protection
|
|
156
|
+
with_kb_mutex((
|
|
157
|
+
% Create entity URI using prefix notation for namespace expansion
|
|
158
|
+
format(atom(EntityURI), 'kb:entity/~w', [Id]),
|
|
159
|
+
% Upsert semantics: remove any existing triples for this entity first.
|
|
160
|
+
rdf_retractall(EntityURI, _, _, Graph),
|
|
161
|
+
% Store type as string literal to prevent URI interpretation
|
|
162
|
+
atom_string(Type, TypeStr),
|
|
163
|
+
rdf_assert(EntityURI, kb:type, TypeStr^^'http://www.w3.org/2001/XMLSchema#string', Graph),
|
|
164
|
+
% Store all properties
|
|
165
|
+
forall(
|
|
166
|
+
member(Key=Value, Props),
|
|
167
|
+
store_property(EntityURI, Key, Value, Graph)
|
|
168
|
+
),
|
|
169
|
+
% Log to audit
|
|
170
|
+
get_time(Timestamp),
|
|
171
|
+
format_time(atom(TS), '%FT%T%:z', Timestamp),
|
|
172
|
+
assert_changeset(TS, upsert, Id, Type-Props)
|
|
173
|
+
)).
|
|
174
|
+
|
|
175
|
+
%% kb_retract_entity(+Id)
|
|
176
|
+
% Remove an entity from the KB with audit logging.
|
|
177
|
+
kb_retract_entity(Id) :-
|
|
178
|
+
kb_graph(Graph),
|
|
179
|
+
with_kb_mutex((
|
|
180
|
+
% Create entity URI
|
|
181
|
+
atom_concat('kb:entity/', Id, EntityURI),
|
|
182
|
+
% Remove all triples for this entity
|
|
183
|
+
rdf_retractall(EntityURI, _, _, Graph),
|
|
184
|
+
% Log to audit
|
|
185
|
+
get_time(Timestamp),
|
|
186
|
+
format_time(atom(TS), '%FT%T%:z', Timestamp),
|
|
187
|
+
assert_changeset(TS, delete, Id, null)
|
|
188
|
+
)).
|
|
189
|
+
|
|
190
|
+
%% kb_entity(?Id, ?Type, ?Properties)
|
|
191
|
+
% Query entities from the KB.
|
|
192
|
+
% Properties is unified with a list of Key=Value pairs.
|
|
193
|
+
kb_entity(Id, Type, Props) :-
|
|
194
|
+
kb_graph(Graph),
|
|
195
|
+
% Find entity by pattern - use unquoted namespace term kb:type
|
|
196
|
+
( var(Id)
|
|
197
|
+
-> rdf(EntityURI, kb:type, TypeLiteral, Graph),
|
|
198
|
+
atom_concat('kb:entity/', Id, EntityURI)
|
|
199
|
+
; atom_concat('kb:entity/', Id, EntityURI),
|
|
200
|
+
rdf(EntityURI, kb:type, TypeLiteral, Graph)
|
|
201
|
+
),
|
|
202
|
+
% Extract type - convert string literal to atom
|
|
203
|
+
literal_to_atom(TypeLiteral, Type),
|
|
204
|
+
% Collect all properties (exclude kb:type which expands to full URI)
|
|
205
|
+
findall(Key=Value, (
|
|
206
|
+
rdf(EntityURI, PropURI, ValueLiteral, Graph),
|
|
207
|
+
kb_uri(BaseURI),
|
|
208
|
+
atom_concat(BaseURI, type, TypeURI),
|
|
209
|
+
PropURI \= TypeURI,
|
|
210
|
+
uri_to_key(PropURI, Key),
|
|
211
|
+
literal_to_value(ValueLiteral, Value)
|
|
212
|
+
), Props).
|
|
213
|
+
|
|
214
|
+
%% kb_entities_by_source(+SourcePath, -Ids)
|
|
215
|
+
% Returns all entity IDs whose source property matches SourcePath (substring match).
|
|
216
|
+
kb_entities_by_source(SourcePath, Ids) :-
|
|
217
|
+
findall(Id,
|
|
218
|
+
(kb_entity(Id, _Type, Props),
|
|
219
|
+
memberchk(source-S, Props),
|
|
220
|
+
sub_atom(S, _, _, _, SourcePath)),
|
|
221
|
+
Ids).
|
|
222
|
+
|
|
223
|
+
%% kb_assert_relationship(+Type, +From, +To, +Metadata)
|
|
224
|
+
% Assert a relationship between two entities with validation.
|
|
225
|
+
kb_assert_relationship(RelType, FromId, ToId, _Metadata) :-
|
|
226
|
+
kb_graph(Graph),
|
|
227
|
+
% Validate entities exist and relationship is valid
|
|
228
|
+
% Use once/1 to keep this predicate deterministic even if the store
|
|
229
|
+
% contains duplicate type triples from previous versions.
|
|
230
|
+
once(kb_entity(FromId, FromType, _)),
|
|
231
|
+
once(kb_entity(ToId, ToType, _)),
|
|
232
|
+
validate_relationship(RelType, FromType, ToType),
|
|
233
|
+
% Execute with mutex protection
|
|
234
|
+
with_kb_mutex((
|
|
235
|
+
% Create entity URIs
|
|
236
|
+
atom_concat('kb:entity/', FromId, FromURI),
|
|
237
|
+
atom_concat('kb:entity/', ToId, ToURI),
|
|
238
|
+
% Create relationship property URI (full URI to match saved/loaded RDF)
|
|
239
|
+
kb_uri(BaseURI),
|
|
240
|
+
atom_concat(BaseURI, RelType, RelURI),
|
|
241
|
+
% Upsert semantics: ensure the exact triple isn't duplicated.
|
|
242
|
+
rdf_retractall(FromURI, RelURI, ToURI, Graph),
|
|
243
|
+
% Assert relationship triple
|
|
244
|
+
rdf_assert(FromURI, RelURI, ToURI, Graph),
|
|
245
|
+
% Log to audit
|
|
246
|
+
get_time(Timestamp),
|
|
247
|
+
format_time(atom(TS), '%FT%T%:z', Timestamp),
|
|
248
|
+
format(atom(RelId), '~w->~w', [FromId, ToId]),
|
|
249
|
+
assert_changeset(TS, upsert_rel, RelId, RelType-[from=FromId, to=ToId])
|
|
250
|
+
)).
|
|
251
|
+
|
|
252
|
+
%% kb_relationship(?Type, ?From, ?To)
|
|
253
|
+
% Query relationships from the KB.
|
|
254
|
+
kb_relationship(RelType, FromId, ToId) :-
|
|
255
|
+
kb_graph(Graph),
|
|
256
|
+
% Create relationship property URI (full URI to match loaded RDF)
|
|
257
|
+
kb_uri(BaseURI),
|
|
258
|
+
atom_concat(BaseURI, RelType, RelURI),
|
|
259
|
+
% Find matching relationships
|
|
260
|
+
rdf(FromURI, RelURI, ToURI, Graph),
|
|
261
|
+
% Extract IDs from URIs
|
|
262
|
+
atom_concat('kb:entity/', FromId, FromURI),
|
|
263
|
+
atom_concat('kb:entity/', ToId, ToURI).
|
|
264
|
+
|
|
265
|
+
% Helper predicates
|
|
266
|
+
|
|
267
|
+
%% store_property(+EntityURI, +Key, +Value, +Graph)
|
|
268
|
+
% Store a property as an RDF triple with appropriate datatype.
|
|
269
|
+
% All values are stored as typed string literals to avoid URI interpretation issues.
|
|
270
|
+
% Uses prefix notation (kb:Key) to enable proper namespace expansion.
|
|
271
|
+
store_property(EntityURI, Key, Value, Graph) :-
|
|
272
|
+
% Build property URI using prefix notation for namespace expansion
|
|
273
|
+
format(atom(PropURI), 'kb:~w', [Key]),
|
|
274
|
+
% Always convert to literal (never store as URI/resource)
|
|
275
|
+
value_to_literal(Value, Literal),
|
|
276
|
+
rdf_assert(EntityURI, PropURI, Literal, Graph).
|
|
277
|
+
|
|
278
|
+
%% value_to_literal(+Value, -Literal)
|
|
279
|
+
% Convert Prolog value to RDF literal with appropriate datatype.
|
|
280
|
+
value_to_literal(Value, Literal) :-
|
|
281
|
+
( string(Value)
|
|
282
|
+
-> Literal = Value^^'http://www.w3.org/2001/XMLSchema#string'
|
|
283
|
+
; is_list(Value)
|
|
284
|
+
-> format(atom(ListStr), '~w', [Value]),
|
|
285
|
+
Literal = ListStr^^'http://www.w3.org/2001/XMLSchema#string'
|
|
286
|
+
; format(atom(Str), '~w', [Value]),
|
|
287
|
+
Literal = Str^^'http://www.w3.org/2001/XMLSchema#string'
|
|
288
|
+
).
|
|
289
|
+
|
|
290
|
+
%% literal_to_value(+Literal, -Value)
|
|
291
|
+
% Extract value from RDF literal, parse list syntax back to Prolog lists.
|
|
292
|
+
literal_to_value(Literal, Value) :-
|
|
293
|
+
( % Handle ^^/2 functor (RDF typed literal shorthand)
|
|
294
|
+
Literal = ^^(StrVal, 'http://www.w3.org/2001/XMLSchema#string')
|
|
295
|
+
-> ( % Preserve RDF typed literal functor for string values so callers
|
|
296
|
+
% can inspect datatype if needed; but also attempt to parse lists
|
|
297
|
+
% encoded as string into Prolog lists when appropriate.
|
|
298
|
+
(atom(StrVal) ; string(StrVal)),
|
|
299
|
+
(atom_concat('[', _, StrVal) ; string_concat("[", _, StrVal)),
|
|
300
|
+
catch(atom_to_term(StrVal, ParsedValue, []), _, fail),
|
|
301
|
+
is_list(ParsedValue)
|
|
302
|
+
-> Value = ParsedValue
|
|
303
|
+
; Value = ^^(StrVal, 'http://www.w3.org/2001/XMLSchema#string')
|
|
304
|
+
)
|
|
305
|
+
; Literal = ^^(Val, Type)
|
|
306
|
+
-> Value = ^^(Val, Type) % Preserve other typed literals as their functor
|
|
307
|
+
; Literal = literal(type('http://www.w3.org/2001/XMLSchema#string', StrVal))
|
|
308
|
+
-> ( % Try to parse as Prolog list term (handles both atoms and strings)
|
|
309
|
+
(atom(StrVal) ; string(StrVal)),
|
|
310
|
+
(atom_concat('[', _, StrVal) ; string_concat("[", _, StrVal)),
|
|
311
|
+
catch(atom_to_term(StrVal, ParsedValue, []), _, fail),
|
|
312
|
+
is_list(ParsedValue)
|
|
313
|
+
-> Value = ParsedValue
|
|
314
|
+
; Value = StrVal
|
|
315
|
+
)
|
|
316
|
+
; Literal = literal(type(_, _))
|
|
317
|
+
-> Value = Literal % Keep other typed literals as-is
|
|
318
|
+
; Literal = literal(lang(_, Val))
|
|
319
|
+
-> Value = Val
|
|
320
|
+
; Literal = literal(Value)
|
|
321
|
+
-> true
|
|
322
|
+
; Value = Literal
|
|
323
|
+
).
|
|
324
|
+
|
|
325
|
+
%% literal_to_atom(+Literal, -Atom)
|
|
326
|
+
% Convert RDF literal to atom (for type field).
|
|
327
|
+
literal_to_atom(Literal, Atom) :-
|
|
328
|
+
( % Handle RDF typed literal shorthand functor ^^(Value, Type)
|
|
329
|
+
Literal = ^^(Val, _Type)
|
|
330
|
+
-> ( % Val may be atom or string
|
|
331
|
+
atom(Val)
|
|
332
|
+
-> Atom = Val
|
|
333
|
+
; atom_string(Atom, Val)
|
|
334
|
+
)
|
|
335
|
+
; Literal = literal(type(_, StringVal))
|
|
336
|
+
-> atom_string(Atom, StringVal)
|
|
337
|
+
; Literal = literal(Value)
|
|
338
|
+
-> (atom(Value) -> Atom = Value ; atom_string(Atom, Value))
|
|
339
|
+
; atom(Literal)
|
|
340
|
+
-> Atom = Literal
|
|
341
|
+
; atom_string(Atom, Literal)
|
|
342
|
+
).
|
|
343
|
+
|
|
344
|
+
%% uri_to_key(+URI, -Key)
|
|
345
|
+
% Convert URI to property key (strip kb: namespace prefix).
|
|
346
|
+
uri_to_key(URI, Key) :-
|
|
347
|
+
( kb_uri(BaseURI),
|
|
348
|
+
atom_concat(BaseURI, Key, URI)
|
|
349
|
+
-> true
|
|
350
|
+
; atom_concat('kb:', Key, URI)
|
|
351
|
+
-> true
|
|
352
|
+
; URI = Key
|
|
353
|
+
).
|
|
354
|
+
|
|
355
|
+
%% ------------------------------------------------------------------
|
|
356
|
+
%% Inference predicates (Phase 1)
|
|
357
|
+
%% ------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
%% transitively_implements(+Symbol, +Req)
|
|
360
|
+
% A symbol transitively implements a requirement if it directly implements it,
|
|
361
|
+
% or if it is covered by a test that validates/verifies the requirement.
|
|
362
|
+
transitively_implements(Symbol, Req) :-
|
|
363
|
+
kb_relationship(implements, Symbol, Req).
|
|
364
|
+
transitively_implements(Symbol, Req) :-
|
|
365
|
+
kb_relationship(covered_by, Symbol, Test),
|
|
366
|
+
kb_relationship(validates, Test, Req).
|
|
367
|
+
transitively_implements(Symbol, Req) :-
|
|
368
|
+
kb_relationship(covered_by, Symbol, Test),
|
|
369
|
+
kb_relationship(verified_by, Req, Test).
|
|
370
|
+
|
|
371
|
+
%% transitively_depends(+Req1, +Req2)
|
|
372
|
+
% Req1 transitively depends on Req2 through depends_on chains.
|
|
373
|
+
transitively_depends(Req1, Req2) :-
|
|
374
|
+
transitively_depends_(Req1, Req2, []).
|
|
375
|
+
|
|
376
|
+
transitively_depends_(Req1, Req2, _) :-
|
|
377
|
+
kb_relationship(depends_on, Req1, Req2).
|
|
378
|
+
transitively_depends_(Req1, Req2, Visited) :-
|
|
379
|
+
kb_relationship(depends_on, Req1, Mid),
|
|
380
|
+
Req1 \= Mid,
|
|
381
|
+
\+ memberchk(Mid, Visited),
|
|
382
|
+
transitively_depends_(Mid, Req2, [Req1|Visited]).
|
|
383
|
+
|
|
384
|
+
%% impacted_by_change(?Entity, +Changed)
|
|
385
|
+
% Entity is impacted if it is connected to Changed by any relationship
|
|
386
|
+
% direction via bounded, cycle-safe traversal.
|
|
387
|
+
impacted_by_change(Changed, Changed).
|
|
388
|
+
impacted_by_change(Entity, Changed) :-
|
|
389
|
+
dif(Entity, Changed),
|
|
390
|
+
connected_entity(Changed, Entity, [Changed]).
|
|
391
|
+
|
|
392
|
+
connected_entity(Current, Target, _Visited) :-
|
|
393
|
+
linked_entity(Current, Target).
|
|
394
|
+
connected_entity(Current, Target, Visited) :-
|
|
395
|
+
linked_entity(Current, Next),
|
|
396
|
+
\+ memberchk(Next, Visited),
|
|
397
|
+
connected_entity(Next, Target, [Next|Visited]).
|
|
398
|
+
|
|
399
|
+
linked_entity(A, B) :-
|
|
400
|
+
relationship_type(RelType),
|
|
401
|
+
kb_relationship(RelType, A, B).
|
|
402
|
+
linked_entity(A, B) :-
|
|
403
|
+
relationship_type(RelType),
|
|
404
|
+
kb_relationship(RelType, B, A).
|
|
405
|
+
|
|
406
|
+
%% affected_symbols(+Req, -Symbols)
|
|
407
|
+
% Symbols affected by a requirement change include symbols implementing Req,
|
|
408
|
+
% and symbols implementing requirements that depend on Req.
|
|
409
|
+
affected_symbols(Req, Symbols) :-
|
|
410
|
+
setof(Symbol,
|
|
411
|
+
RelatedReq^(requirement_in_scope(RelatedReq, Req),
|
|
412
|
+
transitively_implements(Symbol, RelatedReq)),
|
|
413
|
+
Symbols),
|
|
414
|
+
!.
|
|
415
|
+
affected_symbols(_, []).
|
|
416
|
+
|
|
417
|
+
requirement_in_scope(Req, Req).
|
|
418
|
+
requirement_in_scope(RelatedReq, Req) :-
|
|
419
|
+
transitively_depends(RelatedReq, Req).
|
|
420
|
+
|
|
421
|
+
%% coverage_gap(+Req, -Reason)
|
|
422
|
+
% Detects missing scenario/test coverage for MUST requirements.
|
|
423
|
+
coverage_gap(Req, missing_scenario_and_test) :-
|
|
424
|
+
must_requirement(Req),
|
|
425
|
+
\+ has_scenario(Req),
|
|
426
|
+
\+ has_test(Req).
|
|
427
|
+
coverage_gap(Req, missing_scenario) :-
|
|
428
|
+
must_requirement(Req),
|
|
429
|
+
\+ has_scenario(Req),
|
|
430
|
+
has_test(Req).
|
|
431
|
+
coverage_gap(Req, missing_test) :-
|
|
432
|
+
must_requirement(Req),
|
|
433
|
+
has_scenario(Req),
|
|
434
|
+
\+ has_test(Req).
|
|
435
|
+
|
|
436
|
+
must_requirement(Req) :-
|
|
437
|
+
kb_entity(Req, req, Props),
|
|
438
|
+
memberchk(priority=Priority, Props),
|
|
439
|
+
normalize_term_atom(Priority, PriorityAtom),
|
|
440
|
+
atom_string(PriorityAtom, PriorityStr),
|
|
441
|
+
sub_string(PriorityStr, _, 4, 0, "must").
|
|
442
|
+
|
|
443
|
+
has_scenario(Req) :-
|
|
444
|
+
once(kb_relationship(specified_by, Req, _)).
|
|
445
|
+
|
|
446
|
+
has_test(Req) :-
|
|
447
|
+
once(kb_relationship(validates, _, Req)).
|
|
448
|
+
has_test(Req) :-
|
|
449
|
+
once(kb_relationship(verified_by, Req, _)).
|
|
450
|
+
|
|
451
|
+
%% untested_symbols(-Symbols)
|
|
452
|
+
% Returns symbols with no test coverage relationship.
|
|
453
|
+
untested_symbols(Symbols) :-
|
|
454
|
+
setof(Symbol,
|
|
455
|
+
(kb_entity(Symbol, symbol, _),
|
|
456
|
+
\+ kb_relationship(covered_by, Symbol, _)),
|
|
457
|
+
Symbols),
|
|
458
|
+
!.
|
|
459
|
+
untested_symbols([]).
|
|
460
|
+
|
|
461
|
+
%% stale(+Entity, +MaxAgeDays)
|
|
462
|
+
% Entity is stale if updated_at is older than MaxAgeDays.
|
|
463
|
+
stale(Entity, MaxAgeDays) :-
|
|
464
|
+
number(MaxAgeDays),
|
|
465
|
+
MaxAgeDays >= 0,
|
|
466
|
+
kb_entity(Entity, _, Props),
|
|
467
|
+
memberchk(updated_at=UpdatedAt, Props),
|
|
468
|
+
coerce_timestamp_atom(UpdatedAt, UpdatedAtAtom),
|
|
469
|
+
parse_time(UpdatedAtAtom, iso_8601, UpdatedTs),
|
|
470
|
+
get_time(NowTs),
|
|
471
|
+
AgeDays is (NowTs - UpdatedTs) / 86400,
|
|
472
|
+
AgeDays > MaxAgeDays.
|
|
473
|
+
|
|
474
|
+
%% orphaned(+Symbol)
|
|
475
|
+
% Symbol is orphaned if it has no core traceability links.
|
|
476
|
+
orphaned(Symbol) :-
|
|
477
|
+
kb_entity(Symbol, symbol, _),
|
|
478
|
+
\+ kb_relationship(implements, Symbol, _),
|
|
479
|
+
\+ kb_relationship(covered_by, Symbol, _),
|
|
480
|
+
\+ kb_relationship(constrained_by, Symbol, _).
|
|
481
|
+
|
|
482
|
+
%% conflicting(?Adr1, ?Adr2)
|
|
483
|
+
% ADRs conflict if they both constrain the same symbol and are distinct.
|
|
484
|
+
conflicting(Adr1, Adr2) :-
|
|
485
|
+
kb_relationship(constrained_by, Symbol, Adr1),
|
|
486
|
+
kb_relationship(constrained_by, Symbol, Adr2),
|
|
487
|
+
Adr1 \= Adr2,
|
|
488
|
+
Adr1 @< Adr2.
|
|
489
|
+
|
|
490
|
+
%% deprecated_still_used(+Adr, -Symbols)
|
|
491
|
+
% Deprecated/archived/rejected ADRs that still constrain symbols.
|
|
492
|
+
deprecated_still_used(Adr, Symbols) :-
|
|
493
|
+
kb_entity(Adr, adr, Props),
|
|
494
|
+
memberchk(status=Status, Props),
|
|
495
|
+
normalize_term_atom(Status, StatusAtom),
|
|
496
|
+
memberchk(StatusAtom, [deprecated, archived, rejected]),
|
|
497
|
+
setof(Symbol, kb_relationship(constrained_by, Symbol, Adr), Symbols),
|
|
498
|
+
!.
|
|
499
|
+
deprecated_still_used(_, []).
|
|
500
|
+
|
|
501
|
+
%% ------------------------------------------------------------------
|
|
502
|
+
%% ADR Supersession Predicates
|
|
503
|
+
%% ------------------------------------------------------------------
|
|
504
|
+
|
|
505
|
+
%% current_adr(+Id)
|
|
506
|
+
% True when Id is an ADR not superseded by any other ADR.
|
|
507
|
+
current_adr(Id) :-
|
|
508
|
+
kb_entity(Id, adr, _),
|
|
509
|
+
\+ kb_relationship(supersedes, _, Id).
|
|
510
|
+
|
|
511
|
+
%% superseded_by(+OldId, -NewId)
|
|
512
|
+
% Direct supersession.
|
|
513
|
+
superseded_by(OldId, NewId) :-
|
|
514
|
+
kb_relationship(supersedes, NewId, OldId).
|
|
515
|
+
|
|
516
|
+
%% adr_chain(+AnyId, -Chain)
|
|
517
|
+
% Full ordered chain from AnyId to the current ADR (newest last).
|
|
518
|
+
% Cycle-safe via visited accumulator.
|
|
519
|
+
adr_chain(Id, Chain) :-
|
|
520
|
+
adr_chain_acc(Id, [], Chain).
|
|
521
|
+
adr_chain_acc(Id, Visited, [Id]) :-
|
|
522
|
+
\+ member(Id, Visited),
|
|
523
|
+
\+ kb_relationship(supersedes, _, Id).
|
|
524
|
+
adr_chain_acc(Id, Visited, [Id|Rest]) :-
|
|
525
|
+
\+ member(Id, Visited),
|
|
526
|
+
kb_relationship(supersedes, Newer, Id),
|
|
527
|
+
adr_chain_acc(Newer, [Id|Visited], Rest).
|
|
528
|
+
|
|
529
|
+
%% deprecated_no_successor(+OldId)
|
|
530
|
+
% Lint rule: ADR is archived/deprecated but has no supersedes relationship pointing to it.
|
|
531
|
+
deprecated_no_successor(Id) :-
|
|
532
|
+
kb_entity(Id, adr, Props),
|
|
533
|
+
memberchk(status=Status, Props),
|
|
534
|
+
normalize_term_atom(Status, StatusAtom),
|
|
535
|
+
memberchk(StatusAtom, [archived, deprecated]),
|
|
536
|
+
\+ kb_relationship(supersedes, _, Id).
|
|
537
|
+
|
|
538
|
+
%% current_req(+Id)
|
|
539
|
+
% Requirement is current when active and not superseded by another requirement.
|
|
540
|
+
current_req(Id) :-
|
|
541
|
+
kb_entity(Id, req, Props),
|
|
542
|
+
memberchk(status=Status, Props),
|
|
543
|
+
normalize_term_atom(Status, active),
|
|
544
|
+
\+ kb_relationship(supersedes, _, Id).
|
|
545
|
+
|
|
546
|
+
%% contradicting_reqs(-ReqA, -ReqB, -Reason)
|
|
547
|
+
% Two current requirements contradict if they constrain the same fact
|
|
548
|
+
% but require different properties.
|
|
549
|
+
contradicting_reqs(ReqA, ReqB, Reason) :-
|
|
550
|
+
current_req(ReqA),
|
|
551
|
+
current_req(ReqB),
|
|
552
|
+
ReqA @< ReqB,
|
|
553
|
+
kb_relationship(constrains, ReqA, FactId),
|
|
554
|
+
kb_relationship(constrains, ReqB, FactId),
|
|
555
|
+
kb_relationship(requires_property, ReqA, PropA),
|
|
556
|
+
kb_relationship(requires_property, ReqB, PropB),
|
|
557
|
+
PropA \= PropB,
|
|
558
|
+
format(atom(Reason), 'Conflict on ~w: ~w vs ~w', [FactId, PropA, PropB]).
|
|
559
|
+
|
|
560
|
+
normalize_term_atom(Val^^_Type, Atom) :-
|
|
561
|
+
!,
|
|
562
|
+
normalize_term_atom(Val, Atom).
|
|
563
|
+
normalize_term_atom(literal(type(_, Val)), Atom) :-
|
|
564
|
+
!,
|
|
565
|
+
normalize_term_atom(Val, Atom).
|
|
566
|
+
normalize_term_atom(Val, Atom) :-
|
|
567
|
+
string(Val),
|
|
568
|
+
!,
|
|
569
|
+
atom_string(ValAtom, Val),
|
|
570
|
+
normalize_uri_atom(ValAtom, Atom).
|
|
571
|
+
normalize_term_atom(Val, Atom) :-
|
|
572
|
+
atom(Val),
|
|
573
|
+
!,
|
|
574
|
+
normalize_uri_atom(Val, Atom).
|
|
575
|
+
normalize_term_atom(Val, Atom) :-
|
|
576
|
+
term_string(Val, ValStr),
|
|
577
|
+
atom_string(ValAtom, ValStr),
|
|
578
|
+
normalize_uri_atom(ValAtom, Atom).
|
|
579
|
+
|
|
580
|
+
normalize_uri_atom(Value, Atom) :-
|
|
581
|
+
( sub_atom(Value, _, _, _, '/')
|
|
582
|
+
-> atomic_list_concat(Parts, '/', Value),
|
|
583
|
+
last(Parts, Last),
|
|
584
|
+
Atom = Last
|
|
585
|
+
; Atom = Value
|
|
586
|
+
).
|
|
587
|
+
|
|
588
|
+
%% symbol_no_req_coverage(+Symbol, -Reason)
|
|
589
|
+
% Find symbols that are not traceable to any functional requirement.
|
|
590
|
+
symbol_no_req_coverage(Symbol, no_path_to_req) :-
|
|
591
|
+
kb_entity(Symbol, symbol, _),
|
|
592
|
+
\+ transitively_implements(Symbol, _).
|
|
593
|
+
|
|
594
|
+
% Helper predicate for readability - symbols with no traceability
|
|
595
|
+
symbol_uncovered(Symbol) :-
|
|
596
|
+
kb_entity(Symbol, symbol, _),
|
|
597
|
+
\+ transitively_implements(Symbol, _).
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
coerce_timestamp_atom(Val^^_Type, Atom) :-
|
|
601
|
+
!,
|
|
602
|
+
coerce_timestamp_atom(Val, Atom).
|
|
603
|
+
coerce_timestamp_atom(literal(type(_, Val)), Atom) :-
|
|
604
|
+
!,
|
|
605
|
+
coerce_timestamp_atom(Val, Atom).
|
|
606
|
+
coerce_timestamp_atom(Val, Atom) :-
|
|
607
|
+
atom(Val),
|
|
608
|
+
!,
|
|
609
|
+
Atom = Val.
|
|
610
|
+
coerce_timestamp_atom(Val, Atom) :-
|
|
611
|
+
string(Val),
|
|
612
|
+
!,
|
|
613
|
+
atom_string(Atom, Val).
|
|
614
|
+
coerce_timestamp_atom(Val, Atom) :-
|
|
615
|
+
term_string(Val, Str),
|
|
616
|
+
atom_string(Atom, Str).
|