kibi-core 0.27.0 → 0.29.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
 
@@ -155,12 +158,18 @@ compute_requirement_proof(ReqId, ReqProps, Context, Proof) :-
155
158
  PassingE2eStage.receiptEvidence, Context,
156
159
  ProductionStage, ProductionSymbols),
157
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),
158
164
  Stages = _{
159
165
  semanticInventory: SemanticStage,
160
166
  logicGrounding: LogicStage,
161
167
  contradictions: ContradictionStage,
162
168
  scenarios: ScenarioStage,
163
169
  scenarioTests: ScenarioTestStage,
170
+ negativePath: NegativePathStage,
171
+ dimension: DimensionStage,
172
+ cases: CasesStage,
164
173
  passingE2e: PassingE2eStage,
165
174
  executableSymbols: ExecutableStage,
166
175
  productionSymbols: ProductionStage,
@@ -688,6 +697,12 @@ scenario_stage(ReqId, Stage, ScenarioIds) :-
688
697
  unknown_feasibility_reason(Reason, ReasonText)
689
698
  ),
690
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),
691
706
  ( (ScenarioIds = [] ; InvalidScenarioTargets \= [])
692
707
  -> Status = missing
693
708
  ; InfeasibleScenarios \= []
@@ -702,6 +717,7 @@ scenario_stage(ReqId, Stage, ScenarioIds) :-
702
717
  invalidScenarioTargets: InvalidScenarioTargets,
703
718
  infeasibleScenarios: InfeasibleScenarios,
704
719
  unknownFeasibility: UnknownFeasibility,
720
+ negativeScenarios: NegativeScenarios,
705
721
  sources: Sources
706
722
  }.
707
723
 
@@ -716,6 +732,376 @@ unknown_feasibility_reason(undetermined_validity(_), undetermined_validity).
716
732
  existing_scenario(ScenarioId) :-
717
733
  kb_entity(ScenarioId, scenario, _).
718
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
+
719
1105
  scenario_test_stage(ScenarioIds, Stage, ScenarioTests) :-
720
1106
  maplist(scenario_test_obligation, ScenarioIds, Obligations),
721
1107
  findall(TestId,
@@ -2016,15 +2402,15 @@ receipt_claim_fields(Receipt, Evidence0, Evidence) :-
2016
2402
  claim_evidence(ReqId, Props, Inventory, ProofStatus, ScenarioTestStage, Context,
2017
2403
  ClaimEvidence, ProvedTests, Rejections) :-
2018
2404
  ( get_dict(tests, ScenarioTestStage, Tests) -> true ; Tests = [] ),
2019
- ( ProofStatus == tested
2020
- -> findall(TestId-Evidence,
2021
- (member(TestId, Tests),
2022
- test_receipt_evidence(Context, [unit, integration, end_to_end], TestId, Evidence),
2023
- get_dict(state, Evidence, passed),
2024
- get_dict(claimBindings, Evidence, _)),
2025
- BoundTests)
2026
- ; BoundTests = []
2027
- ),
2405
+ % The bindings are read whatever the proof status, so a claim that is not
2406
+ % proved because the requirement is not tested end to end still shows the
2407
+ % binding each receipt holds, and a rejected binding its reason.
2408
+ findall(TestId-Evidence,
2409
+ (member(TestId, Tests),
2410
+ test_receipt_evidence(Context, [unit, integration, end_to_end], TestId, Evidence),
2411
+ get_dict(state, Evidence, passed),
2412
+ get_dict(claimBindings, Evidence, _)),
2413
+ BoundTests),
2028
2414
  intent_claim_level(Props, IntentLevel),
2029
2415
  maplist(claim_entry(ReqId, ProofStatus, IntentLevel, BoundTests), Inventory, Entries0),
2030
2416
  exclude(==(none), Entries0, ClaimEvidence),
@@ -2072,17 +2458,25 @@ claim_entry(ReqId, ProofStatus, IntentLevel, BoundTests, InventoryEntry, Entry)
2072
2458
  ; Status \== modeled
2073
2459
  -> Entry = Base.put(_{applicable: true, level: IntentLevel, bindings: [],
2074
2460
  blocker: unresolved_semantic_proposition})
2075
- ; ProofStatus \== tested
2076
- -> Entry = Base.put(_{applicable: true, level: IntentLevel, bindings: []})
2077
2461
  ; findall(Binding, test_claim_binding(ReqId, Key, BoundTests, Binding), Bindings0),
2078
2462
  sort(Bindings0, Bindings),
2079
2463
  include(binding_is(rejected), Bindings, RejectedBindings),
2080
2464
  maplist(binding_rejection(Key), RejectedBindings, Rejections),
2081
- ( member(B, Bindings), get_dict(binding, B, derived), \+ get_dict(scope, B, intent)
2082
- -> Level = proved
2083
- ; Level = tested
2465
+ ( ProofStatus \== tested
2466
+ -> % A derived binding proves nothing until the requirement is
2467
+ % tested end to end; the claim keeps the intent level and
2468
+ % says why.
2469
+ Entry1 = Base.put(_{applicable: true, level: IntentLevel, bindings: Bindings}),
2470
+ ( member(B, Bindings), get_dict(binding, B, derived)
2471
+ -> Entry0 = Entry1.put(notProved, requirement_not_tested)
2472
+ ; Entry0 = Entry1
2473
+ )
2474
+ ; ( member(B, Bindings), get_dict(binding, B, derived), \+ get_dict(scope, B, intent)
2475
+ -> Level = proved
2476
+ ; Level = tested
2477
+ ),
2478
+ Entry0 = Base.put(_{applicable: true, level: Level, bindings: Bindings})
2084
2479
  ),
2085
- Entry0 = Base.put(_{applicable: true, level: Level, bindings: Bindings}),
2086
2480
  ( Rejections == []
2087
2481
  -> Entry = Entry0
2088
2482
  ; Entry = Entry0.put(rejections, Rejections)
@@ -2280,14 +2674,58 @@ freshness_gap(stale_proof_receipt).
2280
2674
  freshness_gap(proof_contract_mismatch).
2281
2675
 
2282
2676
  freshness_only_gaps(Gaps, Stages) :-
2283
- include(freshness_gap, Gaps, [_|_]),
2284
- exclude(freshness_gap, Gaps, Rest),
2677
+ % A stale certificate receipt of a unit-scope test is not a gap of its
2678
+ % own, so a stage that waits only on one counts as the freshness gap.
2679
+ ( include(freshness_gap, Gaps, [_|_])
2680
+ -> true
2681
+ ; negative_path_freshness_only(Stages)
2682
+ ; dimension_freshness_only(Stages)
2683
+ ; cases_freshness_only(Stages)
2684
+ ),
2685
+ exclude(freshness_gap, Gaps, Rest0),
2686
+ ( negative_path_freshness_only(Stages)
2687
+ -> exclude(==(happy_path_only), Rest0, Rest1)
2688
+ ; Rest1 = Rest0
2689
+ ),
2690
+ ( dimension_freshness_only(Stages)
2691
+ -> exclude(==(dimension_unrepresented), Rest1, Rest2)
2692
+ ; Rest2 = Rest1
2693
+ ),
2694
+ ( cases_freshness_only(Stages)
2695
+ -> exclude(==(uncovered_case), Rest2, Rest)
2696
+ ; Rest = Rest2
2697
+ ),
2285
2698
  ( Rest == []
2286
2699
  -> true
2287
2700
  ; Rest == [missing_production_symbol_coverage],
2288
2701
  production_coverage_freshness_only(Stages)
2289
2702
  ).
2290
2703
 
2704
+ % The failure side is open only because certificate receipts that prove
2705
+ % every open claim are stale.
2706
+ negative_path_freshness_only(Stages) :-
2707
+ get_dict(negativePath, Stages, Stage),
2708
+ Stage.status == missing,
2709
+ Stage.claims \== [],
2710
+ subtract(Stage.claims, Stage.pendingReproofClaims, []).
2711
+
2712
+ % Every open quantified claim waits only on a stale certificate receipt.
2713
+ dimension_freshness_only(Stages) :-
2714
+ get_dict(dimension, Stages, Stage),
2715
+ Stage.status == missing,
2716
+ findall(Key, (member(Claim, Stage.claims), get_dict(claimKey, Claim, Key)), Keys),
2717
+ Keys \== [],
2718
+ subtract(Keys, Stage.pendingReproofClaims, []).
2719
+
2720
+ % Every uncovered rejection case waits only on a stale certificate receipt.
2721
+ cases_freshness_only(Stages) :-
2722
+ get_dict(cases, Stages, Stage),
2723
+ Stage.status == missing,
2724
+ findall(Key, (member(Row, Stage.uncoveredCases), get_dict(claimKey, Row, Key)), Keys0),
2725
+ sort(Keys0, Keys),
2726
+ Keys \== [],
2727
+ subtract(Keys, Stage.pendingReproofClaims, []).
2728
+
2291
2729
  production_coverage_freshness_only(Stages) :-
2292
2730
  Stage = Stages.productionSymbols,
2293
2731
  forall(member(SymbolId, Stage.uncoveredSymbols),
@@ -2351,12 +2789,16 @@ prop_text(Props, Key, Text) :-
2351
2789
  normalize_atom(Raw, Text),
2352
2790
  Text \== ''.
2353
2791
 
2792
+ % The origin is JSON text, read whole: normalize_atom/2 keeps only what
2793
+ % follows the last "/", which cut an origin.ref quoting a URL down to a
2794
+ % fragment of the JSON.
2354
2795
  origin_field(Props, Key, Value) :-
2355
2796
  memberchk(origin=Raw, Props),
2356
- normalize_atom(Raw, Json),
2797
+ kb:source_value_atom(Raw, Json),
2357
2798
  catch(atom_json_dict(Json, Origin, [value_string_as(atom)]), _, fail),
2799
+ is_dict(Origin),
2358
2800
  get_dict(Key, Origin, Value0),
2359
- normalize_atom(Value0, Value),
2801
+ kb:source_value_atom(Value0, Value),
2360
2802
  Value \== ''.
2361
2803
 
2362
2804
  stage_dict(Stages, Stage) :-
@@ -2413,6 +2855,9 @@ proof_gap_present(infeasible_scenario, Stages) :- Stages.scenarios.infeasibleSce
2413
2855
  proof_gap_present(unknown_scenario_feasibility, Stages) :-
2414
2856
  get_dict(unknownFeasibility, Stages.scenarios, Unknown),
2415
2857
  Unknown \= [].
2858
+ proof_gap_present(happy_path_only, Stages) :- Stages.negativePath.status == missing.
2859
+ proof_gap_present(dimension_unrepresented, Stages) :- Stages.dimension.status == missing.
2860
+ proof_gap_present(uncovered_case, Stages) :- Stages.cases.status == missing.
2416
2861
  proof_gap_present(missing_scenario_test, Stages) :- memberchk(Stages.scenarios.status, [passed, blocked]), Stages.scenarioTests.status == missing.
2417
2862
  proof_gap_present(missing_passing_e2e, Stages) :- Stages.passingE2e.status == missing.
2418
2863
  proof_gap_present(missing_proof_receipt, Stages) :- Stages.passingE2e.missingReceiptTests \= [].
@@ -2442,6 +2887,9 @@ gap_definition(contradiction_check_incomplete, 41, contradictions, "Complete log
2442
2887
  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.").
2443
2888
  gap_definition(missing_scenario, 50, scenarios, "Add a specified_by scenario for the requirement.").
2444
2889
  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.").
2890
+ 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.").
2891
+ 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.").
2892
+ 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.").
2445
2893
  gap_definition(missing_scenario_test, 51, scenario_tests, "Link the scenario to a test with verified_by or validates.").
2446
2894
  gap_definition(missing_passing_e2e, 52, passing_e2e, "Record fresh passing end-to-end evidence on a scenario-backed test.").
2447
2895
  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.").
@@ -18,6 +18,7 @@ known_rule('must-priority-coverage').
18
18
  known_rule('symbol-coverage').
19
19
  known_rule('symbol-traceability').
20
20
  known_rule('symbol-granularity').
21
+ known_rule('symbol-coordinates-missing').
21
22
  known_rule('no-dangling-refs').
22
23
  known_rule('source-relationship-parity').
23
24
  known_rule('branch-store-not-compiled').
@@ -28,6 +29,7 @@ known_rule('deprecated-adr-no-successor').
28
29
  known_rule('superseded-requirement-open').
29
30
  known_rule('scenario-feasibility').
30
31
  known_rule('scenario-feasibility-unknown').
32
+ known_rule('scenario-outcome-mismatch').
31
33
  known_rule('exception-claim-keys').
32
34
  known_rule('numeric-string-value').
33
35
  known_rule('rule-key-arguments-missing').
@@ -76,6 +78,7 @@ rule_enforcement_class('must-priority-coverage', canonical).
76
78
  rule_enforcement_class('symbol-coverage', canonical).
77
79
  rule_enforcement_class('symbol-traceability', canonical).
78
80
  rule_enforcement_class('symbol-granularity', canonical).
81
+ rule_enforcement_class('symbol-coordinates-missing', advisory).
79
82
  rule_enforcement_class('no-dangling-refs', canonical).
80
83
  rule_enforcement_class('source-relationship-parity', canonical).
81
84
  rule_enforcement_class('branch-store-not-compiled', canonical).
@@ -86,6 +89,7 @@ rule_enforcement_class('deprecated-adr-no-successor', canonical).
86
89
  rule_enforcement_class('superseded-requirement-open', canonical).
87
90
  rule_enforcement_class('scenario-feasibility', canonical).
88
91
  rule_enforcement_class('scenario-feasibility-unknown', advisory).
92
+ rule_enforcement_class('scenario-outcome-mismatch', canonical).
89
93
  rule_enforcement_class('exception-claim-keys', canonical).
90
94
  rule_enforcement_class('numeric-string-value', advisory).
91
95
  rule_enforcement_class('rule-key-arguments-missing', advisory).
@@ -134,6 +138,7 @@ rule_implementation('must-priority-coverage', prolog).
134
138
  rule_implementation('symbol-coverage', prolog).
135
139
  rule_implementation('symbol-traceability', prolog).
136
140
  rule_implementation('symbol-granularity', typescript).
141
+ rule_implementation('symbol-coordinates-missing', typescript).
137
142
  rule_implementation('no-dangling-refs', prolog).
138
143
  rule_implementation('source-relationship-parity', typescript).
139
144
  rule_implementation('branch-store-not-compiled', typescript).
@@ -144,6 +149,7 @@ rule_implementation('deprecated-adr-no-successor', prolog).
144
149
  rule_implementation('superseded-requirement-open', prolog).
145
150
  rule_implementation('scenario-feasibility', prolog).
146
151
  rule_implementation('scenario-feasibility-unknown', prolog).
152
+ rule_implementation('scenario-outcome-mismatch', prolog).
147
153
  rule_implementation('exception-claim-keys', prolog).
148
154
  rule_implementation('numeric-string-value', prolog).
149
155
  rule_implementation('rule-key-arguments-missing', prolog).
@@ -198,6 +204,7 @@ rule_predicate('deprecated-adr-no-successor', check_deprecated_adrs).
198
204
  rule_predicate('superseded-requirement-open', check_superseded_requirement_open).
199
205
  rule_predicate('scenario-feasibility', check_scenario_feasibility).
200
206
  rule_predicate('scenario-feasibility-unknown', check_scenario_feasibility_unknown).
207
+ rule_predicate('scenario-outcome-mismatch', check_scenario_outcome_mismatch).
201
208
  rule_predicate('exception-claim-keys', check_exception_claim_keys).
202
209
  rule_predicate('numeric-string-value', check_numeric_string_values).
203
210
  rule_predicate('rule-key-arguments-missing', check_rule_key_arguments_missing).
@@ -235,6 +242,7 @@ rule_predicate_arity('deprecated-adr-no-successor', 1).
235
242
  rule_predicate_arity('superseded-requirement-open', 1).
236
243
  rule_predicate_arity('scenario-feasibility', 1).
237
244
  rule_predicate_arity('scenario-feasibility-unknown', 1).
245
+ rule_predicate_arity('scenario-outcome-mismatch', 1).
238
246
  rule_predicate_arity('exception-claim-keys', 1).
239
247
  rule_predicate_arity('numeric-string-value', 1).
240
248
  rule_predicate_arity('rule-key-arguments-missing', 1).
@@ -266,6 +274,7 @@ rule_description('must-priority-coverage', 'Every current must-priority requirem
266
274
  rule_description('symbol-coverage', 'Production symbols need qualifying coverage via covered_by plus a canonical requirement/scenario test path').
267
275
  rule_description('symbol-traceability', 'Production symbols must directly implement requirements for ownership; covered_by is coverage only and executable_for is test identity only').
268
276
  rule_description('symbol-granularity', 'A symbol with traceability relationships must not link its source file coarsely while narrower behavioral symbols are available, unless it carries an allowed granularity_reason; the staged gate\'s symbol_granularity_violation evaluated KB-wide').
277
+ rule_description('symbol-coordinates-missing', 'Advisory: an active symbol names a source file that exists, but Kibi could not place it there (the declaration was not found, the file is unreadable to the analyzer, or a declared plugin or grammar pack is missing), so its link is checked against no code').
269
278
  rule_description('no-dangling-refs', 'All relationship targets must exist in the KB').
270
279
  rule_description('source-relationship-parity', 'Authored Markdown and relationship-shard edges must exactly match the compiled KB').
271
280
  rule_description('branch-store-not-compiled', 'The current branch\'s KB store must be compiled from the authored .kb/ sources; a missing or empty store is reported once, naming kibi sync, instead of every store-backed rule reading an empty KB').
@@ -276,6 +285,7 @@ rule_description('deprecated-adr-no-successor', 'Deprecated ADRs must have a suc
276
285
  rule_description('superseded-requirement-open', 'A requirement that another requirement supersedes must have status closed, and supersedes links between requirements must not form a cycle').
277
286
  rule_description('scenario-feasibility', 'A scenario that expects success must not assume property values that cannot hold, alone or together, with what current requirements require through typed property values or rules, unless an approved exception requirement waives that requirement or the listed clauses').
278
287
  rule_description('scenario-feasibility-unknown', 'Advisory: a scenario that expects success assumes nothing, assumes values that contradict each other, assumes a property value no governing requirement constrains comparably (type, unit or operator mismatch, or conflicting requirements), leaves a governing rule\'s conditions undecided, or conflicts only with constraints whose validity window may not cover it, so its feasibility cannot be decided').
288
+ rule_description('scenario-outcome-mismatch', 'A scenario that expects rejection or error must assume a value or situation a current requirement forbids: when every assumption is governed by current requirements and they permit all of them together, the knowledge base predicts success and the scenario exercises no failure side').
279
289
  rule_description('exception-claim-keys', 'An exception requirement\'s exempts_claims must name claim keys of a requirement it exempts, and requires an exempts link').
280
290
  rule_description('numeric-string-value', 'Advisory: a property value stores a numeric-looking string where the comparison is numeric (ordering operator, or an int/number fact on the same property); the finding names the fact and the typed value, without converting it').
281
291
  rule_description('rule-key-arguments-missing', 'Advisory: an opposing rule pair stays unresolved only because a body predicate declares no key_arguments; the finding names the predicate and the key positions that would decide the pair').