kibi-core 0.13.0 → 0.14.1

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,
@@ -259,15 +288,16 @@ kb_write_lock_owner(Directory) :-
259
288
  ; BootId = ""
260
289
  ),
261
290
  get_time(Now),
262
- format_time(atom(StartedAt), '%FT%TZ', Now),
291
+ stamp_date_time(Now, NowUTC, 'UTC'),
292
+ format_time(atom(StartedAt), '%FT%TZ', NowUTC),
263
293
  atom_concat(Path, '.tmp', TmpPath),
264
294
  setup_call_cleanup(
265
295
  open(TmpPath, write, Stream),
266
296
  json_write_dict(Stream,
267
- json([pid:Pid,
268
- workspaceRoot:Directory,
269
- bootId:BootId,
270
- startedAt:StartedAt]),
297
+ _{pid:Pid,
298
+ workspaceRoot:Directory,
299
+ bootId:BootId,
300
+ startedAt:StartedAt},
271
301
  []),
272
302
  (close(Stream), rename_file(TmpPath, Path))).
273
303
 
@@ -324,6 +354,9 @@ kb_detach :-
324
354
  retractall(kb_dirty),
325
355
  retractall(kb_index_ready),
326
356
  retractall(kb_index_dirty),
357
+ retractall(kb_index_verified_key(_)),
358
+ retractall(kb_entity_props_memo(_, _)),
359
+ retractall(kb_entity_props_memo_key(_)),
327
360
  retractall(kb_index_entity(_, _)),
328
361
  retractall(kb_index_type(_, _)),
329
362
  retractall(kb_index_tag(_, _)),
@@ -1089,6 +1122,62 @@ kb_maybe_check_req_contradiction(req, Id, false) :-
1089
1122
  check_req_contradiction(Id).
1090
1123
  kb_maybe_check_req_contradiction(_, _, _).
1091
1124
 
1125
+ % Stage one upsert inside an already-open transaction. The caller saves once.
1126
+ kb_stage_upsert(Type, Props, Relationships, SkipContradiction, ChangeKind) :-
1127
+ memberchk(id=Id, Props),
1128
+ memberchk(SkipContradiction, [true, false]),
1129
+ upsert_change_kind(Id, ChangeKind),
1130
+ kb_assert_entity_no_audit(Type, Props),
1131
+ kb_commit_relationships_no_audit(Relationships),
1132
+ kb_maybe_check_req_contradiction(Type, Id, SkipContradiction),
1133
+ kb_log_entity_upsert(ChangeKind, Type, Props),
1134
+ kb_commit_relationship_audits(Relationships).
1135
+
1136
+ kb_stage_upserts([], []).
1137
+ kb_stage_upserts([
1138
+ upsert(Type, Props, Relationships, SkipContradiction)|Rest
1139
+ ], [ChangeKind|Kinds]) :-
1140
+ kb_stage_upsert(Type, Props, Relationships, SkipContradiction, ChangeKind),
1141
+ kb_stage_upserts(Rest, Kinds).
1142
+
1143
+ %% kb_commit_upsert_batch(+Entries, -ChangeKinds)
1144
+ % implements REQ-core-atomic-upsert-persistence
1145
+ % Commit many upserts in one RDF transaction and one journal flush.
1146
+ % Entries are upsert(Type, Props, Relationships, SkipContradiction) terms.
1147
+ kb_commit_upsert_batch(Entries, ChangeKinds) :-
1148
+ kb_storage_mode(journaled),
1149
+ !,
1150
+ with_kb_mutex((
1151
+ ( kb_dirty -> WasDirty = true ; WasDirty = false ),
1152
+ catch(
1153
+ rdf_transaction((
1154
+ kb_stage(rdf_mutation),
1155
+ kb_stage_upserts(Entries, ChangeKinds)
1156
+ )),
1157
+ Error,
1158
+ ( (WasDirty == false -> retractall(kb_dirty) ; true),
1159
+ throw(Error)
1160
+ )
1161
+ ),
1162
+ kb_mark_dirty,
1163
+ kb_save_journaled
1164
+ )).
1165
+ kb_commit_upsert_batch(Entries, ChangeKinds) :-
1166
+ kb_attached(Directory),
1167
+ atom_concat(Directory, '/kb.rdf', DataFile),
1168
+ kb_stage(runtime),
1169
+ kb_runtime_diagnostic,
1170
+ kb_stage(lock),
1171
+ with_kb_file_lock(Directory, (
1172
+ audit_store_writable,
1173
+ ensure_snapshot_current(DataFile),
1174
+ rdf_transaction((
1175
+ kb_stage(rdf_mutation),
1176
+ kb_stage_upserts(Entries, ChangeKinds),
1177
+ kb_save_locked(Directory)
1178
+ ))
1179
+ )).
1180
+
1092
1181
  kb_commit_relationship_audits([]).
1093
1182
  kb_commit_relationship_audits([
1094
1183
  rel(RelType, FromId, ToId, _Metadata)|Rest
@@ -1324,6 +1413,42 @@ kb_entity(Id, Type, Props) :-
1324
1413
  % Raw RDF/entity facts are kept separate from the index wrapper. Rebuilding
1325
1414
  % an index must never recurse through the indexed query path.
1326
1415
  kb_entity_raw(Id, Type, Props) :-
1416
+ ( nonvar(Id)
1417
+ -> rdf_entity_solutions(Id, Solutions),
1418
+ member(Type-Props, Solutions)
1419
+ ; kb_entity_raw_rdf(Id, Type, Props)
1420
+ ).
1421
+
1422
+ % Fallback: read from legacy entity/4 facts loaded from .pl files
1423
+ kb_entity_raw(Id, Type, Props) :-
1424
+ entity(Type, Id, _Title, PropList),
1425
+ convert_legacy_props(PropList, Props).
1426
+
1427
+ %% rdf_entity_solutions(+Id, -Solutions)
1428
+ % Coverage, proof, and check predicates look up the same entities tens of
1429
+ % thousands of times, and every lookup materializes and decodes every
1430
+ % property (including receipt histories). Memoize the decoded Type-Props
1431
+ % solutions per entity for the current graph and RDF generation.
1432
+ rdf_entity_solutions(Id, Solutions) :-
1433
+ ( kb_graph(Graph)
1434
+ -> rdf_generation(Generation),
1435
+ Key = Graph-Generation,
1436
+ ( kb_entity_props_memo_key(Current),
1437
+ Current == Key
1438
+ -> true
1439
+ ; retractall(kb_entity_props_memo(_, _)),
1440
+ retractall(kb_entity_props_memo_key(_)),
1441
+ assertz(kb_entity_props_memo_key(Key))
1442
+ ),
1443
+ ( kb_entity_props_memo(Id, Cached)
1444
+ -> Solutions = Cached
1445
+ ; findall(Type-Props, kb_entity_raw_rdf(Id, Type, Props), Solutions),
1446
+ assertz(kb_entity_props_memo(Id, Solutions))
1447
+ )
1448
+ ; Solutions = []
1449
+ ).
1450
+
1451
+ kb_entity_raw_rdf(Id, Type, Props) :-
1327
1452
  kb_graph(Graph),
1328
1453
  % Find entity by pattern - use unquoted namespace term kb:type
1329
1454
  ( var(Id)
@@ -1344,20 +1469,37 @@ kb_entity_raw(Id, Type, Props) :-
1344
1469
  literal_to_value(Key, ValueLiteral, Value)
1345
1470
  ), Props).
1346
1471
 
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
-
1472
+ % Verifying the index counts every type triple in the store. Remember the
1473
+ % inputs of the last successful check (graph, RDF generation, legacy fact
1474
+ % count, and the index's own entity count) and skip the recount while all of
1475
+ % them are unchanged: the recount would then return the same answer. The
1476
+ % index count must be part of the key because incremental index updates are
1477
+ % plain Prolog facts that an RDF rollback does not undo, while the RDF
1478
+ % generation does revert.
1352
1479
  kb_ensure_indexes :-
1353
1480
  ( kb_index_ready,
1354
1481
  \+ kb_index_dirty,
1355
1482
  kb_index_entity_count(Expected),
1356
- current_entity_index_count(Expected)
1483
+ index_verification_key(Expected, Key),
1484
+ ( kb_index_verified_key(Verified),
1485
+ Verified == Key
1486
+ -> true
1487
+ ; current_entity_index_count(Expected),
1488
+ retractall(kb_index_verified_key(_)),
1489
+ assertz(kb_index_verified_key(Key))
1490
+ )
1357
1491
  -> true
1358
1492
  ; kb_rebuild_indexes
1359
1493
  ).
1360
1494
 
1495
+ index_verification_key(IndexCount, key(Graph, Generation, LegacyCount, IndexCount)) :-
1496
+ (kb_graph(Graph) -> true ; Graph = none),
1497
+ rdf_generation(Generation),
1498
+ ( predicate_property(kb:entity(_, _, _, _), number_of_clauses(Count))
1499
+ -> LegacyCount = Count
1500
+ ; LegacyCount = 0
1501
+ ).
1502
+
1361
1503
  % Rebuild all acceleration structures from RDF/entity facts. This is
1362
1504
  % intentionally disposable: no query result is written back to RDF when an
1363
1505
  % index is stale or externally invalidated.
@@ -1380,6 +1522,7 @@ kb_rebuild_indexes :-
1380
1522
  assertz(kb_index_triple_count(TripleCount)),
1381
1523
  retractall(kb_index_dirty),
1382
1524
  retractall(kb_index_ready),
1525
+ retractall(kb_index_verified_key(_)),
1383
1526
  assertz(kb_index_ready)
1384
1527
  )).
1385
1528
 
@@ -1515,6 +1658,97 @@ kb_query_entities(TypeFilter, IdFilter, Tags, SourceFilter, Limit, Offset, Rows,
1515
1658
  kb_entity(Id, Type, Props) ),
1516
1659
  Rows).
1517
1660
 
1661
+ %% kb_query_proof_contracts(+Id, +Limit, +Offset, -Rows)
1662
+ % Page proof-campaign candidates while reading only contract and binding RDF
1663
+ % properties. In particular, do not call kb_entity/3 here: that materializes
1664
+ % complete entity properties, including potentially large receipt histories.
1665
+ % implements REQ-kibi-verification-evidence-contract
1666
+ kb_query_proof_contracts(IdFilter, Limit, Offset, Rows) :-
1667
+ integer(Limit),
1668
+ integer(Offset),
1669
+ Limit >= 0,
1670
+ Offset >= 0,
1671
+ findall([Id, test, Projected],
1672
+ ( limit(Limit,
1673
+ offset(Offset,
1674
+ order_by([asc(Id)],
1675
+ distinct(proof_contract_candidate(IdFilter, Id))))),
1676
+ proof_contract_properties(Id, Contract, Bindings, Source),
1677
+ optional_projected_property(proof_bindings, Bindings,
1678
+ BindingProps),
1679
+ optional_projected_property(source, Source, SourceProps),
1680
+ append([[id=Id, proof_contract=Contract],
1681
+ BindingProps, SourceProps],
1682
+ Projected) ),
1683
+ Rows).
1684
+
1685
+ optional_projected_property(_Key, Value, []) :-
1686
+ var(Value),
1687
+ !.
1688
+ optional_projected_property(Key, Value, [Key=Value]).
1689
+
1690
+ proof_contract_properties(Id, Contract, Bindings) :-
1691
+ proof_contract_properties(Id, Contract, Bindings, _Source).
1692
+
1693
+ % Source is projected alongside the contract so per-contract receipt binding
1694
+ % can hash the authored document without materializing receipt histories.
1695
+ proof_contract_properties(Id, Contract, Bindings, Source) :-
1696
+ ( ( kb_graph(Graph),
1697
+ entity_id_to_uri(Id, EntityURI),
1698
+ rdf(EntityURI, kb:type, TypeLiteral, Graph),
1699
+ literal_to_atom(TypeLiteral, test),
1700
+ kb_property_uri(proof_contract, ContractURI),
1701
+ rdf(EntityURI, ContractURI, ContractLiteral, Graph)
1702
+ )
1703
+ -> literal_to_value(proof_contract, ContractLiteral, Contract),
1704
+ ( kb_property_uri(proof_bindings, BindingsURI),
1705
+ rdf(EntityURI, BindingsURI, BindingsLiteral, Graph)
1706
+ -> literal_to_value(proof_bindings, BindingsLiteral, Bindings)
1707
+ ; true
1708
+ ),
1709
+ ( kb_property_uri(source, SourceURI),
1710
+ rdf(EntityURI, SourceURI, SourceLiteral, Graph)
1711
+ -> literal_to_value(source, SourceLiteral, Source)
1712
+ ; true
1713
+ )
1714
+ ; entity(test, Id, _, Properties),
1715
+ memberchk(proof_contract=Contract, Properties),
1716
+ ( memberchk(proof_bindings=Bindings, Properties)
1717
+ -> true
1718
+ ; true
1719
+ ),
1720
+ ( memberchk(source=Source, Properties)
1721
+ -> true
1722
+ ; true
1723
+ )
1724
+ ).
1725
+
1726
+ %% kb_property_uri(+Key, -URI)
1727
+ % Property predicates are asserted in-session as the prefixed atom 'kb:Key',
1728
+ % but a store reloaded from its snapshot carries the expanded 'urn-kibi:Key'
1729
+ % form. Direct RDF lookups must accept both (as uri_to_key/2 already does),
1730
+ % or projections silently miss every entity of a reloaded store.
1731
+ kb_property_uri(Key, URI) :-
1732
+ kb_uri(BaseURI),
1733
+ atom_concat(BaseURI, Key, URI).
1734
+ kb_property_uri(Key, URI) :-
1735
+ atom_concat('kb:', Key, URI).
1736
+
1737
+ % implements REQ-kibi-verification-evidence-contract
1738
+ proof_contract_candidate(none, Id) :-
1739
+ kb_graph(Graph),
1740
+ kb_property_uri(proof_contract, ContractURI),
1741
+ rdf(EntityURI, ContractURI, _, Graph),
1742
+ entity_uri_to_id(EntityURI, Id),
1743
+ rdf(EntityURI, kb:type, TypeLiteral, Graph),
1744
+ literal_to_atom(TypeLiteral, test).
1745
+ proof_contract_candidate(none, Id) :-
1746
+ entity(test, Id, _, Properties),
1747
+ memberchk(proof_contract=_, Properties).
1748
+ proof_contract_candidate(some(Id), Id) :-
1749
+ atom(Id),
1750
+ proof_contract_properties(Id, _, _).
1751
+
1518
1752
  indexed_entity_match(TypeFilter, IdFilter, Tags, SourceFilter, Id) :-
1519
1753
  ( IdFilter == none
1520
1754
  -> kb_index_entity(Id, _)
@@ -1573,10 +1807,74 @@ kb_search_entities(TypeFilter, Query, Limit, Offset, Rows, Count) :-
1573
1807
  length(Ids, Count),
1574
1808
  drop_index_ids(Offset, Ids, Remaining),
1575
1809
  take_index_ids(Limit, Remaining, PageIds),
1576
- findall([Id, Type, Props],
1577
- ( member(Id, PageIds), kb_entity(Id, Type, Props) ),
1810
+ search_candidate_rows(PageIds, Rows).
1811
+
1812
+ %% kb_list_search_candidates(+Type, +Limit, +Offset, -Rows, -Count)
1813
+ % Page every indexed entity (optionally of one type) as projected search
1814
+ % candidate rows, ordered by id. Semantic discovery uses it when no lexical
1815
+ % token can pre-filter the candidate set.
1816
+ kb_list_search_candidates(TypeFilter, Limit, Offset, Rows, Count) :-
1817
+ integer(Limit),
1818
+ integer(Offset),
1819
+ Limit >= 0,
1820
+ Offset >= 0,
1821
+ kb_ensure_indexes,
1822
+ findall(Id,
1823
+ ( kb_index_entity(Id, _),
1824
+ (TypeFilter == none -> true ; kb_index_type(TypeFilter, Id)) ),
1825
+ RawIds),
1826
+ sort(RawIds, Ids),
1827
+ length(Ids, Count),
1828
+ drop_index_ids(Offset, Ids, Remaining),
1829
+ take_index_ids(Limit, Remaining, PageIds),
1830
+ search_candidate_rows(PageIds, Rows).
1831
+
1832
+ %% kb_entity_ids(-Ids)
1833
+ % Every entity id, without materializing any properties.
1834
+ kb_entity_ids(Ids) :-
1835
+ kb_ensure_indexes,
1836
+ findall(Id, kb_index_entity(Id, _), RawIds),
1837
+ sort(RawIds, Ids).
1838
+
1839
+ % Search candidates carry only the properties ranking, snippets, and summary
1840
+ % output read. Large structured properties (receipt histories, proof
1841
+ % contracts, semantic inventories, rule IR) stay in the store: a candidate
1842
+ % page of receipt-bearing tests otherwise exceeds the bounded engine output.
1843
+ % Callers that need complete entities reload the final page by id.
1844
+ search_candidate_rows(PageIds, Rows) :-
1845
+ findall([Id, Type, Projected],
1846
+ ( member(Id, PageIds),
1847
+ kb_entity(Id, Type, Props),
1848
+ include(search_candidate_property, Props, Projected) ),
1578
1849
  Rows).
1579
1850
 
1851
+ search_candidate_property(Key=_) :-
1852
+ search_candidate_key(Key).
1853
+
1854
+ search_candidate_key(id).
1855
+ search_candidate_key(title).
1856
+ search_candidate_key(status).
1857
+ search_candidate_key(priority).
1858
+ search_candidate_key(severity).
1859
+ search_candidate_key(owner).
1860
+ search_candidate_key(tags).
1861
+ search_candidate_key(source).
1862
+ search_candidate_key(sourceFile).
1863
+ search_candidate_key(sourceLine).
1864
+ search_candidate_key(sourceColumn).
1865
+ search_candidate_key(sourceEndLine).
1866
+ search_candidate_key(sourceEndColumn).
1867
+ search_candidate_key(source_line).
1868
+ search_candidate_key(source_end_line).
1869
+ search_candidate_key(updated_at).
1870
+ search_candidate_key(created_at).
1871
+ search_candidate_key(semantic_text).
1872
+ search_candidate_key(text_ref).
1873
+ search_candidate_key(body).
1874
+ search_candidate_key(content).
1875
+ search_candidate_key(markdownBody).
1876
+ search_candidate_key(markdown_body).
1877
+
1580
1878
  indexed_token_match(QueryToken, Id) :-
1581
1879
  kb_index_token(IndexedToken, Id),
1582
1880
  sub_atom(IndexedToken, _, _, _, QueryToken),
@@ -2269,10 +2567,14 @@ req_conflict_witness(ReqA, ReqB, Witness) :-
2269
2567
  FactA \= FactB,
2270
2568
  scope_intersects(ScopeA, ScopeB),
2271
2569
  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)
2570
+ % Compare canonical quantities (30 min == 1800 s); witnesses keep the
2571
+ % authored values so evidence still points at the source text.
2572
+ canonical_quantity(ValTypeA, ValA, UnitA, CanonTypeA, CanonValA, CanonUnitA),
2573
+ canonical_quantity(ValTypeB, ValB, UnitB, CanonTypeB, CanonValB, CanonUnitB),
2574
+ ( polarity_conflict(SubjectKey, PropertyKey, OpA, CanonTypeA, CanonValA, CanonUnitA, ScopeA, PolarityA,
2575
+ OpB, CanonTypeB, CanonValB, CanonUnitB, ScopeB, PolarityB, Reason)
2576
+ ; property_conflict(SubjectKey, PropertyKey, OpA, CanonTypeA, CanonValA, CanonUnitA, PolarityA,
2577
+ OpB, CanonTypeB, CanonValB, CanonUnitB, PolarityB, Reason)
2276
2578
  ),
2277
2579
  property_conflict_side(ReqA, FactA, SubjectKey, PropertyKey, OpA, ValTypeA, ValA, UnitA, ScopeA, PolarityA, ValidFromA, ValidToA, Left),
2278
2580
  property_conflict_side(ReqB, FactB, SubjectKey, PropertyKey, OpB, ValTypeB, ValB, UnitB, ScopeB, PolarityB, ValidFromB, ValidToB, Right),
@@ -2418,6 +2720,14 @@ fact_property_tuple(FactId, Subject, Property, Op, ValType, Value, Unit, Scope,
2418
2720
  ( memberchk(scope=ScopeRaw, Props) -> normalize_term_atom(ScopeRaw, Scope) ; Scope = '' ),
2419
2721
  ( memberchk(polarity=PolarityRaw, Props) -> normalize_term_atom(PolarityRaw, Polarity) ; Polarity = require ).
2420
2722
 
2723
+ %% canonical_property_tuple(+FactId, -Subject, -Property, -Op, -ValType, -Value, -Unit, -Scope, -Polarity)
2724
+ % fact_property_tuple/9 with the quantity converted to its canonical base unit
2725
+ % (units.pl). Used only for comparison; the authored fact is never rewritten.
2726
+ % implements REQ-kibi-unit-canonicalization
2727
+ canonical_property_tuple(FactId, Subject, Property, Op, CanonType, CanonValue, CanonUnit, Scope, Polarity) :-
2728
+ fact_property_tuple(FactId, Subject, Property, Op, ValType, Value, Unit, Scope, Polarity),
2729
+ canonical_quantity(ValType, Value, Unit, CanonType, CanonValue, CanonUnit).
2730
+
2421
2731
  %% predicate_schema(+FactId, -Namespace, -Name, -Arity, -ArgumentNames, -ArgumentTypes)
2422
2732
  % Read one project-local ontology predicate schema fact.
2423
2733
  predicate_schema(FactId, Namespace, Name, Arity, ArgumentNames, ArgumentTypes) :-
@@ -8,6 +8,9 @@
8
8
  requirement_proof/4
9
9
  ]).
10
10
 
11
+ % Source contains non-ASCII text; do not depend on the host locale.
12
+ :- encoding(utf8).
13
+
11
14
  :- use_module(library(http/json)).
12
15
  :- use_module(library(crypto)).
13
16
  :- use_module(library(date)).
@@ -237,7 +240,47 @@ inventory_entry_field(List, Key, Value) :-
237
240
  is_list(List),
238
241
  memberchk(Key=Value, List).
239
242
 
243
+ % Pure-function memos, content-addressed by variant_sha1/2, so they need no
244
+ % invalidation: equal inputs always produce equal outputs. Proof evaluation
245
+ % re-parses the same receipt histories and re-validates the same receipts on
246
+ % every coverage run; both are bounded so a long-lived engine cannot grow them
247
+ % without limit.
248
+ :- dynamic pure_memo/3.
249
+ :- dynamic pure_memo_size/1.
250
+
251
+ pure_memo_limit(4096).
252
+
253
+ pure_memo(Kind, Input, Goal, Output) :-
254
+ variant_sha1(Kind-Input, Key),
255
+ ( pure_memo(Key, Kind, Cached)
256
+ -> Output = Cached
257
+ ; call(Goal, Input, Result),
258
+ pure_memo_remember(Key, Kind, Result),
259
+ Output = Result
260
+ ).
261
+
262
+ pure_memo_remember(Key, Kind, Output) :-
263
+ ( pure_memo_size(Size)
264
+ -> true
265
+ ; Size = 0
266
+ ),
267
+ pure_memo_limit(Limit),
268
+ ( Size >= Limit
269
+ -> retractall(pure_memo(_, _, _)),
270
+ NextSize = 1
271
+ ; NextSize is Size + 1
272
+ ),
273
+ retractall(pure_memo_size(_)),
274
+ assertz(pure_memo_size(NextSize)),
275
+ assertz(pure_memo(Key, Kind, Output)).
276
+
240
277
  inventory_entries(Raw, Entries) :-
278
+ ( (atom(Raw) ; string(Raw))
279
+ -> pure_memo(inventory_entries, Raw, parse_inventory_entries, Entries)
280
+ ; parse_inventory_entries(Raw, Entries)
281
+ ).
282
+
283
+ parse_inventory_entries(Raw, Entries) :-
241
284
  ( is_list(Raw)
242
285
  -> Entries = Raw
243
286
  ; Raw = ^^(Value, _)
@@ -727,8 +770,8 @@ receipt_evidence_state(TestId, Scope, Props, Receipts, _Present, _Context,
727
770
  proof_contract_binding(Props, ContractBinding),
728
771
  (ContractBinding == invalid
729
772
  ; Receipts == []
730
- ; member(Receipt, Receipts), \+ valid_receipt_shape(TestId, Receipt)
731
- ; \+ chronological_receipt_history(Receipts)),
773
+ ; pure_memo(receipt_history_well_formed, TestId-Receipts,
774
+ receipt_history_well_formed, false)),
732
775
  !.
733
776
  receipt_evidence_state(TestId, Scope, Props, Receipts, _Present, Context, Evidence) :-
734
777
  receipt_for_current_mode(TestId, Context, Receipts, SnapshotReceipts),
@@ -745,6 +788,13 @@ receipt_evidence_state(TestId, Scope, Props, Receipts, _Present, Context, Eviden
745
788
  )
746
789
  ).
747
790
 
791
+ receipt_history_well_formed(TestId-Receipts, WellFormed) :-
792
+ ( forall(member(Receipt, Receipts), valid_receipt_shape(TestId, Receipt)),
793
+ chronological_receipt_history(Receipts)
794
+ -> WellFormed = true
795
+ ; WellFormed = false
796
+ ).
797
+
748
798
  proof_receipt_entries(Props, Receipts, Present) :-
749
799
  ( memberchk(proof_receipts=Raw, Props)
750
800
  -> Present = true,
@@ -889,10 +939,10 @@ receipt_for_current_mode(TestId, Context, Receipts, SnapshotReceipts) :-
889
939
  ( Context.proofBindingMode == per_contract,
890
940
  get_dict(TestId, Context.proofTestBindings, CurrentBinding)
891
941
  -> include(receipt_with_binding_hash, Receipts, BoundReceipts),
892
- include(receipt_for_binding(CurrentBinding), BoundReceipts, SnapshotReceipts),
893
- ( SnapshotReceipts == []
942
+ include(receipt_for_binding(CurrentBinding), BoundReceipts, BindingReceipts),
943
+ ( BindingReceipts == []
894
944
  -> include(receipt_for_snapshot(Context.proofSnapshot), Receipts, SnapshotReceipts)
895
- ; true
945
+ ; SnapshotReceipts = BindingReceipts
896
946
  )
897
947
  ; include(receipt_for_snapshot(Context.proofSnapshot), Receipts, SnapshotReceipts)
898
948
  ).
@@ -34,6 +34,13 @@ known_rule('predicate-verifiability').
34
34
  known_rule('strict-readiness').
35
35
  known_rule('semantic-completeness').
36
36
  known_rule('proof-contract-symbols').
37
+ known_rule('entity-id-style').
38
+ known_rule('domain-redundancy').
39
+ known_rule('domain-implication').
40
+ known_rule('subject-key-identity').
41
+ known_rule('subject-key-shape').
42
+ known_rule('ontology-quality').
43
+ known_rule('predicate-schema-conformance').
37
44
 
38
45
  rule_enforcement_class('must-priority-coverage', canonical).
39
46
  rule_enforcement_class('symbol-coverage', canonical).
@@ -55,6 +62,13 @@ rule_enforcement_class('predicate-verifiability', advisory).
55
62
  rule_enforcement_class('strict-readiness', migration).
56
63
  rule_enforcement_class('semantic-completeness', migration).
57
64
  rule_enforcement_class('proof-contract-symbols', advisory).
65
+ rule_enforcement_class('entity-id-style', advisory).
66
+ rule_enforcement_class('domain-redundancy', advisory).
67
+ rule_enforcement_class('domain-implication', advisory).
68
+ rule_enforcement_class('subject-key-identity', advisory).
69
+ rule_enforcement_class('subject-key-shape', advisory).
70
+ rule_enforcement_class('ontology-quality', advisory).
71
+ rule_enforcement_class('predicate-schema-conformance', advisory).
58
72
 
59
73
  rule_implementation('must-priority-coverage', prolog).
60
74
  rule_implementation('symbol-coverage', prolog).
@@ -76,6 +90,13 @@ rule_implementation('predicate-verifiability', prolog).
76
90
  rule_implementation('strict-readiness', prolog).
77
91
  rule_implementation('semantic-completeness', prolog).
78
92
  rule_implementation('proof-contract-symbols', prolog).
93
+ rule_implementation('entity-id-style', prolog).
94
+ rule_implementation('domain-redundancy', prolog).
95
+ rule_implementation('domain-implication', prolog).
96
+ rule_implementation('subject-key-identity', prolog).
97
+ rule_implementation('subject-key-shape', prolog).
98
+ rule_implementation('ontology-quality', prolog).
99
+ rule_implementation('predicate-schema-conformance', typescript).
79
100
 
80
101
  rule_predicate('must-priority-coverage', check_must_priority_coverage).
81
102
  rule_predicate('symbol-coverage', check_symbol_coverage).
@@ -95,6 +116,12 @@ rule_predicate('predicate-verifiability', check_predicate_verifiability).
95
116
  rule_predicate('strict-readiness', check_strict_readiness).
96
117
  rule_predicate('semantic-completeness', check_semantic_completeness).
97
118
  rule_predicate('proof-contract-symbols', check_proof_contract_symbols).
119
+ rule_predicate('entity-id-style', check_entity_id_style).
120
+ rule_predicate('domain-redundancy', check_domain_redundancy).
121
+ rule_predicate('domain-implication', check_domain_implication).
122
+ rule_predicate('subject-key-identity', check_subject_key_identity).
123
+ rule_predicate('subject-key-shape', check_subject_key_shape).
124
+ rule_predicate('ontology-quality', check_ontology_quality).
98
125
 
99
126
  rule_predicate_arity('must-priority-coverage', 1).
100
127
  rule_predicate_arity('symbol-coverage', 1).
@@ -114,6 +141,12 @@ rule_predicate_arity('predicate-verifiability', 1).
114
141
  rule_predicate_arity('strict-readiness', 1).
115
142
  rule_predicate_arity('semantic-completeness', 1).
116
143
  rule_predicate_arity('proof-contract-symbols', 1).
144
+ rule_predicate_arity('entity-id-style', 1).
145
+ rule_predicate_arity('domain-redundancy', 1).
146
+ rule_predicate_arity('domain-implication', 1).
147
+ rule_predicate_arity('subject-key-identity', 1).
148
+ rule_predicate_arity('subject-key-shape', 1).
149
+ rule_predicate_arity('ontology-quality', 1).
117
150
 
118
151
  rule_description('must-priority-coverage', 'Every must-priority requirement must have a scenario and a test').
119
152
  rule_description('symbol-coverage', 'Production symbols need qualifying coverage via covered_by plus a canonical requirement/scenario test path').
@@ -135,3 +168,10 @@ rule_description('predicate-verifiability', 'Detect requires_predicate links tha
135
168
  rule_description('strict-readiness', 'Report strict contradiction-readiness levels for requirements that are still prose-only or otherwise not contradiction-ready').
136
169
  rule_description('semantic-completeness', 'Every inventoried assertive proposition is modeled or explicitly classified').
137
170
  rule_description('proof-contract-symbols', 'Detect unresolved required_proofs.symbol_id values, type-shape required proofs, and proof_bindings.source_file disagreement with the named symbol sourceFile').
171
+ rule_description('entity-id-style', 'Entity IDs name the governed behavior and match their filename stem; new purely numeric IDs are reported at creation boundaries').
172
+ rule_description('domain-redundancy', 'Two current requirements must not ground the identical logical term or share a ground fact unless linked by supersedes or restates').
173
+ rule_description('domain-implication', 'Informational: one requirement\'s numeric bound strictly implies another requirement\'s bound on the same subject and property').
174
+ rule_description('subject-key-identity', 'Subject keys must name a shared component, not be derived from a requirement ID').
175
+ rule_description('subject-key-shape', 'Subject keys follow dotted component.aspect[.sub] with lowercase snake segments').
176
+ rule_description('ontology-quality', 'Informational: predicate schemas whose argument values mostly occur in only one fact are carrying prose instead of a shared vocabulary').
177
+ rule_description('predicate-schema-conformance', 'Predicate facts match a predicate_schema (project-local, or the built-in catalog in the default namespace) and use its declared argument constants').