kibi-core 0.23.0 → 0.26.1

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