kibi-core 0.14.0 → 0.15.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.
package/src/checks.pl CHANGED
@@ -15,9 +15,24 @@
15
15
  check_no_cycles/1, % Returns list of cycle violations
16
16
  check_required_fields/1, % Returns list of missing required field violations
17
17
  check_deprecated_adrs/1, % Returns list of deprecated ADR violations
18
+ check_superseded_requirement_open/1,
19
+ check_symbol_owner_superseded/1,
20
+ check_adr_unlinked/1,
21
+ check_adr_proposed/1,
22
+ check_scenario_feasibility/1,
23
+ check_scenario_feasibility_unknown/1,
24
+ check_exception_claim_keys/1,
25
+ check_numeric_string_values/1,
26
+ check_rule_key_arguments_missing/1,
27
+ infeasible_scenario/5,
28
+ scenario_feasibility_outcome/2,
18
29
  check_domain_contradictions/1, % Returns list of contradiction violations
19
30
  check_domain_contradictions_and_witnesses/2,
20
31
  check_domain_contradiction_witnesses/1,
32
+ what_if_contradiction_witnesses/2,
33
+ what_if_contradiction_witnesses_json/2,
34
+ what_if_analysis/2,
35
+ what_if_analysis_json/2,
21
36
  check_strict_fact_shape/1, % Returns list of malformed strict fact violations
22
37
  check_strict_req_fact_pairing/1,% Returns list of malformed strict req/fact pairing violations
23
38
  check_strict_readiness/1, % Returns list of strict readiness audit violations
@@ -43,6 +58,7 @@
43
58
  :- use_module(library(http/json)).
44
59
  :- use_module(library(http/json_convert)).
45
60
  :- use_module('kb.pl').
61
+ :- use_module('intervals.pl', [numeric_constraints_satisfiable/2]).
46
62
  :- use_module('../schema/entities.pl', [entity_type/1, required_property/2]).
47
63
  :- use_module('../schema/relationships.pl', [relationship_type/1]).
48
64
  :- use_module('semantic_quality.pl', [
@@ -54,7 +70,7 @@
54
70
  check_subject_key_shape/1,
55
71
  check_ontology_quality/1
56
72
  ]).
57
- :- use_module('logic_ir.pl', [logic_rule_safety/2, logic_rule_from_props/2, logic_rule_conflict/3, logic_rule_conflict_witness/3, logic_rules_stratified/1]).
73
+ :- use_module('logic_ir.pl', [logic_rule_safety/2, logic_rule_from_props/2, logic_rule_render/2, logic_rule_conflict/3, logic_rule_conflict_witness/3, logic_rules_stratified/1, logic_rule_missing_keys/4, functional_predicate_declarations/1, logic_rule_property_form/2]).
58
74
  % GENERATED registry facts; single source is schema/rule-registry.json.
59
75
  :- use_module('rule_registry.pl', [known_rule/1]).
60
76
 
@@ -64,7 +80,8 @@ required_fields([id, title, status, created_at, updated_at, source]).
64
80
  % Relationship types to check for dangling references
65
81
  all_relationship_types([
66
82
  depends_on, verified_by, validates, specified_by,
67
- constrains, requires_property, requires_predicate, requires_rule, supersedes, restates, relates_to
83
+ constrains, requires_property, requires_predicate, requires_rule, supersedes, restates, relates_to,
84
+ assumes, exempts
68
85
  ]).
69
86
 
70
87
  %% check_all(-ViolationsDict)
@@ -79,7 +96,16 @@ check_all(ViolationsDict) :-
79
96
  check_no_cycles(Cycles),
80
97
  check_required_fields(RequiredFields),
81
98
  check_deprecated_adrs(DeprecatedADRs),
99
+ check_superseded_requirement_open(SupersededRequirementOpen),
100
+ check_symbol_owner_superseded(SymbolOwnerSuperseded),
101
+ check_adr_unlinked(AdrUnlinked),
102
+ check_adr_proposed(AdrProposed),
103
+ check_scenario_feasibility(ScenarioFeasibility),
104
+ check_scenario_feasibility_unknown(ScenarioFeasibilityUnknown),
105
+ check_exception_claim_keys(ExceptionClaimKeys),
106
+ check_numeric_string_values(NumericStringValues),
82
107
  check_domain_contradictions(Contradictions),
108
+ check_rule_key_arguments_missing(RuleKeyArgumentsMissing),
83
109
  check_strict_fact_shape(StrictFactShape),
84
110
  check_strict_req_fact_pairing(StrictReqFactPairing),
85
111
  check_strict_readiness(StrictReadiness),
@@ -104,7 +130,16 @@ check_all(ViolationsDict) :-
104
130
  no_cycles: Cycles,
105
131
  required_fields: RequiredFields,
106
132
  deprecated_adr_no_successor: DeprecatedADRs,
133
+ superseded_requirement_open: SupersededRequirementOpen,
134
+ symbol_owner_superseded: SymbolOwnerSuperseded,
135
+ adr_unlinked: AdrUnlinked,
136
+ adr_proposed: AdrProposed,
137
+ scenario_feasibility: ScenarioFeasibility,
138
+ scenario_feasibility_unknown: ScenarioFeasibilityUnknown,
139
+ exception_claim_keys: ExceptionClaimKeys,
140
+ numeric_string_value: NumericStringValues,
107
141
  domain_contradictions: Contradictions,
142
+ rule_key_arguments_missing: RuleKeyArgumentsMissing,
108
143
  strict_fact_shape: StrictFactShape,
109
144
  strict_req_fact_pairing: StrictReqFactPairing,
110
145
  strict_readiness: StrictReadiness,
@@ -524,6 +559,1586 @@ deprecated_adr_violation(violation(
524
559
  ; Source = ""
525
560
  ).
526
561
 
562
+ %% ------------------------------------------------------------------
563
+ %% Lifecycle: superseded requirements, retired owners, ADR drift
564
+ %% ------------------------------------------------------------------
565
+
566
+ %% check_superseded_requirement_open(-Violations)
567
+ % implements REQ-core-validation-rules, REQ-kibi-kb-lifecycle-integrity
568
+ % A requirement that another requirement supersedes is retired, so it must
569
+ % carry the terminal status `closed`; an open superseded requirement reads as
570
+ % current to every agent that finds it. Supersession must also be acyclic:
571
+ % when requirements supersede each other, directly or through a chain, none of
572
+ % them is current and no status edit can decide which one should be. Each
573
+ % such cycle is reported once, naming every member, and its members are not
574
+ % reported again as open. `no-cycles` only follows depends_on, so cycles are
575
+ % reported here.
576
+ check_superseded_requirement_open(Violations) :-
577
+ requirement_supersession_edges(Edges),
578
+ supersession_cycles(Edges, Cycles),
579
+ append(Cycles, CycleMembers0),
580
+ sort(CycleMembers0, CycleMembers),
581
+ findall(To, member(_-To, Edges), Targets0),
582
+ sort(Targets0, Targets),
583
+ findall(
584
+ Violation,
585
+ superseded_open_violation(Edges, Targets, CycleMembers, Violation),
586
+ OpenViolations
587
+ ),
588
+ maplist(supersession_cycle_violation(Edges), Cycles, CycleViolations),
589
+ append(OpenViolations, CycleViolations, Violations0),
590
+ sort(Violations0, Violations).
591
+
592
+ %% requirement_supersession_edges(-Edges)
593
+ % Sorted From-To pairs of supersedes edges whose target is a requirement.
594
+ requirement_supersession_edges(Edges) :-
595
+ findall(
596
+ From-To,
597
+ ( kb_relationship(supersedes, From, To),
598
+ kb_entity(To, req, _)
599
+ ),
600
+ Edges0
601
+ ),
602
+ sort(Edges0, Edges).
603
+
604
+ %% supersession_cycles(+Edges, -Cycles)
605
+ % Every set of requirements that reach each other through supersedes edges
606
+ % (a strongly connected component with a cycle), as sorted member lists.
607
+ supersession_cycles(Edges, Cycles) :-
608
+ findall(From, member(From-_, Edges), Starts0),
609
+ sort(Starts0, Starts),
610
+ findall(
611
+ Component,
612
+ ( member(Start, Starts),
613
+ supersession_reachable(Edges, Start, Reach),
614
+ ord_memberchk(Start, Reach),
615
+ include(supersession_reaches(Edges, Start), Reach, Component0),
616
+ sort(Component0, Component)
617
+ ),
618
+ Components
619
+ ),
620
+ sort(Components, Cycles).
621
+
622
+ % True when Node reaches Target through supersedes edges.
623
+ supersession_reaches(Edges, Target, Node) :-
624
+ supersession_reachable(Edges, Node, Reach),
625
+ ord_memberchk(Target, Reach).
626
+
627
+ %% supersession_reachable(+Edges, +Start, -Reach)
628
+ % Ordered set of nodes reachable from Start in one or more steps.
629
+ supersession_reachable(Edges, Start, Reach) :-
630
+ supersession_successors(Edges, Start, Next),
631
+ supersession_walk(Edges, Next, [], Reach).
632
+
633
+ supersession_walk(_, [], Reach, Reach).
634
+ supersession_walk(Edges, [Node|Queue], Seen, Reach) :-
635
+ ( ord_memberchk(Node, Seen)
636
+ -> supersession_walk(Edges, Queue, Seen, Reach)
637
+ ; ord_add_element(Seen, Node, Seen1),
638
+ supersession_successors(Edges, Node, Next),
639
+ append(Queue, Next, Queue1),
640
+ supersession_walk(Edges, Queue1, Seen1, Reach)
641
+ ).
642
+
643
+ supersession_successors(Edges, Node, Next) :-
644
+ findall(To, member(Node-To, Edges), Next).
645
+
646
+ superseded_open_violation(Edges, Targets, CycleMembers, violation(
647
+ 'superseded-requirement-open',
648
+ ReqId,
649
+ Description,
650
+ Suggestion,
651
+ Source,
652
+ _{supersededBy: Successors, status: Status}
653
+ )) :-
654
+ member(ReqId, Targets),
655
+ kb_entity(ReqId, req, Props),
656
+ findall(From, member(From-ReqId, Edges), Successors0),
657
+ sort(Successors0, Successors),
658
+ requirement_status_atom(Props, Status),
659
+ Status \== closed,
660
+ not_in_supersession_cycle(ReqId, CycleMembers),
661
+ atomic_list_concat(Successors, ', ', SuccessorText),
662
+ format(string(Description),
663
+ "Requirement ~w is superseded by ~w but its status is ~w; a superseded requirement must be closed",
664
+ [ReqId, SuccessorText, Status]),
665
+ format(string(Suggestion),
666
+ "Set status: closed on ~w (kibi migrate does this with its close_superseded_requirements action). If ~w still states current intent, remove the supersedes link instead",
667
+ [ReqId, ReqId]),
668
+ violation_source(ReqId, req, Source).
669
+
670
+ not_in_supersession_cycle(ReqId, CycleMembers) :-
671
+ \+ ord_memberchk(ReqId, CycleMembers).
672
+
673
+ requirement_status_atom(Props, Status) :-
674
+ ( memberchk(status=Raw, Props)
675
+ -> normalize_term_atom(Raw, Status)
676
+ ; Status = missing
677
+ ).
678
+
679
+ supersession_cycle_violation(Edges, Members, violation(
680
+ 'superseded-requirement-open',
681
+ FirstId,
682
+ Description,
683
+ "Decide which requirement states current intent: delete the supersedes link that points at it (kb_delete the relationship), then close the requirements it replaces",
684
+ Source,
685
+ _{cycle: Members, edges: EdgeList}
686
+ )) :-
687
+ Members = [FirstId|_],
688
+ findall(
689
+ [From, To],
690
+ ( member(From-To, Edges),
691
+ ord_memberchk(From, Members),
692
+ ord_memberchk(To, Members)
693
+ ),
694
+ EdgeList
695
+ ),
696
+ findall(
697
+ Text,
698
+ ( member([From, To], EdgeList),
699
+ format(atom(Text), "~w supersedes ~w", [From, To])
700
+ ),
701
+ EdgeTexts
702
+ ),
703
+ atomic_list_concat(EdgeTexts, '; ', EdgeText),
704
+ atomic_list_concat(Members, ', ', MemberText),
705
+ format(string(Description),
706
+ "Supersession cycle: ~w (~w), so none of these requirements is current and none can be closed automatically",
707
+ [MemberText, EdgeText]),
708
+ violation_source(FirstId, req, Source).
709
+
710
+ %% requirement_sets(-ReqIds, -Retired)
711
+ % Ordered sets of every requirement id and of the retired ones: superseded by
712
+ % another requirement, or deprecated. `closed` means done, not retired: code
713
+ % that implements a closed requirement still has a current owner.
714
+ requirement_sets(ReqIds, Retired) :-
715
+ findall(
716
+ ReqId-Status,
717
+ ( kb_entity(ReqId, req, Props),
718
+ requirement_status_atom(Props, Status)
719
+ ),
720
+ Pairs
721
+ ),
722
+ findall(ReqId, member(ReqId-_, Pairs), ReqIds0),
723
+ sort(ReqIds0, ReqIds),
724
+ findall(To, kb_relationship(supersedes, _, To), Superseded),
725
+ findall(ReqId, member(ReqId-deprecated, Pairs), Deprecated),
726
+ append(Superseded, Deprecated, Retired0),
727
+ sort(Retired0, Retired1),
728
+ ord_intersection(Retired1, ReqIds, Retired).
729
+
730
+ %% check_symbol_owner_superseded(-Violations)
731
+ % implements REQ-core-validation-rules
732
+ % Advisory: a symbol whose every `implements` target is superseded or
733
+ % deprecated has no current owner, so its behavior is governed by retired intent. The
734
+ % first symbol_owner_review_limit/1 symbols (by id) are listed, then one
735
+ % summary finding counts the rest, so a large KB yields a bounded queue.
736
+ symbol_owner_review_limit(25).
737
+
738
+ check_symbol_owner_superseded(Violations) :-
739
+ requirement_sets(ReqIds, Retired),
740
+ findall(
741
+ SymbolId-Owners,
742
+ symbol_with_retired_owners(ReqIds, Retired, SymbolId, Owners),
743
+ Pairs0
744
+ ),
745
+ sort(Pairs0, Pairs),
746
+ length(Pairs, Total),
747
+ symbol_owner_review_limit(Limit),
748
+ ( Total > Limit
749
+ -> length(Listed, Limit),
750
+ append(Listed, _, Pairs),
751
+ Remaining is Total - Limit,
752
+ format(string(SummaryText),
753
+ "~w more symbol(s) implement only superseded or deprecated requirements; the list above stops at ~w",
754
+ [Remaining, Limit]),
755
+ Summary = [violation(
756
+ 'symbol-owner-superseded',
757
+ workspace,
758
+ SummaryText,
759
+ "Relink the listed symbols first; the next kb_check lists the following ones",
760
+ "",
761
+ _{total: Total, listed: Limit}
762
+ )]
763
+ ; Listed = Pairs,
764
+ Summary = []
765
+ ),
766
+ maplist(symbol_owner_superseded_violation, Listed, Findings),
767
+ append(Findings, Summary, Violations).
768
+
769
+ symbol_with_retired_owners(ReqIds, Retired, SymbolId, Owners) :-
770
+ findall(Id-ReqId, kb_relationship(implements, Id, ReqId), Edges0),
771
+ sort(Edges0, Edges),
772
+ group_pairs_by_key(Edges, Groups),
773
+ member(SymbolId-Targets, Groups),
774
+ ord_intersection(Targets, ReqIds, Owners),
775
+ Owners \== [],
776
+ ord_subtract(Owners, Retired, []),
777
+ kb_entity(SymbolId, symbol, SymbolProps),
778
+ live_symbol(SymbolProps).
779
+
780
+ live_symbol(Props) :-
781
+ requirement_status_atom(Props, Status),
782
+ \+ memberchk(Status, [deprecated, removed]).
783
+
784
+ symbol_owner_superseded_violation(SymbolId-Owners, violation(
785
+ 'symbol-owner-superseded',
786
+ SymbolId,
787
+ Description,
788
+ Suggestion,
789
+ Source,
790
+ _{owners: Owners, replacements: Replacements}
791
+ )) :-
792
+ findall(
793
+ NewId,
794
+ ( member(ReqId, Owners),
795
+ kb_relationship(supersedes, NewId, ReqId)
796
+ ),
797
+ Replacements0
798
+ ),
799
+ sort(Replacements0, Replacements),
800
+ ( kb_entity(SymbolId, symbol, Props),
801
+ memberchk(title=RawTitle, Props)
802
+ -> violation_text(RawTitle, Title)
803
+ ; Title = SymbolId
804
+ ),
805
+ atomic_list_concat(Owners, ', ', OwnerText),
806
+ format(string(Description),
807
+ "Symbol ~w (~w) implements only superseded or deprecated requirements (~w), so no current requirement owns it",
808
+ [Title, SymbolId, OwnerText]),
809
+ ( Replacements == []
810
+ -> Suggestion = "Link the symbol to the current requirement that governs it (implements), or remove the symbol if its behavior is gone"
811
+ ; atomic_list_concat(Replacements, ', ', ReplacementText),
812
+ format(string(Suggestion),
813
+ "Link the symbol to the requirement that replaced its owner (implements ~w) if it still implements that behavior, or remove the symbol if its behavior is gone",
814
+ [ReplacementText])
815
+ ),
816
+ violation_source(SymbolId, symbol, Source).
817
+
818
+ %% check_adr_unlinked(-Violations)
819
+ % implements REQ-016
820
+ % Advisory: an accepted ADR that no requirement or other ADR is linked with
821
+ % (relates_to, supersedes or any other typed edge, in either direction)
822
+ % explains nothing an agent can reach from the requirements it governs.
823
+ % Symbol constrained_by links do not count: they say where a decision
824
+ % applies, not which intent it serves.
825
+ check_adr_unlinked(Violations) :-
826
+ findall(Violation, adr_unlinked_violation(Violation), Violations0),
827
+ sort(Violations0, Violations).
828
+
829
+ adr_unlinked_violation(violation(
830
+ 'adr-unlinked',
831
+ AdrId,
832
+ Description,
833
+ Suggestion,
834
+ Source
835
+ )) :-
836
+ kb_entity(AdrId, adr, Props),
837
+ requirement_status_atom(Props, accepted),
838
+ adr_without_knowledge_links(AdrId),
839
+ format(string(Description),
840
+ "Accepted ADR ~w is not linked to any requirement or ADR, so nothing that governs behavior points at this decision",
841
+ [AdrId]),
842
+ format(string(Suggestion),
843
+ "Link ~w to the requirements it explains (relates_to), supersede it with the ADR that replaced it, or deprecate it if the decision no longer applies",
844
+ [AdrId]),
845
+ violation_source(AdrId, adr, Source).
846
+
847
+ adr_without_knowledge_links(AdrId) :-
848
+ \+ adr_knowledge_link(AdrId).
849
+
850
+ adr_knowledge_link(AdrId) :-
851
+ relationship_type(Type),
852
+ ( kb_relationship(Type, AdrId, Other)
853
+ ; kb_relationship(Type, Other, AdrId)
854
+ ),
855
+ Other \== AdrId,
856
+ ( kb_entity(Other, req, _)
857
+ ; kb_entity(Other, adr, _)
858
+ ),
859
+ !.
860
+
861
+ %% check_adr_proposed(-Violations)
862
+ % implements REQ-016
863
+ % Informational: an ADR still `proposed` (and not superseded) is a decision
864
+ % nobody accepted or withdrew; agents cannot tell whether it governs.
865
+ check_adr_proposed(Violations) :-
866
+ findall(Violation, adr_proposed_violation(Violation), Violations0),
867
+ sort(Violations0, Violations).
868
+
869
+ adr_proposed_violation(violation(
870
+ 'adr-proposed',
871
+ AdrId,
872
+ Description,
873
+ Suggestion,
874
+ Source
875
+ )) :-
876
+ kb_entity(AdrId, adr, Props),
877
+ requirement_status_atom(Props, proposed),
878
+ adr_not_superseded(AdrId),
879
+ format(string(Description),
880
+ "ADR ~w is still proposed: the decision was never accepted or withdrawn",
881
+ [AdrId]),
882
+ format(string(Suggestion),
883
+ "Ask the decision owner to accept ~w (status: accepted) or withdraw it (status: deprecated, or supersede it with the ADR that was adopted)",
884
+ [AdrId]),
885
+ violation_source(AdrId, adr, Source).
886
+
887
+ adr_not_superseded(AdrId) :-
888
+ \+ kb_relationship(supersedes, _, AdrId).
889
+
890
+ %% check_scenario_feasibility(-Violations)
891
+ % implements REQ-kibi-scenario-feasibility-v2
892
+ % A scenario that expects success and whose assumptions cannot hold together
893
+ % with what current requirements require can never pass. Each witness names
894
+ % the scenario, the requirements, the assumed facts and the requirement facts
895
+ % of one irreducible conflict: one assumption against one requirement
896
+ % constraint (pairwise), several assumptions that are compatible on their own
897
+ % but leave no admissible value once the requirement constraints are added
898
+ % (joint, e.g. q >= 5 and q != 5 against q =< 5), or assumptions that satisfy
899
+ % every condition of a rule forbidding the scenario's action. Requirement
900
+ % constraints come from typed property facts and from typed rules alike
901
+ % (scenario_feasibility_analysis/2 reads both in one constraint form). An
902
+ % approved exception (a current requirement that `exempts` the base
903
+ % requirement, is `specified_by` the scenario and carries a non-empty
904
+ % `approved_by`) waives the base requirement's constraints for that scenario
905
+ % without editing it; with `exempts_claims` it waives only the constraints
906
+ % grounded by the listed claims. An exception without approval does not
907
+ % exempt; the violation says so. Scenarios that expect rejection or error are
908
+ % not checked. A success scenario whose feasibility cannot be decided is
909
+ % reported by scenario-feasibility-unknown instead: absence of a witness is
910
+ % not proof that the scenario is feasible.
911
+ check_scenario_feasibility(Violations) :-
912
+ findall(
913
+ Violation,
914
+ scenario_feasibility_violation(Violation),
915
+ Unsorted
916
+ ),
917
+ sort(Unsorted, Violations).
918
+
919
+ scenario_feasibility_violation(violation(
920
+ 'scenario-feasibility',
921
+ ScenarioId,
922
+ Description,
923
+ Suggestion,
924
+ Source
925
+ )) :-
926
+ infeasible_scenario(ScenarioId, ReqIds, AssumedFacts, ReqFacts, Reason),
927
+ atomic_list_concat(AssumedFacts, ', ', AssumedText),
928
+ atomic_list_concat(ReqIds, ', ', ReqText),
929
+ atomic_list_concat(ReqFacts, ', ', ReqFactText),
930
+ format(string(Description0),
931
+ "Scenario expects success but assumes ~w, which current requirement ~w forbids via ~w: ~w",
932
+ [AssumedText, ReqText, ReqFactText, Reason]),
933
+ findall(ExceptionId,
934
+ ( member(ReqId, ReqIds),
935
+ unapproved_scenario_exception(ScenarioId, ReqId, ExceptionId)
936
+ ),
937
+ Unapproved0),
938
+ sort(Unapproved0, Unapproved),
939
+ findall(ExceptionId,
940
+ ( member(ReqId, ReqIds),
941
+ narrow_scenario_exception(ScenarioId, ReqId, ReqFacts, ExceptionId)
942
+ ),
943
+ Narrow0),
944
+ sort(Narrow0, Narrow),
945
+ ( Unapproved \== []
946
+ -> atomic_list_concat(Unapproved, ', ', UnapprovedText),
947
+ format(string(Description),
948
+ "~w. An exception exists (~w exempts ~w) but is not approved",
949
+ [Description0, UnapprovedText, ReqText]),
950
+ format(string(Suggestion),
951
+ "Have a human approve ~w by setting approved_by (and optionally approval_ref), or set expects: rejection on ~w, or correct the assumption",
952
+ [UnapprovedText, ScenarioId])
953
+ ; Narrow \== []
954
+ -> atomic_list_concat(Narrow, ', ', NarrowText),
955
+ format(string(Description),
956
+ "~w. Approved exception ~w waives only the claims listed in its exempts_claims, and ~w is not grounded by one of them",
957
+ [Description0, NarrowText, ReqFactText]),
958
+ format(string(Suggestion),
959
+ "Add the claim_key of ~w to exempts_claims on ~w if a human approves waiving that clause too, set expects: rejection on ~w, or correct the assumption",
960
+ [ReqFactText, NarrowText, ScenarioId])
961
+ ; Description = Description0,
962
+ format(string(Suggestion),
963
+ "Set expects: rejection on ~w, correct the assumption, or record a human-approved exception requirement (approved_by set) that exempts ~w and is specified_by ~w",
964
+ [ScenarioId, ReqText, ScenarioId])
965
+ ),
966
+ ( kb_entity(ScenarioId, scenario, Props),
967
+ memberchk(source=Source, Props)
968
+ -> true
969
+ ; Source = ""
970
+ ).
971
+
972
+ %% infeasible_scenario(?ScenarioId, -ReqIds, -AssumedFacts, -ReqFacts, -Reason)
973
+ % implements REQ-kibi-scenario-feasibility-v2
974
+ % One blocking witness for a success scenario: sorted lists of current,
975
+ % non-exempted requirements, the scenario's assumed facts and the requirement
976
+ % facts (property values or rules) whose constraints admit no common value.
977
+ % The witness is irreducible: dropping any listed fact leaves a satisfiable
978
+ % set. The blocking rule, the proof ladder and what-if analysis all read this
979
+ % predicate, so pairwise, joint and rule-lane infeasibility block alike.
980
+ infeasible_scenario(ScenarioId, ReqIds, AssumedFacts, ReqFacts, Reason) :-
981
+ success_scenario(ScenarioId),
982
+ scenario_feasibility_analysis(ScenarioId, analysis(Witnesses, _, _, _)),
983
+ member(infeasibility(ReqIds, AssumedFacts, ReqFacts, Reason), Witnesses).
984
+
985
+ success_scenario(ScenarioId) :-
986
+ ( var(ScenarioId)
987
+ -> findall(Id, scenario_expects(Id, success), Ids0),
988
+ sort(Ids0, Ids),
989
+ member(ScenarioId, Ids)
990
+ ; scenario_expects(ScenarioId, success)
991
+ ).
992
+
993
+ scenario_expects(ScenarioId, Outcome) :-
994
+ kb_entity(ScenarioId, scenario, Props),
995
+ memberchk(expects=Raw, Props),
996
+ kb:normalize_term_atom(Raw, Outcome).
997
+
998
+ %% constraint_exempt(+ScenarioId, +ReqId, +FactId)
999
+ % implements REQ-kibi-scenario-feasibility-v2
1000
+ % An approved, current exception requirement exempts ReqId, specifies the
1001
+ % scenario and covers the requirement constraint grounded by FactId: every
1002
+ % constraint when the exception lists no exempts_claims, otherwise only those
1003
+ % whose fact carries one of the listed claim keys.
1004
+ constraint_exempt(ScenarioId, ReqId, FactId) :-
1005
+ scenario_exception(ScenarioId, ReqId, ExceptionId),
1006
+ exception_approved(ExceptionId),
1007
+ exception_covers(ExceptionId, FactId),
1008
+ !.
1009
+
1010
+ exception_covers(ExceptionId, FactId) :-
1011
+ ( exception_claim_keys(ExceptionId, ClaimKeys)
1012
+ -> fact_claim_key(FactId, ClaimKey),
1013
+ memberchk(ClaimKey, ClaimKeys)
1014
+ ; true
1015
+ ).
1016
+
1017
+ %% exception_claim_keys(+ExceptionId, -ClaimKeys)
1018
+ % The non-empty exempts_claims list of an exception requirement.
1019
+ exception_claim_keys(ExceptionId, ClaimKeys) :-
1020
+ kb_entity(ExceptionId, req, Props),
1021
+ memberchk(exempts_claims=Raw, Props),
1022
+ kb:normalize_term_atom_list(Raw, ClaimKeys),
1023
+ ClaimKeys \== [].
1024
+
1025
+ fact_claim_key(FactId, ClaimKey) :-
1026
+ kb_entity(FactId, fact, Props),
1027
+ memberchk(claim_key=Raw, Props),
1028
+ kb:normalize_term_atom(Raw, ClaimKey).
1029
+
1030
+ scenario_exception(ScenarioId, ReqId, ExceptionId) :-
1031
+ kb_relationship(exempts, ExceptionId, ReqId),
1032
+ kb:current_req(ExceptionId),
1033
+ kb_relationship(specified_by, ExceptionId, ScenarioId).
1034
+
1035
+ unapproved_scenario_exception(ScenarioId, ReqId, ExceptionId) :-
1036
+ scenario_exception(ScenarioId, ReqId, ExceptionId),
1037
+ \+ exception_approved(ExceptionId).
1038
+
1039
+ % An approved exception that lists exempts_claims but covers none of the
1040
+ % witness's requirement facts of ReqId.
1041
+ narrow_scenario_exception(ScenarioId, ReqId, ReqFacts, ExceptionId) :-
1042
+ scenario_exception(ScenarioId, ReqId, ExceptionId),
1043
+ exception_approved(ExceptionId),
1044
+ exception_claim_keys(ExceptionId, _),
1045
+ once((
1046
+ member(FactId, ReqFacts),
1047
+ \+ exception_covers(ExceptionId, FactId)
1048
+ )).
1049
+
1050
+ %% exception_approved(+ExceptionId)
1051
+ % An exception counts only once a human approved it: approved_by names them.
1052
+ exception_approved(ExceptionId) :-
1053
+ kb_entity(ExceptionId, req, Props),
1054
+ memberchk(approved_by=Raw, Props),
1055
+ evidence_text(Raw, Text),
1056
+ normalize_space(string(Trimmed), Text),
1057
+ Trimmed \== "".
1058
+
1059
+ evidence_text(Raw, Text) :-
1060
+ kb:unwrap_rdf_value(Raw, Value),
1061
+ ( string(Value) -> Text = Value
1062
+ ; atom(Value) -> atom_string(Value, Text)
1063
+ ; term_string(Value, Text)
1064
+ ).
1065
+
1066
+ %% scenario_feasibility_outcome(?ScenarioId, -Outcome)
1067
+ % implements REQ-kibi-scenario-feasibility-v2
1068
+ % The analysis result for a scenario that expects success:
1069
+ % infeasible(witness(ReqIds, AssumedFacts, ReqFacts, Reason))
1070
+ % the assumptions cannot hold with the constraints
1071
+ % of current, non-exempted requirements (blocking)
1072
+ % unknown(no_assumptions) the scenario assumes nothing, so nothing can be
1073
+ % checked
1074
+ % unknown(contradictory_assumptions(FactIds))
1075
+ % the assumptions cannot hold together on their
1076
+ % own, whatever the requirements say
1077
+ % unknown(unmatched_assumption(FactIds))
1078
+ % some assumption is not a property value whose
1079
+ % subject and property a current requirement
1080
+ % governing the scenario constrains
1081
+ % unknown(incomparable_assumption(FactIds))
1082
+ % some assumption is constrained, but its type,
1083
+ % unit or operator cannot be compared with the
1084
+ % other constraints on that property
1085
+ % unknown(conflicting_requirements(ReqIds))
1086
+ % the governing requirements cannot hold together
1087
+ % on that property, so the scenario's own
1088
+ % feasibility cannot be judged (the requirement
1089
+ % contradiction checks report the conflict)
1090
+ % unknown(undecided_rule(RuleFactIds))
1091
+ % a rule governing the scenario reads an assumed
1092
+ % property, but the assumptions neither satisfy
1093
+ % nor refute its conditions, or the rule uses
1094
+ % constructs this check cannot decide
1095
+ % unknown(undetermined_validity(FactIds))
1096
+ % the scenario would conflict only with
1097
+ % constraints whose validity window may or may
1098
+ % not cover the scenario's time (unspecified, or
1099
+ % partly overlapping)
1100
+ % feasible_by_exception the assumptions conflict only with constraints
1101
+ % an approved exception waives
1102
+ % not_applicable(disjoint_scope(FactIds))
1103
+ % not_applicable(outside_validity(FactIds))
1104
+ % no current requirement governs any assumption:
1105
+ % the requirements that constrain the assumed
1106
+ % properties are scoped elsewhere or are not in
1107
+ % force at the scenario's time
1108
+ % feasible every assumption was compared with every
1109
+ % governing constraint and the conjunction of all
1110
+ % of them is satisfiable
1111
+ % Only infeasible blocks; unknown is a non-blocking quality diagnostic.
1112
+ scenario_feasibility_outcome(ScenarioId, Outcome) :-
1113
+ success_scenario(ScenarioId),
1114
+ scenario_feasibility_analysis(ScenarioId, Analysis),
1115
+ % Decide the outcome before matching it, so asking for unknown(_) never
1116
+ % skips past an infeasible verdict.
1117
+ analysis_outcome(Analysis, Outcome0),
1118
+ Outcome = Outcome0.
1119
+
1120
+ analysis_outcome(analysis([infeasibility(ReqIds, AssumedFacts, ReqFacts, Reason)|_], _, _, _),
1121
+ infeasible(witness(ReqIds, AssumedFacts, ReqFacts, Reason))) :- !.
1122
+ analysis_outcome(analysis([], [Reason|_], _, _), unknown(Reason)) :- !.
1123
+ analysis_outcome(analysis([], [], true, _), feasible_by_exception) :- !.
1124
+ analysis_outcome(analysis([], [], false, [Reason|_]), not_applicable(Reason)) :- !.
1125
+ analysis_outcome(analysis([], [], false, []), feasible).
1126
+
1127
+ %% scenario_feasibility_analysis(+ScenarioId, -analysis(Witnesses, Unknowns, ByException, NotApplicable))
1128
+ % implements REQ-kibi-scenario-feasibility-v2
1129
+ % Every assumed property value becomes a member constraint on its subject and
1130
+ % property. Requirement constraints share that form, whichever lane grounds
1131
+ % them:
1132
+ %
1133
+ % * a property value a current requirement requires (a forbid fact
1134
+ % contributes the negated operator);
1135
+ % * a condition of a current requirement's typed rule whose body reads
1136
+ % subject properties (logic_rule_property_form/2), when the rule restricts
1137
+ % the scenario's action: a forbid or deny rule with one condition on one
1138
+ % property contributes the negated condition (forbid checkout unless
1139
+ % total > 0 requires total > 0), and a permit or oblige rule of kind
1140
+ % constraint ("only when") contributes each condition as stated.
1141
+ %
1142
+ % A rule with a head restricts the scenario only when the scenario performs
1143
+ % that action: it specifies the requirement (or an exception to it), or it
1144
+ % assumes a predicate fact naming the action. Each requirement constraint is
1145
+ % applicable, undetermined or outside by its validity window against the
1146
+ % scenario's time, which is the intersection of the assumed facts' validity
1147
+ % windows (unspecified when none sets one): an unbounded constraint always
1148
+ % applies, a bounded one is undetermined at an unspecified or partly
1149
+ % overlapping time and outside at a disjoint time. Members are grouped by
1150
+ % subject and property, and each group is split into scope contexts: an
1151
+ % unscoped member belongs to every context, a scoped member only to its own.
1152
+ % In each context:
1153
+ %
1154
+ % * every comparable (assumption, applicable constraint) pair whose
1155
+ % constraints admit no common value is a pairwise witness;
1156
+ % * assumptions that admit no value on their own are contradictory
1157
+ % (advisory);
1158
+ % * otherwise, when the assumptions and the governing (applicable,
1159
+ % non-exempted) constraints admit no value and no pairwise witness exists,
1160
+ % an irreducible core is a joint witness, unless the requirement
1161
+ % constraints alone admit no value;
1162
+ % * when they do admit a value but adding the undetermined constraints
1163
+ % leaves none, the context is undetermined_validity (advisory);
1164
+ % * a context with an incomparable member stays undecided.
1165
+ %
1166
+ % A restricting rule that cannot be read as one constraint per property (a
1167
+ % forbid rule with several conditions, or one the property reading cannot
1168
+ % translate) is decided on its own: it blocks when the assumptions entail
1169
+ % every condition, is irrelevant when they refute one, and is undecided
1170
+ % otherwise. Numeric constraints are decided with intervals.pl, with integer
1171
+ % semantics only when every member of the set is declared int; other value
1172
+ % types support eq and neq only. Witnesses are sorted; Unknowns lists the
1173
+ % advisory reasons in reporting order; ByException is true when some check is
1174
+ % satisfiable only because an approved exception waived a constraint;
1175
+ % NotApplicable is non-empty only when no assumption is governed at all.
1176
+ scenario_feasibility_analysis(ScenarioId, analysis(Witnesses, Unknowns, ByException, NotApplicable)) :-
1177
+ findall(FactId, kb_relationship(assumes, ScenarioId, FactId), Assumed0),
1178
+ sort(Assumed0, Assumed),
1179
+ ( Assumed == []
1180
+ -> Witnesses = [],
1181
+ Unknowns = [no_assumptions],
1182
+ ByException = false,
1183
+ NotApplicable = []
1184
+ ; convlist(assumption_member, Assumed, Assumptions),
1185
+ findall(Key, member(m(_, _, _, Key, _, _, _, _, _, _), Assumptions), Keys0),
1186
+ sort(Keys0, Keys),
1187
+ scenario_window(Assumed, Window),
1188
+ property_constraints(ScenarioId, Keys, Window, PropertyConstraints),
1189
+ rule_constraints(ScenarioId, Keys, Window, RuleConstraints, GeneralRules, Actions),
1190
+ append(PropertyConstraints, RuleConstraints, Constraints),
1191
+ findall(Context, scenario_context(Assumptions, Constraints, Context), Contexts),
1192
+ foldl(context_results, Contexts, [], ContextResults),
1193
+ foldl(general_rule_results(Assumptions), GeneralRules, ContextResults, Results),
1194
+ assumption_statuses(Assumed, Assumptions, Constraints, GeneralRules, Actions, Statuses),
1195
+ findall(Witness, member(witness(Witness), Results), Witnesses0),
1196
+ sort(Witnesses0, Witnesses),
1197
+ status_ids(unmatched, Statuses, Unmatched),
1198
+ result_ids(contradictory, Results, Contradictory),
1199
+ result_ids(incomparable, Results, Incomparable),
1200
+ result_ids(conflicting_requirements, Results, Conflicting),
1201
+ result_ids(undecided_rule, Results, Undecided),
1202
+ result_ids(undetermined_validity, Results, Undetermined),
1203
+ include(nonempty_reason,
1204
+ [ contradictory_assumptions(Contradictory),
1205
+ unmatched_assumption(Unmatched),
1206
+ incomparable_assumption(Incomparable),
1207
+ conflicting_requirements(Conflicting),
1208
+ undecided_rule(Undecided),
1209
+ undetermined_validity(Undetermined)
1210
+ ],
1211
+ Unknowns),
1212
+ ( memberchk(by_exception, Results)
1213
+ -> ByException = true
1214
+ ; ByException = false
1215
+ ),
1216
+ ( memberchk(_-governed, Statuses)
1217
+ -> NotApplicable = []
1218
+ ; status_ids(outside_validity, Statuses, Outside),
1219
+ status_ids(disjoint_scope, Statuses, Disjoint),
1220
+ include(nonempty_reason,
1221
+ [outside_validity(Outside), disjoint_scope(Disjoint)],
1222
+ NotApplicable)
1223
+ )
1224
+ ).
1225
+
1226
+ nonempty_reason(Reason) :-
1227
+ arg(1, Reason, Ids),
1228
+ Ids \== [].
1229
+
1230
+ result_ids(Kind, Results, Ids) :-
1231
+ findall(Id,
1232
+ ( member(Result, Results),
1233
+ compound(Result),
1234
+ Result =.. [Kind, ResultIds],
1235
+ member(Id, ResultIds)
1236
+ ),
1237
+ Ids0),
1238
+ sort(Ids0, Ids).
1239
+
1240
+ status_ids(Status, Statuses, Ids) :-
1241
+ findall(FactId, member(FactId-Status, Statuses), Ids0),
1242
+ sort(Ids0, Ids).
1243
+
1244
+ % m(Role, Owner, FactId, Subject-Property, Scope, Type, Unit, Op, Value, Text)
1245
+ % Owner is the fact itself for an assumption and the requirement for a
1246
+ % requirement constraint; FactId is the property or rule fact that grounds
1247
+ % it. Type, Unit and Value are canonical; Op already carries the fact's
1248
+ % polarity; Text is the authored constraint for witnesses.
1249
+ assumption_member(FactId, Member) :-
1250
+ property_member(assumption, FactId, FactId, Member).
1251
+
1252
+ property_member(Role, Owner, FactId,
1253
+ m(Role, Owner, FactId, Subject-Property, Scope, Type, Unit, Op, Value, Text)) :-
1254
+ kb:fact_property_tuple(FactId, Subject, Property, AuthoredOp, ValueType, AuthoredValue, AuthoredUnit, Scope, Polarity),
1255
+ !,
1256
+ kb:comparison_quantity(ValueType, AuthoredValue, AuthoredUnit, Type, Value, Unit),
1257
+ ( polarity_operator(Polarity, AuthoredOp, Op0)
1258
+ -> Op = Op0
1259
+ ; Op = unsupported
1260
+ ),
1261
+ ( AuthoredUnit == ''
1262
+ -> format(atom(Constraint), '~w ~w', [AuthoredOp, AuthoredValue])
1263
+ ; format(atom(Constraint), '~w ~w ~w', [AuthoredOp, AuthoredValue, AuthoredUnit])
1264
+ ),
1265
+ ( Polarity == forbid
1266
+ -> format(atom(Text), 'forbid ~w', [Constraint])
1267
+ ; Text = Constraint
1268
+ ).
1269
+
1270
+ polarity_operator(require, Op, Op).
1271
+ polarity_operator(forbid, Op, Negated) :- negated_operator(Op, Negated).
1272
+
1273
+ negated_operator(eq, neq).
1274
+ negated_operator(neq, eq).
1275
+ negated_operator(lt, gte).
1276
+ negated_operator(gte, lt).
1277
+ negated_operator(gt, lte).
1278
+ negated_operator(lte, gt).
1279
+
1280
+ member_fact(m(_, _, FactId, _, _, _, _, _, _, _), FactId).
1281
+ member_owner(m(_, Owner, _, _, _, _, _, _, _, _), Owner).
1282
+ member_key(Key, m(_, _, _, Key, _, _, _, _, _, _)).
1283
+ member_scope(m(_, _, _, _, Scope, _, _, _, _, _), Scope).
1284
+ member_role(Role, m(Role, _, _, _, _, _, _, _, _, _)).
1285
+
1286
+ negated_member(m(Role, Owner, FactId, Key, Scope, Type, Unit, Op, Value, Text),
1287
+ m(Role, Owner, FactId, Key, Scope, Type, Unit, Negated, Value, Text)) :-
1288
+ negated_operator(Op, Negated).
1289
+
1290
+ %% property_constraints(+ScenarioId, +Keys, +Window, -Constraints)
1291
+ % rc(Member, Applicability, Exempt) for every property value a current
1292
+ % requirement requires on an assumed subject and property.
1293
+ property_constraints(ScenarioId, Keys, Window, Constraints) :-
1294
+ findall(rc(Member, Applicability, Exempt),
1295
+ ( member(Subject-Property, Keys),
1296
+ kb:current_req(ReqId),
1297
+ kb:effective_req_property_fact(ReqId, Subject, FactId, Property, _, _, _, _, _, _, From, To),
1298
+ property_member(requirement, ReqId, FactId, Member),
1299
+ validity_applicability(Window, From, To, Applicability),
1300
+ exemption_flag(ScenarioId, ReqId, FactId, Exempt)
1301
+ ),
1302
+ Constraints0),
1303
+ sort(Constraints0, Constraints).
1304
+
1305
+ exemption_flag(ScenarioId, ReqId, FactId, Exempt) :-
1306
+ ( constraint_exempt(ScenarioId, ReqId, FactId)
1307
+ -> Exempt = true
1308
+ ; Exempt = false
1309
+ ).
1310
+
1311
+ %% rule_constraints(+ScenarioId, +Keys, +Window, -Constraints, -GeneralRules, -Actions)
1312
+ % Rules of current requirements that restrict the scenario's action and read
1313
+ % an assumed property. Rules whose conditions are one constraint per property
1314
+ % become rc/3 constraints like property facts; the rest are decided on their
1315
+ % own as gr/8 terms. Actions are the Namespace-Name heads of those rules.
1316
+ rule_constraints(ScenarioId, Keys, Window, Constraints, GeneralRules, Actions) :-
1317
+ findall(Rule, scenario_rule(ScenarioId, Keys, Window, Rule), Rules0),
1318
+ sort(Rules0, Rules),
1319
+ foldl(rule_parts, Rules, parts([], []), parts(Constraints0, GeneralRules0)),
1320
+ sort(Constraints0, Constraints),
1321
+ sort(GeneralRules0, GeneralRules),
1322
+ findall(Namespace-Name,
1323
+ member(srule(_, _, _, atom(Namespace, Name, _, _, _), _, _, _, _, _), Rules),
1324
+ Actions0),
1325
+ sort(Actions0, Actions).
1326
+
1327
+ % srule(ReqId, FactId, Restriction, Head, Scope, Applicability, Exempt, ReadKeys, Conditions)
1328
+ scenario_rule(ScenarioId, Keys, Window,
1329
+ srule(ReqId, FactId, Restriction, Head, Scope, Applicability, Exempt, ReadKeys, Conditions)) :-
1330
+ kb_relationship(requires_rule, ReqId, FactId),
1331
+ kb:current_req(ReqId),
1332
+ stored_rule_fact(FactId, _Props, Rule),
1333
+ Rule = rule(Kind, _, _, _, _, _, _, _, _, _),
1334
+ logic_rule_property_form(Rule, property_form(Modality, Head, Scope, RuleFrom, RuleTo, ReadKeys, Conditions)),
1335
+ rule_restriction(Kind, Modality, Restriction),
1336
+ once((member(Key, ReadKeys), memberchk(Key, Keys))),
1337
+ rule_head_performed(ScenarioId, ReqId, Head),
1338
+ rule_validity(FactId, RuleFrom, RuleTo, From, To),
1339
+ validity_applicability(Window, From, To, Applicability),
1340
+ exemption_flag(ScenarioId, ReqId, FactId, Exempt).
1341
+
1342
+ % forbid/deny H :- C restricts H to situations where C fails; a permit or
1343
+ % oblige rule of kind constraint states C as a necessary condition for H.
1344
+ rule_restriction(_, forbid, forbids).
1345
+ rule_restriction(_, deny, forbids).
1346
+ rule_restriction(constraint, permit, requires).
1347
+ rule_restriction(constraint, oblige, requires).
1348
+
1349
+ rule_head_performed(_, _, none) :- !.
1350
+ rule_head_performed(ScenarioId, ReqId, _) :-
1351
+ kb_relationship(specified_by, ReqId, ScenarioId),
1352
+ !.
1353
+ rule_head_performed(ScenarioId, ReqId, _) :-
1354
+ scenario_exception(ScenarioId, ReqId, _),
1355
+ !.
1356
+ rule_head_performed(ScenarioId, _, atom(Namespace, Name, _, positive, _)) :-
1357
+ kb_relationship(assumes, ScenarioId, FactId),
1358
+ kb:predicate_fact(FactId, Namespace, Name, _, assert),
1359
+ !.
1360
+
1361
+ % The rule's own validFrom/validTo, else the rule fact's validity window.
1362
+ rule_validity(FactId, '', '', From, To) :-
1363
+ !,
1364
+ kb:fact_valid_interval(FactId, From, To).
1365
+ rule_validity(_, From, To, From, To).
1366
+
1367
+ rule_parts(srule(ReqId, FactId, Restriction, Head, Scope, Applicability, Exempt, ReadKeys, Conditions),
1368
+ parts(Constraints0, General0), parts(Constraints, General)) :-
1369
+ ( rule_condition_members(ReqId, FactId, Restriction, Head, Scope, ReadKeys, Conditions, Members)
1370
+ -> findall(rc(Member, Applicability, Exempt), member(Member, Members), New),
1371
+ append(Constraints0, New, Constraints),
1372
+ General = General0
1373
+ ; general_conditions(ReqId, FactId, Head, Scope, Conditions, GeneralConditions),
1374
+ Constraints = Constraints0,
1375
+ General = [gr(ReqId, FactId, Restriction, Scope, Applicability, Exempt, ReadKeys, GeneralConditions)|General0]
1376
+ ).
1377
+
1378
+ % One forbid condition on the only property the rule reads is the negated
1379
+ % constraint; every condition of a necessary-condition rule is a constraint.
1380
+ rule_condition_members(ReqId, FactId, forbids, Head, Scope, [Key], [Condition], [Member]) :-
1381
+ Condition = cond(Key, _, _, _),
1382
+ condition_member(ReqId, FactId, forbids, Head, Scope, Condition, Member).
1383
+ rule_condition_members(ReqId, FactId, requires, Head, Scope, _ReadKeys, Conditions, Members) :-
1384
+ is_list(Conditions),
1385
+ maplist(condition_member(ReqId, FactId, requires, Head, Scope), Conditions, Members).
1386
+
1387
+ general_conditions(ReqId, FactId, Head, Scope, Conditions, GeneralConditions) :-
1388
+ ( is_list(Conditions)
1389
+ -> ( maplist(condition_member(ReqId, FactId, fires, Head, Scope), Conditions, Members)
1390
+ -> GeneralConditions = Members
1391
+ ; GeneralConditions = untranslatable
1392
+ )
1393
+ ; GeneralConditions = Conditions
1394
+ ).
1395
+
1396
+ condition_member(ReqId, FactId, Restriction, Head, Scope, cond(Key, Op, Term, VarType),
1397
+ m(requirement, ReqId, FactId, Key, Scope, Type, Unit, MemberOp, Value, Text)) :-
1398
+ condition_quantity(Term, VarType, Type, Value, Unit),
1399
+ condition_term_text(Term, TermText),
1400
+ head_text(Head, HeadText),
1401
+ ( Restriction == forbids
1402
+ -> negated_operator(Op, MemberOp),
1403
+ format(atom(Text), 'rule forbids ~w unless ~w ~w', [HeadText, MemberOp, TermText])
1404
+ ; Restriction == requires
1405
+ -> MemberOp = Op,
1406
+ format(atom(Text), 'rule permits ~w only when ~w ~w', [HeadText, Op, TermText])
1407
+ ; MemberOp = Op,
1408
+ format(atom(Text), 'rule condition ~w ~w', [Op, TermText])
1409
+ ).
1410
+
1411
+ % Rule constants in the property lane's canonical quantity form. A number
1412
+ % is an int only when the compared variable is declared int; Logic IR
1413
+ % duration units m and w are minutes and weeks.
1414
+ condition_quantity(number(Value, Unit0), VarType, Type, CanonValue, CanonUnit) :-
1415
+ ( Unit0 == none -> Unit = '' ; Unit = Unit0 ),
1416
+ declared_numeric_type(Value, VarType, Type0),
1417
+ kb:comparison_quantity(Type0, Value, Unit, Type, CanonValue, CanonUnit).
1418
+ condition_quantity(duration(Value, Unit0), VarType, Type, CanonValue, CanonUnit) :-
1419
+ ir_duration_unit(Unit0, Unit),
1420
+ declared_numeric_type(Value, VarType, Type0),
1421
+ kb:comparison_quantity(Type0, Value, Unit, Type, CanonValue, CanonUnit).
1422
+ condition_quantity(const(Raw, ConstType), _VarType, Type, Value, '') :-
1423
+ ( memberchk(ConstType, [int, integer, number]),
1424
+ atom_number(Raw, Number)
1425
+ -> ( integer(Number), ConstType \== number -> Type = int ; Type = number ),
1426
+ Value = Number
1427
+ ; ConstType == bool
1428
+ -> Type = bool,
1429
+ Value = Raw
1430
+ ; Type = string,
1431
+ Value = Raw
1432
+ ).
1433
+
1434
+ declared_numeric_type(Value, VarType, int) :-
1435
+ integer(Value),
1436
+ memberchk(VarType, [int, integer]),
1437
+ !.
1438
+ declared_numeric_type(_, _, number).
1439
+
1440
+ ir_duration_unit(m, min) :- !.
1441
+ ir_duration_unit(w, wk) :- !.
1442
+ ir_duration_unit(Unit, Unit).
1443
+
1444
+ condition_term_text(number(Value, none), Value) :- !.
1445
+ condition_term_text(number(Value, Unit), Text) :- !, format(atom(Text), '~w ~w', [Value, Unit]).
1446
+ condition_term_text(duration(Value, Unit), Text) :- !, format(atom(Text), '~w ~w', [Value, Unit]).
1447
+ condition_term_text(const(Value, _), Value).
1448
+
1449
+ head_text(none, 'the scenario') :- !.
1450
+ head_text(atom(default, Name, _, _, _), Name) :- !.
1451
+ head_text(atom(Namespace, Name, _, _, _), Text) :- format(atom(Text), '~w:~w', [Namespace, Name]).
1452
+
1453
+ %% scenario_window(+AssumedFacts, -Window)
1454
+ % The scenario's time: unspecified when no assumed fact sets valid_from or
1455
+ % valid_to, otherwise window(From, To) with the latest start and the earliest
1456
+ % end ('' when unbounded).
1457
+ scenario_window(Assumed, Window) :-
1458
+ findall(From, (member(FactId, Assumed), kb:fact_valid_interval(FactId, From, _), From \== ''), Froms),
1459
+ findall(To, (member(FactId, Assumed), kb:fact_valid_interval(FactId, _, To), To \== ''), Tos),
1460
+ ( Froms == [], Tos == []
1461
+ -> Window = unspecified
1462
+ ; foldl(later_time, Froms, '', From),
1463
+ foldl(earlier_time, Tos, '', To),
1464
+ Window = window(From, To)
1465
+ ).
1466
+
1467
+ later_time(Time, '', Time) :- !.
1468
+ later_time(Time, Acc, Later) :-
1469
+ ( time_before(Acc, Time) -> Later = Time ; Later = Acc ).
1470
+
1471
+ earlier_time(Time, '', Time) :- !.
1472
+ earlier_time(Time, Acc, Earlier) :-
1473
+ ( time_before(Time, Acc) -> Earlier = Time ; Earlier = Acc ).
1474
+
1475
+ %% validity_applicability(+Window, +From, +To, -Applicability)
1476
+ % implements REQ-kibi-scenario-feasibility-v2
1477
+ % A constraint without a validity window applies. A bounded constraint is
1478
+ % undetermined when the scenario's time is unspecified, outside when the two
1479
+ % windows are disjoint, applies when the scenario's window lies inside the
1480
+ % constraint's, and is undetermined when they only partly overlap. Bounds
1481
+ % are inclusive.
1482
+ validity_applicability(_, '', '', applies) :- !.
1483
+ validity_applicability(unspecified, _, _, undetermined) :- !.
1484
+ validity_applicability(window(ScenarioFrom, ScenarioTo), From, To, Applicability) :-
1485
+ ( ( time_before(To, ScenarioFrom)
1486
+ ; time_before(ScenarioTo, From)
1487
+ )
1488
+ -> Applicability = outside
1489
+ ; ( From == ''
1490
+ ; ScenarioFrom \== '', \+ time_before(ScenarioFrom, From)
1491
+ ),
1492
+ ( To == ''
1493
+ ; ScenarioTo \== '', \+ time_before(To, ScenarioTo)
1494
+ )
1495
+ -> Applicability = applies
1496
+ ; Applicability = undetermined
1497
+ ).
1498
+
1499
+ time_before(Earlier, Later) :-
1500
+ Earlier \== '',
1501
+ Later \== '',
1502
+ ( catch(parse_time(Earlier, iso_8601, EarlierStamp), _, fail),
1503
+ catch(parse_time(Later, iso_8601, LaterStamp), _, fail)
1504
+ -> EarlierStamp < LaterStamp
1505
+ ; Earlier @< Later
1506
+ ).
1507
+
1508
+ constraint_member(rc(Member, _, _), Member).
1509
+ constraint_on_key(Key, rc(Member, _, _)) :- member_key(Key, Member).
1510
+ constraint_outside(rc(_, outside, _)).
1511
+ constraint_in_scope_context(Context, rc(Member, _, _)) :- in_scope_context(Context, Member).
1512
+
1513
+ % ctx(Key, Assumptions, Governing, Exempt, Undetermined) for one
1514
+ % subject/property and one scope context. Constraints outside their validity
1515
+ % window take no part.
1516
+ scenario_context(Assumptions, Constraints, ctx(Key, As, Governing, Exempt, Undetermined)) :-
1517
+ findall(Key0, member(m(_, _, _, Key0, _, _, _, _, _, _), Assumptions), Keys0),
1518
+ sort(Keys0, Keys),
1519
+ member(Key, Keys),
1520
+ include(member_key(Key), Assumptions, KeyAssumptions),
1521
+ include(constraint_on_key(Key), Constraints, KeyConstraints0),
1522
+ exclude(constraint_outside, KeyConstraints0, KeyConstraints),
1523
+ maplist(constraint_member, KeyConstraints, KeyRequirements),
1524
+ append(KeyAssumptions, KeyRequirements, KeyMembers),
1525
+ findall(Scope,
1526
+ ( member(Member, KeyMembers),
1527
+ member_scope(Member, Scope),
1528
+ Scope \== ''
1529
+ ),
1530
+ Scopes0),
1531
+ sort(Scopes0, Scopes1),
1532
+ ( Scopes1 == []
1533
+ -> Scopes = ['']
1534
+ ; Scopes = Scopes1
1535
+ ),
1536
+ member(Scope, Scopes),
1537
+ include(in_scope_context(Scope), KeyAssumptions, As),
1538
+ As \== [],
1539
+ include(constraint_in_scope_context(Scope), KeyConstraints, InScope),
1540
+ partition_constraints(InScope, Governing, Exempt, Undetermined).
1541
+
1542
+ in_scope_context(Context, Member) :-
1543
+ member_scope(Member, Scope),
1544
+ ( Scope == ''
1545
+ -> true
1546
+ ; Scope == Context
1547
+ ).
1548
+
1549
+ partition_constraints([], [], [], []).
1550
+ partition_constraints([rc(Member, Applicability, ExemptFlag)|Rest], Governing, Exempt, Undetermined) :-
1551
+ ( ExemptFlag == true
1552
+ -> Exempt = [Member|Exempt1],
1553
+ partition_constraints(Rest, Governing, Exempt1, Undetermined)
1554
+ ; Applicability == undetermined
1555
+ -> Undetermined = [Member|Undetermined1],
1556
+ partition_constraints(Rest, Governing, Exempt, Undetermined1)
1557
+ ; Governing = [Member|Governing1],
1558
+ partition_constraints(Rest, Governing1, Exempt, Undetermined)
1559
+ ).
1560
+
1561
+ context_results(ctx(Key, As, Governing, Exempt, Undetermined), Results0, Results) :-
1562
+ findall(witness(Witness), pairwise_witness(Key, As, Governing, Witness), Pairwise),
1563
+ append(As, Governing, Constrained),
1564
+ ( \+ members_comparable(As)
1565
+ -> incomparable_facts(As, [], Ids),
1566
+ Joint = [incomparable(Ids)]
1567
+ ; \+ members_satisfiable(As)
1568
+ -> unsat_core(As, Core),
1569
+ maplist(member_fact, Core, Ids0),
1570
+ sort(Ids0, Ids),
1571
+ Joint = [contradictory(Ids)]
1572
+ ; \+ members_comparable(Constrained)
1573
+ -> incomparable_facts(As, Governing, Ids),
1574
+ Joint = [incomparable(Ids)]
1575
+ ; members_satisfiable(Constrained)
1576
+ -> waived_or_undetermined(As, Constrained, Exempt, Undetermined, Joint)
1577
+ ; Pairwise \== []
1578
+ -> Joint = []
1579
+ ; members_satisfiable(Governing)
1580
+ -> % Drop requirement facts first, so the core names as few
1581
+ % requirements as possible.
1582
+ append(Governing, As, Ordered),
1583
+ unsat_core(Ordered, Core),
1584
+ joint_witness(Key, Core, Witness),
1585
+ Joint = [witness(Witness)]
1586
+ ; unsat_core(Governing, Core),
1587
+ maplist(member_owner, Core, ReqIds0),
1588
+ sort(ReqIds0, ReqIds),
1589
+ Joint = [conflicting_requirements(ReqIds)]
1590
+ ),
1591
+ append([Results0, Pairwise, Joint], Results).
1592
+
1593
+ % The governed context is satisfiable. Constraints whose validity window may
1594
+ % cover the scenario make it undecided when adding them leaves no value;
1595
+ % waived constraints mark it feasible only by exception.
1596
+ waived_or_undetermined(As, Constrained, Exempt, Undetermined, Joint) :-
1597
+ append(Constrained, Undetermined, WithUndetermined),
1598
+ ( Undetermined == []
1599
+ -> Joint0 = []
1600
+ ; \+ members_comparable(WithUndetermined)
1601
+ -> incomparable_facts(As, Undetermined, Ids),
1602
+ Joint0 = [incomparable(Ids)]
1603
+ ; \+ members_satisfiable(WithUndetermined)
1604
+ -> % Drop undetermined constraints first, so the report names as few
1605
+ % as possible.
1606
+ append(Undetermined, Constrained, Ordered),
1607
+ unsat_core(Ordered, Core),
1608
+ include(member_in(Undetermined), Core, CoreUndetermined),
1609
+ maplist(member_fact, CoreUndetermined, Ids0),
1610
+ sort(Ids0, Ids),
1611
+ Joint0 = [undetermined_validity(Ids)]
1612
+ ; Joint0 = []
1613
+ ),
1614
+ ( Exempt \== [],
1615
+ append(Constrained, Exempt, WithExempt),
1616
+ members_comparable(WithExempt),
1617
+ \+ members_satisfiable(WithExempt)
1618
+ -> append(Joint0, [by_exception], Joint)
1619
+ ; Joint = Joint0
1620
+ ).
1621
+
1622
+ member_in(Members, Member) :-
1623
+ member(Candidate, Members),
1624
+ Candidate == Member,
1625
+ !.
1626
+
1627
+ pairwise_witness(Key, As, Governing, infeasibility([ReqId], [AssumedFact], [ReqFact], Reason)) :-
1628
+ member(Assumption, As),
1629
+ member(Required, Governing),
1630
+ members_comparable([Assumption, Required]),
1631
+ members_satisfiable([Assumption]),
1632
+ members_satisfiable([Required]),
1633
+ \+ members_satisfiable([Assumption, Required]),
1634
+ member_fact(Assumption, AssumedFact),
1635
+ member_fact(Required, ReqFact),
1636
+ member_owner(Required, ReqId),
1637
+ witness_reason(Key, [Assumption], [Required], Reason).
1638
+
1639
+ joint_witness(Key, Core, infeasibility(ReqIds, AssumedFacts, ReqFacts, Reason)) :-
1640
+ include(member_role(assumption), Core, CoreAssumptions),
1641
+ include(member_role(requirement), Core, CoreRequirements),
1642
+ maplist(member_fact, CoreAssumptions, AssumedFacts0),
1643
+ sort(AssumedFacts0, AssumedFacts),
1644
+ maplist(member_fact, CoreRequirements, ReqFacts0),
1645
+ sort(ReqFacts0, ReqFacts),
1646
+ maplist(member_owner, CoreRequirements, ReqIds0),
1647
+ sort(ReqIds0, ReqIds),
1648
+ witness_reason(Key, CoreAssumptions, CoreRequirements, Reason).
1649
+
1650
+ witness_reason(Subject-Property, Assumptions, Requirements, Reason) :-
1651
+ maplist(member_label, Assumptions, AssumedLabels),
1652
+ maplist(member_label, Requirements, RequiredLabels),
1653
+ atomic_list_concat(AssumedLabels, ', ', AssumedText),
1654
+ atomic_list_concat(RequiredLabels, ', ', RequiredText),
1655
+ format(atom(Reason),
1656
+ 'No value of ~w.~w satisfies assumed ~w together with required ~w',
1657
+ [Subject, Property, AssumedText, RequiredText]).
1658
+
1659
+ member_label(m(_, _, FactId, _, _, _, _, _, _, Text), Label) :-
1660
+ format(atom(Label), '~w (~w)', [FactId, Text]).
1661
+
1662
+ %% general_rule_results(+Assumptions, +GeneralRule, +Results0, -Results)
1663
+ % implements REQ-kibi-scenario-feasibility-v2
1664
+ % Decide a restricting rule that is not one constraint per property against
1665
+ % the assumptions on the properties it reads (in the rule's scope). Its
1666
+ % conditions hold when every read property is assumed and each condition is
1667
+ % entailed (the assumptions admit no value violating it); they fail when the
1668
+ % assumptions admit no value satisfying some condition. Holding conditions
1669
+ % block the scenario (or mark it feasible by exception, or undetermined when
1670
+ % the rule's validity window may not cover the scenario); anything in between
1671
+ % is undecided, never feasible.
1672
+ general_rule_results(Assumptions, GeneralRule, Results0, Results) :-
1673
+ general_rule_verdict(Assumptions, GeneralRule, Verdict),
1674
+ append(Results0, Verdict, Results).
1675
+
1676
+ general_rule_verdict(_, gr(_, _, _, _, outside, _, _, _), []) :- !.
1677
+ general_rule_verdict(Assumptions, gr(ReqId, FactId, Restriction, Scope, Applicability, Exempt, ReadKeys, Conditions), Verdict) :-
1678
+ include(rule_relevant_assumption(Scope, ReadKeys), Assumptions, Relevant),
1679
+ findall(AssumedScope,
1680
+ ( member(Assumption, Relevant),
1681
+ member_scope(Assumption, AssumedScope),
1682
+ AssumedScope \== ''
1683
+ ),
1684
+ AssumedScopes0),
1685
+ sort(AssumedScopes0, AssumedScopes),
1686
+ ( Relevant == []
1687
+ -> Verdict = []
1688
+ ; Conditions == never
1689
+ -> ( Restriction == requires, Exempt == false
1690
+ -> ( Applicability == undetermined
1691
+ -> Verdict = [undetermined_validity([FactId])]
1692
+ ; Verdict = [conflicting_requirements([ReqId])]
1693
+ )
1694
+ ; Verdict = []
1695
+ )
1696
+ ; Exempt == true, Conditions == untranslatable
1697
+ -> Verdict = []
1698
+ ; ( Conditions == untranslatable
1699
+ ; Scope == '', AssumedScopes = [_, _|_]
1700
+ )
1701
+ -> Verdict = [undecided_rule([FactId])]
1702
+ ; condition_verdict(Relevant, ReqId, FactId, Applicability, Exempt, ReadKeys, Conditions, Verdict)
1703
+ ).
1704
+
1705
+ rule_relevant_assumption(Scope, ReadKeys, Assumption) :-
1706
+ member_key(Key, Assumption),
1707
+ memberchk(Key, ReadKeys),
1708
+ member_scope(Assumption, AssumedScope),
1709
+ kb:scope_intersects(Scope, AssumedScope).
1710
+
1711
+ condition_verdict(Relevant, ReqId, FactId, Applicability, Exempt, ReadKeys, Conditions, Verdict) :-
1712
+ ( conditions_incomparable(Relevant, Conditions, Ids)
1713
+ -> Verdict = [incomparable(Ids)]
1714
+ ; member(Key, ReadKeys),
1715
+ include(member_key(Key), Relevant, KeyAssumptions),
1716
+ KeyAssumptions \== [],
1717
+ \+ ( members_comparable(KeyAssumptions),
1718
+ members_satisfiable(KeyAssumptions)
1719
+ )
1720
+ -> % Incomparable or contradictory assumptions are reported by their
1721
+ % property context.
1722
+ Verdict = []
1723
+ ; member(Condition, Conditions),
1724
+ condition_assumptions(Relevant, Condition, KeyAssumptions),
1725
+ KeyAssumptions \== [],
1726
+ \+ members_satisfiable([Condition|KeyAssumptions])
1727
+ -> Verdict = []
1728
+ ; conditions_entailed(Relevant, ReadKeys, Conditions, Core)
1729
+ -> ( Exempt == true
1730
+ -> Verdict = [by_exception]
1731
+ ; Applicability == undetermined
1732
+ -> Verdict = [undetermined_validity([FactId])]
1733
+ ; maplist(member_fact, Core, AssumedFacts0),
1734
+ sort(AssumedFacts0, AssumedFacts),
1735
+ general_rule_reason(FactId, Core, Reason),
1736
+ Verdict = [witness(infeasibility([ReqId], AssumedFacts, [FactId], Reason))]
1737
+ )
1738
+ ; Exempt == true
1739
+ -> Verdict = []
1740
+ ; Verdict = [undecided_rule([FactId])]
1741
+ ).
1742
+
1743
+ condition_assumptions(Relevant, Condition, KeyAssumptions) :-
1744
+ member_key(Key, Condition),
1745
+ include(member_key(Key), Relevant, KeyAssumptions).
1746
+
1747
+ conditions_incomparable(Relevant, Conditions, Ids) :-
1748
+ findall(FactId,
1749
+ ( member(Condition, Conditions),
1750
+ condition_assumptions(Relevant, Condition, KeyAssumptions),
1751
+ member(Assumption, KeyAssumptions),
1752
+ \+ members_comparable([Condition, Assumption]),
1753
+ member_fact(Assumption, FactId)
1754
+ ),
1755
+ Ids0),
1756
+ sort(Ids0, Ids),
1757
+ Ids \== [].
1758
+
1759
+ % Every read property is assumed and every condition is entailed. Core is a
1760
+ % small set of assumptions that does it: an irreducible entailing subset per
1761
+ % condition, plus one assumption per property read without a condition.
1762
+ conditions_entailed(Relevant, ReadKeys, Conditions, Core) :-
1763
+ forall(member(Key, ReadKeys), (member(Assumption, Relevant), member_key(Key, Assumption))),
1764
+ foldl(condition_entailed(Relevant), Conditions, [], Core0),
1765
+ findall(Assumption,
1766
+ ( member(Key, ReadKeys),
1767
+ unconditioned_key_assumption(Relevant, Conditions, Key, Assumption)
1768
+ ),
1769
+ Extra),
1770
+ append(Core0, Extra, Core1),
1771
+ sort(Core1, Core).
1772
+
1773
+ % One assumption on a property the rule reads but sets no condition on.
1774
+ unconditioned_key_assumption(Relevant, Conditions, Key, Assumption) :-
1775
+ once(( member(Assumption, Relevant), member_key(Key, Assumption) )),
1776
+ \+ ( member(Condition, Conditions), member_key(Key, Condition) ).
1777
+
1778
+ condition_entailed(Relevant, Condition, Core0, Core) :-
1779
+ condition_assumptions(Relevant, Condition, KeyAssumptions),
1780
+ KeyAssumptions \== [],
1781
+ negated_member(Condition, Negated),
1782
+ \+ members_satisfiable([Negated|KeyAssumptions]),
1783
+ unsat_core([Negated|KeyAssumptions], Unsat),
1784
+ include(member_role(assumption), Unsat, Entailing),
1785
+ append(Core0, Entailing, Core).
1786
+
1787
+ general_rule_reason(FactId, Core, Reason) :-
1788
+ maplist(member_label, Core, Labels),
1789
+ atomic_list_concat(Labels, ', ', AssumedText),
1790
+ ( stored_rule_fact(FactId, Props, _),
1791
+ logic_rule_render(Props, RuleText)
1792
+ -> true
1793
+ ; RuleText = FactId
1794
+ ),
1795
+ format(atom(Reason),
1796
+ 'Assumed ~w satisfy every condition of rule ~w (~w), which forbids the scenario''s action',
1797
+ [AssumedText, FactId, RuleText]).
1798
+
1799
+ %% assumption_statuses(+Assumed, +Assumptions, +Constraints, +GeneralRules, +Actions, -Statuses)
1800
+ % FactId-Status for every assumed fact:
1801
+ % governed a constraint that is not outside its validity window
1802
+ % constrains the assumed property in an intersecting scope
1803
+ % outside_validity the only such constraints are outside their window
1804
+ % disjoint_scope the only constraints on the property are scoped
1805
+ % elsewhere
1806
+ % action a predicate fact naming the action a governing rule
1807
+ % restricts
1808
+ % unmatched anything else
1809
+ assumption_statuses(Assumed, Assumptions, Constraints, GeneralRules, Actions, Statuses) :-
1810
+ findall(cover(Key, Scope, Applicability),
1811
+ ( member(rc(Member, Applicability, _), Constraints),
1812
+ member_key(Key, Member),
1813
+ member_scope(Member, Scope)
1814
+ ; member(gr(_, _, _, Scope, Applicability, _, ReadKeys, _), GeneralRules),
1815
+ member(Key, ReadKeys)
1816
+ ),
1817
+ Covers),
1818
+ maplist(assumption_status(Assumptions, Covers, Actions), Assumed, Statuses).
1819
+
1820
+ assumption_status(Assumptions, Covers, Actions, FactId, FactId-Status) :-
1821
+ ( member(Assumption, Assumptions),
1822
+ member_fact(Assumption, FactId)
1823
+ -> property_assumption_status(Assumption, Covers, Status)
1824
+ ; kb:predicate_fact(FactId, Namespace, Name, _, assert),
1825
+ memberchk(Namespace-Name, Actions)
1826
+ -> Status = action
1827
+ ; Status = unmatched
1828
+ ).
1829
+
1830
+ property_assumption_status(Assumption, Covers, Status) :-
1831
+ member_key(Key, Assumption),
1832
+ member_scope(Assumption, AssumedScope),
1833
+ ( member(cover(Key, Scope, Applicability), Covers),
1834
+ Applicability \== outside,
1835
+ kb:scope_intersects(Scope, AssumedScope)
1836
+ -> Status = governed
1837
+ ; member(cover(Key, Scope, outside), Covers),
1838
+ kb:scope_intersects(Scope, AssumedScope)
1839
+ -> Status = outside_validity
1840
+ ; memberchk(cover(Key, _, _), Covers)
1841
+ -> Status = disjoint_scope
1842
+ ; Status = unmatched
1843
+ ).
1844
+
1845
+ %% unsat_core(+Members, -Core)
1846
+ % Members admit no common value. Drop each member in turn while the rest
1847
+ % still admits none; what remains is irreducible.
1848
+ unsat_core(Members, Core) :-
1849
+ unsat_core_(Members, [], Core).
1850
+
1851
+ unsat_core_([], Kept, Kept).
1852
+ unsat_core_([Member|Rest], Kept, Core) :-
1853
+ append(Kept, Rest, Others),
1854
+ ( \+ members_satisfiable(Others)
1855
+ -> unsat_core_(Rest, Kept, Core)
1856
+ ; append(Kept, [Member], Kept1),
1857
+ unsat_core_(Rest, Kept1, Core)
1858
+ ).
1859
+
1860
+ %% members_comparable(+Members)
1861
+ % Every member can be translated, and every two members have compatible
1862
+ % types (equal, or int and number) and compatible canonical units (equal,
1863
+ % or one unspecified).
1864
+ members_comparable(Members) :-
1865
+ forall(member(Member, Members), member_translatable(Member)),
1866
+ forall(
1867
+ ( select(Left, Members, Others),
1868
+ member(Right, Others)
1869
+ ),
1870
+ members_compatible(Left, Right)
1871
+ ).
1872
+
1873
+ member_translatable(m(_, _, _, _, _, Type, _, Op, Value, _)) :-
1874
+ ( kb:is_numeric_type(Type)
1875
+ -> number(Value),
1876
+ memberchk(Op, [eq, neq, lt, lte, gt, gte])
1877
+ ; memberchk(Op, [eq, neq])
1878
+ ).
1879
+
1880
+ members_compatible(m(_, _, _, _, _, TypeA, UnitA, _, _, _), m(_, _, _, _, _, TypeB, UnitB, _, _, _)) :-
1881
+ kb:compatible_types(TypeA, TypeB),
1882
+ kb:unit_compatible(UnitA, UnitB).
1883
+
1884
+ % Assumptions that cannot be translated or are incompatible with another
1885
+ % member; when the incompatibility lies elsewhere, every assumption of the
1886
+ % context is undecided.
1887
+ incomparable_facts(As, Others, Ids) :-
1888
+ append(As, Others, Members),
1889
+ include(assumption_incomparable(Members), As, Incomparable),
1890
+ ( Incomparable == []
1891
+ -> maplist(member_fact, As, Ids0)
1892
+ ; maplist(member_fact, Incomparable, Ids0)
1893
+ ),
1894
+ sort(Ids0, Ids).
1895
+
1896
+ assumption_incomparable(Members, Assumption) :-
1897
+ member(Other, Members),
1898
+ Other \== Assumption,
1899
+ \+ members_compatible(Assumption, Other),
1900
+ !.
1901
+ assumption_incomparable(_, Assumption) :-
1902
+ \+ member_translatable(Assumption).
1903
+
1904
+ %% members_satisfiable(+Members)
1905
+ % Some value satisfies every member. Members must be comparable. Numeric
1906
+ % members are decided by intervals.pl over the integers only when every
1907
+ % member is declared int; other types support eq and neq.
1908
+ members_satisfiable([]) :- !.
1909
+ members_satisfiable(Members) :-
1910
+ Members = [m(_, _, _, _, _, Type, _, _, _, _)|_],
1911
+ ( kb:is_numeric_type(Type)
1912
+ -> maplist(member_constraint(Variable), Members, Constraints),
1913
+ ( forall(member(m(_, _, _, _, _, MemberType, _, _, _, _), Members), MemberType == int)
1914
+ -> Integers = [Variable]
1915
+ ; Integers = []
1916
+ ),
1917
+ numeric_constraints_satisfiable(Constraints, Integers)
1918
+ ; symbolic_members_satisfiable(Type, Members)
1919
+ ).
1920
+
1921
+ member_constraint(Variable, m(_, _, _, _, _, _, _, Op, Value, _), c(Op, Variable, Value)).
1922
+
1923
+ % Symbolic values compare by text: an authored string and a rule constant
1924
+ % with the same spelling are the same value.
1925
+ symbolic_members_satisfiable(Type, Members) :-
1926
+ findall(Value,
1927
+ ( member(m(_, _, _, _, _, _, _, eq, Raw, _), Members),
1928
+ symbolic_value(Raw, Value)
1929
+ ),
1930
+ Equal0),
1931
+ sort(Equal0, Equal),
1932
+ findall(Value,
1933
+ ( member(m(_, _, _, _, _, _, _, neq, Raw, _), Members),
1934
+ symbolic_value(Raw, Value)
1935
+ ),
1936
+ Different0),
1937
+ sort(Different0, Different),
1938
+ symbolic_values_satisfiable(Type, Equal, Different).
1939
+
1940
+ symbolic_value(Raw, Value) :-
1941
+ ( atom(Raw) -> Value = Raw
1942
+ ; string(Raw) -> atom_string(Value, Raw)
1943
+ ; term_to_atom(Raw, Value)
1944
+ ).
1945
+
1946
+ % At most one required value, not excluded; with none required, a bool
1947
+ % property still needs one of true and false left. Two different required
1948
+ % values never hold together.
1949
+ symbolic_values_satisfiable(_, [Value], Different) :-
1950
+ \+ value_excluded(Value, Different).
1951
+ symbolic_values_satisfiable(Type, [], Different) :-
1952
+ \+ bool_domain_exhausted(Type, Different).
1953
+
1954
+ value_excluded(Value, Different) :-
1955
+ member(Excluded, Different),
1956
+ Excluded == Value,
1957
+ !.
1958
+
1959
+ bool_domain_exhausted(bool, Different) :-
1960
+ memberchk(true, Different),
1961
+ memberchk(false, Different).
1962
+
1963
+ %% check_scenario_feasibility_unknown(-Violations)
1964
+ % implements REQ-kibi-scenario-feasibility-v2
1965
+ % Advisory: success scenarios whose feasibility the checker cannot decide.
1966
+ check_scenario_feasibility_unknown(Violations) :-
1967
+ findall(Violation, scenario_feasibility_unknown_violation(Violation), Unsorted),
1968
+ sort(Unsorted, Violations).
1969
+
1970
+ scenario_feasibility_unknown_violation(violation(
1971
+ 'scenario-feasibility-unknown',
1972
+ ScenarioId,
1973
+ Description,
1974
+ Suggestion,
1975
+ Source
1976
+ )) :-
1977
+ scenario_feasibility_outcome(ScenarioId, unknown(Reason)),
1978
+ unknown_feasibility_text(Reason, Description, Suggestion),
1979
+ ( kb_entity(ScenarioId, scenario, Props),
1980
+ memberchk(source=Source, Props)
1981
+ -> true
1982
+ ; Source = ""
1983
+ ).
1984
+
1985
+ unknown_feasibility_text(no_assumptions,
1986
+ "Scenario expects success but assumes nothing, so its feasibility against current requirements is unknown",
1987
+ "Link the property values the scenario relies on with assumes facts so feasibility can be checked").
1988
+ unknown_feasibility_text(contradictory_assumptions(FactIds), Description,
1989
+ "Correct the assumed facts so they can hold together; a scenario whose assumptions contradict each other describes no situation") :-
1990
+ atomic_list_concat(FactIds, ', ', FactText),
1991
+ format(string(Description),
1992
+ "Scenario expects success but its feasibility is unknown: assumptions ~w cannot hold together",
1993
+ [FactText]).
1994
+ unknown_feasibility_text(unmatched_assumption(FactIds), Description,
1995
+ "Constrain the assumed subject and property with a current requirement, correct the assumed fact's subject_key/property_key, or, for a requirement whose rule restricts an action, link the scenario to that requirement with specified_by") :-
1996
+ atomic_list_concat(FactIds, ', ', FactText),
1997
+ format(string(Description),
1998
+ "Scenario expects success but its feasibility is unknown: assumption ~w is not constrained by any current requirement that governs this scenario",
1999
+ [FactText]).
2000
+ unknown_feasibility_text(incomparable_assumption(FactIds), Description,
2001
+ "Align the value_type and unit of the assumed fact with the requirement facts on that property (int and number compare; units must match or be convertible), and use eq or neq for non-numeric values") :-
2002
+ atomic_list_concat(FactIds, ', ', FactText),
2003
+ format(string(Description),
2004
+ "Scenario expects success but its feasibility is unknown: assumption ~w cannot be compared with the requirement constraints on its property (type, unit or operator mismatch)",
2005
+ [FactText]).
2006
+ unknown_feasibility_text(conflicting_requirements(ReqIds), Description,
2007
+ "Resolve the conflict between these requirements (see the contradiction checks) before judging the scenario") :-
2008
+ atomic_list_concat(ReqIds, ', ', ReqText),
2009
+ format(string(Description),
2010
+ "Scenario expects success but its feasibility is unknown: requirements ~w admit no common value for an assumed property",
2011
+ [ReqText]).
2012
+ unknown_feasibility_text(undecided_rule(FactIds), Description,
2013
+ "Assume values for every property the rule reads so its conditions are decided, or restate the rule with one comparison per property (forbid ... unless, or a permit-only-when constraint)") :-
2014
+ atomic_list_concat(FactIds, ', ', FactText),
2015
+ format(string(Description),
2016
+ "Scenario expects success but its feasibility is unknown: the assumptions neither satisfy nor refute the conditions of rule ~w",
2017
+ [FactText]).
2018
+ unknown_feasibility_text(undetermined_validity(FactIds), Description,
2019
+ "State when the scenario happens by setting valid_from/valid_to on its assumed facts, or bound the scenario inside or outside the requirement's validity window") :-
2020
+ atomic_list_concat(FactIds, ', ', FactText),
2021
+ format(string(Description),
2022
+ "Scenario expects success but its feasibility is unknown: it conflicts with ~w, whose validity window may or may not cover the scenario's time",
2023
+ [FactText]).
2024
+
2025
+ %% check_exception_claim_keys(-Violations)
2026
+ % implements REQ-kibi-scenario-feasibility-v2
2027
+ % An exception requirement narrows its waiver to individual clauses of the
2028
+ % requirement it exempts by listing their claim keys in exempts_claims. Each
2029
+ % listed key must be a claim of an exempted requirement (its logic_claims, its
2030
+ % semantic inventory or a linked ground fact): a key that names nothing would
2031
+ % silently waive nothing. exempts_claims without an exempts link has nothing
2032
+ % to narrow.
2033
+ check_exception_claim_keys(Violations) :-
2034
+ findall(Violation, exception_claim_key_violation(Violation), Unsorted),
2035
+ sort(Unsorted, Violations).
2036
+
2037
+ exception_claim_key_violation(violation(
2038
+ 'exception-claim-keys',
2039
+ ExceptionId,
2040
+ Description,
2041
+ Suggestion,
2042
+ Source
2043
+ )) :-
2044
+ kb_entity(ExceptionId, req, _),
2045
+ exception_claim_keys(ExceptionId, ClaimKeys),
2046
+ findall(BaseId, kb_relationship(exempts, ExceptionId, BaseId), Bases0),
2047
+ sort(Bases0, Bases),
2048
+ ( Bases == []
2049
+ -> Description = "Requirement lists exempts_claims but exempts no requirement",
2050
+ Suggestion = "Link the exception to the requirement whose clauses it waives with exempts, or remove exempts_claims"
2051
+ ; member(ClaimKey, ClaimKeys),
2052
+ \+ ( member(BaseId, Bases), requirement_declares_claim(BaseId, ClaimKey) ),
2053
+ atomic_list_concat(Bases, ', ', BaseText),
2054
+ format(string(Description),
2055
+ "exempts_claims names ~w, which is not a claim of exempted requirement ~w",
2056
+ [ClaimKey, BaseText]),
2057
+ format(string(Suggestion),
2058
+ "Use a claim key from the logic_claims or semantic_inventory of ~w, or remove ~w from exempts_claims",
2059
+ [BaseText, ClaimKey])
2060
+ ),
2061
+ violation_source(ExceptionId, req, Source).
2062
+
2063
+ requirement_declares_claim(ReqId, ClaimKey) :-
2064
+ requirement_logic_claims(ReqId, ClaimKeys),
2065
+ memberchk(ClaimKey, ClaimKeys),
2066
+ !.
2067
+ requirement_declares_claim(ReqId, ClaimKey) :-
2068
+ requirement_inventory_status(ReqId, ClaimKey, _),
2069
+ !.
2070
+ requirement_declares_claim(ReqId, ClaimKey) :-
2071
+ grounded_requirement_claim(ReqId, ClaimKey),
2072
+ !.
2073
+
2074
+ %% check_numeric_string_values(-Violations)
2075
+ % implements REQ-kibi-truthful-consistency
2076
+ % Advisory: a property_value fact stores a numeric-looking value as a string
2077
+ % where the comparison is numeric, because its operator orders values (lt,
2078
+ % lte, gt, gte) or another fact on the same subject and property is typed int
2079
+ % or number. Strings compare only by equality and are never coerced, so such
2080
+ % a fact silently drops out of numeric contradiction and feasibility checks.
2081
+ % The finding names the fact and the typed value to use instead.
2082
+ check_numeric_string_values(Violations) :-
2083
+ findall(Violation, numeric_string_value_violation(Violation), Unsorted),
2084
+ sort(Unsorted, Violations).
2085
+
2086
+ numeric_string_value_violation(violation(
2087
+ 'numeric-string-value',
2088
+ FactId,
2089
+ Description,
2090
+ Suggestion,
2091
+ Source
2092
+ )) :-
2093
+ kb:fact_property_tuple(FactId, Subject, Property, Op, string, Value, _Unit, _Scope, _Polarity),
2094
+ numeric_literal(Value, Literal, Number),
2095
+ numeric_string_context(FactId, Subject, Property, Op, Why),
2096
+ ( integer(Number)
2097
+ -> TypedType = int, ValueField = value_int
2098
+ ; TypedType = number, ValueField = value_number
2099
+ ),
2100
+ format(string(Description),
2101
+ "Fact ~w stores ~w.~w value \"~w\" as value_type string, but ~w; string values compare only by equality and are not converted to numbers",
2102
+ [FactId, Subject, Property, Literal, Why]),
2103
+ format(string(Suggestion),
2104
+ "Set value_type: ~w and ~w: ~w on ~w (replacing value_string) so the value takes part in numeric comparisons",
2105
+ [TypedType, ValueField, Number, FactId]),
2106
+ violation_source(FactId, fact, Source).
2107
+
2108
+ % A plain decimal literal: optional minus sign, digits, optional fraction.
2109
+ numeric_literal(Value, Trimmed, Number) :-
2110
+ ( string(Value) -> Text = Value
2111
+ ; atom(Value) -> atom_string(Value, Text)
2112
+ ),
2113
+ normalize_space(string(Trimmed), Text),
2114
+ string_codes(Trimmed, Codes),
2115
+ phrase(decimal_literal, Codes),
2116
+ number_string(Number, Trimmed).
2117
+
2118
+ decimal_literal --> optional_minus, decimal_digits, optional_fraction.
2119
+
2120
+ optional_minus --> "-", !.
2121
+ optional_minus --> [].
2122
+
2123
+ decimal_digits --> [Digit], { code_type(Digit, digit) }, more_decimal_digits.
2124
+
2125
+ more_decimal_digits --> [Digit], { code_type(Digit, digit) }, !, more_decimal_digits.
2126
+ more_decimal_digits --> [].
2127
+
2128
+ optional_fraction --> ".", !, decimal_digits.
2129
+ optional_fraction --> [].
2130
+
2131
+ numeric_string_context(_, _, _, Op, Why) :-
2132
+ memberchk(Op, [lt, lte, gt, gte]),
2133
+ !,
2134
+ format(string(Why), "its operator ~w orders values numerically", [Op]).
2135
+ numeric_string_context(FactId, Subject, Property, _, Why) :-
2136
+ kb:fact_property_tuple(PeerId, Subject, Property, _, PeerType, _, _, _, _),
2137
+ PeerId \== FactId,
2138
+ kb:is_numeric_type(PeerType),
2139
+ !,
2140
+ format(string(Why), "~w types the same property as ~w", [PeerId, PeerType]).
2141
+
527
2142
  %% check_req_status_vocabulary(-Violations)
528
2143
  % Rejects requirement statuses outside the canonical+legacy vocabulary.
529
2144
  % Requirement documents carrying ADR vocabulary (e.g. `status: accepted`)
@@ -806,6 +2421,152 @@ check_domain_contradiction_witnesses(Witnesses) :-
806
2421
  append(GroundWitnesses, RuleWitnesses, Witnesses0),
807
2422
  sort(Witnesses0, Witnesses).
808
2423
 
2424
+ %% what_if_contradiction_witnesses(+Entries, -Witnesses)
2425
+ % implements REQ-kibi-truthful-consistency
2426
+ % Contradiction witnesses for the KB as it would be after staging Entries,
2427
+ % computed inside an RDF transaction that is always rolled back. Entries are
2428
+ % upsert(Type, Props, Relationships) or relate(Relationships) terms, with
2429
+ % relationships as rel(Type, From, To, Metadata). Staging validates entities
2430
+ % and relationships exactly as a commit would, so an invalid plan raises the
2431
+ % same error here before anything is written. See what_if_analysis/2 for the
2432
+ % before/after comparison that also covers scenario feasibility.
2433
+ what_if_contradiction_witnesses(Entries, Witnesses) :-
2434
+ what_if_analysis(Entries, Analysis),
2435
+ Witnesses = Analysis.witnesses.
2436
+
2437
+ what_if_contradiction_witnesses_json(Entries, JsonString) :-
2438
+ what_if_contradiction_witnesses(Entries, Witnesses),
2439
+ with_output_to_string(
2440
+ json_write_dict(current_output, Witnesses, [width(0)]),
2441
+ JsonString
2442
+ ).
2443
+
2444
+ %% what_if_analysis(+Entries, -Analysis)
2445
+ % implements REQ-kibi-truthful-consistency
2446
+ % Compare the current KB with the KB after staging Entries (rolled back).
2447
+ % Both sides cover domain contradictions (property, predicate and rule
2448
+ % witnesses) and scenario infeasibility witnesses (kind scenario_feasibility).
2449
+ % Analysis is a dict with
2450
+ % witnesses staged contradiction witnesses (what_if_contradiction_witnesses/2)
2451
+ % before every witness of the current KB
2452
+ % after every witness of the staged KB
2453
+ % introduced after-witnesses with no matching current witness
2454
+ % removed current witnesses the plan resolves
2455
+ % unchanged after-witnesses that already existed
2456
+ % Witnesses are matched by kind, status, requirements, facts and scenario, so
2457
+ % rewording a fact's prose does not make an existing conflict look new.
2458
+ % The entity index is not transactional, so every staged id is re-indexed
2459
+ % from the restored store.
2460
+ what_if_analysis(Entries, Analysis) :-
2461
+ findall(Id, (member(Entry, Entries), what_if_entry_id(Entry, Id)), Ids0),
2462
+ sort(Ids0, Ids),
2463
+ with_kb_mutex(
2464
+ call_cleanup(
2465
+ ( checks:what_if_witnesses(BeforeContradictions, BeforeInfeasible),
2466
+ catch(
2467
+ rdf_transaction((
2468
+ checks:what_if_stage(Entries),
2469
+ checks:what_if_witnesses(StagedContradictions, StagedInfeasible),
2470
+ throw(kibi_what_if_result(StagedContradictions, StagedInfeasible))
2471
+ )),
2472
+ kibi_what_if_result(AfterContradictions, AfterInfeasible),
2473
+ true
2474
+ )
2475
+ ),
2476
+ forall(member(Id, Ids), kb:kb_refresh_entity_index(Id))
2477
+ )
2478
+ ),
2479
+ append(BeforeContradictions, BeforeInfeasible, Before),
2480
+ append(AfterContradictions, AfterInfeasible, After),
2481
+ include(what_if_witness_absent(Before), After, Introduced),
2482
+ include(what_if_witness_absent(After), Before, Removed),
2483
+ exclude(what_if_witness_absent(Before), After, Unchanged),
2484
+ Analysis = _{
2485
+ witnesses: AfterContradictions,
2486
+ before: Before,
2487
+ after: After,
2488
+ introduced: Introduced,
2489
+ removed: Removed,
2490
+ unchanged: Unchanged
2491
+ }.
2492
+
2493
+ what_if_analysis_json(Entries, JsonString) :-
2494
+ what_if_analysis(Entries, Analysis),
2495
+ with_output_to_string(
2496
+ json_write_dict(current_output, Analysis, [width(0)]),
2497
+ JsonString
2498
+ ).
2499
+
2500
+ what_if_witnesses(Contradictions, Infeasible) :-
2501
+ check_domain_contradiction_witnesses(Contradictions),
2502
+ findall(Witness, scenario_infeasibility_witness(Witness), Infeasible0),
2503
+ sort(Infeasible0, Infeasible).
2504
+
2505
+ % implements REQ-kibi-scenario-feasibility-v2
2506
+ scenario_infeasibility_witness(_{
2507
+ kind: scenario_feasibility,
2508
+ status: infeasible,
2509
+ requirements: ReqIds,
2510
+ scenario: ScenarioId,
2511
+ assumedFacts: AssumedFacts,
2512
+ requirementFacts: ReqFacts,
2513
+ reason: ReasonText
2514
+ }) :-
2515
+ infeasible_scenario(ScenarioId, ReqIds, AssumedFacts, ReqFacts, Reason),
2516
+ atomic_list_concat(AssumedFacts, ', ', AssumedText),
2517
+ atomic_list_concat(ReqIds, ', ', ReqText),
2518
+ atomic_list_concat(ReqFacts, ', ', ReqFactText),
2519
+ format(string(ReasonText),
2520
+ "Scenario ~w expects success but assumes ~w, which ~w forbids via ~w: ~w",
2521
+ [ScenarioId, AssumedText, ReqText, ReqFactText, Reason]).
2522
+
2523
+ what_if_witness_absent(Witnesses, Witness) :-
2524
+ what_if_witness_key(Witness, Key),
2525
+ \+ ( member(Other, Witnesses), what_if_witness_key(Other, Key) ).
2526
+
2527
+ what_if_witness_key(Witness, key(Kind, Status, Requirements, Facts, Scenario)) :-
2528
+ witness_field(Witness, kind, Kind),
2529
+ witness_field(Witness, status, Status),
2530
+ witness_field(Witness, requirements, Requirements),
2531
+ witness_field(Witness, scenario, Scenario),
2532
+ findall(FactId,
2533
+ ( member(Side, [left, right]),
2534
+ get_dict(Side, Witness, SideDict),
2535
+ is_dict(SideDict),
2536
+ get_dict(factId, SideDict, FactId)
2537
+ ; member(Field, [assumedFacts, requirementFacts]),
2538
+ get_dict(Field, Witness, FieldFacts),
2539
+ member(FactId, FieldFacts)
2540
+ ),
2541
+ Facts0),
2542
+ sort(Facts0, Facts).
2543
+
2544
+ witness_field(Witness, Field, Value) :-
2545
+ ( get_dict(Field, Witness, Value0) -> Value = Value0 ; Value = none ).
2546
+
2547
+ what_if_entry_id(upsert(_, Props, _), Id) :- memberchk(id=Id, Props).
2548
+ what_if_entry_id(upsert(_, _, Rels), Id) :- what_if_relationship_id(Rels, Id).
2549
+ what_if_entry_id(relate(Rels), Id) :- what_if_relationship_id(Rels, Id).
2550
+
2551
+ what_if_relationship_id(Rels, Id) :-
2552
+ member(rel(_, From, To, _), Rels),
2553
+ member(Id, [From, To]).
2554
+
2555
+ % Entities first, then every relationship, so a step may relate to an entity
2556
+ % that a later step of the same plan creates.
2557
+ what_if_stage(Entries) :-
2558
+ forall(member(upsert(Type, Props, _), Entries),
2559
+ kb_assert_entity_no_audit(Type, Props)),
2560
+ forall(
2561
+ ( member(Entry, Entries),
2562
+ (Entry = upsert(_, _, Rels) ; Entry = relate(Rels))
2563
+ ),
2564
+ what_if_stage_relationships(Rels)).
2565
+
2566
+ what_if_stage_relationships(Rels) :-
2567
+ forall(member(rel(Type, From, To, Metadata), Rels),
2568
+ kb_assert_relationship_no_audit(Type, From, To, Metadata)).
2569
+
809
2570
  contradiction_witness_violation(Witness, violation(
810
2571
  'domain-contradictions',
811
2572
  EntityId,
@@ -825,13 +2586,17 @@ contradiction_witness_violation(Witness, violation(
825
2586
  opposing_rule_requirements(ReqA, ReqB, RuleA, RuleB) :-
826
2587
  opposing_rule_requirement_facts(ReqA, ReqB, _FactA, _FactB, RuleA, RuleB).
827
2588
 
2589
+ % Each unordered pair of (requirement, rule fact) endpoints is compared once,
2590
+ % ordered by the requirement-fact pair: ordering requirements and facts
2591
+ % independently would never compare REQ-A->FACT-Z with REQ-Z->FACT-A.
828
2592
  opposing_rule_requirement_facts(ReqA, ReqB, FactA, FactB, RuleA, RuleB) :-
829
2593
  kb:current_req(ReqA),
830
2594
  kb:current_req(ReqB),
831
- ReqA @< ReqB,
2595
+ ReqA \== ReqB,
832
2596
  kb_relationship(requires_rule, ReqA, FactA),
833
2597
  kb_relationship(requires_rule, ReqB, FactB),
834
- FactA @< FactB,
2598
+ FactA \== FactB,
2599
+ ReqA-FactA @< ReqB-FactB,
835
2600
  kb_entity(FactA, fact, PropsA),
836
2601
  kb_entity(FactB, fact, PropsB),
837
2602
  memberchk(fact_kind=KindA, PropsA),
@@ -859,6 +2624,51 @@ rule_contradiction_witness(Witness) :-
859
2624
  comparison: Comparison
860
2625
  }.
861
2626
 
2627
+ %% check_rule_key_arguments_missing(-Violations)
2628
+ % implements REQ-kibi-truthful-consistency
2629
+ % Advisory: an opposing rule pair that stays unresolved only because a body
2630
+ % predicate declares no key_arguments. Without the declaration the
2631
+ % predicate is multivalued, so atoms of the two bodies are never read as the
2632
+ % same fact; declaring the listed key positions would decide the pair. The
2633
+ % finding names the requirements, the rule facts, the predicate
2634
+ % (namespace:name/arity) and the deciding key positions (with argument names
2635
+ % when a predicate_schema exists). It never claims the predicate is
2636
+ % functional: a genuinely multivalued predicate should stay undeclared.
2637
+ check_rule_key_arguments_missing(Violations) :-
2638
+ functional_predicate_declarations(Functional),
2639
+ findall(Violation, rule_key_arguments_missing_violation(Functional, Violation), Unsorted),
2640
+ sort(Unsorted, Violations).
2641
+
2642
+ rule_key_arguments_missing_violation(Functional, violation(
2643
+ 'rule-key-arguments-missing',
2644
+ EntityId,
2645
+ Description,
2646
+ Suggestion,
2647
+ ""
2648
+ )) :-
2649
+ opposing_rule_requirement_facts(ReqA, ReqB, FactA, FactB, RuleA, RuleB),
2650
+ logic_rule_missing_keys(RuleA, RuleB, Functional, Missing),
2651
+ member(missing_keys(Namespace, Name, Arity, KeySets), Missing),
2652
+ format(string(EntityId), "~w/~w", [ReqA, ReqB]),
2653
+ maplist(key_set_text(Namespace, Name, Arity), KeySets, KeyTexts),
2654
+ atomic_list_concat(KeyTexts, ' or ', KeyText),
2655
+ format(string(Description),
2656
+ "Opposing rules ~w (~w) and ~w (~w) stay unresolved because predicate ~w:~w/~w declares no key_arguments; declaring key_arguments ~w would decide the pair",
2657
+ [FactA, ReqA, FactB, ReqB, Namespace, Name, Arity, KeyText]),
2658
+ format(string(Suggestion),
2659
+ "If the remaining arguments of ~w:~w/~w are determined by ~w, add key_arguments to its predicate_schema fact; if the predicate is genuinely multivalued, leave it undeclared and accept the unresolved result",
2660
+ [Namespace, Name, Arity, KeyText]).
2661
+
2662
+ key_set_text(Namespace, Name, Arity, Keys, Text) :-
2663
+ ( kb:predicate_schema(_, Namespace, Name, Arity, ArgumentNames, _),
2664
+ length(ArgumentNames, Arity)
2665
+ -> findall(ArgumentName, (member(Position, Keys), nth1(Position, ArgumentNames, ArgumentName)), Names),
2666
+ atomic_list_concat(Names, ', ', NameText),
2667
+ format(atom(Text), '[~w]', [NameText])
2668
+ ; atomic_list_concat(Keys, ', ', PositionText),
2669
+ format(atom(Text), '[positions ~w]', [PositionText])
2670
+ ).
2671
+
862
2672
  rule_conflict_side(ReqId, FactId, Side) :-
863
2673
  evidence_entity_source(ReqId, req, RequirementSource),
864
2674
  kb_entity(FactId, fact, Props),
@@ -1599,7 +3409,16 @@ check_selected_dispatch(Rules, _{
1599
3409
  no_cycles: Cycles,
1600
3410
  required_fields: RequiredFields,
1601
3411
  deprecated_adr_no_successor: DeprecatedADRs,
3412
+ superseded_requirement_open: SupersededRequirementOpen,
3413
+ symbol_owner_superseded: SymbolOwnerSuperseded,
3414
+ adr_unlinked: AdrUnlinked,
3415
+ adr_proposed: AdrProposed,
3416
+ scenario_feasibility: ScenarioFeasibility,
3417
+ scenario_feasibility_unknown: ScenarioFeasibilityUnknown,
3418
+ exception_claim_keys: ExceptionClaimKeys,
3419
+ numeric_string_value: NumericStringValues,
1602
3420
  domain_contradictions: Contradictions,
3421
+ rule_key_arguments_missing: RuleKeyArgumentsMissing,
1603
3422
  strict_fact_shape: StrictFactShape,
1604
3423
  strict_req_fact_pairing: StrictReqFactPairing,
1605
3424
  strict_readiness: StrictReadiness,
@@ -1624,7 +3443,16 @@ check_selected_dispatch(Rules, _{
1624
3443
  selected_rule(Rules, 'no-cycles', check_no_cycles, Cycles),
1625
3444
  selected_rule(Rules, 'required-fields', check_required_fields, RequiredFields),
1626
3445
  selected_rule(Rules, 'deprecated-adr-no-successor', check_deprecated_adrs, DeprecatedADRs),
3446
+ selected_rule(Rules, 'superseded-requirement-open', check_superseded_requirement_open, SupersededRequirementOpen),
3447
+ selected_rule(Rules, 'symbol-owner-superseded', check_symbol_owner_superseded, SymbolOwnerSuperseded),
3448
+ selected_rule(Rules, 'adr-unlinked', check_adr_unlinked, AdrUnlinked),
3449
+ selected_rule(Rules, 'adr-proposed', check_adr_proposed, AdrProposed),
3450
+ selected_rule(Rules, 'scenario-feasibility', check_scenario_feasibility, ScenarioFeasibility),
3451
+ selected_rule(Rules, 'scenario-feasibility-unknown', check_scenario_feasibility_unknown, ScenarioFeasibilityUnknown),
3452
+ selected_rule(Rules, 'exception-claim-keys', check_exception_claim_keys, ExceptionClaimKeys),
3453
+ selected_rule(Rules, 'numeric-string-value', check_numeric_string_values, NumericStringValues),
1627
3454
  selected_rule(Rules, 'domain-contradictions', check_domain_contradictions, Contradictions),
3455
+ selected_rule(Rules, 'rule-key-arguments-missing', check_rule_key_arguments_missing, RuleKeyArgumentsMissing),
1628
3456
  selected_rule(Rules, 'strict-fact-shape', check_strict_fact_shape, StrictFactShape),
1629
3457
  selected_rule(Rules, 'strict-req-fact-pairing', check_strict_req_fact_pairing, StrictReqFactPairing),
1630
3458
  selected_rule(Rules, 'strict-readiness', check_strict_readiness, StrictReadiness),
@@ -1686,7 +3514,16 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
1686
3514
  check_no_cycles(Cycles),
1687
3515
  check_required_fields(RequiredFields),
1688
3516
  check_deprecated_adrs(DeprecatedADRs),
3517
+ check_superseded_requirement_open(SupersededRequirementOpen),
3518
+ check_symbol_owner_superseded(SymbolOwnerSuperseded),
3519
+ check_adr_unlinked(AdrUnlinked),
3520
+ check_adr_proposed(AdrProposed),
3521
+ check_scenario_feasibility(ScenarioFeasibility),
3522
+ check_scenario_feasibility_unknown(ScenarioFeasibilityUnknown),
3523
+ check_exception_claim_keys(ExceptionClaimKeys),
3524
+ check_numeric_string_values(NumericStringValues),
1689
3525
  check_domain_contradictions(Contradictions),
3526
+ check_rule_key_arguments_missing(RuleKeyArgumentsMissing),
1690
3527
  check_strict_fact_shape(StrictFactShape),
1691
3528
  check_strict_req_fact_pairing(StrictReqFactPairing),
1692
3529
  check_strict_readiness(StrictReadiness),
@@ -1711,7 +3548,16 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
1711
3548
  no_cycles: Cycles,
1712
3549
  required_fields: RequiredFields,
1713
3550
  deprecated_adr_no_successor: DeprecatedADRs,
3551
+ superseded_requirement_open: SupersededRequirementOpen,
3552
+ symbol_owner_superseded: SymbolOwnerSuperseded,
3553
+ adr_unlinked: AdrUnlinked,
3554
+ adr_proposed: AdrProposed,
3555
+ scenario_feasibility: ScenarioFeasibility,
3556
+ scenario_feasibility_unknown: ScenarioFeasibilityUnknown,
3557
+ exception_claim_keys: ExceptionClaimKeys,
3558
+ numeric_string_value: NumericStringValues,
1714
3559
  domain_contradictions: Contradictions,
3560
+ rule_key_arguments_missing: RuleKeyArgumentsMissing,
1715
3561
  strict_fact_shape: StrictFactShape,
1716
3562
  strict_req_fact_pairing: StrictReqFactPairing,
1717
3563
  strict_readiness: StrictReadiness,