diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py
index 0e298bfb4e92f31bed85f8e463b02f80f6b549d9..9bd44a8d7d96ea7f14149c30448beb968f7907be 100644
--- a/lib/spack/spack/solver/asp.py
+++ b/lib/spack/spack/solver/asp.py
@@ -8,12 +8,11 @@
 import enum
 import itertools
 import os
-import pathlib
 import pprint
 import re
 import types
 import warnings
-from typing import Callable, Dict, List, NamedTuple, Optional, Sequence, Set, Tuple, Union
+from typing import Dict, List, NamedTuple, Optional, Sequence, Tuple, Union
 
 import archspec.cpu
 
@@ -338,13 +337,6 @@ def __getattr__(self, name):
 
 fn = AspFunctionBuilder()
 
-TransformFunction = Callable[[spack.spec.Spec, List[AspFunction]], List[AspFunction]]
-
-
-def remove_node(spec: spack.spec.Spec, facts: List[AspFunction]) -> List[AspFunction]:
-    """Transformation that removes all "node" and "virtual_node" from the input list of facts."""
-    return list(filter(lambda x: x.args[0] not in ("node", "virtual_node"), facts))
-
 
 def _create_counter(specs, tests):
     strategy = spack.config.CONFIG.get("concretizer:duplicates:strategy", "none")
@@ -692,7 +684,7 @@ def extract_args(model, predicate_name):
 class ErrorHandler:
     def __init__(self, model):
         self.model = model
-        self.full_model = None
+        self.error_args = extract_args(model, "error")
 
     def multiple_values_error(self, attribute, pkg):
         return f'Cannot select a single "{attribute}" for package "{pkg}"'
@@ -700,48 +692,6 @@ def multiple_values_error(self, attribute, pkg):
     def no_value_error(self, attribute, pkg):
         return f'Cannot select a single "{attribute}" for package "{pkg}"'
 
-    def _get_cause_tree(
-        self,
-        cause: Tuple[str, str],
-        conditions: Dict[str, str],
-        condition_causes: List[Tuple[Tuple[str, str], Tuple[str, str]]],
-        seen: Set,
-        indent: str = "        ",
-    ) -> List[str]:
-        """
-        Implementation of recursion for self.get_cause_tree. Much of this operates on tuples
-        (condition_id, set_id) in which the latter idea means that the condition represented by
-        the former held in the condition set represented by the latter.
-        """
-        seen.add(cause)
-        parents = [c for e, c in condition_causes if e == cause and c not in seen]
-        local = "required because %s " % conditions[cause[0]]
-
-        return [indent + local] + [
-            c
-            for parent in parents
-            for c in self._get_cause_tree(
-                parent, conditions, condition_causes, seen, indent=indent + "  "
-            )
-        ]
-
-    def get_cause_tree(self, cause: Tuple[str, str]) -> List[str]:
-        """
-        Get the cause tree associated with the given cause.
-
-        Arguments:
-            cause: The root cause of the tree (final condition)
-
-        Returns:
-            A list of strings describing the causes, formatted to display tree structure.
-        """
-        conditions: Dict[str, str] = dict(extract_args(self.full_model, "condition_reason"))
-        condition_causes: List[Tuple[Tuple[str, str], Tuple[str, str]]] = list(
-            ((Effect, EID), (Cause, CID))
-            for Effect, EID, Cause, CID in extract_args(self.full_model, "condition_cause")
-        )
-        return self._get_cause_tree(cause, conditions, condition_causes, set())
-
     def handle_error(self, msg, *args):
         """Handle an error state derived by the solver."""
         if msg == "multiple_values_error":
@@ -750,31 +700,14 @@ def handle_error(self, msg, *args):
         if msg == "no_value_error":
             return self.no_value_error(*args)
 
-        try:
-            idx = args.index("startcauses")
-        except ValueError:
-            msg_args = args
-            causes = []
-        else:
-            msg_args = args[:idx]
-            cause_args = args[idx + 1 :]
-            cause_args_conditions = cause_args[::2]
-            cause_args_ids = cause_args[1::2]
-            causes = list(zip(cause_args_conditions, cause_args_ids))
-
-        msg = msg.format(*msg_args)
-
         # For variant formatting, we sometimes have to construct specs
         # to format values properly. Find/replace all occurances of
         # Spec(...) with the string representation of the spec mentioned
+        msg = msg.format(*args)
         specs_to_construct = re.findall(r"Spec\(([^)]*)\)", msg)
         for spec_str in specs_to_construct:
             msg = msg.replace("Spec(%s)" % spec_str, str(spack.spec.Spec(spec_str)))
 
-        for cause in set(causes):
-            for c in self.get_cause_tree(cause):
-                msg += f"\n{c}"
-
         return msg
 
     def message(self, errors) -> str:
@@ -786,31 +719,11 @@ def message(self, errors) -> str:
         return "\n".join([header] + messages)
 
     def raise_if_errors(self):
-        initial_error_args = extract_args(self.model, "error")
-        if not initial_error_args:
+        if not self.error_args:
             return
 
-        error_causation = clingo.Control()
-
-        parent_dir = pathlib.Path(__file__).parent
-        errors_lp = parent_dir / "error_messages.lp"
-
-        def on_model(model):
-            self.full_model = model.symbols(shown=True, terms=True)
-
-        with error_causation.backend() as backend:
-            for atom in self.model:
-                atom_id = backend.add_atom(atom)
-                backend.add_rule([atom_id], [], choice=False)
-
-            error_causation.load(str(errors_lp))
-            error_causation.ground([("base", []), ("error_messages", [])])
-            _ = error_causation.solve(on_model=on_model)
-
-        # No choices so there will be only one model
-        error_args = extract_args(self.full_model, "error")
         errors = sorted(
-            [(int(priority), msg, args) for priority, msg, *args in error_args], reverse=True
+            [(int(priority), msg, args) for priority, msg, *args in self.error_args], reverse=True
         )
         try:
             msg = self.message(errors)
@@ -1199,7 +1112,7 @@ def conflict_rules(self, pkg):
         default_msg = "{0}: '{1}' conflicts with '{2}'"
         no_constraint_msg = "{0}: conflicts with '{1}'"
         for trigger, constraints in pkg.conflicts.items():
-            trigger_msg = f"conflict is triggered when {str(trigger)}"
+            trigger_msg = "conflict trigger %s" % str(trigger)
             trigger_spec = spack.spec.Spec(trigger)
             trigger_id = self.condition(
                 trigger_spec, name=trigger_spec.name or pkg.name, msg=trigger_msg
@@ -1211,11 +1124,7 @@ def conflict_rules(self, pkg):
                         conflict_msg = no_constraint_msg.format(pkg.name, trigger)
                     else:
                         conflict_msg = default_msg.format(pkg.name, trigger, constraint)
-
-                spec_for_msg = (
-                    spack.spec.Spec(pkg.name) if constraint == spack.spec.Spec() else constraint
-                )
-                constraint_msg = f"conflict applies to spec {str(spec_for_msg)}"
+                constraint_msg = "conflict constraint %s" % str(constraint)
                 constraint_id = self.condition(constraint, name=pkg.name, msg=constraint_msg)
                 self.gen.fact(
                     fn.pkg_fact(pkg.name, fn.conflict(trigger_id, constraint_id, conflict_msg))
@@ -1371,7 +1280,7 @@ def trigger_rules(self):
         self.gen.h2("Trigger conditions")
         for name in self._trigger_cache:
             cache = self._trigger_cache[name]
-            for (spec_str, _), (trigger_id, requirements) in cache.items():
+            for spec_str, (trigger_id, requirements) in cache.items():
                 self.gen.fact(fn.pkg_fact(name, fn.trigger_id(trigger_id)))
                 self.gen.fact(fn.pkg_fact(name, fn.trigger_msg(spec_str)))
                 for predicate in requirements:
@@ -1384,7 +1293,7 @@ def effect_rules(self):
         self.gen.h2("Imposed requirements")
         for name in self._effect_cache:
             cache = self._effect_cache[name]
-            for (spec_str, _), (effect_id, requirements) in cache.items():
+            for spec_str, (effect_id, requirements) in cache.items():
                 self.gen.fact(fn.pkg_fact(name, fn.effect_id(effect_id)))
                 self.gen.fact(fn.pkg_fact(name, fn.effect_msg(spec_str)))
                 for predicate in requirements:
@@ -1483,26 +1392,18 @@ def variant_rules(self, pkg):
 
             self.gen.newline()
 
-    def condition(
-        self,
-        required_spec: spack.spec.Spec,
-        imposed_spec: Optional[spack.spec.Spec] = None,
-        name: Optional[str] = None,
-        msg: Optional[str] = None,
-        transform_required: Optional[TransformFunction] = None,
-        transform_imposed: Optional[TransformFunction] = remove_node,
-    ):
+    def condition(self, required_spec, imposed_spec=None, name=None, msg=None, node=False):
         """Generate facts for a dependency or virtual provider condition.
 
         Arguments:
-            required_spec: the constraints that triggers this condition
-            imposed_spec: the constraints that are imposed when this condition is triggered
-            name: name for `required_spec` (required if required_spec is anonymous, ignored if not)
-            msg: description of the condition
-            transform_required: transformation applied to facts from the required spec. Defaults
-                to leave facts as they are.
-            transform_imposed: transformation applied to facts from the imposed spec. Defaults
-                to removing "node" and "virtual_node" facts.
+            required_spec (spack.spec.Spec): the spec that triggers this condition
+            imposed_spec (spack.spec.Spec or None): the spec with constraints that
+                are imposed when this condition is triggered
+            name (str or None): name for `required_spec` (required if
+                required_spec is anonymous, ignored if not)
+            msg (str or None): description of the condition
+            node (bool): if False does not emit "node" or "virtual_node" requirements
+                from the imposed spec
         Returns:
             int: id of the condition created by this function
         """
@@ -1520,14 +1421,10 @@ def condition(
 
         cache = self._trigger_cache[named_cond.name]
 
-        named_cond_key = (str(named_cond), transform_required)
+        named_cond_key = str(named_cond)
         if named_cond_key not in cache:
             trigger_id = next(self._id_counter)
             requirements = self.spec_clauses(named_cond, body=True, required_from=name)
-
-            if transform_required:
-                requirements = transform_required(named_cond, requirements)
-
             cache[named_cond_key] = (trigger_id, requirements)
         trigger_id, requirements = cache[named_cond_key]
         self.gen.fact(fn.pkg_fact(named_cond.name, fn.condition_trigger(condition_id, trigger_id)))
@@ -1536,14 +1433,14 @@ def condition(
             return condition_id
 
         cache = self._effect_cache[named_cond.name]
-        imposed_spec_key = (str(imposed_spec), transform_imposed)
+        imposed_spec_key = str(imposed_spec)
         if imposed_spec_key not in cache:
             effect_id = next(self._id_counter)
             requirements = self.spec_clauses(imposed_spec, body=False, required_from=name)
-
-            if transform_imposed:
-                requirements = transform_imposed(imposed_spec, requirements)
-
+            if not node:
+                requirements = list(
+                    filter(lambda x: x.args[0] not in ("node", "virtual_node"), requirements)
+                )
             cache[imposed_spec_key] = (effect_id, requirements)
         effect_id, requirements = cache[imposed_spec_key]
         self.gen.fact(fn.pkg_fact(named_cond.name, fn.condition_effect(condition_id, effect_id)))
@@ -1603,33 +1500,22 @@ def package_dependencies_rules(self, pkg):
                 if not depflag:
                     continue
 
-                msg = f"{pkg.name} depends on {dep.spec}"
+                msg = "%s depends on %s" % (pkg.name, dep.spec.name)
                 if cond != spack.spec.Spec():
-                    msg += f" when {cond}"
+                    msg += " when %s" % cond
                 else:
                     pass
 
-                def track_dependencies(input_spec, requirements):
-                    return requirements + [fn.attr("track_dependencies", input_spec.name)]
-
-                def dependency_holds(input_spec, requirements):
-                    return remove_node(input_spec, requirements) + [
-                        fn.attr(
-                            "dependency_holds", pkg.name, input_spec.name, dt.flag_to_string(t)
-                        )
-                        for t in dt.ALL_FLAGS
-                        if t & depflag
-                    ]
-
-                self.condition(
-                    cond,
-                    dep.spec,
-                    name=pkg.name,
-                    msg=msg,
-                    transform_required=track_dependencies,
-                    transform_imposed=dependency_holds,
+                condition_id = self.condition(cond, dep.spec, pkg.name, msg)
+                self.gen.fact(
+                    fn.pkg_fact(pkg.name, fn.dependency_condition(condition_id, dep.spec.name))
                 )
 
+                for t in dt.ALL_FLAGS:
+                    if t & depflag:
+                        # there is a declared dependency of type t
+                        self.gen.fact(fn.dependency_type(condition_id, dt.flag_to_string(t)))
+
                 self.gen.newline()
 
     def virtual_preferences(self, pkg_name, func):
@@ -1724,17 +1610,8 @@ def emit_facts_from_requirement_rules(self, rules: List[RequirementRule]):
                     when_spec = spack.spec.Spec(pkg_name)
 
                 try:
-                    # With virtual we want to emit "node" and "virtual_node" in imposed specs
-                    transform: Optional[TransformFunction] = remove_node
-                    if virtual:
-                        transform = None
-
                     member_id = self.condition(
-                        required_spec=when_spec,
-                        imposed_spec=spec,
-                        name=pkg_name,
-                        transform_imposed=transform,
-                        msg=f"{spec_str} is a requirement for package {pkg_name}",
+                        required_spec=when_spec, imposed_spec=spec, name=pkg_name, node=virtual
                     )
                 except Exception as e:
                     # Do not raise if the rule comes from the 'all' subsection, since usability
@@ -1797,16 +1674,8 @@ def external_packages(self):
             # Declare external conditions with a local index into packages.yaml
             for local_idx, spec in enumerate(external_specs):
                 msg = "%s available as external when satisfying %s" % (spec.name, spec)
-
-                def external_imposition(input_spec, _):
-                    return [fn.attr("external_conditions_hold", input_spec.name, local_idx)]
-
-                self.condition(
-                    spec,
-                    spack.spec.Spec(spec.name),
-                    msg=msg,
-                    transform_imposed=external_imposition,
-                )
+                condition_id = self.condition(spec, msg=msg)
+                self.gen.fact(fn.pkg_fact(pkg_name, fn.possible_external(condition_id, local_idx)))
                 self.possible_versions[spec.name].add(spec.version)
                 self.gen.newline()
 
@@ -2036,7 +1905,6 @@ class Body:
         if not body:
             for virtual in virtuals:
                 clauses.append(fn.attr("provider_set", spec.name, virtual))
-                clauses.append(fn.attr("virtual_node", virtual))
         else:
             for virtual in virtuals:
                 clauses.append(fn.attr("virtual_on_incoming_edges", spec.name, virtual))
@@ -2671,43 +2539,20 @@ def setup(
         self.define_target_constraints()
 
     def literal_specs(self, specs):
-        for spec in specs:
+        for idx, spec in enumerate(specs):
             self.gen.h2("Spec: %s" % str(spec))
-            condition_id = next(self._id_counter)
-            trigger_id = next(self._id_counter)
+            self.gen.fact(fn.literal(idx))
 
-            # Special condition triggered by "literal_solved"
-            self.gen.fact(fn.literal(trigger_id))
-            self.gen.fact(fn.pkg_fact(spec.name, fn.condition_trigger(condition_id, trigger_id)))
-            self.gen.fact(fn.condition_reason(condition_id, f"{spec} requested explicitly"))
-
-            imposed_spec_key = str(spec), None
-            cache = self._effect_cache[spec.name]
-            if imposed_spec_key in cache:
-                effect_id, requirements = cache[imposed_spec_key]
-            else:
-                effect_id = next(self._id_counter)
-                requirements = self.spec_clauses(spec)
-            root_name = spec.name
-            for clause in requirements:
-                clause_name = clause.args[0]
-                if clause_name == "variant_set":
-                    requirements.append(
-                        fn.attr("variant_default_value_from_cli", *clause.args[1:])
+            self.gen.fact(fn.literal(idx, "virtual_root" if spec.virtual else "root", spec.name))
+            for clause in self.spec_clauses(spec):
+                self.gen.fact(fn.literal(idx, *clause.args))
+                if clause.args[0] == "variant_set":
+                    self.gen.fact(
+                        fn.literal(idx, "variant_default_value_from_cli", *clause.args[1:])
                     )
-                elif clause_name in ("node", "virtual_node", "hash"):
-                    # These facts are needed to compute the "condition_set" of the root
-                    pkg_name = clause.args[1]
-                    self.gen.fact(fn.mentioned_in_literal(trigger_id, root_name, pkg_name))
-
-            requirements.append(fn.attr("virtual_root" if spec.virtual else "root", spec.name))
-            cache[imposed_spec_key] = (effect_id, requirements)
-            self.gen.fact(fn.pkg_fact(spec.name, fn.condition_effect(condition_id, effect_id)))
 
             if self.concretize_everything:
-                self.gen.fact(fn.solve_literal(trigger_id))
-
-        self.effect_rules()
+                self.gen.fact(fn.solve_literal(idx))
 
     def validate_and_define_versions_from_requirements(
         self, *, allow_deprecated: bool, require_checksum: bool
diff --git a/lib/spack/spack/solver/concretize.lp b/lib/spack/spack/solver/concretize.lp
index d5f24ddc3b33f0e58b34f23e1d392d68073395ac..46569f61b27b8861b240534188329b4414004e68 100644
--- a/lib/spack/spack/solver/concretize.lp
+++ b/lib/spack/spack/solver/concretize.lp
@@ -10,8 +10,9 @@
 % ID of the nodes in the "root" link-run sub-DAG
 #const min_dupe_id = 0.
 
-#const direct_link_run = 0.
-#const direct_build = 1.
+#const link_run = 0.
+#const direct_link_run =1.
+#const direct_build = 2.
 
 % Allow clingo to create nodes
 { attr("node", node(0..X-1, Package))         } :- max_dupes(Package, X), not virtual(Package).
@@ -29,21 +30,23 @@
 :- attr("variant_value", PackageNode, _, _), not attr("node", PackageNode).
 :- attr("node_flag_compiler_default", PackageNode), not attr("node", PackageNode).
 :- attr("node_flag", PackageNode, _, _), not attr("node", PackageNode).
+:- attr("node_flag_source", PackageNode, _, _), not attr("node", PackageNode).
 :- attr("no_flags", PackageNode, _), not attr("node", PackageNode).
 :- attr("external_spec_selected", PackageNode, _), not attr("node", PackageNode).
 :- attr("depends_on", ParentNode, _, _), not attr("node", ParentNode).
 :- attr("depends_on", _, ChildNode, _), not attr("node", ChildNode).
 :- attr("node_flag_source", ParentNode, _, _), not attr("node", ParentNode).
 :- attr("node_flag_source", _, _, ChildNode), not attr("node", ChildNode).
-:- attr("virtual_node", VirtualNode), not provider(_, VirtualNode), internal_error("virtual node with no provider").
-:- provider(_, VirtualNode), not attr("virtual_node", VirtualNode), internal_error("provider with no virtual node").
-:- provider(PackageNode, _), not attr("node", PackageNode), internal_error("provider with no real node").
 
-:- attr("root", node(ID, PackageNode)), ID > min_dupe_id, internal_error("root with a non-minimal duplicate ID").
+:- attr("virtual_node", VirtualNode), not provider(_, VirtualNode).
+:- provider(_, VirtualNode), not attr("virtual_node", VirtualNode).
+:- provider(PackageNode, _), not attr("node", PackageNode).
+
+:- attr("root", node(ID, PackageNode)), ID > min_dupe_id.
 
 % Nodes in the "root" unification set cannot depend on non-root nodes if the dependency is "link" or "run"
-:- attr("depends_on", node(min_dupe_id, Package), node(ID, _), "link"), ID != min_dupe_id, unification_set("root", node(min_dupe_id, Package)), internal_error("link dependency out of the root unification set").
-:- attr("depends_on", node(min_dupe_id, Package), node(ID, _), "run"),  ID != min_dupe_id, unification_set("root", node(min_dupe_id, Package)), internal_error("run dependency out of the root unification set").
+:- attr("depends_on", node(min_dupe_id, Package), node(ID, _), "link"), ID != min_dupe_id, unification_set("root", node(min_dupe_id, Package)).
+:- attr("depends_on", node(min_dupe_id, Package), node(ID, _), "run"),  ID != min_dupe_id, unification_set("root", node(min_dupe_id, Package)).
 
 % Rules on "unification sets", i.e. on sets of nodes allowing a single configuration of any given package
 unify(SetID, PackageName) :- unification_set(SetID, node(_, PackageName)).
@@ -83,24 +86,22 @@ unification_set(SetID, VirtualNode)
 %----
 
 % In the "root" unification set only ID = 0 are allowed
-:- unification_set("root", node(ID, _)), ID != 0, internal_error("root unification set has node with non-zero unification set ID").
+:- unification_set("root", node(ID, _)), ID != 0.
 
 % In the "root" unification set we allow only packages from the link-run possible subDAG
-:- unification_set("root", node(_, Package)), not possible_in_link_run(Package), not virtual(Package), internal_error("package outside possible link/run graph in root unification set").
+:- unification_set("root", node(_, Package)), not possible_in_link_run(Package), not virtual(Package).
 
 % Each node must belong to at least one unification set
-:- attr("node", PackageNode), not unification_set(_, PackageNode), internal_error("node belongs to no unification set").
+:- attr("node", PackageNode), not unification_set(_, PackageNode).
 
 % Cannot have a node with an ID, if lower ID of the same package are not used
 :- attr("node", node(ID1, Package)),
    not attr("node", node(ID2, Package)),
-   max_dupes(Package, X), ID1=0..X-1, ID2=0..X-1, ID2 < ID1,
-   internal_error("node skipped id number").
+   max_dupes(Package, X), ID1=0..X-1, ID2=0..X-1, ID2 < ID1.
 
 :- attr("virtual_node", node(ID1, Package)),
    not attr("virtual_node", node(ID2, Package)),
-   max_dupes(Package, X), ID1=0..X-1, ID2=0..X-1, ID2 < ID1,
-   internal_error("virtual node skipped id number").
+   max_dupes(Package, X), ID1=0..X-1, ID2=0..X-1, ID2 < ID1.
 
 %-----------------------------------------------------------------------------
 % Map literal input specs to facts that drive the solve
@@ -114,28 +115,29 @@ multiple_nodes_attribute("depends_on").
 multiple_nodes_attribute("virtual_on_edge").
 multiple_nodes_attribute("provider_set").
 
-trigger_condition_holds(TriggerID, node(min_dupe_id, Package)) :-
-  solve_literal(TriggerID),
-  pkg_fact(Package, condition_trigger(_, TriggerID)),
-  literal(TriggerID).
+% Map constraint on the literal ID to facts on the node
+attr(Name, node(min_dupe_id, A1))             :- literal(LiteralID, Name, A1), solve_literal(LiteralID).
+attr(Name, node(min_dupe_id, A1), A2)         :- literal(LiteralID, Name, A1, A2), solve_literal(LiteralID), not multiple_nodes_attribute(Name).
+attr(Name, node(min_dupe_id, A1), A2, A3)     :- literal(LiteralID, Name, A1, A2, A3), solve_literal(LiteralID), not multiple_nodes_attribute(Name).
+attr(Name, node(min_dupe_id, A1), A2, A3, A4) :- literal(LiteralID, Name, A1, A2, A3, A4), solve_literal(LiteralID).
 
-trigger_node(TriggerID, Node, Node) :-
-  trigger_condition_holds(TriggerID, Node),
-  literal(TriggerID).
+% Special cases where nodes occur in arguments other than A1
+attr("node_flag_source", node(min_dupe_id, A1), A2, node(min_dupe_id, A3)) :- literal(LiteralID, "node_flag_source", A1, A2, A3), solve_literal(LiteralID).
+attr("depends_on",       node(min_dupe_id, A1), node(min_dupe_id, A2), A3) :- literal(LiteralID, "depends_on",       A1, A2, A3), solve_literal(LiteralID).
 
-% Since we trigger the existence of literal nodes from a condition, we need to construct
-% the condition_set/2 manually below
-mentioned_in_literal(Root, Mentioned) :- mentioned_in_literal(TriggerID, Root, Mentioned), solve_literal(TriggerID).
-condition_set(node(min_dupe_id, Root), node(min_dupe_id, Mentioned)) :-  mentioned_in_literal(Root, Mentioned).
+attr("virtual_node", node(min_dupe_id, Virtual))                              :- literal(LiteralID, "provider_set", _,        Virtual), solve_literal(LiteralID).
+attr("provider_set", node(min_dupe_id, Provider), node(min_dupe_id, Virtual)) :- literal(LiteralID, "provider_set", Provider, Virtual), solve_literal(LiteralID).
+provider(node(min_dupe_id, Provider), node(min_dupe_id, Virtual))             :- literal(LiteralID, "provider_set", Provider, Virtual), solve_literal(LiteralID).
 
 % Discriminate between "roots" that have been explicitly requested, and roots that are deduced from "virtual roots"
-explicitly_requested_root(node(min_dupe_id, Package)) :-
-  solve_literal(TriggerID),
-  trigger_and_effect(Package, TriggerID, EffectID),
-  imposed_constraint(EffectID, "root", Package).
+explicitly_requested_root(node(min_dupe_id, A1)) :- literal(LiteralID, "root", A1), solve_literal(LiteralID).
 
 #defined concretize_everything/0.
 #defined literal/1.
+#defined literal/3.
+#defined literal/4.
+#defined literal/5.
+#defined literal/6.
 
 % Attributes for node packages which must have a single value
 attr_single_value("version").
@@ -233,8 +235,7 @@ possible_version_weight(node(ID, Package), Weight)
 
 1 { version_weight(node(ID, Package), Weight) : pkg_fact(Package, version_declared(Version, Weight)) } 1
   :- attr("version", node(ID, Package), Version),
-     attr("node", node(ID, Package)),
-     internal_error("version weights must exist and be unique").
+     attr("node", node(ID, Package)).
 
 % node_version_satisfies implies that exactly one of the satisfying versions
 % is the package's version, and vice versa.
@@ -248,8 +249,7 @@ possible_version_weight(node(ID, Package), Weight)
 % bound on the choice rule to avoid false positives with the error below
 1 { attr("version", node(ID, Package), Version) : pkg_fact(Package, version_satisfies(Constraint, Version)) }
   :- attr("node_version_satisfies", node(ID, Package), Constraint),
-     pkg_fact(Package, version_satisfies(Constraint, _)),
-     internal_error("must choose a single version to satisfy version constraints").
+     pkg_fact(Package, version_satisfies(Constraint, _)).
 
 % More specific error message if the version cannot satisfy some constraint
 % Otherwise covered by `no_version_error` and `versions_conflict_error`.
@@ -362,7 +362,7 @@ imposed_nodes(ConditionID, PackageNode, node(X, A1))
 
 % Conditions that hold impose may impose constraints on other specs
 attr(Name, node(X, A1))             :- impose(ID, PackageNode), imposed_constraint(ID, Name, A1),             imposed_nodes(ID, PackageNode, node(X, A1)).
-attr(Name, node(X, A1), A2)         :- impose(ID, PackageNode), imposed_constraint(ID, Name, A1, A2),         imposed_nodes(ID, PackageNode, node(X, A1)), not multiple_nodes_attribute(Name).
+attr(Name, node(X, A1), A2)         :- impose(ID, PackageNode), imposed_constraint(ID, Name, A1, A2),         imposed_nodes(ID, PackageNode, node(X, A1)).
 attr(Name, node(X, A1), A2, A3)     :- impose(ID, PackageNode), imposed_constraint(ID, Name, A1, A2, A3),     imposed_nodes(ID, PackageNode, node(X, A1)), not multiple_nodes_attribute(Name).
 attr(Name, node(X, A1), A2, A3, A4) :- impose(ID, PackageNode), imposed_constraint(ID, Name, A1, A2, A3, A4), imposed_nodes(ID, PackageNode, node(X, A1)).
 
@@ -373,16 +373,6 @@ attr("node_flag_source", node(X, A1), A2, node(Y, A3))
      imposed_constraint(ID, "node_flag_source", A1, A2, A3),
      condition_set(node(Y, A3), node(X, A1)).
 
-% Provider set is relevant only for literals, since it's the only place where `^[virtuals=foo] bar`
-% might appear in the HEAD of a rule
-attr("provider_set", node(min_dupe_id, Provider), node(min_dupe_id, Virtual))
-  :- solve_literal(TriggerID),
-     trigger_and_effect(_, TriggerID, EffectID),
-     impose(EffectID, _),
-     imposed_constraint(EffectID, "provider_set", Provider, Virtual).
-
-provider(ProviderNode, VirtualNode) :- attr("provider_set", ProviderNode, VirtualNode).
-
 % Here we can't use the condition set because it's a recursive definition, that doesn't define the
 % node index, and leads to unsatisfiability. Hence we say that one and only one node index must
 % satisfy the dependency.
@@ -442,11 +432,24 @@ depends_on(PackageNode, DependencyNode) :- attr("depends_on", PackageNode, Depen
 % concrete. We chop off dependencies for externals, and dependencies of
 % concrete specs don't need to be resolved -- they arise from the concrete
 % specs themselves.
-attr("track_dependencies", Node) :- build(Node), not external(Node).
+dependency_holds(node(NodeID, Package), Dependency, Type) :-
+  pkg_fact(Package, dependency_condition(ID, Dependency)),
+  dependency_type(ID, Type),
+  build(node(NodeID, Package)),
+  not external(node(NodeID, Package)),
+  condition_holds(ID, node(NodeID, Package)).
+
+% We cut off dependencies of externals (as we don't really know them).
+% Don't impose constraints on dependencies that don't exist.
+do_not_impose(EffectID, node(NodeID, Package)) :-
+  not dependency_holds(node(NodeID, Package), Dependency, _),
+  attr("node", node(NodeID, Package)),
+  pkg_fact(Package, dependency_condition(ID, Dependency)),
+  pkg_fact(Package, condition_effect(ID, EffectID)).
 
 % If a dependency holds on a package node, there must be one and only one dependency node satisfying it
 1 { attr("depends_on", PackageNode, node(0..Y-1, Dependency), Type) : max_dupes(Dependency, Y) } 1
-  :- attr("dependency_holds", PackageNode, Dependency, Type),
+  :- dependency_holds(PackageNode, Dependency, Type),
      not virtual(Dependency).
 
 % all nodes in the graph must be reachable from some root
@@ -496,7 +499,7 @@ error(100, "Package '{0}' needs to provide both '{1}' and '{2}' together, but pr
 % if a package depends on a virtual, it's not external and we have a
 % provider for that virtual then it depends on the provider
 node_depends_on_virtual(PackageNode, Virtual, Type)
-  :- attr("dependency_holds", PackageNode, Virtual, Type),
+  :- dependency_holds(PackageNode, Virtual, Type),
      virtual(Virtual),
      not external(PackageNode).
 
@@ -506,7 +509,7 @@ node_depends_on_virtual(PackageNode, Virtual) :- node_depends_on_virtual(Package
   :- node_depends_on_virtual(PackageNode, Virtual, Type).
 
 attr("virtual_on_edge", PackageNode, ProviderNode, Virtual)
-  :- attr("dependency_holds", PackageNode, Virtual, Type),
+  :- dependency_holds(PackageNode, Virtual, Type),
      attr("depends_on", PackageNode, ProviderNode, Type),
      provider(ProviderNode, node(_, Virtual)),
      not external(PackageNode).
@@ -615,11 +618,11 @@ possible_provider_weight(node(ProviderID, Provider), VirtualNode, 100, "fallback
     pkg_fact(Package, version_declared(Version, Weight, "external")) }
     :- external(node(ID, Package)).
 
-error(100, "Attempted to use external for '{0}' which does not satisfy any configured external spec version", Package)
+error(100, "Attempted to use external for '{0}' which does not satisfy any configured external spec", Package)
   :- external(node(ID, Package)),
      not external_version(node(ID, Package), _, _).
 
-error(100, "Attempted to use external for '{0}' which does not satisfy a unique configured external spec version", Package)
+error(100, "Attempted to use external for '{0}' which does not satisfy any configured external spec", Package)
   :- external(node(ID, Package)),
      2 { external_version(node(ID, Package), Version, Weight) }.
 
@@ -648,15 +651,18 @@ external(PackageNode) :- attr("external_spec_selected", PackageNode, _).
 
 % determine if an external spec has been selected
 attr("external_spec_selected", node(ID, Package), LocalIndex) :-
-    attr("external_conditions_hold", node(ID, Package), LocalIndex),
+    external_conditions_hold(node(ID, Package), LocalIndex),
     attr("node", node(ID, Package)),
     not attr("hash", node(ID, Package), _).
 
+external_conditions_hold(node(PackageID, Package), LocalIndex) :-
+    pkg_fact(Package, possible_external(ID, LocalIndex)), condition_holds(ID, node(PackageID, Package)).
+
 % it cannot happen that a spec is external, but none of the external specs
 % conditions hold.
 error(100, "Attempted to use external for '{0}' which does not satisfy any configured external spec", Package)
  :- external(node(ID, Package)),
-    not attr("external_conditions_hold", node(ID, Package), _).
+    not external_conditions_hold(node(ID, Package), _).
 
 %-----------------------------------------------------------------------------
 % Config required semantics
@@ -888,9 +894,8 @@ variant_default_not_used(node(ID, Package), Variant, Value)
 % The variant is set in an external spec
 external_with_variant_set(node(NodeID, Package), Variant, Value)
  :- attr("variant_value", node(NodeID, Package), Variant, Value),
-    condition_requirement(TriggerID, "variant_value", Package, Variant, Value),
-    trigger_and_effect(Package, TriggerID, EffectID),
-    imposed_constraint(EffectID, "external_conditions_hold", Package, _),
+    condition_requirement(ID, "variant_value", Package, Variant, Value),
+    pkg_fact(Package, possible_external(ID, _)),
     external(node(NodeID, Package)),
     attr("node", node(NodeID, Package)).
 
diff --git a/lib/spack/spack/solver/display.lp b/lib/spack/spack/solver/display.lp
index 58d04d42ea30022715adfa57160893248b821f1b..fffffb2c0430bd90511310fbdd48dae563b94468 100644
--- a/lib/spack/spack/solver/display.lp
+++ b/lib/spack/spack/solver/display.lp
@@ -24,29 +24,4 @@
 #show error/5.
 #show error/6.
 
-% for error causation
-#show condition_reason/2.
-
-% For error messages to use later
-#show pkg_fact/2.
-#show condition_holds/2.
-#show imposed_constraint/3.
-#show imposed_constraint/4.
-#show imposed_constraint/5.
-#show imposed_constraint/6.
-#show condition_requirement/3.
-#show condition_requirement/4.
-#show condition_requirement/5.
-#show condition_requirement/6.
-#show node_has_variant/2.
-#show build/1.
-#show external/1.
-#show external_version/3.
-#show trigger_and_effect/3.
-#show unification_set/2.
-#show provider/2.
-#show condition_nodes/3.
-#show trigger_node/3.
-#show imposed_nodes/3.
-
 % debug
diff --git a/lib/spack/spack/solver/error_messages.lp b/lib/spack/spack/solver/error_messages.lp
deleted file mode 100644
index 7eb383860d8c7523c77c4f554db2391410c53f04..0000000000000000000000000000000000000000
--- a/lib/spack/spack/solver/error_messages.lp
+++ /dev/null
@@ -1,239 +0,0 @@
-% Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
-% Spack Project Developers. See the top-level COPYRIGHT file for details.
-%
-% SPDX-License-Identifier: (Apache-2.0 OR MIT)
-
-%=============================================================================
-% This logic program adds detailed error messages to Spack's concretizer
-%=============================================================================
-
-#program error_messages.
-
-% Create a causal tree between trigger conditions by locating the effect conditions
-% that are triggers for another condition. Condition2 is caused by Condition1
-condition_cause(Condition2, ID2, Condition1, ID1) :-
-  condition_holds(Condition2, node(ID2, Package2)),
-  pkg_fact(Package2, condition_trigger(Condition2, Trigger)),
-  condition_requirement(Trigger, Name, Package),
-  condition_nodes(Trigger, TriggerNode, node(ID, Package)),
-  trigger_node(Trigger, TriggerNode, node(ID2, Package2)),
-  attr(Name, node(ID, Package)),
-  condition_holds(Condition1, node(ID1, Package1)),
-  pkg_fact(Package1, condition_effect(Condition1, Effect)),
-  imposed_constraint(Effect, Name, Package),
-  imposed_nodes(Effect, node(ID1, Package1), node(ID, Package)).
-
-condition_cause(Condition2, ID2, Condition1, ID1) :-
-  condition_holds(Condition2, node(ID2, Package2)),
-  pkg_fact(Package2, condition_trigger(Condition2, Trigger)),
-  condition_requirement(Trigger, Name, Package, A1),
-  condition_nodes(Trigger, TriggerNode, node(ID, Package)),
-  trigger_node(Trigger, TriggerNode, node(ID2, Package2)),
-  attr(Name, node(ID, Package), A1),
-  condition_holds(Condition1, node(ID1, Package1)),
-  pkg_fact(Package1, condition_effect(Condition1, Effect)),
-  imposed_constraint(Effect, Name, Package, A1),
-  imposed_nodes(Effect, node(ID1, Package1), node(ID, Package)).
-
-condition_cause(Condition2, ID2, Condition1, ID1) :-
-  condition_holds(Condition2, node(ID2, Package2)),
-  pkg_fact(Package2, condition_trigger(Condition2, Trigger)),
-  condition_requirement(Trigger, Name, Package, A1, A2),
-  condition_nodes(Trigger, TriggerNode, node(ID, Package)),
-  trigger_node(Trigger, TriggerNode, node(ID2, Package2)),
-  attr(Name, node(ID, Package), A1, A2),
-  condition_holds(Condition1, node(ID1, Package1)),
-  pkg_fact(Package1, condition_effect(Condition1, Effect)),
-  imposed_constraint(Effect, Name, Package, A1, A2),
-  imposed_nodes(Effect, node(ID1, Package1), node(ID, Package)).
-
-condition_cause(Condition2, ID2, Condition1, ID1) :-
-  condition_holds(Condition2, node(ID2, Package2)),
-  pkg_fact(Package2, condition_trigger(Condition2, Trigger)),
-  condition_requirement(Trigger, Name, Package, A1, A2, A3),
-  condition_nodes(Trigger, TriggerNode, node(ID, Package)),
-  trigger_node(Trigger, TriggerNode, node(ID2, Package2)),
-  attr(Name, node(ID, Package), A1, A2, A3),
-  condition_holds(Condition1, node(ID1, Package1)),
-  pkg_fact(Package1, condition_effect(Condition1, Effect)),
-  imposed_constraint(Effect, Name, Package, A1, A2, A3),
-  imposed_nodes(Effect, node(ID1, Package1), node(ID, Package)).
-
-% special condition cause for dependency conditions
-% we can't simply impose the existence of the node for dependency conditions
-% because we need to allow for the choice of which dupe ID the node gets
-condition_cause(Condition2, ID2, Condition1, ID1) :-
-  condition_holds(Condition2, node(ID2, Package2)),
-  pkg_fact(Package2, condition_trigger(Condition2, Trigger)),
-  condition_requirement(Trigger, "node", Package),
-  condition_nodes(Trigger, TriggerNode, node(ID, Package)),
-  trigger_node(Trigger, TriggerNode, node(ID2, Package2)),
-  attr("node", node(ID, Package)),
-  condition_holds(Condition1, node(ID1, Package1)),
-  pkg_fact(Package1, condition_effect(Condition1, Effect)),
-  imposed_constraint(Effect, "dependency_holds", Parent, Package, Type),
-  imposed_nodes(Effect, node(ID1, Package1), node(ID, Package)),
-  attr("depends_on", node(X, Parent), node(ID, Package), Type).
-
-% The literal startcauses is used to separate the variables that are part of the error from the
-% ones describing the causal tree of the error. After startcauses, each successive pair must be
-% a condition and a condition_set id for which it holds.
-
-% More specific error message if the version cannot satisfy some constraint
-% Otherwise covered by `no_version_error` and `versions_conflict_error`.
-error(1, "Cannot satisfy '{0}@{1}'", Package, Constraint, startcauses, ConstraintCause, CauseID)
-  :- attr("node_version_satisfies", node(ID, Package), Constraint),
-     pkg_fact(TriggerPkg, condition_effect(ConstraintCause, EffectID)),
-     imposed_constraint(EffectID, "node_version_satisfies", Package, Constraint),
-     condition_holds(ConstraintCause, node(CauseID, TriggerPkg)),
-     attr("version", node(ID, Package), Version),
-     not pkg_fact(Package, version_satisfies(Constraint, Version)).
-
-error(0, "Cannot satisfy '{0}@{1}' and '{0}@{2}", Package, Constraint1, Constraint2, startcauses, Cause1, C1ID, Cause2, C2ID)
-  :- attr("node_version_satisfies", node(ID, Package), Constraint1),
-     pkg_fact(TriggerPkg1, condition_effect(Cause1, EffectID1)),
-     imposed_constraint(EffectID1, "node_version_satisfies", Package, Constraint1),
-     condition_holds(Cause1, node(C1ID, TriggerPkg1)),
-     % two constraints
-     attr("node_version_satisfies", node(ID, Package), Constraint2),
-     pkg_fact(TriggerPkg2, condition_effect(Cause2, EffectID2)),
-     imposed_constraint(EffectID2, "node_version_satisfies", Package, Constraint2),
-     condition_holds(Cause2, node(C2ID, TriggerPkg2)),
-     % version chosen
-     attr("version", node(ID, Package), Version),
-     % version satisfies one but not the other
-     pkg_fact(Package, version_satisfies(Constraint1, Version)),
-     not pkg_fact(Package, version_satisfies(Constraint2, Version)).
-
-% causation tracking error for no or multiple virtual providers
-error(0, "Cannot find a valid provider for virtual {0}", Virtual, startcauses, Cause, CID)
-  :- attr("virtual_node", node(X, Virtual)),
-     not provider(_, node(X, Virtual)),
-     imposed_constraint(EID, "dependency_holds", Parent, Virtual, Type),
-     pkg_fact(TriggerPkg, condition_effect(Cause, EID)),
-     condition_holds(Cause, node(CID, TriggerPkg)).
-
-
-% At most one variant value for single-valued variants
-error(0, "'{0}' required multiple values for single-valued variant '{1}'\n    Requested 'Spec({1}={2})' and 'Spec({1}={3})'", Package, Variant, Value1, Value2, startcauses, Cause1, X, Cause2, X)
-  :- attr("node", node(X, Package)),
-     node_has_variant(node(X, Package), Variant),
-     pkg_fact(Package, variant_single_value(Variant)),
-     build(node(X, Package)),
-     attr("variant_value", node(X, Package), Variant, Value1),
-     imposed_constraint(EID1, "variant_set", Package, Variant, Value1),
-     pkg_fact(TriggerPkg1, condition_effect(Cause1, EID1)),
-     condition_holds(Cause1, node(X, TriggerPkg1)),
-     attr("variant_value", node(X, Package), Variant, Value2),
-     imposed_constraint(EID2, "variant_set", Package, Variant, Value2),
-     pkg_fact(TriggerPkg2, condition_effect(Cause2, EID2)),
-     condition_holds(Cause2, node(X, TriggerPkg2)),
-     Value1 < Value2. % see[1] in concretize.lp
-
-% Externals have to specify external conditions
-error(0, "Attempted to use external for {0} which does not satisfy any configured external spec version", Package, startcauses, ExternalCause, CID)
-  :- external(node(ID, Package)),
-     attr("external_spec_selected", node(ID, Package), Index),
-     imposed_constraint(EID, "external_conditions_hold", Package, Index),
-     pkg_fact(TriggerPkg, condition_effect(ExternalCause, EID)),
-     condition_holds(ExternalCause, node(CID, TriggerPkg)),
-     not external_version(node(ID, Package), _, _).
-
-error(0, "Attempted to build package {0} which is not buildable and does not have a satisfying external\n        attr('{1}', '{2}') is an external constraint for {0} which was not satisfied", Package, Name, A1)
-  :- external(node(ID, Package)),
-     not attr("external_conditions_hold", node(ID, Package), _),
-     imposed_constraint(EID, "external_conditions_hold", Package, _),
-     trigger_and_effect(Package, TID, EID),
-     condition_requirement(TID, Name, A1),
-     not attr(Name, node(_, A1)).
-
-error(0, "Attempted to build package {0} which is not buildable and does not have a satisfying external\n        attr('{1}', '{2}', '{3}') is an external constraint for {0} which was not satisfied", Package, Name, A1, A2)
-  :- external(node(ID, Package)),
-     not attr("external_conditions_hold", node(ID, Package), _),
-     imposed_constraint(EID, "external_conditions_hold", Package, _),
-     trigger_and_effect(Package, TID, EID),
-     condition_requirement(TID, Name, A1, A2),
-     not attr(Name, node(_, A1), A2).
-
-error(0, "Attempted to build package {0} which is not buildable and does not have a satisfying external\n        attr('{1}', '{2}', '{3}', '{4}') is an external constraint for {0} which was not satisfied", Package, Name, A1, A2, A3)
-  :- external(node(ID, Package)),
-     not attr("external_conditions_hold", node(ID, Package), _),
-     imposed_constraint(EID, "external_conditions_hold", Package, _),
-     trigger_and_effect(Package, TID, EID),
-     condition_requirement(TID, Name, A1, A2, A3),
-     not attr(Name, node(_, A1), A2, A3).
-
-error(0, "Attempted to build package {0} which is not buildable and does not have a satisfying external\n        'Spec({0} {1}={2})' is an external constraint for {0} which was not satisfied\n        'Spec({0} {1}={3})' required", Package, Variant, Value, OtherValue, startcauses, OtherValueCause, CID)
-  :- external(node(ID, Package)),
-     not attr("external_conditions_hold", node(ID, Package), _),
-     imposed_constraint(EID, "external_conditions_hold", Package, _),
-     trigger_and_effect(Package, TID, EID),
-     condition_requirement(TID, "variant_value", Package, Variant, Value),
-     not attr("variant_value", node(ID, Package), Variant, Value),
-     attr("variant_value", node(ID, Package), Variant, OtherValue),
-     imposed_constraint(EID2, "variant_set", Package, Variant, OtherValue),
-     pkg_fact(TriggerPkg, condition_effect(OtherValueCause, EID2)),
-     condition_holds(OtherValueCause, node(CID, TriggerPkg)).
-
-error(0, "Attempted to build package {0} which is not buildable and does not have a satisfying external\n        attr('{1}', '{2}', '{3}', '{4}', '{5}') is an external constraint for {0} which was not satisfied", Package, Name, A1, A2, A3, A4)
-  :- external(node(ID, Package)),
-     not attr("external_conditions_hold", node(ID, Package), _),
-     imposed_constraint(EID, "external_conditions_hold", Package, _),
-     trigger_and_effect(Package, TID, EID),
-     condition_requirement(TID, Name, A1, A2, A3, A4),
-     not attr(Name, node(_, A1), A2, A3, A4).
-
-% error message with causes for conflicts
-error(0, Msg, startcauses, TriggerID, ID1, ConstraintID, ID2)
-  :- attr("node", node(ID, Package)),
-     pkg_fact(Package, conflict(TriggerID, ConstraintID, Msg)),
-     % node(ID1, TriggerPackage) is node(ID2, Package) in most, but not all, cases
-     condition_holds(TriggerID, node(ID1, TriggerPackage)),
-     condition_holds(ConstraintID, node(ID2, Package)),
-     unification_set(X, node(ID2, Package)),
-     unification_set(X, node(ID1, TriggerPackage)),
-     not external(node(ID, Package)),  % ignore conflicts for externals
-     not attr("hash", node(ID, Package), _).  % ignore conflicts for installed packages
-
-% variables to show
-#show error/2.
-#show error/3.
-#show error/4.
-#show error/5.
-#show error/6.
-#show error/7.
-#show error/8.
-#show error/9.
-#show error/10.
-#show error/11.
-
-#show condition_cause/4.
-#show condition_reason/2.
-
-% Define all variables used to avoid warnings at runtime when the model doesn't happen to have one
-#defined error/2.
-#defined error/3.
-#defined error/4.
-#defined error/5.
-#defined error/6.
-#defined attr/2.
-#defined attr/3.
-#defined attr/4.
-#defined attr/5.
-#defined pkg_fact/2.
-#defined imposed_constraint/3.
-#defined imposed_constraint/4.
-#defined imposed_constraint/5.
-#defined imposed_constraint/6.
-#defined condition_requirement/3.
-#defined condition_requirement/4.
-#defined condition_requirement/5.
-#defined condition_requirement/6.
-#defined condition_holds/2.
-#defined unification_set/2.
-#defined external/1.
-#defined trigger_and_effect/3.
-#defined build/1.
-#defined node_has_variant/2.
-#defined provider/2.
-#defined external_version/3.
diff --git a/lib/spack/spack/solver/heuristic.lp b/lib/spack/spack/solver/heuristic.lp
index cc87207047d4386ea444b7908d6929c76c380f30..242a6e7c5ee81e6b8ff5bf06f9df3a4a371e62b5 100644
--- a/lib/spack/spack/solver/heuristic.lp
+++ b/lib/spack/spack/solver/heuristic.lp
@@ -11,6 +11,10 @@
 %-----------------
 % Domain heuristic
 %-----------------
+#heuristic attr("hash", node(0, Package), Hash) : literal(_, "root", Package). [45, init]
+#heuristic attr("root", node(0, Package)) : literal(_, "root", Package). [45, true]
+#heuristic attr("node", node(0, Package)) : literal(_, "root", Package). [45, true]
+#heuristic attr("node", node(0, Package)) : literal(_, "node", Package). [45, true]
 
 % Root node
 #heuristic attr("version", node(0, Package), Version) : pkg_fact(Package, version_declared(Version, 0)), attr("root", node(0, Package)). [35, true]
@@ -22,3 +26,4 @@
 
 % Providers
 #heuristic attr("node", node(0, Package)) : default_provider_preference(Virtual, Package, 0), possible_in_link_run(Package). [30, true]
+