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.
- package/package.json +1 -1
- package/schema/entities.pl +18 -0
- package/schema/validation.pl +114 -4
- package/src/checks.pl +96 -2
- package/src/discovery.pl +461 -0
- package/src/kb.pl +444 -46
- package/src/status.pl +215 -0
package/src/kb.pl
CHANGED
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
kb_save/0,
|
|
7
7
|
with_kb_mutex/1,
|
|
8
8
|
kb_assert_entity/2,
|
|
9
|
+
kb_assert_entity_no_audit/2,
|
|
10
|
+
kb_log_entity_upsert/2,
|
|
9
11
|
kb_retract_entity/1,
|
|
10
12
|
kb_entity/3,
|
|
11
13
|
kb_entities_by_source/2,
|
|
12
14
|
kb_assert_relationship/4,
|
|
15
|
+
kb_assert_relationship_no_audit/4,
|
|
16
|
+
kb_log_relationship_upsert/4,
|
|
13
17
|
kb_relationship/3,
|
|
14
18
|
transitively_implements/2,
|
|
15
19
|
transitively_depends/2,
|
|
@@ -27,6 +31,7 @@
|
|
|
27
31
|
deprecated_no_successor/1,
|
|
28
32
|
symbol_no_req_coverage/2,
|
|
29
33
|
contradicting_reqs/3,
|
|
34
|
+
check_req_contradiction/1,
|
|
30
35
|
normalize_term_atom/2,
|
|
31
36
|
changeset/4, % Export for testing
|
|
32
37
|
kb_uri/1
|
|
@@ -59,16 +64,15 @@ kb_uri('urn-kibi:').
|
|
|
59
64
|
:- dynamic kb_attached/1.
|
|
60
65
|
:- dynamic kb_audit_db/1.
|
|
61
66
|
:- dynamic kb_graph/1.
|
|
67
|
+
:- dynamic kb_attached_snapshot/1.
|
|
62
68
|
:- dynamic entity/4. % Support legacy .pl file format (Type, Id, Title, Props)
|
|
63
69
|
|
|
64
70
|
%% kb_attach(+Directory)
|
|
65
71
|
% Attach to a KB directory with RDF persistence and file locking.
|
|
66
72
|
% Creates directory if it doesn't exist.
|
|
67
73
|
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
74
|
( kb_attached(_)
|
|
71
|
-
->
|
|
75
|
+
-> throw(error(permission_error(attach, kb, Directory), kb_attach/1))
|
|
72
76
|
; true
|
|
73
77
|
),
|
|
74
78
|
% Ensure directory exists
|
|
@@ -89,6 +93,7 @@ kb_attach(Directory) :-
|
|
|
89
93
|
-> rdf_load(DataFile, [graph(GraphURI), silent(true)])
|
|
90
94
|
; true
|
|
91
95
|
),
|
|
96
|
+
current_data_stamp(DataFile, SnapshotStamp),
|
|
92
97
|
% Set up audit log - only attach if not already attached
|
|
93
98
|
atom_concat(Directory, '/audit.log', AuditLog),
|
|
94
99
|
( db_attached(AuditLog)
|
|
@@ -99,55 +104,114 @@ kb_attach(Directory) :-
|
|
|
99
104
|
assert(kb_attached(Directory)),
|
|
100
105
|
assert(kb_audit_db(AuditLog)),
|
|
101
106
|
assert(kb_graph(GraphURI)),
|
|
107
|
+
assert(kb_attached_snapshot(SnapshotStamp)),
|
|
102
108
|
|
|
103
109
|
% Load legacy .pl entity files if present
|
|
104
110
|
load_kb_pl_files(Directory).
|
|
105
111
|
|
|
106
112
|
|
|
107
113
|
%% kb_detach
|
|
108
|
-
% Safely detach from KB
|
|
114
|
+
% Safely detach from KB without persisting pending changes.
|
|
115
|
+
% Call kb_save/0 explicitly before kb_detach/0 when durability is required.
|
|
116
|
+
% implements REQ-009
|
|
109
117
|
kb_detach :-
|
|
110
118
|
( kb_attached(_Directory)
|
|
111
119
|
-> (
|
|
112
|
-
kb_save,
|
|
113
120
|
% Unload RDF graph from memory to prevent duplication on reattach
|
|
114
121
|
( kb_graph(GraphURI)
|
|
115
122
|
-> rdf_unload_graph(GraphURI)
|
|
116
123
|
; true
|
|
117
124
|
),
|
|
118
|
-
|
|
119
|
-
(
|
|
120
|
-
-> db_sync(AuditLog)
|
|
125
|
+
( db_attached(_)
|
|
126
|
+
-> catch(db_detach, _, true)
|
|
121
127
|
; true
|
|
122
128
|
),
|
|
123
129
|
% Clear state
|
|
124
130
|
retractall(kb_attached(_)),
|
|
125
131
|
retractall(kb_audit_db(_)),
|
|
126
|
-
retractall(kb_graph(_))
|
|
132
|
+
retractall(kb_graph(_)),
|
|
133
|
+
retractall(kb_attached_snapshot(_))
|
|
127
134
|
)
|
|
128
135
|
; true
|
|
129
136
|
).
|
|
130
137
|
|
|
131
138
|
%% kb_save
|
|
132
139
|
% Save RDF graph and sync audit log to disk
|
|
140
|
+
% implements REQ-009
|
|
133
141
|
kb_save :-
|
|
134
142
|
( kb_attached(Directory)
|
|
135
|
-
-> (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
143
|
+
-> with_kb_mutex(with_kb_file_lock(Directory, kb_save_locked(Directory)))
|
|
144
|
+
; true
|
|
145
|
+
).
|
|
146
|
+
|
|
147
|
+
with_kb_file_lock(Directory, Goal) :-
|
|
148
|
+
atom_concat(Directory, '/kb.lock', LockFile),
|
|
149
|
+
setup_call_cleanup(
|
|
150
|
+
open(LockFile, append, LockStream, [lock(write)]),
|
|
151
|
+
call(Goal),
|
|
152
|
+
close(LockStream)
|
|
153
|
+
).
|
|
154
|
+
|
|
155
|
+
kb_save_locked(Directory) :-
|
|
156
|
+
atom_concat(Directory, '/kb.rdf', DataFile),
|
|
157
|
+
temp_rdf_file(Directory, TempFile),
|
|
158
|
+
catch(
|
|
159
|
+
(
|
|
160
|
+
ensure_snapshot_current(DataFile),
|
|
161
|
+
save_rdf_snapshot(TempFile),
|
|
162
|
+
rename_file(TempFile, DataFile),
|
|
163
|
+
current_data_stamp(DataFile, UpdatedStamp),
|
|
164
|
+
retractall(kb_attached_snapshot(_)),
|
|
165
|
+
assert(kb_attached_snapshot(UpdatedStamp)),
|
|
166
|
+
sync_audit_log
|
|
167
|
+
),
|
|
168
|
+
Error,
|
|
169
|
+
(
|
|
170
|
+
cleanup_temp_file(TempFile),
|
|
171
|
+
throw(Error)
|
|
172
|
+
)
|
|
173
|
+
).
|
|
174
|
+
|
|
175
|
+
temp_rdf_file(Directory, TempFile) :-
|
|
176
|
+
get_time(Timestamp),
|
|
177
|
+
Millis is floor(Timestamp * 1000),
|
|
178
|
+
( current_prolog_flag(pid, Pid)
|
|
179
|
+
-> true
|
|
180
|
+
; Pid = 0
|
|
181
|
+
),
|
|
182
|
+
format(atom(TempFile), '~w/kb.rdf.tmp.~w.~w', [Directory, Pid, Millis]).
|
|
183
|
+
|
|
184
|
+
save_rdf_snapshot(TargetFile) :-
|
|
185
|
+
( kb_graph(GraphURI)
|
|
186
|
+
-> rdf_save(TargetFile, [graph(GraphURI), base_uri('urn-kibi:'), namespaces([kb, xsd])])
|
|
187
|
+
; rdf_save(TargetFile, [base_uri('urn-kibi:'), namespaces([kb, xsd])])
|
|
188
|
+
).
|
|
189
|
+
|
|
190
|
+
sync_audit_log :-
|
|
191
|
+
( kb_audit_db(AuditLog)
|
|
192
|
+
-> db_sync(AuditLog)
|
|
193
|
+
; true
|
|
194
|
+
).
|
|
195
|
+
|
|
196
|
+
cleanup_temp_file(TempFile) :-
|
|
197
|
+
( exists_file(TempFile)
|
|
198
|
+
-> catch(delete_file(TempFile), _, true)
|
|
199
|
+
; true
|
|
200
|
+
).
|
|
201
|
+
|
|
202
|
+
current_data_stamp(DataFile, missing) :-
|
|
203
|
+
\+ exists_file(DataFile),
|
|
204
|
+
!.
|
|
205
|
+
current_data_stamp(DataFile, stamp(MTime, Size)) :-
|
|
206
|
+
time_file(DataFile, MTime),
|
|
207
|
+
size_file(DataFile, Size).
|
|
208
|
+
|
|
209
|
+
ensure_snapshot_current(DataFile) :-
|
|
210
|
+
( kb_attached_snapshot(ExpectedStamp)
|
|
211
|
+
-> current_data_stamp(DataFile, CurrentStamp),
|
|
212
|
+
( ExpectedStamp == CurrentStamp
|
|
213
|
+
-> true
|
|
214
|
+
; throw(error(permission_error(save, kb, stale_snapshot), kb_save/0))
|
|
151
215
|
)
|
|
152
216
|
; true
|
|
153
217
|
).
|
|
@@ -179,16 +243,24 @@ load_kb_pl_files(Directory) :-
|
|
|
179
243
|
).
|
|
180
244
|
|
|
181
245
|
%% kb_assert_entity(+Type, +Properties)
|
|
182
|
-
% Assert an entity into the KB with
|
|
246
|
+
% Assert an entity into the KB with audit logging.
|
|
183
247
|
% Properties is a list of Key=Value pairs.
|
|
184
248
|
kb_assert_entity(Type, Props) :-
|
|
249
|
+
kb_assert_entity_no_audit(Type, Props),
|
|
250
|
+
kb_log_entity_upsert(Type, Props).
|
|
251
|
+
|
|
252
|
+
%% kb_assert_entity_no_audit(+Type, +Properties)
|
|
253
|
+
% Assert an entity RDF payload without recording audit side effects.
|
|
254
|
+
% Used by write-gated MCP transactions so failed contradiction checks do not
|
|
255
|
+
% leave partial audit residue.
|
|
256
|
+
kb_assert_entity_no_audit(Type, Props) :-
|
|
185
257
|
% Validate entity
|
|
186
258
|
validate_entity(Type, Props),
|
|
187
259
|
% Extract ID
|
|
188
260
|
memberchk(id=Id, Props),
|
|
189
261
|
% Get current graph
|
|
190
262
|
kb_graph(Graph),
|
|
191
|
-
% Execute with mutex protection
|
|
263
|
+
% Execute RDF operations with mutex protection
|
|
192
264
|
with_kb_mutex((
|
|
193
265
|
% Create entity URI using prefix notation for namespace expansion
|
|
194
266
|
format(atom(EntityURI), 'kb:entity/~w', [Id]),
|
|
@@ -201,8 +273,14 @@ kb_assert_entity(Type, Props) :-
|
|
|
201
273
|
forall(
|
|
202
274
|
member(Key=Value, Props),
|
|
203
275
|
store_property(EntityURI, Key, Value, Graph)
|
|
204
|
-
)
|
|
205
|
-
|
|
276
|
+
)
|
|
277
|
+
)).
|
|
278
|
+
|
|
279
|
+
%% kb_log_entity_upsert(+Type, +Properties)
|
|
280
|
+
% Append the audit entry for a successfully committed entity upsert.
|
|
281
|
+
kb_log_entity_upsert(Type, Props) :-
|
|
282
|
+
memberchk(id=Id, Props),
|
|
283
|
+
with_kb_mutex((
|
|
206
284
|
get_time(Timestamp),
|
|
207
285
|
format_time(atom(TS), '%FT%T%:z', Timestamp),
|
|
208
286
|
assert_changeset(TS, upsert, Id, Type-Props)
|
|
@@ -288,7 +366,15 @@ source_value_atom(Value, Atom) :-
|
|
|
288
366
|
|
|
289
367
|
%% kb_assert_relationship(+Type, +From, +To, +Metadata)
|
|
290
368
|
% Assert a relationship between two entities with validation.
|
|
291
|
-
kb_assert_relationship(RelType, FromId, ToId,
|
|
369
|
+
kb_assert_relationship(RelType, FromId, ToId, Metadata) :-
|
|
370
|
+
kb_assert_relationship_no_audit(RelType, FromId, ToId, Metadata),
|
|
371
|
+
kb_log_relationship_upsert(RelType, FromId, ToId, Metadata).
|
|
372
|
+
|
|
373
|
+
%% kb_assert_relationship_no_audit(+Type, +From, +To, +Metadata)
|
|
374
|
+
% Assert a relationship RDF payload without recording audit side effects.
|
|
375
|
+
% Used by write-gated MCP transactions so failed contradiction checks do not
|
|
376
|
+
% leave partial audit residue.
|
|
377
|
+
kb_assert_relationship_no_audit(RelType, FromId, ToId, _Metadata) :-
|
|
292
378
|
kb_graph(Graph),
|
|
293
379
|
% Validate entities exist and relationship is valid
|
|
294
380
|
% Use once/1 to keep this predicate deterministic even if the store
|
|
@@ -296,7 +382,11 @@ kb_assert_relationship(RelType, FromId, ToId, _Metadata) :-
|
|
|
296
382
|
once(kb_entity(FromId, FromType, _)),
|
|
297
383
|
once(kb_entity(ToId, ToType, _)),
|
|
298
384
|
validate_relationship(RelType, FromType, ToType),
|
|
299
|
-
%
|
|
385
|
+
% NOTE: Strict-lane fact_kind pairing is validated at the MCP layer
|
|
386
|
+
% via validateStrictLanePairing() before the transaction begins.
|
|
387
|
+
% Prolog-level validation is deferred to avoid potential issues with
|
|
388
|
+
% rdf_transaction visibility and nondeterminism inside transactions.
|
|
389
|
+
% Execute RDF operations with mutex protection
|
|
300
390
|
with_kb_mutex((
|
|
301
391
|
% Create entity URIs
|
|
302
392
|
atom_concat('kb:entity/', FromId, FromURI),
|
|
@@ -307,14 +397,56 @@ kb_assert_relationship(RelType, FromId, ToId, _Metadata) :-
|
|
|
307
397
|
% Upsert semantics: ensure the exact triple isn't duplicated.
|
|
308
398
|
rdf_retractall(FromURI, RelURI, ToURI, Graph),
|
|
309
399
|
% Assert relationship triple
|
|
310
|
-
rdf_assert(FromURI, RelURI, ToURI, Graph)
|
|
311
|
-
|
|
400
|
+
rdf_assert(FromURI, RelURI, ToURI, Graph)
|
|
401
|
+
)).
|
|
402
|
+
|
|
403
|
+
%% kb_log_relationship_upsert(+Type, +From, +To, +Metadata)
|
|
404
|
+
% Append the audit entry for a successfully committed relationship upsert.
|
|
405
|
+
kb_log_relationship_upsert(RelType, FromId, ToId, _Metadata) :-
|
|
406
|
+
with_kb_mutex((
|
|
312
407
|
get_time(Timestamp),
|
|
313
408
|
format_time(atom(TS), '%FT%T%:z', Timestamp),
|
|
314
409
|
format(atom(RelId), '~w->~w', [FromId, ToId]),
|
|
315
410
|
assert_changeset(TS, upsert_rel, RelId, RelType-[from=FromId, to=ToId])
|
|
316
411
|
)).
|
|
317
412
|
|
|
413
|
+
%% validate_strict_lane_pairing(+RelType, +FromId, +ToId)
|
|
414
|
+
% Validate that constrains/requires_property relationships target facts
|
|
415
|
+
% with the correct fact_kind for strict-lane semantics.
|
|
416
|
+
% constrains targets must be subject facts (or legacy facts without fact_kind);
|
|
417
|
+
% requires_property targets must be property_value, observation, or meta facts
|
|
418
|
+
% (or legacy facts without fact_kind).
|
|
419
|
+
% implements REQ-011
|
|
420
|
+
validate_strict_lane_pairing(constrains, _FromId, ToId) :-
|
|
421
|
+
!,
|
|
422
|
+
( % Allow: no fact_kind (legacy), subject, observation, or meta
|
|
423
|
+
kb_entity(ToId, fact, Props),
|
|
424
|
+
( \+ memberchk(fact_kind=_, Props)
|
|
425
|
+
-> true % Legacy fact without fact_kind - allowed
|
|
426
|
+
; memberchk(fact_kind=KindRaw, Props),
|
|
427
|
+
normalize_term_atom(KindRaw, Kind),
|
|
428
|
+
memberchk(Kind, [subject, observation, meta])
|
|
429
|
+
)
|
|
430
|
+
-> true
|
|
431
|
+
; format(atom(Msg), 'constrains target ~w must be a subject, observation, or meta fact', [ToId]),
|
|
432
|
+
throw(error(validation_error(Msg), Msg))
|
|
433
|
+
).
|
|
434
|
+
validate_strict_lane_pairing(requires_property, _FromId, ToId) :-
|
|
435
|
+
!,
|
|
436
|
+
( % Allow: no fact_kind (legacy), property_value, observation, or meta
|
|
437
|
+
kb_entity(ToId, fact, Props),
|
|
438
|
+
( \+ memberchk(fact_kind=_, Props)
|
|
439
|
+
-> true % Legacy fact without fact_kind - allowed
|
|
440
|
+
; memberchk(fact_kind=KindRaw, Props),
|
|
441
|
+
normalize_term_atom(KindRaw, Kind),
|
|
442
|
+
memberchk(Kind, [property_value, observation, meta])
|
|
443
|
+
)
|
|
444
|
+
-> true
|
|
445
|
+
; format(atom(Msg), 'requires_property target ~w must be a property_value, observation, or meta fact', [ToId]),
|
|
446
|
+
throw(error(validation_error(Msg), Msg))
|
|
447
|
+
).
|
|
448
|
+
validate_strict_lane_pairing(_, _, _).
|
|
449
|
+
|
|
318
450
|
%% kb_relationship(?Type, ?From, ?To)
|
|
319
451
|
% Query relationships from the KB.
|
|
320
452
|
kb_relationship(RelType, FromId, ToId) :-
|
|
@@ -334,17 +466,30 @@ kb_relationship(RelType, FromId, ToId) :-
|
|
|
334
466
|
% Store a property as an RDF triple with appropriate datatype.
|
|
335
467
|
% All values are stored as typed string literals to avoid URI interpretation issues.
|
|
336
468
|
% Uses prefix notation (kb:Key) to enable proper namespace expansion.
|
|
469
|
+
% Typed fact fields (value_int, value_number, value_bool, closed_world) are stored
|
|
470
|
+
% with their appropriate XSD datatypes for round-trip preservation.
|
|
337
471
|
store_property(EntityURI, Key, Value, Graph) :-
|
|
338
472
|
% Build property URI using prefix notation for namespace expansion
|
|
339
473
|
format(atom(PropURI), 'kb:~w', [Key]),
|
|
340
|
-
%
|
|
341
|
-
value_to_literal(Value, Literal),
|
|
474
|
+
% Convert to literal with key-aware typing for typed fact fields
|
|
475
|
+
value_to_literal(Key, Value, Literal),
|
|
342
476
|
rdf_assert(EntityURI, PropURI, Literal, Graph).
|
|
343
477
|
|
|
344
|
-
%% value_to_literal(+Value, -Literal)
|
|
478
|
+
%% value_to_literal(+Key, +Value, -Literal)
|
|
345
479
|
% Convert Prolog value to RDF literal with appropriate datatype.
|
|
346
|
-
|
|
347
|
-
|
|
480
|
+
% Key-aware: typed fact fields get specific XSD datatypes.
|
|
481
|
+
value_to_literal(Key, Value, Literal) :-
|
|
482
|
+
% Typed fact fields with specific XSD datatypes
|
|
483
|
+
( Key == value_int, integer(Value)
|
|
484
|
+
-> Literal = Value^^'http://www.w3.org/2001/XMLSchema#integer'
|
|
485
|
+
; Key == value_number, number(Value)
|
|
486
|
+
-> Literal = Value^^'http://www.w3.org/2001/XMLSchema#decimal'
|
|
487
|
+
; Key == value_bool, (Value == true ; Value == false)
|
|
488
|
+
-> Literal = Value^^'http://www.w3.org/2001/XMLSchema#boolean'
|
|
489
|
+
; Key == closed_world, (Value == true ; Value == false)
|
|
490
|
+
-> Literal = Value^^'http://www.w3.org/2001/XMLSchema#boolean'
|
|
491
|
+
% Default: string or atom values as XSD string
|
|
492
|
+
; string(Value)
|
|
348
493
|
-> Literal = Value^^'http://www.w3.org/2001/XMLSchema#string'
|
|
349
494
|
; is_list(Value)
|
|
350
495
|
-> format(atom(ListStr), '~w', [Value]),
|
|
@@ -615,18 +760,217 @@ current_req(Id) :-
|
|
|
615
760
|
\+ kb_relationship(supersedes, _, Id).
|
|
616
761
|
|
|
617
762
|
%% contradicting_reqs(-ReqA, -ReqB, -Reason)
|
|
618
|
-
% Two current requirements contradict if they constrain
|
|
619
|
-
% but
|
|
763
|
+
% Two current requirements contradict if they constrain facts that have
|
|
764
|
+
% semantic conflicts (same subject/property but incompatible values or polarities).
|
|
765
|
+
% Checks in order of specificity: polarity conflicts, then value conflicts.
|
|
620
766
|
contradicting_reqs(ReqA, ReqB, Reason) :-
|
|
621
767
|
current_req(ReqA),
|
|
622
768
|
current_req(ReqB),
|
|
623
769
|
ReqA @< ReqB,
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
770
|
+
req_conflict(ReqA, ReqB, Reason).
|
|
771
|
+
|
|
772
|
+
%% ------------------------------------------------------------------
|
|
773
|
+
%% Semantic Contradiction Helpers (Task 4)
|
|
774
|
+
%% ------------------------------------------------------------------
|
|
775
|
+
|
|
776
|
+
%% req_conflict(+ReqA, +ReqB, -Reason)
|
|
777
|
+
% Detects conflicts between requirements via shared subject facts and
|
|
778
|
+
% incompatible property_value facts linked through requires_property.
|
|
779
|
+
req_conflict(ReqA, ReqB, Reason) :-
|
|
780
|
+
kb_relationship(constrains, ReqA, SubjectFactA),
|
|
781
|
+
kb_relationship(constrains, ReqB, SubjectFactB),
|
|
782
|
+
fact_subject_key(SubjectFactA, SubjectKey),
|
|
783
|
+
fact_subject_key(SubjectFactB, SubjectKey),
|
|
784
|
+
effective_req_property_fact(ReqA, SubjectKey, FactA, PropertyKey, OpA, ValTypeA, ValA, UnitA, ScopeA, PolarityA, ValidFromA, ValidToA),
|
|
785
|
+
effective_req_property_fact(ReqB, SubjectKey, FactB, PropertyKey, OpB, ValTypeB, ValB, UnitB, ScopeB, PolarityB, ValidFromB, ValidToB),
|
|
786
|
+
FactA \= FactB,
|
|
787
|
+
scope_intersects(ScopeA, ScopeB),
|
|
788
|
+
intervals_overlap(ValidFromA, ValidToA, ValidFromB, ValidToB),
|
|
789
|
+
( polarity_conflict(SubjectKey, PropertyKey, OpA, ValTypeA, ValA, UnitA, ScopeA, PolarityA,
|
|
790
|
+
OpB, ValTypeB, ValB, UnitB, ScopeB, PolarityB, Reason)
|
|
791
|
+
; property_conflict(SubjectKey, PropertyKey, OpA, ValTypeA, ValA, UnitA, PolarityA,
|
|
792
|
+
OpB, ValTypeB, ValB, UnitB, PolarityB, Reason)
|
|
793
|
+
).
|
|
794
|
+
|
|
795
|
+
%% fact_subject_key(+FactId, -SubjectKey)
|
|
796
|
+
% Extract the normalized subject key for strict subject facts.
|
|
797
|
+
fact_subject_key(FactId, SubjectKey) :-
|
|
798
|
+
kb_entity(FactId, fact, Props),
|
|
799
|
+
memberchk(fact_kind=KindRaw, Props),
|
|
800
|
+
normalize_term_atom(KindRaw, Kind),
|
|
801
|
+
Kind = subject,
|
|
802
|
+
memberchk(subject_key=SubjectRaw, Props),
|
|
803
|
+
normalize_term_atom(SubjectRaw, SubjectKey).
|
|
804
|
+
|
|
805
|
+
%% fact_property_tuple(+FactId, -Subject, -Property, -Op, -ValType, -Value, -Unit, -Scope, -Polarity)
|
|
806
|
+
% Extract typed property_value fact properties with defaults.
|
|
807
|
+
fact_property_tuple(FactId, Subject, Property, Op, ValType, Value, Unit, Scope, Polarity) :-
|
|
808
|
+
kb_entity(FactId, fact, Props),
|
|
809
|
+
memberchk(fact_kind=KindRaw, Props),
|
|
810
|
+
normalize_term_atom(KindRaw, Kind),
|
|
811
|
+
Kind = property_value,
|
|
812
|
+
memberchk(subject_key=SubjectRaw, Props),
|
|
813
|
+
normalize_term_atom(SubjectRaw, Subject),
|
|
814
|
+
memberchk(property_key=PropertyRaw, Props),
|
|
815
|
+
normalize_term_atom(PropertyRaw, Property),
|
|
816
|
+
( memberchk(operator=OpRaw, Props) -> normalize_term_atom(OpRaw, Op) ; Op = eq ),
|
|
817
|
+
( memberchk(value_type=ValTypeRaw, Props) -> normalize_term_atom(ValTypeRaw, ValType) ; ValType = string ),
|
|
818
|
+
value_from_props(Props, ValType, Value),
|
|
819
|
+
( memberchk(unit=UnitRaw, Props) -> normalize_term_atom(UnitRaw, Unit) ; Unit = '' ),
|
|
820
|
+
( memberchk(scope=ScopeRaw, Props) -> normalize_term_atom(ScopeRaw, Scope) ; Scope = '' ),
|
|
821
|
+
( memberchk(polarity=PolarityRaw, Props) -> normalize_term_atom(PolarityRaw, Polarity) ; Polarity = require ).
|
|
822
|
+
|
|
823
|
+
%% fact_valid_interval(+FactId, -ValidFrom, -ValidTo)
|
|
824
|
+
% Extract optional validity bounds for property_value facts.
|
|
825
|
+
fact_valid_interval(FactId, ValidFrom, ValidTo) :-
|
|
826
|
+
kb_entity(FactId, fact, Props),
|
|
827
|
+
( memberchk(valid_from=FromRaw, Props) -> normalize_term_atom(FromRaw, ValidFrom) ; ValidFrom = '' ),
|
|
828
|
+
( memberchk(valid_to=ToRaw, Props) -> normalize_term_atom(ToRaw, ValidTo) ; ValidTo = '' ).
|
|
829
|
+
|
|
830
|
+
%% effective_req_property(+ReqId, -SubjectKey, -PropertyKey, -Operator, -ValueType, -Value, -Unit, -Scope, -Polarity)
|
|
831
|
+
% Effective strict property constraint for a current requirement.
|
|
832
|
+
effective_req_property(ReqId, SubjectKey, PropertyKey, Operator, ValueType, Value, Unit, Scope, Polarity) :-
|
|
833
|
+
effective_req_property_fact(ReqId, SubjectKey, _FactId, PropertyKey, Operator, ValueType, Value, Unit, Scope, Polarity, _ValidFrom, _ValidTo).
|
|
834
|
+
|
|
835
|
+
%% effective_req_property_fact(+ReqId, -SubjectKey, -FactId, -PropertyKey, -Operator, -ValueType, -Value, -Unit, -Scope, -Polarity, -ValidFrom, -ValidTo)
|
|
836
|
+
% Internal helper retaining the source property fact and validity window.
|
|
837
|
+
effective_req_property_fact(ReqId, SubjectKey, FactId, PropertyKey, Operator, ValueType, Value, Unit, Scope, Polarity, ValidFrom, ValidTo) :-
|
|
838
|
+
kb_relationship(constrains, ReqId, SubjectFactId),
|
|
839
|
+
fact_subject_key(SubjectFactId, SubjectKey),
|
|
840
|
+
kb_relationship(requires_property, ReqId, FactId),
|
|
841
|
+
fact_property_tuple(FactId, PropertySubjectKey, PropertyKey, Operator, ValueType, Value, Unit, Scope, Polarity),
|
|
842
|
+
PropertySubjectKey = SubjectKey,
|
|
843
|
+
fact_valid_interval(FactId, ValidFrom, ValidTo).
|
|
844
|
+
|
|
845
|
+
%% value_from_props(+Props, +ValType, -Value)
|
|
846
|
+
% Extract the appropriate value field based on value_type.
|
|
847
|
+
% Handles RDF literal values (^^(Value, Type)) by unwrapping them.
|
|
848
|
+
value_from_props(Props, string, Value) :- memberchk(value_string=ValueRaw, Props), !, unwrap_rdf_value(ValueRaw, Value).
|
|
849
|
+
value_from_props(Props, int, Value) :- memberchk(value_int=ValueRaw, Props), !, unwrap_rdf_value(ValueRaw, Value).
|
|
850
|
+
value_from_props(Props, number, Value) :- memberchk(value_number=ValueRaw, Props), !, unwrap_rdf_value(ValueRaw, Value).
|
|
851
|
+
value_from_props(Props, bool, Value) :- memberchk(value_bool=ValueRaw, Props), !, unwrap_rdf_value(ValueRaw, Value).
|
|
852
|
+
value_from_props(_, _, '').
|
|
853
|
+
|
|
854
|
+
%% unwrap_rdf_value(+Raw, -Value)
|
|
855
|
+
% Unwrap RDF literal ^^(Value, Type) to raw value.
|
|
856
|
+
unwrap_rdf_value(^^(Value, _Type), Value) :- !.
|
|
857
|
+
unwrap_rdf_value(Value, Value).
|
|
858
|
+
|
|
859
|
+
%% polarity_conflict(..., -Reason)
|
|
860
|
+
% Detect require vs forbid only when the rest of the normalized tuple matches.
|
|
861
|
+
polarity_conflict(Subject, Property, OpA, TypeA, ValA, UnitA, ScopeA, require,
|
|
862
|
+
OpB, TypeB, ValB, UnitB, ScopeB, forbid, Reason) :-
|
|
863
|
+
compatible_types(TypeA, TypeB),
|
|
864
|
+
unit_compatible(UnitA, UnitB),
|
|
865
|
+
scope_intersects(ScopeA, ScopeB),
|
|
866
|
+
OpA == OpB,
|
|
867
|
+
same_value(TypeA, ValA, TypeB, ValB),
|
|
868
|
+
!,
|
|
869
|
+
format(atom(Reason), 'Polarity conflict on ~w.~w: ~w ~w vs forbid', [Subject, Property, OpA, ValA]).
|
|
870
|
+
polarity_conflict(Subject, Property, OpA, TypeA, ValA, UnitA, ScopeA, forbid,
|
|
871
|
+
OpB, TypeB, ValB, UnitB, ScopeB, require, Reason) :-
|
|
872
|
+
compatible_types(TypeA, TypeB),
|
|
873
|
+
unit_compatible(UnitA, UnitB),
|
|
874
|
+
scope_intersects(ScopeA, ScopeB),
|
|
875
|
+
OpA == OpB,
|
|
876
|
+
same_value(TypeA, ValA, TypeB, ValB),
|
|
877
|
+
!,
|
|
878
|
+
format(atom(Reason), 'Polarity conflict on ~w.~w: forbid vs ~w ~w', [Subject, Property, OpB, ValB]).
|
|
879
|
+
|
|
880
|
+
%% property_conflict(..., -Reason)
|
|
881
|
+
% Detect value conflicts between two property constraints on the same subject/property.
|
|
882
|
+
property_conflict(Subject, Property, OpA, TypeA, ValA, UnitA, Polarity,
|
|
883
|
+
OpB, TypeB, ValB, UnitB, Polarity, Reason) :-
|
|
884
|
+
unit_compatible(UnitA, UnitB),
|
|
885
|
+
compatible_types(TypeA, TypeB),
|
|
886
|
+
values_conflict(OpA, ValA, OpB, ValB, TypeA),
|
|
887
|
+
format(atom(Reason), 'Value conflict on ~w.~w: ~w ~w vs ~w ~w', [Subject, Property, OpA, ValA, OpB, ValB]).
|
|
888
|
+
|
|
889
|
+
%% compatible_types(+TypeA, +TypeB)
|
|
890
|
+
% Types are compatible if they are the same or both numeric.
|
|
891
|
+
compatible_types(T, T) :- !.
|
|
892
|
+
compatible_types(int, number) :- !.
|
|
893
|
+
compatible_types(number, int) :- !.
|
|
894
|
+
|
|
895
|
+
%% unit_compatible(+UnitA, +UnitB)
|
|
896
|
+
% Units are compatible when equal or when one side is unspecified.
|
|
897
|
+
unit_compatible('', _) :- !.
|
|
898
|
+
unit_compatible(_, '') :- !.
|
|
899
|
+
unit_compatible(Unit, Unit).
|
|
900
|
+
|
|
901
|
+
%% scope_intersects(+ScopeA, +ScopeB)
|
|
902
|
+
% Scopes intersect when equal or when one side is unspecified.
|
|
903
|
+
scope_intersects('', _) :- !.
|
|
904
|
+
scope_intersects(_, '') :- !.
|
|
905
|
+
scope_intersects(Scope, Scope).
|
|
906
|
+
|
|
907
|
+
%% intervals_overlap(+FromA, +ToA, +FromB, +ToB)
|
|
908
|
+
% Validity windows overlap unless one ends strictly before the other begins.
|
|
909
|
+
intervals_overlap(FromA, ToA, FromB, ToB) :-
|
|
910
|
+
\+ interval_ends_before(ToA, FromB),
|
|
911
|
+
\+ interval_ends_before(ToB, FromA).
|
|
912
|
+
|
|
913
|
+
interval_ends_before('', _) :-
|
|
914
|
+
fail.
|
|
915
|
+
interval_ends_before(_, '') :-
|
|
916
|
+
fail.
|
|
917
|
+
interval_ends_before(To, From) :-
|
|
918
|
+
To @< From.
|
|
919
|
+
|
|
920
|
+
%% same_value(+TypeA, +ValA, +TypeB, +ValB)
|
|
921
|
+
% Compare scalar values with numeric coercion for int/number pairs.
|
|
922
|
+
same_value(TypeA, ValA, TypeB, ValB) :-
|
|
923
|
+
( is_numeric_type(TypeA),
|
|
924
|
+
is_numeric_type(TypeB)
|
|
925
|
+
-> ValA =:= ValB
|
|
926
|
+
; ValA == ValB
|
|
927
|
+
).
|
|
928
|
+
|
|
929
|
+
%% values_conflict(+OpA, +ValA, +OpB, +ValB, +Type)
|
|
930
|
+
% Detect specific value conflicts based on operators and types.
|
|
931
|
+
|
|
932
|
+
% Exact value conflict: eq X vs eq Y where X \= Y
|
|
933
|
+
values_conflict(eq, ValA, eq, ValB, Type) :-
|
|
934
|
+
\+ same_value(Type, ValA, Type, ValB).
|
|
935
|
+
|
|
936
|
+
% Eq/neq conflict on the same scalar value.
|
|
937
|
+
values_conflict(eq, ValA, neq, ValB, Type) :-
|
|
938
|
+
same_value(Type, ValA, Type, ValB).
|
|
939
|
+
values_conflict(neq, ValA, eq, ValB, Type) :-
|
|
940
|
+
same_value(Type, ValA, Type, ValB).
|
|
941
|
+
|
|
942
|
+
% Numeric gap conflict: lte X vs gte Y where X < Y
|
|
943
|
+
values_conflict(lte, ValA, gte, ValB, Type) :-
|
|
944
|
+
is_numeric_type(Type),
|
|
945
|
+
ValA < ValB.
|
|
946
|
+
values_conflict(gte, ValB, lte, ValA, Type) :-
|
|
947
|
+
is_numeric_type(Type),
|
|
948
|
+
ValA < ValB.
|
|
949
|
+
|
|
950
|
+
% Also catch lt/gt variants
|
|
951
|
+
values_conflict(lt, ValA, gt, ValB, Type) :-
|
|
952
|
+
is_numeric_type(Type),
|
|
953
|
+
ValA =< ValB.
|
|
954
|
+
values_conflict(gt, ValB, lt, ValA, Type) :-
|
|
955
|
+
is_numeric_type(Type),
|
|
956
|
+
ValA =< ValB.
|
|
957
|
+
values_conflict(lt, ValA, gte, ValB, Type) :-
|
|
958
|
+
is_numeric_type(Type),
|
|
959
|
+
ValA =< ValB.
|
|
960
|
+
values_conflict(gte, ValB, lt, ValA, Type) :-
|
|
961
|
+
is_numeric_type(Type),
|
|
962
|
+
ValA =< ValB.
|
|
963
|
+
values_conflict(lte, ValA, gt, ValB, Type) :-
|
|
964
|
+
is_numeric_type(Type),
|
|
965
|
+
ValA < ValB.
|
|
966
|
+
values_conflict(gt, ValB, lte, ValA, Type) :-
|
|
967
|
+
is_numeric_type(Type),
|
|
968
|
+
ValA < ValB.
|
|
969
|
+
|
|
970
|
+
%% is_numeric_type(+Type)
|
|
971
|
+
% True for numeric value types.
|
|
972
|
+
is_numeric_type(int).
|
|
973
|
+
is_numeric_type(number).
|
|
630
974
|
|
|
631
975
|
normalize_term_atom(Val^^_Type, Atom) :-
|
|
632
976
|
!,
|
|
@@ -722,3 +1066,57 @@ changed_symbol_violation(Symbol, MinLinks, Count, File, Line, Col, Name) :-
|
|
|
722
1066
|
-> File = FileRaw, Name = NameRaw
|
|
723
1067
|
; File = '', Line = 0, Col = 0, Name = ''
|
|
724
1068
|
).
|
|
1069
|
+
|
|
1070
|
+
%% check_req_contradiction(+ReqId)
|
|
1071
|
+
% Validates that a requirement has no contradictions with current requirements.
|
|
1072
|
+
% Called within a transaction after asserting entity and relationships.
|
|
1073
|
+
% Throws error(kb_contradiction(Details)) if contradictions are found.
|
|
1074
|
+
% implements REQ-011
|
|
1075
|
+
% This predicate first checks if the new requirement supersedes the specific
|
|
1076
|
+
% conflicting requirement(s). Only direct supersedes edges from the new req
|
|
1077
|
+
% to the conflicting req allow the write - unrelated supersedes edges do not
|
|
1078
|
+
% mask conflicts.
|
|
1079
|
+
check_req_contradiction(ReqId) :-
|
|
1080
|
+
% Collect all contradictions involving this requirement
|
|
1081
|
+
findall(Reason-OtherReq, (
|
|
1082
|
+
contradicting_reqs(ReqId, OtherReq, Reason)
|
|
1083
|
+
; contradicting_reqs(OtherReq, ReqId, Reason)
|
|
1084
|
+
), AllPairs),
|
|
1085
|
+
AllPairs \= [],
|
|
1086
|
+
!,
|
|
1087
|
+
% Filter out contradictions where ReqId directly supersedes the conflicting requirement
|
|
1088
|
+
% Only the specific supersedes edge from new req -> conflicting req allows the write
|
|
1089
|
+
exclude(superseded_by_contradiction(ReqId), AllPairs, ValidPairs),
|
|
1090
|
+
ValidPairs \= [],
|
|
1091
|
+
!,
|
|
1092
|
+
% Build actionable error message
|
|
1093
|
+
build_contradiction_message(ReqId, ValidPairs, Message),
|
|
1094
|
+
throw(error(kb_contradiction(ValidPairs), Message)).
|
|
1095
|
+
check_req_contradiction(_) :-
|
|
1096
|
+
% No contradictions found
|
|
1097
|
+
true.
|
|
1098
|
+
|
|
1099
|
+
%% supersedes_mask(+ReqId, +Reason-OtherReq)
|
|
1100
|
+
% True when ReqId supersedes OtherReq, meaning this specific contradiction is allowed.
|
|
1101
|
+
% Only direct supersedes edges from the new requirement to the conflicting
|
|
1102
|
+
% requirement mask the conflict - unrelated supersedes edges do not.
|
|
1103
|
+
% implements REQ-011
|
|
1104
|
+
superseded_by_contradiction(ReqId, _-OtherReq) :-
|
|
1105
|
+
kb_relationship(supersedes, ReqId, OtherReq).
|
|
1106
|
+
|
|
1107
|
+
%% build_contradiction_message(+ReqId, +Pairs, -Message)
|
|
1108
|
+
% Build an actionable error message for contradictions.
|
|
1109
|
+
% Message includes conflicting req IDs, subject/property details, and remediation hints.
|
|
1110
|
+
% implements REQ-011
|
|
1111
|
+
build_contradiction_message(ReqId, Pairs, Message) :-
|
|
1112
|
+
format(atom(Header), 'Contradiction detected for requirement ~w:', [ReqId]),
|
|
1113
|
+
build_contradiction_details(Pairs, Details),
|
|
1114
|
+
Remedy = '\n\nTo resolve:\n 1. Add a supersedes relationship from the new requirement to the conflicting one, OR\n 2. Deprecate the conflicting requirement before creating the new one.',
|
|
1115
|
+
atom_concat(Header, Details, Temp),
|
|
1116
|
+
atom_concat(Temp, Remedy, Message).
|
|
1117
|
+
|
|
1118
|
+
build_contradiction_details([], '').
|
|
1119
|
+
build_contradiction_details([Reason-OtherReq|Rest], Details) :-
|
|
1120
|
+
format(atom(Line), '\n - Conflicts with ~w: ~w', [OtherReq, Reason]),
|
|
1121
|
+
build_contradiction_details(Rest, RestDetails),
|
|
1122
|
+
atom_concat(Line, RestDetails, Details).
|