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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-core",
3
- "version": "0.12.0",
3
+ "version": "0.14.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": {
@@ -65,6 +65,9 @@ entity_property(fact, predicate_arity, integer).
65
65
  entity_property(fact, argument_names, list).
66
66
  entity_property(fact, argument_types, list).
67
67
  entity_property(fact, argument_descriptions, list).
68
+ % Predicate-schema vocabularies are JSON objects serialized as strings.
69
+ entity_property(fact, argument_constants, string).
70
+ entity_property(fact, argument_aliases, string).
68
71
  entity_property(fact, aliases, list).
69
72
  entity_property(fact, examples, list).
70
73
  entity_property(fact, predicate_args, list).
@@ -17,6 +17,7 @@ relationship_type(publishes).
17
17
  relationship_type(consumes).
18
18
  relationship_type(relates_to).
19
19
  relationship_type(supersedes).
20
+ relationship_type(restates).
20
21
  relationship_type(constrains).
21
22
  relationship_type(requires_property).
22
23
  relationship_type(requires_predicate).
@@ -49,6 +50,13 @@ valid_relationship(requires_rule, req, fact).
49
50
  %% OldAdrId's status should be archived or deprecated as a consequence.
50
51
  valid_relationship(supersedes, adr, adr).
51
52
  valid_relationship(supersedes, req, req).
53
+
54
+ %% restates(+ReqId, +OtherReqId)
55
+ %% ReqId intentionally restates OtherReqId (for example a product requirement
56
+ %% echoed in a platform requirement). Both stay current; the pair is exempt
57
+ %% from domain-redundancy. Use supersedes when one replaces the other.
58
+ % implements REQ-kibi-restates-relationship
59
+ valid_relationship(restates, req, req).
52
60
  % escape hatch - allow any to any
53
61
  valid_relationship(relates_to, _, _).
54
62
 
@@ -105,6 +105,8 @@ is_fact_only_field(predicate_arity).
105
105
  is_fact_only_field(argument_names).
106
106
  is_fact_only_field(argument_types).
107
107
  is_fact_only_field(argument_descriptions).
108
+ is_fact_only_field(argument_constants).
109
+ is_fact_only_field(argument_aliases).
108
110
  is_fact_only_field(aliases).
109
111
  is_fact_only_field(examples).
110
112
  is_fact_only_field(predicate_args).
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
@@ -26,15 +27,33 @@
26
27
  check_rule_verifiability/1,
27
28
  check_semantic_completeness/1,
28
29
  check_req_status_vocabulary/1,
30
+ check_entity_id_style/1,
31
+ check_domain_redundancy/1,
32
+ check_domain_implication/1,
33
+ check_subject_key_identity/1,
34
+ check_subject_key_shape/1,
35
+ check_ontology_quality/1,
29
36
  run_checks_json/0, % Entry point for JSON output
30
37
  violation_id_text/2 % Extract text from entity ID term (exported for testing)
31
38
  ]).
32
39
 
40
+ % Source contains non-ASCII text; do not depend on the host locale.
41
+ :- encoding(utf8).
42
+
33
43
  :- use_module(library(http/json)).
34
44
  :- use_module(library(http/json_convert)).
35
45
  :- use_module('kb.pl').
36
46
  :- use_module('../schema/entities.pl', [entity_type/1, required_property/2]).
37
47
  :- use_module('../schema/relationships.pl', [relationship_type/1]).
48
+ :- use_module('semantic_quality.pl', [
49
+ logical_ground_signature/2,
50
+ check_entity_id_style/1,
51
+ check_domain_redundancy/1,
52
+ check_domain_implication/1,
53
+ check_subject_key_identity/1,
54
+ check_subject_key_shape/1,
55
+ check_ontology_quality/1
56
+ ]).
38
57
  :- 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
58
  % GENERATED registry facts; single source is schema/rule-registry.json.
40
59
  :- use_module('rule_registry.pl', [known_rule/1]).
@@ -45,7 +64,7 @@ required_fields([id, title, status, created_at, updated_at, source]).
45
64
  % Relationship types to check for dangling references
46
65
  all_relationship_types([
47
66
  depends_on, verified_by, validates, specified_by,
48
- constrains, requires_property, requires_predicate, requires_rule, supersedes, relates_to
67
+ constrains, requires_property, requires_predicate, requires_rule, supersedes, restates, relates_to
49
68
  ]).
50
69
 
51
70
  %% check_all(-ViolationsDict)
@@ -70,6 +89,13 @@ check_all(ViolationsDict) :-
70
89
  check_rule_verifiability(RuleVerifiability),
71
90
  check_semantic_completeness(SemanticCompleteness),
72
91
  check_req_status_vocabulary(ReqStatusVocabulary),
92
+ check_proof_contract_symbols(ProofContractSymbols),
93
+ check_entity_id_style(EntityIdStyle),
94
+ check_domain_redundancy(DomainRedundancy),
95
+ check_domain_implication(DomainImplication),
96
+ check_subject_key_identity(SubjectKeyIdentity),
97
+ check_subject_key_shape(SubjectKeyShape),
98
+ check_ontology_quality(OntologyQuality),
73
99
  ViolationsDict = _{
74
100
  must_priority_coverage: MustPriority,
75
101
  symbol_coverage: SymbolCoverage,
@@ -87,7 +113,14 @@ check_all(ViolationsDict) :-
87
113
  rule_safety: RuleSafety,
88
114
  rule_verifiability: RuleVerifiability,
89
115
  semantic_completeness: SemanticCompleteness,
90
- req_status_vocabulary: ReqStatusVocabulary
116
+ req_status_vocabulary: ReqStatusVocabulary,
117
+ proof_contract_symbols: ProofContractSymbols,
118
+ entity_id_style: EntityIdStyle,
119
+ domain_redundancy: DomainRedundancy,
120
+ domain_implication: DomainImplication,
121
+ subject_key_identity: SubjectKeyIdentity,
122
+ subject_key_shape: SubjectKeyShape,
123
+ ontology_quality: OntologyQuality
91
124
  }.
92
125
 
93
126
  %% check_must_priority_coverage(-Violations)
@@ -131,7 +164,7 @@ symbol_coverage_violation(SymbolId, violation(
131
164
  'symbol-coverage',
132
165
  SymbolId,
133
166
  "Production symbol lacks qualifying requirement coverage.",
134
- "Add 'covered_by: TEST-xxx' for production coverage. If the requirement has specified_by a scenario, use verified_by(scenario,test) or validates(test,scenario). Direct verified_by(req,test) does not count when a scenario exists.",
167
+ "Add 'covered_by: TEST-<area>-<behavior>' for production coverage. If the requirement has specified_by a scenario, use verified_by(scenario,test) or validates(test,scenario). Direct verified_by(req,test) does not count when a scenario exists.",
135
168
  Source
136
169
  )) :-
137
170
  violation_source(SymbolId, symbol, Source).
@@ -148,6 +181,17 @@ check_symbol_traceability(RequireAdr, Violations) :-
148
181
  ),
149
182
  sort(Violations0, Violations).
150
183
 
184
+ symbol_traceability_violation(_RequireAdr, violation(
185
+ 'symbol-traceability',
186
+ SymbolId,
187
+ "Symbol mixes executable_for test identity with production implements or covered_by.",
188
+ "Split the symbol: keep executable_for on the test-code symbol and use a separate production symbol for implements/covered_by.",
189
+ Source
190
+ )) :-
191
+ kb_entity(SymbolId, symbol, _),
192
+ mixed_role_symbol(SymbolId),
193
+ violation_source(SymbolId, symbol, Source).
194
+
151
195
  symbol_traceability_violation(RequireAdr, violation(
152
196
  'symbol-traceability',
153
197
  SymbolId,
@@ -175,17 +219,129 @@ symbol_traceability_violation(RequireAdr, violation(
175
219
  % Determine what is missing
176
220
  ( HasReq = false, HasAdr = false, RequireAdr = true ->
177
221
  Description = "Symbol has no direct requirement ownership and no ADR constraint.",
178
- Suggestion = "Add a direct 'implements: REQ-xxx' link for ownership, use 'covered_by' only for production coverage, use 'executable_for' only for executable test code, and add 'constrained_by: ADR-xxx' in symbols.yaml."
222
+ Suggestion = "Add a direct 'implements: REQ-<area>-<behavior>' link for ownership, use 'covered_by' only for production coverage, use 'executable_for' only for executable test code, and add 'constrained_by: ADR-<decision>' in symbols.yaml."
179
223
  ; HasReq = false ->
180
224
  Description = "Symbol has no direct requirement ownership.",
181
- Suggestion = "Add a direct 'implements: REQ-xxx' link for ownership. Use 'covered_by' for production coverage and 'executable_for' for executable test code identity."
225
+ Suggestion = "Add a direct 'implements: REQ-<area>-<behavior>' link for ownership. Use 'covered_by' for production coverage and 'executable_for' for executable test code identity."
182
226
  ; HasAdr = false ->
183
227
  Description = "Symbol has no ADR constraint.",
184
- Suggestion = "Add 'constrained_by: ADR-xxx' in symbols.yaml."
228
+ Suggestion = "Add 'constrained_by: ADR-<decision>' in symbols.yaml."
185
229
  ; fail % No violation
186
230
  ),
187
231
  violation_source(SymbolId, symbol, Source).
188
232
 
233
+ %% check_proof_contract_symbols(-Violations)
234
+ % Advisory integrity for proof_contract.required_proofs and optional
235
+ % proof_bindings.source_file. Kibi does not infer TEST names from filenames.
236
+ check_proof_contract_symbols(Violations) :-
237
+ findall(Violation, proof_contract_symbol_violation(Violation), Violations0),
238
+ sort(Violations0, Violations).
239
+
240
+ proof_contract_symbol_violation(violation(
241
+ 'proof-contract-symbols',
242
+ TestId,
243
+ Description,
244
+ Suggestion,
245
+ Source
246
+ )) :-
247
+ kb_entity(TestId, test, Props),
248
+ memberchk(proof_contract=RawContract, Props),
249
+ parse_json_object(RawContract, Contract),
250
+ required_proof_symbol_id(Contract, SymbolId),
251
+ \+ kb_entity(SymbolId, symbol, _),
252
+ format(string(Description), "proof_contract.required_proofs names unresolved symbol ~w", [SymbolId]),
253
+ Suggestion = "Point required_proofs.symbol_id at an existing symbol entity; Kibi does not infer TEST names from source filenames.",
254
+ violation_source(TestId, test, Source).
255
+
256
+ proof_contract_symbol_violation(violation(
257
+ 'proof-contract-symbols',
258
+ TestId,
259
+ Description,
260
+ Suggestion,
261
+ Source
262
+ )) :-
263
+ kb_entity(TestId, test, Props),
264
+ memberchk(proof_contract=RawContract, Props),
265
+ parse_json_object(RawContract, Contract),
266
+ required_proof_symbol_id(Contract, SymbolId),
267
+ kb_entity(SymbolId, symbol, SymbolProps),
268
+ memberchk(symbol_role=RawRole, SymbolProps),
269
+ normalize_term_atom(RawRole, 'type-shape'),
270
+ format(string(Description), "proof_contract.required_proofs names type-shape symbol ~w; required proofs must not target structural contracts", [SymbolId]),
271
+ Suggestion = "Required proofs identify executable proof symbols. Use a production or executable-test symbol, not a type-shape contract.",
272
+ violation_source(TestId, test, Source).
273
+
274
+ proof_contract_symbol_violation(violation(
275
+ 'proof-contract-symbols',
276
+ TestId,
277
+ Description,
278
+ Suggestion,
279
+ Source
280
+ )) :-
281
+ kb_entity(TestId, test, Props),
282
+ memberchk(proof_bindings=RawBindings, Props),
283
+ parse_json_list(RawBindings, Bindings),
284
+ member(Binding, Bindings),
285
+ is_dict(Binding),
286
+ (get_dict(source_file, Binding, RawBindingSource) ; get_dict(sourceFile, Binding, RawBindingSource)),
287
+ normalize_term_atom(RawBindingSource, BindingSource),
288
+ BindingSource \= '',
289
+ (get_dict(symbol_id, Binding, RawSymbolId) ; get_dict(symbolId, Binding, RawSymbolId)),
290
+ normalize_term_atom(RawSymbolId, SymbolId),
291
+ SymbolId \= '',
292
+ kb_entity(SymbolId, symbol, SymbolProps),
293
+ ( memberchk(sourceFile=RawSymbolSource, SymbolProps)
294
+ -> normalize_term_atom(RawSymbolSource, SymbolSource)
295
+ ; SymbolSource = ''
296
+ ),
297
+ SymbolSource \= BindingSource,
298
+ format(string(Description), "proof_bindings.source_file for ~w is ~w but the symbol sourceFile is ~w", [SymbolId, BindingSource, SymbolSource]),
299
+ Suggestion = "Align proof_bindings.source_file with the symbol's sourceFile, or omit source_file. Kibi does not infer TEST names from filenames.",
300
+ violation_source(TestId, test, Source).
301
+
302
+ required_proof_symbol_id(Contract, SymbolId) :-
303
+ ( get_dict(required_proofs, Contract, Raw)
304
+ ; get_dict('required_proofs', Contract, Raw)
305
+ ),
306
+ parse_json_list(Raw, Entries),
307
+ member(Entry, Entries),
308
+ ( is_dict(Entry),
309
+ (get_dict(symbol_id, Entry, RawId) ; get_dict(symbolId, Entry, RawId))
310
+ ; is_list(Entry),
311
+ memberchk(symbol_id=RawId, Entry)
312
+ ),
313
+ normalize_term_atom(RawId, SymbolId),
314
+ SymbolId \= ''.
315
+
316
+ parse_json_object(^^(Value, _), Dict) :-
317
+ !,
318
+ parse_json_object(Value, Dict).
319
+ parse_json_object(literal(type(_, Value)), Dict) :-
320
+ !,
321
+ parse_json_object(Value, Dict).
322
+ parse_json_object(literal(Value), Dict) :-
323
+ !,
324
+ parse_json_object(Value, Dict).
325
+ parse_json_object(Dict, Dict) :-
326
+ is_dict(Dict),
327
+ !.
328
+ parse_json_object(Raw, Dict) :-
329
+ (atom(Raw) ; string(Raw)),
330
+ catch(atom_json_dict(Raw, Dict, [value_string_as(string)]), _, fail),
331
+ is_dict(Dict).
332
+
333
+ parse_json_list(Raw, Entries) :-
334
+ is_list(Raw),
335
+ !,
336
+ Entries = Raw.
337
+ parse_json_list(^^(Value, _), Entries) :-
338
+ !,
339
+ parse_json_list(Value, Entries).
340
+ parse_json_list(Raw, Entries) :-
341
+ (atom(Raw) ; string(Raw)),
342
+ catch(atom_json_dict(Raw, Entries, [value_string_as(string)]), _, fail),
343
+ is_list(Entries).
344
+
189
345
  %% check_no_dangling_refs(-Violations)
190
346
  % Finds all relationships referencing non-existent entities.
191
347
  check_no_dangling_refs(Violations) :-
@@ -1181,32 +1337,6 @@ grounded_requirement_claim_fact(ReqId, ClaimKey, FactId) :-
1181
1337
  kb_relationship(requires_rule, ReqId, FactId),
1182
1338
  ground_fact_claim_key(FactId, rule, ClaimKey).
1183
1339
 
1184
- logical_ground_signature(FactId, predicate(Namespace, Name, Args, Polarity)) :-
1185
- kb:predicate_fact(FactId, Namespace, Name, Args, Polarity).
1186
- logical_ground_signature(
1187
- FactId,
1188
- property(Subject, Property, Operator, ValueType, Value, Unit, Scope, Polarity)
1189
- ) :-
1190
- kb:fact_property_tuple(
1191
- FactId,
1192
- Subject,
1193
- Property,
1194
- Operator,
1195
- ValueType,
1196
- Value,
1197
- Unit,
1198
- Scope,
1199
- Polarity
1200
- ).
1201
- logical_ground_signature(FactId, rule(SemanticKey)) :-
1202
- kb_entity(FactId, fact, Props),
1203
- memberchk(fact_kind=RawKind, Props),
1204
- normalize_term_atom(RawKind, rule),
1205
- ( memberchk(semantic_key=RawKey, Props)
1206
- -> normalize_term_atom(RawKey, SemanticKey)
1207
- ; memberchk(rule_hash=RawKey, Props), normalize_term_atom(RawKey, SemanticKey)
1208
- ).
1209
-
1210
1340
  ground_fact_claim_key(FactId, ExpectedKind, ClaimKey) :-
1211
1341
  kb_entity(FactId, fact, Props),
1212
1342
  memberchk(fact_kind=RawKind, Props),
@@ -1478,7 +1608,14 @@ check_selected_dispatch(Rules, _{
1478
1608
  rule_safety: RuleSafety,
1479
1609
  rule_verifiability: RuleVerifiability,
1480
1610
  semantic_completeness: SemanticCompleteness,
1481
- req_status_vocabulary: ReqStatusVocabulary
1611
+ req_status_vocabulary: ReqStatusVocabulary,
1612
+ proof_contract_symbols: ProofContractSymbols,
1613
+ entity_id_style: EntityIdStyle,
1614
+ domain_redundancy: DomainRedundancy,
1615
+ domain_implication: DomainImplication,
1616
+ subject_key_identity: SubjectKeyIdentity,
1617
+ subject_key_shape: SubjectKeyShape,
1618
+ ontology_quality: OntologyQuality
1482
1619
  }) :-
1483
1620
  selected_rule(Rules, 'must-priority-coverage', check_must_priority_coverage, MustPriority),
1484
1621
  selected_rule(Rules, 'symbol-coverage', check_symbol_coverage, SymbolCoverage),
@@ -1496,7 +1633,14 @@ check_selected_dispatch(Rules, _{
1496
1633
  selected_rule(Rules, 'rule-safety', check_rule_safety, RuleSafety),
1497
1634
  selected_rule(Rules, 'rule-verifiability', check_rule_verifiability, RuleVerifiability),
1498
1635
  selected_rule(Rules, 'semantic-completeness', check_semantic_completeness, SemanticCompleteness),
1499
- selected_rule(Rules, 'req-status-vocabulary', check_req_status_vocabulary, ReqStatusVocabulary).
1636
+ selected_rule(Rules, 'req-status-vocabulary', check_req_status_vocabulary, ReqStatusVocabulary),
1637
+ selected_rule(Rules, 'proof-contract-symbols', check_proof_contract_symbols, ProofContractSymbols),
1638
+ selected_rule(Rules, 'entity-id-style', check_entity_id_style, EntityIdStyle),
1639
+ selected_rule(Rules, 'domain-redundancy', check_domain_redundancy, DomainRedundancy),
1640
+ selected_rule(Rules, 'domain-implication', check_domain_implication, DomainImplication),
1641
+ selected_rule(Rules, 'subject-key-identity', check_subject_key_identity, SubjectKeyIdentity),
1642
+ selected_rule(Rules, 'subject-key-shape', check_subject_key_shape, SubjectKeyShape),
1643
+ selected_rule(Rules, 'ontology-quality', check_ontology_quality, OntologyQuality).
1500
1644
 
1501
1645
  selected_rule(Rules, Name, Goal, Violations) :-
1502
1646
  ( memberchk(Name, Rules)
@@ -1552,6 +1696,13 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
1552
1696
  check_rule_verifiability(RuleVerifiability),
1553
1697
  check_semantic_completeness(SemanticCompleteness),
1554
1698
  check_req_status_vocabulary(ReqStatusVocabulary),
1699
+ check_proof_contract_symbols(ProofContractSymbols),
1700
+ check_entity_id_style(EntityIdStyle),
1701
+ check_domain_redundancy(DomainRedundancy),
1702
+ check_domain_implication(DomainImplication),
1703
+ check_subject_key_identity(SubjectKeyIdentity),
1704
+ check_subject_key_shape(SubjectKeyShape),
1705
+ check_ontology_quality(OntologyQuality),
1555
1706
  ViolationsDict = _{
1556
1707
  must_priority_coverage: MustPriority,
1557
1708
  symbol_coverage: SymbolCoverage,
@@ -1569,7 +1720,14 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
1569
1720
  rule_safety: RuleSafety,
1570
1721
  rule_verifiability: RuleVerifiability,
1571
1722
  semantic_completeness: SemanticCompleteness,
1572
- req_status_vocabulary: ReqStatusVocabulary
1723
+ req_status_vocabulary: ReqStatusVocabulary,
1724
+ proof_contract_symbols: ProofContractSymbols,
1725
+ entity_id_style: EntityIdStyle,
1726
+ domain_redundancy: DomainRedundancy,
1727
+ domain_implication: DomainImplication,
1728
+ subject_key_identity: SubjectKeyIdentity,
1729
+ subject_key_shape: SubjectKeyShape,
1730
+ ontology_quality: OntologyQuality
1573
1731
  }.
1574
1732
 
1575
1733
  %% violations_dict_to_json(+ViolationsDict, -JsonDict)
@@ -1588,7 +1746,12 @@ pairs_to_json_pairs([Key-Violations|Rest], [Key-JsonViolations|JsonRest]) :-
1588
1746
 
1589
1747
  %% violation_to_json(+Violation, -JsonDict)
1590
1748
  % Converts a violation(Rule, EntityId, Description, Suggestion, Source) term
1591
- % to a JSON-compatible dict.
1749
+ % to a JSON-compatible dict. violation/6 carries precomputed JSON-safe
1750
+ % evidence (witnesses) as its sixth argument.
1751
+ violation_to_json(violation(Rule, EntityId, Description, Suggestion, Source, Evidence), JsonDict) :-
1752
+ !,
1753
+ violation_term_to_dict(violation(Rule, EntityId, Description, Suggestion, Source), BaseDict),
1754
+ put_dict(evidence, BaseDict, Evidence, JsonDict).
1592
1755
  violation_to_json(Violation, JsonDict) :-
1593
1756
  violation_term_to_dict(Violation, BaseDict),
1594
1757
  ( contradiction_violation_witnesses(Violation, Witnesses),
package/src/discovery.pl CHANGED
@@ -8,15 +8,22 @@
8
8
  coverage_report_json/11,
9
9
  coverage_report_json/12,
10
10
  coverage_evidence_json/5,
11
+ requirement_proof_json/5,
12
+ requirement_proof_json/7,
13
+ symbol_proof_json/7,
11
14
  graph_expand_json/8
12
15
  ]).
13
16
 
17
+ % Source contains non-ASCII text; do not depend on the host locale.
18
+ :- encoding(utf8).
19
+
14
20
  :- use_module(library(http/json)).
15
21
  :- use_module(library(aggregate)).
16
22
  :- use_module(library(apply)).
17
23
  :- use_module(library(lists)).
18
24
  :- use_module(library(pairs)).
19
25
  :- use_module('kb.pl').
26
+ :- use_module(library(semweb/rdf_db), [rdf_generation/1]).
20
27
  :- use_module('requirement_proof.pl', [requirement_proof_context/1, requirement_proof_context/4, requirement_proof_context/6, requirement_proof/4]).
21
28
  :- use_module('status.pl', [status_meta_dict/1]).
22
29
  :- use_module('../schema/relationships.pl', [relationship_type/1]).
@@ -40,6 +47,58 @@ coverage_report_json(By, Tags, IncludePassing, IncludeTransitive, Limit, Offset,
40
47
  coverage_report_json(By, Tags, IncludePassing, IncludeTransitive, Limit, Offset, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
41
48
  coverage_report_json(By, Tags, IncludePassing, strict_snapshot, _{}, IncludeTransitive, Limit, Offset, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString).
42
49
 
50
+ requirement_proof_json(ReqId, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
51
+ requirement_proof_json(ReqId, strict_snapshot, _{}, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString).
52
+
53
+ requirement_proof_json(ReqId, BindingMode, TestBindings, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
54
+ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, Context),
55
+ kb_entity(ReqId, req, Props),
56
+ requirement_proof(ReqId, Props, Context, Proof),
57
+ put_dict(id, Proof, ReqId, ProofWithId),
58
+ dict_json_string(ProofWithId, JsonString).
59
+
60
+ symbol_proof_json(SymbolId, BindingMode, TestBindings, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
61
+ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, Context),
62
+ kb_entity(SymbolId, symbol, _),
63
+ findall(ReqId,
64
+ (kb_relationship(implements, SymbolId, ReqId), kb_entity(ReqId, req, _)),
65
+ ReqIds0),
66
+ sort(ReqIds0, ReqIds),
67
+ maplist(requirement_proof_with_id(Context), ReqIds, Proofs),
68
+ symbol_explain_role(SymbolId, Role),
69
+ (kb:executable_test_symbol(SymbolId) -> ExecutableTest = true ; ExecutableTest = false),
70
+ findall(TestId, kb_relationship(executable_for, SymbolId, TestId), ExecutableFor0),
71
+ sort(ExecutableFor0, ExecutableFor),
72
+ findall(TestId, kb_relationship(covered_by, SymbolId, TestId), CoveredBy0),
73
+ sort(CoveredBy0, CoveredBy),
74
+ Response = _{
75
+ id: SymbolId,
76
+ symbolId: SymbolId,
77
+ role: Role,
78
+ executableTest: ExecutableTest,
79
+ implementingRequirements: ReqIds,
80
+ executableFor: ExecutableFor,
81
+ coveredBy: CoveredBy,
82
+ proofs: Proofs
83
+ },
84
+ dict_json_string(Response, JsonString).
85
+
86
+ requirement_proof_with_id(Context, ReqId, ProofWithId) :-
87
+ kb_entity(ReqId, req, Props),
88
+ requirement_proof(ReqId, Props, Context, Proof),
89
+ put_dict(id, Proof, ReqId, ProofWithId).
90
+
91
+ symbol_explain_role(SymbolId, executable_test) :-
92
+ kb:executable_test_symbol(SymbolId),
93
+ !.
94
+ symbol_explain_role(SymbolId, Role) :-
95
+ kb_entity(SymbolId, symbol, Props),
96
+ memberchk(symbol_role=RawRole, Props),
97
+ kb:normalize_term_atom(RawRole, Role),
98
+ Role \= '',
99
+ !.
100
+ symbol_explain_role(_SymbolId, unknown).
101
+
43
102
  % Slim per-requirement proof evidence for quality diagnostics (W1 push-down).
44
103
  % Projects only the fields the quality diagnostics consume - proof status,
45
104
  % the passing-e2e stage summary, and receipt-related gap codes - instead of
@@ -98,11 +157,11 @@ coverage_evidence_receipt_gap(Gap) :-
98
157
  coverage_report_json(By, Tags, IncludePassing, BindingMode, TestBindings, IncludeTransitive, Limit, Offset, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
99
158
  % Binding mode applies to requirement rows only; other By values keep
100
159
  % 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
- ),
105
- sort_dict_rows(Rows0, SortedRows),
160
+ memo_sorted_coverage(
161
+ binding(By, Tags, IncludePassing, BindingMode, TestBindings, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds),
162
+ binding_coverage_rows(By, Tags, IncludePassing, BindingMode, TestBindings, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds),
163
+ SortedRows,
164
+ Summary),
106
165
  paginate_rows(SortedRows, Offset, Limit, Rows),
107
166
  status_meta_dict(Meta),
108
167
  Response = _{summary: Summary, rows: Rows, meta: Meta},
@@ -116,13 +175,44 @@ coverage_report_json(By, Tags, IncludePassing, BindingMode, TestBindings, Includ
116
175
  % reason — without diffing full exports by hand. The summary is always
117
176
  % computed over all rows so filtered responses keep whole-KB counts.
118
177
  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),
178
+ memo_sorted_coverage(
179
+ status(By, Tags, IncludePassing, StatusFilter, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds),
180
+ status_filtered_coverage_rows(By, Tags, IncludePassing, StatusFilter, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds),
181
+ SortedRows,
182
+ Summary),
121
183
  paginate_rows(SortedRows, Offset, Limit, Rows),
122
184
  status_meta_dict(Meta),
123
185
  Response = _{summary: Summary, rows: Rows, meta: Meta},
124
186
  dict_json_string(Response, JsonString).
125
187
 
188
+ binding_coverage_rows(req, Tags, IncludePassing, BindingMode, TestBindings, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
189
+ !,
190
+ coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, Rows, Summary).
191
+ binding_coverage_rows(By, Tags, IncludePassing, _BindingMode, _TestBindings, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
192
+ coverage_rows(By, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary).
193
+
194
+ :- dynamic coverage_memo/3.
195
+
196
+ %% memo_sorted_coverage(+Args, :Compute, -SortedRows, -Summary)
197
+ % Large coverage reports are fetched in bounded pages so no single answer
198
+ % approaches the engine's output cap. Every page would otherwise recompute
199
+ % the whole report; keep the most recent sorted result for the exact
200
+ % arguments, graph, and RDF generation. Coverage reads only the store and its
201
+ % arguments, so the memo is invalidated by any store change.
202
+ memo_sorted_coverage(Args, Compute, SortedRows, Summary) :-
203
+ (kb:kb_graph(Graph) -> true ; Graph = none),
204
+ rdf_generation(Generation),
205
+ Key = key(Args, Graph, Generation),
206
+ ( coverage_memo(Stored, StoredRows, StoredSummary),
207
+ Stored =@= Key
208
+ -> SortedRows = StoredRows,
209
+ Summary = StoredSummary
210
+ ; call(Compute, Rows0, Summary),
211
+ sort_dict_rows(Rows0, SortedRows),
212
+ retractall(coverage_memo(_, _, _)),
213
+ assertz(coverage_memo(Key, SortedRows, Summary))
214
+ ).
215
+
126
216
  status_filtered_coverage_rows(By, Tags, IncludePassing, [], IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
127
217
  !,
128
218
  coverage_rows(By, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary).