kibi-core 0.10.0 → 0.12.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,64 +4,81 @@
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
 
10
11
  :- use_module(library(http/json)).
12
+ :- use_module(library(crypto)).
11
13
  :- use_module(library(date)).
12
14
  :- use_module(library(pcre)).
13
15
  :- use_module('kb.pl').
14
16
  :- use_module('checks.pl', [
15
- check_domain_contradictions/1,
16
- check_domain_contradiction_witnesses/1,
17
+ check_domain_contradictions_and_witnesses/2,
17
18
  check_rule_safety/1,
18
19
  check_rule_verifiability/1
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) :-
28
- check_domain_contradictions(Contradictions),
29
- check_domain_contradiction_witnesses(ContradictionWitnesses),
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) :-
40
+ check_domain_contradictions_and_witnesses(Contradictions, ContradictionWitnesses),
30
41
  check_rule_safety(UnsafeRules),
31
42
  check_rule_verifiability(UnverifiableRules),
32
43
  normalize_atom(VerificationSnapshot, Snapshot),
33
44
  normalize_atom(CheckedAt, CheckedAtAtom),
34
45
  (catch(parse_time(CheckedAtAtom, CheckedAtStamp), _, fail) -> true ; CheckedAtStamp = -1),
35
46
  normalize_integer(MaxAgeSeconds, MaxAge),
47
+ normalize_atom(BindingMode, BindingModeAtom),
36
48
  Context = _{
37
49
  contradictions: Contradictions,
38
50
  contradictionWitnesses: ContradictionWitnesses,
39
51
  unsafeRules: UnsafeRules,
40
52
  unverifiableRules: UnverifiableRules,
41
- verificationSnapshot: Snapshot,
42
- verificationCheckedAt: CheckedAtAtom,
43
- verificationCheckedAtStamp: CheckedAtStamp,
44
- verificationMaxAgeSeconds: MaxAge
53
+ proofSnapshot: Snapshot,
54
+ proofCheckedAt: CheckedAtAtom,
55
+ proofCheckedAtStamp: CheckedAtStamp,
56
+ proofMaxAgeSeconds: MaxAge,
57
+ proofBindingMode: BindingModeAtom,
58
+ proofTestBindings: TestBindings
45
59
  }.
46
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).
47
70
  requirement_proof(ReqId, _ReqProps, _Context, Proof) :-
48
71
  \+ kb:current_req(ReqId),
49
72
  !,
50
- proof_version(Version),
51
- Proof = _{
52
- proofVersion: Version,
53
- proofStatus: not_applicable,
54
- proofGaps: [],
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
- scenario_test_stage(ScenarioIds, ScenarioTestStage, ScenarioTests),
64
- passing_e2e_stage(ScenarioTests, Context, PassingE2eStage, PassingE2eTests),
80
+ scenario_test_stage(ScenarioIds, ScenarioTestStage, _ScenarioTests),
81
+ passing_e2e_stage(ScenarioTestStage, Context, PassingE2eStage, PassingE2eTests),
65
82
  executable_symbol_stage(PassingE2eTests, ExecutableStage, ExecutableSymbols),
66
83
  production_symbol_stage(ReqId, PassingE2eTests, ProductionStage, ProductionSymbols),
67
84
  source_coordinate_stage(ReqProps, ExecutableSymbols, ProductionSymbols, CoordinateStage),
@@ -76,7 +93,7 @@ requirement_proof(ReqId, ReqProps, Context, Proof) :-
76
93
  productionSymbols: ProductionStage,
77
94
  sourceCoordinates: CoordinateStage
78
95
  },
79
- proof_gaps(Stages, Gaps),
96
+ proof_issues(Stages, Gaps, Advisories),
80
97
  proof_repairs(Gaps, Repairs),
81
98
  proof_status(Stages, Status),
82
99
  proof_version(Version),
@@ -84,10 +101,50 @@ requirement_proof(ReqId, ReqProps, Context, Proof) :-
84
101
  proofVersion: Version,
85
102
  proofStatus: Status,
86
103
  proofGaps: Gaps,
104
+ proofAdvisories: Advisories,
87
105
  proofRepairs: Repairs,
88
106
  proofStages: Stages
89
107
  }.
90
108
 
109
+ exempt_requirement_proof(Reason, Proof) :-
110
+ proof_version(Version),
111
+ Proof = _{
112
+ proofVersion: Version,
113
+ proofStatus: not_applicable,
114
+ proofGaps: [],
115
+ proofAdvisories: [],
116
+ proofRepairs: [],
117
+ proofStages: _{applicability: _{status: not_applicable, reason: Reason}}
118
+ }.
119
+
120
+ %% proof_exempt(+Props, -Reason)
121
+ % A current requirement is exempt from E2E-proof scope when its author set
122
+ % proof_exempt to a truthy value AND supplied a non-empty proof_exempt_reason.
123
+ % Without the reason the exemption does not apply, so an author cannot park a
124
+ % requirement silently; upsert validation enforces the same pairing.
125
+ proof_exempt(Props, Reason) :-
126
+ memberchk(proof_exempt=RawFlag, Props),
127
+ normalize_atom(RawFlag, Flag),
128
+ memberchk(Flag, [true, yes, on, '1']),
129
+ memberchk(proof_exempt_reason=RawReason, Props),
130
+ normalize_atom(RawReason, Reason),
131
+ Reason \= ''.
132
+
133
+ %% applicability_exclusion_reason(+ReqId, -Reason)
134
+ % Typed reason a requirement fails current_req/1: superseded lifecycle, or a
135
+ % status outside the canonical+legacy requirement vocabulary (e.g. the ADR
136
+ % vocabulary 'accepted').
137
+ applicability_exclusion_reason(ReqId, "requirement is superseded by a newer requirement") :-
138
+ kb_relationship(supersedes, _, ReqId),
139
+ !.
140
+ applicability_exclusion_reason(ReqId, Reason) :-
141
+ kb_entity(ReqId, req, Props),
142
+ memberchk(status=Status, Props),
143
+ normalize_term_atom(Status, StatusAtom),
144
+ format(atom(Reason), "status '~w' is not a current requirement status", [StatusAtom]),
145
+ !.
146
+ applicability_exclusion_reason(_ReqId, "requirement is not current").
147
+
91
148
  semantic_inventory_stage(Props, Stage, Entries) :-
92
149
  ( memberchk(semantic_inventory=RawInventory, Props),
93
150
  inventory_entries(RawInventory, Entries0),
@@ -224,6 +281,7 @@ logic_grounding_stage(ReqId, Props, Inventory, Context, Stage) :-
224
281
  InvalidRuleFactIds,
225
282
  Status
226
283
  ),
284
+ ground_source_refs(Evidence, GroundSources),
227
285
  Stage = _{
228
286
  status: Status,
229
287
  manifestClaims: ManifestKeys,
@@ -238,7 +296,8 @@ logic_grounding_stage(ReqId, Props, Inventory, Context, Stage) :-
238
296
  missingManifestClaims: MissingManifestKeys,
239
297
  extraManifestClaims: ExtraManifestKeys,
240
298
  undeclaredGroundClaims: UndeclaredGroundKeys,
241
- invalidRuleFacts: InvalidRuleFactIds
299
+ invalidRuleFacts: InvalidRuleFactIds,
300
+ sources: GroundSources
242
301
  }.
243
302
 
244
303
  requirement_claim_keys(Props, true, Keys) :-
@@ -432,56 +491,213 @@ violation_for_entity(Violations, EntityId) :-
432
491
 
433
492
  scenario_stage(ReqId, Stage, ScenarioIds) :-
434
493
  findall(ScenarioId,
435
- (kb_relationship(specified_by, ReqId, ScenarioId), kb_entity(ScenarioId, scenario, _)),
436
- ScenarioIds0),
437
- sort(ScenarioIds0, ScenarioIds),
438
- (ScenarioIds = [] -> Status = missing ; Status = passed),
439
- Stage = _{status: Status, scenarios: ScenarioIds}.
494
+ kb_relationship(specified_by, ReqId, ScenarioId),
495
+ ScenarioTargets0),
496
+ sort(ScenarioTargets0, ScenarioTargets),
497
+ include(existing_scenario, ScenarioTargets, ScenarioIds),
498
+ findall(ScenarioId,
499
+ (member(ScenarioId, ScenarioTargets), \+ existing_scenario(ScenarioId)),
500
+ InvalidScenarioTargets),
501
+ ((ScenarioIds = [] ; InvalidScenarioTargets \= []) -> Status = missing ; Status = passed),
502
+ maplist(entity_source_ref, ScenarioIds, Sources),
503
+ Stage = _{
504
+ status: Status,
505
+ scenarios: ScenarioIds,
506
+ scenarioTargets: ScenarioTargets,
507
+ invalidScenarioTargets: InvalidScenarioTargets,
508
+ sources: Sources
509
+ }.
510
+
511
+ existing_scenario(ScenarioId) :-
512
+ kb_entity(ScenarioId, scenario, _).
440
513
 
441
514
  scenario_test_stage(ScenarioIds, Stage, ScenarioTests) :-
515
+ maplist(scenario_test_obligation, ScenarioIds, Obligations),
442
516
  findall(TestId,
443
- (member(ScenarioId, ScenarioIds), scenario_test(ScenarioId, TestId)),
517
+ (member(Obligation, Obligations), member(TestId, Obligation.tests)),
444
518
  ScenarioTests0),
445
519
  sort(ScenarioTests0, ScenarioTests),
446
- (ScenarioTests = [] -> Status = missing ; Status = passed),
447
- Stage = _{status: Status, tests: ScenarioTests}.
520
+ findall(TestId,
521
+ (member(Obligation, Obligations), member(TestId, Obligation.scenarioTestTargets)),
522
+ ScenarioTestTargets0),
523
+ sort(ScenarioTestTargets0, ScenarioTestTargets),
524
+ findall(TestId,
525
+ (member(Obligation, Obligations), member(TestId, Obligation.invalidScenarioTestTargets)),
526
+ InvalidScenarioTestTargets0),
527
+ sort(InvalidScenarioTestTargets0, InvalidScenarioTestTargets),
528
+ ( ScenarioIds == []
529
+ -> Status = missing
530
+ ; member(Obligation, Obligations), Obligation.status == missing
531
+ -> Status = missing
532
+ ; Status = passed
533
+ ),
534
+ maplist(entity_source_ref, ScenarioTests, Sources),
535
+ Stage = _{
536
+ status: Status,
537
+ scenarios: ScenarioIds,
538
+ tests: ScenarioTests,
539
+ scenarioTestTargets: ScenarioTestTargets,
540
+ invalidScenarioTestTargets: InvalidScenarioTestTargets,
541
+ sources: Sources,
542
+ obligations: Obligations
543
+ }.
448
544
 
449
- scenario_test(ScenarioId, TestId) :-
450
- kb_relationship(verified_by, ScenarioId, TestId),
545
+ scenario_test_obligation(ScenarioId, Obligation) :-
546
+ findall(TestId, declared_scenario_test_target(ScenarioId, TestId), TestTargets0),
547
+ sort(TestTargets0, TestTargets),
548
+ include(existing_test, TestTargets, TestIds),
549
+ findall(TestId,
550
+ (member(TestId, TestTargets), \+ existing_test(TestId)),
551
+ InvalidScenarioTestTargets),
552
+ maplist(entity_source_ref, TestIds, Sources),
553
+ ( (TestIds == [] ; InvalidScenarioTestTargets \= [])
554
+ -> Status = missing
555
+ ; Status = passed
556
+ ),
557
+ Obligation = _{
558
+ scenarioId: ScenarioId,
559
+ status: Status,
560
+ scenarioTestTargets: TestTargets,
561
+ invalidScenarioTestTargets: InvalidScenarioTestTargets,
562
+ tests: TestIds,
563
+ sources: Sources
564
+ }.
565
+
566
+ declared_scenario_test_target(ScenarioId, TestId) :-
567
+ kb_relationship(verified_by, ScenarioId, TestId).
568
+ declared_scenario_test_target(ScenarioId, TestId) :-
569
+ kb_relationship(validates, TestId, ScenarioId).
570
+
571
+ existing_test(TestId) :-
451
572
  kb_entity(TestId, test, _).
573
+
452
574
  scenario_test(ScenarioId, TestId) :-
453
- kb_relationship(validates, TestId, ScenarioId),
454
- kb_entity(TestId, test, _).
575
+ declared_scenario_test_target(ScenarioId, TestId),
576
+ existing_test(TestId).
455
577
 
456
- passing_e2e_stage(ScenarioTests, Context, Stage, PassingE2eTests) :-
457
- maplist(test_receipt_evidence(Context), ScenarioTests, Evidence),
578
+ passing_e2e_stage(ScenarioTestStage, Context, Stage, PassingE2eTests) :-
579
+ maplist(scenario_passing_e2e_obligation(Context), ScenarioTestStage.obligations, ScenarioObligations),
580
+ findall(Item, (member(Obligation, ScenarioObligations), member(Item, Obligation.evidence)), Evidence0),
581
+ unique_evidence_by_test(Evidence0, Evidence),
458
582
  evidence_tests_with_state(Evidence, passed, PassingE2eTests),
459
583
  evidence_tests_with_state(Evidence, missing, MissingReceiptTests),
460
584
  evidence_tests_with_state(Evidence, stale, StaleReceiptTests),
461
585
  evidence_tests_with_state(Evidence, failed, FailedReceiptTests),
462
586
  evidence_tests_with_state(Evidence, invalid, InvalidReceiptTests),
587
+ evidence_tests_with_state(Evidence, contract_mismatch, ContractMismatchReceiptTests),
463
588
  evidence_tests_with_state(Evidence, snapshot_unavailable, SnapshotUnavailableTests),
464
589
  evidence_tests_with_state(Evidence, not_end_to_end, NonEndToEndTests),
465
- passing_e2e_status(PassingE2eTests, InvalidReceiptTests, SnapshotUnavailableTests, Status),
590
+ passing_e2e_status(ScenarioObligations, Status),
466
591
  Stage = _{
467
592
  status: Status,
468
593
  tests: PassingE2eTests,
594
+ scenarioObligations: ScenarioObligations,
469
595
  receiptEvidence: Evidence,
470
596
  missingReceiptTests: MissingReceiptTests,
471
597
  staleReceiptTests: StaleReceiptTests,
472
598
  failedReceiptTests: FailedReceiptTests,
473
599
  invalidReceiptTests: InvalidReceiptTests,
600
+ contractMismatchReceiptTests: ContractMismatchReceiptTests,
474
601
  snapshotUnavailableTests: SnapshotUnavailableTests,
475
602
  nonEndToEndTests: NonEndToEndTests,
476
- currentCodeSnapshot: Context.verificationSnapshot,
477
- checkedAt: Context.verificationCheckedAt,
478
- maxAgeSeconds: Context.verificationMaxAgeSeconds
603
+ currentCodeSnapshot: Context.proofSnapshot,
604
+ checkedAt: Context.proofCheckedAt,
605
+ maxAgeSeconds: Context.proofMaxAgeSeconds
479
606
  }.
480
607
 
481
- passing_e2e_status(Passing, _, _, passed) :- Passing \= [], !.
482
- passing_e2e_status([], Invalid, _, unresolved) :- Invalid \= [], !.
483
- passing_e2e_status([], _, SnapshotUnavailable, unresolved) :- SnapshotUnavailable \= [], !.
484
- passing_e2e_status([], _, _, missing).
608
+ unique_evidence_by_test(Evidence0, Evidence) :-
609
+ findall(TestId,
610
+ (member(Item, Evidence0), TestId = Item.testId),
611
+ TestIds0),
612
+ sort(TestIds0, TestIds),
613
+ maplist(first_evidence_for_test(Evidence0), TestIds, Evidence).
614
+
615
+ first_evidence_for_test(Evidence, TestId, Item) :-
616
+ member(Item, Evidence),
617
+ Item.testId == TestId,
618
+ !.
619
+
620
+ scenario_passing_e2e_obligation(Context, TestObligation, Obligation) :-
621
+ maplist(test_receipt_evidence(Context), TestObligation.tests, Evidence),
622
+ include(e2e_evidence, Evidence, E2eEvidence),
623
+ include(non_e2e_evidence, Evidence, NonEndToEndEvidence),
624
+ evidence_tests_with_state(E2eEvidence, passed, PassingE2eTests),
625
+ evidence_tests_with_state(E2eEvidence, missing, MissingReceiptTests),
626
+ evidence_tests_with_state(E2eEvidence, stale, StaleReceiptTests),
627
+ evidence_tests_with_state(E2eEvidence, failed, FailedReceiptTests),
628
+ evidence_tests_with_state(E2eEvidence, invalid, InvalidReceiptTests),
629
+ evidence_tests_with_state(E2eEvidence, contract_mismatch, ContractMismatchReceiptTests),
630
+ evidence_tests_with_state(E2eEvidence, snapshot_unavailable, SnapshotUnavailableTests),
631
+ evidence_tests_with_state(NonEndToEndEvidence, not_end_to_end, NonEndToEndTests),
632
+ scenario_obligation_gaps(TestObligation, E2eEvidence, Gaps),
633
+ scenario_obligation_status(TestObligation, E2eEvidence, Status),
634
+ findall(TestId,
635
+ (member(Item, E2eEvidence), TestId = Item.testId),
636
+ E2eTests0),
637
+ sort(E2eTests0, E2eTests),
638
+ Obligation = _{
639
+ scenarioId: TestObligation.scenarioId,
640
+ status: Status,
641
+ scenarioTestTargets: TestObligation.scenarioTestTargets,
642
+ invalidScenarioTestTargets: TestObligation.invalidScenarioTestTargets,
643
+ tests: TestObligation.tests,
644
+ e2eTests: E2eTests,
645
+ nonEndToEndTests: NonEndToEndTests,
646
+ evidence: Evidence,
647
+ gaps: Gaps,
648
+ passingE2eTests: PassingE2eTests,
649
+ missingReceiptTests: MissingReceiptTests,
650
+ staleReceiptTests: StaleReceiptTests,
651
+ failedReceiptTests: FailedReceiptTests,
652
+ invalidReceiptTests: InvalidReceiptTests,
653
+ contractMismatchReceiptTests: ContractMismatchReceiptTests,
654
+ snapshotUnavailableTests: SnapshotUnavailableTests
655
+ }.
656
+
657
+ e2e_evidence(Item) :- Item.state \= not_end_to_end.
658
+ non_e2e_evidence(Item) :- Item.state == not_end_to_end.
659
+
660
+ scenario_obligation_gaps(TestObligation, E2eEvidence, Gaps) :-
661
+ findall(Gap, scenario_obligation_gap(TestObligation, E2eEvidence, Gap), Gaps0),
662
+ sort(Gaps0, Gaps).
663
+
664
+ scenario_obligation_gap(TestObligation, _E2eEvidence, missing_scenario_test) :-
665
+ TestObligation.status == missing.
666
+ scenario_obligation_gap(TestObligation, E2eEvidence, missing_passing_e2e) :-
667
+ TestObligation.status == passed,
668
+ E2eEvidence == [].
669
+ scenario_obligation_gap(_TestObligation, Evidence, missing_proof_receipt) :-
670
+ member(Item, Evidence), Item.state == missing.
671
+ scenario_obligation_gap(_TestObligation, Evidence, stale_proof_receipt) :-
672
+ member(Item, Evidence), Item.state == stale.
673
+ scenario_obligation_gap(_TestObligation, Evidence, failed_proof_receipt) :-
674
+ member(Item, Evidence), Item.state == failed.
675
+ scenario_obligation_gap(_TestObligation, Evidence, invalid_proof_receipt) :-
676
+ member(Item, Evidence), Item.state == invalid.
677
+ scenario_obligation_gap(_TestObligation, Evidence, proof_contract_mismatch) :-
678
+ member(Item, Evidence), Item.state == contract_mismatch.
679
+ scenario_obligation_gap(_TestObligation, Evidence, proof_snapshot_unavailable) :-
680
+ member(Item, Evidence), Item.state == snapshot_unavailable.
681
+
682
+ scenario_obligation_status(TestObligation, _E2eEvidence, missing) :-
683
+ TestObligation.status == missing,
684
+ !.
685
+ scenario_obligation_status(_TestObligation, [], missing) :-
686
+ !.
687
+ scenario_obligation_status(_TestObligation, Evidence, passed) :-
688
+ Evidence \= [],
689
+ forall(member(Item, Evidence), Item.state == passed),
690
+ !.
691
+ scenario_obligation_status(_TestObligation, _Evidence, unresolved).
692
+
693
+ passing_e2e_status([], missing) :- !.
694
+ passing_e2e_status(Obligations, passed) :-
695
+ forall(member(Obligation, Obligations), Obligation.status == passed),
696
+ !.
697
+ passing_e2e_status(Obligations, missing) :-
698
+ member(Obligation, Obligations), Obligation.status == missing,
699
+ !.
700
+ passing_e2e_status(_Obligations, unresolved).
485
701
 
486
702
  evidence_tests_with_state(Evidence, State, TestIds) :-
487
703
  findall(TestId,
@@ -494,72 +710,150 @@ test_receipt_evidence(Context, TestId, Evidence) :-
494
710
  test_scope(Props, Scope),
495
711
  ( Scope \= end_to_end
496
712
  -> Evidence = _{testId: TestId, state: not_end_to_end, scope: Scope}
497
- ; Context.verificationSnapshot == unknown
713
+ ; Context.proofSnapshot == unknown
498
714
  -> Evidence = _{testId: TestId, state: snapshot_unavailable, scope: Scope}
499
- ; verification_receipt_entries(Props, Receipts, ReceiptPropertyPresent),
500
- receipt_evidence_state(TestId, Scope, Receipts, ReceiptPropertyPresent, Context, Evidence)
715
+ ; proof_receipt_entries(Props, Receipts, ReceiptPropertyPresent),
716
+ receipt_evidence_state(TestId, Scope, Props, Receipts, ReceiptPropertyPresent, Context, Evidence)
501
717
  ).
502
718
 
503
- receipt_evidence_state(TestId, Scope, [], false, _Context,
719
+ receipt_evidence_state(TestId, Scope, _Props, [], false, _Context,
504
720
  _{testId: TestId, state: missing, scope: Scope, receiptCount: 0}) :- !.
505
- receipt_evidence_state(TestId, Scope, Receipts, _Present, _Context,
721
+ receipt_evidence_state(TestId, Scope, Props, Receipts, _Present, _Context,
506
722
  _{testId: TestId, state: invalid, scope: Scope, receiptCount: Count}) :-
507
723
  length(Receipts, Count),
508
- (Receipts == []
509
- ; member(Receipt, Receipts), \+ valid_receipt_shape(TestId, Scope, Receipt)
724
+ proof_contract_binding(Props, ContractBinding),
725
+ (ContractBinding == invalid
726
+ ; Receipts == []
727
+ ; member(Receipt, Receipts), \+ valid_receipt_shape(TestId, Receipt)
510
728
  ; \+ chronological_receipt_history(Receipts)),
511
729
  !.
512
- receipt_evidence_state(TestId, Scope, Receipts, _Present, Context, Evidence) :-
513
- include(receipt_for_snapshot(Context.verificationSnapshot), Receipts, CurrentReceipts),
514
- ( CurrentReceipts == []
730
+ receipt_evidence_state(TestId, Scope, Props, Receipts, _Present, Context, Evidence) :-
731
+ receipt_for_current_mode(TestId, Context, Receipts, SnapshotReceipts),
732
+ ( SnapshotReceipts == []
515
733
  -> length(Receipts, Count),
516
734
  Evidence = _{testId: TestId, state: stale, scope: Scope, receiptCount: Count}
517
- ; latest_receipt(CurrentReceipts, Latest, FinishedStamp),
518
- receipt_runtime_state(Latest, FinishedStamp, Context, State, AgeSeconds),
519
- receipt_evidence_dict(TestId, Scope, Latest, State, AgeSeconds, Evidence)
735
+ ; proof_contract_binding(Props, ContractBinding),
736
+ include(receipt_matches_current_binding(Scope, ContractBinding), SnapshotReceipts, CurrentReceipts),
737
+ ( CurrentReceipts == []
738
+ -> receipt_contract_mismatch_evidence(TestId, Scope, SnapshotReceipts, ContractBinding, Evidence)
739
+ ; latest_receipt(CurrentReceipts, Latest, FinishedStamp),
740
+ receipt_runtime_state(Latest, FinishedStamp, Context, State, AgeSeconds),
741
+ receipt_evidence_dict(TestId, Scope, Latest, State, AgeSeconds, Evidence)
742
+ )
520
743
  ).
521
744
 
522
- verification_receipt_entries(Props, Receipts, Present) :-
523
- ( memberchk(verification_receipts=Raw, Props)
745
+ proof_receipt_entries(Props, Receipts, Present) :-
746
+ ( memberchk(proof_receipts=Raw, Props)
524
747
  -> Present = true,
525
748
  inventory_entries(Raw, Receipts)
526
749
  ; Present = false,
527
750
  Receipts = []
528
751
  ).
529
752
 
530
- valid_receipt_shape(TestId, Scope, Receipt) :-
753
+ valid_receipt_shape(TestId, Receipt) :-
531
754
  inventory_entry_field(Receipt, version, RawVersion),
532
- normalize_receipt_atom(RawVersion, 'kibi.verification-receipt.v1'),
755
+ normalize_receipt_atom(RawVersion, 'kibi.proof-receipt.v1'),
533
756
  inventory_entry_field(Receipt, receipt_id, RawReceiptId),
534
757
  normalize_receipt_atom(RawReceiptId, ReceiptId), valid_receipt_id(ReceiptId),
535
758
  inventory_entry_field(Receipt, test_id, RawTestId),
536
759
  normalize_receipt_atom(RawTestId, TestId),
537
- inventory_entry_field(Receipt, runner, RawRunner),
538
- normalize_receipt_atom(RawRunner, Runner), Runner \= '',
539
- inventory_entry_field(Receipt, command, RawCommand),
540
- normalize_receipt_atom(RawCommand, Command), Command \= '',
541
760
  inventory_entry_field(Receipt, scope, RawScope),
542
- normalize_receipt_atom(RawScope, Scope),
761
+ normalize_receipt_atom(RawScope, ReceiptScope),
762
+ memberchk(ReceiptScope, [unit, integration, end_to_end]),
543
763
  inventory_entry_field(Receipt, outcome, RawOutcome),
544
764
  normalize_receipt_atom(RawOutcome, Outcome),
545
- memberchk(Outcome, [passed, failed, errored, cancelled, skipped]),
765
+ memberchk(Outcome, [passed, failed, errored, cancelled, skipped, timed_out, interrupted]),
546
766
  inventory_entry_field(Receipt, code_snapshot, RawSnapshot),
547
767
  normalize_receipt_atom(RawSnapshot, Snapshot), valid_sha256(Snapshot),
548
768
  inventory_entry_field(Receipt, environment_hash, RawEnvironmentHash),
549
769
  normalize_receipt_atom(RawEnvironmentHash, EnvironmentHash), valid_sha256(EnvironmentHash),
550
770
  inventory_entry_field(Receipt, artifact_digest, RawArtifactDigest),
551
771
  normalize_receipt_atom(RawArtifactDigest, ArtifactDigest), valid_sha256(ArtifactDigest),
772
+ inventory_entry_field(Receipt, contract_hash, RawContractHash),
773
+ normalize_receipt_atom(RawContractHash, ContractHash), valid_sha256(ContractHash),
774
+ inventory_entry_field(Receipt, fingerprint, RawFingerprint),
775
+ normalize_receipt_atom(RawFingerprint, Fingerprint), valid_sha256(Fingerprint),
776
+ valid_fingerprint_components(Receipt),
777
+ inventory_entry_field(Receipt, integration_id, RawIntegrationId),
778
+ normalize_receipt_atom(RawIntegrationId, IntegrationId), IntegrationId \= '',
779
+ inventory_entry_field(Receipt, producer, RawProducer),
780
+ receipt_producer_name(RawProducer, ProducerName), ProducerName \= '',
781
+ inventory_entry_field(Receipt, command_argv, RawCommandArgv),
782
+ is_list(RawCommandArgv),
783
+ RawCommandArgv \= [],
784
+ maplist(nonempty_receipt_atom, RawCommandArgv),
785
+ ( inventory_entry_field(Receipt, binding_hash, RawBindingHash)
786
+ -> normalize_receipt_atom(RawBindingHash, BindingHash),
787
+ valid_sha256(BindingHash)
788
+ ; true
789
+ ),
790
+ inventory_entry_field(Receipt, run_outcome, RawRunOutcome),
791
+ normalize_receipt_atom(RawRunOutcome, RunOutcome),
792
+ memberchk(RunOutcome, [passed, failed, errored, cancelled, timed_out, interrupted, no_results]),
793
+ inventory_entry_field(Receipt, proof_results, RawResults),
794
+ is_list(RawResults),
795
+ maplist(valid_receipt_proof_result, RawResults),
552
796
  receipt_timestamp(Receipt, started_at, StartedStamp),
553
797
  receipt_timestamp(Receipt, finished_at, FinishedStamp),
554
798
  FinishedStamp >= StartedStamp.
555
799
 
800
+ receipt_producer_name(Producer, Name) :-
801
+ inventory_entry_field(Producer, name, RawName),
802
+ normalize_receipt_atom(RawName, Name).
803
+
804
+ valid_fingerprint_components(Receipt) :-
805
+ inventory_entry_field(Receipt, fingerprint_components, Components),
806
+ forall(member(Key, [contract, integration, command, bindings, producer]),
807
+ (inventory_entry_field(Components, Key, Raw),
808
+ normalize_receipt_atom(Raw, Value),
809
+ valid_sha256(Value))),
810
+ !.
811
+
812
+ nonempty_receipt_atom(Value) :-
813
+ normalize_receipt_atom(Value, Atom),
814
+ Atom \= ''.
815
+
816
+ valid_receipt_proof_result(Result) :-
817
+ inventory_entry_field(Result, symbol_id, RawSymbolId),
818
+ nonempty_receipt_atom(RawSymbolId),
819
+ inventory_entry_field(Result, target, RawTarget),
820
+ nonempty_receipt_atom(RawTarget),
821
+ inventory_entry_field(Result, outcome, RawOutcome),
822
+ normalize_receipt_atom(RawOutcome, ResultOutcome),
823
+ memberchk(ResultOutcome, [passed, failed, timed_out, skipped, interrupted, errored]),
824
+ inventory_entry_field(Result, binding, RawBinding),
825
+ normalize_receipt_atom(RawBinding, Binding),
826
+ memberchk(Binding, [native_case, aggregate_run]),
827
+ valid_receipt_attempts(Result).
828
+
829
+ valid_receipt_attempts(Result) :-
830
+ inventory_entry_field(Result, attempts, Attempts),
831
+ inventory_entry_field(Attempts, status, RawStatus),
832
+ normalize_receipt_atom(RawStatus, Status),
833
+ valid_attempt_status(Status, Attempts).
834
+
835
+ valid_attempt_status(unavailable, Attempts) :-
836
+ \+ inventory_entry_field(Attempts, entries, _Present), !.
837
+ valid_attempt_status(unavailable, Attempts) :-
838
+ inventory_entry_field(Attempts, entries, Entries),
839
+ Entries == [], !.
840
+ valid_attempt_status(complete, Attempts) :-
841
+ inventory_entry_field(Attempts, entries, Entries),
842
+ is_list(Entries),
843
+ Entries \= [],
844
+ forall(member(Entry, Entries),
845
+ (inventory_entry_field(Entry, outcome, RawEntryOutcome),
846
+ normalize_receipt_atom(RawEntryOutcome, EntryOutcome),
847
+ memberchk(EntryOutcome, [passed, failed, timed_out, skipped, interrupted, errored]))),
848
+ !.
849
+
556
850
  valid_sha256(Value) :-
557
851
  atom_length(Value, 64),
558
852
  atom_chars(Value, Chars),
559
853
  forall(member(Char, Chars), memberchk(Char, ['0','1','2','3','4','5','6','7','8','9',a,b,c,d,e,f])).
560
854
 
561
855
  valid_receipt_id(Value) :-
562
- re_match('^VR-[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$', Value).
856
+ re_match('^PR-[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$', Value).
563
857
 
564
858
  receipt_timestamp(Receipt, Field, Stamp) :-
565
859
  inventory_entry_field(Receipt, Field, RawValue),
@@ -584,6 +878,105 @@ receipt_for_snapshot(Snapshot, Receipt) :-
584
878
  inventory_entry_field(Receipt, code_snapshot, RawSnapshot),
585
879
  normalize_receipt_atom(RawSnapshot, Snapshot).
586
880
 
881
+ % Per-contract binding selector (W2). In per_contract mode a receipt carrying
882
+ % a binding_hash is current when it matches the test's current binding hash;
883
+ % receipts without one keep the strict snapshot semantics so older evidence
884
+ % stays valid until each contract is re-proven.
885
+ receipt_for_current_mode(TestId, Context, Receipts, SnapshotReceipts) :-
886
+ ( Context.proofBindingMode == per_contract,
887
+ get_dict(TestId, Context.proofTestBindings, CurrentBinding)
888
+ -> include(receipt_with_binding_hash, Receipts, BoundReceipts),
889
+ include(receipt_for_binding(CurrentBinding), BoundReceipts, SnapshotReceipts),
890
+ ( SnapshotReceipts == []
891
+ -> include(receipt_for_snapshot(Context.proofSnapshot), Receipts, SnapshotReceipts)
892
+ ; true
893
+ )
894
+ ; include(receipt_for_snapshot(Context.proofSnapshot), Receipts, SnapshotReceipts)
895
+ ).
896
+
897
+ receipt_with_binding_hash(Receipt) :-
898
+ inventory_entry_field(Receipt, binding_hash, _).
899
+
900
+ receipt_for_binding(Binding, Receipt) :-
901
+ inventory_entry_field(Receipt, binding_hash, RawBinding),
902
+ normalize_receipt_atom(RawBinding, Binding).
903
+
904
+ receipt_matches_current_binding(Scope, ContractBinding, Receipt) :-
905
+ inventory_entry_field(Receipt, scope, RawScope),
906
+ normalize_receipt_atom(RawScope, Scope),
907
+ receipt_matches_current_contract(ContractBinding, Receipt).
908
+
909
+ receipt_matches_current_contract(none, _Receipt).
910
+ receipt_matches_current_contract(hash(ExpectedHash), Receipt) :-
911
+ inventory_entry_field(Receipt, version, RawVersion),
912
+ normalize_receipt_atom(RawVersion, 'kibi.proof-receipt.v1'),
913
+ inventory_entry_field(Receipt, contract_hash, RawContractHash),
914
+ normalize_receipt_atom(RawContractHash, ExpectedHash).
915
+
916
+ receipt_contract_mismatch_evidence(TestId, Scope, Receipts, ContractBinding, Evidence) :-
917
+ length(Receipts, Count),
918
+ findall(Hash,
919
+ (member(Receipt, Receipts),
920
+ inventory_entry_field(Receipt, contract_hash, RawHash),
921
+ normalize_receipt_atom(RawHash, Hash)),
922
+ Hashes0),
923
+ sort(Hashes0, Hashes),
924
+ current_contract_hash_value(ContractBinding, CurrentContractHash),
925
+ Evidence = _{
926
+ testId: TestId,
927
+ state: contract_mismatch,
928
+ scope: Scope,
929
+ receiptCount: Count,
930
+ currentContractHash: CurrentContractHash,
931
+ receiptContractHashes: Hashes
932
+ }.
933
+
934
+ current_contract_hash_value(hash(Hash), Hash).
935
+ current_contract_hash_value(none, none).
936
+
937
+ proof_contract_binding(Props, Binding) :-
938
+ ( memberchk(proof_contract=RawContract, Props)
939
+ -> (proof_contract_hash(RawContract, Hash) -> Binding = hash(Hash) ; Binding = invalid)
940
+ ; Binding = none
941
+ ).
942
+
943
+ proof_contract_hash(RawContract, Hash) :-
944
+ proof_contract_dict(RawContract, Contract),
945
+ canonical_json_value(Contract, Canonical),
946
+ crypto_data_hash(Canonical, Hash, [algorithm(sha256), encoding(utf8)]).
947
+
948
+ proof_contract_dict(^^(Value, _), Contract) :- !,
949
+ proof_contract_dict(Value, Contract).
950
+ proof_contract_dict(literal(type(_, Value)), Contract) :- !,
951
+ proof_contract_dict(Value, Contract).
952
+ proof_contract_dict(literal(Value), Contract) :- !,
953
+ proof_contract_dict(Value, Contract).
954
+ proof_contract_dict(Contract, Contract) :-
955
+ is_dict(Contract), !.
956
+ proof_contract_dict(Raw, Contract) :-
957
+ (atom(Raw) ; string(Raw)),
958
+ catch(atom_json_dict(Raw, Contract, [value_string_as(string)]), _, fail),
959
+ is_dict(Contract).
960
+
961
+ canonical_json_value(Value, Json) :-
962
+ is_dict(Value), !,
963
+ dict_pairs(Value, _, Pairs),
964
+ maplist(canonical_json_pair, Pairs, Parts),
965
+ atomics_to_string(Parts, ",", Body),
966
+ format(string(Json), "{~s}", [Body]).
967
+ canonical_json_value(Value, Json) :-
968
+ is_list(Value), !,
969
+ maplist(canonical_json_value, Value, Parts),
970
+ atomics_to_string(Parts, ",", Body),
971
+ format(string(Json), "[~s]", [Body]).
972
+ canonical_json_value(Value, Json) :-
973
+ with_output_to(string(Json), json_write(current_output, Value, [width(0)])).
974
+
975
+ canonical_json_pair(Key-Value, Json) :-
976
+ with_output_to(string(KeyJson), json_write(current_output, Key, [width(0)])),
977
+ canonical_json_value(Value, ValueJson),
978
+ format(string(Json), "~s:~s", [KeyJson, ValueJson]).
979
+
587
980
  latest_receipt(Receipts, Latest, FinishedStamp) :-
588
981
  findall(Stamp-Receipt,
589
982
  (member(Receipt, Receipts), receipt_timestamp(Receipt, finished_at, Stamp)),
@@ -592,23 +985,21 @@ latest_receipt(Receipts, Latest, FinishedStamp) :-
592
985
  last(Sorted, FinishedStamp-Latest).
593
986
 
594
987
  receipt_runtime_state(_Receipt, FinishedStamp, Context, invalid, AgeSeconds) :-
595
- AgeSeconds is Context.verificationCheckedAtStamp - FinishedStamp,
988
+ AgeSeconds is Context.proofCheckedAtStamp - FinishedStamp,
596
989
  AgeSeconds < -300,
597
990
  !.
598
991
  receipt_runtime_state(_Receipt, FinishedStamp, Context, stale, AgeSeconds) :-
599
- AgeSeconds is Context.verificationCheckedAtStamp - FinishedStamp,
600
- AgeSeconds > Context.verificationMaxAgeSeconds,
992
+ AgeSeconds is Context.proofCheckedAtStamp - FinishedStamp,
993
+ AgeSeconds > Context.proofMaxAgeSeconds,
601
994
  !.
602
995
  receipt_runtime_state(Receipt, FinishedStamp, Context, State, AgeSeconds) :-
603
- AgeSeconds is Context.verificationCheckedAtStamp - FinishedStamp,
996
+ AgeSeconds is Context.proofCheckedAtStamp - FinishedStamp,
604
997
  inventory_entry_field(Receipt, outcome, RawOutcome),
605
998
  normalize_receipt_atom(RawOutcome, Outcome),
606
999
  (Outcome == passed -> State = passed ; State = failed).
607
1000
 
608
1001
  receipt_evidence_dict(TestId, Scope, Receipt, State, AgeSeconds, Evidence) :-
609
1002
  receipt_string_field(Receipt, receipt_id, ReceiptId),
610
- receipt_string_field(Receipt, runner, Runner),
611
- receipt_string_field(Receipt, command, Command),
612
1003
  receipt_string_field(Receipt, outcome, Outcome),
613
1004
  receipt_string_field(Receipt, code_snapshot, CodeSnapshot),
614
1005
  receipt_string_field(Receipt, environment_hash, EnvironmentHash),
@@ -620,8 +1011,6 @@ receipt_evidence_dict(TestId, Scope, Receipt, State, AgeSeconds, Evidence) :-
620
1011
  state: State,
621
1012
  scope: Scope,
622
1013
  receiptId: ReceiptId,
623
- runner: Runner,
624
- command: Command,
625
1014
  outcome: Outcome,
626
1015
  codeSnapshot: CodeSnapshot,
627
1016
  environmentHash: EnvironmentHash,
@@ -662,7 +1051,7 @@ test_scope(Props, end_to_end) :-
662
1051
  !.
663
1052
  test_scope(_, unknown).
664
1053
 
665
- executable_symbol_stage([], _{status: blocked, symbols: [], missingTests: []}, []).
1054
+ executable_symbol_stage([], _{status: blocked, symbols: [], missingTests: [], coordinates: []}, []).
666
1055
  executable_symbol_stage(PassingE2eTests, Stage, Symbols) :-
667
1056
  PassingE2eTests \= [],
668
1057
  findall(SymbolId,
@@ -673,7 +1062,8 @@ executable_symbol_stage(PassingE2eTests, Stage, Symbols) :-
673
1062
  sort(Symbols0, Symbols),
674
1063
  include(test_missing_executable_symbol, PassingE2eTests, MissingTests),
675
1064
  (MissingTests = [] -> Status = passed ; Status = missing),
676
- Stage = _{status: Status, symbols: Symbols, missingTests: MissingTests}.
1065
+ maplist(symbol_coordinate_ref, Symbols, Coordinates),
1066
+ Stage = _{status: Status, symbols: Symbols, missingTests: MissingTests, coordinates: Coordinates}.
677
1067
 
678
1068
  test_missing_executable_symbol(TestId) :-
679
1069
  \+ (kb_relationship(executable_for, SymbolId, TestId), kb_entity(SymbolId, symbol, _)).
@@ -683,16 +1073,56 @@ production_symbol_stage(ReqId, PassingE2eTests, Stage, ProductionSymbols) :-
683
1073
  (kb_relationship(implements, SymbolId, ReqId),
684
1074
  kb_entity(SymbolId, symbol, _),
685
1075
  \+ kb:executable_test_symbol(SymbolId)),
686
- Production0),
687
- sort(Production0, ProductionSymbols),
1076
+ Implementing0),
1077
+ sort(Implementing0, ImplementingSymbols),
1078
+ include(type_shape_symbol_with_structural_contract, ImplementingSymbols, StructuralSymbols),
1079
+ exclude(type_shape_symbol_with_structural_contract, ImplementingSymbols, ProductionSymbols),
688
1080
  include(symbol_not_covered_by_tests(PassingE2eTests), ProductionSymbols, UncoveredSymbols),
689
- production_stage_status(ProductionSymbols, PassingE2eTests, UncoveredSymbols, Status),
690
- Stage = _{status: Status, symbols: ProductionSymbols, uncoveredSymbols: UncoveredSymbols}.
1081
+ production_stage_status(ProductionSymbols, StructuralSymbols, PassingE2eTests, UncoveredSymbols, Status, StatusReason),
1082
+ maplist(symbol_coordinate_ref, ProductionSymbols, Coordinates),
1083
+ StageBase = _{
1084
+ status: Status,
1085
+ symbols: ProductionSymbols,
1086
+ structuralSymbols: StructuralSymbols,
1087
+ uncoveredSymbols: UncoveredSymbols,
1088
+ coordinates: Coordinates
1089
+ },
1090
+ ( Status == passed
1091
+ -> Stage = StageBase
1092
+ ; put_dict(reason, StageBase, StatusReason, Stage)
1093
+ ).
691
1094
 
692
- production_stage_status([], _, _, missing) :- !.
693
- production_stage_status(_, [], _, blocked) :- !.
694
- production_stage_status(_, _, [], passed) :- !.
695
- production_stage_status(_, _, _, missing).
1095
+ type_shape_symbol_with_structural_contract(SymbolId) :-
1096
+ kb_entity(SymbolId, symbol, SymbolProps),
1097
+ memberchk(symbol_role=RawRole, SymbolProps),
1098
+ normalize_atom(RawRole, 'type-shape'),
1099
+ kb_relationship(covered_by, SymbolId, TestId),
1100
+ kb_entity(TestId, test, TestProps),
1101
+ test_scope(TestProps, unit),
1102
+ memberchk(status=RawStatus, TestProps),
1103
+ normalize_atom(RawStatus, Status),
1104
+ memberchk(Status, [active, passing]).
1105
+
1106
+ % Stage status plus a typed reason, from a single total clause so status and
1107
+ % reason can never disagree (the Align stale-receipts case reported `blocked`
1108
+ % with no indication that fresh receipts were the unblock). Clause order keeps
1109
+ % the historical precedence: no production symbols at all is `missing` even
1110
+ % when E2E evidence is also absent; only then does absent E2E evidence read
1111
+ % as `blocked`.
1112
+ production_stage_status(ProductionSymbols, StructuralSymbols, PassingE2eTests, UncoveredSymbols, Status, Reason) :-
1113
+ ( ProductionSymbols == [],
1114
+ StructuralSymbols == []
1115
+ -> Status = missing,
1116
+ Reason = "no production symbols implement this requirement; link at least one with implements"
1117
+ ; PassingE2eTests == []
1118
+ -> Status = blocked,
1119
+ Reason = "no passing E2E test evidence is available for the current snapshot; run kibi prove so this stage can evaluate production symbol coverage"
1120
+ ; UncoveredSymbols == []
1121
+ -> Status = passed
1122
+ ; Status = missing,
1123
+ length(UncoveredSymbols, Count),
1124
+ format(atom(Reason), "~w production symbol(s) lack covered_by links to the passing E2E tests", [Count])
1125
+ ).
696
1126
 
697
1127
  symbol_not_covered_by_tests(Tests, SymbolId) :-
698
1128
  \+ (member(TestId, Tests), kb_relationship(covered_by, SymbolId, TestId)).
@@ -707,11 +1137,15 @@ source_coordinate_stage(ReqProps, ExecutableSymbols, ProductionSymbols, Stage) :
707
1137
  ; RequirementSource = present,
708
1138
  coordinate_stage_status(Symbols, MissingSymbols, Status)
709
1139
  ),
1140
+ requirement_source_path(ReqProps, RequirementPath),
1141
+ maplist(symbol_coordinate_ref, Symbols, Coordinates),
710
1142
  Stage = _{
711
1143
  status: Status,
712
1144
  requirementSource: RequirementSource,
1145
+ requirementPath: RequirementPath,
713
1146
  symbols: Symbols,
714
- missingSymbols: MissingSymbols
1147
+ missingSymbols: MissingSymbols,
1148
+ coordinates: Coordinates
715
1149
  }.
716
1150
 
717
1151
  coordinate_stage_status([], _, blocked) :- !.
@@ -724,7 +1158,7 @@ symbol_missing_coordinates(SymbolId) :-
724
1158
 
725
1159
  valid_symbol_coordinates(Props) :-
726
1160
  memberchk(sourceFile=RawSourceFile, Props),
727
- normalize_atom(RawSourceFile, SourceFile),
1161
+ source_path_atom(RawSourceFile, SourceFile),
728
1162
  SourceFile \= '',
729
1163
  memberchk(sourceLine=RawLine, Props),
730
1164
  memberchk(sourceColumn=RawColumn, Props),
@@ -739,10 +1173,66 @@ valid_symbol_coordinates(Props) :-
739
1173
  EndLine >= Line,
740
1174
  EndColumn >= 0.
741
1175
 
742
- nonempty_source(Props) :-
1176
+ % Prefer sourceFile, then source, without URI-basename stripping. Paths must
1177
+ % remain inspectable coordinates for report linking.
1178
+ proof_source_property(Props, RawSource) :-
1179
+ memberchk(sourceFile=RawSource, Props),
1180
+ source_path_atom(RawSource, _),
1181
+ !.
1182
+ proof_source_property(Props, RawSource) :-
743
1183
  memberchk(source=RawSource, Props),
744
- normalize_atom(RawSource, Source),
745
- Source \= ''.
1184
+ source_path_atom(RawSource, _).
1185
+
1186
+ nonempty_source(Props) :-
1187
+ proof_source_property(Props, _).
1188
+
1189
+ requirement_source_path(Props, Path) :-
1190
+ proof_source_property(Props, RawSource),
1191
+ source_path_atom(RawSource, Path),
1192
+ !.
1193
+ requirement_source_path(_, '').
1194
+
1195
+ entity_source_path(Id, Path) :-
1196
+ kb_entity(Id, _, Props),
1197
+ requirement_source_path(Props, Path).
1198
+
1199
+ entity_source_ref(Id, _{id: Id, path: Path}) :-
1200
+ entity_source_path(Id, Path).
1201
+
1202
+ ground_source_refs(Evidence, Refs) :-
1203
+ findall(Ref,
1204
+ (member(Item, Evidence),
1205
+ entity_source_ref(Item.factId, Ref),
1206
+ Ref.path \= ''),
1207
+ Refs0),
1208
+ sort(Refs0, Refs).
1209
+
1210
+ symbol_coordinate_ref(SymbolId, Ref) :-
1211
+ kb_entity(SymbolId, symbol, Props),
1212
+ memberchk(sourceFile=RawFile, Props),
1213
+ source_path_atom(RawFile, File),
1214
+ File \= '',
1215
+ !,
1216
+ ( valid_symbol_coordinates(Props)
1217
+ -> memberchk(sourceLine=RawLine, Props),
1218
+ memberchk(sourceColumn=RawColumn, Props),
1219
+ memberchk(sourceEndLine=RawEndLine, Props),
1220
+ memberchk(sourceEndColumn=RawEndColumn, Props),
1221
+ normalize_integer(RawLine, Line),
1222
+ normalize_integer(RawColumn, Column),
1223
+ normalize_integer(RawEndLine, EndLine),
1224
+ normalize_integer(RawEndColumn, EndColumn),
1225
+ Ref = _{
1226
+ id: SymbolId,
1227
+ path: File,
1228
+ line: Line,
1229
+ column: Column,
1230
+ endLine: EndLine,
1231
+ endColumn: EndColumn
1232
+ }
1233
+ ; Ref = _{id: SymbolId, path: File}
1234
+ ).
1235
+ symbol_coordinate_ref(SymbolId, _{id: SymbolId, path: ''}).
746
1236
 
747
1237
  proof_status(Stages, unresolved) :-
748
1238
  Stages.contradictions.status == blocked,
@@ -762,7 +1252,36 @@ stage_dict(Stages, Stage) :-
762
1252
  member(_-Stage, Pairs).
763
1253
 
764
1254
  proof_gaps(Stages, Gaps) :-
765
- findall(Gap, (gap_definition(Gap, _, _, _), proof_gap_present(Gap, Stages)), Gaps).
1255
+ proof_issues(Stages, Gaps, _).
1256
+
1257
+ proof_advisories(Stages, Advisories) :-
1258
+ proof_issues(Stages, _, Advisories).
1259
+
1260
+ % implements REQ-kibi-conservative-requirement-proof
1261
+ % Every linked scenario is a mandatory proof obligation. Receipt-completeness
1262
+ % failures therefore remain blocking even when another scenario has a passing
1263
+ % receipt; unit/integration-only evidence stays nonblocking in the E2E lane.
1264
+ % Invariant: proofStatus == proven implies proofGaps == [].
1265
+ proof_issues(Stages, Gaps, Advisories) :-
1266
+ findall(Gap,
1267
+ (gap_definition(Gap, _, _, _),
1268
+ proof_gap_present(Gap, Stages),
1269
+ \+ proof_issue_advisory(Gap, Stages)),
1270
+ Gaps),
1271
+ findall(Gap,
1272
+ (gap_definition(Gap, _, _, _),
1273
+ proof_gap_present(Gap, Stages),
1274
+ proof_issue_advisory(Gap, Stages)),
1275
+ Advisories).
1276
+
1277
+ receipt_completeness_issue(missing_proof_receipt).
1278
+ receipt_completeness_issue(stale_proof_receipt).
1279
+ receipt_completeness_issue(failed_proof_receipt).
1280
+ receipt_completeness_issue(invalid_proof_receipt).
1281
+ receipt_completeness_issue(proof_contract_mismatch).
1282
+
1283
+ proof_issue_advisory(_Gap, _Stages) :-
1284
+ fail.
766
1285
 
767
1286
  proof_gap_present(missing_semantic_inventory, Stages) :- Stages.semanticInventory.propositionCount =:= 0.
768
1287
  proof_gap_present(incomplete_semantic_inventory, Stages) :- Stages.semanticInventory.missingCount > 0.
@@ -779,13 +1298,16 @@ proof_gap_present(contradiction_check_incomplete, Stages) :- Stages.contradictio
779
1298
  proof_gap_present(missing_scenario, Stages) :- Stages.scenarios.status == missing.
780
1299
  proof_gap_present(missing_scenario_test, Stages) :- Stages.scenarios.status == passed, Stages.scenarioTests.status == missing.
781
1300
  proof_gap_present(missing_passing_e2e, Stages) :- Stages.passingE2e.status == missing.
782
- proof_gap_present(missing_verification_receipt, Stages) :- Stages.passingE2e.missingReceiptTests \= [].
783
- proof_gap_present(stale_verification_receipt, Stages) :- Stages.passingE2e.staleReceiptTests \= [].
784
- proof_gap_present(failed_verification_receipt, Stages) :- Stages.passingE2e.failedReceiptTests \= [].
785
- proof_gap_present(invalid_verification_receipt, Stages) :- Stages.passingE2e.invalidReceiptTests \= [].
786
- proof_gap_present(verification_snapshot_unavailable, Stages) :- Stages.passingE2e.snapshotUnavailableTests \= [].
1301
+ proof_gap_present(missing_proof_receipt, Stages) :- Stages.passingE2e.missingReceiptTests \= [].
1302
+ proof_gap_present(stale_proof_receipt, Stages) :- Stages.passingE2e.staleReceiptTests \= [].
1303
+ proof_gap_present(failed_proof_receipt, Stages) :- Stages.passingE2e.failedReceiptTests \= [].
1304
+ proof_gap_present(invalid_proof_receipt, Stages) :- Stages.passingE2e.invalidReceiptTests \= [].
1305
+ proof_gap_present(proof_contract_mismatch, Stages) :- Stages.passingE2e.contractMismatchReceiptTests \= [].
1306
+ proof_gap_present(proof_snapshot_unavailable, Stages) :- Stages.passingE2e.snapshotUnavailableTests \= [].
787
1307
  proof_gap_present(missing_executable_test_symbol, Stages) :- Stages.executableSymbols.status == missing.
788
- proof_gap_present(missing_production_symbol, Stages) :- Stages.productionSymbols.symbols == [].
1308
+ proof_gap_present(missing_production_symbol, Stages) :-
1309
+ Stages.productionSymbols.symbols == [],
1310
+ Stages.productionSymbols.structuralSymbols == [].
789
1311
  proof_gap_present(missing_production_symbol_coverage, Stages) :- Stages.productionSymbols.uncoveredSymbols \= [].
790
1312
  proof_gap_present(missing_symbol_coordinates, Stages) :- Stages.sourceCoordinates.missingSymbols \= [].
791
1313
  proof_gap_present(missing_requirement_source, Stages) :- Stages.sourceCoordinates.requirementSource == missing.
@@ -803,14 +1325,15 @@ gap_definition(contradiction_check_incomplete, 41, contradictions, "Complete log
803
1325
  gap_definition(missing_scenario, 50, scenarios, "Add a specified_by scenario for the requirement.").
804
1326
  gap_definition(missing_scenario_test, 51, scenario_tests, "Link the scenario to a test with verified_by or validates.").
805
1327
  gap_definition(missing_passing_e2e, 52, passing_e2e, "Record fresh passing end-to-end evidence on a scenario-backed test.").
806
- gap_definition(missing_verification_receipt, 53, passing_e2e, "Record a kibi.verification-receipt.v1 result for the scenario-backed E2E test.").
807
- gap_definition(stale_verification_receipt, 54, passing_e2e, "Re-run the E2E test against the current code snapshot and append its receipt.").
808
- gap_definition(failed_verification_receipt, 55, passing_e2e, "Repair the failing E2E behavior and append a newer passing receipt for the same snapshot.").
809
- gap_definition(invalid_verification_receipt, 56, passing_e2e, "Repair malformed, future-dated, mismatched, or otherwise uncheckable receipt evidence.").
810
- gap_definition(verification_snapshot_unavailable, 57, passing_e2e, "Run coverage through a CLI or MCP runtime that exposes the deterministic workspace snapshot.").
1328
+ 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.").
1329
+ gap_definition(stale_proof_receipt, 54, passing_e2e, "Re-run kibi prove against the current code snapshot and append fresh receipts.").
1330
+ gap_definition(failed_proof_receipt, 55, passing_e2e, "Repair the failing proof obligation and append a newer passing receipt for the same snapshot.").
1331
+ gap_definition(invalid_proof_receipt, 56, passing_e2e, "Repair malformed, future-dated, mismatched, or otherwise uncheckable receipt evidence.").
1332
+ 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.").
1333
+ gap_definition(proof_snapshot_unavailable, 58, passing_e2e, "Run coverage through a CLI or MCP runtime that exposes the deterministic workspace snapshot.").
811
1334
  gap_definition(missing_executable_test_symbol, 60, executable_symbols, "Link executable test code to every qualifying E2E test with executable_for.").
812
1335
  gap_definition(missing_production_symbol, 70, production_symbols, "Link at least one production symbol to the requirement with implements.").
813
- gap_definition(missing_production_symbol_coverage, 71, production_symbols, "Link every implementing production symbol to a qualifying E2E test with covered_by.").
1336
+ 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.").
814
1337
  gap_definition(missing_symbol_coordinates, 80, source_coordinates, "Refresh and persist exact coordinates for every proof-bearing symbol.").
815
1338
  gap_definition(missing_requirement_source, 81, source_coordinates, "Bind the requirement to its current source document.").
816
1339
 
@@ -822,6 +1345,10 @@ proof_repairs(Gaps, Repairs) :-
822
1345
  normalize_atom(Value, Atom) :-
823
1346
  kb:normalize_term_atom(Value, Atom).
824
1347
 
1348
+ source_path_atom(Value, Atom) :-
1349
+ kb:source_value_atom(Value, Atom),
1350
+ Atom \= ''.
1351
+
825
1352
  normalize_atom_list(Value, Atoms) :-
826
1353
  kb:normalize_term_atom_list(Value, Atoms).
827
1354