kibi-core 0.12.0 → 0.14.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
@@ -14,6 +14,7 @@
14
14
  kb_assert_entity/2,
15
15
  kb_assert_entity_no_audit/2,
16
16
  kb_commit_upsert/5,
17
+ kb_commit_upsert_batch/2,
17
18
  kb_log_entity_upsert/3,
18
19
  kb_retract_entity/1,
19
20
  kb_retract_entity/3,
@@ -24,10 +25,13 @@
24
25
  kb_indexed_sources/1,
25
26
  kb_query_entities/8,
26
27
  kb_search_entities/6,
28
+ kb_list_search_candidates/5,
29
+ kb_entity_ids/1,
27
30
  kb_rebuild_indexes/0,
28
31
  kb_entity/3,
29
32
  kb_entities_by_source/2,
30
33
  kb_assert_relationship/4,
34
+ kb_query_proof_contracts/4,
31
35
  kb_assert_relationship_no_audit/4,
32
36
  kb_log_relationship_upsert/4,
33
37
  kb_relationship/3,
@@ -56,6 +60,7 @@
56
60
  symbol_no_req_coverage/2,
57
61
  predicate_schema/6,
58
62
  predicate_fact/5,
63
+ canonical_property_tuple/9,
59
64
  contradicting_reqs/3,
60
65
  req_conflict_witness/3,
61
66
  check_req_contradiction/1,
@@ -71,12 +76,15 @@
71
76
  :- use_module(library(thread)).
72
77
  :- use_module(library(filesex)).
73
78
  :- use_module(library(readutil)).
79
+ :- use_module(library(sha), [sha_hash/3, hash_atom/2]).
74
80
  :- use_module(library(http/json)).
75
81
  :- use_module(library(aggregate), [aggregate_all/3]).
76
82
  :- use_module(library(lists), [sum_list/2]).
77
83
  :- use_module(library(ordsets)).
84
+ :- use_module(library(solution_sequences), [distinct/1, limit/2, offset/2, order_by/2]).
78
85
  :- use_module('../schema/entities.pl', [entity_type/1, entity_property/3, required_property/2]).
79
86
  :- use_module('../schema/relationships.pl', [relationship_type/1, valid_relationship/3]).
87
+ :- use_module('units.pl', [canonical_quantity/6]).
80
88
  :- use_module('../schema/validation.pl', [validate_entity/2, validate_relationship/3]).
81
89
 
82
90
  % Constants
@@ -117,6 +125,13 @@ kb_uri('urn-kibi:').
117
125
  :- dynamic kb_index_symbol_coordinate/5.
118
126
  :- dynamic kb_index_entity_count/1.
119
127
  :- dynamic kb_index_triple_count/1.
128
+ % Read memo for per-entity property lists, valid only for the recorded
129
+ % Graph-rdf_generation key. rdf_generation/1 changes on every RDF
130
+ % modification (including inside transactions) and reverts on rollback, so
131
+ % the memo never serves a property list the store no longer holds.
132
+ :- dynamic kb_entity_props_memo/2.
133
+ :- dynamic kb_entity_props_memo_key/1.
134
+ :- dynamic kb_index_verified_key/1.
120
135
  :- dynamic entity/4. % Support legacy .pl file format (Type, Id, Title, Props)
121
136
 
122
137
  %% kb_attach(+Directory)
@@ -187,8 +202,22 @@ kb_attach_journaled(Directory) :-
187
202
  load_kb_pl_files(Directory),
188
203
  kb_rebuild_indexes.
189
204
 
205
+ % SWI-Prolog's rdf_db fails to write journals ("invalid term_t ... out of
206
+ % range") for graphs whose URI is roughly 230 characters or longer. Store
207
+ % paths inside deep workspaces reach that length, so those stores use a short
208
+ % digest-based graph URI; shorter paths keep the file:// URI they always had.
209
+ journal_expected_graph_uri(Directory, Expected) :-
210
+ atom_concat('file://', Directory, FileURI),
211
+ atom_length(FileURI, Length),
212
+ ( Length =< 200
213
+ -> Expected = FileURI
214
+ ; sha_hash(Directory, Hash, [algorithm(sha1)]),
215
+ hash_atom(Hash, Digest),
216
+ atom_concat('urn:kibi:store:', Digest, Expected)
217
+ ).
218
+
190
219
  journal_graph_uri(Directory, GraphURI) :-
191
- atom_concat('file://', Directory, Expected),
220
+ journal_expected_graph_uri(Directory, Expected),
192
221
  ( rdf_graph(Expected)
193
222
  -> GraphURI = Expected
194
223
  ; findall(G,
@@ -324,6 +353,9 @@ kb_detach :-
324
353
  retractall(kb_dirty),
325
354
  retractall(kb_index_ready),
326
355
  retractall(kb_index_dirty),
356
+ retractall(kb_index_verified_key(_)),
357
+ retractall(kb_entity_props_memo(_, _)),
358
+ retractall(kb_entity_props_memo_key(_)),
327
359
  retractall(kb_index_entity(_, _)),
328
360
  retractall(kb_index_type(_, _)),
329
361
  retractall(kb_index_tag(_, _)),
@@ -1089,6 +1121,62 @@ kb_maybe_check_req_contradiction(req, Id, false) :-
1089
1121
  check_req_contradiction(Id).
1090
1122
  kb_maybe_check_req_contradiction(_, _, _).
1091
1123
 
1124
+ % Stage one upsert inside an already-open transaction. The caller saves once.
1125
+ kb_stage_upsert(Type, Props, Relationships, SkipContradiction, ChangeKind) :-
1126
+ memberchk(id=Id, Props),
1127
+ memberchk(SkipContradiction, [true, false]),
1128
+ upsert_change_kind(Id, ChangeKind),
1129
+ kb_assert_entity_no_audit(Type, Props),
1130
+ kb_commit_relationships_no_audit(Relationships),
1131
+ kb_maybe_check_req_contradiction(Type, Id, SkipContradiction),
1132
+ kb_log_entity_upsert(ChangeKind, Type, Props),
1133
+ kb_commit_relationship_audits(Relationships).
1134
+
1135
+ kb_stage_upserts([], []).
1136
+ kb_stage_upserts([
1137
+ upsert(Type, Props, Relationships, SkipContradiction)|Rest
1138
+ ], [ChangeKind|Kinds]) :-
1139
+ kb_stage_upsert(Type, Props, Relationships, SkipContradiction, ChangeKind),
1140
+ kb_stage_upserts(Rest, Kinds).
1141
+
1142
+ %% kb_commit_upsert_batch(+Entries, -ChangeKinds)
1143
+ % implements REQ-core-atomic-upsert-persistence
1144
+ % Commit many upserts in one RDF transaction and one journal flush.
1145
+ % Entries are upsert(Type, Props, Relationships, SkipContradiction) terms.
1146
+ kb_commit_upsert_batch(Entries, ChangeKinds) :-
1147
+ kb_storage_mode(journaled),
1148
+ !,
1149
+ with_kb_mutex((
1150
+ ( kb_dirty -> WasDirty = true ; WasDirty = false ),
1151
+ catch(
1152
+ rdf_transaction((
1153
+ kb_stage(rdf_mutation),
1154
+ kb_stage_upserts(Entries, ChangeKinds)
1155
+ )),
1156
+ Error,
1157
+ ( (WasDirty == false -> retractall(kb_dirty) ; true),
1158
+ throw(Error)
1159
+ )
1160
+ ),
1161
+ kb_mark_dirty,
1162
+ kb_save_journaled
1163
+ )).
1164
+ kb_commit_upsert_batch(Entries, ChangeKinds) :-
1165
+ kb_attached(Directory),
1166
+ atom_concat(Directory, '/kb.rdf', DataFile),
1167
+ kb_stage(runtime),
1168
+ kb_runtime_diagnostic,
1169
+ kb_stage(lock),
1170
+ with_kb_file_lock(Directory, (
1171
+ audit_store_writable,
1172
+ ensure_snapshot_current(DataFile),
1173
+ rdf_transaction((
1174
+ kb_stage(rdf_mutation),
1175
+ kb_stage_upserts(Entries, ChangeKinds),
1176
+ kb_save_locked(Directory)
1177
+ ))
1178
+ )).
1179
+
1092
1180
  kb_commit_relationship_audits([]).
1093
1181
  kb_commit_relationship_audits([
1094
1182
  rel(RelType, FromId, ToId, _Metadata)|Rest
@@ -1324,6 +1412,42 @@ kb_entity(Id, Type, Props) :-
1324
1412
  % Raw RDF/entity facts are kept separate from the index wrapper. Rebuilding
1325
1413
  % an index must never recurse through the indexed query path.
1326
1414
  kb_entity_raw(Id, Type, Props) :-
1415
+ ( nonvar(Id)
1416
+ -> rdf_entity_solutions(Id, Solutions),
1417
+ member(Type-Props, Solutions)
1418
+ ; kb_entity_raw_rdf(Id, Type, Props)
1419
+ ).
1420
+
1421
+ % Fallback: read from legacy entity/4 facts loaded from .pl files
1422
+ kb_entity_raw(Id, Type, Props) :-
1423
+ entity(Type, Id, _Title, PropList),
1424
+ convert_legacy_props(PropList, Props).
1425
+
1426
+ %% rdf_entity_solutions(+Id, -Solutions)
1427
+ % Coverage, proof, and check predicates look up the same entities tens of
1428
+ % thousands of times, and every lookup materializes and decodes every
1429
+ % property (including receipt histories). Memoize the decoded Type-Props
1430
+ % solutions per entity for the current graph and RDF generation.
1431
+ rdf_entity_solutions(Id, Solutions) :-
1432
+ ( kb_graph(Graph)
1433
+ -> rdf_generation(Generation),
1434
+ Key = Graph-Generation,
1435
+ ( kb_entity_props_memo_key(Current),
1436
+ Current == Key
1437
+ -> true
1438
+ ; retractall(kb_entity_props_memo(_, _)),
1439
+ retractall(kb_entity_props_memo_key(_)),
1440
+ assertz(kb_entity_props_memo_key(Key))
1441
+ ),
1442
+ ( kb_entity_props_memo(Id, Cached)
1443
+ -> Solutions = Cached
1444
+ ; findall(Type-Props, kb_entity_raw_rdf(Id, Type, Props), Solutions),
1445
+ assertz(kb_entity_props_memo(Id, Solutions))
1446
+ )
1447
+ ; Solutions = []
1448
+ ).
1449
+
1450
+ kb_entity_raw_rdf(Id, Type, Props) :-
1327
1451
  kb_graph(Graph),
1328
1452
  % Find entity by pattern - use unquoted namespace term kb:type
1329
1453
  ( var(Id)
@@ -1344,20 +1468,37 @@ kb_entity_raw(Id, Type, Props) :-
1344
1468
  literal_to_value(Key, ValueLiteral, Value)
1345
1469
  ), Props).
1346
1470
 
1347
- % Fallback: read from legacy entity/4 facts loaded from .pl files
1348
- kb_entity_raw(Id, Type, Props) :-
1349
- entity(Type, Id, _Title, PropList),
1350
- convert_legacy_props(PropList, Props).
1351
-
1471
+ % Verifying the index counts every type triple in the store. Remember the
1472
+ % inputs of the last successful check (graph, RDF generation, legacy fact
1473
+ % count, and the index's own entity count) and skip the recount while all of
1474
+ % them are unchanged: the recount would then return the same answer. The
1475
+ % index count must be part of the key because incremental index updates are
1476
+ % plain Prolog facts that an RDF rollback does not undo, while the RDF
1477
+ % generation does revert.
1352
1478
  kb_ensure_indexes :-
1353
1479
  ( kb_index_ready,
1354
1480
  \+ kb_index_dirty,
1355
1481
  kb_index_entity_count(Expected),
1356
- current_entity_index_count(Expected)
1482
+ index_verification_key(Expected, Key),
1483
+ ( kb_index_verified_key(Verified),
1484
+ Verified == Key
1485
+ -> true
1486
+ ; current_entity_index_count(Expected),
1487
+ retractall(kb_index_verified_key(_)),
1488
+ assertz(kb_index_verified_key(Key))
1489
+ )
1357
1490
  -> true
1358
1491
  ; kb_rebuild_indexes
1359
1492
  ).
1360
1493
 
1494
+ index_verification_key(IndexCount, key(Graph, Generation, LegacyCount, IndexCount)) :-
1495
+ (kb_graph(Graph) -> true ; Graph = none),
1496
+ rdf_generation(Generation),
1497
+ ( predicate_property(kb:entity(_, _, _, _), number_of_clauses(Count))
1498
+ -> LegacyCount = Count
1499
+ ; LegacyCount = 0
1500
+ ).
1501
+
1361
1502
  % Rebuild all acceleration structures from RDF/entity facts. This is
1362
1503
  % intentionally disposable: no query result is written back to RDF when an
1363
1504
  % index is stale or externally invalidated.
@@ -1380,6 +1521,7 @@ kb_rebuild_indexes :-
1380
1521
  assertz(kb_index_triple_count(TripleCount)),
1381
1522
  retractall(kb_index_dirty),
1382
1523
  retractall(kb_index_ready),
1524
+ retractall(kb_index_verified_key(_)),
1383
1525
  assertz(kb_index_ready)
1384
1526
  )).
1385
1527
 
@@ -1515,6 +1657,97 @@ kb_query_entities(TypeFilter, IdFilter, Tags, SourceFilter, Limit, Offset, Rows,
1515
1657
  kb_entity(Id, Type, Props) ),
1516
1658
  Rows).
1517
1659
 
1660
+ %% kb_query_proof_contracts(+Id, +Limit, +Offset, -Rows)
1661
+ % Page proof-campaign candidates while reading only contract and binding RDF
1662
+ % properties. In particular, do not call kb_entity/3 here: that materializes
1663
+ % complete entity properties, including potentially large receipt histories.
1664
+ % implements REQ-kibi-verification-evidence-contract
1665
+ kb_query_proof_contracts(IdFilter, Limit, Offset, Rows) :-
1666
+ integer(Limit),
1667
+ integer(Offset),
1668
+ Limit >= 0,
1669
+ Offset >= 0,
1670
+ findall([Id, test, Projected],
1671
+ ( limit(Limit,
1672
+ offset(Offset,
1673
+ order_by([asc(Id)],
1674
+ distinct(proof_contract_candidate(IdFilter, Id))))),
1675
+ proof_contract_properties(Id, Contract, Bindings, Source),
1676
+ optional_projected_property(proof_bindings, Bindings,
1677
+ BindingProps),
1678
+ optional_projected_property(source, Source, SourceProps),
1679
+ append([[id=Id, proof_contract=Contract],
1680
+ BindingProps, SourceProps],
1681
+ Projected) ),
1682
+ Rows).
1683
+
1684
+ optional_projected_property(_Key, Value, []) :-
1685
+ var(Value),
1686
+ !.
1687
+ optional_projected_property(Key, Value, [Key=Value]).
1688
+
1689
+ proof_contract_properties(Id, Contract, Bindings) :-
1690
+ proof_contract_properties(Id, Contract, Bindings, _Source).
1691
+
1692
+ % Source is projected alongside the contract so per-contract receipt binding
1693
+ % can hash the authored document without materializing receipt histories.
1694
+ proof_contract_properties(Id, Contract, Bindings, Source) :-
1695
+ ( ( kb_graph(Graph),
1696
+ entity_id_to_uri(Id, EntityURI),
1697
+ rdf(EntityURI, kb:type, TypeLiteral, Graph),
1698
+ literal_to_atom(TypeLiteral, test),
1699
+ kb_property_uri(proof_contract, ContractURI),
1700
+ rdf(EntityURI, ContractURI, ContractLiteral, Graph)
1701
+ )
1702
+ -> literal_to_value(proof_contract, ContractLiteral, Contract),
1703
+ ( kb_property_uri(proof_bindings, BindingsURI),
1704
+ rdf(EntityURI, BindingsURI, BindingsLiteral, Graph)
1705
+ -> literal_to_value(proof_bindings, BindingsLiteral, Bindings)
1706
+ ; true
1707
+ ),
1708
+ ( kb_property_uri(source, SourceURI),
1709
+ rdf(EntityURI, SourceURI, SourceLiteral, Graph)
1710
+ -> literal_to_value(source, SourceLiteral, Source)
1711
+ ; true
1712
+ )
1713
+ ; entity(test, Id, _, Properties),
1714
+ memberchk(proof_contract=Contract, Properties),
1715
+ ( memberchk(proof_bindings=Bindings, Properties)
1716
+ -> true
1717
+ ; true
1718
+ ),
1719
+ ( memberchk(source=Source, Properties)
1720
+ -> true
1721
+ ; true
1722
+ )
1723
+ ).
1724
+
1725
+ %% kb_property_uri(+Key, -URI)
1726
+ % Property predicates are asserted in-session as the prefixed atom 'kb:Key',
1727
+ % but a store reloaded from its snapshot carries the expanded 'urn-kibi:Key'
1728
+ % form. Direct RDF lookups must accept both (as uri_to_key/2 already does),
1729
+ % or projections silently miss every entity of a reloaded store.
1730
+ kb_property_uri(Key, URI) :-
1731
+ kb_uri(BaseURI),
1732
+ atom_concat(BaseURI, Key, URI).
1733
+ kb_property_uri(Key, URI) :-
1734
+ atom_concat('kb:', Key, URI).
1735
+
1736
+ % implements REQ-kibi-verification-evidence-contract
1737
+ proof_contract_candidate(none, Id) :-
1738
+ kb_graph(Graph),
1739
+ kb_property_uri(proof_contract, ContractURI),
1740
+ rdf(EntityURI, ContractURI, _, Graph),
1741
+ entity_uri_to_id(EntityURI, Id),
1742
+ rdf(EntityURI, kb:type, TypeLiteral, Graph),
1743
+ literal_to_atom(TypeLiteral, test).
1744
+ proof_contract_candidate(none, Id) :-
1745
+ entity(test, Id, _, Properties),
1746
+ memberchk(proof_contract=_, Properties).
1747
+ proof_contract_candidate(some(Id), Id) :-
1748
+ atom(Id),
1749
+ proof_contract_properties(Id, _, _).
1750
+
1518
1751
  indexed_entity_match(TypeFilter, IdFilter, Tags, SourceFilter, Id) :-
1519
1752
  ( IdFilter == none
1520
1753
  -> kb_index_entity(Id, _)
@@ -1573,10 +1806,74 @@ kb_search_entities(TypeFilter, Query, Limit, Offset, Rows, Count) :-
1573
1806
  length(Ids, Count),
1574
1807
  drop_index_ids(Offset, Ids, Remaining),
1575
1808
  take_index_ids(Limit, Remaining, PageIds),
1576
- findall([Id, Type, Props],
1577
- ( member(Id, PageIds), kb_entity(Id, Type, Props) ),
1809
+ search_candidate_rows(PageIds, Rows).
1810
+
1811
+ %% kb_list_search_candidates(+Type, +Limit, +Offset, -Rows, -Count)
1812
+ % Page every indexed entity (optionally of one type) as projected search
1813
+ % candidate rows, ordered by id. Semantic discovery uses it when no lexical
1814
+ % token can pre-filter the candidate set.
1815
+ kb_list_search_candidates(TypeFilter, Limit, Offset, Rows, Count) :-
1816
+ integer(Limit),
1817
+ integer(Offset),
1818
+ Limit >= 0,
1819
+ Offset >= 0,
1820
+ kb_ensure_indexes,
1821
+ findall(Id,
1822
+ ( kb_index_entity(Id, _),
1823
+ (TypeFilter == none -> true ; kb_index_type(TypeFilter, Id)) ),
1824
+ RawIds),
1825
+ sort(RawIds, Ids),
1826
+ length(Ids, Count),
1827
+ drop_index_ids(Offset, Ids, Remaining),
1828
+ take_index_ids(Limit, Remaining, PageIds),
1829
+ search_candidate_rows(PageIds, Rows).
1830
+
1831
+ %% kb_entity_ids(-Ids)
1832
+ % Every entity id, without materializing any properties.
1833
+ kb_entity_ids(Ids) :-
1834
+ kb_ensure_indexes,
1835
+ findall(Id, kb_index_entity(Id, _), RawIds),
1836
+ sort(RawIds, Ids).
1837
+
1838
+ % Search candidates carry only the properties ranking, snippets, and summary
1839
+ % output read. Large structured properties (receipt histories, proof
1840
+ % contracts, semantic inventories, rule IR) stay in the store: a candidate
1841
+ % page of receipt-bearing tests otherwise exceeds the bounded engine output.
1842
+ % Callers that need complete entities reload the final page by id.
1843
+ search_candidate_rows(PageIds, Rows) :-
1844
+ findall([Id, Type, Projected],
1845
+ ( member(Id, PageIds),
1846
+ kb_entity(Id, Type, Props),
1847
+ include(search_candidate_property, Props, Projected) ),
1578
1848
  Rows).
1579
1849
 
1850
+ search_candidate_property(Key=_) :-
1851
+ search_candidate_key(Key).
1852
+
1853
+ search_candidate_key(id).
1854
+ search_candidate_key(title).
1855
+ search_candidate_key(status).
1856
+ search_candidate_key(priority).
1857
+ search_candidate_key(severity).
1858
+ search_candidate_key(owner).
1859
+ search_candidate_key(tags).
1860
+ search_candidate_key(source).
1861
+ search_candidate_key(sourceFile).
1862
+ search_candidate_key(sourceLine).
1863
+ search_candidate_key(sourceColumn).
1864
+ search_candidate_key(sourceEndLine).
1865
+ search_candidate_key(sourceEndColumn).
1866
+ search_candidate_key(source_line).
1867
+ search_candidate_key(source_end_line).
1868
+ search_candidate_key(updated_at).
1869
+ search_candidate_key(created_at).
1870
+ search_candidate_key(semantic_text).
1871
+ search_candidate_key(text_ref).
1872
+ search_candidate_key(body).
1873
+ search_candidate_key(content).
1874
+ search_candidate_key(markdownBody).
1875
+ search_candidate_key(markdown_body).
1876
+
1580
1877
  indexed_token_match(QueryToken, Id) :-
1581
1878
  kb_index_token(IndexedToken, Id),
1582
1879
  sub_atom(IndexedToken, _, _, _, QueryToken),
@@ -2269,10 +2566,14 @@ req_conflict_witness(ReqA, ReqB, Witness) :-
2269
2566
  FactA \= FactB,
2270
2567
  scope_intersects(ScopeA, ScopeB),
2271
2568
  intervals_overlap(ValidFromA, ValidToA, ValidFromB, ValidToB),
2272
- ( polarity_conflict(SubjectKey, PropertyKey, OpA, ValTypeA, ValA, UnitA, ScopeA, PolarityA,
2273
- OpB, ValTypeB, ValB, UnitB, ScopeB, PolarityB, Reason)
2274
- ; property_conflict(SubjectKey, PropertyKey, OpA, ValTypeA, ValA, UnitA, PolarityA,
2275
- OpB, ValTypeB, ValB, UnitB, PolarityB, Reason)
2569
+ % Compare canonical quantities (30 min == 1800 s); witnesses keep the
2570
+ % authored values so evidence still points at the source text.
2571
+ canonical_quantity(ValTypeA, ValA, UnitA, CanonTypeA, CanonValA, CanonUnitA),
2572
+ canonical_quantity(ValTypeB, ValB, UnitB, CanonTypeB, CanonValB, CanonUnitB),
2573
+ ( polarity_conflict(SubjectKey, PropertyKey, OpA, CanonTypeA, CanonValA, CanonUnitA, ScopeA, PolarityA,
2574
+ OpB, CanonTypeB, CanonValB, CanonUnitB, ScopeB, PolarityB, Reason)
2575
+ ; property_conflict(SubjectKey, PropertyKey, OpA, CanonTypeA, CanonValA, CanonUnitA, PolarityA,
2576
+ OpB, CanonTypeB, CanonValB, CanonUnitB, PolarityB, Reason)
2276
2577
  ),
2277
2578
  property_conflict_side(ReqA, FactA, SubjectKey, PropertyKey, OpA, ValTypeA, ValA, UnitA, ScopeA, PolarityA, ValidFromA, ValidToA, Left),
2278
2579
  property_conflict_side(ReqB, FactB, SubjectKey, PropertyKey, OpB, ValTypeB, ValB, UnitB, ScopeB, PolarityB, ValidFromB, ValidToB, Right),
@@ -2418,6 +2719,14 @@ fact_property_tuple(FactId, Subject, Property, Op, ValType, Value, Unit, Scope,
2418
2719
  ( memberchk(scope=ScopeRaw, Props) -> normalize_term_atom(ScopeRaw, Scope) ; Scope = '' ),
2419
2720
  ( memberchk(polarity=PolarityRaw, Props) -> normalize_term_atom(PolarityRaw, Polarity) ; Polarity = require ).
2420
2721
 
2722
+ %% canonical_property_tuple(+FactId, -Subject, -Property, -Op, -ValType, -Value, -Unit, -Scope, -Polarity)
2723
+ % fact_property_tuple/9 with the quantity converted to its canonical base unit
2724
+ % (units.pl). Used only for comparison; the authored fact is never rewritten.
2725
+ % implements REQ-kibi-unit-canonicalization
2726
+ canonical_property_tuple(FactId, Subject, Property, Op, CanonType, CanonValue, CanonUnit, Scope, Polarity) :-
2727
+ fact_property_tuple(FactId, Subject, Property, Op, ValType, Value, Unit, Scope, Polarity),
2728
+ canonical_quantity(ValType, Value, Unit, CanonType, CanonValue, CanonUnit).
2729
+
2421
2730
  %% predicate_schema(+FactId, -Namespace, -Name, -Arity, -ArgumentNames, -ArgumentTypes)
2422
2731
  % Read one project-local ontology predicate schema fact.
2423
2732
  predicate_schema(FactId, Namespace, Name, Arity, ArgumentNames, ArgumentTypes) :-