kibi-core 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -2
- package/src/checks.pl +395 -0
- package/src/kb.pl +49 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Core Prolog modules and RDF graph logic for Kibi",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
"bun": ">=1.0"
|
|
10
10
|
},
|
|
11
11
|
"main": "./src/kb.pl",
|
|
12
|
-
"files": [
|
|
12
|
+
"files": [
|
|
13
|
+
"src/**/*.pl",
|
|
14
|
+
"schema/**/*.pl"
|
|
15
|
+
],
|
|
13
16
|
"license": "AGPL-3.0-or-later",
|
|
14
17
|
"author": "Piotr Franczyk",
|
|
15
18
|
"repository": {
|
package/src/checks.pl
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
% Module: checks
|
|
2
|
+
% Aggregated validation checks that return all violations in bulk
|
|
3
|
+
% This module provides predicates that compute all violations for a given
|
|
4
|
+
% validation rule in a single Prolog call, avoiding expensive round-trips.
|
|
5
|
+
|
|
6
|
+
:- module(checks, [
|
|
7
|
+
check_all/1, % Returns all violations as a dict
|
|
8
|
+
check_all_json/1, % Returns all violations as JSON string
|
|
9
|
+
check_must_priority_coverage/1, % Returns list of must-priority violations
|
|
10
|
+
check_symbol_coverage/1, % Returns list of uncovered symbols
|
|
11
|
+
check_no_dangling_refs/1, % Returns list of dangling ref violations
|
|
12
|
+
check_no_cycles/1, % Returns list of cycle violations
|
|
13
|
+
check_required_fields/1, % Returns list of missing required field violations
|
|
14
|
+
check_deprecated_adrs/1, % Returns list of deprecated ADR violations
|
|
15
|
+
check_domain_contradictions/1, % Returns list of contradiction violations
|
|
16
|
+
run_checks_json/0 % Entry point for JSON output
|
|
17
|
+
]).
|
|
18
|
+
|
|
19
|
+
:- use_module(library(http/json)).
|
|
20
|
+
:- use_module(library(http/json_convert)).
|
|
21
|
+
:- use_module('kb.pl').
|
|
22
|
+
:- use_module('../schema/entities.pl', [entity_type/1, required_property/2]).
|
|
23
|
+
:- use_module('../schema/relationships.pl', [relationship_type/1]).
|
|
24
|
+
|
|
25
|
+
% Required fields for all entities
|
|
26
|
+
required_fields([id, title, status, created_at, updated_at, source]).
|
|
27
|
+
|
|
28
|
+
% Relationship types to check for dangling references
|
|
29
|
+
all_relationship_types([
|
|
30
|
+
depends_on, verified_by, validates, specified_by,
|
|
31
|
+
constrains, requires_property, supersedes, relates_to
|
|
32
|
+
]).
|
|
33
|
+
|
|
34
|
+
%% check_all(-ViolationsDict)
|
|
35
|
+
% Returns a dict with all violations grouped by rule type.
|
|
36
|
+
% Each value is a list of violation terms: violation(Rule, EntityId, Description, Suggestion, Source)
|
|
37
|
+
check_all(ViolationsDict) :-
|
|
38
|
+
check_must_priority_coverage(MustPriority),
|
|
39
|
+
check_symbol_coverage(SymbolCoverage),
|
|
40
|
+
check_no_dangling_refs(DanglingRefs),
|
|
41
|
+
check_no_cycles(Cycles),
|
|
42
|
+
check_required_fields(RequiredFields),
|
|
43
|
+
check_deprecated_adrs(DeprecatedADRs),
|
|
44
|
+
check_domain_contradictions(Contradictions),
|
|
45
|
+
ViolationsDict = _{
|
|
46
|
+
must_priority_coverage: MustPriority,
|
|
47
|
+
symbol_coverage: SymbolCoverage,
|
|
48
|
+
no_dangling_refs: DanglingRefs,
|
|
49
|
+
no_cycles: Cycles,
|
|
50
|
+
required_fields: RequiredFields,
|
|
51
|
+
deprecated_adr_no_successor: DeprecatedADRs,
|
|
52
|
+
domain_contradictions: Contradictions
|
|
53
|
+
}.
|
|
54
|
+
|
|
55
|
+
%% check_must_priority_coverage(-Violations)
|
|
56
|
+
% Finds all must-priority requirements lacking scenario and/or test coverage.
|
|
57
|
+
% Returns list of violation/5 terms.
|
|
58
|
+
check_must_priority_coverage(Violations) :-
|
|
59
|
+
findall(
|
|
60
|
+
Violation,
|
|
61
|
+
coverage_gap_violation(Violation),
|
|
62
|
+
Violations
|
|
63
|
+
).
|
|
64
|
+
|
|
65
|
+
coverage_gap_violation(violation(
|
|
66
|
+
'must-priority-coverage',
|
|
67
|
+
ReqId,
|
|
68
|
+
Description,
|
|
69
|
+
Suggestion,
|
|
70
|
+
Source
|
|
71
|
+
)) :-
|
|
72
|
+
coverage_gap(ReqId, Reason),
|
|
73
|
+
coverage_gap_desc(Reason, Description),
|
|
74
|
+
coverage_gap_suggestion(Reason, Suggestion),
|
|
75
|
+
violation_source(ReqId, req, Source).
|
|
76
|
+
|
|
77
|
+
coverage_gap_desc(missing_test, "Must-priority requirement lacks test coverage").
|
|
78
|
+
coverage_gap_desc(missing_scenario, "Must-priority requirement lacks scenario coverage").
|
|
79
|
+
coverage_gap_desc(missing_scenario_and_test, "Must-priority requirement lacks scenario and test coverage").
|
|
80
|
+
|
|
81
|
+
coverage_gap_suggestion(missing_test, "Create test that validates this requirement").
|
|
82
|
+
coverage_gap_suggestion(missing_scenario, "Create scenario that specifies this requirement").
|
|
83
|
+
coverage_gap_suggestion(missing_scenario_and_test, "Create scenario that specifies and test that validates this requirement").
|
|
84
|
+
|
|
85
|
+
%% check_symbol_coverage(-Violations)
|
|
86
|
+
% Finds all symbols not traceable to any functional requirement.
|
|
87
|
+
check_symbol_coverage(Violations) :-
|
|
88
|
+
findall(SymbolId, symbol_no_req_coverage(SymbolId, _), SymbolIds0),
|
|
89
|
+
sort(SymbolIds0, SymbolIds),
|
|
90
|
+
maplist(symbol_coverage_violation, SymbolIds, Violations).
|
|
91
|
+
|
|
92
|
+
symbol_coverage_violation(SymbolId, violation(
|
|
93
|
+
'symbol-coverage',
|
|
94
|
+
SymbolId,
|
|
95
|
+
"Code symbol is not traceable to any functional requirement.",
|
|
96
|
+
"Update symbols.yaml to link this symbol to a related requirement.",
|
|
97
|
+
Source
|
|
98
|
+
)) :-
|
|
99
|
+
violation_source(SymbolId, symbol, Source).
|
|
100
|
+
|
|
101
|
+
%% check_no_dangling_refs(-Violations)
|
|
102
|
+
% Finds all relationships referencing non-existent entities.
|
|
103
|
+
check_no_dangling_refs(Violations) :-
|
|
104
|
+
all_relationship_types(Types),
|
|
105
|
+
check_dangling_refs_for_types(Types, [], Violations).
|
|
106
|
+
|
|
107
|
+
check_dangling_refs_for_types([], Acc, Acc).
|
|
108
|
+
check_dangling_refs_for_types([Type|Rest], Acc, Violations) :-
|
|
109
|
+
findall(
|
|
110
|
+
Violation,
|
|
111
|
+
dangling_ref_violation(Type, Violation),
|
|
112
|
+
TypeViolations
|
|
113
|
+
),
|
|
114
|
+
append(Acc, TypeViolations, NewAcc),
|
|
115
|
+
check_dangling_refs_for_types(Rest, NewAcc, Violations).
|
|
116
|
+
|
|
117
|
+
dangling_ref_violation(Type, violation(
|
|
118
|
+
'no-dangling-refs',
|
|
119
|
+
FromId,
|
|
120
|
+
Description,
|
|
121
|
+
"Remove relationship or create missing entity",
|
|
122
|
+
""
|
|
123
|
+
)) :-
|
|
124
|
+
kb_relationship(Type, FromId, ToId),
|
|
125
|
+
\+ kb_entity(FromId, _, _), % From doesn't exist
|
|
126
|
+
format(string(Description), "Relationship references non-existent entity: ~w", [FromId]).
|
|
127
|
+
|
|
128
|
+
dangling_ref_violation(Type, violation(
|
|
129
|
+
'no-dangling-refs',
|
|
130
|
+
ToId,
|
|
131
|
+
Description,
|
|
132
|
+
"Remove relationship or create missing entity",
|
|
133
|
+
""
|
|
134
|
+
)) :-
|
|
135
|
+
kb_relationship(Type, FromId, ToId),
|
|
136
|
+
kb_entity(FromId, _, _), % From exists
|
|
137
|
+
\+ kb_entity(ToId, _, _), % To doesn't exist
|
|
138
|
+
format(string(Description), "Relationship references non-existent entity: ~w", [ToId]).
|
|
139
|
+
|
|
140
|
+
%% check_no_cycles(-Violations)
|
|
141
|
+
% Finds circular dependencies in the depends_on graph.
|
|
142
|
+
check_no_cycles(Violations) :-
|
|
143
|
+
% Build adjacency list from depends_on relationships
|
|
144
|
+
findall(From-To, kb_relationship(depends_on, From, To), EdgePairs0),
|
|
145
|
+
sort(EdgePairs0, Edges),
|
|
146
|
+
cycle_start_nodes(Edges, Starts),
|
|
147
|
+
|
|
148
|
+
% Find at most one representative cycle per start node.
|
|
149
|
+
findall(
|
|
150
|
+
Cycle,
|
|
151
|
+
( member(Start, Starts),
|
|
152
|
+
once(find_cycle_from_start(Edges, Start, Cycle))
|
|
153
|
+
),
|
|
154
|
+
Cycles
|
|
155
|
+
),
|
|
156
|
+
|
|
157
|
+
% Convert cycles to violations (only report first occurrence of each cycle)
|
|
158
|
+
cycles_to_violations(Cycles, [], Violations).
|
|
159
|
+
|
|
160
|
+
cycle_start_nodes(Edges, Starts) :-
|
|
161
|
+
findall(Start, member(Start-_, Edges), Starts0),
|
|
162
|
+
sort(Starts0, Starts).
|
|
163
|
+
|
|
164
|
+
find_cycle_from_start(Edges, Start, [Start, Start]) :-
|
|
165
|
+
memberchk(Start-Start, Edges),
|
|
166
|
+
!.
|
|
167
|
+
find_cycle_from_start(Edges, Start, Cycle) :-
|
|
168
|
+
dfs_cycle(Edges, Start, Start, [Start], Cycle).
|
|
169
|
+
|
|
170
|
+
dfs_cycle(Edges, Start, Current, Path, Cycle) :-
|
|
171
|
+
member(Current-Next, Edges),
|
|
172
|
+
( Next = Start
|
|
173
|
+
-> length(Path, Len),
|
|
174
|
+
Len > 1,
|
|
175
|
+
reverse([Start|Path], Cycle)
|
|
176
|
+
; \+ memberchk(Next, Path),
|
|
177
|
+
dfs_cycle(Edges, Start, Next, [Next|Path], Cycle)
|
|
178
|
+
).
|
|
179
|
+
|
|
180
|
+
cycles_to_violations([], _, []).
|
|
181
|
+
cycles_to_violations([Cycle|Rest], Seen, [Violation|Violations]) :-
|
|
182
|
+
normalize_cycle(Cycle, Normalized),
|
|
183
|
+
\+ memberchk(Normalized, Seen),
|
|
184
|
+
!,
|
|
185
|
+
cycle_to_violation(Cycle, Violation),
|
|
186
|
+
cycles_to_violations(Rest, [Normalized|Seen], Violations).
|
|
187
|
+
cycles_to_violations([_|Rest], Seen, Violations) :-
|
|
188
|
+
cycles_to_violations(Rest, Seen, Violations).
|
|
189
|
+
|
|
190
|
+
normalize_cycle(Cycle, Normalized) :-
|
|
191
|
+
sort(Cycle, Normalized).
|
|
192
|
+
|
|
193
|
+
cycle_to_violation(Cycle, violation(
|
|
194
|
+
'no-cycles',
|
|
195
|
+
FirstId,
|
|
196
|
+
Description,
|
|
197
|
+
"Break cycle by removing one of the depends_on relationships",
|
|
198
|
+
Source
|
|
199
|
+
)) :-
|
|
200
|
+
Cycle = [FirstId|_],
|
|
201
|
+
|
|
202
|
+
% Build cycle description with source names
|
|
203
|
+
findall(
|
|
204
|
+
Name,
|
|
205
|
+
( member(Id, Cycle),
|
|
206
|
+
( kb_entity(Id, _, Props),
|
|
207
|
+
memberchk(source=SourcePath0, Props)
|
|
208
|
+
-> normalize_term_atom(SourcePath0, SourcePath),
|
|
209
|
+
file_base_name(SourcePath, Name)
|
|
210
|
+
; Name = Id
|
|
211
|
+
)
|
|
212
|
+
),
|
|
213
|
+
Names
|
|
214
|
+
),
|
|
215
|
+
|
|
216
|
+
% Join with arrows
|
|
217
|
+
atomic_list_concat(Names, ' → ', NamesStr),
|
|
218
|
+
format(string(Description), "Circular dependency detected: ~w", [NamesStr]),
|
|
219
|
+
|
|
220
|
+
% Get source of first entity
|
|
221
|
+
( kb_entity(FirstId, _, Props),
|
|
222
|
+
memberchk(source=Source0, Props)
|
|
223
|
+
-> normalize_term_atom(Source0, Source)
|
|
224
|
+
; Source = ""
|
|
225
|
+
).
|
|
226
|
+
|
|
227
|
+
%% check_required_fields(-Violations)
|
|
228
|
+
% Finds all entities missing required fields.
|
|
229
|
+
check_required_fields(Violations) :-
|
|
230
|
+
required_fields(Required),
|
|
231
|
+
findall(
|
|
232
|
+
Violation,
|
|
233
|
+
missing_required_field(Required, Violation),
|
|
234
|
+
Violations
|
|
235
|
+
).
|
|
236
|
+
|
|
237
|
+
missing_required_field(Required, violation(
|
|
238
|
+
'required-fields',
|
|
239
|
+
EntityId,
|
|
240
|
+
Description,
|
|
241
|
+
Suggestion,
|
|
242
|
+
Source
|
|
243
|
+
)) :-
|
|
244
|
+
kb_entity(EntityId, Type, Props),
|
|
245
|
+
member(Field, Required),
|
|
246
|
+
\+ memberchk(Field=_, Props),
|
|
247
|
+
|
|
248
|
+
format(string(Description), "Missing required field: ~w", [Field]),
|
|
249
|
+
format(string(Suggestion), "Add ~w to entity definition", [Field]),
|
|
250
|
+
|
|
251
|
+
( memberchk(source=Source, Props)
|
|
252
|
+
-> true
|
|
253
|
+
; Source = ""
|
|
254
|
+
).
|
|
255
|
+
|
|
256
|
+
%% check_deprecated_adrs(-Violations)
|
|
257
|
+
% Finds all deprecated ADRs without successors.
|
|
258
|
+
check_deprecated_adrs(Violations) :-
|
|
259
|
+
findall(
|
|
260
|
+
Violation,
|
|
261
|
+
deprecated_adr_violation(Violation),
|
|
262
|
+
Violations
|
|
263
|
+
).
|
|
264
|
+
|
|
265
|
+
deprecated_adr_violation(violation(
|
|
266
|
+
'deprecated-adr-no-successor',
|
|
267
|
+
AdrId,
|
|
268
|
+
Description,
|
|
269
|
+
Suggestion,
|
|
270
|
+
Source
|
|
271
|
+
)) :-
|
|
272
|
+
deprecated_no_successor(AdrId),
|
|
273
|
+
|
|
274
|
+
Description = "Archived/deprecated ADR has no successor — add a supersedes link from the replacement ADR",
|
|
275
|
+
|
|
276
|
+
format(string(Suggestion), "Create a new ADR and add: links: [{type: supersedes, target: ~w}]", [AdrId]),
|
|
277
|
+
|
|
278
|
+
( kb_entity(AdrId, adr, Props),
|
|
279
|
+
memberchk(source=Source, Props)
|
|
280
|
+
-> true
|
|
281
|
+
; Source = ""
|
|
282
|
+
).
|
|
283
|
+
|
|
284
|
+
%% check_domain_contradictions(-Violations)
|
|
285
|
+
% Finds all pairs of requirements with contradicting required properties.
|
|
286
|
+
check_domain_contradictions(Violations) :-
|
|
287
|
+
findall(
|
|
288
|
+
violation(
|
|
289
|
+
'domain-contradictions',
|
|
290
|
+
EntityId,
|
|
291
|
+
Description,
|
|
292
|
+
"Supersede one requirement or align both to the same required property",
|
|
293
|
+
""
|
|
294
|
+
),
|
|
295
|
+
( contradicting_reqs(ReqA, ReqB, Reason),
|
|
296
|
+
format(string(EntityId), "~w/~w", [ReqA, ReqB]),
|
|
297
|
+
Description = Reason
|
|
298
|
+
),
|
|
299
|
+
Violations
|
|
300
|
+
).
|
|
301
|
+
|
|
302
|
+
%% run_checks_json
|
|
303
|
+
% Entry point for JSON output. Prints all violations as JSON to stdout.
|
|
304
|
+
run_checks_json :-
|
|
305
|
+
catch(
|
|
306
|
+
( check_all(ViolationsDict),
|
|
307
|
+
json_write_dict(current_output, ViolationsDict, [width(0)]),
|
|
308
|
+
nl,
|
|
309
|
+
halt(0)
|
|
310
|
+
),
|
|
311
|
+
Error,
|
|
312
|
+
( format(user_error, '{"error": "~q"}~n', [Error]),
|
|
313
|
+
halt(1)
|
|
314
|
+
)
|
|
315
|
+
).
|
|
316
|
+
|
|
317
|
+
% Alternative: return JSON as a string binding instead of writing to stdout
|
|
318
|
+
check_all_json(JsonString) :-
|
|
319
|
+
check_all(ViolationsDict),
|
|
320
|
+
violations_dict_to_json(ViolationsDict, JsonDict),
|
|
321
|
+
with_output_to_string(
|
|
322
|
+
json_write_dict(current_output, JsonDict, [width(0)]),
|
|
323
|
+
JsonString
|
|
324
|
+
).
|
|
325
|
+
|
|
326
|
+
%% violations_dict_to_json(+ViolationsDict, -JsonDict)
|
|
327
|
+
% Converts a dict of violation/5 term lists to a dict of JSON-compatible dicts.
|
|
328
|
+
violations_dict_to_json(Dict, JsonDict) :-
|
|
329
|
+
dict_pairs(Dict, Tag, Pairs),
|
|
330
|
+
pairs_to_json_pairs(Pairs, JsonPairs),
|
|
331
|
+
dict_pairs(JsonDict, Tag, JsonPairs).
|
|
332
|
+
|
|
333
|
+
%% pairs_to_json_pairs(+Pairs, -JsonPairs)
|
|
334
|
+
% Converts a list of Key-Violations pairs to Key-JsonViolations pairs.
|
|
335
|
+
pairs_to_json_pairs([], []).
|
|
336
|
+
pairs_to_json_pairs([Key-Violations|Rest], [Key-JsonViolations|JsonRest]) :-
|
|
337
|
+
maplist(violation_to_json, Violations, JsonViolations),
|
|
338
|
+
pairs_to_json_pairs(Rest, JsonRest).
|
|
339
|
+
|
|
340
|
+
%% violation_to_json(+Violation, -JsonDict)
|
|
341
|
+
% Converts a violation(Rule, EntityId, Description, Suggestion, Source) term
|
|
342
|
+
% to a JSON-compatible dict.
|
|
343
|
+
violation_to_json(Violation, JsonDict) :-
|
|
344
|
+
violation_term_to_dict(Violation, JsonDict).
|
|
345
|
+
|
|
346
|
+
violation_term_to_dict(violation(Rule, EntityId, Description, Suggestion, Source), JsonDict) :-
|
|
347
|
+
violation_text(Rule, RuleText),
|
|
348
|
+
violation_id_text(EntityId, EntityIdText),
|
|
349
|
+
violation_text(Description, DescriptionText),
|
|
350
|
+
violation_text(Suggestion, SuggestionText),
|
|
351
|
+
violation_id_text(Source, SourceText),
|
|
352
|
+
JsonDict = _{rule: RuleText, entityId: EntityIdText, description: DescriptionText,
|
|
353
|
+
suggestion: SuggestionText, source: SourceText}.
|
|
354
|
+
|
|
355
|
+
violation_text(Val, Text) :-
|
|
356
|
+
nonvar(Val),
|
|
357
|
+
Val =.. ['^^', Inner, _Type],
|
|
358
|
+
!,
|
|
359
|
+
violation_text(Inner, Text).
|
|
360
|
+
violation_text(literal(type(_, Val)), Text) :-
|
|
361
|
+
!,
|
|
362
|
+
violation_text(Val, Text).
|
|
363
|
+
violation_text(Val, Val) :-
|
|
364
|
+
string(Val),
|
|
365
|
+
!.
|
|
366
|
+
violation_text(Val, Text) :-
|
|
367
|
+
atom(Val),
|
|
368
|
+
!,
|
|
369
|
+
atom_string(Val, Text).
|
|
370
|
+
violation_text(Val, Text) :-
|
|
371
|
+
term_string(Val, Text).
|
|
372
|
+
|
|
373
|
+
violation_id_text(Val, Text) :-
|
|
374
|
+
normalize_term_atom(Val, Atom),
|
|
375
|
+
atom_string(Atom, Text).
|
|
376
|
+
|
|
377
|
+
violation_source(EntityId, Type, Source) :-
|
|
378
|
+
( kb_entity(EntityId, Type, Props),
|
|
379
|
+
memberchk(source=Source0, Props)
|
|
380
|
+
-> normalize_term_atom(Source0, Source)
|
|
381
|
+
; Source = ""
|
|
382
|
+
).
|
|
383
|
+
|
|
384
|
+
% Helper: capture output to string
|
|
385
|
+
with_output_to_string(Goal, String) :-
|
|
386
|
+
with_output_to(codes(Codes), Goal),
|
|
387
|
+
string_codes(String, Codes).
|
|
388
|
+
|
|
389
|
+
file_base_name(Path, Base) :-
|
|
390
|
+
normalize_term_atom(Path, PathAtom),
|
|
391
|
+
( sub_atom(PathAtom, _, _, _, '/')
|
|
392
|
+
-> split_string(PathAtom, '/', '', Parts),
|
|
393
|
+
last(Parts, Base)
|
|
394
|
+
; Base = PathAtom
|
|
395
|
+
).
|
package/src/kb.pl
CHANGED
|
@@ -110,6 +110,16 @@ kb_detach :-
|
|
|
110
110
|
( kb_attached(_Directory)
|
|
111
111
|
-> (
|
|
112
112
|
kb_save,
|
|
113
|
+
% Unload RDF graph from memory to prevent duplication on reattach
|
|
114
|
+
( kb_graph(GraphURI)
|
|
115
|
+
-> rdf_unload_graph(GraphURI)
|
|
116
|
+
; true
|
|
117
|
+
),
|
|
118
|
+
% Sync and close audit log
|
|
119
|
+
( kb_audit_db(AuditLog)
|
|
120
|
+
-> db_sync(AuditLog)
|
|
121
|
+
; true
|
|
122
|
+
),
|
|
113
123
|
% Clear state
|
|
114
124
|
retractall(kb_attached(_)),
|
|
115
125
|
retractall(kb_audit_db(_)),
|
|
@@ -151,6 +161,8 @@ with_kb_mutex(Goal) :-
|
|
|
151
161
|
% Load legacy .pl entity files from the KB directory.
|
|
152
162
|
% These files use entity/4 format: entity(Type, Id, Title, Props).
|
|
153
163
|
load_kb_pl_files(Directory) :-
|
|
164
|
+
catch(abolish(entity/4), _, true),
|
|
165
|
+
dynamic(entity/4),
|
|
154
166
|
retractall(entity(_, _, _, _)),
|
|
155
167
|
directory_files(Directory, Files),
|
|
156
168
|
forall(
|
|
@@ -656,3 +668,40 @@ coerce_timestamp_atom(Val, Atom) :-
|
|
|
656
668
|
coerce_timestamp_atom(Val, Atom) :-
|
|
657
669
|
term_string(Val, Str),
|
|
658
670
|
atom_string(Atom, Str).
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
%% Staged symbol traceability predicates
|
|
674
|
+
%% These support the pre-commit traceability gate feature
|
|
675
|
+
|
|
676
|
+
%% Dynamic declarations for overlay facts
|
|
677
|
+
:- dynamic changed_symbol/1.
|
|
678
|
+
:- dynamic changed_symbol_loc/5.
|
|
679
|
+
:- dynamic changed_symbol_req/2.
|
|
680
|
+
|
|
681
|
+
%% changed_symbol_missing_req(+Symbol, +MinLinks, -Count)
|
|
682
|
+
% True if Symbol has fewer than MinLinks requirement connections.
|
|
683
|
+
% changed_symbol_req/2 overlay facts (from code-comment directives) are also
|
|
684
|
+
% counted so that `// implements: REQ-001` can satisfy the gate.
|
|
685
|
+
changed_symbol_missing_req(Symbol, MinLinks, Count) :-
|
|
686
|
+
changed_symbol(Symbol),
|
|
687
|
+
( setof(Req, transitively_implements(Symbol, Req), KbReqs)
|
|
688
|
+
-> true
|
|
689
|
+
; KbReqs = []
|
|
690
|
+
),
|
|
691
|
+
( setof(Req, changed_symbol_req(Symbol, Req), OverlayReqs)
|
|
692
|
+
-> true
|
|
693
|
+
; OverlayReqs = []
|
|
694
|
+
),
|
|
695
|
+
append(KbReqs, OverlayReqs, AllReqs),
|
|
696
|
+
sort(AllReqs, UniqueReqs),
|
|
697
|
+
length(UniqueReqs, Count),
|
|
698
|
+
Count < MinLinks.
|
|
699
|
+
|
|
700
|
+
%% changed_symbol_violation(+Symbol, +MinLinks, -Count, -File, -Line, -Col, -Name)
|
|
701
|
+
% Full violation record for a changed symbol missing requirements.
|
|
702
|
+
changed_symbol_violation(Symbol, MinLinks, Count, File, Line, Col, Name) :-
|
|
703
|
+
changed_symbol_missing_req(Symbol, MinLinks, Count),
|
|
704
|
+
( changed_symbol_loc(Symbol, FileRaw, Line, Col, NameRaw)
|
|
705
|
+
-> File = FileRaw, Name = NameRaw
|
|
706
|
+
; File = '', Line = 0, Col = 0, Name = ''
|
|
707
|
+
).
|