kibi-core 0.22.5 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/discovery.pl CHANGED
@@ -7,10 +7,13 @@
7
7
  coverage_report_json/10,
8
8
  coverage_report_json/11,
9
9
  coverage_report_json/12,
10
+ coverage_report_json/13,
10
11
  coverage_evidence_json/5,
12
+ evidence_floor_json/6,
11
13
  requirement_proof_json/5,
12
14
  requirement_proof_json/7,
13
15
  symbol_proof_json/7,
16
+ test_receipt_states_json/6,
14
17
  graph_expand_json/8,
15
18
  search_answer_verdicts_json/2,
16
19
  ask_predicate_json/9,
@@ -29,7 +32,7 @@
29
32
  :- use_module(library(pairs)).
30
33
  :- use_module('kb.pl').
31
34
  :- use_module(library(semweb/rdf_db), [rdf_generation/1]).
32
- :- use_module('requirement_proof.pl', [requirement_proof_context/1, requirement_proof_context/4, requirement_proof_context/6, requirement_proof/4]).
35
+ :- use_module('requirement_proof.pl', [requirement_proof_context/1, requirement_proof_context/4, requirement_proof_context/6, requirement_proof/4, test_receipt_evidence/4]).
33
36
  :- use_module('checks.pl', [scenario_feasibility_outcome/2, infeasible_scenario/5]).
34
37
  :- use_module('logic_ir.pl', [logic_rule_conflict/3, logic_rule_conflict/4]).
35
38
  :- use_module('status.pl', [status_meta_dict/1, status_scope_dict/1]).
@@ -78,6 +81,21 @@ requirement_proof_json(ReqId, BindingMode, TestBindings, VerificationSnapshot, C
78
81
  put_dict(id, Proof, ReqId, ProofWithId),
79
82
  dict_json_string(ProofWithId, JsonString).
80
83
 
84
+ % implements REQ-kibi-verification-evidence-contract
85
+ % The receipt state of every proof-bearing test under the given binding, as
86
+ % `kibi prove --stale` selects them: passed means a current passing receipt.
87
+ test_receipt_states_json(BindingMode, TestBindings, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
88
+ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, Context),
89
+ findall(TestId-State,
90
+ (kb_entity(TestId, test, Props),
91
+ memberchk(proof_contract=_, Props),
92
+ test_receipt_evidence(Context, [unit, integration, end_to_end], TestId, Evidence),
93
+ State = Evidence.state),
94
+ Pairs0),
95
+ sort(Pairs0, Pairs),
96
+ dict_pairs(States, _, Pairs),
97
+ dict_json_string(_{states: States}, JsonString).
98
+
81
99
  symbol_proof_json(SymbolId, BindingMode, TestBindings, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
82
100
  requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, Context),
83
101
  kb_entity(SymbolId, symbol, _),
@@ -344,6 +362,77 @@ coverage_evidence_json(Tags, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Jso
344
362
  Response = _{rows: Evidences},
345
363
  dict_json_string(Response, JsonString).
346
364
 
365
+ % implements REQ-kibi-evidence-floors
366
+ % Every current requirement whose evidence level is below its floor, for the
367
+ % evidence-floor check rule: the rows that fail (status below) or that only
368
+ % need kibi prove re-run (status pending_reproof), plus how many current
369
+ % requirements were evaluated. Requirements outside proof scope carry no
370
+ % floor.
371
+ evidence_floor_json(BindingMode, TestBindings, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
372
+ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, Context),
373
+ findall(Id-Proof,
374
+ ( kb_entity(Id, req, Props),
375
+ requirement_proof(Id, Props, Context, Proof),
376
+ get_dict(evidenceFloor, Proof, _)
377
+ ),
378
+ Evaluated),
379
+ findall(Row,
380
+ ( member(Id-Proof, Evaluated),
381
+ Floor = Proof.evidenceFloor,
382
+ Floor.status \== met,
383
+ Row = _{
384
+ id: Id,
385
+ evidenceLevel: Proof.evidenceLevel,
386
+ requiredEvidence: Floor.required,
387
+ requiredEvidenceSource: Floor.source,
388
+ status: Floor.status,
389
+ proofStatus: Proof.proofStatus,
390
+ proofGaps: Proof.proofGaps,
391
+ gapDetails: Details
392
+ },
393
+ evidence_floor_gap_details(Proof, Details)
394
+ ),
395
+ Rows0),
396
+ sort(id, @<, Rows0, Rows),
397
+ length(Evaluated, Count),
398
+ dict_json_string(_{evaluated: Count, rows: Rows}, JsonString).
399
+
400
+ % The entities behind an evidence-floor row's gaps, so a person sees what to
401
+ % fix without one kibi proof explain per requirement: the tests whose
402
+ % receipts are missing, stale, failed or invalid, the production symbols no
403
+ % qualifying test covers and the symbols without source coordinates. Empty
404
+ % lists are left out.
405
+ evidence_floor_gap_details(Proof, Details) :-
406
+ ( get_dict(proofStages, Proof, Stages)
407
+ -> true
408
+ ; Stages = _{}
409
+ ),
410
+ findall(Key-Ids,
411
+ ( evidence_floor_gap_field(Key, StageKey, Field),
412
+ get_dict(StageKey, Stages, Stage),
413
+ get_dict(Field, Stage, Ids),
414
+ Ids \== []
415
+ ),
416
+ Pairs0),
417
+ % implements REQ-kibi-claim-bound-proofs
418
+ % Under a proved floor, the claims no certificate binding derives.
419
+ ( get_dict(evidenceFloor, Proof, Floor),
420
+ get_dict(required, Floor, proved),
421
+ get_dict(unprovedClaims, Proof, Unproved),
422
+ Unproved \== []
423
+ -> Pairs = [unprovedClaims-Unproved|Pairs0]
424
+ ; Pairs = Pairs0
425
+ ),
426
+ dict_pairs(Details, _, Pairs).
427
+
428
+ evidence_floor_gap_field(failedReceiptTests, passingE2e, failedReceiptTests).
429
+ evidence_floor_gap_field(missingReceiptTests, passingE2e, missingReceiptTests).
430
+ evidence_floor_gap_field(staleReceiptTests, passingE2e, staleReceiptTests).
431
+ evidence_floor_gap_field(invalidReceiptTests, passingE2e, invalidReceiptTests).
432
+ evidence_floor_gap_field(contractMismatchReceiptTests, passingE2e, contractMismatchReceiptTests).
433
+ evidence_floor_gap_field(uncoveredSymbols, productionSymbols, uncoveredSymbols).
434
+ evidence_floor_gap_field(missingCoordinateSymbols, sourceCoordinates, missingSymbols).
435
+
347
436
  coverage_evidence_row(Id, Proof, Evidence) :-
348
437
  ( get_dict(proofStatus, Proof, ProofStatus)
349
438
  -> true
@@ -398,7 +487,7 @@ coverage_report_json(By, Tags, IncludePassing, BindingMode, TestBindings, Includ
398
487
 
399
488
  % implements REQ-kibi-conservative-requirement-proof
400
489
  % Proof-status filtered report. The status filter selects requirement rows by
401
- % proofStatus (proven, missing, unresolved, not_applicable) and replaces the
490
+ % proofStatus (tested, missing, unresolved, not_applicable) and replaces the
402
491
  % default include-passing filtering, so callers can enumerate exactly one
403
492
  % slice — e.g. every not_applicable row together with its typed applicability
404
493
  % reason — without diffing full exports by hand. The summary is always
@@ -414,6 +503,28 @@ coverage_report_json(By, Tags, IncludePassing, StatusFilter, IncludeTransitive,
414
503
  Response = _{summary: Summary, rows: Rows, meta: Meta},
415
504
  dict_json_string(Response, JsonString).
416
505
 
506
+ % Proof-status filtered report with per-contract receipt binding: the same
507
+ % rows and whole-KB summary as the unfiltered report under that binding mode,
508
+ % so a status filter never judges receipts more strictly than kb_coverage,
509
+ % kibi proof explain or the evidence-floor rule do.
510
+ coverage_report_json(By, Tags, IncludePassing, StatusFilter, BindingMode, TestBindings, IncludeTransitive, Limit, Offset, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
511
+ memo_sorted_coverage(
512
+ status_binding(By, Tags, IncludePassing, StatusFilter, BindingMode, TestBindings, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds),
513
+ status_binding_coverage_rows(By, Tags, IncludePassing, StatusFilter, BindingMode, TestBindings, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds),
514
+ SortedRows,
515
+ Summary),
516
+ paginate_rows(SortedRows, Offset, Limit, Rows),
517
+ status_meta_dict(Meta),
518
+ Response = _{summary: Summary, rows: Rows, meta: Meta},
519
+ dict_json_string(Response, JsonString).
520
+
521
+ status_binding_coverage_rows(req, Tags, _IncludePassing, StatusFilter, BindingMode, TestBindings, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, MatchedRows, Summary) :-
522
+ !,
523
+ coverage_rows(req, Tags, true, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, Rows0, Summary),
524
+ include(req_row_proof_status_in(StatusFilter), Rows0, MatchedRows).
525
+ status_binding_coverage_rows(By, Tags, IncludePassing, StatusFilter, _BindingMode, _TestBindings, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
526
+ status_filtered_coverage_rows(By, Tags, IncludePassing, StatusFilter, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary).
527
+
417
528
  binding_coverage_rows(req, Tags, IncludePassing, BindingMode, TestBindings, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
418
529
  !,
419
530
  coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, Rows, Summary).
@@ -1118,6 +1229,10 @@ symbol_coverage_row(Tags, Row) :-
1118
1229
  coverageStatus: CoverageStatus
1119
1230
  }.
1120
1231
 
1232
+ % Link coverage (evaluated, fullyCovered, uncovered, notApplicable and the
1233
+ % missing* counts) judges only priority: must requirements; linkCoverageScope
1234
+ % says so, so a project without must requirements does not read as having
1235
+ % evaluated nothing next to its proof counts.
1121
1236
  coverage_summary(Rows, Summary) :-
1122
1237
  length(Rows, Total),
1123
1238
  include(req_row_evaluated, Rows, EvaluatedRows),
@@ -1127,7 +1242,7 @@ coverage_summary(Rows, Summary) :-
1127
1242
  include(req_row_missing_scenario, Rows, MissingScenarioRows),
1128
1243
  include(req_row_missing_test, Rows, MissingTestRows),
1129
1244
  include(req_row_missing_both, Rows, MissingBothRows),
1130
- include(req_row_proof_proven, Rows, ProofProvenRows),
1245
+ include(req_row_proof_tested, Rows, ProofTestedRows),
1131
1246
  include(req_row_proof_unresolved, Rows, ProofUnresolvedRows),
1132
1247
  include(req_row_proof_missing, Rows, ProofMissingRows),
1133
1248
  include(req_row_proof_not_applicable, Rows, ProofNotApplicableRows),
@@ -1138,7 +1253,10 @@ coverage_summary(Rows, Summary) :-
1138
1253
  length(MissingScenarioRows, MissingScenario),
1139
1254
  length(MissingTestRows, MissingTest),
1140
1255
  length(MissingBothRows, MissingScenarioAndTest),
1141
- length(ProofProvenRows, ProofProven),
1256
+ length(ProofTestedRows, ProofTested),
1257
+ evidence_level_counts(Rows, EvidenceLevels),
1258
+ claim_level_counts(Rows, ProofClaims),
1259
+ evidence_floor_counts(Rows, EvidenceFloors),
1142
1260
  length(ProofUnresolvedRows, ProofUnresolved),
1143
1261
  length(ProofMissingRows, ProofMissing),
1144
1262
  length(ProofNotApplicableRows, ProofNotApplicable),
@@ -1151,12 +1269,61 @@ coverage_summary(Rows, Summary) :-
1151
1269
  missingScenario: MissingScenario,
1152
1270
  missingTest: MissingTest,
1153
1271
  missingScenarioAndTest: MissingScenarioAndTest,
1154
- proofProven: ProofProven,
1272
+ proofTested: ProofTested,
1155
1273
  proofUnresolved: ProofUnresolved,
1156
1274
  proofMissing: ProofMissing,
1157
- proofNotApplicable: ProofNotApplicable
1275
+ proofNotApplicable: ProofNotApplicable,
1276
+ evidenceLevels: EvidenceLevels,
1277
+ proofClaims: ProofClaims,
1278
+ evidenceFloors: EvidenceFloors,
1279
+ linkCoverageScope: priority_must
1158
1280
  }.
1159
1281
 
1282
+ % implements REQ-kibi-claim-bound-proofs
1283
+ % The applicable requirement claims across Rows, by the level their evidence
1284
+ % reaches: a claim is proved only through a derived certificate binding.
1285
+ claim_level_counts(Rows, _{applicable: Applicable, proved: Proved, attested: Attested,
1286
+ tested: Tested, unresolved: Unresolved}) :-
1287
+ claim_count_sum(Rows, applicable, Applicable),
1288
+ claim_count_sum(Rows, proved, Proved),
1289
+ claim_count_sum(Rows, attested, Attested),
1290
+ claim_count_sum(Rows, tested, Tested),
1291
+ claim_count_sum(Rows, unresolved, Unresolved).
1292
+
1293
+ claim_count_sum(Rows, Key, Sum) :-
1294
+ aggregate_all(sum(N),
1295
+ ( member(Row, Rows),
1296
+ get_dict(claimSummary, Row, Summary),
1297
+ get_dict(Key, Summary, N),
1298
+ number(N)
1299
+ ),
1300
+ Sum).
1301
+
1302
+ % implements REQ-kibi-evidence-floors
1303
+ % Current requirements with a floor (required_evidence, or tested for a firm
1304
+ % requirement), by whether their evidence level reaches it. A floor of
1305
+ % unverified accepts anything and is not counted.
1306
+ evidence_floor_counts(Rows, _{met: Met, below: Below, pendingReproof: Pending}) :-
1307
+ evidence_floor_status_count(Rows, met, Met),
1308
+ evidence_floor_status_count(Rows, below, Below),
1309
+ evidence_floor_status_count(Rows, pending_reproof, Pending).
1310
+
1311
+ evidence_floor_status_count(Rows, Status, Count) :-
1312
+ aggregate_all(count,
1313
+ ( member(Row, Rows),
1314
+ get_dict(evidenceFloor, Row, Floor),
1315
+ Floor.get(required) \== unverified,
1316
+ Floor.get(status) == Status
1317
+ ),
1318
+ Count).
1319
+
1320
+ % implements REQ-kibi-evidence-levels
1321
+ evidence_level_counts(Rows, _{proved: Proved, tested: Tested, asserted: Asserted, unverified: Unverified}) :-
1322
+ aggregate_all(count, (member(Row, Rows), Row.get(evidenceLevel) == proved), Proved),
1323
+ aggregate_all(count, (member(Row, Rows), Row.get(evidenceLevel) == tested), Tested),
1324
+ aggregate_all(count, (member(Row, Rows), Row.get(evidenceLevel) == asserted), Asserted),
1325
+ aggregate_all(count, (member(Row, Rows), Row.get(evidenceLevel) == unverified), Unverified).
1326
+
1160
1327
  req_row_fully_covered(Row) :-
1161
1328
  CoverageStatus = Row.get(coverageStatus),
1162
1329
  CoverageStatus = fully_covered.
@@ -1175,8 +1342,8 @@ req_row_missing_test(Row) :-
1175
1342
  Row.gaps = [missing_test].
1176
1343
  req_row_missing_both(Row) :-
1177
1344
  Row.gaps = [missing_scenario_and_test].
1178
- req_row_proof_proven(Row) :-
1179
- Row.proofStatus == proven.
1345
+ req_row_proof_tested(Row) :-
1346
+ Row.proofStatus == tested.
1180
1347
  req_row_proof_unresolved(Row) :-
1181
1348
  Row.proofStatus == unresolved.
1182
1349
  req_row_proof_missing(Row) :-
@@ -1377,7 +1544,7 @@ filter_req_coverage_rows(false, Rows, Filtered) :-
1377
1544
 
1378
1545
  req_row_proof_passing(Row) :-
1379
1546
  Status = Row.proofStatus,
1380
- memberchk(Status, [proven, not_applicable]).
1547
+ memberchk(Status, [tested, not_applicable]).
1381
1548
 
1382
1549
  filter_symbol_coverage_rows(true, Rows, Rows).
1383
1550
  filter_symbol_coverage_rows(false, Rows, Filtered) :-
package/src/intervals.pl CHANGED
@@ -30,6 +30,7 @@ numeric_constraints_satisfiable(Constraints) :-
30
30
  % integer lies strictly between 0 and 1, and eq 0.5 has no integer solution.
31
31
  % IntegerVariables may also be the atom `all`, making every variable integral.
32
32
  numeric_constraints_satisfiable(Constraints, IntegerVariables) :-
33
+ maplist(finite_constraint, Constraints),
33
34
  term_variables(Constraints, Variables),
34
35
  forall(
35
36
  member(Variable, Variables),
@@ -49,6 +50,16 @@ numeric_constraint_entailed(Premises, Constraint, IntegerVariables) :-
49
50
  numeric_constraint_negation(Constraint, Negated),
50
51
  \+ numeric_constraints_satisfiable([Negated|Premises], IntegerVariables).
51
52
 
53
+ % A NaN or infinite bound has no place on the number line the intervals
54
+ % model, so it is an error rather than an answer.
55
+ finite_constraint(c(_Op, _Variable, Value)) :-
56
+ ( float(Value),
57
+ float_class(Value, Class),
58
+ memberchk(Class, [nan, infinite])
59
+ -> domain_error(finite_number, Value)
60
+ ; true
61
+ ).
62
+
52
63
  integer_variable(_, all) :- !.
53
64
  integer_variable(Variable, IntegerVariables) :-
54
65
  is_list(IntegerVariables),
package/src/kb.pl CHANGED
@@ -3121,6 +3121,13 @@ value_from_props(Props, string, Value) :- memberchk(value_string=ValueRaw, Props
3121
3121
  value_from_props(Props, int, Value) :- memberchk(value_int=ValueRaw, Props), !, unwrap_rdf_value(ValueRaw, Value).
3122
3122
  value_from_props(Props, number, Value) :- memberchk(value_number=ValueRaw, Props), !, unwrap_rdf_value(ValueRaw, Value).
3123
3123
  value_from_props(Props, bool, Value) :- memberchk(value_bool=ValueRaw, Props), !, unwrap_rdf_value(ValueRaw, Value).
3124
+ % A set value is its sorted, unique members, so two facts naming the same set
3125
+ % in any order compare equal.
3126
+ value_from_props(Props, set, Value) :-
3127
+ memberchk(value_set=ValueRaw, Props),
3128
+ normalize_term_atom_list(ValueRaw, Atoms),
3129
+ !,
3130
+ sort(Atoms, Value).
3124
3131
  value_from_props(_, _, '').
3125
3132
 
3126
3133
  %% unwrap_rdf_value(+Raw, -Value)
@@ -0,0 +1,213 @@
1
+ % Module: kb_consistency
2
+ % implements REQ-kibi-kb-certificates
3
+ % The kb-consistency check rules. Each run produces the certificate bundle
4
+ % (certificates.pl), round-trips it through JSON exactly as a saved bundle
5
+ % would be read, and re-checks it with the independent checker
6
+ % (certificate_check.pl). Only what the checker accepts becomes a finding:
7
+ %
8
+ % kb-consistency a re-verified contradiction (an irreducible
9
+ % core of property facts, two facts that
10
+ % disagree on an atom or a decision, or a
11
+ % negation cycle among stored rules), or a
12
+ % certificate the checker rejects
13
+ % kb-consistency-unresolved a context the checker cannot compare (mixed
14
+ % types or units, unsupported operators or
15
+ % polarities, windows that never overlap)
16
+ :- module(kb_consistency, [
17
+ check_kb_consistency/1,
18
+ check_kb_consistency_unresolved/1
19
+ ]).
20
+
21
+ :- encoding(utf8).
22
+
23
+ :- use_module(library(apply)).
24
+ :- use_module(library(lists)).
25
+ :- use_module(library(http/json)).
26
+ :- use_module('kb.pl', [kb_entity/3, kb_relationship/3]).
27
+ :- use_module('certificates.pl', [kb_certificates/1]).
28
+ :- use_module('certificate_check.pl', [verify_certificates/2]).
29
+
30
+ %% check_kb_consistency(-Violations)
31
+ check_kb_consistency(Violations) :-
32
+ consistency_verification(Bundle, Report),
33
+ numbered_certificates(Bundle, Numbered),
34
+ findall(Index, (member(Failure, Report.rejected), get_dict(certificate, Failure, Index)), Rejected),
35
+ findall(Violation,
36
+ ( member(Index-Certificate, Numbered),
37
+ \+ memberchk(Index, Rejected),
38
+ contradiction_violation(Certificate, Violation)
39
+ ),
40
+ Contradictions),
41
+ findall(Violation,
42
+ ( member(Failure, Report.rejected),
43
+ rejected_violation(Failure, Violation)
44
+ ),
45
+ RejectedViolations),
46
+ append(Contradictions, RejectedViolations, Unsorted),
47
+ sort(Unsorted, Violations).
48
+
49
+ %% check_kb_consistency_unresolved(-Violations)
50
+ check_kb_consistency_unresolved(Violations) :-
51
+ consistency_verification(_Bundle, Report),
52
+ findall(Violation,
53
+ ( member(Theorem, Report.theorems),
54
+ get_dict(unresolved, Theorem, Entries),
55
+ member(Entry, Entries),
56
+ unresolved_violation(Theorem.theorem, Entry, Violation)
57
+ ),
58
+ Unsorted),
59
+ sort(Unsorted, Violations).
60
+
61
+ consistency_verification(Bundle, Report) :-
62
+ kb_certificates(Produced),
63
+ with_output_to(string(Json), json_write_dict(current_output, Produced, [width(0)])),
64
+ open_string(Json, Stream),
65
+ json_read_dict(Stream, Bundle, [value_string_as(string)]),
66
+ verify_certificates(Bundle, Report).
67
+
68
+ numbered_certificates(Bundle, Numbered) :-
69
+ findall(Index-Certificate, nth1(Index, Bundle.certificates, Certificate), Numbered).
70
+
71
+ % ------------------------------------------------------------------
72
+ % Re-verified contradictions
73
+ % ------------------------------------------------------------------
74
+
75
+ contradiction_violation(Certificate, violation('kb-consistency', EntityId, Description, Suggestion, "", _{certificate: Certificate})) :-
76
+ get_dict(kind, Certificate, KindText),
77
+ atom_string(Kind, KindText),
78
+ contradiction_text(Kind, Certificate, Facts, Description),
79
+ fact_requirements(Facts, Requirements),
80
+ entity_label(Requirements, Facts, EntityId),
81
+ suggestion_text(Kind, Requirements, Suggestion).
82
+
83
+ contradiction_text(property_contradiction, Certificate, Facts, Description) :-
84
+ Facts = Certificate.core,
85
+ context_text(Certificate, Context),
86
+ atomic_list_concat(Facts, ', ', FactText),
87
+ findall(Text,
88
+ ( member(Drop, Certificate.dropModels),
89
+ format(string(Text), "~w: ~w", [Drop.factId, Drop.value])
90
+ ),
91
+ DropTexts),
92
+ atomic_list_concat(DropTexts, ', ', DropText),
93
+ format(string(Description),
94
+ "Facts ~w cannot all hold for ~w: no value satisfies them together, and dropping any one leaves a value (~w)",
95
+ [FactText, Context, DropText]).
96
+ contradiction_text(predicate_contradiction, Certificate, Facts, Description) :-
97
+ Facts = Certificate.core,
98
+ Facts = [FactA, FactB],
99
+ atom_text(Certificate, Atom),
100
+ format(string(Description),
101
+ "Facts ~w and ~w give ~w opposite truth values", [FactA, FactB, Atom]).
102
+ contradiction_text(decision_contradiction, Certificate, Facts, Description) :-
103
+ Facts = Certificate.core,
104
+ Facts = [FactA, FactB],
105
+ decision_text(Certificate, Decision),
106
+ format(string(Description),
107
+ "Facts ~w and ~w decide ~w differently: one allows it and the other denies it", [FactA, FactB, Decision]).
108
+ contradiction_text(rule_negation_cycle, Certificate, [], Description) :-
109
+ findall(Text,
110
+ ( member(Edge, Certificate.cycle),
111
+ signature_text(Edge.head, Head),
112
+ signature_text(Edge.body, Body),
113
+ ( atom_string(negative, Edge.sign) -> Arrow = "depends on not" ; Arrow = "depends on" ),
114
+ format(string(Text), "~w ~w ~w", [Head, Arrow, Body])
115
+ ),
116
+ Texts),
117
+ atomic_list_concat(Texts, "; ", CycleText),
118
+ format(string(Description),
119
+ "Stored rules are not stratified: a predicate depends on its own negation (~w)", [CycleText]).
120
+
121
+ suggestion_text(rule_negation_cycle, _Requirements,
122
+ "Break the cycle: change one rule so no predicate depends on its own negation, or supersede the requirement that stores it") :- !.
123
+ suggestion_text(_Kind, Requirements, Suggestion) :-
124
+ atomic_list_concat(Requirements, ', ', RequirementText),
125
+ format(string(Suggestion),
126
+ "Supersede or change one of ~w so the facts can hold together; kibi check --write-certificates <file> saves the certificate",
127
+ [RequirementText]).
128
+
129
+ % ------------------------------------------------------------------
130
+ % Rejected certificates and unresolved contexts
131
+ % ------------------------------------------------------------------
132
+
133
+ rejected_violation(Failure, violation('kb-consistency', "kb-certificates", Description,
134
+ "Kibi produced a certificate its own checker rejects. Report it with the bundle from kibi check --write-certificates <file>", "")) :-
135
+ ( get_dict(group, Failure, Group)
136
+ -> group_text(Group, Where)
137
+ ; format(string(Where), "certificate ~w (~w)", [Failure.certificate, Failure.kind])
138
+ ),
139
+ format(string(Description),
140
+ "The independent checker rejected ~w: ~w", [Where, Failure.reason]).
141
+
142
+ unresolved_violation(_Theorem, Entry, violation('kb-consistency-unresolved', EntityId, Description, Suggestion, "")) :-
143
+ group_text(Entry, Where),
144
+ ( get_dict(reason, Entry, Reason)
145
+ -> true
146
+ ; Reason = "the contradicting facts' validity windows never overlap"
147
+ ),
148
+ entry_facts(Entry, Facts),
149
+ fact_requirements(Facts, Requirements),
150
+ entity_label(Requirements, Facts, EntityId0),
151
+ ( EntityId0 == "" -> EntityId = Where ; EntityId = EntityId0 ),
152
+ format(string(Description),
153
+ "Kibi cannot decide whether the facts for ~w hold together: ~w. It never counts as consistent", [Where, Reason]),
154
+ Suggestion = "Align the facts' value_type, unit, operator or polarity so they can be compared, or accept that this context stays unchecked".
155
+
156
+ entry_facts(Entry, Facts) :-
157
+ get_dict(subjectKey, Entry, Subject),
158
+ !,
159
+ get_dict(propertyKey, Entry, Property),
160
+ findall(Fact,
161
+ ( kb:current_req(Req),
162
+ kb:effective_req_property_fact(Req, Subject, Fact, Property, _, _, _, _, _, _, _, _)
163
+ ),
164
+ Facts0),
165
+ sort(Facts0, Facts).
166
+ entry_facts(_Entry, []).
167
+
168
+ % ------------------------------------------------------------------
169
+ % Text helpers
170
+ % ------------------------------------------------------------------
171
+
172
+ fact_requirements(Facts, Requirements) :-
173
+ findall(Req,
174
+ ( member(FactText, Facts),
175
+ atom_string(Fact, FactText),
176
+ member(Type, [requires_property, requires_predicate, requires_rule]),
177
+ kb_relationship(Type, Req, Fact),
178
+ kb:current_req(Req)
179
+ ),
180
+ Requirements0),
181
+ sort(Requirements0, Requirements).
182
+
183
+ entity_label([], [], "") :- !.
184
+ entity_label([], Facts, Label) :- !, atomic_list_concat(Facts, '/', Label0), atom_string(Label0, Label).
185
+ entity_label(Requirements, _Facts, Label) :- atomic_list_concat(Requirements, '/', Label0), atom_string(Label0, Label).
186
+
187
+ context_text(Label, Text) :-
188
+ ( get_dict(scope, Label, Scope0) -> atom_string(Scope0, Scope) ; Scope = "" ),
189
+ ( Scope == ""
190
+ -> format(string(Text), "~w.~w", [Label.subjectKey, Label.propertyKey])
191
+ ; format(string(Text), "~w.~w (scope ~w)", [Label.subjectKey, Label.propertyKey, Scope])
192
+ ).
193
+
194
+ group_text(Group, Text) :-
195
+ ( get_dict(subjectKey, Group, _)
196
+ -> context_text(Group, Text)
197
+ ; get_dict(args, Group, _)
198
+ -> atom_text(Group, Text)
199
+ ; get_dict(key, Group, _)
200
+ -> decision_text(Group, Text)
201
+ ; format(string(Text), "~w", [Group])
202
+ ).
203
+
204
+ atom_text(Label, Text) :-
205
+ atomic_list_concat(Label.args, ', ', Args),
206
+ format(string(Text), "~w:~w(~w)", [Label.namespace, Label.name, Args]).
207
+
208
+ decision_text(Label, Text) :-
209
+ atomic_list_concat(Label.key, ', ', Key),
210
+ format(string(Text), "~w:~w/~w for (~w)", [Label.namespace, Label.name, Label.arity, Key]).
211
+
212
+ signature_text(Signature, Text) :-
213
+ format(string(Text), "~w:~w/~w", [Signature.namespace, Signature.name, Signature.arity]).