kibi-core 0.14.1 → 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/package.json +5 -2
- package/schema/entities.pl +22 -0
- package/schema/relationships.pl +14 -0
- package/schema/validation.pl +45 -1
- package/src/checks.pl +1850 -4
- package/src/discovery.pl +214 -3
- package/src/intervals.pl +175 -0
- package/src/kb.pl +76 -29
- package/src/logic_ir.pl +679 -65
- package/src/requirement_proof.pl +60 -6
- package/src/rule_registry.pl +77 -3
- package/src/semantic_quality.pl +151 -3
- package/src/status.pl +15 -1
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
|
|
2595
|
+
ReqA \== ReqB,
|
|
832
2596
|
kb_relationship(requires_rule, ReqA, FactA),
|
|
833
2597
|
kb_relationship(requires_rule, ReqB, FactB),
|
|
834
|
-
FactA
|
|
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,
|