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.
@@ -5,7 +5,8 @@
5
5
  requirement_proof_context/1,
6
6
  requirement_proof_context/4,
7
7
  requirement_proof_context/6,
8
- requirement_proof/4
8
+ requirement_proof/4,
9
+ test_receipt_evidence/4
9
10
  ]).
10
11
 
11
12
  % Source contains non-ASCII text; do not depend on the host locale.
@@ -24,8 +25,10 @@
24
25
  check_rule_verifiability/1
25
26
  ]).
26
27
  :- use_module('logic_ir.pl', [logic_rule_safety/2]).
28
+ :- use_module('code_certificates.pl', [code_certificate_check/2, code_certificate_digest/2]).
29
+ :- use_module('claim_obligations.pl', [claim_obligation/3, obligation_equal/2]).
27
30
 
28
- proof_version('kibi.requirement-proof.v3').
31
+ proof_version('kibi.requirement-proof.v5').
29
32
 
30
33
  requirement_proof_context(Context) :-
31
34
  requirement_proof_context(unknown, '1970-01-01T00:00:00Z', 604800, Context).
@@ -71,12 +74,12 @@ requirement_proof(ReqId, ReqProps, _Context, Proof) :-
71
74
  kb:current_req(ReqId),
72
75
  proof_exempt(ReqProps, Reason),
73
76
  !,
74
- exempt_requirement_proof(Reason, Proof).
75
- requirement_proof(ReqId, _ReqProps, _Context, Proof) :-
77
+ exempt_requirement_proof(Reason, ReqProps, Proof).
78
+ requirement_proof(ReqId, ReqProps, _Context, Proof) :-
76
79
  \+ kb:current_req(ReqId),
77
80
  !,
78
81
  applicability_exclusion_reason(ReqId, Reason),
79
- exempt_requirement_proof(Reason, Proof).
82
+ exempt_requirement_proof(Reason, ReqProps, Proof).
80
83
  requirement_proof(ReqId, ReqProps, Context, Proof) :-
81
84
  semantic_inventory_stage(ReqProps, SemanticStage, Inventory),
82
85
  logic_grounding_stage(ReqId, ReqProps, Inventory, Context, LogicStage),
@@ -101,22 +104,48 @@ requirement_proof(ReqId, ReqProps, Context, Proof) :-
101
104
  proof_issues(Stages, Gaps, Advisories),
102
105
  proof_repairs(Gaps, Repairs),
103
106
  proof_status(Stages, Status),
107
+ certificate_rejections(ScenarioTestStage, Context, Rejections),
108
+ claim_evidence(ReqId, ReqProps, Inventory, Status, ScenarioTestStage, Context,
109
+ ClaimEvidence, ProvedTests, BindingRejections),
110
+ requirement_evidence(ReqProps, Status, PassingE2eTests, ProvedTests, EvidenceLevel, Evidence),
111
+ evidence_floor(ReqProps, EvidenceLevel, Gaps, Stages, EvidenceFloor),
112
+ claim_summary(ClaimEvidence, ClaimSummary, UnprovedClaims),
104
113
  proof_version(Version),
105
- Proof = _{
114
+ Proof0 = _{
106
115
  proofVersion: Version,
107
116
  proofStatus: Status,
117
+ evidenceLevel: EvidenceLevel,
118
+ evidence: Evidence,
119
+ evidenceFloor: EvidenceFloor,
120
+ claimEvidence: ClaimEvidence,
121
+ claimSummary: ClaimSummary,
122
+ unprovedClaims: UnprovedClaims,
108
123
  proofGaps: Gaps,
109
124
  proofAdvisories: Advisories,
110
125
  proofRepairs: Repairs,
111
126
  proofStages: Stages,
112
127
  testResolutions: ScenarioTestStage.resolutions
113
- }.
128
+ },
129
+ ( Rejections == []
130
+ -> Proof1 = Proof0
131
+ ; Proof1 = Proof0.put(certificateRejections, Rejections)
132
+ ),
133
+ ( BindingRejections == []
134
+ -> Proof = Proof1
135
+ ; Proof = Proof1.put(claimBindingRejections, BindingRejections)
136
+ ).
114
137
 
115
- exempt_requirement_proof(Reason, Proof) :-
138
+ exempt_requirement_proof(Reason, ReqProps, Proof) :-
139
+ requirement_evidence(ReqProps, not_applicable, [], [], EvidenceLevel, Evidence),
116
140
  proof_version(Version),
117
141
  Proof = _{
118
142
  proofVersion: Version,
119
143
  proofStatus: not_applicable,
144
+ evidenceLevel: EvidenceLevel,
145
+ evidence: Evidence,
146
+ claimEvidence: [],
147
+ claimSummary: _{applicable: 0, proved: 0, attested: 0, tested: 0, unresolved: 0},
148
+ unprovedClaims: [],
120
149
  proofGaps: [],
121
150
  proofAdvisories: [],
122
151
  proofRepairs: [],
@@ -443,10 +472,30 @@ inventory_ground_text_mismatches(Inventory, Evidence, Keys) :-
443
472
  normalize_atom(RawKey, Key),
444
473
  inventory_entry_field(Entry, claim_text, RawInventoryText),
445
474
  normalize_atom(RawInventoryText, InventoryText),
446
- InventoryText \== Item.claimText),
475
+ \+ claim_texts_equivalent(InventoryText, Item.claimText)),
447
476
  Keys0),
448
477
  sort(Keys0, Keys).
449
478
 
479
+ % implements REQ-kibi-conservative-requirement-proof
480
+ % Inventory and fact claim texts name the same sentence when they differ only
481
+ % in whitespace or trailing punctuation ("…outcomes." vs "…outcomes").
482
+ claim_texts_equivalent(Left, Right) :-
483
+ canonical_claim_text(Left, Canonical),
484
+ canonical_claim_text(Right, Canonical).
485
+
486
+ canonical_claim_text(Text, Canonical) :-
487
+ normalize_space(codes(Codes0), Text),
488
+ reverse(Codes0, Reversed0),
489
+ drop_trailing_claim_punctuation(Reversed0, Reversed),
490
+ reverse(Reversed, Codes),
491
+ atom_codes(Canonical, Codes).
492
+
493
+ drop_trailing_claim_punctuation([Code|Rest], Trimmed) :-
494
+ (code_type(Code, space) ; memberchk(Code, `.,;:!?`)),
495
+ !,
496
+ drop_trailing_claim_punctuation(Rest, Trimmed).
497
+ drop_trailing_claim_punctuation(Codes, Codes).
498
+
450
499
  linked_ground_keys(ReqId, Keys) :-
451
500
  findall(Key, linked_ground_claim_key(ReqId, Key), Keys0),
452
501
  sort(Keys0, Keys).
@@ -904,7 +953,9 @@ proof_receipt_entries(Props, Receipts, Present) :-
904
953
 
905
954
  valid_receipt_shape(TestId, Receipt) :-
906
955
  inventory_entry_field(Receipt, version, RawVersion),
907
- normalize_receipt_atom(RawVersion, 'kibi.proof-receipt.v1'),
956
+ normalize_receipt_atom(RawVersion, Version),
957
+ receipt_version(Version),
958
+ valid_receipt_version_fields(Version, Receipt),
908
959
  inventory_entry_field(Receipt, receipt_id, RawReceiptId),
909
960
  normalize_receipt_atom(RawReceiptId, ReceiptId), valid_receipt_id(ReceiptId),
910
961
  inventory_entry_field(Receipt, test_id, RawTestId),
@@ -949,6 +1000,34 @@ valid_receipt_shape(TestId, Receipt) :-
949
1000
  receipt_timestamp(Receipt, finished_at, FinishedStamp),
950
1001
  FinishedStamp >= StartedStamp.
951
1002
 
1003
+ % kibi.proof-receipt.v2 adds the contract kind and the evidence level the
1004
+ % receipt supports; v1 receipts are still read, as e2e evidence at tested.
1005
+ receipt_version('kibi.proof-receipt.v1').
1006
+ receipt_version('kibi.proof-receipt.v2').
1007
+
1008
+ valid_receipt_version_fields('kibi.proof-receipt.v1', _Receipt).
1009
+ valid_receipt_version_fields('kibi.proof-receipt.v2', Receipt) :-
1010
+ inventory_entry_field(Receipt, kind, RawKind),
1011
+ normalize_receipt_atom(RawKind, Kind),
1012
+ memberchk(Kind, [e2e, example, property, exhaustive, typecheck, arithmetic, smt]),
1013
+ inventory_entry_field(Receipt, evidence_level, RawLevel),
1014
+ normalize_receipt_atom(RawLevel, Level),
1015
+ memberchk(Level, [proved, tested]),
1016
+ ( inventory_entry_field(Receipt, certificate_digest, RawDigest)
1017
+ -> normalize_receipt_atom(RawDigest, Digest),
1018
+ valid_sha256(Digest)
1019
+ ; true
1020
+ ),
1021
+ ( Level == proved
1022
+ -> memberchk(Kind, [exhaustive, typecheck, arithmetic, smt]),
1023
+ inventory_entry_field(Receipt, outcome, RawOutcome),
1024
+ normalize_receipt_atom(RawOutcome, passed),
1025
+ inventory_entry_field(Receipt, certificate, _),
1026
+ inventory_entry_field(Receipt, certificate_digest, _),
1027
+ \+ inventory_entry_field(Receipt, certificate_rejection, _)
1028
+ ; true
1029
+ ).
1030
+
952
1031
  receipt_producer_name(Producer, Name) :-
953
1032
  inventory_entry_field(Producer, name, RawName),
954
1033
  normalize_receipt_atom(RawName, Name).
@@ -1049,6 +1128,15 @@ receipt_for_current_mode(TestId, Context, Receipts, SnapshotReceipts) :-
1049
1128
  receipt_with_binding_hash(Receipt) :-
1050
1129
  inventory_entry_field(Receipt, binding_hash, _).
1051
1130
 
1131
+ % The current binding is one hash, or a list of the hashes a current receipt
1132
+ % may carry (the current binding and the one written before relationship
1133
+ % links left it).
1134
+ receipt_for_binding(Bindings, Receipt) :-
1135
+ is_list(Bindings),
1136
+ !,
1137
+ inventory_entry_field(Receipt, binding_hash, RawBinding),
1138
+ normalize_receipt_atom(RawBinding, Binding),
1139
+ memberchk(Binding, Bindings).
1052
1140
  receipt_for_binding(Binding, Receipt) :-
1053
1141
  inventory_entry_field(Receipt, binding_hash, RawBinding),
1054
1142
  normalize_receipt_atom(RawBinding, Binding).
@@ -1061,7 +1149,8 @@ receipt_matches_current_binding(Scope, ContractBinding, Receipt) :-
1061
1149
  receipt_matches_current_contract(none, _Receipt).
1062
1150
  receipt_matches_current_contract(hash(ExpectedHash), Receipt) :-
1063
1151
  inventory_entry_field(Receipt, version, RawVersion),
1064
- normalize_receipt_atom(RawVersion, 'kibi.proof-receipt.v1'),
1152
+ normalize_receipt_atom(RawVersion, Version),
1153
+ receipt_version(Version),
1065
1154
  inventory_entry_field(Receipt, contract_hash, RawContractHash),
1066
1155
  normalize_receipt_atom(RawContractHash, ExpectedHash).
1067
1156
 
@@ -1158,7 +1247,7 @@ receipt_evidence_dict(TestId, Scope, Receipt, State, AgeSeconds, Evidence) :-
1158
1247
  receipt_string_field(Receipt, started_at, StartedAt),
1159
1248
  receipt_string_field(Receipt, finished_at, FinishedAt),
1160
1249
  receipt_string_field(Receipt, artifact_digest, ArtifactDigest),
1161
- Evidence = _{
1250
+ Evidence0 = _{
1162
1251
  testId: TestId,
1163
1252
  state: State,
1164
1253
  scope: Scope,
@@ -1170,7 +1259,62 @@ receipt_evidence_dict(TestId, Scope, Receipt, State, AgeSeconds, Evidence) :-
1170
1259
  finishedAt: FinishedAt,
1171
1260
  artifactDigest: ArtifactDigest,
1172
1261
  ageSeconds: AgeSeconds
1173
- }.
1262
+ },
1263
+ receipt_kind_evidence(Receipt, State, Evidence0, Evidence).
1264
+
1265
+ % implements REQ-kibi-code-proof-kinds
1266
+ % A v2 receipt adds its contract kind and the evidence level it supports.
1267
+ % proved needs the certificate to re-check now, not only when it was
1268
+ % ingested: its digest must still match and code_certificates.pl must accept
1269
+ % it. The digest is an unsigned hash, so this catches edits and corruption,
1270
+ % not deliberate forgery. When a receipt that claimed proved no longer
1271
+ % re-checks, the evidence says why (certificateRejection).
1272
+ receipt_kind_evidence(Receipt, State, Evidence0, Evidence) :-
1273
+ receipt_string_field(Receipt, version, 'kibi.proof-receipt.v2'),
1274
+ receipt_string_field(Receipt, kind, Kind),
1275
+ !,
1276
+ ( State == passed,
1277
+ receipt_string_field(Receipt, evidence_level, proved)
1278
+ -> receipt_certificate_recheck(Receipt, Recheck),
1279
+ ( Recheck == ok
1280
+ -> Evidence1 = Evidence0.put(_{kind: Kind, evidenceLevel: proved})
1281
+ ; Recheck = rejected(Reason),
1282
+ Evidence1 = Evidence0.put(_{kind: Kind, evidenceLevel: tested,
1283
+ certificateRejection: Reason})
1284
+ )
1285
+ ; Evidence1 = Evidence0.put(_{kind: Kind, evidenceLevel: tested})
1286
+ ),
1287
+ receipt_claim_fields(Receipt, Evidence1, Evidence).
1288
+ receipt_kind_evidence(_Receipt, _State, Evidence, Evidence).
1289
+
1290
+ % receipt_certificate_recheck(+Receipt, -Result): ok or rejected(Reason).
1291
+ receipt_certificate_recheck(Receipt, Result) :-
1292
+ ( inventory_entry_field(Receipt, certificate, Certificate),
1293
+ is_dict(Certificate)
1294
+ -> true
1295
+ ; Certificate = none
1296
+ ),
1297
+ ( Certificate == none
1298
+ -> Result = rejected("the receipt has no certificate")
1299
+ ; \+ receipt_string_field(Receipt, certificate_digest, _)
1300
+ -> Result = rejected("the receipt has no certificate_digest")
1301
+ ; \+ ( receipt_string_field(Receipt, test_id, TestId),
1302
+ get_dict(test_id, Certificate, CertificateTestId),
1303
+ atom_string(TestId, CertificateTestId) )
1304
+ -> Result = rejected("the certificate is for another test")
1305
+ ; \+ ( receipt_string_field(Receipt, kind, Kind),
1306
+ get_dict(kind, Certificate, CertificateKind),
1307
+ atom_string(Kind, CertificateKind) )
1308
+ -> Result = rejected("the certificate kind is not the receipt kind")
1309
+ ; receipt_string_field(Receipt, certificate_digest, Digest),
1310
+ code_certificate_digest(Certificate, Recomputed),
1311
+ \+ atom_string(Digest, Recomputed)
1312
+ -> Result = rejected("the certificate no longer matches its certificate_digest")
1313
+ ; code_certificate_check(Certificate, Check),
1314
+ Check = rejected(Reason)
1315
+ -> Result = rejected(Reason)
1316
+ ; Result = ok
1317
+ ).
1174
1318
 
1175
1319
  receipt_string_field(Receipt, Field, Value) :-
1176
1320
  inventory_entry_field(Receipt, Field, RawValue),
@@ -1226,7 +1370,8 @@ production_symbol_stage(ReqId, PassingE2eTests, Stage, ProductionSymbols) :-
1226
1370
  scenario_test(ScenarioId, TestId)),
1227
1371
  ScenarioTests0),
1228
1372
  sort(ScenarioTests0, ScenarioTests),
1229
- production_symbol_stage(ReqId, PassingE2eTests, ScenarioTests, [], _{}, Stage, ProductionSymbols).
1373
+ requirement_proof_context(Context),
1374
+ production_symbol_stage(ReqId, PassingE2eTests, ScenarioTests, [], Context, Stage, ProductionSymbols).
1230
1375
 
1231
1376
  production_symbol_stage(ReqId, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, Stage, ProductionSymbols) :-
1232
1377
  findall(SymbolId,
@@ -1237,12 +1382,16 @@ production_symbol_stage(ReqId, PassingE2eTests, ScenarioTests, ReceiptEvidence,
1237
1382
  sort(Implementing0, ImplementingSymbols),
1238
1383
  include(type_shape_symbol_with_structural_contract, ImplementingSymbols, StructuralSymbols),
1239
1384
  exclude(type_shape_symbol_with_structural_contract, ImplementingSymbols, ProductionSymbols),
1240
- include(symbol_not_covered_by_tests(PassingE2eTests), ProductionSymbols, UncoveredSymbols),
1241
- production_stage_status(ProductionSymbols, StructuralSymbols, PassingE2eTests, UncoveredSymbols, Status, StatusReason),
1385
+ include(symbol_not_covered_by_tests(PassingE2eTests), ProductionSymbols, UncoveredProduction),
1386
+ structural_unit_receipts(StructuralSymbols, Context, UnitReceipts),
1387
+ exclude(structural_symbol_covered(PassingE2eTests, UnitReceipts), StructuralSymbols, UncoveredStructural),
1388
+ append(UncoveredProduction, UncoveredStructural, UncoveredSymbols),
1389
+ production_stage_status(ProductionSymbols, StructuralSymbols, PassingE2eTests,
1390
+ UncoveredProduction, UncoveredStructural, Status, StatusReason),
1242
1391
  maplist(symbol_coordinate_ref, ProductionSymbols, Coordinates),
1243
1392
  production_coverage_explanations(
1244
1393
  ReqId, ProductionSymbols, StructuralSymbols, UncoveredSymbols,
1245
- PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, Explanations),
1394
+ PassingE2eTests, ScenarioTests, ReceiptEvidence, UnitReceipts-Context, Explanations),
1246
1395
  StageBase = _{
1247
1396
  status: Status,
1248
1397
  symbols: ProductionSymbols,
@@ -1267,13 +1416,40 @@ type_shape_symbol_with_structural_contract(SymbolId) :-
1267
1416
  normalize_atom(RawStatus, Status),
1268
1417
  memberchk(Status, [active, passing]).
1269
1418
 
1419
+ % implements REQ-kibi-conservative-requirement-proof
1420
+ % A type-shape symbol is structurally covered by a unit test only while that
1421
+ % test holds a current passing receipt: an authored status alone is a claim,
1422
+ % not evidence. Receipt states are computed once per distinct unit test.
1423
+ structural_unit_receipts(StructuralSymbols, Context, UnitReceipts) :-
1424
+ findall(TestId,
1425
+ (member(SymbolId, StructuralSymbols),
1426
+ kb_relationship(covered_by, SymbolId, TestId),
1427
+ structural_unit_contract_test(TestId)),
1428
+ TestIds0),
1429
+ sort(TestIds0, TestIds),
1430
+ findall(TestId-State,
1431
+ (member(TestId, TestIds),
1432
+ test_receipt_evidence(Context, [unit], TestId, Evidence),
1433
+ State = Evidence.state),
1434
+ UnitReceipts).
1435
+
1436
+ structural_symbol_covered(PassingE2eTests, _UnitReceipts, SymbolId) :-
1437
+ member(TestId, PassingE2eTests),
1438
+ kb_relationship(covered_by, SymbolId, TestId),
1439
+ !.
1440
+ structural_symbol_covered(_PassingE2eTests, UnitReceipts, SymbolId) :-
1441
+ kb_relationship(covered_by, SymbolId, TestId),
1442
+ memberchk(TestId-passed, UnitReceipts),
1443
+ !.
1444
+
1270
1445
  % Stage status plus a typed reason, from a single total clause so status and
1271
1446
  % reason can never disagree (a test project's stale-receipts case reported `blocked`
1272
1447
  % with no indication that fresh receipts were the unblock). Clause order keeps
1273
1448
  % the historical precedence: no production symbols at all is `missing` even
1274
1449
  % when E2E evidence is also absent; only then does absent E2E evidence read
1275
1450
  % as `blocked`.
1276
- production_stage_status(ProductionSymbols, StructuralSymbols, PassingE2eTests, UncoveredSymbols, Status, Reason) :-
1451
+ production_stage_status(ProductionSymbols, StructuralSymbols, PassingE2eTests,
1452
+ UncoveredProduction, UncoveredStructural, Status, Reason) :-
1277
1453
  ( ProductionSymbols == [],
1278
1454
  StructuralSymbols == []
1279
1455
  -> Status = missing,
@@ -1281,34 +1457,50 @@ production_stage_status(ProductionSymbols, StructuralSymbols, PassingE2eTests, U
1281
1457
  ; PassingE2eTests == []
1282
1458
  -> Status = blocked,
1283
1459
  Reason = "no passing E2E test evidence is available for the current snapshot; run kibi prove so this stage can evaluate production symbol coverage"
1284
- ; UncoveredSymbols == []
1460
+ ; UncoveredProduction == [],
1461
+ UncoveredStructural == []
1285
1462
  -> Status = passed
1286
- ; Status = missing,
1287
- length(UncoveredSymbols, Count),
1463
+ ; UncoveredStructural == []
1464
+ -> Status = missing,
1465
+ length(UncoveredProduction, Count),
1288
1466
  format(atom(Reason), "~w production symbol(s) lack covered_by links to the passing E2E tests", [Count])
1467
+ ; UncoveredProduction == []
1468
+ -> Status = missing,
1469
+ length(UncoveredStructural, Count),
1470
+ format(atom(Reason), "~w type-shape symbol(s) lack a covered_by unit test with a current passing receipt; run kibi prove on those unit tests", [Count])
1471
+ ; Status = missing,
1472
+ length(UncoveredProduction, ProductionCount),
1473
+ length(UncoveredStructural, StructuralCount),
1474
+ format(atom(Reason), "~w production symbol(s) lack covered_by links to the passing E2E tests and ~w type-shape symbol(s) lack a covered_by unit test with a current passing receipt", [ProductionCount, StructuralCount])
1289
1475
  ).
1290
1476
 
1291
1477
  symbol_not_covered_by_tests(Tests, SymbolId) :-
1292
1478
  \+ (member(TestId, Tests), kb_relationship(covered_by, SymbolId, TestId)).
1293
1479
 
1294
1480
  production_coverage_explanations(ReqId, ProductionSymbols, StructuralSymbols, UncoveredSymbols,
1295
- PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, Explanations) :-
1481
+ PassingE2eTests, ScenarioTests, ReceiptEvidence, UnitReceipts-Context, Explanations) :-
1296
1482
  maplist(production_symbol_explanation(ReqId, UncoveredSymbols, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context),
1297
1483
  ProductionSymbols, ProductionExplanations),
1298
- maplist(structural_symbol_explanation(ReqId, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context),
1484
+ maplist(structural_symbol_explanation(ReqId, UncoveredSymbols, PassingE2eTests, ScenarioTests, ReceiptEvidence, UnitReceipts-Context),
1299
1485
  StructuralSymbols, StructuralExplanations),
1300
1486
  append(ProductionExplanations, StructuralExplanations, Explanations).
1301
1487
 
1302
- structural_symbol_explanation(ReqId, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, SymbolId, Explanation) :-
1488
+ structural_symbol_explanation(ReqId, UncoveredSymbols, PassingE2eTests, ScenarioTests, ReceiptEvidence, UnitReceipts-Context, SymbolId, Explanation) :-
1303
1489
  symbol_classification(SymbolId, Classification),
1304
- symbol_coverage_candidates(structural, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, SymbolId, Candidates),
1305
- explanation_reason_text(structural_unit_contract, ReasonText),
1490
+ symbol_coverage_candidates(structural(UnitReceipts), PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, SymbolId, Candidates),
1491
+ ( memberchk(SymbolId, UncoveredSymbols)
1492
+ -> Status = uncovered,
1493
+ Reason = structural_unit_receipt_not_current
1494
+ ; Status = covered,
1495
+ Reason = structural_unit_contract
1496
+ ),
1497
+ explanation_reason_text(Reason, ReasonText),
1306
1498
  Explanation = _{
1307
1499
  symbolId: SymbolId,
1308
1500
  requirementId: ReqId,
1309
1501
  classification: Classification,
1310
- status: covered,
1311
- reason: structural_unit_contract,
1502
+ status: Status,
1503
+ reason: Reason,
1312
1504
  reasonText: ReasonText,
1313
1505
  coverageCandidates: Candidates
1314
1506
  }.
@@ -1378,9 +1570,20 @@ coverage_candidate_qualification(_Mode, PassingE2eTests, _ScenarioTests, _Receip
1378
1570
  coverage_candidate_qualification(_Mode, _Passing, _Scenario, _Evidence, _Context, TestId, false, missing_test_entity, []) :-
1379
1571
  \+ kb_entity(TestId, test, _),
1380
1572
  !.
1381
- coverage_candidate_qualification(structural, _Passing, _Scenario, _Evidence, _Context, TestId, true, structural_unit_contract, []) :-
1573
+ coverage_candidate_qualification(structural(UnitReceipts), _Passing, _Scenario, _Evidence, _Context, TestId, Qualifies, Primary, Secondaries) :-
1382
1574
  structural_unit_contract_test(TestId),
1383
- !.
1575
+ !,
1576
+ ( memberchk(TestId-passed, UnitReceipts)
1577
+ -> Qualifies = true,
1578
+ Primary = structural_unit_contract,
1579
+ Secondaries = []
1580
+ ; Qualifies = false,
1581
+ Primary = structural_unit_receipt_not_current,
1582
+ ( memberchk(TestId-State, UnitReceipts)
1583
+ -> unit_receipt_secondary(State, Secondaries)
1584
+ ; Secondaries = []
1585
+ )
1586
+ ).
1384
1587
  coverage_candidate_qualification(_Mode, _Passing, ScenarioTests, ReceiptEvidence, Context, TestId, false, Primary, Secondaries) :-
1385
1588
  cheap_scope_reject_reason(TestId, ScopeReason),
1386
1589
  ( \+ memberchk(TestId, ScenarioTests)
@@ -1394,6 +1597,14 @@ coverage_candidate_qualification(_Mode, _Passing, ScenarioTests, ReceiptEvidence
1394
1597
  ; receipt_reject_for_in_chain(ReceiptEvidence, Context, TestId, Primary, Secondaries)
1395
1598
  ).
1396
1599
 
1600
+ unit_receipt_secondary(missing, [missing_proof_receipt]) :- !.
1601
+ unit_receipt_secondary(stale, [stale_proof_receipt]) :- !.
1602
+ unit_receipt_secondary(contract_mismatch, [receipt_contract_mismatch]) :- !.
1603
+ unit_receipt_secondary(failed, [failed_proof_receipt]) :- !.
1604
+ unit_receipt_secondary(invalid, [invalid_proof_receipt]) :- !.
1605
+ unit_receipt_secondary(snapshot_unavailable, [proof_snapshot_unavailable]) :- !.
1606
+ unit_receipt_secondary(_, []).
1607
+
1397
1608
  structural_unit_contract_test(TestId) :-
1398
1609
  kb_entity(TestId, test, TestProps),
1399
1610
  test_scope(TestProps, unit),
@@ -1482,6 +1693,7 @@ explanation_reason_text_def(covered, "Qualifying covered_by to a passing scenari
1482
1693
  explanation_reason_text_def(covered_by_missing, "No covered_by relationship to any test").
1483
1694
  explanation_reason_text_def(no_qualifying_e2e_coverage, "covered_by relationships exist but none qualify as passing scenario-backed E2E coverage").
1484
1695
  explanation_reason_text_def(structural_unit_contract, "type-shape symbol is satisfied by a unit structural contract").
1696
+ explanation_reason_text_def(structural_unit_receipt_not_current, "type-shape symbol needs a covered_by unit test with a current passing receipt; run kibi prove on that unit test").
1485
1697
  explanation_reason_text_def(executable_test, "Symbol is executable test code linked with executable_for").
1486
1698
  explanation_reason_text_def(stage_blocked_no_passing_e2e, "Production coverage cannot be evaluated because no passing E2E evidence is available for the current snapshot").
1487
1699
  explanation_reason_text_def(missing_test_entity, "covered_by target is not a test entity").
@@ -1674,7 +1886,390 @@ proof_status(Stages, unresolved) :-
1674
1886
  stage_dict(Stages, Stage),
1675
1887
  memberchk(Stage.status, [unresolved, blocked]),
1676
1888
  !.
1677
- proof_status(_, proven).
1889
+ proof_status(_, tested).
1890
+
1891
+ % implements REQ-kibi-code-proof-kinds
1892
+ % Scenario tests whose receipt claimed proved but whose certificate no
1893
+ % longer re-checks, with the reason, so a drop to tested is never silent.
1894
+ certificate_rejections(ScenarioTestStage, Context, Rejections) :-
1895
+ ( get_dict(obligations, ScenarioTestStage, Obligations)
1896
+ -> true
1897
+ ; Obligations = []
1898
+ ),
1899
+ findall(_{test: TestId, reason: Reason},
1900
+ (member(Obligation, Obligations),
1901
+ member(TestId, Obligation.tests),
1902
+ test_receipt_evidence(Context, [unit, integration, end_to_end], TestId, Evidence),
1903
+ get_dict(certificateRejection, Evidence, Reason)),
1904
+ Rejections0),
1905
+ sort(Rejections0, Rejections).
1906
+
1907
+ % implements REQ-kibi-claim-bound-proofs
1908
+ %% receipt_claim_fields(+Receipt, +Evidence0, -Evidence)
1909
+ % A certificate receipt keeps the certificate and the claim bindings Kibi
1910
+ % recorded at ingest, so each claim can be judged against the current KB.
1911
+ receipt_claim_fields(Receipt, Evidence0, Evidence) :-
1912
+ ( inventory_entry_field(Receipt, claim_bindings, RawBindings),
1913
+ inventory_entries(RawBindings, Bindings),
1914
+ Bindings \== []
1915
+ -> Evidence1 = Evidence0.put(claimBindings, Bindings)
1916
+ ; Evidence1 = Evidence0
1917
+ ),
1918
+ ( inventory_entry_field(Receipt, certificate, Certificate),
1919
+ is_dict(Certificate)
1920
+ -> Evidence = Evidence1.put(certificate, Certificate)
1921
+ ; Evidence = Evidence1
1922
+ ).
1923
+
1924
+ % implements REQ-kibi-claim-bound-proofs
1925
+ %% claim_evidence(+ReqId, +Props, +Inventory, +ProofStatus, +ScenarioTestStage,
1926
+ %% +Context, -ClaimEvidence, -ProvedTests, -Rejections)
1927
+ % One entry per semantic-inventory claim with the evidence level that claim
1928
+ % has on its own. A claim is proved only when the requirement is tested end
1929
+ % to end and a fresh certificate whose obligation Kibi derived from the
1930
+ % claim's grounding fact (binding derived, scope code) re-checks. A
1931
+ % certificate the author only attests to the claim leaves it tested.
1932
+ % ProvedTests is non-empty, as TestId-Kind pairs, only when every applicable
1933
+ % claim is proved; the requirement is proved exactly then.
1934
+ claim_evidence(ReqId, Props, Inventory, ProofStatus, ScenarioTestStage, Context,
1935
+ ClaimEvidence, ProvedTests, Rejections) :-
1936
+ ( get_dict(tests, ScenarioTestStage, Tests) -> true ; Tests = [] ),
1937
+ ( ProofStatus == tested
1938
+ -> findall(TestId-Evidence,
1939
+ (member(TestId, Tests),
1940
+ test_receipt_evidence(Context, [unit, integration, end_to_end], TestId, Evidence),
1941
+ get_dict(state, Evidence, passed),
1942
+ get_dict(claimBindings, Evidence, _)),
1943
+ BoundTests)
1944
+ ; BoundTests = []
1945
+ ),
1946
+ intent_claim_level(Props, IntentLevel),
1947
+ maplist(claim_entry(ReqId, ProofStatus, IntentLevel, BoundTests), Inventory, Entries0),
1948
+ exclude(==(none), Entries0, ClaimEvidence),
1949
+ findall(R, (member(E, ClaimEvidence), get_dict(rejections, E, Rs), member(R, Rs)), Rejections0),
1950
+ sort(Rejections0, Rejections),
1951
+ include(applicable_claim, ClaimEvidence, Applicable),
1952
+ ( ProofStatus == tested,
1953
+ Applicable \== [],
1954
+ forall(member(E, Applicable), claim_level_is(proved, E))
1955
+ -> findall(T-K,
1956
+ (member(E, Applicable), get_dict(bindings, E, Bs), member(B, Bs),
1957
+ get_dict(binding, B, derived), get_dict(test, B, T), get_dict(kind, B, K)),
1958
+ Pairs0),
1959
+ sort(Pairs0, ProvedTests)
1960
+ ; ProvedTests = []
1961
+ ).
1962
+
1963
+ intent_claim_level(Props, Level) :-
1964
+ ( human_intent(Props, _, _) -> Level = asserted ; Level = unverified ).
1965
+
1966
+ applicable_claim(Entry) :- get_dict(applicable, Entry, true).
1967
+
1968
+ claim_level_is(Level, Entry) :- get_dict(level, Entry, Level).
1969
+
1970
+ claim_attested_only(Entry) :-
1971
+ \+ claim_level_is(proved, Entry),
1972
+ get_dict(bindings, Entry, Bindings),
1973
+ member(B, Bindings),
1974
+ get_dict(binding, B, attested),
1975
+ !.
1976
+
1977
+ claim_unresolved(Entry) :- get_dict(blocker, Entry, _).
1978
+
1979
+ claim_entry(ReqId, ProofStatus, IntentLevel, BoundTests, InventoryEntry, Entry) :-
1980
+ ( inventory_entry_field(InventoryEntry, claim_key, RawKey),
1981
+ normalize_atom(RawKey, Key)
1982
+ -> inventory_entry_status(InventoryEntry, Status),
1983
+ ( inventory_entry_field(InventoryEntry, role, RawRole)
1984
+ -> normalize_atom(RawRole, Role)
1985
+ ; Role = ''
1986
+ ),
1987
+ Base = _{claimKey: Key, status: Status, role: Role},
1988
+ ( Status == nonlogical
1989
+ -> Entry = Base.put(_{applicable: false, level: null, bindings: []})
1990
+ ; Status \== modeled
1991
+ -> Entry = Base.put(_{applicable: true, level: IntentLevel, bindings: [],
1992
+ blocker: unresolved_semantic_proposition})
1993
+ ; ProofStatus \== tested
1994
+ -> Entry = Base.put(_{applicable: true, level: IntentLevel, bindings: []})
1995
+ ; findall(Binding, test_claim_binding(ReqId, Key, BoundTests, Binding), Bindings0),
1996
+ sort(Bindings0, Bindings),
1997
+ include(binding_is(rejected), Bindings, RejectedBindings),
1998
+ maplist(binding_rejection(Key), RejectedBindings, Rejections),
1999
+ ( member(B, Bindings), get_dict(binding, B, derived), get_dict(scope, B, code)
2000
+ -> Level = proved
2001
+ ; Level = tested
2002
+ ),
2003
+ Entry0 = Base.put(_{applicable: true, level: Level, bindings: Bindings}),
2004
+ ( Rejections == []
2005
+ -> Entry = Entry0
2006
+ ; Entry = Entry0.put(rejections, Rejections)
2007
+ )
2008
+ )
2009
+ ; Entry = none
2010
+ ).
2011
+
2012
+ binding_is(Expected, Binding) :- get_dict(binding, Binding, Expected).
2013
+
2014
+ binding_rejection(Key, Binding, _{test: Test, claimKey: Key, reason: Reason}) :-
2015
+ get_dict(test, Binding, Test),
2016
+ get_dict(reason, Binding, Reason).
2017
+
2018
+ % The binding one passed receipt records for this claim of this requirement,
2019
+ % re-judged against the current KB.
2020
+ test_claim_binding(ReqId, Key, BoundTests, Binding) :-
2021
+ member(TestId-Evidence, BoundTests),
2022
+ get_dict(claimBindings, Evidence, Recorded0),
2023
+ member(Recorded, Recorded0),
2024
+ inventory_entry_field(Recorded, claim_key, RawKey),
2025
+ normalize_atom(RawKey, Key),
2026
+ inventory_entry_field(Recorded, requirement_id, RawReq),
2027
+ normalize_atom(RawReq, ReqId),
2028
+ ( get_dict(kind, Evidence, Kind) -> true ; Kind = e2e ),
2029
+ judge_claim_binding(ReqId, Key, Kind, Evidence, Recorded, Verdict, Scope, Reason),
2030
+ Binding0 = _{test: TestId, kind: Kind, binding: Verdict, scope: Scope},
2031
+ ( Reason == ''
2032
+ -> Binding = Binding0
2033
+ ; Binding = Binding0.put(reason, Reason)
2034
+ ).
2035
+
2036
+ % judge_claim_binding(+ReqId, +Key, +Kind, +Evidence, +Recorded, -Verdict, -Scope, -Reason)
2037
+ judge_claim_binding(ReqId, Key, Kind, Evidence, Recorded, Verdict, Scope, Reason) :-
2038
+ receipt_string_field(Recorded, binding, RecordedVerdict),
2039
+ ( receipt_string_field(Recorded, scope, Scope0) -> true ; Scope0 = code ),
2040
+ claim_obligation(ReqId-Key, Kind, Current),
2041
+ ( RecordedVerdict == rejected
2042
+ -> Verdict = rejected, Scope = Scope0,
2043
+ ( receipt_string_field(Recorded, reason, Reason) -> true ; Reason = 'rejected at ingest' )
2044
+ ; Current = unresolved(Reason0)
2045
+ -> Verdict = rejected, Scope = Scope0, atom_string(Reason, Reason0)
2046
+ ; Current = incompatible(_, Reason0)
2047
+ -> Verdict = rejected, Scope = Scope0, atom_string(Reason, Reason0)
2048
+ ; RecordedVerdict == attested
2049
+ -> ( Current = attested(_)
2050
+ -> Verdict = attested, Scope = Scope0, Reason = ''
2051
+ ; Verdict = rejected, Scope = Scope0,
2052
+ Reason = 'the claim now has a derivable obligation; re-prove the test so Kibi can derive it'
2053
+ )
2054
+ ; RecordedVerdict == derived,
2055
+ Current = obligation(FactId, Obligation)
2056
+ -> Scope = Scope0,
2057
+ ( \+ ( receipt_string_field(Recorded, fact_id, RecordedFact), RecordedFact == FactId )
2058
+ -> Verdict = rejected,
2059
+ format(atom(Reason), "~w is now grounded by ~w, not the fact the receipt was ingested against", [Key, FactId])
2060
+ ; \+ ( inventory_entry_field(Recorded, obligation, RecordedObligation),
2061
+ obligation_equal(RecordedObligation, Obligation) )
2062
+ -> Verdict = rejected,
2063
+ format(atom(Reason), "~w changed since the receipt was ingested", [FactId])
2064
+ ; \+ get_dict(evidenceLevel, Evidence, proved)
2065
+ -> Verdict = rejected,
2066
+ Reason = 'the certificate does not re-check'
2067
+ ; certificate_obligation_problem(Kind, Evidence, Obligation, Problem)
2068
+ -> Verdict = rejected, Reason = Problem
2069
+ ; Verdict = derived, Reason = ''
2070
+ )
2071
+ ; Verdict = rejected, Scope = Scope0,
2072
+ Reason = 'the claim has no derivable obligation; re-prove the test'
2073
+ ).
2074
+
2075
+ % The part of a derived binding Kibi can re-check from the stored
2076
+ % certificate alone (code-dependent parts were checked at ingest and stale
2077
+ % with the receipt).
2078
+ certificate_obligation_problem(exhaustive, Evidence, Obligation, Problem) :-
2079
+ get_dict(certificate, Evidence, Certificate),
2080
+ get_dict(domain, Certificate, Domain),
2081
+ get_dict(members, Domain, Raw),
2082
+ maplist([M, S]>>(atom(M) -> atom_string(M, S) ; S = M), Raw, Strings),
2083
+ sort(Strings, Members),
2084
+ \+ obligation_equal(Obligation, _{kind: "set_equals", members: Members}),
2085
+ Problem = 'the certificate domain is not the fact\'s set'.
2086
+ certificate_obligation_problem(arithmetic, Evidence, Obligation, Problem) :-
2087
+ get_dict(certificate, Evidence, Certificate),
2088
+ ( get_dict(claim, Certificate, Claim),
2089
+ get_dict(operator, Claim, Op),
2090
+ get_dict(value, Claim, Value),
2091
+ get_dict(variable, Certificate, Variable),
2092
+ get_dict(value_type, Certificate, Type),
2093
+ obligation_equal(Obligation,
2094
+ _{kind: "numeric_constraint", variable: Variable, value_type: Type,
2095
+ operator: Op, value: Value})
2096
+ -> fail
2097
+ ; Problem = 'the certificate\'s claim is not the fact\'s constraint'
2098
+ ).
2099
+
2100
+ %% claim_summary(+ClaimEvidence, -Summary, -UnprovedClaims)
2101
+ claim_summary(ClaimEvidence, Summary, Unproved) :-
2102
+ include(applicable_claim, ClaimEvidence, Applicable),
2103
+ length(Applicable, ApplicableCount),
2104
+ count_claims(Applicable, claim_level_is(proved), Proved),
2105
+ count_claims(Applicable, claim_attested_only, Attested),
2106
+ count_claims(Applicable, claim_level_is(tested), Tested),
2107
+ count_claims(Applicable, claim_unresolved, Unresolved),
2108
+ Summary = _{applicable: ApplicableCount, proved: Proved, attested: Attested,
2109
+ tested: Tested, unresolved: Unresolved},
2110
+ findall(Key, (member(E, Applicable), \+ claim_level_is(proved, E), get_dict(claimKey, E, Key)), Unproved).
2111
+
2112
+ count_claims(Entries, Goal, Count) :-
2113
+ include(Goal, Entries, Matching),
2114
+ length(Matching, Count).
2115
+
2116
+ % implements REQ-kibi-evidence-levels, REQ-kibi-code-proof-kinds
2117
+ %% requirement_evidence(+Props, +ProofStatus, +PassingE2eTests, +ProvedTests, -Level, -Evidence)
2118
+ % How strong the evidence behind a requirement is, never stronger than what
2119
+ % backs it. Levels, strongest first: proved (the requirement is tested and
2120
+ % every applicable claim is proved by a fresh certificate bound to it with a
2121
+ % derived obligation: ProvedTests is non-empty), tested (passing E2E
2122
+ % receipts cover every linked scenario: proofStatus tested), asserted (a
2123
+ % person stands behind the statement but nothing current checks it),
2124
+ % unverified (an agent wrote it and no person approved it). Evidence lists
2125
+ % what earned the level and anything weaker that also holds, each entry
2126
+ % naming its kind and scope.
2127
+ requirement_evidence(Props, ProofStatus, PassingE2eTests, ProvedTests, Level, Evidence) :-
2128
+ findall(Entry, evidence_entry(Props, ProofStatus, PassingE2eTests, ProvedTests, Entry), Evidence),
2129
+ evidence_level(Evidence, Level).
2130
+
2131
+ evidence_entry(_Props, tested, _PassingE2eTests, ProvedTests, _{kind: certificate, scope: code, level: proved, tests: Tests, kinds: Kinds}) :-
2132
+ ProvedTests \= [],
2133
+ pairs_keys_values(ProvedTests, Tests0, Kinds0),
2134
+ sort(Tests0, Tests),
2135
+ sort(Kinds0, Kinds).
2136
+ evidence_entry(_Props, tested, PassingE2eTests, _ProvedTests, _{kind: e2e, scope: code, level: tested, tests: PassingE2eTests}).
2137
+ evidence_entry(Props, _ProofStatus, _PassingE2eTests, _ProvedTests, Entry) :-
2138
+ human_intent(Props, Basis, Ref),
2139
+ ( Ref == ''
2140
+ -> Entry = _{kind: human_intent, scope: intent, level: asserted, basis: Basis}
2141
+ ; Entry = _{kind: human_intent, scope: intent, level: asserted, basis: Basis, ref: Ref}
2142
+ ).
2143
+
2144
+ evidence_level(Evidence, Level) :-
2145
+ ( member(Level0, [proved, tested, asserted]),
2146
+ member(Entry, Evidence),
2147
+ get_dict(level, Entry, Level0)
2148
+ -> Level = Level0
2149
+ ; Level = unverified
2150
+ ).
2151
+
2152
+ % implements REQ-kibi-evidence-floors
2153
+ %% evidence_floor(+Props, +Level, +Gaps, +Stages, -Floor)
2154
+ % The weakest evidence level the requirement accepts and whether its computed
2155
+ % level reaches it. The floor is the requirement's required_evidence, or,
2156
+ % when absent, tested for a firm requirement and unverified (no floor)
2157
+ % otherwise. Status is met, below, or pending_reproof: the floor is tested
2158
+ % and every gap only says recorded receipts are older than the code, so
2159
+ % re-running kibi prove reaches it without any KB change.
2160
+ evidence_floor(Props, Level, Gaps, Stages, _{required: Required, source: Source, status: Status}) :-
2161
+ required_evidence(Props, Required, Source),
2162
+ evidence_rank(Level, Have),
2163
+ evidence_rank(Required, Need),
2164
+ ( Have >= Need
2165
+ -> Status = met
2166
+ ; Required == tested,
2167
+ freshness_only_gaps(Gaps, Stages)
2168
+ -> Status = pending_reproof
2169
+ ; Status = below
2170
+ ).
2171
+
2172
+ required_evidence(Props, Required, explicit) :-
2173
+ memberchk(required_evidence=Raw, Props),
2174
+ normalize_atom(Raw, Required),
2175
+ evidence_rank(Required, _),
2176
+ !.
2177
+ required_evidence(Props, tested, firmness) :-
2178
+ effective_firmness(Props, firm),
2179
+ !.
2180
+ required_evidence(_Props, unverified, firmness).
2181
+
2182
+ evidence_rank(unverified, 0).
2183
+ evidence_rank(asserted, 1).
2184
+ evidence_rank(tested, 2).
2185
+ evidence_rank(proved, 3).
2186
+
2187
+ % Gaps that only say the evidence is stale: kibi prove clears them. Stale
2188
+ % receipts also leave every production symbol uncovered; that gap is
2189
+ % freshness-only when each uncovered symbol has a coverage candidate that
2190
+ % qualifies or is rejected only for stale evidence.
2191
+ freshness_gap(stale_proof_receipt).
2192
+ freshness_gap(proof_contract_mismatch).
2193
+
2194
+ freshness_only_gaps(Gaps, Stages) :-
2195
+ include(freshness_gap, Gaps, [_|_]),
2196
+ exclude(freshness_gap, Gaps, Rest),
2197
+ ( Rest == []
2198
+ -> true
2199
+ ; Rest == [missing_production_symbol_coverage],
2200
+ production_coverage_freshness_only(Stages)
2201
+ ).
2202
+
2203
+ production_coverage_freshness_only(Stages) :-
2204
+ Stage = Stages.productionSymbols,
2205
+ forall(member(SymbolId, Stage.uncoveredSymbols),
2206
+ ( member(Explanation, Stage.explanations),
2207
+ Explanation.symbolId == SymbolId,
2208
+ member(Candidate, Explanation.coverageCandidates),
2209
+ freshness_only_candidate(Candidate)
2210
+ )).
2211
+
2212
+ freshness_only_candidate(Candidate) :-
2213
+ Candidate.qualifies == true,
2214
+ !.
2215
+ freshness_only_candidate(Candidate) :-
2216
+ Candidate.scope == unit,
2217
+ Candidate.reason == structural_unit_receipt_not_current,
2218
+ Candidate.secondaryReasons = [_|_],
2219
+ forall(member(Reason, Candidate.secondaryReasons),
2220
+ memberchk(Reason, [stale_proof_receipt, receipt_snapshot_mismatch, receipt_contract_mismatch])),
2221
+ !.
2222
+ freshness_only_candidate(Candidate) :-
2223
+ Candidate.scope == end_to_end,
2224
+ forall(member(Reason, [Candidate.reason|Candidate.secondaryReasons]),
2225
+ memberchk(Reason, [stale_proof_receipt, receipt_snapshot_mismatch, receipt_contract_mismatch])).
2226
+
2227
+ %% human_intent(+Props, -Basis, -Ref)
2228
+ % A person stands behind the requirement: someone approved it
2229
+ % (origin.approved_by, the field agent-requirement-unapproved reads), or it
2230
+ % reads as firm (firmness: firm, or no firmness and an origin other than
2231
+ % agent, as for decision firmness). The top-level approved_by approves an
2232
+ % exception; an agent can write it, so on its own it approves nothing here.
2233
+ human_intent(Props, approved, Ref) :-
2234
+ origin_field(Props, approved_by, _),
2235
+ !,
2236
+ intent_ref(Props, Ref).
2237
+ human_intent(Props, firm, Ref) :-
2238
+ effective_firmness(Props, firm),
2239
+ !,
2240
+ intent_ref(Props, Ref).
2241
+
2242
+ effective_firmness(Props, Firmness) :-
2243
+ ( memberchk(firmness=Raw, Props),
2244
+ normalize_atom(Raw, Explicit),
2245
+ memberchk(Explicit, [firm, default, exploratory])
2246
+ -> Firmness = Explicit
2247
+ ; origin_field(Props, kind, agent)
2248
+ -> Firmness = default
2249
+ ; Firmness = firm
2250
+ ).
2251
+
2252
+ intent_ref(Props, Ref) :-
2253
+ ( origin_field(Props, ref, Ref0)
2254
+ -> Ref = Ref0
2255
+ ; prop_text(Props, approval_ref, Ref0)
2256
+ -> Ref = Ref0
2257
+ ; Ref = ''
2258
+ ).
2259
+
2260
+ prop_text(Props, Key, Text) :-
2261
+ Prop =.. [=, Key, Raw],
2262
+ memberchk(Prop, Props),
2263
+ normalize_atom(Raw, Text),
2264
+ Text \== ''.
2265
+
2266
+ origin_field(Props, Key, Value) :-
2267
+ memberchk(origin=Raw, Props),
2268
+ normalize_atom(Raw, Json),
2269
+ catch(atom_json_dict(Json, Origin, [value_string_as(atom)]), _, fail),
2270
+ get_dict(Key, Origin, Value0),
2271
+ normalize_atom(Value0, Value),
2272
+ Value \== ''.
1678
2273
 
1679
2274
  stage_dict(Stages, Stage) :-
1680
2275
  dict_pairs(Stages, _, Pairs),
@@ -1690,7 +2285,7 @@ proof_advisories(Stages, Advisories) :-
1690
2285
  % Every linked scenario is a mandatory proof obligation. Receipt-completeness
1691
2286
  % failures therefore remain blocking even when another scenario has a passing
1692
2287
  % receipt; unit/integration-only evidence stays nonblocking in the E2E lane.
1693
- % Invariant: proofStatus == proven implies proofGaps == [].
2288
+ % Invariant: proofStatus == tested implies proofGaps == [].
1694
2289
  proof_issues(Stages, Gaps, Advisories) :-
1695
2290
  findall(Gap,
1696
2291
  (gap_definition(Gap, _, _, _),
@@ -1769,7 +2364,7 @@ gap_definition(proof_contract_mismatch, 57, passing_e2e, "Run kibi prove so the
1769
2364
  gap_definition(proof_snapshot_unavailable, 58, passing_e2e, "Run coverage through a CLI or MCP runtime that exposes the deterministic workspace snapshot.").
1770
2365
  gap_definition(missing_executable_test_symbol, 60, executable_symbols, "Link executable test code to every qualifying E2E test with executable_for.").
1771
2366
  gap_definition(missing_production_symbol, 70, production_symbols, "Link at least one production symbol to the requirement with implements.").
1772
- gap_definition(missing_production_symbol_coverage, 71, production_symbols, "Link every runtime implementing production symbol to a qualifying E2E test with covered_by; type-shape symbols retain structural unit contracts instead.").
2367
+ gap_definition(missing_production_symbol_coverage, 71, production_symbols, "Link every runtime implementing production symbol to a qualifying E2E test with covered_by; type-shape symbols instead need a covered_by unit test with a current passing receipt.").
1773
2368
  gap_definition(missing_symbol_coordinates, 80, source_coordinates, "Refresh and persist exact coordinates for every proof-bearing symbol.").
1774
2369
  gap_definition(missing_requirement_source, 81, source_coordinates, "Bind the requirement to its current source document.").
1775
2370