kibi-core 0.1.10 → 0.3.0

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.
@@ -0,0 +1,461 @@
1
+ % Module: discovery
2
+ % Curated discovery, reporting, and bounded graph traversal predicates.
3
+
4
+ :- module(discovery, [
5
+ find_gaps_json/8,
6
+ coverage_report_json/7,
7
+ graph_expand_json/8
8
+ ]).
9
+
10
+ :- use_module(library(http/json)).
11
+ :- use_module('kb.pl').
12
+ :- use_module('status.pl', [status_meta_dict/1]).
13
+ :- use_module('../schema/relationships.pl', [relationship_type/1]).
14
+
15
+ find_gaps_json(TypeFilter, MissingRelationships, PresentRelationships, Tags, SourceFilter, Limit, Offset, JsonString) :-
16
+ findall(
17
+ Row,
18
+ matching_gap_row(TypeFilter, MissingRelationships, PresentRelationships, Tags, SourceFilter, Row),
19
+ Rows0
20
+ ),
21
+ sort_dict_rows(Rows0, SortedRows),
22
+ paginate_rows(SortedRows, Offset, Limit, Rows),
23
+ length(SortedRows, Count),
24
+ status_meta_dict(Meta),
25
+ Response = _{rows: Rows, count: Count, meta: Meta},
26
+ dict_json_string(Response, JsonString).
27
+
28
+ coverage_report_json(By, Tags, IncludePassing, IncludeTransitive, Limit, Offset, JsonString) :-
29
+ coverage_rows(By, Tags, IncludePassing, IncludeTransitive, Rows0, Summary),
30
+ sort_dict_rows(Rows0, SortedRows),
31
+ paginate_rows(SortedRows, Offset, Limit, Rows),
32
+ status_meta_dict(Meta),
33
+ Response = _{summary: Summary, rows: Rows, meta: Meta},
34
+ dict_json_string(Response, JsonString).
35
+
36
+ graph_expand_json(SeedIds, Relationships, Direction, Depth, EntityTypes, MaxNodes, MaxEdges, JsonString) :-
37
+ sort(SeedIds, SeedSet),
38
+ bfs_layers(SeedSet, SeedSet, [], Relationships, Direction, Depth, MaxNodes, MaxEdges, SeenNodes0, SeenEdges0, Truncated0),
39
+ append(SeedSet, SeenNodes0, SeededNodes0),
40
+ sort(SeededNodes0, SeenNodes),
41
+ include(keep_entity_type(EntityTypes), SeenNodes, FilteredNodes0),
42
+ sort(FilteredNodes0, FilteredNodes),
43
+ include(edge_kept(FilteredNodes), SeenEdges0, FilteredEdges0),
44
+ sort(FilteredEdges0, FilteredEdges),
45
+ maplist(node_dict, FilteredNodes, Nodes),
46
+ maplist(edge_dict, FilteredEdges, Edges),
47
+ ( Truncated0
48
+ -> Truncated = true
49
+ ; Truncated = false
50
+ ),
51
+ status_meta_dict(Meta),
52
+ Response = _{nodes: Nodes, edges: Edges, truncated: Truncated, meta: Meta},
53
+ dict_json_string(Response, JsonString).
54
+
55
+ matching_gap_row(TypeFilter, MissingRelationships, PresentRelationships, Tags, SourceFilter, Row) :-
56
+ kb_entity(Id, Type, Props),
57
+ matches_type(TypeFilter, Type),
58
+ matches_tags(Tags, Props),
59
+ matches_source(SourceFilter, Props),
60
+ relationships_missing(Id, MissingRelationships),
61
+ relationships_present(Id, PresentRelationships),
62
+ entity_title_status_source(Props, Title, Status, Source),
63
+ relationship_counts(Id, Counts),
64
+ Row = _{
65
+ id: Id,
66
+ type: Type,
67
+ title: Title,
68
+ status: Status,
69
+ missingRelationships: MissingRelationships,
70
+ presentRelationships: PresentRelationships,
71
+ relationshipCounts: Counts,
72
+ source: Source
73
+ }.
74
+
75
+ relationships_missing(_Id, []).
76
+ relationships_missing(Id, [Relationship|Rest]) :-
77
+ relationship_count(Id, Relationship, 0),
78
+ relationships_missing(Id, Rest).
79
+
80
+ relationships_present(_Id, []).
81
+ relationships_present(Id, [Relationship|Rest]) :-
82
+ relationship_count(Id, Relationship, Count),
83
+ Count > 0,
84
+ relationships_present(Id, Rest).
85
+
86
+ relationship_counts(Id, CountsDict) :-
87
+ findall(Relationship-Count,
88
+ (relationship_type(Relationship), relationship_count(Id, Relationship, Count)),
89
+ Pairs),
90
+ dict_pairs(CountsDict, relationship_counts, Pairs).
91
+
92
+ relationship_count(Id, Relationship, Count) :-
93
+ findall(1,
94
+ (kb_relationship(Relationship, Id, _); kb_relationship(Relationship, _, Id)),
95
+ Matches),
96
+ length(Matches, Count).
97
+
98
+ coverage_rows(req, Tags, IncludePassing, IncludeTransitive, Rows, Summary) :-
99
+ !,
100
+ findall(Row,
101
+ requirement_coverage_row(Tags, IncludeTransitive, Row),
102
+ AllRows),
103
+ filter_req_coverage_rows(IncludePassing, AllRows, Rows),
104
+ coverage_summary(AllRows, Summary).
105
+ coverage_rows(symbol, Tags, IncludePassing, _IncludeTransitive, Rows, Summary) :-
106
+ !,
107
+ findall(Row,
108
+ symbol_coverage_row(Tags, Row),
109
+ AllRows),
110
+ filter_symbol_coverage_rows(IncludePassing, AllRows, Rows),
111
+ length(AllRows, Total),
112
+ include(symbol_row_fully_covered, AllRows, CoveredRows),
113
+ length(CoveredRows, Covered),
114
+ Uncovered is Total - Covered,
115
+ Summary = _{total: Total, fullyCovered: Covered, uncovered: Uncovered}.
116
+ coverage_rows(type, _Tags, _IncludePassing, _IncludeTransitive, Rows, Summary) :-
117
+ findall(Type-Count,
118
+ type_entity_count(Type, Count),
119
+ Pairs),
120
+ maplist(type_pair_row, Pairs, Rows),
121
+ length(Rows, Total),
122
+ Summary = _{total: Total}.
123
+
124
+ requirement_coverage_row(Tags, IncludeTransitive, Row) :-
125
+ kb_entity(Id, req, Props),
126
+ matches_tags(Tags, Props),
127
+ entity_title_status_source(Props, Title, Status, _Source),
128
+ entity_priority(Props, Priority),
129
+ count_distinct_targets(specified_by, Id, source, ScenarioCount),
130
+ requirement_test_count(Id, TestCount),
131
+ count_direct_symbols(Id, DirectSymbolCount),
132
+ count_transitive_symbols(Id, IncludeTransitive, TransitiveSymbolCount),
133
+ requirement_coverage_state(Id, Props, Gaps, Evaluated, CoverageStatus),
134
+ Row = _{
135
+ id: Id,
136
+ type: req,
137
+ title: Title,
138
+ status: Status,
139
+ priority: Priority,
140
+ scenarioCount: ScenarioCount,
141
+ testCount: TestCount,
142
+ directSymbolCount: DirectSymbolCount,
143
+ transitiveSymbolCount: TransitiveSymbolCount,
144
+ gaps: Gaps,
145
+ evaluated: Evaluated,
146
+ coverageStatus: CoverageStatus
147
+ }.
148
+
149
+ symbol_coverage_row(Tags, Row) :-
150
+ kb_entity(Id, symbol, Props),
151
+ matches_tags(Tags, Props),
152
+ entity_title_status_source(Props, Title, Status, _Source),
153
+ count_direct_requirements(Id, DirectRequirementCount),
154
+ count_direct_tests(Id, TestCount),
155
+ ( symbol_no_req_coverage(Id, _)
156
+ -> Gaps = [missing_requirement],
157
+ CoverageStatus = uncovered
158
+ ; Gaps = [],
159
+ CoverageStatus = fully_covered
160
+ ),
161
+ Row = _{
162
+ id: Id,
163
+ type: symbol,
164
+ title: Title,
165
+ status: Status,
166
+ directRequirementCount: DirectRequirementCount,
167
+ testCount: TestCount,
168
+ gaps: Gaps,
169
+ coverageStatus: CoverageStatus
170
+ }.
171
+
172
+ coverage_summary(Rows, Summary) :-
173
+ length(Rows, Total),
174
+ include(req_row_evaluated, Rows, EvaluatedRows),
175
+ include(req_row_fully_covered, Rows, FullyCoveredRows),
176
+ include(req_row_uncovered, Rows, UncoveredRows),
177
+ include(req_row_not_applicable, Rows, NotApplicableRows),
178
+ include(req_row_missing_scenario, Rows, MissingScenarioRows),
179
+ include(req_row_missing_test, Rows, MissingTestRows),
180
+ include(req_row_missing_both, Rows, MissingBothRows),
181
+ length(EvaluatedRows, Evaluated),
182
+ length(FullyCoveredRows, FullyCovered),
183
+ length(UncoveredRows, Uncovered),
184
+ length(NotApplicableRows, NotApplicable),
185
+ length(MissingScenarioRows, MissingScenario),
186
+ length(MissingTestRows, MissingTest),
187
+ length(MissingBothRows, MissingScenarioAndTest),
188
+ Summary = _{
189
+ total: Total,
190
+ evaluated: Evaluated,
191
+ fullyCovered: FullyCovered,
192
+ uncovered: Uncovered,
193
+ notApplicable: NotApplicable,
194
+ missingScenario: MissingScenario,
195
+ missingTest: MissingTest,
196
+ missingScenarioAndTest: MissingScenarioAndTest
197
+ }.
198
+
199
+ req_row_fully_covered(Row) :-
200
+ CoverageStatus = Row.get(coverageStatus),
201
+ CoverageStatus = fully_covered.
202
+ req_row_evaluated(Row) :-
203
+ Evaluated = Row.get(evaluated),
204
+ Evaluated = true.
205
+ req_row_uncovered(Row) :-
206
+ CoverageStatus = Row.get(coverageStatus),
207
+ CoverageStatus = uncovered.
208
+ req_row_not_applicable(Row) :-
209
+ CoverageStatus = Row.get(coverageStatus),
210
+ CoverageStatus = not_applicable.
211
+ req_row_missing_scenario(Row) :-
212
+ Row.gaps = [missing_scenario].
213
+ req_row_missing_test(Row) :-
214
+ Row.gaps = [missing_test].
215
+ req_row_missing_both(Row) :-
216
+ Row.gaps = [missing_scenario_and_test].
217
+
218
+ symbol_row_fully_covered(Row) :-
219
+ CoverageStatus = Row.get(coverageStatus),
220
+ CoverageStatus = fully_covered.
221
+
222
+ count_distinct_targets(Relationship, Id, source, Count) :-
223
+ !,
224
+ findall(Target, kb_relationship(Relationship, Id, Target), Targets0),
225
+ sort(Targets0, Targets),
226
+ length(Targets, Count).
227
+
228
+ requirement_test_count(Id, Count) :-
229
+ findall(TestId, kb_relationship(verified_by, Id, TestId), Verified0),
230
+ findall(TestId, kb_relationship(validates, TestId, Id), Validates0),
231
+ append(Verified0, Validates0, Combined0),
232
+ sort(Combined0, Combined),
233
+ length(Combined, Count).
234
+
235
+ count_direct_symbols(Id, Count) :-
236
+ findall(SymbolId, kb_relationship(implements, SymbolId, Id), Symbols0),
237
+ sort(Symbols0, Symbols),
238
+ length(Symbols, Count).
239
+
240
+ count_transitive_symbols(Id, true, Count) :-
241
+ !,
242
+ findall(SymbolId, transitively_implements(SymbolId, Id), Symbols0),
243
+ sort(Symbols0, Symbols),
244
+ length(Symbols, Count).
245
+ count_transitive_symbols(Id, false, Count) :-
246
+ count_direct_symbols(Id, Count).
247
+
248
+ count_direct_requirements(Id, Count) :-
249
+ findall(ReqId, kb_relationship(implements, Id, ReqId), ReqIds0),
250
+ sort(ReqIds0, ReqIds),
251
+ length(ReqIds, Count).
252
+
253
+ count_direct_tests(Id, Count) :-
254
+ findall(TestId, kb_relationship(covered_by, Id, TestId), TestIds0),
255
+ sort(TestIds0, TestIds),
256
+ length(TestIds, Count).
257
+
258
+ requirement_gap_list(Id, [Reason]) :-
259
+ coverage_gap(Id, Reason),
260
+ !.
261
+ requirement_gap_list(_, []).
262
+
263
+ requirement_coverage_state(Id, Props, Gaps, true, CoverageStatus) :-
264
+ entity_priority(Props, Priority),
265
+ must_priority(Priority),
266
+ !,
267
+ requirement_gap_list(Id, Gaps),
268
+ ( Gaps = []
269
+ -> CoverageStatus = fully_covered
270
+ ; CoverageStatus = uncovered
271
+ ).
272
+ requirement_coverage_state(_Id, _Props, [], false, not_applicable).
273
+
274
+ filter_req_coverage_rows(true, Rows, Rows).
275
+ filter_req_coverage_rows(false, Rows, Filtered) :-
276
+ exclude(req_row_fully_covered, Rows, Filtered).
277
+
278
+ filter_symbol_coverage_rows(true, Rows, Rows).
279
+ filter_symbol_coverage_rows(false, Rows, Filtered) :-
280
+ exclude(symbol_row_fully_covered, Rows, Filtered).
281
+
282
+ entity_priority(Props, Priority) :-
283
+ ( memberchk(priority=PriorityValue, Props)
284
+ -> source_value_atom(PriorityValue, Priority)
285
+ ; Priority = ''
286
+ ).
287
+
288
+ must_priority(Priority) :-
289
+ atom(Priority),
290
+ downcase_atom(Priority, Lowercase),
291
+ sub_atom(Lowercase, _, 4, 0, must).
292
+
293
+ type_entity_count(Type, Count) :-
294
+ setof(Id, Props^kb_entity(Id, Type, Props), Ids),
295
+ length(Ids, Count).
296
+
297
+ type_pair_row(Type-Count, _{id: Type, type: Type, count: Count}).
298
+
299
+ bfs_layers(_Frontier, SeenNodes, SeenEdges, _Relationships, _Direction, 0, _MaxNodes, _MaxEdges, SeenNodes, SeenEdges, false) :- !.
300
+ bfs_layers([], SeenNodes, SeenEdges, _Relationships, _Direction, _Depth, _MaxNodes, _MaxEdges, SeenNodes, SeenEdges, false) :- !.
301
+ bfs_layers(Frontier, SeenNodes, SeenEdges, Relationships, Direction, Depth, MaxNodes, MaxEdges, FinalNodes, FinalEdges, Truncated) :-
302
+ ( length(SeenNodes, NodeCount), NodeCount >= MaxNodes
303
+ -> FinalNodes = SeenNodes,
304
+ FinalEdges = SeenEdges,
305
+ Truncated = true
306
+ ; length(SeenEdges, EdgeCount), EdgeCount >= MaxEdges
307
+ -> FinalNodes = SeenNodes,
308
+ FinalEdges = SeenEdges,
309
+ Truncated = true
310
+ ; findall(Edge-Next,
311
+ (member(Current, Frontier), edge_step(Current, Relationships, Direction, Edge, Next)),
312
+ Pairs0),
313
+ sort(Pairs0, Pairs),
314
+ collect_pairs(Pairs, SeenNodes, SeenEdges, MaxNodes, MaxEdges, NextFrontier, NextNodes, NextEdges, HitLimit),
315
+ Depth1 is Depth - 1,
316
+ bfs_layers(NextFrontier, NextNodes, NextEdges, Relationships, Direction, Depth1, MaxNodes, MaxEdges, FinalNodes, FinalEdges, Truncated1),
317
+ ( HitLimit
318
+ -> Truncated = true
319
+ ; Truncated = Truncated1
320
+ )
321
+ ).
322
+
323
+ collect_pairs([], SeenNodes, SeenEdges, _MaxNodes, _MaxEdges, [], SeenNodes, SeenEdges, false).
324
+ collect_pairs([Edge-Next|Rest], SeenNodes, SeenEdges, MaxNodes, MaxEdges, Frontier, FinalNodes, FinalEdges, HitLimit) :-
325
+ ( memberchk(Edge, SeenEdges)
326
+ -> true,
327
+ collect_pairs(Rest, SeenNodes, SeenEdges, MaxNodes, MaxEdges, Frontier, FinalNodes, FinalEdges, HitLimit)
328
+ ; collect_new_pair(Edge, Next, Rest, SeenNodes, SeenEdges, MaxNodes, MaxEdges, Frontier, FinalNodes, FinalEdges, HitLimit)
329
+ ).
330
+
331
+ collect_new_pair(Edge, Next, _Rest, SeenNodes, SeenEdges, MaxNodes, MaxEdges, Frontier, FinalNodes, FinalEdges, HitLimit) :-
332
+ prepare_new_pair(Edge, Next, SeenNodes, SeenEdges, SeenNodes1, SeenEdges1, NewFrontier),
333
+ length(SeenNodes1, NodeCount),
334
+ length(SeenEdges1, EdgeCount),
335
+ ( NodeCount >= MaxNodes
336
+ ; EdgeCount >= MaxEdges
337
+ ),
338
+ !,
339
+ Frontier = NewFrontier,
340
+ FinalNodes = SeenNodes1,
341
+ FinalEdges = SeenEdges1,
342
+ HitLimit = true.
343
+ collect_new_pair(Edge, Next, Rest, SeenNodes, SeenEdges, MaxNodes, MaxEdges, Frontier, FinalNodes, FinalEdges, HitLimit) :-
344
+ prepare_new_pair(Edge, Next, SeenNodes, SeenEdges, SeenNodes1, SeenEdges1, NewFrontier),
345
+ collect_pairs(Rest, SeenNodes1, SeenEdges1, MaxNodes, MaxEdges, FrontierRest, FinalNodes, FinalEdges, HitLimitRest),
346
+ append(NewFrontier, FrontierRest, Frontier),
347
+ HitLimit = HitLimitRest.
348
+
349
+ prepare_new_pair(Edge, Next, SeenNodes, SeenEdges, SeenNodes1, SeenEdges1, NewFrontier) :-
350
+ append(SeenEdges, [Edge], SeenEdges1),
351
+ ( memberchk(Next, SeenNodes)
352
+ -> SeenNodes1 = SeenNodes,
353
+ NewFrontier = []
354
+ ; append(SeenNodes, [Next], SeenNodes1),
355
+ NewFrontier = [Next]
356
+ ).
357
+
358
+ edge_step(Current, Relationships, outgoing, edge(Type, Current, Next), Next) :-
359
+ relationship_allowed(Relationships, Type),
360
+ kb_relationship(Type, Current, Next).
361
+ edge_step(Current, Relationships, incoming, edge(Type, Prev, Current), Prev) :-
362
+ relationship_allowed(Relationships, Type),
363
+ kb_relationship(Type, Prev, Current).
364
+ edge_step(Current, Relationships, both, Edge, Next) :-
365
+ edge_step(Current, Relationships, outgoing, Edge, Next).
366
+ edge_step(Current, Relationships, both, Edge, Next) :-
367
+ edge_step(Current, Relationships, incoming, Edge, Next).
368
+
369
+ relationship_allowed([], Type) :-
370
+ relationship_type(Type).
371
+ relationship_allowed(Relationships, Type) :-
372
+ member(Type, Relationships).
373
+
374
+ keep_entity_type([], _Id).
375
+ keep_entity_type(EntityTypes, Id) :-
376
+ kb_entity(Id, Type, _),
377
+ memberchk(Type, EntityTypes).
378
+
379
+ edge_kept(KeptNodes, edge(_Type, From, To)) :-
380
+ memberchk(From, KeptNodes),
381
+ memberchk(To, KeptNodes).
382
+
383
+ node_dict(Id, _{id: Id, type: Type, title: Title, status: Status}) :-
384
+ kb_entity(Id, Type, Props),
385
+ entity_title_status_source(Props, Title, Status, _Source).
386
+
387
+ edge_dict(edge(Type, From, To), _{type: Type, from: From, to: To}).
388
+
389
+ matches_type(none, _Type).
390
+ matches_type(TypeFilter, Type) :-
391
+ TypeFilter \= none,
392
+ Type = TypeFilter.
393
+
394
+ matches_tags([], _Props).
395
+ matches_tags(Tags, Props) :-
396
+ memberchk(tags=EntityTags, Props),
397
+ member(Tag, Tags),
398
+ member(Tag, EntityTags),
399
+ !.
400
+
401
+ matches_source(none, _Props).
402
+ matches_source(SourceFilter, Props) :-
403
+ memberchk(source=SourceValue, Props),
404
+ source_value_atom(SourceValue, SourceAtom),
405
+ sub_atom(SourceAtom, _, _, _, SourceFilter).
406
+
407
+ entity_title_status_source(Props, Title, Status, Source) :-
408
+ memberchk(title=TitleValue, Props),
409
+ memberchk(status=StatusValue, Props),
410
+ ( memberchk(source=SourceValue, Props)
411
+ -> source_value_atom(SourceValue, Source)
412
+ ; Source = ''
413
+ ),
414
+ source_value_atom(TitleValue, Title),
415
+ source_value_atom(StatusValue, Status).
416
+
417
+ source_value_atom(Val, Atom) :-
418
+ nonvar(Val),
419
+ compound(Val),
420
+ Val =.. ['^^', Inner, _Type],
421
+ !,
422
+ source_value_atom(Inner, Atom).
423
+ source_value_atom(literal(type(_, Val)), Atom) :-
424
+ !,
425
+ source_value_atom(Val, Atom).
426
+ source_value_atom(Val, Atom) :-
427
+ string(Val),
428
+ !,
429
+ atom_string(Atom, Val).
430
+ source_value_atom(Val, Atom) :-
431
+ atom(Val),
432
+ !,
433
+ Atom = Val.
434
+ source_value_atom(Val, Atom) :-
435
+ term_string(Val, Str),
436
+ atom_string(Atom, Str).
437
+
438
+ sort_dict_rows(Rows, SortedRows) :-
439
+ map_list_to_pairs(dict_row_sort_key, Rows, Pairs),
440
+ keysort(Pairs, SortedPairs),
441
+ pairs_values(SortedPairs, SortedRows).
442
+
443
+ dict_row_sort_key(Row, Key) :-
444
+ Id = Row.get(id),
445
+ Type = Row.get(type),
446
+ format(atom(Key), '~w::~w', [Type, Id]).
447
+
448
+ paginate_rows(Rows, Offset, Limit, PaginatedRows) :-
449
+ length(Prefix, Offset),
450
+ append(Prefix, Rest, Rows),
451
+ length(PaginatedRows, Limit),
452
+ append(PaginatedRows, _Tail, Rest),
453
+ !.
454
+ paginate_rows(Rows, Offset, _Limit, PaginatedRows) :-
455
+ length(Prefix, Offset),
456
+ append(Prefix, PaginatedRows, Rows),
457
+ !.
458
+ paginate_rows(_, _, _, []).
459
+
460
+ dict_json_string(Dict, JsonString) :-
461
+ with_output_to(string(JsonString), json_write_dict(current_output, Dict, [])).