kibi-core 0.26.1 → 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,
@@ -88,6 +90,8 @@
88
90
  :- use_module(library(aggregate), [aggregate_all/3]).
89
91
  :- use_module(library(lists), [sum_list/2, subtract/3, intersection/3]).
90
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]).
91
95
  :- use_module(library(solution_sequences), [distinct/1, limit/2, offset/2, order_by/2]).
92
96
  :- use_module('../schema/entities.pl', [entity_type/1, entity_property/3, required_property/2]).
93
97
  :- use_module('../schema/relationships.pl', [relationship_type/1, valid_relationship/3]).
@@ -140,6 +144,17 @@ kb_uri('urn-kibi:').
140
144
  :- dynamic kb_entity_props_memo/2.
141
145
  :- dynamic kb_entity_props_memo_key/1.
142
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.
143
158
  :- dynamic kb_index_verified_key/1.
144
159
  :- dynamic entity/4. % Support legacy .pl file format (Type, Id, Title, Props)
145
160
 
@@ -367,6 +382,17 @@ kb_detach :-
367
382
  retractall(kb_entity_props_memo(_, _)),
368
383
  retractall(kb_entity_props_memo_key(_)),
369
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(_, _, _)),
370
396
  retractall(kb_index_entity(_, _)),
371
397
  retractall(kb_index_type(_, _)),
372
398
  retractall(kb_index_tag(_, _)),
@@ -1698,10 +1724,7 @@ kb_query_entities(TypeFilter, IdFilter, Tags, SourceFilter, Limit, Offset, Rows,
1698
1724
  Offset >= 0,
1699
1725
  is_list(Tags),
1700
1726
  kb_ensure_indexes,
1701
- findall(Id,
1702
- indexed_entity_match(TypeFilter, IdFilter, Tags, SourceFilter, Id),
1703
- RawIds),
1704
- sort(RawIds, Ids),
1727
+ query_index_ids(TypeFilter, IdFilter, Tags, SourceFilter, Ids),
1705
1728
  length(Ids, Count),
1706
1729
  drop_index_ids(Offset, Ids, Remaining),
1707
1730
  take_index_ids(Limit, Remaining, PageIds),
@@ -1710,6 +1733,25 @@ kb_query_entities(TypeFilter, IdFilter, Tags, SourceFilter, Limit, Offset, Rows,
1710
1733
  kb_entity(Id, Type, Props) ),
1711
1734
  Rows).
1712
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
+
1713
1755
  %% kb_query_proof_contracts(+Id, +Limit, +Offset, -Rows)
1714
1756
  % Page proof-campaign candidates while reading only contract and binding RDF
1715
1757
  % properties. In particular, do not call kb_entity/3 here: that materializes
@@ -1848,19 +1890,37 @@ kb_search_entities(TypeFilter, Query, Limit, Offset, Rows, Count) :-
1848
1890
  kb_ensure_indexes,
1849
1891
  index_text_tokens(Query, QueryTokens),
1850
1892
  QueryTokens \= [],
1851
- findall(Id,
1852
- ( kb_index_entity(Id, _),
1853
- (TypeFilter == none -> true ; kb_index_type(TypeFilter, Id)),
1854
- ( indexed_markdown_source(Id)
1855
- ; forall(member(QueryToken, QueryTokens),
1856
- indexed_token_match(QueryToken, Id)) ) ),
1857
- RawIds),
1858
- sort(RawIds, Ids),
1893
+ search_index_ids(TypeFilter, QueryTokens, Ids),
1859
1894
  length(Ids, Count),
1860
1895
  drop_index_ids(Offset, Ids, Remaining),
1861
1896
  take_index_ids(Limit, Remaining, PageIds),
1862
1897
  search_candidate_rows(PageIds, Rows).
1863
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
+
1864
1924
  %% kb_list_search_candidates(+Type, +Limit, +Offset, -Rows, -Count)
1865
1925
  % Page every indexed entity (optionally of one type) as projected search
1866
1926
  % candidate rows, ordered by id. Semantic discovery uses it when no lexical
@@ -1978,6 +2038,25 @@ kb_assert_relationship(RelType, FromId, ToId, Metadata) :-
1978
2038
  kb_log_relationship_upsert(RelType, FromId, ToId, Metadata)
1979
2039
  ).
1980
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
+
1981
2060
  %% kb_assert_relationship_no_audit(+Type, +From, +To, +Metadata)
1982
2061
  % Assert a relationship RDF payload without recording audit side effects.
1983
2062
  % Used by write-gated MCP transactions so failed contradiction checks do not
@@ -2181,6 +2260,37 @@ kb_relationship_with_reluri(RelURI, Graph, FromId, ToId) :-
2181
2260
  entity_uri_to_id(FromURI, FromId),
2182
2261
  entity_uri_to_id(ToURI, ToId).
2183
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
+
2184
2294
  % Helper predicates
2185
2295
 
2186
2296
  %% entity_id_to_uri(+Id, -URI)
@@ -2639,13 +2749,7 @@ contradicting_reqs(ReqA, ReqB, Reason) :-
2639
2749
  % contradiction. This remains deliberately syntactic: differently shaped
2640
2750
  % terms are never treated as equivalent merely because their prose is similar.
2641
2751
  req_conflict_witness(ReqA, ReqB, Witness) :-
2642
- current_req(ReqA),
2643
- current_req(ReqB),
2644
- ReqA @< ReqB,
2645
- kb_relationship(constrains, ReqA, SubjectFactA),
2646
- kb_relationship(constrains, ReqB, SubjectFactB),
2647
- fact_subject_key(SubjectFactA, SubjectKey),
2648
- fact_subject_key(SubjectFactB, SubjectKey),
2752
+ subject_conflict_pair(ReqA, ReqB, SubjectKey),
2649
2753
  effective_req_property_fact(ReqA, SubjectKey, FactA, PropertyKey, OpA, ValTypeA, ValA, UnitA, ScopeA, PolarityA, ValidFromA, ValidToA),
2650
2754
  effective_req_property_fact(ReqB, SubjectKey, FactB, PropertyKey, OpB, ValTypeB, ValB, UnitB, ScopeB, PolarityB, ValidFromB, ValidToB),
2651
2755
  FactA \= FactB,
@@ -2673,11 +2777,7 @@ req_conflict_witness(ReqA, ReqB, Witness) :-
2673
2777
  right: Right
2674
2778
  }.
2675
2779
  req_conflict_witness(ReqA, ReqB, Witness) :-
2676
- current_req(ReqA),
2677
- current_req(ReqB),
2678
- ReqA @< ReqB,
2679
- effective_req_predicate(ReqA, FactA, Namespace, Name, Args, PolarityA),
2680
- effective_req_predicate(ReqB, FactB, Namespace, Name, Args, PolarityB),
2780
+ predicate_conflict_pair(ReqA, ReqB, FactA, FactB, Namespace, Name, Args, PolarityA, PolarityB),
2681
2781
  FactA \= FactB,
2682
2782
  opposite_predicate_polarity(PolarityA, PolarityB),
2683
2783
  atomic_list_concat(Args, ',', ArgsText),
@@ -2757,6 +2857,143 @@ req_conflict_witness(ReqA, ReqB, Witness) :-
2757
2857
  right: Right
2758
2858
  }.
2759
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
+
2760
2997
  %% kb_domain_contradiction_pairs_examined(-Count)
2761
2998
  % implements REQ-search-answer-hygiene
2762
2999
  % How many distinct pairs of requirement facts the domain-contradictions
@@ -2772,22 +3009,12 @@ kb_domain_contradiction_pairs_examined(Count) :-
2772
3009
  length(Pairs, Count).
2773
3010
 
2774
3011
  domain_contradiction_candidate_pair(FactA, FactB) :-
2775
- current_req(ReqA),
2776
- current_req(ReqB),
2777
- ReqA @< ReqB,
2778
- kb_relationship(constrains, ReqA, SubjectFactA),
2779
- kb_relationship(constrains, ReqB, SubjectFactB),
2780
- fact_subject_key(SubjectFactA, SubjectKey),
2781
- fact_subject_key(SubjectFactB, SubjectKey),
3012
+ subject_conflict_pair(ReqA, ReqB, SubjectKey),
2782
3013
  effective_req_property_fact(ReqA, SubjectKey, FactA, PropertyKey, _, _, _, _, _, _, _, _),
2783
3014
  effective_req_property_fact(ReqB, SubjectKey, FactB, PropertyKey, _, _, _, _, _, _, _, _),
2784
3015
  FactA \= FactB.
2785
3016
  domain_contradiction_candidate_pair(FactA, FactB) :-
2786
- current_req(ReqA),
2787
- current_req(ReqB),
2788
- ReqA @< ReqB,
2789
- effective_req_predicate(ReqA, FactA, Namespace, Name, Args, _),
2790
- effective_req_predicate(ReqB, FactB, Namespace, Name, Args, _),
3017
+ predicate_conflict_pair(_ReqA, _ReqB, FactA, FactB, _Namespace, _Name, _Args, _, _),
2791
3018
  FactA \= FactB.
2792
3019
  domain_contradiction_candidate_pair(FactA, FactB) :-
2793
3020
  decision_signatures(Signatures),
@@ -3023,7 +3250,32 @@ comparison_quantity(ValType, Value, Unit, Type, CanonValue, CanonUnit) :-
3023
3250
 
3024
3251
  %% predicate_schema(+FactId, -Namespace, -Name, -Arity, -ArgumentNames, -ArgumentTypes)
3025
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.
3026
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).
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) :-
3027
3279
  kb_entity(FactId, fact, Props),
3028
3280
  memberchk(fact_kind=KindRaw, Props),
3029
3281
  normalize_term_atom(KindRaw, predicate_schema),
@@ -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),