kibi-core 0.11.0 → 0.13.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.13.0",
4
4
  "private": false,
5
5
  "description": "Core Prolog modules and RDF graph logic for Kibi",
6
6
  "type": "module",
@@ -8,10 +8,7 @@
8
8
  "node": ">=18"
9
9
  },
10
10
  "main": "./src/kb.pl",
11
- "files": [
12
- "src/**/*.pl",
13
- "schema/**/*.pl"
14
- ],
11
+ "files": ["src/**/*.pl", "schema/**/*.pl"],
15
12
  "license": "AGPL-3.0-or-later",
16
13
  "author": "Piotr Franczyk",
17
14
  "repository": {
@@ -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
@@ -10,6 +10,7 @@
10
10
  check_must_priority_coverage/1, % Returns list of must-priority violations
11
11
  check_symbol_coverage/1, % Returns list of uncovered symbols
12
12
  check_symbol_traceability/2, % Returns list of symbols lacking requirement traceability (ReqAdr option)
13
+ check_proof_contract_symbols/1, % Returns unresolved or unexpected required_proofs symbol issues
13
14
  check_no_dangling_refs/1, % Returns list of dangling ref violations
14
15
  check_no_cycles/1, % Returns list of cycle violations
15
16
  check_required_fields/1, % Returns list of missing required field violations
@@ -25,6 +26,7 @@
25
26
  check_rule_safety/1,
26
27
  check_rule_verifiability/1,
27
28
  check_semantic_completeness/1,
29
+ check_req_status_vocabulary/1,
28
30
  run_checks_json/0, % Entry point for JSON output
29
31
  violation_id_text/2 % Extract text from entity ID term (exported for testing)
30
32
  ]).
@@ -35,6 +37,8 @@
35
37
  :- use_module('../schema/entities.pl', [entity_type/1, required_property/2]).
36
38
  :- use_module('../schema/relationships.pl', [relationship_type/1]).
37
39
  :- 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]).
40
+ % GENERATED registry facts; single source is schema/rule-registry.json.
41
+ :- use_module('rule_registry.pl', [known_rule/1]).
38
42
 
39
43
  % Required fields for all entities
40
44
  required_fields([id, title, status, created_at, updated_at, source]).
@@ -66,6 +70,8 @@ check_all(ViolationsDict) :-
66
70
  check_rule_safety(RuleSafety),
67
71
  check_rule_verifiability(RuleVerifiability),
68
72
  check_semantic_completeness(SemanticCompleteness),
73
+ check_req_status_vocabulary(ReqStatusVocabulary),
74
+ check_proof_contract_symbols(ProofContractSymbols),
69
75
  ViolationsDict = _{
70
76
  must_priority_coverage: MustPriority,
71
77
  symbol_coverage: SymbolCoverage,
@@ -82,7 +88,9 @@ check_all(ViolationsDict) :-
82
88
  logic_coverage: LogicCoverage,
83
89
  rule_safety: RuleSafety,
84
90
  rule_verifiability: RuleVerifiability,
85
- semantic_completeness: SemanticCompleteness
91
+ semantic_completeness: SemanticCompleteness,
92
+ req_status_vocabulary: ReqStatusVocabulary,
93
+ proof_contract_symbols: ProofContractSymbols
86
94
  }.
87
95
 
88
96
  %% check_must_priority_coverage(-Violations)
@@ -143,6 +151,17 @@ check_symbol_traceability(RequireAdr, Violations) :-
143
151
  ),
144
152
  sort(Violations0, Violations).
145
153
 
154
+ symbol_traceability_violation(_RequireAdr, violation(
155
+ 'symbol-traceability',
156
+ SymbolId,
157
+ "Symbol mixes executable_for test identity with production implements or covered_by.",
158
+ "Split the symbol: keep executable_for on the test-code symbol and use a separate production symbol for implements/covered_by.",
159
+ Source
160
+ )) :-
161
+ kb_entity(SymbolId, symbol, _),
162
+ mixed_role_symbol(SymbolId),
163
+ violation_source(SymbolId, symbol, Source).
164
+
146
165
  symbol_traceability_violation(RequireAdr, violation(
147
166
  'symbol-traceability',
148
167
  SymbolId,
@@ -181,6 +200,118 @@ symbol_traceability_violation(RequireAdr, violation(
181
200
  ),
182
201
  violation_source(SymbolId, symbol, Source).
183
202
 
203
+ %% check_proof_contract_symbols(-Violations)
204
+ % Advisory integrity for proof_contract.required_proofs and optional
205
+ % proof_bindings.source_file. Kibi does not infer TEST names from filenames.
206
+ check_proof_contract_symbols(Violations) :-
207
+ findall(Violation, proof_contract_symbol_violation(Violation), Violations0),
208
+ sort(Violations0, Violations).
209
+
210
+ proof_contract_symbol_violation(violation(
211
+ 'proof-contract-symbols',
212
+ TestId,
213
+ Description,
214
+ Suggestion,
215
+ Source
216
+ )) :-
217
+ kb_entity(TestId, test, Props),
218
+ memberchk(proof_contract=RawContract, Props),
219
+ parse_json_object(RawContract, Contract),
220
+ required_proof_symbol_id(Contract, SymbolId),
221
+ \+ kb_entity(SymbolId, symbol, _),
222
+ format(string(Description), "proof_contract.required_proofs names unresolved symbol ~w", [SymbolId]),
223
+ Suggestion = "Point required_proofs.symbol_id at an existing symbol entity; Kibi does not infer TEST names from source filenames.",
224
+ violation_source(TestId, test, Source).
225
+
226
+ proof_contract_symbol_violation(violation(
227
+ 'proof-contract-symbols',
228
+ TestId,
229
+ Description,
230
+ Suggestion,
231
+ Source
232
+ )) :-
233
+ kb_entity(TestId, test, Props),
234
+ memberchk(proof_contract=RawContract, Props),
235
+ parse_json_object(RawContract, Contract),
236
+ required_proof_symbol_id(Contract, SymbolId),
237
+ kb_entity(SymbolId, symbol, SymbolProps),
238
+ memberchk(symbol_role=RawRole, SymbolProps),
239
+ normalize_term_atom(RawRole, 'type-shape'),
240
+ format(string(Description), "proof_contract.required_proofs names type-shape symbol ~w; required proofs must not target structural contracts", [SymbolId]),
241
+ Suggestion = "Required proofs identify executable proof symbols. Use a production or executable-test symbol, not a type-shape contract.",
242
+ violation_source(TestId, test, Source).
243
+
244
+ proof_contract_symbol_violation(violation(
245
+ 'proof-contract-symbols',
246
+ TestId,
247
+ Description,
248
+ Suggestion,
249
+ Source
250
+ )) :-
251
+ kb_entity(TestId, test, Props),
252
+ memberchk(proof_bindings=RawBindings, Props),
253
+ parse_json_list(RawBindings, Bindings),
254
+ member(Binding, Bindings),
255
+ is_dict(Binding),
256
+ (get_dict(source_file, Binding, RawBindingSource) ; get_dict(sourceFile, Binding, RawBindingSource)),
257
+ normalize_term_atom(RawBindingSource, BindingSource),
258
+ BindingSource \= '',
259
+ (get_dict(symbol_id, Binding, RawSymbolId) ; get_dict(symbolId, Binding, RawSymbolId)),
260
+ normalize_term_atom(RawSymbolId, SymbolId),
261
+ SymbolId \= '',
262
+ kb_entity(SymbolId, symbol, SymbolProps),
263
+ ( memberchk(sourceFile=RawSymbolSource, SymbolProps)
264
+ -> normalize_term_atom(RawSymbolSource, SymbolSource)
265
+ ; SymbolSource = ''
266
+ ),
267
+ SymbolSource \= BindingSource,
268
+ format(string(Description), "proof_bindings.source_file for ~w is ~w but the symbol sourceFile is ~w", [SymbolId, BindingSource, SymbolSource]),
269
+ Suggestion = "Align proof_bindings.source_file with the symbol's sourceFile, or omit source_file. Kibi does not infer TEST names from filenames.",
270
+ violation_source(TestId, test, Source).
271
+
272
+ required_proof_symbol_id(Contract, SymbolId) :-
273
+ ( get_dict(required_proofs, Contract, Raw)
274
+ ; get_dict('required_proofs', Contract, Raw)
275
+ ),
276
+ parse_json_list(Raw, Entries),
277
+ member(Entry, Entries),
278
+ ( is_dict(Entry),
279
+ (get_dict(symbol_id, Entry, RawId) ; get_dict(symbolId, Entry, RawId))
280
+ ; is_list(Entry),
281
+ memberchk(symbol_id=RawId, Entry)
282
+ ),
283
+ normalize_term_atom(RawId, SymbolId),
284
+ SymbolId \= ''.
285
+
286
+ parse_json_object(^^(Value, _), Dict) :-
287
+ !,
288
+ parse_json_object(Value, Dict).
289
+ parse_json_object(literal(type(_, Value)), Dict) :-
290
+ !,
291
+ parse_json_object(Value, Dict).
292
+ parse_json_object(literal(Value), Dict) :-
293
+ !,
294
+ parse_json_object(Value, Dict).
295
+ parse_json_object(Dict, Dict) :-
296
+ is_dict(Dict),
297
+ !.
298
+ parse_json_object(Raw, Dict) :-
299
+ (atom(Raw) ; string(Raw)),
300
+ catch(atom_json_dict(Raw, Dict, [value_string_as(string)]), _, fail),
301
+ is_dict(Dict).
302
+
303
+ parse_json_list(Raw, Entries) :-
304
+ is_list(Raw),
305
+ !,
306
+ Entries = Raw.
307
+ parse_json_list(^^(Value, _), Entries) :-
308
+ !,
309
+ parse_json_list(Value, Entries).
310
+ parse_json_list(Raw, Entries) :-
311
+ (atom(Raw) ; string(Raw)),
312
+ catch(atom_json_dict(Raw, Entries, [value_string_as(string)]), _, fail),
313
+ is_list(Entries).
314
+
184
315
  %% check_no_dangling_refs(-Violations)
185
316
  % Finds all relationships referencing non-existent entities.
186
317
  check_no_dangling_refs(Violations) :-
@@ -363,6 +494,39 @@ deprecated_adr_violation(violation(
363
494
  ; Source = ""
364
495
  ).
365
496
 
497
+ %% check_req_status_vocabulary(-Violations)
498
+ % Rejects requirement statuses outside the canonical+legacy vocabulary.
499
+ % Requirement documents carrying ADR vocabulary (e.g. `status: accepted`)
500
+ % compile and pass schema validation, then silently fall out of
501
+ % current_req/1 — the proof ladder reports them not_applicable with no
502
+ % signal. This rule surfaces the vocabulary mismatch at check time instead.
503
+ canonical_req_statuses([open, in_progress, closed]).
504
+ legacy_req_statuses([active, approved]).
505
+
506
+ check_req_status_vocabulary(Violations) :-
507
+ findall(
508
+ Violation,
509
+ req_status_vocabulary_violation(Violation),
510
+ Violations
511
+ ).
512
+
513
+ req_status_vocabulary_violation(violation(
514
+ 'req-status-vocabulary',
515
+ ReqId,
516
+ Description,
517
+ Suggestion,
518
+ Source
519
+ )) :-
520
+ kb_entity(ReqId, req, Props),
521
+ memberchk(status=RawStatus, Props),
522
+ normalize_term_atom(RawStatus, StatusAtom),
523
+ canonical_req_statuses(Canonical),
524
+ legacy_req_statuses(Legacy),
525
+ \+ (memberchk(StatusAtom, Canonical) ; memberchk(StatusAtom, Legacy)),
526
+ format(string(Description), "Requirement status '~w' is not a current requirement status; it is silently excluded from the proof ladder", [StatusAtom]),
527
+ 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", []),
528
+ violation_source(ReqId, req, Source).
529
+
366
530
  %% check_strict_fact_shape(-Violations)
367
531
  % Finds all strict facts (with fact_kind) that have malformed shape.
368
532
  % Only checks facts with fact_kind present; legacy facts without fact_kind are ignored.
@@ -1412,7 +1576,18 @@ check_selected_json(Rules, JsonString) :-
1412
1576
  JsonString
1413
1577
  ).
1414
1578
 
1415
- check_selected(Rules, _{
1579
+ %% check_selected(+Rules, -ViolationsDict)
1580
+ % Run only the named aggregated checks. This is intentionally separate from
1581
+ % check_all/1 so focused diagnostics cannot evaluate unrelated rule queries.
1582
+ % Unknown rule names fail loudly: a typo must never masquerade as a clean,
1583
+ % violation-free run. Rules implemented on the TypeScript side (recorded in
1584
+ % rule_registry.pl with implementation typescript) are accepted here and
1585
+ % simply produce no Prolog-side violations.
1586
+ check_selected(Rules, ViolationsDict) :-
1587
+ require_known_rules(Rules),
1588
+ check_selected_dispatch(Rules, ViolationsDict).
1589
+
1590
+ check_selected_dispatch(Rules, _{
1416
1591
  must_priority_coverage: MustPriority,
1417
1592
  symbol_coverage: SymbolCoverage,
1418
1593
  symbol_traceability: SymbolTraceability,
@@ -1428,7 +1603,9 @@ check_selected(Rules, _{
1428
1603
  logic_coverage: LogicCoverage,
1429
1604
  rule_safety: RuleSafety,
1430
1605
  rule_verifiability: RuleVerifiability,
1431
- semantic_completeness: SemanticCompleteness
1606
+ semantic_completeness: SemanticCompleteness,
1607
+ req_status_vocabulary: ReqStatusVocabulary,
1608
+ proof_contract_symbols: ProofContractSymbols
1432
1609
  }) :-
1433
1610
  selected_rule(Rules, 'must-priority-coverage', check_must_priority_coverage, MustPriority),
1434
1611
  selected_rule(Rules, 'symbol-coverage', check_symbol_coverage, SymbolCoverage),
@@ -1445,7 +1622,9 @@ check_selected(Rules, _{
1445
1622
  selected_rule(Rules, 'logic-coverage', check_logic_coverage, LogicCoverage),
1446
1623
  selected_rule(Rules, 'rule-safety', check_rule_safety, RuleSafety),
1447
1624
  selected_rule(Rules, 'rule-verifiability', check_rule_verifiability, RuleVerifiability),
1448
- selected_rule(Rules, 'semantic-completeness', check_semantic_completeness, SemanticCompleteness).
1625
+ selected_rule(Rules, 'semantic-completeness', check_semantic_completeness, SemanticCompleteness),
1626
+ selected_rule(Rules, 'req-status-vocabulary', check_req_status_vocabulary, ReqStatusVocabulary),
1627
+ selected_rule(Rules, 'proof-contract-symbols', check_proof_contract_symbols, ProofContractSymbols).
1449
1628
 
1450
1629
  selected_rule(Rules, Name, Goal, Violations) :-
1451
1630
  ( memberchk(Name, Rules)
@@ -1459,6 +1638,17 @@ selected_rule_with_options(Rules, Name, Goal, Violations) :-
1459
1638
  ; Violations = []
1460
1639
  ).
1461
1640
 
1641
+ %% require_known_rules(+Rules)
1642
+ % Fail closed on names outside the generated registry so a typo or a
1643
+ % stale cross-version call surfaces as an error instead of an empty result.
1644
+ require_known_rules(Rules) :-
1645
+ exclude(rule_registry:known_rule, Rules, Unknown),
1646
+ ( Unknown == []
1647
+ -> true
1648
+ ; throw(error(domain_error(check_rule, Unknown),
1649
+ context(check_selected, 'Unknown check rules requested')))
1650
+ ).
1651
+
1462
1652
  %% check_all_json_with_options(-JsonString, +RequireAdr)
1463
1653
  % Returns all violations as JSON string with options.
1464
1654
  % RequireAdr: if true, symbol-traceability also requires ADR constraints.
@@ -1489,6 +1679,8 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
1489
1679
  check_rule_safety(RuleSafety),
1490
1680
  check_rule_verifiability(RuleVerifiability),
1491
1681
  check_semantic_completeness(SemanticCompleteness),
1682
+ check_req_status_vocabulary(ReqStatusVocabulary),
1683
+ check_proof_contract_symbols(ProofContractSymbols),
1492
1684
  ViolationsDict = _{
1493
1685
  must_priority_coverage: MustPriority,
1494
1686
  symbol_coverage: SymbolCoverage,
@@ -1505,7 +1697,9 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
1505
1697
  logic_coverage: LogicCoverage,
1506
1698
  rule_safety: RuleSafety,
1507
1699
  rule_verifiability: RuleVerifiability,
1508
- semantic_completeness: SemanticCompleteness
1700
+ semantic_completeness: SemanticCompleteness,
1701
+ req_status_vocabulary: ReqStatusVocabulary,
1702
+ proof_contract_symbols: ProofContractSymbols
1509
1703
  }.
1510
1704
 
1511
1705
  %% violations_dict_to_json(+ViolationsDict, -JsonDict)
package/src/discovery.pl CHANGED
@@ -5,13 +5,22 @@
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,
11
+ requirement_proof_json/5,
12
+ requirement_proof_json/7,
13
+ symbol_proof_json/7,
8
14
  graph_expand_json/8
9
15
  ]).
10
16
 
11
17
  :- use_module(library(http/json)).
12
18
  :- use_module(library(aggregate)).
19
+ :- use_module(library(apply)).
20
+ :- use_module(library(lists)).
21
+ :- use_module(library(pairs)).
13
22
  :- use_module('kb.pl').
14
- :- use_module('requirement_proof.pl', [requirement_proof_context/1, requirement_proof_context/4, requirement_proof/4]).
23
+ :- use_module('requirement_proof.pl', [requirement_proof_context/1, requirement_proof_context/4, requirement_proof_context/6, requirement_proof/4]).
15
24
  :- use_module('status.pl', [status_meta_dict/1]).
16
25
  :- use_module('../schema/relationships.pl', [relationship_type/1]).
17
26
 
@@ -32,13 +41,157 @@ coverage_report_json(By, Tags, IncludePassing, IncludeTransitive, Limit, Offset,
32
41
  coverage_report_json(By, Tags, IncludePassing, IncludeTransitive, Limit, Offset, unknown, '1970-01-01T00:00:00Z', 604800, JsonString).
33
42
 
34
43
  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),
44
+ coverage_report_json(By, Tags, IncludePassing, strict_snapshot, _{}, IncludeTransitive, Limit, Offset, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString).
45
+
46
+ requirement_proof_json(ReqId, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
47
+ requirement_proof_json(ReqId, strict_snapshot, _{}, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString).
48
+
49
+ requirement_proof_json(ReqId, BindingMode, TestBindings, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
50
+ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, Context),
51
+ kb_entity(ReqId, req, Props),
52
+ requirement_proof(ReqId, Props, Context, Proof),
53
+ put_dict(id, Proof, ReqId, ProofWithId),
54
+ dict_json_string(ProofWithId, JsonString).
55
+
56
+ symbol_proof_json(SymbolId, BindingMode, TestBindings, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
57
+ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, Context),
58
+ kb_entity(SymbolId, symbol, _),
59
+ findall(ReqId,
60
+ (kb_relationship(implements, SymbolId, ReqId), kb_entity(ReqId, req, _)),
61
+ ReqIds0),
62
+ sort(ReqIds0, ReqIds),
63
+ maplist(requirement_proof_with_id(Context), ReqIds, Proofs),
64
+ symbol_explain_role(SymbolId, Role),
65
+ (kb:executable_test_symbol(SymbolId) -> ExecutableTest = true ; ExecutableTest = false),
66
+ findall(TestId, kb_relationship(executable_for, SymbolId, TestId), ExecutableFor0),
67
+ sort(ExecutableFor0, ExecutableFor),
68
+ findall(TestId, kb_relationship(covered_by, SymbolId, TestId), CoveredBy0),
69
+ sort(CoveredBy0, CoveredBy),
70
+ Response = _{
71
+ id: SymbolId,
72
+ symbolId: SymbolId,
73
+ role: Role,
74
+ executableTest: ExecutableTest,
75
+ implementingRequirements: ReqIds,
76
+ executableFor: ExecutableFor,
77
+ coveredBy: CoveredBy,
78
+ proofs: Proofs
79
+ },
80
+ dict_json_string(Response, JsonString).
81
+
82
+ requirement_proof_with_id(Context, ReqId, ProofWithId) :-
83
+ kb_entity(ReqId, req, Props),
84
+ requirement_proof(ReqId, Props, Context, Proof),
85
+ put_dict(id, Proof, ReqId, ProofWithId).
86
+
87
+ symbol_explain_role(SymbolId, executable_test) :-
88
+ kb:executable_test_symbol(SymbolId),
89
+ !.
90
+ symbol_explain_role(SymbolId, Role) :-
91
+ kb_entity(SymbolId, symbol, Props),
92
+ memberchk(symbol_role=RawRole, Props),
93
+ kb:normalize_term_atom(RawRole, Role),
94
+ Role \= '',
95
+ !.
96
+ symbol_explain_role(_SymbolId, unknown).
97
+
98
+ % Slim per-requirement proof evidence for quality diagnostics (W1 push-down).
99
+ % Projects only the fields the quality diagnostics consume - proof status,
100
+ % the passing-e2e stage summary, and receipt-related gap codes - instead of
101
+ % the full coverage row set, keeping the response small on large KBs.
102
+ coverage_evidence_json(Tags, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
103
+ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, ProofContext),
104
+ findall(Evidence,
105
+ ( kb_entity(Id, req, Props),
106
+ matches_tags(Tags, Props),
107
+ requirement_proof(Id, Props, ProofContext, Proof),
108
+ coverage_evidence_row(Id, Proof, Evidence)
109
+ ),
110
+ Evidences),
111
+ Response = _{rows: Evidences},
112
+ dict_json_string(Response, JsonString).
113
+
114
+ coverage_evidence_row(Id, Proof, Evidence) :-
115
+ ( get_dict(proofStatus, Proof, ProofStatus)
116
+ -> true
117
+ ; ProofStatus = unknown
118
+ ),
119
+ ( get_dict(proofStages, Proof, Stages),
120
+ get_dict(passingE2e, Stages, PassingE2e),
121
+ get_dict(status, PassingE2e, PassingE2eStatus)
122
+ -> ( get_dict(tests, PassingE2e, PassingE2eTests)
123
+ -> true
124
+ ; PassingE2eTests = []
125
+ )
126
+ ; PassingE2eStatus = unknown,
127
+ PassingE2eTests = []
128
+ ),
129
+ ( get_dict(proofGaps, Proof, Gaps)
130
+ -> include(coverage_evidence_receipt_gap, Gaps, ReceiptGapCodes)
131
+ ; ReceiptGapCodes = []
132
+ ),
133
+ Evidence = _{
134
+ id: Id,
135
+ proofStatus: ProofStatus,
136
+ passingE2eStatus: PassingE2eStatus,
137
+ passingE2eTests: PassingE2eTests,
138
+ receiptGapCodes: ReceiptGapCodes
139
+ }.
140
+
141
+ coverage_evidence_receipt_gap(Gap) :-
142
+ ( atom(Gap)
143
+ -> Atom = Gap
144
+ ; format(atom(Atom), "~w", [Gap])
145
+ ),
146
+ ( sub_atom(Atom, _, _, _, 'proof_receipt')
147
+ ; sub_atom(Atom, _, _, _, 'proof_contract')
148
+ ; sub_atom(Atom, _, _, _, 'proof_snapshot')
149
+ ).
150
+
151
+ % Per-contract receipt binding (W2): BindingMode strict_snapshot | per_contract
152
+ % with TestBindings mapping TestId -> current binding hash.
153
+ coverage_report_json(By, Tags, IncludePassing, BindingMode, TestBindings, IncludeTransitive, Limit, Offset, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
154
+ % Binding mode applies to requirement rows only; other By values keep
155
+ % their snapshot-era arity so type/symbol reports are unchanged.
156
+ ( By == req
157
+ -> coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, Rows0, Summary)
158
+ ; coverage_rows(By, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows0, Summary)
159
+ ),
36
160
  sort_dict_rows(Rows0, SortedRows),
37
161
  paginate_rows(SortedRows, Offset, Limit, Rows),
38
162
  status_meta_dict(Meta),
39
163
  Response = _{summary: Summary, rows: Rows, meta: Meta},
40
164
  dict_json_string(Response, JsonString).
41
165
 
166
+ % implements REQ-kibi-coverage-status-filter
167
+ % Proof-status filtered report. The status filter selects requirement rows by
168
+ % proofStatus (proven, missing, unresolved, not_applicable) and replaces the
169
+ % default include-passing filtering, so callers can enumerate exactly one
170
+ % slice — e.g. every not_applicable row together with its typed applicability
171
+ % reason — without diffing full exports by hand. The summary is always
172
+ % computed over all rows so filtered responses keep whole-KB counts.
173
+ coverage_report_json(By, Tags, IncludePassing, StatusFilter, IncludeTransitive, Limit, Offset, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
174
+ status_filtered_coverage_rows(By, Tags, IncludePassing, StatusFilter, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, MatchedRows0, Summary),
175
+ sort_dict_rows(MatchedRows0, SortedRows),
176
+ paginate_rows(SortedRows, Offset, Limit, Rows),
177
+ status_meta_dict(Meta),
178
+ Response = _{summary: Summary, rows: Rows, meta: Meta},
179
+ dict_json_string(Response, JsonString).
180
+
181
+ status_filtered_coverage_rows(By, Tags, IncludePassing, [], IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
182
+ !,
183
+ coverage_rows(By, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary).
184
+ status_filtered_coverage_rows(req, Tags, _IncludePassing, StatusFilter, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, MatchedRows, Summary) :-
185
+ !,
186
+ coverage_rows(req, Tags, true, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows0, Summary),
187
+ include(req_row_proof_status_in(StatusFilter), Rows0, MatchedRows).
188
+ status_filtered_coverage_rows(By, Tags, _IncludePassing, _StatusFilter, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
189
+ coverage_rows(By, Tags, true, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary).
190
+
191
+ req_row_proof_status_in(StatusFilter, Row) :-
192
+ Status = Row.proofStatus,
193
+ memberchk(Status, StatusFilter).
194
+
42
195
  graph_expand_json(SeedIds, Relationships, Direction, Depth, EntityTypes, MaxNodes, MaxEdges, JsonString) :-
43
196
  sort(SeedIds, SeedSet),
44
197
  bfs_layers(SeedSet, SeedSet, [], Relationships, Direction, Depth, MaxNodes, MaxEdges, SeenNodes0, SeenEdges0, Truncated0),
@@ -100,9 +253,9 @@ relationship_count(Id, Relationship, Count) :-
100
253
  (kb_relationship(Relationship, Id, _); kb_relationship(Relationship, _, Id)),
101
254
  Count).
102
255
 
103
- coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
256
+ coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, Rows, Summary) :-
104
257
  !,
105
- requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, ProofContext),
258
+ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, ProofContext),
106
259
  findall(Row,
107
260
  requirement_coverage_row(Tags, IncludeTransitive, ProofContext, Row),
108
261
  AllRows),
@@ -140,6 +293,10 @@ coverage_rows(type, _Tags, _IncludePassing, _IncludeTransitive, _VerificationSna
140
293
  length(Rows, Total),
141
294
  Summary = _{total: Total}.
142
295
 
296
+ coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
297
+ !,
298
+ coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, strict_snapshot, _{}, Rows, Summary).
299
+
143
300
  requirement_coverage_row(Tags, IncludeTransitive, ProofContext, Row) :-
144
301
  kb_entity(Id, req, Props),
145
302
  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) :-