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 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).