Repository: cancervariants/metakb
Pull request #479: feat!: update models to VA-Spec 1.0.0-ballot.2025-03.4
Author: login: korikuzma
Labels: name: enhancement
description: New feature or request
name: priority:high
description: High priority
name: stale
Created: 2025-04-22T16:14:02Z | Merged: 2025-04-28T17:07:34Z

close #462 

* Updated dependencies to use the latest GKS models + resolve dependency conflicts
* Evidence should use `Statement` and Assertions should use the AAC 2017 `StudyStatement`s

Linked issue #462: Update to VA-Spec 1.0.0-ballot.2025-03.4
[1.0.0-ballot.2025-03.4](https://github.com/ga4gh/va-spec/releases/tag/1.0.0-ballot.2025-03.4)
Linked issue comments: user: login: github-actions[bot]
created_at: 2025-04-28T17:08:09Z
body: Closed by #479.
author_association: NONE

Diff:
diff --git a/.gitignore b/.gitignore
index a26c5a3b..d21c7a73 100644
--- a/.gitignore
+++ b/.gitignore
@@ -152,4 +152,4 @@ server/src/metakb/data/*
 *.log
 *.pyc
 
-**/node_modules/
\ No newline at end of file
+**/node_modules/
diff --git a/server/Pipfile b/server/Pipfile
index b86d7176..af59ae33 100644
--- a/server/Pipfile
+++ b/server/Pipfile
@@ -4,11 +4,13 @@ verify_ssl = true
 name = "pypi"
 
 [packages]
-"ga4gh.vrs" = "==2.0.0a13"
-gene-normalizer = {version = "~=0.7.0", extras = ["etl"]}
-variation-normalizer = "~=0.12.1"
-disease-normalizer = {version = "~=0.8.0", extras = ["etl"]}
-thera-py = {version = "~=0.8.0", extras = ["etl"]}
+"ga4gh.vrs" = "==2.*"
+"ga4gh.cat_vrs" = "~=0.6.0"
+"ga4gh.va_spec" = "~=0.3.0"
+gene-normalizer = {version = "~=0.10.0", extras = ["etl"]}
+variation-normalizer = "~=0.14.1"
+disease-normalizer = {version = "~=0.11.0", extras = ["etl"]}
+thera-py = {version = "~=0.10.0", extras = ["etl"]}
 civicpy = "~=3.1"
 requests = "*"
 pydantic = "==2.*"
diff --git a/server/pyproject.toml b/server/pyproject.toml
index 5cf0b370..c6c4a580 100644
--- a/server/pyproject.toml
+++ b/server/pyproject.toml
@@ -23,13 +23,13 @@ requires-python = ">=3.11"
 description = "A search interface for cancer variant interpretations assembled by aggregating and harmonizing across multiple cancer variant interpretation knowledgebases."
 license = {file = "LICENSE"}
 dependencies = [
-    "ga4gh.vrs==2.0.0a14",
-    "ga4gh.cat_vrs~=0.5.0",
-    "ga4gh.va_spec==0.2.0",
-    "gene-normalizer[etl]~=0.8.0",
-    "variation-normalizer~=0.13.0",
-    "disease-normalizer[etl]~=0.9.0",
-    "thera-py[etl]~=0.9.0",
+    "ga4gh.vrs==2.*",
+    "ga4gh.cat_vrs~=0.6.0",
+    "ga4gh.va_spec~=0.3.0",
+    "gene-normalizer[etl]~=0.10.0",
+    "variation-normalizer~=0.14.1",
+    "disease-normalizer[etl]~=0.11.0",
+    "thera-py[etl]~=0.10.0",
     "civicpy~=3.1",
     "requests",
     "pydantic==2.*",
diff --git a/server/src/metakb/database.py b/server/src/metakb/database.py
index 6b194586..a95cda3a 100644
--- a/server/src/metakb/database.py
+++ b/server/src/metakb/database.py
@@ -73,7 +73,7 @@ def _get_credentials(
 
 
 _CONSTRAINTS = {
-    "strength_constraint": "CREATE CONSTRAINT coding_constraint IF NOT EXISTS FOR (n:Strength) REQUIRE (n.name, n.primaryCode) IS UNIQUE;",
+    "strength_constraint": "CREATE CONSTRAINT coding_constraint IF NOT EXISTS FOR (n:Strength) REQUIRE n.primaryCoding IS UNIQUE;",
     "gene_id_constraint": "CREATE CONSTRAINT gene_id_constraint IF NOT EXISTS FOR (n:Gene) REQUIRE n.id IS UNIQUE;",
     "disease_id_constraint": "CREATE CONSTRAINT disease_id_constraint IF NOT EXISTS FOR (n:Disease) REQUIRE n.id IS UNIQUE;",
     "therapy_id_constraint": "CREATE CONSTRAINT therapy_id_constraint IF NOT EXISTS FOR (n:Therapy) REQUIRE n.id IS UNIQUE;",
@@ -84,7 +84,7 @@ def _get_credentials(
     "document_id_constraint": "CREATE CONSTRAINT document_id_constraint IF NOT EXISTS FOR (n:Document) REQUIRE n.id IS UNIQUE;",
     "statement_id_constraint": "CREATE CONSTRAINT statement_id_constraint IF NOT EXISTS FOR (n:Statement) REQUIRE n.id IS UNIQUE;",
     "method_id_constraint": "CREATE CONSTRAINT method_id_constraint IF NOT EXISTS FOR (n:Method) REQUIRE n.id IS UNIQUE;",
-    "classification_constraint": "CREATE CONSTRAINT classification_constraint IF NOT EXISTS FOR (n:Classification) REQUIRE n.primaryCode IS UNIQUE;",
+    "classification_constraint": "CREATE CONSTRAINT classification_constraint IF NOT EXISTS FOR (n:Classification) REQUIRE n.primaryCoding IS UNIQUE;",
     "evidence_line_id_constraint": "CREATE CONSTRAINT evidence_line_id_constraint IF NOT EXISTS FOR (n:EvidenceLine) REQUIRE n.id IS UNIQUE;",
 }
 
diff --git a/server/src/metakb/load_data.py b/server/src/metakb/load_data.py
index 427383c8..d49c69be 100644
--- a/server/src/metakb/load_data.py
+++ b/server/src/metakb/load_data.py
@@ -5,10 +5,11 @@
 import uuid
 from pathlib import Path
 
+from ga4gh.va_spec.base import MembershipOperator
 from neo4j import Driver, ManagedTransaction
 
 from metakb.database import get_driver
-from metakb.transformers.base import NormalizerExtensionName, TherapyType
+from metakb.transformers.base import NormalizerExtensionName
 
 _logger = logging.getLogger(__name__)
 
@@ -82,13 +83,7 @@ def _add_method(tx: ManagedTransaction, method: dict, ids_to_load: set[str]) ->
     if method["id"] not in ids_to_load:
         return
 
-    m_keys = [_create_parameterized_query(method, ("id", "name"))]
-
-    method_subtype = method.get("subtype")
-    if method_subtype:
-        method["subtype"] = json.dumps(method_subtype)
-        m_keys.append("subtype:$subtype")
-
+    m_keys = [_create_parameterized_query(method, ("id", "name", "methodType"))]
     m_keys = ", ".join(m_keys)
 
     query = f"""
@@ -158,35 +153,34 @@ def _add_therapy_or_group(
 
     therapy = therapy_in.copy()
     concept_type = therapy.get("conceptType")
-    group_type = therapy.get("groupType", {}).get("name")
+    membership_op = therapy.get("membershipOperator")
 
     if concept_type:
         _add_therapy(tx, therapy)
-    elif group_type in TherapyType.__members__.values():
-        therapy["groupType"] = group_type
-        keys = [_create_parameterized_query(therapy, ("id", "groupType"))]
+    elif membership_op in MembershipOperator.__members__.values():
+        keys = [_create_parameterized_query(therapy, ("id", "membershipOperator"))]
 
         _add_mappings_and_exts_to_obj(therapy, keys)
         keys = ", ".join(keys)
 
-        query = f"MERGE (tg:{group_type}:Therapy {{ {keys} }})"
+        query = f"MERGE (tg:TherapyGroup:Therapy {{ {keys} }})"
         tx.run(query, **therapy)
 
         for ta in therapy["therapies"]:
             _add_therapy(tx, ta)
             query = f"""
-            MERGE (tg:{group_type}:Therapy {{id: '{therapy['id']}'}})
+            MERGE (tg:TherapyGroup:Therapy {{id: '{therapy['id']}'}})
             MERGE (t:Therapy {{id: '{ta['id']}'}})
             """
 
-            if group_type == TherapyType.COMBINATION_THERAPY:
+            if membership_op == MembershipOperator.AND:
                 query += "MERGE (tg) -[:HAS_COMPONENTS] -> (t)"
             else:
                 query += "MERGE (tg) -[:HAS_SUBSTITUTES] -> (t)"
 
             tx.run(query)
     else:
-        msg = f"Therapy `conceptType` not provided and invalid `groupType` provided: {group_type}"
+        msg = f"Therapy `conceptType` not provided and invalid `membershipOperator` provided: {membership_op}"
         raise TypeError(msg)
 
 
@@ -473,17 +467,41 @@ def _add_obj_id_to_set(obj: dict, ids_set: set[str]) -> None:
     return new_ids_to_load
 
 
-def _get_statement_query(statement: dict) -> str:
+def _get_statement_query(statement: dict, is_evidence: bool) -> str:
     """Generate the initial Cypher query to create a statement node and its
     relationships, based on shared properties of evidence and assertion records.
 
     :param statement: Statement record
+    :param is_evidence: Whether or not ``statement`` is an evidence or assertion record
     :return: The base Cypher query string for creating the statement node and
     relationships
     """
     match_line = ""
     rel_line = ""
 
+    strength = statement.get("strength")
+    if strength:
+        strength_prefix = "strength_"
+        if strength.get("name"):
+            strength_keys = [
+                _create_parameterized_query(
+                    strength, ("name",), entity_param_prefix=strength_prefix
+                )
+            ]
+            statement[f"{strength_prefix}name"] = strength["name"]
+        else:
+            strength_keys = []
+
+        for k in ("primaryCoding", "mappings"):
+            v = strength.get(k)
+            if v:
+                statement[f"{strength_prefix}{k}"] = json.dumps(v)
+                strength_keys.append(f"{k}:${strength_prefix}{k}")
+        strength_keys = ", ".join(strength_keys)
+
+        match_line += f"MERGE (strength:Strength {{ {strength_keys} }})"
+        rel_line += "MERGE (s) -[:HAS_STRENGTH] -> (strength)"
+
     proposition = statement["proposition"]
     statement["propositionType"] = proposition["type"]
     match_line += "SET s.propositionType=$propositionType\n"
@@ -528,8 +546,9 @@ def _get_statement_query(statement: dict) -> str:
         statement, ("id", "description", "direction", "type")
     )
 
+    statement_type = "Statement" if is_evidence else "StudyStatement:Statement"
     return f"""
-    MERGE (s:{statement['type']}:StudyStatement {{ {statement_keys} }})
+    MERGE (s:{statement_type} {{ {statement_keys} }})
     {match_line}
     {rel_line}\n
     """
@@ -547,7 +566,7 @@ def _add_statement_evidence(
         return
 
     statement = statement_in.copy()
-    query = _get_statement_query(statement)
+    query = _get_statement_query(statement, is_evidence=True)
 
     is_reported_in_docs = statement.get("reportedIn", [])
     for ri_doc in is_reported_in_docs:
@@ -557,31 +576,6 @@ def _add_statement_evidence(
         MERGE ({name} {{ id: '{ri_doc_id}'}})
         MERGE (s) -[:IS_REPORTED_IN] -> ({name})
         """
-
-    strength = statement.get("strength")
-    if strength:
-        strength_key_fields = ("primaryCode", "name")
-
-        strength_keys = [
-            _create_parameterized_query(
-                strength, strength_key_fields, entity_param_prefix="strength_"
-            )
-        ]
-        for k in strength_key_fields:
-            v = strength.get(k)
-            if v:
-                statement[f"strength_{k}"] = v
-
-        mappings = strength.get("mappings", [])
-        if mappings:
-            statement["strength_mappings"] = json.dumps(mappings)
-            strength_keys.append("mappings:$strength_mappings")
-        strength_keys = ", ".join(strength_keys)
-
-        query += f"""
-        MERGE (strength:Strength {{ {strength_keys} }})
-        MERGE (s) -[:HAS_STRENGTH] -> (strength)
-        """
     tx.run(query, **statement)
 
 
@@ -597,15 +591,15 @@ def _add_statement_assertion(
         return
 
     statement = statement_in.copy()
-    query = _get_statement_query(statement)
+    query = _get_statement_query(statement, is_evidence=False)
 
     classification = statement["classification"]
-    classification_keys = [
-        _create_parameterized_query(
-            classification, ("primaryCode",), entity_param_prefix="classification_"
-        )
-    ]
-    statement["classification_primaryCode"] = classification["primaryCode"]
+    classification_keys = []
+    primary_coding = classification.get("primaryCoding")
+    if primary_coding:
+        statement["classification_primaryCoding"] = json.dumps(primary_coding)
+        classification_keys.append("primaryCoding:$classification_primaryCoding")
+
     _add_mappings_and_exts_to_obj(classification, classification_keys)
     statement.update(classification)
     classification_keys = ", ".join(classification_keys)
diff --git a/server/src/metakb/normalizers.py b/server/src/metakb/normalizers.py
index 8a701a54..2c636c2f 100644
--- a/server/src/metakb/normalizers.py
+++ b/server/src/metakb/normalizers.py
@@ -6,7 +6,7 @@
 from enum import Enum
 
 from botocore.exceptions import TokenRetrievalError
-from disease.cli import update_db as update_disease_db
+from disease.cli import update as update_disease_db
 from disease.database import create_db as create_disease_db
 from disease.database.database import AWS_ENV_VAR_NAME as DISEASE_AWS_ENV_VAR_NAME
 from disease.query import QueryHandler as DiseaseQueryHandler
@@ -17,7 +17,7 @@
     CopyNumberChange,
     CopyNumberCount,
 )
-from gene.cli import update_normalizer_db as update_gene_db
+from gene.cli import update as update_gene_db
 from gene.database import create_db as create_gene_db
 from gene.database.database import AWS_ENV_VAR_NAME as GENE_AWS_ENV_VAR_NAME
 from gene.query import QueryHandler as GeneQueryHandler
@@ -249,7 +249,9 @@ def _normalize_concept(
             )
         else:
             if normalizer_resp.match_type:
-                normalized_id = getattr(normalizer_resp, concept_name).primaryCode.root
+                normalized_id = getattr(normalizer_resp, concept_name).id.split(
+                    f"normalize.{concept_name}."
+                )[-1]
 
         return normalizer_resp, normalized_id
 
diff --git a/server/src/metakb/query.py b/server/src/metakb/query.py
index 1e440cbd..4125aedf 100644
--- a/server/src/metakb/query.py
+++ b/server/src/metakb/query.py
@@ -12,7 +12,15 @@
     VariantPrognosticStudyStatement,
     VariantTherapeuticResponseStudyStatement,
 )
-from ga4gh.va_spec.base import Direction, Document, EvidenceLine, Method, TherapyGroup
+from ga4gh.va_spec.base import (
+    Direction,
+    Document,
+    EvidenceLine,
+    MembershipOperator,
+    Method,
+    Statement,
+    TherapyGroup,
+)
 from ga4gh.vrs.models import Expression, Variation
 from neo4j import Driver
 from neo4j.graph import Node
@@ -30,7 +38,6 @@
     ServiceMeta,
 )
 from metakb.schemas.app import SourceName
-from metakb.transformers.base import TherapyType
 
 logger = logging.getLogger(__name__)
 
@@ -466,7 +473,8 @@ def _get_statements(
     def _get_nested_stmts(
         self, statement_nodes: list[Node]
     ) -> list[
-        VariantDiagnosticStudyStatement
+        Statement
+        | VariantDiagnosticStudyStatement
         | VariantPrognosticStudyStatement
         | VariantTherapeuticResponseStudyStatement
     ]:
@@ -494,7 +502,8 @@ def _get_nested_stmts(
     def _get_nested_stmt(
         self, stmt_node: Node
     ) -> (
-        VariantDiagnosticStudyStatement
+        Statement
+        | VariantDiagnosticStudyStatement
         | VariantPrognosticStudyStatement
         | VariantTherapeuticResponseStudyStatement
     ):
@@ -523,7 +532,6 @@ def _get_nested_stmt(
         }
         params.update(stmt_node)
         for prop_field in {
-            "propositionType",
             "predicate",
             "alleleOriginQualifier",
             condition_key,
@@ -542,6 +550,8 @@ def _get_nested_stmt(
             query, statement_id=statement_id
         ).records
 
+        has_evidence_lines = False  # this is used to determine evidence vs assertion
+
         for item in nodes_and_rels:
             data = item.data()
             rel_type = data["r_type"]
@@ -560,27 +570,31 @@ def _get_nested_stmt(
                 )
             elif rel_type == "IS_SPECIFIED_BY":
                 node["reportedIn"] = self._get_method_document(node["id"])
-                if "subtype" in node:
-                    node["subtype"] = json.loads(node["subtype"])
                 params["specifiedBy"] = Method(**node)
             elif rel_type == "IS_REPORTED_IN":
                 params["reportedIn"] = [self._get_document(node)]
             elif rel_type == "HAS_STRENGTH":
-                if "mappings" in node:
-                    node["mappings"] = json.loads(node["mappings"])
+                for k in ("mappings", "primaryCoding"):
+                    if k in node:
+                        node[k] = json.loads(node[k])
                 params["strength"] = MappableConcept(**node)
             elif rel_type == "HAS_THERAPEUTIC":
                 params["proposition"]["objectTherapeutic"] = self._get_therapy_or_group(
                     node
                 )
             elif rel_type == "HAS_CLASSIFICATION":
-                params["classification"] = self._get_classification(node)
+                node["primaryCoding"] = json.loads(node["primaryCoding"])
+                params["classification"] = MappableConcept(**node)
             elif rel_type == "HAS_EVIDENCE_LINE":
+                has_evidence_lines = True
                 params["hasEvidenceLines"] = self._get_evidence_lines(statement_id)
-            else:
-                logger.warning("relation type not supported: %s", rel_type)
 
-        return PROP_TYPE_TO_CLASS[prop_type](**params)
+        proposition_type = params.pop("propositionType", None)
+        if has_evidence_lines:  # assertions should use AAC 2017 study statements
+            return PROP_TYPE_TO_CLASS[prop_type](**params)
+
+        params["proposition"]["type"] = proposition_type
+        return Statement(**params)
 
     def _get_disease(self, node: dict) -> MappableConcept:
         """Get disease data from a node with relationship ``HAS_TUMOR_TYPE``
@@ -771,45 +785,25 @@ def _get_therapy_or_group(
         :param node: Therapy node data. This will be mutated.
         :return: Therapy if node type is supported.
         """
-        node_type = node.get("groupType") or node.get("conceptType")
-        if node_type in {
-            TherapyType.COMBINATION_THERAPY,
-            TherapyType.THERAPEUTIC_SUBSTITUTE_GROUP,
-        }:
-            civic_therapy_interaction_type = node.pop(
-                "civic_therapy_interaction_type", None
-            )
-            if civic_therapy_interaction_type:
-                node["extensions"] = [
-                    Extension(
-                        name="civic_therapy_interaction_type",
-                        value=civic_therapy_interaction_type,
-                    )
-                ]
-
+        node_type = node.get("membershipOperator") or node.get("conceptType")
+        if node_type in MembershipOperator.__members__.values():
             moa_therapy_type = node.pop("moa_therapy_type", None)
             if moa_therapy_type:
                 node["extensions"] = [
                     Extension(name="moa_therapy_type", value=moa_therapy_type)
                 ]
 
-            if node_type == TherapyType.COMBINATION_THERAPY:
-                node["therapies"] = self._get_therapies(
-                    node["id"],
-                    TherapyType.COMBINATION_THERAPY,
-                    TherapeuticRelation.HAS_COMPONENTS,
-                )
-            else:
-                node["therapies"] = self._get_therapies(
-                    node["id"],
-                    TherapyType.THERAPEUTIC_SUBSTITUTE_GROUP,
-                    TherapeuticRelation.HAS_SUBSTITUTES,
-                )
-
-            node["groupType"] = MappableConcept(name=node_type)
-
+            tp_relation = (
+                TherapeuticRelation.HAS_COMPONENTS
+                if node_type == MembershipOperator.AND
+                else TherapeuticRelation.HAS_SUBSTITUTES
+            )
+            node["therapies"] = self._get_therapies(
+                node["id"],
+                tp_relation,
+            )
             therapy = TherapyGroup(**node)
-        elif node_type == TherapyType.THERAPY:
+        elif node_type == "Therapy":
             therapy = self._get_therapy(node)
         else:
             logger.warning("node type not supported: %s", node_type)
@@ -817,20 +811,6 @@ def _get_therapy_or_group(
 
         return therapy
 
-    @staticmethod
-    def _get_classification(node: dict) -> MappableConcept:
-        """Get classification data from a node with relationship ``HAS_CLASSIFICATION``
-
-        :param node: CLassification node data. This will be mutated
-        :return: Classification data
-        """
-        civic_amp_level = node.pop("civic_amp_level")
-        if civic_amp_level:
-            node["extensions"] = [
-                Extension(name="civic_amp_level", value=civic_amp_level)
-            ]
-        return MappableConcept(**node)
-
     def _get_evidence_lines(self, statement_id: int) -> list[EvidenceLine]:
         """Get EvidenceLine data from a node with relationship ``HAS_CLASSIFICATION``
 
@@ -840,7 +820,7 @@ def _get_evidence_lines(self, statement_id: int) -> list[EvidenceLine]:
         evidence_lines = []
 
         query = f"""
-        MATCH (s:Statement {{id: '{statement_id}'}}) -[:HAS_EVIDENCE_LINE] -> (el:EvidenceLine)
+        MATCH (s:StudyStatement {{id: '{statement_id}'}}) -[:HAS_EVIDENCE_LINE] -> (el:EvidenceLine)
         OPTIONAL MATCH (el) -[:HAS_EVIDENCE_ITEM] -> (ev:Statement)
         RETURN DISTINCT el, ev
         """
@@ -859,19 +839,17 @@ def _get_evidence_lines(self, statement_id: int) -> list[EvidenceLine]:
     def _get_therapies(
         self,
         tp_id: str,
-        tp_type: TherapyType,
         tp_relation: TherapeuticRelation,
     ) -> list[MappableConcept]:
         """Get list of therapies for therapeutic combination or substitutes group
 
         :param tp_id: ID for combination therapy or therapeutic substitute group
-        :param tp_type: Therapeutic object type
         :param tp_relation: Relationship type for therapies
         :return: List of therapies represented as Mappable Concepts for a combination
             therapy or therapeutic substitute group
         """
         query = f"""
-        MATCH (tp:{tp_type.value} {{ id: $tp_id }}) -[:{tp_relation.value}]
+        MATCH (tp:TherapyGroup {{ id: $tp_id }}) -[:{tp_relation.value}]
             -> (ta:Therapy)
         RETURN ta
         """
diff --git a/server/src/metakb/schemas/api.py b/server/src/metakb/schemas/api.py
index db1b78c1..4ea33bc7 100644
--- a/server/src/metakb/schemas/api.py
+++ b/server/src/metakb/schemas/api.py
@@ -7,6 +7,7 @@
     VariantPrognosticStudyStatement,
     VariantTherapeuticResponseStudyStatement,
 )
+from ga4gh.va_spec.base import Statement
 from pydantic import BaseModel, ConfigDict, StrictStr
 
 from metakb import __version__
@@ -49,7 +50,8 @@ class SearchStatementsService(BaseModel):
     warnings: list[StrictStr] = []
     statement_ids: list[StrictStr] = []
     statements: list[
-        VariantTherapeuticResponseStudyStatement
+        Statement
+        | VariantTherapeuticResponseStudyStatement
         | VariantPrognosticStudyStatement
         | VariantDiagnosticStudyStatement
     ] = []
@@ -76,7 +78,8 @@ class BatchSearchStatementsService(BaseModel):
     warnings: list[StrictStr] = []
     statement_ids: list[StrictStr] = []
     statements: list[
-        VariantTherapeuticResponseStudyStatement
+        Statement
+        | VariantTherapeuticResponseStudyStatement
         | VariantPrognosticStudyStatement
         | VariantDiagnosticStudyStatement
     ] = []
diff --git a/server/src/metakb/transformers/base.py b/server/src/metakb/transformers/base.py
index dff54e1a..7129e2da 100644
--- a/server/src/metakb/transformers/base.py
+++ b/server/src/metakb/transformers/base.py
@@ -23,14 +23,19 @@
     Extension,
     MappableConcept,
     Relation,
-    code,
 )
 from ga4gh.va_spec.aac_2017 import (
     VariantDiagnosticStudyStatement,
     VariantPrognosticStudyStatement,
     VariantTherapeuticResponseStudyStatement,
 )
-from ga4gh.va_spec.base import Document, Method, TherapyGroup
+from ga4gh.va_spec.base import (
+    Document,
+    MembershipOperator,
+    Method,
+    Statement,
+    TherapyGroup,
+)
 from ga4gh.vrs.models import Allele
 from gene.schemas import (
     NamespacePrefix as GeneNamespacePrefix,
@@ -117,14 +122,6 @@ class MoaEvidenceLevel(str, Enum):
     INFERENTIAL = "Inferential evidence"
 
 
-class TherapyType(str, Enum):
-    """Define types for supported therapies"""
-
-    THERAPY = "Therapy"
-    THERAPEUTIC_SUBSTITUTE_GROUP = "TherapeuticSubstituteGroup"
-    COMBINATION_THERAPY = "CombinationTherapy"
-
-
 class ViccConceptVocab(BaseModel):
     """Define VICC Concept Vocab model"""
 
@@ -147,11 +144,9 @@ class _TransformedRecordsCache(BaseModel):
 class TransformedData(BaseModel):
     """Define model for transformed data"""
 
-    statements_evidence: list[
-        VariantTherapeuticResponseStudyStatement
-        | VariantPrognosticStudyStatement
-        | VariantDiagnosticStudyStatement
-    ] = Field([], description="Statement objects for evidence records")
+    statements_evidence: list[Statement] = Field(
+        [], description="Statement objects for evidence records"
+    )
     statements_assertions: list[
         VariantTherapeuticResponseStudyStatement
         | VariantPrognosticStudyStatement
@@ -179,9 +174,7 @@ class Transformer(ABC):
                 doi="10.1186/s13073-019-0687-x",
                 pmid=31779674,
             ),
-            subtype=MappableConcept(
-                primaryCode="variant curation standard operating procedure"
-            ),
+            methodType="variant curation standard operating procedure",
         ),
         Method(
             id=MethodId.MOA_ASSERTION_BIORXIV,
@@ -347,68 +340,27 @@ def extract_harvested_data(self) -> _HarvestedData:
 
     def _evidence_level_to_vicc_concept_mapping(
         self,
-    ) -> dict[MoaEvidenceLevel | CivicEvidenceLevel, MappableConcept]:
+    ) -> dict[MoaEvidenceLevel | CivicEvidenceLevel, list[ConceptMapping]]:
         """Get mapping of source evidence level to vicc concept vocab
 
         :return: Dictionary containing mapping from source evidence level (key)
-            to corresponding vicc concept vocab (value) represented as MappableConcept
-            object
+            to corresponding vicc concept vocab (value) represented as a list of
+            ConceptMapping
         """
-
-        def _get_concept_mapping(exact_mapping: str) -> ConceptMapping:
-            """Get system for an exact mapping
-
-            :param exact_mapping: Exact mapping code
-            :raises NotImplementedError: If SourceName not supported yet
-            :return: Concept mapping object
-            """
-            if isinstance(exact_mapping, EcoLevel):
-                id_ = exact_mapping.value.lower()
-                system = "https://www.evidenceontology.org/term/"
-            elif isinstance(exact_mapping, CivicEvidenceLevel):
-                system = (
-                    "https://civic.readthedocs.io/en/latest/model/evidence/level.html"
-                )
-                id_ = f"{SourceName.CIVIC.value}.evidence_level:{exact_mapping.value}"
-            elif isinstance(exact_mapping, MoaEvidenceLevel):
-                system = "https://moalmanac.org/about"
-                id_ = f"{SourceName.MOA.value}.assertion_level:{'_'.join(exact_mapping.value.lower().replace('-', '_').split())}"
-            else:
-                raise NotImplementedError
-
-            return ConceptMapping(
-                coding=Coding(id=id_, system=system, code=exact_mapping),
-                relation=Relation.EXACT_MATCH,
-            )
-
-        mappings = {}
+        concept_mappings: dict[str, list[ConceptMapping]] = {}
         for item in self._vicc_concept_vocabs:
-            primary_code = item.id.split(":")[-1]
             for exact_mapping in item.exact_mappings:
-                concept_mappings = [
+                concept_mappings[exact_mapping] = [
                     ConceptMapping(
                         coding=Coding(
-                            id=item.id,
                             system="https://go.osu.edu/evidence-codes",
+                            code=item.id.split("vicc:")[-1],
                             name=item.term,
-                            code=code(primary_code),
                         ),
                         relation=Relation.EXACT_MATCH,
                     )
                 ]
-
-                concept_mappings.extend(
-                    _get_concept_mapping(exact_mapping_)
-                    for exact_mapping_ in item.exact_mappings
-                )
-
-                mappings[exact_mapping] = MappableConcept(
-                    name=item.term,
-                    primaryCode=primary_code,
-                    mappings=concept_mappings,
-                )
-
-        return mappings
+        return concept_mappings
 
     @staticmethod
     def _get_digest_for_str_lists(str_list: list[str]) -> str:
@@ -444,13 +396,11 @@ def _get_therapeutic_substitute_group(
         self,
         therapeutic_sub_group_id: str,
         therapies: list[dict],
-        therapy_interaction_type: str,
     ) -> TherapyGroup | None:
         """Get Therapeutic Substitute Group for therapies
 
         :param therapeutic_sub_group_id: ID for Therapeutic Substitute Group
         :param therapies: List of therapy objects
-        :param therapy_interaction_type: Therapy interaction type
         :return: Therapeutic Substitute Group
         """
 
@@ -458,13 +408,13 @@ def _get_combination_therapy(
         self,
         combination_therapy_id: str,
         therapies_in: list[dict],
-        therapy_interaction_type: str,
+        therapy_type: str | None = None,
     ) -> TherapyGroup | None:
         """Get Combination Therapy representation for source therapies
 
         :param combination_therapy_id: ID for Combination Therapy
         :param therapies: List of source therapy objects
-        :param therapy_interaction_type: Therapy type provided by source
+        :param therapy_type: Therapy type provided by source
         :return: Combination Therapy
         """
         therapies = []
@@ -478,33 +428,33 @@ def _get_combination_therapy(
             therapy_mc = self._add_therapy(
                 therapy_id,
                 [therapy],
-                TherapyType.THERAPY,
+                membership_operator=None,
             )
             if not therapy_mc:
                 return None
 
             therapies.append(therapy_mc)
 
-        extensions = [
-            Extension(
-                name="moa_therapy_type"
-                if source_name == SourceName.MOA
-                else "civic_therapy_interaction_type",
-                value=therapy_interaction_type,
-            )
-        ]
+        if source_name == SourceName.MOA:
+            extensions = [
+                Extension(
+                    name=f"{SourceName.MOA.value}_therapy_type", value=therapy_type
+                )
+            ]
+        else:
+            extensions = None
 
         try:
             tg = TherapyGroup(
                 id=combination_therapy_id,
                 therapies=therapies,
                 extensions=extensions,
-                groupType=MappableConcept(name=TherapyType.COMBINATION_THERAPY.value),
+                membershipOperator=MembershipOperator.AND,
             )
         except ValidationError as e:
             # if combination validation checks fail
             logger.debug(
-                "ValidationError raised when attempting to create CombinationTherapy: %s",
+                "ValidationError raised when attempting to create Combination Therapy: %s",
                 e,
             )
             tg = None
@@ -515,36 +465,36 @@ def _add_therapy(
         self,
         therapy_id: str,
         therapies: list[dict],
-        therapy_type: TherapyType,
-        therapy_interaction_type: str | None = None,
+        membership_operator: MembershipOperator | None,
+        therapy_type: str | None = None,
     ) -> MappableConcept | None:
         """Create or get therapy mappable concept given therapies
         First look in ``_cache`` for existing therapy, if not found will attempt to
         transform. Will add ``therapy_id`` to ``therapies`` and ``_cache.therapies``
 
         :param therapy_id: ID for therapy
-        :param therapies: List of therapy objects. If `therapy_type` is
-            `TherapyType.THERAPY`, the list will only contain a single therapy.
-        :param therapy_type: The type of therapy
-        :param therapy_interaction_type: drug interaction type
+        :param therapies: List of therapy objects. If `membership_operator` is `None`,
+            the list will only contain a single therapy.
+        :param membership_operator: The logical relationship between ``therapies``
+        :param therapy_type: Therapy type
         :return: Therapy mappable concept, if ``therapy_type`` is supported
         """
         therapy = self._cache.therapies.get(therapy_id)
         if therapy:
             return therapy
 
-        if therapy_type == TherapyType.THERAPY:
+        if membership_operator is None:
             therapy = self._get_therapy(therapy_id, therapies[0])
-        elif therapy_type == TherapyType.THERAPEUTIC_SUBSTITUTE_GROUP:
-            therapy = self._get_therapeutic_substitute_group(
-                therapy_id, therapies, therapy_interaction_type
-            )
-        elif therapy_type == TherapyType.COMBINATION_THERAPY:
+        elif membership_operator == MembershipOperator.OR:
+            therapy = self._get_therapeutic_substitute_group(therapy_id, therapies)
+        elif membership_operator == MembershipOperator.AND:
             therapy = self._get_combination_therapy(
-                therapy_id, therapies, therapy_interaction_type
+                therapy_id, therapies, therapy_type=therapy_type
             )
         else:
-            logger.debug("Therapy type is not supported: %s", therapy_type)
+            logger.debug(
+                "Membership operator is not supported: %s", membership_operator
+            )
             return None
 
         self._cache.therapies[therapy_id] = therapy
@@ -610,7 +560,15 @@ def _update_mapping(
         is_gene = isinstance(normalizer_resp, NormalizedGene)
         is_therapy = isinstance(normalizer_resp, NormalizedTherapy)
 
-        normalizer_mappings = normalizer_resp_obj.mappings or []
+        normalizer_mappings = [
+            ConceptMapping(
+                coding=normalizer_resp_obj.primaryCoding,
+                relation=Relation.EXACT_MATCH,
+            ),
+        ]
+        if normalizer_resp_obj.mappings:
+            normalizer_mappings.extend(normalizer_resp_obj.mappings)
+
         for mapping in normalizer_mappings:
             if normalized_id == mapping.coding.id:
                 mappings.append(
diff --git a/server/src/metakb/transformers/civic.py b/server/src/metakb/transformers/civic.py
index fe983d75..71d979d5 100644
--- a/server/src/metakb/transformers/civic.py
+++ b/server/src/metakb/transformers/civic.py
@@ -5,6 +5,7 @@
 from collections.abc import Callable
 from enum import Enum
 from pathlib import Path
+from types import MappingProxyType
 from typing import ClassVar
 
 from ga4gh.cat_vrs.models import CategoricalVariant, DefiningAlleleConstraint
@@ -16,6 +17,8 @@
     Relation,
 )
 from ga4gh.va_spec.aac_2017 import (
+    Classification,
+    Strength,
     VariantDiagnosticStudyStatement,
     VariantPrognosticStudyStatement,
     VariantTherapeuticResponseStudyStatement,
@@ -25,7 +28,10 @@
     Direction,
     Document,
     EvidenceLine,
+    MembershipOperator,
     PrognosticPredicate,
+    Statement,
+    System,
     TherapeuticResponsePredicate,
     TherapyGroup,
     VariantDiagnosticProposition,
@@ -43,7 +49,6 @@
 from metakb.transformers.base import (
     CivicEvidenceLevel,
     MethodId,
-    TherapyType,
     Transformer,
     _TransformedRecordsCache,
 )
@@ -112,30 +117,11 @@ class _CivicInteractionType(str, Enum):
     COMBINATION = "COMBINATION"
 
 
-class AmpAscoCapClassification(str, Enum):
-    """Define constraints for CIViC AMP/ASCO/CAP classification
-
-    This follows ClinVar: https://github.com/ncbi/clinvar/blob/master/submission_api_schema/submission_api_schema.json#L510-L514
-    """
-
-    TIER_I = "Tier I - Strong"
-    TIER_II = "Tier II - Potential"
-    TIER_III = "Tier III - Unknown"
-    TIER_IV = "Tier IV - Benign/Likely benign"
-
-
-TIER_TO_AMP_ASCO_CAP = {
-    m.value.split(" -")[0]: m.value
-    for m in AmpAscoCapClassification.__members__.values()
-}
-
-
 class _TherapeuticMetadata(BaseModel):
     """Define model for CIVIC therapeutic metadata"""
 
     therapy_id: str
-    interaction_type: _CivicInteractionType | None
-    therapy_type: TherapyType
+    membership_operator: MembershipOperator | None
     therapies: list[dict]
 
 
@@ -173,6 +159,27 @@ class SourcePrefix(str, Enum):
     ASH = "ASH"
 
 
+class CivicEvidenceName(str, Enum):
+    """Define constraints for CIViC evidence names"""
+
+    VALIDATED_ASSOCIATION = "Validated association"
+    CLINICAL_EVIDENCE = "Clinical evidence"
+    CASE_STUDY = "Case study"
+    PRECLINICAL_EVIDENCE = "Preclinical evidence"
+    INFERENTIAL_ASSOCIATION = "Inferential association"
+
+
+CIVIC_EVIDENCE_LEVEL_TO_NAME = MappingProxyType(
+    {
+        CivicEvidenceLevel.A: CivicEvidenceName.VALIDATED_ASSOCIATION,
+        CivicEvidenceLevel.B: CivicEvidenceName.CLINICAL_EVIDENCE,
+        CivicEvidenceLevel.C: CivicEvidenceName.CASE_STUDY,
+        CivicEvidenceLevel.D: CivicEvidenceName.PRECLINICAL_EVIDENCE,
+        CivicEvidenceLevel.E: CivicEvidenceName.INFERENTIAL_ASSOCIATION,
+    }
+)
+
+
 class _CivicTransformedCache(_TransformedRecordsCache):
     """Create model for caching CIViC data"""
 
@@ -181,9 +188,7 @@ class _CivicTransformedCache(_TransformedRecordsCache):
     evidence: ClassVar[
         dict[
             str,
-            VariantDiagnosticStudyStatement
-            | VariantPrognosticStudyStatement
-            | VariantTherapeuticResponseStudyStatement,
+            Statement,
         ]
     ] = {}
 
@@ -355,16 +360,18 @@ def _add_variant_study_stmt(
                 return
 
             reported_in = [document] if document else None
-            # Get strength
+
             evidence_level = CivicEvidenceLevel[record["evidence_level"]]
-            strength = self.evidence_level_to_vicc_concept_mapping[evidence_level]
+            strength = self._get_eid_strength(evidence_level)
         else:
-            strength = None
             reported_in = None
 
             if record["amp_level"]:
-                classification = self._get_classification(record["amp_level"])
-                if not classification:
+                classification, strength = self._get_aid_classification_and_strength(
+                    record["amp_level"]
+                )
+                if not classification and not strength:
+                    _logger.debug("No classification and/or strength found")
                     return
 
             evidence_lines = []
@@ -404,8 +411,7 @@ def _add_variant_study_stmt(
                 civic_therapeutic = self._add_therapy(
                     therapeutic_metadata.therapy_id,
                     therapeutic_metadata.therapies,
-                    therapeutic_metadata.therapy_type,
-                    therapeutic_metadata.interaction_type,
+                    therapeutic_metadata.membership_operator,
                 )
             if not civic_therapeutic:
                 return
@@ -417,14 +423,8 @@ def _add_variant_study_stmt(
         # Get qualifier
         civic_gene = self._cache.genes.get(variation_gene_map.civic_gene_id)
 
-        variant_origin = record["variant_origin"].upper()
-        if variant_origin == "SOMATIC":
-            allele_origin_qualifier = MappableConcept(name="somatic")
-        elif variant_origin in {"RARE_GERMLINE", "COMMON_GERMLINE"}:
-            allele_origin_qualifier = MappableConcept(name="germline")
-        else:
-            allele_origin_qualifier = None
-
+        variant_origin = record["variant_origin"].lower()
+        allele_origin_qualifier = MappableConcept(name=variant_origin)
         statement_id = record["name"].lower()
         statement_id = (
             statement_id.replace("eid", "civic.eid:")
@@ -439,11 +439,13 @@ def _add_variant_study_stmt(
             "strength": strength,
             "specifiedBy": self.processed_data.methods[0],
             "reportedIn": reported_in,
-            "classification": classification,
             "extensions": extensions or None,
             "hasEvidenceLines": evidence_lines or None,
         }
 
+        if not is_evidence:
+            stmt_params["classification"] = classification
+
         prop_params = {
             "predicate": predicate,
             condition_key: civic_disease,
@@ -457,13 +459,25 @@ def _add_variant_study_stmt(
             stmt_params["proposition"] = VariantTherapeuticResponseProposition(
                 **prop_params
             )
-            statement = VariantTherapeuticResponseStudyStatement(**stmt_params)
+            statement = (
+                Statement(**stmt_params)
+                if is_evidence
+                else VariantTherapeuticResponseStudyStatement(**stmt_params)
+            )
         elif record_type == _CivicEvidenceAssertionType.PROGNOSTIC:
             stmt_params["proposition"] = VariantPrognosticProposition(**prop_params)
-            statement = VariantPrognosticStudyStatement(**stmt_params)
+            statement = (
+                Statement(**stmt_params)
+                if is_evidence
+                else VariantPrognosticStudyStatement(**stmt_params)
+            )
         else:
             stmt_params["proposition"] = VariantDiagnosticProposition(**prop_params)
-            statement = VariantDiagnosticStudyStatement(**stmt_params)
+            statement = (
+                Statement(**stmt_params)
+                if is_evidence
+                else VariantDiagnosticStudyStatement(**stmt_params)
+            )
 
         if is_evidence:
             self._cache.evidence[statement_id] = statement
@@ -471,24 +485,60 @@ def _add_variant_study_stmt(
         else:
             self.processed_data.statements_assertions.append(statement)
 
-    @staticmethod
-    def _get_classification(amp_level: str) -> MappableConcept | None:
-        """Get statement classification
+    def _get_eid_strength(self, evidence_level: CivicEvidenceLevel) -> MappableConcept:
+        """Get CIViC Evidence Item strength
+
+        :param evidence_level: CIViC evidence level
+        :return: Strength for CIViC evidence item
+        """
+        return MappableConcept(
+            name=CIVIC_EVIDENCE_LEVEL_TO_NAME[evidence_level],
+            primaryCoding=Coding(
+                system="https://civic.readthedocs.io/en/latest/model/evidence/level.html",
+                code=evidence_level.value,
+            ),
+            mappings=self.evidence_level_to_vicc_concept_mapping[evidence_level],
+        )
+
+    def _get_aid_classification_and_strength(
+        self,
+        amp_level: str,
+    ) -> tuple[MappableConcept | None, MappableConcept | None]:
+        """Get statement classification and strength
 
         :param amp_level: AMP/ASCO/CAP level
-        :return: Classification represented as a mappable concept
+        :return: Classification and strength, if found
         """
-        if amp_level == "NA":
-            classification = None
-        else:
+        classification = None
+        strength = None
+        system = System.AMP_ASCO_CAP
+
+        if amp_level != "NA":
             pattern = re.compile(r"TIER_(?P<tier>[IV]+)(?:_LEVEL_(?P<level>[A-D]))?")
             match = pattern.match(amp_level).groupdict()
-            primary_code = f"Tier {match['tier']}"
             classification = MappableConcept(
-                primaryCode=TIER_TO_AMP_ASCO_CAP[primary_code],
-                extensions=[Extension(name="civic_amp_level", value=amp_level)],
+                primaryCoding=Coding(
+                    code=Classification(f"Tier {match['tier']}"), system=system
+                ),
+            )
+
+            level = match["level"]
+            evidence_strength = self._get_eid_strength(CivicEvidenceLevel(level))
+            mappings = evidence_strength.mappings
+            evidence_strength.primaryCoding.name = evidence_strength.name
+            mappings.append(
+                ConceptMapping(
+                    coding=evidence_strength.primaryCoding,
+                    relation=Relation.EXACT_MATCH,
+                )
+            )
+
+            strength = MappableConcept(
+                primaryCoding=Coding(code=Strength(f"Level {level}"), system=system),
+                mappings=mappings,
             )
-        return classification
+
+        return classification, strength
 
     def _get_direction(self, direction: str) -> Direction | None:
         """Get the normalized evidence or assertion direction
@@ -1015,43 +1065,28 @@ def _get_therapeutic_substitute_group(
         self,
         therapeutic_sub_group_id: str,
         therapies_in: list[dict],
-        therapy_interaction_type: str,
     ) -> TherapyGroup | None:
         """Get Therapeutic Substitute Group for CIViC therapies
 
         :param therapeutic_sub_group_id: ID for Therapeutic Substitute Group
         :param therapies_in: List of CIViC therapy objects
-        :param therapy_interaction_type: Therapy interaction type provided by CIViC
         :return: Therapeutic Substitute Group
         """
         therapies = []
 
         for therapy in therapies_in:
             therapy_id = f"civic.tid:{therapy['id']}"
-            therapy = self._add_therapy(
-                therapy_id,
-                [therapy],
-                TherapyType.THERAPY,
-            )
+            therapy = self._add_therapy(therapy_id, [therapy], membership_operator=None)
             if not therapy:
                 return None
 
             therapies.append(therapy)
 
-        extensions = [
-            Extension(
-                name="civic_therapy_interaction_type", value=therapy_interaction_type
-            )
-        ]
-
         try:
             tg = TherapyGroup(
+                membershipOperator=MembershipOperator.OR,
                 id=therapeutic_sub_group_id,
-                groupType=MappableConcept(
-                    name=TherapyType.THERAPEUTIC_SUBSTITUTE_GROUP.value
-                ),
                 therapies=therapies,
-                extensions=extensions,
             )
         except ValidationError as e:
             # If substitutes validation checks fail
@@ -1152,8 +1187,7 @@ def _get_therapeutic_metadata(
         if len(therapies) == 1:
             # Add therapy
             therapy_id = f"civic.tid:{therapies[0]['id']}"
-            therapy_interaction_type = None
-            therapy_type = TherapyType.THERAPY
+            membership_operator = None
         else:
             # Add therapy group
             therapy_interaction_type = evidence_item["therapy_interaction_type"]
@@ -1162,10 +1196,10 @@ def _get_therapeutic_metadata(
 
             if therapy_interaction_type == _CivicInteractionType.SUBSTITUTES:
                 therapy_id = f"civic.tsgid:{therapeutic_digest}"
-                therapy_type = TherapyType.THERAPEUTIC_SUBSTITUTE_GROUP
+                membership_operator = MembershipOperator.OR
             elif therapy_interaction_type == _CivicInteractionType.COMBINATION:
                 therapy_id = f"civic.ctid:{therapeutic_digest}"
-                therapy_type = TherapyType.COMBINATION_THERAPY
+                membership_operator = MembershipOperator.AND
             else:
                 _logger.debug(
                     "civic therapy_interaction_type not supported: %s",
@@ -1175,8 +1209,7 @@ def _get_therapeutic_metadata(
 
         return _TherapeuticMetadata(
             therapy_id=therapy_id,
-            interaction_type=therapy_interaction_type,
-            therapy_type=therapy_type,
+            membership_operator=membership_operator,
             therapies=therapies,
         )
 
diff --git a/server/src/metakb/transformers/moa.py b/server/src/metakb/transformers/moa.py
index 8dbc2f8c..4c2aaf72 100644
--- a/server/src/metakb/transformers/moa.py
+++ b/server/src/metakb/transformers/moa.py
@@ -14,14 +14,12 @@
     MappableConcept,
     Relation,
 )
-from ga4gh.va_spec.aac_2017 import (
-    VariantPrognosticStudyStatement,
-    VariantTherapeuticResponseStudyStatement,
-)
 from ga4gh.va_spec.base import (
     Direction,
     Document,
+    MembershipOperator,
     PrognosticPredicate,
+    Statement,
     TherapeuticResponsePredicate,
     TherapyGroup,
     VariantPrognosticProposition,
@@ -37,7 +35,6 @@
 from metakb.transformers.base import (
     MethodId,
     MoaEvidenceLevel,
-    TherapyType,
     Transformer,
     _sanitize_name,
     _TransformedRecordsCache,
@@ -128,8 +125,14 @@ async def _add_variant_study_stmt(self, assertion: dict) -> None:
             .replace("-", "_")
             .upper()
         )
-        moa_evidence_level = MoaEvidenceLevel[predictive_implication]
-        strength = self.evidence_level_to_vicc_concept_mapping[moa_evidence_level]
+        evidence_level = MoaEvidenceLevel[predictive_implication]
+        strength = MappableConcept(
+            primaryCoding=Coding(
+                system="https://moalmanac.org/about",
+                code=evidence_level.value,
+            ),
+            mappings=self.evidence_level_to_vicc_concept_mapping[evidence_level],
+        )
 
         # Add disease
         moa_disease = self._add_disease(assertion["disease"])
@@ -194,7 +197,6 @@ async def _add_variant_study_stmt(self, assertion: dict) -> None:
             stmt_params["proposition"] = VariantTherapeuticResponseProposition(
                 **prop_params
             )
-            statement = VariantTherapeuticResponseStudyStatement(**stmt_params)
         else:
             if assertion["favorable_prognosis"]:
                 predicate = PrognosticPredicate.BETTER_OUTCOME
@@ -207,9 +209,7 @@ async def _add_variant_study_stmt(self, assertion: dict) -> None:
             stmt_params["direction"] = direction
             prop_params["objectCondition"] = moa_disease
             stmt_params["proposition"] = VariantPrognosticProposition(**prop_params)
-            statement = VariantPrognosticStudyStatement(**stmt_params)
-
-        self.processed_data.statements_evidence.append(statement)
+        self.processed_data.statements_evidence.append(Statement(**stmt_params))
 
     async def _add_categorical_variants(self, variants: list[dict]) -> None:
         """Create Categorical Variant objects for all MOA variant records.
@@ -436,17 +436,17 @@ def _get_therapy_or_group(
             logger.debug("%s has no therapy_name", assertion["id"])
             return None
 
-        therapy_interaction_type = therapy["type"]
+        therapy_type = therapy["type"]
 
         if "+" in therapy_name:
             # Indicates multiple therapies
-            if therapy_interaction_type.upper() in {
+            if therapy_type.upper() in {
                 "COMBINATION THERAPY",
                 "IMMUNOTHERAPY",
                 "RADIATION THERAPY",
                 "TARGETED THERAPY",
             }:
-                therapy_type = TherapyType.COMBINATION_THERAPY
+                membership_operator = MembershipOperator.AND
             else:
                 # skipping HORMONE and CHEMOTHERAPY for now
                 return None
@@ -459,26 +459,24 @@ def _get_therapy_or_group(
         else:
             therapy_id = f"moa.therapy:{_sanitize_name(therapy_name)}"
             therapies = [{"name": therapy_name}]
-            therapy_type = TherapyType.THERAPY
+            membership_operator = None
 
         return self._add_therapy(
             therapy_id,
             therapies,
+            membership_operator,
             therapy_type,
-            therapy_interaction_type,
         )
 
     def _get_therapeutic_substitute_group(
         self,
         therapeutic_sub_group_id: str,
         therapies: list[dict],
-        therapy_interaction_type: str,
     ) -> None:
         """MOA does not support therapeutic substitute group
 
         :param therapeutic_sub_group_id: ID for Therapeutic Substitute Group
         :param therapies: List of therapy objects
-        :param therapy_interaction_type: Therapy type provided by MOA
         :return: None, since not supported by MOA
         """
 
diff --git a/server/tests/conftest.py b/server/tests/conftest.py
index dbe28adc..2abe32a3 100644
--- a/server/tests/conftest.py
+++ b/server/tests/conftest.py
@@ -471,26 +471,20 @@ def civic_eid2997_study_stmt(
         "description": "Afatinib, an irreversible inhibitor of the ErbB family of tyrosine kinases has been approved in the US for the first-line treatment of patients with metastatic non-small-cell lung cancer (NSCLC) who have tumours with EGFR exon 19 deletions or exon 21 (L858R) substitution mutations as detected by a US FDA-approved test",
         "direction": "supports",
         "strength": {
-            "primaryCode": "e000001",
-            "name": "authoritative evidence",
+            "name": "Validated association",
+            "primaryCoding": {
+                "system": "https://civic.readthedocs.io/en/latest/model/evidence/level.html",
+                "code": "A",
+            },
             "mappings": [
                 {
                     "coding": {
-                        "id": "vicc:e000001",
                         "system": "https://go.osu.edu/evidence-codes",
-                        "name": "authoritative evidence",
                         "code": "e000001",
+                        "name": "authoritative evidence",
                     },
                     "relation": "exactMatch",
-                },
-                {
-                    "coding": {
-                        "id": "civic.evidence_level:A",
-                        "system": "https://civic.readthedocs.io/en/latest/model/evidence/level.html",
-                        "code": "A",
-                    },
-                    "relation": "exactMatch",
-                },
+                }
             ],
         },
         "proposition": {
@@ -1071,13 +1065,7 @@ def civic_tsg(civic_tid16, civic_tid28):
     return {
         "id": "civic.tsgid:7IxyhCwID0QYyVCP2xuIyYvwwu-S_HrZ",
         "therapies": [civic_tid16, civic_tid28],
-        "groupType": {"name": "TherapeuticSubstituteGroup"},
-        "extensions": [
-            {
-                "name": "civic_therapy_interaction_type",
-                "value": "SUBSTITUTES",
-            }
-        ],
+        "membershipOperator": "OR",
     }
 
 
@@ -1116,13 +1104,7 @@ def civic_ct(civic_tid483, civic_tid16):
     return {
         "id": "civic.ctid:P1PY89shAjemg7jquQ0V9pg1VnYnkPeK",
         "therapies": [civic_tid483, civic_tid16],
-        "groupType": {"name": "CombinationTherapy"},
-        "extensions": [
-            {
-                "name": "civic_therapy_interaction_type",
-                "value": "COMBINATION",
-            }
-        ],
+        "membershipOperator": "AND",
     }
 
 
@@ -1191,26 +1173,20 @@ def civic_eid816_study_stmt(
         "description": "This meta-analysis of 7 randomized control trials evaluating overall survival (OS) (8 for progression free survival) could not definitely state that survival benefit of anti-EGFR monoclonal antibodies is limited to patients with wild type BRAF. In other words, the authors believe that there is insufficient data to justify the exclusion of anti-EGFR monoclonal antibody therapy for patients with mutant BRAF. In these studies, mutant BRAF specifically meant the V600E mutation.",
         "direction": "disputes",
         "strength": {
-            "primaryCode": "e000005",
-            "name": "clinical cohort evidence",
+            "name": "Clinical evidence",
+            "primaryCoding": {
+                "system": "https://civic.readthedocs.io/en/latest/model/evidence/level.html",
+                "code": "B",
+            },
             "mappings": [
                 {
                     "coding": {
-                        "id": "vicc:e000005",
                         "system": "https://go.osu.edu/evidence-codes",
                         "code": "e000005",
                         "name": "clinical cohort evidence",
                     },
                     "relation": "exactMatch",
-                },
-                {
-                    "coding": {
-                        "id": "civic.evidence_level:B",
-                        "system": "https://civic.readthedocs.io/en/latest/model/evidence/level.html",
-                        "code": "B",
-                    },
-                    "relation": "exactMatch",
-                },
+                }
             ],
         },
         "proposition": {
@@ -1250,26 +1226,20 @@ def civic_eid9851_study_stmt(
         "description": "The open-label phase 3 BEACON CRC trial included 665 patients with BRAF V600E-mutated metastatic CRC. Patients were randomly assigned in a 1:1:1 ratio to receive encorafenib, binimetinib, and cetuximab (triplet-therapy group); encorafenib and cetuximab (doublet-therapy group); or the investigators\u2019 choice of either cetuximab and irinotecan or cetuximab and FOLFIRI. The median overall survival was 8.4 months (95% CI, 7.5 to 11.0) in the doublet-therapy group and 5.4 months (95% CI, 4.8 to 6.6) in the control group, with a significantly lower risk of death compared to the control group (hazard ratio for death doublet-group vs. control, 0.60; 95% CI, 0.45 to 0.79; P<0.001). The confirmed response rate was 26% (95% CI, 18 to 35) in the triplet-therapy group, 20% in the doublet-therapy group (95% CI 13 to 29) and 2% (95% CI, 0 to 7) in the control group (doublet group vs. control P<0.001). Median PFS was 4.2 months (95% CI, 3.7 to 5.4) in the doublet-therapy group, and 1.5 months (95% CI, 1.5 to 1.7) in the control group (hazard ratio for disease progression doublet-group vs control, 0.40; 95% CI, 0.31 to 0.52, P<0.001).",
         "direction": "supports",
         "strength": {
-            "primaryCode": "e000001",
-            "name": "authoritative evidence",
+            "name": "Validated association",
+            "primaryCoding": {
+                "system": "https://civic.readthedocs.io/en/latest/model/evidence/level.html",
+                "code": "A",
+            },
             "mappings": [
                 {
                     "coding": {
-                        "id": "vicc:e000001",
                         "system": "https://go.osu.edu/evidence-codes",
                         "code": "e000001",
                         "name": "authoritative evidence",
                     },
                     "relation": "exactMatch",
-                },
-                {
-                    "coding": {
-                        "id": "civic.evidence_level:A",
-                        "system": "https://civic.readthedocs.io/en/latest/model/evidence/level.html",
-                        "code": "A",
-                    },
-                    "relation": "exactMatch",
-                },
+                }
             ],
         },
         "proposition": {
@@ -1338,9 +1308,35 @@ def civic_aid6_statement(
             "conditionQualifier": civic_did8,
         },
         "direction": "supports",
+        "strength": {
+            "primaryCoding": {
+                "system": "AMP/ASCO/CAP (AAC) Guidelines, 2017",
+                "code": "Level A",
+            },
+            "mappings": [
+                {
+                    "coding": {
+                        "system": "https://civic.readthedocs.io/en/latest/model/evidence/level.html",
+                        "code": "A",
+                        "name": "Validated association",
+                    },
+                    "relation": "exactMatch",
+                },
+                {
+                    "coding": {
+                        "system": "https://go.osu.edu/evidence-codes",
+                        "code": "e000001",
+                        "name": "authoritative evidence",
+                    },
+                    "relation": "exactMatch",
+                },
+            ],
+        },
         "classification": {
-            "primaryCode": "Tier I - Strong",
-            "extensions": [{"name": "civic_amp_level", "value": "TIER_I_LEVEL_A"}],
+            "primaryCoding": {
+                "system": "AMP/ASCO/CAP (AAC) Guidelines, 2017",
+                "code": "Tier I",
+            },
         },
         "hasEvidenceLines": [
             {
@@ -1374,26 +1370,20 @@ def civic_eid26_study_stmt(
         "description": "In acute myloid leukemia patients, D816 mutation is associated with earlier relapse and poorer prognosis than wildtype KIT.",
         "direction": "supports",
         "strength": {
-            "primaryCode": "e000005",
-            "name": "clinical cohort evidence",
+            "name": "Clinical evidence",
+            "primaryCoding": {
+                "system": "https://civic.readthedocs.io/en/latest/model/evidence/level.html",
+                "code": "B",
+            },
             "mappings": [
                 {
                     "coding": {
-                        "id": "vicc:e000005",
                         "system": "https://go.osu.edu/evidence-codes",
                         "code": "e000005",
                         "name": "clinical cohort evidence",
                     },
                     "relation": "exactMatch",
-                },
-                {
-                    "coding": {
-                        "id": "civic.evidence_level:B",
-                        "system": "https://civic.readthedocs.io/en/latest/model/evidence/level.html",
-                        "code": "B",
-                    },
-                    "relation": "exactMatch",
-                },
+                }
             ],
         },
         "proposition": {
@@ -1728,34 +1718,19 @@ def moa_aid66_study_stmt(
         "id": "moa.assertion:66",
         "description": "T315I mutant ABL1 in p210 BCR-ABL cells resulted in retained high levels of phosphotyrosine at increasing concentrations of inhibitor STI-571, whereas wildtype appropriately received inhibition.",
         "strength": {
-            "primaryCode": "e000009",
-            "name": "preclinical evidence",
+            "primaryCoding": {
+                "system": "https://moalmanac.org/about",
+                "code": "Preclinical evidence",
+            },
             "mappings": [
                 {
                     "coding": {
-                        "id": "vicc:e000009",
                         "system": "https://go.osu.edu/evidence-codes",
                         "code": "e000009",
                         "name": "preclinical evidence",
                     },
                     "relation": "exactMatch",
                 },
-                {
-                    "coding": {
-                        "id": "civic.evidence_level:D",
-                        "system": "https://civic.readthedocs.io/en/latest/model/evidence/level.html",
-                        "code": "D",
-                    },
-                    "relation": "exactMatch",
-                },
-                {
-                    "coding": {
-                        "id": "moa.assertion_level:preclinical_evidence",
-                        "system": "https://moalmanac.org/about",
-                        "code": "Preclinical evidence",
-                    },
-                    "relation": "exactMatch",
-                },
             ],
         },
         "direction": "supports",
@@ -2122,7 +2097,7 @@ def civic_method():
             "pmid": 31779674,
             "type": "Document",
         },
-        "subtype": {"primaryCode": "variant curation standard operating procedure"},
+        "methodType": "variant curation standard operating procedure",
         "type": "Method",
     }
 
diff --git a/server/tests/unit/database/test_database.py b/server/tests/unit/database/test_database.py
index ac9351ed..8aaf53a9 100644
--- a/server/tests/unit/database/test_database.py
+++ b/server/tests/unit/database/test_database.py
@@ -146,6 +146,31 @@ def _check_function(
     return _check_function
 
 
+@pytest.fixture(scope="module")
+def check_tg_relation_count(driver: Driver):
+    """Check that the quantity of relationships from TherapyGroup to Therapy are within
+    a certain range.
+    """
+
+    def _check_function(relationship: str):
+        query = f"""
+        MATCH (s:TherapyGroup)
+        WHERE NOT EXISTS {{
+            MATCH (s)-[r]-(n)
+            WHERE type(r) <> '{relationship}'
+        }}
+        AND NOT EXISTS {{
+            MATCH (s)-[:{relationship}]->(:Therapy)
+        }}
+        RETURN COUNT(s)
+        """
+        with driver.session() as s:
+            record = s.run(query).single()
+        assert record.values()[0] == 0
+
+    return _check_function
+
+
 @pytest.fixture(scope="module")
 def check_extension_props():
     """Check that node extension properties match expected"""
@@ -182,7 +207,7 @@ def _check_function(
             extension_names = set()
         assert node.keys() == expected_keys
         for k in expected_keys - extension_names:
-            if k == "mappings" or (k == "subtype" and isinstance(fixture[k], dict)):
+            if k == "mappings" or (k == "methodType" and isinstance(fixture[k], dict)):
                 diff = DeepDiff(json.loads(node[k]), fixture[k], ignore_order=True)
                 assert diff == {}, k
             elif k == "normalizer_id":
@@ -409,6 +434,7 @@ def test_therapy_rules(
     check_extension_props,
     civic_ct,
     civic_tsg,
+    check_tg_relation_count,
 ):
     """Verify property and relationship rules for Therapy nodes."""
     check_unique_property("Therapy", "id")
@@ -422,36 +448,14 @@ def test_therapy_rules(
         max_rels=None,
         direction="in",
     )
-    check_relation_count(
-        "CombinationTherapy", "Therapy", "HAS_COMPONENTS", max_rels=None
-    )
-    check_relation_count(
-        "CombinationTherapy",
-        "Statement",
-        "HAS_THERAPEUTIC",
-        max_rels=None,
-        direction="in",
-    )
-    check_relation_count(
-        "TherapeuticSubstituteGroup",
-        "Therapy",
-        "HAS_SUBSTITUTES",
-        max_rels=None,
-    )
-    check_relation_count(
-        "TherapeuticSubstituteGroup",
-        "Statement",
-        "HAS_THERAPEUTIC",
-        max_rels=None,
-        direction="in",
-    )
+    check_tg_relation_count(relationship="HAS_COMPONENTS")
 
-    expected_node_labels = [
-        {"Therapy"},
-        {"Therapy", "CombinationTherapy"},
-        {"Therapy", "TherapeuticSubstituteGroup"},
-    ]
-    check_node_labels("Therapy", expected_node_labels, 3)
+    check_tg_relation_count(relationship="HAS_THERAPEUTIC")
+
+    check_tg_relation_count(relationship="HAS_SUBSTITUTES")
+
+    expected_node_labels = [{"Therapy"}, {"Therapy", "TherapyGroup"}]
+    check_node_labels("Therapy", expected_node_labels, 2)
 
     # Test Therapy
     ta = get_node_by_id(civic_tid146["id"])
@@ -473,17 +477,11 @@ def test_therapy_rules(
 
     # Test CombinationTherapy
     ct = get_node_by_id(civic_ct["id"])
-    check_extension_props(
-        ct, civic_ct["extensions"], {"civic_therapy_interaction_type"}
-    )
-    assert ct["groupType"] == civic_ct["groupType"]["name"]
+    assert ct["membershipOperator"] == civic_ct["membershipOperator"]
 
     # Test TherapeuticSubstituteGroup
     tsg = get_node_by_id(civic_tsg["id"])
-    check_extension_props(
-        tsg, civic_tsg["extensions"], {"civic_therapy_interaction_type"}
-    )
-    assert tsg["groupType"] == civic_tsg["groupType"]["name"]
+    assert tsg["membershipOperator"] == civic_tsg["membershipOperator"]
 
 
 def test_condition_rules(
@@ -540,9 +538,10 @@ def test_statement_rules(
     )
 
     expected_node_labels = [
+        {"Statement"},
         {"Statement", "StudyStatement"},
     ]
-    check_node_labels("Statement", expected_node_labels, 1)
+    check_node_labels("Statement", expected_node_labels, 2)
 
     # Evidence items should have documents
     cite_query = """
@@ -622,7 +621,7 @@ def test_strength_rules(driver: Driver, check_relation_count, civic_eid2997_stud
     """Verify property and relationship rules for Strength nodes."""
     query = """
     MATCH (s:Strength)
-    WITH s.name AS name, s.primaryCode AS primaryCode, COUNT(*) AS count
+    WITH s.name AS name, s.primaryCoding AS primaryCoding, COUNT(*) AS count
     WHERE count > 1
     RETURN COUNT(*)
     """
@@ -655,7 +654,7 @@ def test_strength_rules(driver: Driver, check_relation_count, civic_eid2997_stud
     assert record.values()[0] == 0
 
     query = f"""
-    MATCH (s:Strength {{primaryCode: '{civic_eid2997_study_stmt['strength']['primaryCode']}', name: '{civic_eid2997_study_stmt['strength']['name']}'}})
+    MATCH (s:Strength {{primaryCoding: '{json.dumps(civic_eid2997_study_stmt['strength']['primaryCoding'])}', name: '{civic_eid2997_study_stmt['strength']['name']}'}})
     RETURN s
     """
     result = driver.execute_query(query)
@@ -663,7 +662,8 @@ def test_strength_rules(driver: Driver, check_relation_count, civic_eid2997_stud
     strength_node = result.records[0].data()["s"]
 
     assert strength_node.keys() == civic_eid2997_study_stmt["strength"].keys()
-    strength_node["mappings"] = json.loads(strength_node["mappings"])
+    for k in ("mappings", "primaryCoding"):
+        strength_node[k] = json.loads(strength_node[k])
     assert strength_node == civic_eid2997_study_stmt["strength"]
 
 
@@ -671,7 +671,7 @@ def test_classification_rules(
     driver: Driver, check_unique_property, check_relation_count, civic_aid6_statement
 ):
     """Verify property and relationship rules for Classification nodes."""
-    check_unique_property("Classification", "primaryCode")
+    check_unique_property("Classification", "primaryCoding")
 
     check_relation_count(
         "Classification",
@@ -682,19 +682,21 @@ def test_classification_rules(
         direction="in",
     )
 
-    classification_primary_code = civic_aid6_statement["classification"]["primaryCode"]
+    classification_primary_coding = json.dumps(
+        civic_aid6_statement["classification"]["primaryCoding"]
+    )
     query = f"""
-    MATCH (c:Classification {{primaryCode: '{classification_primary_code}'}})
+    MATCH (c:Classification {{primaryCoding: '{classification_primary_coding}'}})
     RETURN c
     """
     result = driver.execute_query(query)
     assert len(result.records) == 1
     classification_node = result.records[0].data()["c"]
+    classification_node["primaryCoding"] = json.loads(
+        classification_node["primaryCoding"]
+    )
     assert classification_node == {
-        "primaryCode": classification_primary_code,
-        "civic_amp_level": civic_aid6_statement["classification"]["extensions"][0][
-            "value"
-        ],
+        "primaryCoding": civic_aid6_statement["classification"]["primaryCoding"],
     }
 
 
@@ -779,7 +781,7 @@ def test_method_rules(
     check_node_labels("Method", expected_node_labels, 1)
 
     method = get_node_by_id(civic_method["id"])
-    expected_keys = {"id", "name", "subtype"}
+    expected_keys = {"id", "name", "methodType"}
     check_node_props(method, civic_method, expected_keys)
 
 
diff --git a/server/tests/unit/transformers/test_civic_transformer_diagnostic.py b/server/tests/unit/transformers/test_civic_transformer_diagnostic.py
index 6fd9cd19..7ed02139 100644
--- a/server/tests/unit/transformers/test_civic_transformer_diagnostic.py
+++ b/server/tests/unit/transformers/test_civic_transformer_diagnostic.py
@@ -273,26 +273,20 @@ def civic_eid2_study_stmt(civic_method, civic_mpid99, civic_gid38, civic_did2):
         "description": "GIST tumors harboring PDGFRA D842V mutation are more likely to be benign than malignant.",
         "direction": "supports",
         "strength": {
-            "primaryCode": "e000005",
-            "name": "clinical cohort evidence",
+            "name": "Clinical evidence",
+            "primaryCoding": {
+                "system": "https://civic.readthedocs.io/en/latest/model/evidence/level.html",
+                "code": "B",
+            },
             "mappings": [
                 {
                     "coding": {
-                        "id": "vicc:e000005",
                         "system": "https://go.osu.edu/evidence-codes",
                         "code": "e000005",
                         "name": "clinical cohort evidence",
                     },
                     "relation": "exactMatch",
-                },
-                {
-                    "coding": {
-                        "id": "civic.evidence_level:B",
-                        "system": "https://civic.readthedocs.io/en/latest/model/evidence/level.html",
-                        "code": "B",
-                    },
-                    "relation": "exactMatch",
-                },
+                }
             ],
         },
         "proposition": {
@@ -570,26 +564,20 @@ def civic_eid74_study_stmt(civic_method, civic_mpid113, civic_gid42, civic_did15
         "description": "In patients with medullary carcinoma, the presence of RET M918T mutation is associated with increased probability of lymph node metastases.",
         "direction": "supports",
         "strength": {
-            "primaryCode": "e000005",
-            "name": "clinical cohort evidence",
+            "name": "Clinical evidence",
+            "primaryCoding": {
+                "system": "https://civic.readthedocs.io/en/latest/model/evidence/level.html",
+                "code": "B",
+            },
             "mappings": [
                 {
                     "coding": {
-                        "id": "vicc:e000005",
                         "system": "https://go.osu.edu/evidence-codes",
                         "code": "e000005",
                         "name": "clinical cohort evidence",
                     },
                     "relation": "exactMatch",
-                },
-                {
-                    "coding": {
-                        "id": "civic.evidence_level:B",
-                        "system": "https://civic.readthedocs.io/en/latest/model/evidence/level.html",
-                        "code": "B",
-                    },
-                    "relation": "exactMatch",
-                },
+                }
             ],
         },
         "proposition": {
diff --git a/server/tests/unit/transformers/test_civic_transformer_therapeutic.py b/server/tests/unit/transformers/test_civic_transformer_therapeutic.py
index 9d3d8aad..e49261d9 100644
--- a/server/tests/unit/transformers/test_civic_transformer_therapeutic.py
+++ b/server/tests/unit/transformers/test_civic_transformer_therapeutic.py
@@ -249,26 +249,20 @@ def civic_not_normalizable_stmt(
         "description": "This is a fake evidence item.",
         "direction": "supports",
         "strength": {
-            "primaryCode": "e000001",
-            "name": "authoritative evidence",
+            "name": "Validated association",
+            "primaryCoding": {
+                "system": "https://civic.readthedocs.io/en/latest/model/evidence/level.html",
+                "code": "A",
+            },
             "mappings": [
                 {
                     "coding": {
-                        "id": "vicc:e000001",
                         "system": "https://go.osu.edu/evidence-codes",
-                        "name": "authoritative evidence",
                         "code": "e000001",
+                        "name": "authoritative evidence",
                     },
                     "relation": "exactMatch",
-                },
-                {
-                    "coding": {
-                        "id": "civic.evidence_level:A",
-                        "system": "https://civic.readthedocs.io/en/latest/model/evidence/level.html",
-                        "code": "A",
-                    },
-                    "relation": "exactMatch",
-                },
+                }
             ],
         },
         "proposition": {
diff --git a/server/tests/unit/transformers/test_moa_transformer_prognostic.py b/server/tests/unit/transformers/test_moa_transformer_prognostic.py
index a4af9046..5ed093f4 100644
--- a/server/tests/unit/transformers/test_moa_transformer_prognostic.py
+++ b/server/tests/unit/transformers/test_moa_transformer_prognostic.py
@@ -217,26 +217,19 @@ def moa_aid141_study_stmt(
         "direction": "disputes",
         "description": "More frequent in Chronic Myelomonocytic Leukemia.",
         "strength": {
-            "primaryCode": "e000007",
-            "name": "observational study evidence",
+            "primaryCoding": {
+                "system": "https://moalmanac.org/about",
+                "code": "Clinical evidence",
+            },
             "mappings": [
                 {
                     "coding": {
-                        "id": "vicc:e000007",
                         "system": "https://go.osu.edu/evidence-codes",
                         "code": "e000007",
                         "name": "observational study evidence",
                     },
                     "relation": "exactMatch",
                 },
-                {
-                    "coding": {
-                        "id": "moa.assertion_level:clinical_evidence",
-                        "system": "https://moalmanac.org/about",
-                        "code": "Clinical evidence",
-                    },
-                    "relation": "exactMatch",
-                },
             ],
         },
         "proposition": {
@@ -392,26 +385,19 @@ def moa_aid532_study_stmt(
         "direction": "supports",
         "description": "The National Comprehensive Cancer Network\u00ae (NCCN\u00ae) highlights SF3B1 E622, Y623, R625, N626, H662, T663, K666, K700E, I704, G740, G742, and D781 missense variants as being associated with a favorable prognosis in patients with myelodysplastic syndromes.",
         "strength": {
-            "primaryCode": "e000003",
-            "name": "professional guideline evidence",
+            "primaryCoding": {
+                "system": "https://moalmanac.org/about",
+                "code": "Guideline",
+            },
             "mappings": [
                 {
                     "coding": {
-                        "id": "vicc:e000003",
                         "system": "https://go.osu.edu/evidence-codes",
                         "code": "e000003",
                         "name": "professional guideline evidence",
                     },
                     "relation": "exactMatch",
                 },
-                {
-                    "coding": {
-                        "id": "moa.assertion_level:guideline",
-                        "system": "https://moalmanac.org/about",
-                        "code": "Guideline",
-                    },
-                    "relation": "exactMatch",
-                },
             ],
         },
         "proposition": {
diff --git a/server/tests/unit/transformers/test_moa_transformer_therapeutic.py b/server/tests/unit/transformers/test_moa_transformer_therapeutic.py
index 3b651f86..cb076b55 100644
--- a/server/tests/unit/transformers/test_moa_transformer_therapeutic.py
+++ b/server/tests/unit/transformers/test_moa_transformer_therapeutic.py
@@ -184,26 +184,19 @@ def moa_aid154_study_stmt(
         "direction": "supports",
         "description": "The U.S. Food and Drug Administration (FDA) granted regular approval to encorafenib in combination with cetuximab for the treatment of adult patients with metastatic colorectal cancer (CRC) with BRAF V600E mutation, as detected by an FDA-approved test, after prior therapy.",
         "strength": {
-            "primaryCode": "e000002",
-            "name": "FDA recognized evidence",
+            "primaryCoding": {
+                "system": "https://moalmanac.org/about",
+                "code": "FDA-Approved",
+            },
             "mappings": [
                 {
                     "coding": {
-                        "id": "vicc:e000002",
                         "system": "https://go.osu.edu/evidence-codes",
                         "code": "e000002",
                         "name": "FDA recognized evidence",
                     },
                     "relation": "exactMatch",
                 },
-                {
-                    "coding": {
-                        "id": "moa.assertion_level:fda_approved",
-                        "system": "https://moalmanac.org/about",
-                        "code": "FDA-Approved",
-                    },
-                    "relation": "exactMatch",
-                },
             ],
         },
         "proposition": {
@@ -211,7 +204,7 @@ def moa_aid154_study_stmt(
             "predicate": "predictsSensitivityTo",
             "subjectVariant": moa_vid144,
             "objectTherapeutic": {
-                "groupType": {"name": "CombinationTherapy"},
+                "membershipOperator": "AND",
                 "id": "moa.ctid:E8RHoiov2ULWPZVlCea5dqttCbNY0IyL",
                 "therapies": [moa_cetuximab, moa_encorafenib],
                 "extensions": [
@@ -389,34 +382,19 @@ def moa_not_normalizable_stmt(
         "id": "moa.assertion:123456789",
         "description": "This is a fake assertion item.",
         "strength": {
-            "primaryCode": "e000009",
-            "name": "preclinical evidence",
+            "primaryCoding": {
+                "system": "https://moalmanac.org/about",
+                "code": "Preclinical evidence",
+            },
             "mappings": [
                 {
                     "coding": {
-                        "id": "vicc:e000009",
                         "system": "https://go.osu.edu/evidence-codes",
                         "code": "e000009",
                         "name": "preclinical evidence",
                     },
                     "relation": "exactMatch",
                 },
-                {
-                    "coding": {
-                        "id": "civic.evidence_level:D",
-                        "system": "https://civic.readthedocs.io/en/latest/model/evidence/level.html",
-                        "code": "D",
-                    },
-                    "relation": "exactMatch",
-                },
-                {
-                    "coding": {
-                        "id": "moa.assertion_level:preclinical_evidence",
-                        "system": "https://moalmanac.org/about",
-                        "code": "Preclinical evidence",
-                    },
-                    "relation": "exactMatch",
-                },
             ],
         },
         "direction": "supports",


Reviews:
user: login: mcannon068nw
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T16:54:17Z
user: login: korikuzma
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T16:56:27Z
user: login: mcannon068nw
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T16:58:47Z
user: login: korikuzma
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T16:59:33Z
user: login: korikuzma
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T17:04:16Z
user: login: mcannon068nw
body: gave this a read through, think everything seems straightforward!
state: APPROVED
author_association: NONE
submitted_at: 2025-04-25T18:27:05Z
user: login: katie-perry
body: lgtm!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T13:51:05Z
user: login: DanielPuthawala
state: APPROVED
author_association: NONE
submitted_at: 2025-04-28T15:20:34Z

Review comments:
diff_hunk: @@ -12,7 +12,15 @@
     VariantPrognosticStudyStatement,
     VariantTherapeuticResponseStudyStatement,
 )
-from ga4gh.va_spec.base import Direction, Document, EvidenceLine, Method, TherapyGroup
+from ga4gh.va_spec.base import (
+    Direction,
+    Document,
+    EvidenceLine,
+    MembershipOperator,
path: server/src/metakb/query.py
user: login: mcannon068nw
body: this is a really interest change. is the concept of "membership" used anywhere else besides Combination Therapies?
created_at: 2025-04-25T16:54:17Z
author_association: NONE
diff_hunk: @@ -12,7 +12,15 @@
     VariantPrognosticStudyStatement,
     VariantTherapeuticResponseStudyStatement,
 )
-from ga4gh.va_spec.base import Direction, Document, EvidenceLine, Method, TherapyGroup
+from ga4gh.va_spec.base import (
+    Direction,
+    Document,
+    EvidenceLine,
+    MembershipOperator,
path: server/src/metakb/query.py
user: login: korikuzma
body: It's used for both TherapyGroups: Combination (MembershipOperator.AND) and Substitution (MembershipOperator.OR) 
created_at: 2025-04-25T16:56:27Z
author_association: MEMBER
diff_hunk: @@ -12,7 +12,15 @@
     VariantPrognosticStudyStatement,
     VariantTherapeuticResponseStudyStatement,
 )
-from ga4gh.va_spec.base import Direction, Document, EvidenceLine, Method, TherapyGroup
+from ga4gh.va_spec.base import (
+    Direction,
+    Document,
+    EvidenceLine,
+    MembershipOperator,
path: server/src/metakb/query.py
user: login: mcannon068nw
body: or rather I guess my question is whats the motivation in va spec for generalizing membership 
created_at: 2025-04-25T16:58:46Z
author_association: NONE
diff_hunk: @@ -12,7 +12,15 @@
     VariantPrognosticStudyStatement,
     VariantTherapeuticResponseStudyStatement,
 )
-from ga4gh.va_spec.base import Direction, Document, EvidenceLine, Method, TherapyGroup
+from ga4gh.va_spec.base import (
+    Direction,
+    Document,
+    EvidenceLine,
+    MembershipOperator,
path: server/src/metakb/query.py
user: login: korikuzma
body: We also use it in Condition Sets
created_at: 2025-04-25T16:59:33Z
author_association: MEMBER
diff_hunk: @@ -12,7 +12,15 @@
     VariantPrognosticStudyStatement,
     VariantTherapeuticResponseStudyStatement,
 )
-from ga4gh.va_spec.base import Direction, Document, EvidenceLine, Method, TherapyGroup
+from ga4gh.va_spec.base import (
+    Direction,
+    Document,
+    EvidenceLine,
+    MembershipOperator,
path: server/src/metakb/query.py
user: login: korikuzma
body: https://github.com/ga4gh/va-spec/issues/252#issuecomment-2631684482
created_at: 2025-04-25T17:04:15Z
author_association: MEMBER

PR comments:
user: login: korikuzma
created_at: 2025-04-24T15:23:38Z
body: Gonna open this now. Cat-VRS / VA-Spec need the API updated
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-04-28T13:43:43Z
body: This PR is stale because it has been open 1 day(s) with no activity. Please review this PR.
author_association: NONE
user: login: DanielPuthawala
created_at: 2025-04-28T15:21:02Z
body: Looks good to me.  I don't see any major read flags for Cat-VRS compatibility or anything.
author_association: NONE