kibi-core 0.26.1 → 0.28.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.
@@ -20,6 +20,8 @@
20
20
  :- use_module('checks.pl', [
21
21
  check_domain_contradictions_and_witnesses/2,
22
22
  infeasible_scenario/5,
23
+ negative_scenario_witness/3,
24
+ opposing_assumption/2,
23
25
  scenario_feasibility_outcome/2,
24
26
  check_rule_safety/1,
25
27
  check_rule_verifiability/1
@@ -28,6 +30,7 @@
28
30
  :- use_module('code_certificates.pl', [code_certificate_check/2, code_certificate_digest/2]).
29
31
  :- use_module('claim_obligations.pl', [claim_obligation/3, obligation_equal/2]).
30
32
  :- use_module('model_check.pl', [model_check_obligation/4]).
33
+ :- use_module('cases.pl', [fact_cases/3, case_covering_scenarios/3, case_dict/2, fact_case_waiver/2]).
31
34
 
32
35
  proof_version('kibi.requirement-proof.v5').
33
36
 
@@ -54,7 +57,13 @@ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds,
54
57
  (catch(parse_time(CheckedAtAtom, CheckedAtStamp), _, fail) -> true ; CheckedAtStamp = -1),
55
58
  normalize_integer(MaxAgeSeconds, MaxAge),
56
59
  normalize_atom(BindingMode, BindingModeAtom),
60
+ % Everything else in the context follows from the KB, so the proof memo
61
+ % key needs only the caller's parameters.
62
+ variant_sha1(params(Snapshot, CheckedAtAtom, MaxAge, BindingModeAtom, TestBindings), ParamsKey),
63
+ variant_sha1(findings(Contradictions, ContradictionWitnesses, UnsafeRules, UnverifiableRules), FindingsKey),
57
64
  Context = _{
65
+ proofMemoKey: ParamsKey,
66
+ ladderMemoKey: FindingsKey,
58
67
  contradictions: Contradictions,
59
68
  contradictionWitnesses: ContradictionWitnesses,
60
69
  unsafeRules: UnsafeRules,
@@ -68,35 +77,99 @@ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds,
68
77
  }.
69
78
 
70
79
  % implements REQ-kibi-conservative-requirement-proof
80
+ % A requirement's proof is kept per KB generation and proof parameters, so
81
+ % the evidence-floor rule, the coverage evidence and a coverage report in one
82
+ % check read each ladder once. Inside a transaction (what-if staging) the
83
+ % KB is not the generation's, so the proof is computed afresh.
84
+ requirement_proof(ReqId, ReqProps, Context, Proof) :-
85
+ ( \+ rdf_db:rdf_active_transaction(_),
86
+ get_dict(proofMemoKey, Context, ParamsKey),
87
+ kb:index_verification_key(requirement_proof, IndexKey)
88
+ -> Key = key(IndexKey, ParamsKey),
89
+ ( kb:kb_requirement_proof_memo(ReqId, Key, Proof0)
90
+ -> Proof = Proof0
91
+ ; compute_requirement_proof(ReqId, ReqProps, Context, Proof),
92
+ ( once(kb:kb_requirement_proof_memo(_, Stored, _)),
93
+ Stored \== Key
94
+ -> retractall(kb:kb_requirement_proof_memo(_, _, _))
95
+ ; true
96
+ ),
97
+ assertz(kb:kb_requirement_proof_memo(ReqId, Key, Proof))
98
+ )
99
+ ; compute_requirement_proof(ReqId, ReqProps, Context, Proof)
100
+ ).
101
+
102
+ % The stages before passing_e2e read the KB and the context's KB-derived
103
+ % findings (contradictions, unsafe and unverifiable rules) only, never the
104
+ % caller's snapshot, receipt bindings or checkedAt. A check passes a new
105
+ % checkedAt every run, so these stages are kept per KB generation and those
106
+ % findings apart from the full proof; inside a transaction they are computed
107
+ % afresh.
108
+ receipt_independent_stages(ReqId, ReqProps, Context, Ladder) :-
109
+ ( \+ rdf_db:rdf_active_transaction(_),
110
+ get_dict(ladderMemoKey, Context, FindingsKey),
111
+ kb:index_verification_key(requirement_ladder, IndexKey)
112
+ -> variant_sha1(ReqProps, PropsKey),
113
+ Key = key(IndexKey, FindingsKey, PropsKey),
114
+ ( kb:kb_requirement_ladder_memo(ReqId, Key, Ladder0)
115
+ -> Ladder = Ladder0
116
+ ; compute_receipt_independent_stages(ReqId, ReqProps, Context, Ladder),
117
+ ( once(kb:kb_requirement_ladder_memo(_, key(Stored, _, _), _)),
118
+ Stored \== IndexKey
119
+ -> retractall(kb:kb_requirement_ladder_memo(_, _, _))
120
+ ; true
121
+ ),
122
+ retractall(kb:kb_requirement_ladder_memo(ReqId, _, _)),
123
+ assertz(kb:kb_requirement_ladder_memo(ReqId, Key, Ladder))
124
+ )
125
+ ; compute_receipt_independent_stages(ReqId, ReqProps, Context, Ladder)
126
+ ).
127
+
128
+ compute_receipt_independent_stages(ReqId, ReqProps, Context,
129
+ ladder(SemanticStage, Inventory, LogicStage, ContradictionStage,
130
+ ScenarioStage, ScenarioTestStage, ScenarioTests, SymbolSets)) :-
131
+ semantic_inventory_stage(ReqProps, SemanticStage, Inventory),
132
+ logic_grounding_stage(ReqId, ReqProps, Inventory, Context, LogicStage),
133
+ contradiction_stage(ReqId, LogicStage.status, SemanticStage.status, Context, ContradictionStage),
134
+ scenario_stage(ReqId, ScenarioStage, ScenarioIds),
135
+ scenario_test_stage(ScenarioIds, ScenarioTestStage, ScenarioTests),
136
+ production_symbol_sets(ReqId, SymbolSets).
137
+
71
138
  % Exemption first: a current requirement explicitly marked proof_exempt with a
72
139
  % reason is intentionally outside E2E-proof scope and must never fall through
73
140
  % to the ladder (which would report misleading receipt gaps).
74
- requirement_proof(ReqId, ReqProps, _Context, Proof) :-
141
+ compute_requirement_proof(ReqId, ReqProps, _Context, Proof) :-
75
142
  kb:current_req(ReqId),
76
143
  proof_exempt(ReqProps, Reason),
77
144
  !,
78
145
  exempt_requirement_proof(Reason, ReqProps, Proof).
79
- requirement_proof(ReqId, ReqProps, _Context, Proof) :-
146
+ compute_requirement_proof(ReqId, ReqProps, _Context, Proof) :-
80
147
  \+ kb:current_req(ReqId),
81
148
  !,
82
149
  applicability_exclusion_reason(ReqId, Reason),
83
150
  exempt_requirement_proof(Reason, ReqProps, Proof).
84
- requirement_proof(ReqId, ReqProps, Context, Proof) :-
85
- semantic_inventory_stage(ReqProps, SemanticStage, Inventory),
86
- logic_grounding_stage(ReqId, ReqProps, Inventory, Context, LogicStage),
87
- contradiction_stage(ReqId, LogicStage.status, SemanticStage.status, Context, ContradictionStage),
88
- scenario_stage(ReqId, ScenarioStage, ScenarioIds),
89
- scenario_test_stage(ScenarioIds, ScenarioTestStage, ScenarioTests),
151
+ compute_requirement_proof(ReqId, ReqProps, Context, Proof) :-
152
+ receipt_independent_stages(ReqId, ReqProps, Context,
153
+ ladder(SemanticStage, Inventory, LogicStage, ContradictionStage,
154
+ ScenarioStage, ScenarioTestStage, ScenarioTests, SymbolSets)),
90
155
  passing_e2e_stage(ScenarioTestStage, Context, PassingE2eStage, PassingE2eTests),
91
156
  executable_symbol_stage(PassingE2eTests, ExecutableStage, ExecutableSymbols),
92
- production_symbol_stage(ReqId, PassingE2eTests, ScenarioTests, PassingE2eStage.receiptEvidence, Context, ProductionStage, ProductionSymbols),
157
+ production_symbol_stage_from_sets(ReqId, SymbolSets, PassingE2eTests, ScenarioTests,
158
+ PassingE2eStage.receiptEvidence, Context,
159
+ ProductionStage, ProductionSymbols),
93
160
  source_coordinate_stage(ReqProps, ExecutableSymbols, ProductionSymbols, CoordinateStage),
161
+ negative_path_stage(ReqId, ScenarioStage, ScenarioTestStage, Context, NegativePathStage),
162
+ dimension_stage(ReqId, ScenarioTestStage, Context, DimensionStage),
163
+ cases_stage(ReqId, ScenarioStage, ScenarioTestStage, Context, CasesStage),
94
164
  Stages = _{
95
165
  semanticInventory: SemanticStage,
96
166
  logicGrounding: LogicStage,
97
167
  contradictions: ContradictionStage,
98
168
  scenarios: ScenarioStage,
99
169
  scenarioTests: ScenarioTestStage,
170
+ negativePath: NegativePathStage,
171
+ dimension: DimensionStage,
172
+ cases: CasesStage,
100
173
  passingE2e: PassingE2eStage,
101
174
  executableSymbols: ExecutableStage,
102
175
  productionSymbols: ProductionStage,
@@ -624,6 +697,12 @@ scenario_stage(ReqId, Stage, ScenarioIds) :-
624
697
  unknown_feasibility_reason(Reason, ReasonText)
625
698
  ),
626
699
  UnknownFeasibility),
700
+ % implements REQ-kibi-happy-path-gap
701
+ % The linked scenarios that exercise this requirement's failure side: they
702
+ % expect rejection or error and assume a value it forbids.
703
+ findall(ScenarioId,
704
+ (member(ScenarioId, ScenarioIds), once(negative_scenario_witness(ScenarioId, ReqId, _))),
705
+ NegativeScenarios),
627
706
  ( (ScenarioIds = [] ; InvalidScenarioTargets \= [])
628
707
  -> Status = missing
629
708
  ; InfeasibleScenarios \= []
@@ -638,6 +717,7 @@ scenario_stage(ReqId, Stage, ScenarioIds) :-
638
717
  invalidScenarioTargets: InvalidScenarioTargets,
639
718
  infeasibleScenarios: InfeasibleScenarios,
640
719
  unknownFeasibility: UnknownFeasibility,
720
+ negativeScenarios: NegativeScenarios,
641
721
  sources: Sources
642
722
  }.
643
723
 
@@ -652,6 +732,376 @@ unknown_feasibility_reason(undetermined_validity(_), undetermined_validity).
652
732
  existing_scenario(ScenarioId) :-
653
733
  kb_entity(ScenarioId, scenario, _).
654
734
 
735
+ %% negative_path_stage(+ReqId, +ScenarioStage, +ScenarioTestStage, +Context, -Stage)
736
+ % implements REQ-kibi-happy-path-gap
737
+ % A requirement is not tested while only its happy path is: when it grounds
738
+ % a claim (a property value, predicate or rule), at least one linked
739
+ % scenario must expect rejection or error and assume a value or situation
740
+ % the requirement forbids. Claims a fresh certificate proves at code scope
741
+ % decide their failure side already and need none. Status is not_required
742
+ % (no such claim, or all certified), passed, waived (an approved
743
+ % failure_side_waiver) or missing.
744
+ negative_path_stage(ReqId, ScenarioStage, ScenarioTestStage, Context, Stage) :-
745
+ findall(Key-FactId,
746
+ ( negative_path_link(Link),
747
+ kb_relationship(Link, ReqId, FactId),
748
+ fact_claim_key_or_id(FactId, Key)
749
+ ),
750
+ ClaimFacts0),
751
+ sort(ClaimFacts0, ClaimFacts),
752
+ certified_claim_keys(ReqId, ScenarioTestStage, Context, Certified),
753
+ exclude(certified_claim(Certified), ClaimFacts, OpenFacts),
754
+ pairs_keys(OpenFacts, Open0),
755
+ sort(Open0, Open),
756
+ pending_certified_claim_keys(ScenarioTestStage, Context, Open, Pending),
757
+ ( get_dict(negativeScenarios, ScenarioStage, Negatives) -> true ; Negatives = [] ),
758
+ failure_side_waiver(ReqId, Waiver),
759
+ ( Open == []
760
+ -> Status = not_required
761
+ ; Negatives \== []
762
+ -> Status = passed
763
+ ; get_dict(approvedBy, Waiver, _)
764
+ -> Status = waived
765
+ ; Status = missing
766
+ ),
767
+ ( Status == missing
768
+ -> findall(_{claimKey: Key, factId: FactId, assumption: Assumption, expects: rejection},
769
+ (member(Key-FactId, OpenFacts), once(opposing_assumption(FactId, Assumption))),
770
+ Suggestions)
771
+ ; Suggestions = []
772
+ ),
773
+ Stage = _{
774
+ status: Status,
775
+ claims: Open,
776
+ certifiedClaims: Certified,
777
+ negativeScenarios: Negatives,
778
+ suggestedNegatives: Suggestions,
779
+ pendingReproofClaims: Pending,
780
+ waiver: Waiver
781
+ }.
782
+
783
+ % Open claims a certificate test proves (its contract lists them in proves)
784
+ % whose receipt is only older than the code: re-running kibi prove certifies
785
+ % them, so the evidence floor treats them like any other stale receipt.
786
+ pending_certified_claim_keys(ScenarioTestStage, Context, Open, Keys) :-
787
+ ( get_dict(tests, ScenarioTestStage, Tests) -> true ; Tests = [] ),
788
+ findall(Key,
789
+ ( member(TestId, Tests),
790
+ test_receipt_evidence(Context, [unit, integration, end_to_end], TestId, Evidence),
791
+ get_dict(state, Evidence, stale),
792
+ kb_entity(TestId, test, Props),
793
+ memberchk(proof_contract=RawContract, Props),
794
+ proof_contract_dict(RawContract, Contract),
795
+ get_dict(proves, Contract, Proves),
796
+ is_list(Proves),
797
+ member(RawKey, Proves),
798
+ normalize_atom(RawKey, Key),
799
+ memberchk(Key, Open)
800
+ ),
801
+ Keys0),
802
+ sort(Keys0, Keys).
803
+
804
+ %% failure_side_waiver(+ReqId, -Waiver)
805
+ % A requirement whose claims state a capability or an output shape has no
806
+ % input to refuse, so its failure side can be waived: failure_side_waiver
807
+ % gives the reason and failure_side_waiver_approved_by names the human who
808
+ % accepted it. Only an approved waiver satisfies the stage; a reason alone
809
+ % is reported as pending approval. Waiver is _{} when none is recorded.
810
+ failure_side_waiver(ReqId, Waiver) :-
811
+ ( kb_entity(ReqId, req, Props),
812
+ memberchk(failure_side_waiver=RawReason, Props),
813
+ normalize_atom(RawReason, Reason),
814
+ Reason \== ''
815
+ -> atom_string(Reason, ReasonText),
816
+ ( memberchk(failure_side_waiver_approved_by=RawBy, Props),
817
+ normalize_atom(RawBy, By),
818
+ By \== ''
819
+ -> atom_string(By, ByText),
820
+ Waiver = _{reason: ReasonText, approvedBy: ByText}
821
+ ; Waiver = _{reason: ReasonText}
822
+ )
823
+ ; Waiver = _{}
824
+ ).
825
+
826
+ certified_claim(Certified, Key-_) :- memberchk(Key, Certified).
827
+
828
+ %% cases_stage(+ReqId, +ScenarioStage, +ScenarioTestStage, +Context, -Stage)
829
+ % implements REQ-kibi-case-coverage
830
+ % Edge cases derived from the requirement's typed property claims
831
+ % (cases.pl): the bound and the value past it, zero, one and many for a
832
+ % count, each member of a set and a value outside it. A case is covered
833
+ % when a linked scenario, other than a draft, assumes a value in its class
834
+ % and expects the outcome the claim predicts. Every case whose predicted
835
+ % outcome is rejection must be covered: that is where a test of the one
836
+ % case that works hides the case that breaks. Uncovered success cases and
837
+ % cases no claim decides (unspecified) are reported, not blocking. Claims
838
+ % a fresh certificate proves at code scope are decided already. A grounding
839
+ % fact can carry a case_waiver with case_waiver_approved_by. Status is
840
+ % not_required (no rejection case), passed, waived (every uncovered
841
+ % rejection case is on a fact with an approved waiver) or missing.
842
+ cases_stage(ReqId, ScenarioStage, ScenarioTestStage, Context, Stage) :-
843
+ findall(FactId, kb_relationship(requires_property, ReqId, FactId), Facts0),
844
+ sort(Facts0, Facts),
845
+ certified_claim_keys(ReqId, ScenarioTestStage, Context, Certified),
846
+ ( get_dict(scenarios, ScenarioStage, ScenarioIds) -> true ; ScenarioIds = [] ),
847
+ foldl(fact_case_rows(Certified, ScenarioIds), Facts, rows([], false), rows(Rows0, LimitReached)),
848
+ reverse(Rows0, Rows),
849
+ include(case_row_blocking, Rows, Blocking),
850
+ partition(case_row_waived, Blocking, WaivedRows, OpenRows),
851
+ findall(Key, (member(Row, OpenRows), get_dict(claimKey, Row, Key)), OpenKeys0),
852
+ sort(OpenKeys0, OpenKeys),
853
+ pending_certified_claim_keys(ScenarioTestStage, Context, OpenKeys, Pending),
854
+ case_summary(Rows, Summary),
855
+ ( \+ (member(Row, Rows), get_dict(predicted, Row, rejection))
856
+ -> Status = not_required
857
+ ; OpenRows == [],
858
+ WaivedRows == []
859
+ -> Status = passed
860
+ ; OpenRows == []
861
+ -> Status = waived
862
+ ; Status = missing
863
+ ),
864
+ Stage = _{
865
+ status: Status,
866
+ cases: Rows,
867
+ uncoveredCases: OpenRows,
868
+ waivedCases: WaivedRows,
869
+ summary: Summary,
870
+ limitReached: LimitReached,
871
+ pendingReproofClaims: Pending
872
+ }.
873
+
874
+ fact_case_rows(Certified, ScenarioIds, FactId, rows(Acc0, Limit0), rows(Acc, Limit)) :-
875
+ ( fact_cases(FactId, Cases, FactLimit),
876
+ Cases = [case(_, ClaimKey, _, _, _, _, _)|_],
877
+ \+ memberchk(ClaimKey, Certified)
878
+ -> fact_case_waiver(FactId, Waiver),
879
+ foldl(case_row(ScenarioIds, Waiver), Cases, Acc0, Acc),
880
+ ( FactLimit == true -> Limit = true ; Limit = Limit0 )
881
+ ; Acc = Acc0,
882
+ Limit = Limit0
883
+ ).
884
+
885
+ case_row(ScenarioIds, Waiver, Case, Acc, [Row|Acc]) :-
886
+ case_dict(Case, Dict),
887
+ case_covering_scenarios(ScenarioIds, Case, Covering),
888
+ ( Covering \== []
889
+ -> Coverage = covered
890
+ ; get_dict(predicted, Dict, unspecified)
891
+ -> Coverage = unspecified
892
+ ; get_dict(approvedBy, Waiver, _)
893
+ -> Coverage = waived
894
+ ; Coverage = uncovered
895
+ ),
896
+ Row0 = Dict.put(_{coverage: Coverage, scenarios: Covering}),
897
+ ( Waiver == _{} -> Row = Row0 ; Row = Row0.put(waiver, Waiver) ).
898
+
899
+ % A rejection case blocks until a scenario covers it or its fact's waiver
900
+ % is approved.
901
+ case_row_blocking(Row) :-
902
+ get_dict(predicted, Row, rejection),
903
+ get_dict(coverage, Row, Coverage),
904
+ memberchk(Coverage, [uncovered, waived]).
905
+
906
+ case_row_waived(Row) :- get_dict(coverage, Row, waived).
907
+
908
+ case_summary(Rows, _{generated: Generated, covered: Covered, uncovered: Uncovered,
909
+ unspecified: Unspecified, waived: Waived}) :-
910
+ length(Rows, Generated),
911
+ coverage_count(Rows, covered, Covered),
912
+ coverage_count(Rows, uncovered, Uncovered),
913
+ coverage_count(Rows, unspecified, Unspecified),
914
+ coverage_count(Rows, waived, Waived).
915
+
916
+ coverage_count(Rows, Coverage, Count) :-
917
+ aggregate_all(count, (member(Row, Rows), get_dict(coverage, Row, Coverage)), Count).
918
+
919
+ %% dimension_stage(+ReqId, +ScenarioTestStage, +Context, -Stage)
920
+ % implements REQ-kibi-dimension-gap
921
+ % A claim that quantifies over a collection (each, every, all) or bounds a
922
+ % cardinality (at least, at most, exactly, no more than) must be grounded on what
923
+ % varies: a count or bound, a set, a rule over a variable or a state
924
+ % machine. A claim whose grounding facts are all booleans or ground
925
+ % predicates states the verdict, not the dimension, so a scenario with one
926
+ % file can pass where many files break. Claims a fresh certificate proves
927
+ % at code scope are decided already. A grounding fact can carry a
928
+ % dimension_waiver reason and a dimension_waiver_approved_by human for a
929
+ % quantifier word that does not range over anything ("without any network
930
+ % access"). Only, any and some are left out: they mostly scope a
931
+ % condition ("only when", "before any write") rather than range over one. Status is not_required (no such claim), waived (each one has an
932
+ % approved waiver) or missing.
933
+ dimension_stage(ReqId, ScenarioTestStage, Context, Stage) :-
934
+ findall(Key-FactId,
935
+ ( negative_path_link(Link),
936
+ kb_relationship(Link, ReqId, FactId),
937
+ fact_claim_key_or_id(FactId, Key)
938
+ ),
939
+ ClaimFacts0),
940
+ sort(ClaimFacts0, ClaimFacts),
941
+ pairs_keys(ClaimFacts, Keys0),
942
+ sort(Keys0, Keys),
943
+ certified_claim_keys(ReqId, ScenarioTestStage, Context, Certified),
944
+ findall(Claim,
945
+ ( member(Key, Keys),
946
+ \+ memberchk(Key, Certified),
947
+ findall(FactId, member(Key-FactId, ClaimFacts), FactIds),
948
+ unrepresented_quantified_claim(Key, FactIds, Claim)
949
+ ),
950
+ Claims),
951
+ partition(approved_dimension_waiver, Claims, Waived, Open),
952
+ findall(Key, (member(Claim, Open), get_dict(claimKey, Claim, Key)), OpenKeys),
953
+ pending_certified_claim_keys(ScenarioTestStage, Context, OpenKeys, Pending),
954
+ ( Claims == []
955
+ -> Status = not_required
956
+ ; Open == []
957
+ -> Status = waived
958
+ ; Status = missing
959
+ ),
960
+ Stage = _{
961
+ status: Status,
962
+ claims: Open,
963
+ waivedClaims: Waived,
964
+ pendingReproofClaims: Pending
965
+ }.
966
+
967
+ % A claim whose text quantifies and whose every grounding fact is a boolean
968
+ % property value or a ground predicate.
969
+ unrepresented_quantified_claim(Key, FactIds, Claim) :-
970
+ FactIds = [_|_],
971
+ forall(member(FactId, FactIds), dimensionless_fact(FactId)),
972
+ claim_text_of(FactIds, Text),
973
+ claim_quantifier(Text, Quantifier),
974
+ !,
975
+ quantifier_rewrites(Quantifier, Rewrites),
976
+ dimension_waiver(FactIds, Waiver),
977
+ atom_string(Quantifier, QuantifierText),
978
+ Claim = _{
979
+ claimKey: Key,
980
+ claimText: Text,
981
+ quantifier: QuantifierText,
982
+ factIds: FactIds,
983
+ rewrites: Rewrites,
984
+ waiver: Waiver
985
+ }.
986
+
987
+ dimensionless_fact(FactId) :-
988
+ kb_entity(FactId, fact, Props),
989
+ memberchk(fact_kind=RawKind, Props),
990
+ normalize_atom(RawKind, Kind),
991
+ ( Kind == predicate
992
+ -> true
993
+ ; Kind == property_value,
994
+ memberchk(value_type=RawType, Props),
995
+ normalize_atom(RawType, bool)
996
+ ).
997
+
998
+ claim_text_of(FactIds, Text) :-
999
+ member(FactId, FactIds),
1000
+ kb_entity(FactId, fact, Props),
1001
+ memberchk(claim_text=Raw, Props),
1002
+ normalize_atom(Raw, Atom),
1003
+ Atom \== '',
1004
+ !,
1005
+ atom_string(Atom, Text).
1006
+ claim_text_of(FactIds, Text) :-
1007
+ member(FactId, FactIds),
1008
+ kb_entity(FactId, fact, Props),
1009
+ memberchk(title=Raw, Props),
1010
+ !,
1011
+ normalize_atom(Raw, Atom),
1012
+ atom_string(Atom, Text).
1013
+
1014
+ %% claim_quantifier(+Text, -Quantifier)
1015
+ % The first quantifier over a collection (each, every, all) or cardinality
1016
+ % bound (exactly, at least, at most, no more than) in Text.
1017
+ claim_quantifier(Text, Quantifier) :-
1018
+ string_lower(Text, Lower),
1019
+ split_string(Lower, " \t\n\r.,;:()[]\"'`/!?-", " \t\n\r.,;:()[]\"'`/!?-", Words0),
1020
+ exclude(==(""), Words0, Words),
1021
+ quantifier_in(Words, Quantifier).
1022
+
1023
+ quantifier_in(["no", "more", "than"|_], no_more_than) :- !.
1024
+ quantifier_in(["at", "least"|_], at_least) :- !.
1025
+ quantifier_in(["at", "most"|_], at_most) :- !.
1026
+ quantifier_in([Word|_], Quantifier) :-
1027
+ memberchk(Word, ["each", "every", "all", "exactly"]),
1028
+ !,
1029
+ atom_string(Quantifier, Word).
1030
+ quantifier_in([_|Rest], Quantifier) :-
1031
+ quantifier_in(Rest, Quantifier).
1032
+
1033
+ % The typed groundings that represent what a quantifier ranges over, most
1034
+ % likely first.
1035
+ quantifier_rewrites(Quantifier, ["bound"]) :-
1036
+ memberchk(Quantifier, [at_least, at_most, exactly, no_more_than]),
1037
+ !.
1038
+ quantifier_rewrites(_, ["rule", "count", "set"]).
1039
+
1040
+ %% dimension_waiver(+FactIds, -Waiver)
1041
+ % A grounding fact's dimension_waiver reason and the human who approved it
1042
+ % in dimension_waiver_approved_by. An approved waiver on any grounding fact
1043
+ % wins; otherwise a reason alone is reported as pending approval. Waiver is
1044
+ % _{} when none is recorded.
1045
+ dimension_waiver(FactIds, Waiver) :-
1046
+ findall(W, (member(FactId, FactIds), fact_dimension_waiver(FactId, W)), Waivers),
1047
+ ( member(W, Waivers), get_dict(approvedBy, W, _)
1048
+ -> Waiver = W
1049
+ ; Waivers = [W|_]
1050
+ -> Waiver = W
1051
+ ; Waiver = _{}
1052
+ ).
1053
+
1054
+ fact_dimension_waiver(FactId, Waiver) :-
1055
+ kb_entity(FactId, fact, Props),
1056
+ memberchk(dimension_waiver=RawReason, Props),
1057
+ normalize_atom(RawReason, Reason),
1058
+ Reason \== '',
1059
+ atom_string(Reason, ReasonText),
1060
+ ( memberchk(dimension_waiver_approved_by=RawBy, Props),
1061
+ normalize_atom(RawBy, By),
1062
+ By \== ''
1063
+ -> atom_string(By, ByText),
1064
+ Waiver = _{reason: ReasonText, approvedBy: ByText}
1065
+ ; Waiver = _{reason: ReasonText}
1066
+ ).
1067
+
1068
+ approved_dimension_waiver(Claim) :-
1069
+ get_dict(approvedBy, Claim.waiver, _).
1070
+
1071
+ negative_path_link(requires_property).
1072
+ negative_path_link(requires_predicate).
1073
+ negative_path_link(requires_rule).
1074
+
1075
+ fact_claim_key_or_id(FactId, Key) :-
1076
+ ( kb_entity(FactId, fact, Props),
1077
+ memberchk(claim_key=Raw, Props),
1078
+ normalize_atom(Raw, Key0),
1079
+ Key0 \== ''
1080
+ -> Key = Key0
1081
+ ; Key = FactId
1082
+ ).
1083
+
1084
+ % Claims of ReqId a passed receipt binds with a derived obligation outside
1085
+ % intent scope: a certificate decides them, both sides.
1086
+ certified_claim_keys(ReqId, ScenarioTestStage, Context, Keys) :-
1087
+ ( get_dict(tests, ScenarioTestStage, Tests) -> true ; Tests = [] ),
1088
+ findall(Key,
1089
+ ( member(TestId, Tests),
1090
+ test_receipt_evidence(Context, [unit, integration, end_to_end], TestId, Evidence),
1091
+ get_dict(state, Evidence, passed),
1092
+ get_dict(claimBindings, Evidence, Recorded),
1093
+ member(Entry, Recorded),
1094
+ inventory_entry_field(Entry, requirement_id, RawReq),
1095
+ normalize_atom(RawReq, ReqId),
1096
+ inventory_entry_field(Entry, claim_key, RawKey),
1097
+ normalize_atom(RawKey, Key),
1098
+ test_claim_binding(ReqId, Key, [TestId-Evidence], Binding),
1099
+ get_dict(binding, Binding, derived),
1100
+ \+ get_dict(scope, Binding, intent)
1101
+ ),
1102
+ Keys0),
1103
+ sort(Keys0, Keys).
1104
+
655
1105
  scenario_test_stage(ScenarioIds, Stage, ScenarioTests) :-
656
1106
  maplist(scenario_test_obligation, ScenarioIds, Obligations),
657
1107
  findall(TestId,
@@ -1211,6 +1661,11 @@ canonical_json_value(Value, Json) :-
1211
1661
  maplist(canonical_json_value, Value, Parts),
1212
1662
  atomics_to_string(Parts, ",", Body),
1213
1663
  format(string(Json), "[~s]", [Body]).
1664
+ % JSON true, false and null read back as atoms; json_write would quote them,
1665
+ % so the hash would differ from the one the CLI recorded at ingest.
1666
+ canonical_json_value(Value, Json) :-
1667
+ memberchk(Value, [true, false, null]), !,
1668
+ atom_string(Value, Json).
1214
1669
  canonical_json_value(Value, Json) :-
1215
1670
  with_output_to(string(Json), json_write(current_output, Value, [width(0)])).
1216
1671
 
@@ -1375,6 +1830,14 @@ production_symbol_stage(ReqId, PassingE2eTests, Stage, ProductionSymbols) :-
1375
1830
  production_symbol_stage(ReqId, PassingE2eTests, ScenarioTests, [], Context, Stage, ProductionSymbols).
1376
1831
 
1377
1832
  production_symbol_stage(ReqId, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, Stage, ProductionSymbols) :-
1833
+ production_symbol_sets(ReqId, Sets),
1834
+ production_symbol_stage_from_sets(ReqId, Sets, PassingE2eTests, ScenarioTests,
1835
+ ReceiptEvidence, Context, Stage, ProductionSymbols).
1836
+
1837
+ % The implementing symbols of a requirement, split into structural
1838
+ % (type-shape with a unit-test contract) and production ones, with the
1839
+ % production symbols' coordinates: all read from the KB alone.
1840
+ production_symbol_sets(ReqId, symbol_sets(StructuralSymbols, ProductionSymbols, Coordinates)) :-
1378
1841
  findall(SymbolId,
1379
1842
  (kb_relationship(implements, SymbolId, ReqId),
1380
1843
  kb_entity(SymbolId, symbol, _),
@@ -1383,13 +1846,17 @@ production_symbol_stage(ReqId, PassingE2eTests, ScenarioTests, ReceiptEvidence,
1383
1846
  sort(Implementing0, ImplementingSymbols),
1384
1847
  include(type_shape_symbol_with_structural_contract, ImplementingSymbols, StructuralSymbols),
1385
1848
  exclude(type_shape_symbol_with_structural_contract, ImplementingSymbols, ProductionSymbols),
1849
+ maplist(symbol_coordinate_ref, ProductionSymbols, Coordinates).
1850
+
1851
+ production_symbol_stage_from_sets(ReqId, symbol_sets(StructuralSymbols, ProductionSymbols, Coordinates),
1852
+ PassingE2eTests, ScenarioTests, ReceiptEvidence, Context,
1853
+ Stage, ProductionSymbols) :-
1386
1854
  include(symbol_not_covered_by_tests(PassingE2eTests), ProductionSymbols, UncoveredProduction),
1387
1855
  structural_unit_receipts(StructuralSymbols, Context, UnitReceipts),
1388
1856
  exclude(structural_symbol_covered(PassingE2eTests, UnitReceipts), StructuralSymbols, UncoveredStructural),
1389
1857
  append(UncoveredProduction, UncoveredStructural, UncoveredSymbols),
1390
1858
  production_stage_status(ProductionSymbols, StructuralSymbols, PassingE2eTests,
1391
1859
  UncoveredProduction, UncoveredStructural, Status, StatusReason),
1392
- maplist(symbol_coordinate_ref, ProductionSymbols, Coordinates),
1393
1860
  production_coverage_explanations(
1394
1861
  ReqId, ProductionSymbols, StructuralSymbols, UncoveredSymbols,
1395
1862
  PassingE2eTests, ScenarioTests, ReceiptEvidence, UnitReceipts-Context, Explanations),
@@ -2199,14 +2666,58 @@ freshness_gap(stale_proof_receipt).
2199
2666
  freshness_gap(proof_contract_mismatch).
2200
2667
 
2201
2668
  freshness_only_gaps(Gaps, Stages) :-
2202
- include(freshness_gap, Gaps, [_|_]),
2203
- exclude(freshness_gap, Gaps, Rest),
2669
+ % A stale certificate receipt of a unit-scope test is not a gap of its
2670
+ % own, so a stage that waits only on one counts as the freshness gap.
2671
+ ( include(freshness_gap, Gaps, [_|_])
2672
+ -> true
2673
+ ; negative_path_freshness_only(Stages)
2674
+ ; dimension_freshness_only(Stages)
2675
+ ; cases_freshness_only(Stages)
2676
+ ),
2677
+ exclude(freshness_gap, Gaps, Rest0),
2678
+ ( negative_path_freshness_only(Stages)
2679
+ -> exclude(==(happy_path_only), Rest0, Rest1)
2680
+ ; Rest1 = Rest0
2681
+ ),
2682
+ ( dimension_freshness_only(Stages)
2683
+ -> exclude(==(dimension_unrepresented), Rest1, Rest2)
2684
+ ; Rest2 = Rest1
2685
+ ),
2686
+ ( cases_freshness_only(Stages)
2687
+ -> exclude(==(uncovered_case), Rest2, Rest)
2688
+ ; Rest = Rest2
2689
+ ),
2204
2690
  ( Rest == []
2205
2691
  -> true
2206
2692
  ; Rest == [missing_production_symbol_coverage],
2207
2693
  production_coverage_freshness_only(Stages)
2208
2694
  ).
2209
2695
 
2696
+ % The failure side is open only because certificate receipts that prove
2697
+ % every open claim are stale.
2698
+ negative_path_freshness_only(Stages) :-
2699
+ get_dict(negativePath, Stages, Stage),
2700
+ Stage.status == missing,
2701
+ Stage.claims \== [],
2702
+ subtract(Stage.claims, Stage.pendingReproofClaims, []).
2703
+
2704
+ % Every open quantified claim waits only on a stale certificate receipt.
2705
+ dimension_freshness_only(Stages) :-
2706
+ get_dict(dimension, Stages, Stage),
2707
+ Stage.status == missing,
2708
+ findall(Key, (member(Claim, Stage.claims), get_dict(claimKey, Claim, Key)), Keys),
2709
+ Keys \== [],
2710
+ subtract(Keys, Stage.pendingReproofClaims, []).
2711
+
2712
+ % Every uncovered rejection case waits only on a stale certificate receipt.
2713
+ cases_freshness_only(Stages) :-
2714
+ get_dict(cases, Stages, Stage),
2715
+ Stage.status == missing,
2716
+ findall(Key, (member(Row, Stage.uncoveredCases), get_dict(claimKey, Row, Key)), Keys0),
2717
+ sort(Keys0, Keys),
2718
+ Keys \== [],
2719
+ subtract(Keys, Stage.pendingReproofClaims, []).
2720
+
2210
2721
  production_coverage_freshness_only(Stages) :-
2211
2722
  Stage = Stages.productionSymbols,
2212
2723
  forall(member(SymbolId, Stage.uncoveredSymbols),
@@ -2270,12 +2781,16 @@ prop_text(Props, Key, Text) :-
2270
2781
  normalize_atom(Raw, Text),
2271
2782
  Text \== ''.
2272
2783
 
2784
+ % The origin is JSON text, read whole: normalize_atom/2 keeps only what
2785
+ % follows the last "/", which cut an origin.ref quoting a URL down to a
2786
+ % fragment of the JSON.
2273
2787
  origin_field(Props, Key, Value) :-
2274
2788
  memberchk(origin=Raw, Props),
2275
- normalize_atom(Raw, Json),
2789
+ kb:source_value_atom(Raw, Json),
2276
2790
  catch(atom_json_dict(Json, Origin, [value_string_as(atom)]), _, fail),
2791
+ is_dict(Origin),
2277
2792
  get_dict(Key, Origin, Value0),
2278
- normalize_atom(Value0, Value),
2793
+ kb:source_value_atom(Value0, Value),
2279
2794
  Value \== ''.
2280
2795
 
2281
2796
  stage_dict(Stages, Stage) :-
@@ -2332,6 +2847,9 @@ proof_gap_present(infeasible_scenario, Stages) :- Stages.scenarios.infeasibleSce
2332
2847
  proof_gap_present(unknown_scenario_feasibility, Stages) :-
2333
2848
  get_dict(unknownFeasibility, Stages.scenarios, Unknown),
2334
2849
  Unknown \= [].
2850
+ proof_gap_present(happy_path_only, Stages) :- Stages.negativePath.status == missing.
2851
+ proof_gap_present(dimension_unrepresented, Stages) :- Stages.dimension.status == missing.
2852
+ proof_gap_present(uncovered_case, Stages) :- Stages.cases.status == missing.
2335
2853
  proof_gap_present(missing_scenario_test, Stages) :- memberchk(Stages.scenarios.status, [passed, blocked]), Stages.scenarioTests.status == missing.
2336
2854
  proof_gap_present(missing_passing_e2e, Stages) :- Stages.passingE2e.status == missing.
2337
2855
  proof_gap_present(missing_proof_receipt, Stages) :- Stages.passingE2e.missingReceiptTests \= [].
@@ -2361,6 +2879,9 @@ gap_definition(contradiction_check_incomplete, 41, contradictions, "Complete log
2361
2879
  gap_definition(unknown_scenario_feasibility, 49, scenarios, "Link the property values the success scenario relies on with assumes facts that a current requirement constrains, so its feasibility can be decided.").
2362
2880
  gap_definition(missing_scenario, 50, scenarios, "Add a specified_by scenario for the requirement.").
2363
2881
  gap_definition(infeasible_scenario, 49, scenarios, "Set expects: rejection on the scenario, correct its assumes fact, or record a human-approved exception requirement that exempts the forbidding requirement.").
2882
+ gap_definition(dimension_unrepresented, 47, dimension, "Ground the quantified claim on what varies: an integer count or bound, a set of members, a rule over a variable, or a state machine. If the quantifier word ranges over nothing, record a dimension_waiver on the grounding fact for a person to approve, and record their approval in the same pull request.").
2883
+ gap_definition(uncovered_case, 46, cases, "Cover each rejection case: add a scenario that assumes a value in the case's class (the case's assumption), expects rejection or error, is linked with specified_by and is verified by a passing end-to-end test. If a person accepts the cases as covered another way, record a case_waiver on the grounding fact for them to approve, and record their approval in the same pull request.").
2884
+ gap_definition(happy_path_only, 48, negative_path, "Add a scenario that expects rejection or error and assumes a value this requirement forbids, link it with specified_by, and verify it with a passing end-to-end test.").
2364
2885
  gap_definition(missing_scenario_test, 51, scenario_tests, "Link the scenario to a test with verified_by or validates.").
2365
2886
  gap_definition(missing_passing_e2e, 52, passing_e2e, "Record fresh passing end-to-end evidence on a scenario-backed test.").
2366
2887
  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.").