kibi-core 0.1.9 → 0.2.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.
- package/package.json +1 -1
- package/src/checks.pl +1 -1
- package/src/discovery.pl +461 -0
- package/src/kb.pl +97 -33
- package/src/status.pl +202 -0
package/package.json
CHANGED
package/src/checks.pl
CHANGED
|
@@ -324,7 +324,7 @@ deprecated_adr_violation(violation(
|
|
|
324
324
|
)) :-
|
|
325
325
|
deprecated_no_successor(AdrId),
|
|
326
326
|
|
|
327
|
-
Description = "
|
|
327
|
+
Description = "Superseded/deprecated ADR has no successor — add a supersedes link from the replacement ADR",
|
|
328
328
|
|
|
329
329
|
format(string(Suggestion), "Create a new ADR and add: links: [{type: supersedes, target: ~w}]", [AdrId]),
|
|
330
330
|
|
package/src/discovery.pl
ADDED
|
@@ -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
|
+
memberchk(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, [])).
|
package/src/kb.pl
CHANGED
|
@@ -59,16 +59,15 @@ kb_uri('urn-kibi:').
|
|
|
59
59
|
:- dynamic kb_attached/1.
|
|
60
60
|
:- dynamic kb_audit_db/1.
|
|
61
61
|
:- dynamic kb_graph/1.
|
|
62
|
+
:- dynamic kb_attached_snapshot/1.
|
|
62
63
|
:- dynamic entity/4. % Support legacy .pl file format (Type, Id, Title, Props)
|
|
63
64
|
|
|
64
65
|
%% kb_attach(+Directory)
|
|
65
66
|
% Attach to a KB directory with RDF persistence and file locking.
|
|
66
67
|
% Creates directory if it doesn't exist.
|
|
67
68
|
kb_attach(Directory) :-
|
|
68
|
-
% If we were already attached in this process, detach first.
|
|
69
|
-
% This prevents accidentally loading the same RDF snapshot multiple times.
|
|
70
69
|
( kb_attached(_)
|
|
71
|
-
->
|
|
70
|
+
-> throw(error(permission_error(attach, kb, Directory), kb_attach/1))
|
|
72
71
|
; true
|
|
73
72
|
),
|
|
74
73
|
% Ensure directory exists
|
|
@@ -89,6 +88,7 @@ kb_attach(Directory) :-
|
|
|
89
88
|
-> rdf_load(DataFile, [graph(GraphURI), silent(true)])
|
|
90
89
|
; true
|
|
91
90
|
),
|
|
91
|
+
current_data_stamp(DataFile, SnapshotStamp),
|
|
92
92
|
% Set up audit log - only attach if not already attached
|
|
93
93
|
atom_concat(Directory, '/audit.log', AuditLog),
|
|
94
94
|
( db_attached(AuditLog)
|
|
@@ -99,55 +99,114 @@ kb_attach(Directory) :-
|
|
|
99
99
|
assert(kb_attached(Directory)),
|
|
100
100
|
assert(kb_audit_db(AuditLog)),
|
|
101
101
|
assert(kb_graph(GraphURI)),
|
|
102
|
+
assert(kb_attached_snapshot(SnapshotStamp)),
|
|
102
103
|
|
|
103
104
|
% Load legacy .pl entity files if present
|
|
104
105
|
load_kb_pl_files(Directory).
|
|
105
106
|
|
|
106
107
|
|
|
107
108
|
%% kb_detach
|
|
108
|
-
% Safely detach from KB
|
|
109
|
+
% Safely detach from KB without persisting pending changes.
|
|
110
|
+
% Call kb_save/0 explicitly before kb_detach/0 when durability is required.
|
|
111
|
+
% implements REQ-009
|
|
109
112
|
kb_detach :-
|
|
110
113
|
( kb_attached(_Directory)
|
|
111
114
|
-> (
|
|
112
|
-
kb_save,
|
|
113
115
|
% Unload RDF graph from memory to prevent duplication on reattach
|
|
114
116
|
( kb_graph(GraphURI)
|
|
115
117
|
-> rdf_unload_graph(GraphURI)
|
|
116
118
|
; true
|
|
117
119
|
),
|
|
118
|
-
|
|
119
|
-
(
|
|
120
|
-
-> db_sync(AuditLog)
|
|
120
|
+
( db_attached(_)
|
|
121
|
+
-> catch(db_detach, _, true)
|
|
121
122
|
; true
|
|
122
123
|
),
|
|
123
124
|
% Clear state
|
|
124
125
|
retractall(kb_attached(_)),
|
|
125
126
|
retractall(kb_audit_db(_)),
|
|
126
|
-
retractall(kb_graph(_))
|
|
127
|
+
retractall(kb_graph(_)),
|
|
128
|
+
retractall(kb_attached_snapshot(_))
|
|
127
129
|
)
|
|
128
130
|
; true
|
|
129
131
|
).
|
|
130
132
|
|
|
131
133
|
%% kb_save
|
|
132
134
|
% Save RDF graph and sync audit log to disk
|
|
135
|
+
% implements REQ-009
|
|
133
136
|
kb_save :-
|
|
134
137
|
( kb_attached(Directory)
|
|
135
|
-
-> (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
138
|
+
-> with_kb_mutex(with_kb_file_lock(Directory, kb_save_locked(Directory)))
|
|
139
|
+
; true
|
|
140
|
+
).
|
|
141
|
+
|
|
142
|
+
with_kb_file_lock(Directory, Goal) :-
|
|
143
|
+
atom_concat(Directory, '/kb.lock', LockFile),
|
|
144
|
+
setup_call_cleanup(
|
|
145
|
+
open(LockFile, append, LockStream, [lock(write)]),
|
|
146
|
+
call(Goal),
|
|
147
|
+
close(LockStream)
|
|
148
|
+
).
|
|
149
|
+
|
|
150
|
+
kb_save_locked(Directory) :-
|
|
151
|
+
atom_concat(Directory, '/kb.rdf', DataFile),
|
|
152
|
+
temp_rdf_file(Directory, TempFile),
|
|
153
|
+
catch(
|
|
154
|
+
(
|
|
155
|
+
ensure_snapshot_current(DataFile),
|
|
156
|
+
save_rdf_snapshot(TempFile),
|
|
157
|
+
rename_file(TempFile, DataFile),
|
|
158
|
+
current_data_stamp(DataFile, UpdatedStamp),
|
|
159
|
+
retractall(kb_attached_snapshot(_)),
|
|
160
|
+
assert(kb_attached_snapshot(UpdatedStamp)),
|
|
161
|
+
sync_audit_log
|
|
162
|
+
),
|
|
163
|
+
Error,
|
|
164
|
+
(
|
|
165
|
+
cleanup_temp_file(TempFile),
|
|
166
|
+
throw(Error)
|
|
167
|
+
)
|
|
168
|
+
).
|
|
169
|
+
|
|
170
|
+
temp_rdf_file(Directory, TempFile) :-
|
|
171
|
+
get_time(Timestamp),
|
|
172
|
+
Millis is floor(Timestamp * 1000),
|
|
173
|
+
( current_prolog_flag(pid, Pid)
|
|
174
|
+
-> true
|
|
175
|
+
; Pid = 0
|
|
176
|
+
),
|
|
177
|
+
format(atom(TempFile), '~w/kb.rdf.tmp.~w.~w', [Directory, Pid, Millis]).
|
|
178
|
+
|
|
179
|
+
save_rdf_snapshot(TargetFile) :-
|
|
180
|
+
( kb_graph(GraphURI)
|
|
181
|
+
-> rdf_save(TargetFile, [graph(GraphURI), base_uri('urn-kibi:'), namespaces([kb, xsd])])
|
|
182
|
+
; rdf_save(TargetFile, [base_uri('urn-kibi:'), namespaces([kb, xsd])])
|
|
183
|
+
).
|
|
184
|
+
|
|
185
|
+
sync_audit_log :-
|
|
186
|
+
( kb_audit_db(AuditLog)
|
|
187
|
+
-> db_sync(AuditLog)
|
|
188
|
+
; true
|
|
189
|
+
).
|
|
190
|
+
|
|
191
|
+
cleanup_temp_file(TempFile) :-
|
|
192
|
+
( exists_file(TempFile)
|
|
193
|
+
-> catch(delete_file(TempFile), _, true)
|
|
194
|
+
; true
|
|
195
|
+
).
|
|
196
|
+
|
|
197
|
+
current_data_stamp(DataFile, missing) :-
|
|
198
|
+
\+ exists_file(DataFile),
|
|
199
|
+
!.
|
|
200
|
+
current_data_stamp(DataFile, stamp(MTime, Size)) :-
|
|
201
|
+
time_file(DataFile, MTime),
|
|
202
|
+
size_file(DataFile, Size).
|
|
203
|
+
|
|
204
|
+
ensure_snapshot_current(DataFile) :-
|
|
205
|
+
( kb_attached_snapshot(ExpectedStamp)
|
|
206
|
+
-> current_data_stamp(DataFile, CurrentStamp),
|
|
207
|
+
( ExpectedStamp == CurrentStamp
|
|
208
|
+
-> true
|
|
209
|
+
; throw(error(permission_error(save, kb, stale_snapshot), kb_save/0))
|
|
151
210
|
)
|
|
152
211
|
; true
|
|
153
212
|
).
|
|
@@ -554,12 +613,12 @@ conflicting(Adr1, Adr2) :-
|
|
|
554
613
|
Adr1 @< Adr2.
|
|
555
614
|
|
|
556
615
|
%% deprecated_still_used(+Adr, -Symbols)
|
|
557
|
-
% Deprecated/
|
|
616
|
+
% Deprecated/superseded ADRs that still constrain symbols.
|
|
558
617
|
deprecated_still_used(Adr, Symbols) :-
|
|
559
618
|
kb_entity(Adr, adr, Props),
|
|
560
619
|
memberchk(status=Status, Props),
|
|
561
620
|
normalize_term_atom(Status, StatusAtom),
|
|
562
|
-
memberchk(StatusAtom, [deprecated,
|
|
621
|
+
memberchk(StatusAtom, [deprecated, superseded]),
|
|
563
622
|
setof(Symbol, kb_relationship(constrained_by, Symbol, Adr), Symbols),
|
|
564
623
|
!.
|
|
565
624
|
deprecated_still_used(_, []).
|
|
@@ -569,9 +628,11 @@ deprecated_still_used(_, []).
|
|
|
569
628
|
%% ------------------------------------------------------------------
|
|
570
629
|
|
|
571
630
|
%% current_adr(+Id)
|
|
572
|
-
% True when Id is an ADR not superseded by any other ADR.
|
|
631
|
+
% True when Id is an accepted ADR not superseded by any other ADR.
|
|
573
632
|
current_adr(Id) :-
|
|
574
|
-
kb_entity(Id, adr,
|
|
633
|
+
kb_entity(Id, adr, Props),
|
|
634
|
+
memberchk(status=Status, Props),
|
|
635
|
+
normalize_term_atom(Status, accepted),
|
|
575
636
|
\+ kb_relationship(supersedes, _, Id).
|
|
576
637
|
|
|
577
638
|
%% superseded_by(+OldId, -NewId)
|
|
@@ -593,20 +654,23 @@ adr_chain_acc(Id, Visited, [Id|Rest]) :-
|
|
|
593
654
|
adr_chain_acc(Newer, [Id|Visited], Rest).
|
|
594
655
|
|
|
595
656
|
%% deprecated_no_successor(+OldId)
|
|
596
|
-
% Lint rule: ADR is
|
|
657
|
+
% Lint rule: ADR is superseded/deprecated but has no supersedes relationship pointing to it.
|
|
597
658
|
deprecated_no_successor(Id) :-
|
|
598
659
|
kb_entity(Id, adr, Props),
|
|
599
660
|
memberchk(status=Status, Props),
|
|
600
661
|
normalize_term_atom(Status, StatusAtom),
|
|
601
|
-
memberchk(StatusAtom, [
|
|
662
|
+
memberchk(StatusAtom, [superseded, deprecated]),
|
|
602
663
|
\+ kb_relationship(supersedes, _, Id).
|
|
603
664
|
|
|
604
665
|
%% current_req(+Id)
|
|
605
|
-
% Requirement is current when
|
|
666
|
+
% Requirement is current when not deprecated and not superseded by another requirement.
|
|
667
|
+
% Canonical statuses: open, in_progress, closed.
|
|
668
|
+
% Legacy statuses accepted for backwards compatibility: active, approved.
|
|
606
669
|
current_req(Id) :-
|
|
607
670
|
kb_entity(Id, req, Props),
|
|
608
671
|
memberchk(status=Status, Props),
|
|
609
|
-
normalize_term_atom(Status,
|
|
672
|
+
normalize_term_atom(Status, StatusAtom),
|
|
673
|
+
memberchk(StatusAtom, [open, in_progress, closed, active, approved]),
|
|
610
674
|
\+ kb_relationship(supersedes, _, Id).
|
|
611
675
|
|
|
612
676
|
%% contradicting_reqs(-ReqA, -ReqB, -Reason)
|
package/src/status.pl
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
% Module: status
|
|
2
|
+
% Curated KB status and freshness reporting for MCP and CLI surfaces.
|
|
3
|
+
|
|
4
|
+
:- module(status, [
|
|
5
|
+
kb_status_json/1,
|
|
6
|
+
status_meta_dict/1
|
|
7
|
+
]).
|
|
8
|
+
|
|
9
|
+
:- use_module(library(http/json)).
|
|
10
|
+
:- use_module('kb.pl').
|
|
11
|
+
|
|
12
|
+
kb_status_json(JsonString) :-
|
|
13
|
+
status_meta_dict(StatusDict),
|
|
14
|
+
dict_json_string(StatusDict, JsonString).
|
|
15
|
+
|
|
16
|
+
status_meta_dict(StatusDict) :-
|
|
17
|
+
attached_kb_info(Branch, KbPath, DataFile),
|
|
18
|
+
snapshot_id(SnapshotId),
|
|
19
|
+
synced_at(DataFile, SyncedAt),
|
|
20
|
+
freshness_state(DataFile, Dirty, SyncState),
|
|
21
|
+
StatusDict = _{
|
|
22
|
+
branch: Branch,
|
|
23
|
+
snapshotId: SnapshotId,
|
|
24
|
+
syncedAt: SyncedAt,
|
|
25
|
+
dirty: Dirty,
|
|
26
|
+
syncState: SyncState,
|
|
27
|
+
kbPath: KbPath,
|
|
28
|
+
lastSyncSource: persisted
|
|
29
|
+
}.
|
|
30
|
+
|
|
31
|
+
attached_kb_info(Branch, KbPath, DataFile) :-
|
|
32
|
+
kb:kb_attached(KbPath),
|
|
33
|
+
branch_workspace_from_kb_path(KbPath, Branch, _WorkspaceRoot),
|
|
34
|
+
directory_file_path(KbPath, 'kb.rdf', DataFile).
|
|
35
|
+
|
|
36
|
+
snapshot_id(SnapshotId) :-
|
|
37
|
+
kb:kb_attached_snapshot(stamp(MTime, Size)),
|
|
38
|
+
!,
|
|
39
|
+
format(atom(SnapshotId), 'stamp:~16f:~w', [MTime, Size]).
|
|
40
|
+
snapshot_id(SnapshotId) :-
|
|
41
|
+
kb:kb_attached_snapshot(missing),
|
|
42
|
+
!,
|
|
43
|
+
SnapshotId = missing.
|
|
44
|
+
snapshot_id(unknown).
|
|
45
|
+
|
|
46
|
+
synced_at(DataFile, SyncedAt) :-
|
|
47
|
+
exists_file(DataFile),
|
|
48
|
+
!,
|
|
49
|
+
time_file(DataFile, Timestamp),
|
|
50
|
+
format_time(atom(SyncedAt), '%FT%TZ', Timestamp).
|
|
51
|
+
synced_at(_, @(null)).
|
|
52
|
+
|
|
53
|
+
freshness_state(DataFile, true, stale) :-
|
|
54
|
+
exists_file(DataFile),
|
|
55
|
+
time_file(DataFile, SnapshotTime),
|
|
56
|
+
workspace_state_changed(SnapshotTime),
|
|
57
|
+
!.
|
|
58
|
+
freshness_state(DataFile, false, fresh) :-
|
|
59
|
+
exists_file(DataFile),
|
|
60
|
+
!.
|
|
61
|
+
freshness_state(_, true, unknown).
|
|
62
|
+
|
|
63
|
+
workspace_state_changed(SnapshotTime) :-
|
|
64
|
+
workspace_source_changed(SnapshotTime),
|
|
65
|
+
!.
|
|
66
|
+
workspace_state_changed(SnapshotTime) :-
|
|
67
|
+
documentation_tree_changed(SnapshotTime),
|
|
68
|
+
!.
|
|
69
|
+
|
|
70
|
+
workspace_source_changed(SnapshotTime) :-
|
|
71
|
+
attached_workspace_root(WorkspaceRoot),
|
|
72
|
+
kb_entity(_, _, Props),
|
|
73
|
+
memberchk(source=SourceValue, Props),
|
|
74
|
+
source_value_atom(SourceValue, SourceAtom),
|
|
75
|
+
repo_relative_source(SourceAtom, RelativeSource),
|
|
76
|
+
directory_file_path(WorkspaceRoot, RelativeSource, SourcePath),
|
|
77
|
+
( exists_file(SourcePath)
|
|
78
|
+
-> time_file(SourcePath, FileTime),
|
|
79
|
+
FileTime > SnapshotTime
|
|
80
|
+
; true
|
|
81
|
+
),
|
|
82
|
+
!.
|
|
83
|
+
|
|
84
|
+
documentation_tree_changed(SnapshotTime) :-
|
|
85
|
+
attached_workspace_root(WorkspaceRoot),
|
|
86
|
+
documentation_markdown_untracked(WorkspaceRoot),
|
|
87
|
+
!.
|
|
88
|
+
documentation_tree_changed(SnapshotTime) :-
|
|
89
|
+
attached_workspace_root(WorkspaceRoot),
|
|
90
|
+
directory_file_path(WorkspaceRoot, 'documentation', DocumentationRoot),
|
|
91
|
+
exists_directory(DocumentationRoot),
|
|
92
|
+
directory_tree_newer(DocumentationRoot, SnapshotTime),
|
|
93
|
+
!.
|
|
94
|
+
|
|
95
|
+
documentation_markdown_untracked(WorkspaceRoot) :-
|
|
96
|
+
directory_file_path(WorkspaceRoot, 'documentation', DocumentationRoot),
|
|
97
|
+
exists_directory(DocumentationRoot),
|
|
98
|
+
documentation_markdown_file(DocumentationRoot, FilePath),
|
|
99
|
+
path_relative_to_workspace(WorkspaceRoot, FilePath, RelativePath),
|
|
100
|
+
\+ known_source_path(RelativePath),
|
|
101
|
+
!.
|
|
102
|
+
|
|
103
|
+
documentation_markdown_file(Path, Path) :-
|
|
104
|
+
exists_file(Path),
|
|
105
|
+
file_name_extension(_, md, Path),
|
|
106
|
+
!.
|
|
107
|
+
documentation_markdown_file(Path, FilePath) :-
|
|
108
|
+
exists_directory(Path),
|
|
109
|
+
directory_files(Path, Entries),
|
|
110
|
+
member(Entry, Entries),
|
|
111
|
+
Entry \= '.',
|
|
112
|
+
Entry \= '..',
|
|
113
|
+
directory_file_path(Path, Entry, ChildPath),
|
|
114
|
+
documentation_markdown_file(ChildPath, FilePath).
|
|
115
|
+
|
|
116
|
+
path_relative_to_workspace(WorkspaceRoot, FilePath, RelativePath) :-
|
|
117
|
+
atom_concat(WorkspaceRoot, '/', Prefix),
|
|
118
|
+
atom_concat(Prefix, RelativePath, FilePath).
|
|
119
|
+
|
|
120
|
+
known_source_path(RelativePath) :-
|
|
121
|
+
kb_entity(_, _, Props),
|
|
122
|
+
memberchk(source=SourceValue, Props),
|
|
123
|
+
source_value_atom(SourceValue, SourceAtom),
|
|
124
|
+
repo_relative_source(SourceAtom, RelativePath).
|
|
125
|
+
|
|
126
|
+
directory_tree_newer(Path, SnapshotTime) :-
|
|
127
|
+
time_file(Path, EntryTime),
|
|
128
|
+
EntryTime > SnapshotTime,
|
|
129
|
+
!.
|
|
130
|
+
directory_tree_newer(Path, SnapshotTime) :-
|
|
131
|
+
exists_directory(Path),
|
|
132
|
+
directory_files(Path, Entries),
|
|
133
|
+
member(Entry, Entries),
|
|
134
|
+
Entry \= '.',
|
|
135
|
+
Entry \= '..',
|
|
136
|
+
directory_file_path(Path, Entry, ChildPath),
|
|
137
|
+
directory_tree_newer(ChildPath, SnapshotTime),
|
|
138
|
+
!.
|
|
139
|
+
|
|
140
|
+
attached_workspace_root(WorkspaceRoot) :-
|
|
141
|
+
kb:kb_attached(KbPath),
|
|
142
|
+
branch_workspace_from_kb_path(KbPath, _Branch, WorkspaceRoot).
|
|
143
|
+
|
|
144
|
+
branch_workspace_from_kb_path(KbPath, Branch, WorkspaceRoot) :-
|
|
145
|
+
branch_path_segments(KbPath, BranchesDir, Segments),
|
|
146
|
+
file_directory_name(BranchesDir, KbRoot),
|
|
147
|
+
file_directory_name(KbRoot, WorkspaceRoot),
|
|
148
|
+
atomic_list_concat(Segments, '/', Branch).
|
|
149
|
+
|
|
150
|
+
branch_path_segments(KbPath, BranchesDir, [Base]) :-
|
|
151
|
+
file_directory_name(KbPath, BranchesDir),
|
|
152
|
+
file_base_name(BranchesDir, branches),
|
|
153
|
+
file_base_name(KbPath, Base).
|
|
154
|
+
branch_path_segments(KbPath, BranchesDir, Segments) :-
|
|
155
|
+
file_directory_name(KbPath, Parent),
|
|
156
|
+
Parent \= KbPath,
|
|
157
|
+
branch_path_segments(Parent, BranchesDir, ParentSegments),
|
|
158
|
+
file_base_name(KbPath, Base),
|
|
159
|
+
append(ParentSegments, [Base], Segments).
|
|
160
|
+
|
|
161
|
+
repo_relative_source(SourceAtom, RelativeSource) :-
|
|
162
|
+
strip_fragment(SourceAtom, NoFragment),
|
|
163
|
+
\+ sub_atom(NoFragment, _, _, _, '://'),
|
|
164
|
+
( attached_workspace_root(WorkspaceRoot),
|
|
165
|
+
workspace_relative_path(WorkspaceRoot, NoFragment, RelativePath)
|
|
166
|
+
-> RelativeSource = RelativePath
|
|
167
|
+
; RelativeSource = NoFragment
|
|
168
|
+
).
|
|
169
|
+
|
|
170
|
+
workspace_relative_path(WorkspaceRoot, SourcePath, RelativePath) :-
|
|
171
|
+
atom_concat(WorkspaceRoot, '/', Prefix),
|
|
172
|
+
atom_concat(Prefix, RelativePath, SourcePath).
|
|
173
|
+
|
|
174
|
+
strip_fragment(SourceAtom, NoFragment) :-
|
|
175
|
+
( sub_atom(SourceAtom, Before, _, _, '#')
|
|
176
|
+
-> sub_atom(SourceAtom, 0, Before, _, NoFragment)
|
|
177
|
+
; NoFragment = SourceAtom
|
|
178
|
+
).
|
|
179
|
+
|
|
180
|
+
source_value_atom(Val, Atom) :-
|
|
181
|
+
nonvar(Val),
|
|
182
|
+
compound(Val),
|
|
183
|
+
Val =.. ['^^', Inner, _Type],
|
|
184
|
+
!,
|
|
185
|
+
source_value_atom(Inner, Atom).
|
|
186
|
+
source_value_atom(literal(type(_, Val)), Atom) :-
|
|
187
|
+
!,
|
|
188
|
+
source_value_atom(Val, Atom).
|
|
189
|
+
source_value_atom(Val, Atom) :-
|
|
190
|
+
string(Val),
|
|
191
|
+
!,
|
|
192
|
+
atom_string(Atom, Val).
|
|
193
|
+
source_value_atom(Val, Atom) :-
|
|
194
|
+
atom(Val),
|
|
195
|
+
!,
|
|
196
|
+
Atom = Val.
|
|
197
|
+
source_value_atom(Val, Atom) :-
|
|
198
|
+
term_string(Val, Str),
|
|
199
|
+
atom_string(Atom, Str).
|
|
200
|
+
|
|
201
|
+
dict_json_string(Dict, JsonString) :-
|
|
202
|
+
with_output_to(string(JsonString), json_write_dict(current_output, Dict, [])).
|