kibi-core 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@
4
4
  :- module(requirement_proof, [
5
5
  requirement_proof_context/1,
6
6
  requirement_proof_context/4,
7
+ requirement_proof_context/6,
7
8
  requirement_proof/4
8
9
  ]).
9
10
 
@@ -19,12 +20,23 @@
19
20
  ]).
20
21
  :- use_module('logic_ir.pl', [logic_rule_safety/2]).
21
22
 
22
- proof_version('kibi.requirement-proof.v2').
23
+ proof_version('kibi.requirement-proof.v3').
23
24
 
24
25
  requirement_proof_context(Context) :-
25
26
  requirement_proof_context(unknown, '1970-01-01T00:00:00Z', 604800, Context).
26
27
 
27
28
  requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, Context) :-
29
+ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds,
30
+ strict_snapshot, _{}, Context).
31
+
32
+ % Per-contract receipt binding (W2): BindingMode is strict_snapshot (the
33
+ % default: a receipt is current only against the whole-workspace snapshot it
34
+ % was proven on) or per_contract (a receipt carrying a binding_hash is current
35
+ % while its contract + receipt-stripped test document are unchanged, via the
36
+ % TestBindings dict TestId -> BindingHash; receipts without a binding_hash
37
+ % still fall back to the snapshot match).
38
+ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds,
39
+ BindingMode, TestBindings, Context) :-
28
40
  check_domain_contradictions_and_witnesses(Contradictions, ContradictionWitnesses),
29
41
  check_rule_safety(UnsafeRules),
30
42
  check_rule_verifiability(UnverifiableRules),
@@ -32,38 +44,43 @@ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, Contex
32
44
  normalize_atom(CheckedAt, CheckedAtAtom),
33
45
  (catch(parse_time(CheckedAtAtom, CheckedAtStamp), _, fail) -> true ; CheckedAtStamp = -1),
34
46
  normalize_integer(MaxAgeSeconds, MaxAge),
47
+ normalize_atom(BindingMode, BindingModeAtom),
35
48
  Context = _{
36
49
  contradictions: Contradictions,
37
50
  contradictionWitnesses: ContradictionWitnesses,
38
51
  unsafeRules: UnsafeRules,
39
52
  unverifiableRules: UnverifiableRules,
40
- verificationSnapshot: Snapshot,
41
- verificationCheckedAt: CheckedAtAtom,
42
- verificationCheckedAtStamp: CheckedAtStamp,
43
- verificationMaxAgeSeconds: MaxAge
53
+ proofSnapshot: Snapshot,
54
+ proofCheckedAt: CheckedAtAtom,
55
+ proofCheckedAtStamp: CheckedAtStamp,
56
+ proofMaxAgeSeconds: MaxAge,
57
+ proofBindingMode: BindingModeAtom,
58
+ proofTestBindings: TestBindings
44
59
  }.
45
60
 
61
+ % implements REQ-kibi-proof-applicability-reason
62
+ % Exemption first: a current requirement explicitly marked proof_exempt with a
63
+ % reason is intentionally outside E2E-proof scope and must never fall through
64
+ % to the ladder (which would report misleading receipt gaps).
65
+ requirement_proof(ReqId, ReqProps, _Context, Proof) :-
66
+ kb:current_req(ReqId),
67
+ proof_exempt(ReqProps, Reason),
68
+ !,
69
+ exempt_requirement_proof(Reason, Proof).
46
70
  requirement_proof(ReqId, _ReqProps, _Context, Proof) :-
47
71
  \+ kb:current_req(ReqId),
48
72
  !,
49
- proof_version(Version),
50
- Proof = _{
51
- proofVersion: Version,
52
- proofStatus: not_applicable,
53
- proofGaps: [],
54
- proofAdvisories: [],
55
- proofRepairs: [],
56
- proofStages: _{applicability: _{status: not_applicable}}
57
- }.
73
+ applicability_exclusion_reason(ReqId, Reason),
74
+ exempt_requirement_proof(Reason, Proof).
58
75
  requirement_proof(ReqId, ReqProps, Context, Proof) :-
59
76
  semantic_inventory_stage(ReqProps, SemanticStage, Inventory),
60
77
  logic_grounding_stage(ReqId, ReqProps, Inventory, Context, LogicStage),
61
78
  contradiction_stage(ReqId, LogicStage.status, Context, ContradictionStage),
62
79
  scenario_stage(ReqId, ScenarioStage, ScenarioIds),
63
80
  scenario_test_stage(ScenarioIds, ScenarioTestStage, ScenarioTests),
64
- passing_e2e_stage(ScenarioTests, Context, PassingE2eStage, PassingE2eTests),
81
+ passing_e2e_stage(ScenarioTestStage, Context, PassingE2eStage, PassingE2eTests),
65
82
  executable_symbol_stage(PassingE2eTests, ExecutableStage, ExecutableSymbols),
66
- production_symbol_stage(ReqId, PassingE2eTests, ProductionStage, ProductionSymbols),
83
+ production_symbol_stage(ReqId, PassingE2eTests, ScenarioTests, PassingE2eStage.receiptEvidence, Context, ProductionStage, ProductionSymbols),
67
84
  source_coordinate_stage(ReqProps, ExecutableSymbols, ProductionSymbols, CoordinateStage),
68
85
  Stages = _{
69
86
  semanticInventory: SemanticStage,
@@ -86,9 +103,49 @@ requirement_proof(ReqId, ReqProps, Context, Proof) :-
86
103
  proofGaps: Gaps,
87
104
  proofAdvisories: Advisories,
88
105
  proofRepairs: Repairs,
89
- proofStages: Stages
106
+ proofStages: Stages,
107
+ testResolutions: ScenarioTestStage.resolutions
90
108
  }.
91
109
 
110
+ exempt_requirement_proof(Reason, Proof) :-
111
+ proof_version(Version),
112
+ Proof = _{
113
+ proofVersion: Version,
114
+ proofStatus: not_applicable,
115
+ proofGaps: [],
116
+ proofAdvisories: [],
117
+ proofRepairs: [],
118
+ proofStages: _{applicability: _{status: not_applicable, reason: Reason}}
119
+ }.
120
+
121
+ %% proof_exempt(+Props, -Reason)
122
+ % A current requirement is exempt from E2E-proof scope when its author set
123
+ % proof_exempt to a truthy value AND supplied a non-empty proof_exempt_reason.
124
+ % Without the reason the exemption does not apply, so an author cannot park a
125
+ % requirement silently; upsert validation enforces the same pairing.
126
+ proof_exempt(Props, Reason) :-
127
+ memberchk(proof_exempt=RawFlag, Props),
128
+ normalize_atom(RawFlag, Flag),
129
+ memberchk(Flag, [true, yes, on, '1']),
130
+ memberchk(proof_exempt_reason=RawReason, Props),
131
+ normalize_atom(RawReason, Reason),
132
+ Reason \= ''.
133
+
134
+ %% applicability_exclusion_reason(+ReqId, -Reason)
135
+ % Typed reason a requirement fails current_req/1: superseded lifecycle, or a
136
+ % status outside the canonical+legacy requirement vocabulary (e.g. the ADR
137
+ % vocabulary 'accepted').
138
+ applicability_exclusion_reason(ReqId, "requirement is superseded by a newer requirement") :-
139
+ kb_relationship(supersedes, _, ReqId),
140
+ !.
141
+ applicability_exclusion_reason(ReqId, Reason) :-
142
+ kb_entity(ReqId, req, Props),
143
+ memberchk(status=Status, Props),
144
+ normalize_term_atom(Status, StatusAtom),
145
+ format(atom(Reason), "status '~w' is not a current requirement status", [StatusAtom]),
146
+ !.
147
+ applicability_exclusion_reason(_ReqId, "requirement is not current").
148
+
92
149
  semantic_inventory_stage(Props, Stage, Entries) :-
93
150
  ( memberchk(semantic_inventory=RawInventory, Props),
94
151
  inventory_entries(RawInventory, Entries0),
@@ -435,31 +492,96 @@ violation_for_entity(Violations, EntityId) :-
435
492
 
436
493
  scenario_stage(ReqId, Stage, ScenarioIds) :-
437
494
  findall(ScenarioId,
438
- (kb_relationship(specified_by, ReqId, ScenarioId), kb_entity(ScenarioId, scenario, _)),
439
- ScenarioIds0),
440
- sort(ScenarioIds0, ScenarioIds),
441
- (ScenarioIds = [] -> Status = missing ; Status = passed),
495
+ kb_relationship(specified_by, ReqId, ScenarioId),
496
+ ScenarioTargets0),
497
+ sort(ScenarioTargets0, ScenarioTargets),
498
+ include(existing_scenario, ScenarioTargets, ScenarioIds),
499
+ findall(ScenarioId,
500
+ (member(ScenarioId, ScenarioTargets), \+ existing_scenario(ScenarioId)),
501
+ InvalidScenarioTargets),
502
+ ((ScenarioIds = [] ; InvalidScenarioTargets \= []) -> Status = missing ; Status = passed),
442
503
  maplist(entity_source_ref, ScenarioIds, Sources),
443
- Stage = _{status: Status, scenarios: ScenarioIds, sources: Sources}.
504
+ Stage = _{
505
+ status: Status,
506
+ scenarios: ScenarioIds,
507
+ scenarioTargets: ScenarioTargets,
508
+ invalidScenarioTargets: InvalidScenarioTargets,
509
+ sources: Sources
510
+ }.
511
+
512
+ existing_scenario(ScenarioId) :-
513
+ kb_entity(ScenarioId, scenario, _).
444
514
 
445
515
  scenario_test_stage(ScenarioIds, Stage, ScenarioTests) :-
516
+ maplist(scenario_test_obligation, ScenarioIds, Obligations),
446
517
  findall(TestId,
447
- (member(ScenarioId, ScenarioIds), scenario_test(ScenarioId, TestId)),
518
+ (member(Obligation, Obligations), member(TestId, Obligation.tests)),
448
519
  ScenarioTests0),
449
520
  sort(ScenarioTests0, ScenarioTests),
450
- (ScenarioTests = [] -> Status = missing ; Status = passed),
521
+ findall(TestId,
522
+ (member(Obligation, Obligations), member(TestId, Obligation.scenarioTestTargets)),
523
+ ScenarioTestTargets0),
524
+ sort(ScenarioTestTargets0, ScenarioTestTargets),
525
+ findall(TestId,
526
+ (member(Obligation, Obligations), member(TestId, Obligation.invalidScenarioTestTargets)),
527
+ InvalidScenarioTestTargets0),
528
+ sort(InvalidScenarioTestTargets0, InvalidScenarioTestTargets),
529
+ ( ScenarioIds == []
530
+ -> Status = missing
531
+ ; member(Obligation, Obligations), Obligation.status == missing
532
+ -> Status = missing
533
+ ; Status = passed
534
+ ),
451
535
  maplist(entity_source_ref, ScenarioTests, Sources),
452
- Stage = _{status: Status, tests: ScenarioTests, sources: Sources}.
536
+ maplist(test_resolution_record, ScenarioTests, Resolutions),
537
+ Stage = _{
538
+ status: Status,
539
+ scenarios: ScenarioIds,
540
+ tests: ScenarioTests,
541
+ scenarioTestTargets: ScenarioTestTargets,
542
+ invalidScenarioTestTargets: InvalidScenarioTestTargets,
543
+ sources: Sources,
544
+ obligations: Obligations,
545
+ resolutions: Resolutions
546
+ }.
453
547
 
454
- scenario_test(ScenarioId, TestId) :-
455
- kb_relationship(verified_by, ScenarioId, TestId),
548
+ scenario_test_obligation(ScenarioId, Obligation) :-
549
+ findall(TestId, declared_scenario_test_target(ScenarioId, TestId), TestTargets0),
550
+ sort(TestTargets0, TestTargets),
551
+ include(existing_test, TestTargets, TestIds),
552
+ findall(TestId,
553
+ (member(TestId, TestTargets), \+ existing_test(TestId)),
554
+ InvalidScenarioTestTargets),
555
+ maplist(entity_source_ref, TestIds, Sources),
556
+ ( (TestIds == [] ; InvalidScenarioTestTargets \= [])
557
+ -> Status = missing
558
+ ; Status = passed
559
+ ),
560
+ Obligation = _{
561
+ scenarioId: ScenarioId,
562
+ status: Status,
563
+ scenarioTestTargets: TestTargets,
564
+ invalidScenarioTestTargets: InvalidScenarioTestTargets,
565
+ tests: TestIds,
566
+ sources: Sources
567
+ }.
568
+
569
+ declared_scenario_test_target(ScenarioId, TestId) :-
570
+ kb_relationship(verified_by, ScenarioId, TestId).
571
+ declared_scenario_test_target(ScenarioId, TestId) :-
572
+ kb_relationship(validates, TestId, ScenarioId).
573
+
574
+ existing_test(TestId) :-
456
575
  kb_entity(TestId, test, _).
576
+
457
577
  scenario_test(ScenarioId, TestId) :-
458
- kb_relationship(validates, TestId, ScenarioId),
459
- kb_entity(TestId, test, _).
578
+ declared_scenario_test_target(ScenarioId, TestId),
579
+ existing_test(TestId).
460
580
 
461
- passing_e2e_stage(ScenarioTests, Context, Stage, PassingE2eTests) :-
462
- maplist(test_receipt_evidence(Context), ScenarioTests, Evidence),
581
+ passing_e2e_stage(ScenarioTestStage, Context, Stage, PassingE2eTests) :-
582
+ maplist(scenario_passing_e2e_obligation(Context), ScenarioTestStage.obligations, ScenarioObligations),
583
+ findall(Item, (member(Obligation, ScenarioObligations), member(Item, Obligation.evidence)), Evidence0),
584
+ unique_evidence_by_test(Evidence0, Evidence),
463
585
  evidence_tests_with_state(Evidence, passed, PassingE2eTests),
464
586
  evidence_tests_with_state(Evidence, missing, MissingReceiptTests),
465
587
  evidence_tests_with_state(Evidence, stale, StaleReceiptTests),
@@ -468,10 +590,11 @@ passing_e2e_stage(ScenarioTests, Context, Stage, PassingE2eTests) :-
468
590
  evidence_tests_with_state(Evidence, contract_mismatch, ContractMismatchReceiptTests),
469
591
  evidence_tests_with_state(Evidence, snapshot_unavailable, SnapshotUnavailableTests),
470
592
  evidence_tests_with_state(Evidence, not_end_to_end, NonEndToEndTests),
471
- passing_e2e_status(PassingE2eTests, InvalidReceiptTests, ContractMismatchReceiptTests, SnapshotUnavailableTests, Status),
593
+ passing_e2e_status(ScenarioObligations, Status),
472
594
  Stage = _{
473
595
  status: Status,
474
596
  tests: PassingE2eTests,
597
+ scenarioObligations: ScenarioObligations,
475
598
  receiptEvidence: Evidence,
476
599
  missingReceiptTests: MissingReceiptTests,
477
600
  staleReceiptTests: StaleReceiptTests,
@@ -480,16 +603,104 @@ passing_e2e_stage(ScenarioTests, Context, Stage, PassingE2eTests) :-
480
603
  contractMismatchReceiptTests: ContractMismatchReceiptTests,
481
604
  snapshotUnavailableTests: SnapshotUnavailableTests,
482
605
  nonEndToEndTests: NonEndToEndTests,
483
- currentCodeSnapshot: Context.verificationSnapshot,
484
- checkedAt: Context.verificationCheckedAt,
485
- maxAgeSeconds: Context.verificationMaxAgeSeconds
606
+ currentCodeSnapshot: Context.proofSnapshot,
607
+ checkedAt: Context.proofCheckedAt,
608
+ maxAgeSeconds: Context.proofMaxAgeSeconds
609
+ }.
610
+
611
+ unique_evidence_by_test(Evidence0, Evidence) :-
612
+ findall(TestId,
613
+ (member(Item, Evidence0), TestId = Item.testId),
614
+ TestIds0),
615
+ sort(TestIds0, TestIds),
616
+ maplist(first_evidence_for_test(Evidence0), TestIds, Evidence).
617
+
618
+ first_evidence_for_test(Evidence, TestId, Item) :-
619
+ member(Item, Evidence),
620
+ Item.testId == TestId,
621
+ !.
622
+
623
+ scenario_passing_e2e_obligation(Context, TestObligation, Obligation) :-
624
+ maplist(test_receipt_evidence(Context), TestObligation.tests, Evidence),
625
+ include(e2e_evidence, Evidence, E2eEvidence),
626
+ include(non_e2e_evidence, Evidence, NonEndToEndEvidence),
627
+ evidence_tests_with_state(E2eEvidence, passed, PassingE2eTests),
628
+ evidence_tests_with_state(E2eEvidence, missing, MissingReceiptTests),
629
+ evidence_tests_with_state(E2eEvidence, stale, StaleReceiptTests),
630
+ evidence_tests_with_state(E2eEvidence, failed, FailedReceiptTests),
631
+ evidence_tests_with_state(E2eEvidence, invalid, InvalidReceiptTests),
632
+ evidence_tests_with_state(E2eEvidence, contract_mismatch, ContractMismatchReceiptTests),
633
+ evidence_tests_with_state(E2eEvidence, snapshot_unavailable, SnapshotUnavailableTests),
634
+ evidence_tests_with_state(NonEndToEndEvidence, not_end_to_end, NonEndToEndTests),
635
+ scenario_obligation_gaps(TestObligation, E2eEvidence, Gaps),
636
+ scenario_obligation_status(TestObligation, E2eEvidence, Status),
637
+ findall(TestId,
638
+ (member(Item, E2eEvidence), TestId = Item.testId),
639
+ E2eTests0),
640
+ sort(E2eTests0, E2eTests),
641
+ Obligation = _{
642
+ scenarioId: TestObligation.scenarioId,
643
+ status: Status,
644
+ scenarioTestTargets: TestObligation.scenarioTestTargets,
645
+ invalidScenarioTestTargets: TestObligation.invalidScenarioTestTargets,
646
+ tests: TestObligation.tests,
647
+ e2eTests: E2eTests,
648
+ nonEndToEndTests: NonEndToEndTests,
649
+ evidence: Evidence,
650
+ gaps: Gaps,
651
+ passingE2eTests: PassingE2eTests,
652
+ missingReceiptTests: MissingReceiptTests,
653
+ staleReceiptTests: StaleReceiptTests,
654
+ failedReceiptTests: FailedReceiptTests,
655
+ invalidReceiptTests: InvalidReceiptTests,
656
+ contractMismatchReceiptTests: ContractMismatchReceiptTests,
657
+ snapshotUnavailableTests: SnapshotUnavailableTests
486
658
  }.
487
659
 
488
- passing_e2e_status(Passing, _, _, _, passed) :- Passing \= [], !.
489
- passing_e2e_status([], Invalid, _, _, unresolved) :- Invalid \= [], !.
490
- passing_e2e_status([], _, ContractMismatch, _, unresolved) :- ContractMismatch \= [], !.
491
- passing_e2e_status([], _, _, SnapshotUnavailable, unresolved) :- SnapshotUnavailable \= [], !.
492
- passing_e2e_status([], _, _, _, missing).
660
+ e2e_evidence(Item) :- Item.state \= not_end_to_end.
661
+ non_e2e_evidence(Item) :- Item.state == not_end_to_end.
662
+
663
+ scenario_obligation_gaps(TestObligation, E2eEvidence, Gaps) :-
664
+ findall(Gap, scenario_obligation_gap(TestObligation, E2eEvidence, Gap), Gaps0),
665
+ sort(Gaps0, Gaps).
666
+
667
+ scenario_obligation_gap(TestObligation, _E2eEvidence, missing_scenario_test) :-
668
+ TestObligation.status == missing.
669
+ scenario_obligation_gap(TestObligation, E2eEvidence, missing_passing_e2e) :-
670
+ TestObligation.status == passed,
671
+ E2eEvidence == [].
672
+ scenario_obligation_gap(_TestObligation, Evidence, missing_proof_receipt) :-
673
+ member(Item, Evidence), Item.state == missing.
674
+ scenario_obligation_gap(_TestObligation, Evidence, stale_proof_receipt) :-
675
+ member(Item, Evidence), Item.state == stale.
676
+ scenario_obligation_gap(_TestObligation, Evidence, failed_proof_receipt) :-
677
+ member(Item, Evidence), Item.state == failed.
678
+ scenario_obligation_gap(_TestObligation, Evidence, invalid_proof_receipt) :-
679
+ member(Item, Evidence), Item.state == invalid.
680
+ scenario_obligation_gap(_TestObligation, Evidence, proof_contract_mismatch) :-
681
+ member(Item, Evidence), Item.state == contract_mismatch.
682
+ scenario_obligation_gap(_TestObligation, Evidence, proof_snapshot_unavailable) :-
683
+ member(Item, Evidence), Item.state == snapshot_unavailable.
684
+
685
+ scenario_obligation_status(TestObligation, _E2eEvidence, missing) :-
686
+ TestObligation.status == missing,
687
+ !.
688
+ scenario_obligation_status(_TestObligation, [], missing) :-
689
+ !.
690
+ scenario_obligation_status(_TestObligation, Evidence, passed) :-
691
+ Evidence \= [],
692
+ forall(member(Item, Evidence), Item.state == passed),
693
+ !.
694
+ scenario_obligation_status(_TestObligation, _Evidence, unresolved).
695
+
696
+ passing_e2e_status([], missing) :- !.
697
+ passing_e2e_status(Obligations, passed) :-
698
+ forall(member(Obligation, Obligations), Obligation.status == passed),
699
+ !.
700
+ passing_e2e_status(Obligations, missing) :-
701
+ member(Obligation, Obligations), Obligation.status == missing,
702
+ !.
703
+ passing_e2e_status(_Obligations, unresolved).
493
704
 
494
705
  evidence_tests_with_state(Evidence, State, TestIds) :-
495
706
  findall(TestId,
@@ -502,9 +713,9 @@ test_receipt_evidence(Context, TestId, Evidence) :-
502
713
  test_scope(Props, Scope),
503
714
  ( Scope \= end_to_end
504
715
  -> Evidence = _{testId: TestId, state: not_end_to_end, scope: Scope}
505
- ; Context.verificationSnapshot == unknown
716
+ ; Context.proofSnapshot == unknown
506
717
  -> Evidence = _{testId: TestId, state: snapshot_unavailable, scope: Scope}
507
- ; verification_receipt_entries(Props, Receipts, ReceiptPropertyPresent),
718
+ ; proof_receipt_entries(Props, Receipts, ReceiptPropertyPresent),
508
719
  receipt_evidence_state(TestId, Scope, Props, Receipts, ReceiptPropertyPresent, Context, Evidence)
509
720
  ).
510
721
 
@@ -513,18 +724,18 @@ receipt_evidence_state(TestId, Scope, _Props, [], false, _Context,
513
724
  receipt_evidence_state(TestId, Scope, Props, Receipts, _Present, _Context,
514
725
  _{testId: TestId, state: invalid, scope: Scope, receiptCount: Count}) :-
515
726
  length(Receipts, Count),
516
- verification_contract_binding(Props, ContractBinding),
727
+ proof_contract_binding(Props, ContractBinding),
517
728
  (ContractBinding == invalid
518
729
  ; Receipts == []
519
730
  ; member(Receipt, Receipts), \+ valid_receipt_shape(TestId, Receipt)
520
731
  ; \+ chronological_receipt_history(Receipts)),
521
732
  !.
522
733
  receipt_evidence_state(TestId, Scope, Props, Receipts, _Present, Context, Evidence) :-
523
- include(receipt_for_snapshot(Context.verificationSnapshot), Receipts, SnapshotReceipts),
734
+ receipt_for_current_mode(TestId, Context, Receipts, SnapshotReceipts),
524
735
  ( SnapshotReceipts == []
525
736
  -> length(Receipts, Count),
526
737
  Evidence = _{testId: TestId, state: stale, scope: Scope, receiptCount: Count}
527
- ; verification_contract_binding(Props, ContractBinding),
738
+ ; proof_contract_binding(Props, ContractBinding),
528
739
  include(receipt_matches_current_binding(Scope, ContractBinding), SnapshotReceipts, CurrentReceipts),
529
740
  ( CurrentReceipts == []
530
741
  -> receipt_contract_mismatch_evidence(TestId, Scope, SnapshotReceipts, ContractBinding, Evidence)
@@ -534,8 +745,8 @@ receipt_evidence_state(TestId, Scope, Props, Receipts, _Present, Context, Eviden
534
745
  )
535
746
  ).
536
747
 
537
- verification_receipt_entries(Props, Receipts, Present) :-
538
- ( memberchk(verification_receipts=Raw, Props)
748
+ proof_receipt_entries(Props, Receipts, Present) :-
749
+ ( memberchk(proof_receipts=Raw, Props)
539
750
  -> Present = true,
540
751
  inventory_entries(Raw, Receipts)
541
752
  ; Present = false,
@@ -544,16 +755,11 @@ verification_receipt_entries(Props, Receipts, Present) :-
544
755
 
545
756
  valid_receipt_shape(TestId, Receipt) :-
546
757
  inventory_entry_field(Receipt, version, RawVersion),
547
- normalize_receipt_atom(RawVersion, Version),
548
- memberchk(Version, ['kibi.verification-receipt.v1', 'kibi.verification-receipt.v2']),
758
+ normalize_receipt_atom(RawVersion, 'kibi.proof-receipt.v1'),
549
759
  inventory_entry_field(Receipt, receipt_id, RawReceiptId),
550
760
  normalize_receipt_atom(RawReceiptId, ReceiptId), valid_receipt_id(ReceiptId),
551
761
  inventory_entry_field(Receipt, test_id, RawTestId),
552
762
  normalize_receipt_atom(RawTestId, TestId),
553
- inventory_entry_field(Receipt, runner, RawRunner),
554
- normalize_receipt_atom(RawRunner, Runner), Runner \= '',
555
- inventory_entry_field(Receipt, command, RawCommand),
556
- normalize_receipt_atom(RawCommand, Command), Command \= '',
557
763
  inventory_entry_field(Receipt, scope, RawScope),
558
764
  normalize_receipt_atom(RawScope, ReceiptScope),
559
765
  memberchk(ReceiptScope, [unit, integration, end_to_end]),
@@ -566,55 +772,83 @@ valid_receipt_shape(TestId, Receipt) :-
566
772
  normalize_receipt_atom(RawEnvironmentHash, EnvironmentHash), valid_sha256(EnvironmentHash),
567
773
  inventory_entry_field(Receipt, artifact_digest, RawArtifactDigest),
568
774
  normalize_receipt_atom(RawArtifactDigest, ArtifactDigest), valid_sha256(ArtifactDigest),
569
- receipt_timestamp(Receipt, started_at, StartedStamp),
570
- receipt_timestamp(Receipt, finished_at, FinishedStamp),
571
- FinishedStamp >= StartedStamp,
572
- valid_receipt_version_fields(Version, Receipt).
573
-
574
- valid_receipt_version_fields('kibi.verification-receipt.v1', _Receipt).
575
- valid_receipt_version_fields('kibi.verification-receipt.v2', Receipt) :-
775
+ inventory_entry_field(Receipt, contract_hash, RawContractHash),
776
+ normalize_receipt_atom(RawContractHash, ContractHash), valid_sha256(ContractHash),
777
+ inventory_entry_field(Receipt, fingerprint, RawFingerprint),
778
+ normalize_receipt_atom(RawFingerprint, Fingerprint), valid_sha256(Fingerprint),
779
+ valid_fingerprint_components(Receipt),
780
+ inventory_entry_field(Receipt, integration_id, RawIntegrationId),
781
+ normalize_receipt_atom(RawIntegrationId, IntegrationId), IntegrationId \= '',
782
+ inventory_entry_field(Receipt, producer, RawProducer),
783
+ receipt_producer_name(RawProducer, ProducerName), ProducerName \= '',
576
784
  inventory_entry_field(Receipt, command_argv, RawCommandArgv),
577
785
  is_list(RawCommandArgv),
578
786
  RawCommandArgv \= [],
579
787
  maplist(nonempty_receipt_atom, RawCommandArgv),
580
- inventory_entry_field(Receipt, contract_hash, RawContractHash),
581
- normalize_receipt_atom(RawContractHash, ContractHash),
582
- valid_sha256(ContractHash),
583
- inventory_entry_field(Receipt, case_results, RawCases),
584
- is_list(RawCases),
585
- RawCases \= [],
586
- maplist(valid_receipt_case, RawCases),
587
- findall(Key,
588
- (member(Case, RawCases), receipt_case_key(Case, Key)), Keys),
589
- sort(Keys, UniqueKeys),
590
- length(Keys, KeyCount),
591
- length(UniqueKeys, KeyCount).
788
+ ( inventory_entry_field(Receipt, binding_hash, RawBindingHash)
789
+ -> normalize_receipt_atom(RawBindingHash, BindingHash),
790
+ valid_sha256(BindingHash)
791
+ ; true
792
+ ),
793
+ inventory_entry_field(Receipt, run_outcome, RawRunOutcome),
794
+ normalize_receipt_atom(RawRunOutcome, RunOutcome),
795
+ memberchk(RunOutcome, [passed, failed, errored, cancelled, timed_out, interrupted, no_results]),
796
+ inventory_entry_field(Receipt, proof_results, RawResults),
797
+ is_list(RawResults),
798
+ maplist(valid_receipt_proof_result, RawResults),
799
+ receipt_timestamp(Receipt, started_at, StartedStamp),
800
+ receipt_timestamp(Receipt, finished_at, FinishedStamp),
801
+ FinishedStamp >= StartedStamp.
802
+
803
+ receipt_producer_name(Producer, Name) :-
804
+ inventory_entry_field(Producer, name, RawName),
805
+ normalize_receipt_atom(RawName, Name).
806
+
807
+ valid_fingerprint_components(Receipt) :-
808
+ inventory_entry_field(Receipt, fingerprint_components, Components),
809
+ forall(member(Key, [contract, integration, command, bindings, producer]),
810
+ (inventory_entry_field(Components, Key, Raw),
811
+ normalize_receipt_atom(Raw, Value),
812
+ valid_sha256(Value))),
813
+ !.
592
814
 
593
815
  nonempty_receipt_atom(Value) :-
594
816
  normalize_receipt_atom(Value, Atom),
595
817
  Atom \= ''.
596
818
 
597
- valid_receipt_case(Case) :-
598
- inventory_entry_field(Case, symbol_id, RawSymbolId),
819
+ valid_receipt_proof_result(Result) :-
820
+ inventory_entry_field(Result, symbol_id, RawSymbolId),
599
821
  nonempty_receipt_atom(RawSymbolId),
600
- inventory_entry_field(Case, project, RawProject),
601
- nonempty_receipt_atom(RawProject),
602
- inventory_entry_field(Case, outcome, RawOutcome),
603
- normalize_receipt_atom(RawOutcome, Outcome),
604
- memberchk(Outcome, [passed, failed, timed_out, skipped, interrupted]),
605
- inventory_entry_field(Case, retries, Retries),
606
- normalize_integer(Retries, RetryCount),
607
- RetryCount >= 0,
608
- inventory_entry_field(Case, duration_ms, Duration),
609
- normalize_integer(Duration, DurationMs),
610
- DurationMs >= 0.
611
-
612
- receipt_case_key(Case, Key) :-
613
- inventory_entry_field(Case, project, Project),
614
- inventory_entry_field(Case, symbol_id, SymbolId),
615
- normalize_receipt_atom(Project, ProjectAtom),
616
- normalize_receipt_atom(SymbolId, SymbolAtom),
617
- atomic_list_concat([ProjectAtom, SymbolAtom], '/', Key).
822
+ inventory_entry_field(Result, target, RawTarget),
823
+ nonempty_receipt_atom(RawTarget),
824
+ inventory_entry_field(Result, outcome, RawOutcome),
825
+ normalize_receipt_atom(RawOutcome, ResultOutcome),
826
+ memberchk(ResultOutcome, [passed, failed, timed_out, skipped, interrupted, errored]),
827
+ inventory_entry_field(Result, binding, RawBinding),
828
+ normalize_receipt_atom(RawBinding, Binding),
829
+ memberchk(Binding, [native_case, aggregate_run]),
830
+ valid_receipt_attempts(Result).
831
+
832
+ valid_receipt_attempts(Result) :-
833
+ inventory_entry_field(Result, attempts, Attempts),
834
+ inventory_entry_field(Attempts, status, RawStatus),
835
+ normalize_receipt_atom(RawStatus, Status),
836
+ valid_attempt_status(Status, Attempts).
837
+
838
+ valid_attempt_status(unavailable, Attempts) :-
839
+ \+ inventory_entry_field(Attempts, entries, _Present), !.
840
+ valid_attempt_status(unavailable, Attempts) :-
841
+ inventory_entry_field(Attempts, entries, Entries),
842
+ Entries == [], !.
843
+ valid_attempt_status(complete, Attempts) :-
844
+ inventory_entry_field(Attempts, entries, Entries),
845
+ is_list(Entries),
846
+ Entries \= [],
847
+ forall(member(Entry, Entries),
848
+ (inventory_entry_field(Entry, outcome, RawEntryOutcome),
849
+ normalize_receipt_atom(RawEntryOutcome, EntryOutcome),
850
+ memberchk(EntryOutcome, [passed, failed, timed_out, skipped, interrupted, errored]))),
851
+ !.
618
852
 
619
853
  valid_sha256(Value) :-
620
854
  atom_length(Value, 64),
@@ -622,7 +856,7 @@ valid_sha256(Value) :-
622
856
  forall(member(Char, Chars), memberchk(Char, ['0','1','2','3','4','5','6','7','8','9',a,b,c,d,e,f])).
623
857
 
624
858
  valid_receipt_id(Value) :-
625
- re_match('^VR-[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$', Value).
859
+ re_match('^PR-[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$', Value).
626
860
 
627
861
  receipt_timestamp(Receipt, Field, Stamp) :-
628
862
  inventory_entry_field(Receipt, Field, RawValue),
@@ -647,6 +881,29 @@ receipt_for_snapshot(Snapshot, Receipt) :-
647
881
  inventory_entry_field(Receipt, code_snapshot, RawSnapshot),
648
882
  normalize_receipt_atom(RawSnapshot, Snapshot).
649
883
 
884
+ % Per-contract binding selector (W2). In per_contract mode a receipt carrying
885
+ % a binding_hash is current when it matches the test's current binding hash;
886
+ % receipts without one keep the strict snapshot semantics so older evidence
887
+ % stays valid until each contract is re-proven.
888
+ receipt_for_current_mode(TestId, Context, Receipts, SnapshotReceipts) :-
889
+ ( Context.proofBindingMode == per_contract,
890
+ get_dict(TestId, Context.proofTestBindings, CurrentBinding)
891
+ -> include(receipt_with_binding_hash, Receipts, BoundReceipts),
892
+ include(receipt_for_binding(CurrentBinding), BoundReceipts, SnapshotReceipts),
893
+ ( SnapshotReceipts == []
894
+ -> include(receipt_for_snapshot(Context.proofSnapshot), Receipts, SnapshotReceipts)
895
+ ; true
896
+ )
897
+ ; include(receipt_for_snapshot(Context.proofSnapshot), Receipts, SnapshotReceipts)
898
+ ).
899
+
900
+ receipt_with_binding_hash(Receipt) :-
901
+ inventory_entry_field(Receipt, binding_hash, _).
902
+
903
+ receipt_for_binding(Binding, Receipt) :-
904
+ inventory_entry_field(Receipt, binding_hash, RawBinding),
905
+ normalize_receipt_atom(RawBinding, Binding).
906
+
650
907
  receipt_matches_current_binding(Scope, ContractBinding, Receipt) :-
651
908
  inventory_entry_field(Receipt, scope, RawScope),
652
909
  normalize_receipt_atom(RawScope, Scope),
@@ -655,7 +912,7 @@ receipt_matches_current_binding(Scope, ContractBinding, Receipt) :-
655
912
  receipt_matches_current_contract(none, _Receipt).
656
913
  receipt_matches_current_contract(hash(ExpectedHash), Receipt) :-
657
914
  inventory_entry_field(Receipt, version, RawVersion),
658
- normalize_receipt_atom(RawVersion, 'kibi.verification-receipt.v2'),
915
+ normalize_receipt_atom(RawVersion, 'kibi.proof-receipt.v1'),
659
916
  inventory_entry_field(Receipt, contract_hash, RawContractHash),
660
917
  normalize_receipt_atom(RawContractHash, ExpectedHash).
661
918
 
@@ -680,26 +937,26 @@ receipt_contract_mismatch_evidence(TestId, Scope, Receipts, ContractBinding, Evi
680
937
  current_contract_hash_value(hash(Hash), Hash).
681
938
  current_contract_hash_value(none, none).
682
939
 
683
- verification_contract_binding(Props, Binding) :-
684
- ( memberchk(verification_contract=RawContract, Props)
685
- -> (verification_contract_hash(RawContract, Hash) -> Binding = hash(Hash) ; Binding = invalid)
940
+ proof_contract_binding(Props, Binding) :-
941
+ ( memberchk(proof_contract=RawContract, Props)
942
+ -> (proof_contract_hash(RawContract, Hash) -> Binding = hash(Hash) ; Binding = invalid)
686
943
  ; Binding = none
687
944
  ).
688
945
 
689
- verification_contract_hash(RawContract, Hash) :-
690
- verification_contract_dict(RawContract, Contract),
946
+ proof_contract_hash(RawContract, Hash) :-
947
+ proof_contract_dict(RawContract, Contract),
691
948
  canonical_json_value(Contract, Canonical),
692
949
  crypto_data_hash(Canonical, Hash, [algorithm(sha256), encoding(utf8)]).
693
950
 
694
- verification_contract_dict(^^(Value, _), Contract) :- !,
695
- verification_contract_dict(Value, Contract).
696
- verification_contract_dict(literal(type(_, Value)), Contract) :- !,
697
- verification_contract_dict(Value, Contract).
698
- verification_contract_dict(literal(Value), Contract) :- !,
699
- verification_contract_dict(Value, Contract).
700
- verification_contract_dict(Contract, Contract) :-
951
+ proof_contract_dict(^^(Value, _), Contract) :- !,
952
+ proof_contract_dict(Value, Contract).
953
+ proof_contract_dict(literal(type(_, Value)), Contract) :- !,
954
+ proof_contract_dict(Value, Contract).
955
+ proof_contract_dict(literal(Value), Contract) :- !,
956
+ proof_contract_dict(Value, Contract).
957
+ proof_contract_dict(Contract, Contract) :-
701
958
  is_dict(Contract), !.
702
- verification_contract_dict(Raw, Contract) :-
959
+ proof_contract_dict(Raw, Contract) :-
703
960
  (atom(Raw) ; string(Raw)),
704
961
  catch(atom_json_dict(Raw, Contract, [value_string_as(string)]), _, fail),
705
962
  is_dict(Contract).
@@ -731,23 +988,21 @@ latest_receipt(Receipts, Latest, FinishedStamp) :-
731
988
  last(Sorted, FinishedStamp-Latest).
732
989
 
733
990
  receipt_runtime_state(_Receipt, FinishedStamp, Context, invalid, AgeSeconds) :-
734
- AgeSeconds is Context.verificationCheckedAtStamp - FinishedStamp,
991
+ AgeSeconds is Context.proofCheckedAtStamp - FinishedStamp,
735
992
  AgeSeconds < -300,
736
993
  !.
737
994
  receipt_runtime_state(_Receipt, FinishedStamp, Context, stale, AgeSeconds) :-
738
- AgeSeconds is Context.verificationCheckedAtStamp - FinishedStamp,
739
- AgeSeconds > Context.verificationMaxAgeSeconds,
995
+ AgeSeconds is Context.proofCheckedAtStamp - FinishedStamp,
996
+ AgeSeconds > Context.proofMaxAgeSeconds,
740
997
  !.
741
998
  receipt_runtime_state(Receipt, FinishedStamp, Context, State, AgeSeconds) :-
742
- AgeSeconds is Context.verificationCheckedAtStamp - FinishedStamp,
999
+ AgeSeconds is Context.proofCheckedAtStamp - FinishedStamp,
743
1000
  inventory_entry_field(Receipt, outcome, RawOutcome),
744
1001
  normalize_receipt_atom(RawOutcome, Outcome),
745
1002
  (Outcome == passed -> State = passed ; State = failed).
746
1003
 
747
1004
  receipt_evidence_dict(TestId, Scope, Receipt, State, AgeSeconds, Evidence) :-
748
1005
  receipt_string_field(Receipt, receipt_id, ReceiptId),
749
- receipt_string_field(Receipt, runner, Runner),
750
- receipt_string_field(Receipt, command, Command),
751
1006
  receipt_string_field(Receipt, outcome, Outcome),
752
1007
  receipt_string_field(Receipt, code_snapshot, CodeSnapshot),
753
1008
  receipt_string_field(Receipt, environment_hash, EnvironmentHash),
@@ -759,8 +1014,6 @@ receipt_evidence_dict(TestId, Scope, Receipt, State, AgeSeconds, Evidence) :-
759
1014
  state: State,
760
1015
  scope: Scope,
761
1016
  receiptId: ReceiptId,
762
- runner: Runner,
763
- command: Command,
764
1017
  outcome: Outcome,
765
1018
  codeSnapshot: CodeSnapshot,
766
1019
  environmentHash: EnvironmentHash,
@@ -819,6 +1072,14 @@ test_missing_executable_symbol(TestId) :-
819
1072
  \+ (kb_relationship(executable_for, SymbolId, TestId), kb_entity(SymbolId, symbol, _)).
820
1073
 
821
1074
  production_symbol_stage(ReqId, PassingE2eTests, Stage, ProductionSymbols) :-
1075
+ findall(TestId,
1076
+ (kb_relationship(specified_by, ReqId, ScenarioId),
1077
+ scenario_test(ScenarioId, TestId)),
1078
+ ScenarioTests0),
1079
+ sort(ScenarioTests0, ScenarioTests),
1080
+ production_symbol_stage(ReqId, PassingE2eTests, ScenarioTests, [], _{}, Stage, ProductionSymbols).
1081
+
1082
+ production_symbol_stage(ReqId, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, Stage, ProductionSymbols) :-
822
1083
  findall(SymbolId,
823
1084
  (kb_relationship(implements, SymbolId, ReqId),
824
1085
  kb_entity(SymbolId, symbol, _),
@@ -828,15 +1089,23 @@ production_symbol_stage(ReqId, PassingE2eTests, Stage, ProductionSymbols) :-
828
1089
  include(type_shape_symbol_with_structural_contract, ImplementingSymbols, StructuralSymbols),
829
1090
  exclude(type_shape_symbol_with_structural_contract, ImplementingSymbols, ProductionSymbols),
830
1091
  include(symbol_not_covered_by_tests(PassingE2eTests), ProductionSymbols, UncoveredSymbols),
831
- production_stage_status(ProductionSymbols, StructuralSymbols, PassingE2eTests, UncoveredSymbols, Status),
1092
+ production_stage_status(ProductionSymbols, StructuralSymbols, PassingE2eTests, UncoveredSymbols, Status, StatusReason),
832
1093
  maplist(symbol_coordinate_ref, ProductionSymbols, Coordinates),
833
- Stage = _{
1094
+ production_coverage_explanations(
1095
+ ReqId, ProductionSymbols, StructuralSymbols, UncoveredSymbols,
1096
+ PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, Explanations),
1097
+ StageBase = _{
834
1098
  status: Status,
835
1099
  symbols: ProductionSymbols,
836
1100
  structuralSymbols: StructuralSymbols,
837
1101
  uncoveredSymbols: UncoveredSymbols,
838
- coordinates: Coordinates
839
- }.
1102
+ coordinates: Coordinates,
1103
+ explanations: Explanations
1104
+ },
1105
+ ( Status == passed
1106
+ -> Stage = StageBase
1107
+ ; put_dict(reason, StageBase, StatusReason, Stage)
1108
+ ).
840
1109
 
841
1110
  type_shape_symbol_with_structural_contract(SymbolId) :-
842
1111
  kb_entity(SymbolId, symbol, SymbolProps),
@@ -849,14 +1118,295 @@ type_shape_symbol_with_structural_contract(SymbolId) :-
849
1118
  normalize_atom(RawStatus, Status),
850
1119
  memberchk(Status, [active, passing]).
851
1120
 
852
- production_stage_status([], [], _, _, missing) :- !.
853
- production_stage_status(_, _, [], _, blocked) :- !.
854
- production_stage_status(_, _, _, [], passed) :- !.
855
- production_stage_status(_, _, _, _, missing).
1121
+ % Stage status plus a typed reason, from a single total clause so status and
1122
+ % reason can never disagree (the Align stale-receipts case reported `blocked`
1123
+ % with no indication that fresh receipts were the unblock). Clause order keeps
1124
+ % the historical precedence: no production symbols at all is `missing` even
1125
+ % when E2E evidence is also absent; only then does absent E2E evidence read
1126
+ % as `blocked`.
1127
+ production_stage_status(ProductionSymbols, StructuralSymbols, PassingE2eTests, UncoveredSymbols, Status, Reason) :-
1128
+ ( ProductionSymbols == [],
1129
+ StructuralSymbols == []
1130
+ -> Status = missing,
1131
+ Reason = "no production symbols implement this requirement; link at least one with implements"
1132
+ ; PassingE2eTests == []
1133
+ -> Status = blocked,
1134
+ Reason = "no passing E2E test evidence is available for the current snapshot; run kibi prove so this stage can evaluate production symbol coverage"
1135
+ ; UncoveredSymbols == []
1136
+ -> Status = passed
1137
+ ; Status = missing,
1138
+ length(UncoveredSymbols, Count),
1139
+ format(atom(Reason), "~w production symbol(s) lack covered_by links to the passing E2E tests", [Count])
1140
+ ).
856
1141
 
857
1142
  symbol_not_covered_by_tests(Tests, SymbolId) :-
858
1143
  \+ (member(TestId, Tests), kb_relationship(covered_by, SymbolId, TestId)).
859
1144
 
1145
+ production_coverage_explanations(ReqId, ProductionSymbols, StructuralSymbols, UncoveredSymbols,
1146
+ PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, Explanations) :-
1147
+ maplist(production_symbol_explanation(ReqId, UncoveredSymbols, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context),
1148
+ ProductionSymbols, ProductionExplanations),
1149
+ maplist(structural_symbol_explanation(ReqId, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context),
1150
+ StructuralSymbols, StructuralExplanations),
1151
+ append(ProductionExplanations, StructuralExplanations, Explanations).
1152
+
1153
+ structural_symbol_explanation(ReqId, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, SymbolId, Explanation) :-
1154
+ symbol_classification(SymbolId, Classification),
1155
+ symbol_coverage_candidates(structural, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, SymbolId, Candidates),
1156
+ explanation_reason_text(structural_unit_contract, ReasonText),
1157
+ Explanation = _{
1158
+ symbolId: SymbolId,
1159
+ requirementId: ReqId,
1160
+ classification: Classification,
1161
+ status: covered,
1162
+ reason: structural_unit_contract,
1163
+ reasonText: ReasonText,
1164
+ coverageCandidates: Candidates
1165
+ }.
1166
+
1167
+ production_symbol_explanation(ReqId, UncoveredSymbols, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, SymbolId, Explanation) :-
1168
+ symbol_classification(SymbolId, Classification),
1169
+ symbol_coverage_candidates(behavioral, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, SymbolId, Candidates),
1170
+ production_symbol_rollup(UncoveredSymbols, PassingE2eTests, SymbolId, Candidates, Status, Reason),
1171
+ explanation_reason_text(Reason, ReasonText),
1172
+ Explanation = _{
1173
+ symbolId: SymbolId,
1174
+ requirementId: ReqId,
1175
+ classification: Classification,
1176
+ status: Status,
1177
+ reason: Reason,
1178
+ reasonText: ReasonText,
1179
+ coverageCandidates: Candidates
1180
+ }.
1181
+
1182
+ production_symbol_rollup(_Uncovered, [], _SymbolId, _Candidates, uncovered, stage_blocked_no_passing_e2e) :-
1183
+ !.
1184
+ production_symbol_rollup(UncoveredSymbols, _Passing, SymbolId, Candidates, uncovered, Reason) :-
1185
+ memberchk(SymbolId, UncoveredSymbols),
1186
+ !,
1187
+ (Candidates == [] -> Reason = covered_by_missing ; Reason = no_qualifying_e2e_coverage).
1188
+ production_symbol_rollup(_Uncovered, _Passing, _SymbolId, _Candidates, covered, covered).
1189
+
1190
+ symbol_classification(SymbolId, Classification) :-
1191
+ kb_entity(SymbolId, symbol, Props),
1192
+ memberchk(symbol_role=RawRole, Props),
1193
+ normalize_atom(RawRole, Role),
1194
+ Role \= '',
1195
+ !,
1196
+ Classification = Role.
1197
+ symbol_classification(_SymbolId, unknown).
1198
+
1199
+ symbol_coverage_candidates(Mode, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, SymbolId, Candidates) :-
1200
+ findall(TestId, kb_relationship(covered_by, SymbolId, TestId), TestIds0),
1201
+ sort(TestIds0, TestIds),
1202
+ maplist(coverage_candidate_dict(Mode, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context), TestIds, Candidates).
1203
+
1204
+ coverage_candidate_dict(Mode, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, TestId, Candidate) :-
1205
+ coverage_candidate_qualification(Mode, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, TestId, Qualifies, Primary, Secondaries),
1206
+ explanation_reason_text(Primary, ReasonText),
1207
+ cheap_candidate_scope(TestId, Scope),
1208
+ reused_receipt_state(ReceiptEvidence, TestId, ReceiptState),
1209
+ CandidateBase = _{
1210
+ testId: TestId,
1211
+ relationship: covered_by,
1212
+ qualifies: Qualifies,
1213
+ reason: Primary,
1214
+ reasonText: ReasonText,
1215
+ secondaryReasons: Secondaries,
1216
+ scope: Scope
1217
+ },
1218
+ ( ReceiptState == none
1219
+ -> Candidate = CandidateBase
1220
+ ; put_dict(receiptState, CandidateBase, ReceiptState, Candidate)
1221
+ ).
1222
+
1223
+ % Shared candidate evaluator. Mode selects the coverage contract:
1224
+ % behavioral — production symbols require passing scenario-backed E2E
1225
+ % structural — type-shape symbols may also qualify via a unit contract
1226
+ coverage_candidate_qualification(_Mode, PassingE2eTests, _ScenarioTests, _ReceiptEvidence, _Context, TestId, true, covered, []) :-
1227
+ memberchk(TestId, PassingE2eTests),
1228
+ !.
1229
+ coverage_candidate_qualification(_Mode, _Passing, _Scenario, _Evidence, _Context, TestId, false, missing_test_entity, []) :-
1230
+ \+ kb_entity(TestId, test, _),
1231
+ !.
1232
+ coverage_candidate_qualification(structural, _Passing, _Scenario, _Evidence, _Context, TestId, true, structural_unit_contract, []) :-
1233
+ structural_unit_contract_test(TestId),
1234
+ !.
1235
+ coverage_candidate_qualification(_Mode, _Passing, ScenarioTests, ReceiptEvidence, Context, TestId, false, Primary, Secondaries) :-
1236
+ cheap_scope_reject_reason(TestId, ScopeReason),
1237
+ ( \+ memberchk(TestId, ScenarioTests)
1238
+ -> Primary = test_not_in_requirement_scenario_chain,
1239
+ maybe_singleton(ScopeReason, ScopeSecondaries),
1240
+ reused_receipt_reject_reasons(ReceiptEvidence, TestId, ReceiptSecondaries),
1241
+ append(ScopeSecondaries, ReceiptSecondaries, Secondaries)
1242
+ ; ScopeReason \= none
1243
+ -> Primary = ScopeReason,
1244
+ reused_receipt_reject_reasons(ReceiptEvidence, TestId, Secondaries)
1245
+ ; receipt_reject_for_in_chain(ReceiptEvidence, Context, TestId, Primary, Secondaries)
1246
+ ).
1247
+
1248
+ structural_unit_contract_test(TestId) :-
1249
+ kb_entity(TestId, test, TestProps),
1250
+ test_scope(TestProps, unit),
1251
+ memberchk(status=RawStatus, TestProps),
1252
+ normalize_atom(RawStatus, Status),
1253
+ memberchk(Status, [active, passing]).
1254
+
1255
+ cheap_candidate_scope(TestId, Scope) :-
1256
+ kb_entity(TestId, test, Props),
1257
+ !,
1258
+ test_scope(Props, Scope).
1259
+ cheap_candidate_scope(_TestId, unknown).
1260
+
1261
+ cheap_scope_reject_reason(TestId, Reason) :-
1262
+ cheap_candidate_scope(TestId, Scope),
1263
+ scope_reject_reason(Scope, Reason).
1264
+
1265
+ scope_reject_reason(unit, test_scope_is_unit) :- !.
1266
+ scope_reject_reason(end_to_end, none) :- !.
1267
+ scope_reject_reason(_Scope, test_scope_is_non_e2e).
1268
+
1269
+ maybe_singleton(none, []) :- !.
1270
+ maybe_singleton(Reason, [Reason]).
1271
+
1272
+ reused_receipt_state(Evidence, TestId, State) :-
1273
+ member(Item, Evidence),
1274
+ Item.testId == TestId,
1275
+ !,
1276
+ State = Item.state.
1277
+ reused_receipt_state(_Evidence, _TestId, none).
1278
+
1279
+ reused_receipt_item(Evidence, TestId, Item) :-
1280
+ member(Item, Evidence),
1281
+ Item.testId == TestId,
1282
+ !.
1283
+
1284
+ reused_receipt_reject_reasons(Evidence, TestId, Reasons) :-
1285
+ reused_receipt_item(Evidence, TestId, Item),
1286
+ !,
1287
+ receipt_item_reject_reason(Item, Reason),
1288
+ maybe_singleton(Reason, Reasons).
1289
+ reused_receipt_reject_reasons(_Evidence, _TestId, []).
1290
+
1291
+ receipt_reject_for_in_chain(Evidence, _Context, TestId, Primary, []) :-
1292
+ reused_receipt_item(Evidence, TestId, Item),
1293
+ !,
1294
+ receipt_item_reject_reason(Item, Reason),
1295
+ (Reason == none -> Primary = no_qualifying_e2e_coverage ; Primary = Reason).
1296
+ receipt_reject_for_in_chain(_Evidence, Context, TestId, Primary, []) :-
1297
+ is_dict(Context),
1298
+ Context \= _{},
1299
+ test_receipt_evidence(Context, TestId, Evidence),
1300
+ !,
1301
+ receipt_item_reject_reason(Evidence, Reason),
1302
+ (Reason == none -> Primary = no_qualifying_e2e_coverage ; Primary = Reason).
1303
+ receipt_reject_for_in_chain(_Evidence, _Context, _TestId, no_qualifying_e2e_coverage, []).
1304
+
1305
+ receipt_item_reject_reason(Item, stale_proof_receipt) :-
1306
+ Item.state == stale,
1307
+ get_dict(codeSnapshot, Item, _),
1308
+ !.
1309
+ receipt_item_reject_reason(Item, receipt_snapshot_mismatch) :-
1310
+ Item.state == stale,
1311
+ !.
1312
+ receipt_item_reject_reason(Item, Reason) :-
1313
+ receipt_state_reject_reason(Item.state, Reason).
1314
+
1315
+ receipt_state_reject_reason(missing, missing_proof_receipt) :- !.
1316
+ receipt_state_reject_reason(snapshot_unavailable, proof_snapshot_unavailable) :- !.
1317
+ receipt_state_reject_reason(contract_mismatch, receipt_contract_mismatch) :- !.
1318
+ receipt_state_reject_reason(invalid, invalid_proof_receipt) :- !.
1319
+ receipt_state_reject_reason(failed, failed_proof_receipt) :- !.
1320
+ receipt_state_reject_reason(not_end_to_end, none) :- !.
1321
+ receipt_state_reject_reason(stale, receipt_snapshot_mismatch) :- !.
1322
+ receipt_state_reject_reason(passed, none) :- !.
1323
+ receipt_state_reject_reason(none, none) :- !.
1324
+ receipt_state_reject_reason(_, none).
1325
+
1326
+ explanation_reason_text(Reason, Text) :-
1327
+ explanation_reason_text_def(Reason, Text),
1328
+ !.
1329
+ explanation_reason_text(Reason, Reason) :-
1330
+ atom(Reason).
1331
+
1332
+ explanation_reason_text_def(covered, "Qualifying covered_by to a passing scenario-backed E2E test").
1333
+ explanation_reason_text_def(covered_by_missing, "No covered_by relationship to any test").
1334
+ explanation_reason_text_def(no_qualifying_e2e_coverage, "covered_by relationships exist but none qualify as passing scenario-backed E2E coverage").
1335
+ explanation_reason_text_def(structural_unit_contract, "type-shape symbol is satisfied by a unit structural contract").
1336
+ explanation_reason_text_def(executable_test, "Symbol is executable test code linked with executable_for").
1337
+ 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").
1338
+ explanation_reason_text_def(missing_test_entity, "covered_by target is not a test entity").
1339
+ explanation_reason_text_def(test_not_in_requirement_scenario_chain, "Test is not in this requirement's scenario chain").
1340
+ explanation_reason_text_def(test_scope_is_unit, "Test verification_scope is unit, which cannot cover behavioral production symbols").
1341
+ explanation_reason_text_def(test_scope_is_non_e2e, "Test verification_scope is not end_to_end").
1342
+ explanation_reason_text_def(proof_snapshot_unavailable, "Workspace snapshot is unavailable so receipt currency cannot be proven").
1343
+ explanation_reason_text_def(missing_proof_receipt, "Scenario-backed E2E test has no kibi.proof-receipt.v1").
1344
+ explanation_reason_text_def(receipt_snapshot_mismatch, "Receipts exist but none match the current snapshot or binding").
1345
+ explanation_reason_text_def(stale_proof_receipt, "Current-snapshot receipt is older than the allowed proof age").
1346
+ explanation_reason_text_def(receipt_contract_mismatch, "Receipt does not match the current proof contract hash or fingerprint").
1347
+ explanation_reason_text_def(invalid_proof_receipt, "Receipt evidence is malformed or otherwise uncheckable").
1348
+ explanation_reason_text_def(failed_proof_receipt, "Latest current receipt outcome is not passed").
1349
+
1350
+ test_resolution_record(TestId, _{
1351
+ testId: TestId,
1352
+ requiredProofs: RequiredProofs,
1353
+ executableFor: ExecutableFor
1354
+ }) :-
1355
+ test_required_proof_refs(TestId, RequiredProofs),
1356
+ test_executable_for_refs(TestId, ExecutableFor).
1357
+
1358
+ test_required_proof_refs(TestId, Refs) :-
1359
+ kb_entity(TestId, test, Props),
1360
+ memberchk(proof_contract=RawContract, Props),
1361
+ proof_contract_dict(RawContract, Contract),
1362
+ contract_required_symbol_ids(Contract, SymbolIds),
1363
+ !,
1364
+ maplist(symbol_resolution_ref, SymbolIds, Refs).
1365
+ test_required_proof_refs(_TestId, []).
1366
+
1367
+ contract_required_symbol_ids(Contract, SymbolIds) :-
1368
+ ( get_dict(required_proofs, Contract, Raw)
1369
+ ; get_dict('required_proofs', Contract, Raw)
1370
+ ),
1371
+ !,
1372
+ inventory_entries(Raw, Entries),
1373
+ findall(SymbolId,
1374
+ (member(Entry, Entries), required_proof_entry_symbol(Entry, SymbolId)),
1375
+ SymbolIds0),
1376
+ sort(SymbolIds0, SymbolIds).
1377
+ contract_required_symbol_ids(_, []).
1378
+
1379
+ required_proof_entry_symbol(Entry, SymbolId) :-
1380
+ ( inventory_entry_field(Entry, symbol_id, Raw)
1381
+ ; inventory_entry_field(Entry, symbolId, Raw)
1382
+ ),
1383
+ normalize_atom(Raw, SymbolId),
1384
+ SymbolId \= ''.
1385
+
1386
+ test_executable_for_refs(TestId, Refs) :-
1387
+ findall(SymbolId,
1388
+ (kb_relationship(executable_for, SymbolId, TestId), kb_entity(SymbolId, symbol, _)),
1389
+ SymbolIds0),
1390
+ sort(SymbolIds0, SymbolIds),
1391
+ maplist(symbol_resolution_ref, SymbolIds, Refs).
1392
+
1393
+ symbol_resolution_ref(SymbolId, Ref) :-
1394
+ kb_entity(SymbolId, symbol, _),
1395
+ !,
1396
+ symbol_coordinate_ref(SymbolId, Coord),
1397
+ symbol_classification(SymbolId, Role),
1398
+ put_dict(_{role: Role, exists: true}, Coord, Ref).
1399
+ symbol_resolution_ref(SymbolId, _{
1400
+ id: SymbolId,
1401
+ sourceFile: '',
1402
+ sourceLine: 0,
1403
+ sourceColumn: 0,
1404
+ sourceEndLine: 0,
1405
+ sourceEndColumn: 0,
1406
+ role: unresolved,
1407
+ exists: false
1408
+ }).
1409
+
860
1410
  source_coordinate_stage(ReqProps, ExecutableSymbols, ProductionSymbols, Stage) :-
861
1411
  append(ExecutableSymbols, ProductionSymbols, Symbols0),
862
1412
  sort(Symbols0, Symbols),
@@ -988,8 +1538,9 @@ proof_advisories(Stages, Advisories) :-
988
1538
  proof_issues(Stages, _, Advisories).
989
1539
 
990
1540
  % implements REQ-kibi-conservative-requirement-proof
991
- % proofGaps are blocking only. Receipt-completeness codes become
992
- % proofAdvisories when passingE2e already supplies strict proof.
1541
+ % Every linked scenario is a mandatory proof obligation. Receipt-completeness
1542
+ % failures therefore remain blocking even when another scenario has a passing
1543
+ % receipt; unit/integration-only evidence stays nonblocking in the E2E lane.
993
1544
  % Invariant: proofStatus == proven implies proofGaps == [].
994
1545
  proof_issues(Stages, Gaps, Advisories) :-
995
1546
  findall(Gap,
@@ -1003,15 +1554,14 @@ proof_issues(Stages, Gaps, Advisories) :-
1003
1554
  proof_issue_advisory(Gap, Stages)),
1004
1555
  Advisories).
1005
1556
 
1006
- receipt_completeness_issue(missing_verification_receipt).
1007
- receipt_completeness_issue(stale_verification_receipt).
1008
- receipt_completeness_issue(failed_verification_receipt).
1009
- receipt_completeness_issue(invalid_verification_receipt).
1010
- receipt_completeness_issue(verification_contract_mismatch).
1557
+ receipt_completeness_issue(missing_proof_receipt).
1558
+ receipt_completeness_issue(stale_proof_receipt).
1559
+ receipt_completeness_issue(failed_proof_receipt).
1560
+ receipt_completeness_issue(invalid_proof_receipt).
1561
+ receipt_completeness_issue(proof_contract_mismatch).
1011
1562
 
1012
- proof_issue_advisory(Gap, Stages) :-
1013
- receipt_completeness_issue(Gap),
1014
- Stages.passingE2e.status == passed.
1563
+ proof_issue_advisory(_Gap, _Stages) :-
1564
+ fail.
1015
1565
 
1016
1566
  proof_gap_present(missing_semantic_inventory, Stages) :- Stages.semanticInventory.propositionCount =:= 0.
1017
1567
  proof_gap_present(incomplete_semantic_inventory, Stages) :- Stages.semanticInventory.missingCount > 0.
@@ -1028,12 +1578,12 @@ proof_gap_present(contradiction_check_incomplete, Stages) :- Stages.contradictio
1028
1578
  proof_gap_present(missing_scenario, Stages) :- Stages.scenarios.status == missing.
1029
1579
  proof_gap_present(missing_scenario_test, Stages) :- Stages.scenarios.status == passed, Stages.scenarioTests.status == missing.
1030
1580
  proof_gap_present(missing_passing_e2e, Stages) :- Stages.passingE2e.status == missing.
1031
- proof_gap_present(missing_verification_receipt, Stages) :- Stages.passingE2e.missingReceiptTests \= [].
1032
- proof_gap_present(stale_verification_receipt, Stages) :- Stages.passingE2e.staleReceiptTests \= [].
1033
- proof_gap_present(failed_verification_receipt, Stages) :- Stages.passingE2e.failedReceiptTests \= [].
1034
- proof_gap_present(invalid_verification_receipt, Stages) :- Stages.passingE2e.invalidReceiptTests \= [].
1035
- proof_gap_present(verification_contract_mismatch, Stages) :- Stages.passingE2e.contractMismatchReceiptTests \= [].
1036
- proof_gap_present(verification_snapshot_unavailable, Stages) :- Stages.passingE2e.snapshotUnavailableTests \= [].
1581
+ proof_gap_present(missing_proof_receipt, Stages) :- Stages.passingE2e.missingReceiptTests \= [].
1582
+ proof_gap_present(stale_proof_receipt, Stages) :- Stages.passingE2e.staleReceiptTests \= [].
1583
+ proof_gap_present(failed_proof_receipt, Stages) :- Stages.passingE2e.failedReceiptTests \= [].
1584
+ proof_gap_present(invalid_proof_receipt, Stages) :- Stages.passingE2e.invalidReceiptTests \= [].
1585
+ proof_gap_present(proof_contract_mismatch, Stages) :- Stages.passingE2e.contractMismatchReceiptTests \= [].
1586
+ proof_gap_present(proof_snapshot_unavailable, Stages) :- Stages.passingE2e.snapshotUnavailableTests \= [].
1037
1587
  proof_gap_present(missing_executable_test_symbol, Stages) :- Stages.executableSymbols.status == missing.
1038
1588
  proof_gap_present(missing_production_symbol, Stages) :-
1039
1589
  Stages.productionSymbols.symbols == [],
@@ -1055,12 +1605,12 @@ gap_definition(contradiction_check_incomplete, 41, contradictions, "Complete log
1055
1605
  gap_definition(missing_scenario, 50, scenarios, "Add a specified_by scenario for the requirement.").
1056
1606
  gap_definition(missing_scenario_test, 51, scenario_tests, "Link the scenario to a test with verified_by or validates.").
1057
1607
  gap_definition(missing_passing_e2e, 52, passing_e2e, "Record fresh passing end-to-end evidence on a scenario-backed test.").
1058
- gap_definition(missing_verification_receipt, 53, passing_e2e, "Run the exact current verification contract through kibi verify and append its kibi.verification-receipt.v2 result for the scenario-backed E2E test.").
1059
- gap_definition(stale_verification_receipt, 54, passing_e2e, "Re-run the E2E test against the current code snapshot and append its receipt.").
1060
- gap_definition(failed_verification_receipt, 55, passing_e2e, "Repair the failing E2E behavior and append a newer passing receipt for the same snapshot.").
1061
- gap_definition(invalid_verification_receipt, 56, passing_e2e, "Repair malformed, future-dated, mismatched, or otherwise uncheckable receipt evidence.").
1062
- gap_definition(verification_contract_mismatch, 57, passing_e2e, "Run the current verification contract and append its receipt without rewriting historical evidence.").
1063
- gap_definition(verification_snapshot_unavailable, 58, passing_e2e, "Run coverage through a CLI or MCP runtime that exposes the deterministic workspace snapshot.").
1608
+ gap_definition(missing_proof_receipt, 53, passing_e2e, "Run the configured proof integration through kibi prove and append its kibi.proof-receipt.v1 result for the scenario-backed proof-bearing test.").
1609
+ gap_definition(stale_proof_receipt, 54, passing_e2e, "Re-run kibi prove against the current code snapshot and append fresh receipts.").
1610
+ gap_definition(failed_proof_receipt, 55, passing_e2e, "Repair the failing proof obligation and append a newer passing receipt for the same snapshot.").
1611
+ gap_definition(invalid_proof_receipt, 56, passing_e2e, "Repair malformed, future-dated, mismatched, or otherwise uncheckable receipt evidence.").
1612
+ gap_definition(proof_contract_mismatch, 57, passing_e2e, "Run kibi prove so the receipt carries the current proof contract hash and execution fingerprint without rewriting historical evidence.").
1613
+ gap_definition(proof_snapshot_unavailable, 58, passing_e2e, "Run coverage through a CLI or MCP runtime that exposes the deterministic workspace snapshot.").
1064
1614
  gap_definition(missing_executable_test_symbol, 60, executable_symbols, "Link executable test code to every qualifying E2E test with executable_for.").
1065
1615
  gap_definition(missing_production_symbol, 70, production_symbols, "Link at least one production symbol to the requirement with implements.").
1066
1616
  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.").