kibi-core 0.2.0 → 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 +1 -1
- package/src/kb.pl +360 -21
- package/src/status.pl +14 -1
package/package.json
CHANGED
package/schema/entities.pl
CHANGED
|
@@ -29,6 +29,24 @@ entity_property(_, severity, atom).
|
|
|
29
29
|
entity_property(_, links, list).
|
|
30
30
|
entity_property(_, text_ref, uri).
|
|
31
31
|
|
|
32
|
+
% Typed fact fields - only valid for fact entities
|
|
33
|
+
entity_property(fact, fact_kind, atom).
|
|
34
|
+
entity_property(fact, subject_key, string).
|
|
35
|
+
entity_property(fact, property_key, string).
|
|
36
|
+
entity_property(fact, operator, atom).
|
|
37
|
+
entity_property(fact, value_type, atom).
|
|
38
|
+
entity_property(fact, value_string, string).
|
|
39
|
+
entity_property(fact, value_int, integer).
|
|
40
|
+
entity_property(fact, value_number, number).
|
|
41
|
+
entity_property(fact, value_bool, boolean).
|
|
42
|
+
entity_property(fact, unit, string).
|
|
43
|
+
entity_property(fact, scope, string).
|
|
44
|
+
entity_property(fact, polarity, atom).
|
|
45
|
+
entity_property(fact, closed_world, boolean).
|
|
46
|
+
entity_property(fact, valid_from, datetime).
|
|
47
|
+
entity_property(fact, valid_to, datetime).
|
|
48
|
+
entity_property(fact, canonical_key, string).
|
|
49
|
+
|
|
32
50
|
% Required properties for all entity types
|
|
33
51
|
required_property(Type, id) :- entity_type(Type).
|
|
34
52
|
required_property(Type, title) :- entity_type(Type).
|
package/schema/validation.pl
CHANGED
|
@@ -17,7 +17,9 @@ validate_entity(Type, Props) :-
|
|
|
17
17
|
% required properties present
|
|
18
18
|
forall(required_property(Type, P), memberchk(P=_Val, Props)),
|
|
19
19
|
% all properties have correct types
|
|
20
|
-
forall(member(Key=Val, Props), validate_property_type(Type, Key, Val))
|
|
20
|
+
forall(member(Key=Val, Props), validate_property_type(Type, Key, Val)),
|
|
21
|
+
% validate entity-specific shape constraints
|
|
22
|
+
validate_entity_shape(Type, Props).
|
|
21
23
|
|
|
22
24
|
% validate_relationship(+RelType, +From, +To)
|
|
23
25
|
% From and To are pairs Type=Id or structures type(Type) - allow Type or Type=Id
|
|
@@ -33,9 +35,8 @@ type_of(Type, Type) :- atom(Type), entity_type(Type), !.
|
|
|
33
35
|
type_of(Type=_Id, Type) :- atom(Type), entity_type(Type), !.
|
|
34
36
|
|
|
35
37
|
% validate_property_type(+EntityType, +Prop, +Value)
|
|
36
|
-
validate_property_type(
|
|
37
|
-
|
|
38
|
-
( entity_property(_Any, Prop, Kind) -> true ; Kind = atom ),
|
|
38
|
+
validate_property_type(Type, Prop, Value) :-
|
|
39
|
+
entity_property(Type, Prop, Kind),
|
|
39
40
|
check_kind(Kind, Value), !.
|
|
40
41
|
|
|
41
42
|
% check_kind(Kind, Value) succeeds if Value matches Kind
|
|
@@ -44,6 +45,115 @@ check_kind(string, V) :- string(V).
|
|
|
44
45
|
check_kind(datetime, V) :- string(V). % accept ISO strings for now
|
|
45
46
|
check_kind(list, V) :- is_list(V).
|
|
46
47
|
check_kind(uri, V) :- string(V).
|
|
48
|
+
check_kind(integer, V) :- integer(V).
|
|
49
|
+
check_kind(number, V) :- number(V).
|
|
50
|
+
check_kind(boolean, true).
|
|
51
|
+
check_kind(boolean, false).
|
|
47
52
|
|
|
48
53
|
% Fallback false
|
|
49
54
|
check_kind(_, _) :- fail.
|
|
55
|
+
|
|
56
|
+
% validate_entity_shape(+Type, +Props)
|
|
57
|
+
% Validates entity-specific shape constraints (e.g., strict fact shapes)
|
|
58
|
+
validate_entity_shape(fact, Props) :-
|
|
59
|
+
!,
|
|
60
|
+
valid_optional_fact_enums(Props),
|
|
61
|
+
valid_polarity_in_props(Props),
|
|
62
|
+
( memberchk(fact_kind=RawKind, Props) -> validate_fact_shape(RawKind, Props) ; true ).
|
|
63
|
+
validate_entity_shape(Type, Props) :-
|
|
64
|
+
Type \= fact,
|
|
65
|
+
% Non-fact entities cannot have fact-only fields
|
|
66
|
+
forall(member(Key=_Val, Props), \+ is_fact_only_field(Key)).
|
|
67
|
+
|
|
68
|
+
% is_fact_only_field(+Key) - true if Key is a fact-specific field
|
|
69
|
+
is_fact_only_field(fact_kind).
|
|
70
|
+
is_fact_only_field(subject_key).
|
|
71
|
+
is_fact_only_field(property_key).
|
|
72
|
+
is_fact_only_field(operator).
|
|
73
|
+
is_fact_only_field(value_type).
|
|
74
|
+
is_fact_only_field(value_string).
|
|
75
|
+
is_fact_only_field(value_int).
|
|
76
|
+
is_fact_only_field(value_number).
|
|
77
|
+
is_fact_only_field(value_bool).
|
|
78
|
+
is_fact_only_field(unit).
|
|
79
|
+
is_fact_only_field(scope).
|
|
80
|
+
is_fact_only_field(polarity).
|
|
81
|
+
is_fact_only_field(closed_world).
|
|
82
|
+
is_fact_only_field(valid_from).
|
|
83
|
+
is_fact_only_field(valid_to).
|
|
84
|
+
is_fact_only_field(canonical_key).
|
|
85
|
+
|
|
86
|
+
% validate_fact_shape(+Kind, +Props)
|
|
87
|
+
validate_fact_shape(subject, Props) :-
|
|
88
|
+
memberchk(subject_key=_Val, Props),
|
|
89
|
+
valid_optional_fact_enums(Props),
|
|
90
|
+
valid_polarity_in_props(Props).
|
|
91
|
+
validate_fact_shape(property_value, Props) :-
|
|
92
|
+
memberchk(subject_key=_Subject, Props),
|
|
93
|
+
memberchk(property_key=_Property, Props),
|
|
94
|
+
memberchk(operator=Op, Props),
|
|
95
|
+
valid_operator(Op),
|
|
96
|
+
memberchk(value_type=VT, Props),
|
|
97
|
+
valid_value_type(VT),
|
|
98
|
+
exactly_one_value_field(Props),
|
|
99
|
+
value_type_matches_field(VT, Props),
|
|
100
|
+
valid_optional_fact_enums(Props),
|
|
101
|
+
valid_polarity_in_props(Props).
|
|
102
|
+
validate_fact_shape(observation, Props) :-
|
|
103
|
+
valid_optional_fact_enums(Props),
|
|
104
|
+
valid_polarity_in_props(Props).
|
|
105
|
+
validate_fact_shape(meta, Props) :-
|
|
106
|
+
% Meta facts are allowed but don't require full strict property tuple yet
|
|
107
|
+
valid_optional_fact_enums(Props),
|
|
108
|
+
valid_polarity_in_props(Props).
|
|
109
|
+
validate_fact_shape(Kind, _Props) :-
|
|
110
|
+
% Unknown fact_kind values fail validation
|
|
111
|
+
\+ memberchk(Kind, [subject, property_value, observation, meta]),
|
|
112
|
+
fail.
|
|
113
|
+
|
|
114
|
+
% valid_operator(+Op)
|
|
115
|
+
valid_operator(eq).
|
|
116
|
+
valid_operator(neq).
|
|
117
|
+
valid_operator(lt).
|
|
118
|
+
valid_operator(lte).
|
|
119
|
+
valid_operator(gt).
|
|
120
|
+
valid_operator(gte).
|
|
121
|
+
|
|
122
|
+
% valid_value_type(+VT)
|
|
123
|
+
valid_value_type(string).
|
|
124
|
+
valid_value_type(int).
|
|
125
|
+
valid_value_type(number).
|
|
126
|
+
valid_value_type(bool).
|
|
127
|
+
|
|
128
|
+
% valid_polarity_in_props(+Props)
|
|
129
|
+
% Validates polarity if present; succeeds if no polarity in props
|
|
130
|
+
valid_polarity_in_props(Props) :-
|
|
131
|
+
( memberchk(polarity=P, Props) -> valid_polarity(P) ; true ).
|
|
132
|
+
|
|
133
|
+
% valid_optional_fact_enums(+Props)
|
|
134
|
+
% Validates enum-typed fact fields whenever they are present
|
|
135
|
+
valid_optional_fact_enums(Props) :-
|
|
136
|
+
( memberchk(operator=Op, Props) -> valid_operator(Op) ; true ),
|
|
137
|
+
( memberchk(value_type=VT, Props) -> valid_value_type(VT) ; true ).
|
|
138
|
+
|
|
139
|
+
% valid_polarity(+P)
|
|
140
|
+
valid_polarity(require).
|
|
141
|
+
valid_polarity(forbid).
|
|
142
|
+
|
|
143
|
+
% exactly_one_value_field(+Props)
|
|
144
|
+
exactly_one_value_field(Props) :-
|
|
145
|
+
findall(F, (member(F=_, Props), is_value_field(F)), Fields),
|
|
146
|
+
length(Fields, 1).
|
|
147
|
+
|
|
148
|
+
% is_value_field(+Field)
|
|
149
|
+
is_value_field(value_string).
|
|
150
|
+
is_value_field(value_int).
|
|
151
|
+
is_value_field(value_number).
|
|
152
|
+
is_value_field(value_bool).
|
|
153
|
+
|
|
154
|
+
% value_type_matches_field(+ValueType, +Props)
|
|
155
|
+
% Ensures that value_type matches the actual value field present
|
|
156
|
+
value_type_matches_field(string, Props) :- memberchk(value_string=_, Props), !.
|
|
157
|
+
value_type_matches_field(int, Props) :- memberchk(value_int=_, Props), !.
|
|
158
|
+
value_type_matches_field(number, Props) :- memberchk(value_number=_, Props), !.
|
|
159
|
+
value_type_matches_field(bool, Props) :- memberchk(value_bool=_, Props), !.
|
package/src/checks.pl
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
check_required_fields/1, % Returns list of missing required field violations
|
|
15
15
|
check_deprecated_adrs/1, % Returns list of deprecated ADR violations
|
|
16
16
|
check_domain_contradictions/1, % Returns list of contradiction violations
|
|
17
|
+
check_strict_fact_shape/1, % Returns list of malformed strict fact violations
|
|
17
18
|
run_checks_json/0 % Entry point for JSON output
|
|
18
19
|
]).
|
|
19
20
|
|
|
@@ -45,6 +46,7 @@ check_all(ViolationsDict) :-
|
|
|
45
46
|
check_required_fields(RequiredFields),
|
|
46
47
|
check_deprecated_adrs(DeprecatedADRs),
|
|
47
48
|
check_domain_contradictions(Contradictions),
|
|
49
|
+
check_strict_fact_shape(StrictFactShape),
|
|
48
50
|
ViolationsDict = _{
|
|
49
51
|
must_priority_coverage: MustPriority,
|
|
50
52
|
symbol_coverage: SymbolCoverage,
|
|
@@ -53,7 +55,8 @@ check_all(ViolationsDict) :-
|
|
|
53
55
|
no_cycles: Cycles,
|
|
54
56
|
required_fields: RequiredFields,
|
|
55
57
|
deprecated_adr_no_successor: DeprecatedADRs,
|
|
56
|
-
domain_contradictions: Contradictions
|
|
58
|
+
domain_contradictions: Contradictions,
|
|
59
|
+
strict_fact_shape: StrictFactShape
|
|
57
60
|
}.
|
|
58
61
|
|
|
59
62
|
%% check_must_priority_coverage(-Violations)
|
|
@@ -334,6 +337,95 @@ deprecated_adr_violation(violation(
|
|
|
334
337
|
; Source = ""
|
|
335
338
|
).
|
|
336
339
|
|
|
340
|
+
%% check_strict_fact_shape(-Violations)
|
|
341
|
+
% Finds all strict facts (with fact_kind) that have malformed shape.
|
|
342
|
+
% Only checks facts with fact_kind present; legacy facts without fact_kind are ignored.
|
|
343
|
+
% implements REQ-006
|
|
344
|
+
check_strict_fact_shape(Violations) :-
|
|
345
|
+
findall(
|
|
346
|
+
Violation,
|
|
347
|
+
strict_fact_shape_violation(Violation),
|
|
348
|
+
Violations0
|
|
349
|
+
),
|
|
350
|
+
sort(Violations0, Violations).
|
|
351
|
+
|
|
352
|
+
strict_fact_shape_violation(violation(
|
|
353
|
+
'strict-fact-shape',
|
|
354
|
+
FactId,
|
|
355
|
+
Description,
|
|
356
|
+
Suggestion,
|
|
357
|
+
Source
|
|
358
|
+
)) :-
|
|
359
|
+
kb_entity(FactId, fact, Props),
|
|
360
|
+
memberchk(fact_kind=RawKind, Props), % Only check facts with fact_kind
|
|
361
|
+
normalize_term_atom(RawKind, Kind), % Handle typed literals like ^^(subject, xsd:string)
|
|
362
|
+
|
|
363
|
+
% Check for malformed shape based on fact kind
|
|
364
|
+
( Kind = subject
|
|
365
|
+
-> ( memberchk(subject_key=_, Props)
|
|
366
|
+
-> fail % Well-formed, no violation
|
|
367
|
+
; Description = "Subject fact missing required field: subject_key",
|
|
368
|
+
Suggestion = "Add subject_key to define the subject domain key"
|
|
369
|
+
)
|
|
370
|
+
; Kind = property_value
|
|
371
|
+
-> findall(Msg, property_value_shape_error(Props, Msg), Errors),
|
|
372
|
+
( Errors = []
|
|
373
|
+
-> fail % Well-formed, no violation
|
|
374
|
+
; Errors = [First|_],
|
|
375
|
+
Description = First,
|
|
376
|
+
Suggestion = "Ensure property_value facts have subject_key, property_key, operator, value_type, and exactly one value field"
|
|
377
|
+
)
|
|
378
|
+
; Kind = observation
|
|
379
|
+
-> fail % Observation facts have no required fields beyond fact_kind
|
|
380
|
+
; Kind = meta
|
|
381
|
+
-> fail % Meta facts have no required fields beyond fact_kind
|
|
382
|
+
; % Unknown fact_kind - report as malformed
|
|
383
|
+
format(string(Description), "Unknown fact_kind: ~w", [Kind]),
|
|
384
|
+
Suggestion = "Use one of: subject, property_value, observation, meta"
|
|
385
|
+
),
|
|
386
|
+
|
|
387
|
+
( memberchk(source=Source0, Props)
|
|
388
|
+
-> normalize_term_atom(Source0, Source)
|
|
389
|
+
; Source = ""
|
|
390
|
+
).
|
|
391
|
+
|
|
392
|
+
% property_value_shape_error(+Props, -ErrorMsg)
|
|
393
|
+
% Returns an error message if the property_value fact has a shape error.
|
|
394
|
+
property_value_shape_error(Props, "Property value fact missing required field: subject_key") :-
|
|
395
|
+
\+ memberchk(subject_key=_, Props).
|
|
396
|
+
property_value_shape_error(Props, "Property value fact missing required field: property_key") :-
|
|
397
|
+
memberchk(subject_key=_, Props),
|
|
398
|
+
\+ memberchk(property_key=_, Props).
|
|
399
|
+
property_value_shape_error(Props, "Property value fact missing required field: operator") :-
|
|
400
|
+
memberchk(property_key=_, Props),
|
|
401
|
+
\+ memberchk(operator=_, Props).
|
|
402
|
+
property_value_shape_error(Props, "Property value fact missing required field: value_type") :-
|
|
403
|
+
memberchk(operator=_, Props),
|
|
404
|
+
\+ memberchk(value_type=_, Props).
|
|
405
|
+
property_value_shape_error(Props, "Property value fact missing value field (value_string, value_int, value_number, or value_bool)") :-
|
|
406
|
+
memberchk(value_type=_, Props),
|
|
407
|
+
\+ (memberchk(value_string=_, Props); memberchk(value_int=_, Props);
|
|
408
|
+
memberchk(value_number=_, Props); memberchk(value_bool=_, Props)).
|
|
409
|
+
property_value_shape_error(Props, "Property value fact has multiple value fields (should have exactly one)") :-
|
|
410
|
+
findall(F, (member(F=_, Props), is_value_field(F)), Fields),
|
|
411
|
+
length(Fields, Count),
|
|
412
|
+
Count > 1.
|
|
413
|
+
property_value_shape_error(Props, "Property value fact value_type does not match value field") :-
|
|
414
|
+
memberchk(value_type=VT, Props),
|
|
415
|
+
\+ value_type_matches_field(VT, Props).
|
|
416
|
+
|
|
417
|
+
% is_value_field(+Field)
|
|
418
|
+
is_value_field(value_string).
|
|
419
|
+
is_value_field(value_int).
|
|
420
|
+
is_value_field(value_number).
|
|
421
|
+
is_value_field(value_bool).
|
|
422
|
+
|
|
423
|
+
% value_type_matches_field(+ValueType, +Props)
|
|
424
|
+
value_type_matches_field(string, Props) :- memberchk(value_string=_, Props), !.
|
|
425
|
+
value_type_matches_field(int, Props) :- memberchk(value_int=_, Props), !.
|
|
426
|
+
value_type_matches_field(number, Props) :- memberchk(value_number=_, Props), !.
|
|
427
|
+
value_type_matches_field(bool, Props) :- memberchk(value_bool=_, Props), !.
|
|
428
|
+
|
|
337
429
|
%% check_domain_contradictions(-Violations)
|
|
338
430
|
% Finds all pairs of requirements with contradicting required properties.
|
|
339
431
|
check_domain_contradictions(Violations) :-
|
|
@@ -398,6 +490,7 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
|
|
|
398
490
|
check_required_fields(RequiredFields),
|
|
399
491
|
check_deprecated_adrs(DeprecatedADRs),
|
|
400
492
|
check_domain_contradictions(Contradictions),
|
|
493
|
+
check_strict_fact_shape(StrictFactShape),
|
|
401
494
|
ViolationsDict = _{
|
|
402
495
|
must_priority_coverage: MustPriority,
|
|
403
496
|
symbol_coverage: SymbolCoverage,
|
|
@@ -406,7 +499,8 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
|
|
|
406
499
|
no_cycles: Cycles,
|
|
407
500
|
required_fields: RequiredFields,
|
|
408
501
|
deprecated_adr_no_successor: DeprecatedADRs,
|
|
409
|
-
domain_contradictions: Contradictions
|
|
502
|
+
domain_contradictions: Contradictions,
|
|
503
|
+
strict_fact_shape: StrictFactShape
|
|
410
504
|
}.
|
|
411
505
|
|
|
412
506
|
%% violations_dict_to_json(+ViolationsDict, -JsonDict)
|
package/src/discovery.pl
CHANGED
|
@@ -369,7 +369,7 @@ edge_step(Current, Relationships, both, Edge, Next) :-
|
|
|
369
369
|
relationship_allowed([], Type) :-
|
|
370
370
|
relationship_type(Type).
|
|
371
371
|
relationship_allowed(Relationships, Type) :-
|
|
372
|
-
|
|
372
|
+
member(Type, Relationships).
|
|
373
373
|
|
|
374
374
|
keep_entity_type([], _Id).
|
|
375
375
|
keep_entity_type(EntityTypes, Id) :-
|
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
|
|
@@ -238,16 +243,24 @@ load_kb_pl_files(Directory) :-
|
|
|
238
243
|
).
|
|
239
244
|
|
|
240
245
|
%% kb_assert_entity(+Type, +Properties)
|
|
241
|
-
% Assert an entity into the KB with
|
|
246
|
+
% Assert an entity into the KB with audit logging.
|
|
242
247
|
% Properties is a list of Key=Value pairs.
|
|
243
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) :-
|
|
244
257
|
% Validate entity
|
|
245
258
|
validate_entity(Type, Props),
|
|
246
259
|
% Extract ID
|
|
247
260
|
memberchk(id=Id, Props),
|
|
248
261
|
% Get current graph
|
|
249
262
|
kb_graph(Graph),
|
|
250
|
-
% Execute with mutex protection
|
|
263
|
+
% Execute RDF operations with mutex protection
|
|
251
264
|
with_kb_mutex((
|
|
252
265
|
% Create entity URI using prefix notation for namespace expansion
|
|
253
266
|
format(atom(EntityURI), 'kb:entity/~w', [Id]),
|
|
@@ -260,8 +273,14 @@ kb_assert_entity(Type, Props) :-
|
|
|
260
273
|
forall(
|
|
261
274
|
member(Key=Value, Props),
|
|
262
275
|
store_property(EntityURI, Key, Value, Graph)
|
|
263
|
-
)
|
|
264
|
-
|
|
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((
|
|
265
284
|
get_time(Timestamp),
|
|
266
285
|
format_time(atom(TS), '%FT%T%:z', Timestamp),
|
|
267
286
|
assert_changeset(TS, upsert, Id, Type-Props)
|
|
@@ -347,7 +366,15 @@ source_value_atom(Value, Atom) :-
|
|
|
347
366
|
|
|
348
367
|
%% kb_assert_relationship(+Type, +From, +To, +Metadata)
|
|
349
368
|
% Assert a relationship between two entities with validation.
|
|
350
|
-
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) :-
|
|
351
378
|
kb_graph(Graph),
|
|
352
379
|
% Validate entities exist and relationship is valid
|
|
353
380
|
% Use once/1 to keep this predicate deterministic even if the store
|
|
@@ -355,7 +382,11 @@ kb_assert_relationship(RelType, FromId, ToId, _Metadata) :-
|
|
|
355
382
|
once(kb_entity(FromId, FromType, _)),
|
|
356
383
|
once(kb_entity(ToId, ToType, _)),
|
|
357
384
|
validate_relationship(RelType, FromType, ToType),
|
|
358
|
-
%
|
|
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
|
|
359
390
|
with_kb_mutex((
|
|
360
391
|
% Create entity URIs
|
|
361
392
|
atom_concat('kb:entity/', FromId, FromURI),
|
|
@@ -366,14 +397,56 @@ kb_assert_relationship(RelType, FromId, ToId, _Metadata) :-
|
|
|
366
397
|
% Upsert semantics: ensure the exact triple isn't duplicated.
|
|
367
398
|
rdf_retractall(FromURI, RelURI, ToURI, Graph),
|
|
368
399
|
% Assert relationship triple
|
|
369
|
-
rdf_assert(FromURI, RelURI, ToURI, Graph)
|
|
370
|
-
|
|
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((
|
|
371
407
|
get_time(Timestamp),
|
|
372
408
|
format_time(atom(TS), '%FT%T%:z', Timestamp),
|
|
373
409
|
format(atom(RelId), '~w->~w', [FromId, ToId]),
|
|
374
410
|
assert_changeset(TS, upsert_rel, RelId, RelType-[from=FromId, to=ToId])
|
|
375
411
|
)).
|
|
376
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
|
+
|
|
377
450
|
%% kb_relationship(?Type, ?From, ?To)
|
|
378
451
|
% Query relationships from the KB.
|
|
379
452
|
kb_relationship(RelType, FromId, ToId) :-
|
|
@@ -393,17 +466,30 @@ kb_relationship(RelType, FromId, ToId) :-
|
|
|
393
466
|
% Store a property as an RDF triple with appropriate datatype.
|
|
394
467
|
% All values are stored as typed string literals to avoid URI interpretation issues.
|
|
395
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.
|
|
396
471
|
store_property(EntityURI, Key, Value, Graph) :-
|
|
397
472
|
% Build property URI using prefix notation for namespace expansion
|
|
398
473
|
format(atom(PropURI), 'kb:~w', [Key]),
|
|
399
|
-
%
|
|
400
|
-
value_to_literal(Value, Literal),
|
|
474
|
+
% Convert to literal with key-aware typing for typed fact fields
|
|
475
|
+
value_to_literal(Key, Value, Literal),
|
|
401
476
|
rdf_assert(EntityURI, PropURI, Literal, Graph).
|
|
402
477
|
|
|
403
|
-
%% value_to_literal(+Value, -Literal)
|
|
478
|
+
%% value_to_literal(+Key, +Value, -Literal)
|
|
404
479
|
% Convert Prolog value to RDF literal with appropriate datatype.
|
|
405
|
-
|
|
406
|
-
|
|
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)
|
|
407
493
|
-> Literal = Value^^'http://www.w3.org/2001/XMLSchema#string'
|
|
408
494
|
; is_list(Value)
|
|
409
495
|
-> format(atom(ListStr), '~w', [Value]),
|
|
@@ -674,18 +760,217 @@ current_req(Id) :-
|
|
|
674
760
|
\+ kb_relationship(supersedes, _, Id).
|
|
675
761
|
|
|
676
762
|
%% contradicting_reqs(-ReqA, -ReqB, -Reason)
|
|
677
|
-
% Two current requirements contradict if they constrain
|
|
678
|
-
% 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.
|
|
679
766
|
contradicting_reqs(ReqA, ReqB, Reason) :-
|
|
680
767
|
current_req(ReqA),
|
|
681
768
|
current_req(ReqB),
|
|
682
769
|
ReqA @< ReqB,
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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).
|
|
689
974
|
|
|
690
975
|
normalize_term_atom(Val^^_Type, Atom) :-
|
|
691
976
|
!,
|
|
@@ -781,3 +1066,57 @@ changed_symbol_violation(Symbol, MinLinks, Count, File, Line, Col, Name) :-
|
|
|
781
1066
|
-> File = FileRaw, Name = NameRaw
|
|
782
1067
|
; File = '', Line = 0, Col = 0, Name = ''
|
|
783
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).
|
package/src/status.pl
CHANGED
|
@@ -15,6 +15,7 @@ kb_status_json(JsonString) :-
|
|
|
15
15
|
|
|
16
16
|
status_meta_dict(StatusDict) :-
|
|
17
17
|
attached_kb_info(Branch, KbPath, DataFile),
|
|
18
|
+
!,
|
|
18
19
|
snapshot_id(SnapshotId),
|
|
19
20
|
synced_at(DataFile, SyncedAt),
|
|
20
21
|
freshness_state(DataFile, Dirty, SyncState),
|
|
@@ -27,6 +28,18 @@ status_meta_dict(StatusDict) :-
|
|
|
27
28
|
kbPath: KbPath,
|
|
28
29
|
lastSyncSource: persisted
|
|
29
30
|
}.
|
|
31
|
+
status_meta_dict(StatusDict) :-
|
|
32
|
+
% Fallback for non-standard KB paths (e.g. temp dirs in tests)
|
|
33
|
+
( kb:kb_attached(KbPath) -> true ; KbPath = unknown ),
|
|
34
|
+
StatusDict = _{
|
|
35
|
+
branch: unknown,
|
|
36
|
+
snapshotId: unknown,
|
|
37
|
+
syncedAt: null,
|
|
38
|
+
dirty: false,
|
|
39
|
+
syncState: unknown,
|
|
40
|
+
kbPath: KbPath,
|
|
41
|
+
lastSyncSource: unknown
|
|
42
|
+
}.
|
|
30
43
|
|
|
31
44
|
attached_kb_info(Branch, KbPath, DataFile) :-
|
|
32
45
|
kb:kb_attached(KbPath),
|
|
@@ -81,7 +94,7 @@ workspace_source_changed(SnapshotTime) :-
|
|
|
81
94
|
),
|
|
82
95
|
!.
|
|
83
96
|
|
|
84
|
-
documentation_tree_changed(
|
|
97
|
+
documentation_tree_changed(_SnapshotTime) :-
|
|
85
98
|
attached_workspace_root(WorkspaceRoot),
|
|
86
99
|
documentation_markdown_untracked(WorkspaceRoot),
|
|
87
100
|
!.
|