kibi-core 0.24.0 → 0.27.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/src/kb.pl CHANGED
@@ -27,6 +27,7 @@
27
27
  kb_indexed_sources/1,
28
28
  kb_query_entities/8,
29
29
  kb_search_entities/6,
30
+ kb_relationship_edges/3,
30
31
  kb_list_search_candidates/5,
31
32
  kb_list_search_candidates/6,
32
33
  kb_entity_ids/1,
@@ -34,6 +35,7 @@
34
35
  kb_entity/3,
35
36
  kb_entities_by_source/2,
36
37
  kb_assert_relationship/4,
38
+ kb_assert_relationships_reporting/2,
37
39
  kb_query_proof_contracts/4,
38
40
  kb_assert_relationship_no_audit/4,
39
41
  kb_log_relationship_upsert/4,
@@ -72,6 +74,7 @@
72
74
  check_req_contradiction/1,
73
75
  normalize_term_atom/2,
74
76
  normalize_term_atom_list/2,
77
+ normalize_literal_atom_list/2,
75
78
  changeset/4, % Export for testing
76
79
  kb_uri/1
77
80
  ]).
@@ -85,8 +88,10 @@
85
88
  :- use_module(library(sha), [sha_hash/3, hash_atom/2]).
86
89
  :- use_module(library(http/json)).
87
90
  :- use_module(library(aggregate), [aggregate_all/3]).
88
- :- use_module(library(lists), [sum_list/2]).
91
+ :- use_module(library(lists), [sum_list/2, subtract/3, intersection/3]).
89
92
  :- use_module(library(ordsets)).
93
+ :- use_module(library(assoc), [list_to_assoc/2, get_assoc/3]).
94
+ :- use_module(library(pairs), [group_pairs_by_key/2, pairs_values/2]).
90
95
  :- use_module(library(solution_sequences), [distinct/1, limit/2, offset/2, order_by/2]).
91
96
  :- use_module('../schema/entities.pl', [entity_type/1, entity_property/3, required_property/2]).
92
97
  :- use_module('../schema/relationships.pl', [relationship_type/1, valid_relationship/3]).
@@ -139,6 +144,17 @@ kb_uri('urn-kibi:').
139
144
  :- dynamic kb_entity_props_memo/2.
140
145
  :- dynamic kb_entity_props_memo_key/1.
141
146
  :- dynamic kb_decision_signature_memo/2.
147
+ :- dynamic kb_req_predicate_memo/2.
148
+ :- dynamic kb_req_subject_memo/2.
149
+ :- dynamic kb_contradiction_witness_memo/2.
150
+ :- dynamic kb_consistency_memo/3.
151
+ :- dynamic kb_predicate_schema_memo/2.
152
+ :- dynamic kb_requirement_proof_memo/3.
153
+ :- dynamic kb_requirement_ladder_memo/3.
154
+ :- dynamic kb_status_source_index_memo/2.
155
+ :- dynamic kb_scenario_feasibility_memo/3.
156
+ :- dynamic kb_search_ids_memo/4.
157
+ :- dynamic kb_query_ids_memo/3.
142
158
  :- dynamic kb_index_verified_key/1.
143
159
  :- dynamic entity/4. % Support legacy .pl file format (Type, Id, Title, Props)
144
160
 
@@ -366,6 +382,17 @@ kb_detach :-
366
382
  retractall(kb_entity_props_memo(_, _)),
367
383
  retractall(kb_entity_props_memo_key(_)),
368
384
  retractall(kb_decision_signature_memo(_, _)),
385
+ retractall(kb_req_predicate_memo(_, _)),
386
+ retractall(kb_req_subject_memo(_, _)),
387
+ retractall(kb_contradiction_witness_memo(_, _)),
388
+ retractall(kb_consistency_memo(_, _, _)),
389
+ retractall(kb_predicate_schema_memo(_, _)),
390
+ retractall(kb_requirement_proof_memo(_, _, _)),
391
+ retractall(kb_requirement_ladder_memo(_, _, _)),
392
+ retractall(kb_status_source_index_memo(_, _)),
393
+ retractall(kb_scenario_feasibility_memo(_, _, _)),
394
+ retractall(kb_search_ids_memo(_, _, _, _)),
395
+ retractall(kb_query_ids_memo(_, _, _)),
369
396
  retractall(kb_index_entity(_, _)),
370
397
  retractall(kb_index_type(_, _)),
371
398
  retractall(kb_index_tag(_, _)),
@@ -1697,10 +1724,7 @@ kb_query_entities(TypeFilter, IdFilter, Tags, SourceFilter, Limit, Offset, Rows,
1697
1724
  Offset >= 0,
1698
1725
  is_list(Tags),
1699
1726
  kb_ensure_indexes,
1700
- findall(Id,
1701
- indexed_entity_match(TypeFilter, IdFilter, Tags, SourceFilter, Id),
1702
- RawIds),
1703
- sort(RawIds, Ids),
1727
+ query_index_ids(TypeFilter, IdFilter, Tags, SourceFilter, Ids),
1704
1728
  length(Ids, Count),
1705
1729
  drop_index_ids(Offset, Ids, Remaining),
1706
1730
  take_index_ids(Limit, Remaining, PageIds),
@@ -1709,6 +1733,25 @@ kb_query_entities(TypeFilter, IdFilter, Tags, SourceFilter, Limit, Offset, Rows,
1709
1733
  kb_entity(Id, Type, Props) ),
1710
1734
  Rows).
1711
1735
 
1736
+ % Clients read a large result in bounded pages, each page a separate query,
1737
+ % so the ids matched for the last filter are kept for its next pages instead
1738
+ % of matching and sorting the whole index again per page. Keyed like the
1739
+ % search memo, so any write starts a fresh match.
1740
+ % The caller ensured the indexes, so the entity count is known.
1741
+ query_index_ids(TypeFilter, IdFilter, Tags, SourceFilter, Ids) :-
1742
+ kb_index_entity_count(IndexCount),
1743
+ index_verification_key(IndexCount, Key),
1744
+ Filters = filters(TypeFilter, IdFilter, Tags, SourceFilter),
1745
+ ( kb_query_ids_memo(Key, Filters, Cached)
1746
+ -> Ids = Cached
1747
+ ; findall(Id,
1748
+ indexed_entity_match(TypeFilter, IdFilter, Tags, SourceFilter, Id),
1749
+ RawIds),
1750
+ sort(RawIds, Ids),
1751
+ retractall(kb_query_ids_memo(_, _, _)),
1752
+ assertz(kb_query_ids_memo(Key, Filters, Ids))
1753
+ ).
1754
+
1712
1755
  %% kb_query_proof_contracts(+Id, +Limit, +Offset, -Rows)
1713
1756
  % Page proof-campaign candidates while reading only contract and binding RDF
1714
1757
  % properties. In particular, do not call kb_entity/3 here: that materializes
@@ -1847,19 +1890,37 @@ kb_search_entities(TypeFilter, Query, Limit, Offset, Rows, Count) :-
1847
1890
  kb_ensure_indexes,
1848
1891
  index_text_tokens(Query, QueryTokens),
1849
1892
  QueryTokens \= [],
1850
- findall(Id,
1851
- ( kb_index_entity(Id, _),
1852
- (TypeFilter == none -> true ; kb_index_type(TypeFilter, Id)),
1853
- ( indexed_markdown_source(Id)
1854
- ; forall(member(QueryToken, QueryTokens),
1855
- indexed_token_match(QueryToken, Id)) ) ),
1856
- RawIds),
1857
- sort(RawIds, Ids),
1893
+ search_index_ids(TypeFilter, QueryTokens, Ids),
1858
1894
  length(Ids, Count),
1859
1895
  drop_index_ids(Offset, Ids, Remaining),
1860
1896
  take_index_ids(Limit, Remaining, PageIds),
1861
1897
  search_candidate_rows(PageIds, Rows).
1862
1898
 
1899
+ % Callers read a large candidate set page by page, each page a separate
1900
+ % query, so the ids matched for the last query are kept for its next pages
1901
+ % instead of scanning the token index again. The memo is keyed like the index
1902
+ % verification (graph, RDF generation and index size), so any write starts a
1903
+ % fresh scan.
1904
+ search_index_ids(TypeFilter, QueryTokens, Ids) :-
1905
+ ( kb_index_entity_count(IndexCount)
1906
+ -> true
1907
+ ; IndexCount = none
1908
+ ),
1909
+ index_verification_key(IndexCount, Key),
1910
+ ( kb_search_ids_memo(Key, TypeFilter, QueryTokens, Cached)
1911
+ -> Ids = Cached
1912
+ ; findall(Id,
1913
+ ( kb_index_entity(Id, _),
1914
+ (TypeFilter == none -> true ; kb_index_type(TypeFilter, Id)),
1915
+ ( indexed_markdown_source(Id)
1916
+ ; forall(member(QueryToken, QueryTokens),
1917
+ indexed_token_match(QueryToken, Id)) ) ),
1918
+ RawIds),
1919
+ sort(RawIds, Ids),
1920
+ retractall(kb_search_ids_memo(_, _, _, _)),
1921
+ assertz(kb_search_ids_memo(Key, TypeFilter, QueryTokens, Ids))
1922
+ ).
1923
+
1863
1924
  %% kb_list_search_candidates(+Type, +Limit, +Offset, -Rows, -Count)
1864
1925
  % Page every indexed entity (optionally of one type) as projected search
1865
1926
  % candidate rows, ordered by id. Semantic discovery uses it when no lexical
@@ -1977,6 +2038,25 @@ kb_assert_relationship(RelType, FromId, ToId, Metadata) :-
1977
2038
  kb_log_relationship_upsert(RelType, FromId, ToId, Metadata)
1978
2039
  ).
1979
2040
 
2041
+ %% kb_assert_relationships_reporting(+Relationships, -FailedPositions)
2042
+ % Assert each rel(Type, From, To) of Relationships with kb_assert_relationship/4
2043
+ % (empty metadata), in order, keeping every one that succeeds. FailedPositions
2044
+ % lists the 1-based positions of those that failed or raised; their writes are
2045
+ % rolled back with their own transaction. Sync uses it after a batch
2046
+ % transaction fails, so one dangling link no longer sends the whole batch
2047
+ % through one round trip per relationship.
2048
+ kb_assert_relationships_reporting(Relationships, FailedPositions) :-
2049
+ foldl(assert_relationship_reporting, Relationships, 1-[], _-Failed0),
2050
+ reverse(Failed0, FailedPositions).
2051
+
2052
+ assert_relationship_reporting(rel(RelType, FromId, ToId), Position-Failed0, Next-Failed) :-
2053
+ Next is Position + 1,
2054
+ % mutalog-disable-next-line literal: an unbound metadata entry is ignored like an empty list
2055
+ ( catch(kb_assert_relationship(RelType, FromId, ToId, []), _, fail)
2056
+ -> Failed = Failed0
2057
+ ; Failed = [Position|Failed0]
2058
+ ).
2059
+
1980
2060
  %% kb_assert_relationship_no_audit(+Type, +From, +To, +Metadata)
1981
2061
  % Assert a relationship RDF payload without recording audit side effects.
1982
2062
  % Used by write-gated MCP transactions so failed contradiction checks do not
@@ -2180,6 +2260,37 @@ kb_relationship_with_reluri(RelURI, Graph, FromId, ToId) :-
2180
2260
  entity_uri_to_id(FromURI, FromId),
2181
2261
  entity_uri_to_id(ToURI, ToId).
2182
2262
 
2263
+ %% kb_relationship_edges(+Types, +Ids, -Edges)
2264
+ % implements REQ-mcp-search-discovery
2265
+ % Every [Type, From, To] relationship of one of Types that starts or ends at
2266
+ % one of Ids (a set), in the order and with the multiplicity of
2267
+ % findall([T,F,O], (member(T, Types), kb_relationship(T, F, O),
2268
+ % (member(F, Ids) ; member(O, Ids))), Edges)
2269
+ % Search reads the graph around its results this way. Ends are compared as
2270
+ % URIs, so only the matching edges are converted back to ids rather than
2271
+ % every edge of every type.
2272
+ kb_relationship_edges(Types, Ids, Edges) :-
2273
+ kb_graph(Graph),
2274
+ kb_uri(BaseURI),
2275
+ atom_concat(BaseURI, 'entity/', Prefix),
2276
+ findall(URI,
2277
+ ( member(Id, Ids),
2278
+ ( atom_concat(Prefix, Id, URI)
2279
+ ; atom_concat('kb:entity/', Id, URI)
2280
+ ) ),
2281
+ URIs0),
2282
+ sort(URIs0, URIs),
2283
+ findall([Type, From, To],
2284
+ ( member(Type, Types),
2285
+ atom_concat(BaseURI, Type, RelURI),
2286
+ rdf(FromURI, RelURI, ToURI, Graph),
2287
+ ( ord_memberchk(FromURI, URIs)
2288
+ ; ord_memberchk(ToURI, URIs)
2289
+ ),
2290
+ entity_uri_to_id(FromURI, From),
2291
+ entity_uri_to_id(ToURI, To) ),
2292
+ Edges).
2293
+
2183
2294
  % Helper predicates
2184
2295
 
2185
2296
  %% entity_id_to_uri(+Id, -URI)
@@ -2638,13 +2749,7 @@ contradicting_reqs(ReqA, ReqB, Reason) :-
2638
2749
  % contradiction. This remains deliberately syntactic: differently shaped
2639
2750
  % terms are never treated as equivalent merely because their prose is similar.
2640
2751
  req_conflict_witness(ReqA, ReqB, Witness) :-
2641
- current_req(ReqA),
2642
- current_req(ReqB),
2643
- ReqA @< ReqB,
2644
- kb_relationship(constrains, ReqA, SubjectFactA),
2645
- kb_relationship(constrains, ReqB, SubjectFactB),
2646
- fact_subject_key(SubjectFactA, SubjectKey),
2647
- fact_subject_key(SubjectFactB, SubjectKey),
2752
+ subject_conflict_pair(ReqA, ReqB, SubjectKey),
2648
2753
  effective_req_property_fact(ReqA, SubjectKey, FactA, PropertyKey, OpA, ValTypeA, ValA, UnitA, ScopeA, PolarityA, ValidFromA, ValidToA),
2649
2754
  effective_req_property_fact(ReqB, SubjectKey, FactB, PropertyKey, OpB, ValTypeB, ValB, UnitB, ScopeB, PolarityB, ValidFromB, ValidToB),
2650
2755
  FactA \= FactB,
@@ -2672,11 +2777,7 @@ req_conflict_witness(ReqA, ReqB, Witness) :-
2672
2777
  right: Right
2673
2778
  }.
2674
2779
  req_conflict_witness(ReqA, ReqB, Witness) :-
2675
- current_req(ReqA),
2676
- current_req(ReqB),
2677
- ReqA @< ReqB,
2678
- effective_req_predicate(ReqA, FactA, Namespace, Name, Args, PolarityA),
2679
- effective_req_predicate(ReqB, FactB, Namespace, Name, Args, PolarityB),
2780
+ predicate_conflict_pair(ReqA, ReqB, FactA, FactB, Namespace, Name, Args, PolarityA, PolarityB),
2680
2781
  FactA \= FactB,
2681
2782
  opposite_predicate_polarity(PolarityA, PolarityB),
2682
2783
  atomic_list_concat(Args, ',', ArgsText),
@@ -2756,6 +2857,143 @@ req_conflict_witness(ReqA, ReqB, Witness) :-
2756
2857
  right: Right
2757
2858
  }.
2758
2859
 
2860
+ % Cost: the search answer layer and the upsert contradiction check ask for
2861
+ % the witnesses of one requirement with the other side unbound, and kb_check
2862
+ % asks for every pair. Enumerating every current requirement as the partner
2863
+ % and only then reading either side's facts cost a full requirement scan per
2864
+ % requirement (quadratic for kb_check). The pairings below return the same
2865
+ % solutions in the same order as that enumeration: partners come from an
2866
+ % index of the current requirements constraining each subject or stating
2867
+ % each predicate, visited in the order current_req/1 enumerates them, so a
2868
+ % requirement without such facts has no partners at all.
2869
+ subject_conflict_pair(ReqA, ReqB, SubjectKey) :-
2870
+ % The mode tests only pick the cheapest route: every route gives the
2871
+ % same solutions in the same order for the arguments it is given.
2872
+ % mutalog-disable-next-line negate,goal_true,goal_fail: routing only; the later branches give the same pairs
2873
+ ( nonvar(ReqA), nonvar(ReqB)
2874
+ -> current_req(ReqA),
2875
+ current_req(ReqB),
2876
+ ReqA @< ReqB
2877
+ % mutalog-disable-next-line negate,goal_true,goal_fail: routing only; the last branch gives the same pairs
2878
+ ; nonvar(ReqB)
2879
+ -> current_req(ReqB),
2880
+ req_subject_partners(ReqB, before, Partners),
2881
+ member(ReqA, Partners)
2882
+ ; current_req(ReqA),
2883
+ req_subject_partners(ReqA, after, Partners),
2884
+ member(ReqB, Partners)
2885
+ ),
2886
+ kb_relationship(constrains, ReqA, SubjectFactA),
2887
+ kb_relationship(constrains, ReqB, SubjectFactB),
2888
+ fact_subject_key(SubjectFactA, SubjectKey),
2889
+ fact_subject_key(SubjectFactB, SubjectKey).
2890
+
2891
+ %% req_subject_partners(+ReqId, +Side, -Partners)
2892
+ % Current requirements constraining a subject that ReqId constrains, in
2893
+ % current_req/1 order: those after ReqId in standard order (Side = after) or
2894
+ % before it (Side = before).
2895
+ req_subject_partners(ReqId, Side, Partners) :-
2896
+ req_subject_index(Index),
2897
+ findall(Position-Partner,
2898
+ ( req_subject_fact_key(ReqId, SubjectKey),
2899
+ get_assoc(SubjectKey, Index, Entries),
2900
+ member(Position-Partner, Entries),
2901
+ subject_partner_side(Side, ReqId, Partner) ),
2902
+ Partners0),
2903
+ sort(Partners0, Partners1),
2904
+ pairs_values(Partners1, Partners).
2905
+
2906
+ % mutalog-disable-next-line cut_insert: the comparison has at most one solution
2907
+ subject_partner_side(after, ReqId, Partner) :- ReqId @< Partner.
2908
+ subject_partner_side(before, ReqId, Partner) :- Partner @< ReqId.
2909
+
2910
+ req_subject_fact_key(ReqId, SubjectKey) :-
2911
+ % mutalog-disable-next-line goal_true: extra partners are dropped by the pair's own constrains check
2912
+ kb_relationship(constrains, ReqId, SubjectFactId),
2913
+ fact_subject_key(SubjectFactId, SubjectKey).
2914
+
2915
+ % Memoized per KB generation, like the predicate index below.
2916
+ req_subject_index(Index) :-
2917
+ index_verification_key(req_subjects, Key),
2918
+ ( kb_req_subject_memo(Key, Cached)
2919
+ -> Index = Cached
2920
+ ; findall(ReqId, current_req(ReqId), ReqIds),
2921
+ findall(SubjectKey-(Position-ReqId),
2922
+ ( nth1(Position, ReqIds, ReqId),
2923
+ req_subject_fact_key(ReqId, SubjectKey) ),
2924
+ Pairs0),
2925
+ sort(Pairs0, Pairs),
2926
+ group_pairs_by_key(Pairs, Grouped),
2927
+ list_to_assoc(Grouped, Computed),
2928
+ retractall(kb_req_subject_memo(_, _)),
2929
+ assertz(kb_req_subject_memo(Key, Computed)),
2930
+ Index = Computed
2931
+ ).
2932
+
2933
+ predicate_conflict_pair(ReqA, ReqB, FactA, FactB, Namespace, Name, Args, PolarityA, PolarityB) :-
2934
+ ( nonvar(ReqA), var(ReqB)
2935
+ -> current_req(ReqA),
2936
+ findall(Position-Partner,
2937
+ ( effective_req_predicate(ReqA, _, PartnerNs, PartnerName, PartnerArgs, _),
2938
+ current_req_predicate(PartnerNs-PartnerName-PartnerArgs, Position, Partner),
2939
+ ReqA @< Partner ),
2940
+ Partners0),
2941
+ sort(Partners0, Partners),
2942
+ member(_-ReqB, Partners)
2943
+ ; nonvar(ReqB), var(ReqA)
2944
+ -> current_req(ReqB),
2945
+ findall(Position-Partner,
2946
+ ( effective_req_predicate(ReqB, _, PartnerNs, PartnerName, PartnerArgs, _),
2947
+ current_req_predicate(PartnerNs-PartnerName-PartnerArgs, Position, Partner),
2948
+ Partner @< ReqB ),
2949
+ Partners0),
2950
+ sort(Partners0, Partners),
2951
+ member(_-ReqA, Partners)
2952
+ % mutalog-disable-next-line negate,goal_true,goal_fail: routing only; the last branch gives the same pairs
2953
+ ; nonvar(ReqA), nonvar(ReqB)
2954
+ -> current_req(ReqA),
2955
+ current_req(ReqB),
2956
+ ReqA @< ReqB
2957
+ ; current_req(ReqA),
2958
+ findall(Position-Partner,
2959
+ ( effective_req_predicate(ReqA, _, PartnerNs, PartnerName, PartnerArgs, _),
2960
+ current_req_predicate(PartnerNs-PartnerName-PartnerArgs, Position, Partner),
2961
+ ReqA @< Partner ),
2962
+ Partners0),
2963
+ sort(Partners0, Partners),
2964
+ member(_-ReqB, Partners)
2965
+ ),
2966
+ effective_req_predicate(ReqA, FactA, Namespace, Name, Args, PolarityA),
2967
+ effective_req_predicate(ReqB, FactB, Namespace, Name, Args, PolarityB).
2968
+
2969
+ %% current_req_predicate(+Signature, -Position, -ReqId)
2970
+ % A current requirement stating a predicate fact with Signature
2971
+ % (Namespace-Name-Args), and its position in current_req/1 order.
2972
+ current_req_predicate(Signature, Position, ReqId) :-
2973
+ req_predicate_index(Index),
2974
+ get_assoc(Signature, Index, Entries),
2975
+ member(Position-ReqId, Entries).
2976
+
2977
+ % Memoized per KB generation, like the decision signatures: reading a
2978
+ % predicate fact decodes its entity, and every lookup would otherwise read
2979
+ % the predicate facts of every requirement.
2980
+ req_predicate_index(Index) :-
2981
+ index_verification_key(req_predicates, Key),
2982
+ ( kb_req_predicate_memo(Key, Cached)
2983
+ -> Index = Cached
2984
+ ; findall(ReqId, current_req(ReqId), ReqIds),
2985
+ findall((Namespace-Name-Args)-(Position-ReqId),
2986
+ ( nth1(Position, ReqIds, ReqId),
2987
+ effective_req_predicate(ReqId, _, Namespace, Name, Args, _) ),
2988
+ Pairs0),
2989
+ sort(Pairs0, Pairs),
2990
+ group_pairs_by_key(Pairs, Grouped),
2991
+ list_to_assoc(Grouped, Computed),
2992
+ retractall(kb_req_predicate_memo(_, _)),
2993
+ assertz(kb_req_predicate_memo(Key, Computed)),
2994
+ Index = Computed
2995
+ ).
2996
+
2759
2997
  %% kb_domain_contradiction_pairs_examined(-Count)
2760
2998
  % implements REQ-search-answer-hygiene
2761
2999
  % How many distinct pairs of requirement facts the domain-contradictions
@@ -2771,22 +3009,12 @@ kb_domain_contradiction_pairs_examined(Count) :-
2771
3009
  length(Pairs, Count).
2772
3010
 
2773
3011
  domain_contradiction_candidate_pair(FactA, FactB) :-
2774
- current_req(ReqA),
2775
- current_req(ReqB),
2776
- ReqA @< ReqB,
2777
- kb_relationship(constrains, ReqA, SubjectFactA),
2778
- kb_relationship(constrains, ReqB, SubjectFactB),
2779
- fact_subject_key(SubjectFactA, SubjectKey),
2780
- fact_subject_key(SubjectFactB, SubjectKey),
3012
+ subject_conflict_pair(ReqA, ReqB, SubjectKey),
2781
3013
  effective_req_property_fact(ReqA, SubjectKey, FactA, PropertyKey, _, _, _, _, _, _, _, _),
2782
3014
  effective_req_property_fact(ReqB, SubjectKey, FactB, PropertyKey, _, _, _, _, _, _, _, _),
2783
3015
  FactA \= FactB.
2784
3016
  domain_contradiction_candidate_pair(FactA, FactB) :-
2785
- current_req(ReqA),
2786
- current_req(ReqB),
2787
- ReqA @< ReqB,
2788
- effective_req_predicate(ReqA, FactA, Namespace, Name, Args, _),
2789
- effective_req_predicate(ReqB, FactB, Namespace, Name, Args, _),
3017
+ predicate_conflict_pair(_ReqA, _ReqB, FactA, FactB, _Namespace, _Name, _Args, _, _),
2790
3018
  FactA \= FactB.
2791
3019
  domain_contradiction_candidate_pair(FactA, FactB) :-
2792
3020
  decision_signatures(Signatures),
@@ -3022,7 +3250,32 @@ comparison_quantity(ValType, Value, Unit, Type, CanonValue, CanonUnit) :-
3022
3250
 
3023
3251
  %% predicate_schema(+FactId, -Namespace, -Name, -Arity, -ArgumentNames, -ArgumentTypes)
3024
3252
  % Read one project-local ontology predicate schema fact.
3253
+ % Finding the schemas of a predicate scans every fact, and contradiction
3254
+ % witnesses, decision signatures and key lookups ask for them per candidate,
3255
+ % so the schema rows are memoized per KB generation (outside RDF
3256
+ % transactions), in the order the scan finds them.
3257
+ predicate_schema(FactId, Namespace, Name, Arity, ArgumentNames, ArgumentTypes) :-
3258
+ var(FactId),
3259
+ \+ rdf_active_transaction(_),
3260
+ !,
3261
+ predicate_schema_rows(Rows),
3262
+ member(schema(FactId, Namespace, Name, Arity, ArgumentNames, ArgumentTypes), Rows).
3025
3263
  predicate_schema(FactId, Namespace, Name, Arity, ArgumentNames, ArgumentTypes) :-
3264
+ predicate_schema_scan(FactId, Namespace, Name, Arity, ArgumentNames, ArgumentTypes).
3265
+
3266
+ predicate_schema_rows(Rows) :-
3267
+ index_verification_key(predicate_schemas, Key),
3268
+ ( kb_predicate_schema_memo(Key, Cached)
3269
+ -> Rows = Cached
3270
+ ; findall(schema(FactId, Namespace, Name, Arity, ArgumentNames, ArgumentTypes),
3271
+ predicate_schema_scan(FactId, Namespace, Name, Arity, ArgumentNames, ArgumentTypes),
3272
+ Computed),
3273
+ retractall(kb_predicate_schema_memo(_, _)),
3274
+ assertz(kb_predicate_schema_memo(Key, Computed)),
3275
+ Rows = Computed
3276
+ ).
3277
+
3278
+ predicate_schema_scan(FactId, Namespace, Name, Arity, ArgumentNames, ArgumentTypes) :-
3026
3279
  kb_entity(FactId, fact, Props),
3027
3280
  memberchk(fact_kind=KindRaw, Props),
3028
3281
  normalize_term_atom(KindRaw, predicate_schema),
@@ -3080,6 +3333,23 @@ normalize_term_atom_list(Raw, Atoms) :-
3080
3333
  !,
3081
3334
  maplist(normalize_term_atom, Value, Atoms).
3082
3335
 
3336
+ %% normalize_literal_atom_list(+Raw, -Atoms)
3337
+ % The members of a stored list as atoms, verbatim. Unlike
3338
+ % normalize_term_atom_list it keeps a member's '/' segments, so set members
3339
+ % such as rxjs/operators, GET /api/users or finance/de stay whole.
3340
+ normalize_literal_atom_list(Raw, Atoms) :-
3341
+ unwrap_rdf_value(Raw, Value),
3342
+ is_list(Value),
3343
+ maplist(normalize_literal_atom, Value, Atoms).
3344
+
3345
+ normalize_literal_atom(Raw, Atom) :-
3346
+ unwrap_rdf_value(Raw, Value),
3347
+ ( atom(Value) -> Atom = Value
3348
+ ; string(Value) -> atom_string(Atom, Value)
3349
+ ; number(Value) -> atom_number(Atom, Value)
3350
+ ; term_string(Value, String), atom_string(Atom, String)
3351
+ ).
3352
+
3083
3353
  normalize_term_integer(Raw, Integer) :-
3084
3354
  unwrap_rdf_value(Raw, Value),
3085
3355
  ( integer(Value)
@@ -3125,7 +3395,7 @@ value_from_props(Props, bool, Value) :- memberchk(value_bool=ValueRaw, Props), !
3125
3395
  % in any order compare equal.
3126
3396
  value_from_props(Props, set, Value) :-
3127
3397
  memberchk(value_set=ValueRaw, Props),
3128
- normalize_term_atom_list(ValueRaw, Atoms),
3398
+ normalize_literal_atom_list(ValueRaw, Atoms),
3129
3399
  !,
3130
3400
  sort(Atoms, Value).
3131
3401
  value_from_props(_, _, '').
@@ -3226,6 +3496,35 @@ same_value(TypeA, ValA, TypeB, ValB) :-
3226
3496
  values_conflict(eq, ValA, eq, ValB, Type) :-
3227
3497
  \+ same_value(Type, ValA, Type, ValB).
3228
3498
 
3499
+ % A set fact with includes says every member of its set has the property;
3500
+ % eq on a set says exactly those members do. Two includes facts always agree
3501
+ % (the members having the property include both sets); includes conflicts
3502
+ % with eq only when it names a member the eq set leaves out.
3503
+ % implements REQ-kibi-non-numeric-proof-kinds
3504
+ values_conflict(includes, Included, eq, Exact, set) :-
3505
+ \+ subtract(Included, Exact, []).
3506
+ values_conflict(eq, Exact, includes, Included, set) :-
3507
+ \+ subtract(Included, Exact, []).
3508
+
3509
+ % subset_of says every member with the property is in its set, excludes that
3510
+ % no member of its set has it. subset_of conflicts with a set that must have
3511
+ % the property (eq or includes) when that set names a member outside it;
3512
+ % excludes conflicts when the two sets share a member. subset_of and excludes
3513
+ % never conflict with each other or themselves: the empty set satisfies them.
3514
+ % implements REQ-kibi-source-proof-kinds
3515
+ values_conflict(subset_of, Allowed, Op, Required, set) :-
3516
+ memberchk(Op, [eq, includes]),
3517
+ \+ subtract(Required, Allowed, []).
3518
+ values_conflict(Op, Required, subset_of, Allowed, set) :-
3519
+ memberchk(Op, [eq, includes]),
3520
+ \+ subtract(Required, Allowed, []).
3521
+ values_conflict(excludes, Excluded, Op, Required, set) :-
3522
+ memberchk(Op, [eq, includes]),
3523
+ intersection(Excluded, Required, [_|_]).
3524
+ values_conflict(Op, Required, excludes, Excluded, set) :-
3525
+ memberchk(Op, [eq, includes]),
3526
+ intersection(Excluded, Required, [_|_]).
3527
+
3229
3528
  % Eq/neq conflict on the same scalar value.
3230
3529
  values_conflict(eq, ValA, neq, ValB, Type) :-
3231
3530
  same_value(Type, ValA, Type, ValB).
@@ -58,7 +58,22 @@ check_kb_consistency_unresolved(Violations) :-
58
58
  Unsorted),
59
59
  sort(Unsorted, Violations).
60
60
 
61
+ % Both rules read the same verification, so it is produced once per KB
62
+ % generation (recomputed inside an RDF transaction).
61
63
  consistency_verification(Bundle, Report) :-
64
+ ( \+ rdf_db:rdf_active_transaction(_),
65
+ kb:index_verification_key(kb_consistency, Key)
66
+ -> ( kb:kb_consistency_memo(Key, Bundle0, Report0)
67
+ -> Bundle = Bundle0,
68
+ Report = Report0
69
+ ; compute_consistency_verification(Bundle, Report),
70
+ retractall(kb:kb_consistency_memo(_, _, _)),
71
+ assertz(kb:kb_consistency_memo(Key, Bundle, Report))
72
+ )
73
+ ; compute_consistency_verification(Bundle, Report)
74
+ ).
75
+
76
+ compute_consistency_verification(Bundle, Report) :-
62
77
  kb_certificates(Produced),
63
78
  with_output_to(string(Json), json_write_dict(current_output, Produced, [width(0)])),
64
79
  open_string(Json, Stream),