kibi-core 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-core",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "private": false,
5
5
  "description": "Core Prolog modules and RDF graph logic for Kibi",
6
6
  "type": "module",
@@ -37,6 +37,8 @@ entity_property(req, semantic_inventory_version, string).
37
37
  entity_property(req, semantic_source_field, string).
38
38
  entity_property(req, semantic_source_hash, string).
39
39
  entity_property(req, semantic_inventory, list_or_json).
40
+ entity_property(req, proof_exempt, boolean).
41
+ entity_property(req, proof_exempt_reason, string).
40
42
 
41
43
  % Typed fact fields - only valid for fact entities
42
44
  entity_property(fact, fact_kind, atom).
@@ -88,8 +90,9 @@ entity_property(symbol, sourceEndColumn, integer).
88
90
  % Typed test verification fields - only valid for test entities
89
91
  entity_property(test, verification_scope, atom).
90
92
  entity_property(test, verification_perspective, atom).
91
- entity_property(test, verification_contract, list_or_json).
92
- entity_property(test, verification_receipts, string).
93
+ entity_property(test, proof_contract, list_or_json).
94
+ entity_property(test, proof_bindings, string).
95
+ entity_property(test, proof_receipts, string).
93
96
 
94
97
  % Required properties for all entity types
95
98
  required_property(Type, id) :- entity_type(Type).
@@ -113,10 +116,13 @@ optional_property(req, semantic_inventory_version).
113
116
  optional_property(req, semantic_source_field).
114
117
  optional_property(req, semantic_source_hash).
115
118
  optional_property(req, semantic_inventory).
119
+ optional_property(req, proof_exempt).
120
+ optional_property(req, proof_exempt_reason).
116
121
  optional_property(test, verification_scope).
117
122
  optional_property(test, verification_perspective).
118
- optional_property(test, verification_contract).
119
- optional_property(test, verification_receipts).
123
+ optional_property(test, proof_contract).
124
+ optional_property(test, proof_bindings).
125
+ optional_property(test, proof_receipts).
120
126
 
121
127
  % Documentation helpers
122
128
  % list all entity types
@@ -119,8 +119,9 @@ is_fact_only_field(claim_span_end).
119
119
  % is_test_only_field(+Key) - true if Key is a test-specific field
120
120
  is_test_only_field(verification_scope).
121
121
  is_test_only_field(verification_perspective).
122
- is_test_only_field(verification_contract).
123
- is_test_only_field(verification_receipts).
122
+ is_test_only_field(proof_contract).
123
+ is_test_only_field(proof_bindings).
124
+ is_test_only_field(proof_receipts).
124
125
 
125
126
  % validate_fact_shape(+Kind, +Props)
126
127
  validate_fact_shape(subject, Props) :-
package/src/checks.pl CHANGED
@@ -25,6 +25,7 @@
25
25
  check_rule_safety/1,
26
26
  check_rule_verifiability/1,
27
27
  check_semantic_completeness/1,
28
+ check_req_status_vocabulary/1,
28
29
  run_checks_json/0, % Entry point for JSON output
29
30
  violation_id_text/2 % Extract text from entity ID term (exported for testing)
30
31
  ]).
@@ -35,6 +36,8 @@
35
36
  :- use_module('../schema/entities.pl', [entity_type/1, required_property/2]).
36
37
  :- use_module('../schema/relationships.pl', [relationship_type/1]).
37
38
  :- use_module('logic_ir.pl', [logic_rule_safety/2, logic_rule_from_props/2, logic_rule_conflict/3, logic_rule_conflict_witness/3, logic_rules_stratified/1]).
39
+ % GENERATED registry facts; single source is schema/rule-registry.json.
40
+ :- use_module('rule_registry.pl', [known_rule/1]).
38
41
 
39
42
  % Required fields for all entities
40
43
  required_fields([id, title, status, created_at, updated_at, source]).
@@ -66,6 +69,7 @@ check_all(ViolationsDict) :-
66
69
  check_rule_safety(RuleSafety),
67
70
  check_rule_verifiability(RuleVerifiability),
68
71
  check_semantic_completeness(SemanticCompleteness),
72
+ check_req_status_vocabulary(ReqStatusVocabulary),
69
73
  ViolationsDict = _{
70
74
  must_priority_coverage: MustPriority,
71
75
  symbol_coverage: SymbolCoverage,
@@ -82,7 +86,8 @@ check_all(ViolationsDict) :-
82
86
  logic_coverage: LogicCoverage,
83
87
  rule_safety: RuleSafety,
84
88
  rule_verifiability: RuleVerifiability,
85
- semantic_completeness: SemanticCompleteness
89
+ semantic_completeness: SemanticCompleteness,
90
+ req_status_vocabulary: ReqStatusVocabulary
86
91
  }.
87
92
 
88
93
  %% check_must_priority_coverage(-Violations)
@@ -363,6 +368,39 @@ deprecated_adr_violation(violation(
363
368
  ; Source = ""
364
369
  ).
365
370
 
371
+ %% check_req_status_vocabulary(-Violations)
372
+ % Rejects requirement statuses outside the canonical+legacy vocabulary.
373
+ % Requirement documents carrying ADR vocabulary (e.g. `status: accepted`)
374
+ % compile and pass schema validation, then silently fall out of
375
+ % current_req/1 — the proof ladder reports them not_applicable with no
376
+ % signal. This rule surfaces the vocabulary mismatch at check time instead.
377
+ canonical_req_statuses([open, in_progress, closed]).
378
+ legacy_req_statuses([active, approved]).
379
+
380
+ check_req_status_vocabulary(Violations) :-
381
+ findall(
382
+ Violation,
383
+ req_status_vocabulary_violation(Violation),
384
+ Violations
385
+ ).
386
+
387
+ req_status_vocabulary_violation(violation(
388
+ 'req-status-vocabulary',
389
+ ReqId,
390
+ Description,
391
+ Suggestion,
392
+ Source
393
+ )) :-
394
+ kb_entity(ReqId, req, Props),
395
+ memberchk(status=RawStatus, Props),
396
+ normalize_term_atom(RawStatus, StatusAtom),
397
+ canonical_req_statuses(Canonical),
398
+ legacy_req_statuses(Legacy),
399
+ \+ (memberchk(StatusAtom, Canonical) ; memberchk(StatusAtom, Legacy)),
400
+ format(string(Description), "Requirement status '~w' is not a current requirement status; it is silently excluded from the proof ladder", [StatusAtom]),
401
+ format(string(Suggestion), "Set status to one of open, in_progress, closed (legacy: active, approved). To park a current requirement out of E2E-proof scope, use proof_exempt: true with proof_exempt_reason instead of an ADR status", []),
402
+ violation_source(ReqId, req, Source).
403
+
366
404
  %% check_strict_fact_shape(-Violations)
367
405
  % Finds all strict facts (with fact_kind) that have malformed shape.
368
406
  % Only checks facts with fact_kind present; legacy facts without fact_kind are ignored.
@@ -1412,7 +1450,18 @@ check_selected_json(Rules, JsonString) :-
1412
1450
  JsonString
1413
1451
  ).
1414
1452
 
1415
- check_selected(Rules, _{
1453
+ %% check_selected(+Rules, -ViolationsDict)
1454
+ % Run only the named aggregated checks. This is intentionally separate from
1455
+ % check_all/1 so focused diagnostics cannot evaluate unrelated rule queries.
1456
+ % Unknown rule names fail loudly: a typo must never masquerade as a clean,
1457
+ % violation-free run. Rules implemented on the TypeScript side (recorded in
1458
+ % rule_registry.pl with implementation typescript) are accepted here and
1459
+ % simply produce no Prolog-side violations.
1460
+ check_selected(Rules, ViolationsDict) :-
1461
+ require_known_rules(Rules),
1462
+ check_selected_dispatch(Rules, ViolationsDict).
1463
+
1464
+ check_selected_dispatch(Rules, _{
1416
1465
  must_priority_coverage: MustPriority,
1417
1466
  symbol_coverage: SymbolCoverage,
1418
1467
  symbol_traceability: SymbolTraceability,
@@ -1428,7 +1477,8 @@ check_selected(Rules, _{
1428
1477
  logic_coverage: LogicCoverage,
1429
1478
  rule_safety: RuleSafety,
1430
1479
  rule_verifiability: RuleVerifiability,
1431
- semantic_completeness: SemanticCompleteness
1480
+ semantic_completeness: SemanticCompleteness,
1481
+ req_status_vocabulary: ReqStatusVocabulary
1432
1482
  }) :-
1433
1483
  selected_rule(Rules, 'must-priority-coverage', check_must_priority_coverage, MustPriority),
1434
1484
  selected_rule(Rules, 'symbol-coverage', check_symbol_coverage, SymbolCoverage),
@@ -1445,7 +1495,8 @@ check_selected(Rules, _{
1445
1495
  selected_rule(Rules, 'logic-coverage', check_logic_coverage, LogicCoverage),
1446
1496
  selected_rule(Rules, 'rule-safety', check_rule_safety, RuleSafety),
1447
1497
  selected_rule(Rules, 'rule-verifiability', check_rule_verifiability, RuleVerifiability),
1448
- selected_rule(Rules, 'semantic-completeness', check_semantic_completeness, SemanticCompleteness).
1498
+ selected_rule(Rules, 'semantic-completeness', check_semantic_completeness, SemanticCompleteness),
1499
+ selected_rule(Rules, 'req-status-vocabulary', check_req_status_vocabulary, ReqStatusVocabulary).
1449
1500
 
1450
1501
  selected_rule(Rules, Name, Goal, Violations) :-
1451
1502
  ( memberchk(Name, Rules)
@@ -1459,6 +1510,17 @@ selected_rule_with_options(Rules, Name, Goal, Violations) :-
1459
1510
  ; Violations = []
1460
1511
  ).
1461
1512
 
1513
+ %% require_known_rules(+Rules)
1514
+ % Fail closed on names outside the generated registry so a typo or a
1515
+ % stale cross-version call surfaces as an error instead of an empty result.
1516
+ require_known_rules(Rules) :-
1517
+ exclude(rule_registry:known_rule, Rules, Unknown),
1518
+ ( Unknown == []
1519
+ -> true
1520
+ ; throw(error(domain_error(check_rule, Unknown),
1521
+ context(check_selected, 'Unknown check rules requested')))
1522
+ ).
1523
+
1462
1524
  %% check_all_json_with_options(-JsonString, +RequireAdr)
1463
1525
  % Returns all violations as JSON string with options.
1464
1526
  % RequireAdr: if true, symbol-traceability also requires ADR constraints.
@@ -1489,6 +1551,7 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
1489
1551
  check_rule_safety(RuleSafety),
1490
1552
  check_rule_verifiability(RuleVerifiability),
1491
1553
  check_semantic_completeness(SemanticCompleteness),
1554
+ check_req_status_vocabulary(ReqStatusVocabulary),
1492
1555
  ViolationsDict = _{
1493
1556
  must_priority_coverage: MustPriority,
1494
1557
  symbol_coverage: SymbolCoverage,
@@ -1505,7 +1568,8 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
1505
1568
  logic_coverage: LogicCoverage,
1506
1569
  rule_safety: RuleSafety,
1507
1570
  rule_verifiability: RuleVerifiability,
1508
- semantic_completeness: SemanticCompleteness
1571
+ semantic_completeness: SemanticCompleteness,
1572
+ req_status_vocabulary: ReqStatusVocabulary
1509
1573
  }.
1510
1574
 
1511
1575
  %% violations_dict_to_json(+ViolationsDict, -JsonDict)
package/src/discovery.pl CHANGED
@@ -5,13 +5,19 @@
5
5
  find_gaps_json/8,
6
6
  coverage_report_json/7,
7
7
  coverage_report_json/10,
8
+ coverage_report_json/11,
9
+ coverage_report_json/12,
10
+ coverage_evidence_json/5,
8
11
  graph_expand_json/8
9
12
  ]).
10
13
 
11
14
  :- use_module(library(http/json)).
12
15
  :- use_module(library(aggregate)).
16
+ :- use_module(library(apply)).
17
+ :- use_module(library(lists)).
18
+ :- use_module(library(pairs)).
13
19
  :- use_module('kb.pl').
14
- :- use_module('requirement_proof.pl', [requirement_proof_context/1, requirement_proof_context/4, requirement_proof/4]).
20
+ :- use_module('requirement_proof.pl', [requirement_proof_context/1, requirement_proof_context/4, requirement_proof_context/6, requirement_proof/4]).
15
21
  :- use_module('status.pl', [status_meta_dict/1]).
16
22
  :- use_module('../schema/relationships.pl', [relationship_type/1]).
17
23
 
@@ -32,13 +38,105 @@ coverage_report_json(By, Tags, IncludePassing, IncludeTransitive, Limit, Offset,
32
38
  coverage_report_json(By, Tags, IncludePassing, IncludeTransitive, Limit, Offset, unknown, '1970-01-01T00:00:00Z', 604800, JsonString).
33
39
 
34
40
  coverage_report_json(By, Tags, IncludePassing, IncludeTransitive, Limit, Offset, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
35
- coverage_rows(By, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows0, Summary),
41
+ coverage_report_json(By, Tags, IncludePassing, strict_snapshot, _{}, IncludeTransitive, Limit, Offset, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString).
42
+
43
+ % Slim per-requirement proof evidence for quality diagnostics (W1 push-down).
44
+ % Projects only the fields the quality diagnostics consume - proof status,
45
+ % the passing-e2e stage summary, and receipt-related gap codes - instead of
46
+ % the full coverage row set, keeping the response small on large KBs.
47
+ coverage_evidence_json(Tags, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
48
+ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, ProofContext),
49
+ findall(Evidence,
50
+ ( kb_entity(Id, req, Props),
51
+ matches_tags(Tags, Props),
52
+ requirement_proof(Id, Props, ProofContext, Proof),
53
+ coverage_evidence_row(Id, Proof, Evidence)
54
+ ),
55
+ Evidences),
56
+ Response = _{rows: Evidences},
57
+ dict_json_string(Response, JsonString).
58
+
59
+ coverage_evidence_row(Id, Proof, Evidence) :-
60
+ ( get_dict(proofStatus, Proof, ProofStatus)
61
+ -> true
62
+ ; ProofStatus = unknown
63
+ ),
64
+ ( get_dict(proofStages, Proof, Stages),
65
+ get_dict(passingE2e, Stages, PassingE2e),
66
+ get_dict(status, PassingE2e, PassingE2eStatus)
67
+ -> ( get_dict(tests, PassingE2e, PassingE2eTests)
68
+ -> true
69
+ ; PassingE2eTests = []
70
+ )
71
+ ; PassingE2eStatus = unknown,
72
+ PassingE2eTests = []
73
+ ),
74
+ ( get_dict(proofGaps, Proof, Gaps)
75
+ -> include(coverage_evidence_receipt_gap, Gaps, ReceiptGapCodes)
76
+ ; ReceiptGapCodes = []
77
+ ),
78
+ Evidence = _{
79
+ id: Id,
80
+ proofStatus: ProofStatus,
81
+ passingE2eStatus: PassingE2eStatus,
82
+ passingE2eTests: PassingE2eTests,
83
+ receiptGapCodes: ReceiptGapCodes
84
+ }.
85
+
86
+ coverage_evidence_receipt_gap(Gap) :-
87
+ ( atom(Gap)
88
+ -> Atom = Gap
89
+ ; format(atom(Atom), "~w", [Gap])
90
+ ),
91
+ ( sub_atom(Atom, _, _, _, 'proof_receipt')
92
+ ; sub_atom(Atom, _, _, _, 'proof_contract')
93
+ ; sub_atom(Atom, _, _, _, 'proof_snapshot')
94
+ ).
95
+
96
+ % Per-contract receipt binding (W2): BindingMode strict_snapshot | per_contract
97
+ % with TestBindings mapping TestId -> current binding hash.
98
+ coverage_report_json(By, Tags, IncludePassing, BindingMode, TestBindings, IncludeTransitive, Limit, Offset, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
99
+ % Binding mode applies to requirement rows only; other By values keep
100
+ % their snapshot-era arity so type/symbol reports are unchanged.
101
+ ( By == req
102
+ -> coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, Rows0, Summary)
103
+ ; coverage_rows(By, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows0, Summary)
104
+ ),
36
105
  sort_dict_rows(Rows0, SortedRows),
37
106
  paginate_rows(SortedRows, Offset, Limit, Rows),
38
107
  status_meta_dict(Meta),
39
108
  Response = _{summary: Summary, rows: Rows, meta: Meta},
40
109
  dict_json_string(Response, JsonString).
41
110
 
111
+ % implements REQ-kibi-coverage-status-filter
112
+ % Proof-status filtered report. The status filter selects requirement rows by
113
+ % proofStatus (proven, missing, unresolved, not_applicable) and replaces the
114
+ % default include-passing filtering, so callers can enumerate exactly one
115
+ % slice — e.g. every not_applicable row together with its typed applicability
116
+ % reason — without diffing full exports by hand. The summary is always
117
+ % computed over all rows so filtered responses keep whole-KB counts.
118
+ coverage_report_json(By, Tags, IncludePassing, StatusFilter, IncludeTransitive, Limit, Offset, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
119
+ status_filtered_coverage_rows(By, Tags, IncludePassing, StatusFilter, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, MatchedRows0, Summary),
120
+ sort_dict_rows(MatchedRows0, SortedRows),
121
+ paginate_rows(SortedRows, Offset, Limit, Rows),
122
+ status_meta_dict(Meta),
123
+ Response = _{summary: Summary, rows: Rows, meta: Meta},
124
+ dict_json_string(Response, JsonString).
125
+
126
+ status_filtered_coverage_rows(By, Tags, IncludePassing, [], IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
127
+ !,
128
+ coverage_rows(By, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary).
129
+ status_filtered_coverage_rows(req, Tags, _IncludePassing, StatusFilter, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, MatchedRows, Summary) :-
130
+ !,
131
+ coverage_rows(req, Tags, true, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows0, Summary),
132
+ include(req_row_proof_status_in(StatusFilter), Rows0, MatchedRows).
133
+ status_filtered_coverage_rows(By, Tags, _IncludePassing, _StatusFilter, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
134
+ coverage_rows(By, Tags, true, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary).
135
+
136
+ req_row_proof_status_in(StatusFilter, Row) :-
137
+ Status = Row.proofStatus,
138
+ memberchk(Status, StatusFilter).
139
+
42
140
  graph_expand_json(SeedIds, Relationships, Direction, Depth, EntityTypes, MaxNodes, MaxEdges, JsonString) :-
43
141
  sort(SeedIds, SeedSet),
44
142
  bfs_layers(SeedSet, SeedSet, [], Relationships, Direction, Depth, MaxNodes, MaxEdges, SeenNodes0, SeenEdges0, Truncated0),
@@ -100,9 +198,9 @@ relationship_count(Id, Relationship, Count) :-
100
198
  (kb_relationship(Relationship, Id, _); kb_relationship(Relationship, _, Id)),
101
199
  Count).
102
200
 
103
- coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
201
+ coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, Rows, Summary) :-
104
202
  !,
105
- requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, ProofContext),
203
+ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, ProofContext),
106
204
  findall(Row,
107
205
  requirement_coverage_row(Tags, IncludeTransitive, ProofContext, Row),
108
206
  AllRows),
@@ -140,6 +238,10 @@ coverage_rows(type, _Tags, _IncludePassing, _IncludeTransitive, _VerificationSna
140
238
  length(Rows, Total),
141
239
  Summary = _{total: Total}.
142
240
 
241
+ coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
242
+ !,
243
+ coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, strict_snapshot, _{}, Rows, Summary).
244
+
143
245
  requirement_coverage_row(Tags, IncludeTransitive, ProofContext, Row) :-
144
246
  kb_entity(Id, req, Props),
145
247
  matches_tags(Tags, Props),
package/src/kb.pl CHANGED
@@ -71,6 +71,7 @@
71
71
  :- use_module(library(thread)).
72
72
  :- use_module(library(filesex)).
73
73
  :- use_module(library(readutil)).
74
+ :- use_module(library(http/json)).
74
75
  :- use_module(library(aggregate), [aggregate_all/3]).
75
76
  :- use_module(library(lists), [sum_list/2]).
76
77
  :- use_module(library(ordsets)).
@@ -156,8 +157,14 @@ kb_attach_journaled(Directory) :-
156
157
  ; make_directory_path(PersistencyDirectory)
157
158
  ),
158
159
  % rdf_persistency owns the database lock for the lifetime of the engine.
159
- rdf_attach_db(PersistencyDirectory,
160
- [access(read_write), silent(true), concurrency(4)]),
160
+ % A failed attach throws a structured store_locked term carrying the
161
+ % ownership journal (if any) so callers can surface the holder or take
162
+ % over when the holder is provably dead.
163
+ catch(rdf_attach_db(PersistencyDirectory,
164
+ [access(read_write), silent(true), concurrency(4)]),
165
+ error(permission_error(_, _, _), Original),
166
+ kb_throw_store_locked(PersistencyDirectory, Directory, Original)),
167
+ catch(kb_write_lock_owner(Directory), _, true),
161
168
  % Create RDF graph name from directory. Do not unload a graph here:
162
169
  % rdf_attach_db has already restored it from its snapshot/journal. A
163
170
  % staged migration can have been atomically moved after the graph was
@@ -229,6 +236,58 @@ kb_attach_legacy(Directory) :-
229
236
  kb_rebuild_indexes.
230
237
 
231
238
 
239
+ %% Store lock ownership journal.
240
+ % rdf_persistency holds the branch-store lock while an engine is attached, but
241
+ % records nothing about the holder. The journal bridges that gap: kibi writes
242
+ % who attached (pid, workspace, boot id, started at) next to the lock so a
243
+ % later store_locked failure can surface the holder and self-heal when the
244
+ % holder is provably dead (crash, kill, removed worktree).
245
+ % implements REQ-core-journaled-engine-persistence
246
+
247
+ kb_lock_owner_path(Directory, Path) :-
248
+ atom_concat(Directory, '/.kibi-lock-owner.json', Path).
249
+
250
+ kb_write_lock_owner(Directory) :-
251
+ kb_lock_owner_path(Directory, Path),
252
+ current_prolog_flag(pid, Pid),
253
+ ( exists_file('/proc/sys/kernel/random/boot_id')
254
+ -> catch(( read_file_to_string('/proc/sys/kernel/random/boot_id',
255
+ BootIdRaw, []),
256
+ split_string(BootIdRaw, "\r\n", "\r\n", [BootId|_])),
257
+ _,
258
+ BootId = "")
259
+ ; BootId = ""
260
+ ),
261
+ get_time(Now),
262
+ format_time(atom(StartedAt), '%FT%TZ', Now),
263
+ atom_concat(Path, '.tmp', TmpPath),
264
+ setup_call_cleanup(
265
+ open(TmpPath, write, Stream),
266
+ json_write_dict(Stream,
267
+ json([pid:Pid,
268
+ workspaceRoot:Directory,
269
+ bootId:BootId,
270
+ startedAt:StartedAt]),
271
+ []),
272
+ (close(Stream), rename_file(TmpPath, Path))).
273
+
274
+ kb_remove_lock_owner :-
275
+ ( kb_attached(Directory)
276
+ -> kb_lock_owner_path(Directory, Path),
277
+ catch(delete_file(Path), _, true)
278
+ ; true
279
+ ).
280
+
281
+ kb_throw_store_locked(PersistencyDirectory, Directory, Original) :-
282
+ kb_lock_owner_path(Directory, Path),
283
+ ( exists_file(Path)
284
+ -> catch(read_file_to_string(Path, OwnerJson, []), _, OwnerJson = "")
285
+ ; OwnerJson = ""
286
+ ),
287
+ term_string(Original, OriginalText),
288
+ throw(error(permission_error(attach, kb_store, Directory),
289
+ kb_store_locked(OwnerJson, PersistencyDirectory, OriginalText))).
290
+
232
291
  %% kb_detach
233
292
  % Safely detach from KB without persisting pending changes.
234
293
  % Call kb_save/0 explicitly before kb_detach/0 when durability is required.
@@ -238,7 +297,8 @@ kb_detach :-
238
297
  -> (
239
298
  ( kb_storage_mode(journaled)
240
299
  -> catch(rdf_flush_journals([min_size(16384)]), _, true),
241
- catch(rdf_detach_db, _, true)
300
+ catch(rdf_detach_db, _, true),
301
+ catch(kb_remove_lock_owner, _, true)
242
302
  ; true
243
303
  ),
244
304
  % Unload RDF graph from memory to prevent duplication on reattach
@@ -1836,10 +1896,13 @@ integer_property_key(Key) :-
1836
1896
  literal_to_value(semantic_inventory, Literal, Value) :-
1837
1897
  !,
1838
1898
  structured_literal_value(Literal, Value).
1839
- literal_to_value(verification_receipts, Literal, Value) :-
1899
+ literal_to_value(proof_receipts, Literal, Value) :-
1900
+ !,
1901
+ structured_literal_value(Literal, Value).
1902
+ literal_to_value(proof_contract, Literal, Value) :-
1840
1903
  !,
1841
1904
  structured_literal_value(Literal, Value).
1842
- literal_to_value(verification_contract, Literal, Value) :-
1905
+ literal_to_value(proof_bindings, Literal, Value) :-
1843
1906
  !,
1844
1907
  structured_literal_value(Literal, Value).
1845
1908
  literal_to_value(_Key, Literal, Value) :-