kibi-core 0.1.4 → 0.1.6
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 +395 -0
- package/src/kb.pl +81 -2
package/package.json
CHANGED
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
|
@@ -59,6 +59,7 @@ 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 entity/4. % Support legacy .pl file format (Type, Id, Title, Props)
|
|
62
63
|
|
|
63
64
|
%% kb_attach(+Directory)
|
|
64
65
|
% Attach to a KB directory with RDF persistence and file locking.
|
|
@@ -97,7 +98,11 @@ kb_attach(Directory) :-
|
|
|
97
98
|
% Track attachment state
|
|
98
99
|
assert(kb_attached(Directory)),
|
|
99
100
|
assert(kb_audit_db(AuditLog)),
|
|
100
|
-
assert(kb_graph(GraphURI))
|
|
101
|
+
assert(kb_graph(GraphURI)),
|
|
102
|
+
|
|
103
|
+
% Load legacy .pl entity files if present
|
|
104
|
+
load_kb_pl_files(Directory).
|
|
105
|
+
|
|
101
106
|
|
|
102
107
|
%% kb_detach
|
|
103
108
|
% Safely detach from KB, flushing journals and closing audit log.
|
|
@@ -142,6 +147,25 @@ kb_save :-
|
|
|
142
147
|
with_kb_mutex(Goal) :-
|
|
143
148
|
with_mutex(kb_lock, Goal).
|
|
144
149
|
|
|
150
|
+
%% load_kb_pl_files(+Directory)
|
|
151
|
+
% Load legacy .pl entity files from the KB directory.
|
|
152
|
+
% These files use entity/4 format: entity(Type, Id, Title, Props).
|
|
153
|
+
load_kb_pl_files(Directory) :-
|
|
154
|
+
retractall(entity(_, _, _, _)),
|
|
155
|
+
directory_files(Directory, Files),
|
|
156
|
+
forall(
|
|
157
|
+
(
|
|
158
|
+
member(File, Files),
|
|
159
|
+
sub_atom(File, _, 3, 0, '.pl'),
|
|
160
|
+
\+ memberchk(File, ['entities.pl', 'relationships.pl', 'validation.pl'])
|
|
161
|
+
),
|
|
162
|
+
(
|
|
163
|
+
atom_concat(Directory, '/', Prefix),
|
|
164
|
+
atom_concat(Prefix, File, FullPath),
|
|
165
|
+
catch(consult(FullPath), Error, (print_message(warning, Error), fail))
|
|
166
|
+
)
|
|
167
|
+
).
|
|
168
|
+
|
|
145
169
|
%% kb_assert_entity(+Type, +Properties)
|
|
146
170
|
% Assert an entity into the KB with validation and audit logging.
|
|
147
171
|
% Properties is a list of Key=Value pairs.
|
|
@@ -209,7 +233,25 @@ kb_entity(Id, Type, Props) :-
|
|
|
209
233
|
PropURI \= TypeURI,
|
|
210
234
|
uri_to_key(PropURI, Key),
|
|
211
235
|
literal_to_value(ValueLiteral, Value)
|
|
212
|
-
|
|
236
|
+
), Props).
|
|
237
|
+
|
|
238
|
+
% Fallback: read from legacy entity/4 facts loaded from .pl files
|
|
239
|
+
kb_entity(Id, Type, Props) :-
|
|
240
|
+
entity(Type, Id, _Title, PropList),
|
|
241
|
+
convert_legacy_props(PropList, Props).
|
|
242
|
+
|
|
243
|
+
% Convert legacy property list format to Key=Value pairs
|
|
244
|
+
convert_legacy_props([], []).
|
|
245
|
+
convert_legacy_props([Prop|Rest], [Key=Value|OutRest]) :-
|
|
246
|
+
convert_legacy_prop(Prop, Key, Value),
|
|
247
|
+
convert_legacy_props(Rest, OutRest).
|
|
248
|
+
|
|
249
|
+
convert_legacy_prop(Prop, Key, Value) :-
|
|
250
|
+
functor(Prop, Key, 1), !,
|
|
251
|
+
arg(1, Prop, Value).
|
|
252
|
+
convert_legacy_prop(Key-Value, Key, Value) :- !.
|
|
253
|
+
convert_legacy_prop(Key=Value, Key, Value) :- !.
|
|
254
|
+
convert_legacy_prop(Prop, Prop, true).
|
|
213
255
|
|
|
214
256
|
%% kb_entities_by_source(+SourcePath, -Ids)
|
|
215
257
|
% Returns all entity IDs whose source property matches SourcePath (substring match).
|
|
@@ -614,3 +656,40 @@ coerce_timestamp_atom(Val, Atom) :-
|
|
|
614
656
|
coerce_timestamp_atom(Val, Atom) :-
|
|
615
657
|
term_string(Val, Str),
|
|
616
658
|
atom_string(Atom, Str).
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
%% Staged symbol traceability predicates
|
|
662
|
+
%% These support the pre-commit traceability gate feature
|
|
663
|
+
|
|
664
|
+
%% Dynamic declarations for overlay facts
|
|
665
|
+
:- dynamic changed_symbol/1.
|
|
666
|
+
:- dynamic changed_symbol_loc/5.
|
|
667
|
+
:- dynamic changed_symbol_req/2.
|
|
668
|
+
|
|
669
|
+
%% changed_symbol_missing_req(+Symbol, +MinLinks, -Count)
|
|
670
|
+
% True if Symbol has fewer than MinLinks requirement connections.
|
|
671
|
+
% changed_symbol_req/2 overlay facts (from code-comment directives) are also
|
|
672
|
+
% counted so that `// implements: REQ-001` can satisfy the gate.
|
|
673
|
+
changed_symbol_missing_req(Symbol, MinLinks, Count) :-
|
|
674
|
+
changed_symbol(Symbol),
|
|
675
|
+
( setof(Req, transitively_implements(Symbol, Req), KbReqs)
|
|
676
|
+
-> true
|
|
677
|
+
; KbReqs = []
|
|
678
|
+
),
|
|
679
|
+
( setof(Req, changed_symbol_req(Symbol, Req), OverlayReqs)
|
|
680
|
+
-> true
|
|
681
|
+
; OverlayReqs = []
|
|
682
|
+
),
|
|
683
|
+
append(KbReqs, OverlayReqs, AllReqs),
|
|
684
|
+
sort(AllReqs, UniqueReqs),
|
|
685
|
+
length(UniqueReqs, Count),
|
|
686
|
+
Count < MinLinks.
|
|
687
|
+
|
|
688
|
+
%% changed_symbol_violation(+Symbol, +MinLinks, -Count, -File, -Line, -Col, -Name)
|
|
689
|
+
% Full violation record for a changed symbol missing requirements.
|
|
690
|
+
changed_symbol_violation(Symbol, MinLinks, Count, File, Line, Col, Name) :-
|
|
691
|
+
changed_symbol_missing_req(Symbol, MinLinks, Count),
|
|
692
|
+
( changed_symbol_loc(Symbol, FileRaw, Line, Col, NameRaw)
|
|
693
|
+
-> File = FileRaw, Name = NameRaw
|
|
694
|
+
; File = '', Line = 0, Col = 0, Name = ''
|
|
695
|
+
).
|